| 1 | import { useEffect, useMemo } from "react"; |
| 2 | import { useCommittedSlot, type CommittedSlot } from "./useCommittedSlot"; |
| 3 | import { useCommittedCommand } from "./useCommittedCommand"; |
| 4 | import { CommandCancelled } from "./commandOutcome"; |
| 5 | import { executeControllerModel, executeControllerProfile, type ControllerProfilePorts, type ControllerProfileResource } from "../app-runtime/controllerProfileOwner"; |
| 6 | import type { SessionResource, useSessionOperations } from "../app-runtime/useSessionOperations"; |
| 7 | |
| 8 | function bindProfileRead(slot: CommittedSlot<readonly ControllerProfileResource[]>) { |
| 9 | return (target: SessionResource): ControllerProfileResource => { |
| 10 | if (slot.phase !== "ready") throw new CommandCancelled(slot.phase === "disposed" ? "disposed" : "not-ready"); |
| 11 | const resource = slot.value?.find(value => value.target.tabId === target.tabId && value.target.sessionKey === target.sessionKey); |
| 12 | if (!resource) throw new CommandCancelled("superseded"); |
| 13 | return resource; |
| 14 | }; |
| 15 | } |
| 16 | |
| 17 | export function useControllerProfileCommands(options: { |
| 18 | target: SessionResource; profiles: readonly ControllerProfileResource[]; ready: boolean; remote: boolean; runtimeEpoch?: string; |
| 19 | ports: ControllerProfilePorts; remoteModel(name: string): Promise<void>; |
| 20 | operations: ReturnType<typeof useSessionOperations>; report(error: unknown): void; |
| 21 | }) { |
| 22 | const { target, profiles, ready, remote, runtimeEpoch, operations, ports, remoteModel, report } = options; |
| 23 | const slot = useCommittedSlot(profiles); |
| 24 | const read = useMemo(() => bindProfileRead(slot), [slot]); |
| 25 | const restore = useCommittedCommand(async (source: SessionResource): Promise<boolean> => { |
| 26 | const result = await operations(source, "controller-profile", { target: source, read, ports }, executeControllerProfile); |
| 27 | if (result.status === "failed") throw result.error; |
| 28 | return result.status === "completed" && result.value; |
| 29 | }); |
| 30 | const applyProfile = useCommittedCommand(async (tabId = target.tabId, propagateError = true): Promise<boolean> => { |
| 31 | const source = profiles.find(value => value.target.tabId === tabId); |
| 32 | if (!source) return false; |
| 33 | try { return await restore(source.target); } catch (error) { |
| 34 | if (propagateError) throw error; |
| 35 | return false; |
| 36 | } |
| 37 | }); |
| 38 | const switchModel = useCommittedCommand(async (name: string, tabId = target.tabId): Promise<boolean> => { |
| 39 | const source = profiles.find(value => value.target.tabId === tabId); |
| 40 | if (!source || (source.remote && tabId !== target.tabId)) return false; |
| 41 | const result = await operations(source.target, "model", { target: source.target, read, ports, restore, |
| 42 | name, remote: source.remote ? remoteModel : undefined }, executeControllerModel); |
| 43 | if (result.status === "failed") throw result.error; |
| 44 | return result.status === "completed" && result.value; |
| 45 | }); |
| 46 | const reportError = useCommittedCommand(report); |
| 47 | const switchModelFromUi = useCommittedCommand(async (name: string): Promise<boolean> => { |
| 48 | try { return await switchModel(name); } catch (error) { reportError(error); return false; } |
| 49 | }); |
| 50 | const active = profiles.find(value => value.target.tabId === target.tabId)?.profile; |
| 51 | useEffect(() => { |
| 52 | if (ready && target.tabId && !remote) void applyProfile().catch(reportError); |
| 53 | }, [ready, remote, runtimeEpoch, target.tabId, target.sessionKey, active?.collaboration, active?.approval, active?.goal, applyProfile, reportError]); |
| 54 | return { applyProfile, switchModel, switchModelFromUi }; |
| 55 | } |
| 56 |