| 1 | import type { CollaborationMode, ToolApprovalMode } from "../lib/types"; |
| 2 | import type { StructuredInvocationSubmit } from "../lib/invocationDisplay"; |
| 3 | import type { SessionOperationAuthority, SessionResource } from "./useSessionOperations"; |
| 4 | |
| 5 | export type InitialGoal = { goal: string; collaborationMode: CollaborationMode; toolApprovalMode: ToolApprovalMode }; |
| 6 | export type SubmissionResource = { |
| 7 | target: SessionResource; remote: boolean; ready: boolean; unavailable: string; goalDraft: boolean; |
| 8 | collaboration: CollaborationMode; approval: ToolApprovalMode; |
| 9 | }; |
| 10 | export type Submission = { display: string; submit?: string; structured?: StructuredInvocationSubmit; initialGoal?: InitialGoal }; |
| 11 | export type SubmissionPorts = { |
| 12 | send(tab: string, display: string, submit?: string, structured?: StructuredInvocationSubmit, goal?: InitialGoal): Promise<void>; |
| 13 | clearUndo(tab: string): void; |
| 14 | setGoal(tab: string, goal: string, remote: boolean): Promise<void>; |
| 15 | patchGoal(tab: string, goal: string): void; |
| 16 | profile(tab: string, propagateError: boolean): Promise<boolean>; |
| 17 | }; |
| 18 | export type SubmissionInput = { |
| 19 | target: SessionResource; read(target: SessionResource): SubmissionResource; ports: SubmissionPorts; |
| 20 | request: { kind: "direct" | "composer"; content: Submission } | { kind: "goal"; goal: string }; |
| 21 | }; |
| 22 | |
| 23 | export function buildInitialGoalSubmission( |
| 24 | content: Submission, |
| 25 | collaborationMode: CollaborationMode, |
| 26 | toolApprovalMode: ToolApprovalMode, |
| 27 | ): Submission { |
| 28 | const display = content.display.trim(); |
| 29 | const submit = (content.submit ?? content.display).trim(); |
| 30 | return { |
| 31 | display, |
| 32 | submit: content.structured ? submit : `/goal ${submit}`, |
| 33 | structured: content.structured, |
| 34 | initialGoal: { goal: display, collaborationMode, toolApprovalMode }, |
| 35 | }; |
| 36 | } |
| 37 | const legacyFlags = new Set(["--research", "--auto-research", "--deep", "--simple", "--no-research"]); |
| 38 | export function goalCommand(input: string) { |
| 39 | const match = /^\/goal(?:\s+(.*))?$/.exec(input); |
| 40 | if (!match) return undefined; |
| 41 | const parts = (match[1] ?? "").trim().split(/\s+/).filter(Boolean); |
| 42 | const legacy = legacyFlags.has(parts[0]?.toLowerCase()); |
| 43 | while (legacyFlags.has(parts[0]?.toLowerCase())) parts.shift(); |
| 44 | const value = parts.join(" "); |
| 45 | const action = value.toLowerCase(); |
| 46 | return { value, legacy, activate: Boolean(value) && !["status", "clear", "off", "stop", "done", "pause", "resume"].includes(action), |
| 47 | clear: ["clear", "off", "stop", "done"].includes(action) }; |
| 48 | } |
| 49 | |
| 50 | async function applyGoal(input: SubmissionInput, goal: string, authority: SessionOperationAuthority) { |
| 51 | authority.checkpoint(); |
| 52 | await input.ports.setGoal(input.target.tabId, goal, input.read(input.target).remote); |
| 53 | authority.checkpoint(); |
| 54 | input.ports.patchGoal(input.target.tabId, goal); |
| 55 | } |
| 56 | |
| 57 | async function send(input: SubmissionInput, content: Submission, authority: SessionOperationAuthority) { |
| 58 | authority.checkpoint(); |
| 59 | const source = input.read(input.target); |
| 60 | if (!source.ready || source.unavailable) throw Error(source.unavailable); |
| 61 | input.ports.clearUndo(input.target.tabId); |
| 62 | await input.ports.send(input.target.tabId, content.display, content.submit, content.structured, content.initialGoal); |
| 63 | authority.checkpoint(); |
| 64 | } |
| 65 | |
| 66 | /** Only minimal source data survives awaits. No active-tab reads or render refs. */ |
| 67 | export async function executeSubmission(input: SubmissionInput, authority: SessionOperationAuthority): Promise<void> { |
| 68 | authority.checkpoint(); |
| 69 | if (input.request.kind === "goal") return applyGoal(input, input.request.goal.trim(), authority); |
| 70 | const { content } = input.request; |
| 71 | if (input.request.kind === "direct") return send(input, content, authority); |
| 72 | const source = input.read(input.target); |
| 73 | const display = content.display.trim(); |
| 74 | const submit = content.submit ?? content.display; |
| 75 | const command = goalCommand(display); |
| 76 | if (command) { |
| 77 | if (command.activate) { |
| 78 | if (command.legacy) input.ports.patchGoal(input.target.tabId, command.value); |
| 79 | else await applyGoal(input, command.value, authority); |
| 80 | } else if (command.clear) await applyGoal(input, "", authority); |
| 81 | authority.checkpoint(); |
| 82 | if (input.read(input.target).ready) await send(input, { display, submit: submit.trim() }, authority); |
| 83 | return; |
| 84 | } |
| 85 | if (!source.ready) return; |
| 86 | if (source.goalDraft) { |
| 87 | await send(input, buildInitialGoalSubmission( |
| 88 | { display, submit, structured: content.structured }, source.collaboration, source.approval, |
| 89 | ), authority); |
| 90 | authority.checkpoint(); |
| 91 | input.ports.patchGoal(input.target.tabId, display); |
| 92 | return; |
| 93 | } |
| 94 | if (!await input.ports.profile(input.target.tabId, false)) return; |
| 95 | authority.checkpoint(); |
| 96 | await send(input, { display, submit: submit.trim(), structured: content.structured }, authority); |
| 97 | } |
| 98 |