refactor(tui): adopt command shapes in session control slice (FEAT-024) - #5951
Conversation
…OL bit, and envelope slot - CommandSessionControlContext object-safe facet with operation-specific delegates for relay/rename/resume/rc/remote-env/title host work (D2/D5) plus portable projections, receipts, and outcome enums (D4-D7) - SESSION_CONTROL = 1 << 14 with u16 storage unchanged (D3); one independently optional control envelope slot with duplicate-rejection builder; existing session and lifecycle facets untouched - Contract suite 52/52 green (5 new tests: capability identity incl. lifecycle non-collision, object safety + full outcome transport, error/empty-state transport, envelope slot independence/duplicate rejection, no-widening coexistence); boundary gates green; fmt clean Signed-off-by: Paulo Aboim Pinto <paulo.aboim.pinto@gmail.com>
Signed-off-by: Paulo Aboim Pinto <paulo.aboim.pinto@gmail.com>
Signed-off-by: Paulo Aboim Pinto <paulo.aboim.pinto@gmail.com>
- SessionControlAdapter implements CommandSessionControlContext over the shared host proxy: relay projection (goal/plan/todo/compact template), rename/title persistence with checkpoint recovery and baseline save/publication order, resume routing/imports, remote-control state/link/synchronous browser attempt, hosted-work Git target (credential-safe) - Bundle gains the control slot + restricted-exposure arm + parts() union - remote_env target helpers exposed crate-wide; RemoteControlController test-only live-link installer; browser outcome mapping isolated for unit proof - 11 new adapter tests green (commands::contract::tests 66/66); fmt clean; contract 52/52; TUI bins exit 0 Signed-off-by: Paulo Aboim Pinto <paulo.aboim.pinto@gmail.com>
Signed-off-by: Paulo Aboim Pinto <paulo.aboim.pinto@gmail.com>
- relay/rename/resume/rc/remote-env/title rewritten as portable contextual handlers (CONTRACT_INFO + ContractRegisterCommand + contextual extractor + pure portable fn); exact parsing, branch order, strings, and D7 action payloads preserved; rename/title/resume validation order matches baseline - Host machinery relocated into contract.rs (rename_with_manager test seam, remote_env Git/URL target resolution); session presentation bridge extended with the five remote-env keys; control_test_support fake-facet harness - Legacy bodies and their in-file App-bound tests removed; fake-facet handler tests with exact strings/actions/call counts replace them - Registrations swapped to ContextualCommand::from_contract for the six (five SESSION_CONTROL-only; remote-env SESSION_CONTROL|PRESENTATION); export/structcopy stay legacy; feat015/023 migration-state tests updated and feat024 registration test added - Session group suite 87/87; commands suite 1007/1007; contract 52/52; locale parity PASS; fmt clean; TUI bins exit 0 Signed-off-by: Paulo Aboim Pinto <paulo.aboim.pinto@gmail.com>
- Public-dispatch e2e test for the six control commands through the seam (relay SendMessage composition, rc status, remote-env localized overview, rename/title validation, bare resume picker) - Migration fixture/live gates PASS with session::control files portable and root session frontier unchanged [config, core, debug, session]; boundary/ CI-wiring/locale gates PASS; acceptance surface compiles; commands 1008/1008 Signed-off-by: Paulo Aboim Pinto <paulo.aboim.pinto@gmail.com>
…, test lints) - PlanProjection::Sections now carries Box<PlanSections> (clippy::large_enum_variant) - field_reassign_with_default and cmp_owned test-lint fixes - Workspace clippy -D warnings clean; contract 52/52; session 87/87; commands 1008/1008 Signed-off-by: Paulo Aboim Pinto <paulo.aboim.pinto@gmail.com>
… boundary gate - Box storage is forbidden in the contract crate by the boundary live gate (D1 fn-pointer rule); use an explicit clippy allow on the enum instead - Boundary live gate PASS; contract 52/52; session 87/87; clippy clean Signed-off-by: Paulo Aboim Pinto <paulo.aboim.pinto@gmail.com>
Restore the pre-migration rename, title, and remote-environment regression closure through public dispatch, keep TUI fixtures outside the movable session group, and remove the duplicate rename test implementation. Represent plan status as a contract-owned enum, pin exact facet call counts and restricted envelope exposure, and cover the missing presentation-authority failure. Signed-off-by: Paulo Aboim Pinto <paulo.aboim.pinto@gmail.com>
Hmbown
left a comment
There was a problem hiding this comment.
Reviewed at d8e43fe1d, specifically against the shape #5902 (FEAT-023) and #5865 (FEAT-020) established, since you framed this as the next slice in that series. It is a faithful continuation — I could not find an envelope, capability, adapter, registration, or dependency-boundary divergence. Three small things I'd like fixed, one of which partially defeats the slice's own purpose.
Tests I ran
Detached worktree at d8e43fe1d, RUST_MIN_STACK=16777216:
| Command | Result |
|---|---|
cargo test -p codewhale-command-contract --lib |
52 passed; 0 failed |
cargo test -p codewhale-tui --lib -- commands::groups::session commands::session_control_regression_tests |
89 passed; 0 failed |
cargo test -p codewhale-tui --lib -- commands::contract |
55 passed; 0 failed |
Check Signed-off-by is green and I confirmed it directly: all ten commits carry Signed-off-by: Paulo Aboim Pinto, no merge commits. Thank you — that is the third series in a row where I have had nothing to say about the trailers.
Shape parity — what I checked rather than assumed
- Facet:
CommandSessionControlContextatfacets.rs:1250-1372— same file, same trailing-append position, same "synchronous operation-granular delegates + portable receipt structs" style asCommandSessionLifecycleContext(facets.rs:1044+). - Capability bit:
handler.rs:47SESSION_CONTROL = 1 << 14, the next free bit afterSESSION_LIFECYCLE, with the samecontexts/ContextParts/with_controlwiring. The comment explicitly records that theu16backing stays per the maintainer resolution on #5902 rather than speculatively widening — exactly the right call, and worth having written down. - Adapter:
SessionControlAdapter<'a> { host: SharedCommandHost<'a> }(contract.rs:1020), intoCommandContextBundle(contract.rs:3947), gated incontexts()(contract.rs:3996). Same wiring asSessionLifecycleAdapter. - Fail-closed: every
*_contextualreturnsCommand capability unavailable: session_controlbefore touching anything, matchingbranch.rs:46'ssession_lifecycleform./remote-envchecks control then presentation, and its test asserts no control call happens when presentation is missing. - Localization: your "FEAT-036 owns localization" claim checks out against precedent. #5902's
branch.rsemits raw English literals throughout, and only surfaces that already calledtr()onorigin/mainwere routed throughPresentationAdapter::translate./remote-envwas the only one of the six that did, so it is the only one declaringPRESENTATION.key_to_session_message_id(contract.rs:1750) is thekey_to_plugin_message_idpattern from #5865. Placeholder sets on the fiveCmdRemoteEnv*ids matchen.json. No defect here. - Duplicated logic:
normalize_repo_slug,read_git_value,hosted_work_url,valid_branch_name,split_scp_origin,resolve_target,import_session_container,import_foreigneach have exactly one definition crate-wide, all now incontract.rs. The originals were deleted, not left behind. That is the "migrate the last consumer or do not start" rule actually honored. - Silent failure: I diffed every added
let _ =,.ok(),unwrap_or*.contract.rs:1319'sopen_url(&url).is_ok()discards the error, butorigin/main:remote_control.rs:53-58already didErr(_) =>;relay.rs'slet _ = writeln!(..)into aStringis infallible and byte-identical to baseline. Nothing new is swallowed. - Data loss:
/renameand/titlepreserve the #5430 first-snapshot recovery (contract.rs:1152-1158,1248-1254) and keep theupdate_session(..)re-sync before persisting, so an in-flight conversation is not overwritten by a stale document; publication failure still returns the post-save partial-success message./resume's route ordering matches baseline including||/&&precedence, andtransition_blocked()runs before any picker or I/O.
The one I'd like fixed before merge
/rename's length policy crossed the boundary; /title's did not — and the constant went from two copies to three.
origin/main had MAX_TITLE_LEN declared twice, in the two handlers that enforce it:
origin/main:crates/tui/src/commands/groups/session/rename.rs:10
origin/main:crates/tui/src/commands/groups/session/title.rs:17
After this PR it is declared three times, and the two commands enforce it on opposite sides of the seam:
crates/tui/src/commands/contract.rs:1130-1131—/rename's check now lives inside the adapter, in the TUI.crates/tui/src/commands/groups/session/title.rs:79-80—/title's check stayed in the portable handler.crates/tui/src/commands/session_control_regression_tests.rs:21— a third copy for the tests.
And contract.rs:1362 documents itself as:
/// Session-name/window-title length policy shared by `/rename` and `/title`.
const MAX_TITLE_LEN: usize = 100;It is not shared — /title never reads it. A comment that is false on the day it is written is worse than no comment.
The practical consequence is the one this slice exists to prevent: when FEAT-043 moves groups/session out, /title takes its user-facing limit along and /rename leaves its behind in codewhale-tui. Every prior slice put validation and exact message composition in the handler and let the facet return only host-stage errors (branch_portable in #5902 is the reference). Please move the /rename check back into rename_portable and let both handlers share one constant — behavior is already identical (I diffed the ordering: sanitize→length→session_id for rename, length→sanitize for title, both preserved), so this is a pure relocation.
Two small ones
TitleSetOutcome forces two unreachable arms with a new untranslated string. title.rs:73 and title.rs:88 both return CommandResult::error("internal title-state error"). The variant is fully determined by the argument the handler just passed one line earlier (None → Cleared, Some → Set), so the enum encodes a state the caller already knows, and the cost is a new user-visible English string with no baseline counterpart — the only "internal … error" in the migrated session group. Either have the delegate return Result<Option<String>, String> directly, or split it into clear_window_title / set_window_title. Then the arms disappear rather than being papered over.
Unnecessary visibility widening. groups/session/mod.rs:14 changes mod remote_env; to pub(crate) mod remote_env;. The only remote_env:: reference crate-wide is mod.rs:85, in the same parent module, where private mod is sufficient — I grepped. Every sibling in that file is private, and #5902 kept all of its migrated modules private. Revert it.
Nits, take or leave
fn message(result: &super::CommandResult) -> &str(strip the"Error: "prefix) is duplicated verbatim in all six handler test modules —rename.rs,title.rs,relay.rs,resume.rs,remote_control.rs,remote_env.rs.control_test_support.rsalready exists as the shared home for exactly this.- Test layout diverges from the slice immediately before it: #5902 collected portable-handler tests in one
lifecycle_portable_tests.rs; this one puts them inline per handler and adds a sharedcontrol_test_support.rs. InlineFake*is the dominant convention elsewhere in the crate, so I do not think it is wrong — butFakeControlvsCannedLifecyclemeans the two adjacent slices now read differently for no reason. /titleerror precedence: baseline resolved theSessionManagerbefore checkingcurrent_session_id; the adapter checkscurrent_session_idfirst (contract.rs:1235-1245). Reachable only when the sessions directory is unopenable and there is no active session, and both paths still error — noting it only because your PR body claims exact check-order preservation.- 15 of 16 capability bits are now allocated. FEAT-025 takes the last one. Whoever picks up FEAT-026 will be widening the type, so it is worth deciding that deliberately rather than under deadline.
One standing risk, not yours
/resume still replaces app.api_messages with the imported transcript without first saving the live conversation (contract.rs:1461). That is unchanged baseline behavior (origin/main:resume.rs:115) and I am explicitly not counting it against this PR — flagging it because this slice is the last time anyone will read that code closely before it moves.
Excellent work, as with the previous three. The dependency-boundary discipline is real and I checked it rather than taking the PR body's word for it.
Signed-off-by: Paulo Aboim Pinto <paulo.aboim.pinto@gmail.com>
|
Thank you for the detailed review and for checking the boundary rather than accepting the PR description at face value. I addressed all three requested changes in
I also took two of the optional cleanup notes while the same code was open:
I left the inline per-handler test layout unchanged because it remains the dominant local convention and does not affect extraction. The unchanged Focused verification on the new head is green:
The PR remains draft while the refreshed complete GitHub matrix runs. Paulo Aboim Pinto |
|
Merged with the matrix and Buildkite green on the exact head. The three nits in the review ( |
|
The refreshed matrix is green on the exact feedback-fix head
All requested review findings are addressed, the branch is mergeable, and the PR is now marked ready for owner review. Paulo Aboim Pinto |
Summary
FEAT-024 converts the TUI session-control slice to portable command shapes while keeping all production files in
codewhale-tui:/relay(/batonpass,/接力)/rename(/gaiming,/chongmingming)/resume(/r)/rc(/remote-control)/remote-env/title(/tabtitle,/window-title)Baseline: current
origin/mainat19e460889. The branch is eleven signed commits ahead with no merge commits.This PR:
CommandSessionControlContextand typed session-control DTOs;SESSION_CONTROL = 1 << 14while retainingCommandCapabilities(u16);SESSION_CONTROLfor/relay,/rename,/resume,/rc, and/title;SESSION_CONTROL | PRESENTATIONfor/remote-env;App, session manager, picker, persistence, Git-origin, browser, remote-service, checkpoint, and Work-state machinery inSessionControlAdapter;/rc opensynchronous through the host facet, preserving existing browser behavior;sessionmigration frontier for FEAT-025 and FEAT-026;groups/sessiondirectory.Tracking: EPIC-006 / FEAT-024 in umbrella #5316.
Dependency boundary
Portable session-control handlers do not access concrete
App, session managers, pickers, persistence services, Git wrappers, browser helpers, prompt/TODO/plan/Work-state types, configuration, or host services. Missing authority fails closed with:/remote-envalso fails safely when its separately declared presentation facet is unavailable.The only temporary TUI-owned result/action references are the exact compatibility payloads assigned to FEAT-037:
CommandResult;AppAction::SendMessage(String);AppAction::LoadSession(PathBuf);AppAction::RemoteControl(Start | Stop)andRemoteControlAction;AppAction::OpenExternalUrl { url, label }.The remaining staged dependencies are explicit:
No new crate dependency and no physical production-file move occur here.
codewhale-command-contractremains independent ofcodewhale-tui.Behavior and scope
/relaycomposes the existing byte-identical relay instruction from typed semantic data./renameand/titlepreserve checkpoint recovery, metadata and Work-state persistence, terminal-control sanitization, and existing length boundaries./remote-envpreserves HTTPS/SSH/CNB origin normalization, credential safety, branch encoding, malformed-origin rejection, detached-HEAD behavior, and localized placeholders./resumeretains transition-gate precedence and exact host call order.Review remediation included
A whole-feature critical review found and this branch resolves the following before PR creation:
PlanStep.status_label: Stringwith contract-ownedPlanStepStatus;/remote-env;Final internal review verdict: APPROVED.
Owner-review follow-up
The feedback reviewed at
d8e43fe1dis resolved ind4b68963d:/renamenow sanitizes and enforces the shared 100-character policy in the portable handler before any host mutation;/rename,/title, and the host regression suite use oneMAX_TITLE_LEN;TitleSetOutcomeand its unreachable mismatch arms are removed; explicitset_window_titleandclear_window_titledelegates return only host-stage errors;remote_envmodule visibility is private again;control_test_support.rs;/titlemanager/session error precedence is restored to the exact baseline order noted in review.Testing
All commands below passed on the final rebased head:
cargo fmt --all -- --checkcargo test -p codewhale-command-contract --locked— 52 passedcargo test -p codewhale-tui --tests --locked -- --test-threads=1— TUI library 11,784 passed / 13 ignored; Cucumber 16 passed; integration 289 passedcargo test --workspace --all-targets --all-features --locked -- --test-threads=1— complete workspace matrix passed; all-feature TUI library 11,799 passed / 13 ignored; Cucumber 33 passed; integration 289 passedcargo build --release --workspace --lockedcodewhale-command-contractandcodewhale-tuiwith--all-targets --all-features --locked -D warnings[config, core, debug, session]git diff --checkThe TUI and workspace runs used
RUST_MIN_STACK=8388608,--test-threads=1, and removed the harnessTMUXmarker so terminal-policy tests observed their intended fixtures.Checklist
CommandSessionControlContextreplaces direct host access for exactly six session-control handlersSigned-off-bytrailersNo-Issue: FEAT-024 is tracked in umbrella issue #5316, which remains open for the remaining EPIC-006 work.
Paulo Aboim Pinto