Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
7c7104d
Add Default for bridge usage snapshots and shrink literals
Finesssee Oct 10, 2026
4813f5b
Drop tray_bridge tests duplicated in tray_presentation_tests
Finesssee Oct 10, 2026
5d48192
Simplify the provider refresh engine and share RFC 3339 parsing
Finesssee Oct 10, 2026
47c83c9
Collapse unreachable fetch-context arms and share browser cookie lookup
Finesssee Oct 10, 2026
0792536
Table-drive the build_fetch_context tests with one shared input helper
Finesssee Oct 10, 2026
3ee11f9
Build test fetch results with ProviderFetchResult::new
Finesssee Oct 10, 2026
72d5a10
Delete the PopOut-only positioning chain
Finesssee Oct 10, 2026
3578bc6
Drop the never-forced same-mode reopen path and test-only reveal pred…
Finesssee Oct 10, 2026
00f45e2
Remove the never-armed startup tray blur grace
Finesssee Oct 10, 2026
067b118
Remove the unused SurfaceTarget::parse
Finesssee Oct 10, 2026
1bed3c8
Add Rect helpers and safe_scale, and share the WindowGeometry impls
Finesssee Oct 10, 2026
2645a83
Build shell test monitors with one helper
Finesssee Oct 10, 2026
c23afc5
Remove the uninvoked float bar commands and claude_accounts_list regi…
Finesssee Oct 10, 2026
c43b6c6
Move inline shell test modules into their own files
Finesssee Oct 10, 2026
d760e8e
Split fetch-context building and notifications out of commands/provid…
Finesssee Oct 10, 2026
ab9c5be
Move the settings and bootstrap snapshots out of commands/bridge.rs
Finesssee Oct 10, 2026
74acd32
Reattach the usage-spend tests orphaned by a merge resolution
Finesssee Oct 10, 2026
dcd1974
Move the usage-spend summary builder out of commands/usage_spend.rs
Finesssee Oct 10, 2026
11b2a3a
Move the local-usage cache out of commands/chart.rs
Finesssee Oct 10, 2026
ffd755a
Move session capture and resume launch out of auto_resume.rs
Finesssee Oct 10, 2026
5cec5b1
Split commands/tests.rs into provider detail, cache and option submod…
Finesssee Oct 10, 2026
3ecd75b
Merge origin/main into simplify/shell-crate
Oct 17, 2026
aad238c
Merge origin/main into simplify/shell-crate
Oct 17, 2026
facb0e5
Merge remote-tracking branch 'origin/main' into pr796-merge
Oct 17, 2026
fdf32fa
Restore error snapshot defaults and drop duplicate cfg
Oct 17, 2026
3dcaf23
Merge remote-tracking branch 'origin/main' into pr796-merge
Oct 18, 2026
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
206 changes: 3 additions & 203 deletions apps/desktop-tauri/src-tauri/src/auto_resume.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ use crate::commands::{ProviderUsageSnapshot, RateWindowSnapshot};
use crate::state::AppState;
use tauri::Manager;

mod session_target;
use session_target::*;

const MAX_SESSION_ID_LEN: usize = 256;

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
Expand Down Expand Up @@ -459,158 +462,6 @@ fn begin_resume_attempt(
Some((target, operation))
}

async fn capture_session(
provider: ProviderId,
token_account_id: Option<uuid::Uuid>,
quota_source: ResumeSource,
) -> Option<ResumeTarget> {
// AgentSession has no account identity. A managed token account therefore
// cannot be positively correlated with a discovered process; do not arm
// a session that could belong to another account.
if token_account_id.is_some() {
return None;
}
let result = LocalAgentSessionScanner::default().scan().await;
if result.error.is_some() {
return None;
}
capture_target_from_sessions(provider, &result.sessions, token_account_id, quota_source)
}

fn capture_target_from_sessions(
provider: ProviderId,
sessions: &[AgentSession],
token_account_id: Option<uuid::Uuid>,
quota_source: ResumeSource,
) -> Option<ResumeTarget> {
if token_account_id.is_some() {
return None;
}
let mut candidates = sessions
.iter()
.filter_map(|session| target_from_active_session(provider, session, None, quota_source));
let target = candidates.next()?;
// Scanner ordering is an activity/display concern, not an account
// correlation. Multiple live candidates are ambiguous, so fail closed.
candidates.next().is_none().then_some(target)
}

fn target_from_active_session(
provider: ProviderId,
session: &AgentSession,
token_account_id: Option<uuid::Uuid>,
quota_source: ResumeSource,
) -> Option<ResumeTarget> {
(session.state == AgentSessionState::Active).then_some(())?;
session.pid?;
target_from_session(provider, session, token_account_id, quota_source)
}

fn target_from_session(
provider: ProviderId,
session: &AgentSession,
token_account_id: Option<uuid::Uuid>,
quota_source: ResumeSource,
) -> Option<ResumeTarget> {
let expected = match provider {
ProviderId::Codex => AgentSessionProvider::Codex,
ProviderId::Claude => AgentSessionProvider::Claude,
_ => return None,
};
if session.provider != expected
|| session.source != AgentSessionSource::Cli
|| !is_local_host(&session.host)
{
return None;
}
let session_id = valid_session_id(&session.id)?.to_string();
let cwd = canonical_workspace(session.workspace.cwd.as_deref())?;
let transcript_path = session
.transcript_path
.as_deref()
.and_then(canonical_transcript);
Some(ResumeTarget {
provider,
quota_source,
session_id,
cwd,
transcript_path,
token_account_id,
})
}

fn matching_session<'a>(
target: &ResumeTarget,
sessions: &'a [AgentSession],
) -> Option<&'a AgentSession> {
sessions.iter().find(|session| {
target_from_session(
target.provider,
session,
target.token_account_id,
target.quota_source,
)
.is_some_and(|candidate| {
candidate.provider == target.provider
&& candidate.session_id == target.session_id
&& candidate.cwd == target.cwd
})
})
}

fn is_local_host(host: &str) -> bool {
let host = host.trim();
if host.eq_ignore_ascii_case("localhost") {
return true;
}
std::env::var("COMPUTERNAME")
.ok()
.is_some_and(|local| !local.trim().is_empty() && local.trim().eq_ignore_ascii_case(host))
}

fn canonical_workspace(raw: Option<&str>) -> Option<PathBuf> {
let raw = raw?.trim();
if raw.is_empty() {
return None;
}
let path = Path::new(raw);
if !path.is_absolute() {
return None;
}
std::fs::canonicalize(path)
.ok()
.filter(|path| path.is_dir())
}

fn canonical_transcript(raw: &str) -> Option<PathBuf> {
let path = Path::new(raw.trim());
path.is_file()
.then(|| std::fs::canonicalize(path).ok())
.flatten()
}

fn transcript_is_still_present(target: &ResumeTarget) -> bool {
target.provider == ProviderId::Claude
&& target
.transcript_path
.as_ref()
.is_some_and(|path| path.is_file())
}

fn valid_session_id(raw: &str) -> Option<&str> {
let id = raw.trim();
if id.is_empty()
|| id.len() > MAX_SESSION_ID_LEN
|| id.starts_with("pid:")
|| !id
.chars()
.all(|ch| ch.is_ascii_alphanumeric() || matches!(ch, '-' | '_' | '.' | ':'))
{
return None;
}
Some(id)
}

async fn resume_captured_session(
app: &tauri::AppHandle,
target: ResumeTarget,
Expand Down Expand Up @@ -793,57 +644,6 @@ fn resume_generation_is_current(state: &AppState, provider: ProviderId, generati
== generation
}

fn launch_resume(target: &ResumeTarget) -> Result<(), String> {
let executable = match target.provider {
ProviderId::Codex => codexbar::codex_cli::locate_codex_binary(),
ProviderId::Claude => codexbar::providers::claude::locate_claude_binary(),
_ => None,
}
.ok_or_else(|| format!("{} CLI was not found", target.provider.display_name()))?;

let command = build_resume_command(target, executable)?;
// This runs after a background refresh, not a user action: the console
// starts minimized and inactive, so it waits on the taskbar instead of
// taking focus from the app the user is working in.
codexbar::host::console_launch::spawn_minimized_console(
&command.program,
&command.args,
&command.cwd,
)
.map_err(|error| {
format!(
"failed to launch {} CLI: {error}",
target.provider.display_name()
)
})
}

fn build_resume_command(
target: &ResumeTarget,
executable: PathBuf,
) -> Result<ResumeCommand, String> {
let session_id = valid_session_id(&target.session_id)
.ok_or_else(|| "captured session id is invalid".to_string())?;
if !target.cwd.is_absolute() {
return Err("captured session workspace must be absolute".to_string());
}

let cli_args = match target.provider {
ProviderId::Codex => vec!["resume".to_string(), session_id.to_string()],
ProviderId::Claude => vec!["--resume".to_string(), session_id.to_string()],
_ => return Err("provider does not support auto-resume".to_string()),
};

// Batch shims such as npm's `claude.cmd` stay the program here;
// `spawn_minimized_console` runs them through cmd.exe with quoting cmd.exe
// parses.
Ok(ResumeCommand {
program: executable,
args: cli_args,
cwd: target.cwd.clone(),
})
}

#[cfg(test)]
#[path = "auto_resume_tests.rs"]
mod auto_resume_tests;
Loading