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.
|
/// bound port). Stopped on every terminal transition.
|
||||||
private var agentLoginListener: OAuthRedirectListener?
|
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):
|
/// 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.
|
/// shown as a red bubble at the bottom of the screen, auto-dismissed after a few seconds.
|
||||||
struct LastError: Equatable, Identifiable {
|
struct LastError: Equatable, Identifiable {
|
||||||
@@ -2002,6 +2012,13 @@ final class RemoteStore: ObservableObject {
|
|||||||
.sorted { $0.1 < $1.1 }
|
.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 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
|
/// 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
|
/// `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?.stop()
|
||||||
agentLoginListener = nil
|
agentLoginListener = nil
|
||||||
agentLoginRequestID = 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)
|
agentLogin = .done(success: result.succeeded, message: result.error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -359,21 +359,23 @@ enum AgentAuthErrors {
|
|||||||
&& lowered.contains("expired")
|
&& lowered.contains("expired")
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Whether the tail of a transcript ended on an auth failure — the banner's trigger. Only
|
/// The seq of the auth failure the transcript's tail ended on, or nil if it didn't — the
|
||||||
/// the events after the last completed run matter: a re-auth mid-history shouldn't nag.
|
/// banner's trigger. Only the events after the last completed run matter: a re-auth mid-history
|
||||||
static func transcriptNeedsLogin(_ events: [AgentEvent]) -> Bool {
|
/// 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() {
|
for event in events.suffix(30).reversed() {
|
||||||
switch event.kind {
|
switch event.kind {
|
||||||
case .runFinished(let finished):
|
case .runFinished(let finished):
|
||||||
guard finished.outcome == .errored else { return false }
|
guard finished.outcome == .errored else { return nil }
|
||||||
return finished.finalText.map(isAuthError) ?? false
|
return (finished.finalText.map(isAuthError) ?? false) ? event.seq : nil
|
||||||
case .error(let error):
|
case .error(let error):
|
||||||
if isAuthError(error.message) { return true }
|
if isAuthError(error.message) { return event.seq }
|
||||||
default:
|
default:
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -385,8 +387,13 @@ struct AgentAuthErrorBanner: View {
|
|||||||
let backend: BackendID
|
let backend: BackendID
|
||||||
|
|
||||||
var body: some View {
|
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,
|
if AgentLoginProvider.forBackend(backend) != nil,
|
||||||
AgentAuthErrors.transcriptNeedsLogin(store.openEvents)
|
let failSeq = AgentAuthErrors.authFailureSeq(store.openEvents),
|
||||||
|
failSeq > store.authResolvedSeq(forSession: sessionID)
|
||||||
{
|
{
|
||||||
HStack(spacing: 10) {
|
HStack(spacing: 10) {
|
||||||
Image(systemName: "key.fill")
|
Image(systemName: "key.fill")
|
||||||
|
|||||||
Reference in New Issue
Block a user