diff --git a/src/api.ts b/src/api.ts index dd39e5c..064d5df 100644 --- a/src/api.ts +++ b/src/api.ts @@ -3,7 +3,7 @@ import { auth } from './auth' import type { Project, Conversation, Message, Run, Artifact, HITLCard, HITLResponse, - AgentProfile, SettingDesc, StreamEvent, PlanSnapshot, TodoList, UploadInfo, PromptModule, + AgentProfile, SettingDesc, StreamEvent, StreamEventType, PlanSnapshot, TodoList, UploadInfo, PromptModule, ActivityCard, } from './contracts' @@ -118,6 +118,34 @@ export interface SSEHandlers { onError?: (err: Error) => void } +// 契约里每一种事件都要有监听。漏掉一种时 tsc 会报缺字段。 +const streamEventListeners: Record = { + 'message.delta': true, + 'thinking.delta': true, + 'tool.call.delta': true, + 'tool.call': true, + 'tool.output': true, + 'tool.result': true, + 'run.phase': true, + 'run.check': true, + 'hitl.request': true, + 'hitl.resolved': true, + 'plan.update': true, + 'todo.update': true, + 'agent.spawn': true, + 'agent.progress': true, + 'agent.result': true, + 'artifact.created': true, + 'artifact.updated': true, + 'run.completed': true, + 'run.failed': true, + 'error': true, + 'card.created': true, + 'card.updated': true, + 'card.status': true, + 'card.resolved': true, +} + export function connectEvents(conversationId: string, since: number, handlers: SSEHandlers, projectId?: string): { close: () => void } { let closed = false let es: EventSource | null = null @@ -137,10 +165,7 @@ export function connectEvents(conversationId: string, since: number, handlers: S } es.onmessage = ev => handle('message', ev.data) // 同时监听具名事件与默认(服务端 event: 字段)。 - const types = ['message.delta', 'thinking.delta', 'tool.call.delta', 'tool.call', 'tool.output', 'tool.result', 'run.phase', - 'hitl.request', 'hitl.resolved', 'plan.update', 'todo.update', 'agent.spawn', 'agent.progress', - 'agent.result', 'artifact.created', 'artifact.updated', 'run.completed', 'run.failed', 'error', - 'card.created', 'card.updated', 'card.status', 'card.resolved'] + const types = Object.keys(streamEventListeners) as StreamEventType[] for (const t of types) es.addEventListener(t, ev => handle(t, (ev as MessageEvent).data)) } diff --git a/src/components/Conversation.tsx b/src/components/Conversation.tsx index 3662403..0b2a214 100644 --- a/src/components/Conversation.tsx +++ b/src/components/Conversation.tsx @@ -7,7 +7,8 @@ import { marked } from 'marked' import { api } from '../api' import { store, useStore, activeRun, type LiveState, type ContextObs } from '../store' import { scrollRatio, scrollTopForRatio } from '../scrollsync' -import { hasSettledReply, liveToMessage, livePhaseLabel, showProcessInline } from '../live' +import { liveToMessage, livePhaseLabel, showProcessInline } from '../live' +import { deriveShellPhase, isShellBusy, showLiveBubble, type ShellPhase } from '../runstate' import type { Message, Part, HITLCard, ActivityCard, Artifact } from '../contracts' import { UIRenderer, InlineConfirmCard, hasOpenConfirm, CardAnchor } from '@ui' import { docBridge } from './Workbench' @@ -414,6 +415,7 @@ export function Conversation() { const activeHitl = useStore(s => s.activeHitl) const live = useStore(s => s.live) const runs = useStore(s => s.runs) + const posting = useStore(s => s.posting) const connected = useStore(s => s.connected) const artifacts = useStore(s => s.artifacts) const profiles = useStore(s => s.profiles) @@ -432,7 +434,14 @@ export function Conversation() { artifactCache = Object.fromEntries(artifacts.map(a => [a.id, a])) cardCache = Object.fromEntries(cards.map(c => [c.id, c])) - const active = activeRun(runs) + const phase: ShellPhase = deriveShellPhase({ + posting, + openRun: !!activeRun(runs), + liveRunId: live?.runId ?? null, + assistantRunIds: messages.filter(m => m.role === 'assistant' && m.runId).map(m => m.runId as string), + }) + const busy = isShellBusy(phase) + const showStop = phase === 'running' useEffect(() => { if (!stickRef.current) return @@ -445,7 +454,7 @@ export function Conversation() { const ids = attachmentIds const names = attachmentNames setText(''); setAttachmentIds([]); setAttachmentNames([]) - if (active) { + if (busy) { store.enqueueOutbox(t, ids, names) return } @@ -499,8 +508,9 @@ export function Conversation() { onClick={() => currentConvId && store.setCrossFlags(currentConvId, { allowCrossRead: conversation?.allowCrossRead === false })}> 跨读{conversation?.allowCrossRead === false ? '·关' : '·开'} - - {active && } + + {!connected && 重连中} + {showStop && }
return })} - {live && !hasSettledReply(messages, live.runId) && + {showLiveBubble(live?.runId ?? null, messages.filter(m => m.role === 'assistant' && m.runId).map(m => m.runId as string)) && live && } {activeHitl.map(c => )} {messages.length === 0 &&
@@ -579,14 +589,14 @@ export function Conversation() { }} />