@poncho-ai/cli 0.18.0 → 0.19.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/.turbo/turbo-build.log +5 -5
- package/CHANGELOG.md +16 -0
- package/dist/{chunk-ETZ3YKFP.js → chunk-7P53QSP5.js} +393 -145
- package/dist/cli.js +1 -1
- package/dist/index.js +1 -1
- package/dist/{run-interactive-ink-VSO7GB3Y.js → run-interactive-ink-4KVVPMR3.js} +1 -1
- package/package.json +4 -4
- package/src/index.ts +180 -119
- package/src/web-ui-client.ts +132 -30
- package/src/web-ui-store.ts +68 -1
- package/src/web-ui-styles.ts +51 -5
- package/src/web-ui.ts +6 -3
package/src/web-ui-client.ts
CHANGED
|
@@ -164,6 +164,62 @@ export const getWebUiClientScript = (markedSource: string): string => `
|
|
|
164
164
|
}
|
|
165
165
|
};
|
|
166
166
|
|
|
167
|
+
// During streaming, incomplete backtick sequences cause marked to
|
|
168
|
+
// swallow all subsequent text into an invisible code element. This
|
|
169
|
+
// helper detects unclosed fences and inline code delimiters and
|
|
170
|
+
// appends the missing closing so marked can render partial text.
|
|
171
|
+
const closeStreamingMarkdown = (text) => {
|
|
172
|
+
const BT = "\\x60";
|
|
173
|
+
let result = text;
|
|
174
|
+
|
|
175
|
+
// 1. Unclosed fenced code blocks (lines starting with 3+ backticks)
|
|
176
|
+
const lines = result.split("\\n");
|
|
177
|
+
let openFenceLen = 0;
|
|
178
|
+
for (let li = 0; li < lines.length; li++) {
|
|
179
|
+
const trimmed = lines[li].trimStart();
|
|
180
|
+
let btCount = 0;
|
|
181
|
+
while (btCount < trimmed.length && trimmed[btCount] === BT) btCount++;
|
|
182
|
+
if (btCount >= 3) {
|
|
183
|
+
if (openFenceLen === 0) {
|
|
184
|
+
openFenceLen = btCount;
|
|
185
|
+
} else if (btCount >= openFenceLen) {
|
|
186
|
+
openFenceLen = 0;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
if (openFenceLen > 0) {
|
|
191
|
+
let fence = "";
|
|
192
|
+
for (let k = 0; k < openFenceLen; k++) fence += BT;
|
|
193
|
+
return result + "\\n" + fence;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// 2. Unclosed inline code delimiters
|
|
197
|
+
let idx = 0;
|
|
198
|
+
let inCode = false;
|
|
199
|
+
let delimLen = 0;
|
|
200
|
+
while (idx < result.length) {
|
|
201
|
+
if (result[idx] === BT) {
|
|
202
|
+
let run = 0;
|
|
203
|
+
while (idx < result.length && result[idx] === BT) { run++; idx++; }
|
|
204
|
+
if (!inCode) {
|
|
205
|
+
inCode = true;
|
|
206
|
+
delimLen = run;
|
|
207
|
+
} else if (run === delimLen) {
|
|
208
|
+
inCode = false;
|
|
209
|
+
}
|
|
210
|
+
} else {
|
|
211
|
+
idx++;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
if (inCode) {
|
|
215
|
+
let closing = "";
|
|
216
|
+
for (let k = 0; k < delimLen; k++) closing += BT;
|
|
217
|
+
result += closing;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return result;
|
|
221
|
+
};
|
|
222
|
+
|
|
167
223
|
const extractToolActivity = (value) => {
|
|
168
224
|
const source = String(value || "");
|
|
169
225
|
let markerIndex = source.lastIndexOf("\\n### Tool activity\\n");
|
|
@@ -230,8 +286,15 @@ export const getWebUiClientScript = (markedSource: string): string => `
|
|
|
230
286
|
);
|
|
231
287
|
})
|
|
232
288
|
.join("");
|
|
289
|
+
const batchButtons = requests.length > 1
|
|
290
|
+
? '<div class="approval-batch-actions">' +
|
|
291
|
+
'<button class="approval-batch-btn approve" data-approval-batch="approve">Approve all (' + requests.length + ')</button>' +
|
|
292
|
+
'<button class="approval-batch-btn deny" data-approval-batch="deny">Deny all (' + requests.length + ')</button>' +
|
|
293
|
+
"</div>"
|
|
294
|
+
: "";
|
|
233
295
|
return (
|
|
234
296
|
'<div class="approval-requests">' +
|
|
297
|
+
batchButtons +
|
|
235
298
|
rows +
|
|
236
299
|
"</div>"
|
|
237
300
|
);
|
|
@@ -761,7 +824,7 @@ export const getWebUiClientScript = (markedSource: string): string => `
|
|
|
761
824
|
// Show current text being typed
|
|
762
825
|
if (isStreaming && i === messages.length - 1 && m._currentText) {
|
|
763
826
|
const textDiv = document.createElement("div");
|
|
764
|
-
textDiv.innerHTML = renderAssistantMarkdown(m._currentText);
|
|
827
|
+
textDiv.innerHTML = renderAssistantMarkdown(closeStreamingMarkdown(m._currentText));
|
|
765
828
|
content.appendChild(textDiv);
|
|
766
829
|
}
|
|
767
830
|
} else {
|
|
@@ -2343,45 +2406,20 @@ export const getWebUiClientScript = (markedSource: string): string => `
|
|
|
2343
2406
|
openLightbox(img.src);
|
|
2344
2407
|
});
|
|
2345
2408
|
|
|
2346
|
-
|
|
2347
|
-
const
|
|
2348
|
-
if (!(target instanceof Element)) {
|
|
2349
|
-
return;
|
|
2350
|
-
}
|
|
2351
|
-
const button = target.closest(".approval-action-btn");
|
|
2352
|
-
if (!button) {
|
|
2353
|
-
return;
|
|
2354
|
-
}
|
|
2355
|
-
const approvalId = button.getAttribute("data-approval-id") || "";
|
|
2356
|
-
const decision = button.getAttribute("data-approval-decision") || "";
|
|
2357
|
-
if (!approvalId || (decision !== "approve" && decision !== "deny")) {
|
|
2358
|
-
return;
|
|
2359
|
-
}
|
|
2360
|
-
if (state.approvalRequestsInFlight[approvalId]) {
|
|
2361
|
-
return;
|
|
2362
|
-
}
|
|
2409
|
+
const submitApproval = async (approvalId, decision, opts) => {
|
|
2410
|
+
const wasStreaming = opts && opts.wasStreaming;
|
|
2363
2411
|
state.approvalRequestsInFlight[approvalId] = true;
|
|
2364
|
-
const wasStreaming = state.isStreaming;
|
|
2365
|
-
if (!wasStreaming) {
|
|
2366
|
-
setStreaming(true);
|
|
2367
|
-
}
|
|
2368
2412
|
updatePendingApproval(approvalId, (request) => ({
|
|
2369
2413
|
...request,
|
|
2370
2414
|
state: "submitting",
|
|
2371
2415
|
pendingDecision: decision,
|
|
2372
2416
|
}));
|
|
2373
|
-
renderMessages(state.activeMessages, state.isStreaming);
|
|
2374
2417
|
try {
|
|
2375
2418
|
await api("/api/approvals/" + encodeURIComponent(approvalId), {
|
|
2376
2419
|
method: "POST",
|
|
2377
2420
|
body: JSON.stringify({ approved: decision === "approve" }),
|
|
2378
2421
|
});
|
|
2379
2422
|
updatePendingApproval(approvalId, () => null);
|
|
2380
|
-
renderMessages(state.activeMessages, state.isStreaming);
|
|
2381
|
-
loadConversations();
|
|
2382
|
-
if (!wasStreaming && state.activeConversationId) {
|
|
2383
|
-
await streamConversationEvents(state.activeConversationId, { liveOnly: true });
|
|
2384
|
-
}
|
|
2385
2423
|
} catch (error) {
|
|
2386
2424
|
const isStale = error && error.payload && error.payload.code === "APPROVAL_NOT_FOUND";
|
|
2387
2425
|
if (isStale) {
|
|
@@ -2395,13 +2433,77 @@ export const getWebUiClientScript = (markedSource: string): string => `
|
|
|
2395
2433
|
_error: errMsg,
|
|
2396
2434
|
}));
|
|
2397
2435
|
}
|
|
2398
|
-
renderMessages(state.activeMessages, state.isStreaming);
|
|
2399
2436
|
} finally {
|
|
2437
|
+
delete state.approvalRequestsInFlight[approvalId];
|
|
2438
|
+
}
|
|
2439
|
+
};
|
|
2440
|
+
|
|
2441
|
+
elements.messages.addEventListener("click", async (event) => {
|
|
2442
|
+
const target = event.target;
|
|
2443
|
+
if (!(target instanceof Element)) {
|
|
2444
|
+
return;
|
|
2445
|
+
}
|
|
2446
|
+
|
|
2447
|
+
// Batch approve/deny all
|
|
2448
|
+
const batchBtn = target.closest(".approval-batch-btn");
|
|
2449
|
+
if (batchBtn) {
|
|
2450
|
+
const decision = batchBtn.getAttribute("data-approval-batch") || "";
|
|
2451
|
+
if (decision !== "approve" && decision !== "deny") return;
|
|
2452
|
+
const messages = state.activeMessages || [];
|
|
2453
|
+
const pending = [];
|
|
2454
|
+
for (const m of messages) {
|
|
2455
|
+
if (Array.isArray(m._pendingApprovals)) {
|
|
2456
|
+
for (const req of m._pendingApprovals) {
|
|
2457
|
+
if (req.approvalId && req.state !== "submitting" && !state.approvalRequestsInFlight[req.approvalId]) {
|
|
2458
|
+
pending.push(req.approvalId);
|
|
2459
|
+
}
|
|
2460
|
+
}
|
|
2461
|
+
}
|
|
2462
|
+
}
|
|
2463
|
+
if (pending.length === 0) return;
|
|
2464
|
+
const wasStreaming = state.isStreaming;
|
|
2465
|
+
if (!wasStreaming) setStreaming(true);
|
|
2466
|
+
renderMessages(state.activeMessages, state.isStreaming);
|
|
2467
|
+
await Promise.all(pending.map((aid) => submitApproval(aid, decision, { wasStreaming })));
|
|
2468
|
+
renderMessages(state.activeMessages, state.isStreaming);
|
|
2469
|
+
loadConversations();
|
|
2470
|
+
if (!wasStreaming && state.activeConversationId) {
|
|
2471
|
+
await streamConversationEvents(state.activeConversationId, { liveOnly: true });
|
|
2472
|
+
}
|
|
2400
2473
|
if (!wasStreaming) {
|
|
2401
2474
|
setStreaming(false);
|
|
2402
2475
|
renderMessages(state.activeMessages, false);
|
|
2403
2476
|
}
|
|
2404
|
-
|
|
2477
|
+
return;
|
|
2478
|
+
}
|
|
2479
|
+
|
|
2480
|
+
// Individual approve/deny
|
|
2481
|
+
const button = target.closest(".approval-action-btn");
|
|
2482
|
+
if (!button) {
|
|
2483
|
+
return;
|
|
2484
|
+
}
|
|
2485
|
+
const approvalId = button.getAttribute("data-approval-id") || "";
|
|
2486
|
+
const decision = button.getAttribute("data-approval-decision") || "";
|
|
2487
|
+
if (!approvalId || (decision !== "approve" && decision !== "deny")) {
|
|
2488
|
+
return;
|
|
2489
|
+
}
|
|
2490
|
+
if (state.approvalRequestsInFlight[approvalId]) {
|
|
2491
|
+
return;
|
|
2492
|
+
}
|
|
2493
|
+
const wasStreaming = state.isStreaming;
|
|
2494
|
+
if (!wasStreaming) {
|
|
2495
|
+
setStreaming(true);
|
|
2496
|
+
}
|
|
2497
|
+
renderMessages(state.activeMessages, state.isStreaming);
|
|
2498
|
+
await submitApproval(approvalId, decision, { wasStreaming });
|
|
2499
|
+
renderMessages(state.activeMessages, state.isStreaming);
|
|
2500
|
+
loadConversations();
|
|
2501
|
+
if (!wasStreaming && state.activeConversationId) {
|
|
2502
|
+
await streamConversationEvents(state.activeConversationId, { liveOnly: true });
|
|
2503
|
+
}
|
|
2504
|
+
if (!wasStreaming) {
|
|
2505
|
+
setStreaming(false);
|
|
2506
|
+
renderMessages(state.activeMessages, false);
|
|
2405
2507
|
}
|
|
2406
2508
|
});
|
|
2407
2509
|
|
package/src/web-ui-store.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createHash, randomUUID, timingSafeEqual } from "node:crypto";
|
|
1
|
+
import { createHash, createHmac, randomUUID, timingSafeEqual } from "node:crypto";
|
|
2
2
|
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
3
3
|
import { basename, dirname, resolve } from "node:path";
|
|
4
4
|
import { homedir } from "node:os";
|
|
@@ -165,11 +165,16 @@ type SessionRecord = {
|
|
|
165
165
|
export class SessionStore {
|
|
166
166
|
private readonly sessions = new Map<string, SessionRecord>();
|
|
167
167
|
private readonly ttlMs: number;
|
|
168
|
+
private signingKey: string | undefined;
|
|
168
169
|
|
|
169
170
|
constructor(ttlMs = 1000 * 60 * 60 * 8) {
|
|
170
171
|
this.ttlMs = ttlMs;
|
|
171
172
|
}
|
|
172
173
|
|
|
174
|
+
setSigningKey(key: string): void {
|
|
175
|
+
if (key) this.signingKey = key;
|
|
176
|
+
}
|
|
177
|
+
|
|
173
178
|
create(ownerId = DEFAULT_OWNER): SessionRecord {
|
|
174
179
|
const now = Date.now();
|
|
175
180
|
const session: SessionRecord = {
|
|
@@ -200,6 +205,68 @@ export class SessionStore {
|
|
|
200
205
|
delete(sessionId: string): void {
|
|
201
206
|
this.sessions.delete(sessionId);
|
|
202
207
|
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Encode a session into a signed cookie value that survives serverless
|
|
211
|
+
* cold starts. Format: `base64url(payload).signature`
|
|
212
|
+
*/
|
|
213
|
+
signSession(session: SessionRecord): string | undefined {
|
|
214
|
+
if (!this.signingKey) return undefined;
|
|
215
|
+
const payload = Buffer.from(
|
|
216
|
+
JSON.stringify({
|
|
217
|
+
sid: session.sessionId,
|
|
218
|
+
o: session.ownerId,
|
|
219
|
+
csrf: session.csrfToken,
|
|
220
|
+
exp: session.expiresAt,
|
|
221
|
+
}),
|
|
222
|
+
).toString("base64url");
|
|
223
|
+
const sig = createHmac("sha256", this.signingKey)
|
|
224
|
+
.update(payload)
|
|
225
|
+
.digest("base64url");
|
|
226
|
+
return `${payload}.${sig}`;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Restore a session from a signed cookie value. Returns the session
|
|
231
|
+
* (also added to the in-memory store) or undefined if invalid/expired.
|
|
232
|
+
*/
|
|
233
|
+
restoreFromSigned(cookieValue: string): SessionRecord | undefined {
|
|
234
|
+
if (!this.signingKey) return undefined;
|
|
235
|
+
const dotIdx = cookieValue.lastIndexOf(".");
|
|
236
|
+
if (dotIdx <= 0) return undefined;
|
|
237
|
+
|
|
238
|
+
const payload = cookieValue.slice(0, dotIdx);
|
|
239
|
+
const sig = cookieValue.slice(dotIdx + 1);
|
|
240
|
+
const expected = createHmac("sha256", this.signingKey)
|
|
241
|
+
.update(payload)
|
|
242
|
+
.digest("base64url");
|
|
243
|
+
if (sig.length !== expected.length) return undefined;
|
|
244
|
+
if (
|
|
245
|
+
!timingSafeEqual(Buffer.from(sig, "utf8"), Buffer.from(expected, "utf8"))
|
|
246
|
+
)
|
|
247
|
+
return undefined;
|
|
248
|
+
|
|
249
|
+
try {
|
|
250
|
+
const data = JSON.parse(
|
|
251
|
+
Buffer.from(payload, "base64url").toString("utf8"),
|
|
252
|
+
) as { sid?: string; o?: string; csrf?: string; exp?: number };
|
|
253
|
+
if (!data.sid || !data.o || !data.csrf || !data.exp) return undefined;
|
|
254
|
+
if (Date.now() > data.exp) return undefined;
|
|
255
|
+
|
|
256
|
+
const session: SessionRecord = {
|
|
257
|
+
sessionId: data.sid,
|
|
258
|
+
ownerId: data.o,
|
|
259
|
+
csrfToken: data.csrf,
|
|
260
|
+
createdAt: data.exp - this.ttlMs,
|
|
261
|
+
expiresAt: data.exp,
|
|
262
|
+
lastSeenAt: Date.now(),
|
|
263
|
+
};
|
|
264
|
+
this.sessions.set(session.sessionId, session);
|
|
265
|
+
return session;
|
|
266
|
+
} catch {
|
|
267
|
+
return undefined;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
203
270
|
}
|
|
204
271
|
|
|
205
272
|
type LoginAttemptState = {
|
package/src/web-ui-styles.ts
CHANGED
|
@@ -270,6 +270,22 @@ export const WEB_UI_STYLES = `
|
|
|
270
270
|
flex-direction: column;
|
|
271
271
|
padding: 12px 8px;
|
|
272
272
|
}
|
|
273
|
+
.sidebar-header {
|
|
274
|
+
display: flex;
|
|
275
|
+
align-items: center;
|
|
276
|
+
gap: 8px;
|
|
277
|
+
}
|
|
278
|
+
.sidebar-agent-name {
|
|
279
|
+
font-size: 14px;
|
|
280
|
+
font-weight: 500;
|
|
281
|
+
color: var(--fg-strong);
|
|
282
|
+
flex: 1;
|
|
283
|
+
min-width: 0;
|
|
284
|
+
overflow: hidden;
|
|
285
|
+
text-overflow: ellipsis;
|
|
286
|
+
white-space: nowrap;
|
|
287
|
+
padding-left: 10px;
|
|
288
|
+
}
|
|
273
289
|
.new-chat-btn {
|
|
274
290
|
background: transparent;
|
|
275
291
|
border: 0;
|
|
@@ -282,6 +298,7 @@ export const WEB_UI_STYLES = `
|
|
|
282
298
|
gap: 8px;
|
|
283
299
|
font-size: 13px;
|
|
284
300
|
cursor: pointer;
|
|
301
|
+
flex-shrink: 0;
|
|
285
302
|
transition: background 0.15s, color 0.15s;
|
|
286
303
|
}
|
|
287
304
|
.new-chat-btn:hover { color: var(--fg); }
|
|
@@ -784,6 +801,30 @@ export const WEB_UI_STYLES = `
|
|
|
784
801
|
opacity: 0.55;
|
|
785
802
|
cursor: not-allowed;
|
|
786
803
|
}
|
|
804
|
+
.approval-batch-actions {
|
|
805
|
+
display: flex;
|
|
806
|
+
gap: 6px;
|
|
807
|
+
margin-bottom: 8px;
|
|
808
|
+
}
|
|
809
|
+
.approval-batch-btn {
|
|
810
|
+
border-radius: 6px;
|
|
811
|
+
border: 1px solid var(--border-5);
|
|
812
|
+
background: var(--surface-4);
|
|
813
|
+
color: var(--fg-approval-btn);
|
|
814
|
+
font-size: 11px;
|
|
815
|
+
font-weight: 600;
|
|
816
|
+
padding: 4px 10px;
|
|
817
|
+
cursor: pointer;
|
|
818
|
+
}
|
|
819
|
+
.approval-batch-btn:hover { background: var(--surface-7); }
|
|
820
|
+
.approval-batch-btn.approve {
|
|
821
|
+
border-color: var(--approve-border);
|
|
822
|
+
color: var(--approve);
|
|
823
|
+
}
|
|
824
|
+
.approval-batch-btn.deny {
|
|
825
|
+
border-color: var(--deny-border);
|
|
826
|
+
color: var(--deny);
|
|
827
|
+
}
|
|
787
828
|
.user-bubble {
|
|
788
829
|
background: var(--bg-elevated);
|
|
789
830
|
border: 1px solid var(--border-2);
|
|
@@ -1180,6 +1221,9 @@ export const WEB_UI_STYLES = `
|
|
|
1180
1221
|
.shell.sidebar-open .sidebar { transform: translateX(0); }
|
|
1181
1222
|
.sidebar-toggle { display: grid; place-items: center; }
|
|
1182
1223
|
.topbar-new-chat { display: grid; place-items: center; }
|
|
1224
|
+
.sidebar-header { padding-right: 130px; }
|
|
1225
|
+
.sidebar-agent-name { padding-left: 0; }
|
|
1226
|
+
.new-chat-btn { order: -1; }
|
|
1183
1227
|
.poncho-badge {
|
|
1184
1228
|
display: none;
|
|
1185
1229
|
position: fixed;
|
|
@@ -1319,15 +1363,17 @@ export const WEB_UI_STYLES = `
|
|
|
1319
1363
|
font-size: 13px;
|
|
1320
1364
|
}
|
|
1321
1365
|
@media (max-width: 768px) {
|
|
1366
|
+
.main-body { flex-direction: column; }
|
|
1322
1367
|
.browser-panel {
|
|
1323
|
-
position:
|
|
1324
|
-
|
|
1325
|
-
|
|
1368
|
+
position: relative;
|
|
1369
|
+
order: -1;
|
|
1370
|
+
max-height: 35vh;
|
|
1326
1371
|
flex: none !important;
|
|
1327
|
-
|
|
1372
|
+
width: auto !important;
|
|
1373
|
+
border-bottom: 1px solid var(--border-1);
|
|
1328
1374
|
}
|
|
1329
1375
|
.browser-panel-resize { display: none !important; }
|
|
1330
|
-
.main-chat.has-browser { flex: 1 1 auto !important; min-width: 0; }
|
|
1376
|
+
.main-chat.has-browser { flex: 1 1 auto !important; min-width: 0; min-height: 0; }
|
|
1331
1377
|
}
|
|
1332
1378
|
|
|
1333
1379
|
/* --- Subagent UI --- */
|
package/src/web-ui.ts
CHANGED
|
@@ -130,9 +130,12 @@ ${WEB_UI_STYLES}
|
|
|
130
130
|
|
|
131
131
|
<div id="app" class="shell hidden">
|
|
132
132
|
<aside class="sidebar">
|
|
133
|
-
<
|
|
134
|
-
<
|
|
135
|
-
|
|
133
|
+
<div class="sidebar-header">
|
|
134
|
+
<span class="sidebar-agent-name">${agentName}</span>
|
|
135
|
+
<button id="new-chat" class="new-chat-btn">
|
|
136
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 5v14M5 12h14"/></svg>
|
|
137
|
+
</button>
|
|
138
|
+
</div>
|
|
136
139
|
<div id="conversation-list" class="conversation-list"></div>
|
|
137
140
|
<div class="sidebar-footer">
|
|
138
141
|
<button id="logout" class="logout-btn">Log out</button>
|