返回 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, CommandMediaContext, CommandMemoryContext, CommandModePolicyContext,
9 CommandModelContext, CommandPluginContext, CommandPresentationContext, CommandProjectContext,
10 CommandSessionContext, CommandSessionControlContext, CommandSessionExportContext,
11 CommandSessionLifecycleContext, CommandSkillGroupContext, CommandSkillsContext,
12 CommandSystemPromptContext, CommandWorkspaceContext,
13 };
14
15 /// Exact host capabilities exposed to one contextual command handler.
16 ///
17 /// The set lives in the external contract crate so command registrations can
18 /// declare least authority without naming the TUI host. The dispatcher uses
19 /// the declaration to populate only those slots in [`CommandContexts`].
20 #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
21 pub struct CommandCapabilities(u16);
22
23 impl CommandCapabilities {
24 pub const NONE: Self = Self(0);
25 pub const SESSION: Self = Self(1 << 0);
26 pub const MODEL: Self = Self(1 << 1);
27 pub const COST: Self = Self(1 << 2);
28 pub const MODE_POLICY: Self = Self(1 << 3);
29 pub const SYSTEM_PROMPT: Self = Self(1 << 4);
30 pub const SKILLS: Self = Self(1 << 5);
31 pub const WORKSPACE: Self = Self(1 << 6);
32 pub const PRESENTATION: Self = Self(1 << 7);
33 pub const MEDIA: Self = Self(1 << 8);
34 /// Memory-group host data (FEAT-019 D1).
35 pub const MEMORY: Self = Self(1 << 9);
36 /// Project-group host data (FEAT-021 D1).
37 pub const PROJECT: Self = Self(1 << 10);
38 /// Skills-group host data (FEAT-022 D1).
39 pub const SKILL_GROUP: Self = Self(1 << 11);
40 /// Plugin-group host data (FEAT-020 D1), appended after current main capabilities.
41 pub const PLUGIN: Self = Self(1 << 12);
42 /// Session-lifecycle host data (FEAT-023 D3), the next non-conflicting bit
43 /// after `PLUGIN`. Required only by the seven host-dependent lifecycle
44 /// commands; `/compact` and `/purge` remain pure. Never widened by the
45 /// basic session capability.
46 pub const SESSION_LIFECYCLE: Self = Self(1 << 13);
47 /// Session-control host data (FEAT-024 D3), the next non-conflicting bit
48 /// after `SESSION_LIFECYCLE`. Required only by the six host-dependent
49 /// control commands (`/relay`, `/rename`, `/resume`, `/rc`, `/remote-env`,
50 /// `/title`); `/remote-env` also declares `PRESENTATION`. The backing
51 /// storage remains `u16` per the resolved maintainer review on FEAT-023 PR
52 /// #5902 — bit 14 is available, so no speculative widening is performed.
53 pub const SESSION_CONTROL: Self = Self(1 << 14);
54 /// Session-export host data (FEAT-025 D1), the next non-conflicting bit
55 /// after `SESSION_CONTROL`. Required only by the host-dependent `/export`
56 /// command and its `/daochu` alias; every concrete App, snapshot, clipboard,
57 /// filesystem, history, and turn-handoff access stays behind the TUI export
58 /// adapter.
59 ///
60 /// **Capacity: this is the last free bit.** Bits 0-15 are now fully
61 /// allocated, so another capability cannot be added without widening the
62 /// backing storage to `u32`. FEAT-026 (session structcopy) needs its own
63 /// exact-minimum facet and therefore owns that widening decision; reusing
64 /// `SESSION_EXPORT` for it would break the least-capability invariant.
65 /// The `export_capability_space_is_exactly_full` test pins the capacity so
66 /// the next author gets a deliberate decision instead of a compile error
67 /// with no context.
68 pub const SESSION_EXPORT: Self = Self(1 << 15);
69
70 /// Raw bit pattern, for tests that pin the capability-space capacity.
71 ///
72 /// Kept `#[cfg(test)]` so the `u16` backing stays an implementation detail
73 /// and nothing can widen it accidentally through a public accessor.
74 #[cfg(test)]
75 pub(crate) const fn bits_for_test(self) -> u16 {
76 self.0
77 }
78
79 pub const fn union(self, other: Self) -> Self {
80 Self(self.0 | other.0)
81 }
82
83 pub const fn contains(self, capability: Self) -> bool {
84 !capability.is_empty() && self.0 & capability.0 == capability.0
85 }
86
87 pub const fn is_empty(self) -> bool {
88 self.0 == 0
89 }
90 }
91
92 impl std::ops::BitOr for CommandCapabilities {
93 type Output = Self;
94
95 fn bitor(self, rhs: Self) -> Self::Output {
96 self.union(rhs)
97 }
98 }
99
100 /// A command handler that is either argument-only or capability-scoped.
101 #[derive(Clone, Copy)]
102 pub enum CommandHandler<R> {
103 Pure(fn(Option<&str>) -> R),
104 Contextual {
105 capabilities: CommandCapabilities,
106 handler: fn(CommandContexts<'_>, Option<&str>) -> R,
107 },
108 }
109
110 /// Transport envelope with one independently optional facet slot.
111 pub struct CommandContexts<'a> {
112 session: Option<&'a mut dyn CommandSessionContext>,
113 model: Option<&'a mut dyn CommandModelContext>,
114 cost: Option<&'a mut dyn CommandCostContext>,
115 mode_policy: Option<&'a mut dyn CommandModePolicyContext>,
116 system_prompt: Option<&'a mut dyn CommandSystemPromptContext>,
117 skills: Option<&'a mut dyn CommandSkillsContext>,
118 workspace: Option<&'a mut dyn CommandWorkspaceContext>,
119 presentation: Option<&'a mut dyn CommandPresentationContext>,
120 media: Option<&'a mut dyn CommandMediaContext>,
121 memory: Option<&'a mut dyn CommandMemoryContext>,
122 project: Option<&'a mut dyn CommandProjectContext>,
123 skill_group: Option<&'a mut dyn CommandSkillGroupContext>,
124 plugin: Option<&'a mut dyn CommandPluginContext>,
125 lifecycle: Option<&'a mut dyn CommandSessionLifecycleContext>,
126 control: Option<&'a mut dyn CommandSessionControlContext>,
127 export: Option<&'a mut dyn CommandSessionExportContext>,
128 }
129
130 /// Consumed envelope used when one handler needs several independent facets.
131 pub struct ContextParts<'a> {
132 pub session: Option<&'a mut dyn CommandSessionContext>,
133 pub model: Option<&'a mut dyn CommandModelContext>,
134 pub cost: Option<&'a mut dyn CommandCostContext>,
135 pub mode_policy: Option<&'a mut dyn CommandModePolicyContext>,
136 pub system_prompt: Option<&'a mut dyn CommandSystemPromptContext>,
137 pub skills: Option<&'a mut dyn CommandSkillsContext>,
138 pub workspace: Option<&'a mut dyn CommandWorkspaceContext>,
139 pub presentation: Option<&'a mut dyn CommandPresentationContext>,
140 pub media: Option<&'a mut dyn CommandMediaContext>,
141 pub memory: Option<&'a mut dyn CommandMemoryContext>,
142 pub project: Option<&'a mut dyn CommandProjectContext>,
143 pub skill_group: Option<&'a mut dyn CommandSkillGroupContext>,
144 pub plugin: Option<&'a mut dyn CommandPluginContext>,
145 pub lifecycle: Option<&'a mut dyn CommandSessionLifecycleContext>,
146 pub control: Option<&'a mut dyn CommandSessionControlContext>,
147 pub export: Option<&'a mut dyn CommandSessionExportContext>,
148 }
149
150 impl<'a> CommandContexts<'a> {
151 pub fn empty() -> Self {
152 Self {
153 session: None,
154 model: None,
155 cost: None,
156 mode_policy: None,
157 system_prompt: None,
158 skills: None,
159 workspace: None,
160 presentation: None,
161 media: None,
162 memory: None,
163 project: None,
164 skill_group: None,
165 plugin: None,
166 lifecycle: None,
167 control: None,
168 export: None,
169 }
170 }
171
172 pub fn into_parts(self) -> ContextParts<'a> {
173 ContextParts {
174 session: self.session,
175 model: self.model,
176 cost: self.cost,
177 mode_policy: self.mode_policy,
178 system_prompt: self.system_prompt,
179 skills: self.skills,
180 workspace: self.workspace,
181 presentation: self.presentation,
182 media: self.media,
183 memory: self.memory,
184 project: self.project,
185 skill_group: self.skill_group,
186 plugin: self.plugin,
187 lifecycle: self.lifecycle,
188 control: self.control,
189 export: self.export,
190 }
191 }
192
193 pub fn with_session(mut self, value: &'a mut dyn CommandSessionContext) -> Self {
194 assert!(
195 self.session.replace(value).is_none(),
196 "session facet already set"
197 );
198 self
199 }
200
201 pub fn with_model(mut self, value: &'a mut dyn CommandModelContext) -> Self {
202 assert!(
203 self.model.replace(value).is_none(),
204 "model facet already set"
205 );
206 self
207 }
208
209 pub fn with_cost(mut self, value: &'a mut dyn CommandCostContext) -> Self {
210 assert!(self.cost.replace(value).is_none(), "cost facet already set");
211 self
212 }
213
214 pub fn with_mode_policy(mut self, value: &'a mut dyn CommandModePolicyContext) -> Self {
215 assert!(
216 self.mode_policy.replace(value).is_none(),
217 "mode-policy facet already set"
218 );
219 self
220 }
221
222 pub fn with_system_prompt(mut self, value: &'a mut dyn CommandSystemPromptContext) -> Self {
223 assert!(
224 self.system_prompt.replace(value).is_none(),
225 "system-prompt facet already set"
226 );
227 self
228 }
229
230 pub fn with_skills(mut self, value: &'a mut dyn CommandSkillsContext) -> Self {
231 assert!(
232 self.skills.replace(value).is_none(),
233 "skills facet already set"
234 );
235 self
236 }
237
238 pub fn with_workspace(mut self, value: &'a mut dyn CommandWorkspaceContext) -> Self {
239 assert!(
240 self.workspace.replace(value).is_none(),
241 "workspace facet already set"
242 );
243 self
244 }
245
246 pub fn with_presentation(mut self, value: &'a mut dyn CommandPresentationContext) -> Self {
247 assert!(
248 self.presentation.replace(value).is_none(),
249 "presentation facet already set"
250 );
251 self
252 }
253
254 pub fn with_media(mut self, value: &'a mut dyn CommandMediaContext) -> Self {
255 assert!(
256 self.media.replace(value).is_none(),
257 "media facet already set"
258 );
259 self
260 }
261
262 pub fn with_memory(mut self, value: &'a mut dyn CommandMemoryContext) -> Self {
263 assert!(
264 self.memory.replace(value).is_none(),
265 "memory facet already set"
266 );
267 self
268 }
269
270 pub fn with_project(mut self, value: &'a mut dyn CommandProjectContext) -> Self {
271 assert!(
272 self.project.replace(value).is_none(),
273 "project facet already set"
274 );
275 self
276 }
277
278 pub fn with_skill_group(mut self, value: &'a mut dyn CommandSkillGroupContext) -> Self {
279 assert!(
280 self.skill_group.replace(value).is_none(),
281 "skill-group facet already set"
282 );
283 self
284 }
285
286 pub fn with_plugin(mut self, value: &'a mut dyn CommandPluginContext) -> Self {
287 assert!(
288 self.plugin.replace(value).is_none(),
289 "plugin facet already set"
290 );
291 self
292 }
293
294 pub fn with_lifecycle(mut self, value: &'a mut dyn CommandSessionLifecycleContext) -> Self {
295 assert!(
296 self.lifecycle.replace(value).is_none(),
297 "lifecycle facet already set"
298 );
299 self
300 }
301
302 pub fn with_control(mut self, value: &'a mut dyn CommandSessionControlContext) -> Self {
303 assert!(
304 self.control.replace(value).is_none(),
305 "control facet already set"
306 );
307 self
308 }
309
310 pub fn with_export(mut self, value: &'a mut dyn CommandSessionExportContext) -> Self {
311 assert!(
312 self.export.replace(value).is_none(),
313 "export facet already set"
314 );
315 self
316 }
317 }
318
319 impl Default for CommandContexts<'_> {
320 fn default() -> Self {
321 Self::empty()
322 }
323 }
324
324 lines RUST