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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/README.md +34 -2
  2. package/dist/module.d.mts +23 -0
  3. package/dist/module.json +9 -0
  4. package/dist/module.mjs +37 -0
  5. package/dist/runtime/client-capabilities.d.ts +29 -0
  6. package/dist/runtime/client-capabilities.js +25 -0
  7. package/dist/runtime/components-nuxt-ui/ForgeCapabilityApproval.d.vue.ts +2 -0
  8. package/dist/runtime/components-nuxt-ui/ForgeCapabilityApproval.vue +125 -0
  9. package/dist/runtime/components-nuxt-ui/ForgeCapabilityApproval.vue.d.ts +2 -0
  10. package/dist/runtime/components-nuxt-ui/ForgeCommandPalette.d.vue.ts +2 -0
  11. package/dist/runtime/components-nuxt-ui/ForgeCommandPalette.vue +218 -0
  12. package/dist/runtime/components-nuxt-ui/ForgeCommandPalette.vue.d.ts +2 -0
  13. package/dist/runtime/composables/defineClientCapability.d.ts +64 -0
  14. package/dist/runtime/composables/defineClientCapability.js +40 -0
  15. package/dist/runtime/composables/defineForgeAction.d.ts +82 -0
  16. package/dist/runtime/composables/defineForgeAction.js +147 -0
  17. package/dist/runtime/composables/useAgentCapabilities.d.ts +78 -0
  18. package/dist/runtime/composables/useAgentCapabilities.js +94 -0
  19. package/dist/runtime/composables/useCapabilityApproval.d.ts +109 -0
  20. package/dist/runtime/composables/useCapabilityApproval.js +74 -0
  21. package/dist/runtime/composables/useCapabilityInvocations.d.ts +150 -0
  22. package/dist/runtime/composables/useCapabilityInvocations.js +129 -0
  23. package/dist/runtime/composables/useCommandPalette.d.ts +28 -0
  24. package/dist/runtime/composables/useCommandPalette.js +49 -0
  25. package/dist/runtime/composables/usePaletteArgStep.d.ts +47 -0
  26. package/dist/runtime/composables/usePaletteArgStep.js +51 -0
  27. package/dist/runtime/server/index.d.ts +7 -0
  28. package/dist/runtime/server/index.js +2 -0
  29. package/dist/runtime/server/utils/capability-registry.d.ts +35 -0
  30. package/dist/runtime/server/utils/capability-registry.js +24 -0
  31. package/dist/runtime/server/utils/client-capability-bridge.d.ts +647 -0
  32. package/dist/runtime/server/utils/client-capability-bridge.js +755 -0
  33. package/dist/runtime/shims-vue.d.ts +8 -0
  34. package/dist/runtime/types/client-capability.d.ts +159 -0
  35. package/dist/runtime/types/client-capability.js +0 -0
  36. package/dist/runtime/types/command-palette.d.ts +75 -0
  37. package/dist/runtime/types/command-palette.js +0 -0
  38. package/dist/runtime/types/forge-action.d.ts +152 -0
  39. package/dist/runtime/types/forge-action.js +0 -0
  40. package/dist/runtime/types/index.d.ts +3 -0
  41. package/dist/runtime/types/index.js +0 -0
  42. package/dist/types.d.mts +9 -0
  43. package/package.json +81 -4
  44. package/src/runtime/client-capabilities.ts +82 -0
  45. package/src/runtime/components-nuxt-ui/ForgeCapabilityApproval.vue +240 -0
  46. package/src/runtime/components-nuxt-ui/ForgeCommandPalette.vue +363 -0
  47. package/src/runtime/composables/defineClientCapability.ts +119 -0
  48. package/src/runtime/composables/defineForgeAction.ts +278 -0
  49. package/src/runtime/composables/useAgentCapabilities.ts +215 -0
  50. package/src/runtime/composables/useCapabilityApproval.ts +219 -0
  51. package/src/runtime/composables/useCapabilityInvocations.ts +343 -0
  52. package/src/runtime/composables/useCommandPalette.ts +97 -0
  53. package/src/runtime/composables/usePaletteArgStep.ts +110 -0
  54. package/src/runtime/server/index.ts +8 -0
  55. package/src/runtime/server/utils/capability-registry.ts +66 -0
  56. package/src/runtime/server/utils/client-capability-bridge.ts +1201 -0
  57. package/src/runtime/shims-vue.d.ts +8 -0
  58. package/src/runtime/types/client-capability.ts +178 -0
  59. package/src/runtime/types/command-palette.ts +78 -0
  60. package/src/runtime/types/forge-action.ts +166 -0
  61. package/src/runtime/types/index.ts +24 -0
@@ -0,0 +1,240 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * ForgeCapabilityApproval — the confirm gate for `requiresApproval`
4
+ * capabilities, built on UModal + UButton.
5
+ *
6
+ * No props, no emits. All state lives in the `useCapabilityApproval` singleton,
7
+ * which `useCapabilityInvocations().handle()` feeds. Mount once in `app.vue`.
8
+ *
9
+ * Closing without choosing (Escape, backdrop) counts as a decline. The safe
10
+ * default for "should this thing touch your workspace?" is no.
11
+ */
12
+ import { computed, onMounted, onUnmounted, ref } from "vue";
13
+ import { useCapabilityApproval } from "../composables/useCapabilityApproval";
14
+
15
+ const { current, approve, decline } = useCapabilityApproval();
16
+
17
+ const title = computed(() => current.value?.displayName || current.value?.name || "");
18
+
19
+ /**
20
+ * The arguments, as rows. An object becomes one row per key; anything else
21
+ * (a bare string, a number) becomes a single unnamed row. The user is
22
+ * approving a specific call, so the specific values have to be on screen.
23
+ */
24
+ const argumentRows = computed<Array<{ key: string; value: string }>>(() => {
25
+ const input = current.value?.input;
26
+ if (input === undefined || input === null) return [];
27
+ if (typeof input === "object" && !Array.isArray(input)) {
28
+ return Object.entries(input as Record<string, unknown>).map(([key, value]) => ({
29
+ key,
30
+ value: format(value),
31
+ }));
32
+ }
33
+ return [{ key: "", value: format(input) }];
34
+ });
35
+
36
+ function format(value: unknown): string {
37
+ if (typeof value === "string") return value;
38
+ try {
39
+ return JSON.stringify(value);
40
+ } catch {
41
+ return String(value);
42
+ }
43
+ }
44
+
45
+ // ── Countdown ──────────────────────────────────────────────────────────────
46
+ // A gate with an invisible deadline is a trap: the user needs to know the
47
+ // dialog will answer for them if they walk away.
48
+ const now = ref(Date.now());
49
+ // Started on mount: the component renders on the server too, where an
50
+ // interval would never be cleared.
51
+ let ticker: ReturnType<typeof setInterval> | undefined;
52
+ onMounted(() => {
53
+ ticker = setInterval(() => {
54
+ now.value = Date.now();
55
+ }, 500);
56
+ });
57
+ onUnmounted(() => clearInterval(ticker));
58
+
59
+ const secondsLeft = computed<number | null>(() => {
60
+ const request = current.value;
61
+ if (request?.expiresAt === undefined) return null;
62
+ // Clamped to the declared window: `now` only ticks twice a second and is
63
+ // seeded at mount, which is earlier than any request it will ever show.
64
+ const remaining = Math.min(
65
+ request.expiresAt - now.value,
66
+ request.timeoutMs ?? Number.MAX_SAFE_INTEGER,
67
+ );
68
+ return Math.max(0, Math.ceil(remaining / 1000));
69
+ });
70
+
71
+ function onApprove() {
72
+ const id = current.value?.id;
73
+ if (id) approve(id);
74
+ }
75
+
76
+ function onDecline() {
77
+ const id = current.value?.id;
78
+ if (id) decline(id);
79
+ }
80
+ </script>
81
+
82
+ <template>
83
+ <UModal
84
+ :open="current !== null"
85
+ title="Allow this action?"
86
+ :ui="{ content: 'max-w-[480px] w-[95vw]' }"
87
+ @update:open="(v: boolean) => !v && onDecline()"
88
+ >
89
+ <template #content>
90
+ <div class="forge-capability-approval" data-testid="capability-approval" @keydown.esc.prevent="onDecline">
91
+ <h2 class="forge-capability-approval__heading">Allow this action?</h2>
92
+
93
+ <template v-if="current">
94
+ <p class="forge-capability-approval__intro">
95
+ The agent wants to run
96
+ <strong data-testid="capability-approval-title">{{ title }}</strong>
97
+ </p>
98
+
99
+ <code class="forge-capability-approval__name" data-testid="capability-approval-name">{{
100
+ current.name
101
+ }}</code>
102
+
103
+ <p
104
+ v-if="current.description"
105
+ class="forge-capability-approval__description"
106
+ data-testid="capability-approval-description"
107
+ >
108
+ {{ current.description }}
109
+ </p>
110
+
111
+ <div class="forge-capability-approval__args" data-testid="capability-approval-args">
112
+ <div class="forge-capability-approval__args-label">Arguments</div>
113
+ <div v-if="argumentRows.length === 0" class="forge-capability-approval__arg-empty">
114
+ none
115
+ </div>
116
+ <div
117
+ v-for="row in argumentRows"
118
+ :key="row.key"
119
+ class="forge-capability-approval__arg"
120
+ >
121
+ <span v-if="row.key" class="forge-capability-approval__arg-key">{{ row.key }}</span>
122
+ <span class="forge-capability-approval__arg-value">{{ row.value }}</span>
123
+ </div>
124
+ </div>
125
+
126
+ <p
127
+ v-if="secondsLeft !== null"
128
+ class="forge-capability-approval__countdown"
129
+ data-testid="capability-approval-countdown"
130
+ >
131
+ Expires in {{ secondsLeft }}s if unanswered.
132
+ </p>
133
+ </template>
134
+
135
+ <div class="forge-capability-approval__actions">
136
+ <!-- Decline is focused first on purpose: this dialog can appear while
137
+ the user is typing, and a stray Enter must not approve an action. -->
138
+ <UButton
139
+ label="Decline"
140
+ color="neutral"
141
+ variant="subtle"
142
+ autofocus
143
+ data-testid="capability-approval-decline"
144
+ @click="onDecline"
145
+ />
146
+ <UButton label="Allow" data-testid="capability-approval-approve" @click="onApprove" />
147
+ </div>
148
+ </div>
149
+ </template>
150
+ </UModal>
151
+ </template>
152
+
153
+ <style scoped>
154
+ .forge-capability-approval {
155
+ display: flex;
156
+ flex-direction: column;
157
+ gap: 10px;
158
+ padding: 16px;
159
+ font-size: 0.9rem;
160
+ }
161
+
162
+ .forge-capability-approval__heading {
163
+ margin: 0;
164
+ font-size: 1rem;
165
+ font-weight: 600;
166
+ }
167
+
168
+ .forge-capability-approval__intro {
169
+ margin: 0;
170
+ }
171
+
172
+ .forge-capability-approval__name {
173
+ align-self: flex-start;
174
+ padding: 2px 6px;
175
+ border-radius: 4px;
176
+ background: #f1f5f9;
177
+ font-size: 0.8rem;
178
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
179
+ color: #475569;
180
+ }
181
+
182
+ .forge-capability-approval__description {
183
+ margin: 0;
184
+ color: #64748b;
185
+ }
186
+
187
+ .forge-capability-approval__args {
188
+ display: flex;
189
+ flex-direction: column;
190
+ gap: 4px;
191
+ padding: 8px 10px;
192
+ border-radius: 6px;
193
+ background: #f8fafc;
194
+ border: 1px solid #e2e8f0;
195
+ }
196
+
197
+ .forge-capability-approval__args-label {
198
+ font-size: 0.7rem;
199
+ font-weight: 700;
200
+ text-transform: uppercase;
201
+ letter-spacing: 0.08em;
202
+ color: #94a3b8;
203
+ }
204
+
205
+ .forge-capability-approval__arg {
206
+ display: flex;
207
+ gap: 8px;
208
+ align-items: baseline;
209
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
210
+ font-size: 0.8rem;
211
+ word-break: break-all;
212
+ }
213
+
214
+ .forge-capability-approval__arg-key {
215
+ color: #64748b;
216
+ flex-shrink: 0;
217
+ }
218
+
219
+ .forge-capability-approval__arg-key::after {
220
+ content: ":";
221
+ }
222
+
223
+ .forge-capability-approval__arg-empty {
224
+ color: #94a3b8;
225
+ font-size: 0.8rem;
226
+ }
227
+
228
+ .forge-capability-approval__countdown {
229
+ margin: 0;
230
+ font-size: 0.75rem;
231
+ color: #94a3b8;
232
+ }
233
+
234
+ .forge-capability-approval__actions {
235
+ display: flex;
236
+ justify-content: flex-end;
237
+ gap: 8px;
238
+ margin-top: 4px;
239
+ }
240
+ </style>
@@ -0,0 +1,363 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * ForgeCommandPalette — global Cmd+K / Ctrl+K command palette, built on
4
+ * UModal (backdrop) + UInput (search).
5
+ *
6
+ * No props, no emits. All state lives in the useCommandPalette singleton.
7
+ * Mount once in app.vue (outside any auth gate).
8
+ */
9
+ import { computed, nextTick, onMounted, onUnmounted, ref, watch } from "vue";
10
+ import { useCommandPalette } from "../composables/useCommandPalette";
11
+ import { usePaletteArgStep } from "../composables/usePaletteArgStep";
12
+ import type { PaletteAction } from "../types/command-palette";
13
+
14
+ const { queryActions, isOpen, open, close } = useCommandPalette();
15
+
16
+ // ── Search input ───────────────────────────────────────────────────────────
17
+ const search = ref("");
18
+ const selectedIndex = ref(0);
19
+ const searchInputRef = ref<{ inputRef?: HTMLInputElement } | null>(null);
20
+
21
+ // ── Argument step ──────────────────────────────────────────────────────────
22
+ // An action that declares `arg` needs one string before it can run. Selecting
23
+ // it swaps the list for its candidates and waits for a second Enter; Escape
24
+ // steps back to the action list rather than closing the palette.
25
+ const argStep = usePaletteArgStep();
26
+
27
+ // ── Filtered results ───────────────────────────────────────────────────────
28
+ const filteredActions = computed(() => (argStep.active.value ? [] : queryActions(search.value)));
29
+
30
+ /** Rows currently rendered — actions, or the pending action's candidates. */
31
+ const rowCount = computed(() =>
32
+ argStep.active.value ? argStep.options.value.length : filteredActions.value.length,
33
+ );
34
+
35
+ // Reset selection when results change
36
+ watch(filteredActions, () => {
37
+ selectedIndex.value = 0;
38
+ });
39
+
40
+ // Typing during the argument step re-queries the action's candidates.
41
+ watch(search, (value) => {
42
+ if (!argStep.active.value) return;
43
+ selectedIndex.value = 0;
44
+ void argStep.query(value);
45
+ });
46
+
47
+ // Reset search when dialog closes
48
+ watch(isOpen, (open) => {
49
+ if (!open) {
50
+ search.value = "";
51
+ selectedIndex.value = 0;
52
+ argStep.cancel();
53
+ } else {
54
+ // Focus input after dialog opens (guard: stub / no-op in tests)
55
+ nextTick(() => focusSearch());
56
+ }
57
+ });
58
+
59
+ /** Focus the palette input. Guarded: a stubbed ref in tests is a no-op. */
60
+ function focusSearch() {
61
+ searchInputRef.value?.inputRef?.focus();
62
+ }
63
+
64
+ // ── Group headers ──────────────────────────────────────────────────────────
65
+ function getGroupHeader(index: number): string | null {
66
+ const action = filteredActions.value[index];
67
+ if (!action?.group) return null;
68
+ if (index === 0) return action.group;
69
+ const prev = filteredActions.value[index - 1];
70
+ if (prev?.group !== action.group) return action.group;
71
+ return null;
72
+ }
73
+
74
+ // ── Keyboard navigation ────────────────────────────────────────────────────
75
+ function handleDialogKeydown(e: KeyboardEvent) {
76
+ // Escape and Enter must work even with an empty list: Escape backs out of the
77
+ // argument step, and a free-text argument has no rows to highlight.
78
+ if (e.key === "Escape") {
79
+ e.preventDefault();
80
+ if (argStep.active.value) {
81
+ argStep.cancel();
82
+ search.value = "";
83
+ selectedIndex.value = 0;
84
+ return;
85
+ }
86
+ close();
87
+ return;
88
+ }
89
+
90
+ const count = rowCount.value;
91
+
92
+ if (e.key === "ArrowDown") {
93
+ if (count === 0) return;
94
+ e.preventDefault();
95
+ selectedIndex.value = (selectedIndex.value + 1) % count;
96
+ } else if (e.key === "ArrowUp") {
97
+ if (count === 0) return;
98
+ e.preventDefault();
99
+ selectedIndex.value = (selectedIndex.value - 1 + count) % count;
100
+ } else if (e.key === "Enter") {
101
+ e.preventDefault();
102
+ if (argStep.active.value) {
103
+ void submitArg();
104
+ } else {
105
+ void selectAction(filteredActions.value[selectedIndex.value]);
106
+ }
107
+ }
108
+ }
109
+
110
+ /**
111
+ * A thrown action must not become an unhandled rejection — the palette has
112
+ * already closed by then and there is nothing left to show it on.
113
+ */
114
+ async function runAction(action: PaletteAction, value?: string) {
115
+ try {
116
+ await action.handler(value);
117
+ } catch (err) {
118
+ console.error(`[ForgeCommandPalette] action "${action.id}" failed:`, err);
119
+ }
120
+ }
121
+
122
+ async function selectAction(action: PaletteAction | undefined) {
123
+ if (!action) return;
124
+ // An action that needs an argument keeps the palette open for the second step.
125
+ if (argStep.begin(action)) {
126
+ search.value = "";
127
+ selectedIndex.value = 0;
128
+ nextTick(() => focusSearch());
129
+ return;
130
+ }
131
+ close();
132
+ await runAction(action);
133
+ }
134
+
135
+ /** Complete the argument step with the highlighted row or the typed value. */
136
+ async function submitArg() {
137
+ const action = argStep.action.value;
138
+ if (!action) return;
139
+ const value = argStep.valueFor(search.value, selectedIndex.value);
140
+ if (value === null) return; // nothing to run with yet
141
+ argStep.cancel();
142
+ close();
143
+ await runAction(action, value);
144
+ }
145
+
146
+ /** Click on a candidate row during the argument step. */
147
+ async function selectArgOption(index: number) {
148
+ selectedIndex.value = index;
149
+ await submitArg();
150
+ }
151
+
152
+ // ── Global Cmd+K shortcut ──────────────────────────────────────────────────
153
+ function handleGlobalKeydown(e: KeyboardEvent) {
154
+ if ((e.metaKey || e.ctrlKey) && e.key === "k") {
155
+ e.preventDefault();
156
+ open();
157
+ }
158
+ }
159
+
160
+ onMounted(() => {
161
+ document.addEventListener("keydown", handleGlobalKeydown);
162
+ });
163
+
164
+ onUnmounted(() => {
165
+ document.removeEventListener("keydown", handleGlobalKeydown);
166
+ });
167
+ </script>
168
+
169
+ <template>
170
+ <!-- Nuxt UI Modal as the palette backdrop. #content gives full control of the
171
+ panel (no default header/footer). -->
172
+ <UModal
173
+ :open="isOpen"
174
+ :ui="{ content: 'max-w-[560px] w-[95vw] p-0' }"
175
+ @update:open="(v: boolean) => !v && close()"
176
+ >
177
+ <template #content>
178
+ <div class="forge-command-palette" @keydown="handleDialogKeydown">
179
+ <div v-if="argStep.active.value" class="forge-command-palette__breadcrumb">
180
+ {{ argStep.prompt.value?.label ?? argStep.action.value?.label }}
181
+ </div>
182
+
183
+ <!-- Search input -->
184
+ <div class="forge-command-palette__search">
185
+ <UInput
186
+ ref="searchInputRef"
187
+ v-model="search"
188
+ :placeholder="argStep.active.value
189
+ ? (argStep.prompt.value?.placeholder ?? 'Type a value…')
190
+ : 'Search commands…'"
191
+ variant="none"
192
+ autofocus
193
+ :ui="{ root: 'w-full' }"
194
+ class="forge-command-palette__input"
195
+ />
196
+ </div>
197
+
198
+ <!-- Results list -->
199
+ <div class="forge-command-palette__list" role="listbox">
200
+ <!-- Argument step: the pending action's candidates -->
201
+ <template v-if="argStep.active.value">
202
+ <div v-if="argStep.options.value.length === 0" class="forge-command-palette__empty">
203
+ {{ argStep.loading.value ? "Searching…" : "Type a value and press Enter" }}
204
+ </div>
205
+ <button
206
+ v-for="(option, index) in argStep.options.value"
207
+ :key="option.value"
208
+ type="button"
209
+ class="forge-command-palette__item"
210
+ :class="{ 'forge-command-palette__item--selected': index === selectedIndex }"
211
+ role="option"
212
+ :aria-selected="index === selectedIndex"
213
+ @click="selectArgOption(index)"
214
+ @mouseenter="selectedIndex = index"
215
+ >
216
+ <span v-if="option.icon" class="forge-command-palette__icon">
217
+ <Icon :name="option.icon" />
218
+ </span>
219
+ <span class="forge-command-palette__text">
220
+ <span class="forge-command-palette__label">{{ option.label }}</span>
221
+ <span
222
+ v-if="option.description"
223
+ class="forge-command-palette__description"
224
+ >{{ option.description }}</span>
225
+ </span>
226
+ </button>
227
+ </template>
228
+
229
+ <template v-if="!argStep.active.value && filteredActions.length === 0">
230
+ <div class="forge-command-palette__empty">No commands found</div>
231
+ </template>
232
+
233
+ <template v-for="(action, index) in filteredActions" :key="action.id">
234
+ <!-- Group header -->
235
+ <div
236
+ v-if="getGroupHeader(index)"
237
+ class="forge-command-palette__group"
238
+ >
239
+ {{ getGroupHeader(index) }}
240
+ </div>
241
+
242
+ <!-- Action row -->
243
+ <button
244
+ type="button"
245
+ class="forge-command-palette__item"
246
+ :class="{ 'forge-command-palette__item--selected': index === selectedIndex }"
247
+ role="option"
248
+ :aria-selected="index === selectedIndex"
249
+ @click="selectAction(action)"
250
+ @mouseenter="selectedIndex = index"
251
+ >
252
+ <span v-if="action.icon" class="forge-command-palette__icon">
253
+ <Icon :name="action.icon" />
254
+ </span>
255
+ <span class="forge-command-palette__text">
256
+ <span class="forge-command-palette__label">{{ action.label }}</span>
257
+ <span
258
+ v-if="action.description"
259
+ class="forge-command-palette__description"
260
+ >{{ action.description }}</span>
261
+ </span>
262
+ </button>
263
+ </template>
264
+ </div>
265
+ </div>
266
+ </template>
267
+ </UModal>
268
+ </template>
269
+
270
+ <style scoped>
271
+ .forge-command-palette__breadcrumb {
272
+ padding: 10px 12px 0;
273
+ font-size: 0.75rem;
274
+ font-weight: 600;
275
+ color: #64748b;
276
+ }
277
+
278
+ .forge-command-palette {
279
+ display: flex;
280
+ flex-direction: column;
281
+ overflow: hidden;
282
+ border-radius: 8px;
283
+ }
284
+
285
+ .forge-command-palette__search {
286
+ padding: 12px;
287
+ border-bottom: 1px solid #e2e8f0;
288
+ }
289
+
290
+ .forge-command-palette__input {
291
+ width: 100%;
292
+ font-size: 0.95rem;
293
+ }
294
+
295
+ .forge-command-palette__list {
296
+ max-height: 360px;
297
+ overflow-y: auto;
298
+ padding: 6px;
299
+ }
300
+
301
+ .forge-command-palette__empty {
302
+ padding: 16px;
303
+ text-align: center;
304
+ color: #94a3b8;
305
+ font-size: 0.875rem;
306
+ }
307
+
308
+ .forge-command-palette__group {
309
+ padding: 4px 8px 2px;
310
+ font-size: 0.7rem;
311
+ font-weight: 700;
312
+ text-transform: uppercase;
313
+ letter-spacing: 0.08em;
314
+ color: #94a3b8;
315
+ margin-top: 4px;
316
+ }
317
+
318
+ .forge-command-palette__item {
319
+ display: flex;
320
+ align-items: center;
321
+ gap: 10px;
322
+ width: 100%;
323
+ padding: 8px 10px;
324
+ border: none;
325
+ border-radius: 6px;
326
+ background: transparent;
327
+ cursor: pointer;
328
+ text-align: left;
329
+ font: inherit;
330
+ color: inherit;
331
+ transition: background 0.1s ease;
332
+ }
333
+
334
+ .forge-command-palette__item--selected {
335
+ background: #f1f5f9;
336
+ }
337
+
338
+ .forge-command-palette__icon {
339
+ width: 20px;
340
+ flex-shrink: 0;
341
+ color: #64748b;
342
+ }
343
+
344
+ .forge-command-palette__text {
345
+ display: flex;
346
+ flex-direction: column;
347
+ gap: 1px;
348
+ min-width: 0;
349
+ }
350
+
351
+ .forge-command-palette__label {
352
+ font-size: 0.875rem;
353
+ font-weight: 500;
354
+ }
355
+
356
+ .forge-command-palette__description {
357
+ font-size: 0.775rem;
358
+ color: #94a3b8;
359
+ white-space: nowrap;
360
+ overflow: hidden;
361
+ text-overflow: ellipsis;
362
+ }
363
+ </style>