@skaile/forge-app-actions 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/README.md +34 -2
  2. package/dist/module.d.mts +23 -0
  3. package/dist/module.json +9 -0
  4. package/dist/module.mjs +37 -0
  5. package/dist/runtime/client-capabilities.d.ts +29 -0
  6. package/dist/runtime/client-capabilities.js +25 -0
  7. package/dist/runtime/components-nuxt-ui/ForgeCapabilityApproval.d.vue.ts +2 -0
  8. package/dist/runtime/components-nuxt-ui/ForgeCapabilityApproval.vue +125 -0
  9. package/dist/runtime/components-nuxt-ui/ForgeCapabilityApproval.vue.d.ts +2 -0
  10. package/dist/runtime/components-nuxt-ui/ForgeCommandPalette.d.vue.ts +2 -0
  11. package/dist/runtime/components-nuxt-ui/ForgeCommandPalette.vue +218 -0
  12. package/dist/runtime/components-nuxt-ui/ForgeCommandPalette.vue.d.ts +2 -0
  13. package/dist/runtime/composables/defineClientCapability.d.ts +64 -0
  14. package/dist/runtime/composables/defineClientCapability.js +40 -0
  15. package/dist/runtime/composables/defineForgeAction.d.ts +82 -0
  16. package/dist/runtime/composables/defineForgeAction.js +147 -0
  17. package/dist/runtime/composables/useAgentCapabilities.d.ts +78 -0
  18. package/dist/runtime/composables/useAgentCapabilities.js +94 -0
  19. package/dist/runtime/composables/useCapabilityApproval.d.ts +109 -0
  20. package/dist/runtime/composables/useCapabilityApproval.js +74 -0
  21. package/dist/runtime/composables/useCapabilityInvocations.d.ts +150 -0
  22. package/dist/runtime/composables/useCapabilityInvocations.js +129 -0
  23. package/dist/runtime/composables/useCommandPalette.d.ts +28 -0
  24. package/dist/runtime/composables/useCommandPalette.js +49 -0
  25. package/dist/runtime/composables/usePaletteArgStep.d.ts +47 -0
  26. package/dist/runtime/composables/usePaletteArgStep.js +51 -0
  27. package/dist/runtime/server/index.d.ts +7 -0
  28. package/dist/runtime/server/index.js +2 -0
  29. package/dist/runtime/server/utils/capability-registry.d.ts +35 -0
  30. package/dist/runtime/server/utils/capability-registry.js +24 -0
  31. package/dist/runtime/server/utils/client-capability-bridge.d.ts +647 -0
  32. package/dist/runtime/server/utils/client-capability-bridge.js +755 -0
  33. package/dist/runtime/shims-vue.d.ts +8 -0
  34. package/dist/runtime/types/client-capability.d.ts +159 -0
  35. package/dist/runtime/types/client-capability.js +0 -0
  36. package/dist/runtime/types/command-palette.d.ts +75 -0
  37. package/dist/runtime/types/command-palette.js +0 -0
  38. package/dist/runtime/types/forge-action.d.ts +152 -0
  39. package/dist/runtime/types/forge-action.js +0 -0
  40. package/dist/runtime/types/index.d.ts +3 -0
  41. package/dist/runtime/types/index.js +0 -0
  42. package/dist/types.d.mts +9 -0
  43. package/package.json +81 -4
  44. package/src/runtime/client-capabilities.ts +82 -0
  45. package/src/runtime/components-nuxt-ui/ForgeCapabilityApproval.vue +240 -0
  46. package/src/runtime/components-nuxt-ui/ForgeCommandPalette.vue +363 -0
  47. package/src/runtime/composables/defineClientCapability.ts +119 -0
  48. package/src/runtime/composables/defineForgeAction.ts +278 -0
  49. package/src/runtime/composables/useAgentCapabilities.ts +215 -0
  50. package/src/runtime/composables/useCapabilityApproval.ts +219 -0
  51. package/src/runtime/composables/useCapabilityInvocations.ts +343 -0
  52. package/src/runtime/composables/useCommandPalette.ts +97 -0
  53. package/src/runtime/composables/usePaletteArgStep.ts +110 -0
  54. package/src/runtime/server/index.ts +8 -0
  55. package/src/runtime/server/utils/capability-registry.ts +66 -0
  56. package/src/runtime/server/utils/client-capability-bridge.ts +1201 -0
  57. package/src/runtime/shims-vue.d.ts +8 -0
  58. package/src/runtime/types/client-capability.ts +178 -0
  59. package/src/runtime/types/command-palette.ts +78 -0
  60. package/src/runtime/types/forge-action.ts +166 -0
  61. package/src/runtime/types/index.ts +24 -0
@@ -0,0 +1,82 @@
1
+ /**
2
+ * `defineForgeAction` — one declaration for Cmd+K and the agent.
3
+ *
4
+ * Spec: `docs/superpowers/specs/2026-08-22-forge-app-channel-design.md` §2.3,
5
+ * which adopts the platform's rule verbatim — *"All user-facing functionality is
6
+ * a command-palette action; register it so it is discoverable via Cmd+K **and**
7
+ * usable by the AI agent"*.
8
+ *
9
+ * ```ts
10
+ * export const openFile = defineForgeAction({
11
+ * id: "workspace.open_file", // → capability `app.workspace.open_file`
12
+ * label: "Open file…",
13
+ * description: "Open a file in the user's file viewer.",
14
+ * input: z.object({ path: z.string().min(1) }),
15
+ * arg: { placeholder: "path/to/file", toInput: (path) => ({ path }) },
16
+ * run: async ({ path }) => { await useFileViewer().openFile(path); return { opened: true, path } },
17
+ * })
18
+ * ```
19
+ *
20
+ * `audience` defaults to `['user', 'llm']`. That default is the whole point:
21
+ * a new UI feature is agent-controllable unless its author opts out, instead of
22
+ * agent-controllability being a bespoke per-feature integration.
23
+ *
24
+ * ## No third registry
25
+ *
26
+ * This does not store anything. It *projects* one declaration into the two
27
+ * registries forge already had — `useCommandPalette()` (idempotent by id) and
28
+ * `useAgentCapabilities()` (idempotent by name) — and `useForgeActions()`
29
+ * writes through to both. Those remain the single sources of truth for "what is
30
+ * in the palette" and "what may the agent call", so nothing can drift.
31
+ *
32
+ * ## The id/name seam
33
+ *
34
+ * The palette has always used bare ids (`workspace.open_file`); the platform
35
+ * **rejects** capability registrations outside the `app.` namespace (spec §2.2).
36
+ * The rule is therefore: **author the palette id, and the capability name is
37
+ * `app.<id>`.** An id that already carries the prefix is a mistake and throws,
38
+ * because otherwise `app.app.workspace.open_file` would ship silently.
39
+ */
40
+ import * as z from "zod";
41
+ import type { DefineForgeActionArgs, ForgeAction, ForgeActionAudience } from "../types/forge-action.js";
42
+ /** The default when `audience` is omitted: reachable from both surfaces. */
43
+ export declare const DEFAULT_FORGE_ACTION_AUDIENCE: ForgeActionAudience[];
44
+ /**
45
+ * Build a {@link ForgeAction} from one declaration.
46
+ *
47
+ * Throws — at define time, i.e. on the first import in dev — for the four
48
+ * declarations that could only fail later and further away:
49
+ * - an `id` that already starts with `app.` (see the id/name seam above),
50
+ * - an llm-audience action with no `description` (the model reads nothing),
51
+ * - an llm-audience `id` that is not `<sector>.<verb>` (spec §2.2's namespace),
52
+ * - a user-audience action whose input needs arguments but declares no `arg`
53
+ * (a palette row that exists only to fail on Enter).
54
+ */
55
+ export declare function defineForgeAction<T extends z.ZodType = z.ZodObject<Record<string, never>>>(args: DefineForgeActionArgs<T>): ForgeAction;
56
+ export interface UseForgeActionsReturn {
57
+ /**
58
+ * Write one action through to the registries its audience names. Idempotent:
59
+ * the palette replaces by id and the capability registry replaces by name,
60
+ * exactly as they already did, so re-registering on HMR or a remount is a
61
+ * no-op rather than a duplicate.
62
+ */
63
+ register(action: ForgeAction): ForgeAction;
64
+ /** Register several, in order. */
65
+ registerAll(actions: ForgeAction[]): void;
66
+ /**
67
+ * Remove an action from both registries, aliases included. Takes the action
68
+ * itself rather than an id — the aliases it owns cannot be derived from one.
69
+ */
70
+ unregister(action: ForgeAction): void;
71
+ }
72
+ /**
73
+ * Register forge actions into the palette and the agent capability registry.
74
+ *
75
+ * ```ts
76
+ * export default defineNuxtPlugin(() => {
77
+ * useForgeActions().registerAll([openFile, toggleSidebar])
78
+ * // then announce upward once — see the app-channel wiring
79
+ * })
80
+ * ```
81
+ */
82
+ export declare function useForgeActions(): UseForgeActionsReturn;
@@ -0,0 +1,147 @@
1
+ import * as z from "zod";
2
+ import { defineClientCapability, deprecatedClientCapabilityAlias } from "./defineClientCapability.js";
3
+ import { useAgentCapabilities } from "./useAgentCapabilities.js";
4
+ import { useCommandPalette } from "./useCommandPalette.js";
5
+ export const DEFAULT_FORGE_ACTION_AUDIENCE = ["user", "llm"];
6
+ const ID_PATTERN = /^[a-z0-9][a-z0-9_-]*(?:\.[a-z0-9][a-z0-9_-]*)+$/;
7
+ const defaultLog = {
8
+ info: (message, ...args) => console.log(message, ...args),
9
+ warn: (message, ...args) => console.warn(message, ...args),
10
+ error: (message, ...args) => console.error(message, ...args)
11
+ };
12
+ function fail(id, message) {
13
+ throw new Error(`[defineForgeAction] ${id}: ${message}`);
14
+ }
15
+ export function defineForgeAction(args) {
16
+ const { id } = args;
17
+ if (!id || id.trim().length === 0) fail("<empty>", "an action needs a non-empty id.");
18
+ const audience = args.audience ?? DEFAULT_FORGE_ACTION_AUDIENCE;
19
+ if (audience.length === 0) {
20
+ fail(id, "audience is empty \u2014 an action nobody can trigger is not an action.");
21
+ }
22
+ const forUser = audience.includes("user");
23
+ const forLlm = audience.includes("llm");
24
+ if (id.startsWith("app.")) {
25
+ fail(
26
+ id,
27
+ `the id is the PALETTE id and must not carry the \`app.\` prefix \u2014 it is added automatically to derive the capability name. Use "${id.slice(4)}".`
28
+ );
29
+ }
30
+ const name = args.name ?? `app.${id}`;
31
+ if (forLlm) {
32
+ if (!args.description) {
33
+ fail(id, "an action with audience 'llm' must declare a description \u2014 the model reads it.");
34
+ }
35
+ if (!args.name && !ID_PATTERN.test(id)) {
36
+ fail(
37
+ id,
38
+ `an agent-visible id must be lowercase \`<sector>.<verb>\` (spec \xA72.2), so the derived capability name lands in the platform's \`app.\` namespace as \`app.${id}\`.`
39
+ );
40
+ }
41
+ if (!name.startsWith("app.")) {
42
+ fail(
43
+ id,
44
+ `capability name "${name}" is outside the \`app.\` namespace, which the platform rejects.`
45
+ );
46
+ }
47
+ }
48
+ const inputZod = args.input ?? z.object({});
49
+ const acceptsNoArgs = inputZod.safeParse({}).success;
50
+ if (forUser && !acceptsNoArgs && !args.arg) {
51
+ fail(
52
+ id,
53
+ "the input schema requires arguments but no `arg` was declared, so the Cmd+K entry could never run. Declare `arg: { placeholder, toInput }` (and optionally `options` for a picker), or narrow the audience to ['llm']."
54
+ );
55
+ }
56
+ async function run(rawInput, ctxIn) {
57
+ const parsed = inputZod.parse(rawInput ?? {});
58
+ const ctx = {
59
+ source: ctxIn?.source ?? "user",
60
+ name: ctxIn?.name ?? name,
61
+ sessionKey: ctxIn?.sessionKey ?? "",
62
+ callId: ctxIn?.callId,
63
+ log: ctxIn?.log ?? defaultLog
64
+ };
65
+ return await args.run(parsed, ctx);
66
+ }
67
+ let paletteAction;
68
+ if (forUser) {
69
+ paletteAction = {
70
+ id,
71
+ label: args.label,
72
+ // The model's description is written to be exhaustive; a palette row is
73
+ // one line. `hint` is where an action visible on both surfaces says the
74
+ // short version.
75
+ description: args.hint ?? args.description,
76
+ icon: args.icon,
77
+ keywords: args.keywords,
78
+ group: args.group,
79
+ // `arg` carries `toInput`, which is this module's business, not the
80
+ // palette's — project down to the prompt half the component consumes.
81
+ arg: args.arg ? {
82
+ label: args.arg.label,
83
+ placeholder: args.arg.placeholder,
84
+ options: args.arg.options,
85
+ allowCustom: args.arg.allowCustom
86
+ } : void 0,
87
+ handler: async (value) => {
88
+ const input = args.arg ? args.arg.toInput(value ?? "") : {};
89
+ await run(input, { source: "user" });
90
+ }
91
+ };
92
+ }
93
+ const capability = forLlm ? defineClientCapability({
94
+ name,
95
+ displayName: args.label,
96
+ description: args.description,
97
+ input: inputZod,
98
+ output: args.output,
99
+ kind: args.kind,
100
+ audience,
101
+ scope: args.scope,
102
+ requiresApproval: args.requiresApproval,
103
+ promptFragment: args.promptFragment,
104
+ callTimeoutMs: args.callTimeoutMs,
105
+ render: args.render,
106
+ // Input arrives already parsed by `useCapabilityInvocations`; re-parsing
107
+ // here would only pay the cost twice.
108
+ handler: async (input, capCtx) => await args.run(input, {
109
+ source: "llm",
110
+ name: capCtx.name,
111
+ sessionKey: capCtx.sessionKey,
112
+ callId: capCtx.callId,
113
+ log: capCtx.log
114
+ })
115
+ }) : void 0;
116
+ const aliases = capability && args.deprecatedNames ? args.deprecatedNames.map((old) => deprecatedClientCapabilityAlias(capability, old)) : [];
117
+ return {
118
+ id,
119
+ name,
120
+ label: args.label,
121
+ audience,
122
+ inputZod,
123
+ paletteAction,
124
+ capability,
125
+ aliases,
126
+ run
127
+ };
128
+ }
129
+ export function useForgeActions() {
130
+ const palette = useCommandPalette();
131
+ const capabilities = useAgentCapabilities();
132
+ function register(action) {
133
+ if (action.paletteAction) palette.registerAction(action.paletteAction);
134
+ if (action.capability) capabilities.register(action.capability);
135
+ for (const alias of action.aliases) capabilities.register(alias);
136
+ return action;
137
+ }
138
+ function registerAll(actions) {
139
+ for (const action of actions) register(action);
140
+ }
141
+ function unregister(action) {
142
+ palette.unregisterAction(action.id);
143
+ capabilities.unregister(action.name);
144
+ for (const alias of action.aliases) capabilities.unregister(alias.name);
145
+ }
146
+ return { register, registerAll, unregister };
147
+ }
@@ -0,0 +1,78 @@
1
+ import { type ShallowRef } from "vue";
2
+ import type { ClientCapabilityDescriptor, DefinedClientCapability } from "../types/client-capability.js";
3
+ /** Minimal `fetch` shape the announce transport needs. Injectable for tests. */
4
+ export type CapabilityAnnounceFetcher = (url: string, init: {
5
+ method: string;
6
+ headers: Record<string, string>;
7
+ body: string;
8
+ }) => Promise<{
9
+ ok: boolean;
10
+ status: number;
11
+ }>;
12
+ export interface AnnounceOptions {
13
+ /**
14
+ * The upward POST endpoint, e.g. `/api/agent/capabilities`. The consuming app
15
+ * owns this route (spec §2) — forge-app-actions never assumes a path.
16
+ */
17
+ endpoint: string;
18
+ /**
19
+ * Extra fields merged into the JSON body. This is where the app supplies
20
+ * whatever identifies the session to its own route — a single-session app
21
+ * sends nothing; a multi-user app would send the parts of its
22
+ * `userId:slug:mode` key. Never send the derived key from the browser if the
23
+ * server can derive it itself.
24
+ */
25
+ body?: Record<string, unknown>;
26
+ /** Override the fetcher. Defaults to `globalThis.fetch`. */
27
+ fetcher?: CapabilityAnnounceFetcher;
28
+ }
29
+ export interface AnnounceResult {
30
+ ok: boolean;
31
+ /** How many capability descriptors were sent. `0` for a withdrawal. */
32
+ sent: number;
33
+ /** Present when `ok` is false. */
34
+ error?: string;
35
+ }
36
+ export interface UseAgentCapabilitiesReturn {
37
+ /** Register a capability. Idempotent by name — replaces an existing entry. */
38
+ register(capability: DefinedClientCapability): void;
39
+ /** Register several at once. Idempotent by name, in order. */
40
+ registerAll(capabilities: DefinedClientCapability[]): void;
41
+ /** Remove a previously registered capability. No-op if the name is unknown. */
42
+ unregister(name: string): void;
43
+ /** Look up one registered capability by name. */
44
+ get(name: string): DefinedClientCapability | undefined;
45
+ /** Snapshot of every registered capability (schemas and handlers included). */
46
+ list(): DefinedClientCapability[];
47
+ /** The JSON-serialisable projection announced upward. */
48
+ toWire(): ClientCapabilityDescriptor[];
49
+ /** Reactive list of registered capabilities. */
50
+ capabilities: Readonly<ShallowRef<DefinedClientCapability[]>>;
51
+ /** Stable per-tab token sent with every announcement. */
52
+ subscriberId: string;
53
+ /**
54
+ * POST the whole registry upward. Announcements are never deltas — the bridge
55
+ * replaces its stored list, so a reconnect re-announce is naturally idempotent
56
+ * (spec §L5).
57
+ *
58
+ * Never throws: a failed announce logs a warning and returns `ok: false`, so a
59
+ * dead route cannot break app mount.
60
+ */
61
+ announce(options: AnnounceOptions): Promise<AnnounceResult>;
62
+ /**
63
+ * POST an empty registry upward — the explicit withdrawal seam for spec §L1's
64
+ * "no browser, no capability". Ticket 05 adds the SSE-disconnect grace period
65
+ * that calls this from the server side; this is the voluntary client-side path.
66
+ */
67
+ withdraw(options: AnnounceOptions): Promise<AnnounceResult>;
68
+ /** Test-only: empty the singleton. Not for application code. */
69
+ reset(): void;
70
+ }
71
+ /**
72
+ * Browser-side agent capability registry.
73
+ *
74
+ * Components and plugins call `register()` to declare what the agent may invoke
75
+ * in this tab, then `announce()` once on mount so Nitro — and through it the
76
+ * agent's `capability_register` — reflects that a UI actually exists.
77
+ */
78
+ export declare function useAgentCapabilities(): UseAgentCapabilitiesReturn;
@@ -0,0 +1,94 @@
1
+ import { shallowRef } from "vue";
2
+ const _capabilities = shallowRef([]);
3
+ const _subscriberId = mintSubscriberId();
4
+ function mintSubscriberId() {
5
+ const c = globalThis.crypto;
6
+ if (typeof c?.randomUUID === "function") return c.randomUUID();
7
+ return `sub-${Math.random().toString(36).slice(2)}${Date.now().toString(36)}`;
8
+ }
9
+ function toDescriptor(capability) {
10
+ const {
11
+ inputZod: _inputZod,
12
+ outputZod: _outputZod,
13
+ handler: _handler,
14
+ ...descriptor
15
+ } = capability;
16
+ return descriptor;
17
+ }
18
+ async function post(options, payload) {
19
+ const sent = payload.capabilities.length;
20
+ const fetcher = options.fetcher ?? globalThis.fetch ?? void 0;
21
+ if (!fetcher) {
22
+ return { ok: false, sent, error: "no fetch implementation available" };
23
+ }
24
+ try {
25
+ const res = await fetcher(options.endpoint, {
26
+ method: "POST",
27
+ headers: { "content-type": "application/json" },
28
+ body: JSON.stringify({ ...options.body, ...payload })
29
+ });
30
+ if (!res.ok) {
31
+ const error = `announce failed: HTTP ${res.status}`;
32
+ console.warn(`[useAgentCapabilities] ${error}`);
33
+ return { ok: false, sent, error };
34
+ }
35
+ return { ok: true, sent };
36
+ } catch (err) {
37
+ const error = err instanceof Error ? err.message : String(err);
38
+ console.warn(`[useAgentCapabilities] announce failed: ${error}`);
39
+ return { ok: false, sent, error };
40
+ }
41
+ }
42
+ export function useAgentCapabilities() {
43
+ function register(capability) {
44
+ const idx = _capabilities.value.findIndex((c) => c.name === capability.name);
45
+ if (idx !== -1) {
46
+ _capabilities.value = [
47
+ ..._capabilities.value.slice(0, idx),
48
+ capability,
49
+ ..._capabilities.value.slice(idx + 1)
50
+ ];
51
+ } else {
52
+ _capabilities.value = [..._capabilities.value, capability];
53
+ }
54
+ }
55
+ function registerAll(capabilities) {
56
+ for (const capability of capabilities) register(capability);
57
+ }
58
+ function unregister(name) {
59
+ const idx = _capabilities.value.findIndex((c) => c.name === name);
60
+ if (idx === -1) return;
61
+ _capabilities.value = _capabilities.value.filter((c) => c.name !== name);
62
+ }
63
+ function get(name) {
64
+ return _capabilities.value.find((c) => c.name === name);
65
+ }
66
+ function list() {
67
+ return [..._capabilities.value];
68
+ }
69
+ function toWire() {
70
+ return _capabilities.value.map(toDescriptor);
71
+ }
72
+ function announce(options) {
73
+ return post(options, { subscriberId: _subscriberId, capabilities: toWire() });
74
+ }
75
+ function withdraw(options) {
76
+ return post(options, { subscriberId: _subscriberId, capabilities: [] });
77
+ }
78
+ function reset() {
79
+ _capabilities.value = [];
80
+ }
81
+ return {
82
+ register,
83
+ registerAll,
84
+ unregister,
85
+ get,
86
+ list,
87
+ toWire,
88
+ capabilities: _capabilities,
89
+ subscriberId: _subscriberId,
90
+ announce,
91
+ withdraw,
92
+ reset
93
+ };
94
+ }
@@ -0,0 +1,109 @@
1
+ /**
2
+ * The browser-side **approval gate** for agent-invoked capabilities
3
+ * (spec §3 L7).
4
+ *
5
+ * On the platform, `requiresApproval` is a gateway feature: the gateway parks
6
+ * the invocation, renders a confirm card, and only then forwards the call to
7
+ * the app. Forge has no such gate — but it does not need one, because the forge
8
+ * handler already runs in the browser. So the gate is simply: the inbound
9
+ * dispatcher awaits a confirm dialog before it dispatches. Far simpler
10
+ * machinery, identical observable contract (`{ error: "declined" }` back to the
11
+ * agent, handler untouched).
12
+ *
13
+ * This module is the queue behind that dialog. It holds no UI of its own —
14
+ * `<ForgeCapabilityApproval />` (one variant per UI kit) renders
15
+ * {@link UseCapabilityApprovalReturn.current} and calls `approve` / `decline`.
16
+ * A consumer that wants its own dialog can ignore the component and read the
17
+ * same queue.
18
+ *
19
+ * **Every request settles exactly once, and settling removes it from the
20
+ * queue.** That is the difference between a cancellable dialog and one that
21
+ * merely loses a race: when the per-call deadline elapses, the entry
22
+ * disappears — the dialog closes, the promise resolves `"timeout"`, and the
23
+ * dispatcher answers the parked call. Nothing is left open for a user to
24
+ * confirm after the agent has already been told the call expired.
25
+ */
26
+ import { type ComputedRef, type ShallowRef } from "vue";
27
+ /**
28
+ * How an approval request ended.
29
+ *
30
+ * `timeout` and `cancelled` are both "no answer", kept apart because they have
31
+ * different causes: `timeout` is this browser's own per-call deadline elapsing,
32
+ * `cancelled` is the caller pulling the request (an `AbortSignal`, or
33
+ * {@link UseCapabilityApprovalReturn.reset} on teardown).
34
+ */
35
+ export type CapabilityApprovalDecision = "approved" | "declined" | "timeout" | "cancelled";
36
+ /** Default per-call deadline for an unanswered dialog. */
37
+ export declare const DEFAULT_APPROVAL_TIMEOUT_MS = 30000;
38
+ /** What the caller asks the user to confirm. */
39
+ export interface CapabilityApprovalOptions {
40
+ /** Correlation token of the originating `capability_invoked` event. */
41
+ callId: string;
42
+ /** Capability name as invoked — possibly a deprecated alias. */
43
+ name: string;
44
+ /** Human-readable label, when the capability declared one. */
45
+ displayName?: string;
46
+ /** The capability's own description, shown as the "what this does" line. */
47
+ description?: string;
48
+ /**
49
+ * The **validated** arguments the handler would receive. Validation runs
50
+ * before the gate, so the user is never asked to approve a call that would
51
+ * have been rejected anyway, and what the dialog shows is what will run.
52
+ */
53
+ input?: unknown;
54
+ /**
55
+ * Deadline in ms. `undefined` falls back to
56
+ * {@link DEFAULT_APPROVAL_TIMEOUT_MS}; `0` or a negative value means no
57
+ * browser-side deadline (the agent runner's own backstop still applies).
58
+ */
59
+ timeoutMs?: number;
60
+ /** Cancels the request from outside — settles it `"cancelled"`. */
61
+ signal?: AbortSignal;
62
+ }
63
+ /** One entry in the approval queue, as the dialog renders it. */
64
+ export interface CapabilityApprovalRequest {
65
+ /** Queue-unique id. Distinct from `callId`: the queue owns this one. */
66
+ id: string;
67
+ callId: string;
68
+ name: string;
69
+ displayName?: string;
70
+ description?: string;
71
+ input?: unknown;
72
+ /** `Date.now()` when the request was enqueued. */
73
+ requestedAt: number;
74
+ /** Effective deadline in ms, or `undefined` when there is none. */
75
+ timeoutMs?: number;
76
+ /** `requestedAt + timeoutMs`, for a countdown. `undefined` when no deadline. */
77
+ expiresAt?: number;
78
+ }
79
+ export interface UseCapabilityApprovalReturn {
80
+ /**
81
+ * The pending queue, oldest first.
82
+ *
83
+ * `shallowRef` for the same reason `useAgentCapabilities` uses one: entries
84
+ * carry arbitrary handler input, and Vue's deep proxy has no business
85
+ * rewriting values that are about to be handed to application code.
86
+ */
87
+ pending: Readonly<ShallowRef<CapabilityApprovalRequest[]>>;
88
+ /** The request the dialog should show — the oldest pending one, or `null`. */
89
+ current: ComputedRef<CapabilityApprovalRequest | null>;
90
+ /**
91
+ * Ask for confirmation. Resolves — never rejects — when the user answers,
92
+ * the deadline elapses, or the request is cancelled.
93
+ */
94
+ request(options: CapabilityApprovalOptions): Promise<CapabilityApprovalDecision>;
95
+ /** Answer a request `"approved"`. Returns false if it already settled. */
96
+ approve(id: string): boolean;
97
+ /** Answer a request `"declined"`. Returns false if it already settled. */
98
+ decline(id: string): boolean;
99
+ /** Withdraw a request `"cancelled"`. Returns false if it already settled. */
100
+ cancel(id: string): boolean;
101
+ /** Cancel everything pending — session teardown, tests. */
102
+ reset(): void;
103
+ }
104
+ /**
105
+ * The approval queue behind `requiresApproval`.
106
+ *
107
+ * Stateless accessor over a module-level singleton — call it from anywhere.
108
+ */
109
+ export declare function useCapabilityApproval(): UseCapabilityApprovalReturn;
@@ -0,0 +1,74 @@
1
+ import { computed, shallowRef } from "vue";
2
+ export const DEFAULT_APPROVAL_TIMEOUT_MS = 3e4;
3
+ const _pending = shallowRef([]);
4
+ const _settlers = /* @__PURE__ */ new Map();
5
+ let _seq = 0;
6
+ function nextId() {
7
+ _seq += 1;
8
+ return `approval-${_seq}`;
9
+ }
10
+ const _current = computed(() => _pending.value[0] ?? null);
11
+ function settle(id, decision) {
12
+ return _settlers.get(id)?.(decision) ?? false;
13
+ }
14
+ function request(options) {
15
+ return new Promise((resolve) => {
16
+ if (options.signal?.aborted) {
17
+ resolve("cancelled");
18
+ return;
19
+ }
20
+ const id = nextId();
21
+ const requestedAt = Date.now();
22
+ const configured = options.timeoutMs ?? DEFAULT_APPROVAL_TIMEOUT_MS;
23
+ const timeoutMs = configured > 0 ? configured : void 0;
24
+ let timer;
25
+ let settled = false;
26
+ const finish = (decision) => {
27
+ if (settled) return false;
28
+ settled = true;
29
+ if (timer !== void 0) clearTimeout(timer);
30
+ options.signal?.removeEventListener("abort", onAbort);
31
+ _settlers.delete(id);
32
+ _pending.value = _pending.value.filter((entry) => entry.id !== id);
33
+ resolve(decision);
34
+ return true;
35
+ };
36
+ function onAbort() {
37
+ finish("cancelled");
38
+ }
39
+ _settlers.set(id, finish);
40
+ if (timeoutMs !== void 0) {
41
+ timer = setTimeout(() => finish("timeout"), timeoutMs);
42
+ timer.unref?.();
43
+ }
44
+ options.signal?.addEventListener("abort", onAbort, { once: true });
45
+ _pending.value = [
46
+ ..._pending.value,
47
+ {
48
+ id,
49
+ callId: options.callId,
50
+ name: options.name,
51
+ displayName: options.displayName,
52
+ description: options.description,
53
+ input: options.input,
54
+ requestedAt,
55
+ timeoutMs,
56
+ expiresAt: timeoutMs === void 0 ? void 0 : requestedAt + timeoutMs
57
+ }
58
+ ];
59
+ });
60
+ }
61
+ export function useCapabilityApproval() {
62
+ return {
63
+ pending: _pending,
64
+ current: _current,
65
+ request,
66
+ approve: (id) => settle(id, "approved"),
67
+ decline: (id) => settle(id, "declined"),
68
+ cancel: (id) => settle(id, "cancelled"),
69
+ reset: () => {
70
+ for (const id of [..._settlers.keys()]) settle(id, "cancelled");
71
+ _pending.value = [];
72
+ }
73
+ };
74
+ }