diff --git a/.pql/changelog/ticket_history/2026-06.sql b/.pql/changelog/ticket_history/2026-06.sql index 0d9faf0b..1b115f29 100644 --- a/.pql/changelog/ticket_history/2026-06.sql +++ b/.pql/changelog/ticket_history/2026-06.sql @@ -401,3 +401,5 @@ INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-248', 'status', 'review', 'done', NULL, '2026-06-06 08:31:25', '2026-06-06 08:31:25', '2026-06-06 08:31:25', NULL, '231d1bb74f6f154f7e888760f1c4e3a6', 1) ON CONFLICT(hash) DO NOTHING; INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-233', 'status', 'backlog', 'in_progress', NULL, '2026-06-06 08:33:42', '2026-06-06 08:33:42', '2026-06-06 08:33:42', NULL, '1ce52a9aabbedd3f4164095e1a0a291e', 1) ON CONFLICT(hash) DO NOTHING; INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-233', 'status', 'in_progress', 'done', NULL, '2026-06-06 08:50:12', '2026-06-06 08:50:12', '2026-06-06 08:50:12', NULL, 'ec94ca747fc852db6bf514b4d5079085', 1) ON CONFLICT(hash) DO NOTHING; +INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-233', 'status', 'done', 'in_progress', NULL, '2026-06-06 09:04:03', '2026-06-06 09:04:03', '2026-06-06 09:04:03', NULL, '9dd345ff2fad6f79a7a1ba7f872dc761', 1) ON CONFLICT(hash) DO NOTHING; +INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-233', 'status', 'in_progress', 'done', NULL, '2026-06-06 09:20:02', '2026-06-06 09:20:02', '2026-06-06 09:20:02', NULL, 'bf55d4f34a22b9d6e69563bc3e9fd95a', 1) ON CONFLICT(hash) DO NOTHING; diff --git a/.pql/changelog/tickets/2026-06.sql b/.pql/changelog/tickets/2026-06.sql index 0b94ce57..0d56f4a5 100644 --- a/.pql/changelog/tickets/2026-06.sql +++ b/.pql/changelog/tickets/2026-06.sql @@ -921,3 +921,5 @@ New scope for this ticket: Net: this is now a verify-and-reconcile task, not a build-from-scratch one. Related: T-249, T-208.', 'done', 'medium', NULL, NULL, 'D-6', '2026-06-06 07:21:17', '2026-06-06 08:31:25', NULL, 'd2d8d30c578bce8229924619542d5aa7', 1) ON CONFLICT(id) DO UPDATE SET type=excluded.type, parent_id=excluded.parent_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 (id, type, parent_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-233', 'task', 'T-208', 'Open a file in the diff panel for review via ui.open', 'Follow-up from T-231 (2026-06-03). T-231 added ''clide ui open '' for the ReaderNav-backed readers (tickets/decisions/markdown), which open by subscribing to a MessageBus ''selection''. The DIFF panel is not covered: DiffController (lib/builtin/diff/src/diff_controller.dart) loads the git working-tree diff via the git.diff IPC verb and refreshes on git.changed events; it has no ''selection''/''focus'' bus channel, so ui.open has no entry point to target a file. Note: because the diff panel shows the working tree, an agent''s uncommitted edits already APPEAR there on the next git.changed refresh -- what''s missing is (a) bringing the diff tab to the front and (b) scrolling/filtering to a specific file. Proposed: give the diff view a bus subscription (publisher ''builtin.diff'', channel ''selection''/''focus'', {path}) that reveals its tab and focuses/filters that file''s diff (DiffController.load already accepts a paths filter), then add ''diff'' as a 4th ui.open target. Related gap: pane.focus is advisory-only (pane_commands.dart) so ''bring tab to front'' isn''t truly wired -- the reveal likely needs the same mechanism the readers use to reveal their tab on selection. Depends in spirit on T-232 (CLI args) only if a non-bus path is chosen; the bus path works regardless.', 'in_progress', 'medium', NULL, NULL, 'D-6', '2026-06-03 13:29:59', '2026-06-06 08:33:42', NULL, '41f3cdaedfb436b6102ce6aebbbc0161', 1) ON CONFLICT(id) DO UPDATE SET type=excluded.type, parent_id=excluded.parent_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 (id, type, parent_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-233', 'task', 'T-208', 'Open a file in the diff panel for review via ui.open', 'Follow-up from T-231 (2026-06-03). T-231 added ''clide ui open '' for the ReaderNav-backed readers (tickets/decisions/markdown), which open by subscribing to a MessageBus ''selection''. The DIFF panel is not covered: DiffController (lib/builtin/diff/src/diff_controller.dart) loads the git working-tree diff via the git.diff IPC verb and refreshes on git.changed events; it has no ''selection''/''focus'' bus channel, so ui.open has no entry point to target a file. Note: because the diff panel shows the working tree, an agent''s uncommitted edits already APPEAR there on the next git.changed refresh -- what''s missing is (a) bringing the diff tab to the front and (b) scrolling/filtering to a specific file. Proposed: give the diff view a bus subscription (publisher ''builtin.diff'', channel ''selection''/''focus'', {path}) that reveals its tab and focuses/filters that file''s diff (DiffController.load already accepts a paths filter), then add ''diff'' as a 4th ui.open target. Related gap: pane.focus is advisory-only (pane_commands.dart) so ''bring tab to front'' isn''t truly wired -- the reveal likely needs the same mechanism the readers use to reveal their tab on selection. Depends in spirit on T-232 (CLI args) only if a non-bus path is chosen; the bus path works regardless.', 'done', 'medium', NULL, NULL, 'D-6', '2026-06-03 13:29:59', '2026-06-06 08:50:12', NULL, 'f2e24a5a804b0d3b95bef0be2f8327b7', 1) ON CONFLICT(id) DO UPDATE SET type=excluded.type, parent_id=excluded.parent_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 (id, type, parent_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-233', 'task', 'T-208', 'Open a file in the diff panel for review via ui.open', 'Follow-up from T-231 (2026-06-03). T-231 added ''clide ui open '' for the ReaderNav-backed readers (tickets/decisions/markdown), which open by subscribing to a MessageBus ''selection''. The DIFF panel is not covered: DiffController (lib/builtin/diff/src/diff_controller.dart) loads the git working-tree diff via the git.diff IPC verb and refreshes on git.changed events; it has no ''selection''/''focus'' bus channel, so ui.open has no entry point to target a file. Note: because the diff panel shows the working tree, an agent''s uncommitted edits already APPEAR there on the next git.changed refresh -- what''s missing is (a) bringing the diff tab to the front and (b) scrolling/filtering to a specific file. Proposed: give the diff view a bus subscription (publisher ''builtin.diff'', channel ''selection''/''focus'', {path}) that reveals its tab and focuses/filters that file''s diff (DiffController.load already accepts a paths filter), then add ''diff'' as a 4th ui.open target. Related gap: pane.focus is advisory-only (pane_commands.dart) so ''bring tab to front'' isn''t truly wired -- the reveal likely needs the same mechanism the readers use to reveal their tab on selection. Depends in spirit on T-232 (CLI args) only if a non-bus path is chosen; the bus path works regardless.', 'in_progress', 'medium', NULL, NULL, 'D-6', '2026-06-03 13:29:59', '2026-06-06 09:04:03', NULL, '6458d14b4f6178c4da81afdb70a6eb6b', 1) ON CONFLICT(id) DO UPDATE SET type=excluded.type, parent_id=excluded.parent_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 (id, type, parent_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-233', 'task', 'T-208', 'Open a file in the diff panel for review via ui.open', 'Follow-up from T-231 (2026-06-03). T-231 added ''clide ui open '' for the ReaderNav-backed readers (tickets/decisions/markdown), which open by subscribing to a MessageBus ''selection''. The DIFF panel is not covered: DiffController (lib/builtin/diff/src/diff_controller.dart) loads the git working-tree diff via the git.diff IPC verb and refreshes on git.changed events; it has no ''selection''/''focus'' bus channel, so ui.open has no entry point to target a file. Note: because the diff panel shows the working tree, an agent''s uncommitted edits already APPEAR there on the next git.changed refresh -- what''s missing is (a) bringing the diff tab to the front and (b) scrolling/filtering to a specific file. Proposed: give the diff view a bus subscription (publisher ''builtin.diff'', channel ''selection''/''focus'', {path}) that reveals its tab and focuses/filters that file''s diff (DiffController.load already accepts a paths filter), then add ''diff'' as a 4th ui.open target. Related gap: pane.focus is advisory-only (pane_commands.dart) so ''bring tab to front'' isn''t truly wired -- the reveal likely needs the same mechanism the readers use to reveal their tab on selection. Depends in spirit on T-232 (CLI args) only if a non-bus path is chosen; the bus path works regardless.', 'done', 'medium', NULL, NULL, 'D-6', '2026-06-03 13:29:59', '2026-06-06 09:20:02', NULL, '747c6d2aa0dad416c150621aa01b9d3d', 1) ON CONFLICT(id) DO UPDATE SET type=excluded.type, parent_id=excluded.parent_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 a55e349b..4cf2126e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,8 +18,10 @@ heading, and (b) bumping `pubspec.yaml` `version:` in the same commit. ### Added -- `clide ui open diff ` reveals the diff tab and scrolls to that file's - diff, highlighting its header — the diff-panel arm of `ui open` (T-233). +- `clide ui open diff ` reveals the diff in a split above the Claude + conversation, scrolls to that file and highlights its header — the + diff-panel arm of `ui open`. Workspace tabs other than Claude/editor now + reveal alongside the conversation with a close affordance (T-233). - Image cards in the Claude conversation log: `clide image show [--caption …]` renders an image inline (PNG/JPG/JPEG/GIF/WebP/BMP), clide-owned and display-only (D-78). The path is resolved workspace-relative diff --git a/lib/app.dart b/lib/app.dart index 513f59cc..bff96263 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -958,10 +958,18 @@ class _WorkspaceSlot extends StatelessWidget { final editorTab = tabs.where((t) => t.id == _editorTabId).firstOrNull; final claude = tabs.where((t) => t.id == _claudeTabId).firstOrNull; - final primary = claude ?? active; - final primaryPane = KeyedSubtree(key: _kWorkspacePrimary, child: primary.build(ctx)); + final primaryPane = KeyedSubtree(key: _kWorkspacePrimary, child: (claude ?? active).build(ctx)); - if (!editorOpen || editorTab == null) { + // A non-Claude, non-editor workspace tab being the active one (e.g. + // diff.view revealed by `clide ui open diff`, T-233) shows in the split + // region above Claude — "review alongside the conversation" — with a + // close affordance back to full-Claude. Only when Claude exists below + // it; with no Claude pane the active tab just takes the whole slot, as + // before. The editor keeps its own editorOpen-gated split. + final reveal = (claude != null && active.id != _claudeTabId && active.id != _editorTabId) ? active : null; + final topTab = reveal ?? (editorOpen ? editorTab : null); + + if (topTab == null) { return Container(color: tokens.panelBackground, child: primaryPane); } @@ -971,10 +979,18 @@ class _WorkspaceSlot extends StatelessWidget { child: LayoutBuilder( builder: (ctx, constraints) { final totalHeight = constraints.maxHeight; - final editorHeight = (totalHeight * ratio).clamp(60.0, totalHeight - 60.0); + final topHeight = (totalHeight * ratio).clamp(60.0, totalHeight - 60.0); return Column( children: [ - SizedBox(height: editorHeight, child: editorTab.build(ctx)), + SizedBox( + height: topHeight, + child: reveal != null + ? _RevealedTab( + tab: reveal, + onClose: () => kernel.panels.activateTab(Slots.workspace, _claudeTabId), + ) + : topTab.build(ctx), + ), _EditorDragHandle(arrangement: kernel.arrangement, totalHeight: totalHeight), Expanded(child: primaryPane), ], @@ -987,6 +1003,58 @@ class _WorkspaceSlot extends StatelessWidget { } } +/// A non-Claude workspace tab revealed in the split region above Claude +/// (T-233): a thin chrome header (title + close) over the tab's body, so the +/// user can review it alongside the conversation and dismiss it back to +/// full-Claude. The editor uses its own split path and never renders here. +class _RevealedTab extends StatelessWidget { + const _RevealedTab({required this.tab, required this.onClose}); + + final TabContribution tab; + final VoidCallback onClose; + + @override + Widget build(BuildContext context) { + final tokens = ClideTheme.of(context).surface; + return Column( + children: [ + Container( + height: 28, + padding: const EdgeInsets.only(left: 10, right: 4), + color: tokens.panelHeader, + child: Row( + children: [ + Expanded( + child: ClideText( + _SlotBody._resolveTitle(context, tab), + fontSize: clideFontCaption, + color: tokens.panelHeaderForeground, + maxLines: 1, + ), + ), + Semantics( + button: true, + label: 'Close', + excludeSemantics: true, + onTap: onClose, + child: ClideTappable( + onTap: onClose, + tooltip: 'Close', + builder: (_, hovered, __) => Padding( + padding: const EdgeInsets.all(6), + child: ClideIcon(PhosphorIcons.xMark, size: 12, color: hovered ? tokens.globalForeground : tokens.globalTextMuted), + ), + ), + ), + ], + ), + ), + Expanded(child: tab.build(context)), + ], + ); + } +} + class _EditorDragHandle extends StatefulWidget { const _EditorDragHandle({required this.arrangement, required this.totalHeight}); diff --git a/test/app_test.dart b/test/app_test.dart index 43e257da..ac32a4a7 100644 --- a/test/app_test.dart +++ b/test/app_test.dart @@ -137,6 +137,38 @@ void main() { expect(tester.takeException(), isNull); }); + testWidgets('a non-Claude workspace tab reveals in the split above Claude with a close (T-233)', (tester) async { + registerTabs(); + f.services.panels.contribute(TabContribution(id: 'diff.view', slot: Slots.workspace, title: 'Diff', build: (_) => const Text('DIFF'))); + f.services.panels.activateTab(Slots.workspace, 'diff.view'); + await pumpLayout(tester); + + expect(tester.takeException(), isNull); + // Revealed alongside the conversation: both the diff and Claude render. + expect(find.text('DIFF'), findsOneWidget); + expect(find.text('CLAUDE'), findsOneWidget); + + // The close affordance returns to full-Claude. + await tester.tap(find.bySemanticsLabel('Close')); + await tester.pump(); + expect(f.services.panels.activeTabIn(Slots.workspace), 'claude.primary'); + expect(find.text('DIFF'), findsNothing); + }); + + testWidgets('with no Claude pane, an active workspace tab takes the whole slot (no reveal chrome)', (tester) async { + final p = f.services.panels; + p.contribute(TabContribution(id: 'files.tree', slot: Slots.sidebar, title: 'Files', build: (_) => const Text('SIDEBAR'))); + p.contribute(TabContribution(id: 'diff.view', slot: Slots.workspace, title: 'Diff', build: (_) => const Text('DIFF'))); + p.contribute(TabContribution(id: 'markdown.viewer', slot: Slots.contextPanel, title: 'Preview', build: (_) => const Text('CONTEXT'))); + p.activateTab(Slots.workspace, 'diff.view'); + await pumpLayout(tester); + + expect(tester.takeException(), isNull); + expect(find.text('DIFF'), findsOneWidget); + // No alongside-Claude chrome, since there is no Claude pane to sit over. + expect(find.bySemanticsLabel('Close'), findsNothing); + }); + testWidgets('global intents dispatch through the app-root Actions', (tester) async { await pumpApp(tester); final ctx = tester.element(find.byType(RootLayout));