agentmux_srv\backend/
rpc_types.rs

1#![allow(dead_code)]
2// Copyright 2025-2026, AgentMux Corp.
3// SPDX-License-Identifier: Apache-2.0
4
5//! RPC wire format types: Rust equivalents of Go structs from
6//! pkg/wshutil/wshrpc.go and pkg/wshrpc/wshrpctypes.go.
7
8
9use std::collections::HashMap;
10
11use serde::{Deserialize, Serialize};
12
13use super::oref::ORef;
14use super::obj::{Block, MetaMapType, Workspace};
15
16// ---- RpcMessage wire format ----
17
18/// Matches Go's `wshutil.RpcMessage` from pkg/wshutil/wshrpc.go.
19/// This is the on-the-wire JSON envelope for all RPC communication.
20#[derive(Debug, Clone, Serialize, Deserialize, Default)]
21pub struct RpcMessage {
22    #[serde(default, skip_serializing_if = "String::is_empty")]
23    pub command: String,
24    #[serde(default, skip_serializing_if = "String::is_empty")]
25    pub reqid: String,
26    #[serde(default, skip_serializing_if = "String::is_empty")]
27    pub resid: String,
28    #[serde(default, skip_serializing_if = "is_zero_i64")]
29    pub timeout: i64,
30    #[serde(default, skip_serializing_if = "String::is_empty")]
31    pub route: String,
32    #[serde(default, skip_serializing_if = "String::is_empty")]
33    pub authtoken: String,
34    #[serde(default, skip_serializing_if = "String::is_empty")]
35    pub source: String,
36    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
37    pub cont: bool,
38    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
39    pub cancel: bool,
40    #[serde(default, skip_serializing_if = "String::is_empty")]
41    pub error: String,
42    #[serde(default, skip_serializing_if = "String::is_empty")]
43    pub datatype: String,
44    #[serde(default, skip_serializing_if = "Option::is_none")]
45    pub data: Option<serde_json::Value>,
46}
47
48impl RpcMessage {
49    pub fn is_rpc_request(&self) -> bool {
50        !self.command.is_empty() || !self.reqid.is_empty()
51    }
52
53    /// Validates the packet structure. Matches Go's `RpcMessage.Validate()`.
54    pub fn validate(&self) -> Result<(), String> {
55        if !self.reqid.is_empty() && !self.resid.is_empty() {
56            return Err("request packets may not have both reqid and resid set".into());
57        }
58        if self.cancel {
59            if !self.command.is_empty() {
60                return Err("cancel packets may not have command set".into());
61            }
62            if self.reqid.is_empty() && self.resid.is_empty() {
63                return Err("cancel packets must have reqid or resid set".into());
64            }
65            if self.data.is_some() {
66                return Err("cancel packets may not have data set".into());
67            }
68            return Ok(());
69        }
70        if !self.command.is_empty() {
71            if !self.resid.is_empty() {
72                return Err("command packets may not have resid set".into());
73            }
74            if !self.error.is_empty() {
75                return Err("command packets may not have error set".into());
76            }
77            if !self.datatype.is_empty() {
78                return Err("command packets may not have datatype set".into());
79            }
80            return Ok(());
81        }
82        if !self.reqid.is_empty() {
83            if self.resid.is_empty() {
84                return Err("request packets must have resid set".into());
85            }
86            if self.timeout != 0 {
87                return Err("non-command request packets may not have timeout set".into());
88            }
89            return Ok(());
90        }
91        if !self.resid.is_empty() {
92            if !self.command.is_empty() {
93                return Err("response packets may not have command set".into());
94            }
95            if self.reqid.is_empty() {
96                return Err("response packets must have reqid set".into());
97            }
98            if self.timeout != 0 {
99                return Err("response packets may not have timeout set".into());
100            }
101            return Ok(());
102        }
103        Err("invalid packet: must have command, reqid, or resid set".into())
104    }
105}
106
107// ---- Size/type constants (match Go) ----
108
109pub const MAX_FILE_SIZE: usize = 50 * 1024 * 1024; // 50M
110pub const MAX_DIR_SIZE: usize = 1024;
111pub const FILE_CHUNK_SIZE: usize = 64 * 1024;
112pub const DIR_CHUNK_SIZE: usize = 128;
113
114pub const LOCAL_CONN_NAME: &str = "local";
115
116// ---- RPC type constants ----
117
118pub const RPC_TYPE_CALL: &str = "call";
119pub const RPC_TYPE_RESPONSE_STREAM: &str = "responsestream";
120pub const RPC_TYPE_STREAMING_REQUEST: &str = "streamingrequest";
121pub const RPC_TYPE_COMPLEX: &str = "complex";
122
123// ---- CreateBlock action constants ----
124
125pub const CREATE_BLOCK_ACTION_REPLACE: &str = "replace";
126pub const CREATE_BLOCK_ACTION_SPLIT_UP: &str = "splitup";
127pub const CREATE_BLOCK_ACTION_SPLIT_DOWN: &str = "splitdown";
128pub const CREATE_BLOCK_ACTION_SPLIT_LEFT: &str = "splitleft";
129pub const CREATE_BLOCK_ACTION_SPLIT_RIGHT: &str = "splitright";
130
131// ---- Command constants (match Go's wshrpc.Command_* constants) ----
132
133// Special commands
134pub const COMMAND_AUTHENTICATE: &str = "authenticate";
135pub const COMMAND_AUTHENTICATE_TOKEN: &str = "authenticatetoken";
136pub const COMMAND_DISPOSE: &str = "dispose";
137pub const COMMAND_ROUTE_ANNOUNCE: &str = "routeannounce";
138pub const COMMAND_ROUTE_UNANNOUNCE: &str = "routeunannounce";
139
140// Core commands
141pub const COMMAND_MESSAGE: &str = "message";
142pub const COMMAND_GET_META: &str = "getmeta";
143pub const COMMAND_SET_META: &str = "setmeta";
144pub const COMMAND_SET_VIEW: &str = "setview";
145
146// Controller commands
147pub const COMMAND_CONTROLLER_INPUT: &str = "controllerinput";
148pub const COMMAND_CONTROLLER_RESTART: &str = "controllerrestart";
149pub const COMMAND_CONTROLLER_STOP: &str = "controllerstop";
150pub const COMMAND_CONTROLLER_RESYNC: &str = "controllerresync";
151
152/// Per-tool-call permission decision RPC. Frontend sends after the
153/// user clicks Allow / Deny in `AgentDecisionPanel`. Today the
154/// handler validates the payload and logs the decision (audit
155/// trail); actual delivery to the agent CLI — rules persistence
156/// vs. interactive subprocess — is deferred to PR-3b/PR-4 per
157/// docs/specs/SPEC_DECISION_PROMPT_2026_04_24.md §9.1.
158pub const COMMAND_TOOL_DECISION: &str = "tooldecision";
159
160// Subprocess agent commands
161pub const COMMAND_SUBPROCESS_SPAWN: &str = "subprocessspawn";
162pub const COMMAND_AGENT_INPUT: &str = "agentinput";
163pub const COMMAND_AGENT_STOP: &str = "agentstop";
164pub const COMMAND_WRITE_AGENT_CONFIG: &str = "writeagentconfig";
165pub const COMMAND_RESOLVE_CLI: &str = "resolvecli";
166pub const COMMAND_CHECK_CLI_AUTH: &str = "checkcliauth";
167
168// Block commands
169pub const COMMAND_MKDIR: &str = "mkdir";
170pub const COMMAND_RESOLVE_IDS: &str = "resolveids";
171pub const COMMAND_BLOCK_INFO: &str = "blockinfo";
172pub const COMMAND_BLOCKS_LIST: &str = "blockslist";
173pub const COMMAND_CREATE_BLOCK: &str = "createblock";
174pub const COMMAND_DELETE_BLOCK: &str = "deleteblock";
175
176// File commands
177pub const COMMAND_FILE_WRITE: &str = "filewrite";
178pub const COMMAND_FILE_READ: &str = "fileread";
179pub const COMMAND_FILE_READ_STREAM: &str = "filereadstream";
180pub const COMMAND_FILE_MOVE: &str = "filemove";
181pub const COMMAND_FILE_COPY: &str = "filecopy";
182pub const COMMAND_FILE_STREAM_TAR: &str = "filestreamtar";
183pub const COMMAND_FILE_APPEND: &str = "fileappend";
184pub const COMMAND_FILE_APPEND_IJSON: &str = "fileappendijson";
185pub const COMMAND_FILE_JOIN: &str = "filejoin";
186pub const COMMAND_FILE_SHARE_CAPABILITY: &str = "filesharecapability";
187
188// Event commands
189pub const COMMAND_EVENT_PUBLISH: &str = "eventpublish";
190pub const COMMAND_EVENT_RECV: &str = "eventrecv";
191pub const COMMAND_EVENT_SUB: &str = "eventsub";
192pub const COMMAND_EVENT_UNSUB: &str = "eventunsub";
193pub const COMMAND_EVENT_UNSUB_ALL: &str = "eventunsuball";
194pub const COMMAND_EVENT_READ_HISTORY: &str = "eventreadhistory";
195
196// Stream/test commands
197pub const COMMAND_STREAM_TEST: &str = "streamtest";
198pub const COMMAND_STREAM_CPU_DATA: &str = "streamcpudata";
199pub const COMMAND_TEST: &str = "test";
200
201// Config commands
202pub const COMMAND_SET_CONFIG: &str = "setconfig";
203pub const COMMAND_SET_CONNECTIONS_CONFIG: &str = "connectionsconfig";
204pub const COMMAND_GET_FULL_CONFIG: &str = "getfullconfig";
205
206// Remote commands
207pub const COMMAND_REMOTE_STREAM_FILE: &str = "remotestreamfile";
208pub const COMMAND_REMOTE_TAR_STREAM: &str = "remotetarstream";
209pub const COMMAND_REMOTE_FILE_INFO: &str = "remotefileinfo";
210pub const COMMAND_REMOTE_FILE_TOUCH: &str = "remotefiletouch";
211pub const COMMAND_REMOTE_WRITE_FILE: &str = "remotewritefile";
212pub const COMMAND_REMOTE_FILE_DELETE: &str = "remotefiledelete";
213pub const COMMAND_REMOTE_FILE_JOIN: &str = "remotefilejoin";
214pub const COMMAND_REMOTE_MKDIR: &str = "remotemkdir";
215pub const COMMAND_REMOTE_GET_INFO: &str = "remotegetinfo";
216pub const COMMAND_REMOTE_INSTALL_RC_FILES: &str = "remoteinstallrcfiles";
217
218// Info/activity commands
219pub const COMMAND_APP_INFO: &str = "waveinfo";
220pub const COMMAND_ACTIVITY: &str = "activity";
221pub const COMMAND_GET_VAR: &str = "getvar";
222pub const COMMAND_SET_VAR: &str = "setvar";
223
224// Connection commands
225pub const COMMAND_CONN_STATUS: &str = "connstatus";
226pub const COMMAND_WSL_STATUS: &str = "wslstatus";
227pub const COMMAND_CONN_ENSURE: &str = "connensure";
228pub const COMMAND_CONN_CONNECT: &str = "connconnect";
229pub const COMMAND_CONN_DISCONNECT: &str = "conndisconnect";
230pub const COMMAND_CONN_LIST: &str = "connlist";
231pub const COMMAND_CONN_LIST_AWS: &str = "connlistaws";
232pub const COMMAND_WSL_LIST: &str = "wsllist";
233pub const COMMAND_WSL_DEFAULT_DISTRO: &str = "wsldefaultdistro";
234// COMMAND_CONN_REINSTALL_WSH / COMMAND_CONN_UPDATE_WSH / COMMAND_DISMISS_WSH_FAIL
235// have been removed — wsh has been retired. See
236// specs/SPEC_RETIRE_WSH_2026_04_12.md.
237
238// Workspace commands
239pub const COMMAND_WORKSPACE_LIST: &str = "workspacelist";
240
241// UI commands
242pub const COMMAND_WEB_SELECTOR: &str = "webselector";
243pub const COMMAND_NOTIFY: &str = "notify";
244pub const COMMAND_FOCUS_WINDOW: &str = "focuswindow";
245pub const COMMAND_GET_UPDATE_CHANNEL: &str = "getupdatechannel";
246
247// VDom commands
248pub const COMMAND_VDOM_CREATE_CONTEXT: &str = "vdomcreatecontext";
249pub const COMMAND_VDOM_ASYNC_INITIATION: &str = "vdomasyncinitiation";
250pub const COMMAND_VDOM_RENDER: &str = "vdomrender";
251pub const COMMAND_VDOM_URL_REQUEST: &str = "vdomurlrequest";
252
253// AI commands
254pub const COMMAND_AI_SEND_MESSAGE: &str = "aisendmessage";
255pub const COMMAND_GET_AI_RATE_LIMIT: &str = "getwaveairatelimit";
256
257// Screenshot
258pub const COMMAND_CAPTURE_BLOCK_SCREENSHOT: &str = "captureblockscreenshot";
259
260// RT info
261pub const COMMAND_GET_RT_INFO: &str = "getrtinfo";
262pub const COMMAND_SET_RT_INFO: &str = "setrtinfo";
263
264// Terminal
265pub const COMMAND_TERM_GET_SCROLLBACK_LINES: &str = "termgetscrollbacklines";
266
267// Agent
268pub const COMMAND_LIST_AGENTS: &str = "listagents";
269pub const COMMAND_CREATE_AGENT: &str = "createagent";
270pub const COMMAND_UPDATE_AGENT: &str = "updateagent";
271pub const COMMAND_DELETE_AGENT: &str = "deleteagent";
272pub const COMMAND_GET_AGENT_CONTENT: &str = "getagentcontent";
273pub const COMMAND_SET_AGENT_CONTENT: &str = "setagentcontent";
274pub const COMMAND_GET_ALL_AGENT_CONTENT: &str = "getallagentcontent";
275
276// Agent Skills
277pub const COMMAND_LIST_AGENT_SKILLS: &str = "listagentskills";
278pub const COMMAND_CREATE_AGENT_SKILL: &str = "createagentskill";
279pub const COMMAND_UPDATE_AGENT_SKILL: &str = "updateagentskill";
280pub const COMMAND_DELETE_AGENT_SKILL: &str = "deleteagentskill";
281
282// Agent History
283pub const COMMAND_APPEND_AGENT_HISTORY: &str = "appendagenthistory";
284pub const COMMAND_LIST_AGENT_HISTORY: &str = "listagenthistory";
285pub const COMMAND_SEARCH_AGENT_HISTORY: &str = "searchagenthistory";
286
287// Agent Import
288pub const COMMAND_IMPORT_AGENT_FROM_CLAW: &str = "importagentfromclaw";
289pub const COMMAND_IMPORT_AGENTS: &str = "importagents";
290
291// Agent Export
292pub const COMMAND_EXPORT_AGENTS: &str = "exportagents";
293
294// Agent Seed
295pub const COMMAND_RESEED_AGENTS: &str = "reseedagents";
296
297// Identity accounts (v6 — replaces localStorage)
298pub const COMMAND_LIST_IDENTITY_ACCOUNTS: &str = "listidentityaccounts";
299pub const COMMAND_GET_IDENTITY_ACCOUNT: &str = "getidentityaccount";
300pub const COMMAND_UPSERT_IDENTITY_ACCOUNT: &str = "upsertidentityaccount";
301pub const COMMAND_DELETE_IDENTITY_ACCOUNT: &str = "deleteidentityaccount";
302
303// Agent ↔ Identity junction
304pub const COMMAND_LINK_AGENT_IDENTITY: &str = "linkagentidentity";
305pub const COMMAND_UNLINK_AGENT_IDENTITY: &str = "unlinkagentidentity";
306pub const COMMAND_LIST_AGENT_IDENTITIES: &str = "listagentidentities";
307
308// Identity bundles (v7 — named credential bundles)
309pub const COMMAND_LIST_IDENTITY_BUNDLES: &str = "listidentitybundles";
310pub const COMMAND_GET_IDENTITY_BUNDLE: &str = "getidentitybundle";
311pub const COMMAND_UPSERT_IDENTITY_BUNDLE: &str = "upsertidentitybundle";
312pub const COMMAND_DELETE_IDENTITY_BUNDLE: &str = "deleteidentitybundle";
313pub const COMMAND_BIND_IDENTITY_ACCOUNT: &str = "bindidentityaccount";
314pub const COMMAND_UNBIND_IDENTITY_ACCOUNT: &str = "unbindidentityaccount";
315pub const COMMAND_LIST_IDENTITY_BINDINGS: &str = "listidentitybindings";
316
317// Memory bundles (v7 — agent personality / capability stack)
318pub const COMMAND_LIST_MEMORIES: &str = "listmemories";
319pub const COMMAND_GET_MEMORY: &str = "getmemory";
320pub const COMMAND_UPSERT_MEMORY: &str = "upsertmemory";
321pub const COMMAND_DELETE_MEMORY: &str = "deletememory";
322
323// Agent instances
324pub const COMMAND_LIST_AGENT_INSTANCES: &str = "listagentinstances";
325pub const COMMAND_GET_AGENT_INSTANCE: &str = "getagentinstance";
326pub const COMMAND_CREATE_AGENT_INSTANCE: &str = "createagentinstance";
327pub const COMMAND_UPDATE_AGENT_INSTANCE: &str = "updateagentinstance";
328pub const COMMAND_DELETE_AGENT_INSTANCE: &str = "deleteagentinstance";
329/// v8 — list named agent instances for the launch modal's "Continue
330/// agent" dropdown. Filters to non-hidden rows with a non-empty
331/// instance_name, joined with definition + identity + memory bundles.
332pub const COMMAND_LIST_NAMED_AGENTS: &str = "listnamedagents";
333/// v8 — soft-delete (hide) a named agent instance from the dropdown.
334/// Row + working directory remain on disk for audit + recovery.
335pub const COMMAND_HIDE_NAMED_AGENT: &str = "hidenamedagent";
336/// Cascade follow-up (2026-05-23) — list recent agent sessions with
337/// conversation previews extracted from the filestore `output.state.json`
338/// snapshot. Powers the AgentPicker's "Recent sessions" surface so a
339/// pane crash that orphans a conversation becomes recoverable from
340/// normal UI. See `docs/recovery/MAKS_CONVERSATION_2026_05_23.md`.
341pub const COMMAND_LIST_RECENT_SESSIONS: &str = "listrecentsessions";
342
343// Agent definition branching
344pub const COMMAND_FORK_AGENT_DEFINITION: &str = "forkagentdefinition";
345
346/// Two-tier picker (Phase 1 — SPEC_AGENT_PICKER_TWO_TIER_2026_05_24.md).
347/// Clone a seeded template into a new user-owned agent definition with
348/// `is_seeded = 0`. Copies provider + cmd + env + auth-config fields
349/// from the template, applies the caller-supplied name + bindings,
350/// returns the new definition_id so the frontend can immediately
351/// launch. Rejects non-template ids + duplicate user-agent names.
352pub const COMMAND_AGENT_DEF_CREATE_FROM_TEMPLATE: &str = "agentdefcreatefromtemplate";
353
354/// Two-tier picker (Phase 2 — SPEC_AGENT_PICKER_TWO_TIER_2026_05_24.md
355/// Q2 Decision Y). Set the `user_hidden` flag on a seeded template so
356/// it disappears from the default `+ New from template` list. Idempotent;
357/// rejects user-owned (`is_seeded = 0`) definitions — those use
358/// `deleteagent` instead. Manifest re-sync resets `user_hidden = 0` for
359/// any newly-added template id so fresh templates always surface once.
360pub const COMMAND_AGENT_DEF_HIDE: &str = "agentdefhide";
361/// Two-tier picker (Phase 2). Inverse of `agentdefhide` — set
362/// `user_hidden = 0` so a previously-hidden template reappears in the
363/// picker's templates tier. Powers the settings "Hidden templates"
364/// unhide affordance. Same validation as hide.
365pub const COMMAND_AGENT_DEF_UNHIDE: &str = "agentdefunhide";
366/// Two-tier picker (Phase 2). Return only the hidden templates
367/// (`is_seeded = 1 AND user_hidden = 1`). Backs the settings UI's
368/// list of templates the user can unhide. The picker proper never
369/// calls this — it uses `listagents` (which excludes hidden rows
370/// by default).
371pub const COMMAND_AGENT_DEF_LIST_HIDDEN_TEMPLATES: &str = "agentdeflisthiddentemplates";
372
373// Drone pane (v8 — issue #753 Phase 1)
374pub const COMMAND_LIST_DRONES: &str = "listdrones";
375pub const COMMAND_GET_DRONE: &str = "getdrone";
376pub const COMMAND_UPSERT_DRONE: &str = "upsertdrone";
377pub const COMMAND_DELETE_DRONE: &str = "deletedrone";
378pub const COMMAND_RUN_DRONE: &str = "rundrone";
379pub const COMMAND_LIST_DRONE_RUNS: &str = "listdroneruns";
380
381// App API Tier 1 — agent lifecycle commands
382pub const COMMAND_AGENT_OPEN: &str = "agent.open";
383pub const COMMAND_AGENT_SEND: &str = "agent.send";
384pub const COMMAND_AGENT_STOP_API: &str = "agent.stop";
385pub const COMMAND_AGENT_STATUS: &str = "agent.status";
386pub const COMMAND_AGENT_LIST: &str = "agent.list";
387pub const COMMAND_AGENT_OUTPUT: &str = "agent.output";
388pub const COMMAND_AGENT_STREAM: &str = "agent.stream";
389/// List every OS process currently tracked for a given agent block.
390/// Returns `AgentProcessListResult`. Consumed by the swarm activity
391/// panel. See `backend::process_tracker`.
392pub const COMMAND_AGENT_PROCESS_LIST: &str = "agent.process-list";
393/// List every block currently tracked (for the swarm aggregate view).
394/// Returns `AgentTrackedBlocksResult`.
395pub const COMMAND_AGENT_TRACKED_BLOCKS: &str = "agent.tracked-blocks";
396/// Terminate a single process by PID if it's a member of a given
397/// block's tracker tree. Silently no-ops if the PID isn't tracked.
398/// Returns `AgentKillResult { ok: bool }`.
399pub const COMMAND_AGENT_KILL_PROCESS: &str = "agent.kill-process";
400/// Terminate the entire process tree for a given block.
401/// On Windows: `TerminateJobObject`. On Linux: `cgroup.kill`. On
402/// macOS: `killpg`. Returns `AgentKillResult { ok: true }` even when
403/// there are no members (idempotent).
404pub const COMMAND_AGENT_KILL_TREE: &str = "agent.kill-tree";
405
406// App API Tier 2 — pane lifecycle commands
407pub const COMMAND_PANE_OPEN: &str = "pane.open";
408
409// App API Tier 1 — blockfile pagination commands
410pub const COMMAND_BLOCKFILE_LINE_COUNT: &str = "blockfile:line_count";
411pub const COMMAND_BLOCKFILE_READ_RANGE: &str = "blockfile:read_range";
412pub const COMMAND_BLOCKFILE_READ_STATE: &str = "blockfile:read_state";
413pub const COMMAND_BLOCKFILE_WRITE_STATE: &str = "blockfile:write_state";
414
415// App API Tier 1 — session archival commands
416pub const COMMAND_SESSION_ARCHIVE: &str = "session:archive";
417pub const COMMAND_SESSION_RESTORE: &str = "session:restore";
418pub const COMMAND_SESSION_EXPORT: &str = "session:export";
419
420// Session digest
421pub const COMMAND_SESSION_DIGEST: &str = "session:digest";
422
423// Option E (PR 1 of 2) — agent-anchored session zones.
424// A session zone is bound to the *agent definition* (`definition_id`),
425// not the identity bundle. Every block of the same agent reads/writes
426// through `agent:<defId>:current`; archiving snapshots to
427// `agent:<defId>:archive:<ts_ms>`. See
428// docs/specs/SPEC_CONTINUATION_SESSION_PERSISTENCE_2026_05_23.md.
429pub const COMMAND_AGENT_SESSION_READ: &str = "agent:session:read";
430pub const COMMAND_AGENT_SESSION_WRITE_STATE: &str = "agent:session:write_state";
431pub const COMMAND_AGENT_SESSION_APPEND_OUTPUT: &str = "agent:session:append_output";
432pub const COMMAND_AGENT_SESSION_ARCHIVE: &str = "agent:session:archive";
433pub const COMMAND_AGENT_SESSION_LIST_ARCHIVES: &str = "agent:session:list_archives";
434
435// ---- Client type constants ----
436
437pub const CLIENT_TYPE_CONN_SERVER: &str = "connserver";
438pub const CLIENT_TYPE_BLOCK_CONTROLLER: &str = "blockcontroller";
439
440// ---- Command data types ----
441
442/// Matches Go's `CommandGetMetaData`
443#[derive(Debug, Clone, Serialize, Deserialize)]
444pub struct CommandGetMetaData {
445    pub oref: ORef,
446}
447
448/// Matches Go's `CommandSetMetaData`
449#[derive(Debug, Clone, Serialize, Deserialize)]
450pub struct CommandSetMetaData {
451    pub oref: ORef,
452    pub meta: MetaMapType,
453}
454
455/// Matches Go's `CommandMessageData`
456#[derive(Debug, Clone, Serialize, Deserialize)]
457pub struct CommandMessageData {
458    #[serde(default)]
459    pub oref: ORef,
460    pub message: String,
461}
462
463/// Matches Go's `CommandAuthenticateRtnData`
464#[derive(Debug, Clone, Serialize, Deserialize, Default)]
465pub struct CommandAuthenticateRtnData {
466    pub routeid: String,
467    #[serde(default, skip_serializing_if = "String::is_empty")]
468    pub authtoken: String,
469    #[serde(default, skip_serializing_if = "Option::is_none")]
470    pub env: Option<HashMap<String, String>>,
471    #[serde(default, skip_serializing_if = "String::is_empty")]
472    pub initscripttext: String,
473}
474
475/// Matches Go's `CommandAuthenticateTokenData`
476#[derive(Debug, Clone, Serialize, Deserialize)]
477pub struct CommandAuthenticateTokenData {
478    pub token: String,
479}
480
481/// Matches Go's `CommandDisposeData`
482#[derive(Debug, Clone, Serialize, Deserialize)]
483pub struct CommandDisposeData {
484    pub routeid: String,
485}
486
487/// Matches Go's `CommandResolveIdsData`
488#[derive(Debug, Clone, Serialize, Deserialize)]
489pub struct CommandResolveIdsData {
490    #[serde(default)]
491    pub blockid: String,
492    pub ids: Vec<String>,
493}
494
495/// Matches Go's `CommandResolveIdsRtnData`
496#[derive(Debug, Clone, Serialize, Deserialize)]
497pub struct CommandResolveIdsRtnData {
498    pub resolvedids: HashMap<String, ORef>,
499}
500
501/// Matches Go's `CommandCreateBlockData`
502#[derive(Debug, Clone, Serialize, Deserialize, Default)]
503pub struct CommandCreateBlockData {
504    #[serde(default)]
505    pub tabid: String,
506    #[serde(default, skip_serializing_if = "Option::is_none")]
507    pub blockdef: Option<serde_json::Value>,
508    #[serde(default, skip_serializing_if = "Option::is_none")]
509    pub rtopts: Option<serde_json::Value>,
510    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
511    pub magnified: bool,
512    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
513    pub ephemeral: bool,
514    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
515    pub focused: bool,
516    #[serde(default, skip_serializing_if = "String::is_empty")]
517    pub targetblockid: String,
518    #[serde(default, skip_serializing_if = "String::is_empty")]
519    pub targetaction: String,
520}
521
522/// Matches Go's `CommandDeleteBlockData`
523#[derive(Debug, Clone, Serialize, Deserialize)]
524pub struct CommandDeleteBlockData {
525    pub blockid: String,
526}
527
528/// Matches Go's `CommandBlockSetViewData`
529#[derive(Debug, Clone, Serialize, Deserialize)]
530pub struct CommandBlockSetViewData {
531    pub blockid: String,
532    pub view: String,
533}
534
535/// Matches Go's `CommandControllerResyncData`
536#[derive(Debug, Clone, Serialize, Deserialize, Default)]
537pub struct CommandControllerResyncData {
538    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
539    pub forcerestart: bool,
540    #[serde(default)]
541    pub tabid: String,
542    #[serde(default)]
543    pub blockid: String,
544    #[serde(default, skip_serializing_if = "Option::is_none")]
545    pub rtopts: Option<serde_json::Value>,
546}
547
548/// Matches Go's `CommandBlockInputData`
549#[derive(Debug, Clone, Serialize, Deserialize, Default)]
550pub struct CommandBlockInputData {
551    pub blockid: String,
552    #[serde(default, skip_serializing_if = "String::is_empty")]
553    pub inputdata64: String,
554    #[serde(default, skip_serializing_if = "String::is_empty")]
555    pub signame: String,
556    #[serde(default, skip_serializing_if = "Option::is_none")]
557    pub termsize: Option<serde_json::Value>,
558    /// Per-TermViewModel monotonic counter for seq-based input ordering (optional, shell only).
559    #[serde(default, skip_serializing_if = "Option::is_none")]
560    pub seq: Option<u64>,
561}
562
563/// Data for `tooldecision` — frontend's reply to a per-tool-call
564/// permission gate. Today the backend validates the outcome and
565/// logs the decision; actual delivery to the agent CLI is deferred
566/// to PR-3b/PR-4 (rules persistence vs. interactive subprocess
567/// path). Spec:
568/// docs/specs/SPEC_DECISION_PROMPT_2026_04_24.md §9.1.
569#[derive(Debug, Clone, Serialize, Deserialize, Default)]
570pub struct CommandToolDecisionData {
571    pub blockid: String,
572    /// Opaque id matched against a `PermissionRequestEvent`. Echoed
573    /// in the audit log so the audit trail can be cross-referenced.
574    pub request_id: String,
575    /// "allow" or "deny". Anything else returns an error.
576    pub outcome: String,
577    /// "once" / "session" / "project" / "global". Captured so the
578    /// rules-persistence layer (PR-3b) can write a matching rule
579    /// without re-asking the user.
580    pub scope: String,
581    /// User-typed denial reason. Optional. Future PR will relay
582    /// this verbatim into the agent's next prompt.
583    #[serde(default, skip_serializing_if = "Option::is_none")]
584    pub feedback: Option<String>,
585}
586
587// ---- Subprocess agent command data types ----
588
589/// Data for SubprocessSpawnCommand — spawn agent CLI for a single turn.
590#[derive(Debug, Clone, Serialize, Deserialize)]
591pub struct CommandSubprocessSpawnData {
592    pub blockid: String,
593    pub tabid: String,
594    pub cli_command: String,
595    #[serde(default)]
596    pub cli_args: Vec<String>,
597    #[serde(default)]
598    pub working_dir: String,
599    #[serde(default)]
600    pub env_vars: std::collections::HashMap<String, String>,
601    /// The user's JSON message to write to subprocess stdin.
602    pub message: String,
603}
604
605/// Data for AgentInputCommand — send a follow-up message (re-spawns with --resume).
606#[derive(Debug, Clone, Serialize, Deserialize)]
607pub struct CommandAgentInputData {
608    pub blockid: String,
609    /// The user's JSON message string.
610    pub message: String,
611    /// Optional client-supplied id. Echoed back via the
612    /// `agent-message-accepted` event when this message transitions
613    /// from queued to running so the frontend can match its pending
614    /// `PendingMessage` entry and promote it into the conversation
615    /// document. Absent for pre-existing callers; treated as no-id.
616    #[serde(default, skip_serializing_if = "Option::is_none")]
617    pub message_id: Option<String>,
618}
619
620/// Data for AgentStopCommand — stop the running subprocess.
621#[derive(Debug, Clone, Serialize, Deserialize)]
622pub struct CommandAgentStopData {
623    pub blockid: String,
624    #[serde(default)]
625    pub force: bool,
626}
627
628/// A file to write as part of agent config.
629#[derive(Debug, Clone, Serialize, Deserialize)]
630pub struct AgentConfigFile {
631    pub path: String,
632    pub content: String,
633}
634
635/// Data for WriteAgentConfigCommand — write config files atomically.
636#[derive(Debug, Clone, Serialize, Deserialize)]
637pub struct CommandWriteAgentConfigData {
638    /// Agent working directory where files are written.
639    pub working_dir: String,
640    /// Files to write (path relative to working_dir, content).
641    pub files: Vec<AgentConfigFile>,
642    /// When true, treat `working_dir` as an auto-generated instance
643    /// path eligible for `<base>-N` collision resolution. When false
644    /// (user-specified `agent.working_directory` like `~/projects/X`),
645    /// write into the path as-is — no rewrite, no suffixing. The
646    /// frontend sets this based on whether it constructed the path
647    /// itself or pulled it from the agent definition.
648    #[serde(default)]
649    pub auto_allocate: bool,
650}
651
652/// Result of WriteAgentConfigCommand. Returns the final working
653/// directory used; callers should compare against the requested
654/// `working_dir` and patch `cmd:cwd` (via SetMeta) when they differ
655/// so the controller spawns the CLI in the actually-created dir.
656#[derive(Debug, Clone, Serialize, Deserialize)]
657pub struct CommandWriteAgentConfigResult {
658    pub working_dir: String,
659}
660
661/// Data for ResolveCliCommand — detect or install a CLI tool.
662#[derive(Debug, Clone, Serialize, Deserialize)]
663pub struct CommandResolveCliData {
664    /// Provider ID (e.g. "claude", "codex", "gemini")
665    pub provider_id: String,
666    /// CLI command name (e.g. "claude")
667    pub cli_command: String,
668    /// npm package name for fallback install (e.g. "@anthropic-ai/claude-code")
669    pub npm_package: String,
670    /// Version to install ("latest" or specific version)
671    pub pinned_version: String,
672    /// Windows install command (e.g. "irm https://claude.ai/install.ps1 | iex")
673    #[serde(default)]
674    pub windows_install_command: String,
675    /// Unix install command (e.g. "curl -fsSL https://claude.ai/install.sh | bash")
676    #[serde(default)]
677    pub unix_install_command: String,
678    /// Block ID to stream install output into (optional — if empty, no streaming)
679    #[serde(default)]
680    pub block_id: String,
681}
682
683/// Result from ResolveCliCommand
684#[derive(Debug, Clone, Serialize, Deserialize)]
685pub struct ResolveCliResult {
686    /// Absolute path to the CLI binary
687    pub cli_path: String,
688    /// CLI version string
689    pub version: String,
690    /// How it was resolved: "path", "local_install", "installed"
691    pub source: String,
692}
693
694/// Data for CheckCliAuthCommand — check if CLI is authenticated.
695#[derive(Debug, Clone, Serialize, Deserialize)]
696pub struct CommandCheckCliAuthData {
697    /// Absolute path to CLI binary
698    pub cli_path: String,
699    /// Auth check args (e.g. ["auth", "status", "--json"])
700    pub auth_check_args: Vec<String>,
701    /// Environment variables to set when running the auth check (e.g. CLAUDE_CONFIG_DIR).
702    /// Must match the env vars used when spawning the actual subprocess so the check
703    /// reads credentials from the same isolated directory.
704    #[serde(default)]
705    pub auth_env: std::collections::HashMap<String, String>,
706}
707
708/// Result from CheckCliAuthCommand
709#[derive(Debug, Clone, Serialize, Deserialize)]
710pub struct CheckCliAuthResult {
711    pub authenticated: bool,
712    pub email: Option<String>,
713    pub auth_method: Option<String>,
714    /// Raw stdout from auth check command
715    pub raw_output: String,
716}
717
718/// Input for RunCliLoginCommand — spawns the CLI login flow and extracts the OAuth URL
719#[derive(Debug, Clone, Serialize, Deserialize)]
720pub struct CommandRunCliLoginData {
721    pub cli_path: String,
722    pub login_args: Vec<String>,
723    #[serde(default)]
724    pub auth_env: HashMap<String, String>,
725}
726
727/// Result from RunCliLoginCommand
728#[derive(Debug, Clone, Serialize, Deserialize)]
729pub struct RunCliLoginResult {
730    /// OAuth URL extracted from the CLI's output (open in browser)
731    pub auth_url: Option<String>,
732    pub raw_output: String,
733}
734
735// ---- App API Tier 1 — request/response types ----
736
737/// Request for agent.open — find or create an agent pane for the given agent_id.
738#[derive(Debug, Clone, Deserialize)]
739#[serde(rename_all = "snake_case")]
740pub struct CommandAgentOpenData {
741    pub agent_id: String,
742    pub tab_id: Option<String>,
743    pub split_direction: Option<String>,
744    pub split_reference_block_id: Option<String>,
745    pub focus: Option<bool>,
746}
747
748/// Request for agent.send — send a message to an agent pane.
749#[derive(Debug, Clone, Deserialize)]
750#[serde(rename_all = "snake_case")]
751pub struct CommandAgentSendData {
752    pub block_id: String,
753    pub message: String,
754}
755
756/// Request for agent.stop — stop a running agent subprocess.
757#[derive(Debug, Clone, Deserialize)]
758#[serde(rename_all = "snake_case")]
759pub struct CommandAgentStopApiData {
760    pub block_id: String,
761    pub signal: Option<String>,
762}
763
764/// Request for agent.status — query status of an agent pane.
765#[derive(Debug, Clone, Deserialize)]
766#[serde(rename_all = "snake_case")]
767pub struct CommandAgentStatusData {
768    pub block_id: String,
769}
770
771/// Request for agent.output — read buffered output lines from an agent pane.
772#[derive(Debug, Clone, Deserialize)]
773#[serde(rename_all = "snake_case")]
774pub struct CommandAgentOutputData {
775    pub block_id: String,
776    pub after_line: Option<usize>,
777    pub max_lines: Option<usize>,
778}
779
780/// Request for agent.stream — subscribe to live output from an agent pane.
781#[derive(Debug, Clone, Deserialize)]
782#[serde(rename_all = "snake_case")]
783pub struct CommandAgentStreamData {
784    pub block_id: String,
785}
786
787/// Response from agent.open.
788#[derive(Debug, Clone, Serialize)]
789#[serde(rename_all = "snake_case")]
790pub struct AgentOpenResult {
791    pub block_id: String,
792    pub tab_id: String,
793    pub agent_id: String,
794    pub provider: String,
795    pub controller_type: String,
796    pub status: String,
797    pub created: bool,
798}
799
800/// Request for pane.open — create a new pane showing the given view.
801///
802/// Supported views: `editor`, `term`, `browser`, `sysinfo`, `help`.
803/// `file` is required for `editor`; `url` is required for `browser`.
804/// Placement: if `split_direction` ("right" / "left" / "down" / "up")
805/// and `split_reference_block_id` are provided, the new pane splits
806/// relative to that block. Otherwise it is inserted at the tab root.
807#[derive(Debug, Clone, Deserialize)]
808#[serde(rename_all = "snake_case")]
809pub struct CommandPaneOpenData {
810    pub view: String,
811    pub file: Option<String>,
812    pub url: Option<String>,
813    pub cwd: Option<String>,
814    pub title: Option<String>,
815    pub tab_id: Option<String>,
816    pub split_direction: Option<String>,
817    pub split_reference_block_id: Option<String>,
818    pub focus: Option<bool>,
819}
820
821/// Response from pane.open.
822#[derive(Debug, Clone, Serialize)]
823#[serde(rename_all = "snake_case")]
824pub struct PaneOpenResult {
825    pub block_id: String,
826    pub tab_id: String,
827    pub view: String,
828    pub created: bool,
829}
830
831/// Response from agent.send.
832#[derive(Debug, Clone, Serialize)]
833#[serde(rename_all = "snake_case")]
834pub struct AgentSendResult {
835    pub block_id: String,
836    pub status: String,
837    pub session_id: Option<String>,
838}
839
840/// Response from agent.stop.
841#[derive(Debug, Clone, Serialize)]
842#[serde(rename_all = "snake_case")]
843pub struct AgentStopResult {
844    pub block_id: String,
845    pub status: String,
846    pub exit_code: Option<i32>,
847}
848
849/// Response from agent.status.
850#[derive(Debug, Clone, Serialize)]
851#[serde(rename_all = "snake_case")]
852pub struct AgentStatusResult {
853    pub block_id: String,
854    pub agent_id: String,
855    pub provider: String,
856    pub controller_type: String,
857    pub status: String,
858    pub session_id: Option<String>,
859    pub pid: Option<u32>,
860    pub exit_code: Option<i32>,
861}
862
863/// A single entry in the agent.list response.
864#[derive(Debug, Clone, Serialize)]
865#[serde(rename_all = "snake_case")]
866pub struct AgentListEntry {
867    pub block_id: String,
868    pub tab_id: String,
869    pub agent_id: String,
870    pub provider: String,
871    pub status: String,
872    pub session_id: Option<String>,
873}
874
875/// Response from agent.list.
876#[derive(Debug, Clone, Serialize)]
877#[serde(rename_all = "snake_case")]
878pub struct AgentListResult {
879    pub agents: Vec<AgentListEntry>,
880}
881
882/// Response from agent.output.
883#[derive(Debug, Clone, Serialize)]
884#[serde(rename_all = "snake_case")]
885pub struct AgentOutputResult {
886    pub block_id: String,
887    pub lines: Vec<String>,
888    pub total_lines: usize,
889    pub has_more: bool,
890}
891
892/// Request for `agent.process-list` — processes tracked under a given block.
893#[derive(Debug, Clone, Deserialize)]
894pub struct AgentProcessListCommand {
895    pub block_id: String,
896}
897
898/// One tracked process row. Mirrors `backend::process_tracker::TrackedProcess`
899/// — defined here so the RPC layer can expose it without leaking the
900/// internal module shape.
901#[derive(Debug, Clone, Serialize)]
902pub struct AgentProcessInfo {
903    pub pid: u32,
904    pub command: String,
905    pub rss_bytes: u64,
906    pub started_at_ms: u64,
907}
908
909/// Response from `agent.process-list`.
910#[derive(Debug, Clone, Serialize)]
911#[serde(rename_all = "snake_case")]
912pub struct AgentProcessListResult {
913    pub block_id: String,
914    /// Platform confidence level — `"high"`, `"best_effort"`, `"none"`.
915    /// Frontend shows a badge when anything less than `high`.
916    pub confidence: String,
917    pub processes: Vec<AgentProcessInfo>,
918}
919
920/// Response from `agent.tracked-blocks` — the list of block IDs for
921/// which a tracker exists. Swarm pane uses this to render per-agent
922/// groups.
923#[derive(Debug, Clone, Serialize)]
924#[serde(rename_all = "snake_case")]
925pub struct AgentTrackedBlocksResult {
926    pub block_ids: Vec<String>,
927}
928
929/// Request for `agent.kill-process` — terminate a single PID if it's
930/// in a given block's tracker tree.
931#[derive(Debug, Clone, Deserialize)]
932pub struct AgentKillProcessCommand {
933    pub block_id: String,
934    pub pid: u32,
935}
936
937/// Request for `agent.kill-tree` — nuke every process tracked under a
938/// given block.
939#[derive(Debug, Clone, Deserialize)]
940pub struct AgentKillTreeCommand {
941    pub block_id: String,
942}
943
944/// Response from `agent.kill-process` / `agent.kill-tree`.
945/// `ok: true` means the kill was dispatched; it does NOT guarantee
946/// the OS has fully torn down every descendant by the time the RPC
947/// returns. The swarm activity panel's next refresh will reflect
948/// actual state.
949#[derive(Debug, Clone, Serialize)]
950#[serde(rename_all = "snake_case")]
951pub struct AgentKillResult {
952    pub ok: bool,
953}
954
955/// Request for blockfile:line_count — count total lines in a blockfile.
956#[derive(Debug, Clone, Deserialize)]
957#[serde(rename_all = "snake_case")]
958pub struct CommandBlockfileLineCountData {
959    pub block_id: String,
960    pub filename: String,
961}
962
963/// Response from blockfile:line_count.
964#[derive(Debug, Clone, Serialize)]
965#[serde(rename_all = "snake_case")]
966pub struct BlockfileLineCountResult {
967    pub count: u64,
968}
969
970/// Request for blockfile:read_range — read a range of lines from a blockfile.
971#[derive(Debug, Clone, Deserialize)]
972#[serde(rename_all = "snake_case")]
973pub struct CommandBlockfileReadRangeData {
974    pub block_id: String,
975    pub filename: String,
976    pub offset: u64,
977    pub limit: u64,
978}
979
980/// Response from blockfile:read_range.
981#[derive(Debug, Clone, Serialize)]
982#[serde(rename_all = "snake_case")]
983pub struct BlockfileReadRangeResult {
984    pub lines: Vec<String>,
985    pub total: u64,
986}
987
988/// Request for blockfile:read_state — read a sidecar JSON file
989/// (e.g. `output.state.json`) associated with a block.
990/// Spec: docs/specs/SPEC_AGENT_PANE_STATE_PERSISTENCE_2026_05_15.md.
991#[derive(Debug, Clone, Deserialize)]
992#[serde(rename_all = "snake_case")]
993pub struct CommandBlockfileReadStateData {
994    pub block_id: String,
995    /// Sidecar filename — e.g. "output.state.json". Resolved within the
996    /// block's filestore directory; must not contain path separators.
997    pub filename: String,
998}
999
1000/// Response from blockfile:read_state. `content` is the raw file bytes
1001/// as a UTF-8 string, or null if the sidecar does not exist.
1002#[derive(Debug, Clone, Serialize)]
1003#[serde(rename_all = "snake_case")]
1004pub struct BlockfileReadStateResult {
1005    pub content: Option<String>,
1006}
1007
1008/// Request for blockfile:write_state — atomically write a sidecar JSON
1009/// file for a block. Uses tmp + fsync + rename to guarantee partial
1010/// writes never surface to readers.
1011#[derive(Debug, Clone, Deserialize)]
1012#[serde(rename_all = "snake_case")]
1013pub struct CommandBlockfileWriteStateData {
1014    pub block_id: String,
1015    pub filename: String,
1016    pub content: String,
1017}
1018
1019/// Response from blockfile:write_state.
1020#[derive(Debug, Clone, Serialize)]
1021#[serde(rename_all = "snake_case")]
1022pub struct BlockfileWriteStateResult {
1023    pub bytes_written: u64,
1024}
1025
1026// ---- Session digest types ----
1027
1028/// Request for session:digest — generate or return a cached AI summary of the session.
1029#[derive(Debug, Clone, Deserialize)]
1030#[serde(rename_all = "snake_case")]
1031pub struct CommandSessionDigestData {
1032    pub block_id: String,
1033    /// If true, regenerate even if a cached digest exists.
1034    pub force: Option<bool>,
1035}
1036
1037/// Response from session:digest.
1038#[derive(Debug, Clone, Serialize)]
1039#[serde(rename_all = "snake_case")]
1040pub struct SessionDigestResult {
1041    /// AI-generated summary text (Markdown).
1042    pub summary: String,
1043    /// Unix milliseconds when this digest was generated.
1044    pub generated_at: i64,
1045    /// true if we returned a previously-cached result (no new activity since last run).
1046    pub cached: bool,
1047}
1048
1049// ---- Session archival types ----
1050
1051/// Request for session:archive — compress and archive a session's FileStore output.
1052#[derive(Debug, Clone, Deserialize)]
1053#[serde(rename_all = "snake_case")]
1054pub struct CommandSessionArchiveData {
1055    pub block_id: String,
1056}
1057
1058/// Response from session:archive.
1059#[derive(Debug, Clone, Serialize)]
1060#[serde(rename_all = "snake_case")]
1061pub struct SessionArchiveResult {
1062    pub block_id: String,
1063    pub archived_bytes: u64,
1064    pub archived_at: i64,
1065}
1066
1067/// Request for session:restore — decompress archive back into FileStore.
1068#[derive(Debug, Clone, Deserialize)]
1069#[serde(rename_all = "snake_case")]
1070pub struct CommandSessionRestoreData {
1071    pub block_id: String,
1072}
1073
1074/// Response from session:restore.
1075#[derive(Debug, Clone, Serialize)]
1076#[serde(rename_all = "snake_case")]
1077pub struct SessionRestoreResult {
1078    pub block_id: String,
1079    pub restored_bytes: u64,
1080}
1081
1082/// Request for session:export — read session output and return as base64 JSONL.
1083#[derive(Debug, Clone, Deserialize)]
1084#[serde(rename_all = "snake_case")]
1085pub struct CommandSessionExportData {
1086    pub block_id: String,
1087}
1088
1089/// Response from session:export.
1090#[derive(Debug, Clone, Serialize)]
1091#[serde(rename_all = "snake_case")]
1092pub struct SessionExportResult {
1093    /// base64-encoded JSONL content (the raw output file bytes).
1094    pub content: String,
1095    pub line_count: u64,
1096    pub byte_count: u64,
1097}
1098
1099/// Matches Go's `FileDataAt`
1100#[derive(Debug, Clone, Serialize, Deserialize)]
1101pub struct FileDataAt {
1102    pub offset: i64,
1103    #[serde(default, skip_serializing_if = "is_zero_usize")]
1104    pub size: usize,
1105}
1106
1107/// Matches Go's `FileData`
1108#[derive(Debug, Clone, Serialize, Deserialize, Default)]
1109pub struct FileData {
1110    #[serde(default, skip_serializing_if = "Option::is_none")]
1111    pub info: Option<FileInfo>,
1112    #[serde(default, skip_serializing_if = "String::is_empty")]
1113    pub data64: String,
1114    #[serde(default, skip_serializing_if = "Option::is_none")]
1115    pub entries: Option<Vec<FileInfo>>,
1116    #[serde(default, skip_serializing_if = "Option::is_none")]
1117    pub at: Option<FileDataAt>,
1118}
1119
1120/// Matches Go's `FileInfo`
1121#[derive(Debug, Clone, Serialize, Deserialize, Default)]
1122pub struct FileInfo {
1123    pub path: String,
1124    #[serde(default, skip_serializing_if = "String::is_empty")]
1125    pub dir: String,
1126    #[serde(default, skip_serializing_if = "String::is_empty")]
1127    pub name: String,
1128    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
1129    pub notfound: bool,
1130    #[serde(default, skip_serializing_if = "Option::is_none")]
1131    pub opts: Option<FileOpts>,
1132    #[serde(default, skip_serializing_if = "is_zero_i64")]
1133    pub size: i64,
1134    #[serde(default, skip_serializing_if = "Option::is_none")]
1135    pub meta: Option<HashMap<String, serde_json::Value>>,
1136    #[serde(default, skip_serializing_if = "is_zero_i64")]
1137    pub modtime: i64,
1138    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
1139    pub isdir: bool,
1140    #[serde(default, skip_serializing_if = "String::is_empty")]
1141    pub mimetype: String,
1142    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
1143    pub readonly: bool,
1144}
1145
1146/// Matches Go's `FileOpts`
1147#[derive(Debug, Clone, Serialize, Deserialize, Default)]
1148pub struct FileOpts {
1149    #[serde(default, skip_serializing_if = "is_zero_i64")]
1150    pub maxsize: i64,
1151    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
1152    pub circular: bool,
1153    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
1154    pub ijson: bool,
1155    #[serde(default, skip_serializing_if = "is_zero_usize")]
1156    pub ijsonbudget: usize,
1157    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
1158    pub truncate: bool,
1159    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
1160    pub append: bool,
1161}
1162
1163/// Matches Go's `CommandEventReadHistoryData`
1164#[derive(Debug, Clone, Serialize, Deserialize)]
1165pub struct CommandEventReadHistoryData {
1166    pub event: String,
1167    pub scope: String,
1168    #[serde(default)]
1169    pub maxitems: usize,
1170}
1171
1172/// Matches Go's `CommandWaitForRouteData`
1173#[derive(Debug, Clone, Serialize, Deserialize)]
1174pub struct CommandWaitForRouteData {
1175    pub routeid: String,
1176    #[serde(default)]
1177    pub waitms: i64,
1178}
1179
1180/// Matches Go's `BlockInfoData`
1181#[derive(Debug, Clone, Serialize, Deserialize)]
1182pub struct BlockInfoData {
1183    pub blockid: String,
1184    pub tabid: String,
1185    pub workspaceid: String,
1186    #[serde(default, skip_serializing_if = "Option::is_none")]
1187    pub block: Option<Block>,
1188    #[serde(default, skip_serializing_if = "Option::is_none")]
1189    pub files: Option<Vec<FileInfo>>,
1190}
1191
1192/// Matches Go's `WaveInfoData`
1193#[derive(Debug, Clone, Serialize, Deserialize, Default)]
1194pub struct WaveInfoData {
1195    #[serde(default)]
1196    pub version: String,
1197    #[serde(default)]
1198    pub clientid: String,
1199    #[serde(default)]
1200    pub buildtime: String,
1201    #[serde(default)]
1202    pub configdir: String,
1203    #[serde(default)]
1204    pub datadir: String,
1205}
1206
1207/// Matches Go's `WorkspaceInfoData`
1208#[derive(Debug, Clone, Serialize, Deserialize)]
1209pub struct WorkspaceInfoData {
1210    pub windowid: String,
1211    #[serde(default, skip_serializing_if = "Option::is_none")]
1212    pub workspacedata: Option<Workspace>,
1213}
1214
1215/// Matches Go's `ConnStatus`
1216#[derive(Debug, Clone, Serialize, Deserialize, Default)]
1217pub struct ConnStatus {
1218    pub status: String,
1219    #[serde(default)]
1220    pub connection: String,
1221    #[serde(default)]
1222    pub connected: bool,
1223    #[serde(default)]
1224    pub hasconnected: bool,
1225    #[serde(default)]
1226    pub activeconnnum: i32,
1227    #[serde(default, skip_serializing_if = "String::is_empty")]
1228    pub error: String,
1229}
1230
1231/// Matches Go's `WaveNotificationOptions`
1232#[derive(Debug, Clone, Serialize, Deserialize, Default)]
1233pub struct WaveNotificationOptions {
1234    #[serde(default, skip_serializing_if = "String::is_empty")]
1235    pub title: String,
1236    #[serde(default, skip_serializing_if = "String::is_empty")]
1237    pub body: String,
1238    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
1239    pub silent: bool,
1240}
1241
1242/// Matches Go's `RpcOpts`
1243#[derive(Debug, Clone, Serialize, Deserialize, Default)]
1244pub struct RpcOpts {
1245    #[serde(default, skip_serializing_if = "is_zero_i64")]
1246    pub timeout: i64,
1247    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
1248    pub noresponse: bool,
1249    #[serde(default, skip_serializing_if = "String::is_empty")]
1250    pub route: String,
1251}
1252
1253/// Matches Go's `RpcContext`
1254#[derive(Debug, Clone, Serialize, Deserialize, Default)]
1255pub struct RpcContext {
1256    #[serde(default, skip_serializing_if = "String::is_empty", rename = "ctype")]
1257    pub client_type: String,
1258    #[serde(default, skip_serializing_if = "String::is_empty")]
1259    pub blockid: String,
1260    #[serde(default, skip_serializing_if = "String::is_empty")]
1261    pub tabid: String,
1262    #[serde(default, skip_serializing_if = "String::is_empty")]
1263    pub conn: String,
1264}
1265
1266/// Matches Go's `CommandVarData`
1267#[derive(Debug, Clone, Serialize, Deserialize)]
1268pub struct CommandVarData {
1269    pub key: String,
1270    #[serde(default, skip_serializing_if = "String::is_empty")]
1271    pub val: String,
1272    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
1273    pub remove: bool,
1274    #[serde(default)]
1275    pub zoneid: String,
1276    #[serde(default)]
1277    pub filename: String,
1278}
1279
1280/// Matches Go's `CommandVarResponseData`
1281#[derive(Debug, Clone, Serialize, Deserialize)]
1282pub struct CommandVarResponseData {
1283    pub key: String,
1284    #[serde(default)]
1285    pub val: String,
1286    #[serde(default)]
1287    pub exists: bool,
1288}
1289
1290/// Matches Go's `TimeSeriesData`
1291#[derive(Debug, Clone, Serialize, Deserialize)]
1292pub struct TimeSeriesData {
1293    pub ts: i64,
1294    pub values: HashMap<String, f64>,
1295}
1296
1297/// Matches Go's `RemoteInfo`
1298#[derive(Debug, Clone, Serialize, Deserialize, Default)]
1299pub struct RemoteInfo {
1300    #[serde(default)]
1301    pub clientarch: String,
1302    #[serde(default)]
1303    pub clientos: String,
1304    #[serde(default)]
1305    pub clientversion: String,
1306    #[serde(default)]
1307    pub shell: String,
1308}
1309
1310// ---- Helper functions ----
1311
1312fn is_zero_i64(v: &i64) -> bool {
1313    *v == 0
1314}
1315
1316fn is_zero_usize(v: &usize) -> bool {
1317    *v == 0
1318}
1319
1320// ---- Agent command data types ----
1321
1322/// Optional filter input for `listagents`. When `is_seeded` is set,
1323/// only definitions whose `is_seeded` column matches are returned
1324/// (`Some(1)` → templates only; `Some(0)` → user-owned agents only).
1325/// Absent / `None` = no filter — backward-compatible with callers
1326/// that pass `{}` or `null`. Phase 1 of the two-tier picker
1327/// (SPEC_AGENT_PICKER_TWO_TIER_2026_05_24.md).
1328///
1329/// `include_hidden` (Phase 2 — Q2 Decision Y): when `false` (default),
1330/// templates with `user_hidden = 1` are filtered out. The settings
1331/// "Hidden templates" surface passes `true` so it can render rows for
1332/// unhiding; the picker proper omits the flag and gets the filtered
1333/// default. `include_hidden` only affects templates — user-owned rows
1334/// never set `user_hidden`, so the flag is a no-op for them.
1335#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1336pub struct CommandListAgentDefinitionsData {
1337    #[serde(default, skip_serializing_if = "Option::is_none")]
1338    pub is_seeded: Option<i64>,
1339    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
1340    pub include_hidden: bool,
1341}
1342
1343/// Request for `agentdefcreatefromtemplate`. Clones a seeded template
1344/// into a new user-owned definition. Phase 1 of the two-tier picker.
1345#[derive(Debug, Clone, Serialize, Deserialize)]
1346pub struct CommandAgentDefCreateFromTemplateData {
1347    /// id of a seeded definition (must have `is_seeded = 1`).
1348    pub template_id: String,
1349    /// User-chosen display name for the new agent. Non-empty, ≤200
1350    /// chars, must not collide with another user-owned agent's name.
1351    pub name: String,
1352    /// Identity bundle id to bind (empty string = ambient creds).
1353    /// Stored on the launch-time `db_agent_instances` row by the
1354    /// launch flow; the definition itself doesn't hold bindings
1355    /// pre-Phase 3, so this is reserved for the frontend to thread
1356    /// through to its subsequent `launchAgentDefinition` call. The
1357    /// server returns it back in the response for symmetry +
1358    /// future-proofing.
1359    #[serde(default)]
1360    pub identity_id: String,
1361    /// Memory bundle id to bind (empty string = vanilla CLI).
1362    /// Same semantics as `identity_id` above.
1363    #[serde(default)]
1364    pub memory_id: String,
1365}
1366
1367/// Response for `agentdefcreatefromtemplate`. The frontend uses
1368/// `definition_id` to launch the freshly-created agent.
1369#[derive(Debug, Clone, Serialize, Deserialize)]
1370pub struct AgentDefCreateFromTemplateResult {
1371    pub definition_id: String,
1372    /// Echoed back so the caller's launch step doesn't need to
1373    /// re-thread these — they flow through to the launch overrides.
1374    pub identity_id: String,
1375    pub memory_id: String,
1376}
1377
1378/// Request for `agentdefhide` / `agentdefunhide`. Phase 2 of the
1379/// two-tier picker (Q2 Decision Y). The two RPCs share the same shape
1380/// — the action is encoded in the command name, not the payload.
1381#[derive(Debug, Clone, Serialize, Deserialize)]
1382pub struct CommandAgentDefHideData {
1383    /// id of a seeded definition (must have `is_seeded = 1`).
1384    pub definition_id: String,
1385}
1386
1387/// Response for `agentdefhide` / `agentdefunhide`. `ok = true` when a
1388/// row was updated; `false` when the id didn't match any row. (A row
1389/// that exists but isn't a template returns an RPC-level error, not
1390/// `ok: false` — the caller should never have been able to send that
1391/// id from the picker UI.)
1392#[derive(Debug, Clone, Serialize, Deserialize)]
1393pub struct AgentDefHideResult {
1394    pub ok: bool,
1395}
1396
1397/// Input for createagent
1398#[derive(Debug, Clone, Serialize, Deserialize)]
1399pub struct CommandCreateAgentDefinitionData {
1400    pub name: String,
1401    #[serde(default = "default_agent_icon")]
1402    pub icon: String,
1403    pub provider: String,
1404    #[serde(default)]
1405    pub description: String,
1406    #[serde(default)]
1407    pub working_directory: String,
1408    #[serde(default)]
1409    pub shell: String,
1410    #[serde(default)]
1411    pub provider_flags: String,
1412    #[serde(default)]
1413    pub auto_start: i64,
1414    #[serde(default)]
1415    pub restart_on_crash: i64,
1416    #[serde(default)]
1417    pub idle_timeout_minutes: i64,
1418    #[serde(default = "default_agent_type")]
1419    pub agent_type: String,
1420    #[serde(default)]
1421    pub environment: String,
1422    #[serde(default)]
1423    pub agent_bus_id: String,
1424}
1425
1426fn default_agent_type() -> String {
1427    "standalone".to_string()
1428}
1429
1430fn default_agent_icon() -> String {
1431    "✦".to_string()
1432}
1433
1434/// Input for updateagent
1435#[derive(Debug, Clone, Serialize, Deserialize)]
1436pub struct CommandUpdateAgentDefinitionData {
1437    pub id: String,
1438    pub name: String,
1439    pub icon: String,
1440    pub provider: String,
1441    #[serde(default)]
1442    pub description: String,
1443    #[serde(default)]
1444    pub working_directory: String,
1445    #[serde(default)]
1446    pub shell: String,
1447    #[serde(default)]
1448    pub provider_flags: String,
1449    #[serde(default)]
1450    pub auto_start: i64,
1451    #[serde(default)]
1452    pub restart_on_crash: i64,
1453    #[serde(default)]
1454    pub idle_timeout_minutes: i64,
1455    #[serde(default = "default_agent_type")]
1456    pub agent_type: String,
1457    #[serde(default)]
1458    pub environment: String,
1459    #[serde(default)]
1460    pub agent_bus_id: String,
1461    /// JSON-encoded per-provider account assignments (see
1462    /// `AgentDefinition.accounts`). Written by the Agent pane's Identity tab.
1463    #[serde(default)]
1464    pub accounts: String,
1465}
1466
1467/// Input for deleteagent
1468#[derive(Debug, Clone, Serialize, Deserialize)]
1469pub struct CommandDeleteAgentDefinitionData {
1470    pub id: String,
1471}
1472
1473/// Input for getagentcontent
1474#[derive(Debug, Clone, Serialize, Deserialize)]
1475pub struct CommandGetAgentContentData {
1476    pub agent_id: String,
1477    pub content_type: String,
1478}
1479
1480/// Input for setagentcontent
1481#[derive(Debug, Clone, Serialize, Deserialize)]
1482pub struct CommandSetAgentContentData {
1483    pub agent_id: String,
1484    pub content_type: String,
1485    pub content: String,
1486}
1487
1488/// Input for getallagentcontent
1489#[derive(Debug, Clone, Serialize, Deserialize)]
1490pub struct CommandGetAllAgentContentData {
1491    pub agent_id: String,
1492}
1493
1494// ---- Agent Skills command data types ----
1495
1496/// Input for listagentskills
1497#[derive(Debug, Clone, Serialize, Deserialize)]
1498pub struct CommandListAgentSkillsData {
1499    pub agent_id: String,
1500}
1501
1502/// Input for createagentskill
1503#[derive(Debug, Clone, Serialize, Deserialize)]
1504pub struct CommandCreateAgentSkillData {
1505    pub agent_id: String,
1506    pub name: String,
1507    #[serde(default)]
1508    pub trigger: String,
1509    #[serde(default = "default_skill_type")]
1510    pub skill_type: String,
1511    #[serde(default)]
1512    pub description: String,
1513    #[serde(default)]
1514    pub content: String,
1515}
1516
1517fn default_skill_type() -> String {
1518    "prompt".to_string()
1519}
1520
1521/// Input for updateagentskill
1522#[derive(Debug, Clone, Serialize, Deserialize)]
1523pub struct CommandUpdateAgentSkillData {
1524    pub id: String,
1525    pub name: String,
1526    #[serde(default)]
1527    pub trigger: String,
1528    #[serde(default)]
1529    pub skill_type: String,
1530    #[serde(default)]
1531    pub description: String,
1532    #[serde(default)]
1533    pub content: String,
1534}
1535
1536/// Input for deleteagentskill
1537#[derive(Debug, Clone, Serialize, Deserialize)]
1538pub struct CommandDeleteAgentSkillData {
1539    pub id: String,
1540}
1541
1542// ---- Agent History command data types ----
1543
1544/// Input for appendagenthistory
1545#[derive(Debug, Clone, Serialize, Deserialize)]
1546pub struct CommandAppendAgentHistoryData {
1547    pub agent_id: String,
1548    pub entry: String,
1549}
1550
1551/// Input for listagenthistory
1552#[derive(Debug, Clone, Serialize, Deserialize)]
1553pub struct CommandListAgentHistoryData {
1554    pub agent_id: String,
1555    #[serde(default)]
1556    pub session_date: Option<String>,
1557    #[serde(default = "default_history_limit")]
1558    pub limit: i64,
1559    #[serde(default)]
1560    pub offset: i64,
1561}
1562
1563fn default_history_limit() -> i64 {
1564    50
1565}
1566
1567/// Input for searchagenthistory
1568#[derive(Debug, Clone, Serialize, Deserialize)]
1569pub struct CommandSearchAgentHistoryData {
1570    pub agent_id: String,
1571    pub query: String,
1572    #[serde(default = "default_history_limit")]
1573    pub limit: i64,
1574}
1575
1576// ---- Agent Import command data types ----
1577
1578/// Input for importagentfromclaw
1579#[derive(Debug, Clone, Serialize, Deserialize)]
1580pub struct CommandImportAgentFromClawData {
1581    pub workspace_path: String,
1582    pub agent_name: String,
1583}
1584
1585/// Input for importagents
1586#[derive(Debug, Clone, Serialize, Deserialize)]
1587pub struct CommandImportAgentDefinitionsData {
1588    pub agents: Vec<AgentDefinitionImport>,
1589}
1590
1591#[derive(Debug, Clone, Serialize, Deserialize)]
1592pub struct AgentDefinitionImport {
1593    pub id: String,
1594    pub name: String,
1595    pub icon: String,
1596    pub description: String,
1597    pub provider: String,
1598    pub shell: String,
1599    pub working_directory: String,
1600    pub agent_bus_id: String,
1601    pub agent_type: String,
1602    pub environment: String,
1603    pub restart_on_crash: bool,
1604    pub content: std::collections::HashMap<String, String>,
1605    pub skills: Vec<AgentSkillImport>,
1606}
1607
1608#[derive(Debug, Clone, Serialize, Deserialize)]
1609pub struct AgentSkillImport {
1610    pub name: String,
1611    pub trigger: String,
1612    pub skill_type: String,
1613    pub description: String,
1614    pub content: String,
1615}
1616
1617#[derive(Debug, Clone, Serialize, Deserialize)]
1618pub struct ImportAgentDefinitionsResult {
1619    pub imported: Vec<String>,
1620    pub skipped: Vec<String>,
1621    pub failed: Vec<String>,
1622}
1623
1624/// Response for exportagents
1625#[derive(Debug, Clone, Serialize, Deserialize)]
1626pub struct ExportAgentDefinitionsResult {
1627    pub version: u32,
1628    pub exported_at: String,
1629    pub source: String,
1630    pub agents: Vec<AgentDefinitionExport>,
1631}
1632
1633#[derive(Debug, Clone, Serialize, Deserialize)]
1634pub struct AgentDefinitionExport {
1635    pub id: String,
1636    pub name: String,
1637    pub icon: String,
1638    pub description: String,
1639    pub provider: String,
1640    pub shell: String,
1641    pub working_directory: String,
1642    pub agent_bus_id: String,
1643    pub agent_type: String,
1644    pub environment: String,
1645    pub restart_on_crash: bool,
1646    pub content: std::collections::HashMap<String, String>,
1647    pub skills: Vec<AgentSkillExport>,
1648}
1649
1650#[derive(Debug, Clone, Serialize, Deserialize)]
1651pub struct AgentSkillExport {
1652    pub name: String,
1653    pub trigger: String,
1654    pub skill_type: String,
1655    pub description: String,
1656    pub content: String,
1657}
1658
1659// ====================================================================
1660// Tool store commands
1661// ====================================================================
1662
1663pub const COMMAND_GET_TOOL_STATUS: &str = "gettoolstatus";
1664pub const COMMAND_INSTALL_TOOL: &str = "installtool";
1665
1666#[derive(Debug, Clone, Serialize, Deserialize, Default)]
1667pub struct CommandInstallToolData {
1668    pub tool_ids: Vec<String>,
1669}
1670
1671#[derive(Debug, Clone, Serialize, Deserialize)]
1672pub struct GetToolStatusResult {
1673    pub tools: Vec<crate::backend::tool_store::ToolStatusEntry>,
1674}
1675
1676#[derive(Debug, Clone, Serialize, Deserialize)]
1677pub struct InstallToolResult {
1678    pub installed: Vec<String>,
1679    pub failed: Vec<InstallFailure>,
1680}
1681
1682#[derive(Debug, Clone, Serialize, Deserialize)]
1683pub struct InstallFailure {
1684    pub id: String,
1685    pub error: String,
1686}
1687
1688// ====================================================================
1689// Identity / Instance / Fork payloads (v6)
1690// See specs/SPEC_FORGE_IDENTITY_AGENT_INSTANCES_IMPL_2026_04_20.md.
1691// Strings use snake_case for cross-language parity with wstore.
1692// ====================================================================
1693
1694#[derive(Debug, Clone, Serialize, Deserialize, Default)]
1695pub struct CommandListIdentityAccountsData {
1696    #[serde(default, skip_serializing_if = "Option::is_none")]
1697    pub provider: Option<String>,
1698}
1699
1700#[derive(Debug, Clone, Serialize, Deserialize)]
1701pub struct CommandGetIdentityAccountData {
1702    pub id: String,
1703}
1704
1705#[derive(Debug, Clone, Serialize, Deserialize)]
1706pub struct CommandDeleteIdentityAccountData {
1707    pub id: String,
1708}
1709
1710#[derive(Debug, Clone, Serialize, Deserialize)]
1711pub struct CommandLinkAgentIdentityData {
1712    pub agent_id: String,
1713    pub account_id: String,
1714    pub provider: String,
1715}
1716
1717#[derive(Debug, Clone, Serialize, Deserialize)]
1718pub struct CommandUnlinkAgentIdentityData {
1719    pub agent_id: String,
1720    pub provider: String,
1721}
1722
1723#[derive(Debug, Clone, Serialize, Deserialize)]
1724pub struct CommandListAgentIdentitiesData {
1725    pub agent_id: String,
1726}
1727
1728// ---- v7 Identity bundle command shapes ----
1729
1730#[derive(Debug, Clone, Serialize, Deserialize)]
1731pub struct CommandGetIdentityBundleData {
1732    pub id: String,
1733}
1734
1735#[derive(Debug, Clone, Serialize, Deserialize)]
1736pub struct CommandDeleteIdentityBundleData {
1737    pub id: String,
1738}
1739
1740#[derive(Debug, Clone, Serialize, Deserialize)]
1741pub struct CommandBindIdentityAccountData {
1742    pub identity_id: String,
1743    pub provider: String,
1744    pub account_id: String,
1745}
1746
1747#[derive(Debug, Clone, Serialize, Deserialize)]
1748pub struct CommandUnbindIdentityAccountData {
1749    pub identity_id: String,
1750    pub provider: String,
1751}
1752
1753#[derive(Debug, Clone, Serialize, Deserialize)]
1754pub struct CommandListIdentityBindingsData {
1755    pub identity_id: String,
1756}
1757
1758// ---- v7 Memory bundle command shapes ----
1759
1760#[derive(Debug, Clone, Serialize, Deserialize)]
1761pub struct CommandGetMemoryData {
1762    pub id: String,
1763}
1764
1765#[derive(Debug, Clone, Serialize, Deserialize)]
1766pub struct CommandDeleteMemoryData {
1767    pub id: String,
1768}
1769
1770#[derive(Debug, Clone, Serialize, Deserialize, Default)]
1771pub struct CommandListAgentInstancesData {
1772    #[serde(default, skip_serializing_if = "Option::is_none")]
1773    pub definition_id: Option<String>,
1774    #[serde(default, skip_serializing_if = "Option::is_none")]
1775    pub status: Option<String>,
1776}
1777
1778#[derive(Debug, Clone, Serialize, Deserialize)]
1779pub struct CommandGetAgentInstanceData {
1780    pub id: String,
1781}
1782
1783#[derive(Debug, Clone, Serialize, Deserialize)]
1784pub struct CommandCreateAgentInstanceData {
1785    pub definition_id: String,
1786    #[serde(default)]
1787    pub block_id: String,
1788    #[serde(default)]
1789    pub parent_instance_id: String,
1790    /// FK to db_identity_bundles. Empty = blank singleton (no env-var
1791    /// injection; agent inherits ambient creds). Set by the launch
1792    /// modal's Identity dropdown.
1793    #[serde(default)]
1794    pub identity_id: String,
1795    /// FK to db_memory_bundles. Empty = blank singleton. Set by the launch
1796    /// modal's Memory dropdown.
1797    #[serde(default)]
1798    pub memory_id: String,
1799    /// User-chosen instance name (becomes `AGENTMUX_AGENT_ID` in the
1800    /// spawn env). Powers the launch modal's "Continue agent"
1801    /// dropdown. Empty = un-named, won't appear in the dropdown.
1802    #[serde(default)]
1803    pub instance_name: String,
1804    /// Absolute working directory path resolved by
1805    /// `allocate_agent_workdir` at spawn time. Stored on the instance
1806    /// row so the continue flow can reuse it without re-deriving the
1807    /// slug.
1808    #[serde(default)]
1809    pub working_directory: String,
1810}
1811
1812/// Request for `listnamedagents`. The launch modal's "Continue
1813/// agent" dropdown calls this; an absent / zero `limit` defaults to
1814/// 200 (capped at 1000 to keep the wire payload bounded).
1815///
1816/// `definition_id` is server-side filtering: when provided, only
1817/// instances of that definition are returned. Required for the
1818/// dropdown to behave correctly when a user has 200+ named agents
1819/// across many definitions — without server filtering, the current
1820/// definition's older instances could fall off the global cap.
1821#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1822pub struct CommandListNamedAgentsData {
1823    #[serde(default)]
1824    pub limit: usize,
1825    #[serde(default, skip_serializing_if = "Option::is_none")]
1826    pub definition_id: Option<String>,
1827}
1828
1829/// One row of the launch modal's "Continue agent" dropdown. Joins
1830/// `db_agent_instances` with `db_agent_definitions` (for the definition's
1831/// display name + provider) and `db_identity_bundles` / `db_memory_bundles`
1832/// (for bundle names) so the frontend renders without further lookups.
1833#[derive(Debug, Clone, Serialize, Deserialize)]
1834pub struct NamedAgentRow {
1835    pub instance_id: String,
1836    pub instance_name: String,
1837    pub definition_id: String,
1838    pub definition_name: String,
1839    pub provider: String,
1840    pub working_directory: String,
1841    pub identity_id: String,
1842    pub identity_name: String,
1843    pub memory_id: String,
1844    pub memory_name: String,
1845    pub started_at: i64,
1846    pub ended_at: i64,
1847    pub status: String,
1848    pub block_id_hint: String,
1849}
1850
1851/// Request for `hidenamedagent`. Sets `display_hidden = 1` on the
1852/// row. Row + working directory remain on disk for audit + recovery
1853/// (destructive deletion is a separate, confirm-gated flow).
1854#[derive(Debug, Clone, Serialize, Deserialize)]
1855pub struct CommandHideNamedAgentData {
1856    pub id: String,
1857}
1858
1859/// Request for `listrecentsessions` — powers the AgentPicker's "Recent
1860/// sessions" surface (cascade follow-up, 2026-05-23). Optional
1861/// `identity_id` filter narrows the results to sessions that used the
1862/// given identity bundle (matches `db_agent_instances.identity_id`).
1863/// `limit` defaults to 20 (capped at 100); rows are sorted by the
1864/// most-recent activity timestamp (filestore `output.state.json` modts
1865/// when available, otherwise instance `started_at`).
1866#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1867pub struct CommandListRecentSessionsData {
1868    #[serde(default)]
1869    pub limit: usize,
1870    /// When set + non-empty, filter to sessions whose `identity_id`
1871    /// matches. `Some("")` is treated the same as `None` (no filter)
1872    /// to make the frontend wiring straightforward.
1873    #[serde(default, skip_serializing_if = "Option::is_none")]
1874    pub identity_id: Option<String>,
1875}
1876
1877/// One row of the AgentPicker's "Recent sessions" list. Mirrors
1878/// `NamedAgentRow` but adds preview fields read from the per-block
1879/// `output.state.json` snapshot in filestore. `node_count == 0` and an
1880/// empty `preview` mean the snapshot wasn't readable (the block may
1881/// pre-date the persistence flow or have crashed before its first
1882/// 30s snapshot) — the row still surfaces so the user can reattach.
1883#[derive(Debug, Clone, Serialize, Deserialize)]
1884pub struct RecentSessionRow {
1885    pub instance_id: String,
1886    pub instance_name: String,
1887    pub definition_id: String,
1888    pub definition_name: String,
1889    pub provider: String,
1890    pub working_directory: String,
1891    pub identity_id: String,
1892    pub identity_name: String,
1893    pub memory_id: String,
1894    pub memory_name: String,
1895    /// The pane / block whose filestore zone holds the conversation.
1896    /// Empty when the instance row exists but no SQLite row resolved
1897    /// the block_id (cross-version registry rows). Reattach falls
1898    /// back to the working-directory continuation path in that case.
1899    pub block_id_hint: String,
1900    /// The CLI-emitted session id (`session_id` for Claude/Gemini,
1901    /// `thread_id` for Codex) captured during the prior run. Empty
1902    /// when the row predates the capture, the CLI didn't emit a
1903    /// session id, or the instance was created via a path that
1904    /// doesn't go through the spawn that captures it. Used by the
1905    /// picker reattach flow to populate `agent:sessionid` on the new
1906    /// block's meta so the spawned subprocess gets a real
1907    /// `--resume <sid>` on the FIRST turn instead of starting a
1908    /// fresh conversation that re-injects the startup context.
1909    #[serde(default)]
1910    pub session_id: String,
1911    /// Snapshot of the first user message in the conversation (up to
1912    /// 240 chars, newlines collapsed). Empty when the snapshot doesn't
1913    /// exist or doesn't contain a user_message node yet.
1914    pub preview: String,
1915    /// Total `nodes.length` from the snapshot. 0 when unavailable.
1916    pub node_count: usize,
1917    /// Last activity timestamp (filestore modts when the snapshot
1918    /// exists, otherwise `started_at`). Drives the sort order.
1919    pub last_active_at: i64,
1920    /// Whether `output.state.json` was found in filestore for this
1921    /// block. False when the snapshot doesn't exist yet (no preview)
1922    /// or the block_id_hint was empty.
1923    pub has_snapshot: bool,
1924}
1925
1926/// Mutable subset of AgentInstance for PATCH-style updates. Every field is
1927/// optional — absent fields preserve their current value.
1928#[derive(Debug, Clone, Serialize, Deserialize)]
1929pub struct CommandUpdateAgentInstanceData {
1930    pub id: String,
1931    #[serde(default, skip_serializing_if = "Option::is_none")]
1932    pub block_id: Option<String>,
1933    #[serde(default, skip_serializing_if = "Option::is_none")]
1934    pub session_id: Option<String>,
1935    #[serde(default, skip_serializing_if = "Option::is_none")]
1936    pub status: Option<String>,
1937    /// JSON-encoded `GitHubContext` or empty string. `None` = leave as-is;
1938    /// `Some("")` = explicitly clear.
1939    #[serde(default, skip_serializing_if = "Option::is_none")]
1940    pub github_context: Option<String>,
1941    #[serde(default, skip_serializing_if = "Option::is_none")]
1942    pub ended_at: Option<i64>,
1943}
1944
1945#[derive(Debug, Clone, Serialize, Deserialize)]
1946pub struct CommandDeleteAgentInstanceData {
1947    pub id: String,
1948}
1949
1950#[derive(Debug, Clone, Serialize, Deserialize)]
1951pub struct CommandForkAgentDefinitionData {
1952    pub source_id: String,
1953    #[serde(default)]
1954    pub branch_label: String,
1955}
1956
1957// ====================================================================
1958// Option E — agent-anchored session zones (PR 1 of 2)
1959// See docs/specs/SPEC_CONTINUATION_SESSION_PERSISTENCE_2026_05_23.md.
1960// ====================================================================
1961
1962/// Request for `agent:session:read`.
1963#[derive(Debug, Clone, Serialize, Deserialize)]
1964pub struct CommandAgentSessionReadData {
1965    pub definition_id: String,
1966}
1967
1968/// Response for `agent:session:read`. `content == None` means no zone /
1969/// snapshot exists for this definition (NOT an error — fresh agent).
1970#[derive(Debug, Clone, Serialize, Deserialize)]
1971pub struct AgentSessionReadResult {
1972    #[serde(default, skip_serializing_if = "Option::is_none")]
1973    pub content: Option<String>,
1974    /// `modts` of the `output.state.json` file in the agent's
1975    /// `:current` zone, if it exists.
1976    #[serde(default, skip_serializing_if = "Option::is_none")]
1977    pub modts: Option<i64>,
1978}
1979
1980/// Request for `agent:session:write_state`. Writes `output.state.json`
1981/// into `agent:<definition_id>:current` (creates the zone if missing).
1982#[derive(Debug, Clone, Serialize, Deserialize)]
1983pub struct CommandAgentSessionWriteStateData {
1984    pub definition_id: String,
1985    pub content: String,
1986}
1987
1988#[derive(Debug, Clone, Serialize, Deserialize)]
1989pub struct AgentSessionWriteStateResult {
1990    pub bytes_written: u64,
1991}
1992
1993/// Request for `agent:session:append_output`. Appends a single
1994/// NDJSON line to `output` in `agent:<definition_id>:current`.
1995#[derive(Debug, Clone, Serialize, Deserialize)]
1996pub struct CommandAgentSessionAppendOutputData {
1997    pub definition_id: String,
1998    pub line: String,
1999}
2000
2001#[derive(Debug, Clone, Serialize, Deserialize)]
2002pub struct AgentSessionAppendOutputResult {
2003    pub bytes_written: u64,
2004}
2005
2006/// Request for `agent:session:archive`. Snapshots `agent:<defId>:current`
2007/// into `agent:<defId>:archive:<now_ms>` then clears the current zone.
2008/// Returns the archive zoneid (empty if no-op).
2009#[derive(Debug, Clone, Serialize, Deserialize)]
2010pub struct CommandAgentSessionArchiveData {
2011    pub definition_id: String,
2012}
2013
2014#[derive(Debug, Clone, Serialize, Deserialize)]
2015pub struct AgentSessionArchiveResult {
2016    /// Empty string when nothing was archived (current zone was empty).
2017    pub archive_zoneid: String,
2018    pub archived_at_ms: i64,
2019}
2020
2021/// Request for `agent:session:list_archives`.
2022#[derive(Debug, Clone, Default, Serialize, Deserialize)]
2023pub struct CommandAgentSessionListArchivesData {
2024    pub definition_id: String,
2025    #[serde(default)]
2026    pub limit: usize,
2027}
2028
2029/// One row of the agent's archive list. Mirrors `RecentSessionRow`
2030/// preview shape so the frontend can reuse the same row component.
2031#[derive(Debug, Clone, Serialize, Deserialize)]
2032pub struct AgentArchiveRow {
2033    pub archive_zoneid: String,
2034    pub archived_at_ms: i64,
2035    /// First user_message in the archived `output.state.json` (up to
2036    /// 240 chars, newlines collapsed). Empty when unreadable.
2037    pub preview: String,
2038    /// Total `nodes.length` from the archived snapshot. 0 when
2039    /// unreadable / missing.
2040    pub node_count: usize,
2041}
2042
2043// ====================================================================
2044// Tests
2045// ====================================================================
2046
2047#[cfg(test)]
2048mod tests {
2049    use super::*;
2050
2051    #[test]
2052    fn test_rpc_message_command_roundtrip() {
2053        let msg = RpcMessage {
2054            command: "getmeta".to_string(),
2055            reqid: "req-123".to_string(),
2056            timeout: 5000,
2057            data: Some(serde_json::json!({"oref": "block:abc-123"})),
2058            ..Default::default()
2059        };
2060        let json = serde_json::to_string(&msg).unwrap();
2061        let parsed: RpcMessage = serde_json::from_str(&json).unwrap();
2062        assert_eq!(parsed.command, "getmeta");
2063        assert_eq!(parsed.reqid, "req-123");
2064        assert_eq!(parsed.timeout, 5000);
2065        assert!(parsed.data.is_some());
2066    }
2067
2068    #[test]
2069    fn test_rpc_message_response_roundtrip() {
2070        let msg = RpcMessage {
2071            reqid: "req-123".to_string(),
2072            resid: "res-456".to_string(),
2073            data: Some(serde_json::json!({"view": "term"})),
2074            ..Default::default()
2075        };
2076        let json = serde_json::to_string(&msg).unwrap();
2077        let parsed: RpcMessage = serde_json::from_str(&json).unwrap();
2078        assert_eq!(parsed.reqid, "req-123");
2079        assert_eq!(parsed.resid, "res-456");
2080    }
2081
2082    #[test]
2083    fn test_rpc_message_empty_fields_omitted() {
2084        let msg = RpcMessage {
2085            command: "test".to_string(),
2086            ..Default::default()
2087        };
2088        let json = serde_json::to_string(&msg).unwrap();
2089        assert!(!json.contains("reqid"));
2090        assert!(!json.contains("resid"));
2091        assert!(!json.contains("timeout"));
2092        assert!(!json.contains("cont"));
2093        assert!(!json.contains("cancel"));
2094    }
2095
2096    #[test]
2097    fn test_rpc_message_validate_command() {
2098        let msg = RpcMessage {
2099            command: "getmeta".to_string(),
2100            ..Default::default()
2101        };
2102        assert!(msg.validate().is_ok());
2103    }
2104
2105    #[test]
2106    fn test_rpc_message_validate_cancel() {
2107        let msg = RpcMessage {
2108            cancel: true,
2109            reqid: "req-1".to_string(),
2110            ..Default::default()
2111        };
2112        assert!(msg.validate().is_ok());
2113
2114        // cancel without reqid or resid
2115        let bad = RpcMessage {
2116            cancel: true,
2117            ..Default::default()
2118        };
2119        assert!(bad.validate().is_err());
2120    }
2121
2122    #[test]
2123    fn test_rpc_message_validate_empty() {
2124        let msg = RpcMessage::default();
2125        assert!(msg.validate().is_err());
2126    }
2127
2128    #[test]
2129    fn test_rpc_message_validate_both_ids() {
2130        let msg = RpcMessage {
2131            reqid: "a".to_string(),
2132            resid: "b".to_string(),
2133            ..Default::default()
2134        };
2135        assert!(msg.validate().is_err());
2136    }
2137
2138    #[test]
2139    fn test_command_get_meta_data() {
2140        let data = CommandGetMetaData {
2141            oref: ORef::new("block", "550e8400-e29b-41d4-a716-446655440000"),
2142        };
2143        let json = serde_json::to_string(&data).unwrap();
2144        let parsed: CommandGetMetaData = serde_json::from_str(&json).unwrap();
2145        assert_eq!(parsed.oref.otype, "block");
2146    }
2147
2148    #[test]
2149    fn test_command_set_meta_data() {
2150        let mut meta = MetaMapType::new();
2151        meta.insert("view".into(), serde_json::json!("term"));
2152
2153        let data = CommandSetMetaData {
2154            oref: ORef::new("block", "550e8400-e29b-41d4-a716-446655440000"),
2155            meta,
2156        };
2157        let json = serde_json::to_string(&data).unwrap();
2158        let parsed: CommandSetMetaData = serde_json::from_str(&json).unwrap();
2159        assert_eq!(parsed.meta["view"], "term");
2160    }
2161
2162    #[test]
2163    fn test_wire_compat_go_rpc_message() {
2164        // Simulated Go-produced JSON
2165        let go_json = r#"{"command":"getmeta","reqid":"abc","timeout":5000,"data":{"oref":"block:123"}}"#;
2166        let msg: RpcMessage = serde_json::from_str(go_json).unwrap();
2167        assert_eq!(msg.command, "getmeta");
2168        assert_eq!(msg.reqid, "abc");
2169        assert_eq!(msg.timeout, 5000);
2170    }
2171
2172    #[test]
2173    fn test_rpc_context_roundtrip() {
2174        let ctx = RpcContext {
2175            client_type: "connserver".to_string(),
2176            blockid: "blk-1".to_string(),
2177            tabid: "tab-1".to_string(),
2178            conn: "local".to_string(),
2179        };
2180        let json = serde_json::to_string(&ctx).unwrap();
2181        assert!(json.contains(r#""ctype":"connserver""#));
2182        let parsed: RpcContext = serde_json::from_str(&json).unwrap();
2183        assert_eq!(parsed.client_type, "connserver");
2184    }
2185
2186    #[test]
2187    fn test_all_command_constants_non_empty() {
2188        // Verify all command constants are non-empty strings
2189        let commands = [
2190            COMMAND_AUTHENTICATE,
2191            COMMAND_AUTHENTICATE_TOKEN,
2192            COMMAND_DISPOSE,
2193            COMMAND_ROUTE_ANNOUNCE,
2194            COMMAND_ROUTE_UNANNOUNCE,
2195            COMMAND_MESSAGE,
2196            COMMAND_GET_META,
2197            COMMAND_SET_META,
2198            COMMAND_SET_VIEW,
2199            COMMAND_CONTROLLER_INPUT,
2200            COMMAND_CONTROLLER_STOP,
2201            COMMAND_CONTROLLER_RESYNC,
2202            COMMAND_CREATE_BLOCK,
2203            COMMAND_DELETE_BLOCK,
2204            COMMAND_FILE_READ,
2205            COMMAND_FILE_WRITE,
2206            COMMAND_FILE_APPEND,
2207            COMMAND_EVENT_PUBLISH,
2208            COMMAND_EVENT_SUB,
2209            COMMAND_EVENT_UNSUB,
2210            COMMAND_CONN_CONNECT,
2211            COMMAND_CONN_DISCONNECT,
2212            COMMAND_WORKSPACE_LIST,
2213            COMMAND_FOCUS_WINDOW,
2214            COMMAND_AI_SEND_MESSAGE,
2215        ];
2216        for cmd in &commands {
2217            assert!(!cmd.is_empty(), "command constant should not be empty");
2218        }
2219    }
2220
2221    #[test]
2222    fn test_file_info_roundtrip() {
2223        let info = FileInfo {
2224            path: "/home/user/test.txt".to_string(),
2225            name: "test.txt".to_string(),
2226            size: 1024,
2227            isdir: false,
2228            mimetype: "text/plain".to_string(),
2229            ..Default::default()
2230        };
2231        let json = serde_json::to_string(&info).unwrap();
2232        let parsed: FileInfo = serde_json::from_str(&json).unwrap();
2233        assert_eq!(parsed.path, "/home/user/test.txt");
2234        assert_eq!(parsed.size, 1024);
2235    }
2236
2237    #[test]
2238    fn test_conn_status_roundtrip() {
2239        let status = ConnStatus {
2240            status: "connected".to_string(),
2241            connection: "ssh:myhost".to_string(),
2242            connected: true,
2243            ..Default::default()
2244        };
2245        let json = serde_json::to_string(&status).unwrap();
2246        let parsed: ConnStatus = serde_json::from_str(&json).unwrap();
2247        assert_eq!(parsed.status, "connected");
2248        assert!(parsed.connected);
2249    }
2250
2251    #[test]
2252    fn test_wave_info_data_roundtrip() {
2253        let info = WaveInfoData {
2254            version: "0.12.15".to_string(),
2255            clientid: "client-123".to_string(),
2256            ..Default::default()
2257        };
2258        let json = serde_json::to_string(&info).unwrap();
2259        let parsed: WaveInfoData = serde_json::from_str(&json).unwrap();
2260        assert_eq!(parsed.version, "0.12.15");
2261    }
2262
2263    #[test]
2264    fn test_create_block_data_wire_compat() {
2265        let go_json = r#"{"tabid":"tab-1","blockdef":{"view":"term"},"magnified":true}"#;
2266        let parsed: CommandCreateBlockData = serde_json::from_str(go_json).unwrap();
2267        assert_eq!(parsed.tabid, "tab-1");
2268        assert!(parsed.magnified);
2269        assert!(parsed.blockdef.is_some());
2270    }
2271}