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
162 changes: 159 additions & 3 deletions apps/staged/src-tauri/src/session_commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -448,6 +448,15 @@ pub enum BranchSessionType {
Review,
}

#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct BranchSessionLaunchContext {
pub source: String,
pub scope: String,
pub commit_sha: String,
pub review_id: Option<String>,
}

/// Response from starting a branch session.
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
Expand Down Expand Up @@ -623,6 +632,7 @@ pub async fn start_branch_session(
session_type: BranchSessionType,
provider: Option<String>,
image_ids: Option<Vec<String>>,
launch_context: Option<BranchSessionLaunchContext>,
) -> Result<BranchSessionResponse, String> {
let store = get_store(&store)?;

Expand Down Expand Up @@ -711,6 +721,7 @@ pub async fn start_branch_session(
&project_information,
&branch_context,
&session_type,
launch_context.as_ref(),
);

// Create the session
Expand Down Expand Up @@ -840,6 +851,7 @@ pub async fn start_branch_session(
/// (commit or note), but does NOT resolve working directory, git context,
/// or spawn an agent. The session will be started later via `drain_queued_sessions`.
#[tauri::command(rename_all = "camelCase")]
#[allow(clippy::too_many_arguments)]
pub fn queue_branch_session(
store: tauri::State<'_, Mutex<Option<Arc<Store>>>>,
registry: tauri::State<'_, Arc<session_runner::SessionRegistry>>,
Expand All @@ -848,6 +860,7 @@ pub fn queue_branch_session(
session_type: BranchSessionType,
provider: Option<String>,
image_ids: Option<Vec<String>>,
launch_context: Option<BranchSessionLaunchContext>,
) -> Result<BranchSessionResponse, String> {
let store = get_store(&store)?;

Expand All @@ -866,7 +879,8 @@ pub fn queue_branch_session(

// Create the queued session — prompt is stored raw (not enriched with
// context) since context will be built when the session is drained.
let mut session = store::Session::new_queued(&prompt);
let queued_prompt = embed_launch_context(&prompt, launch_context.as_ref())?;
let mut session = store::Session::new_queued(&queued_prompt);
if let Some(ref p) = provider {
session = session.with_provider(p);
}
Expand Down Expand Up @@ -975,7 +989,7 @@ pub async fn drain_queued_sessions_for_branch(
};

// Use the original prompt from the queued session.
let prompt = session.prompt.clone();
let (prompt, launch_context) = extract_launch_context(&session.prompt)?;
let session_id = session.id.clone();

// Resolve branch → project (same as start_branch_session).
Expand Down Expand Up @@ -1050,6 +1064,7 @@ pub async fn drain_queued_sessions_for_branch(
&project_information,
&branch_context,
&session_type,
launch_context.as_ref(),
);

// Atomically transition session from queued to running.
Expand Down Expand Up @@ -1290,6 +1305,7 @@ pub async fn trigger_auto_review(
&project_information,
&branch_context,
&BranchSessionType::Review,
None,
);

// Create the session
Expand Down Expand Up @@ -2318,6 +2334,7 @@ fn build_full_prompt(
project_information: &str,
branch_context: &str,
session_type: &BranchSessionType,
launch_context: Option<&BranchSessionLaunchContext>,
) -> String {
let action_instructions = match session_type {
BranchSessionType::Note => {
Expand Down Expand Up @@ -2432,16 +2449,88 @@ Rules:
let action_tag = format!(
"<action>\n{action_instructions}\n\nProject information:\n{project_information}\n</action>"
);
let branch_history = render_branch_history(branch_context, launch_context);

format!(
"{action_tag}\n\n\
<branch-history>\n\
{branch_context}\n\
{branch_history}\n\
</branch-history>\n\n\
{user_prompt}"
)
}

fn render_branch_history(
branch_context: &str,
launch_context: Option<&BranchSessionLaunchContext>,
) -> String {
let mut parts = Vec::new();
if !branch_context.trim().is_empty() {
parts.push(branch_context.trim_end().to_string());
}
if let Some(entry) = render_launch_context_entry(launch_context) {
parts.push(entry);
}
parts.join("\n\n")
}

fn render_launch_context_entry(
launch_context: Option<&BranchSessionLaunchContext>,
) -> Option<String> {
let context = launch_context?;
if context.source != "diff_viewer" {
return None;
}

let scope_suffix = match context.scope.as_str() {
"branch" => String::new(),
_ => format!(" (scope: {})", context.scope),
};

let mut entry = format!(
"Viewed diff before starting this session: commit {}{}.",
context.commit_sha, scope_suffix
);
if let Some(review_id) = context.review_id.as_deref() {
entry = format!(
"Viewed diff before starting this session: review {} on commit {}{}.",
review_id, context.commit_sha, scope_suffix
);
}
Some(entry)
}

fn embed_launch_context(
prompt: &str,
launch_context: Option<&BranchSessionLaunchContext>,
) -> Result<String, String> {
let Some(context) = launch_context else {
return Ok(prompt.to_string());
};
let json = serde_json::to_string(context).map_err(|e| e.to_string())?;
Ok(format!(
"<launch-context>{json}</launch-context>\n\n{}",
prompt.trim_start()
))
}

fn extract_launch_context(
prompt: &str,
) -> Result<(String, Option<BranchSessionLaunchContext>), String> {
const OPEN: &str = "<launch-context>";
const CLOSE: &str = "</launch-context>";

let Some(rest) = prompt.strip_prefix(OPEN) else {
return Ok((prompt.to_string(), None));
};
let Some((json, remainder)) = rest.split_once(CLOSE) else {
return Err("Queued session prompt had malformed launch context".to_string());
Comment on lines +2526 to +2527

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 Fall back when launch-context prefix is user text

drain_queued_sessions_for_branch now parses every queued prompt through extract_launch_context, and this helper returns an error whenever a prompt starts with <launch-context> but does not contain valid BranchSessionLaunchContext JSON. In that case the session never starts and can block later queued sessions on the same branch. This is reachable with ordinary queued prompts (no launch context supplied) if a user message begins with that tag, so parsing should degrade to "treat as plain prompt" instead of hard-failing the drain path.

Useful? React with 👍 / 👎.

};
let context =
serde_json::from_str::<BranchSessionLaunchContext>(json).map_err(|e| e.to_string())?;
Ok((remainder.trim_start().to_string(), Some(context)))
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down Expand Up @@ -2527,6 +2616,7 @@ mod tests {
"project info",
"branch context",
&BranchSessionType::Review,
None,
);

assert!(
Expand Down Expand Up @@ -2589,4 +2679,70 @@ mod tests {
let review = store.get_review(&review.id).unwrap().unwrap();
assert!(review.is_auto);
}

#[test]
fn commit_prompt_appends_diff_viewer_context_to_branch_history() {
let prompt = build_full_prompt(
"user prompt",
"project info",
"branch context",
&BranchSessionType::Commit,
Some(&BranchSessionLaunchContext {
source: "diff_viewer".to_string(),
scope: "commit".to_string(),
commit_sha: "abc123".to_string(),
review_id: Some("review-42".to_string()),
}),
);

assert!(prompt.contains(
"Viewed diff before starting this session: review review-42 on commit abc123 (scope: commit)."
));
}

#[test]
fn commit_prompt_omits_branch_scope_from_diff_viewer_context() {
let prompt = build_full_prompt(
"user prompt",
"project info",
"branch context",
&BranchSessionType::Commit,
Some(&BranchSessionLaunchContext {
source: "diff_viewer".to_string(),
scope: "branch".to_string(),
commit_sha: "abc123".to_string(),
review_id: None,
}),
);

assert!(prompt.contains("Viewed diff before starting this session: commit abc123."));
assert!(!prompt.contains("(scope: branch)"));
}

#[test]
fn queued_prompt_round_trips_launch_context() {
let prompt = embed_launch_context(
"Implement plan",
Some(&BranchSessionLaunchContext {
source: "diff_viewer".to_string(),
scope: "branch".to_string(),
commit_sha: "deadbeef".to_string(),
review_id: None,
}),
)
.unwrap();

let (decoded_prompt, launch_context) = extract_launch_context(&prompt).unwrap();

assert_eq!(decoded_prompt, "Implement plan");
assert_eq!(
launch_context,
Some(BranchSessionLaunchContext {
source: "diff_viewer".to_string(),
scope: "branch".to_string(),
commit_sha: "deadbeef".to_string(),
review_id: None,
})
);
}
}
9 changes: 7 additions & 2 deletions apps/staged/src/lib/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import type {
Branch,
BranchTimeline,
BranchRef,
BranchSessionLaunchContext,
BranchSessionType,
BranchSessionResponse,
StoreIncompatibility,
Expand Down Expand Up @@ -557,14 +558,16 @@ export function startBranchSession(
prompt: string,
sessionType: BranchSessionType,
provider?: string,
imageIds?: string[]
imageIds?: string[],
launchContext?: BranchSessionLaunchContext
): Promise<BranchSessionResponse> {
return invoke('start_branch_session', {
branchId,
prompt,
sessionType,
provider: provider ?? null,
imageIds: imageIds ?? null,
launchContext: launchContext ?? null,
});
}

Expand All @@ -574,14 +577,16 @@ export function queueBranchSession(
prompt: string,
sessionType: BranchSessionType,
provider?: string,
imageIds?: string[]
imageIds?: string[],
launchContext?: BranchSessionLaunchContext
): Promise<BranchSessionResponse> {
return invoke('queue_branch_session', {
branchId,
prompt,
sessionType,
provider: provider ?? null,
imageIds: imageIds ?? null,
launchContext: launchContext ?? null,
});
}

Expand Down
3 changes: 2 additions & 1 deletion apps/staged/src/lib/features/branches/BranchCard.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
import ReasonBanner from './ReasonBanner.svelte';
import RemoteWorkspaceStatusBadge from './RemoteWorkspaceStatusBadge.svelte';
import RemoteWorkspaceStatusView from './RemoteWorkspaceStatusView.svelte';
import { getCommitPrefillFromReviewComments } from './commitSessionPrefill';
import { alerts } from '../../shared/alerts.svelte';

interface Props {
Expand Down Expand Up @@ -236,7 +237,7 @@
const latest = all[0];

if (latest.kind === 'review' && latest.commentCount > 0) {
return 'Resolve code review comments';
return getCommitPrefillFromReviewComments(latest.commentCount);
}
if (latest.kind === 'note' && latest.title.toLowerCase().includes('plan')) {
return 'Implement plan';
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export function getCommitPrefillFromReviewComments(userVisibleCommentCount: number): string {
return userVisibleCommentCount > 0 ? 'Resolve code review comments' : '';
}
Loading