clideck 1.31.3 → 1.31.5
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/codex-hooks.js +7 -7
- package/config.js +3 -43
- package/handlers.js +71 -40
- package/package.json +1 -1
- package/plugin-loader.js +2 -3
- package/public/js/app.js +27 -10
- package/public/js/creator.js +44 -23
- package/public/js/hotkeys.js +12 -0
- package/public/js/settings.js +113 -24
- package/public/js/terminals.js +46 -0
- package/public/tailwind.css +1 -1
- package/sessions.js +35 -58
- package/transcript.js +5 -1
- package/public/js/roles.js +0 -112
package/sessions.js
CHANGED
|
@@ -10,7 +10,7 @@ const opencodeBridge = require('./opencode-bridge');
|
|
|
10
10
|
const plugins = require('./plugin-loader');
|
|
11
11
|
|
|
12
12
|
const THEMES = require('./themes');
|
|
13
|
-
const MAX_BUFFER =
|
|
13
|
+
const MAX_BUFFER = 2 * 1024 * 1024;
|
|
14
14
|
const { PORT, localUrl } = require('./runtime');
|
|
15
15
|
const ANSI_RE = /\x1b\[[0-9;?]*[ -/]*[@-~]|\x1b\].*?(?:\x07|\x1b\\)|\x1b./g;
|
|
16
16
|
const PRESETS = JSON.parse(require('fs').readFileSync(join(__dirname, 'agent-presets.json'), 'utf8'));
|
|
@@ -59,9 +59,26 @@ function broadcast(msg) {
|
|
|
59
59
|
|
|
60
60
|
// --- Spawn a PTY and wire up a session ---
|
|
61
61
|
|
|
62
|
+
function presetForCommand(cmd) {
|
|
63
|
+
if (cmd?.presetId) {
|
|
64
|
+
const preset = PRESETS.find(p => p.presetId === cmd.presetId);
|
|
65
|
+
if (preset) return preset;
|
|
66
|
+
}
|
|
67
|
+
const bin = binName(cmd?.command);
|
|
68
|
+
return PRESETS.find(p => binName(p.command) === bin);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function commandEnv(cmd) {
|
|
72
|
+
const env = {};
|
|
73
|
+
if (!cmd?.env || typeof cmd.env !== 'object' || Array.isArray(cmd.env)) return env;
|
|
74
|
+
for (const [key, value] of Object.entries(cmd.env)) {
|
|
75
|
+
if (/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) env[key] = String(value ?? '');
|
|
76
|
+
}
|
|
77
|
+
return env;
|
|
78
|
+
}
|
|
79
|
+
|
|
62
80
|
function buildTelemetryEnv(id, cmd) {
|
|
63
|
-
const
|
|
64
|
-
const preset = PRESETS.find(p => binName(p.command) === bin);
|
|
81
|
+
const preset = presetForCommand(cmd);
|
|
65
82
|
const telemetryEnabled = cmd.telemetryEnabled ?? (preset?.presetId === 'claude-code');
|
|
66
83
|
const env = { CLIDECK_SESSION_ID: id, CLIDECK_PORT: String(PORT), CLIDECK_URL: localUrl() };
|
|
67
84
|
if (!preset?.telemetryEnv || !telemetryEnabled) return env;
|
|
@@ -85,19 +102,19 @@ function isLightTheme(themeId) {
|
|
|
85
102
|
function spawnSession(id, cmd, parts, cwd, name, themeId, commandId, savedToken, projectId, cols, rows) {
|
|
86
103
|
const telemetryEnv = buildTelemetryEnv(id, cmd);
|
|
87
104
|
const colorEnv = isLightTheme(themeId) ? { COLORFGBG: '0;15' } : { COLORFGBG: '15;0' };
|
|
105
|
+
const extraEnv = commandEnv(cmd);
|
|
88
106
|
let term;
|
|
89
107
|
try {
|
|
90
108
|
term = pty.spawn(parts[0], parts.slice(1), {
|
|
91
109
|
name: 'xterm-256color', cols: cols || 80, rows: rows || 24, cwd,
|
|
92
|
-
env: { ...process.env, ...telemetryEnv, ...colorEnv },
|
|
110
|
+
env: { ...process.env, ...extraEnv, ...telemetryEnv, ...colorEnv },
|
|
93
111
|
});
|
|
94
112
|
} catch (e) {
|
|
95
113
|
return e;
|
|
96
114
|
}
|
|
97
115
|
|
|
98
116
|
const sessionIdRe = cmd.sessionIdPattern ? new RegExp(cmd.sessionIdPattern, 'i') : null;
|
|
99
|
-
const
|
|
100
|
-
const preset = PRESETS.find(p => binName(p.command) === bin);
|
|
117
|
+
const preset = presetForCommand(cmd);
|
|
101
118
|
const session = { name, themeId, commandId, cwd, pty: term, chunks: [], chunksSize: 0, sessionToken: savedToken || null, projectId: projectId || null, presetId: preset?.presetId || 'shell', working: undefined };
|
|
102
119
|
sessions.set(id, session);
|
|
103
120
|
transcript.setFinalizeOnIdle(id, ['claude-code', 'codex', 'gemini-cli', 'opencode', 'clideck-agent'].includes(session.presetId) ? session.presetId : null);
|
|
@@ -105,32 +122,10 @@ function spawnSession(id, cmd, parts, cwd, name, themeId, commandId, savedToken,
|
|
|
105
122
|
// Always watch telemetry-backed agents so OTLP fallback matching can attach
|
|
106
123
|
// early events to this session even when the agent omits clideck.session_id.
|
|
107
124
|
// The receiver itself decides whether to surface a setup prompt.
|
|
108
|
-
if (preset?.telemetryEnv) telemetry.watchSession(id,
|
|
125
|
+
if (preset?.telemetryEnv) telemetry.watchSession(id, binName(cmd.command));
|
|
109
126
|
if (preset?.bridge === 'opencode') opencodeBridge.watchSession(id, cwd);
|
|
110
127
|
|
|
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
128
|
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
129
|
session.chunks.push(data);
|
|
135
130
|
session.chunksSize += data.length;
|
|
136
131
|
while (session.chunksSize > MAX_BUFFER && session.chunks.length > 1) {
|
|
@@ -164,7 +159,6 @@ function spawnSession(id, cmd, parts, cwd, name, themeId, commandId, savedToken,
|
|
|
164
159
|
resumable.push({
|
|
165
160
|
id, name: s.name, commandId: s.commandId, presetId: s.presetId || 'shell', cwd: s.cwd,
|
|
166
161
|
themeId: s.themeId, sessionToken: s.sessionToken, projectId: s.projectId, muted: !!s.muted,
|
|
167
|
-
roleName: s.roleName || null,
|
|
168
162
|
lastPreview: s.lastPreview || '', lastActivityAt: s.lastActivityAt || null,
|
|
169
163
|
savedAt: new Date().toISOString(),
|
|
170
164
|
});
|
|
@@ -202,25 +196,12 @@ function create(msg, ws, cfg) {
|
|
|
202
196
|
return;
|
|
203
197
|
}
|
|
204
198
|
|
|
205
|
-
|
|
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
|
-
const createdPresetId = PRESETS.find(p => binName(p.command) === binName(cmd.command))?.presetId || 'shell';
|
|
199
|
+
const createdPresetId = presetForCommand(cmd)?.presetId || 'shell';
|
|
218
200
|
const installId = msg.installId || undefined;
|
|
219
201
|
broadcast({ type: 'created', id, name, themeId, commandId: cmd.id, presetId: createdPresetId, projectId, installId });
|
|
220
202
|
|
|
221
203
|
// Immediate setup notification if config not detected
|
|
222
|
-
const
|
|
223
|
-
const preset = PRESETS.find(p => binName(p.command) === bin);
|
|
204
|
+
const preset = presetForCommand(cmd);
|
|
224
205
|
if (preset && (preset.telemetrySetup || preset.bridge) && !(cmd.telemetryEnabled && cmd.telemetryStatus?.ok)) {
|
|
225
206
|
broadcast({ type: 'session.needsSetup', id });
|
|
226
207
|
}
|
|
@@ -245,10 +226,9 @@ function createProgrammatic(opts, cfg) {
|
|
|
245
226
|
if (err) return { error: err.message };
|
|
246
227
|
|
|
247
228
|
const s = sessions.get(id);
|
|
248
|
-
if (s && opts.roleName) s.roleName = opts.roleName;
|
|
249
229
|
if (s && opts.ephemeral) s.ephemeral = true;
|
|
250
230
|
|
|
251
|
-
const presetId =
|
|
231
|
+
const presetId = presetForCommand(cmd)?.presetId || 'shell';
|
|
252
232
|
broadcast({ type: 'created', id, name, themeId, commandId: cmd.id, presetId, projectId });
|
|
253
233
|
return { id };
|
|
254
234
|
}
|
|
@@ -292,14 +272,13 @@ function resume(msg, ws, cfg) {
|
|
|
292
272
|
const s = sessions.get(id);
|
|
293
273
|
if (s) {
|
|
294
274
|
if (saved.muted) s.muted = true;
|
|
295
|
-
if (saved.roleName) s.roleName = saved.roleName;
|
|
296
275
|
}
|
|
297
276
|
|
|
298
277
|
// Remove from resumable list and notify all clients
|
|
299
278
|
resumable = resumable.filter(s => s.id !== id);
|
|
300
279
|
broadcast({ type: 'sessions.resumable', list: getResumable(cfg) });
|
|
301
280
|
|
|
302
|
-
const resumePresetId =
|
|
281
|
+
const resumePresetId = presetForCommand(cmd)?.presetId || saved.presetId || 'shell';
|
|
303
282
|
broadcast({ type: 'created', id, name: saved.name, themeId: saved.themeId || saved.profileId || 'default', commandId: saved.commandId, presetId: resumePresetId, projectId: saved.projectId || null, muted: !!saved.muted, resumed: true, lastPreview: saved.lastPreview || '' });
|
|
304
283
|
}
|
|
305
284
|
|
|
@@ -386,7 +365,7 @@ function restart(msg, ws, cfg) {
|
|
|
386
365
|
}
|
|
387
366
|
|
|
388
367
|
const savedToken = s.sessionToken;
|
|
389
|
-
const { name, cwd, commandId, projectId,
|
|
368
|
+
const { name, cwd, commandId, projectId, muted, lastPreview, lastActivityAt } = s;
|
|
390
369
|
|
|
391
370
|
activity.clear(id);
|
|
392
371
|
telemetry.clear(id);
|
|
@@ -405,7 +384,6 @@ function restart(msg, ws, cfg) {
|
|
|
405
384
|
|
|
406
385
|
const next = sessions.get(id);
|
|
407
386
|
if (next) {
|
|
408
|
-
next.roleName = roleName || null;
|
|
409
387
|
next.muted = !!muted;
|
|
410
388
|
next.lastPreview = lastPreview || '';
|
|
411
389
|
next.lastActivityAt = lastActivityAt || null;
|
|
@@ -417,7 +395,6 @@ function restart(msg, ws, cfg) {
|
|
|
417
395
|
function list() {
|
|
418
396
|
return [...sessions].map(([id, s]) => ({
|
|
419
397
|
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
398
|
// Last preview text for sidebar display on reconnect
|
|
422
399
|
lastPreview: s.lastPreview || '', lastActivityAt: s.lastActivityAt || null,
|
|
423
400
|
menu: s._menuKey ? JSON.parse(s._menuKey) : undefined,
|
|
@@ -445,13 +422,18 @@ function getResumable(cfg) {
|
|
|
445
422
|
if (s.presetId) return s;
|
|
446
423
|
const cmd = (cfg.commands || []).find(c => c.id === s.commandId);
|
|
447
424
|
if (!cmd) return { ...s, presetId: 'shell' };
|
|
448
|
-
const preset =
|
|
425
|
+
const preset = presetForCommand(cmd);
|
|
449
426
|
return { ...s, presetId: preset?.presetId || 'shell' };
|
|
450
427
|
});
|
|
451
428
|
}
|
|
452
429
|
|
|
453
430
|
function sendBuffers(ws) {
|
|
454
431
|
for (const [id, s] of sessions) {
|
|
432
|
+
if (s.chunks.length) {
|
|
433
|
+
const data = s.chunks.join('');
|
|
434
|
+
ws.send(JSON.stringify({ type: 'output', id, data, replay: true }));
|
|
435
|
+
continue;
|
|
436
|
+
}
|
|
455
437
|
if (['claude-code', 'codex', 'gemini-cli', 'opencode', 'clideck-agent'].includes(s.presetId) && !s.working) {
|
|
456
438
|
const text = transcript.getReplayText(id, s.presetId);
|
|
457
439
|
if (text) {
|
|
@@ -459,10 +441,6 @@ function sendBuffers(ws) {
|
|
|
459
441
|
continue;
|
|
460
442
|
}
|
|
461
443
|
}
|
|
462
|
-
if (s.chunks.length) {
|
|
463
|
-
const data = s.chunks.join('');
|
|
464
|
-
ws.send(JSON.stringify({ type: 'output', id, data, replay: true }));
|
|
465
|
-
}
|
|
466
444
|
}
|
|
467
445
|
}
|
|
468
446
|
|
|
@@ -486,7 +464,6 @@ function saveSessions(cfg) {
|
|
|
486
464
|
.map(([id, s]) => ({
|
|
487
465
|
id, name: s.name, commandId: s.commandId, presetId: s.presetId || 'shell', cwd: s.cwd,
|
|
488
466
|
themeId: s.themeId, sessionToken: s.sessionToken, projectId: s.projectId, muted: !!s.muted,
|
|
489
|
-
roleName: s.roleName || null,
|
|
490
467
|
lastPreview: s.lastPreview || '', lastActivityAt: s.lastActivityAt || null,
|
|
491
468
|
savedAt: new Date().toISOString(),
|
|
492
469
|
}));
|
package/transcript.js
CHANGED
|
@@ -226,9 +226,13 @@ function getTurns(id, n, order) {
|
|
|
226
226
|
|
|
227
227
|
function getCache() { return { ...cache }; }
|
|
228
228
|
|
|
229
|
+
function hasSettledReplay(entries) {
|
|
230
|
+
return entries?.some(e => e.role === 'agent') && entries[entries.length - 1]?.role === 'agent';
|
|
231
|
+
}
|
|
232
|
+
|
|
229
233
|
function getReplayText(id, presetId) {
|
|
230
234
|
const entries = builder.compactEntries(entriesById[id], presetId);
|
|
231
|
-
if (!entries
|
|
235
|
+
if (!hasSettledReplay(entries)) return '';
|
|
232
236
|
const marks = {
|
|
233
237
|
'claude-code': { user: '❯', agent: '⏺' },
|
|
234
238
|
codex: { user: '›', agent: '•' },
|
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
|
-
}
|