decide diff view placement: editor-mode above Claude (D-84, closes T-42; impl tracked in T-243)

This commit is contained in:
2026-06-05 17:17:53 +02:00
parent 62c5b88835
commit c2dc22be78
3 changed files with 50 additions and 0 deletions
+15
View File
@@ -233,3 +233,18 @@ ProjectManager sticky-startup logic is already covered by test/kernel/src/projec
ROOT CAUSE FOUND + FIXED (2026-06-05). The strand is NOT nested ClideTappable / intrinsic sizing (that prior hypothesis is disproven: seeding an EMPTY recents list — no rows, no nested tappables — also hangs, while rendering real recent rows seeded correctly passes). Real cause: SettingsStore.set does real file I/O (writeAsString); awaiting settings.set + project.loadRecents INSIDE the testWidgets body runs that I/O in fake-async, which traps the completion callback so the await never returns (a +0 strand only SIGKILL clears). FIX: seed recents via tester.runAsync(() async {...}) (real event loop), then pump in a tight bounded tree (not the shared harness, whose unbounded width is a separate WelcomeView layout hazard). test/builtin/welcome/widget_test.dart now has working recents render + sticky-toggle + open-recent tests; the skip is removed. Harness lesson recorded.', NULL, '2026-06-05 13:08:02', '2026-06-05 13:08:02', '2026-06-05 13:08:02', NULL, 'cc247a7da845b921a5e1864a0cb5c9cc', 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-122', 'status', 'in_progress', 'done', NULL, '2026-06-05 13:08:05', '2026-06-05 13:08:05', '2026-06-05 13:08:05', NULL, '18577271ba7a5602ab1229cc1c97281b', 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-42', 'description', 'Diff view currently contributes to workspace. Spec doesn''t place it. Likely belongs in context panel (viewer) or as an editor overlay. Depends on Q-27.', 'Diff view currently contributes to workspace. Spec doesn''t place it. Likely belongs in context panel (viewer) or as an editor overlay. Depends on Q-27.
DECIDED 2026-06-05 (D-84): diff view = editor-mode surface, inline above Claude (NOT a workspace tab, NOT a context-panel viewer), spawned from the git sidebar. Rationale: a diff is a review+intervention surface needing room for hunk stage/discard and conflict-resolution widgets — too cramped for the ~420px context panel; the middle column above Claude gives space while keeping Claude''s prompt bar fixed (D-47/D-49). This decision ticket is satisfied; the actual re-placement (generalize the editor-mode slot to host a diff, wire git-sidebar spawn, build resolution widgets) is a follow-up implementation story.', NULL, '2026-06-05 15:14:18', '2026-06-05 15:14:18', '2026-06-05 15:14:18', NULL, '03769a838d7ed3117bd939bb365d2588', 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-243', 'description', NULL, 'Implements D-84. Today the diff view is a workspace TAB (lib/builtin/diff/src/extension.dart, slot: Slots.workspace) — remove that. Instead: (1) generalize the editor-mode surface (currently hosts the single inline editor in _WorkspaceSlot, app.dart, gated by arrangement.editorOpen/editorRatio) so it can host EITHER the editor or a diff (mutually exclusive in that slot, like the viewer<->editor swap in D-49); (2) selecting a changed file in the git sidebar opens its diff in that above-Claude surface; (3) the diff keeps Claude''s prompt bar fixed (D-47) and gets a draggable divider like the editor. Acceptance: diff no longer appears as a workspace tab; clicking a file in the git panel opens its diff above Claude; closing it (Ctrl/Cmd+W) returns to Claude full-height; coexists with the editor by swap, not both at once. Hunk stage/discard + conflict-resolution widgets can be a further follow-up. Tests: arrangement state for the diff surface; git-sidebar -> diff open wiring; widget render.', NULL, '2026-06-05 15:17:09', '2026-06-05 15:17:09', '2026-06-05 15:17:09', NULL, '855f8b3cb0ef90acd37ef432e4860431', 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-42', 'status', 'ready', 'done', NULL, '2026-06-05 15:17:09', '2026-06-05 15:17:09', '2026-06-05 15:17:09', NULL, 'e23ca4c4328443cd81fce0a9abad3d9b', 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-50', 'description', 'Non-modal toast notifications for operation feedback (git push succeeded, extension activated, update available, errors). Slide in from bottom-right or top-right, auto-dismiss after timeout, manually dismissable. Queue multiple toasts. Severity levels map to status tokens (success/warning/error/info).', 'Non-modal toast notifications for operation feedback (git push succeeded, extension activated, update available, errors). Slide in from bottom-right or top-right, auto-dismiss after timeout, manually dismissable. Queue multiple toasts. Severity levels map to status tokens (success/warning/error/info).
REFINED 2026-06-05. Build (bottom-right, system + proof emitter):
- kernel ToastService (ChangeNotifier): show(message, {severity, duration}) -> id, dismiss(id), queue of active toasts, per-toast auto-dismiss Timer (default ~4s; errors longer/sticky), cancel on manual dismiss. ToastSeverity {success, warning, error, info}.
- ClideToast widget (custom, NO Material per D-7): severity accent from status tokens (statusSuccess/Warning/Error/info), message text, dismiss affordance (x / Esc-less, click); slide+fade in from bottom-right, auto slide-out.
- ToastOverlay: mounted in the app-root Stack (app.dart) alongside ClidePalette/QuickOpenOverlay, anchored bottom-right; stacks multiple toasts vertically; animates enter/exit.
- a11y: Semantics liveRegion for the message + labelled dismiss; keyboard-dismissable.
- Proof emitter: git push success/failure raises a toast (wire in the git command path). Broad wiring (extension activated, update available, generic errors) = follow-up tickets.
- Tests: ToastService queue + auto-dismiss (fakeAsync timers) + severity mapping; ClideToast render per severity; a11y contract; overlay mount.', NULL, '2026-06-05 15:17:36', '2026-06-05 15:17:36', '2026-06-05 15:17:36', NULL, '800e0d1a19a42a0080643bde37283fd0', 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-50', 'status', 'ready', 'in_progress', NULL, '2026-06-05 15:17:36', '2026-06-05 15:17:36', '2026-06-05 15:17:36', NULL, 'ca87b010b519f2afaf004d0f448b0063', 1) ON CONFLICT(hash) DO NOTHING;
+26
View File
@@ -573,3 +573,29 @@ Next steps:
ProjectManager sticky-startup logic is already covered by test/kernel/src/project_test.dart (14 cases).
ROOT CAUSE FOUND + FIXED (2026-06-05). The strand is NOT nested ClideTappable / intrinsic sizing (that prior hypothesis is disproven: seeding an EMPTY recents list — no rows, no nested tappables — also hangs, while rendering real recent rows seeded correctly passes). Real cause: SettingsStore.set does real file I/O (writeAsString); awaiting settings.set + project.loadRecents INSIDE the testWidgets body runs that I/O in fake-async, which traps the completion callback so the await never returns (a +0 strand only SIGKILL clears). FIX: seed recents via tester.runAsync(() async {...}) (real event loop), then pump in a tight bounded tree (not the shared harness, whose unbounded width is a separate WelcomeView layout hazard). test/builtin/welcome/widget_test.dart now has working recents render + sticky-toggle + open-recent tests; the skip is removed. Harness lesson recorded.', 'done', 'medium', NULL, NULL, NULL, '2026-05-18 09:06:59', '2026-06-05 13:08:05', NULL, 'accb384a88b9bd863b2d0a440a1a2c3d', 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-42', 'task', 'T-5', 'Decide diff view placement', 'Diff view currently contributes to workspace. Spec doesn''t place it. Likely belongs in context panel (viewer) or as an editor overlay. Depends on Q-27.
DECIDED 2026-06-05 (D-84): diff view = editor-mode surface, inline above Claude (NOT a workspace tab, NOT a context-panel viewer), spawned from the git sidebar. Rationale: a diff is a review+intervention surface needing room for hunk stage/discard and conflict-resolution widgets — too cramped for the ~420px context panel; the middle column above Claude gives space while keeping Claude''s prompt bar fixed (D-47/D-49). This decision ticket is satisfied; the actual re-placement (generalize the editor-mode slot to host a diff, wire git-sidebar spawn, build resolution widgets) is a follow-up implementation story.', 'ready', 'low', NULL, NULL, 'D-48', '2026-04-22 21:03:40', '2026-06-05 15:14:18', NULL, '8eee2f6fa4689e0d7c965ae65b73b3a3', 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-243', 'story', 'T-5', 'Move diff view to the editor-mode surface (D-84): spawn from git sidebar, above Claude', NULL, 'backlog', 'medium', NULL, NULL, NULL, '2026-06-05 15:14:25', '2026-06-05 15:14:25', NULL, 'd854264edbb54e71b3a3d3d9f93ba059', 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-243', 'story', 'T-5', 'Move diff view to the editor-mode surface (D-84): spawn from git sidebar, above Claude', 'Implements D-84. Today the diff view is a workspace TAB (lib/builtin/diff/src/extension.dart, slot: Slots.workspace) — remove that. Instead: (1) generalize the editor-mode surface (currently hosts the single inline editor in _WorkspaceSlot, app.dart, gated by arrangement.editorOpen/editorRatio) so it can host EITHER the editor or a diff (mutually exclusive in that slot, like the viewer<->editor swap in D-49); (2) selecting a changed file in the git sidebar opens its diff in that above-Claude surface; (3) the diff keeps Claude''s prompt bar fixed (D-47) and gets a draggable divider like the editor. Acceptance: diff no longer appears as a workspace tab; clicking a file in the git panel opens its diff above Claude; closing it (Ctrl/Cmd+W) returns to Claude full-height; coexists with the editor by swap, not both at once. Hunk stage/discard + conflict-resolution widgets can be a further follow-up. Tests: arrangement state for the diff surface; git-sidebar -> diff open wiring; widget render.', 'backlog', 'medium', NULL, NULL, NULL, '2026-06-05 15:14:25', '2026-06-05 15:17:09', NULL, 'f5b98dc529e06114817586e23b2d5562', 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-42', 'task', 'T-5', 'Decide diff view placement', 'Diff view currently contributes to workspace. Spec doesn''t place it. Likely belongs in context panel (viewer) or as an editor overlay. Depends on Q-27.
DECIDED 2026-06-05 (D-84): diff view = editor-mode surface, inline above Claude (NOT a workspace tab, NOT a context-panel viewer), spawned from the git sidebar. Rationale: a diff is a review+intervention surface needing room for hunk stage/discard and conflict-resolution widgets — too cramped for the ~420px context panel; the middle column above Claude gives space while keeping Claude''s prompt bar fixed (D-47/D-49). This decision ticket is satisfied; the actual re-placement (generalize the editor-mode slot to host a diff, wire git-sidebar spawn, build resolution widgets) is a follow-up implementation story.', 'done', 'low', NULL, NULL, 'D-48', '2026-04-22 21:03:40', '2026-06-05 15:17:09', NULL, '3349f66187c4db86850154af52d8f546', 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-50', 'story', NULL, 'toast notification system', 'Non-modal toast notifications for operation feedback (git push succeeded, extension activated, update available, errors). Slide in from bottom-right or top-right, auto-dismiss after timeout, manually dismissable. Queue multiple toasts. Severity levels map to status tokens (success/warning/error/info).
REFINED 2026-06-05. Build (bottom-right, system + proof emitter):
- kernel ToastService (ChangeNotifier): show(message, {severity, duration}) -> id, dismiss(id), queue of active toasts, per-toast auto-dismiss Timer (default ~4s; errors longer/sticky), cancel on manual dismiss. ToastSeverity {success, warning, error, info}.
- ClideToast widget (custom, NO Material per D-7): severity accent from status tokens (statusSuccess/Warning/Error/info), message text, dismiss affordance (x / Esc-less, click); slide+fade in from bottom-right, auto slide-out.
- ToastOverlay: mounted in the app-root Stack (app.dart) alongside ClidePalette/QuickOpenOverlay, anchored bottom-right; stacks multiple toasts vertically; animates enter/exit.
- a11y: Semantics liveRegion for the message + labelled dismiss; keyboard-dismissable.
- Proof emitter: git push success/failure raises a toast (wire in the git command path). Broad wiring (extension activated, update available, generic errors) = follow-up tickets.
- Tests: ToastService queue + auto-dismiss (fakeAsync timers) + severity mapping; ClideToast render per severity; a11y contract; overlay mount.', 'ready', 'medium', NULL, NULL, NULL, '2026-04-23 20:31:48', '2026-06-05 15:17:36', NULL, '5f5b8ffa2e1f767a2dd594764258e370', 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-50', 'story', NULL, 'toast notification system', 'Non-modal toast notifications for operation feedback (git push succeeded, extension activated, update available, errors). Slide in from bottom-right or top-right, auto-dismiss after timeout, manually dismissable. Queue multiple toasts. Severity levels map to status tokens (success/warning/error/info).
REFINED 2026-06-05. Build (bottom-right, system + proof emitter):
- kernel ToastService (ChangeNotifier): show(message, {severity, duration}) -> id, dismiss(id), queue of active toasts, per-toast auto-dismiss Timer (default ~4s; errors longer/sticky), cancel on manual dismiss. ToastSeverity {success, warning, error, info}.
- ClideToast widget (custom, NO Material per D-7): severity accent from status tokens (statusSuccess/Warning/Error/info), message text, dismiss affordance (x / Esc-less, click); slide+fade in from bottom-right, auto slide-out.
- ToastOverlay: mounted in the app-root Stack (app.dart) alongside ClidePalette/QuickOpenOverlay, anchored bottom-right; stacks multiple toasts vertically; animates enter/exit.
- a11y: Semantics liveRegion for the message + labelled dismiss; keyboard-dismissable.
- Proof emitter: git push success/failure raises a toast (wire in the git command path). Broad wiring (extension activated, update available, generic errors) = follow-up tickets.
- Tests: ToastService queue + auto-dismiss (fakeAsync timers) + severity mapping; ClideToast render per severity; a11y contract; overlay mount.', 'in_progress', 'medium', NULL, NULL, NULL, '2026-04-23 20:31:48', '2026-06-05 15:17:36', NULL, '33d606d624889ed2d075c5cc771dbf23', 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);
+9
View File
@@ -401,4 +401,13 @@ Core, rendering, IPC, kernel, panel manager.
- **Cross-reference:** answers Gap 5 of [`docs/self-analysis.md`](../../docs/self-analysis.md); gates [D-77](#d-77-drive-claude-via-the-stream-json-control-protocol-teams-become-a-clide-owned-coordination-layer)/[D-78](#d-78-claude-permissionprompt-transport-is-the-stdio-control-channel) (the hosted session this names primary), [D-68](#d-68-dual-integration-surface--bash-cli-primary-mcp-secondary) (the external CLI surface kept as secondary), [D-6](#d-6-cli-and-event-surface-contract) (parity scoped here), [D-70](#d-70-ipc-socket-path-is-per-workspace-deterministic) (the socket an external agent reaches). Frames Epic B ([T-214](#)) bootstrap target and Epic C ([T-218](#)) observability scope under initiative [T-208](#) "Give Claude hands". Implemented/decided by T-224.
- **Raised by:** 2026-06-03 — T-224, processing the self-analysis.md dogfood probe from inside clide. The probe itself ran as an external harness (B) while the product is built around the hosted session (A); user chose "both, primary = hosted (A)" so Epic B optimizes the bootstrap clide can actually control while keeping the external driver a supported, best-effort integration.
### D-84: Diff view placement — editor-mode inline above Claude, spawned from the git sidebar
- **Date:** 2026-06-05
- **Status:** accepted
- **Decision:** The diff view is **not** a workspace tab (its current placement violates [D-48](#d-48-chrome-budget-no-tabs-no-breadcrumbs-keyboard-first) "no buffer/workspace tabs"). It opens as an **editor-mode surface** — lifting *above* Claude in the middle column, the same mechanism as the inline editor ([D-49](#d-49-editor-mode-inline-above-claude-viewer-swap)) — and is **spawned from the git sidebar** (selecting a changed file opens its diff there). It is **not** a context-panel viewer.
- **Rationale:** A diff is a review *and intervention* surface, not just passive inspection: to be fully functional it must host hunk-level stage/discard and (later) conflict-resolution widgets, which need horizontal room. The context panel (≈420px, [D-50](#d-50-context-auto-behavior-right-panel-reacts-to-claude) viewer semantics) is too narrow to comfortably hold those controls; the middle column above Claude gives the space while keeping Claude's prompt bar fixed ([D-47](#d-47-interaction-model-claude-is-home-layout)). Spawning from the git sidebar matches the user's mental model (left = change list, the diff opens in the work area). This also partially serves the side-by-side-compare desire behind [Q-27](../questions/architecture.md#q-27-two-editor-split) without a true editor split.
- **Cost / risk:** The editor-mode surface currently hosts the single inline editor; it must generalize to host a diff too (editor and diff are mutually exclusive in that slot, like the viewer↔editor swap in D-49). Implementation is a follow-up story, not part of this decision ticket (T-42).
- **Cross-reference:** [D-48](#d-48-chrome-budget-no-tabs-no-breadcrumbs-keyboard-first) (no workspace tabs — the rule the old placement broke), [D-49](#d-49-editor-mode-inline-above-claude-viewer-swap) (the editor-mode mechanism reused), [D-50](#d-50-context-auto-behavior-right-panel-reacts-to-claude) (the viewer model explicitly rejected here), [D-47](#d-47-interaction-model-claude-is-home-layout) (prompt bar fixed), [Q-27](../questions/architecture.md#q-27-two-editor-split) (partially served). Decides T-42.
- **Raised by:** 2026-06-05 — T-42 (decide diff view placement). User chose editor-mode over the context-panel viewer: a diff needs space for resolution widgets and spawns from the git sidebar, so it's too cramped to compress into the right panel.
---