返回 DeepSeek-Reasonix
pendingFollowup.ts
根目录 / desktop / frontend / src / lib / pendingFollowup.ts
1 import type { AppBindings } from "./bridge";
2 import type { InvocationRequest, StructuredInvocationSubmit } from "./invocationDisplay";
3 import { submissionOutcome } from "./modelApplication";
4
5 export type InboxTarget = { tabId: string; sessionPath: string; generation: number; selection: number; remote: boolean; hostId?: string; workspace?: string };
6 export type FollowupReceipt = { itemId: string; disposition: string; position: number; paused: boolean; idempotent?: boolean; error?: string };
7 export interface FollowupBindings {
8 EnqueueInboxFollowup(tabID: string, display: string, submit: string, idempotency: string): Promise<FollowupReceipt>;
9 EnqueueInboxFollowupWithInvocations(tabID: string, display: string, submit: string, invocations: InvocationRequest[], idempotency: string): Promise<FollowupReceipt>;
10 CaptureInboxTarget?(tabID: string, expectedPath: string): Promise<InboxTarget>;
11 EnqueueInboxFollowupForTarget?(target: InboxTarget, display: string, submit: string, invocations: InvocationRequest[], key: string): Promise<FollowupReceipt>;
12 LookupInboxFollowupForTarget?(target: InboxTarget, key: string): Promise<FollowupReceipt>;
13 }
14 export type PendingFollowup = {
15 key: string; target?: InboxTarget; tabId: string; display: string; submit: string;
16 structured?: StructuredInvocationSubmit; draft: string;
17 };
18
19 // TabMeta supplies the backend's session path. UI topic/draft keys and tab
20 // generations are deliberately excluded from this durable request identity.
21 export function followupSessionKey(sessionPath?: string, hostId?: string, workspace?: string): string {
22 const path = sessionPath?.trim();
23 if (!path || (hostId && !workspace)) return "";
24 return JSON.stringify(hostId ? ["remote", hostId, workspace, path] : ["local", path]);
25 }
26
27 // Process-local draft ownership survives Composer remounts. Nothing here is
28 // inferred from the currently executing turn, and unresolved entries never expire.
29 const requests = new Map<string, PendingFollowup>();
30 const listeners = new Set<() => void>();
31 export const pendingFollowups = {
32 get: (draft: string) => requests.get(draft),
33 subscribe: (listener: () => void) => { listeners.add(listener); return () => { listeners.delete(listener); }; },
34 set(draft: string, request: PendingFollowup) { requests.set(draft, request); listeners.forEach(listener => listener()); },
35 clear(draft: string, request: PendingFollowup) {
36 if (requests.get(draft) !== request) return;
37 requests.delete(draft); listeners.forEach(listener => listener());
38 },
39 };
40
41 export async function confirmFollowup(binding: AppBindings, request: PendingFollowup): Promise<FollowupReceipt> {
42 if (!request.target || !binding.LookupInboxFollowupForTarget) throw new Error("Follow-up receipt lookup unavailable");
43 const receipt = await binding.LookupInboxFollowupForTarget(request.target, request.key);
44 if (!receipt?.itemId || receipt.error) throw new Error(receipt?.error || "Follow-up receipt unconfirmed");
45 return receipt;
46 }
47
48 // A busy-window submit answers with the durable queue receipt: the desktop
49 // queued the message instead of starting a turn. The receipt rides the bridge
50 // error data so the composer can show the queue entry immediately.
51 export type QueuedFollowupOutcome = { itemId: string; disposition: string; position: number; paused: boolean };
52
53 export function queuedFollowupOutcome(error: unknown): QueuedFollowupOutcome | undefined {
54 const data = (error as { data?: { queuedFollowup?: unknown } } | undefined)?.data?.queuedFollowup;
55 if (!data || typeof data !== "object") return undefined;
56 const receipt = data as Partial<QueuedFollowupOutcome>;
57 if (typeof receipt.itemId !== "string" || receipt.itemId === "") return undefined;
58 return {
59 itemId: receipt.itemId,
60 disposition: typeof receipt.disposition === "string" && receipt.disposition ? receipt.disposition : "queued_followup",
61 position: typeof receipt.position === "number" ? receipt.position : 0,
62 paused: receipt.paused === true,
63 };
64 }
65
66 export function followupNotSubmitted(error: unknown): boolean {
67 const outcome = submissionOutcome(error);
68 if (outcome) return outcome === "not_accepted";
69 return /reasonix_error:(?:inbox_(?:not_submitted|capacity_items|capacity_bytes|item_too_large|empty|schema_readonly)|channel_read_only|workspace_start(?:ing|_failed)|image_attachment_unreadable)(?:$|\b)/.test(String(error));
70 }
71
71 lines TYPESCRIPT