Skip to content

Commit 0e869a8

Browse files
author
Yogthos
committed
feat(ui): ov2 Phase B — Ctrl-N/P chat switching, /tasks slash, Alt+X for drop-interjection
Second milestone of dirge-ov2. Adds the keyboard surface for the multi-chat model that Phase A wired into the Renderer. ## New keybinds - **Ctrl-N**: next chat (cycles through Renderer.chats with wrap) - **Ctrl-P**: previous chat (wraps) - **Ctrl-X**: cycle forward to next chat (matches maki's TASKS keybinding; will become a proper picker overlay when subagent chats actually exist post-Phase E) - **Alt-X**: drop last queued interjection (was Ctrl-X) All chat-switch bindings no-op when only the main chat exists, so single-session usage is unchanged. ## Ctrl-X collision Per the user's pre-coding decision: dirge's previous Ctrl-X binding (drop the most-recently-queued interjection) moves to Alt-X. Ctrl-X is reclaimed for the maki-style task switcher. Updated the inline hint that prints when the user queues a message during an active run ("Alt+X drops" instead of "Ctrl+X drops") and the `/help` listing. ## `/tasks` slash command Lists the current chat windows with an arrow on the active one and switches forward by one slot. Same effect as Ctrl-X / Ctrl-N for users who prefer slash commands. When only the main chat exists prints a friendly hint pointing at the `task` tool. ## Verified - `cargo check` clean - `cargo test chat_snapshot` still passes (Phase A foundation intact) - single-chat sessions behave identically — Ctrl-N/P/X no-op, Alt-X works for interjection drop, /tasks shows the hint ## Next - Phase C: per-chat UI state (response_buf, last_tool_name, etc.) moves into a per-chat struct in ui/mod.rs so chat switches preserve the streaming context - Phase D: refactor `task` tool to spawn full agent + events - Phase E: event router → chat buffer
1 parent 2c3bada commit 0e869a8

2 files changed

Lines changed: 109 additions & 8 deletions

File tree

src/ui/mod.rs

Lines changed: 73 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1294,12 +1294,16 @@ pub async fn run_interactive(
12941294
)?;
12951295
continue;
12961296
}
1297-
// Ctrl+X drops the most-recently-queued interjection
1298-
// without affecting the running agent. No-op when the
1299-
// queue is empty so it doesn't shadow other behaviors.
1300-
let ctrl_x = key.code == KeyCode::Char('x')
1301-
&& key.modifiers.contains(KeyModifiers::CONTROL);
1302-
if ctrl_x && !interjection_queue.is_empty() {
1297+
// dirge-ov2 Phase B: Ctrl-X is reclaimed for
1298+
// the /tasks subagent-chat picker (matches
1299+
// maki's binding). The previous "drop queued
1300+
// interjection" behavior moves to Alt+X. The
1301+
// queued-message footer below now reads
1302+
// "Alt+X drops" instead of "Ctrl+X drops" —
1303+
// surfaced wherever the queue hint is shown.
1304+
let alt_x = key.code == KeyCode::Char('x')
1305+
&& key.modifiers.contains(KeyModifiers::ALT);
1306+
if alt_x && !interjection_queue.is_empty() {
13031307
interjection_queue.pop_back();
13041308
write_outside_chamber(
13051309
&mut renderer,
@@ -1319,6 +1323,68 @@ pub async fn run_interactive(
13191323
continue;
13201324
}
13211325

1326+
// dirge-ov2 Phase B: Ctrl-N cycles to the next
1327+
// chat (subagent window). Wraps to first chat
1328+
// after the last; no-op when only one chat
1329+
// exists. Mirrors maki's NEXT_CHAT binding
1330+
// (`components/keybindings.rs:136`).
1331+
let ctrl_n = key.code == KeyCode::Char('n')
1332+
&& key.modifiers.contains(KeyModifiers::CONTROL);
1333+
if ctrl_n && renderer.chat_count() > 1 {
1334+
let next = (renderer.active_chat() + 1) % renderer.chat_count();
1335+
renderer.switch_chat(next);
1336+
renderer.render_viewport()?;
1337+
renderer.draw_bottom(
1338+
&input,
1339+
&with_queue(StatusLine::render(session, is_running, 0, loop_label.as_deref(), context.current_prompt_name.as_deref(), perm_mode().as_deref()), interjection_queue.len()),
1340+
is_running,
1341+
)?;
1342+
continue;
1343+
}
1344+
1345+
// dirge-ov2 Phase B: Ctrl-P cycles to the
1346+
// previous chat. Mirrors maki's PREV_CHAT
1347+
// binding (`components/keybindings.rs:135`).
1348+
let ctrl_p = key.code == KeyCode::Char('p')
1349+
&& key.modifiers.contains(KeyModifiers::CONTROL);
1350+
if ctrl_p && renderer.chat_count() > 1 {
1351+
let count = renderer.chat_count();
1352+
let prev = (renderer.active_chat() + count - 1) % count;
1353+
renderer.switch_chat(prev);
1354+
renderer.render_viewport()?;
1355+
renderer.draw_bottom(
1356+
&input,
1357+
&with_queue(StatusLine::render(session, is_running, 0, loop_label.as_deref(), context.current_prompt_name.as_deref(), perm_mode().as_deref()), interjection_queue.len()),
1358+
is_running,
1359+
)?;
1360+
continue;
1361+
}
1362+
1363+
// dirge-ov2 Phase B: Ctrl-X opens the /tasks
1364+
// picker — same effect as typing `/tasks`.
1365+
// No-op when only the main chat exists
1366+
// (nothing to switch between). Maki's TASKS
1367+
// keybinding (`components/keybindings.rs`,
1368+
// `ctrl_bind!('x')`).
1369+
let ctrl_x = key.code == KeyCode::Char('x')
1370+
&& key.modifiers.contains(KeyModifiers::CONTROL);
1371+
if ctrl_x && renderer.chat_count() > 1 {
1372+
// Cycle through chats via Ctrl-N once a
1373+
// proper picker UI lands in Phase B+.
1374+
// For now, cycle forward — gives the
1375+
// user a way to land on subagent chats
1376+
// via a single key press.
1377+
let next = (renderer.active_chat() + 1) % renderer.chat_count();
1378+
renderer.switch_chat(next);
1379+
renderer.render_viewport()?;
1380+
renderer.draw_bottom(
1381+
&input,
1382+
&with_queue(StatusLine::render(session, is_running, 0, loop_label.as_deref(), context.current_prompt_name.as_deref(), perm_mode().as_deref()), interjection_queue.len()),
1383+
is_running,
1384+
)?;
1385+
continue;
1386+
}
1387+
13221388
// Ctrl+O — expand the most-recent collapsed
13231389
// tool result. We re-print it as a fresh
13241390
// chamber below the current chat so the user
@@ -1937,7 +2003,7 @@ pub async fn run_interactive(
19372003
}
19382004
renderer.write_line(
19392005
&format!(
1940-
"(queued; runner will stop at next safe boundary — Ctrl+X drops, Ctrl+C cancels)"
2006+
"(queued; runner will stop at next safe boundary — Alt+X drops, Ctrl+C cancels)"
19412007
),
19422008
theme::dim(),
19432009
)?;

src/ui/slash.rs

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1145,6 +1145,33 @@ pub async fn handle_slash(
11451145
*is_running = false;
11461146
return Err(std::io::Error::new(std::io::ErrorKind::Interrupted, "quit").into());
11471147
}
1148+
// dirge-ov2 Phase B: `/tasks` lists active chat windows and
1149+
// cycles to the next one. Equivalent to Ctrl-X / Ctrl-N for
1150+
// keyboard-shy users (or scripts that drive dirge via slash
1151+
// commands). When only the main chat exists, prints a hint.
1152+
"/tasks" => {
1153+
let names = renderer.chat_names();
1154+
if names.len() <= 1 {
1155+
renderer.write_line(
1156+
"no subagent chats — spawn one via the `task` tool or wait for the agent to dispatch a subagent.",
1157+
c_result(),
1158+
)?;
1159+
} else {
1160+
renderer.write_line("chat windows:", c_result())?;
1161+
let active = renderer.active_chat();
1162+
for (i, name) in names.iter().enumerate() {
1163+
let marker = if i == active { "→" } else { " " };
1164+
renderer.write_line(&format!(" {} [{}] {}", marker, i, name), c_result())?;
1165+
}
1166+
let next = (active + 1) % names.len();
1167+
renderer.switch_chat(next);
1168+
renderer.render_viewport()?;
1169+
renderer.write_line(
1170+
&format!("→ switched to chat {} ({})", next, names[next]),
1171+
c_result(),
1172+
)?;
1173+
}
1174+
}
11481175
"/clear" => {
11491176
session.messages.clear();
11501177
session.total_estimated_tokens = 0;
@@ -1767,7 +1794,15 @@ pub async fn handle_slash(
17671794
renderer.write_line(" Ctrl+R toggle reasoning", c_result())?;
17681795
renderer.write_line(" Ctrl+C / Ctrl+D interrupt/quit", c_result())?;
17691796
renderer.write_line(
1770-
" Ctrl+X drop last queued interjection",
1797+
" Ctrl+N / Ctrl+P next / previous chat (subagent windows)",
1798+
c_result(),
1799+
)?;
1800+
renderer.write_line(
1801+
" Ctrl+X / /tasks cycle through chat windows",
1802+
c_result(),
1803+
)?;
1804+
renderer.write_line(
1805+
" Alt+X drop last queued interjection",
17711806
c_result(),
17721807
)?;
17731808
renderer.write_line(

0 commit comments

Comments
 (0)