Skip to content

Commit 41fecf8

Browse files
authored
Merge pull request #5902 from aboimpinto/feat/FEAT-023-adopt-command-shapes-in-tui-session-lifecycle-slice
refactor(tui): adopt command shapes in session lifecycle slice (FEAT-023)
2 parents f10a468 + 52a53ce commit 41fecf8

20 files changed

Lines changed: 3792 additions & 1678 deletions

crates/command-contract/src/facets.rs

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -940,3 +940,171 @@ pub trait CommandSkillGroupContext {
940940
/// `/restore` trust gate posture (yolo / trust_mode).
941941
fn approval_state(&self) -> CommandApprovalState;
942942
}
943+
944+
// ---------------------------------------------------------------------------
945+
// Session lifecycle capability (FEAT-023).
946+
//
947+
// One contract-owned facet for the seven host-dependent lifecycle commands;
948+
// `/compact` and `/purge` stay pure. The shared `CommandSessionContext` above
949+
// stays unchanged: it
950+
// serves commands outside this slice and must not gain persistence,
951+
// navigation, picker, or lifecycle mutation authority (D2). No concrete App,
952+
// SessionManager, session-journal, picker, configuration, or view-stack type
953+
// crosses this boundary; successful results are structured portable fields so
954+
// the handlers retain exact message composition (D2/D5).
955+
// ---------------------------------------------------------------------------
956+
957+
/// Portable synchronization fields a lifecycle handler maps into the
958+
/// temporary `SyncSession` action payload. The conversation and prompt types
959+
/// are `codewhale-core` request types shared by the contract and the TUI
960+
/// (FEAT-037 will move shared outcome ownership; FEAT-023 keeps the bounded
961+
/// reference only for `/fork` and `/new` transitions, D6).
962+
#[derive(Clone, Debug, PartialEq)]
963+
pub struct SessionSyncPayload {
964+
pub session_id: Option<String>,
965+
pub messages: Vec<Message>,
966+
pub system_prompt: Option<SystemPrompt>,
967+
pub model: String,
968+
pub workspace: PathBuf,
969+
pub mode: CommandMode,
970+
}
971+
972+
/// `/branch` success projection (`session/branch.rs`). The handler composes
973+
/// the exact success line from these deterministic fields.
974+
#[derive(Clone, Debug, PartialEq)]
975+
pub struct SessionBranchOutcome {
976+
pub leaf_display: String,
977+
pub journal_entries_before: usize,
978+
}
979+
980+
/// `/fork` success projection for an active-conversation fork. The handler
981+
/// composes `Forked session {parent} -> {fork}` from these required fields.
982+
#[derive(Clone, Debug, PartialEq)]
983+
pub struct SessionForkReceipt {
984+
pub parent_label: String,
985+
pub fork_label: String,
986+
pub sync: SessionSyncPayload,
987+
}
988+
989+
/// `/fork <session_id|prefix>` success projection. Explicit-source forks
990+
/// always report their spawn depth, so the contract makes that field required
991+
/// rather than permitting an invalid missing-depth state.
992+
#[derive(Clone, Debug, PartialEq)]
993+
pub struct SessionForkFromReceipt {
994+
pub parent_label: String,
995+
pub fork_label: String,
996+
pub spawn_depth: u64,
997+
pub sync: SessionSyncPayload,
998+
}
999+
1000+
/// `/save` success projection. The host performs the full baseline sequence
1001+
/// (snapshot, serialization, atomic write, metadata application, work-state
1002+
/// publication); the handler renders `Session saved to {display_path} (ID:
1003+
/// {truncated_id})`.
1004+
#[derive(Clone, Debug, PartialEq)]
1005+
pub struct SessionSaveReceipt {
1006+
pub display_path: String,
1007+
pub truncated_id: String,
1008+
}
1009+
1010+
/// `/new` success projection. The handler renders
1011+
/// `Started new session {truncated_id} (New Session). Previous sessions
1012+
/// remain available via /resume.`
1013+
#[derive(Clone, Debug, PartialEq)]
1014+
pub struct SessionNewReceipt {
1015+
pub truncated_id: String,
1016+
pub sync: SessionSyncPayload,
1017+
}
1018+
1019+
/// `/sessions archive|unarchive|restore` success projection. The handler
1020+
/// renders `Archived session {id} ({title})` or `Restored session ...` from
1021+
/// the verb it dispatched.
1022+
#[derive(Clone, Debug, PartialEq)]
1023+
pub struct SessionArchiveReceipt {
1024+
pub truncated_id: String,
1025+
pub title: String,
1026+
}
1027+
1028+
/// `/tree` body projection. The body rendering source (journal tree and
1029+
/// linear transcript) stays TUI-owned; the handler appends the exact
1030+
/// guidance lines (D5).
1031+
#[derive(Clone, Debug, PartialEq)]
1032+
pub enum TreeBodyProjection {
1033+
/// Journal render already includes the trailing newline before guidance.
1034+
Journal {
1035+
rendered: String,
1036+
},
1037+
/// Linear pre-journal render (the marker lines).
1038+
Linear {
1039+
rendered: String,
1040+
},
1041+
EmptySession,
1042+
NoSession,
1043+
}
1044+
1045+
/// Lifecycle authority for the session command slice (FEAT-023 D2).
1046+
///
1047+
/// Operation-granular synchronous delegates over the exact minimum host work
1048+
/// the nine commands consume. Delegates may return the explicit host-error
1049+
/// text the baseline surfaces for a failing stage; successful results are
1050+
/// structured portable fields so handlers retain byte-identical composition.
1051+
pub trait CommandSessionLifecycleContext {
1052+
/// Live transition gate. Handlers return their own blocked-error text
1053+
/// before invoking any mutating delegate, matching the baseline ordering
1054+
/// (`/branch`, `/fork`, `/load`, `/new`). `/fork picker` and `/tree`
1055+
/// never consult it in the baseline, so their paths must not either.
1056+
fn transition_blocked(&self) -> bool;
1057+
1058+
/// `/branch` with no argument: the current leaf when an active journaled
1059+
/// session resolves, otherwise `None` (the baseline silently falls back
1060+
/// to the usage message on this path).
1061+
fn branch_current_leaf_hint(&self) -> Option<String>;
1062+
1063+
/// `/branch <entry_id>`: persist the leaf move and apply the branched
1064+
/// transcript. Errors are the exact baseline message for the failing
1065+
/// stage (no active session, directory open, load, persist, or branch
1066+
/// failure).
1067+
fn branch_to(&mut self, entry_id: &str) -> Result<SessionBranchOutcome, String>;
1068+
1069+
/// `/tree`: produce the journal/linear/empty/no-session projection.
1070+
/// Errors are the exact baseline directory-open message.
1071+
fn tree_body(&self) -> Result<TreeBodyProjection, String>;
1072+
1073+
/// `/save [path]`: the full baseline persistence sequence.
1074+
fn save_session(&mut self, explicit_path: Option<String>)
1075+
-> Result<SessionSaveReceipt, String>;
1076+
1077+
/// `/fork` (active conversation): the full baseline parent/child save and
1078+
/// switch sequence.
1079+
fn fork_active(&mut self) -> Result<SessionForkReceipt, String>;
1080+
1081+
/// `/fork <session_id|prefix>`: explicit-source fork.
1082+
fn fork_from(&mut self, session_id_or_prefix: &str) -> Result<SessionForkFromReceipt, String>;
1083+
1084+
/// `/new [--force]`: fresh-session transition. The caller has already
1085+
/// parsed the argument and applied the transition-blocked gate; blocker,
1086+
/// busy-work-state, and success handling match the baseline.
1087+
fn fresh_session(&mut self, force: bool) -> Result<SessionNewReceipt, String>;
1088+
1089+
/// `/load <path>`: resolve the path (separator-bearing direct vs
1090+
/// workspace-relative) and validate the saved-session shape without
1091+
/// applying state or emitting a premature success receipt.
1092+
fn load_session(&mut self, path: &str) -> Result<PathBuf, String>;
1093+
1094+
/// `/sessions` picker open with optional preselection (bare, `show`,
1095+
/// `list`, `picker`, and `open <id>` forms). Picker construction and
1096+
/// locale selection stay host-side.
1097+
fn open_picker(&mut self, preselected: Option<String>);
1098+
1099+
/// `/sessions archive|unarchive|restore <id>`: durable lifecycle state
1100+
/// update that also syncs the live cached metadata atomically.
1101+
fn set_archived(
1102+
&mut self,
1103+
session_id: &str,
1104+
archived: bool,
1105+
) -> Result<SessionArchiveReceipt, String>;
1106+
1107+
/// `/sessions prune <days>`: prune persisted sessions older than `days`
1108+
/// days while protecting the active session; returns the number pruned.
1109+
fn prune_sessions(&mut self, days: u64) -> Result<usize, String>;
1110+
}

crates/command-contract/src/handler.rs

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@
77
use crate::facets::{
88
CommandCostContext, CommandMediaContext, CommandMemoryContext, CommandModePolicyContext,
99
CommandModelContext, CommandPluginContext, CommandPresentationContext, CommandProjectContext,
10-
CommandSessionContext, CommandSkillGroupContext, CommandSkillsContext,
11-
CommandSystemPromptContext, CommandWorkspaceContext,
10+
CommandSessionContext, CommandSessionLifecycleContext, CommandSkillGroupContext,
11+
CommandSkillsContext, CommandSystemPromptContext, CommandWorkspaceContext,
1212
};
1313

1414
/// Exact host capabilities exposed to one contextual command handler.
@@ -38,6 +38,11 @@ impl CommandCapabilities {
3838
pub const SKILL_GROUP: Self = Self(1 << 11);
3939
/// Plugin-group host data (FEAT-020 D1), appended after current main capabilities.
4040
pub const PLUGIN: Self = Self(1 << 12);
41+
/// Session-lifecycle host data (FEAT-023 D3), the next non-conflicting bit
42+
/// after `PLUGIN`. Required only by the seven host-dependent lifecycle
43+
/// commands; `/compact` and `/purge` remain pure. Never widened by the
44+
/// basic session capability.
45+
pub const SESSION_LIFECYCLE: Self = Self(1 << 13);
4146

4247
pub const fn union(self, other: Self) -> Self {
4348
Self(self.0 | other.0)
@@ -85,6 +90,7 @@ pub struct CommandContexts<'a> {
8590
project: Option<&'a mut dyn CommandProjectContext>,
8691
skill_group: Option<&'a mut dyn CommandSkillGroupContext>,
8792
plugin: Option<&'a mut dyn CommandPluginContext>,
93+
lifecycle: Option<&'a mut dyn CommandSessionLifecycleContext>,
8894
}
8995

9096
/// Consumed envelope used when one handler needs several independent facets.
@@ -102,6 +108,7 @@ pub struct ContextParts<'a> {
102108
pub project: Option<&'a mut dyn CommandProjectContext>,
103109
pub skill_group: Option<&'a mut dyn CommandSkillGroupContext>,
104110
pub plugin: Option<&'a mut dyn CommandPluginContext>,
111+
pub lifecycle: Option<&'a mut dyn CommandSessionLifecycleContext>,
105112
}
106113

107114
impl<'a> CommandContexts<'a> {
@@ -120,6 +127,7 @@ impl<'a> CommandContexts<'a> {
120127
project: None,
121128
skill_group: None,
122129
plugin: None,
130+
lifecycle: None,
123131
}
124132
}
125133

@@ -138,6 +146,7 @@ impl<'a> CommandContexts<'a> {
138146
project: self.project,
139147
skill_group: self.skill_group,
140148
plugin: self.plugin,
149+
lifecycle: self.lifecycle,
141150
}
142151
}
143152

@@ -241,6 +250,14 @@ impl<'a> CommandContexts<'a> {
241250
);
242251
self
243252
}
253+
254+
pub fn with_lifecycle(mut self, value: &'a mut dyn CommandSessionLifecycleContext) -> Self {
255+
assert!(
256+
self.lifecycle.replace(value).is_none(),
257+
"lifecycle facet already set"
258+
);
259+
self
260+
}
244261
}
245262

246263
impl Default for CommandContexts<'_> {

0 commit comments

Comments
 (0)