clideck 1.31.17 → 1.31.19
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 +4 -4
- package/agent-presets.json +19 -1
- package/bin/claude-hook.js +3 -0
- package/handlers.js +80 -4
- package/package.json +1 -1
- package/pi-bridge.js +51 -0
- package/pi-extension/clideck-bridge.ts +34 -0
- package/public/img/pi.svg +13 -0
- package/public/index.html +4 -1
- package/public/js/app.js +15 -4
- package/public/js/creator.js +9 -1
- package/public/js/hotkeys.js +11 -0
- package/public/js/terminals.js +2 -1
- package/public/tailwind.css +1 -1
- package/server.js +27 -5
- package/sessions.js +12 -4
- package/transcript.js +1 -0
- package/skills/autonomous-session/SKILL.md +0 -211
- package/skills/research-experiment/SKILL.md +0 -411
- package/skills/research-experiment/SKILL.md.bak +0 -224
- package/skills/research-experiment/scripts/init-research-layout.mjs +0 -184
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
|
|
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
|
|
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
|
|
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
|
|
package/agent-presets.json
CHANGED
|
@@ -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
|
|
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",
|
package/bin/claude-hook.js
CHANGED
|
@@ -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 =>
|
|
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 &&
|
|
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')
|
|
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
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-
|
|
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')
|
|
510
|
-
|
|
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));
|
package/public/js/creator.js
CHANGED
|
@@ -104,7 +104,15 @@ function sortedPresets() {
|
|
|
104
104
|
const commands = (state.cfg.commands || [])
|
|
105
105
|
.filter(c => c.enabled !== false)
|
|
106
106
|
.map(c => ({ type: 'command', command: c, preset: presetForCommand(c) || { name: c.label, icon: c.icon || 'terminal', isAgent: !!c.isAgent } }));
|
|
107
|
-
const
|
|
107
|
+
const seenShell = new Set();
|
|
108
|
+
const shell = commands.filter(item => {
|
|
109
|
+
if (item.command.isAgent) return false;
|
|
110
|
+
const isBuiltinShell = item.command.presetId === 'shell' || item.preset?.presetId === 'shell';
|
|
111
|
+
if (!isBuiltinShell) return true;
|
|
112
|
+
if (seenShell.has('shell')) return false;
|
|
113
|
+
seenShell.add('shell');
|
|
114
|
+
return true;
|
|
115
|
+
});
|
|
108
116
|
const agents = [...missing, ...commands.filter(item => item.command.isAgent)];
|
|
109
117
|
const lastId = localStorage.getItem(MRU_KEY);
|
|
110
118
|
if (lastId) {
|
package/public/js/hotkeys.js
CHANGED
|
@@ -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;
|
package/public/js/terminals.js
CHANGED
|
@@ -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 = '';
|