返回 CodeWhale
branch.rs
根目录 / crates / tui / src / commands / groups / session / branch.rs
1 use super::CommandResult;
2
3 use codewhale_command_contract::facets::CommandSessionLifecycleContext;
4 use codewhale_command_contract::handler::{CommandContexts, CommandHandler};
5 use codewhale_command_contract::metadata::{
6 CommandInfo as ContractInfo, RegisterCommand as ContractRegisterCommand,
7 };
8
9 pub(in crate::commands) struct BranchCmd;
10
11 // ---------------------------------------------------------------------------
12 // FEAT-023 Phase 4 (D3/D5/D6): portable contextual registration and handler.
13 // The handler owns parsing, branch order, exact messages, guidance appends,
14 // and action composition; all concrete host work stays behind the lifecycle
15 // facet. Missing lifecycle authority fails safely with the exact capability
16 // error (never a panic).
17 // ---------------------------------------------------------------------------
18
19 pub(in crate::commands) const CONTRACT_INFO: ContractInfo = ContractInfo {
20 name: "branch",
21 aliases: &[],
22 usage: "/branch <entry_id>",
23 description_key: "cmd_branch_description",
24 };
25
26 impl ContractRegisterCommand<CommandResult> for BranchCmd {
27 fn info() -> &'static ContractInfo {
28 &CONTRACT_INFO
29 }
30 fn handler() -> CommandHandler<CommandResult> {
31 CommandHandler::Contextual {
32 capabilities:
33 codewhale_command_contract::handler::CommandCapabilities::SESSION_LIFECYCLE,
34 handler: branch_contextual,
35 }
36 }
37 }
38
39 pub(in crate::commands) fn branch_contextual(
40 contexts: CommandContexts<'_>,
41 arg: Option<&str>,
42 ) -> CommandResult {
43 let mut parts = contexts.into_parts();
44 let Some(lifecycle) = parts.lifecycle.as_deref_mut() else {
45 return CommandResult::error(
46 "Command capability unavailable: session_lifecycle".to_string(),
47 );
48 };
49 branch_portable(lifecycle, arg)
50 }
51
52 pub(in crate::commands) fn branch_portable(
53 lifecycle: &mut dyn CommandSessionLifecycleContext,
54 arg: Option<&str>,
55 ) -> CommandResult {
56 if lifecycle.transition_blocked() {
57 return CommandResult::error(
58 "Cannot branch while runtime work is active. Wait for the turn to finish, or cancel it first."
59 .to_string(),
60 );
61 }
62 let Some(entry_id) = arg.map(str::trim).filter(|s| !s.is_empty()) else {
63 if let Some(leaf) = lifecycle.branch_current_leaf_hint() {
64 return CommandResult::message(format!(
65 "Current leaf: {leaf}\nUse `/branch <entry_id>` to move the leaf (history is never rewritten).\nUse `/tree` to list entry ids."
66 ));
67 }
68 return CommandResult::message(
69 "Usage: /branch <entry_id>\nMoves the active leaf to an existing entry. Future appends become children of that entry.\nHistory is never rewritten — branching only moves the leaf.\n\nUse `/tree` to see entry ids."
70 .to_string(),
71 );
72 };
73 match lifecycle.branch_to(entry_id) {
74 Ok(outcome) => CommandResult::with_message_and_action(
75 format!(
76 "Branched to entry {entry_id} (leaf now {}); journal entries {} (history preserved, leaf moved only)",
77 outcome.leaf_display, outcome.journal_entries_before
78 ),
79 super::sync_session_action(outcome.sync),
80 ),
81 Err(error) => CommandResult::error(error),
82 }
83 }
84
84 lines RUST