clideck 1.26.0 → 1.26.3
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 +17 -4
- package/activity.js +15 -1
- package/config.js +74 -1
- package/handlers.js +31 -4
- package/package.json +2 -1
- package/plugin-loader.js +128 -11
- package/plugins/autopilot/clideck-plugin.json +52 -0
- package/plugins/autopilot/client.js +84 -0
- package/plugins/autopilot/index.js +797 -0
- package/plugins/autopilot/prompt.md +68 -0
- package/public/index.html +22 -4
- package/public/js/app.js +77 -7
- package/public/js/creator.js +72 -6
- package/public/js/folder-picker.js +87 -4
- package/public/js/nav.js +2 -2
- package/public/js/prompts.js +1 -1
- package/public/js/roles.js +112 -0
- package/public/js/state.js +2 -0
- package/public/js/terminals.js +219 -2
- package/public/js/toast.js +28 -9
- package/public/tailwind.css +1 -1
- package/server.js +7 -4
- package/sessions.js +74 -6
- package/telemetry-receiver.js +75 -41
- package/transcript.js +15 -3
- package/utils.js +2 -2
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
You are an autonomous dispatcher for project: {{projectName}}.
|
|
2
|
+
|
|
3
|
+
YOUR ROLE
|
|
4
|
+
You control workflow routing between agents.
|
|
5
|
+
You do not do the work yourself.
|
|
6
|
+
You do not rewrite agent output.
|
|
7
|
+
You do not send summaries, edits, or instructions of your own to agents.
|
|
8
|
+
The system forwards existing agent output verbatim. Your job is to choose the best next handoff.
|
|
9
|
+
|
|
10
|
+
IMPORTANT
|
|
11
|
+
You are not a final judge of whether work is good or bad.
|
|
12
|
+
But you must understand the project, the goals, the current state of the work, and what each agent is responsible for, so you can decide the best next routing move.
|
|
13
|
+
|
|
14
|
+
This means:
|
|
15
|
+
- You should understand what the project is trying to achieve.
|
|
16
|
+
- You should understand what each agent just produced.
|
|
17
|
+
- You should understand what kind of specialist should act next.
|
|
18
|
+
- You may decide that the next step is not the most obvious direct handoff if another specialist should look first.
|
|
19
|
+
- Example: if a creative output needs analytical grounding, the right next move may be to route it to an analyst before routing it back to the creative.
|
|
20
|
+
- Your task is not to judge quality for the team. Your task is to route work to the agent best positioned to move the project forward.
|
|
21
|
+
|
|
22
|
+
AGENTS
|
|
23
|
+
{{agents}}
|
|
24
|
+
|
|
25
|
+
For each agent, treat their role description as the source of truth for:
|
|
26
|
+
- what they are responsible for
|
|
27
|
+
- what they should not do
|
|
28
|
+
- what kind of outputs they should receive
|
|
29
|
+
|
|
30
|
+
STATE
|
|
31
|
+
You will receive structured workflow state describing:
|
|
32
|
+
- which agents are WORKING or IDLE
|
|
33
|
+
- which outputs are new
|
|
34
|
+
- which outputs were already routed, and to whom
|
|
35
|
+
- what the last route was
|
|
36
|
+
- which role Autopilot is currently waiting on
|
|
37
|
+
- whether the workflow appears stale
|
|
38
|
+
|
|
39
|
+
TOOLS
|
|
40
|
+
- route(from, to): Forward one agent's existing output to another idle agent.
|
|
41
|
+
- notify_user(reason): Stop autopilot and notify the user. Use light markdown: **bold** for key terms, `code` for file/function names, bullet lists for summaries. Keep it concise (2-5 sentences). Use ONLY when the work is naturally complete, truly blocked, or requires human input.
|
|
42
|
+
|
|
43
|
+
RULES
|
|
44
|
+
- Call exactly ONE tool per response.
|
|
45
|
+
- Read the workflow state first, then read the agent outputs.
|
|
46
|
+
- Prefer routing new output over previously routed output.
|
|
47
|
+
- Use the project goal and current state to decide the best next specialist.
|
|
48
|
+
- Use role responsibilities and restrictions when choosing the next receiver.
|
|
49
|
+
- Do not route to an agent whose role makes the handoff inappropriate.
|
|
50
|
+
- Do not invent new instructions for agents. You only choose who receives whose output.
|
|
51
|
+
|
|
52
|
+
DO NOT USE notify_user UNLESS ABSOLUTELY NECESSARY
|
|
53
|
+
- Do NOT ask the user if you should continue. Do NOT notify them with requests like "Please resume agent X" or "Should I keep going?" or "Is this a good stopping point?"
|
|
54
|
+
- The user may be away from the computer and expects the agents to keep working until the task is naturally complete.
|
|
55
|
+
- You are autonomous. If you are unsure how to proceed, re-read the workflow state and the latest agent outputs, think differently, and route again.
|
|
56
|
+
- Repeat agents with the same output if needed, unless the routing state shows that the same handoff is being repeated without progress.
|
|
57
|
+
- You steer between agents until the task is complete or the user interrupts you, period.
|
|
58
|
+
|
|
59
|
+
HOW TO THINK
|
|
60
|
+
For each decision, reason in this order:
|
|
61
|
+
1. What is the project trying to achieve right now?
|
|
62
|
+
2. What changed most recently?
|
|
63
|
+
3. Which specialist is best suited for the next step?
|
|
64
|
+
4. Has this output already been consumed by that role?
|
|
65
|
+
5. Is there a better intermediate handoff before sending it to the most obvious role?
|
|
66
|
+
|
|
67
|
+
GOAL
|
|
68
|
+
Keep the work moving until the task is complete or truly blocked, by routing each output to the most appropriate next agent.
|
package/public/index.html
CHANGED
|
@@ -22,16 +22,19 @@
|
|
|
22
22
|
--color-session-icon-bg: #f7f5f3;
|
|
23
23
|
}
|
|
24
24
|
.session-row,
|
|
25
|
-
.resumable-row
|
|
25
|
+
.resumable-row,
|
|
26
|
+
.pill-row {
|
|
26
27
|
margin: 2px 8px;
|
|
27
28
|
border-radius: 10px;
|
|
28
29
|
transition: background-color 0.18s ease;
|
|
29
30
|
}
|
|
30
31
|
.session-row:hover,
|
|
31
|
-
.resumable-row:hover
|
|
32
|
+
.resumable-row:hover,
|
|
33
|
+
.pill-row:hover {
|
|
32
34
|
background: var(--color-list-row-hover) !important;
|
|
33
35
|
}
|
|
34
|
-
.session-row.active-session
|
|
36
|
+
.session-row.active-session,
|
|
37
|
+
.pill-row.active-session {
|
|
35
38
|
background: var(--color-list-row-active) !important;
|
|
36
39
|
}
|
|
37
40
|
.project-group {
|
|
@@ -144,6 +147,9 @@
|
|
|
144
147
|
<button class="rail-btn w-9 h-9 flex items-center justify-center rounded-lg text-slate-500 hover:text-slate-300 hover:bg-slate-800/50 transition-colors" data-panel="prompts" title="Prompt Library">
|
|
145
148
|
<svg class="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><path d="M14 2v6h6"/><path d="M16 13H8"/><path d="M16 17H8"/></svg>
|
|
146
149
|
</button>
|
|
150
|
+
<button class="rail-btn w-9 h-9 flex items-center justify-center rounded-lg text-slate-500 hover:text-slate-300 hover:bg-slate-800/50 transition-colors" data-panel="roles" title="Roles">
|
|
151
|
+
<svg class="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M22 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg>
|
|
152
|
+
</button>
|
|
147
153
|
<button class="rail-btn w-9 h-9 flex items-center justify-center rounded-lg text-slate-500 hover:text-slate-300 hover:bg-slate-800/50 transition-colors" data-panel="plugins" title="Plugins">
|
|
148
154
|
<svg class="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2v4m0 12v4M2 12h4m12 0h4"/><circle cx="12" cy="12" r="3"/><path d="M12 8V6m0 12v-2M8 12H6m12 0h-2"/></svg>
|
|
149
155
|
</button>
|
|
@@ -194,6 +200,8 @@
|
|
|
194
200
|
</div>
|
|
195
201
|
<!-- Prompts panel (rendered by prompts.js) -->
|
|
196
202
|
<div id="panel-prompts" class="hidden flex-col flex-1 min-h-0"></div>
|
|
203
|
+
<!-- Roles panel (rendered by roles.js) -->
|
|
204
|
+
<div id="panel-roles" class="hidden flex-col flex-1 min-h-0"></div>
|
|
197
205
|
<!-- Plugins panel -->
|
|
198
206
|
<div id="panel-plugins" class="hidden flex-col flex-1 min-h-0">
|
|
199
207
|
<div class="flex items-center px-4 py-3 border-b border-slate-700/50">
|
|
@@ -453,7 +461,17 @@
|
|
|
453
461
|
<!-- Folder picker -->
|
|
454
462
|
<div id="folder-picker" class="absolute inset-0 z-[300] bg-black/60 backdrop-blur-sm hidden items-center justify-center">
|
|
455
463
|
<div class="bg-slate-800 border border-slate-600 rounded-xl shadow-2xl shadow-black/50 w-[420px] max-h-[460px] flex flex-col">
|
|
456
|
-
<div class="px-4 py-3 border-b border-slate-700
|
|
464
|
+
<div class="px-4 py-3 border-b border-slate-700 flex items-center justify-between">
|
|
465
|
+
<span class="text-sm font-semibold">Choose Directory</span>
|
|
466
|
+
<div class="flex items-center gap-2">
|
|
467
|
+
<button id="fp-new-folder" class="p-1 rounded hover:bg-slate-700 text-slate-400 hover:text-slate-200 transition-colors" title="New folder">
|
|
468
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 5v14M5 12h14"/></svg>
|
|
469
|
+
</button>
|
|
470
|
+
<button id="fp-toggle-hidden" class="p-1 rounded hover:bg-slate-700 text-slate-500 transition-colors" title="Show hidden files">
|
|
471
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg>
|
|
472
|
+
</button>
|
|
473
|
+
</div>
|
|
474
|
+
</div>
|
|
457
475
|
<div id="fp-path" class="px-4 py-2 text-xs text-slate-400 border-b border-slate-700 break-all"></div>
|
|
458
476
|
<div id="fp-listing" class="flex-1 overflow-y-auto py-1 min-h-[200px]"></div>
|
|
459
477
|
<div class="px-4 py-3 border-t border-slate-700 flex justify-end gap-2">
|
package/public/js/app.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { state, send } from './state.js';
|
|
2
2
|
import { esc, binName } from './utils.js';
|
|
3
|
-
import { addTerminal, removeTerminal, select, startRename, startProjectRename, setSessionTheme, openMenu, closeMenu, setStatus, updateMuteIndicator, updatePreview, markUnread, applyFilter, setTab, renderResumable, regroupSessions, toggleProjectCollapse, setSessionProject, estimateSize, restartComplete, positionMenu } from './terminals.js';
|
|
3
|
+
import { addTerminal, removeTerminal, select, startRename, startProjectRename, setSessionTheme, openMenu, closeMenu, setStatus, updateMuteIndicator, updatePreview, markUnread, applyFilter, setTab, renderResumable, regroupSessions, toggleProjectCollapse, setSessionProject, estimateSize, restartComplete, positionMenu, addPill, updatePill, removePill, appendPillLog, setPillLogs, closePillLog } from './terminals.js';
|
|
4
4
|
import { renderSettings, updateVersionFooter } from './settings.js';
|
|
5
5
|
import { openCreator, closeCreator, refreshCreator } from './creator.js';
|
|
6
|
-
import { handleDirsResponse, openFolderPicker } from './folder-picker.js';
|
|
6
|
+
import { handleDirsResponse, handleMkdirResponse, openFolderPicker } from './folder-picker.js';
|
|
7
7
|
import { confirmClose } from './confirm.js';
|
|
8
8
|
import { applyTheme } from './profiles.js';
|
|
9
9
|
import { toggleMode, applyMode } from './color-mode.js';
|
|
@@ -12,6 +12,7 @@ import './nav.js';
|
|
|
12
12
|
import { initDrag, wasDragging } from './drag.js';
|
|
13
13
|
import { registerHotkey, unregisterHotkey, unregisterAllForPlugin } from './hotkeys.js';
|
|
14
14
|
import { renderPrompts } from './prompts.js';
|
|
15
|
+
import { renderRoles } from './roles.js';
|
|
15
16
|
|
|
16
17
|
function connect() {
|
|
17
18
|
state.ws = new WebSocket(`ws://${location.host}`);
|
|
@@ -19,7 +20,10 @@ function connect() {
|
|
|
19
20
|
state.ws.onopen = () => {
|
|
20
21
|
for (const [, e] of state.terms) { e.ro.disconnect(); e.term.dispose(); e.el.remove(); }
|
|
21
22
|
state.terms.clear();
|
|
23
|
+
state.pills.clear();
|
|
24
|
+
state.activePill = null;
|
|
22
25
|
document.getElementById('session-list').innerHTML = '';
|
|
26
|
+
document.getElementById('pill-log-panel')?.remove();
|
|
23
27
|
state.active = null;
|
|
24
28
|
document.getElementById('empty').style.display = 'flex';
|
|
25
29
|
send({ type: 'remote.status' });
|
|
@@ -34,6 +38,7 @@ function connect() {
|
|
|
34
38
|
regroupSessions();
|
|
35
39
|
renderSettings();
|
|
36
40
|
renderPrompts();
|
|
41
|
+
renderRoles();
|
|
37
42
|
for (const [, entry] of state.terms) applyTheme(entry.term, entry.themeId);
|
|
38
43
|
break;
|
|
39
44
|
case 'themes':
|
|
@@ -140,6 +145,9 @@ function connect() {
|
|
|
140
145
|
case 'dirs':
|
|
141
146
|
handleDirsResponse(msg);
|
|
142
147
|
break;
|
|
148
|
+
case 'dirs.mkdir':
|
|
149
|
+
handleMkdirResponse(msg);
|
|
150
|
+
break;
|
|
143
151
|
case 'session.theme': {
|
|
144
152
|
const entry = state.terms.get(msg.id);
|
|
145
153
|
if (entry) {
|
|
@@ -202,6 +210,25 @@ function connect() {
|
|
|
202
210
|
case 'plugins':
|
|
203
211
|
loadPlugins(msg.list);
|
|
204
212
|
break;
|
|
213
|
+
case 'pills':
|
|
214
|
+
state.pills.clear();
|
|
215
|
+
for (const p of msg.list) addPill(p);
|
|
216
|
+
break;
|
|
217
|
+
case 'pill.added':
|
|
218
|
+
addPill(msg.pill);
|
|
219
|
+
break;
|
|
220
|
+
case 'pill.updated':
|
|
221
|
+
updatePill(msg.pill);
|
|
222
|
+
break;
|
|
223
|
+
case 'pill.removed':
|
|
224
|
+
removePill(msg.id);
|
|
225
|
+
break;
|
|
226
|
+
case 'pill.log':
|
|
227
|
+
appendPillLog(msg.id, msg.entry);
|
|
228
|
+
break;
|
|
229
|
+
case 'pill.logs':
|
|
230
|
+
setPillLogs(msg.id, msg.logs);
|
|
231
|
+
break;
|
|
205
232
|
case 'plugin.delete.error':
|
|
206
233
|
showToast(`Failed to remove plugin: ${msg.error}`, { duration: 4000 });
|
|
207
234
|
break;
|
|
@@ -247,6 +274,7 @@ mobileQuery.addEventListener('change', (e) => { if (!e.matches) closeMobileSideb
|
|
|
247
274
|
|
|
248
275
|
// Sidebar events
|
|
249
276
|
const sessionList = document.getElementById('session-list');
|
|
277
|
+
sessionList.addEventListener('projects-rendered', () => renderProjectActions());
|
|
250
278
|
|
|
251
279
|
sessionList.addEventListener('click', (e) => {
|
|
252
280
|
closeCreator();
|
|
@@ -254,6 +282,7 @@ sessionList.addEventListener('click', (e) => {
|
|
|
254
282
|
|
|
255
283
|
// Project header click — toggle collapse (skip if just finished a drag)
|
|
256
284
|
const projHeader = e.target.closest('.project-header');
|
|
285
|
+
if (e.target.closest('.plugin-project-btn')) return; // handled by btn's own click listener
|
|
257
286
|
if (projHeader && !e.target.closest('.project-menu-btn') && !wasDragging()) {
|
|
258
287
|
toggleProjectCollapse(projHeader.dataset.projectId);
|
|
259
288
|
return;
|
|
@@ -279,6 +308,9 @@ sessionList.addEventListener('click', (e) => {
|
|
|
279
308
|
return;
|
|
280
309
|
}
|
|
281
310
|
|
|
311
|
+
// Pill row click — handled by pill's own listener
|
|
312
|
+
if (e.target.closest('.pill-row')) return;
|
|
313
|
+
|
|
282
314
|
const item = e.target.closest('.group');
|
|
283
315
|
if (!item) return;
|
|
284
316
|
|
|
@@ -720,7 +752,7 @@ function renderPluginsPanel(list) {
|
|
|
720
752
|
</div>
|
|
721
753
|
<div class="plugin-body ${open ? '' : 'hidden'}">
|
|
722
754
|
<div class="px-4 pb-3">
|
|
723
|
-
${(p.settings || []).map(s => renderSettingField(p.id, s, p.settingValues[s.key] ?? s.default)).join('')}
|
|
755
|
+
${(p.settings || []).map(s => renderSettingField(p.id, s, p.settingValues[s.key] ?? s.default, p.dynamicOptions)).join('')}
|
|
724
756
|
</div>
|
|
725
757
|
</div>
|
|
726
758
|
</div>`;
|
|
@@ -756,11 +788,11 @@ function renderPluginsPanel(list) {
|
|
|
756
788
|
if (el.type === 'checkbox') el.addEventListener('change', () => onChange(el.checked));
|
|
757
789
|
else if (el.tagName === 'SELECT') el.addEventListener('change', () => onChange(el.value));
|
|
758
790
|
else if (el.type === 'number') el.addEventListener('change', () => onChange(Number(el.value)));
|
|
759
|
-
else el.addEventListener('
|
|
791
|
+
else el.addEventListener('change', () => onChange(el.value));
|
|
760
792
|
});
|
|
761
793
|
}
|
|
762
794
|
|
|
763
|
-
function renderSettingField(pluginId, setting, value) {
|
|
795
|
+
function renderSettingField(pluginId, setting, value, dynamicOptions) {
|
|
764
796
|
const id = `ps-${pluginId}-${setting.key}`;
|
|
765
797
|
const attrs = `data-plugin="${esc(pluginId)}" data-setting="${esc(setting.key)}"`;
|
|
766
798
|
const label = esc(setting.label || setting.key);
|
|
@@ -772,12 +804,17 @@ function renderSettingField(pluginId, setting, value) {
|
|
|
772
804
|
<span class="text-xs text-slate-400">${label}</span>
|
|
773
805
|
</label>${desc}`;
|
|
774
806
|
}
|
|
775
|
-
if (setting.type === 'select') {
|
|
776
|
-
const
|
|
807
|
+
if (setting.type === 'select' || setting.type === 'dynamic-select') {
|
|
808
|
+
const source = setting.type === 'dynamic-select' ? (dynamicOptions?.[setting.key] || []) : (setting.options || []);
|
|
809
|
+
let opts = source.map(o => {
|
|
777
810
|
const optVal = typeof o === 'object' ? o.value : o;
|
|
778
811
|
const optLabel = typeof o === 'object' ? o.label : o;
|
|
779
812
|
return `<option value="${esc(String(optVal))}" ${String(value) === String(optVal) ? 'selected' : ''}>${esc(String(optLabel))}</option>`;
|
|
780
813
|
}).join('');
|
|
814
|
+
// Dynamic-select with no options yet: show the saved value so the control isn't blank
|
|
815
|
+
if (setting.type === 'dynamic-select' && !source.length && value) {
|
|
816
|
+
opts = `<option value="${esc(String(value))}" selected>${esc(String(value))}</option>`;
|
|
817
|
+
}
|
|
781
818
|
return `<div class="mt-2">
|
|
782
819
|
<label class="block text-xs text-slate-400 mb-1">${label}</label>
|
|
783
820
|
<select id="${id}" ${attrs} class="w-full px-2 py-1.5 text-xs bg-slate-800 border border-slate-700 rounded-md text-slate-200 outline-none focus:border-blue-500 transition-colors">${opts}</select>
|
|
@@ -817,6 +854,15 @@ async function loadPlugins(list) {
|
|
|
817
854
|
|
|
818
855
|
renderPluginsPanel(list);
|
|
819
856
|
|
|
857
|
+
// Store project-header actions from plugins (used by regroupSessions to render icons)
|
|
858
|
+
state.projectActions = [];
|
|
859
|
+
for (const plugin of list) {
|
|
860
|
+
for (const action of plugin.actions || []) {
|
|
861
|
+
if (action.slot === 'project-header') state.projectActions.push({ ...action, pluginId: plugin.id });
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
renderProjectActions();
|
|
865
|
+
|
|
820
866
|
// Render server-registered toolbar actions — also clears stale client toolbar buttons
|
|
821
867
|
const toolbar = document.getElementById('plugin-toolbar');
|
|
822
868
|
toolbar.querySelectorAll('.plugin-btn').forEach(b => {
|
|
@@ -863,6 +909,30 @@ async function loadPlugins(list) {
|
|
|
863
909
|
}
|
|
864
910
|
}
|
|
865
911
|
|
|
912
|
+
// Render plugin-registered project header action buttons into all project groups
|
|
913
|
+
function renderProjectActions() {
|
|
914
|
+
const actions = state.projectActions || [];
|
|
915
|
+
for (const slot of document.querySelectorAll('.project-plugin-actions')) {
|
|
916
|
+
slot.innerHTML = '';
|
|
917
|
+
const projId = slot.closest('.project-header')?.dataset.projectId;
|
|
918
|
+
if (!projId) continue;
|
|
919
|
+
for (const action of actions) {
|
|
920
|
+
const btn = document.createElement('button');
|
|
921
|
+
btn.className = 'project-plugin-action plugin-project-btn opacity-0 group-hover:opacity-100 text-slate-600 hover:text-indigo-400 flex-shrink-0 transition-opacity p-0.5';
|
|
922
|
+
btn.title = action.title || '';
|
|
923
|
+
btn.innerHTML = action.icon || '';
|
|
924
|
+
btn.dataset.pluginId = action.pluginId;
|
|
925
|
+
btn.dataset.actionId = action.id;
|
|
926
|
+
btn.dataset.projectId = projId;
|
|
927
|
+
btn.addEventListener('click', (e) => {
|
|
928
|
+
e.stopPropagation();
|
|
929
|
+
send({ type: `plugin.${action.pluginId}.${action.id}`, action: action.id, projectId: projId });
|
|
930
|
+
});
|
|
931
|
+
slot.appendChild(btn);
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
|
|
866
936
|
let saveTimer = null;
|
|
867
937
|
function flashSaveIndicator() {
|
|
868
938
|
const el = document.getElementById('save-indicator');
|
package/public/js/creator.js
CHANGED
|
@@ -74,7 +74,7 @@ function sortedPresets() {
|
|
|
74
74
|
return [...agents, ...shell];
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
-
function createFromPreset(preset, sessionName, cwd, projectId) {
|
|
77
|
+
function createFromPreset(preset, sessionName, cwd, projectId, roleId) {
|
|
78
78
|
// Find existing command matching this preset
|
|
79
79
|
let cmd = findCommandForPreset(preset);
|
|
80
80
|
// Auto-create the command if it doesn't exist yet
|
|
@@ -99,7 +99,7 @@ function createFromPreset(preset, sessionName, cwd, projectId) {
|
|
|
99
99
|
state.cfg.commands.push(cmd);
|
|
100
100
|
send({ type: 'config.update', config: state.cfg });
|
|
101
101
|
}
|
|
102
|
-
send({ type: 'create', commandId: cmd.id, name: sessionName, cwd, projectId: projectId || undefined, ...estimateSize() });
|
|
102
|
+
send({ type: 'create', commandId: cmd.id, name: sessionName, cwd, projectId: projectId || undefined, roleId: roleId || undefined, ...estimateSize() });
|
|
103
103
|
localStorage.setItem(MRU_KEY, preset.presetId);
|
|
104
104
|
}
|
|
105
105
|
|
|
@@ -124,7 +124,13 @@ export function openCreator() {
|
|
|
124
124
|
${(state.cfg.projects?.length) ? `
|
|
125
125
|
<input type="hidden" id="creator-project" value="">
|
|
126
126
|
<button type="button" id="creator-project-trigger" class="w-full px-3 py-1.5 text-xs bg-slate-900 border border-slate-700 rounded-md text-slate-400 text-left flex items-center justify-between outline-none hover:border-slate-500 transition-colors cursor-pointer mb-2">
|
|
127
|
-
<span id="creator-project-label">
|
|
127
|
+
<span id="creator-project-label">Select project <span class="opacity-40">- optional</span></span>
|
|
128
|
+
<span class="text-slate-600 ml-2">▾</span>
|
|
129
|
+
</button>` : ''}
|
|
130
|
+
${(state.cfg.roles?.length) ? `
|
|
131
|
+
<input type="hidden" id="creator-role" value="">
|
|
132
|
+
<button type="button" id="creator-role-trigger" class="w-full px-3 py-1.5 text-xs bg-slate-900 border border-slate-700 rounded-md text-slate-400 text-left flex items-center justify-between outline-none hover:border-slate-500 transition-colors cursor-pointer mb-2">
|
|
133
|
+
<span id="creator-role-label">Select role <span class="opacity-40">- optional</span></span>
|
|
128
134
|
<span class="text-slate-600 ml-2">▾</span>
|
|
129
135
|
</button>` : ''}
|
|
130
136
|
<input id="creator-name" type="text" maxlength="35" placeholder="Session / Agent name"
|
|
@@ -179,7 +185,7 @@ export function openCreator() {
|
|
|
179
185
|
menu.style.width = rect.width + 'px';
|
|
180
186
|
|
|
181
187
|
menu.innerHTML = `
|
|
182
|
-
<div class="proj-option px-3 py-1.5 cursor-pointer hover:bg-slate-700 transition-colors text-xs text-slate-400 ${!hidden.value ? 'bg-slate-700/50' : ''}" data-value="">
|
|
188
|
+
<div class="proj-option px-3 py-1.5 cursor-pointer hover:bg-slate-700 transition-colors text-xs text-slate-400 ${!hidden.value ? 'bg-slate-700/50' : ''}" data-value="">None</div>
|
|
183
189
|
${projects.map(p => `
|
|
184
190
|
<div class="proj-option flex items-center gap-2 px-3 py-1.5 cursor-pointer hover:bg-slate-700 transition-colors text-xs text-slate-300 ${hidden.value === p.id ? 'bg-slate-700/50' : ''}" data-value="${p.id}">
|
|
185
191
|
<span class="w-2 h-2 rounded-full flex-shrink-0" style="background:${p.color || '#3b82f6'}"></span>
|
|
@@ -193,7 +199,7 @@ export function openCreator() {
|
|
|
193
199
|
if (!item) return;
|
|
194
200
|
hidden.value = item.dataset.value;
|
|
195
201
|
const proj = projects.find(p => p.id === item.dataset.value);
|
|
196
|
-
label.
|
|
202
|
+
label.innerHTML = proj ? esc(proj.name) : 'Select project <span class="opacity-40">- optional</span>';
|
|
197
203
|
// Auto-set working directory from project path
|
|
198
204
|
if (proj?.path) cwdInput.value = proj.path;
|
|
199
205
|
else cwdInput.value = defaultPath;
|
|
@@ -214,6 +220,64 @@ export function openCreator() {
|
|
|
214
220
|
});
|
|
215
221
|
}
|
|
216
222
|
|
|
223
|
+
// Role picker dropdown
|
|
224
|
+
const roleTrigger = card.querySelector('#creator-role-trigger');
|
|
225
|
+
if (roleTrigger) {
|
|
226
|
+
let roleMenuCleanup = null;
|
|
227
|
+
roleTrigger.addEventListener('click', () => {
|
|
228
|
+
if (roleMenuCleanup) { roleMenuCleanup(); return; }
|
|
229
|
+
const rect = roleTrigger.getBoundingClientRect();
|
|
230
|
+
const hidden = card.querySelector('#creator-role');
|
|
231
|
+
const label = card.querySelector('#creator-role-label');
|
|
232
|
+
const roles = state.cfg.roles || [];
|
|
233
|
+
|
|
234
|
+
const menu = document.createElement('div');
|
|
235
|
+
menu.className = 'fixed z-[500] bg-slate-800 border border-slate-600 rounded-lg shadow-xl shadow-black/40 py-1 overflow-y-auto';
|
|
236
|
+
menu.style.maxHeight = '200px';
|
|
237
|
+
menu.style.left = rect.left + 'px';
|
|
238
|
+
menu.style.top = (rect.bottom + 4) + 'px';
|
|
239
|
+
menu.style.width = rect.width + 'px';
|
|
240
|
+
|
|
241
|
+
menu.innerHTML = `
|
|
242
|
+
<div class="role-option px-3 py-1.5 cursor-pointer hover:bg-slate-700 transition-colors text-xs text-slate-400 ${!hidden.value ? 'bg-slate-700/50' : ''}" data-value="" data-name="">None</div>
|
|
243
|
+
${roles.map(r => `
|
|
244
|
+
<div class="role-option px-3 py-1.5 cursor-pointer hover:bg-slate-700 transition-colors text-xs text-slate-300 ${hidden.value === r.id ? 'bg-slate-700/50' : ''}" data-value="${r.id}" data-name="${esc(r.name)}">
|
|
245
|
+
${esc(r.name)}
|
|
246
|
+
</div>`).join('')}`;
|
|
247
|
+
|
|
248
|
+
document.body.appendChild(menu);
|
|
249
|
+
|
|
250
|
+
const onClick = (e) => {
|
|
251
|
+
const item = e.target.closest('.role-option');
|
|
252
|
+
if (!item) return;
|
|
253
|
+
hidden.value = item.dataset.value;
|
|
254
|
+
const roleName = item.dataset.name;
|
|
255
|
+
label.innerHTML = roleName ? esc(roleName) : 'Select role <span class="opacity-40">- optional</span>';
|
|
256
|
+
// Auto-fill session name from role name (only if user hasn't typed a custom name)
|
|
257
|
+
if (roleName && (!nameInput.value.trim() || nameInput.dataset.autoFilled === '1')) {
|
|
258
|
+
nameInput.value = roleName;
|
|
259
|
+
nameInput.dataset.autoFilled = '1';
|
|
260
|
+
}
|
|
261
|
+
if (!item.dataset.value) nameInput.dataset.autoFilled = '';
|
|
262
|
+
roleMenuCleanup();
|
|
263
|
+
};
|
|
264
|
+
const onOutside = (e) => {
|
|
265
|
+
if (!menu.contains(e.target) && !roleTrigger.contains(e.target)) roleMenuCleanup();
|
|
266
|
+
};
|
|
267
|
+
menu.addEventListener('click', onClick);
|
|
268
|
+
requestAnimationFrame(() => document.addEventListener('click', onOutside));
|
|
269
|
+
|
|
270
|
+
roleMenuCleanup = () => {
|
|
271
|
+
menu.removeEventListener('click', onClick);
|
|
272
|
+
document.removeEventListener('click', onOutside);
|
|
273
|
+
menu.remove();
|
|
274
|
+
roleMenuCleanup = null;
|
|
275
|
+
};
|
|
276
|
+
});
|
|
277
|
+
// Clear auto-fill flag when user manually types
|
|
278
|
+
nameInput.addEventListener('input', () => { nameInput.dataset.autoFilled = ''; });
|
|
279
|
+
}
|
|
280
|
+
|
|
217
281
|
// "Add" button for missing agents — opens install toaster
|
|
218
282
|
card.addEventListener('click', (e) => {
|
|
219
283
|
const installBtn = e.target.closest('.install-btn');
|
|
@@ -230,7 +294,9 @@ export function openCreator() {
|
|
|
230
294
|
const cwd = cwdInput.value.trim() || undefined;
|
|
231
295
|
const projectSelect = card.querySelector('#creator-project');
|
|
232
296
|
const projectId = projectSelect?.value || undefined;
|
|
233
|
-
|
|
297
|
+
const roleSelect = card.querySelector('#creator-role');
|
|
298
|
+
const roleId = roleSelect?.value || undefined;
|
|
299
|
+
createFromPreset(preset, name, cwd, projectId, roleId);
|
|
234
300
|
closeCreator();
|
|
235
301
|
});
|
|
236
302
|
}
|
|
@@ -19,9 +19,12 @@ const overlay = document.getElementById('folder-picker');
|
|
|
19
19
|
const pathBar = document.getElementById('fp-path');
|
|
20
20
|
const listing = document.getElementById('fp-listing');
|
|
21
21
|
const selectBtn = document.getElementById('fp-select');
|
|
22
|
+
const hiddenBtn = document.getElementById('fp-toggle-hidden');
|
|
23
|
+
const newFolderBtn = document.getElementById('fp-new-folder');
|
|
22
24
|
let currentPath = '';
|
|
23
25
|
let pendingPath = '';
|
|
24
26
|
let onSelect = null;
|
|
27
|
+
let showHidden = false;
|
|
25
28
|
|
|
26
29
|
export function openFolderPicker(startPath, callback) {
|
|
27
30
|
currentPath = '';
|
|
@@ -35,6 +38,7 @@ export function closeFolderPicker() {
|
|
|
35
38
|
overlay.classList.add('hidden');
|
|
36
39
|
overlay.classList.remove('flex');
|
|
37
40
|
onSelect = null;
|
|
41
|
+
closeNewFolderInput();
|
|
38
42
|
}
|
|
39
43
|
|
|
40
44
|
function navigate(path) {
|
|
@@ -42,7 +46,8 @@ function navigate(path) {
|
|
|
42
46
|
pathBar.textContent = path;
|
|
43
47
|
listing.innerHTML = '<div class="p-4 text-center text-slate-500 text-sm">Loading...</div>';
|
|
44
48
|
selectBtn.disabled = true;
|
|
45
|
-
|
|
49
|
+
closeNewFolderInput();
|
|
50
|
+
send({ type: 'dirs.list', path, showHidden });
|
|
46
51
|
}
|
|
47
52
|
|
|
48
53
|
export function handleDirsResponse(msg) {
|
|
@@ -62,12 +67,90 @@ export function handleDirsResponse(msg) {
|
|
|
62
67
|
if (msg.entries.length === 0 && !html) {
|
|
63
68
|
html = '<div class="p-4 text-center text-slate-500 text-sm">Empty directory</div>';
|
|
64
69
|
}
|
|
65
|
-
html += msg.entries.map(name =>
|
|
66
|
-
|
|
67
|
-
|
|
70
|
+
html += msg.entries.map(name => {
|
|
71
|
+
const dimClass = name.startsWith('.') ? ' text-slate-500' : ' text-slate-200';
|
|
72
|
+
return `<div class="fp-item px-4 py-1.5 cursor-pointer hover:bg-slate-700 text-sm${dimClass} transition-colors" data-path="${esc(joinChild(currentPath, name))}">${esc(name)}</div>`;
|
|
73
|
+
}).join('');
|
|
68
74
|
listing.innerHTML = html;
|
|
69
75
|
}
|
|
70
76
|
|
|
77
|
+
// --- Hidden files toggle ---
|
|
78
|
+
|
|
79
|
+
function updateHiddenBtn() {
|
|
80
|
+
hiddenBtn.classList.toggle('text-slate-200', showHidden);
|
|
81
|
+
hiddenBtn.classList.toggle('text-slate-500', !showHidden);
|
|
82
|
+
hiddenBtn.title = showHidden ? 'Hide hidden files' : 'Show hidden files';
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
hiddenBtn.addEventListener('click', () => {
|
|
86
|
+
showHidden = !showHidden;
|
|
87
|
+
updateHiddenBtn();
|
|
88
|
+
if (currentPath) navigate(currentPath);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
// --- New folder inline input ---
|
|
92
|
+
|
|
93
|
+
let newFolderActive = false;
|
|
94
|
+
|
|
95
|
+
function closeNewFolderInput() {
|
|
96
|
+
if (!newFolderActive) return;
|
|
97
|
+
newFolderActive = false;
|
|
98
|
+
const row = listing.querySelector('.fp-new-folder-row');
|
|
99
|
+
if (row) row.remove();
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function openNewFolderInput() {
|
|
103
|
+
if (newFolderActive || !currentPath) return;
|
|
104
|
+
newFolderActive = true;
|
|
105
|
+
const row = document.createElement('div');
|
|
106
|
+
row.className = 'fp-new-folder-row flex items-center gap-2 px-4 py-1.5';
|
|
107
|
+
row.innerHTML = `
|
|
108
|
+
<svg class="flex-shrink-0 text-slate-400" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 19a2 2 0 01-2 2H4a2 2 0 01-2-2V5a2 2 0 012-2h5l2 3h9a2 2 0 012 2z"/></svg>
|
|
109
|
+
<input type="text" class="fp-new-folder-input flex-1 bg-slate-700 border border-slate-600 rounded px-2 py-0.5 text-sm text-slate-200 placeholder-slate-500 outline-none focus:border-blue-500 transition-colors" placeholder="Folder name" spellcheck="false" />
|
|
110
|
+
<button class="fp-new-folder-ok p-0.5 rounded hover:bg-slate-700 text-emerald-400 hover:text-emerald-300 transition-colors" title="Create">
|
|
111
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>
|
|
112
|
+
</button>
|
|
113
|
+
<button class="fp-new-folder-no p-0.5 rounded hover:bg-slate-700 text-slate-400 hover:text-slate-300 transition-colors" title="Cancel">
|
|
114
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
|
|
115
|
+
</button>`;
|
|
116
|
+
listing.prepend(row);
|
|
117
|
+
const input = row.querySelector('.fp-new-folder-input');
|
|
118
|
+
input.focus();
|
|
119
|
+
|
|
120
|
+
function submit() {
|
|
121
|
+
const name = input.value.trim();
|
|
122
|
+
if (!name) { closeNewFolderInput(); return; }
|
|
123
|
+
input.disabled = true;
|
|
124
|
+
send({ type: 'dirs.mkdir', parent: currentPath, name });
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
input.addEventListener('keydown', (e) => {
|
|
128
|
+
if (e.key === 'Enter') { e.preventDefault(); submit(); }
|
|
129
|
+
if (e.key === 'Escape') { e.preventDefault(); closeNewFolderInput(); }
|
|
130
|
+
});
|
|
131
|
+
row.querySelector('.fp-new-folder-ok').addEventListener('click', submit);
|
|
132
|
+
row.querySelector('.fp-new-folder-no').addEventListener('click', closeNewFolderInput);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
newFolderBtn.addEventListener('click', openNewFolderInput);
|
|
136
|
+
|
|
137
|
+
export function handleMkdirResponse(msg) {
|
|
138
|
+
if (!newFolderActive) return;
|
|
139
|
+
closeNewFolderInput();
|
|
140
|
+
if (msg.success) {
|
|
141
|
+
navigate(msg.path);
|
|
142
|
+
} else {
|
|
143
|
+
// Show error inline briefly
|
|
144
|
+
const err = document.createElement('div');
|
|
145
|
+
err.className = 'px-4 py-1.5 text-xs text-red-400';
|
|
146
|
+
err.textContent = msg.error || 'Failed to create folder';
|
|
147
|
+
listing.prepend(err);
|
|
148
|
+
setTimeout(() => err.remove(), 3000);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// --- Navigation and select ---
|
|
153
|
+
|
|
71
154
|
listing.addEventListener('click', (e) => {
|
|
72
155
|
const item = e.target.closest('.fp-item');
|
|
73
156
|
if (item) navigate(item.dataset.path);
|
package/public/js/nav.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { closeThemeMenu } from './settings.js';
|
|
2
2
|
import { closeDropdown } from './prompts.js';
|
|
3
3
|
|
|
4
|
-
const ALL_PANELS = ['chats', 'prompts', 'plugins', 'settings'];
|
|
5
|
-
const PANEL_TITLES = { chats: 'Sessions', prompts: 'Prompts', plugins: 'Plugins', settings: 'Settings' };
|
|
4
|
+
const ALL_PANELS = ['chats', 'prompts', 'roles', 'plugins', 'settings'];
|
|
5
|
+
const PANEL_TITLES = { chats: 'Sessions', prompts: 'Prompts', roles: 'Roles', plugins: 'Plugins', settings: 'Settings' };
|
|
6
6
|
const ACTIVE = ['text-slate-200', 'bg-slate-800'];
|
|
7
7
|
const INACTIVE = ['text-slate-500', 'hover:text-slate-300', 'hover:bg-slate-800/50'];
|
|
8
8
|
|
package/public/js/prompts.js
CHANGED
|
@@ -139,7 +139,7 @@ function openEditor(idx) {
|
|
|
139
139
|
<input id="pe-name" type="text" maxlength="60" placeholder="Prompt name" value="${esc(existing?.name || '')}"
|
|
140
140
|
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">
|
|
141
141
|
<textarea id="pe-text" rows="4" placeholder="Prompt text to paste into terminal"
|
|
142
|
-
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-
|
|
142
|
+
class="w-full max-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-y leading-relaxed font-mono mb-2" style="min-height:5lh">${esc(existing?.text || '')}</textarea>
|
|
143
143
|
<div class="flex items-center gap-2">
|
|
144
144
|
<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>
|
|
145
145
|
<button id="pe-cancel" class="px-3 py-1.5 text-xs text-slate-500 hover:text-slate-300 transition-colors">Cancel</button>
|