@skaile/forge-app-actions 0.0.0-stage → 0.2.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 (73) hide show
  1. package/README.md +61 -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/chat/index.d.ts +52 -0
  6. package/dist/runtime/chat/index.js +100 -0
  7. package/dist/runtime/chat/server.d.ts +36 -0
  8. package/dist/runtime/chat/server.js +27 -0
  9. package/dist/runtime/chat/types.d.ts +10 -0
  10. package/dist/runtime/chat/types.js +0 -0
  11. package/dist/runtime/client-capabilities.d.ts +29 -0
  12. package/dist/runtime/client-capabilities.js +25 -0
  13. package/dist/runtime/components-nuxt-ui/ForgeCapabilityApproval.d.vue.ts +2 -0
  14. package/dist/runtime/components-nuxt-ui/ForgeCapabilityApproval.vue +125 -0
  15. package/dist/runtime/components-nuxt-ui/ForgeCapabilityApproval.vue.d.ts +2 -0
  16. package/dist/runtime/components-nuxt-ui/ForgeCommandPalette.d.vue.ts +2 -0
  17. package/dist/runtime/components-nuxt-ui/ForgeCommandPalette.vue +218 -0
  18. package/dist/runtime/components-nuxt-ui/ForgeCommandPalette.vue.d.ts +2 -0
  19. package/dist/runtime/composables/defineClientCapability.d.ts +64 -0
  20. package/dist/runtime/composables/defineClientCapability.js +40 -0
  21. package/dist/runtime/composables/defineForgeAction.d.ts +82 -0
  22. package/dist/runtime/composables/defineForgeAction.js +147 -0
  23. package/dist/runtime/composables/useAgentCapabilities.d.ts +78 -0
  24. package/dist/runtime/composables/useAgentCapabilities.js +94 -0
  25. package/dist/runtime/composables/useCapabilityApproval.d.ts +109 -0
  26. package/dist/runtime/composables/useCapabilityApproval.js +74 -0
  27. package/dist/runtime/composables/useCapabilityInvocations.d.ts +150 -0
  28. package/dist/runtime/composables/useCapabilityInvocations.js +118 -0
  29. package/dist/runtime/composables/useCommandPalette.d.ts +28 -0
  30. package/dist/runtime/composables/useCommandPalette.js +49 -0
  31. package/dist/runtime/composables/usePaletteArgStep.d.ts +47 -0
  32. package/dist/runtime/composables/usePaletteArgStep.js +51 -0
  33. package/dist/runtime/internal/describe-error.d.ts +10 -0
  34. package/dist/runtime/internal/describe-error.js +12 -0
  35. package/dist/runtime/server/index.d.ts +7 -0
  36. package/dist/runtime/server/index.js +2 -0
  37. package/dist/runtime/server/utils/capability-registry.d.ts +35 -0
  38. package/dist/runtime/server/utils/capability-registry.js +24 -0
  39. package/dist/runtime/server/utils/client-capability-bridge.d.ts +647 -0
  40. package/dist/runtime/server/utils/client-capability-bridge.js +755 -0
  41. package/dist/runtime/shims-vue.d.ts +8 -0
  42. package/dist/runtime/types/client-capability.d.ts +159 -0
  43. package/dist/runtime/types/client-capability.js +0 -0
  44. package/dist/runtime/types/command-palette.d.ts +75 -0
  45. package/dist/runtime/types/command-palette.js +0 -0
  46. package/dist/runtime/types/forge-action.d.ts +152 -0
  47. package/dist/runtime/types/forge-action.js +0 -0
  48. package/dist/runtime/types/index.d.ts +3 -0
  49. package/dist/runtime/types/index.js +0 -0
  50. package/dist/types.d.mts +9 -0
  51. package/package.json +94 -4
  52. package/src/runtime/chat/index.ts +190 -0
  53. package/src/runtime/chat/server.ts +64 -0
  54. package/src/runtime/chat/types.ts +13 -0
  55. package/src/runtime/client-capabilities.ts +82 -0
  56. package/src/runtime/components-nuxt-ui/ForgeCapabilityApproval.vue +240 -0
  57. package/src/runtime/components-nuxt-ui/ForgeCommandPalette.vue +363 -0
  58. package/src/runtime/composables/defineClientCapability.ts +119 -0
  59. package/src/runtime/composables/defineForgeAction.ts +278 -0
  60. package/src/runtime/composables/useAgentCapabilities.ts +215 -0
  61. package/src/runtime/composables/useCapabilityApproval.ts +219 -0
  62. package/src/runtime/composables/useCapabilityInvocations.ts +325 -0
  63. package/src/runtime/composables/useCommandPalette.ts +97 -0
  64. package/src/runtime/composables/usePaletteArgStep.ts +110 -0
  65. package/src/runtime/internal/describe-error.ts +21 -0
  66. package/src/runtime/server/index.ts +8 -0
  67. package/src/runtime/server/utils/capability-registry.ts +66 -0
  68. package/src/runtime/server/utils/client-capability-bridge.ts +1201 -0
  69. package/src/runtime/shims-vue.d.ts +8 -0
  70. package/src/runtime/types/client-capability.ts +178 -0
  71. package/src/runtime/types/command-palette.ts +78 -0
  72. package/src/runtime/types/forge-action.ts +166 -0
  73. package/src/runtime/types/index.ts +24 -0
package/README.md CHANGED
@@ -1,3 +1,62 @@
1
- # Temporary Holding Version
1
+ # @skaile/forge-app-actions
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Browser-declared app actions for Skaile forge apps, as a Nuxt module.
4
+
5
+ One `defineForgeAction` declaration is both a command-palette row and an agent capability. The package ships
6
+
7
+ - `defineForgeAction`, `useForgeActions`, `defineClientCapability` (auto-imported in a Nuxt app, or imported from `@skaile/forge-app-actions/client-capabilities`),
8
+ - `useAgentCapabilities` (announce) and `useCapabilityInvocations` (handle `capability_invoked` from the app's SSE and post the result),
9
+ - `useCapabilityApproval` + `<ForgeCapabilityApproval />` (approval dialog),
10
+ - `useCommandPalette` + `<ForgeCommandPalette />`,
11
+ - the Nitro-side bridge (`clientCapabilityBridge`, `capabilityRegistry`) via `@skaile/forge-app-actions/server`.
12
+
13
+ ## Install
14
+
15
+ ```ts
16
+ // nuxt.config.ts — @nuxt/ui must come first
17
+ export default defineNuxtConfig({
18
+ modules: ['@nuxt/ui', '@skaile/forge-app-actions'],
19
+ })
20
+ ```
21
+
22
+ Peers: `@nuxt/ui ^4.11`, `nuxt`, `vue`; `@skaile/workspaces ^6` is an optional peer (type-only imports from `@skaile/workspaces/sdk/types`).
23
+
24
+ `@skaile/forge-common` installs this module for you.
25
+
26
+ ## Exports
27
+
28
+ | Subpath | Contents |
29
+ |---|---|
30
+ | `.` | the Nuxt module (`forgeAppActions` config key) |
31
+ | `./client-capabilities` | browser-side API, explicit-import entry for tests and non-auto-import contexts |
32
+ | `./chat` | `useChatActions`, `toolNameOf` — forge actions as forge-chat client tools (browser) |
33
+ | `./chat/server` | `clientToolsFromBody` — validates the `clientTools` request field (no Vue) |
34
+ | `./server` | `clientCapabilityBridge`, `capabilityRegistry`, `routeCapabilityInvocation`, … |
35
+ | `./types` | `ClientCapability*`, `ForgeAction*`, `Palette*` types |
36
+
37
+ Moved here from `@skaile-ai/forge-common` 9.x (`./client-capabilities` and the capability parts of `./server` and `./types`).
38
+
39
+ ## Using actions from an AI-SDK chat
40
+
41
+ A plain Nuxt app with an AI-SDK route (no Skaile platform, no SSE agent session) can let the model call the browser's actions through `@skaile/forge-chat` ≥ 0.2 client tools. Every action whose `audience` includes `'llm'` becomes a tool; `requiresApproval` actions wait for the user's confirmation first.
42
+
43
+ **Tool names.** Model providers reject dots in tool names (Anthropic `^[a-zA-Z0-9_-]{1,64}$`, OpenAI `^[a-zA-Z0-9_-]+$`), so the tool name is `toolNameOf(capabilityName)`: every character outside `[A-Za-z0-9_-]` becomes `_`, truncated to 64. Action `doc.read` (capability `app.doc.read`) is the tool `app_doc_read`. Name tools in prompts with `toolNameOf('app.doc.read')` (exported from `@skaile/forge-app-actions/chat`). Two actions that map to the same tool name make `descriptors()` and the handler lookup throw. The handler still runs the action under its real capability name (`ctx.name === 'app.doc.read'`). Descriptor schemas are the zod-derived JSON Schema without the top-level `$schema` key.
44
+
45
+ ```ts
46
+ // app/pages/index.vue
47
+ const chatActions = useChatActions({ actions: () => actions }) // from '@skaile/forge-app-actions/chat'
48
+ const transport = createAiSdkTransport({ url: '/api/chat', body: () => ({ clientTools: chatActions.descriptors() }) })
49
+ const session = createChatSession({ transport, tools: chatActions.handlers })
50
+ // app/app.vue: <ForgeCapabilityApproval /> — mount once
51
+
52
+ // server/api/chat.post.ts
53
+ import { clientToolsFromBody } from '@skaile/forge-app-actions/chat/server'
54
+ export default defineEventHandler((event) =>
55
+ createAiSdkChatHandler({ model, clientTools: (ctx) => clientToolsFromBody(ctx.body) })(toWebRequest(event)))
56
+ ```
57
+
58
+ Round trip: the model emits a tool call → the handler ends the turn with `turn-end` reason `tool-calls` → the session runs `chatActions.handlers[name]` → the result is pushed and the turn resumes. A handler validates the input against the action's zod schema first (invalid → the model gets `invalid input: <issues>`), then asks for approval when required (`declined` → the model gets `declined`), then runs the action with `source: 'llm'`.
59
+
60
+ `clientToolsFromBody` returns `undefined` when the body has no `clientTools`, and throws an error with `statusCode: 400` (`clientTools: <issues>`) for a bad tool name, an over-long description or a non-object schema — `createAiSdkChatHandler` answers such an error with that status.
61
+
62
+ Peer: `@skaile/forge-chat ^0.2.0` (optional; type-only imports).
@@ -0,0 +1,23 @@
1
+ import { NuxtModule } from '@nuxt/schema';
2
+
3
+ interface ForgeAppActionsOptions {
4
+ /** Disable server-side auto-imports (use explicit imports via the `./server` subpath instead). */
5
+ serverAutoImport?: boolean;
6
+ /** Disable component auto-registration. */
7
+ componentAutoRegister?: boolean;
8
+ /** Disable composable auto-imports. */
9
+ composableAutoImport?: boolean;
10
+ }
11
+ /**
12
+ * Nuxt module for browser-declared app actions: `defineForgeAction` /
13
+ * `defineClientCapability`, the agent capability bridge, the command palette
14
+ * and the approval dialog.
15
+ *
16
+ * Requires `@nuxt/ui` (^4.11) registered BEFORE this module in `modules`; the
17
+ * components render Nuxt UI primitives (<UModal>, <UButton>, <UInput>) and the
18
+ * host root must be wrapped in `<UApp>`.
19
+ */
20
+ declare const forgeAppActionsModule: NuxtModule<ForgeAppActionsOptions>;
21
+
22
+ export { forgeAppActionsModule as default };
23
+ export type { ForgeAppActionsOptions };
@@ -0,0 +1,9 @@
1
+ {
2
+ "name": "@skaile/forge-app-actions",
3
+ "configKey": "forgeAppActions",
4
+ "version": "0.2.0",
5
+ "builder": {
6
+ "@nuxt/module-builder": "1.0.3",
7
+ "unbuild": "3.6.1"
8
+ }
9
+ }
@@ -0,0 +1,37 @@
1
+ import { defineNuxtModule, createResolver, hasNuxtModule, addServerImportsDir, addComponentsDir, addImportsDir } from '@nuxt/kit';
2
+
3
+ const forgeAppActionsModule = defineNuxtModule({
4
+ meta: {
5
+ name: "@skaile/forge-app-actions",
6
+ configKey: "forgeAppActions"
7
+ },
8
+ defaults: {
9
+ serverAutoImport: true,
10
+ componentAutoRegister: true,
11
+ composableAutoImport: true
12
+ },
13
+ async setup(options, nuxt) {
14
+ const { resolve } = createResolver(import.meta.url);
15
+ if (!nuxt.options._prepare && !hasNuxtModule("@nuxt/ui", nuxt)) {
16
+ throw new Error(
17
+ "[@skaile/forge-app-actions] requires the '@nuxt/ui' module, but it was not registered before '@skaile/forge-app-actions'. Add '@nuxt/ui' to the `modules` array in your nuxt.config *before* '@skaile/forge-app-actions' (and install it as a dependency): the command palette and approval dialog render Nuxt UI primitives (<UModal>, <UButton>, \u2026)."
18
+ );
19
+ }
20
+ if (options.serverAutoImport) {
21
+ addServerImportsDir(resolve("./runtime/server/utils"));
22
+ }
23
+ if (options.componentAutoRegister) {
24
+ addComponentsDir({
25
+ path: resolve("./runtime/components-nuxt-ui"),
26
+ prefix: "Forge",
27
+ global: false,
28
+ pathPrefix: false
29
+ });
30
+ }
31
+ if (options.composableAutoImport) {
32
+ addImportsDir(resolve("./runtime/composables"));
33
+ }
34
+ }
35
+ });
36
+
37
+ export { forgeAppActionsModule as default };
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Browser half of the AI-SDK chat binding (`@skaile/forge-app-actions/chat`).
3
+ *
4
+ * Turns registered {@link ForgeAction}s into forge-chat **client tools**:
5
+ *
6
+ * - `descriptors()` — what the model may call. Goes into the chat request body
7
+ * (`createAiSdkTransport({ body })`); the server validates it with
8
+ * `clientToolsFromBody` from `@skaile/forge-app-actions/chat/server`.
9
+ * - `handlers` — what runs when the model calls one. Pass as
10
+ * `createChatSession({ tools })`. Same contract as the SSE bridge's
11
+ * `useCapabilityInvocations().handle`: validate against the live zod schema,
12
+ * ask for approval (`<ForgeCapabilityApproval />`) when the action declares
13
+ * `requiresApproval`, then run the handler with a `ClientCapabilityContext`.
14
+ *
15
+ * Only actions whose audience includes `'llm'` are exposed. Tools are named
16
+ * {@link toolNameOf}(capability name) — `app.<id>` becomes `app_<id>` — because
17
+ * model providers reject dots in tool names.
18
+ */
19
+ import type { ClientCapabilityLogger } from "../types/client-capability.js";
20
+ import type { ForgeAction } from "../types/forge-action.js";
21
+ import type { ClientToolDecls, ClientToolHandlers } from "./types.js";
22
+ export type { ClientToolDecls, ClientToolHandlers } from "./types.js";
23
+ export interface UseChatActionsOptions {
24
+ /**
25
+ * The actions to expose, read on every use — the list may change over the
26
+ * lifetime of the chat (an editor that mounts later, a route-scoped action).
27
+ */
28
+ actions: () => readonly ForgeAction[];
29
+ /** Opaque session key for the handler's {@link ClientCapabilityContext}. Informational. */
30
+ sessionKey?: string;
31
+ /** Logger handed to handlers. Defaults to `console`. */
32
+ log?: ClientCapabilityLogger;
33
+ /**
34
+ * Deadline for an unanswered approval dialog, in ms; the action's own
35
+ * `callTimeoutMs` wins. Defaults to {@link DEFAULT_APPROVAL_TIMEOUT_MS}; `0`
36
+ * disables the deadline.
37
+ */
38
+ approvalTimeoutMs?: number;
39
+ }
40
+ export interface UseChatActionsReturn {
41
+ /** Tool descriptors for the request body: one per LLM-audience action. */
42
+ descriptors(): ClientToolDecls;
43
+ /** Tool handlers for `createChatSession({ tools })`, resolved against `actions()` at call time. */
44
+ handlers: ClientToolHandlers;
45
+ }
46
+ /**
47
+ * The provider-safe tool name of a capability name: every character outside
48
+ * `[A-Za-z0-9_-]` becomes `_` (Anthropic and OpenAI reject others), truncated
49
+ * to 64. `app.doc.read` → `app_doc_read`. Hosts use it to name tools in prompts.
50
+ */
51
+ export declare function toolNameOf(capabilityName: string): string;
52
+ export declare function useChatActions(options: UseChatActionsOptions): UseChatActionsReturn;
@@ -0,0 +1,100 @@
1
+ import {
2
+ DEFAULT_APPROVAL_TIMEOUT_MS,
3
+ useCapabilityApproval
4
+ } from "../composables/useCapabilityApproval.js";
5
+ import { describeError } from "../internal/describe-error.js";
6
+ const defaultLog = {
7
+ info: (message, ...args) => console.log(message, ...args),
8
+ warn: (message, ...args) => console.warn(message, ...args),
9
+ error: (message, ...args) => console.error(message, ...args)
10
+ };
11
+ const TOOL_NAME_MAX = 64;
12
+ export function toolNameOf(capabilityName) {
13
+ return capabilityName.replace(/[^A-Za-z0-9_-]/g, "_").slice(0, TOOL_NAME_MAX);
14
+ }
15
+ function withoutSchemaKey(schema) {
16
+ const { $schema: _draft, ...rest } = schema;
17
+ return rest;
18
+ }
19
+ export function useChatActions(options) {
20
+ const log = options.log ?? defaultLog;
21
+ function exposed() {
22
+ const byTool = /* @__PURE__ */ new Map();
23
+ for (const action of options.actions()) {
24
+ const capability = action.capability;
25
+ if (!capability || !action.audience.includes("llm")) continue;
26
+ const toolName = toolNameOf(capability.name);
27
+ const other = byTool.get(toolName);
28
+ if (other && other.name !== capability.name) {
29
+ throw new Error(
30
+ `[useChatActions] "${other.name}" and "${capability.name}" both map to the tool name "${toolName}"; rename one action.`
31
+ );
32
+ }
33
+ byTool.set(toolName, capability);
34
+ }
35
+ return byTool;
36
+ }
37
+ function descriptors() {
38
+ const decls = {};
39
+ for (const [toolName, capability] of exposed()) {
40
+ decls[toolName] = {
41
+ description: capability.description,
42
+ // Derived by `defineClientCapability` via `z.toJSONSchema()`; the
43
+ // draft marker is dropped, providers do not take it.
44
+ inputSchema: withoutSchemaKey(capability.inputSchema)
45
+ };
46
+ }
47
+ return decls;
48
+ }
49
+ function handlerFor(toolName) {
50
+ const capability = exposed().get(toolName);
51
+ if (!capability) return void 0;
52
+ const name = capability.name;
53
+ return async (rawInput, { toolCallId, signal }) => {
54
+ if (signal.aborted) throw new Error("aborted");
55
+ const checked = capability.inputZod.safeParse(rawInput ?? {});
56
+ if (!checked.success) throw new Error(`invalid input: ${describeError(checked.error)}`);
57
+ const input = checked.data;
58
+ if (capability.requiresApproval) {
59
+ const decision = await useCapabilityApproval().request({
60
+ callId: toolCallId,
61
+ name,
62
+ displayName: capability.displayName,
63
+ description: capability.description,
64
+ input,
65
+ timeoutMs: capability.callTimeoutMs ?? options.approvalTimeoutMs ?? DEFAULT_APPROVAL_TIMEOUT_MS,
66
+ signal
67
+ });
68
+ if (decision === "declined") throw new Error("declined");
69
+ if (decision === "timeout") throw new Error("approval timed out");
70
+ if (decision !== "approved") throw new Error("approval cancelled");
71
+ }
72
+ const ctx = {
73
+ sessionKey: options.sessionKey ?? "",
74
+ callId: toolCallId,
75
+ name,
76
+ log
77
+ };
78
+ const result = await capability.handler(input, ctx);
79
+ if (capability.outputZod) {
80
+ const check = capability.outputZod.safeParse(result);
81
+ if (!check.success) {
82
+ log.warn(
83
+ `[useChatActions] ${name} returned a value that does not match its declared output schema: ${describeError(check.error)}`
84
+ );
85
+ }
86
+ }
87
+ return result ?? null;
88
+ };
89
+ }
90
+ const handlers = new Proxy({}, {
91
+ get: (_target, key) => typeof key === "string" ? handlerFor(key) : void 0,
92
+ has: (_target, key) => typeof key === "string" && exposed().has(key),
93
+ ownKeys: () => [...exposed().keys()],
94
+ getOwnPropertyDescriptor: (_target, key) => {
95
+ const handler = typeof key === "string" ? handlerFor(key) : void 0;
96
+ return handler ? { value: handler, enumerable: true, configurable: true, writable: false } : void 0;
97
+ }
98
+ });
99
+ return { descriptors, handlers };
100
+ }
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Server half of the AI-SDK chat binding (`@skaile/forge-app-actions/chat/server`).
3
+ *
4
+ * The browser sends the descriptors of its LLM-audience actions in the chat
5
+ * request body (`clientTools`). This module validates that untrusted blob so a
6
+ * host can hand it straight to `createAiSdkChatHandler`:
7
+ *
8
+ * ```ts
9
+ * createAiSdkChatHandler({ clientTools: (ctx) => clientToolsFromBody(ctx.body), … })
10
+ * ```
11
+ *
12
+ * No Vue, no Nuxt, no `ai` import — safe for any Nitro route.
13
+ */
14
+ import type { ClientToolDecls } from "./types.js";
15
+ /** Same pattern forge-chat's handler enforces; checked here first for a precise message. */
16
+ export declare const CLIENT_TOOL_NAME_PATTERN: RegExp;
17
+ /** Upper bound on a tool description — descriptors come from an untrusted browser. */
18
+ export declare const CLIENT_TOOL_DESCRIPTION_MAX = 2000;
19
+ /**
20
+ * Thrown for a malformed `clientTools` body field. Carries `statusCode: 400`, so
21
+ * `createAiSdkChatHandler` answers with that status when it is thrown from an
22
+ * option callback.
23
+ */
24
+ export declare class ClientToolsBodyError extends Error {
25
+ readonly statusCode = 400;
26
+ constructor(message: string);
27
+ }
28
+ /**
29
+ * Read and validate `body.clientTools`.
30
+ *
31
+ * - absent (`undefined`) → `undefined`
32
+ * - valid → the descriptors, unchanged
33
+ * - malformed → throws {@link ClientToolsBodyError} (`statusCode` 400,
34
+ * message `clientTools: <path>: <issue>; …`)
35
+ */
36
+ export declare function clientToolsFromBody(body: Record<string, unknown>): ClientToolDecls | undefined;
@@ -0,0 +1,27 @@
1
+ import * as z from "zod";
2
+ import { describeError } from "../internal/describe-error.js";
3
+ export const CLIENT_TOOL_NAME_PATTERN = /^[a-z][a-z0-9_.-]{0,63}$/i;
4
+ export const CLIENT_TOOL_DESCRIPTION_MAX = 2e3;
5
+ const clientToolsSchema = z.record(
6
+ z.string().regex(CLIENT_TOOL_NAME_PATTERN, "invalid tool name"),
7
+ z.object({
8
+ description: z.string().max(CLIENT_TOOL_DESCRIPTION_MAX),
9
+ inputSchema: z.record(z.string(), z.unknown())
10
+ })
11
+ );
12
+ export class ClientToolsBodyError extends Error {
13
+ statusCode = 400;
14
+ constructor(message) {
15
+ super(message);
16
+ this.name = "ClientToolsBodyError";
17
+ }
18
+ }
19
+ export function clientToolsFromBody(body) {
20
+ const raw = body.clientTools;
21
+ if (raw === void 0) return void 0;
22
+ const parsed = clientToolsSchema.safeParse(raw);
23
+ if (!parsed.success) {
24
+ throw new ClientToolsBodyError(`clientTools: ${describeError(parsed.error)}`);
25
+ }
26
+ return parsed.data;
27
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * The two forge-chat contracts this binding speaks. Type-only, so
3
+ * `@skaile/forge-chat` stays an optional peer.
4
+ *
5
+ * `ClientToolHandlers` is derived from `ChatSessionOptions['tools']` because
6
+ * forge-chat does not export the alias by name.
7
+ */
8
+ import type { ChatSessionOptions } from "@skaile/forge-chat/core";
9
+ export type { ClientToolDecls } from "@skaile/forge-chat/server/ai-sdk";
10
+ export type ClientToolHandlers = NonNullable<ChatSessionOptions["tools"]>;
File without changes
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Explicit entry point for the browser-side agent capability API.
3
+ *
4
+ * In a Nuxt consumer both symbols are auto-imported (the module's
5
+ * `addImportsDir` on `runtime/composables`), so application code needs no
6
+ * import at all. This subpath exists for the contexts auto-imports do not
7
+ * reach: vitest specs, plain `.ts` modules outside the Nuxt app dir, and
8
+ * non-Nuxt consumers.
9
+ *
10
+ * import { defineClientCapability } from "@skaile/forge-app-actions/client-capabilities"
11
+ *
12
+ * Since ticket 07 this is also where the *unified* action API lives —
13
+ * `defineForgeAction` / `useForgeActions`, plus `useCommandPalette` itself, so a
14
+ * consumer's tests can reach both halves of one declaration without relying on
15
+ * Nuxt's auto-imports.
16
+ *
17
+ * The Nitro-side counterpart (`ClientCapabilityBridge`) is server-only and
18
+ * ships through the `./server` subpath instead.
19
+ */
20
+ export { defineClientCapability, deprecatedClientCapabilityAlias, } from "./composables/defineClientCapability.js";
21
+ export { DEFAULT_FORGE_ACTION_AUDIENCE, defineForgeAction, type UseForgeActionsReturn, useForgeActions, } from "./composables/defineForgeAction.js";
22
+ export { type AnnounceOptions, type AnnounceResult, type CapabilityAnnounceFetcher, type UseAgentCapabilitiesReturn, useAgentCapabilities, } from "./composables/useAgentCapabilities.js";
23
+ export { type CapabilityApprovalDecision, type CapabilityApprovalOptions, type CapabilityApprovalRequest, DEFAULT_APPROVAL_TIMEOUT_MS, type UseCapabilityApprovalReturn, useCapabilityApproval, } from "./composables/useCapabilityApproval.js";
24
+ export { type CapabilityInvokedEvent, type HandleInvocationOptions, type InvocationFailure, type InvocationOutcome, type UseCapabilityInvocationsReturn, useCapabilityInvocations, } from "./composables/useCapabilityInvocations.js";
25
+ export { type UseCommandPaletteReturn, useCommandPalette, } from "./composables/useCommandPalette.js";
26
+ export { type UsePaletteArgStepReturn, usePaletteArgStep, } from "./composables/usePaletteArgStep.js";
27
+ export type { ClientCapabilityAnnouncement, ClientCapabilityContext, ClientCapabilityDescriptor, ClientCapabilityLogger, ClientCapabilityOrigin, DefineClientCapabilityArgs, DefinedClientCapability, } from "./types/client-capability.js";
28
+ export type { PaletteAction, PaletteArgOption, PaletteArgPrompt, } from "./types/command-palette.js";
29
+ export type { DefineForgeActionArgs, ForgeAction, ForgeActionArg, ForgeActionAudience, ForgeActionContext, } from "./types/forge-action.js";
@@ -0,0 +1,25 @@
1
+ export {
2
+ defineClientCapability,
3
+ deprecatedClientCapabilityAlias
4
+ } from "./composables/defineClientCapability.js";
5
+ export {
6
+ DEFAULT_FORGE_ACTION_AUDIENCE,
7
+ defineForgeAction,
8
+ useForgeActions
9
+ } from "./composables/defineForgeAction.js";
10
+ export {
11
+ useAgentCapabilities
12
+ } from "./composables/useAgentCapabilities.js";
13
+ export {
14
+ DEFAULT_APPROVAL_TIMEOUT_MS,
15
+ useCapabilityApproval
16
+ } from "./composables/useCapabilityApproval.js";
17
+ export {
18
+ useCapabilityInvocations
19
+ } from "./composables/useCapabilityInvocations.js";
20
+ export {
21
+ useCommandPalette
22
+ } from "./composables/useCommandPalette.js";
23
+ export {
24
+ usePaletteArgStep
25
+ } from "./composables/usePaletteArgStep.js";
@@ -0,0 +1,2 @@
1
+ declare const _default: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
2
+ export default _default;
@@ -0,0 +1,125 @@
1
+ <script setup>
2
+ import { computed, onMounted, onUnmounted, ref } from "vue";
3
+ import { useCapabilityApproval } from "../composables/useCapabilityApproval";
4
+ const { current, approve, decline } = useCapabilityApproval();
5
+ const title = computed(() => current.value?.displayName || current.value?.name || "");
6
+ const argumentRows = computed(() => {
7
+ const input = current.value?.input;
8
+ if (input === void 0 || input === null) return [];
9
+ if (typeof input === "object" && !Array.isArray(input)) {
10
+ return Object.entries(input).map(([key, value]) => ({
11
+ key,
12
+ value: format(value)
13
+ }));
14
+ }
15
+ return [{ key: "", value: format(input) }];
16
+ });
17
+ function format(value) {
18
+ if (typeof value === "string") return value;
19
+ try {
20
+ return JSON.stringify(value);
21
+ } catch {
22
+ return String(value);
23
+ }
24
+ }
25
+ const now = ref(Date.now());
26
+ let ticker;
27
+ onMounted(() => {
28
+ ticker = setInterval(() => {
29
+ now.value = Date.now();
30
+ }, 500);
31
+ });
32
+ onUnmounted(() => clearInterval(ticker));
33
+ const secondsLeft = computed(() => {
34
+ const request = current.value;
35
+ if (request?.expiresAt === void 0) return null;
36
+ const remaining = Math.min(
37
+ request.expiresAt - now.value,
38
+ request.timeoutMs ?? Number.MAX_SAFE_INTEGER
39
+ );
40
+ return Math.max(0, Math.ceil(remaining / 1e3));
41
+ });
42
+ function onApprove() {
43
+ const id = current.value?.id;
44
+ if (id) approve(id);
45
+ }
46
+ function onDecline() {
47
+ const id = current.value?.id;
48
+ if (id) decline(id);
49
+ }
50
+ </script>
51
+
52
+ <template>
53
+ <UModal
54
+ :open="current !== null"
55
+ title="Allow this action?"
56
+ :ui="{ content: 'max-w-[480px] w-[95vw]' }"
57
+ @update:open="(v) => !v && onDecline()"
58
+ >
59
+ <template #content>
60
+ <div class="forge-capability-approval" data-testid="capability-approval" @keydown.esc.prevent="onDecline">
61
+ <h2 class="forge-capability-approval__heading">Allow this action?</h2>
62
+
63
+ <template v-if="current">
64
+ <p class="forge-capability-approval__intro">
65
+ The agent wants to run
66
+ <strong data-testid="capability-approval-title">{{ title }}</strong>
67
+ </p>
68
+
69
+ <code class="forge-capability-approval__name" data-testid="capability-approval-name">{{
70
+ current.name
71
+ }}</code>
72
+
73
+ <p
74
+ v-if="current.description"
75
+ class="forge-capability-approval__description"
76
+ data-testid="capability-approval-description"
77
+ >
78
+ {{ current.description }}
79
+ </p>
80
+
81
+ <div class="forge-capability-approval__args" data-testid="capability-approval-args">
82
+ <div class="forge-capability-approval__args-label">Arguments</div>
83
+ <div v-if="argumentRows.length === 0" class="forge-capability-approval__arg-empty">
84
+ none
85
+ </div>
86
+ <div
87
+ v-for="row in argumentRows"
88
+ :key="row.key"
89
+ class="forge-capability-approval__arg"
90
+ >
91
+ <span v-if="row.key" class="forge-capability-approval__arg-key">{{ row.key }}</span>
92
+ <span class="forge-capability-approval__arg-value">{{ row.value }}</span>
93
+ </div>
94
+ </div>
95
+
96
+ <p
97
+ v-if="secondsLeft !== null"
98
+ class="forge-capability-approval__countdown"
99
+ data-testid="capability-approval-countdown"
100
+ >
101
+ Expires in {{ secondsLeft }}s if unanswered.
102
+ </p>
103
+ </template>
104
+
105
+ <div class="forge-capability-approval__actions">
106
+ <!-- Decline is focused first on purpose: this dialog can appear while
107
+ the user is typing, and a stray Enter must not approve an action. -->
108
+ <UButton
109
+ label="Decline"
110
+ color="neutral"
111
+ variant="subtle"
112
+ autofocus
113
+ data-testid="capability-approval-decline"
114
+ @click="onDecline"
115
+ />
116
+ <UButton label="Allow" data-testid="capability-approval-approve" @click="onApprove" />
117
+ </div>
118
+ </div>
119
+ </template>
120
+ </UModal>
121
+ </template>
122
+
123
+ <style scoped>
124
+ .forge-capability-approval{display:flex;flex-direction:column;font-size:.9rem;gap:10px;padding:16px}.forge-capability-approval__heading{font-size:1rem;font-weight:600;margin:0}.forge-capability-approval__intro{margin:0}.forge-capability-approval__name{align-self:flex-start;background:#f1f5f9;border-radius:4px;color:#475569;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:.8rem;padding:2px 6px}.forge-capability-approval__description{color:#64748b;margin:0}.forge-capability-approval__args{background:#f8fafc;border:1px solid #e2e8f0;border-radius:6px;display:flex;flex-direction:column;gap:4px;padding:8px 10px}.forge-capability-approval__args-label{color:#94a3b8;font-size:.7rem;font-weight:700;letter-spacing:.08em;text-transform:uppercase}.forge-capability-approval__arg{align-items:baseline;display:flex;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:.8rem;gap:8px;word-break:break-all}.forge-capability-approval__arg-key{color:#64748b;flex-shrink:0}.forge-capability-approval__arg-key:after{content:":"}.forge-capability-approval__arg-empty{color:#94a3b8;font-size:.8rem}.forge-capability-approval__countdown{color:#94a3b8;font-size:.75rem;margin:0}.forge-capability-approval__actions{display:flex;gap:8px;justify-content:flex-end;margin-top:4px}
125
+ </style>
@@ -0,0 +1,2 @@
1
+ declare const _default: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
2
+ export default _default;
@@ -0,0 +1,2 @@
1
+ declare const _default: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
2
+ export default _default;