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