@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
package/README.md
CHANGED
|
@@ -1,3 +1,35 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @skaile/forge-app-actions
|
|
2
2
|
|
|
3
|
-
|
|
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 };
|
package/dist/module.json
ADDED
package/dist/module.mjs
ADDED
|
@@ -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
|
+
}
|