uv-suite 0.26.1 → 0.26.3
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/checkpoint-helper.sh +124 -0
- package/hooks/confirm-prompt.sh +51 -0
- package/hooks/session-label-nag.sh +63 -0
- package/hooks/session-meta.sh +121 -0
- package/hooks/session-start.sh +37 -9
- package/hooks/status-line.sh +50 -25
- package/hooks/watchtower-send.sh +44 -12
- package/install.sh +5 -3
- package/package.json +1 -1
- package/personas/auto.json +77 -19
- package/personas/professional.json +29 -7
- package/personas/spike.json +72 -18
- package/personas/sport.json +72 -18
- package/skills/checkpoint/SKILL.md +36 -11
- package/skills/confirm/SKILL.md +35 -0
- package/skills/restore/SKILL.md +40 -7
- package/skills/session-init/SKILL.md +45 -0
- package/uv.sh +84 -14
- package/watchtower/dashboard.html +125 -20
- package/watchtower/events.json +1 -22
package/skills/restore/SKILL.md
CHANGED
|
@@ -1,22 +1,55 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: restore
|
|
3
3
|
description: >
|
|
4
|
-
Restore the latest checkpoint
|
|
5
|
-
key decisions, current state, and what's next.
|
|
4
|
+
Restore the latest checkpoint for the current session — shows what was done,
|
|
5
|
+
key decisions, current state, and what's next. With no arguments, picks the
|
|
6
|
+
current `UVS_SESSION_ID`'s most recent checkpoint. Pass a session id prefix
|
|
7
|
+
or name to restore from a different session.
|
|
8
|
+
argument-hint: "[<session-id-prefix> | <session-name> | list]"
|
|
6
9
|
user-invocable: true
|
|
7
10
|
allowed-tools:
|
|
8
11
|
- Read(*)
|
|
9
12
|
- Bash(ls *)
|
|
13
|
+
- Bash(cat *)
|
|
14
|
+
- Bash(grep *)
|
|
15
|
+
- Bash(find *)
|
|
16
|
+
- Bash(git rev-parse *)
|
|
17
|
+
- Bash("$CLAUDE_PROJECT_DIR"/.claude/hooks/checkpoint-helper.sh *)
|
|
10
18
|
---
|
|
11
19
|
|
|
12
|
-
##
|
|
20
|
+
## Available sessions with checkpoints
|
|
13
21
|
|
|
14
|
-
!`
|
|
22
|
+
!`"$CLAUDE_PROJECT_DIR"/.claude/hooks/checkpoint-helper.sh list`
|
|
15
23
|
|
|
16
|
-
|
|
24
|
+
(`*` marks the current session.)
|
|
17
25
|
|
|
18
|
-
|
|
26
|
+
## Latest checkpoint for the current session
|
|
27
|
+
|
|
28
|
+
!`"$CLAUDE_PROJECT_DIR"/.claude/hooks/checkpoint-helper.sh latest`
|
|
29
|
+
|
|
30
|
+
## Argument
|
|
31
|
+
|
|
32
|
+
$ARGUMENTS
|
|
19
33
|
|
|
20
34
|
## Instructions
|
|
21
35
|
|
|
22
|
-
|
|
36
|
+
1. **If `$ARGUMENTS` is empty or "latest"**: read the checkpoint shown above (the
|
|
37
|
+
current session's `latest.md`). Summarize it in 3-4 sentences: what was
|
|
38
|
+
done, current state, what's next. Then ask: "Ready to pick up from here, or
|
|
39
|
+
do you want to take a different direction?"
|
|
40
|
+
|
|
41
|
+
2. **If `$ARGUMENTS` is "list"**: just show the user the available-sessions
|
|
42
|
+
list above and ask which one they want to restore.
|
|
43
|
+
|
|
44
|
+
3. **If `$ARGUMENTS` looks like a session id prefix** (8-char hex / UUID-ish)
|
|
45
|
+
**or a session name**: match it against the list above. Read the
|
|
46
|
+
matching session's `latest.md` from
|
|
47
|
+
`<project>/uv-out/checkpoints/<full-session-id>/latest.md` using the Read
|
|
48
|
+
tool, then summarize as in (1).
|
|
49
|
+
|
|
50
|
+
4. If no match is found, list the available sessions and ask the user to
|
|
51
|
+
pick one.
|
|
52
|
+
|
|
53
|
+
When summarizing, include the session's name and purpose from the
|
|
54
|
+
frontmatter at the top of the checkpoint — that's the context the next
|
|
55
|
+
session needs to know what it's picking up.
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: session-init
|
|
3
|
+
description: >
|
|
4
|
+
Label or relabel the current UV Suite session — sets name, kind (long-running
|
|
5
|
+
vs outcome), purpose, and priority (low/med/high). Metadata appears in the
|
|
6
|
+
watchtower dashboard. Use when you skipped the launcher prompt or want to
|
|
7
|
+
rename a session mid-flight.
|
|
8
|
+
argument-hint: "<name> | --kind long|outcome | --purpose <text> | --priority low|med|high | show | clear"
|
|
9
|
+
user-invocable: true
|
|
10
|
+
allowed-tools:
|
|
11
|
+
- Bash("$CLAUDE_PROJECT_DIR"/.claude/hooks/session-meta.sh *)
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Apply /session-init $ARGUMENTS
|
|
15
|
+
|
|
16
|
+
!`HELPER="$CLAUDE_PROJECT_DIR/.claude/hooks/session-meta.sh"; ARGS="$ARGUMENTS"; if [ -z "$ARGS" ] || [ "$ARGS" = "show" ]; then "$HELPER" show; elif [ "$ARGS" = "clear" ]; then "$HELPER" clear; else FIRST=$(printf '%s' "$ARGS" | awk '{print $1}'); REST=$(printf '%s' "$ARGS" | sed 's/^[^ ]* *//'); case "$FIRST" in --kind) "$HELPER" set-kind "$REST" ;; --priority) "$HELPER" set-priority "$REST" ;; --purpose) "$HELPER" set-purpose "$REST" ;; --name) "$HELPER" set-name "$REST" ;; *) "$HELPER" set-name "$ARGS" ;; esac; fi`
|
|
17
|
+
|
|
18
|
+
## Instructions
|
|
19
|
+
|
|
20
|
+
Show the user the output of the command above. Do not add commentary — the
|
|
21
|
+
command itself confirms what changed. The dashboard reflects changes within
|
|
22
|
+
a few seconds (next event refreshes session metadata).
|
|
23
|
+
|
|
24
|
+
## Usage
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
/session-init show current values
|
|
28
|
+
/session-init show show current values
|
|
29
|
+
/session-init <free-text name> set the name (default action)
|
|
30
|
+
/session-init --kind long set kind: long-running
|
|
31
|
+
/session-init --kind outcome set kind: outcome
|
|
32
|
+
/session-init --priority high set priority: low | med | high
|
|
33
|
+
/session-init --purpose <free text> set the purpose
|
|
34
|
+
/session-init clear clear name, kind, purpose, priority
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Notes
|
|
38
|
+
|
|
39
|
+
- The session id is generated by `uv.sh` at launch and exported as
|
|
40
|
+
`UVS_SESSION_ID`. If you launched `claude` directly, this falls back
|
|
41
|
+
to an `ad-hoc-<timestamp>` id.
|
|
42
|
+
- Metadata lives at `.uv-suite-state/sessions/<id>.json`.
|
|
43
|
+
- Persona is captured at launch time and is not editable from here —
|
|
44
|
+
re-launch with a different `uv` persona to change it.
|
|
45
|
+
- Each invocation sets one field at a time. Run multiple times to set more.
|
package/uv.sh
CHANGED
|
@@ -41,6 +41,12 @@ case "$1" in
|
|
|
41
41
|
echo " pro Production code (all hooks, all guardrails)"
|
|
42
42
|
echo " auto Fully autonomous (max effort, everything approved)"
|
|
43
43
|
echo ""
|
|
44
|
+
echo " Session metadata:"
|
|
45
|
+
echo " On launch you'll be prompted for name, kind, purpose, and"
|
|
46
|
+
echo " priority. Press Enter to skip any field; you'll be reminded"
|
|
47
|
+
echo " until the session is named. Use /session-init to relabel."
|
|
48
|
+
echo " Set UVS_NO_PROMPT=1 to suppress prompts entirely."
|
|
49
|
+
echo ""
|
|
44
50
|
exit 0
|
|
45
51
|
;;
|
|
46
52
|
"")
|
|
@@ -73,6 +79,76 @@ case "$PERSONA" in
|
|
|
73
79
|
auto) LABEL="Auto — autonomous (max, everything approved)" ;;
|
|
74
80
|
esac
|
|
75
81
|
|
|
82
|
+
# --- Session metadata: generate id and prompt for label ---
|
|
83
|
+
PROJECT_DIR="$(pwd)"
|
|
84
|
+
STATE_DIR="$PROJECT_DIR/.uv-suite-state"
|
|
85
|
+
SESSIONS_DIR="$STATE_DIR/sessions"
|
|
86
|
+
mkdir -p "$SESSIONS_DIR"
|
|
87
|
+
|
|
88
|
+
if command -v uuidgen >/dev/null 2>&1; then
|
|
89
|
+
UVS_SESSION_ID=$(uuidgen | tr '[:upper:]' '[:lower:]')
|
|
90
|
+
else
|
|
91
|
+
UVS_SESSION_ID="uvs-$(date +%s)-$$"
|
|
92
|
+
fi
|
|
93
|
+
export UVS_SESSION_ID
|
|
94
|
+
|
|
95
|
+
UVS_NAME=""
|
|
96
|
+
UVS_KIND=""
|
|
97
|
+
UVS_PURPOSE=""
|
|
98
|
+
UVS_PRIORITY=""
|
|
99
|
+
|
|
100
|
+
# Prompt only if attached to a TTY and not explicitly suppressed
|
|
101
|
+
if [ -t 0 ] && [ -z "$UVS_NO_PROMPT" ]; then
|
|
102
|
+
echo ""
|
|
103
|
+
echo "Label this session (Enter to skip — you'll be reminded):"
|
|
104
|
+
read -r -p " name: " UVS_NAME
|
|
105
|
+
read -r -p " kind [long/outcome]: " UVS_KIND_RAW
|
|
106
|
+
read -r -p " purpose: " UVS_PURPOSE
|
|
107
|
+
read -r -p " priority [low/med/high]: " UVS_PRIORITY_RAW
|
|
108
|
+
|
|
109
|
+
case "$UVS_KIND_RAW" in
|
|
110
|
+
l|long|long-running) UVS_KIND="long-running" ;;
|
|
111
|
+
o|outcome) UVS_KIND="outcome" ;;
|
|
112
|
+
"") UVS_KIND="" ;;
|
|
113
|
+
*) echo " (kind '$UVS_KIND_RAW' not recognized — leaving blank)"; UVS_KIND="" ;;
|
|
114
|
+
esac
|
|
115
|
+
|
|
116
|
+
case "$UVS_PRIORITY_RAW" in
|
|
117
|
+
l|low) UVS_PRIORITY="low" ;;
|
|
118
|
+
m|med|medium) UVS_PRIORITY="med" ;;
|
|
119
|
+
h|high) UVS_PRIORITY="high" ;;
|
|
120
|
+
"") UVS_PRIORITY="" ;;
|
|
121
|
+
*) echo " (priority '$UVS_PRIORITY_RAW' not recognized — leaving blank)"; UVS_PRIORITY="" ;;
|
|
122
|
+
esac
|
|
123
|
+
fi
|
|
124
|
+
|
|
125
|
+
# Write metadata as JSON. Use python3 for proper escaping of free-text fields.
|
|
126
|
+
META_FILE="$SESSIONS_DIR/$UVS_SESSION_ID.json"
|
|
127
|
+
SID="$UVS_SESSION_ID" \
|
|
128
|
+
NAME="$UVS_NAME" \
|
|
129
|
+
KIND="$UVS_KIND" \
|
|
130
|
+
PURPOSE="$UVS_PURPOSE" \
|
|
131
|
+
PRIORITY="$UVS_PRIORITY" \
|
|
132
|
+
PERSONA_VAL="$PERSONA" \
|
|
133
|
+
CWD_VAL="$PROJECT_DIR" \
|
|
134
|
+
STARTED="$(date +%s)" \
|
|
135
|
+
python3 -c '
|
|
136
|
+
import json, os
|
|
137
|
+
print(json.dumps({
|
|
138
|
+
"uvs_session_id": os.environ["SID"],
|
|
139
|
+
"name": os.environ["NAME"],
|
|
140
|
+
"kind": os.environ["KIND"],
|
|
141
|
+
"purpose": os.environ["PURPOSE"],
|
|
142
|
+
"priority": os.environ["PRIORITY"],
|
|
143
|
+
"persona": os.environ["PERSONA_VAL"],
|
|
144
|
+
"cwd": os.environ["CWD_VAL"],
|
|
145
|
+
"started_at": int(os.environ["STARTED"]),
|
|
146
|
+
}, indent=2))
|
|
147
|
+
' > "$META_FILE" 2>/dev/null
|
|
148
|
+
|
|
149
|
+
# Latest-session pointer (used by hooks that lack UVS_SESSION_ID in their env)
|
|
150
|
+
echo "$UVS_SESSION_ID" > "$STATE_DIR/current-session.txt"
|
|
151
|
+
|
|
76
152
|
SETTINGS=".claude/personas/$PERSONA.json"
|
|
77
153
|
|
|
78
154
|
if [ "$TOOL" = "claude" ]; then
|
|
@@ -87,7 +163,9 @@ if [ "$TOOL" = "claude" ]; then
|
|
|
87
163
|
exit 1
|
|
88
164
|
fi
|
|
89
165
|
|
|
166
|
+
echo ""
|
|
90
167
|
echo "UV Suite | Claude Code | $LABEL"
|
|
168
|
+
echo "Session: ${UVS_SESSION_ID:0:8}${UVS_NAME:+ — $UVS_NAME}"
|
|
91
169
|
echo ""
|
|
92
170
|
exec claude --settings "$SETTINGS" "$@"
|
|
93
171
|
|
|
@@ -99,24 +177,16 @@ elif [ "$TOOL" = "codex" ]; then
|
|
|
99
177
|
exit 1
|
|
100
178
|
fi
|
|
101
179
|
|
|
102
|
-
# Codex doesn't have --settings, but reads AGENTS.md and .codex/agents/
|
|
103
|
-
# We can pass model and approval mode based on persona
|
|
104
180
|
case "$PERSONA" in
|
|
105
|
-
spike)
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
CODEX_ARGS="--approval-mode auto-edit"
|
|
110
|
-
;;
|
|
111
|
-
professional)
|
|
112
|
-
CODEX_ARGS="--approval-mode suggest"
|
|
113
|
-
;;
|
|
114
|
-
auto)
|
|
115
|
-
CODEX_ARGS="--approval-mode full-auto"
|
|
116
|
-
;;
|
|
181
|
+
spike) CODEX_ARGS="--model o3 --approval-mode suggest" ;;
|
|
182
|
+
sport) CODEX_ARGS="--approval-mode auto-edit" ;;
|
|
183
|
+
professional) CODEX_ARGS="--approval-mode suggest" ;;
|
|
184
|
+
auto) CODEX_ARGS="--approval-mode full-auto" ;;
|
|
117
185
|
esac
|
|
118
186
|
|
|
187
|
+
echo ""
|
|
119
188
|
echo "UV Suite | Codex | $LABEL"
|
|
189
|
+
echo "Session: ${UVS_SESSION_ID:0:8}${UVS_NAME:+ — $UVS_NAME}"
|
|
120
190
|
echo ""
|
|
121
191
|
exec codex $CODEX_ARGS "$@"
|
|
122
192
|
fi
|
|
@@ -279,6 +279,42 @@
|
|
|
279
279
|
.event.user-prompt { background: var(--user-prompt-bg); }
|
|
280
280
|
.event.user-prompt .detail { color: var(--user-prompt-text); font-style: italic; }
|
|
281
281
|
|
|
282
|
+
/* Session priority: low rows are dimmed, high rows get an accent strip */
|
|
283
|
+
.event.priority-low { opacity: 0.45; }
|
|
284
|
+
.event.priority-low:hover { opacity: 0.85; }
|
|
285
|
+
.event.priority-high { border-left: 3px solid var(--accent); padding-left: 25px; }
|
|
286
|
+
|
|
287
|
+
/* Pills shown next to a session label */
|
|
288
|
+
.pill {
|
|
289
|
+
display: inline-block;
|
|
290
|
+
padding: 1px 7px;
|
|
291
|
+
margin-left: 6px;
|
|
292
|
+
font-size: 10.5px;
|
|
293
|
+
font-weight: 600;
|
|
294
|
+
letter-spacing: 0.04em;
|
|
295
|
+
text-transform: uppercase;
|
|
296
|
+
border-radius: 4px;
|
|
297
|
+
vertical-align: 1px;
|
|
298
|
+
}
|
|
299
|
+
.pill.persona-spike { background: rgba(191, 90, 242, 0.18); color: var(--purple); }
|
|
300
|
+
.pill.persona-sport { background: rgba(48, 209, 88, 0.18); color: var(--success); }
|
|
301
|
+
.pill.persona-professional { background: rgba(10, 132, 255, 0.18); color: var(--accent); }
|
|
302
|
+
.pill.persona-auto { background: rgba(255, 159, 10, 0.18); color: var(--warning); }
|
|
303
|
+
.pill.priority-low { background: rgba(154, 154, 163, 0.18); color: var(--text-muted); }
|
|
304
|
+
.pill.priority-med { background: rgba(255, 214, 10, 0.18); color: var(--yellow); }
|
|
305
|
+
.pill.priority-high { background: rgba(255, 69, 58, 0.18); color: var(--danger); }
|
|
306
|
+
.pill.kind-long-running { background: rgba(100, 210, 255, 0.18); color: var(--info); }
|
|
307
|
+
.pill.kind-outcome { background: rgba(255, 105, 97, 0.18); color: var(--peach); }
|
|
308
|
+
|
|
309
|
+
.session-tag.priority-low { opacity: 0.6; }
|
|
310
|
+
.session-tag .meta-line {
|
|
311
|
+
display: block;
|
|
312
|
+
font-size: 11px;
|
|
313
|
+
font-weight: 400;
|
|
314
|
+
color: var(--text-muted);
|
|
315
|
+
margin-top: 2px;
|
|
316
|
+
}
|
|
317
|
+
|
|
282
318
|
.timeline-end { padding: 20px 24px; text-align: center; border-bottom: 1px solid var(--border-subtle); }
|
|
283
319
|
.loader { display: inline-block; width: 48px; height: 4px; position: relative; }
|
|
284
320
|
.loader::before {
|
|
@@ -393,9 +429,21 @@ let lastEventDiv = null;
|
|
|
393
429
|
// Session colors and naming
|
|
394
430
|
const palette = ['#0a84ff','#30d158','#ff9f0a','#bf5af2','#ff375f','#64d2ff','#ffd60a','#ac8ee0','#ff6961','#5e5ce6'];
|
|
395
431
|
let colorIdx = 0;
|
|
432
|
+
|
|
433
|
+
// Resolve the canonical session key for an event: prefer the UV Suite id (one
|
|
434
|
+
// per `uv` launch), fall back to Claude Code's session id, then source_app.
|
|
435
|
+
function eventSid(ev) {
|
|
436
|
+
return ev.uvs_session_id || ev.session_id || ev.source_app || 'unknown';
|
|
437
|
+
}
|
|
438
|
+
|
|
396
439
|
function sessionColor(id) {
|
|
397
440
|
if (!sessions[id]) {
|
|
398
|
-
sessions[id] = {
|
|
441
|
+
sessions[id] = {
|
|
442
|
+
color: palette[colorIdx++ % palette.length],
|
|
443
|
+
count: 0, lastEvent: null,
|
|
444
|
+
name: '', kind: '', purpose: '', priority: '', persona: '',
|
|
445
|
+
app: null, label: null,
|
|
446
|
+
};
|
|
399
447
|
updateSessionBar();
|
|
400
448
|
updateFilterSession();
|
|
401
449
|
}
|
|
@@ -406,33 +454,54 @@ function sessionColor(id) {
|
|
|
406
454
|
|
|
407
455
|
function updateSessionLabel(sid, ev) {
|
|
408
456
|
if (!sessions[sid]) return;
|
|
409
|
-
|
|
410
|
-
|
|
457
|
+
const s = sessions[sid];
|
|
458
|
+
let changed = false;
|
|
459
|
+
|
|
460
|
+
if (!s.app && ev.source_app) { s.app = ev.source_app; changed = true; }
|
|
461
|
+
|
|
462
|
+
// Configured metadata wins over heuristics. Update on every event so a
|
|
463
|
+
// mid-session /session-init relabel is reflected without a refresh.
|
|
464
|
+
for (const [evKey, sKey] of [
|
|
465
|
+
['session_name','name'], ['session_kind','kind'],
|
|
466
|
+
['session_purpose','purpose'], ['session_priority','priority'],
|
|
467
|
+
['persona','persona'],
|
|
468
|
+
]) {
|
|
469
|
+
if (ev[evKey] !== undefined && ev[evKey] !== null && ev[evKey] !== s[sKey]) {
|
|
470
|
+
s[sKey] = ev[evKey];
|
|
471
|
+
changed = true;
|
|
472
|
+
}
|
|
411
473
|
}
|
|
412
|
-
|
|
474
|
+
|
|
475
|
+
// Fall back to first UserPromptSubmit if no configured name yet
|
|
413
476
|
const type = ev.event_type || ev.hook_event_name || '';
|
|
414
|
-
if (!
|
|
477
|
+
if (!s.name && !s.label && type === 'UserPromptSubmit') {
|
|
415
478
|
const prompt = ev.tool_input?.prompt || ev.tool_input?.content || ev.message || '';
|
|
416
479
|
if (prompt.length > 0) {
|
|
417
480
|
let label = prompt.slice(0, 45).replace(/\s+\S*$/, '');
|
|
418
481
|
if (prompt.length > label.length) label += '...';
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
updateFilterSession();
|
|
482
|
+
s.label = label;
|
|
483
|
+
changed = true;
|
|
422
484
|
}
|
|
423
485
|
}
|
|
486
|
+
|
|
487
|
+
if (changed) {
|
|
488
|
+
updateSessionBar();
|
|
489
|
+
updateFilterSession();
|
|
490
|
+
}
|
|
424
491
|
}
|
|
425
492
|
|
|
426
493
|
function sessionDisplayName(id) {
|
|
427
494
|
const s = sessions[id];
|
|
428
495
|
if (!s) return shortId(id);
|
|
429
|
-
if (s.
|
|
430
|
-
|
|
431
|
-
}
|
|
496
|
+
if (s.name) return s.name;
|
|
497
|
+
if (s.label) return s.app ? s.app + ': ' + s.label : s.label;
|
|
432
498
|
if (s.app) return s.app;
|
|
433
499
|
return shortId(id);
|
|
434
500
|
}
|
|
435
501
|
|
|
502
|
+
// Sort order: high → med → unset → low (low last so it groups at the bottom)
|
|
503
|
+
const PRIORITY_ORDER = { high: 0, med: 1, '': 2, low: 3 };
|
|
504
|
+
|
|
436
505
|
function shortId(id) {
|
|
437
506
|
if (!id) return '—';
|
|
438
507
|
return id.length > 10 ? id.slice(0, 8) + '..' : id;
|
|
@@ -526,7 +595,7 @@ function eventDetail(ev) {
|
|
|
526
595
|
}
|
|
527
596
|
|
|
528
597
|
function renderEvent(ev) {
|
|
529
|
-
const sid = ev
|
|
598
|
+
const sid = eventSid(ev);
|
|
530
599
|
const color = sessionColor(sid);
|
|
531
600
|
const type = ev.event_type || ev.hook_event_name || '?';
|
|
532
601
|
const tool = ev.tool_name || '';
|
|
@@ -534,6 +603,7 @@ function renderEvent(ev) {
|
|
|
534
603
|
const fail = isFailure(ev);
|
|
535
604
|
const boundary = isSessionBoundary(ev);
|
|
536
605
|
const prompt = isUserPrompt(ev);
|
|
606
|
+
const priority = sessions[sid]?.priority || '';
|
|
537
607
|
|
|
538
608
|
const div = document.createElement('div');
|
|
539
609
|
let cls = 'event';
|
|
@@ -541,6 +611,8 @@ function renderEvent(ev) {
|
|
|
541
611
|
else if (fail) cls += ' failure';
|
|
542
612
|
else if (prompt) cls += ' user-prompt';
|
|
543
613
|
else if (boundary) cls += ' session-boundary';
|
|
614
|
+
if (priority === 'high') cls += ' priority-high';
|
|
615
|
+
if (priority === 'low') cls += ' priority-low';
|
|
544
616
|
div.className = cls;
|
|
545
617
|
|
|
546
618
|
const humanBadge = human ? '<span class="human-badge">NEEDS HUMAN</span>' : '';
|
|
@@ -567,7 +639,9 @@ function renderEvent(ev) {
|
|
|
567
639
|
function addEvent(ev) {
|
|
568
640
|
events.push(ev);
|
|
569
641
|
if (emptyState.parentNode) emptyState.remove();
|
|
570
|
-
const sid = ev
|
|
642
|
+
const sid = eventSid(ev);
|
|
643
|
+
// Make sure the session is registered before we try to update its metadata
|
|
644
|
+
sessionColor(sid);
|
|
571
645
|
updateSessionLabel(sid, ev);
|
|
572
646
|
|
|
573
647
|
// Remove "latest" class from previous latest
|
|
@@ -592,7 +666,7 @@ function addEvent(ev) {
|
|
|
592
666
|
|
|
593
667
|
function updateWaitingText(ev) {
|
|
594
668
|
if (ev) {
|
|
595
|
-
const sid = ev
|
|
669
|
+
const sid = eventSid(ev);
|
|
596
670
|
waitingText.textContent = `Last: ${sessionDisplayName(sid)} — ${timeSince(ev._ts)}`;
|
|
597
671
|
}
|
|
598
672
|
}
|
|
@@ -613,7 +687,7 @@ function updateStats() {
|
|
|
613
687
|
// number drops back down once the session continues past the prompt.
|
|
614
688
|
const latestBySession = {};
|
|
615
689
|
for (const ev of events) {
|
|
616
|
-
const sid = ev
|
|
690
|
+
const sid = eventSid(ev);
|
|
617
691
|
latestBySession[sid] = ev;
|
|
618
692
|
}
|
|
619
693
|
const humans = Object.values(latestBySession).filter(needsHuman).length;
|
|
@@ -621,15 +695,46 @@ function updateStats() {
|
|
|
621
695
|
document.getElementById('humanCount').className = 'n' + (humans > 0 ? ' alert' : '');
|
|
622
696
|
}
|
|
623
697
|
|
|
698
|
+
function escapeHtml(s) {
|
|
699
|
+
return String(s).replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
function pill(cls, label) {
|
|
703
|
+
return `<span class="pill ${cls}">${escapeHtml(label)}</span>`;
|
|
704
|
+
}
|
|
705
|
+
|
|
624
706
|
function updateSessionBar() {
|
|
625
707
|
sessionBar.innerHTML = '';
|
|
626
|
-
|
|
708
|
+
// Sort: high priority first, then med/unset, then low; within a tier, most
|
|
709
|
+
// recent activity wins so the dashboard surfaces what's happening now.
|
|
710
|
+
const ids = Object.keys(sessions).sort((a, b) => {
|
|
711
|
+
const pa = PRIORITY_ORDER[sessions[a].priority] ?? PRIORITY_ORDER[''];
|
|
712
|
+
const pb = PRIORITY_ORDER[sessions[b].priority] ?? PRIORITY_ORDER[''];
|
|
713
|
+
if (pa !== pb) return pa - pb;
|
|
714
|
+
return (sessions[b].lastEvent || 0) - (sessions[a].lastEvent || 0);
|
|
715
|
+
});
|
|
716
|
+
|
|
717
|
+
for (const id of ids) {
|
|
718
|
+
const s = sessions[id];
|
|
627
719
|
const tag = document.createElement('span');
|
|
628
|
-
|
|
720
|
+
let cls = 'session-tag';
|
|
721
|
+
if (selectedSession === id) cls += ' active';
|
|
722
|
+
if (s.priority === 'low') cls += ' priority-low';
|
|
723
|
+
tag.className = cls;
|
|
629
724
|
tag.style.background = s.color + '22';
|
|
630
725
|
tag.style.color = s.color;
|
|
631
|
-
tag.
|
|
632
|
-
|
|
726
|
+
tag.title = `${id}${s.purpose ? '\n' + s.purpose : ''}`;
|
|
727
|
+
|
|
728
|
+
const pills = [];
|
|
729
|
+
if (s.persona) pills.push(pill('persona-' + s.persona, s.persona));
|
|
730
|
+
if (s.priority) pills.push(pill('priority-' + s.priority, 'P:' + s.priority));
|
|
731
|
+
if (s.kind) pills.push(pill('kind-' + s.kind, s.kind));
|
|
732
|
+
|
|
733
|
+
const namePart = `<strong>${escapeHtml(sessionDisplayName(id))}</strong>` +
|
|
734
|
+
` <span style="opacity:0.7">(${s.count})</span>`;
|
|
735
|
+
const meta = pills.length ? `<span class="meta-line">${pills.join('')}</span>` : '';
|
|
736
|
+
tag.innerHTML = namePart + meta;
|
|
737
|
+
|
|
633
738
|
tag.onclick = () => { selectedSession = selectedSession === id ? '' : id; refilter(); updateSessionBar(); };
|
|
634
739
|
sessionBar.appendChild(tag);
|
|
635
740
|
}
|
|
@@ -662,7 +767,7 @@ function refilter() {
|
|
|
662
767
|
rows.forEach((row) => {
|
|
663
768
|
const ev = row._ev;
|
|
664
769
|
if (!ev) return;
|
|
665
|
-
const sid = ev
|
|
770
|
+
const sid = eventSid(ev);
|
|
666
771
|
const type = ev.event_type || ev.hook_event_name || '';
|
|
667
772
|
const show = (!selectedType || type === selectedType)
|
|
668
773
|
&& (!selectedSession || sid === selectedSession)
|
package/watchtower/events.json
CHANGED
|
@@ -1,22 +1 @@
|
|
|
1
|
-
[
|
|
2
|
-
{
|
|
3
|
-
"event_type": "PostToolUse",
|
|
4
|
-
"session_id": "test-123",
|
|
5
|
-
"source_app": "uv-suite",
|
|
6
|
-
"tool_name": "Edit",
|
|
7
|
-
"cwd": "/tmp",
|
|
8
|
-
"_ts": 1776756371726,
|
|
9
|
-
"_id": "3f130976-6226-47ea-a477-18a16e194415"
|
|
10
|
-
},
|
|
11
|
-
{
|
|
12
|
-
"event_type": "PostToolUse",
|
|
13
|
-
"session_id": "test-session",
|
|
14
|
-
"source_app": "my-project",
|
|
15
|
-
"tool_name": "Edit",
|
|
16
|
-
"tool_input": {
|
|
17
|
-
"file_path": "src/app.ts"
|
|
18
|
-
},
|
|
19
|
-
"_ts": 1776757586012,
|
|
20
|
-
"_id": "efa9ae75-c0e0-48c2-a078-e7075ccef5a7"
|
|
21
|
-
}
|
|
22
|
-
]
|
|
1
|
+
[]
|