@mjasnikovs/pi-task 0.17.22 → 0.17.24
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/dist/remote/bridge.d.ts +8 -0
- package/dist/remote/bridge.js +10 -0
- package/dist/remote/events.js +10 -4
- package/dist/remote/history.d.ts +9 -1
- package/dist/remote/history.js +1 -1
- package/dist/remote/protocol.d.ts +26 -2
- package/dist/remote/protocol.js +2 -0
- package/dist/remote/register.js +7 -3
- package/dist/remote/server.d.ts +1 -1
- package/dist/remote/server.js +5 -1
- package/dist/remote/session-state.d.ts +16 -5
- package/dist/remote/session-state.js +44 -8
- package/dist/remote/ui-render.js +6 -2
- package/dist/remote/ui-script.js +378 -23
- package/dist/remote/ui-styles.d.ts +1 -1
- package/dist/remote/ui-styles.js +125 -3
- package/dist/remote/ui.js +13 -1
- package/dist/task/impl-widget.js +2 -2
- package/dist/task/widget.d.ts +9 -0
- package/dist/task/widget.js +60 -2
- package/package.json +1 -1
package/dist/remote/ui-script.js
CHANGED
|
@@ -15,18 +15,57 @@ export function clientScript(wsUrl) {
|
|
|
15
15
|
const contextFill = document.getElementById('context-bar-fill');
|
|
16
16
|
function setContextBar(usage) {
|
|
17
17
|
if (usage && usage.percent != null) contextFill.style.width = usage.percent + '%';
|
|
18
|
+
setStatusChip(usage);
|
|
19
|
+
}
|
|
20
|
+
// Compact token count for the header chip (mirrors formatContextTokens).
|
|
21
|
+
function fmtTokens(n) {
|
|
22
|
+
if (n == null) return '';
|
|
23
|
+
if (n < 1000) return String(n);
|
|
24
|
+
if (n < 10000) return (n / 1000).toFixed(1) + 'k';
|
|
25
|
+
if (n < 1000000) return Math.round(n / 1000) + 'k';
|
|
26
|
+
return (n / 1000000).toFixed(1) + 'M';
|
|
27
|
+
}
|
|
28
|
+
function setStatusChip(usage) {
|
|
29
|
+
if (!usage) return;
|
|
30
|
+
var parts = [];
|
|
31
|
+
if (usage.percent != null) parts.push(Math.round(usage.percent) + '%');
|
|
32
|
+
if (usage.tokens != null && usage.contextWindow) {
|
|
33
|
+
parts.push(fmtTokens(usage.tokens) + '/' + fmtTokens(usage.contextWindow));
|
|
34
|
+
}
|
|
35
|
+
statusCtx.textContent = parts.join(' \\u00B7 ');
|
|
36
|
+
}
|
|
37
|
+
function setModelName(name) {
|
|
38
|
+
if (name && name !== modelName) { modelName = name; statusModel.textContent = name; }
|
|
39
|
+
}
|
|
40
|
+
// Connection dot: red disconnected, mauve pulsing while running, green idle.
|
|
41
|
+
function updateStatusDot() {
|
|
42
|
+
statusDot.className = !connected ? 'disconnected' : (agentRunning ? 'running' : 'idle');
|
|
18
43
|
}
|
|
19
44
|
const reconnectOverlay = document.getElementById('reconnect-overlay');
|
|
20
45
|
const reconnectMsg = document.getElementById('reconnect-msg');
|
|
21
46
|
const cmdSuggestions = document.getElementById('cmd-suggestions');
|
|
22
47
|
const statusPanel = document.getElementById('status-panel');
|
|
48
|
+
const statusDot = document.getElementById('status-dot');
|
|
49
|
+
const statusModel = document.getElementById('status-model');
|
|
50
|
+
const statusCtx = document.getElementById('status-ctx');
|
|
51
|
+
const notifPanel = document.getElementById('notif-panel');
|
|
52
|
+
const notifList = document.getElementById('notif-list');
|
|
53
|
+
const notifToggle = document.getElementById('notif-toggle');
|
|
54
|
+
// Last ~20 toasts, newest first, for the bell dropdown history.
|
|
55
|
+
let notifHistory = [];
|
|
56
|
+
let modelName = '';
|
|
23
57
|
// Widgets are keyed (e.g. 'pi-tasks', 'pi-task-auto'); track them per key so a
|
|
24
58
|
// clear for one key can't be masked by a stale message from another.
|
|
25
59
|
// Single authoritative task-widget slot. The snapshot and the live 'widget'
|
|
26
60
|
// delta both set this; null hides the panel. (No more per-key map that could
|
|
27
61
|
// strand an orphaned widget on screen.)
|
|
28
62
|
let taskWidgetLines = null;
|
|
63
|
+
let taskWidgetData = null;
|
|
29
64
|
function renderWidgets() {
|
|
65
|
+
// Structured payload wins (progress bar + phase badge + elapsed); the plain
|
|
66
|
+
// text lines are the fallback for older/unstructured producers.
|
|
67
|
+
if (taskWidgetData) { renderStructuredWidget(taskWidgetData); return; }
|
|
68
|
+
statusPanel.classList.remove('structured');
|
|
30
69
|
if (taskWidgetLines && taskWidgetLines.length) {
|
|
31
70
|
statusPanel.textContent = taskWidgetLines.join('\\n');
|
|
32
71
|
statusPanel.style.display = 'block';
|
|
@@ -34,6 +73,26 @@ export function clientScript(wsUrl) {
|
|
|
34
73
|
statusPanel.style.display = 'none';
|
|
35
74
|
}
|
|
36
75
|
}
|
|
76
|
+
function renderStructuredWidget(d) {
|
|
77
|
+
statusPanel.classList.add('structured');
|
|
78
|
+
statusPanel.style.display = 'block';
|
|
79
|
+
// Title on its own line (wraps to 2, never truncated to a stub).
|
|
80
|
+
var title = '<div class="widget-title">' + escHtml(d.title || '') + '</div>';
|
|
81
|
+
// Meta row: phase badge · step count · elapsed clock.
|
|
82
|
+
var meta = '<div class="widget-meta">'
|
|
83
|
+
+ (d.phase ? '<span class="widget-phase">' + escHtml(d.phase) + '</span>' : '')
|
|
84
|
+
+ (d.total > 0 && d.done != null ? '<span class="widget-step">' + d.done + '/' + d.total + '</span>' : '')
|
|
85
|
+
+ (d.elapsed ? '<span class="widget-elapsed">' + escHtml(d.elapsed) + '</span>' : '')
|
|
86
|
+
+ '</div>';
|
|
87
|
+
var bar = '';
|
|
88
|
+
if (d.total > 0 && d.done != null) {
|
|
89
|
+
var pct = Math.max(0, Math.min(100, Math.round((d.done / d.total) * 100)));
|
|
90
|
+
bar = '<div class="widget-bar"><div class="widget-bar-fill" style="width:' + pct + '%"></div></div>';
|
|
91
|
+
}
|
|
92
|
+
// Current action — the terminal's last worker/child line, one ellipsized line.
|
|
93
|
+
var action = d.action ? '<div class="widget-action">↳ ' + escHtml(d.action) + '</div>' : '';
|
|
94
|
+
statusPanel.innerHTML = title + meta + bar + action;
|
|
95
|
+
}
|
|
37
96
|
const promptCard = document.getElementById('prompt-card');
|
|
38
97
|
const promptQ = document.getElementById('prompt-q');
|
|
39
98
|
const promptRec = document.getElementById('prompt-rec');
|
|
@@ -50,6 +109,19 @@ export function clientScript(wsUrl) {
|
|
|
50
109
|
const toolCallMap = {};
|
|
51
110
|
let currentBubble = null;
|
|
52
111
|
let streamText = '';
|
|
112
|
+
// Composer state: input is enabled whenever connected AND no prompt card is
|
|
113
|
+
// open (messages typed mid-run steer the live turn); the Send button morphs
|
|
114
|
+
// into a red Stop while the agent is running.
|
|
115
|
+
let agentRunning = false;
|
|
116
|
+
let connected = false;
|
|
117
|
+
// Whether the current live turn produced any content — gates the trailing
|
|
118
|
+
// turn-timestamp so an empty/aborted turn doesn't leave a stray clock.
|
|
119
|
+
let turnHadContent = false;
|
|
120
|
+
let stopArmed = false;
|
|
121
|
+
let stopArmTimer = null;
|
|
122
|
+
// The live thinking (<details>) block being streamed, and its accumulated text.
|
|
123
|
+
let currentThinking = null;
|
|
124
|
+
let thinkingText = '';
|
|
53
125
|
let autoScroll = true;
|
|
54
126
|
let reconnectDelay = 1000;
|
|
55
127
|
let reconnectAnim = null;
|
|
@@ -144,7 +216,7 @@ export function clientScript(wsUrl) {
|
|
|
144
216
|
el.className = 'bubble ' + role;
|
|
145
217
|
// Only assistant text is markdown-rendered; user/error bubbles stay plain so a
|
|
146
218
|
// user's literal *asterisks* or backticks show verbatim.
|
|
147
|
-
if (role === 'assistant') setContent(el, text);
|
|
219
|
+
if (role === 'assistant') { setContent(el, text); attachBubbleCopy(el, text); }
|
|
148
220
|
else el.textContent = text;
|
|
149
221
|
chatLog.appendChild(el);
|
|
150
222
|
scrollBottom();
|
|
@@ -235,10 +307,160 @@ export function clientScript(wsUrl) {
|
|
|
235
307
|
s.appendChild(e);
|
|
236
308
|
}
|
|
237
309
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
310
|
+
// Reconcile the composer (input + Send/Stop button) with the current state.
|
|
311
|
+
// The input is disabled ONLY while disconnected or a prompt card is open — a
|
|
312
|
+
// running agent no longer locks it, so messages can steer the live turn.
|
|
313
|
+
function refreshComposer() {
|
|
314
|
+
updateStatusDot();
|
|
315
|
+
const promptOpen = activePromptId !== null;
|
|
316
|
+
inputEl.disabled = !connected || promptOpen;
|
|
317
|
+
inputEl.placeholder = agentRunning
|
|
318
|
+
? 'message the agent — delivered mid-run'
|
|
319
|
+
: 'type a message\\u2026 (/ for commands)';
|
|
320
|
+
if (agentRunning && !promptOpen) {
|
|
321
|
+
// Send morphs into a red Stop that interrupts the running turn.
|
|
322
|
+
sendBtn.classList.add('stop');
|
|
323
|
+
sendBtn.disabled = !connected;
|
|
324
|
+
if (!stopArmed) sendBtn.textContent = 'Stop';
|
|
325
|
+
} else {
|
|
326
|
+
disarmStop();
|
|
327
|
+
sendBtn.classList.remove('stop');
|
|
328
|
+
sendBtn.textContent = 'Send';
|
|
329
|
+
sendBtn.disabled = !connected || promptOpen;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function disarmStop() {
|
|
334
|
+
stopArmed = false;
|
|
335
|
+
if (stopArmTimer) { clearTimeout(stopArmTimer); stopArmTimer = null; }
|
|
336
|
+
sendBtn.classList.remove('armed');
|
|
337
|
+
if (agentRunning) sendBtn.textContent = 'Stop';
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function sendInterrupt() {
|
|
341
|
+
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
342
|
+
ws.send(JSON.stringify({ type: 'interrupt' }));
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// Stop needs a two-tap confirm (same pattern as the prompt card's cancel):
|
|
347
|
+
// first tap arms it, a second tap within 3s actually interrupts.
|
|
348
|
+
function onSendClick() {
|
|
349
|
+
if (agentRunning && activePromptId === null) {
|
|
350
|
+
if (stopArmed) { disarmStop(); sendInterrupt(); return; }
|
|
351
|
+
stopArmed = true;
|
|
352
|
+
sendBtn.classList.add('armed');
|
|
353
|
+
sendBtn.textContent = 'Tap to stop';
|
|
354
|
+
if (stopArmTimer) clearTimeout(stopArmTimer);
|
|
355
|
+
stopArmTimer = setTimeout(function () {
|
|
356
|
+
stopArmed = false;
|
|
357
|
+
sendBtn.classList.remove('armed');
|
|
358
|
+
if (agentRunning) sendBtn.textContent = 'Stop';
|
|
359
|
+
stopArmTimer = null;
|
|
360
|
+
}, 3000);
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
sendMessage();
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// Copy source text to the clipboard, flashing the button. Falls back to a
|
|
367
|
+
// hidden textarea + execCommand for non-secure (plain-http LAN) contexts where
|
|
368
|
+
// navigator.clipboard is unavailable.
|
|
369
|
+
function flashCopied(btn) {
|
|
370
|
+
if (!btn) return;
|
|
371
|
+
const prev = btn.textContent;
|
|
372
|
+
btn.textContent = 'Copied';
|
|
373
|
+
btn.classList.add('copied');
|
|
374
|
+
setTimeout(function () { btn.textContent = prev; btn.classList.remove('copied'); }, 1200);
|
|
375
|
+
}
|
|
376
|
+
function fallbackCopy(text, btn) {
|
|
377
|
+
try {
|
|
378
|
+
const ta = document.createElement('textarea');
|
|
379
|
+
ta.value = text;
|
|
380
|
+
ta.style.position = 'fixed';
|
|
381
|
+
ta.style.top = '0';
|
|
382
|
+
ta.style.opacity = '0';
|
|
383
|
+
document.body.appendChild(ta);
|
|
384
|
+
ta.focus(); ta.select();
|
|
385
|
+
document.execCommand('copy');
|
|
386
|
+
document.body.removeChild(ta);
|
|
387
|
+
flashCopied(btn);
|
|
388
|
+
} catch (e) {}
|
|
389
|
+
}
|
|
390
|
+
function copyText(text, btn) {
|
|
391
|
+
if (navigator.clipboard && navigator.clipboard.writeText) {
|
|
392
|
+
navigator.clipboard.writeText(text).then(function () { flashCopied(btn); },
|
|
393
|
+
function () { fallbackCopy(text, btn); });
|
|
394
|
+
} else {
|
|
395
|
+
fallbackCopy(text, btn);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
// One delegated handler for every copy button (code-block headers live inside
|
|
399
|
+
// markdown HTML; bubble buttons are appended in JS). Resolve the source text
|
|
400
|
+
// from whichever container the button sits in.
|
|
401
|
+
chatLog.addEventListener('click', function (e) {
|
|
402
|
+
const btn = e.target && e.target.closest ? e.target.closest('.copy-btn') : null;
|
|
403
|
+
if (!btn) return;
|
|
404
|
+
const block = btn.closest('.code-block');
|
|
405
|
+
if (block) {
|
|
406
|
+
const code = block.querySelector('code');
|
|
407
|
+
copyText(code ? code.textContent : '', btn);
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
const bub = btn.closest('.bubble');
|
|
411
|
+
if (bub) copyText(bub.__copyText != null ? bub.__copyText : bub.textContent, btn);
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
// Attach a copy button to a finished assistant bubble and stash its raw text
|
|
415
|
+
// (the pre-markdown source) so the delegated handler copies the original.
|
|
416
|
+
function attachBubbleCopy(el, text) {
|
|
417
|
+
el.__copyText = text;
|
|
418
|
+
const b = document.createElement('button');
|
|
419
|
+
b.className = 'copy-btn bubble-copy';
|
|
420
|
+
b.type = 'button';
|
|
421
|
+
b.setAttribute('aria-label', 'Copy message');
|
|
422
|
+
b.textContent = 'Copy';
|
|
423
|
+
el.appendChild(b);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// "✻ Thinking… (n lines)" collapsed-block summary.
|
|
427
|
+
function thinkingSummary(n) {
|
|
428
|
+
return '\\u273B Thinking\\u2026 (' + n + (n === 1 ? ' line' : ' lines') + ')';
|
|
429
|
+
}
|
|
430
|
+
function thinkingLineCount(text) {
|
|
431
|
+
return text ? text.split('\\n').length : 0;
|
|
432
|
+
}
|
|
433
|
+
// Build a collapsed thinking <details>. A live block keeps a spinner in the
|
|
434
|
+
// summary and stays open to further deltas; otherwise it's a finished, static one.
|
|
435
|
+
function makeThinkingEl(text, live) {
|
|
436
|
+
const d = document.createElement('details');
|
|
437
|
+
d.className = 'thinking-block';
|
|
438
|
+
const s = document.createElement('summary');
|
|
439
|
+
const lbl = document.createElement('span');
|
|
440
|
+
lbl.className = 'thinking-label';
|
|
441
|
+
lbl.textContent = thinkingSummary(thinkingLineCount(text));
|
|
442
|
+
s.appendChild(lbl);
|
|
443
|
+
if (live) {
|
|
444
|
+
const sp = document.createElement('span');
|
|
445
|
+
sp.className = 'thinking-spin spin';
|
|
446
|
+
s.appendChild(sp);
|
|
447
|
+
}
|
|
448
|
+
d.appendChild(s);
|
|
449
|
+
const body = document.createElement('div');
|
|
450
|
+
body.className = 'thinking-body';
|
|
451
|
+
body.textContent = text || '';
|
|
452
|
+
d.appendChild(body);
|
|
453
|
+
return d;
|
|
454
|
+
}
|
|
455
|
+
// Close out the live thinking block (drop its spinner) — called when the model
|
|
456
|
+
// moves on to text/tools, or the turn ends.
|
|
457
|
+
function finalizeThinking() {
|
|
458
|
+
if (currentThinking) {
|
|
459
|
+
const sp = currentThinking.querySelector('.thinking-spin');
|
|
460
|
+
if (sp) { sp.remove(); stopSpinIfIdle(); }
|
|
461
|
+
currentThinking = null;
|
|
462
|
+
}
|
|
463
|
+
thinkingText = '';
|
|
242
464
|
}
|
|
243
465
|
|
|
244
466
|
// Pull the human-readable text out of a tool result. Many tools (Read, Bash,
|
|
@@ -289,6 +511,23 @@ export function clientScript(wsUrl) {
|
|
|
289
511
|
return d;
|
|
290
512
|
}
|
|
291
513
|
|
|
514
|
+
// Dim HH:MM shown under a turn's last bubble (local time).
|
|
515
|
+
function fmtClock(ts) {
|
|
516
|
+
if (!ts) return '';
|
|
517
|
+
var d = new Date(ts);
|
|
518
|
+
var pad = function (n) { return (n < 10 ? '0' : '') + n; };
|
|
519
|
+
return pad(d.getHours()) + ':' + pad(d.getMinutes());
|
|
520
|
+
}
|
|
521
|
+
function addTurnTime(ts, roleClass) {
|
|
522
|
+
var clock = fmtClock(ts || Date.now());
|
|
523
|
+
if (!clock) return;
|
|
524
|
+
var el = document.createElement('div');
|
|
525
|
+
el.className = 'turn-time ' + roleClass;
|
|
526
|
+
el.textContent = clock;
|
|
527
|
+
chatLog.appendChild(el);
|
|
528
|
+
scrollBottom();
|
|
529
|
+
}
|
|
530
|
+
|
|
292
531
|
// A muted, centered system note (e.g. "Context compacted").
|
|
293
532
|
function addSystemLine(text) {
|
|
294
533
|
const el = document.createElement('div');
|
|
@@ -303,13 +542,17 @@ export function clientScript(wsUrl) {
|
|
|
303
542
|
// parts (text segments + tool calls), so the layout matches the terminal's
|
|
304
543
|
// interleaving instead of one merged blob with tools dumped at the end.
|
|
305
544
|
function renderTurn(t) {
|
|
306
|
-
if (t.error) { addBubble('error', t.text); return; }
|
|
307
|
-
if (t.role === 'system') { addSystemLine(t.text); return; }
|
|
308
|
-
if (t.role === 'user') { addBubble('user', t.text); return; }
|
|
545
|
+
if (t.error) { addBubble('error', t.text); addTurnTime(t.ts, 'assistant'); return; }
|
|
546
|
+
if (t.role === 'system') { addSystemLine(t.text); addTurnTime(t.ts, 'system'); return; }
|
|
547
|
+
if (t.role === 'user') { addBubble('user', t.text); addTurnTime(t.ts, 'user'); return; }
|
|
309
548
|
for (const p of (t.parts || [])) {
|
|
310
549
|
if (p.kind === 'text') { if (p.text) addBubble('assistant', p.text); }
|
|
550
|
+
else if (p.kind === 'thinking') {
|
|
551
|
+
if (p.text) { chatLog.appendChild(makeThinkingEl(p.text, false)); scrollBottom(); }
|
|
552
|
+
}
|
|
311
553
|
else renderToolPart(p);
|
|
312
554
|
}
|
|
555
|
+
addTurnTime(t.ts, 'assistant');
|
|
313
556
|
}
|
|
314
557
|
|
|
315
558
|
// Render the in-progress assistant turn from a snapshot, preserving order. The
|
|
@@ -335,6 +578,18 @@ export function clientScript(wsUrl) {
|
|
|
335
578
|
} else if (p.text) {
|
|
336
579
|
addBubble('assistant', p.text);
|
|
337
580
|
}
|
|
581
|
+
} else if (p.kind === 'thinking') {
|
|
582
|
+
if (last && !p.done) {
|
|
583
|
+
// Reconnected mid-thinking: keep the block live so further deltas flow in.
|
|
584
|
+
currentThinking = makeThinkingEl(p.text || '', true);
|
|
585
|
+
thinkingText = p.text || '';
|
|
586
|
+
chatLog.appendChild(currentThinking);
|
|
587
|
+
startSpin();
|
|
588
|
+
scrollBottom();
|
|
589
|
+
} else if (p.text) {
|
|
590
|
+
chatLog.appendChild(makeThinkingEl(p.text, false));
|
|
591
|
+
scrollBottom();
|
|
592
|
+
}
|
|
338
593
|
} else {
|
|
339
594
|
renderToolPart(p);
|
|
340
595
|
}
|
|
@@ -347,6 +602,37 @@ export function clientScript(wsUrl) {
|
|
|
347
602
|
t.textContent = message;
|
|
348
603
|
document.body.appendChild(t);
|
|
349
604
|
setTimeout(function () { t.remove(); }, 4000);
|
|
605
|
+
recordNotif(message, level);
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
// Keep the last ~20 toasts (newest first) for the bell's history dropdown.
|
|
609
|
+
function recordNotif(message, level) {
|
|
610
|
+
notifHistory.unshift({message: message, level: level || 'info', ts: Date.now()});
|
|
611
|
+
if (notifHistory.length > 20) notifHistory.pop();
|
|
612
|
+
renderNotifList();
|
|
613
|
+
}
|
|
614
|
+
function renderNotifList() {
|
|
615
|
+
if (!notifHistory.length) {
|
|
616
|
+
notifList.innerHTML = '<div id="notif-empty">No notifications yet.</div>';
|
|
617
|
+
return;
|
|
618
|
+
}
|
|
619
|
+
var html = '';
|
|
620
|
+
for (var i = 0; i < notifHistory.length; i++) {
|
|
621
|
+
var n = notifHistory[i];
|
|
622
|
+
html += '<div class="notif-item ' + escHtml(n.level) + '">'
|
|
623
|
+
+ '<span class="notif-dot"></span>'
|
|
624
|
+
+ '<span class="notif-msg">' + escHtml(n.message) + '</span>'
|
|
625
|
+
+ '<span class="notif-time">' + fmtClock(n.ts) + '</span></div>';
|
|
626
|
+
}
|
|
627
|
+
notifList.innerHTML = html;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
let notifOpen = false;
|
|
631
|
+
function setNotifOpen(open) {
|
|
632
|
+
notifOpen = open;
|
|
633
|
+
notifPanel.classList.toggle('open', open);
|
|
634
|
+
notifPanel.setAttribute('aria-hidden', open ? 'false' : 'true');
|
|
635
|
+
if (open) { renderNotifList(); updateNotifToggle(); }
|
|
350
636
|
}
|
|
351
637
|
|
|
352
638
|
const bell = document.getElementById('bell');
|
|
@@ -363,6 +649,12 @@ export function clientScript(wsUrl) {
|
|
|
363
649
|
var on = notifyEnabled();
|
|
364
650
|
bell.textContent = on ? '\\u25C9' : '\\u25EF';
|
|
365
651
|
bell.classList.toggle('on', on);
|
|
652
|
+
updateNotifToggle();
|
|
653
|
+
}
|
|
654
|
+
function updateNotifToggle() {
|
|
655
|
+
var on = notifyEnabled();
|
|
656
|
+
notifToggle.textContent = on ? 'On' : 'Enable';
|
|
657
|
+
notifToggle.classList.toggle('on', on);
|
|
366
658
|
}
|
|
367
659
|
|
|
368
660
|
// Why notifications can't be enabled here, or null if they can.
|
|
@@ -376,7 +668,17 @@ export function clientScript(wsUrl) {
|
|
|
376
668
|
return null;
|
|
377
669
|
}
|
|
378
670
|
|
|
379
|
-
bell
|
|
671
|
+
// The bell opens the notification dropdown (history + push toggle); the push
|
|
672
|
+
// enable/disable lives on the toggle inside it.
|
|
673
|
+
bell.addEventListener('click', function (e) {
|
|
674
|
+
e.stopPropagation();
|
|
675
|
+
setNotifOpen(!notifOpen);
|
|
676
|
+
});
|
|
677
|
+
document.addEventListener('click', function (e) {
|
|
678
|
+
if (notifOpen && !notifPanel.contains(e.target) && e.target !== bell) setNotifOpen(false);
|
|
679
|
+
});
|
|
680
|
+
|
|
681
|
+
function togglePush() {
|
|
380
682
|
// Turning OFF always works regardless of environment.
|
|
381
683
|
if (localStorage.getItem(NOTIFY_KEY) === '1') {
|
|
382
684
|
localStorage.setItem(NOTIFY_KEY, '0'); updateBell(); return;
|
|
@@ -400,7 +702,8 @@ export function clientScript(wsUrl) {
|
|
|
400
702
|
updateBell();
|
|
401
703
|
});
|
|
402
704
|
});
|
|
403
|
-
}
|
|
705
|
+
}
|
|
706
|
+
notifToggle.addEventListener('click', togglePush);
|
|
404
707
|
|
|
405
708
|
// VAPID public key (base64url) -> Uint8Array for applicationServerKey.
|
|
406
709
|
function urlB64ToUint8Array(base64) {
|
|
@@ -453,7 +756,7 @@ export function clientScript(wsUrl) {
|
|
|
453
756
|
promptRec.style.display = 'none';
|
|
454
757
|
activeRecommended2 = '';
|
|
455
758
|
if (cancelArmTimer) { clearTimeout(cancelArmTimer); cancelArmTimer = null; }
|
|
456
|
-
|
|
759
|
+
refreshComposer();
|
|
457
760
|
}
|
|
458
761
|
|
|
459
762
|
function makeBtn(label, cls, onClick) {
|
|
@@ -549,7 +852,7 @@ export function clientScript(wsUrl) {
|
|
|
549
852
|
promptInput.focus();
|
|
550
853
|
}
|
|
551
854
|
promptCard.style.display = 'block';
|
|
552
|
-
|
|
855
|
+
refreshComposer();
|
|
553
856
|
}
|
|
554
857
|
|
|
555
858
|
function handleMsg(msg) {
|
|
@@ -561,6 +864,7 @@ export function clientScript(wsUrl) {
|
|
|
561
864
|
chatLog.innerHTML = '';
|
|
562
865
|
closePrompt();
|
|
563
866
|
hideThinking();
|
|
867
|
+
currentThinking = null; thinkingText = '';
|
|
564
868
|
currentBubble = null; streamText = '';
|
|
565
869
|
for (const k in toolCallMap) delete toolCallMap[k];
|
|
566
870
|
// Per-turn try/catch: one malformed turn must never abort the rebuild
|
|
@@ -568,10 +872,14 @@ export function clientScript(wsUrl) {
|
|
|
568
872
|
for (const t of (msg.turns || [])) { try { renderTurn(t); } catch (e) {} }
|
|
569
873
|
if (msg.live) { try { renderLiveTurn(msg.live); } catch (e) {} }
|
|
570
874
|
taskWidgetLines = (msg.taskWidget && msg.taskWidget.length) ? msg.taskWidget : null;
|
|
875
|
+
taskWidgetData = msg.taskWidgetData || null;
|
|
571
876
|
renderWidgets();
|
|
877
|
+
setModelName(msg.model);
|
|
572
878
|
if (msg.context) setContextBar(msg.context); else contextFill.style.width = '0%';
|
|
879
|
+
agentRunning = !!msg.agentRunning;
|
|
880
|
+
turnHadContent = !!(msg.live && msg.live.parts && msg.live.parts.length);
|
|
573
881
|
if (msg.prompt) showPrompt(msg.prompt);
|
|
574
|
-
|
|
882
|
+
refreshComposer();
|
|
575
883
|
if (msg.agentRunning && !msg.live) showThinking();
|
|
576
884
|
break;
|
|
577
885
|
}
|
|
@@ -579,12 +887,37 @@ export function clientScript(wsUrl) {
|
|
|
579
887
|
autoScroll = true;
|
|
580
888
|
streamText = '';
|
|
581
889
|
currentBubble = null;
|
|
582
|
-
|
|
890
|
+
turnHadContent = false;
|
|
891
|
+
agentRunning = true;
|
|
892
|
+
setModelName(msg.model);
|
|
893
|
+
refreshComposer();
|
|
894
|
+
showThinking();
|
|
895
|
+
break;
|
|
896
|
+
case 'thinking_delta':
|
|
897
|
+
turnHadContent = true;
|
|
898
|
+
hideThinking(); // drop the spinner-only bubble
|
|
899
|
+
if (!currentThinking) {
|
|
900
|
+
currentThinking = makeThinkingEl('', true);
|
|
901
|
+
chatLog.appendChild(currentThinking);
|
|
902
|
+
thinkingText = '';
|
|
903
|
+
startSpin();
|
|
904
|
+
}
|
|
905
|
+
thinkingText += msg.delta;
|
|
906
|
+
currentThinking.querySelector('.thinking-body').textContent = thinkingText;
|
|
907
|
+
currentThinking.querySelector('.thinking-label').textContent =
|
|
908
|
+
thinkingSummary(thinkingLineCount(thinkingText));
|
|
909
|
+
scrollBottom();
|
|
910
|
+
break;
|
|
911
|
+
case 'thinking_end':
|
|
912
|
+
finalizeThinking();
|
|
913
|
+
// Model moves to text/tool next — show the spinner meanwhile.
|
|
583
914
|
showThinking();
|
|
584
915
|
break;
|
|
585
916
|
case 'text_delta':
|
|
917
|
+
turnHadContent = true;
|
|
586
918
|
if (!currentBubble) {
|
|
587
919
|
hideThinking();
|
|
920
|
+
finalizeThinking();
|
|
588
921
|
currentBubble = document.createElement('div');
|
|
589
922
|
currentBubble.className = 'bubble assistant';
|
|
590
923
|
const cursor = document.createElement('span');
|
|
@@ -604,7 +937,7 @@ export function clientScript(wsUrl) {
|
|
|
604
937
|
if (currentBubble) {
|
|
605
938
|
const c = currentBubble.querySelector('.cursor');
|
|
606
939
|
if (c) c.remove();
|
|
607
|
-
if (streamText) setContent(currentBubble, streamText);
|
|
940
|
+
if (streamText) { setContent(currentBubble, streamText); attachBubbleCopy(currentBubble, streamText); }
|
|
608
941
|
// Close this message's bubble so the next text segment (after a tool or
|
|
609
942
|
// the next message) starts a fresh bubble — matching the terminal.
|
|
610
943
|
currentBubble = null;
|
|
@@ -613,7 +946,9 @@ export function clientScript(wsUrl) {
|
|
|
613
946
|
}
|
|
614
947
|
break;
|
|
615
948
|
case 'tool_start': {
|
|
949
|
+
turnHadContent = true;
|
|
616
950
|
hideThinking();
|
|
951
|
+
finalizeThinking();
|
|
617
952
|
const d = addToolCall(msg.toolName, msg.args, false);
|
|
618
953
|
const sp = document.createElement('span');
|
|
619
954
|
sp.className = 'tool-spin spin';
|
|
@@ -642,28 +977,39 @@ export function clientScript(wsUrl) {
|
|
|
642
977
|
}
|
|
643
978
|
case 'user_message':
|
|
644
979
|
addBubble('user', msg.text);
|
|
980
|
+
addTurnTime(Date.now(), 'user');
|
|
645
981
|
break;
|
|
646
982
|
case 'system_note':
|
|
647
983
|
addSystemLine(msg.text);
|
|
984
|
+
addTurnTime(Date.now(), 'system');
|
|
648
985
|
break;
|
|
649
986
|
case 'agent_error':
|
|
650
987
|
hideThinking();
|
|
988
|
+
finalizeThinking();
|
|
651
989
|
if (currentBubble) {
|
|
652
990
|
const c = currentBubble.querySelector('.cursor');
|
|
653
991
|
if (c) c.remove();
|
|
654
|
-
if (streamText) setContent(currentBubble, streamText);
|
|
992
|
+
if (streamText) { setContent(currentBubble, streamText); attachBubbleCopy(currentBubble, streamText); }
|
|
655
993
|
currentBubble = null;
|
|
656
994
|
streamText = '';
|
|
657
995
|
stopSpinIfIdle();
|
|
658
996
|
}
|
|
659
997
|
addBubble('error', msg.message || 'Error');
|
|
660
|
-
|
|
998
|
+
if (turnHadContent) addTurnTime(Date.now(), 'assistant');
|
|
999
|
+
turnHadContent = false;
|
|
1000
|
+
agentRunning = false;
|
|
1001
|
+
refreshComposer();
|
|
661
1002
|
break;
|
|
662
1003
|
case 'agent_end':
|
|
663
1004
|
hideThinking();
|
|
1005
|
+
finalizeThinking();
|
|
664
1006
|
currentBubble = null;
|
|
665
1007
|
streamText = '';
|
|
666
|
-
|
|
1008
|
+
if (turnHadContent) addTurnTime(Date.now(), 'assistant');
|
|
1009
|
+
turnHadContent = false;
|
|
1010
|
+
agentRunning = false;
|
|
1011
|
+
setModelName(msg.model);
|
|
1012
|
+
refreshComposer();
|
|
667
1013
|
setContextBar(msg.contextUsage);
|
|
668
1014
|
break;
|
|
669
1015
|
case 'context':
|
|
@@ -678,6 +1024,7 @@ export function clientScript(wsUrl) {
|
|
|
678
1024
|
break;
|
|
679
1025
|
case 'widget':
|
|
680
1026
|
taskWidgetLines = (msg.lines && msg.lines.length) ? msg.lines : null;
|
|
1027
|
+
taskWidgetData = msg.data || null;
|
|
681
1028
|
renderWidgets();
|
|
682
1029
|
break;
|
|
683
1030
|
case 'notify':
|
|
@@ -691,9 +1038,14 @@ export function clientScript(wsUrl) {
|
|
|
691
1038
|
// A new session started — wipe the previous session's transcript.
|
|
692
1039
|
chatLog.innerHTML = '';
|
|
693
1040
|
hideThinking();
|
|
1041
|
+
finalizeThinking();
|
|
694
1042
|
currentBubble = null; streamText = '';
|
|
1043
|
+
turnHadContent = false;
|
|
695
1044
|
closePrompt();
|
|
1045
|
+
agentRunning = false;
|
|
1046
|
+
refreshComposer();
|
|
696
1047
|
taskWidgetLines = null;
|
|
1048
|
+
taskWidgetData = null;
|
|
697
1049
|
renderWidgets();
|
|
698
1050
|
contextFill.style.width = '0%';
|
|
699
1051
|
break;
|
|
@@ -712,11 +1064,12 @@ export function clientScript(wsUrl) {
|
|
|
712
1064
|
// The server records the message via addUserTurn and broadcasts a
|
|
713
1065
|
// user_message back to every client (us included), which renders the
|
|
714
1066
|
// bubble. Don't render it here too, or the sender sees it twice.
|
|
715
|
-
|
|
716
|
-
|
|
1067
|
+
// Mid-run the message steers the live turn (no state change here); when
|
|
1068
|
+
// idle, optimistically show the spinner until agent_start lands.
|
|
1069
|
+
if (!agentRunning) showThinking();
|
|
717
1070
|
}
|
|
718
1071
|
|
|
719
|
-
sendBtn.addEventListener('click',
|
|
1072
|
+
sendBtn.addEventListener('click', onSendClick);
|
|
720
1073
|
inputEl.addEventListener('keydown', (e) => {
|
|
721
1074
|
if (cmdActive.length > 0) {
|
|
722
1075
|
if (e.key === 'ArrowDown') { e.preventDefault(); cmdIndex = Math.min(cmdIndex + 1, cmdActive.length - 1); renderSuggestions(); return; }
|
|
@@ -746,7 +1099,8 @@ export function clientScript(wsUrl) {
|
|
|
746
1099
|
if (reconnectAnim) { clearInterval(reconnectAnim); reconnectAnim = null; }
|
|
747
1100
|
reconnectOverlay.classList.remove('visible');
|
|
748
1101
|
reconnectDelay = 1000;
|
|
749
|
-
|
|
1102
|
+
connected = true;
|
|
1103
|
+
refreshComposer();
|
|
750
1104
|
// Self-heal on every (re)connect, not just page load: if the server
|
|
751
1105
|
// restarted it may have lost (or be rehydrating) our subscription, and
|
|
752
1106
|
// browsers can rotate it. Re-registering here covers reconnects the
|
|
@@ -760,7 +1114,8 @@ export function clientScript(wsUrl) {
|
|
|
760
1114
|
});
|
|
761
1115
|
sock.addEventListener('close', () => {
|
|
762
1116
|
if (ws !== sock) return;
|
|
763
|
-
|
|
1117
|
+
connected = false;
|
|
1118
|
+
refreshComposer();
|
|
764
1119
|
reconnectOverlay.classList.add('visible');
|
|
765
1120
|
// Animate the same braille spinner used elsewhere, with a live countdown.
|
|
766
1121
|
const until = Date.now() + reconnectDelay;
|