clideck 1.31.17 → 1.31.18

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
@@ -18,9 +18,9 @@
18
18
  <img src="assets/clideck-themes.jpg" width="720" alt="clideck dashboard">
19
19
  </p>
20
20
 
21
- clideck is a local app for running multiple AI coding agents without juggling terminals. Claude Code, Codex, Gemini CLI, and OpenCode all live in one browser window with a chat-style sidebar, live status, message previews, session resume, and projects to keep things organized. an autopilot routes work between agents automatically, and an E2E encrypted mobile relay gives full control over all agents from a phone.
21
+ clideck is a local app for running multiple AI coding agents without juggling terminals. Claude Code, Codex, Gemini CLI, OpenCode, and Pi all live in one browser window with a chat-style sidebar, live status, message previews, session resume, and projects to keep things organized. an autopilot routes work between agents automatically, and an E2E encrypted mobile relay gives full control over all agents from a phone.
22
22
 
23
- the main problem with using multiple agents is not starting them. it is managing them. terminals pile up, finished work gets missed, good sessions disappear after a restart. clideck does not sit in the middle rewriting prompts or output - it only watches lightweight status signals (OpenTelemetry) so it can tell which agent is working, which is idle, and which is waiting. everything runs locally, no data leaves your machine.
23
+ the main problem with using multiple agents is not starting them. it is managing them. terminals pile up, finished work gets missed, good sessions disappear after a restart. clideck does not sit in the middle rewriting prompts or output - it only watches lightweight status signals from each agent so it can tell which agent is working, which is idle, and which is waiting. everything runs locally, no data leaves your machine.
24
24
 
25
25
  ## Why this exists
26
26
 
@@ -47,7 +47,7 @@ clideck --port 4001
47
47
 
48
48
  ## What makes it useful
49
49
 
50
- **Live status** - see which agent is working and which is waiting. Status detection for Claude Code, Codex, Gemini CLI, and OpenCode.
50
+ **Live status** - see which agent is working and which is waiting. Status detection for Claude Code, Codex, Gemini CLI, OpenCode, and Pi.
51
51
 
52
52
  **Session resume** - close the lid, reopen tomorrow, pick up where things left off. each agent's session ID is captured automatically.
53
53
 
@@ -79,7 +79,7 @@ If project or session names contain spaces, quote the whole target. The target i
79
79
 
80
80
  ## Supported agents
81
81
 
82
- Claude Code, Codex, Gemini CLI, OpenCode, Shell, and any other terminal tool.
82
+ Claude Code, Codex, Gemini CLI, OpenCode, Pi, Shell, and any other terminal tool.
83
83
 
84
84
  ## Also
85
85
 
@@ -12,7 +12,7 @@
12
12
  "sessionIdPattern": "Session ID:\\s+([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})",
13
13
  "outputMarker": "⏺",
14
14
  "telemetryConfigPath": "~/.claude/settings.json",
15
- "telemetrySetup": "Required for working/idle status, Autopilot, notifications, and mobile remote.\n\nCliDeck will add start/stop hooks to ~/.claude/settings.json. Claude will ask for one-time approval on next launch.",
15
+ "telemetrySetup": "Required for working/idle status, Autopilot, notifications, and mobile remote.\n\nCliDeck will add lifecycle hooks to ~/.claude/settings.json. Claude will ask for one-time approval on next launch.",
16
16
  "telemetryAutoSetup": {
17
17
  "label": "Patch Claude"
18
18
  },
@@ -85,6 +85,24 @@
85
85
  "label": "Install plugin"
86
86
  }
87
87
  },
88
+ {
89
+ "presetId": "pi",
90
+ "name": "Pi",
91
+ "icon": "/img/pi.svg",
92
+ "command": "pi",
93
+ "minVersion": "0.79.3",
94
+ "installCmd": "npm install -g --ignore-scripts @earendil-works/pi-coding-agent",
95
+ "isAgent": true,
96
+ "canResume": true,
97
+ "resumeCommand": "pi --session {{sessionId}}",
98
+ "sessionIdPattern": null,
99
+ "outputMarker": "•",
100
+ "telemetryConfigPath": "~/.pi/agent/extensions/clideck-bridge.ts",
101
+ "telemetrySetup": "Required for working/idle status, resume, Autopilot, notifications, and mobile remote.\n\nCliDeck will install a small Pi extension to ~/.pi/agent/extensions/clideck-bridge.ts.",
102
+ "telemetryAutoSetup": {
103
+ "label": "Install extension"
104
+ }
105
+ },
88
106
  {
89
107
  "presetId": "clideck-agent",
90
108
  "name": "Clideck Agent",
@@ -15,6 +15,9 @@ process.stdin.on('end', () => {
15
15
  const body = JSON.stringify({
16
16
  clideck_id: process.env.CLIDECK_SESSION_ID || '',
17
17
  session_id: hook.session_id || '',
18
+ hook_event_name: hook.hook_event_name || '',
19
+ source: hook.source || '',
20
+ reason: hook.reason || '',
18
21
  payload: stdin.trim() || undefined,
19
22
  });
20
23
  const req = http.request({
package/handlers.js CHANGED
@@ -21,7 +21,11 @@ function clientPresets() {
21
21
  function filterClientCommands(commands) {
22
22
  const allowedPresetIds = new Set(clientPresets().map(p => p.presetId));
23
23
  const knownPresetIds = new Set(presets.map(p => p.presetId));
24
- return (commands || []).filter(cmd => !cmd.presetId || allowedPresetIds.has(cmd.presetId) || !knownPresetIds.has(cmd.presetId));
24
+ return (commands || []).filter(cmd => {
25
+ if (cmd.presetId && !allowedPresetIds.has(cmd.presetId) && knownPresetIds.has(cmd.presetId)) return false;
26
+ const preset = cmd.presetId ? presets.find(p => p.presetId === cmd.presetId) : null;
27
+ return !(preset?.available === false && String(cmd.command || '').trim() === String(preset.command || '').trim());
28
+ });
25
29
  }
26
30
  const transcript = require('./transcript');
27
31
  const plugins = require('./plugin-loader');
@@ -93,6 +97,7 @@ function configRootFor(preset, cmd) {
93
97
  if (preset?.presetId === 'claude-code') return expandHomePath(env.CLAUDE_CONFIG_DIR) || join(os.homedir(), '.claude');
94
98
  if (preset?.presetId === 'codex') return expandHomePath(env.CODEX_HOME) || join(os.homedir(), '.codex');
95
99
  if (preset?.presetId === 'gemini-cli') return join(expandHomePath(env.GEMINI_CLI_HOME) || os.homedir(), '.gemini');
100
+ if (preset?.presetId === 'pi') return expandHomePath(env.PI_CODING_AGENT_DIR) || join(os.homedir(), '.pi', 'agent');
96
101
  return os.homedir();
97
102
  }
98
103
 
@@ -173,6 +178,14 @@ function hasExistingHook(arr, hookFile, route) {
173
178
  }));
174
179
  }
175
180
 
181
+ function hasAnyExistingHook(hooks, hookFile) {
182
+ return Object.values(hooks || {}).some(arr => arr?.some(h => h.hooks?.some(x => {
183
+ if (!x.command?.includes(hookFile)) return false;
184
+ const hookPath = extractQuotedPath(x.command, hookFile);
185
+ return !!hookPath && existsSync(hookPath);
186
+ })));
187
+ }
188
+
176
189
  function codexHooksFeatureEnabled(content) {
177
190
  let inFeatures = false;
178
191
  for (const line of String(content || '').split(/\r?\n/)) {
@@ -211,6 +224,23 @@ function opencodeBridgeLooksHealthy() {
211
224
  }
212
225
  }
213
226
 
227
+ function piBridgePath(cmd) {
228
+ return join(configRootFor({ presetId: 'pi' }, cmd), 'extensions', 'clideck-bridge.ts');
229
+ }
230
+
231
+ function piBridgeLooksHealthy(cmd) {
232
+ const bridgePath = piBridgePath(cmd);
233
+ if (!existsSync(bridgePath)) return false;
234
+ try {
235
+ const content = readFileSync(bridgePath, 'utf8');
236
+ return content.includes('/hook/pi')
237
+ && content.includes('CLIDECK_SESSION_ID')
238
+ && content.includes('sessionManager.getSessionId');
239
+ } catch {
240
+ return false;
241
+ }
242
+ }
243
+
214
244
  function detectTelemetryConfig(c) {
215
245
  const port = String(PORT);
216
246
  let changed = false;
@@ -223,15 +253,23 @@ function detectTelemetryConfig(c) {
223
253
  if (!preset) continue;
224
254
  let detected = false;
225
255
  let reason = '';
256
+ let repairAllowed = cmd.telemetrySetupConsent === true;
226
257
  if (preset.presetId === 'claude-code') {
227
258
  try {
228
259
  const s = JSON.parse(readFileSync(join(configRootFor(preset, cmd), 'settings.json'), 'utf8'));
229
260
  const hooks = s.hooks || {};
261
+ repairAllowed = repairAllowed || hasAnyExistingHook(hooks, 'claude-hook.js');
230
262
  detected = hasExistingHook(hooks.UserPromptSubmit, 'claude-hook.js', 'start')
231
263
  && hasExistingHook(hooks.Stop, 'claude-hook.js', 'stop')
232
264
  && hasExistingHook(hooks.StopFailure, 'claude-hook.js', 'stop')
265
+ && hasExistingHook(hooks.SessionStart, 'claude-hook.js', 'session-start')
266
+ && hasExistingHook(hooks.SessionEnd, 'claude-hook.js', 'session-end')
233
267
  && hasExistingHook(hooks.PreToolUse, 'claude-hook.js', 'menu')
234
268
  && hooks.Notification?.some(h => h.matcher === 'idle_prompt' && hasExistingHook([h], 'claude-hook.js', 'idle'));
269
+ if (detected && cmd.telemetrySetupConsent !== true) {
270
+ cmd.telemetrySetupConsent = true;
271
+ changed = true;
272
+ }
235
273
  if (!detected) reason = 'Needs re-patch';
236
274
  } catch {}
237
275
  } else if (preset.presetId === 'codex') {
@@ -254,11 +292,14 @@ function detectTelemetryConfig(c) {
254
292
  } else if (preset.presetId === 'opencode') {
255
293
  detected = opencodeBridgeLooksHealthy();
256
294
  if (!detected) reason = 'Needs re-patch';
295
+ } else if (preset.presetId === 'pi') {
296
+ detected = piBridgeLooksHealthy(cmd);
297
+ if (!detected) reason = 'Needs re-patch';
257
298
  } else { continue; }
258
299
  if (preset.available && preset.minVersion && !preset.versionOk) {
259
300
  detected = false;
260
301
  reason = `Update required (${preset.minVersion}+)`;
261
- } else if (!detected && cmd.telemetryEnabled && cmd.telemetrySetupConsent === true && preset.telemetryAutoSetup && preset.available && preset.versionOk && !attemptedRepairs.has(cmd.id || preset.presetId)) {
302
+ } else if (!detected && cmd.telemetryEnabled && repairAllowed && preset.telemetryAutoSetup && preset.available && preset.versionOk && !attemptedRepairs.has(cmd.id || preset.presetId)) {
262
303
  attemptedRepairs.add(cmd.id || preset.presetId);
263
304
  const repaired = applyTelemetryConfig(preset, cmd);
264
305
  if (repaired.success) {
@@ -364,6 +405,7 @@ function onConnection(ws) {
364
405
  if (choices) transcript.commitAgentCandidate(msg.id, sess.presetId);
365
406
  if (key !== (sess._menuKey || '')) {
366
407
  sess._menuKey = key;
408
+ sess._menuStartsWork = !(sess.presetId === 'claude-code' && !msg.menuVersion);
367
409
  sessions.broadcast({ type: 'session.menu', id: msg.id, choices: choices || [] });
368
410
  if (choices) {
369
411
  if (sess.presetId === 'claude-code' && msg.menuVersion) sess._menuActiveVersion = msg.menuVersion;
@@ -387,6 +429,7 @@ function onConnection(ws) {
387
429
  checkAvailability();
388
430
  if (detectTelemetryConfig(cfg)) config.save(cfg);
389
431
  ws.send(JSON.stringify({ type: 'presets', presets: clientPresets() }));
432
+ ws.send(JSON.stringify({ type: 'config', config: configForClient() }));
390
433
  break;
391
434
 
392
435
  case 'config.update':
@@ -409,6 +452,16 @@ function onConnection(ws) {
409
452
  const targetCmd = msg.commandId ? cfg.commands.find(c => c.id === msg.commandId) : null;
410
453
  const preset = targetCmd ? presetForCommand(targetCmd) : presets.find(p => p.presetId === msg.presetId);
411
454
  if (!preset?.telemetryAutoSetup) break;
455
+ if (preset.available === false) {
456
+ ws.send(JSON.stringify({
457
+ type: 'telemetry.autosetup.result',
458
+ presetId: preset.presetId,
459
+ commandId: msg.commandId || null,
460
+ success: false,
461
+ output: `${preset.name} is not installed`,
462
+ }));
463
+ break;
464
+ }
412
465
  const result = applyTelemetryConfig(preset, targetCmd);
413
466
  for (const cmd of cfg.commands) {
414
467
  if (targetCmd ? cmd.id === targetCmd.id : presetForCommand(cmd)?.presetId === preset.presetId) {
@@ -709,18 +762,28 @@ function applyTelemetryConfig(preset, cmd = null) {
709
762
  const hookCmd = (route) => `"${process.execPath.replace(/\\/g, '/')}" "${join(__dirname, 'bin', 'claude-hook.js').replace(/\\/g, '/')}" ${port} ${route}`;
710
763
  const clideckHook = (route) => ({ hooks: [{ type: 'command', command: hookCmd(route) }] });
711
764
  const hasClideck = (arr, path) => arr?.some(h => h.hooks?.some(x => x.command === hookCmd(path)));
712
- if (hasClideck(hooks.UserPromptSubmit, 'start') && hasClideck(hooks.Stop, 'stop') && hasClideck(hooks.StopFailure, 'stop') && hasClideck(hooks.PreToolUse, 'menu') && hooks.Notification?.some(h => h.matcher === 'idle_prompt' && h.hooks?.some(x => x.command === hookCmd('idle')))) {
765
+ if (hasClideck(hooks.UserPromptSubmit, 'start')
766
+ && hasClideck(hooks.Stop, 'stop')
767
+ && hasClideck(hooks.StopFailure, 'stop')
768
+ && hasClideck(hooks.SessionStart, 'session-start')
769
+ && hasClideck(hooks.SessionEnd, 'session-end')
770
+ && hasClideck(hooks.PreToolUse, 'menu')
771
+ && hooks.Notification?.some(h => h.matcher === 'idle_prompt' && h.hooks?.some(x => x.command === hookCmd('idle')))) {
713
772
  return { success: true, message: 'Already configured' };
714
773
  }
715
774
  const stripOld = (arr) => (arr || []).filter(h => !h.hooks?.some(x => x.url?.includes('/hook/claude/') || x.command?.includes('claude-hook.js')));
716
775
  hooks.UserPromptSubmit = stripOld(hooks.UserPromptSubmit);
717
776
  hooks.Stop = stripOld(hooks.Stop);
718
777
  hooks.StopFailure = stripOld(hooks.StopFailure);
778
+ hooks.SessionStart = stripOld(hooks.SessionStart);
779
+ hooks.SessionEnd = stripOld(hooks.SessionEnd);
719
780
  hooks.PreToolUse = stripOld(hooks.PreToolUse);
720
781
  hooks.Notification = stripOld(hooks.Notification);
721
782
  if (!hasClideck(hooks.UserPromptSubmit, 'start')) hooks.UserPromptSubmit = [...(hooks.UserPromptSubmit || []), clideckHook('start')];
722
783
  if (!hasClideck(hooks.Stop, 'stop')) hooks.Stop = [...(hooks.Stop || []), clideckHook('stop')];
723
784
  if (!hasClideck(hooks.StopFailure, 'stop')) hooks.StopFailure = [...(hooks.StopFailure || []), clideckHook('stop')];
785
+ if (!hasClideck(hooks.SessionStart, 'session-start')) hooks.SessionStart = [...(hooks.SessionStart || []), clideckHook('session-start')];
786
+ if (!hasClideck(hooks.SessionEnd, 'session-end')) hooks.SessionEnd = [...(hooks.SessionEnd || []), clideckHook('session-end')];
724
787
  if (!hasClideck(hooks.Notification, 'idle')) hooks.Notification = [...(hooks.Notification || []), { matcher: 'idle_prompt', ...clideckHook('idle') }];
725
788
  if (!hasClideck(hooks.PreToolUse, 'menu')) hooks.PreToolUse = [...(hooks.PreToolUse || []), clideckHook('menu')];
726
789
  settings.hooks = hooks;
@@ -797,6 +860,14 @@ function applyTelemetryConfig(preset, cmd = null) {
797
860
  return { success: true, message: `Installed bridge plugin to ${opencodePluginDir}` };
798
861
  }
799
862
 
863
+ if (preset.presetId === 'pi') {
864
+ const src = join(__dirname, 'pi-extension', 'clideck-bridge.ts');
865
+ const dest = piBridgePath(cmd);
866
+ mkdirSync(dirname(dest), { recursive: true });
867
+ copyFileSync(src, dest);
868
+ return { success: true, message: `Installed Pi extension to ${dest}` };
869
+ }
870
+
800
871
  return { success: false, message: `No auto-setup for ${preset.presetId}` };
801
872
  } catch (err) {
802
873
  return { success: false, message: err.message };
@@ -811,7 +882,7 @@ function removeTelemetryConfig(preset, cmd = null) {
811
882
  let settings = {};
812
883
  try { settings = JSON.parse(readFileSync(configPath, 'utf8')); } catch {}
813
884
  if (!settings.hooks) return { success: true, message: 'No hooks to remove' };
814
- for (const event of ['UserPromptSubmit', 'Stop', 'StopFailure', 'Notification', 'PreToolUse']) {
885
+ for (const event of ['UserPromptSubmit', 'Stop', 'StopFailure', 'SessionStart', 'SessionEnd', 'Notification', 'PreToolUse']) {
815
886
  const arr = settings.hooks[event];
816
887
  if (!arr) continue;
817
888
  settings.hooks[event] = arr.filter(h => !h.hooks?.some(x => x.url?.includes('/hook/claude/') || x.command?.includes('claude-hook.js')));
@@ -858,6 +929,11 @@ function removeTelemetryConfig(preset, cmd = null) {
858
929
  return { success: true, message: 'Removed bridge plugin' };
859
930
  }
860
931
 
932
+ if (preset.presetId === 'pi') {
933
+ try { unlinkSync(piBridgePath(cmd)); } catch {}
934
+ return { success: true, message: 'Removed Pi extension' };
935
+ }
936
+
861
937
  return { success: false, message: `No removal logic for ${preset.presetId}` };
862
938
  } catch (err) {
863
939
  return { success: false, message: err.message };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clideck",
3
- "version": "1.31.17",
3
+ "version": "1.31.18",
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/pi-bridge.js ADDED
@@ -0,0 +1,51 @@
1
+ // Pi bridge — receives lifecycle events from the CliDeck Pi extension.
2
+
3
+ let broadcastFn = null;
4
+ let sessionsFn = null;
5
+
6
+ function init(broadcast, getSessions) {
7
+ broadcastFn = broadcast;
8
+ sessionsFn = getSessions;
9
+ }
10
+
11
+ function findByToken(token) {
12
+ if (!token) return null;
13
+ for (const [id, session] of sessionsFn?.() || []) {
14
+ if (session.sessionToken === token) return id;
15
+ }
16
+ return null;
17
+ }
18
+
19
+ function capture(id) {
20
+ setTimeout(() => broadcastFn?.({ type: 'terminal.capture', id }), 500);
21
+ }
22
+
23
+ function handleEvent(payload) {
24
+ if (!payload || !payload.event) return;
25
+ const sessions = sessionsFn?.();
26
+ if (!sessions) return;
27
+
28
+ const clideckId = payload.clideck_id && sessions.has(payload.clideck_id)
29
+ ? payload.clideck_id
30
+ : findByToken(payload.session_id);
31
+ if (!clideckId) return;
32
+
33
+ const session = sessions.get(clideckId);
34
+ if (session && payload.session_id && payload.event !== 'session_shutdown') {
35
+ session.sessionToken = payload.session_id;
36
+ }
37
+
38
+ if (payload.event === 'agent_start') {
39
+ broadcastFn?.({ type: 'session.status', id: clideckId, working: true, source: 'hook' });
40
+ return;
41
+ }
42
+
43
+ if (payload.event === 'agent_end' || payload.event === 'session_start' || payload.event === 'session_shutdown') {
44
+ broadcastFn?.({ type: 'session.status', id: clideckId, working: false, source: 'hook' });
45
+ capture(clideckId);
46
+ }
47
+ }
48
+
49
+ function clear() {}
50
+
51
+ module.exports = { init, handleEvent, clear };
@@ -0,0 +1,34 @@
1
+ // CliDeck bridge extension for Pi.
2
+ // Pi auto-loads this file from ~/.pi/agent/extensions/.
3
+
4
+ const env = globalThis.process?.env || {};
5
+ const baseUrl = env.CLIDECK_URL || `http://localhost:${env.CLIDECK_PORT || "4000"}`;
6
+ const endpoint = `${baseUrl.replace(/\/$/, "")}/hook/pi`;
7
+
8
+ function safeCall(fn: (() => string | undefined) | undefined): string | undefined {
9
+ try { return fn?.(); } catch { return undefined; }
10
+ }
11
+
12
+ function post(event: any, ctx: any): void {
13
+ const payload = {
14
+ event: event?.type || "",
15
+ reason: event?.reason || "",
16
+ clideck_id: env.CLIDECK_SESSION_ID || "",
17
+ session_id: safeCall(() => ctx.sessionManager.getSessionId()) || "",
18
+ session_file: safeCall(() => ctx.sessionManager.getSessionFile()) || "",
19
+ cwd: ctx?.cwd || "",
20
+ };
21
+
22
+ fetch(endpoint, {
23
+ method: "POST",
24
+ headers: { "Content-Type": "application/json" },
25
+ body: JSON.stringify(payload),
26
+ }).catch(() => {});
27
+ }
28
+
29
+ export default function (pi: any): void {
30
+ pi.on("session_start", (event: any, ctx: any) => post(event, ctx));
31
+ pi.on("session_shutdown", (event: any, ctx: any) => post(event, ctx));
32
+ pi.on("agent_start", (event: any, ctx: any) => post(event, ctx));
33
+ pi.on("agent_end", (event: any, ctx: any) => post(event, ctx));
34
+ }
@@ -0,0 +1,13 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" role="img" aria-label="Pi">
2
+ <defs>
3
+ <linearGradient id="pi-bg" x1="10" y1="8" x2="56" y2="58" gradientUnits="userSpaceOnUse">
4
+ <stop stop-color="#2dd4bf"/>
5
+ <stop offset="0.5" stop-color="#60a5fa"/>
6
+ <stop offset="1" stop-color="#a78bfa"/>
7
+ </linearGradient>
8
+ </defs>
9
+ <rect width="64" height="64" rx="18" fill="#0f172a"/>
10
+ <path d="M15 20.5c5.4-2.6 11.5-3.9 18.3-3.9 6.1 0 11.4 1.1 15.9 3.2" fill="none" stroke="url(#pi-bg)" stroke-width="5.5" stroke-linecap="round"/>
11
+ <path d="M24 23.5v22M40 23.5v22" fill="none" stroke="#f8fafc" stroke-width="5.5" stroke-linecap="round"/>
12
+ <path d="M17 45.5h30" fill="none" stroke="url(#pi-bg)" stroke-width="5.5" stroke-linecap="round"/>
13
+ </svg>
package/public/index.html CHANGED
@@ -236,7 +236,10 @@
236
236
  <div class="px-2.5 pb-2.5 flex flex-col gap-2">
237
237
  <div class="relative">
238
238
  <svg class="absolute left-2.5 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-500 pointer-events-none" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.35-4.35"/></svg>
239
- <input id="search-input" type="text" placeholder="Search" class="w-full pl-9 pr-3 py-[7px] text-[13px] bg-slate-800/50 rounded-full text-slate-300 placeholder-slate-600 outline-none border border-transparent focus:border-slate-600/60 focus:bg-slate-800/80 transition-all">
239
+ <input id="search-input" type="text" placeholder="Search" class="w-full pl-9 pr-9 py-[7px] text-[13px] bg-slate-800/50 rounded-full text-slate-300 placeholder-slate-600 outline-none border border-transparent focus:border-slate-600/60 focus:bg-slate-800/80 transition-all">
240
+ <button id="search-clear" class="hidden absolute right-3 top-1/2 -translate-y-1/2 w-6 h-6 rounded-full border border-slate-700/40 bg-slate-800/40 text-slate-500 hover:text-slate-200 hover:bg-slate-700/60 transition-all flex items-center justify-center" title="Clear search" aria-label="Clear search">
241
+ <svg class="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2.4" stroke-linecap="round"><path d="M18 6 6 18M6 6l12 12"/></svg>
242
+ </button>
240
243
  </div>
241
244
  <div class="flex bg-slate-800/30 rounded-lg p-[3px]">
242
245
  <button class="filter-tab flex-1 text-[11px] font-medium py-[5px] rounded-md transition-all bg-slate-700/60 text-slate-200" data-tab="all">All</button>
package/public/js/app.js CHANGED
@@ -88,7 +88,7 @@ function connect() {
88
88
  for (const id of [...state.terms.keys()]) {
89
89
  if (!liveIds.has(id)) removeTerminal(id);
90
90
  }
91
- msg.list.forEach(s => addTerminal(s.id, s.name, s.themeId, s.commandId, s.projectId, s.muted, s.lastPreview, s.presetId));
91
+ msg.list.forEach(s => addTerminal(s.id, s.name, s.themeId, s.commandId, s.projectId, s.muted, s.lastPreview, s.presetId, s.working));
92
92
  if (!state.active || !state.terms.has(state.active)) {
93
93
  const savedActive = localStorage.getItem('clideck.activeSessionId');
94
94
  const nextId = savedActive && liveIds.has(savedActive) ? savedActive : msg.list[0]?.id;
@@ -98,7 +98,7 @@ function connect() {
98
98
  break;
99
99
  case 'created':
100
100
  expandProjectForNewSession(msg.projectId);
101
- if (!state.terms.has(msg.id)) addTerminal(msg.id, msg.name, msg.themeId, msg.commandId, msg.projectId, msg.muted, msg.lastPreview, msg.presetId);
101
+ if (!state.terms.has(msg.id)) addTerminal(msg.id, msg.name, msg.themeId, msg.commandId, msg.projectId, msg.muted, msg.lastPreview, msg.presetId, msg.working);
102
102
  select(msg.id);
103
103
  applyFilter();
104
104
  closeMobileSidebar();
@@ -506,9 +506,20 @@ document.getElementById('btn-new-project').addEventListener('click', () => {
506
506
  });
507
507
 
508
508
  // Search & filter toolbar
509
- document.getElementById('search-input').addEventListener('input', (e) => {
510
- state.filter.query = e.target.value;
509
+ const searchInput = document.getElementById('search-input');
510
+ const searchClear = document.getElementById('search-clear');
511
+ function setSearchQuery(value, focus = false) {
512
+ searchInput.value = value;
513
+ state.filter.query = value;
514
+ searchClear.classList.toggle('hidden', !value);
511
515
  applyFilter();
516
+ if (focus) searchInput.focus();
517
+ }
518
+ searchInput.addEventListener('input', (e) => {
519
+ setSearchQuery(e.target.value);
520
+ });
521
+ searchClear.addEventListener('click', () => {
522
+ setSearchQuery('', true);
512
523
  });
513
524
  document.querySelectorAll('.filter-tab').forEach(btn => {
514
525
  btn.addEventListener('click', () => setTab(btn.dataset.tab));
@@ -58,6 +58,7 @@ function dispatch(e) {
58
58
  // Catch keys outside terminals — skip text inputs
59
59
  document.addEventListener('keydown', (e) => {
60
60
  if (isInput(e.target)) return;
61
+ if (e.ctrlKey && !e.metaKey && !e.altKey && !e.shiftKey && e.code === 'KeyK') return;
61
62
  dispatch(e);
62
63
  }, true);
63
64
 
@@ -114,6 +115,16 @@ export function attachToTerminal(term, presetId) {
114
115
  term.input('\x1b[13;2u');
115
116
  return false;
116
117
  }
118
+ if (e.type === 'keydown'
119
+ && e.ctrlKey
120
+ && !e.metaKey
121
+ && !e.altKey
122
+ && !e.shiftKey
123
+ && e.code === 'KeyK') {
124
+ e.preventDefault();
125
+ term.input('\x0b');
126
+ return false;
127
+ }
117
128
  const promptResult = handleTerminalKey(e);
118
129
  if (promptResult === false) return false;
119
130
  if (e.type !== 'keydown') return true;
@@ -531,7 +531,7 @@ export function estimateSize() {
531
531
 
532
532
  // --- Terminal management ---
533
533
 
534
- export function addTerminal(id, name, themeId, commandId, projectId, muted, lastPreview, presetId) {
534
+ export function addTerminal(id, name, themeId, commandId, projectId, muted, lastPreview, presetId, working = false) {
535
535
  if (state.terms.has(id)) return;
536
536
  themeId = themeId || state.cfg.defaultTheme || 'default';
537
537
 
@@ -726,6 +726,7 @@ export function addTerminal(id, name, themeId, commandId, projectId, muted, last
726
726
  }, 500);
727
727
  const cancelFitRaf = () => { if (fitRaf) { cancelAnimationFrame(fitRaf); fitRaf = 0; } };
728
728
  state.terms.set(id, { term, fit, el, ro, cancelFitRaf, onContextMenu, inputLength: 0, inputHasText: false, scrolledUp: false, 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: '' });
729
+ if (working) setStatus(id, true);
729
730
  refreshTerminalInputActions();
730
731
  document.getElementById('empty').style.display = 'none';
731
732
  document.getElementById('terminals').style.pointerEvents = '';