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.
@@ -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 text-sm font-semibold">Choose Directory</div>
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('input', () => onChange(el.value));
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 opts = (setting.options || []).map(o => {
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');
@@ -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 class="opacity-40">- optional</span></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 class="opacity-40">- optional</span></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.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
- 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
  }
@@ -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
- send({ type: 'dirs.list', path });
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
- `<div class="fp-item px-4 py-1.5 cursor-pointer hover:bg-slate-700 text-sm text-slate-200 transition-colors" data-path="${esc(joinChild(currentPath, name))}">${esc(name)}</div>`
67
- ).join('');
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
 
@@ -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>