Why: The send button, stop button, and queued sends each looked at a different slice of run state, so a reconnect or a late assistant message could disagree. Evidence: A depth-4 walk of send, HTTP results, SSE, reconnect, conversation switch, and flush keeps busy aligned with an in-flight run, hides the live bubble once an assistant message exists, and keeps one user message per request id. Impact: The event listener list is exhaustive over the stream contract, including run.check. A disconnected stream shows 重连中. Refs: feature/runstate-shell Co-authored-by: Cursor <cursoragent@cursor.com>
40 lines
1.5 KiB
TypeScript
40 lines
1.5 KiB
TypeScript
// 产品壳运行态的唯一来源。按钮、停止、取消和排队发送都读这里,不再各自看 runs 或 live。
|
||
// sending:请求已发出、run 还没出现。running:有未结束的 run。
|
||
// settling:run 已结束,但这条 run 的助手消息还没落库。idle:可以发送下一条。
|
||
|
||
export type ShellPhase = 'sending' | 'running' | 'settling' | 'idle'
|
||
|
||
export interface ShellFacts {
|
||
posting: boolean
|
||
openRun: boolean
|
||
liveRunId: string | null
|
||
assistantRunIds: string[]
|
||
}
|
||
|
||
export function deriveShellPhase(facts: ShellFacts): ShellPhase {
|
||
const livePending = showLiveBubble(facts.liveRunId, facts.assistantRunIds)
|
||
if (facts.posting && !facts.openRun) return 'sending'
|
||
if (facts.openRun) return 'running'
|
||
if (livePending) return 'settling'
|
||
return 'idle'
|
||
}
|
||
|
||
export function isShellBusy(phase: ShellPhase): boolean {
|
||
return phase !== 'idle'
|
||
}
|
||
|
||
// 流式气泡只在这条 run 还没有助手消息时显示。用户消息上的同一个 runId 不算回复已落库。
|
||
export function showLiveBubble(liveRunId: string | null, assistantRunIds: string[]): boolean {
|
||
if (!liveRunId) return false
|
||
return !assistantRunIds.includes(liveRunId)
|
||
}
|
||
|
||
export function canFlushOutbox(phase: ShellPhase): boolean {
|
||
return phase === 'idle'
|
||
}
|
||
|
||
// 在途:正在发送、有未结束的 run,或实时气泡还在等助手消息。
|
||
export function hasInFlight(facts: ShellFacts): boolean {
|
||
return facts.posting || facts.openRun || showLiveBubble(facts.liveRunId, facts.assistantRunIds)
|
||
}
|