@mjasnikovs/pi-task 0.2.3 → 0.4.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/README.md +13 -0
- package/dist/index.js +2 -0
- package/dist/remote/bridge.d.ts +78 -0
- package/dist/remote/bridge.js +184 -0
- package/dist/remote/broadcast.d.ts +6 -0
- package/dist/remote/broadcast.js +27 -0
- package/dist/remote/events.d.ts +3 -0
- package/dist/remote/events.js +81 -0
- package/dist/remote/history.d.ts +22 -0
- package/dist/remote/history.js +25 -0
- package/dist/remote/protocol.d.ts +42 -0
- package/dist/remote/protocol.js +13 -0
- package/dist/remote/qr.d.ts +1 -0
- package/dist/remote/qr.js +5 -0
- package/dist/remote/register.d.ts +2 -0
- package/dist/remote/register.js +149 -0
- package/dist/remote/server.d.ts +31 -0
- package/dist/remote/server.js +126 -0
- package/dist/remote/state.d.ts +2 -0
- package/dist/remote/state.js +7 -0
- package/dist/remote/ui.d.ts +1 -0
- package/dist/remote/ui.js +660 -0
- package/dist/task/auto-orchestrator.js +11 -4
- package/dist/task/orchestrator.js +10 -4
- package/dist/task/phases.js +9 -2
- package/dist/task/widget.js +15 -2
- package/package.json +16 -10
|
@@ -0,0 +1,660 @@
|
|
|
1
|
+
export function html(wsUrl) {
|
|
2
|
+
const iconSvg = encodeURIComponent(`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 180 180">`
|
|
3
|
+
+ `<rect width="180" height="180" rx="38" fill="#1e1e2e"/>`
|
|
4
|
+
+ `<text x="90" y="130" font-family="Georgia,serif" font-size="100" `
|
|
5
|
+
+ `text-anchor="middle" fill="#cba6f7">π</text></svg>`);
|
|
6
|
+
const iconUrl = `data:image/svg+xml,${iconSvg}`;
|
|
7
|
+
const manifest = encodeURIComponent(JSON.stringify({
|
|
8
|
+
name: 'pi remote',
|
|
9
|
+
short_name: 'pi remote',
|
|
10
|
+
display: 'standalone',
|
|
11
|
+
background_color: '#1e1e2e',
|
|
12
|
+
theme_color: '#1e1e2e',
|
|
13
|
+
icons: [{ src: iconUrl, sizes: 'any', type: 'image/svg+xml' }]
|
|
14
|
+
}));
|
|
15
|
+
return `<!DOCTYPE html>
|
|
16
|
+
<html lang="en">
|
|
17
|
+
<head>
|
|
18
|
+
<meta charset="UTF-8">
|
|
19
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
|
|
20
|
+
<meta name="apple-mobile-web-app-capable" content="yes">
|
|
21
|
+
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
|
22
|
+
<meta name="apple-mobile-web-app-title" content="pi remote">
|
|
23
|
+
<meta name="theme-color" content="#1e1e2e">
|
|
24
|
+
<link rel="apple-touch-icon" href="${iconUrl}">
|
|
25
|
+
<link rel="manifest" href="data:application/manifest+json,${manifest}">
|
|
26
|
+
<title>pi remote</title>
|
|
27
|
+
<style>
|
|
28
|
+
:root {
|
|
29
|
+
--base: #1e1e2e; --mantle: #181825; --crust: #11111b;
|
|
30
|
+
--surface0: #313244; --surface1: #45475a; --surface2: #585b70;
|
|
31
|
+
--text: #cdd6f4; --subtext1: #a6adc8; --subtext0: #7f849c;
|
|
32
|
+
--mauve: #cba6f7; --blue: #89b4fa; --green: #a6e3a1; --red: #f38ba8;
|
|
33
|
+
--yellow: #f9e2af; --peach: #fab387; --teal: #94e2d5;
|
|
34
|
+
}
|
|
35
|
+
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
36
|
+
body {
|
|
37
|
+
background: var(--base); color: var(--text);
|
|
38
|
+
font-family: ui-monospace, monospace; height: 100dvh;
|
|
39
|
+
display: flex; flex-direction: column; overflow: hidden;
|
|
40
|
+
padding: env(safe-area-inset-top, 0px) env(safe-area-inset-right, 0px)
|
|
41
|
+
env(safe-area-inset-bottom, 0px) env(safe-area-inset-left, 0px);
|
|
42
|
+
}
|
|
43
|
+
#context-bar { height: 4px; background: var(--surface0); flex-shrink: 0; }
|
|
44
|
+
#context-bar-fill { height: 100%; background: var(--mauve); width: 0%; transition: width 0.4s ease; }
|
|
45
|
+
#header {
|
|
46
|
+
background: var(--mantle); padding: 8px 16px;
|
|
47
|
+
display: flex; justify-content: space-between; align-items: center;
|
|
48
|
+
font-size: 13px; flex-shrink: 0; border-bottom: 1px solid var(--surface0);
|
|
49
|
+
}
|
|
50
|
+
#header .title { font-weight: bold; color: var(--mauve); letter-spacing: 0.05em; }
|
|
51
|
+
#header .status { color: var(--subtext0); font-size: 11px; }
|
|
52
|
+
#chat-log {
|
|
53
|
+
flex: 1; overflow-y: auto; padding: 16px;
|
|
54
|
+
display: flex; flex-direction: column; gap: 8px;
|
|
55
|
+
}
|
|
56
|
+
#chat-log::-webkit-scrollbar { width: 6px; }
|
|
57
|
+
#chat-log::-webkit-scrollbar-track { background: transparent; }
|
|
58
|
+
#chat-log::-webkit-scrollbar-thumb { background: var(--surface2); border-radius: 3px; }
|
|
59
|
+
.bubble {
|
|
60
|
+
max-width: 82%; padding: 8px 12px; border-radius: 8px;
|
|
61
|
+
line-height: 1.6; white-space: pre-wrap; word-break: break-word; font-size: 13px;
|
|
62
|
+
}
|
|
63
|
+
.bubble.user { background: var(--surface1); color: var(--text); align-self: flex-end; }
|
|
64
|
+
.bubble.assistant { background: var(--surface0); color: var(--text); align-self: flex-start; }
|
|
65
|
+
.bubble.error {
|
|
66
|
+
background: var(--crust); color: var(--red); align-self: stretch;
|
|
67
|
+
max-width: 100%; border: 1px solid var(--red); font-size: 12px;
|
|
68
|
+
}
|
|
69
|
+
.bubble.thinking {
|
|
70
|
+
display: flex; gap: 5px; align-items: center; padding: 12px 14px;
|
|
71
|
+
}
|
|
72
|
+
.bubble.thinking .dot {
|
|
73
|
+
width: 7px; height: 7px; border-radius: 50%; background: var(--subtext0);
|
|
74
|
+
animation: thinking-bounce 1.2s infinite ease-in-out both;
|
|
75
|
+
}
|
|
76
|
+
.bubble.thinking .dot:nth-child(1) { animation-delay: -0.24s; }
|
|
77
|
+
.bubble.thinking .dot:nth-child(2) { animation-delay: -0.12s; }
|
|
78
|
+
@keyframes thinking-bounce {
|
|
79
|
+
0%, 80%, 100% { transform: scale(0.6); opacity: 0.4; }
|
|
80
|
+
40% { transform: scale(1); opacity: 1; }
|
|
81
|
+
}
|
|
82
|
+
.tool-call {
|
|
83
|
+
background: var(--crust); border-radius: 6px; align-self: flex-start;
|
|
84
|
+
max-width: 90%; font-size: 12px; border: 1px solid var(--surface0);
|
|
85
|
+
}
|
|
86
|
+
.tool-call summary {
|
|
87
|
+
padding: 6px 10px; color: var(--subtext1); cursor: pointer;
|
|
88
|
+
user-select: none; list-style: none;
|
|
89
|
+
}
|
|
90
|
+
.tool-call summary::-webkit-details-marker { display: none; }
|
|
91
|
+
.tool-call summary::before { content: "▶ "; }
|
|
92
|
+
.tool-call[open] > summary::before { content: "▼ "; }
|
|
93
|
+
.tool-call.error > summary { color: var(--red); }
|
|
94
|
+
.tool-call pre {
|
|
95
|
+
padding: 8px 12px; overflow-x: auto; overflow-y: auto;
|
|
96
|
+
color: var(--subtext1); font-size: 11px; max-height: 280px;
|
|
97
|
+
border-top: 1px solid var(--surface0);
|
|
98
|
+
}
|
|
99
|
+
.code-block {
|
|
100
|
+
background: var(--crust); border: 1px solid var(--surface0);
|
|
101
|
+
border-radius: 6px; overflow: hidden; margin: 4px 0;
|
|
102
|
+
align-self: stretch; max-width: 100%; font-size: 12px;
|
|
103
|
+
}
|
|
104
|
+
.code-lang {
|
|
105
|
+
background: var(--surface0); color: var(--subtext0);
|
|
106
|
+
font-size: 10px; padding: 3px 10px; letter-spacing: 0.05em;
|
|
107
|
+
}
|
|
108
|
+
.code-block code {
|
|
109
|
+
display: block; padding: 10px 12px; overflow-x: auto;
|
|
110
|
+
color: var(--text); white-space: pre; line-height: 1.55;
|
|
111
|
+
}
|
|
112
|
+
.hl-kw { color: var(--mauve); }
|
|
113
|
+
.hl-str { color: var(--green); }
|
|
114
|
+
.hl-cmt { color: var(--subtext0); font-style: italic; }
|
|
115
|
+
.hl-num { color: var(--blue); }
|
|
116
|
+
.hl-fn { color: var(--yellow); }
|
|
117
|
+
#input-bar {
|
|
118
|
+
background: var(--mantle); padding: 10px 16px;
|
|
119
|
+
display: flex; gap: 8px; flex-shrink: 0;
|
|
120
|
+
border-top: 1px solid var(--surface0);
|
|
121
|
+
position: relative;
|
|
122
|
+
}
|
|
123
|
+
#cmd-suggestions {
|
|
124
|
+
display: none; position: absolute; bottom: 100%; left: 16px; right: 16px;
|
|
125
|
+
background: var(--mantle); border: 1px solid var(--surface1);
|
|
126
|
+
border-bottom: none; border-radius: 8px 8px 0 0;
|
|
127
|
+
overflow: hidden; z-index: 10;
|
|
128
|
+
}
|
|
129
|
+
.cmd-item {
|
|
130
|
+
display: flex; align-items: baseline; gap: 10px;
|
|
131
|
+
padding: 7px 12px; cursor: pointer; font-size: 12px;
|
|
132
|
+
border-bottom: 1px solid var(--surface0);
|
|
133
|
+
}
|
|
134
|
+
.cmd-item:last-child { border-bottom: none; }
|
|
135
|
+
.cmd-item:hover, .cmd-item.active { background: var(--surface0); }
|
|
136
|
+
.cmd-item .cmd-name { color: var(--blue); font-weight: bold; flex-shrink: 0; }
|
|
137
|
+
.cmd-item .cmd-desc { color: var(--subtext0); }
|
|
138
|
+
#input {
|
|
139
|
+
flex: 1; background: var(--surface0); color: var(--text);
|
|
140
|
+
border: none; border-radius: 6px; padding: 8px 12px;
|
|
141
|
+
font-family: inherit; font-size: 13px; resize: none;
|
|
142
|
+
outline: none; line-height: 1.5; min-height: 36px; max-height: 120px;
|
|
143
|
+
}
|
|
144
|
+
#input::placeholder { color: var(--subtext0); }
|
|
145
|
+
#input:focus { box-shadow: 0 0 0 1px var(--mauve); }
|
|
146
|
+
#send {
|
|
147
|
+
background: var(--blue); color: var(--crust); border: none;
|
|
148
|
+
border-radius: 6px; padding: 8px 16px; font-weight: bold;
|
|
149
|
+
cursor: pointer; font-size: 13px; font-family: inherit;
|
|
150
|
+
white-space: nowrap; align-self: flex-end;
|
|
151
|
+
}
|
|
152
|
+
#send:disabled, #input:disabled { opacity: 0.45; cursor: not-allowed; }
|
|
153
|
+
#reconnect-overlay {
|
|
154
|
+
display: none; position: fixed; inset: 0;
|
|
155
|
+
background: rgba(30,30,46,0.88); color: var(--subtext1);
|
|
156
|
+
justify-content: center; align-items: center;
|
|
157
|
+
font-size: 13px; z-index: 100; letter-spacing: 0.03em;
|
|
158
|
+
}
|
|
159
|
+
#reconnect-overlay.visible { display: flex; }
|
|
160
|
+
.cursor {
|
|
161
|
+
display: inline-block; width: 7px; height: 1em; vertical-align: text-bottom;
|
|
162
|
+
background: var(--green); animation: blink 1s step-end infinite; margin-left: 1px;
|
|
163
|
+
}
|
|
164
|
+
@keyframes blink { 50% { opacity: 0; } }
|
|
165
|
+
#status-panel { padding: 6px 12px; border-bottom: 1px solid var(--surface1);
|
|
166
|
+
color: var(--subtext1); white-space: pre-wrap; font-size: 13px; display: none; }
|
|
167
|
+
#prompt-card { position: fixed; left: 0; right: 0; bottom: 0; background: var(--mantle);
|
|
168
|
+
border-top: 2px solid var(--mauve); padding: 14px; display: none; z-index: 50; }
|
|
169
|
+
#prompt-card .q { color: var(--text); margin-bottom: 8px; white-space: pre-wrap; }
|
|
170
|
+
#prompt-card textarea { width: 100%; background: var(--surface0); color: var(--text);
|
|
171
|
+
border: 1px solid var(--surface2); border-radius: 6px; padding: 8px; }
|
|
172
|
+
#prompt-card .row { display: flex; gap: 8px; margin-top: 8px; }
|
|
173
|
+
#prompt-card button { padding: 6px 12px; border-radius: 6px; border: none; cursor: pointer; }
|
|
174
|
+
.toast { position: fixed; top: 12px; right: 12px; padding: 8px 12px; border-radius: 6px;
|
|
175
|
+
background: var(--surface1); color: var(--text); z-index: 60; }
|
|
176
|
+
.toast.warning { background: var(--peach); color: var(--crust); }
|
|
177
|
+
.toast.error { background: var(--red); color: var(--crust); }
|
|
178
|
+
#viewer { position: fixed; inset: 24px; background: var(--mantle); border: 1px solid var(--surface2);
|
|
179
|
+
border-radius: 8px; padding: 16px; overflow: auto; white-space: pre-wrap; display: none; z-index: 70; }
|
|
180
|
+
#viewer .close { position: absolute; top: 8px; right: 12px; cursor: pointer; color: var(--subtext0); }
|
|
181
|
+
</style>
|
|
182
|
+
</head>
|
|
183
|
+
<body>
|
|
184
|
+
<div id="context-bar"><div id="context-bar-fill"></div></div>
|
|
185
|
+
<div id="header">
|
|
186
|
+
<span class="title">pi remote</span>
|
|
187
|
+
<span class="status" id="client-status">connecting…</span>
|
|
188
|
+
</div>
|
|
189
|
+
<div id="chat-log"></div>
|
|
190
|
+
<div id="status-panel"></div>
|
|
191
|
+
<div id="input-bar">
|
|
192
|
+
<div id="cmd-suggestions"></div>
|
|
193
|
+
<textarea id="input" placeholder="type a message… (/ for commands)" rows="1" disabled></textarea>
|
|
194
|
+
<button id="send" disabled>Send</button>
|
|
195
|
+
</div>
|
|
196
|
+
<div id="reconnect-overlay"><span id="reconnect-msg">reconnecting…</span></div>
|
|
197
|
+
<div id="prompt-card">
|
|
198
|
+
<div class="q" id="prompt-q"></div>
|
|
199
|
+
<textarea id="prompt-input" rows="2"></textarea>
|
|
200
|
+
<div class="row" id="prompt-buttons"></div>
|
|
201
|
+
</div>
|
|
202
|
+
<div id="viewer"><span class="close" id="viewer-close">✕</span><div id="viewer-body"></div></div>
|
|
203
|
+
<script>
|
|
204
|
+
// Connect the WebSocket back to whatever host served this page (LAN or
|
|
205
|
+
// Tailscale), not a server-baked IP — otherwise opening the LAN URL on a
|
|
206
|
+
// non-Tailscale device tries to reach the Tailscale IP and hangs. Fall back
|
|
207
|
+
// to the server-provided URL only if location is somehow unavailable.
|
|
208
|
+
const FALLBACK_WS_URL = ${JSON.stringify(wsUrl)};
|
|
209
|
+
const WS_URL = (location && location.host)
|
|
210
|
+
? (location.protocol === 'https:' ? 'wss://' : 'ws://') + location.host + '/ws'
|
|
211
|
+
: FALLBACK_WS_URL;
|
|
212
|
+
const chatLog = document.getElementById('chat-log');
|
|
213
|
+
const inputEl = document.getElementById('input');
|
|
214
|
+
const sendBtn = document.getElementById('send');
|
|
215
|
+
const contextFill = document.getElementById('context-bar-fill');
|
|
216
|
+
const clientStatus = document.getElementById('client-status');
|
|
217
|
+
const reconnectOverlay = document.getElementById('reconnect-overlay');
|
|
218
|
+
const reconnectMsg = document.getElementById('reconnect-msg');
|
|
219
|
+
const cmdSuggestions = document.getElementById('cmd-suggestions');
|
|
220
|
+
const statusPanel = document.getElementById('status-panel');
|
|
221
|
+
const promptCard = document.getElementById('prompt-card');
|
|
222
|
+
const promptQ = document.getElementById('prompt-q');
|
|
223
|
+
const promptInput = document.getElementById('prompt-input');
|
|
224
|
+
const promptButtons = document.getElementById('prompt-buttons');
|
|
225
|
+
const viewer = document.getElementById('viewer');
|
|
226
|
+
const viewerBody = document.getElementById('viewer-body');
|
|
227
|
+
document.getElementById('viewer-close').onclick = function () { viewer.style.display = 'none'; };
|
|
228
|
+
let activePromptId = null;
|
|
229
|
+
const toolCallMap = {};
|
|
230
|
+
let currentBubble = null;
|
|
231
|
+
let streamText = '';
|
|
232
|
+
let autoScroll = true;
|
|
233
|
+
let reconnectDelay = 1000;
|
|
234
|
+
let ws = null;
|
|
235
|
+
|
|
236
|
+
const BT = String.fromCharCode(96);
|
|
237
|
+
const JS_LANGS = new Set(['js','jsx','mjs','cjs','javascript','ts','tsx','typescript']);
|
|
238
|
+
const JS_KW = new Set(['break','case','catch','class','const','continue','debugger',
|
|
239
|
+
'default','delete','do','else','export','extends','finally','for','from','function',
|
|
240
|
+
'if','import','in','instanceof','let','new','of','return','static','super','switch',
|
|
241
|
+
'this','throw','try','typeof','var','void','while','with','yield','async','await',
|
|
242
|
+
'type','interface','enum','implements','abstract','as','declare','namespace',
|
|
243
|
+
'readonly','undefined','null','true','false','override','satisfies']);
|
|
244
|
+
|
|
245
|
+
function escHtml(s) {
|
|
246
|
+
return s.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function syntaxHighlight(code, lang) {
|
|
250
|
+
if (!JS_LANGS.has((lang || '').toLowerCase())) return escHtml(code);
|
|
251
|
+
let r = '', i = 0;
|
|
252
|
+
while (i < code.length) {
|
|
253
|
+
const ch = code[i];
|
|
254
|
+
// Template literal
|
|
255
|
+
if (ch === BT) {
|
|
256
|
+
let j = i + 1;
|
|
257
|
+
while (j < code.length) {
|
|
258
|
+
if (code[j] === '\\\\') { j += 2; continue; }
|
|
259
|
+
if (code[j] === BT) { j++; break; }
|
|
260
|
+
j++;
|
|
261
|
+
}
|
|
262
|
+
r += '<span class="hl-str">' + escHtml(code.slice(i, j)) + '</span>';
|
|
263
|
+
i = j; continue;
|
|
264
|
+
}
|
|
265
|
+
// Single / double quoted string
|
|
266
|
+
if (ch === '"' || ch === "'") {
|
|
267
|
+
let j = i + 1;
|
|
268
|
+
while (j < code.length) {
|
|
269
|
+
if (code[j] === '\\\\') { j += 2; continue; }
|
|
270
|
+
if (code[j] === ch || code[j] === '\\n') break;
|
|
271
|
+
j++;
|
|
272
|
+
}
|
|
273
|
+
if (code[j] === ch) j++;
|
|
274
|
+
r += '<span class="hl-str">' + escHtml(code.slice(i, j)) + '</span>';
|
|
275
|
+
i = j; continue;
|
|
276
|
+
}
|
|
277
|
+
// Line comment
|
|
278
|
+
if (ch === '/' && code[i + 1] === '/') {
|
|
279
|
+
let j = i + 2;
|
|
280
|
+
while (j < code.length && code[j] !== '\\n') j++;
|
|
281
|
+
r += '<span class="hl-cmt">' + escHtml(code.slice(i, j)) + '</span>';
|
|
282
|
+
i = j; continue;
|
|
283
|
+
}
|
|
284
|
+
// Block comment
|
|
285
|
+
if (ch === '/' && code[i + 1] === '*') {
|
|
286
|
+
let j = i + 2;
|
|
287
|
+
while (j < code.length && !(code[j] === '*' && code[j + 1] === '/')) j++;
|
|
288
|
+
j += 2;
|
|
289
|
+
r += '<span class="hl-cmt">' + escHtml(code.slice(i, j)) + '</span>';
|
|
290
|
+
i = j; continue;
|
|
291
|
+
}
|
|
292
|
+
// Number
|
|
293
|
+
if (ch >= '0' && ch <= '9') {
|
|
294
|
+
let j = i;
|
|
295
|
+
if (code[i] === '0' && /[xXoObB]/.test(code[i + 1] || '')) {
|
|
296
|
+
j += 2; while (j < code.length && /[0-9a-fA-F_]/.test(code[j])) j++;
|
|
297
|
+
} else {
|
|
298
|
+
while (j < code.length && (code[j] >= '0' && code[j] <= '9' || code[j] === '_')) j++;
|
|
299
|
+
if (code[j] === '.') { j++; while (j < code.length && code[j] >= '0' && code[j] <= '9') j++; }
|
|
300
|
+
if (code[j] === 'e' || code[j] === 'E') {
|
|
301
|
+
j++; if (code[j] === '+' || code[j] === '-') j++;
|
|
302
|
+
while (j < code.length && code[j] >= '0' && code[j] <= '9') j++;
|
|
303
|
+
}
|
|
304
|
+
if (code[j] === 'n') j++;
|
|
305
|
+
}
|
|
306
|
+
r += '<span class="hl-num">' + escHtml(code.slice(i, j)) + '</span>';
|
|
307
|
+
i = j; continue;
|
|
308
|
+
}
|
|
309
|
+
// Identifier / keyword / function call
|
|
310
|
+
if (/[a-zA-Z_$]/.test(ch)) {
|
|
311
|
+
let j = i;
|
|
312
|
+
while (j < code.length && /[a-zA-Z0-9_$]/.test(code[j])) j++;
|
|
313
|
+
const word = code.slice(i, j);
|
|
314
|
+
if (JS_KW.has(word)) {
|
|
315
|
+
r += '<span class="hl-kw">' + word + '</span>';
|
|
316
|
+
} else if (code[j] === '(') {
|
|
317
|
+
r += '<span class="hl-fn">' + escHtml(word) + '</span>';
|
|
318
|
+
} else {
|
|
319
|
+
r += escHtml(word);
|
|
320
|
+
}
|
|
321
|
+
i = j; continue;
|
|
322
|
+
}
|
|
323
|
+
r += escHtml(ch); i++;
|
|
324
|
+
}
|
|
325
|
+
return r;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function setContent(el, text) {
|
|
329
|
+
el.innerHTML = '';
|
|
330
|
+
const BT3 = BT + BT + BT;
|
|
331
|
+
const re = new RegExp(BT3 + '([^\\n' + BT + ']*)\\n([\\s\\S]*?)' + BT3, 'g');
|
|
332
|
+
let last = 0, m;
|
|
333
|
+
while ((m = re.exec(text)) !== null) {
|
|
334
|
+
if (m.index > last) el.appendChild(document.createTextNode(text.slice(last, m.index)));
|
|
335
|
+
const lang = m[1].trim();
|
|
336
|
+
const code = m[2];
|
|
337
|
+
const wrap = document.createElement('div');
|
|
338
|
+
wrap.className = 'code-block';
|
|
339
|
+
if (lang) { const lb = document.createElement('div'); lb.className = 'code-lang'; lb.textContent = lang; wrap.appendChild(lb); }
|
|
340
|
+
const pre = document.createElement('pre');
|
|
341
|
+
const codeEl = document.createElement('code');
|
|
342
|
+
codeEl.innerHTML = syntaxHighlight(code, lang);
|
|
343
|
+
pre.appendChild(codeEl);
|
|
344
|
+
wrap.appendChild(pre);
|
|
345
|
+
el.appendChild(wrap);
|
|
346
|
+
last = m.index + m[0].length;
|
|
347
|
+
}
|
|
348
|
+
if (last < text.length) el.appendChild(document.createTextNode(text.slice(last)));
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
const COMMANDS = [
|
|
352
|
+
{ name: '/new', desc: 'Start a new session' },
|
|
353
|
+
{ name: '/clear', desc: 'Clear the conversation' },
|
|
354
|
+
{ name: '/compact', desc: 'Compact context to save tokens' },
|
|
355
|
+
{ name: '/help', desc: 'Show available commands' },
|
|
356
|
+
{ name: '/fast', desc: 'Toggle fast mode' },
|
|
357
|
+
{ name: '/remote stop', desc: 'Stop the remote server' },
|
|
358
|
+
];
|
|
359
|
+
let cmdActive = [];
|
|
360
|
+
let cmdIndex = -1;
|
|
361
|
+
|
|
362
|
+
function renderSuggestions() {
|
|
363
|
+
if (cmdActive.length === 0) { cmdSuggestions.style.display = 'none'; return; }
|
|
364
|
+
cmdSuggestions.style.display = 'block';
|
|
365
|
+
cmdSuggestions.innerHTML = '';
|
|
366
|
+
cmdActive.forEach((cmd, i) => {
|
|
367
|
+
const el = document.createElement('div');
|
|
368
|
+
el.className = 'cmd-item' + (i === cmdIndex ? ' active' : '');
|
|
369
|
+
el.innerHTML = '<span class="cmd-name">' + cmd.name + '</span><span class="cmd-desc">' + cmd.desc + '</span>';
|
|
370
|
+
el.addEventListener('mousedown', (e) => { e.preventDefault(); pickCmd(i); });
|
|
371
|
+
cmdSuggestions.appendChild(el);
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function updateSuggestions() {
|
|
376
|
+
const val = inputEl.value;
|
|
377
|
+
if (!val.startsWith('/')) { cmdActive = []; cmdIndex = -1; renderSuggestions(); return; }
|
|
378
|
+
cmdActive = COMMANDS.filter(c => c.name.startsWith(val));
|
|
379
|
+
cmdIndex = cmdActive.length === 1 ? 0 : -1;
|
|
380
|
+
renderSuggestions();
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function pickCmd(i) {
|
|
384
|
+
if (!cmdActive[i]) return;
|
|
385
|
+
inputEl.value = cmdActive[i].name + ' ';
|
|
386
|
+
cmdActive = []; cmdIndex = -1; renderSuggestions();
|
|
387
|
+
inputEl.focus();
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
chatLog.addEventListener('scroll', () => {
|
|
391
|
+
const { scrollTop, scrollHeight, clientHeight } = chatLog;
|
|
392
|
+
autoScroll = scrollTop + clientHeight >= scrollHeight - 24;
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
function scrollBottom() { if (autoScroll) chatLog.scrollTop = chatLog.scrollHeight; }
|
|
396
|
+
|
|
397
|
+
function addBubble(role, text) {
|
|
398
|
+
const el = document.createElement('div');
|
|
399
|
+
el.className = 'bubble ' + role;
|
|
400
|
+
setContent(el, text);
|
|
401
|
+
chatLog.appendChild(el);
|
|
402
|
+
scrollBottom();
|
|
403
|
+
return el;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
let thinkingEl = null;
|
|
407
|
+
function showThinking() {
|
|
408
|
+
if (!thinkingEl) {
|
|
409
|
+
thinkingEl = document.createElement('div');
|
|
410
|
+
thinkingEl.className = 'bubble assistant thinking';
|
|
411
|
+
thinkingEl.innerHTML = '<span class="dot"></span><span class="dot"></span><span class="dot"></span>';
|
|
412
|
+
}
|
|
413
|
+
chatLog.appendChild(thinkingEl); // append (or move) to bottom
|
|
414
|
+
scrollBottom();
|
|
415
|
+
}
|
|
416
|
+
function hideThinking() {
|
|
417
|
+
if (thinkingEl) thinkingEl.remove();
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
function addToolCall(toolName, argsStr, isError) {
|
|
421
|
+
const label = (toolName + ': ' + argsStr).slice(0, 64);
|
|
422
|
+
const d = document.createElement('details');
|
|
423
|
+
d.className = 'tool-call' + (isError ? ' error' : '');
|
|
424
|
+
const s = document.createElement('summary');
|
|
425
|
+
s.textContent = label;
|
|
426
|
+
d.appendChild(s);
|
|
427
|
+
chatLog.appendChild(d);
|
|
428
|
+
scrollBottom();
|
|
429
|
+
return d;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
function setEnabled(on) {
|
|
433
|
+
const allow = on && activePromptId === null;
|
|
434
|
+
inputEl.disabled = !allow;
|
|
435
|
+
sendBtn.disabled = !allow;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
function showToast(message, level) {
|
|
439
|
+
const t = document.createElement('div');
|
|
440
|
+
t.className = 'toast ' + (level || 'info');
|
|
441
|
+
t.textContent = message;
|
|
442
|
+
document.body.appendChild(t);
|
|
443
|
+
setTimeout(function () { t.remove(); }, 4000);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
function answer(value) {
|
|
447
|
+
if (activePromptId === null) return;
|
|
448
|
+
ws.send(JSON.stringify({ type: 'prompt_answer', id: activePromptId, value: value }));
|
|
449
|
+
closePrompt();
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
function closePrompt() {
|
|
453
|
+
activePromptId = null;
|
|
454
|
+
promptCard.style.display = 'none';
|
|
455
|
+
promptInput.value = '';
|
|
456
|
+
setEnabled(true);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
function makeBtn(label, bg, onClick) {
|
|
460
|
+
const btn = document.createElement('button');
|
|
461
|
+
btn.textContent = label;
|
|
462
|
+
btn.style.background = bg;
|
|
463
|
+
btn.onclick = onClick;
|
|
464
|
+
return btn;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
function showPrompt(msg) {
|
|
468
|
+
activePromptId = msg.id;
|
|
469
|
+
promptQ.textContent = msg.question;
|
|
470
|
+
promptInput.value = msg.recommended || '';
|
|
471
|
+
promptButtons.innerHTML = '';
|
|
472
|
+
promptButtons.appendChild(makeBtn('Submit', 'var(--green)', function () { answer(promptInput.value); }));
|
|
473
|
+
if (msg.recommended) {
|
|
474
|
+
promptButtons.appendChild(makeBtn('Accept', 'var(--blue)', function () { answer(''); }));
|
|
475
|
+
}
|
|
476
|
+
if (msg.allowSkip) {
|
|
477
|
+
promptButtons.appendChild(makeBtn('Skip', 'var(--surface2)', function () { answer(''); }));
|
|
478
|
+
}
|
|
479
|
+
promptButtons.appendChild(makeBtn('Cancel task', 'var(--red)', function () { answer(undefined); }));
|
|
480
|
+
promptCard.style.display = 'block';
|
|
481
|
+
setEnabled(false);
|
|
482
|
+
promptInput.focus();
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
function handleMsg(msg) {
|
|
486
|
+
switch (msg.type) {
|
|
487
|
+
case 'history':
|
|
488
|
+
for (const t of (msg.turns || [])) {
|
|
489
|
+
if (t.error) { addBubble('error', t.text); continue; }
|
|
490
|
+
addBubble(t.role, t.text);
|
|
491
|
+
for (const tool of (t.tools || [])) {
|
|
492
|
+
const d = addToolCall(tool.toolName, JSON.stringify(tool.args), tool.isError);
|
|
493
|
+
const pre = document.createElement('pre');
|
|
494
|
+
const r = typeof tool.result === 'string' ? tool.result : JSON.stringify(tool.result, null, 2);
|
|
495
|
+
pre.textContent = r.slice(0, 8000);
|
|
496
|
+
d.appendChild(pre);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
break;
|
|
500
|
+
case 'agent_start':
|
|
501
|
+
autoScroll = true;
|
|
502
|
+
streamText = '';
|
|
503
|
+
currentBubble = null;
|
|
504
|
+
setEnabled(false);
|
|
505
|
+
showThinking();
|
|
506
|
+
break;
|
|
507
|
+
case 'text_delta':
|
|
508
|
+
if (!currentBubble) {
|
|
509
|
+
hideThinking();
|
|
510
|
+
currentBubble = document.createElement('div');
|
|
511
|
+
currentBubble.className = 'bubble assistant';
|
|
512
|
+
const cursor = document.createElement('span');
|
|
513
|
+
cursor.className = 'cursor';
|
|
514
|
+
currentBubble.appendChild(cursor);
|
|
515
|
+
chatLog.appendChild(currentBubble);
|
|
516
|
+
}
|
|
517
|
+
streamText += msg.delta;
|
|
518
|
+
{
|
|
519
|
+
const c = currentBubble.querySelector('.cursor');
|
|
520
|
+
currentBubble.insertBefore(document.createTextNode(msg.delta), c);
|
|
521
|
+
}
|
|
522
|
+
scrollBottom();
|
|
523
|
+
break;
|
|
524
|
+
case 'text_end':
|
|
525
|
+
if (currentBubble) {
|
|
526
|
+
const c = currentBubble.querySelector('.cursor');
|
|
527
|
+
if (c) c.remove();
|
|
528
|
+
if (streamText) setContent(currentBubble, streamText);
|
|
529
|
+
}
|
|
530
|
+
break;
|
|
531
|
+
case 'tool_start': {
|
|
532
|
+
hideThinking();
|
|
533
|
+
const argsStr = typeof msg.args === 'string' ? msg.args : JSON.stringify(msg.args);
|
|
534
|
+
toolCallMap[msg.toolCallId] = addToolCall(msg.toolName, argsStr, false);
|
|
535
|
+
break;
|
|
536
|
+
}
|
|
537
|
+
case 'tool_end': {
|
|
538
|
+
const d = toolCallMap[msg.toolCallId];
|
|
539
|
+
if (d) {
|
|
540
|
+
if (msg.isError) d.classList.add('error');
|
|
541
|
+
const pre = document.createElement('pre');
|
|
542
|
+
const r = typeof msg.result === 'string' ? msg.result : JSON.stringify(msg.result, null, 2);
|
|
543
|
+
pre.textContent = r.slice(0, 8000);
|
|
544
|
+
d.appendChild(pre);
|
|
545
|
+
delete toolCallMap[msg.toolCallId];
|
|
546
|
+
}
|
|
547
|
+
// Tool finished: model is thinking about the result next.
|
|
548
|
+
currentBubble = null;
|
|
549
|
+
streamText = '';
|
|
550
|
+
showThinking();
|
|
551
|
+
break;
|
|
552
|
+
}
|
|
553
|
+
case 'user_message':
|
|
554
|
+
addBubble('user', msg.text);
|
|
555
|
+
break;
|
|
556
|
+
case 'agent_error':
|
|
557
|
+
hideThinking();
|
|
558
|
+
if (currentBubble) {
|
|
559
|
+
const c = currentBubble.querySelector('.cursor');
|
|
560
|
+
if (c) c.remove();
|
|
561
|
+
if (streamText) setContent(currentBubble, streamText);
|
|
562
|
+
currentBubble = null;
|
|
563
|
+
streamText = '';
|
|
564
|
+
}
|
|
565
|
+
addBubble('error', msg.message || 'Error');
|
|
566
|
+
setEnabled(true);
|
|
567
|
+
break;
|
|
568
|
+
case 'agent_end':
|
|
569
|
+
hideThinking();
|
|
570
|
+
currentBubble = null;
|
|
571
|
+
streamText = '';
|
|
572
|
+
setEnabled(true);
|
|
573
|
+
if (msg.contextUsage && msg.contextUsage.percent != null) {
|
|
574
|
+
contextFill.style.width = msg.contextUsage.percent + '%';
|
|
575
|
+
}
|
|
576
|
+
break;
|
|
577
|
+
case 'client_count':
|
|
578
|
+
clientStatus.textContent = msg.count + ' client' + (msg.count === 1 ? '' : 's') + ' connected';
|
|
579
|
+
break;
|
|
580
|
+
case 'prompt':
|
|
581
|
+
showPrompt(msg);
|
|
582
|
+
break;
|
|
583
|
+
case 'prompt_resolved':
|
|
584
|
+
if (activePromptId === msg.id) closePrompt();
|
|
585
|
+
break;
|
|
586
|
+
case 'widget':
|
|
587
|
+
if (msg.lines && msg.lines.length) {
|
|
588
|
+
statusPanel.textContent = msg.lines.join('\\n');
|
|
589
|
+
statusPanel.style.display = 'block';
|
|
590
|
+
} else {
|
|
591
|
+
statusPanel.style.display = 'none';
|
|
592
|
+
}
|
|
593
|
+
break;
|
|
594
|
+
case 'notify':
|
|
595
|
+
showToast(msg.message, msg.level);
|
|
596
|
+
break;
|
|
597
|
+
case 'viewer':
|
|
598
|
+
viewerBody.textContent = msg.text;
|
|
599
|
+
viewer.style.display = 'block';
|
|
600
|
+
break;
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
function sendMessage() {
|
|
605
|
+
const text = inputEl.value.trim();
|
|
606
|
+
if (!text || !ws || ws.readyState !== WebSocket.OPEN) return;
|
|
607
|
+
ws.send(JSON.stringify({ type: 'message', text }));
|
|
608
|
+
inputEl.value = '';
|
|
609
|
+
inputEl.style.height = 'auto';
|
|
610
|
+
cmdActive = []; cmdIndex = -1; renderSuggestions();
|
|
611
|
+
// Slash commands are handled server-side and produce no chat turn.
|
|
612
|
+
if (text.startsWith('/')) return;
|
|
613
|
+
// Optimistic echo: remote-typed messages arrive as source "extension",
|
|
614
|
+
// which the server does not broadcast back, so render locally now.
|
|
615
|
+
addBubble('user', text);
|
|
616
|
+
setEnabled(false);
|
|
617
|
+
showThinking();
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
sendBtn.addEventListener('click', sendMessage);
|
|
621
|
+
inputEl.addEventListener('keydown', (e) => {
|
|
622
|
+
if (cmdActive.length > 0) {
|
|
623
|
+
if (e.key === 'ArrowDown') { e.preventDefault(); cmdIndex = Math.min(cmdIndex + 1, cmdActive.length - 1); renderSuggestions(); return; }
|
|
624
|
+
if (e.key === 'ArrowUp') { e.preventDefault(); cmdIndex = Math.max(cmdIndex - 1, 0); renderSuggestions(); return; }
|
|
625
|
+
if (e.key === 'Tab') { e.preventDefault(); pickCmd(cmdIndex >= 0 ? cmdIndex : 0); return; }
|
|
626
|
+
if (e.key === 'Escape') { cmdActive = []; cmdIndex = -1; renderSuggestions(); return; }
|
|
627
|
+
if (e.key === 'Enter' && !e.shiftKey && cmdIndex >= 0) { e.preventDefault(); pickCmd(cmdIndex); return; }
|
|
628
|
+
}
|
|
629
|
+
if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendMessage(); }
|
|
630
|
+
});
|
|
631
|
+
inputEl.addEventListener('input', () => {
|
|
632
|
+
inputEl.style.height = 'auto';
|
|
633
|
+
inputEl.style.height = Math.min(inputEl.scrollHeight, 120) + 'px';
|
|
634
|
+
updateSuggestions();
|
|
635
|
+
});
|
|
636
|
+
|
|
637
|
+
function connect() {
|
|
638
|
+
ws = new WebSocket(WS_URL);
|
|
639
|
+
ws.addEventListener('open', () => {
|
|
640
|
+
reconnectOverlay.classList.remove('visible');
|
|
641
|
+
reconnectDelay = 1000;
|
|
642
|
+
clientStatus.textContent = 'connected';
|
|
643
|
+
setEnabled(true);
|
|
644
|
+
});
|
|
645
|
+
ws.addEventListener('message', (e) => {
|
|
646
|
+
try { handleMsg(JSON.parse(e.data)); } catch {}
|
|
647
|
+
});
|
|
648
|
+
ws.addEventListener('close', () => {
|
|
649
|
+
setEnabled(false);
|
|
650
|
+
reconnectOverlay.classList.add('visible');
|
|
651
|
+
reconnectMsg.textContent = 'reconnecting in ' + (reconnectDelay / 1000) + 's…';
|
|
652
|
+
setTimeout(() => { reconnectDelay = Math.min(reconnectDelay * 2, 30000); connect(); }, reconnectDelay);
|
|
653
|
+
});
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
connect();
|
|
657
|
+
</script>
|
|
658
|
+
</body>
|
|
659
|
+
</html>`;
|
|
660
|
+
}
|