Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
ded2687
fix(chat): preserve native selection menu on touch long press
mrytsr Sep 4, 2026
64f8a35
feat(workspace): 工作区浏览器后退历史与离开守卫
Sep 5, 2026
32b45a1
feat: add model provider settings and source selection UI
mrytsr Sep 6, 2026
e7cfacc
feat: add file-backed model provider backend
mrytsr Sep 6, 2026
b58473f
feat: persist agent model source and switch provider settings to tran…
mrytsr Sep 6, 2026
3266afb
feat: persist conversation model provider selections
mrytsr Sep 6, 2026
3f00ce7
feat: expose conversation model selections
mrytsr Sep 6, 2026
4a1308a
feat: resolve conversation model selections at launch
mrytsr Sep 6, 2026
49a7226
feat: pass conversation model selection into agent launch
mrytsr Sep 6, 2026
0511017
docs: update model provider backend progress
mrytsr Sep 6, 2026
d144b3d
feat: add model provider launch adapters (Phase 4)
mrytsr Sep 6, 2026
99d11f1
docs: update model provider backend progress (Phase 4)
mrytsr Sep 6, 2026
bc431ef
feat: complete Phase 5 — probes, proxy projection, and tests
mrytsr Sep 6, 2026
12c078e
Codex Claude
mrytsr Sep 7, 2026
21758c2
Merge branch 'fix/chat-mobile-selection-menu' into mine
mrytsr Sep 7, 2026
b39a6e6
Merge branch 'feat/workspace-back-history-leave-guard' into mine
mrytsr Sep 7, 2026
627c421
Aoto model select
mrytsr Sep 7, 2026
939ef35
Merge main into mine: resolve message-input test mocks and closeAllFi…
mrytsr Sep 7, 2026
b9078f1
fix read conv no id
mrytsr Sep 7, 2026
49bc0b1
Merge branch 'feat/model-provider-backend' into feat(provider)/provid…
mrytsr Sep 8, 2026
5af70fc
Merge branch 'bug/fix-read-no-id' into feat(provider)/provider-compat…
mrytsr Sep 8, 2026
922bf5d
fix: reconcile provider auto-select merge (mock dedup, agentType param)
mrytsr Sep 8, 2026
825530a
fix: align provider launch tests with Codex Responses-only protocol
mrytsr Sep 8, 2026
3615027
fix: add draftAgentType to handleSend deps
mrytsr Sep 8, 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 package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@
"test:watch": "vitest",
"test:ui": "vitest --ui",
"test:coverage": "vitest run --coverage",
"server:build": "cd src-tauri && cargo build --release --bin codeg-server --no-default-features",
"server:build": "cd src-tauri && cargo build --bin codeg-server --no-default-features",
"buildall": "next build && cd src-tauri && cargo build --bin codeg-server --no-default-features",
"server:dev": "cd src-tauri && cargo run --bin codeg-server --no-default-features",
"tauri:prepare-sidecars": "node src-tauri/scripts/prepare-sidecars.mjs",
"tauri:before-dev": "pnpm tauri:prepare-sidecars && pnpm dev",
Expand Down
1 change: 1 addition & 0 deletions src-tauri/Cargo.lock

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

1 change: 1 addition & 0 deletions src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ tauri-plugin-dialog = { version = "2", optional = true }
async-trait = "0.1"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
indexmap = { version = "2", features = ["serde"] }
chrono = { version = "0.4", features = ["serde"] }
chrono-tz = "0.10"
# Host IANA zone name (e.g. `Asia/Shanghai`) — chrono only exposes the offset.
Expand Down
6 changes: 4 additions & 2 deletions src-tauri/src/acp/agent_mentions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,8 @@ fn valid_agent_wire_syntax(agent_type: &str) -> bool {
}

fn parse_internal_agent_routes(candidate: &str) -> Option<InternalAgentRoutes> {
if !candidate.starts_with(ROUTE_FRAME_SEPARATOR) || !candidate.ends_with(ROUTE_FRAME_SEPARATOR) {
if !candidate.starts_with(ROUTE_FRAME_SEPARATOR) || !candidate.ends_with(ROUTE_FRAME_SEPARATOR)
{
return None;
}
parse_internal_agent_routes_body(
Expand Down Expand Up @@ -835,7 +836,8 @@ mod tests {
#[test]
fn a_title_is_cut_at_whichever_frame_marker_comes_first() {
// Separator intact: the cut lands on it, as it always has.
let mut separated = String::from("hi \u{001e}{\"kind\":\"codeg_internal_agent_routes\",\"ve");
let mut separated =
String::from("hi \u{001e}{\"kind\":\"codeg_internal_agent_routes\",\"ve");
cut_at_route_frame_marker(&mut separated);
assert_eq!(separated, "hi");

Expand Down
91 changes: 51 additions & 40 deletions src-tauri/src/acp/background_watch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -615,7 +615,12 @@ impl WatchState {
return;
}
let before = self.resolved_title();
capture_title_record(value, record_type, &mut self.custom_title, &mut self.ai_title);
capture_title_record(
value,
record_type,
&mut self.custom_title,
&mut self.ai_title,
);
let after = self.resolved_title();
// `capture_title_record` ignores blank values, so `after` is `None`
// only when nothing has ever been captured — never a clear.
Expand Down Expand Up @@ -850,8 +855,7 @@ impl WatchState {
});

let outstanding = self.tasks.len() as u32;
let accounting_changed =
expired_any || self.last_emitted_outstanding != Some(outstanding);
let accounting_changed = expired_any || self.last_emitted_outstanding != Some(outstanding);
if changed_turns.is_empty() && settled.is_empty() && !accounting_changed {
return None;
}
Expand Down Expand Up @@ -1056,8 +1060,7 @@ impl WatchState {
.and_then(|v| v.as_str())
.filter(|s| !s.is_empty())
{
let status =
task.get("status").and_then(|s| s.as_str()).unwrap_or("");
let status = task.get("status").and_then(|s| s.as_str()).unwrap_or("");
if is_terminal_task_status(status) && self.tasks.remove(id).is_some() {
self.settled_ids.insert(id.to_string());
tracing::info!(
Expand Down Expand Up @@ -1124,8 +1127,7 @@ impl WatchState {
// again — the notification's own <note> documents
// multi-notify).
"SendMessage" => {
let Some(to) =
input.and_then(|i| i.get("to")).and_then(|t| t.as_str())
let Some(to) = input.and_then(|i| i.get("to")).and_then(|t| t.as_str())
else {
continue;
};
Expand Down Expand Up @@ -1268,8 +1270,7 @@ impl WatchState {
// Cosmetic re-basing of the SAME continuous out-of-turn
// stretch (not a new initiator record) — the origin carries
// over unchanged.
let inherited_origin =
self.episode.as_ref().and_then(|e| e.origin_task_id.clone());
let inherited_origin = self.episode.as_ref().and_then(|e| e.origin_task_id.clone());
self.episode = Some(Episode {
start_offset: self.next_episode_base(),
acc: ClaudeRecordAccumulator::new(
Expand Down Expand Up @@ -1638,9 +1639,7 @@ mod tests {
ws.tick(ledger, "/tmp", "conn-test", false, true)
}

fn unpack(
event: AcpEvent,
) -> (Vec<MessageTurn>, u32, Vec<BackgroundSettledInfo>, u64) {
fn unpack(event: AcpEvent) -> (Vec<MessageTurn>, u32, Vec<BackgroundSettledInfo>, u64) {
match event {
AcpEvent::BackgroundActivity {
turns,
Expand Down Expand Up @@ -1756,7 +1755,8 @@ mod tests {
let path = temp_session(&dir);
// Fork-time metadata at the head (post-epoch stamp), then the copied
// history (original pre-fork stamps), then the genuinely-new prompt.
let queue_op = r#"{"type":"queue-operation","timestamp":"2026-07-07T03:50:00.100Z","uuid":"q-1"}"#;
let queue_op =
r#"{"type":"queue-operation","timestamp":"2026-07-07T03:50:00.100Z","uuid":"q-1"}"#;
let copied_user = r#"{"type":"user","timestamp":"2026-07-07T03:46:00.000Z","uuid":"u-hi","message":{"role":"user","content":[{"type":"text","text":"hi"}]}}"#;
let copied_asst = r#"{"type":"assistant","timestamp":"2026-07-07T03:46:05.000Z","uuid":"a-hi","message":{"role":"assistant","content":[{"type":"text","text":"Hi!"}]}}"#;
let new_user = r#"{"type":"user","timestamp":"2026-07-07T03:50:10.000Z","uuid":"u-hello","message":{"role":"user","content":[{"type":"text","text":"hello"}]}}"#;
Expand Down Expand Up @@ -1801,8 +1801,7 @@ mod tests {
// The flush completes: the reconstructed ack must account.
append_raw(&path, tail);
append_raw(&path, "\n");
let (_, outstanding, ..) =
unpack(tick_now(&mut ws, &ledger).expect("ack event"));
let (_, outstanding, ..) = unpack(tick_now(&mut ws, &ledger).expect("ack event"));
assert_eq!(outstanding, 1, "mid-flush ack must survive discovery");
}

Expand Down Expand Up @@ -1836,8 +1835,7 @@ mod tests {
// ahead of the baseline and re-arms the accounting.
ws.rearm("s2".into(), epoch("2026-07-07T03:50:00.000Z"));
ws.adopt_file(forked.clone());
let (_, outstanding, ..) =
unpack(tick_now(&mut ws, &ledger).expect("resume event"));
let (_, outstanding, ..) = unpack(tick_now(&mut ws, &ledger).expect("resume event"));
assert_eq!(
outstanding, 1,
"a resume written before the watcher noticed the fork must re-arm"
Expand Down Expand Up @@ -1877,8 +1875,7 @@ mod tests {
assert_eq!(outstanding, 1, "post-fork resume must re-arm");

write_lines(&forked, &[&notification("agentX", "completed")]);
let (_, outstanding, settled, _) =
unpack(tick_now(&mut ws, &ledger).unwrap());
let (_, outstanding, settled, _) = unpack(tick_now(&mut ws, &ledger).unwrap());
assert_eq!(outstanding, 0);
assert_eq!(settled.len(), 1);
}
Expand Down Expand Up @@ -1995,8 +1992,7 @@ mod tests {
&assistant_text("a2", "resuming"),
],
);
let (turns, ..) =
unpack(tick_prompting(&mut ws, &ledger).expect("turns event"));
let (turns, ..) = unpack(tick_prompting(&mut ws, &ledger).expect("turns event"));
assert!(
!turns.is_empty(),
"an autonomous initiator after the reply is out-of-turn as before"
Expand Down Expand Up @@ -2106,8 +2102,7 @@ mod tests {
&path,
&[command, cron, &assistant_text("a1", "Working on it.")],
);
let (turns, ..) =
unpack(tick_prompting(&mut ws, &ledger).expect("turns event"));
let (turns, ..) = unpack(tick_prompting(&mut ws, &ledger).expect("turns event"));
assert!(
!turns.is_empty(),
"a different submission is not this one's side record"
Expand Down Expand Up @@ -2146,8 +2141,7 @@ mod tests {
&assistant_text("a1", "Build finished cleanly."),
],
);
let (turns, ..) =
unpack(tick_prompting(&mut ws, &ledger).expect("settle event"));
let (turns, ..) = unpack(tick_prompting(&mut ws, &ledger).expect("settle event"));
// Not just "an episode opened": the settlement's own follow-up is what
// has nowhere else to render, so it must be IN the emitted turn.
assert!(
Expand Down Expand Up @@ -2275,8 +2269,7 @@ mod tests {
&path,
&[&cron_prompt("new pass"), &assistant_text("a9", "hi")],
);
let (turns, outstanding, ..) =
unpack(tick_now(&mut ws, &ledger).expect("turns event"));
let (turns, outstanding, ..) = unpack(tick_now(&mut ws, &ledger).expect("turns event"));
assert_eq!(outstanding, 0, "historical ack must NOT register");
assert_eq!(turns.len(), 1);
}
Expand Down Expand Up @@ -2527,11 +2520,18 @@ mod tests {
let _ = tick_now(&mut ws, &ledger);
write_lines(
&path,
&[&notification("shell1", "completed"), &assistant_text("a1", "Done.")],
&[
&notification("shell1", "completed"),
&assistant_text("a1", "Done."),
],
);
let (turns, outstanding, settled, _) =
unpack(tick_now(&mut ws, &ledger).expect("settle event"));
assert_eq!(turns.len(), 1, "a shell follow-up has nowhere else to render");
assert_eq!(
turns.len(),
1,
"a shell follow-up has nowhere else to render"
);
assert_eq!(outstanding, 0);
assert_eq!(
settled.len(),
Expand Down Expand Up @@ -2612,8 +2612,7 @@ mod tests {
&assistant_text("a1", "Working on it."),
],
);
let (turns, ..) =
unpack(tick_prompting(&mut ws, &ledger).expect("turns event"));
let (turns, ..) = unpack(tick_prompting(&mut ws, &ledger).expect("turns event"));
assert_eq!(
turns.len(),
1,
Expand Down Expand Up @@ -2648,8 +2647,7 @@ mod tests {
&assistant_text("a1", "Build finished cleanly."),
],
);
let (turns, ..) =
unpack(tick_prompting(&mut ws, &ledger).expect("settle event"));
let (turns, ..) = unpack(tick_prompting(&mut ws, &ledger).expect("settle event"));
assert_eq!(
turns.len(),
1,
Expand Down Expand Up @@ -2810,7 +2808,10 @@ mod tests {
let more = assistant_text("a2", "step two");
let (head, tail) = more.split_at(more.len() / 2);
{
let mut f = std::fs::OpenOptions::new().append(true).open(&path).unwrap();
let mut f = std::fs::OpenOptions::new()
.append(true)
.open(&path)
.unwrap();
f.write_all(head.as_bytes()).unwrap();
}
assert!(
Expand All @@ -2819,7 +2820,10 @@ mod tests {
);

{
let mut f = std::fs::OpenOptions::new().append(true).open(&path).unwrap();
let mut f = std::fs::OpenOptions::new()
.append(true)
.open(&path)
.unwrap();
f.write_all(tail.as_bytes()).unwrap();
f.write_all(b"\n").unwrap();
}
Expand Down Expand Up @@ -2903,7 +2907,9 @@ mod tests {
// cron prompt (isMeta + string): initiates with the prompt text.
let cron: serde_json::Value = serde_json::from_str(&cron_prompt("check weather")).unwrap();
assert_eq!(
turn_initiator_text(&cron).as_ref().map(|text| text.as_str()),
turn_initiator_text(&cron)
.as_ref()
.map(|text| text.as_str()),
Some("check weather")
);

Expand Down Expand Up @@ -2965,7 +2971,10 @@ mod tests {

write_lines(&path, &[&ai_title("Fix the login flow")]);
let _ = tick_now(&mut ws, &ledger);
assert_eq!(ws.pending_title.take().as_deref(), Some("Fix the login flow"));
assert_eq!(
ws.pending_title.take().as_deref(),
Some("Fix the login flow")
);

// Claude Code re-emits the record; the resolved name did not change.
write_lines(&path, &[&ai_title("Fix the login flow")]);
Expand All @@ -2975,7 +2984,10 @@ mod tests {
// A genuinely new name is queued again.
write_lines(&path, &[&ai_title("Fix the signup flow")]);
let _ = tick_now(&mut ws, &ledger);
assert_eq!(ws.pending_title.take().as_deref(), Some("Fix the signup flow"));
assert_eq!(
ws.pending_title.take().as_deref(),
Some("Fix the signup flow")
);
}

/// `customTitle ?? aiTitle` — Claude Code's own precedence, and the one
Expand All @@ -2996,8 +3008,7 @@ mod tests {
write_lines(&path, &[&ai_title("Concise AI Summary")]);
let _ = tick_now(&mut ws, &ledger);
assert_eq!(
ws.pending_title,
None,
ws.pending_title, None,
"the generated title must not displace the user's own name"
);
assert_eq!(ws.resolved_title().as_deref(), Some("auth-refactor"));
Expand Down
5 changes: 3 additions & 2 deletions src-tauri/src/acp/binary_cache.rs
Original file line number Diff line number Diff line change
Expand Up @@ -482,8 +482,9 @@ pub async fn ensure_binary_for_agent_with_progress(
/// Hex SHA-256 of a file, streamed so a large archive never lands in memory.
fn file_sha256(path: &std::path::Path) -> Result<String, AcpError> {
use sha2::{Digest, Sha256};
let mut file = std::fs::File::open(path)
.map_err(|e| AcpError::DownloadFailed(format!("failed to open archive for hashing: {e}")))?;
let mut file = std::fs::File::open(path).map_err(|e| {
AcpError::DownloadFailed(format!("failed to open archive for hashing: {e}"))
})?;
let mut hasher = Sha256::new();
let mut buf = vec![0u8; 64 * 1024];
loop {
Expand Down
21 changes: 11 additions & 10 deletions src-tauri/src/acp/codex_goal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -296,10 +296,7 @@ mod tests {
});
let m = goal_marker(&goal).expect("goal marker");
assert_eq!(m.tool_name, "update_goal");
assert_eq!(
m.title,
"Goal updated (budget_limited): Fix the login bug"
);
assert_eq!(m.title, "Goal updated (budget_limited): Fix the login bug");
let out: Value = serde_json::from_str(&m.output_json).unwrap();
assert_eq!(out["goal"]["status"], "budget_limited");
assert_eq!(out["goal"]["tokensUsed"], 5200);
Expand Down Expand Up @@ -380,9 +377,11 @@ mod tests {
#[test]
fn active_then_clear_closes_one_card_with_objective() {
let mut open = None;
let create =
next_goal_marker(&mut open, &json!({ "objective": "Ship it", "status": "active" }))
.unwrap();
let create = next_goal_marker(
&mut open,
&json!({ "objective": "Ship it", "status": "active" }),
)
.unwrap();
assert_eq!(create.tool_name, "create_goal");
assert_eq!(open.as_deref(), Some("Ship it"));

Expand All @@ -399,9 +398,11 @@ mod tests {
// a no-op (no stray standalone "complete" card).
let mut open = None;
next_goal_marker(&mut open, &json!({ "objective": "Y", "status": "active" })).unwrap();
let terminal =
next_goal_marker(&mut open, &json!({ "objective": "Y", "status": "budgetLimited" }))
.unwrap();
let terminal = next_goal_marker(
&mut open,
&json!({ "objective": "Y", "status": "budgetLimited" }),
)
.unwrap();
assert_eq!(terminal.tool_name, "update_goal");
assert_eq!(status_of(&terminal), "budget_limited");
assert_eq!(open, None);
Expand Down
Loading
Loading