-
Notifications
You must be signed in to change notification settings - Fork 3.5k
Codewhale 0.9.12: Fleet-only UX (workbar, startup, underwater default, provider, settings, logo, hover, roles, retro theme) #5862
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
7b13b57
091f133
01ec884
ac5fcae
2742abf
1621360
08d653f
41d9d39
1a3e35b
4888eda
bfcb23a
8560081
fb9dbd7
2b54b22
9a29726
6f2e4c9
ed144de
9c859bf
9b1440b
f14ed0a
ca4da67
c891bd3
bc4c0bd
b8ab165
607b65e
c1ffd20
f53cacd
035354e
21f1c54
cc02ee0
3ad2671
611db94
3b92b5b
fa0ce0a
fe75cf9
a5edc34
d8c31a1
6462596
0dc26af
b536e4f
30802ab
1ccf22d
45d9b4e
d3e11b5
4a10a8d
75f5854
5b836ec
67a122d
76b3c0c
1454f89
09f102d
0f90457
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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)] | ||
|
|
@@ -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. | ||
|
|
@@ -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" | ||
| ) | ||
| } | ||
|
|
||
|
|
@@ -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 | ||
|
|
@@ -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. | ||
|
|
@@ -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)] | ||
|
|
@@ -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}`"))?; | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -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); | ||
|
|
@@ -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(), | ||
|
|
@@ -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"), | ||
| "the retired Fleet-led summary must be gone from top-level help" | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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!( | ||
|
|
@@ -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") | ||
|
|
@@ -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"]) | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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)] | ||
|
|
@@ -1447,7 +1433,7 @@ pub fn resolve_hotbar_bindings( | |
| .iter() | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [WARNING] Removal of hotbar action normalization breaks existing The function There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Consider keeping a lightweight migration or adding a warning when a legacy |
||
| .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<_>>(), | ||
|
|
||
There was a problem hiding this comment.
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 testfleet_is_the_only_top_level_command_and_pod_is_rejectedasserts!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.