uv-suite 0.24.0 → 0.26.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "uv-suite",
3
- "version": "0.24.0",
3
+ "version": "0.26.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",
@@ -5,95 +5,340 @@
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1">
6
6
  <title>UV Suite Watchtower</title>
7
7
  <style>
8
+ :root {
9
+ color-scheme: dark;
10
+ --bg: #0b0b0d;
11
+ --surface: #16161a;
12
+ --surface-hover: #1d1d22;
13
+ --border: #26262c;
14
+ --border-subtle: #1a1a1e;
15
+ --text: #e9e9ec;
16
+ --text-muted: #9a9aa3;
17
+ --text-dim: #6a6a73;
18
+ --accent: #0a84ff;
19
+ --accent-contrast: #ffffff;
20
+ --success: #30d158;
21
+ --success-soft: rgba(48, 209, 88, 0.18);
22
+ --danger: #ff453a;
23
+ --danger-soft: #ff375f;
24
+ --warning: #ff9f0a;
25
+ --yellow: #ffd60a;
26
+ --info: #64d2ff;
27
+ --purple: #bf5af2;
28
+ --purple-soft: #ac8ee0;
29
+ --peach: #ff6961;
30
+
31
+ --event-latest-bg: #17171c;
32
+ --needs-human-bg: rgba(255, 55, 95, 0.14);
33
+ --failure-bg: rgba(255, 105, 97, 0.12);
34
+ --session-boundary-bg: rgba(48, 209, 88, 0.06);
35
+ --user-prompt-bg: rgba(255, 214, 10, 0.06);
36
+ --user-prompt-text: #ffd60a;
37
+
38
+ --font-sans: -apple-system, BlinkMacSystemFont, 'Inter', 'SF Pro Text', system-ui, sans-serif;
39
+ --font-mono: 'SF Mono', ui-monospace, Menlo, Consolas, monospace;
40
+ }
41
+
42
+ [data-theme="light"] {
43
+ color-scheme: light;
44
+ --bg: #fafafa;
45
+ --surface: #ffffff;
46
+ --surface-hover: #f4f4f5;
47
+ --border: #e4e4e7;
48
+ --border-subtle: #ededef;
49
+ --text: #18181b;
50
+ --text-muted: #52525b;
51
+ --text-dim: #8a8a93;
52
+ --accent: #0066cc;
53
+ --accent-contrast: #ffffff;
54
+ --success: #1a9e3e;
55
+ --success-soft: rgba(26, 158, 62, 0.15);
56
+ --danger: #d8302a;
57
+ --danger-soft: #dc184a;
58
+ --warning: #c07300;
59
+ --yellow: #a5720d;
60
+ --info: #0b8aa4;
61
+ --purple: #7c2fbc;
62
+ --purple-soft: #8b3fd4;
63
+ --peach: #c13a35;
64
+
65
+ --event-latest-bg: #f4f4f5;
66
+ --needs-human-bg: rgba(220, 24, 74, 0.08);
67
+ --failure-bg: rgba(216, 48, 42, 0.07);
68
+ --session-boundary-bg: rgba(26, 158, 62, 0.06);
69
+ --user-prompt-bg: rgba(165, 114, 13, 0.06);
70
+ --user-prompt-text: #8a5f0b;
71
+ }
72
+
8
73
  * { box-sizing: border-box; margin: 0; padding: 0; }
9
- body { font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Text', system-ui, sans-serif; background: #000; color: #f5f5f7; }
10
- .header { padding: 16px 24px; border-bottom: 1px solid #2d2d2f; display: flex; align-items: center; justify-content: space-between; }
11
- .header h1 { font-size: 16px; font-weight: 600; letter-spacing: -0.01em; }
12
- .header .status { font-size: 11px; color: #86868b; }
13
- .header .status .dot { display: inline-block; width: 6px; height: 6px; border-radius: 50%; margin-right: 4px; }
14
- .header .status .dot.on { background: #30d158; }
15
- .header .status .dot.off { background: #ff453a; }
16
-
17
- .filters { padding: 8px 24px; border-bottom: 1px solid #1d1d1f; display: flex; gap: 8px; flex-wrap: wrap; }
18
- .filters select, .filters button { background: #1d1d1f; color: #a1a1a6; border: 1px solid #2d2d2f; border-radius: 6px; padding: 4px 10px; font-size: 11px; cursor: pointer; }
19
- .filters select:focus, .filters button:hover { border-color: #424245; color: #f5f5f7; }
20
- .filters button.active { background: #0071e3; color: #fff; border-color: #0071e3; }
21
-
22
- .stats { padding: 12px 24px; display: flex; gap: 24px; border-bottom: 1px solid #1d1d1f; }
23
- .stat { text-align: center; }
24
- .stat .n { font-size: 20px; font-weight: 600; }
25
- .stat .l { font-size: 10px; color: #86868b; text-transform: uppercase; letter-spacing: 0.5px; margin-top: 2px; }
26
- .stat .n.alert { color: #ff375f; }
27
-
28
- .sessions { padding: 12px 24px; border-bottom: 1px solid #1d1d1f; display: flex; gap: 8px; flex-wrap: wrap; }
29
- .session-tag { padding: 3px 10px; border-radius: 12px; font-size: 11px; font-weight: 500; cursor: pointer; border: 1px solid transparent; }
30
- .session-tag:hover { border-color: #424245; }
31
- .session-tag.active { border-color: #fff; }
32
-
33
- .timeline { padding: 8px 0; overflow-y: auto; max-height: calc(100vh - 280px); }
34
-
35
- .event { padding: 10px 24px; display: grid; grid-template-columns: 70px 110px 140px 65px 1fr; gap: 10px; align-items: start; font-size: 13px; border-bottom: 1px solid #0d0d0d; transition: all 0.2s; opacity: 0.75; }
36
- .event:hover { background: #0d0d0d; opacity: 1; }
37
- .event .time { color: #6e6e73; font-variant-numeric: tabular-nums; font-family: 'SF Mono', monospace; font-size: 11px; padding-top: 2px; }
38
- .event .type { font-weight: 500; font-size: 12px; }
39
- .event .session { font-size: 11px; border-radius: 8px; padding: 2px 8px; display: inline-block; max-width: 140px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
40
- .event .tool { color: #a1a1a6; font-family: 'SF Mono', monospace; font-size: 11px; padding-top: 2px; }
41
- .event .detail { color: #a1a1a6; font-size: 12px; line-height: 1.5; word-break: break-all; }
42
- .event .detail .filepath { color: #64d2ff; }
43
- .event .detail .cmd { color: #ffd60a; }
44
-
45
- /* Latest event — full opacity, larger, highlighted */
46
- .event.latest { opacity: 1; font-size: 14px; background: #0a0a0a; border-bottom: 2px solid #2d2d2f; }
47
- .event.latest .type { font-size: 13px; }
48
- .event.latest .detail { font-size: 13px; color: #d1d1d6; }
49
-
50
- /* Human intervention — strong highlight */
51
- .event.needs-human { background: #ff375f18; border-left: 4px solid #ff375f; opacity: 1; }
52
- .event.needs-human .type { color: #ff375f; }
53
- .event.needs-human .human-badge { display: inline-block; background: #ff375f; color: #fff; font-size: 9px; font-weight: 700; padding: 1px 6px; border-radius: 3px; letter-spacing: 0.5px; margin-left: 8px; vertical-align: middle; }
54
-
55
- /* Failure highlight */
56
- .event.failure { background: #ff696115; border-left: 4px solid #ff6961; opacity: 1; }
57
-
58
- /* Session start/end */
59
- .event.session-boundary { background: #30d15808; }
60
-
61
- /* User prompt — show what was asked */
62
- .event.user-prompt { background: #ffd60a08; }
63
- .event.user-prompt .detail { color: #ffd60a; font-style: italic; }
64
-
65
- /* Bottom loader area */
66
- .timeline-end { padding: 24px; text-align: center; }
67
- .loader { display: inline-block; width: 40px; height: 4px; position: relative; }
68
- .loader::before { content: ''; position: absolute; width: 8px; height: 4px; background: #424245; border-radius: 2px; animation: loader 1.2s ease-in-out infinite; }
74
+
75
+ html, body { height: 100%; }
76
+ body {
77
+ font-family: var(--font-sans);
78
+ background: var(--bg);
79
+ color: var(--text);
80
+ font-size: 14px;
81
+ line-height: 1.5;
82
+ -webkit-font-smoothing: antialiased;
83
+ -moz-osx-font-smoothing: grayscale;
84
+ display: flex;
85
+ flex-direction: column;
86
+ transition: background-color 0.2s ease, color 0.2s ease;
87
+ }
88
+
89
+ .header {
90
+ padding: 18px 28px;
91
+ border-bottom: 1px solid var(--border);
92
+ display: flex;
93
+ align-items: center;
94
+ justify-content: space-between;
95
+ gap: 16px;
96
+ }
97
+ .header h1 { font-size: 18px; font-weight: 600; letter-spacing: -0.02em; }
98
+ .header .meta { display: flex; align-items: center; gap: 14px; }
99
+ .header .status { font-size: 13px; color: var(--text-muted); font-variant-numeric: tabular-nums; }
100
+ .header .status .dot { display: inline-block; width: 8px; height: 8px; border-radius: 50%; margin-right: 7px; vertical-align: 1px; }
101
+ .header .status .dot.on { background: var(--success); box-shadow: 0 0 0 3px var(--success-soft); }
102
+ .header .status .dot.off { background: var(--danger); }
103
+
104
+ .theme-toggle {
105
+ background: transparent;
106
+ color: var(--text-muted);
107
+ border: 1px solid var(--border);
108
+ border-radius: 7px;
109
+ width: 34px;
110
+ height: 30px;
111
+ display: inline-flex;
112
+ align-items: center;
113
+ justify-content: center;
114
+ cursor: pointer;
115
+ padding: 0;
116
+ transition: color 0.15s ease, border-color 0.15s ease, background-color 0.15s ease;
117
+ }
118
+ .theme-toggle:hover { color: var(--text); border-color: var(--text-dim); background: var(--surface); }
119
+ .theme-toggle svg { width: 15px; height: 15px; }
120
+ .theme-toggle .icon-sun { display: block; }
121
+ .theme-toggle .icon-moon { display: none; }
122
+ [data-theme="light"] .theme-toggle .icon-sun { display: none; }
123
+ [data-theme="light"] .theme-toggle .icon-moon { display: block; }
124
+
125
+ .stats {
126
+ padding: 16px 28px;
127
+ display: flex;
128
+ gap: 36px;
129
+ border-bottom: 1px solid var(--border-subtle);
130
+ }
131
+ .stat { display: flex; flex-direction: column; }
132
+ .stat .n {
133
+ font-size: 26px;
134
+ font-weight: 600;
135
+ letter-spacing: -0.02em;
136
+ font-variant-numeric: tabular-nums;
137
+ line-height: 1.1;
138
+ }
139
+ .stat .l {
140
+ font-size: 11px;
141
+ color: var(--text-muted);
142
+ text-transform: uppercase;
143
+ letter-spacing: 0.08em;
144
+ font-weight: 500;
145
+ margin-top: 4px;
146
+ }
147
+ .stat .n.alert { color: var(--danger-soft); }
148
+
149
+ .sessions {
150
+ padding: 12px 28px;
151
+ border-bottom: 1px solid var(--border-subtle);
152
+ display: flex;
153
+ gap: 8px;
154
+ flex-wrap: wrap;
155
+ }
156
+ .session-tag {
157
+ padding: 4px 12px;
158
+ border-radius: 14px;
159
+ font-size: 13px;
160
+ font-weight: 500;
161
+ cursor: pointer;
162
+ border: 1px solid transparent;
163
+ transition: border-color 0.15s ease;
164
+ }
165
+ .session-tag:hover { border-color: var(--text-dim); }
166
+ .session-tag.active { border-color: var(--text); }
167
+
168
+ .filters {
169
+ padding: 10px 28px;
170
+ border-bottom: 1px solid var(--border-subtle);
171
+ display: flex;
172
+ gap: 10px;
173
+ flex-wrap: wrap;
174
+ align-items: center;
175
+ }
176
+ .filters select, .filters button {
177
+ background: var(--surface);
178
+ color: var(--text-muted);
179
+ border: 1px solid var(--border);
180
+ border-radius: 6px;
181
+ padding: 0 12px;
182
+ font-size: 13px;
183
+ font-family: inherit;
184
+ cursor: pointer;
185
+ height: 32px;
186
+ transition: color 0.15s ease, border-color 0.15s ease, background-color 0.15s ease;
187
+ }
188
+ .filters select:hover, .filters button:hover { border-color: var(--text-dim); color: var(--text); }
189
+ .filters select:focus { outline: none; border-color: var(--accent); }
190
+ .filters button.active { background: var(--accent); color: var(--accent-contrast); border-color: var(--accent); }
191
+
192
+ .timeline { flex: 1; min-height: 0; overflow-y: auto; padding: 4px 0; }
193
+
194
+ .event {
195
+ padding: 14px 28px;
196
+ display: grid;
197
+ grid-template-columns: 82px 150px 170px 90px 1fr;
198
+ gap: 14px;
199
+ align-items: start;
200
+ font-size: 14px;
201
+ border-bottom: 1px solid var(--border-subtle);
202
+ transition: background-color 0.15s ease, opacity 0.15s ease;
203
+ opacity: 0.82;
204
+ }
205
+ .event:hover { background: var(--surface-hover); opacity: 1; }
206
+ .event .time {
207
+ color: var(--text-dim);
208
+ font-variant-numeric: tabular-nums;
209
+ font-family: var(--font-mono);
210
+ font-size: 12.5px;
211
+ padding-top: 2px;
212
+ }
213
+ .event .type {
214
+ font-weight: 600;
215
+ font-size: 14px;
216
+ letter-spacing: -0.01em;
217
+ }
218
+ .event .session {
219
+ font-size: 12.5px;
220
+ font-weight: 500;
221
+ border-radius: 10px;
222
+ padding: 3px 10px;
223
+ display: inline-block;
224
+ max-width: 170px;
225
+ overflow: hidden;
226
+ text-overflow: ellipsis;
227
+ white-space: nowrap;
228
+ }
229
+ .event .tool {
230
+ color: var(--text-muted);
231
+ font-family: var(--font-mono);
232
+ font-size: 12.5px;
233
+ padding-top: 2px;
234
+ }
235
+ .event .detail {
236
+ color: var(--text-muted);
237
+ font-size: 14px;
238
+ line-height: 1.55;
239
+ word-break: break-word;
240
+ }
241
+ .event .detail .filepath { color: var(--info); }
242
+ .event .detail .cmd { color: var(--yellow); font-family: var(--font-mono); }
243
+
244
+ .event.latest {
245
+ opacity: 1;
246
+ background: var(--event-latest-bg);
247
+ border-bottom: 2px solid var(--border);
248
+ }
249
+ .event.latest .type { font-size: 15px; }
250
+ .event.latest .detail { font-size: 15px; color: var(--text); }
251
+
252
+ .event.needs-human {
253
+ background: var(--needs-human-bg);
254
+ border-left: 4px solid var(--danger-soft);
255
+ padding-left: 24px;
256
+ opacity: 1;
257
+ }
258
+ .event.needs-human .type { color: var(--danger-soft); }
259
+ .event.needs-human .human-badge {
260
+ display: inline-block;
261
+ background: var(--danger-soft);
262
+ color: #fff;
263
+ font-size: 10px;
264
+ font-weight: 700;
265
+ padding: 2px 7px;
266
+ border-radius: 4px;
267
+ letter-spacing: 0.06em;
268
+ margin-left: 10px;
269
+ vertical-align: middle;
270
+ }
271
+
272
+ .event.failure {
273
+ background: var(--failure-bg);
274
+ border-left: 4px solid var(--peach);
275
+ padding-left: 24px;
276
+ opacity: 1;
277
+ }
278
+ .event.session-boundary { background: var(--session-boundary-bg); }
279
+ .event.user-prompt { background: var(--user-prompt-bg); }
280
+ .event.user-prompt .detail { color: var(--user-prompt-text); font-style: italic; }
281
+
282
+ .timeline-end { padding: 20px 24px; text-align: center; border-bottom: 1px solid var(--border-subtle); }
283
+ .loader { display: inline-block; width: 48px; height: 4px; position: relative; }
284
+ .loader::before {
285
+ content: '';
286
+ position: absolute;
287
+ width: 10px;
288
+ height: 4px;
289
+ background: var(--text-dim);
290
+ border-radius: 2px;
291
+ animation: loader 1.4s ease-in-out infinite;
292
+ }
69
293
  @keyframes loader {
70
294
  0%, 100% { left: 0; }
71
- 50% { left: 32px; }
72
- }
73
- .timeline-end .waiting { font-size: 11px; color: #424245; margin-top: 8px; }
74
-
75
- .empty { padding: 60px 24px; text-align: center; color: #6e6e73; }
76
- .empty p { margin-top: 8px; font-size: 13px; }
77
-
78
- /* Event type colors */
79
- .type-SessionStart { color: #30d158; }
80
- .type-SessionEnd, .type-Stop { color: #ff453a; }
81
- .type-PreToolUse { color: #0a84ff; }
82
- .type-PostToolUse { color: #64d2ff; }
83
- .type-PostToolUseFailure { color: #ff6961; }
84
- .type-UserPromptSubmit { color: #ffd60a; }
85
- .type-SubagentStart { color: #bf5af2; }
86
- .type-SubagentStop { color: #ac8ee0; }
87
- .type-Notification { color: #ff9f0a; }
88
- .type-PermissionRequest { color: #ff375f; }
89
- .type-PreCompact { color: #6e6e73; }
295
+ 50% { left: 38px; }
296
+ }
297
+ .timeline-end .waiting {
298
+ font-size: 13px;
299
+ color: var(--text-dim);
300
+ margin-top: 10px;
301
+ font-variant-numeric: tabular-nums;
302
+ }
303
+
304
+ .empty { padding: 72px 28px; text-align: center; color: var(--text-dim); }
305
+ .empty p { margin-top: 10px; font-size: 14px; }
306
+ .empty p strong { color: var(--text-muted); font-weight: 600; font-size: 15px; }
307
+
308
+ .timeline::-webkit-scrollbar { width: 10px; }
309
+ .timeline::-webkit-scrollbar-track { background: transparent; }
310
+ .timeline::-webkit-scrollbar-thumb { background: var(--border); border-radius: 5px; }
311
+ .timeline::-webkit-scrollbar-thumb:hover { background: var(--text-dim); }
312
+
313
+ .type-SessionStart { color: var(--success); }
314
+ .type-SessionEnd, .type-Stop { color: var(--danger); }
315
+ .type-PreToolUse { color: var(--accent); }
316
+ .type-PostToolUse { color: var(--info); }
317
+ .type-PostToolUseFailure { color: var(--peach); }
318
+ .type-UserPromptSubmit { color: var(--yellow); }
319
+ .type-SubagentStart { color: var(--purple); }
320
+ .type-SubagentStop { color: var(--purple-soft); }
321
+ .type-Notification { color: var(--warning); }
322
+ .type-PermissionRequest { color: var(--danger-soft); }
323
+ .type-PreCompact { color: var(--text-muted); }
90
324
  </style>
91
325
  </head>
92
326
  <body>
93
327
 
94
328
  <div class="header">
95
329
  <h1>UV Suite Watchtower</h1>
96
- <div class="status"><span class="dot" id="statusDot"></span><span id="statusText">Connecting...</span></div>
330
+ <div class="meta">
331
+ <div class="status"><span class="dot" id="statusDot"></span><span id="statusText">Connecting...</span></div>
332
+ <button class="theme-toggle" id="themeToggle" type="button" aria-label="Toggle theme" title="Toggle theme">
333
+ <svg class="icon-sun" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
334
+ <circle cx="12" cy="12" r="4"/>
335
+ <path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M4.93 19.07l1.41-1.41M17.66 6.34l1.41-1.41"/>
336
+ </svg>
337
+ <svg class="icon-moon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
338
+ <path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/>
339
+ </svg>
340
+ </button>
341
+ </div>
97
342
  </div>
98
343
 
99
344
  <div class="stats">
@@ -111,7 +356,12 @@
111
356
  <select id="filterSession"><option value="">All sessions</option></select>
112
357
  <button id="btnHumanOnly" onclick="toggleHumanOnly()">Human needed</button>
113
358
  <button id="btnClear" onclick="clearEvents()">Clear</button>
114
- <button id="btnAutoScroll" class="active" onclick="toggleAutoScroll()">Auto-scroll</button>
359
+ </div>
360
+
361
+ <!-- Loader at top — latest events appear here -->
362
+ <div class="timeline-end" id="timelineEnd">
363
+ <div class="loader"></div>
364
+ <div class="waiting" id="waitingText">Listening for events...</div>
115
365
  </div>
116
366
 
117
367
  <div class="timeline" id="timeline">
@@ -121,11 +371,6 @@
121
371
  </div>
122
372
  </div>
123
373
 
124
- <!-- Bottom loader — always visible, shows system is listening -->
125
- <div class="timeline-end" id="timelineEnd">
126
- <div class="loader"></div>
127
- <div class="waiting" id="waitingText">Listening for events...</div>
128
- </div>
129
374
 
130
375
  <script>
131
376
  const timeline = document.getElementById('timeline');
@@ -329,14 +574,17 @@ function addEvent(ev) {
329
574
  const div = renderEvent(ev);
330
575
  div.classList.add('latest');
331
576
  lastEventDiv = div;
332
- timeline.appendChild(div);
577
+
578
+ // Prepend — newest at top
579
+ timeline.prepend(div);
333
580
 
334
581
  updateStats();
335
582
  updateFilterType(ev);
336
583
  updateWaitingText(ev);
337
584
 
585
+ // Scroll to top to show latest
338
586
  if (autoScroll) {
339
- document.getElementById('timelineEnd').scrollIntoView({ behavior: 'smooth' });
587
+ timeline.scrollTop = 0;
340
588
  }
341
589
  }
342
590
 
@@ -453,6 +701,27 @@ function connect() {
453
701
  };
454
702
  }
455
703
 
704
+ // Theme: persist choice in localStorage, fall back to system preference
705
+ const THEME_KEY = 'uv-watchtower-theme';
706
+ const themeToggle = document.getElementById('themeToggle');
707
+ const mql = window.matchMedia('(prefers-color-scheme: light)');
708
+
709
+ function applyTheme(t) { document.documentElement.setAttribute('data-theme', t); }
710
+ function resolvedTheme() {
711
+ return localStorage.getItem(THEME_KEY) || (mql.matches ? 'light' : 'dark');
712
+ }
713
+ applyTheme(resolvedTheme());
714
+
715
+ themeToggle.onclick = () => {
716
+ const next = resolvedTheme() === 'light' ? 'dark' : 'light';
717
+ localStorage.setItem(THEME_KEY, next);
718
+ applyTheme(next);
719
+ };
720
+
721
+ mql.addEventListener('change', () => {
722
+ if (!localStorage.getItem(THEME_KEY)) applyTheme(resolvedTheme());
723
+ });
724
+
456
725
  connect();
457
726
  </script>
458
727
  </body>