Skip to content

Commit 964968d

Browse files
committed
Track AI changes across agents and bash
1 parent 45aef93 commit 964968d

34 files changed

+837
-65
lines changed

.github/workflows/release-desktop.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ jobs:
6868
shopt -u nullglob
6969
for patch_file in "${patches[@]}"; do
7070
echo "Applying patch: $(basename "$patch_file")"
71-
git -C "$TMP_DIR/upstream" apply --whitespace=nowarn "$patch_file"
71+
patch -p0 -d "$TMP_DIR/upstream" < "$patch_file"
7272
done
7373
7474
rm -rf "$VENDOR_DIR"

desktop/src-tauri/src/commands/browser.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,7 @@ fn make_minimal_ctx() -> crate::services::tools::trait_def::ToolExecutionContext
123123
task_context: None,
124124
core_context: None,
125125
file_change_tracker: None,
126+
file_change_turn_index: None,
126127
permission_gate: None,
127128
knowledge_pipeline: None,
128129
knowledge_project_id: None,

desktop/src-tauri/src/commands/claude_code.rs

Lines changed: 237 additions & 1 deletion
Large diffs are not rendered by default.

desktop/src-tauri/src/commands/file_changes.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -116,12 +116,12 @@ pub async fn get_file_change_diff(
116116
session_id: String,
117117
project_root: String,
118118
before_hash: Option<String>,
119-
after_hash: String,
119+
after_hash: Option<String>,
120120
state: tauri::State<'_, FileChangesState>,
121121
) -> Result<CommandResponse<String>, String> {
122122
let tracker = state.get_or_create(&session_id, &project_root).await;
123123
let result = match tracker.lock() {
124-
Ok(t) => match t.get_file_diff(before_hash.as_deref(), &after_hash) {
124+
Ok(t) => match t.get_file_diff(before_hash.as_deref(), after_hash.as_deref()) {
125125
Ok(diff) => CommandResponse::ok(diff),
126126
Err(e) => CommandResponse::err(e),
127127
},

desktop/src-tauri/src/commands/plan_mode/planning_execution_commands.rs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -390,6 +390,7 @@ pub async fn generate_plan(
390390
pub async fn approve_plan(
391391
request: ApprovePlanRequest,
392392
state: tauri::State<'_, PlanModeState>,
393+
file_changes_state: tauri::State<'_, crate::commands::file_changes::FileChangesState>,
393394
app_state: tauri::State<'_, AppState>,
394395
knowledge_state: tauri::State<'_, crate::commands::knowledge::KnowledgeState>,
395396
standalone_state: tauri::State<'_, crate::commands::standalone::StandaloneState>,
@@ -546,6 +547,19 @@ pub async fn approve_plan(
546547
_ => standalone_state.working_directory.read().await.clone(),
547548
};
548549
let resolved_project_path = resolved_project_root.to_string_lossy().to_string();
550+
let tracker_session_id = kernel_state
551+
.inner()
552+
.linked_kernel_sessions_for_mode_session(WorkflowMode::Plan, &session_id)
553+
.await
554+
.into_iter()
555+
.next()
556+
.unwrap_or_else(|| session_id.clone());
557+
let file_change_tracker = file_changes_state
558+
.get_or_create(&tracker_session_id, &resolved_project_path)
559+
.await;
560+
if let Ok(mut tracker) = file_change_tracker.lock() {
561+
tracker.set_app_handle(app_handle.clone());
562+
}
549563

550564
let selected_skills =
551565
crate::services::task_mode::context_provider::hydrate_skill_matches_by_ids(
@@ -572,6 +586,7 @@ pub async fn approve_plan(
572586
let step_runtime = crate::services::plan_mode::step_executor::StepExecutionRuntime {
573587
provider_config,
574588
project_root: resolved_project_root,
589+
file_change_tracker: Some(file_change_tracker),
575590
index_store,
576591
embedding_service,
577592
embedding_manager,
@@ -751,6 +766,7 @@ pub async fn approve_plan(
751766
pub async fn retry_plan_step(
752767
request: RetryPlanStepRequest,
753768
state: tauri::State<'_, PlanModeState>,
769+
file_changes_state: tauri::State<'_, crate::commands::file_changes::FileChangesState>,
754770
app_state: tauri::State<'_, AppState>,
755771
knowledge_state: tauri::State<'_, crate::commands::knowledge::KnowledgeState>,
756772
standalone_state: tauri::State<'_, crate::commands::standalone::StandaloneState>,
@@ -920,6 +936,19 @@ pub async fn retry_plan_step(
920936
_ => standalone_state.working_directory.read().await.clone(),
921937
};
922938
let resolved_project_path = resolved_project_root.to_string_lossy().to_string();
939+
let tracker_session_id = kernel_state
940+
.inner()
941+
.linked_kernel_sessions_for_mode_session(WorkflowMode::Plan, &session_id)
942+
.await
943+
.into_iter()
944+
.next()
945+
.unwrap_or_else(|| session_id.clone());
946+
let file_change_tracker = file_changes_state
947+
.get_or_create(&tracker_session_id, &resolved_project_path)
948+
.await;
949+
if let Ok(mut tracker) = file_change_tracker.lock() {
950+
tracker.set_app_handle(app_handle.clone());
951+
}
923952

924953
let selected_skills =
925954
crate::services::task_mode::context_provider::hydrate_skill_matches_by_ids(
@@ -946,6 +975,7 @@ pub async fn retry_plan_step(
946975
let step_runtime = crate::services::plan_mode::step_executor::StepExecutionRuntime {
947976
provider_config,
948977
project_root: resolved_project_root,
978+
file_change_tracker: Some(file_change_tracker),
949979
index_store,
950980
embedding_service,
951981
embedding_manager,

desktop/src-tauri/src/commands/standalone.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1532,6 +1532,7 @@ pub async fn execute_standalone(
15321532
let next = t.turn_index() + 1;
15331533
t.set_turn_index(next);
15341534
t.set_app_handle(app.clone());
1535+
orchestrator = orchestrator.with_file_change_turn_index(next);
15351536
}
15361537
orchestrator = orchestrator.with_file_change_tracker(tracker);
15371538
}
@@ -2117,6 +2118,7 @@ pub async fn execute_standalone_with_session(
21172118
let next = t.turn_index() + 1;
21182119
t.set_turn_index(next);
21192120
t.set_app_handle(app.clone());
2121+
orchestrator = orchestrator.with_file_change_turn_index(next);
21202122
}
21212123
orchestrator = orchestrator.with_file_change_tracker(tracker);
21222124
}
@@ -2737,6 +2739,7 @@ pub async fn resume_standalone_execution(
27372739
let next = t.turn_index() + 1;
27382740
t.set_turn_index(next);
27392741
t.set_app_handle(app.clone());
2742+
orchestrator = orchestrator.with_file_change_turn_index(next);
27402743
}
27412744
orchestrator = orchestrator.with_file_change_tracker(tracker);
27422745
}

desktop/src-tauri/src/commands/task_mode.rs

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2123,6 +2123,9 @@ fn build_story_executor(
21232123
skills_block: String,
21242124
selected_skill_matches: Vec<crate::services::skills::model::SkillMatch>,
21252125
knowledge_tool_params: Option<KnowledgeToolParams>,
2126+
file_change_tracker: Option<
2127+
Arc<std::sync::Mutex<crate::services::file_change_tracker::FileChangeTracker>>,
2128+
>,
21262129
) -> impl Fn(StoryExecutionContext) -> Pin<Box<dyn Future<Output = StoryExecutionOutcome> + Send>>
21272130
+ Send
21282131
+ Sync
@@ -2141,6 +2144,7 @@ fn build_story_executor(
21412144
let skills_block = skills_block.clone();
21422145
let selected_skill_matches = selected_skill_matches.clone();
21432146
let knowledge_tool_params = knowledge_tool_params.clone();
2147+
let file_change_tracker = file_change_tracker.clone();
21442148
Box::pin(async move {
21452149
eprintln!(
21462150
"[INFO] Executing story '{}' (attempt {}) with agent '{}' in {} [mode: {:?}]",
@@ -2181,6 +2185,13 @@ fn build_story_executor(
21812185

21822186
// Build execution prompt from story context
21832187
let prompt = build_story_prompt(&ctx, &knowledge_block, &memory_block, &skills_block);
2188+
let file_change_turn_index = file_change_tracker.as_ref().and_then(|tracker| {
2189+
tracker.lock().ok().map(|mut guard| {
2190+
let next = guard.turn_index().saturating_add(1);
2191+
guard.set_turn_index(next);
2192+
next
2193+
})
2194+
});
21842195

21852196
match mode {
21862197
StoryExecutionMode::Cli => {
@@ -2190,6 +2201,8 @@ fn build_story_executor(
21902201
&prompt,
21912202
&ctx.project_path,
21922203
ctx.cancel_token.clone(),
2204+
file_change_tracker.clone(),
2205+
file_change_turn_index,
21932206
)
21942207
.await
21952208
}
@@ -2210,6 +2223,8 @@ fn build_story_executor(
22102223
selected_skill_matches.as_slice(),
22112224
knowledge_tool_params.as_ref(),
22122225
ctx.cancel_token.clone(),
2226+
file_change_tracker.clone(),
2227+
file_change_turn_index,
22132228
)
22142229
.await
22152230
}
@@ -2228,6 +2243,10 @@ async fn execute_story_via_agent(
22282243
prompt: &str,
22292244
project_path: &std::path::Path,
22302245
cancel_token: tokio_util::sync::CancellationToken,
2246+
file_change_tracker: Option<
2247+
Arc<std::sync::Mutex<crate::services::file_change_tracker::FileChangeTracker>>,
2248+
>,
2249+
file_change_turn_index: Option<u32>,
22312250
) -> StoryExecutionOutcome {
22322251
use tokio::process::Command;
22332252

@@ -2266,6 +2285,11 @@ async fn execute_story_via_agent(
22662285
project_path.display()
22672286
);
22682287

2288+
let before_workspace_snapshot = file_change_tracker
2289+
.as_ref()
2290+
.and_then(|tracker| tracker.lock().ok())
2291+
.and_then(|tracker| tracker.capture_workspace_snapshot().ok());
2292+
22692293
let mut process_builder = Command::new(&command);
22702294
process_builder
22712295
.args(&args)
@@ -2288,7 +2312,7 @@ async fn execute_story_via_agent(
22882312

22892313
let mut wait_handle = tokio::spawn(async move { child.wait_with_output().await });
22902314

2291-
tokio::select! {
2315+
let outcome = tokio::select! {
22922316
_ = cancel_token.cancelled() => {
22932317
wait_handle.abort();
22942318
eprintln!("[INFO] Agent '{}' execution cancelled", command);
@@ -2338,7 +2362,29 @@ async fn execute_story_via_agent(
23382362
}
23392363
}
23402364
}
2365+
};
2366+
2367+
if let (Some(tracker), Some(before_snapshot)) = (
2368+
file_change_tracker.as_ref(),
2369+
before_workspace_snapshot.as_ref(),
2370+
) {
2371+
if let Ok(mut tracker_guard) = tracker.lock() {
2372+
if let Ok(after_snapshot) = tracker_guard.capture_workspace_snapshot() {
2373+
let turn_index = file_change_turn_index
2374+
.unwrap_or_else(|| tracker_guard.turn_index());
2375+
tracker_guard.record_workspace_delta_between_at(
2376+
turn_index,
2377+
&format!("agent-{}", uuid::Uuid::new_v4()),
2378+
"Bash",
2379+
before_snapshot,
2380+
&after_snapshot,
2381+
&format!("{} story execution", command),
2382+
);
2383+
}
2384+
}
23412385
}
2386+
2387+
outcome
23422388
}
23432389

23442390
/// Execute a story via the OrchestratorService using direct LLM API.
@@ -2361,6 +2407,10 @@ async fn execute_story_via_llm(
23612407
selected_skill_matches: &[crate::services::skills::model::SkillMatch],
23622408
knowledge_tool_params: Option<&KnowledgeToolParams>,
23632409
cancel_token: tokio_util::sync::CancellationToken,
2410+
file_change_tracker: Option<
2411+
Arc<std::sync::Mutex<crate::services::file_change_tracker::FileChangeTracker>>,
2412+
>,
2413+
file_change_turn_index: Option<u32>,
23642414
) -> StoryExecutionOutcome {
23652415
use crate::services::orchestrator::{OrchestratorConfig, OrchestratorService};
23662416
use crate::services::streaming::UnifiedStreamEvent;
@@ -2424,6 +2474,13 @@ async fn execute_story_via_llm(
24242474
.with_search_provider(&search_provider, search_api_key)
24252475
.with_permission_gate(permission_gate.clone());
24262476

2477+
if let Some(ref tracker) = file_change_tracker {
2478+
orchestrator = orchestrator.with_file_change_tracker(Arc::clone(tracker));
2479+
}
2480+
if let Some(turn_index) = file_change_turn_index {
2481+
orchestrator = orchestrator.with_file_change_turn_index(turn_index);
2482+
}
2483+
24272484
if !selected_skill_matches.is_empty() {
24282485
let selected_skills =
24292486
std::sync::Arc::new(tokio::sync::RwLock::new(selected_skill_matches.to_vec()));

desktop/src-tauri/src/commands/task_mode/execution_commands.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,7 @@ pub async fn approve_task_prd(
261261
app: tauri::AppHandle,
262262
request: ApproveTaskPrdRequest,
263263
state: tauri::State<'_, TaskModeState>,
264+
file_changes_state: tauri::State<'_, crate::commands::file_changes::FileChangesState>,
264265
kernel_state: tauri::State<'_, WorkflowKernelState>,
265266
app_state: tauri::State<'_, AppState>,
266267
permission_state: tauri::State<'_, crate::commands::permissions::PermissionState>,
@@ -462,6 +463,19 @@ pub async fn approve_task_prd(
462463
.filter(|path| !path.is_empty())
463464
.unwrap_or(".")
464465
.to_string();
466+
let tracker_session_id = kernel_state
467+
.inner()
468+
.linked_kernel_sessions_for_mode_session(WorkflowMode::Task, &session_id)
469+
.await
470+
.into_iter()
471+
.next()
472+
.unwrap_or_else(|| session_id.clone());
473+
let file_change_tracker = file_changes_state
474+
.get_or_create(&tracker_session_id, &project_path_str)
475+
.await;
476+
if let Ok(mut tracker) = file_change_tracker.lock() {
477+
tracker.set_app_handle(app.clone());
478+
}
465479
let enriched_ctx = assemble_enriched_context_v2(
466480
app_state.inner(),
467481
knowledge_state.inner(),
@@ -728,6 +742,7 @@ pub async fn approve_task_prd(
728742
skills_block,
729743
selected_skill_matches,
730744
knowledge_tool_params,
745+
Some(file_change_tracker),
731746
);
732747

733748
let result = executor

desktop/src-tauri/src/commands/task_mode/generation_commands.rs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1191,6 +1191,7 @@ pub async fn explore_project(
11911191
app_state: tauri::State<'_, AppState>,
11921192
standalone_state: tauri::State<'_, crate::commands::standalone::StandaloneState>,
11931193
knowledge_state: tauri::State<'_, crate::commands::knowledge::KnowledgeState>,
1194+
file_changes_state: tauri::State<'_, crate::commands::file_changes::FileChangesState>,
11941195
kernel_state: tauri::State<'_, WorkflowKernelState>,
11951196
app_handle: tauri::AppHandle,
11961197
) -> Result<CommandResponse<ExplorationResult>, String> {
@@ -1443,6 +1444,26 @@ pub async fn explore_project(
14431444
let (search_provider, search_api_key) = resolve_search_provider_for_tools();
14441445
let mut coordinator = crate::services::orchestrator::OrchestratorService::new(config)
14451446
.with_search_provider(&search_provider, search_api_key);
1447+
let tracker_session_id = kernel_state
1448+
.inner()
1449+
.linked_kernel_sessions_for_mode_session(WorkflowMode::Task, &session_id)
1450+
.await
1451+
.into_iter()
1452+
.next()
1453+
.unwrap_or_else(|| session_id.clone());
1454+
let file_change_tracker = file_changes_state
1455+
.get_or_create(&tracker_session_id, &project_path.to_string_lossy())
1456+
.await;
1457+
let tracker_for_orchestrator = std::sync::Arc::clone(&file_change_tracker);
1458+
if let Ok(mut tracker) = file_change_tracker.lock() {
1459+
tracker.set_app_handle(app_handle.clone());
1460+
let next = tracker.turn_index().saturating_add(1);
1461+
tracker.set_turn_index(next);
1462+
drop(tracker);
1463+
coordinator = coordinator
1464+
.with_file_change_tracker(tracker_for_orchestrator)
1465+
.with_file_change_turn_index(next);
1466+
}
14461467

14471468
// Wire database pool for CodebaseSearch
14481469
if let Ok(pool) = app_state.with_database(|db| Ok(db.pool().clone())).await {

desktop/src-tauri/src/commands/workflow.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -633,6 +633,7 @@ pub async fn workflow_resume_background_runs(
633633
state: tauri::State<'_, WorkflowKernelState>,
634634
plan_mode_state: tauri::State<'_, PlanModeState>,
635635
task_mode_state: tauri::State<'_, TaskModeState>,
636+
file_changes_state: tauri::State<'_, crate::commands::file_changes::FileChangesState>,
636637
app_state: tauri::State<'_, AppState>,
637638
knowledge_state: tauri::State<'_, crate::commands::knowledge::KnowledgeState>,
638639
plugin_state: tauri::State<'_, crate::commands::plugins::PluginState>,
@@ -731,6 +732,7 @@ pub async fn workflow_resume_background_runs(
731732
locale: payload.locale,
732733
},
733734
plan_mode_state.clone(),
735+
file_changes_state.clone(),
734736
app_state.clone(),
735737
knowledge_state.clone(),
736738
standalone_state.clone(),
@@ -841,6 +843,7 @@ pub async fn workflow_resume_background_runs(
841843
project_path: payload.project_path,
842844
},
843845
task_mode_state.clone(),
846+
file_changes_state.clone(),
844847
state.clone(),
845848
app_state.clone(),
846849
permission_state.clone(),

0 commit comments

Comments
 (0)