Skip to content

Commit 260b00d

Browse files
committed
refactor(backend): extract diff_utils to shared module for session replay
1 parent a30fc36 commit 260b00d

File tree

4 files changed

+69
-42
lines changed

4 files changed

+69
-42
lines changed

src-tauri/src/backend/event_translator.rs

Lines changed: 24 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,11 @@
77
//! OpenCode ↔ CodexMonitor translation happens here in Rust.
88
99
use serde_json::{json, Value};
10-
use similar::TextDiff;
1110
use std::collections::HashMap;
1211
use std::sync::atomic::{AtomicU64, Ordering};
1312

13+
use crate::shared::diff_utils::generate_edit_diff;
14+
1415
/// Per-session turn state — tracks active turn and item IDs for a single session.
1516
#[derive(Default)]
1617
struct PerSessionTurnState {
@@ -282,7 +283,9 @@ fn unseen_suffix<'a>(full_text: &'a str, emitted_len: usize) -> Option<&'a str>
282283
if full_text.len() <= emitted_len {
283284
return None;
284285
}
285-
full_text.get(emitted_len..).filter(|suffix| !suffix.is_empty())
286+
full_text
287+
.get(emitted_len..)
288+
.filter(|suffix| !suffix.is_empty())
286289
}
287290

288291
fn parse_u64(value: Option<&Value>) -> Option<u64> {
@@ -573,7 +576,10 @@ fn translate_part_updated(properties: &Value, state: &mut SessionTranslationStat
573576
// aren't injected by send_user_message_core).
574577
let effective_text_owned;
575578
let effective_text = if delta.is_empty() {
576-
let full_text = part.get("text").and_then(|v| v.as_str()).unwrap_or_default();
579+
let full_text = part
580+
.get("text")
581+
.and_then(|v| v.as_str())
582+
.unwrap_or_default();
577583
let current_user_text = state
578584
.get_turn_state(&thread_id)
579585
.map(|ts| ts.user_message_text.clone())
@@ -620,7 +626,10 @@ fn translate_part_updated(properties: &Value, state: &mut SessionTranslationStat
620626
}
621627
}
622628
let effective_delta = if delta.is_empty() {
623-
let full_text = part.get("text").and_then(|v| v.as_str()).unwrap_or_default();
629+
let full_text = part
630+
.get("text")
631+
.and_then(|v| v.as_str())
632+
.unwrap_or_default();
624633
let emitted_len = state
625634
.get_turn_state(&thread_id)
626635
.map(|ts| ts.agent_message_text_len)
@@ -643,7 +652,8 @@ fn translate_part_updated(properties: &Value, state: &mut SessionTranslationStat
643652
.unwrap_or(effective_delta.len());
644653
state.get_turn_state_mut(&thread_id).agent_message_text_len = full_len;
645654
} else {
646-
state.get_turn_state_mut(&thread_id).agent_message_text_len += effective_delta.len();
655+
state.get_turn_state_mut(&thread_id).agent_message_text_len +=
656+
effective_delta.len();
647657
}
648658

649659
let item_id = state.agent_message_item(&thread_id);
@@ -670,7 +680,10 @@ fn translate_part_updated(properties: &Value, state: &mut SessionTranslationStat
670680
}
671681
}
672682
let effective_delta = if delta.is_empty() {
673-
let full_text = part.get("text").and_then(|v| v.as_str()).unwrap_or_default();
683+
let full_text = part
684+
.get("text")
685+
.and_then(|v| v.as_str())
686+
.unwrap_or_default();
674687
let emitted_len = state
675688
.get_turn_state(&thread_id)
676689
.map(|ts| ts.reasoning_text_len)
@@ -1612,10 +1625,7 @@ fn translate_question_completed(properties: &Value) -> Vec<Value> {
16121625

16131626
/// Translate an OpenCode `todo.updated` SSE event into a `turn/plan/updated`
16141627
/// event so the PlanPanel sidebar reflects the current session todo list.
1615-
fn translate_todo_updated(
1616-
properties: &Value,
1617-
state: &mut SessionTranslationState,
1618-
) -> Vec<Value> {
1628+
fn translate_todo_updated(properties: &Value, state: &mut SessionTranslationState) -> Vec<Value> {
16191629
let session_id = properties
16201630
.get("sessionID")
16211631
.or_else(|| properties.get("sessionId"))
@@ -1850,36 +1860,6 @@ fn file_path_from_raw_input(raw_input: &Value) -> Option<String> {
18501860
.map(ToOwned::to_owned)
18511861
}
18521862

1853-
/// Generate a unified diff from oldString/newString edit input.
1854-
/// Returns a unified diff string with @@ hunk headers that the frontend can render.
1855-
fn generate_edit_diff(raw_input: &Value, file_path: &str) -> Option<String> {
1856-
let old_string = raw_input.get("oldString").and_then(|v| v.as_str())?;
1857-
let new_string = raw_input.get("newString").and_then(|v| v.as_str())?;
1858-
1859-
// Don't generate diff for empty old/new (pure create or delete)
1860-
if old_string.is_empty() && new_string.is_empty() {
1861-
return None;
1862-
}
1863-
1864-
let diff = TextDiff::from_lines(old_string, new_string);
1865-
let mut output = String::new();
1866-
1867-
// Add file header
1868-
output.push_str(&format!("--- a/{file_path}\n"));
1869-
output.push_str(&format!("+++ b/{file_path}\n"));
1870-
1871-
// Generate unified diff hunks
1872-
for hunk in diff.unified_diff().context_radius(3).iter_hunks() {
1873-
output.push_str(&hunk.to_string());
1874-
}
1875-
1876-
if output.contains("@@") {
1877-
Some(output)
1878-
} else {
1879-
None
1880-
}
1881-
}
1882-
18831863
fn build_tool_item(
18841864
item_id: &str,
18851865
item_type: &str,
@@ -2524,7 +2504,10 @@ mod tests {
25242504
});
25252505
let events1 = translate_sse_event(&delta1, &mut state);
25262506
assert_eq!(events1.len(), 1);
2527-
assert_eq!(events1[0]["params"]["delta"], "I'm currently in Plan Mode (read-only), ");
2507+
assert_eq!(
2508+
events1[0]["params"]["delta"],
2509+
"I'm currently in Plan Mode (read-only), "
2510+
);
25282511

25292512
let delta2 = json!({
25302513
"type": "message.part.delta",

src-tauri/src/shared/codex_core.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ use crate::codex::config as codex_config;
1616
use crate::codex::home::{resolve_default_codex_home, resolve_workspace_codex_home};
1717
use crate::rules;
1818
use crate::shared::account::{build_account_response, read_auth_account};
19+
use crate::shared::diff_utils::generate_edit_diff;
1920
use crate::types::WorkspaceEntry;
2021

2122
pub(crate) enum CodexLoginCancelState {
@@ -656,7 +657,12 @@ fn replay_build_tool_item(
656657
for key in &["filePath", "path"] {
657658
if let Some(path) = inp.get(key).and_then(|v| v.as_str()) {
658659
if !path.is_empty() {
659-
changes.push(json!({ "path": path, "kind": "modify" }));
660+
let mut change = json!({ "path": path, "kind": "modify" });
661+
// Generate diff from oldString/newString if available
662+
if let Some(diff) = generate_edit_diff(inp, path) {
663+
change["diff"] = json!(diff);
664+
}
665+
changes.push(change);
660666
break;
661667
}
662668
}

src-tauri/src/shared/diff_utils.rs

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
//! Shared diff generation utilities.
2+
//!
3+
//! Used by both live SSE event translation and session replay to generate
4+
//! unified diffs for file edit operations.
5+
6+
use serde_json::Value;
7+
use similar::TextDiff;
8+
9+
/// Generate a unified diff from oldString/newString edit input.
10+
/// Returns a unified diff string with @@ hunk headers that the frontend can render.
11+
pub(crate) fn generate_edit_diff(raw_input: &Value, file_path: &str) -> Option<String> {
12+
let old_string = raw_input.get("oldString").and_then(|v| v.as_str())?;
13+
let new_string = raw_input.get("newString").and_then(|v| v.as_str())?;
14+
15+
// Don't generate diff for empty old/new (pure create or delete)
16+
if old_string.is_empty() && new_string.is_empty() {
17+
return None;
18+
}
19+
20+
let diff = TextDiff::from_lines(old_string, new_string);
21+
let mut output = String::new();
22+
23+
// Add file header
24+
output.push_str(&format!("--- a/{file_path}\n"));
25+
output.push_str(&format!("+++ b/{file_path}\n"));
26+
27+
// Generate unified diff hunks
28+
for hunk in diff.unified_diff().context_radius(3).iter_hunks() {
29+
output.push_str(&hunk.to_string());
30+
}
31+
32+
if output.contains("@@") {
33+
Some(output)
34+
} else {
35+
None
36+
}
37+
}

src-tauri/src/shared/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
pub(crate) mod account;
22
pub(crate) mod codex_aux_core;
33
pub(crate) mod codex_core;
4+
pub(crate) mod diff_utils;
45
pub(crate) mod codex_update_core;
56
pub(crate) mod files_core;
67
pub(crate) mod git_core;

0 commit comments

Comments
 (0)