返回 CodeWhale
types.rs
根目录 / crates / tui / src / tui / app / types.rs
1 //! Plain data types shared across the TUI: modes, effort/collapse/display
2 //! enums, the public `TuiOptions` construction bag, queued-message records,
3 //! and the action enums drained by the event loop.
4 //!
5 //! Everything here is pure data (plus parsing/labeling helpers that need no
6 //! `App` state). The TUI-owned items are re-exported from `app.rs` so existing
7 //! `crate::tui::app::X` paths are unchanged; types owned by another crate
8 //! (such as [`AppMode`]) are named at their own crate path instead.
9
10 use super::*;
11
12 use codewhale_config::AppMode;
13
14 /// What an interactive setting selection actually did.
15 ///
16 /// The three cases are genuinely different to the user, and the boolean this
17 /// replaced conflated the last two: a refused selection and an accepted one
18 /// that only wrote the startup default both returned `false`, so every caller
19 /// reported "already in that mode" and showed no receipt for the write.
20 ///
21 /// Only [`Self::Changed`] means live session state moved — that is the case
22 /// that must still emit an `AppAction` so the engine is resynchronized.
23 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
24 pub enum SettingSelection {
25 /// Live state moved, and the startup default was persisted.
26 Changed,
27 /// Live state already matched, and the startup default was persisted. This
28 /// is the normal shape after a session restore, where the live value and
29 /// the startup default legitimately disagree.
30 PersistedSame,
31 /// Refused by the turn lock (#2982). Nothing was written anywhere.
32 Refused,
33 }
34
35 impl SettingSelection {
36 /// Whether live state moved — i.e. whether the engine needs resyncing.
37 #[must_use]
38 pub fn changed_live_state(self) -> bool {
39 matches!(self, Self::Changed)
40 }
41
42 /// Whether the selection was accepted at all (either case that persisted).
43 #[must_use]
44 #[cfg(test)]
45 pub fn accepted(self) -> bool {
46 !matches!(self, Self::Refused)
47 }
48 }
49
50 /// Localized, TUI-only presentation of [`AppMode`]. Kept out of
51 /// codewhale-config so the mode type does not depend on the locale packs.
52 pub trait AppModeUi {
53 /// Localized short name for the mode picker (user-facing surface only).
54 fn display_name_localized(self, locale: Locale) -> Cow<'static, str>;
55 /// Localized one-line hint for the mode picker (user-facing surface only).
56 fn picker_hint_localized(self, locale: Locale) -> Cow<'static, str>;
57 }
58
59 impl AppModeUi for AppMode {
60 /// Localized short name for the mode picker (user-facing surface only).
61 fn display_name_localized(self, locale: Locale) -> Cow<'static, str> {
62 tr(
63 locale,
64 match self {
65 AppMode::Agent => MessageId::AppModeAgent,
66 AppMode::Plan => MessageId::AppModePlan,
67 AppMode::Operate => MessageId::AppModeOperate,
68 },
69 )
70 }
71
72 /// Localized one-line hint for the mode picker (user-facing surface only).
73 fn picker_hint_localized(self, locale: Locale) -> Cow<'static, str> {
74 tr(
75 locale,
76 match self {
77 AppMode::Agent => MessageId::AppModeAgentHint,
78 AppMode::Plan => MessageId::AppModePlanHint,
79 AppMode::Operate => MessageId::AppModeOperateHint,
80 },
81 )
82 }
83 }
84
85 /// Exact provider/model route whose prompt can be inspected or replayed.
86 ///
87 /// Auto-model sessions keep `model == "auto"` as the user's selection, so
88 /// cache operations must carry the last concrete route separately. The base
89 /// URL is absent after restoring an older session because saved Auto receipts
90 /// intentionally do not persist raw endpoints.
91 #[derive(Debug, Clone, PartialEq, Eq)]
92 pub(crate) struct CacheReplayTarget {
93 pub(crate) provider: ProviderKind,
94 pub(crate) provider_identity: String,
95 /// Additive exact provider id used by persisted-route resolution.
96 /// `None` is meaningful for the legacy root-level `custom` route.
97 pub(crate) provider_id: Option<String>,
98 pub(crate) model: String,
99 pub(crate) base_url: Option<String>,
100 }
101
102 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
103 pub enum ComposerDensity {
104 Compact,
105 Comfortable,
106 Spacious,
107 }
108
109 impl ComposerDensity {
110 #[must_use]
111 pub fn from_setting(value: &str) -> Self {
112 match value.trim().to_ascii_lowercase().as_str() {
113 "compact" | "tight" => Self::Compact,
114 "spacious" | "loose" => Self::Spacious,
115 _ => Self::Comfortable,
116 }
117 }
118 }
119
120 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
121 pub enum TranscriptSpacing {
122 Compact,
123 Comfortable,
124 Spacious,
125 }
126
127 impl TranscriptSpacing {
128 #[must_use]
129 pub fn from_setting(value: &str) -> Self {
130 match value.trim().to_ascii_lowercase().as_str() {
131 "compact" | "tight" => Self::Compact,
132 "spacious" | "loose" => Self::Spacious,
133 _ => Self::Comfortable,
134 }
135 }
136 }
137
138 /// Controls how dense tool-call runs are collapsed in the transcript.
139 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
140 pub enum ToolCollapseMode {
141 /// Collapse qualifying tool runs by default.
142 ///
143 /// Collapsed success cells keep the tool-name + arg/command summary as the
144 /// single intent line (#3256 decision): that is already the model-visible
145 /// call summary, so a second "intent" source is not required.
146 Compact,
147 /// Never collapse tool runs automatically.
148 Expanded,
149 /// Collapse only when calm mode is active.
150 Calm,
151 }
152
153 impl ToolCollapseMode {
154 #[must_use]
155 pub fn from_setting(value: &str) -> Self {
156 match value.trim().to_ascii_lowercase().as_str() {
157 "expanded" | "off" | "none" => Self::Expanded,
158 "calm" | "calm-mode" | "calm_only" | "calm-only" => Self::Calm,
159 // `collapsed`/`collapse` are issue #3256's preferred names for the
160 // default; treat them like the canonical `compact`.
161 _ => Self::Compact,
162 }
163 }
164
165 #[must_use]
166 pub fn as_setting(self) -> &'static str {
167 match self {
168 Self::Compact => "compact",
169 Self::Expanded => "expanded",
170 Self::Calm => "calm",
171 }
172 }
173
174 #[must_use]
175 pub fn is_active(self, calm_mode: bool) -> bool {
176 match self {
177 Self::Compact => true,
178 Self::Expanded => false,
179 Self::Calm => calm_mode,
180 }
181 }
182 }
183
184 /// Configuration required to bootstrap the TUI.
185 #[derive(Clone)]
186 #[allow(clippy::struct_excessive_bools)]
187 pub struct TuiOptions {
188 pub model: String,
189 pub workspace: PathBuf,
190 pub config_path: Option<PathBuf>,
191 pub config_profile: Option<String>,
192 pub allow_shell: bool,
193 /// Screen the TUI starts on (alternate screen, or a full-height inline
194 /// viewport that leaves the host scrollback intact).
195 pub screen_mode: ScreenMode,
196 /// Capture mouse input for internal scrolling/selection, on the screen
197 /// the session starts on.
198 pub use_mouse_capture: bool,
199 /// The user's mouse-capture answer with the screen factored out (CLI
200 /// flag, `tui.mouse_capture`, or the host default). `/fullscreen` and
201 /// `/inline` re-derive `use_mouse_capture` from it, so the documented
202 /// default keeps applying after a runtime switch.
203 pub mouse_capture_preference: bool,
204 /// Enable terminal bracketed-paste mode (OSC `?2004h` / `?2004l`). Defaults
205 /// on; settable via `bracketed_paste = false` in `settings.toml` for the
206 /// rare terminal that mishandles it.
207 pub use_bracketed_paste: bool,
208 /// Maximum number of concurrent sub-agents.
209 pub max_subagents: usize,
210 pub skills_dir: PathBuf,
211 pub memory_path: PathBuf,
212 #[expect(dead_code)]
213 pub notes_path: PathBuf,
214 pub mcp_config_path: PathBuf,
215 pub use_memory: bool,
216 /// Start in agent mode (defaults to agent; --yolo starts in YOLO)
217 pub start_in_agent_mode: bool,
218 /// Skip onboarding screens
219 pub skip_onboarding: bool,
220 /// Auto-approve tool executions (yolo mode)
221 pub yolo: bool,
222 /// Resume a previous session by ID
223 pub resume_session_id: Option<String>,
224 /// Pre-populate the composer with this text when the TUI starts.
225 /// Used by `deepseek pr <N>` (#451) to drop the model into a
226 /// session with the PR context already typed — the user can edit
227 /// before sending or hit Enter to fire as-is.
228 pub initial_input: Option<InitialInput>,
229 /// One-line receipt to show once at startup.
230 ///
231 /// Auto-resume uses this to say what it did — reattached, or fell back to
232 /// a fresh transcript because the candidate was missing, unreadable, or
233 /// recorded against a different workspace (#2934). Silence is the correct
234 /// value when nothing happened worth reporting.
235 pub startup_notice: Option<String>,
236 }
237
238 #[derive(Debug, Clone, PartialEq, Eq)]
239 pub enum InitialInput {
240 /// Pre-populate the composer and wait for the user to press Enter.
241 ///
242 /// Used by `codewhale pr <N>` (#451) to drop the model into a session
243 /// with the PR context already typed so the user can edit before sending.
244 Prefill(String),
245 /// Pre-populate the composer, submit it once startup is ready, then keep
246 /// the interactive session open for follow-up messages (#2370).
247 Submit(String),
248 /// Begin account-owned web remote control after the TUI is initialized.
249 RemoteControl,
250 }
251
252 // === Sub-state structs for App field organization (#377) ===
253
254 /// Vim modal editing mode for the composer input area.
255 ///
256 /// Enabled via `[composer] mode = "vim"` in `settings.toml`. When the
257 /// composer vim mode is active the user starts in `Normal` mode and presses
258 /// `i`, `a`, or `o` to enter `Insert` mode. `Esc` from `Insert` returns to
259 /// `Normal`. Standard vim motions (`h`/`j`/`k`/`l`, `w`/`b`, `0`/`$`, `x`,
260 /// `dd`) work in `Normal` mode. `Visual` is reserved for future selection
261 /// support and currently behaves like `Normal`.
262 #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
263 pub enum VimMode {
264 /// Normal / command mode — motions and operators, no text insertion.
265 #[default]
266 Normal,
267 /// Insert mode — characters are appended at the cursor as typed.
268 Insert,
269 /// Visual mode — reserved for future selection support.
270 Visual,
271 }
272
273 impl VimMode {}
274
275 /// Message queued while the engine is busy.
276 #[derive(Debug, Clone, PartialEq, Eq)]
277 pub struct QueuedMessage {
278 pub display: String,
279 pub skill_instruction: Option<String>,
280 pub skill_provenance: Option<crate::skills::SkillProvenance>,
281 /// True once this turn has been painted into `history` as `HistoryCell::User`.
282 /// Queue/offline submit echoes before the model runs; Immediate prepare skips
283 /// a second paint when this is set so drained queued turns do not double.
284 pub history_echoed: bool,
285 }
286
287 /// The message the current turn was dispatched with, and the transcript cell
288 /// that shows it. When the engine reports the turn was never sent (a key
289 /// rejected before any model output), this is what goes back in the composer,
290 /// skill included, and the bubble that comes out of the transcript (#6566).
291 #[derive(Debug, Clone)]
292 pub struct UnansweredSubmission {
293 pub message: QueuedMessage,
294 pub history_cell: usize,
295 }
296
297 /// A steer handed to the engine that the engine has not yet recorded.
298 ///
299 /// Live-only, and deliberately not in `api_messages`: `EngineHandle::steer`
300 /// succeeding means the channel took the text, not that a turn accepted it.
301 /// The engine commits a steer at a step boundary and drops one whose turn has
302 /// already moved on, so painting a settled transcript cell at send time
303 /// produced a cell that could sit above the work it followed, or survive
304 /// forever for a steer the model never saw (#6190). It becomes a real cell
305 /// when the engine's own record shows it, and a "could not send" receipt when
306 /// the turn ends without it.
307 #[derive(Debug, Clone)]
308 pub struct InflightSteer {
309 /// The composed message, carried so acceptance can paint the same cell
310 /// (including the queue-time echo it may already own).
311 pub message: QueuedMessage,
312 /// Exactly what was handed to `EngineHandle::steer`. The engine records
313 /// this as the first text block of the accepted user message, which is
314 /// what acceptance matches on.
315 pub content: String,
316 /// `api_messages.len()` when the steer was sent — the lower bound for the
317 /// acceptance search, so an identical earlier message cannot claim it.
318 pub sent_after_index: usize,
319 /// Held until acceptance knows the message index to anchor them to.
320 pub references: Vec<codewhale_core::ContextReference>,
321 }
322
323 /// Prefix for the bounded, tool-less model turn produced by `/workflow`.
324 ///
325 /// The marker travels with the queued message so a draft that waits behind an
326 /// active turn keeps the same no-tools policy when it is eventually sent.
327 pub(crate) const WORKFLOW_DRAFT_INSTRUCTION_PREFIX: &str = "[codewhale.workflow-draft.v1]";
328
329 /// How a freshly-typed user input should be sent.
330 ///
331 /// Picked by [`App::decide_composer_submit`] when the user submits a
332 /// non-empty composer.
333 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
334 pub enum SubmitDisposition {
335 /// Engine idle and online: send immediately.
336 Immediate,
337 /// Park on `queued_messages` (offline, or engine busy — #382).
338 Queue,
339 /// Amend the active turn immediately (#382).
340 Steer,
341 /// Park on `queued_messages` for dispatch after TurnComplete.
342 /// Legacy path; #382 unified busy states under `Queue`.
343 #[expect(dead_code)]
344 QueueFollowUp,
345 }
346
347 /// Enter-shaped gestures understood by the composer state machine.
348 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
349 pub enum ComposerSubmitChord {
350 Enter,
351 CtrlEnter,
352 }
353
354 /// The complete result of resolving a submit gesture against composer state.
355 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
356 pub enum ComposerSubmitAction {
357 Submit(SubmitDisposition),
358 /// Promote the oldest already-queued message into the active turn.
359 SendQueuedNow,
360 Noop,
361 }
362
363 /// Detailed tool payload attached to a history cell.
364 #[derive(Debug, Clone)]
365 pub struct ToolDetailRecord {
366 pub tool_id: String,
367 pub tool_name: String,
368 pub input: Value,
369 pub output: Option<String>,
370 }
371
372 /// Lightweight task view for sidebar rendering.
373 #[derive(Debug, Clone, PartialEq, Eq)]
374 pub struct TaskPanelEntry {
375 pub id: String,
376 pub status: String,
377 pub prompt_summary: String,
378 pub duration_ms: Option<u64>,
379 pub kind: TaskPanelEntryKind,
380 pub stale: bool,
381 pub elapsed_since_output_ms: Option<u64>,
382 pub owner_agent_id: Option<String>,
383 pub owner_agent_name: Option<String>,
384 /// #2889: structured current activity for the Work panel.
385 pub current_tool: Option<String>,
386 pub role: Option<String>,
387 pub files_touched: u32,
388 /// A finished shell's exit code. `None` while running, and for durable
389 /// tasks.
390 pub exit_code: Option<i64>,
391 }
392
393 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
394 pub enum TaskPanelEntryKind {
395 Background,
396 Shell,
397 }
398
399 impl QueuedMessage {
400 pub fn new(display: String, skill_instruction: Option<String>) -> Self {
401 Self {
402 display,
403 skill_instruction,
404 skill_provenance: None,
405 history_echoed: false,
406 }
407 }
408
409 #[must_use]
410 pub fn with_skill_provenance(
411 mut self,
412 provenance: Option<crate::skills::SkillProvenance>,
413 ) -> Self {
414 self.skill_provenance = provenance;
415 self
416 }
417
418 #[must_use]
419 pub(crate) fn is_workflow_draft(&self) -> bool {
420 self.skill_instruction
421 .as_deref()
422 .is_some_and(|instruction| instruction.starts_with(WORKFLOW_DRAFT_INSTRUCTION_PREFIX))
423 }
424
425 #[allow(dead_code)] // Tests and queue helpers use the display-only form; send path resolves @mentions.
426 pub fn content(&self) -> String {
427 if let Some(skill_instruction) = self.skill_instruction.as_ref() {
428 format!(
429 "{skill_instruction}\n\n---\n\nUser request: {}",
430 self.display
431 )
432 } else {
433 self.display.clone()
434 }
435 }
436 }
437
438 // === Actions ===
439
440 /// A typed goal-control request accepted by the TUI and delivered to the
441 /// engine mailbox. Keeping this separate from transcript text lets the host
442 /// persist, retry, and reconcile controls without impersonating the user.
443 #[derive(Debug, Clone, PartialEq, Eq)]
444 pub(crate) enum GoalControlIntent {
445 SetStatus {
446 status: crate::tools::goal::GoalStatus,
447 clear: bool,
448 },
449 SetObjective {
450 objective: String,
451 token_budget: Option<u32>,
452 },
453 }
454
455 /// One accepted goal control waiting for its authoritative GoalUpdated
456 /// receipt. `dispatched` distinguishes mailbox backpressure from an operation
457 /// already ordered in the engine channel; both remain pending until receipt.
458 #[derive(Debug, Clone, PartialEq, Eq)]
459 pub(crate) struct PendingGoalControl {
460 pub goal_id: Option<String>,
461 pub intent: GoalControlIntent,
462 pub dispatched: bool,
463 }
464
465 /// Which screen the TUI paints on.
466 ///
467 /// This is the single source of truth for the alternate screen: `App` stores
468 /// the mode and derives `use_alt_screen()` from it, so a switch cannot leave
469 /// the flag and the live terminal disagreeing.
470 #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
471 pub enum ScreenMode {
472 /// Alternate screen buffer. The TUI owns the whole terminal; the host
473 /// scrollback is preserved but unreachable until the session exits.
474 #[default]
475 Fullscreen,
476 /// A ratatui inline viewport the full height of the terminal, with no
477 /// alternate screen. The shell's scrollback stays intact and scrollable
478 /// after exit, at the cost of the TUI no longer owning a private buffer.
479 Inline,
480 }
481
482 impl ScreenMode {
483 /// Whether this mode runs on the alternate screen buffer.
484 #[must_use]
485 pub const fn uses_alt_screen(self) -> bool {
486 matches!(self, Self::Fullscreen)
487 }
488
489 /// Whether mouse capture is on for this screen, given the user's
490 /// preference. This is the one rule: startup and the `/fullscreen` ·
491 /// `/inline` switch both ask it. Capture needs the alternate screen —
492 /// inline mode exists so the terminal owns selection and scrollback.
493 #[must_use]
494 pub const fn mouse_capture(self, preferred: bool) -> bool {
495 self.uses_alt_screen() && preferred
496 }
497
498 /// Canonical name, as `/screen` prints it and `parse` accepts it.
499 #[must_use]
500 pub const fn as_str(self) -> &'static str {
501 match self {
502 Self::Fullscreen => "fullscreen",
503 Self::Inline => "inline",
504 }
505 }
506
507 /// Parse a user-supplied mode word, including the legacy
508 /// `tui.alternate_screen` vocabulary (`auto`/`always` → fullscreen,
509 /// `never` → inline) so the existing config key keeps selecting a real
510 /// behaviour instead of being parsed and ignored.
511 #[must_use]
512 pub fn parse(value: &str) -> Option<Self> {
513 match value.trim().to_ascii_lowercase().as_str() {
514 "fullscreen" | "full" | "alt" | "alt-screen" | "auto" | "always" => {
515 Some(Self::Fullscreen)
516 }
517 "inline" | "scrollback" | "never" | "off" => Some(Self::Inline),
518 _ => None,
519 }
520 }
521 }
522
523 /// Actions emitted by the UI event loop.
524 #[derive(Debug, Clone, PartialEq)]
525 pub enum AppAction {
526 SetWorkspaceTrust {
527 trusted: bool,
528 save: bool,
529 },
530 Quit,
531 #[allow(dead_code)] // For explicit /load command
532 LoadSession(PathBuf),
533 RemoteControl(crate::remote_control::RemoteControlAction),
534 SyncSession {
535 session_id: Option<String>,
536 messages: Vec<Message>,
537 system_prompt: Option<SystemPrompt>,
538 model: String,
539 workspace: PathBuf,
540 mode: AppMode,
541 },
542 OpenConfigView,
543 /// Open this workspace's `.codewhale/hooks.toml` in `$EDITOR`, creating
544 /// it from a commented template first when it does not exist yet.
545 EditProjectHooks,
546 /// Open the native git worktree manager.
547 OpenWorktreeManager,
548 /// Open the `/model` two-pane picker (Pro/Flash + Off/High/Max).
549 OpenModelPicker,
550 /// Open the `/provider` picker modal — DeepSeek / NVIDIA NIM / OpenRouter
551 /// / Novita with inline API-key prompt for un-configured providers (#52).
552 OpenProviderPicker,
553 /// Open the `/provider` picker in setup/catalog mode, optionally focused on
554 /// a built-in provider that needs credentials before first use.
555 OpenProviderSetup {
556 provider: Option<codewhale_config::ProviderId>,
557 },
558 /// Open the named, keyless DS4 local-runtime preset for review and save.
559 OpenDs4Setup,
560 /// Run the xAI/Grok device-code flow with the TUI temporarily suspended.
561 StartXaiDeviceLogin,
562 /// Run native ChatGPT PKCE sign-in with the TUI temporarily suspended.
563 StartChatgptPkceLogin,
564 StartChatgptRevoke,
565 /// Open the `/mode` picker modal for Act / Plan / Operate.
566 OpenModePicker,
567 /// Switch the live terminal between `/fullscreen` and `/inline`. Handled
568 /// where the ratatui `Terminal` lives, because stock ratatui cannot change
569 /// an existing terminal's viewport — the switch rebuilds it behind a probe.
570 SetScreenMode(ScreenMode),
571 /// Refresh the engine prompt after the UI operating mode changes.
572 ModeChanged(AppMode),
573 /// Synchronize a saved top-level approval policy into the live Config,
574 /// then refresh the engine prompt from the App's updated permission mode.
575 ApprovalPolicyPersisted {
576 policy: Option<String>,
577 },
578 /// Reload the active user permission rules after `/permissions` safely
579 /// removes one from the sibling `permissions.toml`.
580 PermissionRulesChanged,
581 /// Rebuild the engine's Skill/MCP catalogue from the App's newly replaced
582 /// immutable plugin snapshot after trust, enable, revoke, or reload.
583 PluginRegistryChanged,
584 /// Open the `/statusline` multi-select picker for footer items.
585 OpenStatusPicker,
586 /// Open the `/feedback` picker for GitHub issue/security destinations.
587 OpenFeedbackPicker,
588 /// Read/review a scoped immutable issue draft without parking the UI.
589 ReviewIssueReport {
590 id: String,
591 change: Option<String>,
592 },
593 /// Open the `/theme` picker modal with live preview of every preset.
594 OpenThemePicker,
595 /// Open the `/skills manage` manager — audit inventory + owned mutations.
596 OpenSkillsManager,
597 /// Open the `/workflows` run dashboard — live and retained workflow runs.
598 OpenWorkflowsManager,
599 /// Open the unified, read-only extensions inventory on a specific tab.
600 OpenExtensions {
601 tab: crate::tui::views::extensions::ExtensionsTab,
602 },
603 /// Open `/fleet` — the saved named-Fleet list (the primary Fleet surface).
604 OpenFleetList,
605 /// Open the `/fleet` roster — the saved-party view of the agent team.
606 OpenFleetRoster,
607 /// Open the selected v2 Fleet editor, or legacy profile setup when no
608 /// named Fleet is selected.
609 OpenFleetSetup,
610 /// `/fleet add`: validate the provider against the live config, write
611 /// the member rows, and mark the engine roster stale.
612 FleetAddModel {
613 provider: String,
614 model: String,
615 roles: Vec<String>,
616 },
617 /// `/fleet remove`: drop every member row pinning the route and mark the
618 /// engine roster stale.
619 FleetRemoveModel {
620 provider: String,
621 model: String,
622 },
623 /// Open the `/hotbar` setup wizard.
624 OpenHotbarSetup,
625 /// Open the constitution-first `/setup` wizard shell.
626 OpenSetupWizard,
627 /// Open the constitution-first `/setup` wizard at a specific step.
628 OpenSetupWizardAt {
629 step: codewhale_config::SetupStep,
630 },
631 /// Record that the bundled/default constitution should be used.
632 UseBundledConstitution,
633 /// Open the exact effective base-prompt preview for the next turn (#3928).
634 ///
635 /// Handled where the session config lives, so the preview is built by the
636 /// same function the dispatch path uses. Human-only: it issues no provider
637 /// request and expands no tool catalog.
638 PreviewEffectiveBasePrompt,
639 /// Disable the Hotbar: persist `hotbar = []` and clear the live slots.
640 DisableHotbar,
641 /// Restore the default recommended Hotbar slots: remove the `hotbar` key so
642 /// the resolver falls back to the built-in defaults.
643 RestoreHotbarDefaults,
644 /// Open an external URL in the system browser.
645 OpenExternalUrl {
646 url: String,
647 label: String,
648 },
649 /// Run an extension command in the extension host (`/name input`). The UI
650 /// event loop awaits it, then shows its text and/or submits its prompt
651 /// as the user's next message.
652 RunExtensionCommand {
653 command: crate::extension_host::command::ExtensionCommandRef,
654 name: String,
655 input: String,
656 },
657 /// Send a message to the AI (normal chat mode).
658 SendMessage(String),
659 /// Same-session rollback. A retry is admitted only after the Engine
660 /// acknowledges this history and its persisted snapshot is durable.
661 ConversationUndo {
662 sync: codewhale_command_contract::facets::SessionSyncPayload,
663 retry_input: Option<String>,
664 /// `retry_input` is the text of a pending `/edit`, already taken from
665 /// the composer. If the rollback is refused it must go back there, with
666 /// edit mode re-armed, or the user's revision is lost.
667 edit_replacement: bool,
668 },
669 /// Send a built-in Workflow planning turn with separate user-visible text
670 /// and bounded runtime guidance. Draft instructions carry a typed marker
671 /// that makes the dispatch path expose no tools for that turn.
672 WorkflowInstruction {
673 display: String,
674 instruction: String,
675 },
676 /// Cancel a running sub-agent through the engine manager.
677 CancelSubAgent {
678 agent_id: String,
679 },
680 /// Update the runtime goal status (`/goal pause|resume|clear|…`) without
681 /// dispatching a model turn. The UI layer translates this into
682 /// `Op::SetGoalStatus`.
683 SetGoalStatus {
684 status: crate::tools::goal::GoalStatus,
685 clear: bool,
686 },
687 /// Set or replace the goal objective (`/goal <objective>`). The engine
688 /// owns the goal and starts the first goal turn itself as runtime
689 /// steering; the objective is never sent as a raw user message.
690 SetGoalObjective {
691 objective: String,
692 token_budget: Option<u32>,
693 },
694 ListSubAgents,
695 /// Ask the engine to describe the exact next outbound request
696 /// (`/preview-request`, #1004). The engine is the authority: only it can
697 /// rebuild the current tool catalog, MCP state, gates, and resolved route.
698 PreviewOutboundRequest {
699 /// Render the manifest as JSON instead of the human-readable table.
700 json: bool,
701 /// Render the exact base prompt only. Never includes runtime/system layers.
702 base_prompt_only: bool,
703 /// Optional text used only to resolve `auto` reasoning/routing. Never
704 /// added to the conversation and never sent to a provider.
705 hypothetical_prompt: Option<String>,
706 },
707 /// Show bounded read-only text without copying it into transcript history.
708 OpenTextPager {
709 title: String,
710 content: String,
711 },
712 /// Review a host-generated command; the pager carries its exact token
713 /// through explicit confirmation and the normal command dispatcher.
714 OpenCommandReview {
715 title: String,
716 content: String,
717 command: String,
718 },
719 /// Router setup (`/router`, `/model router`, #6525): open the view, test a
720 /// preset with one routing call, or save one to `[auto.router]`.
721 RouterSetup {
722 request: crate::tui::views::router_setup::RouterRequest,
723 },
724 /// Live remaining-credit lookup for prepaid providers (`/balance`).
725 FetchBalance,
726 FetchModels,
727 /// Force a Models.dev live-catalog refresh into ProviderLake (#4187).
728 RefreshModelsDevCatalog,
729 CacheWarmup,
730 /// Switch the active LLM backend (DeepSeek vs NVIDIA NIM) without
731 /// restarting the process. The runtime rebuilds its API client from
732 /// the updated config. `model` overrides the post-switch model
733 /// (already normalized but not yet provider-prefixed).
734 SwitchProvider {
735 provider: codewhale_config::ProviderId,
736 model: Option<String>,
737 },
738 /// Switch provider+model through the same apply path as a `/model` route
739 /// row. Used by Hotbar route slots so dispatch does not hand-mutate config.
740 SwitchModelRoute {
741 identity: crate::config::ProviderIdentity,
742 model: String,
743 },
744 UpdateCompaction(CompactionConfig),
745 UpdateStreamChunkTimeout(u64),
746 UpdateSubagentRuntimeConfig {
747 enabled: bool,
748 max_subagents: usize,
749 launch_concurrency: usize,
750 max_spawn_depth: u32,
751 api_timeout_secs: u64,
752 heartbeat_timeout_secs: u64,
753 },
754 /// Apply `/config search.provider` to the live Config and engine.
755 UpdateSearchProvider {
756 provider: crate::config::SearchProvider,
757 },
758 /// Apply `/config prompt_suggestion` to the live Config.
759 UpdatePromptSuggestion {
760 enabled: bool,
761 },
762 /// Apply one `/config notifications` scalar to the live Config.
763 UpdateNotification {
764 update: crate::config::NotificationConfigUpdate,
765 },
766 /// Enable or disable the background advisor watcher for this session (#3982).
767 SetAdvisorEnabled {
768 enabled: bool,
769 },
770 /// Open the live transcript overlay through a terminal-safe command path.
771 OpenLiveTranscript,
772 /// Open the whole-turn inspector (Ctrl+Alt+O, /turn inspect).
773 OpenTurnInspector,
774 OpenContextInspector,
775 CompactContext {
776 /// Optional user focus from `/compact <focus>`, forwarded into the
777 /// successor-brief summary prompt.
778 focus: Option<String>,
779 },
780 PurgeContext,
781 TaskAdd {
782 prompt: String,
783 },
784 TaskList,
785 TaskShow {
786 id: String,
787 },
788 TaskCancel {
789 id: String,
790 },
791 Automation(AutomationAction),
792 ShellJob(ShellJobAction),
793 Mcp(McpUiAction),
794 /// Switch to a different config profile without restarting.
795 SwitchProfile {
796 /// Profile name to load.
797 profile: String,
798 },
799 /// Switch the workspace used by tools, hooks, tasks, and session metadata.
800 SwitchWorkspace {
801 workspace: PathBuf,
802 },
803 /// Record from the microphone and route the transcription into the
804 /// composer (or auto-send it). Emitted by `/voice` and the voice hotbar
805 /// action; handled in the UI event loop where the live `Config` supplies
806 /// provider credentials.
807 VoiceCapture,
808 /// Upload an already-rendered, redacted session page as a secret gist.
809 /// Emitted only by `/share confirm`.
810 ShareSession {
811 html: String,
812 },
813 }
814
815 #[derive(Debug, Clone, PartialEq, Eq)]
816 pub enum AutomationAction {
817 /// Open the automations room, optionally focused on one id.
818 Open {
819 focus: Option<String>,
820 },
821 List,
822 Show(String),
823 Pause(String),
824 Resume(String),
825 Delete {
826 id: String,
827 confirmation: Option<String>,
828 },
829 Run(String),
830 }
831
832 #[derive(Debug, Clone, PartialEq, Eq)]
833 pub enum ShellJobAction {
834 List,
835 Show {
836 id: String,
837 },
838 Poll {
839 id: String,
840 wait: bool,
841 },
842 SendStdin {
843 id: String,
844 input: String,
845 close: bool,
846 },
847 Cancel {
848 id: String,
849 },
850 CancelAll,
851 }
852
853 #[derive(Debug, Clone, PartialEq, Eq)]
854 pub enum McpUiAction {
855 Show,
856 Init {
857 force: bool,
858 },
859 AddStdio {
860 name: String,
861 command: String,
862 args: Vec<String>,
863 },
864 AddHttp {
865 name: String,
866 url: String,
867 transport: Option<String>,
868 },
869 Enable {
870 name: String,
871 },
872 Disable {
873 name: String,
874 },
875 Remove {
876 name: String,
877 },
878 Login {
879 name: String,
880 scopes: Vec<String>,
881 },
882 Logout {
883 name: String,
884 },
885 /// Retry one failed/timed-out server through the engine-owned live pool.
886 Retry {
887 name: String,
888 },
889 /// List consent-gated external MCP import candidates with provenance.
890 ImportList,
891 /// Approve importing one discovered external server into user mcp.json.
892 ImportApprove {
893 name: String,
894 },
895 /// Decline an external candidate (durable until source content changes).
896 ImportDecline {
897 name: String,
898 },
899 Validate,
900 /// Report this server's last observed state without starting a new pool.
901 Diagnose {
902 name: String,
903 },
904 Reload,
905 }
906
906 lines RUST