Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 6 additions & 7 deletions design/cli-lock-board-index.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,12 @@ Verifier tools are the hidden `cortex mcp-server --verify` surface
`lock.palette_audit`, `lock.diff_txt`, `tui.*`, `login.run`, `api.*`,
`mcp.*`, `report.finish`. Guide: [Verification MCP](../docs/guides/development.md).

`tui.key` (`verify_mcp/tui.rs` `apply_key`) only applies composer **Clear**,
**NewLine** (`Shift+Enter` in the production mapper), **Backspace**, and
**single-character insert**. `F2`, `Shift+Tab`, `Alt+Enter`, and `Ctrl+x`
parse as keys and return success but are no-ops: they do not open Settings
or Shortcuts, cycle mode, or insert a newline. Designer lock copy still
shows those chords; cover those boards with `lock.render`,
`cargo test -p cortex-tui`, or `tui.type` / `tui.slash`, not `tui.key`.
`tui.key` (`verify_mcp/tui.rs` `apply_key`) applies composer **Clear**,
**NewLine** (`Shift+Enter` and `Alt+Enter`), **Backspace**, **single-character
insert**, **Ctrl+x** / **Esc** / **Up** / **Down** on the shortcuts sheet,
**F2** (settings), **Shift+Tab** (Agent / Plan / Ask), and slash-palette
**Up** / **Down**. Cover remaining boards with `lock.render` or
`cargo test -p cortex-tui`.

**Flow** in the tables is a named interactive scenario on that verify server
(or the matching `cargo test -p cortex-tui` case), not a separate spec:
Expand Down
Binary file modified docs/media/tui-lock-v2/runtime/120x40/clear-confirm.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/media/tui-lock-v2/runtime/120x40/cloud-handoff.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/media/tui-lock-v2/runtime/120x40/code-fence.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/media/tui-lock-v2/runtime/120x40/composer-multiline.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/media/tui-lock-v2/runtime/120x40/composer-typing-blink.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/media/tui-lock-v2/runtime/120x40/composer-typing.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/media/tui-lock-v2/runtime/120x40/diff-hunk.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/media/tui-lock-v2/runtime/120x40/first-run-tips.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/media/tui-lock-v2/runtime/120x40/goal-chip-active.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/media/tui-lock-v2/runtime/120x40/goal-chip-blocked.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/media/tui-lock-v2/runtime/120x40/goal-chip-budget.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/media/tui-lock-v2/runtime/120x40/goal-chip-done.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/media/tui-lock-v2/runtime/120x40/goal-chip-paused.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/media/tui-lock-v2/runtime/120x40/interrupt-stopped.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/media/tui-lock-v2/runtime/120x40/mcp-drop.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/media/tui-lock-v2/runtime/120x40/mcp-servers.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/media/tui-lock-v2/runtime/120x40/md-table.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/media/tui-lock-v2/runtime/120x40/mode-agent.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/media/tui-lock-v2/runtime/120x40/mode-ask.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/media/tui-lock-v2/runtime/120x40/mode-plan.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/media/tui-lock-v2/runtime/120x40/model-effort-high.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/media/tui-lock-v2/runtime/120x40/model-effort-hover.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/media/tui-lock-v2/runtime/120x40/model-effort-low.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/media/tui-lock-v2/runtime/120x40/model-effort-medium.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/media/tui-lock-v2/runtime/120x40/model-list-hover.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/media/tui-lock-v2/runtime/120x40/model-list.png
Binary file modified docs/media/tui-lock-v2/runtime/120x40/permission-prompt.png
Binary file modified docs/media/tui-lock-v2/runtime/120x40/permissions-picker.png
Binary file modified docs/media/tui-lock-v2/runtime/120x40/plan-confirm.png
Binary file modified docs/media/tui-lock-v2/runtime/120x40/question.png
Binary file modified docs/media/tui-lock-v2/runtime/120x40/resume-picker.png
Binary file modified docs/media/tui-lock-v2/runtime/120x40/sandbox-deny.png
Binary file modified docs/media/tui-lock-v2/runtime/120x40/shortcuts-overlay.png
Binary file modified docs/media/tui-lock-v2/runtime/120x40/slash-palette.png
Binary file modified docs/media/tui-lock-v2/runtime/120x40/todos.png
Binary file modified docs/media/tui-lock-v2/runtime/120x40/tokens-topright.png
Binary file modified docs/media/tui-lock-v2/runtime/120x40/tool-tiles-collapsed.png
Binary file modified docs/media/tui-lock-v2/runtime/120x40/tool-tiles.png
Binary file modified docs/media/tui-lock-v2/runtime/120x40/welcome-agent.png
Binary file modified docs/media/tui-lock-v2/runtime/120x40/welcome-cortex.png
Binary file modified docs/media/tui-lock-v2/runtime/40x12/composer-typing.png
Binary file modified docs/media/tui-lock-v2/runtime/40x12/diff-hunk.png
Binary file modified docs/media/tui-lock-v2/runtime/40x12/goal-chip-active.png
Binary file modified docs/media/tui-lock-v2/runtime/40x12/goal-chip-blocked.png
Binary file modified docs/media/tui-lock-v2/runtime/40x12/goal-chip-budget.png
Binary file modified docs/media/tui-lock-v2/runtime/40x12/goal-chip-done.png
Binary file modified docs/media/tui-lock-v2/runtime/40x12/goal-chip-paused.png
Binary file modified docs/media/tui-lock-v2/runtime/40x12/interrupt-stopped.png
Binary file modified docs/media/tui-lock-v2/runtime/40x12/mcp-servers.png
Binary file modified docs/media/tui-lock-v2/runtime/40x12/mode-ask.png
Binary file modified docs/media/tui-lock-v2/runtime/40x12/mode-plan.png
Binary file modified docs/media/tui-lock-v2/runtime/40x12/model-effort-high.png
Binary file modified docs/media/tui-lock-v2/runtime/40x12/model-list.png
Binary file modified docs/media/tui-lock-v2/runtime/40x12/permission-prompt.png
Binary file modified docs/media/tui-lock-v2/runtime/40x12/shortcuts-overlay.png
Binary file modified docs/media/tui-lock-v2/runtime/40x12/slash-palette.png
Binary file modified docs/media/tui-lock-v2/runtime/40x12/welcome-agent.png
Binary file modified docs/media/tui-lock-v2/runtime/40x12/welcome-cortex.png
19 changes: 12 additions & 7 deletions docs/reference/keyboard.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,10 @@ or `F1` at any time for the in-app version.
| `Ctrl+Shift+C` | Copy the selection |
| `Ctrl+Shift+V` | Paste |
| `?` or `F1` | Help |
| `Esc` | Cancel or close |
| `Esc` | Cancel, close a sheet, or interrupt the running turn |
| `Tab` | Focus the next element |
| `Shift+Tab` | Cycle the autonomy level |
| `Shift+Tab` | Cycle Agent / Plan / Ask |
| `F2` | Open settings |
| `Ctrl+K` or `Ctrl+P` | Command palette |
| `Ctrl+I` | Focus the composer |
| `Ctrl+B` | Toggle the sidebar |
Expand All @@ -29,8 +30,10 @@ or `F1` at any time for the in-app version.
| Key | Action |
|-----|--------|
| `Enter` | Send |
| `Shift+Enter` | Insert a newline |
| `Up` / `Down` | Previous / next prompt in history |
| `Shift+Enter` or `Alt+Enter` | Insert a newline |
| `Ctrl+x` | Open or close the shortcuts sheet |
| `Up` / `Down` | Previous / next prompt in history; move the slash-palette selection |
| `Ctrl+U` or `Ctrl+L` | Clear the composer |
| `Ctrl+U` or `Ctrl+L` | Clear the composer |
| `Ctrl+V` | Paste |
| `Ctrl+A` | Select all |
Expand Down Expand Up @@ -94,8 +97,9 @@ Shown at startup when there are sessions to resume.
| Key | Action |
|-----|--------|
| `Enter` | Resume the selected session |
| `F` | Fork from it |
| `N` or `Esc` | Start a new session |
| `f` | Favorite it |
| `d` | Delete it |
| `Esc` | Close |

## Modals

Expand All @@ -113,7 +117,8 @@ reverts to the one you started with.
| Key | Action |
|-----|--------|
| `Esc` | Interrupt the turn |
| `Ctrl+C` | Force quit |
| `Ctrl+C` | Interrupt the turn. When idle: copy the selection, or press twice to quit |
| `Ctrl+x` | Open or close the shortcuts sheet |

## See also

Expand Down
1 change: 1 addition & 0 deletions docs/reference/slash-commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ ordinary message.
| `/install-github-app` | | Install the Cortex GitHub App |
| `/bug` | | `/bug [description]` |
| `/goal` | | `/goal [objective\|status\|pause\|resume\|clear]` — persisted long-horizon objective |
| `/shortcuts` | `keys` | Open the shortcuts sheet (also `Ctrl+x`) |

## Authentication and billing

Expand Down
94 changes: 94 additions & 0 deletions src/cortex-cli/src/verify_mcp/tui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -307,11 +307,48 @@ pub fn stop(state: &mut VerifyState, args: &Value) -> Result<Value> {
}

fn apply_key(session: &mut TuiSession, name: &str) -> Result<()> {
if name.eq_ignore_ascii_case("Ctrl+x") {
session.app_state.toggle_shortcuts_sheet();
return Ok(());
}
if session.app_state.shortcuts_open {
if name.eq_ignore_ascii_case("Esc") || name.eq_ignore_ascii_case("F2") {
session.app_state.close_shortcuts_sheet();
return Ok(());
}
if name.eq_ignore_ascii_case("Down") {
session.app_state.shortcuts_move(1);
return Ok(());
}
if name.eq_ignore_ascii_case("Up") {
session.app_state.shortcuts_move(-1);
return Ok(());
}
}
if session.app_state.autocomplete.visible {
if name.eq_ignore_ascii_case("Down") {
session.app_state.autocomplete.select_next();
return Ok(());
}
if name.eq_ignore_ascii_case("Up") {
session.app_state.autocomplete.select_prev();
return Ok(());
}
}
let event = parse_key_string(name).ok_or_else(|| anyhow::anyhow!("unknown key {name}"))?;
let action = session.mapper.get_action(event, ActionContext::Input);
match action {
KeyAction::Clear => session.app_state.input.set_text(""),
KeyAction::NewLine => session.app_state.input.insert_str("\n"),
KeyAction::CyclePermissionMode => session.app_state.cycle_agent_mode(),
KeyAction::ToggleSettings => session.app_state.open_settings_modal(),
KeyAction::Cancel => {
if session.app_state.streaming.is_streaming {
session.app_state.stop_streaming();
} else if session.app_state.is_interactive_mode() {
session.app_state.exit_interactive_mode();
}
}
_ if name.eq_ignore_ascii_case("Backspace") => {
let text = session.app_state.input.text();
let mut chars: Vec<char> = text.chars().collect();
Expand Down Expand Up @@ -430,6 +467,63 @@ mod tests {
assert!(state.sessions.is_empty());
}

#[test]
fn keyboard_map_ctrl_x_shift_tab_palette_and_cancel() {
let (mut state, id) = started("cortex");
key(&mut state, &json!({"session_id": id, "keys": ["Ctrl+x"]})).expect("open sheet");
assert!(session_ref(&state, &id).unwrap().app_state.shortcuts_open);
key(
&mut state,
&json!({"session_id": id, "keys": ["Down", "Up", "F2"]}),
)
.expect("sheet nav");
assert!(!session_ref(&state, &id).unwrap().app_state.shortcuts_open);

key(
&mut state,
&json!({"session_id": id, "keys": ["Ctrl+x", "Esc"]}),
)
.expect("esc sheet");
assert!(!session_ref(&state, &id).unwrap().app_state.shortcuts_open);

slash(&mut state, &json!({"session_id": id, "query": "/"})).expect("slash");
assert!(
session_ref(&state, &id)
.unwrap()
.app_state
.autocomplete
.visible
);
key(
&mut state,
&json!({"session_id": id, "keys": ["Down", "Up"]}),
)
.expect("palette");

key(
&mut state,
&json!({"session_id": id, "keys": ["Shift+Tab", "F2"]}),
)
.expect("mode and settings");
assert!(
session_ref(&state, &id)
.unwrap()
.app_state
.settings_modal
.is_some()
);

let session = session_mut(&mut state, &id).unwrap();
session.app_state.start_streaming(None, true);
apply_key(session, "Esc").expect("interrupt");
assert!(!session.app_state.streaming.is_streaming);

let picker = cortex_tui::interactive::builders::build_clear_confirm();
session.app_state.enter_interactive_mode(picker);
apply_key(session, "Esc").expect("close picker");
assert!(!session.app_state.is_interactive_mode());
}

#[test]
fn agent_entry_and_error_paths() {
let (mut state, id) = started("agent");
Expand Down
26 changes: 24 additions & 2 deletions src/cortex-engine/src/goal/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -254,9 +254,19 @@ impl Goal {
)
}

/// Pause / resume / set acknowledgement that repeats the chip.
/// Pause / resume / set acknowledgement that repeats the chip and next step.
pub fn action_text(&self, action: &str) -> String {
format!("{action} {}", self.chip())
format!("{action} {}\n next: {}", self.chip(), self.next_step())
}

/// Compact TUI card for `/goal` and `/goal status`.
pub fn status_card(&self) -> String {
format!(
"{}\n {}\n next: {}\n /goal pause · /goal resume · /goal clear",
self.chip(),
self.objective,
self.next_step()
)
}

pub fn turns_remaining(&self) -> u32 {
Expand Down Expand Up @@ -325,6 +335,18 @@ mod tests {
assert!(text.starts_with("Goal · 7/8"), "{text}");
assert!(text.contains("wrap-up"), "{text}");
assert!(text.contains("write out.txt"), "{text}");
let card = goal.status_card();
assert!(card.contains("Goal · 7/8"), "{card}");
assert!(card.contains("/goal pause"), "{card}");
assert!(card.contains("/goal resume"), "{card}");
assert!(card.contains("/goal clear"), "{card}");
let paused = {
let mut g = goal.clone();
g.state = GoalState::Paused;
g.action_text("Goal paused.")
};
assert!(paused.contains("Goal · paused"), "{paused}");
assert!(paused.contains("next:"), "{paused}");
}

#[test]
Expand Down
43 changes: 42 additions & 1 deletion src/cortex-tui/src/actions/mapper.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,21 @@ impl ActionMapper {
KeyAction::CyclePermissionMode,
"Cycle Agent / Plan / Ask",
),
KeyBinding::global(
KeyEvent::new(KeyCode::Tab, KeyModifiers::SHIFT),
KeyAction::CyclePermissionMode,
"Cycle Agent / Plan / Ask",
),
KeyBinding::global(
KeyEvent::new(KeyCode::BackTab, KeyModifiers::NONE),
KeyAction::CyclePermissionMode,
"Cycle Agent / Plan / Ask",
),
KeyBinding::global(
KeyEvent::new(KeyCode::F(2), KeyModifiers::NONE),
KeyAction::ToggleSettings,
"Open settings",
),
// Sidebar toggle (Ctrl+B only)
KeyBinding::global(
KeyEvent::new(KeyCode::Char('b'), KeyModifiers::CONTROL),
Expand Down Expand Up @@ -159,12 +174,17 @@ impl ActionMapper {
KeyAction::Submit,
"Submit message",
),
// New line (Shift+Enter)
// New line (Shift+Enter or Alt+Enter — lock footer)
KeyBinding::input(
KeyEvent::new(KeyCode::Enter, KeyModifiers::SHIFT),
KeyAction::NewLine,
"Insert new line",
),
KeyBinding::input(
KeyEvent::new(KeyCode::Enter, KeyModifiers::ALT),
KeyAction::NewLine,
"Insert new line",
),
// History navigation
KeyBinding::input(
KeyEvent::new(KeyCode::Up, KeyModifiers::NONE),
Expand Down Expand Up @@ -552,6 +572,27 @@ mod tests {
KeyAction::Submit
);

let alt_enter = KeyEvent::new(KeyCode::Enter, KeyModifiers::ALT);
assert_eq!(
mapper.get_action(alt_enter, ActionContext::Input),
KeyAction::NewLine
);
let shift_enter = KeyEvent::new(KeyCode::Enter, KeyModifiers::SHIFT);
assert_eq!(
mapper.get_action(shift_enter, ActionContext::Input),
KeyAction::NewLine
);
let shift_tab = KeyEvent::new(KeyCode::Tab, KeyModifiers::SHIFT);
assert_eq!(
mapper.get_action(shift_tab, ActionContext::Global),
KeyAction::CyclePermissionMode
);
let f2 = KeyEvent::new(KeyCode::F(2), KeyModifiers::NONE);
assert_eq!(
mapper.get_action(f2, ActionContext::Global),
KeyAction::ToggleSettings
);

// Test global fallback
let help_key = KeyEvent::new(KeyCode::Char('?'), KeyModifiers::NONE);
assert_eq!(
Expand Down
72 changes: 72 additions & 0 deletions src/cortex-tui/src/app/checklist.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
//! Session working checklist (lock `todos`).

use super::subagent::{SubagentTodoItem, SubagentTodoStatus};

/// Live turn checklist painted above the composer — distinct from subagent tiles.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WorkingChecklist {
pub elapsed_secs: u32,
pub tokens: u64,
pub items: Vec<SubagentTodoItem>,
}

impl WorkingChecklist {
pub fn new(items: Vec<SubagentTodoItem>, elapsed_secs: u32, tokens: u64) -> Self {
Self {
elapsed_secs,
tokens,
items,
}
}

pub fn done(&self) -> usize {
self.items
.iter()
.filter(|t| matches!(t.status, SubagentTodoStatus::Completed))
.count()
}

pub fn header(&self) -> String {
format!(
"⠇ Working {}/{} · {}s · {}",
self.done(),
self.items.len(),
self.elapsed_secs,
compact_tokens(self.tokens)
)
}
}

fn compact_tokens(n: u64) -> String {
if n >= 1_000 {
let k = n as f64 / 1_000.0;
if (k - k.round()).abs() < 0.05 {
format!("{}k tokens", k as u64)
} else {
format!("{k:.1}k tokens")
}
} else {
format!("{n} tokens")
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn header_matches_lock_copy() {
let board = WorkingChecklist::new(
vec![
SubagentTodoItem::new("a", SubagentTodoStatus::Completed),
SubagentTodoItem::new("b", SubagentTodoStatus::Completed),
SubagentTodoItem::new("c", SubagentTodoStatus::InProgress),
SubagentTodoItem::new("d", SubagentTodoStatus::Pending),
SubagentTodoItem::new("e", SubagentTodoStatus::Pending),
],
38,
6_100,
);
assert_eq!(board.header(), "⠇ Working 2/5 · 38s · 6.1k tokens");
}
}
11 changes: 11 additions & 0 deletions src/cortex-tui/src/app/methods.rs
Original file line number Diff line number Diff line change
Expand Up @@ -850,4 +850,15 @@ mod agent_mode_tests {
assert!(!state.has_pending_approval());
assert!(!state.is_interactive_mode());
}

#[test]
fn clear_and_new_session_drop_stopped_placeholder() {
let mut state = AppState::default();
state.last_turn_stopped = true;
state.clear_messages();
assert!(!state.last_turn_stopped);
state.last_turn_stopped = true;
state.new_session();
assert!(!state.last_turn_stopped);
}
}
Loading