返回 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: ApiProvider,
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::plugins::types::PluginAuthority>,
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 /// A steer handed to the engine that the engine has not yet recorded.
288 ///
289 /// Live-only, and deliberately not in `api_messages`: `EngineHandle::steer`
290 /// succeeding means the channel took the text, not that a turn accepted it.
291 /// The engine commits a steer at a step boundary and drops one whose turn has
292 /// already moved on, so painting a settled transcript cell at send time
293 /// produced a cell that could sit above the work it followed, or survive
294 /// forever for a steer the model never saw (#6190). It becomes a real cell
295 /// when the engine's own record shows it, and a "could not send" receipt when
296 /// the turn ends without it.
297 #[derive(Debug, Clone)]
298 pub struct InflightSteer {
299 /// The composed message, carried so acceptance can paint the same cell
300 /// (including the queue-time echo it may already own).
301 pub message: QueuedMessage,
302 /// Exactly what was handed to `EngineHandle::steer`. The engine records
303 /// this as the first text block of the accepted user message, which is
304 /// what acceptance matches on.
305 pub content: String,
306 /// `api_messages.len()` when the steer was sent — the lower bound for the
307 /// acceptance search, so an identical earlier message cannot claim it.
308 pub sent_after_index: usize,
309 /// Held until acceptance knows the message index to anchor them to.
310 pub references: Vec<codewhale_core::ContextReference>,
311 }
312
313 /// Prefix for the bounded, tool-less model turn produced by `/workflow`.
314 ///
315 /// The marker travels with the queued message so a draft that waits behind an
316 /// active turn keeps the same no-tools policy when it is eventually sent.
317 pub(crate) const WORKFLOW_DRAFT_INSTRUCTION_PREFIX: &str = "[codewhale.workflow-draft.v1]";
318
319 /// How a freshly-typed user input should be sent.
320 ///
321 /// Picked by [`App::decide_composer_submit`] when the user submits a
322 /// non-empty composer.
323 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
324 pub enum SubmitDisposition {
325 /// Engine idle and online: send immediately.
326 Immediate,
327 /// Park on `queued_messages` (offline, or engine busy — #382).
328 Queue,
329 /// Amend the active turn immediately (#382).
330 Steer,
331 /// Park on `queued_messages` for dispatch after TurnComplete.
332 /// Legacy path; #382 unified busy states under `Queue`.
333 #[expect(dead_code)]
334 QueueFollowUp,
335 }
336
337 /// Enter-shaped gestures understood by the composer state machine.
338 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
339 pub enum ComposerSubmitChord {
340 Enter,
341 CtrlEnter,
342 }
343
344 /// The complete result of resolving a submit gesture against composer state.
345 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
346 pub enum ComposerSubmitAction {
347 Submit(SubmitDisposition),
348 /// Promote the oldest already-queued message into the active turn.
349 SendQueuedNow,
350 Noop,
351 }
352
353 /// Detailed tool payload attached to a history cell.
354 #[derive(Debug, Clone)]
355 pub struct ToolDetailRecord {
356 pub tool_id: String,
357 pub tool_name: String,
358 pub input: Value,
359 pub output: Option<String>,
360 }
361
362 /// Lightweight task view for sidebar rendering.
363 #[derive(Debug, Clone, PartialEq, Eq)]
364 pub struct TaskPanelEntry {
365 pub id: String,
366 pub status: String,
367 pub prompt_summary: String,
368 pub duration_ms: Option<u64>,
369 pub kind: TaskPanelEntryKind,
370 pub stale: bool,
371 pub elapsed_since_output_ms: Option<u64>,
372 pub owner_agent_id: Option<String>,
373 pub owner_agent_name: Option<String>,
374 /// #2889: structured current activity for the Work panel.
375 pub current_tool: Option<String>,
376 pub role: Option<String>,
377 pub files_touched: u32,
378 }
379
380 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
381 pub enum TaskPanelEntryKind {
382 Background,
383 }
384
385 impl QueuedMessage {
386 pub fn new(display: String, skill_instruction: Option<String>) -> Self {
387 Self {
388 display,
389 skill_instruction,
390 skill_provenance: None,
391 history_echoed: false,
392 }
393 }
394
395 #[must_use]
396 pub fn with_skill_provenance(
397 mut self,
398 provenance: Option<crate::plugins::types::PluginAuthority>,
399 ) -> Self {
400 self.skill_provenance = provenance;
401 self
402 }
403
404 #[must_use]
405 pub(crate) fn is_workflow_draft(&self) -> bool {
406 self.skill_instruction
407 .as_deref()
408 .is_some_and(|instruction| instruction.starts_with(WORKFLOW_DRAFT_INSTRUCTION_PREFIX))
409 }
410
411 #[allow(dead_code)] // Tests and queue helpers use the display-only form; send path resolves @mentions.
412 pub fn content(&self) -> String {
413 if let Some(skill_instruction) = self.skill_instruction.as_ref() {
414 format!(
415 "{skill_instruction}\n\n---\n\nUser request: {}",
416 self.display
417 )
418 } else {
419 self.display.clone()
420 }
421 }
422 }
423
424 // === Actions ===
425
426 /// A typed goal-control request accepted by the TUI and delivered to the
427 /// engine mailbox. Keeping this separate from transcript text lets the host
428 /// persist, retry, and reconcile controls without impersonating the user.
429 #[derive(Debug, Clone, PartialEq, Eq)]
430 pub(crate) enum GoalControlIntent {
431 SetStatus {
432 status: crate::tools::goal::GoalStatus,
433 clear: bool,
434 },
435 SetObjective {
436 objective: String,
437 token_budget: Option<u32>,
438 },
439 }
440
441 /// One accepted goal control waiting for its authoritative GoalUpdated
442 /// receipt. `dispatched` distinguishes mailbox backpressure from an operation
443 /// already ordered in the engine channel; both remain pending until receipt.
444 #[derive(Debug, Clone, PartialEq, Eq)]
445 pub(crate) struct PendingGoalControl {
446 pub goal_id: Option<String>,
447 pub intent: GoalControlIntent,
448 pub dispatched: bool,
449 }
450
451 /// Which screen the TUI paints on.
452 ///
453 /// This is the single source of truth for the alternate screen: `App` stores
454 /// the mode and derives `use_alt_screen()` from it, so a switch cannot leave
455 /// the flag and the live terminal disagreeing.
456 #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
457 pub enum ScreenMode {
458 /// Alternate screen buffer. The TUI owns the whole terminal; the host
459 /// scrollback is preserved but unreachable until the session exits.
460 #[default]
461 Fullscreen,
462 /// A ratatui inline viewport the full height of the terminal, with no
463 /// alternate screen. The shell's scrollback stays intact and scrollable
464 /// after exit, at the cost of the TUI no longer owning a private buffer.
465 Inline,
466 }
467
468 impl ScreenMode {
469 /// Whether this mode runs on the alternate screen buffer.
470 #[must_use]
471 pub const fn uses_alt_screen(self) -> bool {
472 matches!(self, Self::Fullscreen)
473 }
474
475 /// Whether mouse capture is on for this screen, given the user's
476 /// preference. This is the one rule: startup and the `/fullscreen` ·
477 /// `/inline` switch both ask it. Capture needs the alternate screen —
478 /// inline mode exists so the terminal owns selection and scrollback.
479 #[must_use]
480 pub const fn mouse_capture(self, preferred: bool) -> bool {
481 self.uses_alt_screen() && preferred
482 }
483
484 /// Canonical name, as `/screen` prints it and `parse` accepts it.
485 #[must_use]
486 pub const fn as_str(self) -> &'static str {
487 match self {
488 Self::Fullscreen => "fullscreen",
489 Self::Inline => "inline",
490 }
491 }
492
493 /// Parse a user-supplied mode word, including the legacy
494 /// `tui.alternate_screen` vocabulary (`auto`/`always` → fullscreen,
495 /// `never` → inline) so the existing config key keeps selecting a real
496 /// behaviour instead of being parsed and ignored.
497 #[must_use]
498 pub fn parse(value: &str) -> Option<Self> {
499 match value.trim().to_ascii_lowercase().as_str() {
500 "fullscreen" | "full" | "alt" | "alt-screen" | "auto" | "always" => {
501 Some(Self::Fullscreen)
502 }
503 "inline" | "scrollback" | "never" | "off" => Some(Self::Inline),
504 _ => None,
505 }
506 }
507 }
508
509 /// Actions emitted by the UI event loop.
510 #[derive(Debug, Clone, PartialEq)]
511 pub enum AppAction {
512 Quit,
513 #[allow(dead_code)] // For explicit /load command
514 LoadSession(PathBuf),
515 RemoteControl(crate::remote_control::RemoteControlAction),
516 SyncSession {
517 session_id: Option<String>,
518 messages: Vec<Message>,
519 system_prompt: Option<SystemPrompt>,
520 model: String,
521 workspace: PathBuf,
522 mode: AppMode,
523 },
524 OpenConfigView,
525 /// Open this workspace's `.codewhale/hooks.toml` in `$EDITOR`, creating
526 /// it from a commented template first when it does not exist yet.
527 EditProjectHooks,
528 /// Open the native git worktree manager.
529 OpenWorktreeManager,
530 /// Open the `/model` two-pane picker (Pro/Flash + Off/High/Max).
531 OpenModelPicker,
532 /// Open the `/provider` picker modal — DeepSeek / NVIDIA NIM / OpenRouter
533 /// / Novita with inline API-key prompt for un-configured providers (#52).
534 OpenProviderPicker,
535 /// Open the `/provider` picker in setup/catalog mode, optionally focused on
536 /// a built-in provider that needs credentials before first use.
537 OpenProviderSetup {
538 provider: Option<ApiProvider>,
539 },
540 /// Open the named, keyless DS4 local-runtime preset for review and save.
541 OpenDs4Setup,
542 /// Run the xAI/Grok device-code flow with the TUI temporarily suspended.
543 StartXaiDeviceLogin,
544 /// Run native ChatGPT PKCE sign-in with the TUI temporarily suspended.
545 StartChatgptPkceLogin,
546 StartChatgptRevoke,
547 /// Open the `/mode` picker modal for Act / Plan / Operate.
548 OpenModePicker,
549 /// Switch the live terminal between `/fullscreen` and `/inline`. Handled
550 /// where the ratatui `Terminal` lives, because stock ratatui cannot change
551 /// an existing terminal's viewport — the switch rebuilds it behind a probe.
552 SetScreenMode(ScreenMode),
553 /// Refresh the engine prompt after the UI operating mode changes.
554 ModeChanged(AppMode),
555 /// Synchronize a saved top-level approval policy into the live Config,
556 /// then refresh the engine prompt from the App's updated permission mode.
557 ApprovalPolicyPersisted {
558 policy: Option<String>,
559 },
560 /// Reload the active user permission rules after `/permissions` safely
561 /// removes one from the sibling `permissions.toml`.
562 PermissionRulesChanged,
563 /// Rebuild the engine's Skill/MCP catalogue from the App's newly replaced
564 /// immutable plugin snapshot after trust, enable, revoke, or reload.
565 PluginRegistryChanged,
566 /// Open the `/statusline` multi-select picker for footer items.
567 OpenStatusPicker,
568 /// Open the `/feedback` picker for GitHub issue/security destinations.
569 OpenFeedbackPicker,
570 /// Open the `/theme` picker modal with live preview of every preset.
571 OpenThemePicker,
572 /// Open the `/skills manage` manager — audit inventory + owned mutations.
573 OpenSkillsManager,
574 /// Open the `/workflows` run dashboard — live and retained workflow runs.
575 OpenWorkflowsManager,
576 /// Open the unified, read-only extensions inventory on a specific tab.
577 OpenExtensions {
578 tab: crate::tui::views::extensions::ExtensionsTab,
579 },
580 /// Open `/fleet` — the saved named-Fleet list (the primary Fleet surface).
581 OpenFleetList,
582 /// Open the `/fleet` roster — the saved-party view of the agent team.
583 OpenFleetRoster,
584 /// Open the selected v2 Fleet editor, or legacy profile setup when no
585 /// named Fleet is selected.
586 OpenFleetSetup,
587 /// `/fleet add`: validate the provider against the live config, write
588 /// the member rows, and mark the engine roster stale.
589 FleetAddModel {
590 provider: String,
591 model: String,
592 roles: Vec<String>,
593 },
594 /// `/fleet remove`: drop every member row pinning the route and mark the
595 /// engine roster stale.
596 FleetRemoveModel {
597 provider: String,
598 model: String,
599 },
600 /// Open the `/hotbar` setup wizard.
601 OpenHotbarSetup,
602 /// Open the constitution-first `/setup` wizard shell.
603 OpenSetupWizard,
604 /// Open the constitution-first `/setup` wizard at a specific step.
605 OpenSetupWizardAt {
606 step: codewhale_config::SetupStep,
607 },
608 /// Record that the bundled/default constitution should be used.
609 UseBundledConstitution,
610 /// Open the exact effective base-prompt preview for the next turn (#3928).
611 ///
612 /// Handled where the session config lives, so the preview is built by the
613 /// same function the dispatch path uses. Human-only: it issues no provider
614 /// request and expands no tool catalog.
615 PreviewEffectiveBasePrompt,
616 /// Disable the Hotbar: persist `hotbar = []` and clear the live slots.
617 DisableHotbar,
618 /// Restore the default recommended Hotbar slots: remove the `hotbar` key so
619 /// the resolver falls back to the built-in defaults.
620 RestoreHotbarDefaults,
621 /// Open an external URL in the system browser.
622 OpenExternalUrl {
623 url: String,
624 label: String,
625 },
626 /// Send a message to the AI (normal chat mode).
627 SendMessage(String),
628 /// Send a built-in Workflow planning turn with separate user-visible text
629 /// and bounded runtime guidance. Draft instructions carry a typed marker
630 /// that makes the dispatch path expose no tools for that turn.
631 WorkflowInstruction {
632 display: String,
633 instruction: String,
634 },
635 /// Cancel a running sub-agent through the engine manager.
636 CancelSubAgent {
637 agent_id: String,
638 },
639 /// Update the runtime goal status (`/goal pause|resume|clear|…`) without
640 /// dispatching a model turn. The UI layer translates this into
641 /// `Op::SetGoalStatus`.
642 SetGoalStatus {
643 status: crate::tools::goal::GoalStatus,
644 clear: bool,
645 },
646 /// Set or replace the goal objective (`/goal <objective>`). The engine
647 /// owns the goal and starts the first goal turn itself as runtime
648 /// steering; the objective is never sent as a raw user message.
649 SetGoalObjective {
650 objective: String,
651 token_budget: Option<u32>,
652 },
653 ListSubAgents,
654 /// Ask the engine to describe the exact next outbound request
655 /// (`/preview-request`, #1004). The engine is the authority: only it can
656 /// rebuild the current tool catalog, MCP state, gates, and resolved route.
657 PreviewOutboundRequest {
658 /// Render the manifest as JSON instead of the human-readable table.
659 json: bool,
660 /// Render the exact base prompt only. Never includes runtime/system layers.
661 base_prompt_only: bool,
662 /// Optional text used only to resolve `auto` reasoning/routing. Never
663 /// added to the conversation and never sent to a provider.
664 hypothetical_prompt: Option<String>,
665 },
666 /// Show bounded read-only text without copying it into transcript history.
667 OpenTextPager {
668 title: String,
669 content: String,
670 },
671 /// Review a host-generated command; the pager carries its exact token
672 /// through explicit confirmation and the normal command dispatcher.
673 OpenCommandReview {
674 title: String,
675 content: String,
676 command: String,
677 },
678 /// Live remaining-credit lookup for prepaid providers (`/balance`).
679 FetchBalance,
680 FetchModels,
681 /// Force a Models.dev live-catalog refresh into ProviderLake (#4187).
682 RefreshModelsDevCatalog,
683 CacheWarmup,
684 /// Switch the active LLM backend (DeepSeek vs NVIDIA NIM) without
685 /// restarting the process. The runtime rebuilds its API client from
686 /// the updated config. `model` overrides the post-switch model
687 /// (already normalized but not yet provider-prefixed).
688 SwitchProvider {
689 provider: ApiProvider,
690 model: Option<String>,
691 },
692 /// Switch provider+model through the same apply path as a `/model` route
693 /// row. Used by Hotbar route slots so dispatch does not hand-mutate config.
694 SwitchModelRoute {
695 provider: ApiProvider,
696 model: String,
697 },
698 UpdateCompaction(CompactionConfig),
699 UpdateStreamChunkTimeout(u64),
700 UpdateSubagentRuntimeConfig {
701 enabled: bool,
702 max_subagents: usize,
703 launch_concurrency: usize,
704 max_spawn_depth: u32,
705 api_timeout_secs: u64,
706 heartbeat_timeout_secs: u64,
707 },
708 /// Apply `/config search.provider` to the live Config and engine.
709 UpdateSearchProvider {
710 provider: crate::config::SearchProvider,
711 },
712 /// Apply `/config prompt_suggestion` to the live Config.
713 UpdatePromptSuggestion {
714 enabled: bool,
715 },
716 /// Apply one `/config notifications` scalar to the live Config.
717 UpdateNotification {
718 update: crate::config::NotificationConfigUpdate,
719 },
720 /// Enable or disable the background advisor watcher for this session (#3982).
721 SetAdvisorEnabled {
722 enabled: bool,
723 },
724 /// Open the live transcript overlay through a terminal-safe command path.
725 OpenLiveTranscript,
726 /// Open the whole-turn inspector (Ctrl+Alt+O, /turn inspect).
727 OpenTurnInspector,
728 OpenContextInspector,
729 CompactContext {
730 /// Optional user focus from `/compact <focus>`, forwarded into the
731 /// successor-brief summary prompt.
732 focus: Option<String>,
733 },
734 PurgeContext,
735 TaskAdd {
736 prompt: String,
737 },
738 TaskList,
739 TaskShow {
740 id: String,
741 },
742 TaskCancel {
743 id: String,
744 },
745 Automation(AutomationAction),
746 ShellJob(ShellJobAction),
747 Mcp(McpUiAction),
748 /// Switch to a different config profile without restarting.
749 SwitchProfile {
750 /// Profile name to load.
751 profile: String,
752 },
753 /// Switch the workspace used by tools, hooks, tasks, and session metadata.
754 SwitchWorkspace {
755 workspace: PathBuf,
756 },
757 /// Record from the microphone and route the transcription into the
758 /// composer (or auto-send it). Emitted by `/voice` and the voice hotbar
759 /// action; handled in the UI event loop where the live `Config` supplies
760 /// provider credentials.
761 VoiceCapture,
762 /// Export and share the current session as a web URL.
763 ShareSession {
764 history_len: usize,
765 model: String,
766 mode: String,
767 },
768 }
769
770 #[derive(Debug, Clone, PartialEq, Eq)]
771 pub enum AutomationAction {
772 /// Open the automations room, optionally focused on one id.
773 Open {
774 focus: Option<String>,
775 },
776 List,
777 Show(String),
778 Pause(String),
779 Resume(String),
780 Delete {
781 id: String,
782 confirmation: Option<String>,
783 },
784 Run(String),
785 }
786
787 #[derive(Debug, Clone, PartialEq, Eq)]
788 pub enum ShellJobAction {
789 List,
790 Show {
791 id: String,
792 },
793 Poll {
794 id: String,
795 wait: bool,
796 },
797 SendStdin {
798 id: String,
799 input: String,
800 close: bool,
801 },
802 Cancel {
803 id: String,
804 },
805 CancelAll,
806 }
807
808 #[derive(Debug, Clone, PartialEq, Eq)]
809 pub enum McpUiAction {
810 Show,
811 Init {
812 force: bool,
813 },
814 AddStdio {
815 name: String,
816 command: String,
817 args: Vec<String>,
818 },
819 AddHttp {
820 name: String,
821 url: String,
822 transport: Option<String>,
823 },
824 Enable {
825 name: String,
826 },
827 Disable {
828 name: String,
829 },
830 Remove {
831 name: String,
832 },
833 Login {
834 name: String,
835 scopes: Vec<String>,
836 },
837 Logout {
838 name: String,
839 },
840 /// Retry one failed/timed-out server through the engine-owned live pool.
841 Retry {
842 name: String,
843 },
844 /// List consent-gated external MCP import candidates with provenance.
845 ImportList,
846 /// Approve importing one discovered external server into user mcp.json.
847 ImportApprove {
848 name: String,
849 },
850 /// Decline an external candidate (durable until source content changes).
851 ImportDecline {
852 name: String,
853 },
854 Validate,
855 /// Report this server's last observed state without starting a new pool.
856 Diagnose {
857 name: String,
858 },
859 Reload,
860 }
861
861 lines RUST