Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
19 changes: 19 additions & 0 deletions apps/staged/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 apps/staged/src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ acp-client = { path = "../../../crates/acp-client" }
blox-cli = { path = "../../../crates/blox-cli" }

regex = "1"
strip-ansi-escapes = "0.2"
async-trait = "0.1"
tokio-util = { version = "0.7", features = ["compat"] }
tauri-plugin-store = "2.4.2"
Expand Down
49 changes: 35 additions & 14 deletions apps/staged/src-tauri/src/actions/run_detector.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,13 @@ use super::registry::{ActionRegistry, RunPhase};

use crate::store::Store;

/// Strip ANSI escape sequences so regexes written against plain text can match
/// terminal output that includes colour/style codes.
fn strip_ansi_codes(s: &str) -> String {
let stripped = strip_ansi_escapes::strip(s);
String::from_utf8_lossy(&stripped).into_owned()
}

/// Spawns a background task that polls the shared output buffer every 2 seconds,
/// applies the given regex against new lines, and transitions `RunPhase` to
/// `Running` when the pattern matches.
Expand Down Expand Up @@ -102,9 +109,10 @@ pub fn spawn_regex_matcher(
lines
};

// Apply regex to each new line.
// Apply regex to each new line (strip ANSI codes before matching).
for line in &new_lines {
if let Some(caps) = re.captures(line) {
let clean = strip_ansi_codes(line);
if let Some(caps) = re.captures(&clean) {
let endpoint = if has_endpoint_capture {
caps.name("endpoint").map(|m| m.as_str().to_string())
} else {
Expand Down Expand Up @@ -217,11 +225,10 @@ pub fn spawn_autodetect_poller(
if tail.is_empty() {
continue;
}
let output = tail
.iter()
.map(|s| s.as_str())
.collect::<Vec<_>>()
.join("\n");

// Strip ANSI codes before sending to AI and before regex matching.
let clean_lines: Vec<String> = tail.iter().map(|s| strip_ansi_codes(s)).collect();
let output = clean_lines.join("\n");

// ---- Build and send the AI prompt ----
let prompt = format!(
Expand All @@ -237,11 +244,24 @@ Recent terminal output (last ~200 lines):

Analyze this output and determine:
1. Is the application still building/compiling, or has it reached a running/ready state?
2. If running: identify the specific output that indicates readiness (e.g.,
"Listening on http://0.0.0.0:3000", "Server started on port 8080", "ready in 300ms", "Local: http://localhost:1234/").
3. Provide a regex pattern that would match for this and future runs. Be careful to avoid volatile values like timestamps, PIDs, version numbers or build durations.
- If the line contains a URL/endpoint, include a named capture group `(?P<endpoint>...)` for it.
- The regex should be general enough to work across restarts
2. If running: identify the specific output line from the server or build tool that
indicates readiness (e.g., "Listening on http://0.0.0.0:3000", "Server started
on port 8080", "ready in 300ms", "Local: http://localhost:1234/").
- IMPORTANT: Pick the server/framework readiness message, NOT application-level
log output. For example, Vite prints "Local: http://localhost:PORT/" when ready —
use that, not subsequent browser console logs or webview messages that happen
to contain URLs.
- Prefer the EARLIEST line that indicates the service is up and accepting
connections.
3. Provide a regex pattern that would match this readiness line in future runs.
The regex is tested against each output line individually (single-line matching),
so it must match within a single line.
Be careful to avoid volatile values like timestamps, PIDs, version numbers,
or build durations.
- If the readiness line contains a URL/endpoint, include a named capture group
`(?P<endpoint>...)` for it.
- The regex should be general enough to work across restarts but specific enough
to avoid matching unrelated log lines that happen to contain URLs.

Respond ONLY with JSON, no other text:
{{
Expand Down Expand Up @@ -313,8 +333,9 @@ If still building, set regex and has_endpoint_capture to null/false."#,
}
};

// Validate that the regex matches at least one line in the current output.
let matched_line = lines.iter().find(|line| re.is_match(line));
// Validate that the regex matches at least one line in the current
// output (using the already-stripped lines).
let matched_line = clean_lines.iter().find(|line| re.is_match(line));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Match AI regex against full output buffer

The new validation step only checks clean_lines, which is built from tail (last ~200 lines), while previously it searched the full lines buffer. In runs that emit a lot of logs after startup, the readiness line can scroll out of the last 200 lines before this poll executes, so a correct readiness regex gets rejected and autodetect can loop until timeout/NoDetection. This regression is introduced by narrowing validation scope at this line; the check should still consider the full buffered output (after ANSI stripping) to avoid missing earlier readiness lines.

Useful? React with 👍 / 👎.

if matched_line.is_none() {
log::warn!(
"autodetect_poller: AI regex does not match any output line for {execution_id}"
Expand Down