You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Browse filesBrowse the repository at this point in the historyBrowse files
Yogthos
committed
feat(ui): ov2 Phase A — multi-chat snapshot model in Renderer
First milestone of dirge-ov2 (subagent multi-chat-windows + Ctrl-N/P
+ /tasks picker, ported from maki). Pure additive change — single-
chat behavior unchanged, backward-compatible for all current call
sites.
## What
`Renderer` gains:
- `chats: Vec<ChatSnapshot>` — saved state of inactive chats
- `active_chat: usize` — index of the chat currently on screen
- `add_chat(name) -> usize` — append a new chat (typically a
subagent), returns its index
- `switch_chat(idx)` — saves current state to chats[active_chat],
loads chats[idx] into the hot fields
- `active_chat()` / `chat_count()` / `chat_names()` — read accessors
`ChatSnapshot` mirrors the per-chat fields (buffer, partial,
partial_color, scroll_offset, lines, col, selection_active/start/
end). The HOT path is unchanged — the active chat's state lives in
the Renderer's own fields; snapshots only matter at chat-switch
boundaries via `save_active` / `load_active`.
## Why this shape
Alternative considered: make every per-chat field be a Vec<T>
indexed by chat. Rejected — would touch every method in the
renderer and every call site. The save/swap pattern keeps render
hot-paths byte-for-byte identical to the single-chat baseline.
`Renderer::new()` creates one default chat named "main" at index 0.
Code that doesn't call `add_chat` / `switch_chat` sees zero
behavior change.
## Test
`chat_snapshot_save_load_roundtrip` exercises add/switch/restore:
- main chat seeded with content + scroll position
- subagent chat added and switched to (starts empty)
- subagent populated independently
- switch back to main → main's state restored
- switch back to subagent → subagent's state restored
- same-chat switch is no-op
- out-of-range index is no-op (defensive)
## Next phases
- Phase B: Ctrl-N/P/X keybinds + /tasks slash command (rebind
interjection-drop to Alt+X)
- Phase C: per-chat UI state in ui/mod.rs (response_buf etc.)
- Phase D: refactor `task` tool to spawn full agent (with tools)
- Phase E: subagent event router to chat buffer
Copy file name to clipboardExpand all lines: .beads/issues.jsonl
+1-1Lines changed: 1 addition & 1 deletion
Original file line number
Diff line number
Diff line change
@@ -25,7 +25,7 @@
25
25
{"_type":"issue","id":"dirge-86e","title":"ANSI injection in permission ALERT prompt","description":"ask_req.tool / ask_req.input rendered un-sanitized at mod.rs:2584-2585. Reopen path already sanitizes — asymmetric. Sec impl: ANSI at the permission-decision moment.","status":"closed","priority":1,"issue_type":"bug","assignee":"Yogthos","owner":"yogthos@gmail.com","created_at":"2026-05-21T22:17:34Z","created_by":"Yogthos","updated_at":"2026-05-21T22:26:37Z","started_at":"2026-05-21T22:17:42Z","closed_at":"2026-05-21T22:26:37Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0}
26
26
{"_type":"issue","id":"dirge-9f1","title":"Chat history ignores 120-col content_width cap","description":"max_line_width and wrap_line use raw content_cols, so on wide terminals scrollback overflows the centered band into divider/panel margin.","status":"closed","priority":1,"issue_type":"bug","assignee":"Yogthos","owner":"yogthos@gmail.com","created_at":"2026-05-21T22:17:33Z","created_by":"Yogthos","updated_at":"2026-05-21T22:26:36Z","started_at":"2026-05-21T22:17:42Z","closed_at":"2026-05-21T22:26:36Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0}
27
27
{"_type":"issue","id":"dirge-woq","title":"R1: fix 3 critical plugin bugs (FFI panic, dialog deadlock, init hang)","description":"From the plugin subsystem audit: (1) wrap JanetCFunctions in catch_unwind so Rust panics don't unwind across the C-FFI boundary into Janet; (2) cancel send_dialog's reply_rx.recv() on worker shutdown so the worker thread doesn't block forever when the UI exits mid-dialog; (3) add timeout to the init handshake so a worker panic before init_tx.send() doesn't hang the main thread. Also: (4) bounds-assert wrap_string's i32 cast for the unlikely \u003e2GB case, (5) make take_string_slot atomic to close the race window, (6) don't eat unrelated user events in the dialog arm.","status":"closed","priority":1,"issue_type":"bug","owner":"yogthos@gmail.com","created_at":"2026-05-20T14:59:57Z","created_by":"Yogthos","updated_at":"2026-05-20T15:30:28Z","started_at":"2026-05-20T15:00:10Z","closed_at":"2026-05-20T15:30:28Z","dependency_count":0,"dependent_count":1,"comment_count":0}
28
-
{"_type":"issue","id":"dirge-ov2","title":"UI: subagent multi-chat-window (Ctrl-N/P, /tasks picker) — port from maki","description":"Each subagent task gets its own chat window; navigate via Ctrl-N (next) / Ctrl-P (previous) and /tasks slash command opens a picker. Maki: maki-ui/src/app/mod.rs:317-322 (open_tasks), :341-352 (Ctrl-N/P), components/keybindings.rs:135-136 (NEXT_CHAT/PREV_CHAT), components/command.rs:22-23 (/tasks).\n\nBigger refactor: dirge's renderer holds ONE buffer. Need to model Vec\u003cChat\u003e with active_chat index, swap renderer buffer on switch. Conflict: dirge's Ctrl-X currently drops queued interjections — would need a different binding for next-task, or rebind interjection-drop. Coordinate with user before starting.","status":"open","priority":2,"issue_type":"feature","owner":"yogthos@gmail.com","created_at":"2026-05-23T17:37:07Z","created_by":"Yogthos","updated_at":"2026-05-23T17:37:07Z","dependency_count":0,"dependent_count":0,"comment_count":0}
28
+
{"_type":"issue","id":"dirge-ov2","title":"UI: subagent multi-chat-window (Ctrl-N/P, /tasks picker) — port from maki","description":"Each subagent task gets its own chat window; navigate via Ctrl-N (next) / Ctrl-P (previous) and /tasks slash command opens a picker. Maki: maki-ui/src/app/mod.rs:317-322 (open_tasks), :341-352 (Ctrl-N/P), components/keybindings.rs:135-136 (NEXT_CHAT/PREV_CHAT), components/command.rs:22-23 (/tasks).\n\nBigger refactor: dirge's renderer holds ONE buffer. Need to model Vec\u003cChat\u003e with active_chat index, swap renderer buffer on switch. Conflict: dirge's Ctrl-X currently drops queued interjections — would need a different binding for next-task, or rebind interjection-drop. Coordinate with user before starting.","notes":"## Design (post-scope-decision: user picked Heavier refactor)\n\nFive phases, each independently committable so we can ship partial\nprogress if needed.\n\n### Phase A: Multi-buffer in Renderer (~200-300 lines)\n\nPer-chat state extracted into a `ChatBuffer` struct:\n- buffer: Vec\u003cLineEntry\u003e\n- partial: CompactString + partial_color\n- scroll_offset, lines, col\n- selection_active, selection_start, selection_end\n\nGlobal state stays on Renderer:\n- input_rows, monochrome\n- panel_mode, panel_data\n- avatar_state, avatar_tick\n\nRenderer gains:\n- chats: Vec\u003cChatBuffer\u003e\n- active_chat: usize\n- add_chat(name) -\u003e ChatId\n- switch_chat(id)\n- chat_names()\n\nAll existing public methods (write_line, write, replace_from,\nrender_viewport, scroll_*, selection_*) route to chats[active_chat].\n\nBackward compat: Renderer::new() creates one default chat (active=0).\nSingle-chat usage = existing behavior.\n\n### Phase B: Keybinds + /tasks (~80 lines)\n\n- Ctrl-N: next chat\n- Ctrl-P: prev chat\n- Ctrl-X: open /tasks picker (rebind interjection-drop to Alt+X\n per ov2 plan)\n- `/tasks` slash command: same picker\n\nPicker shows chat names; selection switches active.\n\n### Phase C: Per-chat UI state (~150 lines)\n\nui/mod.rs has 10+ state variables that are currently per-session:\n- response_buf, response_start_line\n- reasoning_buf, reasoning_start_line\n- last_tool_name, last_tool_call_id\n- tool_chamber_open, agent_line_started\n- was_reasoning, tool_calls_buf\n\nMove to a `ChatUiState` struct, one per chat. The UI loop reads\nstate from `chat_states[active_chat]` on each event.\n\n### Phase D: task tool refactor (~200 lines)\n\nCurrently `task` tool calls `model.btw_query(prompt)` — one-shot,\nno tools. Refactor to:\n- Spawn a sub-runner using same provider but limited tool set\n (read-only? or same as parent? — needs decision)\n- Subagent emits AgentEvents to its own mpsc channel\n- task tool returns the chat_id; UI routes events from the channel\n to chats[chat_id]\n\n### Phase E: Event router (~80 lines)\n\nUI loop selects across multiple agent_rx channels (one per active\nsubagent). Each event lands in chat_states[event.chat_id] and\nrenders via chats[event.chat_id].\n\n## Open question for Phase D\n\nShould subagent inherit parent's tool set, or be restricted to\nread-only? Maki gives subagents full tools by default. Dirge's\npermission system already gates per-tool, so safety isn't broken\neither way. Inheriting is simpler.","status":"in_progress","priority":2,"issue_type":"feature","owner":"yogthos@gmail.com","created_at":"2026-05-23T17:37:07Z","created_by":"Yogthos","updated_at":"2026-05-23T17:59:28Z","started_at":"2026-05-23T17:53:50Z","dependency_count":0,"dependent_count":0,"comment_count":0}
29
29
{"_type":"issue","id":"dirge-rmk","title":"CLI: --output-format stream-json headless mode (port from maki)","description":"dirge has --print boolean but no output format selector. Port from maki:\n- maki/src/main.rs:32-58 (Cli with --print + --output-format)\n- maki/src/print.rs:44-49 (OutputFormat enum: Text | Json | StreamJson)\n- maki/src/print.rs:51-110 (PrintResult / InitEvent / AssistantEvent / UserEvent structs — Claude-Code-compatible JSON schema)\n- maki/src/print.rs:126-388 (headless run loop with NDJSON event serialization at lines 280, 285, 304, 325, 334)\n\nAdapt: dirge's run_print already exists at src/provider.rs:572. Extend it with format dispatch. For stream-json: serde-serialize each AgentEvent (or a JSON-shaped envelope) and println! one object per line.","status":"closed","priority":2,"issue_type":"feature","owner":"yogthos@gmail.com","created_at":"2026-05-23T17:37:06Z","created_by":"Yogthos","updated_at":"2026-05-23T17:51:48Z","started_at":"2026-05-23T17:44:18Z","closed_at":"2026-05-23T17:51:48Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0}
30
30
{"_type":"issue","id":"dirge-efw","title":"Perm review F3: space-star pattern optional-arg semantics","description":"LOW-polish gap found in opencode-vs-dirge review. Opencode's wildcard matcher (util/wildcard.ts:13-15) rewrites trailing ' *' as '( .*)?' — making the trailing args optional. So 'ls *' matches BOTH 'ls' and 'ls -la'. Dirge's pattern.rs glob_to_regex requires the args (' *' becomes ' .*' which needs at least the space).\n\nConcrete UX friction: a user accepts 'ls *' from a session allowlist after the agent runs 'ls -la', then the agent runs bare 'ls' — re-prompted because the saved pattern doesn't match. Port the optional-suffix rewrite to src/permission/pattern.rs's glob_to_regex.","status":"closed","priority":2,"issue_type":"task","owner":"yogthos@gmail.com","created_at":"2026-05-23T15:48:20Z","created_by":"Yogthos","updated_at":"2026-05-23T16:07:24Z","started_at":"2026-05-23T15:57:07Z","closed_at":"2026-05-23T16:07:24Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0}
31
31
{"_type":"issue","id":"dirge-84j","title":"B3-9: edit tool fuzzy-match cascade","description":"edit.rs:155-160 returns 'old_text not found' on whitespace/indent/CRLF drift. LLMs frequently hit this. opencode edit.ts:222-432 has simple → lineTrimmed → whitespace-normalized → indentation-flexible → levenshtein. pi edit-diff.ts:91-132 has fuzzyFindText. Port the cascade.","status":"closed","priority":2,"issue_type":"feature","assignee":"Yogthos","owner":"yogthos@gmail.com","created_at":"2026-05-23T03:24:44Z","created_by":"Yogthos","updated_at":"2026-05-23T03:43:39Z","started_at":"2026-05-23T03:37:16Z","closed_at":"2026-05-23T03:43:39Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0}
0 commit comments