chore(plan): close T-407 (ex-line overlay; review skipped per user)

User opted to skip the live GUI review; improvement tickets will be filed
if anything needs polish. Completes all built children of T-403; only
T-405 part 2 (gt/gT) remains.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-16 11:57:28 +02:00
co-authored by Claude Opus 4.8
parent e54d5263e0
commit 3a59d5be8a
2 changed files with 72 additions and 0 deletions
@@ -5751,3 +5751,4 @@ NOT yet exercised in the running GUI — recommend a quick live confirm (vim pre
This completes the ex-line. T-405 part 2 (gt/gT) remains the only open T-403 child and still needs the bare-g design decision (see T-405).', NULL, '2026-06-16 09:42:34', '2026-06-16 09:42:34', '2026-06-16 09:42:34', NULL, 'c53afd4a1354012e36081b84dab66bfd', 2) ON CONFLICT(hash) DO NOTHING; This completes the ex-line. T-405 part 2 (gt/gT) remains the only open T-403 child and still needs the bare-g design decision (see T-405).', NULL, '2026-06-16 09:42:34', '2026-06-16 09:42:34', '2026-06-16 09:42:34', NULL, 'c53afd4a1354012e36081b84dab66bfd', 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 ('06FBKPFTC7H5NY0XHBTEGF8XQ4', 'status', 'ready', 'review', NULL, '2026-06-16 09:42:39', '2026-06-16 09:42:39', '2026-06-16 09:42:39', NULL, 'e2cc00b9f1b80080e17e0213079400f1', 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 ('06FBKPFTC7H5NY0XHBTEGF8XQ4', 'status', 'ready', 'review', NULL, '2026-06-16 09:42:39', '2026-06-16 09:42:39', '2026-06-16 09:42:39', NULL, 'e2cc00b9f1b80080e17e0213079400f1', 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 ('06FBKPFTC7H5NY0XHBTEGF8XQ4', 'status', 'review', 'done', NULL, '2026-06-16 09:56:18', '2026-06-16 09:56:18', '2026-06-16 09:56:18', NULL, 'e0ea178ba163bf3395c21793c8cf9a14', 2) ON CONFLICT(hash) DO NOTHING;
+71
View File
@@ -6691,3 +6691,74 @@ Tests: parser/controller/executors (ex_line_test), editor.goto-line daemon tests
NOT yet exercised in the running GUI recommend a quick live confirm (vim preset: :w / :q / :42 / :e / ZZ / a bad command) before closing. NOT yet exercised in the running GUI recommend a quick live confirm (vim preset: :w / :q / :42 / :e / ZZ / a bad command) before closing.
This completes the ex-line. T-405 part 2 (gt/gT) remains the only open T-403 child and still needs the bare-g design decision (see T-405).', 'review', 'low', NULL, NULL, NULL, '2026-06-12 03:22:10', '2026-06-16 09:42:39', NULL, 'e320e4acb9a1a9100e39ebf8199e72d2', 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); This completes the ex-line. T-405 part 2 (gt/gT) remains the only open T-403 child and still needs the bare-g design decision (see T-405).', 'review', 'low', NULL, NULL, NULL, '2026-06-12 03:22:10', '2026-06-16 09:42:39', NULL, 'e320e4acb9a1a9100e39ebf8199e72d2', 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 ('06FBKPFTC7H5NY0XHBTEGF8XQ4', 'task', '06FBKP67X1Y1FEE9T5R0E5DA9C', 'vim ex command-line overlay (:w :q :e :N)', 'The deferred piece vim_mode_service.dart already names: "command-line is surfaced separately as a transient overlay rather than a persistent mode." Minimal ex line, not a vimscript interpreter:
- `:` (shift+semicolon) when vim.normal opens a one-line overlay (reuse the quick-open overlay chrome/widgets; it is NOT a mode Esc dismisses back to normal, no scope-flag churn beyond an exline.open flag for its own enter/escape bindings).
- v1 grammar, one table, no parsing cleverness:
:w editor save (find the editor''s save command id; check editor_commands.dart _save), :q command:editor.close, :wq / :x save then close, :e <text> quickOpen.open pre-seeded with <text> (check QuickOpenIntent for a seed param; add one if absent), :<digits> editor goto-line (editor has a goto? if not, smallest possible addition to editor.vim ops), :<unknown> shake/flash + stay open.
- ZZ ("shift+z shift+z" sequence) save-close, riding the same plumbing include it here, it''s one YAML line once :wq exists.
- Cross-pane angle: the ex line is GLOBAL under vim.normal (works with tree/conversation focused :q closes the focused tab via editor.close fallback to active workspace tab; keep v1 simple: editor-targeted only, document it).
Done when: : opens the overlay from any pane under the vim preset; the v1 table works with widget tests; unknown commands don''t execute anything; ZZ saves+closes.
--- REFINEMENT (2026-06-12, parallel workflow refine-t403-tickets) ---
SHARPENED: Build the transient ex-line overlay vim_mode_service.dart already names as deferred. `:` (shift+semicolon under vim.normal) opens a one-line overlay modeled on the quick-open chrome (lib/widgets/src/quick_open_overlay.dart + lib/kernel/src/quick_open.dart); it is NOT a vim mode an overlay with its own exline.open scope flag for enter/escape, Esc dismissing to normal. v1 is a fixed dispatch table, no parser. GROUNDING FINDINGS that reshape scope: (1) `:q`editor.close exists (default_layout extension _closeEditor) but closes the ENTIRE editor split via arrangement.closeEditor(), NOT a single buffer/tab document this; a true single-tab :q needs new wiring (EditorController.closeBuffer is per-id but not a registry command). (2) There is NO editor.save CommandRegistry command save exists only as an IPC verb (editor.save in lib/src/daemon/editor_commands.dart) and EditorController.save()/the editor''s ctrl+S. So `:w` cannot just dispatch command:editor.save today this ticket must ADD a save command (real work, not one YAML line). (3) :e <text>quick-open seeded with text: QuickOpenController.open() takes NO seed param (verified quick_open.dart:45) add one. (4) :<digits> goto-line: editor_commands.dart supports a `line` arg on editor.open (IPC, lines 79-90) but there''s no registry goto-line for the OPEN buffer smallest addition needed. Keep v1 editor-targeted and document it. ZZ (`shift+z shift+z`) rides the :wq plumbing once save+close exist.
ACCEPTANCE CRITERIA:
- `:` (shift+semicolon) under vim.normal opens a one-line ex overlay reusing quick-open chrome; an exline.open scope flag gates its enter/escape; Esc dismisses to normal with no vim-mode churn.
- Fixed v1 table: :w saves the active buffer, :q closes (documented: closes the editor split via editor.close), :wq/:x save then close, :e <text> opens quick-open seeded with <text>, :<digits> jumps the active buffer to that line.
- :<unknown> executes nothing and flashes/shakes + stays open (no silent command:foo dispatch).
- ZZ (`shift+z shift+z`) under vim.normal saves and closes, sharing the :wq path.
- A save command reachable from the keymap is added (none exists today), and an editor goto-line registry command is added (or the smallest editor.vim op extension).
- QuickOpenController.open() gains a seed/initialQuery parameter and the overlay honors it.
- Widget tests cover overlay open/dismiss + each table row; bindings asserted under the vim preset only; no behavior change under other presets.
FILES: assets/keymaps/vim.yaml (`:` open under vim.normal; exline enter/escape under exline.open; ZZ as `shift+z shift+z`); lib/builtin/vim/src/vim_mode_service.dart (the deferral point; may host overlay open state); lib/builtin/vim/src/extension.dart (register ex-line command(s)/overlay as CommandContributions, like _modeCommand); lib/widgets/src/quick_open_overlay.dart + lib/kernel/src/quick_open.dart (reuse chrome; ADD seed/initialQuery to open()); lib/builtin/default_layout/src/extension.dart (editor.close is here, closes the split for :q; add editor.save/goto-line registry command here or in editor ext); lib/builtin/editor/src/editor_controller.dart (save()/closeBuffer() the per-buffer ops); lib/src/daemon/editor_commands.dart (editor.save / editor.open `line` arg IPC-only reference for goto-line _offsetForLine); NEW lib/builtin/vim/src/ex_line_overlay.dart + tests under test/builtin/vim/.
DEPENDENCIES: Depends on / overlaps T-404 both reference command:editor.close. T-404 settles the bare-ctrl+w vs ctrl+w-prefix ambiguity and exercises editor.close cross-pane; T-407''s :q should reuse whatever close semantics T-404 settles (and surface the split-vs-tab close question). Shares vim.yaml. The `z` prefix (ZZ) is new and collides with nothing; `:` (shift+semicolon) is free. Independent of T-405/T-406 except the common vim.yaml. Best sequenced after T-404 so close semantics are fixed first.
OPEN QUESTIONS:
- :q todayeditor.close closes the whole split (arrangement.closeEditor), not the focused tab acceptable v1, or must :q close only the active buffer (new per-tab close command wrapping EditorController.closeBuffer)? Surprises vim users.
- No save command in CommandRegistry (only IPC editor.save + the editor''s ctrl+S). Confirm the :w mechanism a new CommandContribution reaching the active EditorController.save() vs dispatching the IPC verb and where it lives (editor ext vs vim ext).
- Cross-pane: ZZ/:w/:q only make sense with an editor buffer active. When tree/conversation is focused and no editor is open, should :w/:q no-op, flash, or close the focused workspace tab? Ticket says "editor-targeted only, document it" confirm the no-buffer behavior.
- Should the ex overlay live in the vim builtin (inert under non-vim presets), gated by VimModeService.enabled, matching how mode commands are gated?
- goto-line for the OPEN buffer: editor.open accepts a `line` arg but reopening isn''t right for an already-open buffer add an editor.vim.gotoLine op (vim_edit_ops.dart) or a registry command that sets selection on the active buffer?
--- DECISION: :q / ZZ close semantics (2026-06-12, user) ---
RESOLVED (was the open "split vs tab" question): `:q` closes the ACTIVE TAB, not the whole editor split. After closing it focuses the next editor tab, so repeated `:q` walks the tabs and the LAST `:q` ends up collapsing the split (the "ends up doing editor.close in the end" behavior the user wants).
KEY MECHANISM (verified in code) this falls out of existing wiring, so `:q` should NOT map to command:editor.close at all:
- `:q` EditorController.closeBuffer(activeId) (lib/builtin/editor/src/editor_controller.dart:90) the same per-tab close the tab-strip X already uses (editor_view.dart:306-322 onCloseRequested).
- Server registry close(id) (lib/src/editor/registry.dart:178-187) removes the buffer and, when it was active, re-activates another and emits editor.active-changed; when the LAST buffer closes it emits editor.active-changed{id:null}.
- The editor extension already turns that null-active event into arrangement.closeEditor() (lib/builtin/editor/src/extension.dart:22-45 lib/kernel/src/panels/arrangement.dart:108-112). So the split self-collapses on the final tab no explicit editor.close needed, and command:editor.close (the whole-split close, default_layout extension.dart:244-252) stays the ctrl+w binding only.
THE ONE REAL GAP: registry close() re-focuses `_buffers.values.first` (registry.dart:182), i.e. the FIRST remaining buffer, not the NEXT tab in visual order. Vim `:q` wants focus to move to the tab to the RIGHT of the closed one (else the LEFT if it was last). Two options:
(a) UI-side: before closeBuffer, compute the next tab from _tabs.entries (editor_view.dart) and activate it, then close no protocol change; keeps tab-visual-order knowledge in the view that owns it.
(b) Server-side: teach registry.close() a focus-direction (next-not-first), so the tab-strip X button also gets vim-correct next-focus. Wider blast radius (protocol + all close callers) but fixes the focus order everywhere, not just for :q.
RECOMMEND (a) for the :q scope, and file (b) separately if we want the X button to match. Confirm before building.
ACCEPTANCE CRITERIA (supersede the earlier ":q closes the split" line):
- `:q` closes the active editor tab; focus moves to the next tab (right, else left). With one tab open, `:q` closes it and the editor split collapses (via the existing null-active closeEditor path) no separate editor.close dispatch.
- N tabs open + N `:q` in a row closes them left-to-focus-order and ends with the split collapsed.
- `:wq` / `:x` / `ZZ` save the active buffer then run the same close-active-tab path.
- `:q` with no editor buffer active (tree/conversation focused, editor closed) no-ops or flashes does NOT touch other panes (still an open question below).
STILL OPEN: when no editor buffer is active, does `:q` no-op, flash, or close the focused workspace tab? (Cross-pane angle keep v1 editor-targeted.)
--- DECISION: no-active-buffer behavior (2026-06-13, user) ---
RESOLVED (was the last STILL OPEN question): when no editor buffer is active (tree/conversation focused, editor split closed), `:q` / `:w` / `:wq` / `:x` / `ZZ` NO-OP for v1 they do nothing, touch no other pane, and don''t close the focused workspace tab. (A flash/shake is optional polish, not required.) Keeps v1 strictly editor-targeted; closing non-editor workspace tabs via `:q` is explicitly out of scope and can be revisited later if wanted.
IMPLEMENTED 2026-06-16. Built the ex command-line overlay end to end. Architecture chose the QuickOpen precedent over the ticket''s tentative vim-builtin layout (cleaner, no extension/ctx plumbing):
- ExLineController + parseExCommand grammar + editor-targeted executors in lib/kernel/src/ex_line.dart (controller mirrors QuickOpenController; executors go through editor IPC verbs).
- ExLineOverlay in lib/widgets/src/ex_line_overlay.dart, mounted in the root_shell Stack beside QuickOpenOverlay; publishes the exline.open scope flag; rejected commands flash a red border + "Not an editor command" hint and stay open (no timer test-friendly).
- v1 table: :w (editor.save, active-default), :q (editor.close active id; registry promotes next / collapses split on last, per the 2026-06-12 decision), :wq/:x and ZZ (save+close), :e <path> (quickOpen.open(seed:) new seed param), :N (new editor.goto-line IPC/CLI verb reusing _offsetForLine). No-active-buffer = no-op (2026-06-13 decision). Unknown = flash + stay open.
- Routing: ":" and ZZ are TYPED intents (ExLineOpenIntent/ExLineWriteQuitIntent). The editor''s _dispatchVim and PaneKeyNav now BUBBLE unhandled typed intents to the app-root Actions (small generalization), so they fire whether the editor, a pane, or nothing is focused. editor.vim.* stays swallowed in panes.
- Goto-line needed caret sync: EditorController now handles editor.selection-changed and editor_view moves the caret on a selection-only change (the content-gated branch previously skipped it).
- vim.yaml: ":" -> exline.open (vim.normal); ZZ (shift+z shift+z) -> exline.writeQuit (vim.normal); Esc -> dismiss (exline.open).
Tests: parser/controller/executors (ex_line_test), editor.goto-line daemon tests, editor.selection-changed controller + view tests, full overlay widget test (each row, unknown-hint, dismiss, scope flag). make test green; analyze + format clean. CHANGELOG updated.
NOT yet exercised in the running GUI recommend a quick live confirm (vim preset: :w / :q / :42 / :e / ZZ / a bad command) before closing.
This completes the ex-line. T-405 part 2 (gt/gT) remains the only open T-403 child and still needs the bare-g design decision (see T-405).', 'done', 'low', NULL, NULL, NULL, '2026-06-12 03:22:10', '2026-06-16 09:56:18', NULL, 'aec7002f9cec6777eafbfdf163d1d4c3', 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);