clideck 1.30.2 → 1.30.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.
@@ -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
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() {
@@ -96,7 +97,7 @@ function sortedPresets() {
96
97
  return [...agents, ...shell];
97
98
  }
98
99
 
99
- function createFromPreset(preset, sessionName, cwd, projectId, roleId) {
100
+ function createFromPreset(preset, sessionName, cwd, projectId) {
100
101
  // Find existing command matching this preset
101
102
  let cmd = findCommandForPreset(preset);
102
103
  // Auto-create the command if it doesn't exist yet
@@ -121,7 +122,7 @@ function createFromPreset(preset, sessionName, cwd, projectId, roleId) {
121
122
  state.cfg.commands.push(cmd);
122
123
  send({ type: 'config.update', config: state.cfg });
123
124
  }
124
- send({ type: 'create', commandId: cmd.id, name: sessionName, cwd, projectId: projectId || undefined, roleId: roleId || undefined, ...estimateSize() });
125
+ send({ type: 'create', commandId: cmd.id, name: sessionName, cwd, projectId: projectId || undefined, ...estimateSize() });
125
126
  localStorage.setItem(MRU_KEY, preset.presetId);
126
127
  }
127
128
 
@@ -144,26 +145,23 @@ export function openCreator() {
144
145
  card.className = 'p-3 border-b border-slate-700/50 bg-slate-800/30';
145
146
  card.innerHTML = `
146
147
  ${(state.cfg.projects?.length) ? `
148
+ <div class="text-[10px] font-semibold uppercase tracking-wider text-slate-500 mb-1.5">Select project</div>
147
149
  <input type="hidden" id="creator-project" value="">
148
150
  <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>
151
+ <span id="creator-project-label">Select project</span>
150
152
  <span class="text-slate-600 ml-2">&#9662;</span>
151
153
  </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"
154
+ <div class="text-[10px] font-semibold uppercase tracking-wider text-slate-500 mb-1.5">Session name</div>
155
+ <input id="creator-name" type="text" maxlength="35" placeholder="Session name"
159
156
  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">
157
+ <div id="creator-cwd-wrap" class="flex items-center gap-1.5 mb-2 ${(state.cfg.projects?.length) ? 'hidden' : ''}">
161
158
  <input id="creator-cwd" type="text" value="${esc(defaultPath)}" placeholder="Working directory"
162
159
  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
160
  <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
161
  ${FOLDER_SVG}
165
162
  </button>
166
163
  </div>
164
+ <div class="text-[10px] font-semibold uppercase tracking-wider text-slate-500 mb-1.5">Choose agent provider</div>
167
165
  <div id="creator-presets" class="space-y-0.5">
168
166
  ${renderPresetButtons()}
169
167
  </div>`;
@@ -173,7 +171,10 @@ export function openCreator() {
173
171
 
174
172
  const nameInput = card.querySelector('#creator-name');
175
173
  const cwdInput = card.querySelector('#creator-cwd');
176
- nameInput.focus();
174
+ const cwdWrap = card.querySelector('#creator-cwd-wrap');
175
+ const projHidden = card.querySelector('#creator-project');
176
+ const projTrigger = card.querySelector('#creator-project-trigger');
177
+ (projTrigger || nameInput).focus();
177
178
 
178
179
  nameInput.addEventListener('keydown', (e) => {
179
180
  if (e.key === 'Escape') closeCreator();
@@ -189,15 +190,33 @@ export function openCreator() {
189
190
  });
190
191
 
191
192
  // Project picker dropdown
192
- const projTrigger = card.querySelector('#creator-project-trigger');
193
193
  if (projTrigger) {
194
+ const projects = [...(state.cfg.projects || [])].sort((a, b) => a.name.localeCompare(b.name, undefined, { sensitivity: 'base' }));
195
+ const projLabel = card.querySelector('#creator-project-label');
196
+ const setProjectSelection = (value) => {
197
+ projHidden.value = value;
198
+ const proj = projects.find(p => p.id === value);
199
+ if (proj) {
200
+ projLabel.textContent = proj.name;
201
+ cwdWrap.classList.add('hidden');
202
+ cwdInput.value = proj.path || defaultPath;
203
+ return;
204
+ }
205
+ if (value === NO_PROJECT_VALUE) {
206
+ projLabel.textContent = 'None (outside project hierarchy)';
207
+ cwdWrap.classList.remove('hidden');
208
+ cwdInput.value = cwdInput.value.trim() || defaultPath;
209
+ return;
210
+ }
211
+ projLabel.textContent = 'Select project';
212
+ cwdWrap.classList.add('hidden');
213
+ cwdInput.value = defaultPath;
214
+ };
215
+
194
216
  let projMenuCleanup = null;
195
217
  projTrigger.addEventListener('click', () => {
196
218
  if (projMenuCleanup) { projMenuCleanup(); return; }
197
219
  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
220
 
202
221
  const menu = document.createElement('div');
203
222
  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 +226,9 @@ export function openCreator() {
207
226
  menu.style.width = rect.width + 'px';
208
227
 
209
228
  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>
229
+ <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
230
  ${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}">
231
+ <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
232
  <span class="w-2 h-2 rounded-full flex-shrink-0" style="background:${p.color || '#3b82f6'}"></span>
214
233
  ${esc(p.name)}
215
234
  </div>`).join('')}`;
@@ -219,12 +238,7 @@ export function openCreator() {
219
238
  const onClick = (e) => {
220
239
  const item = e.target.closest('.proj-option');
221
240
  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;
241
+ setProjectSelection(item.dataset.value);
228
242
  projMenuCleanup();
229
243
  };
230
244
  const onOutside = (e) => {
@@ -242,64 +256,6 @@ export function openCreator() {
242
256
  });
243
257
  }
244
258
 
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
259
  // "Add" button for missing agents — opens install toaster
304
260
  card.addEventListener('click', (e) => {
305
261
  const installBtn = e.target.closest('.install-btn');
@@ -325,13 +281,15 @@ export function openCreator() {
325
281
  if (!btn) return;
326
282
  const preset = state.presets.find(p => p.presetId === btn.dataset.preset);
327
283
  if (!preset) return;
284
+ if (projTrigger && !projHidden.value) {
285
+ showToast('Choose a project or select `None (outside project hierarchy)`.', { title: 'Choose Project', type: 'warn' });
286
+ projTrigger.focus();
287
+ return;
288
+ }
328
289
  const name = nameInput.value.trim() || fallbackName;
329
290
  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);
291
+ const projectId = projHidden?.value && projHidden.value !== NO_PROJECT_VALUE ? projHidden.value : undefined;
292
+ createFromPreset(preset, name, cwd, projectId);
335
293
  closeCreator();
336
294
  });
337
295
  }
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
 
@@ -3,6 +3,7 @@ import { esc, miniMarkdown, resolveIconPath } from './utils.js';
3
3
  import { resolveTheme, resolveAccent, applyTheme } from './profiles.js';
4
4
  import { attachToTerminal, registerHotkey } from './hotkeys.js';
5
5
  import { closeDropdown } from './prompts.js';
6
+ import { showToast } from './toast.js';
6
7
  function isLightBg(themeId) {
7
8
  const bg = resolveTheme(themeId)?.background;
8
9
  if (!bg || bg[0] !== '#') return false;
@@ -119,27 +120,68 @@ function positionMenu(menu, anchorRect) {
119
120
  menu.style.visibility = 'hidden';
120
121
  document.body.appendChild(menu);
121
122
  const mh = menu.offsetHeight;
123
+ const mw = menu.offsetWidth;
122
124
  const gap = 4;
123
125
  const spaceBelow = window.innerHeight - anchorRect.bottom - gap;
126
+ const left = Math.min(
127
+ Math.max(gap, anchorRect.left),
128
+ Math.max(gap, window.innerWidth - mw - gap)
129
+ );
124
130
  menu.style.top = (spaceBelow >= mh
125
131
  ? anchorRect.bottom + gap
126
132
  : Math.max(gap, anchorRect.top - gap - mh)) + 'px';
127
- menu.style.right = (window.innerWidth - anchorRect.right) + 'px';
133
+ menu.style.left = left + 'px';
128
134
  menu.style.visibility = '';
129
135
  }
130
136
 
131
- function openMenu(sessionId, anchorEl) {
137
+ function pointRect(x, y) {
138
+ return { top: y, bottom: y, left: x, right: x };
139
+ }
140
+
141
+ async function copyTerminalSelection(sessionId) {
142
+ const entry = state.terms.get(sessionId);
143
+ const text = entry?.term?.getSelection() || '';
144
+ if (!text) return;
145
+ try {
146
+ await navigator.clipboard.writeText(text);
147
+ } catch {
148
+ showToast('Clipboard write failed.', { type: 'error' });
149
+ }
150
+ }
151
+
152
+ async function pasteIntoTerminal(sessionId) {
153
+ try {
154
+ const text = await navigator.clipboard.readText();
155
+ if (text) send({ type: 'input', id: sessionId, data: text });
156
+ } catch {
157
+ showToast('Clipboard read failed.', { type: 'error' });
158
+ }
159
+ }
160
+
161
+ function openMenu(sessionId, anchor) {
132
162
  closeMenu();
133
163
 
134
- const rect = anchorEl.getBoundingClientRect();
164
+ const rect = anchor?.getBoundingClientRect ? anchor.getBoundingClientRect() : pointRect(anchor.x, anchor.y);
135
165
  const menu = document.createElement('div');
136
166
  menu.className = 'fixed z-[400] min-w-[160px] bg-slate-800 border border-slate-700 rounded-lg shadow-xl shadow-black/40 py-1';
137
167
 
138
168
  const entry = state.terms.get(sessionId);
139
169
  const projects = state.cfg.projects || [];
170
+ const hasSelection = !!entry?.term?.hasSelection();
140
171
 
141
172
  let html = '';
142
173
 
174
+ html += `
175
+ <button class="menu-action flex items-center gap-2.5 w-full px-3 py-2 text-sm ${hasSelection ? 'text-slate-300 hover:bg-slate-700' : 'text-slate-600 cursor-default'} transition-colors text-left" data-action="copy" ${hasSelection ? '' : 'disabled'}>
176
+ <span class="flex-shrink-0 text-slate-400"><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"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg></span>
177
+ Copy
178
+ </button>
179
+ <button class="menu-action flex items-center gap-2.5 w-full px-3 py-2 text-sm text-slate-300 hover:bg-slate-700 transition-colors text-left" data-action="paste">
180
+ <span class="flex-shrink-0 text-slate-400"><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="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2v-2"/><path d="M8 2h6a2 2 0 0 1 2 2v6H8a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2Z"/><path d="M8 10v4"/><path d="M12 14H4a2 2 0 0 0-2 2v2"/></svg></span>
181
+ Paste
182
+ </button>
183
+ <div class="border-t border-slate-700/50 my-1"></div>`;
184
+
143
185
  // Project submenu items
144
186
  if (projects.length) {
145
187
  html += `<div class="px-3 py-1 text-[10px] font-semibold uppercase tracking-wider text-slate-600">Move to project</div>`;
@@ -187,12 +229,16 @@ function openMenu(sessionId, anchorEl) {
187
229
  menu.innerHTML = html;
188
230
  positionMenu(menu, rect);
189
231
 
190
- const onClick = (e) => {
232
+ const onClick = async (e) => {
191
233
  const btn = e.target.closest('.menu-action');
192
234
  if (!btn) return;
193
235
  closeMenu();
194
236
  const action = btn.dataset.action;
195
- if (action === 'rename') {
237
+ if (action === 'copy') {
238
+ await copyTerminalSelection(sessionId);
239
+ } else if (action === 'paste') {
240
+ await pasteIntoTerminal(sessionId);
241
+ } else if (action === 'rename') {
196
242
  startRename(sessionId);
197
243
  } else if (action === 'mute') {
198
244
  toggleMute(sessionId);
@@ -417,6 +463,14 @@ export function addTerminal(id, name, themeId, commandId, projectId, muted, last
417
463
 
418
464
  term.open(el);
419
465
  attachToTerminal(term, presetId);
466
+ const onContextMenu = (e) => {
467
+ if (e.shiftKey) return;
468
+ e.preventDefault();
469
+ e.stopPropagation();
470
+ select(id);
471
+ openMenu(id, { x: e.clientX, y: e.clientY });
472
+ };
473
+ el.addEventListener('contextmenu', onContextMenu);
420
474
  let fitted = false, pending = [];
421
475
  // [FIT-GUARD] only call fit() when proposed dimensions actually change — prevents
422
476
  // unnecessary buffer reflows that cause scrollbar jumpiness on sub-pixel layout shifts
@@ -461,7 +515,7 @@ export function addTerminal(id, name, themeId, commandId, projectId, muted, last
461
515
  }
462
516
  }, 500);
463
517
  const cancelFitRaf = () => { if (fitRaf) { cancelAnimationFrame(fitRaf); fitRaf = 0; } };
464
- state.terms.set(id, { term, fit, el, ro, cancelFitRaf, themeId, commandId, presetId: presetId || null, projectId: projectId || null, muted: !!muted, working: false, workStartedAt: null, stopBounce, queue: (data) => { if (!fitted) { pending.push(data); return true; } return false; }, lastActivityAt: Date.now(), unread: false, lastPreviewText: lastPreview || '', searchText: '' });
518
+ state.terms.set(id, { term, fit, el, ro, cancelFitRaf, onContextMenu, themeId, commandId, presetId: presetId || null, projectId: projectId || null, muted: !!muted, working: false, workStartedAt: null, stopBounce, queue: (data) => { if (!fitted) { pending.push(data); return true; } return false; }, lastActivityAt: Date.now(), unread: false, lastPreviewText: lastPreview || '', searchText: '' });
465
519
  document.getElementById('empty').style.display = 'none';
466
520
  document.getElementById('terminals').style.pointerEvents = '';
467
521
  if (muted) requestAnimationFrame(() => updateMuteIndicator(id));
@@ -475,6 +529,7 @@ export function removeTerminal(id) {
475
529
  if (entry.stopBounce) entry.stopBounce();
476
530
  entry.cancelFitRaf?.();
477
531
  entry.ro?.disconnect();
532
+ entry.el.removeEventListener?.('contextmenu', entry.onContextMenu);
478
533
  entry.term.dispose();
479
534
  entry.el.remove();
480
535
  state.terms.delete(id);
@@ -823,11 +878,11 @@ export function regroupSessions() {
823
878
  <span class="w-2 h-2 rounded-full flex-shrink-0" style="background:${projectColor(proj)}"></span>
824
879
  <span class="project-name flex-1 text-[11px] font-semibold uppercase tracking-wider text-slate-500 truncate">${esc(proj.name)}</span>
825
880
  <span class="project-count text-[10px] text-slate-600">0</span>
826
- <button class="project-path-btn opacity-0 group-hover:opacity-100 ${proj.path ? 'text-slate-600 hover:text-slate-300' : 'text-slate-700 cursor-default'} flex-shrink-0 transition-opacity p-0.5" title="${proj.path ? 'Open project folder' : 'Project path not set'}" ${proj.path ? '' : 'disabled'}>
881
+ <button class="project-path-btn ${proj.path ? 'text-slate-600 hover:text-slate-300' : 'text-slate-700 cursor-default'} flex-shrink-0 p-0.5" title="${proj.path ? 'Open project folder' : 'Project path not set'}" ${proj.path ? '' : 'disabled'}>
827
882
  ${PATH_SVG}
828
883
  </button>
829
884
  <span class="project-plugin-actions"></span>
830
- <button class="project-menu-btn opacity-0 group-hover:opacity-100 text-slate-600 hover:text-slate-400 flex-shrink-0 transition-opacity p-0.5" title="Project menu">
885
+ <button class="project-menu-btn text-slate-600 hover:text-slate-400 flex-shrink-0 p-0.5" title="Project menu">
831
886
  <svg class="w-3.5 h-3.5" fill="none" viewBox="0 0 20 20"><circle cx="10" cy="4" r="1.5" fill="currentColor"/><circle cx="10" cy="10" r="1.5" fill="currentColor"/><circle cx="10" cy="16" r="1.5" fill="currentColor"/></svg>
832
887
  </button>
833
888
  </div>