Merge nucleic/gentle-yarn-egret-2ljl into dev
This commit is contained in:
@@ -466,6 +466,16 @@ final class RemoteStore: ObservableObject {
|
||||
/// bound port). Stopped on every terminal transition.
|
||||
private var agentLoginListener: OAuthRedirectListener?
|
||||
|
||||
/// Per-session high-water seq below which a tail auth failure is considered resolved by a
|
||||
/// completed sign-in. Set to the open transcript's newest seq the moment an agent sign-in
|
||||
/// succeeds (`agentLoginResultReceived`), so the in-chat "Sign in" banner clears immediately —
|
||||
/// without waiting for a fresh run to push a non-error event onto the tail. A genuinely newer
|
||||
/// auth failure (a token that lapses again) carries a higher seq, so it re-arms the banner.
|
||||
/// Keyed by session because seqs are per-session; this is the *observable* dismissal signal,
|
||||
/// deliberately not tied to credential *presence* (an expired-but-present login still reads as
|
||||
/// "authenticated", so presence can't tell a stale token from a fresh sign-in).
|
||||
@Published private(set) var authResolvedSeqBySession: [SessionID: UInt64] = [:]
|
||||
|
||||
/// A transient host-reported error (the mobile echo of the Mac's last-error bubble):
|
||||
/// shown as a red bubble at the bottom of the screen, auto-dismissed after a few seconds.
|
||||
struct LastError: Equatable, Identifiable {
|
||||
@@ -2002,6 +2012,13 @@ final class RemoteStore: ObservableObject {
|
||||
.sorted { $0.1 < $1.1 }
|
||||
}
|
||||
|
||||
/// The seq at/below which `sessionID`'s tail auth failure has been resolved by a completed
|
||||
/// sign-in — 0 (nothing resolved yet) until an agent login succeeds while that session is open.
|
||||
/// The in-chat banner shows only for an auth failure *newer* than this.
|
||||
func authResolvedSeq(forSession sessionID: SessionID) -> UInt64 {
|
||||
authResolvedSeqBySession[sessionID] ?? 0
|
||||
}
|
||||
|
||||
/// Begin a provider sign-in brokered by `hostID`. Binds the redirect listener FIRST (the
|
||||
/// begin message carries the bound port): Codex's OAuth client only permits the fixed
|
||||
/// `localhost:1455`, Claude accepts any port — and no port at all falls back to Claude's
|
||||
@@ -2120,6 +2137,16 @@ final class RemoteStore: ObservableObject {
|
||||
agentLoginListener?.stop()
|
||||
agentLoginListener = nil
|
||||
agentLoginRequestID = nil
|
||||
// A successful sign-in resolves any auth failure already in the open transcript's tail:
|
||||
// anchor the dismissal at the newest seq so the in-chat banner clears now, and only a
|
||||
// *newer* failure re-arms it. Scoped to the open session whose provider we just signed in
|
||||
// — the in-chat "Sign in" CTA is always the open session's, and a provider mismatch (a
|
||||
// Settings-initiated login for a different provider) must not dismiss an unrelated banner.
|
||||
if result.succeeded, let sid = openSessionID,
|
||||
let backend = sessions.first(where: { $0.sessionID == sid })?.backend,
|
||||
AgentLoginProvider.forBackend(backend) == agentLoginProvider {
|
||||
authResolvedSeqBySession[sid] = openEvents.map(\.seq).max() ?? 0
|
||||
}
|
||||
agentLogin = .done(success: result.succeeded, message: result.error)
|
||||
}
|
||||
|
||||
|
||||
@@ -359,21 +359,23 @@ enum AgentAuthErrors {
|
||||
&& lowered.contains("expired")
|
||||
}
|
||||
|
||||
/// Whether the tail of a transcript ended on an auth failure — the banner's trigger. Only
|
||||
/// the events after the last completed run matter: a re-auth mid-history shouldn't nag.
|
||||
static func transcriptNeedsLogin(_ events: [AgentEvent]) -> Bool {
|
||||
/// The seq of the auth failure the transcript's tail ended on, or nil if it didn't — the
|
||||
/// banner's trigger. Only the events after the last completed run matter: a re-auth mid-history
|
||||
/// shouldn't nag. Returning the seq (not just a Bool) lets the banner suppress a failure a
|
||||
/// completed sign-in has already resolved while still re-arming for a *newer* one.
|
||||
static func authFailureSeq(_ events: [AgentEvent]) -> UInt64? {
|
||||
for event in events.suffix(30).reversed() {
|
||||
switch event.kind {
|
||||
case .runFinished(let finished):
|
||||
guard finished.outcome == .errored else { return false }
|
||||
return finished.finalText.map(isAuthError) ?? false
|
||||
guard finished.outcome == .errored else { return nil }
|
||||
return (finished.finalText.map(isAuthError) ?? false) ? event.seq : nil
|
||||
case .error(let error):
|
||||
if isAuthError(error.message) { return true }
|
||||
if isAuthError(error.message) { return event.seq }
|
||||
default:
|
||||
continue
|
||||
}
|
||||
}
|
||||
return false
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -385,8 +387,13 @@ struct AgentAuthErrorBanner: View {
|
||||
let backend: BackendID
|
||||
|
||||
var body: some View {
|
||||
// Show only while the transcript's tail still ends on an auth failure that a completed
|
||||
// sign-in hasn't already resolved. A successful sign-in anchors `authResolvedSeq` at the
|
||||
// tail's newest seq, so the banner clears the instant login finishes — no waiting for a
|
||||
// fresh run to rewrite the tail — while a genuinely newer failure (a higher seq) re-arms it.
|
||||
if AgentLoginProvider.forBackend(backend) != nil,
|
||||
AgentAuthErrors.transcriptNeedsLogin(store.openEvents)
|
||||
let failSeq = AgentAuthErrors.authFailureSeq(store.openEvents),
|
||||
failSeq > store.authResolvedSeq(forSession: sessionID)
|
||||
{
|
||||
HStack(spacing: 10) {
|
||||
Image(systemName: "key.fill")
|
||||
|
||||
Reference in New Issue
Block a user