clideck 1.31.3 → 1.31.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.
- package/README.md +1 -1
- package/config.js +0 -43
- package/package.json +1 -1
- package/plugin-loader.js +2 -3
- package/public/js/hotkeys.js +12 -0
- package/public/js/terminals.js +45 -0
- package/sessions.js +1 -41
- package/public/js/roles.js +0 -112
package/README.md
CHANGED
|
@@ -26,7 +26,7 @@ the main problem with using multiple agents is not starting them. it is managing
|
|
|
26
26
|
|
|
27
27
|
Terminal multiplexers are great at panes. clideck is about conversations.
|
|
28
28
|
|
|
29
|
-
A pane grid is flat. agent work usually is not. projects,
|
|
29
|
+
A pane grid is flat. agent work usually is not. projects, previews, timestamps, notifications, resume, and sometimes a bit of routing between specialists all fit more naturally into a chat app layout. it also maps naturally to mobile, so the same mental model works on desktop and phone.
|
|
30
30
|
|
|
31
31
|
## Quick start
|
|
32
32
|
|
package/config.js
CHANGED
|
@@ -32,46 +32,6 @@ list your fidings please.`,
|
|
|
32
32
|
},
|
|
33
33
|
];
|
|
34
34
|
|
|
35
|
-
const STARTER_ROLES = [
|
|
36
|
-
{
|
|
37
|
-
id: 'starter-role-programmer',
|
|
38
|
-
name: 'Programmer',
|
|
39
|
-
instructions: `You are the main programmer of this project.
|
|
40
|
-
Do you not apply workarounds or bandaids, prefer pure solutions.
|
|
41
|
-
NEVER use plan tool/mode, start to build immediatly and ask questions along the way if any.
|
|
42
|
-
Check if any external findings are valid before applying changes, the reviewer doesnt always updated with the full scope.
|
|
43
|
-
When you done with changes, list concisely what you did.
|
|
44
|
-
|
|
45
|
-
Learn the project quickly if exist. Go over the structure, code and functionality.
|
|
46
|
-
Let me know when you are ready.`,
|
|
47
|
-
},
|
|
48
|
-
{
|
|
49
|
-
id: 'starter-role-reviewer',
|
|
50
|
-
name: 'Reviewer',
|
|
51
|
-
instructions: `You are the code reviewer in this project.
|
|
52
|
-
Your task is check the coder output and list critical / logical design flow issues, ugly workarounds or functionalty you just dont understand why its there. Do not waste time and list insignificunt findings.
|
|
53
|
-
|
|
54
|
-
If you didnt find anything, response with no findings.
|
|
55
|
-
|
|
56
|
-
You never write code!.
|
|
57
|
-
|
|
58
|
-
Quickly learn the project if exist. Go over the structure, code and functionality and let me know when you are ready.`,
|
|
59
|
-
},
|
|
60
|
-
{
|
|
61
|
-
id: 'starter-role-product-manager',
|
|
62
|
-
name: 'Product manager',
|
|
63
|
-
instructions: `You are the product manager of this project, you should understand why we do what we do and what is the best way to do it. You dont care about technical limitations or directions, the only thing matter to you is the user UI/UX and how this agents team will ship a top notch, professional deliveries.
|
|
64
|
-
Do not allow the team to round angles and skip small stuff that will basly impact the user.
|
|
65
|
-
|
|
66
|
-
You never write code!
|
|
67
|
-
You dont use your plan tool/mode - instead you are planning immediatly as you go.
|
|
68
|
-
|
|
69
|
-
Go over the project if exist and understand from the code, documentations and readme what is it and why we do it.
|
|
70
|
-
|
|
71
|
-
Let me know when you are ready`,
|
|
72
|
-
},
|
|
73
|
-
];
|
|
74
|
-
|
|
75
35
|
const DEFAULTS = {
|
|
76
36
|
defaultPath: join(os.homedir(), 'Documents'),
|
|
77
37
|
commands: [
|
|
@@ -88,7 +48,6 @@ const DEFAULTS = {
|
|
|
88
48
|
defaultTheme: 'catppuccin-mocha',
|
|
89
49
|
defaultShell,
|
|
90
50
|
prompts: [],
|
|
91
|
-
roles: [],
|
|
92
51
|
projects: [],
|
|
93
52
|
};
|
|
94
53
|
|
|
@@ -185,7 +144,6 @@ function migrate(cfg) {
|
|
|
185
144
|
}
|
|
186
145
|
}
|
|
187
146
|
if (!cfg.projects) cfg.projects = [];
|
|
188
|
-
if (!cfg.roles) cfg.roles = [];
|
|
189
147
|
return cfg;
|
|
190
148
|
}
|
|
191
149
|
|
|
@@ -194,7 +152,6 @@ function load() {
|
|
|
194
152
|
return {
|
|
195
153
|
...deepCopy(DEFAULTS),
|
|
196
154
|
prompts: deepCopy(STARTER_PROMPTS),
|
|
197
|
-
roles: deepCopy(STARTER_ROLES),
|
|
198
155
|
};
|
|
199
156
|
}
|
|
200
157
|
try {
|
package/package.json
CHANGED
package/plugin-loader.js
CHANGED
|
@@ -207,13 +207,13 @@ function buildApi(pluginId, pluginDir, state) {
|
|
|
207
207
|
const s = sessionsFn?.()?.get(id);
|
|
208
208
|
if (!s) return null;
|
|
209
209
|
const state = sessionStatus.get(id) || '';
|
|
210
|
-
return { id, name: s.name, cwd: s.cwd, commandId: s.commandId, presetId: s.presetId || 'shell', themeId: s.themeId, projectId: s.projectId,
|
|
210
|
+
return { id, name: s.name, cwd: s.cwd, commandId: s.commandId, presetId: s.presetId || 'shell', themeId: s.themeId, projectId: s.projectId, working: state.startsWith('1:') };
|
|
211
211
|
},
|
|
212
212
|
getSessions() {
|
|
213
213
|
const sessions = sessionsFn?.();
|
|
214
214
|
if (!sessions) return [];
|
|
215
215
|
return [...sessions].map(([id, s]) => ({
|
|
216
|
-
id, name: s.name, cwd: s.cwd, commandId: s.commandId, presetId: s.presetId || 'shell', themeId: s.themeId, projectId: s.projectId,
|
|
216
|
+
id, name: s.name, cwd: s.cwd, commandId: s.commandId, presetId: s.presetId || 'shell', themeId: s.themeId, projectId: s.projectId, working: (sessionStatus.get(id) || '').startsWith('1:'),
|
|
217
217
|
}));
|
|
218
218
|
},
|
|
219
219
|
|
|
@@ -232,7 +232,6 @@ function buildApi(pluginId, pluginDir, state) {
|
|
|
232
232
|
inputToSession(id, data) { inputFn?.({ id, data }); },
|
|
233
233
|
setAutoApproveMenu(id, enabled) { enabled ? autoApproveMenus.add(id) : autoApproveMenus.delete(id); },
|
|
234
234
|
|
|
235
|
-
getRoles() { return JSON.parse(JSON.stringify(getConfigFn?.()?.roles || [])); },
|
|
236
235
|
getProjects() { return JSON.parse(JSON.stringify(getConfigFn?.()?.projects || [])); },
|
|
237
236
|
getTranscript(id, n, order) { return transcript.getTurns(id, n || 20, order || 'end'); },
|
|
238
237
|
detectMenu(lines, presetId) { return transcript.detectMenu(lines, presetId); },
|
package/public/js/hotkeys.js
CHANGED
|
@@ -88,6 +88,18 @@ export function unregisterAllForPlugin(pluginId) {
|
|
|
88
88
|
// Prompt autocomplete (// trigger) runs first, then hotkey dispatch.
|
|
89
89
|
export function attachToTerminal(term, presetId) {
|
|
90
90
|
term.attachCustomKeyEventHandler((e) => {
|
|
91
|
+
if (e.type === 'keydown'
|
|
92
|
+
&& e.ctrlKey
|
|
93
|
+
&& !e.metaKey
|
|
94
|
+
&& !e.altKey
|
|
95
|
+
&& !e.shiftKey
|
|
96
|
+
&& e.code === 'KeyC'
|
|
97
|
+
&& term.hasSelection()) {
|
|
98
|
+
e.preventDefault();
|
|
99
|
+
navigator.clipboard?.writeText(term.getSelection()).catch(() => {});
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
|
|
91
103
|
// Claude Code uses Shift+Enter for multiline input, but xterm emits the
|
|
92
104
|
// same "\r" as plain Enter. Scope CSI-u translation to Claude only so
|
|
93
105
|
// other terminals keep their existing Enter behavior unchanged.
|
package/public/js/terminals.js
CHANGED
|
@@ -40,6 +40,50 @@ const MIN_CONTRAST_RATIO = 4.5;
|
|
|
40
40
|
const DARK_BALLS = ['#00e5ff', '#5df0d6', '#9b8cff'];
|
|
41
41
|
const LIGHT_BALLS = ['#0891b2', '#059669', '#7c3aed'];
|
|
42
42
|
|
|
43
|
+
const URL_RE = /\bhttps?:\/\/[^\s<>"'`]+/g;
|
|
44
|
+
|
|
45
|
+
function cleanUrlMatch(text, index) {
|
|
46
|
+
let url = text;
|
|
47
|
+
while (/[),.;:!?\\\]}]+$/.test(url)) url = url.slice(0, -1);
|
|
48
|
+
if (!url) return null;
|
|
49
|
+
try {
|
|
50
|
+
const parsed = new URL(url);
|
|
51
|
+
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') return null;
|
|
52
|
+
} catch {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
return { text: url, index };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function openTerminalLink(url) {
|
|
59
|
+
const win = window.open(url, '_blank', 'noopener,noreferrer');
|
|
60
|
+
if (win) win.opener = null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function addLinkProvider(term) {
|
|
64
|
+
term.registerLinkProvider({
|
|
65
|
+
provideLinks(y, callback) {
|
|
66
|
+
const line = term.buffer.active.getLine(y - 1);
|
|
67
|
+
if (!line) return callback(undefined);
|
|
68
|
+
const text = line.translateToString(true);
|
|
69
|
+
const links = [];
|
|
70
|
+
for (const match of text.matchAll(URL_RE)) {
|
|
71
|
+
const cleaned = cleanUrlMatch(match[0], match.index || 0);
|
|
72
|
+
if (!cleaned) continue;
|
|
73
|
+
links.push({
|
|
74
|
+
text: cleaned.text,
|
|
75
|
+
range: {
|
|
76
|
+
start: { x: cleaned.index + 1, y },
|
|
77
|
+
end: { x: cleaned.index + cleaned.text.length, y },
|
|
78
|
+
},
|
|
79
|
+
activate: (_event, linkText) => openTerminalLink(linkText),
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
callback(links.length ? links : undefined);
|
|
83
|
+
},
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
43
87
|
function startBounce(container) {
|
|
44
88
|
const isDark = !document.documentElement.classList.contains('light');
|
|
45
89
|
const colors = isDark ? DARK_BALLS : LIGHT_BALLS;
|
|
@@ -386,6 +430,7 @@ export function addTerminal(id, name, themeId, commandId, projectId, muted, last
|
|
|
386
430
|
});
|
|
387
431
|
const fit = new FitAddon.FitAddon();
|
|
388
432
|
term.loadAddon(fit);
|
|
433
|
+
addLinkProvider(term);
|
|
389
434
|
term.onData(data => send({ type: 'input', id, data }));
|
|
390
435
|
|
|
391
436
|
// [TRANSCRIPT-CAPTURE] initial settled capture plus one delayed idle save
|
package/sessions.js
CHANGED
|
@@ -108,29 +108,7 @@ function spawnSession(id, cmd, parts, cwd, name, themeId, commandId, savedToken,
|
|
|
108
108
|
if (preset?.telemetryEnv) telemetry.watchSession(id, bin);
|
|
109
109
|
if (preset?.bridge === 'opencode') opencodeBridge.watchSession(id, cwd);
|
|
110
110
|
|
|
111
|
-
function injectRolePrompt() {
|
|
112
|
-
if (!session.pendingRolePrompt) return;
|
|
113
|
-
transcript.recordInjectedInput(id, session.pendingRolePrompt);
|
|
114
|
-
term.write(session.pendingRolePrompt);
|
|
115
|
-
setTimeout(() => term.write('\r'), 150);
|
|
116
|
-
console.log(`Session ${id.slice(0, 8)}: injected role prompt`);
|
|
117
|
-
delete session.pendingRolePrompt;
|
|
118
|
-
delete session._rolePromptTimer;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
111
|
term.onData((data) => {
|
|
122
|
-
// Role prompts should be injected only when the agent is likely ready for
|
|
123
|
-
// input. For Codex, use the first OTLP startup event instead of a blind
|
|
124
|
-
// fixed startup delay; other agents keep the existing delayed path.
|
|
125
|
-
if (session.pendingRolePrompt && !session._rolePromptTimer) {
|
|
126
|
-
if (session.presetId === 'codex') {
|
|
127
|
-
if (telemetry.hasEvents(id)) injectRolePrompt();
|
|
128
|
-
} else {
|
|
129
|
-
session._rolePromptTimer = setTimeout(() => {
|
|
130
|
-
if (session.pendingRolePrompt) injectRolePrompt();
|
|
131
|
-
}, 3000);
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
112
|
session.chunks.push(data);
|
|
135
113
|
session.chunksSize += data.length;
|
|
136
114
|
while (session.chunksSize > MAX_BUFFER && session.chunks.length > 1) {
|
|
@@ -164,7 +142,6 @@ function spawnSession(id, cmd, parts, cwd, name, themeId, commandId, savedToken,
|
|
|
164
142
|
resumable.push({
|
|
165
143
|
id, name: s.name, commandId: s.commandId, presetId: s.presetId || 'shell', cwd: s.cwd,
|
|
166
144
|
themeId: s.themeId, sessionToken: s.sessionToken, projectId: s.projectId, muted: !!s.muted,
|
|
167
|
-
roleName: s.roleName || null,
|
|
168
145
|
lastPreview: s.lastPreview || '', lastActivityAt: s.lastActivityAt || null,
|
|
169
146
|
savedAt: new Date().toISOString(),
|
|
170
147
|
});
|
|
@@ -202,18 +179,6 @@ function create(msg, ws, cfg) {
|
|
|
202
179
|
return;
|
|
203
180
|
}
|
|
204
181
|
|
|
205
|
-
// If a role was selected, store identity on session and queue prompt injection
|
|
206
|
-
if (msg.roleId) {
|
|
207
|
-
const role = (cfg.roles || []).find(r => r.id === msg.roleId);
|
|
208
|
-
if (role) {
|
|
209
|
-
const s = sessions.get(id);
|
|
210
|
-
if (s) {
|
|
211
|
-
s.roleName = role.name;
|
|
212
|
-
if (role.instructions) s.pendingRolePrompt = role.instructions;
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
|
|
217
182
|
const createdPresetId = PRESETS.find(p => binName(p.command) === binName(cmd.command))?.presetId || 'shell';
|
|
218
183
|
const installId = msg.installId || undefined;
|
|
219
184
|
broadcast({ type: 'created', id, name, themeId, commandId: cmd.id, presetId: createdPresetId, projectId, installId });
|
|
@@ -245,7 +210,6 @@ function createProgrammatic(opts, cfg) {
|
|
|
245
210
|
if (err) return { error: err.message };
|
|
246
211
|
|
|
247
212
|
const s = sessions.get(id);
|
|
248
|
-
if (s && opts.roleName) s.roleName = opts.roleName;
|
|
249
213
|
if (s && opts.ephemeral) s.ephemeral = true;
|
|
250
214
|
|
|
251
215
|
const presetId = PRESETS.find(p => binName(p.command) === binName(cmd.command))?.presetId || 'shell';
|
|
@@ -292,7 +256,6 @@ function resume(msg, ws, cfg) {
|
|
|
292
256
|
const s = sessions.get(id);
|
|
293
257
|
if (s) {
|
|
294
258
|
if (saved.muted) s.muted = true;
|
|
295
|
-
if (saved.roleName) s.roleName = saved.roleName;
|
|
296
259
|
}
|
|
297
260
|
|
|
298
261
|
// Remove from resumable list and notify all clients
|
|
@@ -386,7 +349,7 @@ function restart(msg, ws, cfg) {
|
|
|
386
349
|
}
|
|
387
350
|
|
|
388
351
|
const savedToken = s.sessionToken;
|
|
389
|
-
const { name, cwd, commandId, projectId,
|
|
352
|
+
const { name, cwd, commandId, projectId, muted, lastPreview, lastActivityAt } = s;
|
|
390
353
|
|
|
391
354
|
activity.clear(id);
|
|
392
355
|
telemetry.clear(id);
|
|
@@ -405,7 +368,6 @@ function restart(msg, ws, cfg) {
|
|
|
405
368
|
|
|
406
369
|
const next = sessions.get(id);
|
|
407
370
|
if (next) {
|
|
408
|
-
next.roleName = roleName || null;
|
|
409
371
|
next.muted = !!muted;
|
|
410
372
|
next.lastPreview = lastPreview || '';
|
|
411
373
|
next.lastActivityAt = lastActivityAt || null;
|
|
@@ -417,7 +379,6 @@ function restart(msg, ws, cfg) {
|
|
|
417
379
|
function list() {
|
|
418
380
|
return [...sessions].map(([id, s]) => ({
|
|
419
381
|
id, name: s.name, themeId: s.themeId, commandId: s.commandId, presetId: s.presetId || 'shell', projectId: s.projectId, muted: !!s.muted,
|
|
420
|
-
roleName: s.roleName || null,
|
|
421
382
|
// Last preview text for sidebar display on reconnect
|
|
422
383
|
lastPreview: s.lastPreview || '', lastActivityAt: s.lastActivityAt || null,
|
|
423
384
|
menu: s._menuKey ? JSON.parse(s._menuKey) : undefined,
|
|
@@ -486,7 +447,6 @@ function saveSessions(cfg) {
|
|
|
486
447
|
.map(([id, s]) => ({
|
|
487
448
|
id, name: s.name, commandId: s.commandId, presetId: s.presetId || 'shell', cwd: s.cwd,
|
|
488
449
|
themeId: s.themeId, sessionToken: s.sessionToken, projectId: s.projectId, muted: !!s.muted,
|
|
489
|
-
roleName: s.roleName || null,
|
|
490
450
|
lastPreview: s.lastPreview || '', lastActivityAt: s.lastActivityAt || null,
|
|
491
451
|
savedAt: new Date().toISOString(),
|
|
492
452
|
}));
|
package/public/js/roles.js
DELETED
|
@@ -1,112 +0,0 @@
|
|
|
1
|
-
// Roles panel — manage worker role definitions (core feature, not plugin-specific)
|
|
2
|
-
import { state, send } from './state.js';
|
|
3
|
-
import { esc } from './utils.js';
|
|
4
|
-
|
|
5
|
-
const panel = document.getElementById('panel-roles');
|
|
6
|
-
|
|
7
|
-
function getRoles() { return state.cfg.roles || []; }
|
|
8
|
-
|
|
9
|
-
function save() { send({ type: 'config.update', config: state.cfg }); }
|
|
10
|
-
|
|
11
|
-
export function renderRoles() {
|
|
12
|
-
const roles = getRoles();
|
|
13
|
-
panel.innerHTML = `
|
|
14
|
-
<div class="flex items-center justify-between px-3 pt-3 pb-2">
|
|
15
|
-
<span class="text-sm font-bold text-slate-200 tracking-tight" style="font-family:'JetBrains Mono',monospace">Roles</span>
|
|
16
|
-
<button id="btn-add-role" class="icon-btn w-7 h-7 flex items-center justify-center rounded-md border border-slate-600 text-slate-400 hover:bg-slate-700 hover:text-slate-200 transition-colors text-sm" title="New role">+</button>
|
|
17
|
-
</div>
|
|
18
|
-
<div id="roles-list" class="tmx-scroll flex-1 overflow-y-auto border-t border-slate-700/50"></div>`;
|
|
19
|
-
|
|
20
|
-
panel.querySelector('#btn-add-role').addEventListener('click', () => openEditor());
|
|
21
|
-
|
|
22
|
-
const list = panel.querySelector('#roles-list');
|
|
23
|
-
list.addEventListener('click', (e) => {
|
|
24
|
-
if (e.target.closest('.role-edit')) {
|
|
25
|
-
const idx = +e.target.closest('.role-row').dataset.idx;
|
|
26
|
-
openEditor(idx);
|
|
27
|
-
return;
|
|
28
|
-
}
|
|
29
|
-
if (e.target.closest('.role-del')) {
|
|
30
|
-
const idx = +e.target.closest('.role-row').dataset.idx;
|
|
31
|
-
state.cfg.roles.splice(idx, 1);
|
|
32
|
-
save();
|
|
33
|
-
renderRoles();
|
|
34
|
-
return;
|
|
35
|
-
}
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
renderRoleList(roles);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function renderRoleList(roles) {
|
|
42
|
-
const list = panel.querySelector('#roles-list');
|
|
43
|
-
if (!roles.length) {
|
|
44
|
-
list.innerHTML = `<div class="flex flex-col items-center justify-center h-full px-6 text-center">
|
|
45
|
-
<p class="text-sm text-slate-400 mb-1">No roles defined</p>
|
|
46
|
-
<p class="text-xs text-slate-600 leading-relaxed">Define agent identities with a name and instructions.<br>Roles are sent to the agent when a session starts<br>and can be used by plugins like Autopilot.</p>
|
|
47
|
-
</div>`;
|
|
48
|
-
return;
|
|
49
|
-
}
|
|
50
|
-
list.innerHTML = roles.map((r, idx) => `
|
|
51
|
-
<div class="role-row group flex items-start gap-2 px-3 py-2.5 cursor-default hover:bg-slate-800/40 transition-colors ${idx > 0 ? 'border-t border-slate-700/30' : ''}" data-idx="${idx}">
|
|
52
|
-
<div class="flex-1 min-w-0">
|
|
53
|
-
<div class="text-[13px] font-medium text-slate-200 truncate">${esc(r.name)}</div>
|
|
54
|
-
<div class="text-[11px] text-slate-500 mt-0.5 line-clamp-2 leading-relaxed">${esc(r.instructions)}</div>
|
|
55
|
-
</div>
|
|
56
|
-
<div class="flex items-center gap-0.5 opacity-0 group-hover:opacity-100 transition-opacity flex-shrink-0 mt-0.5">
|
|
57
|
-
<button class="role-edit w-6 h-6 flex items-center justify-center rounded text-slate-500 hover:text-slate-300 hover:bg-slate-700/60 transition-colors" title="Edit">
|
|
58
|
-
<svg class="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5"><path d="M17 3a2.85 2.85 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z"/></svg>
|
|
59
|
-
</button>
|
|
60
|
-
<button class="role-del w-6 h-6 flex items-center justify-center rounded text-slate-500 hover:text-red-400 hover:bg-slate-700/60 transition-colors" title="Delete">
|
|
61
|
-
<svg class="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5"><path d="M18 6L6 18M6 6l12 12"/></svg>
|
|
62
|
-
</button>
|
|
63
|
-
</div>
|
|
64
|
-
</div>
|
|
65
|
-
`).join('');
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
function closeEditor() { document.getElementById('role-editor')?.remove(); }
|
|
69
|
-
|
|
70
|
-
function openEditor(idx) {
|
|
71
|
-
if (document.getElementById('role-editor')) { closeEditor(); if (idx == null) return; }
|
|
72
|
-
const existing = idx != null ? getRoles()[idx] : null;
|
|
73
|
-
|
|
74
|
-
const card = document.createElement('div');
|
|
75
|
-
card.id = 'role-editor';
|
|
76
|
-
card.className = 'p-3 border-b border-slate-700/50 bg-slate-800/30';
|
|
77
|
-
card.innerHTML = `
|
|
78
|
-
<input id="re-name" type="text" maxlength="40" placeholder="Role name" value="${esc(existing?.name || '')}"
|
|
79
|
-
class="w-full px-3 py-2 text-sm bg-slate-900 border border-slate-700 rounded-md text-slate-200 placeholder-slate-500 outline-none focus:border-blue-500 transition-colors mb-2">
|
|
80
|
-
<textarea id="re-instructions" rows="4" placeholder="Who am I? e.g. You are a senior software architect. You break down goals into clear, actionable tasks with acceptance criteria."
|
|
81
|
-
class="w-full max-w-full px-3 py-1.5 text-xs bg-slate-900 border border-slate-700 rounded-md text-slate-200 placeholder-slate-600 outline-none focus:border-blue-500 transition-colors resize-y leading-relaxed font-mono mb-2" style="min-height:5lh">${esc(existing?.instructions || '')}</textarea>
|
|
82
|
-
<div class="flex items-center gap-2">
|
|
83
|
-
<button id="re-save" class="px-4 py-1.5 text-xs font-medium bg-blue-600 hover:bg-blue-500 text-white rounded-md transition-colors">${existing ? 'Save' : 'Add'}</button>
|
|
84
|
-
<button id="re-cancel" class="px-3 py-1.5 text-xs text-slate-500 hover:text-slate-300 transition-colors">Cancel</button>
|
|
85
|
-
</div>`;
|
|
86
|
-
|
|
87
|
-
const list = panel.querySelector('#roles-list');
|
|
88
|
-
list.parentElement.insertBefore(card, list);
|
|
89
|
-
|
|
90
|
-
const nameEl = card.querySelector('#re-name');
|
|
91
|
-
const instrEl = card.querySelector('#re-instructions');
|
|
92
|
-
nameEl.focus();
|
|
93
|
-
|
|
94
|
-
const doSave = () => {
|
|
95
|
-
const name = nameEl.value.trim();
|
|
96
|
-
const instructions = instrEl.value.trim();
|
|
97
|
-
if (!name || !instructions) return;
|
|
98
|
-
if (!state.cfg.roles) state.cfg.roles = [];
|
|
99
|
-
if (idx != null) {
|
|
100
|
-
state.cfg.roles[idx] = { ...state.cfg.roles[idx], name, instructions };
|
|
101
|
-
} else {
|
|
102
|
-
state.cfg.roles.push({ id: crypto.randomUUID(), name, instructions });
|
|
103
|
-
}
|
|
104
|
-
save();
|
|
105
|
-
closeEditor();
|
|
106
|
-
renderRoles();
|
|
107
|
-
};
|
|
108
|
-
|
|
109
|
-
card.querySelector('#re-save').addEventListener('click', doSave);
|
|
110
|
-
card.querySelector('#re-cancel').addEventListener('click', closeEditor);
|
|
111
|
-
nameEl.addEventListener('keydown', (e) => { if (e.key === 'Escape') closeEditor(); });
|
|
112
|
-
}
|