claude-live 0.4.0 → 0.4.2
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/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +10 -3
- package/client/dist/assets/index-B1BUdq7a.js +47 -0
- package/client/dist/assets/index-DjcKbX6b.css +1 -0
- package/client/dist/index.html +2 -2
- package/package.json +4 -2
- package/scripts/sync-plugin-version.js +32 -0
- package/server/index.js +143 -7
- package/test-agent-animations-long.js +144 -0
- package/test-agent-animations.js +126 -0
- package/test-agents.js +61 -0
- package/tests/store.test.ts +72 -0
- package/vitest.config.ts +10 -0
- package/client/dist/assets/index-C2EVAfon.js +0 -47
- package/client/dist/assets/index-DMfuaPRG.css +0 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
@import"https://fonts.googleapis.com/css2?family=Fira+Code:wght@300;400;500&family=Space+Mono:wght@400;700&display=swap";*{margin:0;padding:0;box-sizing:border-box}html,body,#root{width:100%;height:100%;overflow:hidden;background:#050507}#root{position:relative}#root:before{content:"";position:fixed;top:0;right:0;bottom:0;left:0;background-image:url("data:image/svg+xml,%3Csvg viewBox='0 0 200 200' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noise'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noise)' opacity='1'/%3E%3C/svg%3E");background-size:150px;opacity:.035;pointer-events:none;z-index:10}.hud{position:fixed;top:24px;left:24px;z-index:20;font-family:Space Mono,monospace;pointer-events:none}.hud-title{font-size:17px;font-weight:700;color:#fff;letter-spacing:-.5px;margin-bottom:14px;line-height:1}.hud-title span{color:#4ade80}.hud-stat{display:flex;align-items:baseline;gap:8px;margin-bottom:5px}.hud-label{font-size:9px;color:#666;text-transform:uppercase;letter-spacing:1px;width:52px}.hud-value{font-size:13px;color:#ccc}.hud-tool{font-weight:700}.tooltip{position:fixed;z-index:30;background:#08080ef0;border:1px solid rgba(255,255,255,.12);border-radius:6px;padding:10px 14px;pointer-events:none;transition:opacity .1s ease;-webkit-backdrop-filter:blur(16px);backdrop-filter:blur(16px);min-width:160px;box-shadow:0 8px 32px #000000b3}.tooltip-label{font-family:Fira Code,monospace;font-size:12px;color:#fff;font-weight:500;margin-bottom:2px;white-space:nowrap;max-width:260px;overflow:hidden;text-overflow:ellipsis}.tooltip-type{font-family:Space Mono,monospace;font-size:9px;color:#888;text-transform:uppercase;letter-spacing:1px;margin-bottom:7px}.tooltip-meta{font-family:Space Mono,monospace;font-size:10px;color:#aaa;margin-bottom:3px}.tooltip-count{font-family:Space Mono,monospace;font-size:9px;color:#666}.sidebar{position:fixed;right:0;top:0;height:100%;width:280px;background:#06060bf5;border-left:1px solid rgba(255,255,255,.08);z-index:25;transform:translate(100%);transition:transform .25s cubic-bezier(.16,1,.3,1);-webkit-backdrop-filter:blur(24px);backdrop-filter:blur(24px);display:flex;flex-direction:column;overflow:hidden}.sidebar--open{transform:translate(0)}.sidebar-header{padding:24px 20px 16px;border-bottom:1px solid rgba(255,255,255,.06);position:relative}.sidebar-close{position:absolute;top:20px;right:16px;width:26px;height:26px;display:flex;align-items:center;justify-content:center;color:#666;cursor:pointer;font-size:18px;border-radius:4px;transition:color .15s,background .15s;pointer-events:all;line-height:1}.sidebar-close:hover{color:#fff;background:#ffffff14}.sidebar-title{font-family:Fira Code,monospace;font-size:13px;color:#fff;font-weight:500;margin-bottom:4px;padding-right:32px;word-break:break-all}.sidebar-type{font-family:Space Mono,monospace;font-size:9px;color:#666;text-transform:uppercase;letter-spacing:1px}.sidebar-section{padding:16px 20px;border-bottom:1px solid rgba(255,255,255,.05)}.sidebar-section-label{font-family:Space Mono,monospace;font-size:9px;color:#555;text-transform:uppercase;letter-spacing:1px;margin-bottom:8px}.sidebar-action{font-family:Fira Code,monospace;font-size:15px;font-weight:500;margin-bottom:3px}.sidebar-time{font-family:Space Mono,monospace;font-size:10px;color:#777}.sidebar-count{font-family:Space Mono,monospace;font-size:24px;color:#ccc;font-weight:700}.sidebar-session{font-family:Fira Code,monospace;font-size:11px;color:#4ade80}.perm-badge{display:inline-flex;align-items:center;gap:5px;font-family:Space Mono,monospace;font-size:9px;color:#fbbf24;background:#fbbf241a;border:1px solid rgba(251,191,36,.3);border-radius:3px;padding:2px 6px;margin-top:6px}.perm-dot{width:5px;height:5px;border-radius:50%;background:#fbbf24;animation:perm-pulse 1s ease-in-out infinite}@keyframes perm-pulse{0%,to{opacity:1;transform:scale(1)}50%{opacity:.4;transform:scale(.7)}}.bottom-left-panel{position:fixed;bottom:24px;left:24px;z-index:20;display:flex;flex-direction:column;gap:10px;pointer-events:none}.bottom-left-panel>*{pointer-events:all}.event-log{display:flex;flex-direction:column;gap:5px;max-width:250px}.event-log-entry{display:flex;align-items:center;gap:5px;position:relative;overflow:hidden;animation:log-entry-in .38s cubic-bezier(.16,1,.3,1)}.event-log-entry:after{content:"";position:absolute;top:0;bottom:0;left:-60%;width:60%;background:linear-gradient(90deg,transparent 0%,var(--entry-color, #888) 60%,transparent 100%);opacity:.22;animation:log-scan .55s ease-out forwards;pointer-events:none}@keyframes log-entry-in{0%{opacity:0;transform:translate(-16px)}55%{opacity:1;transform:translate(3px)}78%{transform:translate(-1px)}to{opacity:1;transform:translate(0)}}@keyframes log-scan{0%{left:-60%;opacity:.22}to{left:130%;opacity:0}}.event-log-dot{width:5px;height:5px;border-radius:50%;flex-shrink:0;animation:log-dot-pop .42s cubic-bezier(.34,1.56,.64,1)}@keyframes log-dot-pop{0%{transform:scale(0) rotate(-90deg)}70%{transform:scale(1.5) rotate(8deg)}to{transform:scale(1) rotate(0)}}.event-log-tool{font-family:Space Mono,monospace;font-size:8px;font-weight:700;min-width:38px;text-transform:uppercase;letter-spacing:.04em;animation:log-tool-in .3s ease-out}@keyframes log-tool-in{0%{opacity:0;letter-spacing:.28em}to{opacity:1;letter-spacing:.04em}}.event-log-file{font-family:Fira Code,monospace;font-size:8px;color:#999;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:100px}.event-log-session{font-family:Fira Code,monospace;font-size:7px;color:#3a3a3a;white-space:nowrap;margin-left:auto}.event-log-entry--static,.event-log-entry--static .event-log-dot,.event-log-entry--static .event-log-tool{animation:none}.event-log-entry--static:after{display:none}.event-log-history{width:280px;height:calc(100vh - 80px);overflow-y:scroll;display:flex;flex-direction:column;gap:5px;background:#020209eb;border:1px solid rgba(255,255,255,.08);border-radius:4px;padding:6px;box-sizing:border-box}.event-log-history-back{font-family:Space Mono,monospace;font-size:8px;color:#555;background:none;border:none;cursor:pointer;text-align:left;padding:0 0 4px}.event-log-history-back:hover{color:#aaa}.event-log-history-btn{font-family:Space Mono,monospace;font-size:7px;color:#333;background:none;border:none;cursor:pointer;text-align:left;padding:2px 0 0 10px}.event-log-history-btn:hover{color:#777}.help-btn{background:none;border:1px solid rgba(255,255,255,.1);border-radius:4px;color:#444;font-family:Space Mono,monospace;font-size:9px;padding:3px 8px;cursor:pointer;width:fit-content;transition:color .15s,border-color .15s;letter-spacing:.05em;text-transform:uppercase}.help-btn:hover{color:#999;border-color:#ffffff40}.top-right-buttons{position:fixed;top:24px;right:24px;z-index:50;display:flex;gap:8px;align-items:center;pointer-events:auto}.audio-toggle{background:none;border:1px solid rgba(255,255,255,.1);border-radius:4px;color:#666;cursor:pointer;transition:color .15s,border-color .15s,background-color .15s;display:flex;align-items:center;justify-content:center;width:32px;height:32px;padding:0;font-size:0}.audio-toggle svg{width:16px;height:16px;stroke:currentColor}.audio-toggle:hover{color:#999;border-color:#ffffff40;background-color:#ffffff0d}.audio-toggle:active{background-color:#ffffff1a}.autofit-toggle{background:none;border:1px solid rgba(255,255,255,.1);color:#ccc;cursor:pointer;display:inline-flex;align-items:center;justify-content:center;border-radius:4px;transition:all .2s ease;width:32px;height:32px;padding:0;font-size:0}.autofit-toggle svg{width:16px;height:16px;stroke:currentColor}.autofit-toggle:hover{color:#999;border-color:#ffffff40;background-color:#ffffff0d}.autofit-toggle:active{background-color:#ffffff1a}.hud-button{width:32px;height:32px;background:#ffffff14;border:1px solid rgba(255,255,255,.12);border-radius:4px;color:#666;cursor:pointer;font-size:14px;display:flex;align-items:center;justify-content:center;transition:color .2s}.hud-button:hover{color:#aaa}.help-overlay{position:fixed;top:0;right:0;bottom:0;left:0;z-index:60;background:#0000008c;display:flex;align-items:center;justify-content:center;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px)}.help-panel{background:#08080efa;border:1px solid rgba(255,255,255,.1);border-radius:8px;padding:20px 24px;min-width:200px;box-shadow:0 16px 48px #000c}.help-panel-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:14px}.help-panel-title{font-family:Space Mono,monospace;font-size:9px;color:#555;text-transform:uppercase;letter-spacing:1.5px}.legend-title{font-size:8px;color:#444;text-transform:uppercase;letter-spacing:1.5px;margin-bottom:8px}.legend-items{display:flex;flex-direction:column;gap:5px}.legend-item{display:flex;align-items:center;gap:8px}.legend-dot{width:8px;height:8px;border-radius:50%;flex-shrink:0}.legend-badge{width:14px;height:14px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:7px;font-weight:700;color:#000;flex-shrink:0}.legend-name{font-size:9px;color:#888}.legend-key{font-size:8px;color:#555;margin-left:auto;padding-left:8px}.legend-perm{display:flex;align-items:center;gap:8px;margin-top:4px;padding-top:6px;border-top:1px solid rgba(255,255,255,.04)}.legend-perm-ring{width:14px;height:14px;border-radius:50%;border:2px dashed #fbbf24;flex-shrink:0}.legend-perm-name{font-size:9px;color:#fbbf24}.debug-toggle{position:fixed;bottom:24px;right:24px;z-index:40;background:#fbbf241f;border:1px solid rgba(251,191,36,.4);border-radius:5px;color:#fbbf24e6;font-family:Space Mono,monospace;font-size:10px;font-weight:700;letter-spacing:.15em;padding:6px 12px;cursor:pointer;transition:color .15s,background .15s,border-color .15s}.debug-toggle:hover{background:#fbbf2438;border-color:#fbbf24b3;color:#fbbf24}.debug-panel{position:fixed;bottom:56px;right:24px;width:260px;background:#06060cf7;border:1px solid rgba(255,255,255,.1);border-radius:8px;z-index:40;-webkit-backdrop-filter:blur(20px);backdrop-filter:blur(20px);box-shadow:0 16px 48px #000c;transform:translateY(8px) scale(.97);opacity:0;pointer-events:none;transition:opacity .18s ease,transform .18s ease}.debug-panel--open{opacity:1;transform:translateY(0) scale(1);pointer-events:all}.debug-panel-header{display:flex;align-items:center;justify-content:space-between;padding:12px 14px 10px;border-bottom:1px solid rgba(255,255,255,.07)}.debug-panel-title{font-family:Space Mono,monospace;font-size:9px;color:#555;text-transform:uppercase;letter-spacing:1.5px}.debug-close{background:none;border:none;color:#555;font-size:16px;cursor:pointer;padding:0 2px;line-height:1;transition:color .15s}.debug-close:hover{color:#ccc}.debug-section{padding:10px 14px;border-bottom:1px solid rgba(255,255,255,.05)}.debug-section:last-child{border-bottom:none}.debug-section-label{font-family:Space Mono,monospace;font-size:8px;color:#444;text-transform:uppercase;letter-spacing:1.5px;margin-bottom:8px}.debug-session-row{display:flex;gap:6px}.debug-select{flex:1;background:#ffffff0d;border:1px solid rgba(255,255,255,.1);border-radius:4px;color:#aaa;font-family:Fira Code,monospace;font-size:10px;padding:4px 7px;outline:none;cursor:pointer}.debug-new-btn{background:#ffffff0f;border:1px solid rgba(255,255,255,.12);border-radius:4px;color:#888;font-family:Space Mono,monospace;font-size:9px;padding:4px 8px;cursor:pointer;white-space:nowrap;transition:color .15s,background .15s}.debug-new-btn:hover{color:#ccc;background:#ffffff1a}.debug-tool-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:5px}.debug-tool-btn{background:#ffffff0a;border:1px solid rgba(255,255,255,.08);border-radius:4px;color:var(--tool-color, #888);font-family:Space Mono,monospace;font-size:9px;padding:6px 4px;cursor:pointer;transition:background .15s,border-color .15s,opacity .15s;text-align:center}.debug-tool-btn:hover{background:color-mix(in srgb,var(--tool-color, #888) 15%,transparent);border-color:color-mix(in srgb,var(--tool-color, #888) 40%,transparent)}.debug-tool-btn:active{opacity:.6}.perm-notifications{position:fixed;left:24px;top:130px;display:flex;flex-direction:column;gap:6px;z-index:100;pointer-events:none;max-width:280px}.perm-notifications-title{font-size:9px;letter-spacing:.2em;text-transform:uppercase;color:#fbbf24;text-align:left;margin-bottom:2px;opacity:.7}.perm-notification-item{display:flex;align-items:flex-start;gap:10px;background:#fbbf240f;border:1px solid rgba(251,191,36,.25);border-radius:3px;padding:8px 14px;animation:perm-fadein .3s ease}@keyframes perm-fadein{0%{opacity:0;transform:translateY(6px)}to{opacity:1;transform:translateY(0)}}.perm-notification-dot{width:6px;height:6px;border-radius:50%;background:#fbbf24;margin-top:3px;flex-shrink:0;animation:perm-pulse 1s ease-in-out infinite}.perm-notification-body{display:flex;flex-direction:column;gap:2px}.perm-notification-session{font-size:9px;letter-spacing:.15em;color:#fbbf2499;text-transform:uppercase}.perm-notification-msg{font-size:11px;color:#fbbf24e6;max-width:320px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.panel-overlay{position:fixed;top:24px;right:24px;max-width:400px;max-height:calc(100vh - 48px);background:#020209f2;border:1px solid rgba(255,255,255,.1);border-radius:8px;padding:16px;overflow-y:auto;z-index:100;font-size:12px;line-height:1.6;color:#ccc}.panel-close,.panel-close-btn{position:absolute;top:8px;right:8px;background:none;border:none;color:#666;cursor:pointer;font-size:16px;padding:0;width:24px;height:24px}.panel-close:hover,.panel-close-btn:hover{color:#aaa}.panel-overlay h3{margin:0 0 12px;font-size:14px;font-weight:600;color:#fff}.panel-overlay h4{margin:8px 0 6px;font-size:12px;font-weight:600;color:#aaa;text-transform:uppercase;letter-spacing:.05em}.panel-section{margin-bottom:16px}.panel-section:last-child{margin-bottom:0}.node-types-grid{display:flex;flex-direction:column;gap:8px}.node-type-item{display:flex;align-items:center;gap:8px}.node-type-badge{width:28px;height:28px;border-radius:4px;display:flex;align-items:center;justify-content:center;font-size:11px;font-weight:600;color:#fff;flex-shrink:0}.node-type-info{display:flex;flex-direction:column;gap:2px}.node-type-name{font-size:12px;font-weight:500;color:#fff}.node-type-description{font-size:10px;color:#888}.animations-list{display:flex;flex-direction:column;gap:10px}.animation-item{font-size:11px}.animation-name{font-weight:500;color:#fff;margin-bottom:2px}.animation-description{font-size:10px;color:#888;padding-left:8px;border-left:2px solid rgba(255,255,255,.1)}
|
package/client/dist/index.html
CHANGED
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
body { background: #080808; overflow: hidden; }
|
|
10
10
|
#root { width: 100vw; height: 100vh; }
|
|
11
11
|
</style>
|
|
12
|
-
<script type="module" crossorigin src="/assets/index-
|
|
13
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
12
|
+
<script type="module" crossorigin src="/assets/index-B1BUdq7a.js"></script>
|
|
13
|
+
<link rel="stylesheet" crossorigin href="/assets/index-DjcKbX6b.css">
|
|
14
14
|
</head>
|
|
15
15
|
<body>
|
|
16
16
|
<div id="root"></div>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-live",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.2",
|
|
4
4
|
"description": "Realtime Claude Code activity visualizer",
|
|
5
5
|
"bin": {
|
|
6
6
|
"claude-live": "./bin/claude-live.js"
|
|
@@ -10,10 +10,12 @@
|
|
|
10
10
|
"build": "cd client && vite build",
|
|
11
11
|
"start": "node server/index.js",
|
|
12
12
|
"test": "vitest run",
|
|
13
|
-
"
|
|
13
|
+
"sync-versions": "node scripts/sync-plugin-version.js",
|
|
14
|
+
"prepublishOnly": "npm run sync-versions && npm run build"
|
|
14
15
|
},
|
|
15
16
|
"dependencies": {
|
|
16
17
|
"express": "^4.18.2",
|
|
18
|
+
"matter-js": "^0.20.0",
|
|
17
19
|
"open": "^10.1.0",
|
|
18
20
|
"uuid": "^9.0.0"
|
|
19
21
|
},
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { readFileSync, writeFileSync } from 'fs';
|
|
4
|
+
import { resolve } from 'path';
|
|
5
|
+
import { fileURLToPath } from 'url';
|
|
6
|
+
|
|
7
|
+
const __dirname = resolve(fileURLToPath(import.meta.url), '..');
|
|
8
|
+
|
|
9
|
+
const packagePath = resolve(__dirname, '../package.json');
|
|
10
|
+
const pluginJsonPath = resolve(__dirname, '../.claude-plugin/plugin.json');
|
|
11
|
+
const marketplaceJsonPath = resolve(__dirname, '../.claude-plugin/marketplace.json');
|
|
12
|
+
|
|
13
|
+
// Read package.json to get the version
|
|
14
|
+
const packageJson = JSON.parse(readFileSync(packagePath, 'utf8'));
|
|
15
|
+
const version = packageJson.version;
|
|
16
|
+
|
|
17
|
+
console.log(`Syncing plugin versions to ${version}...`);
|
|
18
|
+
|
|
19
|
+
// Update plugin.json
|
|
20
|
+
const pluginJson = JSON.parse(readFileSync(pluginJsonPath, 'utf8'));
|
|
21
|
+
pluginJson.version = version;
|
|
22
|
+
writeFileSync(pluginJsonPath, JSON.stringify(pluginJson, null, 2) + '\n');
|
|
23
|
+
console.log(`✓ Updated .claude-plugin/plugin.json`);
|
|
24
|
+
|
|
25
|
+
// Update marketplace.json
|
|
26
|
+
const marketplaceJson = JSON.parse(readFileSync(marketplaceJsonPath, 'utf8'));
|
|
27
|
+
marketplaceJson.metadata.version = version;
|
|
28
|
+
marketplaceJson.plugins[0].version = version;
|
|
29
|
+
writeFileSync(marketplaceJsonPath, JSON.stringify(marketplaceJson, null, 2) + '\n');
|
|
30
|
+
console.log(`✓ Updated .claude-plugin/marketplace.json`);
|
|
31
|
+
|
|
32
|
+
console.log(`Done! Plugin versions are now ${version}`);
|
package/server/index.js
CHANGED
|
@@ -10,6 +10,141 @@ const EVENTS_PER_SESSION = 50 // rolling buffer per session
|
|
|
10
10
|
const SESSION_TIMEOUT_MS = 11 * 60 * 1000 // 11 minutes (server-side cleanup)
|
|
11
11
|
const HEARTBEAT_MS = 15000
|
|
12
12
|
|
|
13
|
+
// ── Ported from client constants/nodeKeys (JS) ──────────────────────────────
|
|
14
|
+
|
|
15
|
+
const TOOL_COLOR_HEX = {
|
|
16
|
+
Read: '#4ade80', Edit: '#60a5fa', Write: '#60a5fa',
|
|
17
|
+
Bash: '#f59e0b', Grep: '#a78bfa', Glob: '#a78bfa',
|
|
18
|
+
WebFetch: '#f472b6', Stop: '#888888', Notification: '#34d399',
|
|
19
|
+
}
|
|
20
|
+
const DEFAULT_HEX = '#555555'
|
|
21
|
+
const FILE_TOOLS = new Set(['Read', 'Edit', 'Write', 'Glob', 'Grep'])
|
|
22
|
+
|
|
23
|
+
function nodeKeyFor(event) {
|
|
24
|
+
const t = event.tool_name
|
|
25
|
+
if (!t) {
|
|
26
|
+
if (event.hook_event_name === 'Stop') return 'session:stop'
|
|
27
|
+
if (event.hook_event_name === 'Notification') {
|
|
28
|
+
const msg = event.tool_input?.message || ''
|
|
29
|
+
return `notification:${msg.slice(0, 20)}`
|
|
30
|
+
}
|
|
31
|
+
return null
|
|
32
|
+
}
|
|
33
|
+
const input = event.tool_input || {}
|
|
34
|
+
if (FILE_TOOLS.has(t)) {
|
|
35
|
+
const fp = input.file_path || input.path || null
|
|
36
|
+
return fp ? `file:${fp}` : null
|
|
37
|
+
}
|
|
38
|
+
if (t === 'Bash') return `bash:${input.command || ''}`
|
|
39
|
+
if (t === 'WebFetch') {
|
|
40
|
+
try { return `web:${new URL(input.url || '').hostname}` } catch { return 'web:unknown' }
|
|
41
|
+
}
|
|
42
|
+
return `tool:${t}`
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function nodeTypeFor(event) {
|
|
46
|
+
const t = event.tool_name
|
|
47
|
+
if (FILE_TOOLS.has(t || '')) return 'file'
|
|
48
|
+
if (t === 'Bash') return 'bash'
|
|
49
|
+
if (t === 'WebFetch') return 'web'
|
|
50
|
+
if (event.hook_event_name === 'Stop') return 'stop'
|
|
51
|
+
if (event.hook_event_name === 'Notification') return 'notification'
|
|
52
|
+
return 'tool'
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function labelFor(event) {
|
|
56
|
+
const t = event.tool_name
|
|
57
|
+
const input = event.tool_input || {}
|
|
58
|
+
if (FILE_TOOLS.has(t || '')) {
|
|
59
|
+
const fp = input.file_path || input.path || ''
|
|
60
|
+
return fp.split('/').pop() || fp
|
|
61
|
+
}
|
|
62
|
+
if (t === 'Bash') return `$ ${(input.command || '').slice(0, 22)}`
|
|
63
|
+
if (t === 'WebFetch') { try { return `↗ ${new URL(input.url || '').hostname}` } catch { return '↗ web' } }
|
|
64
|
+
if (event.hook_event_name === 'Stop') return '✓ done'
|
|
65
|
+
if (event.hook_event_name === 'Notification') return (input.message || 'notification').slice(0, 24)
|
|
66
|
+
return t || event.hook_event_name || '?'
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ── State snapshot computation ───────────────────────────────────────────────
|
|
70
|
+
|
|
71
|
+
// Compute a lightweight state snapshot from a session's event buffer.
|
|
72
|
+
// Returns null if the session has no events.
|
|
73
|
+
function computeSessionSnapshot(session_id, events) {
|
|
74
|
+
if (events.length === 0) return null
|
|
75
|
+
|
|
76
|
+
let label = null
|
|
77
|
+
let cwd = null
|
|
78
|
+
let stopping = false
|
|
79
|
+
let eventCount = events.length
|
|
80
|
+
|
|
81
|
+
// file nodes: persistent, grow with each touch
|
|
82
|
+
const fileNodes = new Map() // key → { key, nodeType, label, colorHex, baseRadius }
|
|
83
|
+
// ephemeral keys seen in recent N events (they decay fast so only show recent ones)
|
|
84
|
+
const RECENT_N = 15
|
|
85
|
+
const recentEphemeralKeys = new Set()
|
|
86
|
+
|
|
87
|
+
for (let i = 0; i < events.length; i++) {
|
|
88
|
+
const event = events[i]
|
|
89
|
+
|
|
90
|
+
if (event.cwd) cwd = event.cwd
|
|
91
|
+
if (cwd && (!label || label.length <= 8)) {
|
|
92
|
+
const parts = cwd.split('/').filter(Boolean)
|
|
93
|
+
label = parts[parts.length - 1] || null
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (event.hook_event_name === 'Stop') {
|
|
97
|
+
stopping = true
|
|
98
|
+
} else if (event.hook_event_name !== 'SessionEnd') {
|
|
99
|
+
stopping = false
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const key = nodeKeyFor(event)
|
|
103
|
+
if (!key) continue
|
|
104
|
+
const type = nodeTypeFor(event)
|
|
105
|
+
const isFile = type === 'file'
|
|
106
|
+
const colorHex = TOOL_COLOR_HEX[event.tool_name || event.hook_event_name] ?? DEFAULT_HEX
|
|
107
|
+
|
|
108
|
+
if (isFile) {
|
|
109
|
+
if (fileNodes.has(key)) {
|
|
110
|
+
fileNodes.get(key).baseRadius = Math.min(8, fileNodes.get(key).baseRadius + 0.3)
|
|
111
|
+
} else {
|
|
112
|
+
fileNodes.set(key, { key, nodeType: type, label: labelFor(event), colorHex, baseRadius: 2.5 })
|
|
113
|
+
}
|
|
114
|
+
} else if (i >= events.length - RECENT_N) {
|
|
115
|
+
recentEphemeralKeys.add(key)
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Build node list: files first, then recent ephemerals
|
|
120
|
+
const nodes = [...fileNodes.values()]
|
|
121
|
+
|
|
122
|
+
// Add recent ephemerals (not already in files)
|
|
123
|
+
const fileKeySet = new Set(fileNodes.keys())
|
|
124
|
+
for (let i = Math.max(0, events.length - RECENT_N); i < events.length; i++) {
|
|
125
|
+
const event = events[i]
|
|
126
|
+
const key = nodeKeyFor(event)
|
|
127
|
+
if (!key || fileKeySet.has(key)) continue
|
|
128
|
+
const type = nodeTypeFor(event)
|
|
129
|
+
const colorHex = TOOL_COLOR_HEX[event.tool_name || event.hook_event_name] ?? DEFAULT_HEX
|
|
130
|
+
if (!nodes.find(n => n.key === key)) {
|
|
131
|
+
nodes.push({ key, nodeType: type, label: labelFor(event), colorHex, baseRadius: 4 })
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Ring assignment is handled by the client (server just sends node data)
|
|
136
|
+
return {
|
|
137
|
+
session_id,
|
|
138
|
+
label: label || session_id.slice(0, 8),
|
|
139
|
+
cwd,
|
|
140
|
+
stopping,
|
|
141
|
+
eventCount,
|
|
142
|
+
nodes, // [{ key, nodeType, label, colorHex, baseRadius }]
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// ── Server ───────────────────────────────────────────────────────────────────
|
|
147
|
+
|
|
13
148
|
function makeSessionId(ip, ts) {
|
|
14
149
|
return 'unknown-' + createHash('sha1').update(ip + ts).digest('hex').slice(0, 8)
|
|
15
150
|
}
|
|
@@ -28,7 +163,6 @@ function normalizeEvent(raw, remoteIp) {
|
|
|
28
163
|
agent_type: raw.agent_type ?? null,
|
|
29
164
|
cwd: raw.cwd ?? null,
|
|
30
165
|
error: raw.error ?? null,
|
|
31
|
-
// Extended fields
|
|
32
166
|
tool_use_id: raw.tool_use_id ?? null,
|
|
33
167
|
prompt: raw.prompt ?? null,
|
|
34
168
|
model: raw.model ?? null,
|
|
@@ -91,13 +225,15 @@ export function createServer({ port = 43451 } = {}) {
|
|
|
91
225
|
res.setHeader('Cache-Control', 'no-cache')
|
|
92
226
|
res.setHeader('Connection', 'keep-alive')
|
|
93
227
|
res.flushHeaders()
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
228
|
+
|
|
229
|
+
// Send state snapshot instead of replaying raw events
|
|
230
|
+
const sessions = []
|
|
231
|
+
for (const [sid, session] of sessionBuffers) {
|
|
232
|
+
const snap = computeSessionSnapshot(sid, session.events)
|
|
233
|
+
if (snap) sessions.push(snap)
|
|
99
234
|
}
|
|
100
|
-
res.write(`data: ${JSON.stringify({ type: '
|
|
235
|
+
res.write(`data: ${JSON.stringify({ type: 'state_snapshot', sessions })}\n\n`)
|
|
236
|
+
|
|
101
237
|
clients.add(res)
|
|
102
238
|
const heartbeat = setInterval(() => {
|
|
103
239
|
try { res.write(': heartbeat\n\n') } catch { clients.delete(res); clearInterval(heartbeat) }
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Extended test with more agents and longer animation sequence
|
|
3
|
+
|
|
4
|
+
import http from 'http';
|
|
5
|
+
import { URL } from 'url';
|
|
6
|
+
|
|
7
|
+
const SERVER = 'http://localhost:43453';
|
|
8
|
+
const SESSION_ID = 'test-session-' + Date.now();
|
|
9
|
+
|
|
10
|
+
function sendHook(eventName, data = {}) {
|
|
11
|
+
return new Promise((resolve, reject) => {
|
|
12
|
+
const payload = JSON.stringify({
|
|
13
|
+
hook_event_name: eventName,
|
|
14
|
+
session_id: SESSION_ID,
|
|
15
|
+
timestamp: Date.now(),
|
|
16
|
+
...data
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
const url = new URL('/hook', SERVER);
|
|
20
|
+
const options = {
|
|
21
|
+
hostname: url.hostname,
|
|
22
|
+
port: url.port || 80,
|
|
23
|
+
path: url.pathname,
|
|
24
|
+
method: 'POST',
|
|
25
|
+
headers: {
|
|
26
|
+
'Content-Type': 'application/json',
|
|
27
|
+
'Content-Length': Buffer.byteLength(payload)
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const req = http.request(options, (res) => {
|
|
32
|
+
resolve(`${eventName}: ${res.statusCode}`);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
req.on('error', reject);
|
|
36
|
+
req.write(payload);
|
|
37
|
+
req.end();
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function wait(ms) {
|
|
42
|
+
return new Promise(r => setTimeout(r, ms));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function agentAction(agentId, tool, filePath, delay = 800) {
|
|
46
|
+
await sendHook('PreToolUse', {
|
|
47
|
+
tool_name: tool,
|
|
48
|
+
tool_input: { file_path: filePath, pattern: filePath },
|
|
49
|
+
agent_id: agentId
|
|
50
|
+
});
|
|
51
|
+
await wait(delay);
|
|
52
|
+
await sendHook('PostToolUse', {
|
|
53
|
+
tool_name: tool,
|
|
54
|
+
tool_input: { file_path: filePath, pattern: filePath },
|
|
55
|
+
agent_id: agentId
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function testAgentAnimationsLong() {
|
|
60
|
+
console.log('🚀 Extended Agent Animation Test (4 agents, many actions)\n');
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
// Spawn 4 agents
|
|
64
|
+
console.log('📍 Spawning 4 agents...');
|
|
65
|
+
for (let i = 1; i <= 4; i++) {
|
|
66
|
+
await sendHook('SubagentStart', {
|
|
67
|
+
agent_id: `agent-${i}`,
|
|
68
|
+
agent_type: 'claude-opus-4-6'
|
|
69
|
+
});
|
|
70
|
+
console.log(` Agent ${i} spawned`);
|
|
71
|
+
await wait(300);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
await wait(1000);
|
|
75
|
+
|
|
76
|
+
// Agent 1: Multiple reads
|
|
77
|
+
console.log('\n📖 Agent 1: Multiple read operations...');
|
|
78
|
+
await agentAction('agent-1', 'Read', 'client/src/store.ts', 1000);
|
|
79
|
+
await wait(500);
|
|
80
|
+
await agentAction('agent-1', 'Read', 'client/src/types.ts', 1000);
|
|
81
|
+
await wait(500);
|
|
82
|
+
await agentAction('agent-1', 'Read', 'README.md', 1000);
|
|
83
|
+
|
|
84
|
+
// Agent 2: Grep operations
|
|
85
|
+
console.log('\n🔎 Agent 2: Search operations...');
|
|
86
|
+
await agentAction('agent-2', 'Grep', 'agentPositionMap', 1000);
|
|
87
|
+
await wait(500);
|
|
88
|
+
await agentAction('agent-2', 'Grep', 'getAnimationOrigin', 1000);
|
|
89
|
+
|
|
90
|
+
// Agent 3: Glob operations
|
|
91
|
+
console.log('\n📂 Agent 3: File matching operations...');
|
|
92
|
+
await agentAction('agent-3', 'Glob', 'client/src/**/*.tsx', 1200);
|
|
93
|
+
await wait(600);
|
|
94
|
+
await agentAction('agent-3', 'Glob', '**/*.ts', 1200);
|
|
95
|
+
|
|
96
|
+
// Agent 4: Mixed operations
|
|
97
|
+
console.log('\n🔄 Agent 4: Mixed operations...');
|
|
98
|
+
await agentAction('agent-4', 'Read', 'package.json', 1000);
|
|
99
|
+
await wait(500);
|
|
100
|
+
await agentAction('agent-4', 'Grep', 'vite', 1000);
|
|
101
|
+
|
|
102
|
+
await wait(1000);
|
|
103
|
+
|
|
104
|
+
// More concurrent-looking operations
|
|
105
|
+
console.log('\n⚡ Agents 1 & 2: Concurrent operations...');
|
|
106
|
+
await Promise.all([
|
|
107
|
+
agentAction('agent-1', 'Read', 'client/src/canvas/renderer.ts', 1200),
|
|
108
|
+
agentAction('agent-2', 'Grep', 'projectile', 1200)
|
|
109
|
+
]);
|
|
110
|
+
|
|
111
|
+
await wait(800);
|
|
112
|
+
|
|
113
|
+
// Agent 3 continues
|
|
114
|
+
console.log('\n📂 Agent 3: More operations...');
|
|
115
|
+
await agentAction('agent-3', 'Glob', 'docs/**/*.md', 1200);
|
|
116
|
+
|
|
117
|
+
await wait(1000);
|
|
118
|
+
|
|
119
|
+
// Final operations before termination
|
|
120
|
+
console.log('\n🎬 Final actions before termination...');
|
|
121
|
+
await agentAction('agent-1', 'Read', 'client/src/canvas/graph.ts', 1000);
|
|
122
|
+
await wait(400);
|
|
123
|
+
await agentAction('agent-4', 'Grep', 'animation', 1000);
|
|
124
|
+
await wait(400);
|
|
125
|
+
await agentAction('agent-2', 'Read', '.gitignore', 1000);
|
|
126
|
+
|
|
127
|
+
await wait(1500);
|
|
128
|
+
|
|
129
|
+
// Terminate agents one by one
|
|
130
|
+
console.log('\n🛑 Terminating agents...');
|
|
131
|
+
for (let i = 1; i <= 4; i++) {
|
|
132
|
+
await sendHook('SubagentStop', { agent_id: `agent-${i}` });
|
|
133
|
+
console.log(` Agent ${i} terminated (star fading out)`);
|
|
134
|
+
await wait(600);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
console.log('\n✅ Extended test complete! All agents have terminated.\n');
|
|
138
|
+
} catch (err) {
|
|
139
|
+
console.error('❌ Error:', err.message);
|
|
140
|
+
process.exit(1);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
testAgentAnimationsLong();
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Test script to trigger agent animation routing by sending hooks to claude-live server
|
|
3
|
+
|
|
4
|
+
import http from 'http';
|
|
5
|
+
import { URL } from 'url';
|
|
6
|
+
|
|
7
|
+
const SERVER = 'http://localhost:43453';
|
|
8
|
+
const SESSION_ID = 'test-session-' + Date.now();
|
|
9
|
+
|
|
10
|
+
function sendHook(eventName, data = {}) {
|
|
11
|
+
return new Promise((resolve, reject) => {
|
|
12
|
+
const payload = JSON.stringify({
|
|
13
|
+
hook_event_name: eventName,
|
|
14
|
+
session_id: SESSION_ID,
|
|
15
|
+
timestamp: Date.now(),
|
|
16
|
+
...data
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
const url = new URL('/hook', SERVER);
|
|
20
|
+
const options = {
|
|
21
|
+
hostname: url.hostname,
|
|
22
|
+
port: url.port || 80,
|
|
23
|
+
path: url.pathname,
|
|
24
|
+
method: 'POST',
|
|
25
|
+
headers: {
|
|
26
|
+
'Content-Type': 'application/json',
|
|
27
|
+
'Content-Length': Buffer.byteLength(payload)
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const req = http.request(options, (res) => {
|
|
32
|
+
resolve(`${eventName}: ${res.statusCode}`);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
req.on('error', reject);
|
|
36
|
+
req.write(payload);
|
|
37
|
+
req.end();
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function testAgentAnimations() {
|
|
42
|
+
console.log('🚀 Testing Agent Animation Routing...\n');
|
|
43
|
+
|
|
44
|
+
try {
|
|
45
|
+
// Agent 1: Spawn and read
|
|
46
|
+
console.log('📍 Agent 1: Spawning...');
|
|
47
|
+
await sendHook('SubagentStart', {
|
|
48
|
+
agent_id: 'agent-1',
|
|
49
|
+
agent_type: 'claude-opus-4-6'
|
|
50
|
+
});
|
|
51
|
+
await new Promise(r => setTimeout(r, 500));
|
|
52
|
+
|
|
53
|
+
console.log('📖 Agent 1: Reading file...');
|
|
54
|
+
await sendHook('PreToolUse', {
|
|
55
|
+
tool_name: 'Read',
|
|
56
|
+
tool_input: { file_path: 'client/src/store.ts' },
|
|
57
|
+
agent_id: 'agent-1'
|
|
58
|
+
});
|
|
59
|
+
await new Promise(r => setTimeout(r, 300));
|
|
60
|
+
|
|
61
|
+
await sendHook('PostToolUse', {
|
|
62
|
+
tool_name: 'Read',
|
|
63
|
+
tool_input: { file_path: 'client/src/store.ts' },
|
|
64
|
+
agent_id: 'agent-1'
|
|
65
|
+
});
|
|
66
|
+
await new Promise(r => setTimeout(r, 500));
|
|
67
|
+
|
|
68
|
+
// Agent 2: Spawn and read
|
|
69
|
+
console.log('📍 Agent 2: Spawning...');
|
|
70
|
+
await sendHook('SubagentStart', {
|
|
71
|
+
agent_id: 'agent-2',
|
|
72
|
+
agent_type: 'claude-opus-4-6'
|
|
73
|
+
});
|
|
74
|
+
await new Promise(r => setTimeout(r, 500));
|
|
75
|
+
|
|
76
|
+
console.log('📖 Agent 2: Reading file...');
|
|
77
|
+
await sendHook('PreToolUse', {
|
|
78
|
+
tool_name: 'Read',
|
|
79
|
+
tool_input: { file_path: 'README.md' },
|
|
80
|
+
agent_id: 'agent-2'
|
|
81
|
+
});
|
|
82
|
+
await new Promise(r => setTimeout(r, 300));
|
|
83
|
+
|
|
84
|
+
await sendHook('PostToolUse', {
|
|
85
|
+
tool_name: 'Read',
|
|
86
|
+
tool_input: { file_path: 'README.md' },
|
|
87
|
+
agent_id: 'agent-2'
|
|
88
|
+
});
|
|
89
|
+
await new Promise(r => setTimeout(r, 500));
|
|
90
|
+
|
|
91
|
+
// Agent 1: Glob operation
|
|
92
|
+
console.log('🔍 Agent 1: Globbing...');
|
|
93
|
+
await sendHook('PreToolUse', {
|
|
94
|
+
tool_name: 'Glob',
|
|
95
|
+
tool_input: { pattern: 'client/src/**/*.tsx' },
|
|
96
|
+
agent_id: 'agent-1'
|
|
97
|
+
});
|
|
98
|
+
await new Promise(r => setTimeout(r, 300));
|
|
99
|
+
|
|
100
|
+
await sendHook('PostToolUse', {
|
|
101
|
+
tool_name: 'Glob',
|
|
102
|
+
tool_input: { pattern: 'client/src/**/*.tsx' },
|
|
103
|
+
agent_id: 'agent-1'
|
|
104
|
+
});
|
|
105
|
+
await new Promise(r => setTimeout(r, 500));
|
|
106
|
+
|
|
107
|
+
// Terminate agents
|
|
108
|
+
console.log('🛑 Agent 1: Terminating...');
|
|
109
|
+
await sendHook('SubagentStop', {
|
|
110
|
+
agent_id: 'agent-1'
|
|
111
|
+
});
|
|
112
|
+
await new Promise(r => setTimeout(r, 300));
|
|
113
|
+
|
|
114
|
+
console.log('🛑 Agent 2: Terminating...');
|
|
115
|
+
await sendHook('SubagentStop', {
|
|
116
|
+
agent_id: 'agent-2'
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
console.log('\n✅ Test complete! Check http://localhost:43451 for animations.\n');
|
|
120
|
+
} catch (err) {
|
|
121
|
+
console.error('❌ Error:', err.message);
|
|
122
|
+
process.exit(1);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
testAgentAnimations();
|
package/test-agents.js
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { fileURLToPath } from "url";
|
|
4
|
+
|
|
5
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
6
|
+
|
|
7
|
+
// Get all files in current directory
|
|
8
|
+
function getAllFiles(dir = ".") {
|
|
9
|
+
try {
|
|
10
|
+
return fs
|
|
11
|
+
.readdirSync(dir)
|
|
12
|
+
.filter((f) => fs.statSync(path.join(dir, f)).isFile())
|
|
13
|
+
.map((f) => path.join(dir, f));
|
|
14
|
+
} catch {
|
|
15
|
+
return [];
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Pick random items from array
|
|
20
|
+
function pickRandom(arr, count) {
|
|
21
|
+
const shuffled = [...arr].sort(() => Math.random() - 0.5);
|
|
22
|
+
return shuffled.slice(0, Math.min(count, arr.length));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Agent function
|
|
26
|
+
async function agent(id, interval = 2000) {
|
|
27
|
+
console.log(`[Agent ${id}] Started`);
|
|
28
|
+
|
|
29
|
+
const tick = () => {
|
|
30
|
+
const allFiles = getAllFiles();
|
|
31
|
+
const randomCount = Math.floor(Math.random() * 3) + 1; // 1-3 files
|
|
32
|
+
const randomFiles = pickRandom(allFiles, randomCount);
|
|
33
|
+
|
|
34
|
+
console.log(
|
|
35
|
+
`[Agent ${id}] Processing ${randomFiles.length} files:`,
|
|
36
|
+
randomFiles.join(", ")
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
// Simulate work
|
|
40
|
+
randomFiles.forEach((file) => {
|
|
41
|
+
try {
|
|
42
|
+
const stats = fs.statSync(file);
|
|
43
|
+
console.log(` ├─ ${file}: ${stats.size} bytes`);
|
|
44
|
+
} catch {
|
|
45
|
+
console.log(` ├─ ${file}: (error reading)`);
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
// Run immediately and then every interval
|
|
51
|
+
tick();
|
|
52
|
+
setInterval(tick, interval);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Start 3 agents
|
|
56
|
+
console.log("Starting 3 test agents...\n");
|
|
57
|
+
agent(1);
|
|
58
|
+
agent(2);
|
|
59
|
+
agent(3);
|
|
60
|
+
|
|
61
|
+
console.log("\nPress Ctrl+C to stop\n");
|
package/tests/store.test.ts
CHANGED
|
@@ -66,4 +66,76 @@ describe('store', () => {
|
|
|
66
66
|
expect(node.age).toBeGreaterThanOrEqual(80)
|
|
67
67
|
}
|
|
68
68
|
})
|
|
69
|
+
|
|
70
|
+
it('spawns ResponseSnakes on PostToolUse Write with words', () => {
|
|
71
|
+
const store = createStore()
|
|
72
|
+
store.addEvent(makeEvent({ id: '1', tool_input: { file_path: '/test.ts' }, tool_name: 'Write' }))
|
|
73
|
+
// First pass: replay phase (skipAnimations=true)
|
|
74
|
+
store.addEvent(makeEvent({
|
|
75
|
+
id: '2',
|
|
76
|
+
hook_event_name: 'PostToolUse',
|
|
77
|
+
tool_name: 'Write',
|
|
78
|
+
tool_input: { file_path: '/test.ts', content: 'function hello world test animation' },
|
|
79
|
+
tool_response: null
|
|
80
|
+
}), true)
|
|
81
|
+
let cluster = store.getSessions().get('sess-1')!
|
|
82
|
+
expect(cluster.promptSnakes.length).toBe(0) // No snakes during replay
|
|
83
|
+
|
|
84
|
+
// Second pass: live phase (skipAnimations=false)
|
|
85
|
+
store.addEvent(makeEvent({
|
|
86
|
+
id: '3',
|
|
87
|
+
hook_event_name: 'PostToolUse',
|
|
88
|
+
tool_name: 'Write',
|
|
89
|
+
tool_input: { file_path: '/test.ts', content: 'function hello world test animation' },
|
|
90
|
+
tool_response: null
|
|
91
|
+
}), false)
|
|
92
|
+
cluster = store.getSessions().get('sess-1')!
|
|
93
|
+
expect(cluster.promptSnakes.length).toBe(1)
|
|
94
|
+
expect(cluster.promptSnakes[0].words.length).toBeGreaterThan(0)
|
|
95
|
+
expect(cluster.promptSnakes[0].words[0]).toBe('function')
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
it('spawns ResponseSnakes on PostToolUse Read with file content', () => {
|
|
99
|
+
const store = createStore()
|
|
100
|
+
store.addEvent(makeEvent({ id: '1', tool_input: { file_path: '/test.ts' } }))
|
|
101
|
+
store.addEvent(makeEvent({
|
|
102
|
+
id: '2',
|
|
103
|
+
hook_event_name: 'PostToolUse',
|
|
104
|
+
tool_name: 'Read',
|
|
105
|
+
tool_input: { file_path: '/test.ts' },
|
|
106
|
+
tool_response: { type: 'text', file: { filePath: '/test.ts', content: 'const x = 42 testing' } }
|
|
107
|
+
}), false)
|
|
108
|
+
const cluster = store.getSessions().get('sess-1')!
|
|
109
|
+
expect(cluster.promptSnakes.length).toBe(1)
|
|
110
|
+
expect(cluster.promptSnakes[0].words[0]).toBe('const')
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
it('spawns ResponseSnakes on PostToolUse Bash with stdout', () => {
|
|
114
|
+
const store = createStore()
|
|
115
|
+
store.addEvent(makeEvent({ id: '1', tool_input: { file_path: '/test.sh' } }))
|
|
116
|
+
store.addEvent(makeEvent({
|
|
117
|
+
id: '2',
|
|
118
|
+
hook_event_name: 'PostToolUse',
|
|
119
|
+
tool_name: 'Bash',
|
|
120
|
+
tool_input: { command: 'echo hello world' },
|
|
121
|
+
tool_response: { stdout: 'hello world from bash output', stderr: '', interrupted: false }
|
|
122
|
+
}), false)
|
|
123
|
+
const cluster = store.getSessions().get('sess-1')!
|
|
124
|
+
expect(cluster.promptSnakes.length).toBe(1)
|
|
125
|
+
expect(cluster.promptSnakes[0].words[0]).toBe('hello')
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
it('skips ResponseSnakes during replay phase', () => {
|
|
129
|
+
const store = createStore()
|
|
130
|
+
store.addEvent(makeEvent({ id: '1', tool_input: { file_path: '/test.ts' } }))
|
|
131
|
+
store.addEvent(makeEvent({
|
|
132
|
+
id: '2',
|
|
133
|
+
hook_event_name: 'PostToolUse',
|
|
134
|
+
tool_name: 'Write',
|
|
135
|
+
tool_input: { content: 'test content words' },
|
|
136
|
+
tool_response: null
|
|
137
|
+
}), true) // skipAnimations=true
|
|
138
|
+
const cluster = store.getSessions().get('sess-1')!
|
|
139
|
+
expect(cluster.promptSnakes.length).toBe(0)
|
|
140
|
+
})
|
|
69
141
|
})
|