clideck 1.26.0 → 1.26.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/README.md +17 -4
- package/activity.js +15 -1
- package/config.js +74 -1
- package/handlers.js +14 -3
- 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 +11 -3
- package/public/js/app.js +73 -6
- package/public/js/creator.js +72 -6
- 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
|
@@ -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">
|
package/public/js/app.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
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
6
|
import { handleDirsResponse, openFolderPicker } from './folder-picker.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':
|
|
@@ -202,6 +207,25 @@ function connect() {
|
|
|
202
207
|
case 'plugins':
|
|
203
208
|
loadPlugins(msg.list);
|
|
204
209
|
break;
|
|
210
|
+
case 'pills':
|
|
211
|
+
state.pills.clear();
|
|
212
|
+
for (const p of msg.list) addPill(p);
|
|
213
|
+
break;
|
|
214
|
+
case 'pill.added':
|
|
215
|
+
addPill(msg.pill);
|
|
216
|
+
break;
|
|
217
|
+
case 'pill.updated':
|
|
218
|
+
updatePill(msg.pill);
|
|
219
|
+
break;
|
|
220
|
+
case 'pill.removed':
|
|
221
|
+
removePill(msg.id);
|
|
222
|
+
break;
|
|
223
|
+
case 'pill.log':
|
|
224
|
+
appendPillLog(msg.id, msg.entry);
|
|
225
|
+
break;
|
|
226
|
+
case 'pill.logs':
|
|
227
|
+
setPillLogs(msg.id, msg.logs);
|
|
228
|
+
break;
|
|
205
229
|
case 'plugin.delete.error':
|
|
206
230
|
showToast(`Failed to remove plugin: ${msg.error}`, { duration: 4000 });
|
|
207
231
|
break;
|
|
@@ -247,6 +271,7 @@ mobileQuery.addEventListener('change', (e) => { if (!e.matches) closeMobileSideb
|
|
|
247
271
|
|
|
248
272
|
// Sidebar events
|
|
249
273
|
const sessionList = document.getElementById('session-list');
|
|
274
|
+
sessionList.addEventListener('projects-rendered', () => renderProjectActions());
|
|
250
275
|
|
|
251
276
|
sessionList.addEventListener('click', (e) => {
|
|
252
277
|
closeCreator();
|
|
@@ -254,6 +279,7 @@ sessionList.addEventListener('click', (e) => {
|
|
|
254
279
|
|
|
255
280
|
// Project header click — toggle collapse (skip if just finished a drag)
|
|
256
281
|
const projHeader = e.target.closest('.project-header');
|
|
282
|
+
if (e.target.closest('.plugin-project-btn')) return; // handled by btn's own click listener
|
|
257
283
|
if (projHeader && !e.target.closest('.project-menu-btn') && !wasDragging()) {
|
|
258
284
|
toggleProjectCollapse(projHeader.dataset.projectId);
|
|
259
285
|
return;
|
|
@@ -279,6 +305,9 @@ sessionList.addEventListener('click', (e) => {
|
|
|
279
305
|
return;
|
|
280
306
|
}
|
|
281
307
|
|
|
308
|
+
// Pill row click — handled by pill's own listener
|
|
309
|
+
if (e.target.closest('.pill-row')) return;
|
|
310
|
+
|
|
282
311
|
const item = e.target.closest('.group');
|
|
283
312
|
if (!item) return;
|
|
284
313
|
|
|
@@ -720,7 +749,7 @@ function renderPluginsPanel(list) {
|
|
|
720
749
|
</div>
|
|
721
750
|
<div class="plugin-body ${open ? '' : 'hidden'}">
|
|
722
751
|
<div class="px-4 pb-3">
|
|
723
|
-
${(p.settings || []).map(s => renderSettingField(p.id, s, p.settingValues[s.key] ?? s.default)).join('')}
|
|
752
|
+
${(p.settings || []).map(s => renderSettingField(p.id, s, p.settingValues[s.key] ?? s.default, p.dynamicOptions)).join('')}
|
|
724
753
|
</div>
|
|
725
754
|
</div>
|
|
726
755
|
</div>`;
|
|
@@ -756,11 +785,11 @@ function renderPluginsPanel(list) {
|
|
|
756
785
|
if (el.type === 'checkbox') el.addEventListener('change', () => onChange(el.checked));
|
|
757
786
|
else if (el.tagName === 'SELECT') el.addEventListener('change', () => onChange(el.value));
|
|
758
787
|
else if (el.type === 'number') el.addEventListener('change', () => onChange(Number(el.value)));
|
|
759
|
-
else el.addEventListener('
|
|
788
|
+
else el.addEventListener('change', () => onChange(el.value));
|
|
760
789
|
});
|
|
761
790
|
}
|
|
762
791
|
|
|
763
|
-
function renderSettingField(pluginId, setting, value) {
|
|
792
|
+
function renderSettingField(pluginId, setting, value, dynamicOptions) {
|
|
764
793
|
const id = `ps-${pluginId}-${setting.key}`;
|
|
765
794
|
const attrs = `data-plugin="${esc(pluginId)}" data-setting="${esc(setting.key)}"`;
|
|
766
795
|
const label = esc(setting.label || setting.key);
|
|
@@ -772,12 +801,17 @@ function renderSettingField(pluginId, setting, value) {
|
|
|
772
801
|
<span class="text-xs text-slate-400">${label}</span>
|
|
773
802
|
</label>${desc}`;
|
|
774
803
|
}
|
|
775
|
-
if (setting.type === 'select') {
|
|
776
|
-
const
|
|
804
|
+
if (setting.type === 'select' || setting.type === 'dynamic-select') {
|
|
805
|
+
const source = setting.type === 'dynamic-select' ? (dynamicOptions?.[setting.key] || []) : (setting.options || []);
|
|
806
|
+
let opts = source.map(o => {
|
|
777
807
|
const optVal = typeof o === 'object' ? o.value : o;
|
|
778
808
|
const optLabel = typeof o === 'object' ? o.label : o;
|
|
779
809
|
return `<option value="${esc(String(optVal))}" ${String(value) === String(optVal) ? 'selected' : ''}>${esc(String(optLabel))}</option>`;
|
|
780
810
|
}).join('');
|
|
811
|
+
// Dynamic-select with no options yet: show the saved value so the control isn't blank
|
|
812
|
+
if (setting.type === 'dynamic-select' && !source.length && value) {
|
|
813
|
+
opts = `<option value="${esc(String(value))}" selected>${esc(String(value))}</option>`;
|
|
814
|
+
}
|
|
781
815
|
return `<div class="mt-2">
|
|
782
816
|
<label class="block text-xs text-slate-400 mb-1">${label}</label>
|
|
783
817
|
<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 +851,15 @@ async function loadPlugins(list) {
|
|
|
817
851
|
|
|
818
852
|
renderPluginsPanel(list);
|
|
819
853
|
|
|
854
|
+
// Store project-header actions from plugins (used by regroupSessions to render icons)
|
|
855
|
+
state.projectActions = [];
|
|
856
|
+
for (const plugin of list) {
|
|
857
|
+
for (const action of plugin.actions || []) {
|
|
858
|
+
if (action.slot === 'project-header') state.projectActions.push({ ...action, pluginId: plugin.id });
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
renderProjectActions();
|
|
862
|
+
|
|
820
863
|
// Render server-registered toolbar actions — also clears stale client toolbar buttons
|
|
821
864
|
const toolbar = document.getElementById('plugin-toolbar');
|
|
822
865
|
toolbar.querySelectorAll('.plugin-btn').forEach(b => {
|
|
@@ -863,6 +906,30 @@ async function loadPlugins(list) {
|
|
|
863
906
|
}
|
|
864
907
|
}
|
|
865
908
|
|
|
909
|
+
// Render plugin-registered project header action buttons into all project groups
|
|
910
|
+
function renderProjectActions() {
|
|
911
|
+
const actions = state.projectActions || [];
|
|
912
|
+
for (const slot of document.querySelectorAll('.project-plugin-actions')) {
|
|
913
|
+
slot.innerHTML = '';
|
|
914
|
+
const projId = slot.closest('.project-header')?.dataset.projectId;
|
|
915
|
+
if (!projId) continue;
|
|
916
|
+
for (const action of actions) {
|
|
917
|
+
const btn = document.createElement('button');
|
|
918
|
+
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';
|
|
919
|
+
btn.title = action.title || '';
|
|
920
|
+
btn.innerHTML = action.icon || '';
|
|
921
|
+
btn.dataset.pluginId = action.pluginId;
|
|
922
|
+
btn.dataset.actionId = action.id;
|
|
923
|
+
btn.dataset.projectId = projId;
|
|
924
|
+
btn.addEventListener('click', (e) => {
|
|
925
|
+
e.stopPropagation();
|
|
926
|
+
send({ type: `plugin.${action.pluginId}.${action.id}`, action: action.id, projectId: projId });
|
|
927
|
+
});
|
|
928
|
+
slot.appendChild(btn);
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
|
|
866
933
|
let saveTimer = null;
|
|
867
934
|
function flashSaveIndicator() {
|
|
868
935
|
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>
|
|
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>
|
|
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.textContent = proj ? proj.name : '
|
|
202
|
+
label.textContent = proj ? proj.name : 'Select project';
|
|
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.textContent = roleName || 'Select role';
|
|
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
|
}
|
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>
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
// Roles panel — manage worker role definitions (core feature, not plugin-specific)
|
|
2
|
+
import { state, send } from './state.js';
|
|
3
|
+
import { esc } from './utils.js';
|
|
4
|
+
|
|
5
|
+
const panel = document.getElementById('panel-roles');
|
|
6
|
+
|
|
7
|
+
function getRoles() { return state.cfg.roles || []; }
|
|
8
|
+
|
|
9
|
+
function save() { send({ type: 'config.update', config: state.cfg }); }
|
|
10
|
+
|
|
11
|
+
export function renderRoles() {
|
|
12
|
+
const roles = getRoles();
|
|
13
|
+
panel.innerHTML = `
|
|
14
|
+
<div class="flex items-center justify-between px-3 pt-3 pb-2">
|
|
15
|
+
<span class="text-sm font-bold text-slate-200 tracking-tight" style="font-family:'JetBrains Mono',monospace">Roles</span>
|
|
16
|
+
<button id="btn-add-role" 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 role">+</button>
|
|
17
|
+
</div>
|
|
18
|
+
<div id="roles-list" class="tmx-scroll flex-1 overflow-y-auto border-t border-slate-700/50"></div>`;
|
|
19
|
+
|
|
20
|
+
panel.querySelector('#btn-add-role').addEventListener('click', () => openEditor());
|
|
21
|
+
|
|
22
|
+
const list = panel.querySelector('#roles-list');
|
|
23
|
+
list.addEventListener('click', (e) => {
|
|
24
|
+
if (e.target.closest('.role-edit')) {
|
|
25
|
+
const idx = +e.target.closest('.role-row').dataset.idx;
|
|
26
|
+
openEditor(idx);
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
if (e.target.closest('.role-del')) {
|
|
30
|
+
const idx = +e.target.closest('.role-row').dataset.idx;
|
|
31
|
+
state.cfg.roles.splice(idx, 1);
|
|
32
|
+
save();
|
|
33
|
+
renderRoles();
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
renderRoleList(roles);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function renderRoleList(roles) {
|
|
42
|
+
const list = panel.querySelector('#roles-list');
|
|
43
|
+
if (!roles.length) {
|
|
44
|
+
list.innerHTML = `<div class="flex flex-col items-center justify-center h-full px-6 text-center">
|
|
45
|
+
<p class="text-sm text-slate-400 mb-1">No roles defined</p>
|
|
46
|
+
<p class="text-xs text-slate-600 leading-relaxed">Define agent identities with a name and instructions.<br>Roles are sent to the agent when a session starts<br>and can be used by plugins like Autopilot.</p>
|
|
47
|
+
</div>`;
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
list.innerHTML = roles.map((r, idx) => `
|
|
51
|
+
<div class="role-row group flex items-start gap-2 px-3 py-2.5 cursor-default hover:bg-slate-800/40 transition-colors ${idx > 0 ? 'border-t border-slate-700/30' : ''}" data-idx="${idx}">
|
|
52
|
+
<div class="flex-1 min-w-0">
|
|
53
|
+
<div class="text-[13px] font-medium text-slate-200 truncate">${esc(r.name)}</div>
|
|
54
|
+
<div class="text-[11px] text-slate-500 mt-0.5 line-clamp-2 leading-relaxed">${esc(r.instructions)}</div>
|
|
55
|
+
</div>
|
|
56
|
+
<div class="flex items-center gap-0.5 opacity-0 group-hover:opacity-100 transition-opacity flex-shrink-0 mt-0.5">
|
|
57
|
+
<button class="role-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">
|
|
58
|
+
<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>
|
|
59
|
+
</button>
|
|
60
|
+
<button class="role-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">
|
|
61
|
+
<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>
|
|
62
|
+
</button>
|
|
63
|
+
</div>
|
|
64
|
+
</div>
|
|
65
|
+
`).join('');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function closeEditor() { document.getElementById('role-editor')?.remove(); }
|
|
69
|
+
|
|
70
|
+
function openEditor(idx) {
|
|
71
|
+
if (document.getElementById('role-editor')) { closeEditor(); if (idx == null) return; }
|
|
72
|
+
const existing = idx != null ? getRoles()[idx] : null;
|
|
73
|
+
|
|
74
|
+
const card = document.createElement('div');
|
|
75
|
+
card.id = 'role-editor';
|
|
76
|
+
card.className = 'p-3 border-b border-slate-700/50 bg-slate-800/30';
|
|
77
|
+
card.innerHTML = `
|
|
78
|
+
<input id="re-name" type="text" maxlength="40" placeholder="Role name" value="${esc(existing?.name || '')}"
|
|
79
|
+
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">
|
|
80
|
+
<textarea id="re-instructions" rows="4" placeholder="Who am I? e.g. You are a senior software architect. You break down goals into clear, actionable tasks with acceptance criteria."
|
|
81
|
+
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?.instructions || '')}</textarea>
|
|
82
|
+
<div class="flex items-center gap-2">
|
|
83
|
+
<button id="re-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>
|
|
84
|
+
<button id="re-cancel" class="px-3 py-1.5 text-xs text-slate-500 hover:text-slate-300 transition-colors">Cancel</button>
|
|
85
|
+
</div>`;
|
|
86
|
+
|
|
87
|
+
const list = panel.querySelector('#roles-list');
|
|
88
|
+
list.parentElement.insertBefore(card, list);
|
|
89
|
+
|
|
90
|
+
const nameEl = card.querySelector('#re-name');
|
|
91
|
+
const instrEl = card.querySelector('#re-instructions');
|
|
92
|
+
nameEl.focus();
|
|
93
|
+
|
|
94
|
+
const doSave = () => {
|
|
95
|
+
const name = nameEl.value.trim();
|
|
96
|
+
const instructions = instrEl.value.trim();
|
|
97
|
+
if (!name || !instructions) return;
|
|
98
|
+
if (!state.cfg.roles) state.cfg.roles = [];
|
|
99
|
+
if (idx != null) {
|
|
100
|
+
state.cfg.roles[idx] = { ...state.cfg.roles[idx], name, instructions };
|
|
101
|
+
} else {
|
|
102
|
+
state.cfg.roles.push({ id: crypto.randomUUID(), name, instructions });
|
|
103
|
+
}
|
|
104
|
+
save();
|
|
105
|
+
closeEditor();
|
|
106
|
+
renderRoles();
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
card.querySelector('#re-save').addEventListener('click', doSave);
|
|
110
|
+
card.querySelector('#re-cancel').addEventListener('click', closeEditor);
|
|
111
|
+
nameEl.addEventListener('keydown', (e) => { if (e.key === 'Escape') closeEditor(); });
|
|
112
|
+
}
|