Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
ab1a2c3
test(todo): cover the new-task panel reset end to end; fix case-sensi…
1jehuang Aug 1, 2026
c8c923c
test(provider): cover custom config profile routing end to end (refs …
1jehuang Aug 1, 2026
02f4afc
chore: rebaseline size ratchets for triage test additions (refs #694,…
1jehuang Aug 1, 2026
00f2929
test: cover the remote key path and both model-route sources (refs #6…
1jehuang Aug 1, 2026
0fb1f5f
test(todo): cover the stale ungrouped goal the panel renders (refs #695)
1jehuang Aug 1, 2026
6c822fa
fix(tui): stop treating a just-touched notice screen as deep idle
1jehuang Aug 1, 2026
2795505
Soften Ctrl+L to scroll-to-bottom; add Cmd+L equivalent
1jehuang Aug 1, 2026
375539f
Record measured Discovery trigger baselines
1jehuang Aug 1, 2026
1f9dbe5
Make discover_tools state its trigger points and withhold setup until…
1jehuang Aug 1, 2026
3644737
Add credit-free end-to-end check of the Discovery browse-to-select ha…
1jehuang Aug 1, 2026
1b04ddc
Document the Discovery trigger fixes and what remains unverified
1jehuang Aug 1, 2026
8a6e3de
Record flash-lite Discovery baseline and a half-cost path to finish t…
1jehuang Aug 2, 2026
fe059d5
Preserve the pre-change Discovery baseline in the repo
1jehuang Aug 2, 2026
843b820
Point the Discovery benchmark at the models that matter
1jehuang Aug 2, 2026
8e36dba
Make Ctrl+L a true terminal-style clear (blank screen, history above)
1jehuang Aug 2, 2026
45d9232
Preserve the Claude pre-change Discovery baseline
1jehuang Aug 2, 2026
9aa0823
Reframe Discovery findings around the Claude baseline
1jehuang Aug 2, 2026
b14cf1c
test: cover Cmd+L terminal-style clear directly
1jehuang Aug 2, 2026
dea9c8e
Fix a package-install false positive and record skill preemption
1jehuang Aug 2, 2026
8b9e6a1
Record the matched Claude before/after: browse recall 83% to 100%, by…
1jehuang Aug 2, 2026
2f82e91
Ctrl+L: collapse cleared screen so the prompt sits at the top
1jehuang Aug 2, 2026
6a1e0a9
fix(openai): untyped MCP properties no longer break the tool catalog …
1jehuang Aug 2, 2026
8035aa4
fix(todo): use per-todo continuation messages in run auto-poke gates
1jehuang Aug 2, 2026
53599b4
chore: sync Cargo.lock with jcode-provider-core dependency
1jehuang Aug 2, 2026
d847d62
chore(release): v0.65.0
1jehuang Aug 2, 2026
32ddfbc
tui: apply keybinding config edits on the next keystroke
1jehuang Aug 2, 2026
bdf64e7
tui: make Up/Down move by visual wrapped row in the composer
1jehuang Aug 2, 2026
95872e0
fix(auth): stop reporting antigravity/gemini as expired when refresh …
1jehuang Aug 2, 2026
0469546
fix: never wedge a session with duplicate tool_results
1jehuang Aug 2, 2026
e29f500
prompt: allow user to replace base system prompt via .jcode/system-pr…
1jehuang Aug 2, 2026
8f554a1
test(anthropic): opt-in check that a real transcript formats to a val…
1jehuang Aug 2, 2026
fef879c
discovery: detect and measure off-catalog selects
1jehuang Aug 2, 2026
7b0b6e2
config: report what a config.toml edit changed and whether it is live
1jehuang Aug 2, 2026
122f388
test(config): drive the change report through the real write tool
1jehuang Aug 2, 2026
c827fce
config: report config.toml edits made through patch tools too
1jehuang Aug 2, 2026
65e2fa8
test(config): pin the restart-required section list
1jehuang Aug 2, 2026
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
3 changes: 2 additions & 1 deletion Cargo.lock

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

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "jcode"
version = "0.64.2"
version = "0.65.0"
description = "Possibly the greatest coding agent ever built — blazing-fast TUI, multi-model, swarm coordination, 30+ tools"
edition = "2024"
autobins = false
Expand Down
6 changes: 5 additions & 1 deletion changelog/index.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
{
"entries": [
{
"version": "0.65.0",
"date": "2026-08-02"
},
{
"version": "0.64.2",
"date": "2026-07-30"
Expand Down Expand Up @@ -149,4 +153,4 @@
"date": "2026-07-02"
}
]
}
}
34 changes: 34 additions & 0 deletions changelog/v0.65.0.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
{
"version": "0.65.0",
"date": "2026-08-02",
"title": "Seamless self-update, calmer TUI",
"highlights": [
"Self-update is now seamless: a live progress bar during download and a graceful in-place reload when it finishes",
"Todos you are working on stay pinned in a band at the top of the viewport while you scroll",
"Ctrl+L is a true terminal-style clear: the screen blanks, history stays above, and the prompt sits at the top"
],
"improvements": [
"New display.external_sessions setting hides other CLIs' sessions from the session picker",
"The swarm gallery and snapshots now show which provider and auth route each agent is using",
"Markdown tables honour column alignment in every renderer, and copying a table gives you clean text",
"Inline math stays inline in image mode and blends with the surrounding prose colour",
"The discover_tools browse card is now a single compact line, and the sponsored-discovery notice is gone",
"Tool descriptions and parameter docs are capped, leaving more of the context window for your work",
"Ollama reports the context window it is actually serving instead of the trained window",
"Crash-resume hints now point at the session picker"
],
"fixes": [
"Custom OpenAI-compatible profile models route to their own profile instead of Copilot",
"OpenAI tool catalogs no longer break on MCP tools with untyped or unsupported schema properties",
"Ctrl+D forward-deletes mid-edit instead of quitting",
"Copying on X11 uses xclip/xsel so the selection survives after jcode exits",
"One bad value in [display] no longer discards the rest of config.toml",
"Reasoning text with multi-byte characters no longer crashes the TUI",
"Cursor provider uses the correct regional agent host instead of hardcoding global",
"Anthropic reasoning effort chosen with Ctrl+O in /model now persists",
"launch_hotkeys enabled = false is honoured on macOS",
"The API key login prompt visibly responds and tells you when a key already exists",
"Desktop no longer shows a false 'update ready' banner on dev builds",
"Builds on musl and Termux/aarch64 targets are fixed"
]
}
15 changes: 13 additions & 2 deletions crates/jcode-app-core/src/agent.rs
Original file line number Diff line number Diff line change
Expand Up @@ -803,9 +803,20 @@ impl Agent {
let mut missing_for_message = Vec::new();
for id in tool_uses {
self.tool_call_ids.insert(id.clone());
if !self.tool_result_ids.contains(&id) {
missing_for_message.push(id);
if self.tool_result_ids.contains(&id) {
continue;
}
// A tool that is still executing is not an interrupted tool:
// its real result is on the way, and synthesizing a
// placeholder now produces a duplicate tool_result that
// Anthropic rejects outright. See `tool::inflight`.
if crate::tool::inflight::is_tool_in_flight(&id) {
logging::info(&format!(
"Skipping missing tool-output repair for {id}: tool is still executing"
));
continue;
}
missing_for_message.push(id);
}
if !missing_for_message.is_empty() {
missing_repairs.push((index, missing_for_message));
Expand Down
55 changes: 55 additions & 0 deletions crates/jcode-app-core/src/agent_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1583,3 +1583,58 @@ async fn stranded_tool_use_stop_continues_instead_of_ending_the_turn() {
"the recovered turn must deliver the model's real completion, got {text:?}"
);
}

/// Regression: the missing tool-output repair must not synthesize a
/// placeholder result for a tool call that is still executing. Doing so
/// produced a duplicate `tool_result` once the real output landed, and
/// Anthropic then rejected every later request for that session with
/// "unexpected `tool_use_id` found in `tool_result` blocks".
#[tokio::test]
async fn repair_skips_tool_calls_that_are_still_executing() {
let _guard = crate::storage::lock_test_env();
let provider: Arc<dyn Provider> = Arc::new(NativeAutoCompactionProvider);
let registry = Registry::new(provider.clone()).await;
let mut agent = Agent::new(provider, registry);

let tool_id = "toolu_still_running_regression";
agent.session.add_message(
Role::User,
vec![ContentBlock::Text {
text: "run it".to_string(),
cache_control: None,
}],
);
agent.session.add_message(
Role::Assistant,
vec![ContentBlock::ToolUse {
id: tool_id.to_string(),
name: "bash".to_string(),
input: serde_json::json!({"command": "sleep 300"}),
thought_signature: None,
}],
);

let in_flight = crate::tool::inflight::mark_tool_in_flight(tool_id).expect("guard");
assert_eq!(
agent.repair_missing_tool_outputs(),
0,
"a running tool must not be repaired"
);
assert!(
!agent.session.messages.iter().any(|m| m
.content
.iter()
.any(|b| matches!(b, ContentBlock::ToolResult { .. }))),
"no synthetic tool_result may be inserted while the tool runs"
);

// Once the tool finishes without delivering a result (a genuine
// interruption), repair is allowed to step in.
drop(in_flight);
agent.reset_tool_output_tracking();
assert_eq!(
agent.repair_missing_tool_outputs(),
1,
"a finished-but-unanswered tool must still be repaired"
);
}
9 changes: 8 additions & 1 deletion crates/jcode-app-core/src/tool/apply_patch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,11 @@ impl Tool for ApplyPatchTool {
let params: ApplyPatchInput = serde_json::from_value(input)?;
let hunks = parse_apply_patch(&params.patch_text)?;

// A patch can reach config.toml through any hunk kind (add, update,
// move), so watch the file across the whole invocation rather than
// threading before/after content through each branch.
let config_watch = super::config_edit_notice::ConfigEditWatch::begin();

let mut results = Vec::new();
let mut touched_paths = Vec::new();

Expand Down Expand Up @@ -236,7 +241,9 @@ impl Tool for ApplyPatchTool {
if results.is_empty() {
Ok(ToolOutput::new("No changes applied"))
} else {
let output = ToolOutput::new(results.join("\n"));
let mut body = results.join("\n");
config_watch.finish(&mut body);
let output = ToolOutput::new(body);
if touched_paths.len() == 1 {
Ok(output.with_title(touched_paths[0].clone()))
} else {
Expand Down
118 changes: 118 additions & 0 deletions crates/jcode-app-core/src/tool/config_edit_notice.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
//! Tell the agent (and through it, the user) what a config.toml edit did.
//!
//! When a user asks jcode to change a setting, the agent writes
//! `~/.jcode/config.toml` and then has to guess whether the change took
//! effect. That guess is where the confusion comes from. Instead, every file
//! write that lands on the active config file appends an explicit report:
//! which keys changed, and whether each one is live in running sessions or
//! needs a restart.

use std::path::Path;

/// Resolve a path for comparison, falling back to the path as given.
///
/// A config file that does not exist yet cannot be canonicalized, and that is
/// a normal case here (the very first write creates it), so the unresolved
/// path is the correct answer rather than an error to report.
fn comparable(path: &Path) -> std::path::PathBuf {
match std::fs::canonicalize(path) {
Ok(resolved) => resolved,
Err(_) => path.to_path_buf(),
}
}

/// Whether `path` is the config file the running process actually reads.
///
/// Compares resolved paths so `~/.jcode/config.toml`, a relative path, and a
/// symlinked jcode home all resolve to the same file.
fn is_active_config_file(path: &Path) -> bool {
let Some(config_path) = crate::config::Config::path() else {
return false;
};
comparable(path) == comparable(&config_path)
}

/// Report appended to a tool result after a write to the active config file.
///
/// Returns `None` for non-config files and for edits that changed no settings
/// (comments or formatting), so ordinary writes stay untouched.
pub fn config_edit_notice(path: &Path, before: &str, after: &str) -> Option<String> {
if !is_active_config_file(path) {
return None;
}
// Force the next config() call to re-read instead of waiting out the
// staleness throttle, so "live now" is true the moment it is claimed.
crate::config::Config::invalidate_cache();

// A config file that no longer parses is silently ignored by
// `Config::load`, which falls back to defaults. That is the worst possible
// outcome to leave unreported: the write "succeeded" while every setting
// in the file quietly stopped applying. Surface it instead.
if let Err(error) = crate::config::Config::load_strict() {
return Some(format!(
"\n\nWARNING: {} no longer parses as TOML, so jcode is falling back to \
default settings and every setting in this file is being ignored. \
Fix the syntax error: {error}",
path.display()
));
}

let summary = crate::config::change_report::summarize_toml_change(before, after)?;
Some(format!("\n\n{summary}"))
}

/// Append [`config_edit_notice`] to a tool output body when applicable.
pub fn append_config_edit_notice(body: &mut String, path: &Path, before: &str, after: &str) {
if let Some(notice) = config_edit_notice(path, before, after) {
body.push_str(&notice);
}
}

/// Read the config file, treating "absent or unreadable" as empty.
///
/// An absent config file is the normal pre-state for the write that creates
/// it, and an unreadable one is reported by the change summary itself, so
/// there is no error here worth propagating: empty is the meaningful value.
fn read_config_text(path: &Path) -> String {
std::fs::read_to_string(path).unwrap_or_default()
}

/// Watches the active config file across a whole tool invocation.
///
/// Tools that touch several files, or write through several code paths (patch
/// application, moves, deletes), cannot easily thread before/after content to
/// the place that builds the result string. This captures the config file
/// content up front and re-reads it at the end, so a config edit is reported
/// no matter which path produced it.
pub struct ConfigEditWatch {
path: Option<std::path::PathBuf>,
before: String,
}

impl ConfigEditWatch {
/// Snapshot the active config file before a tool runs.
pub fn begin() -> Self {
let path = crate::config::Config::path();
let before = match path.as_deref() {
Some(path) => read_config_text(path),
None => String::new(),
};
Self { path, before }
}

/// Append a change report if the config file changed while the tool ran.
pub fn finish(self, body: &mut String) {
let Some(path) = self.path else {
return;
};
let after = read_config_text(&path);
if after == self.before {
return;
}
append_config_edit_notice(body, &path, &self.before, &after);
}
}

#[cfg(test)]
#[path = "config_edit_notice_tests.rs"]
mod tests;
Loading
Loading