clideck 1.22.2
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/LICENSE +21 -0
- package/README.md +77 -0
- package/activity.js +56 -0
- package/agent-presets.json +93 -0
- package/assets/clideck-themes.jpg +0 -0
- package/bin/clideck.js +2 -0
- package/config.js +96 -0
- package/handlers.js +297 -0
- package/opencode-bridge.js +148 -0
- package/opencode-plugin/clideck-bridge.js +24 -0
- package/package.json +47 -0
- package/paths.js +41 -0
- package/plugin-loader.js +285 -0
- package/plugins/trim-clip/clideck-plugin.json +13 -0
- package/plugins/trim-clip/client.js +31 -0
- package/plugins/trim-clip/index.js +10 -0
- package/plugins/voice-input/clideck-plugin.json +49 -0
- package/plugins/voice-input/client.js +196 -0
- package/plugins/voice-input/index.js +342 -0
- package/plugins/voice-input/python/mel_filters.npz +0 -0
- package/plugins/voice-input/python/whisper_turbo.py +416 -0
- package/plugins/voice-input/python/worker.py +135 -0
- package/public/fx/bold-beep-idle.mp3 +0 -0
- package/public/fx/default-beep.mp3 +0 -0
- package/public/fx/echo-beep-idle.mp3 +0 -0
- package/public/fx/musical-beep-idle.mp3 +0 -0
- package/public/fx/small-bleep-idle.mp3 +0 -0
- package/public/fx/soft-beep.mp3 +0 -0
- package/public/fx/space-idle.mp3 +0 -0
- package/public/img/claude-code.png +0 -0
- package/public/img/clideck-logo-icon.png +0 -0
- package/public/img/clideck-logo-terminal-panel.png +0 -0
- package/public/img/codex.png +0 -0
- package/public/img/gemini.png +0 -0
- package/public/img/opencode.png +0 -0
- package/public/index.html +243 -0
- package/public/js/app.js +794 -0
- package/public/js/color-mode.js +51 -0
- package/public/js/confirm.js +27 -0
- package/public/js/creator.js +201 -0
- package/public/js/drag.js +134 -0
- package/public/js/folder-picker.js +81 -0
- package/public/js/hotkeys.js +90 -0
- package/public/js/nav.js +56 -0
- package/public/js/profiles.js +22 -0
- package/public/js/prompts.js +325 -0
- package/public/js/settings.js +489 -0
- package/public/js/state.js +15 -0
- package/public/js/terminals.js +905 -0
- package/public/js/toast.js +62 -0
- package/public/js/utils.js +27 -0
- package/public/tailwind.css +1 -0
- package/server.js +126 -0
- package/sessions.js +375 -0
- package/telemetry-receiver.js +129 -0
- package/themes.js +247 -0
- package/transcript.js +90 -0
- package/utils.js +66 -0
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
// Prompt Library — manage saved prompts and // trigger autocomplete
|
|
2
|
+
import { state, send } from './state.js';
|
|
3
|
+
import { esc } from './utils.js';
|
|
4
|
+
|
|
5
|
+
// --- Panel rendering ---
|
|
6
|
+
|
|
7
|
+
const panel = document.getElementById('panel-prompts');
|
|
8
|
+
|
|
9
|
+
function getPrompts() { return state.cfg.prompts || []; }
|
|
10
|
+
|
|
11
|
+
function save() {
|
|
12
|
+
send({ type: 'config.update', config: state.cfg });
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function renderPrompts() {
|
|
16
|
+
const prompts = getPrompts();
|
|
17
|
+
panel.innerHTML = `
|
|
18
|
+
<div class="flex items-center justify-between px-3 pt-3 pb-2">
|
|
19
|
+
<span class="text-sm font-bold text-slate-200 tracking-tight" style="font-family:'JetBrains Mono',monospace">Prompts</span>
|
|
20
|
+
<button id="btn-add-prompt" class="icon-btn w-7 h-7 flex items-center justify-center rounded-md border border-slate-600 text-slate-400 hover:bg-slate-700 hover:text-slate-200 transition-colors text-sm" title="New prompt">+</button>
|
|
21
|
+
</div>
|
|
22
|
+
<div class="px-3 pb-2.5">
|
|
23
|
+
<div class="flex items-center gap-1.5 px-2.5 py-1.5 rounded-lg bg-slate-800/40 text-slate-600 text-[11px]">
|
|
24
|
+
<kbd class="px-1.5 py-0.5 rounded bg-slate-700/60 text-slate-400 font-mono text-[10px]">//</kbd>
|
|
25
|
+
<span>Type in terminal to search & paste</span>
|
|
26
|
+
</div>
|
|
27
|
+
</div>
|
|
28
|
+
<div id="prompts-list" class="tmx-scroll flex-1 overflow-y-auto border-t border-slate-700/50"></div>`;
|
|
29
|
+
|
|
30
|
+
const list = panel.querySelector('#prompts-list');
|
|
31
|
+
if (!prompts.length) {
|
|
32
|
+
list.innerHTML = `<div class="flex flex-col items-center justify-center h-full px-6 text-center">
|
|
33
|
+
<p class="text-sm text-slate-400 mb-1">No prompts saved</p>
|
|
34
|
+
<p class="text-xs text-slate-600 leading-relaxed">Add prompts and paste them into any terminal<br>by typing <kbd class="px-1 py-0.5 rounded bg-slate-800 text-slate-400 text-[11px] font-mono">//</kbd> followed by a few letters.</p>
|
|
35
|
+
</div>`;
|
|
36
|
+
} else {
|
|
37
|
+
list.innerHTML = prompts.map((p, i) => `
|
|
38
|
+
<div class="prompt-row group flex items-start gap-2 px-3 py-2.5 cursor-pointer hover:bg-slate-800/40 transition-colors ${i > 0 ? 'border-t border-slate-700/30' : ''}" data-idx="${i}">
|
|
39
|
+
<div class="flex-1 min-w-0">
|
|
40
|
+
<div class="text-[13px] font-medium text-slate-200 truncate">${esc(p.name)}</div>
|
|
41
|
+
<div class="text-[11px] text-slate-500 mt-0.5 line-clamp-2 leading-relaxed">${esc(p.text)}</div>
|
|
42
|
+
</div>
|
|
43
|
+
<div class="flex items-center gap-0.5 opacity-0 group-hover:opacity-100 transition-opacity flex-shrink-0 mt-0.5">
|
|
44
|
+
<button class="prompt-edit w-6 h-6 flex items-center justify-center rounded text-slate-500 hover:text-slate-300 hover:bg-slate-700/60 transition-colors" title="Edit">
|
|
45
|
+
<svg class="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5"><path d="M17 3a2.85 2.85 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z"/></svg>
|
|
46
|
+
</button>
|
|
47
|
+
<button class="prompt-del w-6 h-6 flex items-center justify-center rounded text-slate-500 hover:text-red-400 hover:bg-slate-700/60 transition-colors" title="Delete">
|
|
48
|
+
<svg class="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5"><path d="M18 6L6 18M6 6l12 12"/></svg>
|
|
49
|
+
</button>
|
|
50
|
+
</div>
|
|
51
|
+
</div>`).join('');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Events
|
|
55
|
+
panel.querySelector('#btn-add-prompt').addEventListener('click', () => openEditor());
|
|
56
|
+
|
|
57
|
+
list.addEventListener('click', (e) => {
|
|
58
|
+
if (e.target.closest('.prompt-edit')) {
|
|
59
|
+
const idx = +e.target.closest('.prompt-row').dataset.idx;
|
|
60
|
+
openEditor(idx);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
if (e.target.closest('.prompt-del')) {
|
|
64
|
+
const idx = +e.target.closest('.prompt-row').dataset.idx;
|
|
65
|
+
state.cfg.prompts.splice(idx, 1);
|
|
66
|
+
save();
|
|
67
|
+
renderPrompts();
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
const row = e.target.closest('.prompt-row');
|
|
71
|
+
if (row) {
|
|
72
|
+
const idx = +row.dataset.idx;
|
|
73
|
+
pastePrompt(prompts[idx].text);
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function closeEditor() {
|
|
79
|
+
document.getElementById('prompt-editor')?.remove();
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function openEditor(idx) {
|
|
83
|
+
// Toggle off if already open
|
|
84
|
+
if (document.getElementById('prompt-editor')) { closeEditor(); if (idx == null) return; }
|
|
85
|
+
const existing = idx != null ? getPrompts()[idx] : null;
|
|
86
|
+
|
|
87
|
+
const card = document.createElement('div');
|
|
88
|
+
card.id = 'prompt-editor';
|
|
89
|
+
card.className = 'p-3 border-b border-slate-700/50 bg-slate-800/30';
|
|
90
|
+
card.innerHTML = `
|
|
91
|
+
<input id="pe-name" type="text" maxlength="60" placeholder="Prompt name" value="${esc(existing?.name || '')}"
|
|
92
|
+
class="w-full px-3 py-2 text-sm bg-slate-900 border border-slate-700 rounded-md text-slate-200 placeholder-slate-500 outline-none focus:border-blue-500 transition-colors mb-2">
|
|
93
|
+
<textarea id="pe-text" rows="4" placeholder="Prompt text to paste into terminal"
|
|
94
|
+
class="w-full px-3 py-1.5 text-xs bg-slate-900 border border-slate-700 rounded-md text-slate-200 placeholder-slate-600 outline-none focus:border-blue-500 transition-colors resize-none leading-relaxed font-mono mb-2">${esc(existing?.text || '')}</textarea>
|
|
95
|
+
<div class="flex items-center gap-2">
|
|
96
|
+
<button id="pe-save" class="px-4 py-1.5 text-xs font-medium bg-blue-600 hover:bg-blue-500 text-white rounded-md transition-colors">${existing ? 'Save' : 'Add'}</button>
|
|
97
|
+
<button id="pe-cancel" class="px-3 py-1.5 text-xs text-slate-500 hover:text-slate-300 transition-colors">Cancel</button>
|
|
98
|
+
</div>`;
|
|
99
|
+
|
|
100
|
+
const list = panel.querySelector('#prompts-list');
|
|
101
|
+
list.parentElement.insertBefore(card, list);
|
|
102
|
+
|
|
103
|
+
const nameInput = card.querySelector('#pe-name');
|
|
104
|
+
const textInput = card.querySelector('#pe-text');
|
|
105
|
+
nameInput.focus();
|
|
106
|
+
|
|
107
|
+
const doSave = () => {
|
|
108
|
+
const name = nameInput.value.trim();
|
|
109
|
+
const text = textInput.value.trim();
|
|
110
|
+
if (!name || !text) return;
|
|
111
|
+
if (!state.cfg.prompts) state.cfg.prompts = [];
|
|
112
|
+
if (existing) {
|
|
113
|
+
state.cfg.prompts[idx] = { ...existing, name, text };
|
|
114
|
+
} else {
|
|
115
|
+
state.cfg.prompts.push({ id: crypto.randomUUID(), name, text });
|
|
116
|
+
}
|
|
117
|
+
save();
|
|
118
|
+
closeEditor();
|
|
119
|
+
renderPrompts();
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
card.querySelector('#pe-save').addEventListener('click', doSave);
|
|
123
|
+
card.querySelector('#pe-cancel').addEventListener('click', closeEditor);
|
|
124
|
+
nameInput.addEventListener('keydown', (e) => {
|
|
125
|
+
if (e.key === 'Escape') closeEditor();
|
|
126
|
+
});
|
|
127
|
+
textInput.addEventListener('keydown', (e) => {
|
|
128
|
+
if (e.key === 'Escape') closeEditor();
|
|
129
|
+
if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) doSave();
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// --- Paste prompt into active terminal ---
|
|
134
|
+
|
|
135
|
+
function pastePrompt(text) {
|
|
136
|
+
if (!state.active) return;
|
|
137
|
+
send({ type: 'input', id: state.active, data: text });
|
|
138
|
+
// Refocus the terminal after pasting
|
|
139
|
+
const entry = state.terms.get(state.active);
|
|
140
|
+
if (entry) entry.term.focus();
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// --- // Autocomplete trigger ---
|
|
144
|
+
|
|
145
|
+
let buffer = ''; // chars typed after //
|
|
146
|
+
let active = false; // autocomplete is open
|
|
147
|
+
let dropdown = null;
|
|
148
|
+
let selectedIdx = 0;
|
|
149
|
+
|
|
150
|
+
function getMatches() {
|
|
151
|
+
const q = buffer.toLowerCase();
|
|
152
|
+
return getPrompts().filter(p =>
|
|
153
|
+
p.name.toLowerCase().includes(q) || p.text.toLowerCase().includes(q)
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function showDropdown() {
|
|
158
|
+
closeDropdown();
|
|
159
|
+
const matches = getMatches();
|
|
160
|
+
|
|
161
|
+
dropdown = document.createElement('div');
|
|
162
|
+
dropdown.className = 'prompt-autocomplete';
|
|
163
|
+
renderDropdownContent(matches);
|
|
164
|
+
|
|
165
|
+
// Position: append hidden, measure, bottom-anchor to viewport
|
|
166
|
+
const sidebar = document.getElementById('sidebar');
|
|
167
|
+
const sidebarRect = sidebar ? sidebar.getBoundingClientRect() : { right: 60 };
|
|
168
|
+
const gap = 8;
|
|
169
|
+
dropdown.style.visibility = 'hidden';
|
|
170
|
+
document.body.appendChild(dropdown);
|
|
171
|
+
const h = dropdown.offsetHeight;
|
|
172
|
+
let left = sidebarRect.right + 32;
|
|
173
|
+
let top = window.innerHeight - h - gap;
|
|
174
|
+
if (left + 340 > window.innerWidth - gap) left = window.innerWidth - 340 - gap;
|
|
175
|
+
if (left < gap) left = gap;
|
|
176
|
+
if (top < gap) top = gap;
|
|
177
|
+
dropdown.style.left = left + 'px';
|
|
178
|
+
dropdown.style.top = top + 'px';
|
|
179
|
+
dropdown.style.visibility = '';
|
|
180
|
+
|
|
181
|
+
// Only activate input capture after dropdown is successfully mounted
|
|
182
|
+
active = true;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function renderDropdownContent(matches) {
|
|
186
|
+
if (!dropdown) return;
|
|
187
|
+
const q = buffer.toLowerCase();
|
|
188
|
+
|
|
189
|
+
if (!matches.length) {
|
|
190
|
+
dropdown.innerHTML = `
|
|
191
|
+
<div class="pa-empty">No matching prompts</div>
|
|
192
|
+
<div class="pa-hint">Type <kbd>//</kbd> to search your prompt library</div>`;
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
dropdown.innerHTML = `
|
|
197
|
+
<div class="pa-header">
|
|
198
|
+
<span class="pa-label">Prompts</span>
|
|
199
|
+
<span class="pa-query">//${esc(buffer)}</span>
|
|
200
|
+
</div>
|
|
201
|
+
<div class="pa-list">${matches.map((p, i) => `
|
|
202
|
+
<div class="pa-item${i === selectedIdx ? ' pa-selected' : ''}" data-idx="${i}">
|
|
203
|
+
<div class="pa-name">${highlight(p.name, q)}</div>
|
|
204
|
+
<div class="pa-text">${highlight(truncate(p.text, 80), q)}</div>
|
|
205
|
+
</div>`).join('')}
|
|
206
|
+
</div>
|
|
207
|
+
<div class="pa-footer">
|
|
208
|
+
<span><kbd>↑↓</kbd> navigate</span>
|
|
209
|
+
<span><kbd>Enter</kbd> paste</span>
|
|
210
|
+
<span><kbd>Esc</kbd> cancel</span>
|
|
211
|
+
</div>`;
|
|
212
|
+
|
|
213
|
+
// Click to select
|
|
214
|
+
dropdown.querySelectorAll('.pa-item').forEach(el => {
|
|
215
|
+
el.addEventListener('mousedown', (e) => {
|
|
216
|
+
e.preventDefault();
|
|
217
|
+
const match = matches[+el.dataset.idx];
|
|
218
|
+
if (match) { closeDropdown(); pastePrompt(match.text); }
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function updateDropdown() {
|
|
224
|
+
if (!dropdown) return;
|
|
225
|
+
const matches = getMatches();
|
|
226
|
+
selectedIdx = Math.min(selectedIdx, Math.max(0, matches.length - 1));
|
|
227
|
+
renderDropdownContent(matches);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export function closeDropdown() {
|
|
231
|
+
active = false;
|
|
232
|
+
buffer = '';
|
|
233
|
+
selectedIdx = 0;
|
|
234
|
+
if (dropdown) { dropdown.remove(); dropdown = null; }
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function highlight(text, q) {
|
|
238
|
+
if (!q) return esc(text);
|
|
239
|
+
const i = text.toLowerCase().indexOf(q);
|
|
240
|
+
if (i < 0) return esc(text);
|
|
241
|
+
return esc(text.slice(0, i)) + '<mark>' + esc(text.slice(i, i + q.length)) + '</mark>' + esc(text.slice(i + q.length));
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function truncate(s, n) {
|
|
245
|
+
return s.length > n ? s.slice(0, n) + '…' : s;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// --- Key interception (called from hotkeys.js attachToTerminal) ---
|
|
249
|
+
|
|
250
|
+
// Trigger detection: let the first / go to the terminal immediately (no lag).
|
|
251
|
+
// If a second / arrives within 300ms, erase the first / with backspace and open autocomplete.
|
|
252
|
+
let lastSlashTime = 0;
|
|
253
|
+
|
|
254
|
+
export function handleTerminalKey(e) {
|
|
255
|
+
if (e.type !== 'keydown') return true;
|
|
256
|
+
|
|
257
|
+
// If autocomplete is open, consume ALL keys — nothing should leak to hotkeys
|
|
258
|
+
if (active) {
|
|
259
|
+
const matches = getMatches();
|
|
260
|
+
if (e.key === 'Escape') {
|
|
261
|
+
e.preventDefault();
|
|
262
|
+
closeDropdown();
|
|
263
|
+
return false;
|
|
264
|
+
}
|
|
265
|
+
if (e.key === 'ArrowUp') {
|
|
266
|
+
e.preventDefault();
|
|
267
|
+
selectedIdx = Math.max(0, selectedIdx - 1);
|
|
268
|
+
updateDropdown();
|
|
269
|
+
return false;
|
|
270
|
+
}
|
|
271
|
+
if (e.key === 'ArrowDown') {
|
|
272
|
+
e.preventDefault();
|
|
273
|
+
selectedIdx = Math.min(matches.length - 1, selectedIdx + 1);
|
|
274
|
+
updateDropdown();
|
|
275
|
+
return false;
|
|
276
|
+
}
|
|
277
|
+
if (e.key === 'Enter' || e.key === 'Tab') {
|
|
278
|
+
e.preventDefault();
|
|
279
|
+
const match = matches[selectedIdx];
|
|
280
|
+
closeDropdown();
|
|
281
|
+
if (match) pastePrompt(match.text);
|
|
282
|
+
return false;
|
|
283
|
+
}
|
|
284
|
+
if (e.key === 'Backspace') {
|
|
285
|
+
e.preventDefault();
|
|
286
|
+
if (buffer.length > 0) {
|
|
287
|
+
buffer = buffer.slice(0, -1);
|
|
288
|
+
updateDropdown();
|
|
289
|
+
} else {
|
|
290
|
+
closeDropdown();
|
|
291
|
+
}
|
|
292
|
+
return false;
|
|
293
|
+
}
|
|
294
|
+
// Printable character — append to buffer
|
|
295
|
+
if (e.key.length === 1 && !e.ctrlKey && !e.metaKey && !e.altKey) {
|
|
296
|
+
e.preventDefault();
|
|
297
|
+
buffer += e.key;
|
|
298
|
+
updateDropdown();
|
|
299
|
+
return false;
|
|
300
|
+
}
|
|
301
|
+
// Block everything else (modifiers, function keys) while autocomplete is open
|
|
302
|
+
e.preventDefault();
|
|
303
|
+
return false;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Detect // trigger — first / goes through normally, second / within 300ms activates
|
|
307
|
+
if (e.key === '/' && !e.ctrlKey && !e.metaKey && !e.altKey && getPrompts().length > 0) {
|
|
308
|
+
const now = Date.now();
|
|
309
|
+
if (now - lastSlashTime < 300) {
|
|
310
|
+
// Second / — erase the first / from terminal, open autocomplete
|
|
311
|
+
lastSlashTime = 0;
|
|
312
|
+
e.preventDefault();
|
|
313
|
+
if (state.active) send({ type: 'input', id: state.active, data: '\x7f' }); // backspace to remove first /
|
|
314
|
+
showDropdown();
|
|
315
|
+
return false;
|
|
316
|
+
}
|
|
317
|
+
// First / — let it through, record time
|
|
318
|
+
lastSlashTime = now;
|
|
319
|
+
return true;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// Any other key resets the slash timer
|
|
323
|
+
lastSlashTime = 0;
|
|
324
|
+
return true;
|
|
325
|
+
}
|