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