uv-suite 0.26.1 → 0.26.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/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 +17 -5
- package/skills/confirm/SKILL.md +35 -0
- package/skills/restore/SKILL.md +5 -2
- 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
|
@@ -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
|
+
[]
|