spawn terminal panes in the open workspace root (T-381)

The shell spawned with Directory.current — $HOME for desktop-entry
launches, and stale after a project switch since the process CWD
never moves. Use the kernel project root, falling back to the
process CWD only when no project is open.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-06-12 01:06:45 +02:00
co-authored by Claude Fable 5
parent 5f9c054420
commit 5d9443d9f7
5 changed files with 45 additions and 1 deletions
@@ -3997,3 +3997,5 @@ INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, chang
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBHD098CV2N73823KX4Z99P4', 'status', 'backlog', 'in_progress', NULL, '2026-06-11 23:01:24', '2026-06-11 23:01:24', '2026-06-11 23:01:24', NULL, '33ef6141e9af3e6957def829e635b138', 2) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM5W6VN98RQM6S22X28', 'status', 'backlog', 'done', NULL, '2026-06-11 23:04:46', '2026-06-11 23:04:46', '2026-06-11 23:04:46', NULL, '5b0e12802a8f9c0b90f8b08e97f85e29', 2) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBHD098CV2N73823KX4Z99P4', 'status', 'in_progress', 'done', NULL, '2026-06-11 23:04:46', '2026-06-11 23:04:46', '2026-06-11 23:04:46', NULL, '5d8d8c7a17da4894510db957e7af3c3f', 2) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBHCP03EJ9CDBGZGRPD19N8W', 'status', 'backlog', 'in_progress', NULL, '2026-06-11 23:05:16', '2026-06-11 23:05:16', '2026-06-11 23:05:16', NULL, '252a8c28446f3c86876ec826fce03987', 2) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBHCP03EJ9CDBGZGRPD19N8W', 'status', 'in_progress', 'done', NULL, '2026-06-11 23:06:26', '2026-06-11 23:06:26', '2026-06-11 23:06:26', NULL, '4e1cc82f703d717f7592890e230caf00', 2) ON CONFLICT(hash) DO NOTHING;
+10
View File
@@ -4089,3 +4089,13 @@ INSERT INTO tickets (record_id, type, parent_record_id, title, description, stat
Fix: write one small ValueStream<T> wrapper (a broadcast stream that replays the latest value to each new subscriber, plus a .value getter) in the kernel; retrofit statusStream, busyStream, and pendingPromptStream in the claude builtin; delete the per-site workarounds it obsoletes. No third-party dep (rxdart) prefer-zero-deps; the wrapper is ~30 LOC.
Acceptance: unit tests for the wrapper (late subscriber gets latest value; no value yet = no synthetic emit unless seeded); T-274 repro covered: subscribing after the init event still yields the status. Closing this should make T-274 fixable in one line at the call site.', 'done', 'high', NULL, NULL, NULL, '2026-06-11 22:01:07', '2026-06-11 23:04:46', NULL, '9cd3d1777cfbe56c117a91aa3f303855', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBHCP03EJ9CDBGZGRPD19N8W', 'bug', '06FBHBGHNEQTAEPGNJKN42C1E8', 'Terminal panes spawn in Directory.current, not the open project root', 'lib/builtin/terminal/src/terminal_pane.dart:69 — the shell spawns with Directory.current as cwd. Launched from a desktop entry, that is $HOME, not the open workspace; after a project switch it is whatever the process started in. SpawnSpec.cwd already exists in the PTY layer.
Fix: pass the active workspace root as the spawn cwd (and re-derive it on project switch for new panes).
Acceptance: test that a terminal pane''s SpawnSpec.cwd equals the workspace root, not Directory.current.', 'in_progress', 'medium', NULL, NULL, NULL, '2026-06-11 21:59:42', '2026-06-11 23:05:16', NULL, '09bb1689e960768202cfe42d30caf567', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBHCP03EJ9CDBGZGRPD19N8W', 'bug', '06FBHBGHNEQTAEPGNJKN42C1E8', 'Terminal panes spawn in Directory.current, not the open project root', 'lib/builtin/terminal/src/terminal_pane.dart:69 — the shell spawns with Directory.current as cwd. Launched from a desktop entry, that is $HOME, not the open workspace; after a project switch it is whatever the process started in. SpawnSpec.cwd already exists in the PTY layer.
Fix: pass the active workspace root as the spawn cwd (and re-derive it on project switch for new panes).
Acceptance: test that a terminal pane''s SpawnSpec.cwd equals the workspace root, not Directory.current.', 'done', 'medium', NULL, NULL, NULL, '2026-06-11 21:59:42', '2026-06-11 23:06:26', NULL, '8cad068c28a7a0cb6772a5fbdcbc34c3', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
+4
View File
@@ -90,6 +90,10 @@ heading, and (b) bumping `pubspec.yaml` `version:` in the same commit.
session state streams now replay their latest value to late subscribers.
(T-274, T-386)
- **New terminal panes open in the project root.** The shell spawned in the
app process's working directory — `$HOME` for desktop launches, and the
wrong repo after a project switch. (T-381)
- **Accepting ExitPlanMode now leaves plan mode in the conversation panel.**
Approving Claude's plan (the ExitPlanMode tool) transitioned the underlying
session out of plan mode, but clide's tracked permission mode didn't follow,
+3 -1
View File
@@ -78,7 +78,9 @@ class _TerminalPaneState extends State<TerminalPane> {
}
final shell = Platform.environment['SHELL'] ?? '/bin/bash';
final cwd = Directory.current.path;
// The open workspace, not Directory.current — a desktop launch starts
// in $HOME and a project switch doesn't move the process CWD (T-381).
final cwd = _kernel?.project.current?.path ?? Directory.current.path;
final response = await ipc.request(
'pane.spawn',
@@ -6,6 +6,8 @@
/// was never sent and the backend PTY + daemon pane leaked.
library;
import 'dart:io';
import 'package:clide/builtin/terminal/src/terminal_pane.dart';
import 'package:clide/clide.dart';
import 'package:flutter/widgets.dart';
@@ -49,6 +51,30 @@ void main() {
expect(closed, ['pane-7']);
});
testWidgets('spawns the shell in the open workspace, not Directory.current (T-381)', (tester) async {
String? spawnedCwd;
fixture.ipc.setConnected(true);
fixture.ipc.stub('pane.spawn', (args) async {
spawnedCwd = args['cwd'] as String?;
return IpcResponse.ok(id: 'r1', data: {'id': 'pane-9', 'pid': 1});
});
// Open a project so the kernel has a workspace root.
final repo = await tester.runAsync(() async {
final dir = fixture.tempDir.createTempSync('repo-');
Directory('${dir.path}/.git').createSync();
return dir;
});
final opened = await tester.runAsync(() => fixture.services.project.open(repo!.path));
expect(opened, isTrue);
await tester.pumpWidget(harness(fixture, const TerminalPane()));
await pumpAsync(tester);
expect(spawnedCwd, repo!.path);
expect(spawnedCwd, isNot(Directory.current.path));
});
testWidgets('disposing before spawn completes sends no close', (tester) async {
final closed = <String>[];
fixture.ipc.setConnected(false); // spawn bails out: no pane id