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.
@@ -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('input', () => onChange(el.value));
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 opts = (setting.options || []).map(o => {
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');
@@ -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">No project</span>
127
+ <span id="creator-project-label">Select project</span>
128
+ <span class="text-slate-600 ml-2">&#9662;</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">&#9662;</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="">No project</div>
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 : 'No project';
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
- createFromPreset(preset, name, cwd, projectId);
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
 
@@ -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-none leading-relaxed font-mono mb-2">${esc(existing?.text || '')}</textarea>
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
+ }
@@ -7,6 +7,8 @@ export const state = {
7
7
  presets: [],
8
8
  resumable: [],
9
9
  filter: { query: '', tab: 'all' },
10
+ pills: new Map(),
11
+ activePill: null,
10
12
  transcriptCache: {},
11
13
  remoteVersion: null,
12
14
  };