diff --git a/.pql/changelog/ticket_history/2026-06.sql b/.pql/changelog/ticket_history/2026-06.sql index f43d0ab7..ac411854 100644 --- a/.pql/changelog/ticket_history/2026-06.sql +++ b/.pql/changelog/ticket_history/2026-06.sql @@ -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; diff --git a/.pql/changelog/tickets/2026-06.sql b/.pql/changelog/tickets/2026-06.sql index ae2d3037..f905f89b 100644 --- a/.pql/changelog/tickets/2026-06.sql +++ b/.pql/changelog/tickets/2026-06.sql @@ -4089,3 +4089,13 @@ INSERT INTO tickets (record_id, type, parent_record_id, title, description, stat Fix: write one small ValueStream 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); diff --git a/CHANGELOG.md b/CHANGELOG.md index c5a2c435..ca0cf08d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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, diff --git a/lib/builtin/terminal/src/terminal_pane.dart b/lib/builtin/terminal/src/terminal_pane.dart index aaa89c17..0c8b281b 100644 --- a/lib/builtin/terminal/src/terminal_pane.dart +++ b/lib/builtin/terminal/src/terminal_pane.dart @@ -78,7 +78,9 @@ class _TerminalPaneState extends State { } 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', diff --git a/test/builtin/terminal/terminal_pane_test.dart b/test/builtin/terminal/terminal_pane_test.dart index 215bc859..caf9e484 100644 --- a/test/builtin/terminal/terminal_pane_test.dart +++ b/test/builtin/terminal/terminal_pane_test.dart @@ -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 = []; fixture.ipc.setConnected(false); // spawn bails out: no pane id