Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
7b13b57
tui: carry custom themes inside the typed theme value
Sep 2, 2026
091f133
tui: drop orphaned launch-screen and sidebar locale keys
Sep 2, 2026
01ec884
tui: align typed config and schema with the live value spaces
Sep 2, 2026
ac5fcae
tui: /theme underwater applies the Deepsea pair
Hmbown Sep 2, 2026
2742abf
tui: launch card runs nothing on a reflexive Enter; every launch flow…
Hmbown Sep 2, 2026
1621360
tui: locale strings for the ocean_treatment collapse
Sep 2, 2026
08d653f
tui: regenerated launch mark assets
Sep 2, 2026
41d9d39
tui: collapse ocean_treatment into ThemeId::Underwater
Sep 2, 2026
1a3e35b
tui: route commands and engine through ThemeId::Underwater
Sep 2, 2026
4888eda
tui: repaint ocean, picker, widgets, and goldens onto the theme
Sep 2, 2026
bfcb23a
tui: finish the context_percent plumbing (call sites, abyss test, dro…
Sep 2, 2026
8560081
tui: align tests and startup goldens with the collapse rules
Sep 2, 2026
fb9dbd7
tui: align tests and startup goldens with the collapse rules
Sep 2, 2026
2b54b22
tui: delete orphaned non-context color shims, checked cache division
Sep 2, 2026
9a29726
fix: drop needless borrow flagged by clippy 1.98 in placement round-t…
Sep 2, 2026
6f2e4c9
tui: align PTY proof and worktree test with the collapse's honest chrome
Sep 2, 2026
ed144de
Merge branch 'feat/shell-ux-collapse-20260902' into fix/0912-ux-20260902
Sep 2, 2026
9c859bf
Merge branch 'fix/config-truth-20260902' into fix/0912-ux-20260902
Sep 2, 2026
9b1440b
Merge remote-tracking branch 'origin/feat/shell-ux-collapse-20260902'…
Sep 2, 2026
f14ed0a
Merge remote-tracking branch 'origin/fix/config-truth-20260902' into …
Sep 2, 2026
ca4da67
feat: add underwater-retro selectable theme (legacy flat deepsea look)
Sep 3, 2026
c891bd3
feat: unified hover contract (hovered_row_style, HoverTargetKind, hov…
Sep 3, 2026
bc4c0bd
feat: rename sidebar/rail to workbar, bottom default, /workbar command
Sep 3, 2026
b8ab165
feat: fleet menu regroup under Model cluster, TAB_FLEET removed, hove…
Sep 3, 2026
607b65e
feat: theme picker live preview with Esc rollback, underwater default
Sep 3, 2026
c1ffd20
feat: provider screen redo with per-model cost labels
Sep 3, 2026
f53cacd
feat: startup screen redo with recent sessions and hover
Sep 3, 2026
035354e
feat: braille + image logo on startup (Sixel tiers, OSC11 probe)
Sep 3, 2026
21f1c54
feat: fleet simplified to role layer over subagents
Sep 3, 2026
cc02ee0
feat: rip out Pod, Fleet-only (compat shims deleted)
Sep 3, 2026
3ad2671
merge: hover contract slice (G)
Sep 3, 2026
611db94
merge: workbar rename slice (A)
Sep 3, 2026
3b92b5b
merge: fleet menu regroup slice (E)
Sep 3, 2026
fa0ce0a
merge: theme picker slice (C)
Sep 3, 2026
fe75cf9
fix: drop stray merge marker in views/mod.rs
Sep 3, 2026
a5edc34
merge: provider screen slice (D)
Sep 3, 2026
d8c31a1
Consolidate tiny TUI unit tests into scenario tests (testcut slice)
Sep 3, 2026
6462596
merge: startup screen slice (B)
Sep 3, 2026
0dc26af
merge: logo slice (F)
Sep 3, 2026
b536e4f
merge: fleet roles simplification
Sep 3, 2026
30802ab
merge: underwater-retro theme
Sep 3, 2026
1ccf22d
merge: test consolidation (-324 tests)
Sep 3, 2026
45d9b4e
fix: resolve committed B×F markers in tideline_tests, keep both tests
Sep 3, 2026
d3e11b5
style: cargo fmt on merged tree
Sep 3, 2026
4a10a8d
fix: cut unwired hover kinds and dead hover primitives
Sep 3, 2026
75f5854
fix: rewrite kind registry test for surviving kinds
Sep 3, 2026
5b836ec
fix: integrate retro theme into picker counts, band hover, hitboxes
Sep 3, 2026
67a122d
fix: route Moved to choice hover-preview when editing
Sep 3, 2026
76b3c0c
test: re-bless joint B+F+E+C+retro goldens, fix retro index fallout
Sep 3, 2026
1454f89
fix: clippy clean on merged tree
Sep 3, 2026
09f102d
fix: stopship release_lead resolves to advisor in role-only world
Sep 3, 2026
0f90457
fix: drop pod-alias pins from web fleet surface test + vocabulary
Sep 3, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions config.example.toml
Original file line number Diff line number Diff line change
Expand Up @@ -126,15 +126,15 @@ check_interval_hours = 24
# ─────────────────────────────────────────────────────────────────────────────────
# Hotbar slots (#2061 / #2064)
# ─────────────────────────────────────────────────────────────────────────────────
# Optional 1-8 sidebar hotbar bindings. Since #3807 a missing `hotbar` key
# Optional 1-8 workbar hotbar bindings. Since #3807 a missing `hotbar` key
# renders NO bar — the Hotbar is hidden until you configure [[hotbar]]
# tables here (or via `/hotbar`). An explicit `hotbar = []` also disables it.
#
# Invalid slots are skipped with a warning, duplicate slots use the last entry,
# and unknown actions are preserved so the UI can show a disabled entry.
# Slash commands can be bound as slash.<name>, for example slash.workflow.
# `/hotbar on` writes the default slots: slash.workflow, slash.goal, slash.auto,
# then Plan/Work/Operate, palette, and sidebar. Commands that require arguments
# then Plan/Work/Operate, palette, and workbar toggle. Commands that require arguments
# pre-fill the composer instead of running incomplete.
#
# [[hotbar]]
Expand Down
100 changes: 48 additions & 52 deletions crates/cli/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ struct Cli {
long,
value_name = "PROVIDER",
value_parser = parse_provider_identifier,
help = "Provider selector; exec/pod also accept configured custom provider identifiers"
help = "Provider selector; exec/fleet also accept configured custom provider identifiers"
)]
provider: Option<String>,
#[arg(long)]
Expand Down Expand Up @@ -203,25 +203,17 @@ path used by stream-json wrappers.
")]
Exec(TuiPassthroughArgs),
/// Manage durable Agent fleet runs.
///
/// `fleet` is the canonical spelling. `codewhale pod` remains accepted as
/// a compatibility alias for the identical command: the durable ledger,
/// receipts, config tables, and `--fleet` workflow flag keep the Fleet
/// serialization name.
#[command(
name = "fleet",
alias = "pod",
after_help = "\
Examples:
codewhale fleet init
codewhale fleet run tasks.json --max-workers 4
codewhale fleet status

`codewhale pod` is a compatibility alias for this command and dispatches
identically, as `/pod` does for the `/fleet` slash command. What keeps the
Fleet name is everything that has to stay readable across versions: the
durable ledger `.codewhale/fleet.jsonl`, saved rosters `fleets/<name>.toml`,
the `[fleet]` and `[fleets.*]` config tables, and `workflow run --fleet`."
The durable ledger `.codewhale/fleet.jsonl`, saved rosters `fleets/<name>.toml`,
the `[fleet]` and `[fleets.*]` config tables, and `workflow run --fleet` keep
the Fleet name across versions."
)]
Fleet(TuiPassthroughArgs),
/// Internal model-free Workflow tool dispatcher used by Lane Runtime.
Expand Down Expand Up @@ -496,7 +488,7 @@ fn top_level_provider_override(

let expected = ProviderKind::names_hint();
bail!(
"invalid value '{provider}' for '--provider <PROVIDER>': expected one of {expected}; configured custom providers are accepted only by exec and pod"
"invalid value '{provider}' for '--provider <PROVIDER>': expected one of {expected}; configured custom providers are accepted only by exec and fleet"
)
}

Expand All @@ -517,8 +509,8 @@ fn prepare_raw_provider_tui_dispatch(
reject_exec_global_flags(&args.args)?;
tui_args("exec", args.clone())
}
Some(Commands::Fleet(args)) => tui_args("pod", args.clone()),
_ => unreachable!("raw provider validation only permits Exec and Pod"),
Some(Commands::Fleet(args)) => tui_args("fleet", args.clone()),
_ => unreachable!("raw provider validation only permits Exec and Fleet"),
};

// Dynamic provider config belongs to the TUI schema. Do not parse it
Expand Down Expand Up @@ -660,7 +652,7 @@ enum LaneCommand {
/// Workflow name (e.g. `stopship`).
#[arg(long)]
workflow: Option<String>,
/// Pod roster name (e.g. `stopship`); the flag keeps its compatibility spelling.
/// Fleet roster name (e.g. `stopship`); the flag keeps its compatibility spelling.
#[arg(long)]
fleet: Option<String>,
/// Issue id binding.
Expand Down Expand Up @@ -703,7 +695,7 @@ enum WorkflowCommand {
Run {
/// Workflow name or path. `stopship` maps to workflows/stopship.workflow.js.
workflow: String,
/// Named Pod roster (e.g. stopship). The flag keeps its compatibility
/// Named Fleet roster (e.g. stopship). The flag keeps its compatibility
/// spelling. Without one, roles resolve against the built-in roster
/// and the session route.
#[arg(long)]
Expand Down Expand Up @@ -1076,12 +1068,14 @@ fn run_workflow_command(
// loaded and validated before the run starts.
if let Some(name) = fleet.as_deref() {
let roots = named_fleet_search_roots(&workspace);
let loaded = codewhale_workflow::load_named_fleet(name, &roots)
.with_context(|| format!("load Pod `{name}` from {}", display_roots(&roots)))?;
let loaded =
codewhale_workflow::load_named_fleet(name, &roots).with_context(|| {
format!("load Fleet `{name}` from {}", display_roots(&roots))
})?;
if workflow == "stopship" || name == "stopship" {
loaded
.validate_stopship_roles()
.with_context(|| format!("validate stopship roles in Pod `{name}`"))?;
.with_context(|| format!("validate stopship roles in Fleet `{name}`"))?;
}
}

Expand Down Expand Up @@ -1957,7 +1951,7 @@ fn run() -> Result<()> {
}
Some(Commands::Fleet(args)) => {
let resolved_runtime = resolve_runtime_for_dispatch(&mut store, &runtime_overrides);
run_tui_in_process(&cli, &resolved_runtime, tui_args("pod", args))
run_tui_in_process(&cli, &resolved_runtime, tui_args("fleet", args))
}
Some(Commands::WorkflowTool(args)) => {
let resolved_runtime = resolve_runtime_for_dispatch(&mut store, &runtime_overrides);
Expand Down Expand Up @@ -6516,46 +6510,50 @@ verbosity = "project-imported"
));
}

/// Fleet is the canonical customer-facing top-level command; `pod` is a
/// compatibility alias that must keep dispatching to the same code path.
/// The Fleet spelling was always the one in the durable ledger, saved
/// roster files, config tables, and the `workflow --fleet` flag.
/// Fleet is the only top-level spelling for durable runs. The retired
/// `pod` spelling must fail to parse instead of dispatching.
#[test]
fn fleet_is_the_canonical_top_level_command_and_pod_stays_a_compatibility_alias() {
fn fleet_is_the_only_top_level_command_and_pod_is_rejected() {
for tail in [
vec!["init"],
vec!["status"],
vec!["run", "tasks.json", "--max-workers", "2"],
] {
let pod = parse_ok(
let fleet = parse_ok(
&std::iter::once("codewhale")
.chain(["pod"])
.chain(["fleet"])
.chain(tail.iter().copied())
.collect::<Vec<_>>(),
);
let fleet = parse_ok(
let Some(Commands::Fleet(fleet_args)) = &fleet.command else {
panic!("fleet must parse into the fleet command: {tail:?}");
};
assert_eq!(fleet_args.args, tail, "{tail:?}");
assert!(fleet.prompt.is_empty(), "{tail:?}");

let retired = parse_ok(
&std::iter::once("codewhale")
.chain(["fleet"])
.chain(["pod"])
.chain(tail.iter().copied())
.collect::<Vec<_>>(),
);
let (Some(Commands::Fleet(pod_args)), Some(Commands::Fleet(fleet_args))) =
(&pod.command, &fleet.command)
else {
panic!("both spellings must parse into the same command: {tail:?}");
};
assert_eq!(pod_args.args, tail, "{tail:?}");
assert_eq!(pod_args.args, fleet_args.args, "{tail:?}");
assert!(pod.prompt.is_empty() && fleet.prompt.is_empty(), "{tail:?}");
assert!(
retired.command.is_none(),
"retired pod must not dispatch to any command: {tail:?}"
);
assert_eq!(
retired.prompt.first().map(String::as_str),
Some("pod"),
"retired pod words fall through to prompt text: {tail:?}"
);
}

// Help advertises fleet. The alias still resolves, but discovery has one
// canonical answer, so `pod` must not be listed as its own command.
// Help advertises fleet only.
let help = help_for(&["codewhale", "--help"]);
let commands = help
.lines()
.map(str::trim_start)
.filter(|line| line.starts_with("pod") || line.starts_with("fleet"))
.filter(|line| line.starts_with("fleet"))
.collect::<Vec<_>>();
assert_eq!(
commands.len(),
Expand All @@ -6568,29 +6566,28 @@ verbosity = "project-imported"
"help summary should name fleet: {commands:?}"
);
assert!(
!help.contains("Manage durable Agent Pod runs"),
"the retired Pod-led summary must be gone from top-level help"
!help.contains("Manage durable Agent Fleet runs"),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[WARNING] CLI test assertion is ineffective due to case mismatch

In crates/cli/src/lib.rs, the test fleet_is_the_only_top_level_command_and_pod_is_rejected asserts !help.contains("Manage durable Agent Fleet runs"). The actual command summary in the doc comment is 'Manage durable Agent fleet runs' (lowercase 'f'). The assertion therefore always passes, even if the summary is present in top-level help. The intent is unclear because Fleet is not retired; the test comment says 'the retired Fleet-led summary must be gone', but Fleet remains the command. This does not guard against regressions where the old Pod text is reintroduced.

"the retired Fleet-led summary must be gone from top-level help"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove the ineffective assertion or correct the string to match the actual command summary ('Manage durable Agent fleet runs') if the intent is to ensure that text is absent from top-level help. Since Fleet is a valid subcommand, that line likely should not be in top-level help in the first place; consider dropping the assertion entirely.

);

let fleet_help = help_for(&["codewhale", "fleet", "--help"]);
assert!(fleet_help.contains("Manage durable Agent fleet runs"));
assert!(fleet_help.contains("codewhale fleet run tasks.json --max-workers 4"));
assert!(fleet_help.contains("codewhale pod` is a compatibility alias"));

// Both spellings normalize to the canonical inner command so receipts
// and any echoed invocation never regress to the compatibility name.
// The inner command token matches the canonical name so receipts
// and any echoed invocation never regress to the retired name.
let args = TuiPassthroughArgs {
args: vec!["status".into()],
};
assert_eq!(
tui_args("pod", args.clone()),
vec!["pod".to_string(), "status".to_string()]
tui_args("fleet", args.clone()),
vec!["fleet".to_string(), "status".to_string()]
);
assert!(command_accepts_raw_provider(Some(&Commands::Fleet(args))));
}

#[test]
fn exec_and_pod_accept_builtin_and_raw_provider_identifiers() {
fn exec_and_fleet_accept_builtin_and_raw_provider_identifiers() {
let builtin = parse_ok(&["codewhale", "--provider", "openrouter", "exec", "Reply OK"]);
assert_eq!(builtin.provider.as_deref(), Some("openrouter"));
assert_eq!(
Expand All @@ -6612,7 +6609,6 @@ verbosity = "project-imported"

for (provider, command) in [
("lm-studio", vec!["exec", "Reply OK"]),
("lm-studio", vec!["pod", "status"]),
("lm-studio", vec!["fleet", "status"]),
] {
let argv = std::iter::once("codewhale")
Expand Down Expand Up @@ -6696,13 +6692,13 @@ verbosity = "project-imported"
}

#[test]
fn raw_provider_ids_remain_restricted_to_exec_and_pod() {
fn raw_provider_ids_remain_restricted_to_exec_and_fleet() {
let cli = parse_ok(&["codewhale", "--provider", "lm-studio", "model", "list"]);
let err = top_level_provider_override(cli.provider.as_deref(), cli.command.as_ref())
.expect_err("model registry commands still require a built-in provider");
assert!(
err.to_string()
.contains("configured custom providers are accepted only by exec and pod")
.contains("configured custom providers are accepted only by exec and fleet")
);

let err = Cli::try_parse_from(["codewhale", "auth", "set", "--provider", "lm-studio"])
Expand Down
4 changes: 2 additions & 2 deletions crates/config/src/app_mode.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ pub enum AppMode {
impl AppMode {
/// Productive keyboard cycle: Plan -> Act -> Operate -> Plan.
///
/// Operate joins the visible cycle as the always-on pod operation:
/// Operate joins the visible cycle as the always-on fleet operation:
/// a lead plans slices, then workers execute against an optional burn rate.
pub const CYCLE: [Self; 3] = [Self::Plan, Self::Agent, Self::Operate];

Expand Down Expand Up @@ -101,7 +101,7 @@ impl AppMode {
AppMode::Agent => "Act mode - direct work in the current session with tools",
AppMode::Plan => "Plan mode - research and design before implementing",
AppMode::Operate => {
"Operate mode - always-on pod operation: lead plans, optional $/time burn rate, workers follow the plan"
"Operate mode - always-on fleet operation: lead plans, optional $/time burn rate, workers follow the plan"
}
}
}
Expand Down
16 changes: 1 addition & 15 deletions crates/config/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1324,20 +1324,6 @@ pub const DEFAULT_HOTBAR_ACTIONS: [&str; HOTBAR_SLOT_COUNT as usize] = [
"sidebar.toggle",
];

/// Normalize persisted action ids at the compatibility boundary.
///
/// `/pod` is the canonical public command, but existing settings may still
/// contain the former `slash.pod` hotbar id. Resolution and direct registry
/// lookup both use this helper so those slots continue to dispatch while any
/// subsequent save naturally writes the canonical id.
#[must_use]
pub fn normalize_hotbar_action_id(action_id: &str) -> &str {
match action_id {
"slash.pod" => "slash.fleet",
other => other,
}
}

/// On-disk schema for one `[[hotbar]]` table.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(deny_unknown_fields)]
Expand Down Expand Up @@ -1447,7 +1433,7 @@ pub fn resolve_hotbar_bindings(
.iter()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[WARNING] Removal of hotbar action normalization breaks existing slash.pod bindings

The function normalize_hotbar_action_id was removed and resolve_hotbar_bindings now clones the action directly. Users with saved hotbar slots containing slash.pod will have those slots treated as unknown actions without migration. This is a deliberate breaking change, but it is likely to silently disable functionality for existing users. A migration or a load-time warning would be safer.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider keeping a lightweight migration or adding a warning when a legacy slash.pod action is encountered during hotbar resolution, so users are not silently left with broken bindings.

.map(|binding| HotbarBinding {
slot: binding.slot,
action: normalize_hotbar_action_id(&binding.action).to_string(),
action: binding.action.clone(),
label: binding.label.clone(),
})
.collect::<Vec<_>>(),
Expand Down
Loading
Loading