返回 CodeWhale
handler.rs
根目录 / crates / command-contract / src / handler.rs
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
426 lines RUST