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 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, roles, 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.
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/codex-hooks.js CHANGED
@@ -18,7 +18,7 @@ function readHooksFile(path) {
18
18
  if (!existsSync(path)) return { hooks: {} };
19
19
  const parsed = JSON.parse(readFileSync(path, 'utf8'));
20
20
  if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
21
- throw new Error('Invalid ~/.codex/hooks.json shape');
21
+ throw new Error('Invalid Codex hooks.json shape');
22
22
  }
23
23
  if (!parsed.hooks || typeof parsed.hooks !== 'object' || Array.isArray(parsed.hooks)) parsed.hooks = {};
24
24
  return parsed;
@@ -33,8 +33,8 @@ function stripClideckHooks(groups) {
33
33
  .filter(group => group.hooks.length);
34
34
  }
35
35
 
36
- function installCodexHooks(home, nodePath, helperPath, port) {
37
- const hooksPath = join(home, '.codex', 'hooks.json');
36
+ function installCodexHooks(codexHome, nodePath, helperPath, port) {
37
+ const hooksPath = join(codexHome, 'hooks.json');
38
38
  const doc = readHooksFile(hooksPath);
39
39
 
40
40
  for (const [event, route] of Object.entries(EVENTS)) {
@@ -53,8 +53,8 @@ function installCodexHooks(home, nodePath, helperPath, port) {
53
53
  writeFileSync(hooksPath, JSON.stringify(doc, null, 2) + '\n');
54
54
  }
55
55
 
56
- function removeCodexHooks(home) {
57
- const hooksPath = join(home, '.codex', 'hooks.json');
56
+ function removeCodexHooks(codexHome) {
57
+ const hooksPath = join(codexHome, 'hooks.json');
58
58
  if (!existsSync(hooksPath)) return;
59
59
  const doc = readHooksFile(hooksPath);
60
60
 
@@ -73,9 +73,9 @@ function removeCodexHooks(home) {
73
73
  }
74
74
  }
75
75
 
76
- function codexHooksHealthy(home, helperPath, port) {
76
+ function codexHooksHealthy(codexHome, helperPath, port) {
77
77
  try {
78
- const doc = readHooksFile(join(home, '.codex', 'hooks.json'));
78
+ const doc = readHooksFile(join(codexHome, 'hooks.json'));
79
79
  for (const [event, route] of Object.entries(EVENTS)) {
80
80
  const groups = Array.isArray(doc.hooks?.[event]) ? doc.hooks[event] : [];
81
81
  const expected = commandFor(process.execPath.replace(/\\/g, '/'), helperPath, port, route);
package/config.js CHANGED
@@ -32,52 +32,13 @@ 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: [
78
38
  {
79
39
  id: '1', label: 'Shell', icon: 'terminal', command: defaultShell, enabled: true,
80
40
  defaultPath: '', isAgent: false, canResume: false, resumeCommand: null, sessionIdPattern: null,
41
+ env: {},
81
42
  },
82
43
  ],
83
44
  confirmClose: true,
@@ -88,7 +49,6 @@ const DEFAULTS = {
88
49
  defaultTheme: 'catppuccin-mocha',
89
50
  defaultShell,
90
51
  prompts: [],
91
- roles: [],
92
52
  projects: [],
93
53
  };
94
54
 
@@ -155,6 +115,7 @@ function migrate(cfg) {
155
115
  if (cmd.resumeCommand === undefined) cmd.resumeCommand = preset?.resumeCommand || null;
156
116
  if (cmd.sessionIdPattern === undefined) cmd.sessionIdPattern = preset?.sessionIdPattern || null;
157
117
  if (cmd.outputMarker === undefined) cmd.outputMarker = preset?.outputMarker || null;
118
+ if (!cmd.env || typeof cmd.env !== 'object' || Array.isArray(cmd.env)) cmd.env = {};
158
119
  // Claude Code telemetry is built-in, always on
159
120
  if (preset?.telemetryEnabled === true) cmd.telemetryEnabled = true;
160
121
  else if (preset?.presetId === 'claude-code') cmd.telemetryEnabled = true;
@@ -181,11 +142,11 @@ function migrate(cfg) {
181
142
  isAgent: preset.isAgent, canResume: preset.canResume,
182
143
  resumeCommand: preset.resumeCommand, sessionIdPattern: preset.sessionIdPattern,
183
144
  outputMarker: preset.outputMarker || null,
145
+ env: {},
184
146
  });
185
147
  }
186
148
  }
187
149
  if (!cfg.projects) cfg.projects = [];
188
- if (!cfg.roles) cfg.roles = [];
189
150
  return cfg;
190
151
  }
191
152
 
@@ -194,7 +155,6 @@ function load() {
194
155
  return {
195
156
  ...deepCopy(DEFAULTS),
196
157
  prompts: deepCopy(STARTER_PROMPTS),
197
- roles: deepCopy(STARTER_ROLES),
198
158
  };
199
159
  }
200
160
  try {
package/handlers.js CHANGED
@@ -19,7 +19,8 @@ function clientPresets() {
19
19
  }
20
20
  function filterClientCommands(commands) {
21
21
  const allowedPresetIds = new Set(clientPresets().map(p => p.presetId));
22
- return (commands || []).filter(cmd => !cmd.presetId || allowedPresetIds.has(cmd.presetId));
22
+ const knownPresetIds = new Set(presets.map(p => p.presetId));
23
+ return (commands || []).filter(cmd => !cmd.presetId || allowedPresetIds.has(cmd.presetId) || !knownPresetIds.has(cmd.presetId));
23
24
  }
24
25
  const transcript = require('./transcript');
25
26
  const plugins = require('./plugin-loader');
@@ -70,6 +71,35 @@ function getInstalledVersion(bin) {
70
71
  return '';
71
72
  }
72
73
 
74
+ function presetForCommand(cmd) {
75
+ if (cmd?.presetId) {
76
+ const preset = presets.find(p => p.presetId === cmd.presetId);
77
+ if (preset) return preset;
78
+ }
79
+ const bin = binName(cmd?.command);
80
+ return presets.find(p => binName(p.command) === bin);
81
+ }
82
+
83
+ function commandEnv(cmd) {
84
+ return cmd?.env && typeof cmd.env === 'object' && !Array.isArray(cmd.env) ? cmd.env : {};
85
+ }
86
+
87
+ function expandHomePath(value) {
88
+ const text = String(value || '').trim();
89
+ if (!text) return '';
90
+ if (text === '~') return os.homedir();
91
+ if (text.startsWith('~/') || text.startsWith('~\\')) return join(os.homedir(), text.slice(2));
92
+ return text;
93
+ }
94
+
95
+ function configRootFor(preset, cmd) {
96
+ const env = commandEnv(cmd);
97
+ if (preset?.presetId === 'claude-code') return expandHomePath(env.CLAUDE_CONFIG_DIR) || join(os.homedir(), '.claude');
98
+ if (preset?.presetId === 'codex') return expandHomePath(env.CODEX_HOME) || join(os.homedir(), '.codex');
99
+ if (preset?.presetId === 'gemini-cli') return join(expandHomePath(env.GEMINI_CLI_HOME) || os.homedir(), '.gemini');
100
+ return os.homedir();
101
+ }
102
+
73
103
  function checkRemoteUpdate(ws) {
74
104
  const now = Date.now();
75
105
  if (remoteUpdateCache && now - remoteUpdateCheckedAt < REMOTE_UPDATE_INTERVAL) {
@@ -154,11 +184,11 @@ function codexHooksFeatureEnabled(content) {
154
184
  return false;
155
185
  }
156
186
 
157
- function codexConfigLooksHealthy(content, port) {
187
+ function codexConfigLooksHealthy(content, port, codexHome) {
158
188
  if (!content.includes('[otel]') || !content.includes(`localhost:${port}`)) return false;
159
189
  const codexHookPath = join(__dirname, 'bin', 'codex-hook.js').replace(/\\/g, '/');
160
190
  if (!codexHooksFeatureEnabled(content)) return false;
161
- if (!codexHooksHealthy(os.homedir(), codexHookPath, port)) return false;
191
+ if (!codexHooksHealthy(codexHome, codexHookPath, port)) return false;
162
192
  const notifyLine = content.match(/^\s*notify\s*=\s*\[(.+)\]\s*$/m)?.[1] || '';
163
193
  if (!notifyLine.includes('notify-helper')) return false;
164
194
  const quoted = [...notifyLine.matchAll(/"([^"]+)"/g)].map(m => m[1]);
@@ -167,7 +197,6 @@ function codexConfigLooksHealthy(content, port) {
167
197
  }
168
198
 
169
199
  function detectTelemetryConfig(c) {
170
- const home = os.homedir();
171
200
  const port = String(PORT);
172
201
  let changed = false;
173
202
  const attemptedRepairs = new Set();
@@ -175,14 +204,13 @@ function detectTelemetryConfig(c) {
175
204
  for (let pass = 0; pass < 2; pass++) {
176
205
  let repairedAny = false;
177
206
  for (const cmd of c.commands || []) {
178
- const bin = binName(cmd.command);
179
- const preset = presets.find(p => binName(p.command) === bin);
207
+ const preset = presetForCommand(cmd);
180
208
  if (!preset) continue;
181
209
  let detected = false;
182
210
  let reason = '';
183
211
  if (preset.presetId === 'claude-code') {
184
212
  try {
185
- const s = JSON.parse(readFileSync(join(home, '.claude', 'settings.json'), 'utf8'));
213
+ const s = JSON.parse(readFileSync(join(configRootFor(preset, cmd), 'settings.json'), 'utf8'));
186
214
  const hooks = s.hooks || {};
187
215
  detected = hasExistingHook(hooks.UserPromptSubmit, 'claude-hook.js', 'start')
188
216
  && hasExistingHook(hooks.Stop, 'claude-hook.js', 'stop')
@@ -193,13 +221,14 @@ function detectTelemetryConfig(c) {
193
221
  } catch {}
194
222
  } else if (preset.presetId === 'codex') {
195
223
  try {
196
- const content = readFileSync(join(home, '.codex', 'config.toml'), 'utf8');
197
- detected = codexConfigLooksHealthy(content, port);
224
+ const codexHome = configRootFor(preset, cmd);
225
+ const content = readFileSync(join(codexHome, 'config.toml'), 'utf8');
226
+ detected = codexConfigLooksHealthy(content, port, codexHome);
198
227
  if (!detected) reason = 'Needs re-patch';
199
228
  } catch {}
200
229
  } else if (preset.presetId === 'gemini-cli') {
201
230
  try {
202
- const s = JSON.parse(readFileSync(join(home, '.gemini', 'settings.json'), 'utf8'));
231
+ const s = JSON.parse(readFileSync(join(configRootFor(preset, cmd), 'settings.json'), 'utf8'));
203
232
  const hooks = s.hooks || {};
204
233
  detected = hasExistingHook(hooks.BeforeAgent, 'gemini-hook.js', 'start')
205
234
  && hasExistingHook(hooks.AfterAgent, 'gemini-hook.js', 'stop')
@@ -214,9 +243,9 @@ function detectTelemetryConfig(c) {
214
243
  if (preset.available && preset.minVersion && !preset.versionOk) {
215
244
  detected = false;
216
245
  reason = `Update required (${preset.minVersion}+)`;
217
- } else if (!detected && cmd.telemetryEnabled && preset.telemetryAutoSetup && preset.available && preset.versionOk && !attemptedRepairs.has(preset.presetId)) {
218
- attemptedRepairs.add(preset.presetId);
219
- const repaired = applyTelemetryConfig(preset);
246
+ } else if (!detected && cmd.telemetryEnabled && preset.telemetryAutoSetup && preset.available && preset.versionOk && !attemptedRepairs.has(cmd.id || preset.presetId)) {
247
+ attemptedRepairs.add(cmd.id || preset.presetId);
248
+ const repaired = applyTelemetryConfig(preset, cmd);
220
249
  if (repaired.success) {
221
250
  repairedAny = true;
222
251
  continue;
@@ -360,11 +389,12 @@ function onConnection(ws) {
360
389
  }
361
390
 
362
391
  case 'telemetry.autosetup': {
363
- const preset = presets.find(p => p.presetId === msg.presetId);
392
+ const targetCmd = msg.commandId ? cfg.commands.find(c => c.id === msg.commandId) : null;
393
+ const preset = targetCmd ? presetForCommand(targetCmd) : presets.find(p => p.presetId === msg.presetId);
364
394
  if (!preset?.telemetryAutoSetup) break;
365
- const result = applyTelemetryConfig(preset);
395
+ const result = applyTelemetryConfig(preset, targetCmd);
366
396
  for (const cmd of cfg.commands) {
367
- if (binName(cmd.command) === binName(preset.command)) {
397
+ if (targetCmd ? cmd.id === targetCmd.id : presetForCommand(cmd)?.presetId === preset.presetId) {
368
398
  cmd.telemetryEnabled = result.success;
369
399
  cmd.telemetryStatus = result.success ? { ok: true } : { ok: false, error: result.message };
370
400
  // Enable the agent when setup succeeds, disable if it fails
@@ -375,7 +405,8 @@ function onConnection(ws) {
375
405
  sessions.broadcast({ type: 'config', config: configForClient() });
376
406
  ws.send(JSON.stringify({
377
407
  type: 'telemetry.autosetup.result',
378
- presetId: msg.presetId,
408
+ presetId: preset.presetId,
409
+ commandId: msg.commandId || null,
379
410
  success: result.success,
380
411
  output: result.message,
381
412
  }));
@@ -383,18 +414,19 @@ function onConnection(ws) {
383
414
  }
384
415
 
385
416
  case 'telemetry.configure': {
386
- const preset = presets.find(p => p.presetId === msg.presetId);
417
+ const targetCmd = msg.commandId ? cfg.commands.find(c => c.id === msg.commandId) : null;
418
+ const preset = targetCmd ? presetForCommand(targetCmd) : presets.find(p => p.presetId === msg.presetId);
387
419
  if (!preset) break;
388
420
  const enable = !!msg.enable;
389
421
  let result;
390
422
  if (enable) {
391
- result = applyTelemetryConfig(preset);
423
+ result = applyTelemetryConfig(preset, targetCmd);
392
424
  } else {
393
- result = removeTelemetryConfig(preset);
425
+ result = removeTelemetryConfig(preset, targetCmd);
394
426
  }
395
427
  // Update all matching commands in config
396
428
  for (const cmd of cfg.commands) {
397
- if (binName(cmd.command) === binName(preset.command)) {
429
+ if (targetCmd ? cmd.id === targetCmd.id : presetForCommand(cmd)?.presetId === preset.presetId) {
398
430
  cmd.telemetryEnabled = enable && result.success;
399
431
  cmd.telemetryStatus = enable
400
432
  ? (result.success ? { ok: true } : { ok: false, error: result.message })
@@ -576,13 +608,12 @@ function onConnection(ws) {
576
608
  }
577
609
 
578
610
  // Deterministic telemetry config writers per agent — no AI, no YOLO
579
- function applyTelemetryConfig(preset) {
611
+ function applyTelemetryConfig(preset, cmd = null) {
580
612
  const port = String(PORT);
581
- const home = os.homedir();
582
613
 
583
614
  try {
584
615
  if (preset.presetId === 'claude-code') {
585
- const configPath = join(home, '.claude', 'settings.json');
616
+ const configPath = join(configRootFor(preset, cmd), 'settings.json');
586
617
  let settings = {};
587
618
  if (existsSync(configPath)) {
588
619
  try { settings = JSON.parse(readFileSync(configPath, 'utf8')); } catch {}
@@ -608,11 +639,12 @@ function applyTelemetryConfig(preset) {
608
639
  settings.hooks = hooks;
609
640
  mkdirSync(dirname(configPath), { recursive: true });
610
641
  writeFileSync(configPath, JSON.stringify(settings, null, 2) + '\n');
611
- return { success: true, message: 'Added hooks to ~/.claude/settings.json — Claude will ask for one-time approval' };
642
+ return { success: true, message: `Added hooks to ${configPath} — Claude will ask for one-time approval` };
612
643
  }
613
644
 
614
645
  if (preset.presetId === 'codex') {
615
- const configPath = join(home, '.codex', 'config.toml');
646
+ const codexHome = configRootFor(preset, cmd);
647
+ const configPath = join(codexHome, 'config.toml');
616
648
  let content = '';
617
649
  if (existsSync(configPath)) content = readFileSync(configPath, 'utf8');
618
650
  const hasOtel = content.includes('[otel]');
@@ -620,7 +652,7 @@ function applyTelemetryConfig(preset) {
620
652
  const hasNotify = /^\s*notify\s*=.*notify-helper/m.test(content);
621
653
  const hasWrongOtel = content.includes(`endpoint = "http://localhost:${port}/v1/logs"`);
622
654
  const codexHookPath = join(__dirname, 'bin', 'codex-hook.js').replace(/\\/g, '/');
623
- const hasHooks = codexHooksFeatureEnabled(content) && codexHooksHealthy(home, codexHookPath, port);
655
+ const hasHooks = codexHooksFeatureEnabled(content) && codexHooksHealthy(codexHome, codexHookPath, port);
624
656
  if (hasOtel && hasCurrentOtel && hasNotify && !hasWrongOtel && hasHooks) {
625
657
  return { success: true, message: 'Already configured' };
626
658
  }
@@ -630,12 +662,12 @@ function applyTelemetryConfig(preset) {
630
662
  if (!valid.ok) return { success: false, message: valid.error };
631
663
  mkdirSync(dirname(configPath), { recursive: true });
632
664
  writeFileSync(configPath, nextContent);
633
- installCodexHooks(home, process.execPath.replace(/\\/g, '/'), codexHookPath, port);
665
+ installCodexHooks(codexHome, process.execPath.replace(/\\/g, '/'), codexHookPath, port);
634
666
  return { success: true, message: 'Configured. If Codex shows "2 hooks need review", open /hooks and approve the CliDeck hooks once.' };
635
667
  }
636
668
 
637
669
  if (preset.presetId === 'gemini-cli') {
638
- const configPath = join(home, '.gemini', 'settings.json');
670
+ const configPath = join(configRootFor(preset, cmd), 'settings.json');
639
671
  let settings = {};
640
672
  if (existsSync(configPath)) {
641
673
  try { settings = JSON.parse(readFileSync(configPath, 'utf8')); } catch {}
@@ -665,7 +697,7 @@ function applyTelemetryConfig(preset) {
665
697
  if (settings.telemetry?.target === 'local' && /localhost:\d+/.test(String(settings.telemetry?.otlpEndpoint || ''))) delete settings.telemetry;
666
698
  mkdirSync(dirname(configPath), { recursive: true });
667
699
  writeFileSync(configPath, JSON.stringify(settings, null, 2) + '\n');
668
- return { success: true, message: 'Added CliDeck hooks to ~/.gemini/settings.json' };
700
+ return { success: true, message: `Added CliDeck hooks to ${configPath}` };
669
701
  }
670
702
 
671
703
  if (preset.presetId === 'opencode') {
@@ -684,12 +716,10 @@ function applyTelemetryConfig(preset) {
684
716
  }
685
717
  }
686
718
 
687
- function removeTelemetryConfig(preset) {
688
- const home = os.homedir();
689
-
719
+ function removeTelemetryConfig(preset, cmd = null) {
690
720
  try {
691
721
  if (preset.presetId === 'claude-code') {
692
- const configPath = join(home, '.claude', 'settings.json');
722
+ const configPath = join(configRootFor(preset, cmd), 'settings.json');
693
723
  if (!existsSync(configPath)) return { success: true, message: 'No config file to clean' };
694
724
  let settings = {};
695
725
  try { settings = JSON.parse(readFileSync(configPath, 'utf8')); } catch {}
@@ -702,23 +732,24 @@ function removeTelemetryConfig(preset) {
702
732
  }
703
733
  if (!Object.keys(settings.hooks).length) delete settings.hooks;
704
734
  writeFileSync(configPath, JSON.stringify(settings, null, 2) + '\n');
705
- return { success: true, message: 'Removed CliDeck hooks from ~/.claude/settings.json' };
735
+ return { success: true, message: `Removed CliDeck hooks from ${configPath}` };
706
736
  }
707
737
 
708
738
  if (preset.presetId === 'codex') {
709
- const configPath = join(home, '.codex', 'config.toml');
739
+ const codexHome = configRootFor(preset, cmd);
740
+ const configPath = join(codexHome, 'config.toml');
710
741
  if (!existsSync(configPath)) return { success: true, message: 'No config file to clean' };
711
742
  let content = readFileSync(configPath, 'utf8');
712
743
  content = content.replace(/\n?\[otel\][^\[]*/, '');
713
744
  content = content.replace(/\n?notify\s*=\s*\[.*?notify-helper.*?\]\s*/g, '');
714
745
  content = content.replace(/\n?codex_hooks\s*=\s*(true|false)\s*/g, '\n');
715
746
  writeFileSync(configPath, content.trimEnd() + '\n');
716
- removeCodexHooks(home);
717
- return { success: true, message: 'Removed otel + CliDeck hooks from ~/.codex config' };
747
+ removeCodexHooks(codexHome);
748
+ return { success: true, message: `Removed otel + CliDeck hooks from ${configPath}` };
718
749
  }
719
750
 
720
751
  if (preset.presetId === 'gemini-cli') {
721
- const configPath = join(home, '.gemini', 'settings.json');
752
+ const configPath = join(configRootFor(preset, cmd), 'settings.json');
722
753
  if (!existsSync(configPath)) return { success: true, message: 'No config file to clean' };
723
754
  let settings = {};
724
755
  try { settings = JSON.parse(readFileSync(configPath, 'utf8')); } catch {}
@@ -731,7 +762,7 @@ function removeTelemetryConfig(preset) {
731
762
  if (settings.hooks && !Object.keys(settings.hooks).length) delete settings.hooks;
732
763
  if (settings.telemetry?.target === 'local' && /localhost:\d+/.test(String(settings.telemetry?.otlpEndpoint || ''))) delete settings.telemetry;
733
764
  writeFileSync(configPath, JSON.stringify(settings, null, 2) + '\n');
734
- return { success: true, message: 'Removed CliDeck hooks from ~/.gemini/settings.json' };
765
+ return { success: true, message: `Removed CliDeck hooks from ${configPath}` };
735
766
  }
736
767
 
737
768
  if (preset.presetId === 'opencode') {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clideck",
3
- "version": "1.31.3",
3
+ "version": "1.31.5",
4
4
  "description": "One screen for all your AI coding agents — run, monitor, and manage multiple CLI agents from a single browser tab",
5
5
  "main": "server.js",
6
6
  "bin": {
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, roleName: s.roleName || null, working: state.startsWith('1:') };
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, roleName: s.roleName || null, working: (sessionStatus.get(id) || '').startsWith('1:'),
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/app.js CHANGED
@@ -16,6 +16,19 @@ import { renderPrompts } from './prompts.js';
16
16
  const shownAgentHealthToasts = new Set();
17
17
  let reconnectReplaySkip = null;
18
18
 
19
+ function normalizeTerminalHistoryText(text) {
20
+ return `${text || ''}\n`.replace(/\r?\n/g, '\r\n');
21
+ }
22
+
23
+ function presetForCommand(cmd) {
24
+ if (cmd?.presetId) {
25
+ const byId = state.presets.find(p => p.presetId === cmd.presetId);
26
+ if (byId) return byId;
27
+ }
28
+ const bin = binName(cmd?.command);
29
+ return state.presets.find(p => binName(p.command) === bin);
30
+ }
31
+
19
32
  function connect() {
20
33
  const wsProtocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
21
34
  state.ws = new WebSocket(`${wsProtocol}//${location.host}`);
@@ -67,7 +80,9 @@ function connect() {
67
80
  }
68
81
  msg.list.forEach(s => addTerminal(s.id, s.name, s.themeId, s.commandId, s.projectId, s.muted, s.lastPreview, s.presetId));
69
82
  if (!state.active || !state.terms.has(state.active)) {
70
- if (msg.list.length) select(msg.list[0].id);
83
+ const savedActive = localStorage.getItem('clideck.activeSessionId');
84
+ const nextId = savedActive && liveIds.has(savedActive) ? savedActive : msg.list[0]?.id;
85
+ if (nextId) select(nextId);
71
86
  }
72
87
  }
73
88
  break;
@@ -110,7 +125,8 @@ function connect() {
110
125
  case 'session.history': {
111
126
  const entry = state.terms.get(msg.id);
112
127
  if (msg.replay && reconnectReplaySkip?.has(msg.id) && entry) break;
113
- if (entry && !entry.queue(msg.text + '\n')) entry.term.write(msg.text + '\n');
128
+ const historyText = normalizeTerminalHistoryText(msg.text);
129
+ if (entry && !entry.queue(historyText)) entry.term.write(historyText);
114
130
  updatePreview(msg.id);
115
131
  break;
116
132
  }
@@ -209,7 +225,7 @@ function connect() {
209
225
  break;
210
226
  }
211
227
  case 'telemetry.autosetup.result': {
212
- const toast = document.querySelector(`[data-setup-preset="${msg.presetId}"]`);
228
+ const toast = document.querySelector(msg.commandId ? `[data-command-id="${msg.commandId}"]` : `[data-setup-preset="${msg.presetId}"]`);
213
229
  if (!toast) break;
214
230
  const actionsEl = toast.querySelector('.setup-actions');
215
231
  if (msg.success) {
@@ -240,7 +256,7 @@ function connect() {
240
256
  toast.remove();
241
257
  };
242
258
  } else {
243
- shownSetup.delete(msg.presetId);
259
+ shownSetup.delete(msg.commandId || msg.presetId);
244
260
  const btn = toast.querySelector('.auto-setup-btn');
245
261
  btn.textContent = 'Failed — configure manually';
246
262
  btn.className = 'auto-setup-btn flex-1 px-3 py-2 text-xs font-medium bg-red-600/20 text-red-400 border border-red-500/30 rounded-lg cursor-default';
@@ -469,11 +485,12 @@ function showTelemetrySetup(commandId, sessionId) {
469
485
  if (!cmd) return;
470
486
  // Skip if telemetry is already configured via settings
471
487
  if (cmd.telemetryEnabled && cmd.telemetryStatus?.ok) return;
472
- const bin = binName(cmd.command);
473
- const preset = state.presets.find(p => binName(p.command) === bin);
488
+ const preset = presetForCommand(cmd);
489
+ if (!preset) return;
474
490
  const setupRaw = preset.telemetrySetup || preset.pluginSetup;
475
- if (!setupRaw || shownSetup.has(preset.presetId)) return;
476
- shownSetup.add(preset.presetId);
491
+ const setupKey = commandId || preset.presetId;
492
+ if (!setupRaw || shownSetup.has(setupKey)) return;
493
+ shownSetup.add(setupKey);
477
494
 
478
495
  const port = location.port || '4000';
479
496
  const setupText = setupRaw.replace(/\{\{port\}\}/g, port);
@@ -512,7 +529,7 @@ function showTelemetrySetup(commandId, sessionId) {
512
529
  </div>`;
513
530
 
514
531
  toast.querySelectorAll('.dismiss-btn').forEach(b => b.onclick = () => {
515
- shownSetup.delete(preset.presetId);
532
+ shownSetup.delete(setupKey);
516
533
  toast.remove();
517
534
  });
518
535
 
@@ -522,7 +539,7 @@ function showTelemetrySetup(commandId, sessionId) {
522
539
  autoBtn.disabled = true;
523
540
  autoBtn.innerHTML = `<svg class="w-3.5 h-3.5 inline animate-spin -mt-px mr-1.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M12 2a10 10 0 0 1 10 10"/></svg>Configuring…`;
524
541
  autoBtn.className = 'auto-setup-btn flex-1 px-3 py-2 text-xs font-medium bg-slate-700 text-slate-300 rounded-lg cursor-wait';
525
- send({ type: 'telemetry.autosetup', presetId: preset.presetId });
542
+ send({ type: 'telemetry.autosetup', presetId: preset.presetId, commandId });
526
543
  };
527
544
  }
528
545