Skip to content

Commit fd89772

Browse files
committed
feat: improve scoped search and workspace browsing
1 parent 24ae4f6 commit fd89772

20 files changed

Lines changed: 1847 additions & 525 deletions

src-tauri/src/app/agent_harness.rs

Lines changed: 176 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ use super::*;
22
use std::process::Stdio;
33
use std::sync::atomic::{AtomicBool, Ordering};
44
use std::sync::Arc;
5+
use std::time::{SystemTime, UNIX_EPOCH};
56
use tokio::io::{AsyncBufReadExt, AsyncRead, BufReader};
67
use tokio::process::Command;
78
use tokio::sync::Mutex as AsyncMutex;
@@ -13,32 +14,41 @@ static AGENT_HARNESS_PROCESSES: LazyLock<AsyncMutex<HashMap<String, u32>>> =
1314
#[serde(rename_all = "camelCase", tag = "kind")]
1415
pub(crate) enum AgentHarnessEvent {
1516
Started {
17+
#[serde(rename = "sessionId")]
1618
session_id: String,
1719
provider: String,
1820
command: String,
1921
},
2022
Json {
23+
#[serde(rename = "sessionId")]
2124
session_id: String,
2225
stream: String,
26+
#[serde(rename = "eventType")]
2327
event_type: Option<String>,
28+
#[serde(rename = "itemId")]
29+
item_id: Option<String>,
2430
role: Option<String>,
2531
text: Option<String>,
2632
raw: Value,
2733
},
2834
Stdout {
35+
#[serde(rename = "sessionId")]
2936
session_id: String,
3037
text: String,
3138
},
3239
Stderr {
40+
#[serde(rename = "sessionId")]
3341
session_id: String,
3442
text: String,
3543
},
3644
Exit {
45+
#[serde(rename = "sessionId")]
3746
session_id: String,
3847
code: Option<i32>,
3948
success: bool,
4049
},
4150
Error {
51+
#[serde(rename = "sessionId")]
4252
session_id: String,
4353
message: String,
4454
},
@@ -47,6 +57,7 @@ pub(crate) enum AgentHarnessEvent {
4757
struct HarnessCommand {
4858
program: String,
4959
args: Vec<String>,
60+
source: &'static str,
5061
}
5162

5263
impl HarnessCommand {
@@ -79,7 +90,11 @@ fn build_harness_command(
7990
args.push(resume_id.to_string());
8091
}
8192
args.push(prompt.to_string());
82-
return Ok(HarnessCommand { program, args });
93+
return Ok(HarnessCommand {
94+
program,
95+
args,
96+
source: "custom-harness",
97+
});
8398
}
8499

85100
let command_name = match provider {
@@ -99,10 +114,11 @@ fn build_harness_command(
99114
let args = match provider {
100115
"claude" => {
101116
let mut args = vec![
102-
"--print".to_string(),
103-
"--verbose".to_string(),
104117
"--output-format".to_string(),
105118
"stream-json".to_string(),
119+
"--verbose".to_string(),
120+
"-p".to_string(),
121+
prompt.to_string(),
106122
"--include-partial-messages".to_string(),
107123
];
108124
if let Some(resume_id) = resume_session_id.filter(|id| !id.trim().is_empty()) {
@@ -112,7 +128,6 @@ fn build_harness_command(
112128
args.push("--session-id".to_string());
113129
args.push(session_id.to_string());
114130
}
115-
args.push(prompt.to_string());
116131
args
117132
}
118133
"codex" => {
@@ -133,11 +148,24 @@ fn build_harness_command(
133148
_ => unreachable!(),
134149
};
135150

136-
Ok(HarnessCommand { program, args })
151+
Ok(HarnessCommand {
152+
program,
153+
args,
154+
source: "direct-cli",
155+
})
137156
}
138157

139158
fn emit_harness_event(app: &tauri::AppHandle, event: AgentHarnessEvent) {
140-
let _ = app.emit("agent-harness-event", event);
159+
if let Err(error) = app.emit("agent-harness-event", event) {
160+
eprintln!("[DEBUG][HarnessBackend] emit failed: {}", error);
161+
}
162+
}
163+
164+
fn harness_debug_now_ms() -> u128 {
165+
SystemTime::now()
166+
.duration_since(UNIX_EPOCH)
167+
.map(|duration| duration.as_millis())
168+
.unwrap_or_default()
141169
}
142170

143171
fn json_string(value: &Value, keys: &[&str]) -> Option<String> {
@@ -160,7 +188,14 @@ fn collect_content_text(value: &Value, out: &mut Vec<String>) {
160188
Value::Object(map) => {
161189
if matches!(
162190
map.get("type").and_then(Value::as_str),
163-
Some("text" | "output_text" | "input_text")
191+
Some(
192+
"text"
193+
| "text_delta"
194+
| "output_text"
195+
| "output_text_delta"
196+
| "input_text"
197+
| "agent_message"
198+
)
164199
) {
165200
if let Some(text) = map.get("text").and_then(Value::as_str) {
166201
if !text.trim().is_empty() {
@@ -187,15 +222,29 @@ fn collect_content_text(value: &Value, out: &mut Vec<String>) {
187222
}
188223

189224
fn extract_harness_text(raw: &Value) -> Option<String> {
225+
if let Some(text) = raw
226+
.get("item")
227+
.filter(|item| item.get("type").and_then(Value::as_str) == Some("agent_message"))
228+
.and_then(|item| item.get("text").and_then(Value::as_str))
229+
.map(str::to_string)
230+
{
231+
if !text.trim().is_empty() {
232+
return Some(text);
233+
}
234+
}
235+
190236
if let Some(text) = json_string(raw, &["text", "result", "last_message"]) {
191237
if !text.trim().is_empty() {
192238
return Some(text);
193239
}
194240
}
195241

196242
let mut parts = Vec::new();
197-
for key in ["message", "content", "delta", "item", "payload"] {
243+
for key in ["message", "content", "delta", "event", "item", "payload"] {
198244
if let Some(value) = raw.get(key) {
245+
if key == "event" && !value.is_object() && !value.is_array() {
246+
continue;
247+
}
199248
collect_content_text(value, &mut parts);
200249
}
201250
}
@@ -207,10 +256,41 @@ fn extract_harness_text(raw: &Value) -> Option<String> {
207256
}
208257
}
209258

259+
fn json_event_type(raw: &Value) -> Option<String> {
260+
let event_type = json_string(raw, &["type", "event", "kind"]);
261+
if event_type.as_deref() == Some("stream_event") {
262+
return raw
263+
.get("event")
264+
.and_then(|event| json_string(event, &["type", "event", "kind"]))
265+
.or(event_type);
266+
}
267+
event_type
268+
}
269+
270+
fn json_item_id(raw: &Value) -> Option<String> {
271+
json_string(raw, &["id", "message_id", "item_id"])
272+
.or_else(|| raw.get("message").and_then(|v| json_string(v, &["id"])))
273+
.or_else(|| raw.get("event")?.get("message").and_then(|v| json_string(v, &["id"])))
274+
.or_else(|| raw.get("item").and_then(|v| json_string(v, &["id"])))
275+
.or_else(|| raw.get("payload").and_then(|v| json_string(v, &["id"])))
276+
}
277+
210278
fn json_role(raw: &Value) -> Option<String> {
279+
if let Some(item_type) = raw.get("item").and_then(|item| item.get("type")).and_then(Value::as_str) {
280+
return match item_type {
281+
"agent_message" => Some("assistant".to_string()),
282+
"command_execution" | "file_change" | "mcp_tool_call" | "web_search" | "todo_list" => {
283+
Some("tool".to_string())
284+
}
285+
"error" => Some("error".to_string()),
286+
_ => None,
287+
};
288+
}
289+
211290
raw.get("role")
212291
.and_then(Value::as_str)
213292
.or_else(|| raw.get("message")?.get("role")?.as_str())
293+
.or_else(|| raw.get("event")?.get("message")?.get("role")?.as_str())
214294
.map(str::to_string)
215295
}
216296

@@ -220,27 +300,58 @@ fn emit_harness_line(app: &tauri::AppHandle, session_id: &str, stream: &str, lin
220300
}
221301

222302
if let Ok(raw) = serde_json::from_str::<Value>(&line) {
223-
let event_type = json_string(&raw, &["type", "event", "kind"]);
303+
let event_type = json_event_type(&raw);
304+
let item_id = json_item_id(&raw);
305+
let role = json_role(&raw);
306+
let text = extract_harness_text(&raw);
307+
let event_type_for_log = event_type.as_deref().unwrap_or("json");
308+
if matches!(
309+
event_type_for_log,
310+
"content_block_delta" | "assistant" | "result" | "message_stop" | "content_block_stop"
311+
) {
312+
eprintln!(
313+
"[DEBUG][HarnessBackend] emit-json t={} session={} stream={} event_type={} text_len={} line_len={}",
314+
harness_debug_now_ms(),
315+
session_id,
316+
stream,
317+
event_type_for_log,
318+
text.as_ref().map(|value| value.len()).unwrap_or(0),
319+
line.len()
320+
);
321+
}
224322
emit_harness_event(
225323
app,
226324
AgentHarnessEvent::Json {
227325
session_id: session_id.to_string(),
228326
stream: stream.to_string(),
229327
event_type,
230-
role: json_role(&raw),
231-
text: extract_harness_text(&raw),
328+
item_id,
329+
role,
330+
text,
232331
raw,
233332
},
234333
);
235334
return;
236335
}
237336

238337
let event = if stream == "stderr" {
338+
eprintln!(
339+
"[DEBUG][HarnessBackend] emit-stderr t={} session={} text_len={}",
340+
harness_debug_now_ms(),
341+
session_id,
342+
line.len()
343+
);
239344
AgentHarnessEvent::Stderr {
240345
session_id: session_id.to_string(),
241346
text: line,
242347
}
243348
} else {
349+
eprintln!(
350+
"[DEBUG][HarnessBackend] emit-stdout t={} session={} text_len={}",
351+
harness_debug_now_ms(),
352+
session_id,
353+
line.len()
354+
);
244355
AgentHarnessEvent::Stdout {
245356
session_id: session_id.to_string(),
246357
text: line,
@@ -280,6 +391,16 @@ async fn read_harness_lines<R>(
280391
}
281392
}
282393

394+
#[tauri::command]
395+
pub(crate) async fn list_agent_harness_sessions() -> Vec<String> {
396+
AGENT_HARNESS_PROCESSES
397+
.lock()
398+
.await
399+
.keys()
400+
.cloned()
401+
.collect()
402+
}
403+
283404
#[tauri::command]
284405
pub(crate) async fn start_agent_harness_session(
285406
app: tauri::AppHandle,
@@ -312,6 +433,20 @@ pub(crate) async fn start_agent_harness_session(
312433
resume_session_id.as_deref(),
313434
)?;
314435
let display_command = harness.display();
436+
eprintln!(
437+
"[DEBUG][HarnessBackend] start t={} session={} provider={} source={} prompt_len={} has_resume={} program={} arg_count={}",
438+
harness_debug_now_ms(),
439+
session_id,
440+
provider,
441+
harness.source,
442+
prompt.len(),
443+
resume_session_id
444+
.as_deref()
445+
.map(|id| !id.trim().is_empty())
446+
.unwrap_or(false),
447+
harness.program,
448+
harness.args.len()
449+
);
315450

316451
let mut child = Command::new(&harness.program)
317452
.args(&harness.args)
@@ -367,21 +502,38 @@ pub(crate) async fn start_agent_harness_session(
367502
cancelled.store(true, Ordering::Relaxed);
368503
AGENT_HARNESS_PROCESSES.lock().await.remove(&session_id);
369504
match status {
370-
Ok(status) => emit_harness_event(
371-
&app,
372-
AgentHarnessEvent::Exit {
505+
Ok(status) => {
506+
eprintln!(
507+
"[DEBUG][HarnessBackend] exit t={} session={} code={:?} success={}",
508+
harness_debug_now_ms(),
373509
session_id,
374-
code: status.code(),
375-
success: status.success(),
376-
},
377-
),
378-
Err(error) => emit_harness_event(
379-
&app,
380-
AgentHarnessEvent::Error {
510+
status.code(),
511+
status.success()
512+
);
513+
emit_harness_event(
514+
&app,
515+
AgentHarnessEvent::Exit {
516+
session_id,
517+
code: status.code(),
518+
success: status.success(),
519+
},
520+
);
521+
}
522+
Err(error) => {
523+
eprintln!(
524+
"[DEBUG][HarnessBackend] wait-error t={} session={} message={}",
525+
harness_debug_now_ms(),
381526
session_id,
382-
message: error.to_string(),
383-
},
384-
),
527+
error
528+
);
529+
emit_harness_event(
530+
&app,
531+
AgentHarnessEvent::Error {
532+
session_id,
533+
message: error.to_string(),
534+
},
535+
);
536+
}
385537
}
386538
});
387539

0 commit comments

Comments
 (0)