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
16 changes: 16 additions & 0 deletions .cspell/words.txt
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ atuin
cachain
cachix
catppuccin
CDPATH
chdir
claudemd
Clawd
clippy
Expand All @@ -19,6 +21,7 @@ codex
CREAT
creds
crossterm
CTYPE
DECRC
DECSC
dedupe
Expand All @@ -38,10 +41,15 @@ envrc
ESRCH
fcntl
feff
fpats
fprint
fprintf
frappe
getpwuid
Ghostty
gitui
GOARCH
GOEXPERIMENT
hakula
idna
impls
Expand All @@ -54,10 +62,12 @@ killpg
Kobalte
Konsole
latte
LSCOLORS
macchiato
misparse
misparses
mkfifo
mksh
MMIX
mplan
mpsc
Expand All @@ -77,15 +87,20 @@ nonewline
noprefix
nucleo
numtide
okdir
OLDPWD
oxalica
onig
pgid
PKCE
pkgs
pname
popd
println
Ptmux
pulldown
pushd
PYTHONDONTWRITEBYTECODE
RAII
ratatui
rects
Expand All @@ -109,6 +124,7 @@ syntect
sysname
SYSPROMPT
taplo
tcsh
tengu
Tera
termio
Expand Down
39 changes: 39 additions & 0 deletions Cargo.lock

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

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,8 @@ tracing-subscriber = { version = "0.3", default-features = false, features = [
"env-filter",
"fmt",
] }
tree-sitter = "0.27"
tree-sitter-bash = "0.25"
unicode-normalization = "0.1"
unicode-segmentation = "1"
unicode-width = "0.2"
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ Early development. What works today:
- Agent loop with extended thinking and tool-use round-trip
- Claude Opus 5.5, Sonnet 5.5, and Haiku 5.5, each with a 1M-token context window and adjustable effort
- File and search tools: `read`, `write`, `edit`, `glob`, `grep`, `bash`
- Tool permissions: `auto` (default), `plan`, and `yolo` modes, allow / deny rules, protected config paths whose edits need approval in `auto` mode even when an allow rule matches, and an approval prompt for gated calls in the TUI and REPL that can allow matching calls for the rest of the session or deny with feedback for the model
- Tool permissions: `auto` (default), `plan`, and `yolo` modes, allow / deny rules that match each simple command of a parsed `bash` command line, read-only commands that run without approval, protected config paths whose edits need approval in `auto` mode even when an allow rule matches, and an approval prompt for gated calls in the TUI and REPL that can allow matching calls for the rest of the session or deny with feedback for the model
- Turn interruption (Esc / Ctrl+C) plus mid-turn queued follow-up prompts that splice into the same turn between tool calls, with double-press Ctrl+C exit confirmation
- Slash commands with `/`-triggered autocomplete (name and curated-arg modes): `/archive`, `/clear`, `/compact`, `/config`, `/delete`, `/diff`, `/effort`, `/help`, `/init`, `/login`, `/logout`, `/model`, `/rename`, `/resume`, `/status`, `/theme`, `/unarchive`
- Context compression with manual `/compact` and default-on auto-compaction
Expand Down
2 changes: 2 additions & 0 deletions crates/oxide-code/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ toml.workspace = true
tracing.workspace = true
tracing-appender.workspace = true
tracing-subscriber.workspace = true
tree-sitter.workspace = true
tree-sitter-bash.workspace = true
unicode-normalization.workspace = true
unicode-segmentation.workspace = true
unicode-width.workspace = true
Expand Down
2 changes: 1 addition & 1 deletion crates/oxide-code/src/driver/headless.rs
Original file line number Diff line number Diff line change
Expand Up @@ -301,7 +301,7 @@ mod tests {
for &format in OutputFormat::value_variants() {
let server = MockServer::start().await;
mount_tool_call_once(&server, "call-1", "look", "{}").await;
mount_tool_call_once(&server, "call-2", "bash", r#"{"command":"ls"}"#).await;
mount_tool_call_once(&server, "call-2", "bash", r#"{"command":"git push"}"#).await;
mount_reply(&server, "Done.").await;
let dir = tempfile::tempdir().unwrap();
let tools = ToolRegistry::new(vec![Box::new(LookTool), Box::new(BashTool)]);
Expand Down
15 changes: 9 additions & 6 deletions crates/oxide-code/src/driver/repl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,8 @@ fn shown(text: &str) -> String {

#[cfg(test)]
mod tests {
use std::path::Path;

use indoc::indoc;
use wiremock::matchers::{method, path};
use wiremock::{Mock, MockServer, ResponseTemplate};
Expand All @@ -265,7 +267,7 @@ mod tests {
Policy::default()
.session_grant(
PermissionClass::Bash,
&GateTarget::Command(command.to_owned()),
&GateTarget::for_command(command, Path::new("/repo")),
)
.unwrap()
}
Expand Down Expand Up @@ -373,7 +375,7 @@ mod tests {
async fn drive_answers_an_approval_from_the_next_input_line() {
with_pinned_home(async {
let server = MockServer::start().await;
mount_tool_call_once(&server, "call-1", "bash", r#"{"command":"ls"}"#).await;
mount_tool_call_once(&server, "call-1", "bash", r#"{"command":"git push"}"#).await;
mount_reply(&server, "Done.").await;
let dir = tempfile::tempdir().unwrap();
let engine = engine(server.uri(), &test_store(dir.path()), dir.path())
Expand Down Expand Up @@ -410,7 +412,7 @@ mod tests {
async fn drive_without_a_terminal_denies_approvals_and_keeps_every_line_a_prompt() {
with_pinned_home(async {
let server = MockServer::start().await;
mount_tool_call_once(&server, "call-1", "bash", r#"{"command":"ls"}"#).await;
mount_tool_call_once(&server, "call-1", "bash", r#"{"command":"git push"}"#).await;
mount_reply(&server, "Done.").await;
let dir = tempfile::tempdir().unwrap();
let engine = engine(server.uri(), &test_store(dir.path()), dir.path())
Expand Down Expand Up @@ -489,16 +491,17 @@ mod tests {
f

maybe
run the tests
n
"};
let (decision, out) = decide(input, None).await;

assert_eq!(decision, Decision::Deny);
assert_eq!(out.matches(CHOICES).count(), 4, "{out}");
assert_eq!(out.matches(CHOICES).count(), 5, "{out}");
assert_eq!(
out.matches("Answer with one of the letters").count(),
2,
"{out}"
3,
"a prompt typed ahead re-asks: {out}"
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,5 @@ expression: "stderrs[0]"
⟡ Look
README.md

$ ls
The bash call was denied with this feedback: This run cannot ask for approval. To allow this call, add `"bash(ls)"` to the `allow` list in the `[permission]` table of the user config.
$ git push
The bash call was denied with this feedback: This run cannot ask for approval. To allow this call, add `"bash(git push)"` to the `allow` list in the `[permission]` table of the user config.
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ expression: stdout
---
{"type":"tool_call_start","data":{"id":"call-1","name":"look","input":{},"icon":"⟡","label":"Look"}}
{"type":"tool_call_end","data":{"id":"call-1","content":"README.md","is_error":false,"title":null,"view":{"type":"text","data":{"content":"README.md"}}}}
{"type":"tool_call_start","data":{"id":"call-2","name":"bash","input":{"command":"ls"},"icon":"$","label":"ls"}}
{"type":"tool_call_end","data":{"id":"call-2","content":"The bash call was denied with this feedback: This run cannot ask for approval. To allow this call, add `\"bash(ls)\"` to the `allow` list in the `[permission]` table of the user config.","is_error":true,"title":null,"view":{"type":"text","data":{"content":"The bash call was denied with this feedback: This run cannot ask for approval. To allow this call, add `\"bash(ls)\"` to the `allow` list in the `[permission]` table of the user config."}}}}
{"type":"tool_call_start","data":{"id":"call-2","name":"bash","input":{"command":"git push"},"icon":"$","label":"git push"}}
{"type":"tool_call_end","data":{"id":"call-2","content":"The bash call was denied with this feedback: This run cannot ask for approval. To allow this call, add `\"bash(git push)\"` to the `allow` list in the `[permission]` table of the user config.","is_error":true,"title":null,"view":{"type":"text","data":{"content":"The bash call was denied with this feedback: This run cannot ask for approval. To allow this call, add `\"bash(git push)\"` to the `allow` list in the `[permission]` table of the user config."}}}}
{"type":"stream_token","data":"Done."}
{"type":"usage_updated","data":{"context_tokens":1000,"context_window":1000000,"estimated_cost_usd":0.018000000000000002}}
{"type":"turn_complete"}
Expand Down
4 changes: 3 additions & 1 deletion crates/oxide-code/src/driver/stdio.rs
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,8 @@ impl EventSink for StdioSink {

#[cfg(test)]
mod tests {
use std::path::Path;

use indoc::indoc;

use super::*;
Expand Down Expand Up @@ -241,7 +243,7 @@ mod tests {
Policy::default()
.session_grant(
PermissionClass::Bash,
&GateTarget::Command(command.to_owned()),
&GateTarget::for_command(command, Path::new("/repo")),
)
.unwrap()
}
Expand Down
Loading
Loading