| 1 | //! Generic handler transport for staged command migration. |
| 2 | //! |
| 3 | //! The output type is generic so FEAT-014 does not move or duplicate the |
| 4 | //! TUI-owned `CommandResult`. During in-place adoption, the TUI instantiates |
| 5 | //! `CommandHandler<crate::commands::CommandResult>`. |
| 6 | |
| 7 | use crate::facets::{ |
| 8 | CommandCostContext, CommandDebugChangeContext, CommandDebugDiagnosticsContext, |
| 9 | CommandDebugDiffContext, CommandDebugHistoryContext, CommandDebugReceiptsContext, |
| 10 | CommandDebugUndoContext, CommandMediaContext, CommandMemoryContext, CommandModePolicyContext, |
| 11 | CommandModelContext, CommandPluginContext, CommandPresentationContext, CommandProjectContext, |
| 12 | CommandSessionContext, CommandSessionControlContext, CommandSessionExportContext, |
| 13 | CommandSessionLifecycleContext, CommandSessionStructcopyContext, CommandSkillGroupContext, |
| 14 | CommandSkillsContext, CommandSystemPromptContext, CommandWorkspaceContext, |
| 15 | }; |
| 16 | |
| 17 | /// Exact host capabilities exposed to one contextual command handler. |
| 18 | /// |
| 19 | /// The set lives in the external contract crate so command registrations can |
| 20 | /// declare least authority without naming the TUI host. The dispatcher uses |
| 21 | /// the declaration to populate only those slots in [`CommandContexts`]. |
| 22 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] |
| 23 | pub struct CommandCapabilities(u32); |
| 24 | |
| 25 | impl CommandCapabilities { |
| 26 | pub const NONE: Self = Self(0); |
| 27 | pub const SESSION: Self = Self(1 << 0); |
| 28 | pub const MODEL: Self = Self(1 << 1); |
| 29 | pub const COST: Self = Self(1 << 2); |
| 30 | pub const MODE_POLICY: Self = Self(1 << 3); |
| 31 | pub const SYSTEM_PROMPT: Self = Self(1 << 4); |
| 32 | pub const SKILLS: Self = Self(1 << 5); |
| 33 | pub const WORKSPACE: Self = Self(1 << 6); |
| 34 | pub const PRESENTATION: Self = Self(1 << 7); |
| 35 | pub const MEDIA: Self = Self(1 << 8); |
| 36 | /// Memory-group host data (FEAT-019 D1). |
| 37 | pub const MEMORY: Self = Self(1 << 9); |
| 38 | /// Project-group host data (FEAT-021 D1). |
| 39 | pub const PROJECT: Self = Self(1 << 10); |
| 40 | /// Skills-group host data (FEAT-022 D1). |
| 41 | pub const SKILL_GROUP: Self = Self(1 << 11); |
| 42 | /// Plugin-group host data (FEAT-020 D1), appended after current main capabilities. |
| 43 | pub const PLUGIN: Self = Self(1 << 12); |
| 44 | /// Session-lifecycle host data (FEAT-023 D3), the next non-conflicting bit |
| 45 | /// after `PLUGIN`. Required only by the seven host-dependent lifecycle |
| 46 | /// commands; `/compact` and `/purge` remain pure. Never widened by the |
| 47 | /// basic session capability. |
| 48 | pub const SESSION_LIFECYCLE: Self = Self(1 << 13); |
| 49 | /// Session-control host data (FEAT-024 D3), the next non-conflicting bit |
| 50 | /// after `SESSION_LIFECYCLE`. Required only by the six host-dependent |
| 51 | /// control commands (`/relay`, `/rename`, `/resume`, `/rc`, `/remote-env`, |
| 52 | /// `/title`); `/remote-env` also declares `PRESENTATION`. Bit 14 fit the |
| 53 | /// original `u16` backing without speculative widening in FEAT-023. |
| 54 | pub const SESSION_CONTROL: Self = Self(1 << 14); |
| 55 | /// Session-export host data (FEAT-025 D1), the next non-conflicting bit |
| 56 | /// after `SESSION_CONTROL`. Required only by the host-dependent `/export` |
| 57 | /// command (and its `/daochu` alias) and by `/share`, which publishes the |
| 58 | /// same redacted projection; every concrete App, snapshot, clipboard, |
| 59 | /// filesystem, history, and turn-handoff access stays behind the TUI export |
| 60 | /// adapter. |
| 61 | /// |
| 62 | /// This filled the original 16-bit space. FEAT-029 widened the backing |
| 63 | /// storage before allocating the next independent diagnostics authority; |
| 64 | /// the published identity of this bit remains unchanged. |
| 65 | pub const SESSION_EXPORT: Self = Self(1 << 15); |
| 66 | /// Debug diagnostics host data (FEAT-029 D3/D4). This is the first bit in |
| 67 | /// the widened backing storage; other debug commands retain their own independent |
| 68 | /// authority and do not borrow this facet. |
| 69 | pub const DEBUG_DIAGNOSTICS: Self = Self(1 << 16); |
| 70 | |
| 71 | /// Debug receipts authority; independent from diagnostics and other debug operations. |
| 72 | pub const DEBUG_RECEIPTS: Self = Self(1 << 17); |
| 73 | /// Debug change authority; independent from diagnostics and other debug operations. |
| 74 | pub const DEBUG_CHANGE: Self = Self(1 << 18); |
| 75 | /// Debug history authority; independent from diagnostics and other debug operations. |
| 76 | pub const DEBUG_HISTORY: Self = Self(1 << 19); |
| 77 | /// Debug diff authority; independent from diagnostics and other debug operations. |
| 78 | pub const DEBUG_DIFF: Self = Self(1 << 20); |
| 79 | /// Debug undo authority; independent from diagnostics and other debug operations. |
| 80 | pub const DEBUG_UNDO: Self = Self(1 << 21); |
| 81 | |
| 82 | /// One human-selected structural copy; independent from export/recovery. |
| 83 | pub const SESSION_STRUCTCOPY: Self = Self(1 << 22); |
| 84 | |
| 85 | /// Raw bit pattern, for tests that pin the capability-space capacity. |
| 86 | /// |
| 87 | /// Kept `#[cfg(test)]` so the `u32` backing stays an implementation detail |
| 88 | /// and nothing can widen it accidentally through a public accessor. |
| 89 | #[cfg(test)] |
| 90 | pub(crate) const fn bits_for_test(self) -> u32 { |
| 91 | self.0 |
| 92 | } |
| 93 | |
| 94 | pub const fn union(self, other: Self) -> Self { |
| 95 | Self(self.0 | other.0) |
| 96 | } |
| 97 | |
| 98 | pub const fn contains(self, capability: Self) -> bool { |
| 99 | !capability.is_empty() && self.0 & capability.0 == capability.0 |
| 100 | } |
| 101 | |
| 102 | pub const fn is_empty(self) -> bool { |
| 103 | self.0 == 0 |
| 104 | } |
| 105 | } |
| 106 | |
| 107 | impl std::ops::BitOr for CommandCapabilities { |
| 108 | type Output = Self; |
| 109 | |
| 110 | fn bitor(self, rhs: Self) -> Self::Output { |
| 111 | self.union(rhs) |
| 112 | } |
| 113 | } |
| 114 | |
| 115 | /// A command handler that is either argument-only or capability-scoped. |
| 116 | #[derive(Clone, Copy)] |
| 117 | pub enum CommandHandler<R> { |
| 118 | Pure(fn(Option<&str>) -> R), |
| 119 | Contextual { |
| 120 | capabilities: CommandCapabilities, |
| 121 | handler: fn(CommandContexts<'_>, Option<&str>) -> R, |
| 122 | }, |
| 123 | } |
| 124 | |
| 125 | /// Transport envelope with one independently optional facet slot. |
| 126 | pub struct CommandContexts<'a> { |
| 127 | session: Option<&'a mut dyn CommandSessionContext>, |
| 128 | model: Option<&'a mut dyn CommandModelContext>, |
| 129 | cost: Option<&'a mut dyn CommandCostContext>, |
| 130 | mode_policy: Option<&'a mut dyn CommandModePolicyContext>, |
| 131 | system_prompt: Option<&'a mut dyn CommandSystemPromptContext>, |
| 132 | skills: Option<&'a mut dyn CommandSkillsContext>, |
| 133 | workspace: Option<&'a mut dyn CommandWorkspaceContext>, |
| 134 | presentation: Option<&'a mut dyn CommandPresentationContext>, |
| 135 | media: Option<&'a mut dyn CommandMediaContext>, |
| 136 | memory: Option<&'a mut dyn CommandMemoryContext>, |
| 137 | project: Option<&'a mut dyn CommandProjectContext>, |
| 138 | skill_group: Option<&'a mut dyn CommandSkillGroupContext>, |
| 139 | plugin: Option<&'a mut dyn CommandPluginContext>, |
| 140 | lifecycle: Option<&'a mut dyn CommandSessionLifecycleContext>, |
| 141 | control: Option<&'a mut dyn CommandSessionControlContext>, |
| 142 | export: Option<&'a mut dyn CommandSessionExportContext>, |
| 143 | structcopy: Option<&'a mut dyn CommandSessionStructcopyContext>, |
| 144 | debug_receipts: Option<&'a mut dyn CommandDebugReceiptsContext>, |
| 145 | debug_change: Option<&'a mut dyn CommandDebugChangeContext>, |
| 146 | debug_history: Option<&'a mut dyn CommandDebugHistoryContext>, |
| 147 | debug_diff: Option<&'a mut dyn CommandDebugDiffContext>, |
| 148 | debug_undo: Option<&'a mut dyn CommandDebugUndoContext>, |
| 149 | debug_diagnostics: Option<&'a mut dyn CommandDebugDiagnosticsContext>, |
| 150 | } |
| 151 | |
| 152 | /// Consumed envelope used when one handler needs several independent facets. |
| 153 | pub struct ContextParts<'a> { |
| 154 | pub session: Option<&'a mut dyn CommandSessionContext>, |
| 155 | pub model: Option<&'a mut dyn CommandModelContext>, |
| 156 | pub cost: Option<&'a mut dyn CommandCostContext>, |
| 157 | pub mode_policy: Option<&'a mut dyn CommandModePolicyContext>, |
| 158 | pub system_prompt: Option<&'a mut dyn CommandSystemPromptContext>, |
| 159 | pub skills: Option<&'a mut dyn CommandSkillsContext>, |
| 160 | pub workspace: Option<&'a mut dyn CommandWorkspaceContext>, |
| 161 | pub presentation: Option<&'a mut dyn CommandPresentationContext>, |
| 162 | pub media: Option<&'a mut dyn CommandMediaContext>, |
| 163 | pub memory: Option<&'a mut dyn CommandMemoryContext>, |
| 164 | pub project: Option<&'a mut dyn CommandProjectContext>, |
| 165 | pub skill_group: Option<&'a mut dyn CommandSkillGroupContext>, |
| 166 | pub plugin: Option<&'a mut dyn CommandPluginContext>, |
| 167 | pub lifecycle: Option<&'a mut dyn CommandSessionLifecycleContext>, |
| 168 | pub control: Option<&'a mut dyn CommandSessionControlContext>, |
| 169 | pub export: Option<&'a mut dyn CommandSessionExportContext>, |
| 170 | pub structcopy: Option<&'a mut dyn CommandSessionStructcopyContext>, |
| 171 | pub debug_receipts: Option<&'a mut dyn CommandDebugReceiptsContext>, |
| 172 | pub debug_change: Option<&'a mut dyn CommandDebugChangeContext>, |
| 173 | pub debug_history: Option<&'a mut dyn CommandDebugHistoryContext>, |
| 174 | pub debug_diff: Option<&'a mut dyn CommandDebugDiffContext>, |
| 175 | pub debug_undo: Option<&'a mut dyn CommandDebugUndoContext>, |
| 176 | pub debug_diagnostics: Option<&'a mut dyn CommandDebugDiagnosticsContext>, |
| 177 | } |
| 178 | |
| 179 | impl<'a> CommandContexts<'a> { |
| 180 | pub fn empty() -> Self { |
| 181 | Self { |
| 182 | session: None, |
| 183 | model: None, |
| 184 | cost: None, |
| 185 | mode_policy: None, |
| 186 | system_prompt: None, |
| 187 | skills: None, |
| 188 | workspace: None, |
| 189 | presentation: None, |
| 190 | media: None, |
| 191 | memory: None, |
| 192 | project: None, |
| 193 | skill_group: None, |
| 194 | plugin: None, |
| 195 | lifecycle: None, |
| 196 | control: None, |
| 197 | export: None, |
| 198 | structcopy: None, |
| 199 | debug_receipts: None, |
| 200 | debug_change: None, |
| 201 | debug_history: None, |
| 202 | debug_diff: None, |
| 203 | debug_undo: None, |
| 204 | debug_diagnostics: None, |
| 205 | } |
| 206 | } |
| 207 | |
| 208 | pub fn into_parts(self) -> ContextParts<'a> { |
| 209 | ContextParts { |
| 210 | session: self.session, |
| 211 | model: self.model, |
| 212 | cost: self.cost, |
| 213 | mode_policy: self.mode_policy, |
| 214 | system_prompt: self.system_prompt, |
| 215 | skills: self.skills, |
| 216 | workspace: self.workspace, |
| 217 | presentation: self.presentation, |
| 218 | media: self.media, |
| 219 | memory: self.memory, |
| 220 | project: self.project, |
| 221 | skill_group: self.skill_group, |
| 222 | plugin: self.plugin, |
| 223 | lifecycle: self.lifecycle, |
| 224 | control: self.control, |
| 225 | export: self.export, |
| 226 | structcopy: self.structcopy, |
| 227 | debug_receipts: self.debug_receipts, |
| 228 | debug_change: self.debug_change, |
| 229 | debug_history: self.debug_history, |
| 230 | debug_diff: self.debug_diff, |
| 231 | debug_undo: self.debug_undo, |
| 232 | debug_diagnostics: self.debug_diagnostics, |
| 233 | } |
| 234 | } |
| 235 | |
| 236 | pub fn with_session(mut self, value: &'a mut dyn CommandSessionContext) -> Self { |
| 237 | assert!( |
| 238 | self.session.replace(value).is_none(), |
| 239 | "session facet already set" |
| 240 | ); |
| 241 | self |
| 242 | } |
| 243 | |
| 244 | pub fn with_model(mut self, value: &'a mut dyn CommandModelContext) -> Self { |
| 245 | assert!( |
| 246 | self.model.replace(value).is_none(), |
| 247 | "model facet already set" |
| 248 | ); |
| 249 | self |
| 250 | } |
| 251 | |
| 252 | pub fn with_cost(mut self, value: &'a mut dyn CommandCostContext) -> Self { |
| 253 | assert!(self.cost.replace(value).is_none(), "cost facet already set"); |
| 254 | self |
| 255 | } |
| 256 | |
| 257 | pub fn with_mode_policy(mut self, value: &'a mut dyn CommandModePolicyContext) -> Self { |
| 258 | assert!( |
| 259 | self.mode_policy.replace(value).is_none(), |
| 260 | "mode-policy facet already set" |
| 261 | ); |
| 262 | self |
| 263 | } |
| 264 | |
| 265 | pub fn with_system_prompt(mut self, value: &'a mut dyn CommandSystemPromptContext) -> Self { |
| 266 | assert!( |
| 267 | self.system_prompt.replace(value).is_none(), |
| 268 | "system-prompt facet already set" |
| 269 | ); |
| 270 | self |
| 271 | } |
| 272 | |
| 273 | pub fn with_skills(mut self, value: &'a mut dyn CommandSkillsContext) -> Self { |
| 274 | assert!( |
| 275 | self.skills.replace(value).is_none(), |
| 276 | "skills facet already set" |
| 277 | ); |
| 278 | self |
| 279 | } |
| 280 | |
| 281 | pub fn with_workspace(mut self, value: &'a mut dyn CommandWorkspaceContext) -> Self { |
| 282 | assert!( |
| 283 | self.workspace.replace(value).is_none(), |
| 284 | "workspace facet already set" |
| 285 | ); |
| 286 | self |
| 287 | } |
| 288 | |
| 289 | pub fn with_presentation(mut self, value: &'a mut dyn CommandPresentationContext) -> Self { |
| 290 | assert!( |
| 291 | self.presentation.replace(value).is_none(), |
| 292 | "presentation facet already set" |
| 293 | ); |
| 294 | self |
| 295 | } |
| 296 | |
| 297 | pub fn with_media(mut self, value: &'a mut dyn CommandMediaContext) -> Self { |
| 298 | assert!( |
| 299 | self.media.replace(value).is_none(), |
| 300 | "media facet already set" |
| 301 | ); |
| 302 | self |
| 303 | } |
| 304 | |
| 305 | pub fn with_memory(mut self, value: &'a mut dyn CommandMemoryContext) -> Self { |
| 306 | assert!( |
| 307 | self.memory.replace(value).is_none(), |
| 308 | "memory facet already set" |
| 309 | ); |
| 310 | self |
| 311 | } |
| 312 | |
| 313 | pub fn with_project(mut self, value: &'a mut dyn CommandProjectContext) -> Self { |
| 314 | assert!( |
| 315 | self.project.replace(value).is_none(), |
| 316 | "project facet already set" |
| 317 | ); |
| 318 | self |
| 319 | } |
| 320 | |
| 321 | pub fn with_skill_group(mut self, value: &'a mut dyn CommandSkillGroupContext) -> Self { |
| 322 | assert!( |
| 323 | self.skill_group.replace(value).is_none(), |
| 324 | "skill-group facet already set" |
| 325 | ); |
| 326 | self |
| 327 | } |
| 328 | |
| 329 | pub fn with_plugin(mut self, value: &'a mut dyn CommandPluginContext) -> Self { |
| 330 | assert!( |
| 331 | self.plugin.replace(value).is_none(), |
| 332 | "plugin facet already set" |
| 333 | ); |
| 334 | self |
| 335 | } |
| 336 | |
| 337 | pub fn with_lifecycle(mut self, value: &'a mut dyn CommandSessionLifecycleContext) -> Self { |
| 338 | assert!( |
| 339 | self.lifecycle.replace(value).is_none(), |
| 340 | "lifecycle facet already set" |
| 341 | ); |
| 342 | self |
| 343 | } |
| 344 | |
| 345 | pub fn with_control(mut self, value: &'a mut dyn CommandSessionControlContext) -> Self { |
| 346 | assert!( |
| 347 | self.control.replace(value).is_none(), |
| 348 | "control facet already set" |
| 349 | ); |
| 350 | self |
| 351 | } |
| 352 | |
| 353 | pub fn with_structcopy(mut self, value: &'a mut dyn CommandSessionStructcopyContext) -> Self { |
| 354 | assert!( |
| 355 | self.structcopy.replace(value).is_none(), |
| 356 | "structcopy facet already set" |
| 357 | ); |
| 358 | self |
| 359 | } |
| 360 | |
| 361 | pub fn with_export(mut self, value: &'a mut dyn CommandSessionExportContext) -> Self { |
| 362 | assert!( |
| 363 | self.export.replace(value).is_none(), |
| 364 | "export facet already set" |
| 365 | ); |
| 366 | self |
| 367 | } |
| 368 | |
| 369 | pub fn with_debug_receipts(mut self, value: &'a mut dyn CommandDebugReceiptsContext) -> Self { |
| 370 | assert!( |
| 371 | self.debug_receipts.replace(value).is_none(), |
| 372 | "debug_receipts facet already set" |
| 373 | ); |
| 374 | self |
| 375 | } |
| 376 | |
| 377 | pub fn with_debug_change(mut self, value: &'a mut dyn CommandDebugChangeContext) -> Self { |
| 378 | assert!( |
| 379 | self.debug_change.replace(value).is_none(), |
| 380 | "debug_change facet already set" |
| 381 | ); |
| 382 | self |
| 383 | } |
| 384 | |
| 385 | pub fn with_debug_history(mut self, value: &'a mut dyn CommandDebugHistoryContext) -> Self { |
| 386 | assert!( |
| 387 | self.debug_history.replace(value).is_none(), |
| 388 | "debug_history facet already set" |
| 389 | ); |
| 390 | self |
| 391 | } |
| 392 | |
| 393 | pub fn with_debug_diff(mut self, value: &'a mut dyn CommandDebugDiffContext) -> Self { |
| 394 | assert!( |
| 395 | self.debug_diff.replace(value).is_none(), |
| 396 | "debug_diff facet already set" |
| 397 | ); |
| 398 | self |
| 399 | } |
| 400 | |
| 401 | pub fn with_debug_undo(mut self, value: &'a mut dyn CommandDebugUndoContext) -> Self { |
| 402 | assert!( |
| 403 | self.debug_undo.replace(value).is_none(), |
| 404 | "debug_undo facet already set" |
| 405 | ); |
| 406 | self |
| 407 | } |
| 408 | |
| 409 | pub fn with_debug_diagnostics( |
| 410 | mut self, |
| 411 | value: &'a mut dyn CommandDebugDiagnosticsContext, |
| 412 | ) -> Self { |
| 413 | assert!( |
| 414 | self.debug_diagnostics.replace(value).is_none(), |
| 415 | "debug diagnostics facet already set" |
| 416 | ); |
| 417 | self |
| 418 | } |
| 419 | } |
| 420 | |
| 421 | impl Default for CommandContexts<'_> { |
| 422 | fn default() -> Self { |
| 423 | Self::empty() |
| 424 | } |
| 425 | } |
| 426 |