Skip to content

Commit 71e5683

Browse files
authored
Merge pull request #453 from quangdang46/sync-upstream-20260622
Sync upstream master (v0.31.2) — 118 commits with extracted module preservation
2 parents e485923 + 8612ea1 commit 71e5683

2,506 files changed

Lines changed: 26397 additions & 16897 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitignore

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,17 @@ ios_simulator_screenshot.png
2020
session-ses_*.md
2121
target-ttest/
2222
target-tttest*
23+
target-tttest*/
2324
libjcode_base.rlib
2425
.DS_Store
2526
.fastembed_cache/
2627

2728
# bv (beads viewer) local config and caches
2829
.bv/
30+
31+
# Stray experiment/debug artifacts at the repo root. Real assets live under
32+
# assets/, docs/, ios/, and tests/ and are committed explicitly.
33+
/*.log
34+
/*.avif
35+
/*.mp4
36+
/*.png

Cargo.toml

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "jcode"
3-
version = "0.29.0"
3+
version = "0.31.2"
44
description = "Possibly the greatest coding agent ever built — blazing-fast TUI, multi-model, swarm coordination, 30+ tools"
55
edition = "2024"
66
autobins = false
@@ -81,8 +81,6 @@ members = [
8181
"crates/jcode-terminal-image",
8282
"crates/jcode-telemetry-core",
8383
"crates/jcode-tui-workspace",
84-
"crates/jcode-mobile-core",
85-
"crates/jcode-mobile-sim",
8684
"crates/jcode-best-of-n",
8785
"crates/jcode-desktop",
8886
"crates/jcode-hooks",
@@ -317,6 +315,7 @@ jcode-experiment-flags = { path = "crates/jcode-experiment-flags" }
317315
flate2 = "1"
318316
tar = "0.4"
319317
tempfile = "3"
318+
agentgrep = { git = "https://github.com/1jehuang/agentgrep.git", tag = "v0.1.3" }
320319
qrcode = { version = "0.14.1", default-features = false }
321320
aws-config = "1.8.16"
322321
aws-credential-types = "1.2.14"
@@ -360,6 +359,38 @@ windows-sys = { version = "0.59", features = ["Win32_Foundation", "Win32_System_
360359

361360
[target.'cfg(target_os = "macos")'.dependencies]
362361
global-hotkey = "0.7"
362+
objc2 = "0.6"
363+
block2 = "0.6"
364+
objc2-foundation = { version = "0.3", features = [
365+
"NSString",
366+
"NSThread",
367+
"NSRunLoop",
368+
"NSDate",
369+
"NSTimer",
370+
"NSUserDefaults",
371+
"NSValue",
372+
"NSAttributedString",
373+
"NSDictionary",
374+
"block2",
375+
] }
376+
objc2-app-kit = { version = "0.3", features = [
377+
"NSApplication",
378+
"NSResponder",
379+
"NSStatusBar",
380+
"NSStatusItem",
381+
"NSStatusBarButton",
382+
"NSButton",
383+
"NSCell",
384+
"NSColor",
385+
"NSControl",
386+
"NSFont",
387+
"NSFontDescriptor",
388+
"NSImage",
389+
"NSAttributedString",
390+
"NSMenu",
391+
"NSMenuItem",
392+
"NSRunningApplication",
393+
] }
363394

364395
[profile.release]
365396
opt-level = 1

README.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -849,7 +849,6 @@ mempalace) so the base build stays lean.
849849
- [Memory Architecture](docs/MEMORY_ARCHITECTURE.md)
850850
- [Swarm Architecture](docs/SWARM_ARCHITECTURE.md)
851851
- [Server Architecture](docs/SERVER_ARCHITECTURE.md)
852-
- [iOS Client Notes](docs/IOS_CLIENT.md)
853852
- [Safety System](docs/SAFETY_SYSTEM.md)
854853
- [Windows Notes](docs/WINDOWS.md)
855854
- [Wrappers and Shell Integration](docs/WRAPPERS.md)

codemagic.yaml

Lines changed: 0 additions & 66 deletions
This file was deleted.

crates/jcode-app-core/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,7 @@ flate2 = "1"
144144
tar = "0.4"
145145
tempfile = "3"
146146
hashline = { git = "https://github.com/quangdang46/hashline.git", rev = "1780f4b81333047323058041d3d0e64abce0c9ad", }
147+
agentgrep = { git = "https://github.com/1jehuang/agentgrep.git", tag = "v0.1.3" }
147148
qrcode = { version = "0.14.1", default-features = false }
148149
# DCP integration (dynamic context pruning)
149150
dynamic_context_pruning = { git = "https://github.com/quangdang46/dynamic_context_pruning", branch = "main", package = "dynamic_context_pruning", optional = true }

crates/jcode-app-core/src/agent/turn_execution.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1033,8 +1033,8 @@ impl Agent {
10331033
transcript.push('\n');
10341034
}
10351035

1036-
if !crate::memory::memory_sidecar_enabled() {
1037-
logging::info("Memory extraction skipped: memory sidecar disabled");
1036+
if !crate::memory::memory_llm_judge_available() {
1037+
logging::info("Memory extraction skipped: LLM judge unavailable");
10381038
return 0;
10391039
}
10401040

crates/jcode-app-core/src/agent/turn_loops.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ impl Agent {
1010
pub(super) async fn run_turn(&mut self, print_output: bool) -> Result<String> {
1111
self.set_log_context();
1212
crate::session_metrics::record_turn(&self.session.id);
13+
// Mark this session as actively streaming for presence UIs (e.g. the
14+
// macOS menu bar indicator). Cleared automatically on every exit path.
15+
let _streaming_guard = crate::session::StreamingGuard::new(self.session.id.clone());
1316
let mut final_text = String::new();
1417
let trace = trace_enabled();
1518
let mut context_limit_retries = 0u32;

crates/jcode-app-core/src/agent/turn_streaming_mpsc.rs

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,9 @@ impl Agent {
8181
event_tx: mpsc::UnboundedSender<ServerEvent>,
8282
) -> Result<()> {
8383
self.set_log_context();
84+
// Mark this session as actively streaming for presence UIs (e.g. the
85+
// macOS menu bar indicator). Cleared automatically on every exit path.
86+
let _streaming_guard = crate::session::StreamingGuard::new(self.session.id.clone());
8487
let trace = trace_enabled();
8588
let mut context_limit_retries = 0u32;
8689
let mut incomplete_continuations = 0u32;
@@ -190,6 +193,14 @@ impl Agent {
190193
&messages_with_memory
191194
};
192195
let provider = Arc::clone(&self.provider);
196+
// Capture the model id the request was issued with. A provider may
197+
// transparently switch models mid-request (e.g. Anthropic's retired
198+
// `claude-fable-5` falls back to `claude-opus-4-8`). When that
199+
// happens the provider mutates its own model state, but the session
200+
// and clients still believe they are on the originally requested
201+
// model. Compare against this after the stream so we can emit a
202+
// `ModelChanged` and resync the UI/context-limit.
203+
let model_at_request_start = provider.model().to_string();
193204
let resume_session_id = self.provider_session_id.clone();
194205
self.last_status_detail = None;
195206
let _ = event_tx.send(kv_cache_request_event(
@@ -897,6 +908,34 @@ impl Agent {
897908
cache_creation_input_tokens: usage_cache_creation,
898909
};
899910

911+
// Detect a transparent mid-request model switch (e.g. Anthropic's
912+
// retired `claude-fable-5` falling back to `claude-opus-4-8`). The
913+
// provider mutates its own model state during the stream, so the
914+
// session and clients would otherwise keep showing the originally
915+
// requested model with a stale context-limit. Resync the session and
916+
// notify clients with a `ModelChanged` so the header, picker, and
917+
// context budget all reflect the model that actually served.
918+
let model_after_stream = self.provider.model();
919+
if model_after_stream != model_at_request_start {
920+
let provider_name = self.provider.display_name();
921+
logging::warn(&format!(
922+
"Provider switched model mid-request: '{}' -> '{}' (resyncing session/UI)",
923+
model_at_request_start, model_after_stream
924+
));
925+
self.session.model = Some(model_after_stream.clone());
926+
self.provider_runtime_state
927+
.apply(crate::provider::ProviderStateEvent::RuntimeModelObserved {
928+
model: model_after_stream.clone(),
929+
});
930+
self.persist_session_best_effort("model fallback");
931+
let _ = event_tx.send(ServerEvent::ModelChanged {
932+
id: 0,
933+
model: model_after_stream,
934+
provider_name: Some(provider_name),
935+
error: None,
936+
});
937+
}
938+
900939
let had_tool_calls_before = !tool_calls.is_empty();
901940
self.recover_text_wrapped_tool_call(&mut text_content, &mut tool_calls);
902941

crates/jcode-app-core/src/agent_tests.rs

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,102 @@ async fn run_turn_streaming_mpsc_emits_keepalive_while_provider_is_quiet() {
216216
task.await.unwrap().unwrap();
217217
}
218218

219+
/// Provider that transparently switches its model mid-stream, mimicking the
220+
/// Anthropic retired-model fallback (`claude-fable-5` -> `claude-opus-4-8`).
221+
struct MidStreamModelSwitchProvider {
222+
model: std::sync::Mutex<String>,
223+
switch_to: String,
224+
}
225+
226+
#[async_trait]
227+
impl Provider for MidStreamModelSwitchProvider {
228+
async fn complete(
229+
&self,
230+
_messages: &[Message],
231+
_tools: &[ToolDefinition],
232+
_system: &str,
233+
_resume_session_id: Option<&str>,
234+
) -> Result<EventStream> {
235+
// Emulate the provider switching its own model state during the request.
236+
*self.model.lock().unwrap() = self.switch_to.clone();
237+
let (tx, rx) = tokio_mpsc::channel::<Result<StreamEvent>>(8);
238+
tokio::spawn(async move {
239+
let _ = tx
240+
.send(Ok(StreamEvent::TextDelta("hello".to_string())))
241+
.await;
242+
let _ = tx
243+
.send(Ok(StreamEvent::MessageEnd {
244+
stop_reason: Some("end_turn".to_string()),
245+
}))
246+
.await;
247+
});
248+
Ok(Box::pin(ReceiverStream::new(rx)))
249+
}
250+
251+
fn name(&self) -> &str {
252+
"claude"
253+
}
254+
255+
fn model(&self) -> String {
256+
self.model.lock().unwrap().clone()
257+
}
258+
259+
fn fork(&self) -> Arc<dyn Provider> {
260+
Arc::new(Self {
261+
model: std::sync::Mutex::new(self.model.lock().unwrap().clone()),
262+
switch_to: self.switch_to.clone(),
263+
})
264+
}
265+
}
266+
267+
#[tokio::test]
268+
async fn run_turn_streaming_mpsc_emits_model_changed_on_midstream_switch() {
269+
let _guard = crate::storage::lock_test_env();
270+
let provider: Arc<dyn Provider> = Arc::new(MidStreamModelSwitchProvider {
271+
model: std::sync::Mutex::new("claude-fable-5".to_string()),
272+
switch_to: "claude-opus-4-8".to_string(),
273+
});
274+
let registry = Registry::new(provider.clone()).await;
275+
let mut agent = Agent::new(provider, registry);
276+
agent.add_message(
277+
Role::User,
278+
vec![ContentBlock::Text {
279+
text: "test".to_string(),
280+
cache_control: None,
281+
}],
282+
);
283+
284+
let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel();
285+
let task = tokio::spawn(async move { agent.run_turn_streaming_mpsc(tx).await });
286+
287+
let mut switched_model = None;
288+
let deadline = Instant::now() + Duration::from_secs(20);
289+
while Instant::now() < deadline {
290+
match tokio::time::timeout(Duration::from_secs(1), rx.recv()).await {
291+
Ok(Some(ServerEvent::ModelChanged { model, error, .. })) => {
292+
assert!(error.is_none(), "unexpected model-change error: {error:?}");
293+
switched_model = Some(model);
294+
break;
295+
}
296+
Ok(Some(_)) => {}
297+
Ok(None) => break,
298+
Err(_) => {
299+
if task.is_finished() {
300+
break;
301+
}
302+
}
303+
}
304+
}
305+
306+
task.await.unwrap().unwrap();
307+
assert_eq!(
308+
switched_model.as_deref(),
309+
Some("claude-opus-4-8"),
310+
"expected a ModelChanged event resyncing to the served model"
311+
);
312+
}
313+
314+
219315
#[tokio::test]
220316
async fn messages_for_provider_replays_persisted_native_compaction_in_auto_mode() {
221317
let provider: Arc<dyn Provider> = Arc::new(NativeAutoCompactionProvider);

0 commit comments

Comments
 (0)