@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.
- package/README.md +34 -2
- package/dist/module.d.mts +23 -0
- package/dist/module.json +9 -0
- package/dist/module.mjs +37 -0
- package/dist/runtime/client-capabilities.d.ts +29 -0
- package/dist/runtime/client-capabilities.js +25 -0
- package/dist/runtime/components-nuxt-ui/ForgeCapabilityApproval.d.vue.ts +2 -0
- package/dist/runtime/components-nuxt-ui/ForgeCapabilityApproval.vue +125 -0
- package/dist/runtime/components-nuxt-ui/ForgeCapabilityApproval.vue.d.ts +2 -0
- package/dist/runtime/components-nuxt-ui/ForgeCommandPalette.d.vue.ts +2 -0
- package/dist/runtime/components-nuxt-ui/ForgeCommandPalette.vue +218 -0
- package/dist/runtime/components-nuxt-ui/ForgeCommandPalette.vue.d.ts +2 -0
- package/dist/runtime/composables/defineClientCapability.d.ts +64 -0
- package/dist/runtime/composables/defineClientCapability.js +40 -0
- package/dist/runtime/composables/defineForgeAction.d.ts +82 -0
- package/dist/runtime/composables/defineForgeAction.js +147 -0
- package/dist/runtime/composables/useAgentCapabilities.d.ts +78 -0
- package/dist/runtime/composables/useAgentCapabilities.js +94 -0
- package/dist/runtime/composables/useCapabilityApproval.d.ts +109 -0
- package/dist/runtime/composables/useCapabilityApproval.js +74 -0
- package/dist/runtime/composables/useCapabilityInvocations.d.ts +150 -0
- package/dist/runtime/composables/useCapabilityInvocations.js +129 -0
- package/dist/runtime/composables/useCommandPalette.d.ts +28 -0
- package/dist/runtime/composables/useCommandPalette.js +49 -0
- package/dist/runtime/composables/usePaletteArgStep.d.ts +47 -0
- package/dist/runtime/composables/usePaletteArgStep.js +51 -0
- package/dist/runtime/server/index.d.ts +7 -0
- package/dist/runtime/server/index.js +2 -0
- package/dist/runtime/server/utils/capability-registry.d.ts +35 -0
- package/dist/runtime/server/utils/capability-registry.js +24 -0
- package/dist/runtime/server/utils/client-capability-bridge.d.ts +647 -0
- package/dist/runtime/server/utils/client-capability-bridge.js +755 -0
- package/dist/runtime/shims-vue.d.ts +8 -0
- package/dist/runtime/types/client-capability.d.ts +159 -0
- package/dist/runtime/types/client-capability.js +0 -0
- package/dist/runtime/types/command-palette.d.ts +75 -0
- package/dist/runtime/types/command-palette.js +0 -0
- package/dist/runtime/types/forge-action.d.ts +152 -0
- package/dist/runtime/types/forge-action.js +0 -0
- package/dist/runtime/types/index.d.ts +3 -0
- package/dist/runtime/types/index.js +0 -0
- package/dist/types.d.mts +9 -0
- package/package.json +81 -4
- package/src/runtime/client-capabilities.ts +82 -0
- package/src/runtime/components-nuxt-ui/ForgeCapabilityApproval.vue +240 -0
- package/src/runtime/components-nuxt-ui/ForgeCommandPalette.vue +363 -0
- package/src/runtime/composables/defineClientCapability.ts +119 -0
- package/src/runtime/composables/defineForgeAction.ts +278 -0
- package/src/runtime/composables/useAgentCapabilities.ts +215 -0
- package/src/runtime/composables/useCapabilityApproval.ts +219 -0
- package/src/runtime/composables/useCapabilityInvocations.ts +343 -0
- package/src/runtime/composables/useCommandPalette.ts +97 -0
- package/src/runtime/composables/usePaletteArgStep.ts +110 -0
- package/src/runtime/server/index.ts +8 -0
- package/src/runtime/server/utils/capability-registry.ts +66 -0
- package/src/runtime/server/utils/client-capability-bridge.ts +1201 -0
- package/src/runtime/shims-vue.d.ts +8 -0
- package/src/runtime/types/client-capability.ts +178 -0
- package/src/runtime/types/command-palette.ts +78 -0
- package/src/runtime/types/forge-action.ts +166 -0
- package/src/runtime/types/index.ts +24 -0
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser half of the forge app channel's **invoke** leg.
|
|
3
|
+
*
|
|
4
|
+
* `useAgentCapabilities()` announces what the browser can do; this composable
|
|
5
|
+
* is what happens when the agent takes it up on the offer. One
|
|
6
|
+
* `capability_invoked` arrives over the app's existing SSE stream and this
|
|
7
|
+
* module:
|
|
8
|
+
*
|
|
9
|
+
* 1. looks the name up in the browser registry (deprecated aliases included,
|
|
10
|
+
* because an alias is registered as its own entry sharing the handler),
|
|
11
|
+
* 2. validates the raw input against the capability's **live zod schema** —
|
|
12
|
+
* `inputSchema` on the wire is only a hint to the model, the schema here is
|
|
13
|
+
* the enforcement, and it runs BEFORE the handler,
|
|
14
|
+
* 3. for a capability that declares `requiresApproval`, awaits the user's
|
|
15
|
+
* confirmation (spec §3 L7) — declining answers the agent and the handler
|
|
16
|
+
* never runs,
|
|
17
|
+
* 4. runs the handler, where the real `ref()`s live,
|
|
18
|
+
* 5. POSTs `{ callId, result }` back so Nitro can answer the parked call.
|
|
19
|
+
*
|
|
20
|
+
* A thrown handler error becomes `{ error }`, which the agent runner turns into
|
|
21
|
+
* a rejected tool call — the agent is told the truth, not a silent success.
|
|
22
|
+
*
|
|
23
|
+
* See `docs/superpowers/specs/2026-08-22-forge-app-channel-design.md` §2.
|
|
24
|
+
*/
|
|
25
|
+
import type { ClientCapabilityLogger } from "../types/client-capability.js";
|
|
26
|
+
import { type CapabilityAnnounceFetcher } from "./useAgentCapabilities.js";
|
|
27
|
+
/**
|
|
28
|
+
* The agent → browser event, as it arrives on the SSE stream. Consumer apps tag
|
|
29
|
+
* it with their own routing fields (forge-workspace adds `_mode`); those are
|
|
30
|
+
* ignored here.
|
|
31
|
+
*/
|
|
32
|
+
export interface CapabilityInvokedEvent {
|
|
33
|
+
type: "capability_invoked";
|
|
34
|
+
/** Correlation token to answer with. */
|
|
35
|
+
callId: string;
|
|
36
|
+
/** Capability name as invoked — possibly a deprecated alias. */
|
|
37
|
+
name: string;
|
|
38
|
+
/** Raw, unvalidated arguments from the model. */
|
|
39
|
+
input?: unknown;
|
|
40
|
+
/**
|
|
41
|
+
* Spec §L4 — the one subscriber this invocation is addressed to, as elected
|
|
42
|
+
* by `ClientCapabilityBridge.routeTo()`. Present only when the app forwards
|
|
43
|
+
* it; absent (or `null`) means "whoever is listening", the pre-05 behaviour.
|
|
44
|
+
*
|
|
45
|
+
* A tab whose own `useAgentCapabilities().subscriberId` does not match is the
|
|
46
|
+
* *incumbent*: it stays connected and simply ignores the frame.
|
|
47
|
+
*/
|
|
48
|
+
ownerId?: string | null;
|
|
49
|
+
/**
|
|
50
|
+
* Apps tag their SSE frames with their own routing fields — forge-workspace
|
|
51
|
+
* adds `_mode`. They are passed through untouched, and kept on the type so a
|
|
52
|
+
* caller can still read its own tag after the guard has narrowed the frame.
|
|
53
|
+
*/
|
|
54
|
+
[key: string]: unknown;
|
|
55
|
+
}
|
|
56
|
+
export interface HandleInvocationOptions {
|
|
57
|
+
/**
|
|
58
|
+
* The upward POST endpoint that answers a parked call, e.g.
|
|
59
|
+
* `/api/agent/capability-result`. The consuming app owns this route —
|
|
60
|
+
* forge-app-actions never assumes a path.
|
|
61
|
+
*/
|
|
62
|
+
endpoint: string;
|
|
63
|
+
/**
|
|
64
|
+
* Extra fields merged into the JSON body, identifying the session to the
|
|
65
|
+
* app's own route (e.g. `{ mode: "work" }`). The session key
|
|
66
|
+
* itself is derived server-side; never send it from the browser.
|
|
67
|
+
*/
|
|
68
|
+
body?: Record<string, unknown>;
|
|
69
|
+
/**
|
|
70
|
+
* Opaque session key for the handler's {@link ClientCapabilityContext}.
|
|
71
|
+
* Informational only — the browser does not get to name its own session.
|
|
72
|
+
*/
|
|
73
|
+
sessionKey?: string;
|
|
74
|
+
/** Override the fetcher. Defaults to `globalThis.fetch`. */
|
|
75
|
+
fetcher?: CapabilityAnnounceFetcher;
|
|
76
|
+
/** Logger handed to the handler and used for this module's own warnings. */
|
|
77
|
+
log?: ClientCapabilityLogger;
|
|
78
|
+
/**
|
|
79
|
+
* Deadline for an unanswered approval dialog, in ms. Only consulted for a
|
|
80
|
+
* capability that declares `requiresApproval`, and only when the capability
|
|
81
|
+
* itself declares no `callTimeoutMs`. Defaults to
|
|
82
|
+
* {@link DEFAULT_APPROVAL_TIMEOUT_MS}; `0` disables the browser-side deadline
|
|
83
|
+
* and leaves the agent runner's own backstop as the only limit.
|
|
84
|
+
*/
|
|
85
|
+
approvalTimeoutMs?: number;
|
|
86
|
+
/**
|
|
87
|
+
* Cancels a pending approval dialog from outside — session teardown, a mode
|
|
88
|
+
* switch, a route change. An aborted signal settles the dialog as
|
|
89
|
+
* `approval-cancelled`; it does **not** interrupt a handler that has already
|
|
90
|
+
* started.
|
|
91
|
+
*/
|
|
92
|
+
signal?: AbortSignal;
|
|
93
|
+
}
|
|
94
|
+
/** Why an invocation failed, when it did. */
|
|
95
|
+
export type InvocationFailure =
|
|
96
|
+
/** No capability of that name is registered in this browser. */
|
|
97
|
+
"unknown-capability"
|
|
98
|
+
/** The input did not satisfy the declared zod schema. The handler never ran. */
|
|
99
|
+
| "invalid-input"
|
|
100
|
+
/**
|
|
101
|
+
* The capability declared `requiresApproval` and the user said no. The
|
|
102
|
+
* handler never ran and nothing was mutated.
|
|
103
|
+
*/
|
|
104
|
+
| "declined"
|
|
105
|
+
/**
|
|
106
|
+
* An approval dialog went unanswered past the per-call deadline. The dialog
|
|
107
|
+
* was closed and the handler never ran.
|
|
108
|
+
*/
|
|
109
|
+
| "approval-timeout"
|
|
110
|
+
/**
|
|
111
|
+
* A pending approval dialog was withdrawn — `options.signal` aborted, or
|
|
112
|
+
* `useCapabilityApproval().reset()` ran on teardown. The handler never ran.
|
|
113
|
+
*/
|
|
114
|
+
| "approval-cancelled"
|
|
115
|
+
/** The handler itself threw. */
|
|
116
|
+
| "handler-error";
|
|
117
|
+
/** What {@link UseCapabilityInvocationsReturn.handle} did with one event. */
|
|
118
|
+
export interface InvocationOutcome {
|
|
119
|
+
callId: string;
|
|
120
|
+
name: string;
|
|
121
|
+
ok: boolean;
|
|
122
|
+
/** The handler's return value, when `ok`. */
|
|
123
|
+
result?: unknown;
|
|
124
|
+
/** The message sent back to the agent, when not `ok`. */
|
|
125
|
+
error?: string;
|
|
126
|
+
/** Present when not `ok`. */
|
|
127
|
+
failure?: InvocationFailure;
|
|
128
|
+
/** Whether the answering POST succeeded. */
|
|
129
|
+
posted: boolean;
|
|
130
|
+
}
|
|
131
|
+
export interface UseCapabilityInvocationsReturn {
|
|
132
|
+
/** Narrowing type-guard for an SSE frame. */
|
|
133
|
+
isInvocation(event: unknown): event is CapabilityInvokedEvent;
|
|
134
|
+
/**
|
|
135
|
+
* Handle one SSE frame. Returns `null` — and does nothing — when the frame is
|
|
136
|
+
* not a `capability_invoked`, so a caller can use it as the first line of its
|
|
137
|
+
* event switch and fall through. Also `null` when the frame names a different
|
|
138
|
+
* tab as its §L4 owner: this tab is the inert incumbent for that call.
|
|
139
|
+
*
|
|
140
|
+
* Never throws: every failure path ends in an `{ error }` answer to the agent.
|
|
141
|
+
*/
|
|
142
|
+
handle(event: unknown, options: HandleInvocationOptions): Promise<InvocationOutcome | null>;
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Inbound dispatcher for agent-invoked browser capabilities.
|
|
146
|
+
*
|
|
147
|
+
* Stateless — the registry it dispatches against is
|
|
148
|
+
* `useAgentCapabilities()`'s module-level singleton.
|
|
149
|
+
*/
|
|
150
|
+
export declare function useCapabilityInvocations(): UseCapabilityInvocationsReturn;
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { useAgentCapabilities } from "./useAgentCapabilities.js";
|
|
2
|
+
import { DEFAULT_APPROVAL_TIMEOUT_MS, useCapabilityApproval } from "./useCapabilityApproval.js";
|
|
3
|
+
const defaultLog = {
|
|
4
|
+
info: (message, ...args) => console.log(message, ...args),
|
|
5
|
+
warn: (message, ...args) => console.warn(message, ...args),
|
|
6
|
+
error: (message, ...args) => console.error(message, ...args)
|
|
7
|
+
};
|
|
8
|
+
function isInvocationEvent(event) {
|
|
9
|
+
if (!event || typeof event !== "object") return false;
|
|
10
|
+
const e = event;
|
|
11
|
+
return e.type === "capability_invoked" && typeof e.callId === "string" && e.callId.length > 0 && typeof e.name === "string" && e.name.length > 0;
|
|
12
|
+
}
|
|
13
|
+
function describeError(err) {
|
|
14
|
+
if (err && typeof err === "object" && Array.isArray(err.issues)) {
|
|
15
|
+
const issues = err.issues;
|
|
16
|
+
const lines = issues.map((issue) => {
|
|
17
|
+
const path = Array.isArray(issue.path) ? issue.path.join(".") : "";
|
|
18
|
+
return path ? `${path}: ${issue.message}` : String(issue.message);
|
|
19
|
+
});
|
|
20
|
+
if (lines.length > 0) return lines.join("; ");
|
|
21
|
+
}
|
|
22
|
+
if (err instanceof Error) return err.message;
|
|
23
|
+
return String(err);
|
|
24
|
+
}
|
|
25
|
+
async function postResult(options, callId, result, log) {
|
|
26
|
+
const fetcher = options.fetcher ?? globalThis.fetch ?? void 0;
|
|
27
|
+
if (!fetcher) {
|
|
28
|
+
log.warn("[useCapabilityInvocations] no fetch implementation available");
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
try {
|
|
32
|
+
const res = await fetcher(options.endpoint, {
|
|
33
|
+
method: "POST",
|
|
34
|
+
headers: { "content-type": "application/json" },
|
|
35
|
+
body: JSON.stringify({ ...options.body, callId, result })
|
|
36
|
+
});
|
|
37
|
+
if (!res.ok) {
|
|
38
|
+
log.warn(`[useCapabilityInvocations] capability-result POST failed: HTTP ${res.status}`);
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
return true;
|
|
42
|
+
} catch (err) {
|
|
43
|
+
log.warn(`[useCapabilityInvocations] capability-result POST failed: ${describeError(err)}`);
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
export function useCapabilityInvocations() {
|
|
48
|
+
async function handle(event, options) {
|
|
49
|
+
if (!isInvocationEvent(event)) return null;
|
|
50
|
+
const log = options.log ?? defaultLog;
|
|
51
|
+
const { callId, name } = event;
|
|
52
|
+
const registry = useAgentCapabilities();
|
|
53
|
+
if (typeof event.ownerId === "string" && event.ownerId !== registry.subscriberId) {
|
|
54
|
+
log.info(
|
|
55
|
+
`[useCapabilityInvocations] ${name} (${callId}) is addressed to another tab; ignoring`
|
|
56
|
+
);
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
const capability = registry.get(name);
|
|
60
|
+
const fail = async (failure, error) => {
|
|
61
|
+
log.warn(`[useCapabilityInvocations] ${name} (${callId}) ${failure}: ${error}`);
|
|
62
|
+
const posted2 = await postResult(options, callId, { error }, log);
|
|
63
|
+
return { callId, name, ok: false, error, failure, posted: posted2 };
|
|
64
|
+
};
|
|
65
|
+
if (!capability) {
|
|
66
|
+
return fail(
|
|
67
|
+
"unknown-capability",
|
|
68
|
+
`No capability named "${name}" is registered in this browser.`
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
let input;
|
|
72
|
+
try {
|
|
73
|
+
input = capability.inputZod.parse(event.input ?? {});
|
|
74
|
+
} catch (err) {
|
|
75
|
+
return fail("invalid-input", `Invalid input for "${name}": ${describeError(err)}`);
|
|
76
|
+
}
|
|
77
|
+
if (capability.requiresApproval) {
|
|
78
|
+
const decision = await useCapabilityApproval().request({
|
|
79
|
+
callId,
|
|
80
|
+
name,
|
|
81
|
+
displayName: capability.displayName,
|
|
82
|
+
description: capability.description,
|
|
83
|
+
input,
|
|
84
|
+
// The capability's own per-call timeout governs its dialog, so the gate
|
|
85
|
+
// cannot outlive the call the bridge parked for it.
|
|
86
|
+
timeoutMs: capability.callTimeoutMs ?? options.approvalTimeoutMs ?? DEFAULT_APPROVAL_TIMEOUT_MS,
|
|
87
|
+
signal: options.signal
|
|
88
|
+
});
|
|
89
|
+
if (decision === "declined") {
|
|
90
|
+
return fail("declined", `The user declined "${name}".`);
|
|
91
|
+
}
|
|
92
|
+
if (decision === "timeout") {
|
|
93
|
+
return fail(
|
|
94
|
+
"approval-timeout",
|
|
95
|
+
`Approval for "${name}" was not answered in time; the action did not run.`
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
if (decision !== "approved") {
|
|
99
|
+
return fail(
|
|
100
|
+
"approval-cancelled",
|
|
101
|
+
`Approval for "${name}" was cancelled; the action did not run.`
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
const ctx = {
|
|
106
|
+
sessionKey: options.sessionKey ?? "",
|
|
107
|
+
callId,
|
|
108
|
+
name,
|
|
109
|
+
log
|
|
110
|
+
};
|
|
111
|
+
let result;
|
|
112
|
+
try {
|
|
113
|
+
result = await capability.handler(input, ctx);
|
|
114
|
+
} catch (err) {
|
|
115
|
+
return fail("handler-error", describeError(err));
|
|
116
|
+
}
|
|
117
|
+
if (capability.outputZod) {
|
|
118
|
+
const check = capability.outputZod.safeParse(result);
|
|
119
|
+
if (!check.success) {
|
|
120
|
+
log.warn(
|
|
121
|
+
`[useCapabilityInvocations] ${name} returned a value that does not match its declared output schema: ${describeError(check.error)}`
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
const posted = await postResult(options, callId, result, log);
|
|
126
|
+
return { callId, name, ok: true, result, posted };
|
|
127
|
+
}
|
|
128
|
+
return { isInvocation: isInvocationEvent, handle };
|
|
129
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { DeepReadonly, Ref } from "vue";
|
|
2
|
+
import type { PaletteAction } from "../types/command-palette.js";
|
|
3
|
+
export interface UseCommandPaletteReturn {
|
|
4
|
+
/** Register an action. Idempotent by id — replaces existing if same id. */
|
|
5
|
+
registerAction(action: PaletteAction): void;
|
|
6
|
+
/** Remove a previously registered action. No-op if id not found. */
|
|
7
|
+
unregisterAction(id: string): void;
|
|
8
|
+
/**
|
|
9
|
+
* Fuzzy-filter registered actions by search string.
|
|
10
|
+
* Matches against label, description, and keywords.
|
|
11
|
+
* Returns all actions when search is empty or whitespace-only.
|
|
12
|
+
* Results are sorted: exact-label-start matches first, then substring.
|
|
13
|
+
*/
|
|
14
|
+
queryActions(search: string): PaletteAction[];
|
|
15
|
+
/** Reactive list of all currently registered actions. */
|
|
16
|
+
actions: DeepReadonly<Ref<PaletteAction[]>>;
|
|
17
|
+
/** Open/close state (read-only — toggled by ForgeCommandPalette). */
|
|
18
|
+
isOpen: Readonly<Ref<boolean>>;
|
|
19
|
+
/** Programmatically open the palette. */
|
|
20
|
+
open(): void;
|
|
21
|
+
/** Programmatically close the palette. */
|
|
22
|
+
close(): void;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Global command palette registry.
|
|
26
|
+
* Components call registerAction() to expose actions in the Cmd+K palette.
|
|
27
|
+
*/
|
|
28
|
+
export declare function useCommandPalette(): UseCommandPaletteReturn;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { readonly, ref } from "vue";
|
|
2
|
+
const _actions = ref([]);
|
|
3
|
+
const _isOpen = ref(false);
|
|
4
|
+
export function useCommandPalette() {
|
|
5
|
+
function registerAction(action) {
|
|
6
|
+
const idx = _actions.value.findIndex((a) => a.id === action.id);
|
|
7
|
+
if (idx !== -1) {
|
|
8
|
+
_actions.value = [..._actions.value.slice(0, idx), action, ..._actions.value.slice(idx + 1)];
|
|
9
|
+
} else {
|
|
10
|
+
_actions.value = [..._actions.value, action];
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
function unregisterAction(id) {
|
|
14
|
+
const idx = _actions.value.findIndex((a) => a.id === id);
|
|
15
|
+
if (idx === -1) return;
|
|
16
|
+
_actions.value = _actions.value.filter((a) => a.id !== id);
|
|
17
|
+
}
|
|
18
|
+
function queryActions(search) {
|
|
19
|
+
const trimmed = search.trim().toLowerCase();
|
|
20
|
+
if (!trimmed) return [..._actions.value];
|
|
21
|
+
const matches = _actions.value.filter((action) => {
|
|
22
|
+
const haystack = [action.label, action.description ?? "", ...action.keywords ?? []].join(" ").toLowerCase();
|
|
23
|
+
return haystack.includes(trimmed);
|
|
24
|
+
});
|
|
25
|
+
matches.sort((a, b) => {
|
|
26
|
+
const aStarts = a.label.toLowerCase().startsWith(trimmed);
|
|
27
|
+
const bStarts = b.label.toLowerCase().startsWith(trimmed);
|
|
28
|
+
if (aStarts && !bStarts) return -1;
|
|
29
|
+
if (!aStarts && bStarts) return 1;
|
|
30
|
+
return 0;
|
|
31
|
+
});
|
|
32
|
+
return matches;
|
|
33
|
+
}
|
|
34
|
+
function open() {
|
|
35
|
+
_isOpen.value = true;
|
|
36
|
+
}
|
|
37
|
+
function close() {
|
|
38
|
+
_isOpen.value = false;
|
|
39
|
+
}
|
|
40
|
+
return {
|
|
41
|
+
registerAction,
|
|
42
|
+
unregisterAction,
|
|
43
|
+
queryActions,
|
|
44
|
+
actions: readonly(_actions),
|
|
45
|
+
isOpen: readonly(_isOpen),
|
|
46
|
+
open,
|
|
47
|
+
close
|
|
48
|
+
};
|
|
49
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The command palette's **argument step**.
|
|
3
|
+
*
|
|
4
|
+
* A `PaletteAction` that declares `arg` needs one string before it can run
|
|
5
|
+
* (`defineForgeAction`'s `arg.toInput` turns that string into the action's
|
|
6
|
+
* typed input). Selecting such an action does not close the palette: it swaps
|
|
7
|
+
* the list for the action's candidates and waits for a second Enter.
|
|
8
|
+
*
|
|
9
|
+
* This holds the per-component state for that step. It lives here rather than
|
|
10
|
+
* inline in the components because both UI-kit variants of
|
|
11
|
+
* `ForgeCommandPalette` run it and are kept in deliberate lock-step — one copy
|
|
12
|
+
* cannot drift from the other if there is only one copy.
|
|
13
|
+
*
|
|
14
|
+
* Not a singleton: `useCommandPalette()` owns the shared registry, this owns
|
|
15
|
+
* one palette instance's transient step.
|
|
16
|
+
*/
|
|
17
|
+
import { type ComputedRef, type Ref, type ShallowRef } from "vue";
|
|
18
|
+
import type { PaletteAction, PaletteArgOption, PaletteArgPrompt } from "../types/command-palette.js";
|
|
19
|
+
export interface UsePaletteArgStepReturn {
|
|
20
|
+
/** The action awaiting its argument, or `null` when the step is not active. */
|
|
21
|
+
action: ShallowRef<PaletteAction | null>;
|
|
22
|
+
/** Candidates for the current query. Empty for a free-text argument. */
|
|
23
|
+
options: ShallowRef<PaletteArgOption[]>;
|
|
24
|
+
/** Whether a candidate lookup is in flight. */
|
|
25
|
+
loading: Ref<boolean>;
|
|
26
|
+
/** Whether the palette is currently collecting an argument. */
|
|
27
|
+
active: ComputedRef<boolean>;
|
|
28
|
+
/** The active action's prompt declaration. */
|
|
29
|
+
prompt: ComputedRef<PaletteArgPrompt | null>;
|
|
30
|
+
/**
|
|
31
|
+
* Enter the argument step for `action`. Returns `false` — and changes
|
|
32
|
+
* nothing — when the action takes no argument, so a caller can use it as
|
|
33
|
+
* `if (!begin(a)) { close(); run(a) }`.
|
|
34
|
+
*/
|
|
35
|
+
begin(action: PaletteAction): boolean;
|
|
36
|
+
/** Recompute the candidates for `query`. Stale responses are discarded. */
|
|
37
|
+
query(query: string): Promise<void>;
|
|
38
|
+
/** Leave the step without running anything. */
|
|
39
|
+
cancel(): void;
|
|
40
|
+
/**
|
|
41
|
+
* The string the action should run with, given what is typed and which row is
|
|
42
|
+
* highlighted — or `null` when the step cannot complete yet (nothing typed
|
|
43
|
+
* and nothing highlighted, or a typed value the prompt does not accept).
|
|
44
|
+
*/
|
|
45
|
+
valueFor(typed: string, index: number): string | null;
|
|
46
|
+
}
|
|
47
|
+
export declare function usePaletteArgStep(): UsePaletteArgStepReturn;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { computed, ref, shallowRef } from "vue";
|
|
2
|
+
export function usePaletteArgStep() {
|
|
3
|
+
const action = shallowRef(null);
|
|
4
|
+
const options = shallowRef([]);
|
|
5
|
+
const loading = ref(false);
|
|
6
|
+
let ticket = 0;
|
|
7
|
+
const active = computed(() => action.value !== null);
|
|
8
|
+
const prompt = computed(() => action.value?.arg ?? null);
|
|
9
|
+
function begin(next) {
|
|
10
|
+
if (!next.arg) return false;
|
|
11
|
+
action.value = next;
|
|
12
|
+
options.value = [];
|
|
13
|
+
void query("");
|
|
14
|
+
return true;
|
|
15
|
+
}
|
|
16
|
+
async function query(text) {
|
|
17
|
+
const arg = action.value?.arg;
|
|
18
|
+
if (!arg?.options) {
|
|
19
|
+
options.value = [];
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
const mine = ++ticket;
|
|
23
|
+
loading.value = true;
|
|
24
|
+
try {
|
|
25
|
+
const found = await arg.options(text);
|
|
26
|
+
if (mine !== ticket) return;
|
|
27
|
+
options.value = found;
|
|
28
|
+
} catch {
|
|
29
|
+
if (mine === ticket) options.value = [];
|
|
30
|
+
} finally {
|
|
31
|
+
if (mine === ticket) loading.value = false;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
function cancel() {
|
|
35
|
+
ticket++;
|
|
36
|
+
action.value = null;
|
|
37
|
+
options.value = [];
|
|
38
|
+
loading.value = false;
|
|
39
|
+
}
|
|
40
|
+
function valueFor(typed, index) {
|
|
41
|
+
const arg = action.value?.arg;
|
|
42
|
+
if (!arg) return null;
|
|
43
|
+
const picked = options.value[index];
|
|
44
|
+
if (picked) return picked.value;
|
|
45
|
+
const allowCustom = arg.allowCustom ?? !arg.options;
|
|
46
|
+
const trimmed = typed.trim();
|
|
47
|
+
if (allowCustom && trimmed.length > 0) return trimmed;
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
return { action, options, loading, active, prompt, begin, query, cancel, valueFor };
|
|
51
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Explicit entry point for the server-side client-capability bridge, via the
|
|
3
|
+
* `./server` subpath. In a Nuxt consumer the same symbols are auto-imported
|
|
4
|
+
* (the module's `addServerImportsDir` on `runtime/server/utils`).
|
|
5
|
+
*/
|
|
6
|
+
export * from "./utils/capability-registry.js";
|
|
7
|
+
export * from "./utils/client-capability-bridge.js";
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { AgentEvent, Capability } from "@skaile/workspaces/sdk/types";
|
|
2
|
+
/** Event sink handed to a capability handler when it runs. */
|
|
3
|
+
export interface CapabilityHandlerCtx {
|
|
4
|
+
/** Emit a synthetic AgentEvent into the app's SSE stream. */
|
|
5
|
+
emitEvent: (event: AgentEvent) => void;
|
|
6
|
+
}
|
|
7
|
+
export type CapabilityHandler = (input: Record<string, unknown>, ctx: CapabilityHandlerCtx) => Promise<unknown>;
|
|
8
|
+
export interface CapabilityEntry {
|
|
9
|
+
capability: Capability;
|
|
10
|
+
handler: CapabilityHandler;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Plain-TS capability registry for the forge app — holds client-side v2
|
|
14
|
+
* capabilities and routes `capability_invoked` calls to their handlers.
|
|
15
|
+
*/
|
|
16
|
+
export declare class CapabilityRegistry {
|
|
17
|
+
private entries;
|
|
18
|
+
register(entry: CapabilityEntry): void;
|
|
19
|
+
list(): Capability[];
|
|
20
|
+
dispatch(name: string, input: Record<string, unknown>, ctx: CapabilityHandlerCtx): Promise<unknown | {
|
|
21
|
+
error: string;
|
|
22
|
+
}>;
|
|
23
|
+
}
|
|
24
|
+
/** Dispatch a `capability_invoked` event and send back its `capability_result`. */
|
|
25
|
+
export declare function handleCapabilityInvoked(event: {
|
|
26
|
+
callId: string;
|
|
27
|
+
name: string;
|
|
28
|
+
input: unknown;
|
|
29
|
+
}, deps: {
|
|
30
|
+
registry: CapabilityRegistry;
|
|
31
|
+
ctx: CapabilityHandlerCtx;
|
|
32
|
+
sendResult: (callId: string, result: unknown) => Promise<void> | void;
|
|
33
|
+
}): Promise<void>;
|
|
34
|
+
/** Process-wide singleton — capabilities are registered once at module load. */
|
|
35
|
+
export declare const capabilityRegistry: CapabilityRegistry;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export class CapabilityRegistry {
|
|
2
|
+
entries = /* @__PURE__ */ new Map();
|
|
3
|
+
register(entry) {
|
|
4
|
+
this.entries.set(entry.capability.name, entry);
|
|
5
|
+
}
|
|
6
|
+
list() {
|
|
7
|
+
return [...this.entries.values()].map((e) => e.capability);
|
|
8
|
+
}
|
|
9
|
+
async dispatch(name, input, ctx) {
|
|
10
|
+
const entry = this.entries.get(name);
|
|
11
|
+
if (!entry) return { error: `Unknown capability: ${name}` };
|
|
12
|
+
try {
|
|
13
|
+
return await entry.handler(input, ctx);
|
|
14
|
+
} catch (err) {
|
|
15
|
+
return { error: err instanceof Error ? err.message : String(err) };
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
export async function handleCapabilityInvoked(event, deps) {
|
|
20
|
+
const input = event.input && typeof event.input === "object" ? event.input : {};
|
|
21
|
+
const result = await deps.registry.dispatch(event.name, input, deps.ctx);
|
|
22
|
+
await deps.sendResult(event.callId, result);
|
|
23
|
+
}
|
|
24
|
+
export const capabilityRegistry = new CapabilityRegistry();
|