Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
15 changes: 15 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -561,6 +561,15 @@ jobs:
continue-on-error: true
shell: bash
run: sccache --show-stats
- name: Check core navigation source and normal dependency boundary
if: needs.changes.outputs.heavy == 'true'
shell: bash
run: |
python3 -B scripts/test_check_command_core_navigation_proof.py
graph=$(mktemp)
trap 'rm -f "$graph"' EXIT
cargo tree --locked -p codewhale-portable-core-navigation --edges normal --all-features --target all --prefix none > "$graph"
python3 -B scripts/check-command-core-navigation-proof.py --graph "$graph"
- name: Check provider registry drift
if: needs.changes.outputs.heavy == 'true'
run: |
Expand Down Expand Up @@ -875,6 +884,12 @@ jobs:
run: |
python3 -B scripts/test_check_command_config_proof.py
sh scripts/check-portable-config.sh
- name: Check portable core navigation boundary
if: needs.changes.outputs.heavy == 'true' && matrix.os == 'ubuntu-latest' && (github.event_name == 'workflow_dispatch' || github.event_name == 'pull_request')
shell: bash
run: |
python3 -B scripts/test_check_command_core_navigation_proof.py
sh scripts/check-portable-core-navigation.sh
- uses: actions/setup-node@v7.1.0
# The extension-host integration tests spawn the real bundle under
# Node >= 22.19; CODEWHALE_EXT_HOST_TESTS below makes a missing Node a
Expand Down
7 changes: 7 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ members = [
"crates/workflow-js",
"tests/portable-session",
"tests/portable-config",
"tests/portable-core-navigation",
]
exclude = ["vendor/codewhale-ratatui"]
default-members = ["crates/cli"]
Expand Down
2 changes: 2 additions & 0 deletions crates/command-contract/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,8 @@ pub const EFFECT_REPLY_TEMPLATES: &[(&str, &[&str])] = &[
pub trait CommandConfigContext {
/// Path of the active user configuration file, when one is resolved.
fn config_path(&self) -> Option<PathBuf>;
fn translation_enabled(&self) -> bool;
fn set_translation_enabled(&mut self, enabled: bool);

/// `/config status|audit|editability|editable` — typed audit rows.
fn audit(&mut self) -> Result<CommandConfigAuditReport, String>;
Expand Down
10 changes: 9 additions & 1 deletion crates/command-contract/src/facets.rs
Original file line number Diff line number Diff line change
Expand Up @@ -107,9 +107,17 @@ pub trait CommandSkillsContext {
fn refresh_skill_cache(&mut self);
}

/// Workspace path and a bounded serialized work-state snapshot.
/// Workspace path, bounded work-state observations and confined anchor storage.
pub trait CommandWorkspaceContext {
fn workspace(&self) -> PathBuf;
fn effective_home_dir(&self) -> Option<PathBuf>;
fn inspect_workspace_target(&self, candidate: &Path) -> crate::navigation::WorkspaceTarget;
fn read_anchor_document(&self) -> Result<Option<String>, String>;
fn append_anchor_bytes(
&mut self,
parts: &[&[u8]],
) -> Result<PathBuf, crate::navigation::AnchorWriteError>;
fn replace_anchor_document(&mut self, bytes: &[u8]) -> Result<(), String>;
fn work_state_snapshot(&self) -> Result<Option<String>, String>;
/// Session-aware canonical operation digest. Returns the final user-facing
/// digest text or a safe explicit error; never a serialized snapshot.
Expand Down
34 changes: 33 additions & 1 deletion crates/command-contract/src/handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -95,12 +95,14 @@ impl CommandCapabilities {
/// general configuration and system settings.
pub const PET: Self = Self(1 << 26);
/// Workspace trust and trusted-path allow-list host data (FEAT-028 D3),
/// independent of the general config facet and of `WORKSPACE` path reads.
/// independent of the general config facet and of `WORKSPACE` operations.
pub const TRUST: Self = Self(1 << 27);
/// Account/provider session state for `/login` and `/logout` (FEAT-028 D3).
pub const LOGIN: Self = Self(1 << 28);
/// Workbar placement/panel state for `/workbar` (FEAT-028 D3).
pub const WORK_SURFACE: Self = Self(1 << 29);
pub const NAVIGATION: Self = Self(1 << 30);
pub const VOICE_INPUT: Self = Self(1 << 31);

/// Raw bit pattern, for tests that pin the capability-space capacity.
///
Expand Down Expand Up @@ -174,6 +176,8 @@ pub struct CommandContexts<'a> {
trust: Option<&'a mut dyn crate::config::CommandTrustContext>,
login: Option<&'a mut dyn crate::config::CommandLoginContext>,
work_surface: Option<&'a mut dyn crate::config::CommandWorkSurfaceContext>,
navigation: Option<&'a mut dyn crate::navigation::CommandNavigationContext>,
voice_input: Option<&'a mut dyn crate::navigation::CommandVoiceInputContext>,
}

/// Consumed envelope used when one handler needs several independent facets.
Expand Down Expand Up @@ -208,6 +212,8 @@ pub struct ContextParts<'a> {
pub trust: Option<&'a mut dyn crate::config::CommandTrustContext>,
pub login: Option<&'a mut dyn crate::config::CommandLoginContext>,
pub work_surface: Option<&'a mut dyn crate::config::CommandWorkSurfaceContext>,
pub navigation: Option<&'a mut dyn crate::navigation::CommandNavigationContext>,
pub voice_input: Option<&'a mut dyn crate::navigation::CommandVoiceInputContext>,
}

impl<'a> CommandContexts<'a> {
Expand Down Expand Up @@ -243,6 +249,8 @@ impl<'a> CommandContexts<'a> {
trust: None,
login: None,
work_surface: None,
navigation: None,
voice_input: None,
}
}

Expand Down Expand Up @@ -278,6 +286,8 @@ impl<'a> CommandContexts<'a> {
trust: self.trust,
login: self.login,
work_surface: self.work_surface,
navigation: self.navigation,
voice_input: self.voice_input,
}
}

Expand Down Expand Up @@ -321,6 +331,28 @@ impl<'a> CommandContexts<'a> {
self
}

pub fn with_navigation(
mut self,
value: &'a mut dyn crate::navigation::CommandNavigationContext,
) -> Self {
assert!(
self.navigation.replace(value).is_none(),
"navigation facet already set"
);
self
}

pub fn with_voice_input(
mut self,
value: &'a mut dyn crate::navigation::CommandVoiceInputContext,
) -> Self {
assert!(
self.voice_input.replace(value).is_none(),
"voice-input facet already set"
);
self
}

pub fn with_permissions(mut self, value: &'a mut dyn CommandPermissionsContext) -> Self {
assert!(
self.permissions.replace(value).is_none(),
Expand Down
1 change: 1 addition & 0 deletions crates/command-contract/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ pub mod elapsed;
pub mod facets;
pub mod handler;
pub mod metadata;
pub mod navigation;
pub mod outcome;
pub mod types;

Expand Down
105 changes: 105 additions & 0 deletions crates/command-contract/src/navigation.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
use std::path::PathBuf;

use crate::types::CommandMode;

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct NavigationUserHelp {
pub name: String,
pub description: Option<String>,
pub usage: Option<String>,
pub aliases: Vec<String>,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct NavigationBuiltinHelp {
pub name: String,
pub usage: String,
pub description: String,
pub aliases: Vec<String>,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct NavigationHelpTopic {
pub user: Option<NavigationUserHelp>,
pub builtin: Option<NavigationBuiltinHelp>,
pub shadowed_names: Vec<String>,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct NavigationSkill {
pub name: String,
pub description: String,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct NavigationOverview {
pub model: String,
pub mode: CommandMode,
pub mode_label: String,
pub workspace: PathBuf,
pub history_count: usize,
pub total_tokens: u32,
pub queued_count: usize,
pub subagent_count: usize,
pub active_skill: Option<String>,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum NavigationProviderKind {
Moonshot,
Other,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct NavigationProviderLinks {
pub id: String,
pub name: String,
pub credential_url: Option<String>,
pub docs_url: Option<String>,
pub guidance: String,
pub env_names: Vec<String>,
pub kind: NavigationProviderKind,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct NavigationLinks {
pub active_provider: String,
pub locale_tag: String,
pub providers: Vec<NavigationProviderLinks>,
pub kimi_route: String,
pub kimi_console: String,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum WorkspaceTarget {
Missing,
NotDirectory,
Directory { canonical_or_candidate: PathBuf },
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum AnchorWriteError {
Open(String),
Write(String),
}

pub trait CommandNavigationContext {
fn help_topic(&self, topic: &str) -> NavigationHelpTopic;
fn cached_skills(&self) -> Vec<NavigationSkill>;
fn help_is_open(&self) -> bool;
fn open_help(&mut self);
fn home_is_blocked(&self) -> bool;
fn restore_home(&mut self);
fn overview(&self) -> NavigationOverview;
fn links(&self) -> NavigationLinks;
}

pub trait CommandVoiceInputContext {
fn recorder_available(&self) -> bool;
fn voice_enabled(&self) -> bool;
fn set_voice_enabled(&mut self, enabled: bool);
fn send_enabled(&self) -> bool;
fn set_send_enabled(&mut self, enabled: bool);
fn control_enabled(&self) -> bool;
fn set_control_enabled(&mut self, enabled: bool);
}
14 changes: 14 additions & 0 deletions crates/command-contract/src/outcome.rs
Original file line number Diff line number Diff line change
Expand Up @@ -162,3 +162,17 @@ pub type ConfigCommandResult = CommandResult<ConfigAction>;
pub type ConfigPolicyAction = ConfigAction;
pub type ConfigPolicyCommandResult = ConfigCommandResult;
pub type ConfigStatusCommandResult = CommandResult<std::convert::Infallible>;

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum CoreNavigationAction {
OpenHotbarSetup,
DisableHotbar,
RestoreHotbarDefaults,
OpenLiveTranscript,
OpenTurnInspector,
OpenWorktreeManager,
SwitchWorkspace { workspace: std::path::PathBuf },
VoiceCapture,
}

pub type CoreNavigationCommandResult = CommandResult<CoreNavigationAction>;
42 changes: 42 additions & 0 deletions crates/command-contract/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,24 @@ impl CommandSkillsContext for Skills {

struct Workspace;
impl CommandWorkspaceContext for Workspace {
fn effective_home_dir(&self) -> Option<std::path::PathBuf> {
unreachable!("navigation is not used by this fixture")
}
fn inspect_workspace_target(&self, _: &std::path::Path) -> crate::navigation::WorkspaceTarget {
unreachable!("navigation is not used by this fixture")
}
fn read_anchor_document(&self) -> Result<Option<String>, String> {
unreachable!("anchors are not used by this fixture")
}
fn append_anchor_bytes(
&mut self,
_: &[&[u8]],
) -> Result<std::path::PathBuf, crate::navigation::AnchorWriteError> {
unreachable!("anchors are not used by this fixture")
}
fn replace_anchor_document(&mut self, _: &[u8]) -> Result<(), String> {
unreachable!("anchors are not used by this fixture")
}
fn workspace(&self) -> PathBuf {
PathBuf::from(".")
}
Expand Down Expand Up @@ -646,6 +664,24 @@ impl CommandMediaContext for Media {

struct DigestWorkspace;
impl CommandWorkspaceContext for DigestWorkspace {
fn effective_home_dir(&self) -> Option<std::path::PathBuf> {
unreachable!("navigation is not used by digest")
}
fn inspect_workspace_target(&self, _: &std::path::Path) -> crate::navigation::WorkspaceTarget {
unreachable!("navigation is not used by digest")
}
fn read_anchor_document(&self) -> Result<Option<String>, String> {
unreachable!("anchors are not used by digest")
}
fn append_anchor_bytes(
&mut self,
_: &[&[u8]],
) -> Result<std::path::PathBuf, crate::navigation::AnchorWriteError> {
unreachable!("anchors are not used by digest")
}
fn replace_anchor_document(&mut self, _: &[u8]) -> Result<(), String> {
unreachable!("anchors are not used by digest")
}
fn workspace(&self) -> PathBuf {
PathBuf::from(".")
}
Expand Down Expand Up @@ -3564,6 +3600,8 @@ fn structcopy_capability_preserves_all_published_identities() {
CommandCapabilities::TRUST,
CommandCapabilities::LOGIN,
CommandCapabilities::WORK_SURFACE,
CommandCapabilities::NAVIGATION,
CommandCapabilities::VOICE_INPUT,
];
let exact = CommandCapabilities::SESSION_STRUCTCOPY | CommandCapabilities::PRESENTATION;
for (index, capability) in capabilities.into_iter().enumerate() {
Expand Down Expand Up @@ -3628,6 +3666,8 @@ fn structcopy_slot_is_optional_and_does_not_grant_other_authority() {
trust,
login,
work_surface,
navigation,
voice_input,
} = CommandContexts::empty()
.with_structcopy(&mut fixture)
.with_presentation(&mut presentation)
Expand Down Expand Up @@ -3677,6 +3717,8 @@ fn structcopy_slot_is_optional_and_does_not_grant_other_authority() {
trust.is_some(),
login.is_some(),
work_surface.is_some(),
navigation.is_some(),
voice_input.is_some(),
] {
assert!(!present);
}
Expand Down
Loading
Loading