forge_primitives/command.rs
1//! `Command` - the UI → agent channel envelope.
2//!
3//! One variant per fire-and-forget action the UI can ask the agent
4//! to perform. Direct-return accessors (config_dir, settings_documents,
5//! etc.) live on a separate sync surface, not in `Command`.
6
7use crate::ids::{SessionId, ToolUseId};
8use crate::image::ImageAttachment;
9use crate::{PermissionMode, PermissionOutcome, QuestionOutcome};
10
11/// UI → agent channel envelope. Each variant maps to one inherent
12/// method on `forge_agent::ForgeSdkBridge`. Named `AgentCommand`
13/// to disambiguate from the higher-level
14/// `forge_workspace::protocol::Command` (UI → workspace) - the two
15/// envelopes are deliberately different shapes with overlapping
16/// names, and disambiguating the agent-side enum makes which one
17/// is meant obvious at every call site.
18#[derive(Debug, Clone, PartialEq)]
19pub enum AgentCommand {
20 // --- Session lifecycle ---
21 NewSession {
22 /// The id the child runs under, passed as `--session-id`. `None`
23 /// leaves the choice to the CLI. Never combined with a resume:
24 /// the CLI refuses both flags together, and `--session-id` is
25 /// create-only, so it is refused outright when a transcript of
26 /// that id already exists.
27 session_id: Option<String>,
28 cwd: String,
29 launch_settings: serde_json::Value,
30 },
31 ResumeSession {
32 session_id: SessionId,
33 /// Working directory for the spawned `claude` subprocess.
34 /// `claude --resume <id>` indexes sessions by the project key
35 /// derived from this directory; an empty value makes the
36 /// subprocess inherit forge's `$PWD` which usually does not
37 /// match the resumed session's project, causing "No
38 /// conversation found with session ID …" even when the
39 /// `.jsonl` exists. Source from the catalog when picking a
40 /// session by id.
41 cwd: String,
42 launch_settings: serde_json::Value,
43 },
44 /// Resume `session_id`; if the resume fails at the transport level
45 /// (e.g. the underlying `.jsonl` is missing, locked, or otherwise
46 /// unreadable by the `claude` subprocess), automatically retry as
47 /// `NewSession { cwd, … }`. Used by every project-rooted entry
48 /// path (`forge`, `forge <project>`, Projects-pane project-header
49 /// click) so a stale or cross-account catalog entry can't strand
50 /// the user with no recoverable session.
51 ResumeOrNewSession {
52 session_id: SessionId,
53 cwd: String,
54 launch_settings: serde_json::Value,
55 },
56
57 // --- Conversation ---
58 Prompt {
59 session_id: SessionId,
60 text: String,
61 /// The prompt's own id, stamped on the frame. The CLI emits this
62 /// prompt's `command_lifecycle` frames only under a stamped id, and a
63 /// view keys its row to those frames by it. Minted by whoever
64 /// dispatches the prompt.
65 uuid: String,
66 },
67 PromptWithImages {
68 session_id: SessionId,
69 text: String,
70 images: Vec<ImageAttachment>,
71 /// As on [`Self::Prompt`].
72 uuid: String,
73 },
74 Cancel {
75 session_id: SessionId,
76 },
77 /// Drop one prompt still sitting in the CLI's queue, by the uuid it was
78 /// sent under. Answered by [`crate::Message`]-side lifecycle state: a
79 /// prompt the CLI already took cannot be reached this way.
80 CancelQueuedPrompt {
81 session_id: SessionId,
82 uuid: String,
83 },
84
85 // --- Controls ---
86 SetMode {
87 session_id: SessionId,
88 mode: PermissionMode,
89 },
90 SetModel {
91 session_id: SessionId,
92 model: String,
93 },
94
95 // --- Snapshots requested by UI ---
96 GetContextUsage {
97 session_id: SessionId,
98 },
99 GetMcpSnapshot {
100 session_id: SessionId,
101 },
102
103 // --- Callback responses ---
104 PermissionResponse {
105 session_id: SessionId,
106 tool_call_id: ToolUseId,
107 outcome: PermissionOutcome,
108 },
109 QuestionResponse {
110 session_id: SessionId,
111 tool_call_id: ToolUseId,
112 outcome: QuestionOutcome,
113 },
114}