@virtengine/openfleet 0.25.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/.env.example +914 -0
- package/LICENSE +190 -0
- package/README.md +500 -0
- package/agent-endpoint.mjs +918 -0
- package/agent-hook-bridge.mjs +230 -0
- package/agent-hooks.mjs +1188 -0
- package/agent-pool.mjs +2403 -0
- package/agent-prompts.mjs +689 -0
- package/agent-sdk.mjs +141 -0
- package/anomaly-detector.mjs +1195 -0
- package/autofix.mjs +1294 -0
- package/claude-shell.mjs +708 -0
- package/cli.mjs +906 -0
- package/codex-config.mjs +1274 -0
- package/codex-model-profiles.mjs +135 -0
- package/codex-shell.mjs +762 -0
- package/config-doctor.mjs +613 -0
- package/config.mjs +1720 -0
- package/conflict-resolver.mjs +248 -0
- package/container-runner.mjs +450 -0
- package/copilot-shell.mjs +827 -0
- package/daemon-restart-policy.mjs +56 -0
- package/diff-stats.mjs +282 -0
- package/error-detector.mjs +829 -0
- package/fetch-runtime.mjs +34 -0
- package/fleet-coordinator.mjs +838 -0
- package/get-telegram-chat-id.mjs +71 -0
- package/git-safety.mjs +170 -0
- package/github-reconciler.mjs +403 -0
- package/hook-profiles.mjs +651 -0
- package/kanban-adapter.mjs +4491 -0
- package/lib/logger.mjs +645 -0
- package/maintenance.mjs +828 -0
- package/merge-strategy.mjs +1171 -0
- package/monitor.mjs +12207 -0
- package/openfleet.config.example.json +115 -0
- package/openfleet.schema.json +465 -0
- package/package.json +203 -0
- package/postinstall.mjs +187 -0
- package/pr-cleanup-daemon.mjs +978 -0
- package/preflight.mjs +408 -0
- package/prepublish-check.mjs +90 -0
- package/presence.mjs +328 -0
- package/primary-agent.mjs +282 -0
- package/publish.mjs +151 -0
- package/repo-root.mjs +29 -0
- package/restart-controller.mjs +100 -0
- package/review-agent.mjs +557 -0
- package/rotate-agent-logs.sh +133 -0
- package/sdk-conflict-resolver.mjs +973 -0
- package/session-tracker.mjs +880 -0
- package/setup.mjs +3937 -0
- package/shared-knowledge.mjs +410 -0
- package/shared-state-manager.mjs +841 -0
- package/shared-workspace-cli.mjs +199 -0
- package/shared-workspace-registry.mjs +537 -0
- package/shared-workspaces.json +18 -0
- package/startup-service.mjs +1070 -0
- package/sync-engine.mjs +1063 -0
- package/task-archiver.mjs +801 -0
- package/task-assessment.mjs +550 -0
- package/task-claims.mjs +924 -0
- package/task-complexity.mjs +581 -0
- package/task-executor.mjs +5111 -0
- package/task-store.mjs +753 -0
- package/telegram-bot.mjs +9281 -0
- package/telegram-sentinel.mjs +2010 -0
- package/ui/app.js +867 -0
- package/ui/app.legacy.js +1464 -0
- package/ui/app.monolith.js +2488 -0
- package/ui/components/charts.js +226 -0
- package/ui/components/chat-view.js +567 -0
- package/ui/components/command-palette.js +587 -0
- package/ui/components/diff-viewer.js +190 -0
- package/ui/components/forms.js +327 -0
- package/ui/components/kanban-board.js +451 -0
- package/ui/components/session-list.js +305 -0
- package/ui/components/shared.js +473 -0
- package/ui/index.html +70 -0
- package/ui/modules/api.js +297 -0
- package/ui/modules/icons.js +461 -0
- package/ui/modules/router.js +81 -0
- package/ui/modules/settings-schema.js +261 -0
- package/ui/modules/state.js +679 -0
- package/ui/modules/telegram.js +331 -0
- package/ui/modules/utils.js +270 -0
- package/ui/styles/animations.css +140 -0
- package/ui/styles/base.css +98 -0
- package/ui/styles/components.css +1915 -0
- package/ui/styles/kanban.css +286 -0
- package/ui/styles/layout.css +809 -0
- package/ui/styles/sessions.css +827 -0
- package/ui/styles/variables.css +188 -0
- package/ui/styles.css +141 -0
- package/ui/styles.monolith.css +1046 -0
- package/ui/tabs/agents.js +1417 -0
- package/ui/tabs/chat.js +74 -0
- package/ui/tabs/control.js +887 -0
- package/ui/tabs/dashboard.js +515 -0
- package/ui/tabs/infra.js +537 -0
- package/ui/tabs/logs.js +783 -0
- package/ui/tabs/settings.js +1487 -0
- package/ui/tabs/tasks.js +1385 -0
- package/ui-server.mjs +4073 -0
- package/update-check.mjs +465 -0
- package/utils.mjs +172 -0
- package/ve-kanban.mjs +654 -0
- package/ve-kanban.ps1 +1365 -0
- package/ve-kanban.sh +18 -0
- package/ve-orchestrator.mjs +340 -0
- package/ve-orchestrator.ps1 +6546 -0
- package/ve-orchestrator.sh +18 -0
- package/vibe-kanban-wrapper.mjs +41 -0
- package/vk-error-resolver.mjs +470 -0
- package/vk-log-stream.mjs +914 -0
- package/whatsapp-channel.mjs +520 -0
- package/workspace-monitor.mjs +581 -0
- package/workspace-reaper.mjs +405 -0
- package/workspace-registry.mjs +238 -0
- package/worktree-manager.mjs +1266 -0
|
@@ -0,0 +1,587 @@
|
|
|
1
|
+
/* ─────────────────────────────────────────────────────────────
|
|
2
|
+
* VirtEngine Control Center – Command Palette
|
|
3
|
+
* Global fuzzy search palette (Cmd+K / Ctrl+K)
|
|
4
|
+
* ────────────────────────────────────────────────────────────── */
|
|
5
|
+
|
|
6
|
+
import { h } from "preact";
|
|
7
|
+
import {
|
|
8
|
+
useState,
|
|
9
|
+
useEffect,
|
|
10
|
+
useRef,
|
|
11
|
+
useCallback,
|
|
12
|
+
useMemo,
|
|
13
|
+
} from "preact/hooks";
|
|
14
|
+
import { signal } from "@preact/signals";
|
|
15
|
+
import htm from "htm";
|
|
16
|
+
|
|
17
|
+
const html = htm.bind(h);
|
|
18
|
+
|
|
19
|
+
import { ICONS } from "../modules/icons.js";
|
|
20
|
+
import { navigateTo, TAB_CONFIG } from "../modules/router.js";
|
|
21
|
+
import { sendCommandToChat } from "../modules/api.js";
|
|
22
|
+
import { executorData, refreshTab } from "../modules/state.js";
|
|
23
|
+
import { activeTab } from "../modules/router.js";
|
|
24
|
+
|
|
25
|
+
/* ═══════════════════════════════════════════════
|
|
26
|
+
* Palette Items
|
|
27
|
+
* ═══════════════════════════════════════════════ */
|
|
28
|
+
|
|
29
|
+
const TAB_DESCRIPTIONS = {
|
|
30
|
+
dashboard: "Overview, status, and metrics",
|
|
31
|
+
tasks: "View and manage tasks",
|
|
32
|
+
agents: "Monitor running agents",
|
|
33
|
+
logs: "Application and agent logs",
|
|
34
|
+
control: "Executor and system controls",
|
|
35
|
+
settings: "Preferences and configuration",
|
|
36
|
+
infra: "Infrastructure and worktrees",
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
function buildNavigationItems() {
|
|
40
|
+
return TAB_CONFIG.map((tab, i) => ({
|
|
41
|
+
id: `nav-${tab.id}`,
|
|
42
|
+
category: "Navigation",
|
|
43
|
+
title: tab.label,
|
|
44
|
+
description: TAB_DESCRIPTIONS[tab.id] || "",
|
|
45
|
+
hint: String(i + 1),
|
|
46
|
+
icon: tab.icon,
|
|
47
|
+
action: () => navigateTo(tab.id),
|
|
48
|
+
}));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const COMMAND_ITEMS = [
|
|
52
|
+
{
|
|
53
|
+
id: "cmd-status",
|
|
54
|
+
category: "Commands",
|
|
55
|
+
title: "/status",
|
|
56
|
+
description: "Check orchestrator status",
|
|
57
|
+
action: () => sendCommandToChat("/status"),
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
id: "cmd-health",
|
|
61
|
+
category: "Commands",
|
|
62
|
+
title: "/health",
|
|
63
|
+
description: "Health check",
|
|
64
|
+
action: () => sendCommandToChat("/health"),
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
id: "cmd-plan",
|
|
68
|
+
category: "Commands",
|
|
69
|
+
title: "/plan",
|
|
70
|
+
description: "Generate plan",
|
|
71
|
+
action: () => sendCommandToChat("/plan"),
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
id: "cmd-logs",
|
|
75
|
+
category: "Commands",
|
|
76
|
+
title: "/logs 50",
|
|
77
|
+
description: "View recent logs",
|
|
78
|
+
action: () => sendCommandToChat("/logs 50"),
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
id: "cmd-menu",
|
|
82
|
+
category: "Commands",
|
|
83
|
+
title: "/menu",
|
|
84
|
+
description: "Show menu",
|
|
85
|
+
action: () => sendCommandToChat("/menu"),
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
id: "cmd-helpfull",
|
|
89
|
+
category: "Commands",
|
|
90
|
+
title: "/helpfull",
|
|
91
|
+
description: "Full help",
|
|
92
|
+
action: () => sendCommandToChat("/helpfull"),
|
|
93
|
+
},
|
|
94
|
+
];
|
|
95
|
+
|
|
96
|
+
function buildQuickActions() {
|
|
97
|
+
const paused = executorData.value?.paused;
|
|
98
|
+
return [
|
|
99
|
+
{
|
|
100
|
+
id: "qa-new-task",
|
|
101
|
+
category: "Quick Actions",
|
|
102
|
+
title: "Create Task",
|
|
103
|
+
description: "Open task creation",
|
|
104
|
+
action: () => {
|
|
105
|
+
navigateTo("tasks");
|
|
106
|
+
// Dispatch event for task creation UI
|
|
107
|
+
globalThis.dispatchEvent(new CustomEvent("ve:create-task"));
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
id: "qa-toggle-executor",
|
|
112
|
+
category: "Quick Actions",
|
|
113
|
+
title: paused ? "Resume Executor" : "Pause Executor",
|
|
114
|
+
description: paused
|
|
115
|
+
? "Resume task processing"
|
|
116
|
+
: "Stop processing new tasks",
|
|
117
|
+
action: () => {
|
|
118
|
+
const cmd = paused ? "/resume" : "/pause";
|
|
119
|
+
sendCommandToChat(cmd);
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
id: "qa-refresh",
|
|
124
|
+
category: "Quick Actions",
|
|
125
|
+
title: "Refresh Data",
|
|
126
|
+
description: "Force refresh current tab",
|
|
127
|
+
action: () => refreshTab(activeTab.value),
|
|
128
|
+
},
|
|
129
|
+
];
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/* ═══════════════════════════════════════════════
|
|
133
|
+
* Fuzzy Matching
|
|
134
|
+
* ═══════════════════════════════════════════════ */
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Score a query against text. Higher = better match.
|
|
138
|
+
* Returns { score, indices } or null if no match.
|
|
139
|
+
*/
|
|
140
|
+
function fuzzyMatch(query, text) {
|
|
141
|
+
if (!query) return { score: 0, indices: [] };
|
|
142
|
+
|
|
143
|
+
const q = query.toLowerCase();
|
|
144
|
+
const t = text.toLowerCase();
|
|
145
|
+
|
|
146
|
+
// Exact prefix match — highest score
|
|
147
|
+
if (t.startsWith(q)) {
|
|
148
|
+
const indices = [];
|
|
149
|
+
for (let i = 0; i < q.length; i++) indices.push(i);
|
|
150
|
+
return { score: 100, indices };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Word-start match
|
|
154
|
+
const words = t.split(/[\s/\-_]+/);
|
|
155
|
+
let wordStartIndices = [];
|
|
156
|
+
let wordPos = 0;
|
|
157
|
+
let qi = 0;
|
|
158
|
+
for (const word of words) {
|
|
159
|
+
const startIdx = t.indexOf(word, wordPos);
|
|
160
|
+
if (qi < q.length && word.startsWith(q[qi])) {
|
|
161
|
+
// Match characters from the start of each word
|
|
162
|
+
let wi = 0;
|
|
163
|
+
while (qi < q.length && wi < word.length && word[wi] === q[qi]) {
|
|
164
|
+
wordStartIndices.push(startIdx + wi);
|
|
165
|
+
qi++;
|
|
166
|
+
wi++;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
wordPos = startIdx + word.length;
|
|
170
|
+
}
|
|
171
|
+
if (qi === q.length) {
|
|
172
|
+
return { score: 75, indices: wordStartIndices };
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Substring match
|
|
176
|
+
const subIdx = t.indexOf(q);
|
|
177
|
+
if (subIdx !== -1) {
|
|
178
|
+
const indices = [];
|
|
179
|
+
for (let i = 0; i < q.length; i++) indices.push(subIdx + i);
|
|
180
|
+
return { score: 50, indices };
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Fuzzy character-by-character match
|
|
184
|
+
const indices = [];
|
|
185
|
+
let ti = 0;
|
|
186
|
+
for (let i = 0; i < q.length; i++) {
|
|
187
|
+
const found = t.indexOf(q[i], ti);
|
|
188
|
+
if (found === -1) return null;
|
|
189
|
+
indices.push(found);
|
|
190
|
+
ti = found + 1;
|
|
191
|
+
}
|
|
192
|
+
return { score: 25, indices };
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Highlight matched characters in text by wrapping them in <mark>.
|
|
197
|
+
*/
|
|
198
|
+
function HighlightedText({ text, indices }) {
|
|
199
|
+
if (!indices || indices.length === 0) return html`<span>${text}</span>`;
|
|
200
|
+
|
|
201
|
+
const set = new Set(indices);
|
|
202
|
+
const parts = [];
|
|
203
|
+
let buf = "";
|
|
204
|
+
let inMatch = false;
|
|
205
|
+
|
|
206
|
+
for (let i = 0; i < text.length; i++) {
|
|
207
|
+
const match = set.has(i);
|
|
208
|
+
if (match !== inMatch) {
|
|
209
|
+
if (buf) {
|
|
210
|
+
parts.push(
|
|
211
|
+
inMatch
|
|
212
|
+
? html`<mark class="cp-highlight">${buf}</mark>`
|
|
213
|
+
: html`<span>${buf}</span>`,
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
buf = "";
|
|
217
|
+
inMatch = match;
|
|
218
|
+
}
|
|
219
|
+
buf += text[i];
|
|
220
|
+
}
|
|
221
|
+
if (buf) {
|
|
222
|
+
parts.push(
|
|
223
|
+
inMatch
|
|
224
|
+
? html`<mark class="cp-highlight">${buf}</mark>`
|
|
225
|
+
: html`<span>${buf}</span>`,
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
return html`<span>${parts}</span>`;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/* ═══════════════════════════════════════════════
|
|
232
|
+
* Styles
|
|
233
|
+
* ═══════════════════════════════════════════════ */
|
|
234
|
+
|
|
235
|
+
const PALETTE_STYLES = `
|
|
236
|
+
.cp-overlay {
|
|
237
|
+
position: fixed;
|
|
238
|
+
inset: 0;
|
|
239
|
+
z-index: 9999;
|
|
240
|
+
display: flex;
|
|
241
|
+
align-items: flex-start;
|
|
242
|
+
justify-content: center;
|
|
243
|
+
padding-top: min(20vh, 120px);
|
|
244
|
+
background: rgba(0,0,0,0.5);
|
|
245
|
+
backdrop-filter: blur(12px);
|
|
246
|
+
-webkit-backdrop-filter: blur(12px);
|
|
247
|
+
animation: cpFadeIn 0.15s ease-out;
|
|
248
|
+
}
|
|
249
|
+
@keyframes cpFadeIn {
|
|
250
|
+
from { opacity: 0; }
|
|
251
|
+
to { opacity: 1; }
|
|
252
|
+
}
|
|
253
|
+
.cp-container {
|
|
254
|
+
width: min(560px, 92vw);
|
|
255
|
+
max-height: 70vh;
|
|
256
|
+
background: var(--card-bg, rgba(30,30,46,0.95));
|
|
257
|
+
border: 1px solid var(--border, rgba(255,255,255,0.1));
|
|
258
|
+
border-radius: 16px;
|
|
259
|
+
box-shadow: 0 24px 64px rgba(0,0,0,0.4);
|
|
260
|
+
display: flex;
|
|
261
|
+
flex-direction: column;
|
|
262
|
+
overflow: hidden;
|
|
263
|
+
animation: cpSlideIn 0.15s ease-out;
|
|
264
|
+
}
|
|
265
|
+
@keyframes cpSlideIn {
|
|
266
|
+
from { opacity: 0; transform: translateY(-12px) scale(0.98); }
|
|
267
|
+
to { opacity: 1; transform: translateY(0) scale(1); }
|
|
268
|
+
}
|
|
269
|
+
.cp-search-row {
|
|
270
|
+
display: flex;
|
|
271
|
+
align-items: center;
|
|
272
|
+
gap: 10px;
|
|
273
|
+
padding: 16px 20px;
|
|
274
|
+
border-bottom: 1px solid var(--border, rgba(255,255,255,0.08));
|
|
275
|
+
}
|
|
276
|
+
.cp-search-icon {
|
|
277
|
+
width: 20px;
|
|
278
|
+
height: 20px;
|
|
279
|
+
opacity: 0.5;
|
|
280
|
+
flex-shrink: 0;
|
|
281
|
+
color: var(--text-secondary, #999);
|
|
282
|
+
}
|
|
283
|
+
.cp-search-input {
|
|
284
|
+
flex: 1;
|
|
285
|
+
background: none;
|
|
286
|
+
border: none;
|
|
287
|
+
outline: none;
|
|
288
|
+
font-size: 17px;
|
|
289
|
+
color: var(--text, #e0e0e0);
|
|
290
|
+
font-family: inherit;
|
|
291
|
+
}
|
|
292
|
+
.cp-search-input::placeholder {
|
|
293
|
+
color: var(--text-secondary, #666);
|
|
294
|
+
}
|
|
295
|
+
.cp-kbd {
|
|
296
|
+
font-size: 11px;
|
|
297
|
+
padding: 2px 6px;
|
|
298
|
+
border-radius: 4px;
|
|
299
|
+
background: var(--bg-secondary, rgba(255,255,255,0.06));
|
|
300
|
+
color: var(--text-secondary, #888);
|
|
301
|
+
font-family: monospace;
|
|
302
|
+
flex-shrink: 0;
|
|
303
|
+
}
|
|
304
|
+
.cp-results {
|
|
305
|
+
overflow-y: auto;
|
|
306
|
+
padding: 8px;
|
|
307
|
+
flex: 1;
|
|
308
|
+
}
|
|
309
|
+
.cp-group-label {
|
|
310
|
+
font-size: 11px;
|
|
311
|
+
font-weight: 600;
|
|
312
|
+
text-transform: uppercase;
|
|
313
|
+
letter-spacing: 0.05em;
|
|
314
|
+
color: var(--text-secondary, #888);
|
|
315
|
+
padding: 8px 12px 4px;
|
|
316
|
+
}
|
|
317
|
+
.cp-item {
|
|
318
|
+
display: flex;
|
|
319
|
+
align-items: center;
|
|
320
|
+
gap: 12px;
|
|
321
|
+
padding: 10px 12px;
|
|
322
|
+
border-radius: 10px;
|
|
323
|
+
cursor: pointer;
|
|
324
|
+
transition: background 0.1s;
|
|
325
|
+
}
|
|
326
|
+
.cp-item:hover,
|
|
327
|
+
.cp-item.selected {
|
|
328
|
+
background: var(--bg-hover, rgba(255,255,255,0.08));
|
|
329
|
+
}
|
|
330
|
+
.cp-item-icon {
|
|
331
|
+
width: 18px;
|
|
332
|
+
height: 18px;
|
|
333
|
+
flex-shrink: 0;
|
|
334
|
+
opacity: 0.7;
|
|
335
|
+
color: var(--text-secondary, #aaa);
|
|
336
|
+
}
|
|
337
|
+
.cp-item-text {
|
|
338
|
+
flex: 1;
|
|
339
|
+
min-width: 0;
|
|
340
|
+
}
|
|
341
|
+
.cp-item-title {
|
|
342
|
+
font-size: 14px;
|
|
343
|
+
font-weight: 500;
|
|
344
|
+
color: var(--text, #e0e0e0);
|
|
345
|
+
}
|
|
346
|
+
.cp-item-desc {
|
|
347
|
+
font-size: 12px;
|
|
348
|
+
color: var(--text-secondary, #888);
|
|
349
|
+
white-space: nowrap;
|
|
350
|
+
overflow: hidden;
|
|
351
|
+
text-overflow: ellipsis;
|
|
352
|
+
}
|
|
353
|
+
.cp-item-hint {
|
|
354
|
+
font-size: 11px;
|
|
355
|
+
padding: 2px 6px;
|
|
356
|
+
border-radius: 4px;
|
|
357
|
+
background: var(--bg-secondary, rgba(255,255,255,0.06));
|
|
358
|
+
color: var(--text-secondary, #888);
|
|
359
|
+
font-family: monospace;
|
|
360
|
+
flex-shrink: 0;
|
|
361
|
+
}
|
|
362
|
+
.cp-highlight {
|
|
363
|
+
background: rgba(99,102,241,0.35);
|
|
364
|
+
color: inherit;
|
|
365
|
+
border-radius: 2px;
|
|
366
|
+
}
|
|
367
|
+
.cp-empty {
|
|
368
|
+
text-align: center;
|
|
369
|
+
padding: 32px 16px;
|
|
370
|
+
color: var(--text-secondary, #888);
|
|
371
|
+
font-size: 14px;
|
|
372
|
+
}
|
|
373
|
+
`;
|
|
374
|
+
|
|
375
|
+
/* ═══════════════════════════════════════════════
|
|
376
|
+
* CommandPalette Component
|
|
377
|
+
* ═══════════════════════════════════════════════ */
|
|
378
|
+
|
|
379
|
+
export function CommandPalette({ open, onClose }) {
|
|
380
|
+
const [query, setQuery] = useState("");
|
|
381
|
+
const [selectedIdx, setSelectedIdx] = useState(0);
|
|
382
|
+
const inputRef = useRef(null);
|
|
383
|
+
const listRef = useRef(null);
|
|
384
|
+
|
|
385
|
+
// Reset state when opening
|
|
386
|
+
useEffect(() => {
|
|
387
|
+
if (open) {
|
|
388
|
+
setQuery("");
|
|
389
|
+
setSelectedIdx(0);
|
|
390
|
+
// Auto-focus with a small delay for the render
|
|
391
|
+
requestAnimationFrame(() => inputRef.current?.focus());
|
|
392
|
+
}
|
|
393
|
+
}, [open]);
|
|
394
|
+
|
|
395
|
+
// Build all items (nav items are static, quick actions are dynamic)
|
|
396
|
+
const allItems = useMemo(() => {
|
|
397
|
+
return [
|
|
398
|
+
...buildNavigationItems(),
|
|
399
|
+
...COMMAND_ITEMS,
|
|
400
|
+
...buildQuickActions(),
|
|
401
|
+
];
|
|
402
|
+
}, [open, executorData.value]);
|
|
403
|
+
|
|
404
|
+
// Filter and score
|
|
405
|
+
const filtered = useMemo(() => {
|
|
406
|
+
if (!query.trim()) return allItems;
|
|
407
|
+
|
|
408
|
+
const results = [];
|
|
409
|
+
const q = query.trim();
|
|
410
|
+
for (const item of allItems) {
|
|
411
|
+
const titleMatch = fuzzyMatch(q, item.title);
|
|
412
|
+
const descMatch = fuzzyMatch(q, item.description);
|
|
413
|
+
const bestScore = Math.max(
|
|
414
|
+
titleMatch?.score ?? 0,
|
|
415
|
+
(descMatch?.score ?? 0) * 0.8,
|
|
416
|
+
);
|
|
417
|
+
if (titleMatch || descMatch) {
|
|
418
|
+
results.push({
|
|
419
|
+
...item,
|
|
420
|
+
_score: bestScore,
|
|
421
|
+
_titleIndices: titleMatch?.indices || [],
|
|
422
|
+
_descIndices: descMatch?.indices || [],
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
results.sort((a, b) => b._score - a._score);
|
|
427
|
+
return results;
|
|
428
|
+
}, [query, allItems]);
|
|
429
|
+
|
|
430
|
+
// Group results by category
|
|
431
|
+
const grouped = useMemo(() => {
|
|
432
|
+
const groups = new Map();
|
|
433
|
+
for (const item of filtered) {
|
|
434
|
+
if (!groups.has(item.category)) groups.set(item.category, []);
|
|
435
|
+
groups.get(item.category).push(item);
|
|
436
|
+
}
|
|
437
|
+
return groups;
|
|
438
|
+
}, [filtered]);
|
|
439
|
+
|
|
440
|
+
// Flat list for keyboard navigation
|
|
441
|
+
const flatList = useMemo(() => {
|
|
442
|
+
const flat = [];
|
|
443
|
+
for (const items of grouped.values()) flat.push(...items);
|
|
444
|
+
return flat;
|
|
445
|
+
}, [grouped]);
|
|
446
|
+
|
|
447
|
+
// Clamp selection
|
|
448
|
+
useEffect(() => {
|
|
449
|
+
if (selectedIdx >= flatList.length) {
|
|
450
|
+
setSelectedIdx(Math.max(0, flatList.length - 1));
|
|
451
|
+
}
|
|
452
|
+
}, [flatList.length]);
|
|
453
|
+
|
|
454
|
+
// Scroll selected item into view
|
|
455
|
+
useEffect(() => {
|
|
456
|
+
if (!listRef.current) return;
|
|
457
|
+
const el = listRef.current.querySelector(".cp-item.selected");
|
|
458
|
+
if (el) el.scrollIntoView({ block: "nearest" });
|
|
459
|
+
}, [selectedIdx]);
|
|
460
|
+
|
|
461
|
+
const execute = useCallback(
|
|
462
|
+
(item) => {
|
|
463
|
+
if (!item) return;
|
|
464
|
+
onClose();
|
|
465
|
+
// Defer action slightly so the palette closes first
|
|
466
|
+
requestAnimationFrame(() => item.action());
|
|
467
|
+
},
|
|
468
|
+
[onClose],
|
|
469
|
+
);
|
|
470
|
+
|
|
471
|
+
const handleKeyDown = useCallback(
|
|
472
|
+
(e) => {
|
|
473
|
+
if (e.key === "ArrowDown") {
|
|
474
|
+
e.preventDefault();
|
|
475
|
+
setSelectedIdx((i) => Math.min(i + 1, flatList.length - 1));
|
|
476
|
+
} else if (e.key === "ArrowUp") {
|
|
477
|
+
e.preventDefault();
|
|
478
|
+
setSelectedIdx((i) => Math.max(i - 1, 0));
|
|
479
|
+
} else if (e.key === "Enter") {
|
|
480
|
+
e.preventDefault();
|
|
481
|
+
execute(flatList[selectedIdx]);
|
|
482
|
+
} else if (e.key === "Escape") {
|
|
483
|
+
e.preventDefault();
|
|
484
|
+
onClose();
|
|
485
|
+
}
|
|
486
|
+
},
|
|
487
|
+
[flatList, selectedIdx, execute, onClose],
|
|
488
|
+
);
|
|
489
|
+
|
|
490
|
+
if (!open) return null;
|
|
491
|
+
|
|
492
|
+
let itemIndex = 0;
|
|
493
|
+
|
|
494
|
+
return html`
|
|
495
|
+
<style>
|
|
496
|
+
${PALETTE_STYLES}
|
|
497
|
+
</style>
|
|
498
|
+
<div class="cp-overlay" onClick=${(e) => e.target === e.currentTarget && onClose()}>
|
|
499
|
+
<div class="cp-container" onKeyDown=${handleKeyDown}>
|
|
500
|
+
<div class="cp-search-row">
|
|
501
|
+
<svg class="cp-search-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
|
|
502
|
+
<input
|
|
503
|
+
ref=${inputRef}
|
|
504
|
+
class="cp-search-input"
|
|
505
|
+
type="text"
|
|
506
|
+
placeholder="Search commands, tabs, actions…"
|
|
507
|
+
value=${query}
|
|
508
|
+
onInput=${(e) => {
|
|
509
|
+
setQuery(e.target.value);
|
|
510
|
+
setSelectedIdx(0);
|
|
511
|
+
}}
|
|
512
|
+
/>
|
|
513
|
+
<span class="cp-kbd">esc</span>
|
|
514
|
+
</div>
|
|
515
|
+
<div class="cp-results" ref=${listRef}>
|
|
516
|
+
${flatList.length === 0
|
|
517
|
+
? html`<div class="cp-empty">No results for "${query}"</div>`
|
|
518
|
+
: Array.from(grouped.entries()).map(
|
|
519
|
+
([category, items]) => html`
|
|
520
|
+
<div key=${category}>
|
|
521
|
+
<div class="cp-group-label">${category}</div>
|
|
522
|
+
${items.map((item) => {
|
|
523
|
+
const idx = itemIndex++;
|
|
524
|
+
return html`
|
|
525
|
+
<div
|
|
526
|
+
key=${item.id}
|
|
527
|
+
class="cp-item ${idx === selectedIdx ? "selected" : ""}"
|
|
528
|
+
onClick=${() => execute(item)}
|
|
529
|
+
onMouseEnter=${() => setSelectedIdx(idx)}
|
|
530
|
+
>
|
|
531
|
+
${item.icon
|
|
532
|
+
? html`<div class="cp-item-icon">${ICONS[item.icon]}</div>`
|
|
533
|
+
: null}
|
|
534
|
+
<div class="cp-item-text">
|
|
535
|
+
<div class="cp-item-title">
|
|
536
|
+
<${HighlightedText}
|
|
537
|
+
text=${item.title}
|
|
538
|
+
indices=${item._titleIndices || []}
|
|
539
|
+
/>
|
|
540
|
+
</div>
|
|
541
|
+
${item.description
|
|
542
|
+
? html`<div class="cp-item-desc">
|
|
543
|
+
<${HighlightedText}
|
|
544
|
+
text=${item.description}
|
|
545
|
+
indices=${item._descIndices || []}
|
|
546
|
+
/>
|
|
547
|
+
</div>`
|
|
548
|
+
: null}
|
|
549
|
+
</div>
|
|
550
|
+
${item.hint
|
|
551
|
+
? html`<span class="cp-item-hint">${item.hint}</span>`
|
|
552
|
+
: null}
|
|
553
|
+
</div>
|
|
554
|
+
`;
|
|
555
|
+
})}
|
|
556
|
+
</div>
|
|
557
|
+
`,
|
|
558
|
+
)}
|
|
559
|
+
</div>
|
|
560
|
+
</div>
|
|
561
|
+
</div>
|
|
562
|
+
`;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
/* ═══════════════════════════════════════════════
|
|
566
|
+
* useCommandPalette hook
|
|
567
|
+
* Manages open state and global Cmd+K listener
|
|
568
|
+
* ═══════════════════════════════════════════════ */
|
|
569
|
+
|
|
570
|
+
export function useCommandPalette() {
|
|
571
|
+
const [open, setOpen] = useState(false);
|
|
572
|
+
|
|
573
|
+
useEffect(() => {
|
|
574
|
+
function handleKeyDown(e) {
|
|
575
|
+
if ((e.metaKey || e.ctrlKey) && e.key === "k") {
|
|
576
|
+
e.preventDefault();
|
|
577
|
+
setOpen((v) => !v);
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
document.addEventListener("keydown", handleKeyDown);
|
|
581
|
+
return () => document.removeEventListener("keydown", handleKeyDown);
|
|
582
|
+
}, []);
|
|
583
|
+
|
|
584
|
+
const onClose = useCallback(() => setOpen(false), []);
|
|
585
|
+
|
|
586
|
+
return { open, onClose, setOpen };
|
|
587
|
+
}
|