返回 CodeWhale
fork.rs
根目录 / crates / tui / src / commands / groups / session / fork.rs
1 //! `/fork` command — interactive picker (#576) + direct fork.
2
3 use super::CommandResult;
4
5 use codewhale_command_contract::facets::CommandSessionLifecycleContext;
6 use codewhale_command_contract::handler::{CommandContexts, CommandHandler};
7 use codewhale_command_contract::metadata::{
8 CommandInfo as ContractInfo, RegisterCommand as ContractRegisterCommand,
9 };
10
11 pub(in crate::commands) struct ForkCmd;
12
13 // ---------------------------------------------------------------------------
14 // FEAT-023 Phase 4 (D3/D5/D6): portable contextual registration and handler.
15 // The handler owns parsing, branch order, exact messages, guidance appends,
16 // and action composition; all concrete host work stays behind the lifecycle
17 // facet. Missing lifecycle authority fails safely with the exact capability
18 // error (never a panic).
19 // ---------------------------------------------------------------------------
20
21 pub(in crate::commands) const CONTRACT_INFO: ContractInfo = ContractInfo {
22 name: "fork",
23 aliases: &["f"],
24 usage: "/fork [session_id|picker]",
25 description_key: "cmd_fork_description",
26 };
27
28 impl ContractRegisterCommand<CommandResult> for ForkCmd {
29 fn info() -> &'static ContractInfo {
30 &CONTRACT_INFO
31 }
32 fn handler() -> CommandHandler<CommandResult> {
33 CommandHandler::Contextual {
34 capabilities:
35 codewhale_command_contract::handler::CommandCapabilities::SESSION_LIFECYCLE,
36 handler: fork_contextual,
37 }
38 }
39 }
40
41 pub(in crate::commands) fn fork_contextual(
42 contexts: CommandContexts<'_>,
43 arg: Option<&str>,
44 ) -> CommandResult {
45 let mut parts = contexts.into_parts();
46 let Some(lifecycle) = parts.lifecycle.as_deref_mut() else {
47 return CommandResult::error(
48 "Command capability unavailable: session_lifecycle".to_string(),
49 );
50 };
51 fork_portable(lifecycle, arg)
52 }
53
54 pub(in crate::commands) fn fork_portable(
55 lifecycle: &mut dyn CommandSessionLifecycleContext,
56 arg: Option<&str>,
57 ) -> CommandResult {
58 let trimmed = arg.map(str::trim).filter(|s| !s.is_empty());
59 if let Some(a) = trimmed {
60 if matches!(
61 a.to_ascii_lowercase().as_str(),
62 "picker" | "list" | "--picker" | "pick"
63 ) {
64 lifecycle.open_picker(None);
65 return CommandResult::message(
66 "Fork picker: select a session and then run `/fork <id>` to fork it.".to_string(),
67 );
68 }
69 if lifecycle.transition_blocked() {
70 return CommandResult::error(
71 "Cannot fork a session while runtime work is active. Wait for the current turn, maintenance, and background tasks to finish, or cancel that specific work first."
72 .to_string(),
73 );
74 }
75 return match lifecycle.fork_from(a) {
76 Ok(receipt) => CommandResult::with_message_and_action(
77 format!(
78 "Forked session {} -> {} (spawn_depth {})",
79 receipt.parent_label, receipt.fork_label, receipt.spawn_depth
80 ),
81 super::sync_session_action(receipt.sync),
82 ),
83 Err(error) => CommandResult::error(error),
84 };
85 }
86 if lifecycle.transition_blocked() {
87 return CommandResult::error(
88 "Cannot fork a session while runtime work is active. Wait for the current turn, maintenance, and background tasks to finish, or cancel that specific work first."
89 .to_string(),
90 );
91 }
92 match lifecycle.fork_active() {
93 Ok(receipt) => CommandResult::with_message_and_action(
94 format!(
95 "Forked session {} -> {}",
96 receipt.parent_label, receipt.fork_label
97 ),
98 super::sync_session_action(receipt.sync),
99 ),
100 Err(error) => CommandResult::error(error),
101 }
102 }
103
103 lines RUST