@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
package/README.md CHANGED
@@ -1,3 +1,35 @@
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
+ | `./server` | `clientCapabilityBridge`, `capabilityRegistry`, `routeCapabilityInvocation`, … |
33
+ | `./types` | `ClientCapability*`, `ForgeAction*`, `Palette*` types |
34
+
35
+ Moved here from `@skaile-ai/forge-common` 9.x (`./client-capabilities` and the capability parts of `./server` and `./types`).
@@ -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.1.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,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;
@@ -0,0 +1,218 @@
1
+ <script setup>
2
+ import { computed, nextTick, onMounted, onUnmounted, ref, watch } from "vue";
3
+ import { useCommandPalette } from "../composables/useCommandPalette";
4
+ import { usePaletteArgStep } from "../composables/usePaletteArgStep";
5
+ const { queryActions, isOpen, open, close } = useCommandPalette();
6
+ const search = ref("");
7
+ const selectedIndex = ref(0);
8
+ const searchInputRef = ref(null);
9
+ const argStep = usePaletteArgStep();
10
+ const filteredActions = computed(() => argStep.active.value ? [] : queryActions(search.value));
11
+ const rowCount = computed(
12
+ () => argStep.active.value ? argStep.options.value.length : filteredActions.value.length
13
+ );
14
+ watch(filteredActions, () => {
15
+ selectedIndex.value = 0;
16
+ });
17
+ watch(search, (value) => {
18
+ if (!argStep.active.value) return;
19
+ selectedIndex.value = 0;
20
+ void argStep.query(value);
21
+ });
22
+ watch(isOpen, (open2) => {
23
+ if (!open2) {
24
+ search.value = "";
25
+ selectedIndex.value = 0;
26
+ argStep.cancel();
27
+ } else {
28
+ nextTick(() => focusSearch());
29
+ }
30
+ });
31
+ function focusSearch() {
32
+ searchInputRef.value?.inputRef?.focus();
33
+ }
34
+ function getGroupHeader(index) {
35
+ const action = filteredActions.value[index];
36
+ if (!action?.group) return null;
37
+ if (index === 0) return action.group;
38
+ const prev = filteredActions.value[index - 1];
39
+ if (prev?.group !== action.group) return action.group;
40
+ return null;
41
+ }
42
+ function handleDialogKeydown(e) {
43
+ if (e.key === "Escape") {
44
+ e.preventDefault();
45
+ if (argStep.active.value) {
46
+ argStep.cancel();
47
+ search.value = "";
48
+ selectedIndex.value = 0;
49
+ return;
50
+ }
51
+ close();
52
+ return;
53
+ }
54
+ const count = rowCount.value;
55
+ if (e.key === "ArrowDown") {
56
+ if (count === 0) return;
57
+ e.preventDefault();
58
+ selectedIndex.value = (selectedIndex.value + 1) % count;
59
+ } else if (e.key === "ArrowUp") {
60
+ if (count === 0) return;
61
+ e.preventDefault();
62
+ selectedIndex.value = (selectedIndex.value - 1 + count) % count;
63
+ } else if (e.key === "Enter") {
64
+ e.preventDefault();
65
+ if (argStep.active.value) {
66
+ void submitArg();
67
+ } else {
68
+ void selectAction(filteredActions.value[selectedIndex.value]);
69
+ }
70
+ }
71
+ }
72
+ async function runAction(action, value) {
73
+ try {
74
+ await action.handler(value);
75
+ } catch (err) {
76
+ console.error(`[ForgeCommandPalette] action "${action.id}" failed:`, err);
77
+ }
78
+ }
79
+ async function selectAction(action) {
80
+ if (!action) return;
81
+ if (argStep.begin(action)) {
82
+ search.value = "";
83
+ selectedIndex.value = 0;
84
+ nextTick(() => focusSearch());
85
+ return;
86
+ }
87
+ close();
88
+ await runAction(action);
89
+ }
90
+ async function submitArg() {
91
+ const action = argStep.action.value;
92
+ if (!action) return;
93
+ const value = argStep.valueFor(search.value, selectedIndex.value);
94
+ if (value === null) return;
95
+ argStep.cancel();
96
+ close();
97
+ await runAction(action, value);
98
+ }
99
+ async function selectArgOption(index) {
100
+ selectedIndex.value = index;
101
+ await submitArg();
102
+ }
103
+ function handleGlobalKeydown(e) {
104
+ if ((e.metaKey || e.ctrlKey) && e.key === "k") {
105
+ e.preventDefault();
106
+ open();
107
+ }
108
+ }
109
+ onMounted(() => {
110
+ document.addEventListener("keydown", handleGlobalKeydown);
111
+ });
112
+ onUnmounted(() => {
113
+ document.removeEventListener("keydown", handleGlobalKeydown);
114
+ });
115
+ </script>
116
+
117
+ <template>
118
+ <!-- Nuxt UI Modal as the palette backdrop. #content gives full control of the
119
+ panel (no default header/footer). -->
120
+ <UModal
121
+ :open="isOpen"
122
+ :ui="{ content: 'max-w-[560px] w-[95vw] p-0' }"
123
+ @update:open="(v) => !v && close()"
124
+ >
125
+ <template #content>
126
+ <div class="forge-command-palette" @keydown="handleDialogKeydown">
127
+ <div v-if="argStep.active.value" class="forge-command-palette__breadcrumb">
128
+ {{ argStep.prompt.value?.label ?? argStep.action.value?.label }}
129
+ </div>
130
+
131
+ <!-- Search input -->
132
+ <div class="forge-command-palette__search">
133
+ <UInput
134
+ ref="searchInputRef"
135
+ v-model="search"
136
+ :placeholder="argStep.active.value ? argStep.prompt.value?.placeholder ?? 'Type a value\u2026' : 'Search commands\u2026'"
137
+ variant="none"
138
+ autofocus
139
+ :ui="{ root: 'w-full' }"
140
+ class="forge-command-palette__input"
141
+ />
142
+ </div>
143
+
144
+ <!-- Results list -->
145
+ <div class="forge-command-palette__list" role="listbox">
146
+ <!-- Argument step: the pending action's candidates -->
147
+ <template v-if="argStep.active.value">
148
+ <div v-if="argStep.options.value.length === 0" class="forge-command-palette__empty">
149
+ {{ argStep.loading.value ? "Searching\u2026" : "Type a value and press Enter" }}
150
+ </div>
151
+ <button
152
+ v-for="(option, index) in argStep.options.value"
153
+ :key="option.value"
154
+ type="button"
155
+ class="forge-command-palette__item"
156
+ :class="{ 'forge-command-palette__item--selected': index === selectedIndex }"
157
+ role="option"
158
+ :aria-selected="index === selectedIndex"
159
+ @click="selectArgOption(index)"
160
+ @mouseenter="selectedIndex = index"
161
+ >
162
+ <span v-if="option.icon" class="forge-command-palette__icon">
163
+ <Icon :name="option.icon" />
164
+ </span>
165
+ <span class="forge-command-palette__text">
166
+ <span class="forge-command-palette__label">{{ option.label }}</span>
167
+ <span
168
+ v-if="option.description"
169
+ class="forge-command-palette__description"
170
+ >{{ option.description }}</span>
171
+ </span>
172
+ </button>
173
+ </template>
174
+
175
+ <template v-if="!argStep.active.value && filteredActions.length === 0">
176
+ <div class="forge-command-palette__empty">No commands found</div>
177
+ </template>
178
+
179
+ <template v-for="(action, index) in filteredActions" :key="action.id">
180
+ <!-- Group header -->
181
+ <div
182
+ v-if="getGroupHeader(index)"
183
+ class="forge-command-palette__group"
184
+ >
185
+ {{ getGroupHeader(index) }}
186
+ </div>
187
+
188
+ <!-- Action row -->
189
+ <button
190
+ type="button"
191
+ class="forge-command-palette__item"
192
+ :class="{ 'forge-command-palette__item--selected': index === selectedIndex }"
193
+ role="option"
194
+ :aria-selected="index === selectedIndex"
195
+ @click="selectAction(action)"
196
+ @mouseenter="selectedIndex = index"
197
+ >
198
+ <span v-if="action.icon" class="forge-command-palette__icon">
199
+ <Icon :name="action.icon" />
200
+ </span>
201
+ <span class="forge-command-palette__text">
202
+ <span class="forge-command-palette__label">{{ action.label }}</span>
203
+ <span
204
+ v-if="action.description"
205
+ class="forge-command-palette__description"
206
+ >{{ action.description }}</span>
207
+ </span>
208
+ </button>
209
+ </template>
210
+ </div>
211
+ </div>
212
+ </template>
213
+ </UModal>
214
+ </template>
215
+
216
+ <style scoped>
217
+ .forge-command-palette__breadcrumb{color:#64748b;font-size:.75rem;font-weight:600;padding:10px 12px 0}.forge-command-palette{border-radius:8px;display:flex;flex-direction:column;overflow:hidden}.forge-command-palette__search{border-bottom:1px solid #e2e8f0;padding:12px}.forge-command-palette__input{font-size:.95rem;width:100%}.forge-command-palette__list{max-height:360px;overflow-y:auto;padding:6px}.forge-command-palette__empty{color:#94a3b8;font-size:.875rem;padding:16px;text-align:center}.forge-command-palette__group{color:#94a3b8;font-size:.7rem;font-weight:700;letter-spacing:.08em;margin-top:4px;padding:4px 8px 2px;text-transform:uppercase}.forge-command-palette__item{align-items:center;background:transparent;border:none;border-radius:6px;color:inherit;cursor:pointer;display:flex;font:inherit;gap:10px;padding:8px 10px;text-align:left;transition:background .1s ease;width:100%}.forge-command-palette__item--selected{background:#f1f5f9}.forge-command-palette__icon{color:#64748b;flex-shrink:0;width:20px}.forge-command-palette__text{display:flex;flex-direction:column;gap:1px;min-width:0}.forge-command-palette__label{font-size:.875rem;font-weight:500}.forge-command-palette__description{color:#94a3b8;font-size:.775rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
218
+ </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,64 @@
1
+ /**
2
+ * `defineClientCapability` — authoring helper for a browser-side agent
3
+ * capability. Mirrors `@skaile/workspaces/app-sdk`'s `defineAppCapability`
4
+ * field-for-field; see `../types/client-capability` for the two deliberate
5
+ * divergences (`side`, `origin`).
6
+ *
7
+ * Auto-imported in consumer apps via the module's `addImportsDir` on
8
+ * `runtime/composables`. Also reachable explicitly through the
9
+ * `@skaile/forge-app-actions/client-capabilities` subpath.
10
+ */
11
+ import * as z from "zod";
12
+ import type { DefineClientCapabilityArgs, DefinedClientCapability } from "../types/client-capability.js";
13
+ /**
14
+ * Build a {@link DefinedClientCapability} from author-friendly Zod-typed args.
15
+ *
16
+ * The wire `inputSchema` / `outputSchema` are **derived** from the Zod schemas
17
+ * via `z.toJSONSchema()` (zod v4 only) — they are never hand-written, which is
18
+ * what buys the validation the raw-JSON-Schema capabilities never had.
19
+ *
20
+ * `side` is fixed to `"client"`. `origin` is stamped by `ClientCapabilityBridge`
21
+ * when the announcement reaches Nitro.
22
+ *
23
+ * @example
24
+ * ```ts
25
+ * export const openFile = defineClientCapability({
26
+ * name: "app.workspace.open_file",
27
+ * displayName: "Open file in viewer",
28
+ * description: "Open a file in the user's file viewer.",
29
+ * kind: "effect",
30
+ * audience: ["llm", "user"],
31
+ * input: z.object({ path: z.string().min(1), line: z.number().optional() }),
32
+ * output: z.object({ opened: z.boolean(), path: z.string() }),
33
+ * handler: async ({ path, line }) => ({ opened: await openInViewer(path, line), path }),
34
+ * })
35
+ * ```
36
+ */
37
+ export declare function defineClientCapability<T extends z.ZodType, R extends z.ZodType | undefined = undefined>(args: DefineClientCapabilityArgs<T, R>): DefinedClientCapability;
38
+ /**
39
+ * Register an old capability name as a **deprecated alias** of a defined one.
40
+ *
41
+ * Renaming a capability is not free: `promptFragment` text names the old name
42
+ * literally, and agent definitions in the wild may too (spec §2.2). The alias is
43
+ * a second registry entry — same `inputZod`, same `outputZod`, same handler
44
+ * *instance* — so an invocation of either name runs exactly the same code, and
45
+ * the alias needs no separate maintenance.
46
+ *
47
+ * Two deliberate differences from the capability it aliases:
48
+ * - `audience` is narrowed to `["llm"]`. The alias exists only for callers that
49
+ * already know the old name; a deprecated duplicate has no business appearing
50
+ * in a user-facing surface like Cmd+K.
51
+ * - `description` and `promptFragment` are prefixed with a deprecation notice
52
+ * naming the replacement, because the wire `Capability` type has no
53
+ * `deprecated` flag — the model reads prose or it reads nothing.
54
+ *
55
+ * @param capability the current definition, which owns the handler
56
+ * @param name the old name to keep answering
57
+ *
58
+ * @example
59
+ * ```ts
60
+ * export const openFile = defineClientCapability({ name: "app.workspace.open_file", … })
61
+ * export const openFileAlias = deprecatedClientCapabilityAlias(openFile, "forge.open_file")
62
+ * ```
63
+ */
64
+ export declare function deprecatedClientCapabilityAlias(capability: DefinedClientCapability, name: string): DefinedClientCapability;
@@ -0,0 +1,40 @@
1
+ import * as z from "zod";
2
+ export function defineClientCapability(args) {
3
+ const inputSchema = z.toJSONSchema(args.input, {
4
+ target: "draft-07",
5
+ io: "input"
6
+ });
7
+ const outputSchema = args.output ? z.toJSONSchema(args.output, {
8
+ target: "draft-07",
9
+ io: "output"
10
+ }) : void 0;
11
+ return {
12
+ name: args.name,
13
+ displayName: args.displayName,
14
+ description: args.description,
15
+ inputSchema,
16
+ outputSchema,
17
+ side: "client",
18
+ scope: args.scope,
19
+ requiresApproval: args.requiresApproval,
20
+ kind: args.kind,
21
+ audience: args.audience,
22
+ promptFragment: args.promptFragment,
23
+ callTimeoutMs: args.callTimeoutMs,
24
+ render: args.render,
25
+ inputZod: args.input,
26
+ outputZod: args.output,
27
+ handler: args.handler
28
+ };
29
+ }
30
+ export function deprecatedClientCapabilityAlias(capability, name) {
31
+ const notice = `DEPRECATED: use \`${capability.name}\` instead. This name still works but will be removed.`;
32
+ return {
33
+ ...capability,
34
+ name,
35
+ description: `${notice} ${capability.description}`,
36
+ audience: ["llm"],
37
+ promptFragment: capability.promptFragment ? `${notice}
38
+ ${capability.promptFragment}` : void 0
39
+ };
40
+ }