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
14 changes: 7 additions & 7 deletions _docs/config/opencode-compatibility.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -40,13 +40,13 @@ Blank cells mean that runtime behavior is not supported by that project today. `
| `notifications` | | ✅ | crabcode-specific sounds, desktop notifications, and terminal alert signals such as Zed tab dots. |
| `mcp` | ✅ | ✅ | Uses the OpenCode MCP config shape. Enabled servers are connected at runtime and their tools are exposed as crabcode tools. |
| `permission` | ✅ | ✅ | Global tool permission rules are enforced during AI SDK tool execution. |
| `instructions` | ✅ | | Accepted at the top level, but config-driven instruction files are not loaded yet. |
| `tools` | ✅ | | Accepted at the top level, not used as global tool config yet. |
| `compaction` | ✅ | | Accepted at the top level, not used as config yet. |
| `watcher` | ✅ | | Accepted at the top level, not used as config yet. |
| `formatter` | ✅ | | Accepted at the top level, not used as config yet. |
| `disabled_providers` | ✅ | | Accepted at the top level, not applied yet. |
| `enabled_providers` | ✅ | | Accepted at the top level, not applied yet. |
| `instructions` | ✅ | | Loads the listed files relative to the project root (or from absolute and `~/` paths) and appends their contents to the system prompt. Unreadable files produce a config warning. |
| `tools` | ✅ | | Global tool enable/disable map. Disabled tools are removed before agent-specific tool policies are applied. |
| `compaction` | ✅ | | Accepted and parsed (`false` or `{ "auto", "prune" }`), but compaction behavior is not applied yet. |
| `watcher` | ✅ | | Controls file suggestions in command autocomplete. Use `false` to disable them or `{ "ignore": ["path"] }` to exclude paths. |
| `formatter` | ✅ | | Accepted and parsed by file extension, but configured formatter commands are not run yet. |
| `disabled_providers` | ✅ | | Removes the listed provider IDs from model discovery and selection. |
| `enabled_providers` | ✅ | | Restricts model discovery and selection to the listed provider IDs. `disabled_providers` takes precedence when both are set. |
| `keybinds` | ✅ | ❌ | Ignored because crabcode does not use OpenCode keybind config. |
| `share` | ✅ | ❌ | Ignored. |
| `tui` | ✅ | ❌ | Ignored. crabcode owns its terminal UI. |
Expand Down
37 changes: 21 additions & 16 deletions src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -859,6 +859,7 @@ pub struct App {
pub websearch: crate::config::configuration::WebsearchConfig,
pub mcp: crate::config::configuration::McpConfig,
pub config_raw_merged: serde_json::Value,
custom_instructions: String,
terminal_focused: bool,
pub tool_permissions: crate::tools::ToolPermissions,
pub skills_dirs: Vec<std::path::PathBuf>,
Expand Down Expand Up @@ -1017,8 +1018,13 @@ impl App {
})
.collect();
input.autocomplete = Some(
AutoComplete::new_at(crate::autocomplete::CommandAuto::new(&registry), &cwd_path)
.with_agents(agent_suggestions),
AutoComplete::new_at_with_file_config(
crate::autocomplete::CommandAuto::new(&registry),
&cwd_path,
loaded_config.merged_config.watcher.is_enabled(),
loaded_config.merged_config.watcher.ignored_paths().to_vec(),
)
.with_agents(agent_suggestions),
);

if let Some(default_agent) = loaded_config.merged_config.default_agent.clone() {
Expand Down Expand Up @@ -1116,19 +1122,14 @@ impl App {

let chat_state = init_chat(chat, &agent, &colors);
let session_rename_dialog_state = init_session_rename_dialog(colors);
let mut agent_policies = crate::tools::AgentToolPolicies::default();
for (mode, tools) in agent_registry.tool_policy_map() {
agent_policies = agent_policies.with_custom_tools(mode.clone(), tools.clone());
}
let tool_permissions = crate::tools::ToolPermissions::new(cwd_path.clone())
.with_agent_policies(agent_policies)
.with_permission_rules(loaded_config.merged_config.permission_rules.clone())
.with_agent_permission_rules(agent_registry.permission_rules_map());

let discovery = crate::model::discovery::Discovery::new_with_custom(Some(
loaded_config.merged_config.custom_providers.clone(),
))
.ok();
let runtime = crate::config::ConfigRuntime::from_merged(
&loaded_config.merged_config,
cwd_path.clone(),
crate::config::ConfigRuntimeOptions::default(),
);
let tool_permissions = runtime.tool_permissions;
let discovery = runtime.discovery;
let custom_instructions = runtime.custom_instructions;
let now = std::time::Instant::now();

Ok(Self {
Expand Down Expand Up @@ -1204,6 +1205,7 @@ impl App {
websearch: loaded_config.merged_config.websearch,
mcp: mcp_config,
config_raw_merged: loaded_config.raw_merged,
custom_instructions,
terminal_focused: true,
tool_permissions,
skills_dirs: loaded_config.inventory.opencode_skills_dirs,
Expand Down Expand Up @@ -9406,6 +9408,7 @@ impl App {
let agent_registry = self.agent_registry.clone();
let websearch_config = self.websearch.clone();
let mcp_config = self.mcp.clone();
let custom_instructions = self.custom_instructions.clone();
let cwd = self.cwd.clone();
let is_git_repo = crate::utils::git::is_git_repo(&cwd).unwrap_or(false);

Expand Down Expand Up @@ -9451,7 +9454,8 @@ impl App {
)
.with_tool_registry(prompt_registry)
.with_agent_registry(agent_registry.clone())
.with_active_agent(agent_mode.clone());
.with_active_agent(agent_mode.clone())
.with_custom_instructions(custom_instructions);
let system_prompt = tokio::task::block_in_place(|| {
tokio::runtime::Handle::current().block_on(async { composer.compose().await })
});
Expand Down Expand Up @@ -10997,6 +11001,7 @@ mod tests {
websearch: crate::config::configuration::WebsearchConfig::default(),
mcp: crate::config::configuration::McpConfig::default(),
config_raw_merged: serde_json::json!({}),
custom_instructions: String::new(),
terminal_focused: true,
tool_permissions: crate::tools::ToolPermissions::new(".".to_string()),
skills_dirs: Vec::new(),
Expand Down
43 changes: 35 additions & 8 deletions src/autocomplete/file.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,14 @@ impl FileAuto {
}

pub fn new_at(root: impl Into<PathBuf>) -> Self {
Self::new_at_with_config(root, true, Vec::new())
}

pub fn new_at_with_config(
root: impl Into<PathBuf>,
watcher_enabled: bool,
ignored_paths: Vec<String>,
) -> Self {
let root = root.into();
let (refresh_tx, refresh_rx) = mpsc::sync_channel(1);
let inner = Arc::new(FileAutoInner {
Expand All @@ -67,10 +75,19 @@ impl FileAuto {

if thread::Builder::new()
.name("crabcode-file-index".to_string())
.spawn(move || run_indexer(root, weak_inner, refresh_tx, refresh_rx))
.spawn(move || {
run_indexer(
root,
weak_inner,
refresh_tx,
refresh_rx,
watcher_enabled,
ignored_paths,
)
})
.is_err()
{
publish_entries(&fallback_inner, collect_entries(&fallback_root));
publish_entries(&fallback_inner, collect_entries(&fallback_root, &[]));
}

Self { inner }
Expand Down Expand Up @@ -173,16 +190,20 @@ fn run_indexer(
inner: Weak<FileAutoInner>,
refresh_tx: SyncSender<()>,
refresh_rx: Receiver<()>,
watcher_enabled: bool,
ignored_paths: Vec<String>,
) {
let watcher = create_watcher(&root, refresh_tx);
let watcher = watcher_enabled
.then(|| create_watcher(&root, refresh_tx))
.flatten();
let safety_refresh_interval = if watcher.is_some() {
WATCHED_SAFETY_REFRESH_INTERVAL
} else {
UNWATCHED_REFRESH_INTERVAL
};
let mut last_refresh = Instant::now();

if !refresh_index(&root, &inner) {
if !refresh_index(&root, &inner, &ignored_paths) {
return;
}

Expand All @@ -202,7 +223,7 @@ fn run_indexer(
}

if refresh_requested || last_refresh.elapsed() >= safety_refresh_interval {
if !refresh_index(&root, &inner) {
if !refresh_index(&root, &inner, &ignored_paths) {
break;
}
last_refresh = Instant::now();
Expand Down Expand Up @@ -263,8 +284,8 @@ fn event_requires_refresh(event: &Event) -> bool {
})
}

fn refresh_index(root: &Path, inner: &Weak<FileAutoInner>) -> bool {
let entries = collect_entries(root);
fn refresh_index(root: &Path, inner: &Weak<FileAutoInner>, ignored_paths: &[String]) -> bool {
let entries = collect_entries(root, ignored_paths);
let Some(inner) = inner.upgrade() else {
return false;
};
Expand All @@ -282,7 +303,7 @@ fn publish_entries(inner: &FileAutoInner, entries: Vec<FileEntry>) {
inner.state_changed.notify_all();
}

fn collect_entries(root: &Path) -> Vec<FileEntry> {
fn collect_entries(root: &Path, ignored_paths: &[String]) -> Vec<FileEntry> {
let mut builder = WalkBuilder::new(root);
builder
.hidden(false)
Expand Down Expand Up @@ -310,6 +331,12 @@ fn collect_entries(root: &Path) -> Vec<FileEntry> {
if display.is_empty() {
return None;
}
if ignored_paths.iter().any(|pattern| {
let pattern = pattern.trim_end_matches('/');
display == pattern || display.starts_with(&format!("{pattern}/"))
}) {
return None;
}
if is_directory && !display.ends_with('/') {
display.push('/');
}
Expand Down
11 changes: 10 additions & 1 deletion src/autocomplete/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,18 @@ impl AutoComplete {
}

pub fn new_at(command_auto: CommandAuto, root: impl Into<std::path::PathBuf>) -> Self {
Self::new_at_with_file_config(command_auto, root, true, Vec::new())
}

pub fn new_at_with_file_config(
command_auto: CommandAuto,
root: impl Into<std::path::PathBuf>,
watcher_enabled: bool,
ignored_paths: Vec<String>,
) -> Self {
Self {
command_auto,
file_auto: FileAuto::new_at(root),
file_auto: FileAuto::new_at_with_config(root, watcher_enabled, ignored_paths),
agents: Vec::new(),
mode: AutoCompleteMode::Command,
}
Expand Down
Loading
Loading