返回 DeepSeek-Reasonix
pendingFollowup.ts
根目录 / desktop / frontend / src / lib / pendingFollowup.ts
1 import type { AppBindings } from "./bridge";
2 import type { InvocationRequest, StructuredInvocationSubmit } from "./invocationDisplay";
3
4 export type InboxTarget = { tabId: string; sessionPath: string; generation: number; selection: number; remote: boolean; hostId?: string; workspace?: string };
5 export type FollowupReceipt = { itemId: string; disposition: string; position: number; paused: boolean; idempotent?: boolean; error?: string };
6 export interface FollowupBindings {
7 EnqueueInboxFollowup(tabID: string, display: string, submit: string, idempotency: string): Promise<FollowupReceipt>;
8 EnqueueInboxFollowupWithInvocations(tabID: string, display: string, submit: string, invocations: InvocationRequest[], idempotency: string): Promise<FollowupReceipt>;
9 CaptureInboxTarget?(tabID: string, expectedPath: string): Promise<InboxTarget>;
10 EnqueueInboxFollowupForTarget?(target: InboxTarget, display: string, submit: string, invocations: InvocationRequest[], key: string): Promise<FollowupReceipt>;
11 LookupInboxFollowupForTarget?(target: InboxTarget, key: string): Promise<FollowupReceipt>;
12 }
13 export type PendingFollowup = {
14 key: string; target?: InboxTarget; tabId: string; display: string; submit: string;
15 structured?: StructuredInvocationSubmit; draft: string;
16 };
17
18 // TabMeta supplies the backend's session path. UI topic/draft keys and tab
19 // generations are deliberately excluded from this durable request identity.
20 export function followupSessionKey(sessionPath?: string, hostId?: string, workspace?: string): string {
21 const path = sessionPath?.trim();
22 if (!path || (hostId && !workspace)) return "";
23 return JSON.stringify(hostId ? ["remote", hostId, workspace, path] : ["local", path]);
24 }
25
26 // Process-local draft ownership survives Composer remounts. Nothing here is
27 // inferred from the currently executing turn, and unresolved entries never expire.
28 const requests = new Map<string, PendingFollowup>();
29 const listeners = new Set<() => void>();
30 export const pendingFollowups = {
31 get: (draft: string) => requests.get(draft),
32 subscribe: (listener: () => void) => { listeners.add(listener); return () => { listeners.delete(listener); }; },
33 set(draft: string, request: PendingFollowup) { requests.set(draft, request); listeners.forEach(listener => listener()); },
34 clear(draft: string, request: PendingFollowup) {
35 if (requests.get(draft) !== request) return;
36 requests.delete(draft); listeners.forEach(listener => listener());
37 },
38 };
39
40 export async function confirmFollowup(binding: AppBindings, request: PendingFollowup): Promise<FollowupReceipt> {
41 if (!request.target || !binding.LookupInboxFollowupForTarget) throw new Error("Follow-up receipt lookup unavailable");
42 const receipt = await binding.LookupInboxFollowupForTarget(request.target, request.key);
43 if (!receipt?.itemId || receipt.error) throw new Error(receipt?.error || "Follow-up receipt unconfirmed");
44 return receipt;
45 }
46
47 export function followupNotSubmitted(error: unknown): boolean {
48 return /reasonix_error:(inbox_not_submitted|inbox_capacity_items|inbox_capacity_bytes|inbox_item_too_large|inbox_empty|inbox_schema_readonly|channel_read_only)(?:$|\b)/.test(String(error));
49 }
50
50 lines TYPESCRIPT