uv-suite 0.20.0 → 0.22.0
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/hooks/watchtower-send.sh +22 -9
- package/package.json +1 -1
- package/personas/auto.json +10 -1
- package/personas/professional.json +19 -0
- package/personas/spike.json +10 -1
- package/personas/sport.json +10 -1
- package/watchtower/dashboard.html +159 -27
package/hooks/watchtower-send.sh
CHANGED
|
@@ -1,24 +1,37 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
2
|
# UV Suite Hook Helper: Send event to Watchtower server
|
|
3
|
-
# Called by other hooks
|
|
3
|
+
# Called by other hooks or directly from persona hook config.
|
|
4
|
+
# Non-blocking. Fails silently if server not running.
|
|
4
5
|
#
|
|
5
|
-
# Usage
|
|
6
|
+
# Usage from hook config:
|
|
7
|
+
# "command": ".claude/hooks/watchtower-send.sh PostToolUse"
|
|
8
|
+
# The hook input JSON comes via stdin from Claude Code.
|
|
6
9
|
|
|
7
10
|
EVENT_TYPE="${1:-Unknown}"
|
|
8
11
|
INPUT=$(cat)
|
|
9
12
|
WATCHTOWER_URL="${UVS_WATCHTOWER_URL:-http://localhost:4200}"
|
|
10
13
|
|
|
11
|
-
#
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
14
|
+
# Forward the full hook input to the watchtower, adding event_type and source_app
|
|
15
|
+
# jq merges the original JSON with our extra fields
|
|
16
|
+
PAYLOAD=$(echo "$INPUT" | jq -c ". + {
|
|
17
|
+
event_type: \"$EVENT_TYPE\",
|
|
18
|
+
source_app: (.cwd // \"\" | split(\"/\") | last),
|
|
19
|
+
_hook_ts: now
|
|
20
|
+
}" 2>/dev/null)
|
|
21
|
+
|
|
22
|
+
# Fallback if jq isn't available or fails
|
|
23
|
+
if [ -z "$PAYLOAD" ] || [ "$PAYLOAD" = "null" ]; then
|
|
24
|
+
SESSION_ID=$(echo "$INPUT" | grep -o '"session_id":"[^"]*"' | head -1 | cut -d'"' -f4)
|
|
25
|
+
TOOL_NAME=$(echo "$INPUT" | grep -o '"tool_name":"[^"]*"' | head -1 | cut -d'"' -f4)
|
|
26
|
+
CWD=$(echo "$INPUT" | grep -o '"cwd":"[^"]*"' | head -1 | cut -d'"' -f4)
|
|
27
|
+
SOURCE_APP=$(basename "$CWD" 2>/dev/null)
|
|
28
|
+
PAYLOAD="{\"event_type\":\"$EVENT_TYPE\",\"session_id\":\"$SESSION_ID\",\"source_app\":\"$SOURCE_APP\",\"tool_name\":\"$TOOL_NAME\",\"cwd\":\"$CWD\"}"
|
|
29
|
+
fi
|
|
17
30
|
|
|
18
31
|
# Send to watchtower (non-blocking, fire-and-forget)
|
|
19
32
|
curl -s -X POST "$WATCHTOWER_URL/events" \
|
|
20
33
|
-H "Content-Type: application/json" \
|
|
21
|
-
-d "
|
|
34
|
+
-d "$PAYLOAD" \
|
|
22
35
|
&>/dev/null &
|
|
23
36
|
|
|
24
37
|
exit 0
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "uv-suite",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.22.0",
|
|
4
4
|
"description": "Portable framework for AI-assisted software development. 10 agents, 9 skills, 5 hooks, 4 personas. Works with Claude Code, Cursor, and Codex.",
|
|
5
5
|
"author": "Utsav Anand",
|
|
6
6
|
"license": "MIT",
|
package/personas/auto.json
CHANGED
|
@@ -54,7 +54,16 @@
|
|
|
54
54
|
{
|
|
55
55
|
"matcher": "*",
|
|
56
56
|
"hooks": [
|
|
57
|
-
{ "type": "command", "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/session-start.sh", "timeout": 5 }
|
|
57
|
+
{ "type": "command", "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/session-start.sh", "timeout": 5 },
|
|
58
|
+
{ "type": "command", "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/watchtower-send.sh SessionStart", "timeout": 2, "async": true }
|
|
59
|
+
]
|
|
60
|
+
}
|
|
61
|
+
],
|
|
62
|
+
"UserPromptSubmit": [
|
|
63
|
+
{
|
|
64
|
+
"matcher": "*",
|
|
65
|
+
"hooks": [
|
|
66
|
+
{ "type": "command", "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/watchtower-send.sh UserPromptSubmit", "timeout": 2, "async": true }
|
|
58
67
|
]
|
|
59
68
|
}
|
|
60
69
|
],
|
|
@@ -56,6 +56,25 @@
|
|
|
56
56
|
"type": "command",
|
|
57
57
|
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/session-start.sh",
|
|
58
58
|
"timeout": 5
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
"type": "command",
|
|
62
|
+
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/watchtower-send.sh SessionStart",
|
|
63
|
+
"timeout": 2,
|
|
64
|
+
"async": true
|
|
65
|
+
}
|
|
66
|
+
]
|
|
67
|
+
}
|
|
68
|
+
],
|
|
69
|
+
"UserPromptSubmit": [
|
|
70
|
+
{
|
|
71
|
+
"matcher": "*",
|
|
72
|
+
"hooks": [
|
|
73
|
+
{
|
|
74
|
+
"type": "command",
|
|
75
|
+
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/watchtower-send.sh UserPromptSubmit",
|
|
76
|
+
"timeout": 2,
|
|
77
|
+
"async": true
|
|
59
78
|
}
|
|
60
79
|
]
|
|
61
80
|
}
|
package/personas/spike.json
CHANGED
|
@@ -40,7 +40,16 @@
|
|
|
40
40
|
{
|
|
41
41
|
"matcher": "*",
|
|
42
42
|
"hooks": [
|
|
43
|
-
{ "type": "command", "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/session-start.sh", "timeout": 5 }
|
|
43
|
+
{ "type": "command", "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/session-start.sh", "timeout": 5 },
|
|
44
|
+
{ "type": "command", "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/watchtower-send.sh SessionStart", "timeout": 2, "async": true }
|
|
45
|
+
]
|
|
46
|
+
}
|
|
47
|
+
],
|
|
48
|
+
"UserPromptSubmit": [
|
|
49
|
+
{
|
|
50
|
+
"matcher": "*",
|
|
51
|
+
"hooks": [
|
|
52
|
+
{ "type": "command", "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/watchtower-send.sh UserPromptSubmit", "timeout": 2, "async": true }
|
|
44
53
|
]
|
|
45
54
|
}
|
|
46
55
|
],
|
package/personas/sport.json
CHANGED
|
@@ -26,7 +26,16 @@
|
|
|
26
26
|
{
|
|
27
27
|
"matcher": "*",
|
|
28
28
|
"hooks": [
|
|
29
|
-
{ "type": "command", "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/session-start.sh", "timeout": 5 }
|
|
29
|
+
{ "type": "command", "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/session-start.sh", "timeout": 5 },
|
|
30
|
+
{ "type": "command", "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/watchtower-send.sh SessionStart", "timeout": 2, "async": true }
|
|
31
|
+
]
|
|
32
|
+
}
|
|
33
|
+
],
|
|
34
|
+
"UserPromptSubmit": [
|
|
35
|
+
{
|
|
36
|
+
"matcher": "*",
|
|
37
|
+
"hooks": [
|
|
38
|
+
{ "type": "command", "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/watchtower-send.sh UserPromptSubmit", "timeout": 2, "async": true }
|
|
30
39
|
]
|
|
31
40
|
}
|
|
32
41
|
],
|
|
@@ -23,20 +23,35 @@
|
|
|
23
23
|
.stat { text-align: center; }
|
|
24
24
|
.stat .n { font-size: 20px; font-weight: 600; }
|
|
25
25
|
.stat .l { font-size: 10px; color: #86868b; text-transform: uppercase; letter-spacing: 0.5px; margin-top: 2px; }
|
|
26
|
+
.stat .n.alert { color: #ff375f; }
|
|
26
27
|
|
|
27
28
|
.sessions { padding: 12px 24px; border-bottom: 1px solid #1d1d1f; display: flex; gap: 8px; flex-wrap: wrap; }
|
|
28
29
|
.session-tag { padding: 3px 10px; border-radius: 12px; font-size: 11px; font-weight: 500; cursor: pointer; border: 1px solid transparent; }
|
|
29
30
|
.session-tag:hover { border-color: #424245; }
|
|
30
31
|
.session-tag.active { border-color: #fff; }
|
|
31
32
|
|
|
32
|
-
.timeline { padding: 8px 0; overflow-y: auto; max-height: calc(100vh -
|
|
33
|
-
.event { padding:
|
|
33
|
+
.timeline { padding: 8px 0; overflow-y: auto; max-height: calc(100vh - 240px); }
|
|
34
|
+
.event { padding: 8px 24px; display: grid; grid-template-columns: 70px 110px 120px 60px 1fr; gap: 8px; align-items: start; font-size: 12px; border-bottom: 1px solid #0d0d0d; transition: background 0.15s; }
|
|
34
35
|
.event:hover { background: #0d0d0d; }
|
|
35
36
|
.event .time { color: #6e6e73; font-variant-numeric: tabular-nums; font-family: 'SF Mono', monospace; font-size: 11px; }
|
|
36
37
|
.event .type { font-weight: 500; }
|
|
37
38
|
.event .session { font-size: 11px; border-radius: 8px; padding: 1px 8px; display: inline-block; }
|
|
38
|
-
.event .
|
|
39
|
-
.event .
|
|
39
|
+
.event .tool { color: #a1a1a6; font-family: 'SF Mono', monospace; font-size: 11px; }
|
|
40
|
+
.event .detail { color: #a1a1a6; font-size: 11px; line-height: 1.4; }
|
|
41
|
+
.event .detail .filepath { color: #64d2ff; }
|
|
42
|
+
.event .detail .cmd { color: #ffd60a; }
|
|
43
|
+
|
|
44
|
+
/* Human intervention — pulsing highlight */
|
|
45
|
+
.event.needs-human { background: #ff375f12; border-left: 3px solid #ff375f; }
|
|
46
|
+
.event.needs-human .type { color: #ff375f; }
|
|
47
|
+
.event.needs-human::after { content: 'NEEDS HUMAN'; position: absolute; right: 24px; font-size: 9px; font-weight: 700; color: #ff375f; letter-spacing: 0.5px; }
|
|
48
|
+
.event.needs-human { position: relative; }
|
|
49
|
+
|
|
50
|
+
/* Failure highlight */
|
|
51
|
+
.event.failure { background: #ff696112; border-left: 3px solid #ff6961; }
|
|
52
|
+
|
|
53
|
+
/* Session start/end highlight */
|
|
54
|
+
.event.session-boundary { background: #30d15808; }
|
|
40
55
|
|
|
41
56
|
.empty { padding: 60px 24px; text-align: center; color: #6e6e73; }
|
|
42
57
|
.empty p { margin-top: 8px; font-size: 13px; }
|
|
@@ -67,6 +82,7 @@
|
|
|
67
82
|
<div class="stat"><div class="n" id="eventCount">0</div><div class="l">Events</div></div>
|
|
68
83
|
<div class="stat"><div class="n" id="toolCount">0</div><div class="l">Tool calls</div></div>
|
|
69
84
|
<div class="stat"><div class="n" id="errorCount">0</div><div class="l">Errors</div></div>
|
|
85
|
+
<div class="stat"><div class="n" id="humanCount">0</div><div class="l">Need human</div></div>
|
|
70
86
|
</div>
|
|
71
87
|
|
|
72
88
|
<div class="sessions" id="sessionBar"></div>
|
|
@@ -74,6 +90,7 @@
|
|
|
74
90
|
<div class="filters">
|
|
75
91
|
<select id="filterType"><option value="">All events</option></select>
|
|
76
92
|
<select id="filterSession"><option value="">All sessions</option></select>
|
|
93
|
+
<button id="btnHumanOnly" onclick="toggleHumanOnly()">Human needed</button>
|
|
77
94
|
<button id="btnClear" onclick="clearEvents()">Clear</button>
|
|
78
95
|
<button id="btnAutoScroll" class="active" onclick="toggleAutoScroll()">Auto-scroll</button>
|
|
79
96
|
</div>
|
|
@@ -97,16 +114,16 @@ const sessionBar = document.getElementById('sessionBar');
|
|
|
97
114
|
let events = [];
|
|
98
115
|
let sessions = {};
|
|
99
116
|
let autoScroll = true;
|
|
117
|
+
let humanOnly = false;
|
|
100
118
|
let selectedSession = '';
|
|
101
119
|
let selectedType = '';
|
|
102
|
-
let ws;
|
|
103
120
|
|
|
104
|
-
// Session colors
|
|
121
|
+
// Session colors and naming
|
|
105
122
|
const palette = ['#0a84ff','#30d158','#ff9f0a','#bf5af2','#ff375f','#64d2ff','#ffd60a','#ac8ee0','#ff6961','#5e5ce6'];
|
|
106
123
|
let colorIdx = 0;
|
|
107
124
|
function sessionColor(id) {
|
|
108
125
|
if (!sessions[id]) {
|
|
109
|
-
sessions[id] = { color: palette[colorIdx++ % palette.length], count: 0, lastEvent: null };
|
|
126
|
+
sessions[id] = { color: palette[colorIdx++ % palette.length], count: 0, lastEvent: null, label: null, app: null };
|
|
110
127
|
updateSessionBar();
|
|
111
128
|
updateFilterSession();
|
|
112
129
|
}
|
|
@@ -115,41 +132,145 @@ function sessionColor(id) {
|
|
|
115
132
|
return sessions[id].color;
|
|
116
133
|
}
|
|
117
134
|
|
|
135
|
+
// Build a meaningful session label from the first user prompt or app name
|
|
136
|
+
function updateSessionLabel(sid, ev) {
|
|
137
|
+
if (!sessions[sid]) return;
|
|
138
|
+
// Capture source_app as fallback name
|
|
139
|
+
if (!sessions[sid].app && ev.source_app) {
|
|
140
|
+
sessions[sid].app = ev.source_app;
|
|
141
|
+
}
|
|
142
|
+
// Use first UserPromptSubmit content as the session label
|
|
143
|
+
if (!sessions[sid].label && ev.event_type === 'UserPromptSubmit') {
|
|
144
|
+
const prompt = ev.tool_input?.prompt || ev.tool_input?.content || ev.message || '';
|
|
145
|
+
if (prompt.length > 0) {
|
|
146
|
+
// Take first 40 chars, trim to last word boundary
|
|
147
|
+
let label = prompt.slice(0, 40).replace(/\s+\S*$/, '');
|
|
148
|
+
if (prompt.length > label.length) label += '...';
|
|
149
|
+
sessions[sid].label = label;
|
|
150
|
+
updateSessionBar();
|
|
151
|
+
updateFilterSession();
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function sessionDisplayName(id) {
|
|
157
|
+
const s = sessions[id];
|
|
158
|
+
if (!s) return shortId(id);
|
|
159
|
+
if (s.label) return s.label;
|
|
160
|
+
if (s.app) return s.app;
|
|
161
|
+
return shortId(id);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function shortId(id) {
|
|
165
|
+
if (!id) return '—';
|
|
166
|
+
return id.length > 10 ? id.slice(0, 8) + '..' : id;
|
|
167
|
+
}
|
|
168
|
+
|
|
118
169
|
function formatTime(ts) {
|
|
119
170
|
const d = new Date(ts);
|
|
120
171
|
return d.toLocaleTimeString('en-US', { hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit' });
|
|
121
172
|
}
|
|
122
173
|
|
|
123
174
|
function shortSession(id) {
|
|
124
|
-
|
|
125
|
-
return id.length > 12 ? id.slice(0, 8) + '...' : id;
|
|
175
|
+
return sessionDisplayName(id);
|
|
126
176
|
}
|
|
127
177
|
|
|
178
|
+
// Detect if event needs human intervention
|
|
179
|
+
function needsHuman(ev) {
|
|
180
|
+
const type = ev.event_type || ev.hook_event_name || '';
|
|
181
|
+
// Permission requests always need human
|
|
182
|
+
if (type === 'PermissionRequest') return true;
|
|
183
|
+
// Notifications that are permission prompts
|
|
184
|
+
if (type === 'Notification' && ev.notification_type === 'permission_prompt') return true;
|
|
185
|
+
// Tool failures after multiple attempts
|
|
186
|
+
if (type === 'PostToolUseFailure') return true;
|
|
187
|
+
// Stuck indicators in messages
|
|
188
|
+
if (ev.message && /stuck|blocked|escalat|need.*help|can.?t.*find/i.test(ev.message)) return true;
|
|
189
|
+
return false;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function isFailure(ev) {
|
|
193
|
+
const type = ev.event_type || ev.hook_event_name || '';
|
|
194
|
+
return type.includes('Failure') || type === 'StopFailure';
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function isSessionBoundary(ev) {
|
|
198
|
+
const type = ev.event_type || ev.hook_event_name || '';
|
|
199
|
+
return type === 'SessionStart' || type === 'SessionEnd' || type === 'Stop';
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Build rich detail string
|
|
128
203
|
function eventDetail(ev) {
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
204
|
+
const parts = [];
|
|
205
|
+
const tool = ev.tool_name || '';
|
|
206
|
+
const input = ev.tool_input || {};
|
|
207
|
+
|
|
208
|
+
if (tool === 'Read' && input.file_path) {
|
|
209
|
+
parts.push(`<span class="filepath">${input.file_path}</span>`);
|
|
210
|
+
if (input.offset) parts.push(`offset:${input.offset}`);
|
|
211
|
+
if (input.limit) parts.push(`limit:${input.limit}`);
|
|
212
|
+
} else if (tool === 'Write' && input.file_path) {
|
|
213
|
+
parts.push(`<span class="filepath">${input.file_path}</span>`);
|
|
214
|
+
if (input.content) parts.push(`(${input.content.length} chars)`);
|
|
215
|
+
} else if (tool === 'Edit' && input.file_path) {
|
|
216
|
+
parts.push(`<span class="filepath">${input.file_path}</span>`);
|
|
217
|
+
if (input.old_string) parts.push(`replacing "${input.old_string.slice(0, 30)}..."`);
|
|
218
|
+
} else if (tool === 'Bash' && input.command) {
|
|
219
|
+
parts.push(`<span class="cmd">$ ${input.command.slice(0, 80)}</span>`);
|
|
220
|
+
if (input.timeout) parts.push(`timeout:${input.timeout}ms`);
|
|
221
|
+
} else if (tool === 'Grep' && input.pattern) {
|
|
222
|
+
parts.push(`pattern: "${input.pattern}"`);
|
|
223
|
+
if (input.path) parts.push(`in <span class="filepath">${input.path}</span>`);
|
|
224
|
+
} else if (tool === 'Glob' && input.pattern) {
|
|
225
|
+
parts.push(`<span class="filepath">${input.pattern}</span>`);
|
|
226
|
+
} else if (tool === 'Agent') {
|
|
227
|
+
parts.push(input.description || input.prompt?.slice(0, 60) || 'subagent');
|
|
228
|
+
} else if (tool === 'WebSearch' && input.query) {
|
|
229
|
+
parts.push(`"${input.query.slice(0, 60)}"`);
|
|
230
|
+
} else if (tool === 'WebFetch' && input.url) {
|
|
231
|
+
parts.push(`<span class="filepath">${input.url.slice(0, 60)}</span>`);
|
|
232
|
+
} else if (ev.message) {
|
|
233
|
+
parts.push(ev.message.slice(0, 80));
|
|
234
|
+
} else if (input.file_path) {
|
|
235
|
+
parts.push(`<span class="filepath">${input.file_path}</span>`);
|
|
236
|
+
} else if (input.command) {
|
|
237
|
+
parts.push(`<span class="cmd">$ ${input.command.slice(0, 60)}</span>`);
|
|
238
|
+
} else if (ev.source_app) {
|
|
239
|
+
parts.push(ev.source_app);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return parts.join(' ');
|
|
134
243
|
}
|
|
135
244
|
|
|
136
245
|
function renderEvent(ev) {
|
|
137
246
|
const sid = ev.session_id || ev.source_app || 'unknown';
|
|
138
247
|
const color = sessionColor(sid);
|
|
248
|
+
const type = ev.event_type || ev.hook_event_name || '?';
|
|
249
|
+
const tool = ev.tool_name || '';
|
|
250
|
+
const human = needsHuman(ev);
|
|
251
|
+
const fail = isFailure(ev);
|
|
252
|
+
const boundary = isSessionBoundary(ev);
|
|
253
|
+
|
|
139
254
|
const div = document.createElement('div');
|
|
140
|
-
|
|
255
|
+
let cls = 'event';
|
|
256
|
+
if (human) cls += ' needs-human';
|
|
257
|
+
else if (fail) cls += ' failure';
|
|
258
|
+
else if (boundary) cls += ' session-boundary';
|
|
259
|
+
div.className = cls;
|
|
260
|
+
|
|
141
261
|
div.innerHTML = `
|
|
142
262
|
<span class="time">${formatTime(ev._ts)}</span>
|
|
143
|
-
<span class="type type-${
|
|
263
|
+
<span class="type type-${type}">${type}</span>
|
|
144
264
|
<span class="session" style="background:${color}22;color:${color}">${shortSession(sid)}</span>
|
|
145
|
-
<span class="
|
|
146
|
-
<span class="
|
|
265
|
+
<span class="tool">${tool}</span>
|
|
266
|
+
<span class="detail">${eventDetail(ev)}</span>
|
|
147
267
|
`;
|
|
148
268
|
|
|
149
269
|
// Check filters
|
|
150
|
-
const typeMatch = !selectedType ||
|
|
270
|
+
const typeMatch = !selectedType || type === selectedType;
|
|
151
271
|
const sessMatch = !selectedSession || sid === selectedSession;
|
|
152
|
-
|
|
272
|
+
const humanMatch = !humanOnly || human;
|
|
273
|
+
if (!typeMatch || !sessMatch || !humanMatch) div.style.display = 'none';
|
|
153
274
|
|
|
154
275
|
return div;
|
|
155
276
|
}
|
|
@@ -157,6 +278,8 @@ function renderEvent(ev) {
|
|
|
157
278
|
function addEvent(ev) {
|
|
158
279
|
events.push(ev);
|
|
159
280
|
if (emptyState.parentNode) emptyState.remove();
|
|
281
|
+
const sid = ev.session_id || ev.source_app || 'unknown';
|
|
282
|
+
updateSessionLabel(sid, ev);
|
|
160
283
|
timeline.appendChild(renderEvent(ev));
|
|
161
284
|
updateStats();
|
|
162
285
|
updateFilterType(ev);
|
|
@@ -167,7 +290,12 @@ function updateStats() {
|
|
|
167
290
|
document.getElementById('sessionCount').textContent = Object.keys(sessions).length;
|
|
168
291
|
document.getElementById('eventCount').textContent = events.length;
|
|
169
292
|
document.getElementById('toolCount').textContent = events.filter(e => (e.event_type || e.hook_event_name || '').includes('ToolUse')).length;
|
|
170
|
-
|
|
293
|
+
const errors = events.filter(e => (e.event_type || e.hook_event_name || '').includes('Failure')).length;
|
|
294
|
+
document.getElementById('errorCount').textContent = errors;
|
|
295
|
+
document.getElementById('errorCount').className = 'n' + (errors > 0 ? ' alert' : '');
|
|
296
|
+
const humans = events.filter(needsHuman).length;
|
|
297
|
+
document.getElementById('humanCount').textContent = humans;
|
|
298
|
+
document.getElementById('humanCount').className = 'n' + (humans > 0 ? ' alert' : '');
|
|
171
299
|
}
|
|
172
300
|
|
|
173
301
|
function updateSessionBar() {
|
|
@@ -211,7 +339,9 @@ function refilter() {
|
|
|
211
339
|
const ev = events[i];
|
|
212
340
|
const sid = ev.session_id || ev.source_app || 'unknown';
|
|
213
341
|
const type = ev.event_type || ev.hook_event_name || '';
|
|
214
|
-
const show = (!selectedType || type === selectedType)
|
|
342
|
+
const show = (!selectedType || type === selectedType)
|
|
343
|
+
&& (!selectedSession || sid === selectedSession)
|
|
344
|
+
&& (!humanOnly || needsHuman(ev));
|
|
215
345
|
row.style.display = show ? '' : 'none';
|
|
216
346
|
});
|
|
217
347
|
}
|
|
@@ -224,8 +354,13 @@ function toggleAutoScroll() {
|
|
|
224
354
|
autoScroll = !autoScroll;
|
|
225
355
|
document.getElementById('btnAutoScroll').classList.toggle('active', autoScroll);
|
|
226
356
|
}
|
|
357
|
+
function toggleHumanOnly() {
|
|
358
|
+
humanOnly = !humanOnly;
|
|
359
|
+
document.getElementById('btnHumanOnly').classList.toggle('active', humanOnly);
|
|
360
|
+
refilter();
|
|
361
|
+
}
|
|
227
362
|
|
|
228
|
-
// Server-Sent Events connection
|
|
363
|
+
// Server-Sent Events connection
|
|
229
364
|
function connect() {
|
|
230
365
|
const source = new EventSource('/stream');
|
|
231
366
|
|
|
@@ -237,7 +372,6 @@ function connect() {
|
|
|
237
372
|
source.onerror = () => {
|
|
238
373
|
statusDot.className = 'dot off';
|
|
239
374
|
statusText.textContent = 'Reconnecting...';
|
|
240
|
-
// EventSource auto-reconnects — no manual retry needed
|
|
241
375
|
};
|
|
242
376
|
|
|
243
377
|
source.onmessage = (msg) => {
|
|
@@ -248,9 +382,7 @@ function connect() {
|
|
|
248
382
|
} else {
|
|
249
383
|
addEvent(data);
|
|
250
384
|
}
|
|
251
|
-
} catch (e) {
|
|
252
|
-
// ignore parse errors (ping frames, etc.)
|
|
253
|
-
}
|
|
385
|
+
} catch (e) {}
|
|
254
386
|
};
|
|
255
387
|
}
|
|
256
388
|
|