Explorar o código

merge: gaebal/cli-dispatch-top5 into main

Yeachan-Heo hai 2 meses
pai
achega
9cad6d2de3
Modificáronse 2 ficheiros con 212 adicións e 5 borrados
  1. 38 0
      .github/workflows/rust-ci.yml
  2. 174 5
      rust/crates/rusty-claude-cli/src/app.rs

+ 38 - 0
.github/workflows/rust-ci.yml

@@ -0,0 +1,38 @@
+name: rust-ci
+
+on:
+  push:
+    branches:
+      - main
+      - 'gaebal/**'
+      - 'omx-issue-*'
+  pull_request:
+
+jobs:
+  rust-ci:
+    runs-on: ubuntu-latest
+    defaults:
+      run:
+        working-directory: rust
+    steps:
+      - name: Checkout
+        uses: actions/checkout@v4
+
+      - name: Install Rust toolchain
+        uses: dtolnay/rust-toolchain@stable
+        with:
+          components: rustfmt, clippy
+
+      - name: Cache cargo
+        uses: Swatinem/rust-cache@v2
+        with:
+          workspaces: rust
+
+      - name: cargo fmt
+        run: cargo fmt --all --check
+
+      - name: cargo clippy
+        run: cargo clippy -p rusty-claude-cli --bin claw --no-deps -- -D warnings
+
+      - name: cargo test
+        run: cargo test -p rusty-claude-cli

+ 174 - 5
rust/crates/rusty-claude-cli/src/app.rs

@@ -44,6 +44,11 @@ pub enum SlashCommand {
     Help,
     Status,
     Compact,
+    Model { model: Option<String> },
+    Permissions { mode: Option<String> },
+    Config { section: Option<String> },
+    Memory,
+    Clear { confirm: bool },
     Unknown(String),
 }
 
@@ -55,15 +60,25 @@ impl SlashCommand {
             return None;
         }
 
-        let command = trimmed
-            .trim_start_matches('/')
-            .split_whitespace()
-            .next()
-            .unwrap_or_default();
+        let mut parts = trimmed.trim_start_matches('/').split_whitespace();
+        let command = parts.next().unwrap_or_default();
         Some(match command {
             "help" => Self::Help,
             "status" => Self::Status,
             "compact" => Self::Compact,
+            "model" => Self::Model {
+                model: parts.next().map(ToOwned::to_owned),
+            },
+            "permissions" => Self::Permissions {
+                mode: parts.next().map(ToOwned::to_owned),
+            },
+            "config" => Self::Config {
+                section: parts.next().map(ToOwned::to_owned),
+            },
+            "memory" => Self::Memory,
+            "clear" => Self::Clear {
+                confirm: parts.next() == Some("--confirm"),
+            },
             other => Self::Unknown(other.to_string()),
         })
     }
@@ -87,6 +102,26 @@ const SLASH_COMMAND_HANDLERS: &[SlashCommandHandler] = &[
         command: SlashCommand::Compact,
         summary: "Compact local session history",
     },
+    SlashCommandHandler {
+        command: SlashCommand::Model { model: None },
+        summary: "Show or switch the active model",
+    },
+    SlashCommandHandler {
+        command: SlashCommand::Permissions { mode: None },
+        summary: "Show or switch the active permission mode",
+    },
+    SlashCommandHandler {
+        command: SlashCommand::Config { section: None },
+        summary: "Inspect current config path or section",
+    },
+    SlashCommandHandler {
+        command: SlashCommand::Memory,
+        summary: "Inspect loaded memory/instruction files",
+    },
+    SlashCommandHandler {
+        command: SlashCommand::Clear { confirm: false },
+        summary: "Start a fresh local session",
+    },
 ];
 
 pub struct CliApp {
@@ -158,6 +193,11 @@ impl CliApp {
             SlashCommand::Help => Self::handle_help(out),
             SlashCommand::Status => self.handle_status(out),
             SlashCommand::Compact => self.handle_compact(out),
+            SlashCommand::Model { model } => self.handle_model(model.as_deref(), out),
+            SlashCommand::Permissions { mode } => self.handle_permissions(mode.as_deref(), out),
+            SlashCommand::Config { section } => self.handle_config(section.as_deref(), out),
+            SlashCommand::Memory => self.handle_memory(out),
+            SlashCommand::Clear { confirm } => self.handle_clear(confirm, out),
             SlashCommand::Unknown(name) => {
                 writeln!(out, "Unknown slash command: /{name}")?;
                 Ok(CommandResult::Continue)
@@ -172,6 +212,11 @@ impl CliApp {
                 SlashCommand::Help => "/help",
                 SlashCommand::Status => "/status",
                 SlashCommand::Compact => "/compact",
+                SlashCommand::Model { .. } => "/model [model]",
+                SlashCommand::Permissions { .. } => "/permissions [mode]",
+                SlashCommand::Config { .. } => "/config [section]",
+                SlashCommand::Memory => "/memory",
+                SlashCommand::Clear { .. } => "/clear [--confirm]",
                 SlashCommand::Unknown(_) => continue,
             };
             writeln!(out, "  {name:<9} {}", handler.summary)?;
@@ -209,6 +254,102 @@ impl CliApp {
         Ok(CommandResult::Continue)
     }
 
+    fn handle_model(
+        &mut self,
+        model: Option<&str>,
+        out: &mut impl Write,
+    ) -> io::Result<CommandResult> {
+        match model {
+            Some(model) => {
+                self.config.model = model.to_string();
+                self.state.last_model = model.to_string();
+                writeln!(out, "Active model set to {model}")?;
+            }
+            None => {
+                writeln!(out, "Active model: {}", self.config.model)?;
+            }
+        }
+        Ok(CommandResult::Continue)
+    }
+
+    fn handle_permissions(
+        &mut self,
+        mode: Option<&str>,
+        out: &mut impl Write,
+    ) -> io::Result<CommandResult> {
+        match mode {
+            None => writeln!(out, "Permission mode: {:?}", self.config.permission_mode)?,
+            Some("read-only") => {
+                self.config.permission_mode = PermissionMode::ReadOnly;
+                writeln!(out, "Permission mode set to read-only")?;
+            }
+            Some("workspace-write") => {
+                self.config.permission_mode = PermissionMode::WorkspaceWrite;
+                writeln!(out, "Permission mode set to workspace-write")?;
+            }
+            Some("danger-full-access") => {
+                self.config.permission_mode = PermissionMode::DangerFullAccess;
+                writeln!(out, "Permission mode set to danger-full-access")?;
+            }
+            Some(other) => {
+                writeln!(out, "Unknown permission mode: {other}")?;
+            }
+        }
+        Ok(CommandResult::Continue)
+    }
+
+    fn handle_config(
+        &mut self,
+        section: Option<&str>,
+        out: &mut impl Write,
+    ) -> io::Result<CommandResult> {
+        match section {
+            None => writeln!(
+                out,
+                "Config path: {}",
+                self.config
+                    .config
+                    .as_ref()
+                    .map_or_else(|| String::from("<none>"), |path| path.display().to_string())
+            )?,
+            Some(section) => writeln!(
+                out,
+                "Config section `{section}` is not fully implemented yet; current config path is {}",
+                self.config
+                    .config
+                    .as_ref()
+                    .map_or_else(|| String::from("<none>"), |path| path.display().to_string())
+            )?,
+        }
+        Ok(CommandResult::Continue)
+    }
+
+    fn handle_memory(&mut self, out: &mut impl Write) -> io::Result<CommandResult> {
+        writeln!(
+            out,
+            "Loaded memory/config file: {}",
+            self.config
+                .config
+                .as_ref()
+                .map_or_else(|| String::from("<none>"), |path| path.display().to_string())
+        )?;
+        Ok(CommandResult::Continue)
+    }
+
+    fn handle_clear(&mut self, confirm: bool, out: &mut impl Write) -> io::Result<CommandResult> {
+        if !confirm {
+            writeln!(out, "Refusing to clear without confirmation. Re-run as /clear --confirm")?;
+            return Ok(CommandResult::Continue);
+        }
+
+        self.state.turns = 0;
+        self.state.compacted_messages = 0;
+        self.state.last_usage = UsageSummary::default();
+        self.conversation_history.clear();
+        writeln!(out, "Started a fresh local session.")?;
+        Ok(CommandResult::Continue)
+    }
+
     fn handle_stream_event(
         renderer: &TerminalRenderer,
         event: StreamEvent,
@@ -369,6 +510,29 @@ mod tests {
             SlashCommand::parse("/compact now"),
             Some(SlashCommand::Compact)
         );
+        assert_eq!(
+            SlashCommand::parse("/model claude-sonnet"),
+            Some(SlashCommand::Model {
+                model: Some("claude-sonnet".into()),
+            })
+        );
+        assert_eq!(
+            SlashCommand::parse("/permissions workspace-write"),
+            Some(SlashCommand::Permissions {
+                mode: Some("workspace-write".into()),
+            })
+        );
+        assert_eq!(
+            SlashCommand::parse("/config hooks"),
+            Some(SlashCommand::Config {
+                section: Some("hooks".into()),
+            })
+        );
+        assert_eq!(SlashCommand::parse("/memory"), Some(SlashCommand::Memory));
+        assert_eq!(
+            SlashCommand::parse("/clear --confirm"),
+            Some(SlashCommand::Clear { confirm: true })
+        );
     }
 
     #[test]
@@ -380,6 +544,11 @@ mod tests {
         assert!(output.contains("/help"));
         assert!(output.contains("/status"));
         assert!(output.contains("/compact"));
+        assert!(output.contains("/model [model]"));
+        assert!(output.contains("/permissions [mode]"));
+        assert!(output.contains("/config [section]"));
+        assert!(output.contains("/memory"));
+        assert!(output.contains("/clear [--confirm]"));
     }
 
     #[test]