@mjasnikovs/pi-task 0.6.0 → 0.7.1
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/README.md +26 -2
- package/dist/remote/bridge.d.ts +1 -10
- package/dist/remote/bridge.js +6 -19
- package/dist/remote/events.d.ts +3 -2
- package/dist/remote/events.js +28 -50
- package/dist/remote/history.d.ts +18 -5
- package/dist/remote/history.js +11 -4
- package/dist/remote/protocol.d.ts +13 -5
- package/dist/remote/push.d.ts +52 -0
- package/dist/remote/push.js +156 -0
- package/dist/remote/register.js +13 -10
- package/dist/remote/server.d.ts +1 -2
- package/dist/remote/server.js +36 -13
- package/dist/remote/session-state.d.ts +54 -0
- package/dist/remote/session-state.js +179 -0
- package/dist/remote/sw.d.ts +8 -0
- package/dist/remote/sw.js +40 -0
- package/dist/remote/ui.js +246 -60
- package/dist/task/widget.js +7 -7
- package/dist/workers/pi-worker-docs.js +14 -11
- package/dist/workers/pi-worker-search.js +8 -3
- package/dist/workers/pi-worker.js +9 -6
- package/package.json +6 -4
package/dist/remote/ui.js
CHANGED
|
@@ -85,6 +85,13 @@ export function html(wsUrl) {
|
|
|
85
85
|
background: var(--crust); color: var(--red); align-self: stretch;
|
|
86
86
|
max-width: 100%; border: 1px solid var(--red); font-size: 12px;
|
|
87
87
|
}
|
|
88
|
+
/* Persistent inline system note (e.g. context compaction) — a muted centered
|
|
89
|
+
divider, distinct from chat bubbles. */
|
|
90
|
+
.sysnote {
|
|
91
|
+
align-self: center; color: var(--subtext0); font-size: 11px;
|
|
92
|
+
font-family: ui-monospace, monospace; letter-spacing: 0.5px;
|
|
93
|
+
padding: 2px 10px; opacity: 0.85;
|
|
94
|
+
}
|
|
88
95
|
.bubble.thinking {
|
|
89
96
|
display: flex; gap: 5px; align-items: center; padding: 10px 14px;
|
|
90
97
|
}
|
|
@@ -172,11 +179,12 @@ export function html(wsUrl) {
|
|
|
172
179
|
font-size: 13px; z-index: 100; letter-spacing: 0.03em;
|
|
173
180
|
}
|
|
174
181
|
#reconnect-overlay.visible { display: flex; }
|
|
182
|
+
/* Trailing stream indicator: the same braille spinner as the thinking bubble,
|
|
183
|
+
inline at the end of the streaming text (not a green blinking block). */
|
|
175
184
|
.cursor {
|
|
176
|
-
|
|
177
|
-
|
|
185
|
+
color: var(--mauve); margin-left: 2px;
|
|
186
|
+
font-family: ui-monospace, monospace;
|
|
178
187
|
}
|
|
179
|
-
@keyframes blink { 50% { opacity: 0; } }
|
|
180
188
|
#status-panel { padding: 6px 12px; border-bottom: 1px solid var(--surface1);
|
|
181
189
|
color: var(--subtext1); white-space: pre-wrap; font-size: 13px; display: none; }
|
|
182
190
|
#prompt-card { position: fixed; left: 0; right: 0; bottom: 0; background: var(--mantle);
|
|
@@ -208,7 +216,8 @@ export function html(wsUrl) {
|
|
|
208
216
|
font-size: 12px; font-weight: 500; padding: 8px 10px; }
|
|
209
217
|
#prompt-card button.cancel:hover { color: var(--red); filter: none; }
|
|
210
218
|
#prompt-card button.cancel.armed { background: var(--red); color: var(--crust); font-weight: 700; }
|
|
211
|
-
.toast { position: fixed; top:
|
|
219
|
+
.toast { position: fixed; top: calc(env(safe-area-inset-top, 0px) + 12px);
|
|
220
|
+
right: calc(env(safe-area-inset-right, 0px) + 12px); max-width: calc(100vw - 24px);
|
|
212
221
|
padding: 8px 12px; border-radius: 6px; overflow-wrap: anywhere; word-break: break-word;
|
|
213
222
|
background: var(--surface1); color: var(--text); z-index: 60; }
|
|
214
223
|
.toast.warning { background: var(--peach); color: var(--crust); }
|
|
@@ -224,7 +233,7 @@ export function html(wsUrl) {
|
|
|
224
233
|
<div id="header">
|
|
225
234
|
<span class="title">pi-task remote</span>
|
|
226
235
|
<div class="hgroup">
|
|
227
|
-
<span class="status" id="client-status"><span class="cdot" id="conn-dot">○</span
|
|
236
|
+
<span class="status" id="client-status"><span class="cdot" id="conn-dot">○</span></span>
|
|
228
237
|
<button id="bell" aria-label="Toggle notifications" title="Notifications">◯</button>
|
|
229
238
|
</div>
|
|
230
239
|
</div>
|
|
@@ -264,17 +273,29 @@ export function html(wsUrl) {
|
|
|
264
273
|
if (usage && usage.percent != null) contextFill.style.width = usage.percent + '%';
|
|
265
274
|
}
|
|
266
275
|
const connDot = document.getElementById('conn-dot');
|
|
267
|
-
const clientLabel = document.getElementById('client-label');
|
|
268
276
|
// state: 'connecting' (○ yellow) | 'up' (● green) | 'down' (● red)
|
|
269
|
-
function setConn(state
|
|
277
|
+
function setConn(state) {
|
|
270
278
|
connDot.textContent = state === 'connecting' ? '\\u25CB' : '\\u25CF';
|
|
271
279
|
connDot.className = 'cdot' + (state === 'up' ? ' up' : state === 'down' ? ' down' : '');
|
|
272
|
-
if (label !== undefined) clientLabel.textContent = label;
|
|
273
280
|
}
|
|
274
281
|
const reconnectOverlay = document.getElementById('reconnect-overlay');
|
|
275
282
|
const reconnectMsg = document.getElementById('reconnect-msg');
|
|
276
283
|
const cmdSuggestions = document.getElementById('cmd-suggestions');
|
|
277
284
|
const statusPanel = document.getElementById('status-panel');
|
|
285
|
+
// Widgets are keyed (e.g. 'pi-tasks', 'pi-task-auto'); track them per key so a
|
|
286
|
+
// clear for one key can't be masked by a stale message from another.
|
|
287
|
+
// Single authoritative task-widget slot. The snapshot and the live 'widget'
|
|
288
|
+
// delta both set this; null hides the panel. (No more per-key map that could
|
|
289
|
+
// strand an orphaned widget on screen.)
|
|
290
|
+
let taskWidgetLines = null;
|
|
291
|
+
function renderWidgets() {
|
|
292
|
+
if (taskWidgetLines && taskWidgetLines.length) {
|
|
293
|
+
statusPanel.textContent = taskWidgetLines.join('\\n');
|
|
294
|
+
statusPanel.style.display = 'block';
|
|
295
|
+
} else {
|
|
296
|
+
statusPanel.style.display = 'none';
|
|
297
|
+
}
|
|
298
|
+
}
|
|
278
299
|
const promptCard = document.getElementById('prompt-card');
|
|
279
300
|
const promptQ = document.getElementById('prompt-q');
|
|
280
301
|
const promptRec = document.getElementById('prompt-rec');
|
|
@@ -292,6 +313,7 @@ export function html(wsUrl) {
|
|
|
292
313
|
let streamText = '';
|
|
293
314
|
let autoScroll = true;
|
|
294
315
|
let reconnectDelay = 1000;
|
|
316
|
+
let reconnectAnim = null;
|
|
295
317
|
let ws = null;
|
|
296
318
|
|
|
297
319
|
const BT = String.fromCharCode(96);
|
|
@@ -475,30 +497,46 @@ export function html(wsUrl) {
|
|
|
475
497
|
let spinTimer = null;
|
|
476
498
|
let spinIdx = 0;
|
|
477
499
|
const SPIN = '\\u280B\\u2819\\u2839\\u2838\\u283C\\u2834\\u2826\\u2827\\u2807\\u280F';
|
|
500
|
+
// One braille ticker drives every '.spin' element — the thinking bubble AND the
|
|
501
|
+
// trailing stream cursor — so they share the same frame and look identical.
|
|
502
|
+
function spinPaint() {
|
|
503
|
+
const g = SPIN[spinIdx % SPIN.length];
|
|
504
|
+
const els = document.getElementsByClassName('spin');
|
|
505
|
+
for (let i = 0; i < els.length; i++) els[i].textContent = g;
|
|
506
|
+
}
|
|
507
|
+
function startSpin() {
|
|
508
|
+
spinPaint();
|
|
509
|
+
if (spinTimer) return;
|
|
510
|
+
spinTimer = setInterval(function () {
|
|
511
|
+
spinIdx = (spinIdx + 1) % SPIN.length;
|
|
512
|
+
spinPaint();
|
|
513
|
+
}, 90);
|
|
514
|
+
}
|
|
515
|
+
// Stop the ticker once nothing on screen needs spinning.
|
|
516
|
+
function stopSpinIfIdle() {
|
|
517
|
+
if (spinTimer && !document.querySelector('.spin')) {
|
|
518
|
+
clearInterval(spinTimer); spinTimer = null;
|
|
519
|
+
}
|
|
520
|
+
}
|
|
478
521
|
function showThinking() {
|
|
479
522
|
if (!thinkingEl) {
|
|
480
523
|
thinkingEl = document.createElement('div');
|
|
481
524
|
thinkingEl.className = 'bubble assistant thinking';
|
|
482
|
-
thinkingEl.innerHTML = '<span class="spinner"></span>';
|
|
483
|
-
}
|
|
484
|
-
if (!spinTimer) {
|
|
485
|
-
var sp = thinkingEl.firstChild;
|
|
486
|
-
sp.textContent = SPIN[spinIdx % SPIN.length];
|
|
487
|
-
spinTimer = setInterval(function () {
|
|
488
|
-
spinIdx = (spinIdx + 1) % SPIN.length;
|
|
489
|
-
sp.textContent = SPIN[spinIdx];
|
|
490
|
-
}, 90);
|
|
525
|
+
thinkingEl.innerHTML = '<span class="spinner spin"></span>';
|
|
491
526
|
}
|
|
492
527
|
chatLog.appendChild(thinkingEl); // append (or move) to bottom
|
|
528
|
+
startSpin();
|
|
493
529
|
scrollBottom();
|
|
494
530
|
}
|
|
495
531
|
function hideThinking() {
|
|
496
|
-
if (spinTimer) { clearInterval(spinTimer); spinTimer = null; }
|
|
497
532
|
if (thinkingEl) thinkingEl.remove();
|
|
533
|
+
stopSpinIfIdle();
|
|
498
534
|
}
|
|
499
535
|
|
|
500
536
|
function addToolCall(toolName, argsStr, isError) {
|
|
501
|
-
|
|
537
|
+
// argsStr can be undefined (no args / JSON.stringify(undefined)); don't let
|
|
538
|
+
// that render as the literal "name: undefined" in the collapsed summary.
|
|
539
|
+
const label = (toolName + (argsStr ? ': ' + argsStr : '')).slice(0, 64);
|
|
502
540
|
const d = document.createElement('details');
|
|
503
541
|
d.className = 'tool-call' + (isError ? ' error' : '');
|
|
504
542
|
const s = document.createElement('summary');
|
|
@@ -515,6 +553,82 @@ export function html(wsUrl) {
|
|
|
515
553
|
sendBtn.disabled = !allow;
|
|
516
554
|
}
|
|
517
555
|
|
|
556
|
+
// Stringify a tool result safely. A null/undefined result (e.g. a tool that
|
|
557
|
+
// hasn't produced output) must NOT become the JS value undefined, whose
|
|
558
|
+
// .slice() throws — a throw here aborts the whole snapshot rebuild after the
|
|
559
|
+
// log was already cleared, blanking the transcript on reconnect.
|
|
560
|
+
function toolResultText(result) {
|
|
561
|
+
if (result == null) return '';
|
|
562
|
+
const r = typeof result === 'string' ? result : JSON.stringify(result, null, 2);
|
|
563
|
+
return (r == null ? '' : r).slice(0, 8000);
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
// Render one tool part from the ordered parts list (running or finished).
|
|
567
|
+
function renderToolPart(p) {
|
|
568
|
+
const argsStr = typeof p.args === 'string' ? p.args : JSON.stringify(p.args);
|
|
569
|
+
const d = addToolCall(p.toolName, argsStr, p.isError);
|
|
570
|
+
if (p.done) {
|
|
571
|
+
const pre = document.createElement('pre');
|
|
572
|
+
pre.textContent = toolResultText(p.result);
|
|
573
|
+
d.appendChild(pre);
|
|
574
|
+
} else {
|
|
575
|
+
toolCallMap[p.toolCallId] = d; // a later tool_end delta fills the result
|
|
576
|
+
}
|
|
577
|
+
return d;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
// A muted, centered system note (e.g. "Context compacted").
|
|
581
|
+
function addSystemLine(text) {
|
|
582
|
+
const el = document.createElement('div');
|
|
583
|
+
el.className = 'sysnote';
|
|
584
|
+
el.textContent = text;
|
|
585
|
+
chatLog.appendChild(el);
|
|
586
|
+
scrollBottom();
|
|
587
|
+
return el;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
// Render one committed transcript turn. Assistant turns are an ordered list of
|
|
591
|
+
// parts (text segments + tool calls), so the layout matches the terminal's
|
|
592
|
+
// interleaving instead of one merged blob with tools dumped at the end.
|
|
593
|
+
function renderTurn(t) {
|
|
594
|
+
if (t.error) { addBubble('error', t.text); return; }
|
|
595
|
+
if (t.role === 'system') { addSystemLine(t.text); return; }
|
|
596
|
+
if (t.role === 'user') { addBubble('user', t.text); return; }
|
|
597
|
+
for (const p of (t.parts || [])) {
|
|
598
|
+
if (p.kind === 'text') { if (p.text) addBubble('assistant', p.text); }
|
|
599
|
+
else renderToolPart(p);
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
// Render the in-progress assistant turn from a snapshot, preserving order. The
|
|
604
|
+
// trailing OPEN text segment becomes the live streaming bubble (cursor + spin)
|
|
605
|
+
// so subsequent text_delta frames keep flowing into it.
|
|
606
|
+
function renderLiveTurn(live) {
|
|
607
|
+
const parts = live.parts || [];
|
|
608
|
+
for (let i = 0; i < parts.length; i++) {
|
|
609
|
+
const p = parts[i];
|
|
610
|
+
const last = i === parts.length - 1;
|
|
611
|
+
if (p.kind === 'text') {
|
|
612
|
+
if (last && live.textOpen) {
|
|
613
|
+
currentBubble = document.createElement('div');
|
|
614
|
+
currentBubble.className = 'bubble assistant';
|
|
615
|
+
const cursor = document.createElement('span');
|
|
616
|
+
cursor.className = 'cursor spin';
|
|
617
|
+
currentBubble.appendChild(cursor);
|
|
618
|
+
if (p.text) currentBubble.insertBefore(document.createTextNode(p.text), cursor);
|
|
619
|
+
chatLog.appendChild(currentBubble);
|
|
620
|
+
streamText = p.text || '';
|
|
621
|
+
startSpin();
|
|
622
|
+
scrollBottom();
|
|
623
|
+
} else if (p.text) {
|
|
624
|
+
addBubble('assistant', p.text);
|
|
625
|
+
}
|
|
626
|
+
} else {
|
|
627
|
+
renderToolPart(p);
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
|
|
518
632
|
function showToast(message, level) {
|
|
519
633
|
const t = document.createElement('div');
|
|
520
634
|
t.className = 'toast ' + (level || 'info');
|
|
@@ -557,26 +671,65 @@ export function html(wsUrl) {
|
|
|
557
671
|
}
|
|
558
672
|
const issue = notifyEnvIssue();
|
|
559
673
|
if (issue) { showToast(issue, 'warning'); return; }
|
|
674
|
+
if (!('serviceWorker' in navigator) || !('PushManager' in window)) {
|
|
675
|
+
showToast('This browser doesn\\u2019t support push notifications.', 'warning'); return;
|
|
676
|
+
}
|
|
560
677
|
Notification.requestPermission().then(function (perm) {
|
|
561
|
-
if (perm
|
|
562
|
-
|
|
563
|
-
|
|
678
|
+
if (perm !== 'granted') {
|
|
679
|
+
showToast('Notifications blocked in browser settings.', 'warning');
|
|
680
|
+
updateBell(); return;
|
|
681
|
+
}
|
|
682
|
+
subscribePush().then(function (ok) {
|
|
683
|
+
if (ok) { localStorage.setItem(NOTIFY_KEY, '1'); showToast('Notifications on.', 'info'); }
|
|
684
|
+
else { showToast('Could not register for notifications.', 'warning'); }
|
|
685
|
+
updateBell();
|
|
686
|
+
}).catch(function (e) {
|
|
687
|
+
showToast('Notification setup failed: ' + (e && e.message ? e.message : e), 'warning');
|
|
688
|
+
updateBell();
|
|
689
|
+
});
|
|
564
690
|
});
|
|
565
691
|
});
|
|
566
692
|
|
|
567
|
-
//
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
693
|
+
// VAPID public key (base64url) -> Uint8Array for applicationServerKey.
|
|
694
|
+
function urlB64ToUint8Array(base64) {
|
|
695
|
+
const pad = '='.repeat((4 - base64.length % 4) % 4);
|
|
696
|
+
const b64 = (base64 + pad).replace(/-/g, '+').replace(/_/g, '/');
|
|
697
|
+
const raw = atob(b64);
|
|
698
|
+
const arr = new Uint8Array(raw.length);
|
|
699
|
+
for (let i = 0; i < raw.length; i++) arr[i] = raw.charCodeAt(i);
|
|
700
|
+
return arr;
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
// Register the service worker, subscribe via the Push API, and hand the
|
|
704
|
+
// subscription to the server. The server (not the page) sends notifications,
|
|
705
|
+
// so they arrive even when this PWA is backgrounded/suspended on iOS.
|
|
706
|
+
function subscribePush() {
|
|
707
|
+
return navigator.serviceWorker.register('/sw.js')
|
|
708
|
+
.then(function () { return navigator.serviceWorker.ready; })
|
|
709
|
+
.then(function (reg) {
|
|
710
|
+
return fetch('/push-key').then(function (r) { return r.text(); }).then(function (key) {
|
|
711
|
+
return reg.pushManager.getSubscription().then(function (existing) {
|
|
712
|
+
return existing || reg.pushManager.subscribe({
|
|
713
|
+
userVisibleOnly: true,
|
|
714
|
+
applicationServerKey: urlB64ToUint8Array(key.trim())
|
|
715
|
+
});
|
|
716
|
+
});
|
|
717
|
+
});
|
|
718
|
+
})
|
|
719
|
+
.then(function (subscription) {
|
|
720
|
+
return fetch('/subscribe', {
|
|
721
|
+
method: 'POST',
|
|
722
|
+
headers: { 'Content-Type': 'application/json' },
|
|
723
|
+
body: JSON.stringify(subscription)
|
|
724
|
+
}).then(function (res) { return res.ok; });
|
|
725
|
+
});
|
|
577
726
|
}
|
|
578
727
|
|
|
579
728
|
updateBell();
|
|
729
|
+
// Self-heal: if notifications were enabled before, re-register the
|
|
730
|
+
// subscription on load (the server keeps subscriptions in memory and may
|
|
731
|
+
// have restarted, and browsers can rotate the subscription).
|
|
732
|
+
if (notifyEnabled()) { subscribePush().catch(function () {}); }
|
|
580
733
|
|
|
581
734
|
function answer(value) {
|
|
582
735
|
if (activePromptId === null) return;
|
|
@@ -679,19 +832,27 @@ export function html(wsUrl) {
|
|
|
679
832
|
|
|
680
833
|
function handleMsg(msg) {
|
|
681
834
|
switch (msg.type) {
|
|
682
|
-
case '
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
}
|
|
835
|
+
case 'snapshot': {
|
|
836
|
+
// Authoritative full state on every (re)connect: replace the WHOLE view.
|
|
837
|
+
// This is what kills duplicated transcript / stale-orphaned widgets —
|
|
838
|
+
// whatever was on screen is discarded and rebuilt from server truth.
|
|
839
|
+
chatLog.innerHTML = '';
|
|
840
|
+
closePrompt();
|
|
841
|
+
hideThinking();
|
|
842
|
+
currentBubble = null; streamText = '';
|
|
843
|
+
for (const k in toolCallMap) delete toolCallMap[k];
|
|
844
|
+
// Per-turn try/catch: one malformed turn must never abort the rebuild
|
|
845
|
+
// and leave the (already-cleared) transcript blank.
|
|
846
|
+
for (const t of (msg.turns || [])) { try { renderTurn(t); } catch (e) {} }
|
|
847
|
+
if (msg.live) { try { renderLiveTurn(msg.live); } catch (e) {} }
|
|
848
|
+
taskWidgetLines = (msg.taskWidget && msg.taskWidget.length) ? msg.taskWidget : null;
|
|
849
|
+
renderWidgets();
|
|
850
|
+
if (msg.context) setContextBar(msg.context); else contextFill.style.width = '0%';
|
|
851
|
+
if (msg.prompt) showPrompt(msg.prompt);
|
|
852
|
+
setEnabled(!msg.agentRunning && !msg.prompt);
|
|
853
|
+
if (msg.agentRunning && !msg.live) showThinking();
|
|
694
854
|
break;
|
|
855
|
+
}
|
|
695
856
|
case 'agent_start':
|
|
696
857
|
autoScroll = true;
|
|
697
858
|
streamText = '';
|
|
@@ -705,9 +866,10 @@ export function html(wsUrl) {
|
|
|
705
866
|
currentBubble = document.createElement('div');
|
|
706
867
|
currentBubble.className = 'bubble assistant';
|
|
707
868
|
const cursor = document.createElement('span');
|
|
708
|
-
cursor.className = 'cursor';
|
|
869
|
+
cursor.className = 'cursor spin';
|
|
709
870
|
currentBubble.appendChild(cursor);
|
|
710
871
|
chatLog.appendChild(currentBubble);
|
|
872
|
+
startSpin();
|
|
711
873
|
}
|
|
712
874
|
streamText += msg.delta;
|
|
713
875
|
{
|
|
@@ -721,6 +883,11 @@ export function html(wsUrl) {
|
|
|
721
883
|
const c = currentBubble.querySelector('.cursor');
|
|
722
884
|
if (c) c.remove();
|
|
723
885
|
if (streamText) setContent(currentBubble, streamText);
|
|
886
|
+
// Close this message's bubble so the next text segment (after a tool or
|
|
887
|
+
// the next message) starts a fresh bubble — matching the terminal.
|
|
888
|
+
currentBubble = null;
|
|
889
|
+
streamText = '';
|
|
890
|
+
stopSpinIfIdle();
|
|
724
891
|
}
|
|
725
892
|
break;
|
|
726
893
|
case 'tool_start': {
|
|
@@ -734,8 +901,7 @@ export function html(wsUrl) {
|
|
|
734
901
|
if (d) {
|
|
735
902
|
if (msg.isError) d.classList.add('error');
|
|
736
903
|
const pre = document.createElement('pre');
|
|
737
|
-
|
|
738
|
-
pre.textContent = r.slice(0, 8000);
|
|
904
|
+
pre.textContent = toolResultText(msg.result);
|
|
739
905
|
d.appendChild(pre);
|
|
740
906
|
delete toolCallMap[msg.toolCallId];
|
|
741
907
|
}
|
|
@@ -748,6 +914,9 @@ export function html(wsUrl) {
|
|
|
748
914
|
case 'user_message':
|
|
749
915
|
addBubble('user', msg.text);
|
|
750
916
|
break;
|
|
917
|
+
case 'system_note':
|
|
918
|
+
addSystemLine(msg.text);
|
|
919
|
+
break;
|
|
751
920
|
case 'agent_error':
|
|
752
921
|
hideThinking();
|
|
753
922
|
if (currentBubble) {
|
|
@@ -756,9 +925,9 @@ export function html(wsUrl) {
|
|
|
756
925
|
if (streamText) setContent(currentBubble, streamText);
|
|
757
926
|
currentBubble = null;
|
|
758
927
|
streamText = '';
|
|
928
|
+
stopSpinIfIdle();
|
|
759
929
|
}
|
|
760
930
|
addBubble('error', msg.message || 'Error');
|
|
761
|
-
notify('Agent error', msg.message || 'Error', 'pi-error');
|
|
762
931
|
setEnabled(true);
|
|
763
932
|
break;
|
|
764
933
|
case 'agent_end':
|
|
@@ -766,7 +935,6 @@ export function html(wsUrl) {
|
|
|
766
935
|
currentBubble = null;
|
|
767
936
|
streamText = '';
|
|
768
937
|
setEnabled(true);
|
|
769
|
-
notify('Task finished', '', 'pi-end');
|
|
770
938
|
setContextBar(msg.contextUsage);
|
|
771
939
|
break;
|
|
772
940
|
case 'context':
|
|
@@ -774,22 +942,17 @@ export function html(wsUrl) {
|
|
|
774
942
|
setContextBar(msg.contextUsage);
|
|
775
943
|
break;
|
|
776
944
|
case 'client_count':
|
|
777
|
-
setConn('up'
|
|
945
|
+
setConn('up');
|
|
778
946
|
break;
|
|
779
947
|
case 'prompt':
|
|
780
948
|
showPrompt(msg);
|
|
781
|
-
notify('pi needs your input', msg.question, 'pi-prompt');
|
|
782
949
|
break;
|
|
783
950
|
case 'prompt_resolved':
|
|
784
951
|
if (activePromptId === msg.id) closePrompt();
|
|
785
952
|
break;
|
|
786
953
|
case 'widget':
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
statusPanel.style.display = 'block';
|
|
790
|
-
} else {
|
|
791
|
-
statusPanel.style.display = 'none';
|
|
792
|
-
}
|
|
954
|
+
taskWidgetLines = (msg.lines && msg.lines.length) ? msg.lines : null;
|
|
955
|
+
renderWidgets();
|
|
793
956
|
break;
|
|
794
957
|
case 'notify':
|
|
795
958
|
showToast(msg.message, msg.level);
|
|
@@ -798,6 +961,16 @@ export function html(wsUrl) {
|
|
|
798
961
|
viewerBody.textContent = msg.text;
|
|
799
962
|
viewer.style.display = 'block';
|
|
800
963
|
break;
|
|
964
|
+
case 'reset':
|
|
965
|
+
// A new session started — wipe the previous session's transcript.
|
|
966
|
+
chatLog.innerHTML = '';
|
|
967
|
+
hideThinking();
|
|
968
|
+
currentBubble = null; streamText = '';
|
|
969
|
+
closePrompt();
|
|
970
|
+
taskWidgetLines = null;
|
|
971
|
+
renderWidgets();
|
|
972
|
+
contextFill.style.width = '0%';
|
|
973
|
+
break;
|
|
801
974
|
}
|
|
802
975
|
}
|
|
803
976
|
|
|
@@ -837,9 +1010,10 @@ export function html(wsUrl) {
|
|
|
837
1010
|
function connect() {
|
|
838
1011
|
ws = new WebSocket(WS_URL);
|
|
839
1012
|
ws.addEventListener('open', () => {
|
|
1013
|
+
if (reconnectAnim) { clearInterval(reconnectAnim); reconnectAnim = null; }
|
|
840
1014
|
reconnectOverlay.classList.remove('visible');
|
|
841
1015
|
reconnectDelay = 1000;
|
|
842
|
-
setConn('up'
|
|
1016
|
+
setConn('up');
|
|
843
1017
|
setEnabled(true);
|
|
844
1018
|
});
|
|
845
1019
|
ws.addEventListener('message', (e) => {
|
|
@@ -847,9 +1021,21 @@ export function html(wsUrl) {
|
|
|
847
1021
|
});
|
|
848
1022
|
ws.addEventListener('close', () => {
|
|
849
1023
|
setEnabled(false);
|
|
850
|
-
setConn('down'
|
|
1024
|
+
setConn('down');
|
|
851
1025
|
reconnectOverlay.classList.add('visible');
|
|
852
|
-
|
|
1026
|
+
// Animate the same braille spinner used elsewhere, with a live countdown.
|
|
1027
|
+
const until = Date.now() + reconnectDelay;
|
|
1028
|
+
let frame = 0;
|
|
1029
|
+
const paint = () => {
|
|
1030
|
+
const left = Math.max(0, Math.ceil((until - Date.now()) / 1000));
|
|
1031
|
+
const glyph = SPIN[frame++ % SPIN.length];
|
|
1032
|
+
reconnectMsg.textContent = left > 0
|
|
1033
|
+
? glyph + ' connection lost — retrying in ' + left + 's'
|
|
1034
|
+
: glyph + ' reconnecting…';
|
|
1035
|
+
};
|
|
1036
|
+
if (reconnectAnim) clearInterval(reconnectAnim);
|
|
1037
|
+
paint();
|
|
1038
|
+
reconnectAnim = setInterval(paint, 90);
|
|
853
1039
|
setTimeout(() => { reconnectDelay = Math.min(reconnectDelay * 2, 30000); connect(); }, reconnectDelay);
|
|
854
1040
|
});
|
|
855
1041
|
}
|
package/dist/task/widget.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* context usage, and the latest child-process line.
|
|
6
6
|
*/
|
|
7
7
|
import { PHASE_INDEX, PHASE_ORDER } from './task-file.js';
|
|
8
|
-
import {
|
|
8
|
+
import { setTaskWidget } from '../remote/session-state.js';
|
|
9
9
|
// ─── Constants ───────────────────────────────────────────────────────────────
|
|
10
10
|
export const WIDGET_KEY = 'pi-tasks';
|
|
11
11
|
export const AUTO_WIDGET_KEY = 'pi-task-auto';
|
|
@@ -101,7 +101,7 @@ export function startWidget(ctx, getState) {
|
|
|
101
101
|
catch {
|
|
102
102
|
/* stale ctx */
|
|
103
103
|
}
|
|
104
|
-
|
|
104
|
+
setTaskWidget(plain);
|
|
105
105
|
};
|
|
106
106
|
render();
|
|
107
107
|
const timer = setInterval(render, WIDGET_REFRESH_MS);
|
|
@@ -114,7 +114,7 @@ export function startWidget(ctx, getState) {
|
|
|
114
114
|
catch {
|
|
115
115
|
/* stale ctx */
|
|
116
116
|
}
|
|
117
|
-
|
|
117
|
+
setTaskWidget(undefined);
|
|
118
118
|
};
|
|
119
119
|
}
|
|
120
120
|
export function buildAutoLoaderLines(s, theme) {
|
|
@@ -150,7 +150,7 @@ export function startAutoLoader(ctx, getState) {
|
|
|
150
150
|
catch {
|
|
151
151
|
/* stale ctx */
|
|
152
152
|
}
|
|
153
|
-
|
|
153
|
+
setTaskWidget(plain);
|
|
154
154
|
};
|
|
155
155
|
render();
|
|
156
156
|
const timer = setInterval(render, WIDGET_REFRESH_MS);
|
|
@@ -163,7 +163,7 @@ export function startAutoLoader(ctx, getState) {
|
|
|
163
163
|
catch {
|
|
164
164
|
/* stale ctx */
|
|
165
165
|
}
|
|
166
|
-
|
|
166
|
+
setTaskWidget(undefined);
|
|
167
167
|
};
|
|
168
168
|
}
|
|
169
169
|
export function flashTerminalWidget(ctx, state, taskId, reason) {
|
|
@@ -185,7 +185,7 @@ export function flashTerminalWidget(ctx, state, taskId, reason) {
|
|
|
185
185
|
: `✘ ${taskId} failed${reason ? ': ' + reason : ''}`;
|
|
186
186
|
try {
|
|
187
187
|
ctx.ui.setWidget(WIDGET_KEY, [line]);
|
|
188
|
-
|
|
188
|
+
setTaskWidget([plainLine]);
|
|
189
189
|
}
|
|
190
190
|
catch {
|
|
191
191
|
/* stale ctx */
|
|
@@ -193,7 +193,7 @@ export function flashTerminalWidget(ctx, state, taskId, reason) {
|
|
|
193
193
|
setTimeout(() => {
|
|
194
194
|
try {
|
|
195
195
|
ctx.ui.setWidget(WIDGET_KEY, undefined);
|
|
196
|
-
|
|
196
|
+
setTaskWidget(undefined);
|
|
197
197
|
}
|
|
198
198
|
catch {
|
|
199
199
|
/* stale ctx */
|
|
@@ -20,17 +20,20 @@ export function registerPiWorkerDocs(pi, internals = {}) {
|
|
|
20
20
|
pi.registerTool({
|
|
21
21
|
name: 'pi-worker-docs',
|
|
22
22
|
label: 'Pi Worker Docs',
|
|
23
|
-
description: 'Look up an npm package
|
|
24
|
-
+ 'answer
|
|
25
|
-
+ 'live npm registry call
|
|
26
|
-
+ '
|
|
27
|
-
+ '
|
|
28
|
-
+ '
|
|
29
|
-
+ '
|
|
30
|
-
+ '
|
|
31
|
-
+ '
|
|
32
|
-
+ '
|
|
33
|
-
+ '
|
|
23
|
+
description: 'Look up an INSTALLED npm package and return a focused, version-pinned '
|
|
24
|
+
+ 'answer from its .d.ts types and README, PLUS the latest published version '
|
|
25
|
+
+ 'from a live npm registry call. USE THIS BEFORE ANSWERING any question '
|
|
26
|
+
+ 'about how to use a library, what it exports, its types/overloads/config, '
|
|
27
|
+
+ 'or the latest published version of an npm package. Do NOT answer package '
|
|
28
|
+
+ 'APIs from memory, do NOT run `npm view`/bash to get a package version, and '
|
|
29
|
+
+ 'do NOT web-search for an installed package — this tool is the source of '
|
|
30
|
+
+ 'truth and is version-pinned to what is actually installed (training-data '
|
|
31
|
+
+ 'versions and APIs are typically months stale).\n'
|
|
32
|
+
+ 'For a non-package framework/runtime version (e.g. Node.js, Ubuntu), use '
|
|
33
|
+
+ '`pi-worker-search` instead. If the package is not installed it is '
|
|
34
|
+
+ 'auto-installed via bun add or npm install. The cache lives at '
|
|
35
|
+
+ '~/.cache/pi-worker/docs.sqlite, keyed by exact installed version; the '
|
|
36
|
+
+ 'registry lookup is best-effort and silently absent when offline.\n'
|
|
34
37
|
+ '\n'
|
|
35
38
|
+ 'Good fits:\n'
|
|
36
39
|
+ '- "What does library X export?" / "How does function Y work?"\n'
|
|
@@ -14,9 +14,14 @@ export function registerPiWorkerSearch(pi, internals = {}) {
|
|
|
14
14
|
pi.registerTool({
|
|
15
15
|
name: 'pi-worker-search',
|
|
16
16
|
label: 'Pi Worker Search',
|
|
17
|
-
description: 'Search the web via Brave Search.
|
|
18
|
-
+ '
|
|
19
|
-
+ '
|
|
17
|
+
description: 'Search the live web via Brave Search. CALL THIS BEFORE ANSWERING any '
|
|
18
|
+
+ 'question about current or version-specific external facts: '
|
|
19
|
+
+ 'library/framework versions and their APIs, latest releases, recently '
|
|
20
|
+
+ 'shipped features, current events, prices, or who currently holds a '
|
|
21
|
+
+ 'role. Your built-in knowledge is out of date — do NOT answer such '
|
|
22
|
+
+ 'questions from memory and do NOT shell out with bash to guess. Returns '
|
|
23
|
+
+ 'a compact markdown list of up to 10 results (title, URL, snippet); then '
|
|
24
|
+
+ 'call `pi-worker-fetch` on the URL you want to read. '
|
|
20
25
|
+ 'Requires BRAVE_SEARCH_API_KEY env var.',
|
|
21
26
|
parameters: Params,
|
|
22
27
|
executionMode: 'parallel',
|
|
@@ -18,16 +18,19 @@ export function registerPiWorker(pi) {
|
|
|
18
18
|
pi.registerTool({
|
|
19
19
|
name: 'pi-worker',
|
|
20
20
|
label: 'Pi Worker',
|
|
21
|
-
description: 'Dispatch an isolated child Pi to investigate
|
|
22
|
-
+ '
|
|
23
|
-
+ '
|
|
24
|
-
+ 'you
|
|
21
|
+
description: 'Dispatch an isolated child Pi to investigate and return its CONCLUSION — '
|
|
22
|
+
+ 'not the raw evidence. USE THIS FIRST, instead of running your own '
|
|
23
|
+
+ 'ls/grep/find/read, whenever a question spans MULTIPLE files or means '
|
|
24
|
+
+ 'searching/scanning code you have not already located. Doing it yourself '
|
|
25
|
+
+ 'floods your context with raw file output; the worker reads in isolation '
|
|
26
|
+
+ 'and returns only the answer. You can dispatch several in one turn for '
|
|
27
|
+
+ 'independent questions.\n'
|
|
25
28
|
+ '\n'
|
|
26
29
|
+ 'Good fits:\n'
|
|
27
30
|
+ '- "Where/how is X handled in this repo?" across unfamiliar code\n'
|
|
28
31
|
+ '- Audits and pattern scans across many files ("every place we log PII")\n'
|
|
29
|
-
+ '-
|
|
30
|
-
+ '-
|
|
32
|
+
+ '- Tracing a flow across layers (router → service → database)\n'
|
|
33
|
+
+ '- Summarising long test output, logs, or shell output you do not need verbatim\n'
|
|
31
34
|
+ '\n'
|
|
32
35
|
+ 'Skip when:\n'
|
|
33
36
|
+ '- You already know the exact file — call `read` directly\n'
|