import type { StoreApi } from 'zustand/vanilla'; import type { ChatError, ConnectionState, Conversation, ConversationLastMessageKind, Message, QuestionRequest, ApprovalRequestWithId, ToolCall, RecoveryStatus } from '../types.js'; import type { NormalizedTask } from '../tasks/types.js'; export interface ChatStoreState { connectionState: ConnectionState; connectionError: ChatError | null; reconnectAttempt: number; /** * Session-level recovery flag (#29). Set true on `session_recovering` server * event, cleared on `session_recovered`. Orthogonal to `connectionState`: * the WS can be perfectly connected while the server is replaying interrupted * conversation context. Decoupling these lets a missed `session_recovered` * (gateway bug, network blip) not strand the transport-status UI. */ isRecovering: boolean; sessionId: string | null; userId: string | null; provider: string | null; model: string | null; sessionMode: string | null; conversations: Conversation[]; currentConversationId: string | null; /** * Latest in-flight restore requestId (`${Date.now()}-${seq}` from * setCurrentConversationId side-effect). event-parser compares incoming * restore_complete/failed.requestId against this to filter stale responses * when the user rapidly switches conversations. null when no conversation * is selected or the last setCurrentConversationId fired with gatewayClient * disconnected (no restore was requested). */ latestRestoreRequestId: string | null; conversationsLoading: boolean; conversationsError: ChatError | null; /** 会话置顶能力位(spec I8),来自 session_ready.session.supportsConversationPin;仅由 setConversationPinSupported 写入。 */ supportsConversationPin: boolean; messages: Record; isStreaming: boolean; isThinking: boolean; currentChatError: ChatError | null; hasMoreHistory: Record; historyLoading: boolean; /** #161B: per-conversation question slot (was a single global slot). Keyed by * conversationId; '' holds legacy events the parser couldn't attribute to a * conversation (see event-parser.ts `?? activeConversationId ?? ''` fallback). */ pendingQuestions: Record; pendingApprovals: ApprovalRequestWithId[]; processingConversationIds: Set; tasks: NormalizedTask[]; subagentProgress: Record; recoveryStatus: Record; restoreFailureCount: Record; explicitFailureCount: Record; outstandingRestores: Record; restoringStartedAt: Record; } export interface ChatStoreActions { setConnectionState: (state: ConnectionState) => void; setConnectionError: (error: ChatError | null) => void; setReconnectAttempt: (n: number) => void; setRecovering: (recovering: boolean) => void; setSessionInfo: (info: { sessionId: string; userId: string; provider?: string; model?: string; mode?: string; }) => void; setConversations: (conversations: Conversation[]) => void; addConversation: (conversation: Conversation) => void; removeConversation: (conversationId: string) => void; mergeConversations: (conversations: Conversation[]) => void; setConversationPinSupported: (supported: boolean) => void; setConversationPinnedAt: (conversationId: string, pinnedAt: number | null) => void; updateConversationTitle: (conversationId: string, title: string) => void; updateConversationActivity: (conversationId: string, patch: { lastMessageAt: number; lastMessagePreview?: string; lastMessageKind?: ConversationLastMessageKind; messageCount?: number; }) => void; setCurrentConversationId: (id: string | null) => void; addMessage: (conversationId: string, message: Message) => void; /** * gw#1587:按消息 id(=client_msg_id,同一 UUID)更新投递状态。消息可能尚未定位到 * conv(跨 conv 扫描);只写消息级字段,绝不触 processingConversationIds(spec §5.4)。 */ setDeliveryState: (messageId: string, state: 'pending' | 'delivered' | 'failed', cause?: string) => void; createAssistantMessage: (conversationId: string) => void; updateMessage: (conversationId: string, messageId: string, updates: Partial) => void; appendContent: (conversationId: string, text: string) => void; appendReasoning: (conversationId: string, text: string) => void; addToolCall: (conversationId: string, toolCall: ToolCall) => void; updateToolCall: (conversationId: string, toolCallId: string, updates: Partial) => void; setStreaming: (streaming: boolean) => void; setThinking: (thinking: boolean) => void; setCurrentChatError: (error: ChatError | null) => void; prependMessages: (conversationId: string, messages: Message[]) => void; setHasMoreHistory: (conversationId: string, hasMore: boolean) => void; setHistoryLoading: (loading: boolean) => void; /** #161B: add-or-replace the slot for `question.conversationId` (same-conversation * REPLACE; other conversations' slots untouched). */ upsertPendingQuestion: (question: QuestionRequest) => void; /** #161B: remove whichever conversation's slot holds `requestId` (answer/dismiss * self-address by requestId, possibly not the current conversation). Miss = no-op. */ clearPendingQuestionByRequestId: (requestId: string) => void; addPendingApproval: (approval: ApprovalRequestWithId) => void; removePendingApproval: (requestId: string) => void; addProcessingConversation: (conversationId: string) => void; removeProcessingConversation: (conversationId: string) => void; clearProcessingConversations: () => void; /** * ac#917:把一个「本地以为还在跑、实际早已不在跑」的会话**就地收尾**,一次 `set()` 原子完成 * 三件事。这是 composer 解锁的**唯一**共用原语,两个调用方: * - **对账**(`session_ready` 带 `inFlightTurnConversationId` 时,服务端说没在跑的那些); * - **逃生口**(用户按停止键后 10 秒仍无终态,见 `useAbortWithEscapeHatch`)。 * * 三件事缺一不可 —— 少做任何一件,composer 看起来还是锁着的: * 1. **移出 `processingConversationIds`** —— 侧栏「新建对话」与跨会话发送闸读它; * 2. **settle 幻影 assistant 消息** —— `thinking` 一到就建好了那条 `status:'streaming'` * 的空消息,只清集合它还会一直转。收成 **`'error'` 而不是 `'complete'`**:这一轮并没有 * 确认结束,标 complete 会让一段可能残缺的正文**看起来是完整回复**,而 ac#670 的重拉 * 不会再覆盖它。 * 3. **复位全局 `isStreaming`/`isThinking`** —— 只在收尾的是**当前打开的会话**时动 * (门同 `finishMessage`:后台会话的收尾不该改全局 flag)。 * * ⚠️ 三件事都做成幂等:不在集合里 / 没有幻影消息 / flag 本就是 false 时各自跳过, * 整体是 no-op(返回空 patch,不churn store)。 */ settleStuckTurn: (conversationId: string) => void; registerRestoreAttempt(conversationId: string, requestId: string): void; recordRestoreFailure(conversationId: string, requestId?: string): void; recordRestoreSuccess(conversationIds: string[]): void; resolveOutstandingAsDropped(): void; _tryAutoRestore(conversationId: string): void; retryRestore(conversationId: string): void; /** Replace all tasks (used by B4 REST hydration). */ setTasks: (tasks: NormalizedTask[]) => void; /** * Add-or-replace a task row with status 'running'. * `conversationId` (COO_TASK_CONV_ENABLED) binds the task to its dedicated * conversation; absent when the gateway flag is off. */ upsertTask: (task: { id: string; name: string; startedAt: string; conversationId?: string; }) => void; /** * Set task to 'completed'. Upserts if id absent (a completion may arrive * before the corresponding task_started event). A `conversationId` on the * patch overrides; otherwise any value from task_started is preserved. */ completeTask: (id: string, patch: { completedAt: string; result: NormalizedTask['result']; conversationId?: string; }) => void; /** * Set task to 'failed'. Upserts if id absent. * Design choice: `failedAt` (WS delta field) is stored as `completedAt` so * the row always has a terminal timestamp for display ordering. The REST * WorkerTaskState type has no `failedAt` field. * `conversationId` semantics match completeTask. */ failTask: (id: string, patch: { failedAt: string; error: string; conversationId?: string; }) => void; /** * 写 agentId 的最新进度相。phase ∈ {done,failed} → 清键(done() 早于 * task_completed,写进去会造「空白卡」窗口)。gateway flag 关时无来源。 */ setSubagentProgress: (agentId: string, progress: { phase: string; detail?: string; }) => void; } export type ChatStore = ChatStoreState & ChatStoreActions; /** * Create a fresh Zustand store. Each ChatProvider instance gets its own store * — this ensures test isolation and supports multiple providers on a page. * * `opts.gatewayClient` (optional) wires setCurrentConversationId side-effect * for on-demand restore: when connected, switching conversation triggers * `gatewayClient.requestRestore(id, requestId)`. Callers that don't pass it * (legacy tests, standalone use) get pure state-update behavior. */ export declare function createChatStore(opts?: { gatewayClient?: { isConnected(): boolean; requestRestore(id: string, reqId?: string): boolean; }; }): StoreApi; //# sourceMappingURL=chat-store.d.ts.map