返回 CodeWhale
facets.rs
根目录 / crates / command-contract / src / facets.rs
1 //! Independent, object-safe capability shapes for staged command migration.
2 //!
3 //! FEAT-014 publishes these interfaces without implementing them for the TUI
4 //! or changing an existing command. Later work adopts them inside
5 //! `codewhale-tui` one command group at a time. Only after every group uses
6 //! these shapes will groups move physically into a commands crate.
7
8 use std::path::{Path, PathBuf};
9
10 mod session_structcopy;
11 pub use session_structcopy::*;
12
13 mod debug_operations;
14 pub mod debug_receipts;
15 pub use debug_operations::*;
16 pub use debug_receipts::*;
17
18 mod diagnostics_report;
19 mod diagnostics_tools;
20 pub use diagnostics_report::*;
21 pub use diagnostics_tools::*;
22
23 use codewhale_protocol::request::{Message, SystemPrompt};
24 use serde_json::Value;
25
26 use crate::types::{CommandApprovalMode, CommandCurrency, CommandMode, CommandProviderId};
27
28 /// Session identity, messages, queue operations, and token totals.
29 pub trait CommandSessionContext {
30 fn session_id(&self) -> Option<String>;
31 fn api_messages(&self) -> Vec<Message>;
32 fn add_message(&mut self, message: Message);
33 fn queued_message_count(&self) -> usize;
34 fn remove_queued_message(&mut self, index: usize) -> Result<(), String>;
35 fn total_tokens(&self) -> u64;
36 }
37
38 /// Model selection, provider identity, and fallback chain.
39 ///
40 /// Slice 4 dropped the `reasoning_effort()` facet: reasoning preference is
41 /// owned by `codewhale_tui::reasoning_preference::ReasoningEffort` and no
42 /// command group consumed the duplicated boundary enum.
43 pub trait CommandModelContext {
44 fn current_model(&self) -> String;
45 fn auto_model(&self) -> bool;
46 fn set_model_selection(&mut self, model: String, provider: Option<CommandProviderId>);
47 fn provider_identity(&self) -> Option<CommandProviderId>;
48 fn fallback_chain(&self) -> Vec<CommandProviderId>;
49 }
50
51 /// Cost display and accounting operations.
52 pub trait CommandCostContext {
53 fn display_currency(&self) -> CommandCurrency;
54 fn session_cost_for_currency(&self, currency: CommandCurrency) -> f64;
55 fn subagent_cost_for_currency(&self, currency: CommandCurrency) -> f64;
56 fn accrue_cost_estimate(&mut self, amount: f64, currency: CommandCurrency);
57 fn record_turn_cost(
58 &mut self,
59 amount: f64,
60 currency: CommandCurrency,
61 route_receipt: Option<String>,
62 );
63 }
64
65 /// Operating mode, approval posture, shell access, and policy lock.
66 pub trait CommandModePolicyContext {
67 fn mode(&self) -> CommandMode;
68 fn set_mode(&mut self, mode: CommandMode);
69 fn approval_mode(&self) -> CommandApprovalMode;
70 fn allow_shell(&self) -> bool;
71 fn set_shell_access(&mut self, allow: bool);
72 fn policy_locked(&self) -> bool;
73 }
74
75 /// Read access to the effective system prompt.
76 pub trait CommandSystemPromptContext {
77 fn system_prompt(&self) -> Option<SystemPrompt>;
78 }
79
80 /// Active skill identity and skill-cache refresh.
81 pub trait CommandSkillsContext {
82 fn active_skill(&self) -> Option<String>;
83 fn active_skill_provenance(&self) -> Option<String>;
84 fn refresh_skill_cache(&mut self);
85 }
86
87 /// Workspace path and a bounded serialized work-state snapshot.
88 pub trait CommandWorkspaceContext {
89 fn workspace(&self) -> PathBuf;
90 fn work_state_snapshot(&self) -> Result<Option<String>, String>;
91 /// Session-aware canonical operation digest. Returns the final user-facing
92 /// digest text or a safe explicit error; never a serialized snapshot.
93 /// No-active-work and temporary-unavailability semantics are preserved by
94 /// the host implementation (FEAT-018 D5).
95 fn operation_digest(&mut self) -> Result<String, String>;
96 }
97
98 /// Stable-key translation with named replacements (FEAT-018 D3).
99 ///
100 /// Message identity uses stable snake_case keys plus named replacements. The
101 /// TUI host maps those keys to the current catalog and preserves the existing
102 /// English fallback for intentionally incomplete locale packs. Unknown keys or
103 /// invalid replacement contracts fail safely and produce a command error; they
104 /// never panic and never display a raw lookup key.
105 pub trait CommandPresentationContext {
106 /// Resolve a stable message key with its named replacements.
107 fn translate(&self, key: &str, replacements: &[(&str, &str)]) -> Result<String, String>;
108 }
109
110 /// Portable receipt for a successful atomic media attachment (FEAT-018 D4).
111 /// Carries only the information needed for the existing confirmation text.
112 #[derive(Debug, Clone, PartialEq, Eq)]
113 pub struct MediaAttachmentReceipt {
114 pub kind: String,
115 pub path: std::path::PathBuf,
116 }
117
118 /// Atomic composer/media capability (FEAT-018 D4).
119 ///
120 /// The host performs media validation and composer insertion as one atomic
121 /// operation. Rejected, missing, unsupported, corrupt, or oversized media
122 /// leaves composer state unchanged and returns a safe error. Only portable
123 /// success information crosses the boundary; composer markup, mutable input
124 /// text, decoder internals, and TUI types never do.
125 pub trait CommandMediaContext {
126 /// Validate and insert a resolved media path atomically.
127 fn attach_media(&mut self, resolved_path: &Path) -> Result<MediaAttachmentReceipt, String>;
128 }
129
130 // ---------------------------------------------------------------------------
131 // Debug diagnostics (FEAT-029 D3)
132 // ---------------------------------------------------------------------------
133
134 /// Only the provider identity and support decision consumed by `/balance`.
135 /// The adapter derives support from the authoritative provider policy; the
136 /// portable handler decides whether to emit `FetchBalance` or the original text.
137 #[derive(Debug, Clone, PartialEq, Eq)]
138 pub struct DebugBalanceProjection {
139 pub provider_display_name: String,
140 pub supports_balance_api: bool,
141 }
142
143 /// Source text consumed by `/system`; retain Text/Blocks/None separately
144 /// so the portable handler alone owns the separators and empty-state text.
145 #[derive(Debug, Clone, PartialEq, Eq)]
146 pub enum DebugSystemPrompt {
147 None,
148 Text(String),
149 Blocks(Vec<String>),
150 }
151
152 /// Only the data read by the `/system` renderer, with the host's mode label.
153 #[derive(Debug, Clone, PartialEq, Eq)]
154 pub struct DebugSystemProjection {
155 pub mode_label: String,
156 pub prompt: DebugSystemPrompt,
157 }
158
159 /// Published usage telemetry remains optional: absence is not zero.
160 #[derive(Debug, Clone, PartialEq)]
161 pub struct DebugTokenProjection {
162 pub active_context_used: usize,
163 pub context_window: u32,
164 pub last_input: Option<u32>,
165 pub last_output: Option<u32>,
166 pub cache_hit: Option<u32>,
167 pub cache_miss: Option<u32>,
168 pub total_tokens: u64,
169 pub cache_write_tokens: u64,
170 pub api_message_count: usize,
171 pub chat_message_count: usize,
172 pub model: String,
173 pub cost: DebugCostProjection,
174 }
175
176 /// Already-authoritative monetary values and bounded route attribution.
177 /// Calculation and price/source selection stay with the TUI host; formatting,
178 /// ordering and coverage wording belong to the portable handlers.
179 #[derive(Debug, Clone, PartialEq)]
180 pub struct DebugCostProjection {
181 pub currency: CommandCurrency,
182 pub total: f64,
183 pub parent_turns: f64,
184 pub subagents: f64,
185 pub display_floor: f64,
186 pub priced_turns: u32,
187 pub unpriced_turns: u32,
188 pub legacy_coverage_unknown: bool,
189 pub user_declared_estimates: bool,
190 pub itemized_turns: u32,
191 pub route_amounts: Vec<DebugRouteCost>,
192 pub turn_history_capacity: usize,
193 pub unpriced_reason_labels: Vec<String>,
194 pub unpriced_classes: Vec<String>,
195 pub pricing_provenances: Vec<String>,
196 pub live_pricing_defects: Vec<String>,
197 pub unusable_pricing_defects: Vec<String>,
198 pub route_receipts: Vec<String>,
199 }
200
201 #[derive(Debug, Clone, PartialEq)]
202 pub struct DebugRouteCost {
203 pub route: String,
204 pub amount: f64,
205 }
206
207 /// A cache inspection's semantic layer, retaining the same public JSON
208 /// field order as the baseline without borrowing the client request type.
209 #[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
210 pub struct DebugPromptLayer {
211 pub name: String,
212 pub stability: DebugPromptLayerStability,
213 pub char_len: usize,
214 pub byte_len: usize,
215 pub token_estimate: usize,
216 pub sha256: String,
217 pub tool_result: Option<DebugToolResultInspection>,
218 pub turn_meta: Option<DebugTurnMetaInspection>,
219 }
220
221 #[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize)]
222 pub enum DebugPromptLayerStability {
223 Static,
224 History,
225 Dynamic,
226 }
227
228 impl DebugPromptLayerStability {
229 pub const fn label(self) -> &'static str {
230 match self {
231 Self::Static => "static",
232 Self::History => "history",
233 Self::Dynamic => "dynamic",
234 }
235 }
236 }
237
238 #[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
239 pub struct DebugToolResultInspection {
240 pub original_chars: usize,
241 pub sent_chars: usize,
242 pub truncated: bool,
243 pub deduplicated: bool,
244 }
245
246 #[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
247 pub struct DebugTurnMetaInspection {
248 pub original_chars: usize,
249 pub sent_chars: usize,
250 pub deduplicated: bool,
251 pub sha256: String,
252 }
253
254 #[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
255 pub struct DebugPromptInspection {
256 pub base_static_prefix_hash: String,
257 pub full_request_prefix_hash: String,
258 pub tool_catalog_hash: String,
259 pub layers: Vec<DebugPromptLayer>,
260 }
261
262 /// Full key fields are needed for the existing comparison and JSON report;
263 /// the short hash is computed by the authoritative host hashing function.
264 #[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
265 pub struct DebugWarmupKey {
266 pub provider: String,
267 pub model: String,
268 pub base_url: String,
269 pub static_prefix_hash: String,
270 pub tool_catalog_hash: String,
271 pub project_pack_hash: String,
272 pub skills_hash: String,
273 }
274
275 /// One coherent observation, including previous inspection before any write.
276 /// Inspect errors occur before this value exists and never update session state.
277 #[derive(Debug, Clone, PartialEq, Eq)]
278 pub struct DebugCacheInspectionObservation {
279 pub current: DebugPromptInspection,
280 pub previous: Option<DebugPromptInspection>,
281 pub current_warmup_key: DebugWarmupKey,
282 pub last_warmup_key: Option<DebugWarmupKey>,
283 pub current_warmup_hash_short: String,
284 pub last_warmup_hash_short: Option<String>,
285 }
286
287 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
288 pub enum DebugCacheInspectionUnavailable {
289 NoConcreteRoute,
290 MissingCapturedEndpoint,
291 }
292
293 /// A bounded turn row. Pricing class partition and amount come from the
294 /// authoritative host, not a second implementation of provider billing.
295 /// Missing telemetry, missing audit and a measured zero remain distinct.
296 #[derive(Debug, Clone, PartialEq)]
297 pub struct DebugCacheTurn {
298 pub provider: Option<String>,
299 pub provider_identity: Option<String>,
300 pub model: Option<String>,
301 pub auto_model: bool,
302 pub input_tokens: u32,
303 pub output_tokens: u32,
304 pub cache_hit_tokens: Option<u32>,
305 pub cache_miss_tokens: Option<u32>,
306 pub cache_write_tokens: Option<u32>,
307 pub reasoning_tokens: Option<u32>,
308 pub reasoning_replay_tokens: Option<u32>,
309 pub priced_amount: Option<f64>,
310 pub unpriced_reason_key: Option<String>,
311 /// Original host enum order for deduplicated /cache note ordering.
312 pub unpriced_reason_sort_rank: Option<u8>,
313 pub unpriced_classes: Vec<String>,
314 pub priced_cache_read: u64,
315 pub priced_cache_miss: u64,
316 pub priced_cache_write: u64,
317 /// Coherent age at observation time; portable display rounds seconds.
318 pub age_seconds: u64,
319 }
320
321 /// Prompt-cache hit rates, each labelled by whose requests it covers (#6565).
322 ///
323 /// `parent` is this conversation's own requests: the footer `cache N%` and it
324 /// never change meaning. `agents` covers sub-agent and other background
325 /// requests. `combined` weights both by their tokens. Each is `None` when its
326 /// requests reported no cache telemetry; no report is never 0%.
327 #[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
328 pub struct DebugCacheRates {
329 pub parent: Option<u8>,
330 pub agents: Option<u8>,
331 pub combined: Option<u8>,
332 }
333
334 impl DebugCacheRates {
335 /// `parent 82% · agents 64% · combined 75%` with the given words, or just
336 /// `82%` when only the parent reported. `None` when nothing did.
337 #[must_use]
338 pub fn labelled(&self, parent: &str, agents: &str, combined: &str) -> Option<String> {
339 match (self.parent, self.agents) {
340 (Some(pct), None) => Some(format!("{pct}%")),
341 (None, None) => None,
342 _ => Some(
343 [
344 (parent, self.parent),
345 (agents, self.agents),
346 (combined, self.combined),
347 ]
348 .into_iter()
349 .filter_map(|(word, pct)| pct.map(|pct| format!("{word} {pct}%")))
350 .collect::<Vec<_>>()
351 .join(" · "),
352 ),
353 }
354 }
355 }
356
357 /// Shared source for `/cache [count|stats|zones]` branches. One host read
358 /// preserves ring order, optional telemetry and prefix stability evidence.
359 #[derive(Debug, Clone, PartialEq)]
360 pub struct DebugCacheTelemetry {
361 pub model: String,
362 /// Parent/agent/combined percentages computed once by the host.
363 pub session_cache_rates: DebugCacheRates,
364 pub history: Vec<DebugCacheTurn>,
365 pub history_capacity: usize,
366 pub prefix_stability_pct: Option<u32>,
367 pub prefix_checks_total: u64,
368 pub prefix_change_count: u64,
369 pub prefix_drift_count: u64,
370 pub prefix_context_updates: u64,
371 pub prefix_pin_reason: Option<String>,
372 pub prefix_last_miss_reason: Option<String>,
373 pub last_prefix_change_desc: Option<String>,
374 pub last_pinned_prefix_hash: Option<String>,
375 pub api_message_count: usize,
376 pub non_system_message_count: usize,
377 }
378
379 /// Narrow, synchronous data boundary for the debug diagnostics slice.
380 ///
381 /// No concrete provider, App, completed message, or network operation crosses
382 /// this interface. Add operations only when their live branches require them.
383 /// Portable handlers consume these facts without concrete host access.
384 pub trait CommandDebugDiagnosticsContext {
385 fn balance_projection(&self) -> DebugBalanceProjection;
386 fn system_projection(&self) -> DebugSystemProjection;
387 fn token_projection(&self) -> DebugTokenProjection;
388 fn cost_projection(&self) -> DebugCostProjection;
389 fn cache_telemetry(&self) -> DebugCacheTelemetry;
390 /// The report builder stays host-owned; format and JSON serialization
391 /// consume only this data-only source map.
392 fn context_source_map(&self) -> DebugPromptSourceMap;
393 fn prompt_context(&self) -> DebugPromptContext;
394 /// None means no prepared snapshot exists; argument validation occurs
395 /// only after this check in the portable `/tools` handler.
396 fn tool_snapshot(&self) -> Option<DebugToolSnapshot>;
397 /// Route resolution and request inspection remain host-owned. This call
398 /// must not update the remembered inspection on failure or success.
399 fn inspect_cache(
400 &self,
401 ) -> Result<DebugCacheInspectionObservation, DebugCacheInspectionUnavailable>;
402 /// Store the already-observed inspection after portable rendering, without
403 /// rebuilding or re-inspecting the request. The baseline also commits on
404 /// JSON serialization fallback.
405 fn remember_cache_inspection(&mut self, inspection: DebugPromptInspection);
406 }
407
408 // ---------------------------------------------------------------------------
409 // Project (FEAT-021 D1/D2/D3/D4)
410 // ---------------------------------------------------------------------------
411
412 /// Portable goal status for the project facet (FEAT-021 D1).
413 ///
414 /// Mirrors the four TUI-owned `tools::goal::GoalStatus` variants without
415 /// naming the TUI type. The adapter maps host state onto this enum; handlers
416 /// compare and render it directly.
417 #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
418 pub enum ProjectGoalStatus {
419 #[default]
420 Active,
421 Paused,
422 Complete,
423 Blocked,
424 }
425
426 /// Portable goal projection (FEAT-021 D1).
427 ///
428 /// Carries the visible goal state, the effective pending-control view, and the
429 /// session-derived token fallback the live `/goal` handler consumes. Concrete
430 /// goal-service, session-manager, and `App` types never cross the boundary.
431 #[derive(Debug, Clone, PartialEq, Eq)]
432 pub struct ProjectGoalState {
433 /// Visible goal objective.
434 pub objective: Option<String>,
435 /// Visible goal status.
436 pub status: ProjectGoalStatus,
437 /// Pause reason label when the goal is paused (already rendered).
438 pub pause_reason: Option<String>,
439 /// Elapsed seconds from `started_at` when present (host-computed).
440 pub started_at_elapsed_seconds: Option<u64>,
441 /// Seconds of goal time used (stable budget/elapsed source).
442 pub time_used_seconds: u64,
443 /// Optional token budget.
444 pub token_budget: Option<u32>,
445 /// Tokens used by the goal engine.
446 pub tokens_used: u64,
447 /// Session conversation-token total (fallback when tokens_used == 0).
448 pub session_total_tokens: u32,
449 /// Goal continuation count.
450 pub continuation_count: u32,
451 /// Whether pending goal controls are queued (effective-state gate).
452 pub pending_controls: bool,
453 /// Last-known durable objective (session-derived effective source).
454 pub last_known_objective: Option<String>,
455 /// Last-known durable status (session-derived effective source).
456 pub last_known_status: Option<ProjectGoalStatus>,
457 /// Whether the conversation has API messages (bare `/goal` context gate).
458 pub conversation_present: bool,
459 /// Whether the host is currently loading (idle-hint gate).
460 pub is_loading: bool,
461 /// Whether the goal continuation loop is waiting (idle-hint gate).
462 pub goal_continuation_waiting: bool,
463 }
464
465 /// Host project data for the project command group (FEAT-021 D1).
466 ///
467 /// Exposes the typed, exact-minimum operations the live project handlers
468 /// consume: `/lsp` status/set state, `/share` session payload data, and
469 /// `/goal` goal state including the session-derived effective values.
470 /// `/init` host data flows through the existing `WORKSPACE` facet (D2), so
471 /// `/init` destructures exactly `WORKSPACE` (D4) and consumes no
472 /// project-facet method. All results are contract-owned portable values; implementation
473 /// errors cross as safe text. The TUI adapter is the only place that touches
474 /// `App`, `config::config`, the goal service, or the session manager.
475 pub trait CommandProjectContext {
476 /// `/lsp` status: whether LSP diagnostics are enabled.
477 fn lsp_enabled(&self) -> bool;
478 /// `/lsp` set: enable or disable LSP diagnostics.
479 fn lsp_set(&mut self, enabled: bool) -> Result<(), String>;
480 /// `/goal` projection: visible and effective goal state.
481 fn goal_state(&self) -> ProjectGoalState;
482 }
483
484 // ---------------------------------------------------------------------------
485 // Memory (FEAT-019 D1/D2/D8/D9)
486 // ---------------------------------------------------------------------------
487
488 /// Portable semantic hit for a native-memory search or get result.
489 ///
490 /// Carries only the typed location and text the handler consumes for
491 /// formatting; the TUI-owned `NativeMemoryHit` never crosses the boundary.
492 #[derive(Debug, Clone, PartialEq, Eq)]
493 pub struct MemoryHit {
494 pub source: PathBuf,
495 pub line_start: usize,
496 pub line_end: usize,
497 pub text: String,
498 }
499
500 /// Portable native-memory location summary (status operation).
501 #[derive(Debug, Clone, PartialEq, Eq)]
502 pub struct MemoryStatus {
503 pub root: PathBuf,
504 pub source: PathBuf,
505 pub index: PathBuf,
506 }
507
508 /// Portable result of a successful remember operation.
509 #[derive(Debug, Clone, PartialEq, Eq)]
510 pub struct MemoryRemembered {
511 pub source: PathBuf,
512 pub line_start: usize,
513 }
514
515 /// Portable import outcome: imported (with destination) or skipped.
516 #[derive(Debug, Clone, PartialEq, Eq)]
517 pub enum MemoryImportOutcome {
518 Imported { destination: PathBuf },
519 Skipped,
520 }
521
522 /// Portable get outcome: found hit or explicit not-found.
523 #[derive(Debug, Clone, PartialEq, Eq)]
524 pub enum MemoryGetOutcome {
525 Found(MemoryHit),
526 NotFound,
527 }
528
529 /// Portable export payload — the exported memory document itself, never a
530 /// preformatted command response.
531 #[derive(Debug, Clone, PartialEq, Eq)]
532 pub struct MemoryExport {
533 pub content: String,
534 }
535
536 /// Portable reindex entry count.
537 #[derive(Debug, Clone, PartialEq, Eq)]
538 pub struct MemoryReindex {
539 pub entry_count: usize,
540 }
541
542 /// Zero-field success value for delete operations (D2): the handler already
543 /// owns the selected scope and needs no additional success data.
544 #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
545 pub struct MemoryDelete;
546
547 /// Typed remember target (D9): the handler resolves workspace identity through
548 /// the workspace facet and passes the resulting typed ID here.
549 #[derive(Debug, Clone, PartialEq, Eq)]
550 pub enum MemoryRememberTarget {
551 Global,
552 Workspace { workspace_id: String },
553 }
554
555 /// Typed delete scope for the non-workspace delete method (D8/D9).
556 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
557 pub enum MemoryDeleteScope {
558 /// Delete every memory entry (global and all workspace scopes).
559 All,
560 /// Delete only the global scope entries.
561 Global,
562 }
563
564 /// Host memory data for the memory command group (FEAT-019 D1).
565 ///
566 /// Exposes the resolved user-memory file path, the enablement flag, and one
567 /// typed method per exposed native-memory operation. All results are
568 /// contract-owned portable values; implementation errors cross as safe text.
569 /// Workspace-scoped operations take the borrowed workspace path as their first
570 /// argument (D8); non-workspace operations never receive workspace authority
571 /// and the facet never captures or retains workspace state internally.
572 pub trait CommandMemoryContext {
573 /// The resolved user-memory file path.
574 fn memory_path(&self) -> PathBuf;
575 /// Whether the `[memory] enabled` / `DEEPSEEK_MEMORY=on` flag is set.
576 fn memory_enabled(&self) -> bool;
577 /// Native-memory root, global source, and index paths.
578 fn status(&self) -> Result<MemoryStatus, String>;
579 /// The native-memory root path.
580 fn path(&self) -> Result<PathBuf, String>;
581 /// Workspace identity for the given workspace path.
582 fn workspace_id(&self, workspace: &Path) -> Result<String, String>;
583 /// Workspace-scoped search over the native-memory store.
584 fn search(&self, workspace: &Path, query: &str, limit: usize)
585 -> Result<Vec<MemoryHit>, String>;
586 /// Append a reviewed note to the typed global or workspace target.
587 fn remember(
588 &self,
589 target: MemoryRememberTarget,
590 note: &str,
591 ) -> Result<MemoryRemembered, String>;
592 /// Import legacy memory; distinguishes imported from skipped.
593 fn import(&self) -> Result<MemoryImportOutcome, String>;
594 /// Workspace-scoped get by entry id; not-found is a typed outcome.
595 fn get(&self, workspace: &Path, id: i64) -> Result<MemoryGetOutcome, String>;
596 /// Export the native-memory document content.
597 fn export(&self) -> Result<MemoryExport, String>;
598 /// Reindex the native-memory store; returns the indexed entry count.
599 fn reindex(&self) -> Result<MemoryReindex, String>;
600 /// Delete all or global scope; never receives workspace authority.
601 fn delete(&self, scope: MemoryDeleteScope) -> Result<MemoryDelete, String>;
602 /// Delete the given workspace scope; workspace path is the first argument.
603 fn delete_workspace(&self, workspace: &Path) -> Result<MemoryDelete, String>;
604 }
605
606 // ---------------------------------------------------------------------------
607 // Plugin (FEAT-020 D1/D2/D10/D11)
608 // ---------------------------------------------------------------------------
609
610 /// Portable plugin diagnostic level (FEAT-020 D2).
611 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
612 pub enum PluginDiagnosticLevel {
613 Warning,
614 Error,
615 }
616
617 /// Portable plugin diagnostic entry.
618 #[derive(Debug, Clone, PartialEq, Eq)]
619 pub struct PluginDiagnostic {
620 pub level: PluginDiagnosticLevel,
621 pub code: String,
622 pub message: String,
623 pub path: Option<PathBuf>,
624 }
625
626 /// Portable MCP transport classification for the capability review body.
627 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
628 pub enum PluginMcpTransport {
629 Stdio,
630 Http,
631 Invalid,
632 }
633
634 /// Portable MCP server detail for the capability review body (FEAT-020 D2).
635 ///
636 /// Carries only the semantic fields `render_mcp_inventory` consumes:
637 /// transport, command/url, argv, cwd, env provenance, timeouts, required,
638 /// enabled/disabled tool lists, and the enabled flag. Host `McpServerConfig`
639 /// never crosses the boundary.
640 #[derive(Debug, Clone, PartialEq, Eq)]
641 pub struct PluginMcpServerDetail {
642 pub name: String,
643 pub transport: PluginMcpTransport,
644 pub command: Option<String>,
645 pub argv: Vec<String>,
646 pub cwd: Option<PathBuf>,
647 pub env: Vec<(String, String)>,
648 pub url: Option<String>,
649 pub env_headers: Vec<(String, String)>,
650 pub bearer_token_env_var: Option<String>,
651 pub connect_timeout_secs: Option<u64>,
652 pub execute_timeout_secs: Option<u64>,
653 pub read_timeout_secs: Option<u64>,
654 pub required: bool,
655 pub enabled_tools: Vec<String>,
656 pub disabled_tools: Vec<String>,
657 pub enabled: bool,
658 }
659
660 /// Portable summary of one loaded plugin bundle (list output, FEAT-020 D2).
661 #[derive(Debug, Clone, PartialEq, Eq)]
662 pub struct PluginSummary {
663 pub name: String,
664 pub id: String,
665 pub state_label: String,
666 pub scope: String,
667 pub trust_status: String,
668 pub compatibility: String,
669 pub inventory: String,
670 pub active: bool,
671 pub trusted: bool,
672 pub enabled: bool,
673 }
674
675 /// Portable full bundle detail for show/review/validate rendering (FEAT-020 D2).
676 ///
677 /// Carries every semantic value the render helpers consume. The complete
678 /// `LoadedPlugin` never crosses the boundary; only branch-consumed fields are
679 /// projected here (D10).
680 #[derive(Debug, Clone, PartialEq, Eq)]
681 pub struct PluginDetail {
682 /// Inventory summary string (host-computed, e.g. `skills=1 mcp=0`).
683 pub inventory_summary: String,
684 pub name: String,
685 pub id: String,
686 pub version: String,
687 pub origin: String,
688 pub scope: String,
689 pub state_label: String,
690 pub trust_status: String,
691 pub compatibility: String,
692 pub content_hash: String,
693 pub capability_hash: String,
694 pub canonical_root: PathBuf,
695 pub active: bool,
696 pub trusted: bool,
697 pub enabled: bool,
698 pub unsupported_labels: Vec<String>,
699 pub supported_labels: Vec<String>,
700 pub skills: Vec<String>,
701 pub filesystem_roots: Vec<String>,
702 pub network_hosts: Vec<String>,
703 pub stdio_mcp_servers: usize,
704 pub lifecycle_mutation: bool,
705 pub mcp_servers: Vec<PluginMcpServerDetail>,
706 pub diagnostics: Vec<PluginDiagnostic>,
707 }
708
709 /// Portable outcome of a plugin mutation (FEAT-020 D2/D11).
710 #[derive(Debug, Clone, PartialEq, Eq)]
711 pub enum PluginMutationOutcome {
712 Installed,
713 Updated,
714 NoChange,
715 Uninstalled,
716 NeedsApproval(String),
717 NetworkDenied(String),
718 }
719
720 /// Portable mutation receipt returned synchronously by the facet (FEAT-020 D11).
721 #[derive(Debug, Clone, PartialEq, Eq)]
722 pub struct PluginMutationReceipt {
723 pub name: String,
724 pub path: Option<PathBuf>,
725 pub content_hash: Option<String>,
726 pub installed_content_hash: Option<String>,
727 pub outcome: PluginMutationOutcome,
728 }
729
730 /// Portable bundle export receipt (FEAT-020 D2).
731 #[derive(Debug, Clone, PartialEq, Eq)]
732 pub struct PluginExportReceipt {
733 pub exported_name: String,
734 pub target: PathBuf,
735 pub display_name: Option<String>,
736 pub wrote_mcp_json: bool,
737 pub files_copied: u64,
738 pub skills_normalized: bool,
739 }
740
741 /// Portable legacy executable-tool detail (FEAT-020 D2).
742 #[derive(Debug, Clone, PartialEq, Eq)]
743 pub struct PluginLegacyTool {
744 pub name: String,
745 pub description: String,
746 pub approval: String,
747 pub input_schema: Option<String>,
748 pub path: PathBuf,
749 }
750
751 /// Portable legacy-tool scan result: directory, discovered tools, and load
752 /// diagnostics for scripts that asked for something the loader ignored.
753 #[derive(Debug, Clone, PartialEq, Eq)]
754 pub struct PluginLegacyScan {
755 pub dir: PathBuf,
756 pub tools: Vec<PluginLegacyTool>,
757 pub diagnostics: Vec<PluginDiagnostic>,
758 }
759
760 /// Portable Kimi managed-plugin candidate (FEAT-020 D2).
761 #[derive(Debug, Clone, PartialEq, Eq)]
762 pub struct PluginManagedCandidate {
763 pub name: String,
764 pub version: String,
765 pub license: Option<String>,
766 pub canonical_path: PathBuf,
767 pub content_hash: String,
768 pub capability_hash: String,
769 pub inventory: String,
770 pub applicable: bool,
771 }
772
773 /// Portable Kimi managed-scan result (FEAT-020 D2).
774 #[derive(Debug, Clone, PartialEq, Eq)]
775 pub struct PluginManagedScan {
776 pub root: PathBuf,
777 pub candidates: Vec<PluginManagedCandidate>,
778 pub rejected: Vec<String>,
779 }
780
781 /// Portable review of a DeepSeek Harness bundle package before import: what
782 /// converts, what is skipped, the authority it will request, and the content
783 /// hash an exact install of the same package stages.
784 #[derive(Debug, Clone, PartialEq, Eq)]
785 pub struct PluginDshPreview {
786 pub package_path: PathBuf,
787 pub plugin_name: String,
788 pub source_package: Option<String>,
789 pub source_version: Option<String>,
790 pub content_hash: String,
791 pub skills: Vec<String>,
792 pub remote_servers: Vec<String>,
793 pub local_servers: Vec<String>,
794 pub network_hosts: Vec<String>,
795 pub requires_node: bool,
796 /// One line per skipped row or patch operation (a manual port).
797 pub manual_ports: Vec<String>,
798 pub diagnostics: Vec<String>,
799 }
800
801 /// Portable marketplace candidate install plan (FEAT-020 D2).
802 #[derive(Debug, Clone, PartialEq, Eq)]
803 pub enum PluginMarketplaceInstallPlan {
804 Supported { spec: String, source_kind: String },
805 AlreadyPresent { selector: String, reason: String },
806 Unsupported { reason: String },
807 }
808
809 /// Portable marketplace candidate (FEAT-020 D2).
810 #[derive(Debug, Clone, PartialEq, Eq)]
811 pub struct PluginMarketplaceCandidate {
812 pub name: String,
813 pub display_name: Option<String>,
814 pub version: Option<String>,
815 pub tier: String,
816 pub compatibility: Option<String>,
817 pub install_plan: PluginMarketplaceInstallPlan,
818 pub description: Option<String>,
819 pub homepage: Option<String>,
820 pub repository: Option<String>,
821 pub author: Option<String>,
822 pub license: Option<String>,
823 pub keywords: Vec<String>,
824 pub when: Option<String>,
825 pub diagnostics: Vec<PluginDiagnostic>,
826 pub has_errors: bool,
827 }
828
829 /// Portable marketplace catalog (FEAT-020 D2).
830 #[derive(Debug, Clone, PartialEq, Eq)]
831 pub struct PluginMarketplaceCatalog {
832 pub id: String,
833 /// Source document path (for the `show` provenance line).
834 pub source_path: Option<String>,
835 pub display_name: Option<String>,
836 pub description: Option<String>,
837 pub format: String,
838 pub tier: String,
839 pub publisher: Option<String>,
840 pub total_candidates: usize,
841 pub warning_count: usize,
842 pub candidates: Vec<PluginMarketplaceCandidate>,
843 pub diagnostics: Vec<PluginDiagnostic>,
844 }
845
846 /// Portable marketplace add receipt (FEAT-020 D2).
847 #[derive(Debug, Clone, PartialEq, Eq)]
848 pub struct PluginMarketplaceAddReceipt {
849 pub name: String,
850 pub candidate_count: usize,
851 pub warning_count: usize,
852 pub catalog: PluginMarketplaceCatalog,
853 }
854
855 /// Portable marketplace state: stored catalogs plus an optional host-provided
856 /// built-in `official` catalog.
857 #[derive(Debug, Clone, PartialEq, Eq)]
858 pub struct PluginMarketplaceState {
859 /// Optional host-provided built-in catalog. Current main provides none;
860 /// retaining the option keeps the portable boundary future-compatible
861 /// without inventing a catalog in the handler.
862 pub official: Option<PluginMarketplaceCatalog>,
863 pub stored: Vec<PluginMarketplaceCatalog>,
864 }
865
866 /// Portable suggestion for the `/plugin suggest` recommendation output.
867 #[derive(Debug, Clone, PartialEq, Eq)]
868 pub struct PluginSuggestion {
869 pub name: String,
870 /// State label rendered beside the plugin name (active/not-reviewed/…).
871 pub state_label: String,
872 pub description: String,
873 pub why: Vec<String>,
874 /// The actionable next step rendered under the suggestion.
875 pub next_step: String,
876 }
877
878 /// Plugins hidden from proactive suggestions (plugin policy rule 9). Names
879 /// are lowercase; each list is sorted.
880 #[derive(Debug, Clone, Default, PartialEq, Eq)]
881 pub struct PluginSuggestionDismissals {
882 /// "Don't suggest again": kept across sessions until reset.
883 pub persisted: Vec<String>,
884 /// Hidden for this session only (Esc, or a review the user already opened).
885 pub session: Vec<String>,
886 }
887
888 /// Host plugin data for the plugin command group (FEAT-020 D1).
889 ///
890 /// One object-safe, synchronous facet exposing the exact-minimum typed
891 /// operations the live `/plugin` branch closure consumes. Registry reads and
892 /// mutations, async-bridged install/update/uninstall (returning synchronous
893 /// portable receipts), export, legacy executable-tool scan, Kimi managed
894 /// import, and marketplace operations are all represented. The handler never
895 /// names `crate::plugins`, `PluginRegistry`, `LoadedPlugin`, `Config`, or
896 /// another concrete host service; implementation errors cross as safe text.
897 ///
898 /// Post-mutation side effects (rediscovery, skill-cache refresh, active-skill
899 /// reset) happen host-side inside the facet implementation; the handler only
900 /// renders the returned receipt (D11).
901 pub trait CommandPluginContext {
902 /// Read-only: registry summaries for list output.
903 fn summaries(&self) -> Result<Vec<PluginSummary>, String>;
904 /// Read-only: full portable detail for show/review/validate.
905 fn detail(&self, selector: &str) -> Result<PluginDetail, String>;
906 /// Read-only: registry-level diagnostics.
907 fn registry_diagnostics(&self) -> Vec<PluginDiagnostic>;
908 /// Read-only: whether validation reports no errors.
909 fn validation_is_clean(&self) -> bool;
910 /// Read-only: registry length (used by list/reload empty branches).
911 fn len(&self) -> usize;
912 /// Mutation: rediscover the workspace registry and refresh the skill
913 /// cache; returns the new registry length for the reload message.
914 fn reload(&mut self) -> Result<usize, String>;
915 /// Read-only: whether the registry is empty.
916 fn is_empty(&self) -> bool;
917 /// Return the one-shot on-disk-change nudge, if the host detects one.
918 /// The host owns the mutable catalog-stamp state; handlers only render.
919 fn reload_nudge(&mut self) -> Option<String>;
920 /// Read-only: persistence store path for marketplace state.
921 fn state_path(&self) -> Option<PathBuf>;
922 /// Read-only: recommend installed bundles for a task without side effects.
923 fn suggest(&self, task: &str) -> Result<Vec<PluginSuggestion>, String>;
924 /// Mutation: trust a bundle by exact review token. Success means the
925 /// mutation was applied; the handler renders the action word from its own
926 /// dispatch arm and may re-read `detail` for post-mutation state.
927 fn trust(&mut self, selector: &str, token: &str) -> Result<(), String>;
928 /// Mutation: enable a bundle. Success means enabled; re-read `detail` for
929 /// the post-mutation compatibility note.
930 fn enable(&mut self, selector: &str) -> Result<(), String>;
931 /// Mutation: disable a bundle.
932 fn disable(&mut self, selector: &str) -> Result<(), String>;
933 /// Mutation: revoke trust.
934 fn revoke_trust(&mut self, selector: &str) -> Result<(), String>;
935 /// Async-bridged install; returns a synchronous portable receipt (D11).
936 fn install(
937 &mut self,
938 source: &str,
939 expected_content_hash: Option<&str>,
940 ) -> Result<PluginMutationReceipt, String>;
941 /// Async-bridged update; returns a synchronous portable receipt (D11).
942 fn update(&mut self, selector: &str) -> Result<PluginMutationReceipt, String>;
943 /// Async-bridged uninstall; returns a synchronous portable receipt (D11).
944 fn uninstall(&mut self, selector: &str) -> Result<PluginMutationReceipt, String>;
945 /// File-level removal of a just-installed bundle whose content hash
946 /// mismatched (rollback). Unlike [`Self::uninstall`] it does not resolve a
947 /// registry selector and triggers no rediscovery or skill-cache side
948 /// effects; the host adapter owns the `crate::plugins` call (D1).
949 fn uninstall_path(&mut self, name: &str, plugins_dir: &Path) -> Result<(), String>;
950 /// Read-only: export a loaded bundle to a target directory.
951 fn export(&self, selector: &str, target: &Path) -> Result<PluginExportReceipt, String>;
952 /// Read-only: scan legacy executable plugin tools.
953 fn legacy_scan(&self) -> Result<Option<PluginLegacyScan>, String>;
954 /// Read-only: Kimi managed-plugin directory scan.
955 fn managed_scan(&self, home_override: Option<&Path>) -> Result<PluginManagedScan, String>;
956 /// Mutation: install a Kimi managed candidate by exact content hash.
957 fn managed_install(
958 &mut self,
959 canonical_path: &Path,
960 expected_content_hash: &str,
961 ) -> Result<PluginMutationReceipt, String>;
962 /// Read-only: convert a DeepSeek Harness bundle package into scratch and
963 /// review it without installing anything.
964 fn dsh_preview(&self, package: &Path) -> Result<PluginDshPreview, String> {
965 let _ = package;
966 Err("DeepSeek Harness import is not available in this host.".to_string())
967 }
968 /// Read-only: marketplace state (optional host catalog + stored catalogs).
969 fn marketplace_state(&self) -> Result<PluginMarketplaceState, String>;
970 /// Mutation: add a local catalog document to the marketplace store.
971 fn marketplace_add(
972 &mut self,
973 name: &str,
974 path: &Path,
975 ) -> Result<PluginMarketplaceAddReceipt, String>;
976 /// Mutation: remove a stored marketplace catalog.
977 fn marketplace_remove(&mut self, name: &str) -> Result<bool, String>;
978 /// Mutation: install a marketplace candidate through the reviewed installer.
979 fn marketplace_install(
980 &mut self,
981 catalog: &str,
982 candidate: &str,
983 ) -> Result<PluginMutationReceipt, String>;
984 /// Read-only: which plugins proactive suggestions currently skip.
985 fn suggestion_dismissals(&self) -> Result<PluginSuggestionDismissals, String>;
986 /// Mutation: let suggestions offer `name` again (every dismissed plugin
987 /// when `None`), in this session and future ones. Returns the names
988 /// cleared, sorted.
989 fn reset_suggestion_dismissals(&mut self, name: Option<&str>) -> Result<Vec<String>, String>;
990 }
991
992 // ---------------------------------------------------------------------------
993 // Skill group (FEAT-022 D1)
994 // ---------------------------------------------------------------------------
995
996 /// Source provenance of a discovered skill (native file vs reviewed plugin snapshot).
997 #[derive(Debug, Clone, PartialEq, Eq)]
998 pub enum SkillSourceKind {
999 Native,
1000 Plugin {
1001 plugin_name: String,
1002 plugin_id: String,
1003 },
1004 }
1005
1006 /// Curated product tier for bundled (shipped) skills.
1007 ///
1008 /// The canonical name→tier classification stays in the TUI host
1009 /// (`crate::skills::system::bundled_skill_tier`); the portable projection
1010 /// carries the resolved tier so the handler can render the curated listing
1011 /// without duplicating the canonical bundle list.
1012 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
1013 pub enum SkillBundledTier {
1014 CoreAgentic,
1015 FormatTooling,
1016 }
1017
1018 impl SkillBundledTier {
1019 /// Product-facing tier heading used by the `/skills` listing.
1020 #[must_use]
1021 pub fn heading(self) -> &'static str {
1022 match self {
1023 Self::CoreAgentic => "Core agentic",
1024 Self::FormatTooling => "Format & tooling",
1025 }
1026 }
1027 }
1028
1029 /// One discovered skill entry (portable).
1030 ///
1031 /// The body is intentionally excluded: activation and review receive body
1032 /// text through their own delegates (`SkillActivationOutcome`/`ReviewOutcome`);
1033 /// listing and inspect render name, description, source, and path only (D1
1034 /// exact-minimum).
1035 #[derive(Debug, Clone, PartialEq, Eq)]
1036 pub struct SkillEntry {
1037 pub name: String,
1038 pub description: String,
1039 pub source: SkillSourceKind,
1040 /// Native skills carry their on-disk path (inspect output).
1041 pub path: Option<String>,
1042 /// Bundled catalog tier; `None` for user/compatible skills.
1043 pub bundled_tier: Option<SkillBundledTier>,
1044 }
1045
1046 /// Portable projection of the host skill registry (discovery, D1).
1047 ///
1048 /// Carries every value the `/skills` and `/skill` handlers render: workspace
1049 /// and configured skills dir displays, discovery mode label, searched
1050 /// directories, entries, warnings, and the enabled-skill total.
1051 #[derive(Debug, Clone, PartialEq, Eq)]
1052 pub struct SkillRegistryProjection {
1053 pub workspace: String,
1054 pub skills_dir: String,
1055 pub mode_label: String,
1056 pub dirs: Vec<String>,
1057 pub entries: Vec<SkillEntry>,
1058 pub warnings: Vec<String>,
1059 pub total: usize,
1060 }
1061
1062 /// Target scope for skill mutations (`/skill install|update|uninstall|trust`).
1063 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
1064 pub enum SkillTargetScope {
1065 Project,
1066 Global,
1067 }
1068
1069 /// Portable mutation outcome mirroring the host receipt variants.
1070 #[derive(Debug, Clone, PartialEq, Eq)]
1071 pub enum SkillMutationOutcome {
1072 Installed,
1073 Updated,
1074 NoChange,
1075 Removed,
1076 Trusted,
1077 Imported,
1078 AlreadyPresent,
1079 NeedsApproval(String),
1080 NetworkDenied(String),
1081 }
1082
1083 /// Synchronous portable receipt for a skill mutation (FEAT-020 D11 mirror):
1084 /// the host owns the async network bridge; the handler renders the receipt
1085 /// byte-identically from these values.
1086 #[derive(Debug, Clone, PartialEq, Eq)]
1087 pub struct SkillMutationReceipt {
1088 pub name: String,
1089 pub safe_target_path: String,
1090 pub outcome: SkillMutationOutcome,
1091 }
1092
1093 /// One curated remote registry entry (`/skills --remote`).
1094 #[derive(Debug, Clone, PartialEq, Eq)]
1095 pub struct RemoteSkillEntry {
1096 pub name: String,
1097 pub description: Option<String>,
1098 pub source: String,
1099 }
1100
1101 /// Remote registry fetch outcome (`/skills --remote`, suggest source).
1102 #[derive(Debug, Clone, PartialEq, Eq)]
1103 pub enum RemoteRegistryOutcome {
1104 Loaded { entries: Vec<RemoteSkillEntry> },
1105 NeedsApproval(String),
1106 Denied(String),
1107 }
1108
1109 /// Remote recommendation for `/skills suggest <task>`.
1110 #[derive(Debug, Clone, PartialEq, Eq)]
1111 pub struct SkillRecommendation {
1112 pub name: String,
1113 pub description: Option<String>,
1114 pub matched_terms: Vec<String>,
1115 }
1116
1117 /// Per-skill outcome of `/skills sync`.
1118 #[derive(Debug, Clone, PartialEq, Eq)]
1119 pub enum SkillSyncEntry {
1120 Downloaded { name: String, path: String },
1121 Fresh { name: String },
1122 Failed { name: String, reason: String },
1123 Denied { name: String, host: String },
1124 NeedsApproval { name: String, host: String },
1125 }
1126
1127 /// Aggregate `/skills sync` outcome.
1128 ///
1129 /// Registry-level network-policy outcomes are carried as variants so the
1130 /// portable handler composes the exact `needs_approval` / `denied` messages.
1131 #[derive(Debug, Clone, PartialEq, Eq)]
1132 pub enum SkillSyncOutcome {
1133 Done {
1134 total: usize,
1135 downloaded: usize,
1136 fresh: usize,
1137 failed: usize,
1138 entries: Vec<SkillSyncEntry>,
1139 },
1140 RegistryNeedsApproval(String),
1141 RegistryDenied(String),
1142 }
1143
1144 /// Successful skill activation data (host performs the side effects).
1145 #[derive(Debug, Clone, PartialEq, Eq)]
1146 pub struct SkillActivationOutcome {
1147 pub name: String,
1148 pub description: String,
1149 }
1150
1151 /// Activation failures with the exact data the handler renders.
1152 #[derive(Debug, Clone, PartialEq, Eq)]
1153 pub enum SkillActivationError {
1154 NotFound {
1155 requested: String,
1156 available: Vec<String>,
1157 warnings: Vec<String>,
1158 },
1159 InvocationRejected {
1160 name: String,
1161 reason: String,
1162 },
1163 PluginRejected {
1164 name: String,
1165 reason: String,
1166 },
1167 }
1168
1169 /// `/review` outcome data (host performs the side effects).
1170 ///
1171 /// On success the baseline `/review` renders no message — it only emits the
1172 /// `SendMessage` action — so `Ready` carries no payload (D1 exact-minimum).
1173 /// Warnings are only rendered on the not-found path.
1174 #[derive(Debug, Clone, PartialEq, Eq)]
1175 pub enum ReviewOutcome {
1176 Ready,
1177 NotFound {
1178 skills_dir: String,
1179 global_dir: String,
1180 warnings: Vec<String>,
1181 },
1182 }
1183
1184 /// One snapshot entry for `/restore` listings.
1185 #[derive(Debug, Clone, PartialEq, Eq)]
1186 pub struct SnapshotEntry {
1187 pub id: String,
1188 pub label: String,
1189 pub timestamp: i64,
1190 }
1191
1192 /// Host approval posture for the `/restore` trust gate (D4: no MODE_POLICY).
1193 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
1194 pub struct CommandApprovalState {
1195 pub yolo: bool,
1196 pub trust_mode: bool,
1197 }
1198
1199 /// Host skill data for the skills command group (FEAT-022 D1).
1200 ///
1201 /// Exposes the typed, exact-minimum operations the live skills handlers
1202 /// consume: discovery (`/skills`), activation (`/skill`), synchronous
1203 /// mutation receipts (`/skill install|update|uninstall|trust`), remote
1204 /// registry + sync (`/skills --remote|sync|suggest`), review (`/review`),
1205 /// and snapshot list/restore plus approval state (`/restore`). The host
1206 /// adapter is the only place that touches `App`, `crate::plugins`,
1207 /// `SnapshotRepo`, `crate::skills` services, config/network policy, and the
1208 /// async runtime bridge. The shared FEAT-015 `CommandSkillsContext` is never
1209 /// widened; active-skill reads use that facet, mutations flow through the
1210 /// delegates here (D2). All results are contract-owned portable values;
1211 /// implementation errors cross as safe text. `/skill` declares this facet
1212 /// plus `CommandSkillsContext` for the baseline cache-refresh policy;
1213 /// `/skills`, `/review`, and `/restore` declare exactly this facet.
1214 pub trait CommandSkillGroupContext {
1215 /// `/skills` discovery projection (workspace, skills dir, scan mode,
1216 /// searched directories, plugin-provided skills, warnings).
1217 fn skill_registry_projection(&self) -> SkillRegistryProjection;
1218 /// `/skill` activation: host lookup, plugin-authority verification, and
1219 /// active-skill/history side effects. `SendMessage` task composition is
1220 /// handler-side.
1221 fn activate_skill(
1222 &mut self,
1223 name: &str,
1224 ) -> Result<SkillActivationOutcome, SkillActivationError>;
1225 /// `/skill install` — synchronous portable receipt; host owns network/async.
1226 fn install_skill(
1227 &mut self,
1228 scope: Option<SkillTargetScope>,
1229 spec: &str,
1230 ) -> Result<SkillMutationReceipt, String>;
1231 /// `/skill update` — synchronous portable receipt; host owns network/async.
1232 fn update_skill(
1233 &mut self,
1234 scope: Option<SkillTargetScope>,
1235 name: &str,
1236 ) -> Result<SkillMutationReceipt, String>;
1237 /// `/skill uninstall` — synchronous portable receipt.
1238 fn uninstall_skill(
1239 &mut self,
1240 scope: Option<SkillTargetScope>,
1241 name: &str,
1242 ) -> Result<SkillMutationReceipt, String>;
1243 /// `/skill trust` — synchronous portable receipt.
1244 fn trust_skill(
1245 &mut self,
1246 scope: Option<SkillTargetScope>,
1247 name: &str,
1248 ) -> Result<SkillMutationReceipt, String>;
1249 /// `/skills --remote` registry fetch (network policy host-side).
1250 fn fetch_remote_registry(&mut self) -> Result<RemoteRegistryOutcome, String>;
1251 /// `/skills suggest <task>` — host fetch + recommendation computation.
1252 fn recommend_skills(&mut self, task: &str) -> Result<Vec<SkillRecommendation>, String>;
1253 /// `/skills sync` — host registry sync (async bridge host-side).
1254 fn sync_registry(&mut self) -> Result<SkillSyncOutcome, String>;
1255 /// `/review` activation: host discovery + side effects (empty-target
1256 /// validation and `SendMessage` composition are handler-side).
1257 fn run_review(&mut self) -> Result<ReviewOutcome, String>;
1258 /// `/restore` snapshot listing.
1259 fn snapshot_list(&mut self, limit: usize) -> Result<Vec<SnapshotEntry>, String>;
1260 /// `/restore <N>`: host restores by snapshot id; handler composes the
1261 /// exact success message from its list entry.
1262 fn restore_snapshot(&mut self, id: &str) -> Result<(), String>;
1263 /// `/restore` trust gate posture (yolo / trust_mode).
1264 fn approval_state(&self) -> CommandApprovalState;
1265 }
1266
1267 // ---------------------------------------------------------------------------
1268 // Session lifecycle capability (FEAT-023).
1269 //
1270 // One contract-owned facet for the seven host-dependent lifecycle commands;
1271 // `/compact` and `/purge` stay pure. The shared `CommandSessionContext` above
1272 // stays unchanged: it
1273 // serves commands outside this slice and must not gain persistence,
1274 // navigation, picker, or lifecycle mutation authority (D2). No concrete App,
1275 // SessionManager, session-journal, picker, configuration, or view-stack type
1276 // crosses this boundary; successful results are structured portable fields so
1277 // the handlers retain exact message composition (D2/D5).
1278 // ---------------------------------------------------------------------------
1279
1280 /// Portable synchronization fields carried by shared session and debug actions.
1281 /// Conversation and prompt types are protocol-owned shapes; concrete runtime
1282 /// state and host action conversion remain outside the command contract.
1283 #[derive(Clone, Debug, PartialEq)]
1284 pub struct SessionSyncPayload {
1285 pub session_id: Option<String>,
1286 pub messages: Vec<Message>,
1287 pub system_prompt: Option<SystemPrompt>,
1288 pub model: String,
1289 pub workspace: PathBuf,
1290 pub mode: CommandMode,
1291 }
1292
1293 /// `/branch` success projection (`session/branch.rs`). The handler composes
1294 /// the exact success line from these deterministic fields.
1295 #[derive(Clone, Debug, PartialEq)]
1296 pub struct SessionBranchOutcome {
1297 pub leaf_display: String,
1298 pub journal_entries_before: usize,
1299 pub sync: SessionSyncPayload,
1300 }
1301
1302 /// `/fork` success projection for an active-conversation fork. The handler
1303 /// composes `Forked session {parent} -> {fork}` from these required fields.
1304 #[derive(Clone, Debug, PartialEq)]
1305 pub struct SessionForkReceipt {
1306 pub parent_label: String,
1307 pub fork_label: String,
1308 pub sync: SessionSyncPayload,
1309 }
1310
1311 /// `/fork <session_id|prefix>` success projection. Explicit-source forks
1312 /// always report their spawn depth, so the contract makes that field required
1313 /// rather than permitting an invalid missing-depth state.
1314 #[derive(Clone, Debug, PartialEq)]
1315 pub struct SessionForkFromReceipt {
1316 pub parent_label: String,
1317 pub fork_label: String,
1318 pub spawn_depth: u64,
1319 pub sync: SessionSyncPayload,
1320 }
1321
1322 /// `/save` success projection. The host performs the full baseline sequence
1323 /// (snapshot, serialization, atomic write, metadata application, work-state
1324 /// publication); the handler renders `Session saved to {display_path} (ID:
1325 /// {truncated_id})`.
1326 #[derive(Clone, Debug, PartialEq)]
1327 pub struct SessionSaveReceipt {
1328 pub display_path: String,
1329 pub truncated_id: String,
1330 }
1331
1332 /// `/new` success projection. The handler renders
1333 /// `Started new session {truncated_id} (New Session). Previous sessions
1334 /// remain available via /resume.`
1335 #[derive(Clone, Debug, PartialEq)]
1336 pub struct SessionNewReceipt {
1337 pub truncated_id: String,
1338 pub sync: SessionSyncPayload,
1339 }
1340
1341 /// `/sessions archive|unarchive|restore` success projection. The handler
1342 /// renders `Archived session {id} ({title})` or `Restored session ...` from
1343 /// the verb it dispatched.
1344 #[derive(Clone, Debug, PartialEq)]
1345 pub struct SessionArchiveReceipt {
1346 pub truncated_id: String,
1347 pub title: String,
1348 }
1349
1350 /// `/tree` body projection. The body rendering source (journal tree and
1351 /// linear transcript) stays TUI-owned; the handler appends the exact
1352 /// guidance lines (D5).
1353 #[derive(Clone, Debug, PartialEq)]
1354 pub enum TreeBodyProjection {
1355 /// Journal render already includes the trailing newline before guidance.
1356 Journal {
1357 rendered: String,
1358 },
1359 /// Linear pre-journal render (the marker lines).
1360 Linear {
1361 rendered: String,
1362 },
1363 EmptySession,
1364 NoSession,
1365 }
1366
1367 /// Lifecycle authority for the session command slice (FEAT-023 D2).
1368 ///
1369 /// Operation-granular synchronous delegates over the exact minimum host work
1370 /// the nine commands consume. Delegates may return the explicit host-error
1371 /// text the baseline surfaces for a failing stage; successful results are
1372 /// structured portable fields so handlers retain byte-identical composition.
1373 pub trait CommandSessionLifecycleContext {
1374 /// Live transition gate. Handlers return their own blocked-error text
1375 /// before invoking any mutating delegate, matching the baseline ordering
1376 /// (`/branch`, `/fork`, `/load`, `/new`). `/fork picker` and `/tree`
1377 /// never consult it in the baseline, so their paths must not either.
1378 fn transition_blocked(&self) -> bool;
1379
1380 /// `/branch` with no argument: the current leaf when an active journaled
1381 /// session resolves, otherwise `None` (the baseline silently falls back
1382 /// to the usage message on this path).
1383 fn branch_current_leaf_hint(&self) -> Option<String>;
1384
1385 /// `/branch <entry_id>`: persist the leaf move and apply the branched
1386 /// transcript. Errors are the exact baseline message for the failing
1387 /// stage (no active session, directory open, load, persist, or branch
1388 /// failure).
1389 fn branch_to(&mut self, entry_id: &str) -> Result<SessionBranchOutcome, String>;
1390
1391 /// `/tree`: produce the journal/linear/empty/no-session projection.
1392 /// Errors are the exact baseline directory-open message.
1393 fn tree_body(&self) -> Result<TreeBodyProjection, String>;
1394
1395 /// `/save [path]`: the full baseline persistence sequence.
1396 fn save_session(&mut self, explicit_path: Option<String>)
1397 -> Result<SessionSaveReceipt, String>;
1398
1399 /// `/fork` (active conversation): the full baseline parent/child save and
1400 /// switch sequence.
1401 fn fork_active(&mut self) -> Result<SessionForkReceipt, String>;
1402
1403 /// `/fork <session_id|prefix>`: explicit-source fork.
1404 fn fork_from(&mut self, session_id_or_prefix: &str) -> Result<SessionForkFromReceipt, String>;
1405
1406 /// `/new [--force]`: fresh-session transition. The caller has already
1407 /// parsed the argument and applied the transition-blocked gate; blocker,
1408 /// busy-work-state, and success handling match the baseline.
1409 fn fresh_session(&mut self, force: bool) -> Result<SessionNewReceipt, String>;
1410
1411 /// `/load <path>`: resolve the path (separator-bearing direct vs
1412 /// workspace-relative) and validate the saved-session shape without
1413 /// applying state or emitting a premature success receipt.
1414 fn load_session(&mut self, path: &str) -> Result<PathBuf, String>;
1415
1416 /// `/sessions` picker open with optional preselection (bare, `show`,
1417 /// `list`, `picker`, and `open <id>` forms). Picker construction and
1418 /// locale selection stay host-side.
1419 fn open_picker(&mut self, preselected: Option<String>);
1420
1421 /// `/sessions archive|unarchive|restore <id>`: durable lifecycle state
1422 /// update that also syncs the live cached metadata atomically.
1423 fn set_archived(
1424 &mut self,
1425 session_id: &str,
1426 archived: bool,
1427 ) -> Result<SessionArchiveReceipt, String>;
1428
1429 /// `/sessions prune <days>`: prune persisted sessions older than `days`
1430 /// days while protecting the active session; returns the number pruned.
1431 fn prune_sessions(&mut self, days: u64) -> Result<usize, String>;
1432 }
1433
1434 // ---------------------------------------------------------------------------
1435 // FEAT-024: session control slice (D2-D7).
1436 //
1437 // One independently optional session-control authority covering exactly the
1438 // host work the six control commands (`/relay`, `/rename`, `/resume`, `/rc`,
1439 // `/remote-env`, `/title`) consume. `CommandSessionContext` and
1440 // `CommandSessionLifecycleContext` are deliberately not widened: control
1441 // authority exists only on this facet, and every delegate is an atomic host
1442 // operation or a semantic projection so handlers keep byte-identical
1443 // composition. Portable values never expose TUI state beyond what the
1444 // baseline branches on.
1445 // ---------------------------------------------------------------------------
1446
1447 /// `/relay` semantic snapshot (D4). The handler composes the byte-identical
1448 /// instruction from these deterministic fields; `crate::prompts`,
1449 /// `crate::todo_snapshot`, goal/todo/plan machinery, Work-state objects, and
1450 /// locks stay host-side.
1451 #[derive(Clone, Debug, PartialEq)]
1452 pub struct RelayProjection {
1453 /// Authoritative compact-template text (`COMPACT_TEMPLATE`), echoed with
1454 /// a trailing trim by the handler exactly as today.
1455 pub compact_template: String,
1456 pub workspace: String,
1457 pub mode: String,
1458 pub model: String,
1459 pub goal_objective: Option<String>,
1460 pub goal_token_budget: Option<u32>,
1461 pub todos: TodoProjection,
1462 pub plan: PlanProjection,
1463 }
1464
1465 /// To-do state distinction for the relay snapshot. The rendered body (if any)
1466 /// is produced host-side from the authoritative graph-backed snapshot seam.
1467 #[derive(Clone, Debug, PartialEq)]
1468 pub enum TodoProjection {
1469 /// Rendered to-do body lines.
1470 Body(String),
1471 /// Work state could not be read (`To-do: unavailable because the list is
1472 /// busy.`).
1473 Unavailable,
1474 /// No Work state or no to-do body.
1475 Absent,
1476 }
1477
1478 /// Plan-state distinction for the relay snapshot. `Busy` reproduces the
1479 /// baseline `try_lock` failure branch; `Absent` reproduces an empty snapshot.
1480 ///
1481 /// `PlanSections` is intentionally not boxed: the command-crate boundary gate
1482 /// forbids boxed storage in the contract, and the section payload is only ever
1483 /// built once per `/relay` dispatch.
1484 #[derive(Clone, Debug, PartialEq)]
1485 #[allow(clippy::large_enum_variant)]
1486 pub enum PlanProjection {
1487 Sections(PlanSections),
1488 Busy,
1489 Absent,
1490 }
1491
1492 /// Semantic plan snapshot fields consumed by `/relay`. Values are the raw
1493 /// snapshot values; the handler applies the baseline trim/empty filtering and
1494 /// label composition so ordering and spacing stay byte-identical.
1495 #[derive(Clone, Debug, Default, PartialEq)]
1496 pub struct PlanSections {
1497 pub title: Option<String>,
1498 pub objective: Option<String>,
1499 pub context_summary: Option<String>,
1500 pub explanation: Option<String>,
1501 pub sources_used: Vec<String>,
1502 pub critical_files: Vec<String>,
1503 pub constraints: Vec<String>,
1504 pub recommended_approach: Option<String>,
1505 pub verification_plan: Option<String>,
1506 pub risks_and_unknowns: Option<String>,
1507 pub handoff_packet: Option<String>,
1508 pub items: Vec<PlanStep>,
1509 }
1510
1511 /// Portable plan-step status. The adapter maps the TUI plan status onto this
1512 /// semantic enum; the command handler remains the sole owner of the exact
1513 /// `pending`/`in_progress`/`completed` labels.
1514 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
1515 pub enum PlanStepStatus {
1516 Pending,
1517 InProgress,
1518 Completed,
1519 }
1520
1521 /// One semantic plan checklist item.
1522 #[derive(Clone, Debug, PartialEq, Eq)]
1523 pub struct PlanStep {
1524 pub status: PlanStepStatus,
1525 pub text: String,
1526 }
1527
1528 /// `/resume` route resolution (D6). The host resolves argument shape and
1529 /// performs container imports atomically; the handler selects the exact
1530 /// baseline message/action per variant.
1531 #[derive(Clone, Debug, PartialEq)]
1532 pub enum ResumeSource {
1533 /// Argument resolves to a readable file (`raw` direct path or
1534 /// workspace-relative path); the handler calls `import_session_file`.
1535 File(PathBuf),
1536 /// Argument resolved through the session manager (id or prefix).
1537 Session {
1538 /// Durable session file when present; `None` reproduces the baseline
1539 /// non-file fallback message arm.
1540 load_path: Option<PathBuf>,
1541 truncated_id: String,
1542 title: String,
1543 },
1544 /// Argument parsed as a foreign session container, which was imported
1545 /// atomically by the resolver.
1546 Imported(ResumeImportReceipt),
1547 /// Argument matched neither a file, a session, nor a container.
1548 NotFound { raw: String, error: String },
1549 }
1550
1551 /// Portable `/resume` import receipt. The handler renders
1552 /// `Imported foreign session as {truncated_id} ({entry_count} entries, leaf
1553 /// {leaf_display})`, and carries `sync` so the engine adopts the imported
1554 /// conversation instead of staying on the previous one.
1555 #[derive(Clone, Debug, PartialEq)]
1556 pub struct ResumeImportReceipt {
1557 pub truncated_id: String,
1558 pub entry_count: usize,
1559 pub leaf_display: String,
1560 pub sync: SessionSyncPayload,
1561 }
1562
1563 /// `/rename` success receipt; the title is the sanitized persisted value so
1564 /// the handler echoes exactly what was written.
1565 #[derive(Clone, Debug, PartialEq)]
1566 pub struct SessionTitleReceipt {
1567 pub title: String,
1568 }
1569
1570 /// Bare `/title` status projection (`Window title: [{effective}]{source}`).
1571 #[derive(Clone, Debug, PartialEq)]
1572 pub struct TitleReport {
1573 /// Effective window-title prefix, or `unset`.
1574 pub effective: String,
1575 pub source: TitleSource,
1576 }
1577
1578 #[derive(Clone, Debug, PartialEq)]
1579 pub enum TitleSource {
1580 /// Session-level window title set.
1581 Session,
1582 /// Config-default title applies.
1583 ConfigDefault,
1584 /// Neither a session title nor a config default.
1585 None,
1586 }
1587
1588 /// `/rc link` structured link data.
1589 #[derive(Clone, Debug, PartialEq)]
1590 pub struct RemoteLink {
1591 pub url: String,
1592 pub computer_url: Option<String>,
1593 }
1594
1595 /// `/rc open` outcome. Browser launch stays synchronous and single-attempt;
1596 /// no deferred external-URL action is produced (D6).
1597 #[derive(Clone, Debug, PartialEq)]
1598 pub enum RemoteOpenOutcome {
1599 NoLink,
1600 Opened { url: String },
1601 LaunchFailed { url: String },
1602 }
1603
1604 /// `/rc start` wording input: the active-turn copy is used while a turn is
1605 /// loading or a dispatch is in flight.
1606 #[derive(Clone, Debug, PartialEq)]
1607 pub struct RemoteStartInfo {
1608 pub connecting: bool,
1609 }
1610
1611 /// `/remote-env open` hosted-work target. The URL is fully encoded host-side
1612 /// (the portable handler must not depend on `urlencoding`); repo/branch echo
1613 /// the raw values used by the baseline message replacements.
1614 #[derive(Clone, Debug, PartialEq)]
1615 pub struct HostedWorkTarget {
1616 pub url: String,
1617 pub repo: String,
1618 pub branch: String,
1619 }
1620
1621 /// Control authority for the session command slice (FEAT-024 D2/D5).
1622 ///
1623 /// Operation-granular synchronous delegates over the exact minimum host work
1624 /// the six control commands consume. Delegates reproduce the baseline
1625 /// check/mutation order (transition gate before resume I/O, save before
1626 /// publication, single-attempt browser launch) and return portable receipts/
1627 /// projections or the exact host-error text the baseline surfaces. No
1628 /// `SessionManager`, saved-session/container type, `SessionPickerView`,
1629 /// remote-control service, Git wrapper, configuration, model/history type,
1630 /// lock, or host callback crosses the facet.
1631 pub trait CommandSessionControlContext {
1632 /// Live transition gate consulted by `/resume` before any picker or I/O.
1633 fn transition_blocked(&self) -> bool;
1634
1635 /// `/relay`: authoritative semantic snapshot (workspace/mode/model/goal/
1636 /// to-do/plan/compact-template). Unavailable sources are represented as
1637 /// explicit states, never panics.
1638 fn relay_projection(&self) -> RelayProjection;
1639
1640 /// Bare `/resume`: push the existing picker without preselection.
1641 fn open_resume_picker(&mut self);
1642
1643 /// `/resume <raw>`: resolve direct-path, workspace-relative, session
1644 /// id/prefix, and inline-container routes in the established order; a
1645 /// recognized inline container is imported atomically here.
1646 fn resolve_resume_source(&mut self, raw: &str) -> Result<ResumeSource, String>;
1647
1648 /// `/resume <file>`: read, parse, persist, and apply a foreign session
1649 /// file (container or plain saved session). Errors are the exact baseline
1650 /// read/parse/import text.
1651 fn import_session_file(&mut self, path: PathBuf) -> Result<ResumeImportReceipt, String>;
1652
1653 /// Apply the authoritative session-title character policy before the
1654 /// portable `/rename` and `/title` handlers validate and compose output.
1655 fn sanitize_session_title(&self, raw_title: &str) -> String;
1656
1657 /// `/rename <title>`: recover first-snapshot state, sync live state,
1658 /// persist, and publish with baseline order. The handler already applied
1659 /// sanitization plus blank and 100-character validation.
1660 fn rename_session(&mut self, title: &str) -> Result<SessionTitleReceipt, String>;
1661
1662 /// Bare `/title`: effective prefix and its source.
1663 fn title_report(&self) -> TitleReport;
1664
1665 /// `/title <title>`: persist an already sanitized and validated window
1666 /// title with baseline save/publication/redraw semantics.
1667 fn set_window_title(&mut self, title: String) -> Result<(), String>;
1668
1669 /// `/title off|clear|none`: clear the session window title with the same
1670 /// baseline persistence/redraw semantics.
1671 fn clear_window_title(&mut self) -> Result<(), String>;
1672
1673 /// `/rc status`: current remote-control status line.
1674 fn remote_status(&self) -> String;
1675
1676 /// `/rc link`: live session link plus optional computer-management URL.
1677 fn remote_link(&self) -> Option<RemoteLink>;
1678
1679 /// `/rc open`: synchronous single browser attempt over the authoritative
1680 /// URL-opening helper; outcome carries the URL for exact message text.
1681 fn remote_browser_open(&self) -> RemoteOpenOutcome;
1682
1683 /// `/rc start`: whether the active-turn copy applies.
1684 fn remote_start_info(&self) -> RemoteStartInfo;
1685
1686 /// `/rc stop`: refusal reason while a remote turn/envelope is active.
1687 fn remote_stop_refusal(&self) -> Option<String>;
1688
1689 /// `/remote-env open`: validate the hosted-work Git target host-side and
1690 /// return the encoded URL plus raw repo/branch echoes; `None` reproduces
1691 /// the unavailable-target error. Credentials never appear in values or
1692 /// errors.
1693 fn resolve_hosted_work_target(&self) -> Option<HostedWorkTarget>;
1694 }
1695
1696 // ---------------------------------------------------------------------------
1697 // FEAT-025: session export slice (D1-D9).
1698 //
1699 // One independently optional session-export authority covering exactly the
1700 // host work `/export` (and its `/daochu` alias) consumes. The shared
1701 // `CommandSessionContext`, `CommandSessionLifecycleContext`, and
1702 // `CommandSessionControlContext` facets are deliberately not widened: export
1703 // authority exists only on this facet, and every delegate is an atomic host
1704 // operation or a semantic projection so the portable handler keeps
1705 // byte-identical composition. Hidden payloads are excluded while projections
1706 // are built (D9), so internal reasoning, reasoning signatures, and inline or
1707 // local image bytes never enter these DTOs. No `App`, clipboard handler,
1708 // snapshot repository, history cell, session manager, configuration, client,
1709 // filesystem handle, or host callback crosses this boundary (D1/D3/D5/D7).
1710 // ---------------------------------------------------------------------------
1711
1712 /// Portable conversation metadata for the export header (D3).
1713 ///
1714 /// Values that already have an authoritative host derivation keep it
1715 /// (session-label truncation, provider identity, model label, mode display,
1716 /// workspace basename, message count, clock); portable rendering adds only
1717 /// export formatting and sanitization (D10).
1718 #[derive(Clone, Debug, PartialEq, Eq)]
1719 pub struct ExportMetadata {
1720 /// Host-truncated session id, or the baseline `unsaved` fallback.
1721 pub session_label: String,
1722 pub provider: String,
1723 pub model: String,
1724 pub mode: String,
1725 /// Workspace directory basename, or the baseline `workspace` fallback.
1726 pub workspace_name: String,
1727 /// `api_messages.len()` when authoritative, otherwise `history.len()`.
1728 pub message_count: usize,
1729 pub exported_at_unix: i64,
1730 }
1731
1732 /// One tool-call caller projection (D3). Only the fields the baseline export
1733 /// renders cross the boundary.
1734 #[derive(Clone, Debug, PartialEq, Eq)]
1735 pub struct ToolCallerProjection {
1736 pub caller_type: String,
1737 pub tool_id: Option<String>,
1738 }
1739
1740 /// One projected content block (D3/D9).
1741 ///
1742 /// Visible text and structured content cross as portable data; internal
1743 /// reasoning bodies, reasoning signatures, and inline or local image payloads
1744 /// are replaced by typed omission markers at projection time and never cross.
1745 #[derive(Clone, Debug, PartialEq)]
1746 pub enum ExportBlock {
1747 /// Visible text block; portable rendering sanitizes it.
1748 Text {
1749 text: String,
1750 },
1751 /// External image reference (`http`/`https` only); portable rendering
1752 /// redacts credential-bearing URLs.
1753 ImageReference {
1754 url: String,
1755 },
1756 /// Inline or local image payload excluded at projection time (D9).
1757 ImageOmitted,
1758 /// Internal reasoning body and reasoning signature excluded (D9).
1759 InternalReasoning,
1760 ToolCall {
1761 id: String,
1762 name: String,
1763 caller: Option<ToolCallerProjection>,
1764 input: Value,
1765 },
1766 ToolResult {
1767 tool_use_id: String,
1768 content: String,
1769 is_error: bool,
1770 /// `Some` when the host message carried structured result blocks; the
1771 /// host has already applied the safe-result filter (D9).
1772 structured: Option<Value>,
1773 },
1774 ServerToolCall {
1775 id: String,
1776 name: String,
1777 input: Value,
1778 },
1779 ToolSearchResult {
1780 tool_use_id: String,
1781 content: Value,
1782 },
1783 CodeExecutionResult {
1784 tool_use_id: String,
1785 content: Value,
1786 },
1787 }
1788
1789 /// One projected authoritative message (D3).
1790 ///
1791 /// `prompt_snippet` is the host-computed `snapshot_label_prompt_snippet` of
1792 /// the first visible text block. The parser and snippet algorithm stay
1793 /// TUI-owned (D8), so correlation compares authoritative values instead of
1794 /// re-deriving them portably.
1795 ///
1796 /// `is_user_role` carries the host's exact `Role::User` comparison. `role` is
1797 /// the rendered wire string, and comparing it textually would also match a
1798 /// `Role::Unrecognized("user")`, which the baseline never treated as a user
1799 /// turn. The flag keeps restore-point correlation faithful to the baseline.
1800 #[derive(Clone, Debug, PartialEq)]
1801 pub struct ExportMessage {
1802 pub role: String,
1803 /// Exact `message.role == Role::User`, not a string comparison.
1804 pub is_user_role: bool,
1805 pub blocks: Vec<ExportBlock>,
1806 pub prompt_snippet: Option<String>,
1807 }
1808
1809 /// One projected visible-history fallback entry (D3).
1810 #[derive(Clone, Debug, PartialEq, Eq)]
1811 pub enum HistoryEntry {
1812 /// Visible host content that portable rendering must still sanitize.
1813 Sanitized { role: String, body: String },
1814 /// An already-final baseline marker line that must not be sanitized again.
1815 Literal { role: String, body: String },
1816 }
1817
1818 /// Transcript source precedence (D3): authoritative API messages when
1819 /// present, otherwise the sanitized visible-history fallback.
1820 #[derive(Clone, Debug, PartialEq)]
1821 pub enum TranscriptProjection {
1822 Authoritative(Vec<ExportMessage>),
1823 HistoryFallback(Vec<HistoryEntry>),
1824 }
1825
1826 /// One snapshot projected to semantic fields (D8).
1827 ///
1828 /// `kind`, `sequence`, and `prompt_snippet` are the host-parsed label fields;
1829 /// the raw `label` is kept only for the human-readable table column. No
1830 /// preformatted correlation line crosses the boundary.
1831 #[derive(Clone, Debug, PartialEq, Eq)]
1832 pub struct RestoreSnapshot {
1833 pub id: String,
1834 pub label: String,
1835 pub timestamp_unix: i64,
1836 pub kind: String,
1837 pub sequence: Option<u64>,
1838 pub prompt_snippet: Option<String>,
1839 }
1840
1841 /// Restore-point projection with distinct baseline states (D3/D8).
1842 ///
1843 /// `None` means no snapshot repository exists, `Unreadable` preserves the host
1844 /// failure reason, and `Recorded` distinguishes an existing-but-empty
1845 /// repository from one with snapshots by the vector length.
1846 #[derive(Clone, Debug, PartialEq, Eq)]
1847 pub enum RestorePointProjection {
1848 None,
1849 Unreadable { reason: String },
1850 Recorded { snapshots: Vec<RestoreSnapshot> },
1851 }
1852
1853 /// Full conversation projection (D3/D8/D9).
1854 #[derive(Clone, Debug, PartialEq)]
1855 pub struct ConversationExportProjection {
1856 pub metadata: ExportMetadata,
1857 pub transcript: TranscriptProjection,
1858 pub restore_points: RestorePointProjection,
1859 }
1860
1861 /// Turn-handoff projection (D2).
1862 ///
1863 /// `markdown` is the unmodified shared TUI renderer output and
1864 /// `workspace_path` is the value the portable handler replaces with `.` after
1865 /// sanitizing; the renderer itself is neither moved nor duplicated.
1866 #[derive(Clone, Debug, PartialEq, Eq)]
1867 pub struct TurnHandoffProjection {
1868 pub markdown: String,
1869 pub workspace_path: String,
1870 }
1871
1872 /// Session-export authority for the `/export` slice (FEAT-025 D1-D9).
1873 ///
1874 /// Operation-granular synchronous delegates over the exact minimum host work
1875 /// the command consumes. The portable handler parses the request first, renders
1876 /// the selected scope second, and then uses these delegates in baseline order:
1877 /// clipboard exports call terminal-paste detection, recovery write, and
1878 /// clipboard delivery exactly once each with the same Markdown; file exports
1879 /// resolve the destination before writing it. A recovery-write `None` never
1880 /// prevents the clipboard attempt, and a turn-only export never requests the
1881 /// conversation projection (D6/D7).
1882 pub trait CommandSessionExportContext {
1883 /// Conversation export projection: metadata, authoritative-or-fallback
1884 /// transcript, and restore-point state. Read-only; opens only an existing
1885 /// snapshot repository and never creates one (D8).
1886 fn conversation_projection(&self) -> ConversationExportProjection;
1887
1888 /// Turn-handoff projection: unmodified shared renderer Markdown plus the
1889 /// workspace path value (D2).
1890 fn turn_handoff_projection(&self) -> TurnHandoffProjection;
1891
1892 /// Whether clipboard delivery goes through the terminal-client (SSH/OSC 52
1893 /// via tmux) path (D6).
1894 fn clipboard_requires_terminal_paste(&self) -> bool;
1895
1896 /// Write the shared `last-copy.md` recovery file. `None` reproduces the
1897 /// baseline silent failure; recovery writing never falls through to an
1898 /// error (D5/D6).
1899 fn write_recovery_copy(&self, markdown: &str) -> Option<PathBuf>;
1900
1901 /// Attempt clipboard delivery. `Err` carries the raw host clipboard error
1902 /// text; the handler composes the exact failure wording (D6).
1903 fn write_clipboard(&self, markdown: &str) -> Result<(), String>;
1904
1905 /// Resolve a file destination exactly as the baseline does (trim, empty
1906 /// check, `..` rejection, workspace canonicalization and rebasing, filename
1907 /// requirement). Errors are returned unwrapped (D7).
1908 fn resolve_export_path(&self, raw: &str) -> Result<PathBuf, String>;
1909
1910 /// Write the rendered export to a resolved destination with the baseline
1911 /// protection checks. Errors are returned unwrapped; the handler wraps them
1912 /// in `Failed to export {label} to {path}: {err}` (D7).
1913 fn write_export_file(&self, path: &Path, contents: &[u8], force: bool) -> Result<(), String>;
1914 }
1915
1915 lines RUST