| 1 | import type { AppBindings } from "./bridge"; |
| 2 | import type { StructuredInvocationSubmit } from "./invocationDisplay"; |
| 3 | import type { ComposerTarget } from "../generated/desktopContract.generated"; |
| 4 | import type { WorkspaceReference } from "./composerDraftTypes"; |
| 5 | |
| 6 | export async function restoreExternalFolderReferences(app: AppBindings, target: ComposerTarget, refs: WorkspaceReference[]) { |
| 7 | const external = refs.filter(ref => ref.isDir && ref.path.startsWith("__reasonix_external_folder/")); |
| 8 | if (!external.length) return; |
| 9 | const token = await captureAttachmentTarget(app, target, ["AttachDroppedForTarget"]); |
| 10 | try { |
| 11 | for (const ref of external) { |
| 12 | if (!ref.displayPath) throw new Error("External folder source is unavailable; reattach the folder before sending"); |
| 13 | const restored = await app.AttachDroppedForTarget!(token, ref.displayPath); |
| 14 | if (restored.kind !== "workspace" || !restored.isDir || restored.path !== ref.path) { |
| 15 | throw new Error("External folder changed; reattach the folder before sending"); |
| 16 | } |
| 17 | } |
| 18 | } finally { await app.ReleaseAttachmentTarget?.(token); } |
| 19 | } |
| 20 | |
| 21 | const pendingImageSubmissions = new Map<string, { fingerprint: string; id: string }>(); |
| 22 | |
| 23 | export function imageSubmissionIdentity(draft: string, fingerprint: string): string { |
| 24 | const previous = pendingImageSubmissions.get(draft); |
| 25 | if (previous?.fingerprint === fingerprint) return previous.id; |
| 26 | const id = `image-${crypto.randomUUID()}`; |
| 27 | pendingImageSubmissions.set(draft, { fingerprint, id }); |
| 28 | return id; |
| 29 | } |
| 30 | |
| 31 | export function settleImageSubmission(draft: string, id?: string): void { |
| 32 | if (pendingImageSubmissions.get(draft)?.id === id) pendingImageSubmissions.delete(draft); |
| 33 | } |
| 34 | |
| 35 | export async function prepareImageSubmission( |
| 36 | app: AppBindings, |
| 37 | target: ComposerTarget, |
| 38 | draftKey: string, |
| 39 | fingerprint: string, |
| 40 | attachments: Array<{ draftId?: string; clientAttachmentId?: string; recoveryPath?: string; displayName?: string }>, |
| 41 | structured: StructuredInvocationSubmit | undefined, |
| 42 | display: string, |
| 43 | input: string, |
| 44 | ) { |
| 45 | const token = await captureImageTarget(app, target); |
| 46 | const submissionId = imageSubmissionIdentity(draftKey, fingerprint); |
| 47 | const restored = []; |
| 48 | try { |
| 49 | for (const [index, item] of attachments.entries()) { |
| 50 | let draftId = item.draftId; |
| 51 | if (!draftId && item.recoveryPath) { |
| 52 | if (!app.AttachmentDataURLForTarget || !app.StageImageForTarget) throw new Error("unsupported: attachments-v2"); |
| 53 | const data = await app.AttachmentDataURLForTarget(token, item.recoveryPath); |
| 54 | const staged = await app.StageImageForTarget(token, `${submissionId}:restore:${index}`, item.displayName || "image", "", data); |
| 55 | draftId = staged.draftId; |
| 56 | } |
| 57 | if (!draftId) throw new Error("Image source is unavailable; reattach the image before sending"); |
| 58 | restored.push({ clientAttachmentId: item.clientAttachmentId || `image-${index + 1}`, draftId }); |
| 59 | } |
| 60 | } catch (error) { |
| 61 | await app.ReleaseAttachmentTarget?.(token).catch(() => {}); |
| 62 | throw error; |
| 63 | } |
| 64 | return { |
| 65 | token, |
| 66 | submissionId, |
| 67 | structured: { |
| 68 | display: structured?.display ?? display, |
| 69 | input: structured?.input ?? input, |
| 70 | invocations: structured?.invocations ?? [], |
| 71 | attachmentTarget: token, |
| 72 | attachmentSubmissionId: submissionId, |
| 73 | attachments: restored, |
| 74 | } satisfies StructuredInvocationSubmit, |
| 75 | }; |
| 76 | } |
| 77 | |
| 78 | export function submitAttachmentTurn(app: AppBindings, submissionId: string, structured: StructuredInvocationSubmit, original: string, initialGoal?: { goal: string; toolApprovalMode?: string }) { |
| 79 | if (!app.StartTurnForAttachmentTarget || !structured.attachmentTarget) throw new Error("unsupported: attachments-v2"); |
| 80 | return app.StartTurnForAttachmentTarget(structured.attachmentTarget, submissionId, { |
| 81 | input: structured.input, display: structured.display, original, |
| 82 | goal: initialGoal?.goal, toolApprovalMode: initialGoal?.toolApprovalMode, |
| 83 | invocations: structured.invocations, attachments: structured.attachments ?? [], |
| 84 | }); |
| 85 | } |
| 86 | |
| 87 | export function readFileAsDataURL(file: File): Promise<string> { |
| 88 | return new Promise((resolve, reject) => { |
| 89 | const reader = new FileReader(); |
| 90 | reader.onload = () => resolve(String(reader.result)); |
| 91 | reader.onerror = () => reject(reader.error); |
| 92 | reader.readAsDataURL(file); |
| 93 | }); |
| 94 | } |
| 95 | |
| 96 | export async function captureImageTarget(app: AppBindings, target: ComposerTarget): Promise<string> { |
| 97 | return captureAttachmentTarget(app, target, target.kind === "draft" |
| 98 | ? ["StageImageForTarget", "AttachmentDataURLForTarget"] |
| 99 | : ["StageImageForTarget", "ReadDraftImageForTarget"]); |
| 100 | } |
| 101 | |
| 102 | export async function captureAttachmentTarget( |
| 103 | app: AppBindings, |
| 104 | target: ComposerTarget, |
| 105 | required: ReadonlyArray<keyof AppBindings>, |
| 106 | ): Promise<string> { |
| 107 | if (!app.CaptureAttachmentTarget || required.some((name) => typeof app[name] !== "function")) { |
| 108 | throw new Error("unsupported: attachments-v2"); |
| 109 | } |
| 110 | const captured = await app.CaptureAttachmentTarget(target); |
| 111 | if (!captured.capabilities.includes("attachments-v2")) throw new Error("unsupported: attachments-v2"); |
| 112 | return captured.token; |
| 113 | } |
| 114 | |
| 115 | export async function stageImageFile(app: AppBindings, target: string, draftKey: string, file: File, persistentTarget?: ComposerTarget) { |
| 116 | const dataURL = await readFileAsDataURL(file); |
| 117 | const staged = await app.StageImageForTarget!(target, `${draftKey}:${file.name}:${file.lastModified}`, file.name, file.type, dataURL); |
| 118 | const path = staged.draftId ? `draft:${staged.draftId}` : staged.path; |
| 119 | if (!path) throw new Error("attachment staging returned no source"); |
| 120 | const previewUrl = staged.draftId |
| 121 | ? await app.ReadDraftImageForTarget!(target, staged.draftId) |
| 122 | : await app.AttachmentDataURLForTarget!(target, path); |
| 123 | const recoveryPath = persistentTarget && staged.draftId ? await app.SavePastedImageForComposerTarget(persistentTarget, dataURL) : undefined; |
| 124 | return { path, previewUrl, displayName: file.name, draftId: staged.draftId || undefined, recoveryPath, file }; |
| 125 | } |
| 126 |