claude-pager 0.1.7 → 0.2.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.
Files changed (48) hide show
  1. package/README.md +26 -0
  2. package/dist/cli/run.d.ts.map +1 -1
  3. package/dist/cli/run.js +8 -0
  4. package/dist/cli/run.js.map +1 -1
  5. package/dist/cli/setup.d.ts.map +1 -1
  6. package/dist/cli/setup.js +70 -0
  7. package/dist/cli/setup.js.map +1 -1
  8. package/dist/daemon/handlers.d.ts.map +1 -1
  9. package/dist/daemon/handlers.js +10 -2
  10. package/dist/daemon/handlers.js.map +1 -1
  11. package/dist/daemon/server.d.ts.map +1 -1
  12. package/dist/daemon/server.js +94 -6
  13. package/dist/daemon/server.js.map +1 -1
  14. package/dist/dashboard/ci-provider.d.ts +20 -0
  15. package/dist/dashboard/ci-provider.d.ts.map +1 -0
  16. package/dist/dashboard/ci-provider.js +194 -0
  17. package/dist/dashboard/ci-provider.js.map +1 -0
  18. package/dist/dashboard/enricher.d.ts +34 -0
  19. package/dist/dashboard/enricher.d.ts.map +1 -0
  20. package/dist/dashboard/enricher.js +143 -0
  21. package/dist/dashboard/enricher.js.map +1 -0
  22. package/dist/dashboard/git-status.d.ts +7 -0
  23. package/dist/dashboard/git-status.d.ts.map +1 -0
  24. package/dist/dashboard/git-status.js +33 -0
  25. package/dist/dashboard/git-status.js.map +1 -0
  26. package/dist/dashboard/html.d.ts +2 -0
  27. package/dist/dashboard/html.d.ts.map +1 -0
  28. package/dist/dashboard/html.js +878 -0
  29. package/dist/dashboard/html.js.map +1 -0
  30. package/dist/dashboard/routes.d.ts +3 -0
  31. package/dist/dashboard/routes.d.ts.map +1 -0
  32. package/dist/dashboard/routes.js +14 -0
  33. package/dist/dashboard/routes.js.map +1 -0
  34. package/dist/dashboard/transcript.d.ts +10 -0
  35. package/dist/dashboard/transcript.d.ts.map +1 -0
  36. package/dist/dashboard/transcript.js +217 -0
  37. package/dist/dashboard/transcript.js.map +1 -0
  38. package/dist/sessions/events.d.ts +1 -0
  39. package/dist/sessions/events.d.ts.map +1 -1
  40. package/dist/sessions/events.js +13 -0
  41. package/dist/sessions/events.js.map +1 -1
  42. package/dist/sessions/tracker.d.ts +1 -0
  43. package/dist/sessions/tracker.d.ts.map +1 -1
  44. package/dist/sessions/tracker.js +15 -0
  45. package/dist/sessions/tracker.js.map +1 -1
  46. package/dist/types.d.ts +11 -0
  47. package/dist/types.d.ts.map +1 -1
  48. package/package.json +1 -1
@@ -0,0 +1,878 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DASHBOARD_HTML = void 0;
4
+ exports.DASHBOARD_HTML = `<!DOCTYPE html>
5
+ <html lang="en">
6
+ <head>
7
+ <meta charset="utf-8">
8
+ <meta name="viewport" content="width=device-width, initial-scale=1">
9
+ <title>claude-pager dashboard</title>
10
+ <link rel="preconnect" href="https://fonts.googleapis.com">
11
+ <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;600;700&display=swap" rel="stylesheet">
12
+ <style>
13
+ * { margin: 0; padding: 0; box-sizing: border-box; }
14
+
15
+ body {
16
+ background: #0d1117;
17
+ color: #c9d1d9;
18
+ font-family: 'JetBrains Mono', monospace;
19
+ min-height: 100vh;
20
+ padding: 24px;
21
+ }
22
+
23
+ header {
24
+ display: flex;
25
+ align-items: center;
26
+ justify-content: space-between;
27
+ margin-bottom: 32px;
28
+ padding-bottom: 16px;
29
+ border-bottom: 1px solid #21262d;
30
+ }
31
+
32
+ .logo {
33
+ display: flex;
34
+ align-items: center;
35
+ gap: 12px;
36
+ }
37
+
38
+ .logo h1 {
39
+ font-size: 22px;
40
+ font-weight: 700;
41
+ color: #f0f6fc;
42
+ }
43
+
44
+ .cursor {
45
+ display: inline-block;
46
+ width: 10px;
47
+ height: 20px;
48
+ background: #58a6ff;
49
+ animation: blink 1s step-end infinite;
50
+ vertical-align: middle;
51
+ margin-left: 4px;
52
+ }
53
+
54
+ @keyframes blink {
55
+ 50% { opacity: 0; }
56
+ }
57
+
58
+ .status-dot {
59
+ width: 8px;
60
+ height: 8px;
61
+ border-radius: 50%;
62
+ display: inline-block;
63
+ }
64
+
65
+ .status-dot.connected { background: #3fb950; box-shadow: 0 0 6px #3fb950; }
66
+ .status-dot.disconnected { background: #f85149; box-shadow: 0 0 6px #f85149; }
67
+
68
+ .meta {
69
+ font-size: 12px;
70
+ color: #484f58;
71
+ display: flex;
72
+ align-items: center;
73
+ gap: 8px;
74
+ }
75
+
76
+ .project {
77
+ margin-bottom: 28px;
78
+ }
79
+
80
+ .project-header {
81
+ display: flex;
82
+ align-items: center;
83
+ gap: 10px;
84
+ margin-bottom: 12px;
85
+ }
86
+
87
+ .project-header h2 {
88
+ font-size: 16px;
89
+ font-weight: 600;
90
+ color: #58a6ff;
91
+ }
92
+
93
+ .project-count {
94
+ background: #21262d;
95
+ color: #8b949e;
96
+ font-size: 11px;
97
+ padding: 2px 8px;
98
+ border-radius: 10px;
99
+ }
100
+
101
+ .project-path {
102
+ font-size: 11px;
103
+ color: #484f58;
104
+ margin-left: auto;
105
+ }
106
+
107
+ .pin-btn {
108
+ background: none;
109
+ border: none;
110
+ cursor: pointer;
111
+ font-size: 14px;
112
+ opacity: 0.3;
113
+ transition: opacity 0.2s;
114
+ padding: 2px 4px;
115
+ }
116
+
117
+ .pin-btn:hover { opacity: 0.7; }
118
+ .pin-btn.pinned { opacity: 1; }
119
+
120
+ .dismiss-btn {
121
+ background: none;
122
+ border: none;
123
+ cursor: pointer;
124
+ font-size: 12px;
125
+ opacity: 0.25;
126
+ transition: opacity 0.2s;
127
+ padding: 2px 4px;
128
+ }
129
+
130
+ .dismiss-btn:hover { opacity: 0.8; color: #f85149; }
131
+
132
+ .ci-row {
133
+ display: flex;
134
+ gap: 12px;
135
+ margin-bottom: 12px;
136
+ font-size: 11px;
137
+ }
138
+
139
+ .ci-badge {
140
+ display: inline-flex;
141
+ align-items: center;
142
+ gap: 5px;
143
+ padding: 3px 10px;
144
+ border-radius: 12px;
145
+ font-weight: 600;
146
+ text-decoration: none;
147
+ transition: opacity 0.2s;
148
+ }
149
+
150
+ .ci-badge:hover { opacity: 0.8; }
151
+
152
+ .ci-badge.success { background: #0d2818; color: #3fb950; }
153
+ .ci-badge.failed { background: #490202; color: #f85149; }
154
+ .ci-badge.running { background: #0d419d; color: #58a6ff; animation: pulse 2s ease-in-out infinite; }
155
+ .ci-badge.pending { background: #3d2e00; color: #d29922; }
156
+ .ci-badge.canceled { background: #21262d; color: #8b949e; }
157
+ .ci-badge.unknown { background: #21262d; color: #484f58; }
158
+
159
+ .ci-dot {
160
+ width: 7px;
161
+ height: 7px;
162
+ border-radius: 50%;
163
+ display: inline-block;
164
+ }
165
+
166
+ .ci-dot.success { background: #3fb950; }
167
+ .ci-dot.failed { background: #f85149; }
168
+ .ci-dot.running { background: #58a6ff; }
169
+ .ci-dot.pending { background: #d29922; }
170
+ .ci-dot.canceled { background: #8b949e; }
171
+ .ci-dot.unknown { background: #484f58; }
172
+
173
+ .sessions {
174
+ display: grid;
175
+ grid-template-columns: repeat(auto-fill, minmax(380px, 1fr));
176
+ gap: 12px;
177
+ }
178
+
179
+ .card {
180
+ background: #161b22;
181
+ border: 1px solid #21262d;
182
+ border-radius: 8px;
183
+ padding: 16px;
184
+ transition: border-color 0.2s, box-shadow 0.2s, opacity 0.3s;
185
+ }
186
+
187
+ .card:hover {
188
+ border-color: #388bfd44;
189
+ box-shadow: 0 0 12px #388bfd22;
190
+ }
191
+
192
+ .card.stale {
193
+ opacity: 0.45;
194
+ border-style: dashed;
195
+ }
196
+
197
+ .card.stale:hover {
198
+ opacity: 0.8;
199
+ }
200
+
201
+ .card.active {
202
+ border-color: #388bfd44;
203
+ border-left: 3px solid #58a6ff;
204
+ }
205
+
206
+ .card.alert {
207
+ border-color: #f0883e44;
208
+ border-left: 3px solid #f0883e;
209
+ }
210
+
211
+ .card-header {
212
+ display: flex;
213
+ align-items: flex-start;
214
+ justify-content: space-between;
215
+ margin-bottom: 10px;
216
+ }
217
+
218
+ .card-title {
219
+ font-size: 14px;
220
+ font-weight: 600;
221
+ color: #f0f6fc;
222
+ line-height: 1.3;
223
+ flex: 1;
224
+ margin-right: 8px;
225
+ overflow: hidden;
226
+ display: -webkit-box;
227
+ -webkit-line-clamp: 2;
228
+ -webkit-box-orient: vertical;
229
+ }
230
+
231
+ .card-title.expanded {
232
+ -webkit-line-clamp: unset;
233
+ overflow: visible;
234
+ white-space: pre-wrap;
235
+ }
236
+
237
+ .expand-btn {
238
+ background: none;
239
+ border: none;
240
+ color: #58a6ff;
241
+ font-family: 'JetBrains Mono', monospace;
242
+ font-size: 11px;
243
+ cursor: pointer;
244
+ padding: 2px 0;
245
+ opacity: 0.8;
246
+ }
247
+
248
+ .expand-btn:hover { opacity: 1; }
249
+
250
+ .badge {
251
+ font-size: 10px;
252
+ font-weight: 600;
253
+ padding: 3px 8px;
254
+ border-radius: 12px;
255
+ white-space: nowrap;
256
+ text-transform: uppercase;
257
+ letter-spacing: 0.5px;
258
+ }
259
+
260
+ .badge.working {
261
+ background: #0d419d;
262
+ color: #58a6ff;
263
+ animation: pulse 2s ease-in-out infinite;
264
+ }
265
+
266
+ .badge.waiting_permission {
267
+ background: #5a1e02;
268
+ color: #f0883e;
269
+ }
270
+
271
+ .badge.waiting_input {
272
+ background: #3d2e00;
273
+ color: #d29922;
274
+ }
275
+
276
+ .badge.idle {
277
+ background: #21262d;
278
+ color: #8b949e;
279
+ }
280
+
281
+ .badge.unknown {
282
+ background: #21262d;
283
+ color: #484f58;
284
+ }
285
+
286
+ @keyframes pulse {
287
+ 0%, 100% { opacity: 1; }
288
+ 50% { opacity: 0.6; }
289
+ }
290
+
291
+ .pending-box {
292
+ background: #1c1208;
293
+ border: 1px solid #3d2e00;
294
+ border-radius: 6px;
295
+ padding: 8px 10px;
296
+ margin-bottom: 10px;
297
+ font-size: 12px;
298
+ color: #d29922;
299
+ }
300
+
301
+ .pending-box .tool {
302
+ color: #f0883e;
303
+ font-weight: 600;
304
+ }
305
+
306
+ .pending-box .ago {
307
+ color: #8b949e;
308
+ float: right;
309
+ }
310
+
311
+ .action-row {
312
+ display: flex;
313
+ gap: 8px;
314
+ margin-top: 8px;
315
+ }
316
+
317
+ .action-btn {
318
+ font-family: 'JetBrains Mono', monospace;
319
+ font-size: 11px;
320
+ font-weight: 600;
321
+ padding: 4px 14px;
322
+ border-radius: 6px;
323
+ border: none;
324
+ cursor: pointer;
325
+ transition: opacity 0.2s, transform 0.1s;
326
+ }
327
+
328
+ .action-btn:hover { opacity: 0.85; }
329
+ .action-btn:active { transform: scale(0.96); }
330
+
331
+ .action-btn.allow {
332
+ background: #238636;
333
+ color: #ffffff;
334
+ }
335
+
336
+ .action-btn.deny {
337
+ background: #da3633;
338
+ color: #ffffff;
339
+ }
340
+
341
+ .action-btn.allow-all {
342
+ background: #1f6feb;
343
+ color: #ffffff;
344
+ margin-left: auto;
345
+ }
346
+
347
+ .action-btn:disabled {
348
+ opacity: 0.4;
349
+ cursor: not-allowed;
350
+ }
351
+
352
+ .reply-input {
353
+ flex: 1;
354
+ font-family: 'JetBrains Mono', monospace;
355
+ font-size: 11px;
356
+ padding: 4px 10px;
357
+ border-radius: 6px;
358
+ border: 1px solid #30363d;
359
+ background: #0d1117;
360
+ color: #c9d1d9;
361
+ outline: none;
362
+ }
363
+
364
+ .reply-input:focus {
365
+ border-color: #58a6ff;
366
+ }
367
+
368
+ .git-row {
369
+ display: flex;
370
+ align-items: center;
371
+ gap: 12px;
372
+ font-size: 11px;
373
+ margin-bottom: 6px;
374
+ }
375
+
376
+ .git-branch {
377
+ color: #8b949e;
378
+ }
379
+
380
+ .git-branch::before {
381
+ content: '⎇ ';
382
+ }
383
+
384
+ .git-modified {
385
+ color: #f85149;
386
+ }
387
+
388
+ .git-unpushed {
389
+ color: #d29922;
390
+ }
391
+
392
+ .git-clean {
393
+ color: #3fb950;
394
+ }
395
+
396
+ .needs-testing {
397
+ display: inline-block;
398
+ font-size: 10px;
399
+ font-weight: 600;
400
+ padding: 2px 8px;
401
+ border-radius: 10px;
402
+ background: #490202;
403
+ color: #f85149;
404
+ text-transform: uppercase;
405
+ letter-spacing: 0.5px;
406
+ }
407
+
408
+ .flag {
409
+ display: inline-block;
410
+ font-size: 10px;
411
+ font-weight: 600;
412
+ padding: 2px 8px;
413
+ border-radius: 10px;
414
+ letter-spacing: 0.3px;
415
+ }
416
+
417
+ .flag.ok {
418
+ background: #0d2818;
419
+ color: #3fb950;
420
+ }
421
+
422
+ .flag.pending {
423
+ background: #3d2e00;
424
+ color: #d29922;
425
+ }
426
+
427
+ .card-footer {
428
+ display: flex;
429
+ align-items: center;
430
+ flex-wrap: wrap;
431
+ gap: 8px;
432
+ margin-top: 8px;
433
+ font-size: 10px;
434
+ color: #484f58;
435
+ }
436
+
437
+ .card-footer .spacer {
438
+ margin-left: auto;
439
+ }
440
+
441
+ .empty {
442
+ text-align: center;
443
+ padding: 60px 20px;
444
+ color: #484f58;
445
+ }
446
+
447
+ .empty h2 {
448
+ font-size: 18px;
449
+ color: #8b949e;
450
+ margin-bottom: 8px;
451
+ }
452
+
453
+ .empty p {
454
+ font-size: 13px;
455
+ }
456
+
457
+ @media (max-width: 768px) {
458
+ body { padding: 12px; }
459
+
460
+ header { flex-direction: column; align-items: flex-start; gap: 8px; }
461
+
462
+ .logo h1 { font-size: 18px; }
463
+
464
+ .sessions {
465
+ grid-template-columns: 1fr;
466
+ gap: 10px;
467
+ }
468
+
469
+ .project-header {
470
+ flex-wrap: wrap;
471
+ }
472
+
473
+ .project-path { display: none; }
474
+
475
+ .card { padding: 12px; }
476
+
477
+ .card-title { font-size: 13px; }
478
+
479
+ .action-btn {
480
+ padding: 8px 18px;
481
+ font-size: 13px;
482
+ }
483
+
484
+ .reply-input {
485
+ font-size: 13px;
486
+ padding: 8px 10px;
487
+ }
488
+
489
+ .ci-row { flex-wrap: wrap; gap: 6px; }
490
+
491
+ .pending-box { font-size: 11px; }
492
+
493
+ .pending-box code { font-size: 9px; }
494
+ }
495
+
496
+ @media (max-width: 480px) {
497
+ body { padding: 8px; }
498
+
499
+ .logo h1 { font-size: 16px; }
500
+
501
+ .badge { font-size: 9px; padding: 2px 6px; }
502
+
503
+ .git-row { flex-wrap: wrap; gap: 6px; }
504
+
505
+ .action-btn {
506
+ padding: 10px 20px;
507
+ font-size: 14px;
508
+ }
509
+
510
+ .action-btn.allow-all {
511
+ width: 100%;
512
+ text-align: center;
513
+ }
514
+ }
515
+
516
+ .scanline {
517
+ position: fixed;
518
+ top: 0;
519
+ left: 0;
520
+ right: 0;
521
+ bottom: 0;
522
+ pointer-events: none;
523
+ background: repeating-linear-gradient(
524
+ 0deg,
525
+ transparent,
526
+ transparent 2px,
527
+ rgba(0, 0, 0, 0.03) 2px,
528
+ rgba(0, 0, 0, 0.03) 4px
529
+ );
530
+ z-index: 999;
531
+ }
532
+ </style>
533
+ </head>
534
+ <body>
535
+ <div class="scanline"></div>
536
+ <header>
537
+ <div class="logo">
538
+ <h1>claude-pager<span class="cursor"></span></h1>
539
+ </div>
540
+ <div class="meta">
541
+ <button class="action-btn allow-all" id="allowAllBtn" style="display:none" onclick="allowAll()">Allow All</button>
542
+ <span class="status-dot connected" id="statusDot"></span>
543
+ <span id="lastUpdate">connecting...</span>
544
+ </div>
545
+ </header>
546
+ <main id="projects"></main>
547
+
548
+ <script>
549
+ let data = null;
550
+
551
+ function getPinnedOrder() {
552
+ try { return JSON.parse(localStorage.getItem('dashboard-pin-order') || '[]'); }
553
+ catch { return []; }
554
+ }
555
+
556
+ function savePinnedOrder(order) {
557
+ localStorage.setItem('dashboard-pin-order', JSON.stringify(order));
558
+ }
559
+
560
+ function togglePin(name) {
561
+ const order = getPinnedOrder();
562
+ const idx = order.indexOf(name);
563
+ if (idx >= 0) {
564
+ order.splice(idx, 1);
565
+ } else {
566
+ order.push(name);
567
+ }
568
+ savePinnedOrder(order);
569
+ if (data) render(data);
570
+ }
571
+
572
+ function sortProjects(projects) {
573
+ const pinned = getPinnedOrder();
574
+ return [...projects].sort((a, b) => {
575
+ const aPin = pinned.indexOf(a.name);
576
+ const bPin = pinned.indexOf(b.name);
577
+ const aIsPinned = aPin >= 0;
578
+ const bIsPinned = bPin >= 0;
579
+ // Pinned projects first, in their pinned order
580
+ if (aIsPinned && bIsPinned) return aPin - bPin;
581
+ if (aIsPinned) return -1;
582
+ if (bIsPinned) return 1;
583
+ // Unpinned: keep the original sort (by state)
584
+ return 0;
585
+ });
586
+ }
587
+
588
+ function escapeHtml(s) {
589
+ return s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
590
+ }
591
+
592
+ function timeAgo(epochMs) {
593
+ if (!epochMs) return 'unknown';
594
+ const s = Math.floor((Date.now() - epochMs) / 1000);
595
+ if (s < 10) return 'just now';
596
+ if (s < 60) return s + 's ago';
597
+ if (s < 3600) return Math.floor(s / 60) + 'm ago';
598
+ if (s < 86400) return Math.floor(s / 3600) + 'h ago';
599
+ return Math.floor(s / 86400) + 'd ago';
600
+ }
601
+
602
+ function stateLabel(state) {
603
+ const labels = {
604
+ working: 'Working',
605
+ waiting_permission: 'Permission',
606
+ waiting_input: 'Waiting',
607
+ idle: 'Idle',
608
+ unknown: '?',
609
+ };
610
+ return labels[state] || state;
611
+ }
612
+
613
+ function renderSession(s) {
614
+ const ageMs = Date.now() - s.lastActivity;
615
+ const isStale = (s.state === 'idle' || s.state === 'unknown') && ageMs > 2 * 3600_000;
616
+ const isAlert = s.state === 'waiting_permission' || s.state === 'waiting_input';
617
+ const isActive = s.state === 'working';
618
+ const cardClass = isStale ? 'stale' : isAlert ? 'alert' : isActive ? 'active' : '';
619
+
620
+ let pending = '';
621
+ if (s.pendingQuestion) {
622
+ const q = s.pendingQuestion;
623
+ const isPermission = q.type === 'permission_prompt';
624
+ const toolInfo = q.toolName
625
+ ? '<span class="tool">' + escapeHtml(q.toolName) + '</span>' +
626
+ (q.toolInput ? '<br><code style="font-size:10px;color:#8b949e;word-break:break-all">' + escapeHtml(q.toolInput.slice(0, 200)) + '</code>' : '')
627
+ : escapeHtml(q.message.slice(0, 150));
628
+
629
+ const actions = isPermission
630
+ ? \`<div class="action-row">
631
+ <button class="action-btn allow" onclick="respondTo('\${q.eventId}', 'allow', this)">✓ Allow</button>
632
+ <button class="action-btn deny" onclick="respondTo('\${q.eventId}', 'deny', this)">✗ Deny</button>
633
+ </div>\`
634
+ : \`<div class="action-row" style="align-items:center">
635
+ <input type="text" class="reply-input" id="reply-\${q.eventId}" placeholder="Type a reply..." onkeydown="if(event.key==='Enter')respondTo('\${q.eventId}',this.value,this)">
636
+ <button class="action-btn allow" onclick="respondTo('\${q.eventId}',document.getElementById('reply-\${q.eventId}').value,this)">Send</button>
637
+ </div>\`;
638
+
639
+ pending = \`
640
+ <div class="pending-box">
641
+ <span class="ago">\${timeAgo(Date.now() - q.agoSeconds * 1000)}</span>
642
+ \${toolInfo}
643
+ \${actions}
644
+ </div>
645
+ \`;
646
+ }
647
+
648
+ const hasGit = s.git.branch !== 'unknown';
649
+
650
+ const gitParts = [];
651
+ if (hasGit) {
652
+ gitParts.push('<span class="git-branch">' + escapeHtml(s.git.branch) + '</span>');
653
+ gitParts.push(s.git.modifiedFiles > 0
654
+ ? '<span class="git-modified">' + s.git.modifiedFiles + ' mod</span>'
655
+ : '<span class="git-clean">clean</span>');
656
+ if (s.git.unpushedCommits > 0) gitParts.push('<span class="git-unpushed">' + s.git.unpushedCommits + ' unpush</span>');
657
+ gitParts.push(s.committed
658
+ ? '<span class="flag ok">✓ commit</span>'
659
+ : '<span class="flag pending">○ uncommit</span>');
660
+ gitParts.push(s.pushed
661
+ ? '<span class="flag ok">✓ push</span>'
662
+ : '<span class="flag pending">○ unpush</span>');
663
+ }
664
+
665
+ // Show reply input for idle/waiting sessions without a pending question
666
+ const idleInput = (!s.pendingQuestion && (s.state === 'idle' || s.state === 'waiting_input' || s.state === 'unknown'))
667
+ ? \`<div class="action-row" style="margin-top:6px">
668
+ <input type="text" class="reply-input" id="idle-\${s.sessionId}" placeholder="Send a message..." onkeydown="if(event.key==='Enter')sendToSession('\${s.sessionId}',this.value,this)">
669
+ <button class="action-btn allow" onclick="sendToSession('\${s.sessionId}',document.getElementById('idle-\${s.sessionId}').value,this)">Send</button>
670
+ </div>\`
671
+ : '';
672
+
673
+ const titleId = 'title-' + s.sessionId.slice(0, 8);
674
+ const longTitle = s.title.length > 80;
675
+ const expandBtn = longTitle ? \`<button class="expand-btn" onclick="document.getElementById('\${titleId}').classList.toggle('expanded');this.textContent=this.textContent==='...'?'▲':'...'">...</button>\` : '';
676
+
677
+ return \`
678
+ <div class="card \${cardClass}">
679
+ <div class="card-header">
680
+ <span class="card-title" id="\${titleId}">\${escapeHtml(s.title)}</span>
681
+ <span class="badge \${s.state}">\${stateLabel(s.state)}</span>
682
+ <button class="dismiss-btn" onclick="dismissSession('\${s.sessionId}')" title="Dismiss session">🗑</button>
683
+ </div>
684
+ \${expandBtn}
685
+ \${pending}
686
+ \${idleInput}
687
+ <div class="card-footer">
688
+ \${gitParts.join(' ')}
689
+ <span class="spacer"></span>
690
+ <span>pane \${escapeHtml(s.tmuxPane)}</span>
691
+ <span>\${timeAgo(s.lastActivity)}</span>
692
+ </div>
693
+ </div>
694
+ \`;
695
+ }
696
+
697
+ function renderPipeline(label, pipeline) {
698
+ if (!pipeline) return '';
699
+ const s = pipeline.status;
700
+ const dot = '<span class="ci-dot ' + s + '"></span>';
701
+ const text = label + ': ' + s;
702
+ if (pipeline.url) {
703
+ return '<a class="ci-badge ' + s + '" href="' + escapeHtml(pipeline.url) + '" target="_blank">' + dot + ' ' + text + '</a>';
704
+ }
705
+ return '<span class="ci-badge ' + s + '">' + dot + ' ' + text + '</span>';
706
+ }
707
+
708
+ function renderCI(ci) {
709
+ if (!ci) return '';
710
+ const main = renderPipeline('main', ci.main);
711
+ const staging = renderPipeline('staging', ci.staging);
712
+ return main + staging;
713
+ }
714
+
715
+ function renderProject(p) {
716
+ const isPinned = getPinnedOrder().includes(p.name);
717
+ const anyNeedsTesting = p.sessions.some(s => s.needsTesting);
718
+ const testBadge = anyNeedsTesting ? '<span class="needs-testing">needs testing</span>' : '';
719
+ const ciBadges = renderCI(p.ci);
720
+ const infoRow = (ciBadges || testBadge) ? '<div class="ci-row">' + ciBadges + testBadge + '</div>' : '';
721
+
722
+ return \`
723
+ <div class="project">
724
+ <div class="project-header">
725
+ <button class="pin-btn \${isPinned ? 'pinned' : ''}" onclick="togglePin('\${escapeHtml(p.name)}')" title="\${isPinned ? 'Unpin' : 'Pin'}">\${isPinned ? '📌' : '📌'}</button>
726
+ <h2>\${escapeHtml(p.name)}</h2>
727
+ <span class="project-count">\${p.sessions.length} session\${p.sessions.length > 1 ? 's' : ''}</span>
728
+ <span class="project-path">\${escapeHtml(p.path)}</span>
729
+ </div>
730
+ \${infoRow}
731
+ <div class="sessions">
732
+ \${p.sessions.map(renderSession).join('')}
733
+ </div>
734
+ </div>
735
+ \`;
736
+ }
737
+
738
+ function countPending(data) {
739
+ let count = 0;
740
+ for (const p of data.projects) {
741
+ for (const s of p.sessions) {
742
+ if (s.pendingQuestion && s.pendingQuestion.type === 'permission_prompt') count++;
743
+ }
744
+ }
745
+ return count;
746
+ }
747
+
748
+ function render(data) {
749
+ const container = document.getElementById('projects');
750
+ if (!data.projects || data.projects.length === 0) {
751
+ container.innerHTML = \`
752
+ <div class="empty">
753
+ <h2>No active sessions</h2>
754
+ <p>Start Claude Code in tmux and sessions will appear here.</p>
755
+ </div>
756
+ \`;
757
+ document.getElementById('allowAllBtn').style.display = 'none';
758
+ return;
759
+ }
760
+
761
+ const pendingCount = countPending(data);
762
+ const allowAllBtn = document.getElementById('allowAllBtn');
763
+ if (pendingCount > 1) {
764
+ allowAllBtn.style.display = 'inline-block';
765
+ allowAllBtn.textContent = 'Allow All (' + pendingCount + ')';
766
+ } else {
767
+ allowAllBtn.style.display = 'none';
768
+ }
769
+
770
+ // Skip DOM update if user is typing in an input field
771
+ if (document.activeElement && document.activeElement.tagName === 'INPUT') return;
772
+
773
+ // Preserve expanded title state across re-renders
774
+ const expandedTitles = new Set();
775
+ document.querySelectorAll('.card-title.expanded').forEach(el => expandedTitles.add(el.id));
776
+
777
+ container.innerHTML = sortProjects(data.projects).map(renderProject).join('');
778
+
779
+ // Restore expanded titles
780
+ expandedTitles.forEach(id => {
781
+ const el = document.getElementById(id);
782
+ if (el) {
783
+ el.classList.add('expanded');
784
+ const btn = el.parentElement?.querySelector('.expand-btn');
785
+ if (btn) btn.textContent = '▲';
786
+ }
787
+ });
788
+ }
789
+
790
+ async function respondTo(eventId, response, btn) {
791
+ if (btn) btn.disabled = true;
792
+ try {
793
+ const res = await fetch('/api/v1/respond-to', {
794
+ method: 'POST',
795
+ headers: { 'Content-Type': 'application/json' },
796
+ body: JSON.stringify({ eventId, response }),
797
+ });
798
+ if (res.ok) {
799
+ fetchDashboard();
800
+ } else {
801
+ const err = await res.json();
802
+ console.error('respond-to failed:', err);
803
+ }
804
+ } catch (e) {
805
+ console.error('respond-to error:', e);
806
+ }
807
+ if (btn) btn.disabled = false;
808
+ }
809
+
810
+ async function dismissSession(sessionId) {
811
+ try {
812
+ await fetch('/api/v1/dismiss-session', {
813
+ method: 'POST',
814
+ headers: { 'Content-Type': 'application/json' },
815
+ body: JSON.stringify({ sessionId }),
816
+ });
817
+ fetchDashboard();
818
+ } catch (e) {
819
+ console.error('dismiss error:', e);
820
+ }
821
+ }
822
+
823
+ async function sendToSession(sessionId, text, btn) {
824
+ if (!text || !text.trim()) return;
825
+ if (btn) btn.disabled = true;
826
+ try {
827
+ const res = await fetch('/api/v1/send-to', {
828
+ method: 'POST',
829
+ headers: { 'Content-Type': 'application/json' },
830
+ body: JSON.stringify({ sessionId, text: text.trim() }),
831
+ });
832
+ if (res.ok) {
833
+ fetchDashboard();
834
+ } else {
835
+ const err = await res.json();
836
+ console.error('send-to failed:', err);
837
+ }
838
+ } catch (e) {
839
+ console.error('send-to error:', e);
840
+ }
841
+ if (btn) btn.disabled = false;
842
+ }
843
+
844
+ async function allowAll() {
845
+ if (!data) return;
846
+ const pending = [];
847
+ for (const p of data.projects) {
848
+ for (const s of p.sessions) {
849
+ if (s.pendingQuestion && s.pendingQuestion.type === 'permission_prompt') {
850
+ pending.push(s.pendingQuestion);
851
+ }
852
+ }
853
+ }
854
+ for (const q of pending) {
855
+ await respondTo(q.eventId, 'allow', null);
856
+ }
857
+ fetchDashboard();
858
+ }
859
+
860
+ async function fetchDashboard() {
861
+ try {
862
+ const res = await fetch('/api/v1/dashboard');
863
+ data = await res.json();
864
+ render(data);
865
+ document.getElementById('statusDot').className = 'status-dot connected';
866
+ document.getElementById('lastUpdate').textContent = 'updated ' + timeAgo(data.updatedAt);
867
+ } catch {
868
+ document.getElementById('statusDot').className = 'status-dot disconnected';
869
+ document.getElementById('lastUpdate').textContent = 'disconnected';
870
+ }
871
+ }
872
+
873
+ fetchDashboard();
874
+ setInterval(fetchDashboard, 2000);
875
+ </script>
876
+ </body>
877
+ </html>`;
878
+ //# sourceMappingURL=html.js.map