Offer Claude sign-in from failed sessions

This commit is contained in:
iris-ai committed 2026-09-15 12:24:25 -04:00
1 parent 0be15adbee
commit f0661919bb
9 files changed
+140 -12

No files matched your search

@@ -192,6 +192,8 @@ sealed class SessionEvent {
*/
data class LimitReached(val resetsAt: Double?) : SessionEvent()
data class AuthenticationRequired(val message: String) : SessionEvent()
data class Error(val message: String) : SessionEvent()
/**
@@ -303,6 +305,8 @@ fun parseSeqEvent(json: String): SeqEvent {
SessionEvent.LimitReached(
if (body.has("resetsAt")) body.getDouble("resetsAt") else null
)
"authenticationRequired" ->
SessionEvent.AuthenticationRequired(body.getString("message"))
"error" -> SessionEvent.Error(body.getString("message"))
else -> SessionEvent.Unknown(type)
}
@@ -319,6 +323,19 @@ fun parseSeqEvent(json: String): SeqEvent {
*/
fun sessionWorking(state: String): Boolean = state == "running" || state == "compacting"
/** Whether the latest events still say this session needs an explicit provider login. */
internal fun authenticationPromptAfter(open: Boolean, event: SessionEvent): Boolean =
when (event) {
is SessionEvent.AuthenticationRequired -> true
// A later provider response proves an older authentication failure in a replayed page is
// no longer current. Without this, one old failure reopened sign-in after every later
// successful turn.
is SessionEvent.AssistantText,
is SessionEvent.AssistantTextFinal,
is SessionEvent.ToolStart -> false
else -> open
}
/**
* The context after [event], given what it was before.
*
@@ -387,6 +387,9 @@ fun SessionScreen(
var loadingHistory by remember { mutableStateOf(false) }
var historyError by remember(address, epoch) { mutableStateOf<String?>(null) }
var ready by remember { mutableStateOf(false) }
// The driver owns recognising provider prose; this screen responds only to its actionable
// event.
var loginOpen by remember(address) { mutableStateOf(false) }
// Replies parsed ahead of the rows that draw them; see [ParsedReplies].
val replies = remember(address) { ParsedReplies() }
// Keyed like everything else describing one transcript. `rememberLazyListState` saves through
@@ -535,6 +538,7 @@ fun SessionScreen(
}
status = event.state
}
if (!isSubagent) loginOpen = authenticationPromptAfter(loginOpen, event)
// In order, always: one late event recorded ahead of the backlog would fold a
// streamed delta into whatever row happened to be last by then.
// Read on the UI thread (the stream below marshals every frame here). This direct
@@ -2061,6 +2065,19 @@ fun SessionScreen(
)
}
}
if (loginOpen) {
ProviderLoginDialog(
settings = settings,
machineId = summary.machine,
machineName = summary.machineName,
provider = summary.provider,
onDismiss = { loginOpen = false },
onSignedIn = {
loginOpen = false
usageFeed?.refresh?.invoke()
},
)
}
if (settingsOpen) {
// Measured when the dialog opens rather than kept up to date: what the reader is being told
// is what pressing the button now would discard, and null until the walk of the directory
@@ -518,6 +518,8 @@ fun foldEvent(items: List<TranscriptItem>, entry: SeqEvent): List<TranscriptItem
is SessionEvent.MessageDropped -> items
is SessionEvent.Settings -> items
is SessionEvent.Status -> settleReply(items, event.state)
is SessionEvent.AuthenticationRequired ->
items + TranscriptItem.ErrorMsg(entry.seq, event.message)
is SessionEvent.Error -> items + TranscriptItem.ErrorMsg(entry.seq, event.message)
is SessionEvent.Image ->
// Under the call that produced it when there is one, and a row of its own when there is