reveal non-Claude workspace tabs in a split above Claude
A live test of ui open diff surfaced that the workspace slot hardcoded the Claude pane (primary = claude ?? active), so activating diff.view flipped the registry but never rendered. Generalise _WorkspaceSlot: when a non-Claude, non-editor workspace tab is the active one, reveal it in the split region above Claude with a close affordance back to full-Claude — so clide ui open diff actually shows the diff alongside the conversation. Fixes the reveal for terminal/team-chat tabs too. Closes T-233. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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 <reader> <id|path>'' 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 <reader> <id|path>'' 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 <reader> <id|path>'' 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 <reader> <id|path>'' 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);
|
||||
|
||||
+4
-2
@@ -18,8 +18,10 @@ heading, and (b) bumping `pubspec.yaml` `version:` in the same commit.
|
||||
|
||||
### Added
|
||||
|
||||
- `clide ui open diff <path>` 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 <path>` 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 <path>
|
||||
[--caption …]` renders an image inline (PNG/JPG/JPEG/GIF/WebP/BMP),
|
||||
clide-owned and display-only (D-78). The path is resolved workspace-relative
|
||||
|
||||
+73
-5
@@ -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});
|
||||
|
||||
|
||||
@@ -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));
|
||||
|
||||
Reference in New Issue
Block a user