Skip to content
Merged
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
40 changes: 39 additions & 1 deletion apps/staged/src-tauri/src/session_runner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2267,7 +2267,7 @@ struct SuggestedNextSteps {
/// inside. Returns `None` if the block is missing or cannot be parsed.
fn extract_suggested_next_steps(text: &str) -> Option<SuggestedNextSteps> {
let marker = "```suggested-next-steps";
let start_pos = find_opening_fence(text, marker)?;
let start_pos = find_suggested_next_steps_opening_fence(text, marker)?;
let block_start = start_pos + marker.len();
let content_start = block_start + text[block_start..].find('\n')? + 1;
let end_pos = find_closing_fence(&text[content_start..])?;
Expand All @@ -2281,6 +2281,30 @@ fn extract_suggested_next_steps(text: &str) -> Option<SuggestedNextSteps> {
}
}

/// Find a `suggested-next-steps` opening fence.
///
/// The normal fence finder requires markers to appear at the start of a line
/// because review extraction should not match markers mentioned in prose. Notes
/// can arrive with this fence attached to the final sentence, so this finder
/// accepts inline markers while still requiring the rest of the marker line to
/// contain only optional whitespace before the newline.
fn find_suggested_next_steps_opening_fence(text: &str, marker: &str) -> Option<usize> {
let mut pos = 0;
while pos < text.len() {
let candidate = text[pos..].find(marker)?;
let abs = pos + candidate;
let after_marker = &text[abs + marker.len()..];
if after_marker
.find('\n')
.is_some_and(|newline| after_marker[..newline].trim().is_empty())
{
return Some(abs);
Comment on lines +2297 to +2301

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 Keep prose mentions from shadowing the real steps block

When an assistant message mentions the marker inline before the actual block (for example a sentence or code sample ending with ````suggested-next-stepsfollowed by a newline), this now returns that mention as the opening fence because it only validates the text after the marker.extract_suggested_next_steps` then tries to parse everything until the next closing fence and returns `None` on malformed JSON, so it never reaches a later valid `suggested-next-steps` block in the same message and the note loses its suggested next actions. Consider either continuing after parse failures or adding a preceding-character/fence-boundary check that still permits the intended `sentence.```suggested-next-steps` case.

Useful? React with 👍 / 👎.

}
pos = abs + marker.len();
}
None
}

/// Find an opening fence marker (e.g. ` ```review-title `) that appears at the
/// start of a line (position 0 or immediately after `\n`). Returns the byte
/// offset of the marker within `text`, or `None` if no line-start match exists.
Expand Down Expand Up @@ -3196,6 +3220,20 @@ Body
);
}

#[test]
fn extract_steps_valid_inline_opening_fence() {
let text = "Ready to return the note.```suggested-next-steps\n{\"suggestedNextCommitStep\": \"Reduce churn\", \"suggestedNextNoteStep\": \"Plan IPC fix\"}\n```\n";
let steps = extract_suggested_next_steps(text).unwrap();
assert_eq!(
steps.suggested_next_commit_step.as_deref(),
Some("Reduce churn")
);
assert_eq!(
steps.suggested_next_note_step.as_deref(),
Some("Plan IPC fix")
);
}

#[test]
fn extract_steps_null_fields() {
let text = "```suggested-next-steps\n{\"suggestedNextCommitStep\": null, \"suggestedNextNoteStep\": null}\n```\n";
Expand Down