clideck 1.30.2 → 1.30.4

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.
@@ -1,7 +1,7 @@
1
1
  You are an autonomous dispatcher for project: {{projectName}}.
2
2
 
3
3
  YOUR ROLE
4
- You control workflow routing between agents.
4
+ You control workflow routing between agents to accomplish the session goal.
5
5
  You do not do the work yourself.
6
6
  You do not rewrite agent output.
7
7
  You do not send summaries, edits, or instructions of your own to agents.
@@ -9,23 +9,21 @@ The system forwards existing agent output verbatim. Your job is to choose the be
9
9
 
10
10
  IMPORTANT
11
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.
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
13
+ the best next routing move.
14
+ You don't stop steering between agents until the seassion goal is complete or truly blocked. Do not stop to ask the user what to do next, just keep routing between agents until there is absolutely no way to proceed without user input, or until the task is complete.
13
15
 
14
16
  This means:
15
17
  - You should understand what the project is trying to achieve.
16
18
  - You should understand what each agent just produced.
17
19
  - You should understand what kind of specialist should act next.
18
20
  - 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
+ - Example: if a creative output needs analytical grounding, the right next move may be to route it to an analyst before routing it back
22
+ to the creative.
21
23
 
22
24
  AGENTS
23
25
  {{agents}}
24
26
 
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
27
 
30
28
  STATE
31
29
  You will receive structured workflow state describing:
@@ -38,26 +36,22 @@ You will receive structured workflow state describing:
38
36
 
39
37
  TOOLS
40
38
  - 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.
39
+ - notify_user(reason): Stop autopilot and notify the user. Use light markdown: **bold** for key terms, `code` for file/function names,
40
+ bullet lists for summaries. Keep it concise (2-5 sentences). Use ONLY when the work is naturally complete, truly blocked, or requires
41
+ human input.
42
42
 
43
- RULES
43
+ When calling `route(from, to)`, use the displayed agent labels exactly as shown in the AGENTS section and workflow state.
44
+
45
+ STEERING RULES
44
46
  - Call exactly ONE tool per response.
45
47
  - Read the workflow state first, then read the agent outputs.
46
48
  - Prefer routing new output over previously routed output.
47
49
  - 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
+ - Use inferred responsibilities when choosing the next receiver.
51
+ - Do not route to an agent whose inferred role makes the handoff inappropriate.
50
52
  - Do not invent new instructions for agents. You only choose who receives whose output.
51
53
 
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
- - Do NOT alert the user that some agent asking for the user input before proceeding, this is not an execuse to stop and ask the user what to do. You should route the work to the next best agent until the workflow is truly blocked and cannot proceed without user input. (e.g. if the programmer ask for the user input, first make sure the reivewer or QA agent has not already reviewed the code, if not route it to them first)
55
- - The user may be away from the computer and expects the agents to keep working until the task is naturally complete.
56
- - 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.
57
- - Repeat agents with the same output if needed, unless the routing state shows that the same handoff is being repeated without progress.
58
- - You steer between agents until the task is complete or the user interrupts you, period.
59
-
60
- HOW TO THINK
54
+ STEERING DECISION-MAKING
61
55
  For each decision, reason in this order:
62
56
  1. What is the project trying to achieve right now?
63
57
  2. What changed most recently?
@@ -65,5 +59,13 @@ For each decision, reason in this order:
65
59
  4. Has this output already been consumed by that role?
66
60
  5. Is there a better intermediate handoff before sending it to the most obvious role?
67
61
 
62
+ DO NOT USE notify_user UNLESS ABSOLUTELY NECESSARY
63
+ - Do NOT ask the user if you should continue. Because you MUST continue until the task is complete or truly blocked, asking the user if you should continue is redundant, disruptive and a signed you failed to steer the workflow effectively.
64
+ - Even if the agent ask for the user input, Do NOT alert the user unless you are 100% sure the workflow is truly blocked and cannot proceed without user input. Instead, continue steering between agents.
65
+ - The user may be away from the computer and expects the agents to keep working until the task is naturally complete. stopping to ask for user input when the workflow can continue is disruptive and frustrating for the user.
66
+ - You are autonomous. If you are unsure how to proceed, re-read the workflow state and the latest agent outputs, think differently, and
67
+ route again.
68
+ - One last time, to eliminate mistakes: You steer between agents until the task is complete or the user interrupts you, period.
69
+
68
70
  GOAL
69
71
  Keep the work moving until the task is complete or truly blocked, by routing each output to the most appropriate next agent.
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
@@ -0,0 +1,25 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" fill="none">
2
+ <title>Clideck Agent</title>
3
+ <defs>
4
+ <linearGradient id="bubbleGradient" x1="64" y1="72" x2="448" y2="408" gradientUnits="userSpaceOnUse">
5
+ <stop offset="0" stop-color="#2C2F38"/>
6
+ <stop offset="1" stop-color="#171A20"/>
7
+ </linearGradient>
8
+ <linearGradient id="glyphGradient" x1="122" y1="150" x2="395" y2="355" gradientUnits="userSpaceOnUse">
9
+ <stop offset="0" stop-color="#FFFFFF"/>
10
+ <stop offset="1" stop-color="#E7EAF0"/>
11
+ </linearGradient>
12
+ <filter id="glyphShadow" x="82" y="126" width="350" height="254" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
13
+ <feOffset dx="0" dy="10"/>
14
+ <feGaussianBlur stdDeviation="10"/>
15
+ <feColorMatrix type="matrix" values="0 0 0 0 0.03 0 0 0 0 0.04 0 0 0 0 0.07 0 0 0 0.35 0"/>
16
+ <feBlend in2="SourceGraphic" result="shape"/>
17
+ </filter>
18
+ </defs>
19
+ <path d="M120 70H392C435.078 70 470 104.922 470 148V266C470 309.078 435.078 344 392 344H215.642L118.159 416.774C111.875 421.467 102.86 417.2 102.562 409.363L99.996 344H88C52.654 344 24 315.346 24 280V166C24 112.98 66.98 70 120 70Z" fill="url(#bubbleGradient)"/>
20
+ <path d="M120 70H392C435.078 70 470 104.922 470 148V266C470 309.078 435.078 344 392 344H215.642L118.159 416.774C111.875 421.467 102.86 417.2 102.562 409.363L99.996 344H88C52.654 344 24 315.346 24 280V166C24 112.98 66.98 70 120 70Z" stroke="#3E4350" stroke-width="2"/>
21
+ <g filter="url(#glyphShadow)" stroke="url(#glyphGradient)" stroke-linecap="round" stroke-linejoin="round">
22
+ <path d="M134 168L245 256L134 344" stroke-width="40"/>
23
+ <path d="M292 344H390" stroke-width="38"/>
24
+ </g>
25
+ </svg>
@@ -0,0 +1,25 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" fill="none">
2
+ <title>Clideck Agent</title>
3
+ <defs>
4
+ <linearGradient id="bubbleGradient" x1="64" y1="72" x2="448" y2="408" gradientUnits="userSpaceOnUse">
5
+ <stop offset="0" stop-color="#F7F8FB"/>
6
+ <stop offset="1" stop-color="#E5E9F1"/>
7
+ </linearGradient>
8
+ <linearGradient id="glyphGradient" x1="122" y1="150" x2="395" y2="355" gradientUnits="userSpaceOnUse">
9
+ <stop offset="0" stop-color="#1D212B"/>
10
+ <stop offset="1" stop-color="#3D4656"/>
11
+ </linearGradient>
12
+ <filter id="glyphShadow" x="82" y="126" width="350" height="254" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
13
+ <feOffset dx="0" dy="6"/>
14
+ <feGaussianBlur stdDeviation="7"/>
15
+ <feColorMatrix type="matrix" values="0 0 0 0 0.47 0 0 0 0 0.53 0 0 0 0 0.63 0 0 0 0.18 0"/>
16
+ <feBlend in2="SourceGraphic" result="shape"/>
17
+ </filter>
18
+ </defs>
19
+ <path d="M120 70H392C435.078 70 470 104.922 470 148V266C470 309.078 435.078 344 392 344H215.642L118.159 416.774C111.875 421.467 102.86 417.2 102.562 409.363L99.996 344H88C52.654 344 24 315.346 24 280V166C24 112.98 66.98 70 120 70Z" fill="url(#bubbleGradient)"/>
20
+ <path d="M120 70H392C435.078 70 470 104.922 470 148V266C470 309.078 435.078 344 392 344H215.642L118.159 416.774C111.875 421.467 102.86 417.2 102.562 409.363L99.996 344H88C52.654 344 24 315.346 24 280V166C24 112.98 66.98 70 120 70Z" stroke="#C6CEDA" stroke-width="2"/>
21
+ <g filter="url(#glyphShadow)" stroke="url(#glyphGradient)" stroke-linecap="round" stroke-linejoin="round">
22
+ <path d="M134 168L245 256L134 344" stroke-width="40"/>
23
+ <path d="M292 344H390" stroke-width="38"/>
24
+ </g>
25
+ </svg>
package/public/index.html CHANGED
@@ -147,9 +147,6 @@
147
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">
148
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>
149
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>
153
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="plugins" title="Plugins">
154
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="M12 2v4m0 12v4M2 12h4m12 0h4"/><circle cx="12" cy="12" r="3"/><path d="M12 8V6m0 12v-2M8 12H6m12 0h-2"/></svg>
155
152
  </button>
@@ -200,8 +197,6 @@
200
197
  </div>
201
198
  <!-- Prompts panel (rendered by prompts.js) -->
202
199
  <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>
205
200
  <!-- Plugins panel -->
206
201
  <div id="panel-plugins" class="hidden flex-col flex-1 min-h-0">
207
202
  <div class="flex items-center px-4 py-3 border-b border-slate-700/50">
package/public/js/app.js CHANGED
@@ -12,7 +12,6 @@ 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';
16
15
 
17
16
  const shownAgentHealthToasts = new Set();
18
17
  let reconnectReplaySkip = null;
@@ -34,7 +33,6 @@ function connect() {
34
33
  regroupSessions();
35
34
  renderSettings();
36
35
  renderPrompts();
37
- renderRoles();
38
36
  refreshCreator();
39
37
  for (const [, entry] of state.terms) applyTheme(entry.term, entry.themeId);
40
38
  break;
@@ -682,7 +680,7 @@ function openProjectCreator() {
682
680
  card.id = 'project-creator';
683
681
  card.className = 'p-3 border-b border-slate-700/50 bg-slate-800/30';
684
682
  card.innerHTML = `
685
- <div class="text-[10px] font-semibold uppercase tracking-wider text-slate-500 mb-2">New Project</div>
683
+ <div class="text-[10px] font-semibold uppercase tracking-wider text-slate-500 mb-1.5">Project folder</div>
686
684
  <div class="flex items-center gap-1.5 mb-2">
687
685
  <input id="pc-path" type="text" value="${esc(defaultPath)}" placeholder="Project folder path"
688
686
  class="flex-1 px-3 py-1.5 text-xs bg-slate-900 border border-slate-700 rounded-md text-slate-400 placeholder-slate-600 outline-none focus:border-blue-500 transition-colors font-mono">
@@ -690,6 +688,9 @@ function openProjectCreator() {
690
688
  ${FOLDER_SVG}
691
689
  </button>
692
690
  </div>
691
+ <div class="text-[10px] font-semibold uppercase tracking-wider text-slate-500 mb-1.5">
692
+ Project name <span class="text-slate-600 font-medium normal-case tracking-normal">(auto-filled from folder name)</span>
693
+ </div>
693
694
  <input id="pc-name" type="text" maxlength="35" placeholder="Project name"
694
695
  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">
695
696
  <div class="flex items-center gap-2">
@@ -1016,7 +1017,7 @@ function renderProjectActions() {
1016
1017
  if (!projId) continue;
1017
1018
  for (const action of actions) {
1018
1019
  const btn = document.createElement('button');
1019
- 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';
1020
+ btn.className = 'project-plugin-action plugin-project-btn text-slate-600 hover:text-indigo-400 flex-shrink-0 p-0.5';
1020
1021
  btn.title = action.title || '';
1021
1022
  btn.innerHTML = action.icon || '';
1022
1023
  btn.dataset.pluginId = action.pluginId;
@@ -13,6 +13,7 @@ const ANIMALS = [
13
13
  'Dolphin', 'Lynx', 'Hawk', 'Raven', 'Otter', 'Panther', 'Crane', 'Bison',
14
14
  ];
15
15
  const MRU_KEY = 'termui-last-preset';
16
+ const NO_PROJECT_VALUE = '__none__';
16
17
  const FOLDER_SVG = `<svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg>`;
17
18
 
18
19
  function randomName() {
@@ -26,6 +27,11 @@ function findCommandForPreset(p) {
26
27
  || state.cfg.commands.find(c => binName(c.command) === binName(p.command));
27
28
  }
28
29
 
30
+ function telemetryEnabledForPreset(preset, existing) {
31
+ if (preset?.telemetryEnabled === true) return true;
32
+ return !!existing?.telemetryEnabled;
33
+ }
34
+
29
35
  // True if preset binary is missing and the configured command is unchanged from the preset default
30
36
  function isPresetMissing(p) {
31
37
  if (p.available !== false) return false;
@@ -96,7 +102,7 @@ function sortedPresets() {
96
102
  return [...agents, ...shell];
97
103
  }
98
104
 
99
- function createFromPreset(preset, sessionName, cwd, projectId, roleId) {
105
+ function createFromPreset(preset, sessionName, cwd, projectId) {
100
106
  // Find existing command matching this preset
101
107
  let cmd = findCommandForPreset(preset);
102
108
  // Auto-create the command if it doesn't exist yet
@@ -114,14 +120,14 @@ function createFromPreset(preset, sessionName, cwd, projectId, roleId) {
114
120
  resumeCommand: preset.resumeCommand,
115
121
  sessionIdPattern: preset.sessionIdPattern,
116
122
  outputMarker: preset.outputMarker || null,
117
- telemetryEnabled: false,
123
+ telemetryEnabled: telemetryEnabledForPreset(preset),
118
124
  telemetryStatus: null,
119
125
  bridge: preset.bridge,
120
126
  };
121
127
  state.cfg.commands.push(cmd);
122
128
  send({ type: 'config.update', config: state.cfg });
123
129
  }
124
- send({ type: 'create', commandId: cmd.id, name: sessionName, cwd, projectId: projectId || undefined, roleId: roleId || undefined, ...estimateSize() });
130
+ send({ type: 'create', commandId: cmd.id, name: sessionName, cwd, projectId: projectId || undefined, ...estimateSize() });
125
131
  localStorage.setItem(MRU_KEY, preset.presetId);
126
132
  }
127
133
 
@@ -144,26 +150,23 @@ export function openCreator() {
144
150
  card.className = 'p-3 border-b border-slate-700/50 bg-slate-800/30';
145
151
  card.innerHTML = `
146
152
  ${(state.cfg.projects?.length) ? `
153
+ <div class="text-[10px] font-semibold uppercase tracking-wider text-slate-500 mb-1.5">Select project</div>
147
154
  <input type="hidden" id="creator-project" value="">
148
155
  <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">
149
- <span id="creator-project-label">Select project <span class="opacity-40">- optional</span></span>
156
+ <span id="creator-project-label">Select project</span>
150
157
  <span class="text-slate-600 ml-2">&#9662;</span>
151
158
  </button>` : ''}
152
- ${(state.cfg.roles?.length) ? `
153
- <input type="hidden" id="creator-role" value="">
154
- <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">
155
- <span id="creator-role-label">Select role <span class="opacity-40">- optional</span></span>
156
- <span class="text-slate-600 ml-2">&#9662;</span>
157
- </button>` : ''}
158
- <input id="creator-name" type="text" maxlength="35" placeholder="Session / Agent name"
159
+ <div class="text-[10px] font-semibold uppercase tracking-wider text-slate-500 mb-1.5">Session name</div>
160
+ <input id="creator-name" type="text" maxlength="35" placeholder="Session name"
159
161
  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">
160
- <div class="flex items-center gap-1.5 mb-2">
162
+ <div id="creator-cwd-wrap" class="flex items-center gap-1.5 mb-2 ${(state.cfg.projects?.length) ? 'hidden' : ''}">
161
163
  <input id="creator-cwd" type="text" value="${esc(defaultPath)}" placeholder="Working directory"
162
164
  class="flex-1 px-3 py-1.5 text-xs bg-slate-900 border border-slate-700 rounded-md text-slate-400 placeholder-slate-600 outline-none focus:border-blue-500 transition-colors font-mono">
163
165
  <button id="creator-browse" class="flex-shrink-0 w-7 h-7 flex items-center justify-center rounded-md border border-slate-700 text-slate-500 hover:text-slate-300 hover:bg-slate-700 transition-colors" title="Browse">
164
166
  ${FOLDER_SVG}
165
167
  </button>
166
168
  </div>
169
+ <div class="text-[10px] font-semibold uppercase tracking-wider text-slate-500 mb-1.5">Choose agent provider</div>
167
170
  <div id="creator-presets" class="space-y-0.5">
168
171
  ${renderPresetButtons()}
169
172
  </div>`;
@@ -173,7 +176,10 @@ export function openCreator() {
173
176
 
174
177
  const nameInput = card.querySelector('#creator-name');
175
178
  const cwdInput = card.querySelector('#creator-cwd');
176
- nameInput.focus();
179
+ const cwdWrap = card.querySelector('#creator-cwd-wrap');
180
+ const projHidden = card.querySelector('#creator-project');
181
+ const projTrigger = card.querySelector('#creator-project-trigger');
182
+ (projTrigger || nameInput).focus();
177
183
 
178
184
  nameInput.addEventListener('keydown', (e) => {
179
185
  if (e.key === 'Escape') closeCreator();
@@ -189,15 +195,33 @@ export function openCreator() {
189
195
  });
190
196
 
191
197
  // Project picker dropdown
192
- const projTrigger = card.querySelector('#creator-project-trigger');
193
198
  if (projTrigger) {
199
+ const projects = [...(state.cfg.projects || [])].sort((a, b) => a.name.localeCompare(b.name, undefined, { sensitivity: 'base' }));
200
+ const projLabel = card.querySelector('#creator-project-label');
201
+ const setProjectSelection = (value) => {
202
+ projHidden.value = value;
203
+ const proj = projects.find(p => p.id === value);
204
+ if (proj) {
205
+ projLabel.textContent = proj.name;
206
+ cwdWrap.classList.add('hidden');
207
+ cwdInput.value = proj.path || defaultPath;
208
+ return;
209
+ }
210
+ if (value === NO_PROJECT_VALUE) {
211
+ projLabel.textContent = 'None (outside project hierarchy)';
212
+ cwdWrap.classList.remove('hidden');
213
+ cwdInput.value = cwdInput.value.trim() || defaultPath;
214
+ return;
215
+ }
216
+ projLabel.textContent = 'Select project';
217
+ cwdWrap.classList.add('hidden');
218
+ cwdInput.value = defaultPath;
219
+ };
220
+
194
221
  let projMenuCleanup = null;
195
222
  projTrigger.addEventListener('click', () => {
196
223
  if (projMenuCleanup) { projMenuCleanup(); return; }
197
224
  const rect = projTrigger.getBoundingClientRect();
198
- const hidden = card.querySelector('#creator-project');
199
- const label = card.querySelector('#creator-project-label');
200
- const projects = state.cfg.projects || [];
201
225
 
202
226
  const menu = document.createElement('div');
203
227
  menu.className = 'fixed z-[500] bg-slate-800 border border-slate-600 rounded-lg shadow-xl shadow-black/40 py-1 overflow-y-auto';
@@ -207,9 +231,9 @@ export function openCreator() {
207
231
  menu.style.width = rect.width + 'px';
208
232
 
209
233
  menu.innerHTML = `
210
- <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>
234
+ <div class="proj-option px-3 py-1.5 cursor-pointer hover:bg-slate-700 transition-colors text-xs text-slate-400 ${projHidden.value === NO_PROJECT_VALUE ? 'bg-slate-700/50' : ''}" data-value="${NO_PROJECT_VALUE}">None (outside project hierarchy)</div>
211
235
  ${projects.map(p => `
212
- <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}">
236
+ <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 ${projHidden.value === p.id ? 'bg-slate-700/50' : ''}" data-value="${p.id}">
213
237
  <span class="w-2 h-2 rounded-full flex-shrink-0" style="background:${p.color || '#3b82f6'}"></span>
214
238
  ${esc(p.name)}
215
239
  </div>`).join('')}`;
@@ -219,12 +243,7 @@ export function openCreator() {
219
243
  const onClick = (e) => {
220
244
  const item = e.target.closest('.proj-option');
221
245
  if (!item) return;
222
- hidden.value = item.dataset.value;
223
- const proj = projects.find(p => p.id === item.dataset.value);
224
- label.innerHTML = proj ? esc(proj.name) : 'Select project <span class="opacity-40">- optional</span>';
225
- // Auto-set working directory from project path
226
- if (proj?.path) cwdInput.value = proj.path;
227
- else cwdInput.value = defaultPath;
246
+ setProjectSelection(item.dataset.value);
228
247
  projMenuCleanup();
229
248
  };
230
249
  const onOutside = (e) => {
@@ -242,64 +261,6 @@ export function openCreator() {
242
261
  });
243
262
  }
244
263
 
245
- // Role picker dropdown
246
- const roleTrigger = card.querySelector('#creator-role-trigger');
247
- if (roleTrigger) {
248
- let roleMenuCleanup = null;
249
- roleTrigger.addEventListener('click', () => {
250
- if (roleMenuCleanup) { roleMenuCleanup(); return; }
251
- const rect = roleTrigger.getBoundingClientRect();
252
- const hidden = card.querySelector('#creator-role');
253
- const label = card.querySelector('#creator-role-label');
254
- const roles = state.cfg.roles || [];
255
-
256
- const menu = document.createElement('div');
257
- menu.className = 'fixed z-[500] bg-slate-800 border border-slate-600 rounded-lg shadow-xl shadow-black/40 py-1 overflow-y-auto';
258
- menu.style.maxHeight = '200px';
259
- menu.style.left = rect.left + 'px';
260
- menu.style.top = (rect.bottom + 4) + 'px';
261
- menu.style.width = rect.width + 'px';
262
-
263
- menu.innerHTML = `
264
- <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>
265
- ${roles.map(r => `
266
- <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)}">
267
- ${esc(r.name)}
268
- </div>`).join('')}`;
269
-
270
- document.body.appendChild(menu);
271
-
272
- const onClick = (e) => {
273
- const item = e.target.closest('.role-option');
274
- if (!item) return;
275
- hidden.value = item.dataset.value;
276
- const roleName = item.dataset.name;
277
- label.innerHTML = roleName ? esc(roleName) : 'Select role <span class="opacity-40">- optional</span>';
278
- // Auto-fill session name from role name (only if user hasn't typed a custom name)
279
- if (roleName && (!nameInput.value.trim() || nameInput.dataset.autoFilled === '1')) {
280
- nameInput.value = roleName;
281
- nameInput.dataset.autoFilled = '1';
282
- }
283
- if (!item.dataset.value) nameInput.dataset.autoFilled = '';
284
- roleMenuCleanup();
285
- };
286
- const onOutside = (e) => {
287
- if (!menu.contains(e.target) && !roleTrigger.contains(e.target)) roleMenuCleanup();
288
- };
289
- menu.addEventListener('click', onClick);
290
- requestAnimationFrame(() => document.addEventListener('click', onOutside));
291
-
292
- roleMenuCleanup = () => {
293
- menu.removeEventListener('click', onClick);
294
- document.removeEventListener('click', onOutside);
295
- menu.remove();
296
- roleMenuCleanup = null;
297
- };
298
- });
299
- // Clear auto-fill flag when user manually types
300
- nameInput.addEventListener('input', () => { nameInput.dataset.autoFilled = ''; });
301
- }
302
-
303
264
  // "Add" button for missing agents — opens install toaster
304
265
  card.addEventListener('click', (e) => {
305
266
  const installBtn = e.target.closest('.install-btn');
@@ -314,7 +275,7 @@ export function openCreator() {
314
275
  if (!preset) return;
315
276
  let cmd = findCommandForPreset(preset);
316
277
  if (!cmd) {
317
- cmd = { id: crypto.randomUUID(), presetId: preset.presetId, label: preset.name, icon: preset.icon, command: preset.command, enabled: true, defaultPath: '', isAgent: preset.isAgent, canResume: preset.canResume, resumeCommand: preset.resumeCommand, sessionIdPattern: preset.sessionIdPattern, outputMarker: preset.outputMarker || null, telemetryEnabled: false, telemetryStatus: null, bridge: preset.bridge };
278
+ cmd = { id: crypto.randomUUID(), presetId: preset.presetId, label: preset.name, icon: preset.icon, command: preset.command, enabled: true, defaultPath: '', isAgent: preset.isAgent, canResume: preset.canResume, resumeCommand: preset.resumeCommand, sessionIdPattern: preset.sessionIdPattern, outputMarker: preset.outputMarker || null, telemetryEnabled: telemetryEnabledForPreset(preset), telemetryStatus: null, bridge: preset.bridge };
318
279
  state.cfg.commands.push(cmd);
319
280
  send({ type: 'config.update', config: state.cfg });
320
281
  }
@@ -325,13 +286,15 @@ export function openCreator() {
325
286
  if (!btn) return;
326
287
  const preset = state.presets.find(p => p.presetId === btn.dataset.preset);
327
288
  if (!preset) return;
289
+ if (projTrigger && !projHidden.value) {
290
+ showToast('Choose a project or select `None (outside project hierarchy)`.', { title: 'Choose Project', type: 'warn' });
291
+ projTrigger.focus();
292
+ return;
293
+ }
328
294
  const name = nameInput.value.trim() || fallbackName;
329
295
  const cwd = cwdInput.value.trim() || undefined;
330
- const projectSelect = card.querySelector('#creator-project');
331
- const projectId = projectSelect?.value || undefined;
332
- const roleSelect = card.querySelector('#creator-role');
333
- const roleId = roleSelect?.value || undefined;
334
- createFromPreset(preset, name, cwd, projectId, roleId);
296
+ const projectId = projHidden?.value && projHidden.value !== NO_PROJECT_VALUE ? projHidden.value : undefined;
297
+ createFromPreset(preset, name, cwd, projectId);
335
298
  closeCreator();
336
299
  });
337
300
  }
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', 'roles', 'plugins', 'settings'];
5
- const PANEL_TITLES = { chats: 'Sessions', prompts: 'Prompts', roles: 'Roles', plugins: 'Plugins', settings: 'Settings' };
4
+ const ALL_PANELS = ['chats', 'prompts', 'plugins', 'settings'];
5
+ const PANEL_TITLES = { chats: 'Sessions', prompts: 'Prompts', 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
 
@@ -24,15 +24,69 @@ document.getElementById('settings-nav').addEventListener('click', (e) => {
24
24
  if (btn) switchCategory(btn.dataset.cat);
25
25
  });
26
26
 
27
+ function captureSettingsFocus() {
28
+ const active = document.activeElement;
29
+ if (!active || !(active instanceof HTMLElement)) return null;
30
+ if (!active.closest('#panel-settings')) return null;
31
+
32
+ const snapshot = {
33
+ id: active.id || null,
34
+ selector: null,
35
+ cardIdx: null,
36
+ selectionStart: null,
37
+ selectionEnd: null,
38
+ };
39
+
40
+ const card = active.closest('.agent-card');
41
+ if (card) {
42
+ snapshot.cardIdx = card.dataset.idx || null;
43
+ for (const cls of ['agent-name', 'agent-command', 'agent-enabled', 'agent-is-agent', 'agent-can-resume', 'agent-resume-cmd']) {
44
+ if (active.classList.contains(cls)) {
45
+ snapshot.selector = `.${cls}`;
46
+ break;
47
+ }
48
+ }
49
+ }
50
+
51
+ if (typeof active.selectionStart === 'number' && typeof active.selectionEnd === 'number') {
52
+ snapshot.selectionStart = active.selectionStart;
53
+ snapshot.selectionEnd = active.selectionEnd;
54
+ }
55
+
56
+ return snapshot;
57
+ }
58
+
59
+ function restoreSettingsFocus(snapshot) {
60
+ if (!snapshot) return;
61
+
62
+ let target = null;
63
+ if (snapshot.id) target = document.getElementById(snapshot.id);
64
+ if (!target && snapshot.cardIdx != null && snapshot.selector) {
65
+ target = document.querySelector(`.agent-card[data-idx="${snapshot.cardIdx}"] ${snapshot.selector}`);
66
+ }
67
+ if (!(target instanceof HTMLElement)) return;
68
+
69
+ target.focus({ preventScroll: true });
70
+ if (
71
+ typeof snapshot.selectionStart === 'number' &&
72
+ typeof snapshot.selectionEnd === 'number' &&
73
+ typeof target.setSelectionRange === 'function'
74
+ ) {
75
+ target.setSelectionRange(snapshot.selectionStart, snapshot.selectionEnd);
76
+ }
77
+ }
78
+
27
79
  // ── Render all ──
28
80
 
29
81
  export function renderSettings() {
82
+ const focusSnapshot = captureSettingsFocus();
30
83
  document.getElementById('cfg-default-path').value = state.cfg.defaultPath || '';
31
84
  document.getElementById('cfg-confirm-close').checked = state.cfg.confirmClose !== false;
32
85
  renderAgentList();
33
86
  renderThemeSection();
34
87
  renderNotifications();
35
88
  updateVersionFooter();
89
+ restoreSettingsFocus(focusSnapshot);
36
90
  }
37
91
 
38
92
  export function updateVersionFooter() {
@@ -107,6 +161,22 @@ function telemetryPreset(cmd) {
107
161
  return (state.presets || []).find(p => binName(p.command) === bin);
108
162
  }
109
163
 
164
+ function presetForCommand(existing, command) {
165
+ const presets = state.presets || [];
166
+ if (existing?.presetId) {
167
+ const byId = presets.find(p => p.presetId === existing.presetId);
168
+ if (byId) return byId;
169
+ }
170
+ const bin = binName(command || existing?.command || '');
171
+ return presets.find(p => binName(p.command) === bin) || null;
172
+ }
173
+
174
+ function telemetryEnabledForCommand(existing, command) {
175
+ const preset = presetForCommand(existing, command);
176
+ if (preset?.telemetryEnabled === true) return true;
177
+ return !!existing?.telemetryEnabled;
178
+ }
179
+
110
180
  function integrationSection(c) {
111
181
  const preset = telemetryPreset(c);
112
182
  if (!preset) return '';
@@ -217,7 +287,7 @@ function openPresetMenu(anchorEl) {
217
287
  enabled: true, defaultPath: '', isAgent: p.isAgent, canResume: p.canResume,
218
288
  resumeCommand: p.resumeCommand, sessionIdPattern: p.sessionIdPattern,
219
289
  outputMarker: p.outputMarker || null,
220
- telemetryEnabled: false,
290
+ telemetryEnabled: telemetryEnabledForCommand({ presetId: p.presetId, command: p.command }, p.command),
221
291
  telemetryStatus: null,
222
292
  bridge: p.bridge,
223
293
  });
@@ -453,7 +523,7 @@ function saveConfig() {
453
523
  resumeCommand: card.querySelector('.agent-resume-cmd')?.value.trim() || null,
454
524
  sessionIdPattern: existing.sessionIdPattern || null,
455
525
  outputMarker: existing.outputMarker || null,
456
- telemetryEnabled: existing.telemetryEnabled || false,
526
+ telemetryEnabled: telemetryEnabledForCommand(existing, command),
457
527
  telemetryStatus: existing.telemetryStatus || null,
458
528
  bridge: existing.bridge,
459
529
  };