Skip to content
Open
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 README.md
Original file line number Diff line number Diff line change
Expand Up @@ -568,6 +568,22 @@ jcode dictate
jcode supports interactive TUI use, non-interactive runs, persistent server/client workflows,
and hotkey-friendly dictation without requiring a bundled speech-to-text stack.

### Context File Control

Skip loading project `AGENTS.md` and global `~/.AGENTS.md` context files for a session:

```bash
# CLI flag (preferred)
jcode --no-context-files

# Or via environment variable
JCODE_NO_CONTEXT_FILES=1 jcode

# Both work identically; the CLI flag sets the env var internally
```

This is useful when you want to test with a clean context or run sessions without project instructions.

<div align="center">

<a href="https://github.com/1jehuang/jcode/releases/download/readme-assets/workflow.mp4">
Expand Down
3 changes: 3 additions & 0 deletions src/cli/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,9 @@ pub(crate) struct Args {
/// Suppress non-error CLI/status output for scripting and wrappers
#[arg(long, global = true)]
pub(crate) quiet: bool,
/// Skip loading AGENTS.md project/global context files for this session
#[arg(long, global = true)]
pub(crate) no_context_files: bool,

/// Resume a session by ID, or list sessions if no ID provided
#[arg(long, global = true, num_args = 0..=1, default_missing_value = "")]
Expand Down
4 changes: 4 additions & 0 deletions src/cli/startup.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,10 @@ fn parse_and_prepare_args() -> Result<Args> {
crate::env::set_var("JCODE_TRACE", "1");
}

if args.no_context_files {
crate::env::set_var("JCODE_NO_CONTEXT_FILES", "1");
}

if let Some(ref socket) = args.socket {
server::set_socket_path(socket);
}
Expand Down
9 changes: 9 additions & 0 deletions src/prompt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@
use std::path::{Path, PathBuf};
use std::process::Command;

/// Whether context files should be skipped (set by --no-context-files or JCODE_NO_CONTEXT_FILES env var).
pub fn context_files_disabled() -> bool {
std::env::var("JCODE_NO_CONTEXT_FILES").is_ok()
}

/// Default system prompt for jcode (embedded at compile time)
pub const DEFAULT_SYSTEM_PROMPT: &str = include_str!("prompt/system_prompt.md");
const SELFDEV_HINT_PROMPT: &str = include_str!("prompt/selfdev_hint.txt");
Expand Down Expand Up @@ -601,6 +606,10 @@ fn gpu_summary() -> Option<String> {

/// Load AGENTS.md files from a specific working directory
pub fn load_agents_md_files_from_dir(working_dir: Option<&Path>) -> (Option<String>, ContextInfo) {
if context_files_disabled() {
eprintln!("Context files disabled (--no-context-files)");
return (None, ContextInfo::default());
}
let mut contents = vec![];
let mut info = ContextInfo::default();

Expand Down
97 changes: 97 additions & 0 deletions src/prompt_tests.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
use super::*;
use crate::cli::args::Args;
use clap::Parser;

/// Verify the default system prompt does NOT identify as "Claude Code"
/// It's fine to say "powered by Claude" but not "Claude Code" (Anthropic's product)
Expand Down Expand Up @@ -200,3 +202,98 @@ fn split_prompt_estimated_tokens_is_positive_when_populated() {
assert!(split.chars() > 0);
assert!(split.estimated_tokens() > 0);
}

#[test]
fn test_context_files_disabled_returns_false_by_default() {
let _guard = crate::storage::lock_test_env();
// Ensure the env var is NOT set
crate::env::remove_var("JCODE_NO_CONTEXT_FILES");
assert!(!context_files_disabled());
}

#[test]
fn test_context_files_disabled_returns_true_when_env_set() {
let _guard = crate::storage::lock_test_env();
crate::env::set_var("JCODE_NO_CONTEXT_FILES", "1");
assert!(context_files_disabled());
}

#[test]
fn test_load_agents_md_from_dir_returns_none_when_disabled() {
let _guard = crate::storage::lock_test_env();
let prev_home = std::env::var_os("JCODE_HOME");
let temp = tempfile::TempDir::new().unwrap();
crate::env::set_var("JCODE_HOME", temp.path());
crate::env::set_var("JCODE_NO_CONTEXT_FILES", "1");

// Even with a global AGENTS.md present, loading should be skipped
std::fs::create_dir_all(temp.path()).unwrap();
std::fs::write(temp.path().join("AGENTS.md"), "global agents instructions").unwrap();

let (content, _info) = load_agents_md_files_from_dir(None);
assert!(content.is_none());

if let Some(prev_home) = prev_home {
crate::env::set_var("JCODE_HOME", prev_home);
} else {
crate::env::remove_var("JCODE_HOME");
}
crate::env::remove_var("JCODE_NO_CONTEXT_FILES");
}

#[test]
fn test_load_agents_md_from_dir_loads_files_when_not_disabled() {
let _guard = crate::storage::lock_test_env();
let prev_home = std::env::var_os("JCODE_HOME");
let temp = tempfile::TempDir::new().unwrap();
crate::env::set_var("JCODE_HOME", temp.path());

// Remove any leftover env var
crate::env::remove_var("JCODE_NO_CONTEXT_FILES");

std::fs::create_dir_all(temp.path().join("external")).unwrap();
std::fs::write(
temp.path().join("external/AGENTS.md"),
"global agents instructions",
)
.unwrap();

let (content, info) = load_agents_md_files_from_dir(None);
assert!(info.has_global_agents_md);
assert!(
content
.as_ref()
.map(|c| c.contains("global agents instructions"))
.unwrap_or(false)
);

if let Some(prev_home) = prev_home {
crate::env::set_var("JCODE_HOME", prev_home);
} else {
crate::env::remove_var("JCODE_HOME");
}
}

#[test]
fn test_cli_flag_no_short_alias() {
// Verify that -c is NOT a valid alias for --no-context-files
let result = Args::try_parse_from(&["jcode", "-c", "--provider", "openai"]);
assert!(
result.is_err(),
"-c should not be a valid short flag for --no-context-files"
);
}

#[test]
fn test_cli_flag_no_context_files_parsed() {
let args = Args::parse_from(&["jcode", "--no-context-files"]);
assert!(args.no_context_files);

// Without the flag, should be false
let args2 = Args::parse_from(&["jcode"]);
assert!(!args2.no_context_files);

// With subcommand
let args3 = Args::parse_from(&["jcode", "--no-context-files", "run", "hello"]);
assert!(args3.no_context_files);
}