@poncho-ai/cli 0.7.1 → 0.8.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/dist/chunk-3QVOY7B6.js +5715 -0
- package/dist/chunk-ANOPJ4X2.js +5642 -0
- package/dist/chunk-CC667GH3.js +5730 -0
- package/dist/chunk-CH4XYHJE.js +5727 -0
- package/dist/chunk-CP6UFUK2.js +5724 -0
- package/dist/chunk-EZLJV7OP.js +5710 -0
- package/dist/chunk-KR5CYUSD.js +5709 -0
- package/dist/chunk-LSOIOT57.js +5715 -0
- package/dist/chunk-MVYF325X.js +5695 -0
- package/dist/chunk-N6NRICKA.js +5669 -0
- package/dist/chunk-PPPOPLC5.js +5683 -0
- package/dist/chunk-QXF4F3KZ.js +5725 -0
- package/dist/chunk-RJTGWTAI.js +5648 -0
- package/dist/chunk-RYXYGUDN.js +5710 -0
- package/dist/chunk-TDPERUEG.js +5669 -0
- package/dist/chunk-XPDWQPS2.js +5724 -0
- package/dist/chunk-YSDDBHRF.js +5705 -0
- package/dist/chunk-ZKK2TINV.js +5709 -0
- package/dist/chunk-ZL7H73NW.js +5458 -0
- package/dist/chunk-ZMR6K3AO.js +5730 -0
- package/dist/cli.js +1 -1
- package/dist/index.js +1 -1
- package/dist/run-interactive-ink-35CB7WDL.js +494 -0
- package/dist/run-interactive-ink-4ANAOBOL.js +517 -0
- package/dist/run-interactive-ink-4KWEORVB.js +517 -0
- package/dist/run-interactive-ink-4PXAYUJG.js +517 -0
- package/dist/run-interactive-ink-7SLXGC72.js +517 -0
- package/dist/run-interactive-ink-C7TDMV7Z.js +517 -0
- package/dist/run-interactive-ink-IP53WQ3O.js +517 -0
- package/dist/run-interactive-ink-KLIGYEVZ.js +520 -0
- package/dist/run-interactive-ink-LNAAWJDA.js +517 -0
- package/dist/run-interactive-ink-OQUOMIO3.js +520 -0
- package/dist/run-interactive-ink-RDRKOXZ5.js +517 -0
- package/dist/run-interactive-ink-RDSD5STV.js +517 -0
- package/dist/run-interactive-ink-RO6MARC3.js +517 -0
- package/dist/run-interactive-ink-SVHTURD5.js +517 -0
- package/dist/run-interactive-ink-U4JKAUVO.js +517 -0
- package/dist/run-interactive-ink-UL5MFLAE.js +517 -0
- package/dist/run-interactive-ink-VIE5B4YO.js +517 -0
- package/dist/run-interactive-ink-WTEVB7TK.js +517 -0
- package/dist/run-interactive-ink-YZM7ZTJJ.js +517 -0
- package/dist/run-interactive-ink-YZRP7BPI.js +525 -0
- package/package.json +3 -3
- package/src/index.ts +160 -15
- package/src/run-interactive-ink.ts +45 -18
- package/src/web-ui.ts +130 -16
package/src/index.ts
CHANGED
|
@@ -307,6 +307,11 @@ poncho dev
|
|
|
307
307
|
Open \`http://localhost:3000\` for the web UI.
|
|
308
308
|
|
|
309
309
|
On your first interactive session, the agent introduces its configurable capabilities.
|
|
310
|
+
While a response is streaming, you can stop it:
|
|
311
|
+
- Web UI: click the send button again (it switches to a stop icon)
|
|
312
|
+
- Interactive CLI: press \`Ctrl+C\`
|
|
313
|
+
|
|
314
|
+
Stopping is best-effort and keeps partial assistant output/tool activity already produced.
|
|
310
315
|
|
|
311
316
|
## Common Commands
|
|
312
317
|
|
|
@@ -857,6 +862,12 @@ export const createRequestHandler = async (options?: {
|
|
|
857
862
|
} catch {}
|
|
858
863
|
const runOwners = new Map<string, string>();
|
|
859
864
|
const runConversations = new Map<string, string>();
|
|
865
|
+
type ActiveConversationRun = {
|
|
866
|
+
ownerId: string;
|
|
867
|
+
abortController: AbortController;
|
|
868
|
+
runId: string | null;
|
|
869
|
+
};
|
|
870
|
+
const activeConversationRuns = new Map<string, ActiveConversationRun>();
|
|
860
871
|
type PendingApproval = {
|
|
861
872
|
ownerId: string;
|
|
862
873
|
runId: string;
|
|
@@ -924,6 +935,16 @@ export const createRequestHandler = async (options?: {
|
|
|
924
935
|
}));
|
|
925
936
|
await conversationStore.update(conversation);
|
|
926
937
|
};
|
|
938
|
+
const clearPendingApprovalsForConversation = async (conversationId: string): Promise<void> => {
|
|
939
|
+
for (const [approvalId, pending] of pendingApprovals.entries()) {
|
|
940
|
+
if (pending.conversationId !== conversationId) {
|
|
941
|
+
continue;
|
|
942
|
+
}
|
|
943
|
+
pendingApprovals.delete(approvalId);
|
|
944
|
+
pending.resolve(false);
|
|
945
|
+
}
|
|
946
|
+
await persistConversationPendingApprovals(conversationId);
|
|
947
|
+
};
|
|
927
948
|
const harness = new AgentHarness({
|
|
928
949
|
workingDir,
|
|
929
950
|
environment: resolveHarnessEnvironment(),
|
|
@@ -1319,6 +1340,61 @@ export const createRequestHandler = async (options?: {
|
|
|
1319
1340
|
}
|
|
1320
1341
|
}
|
|
1321
1342
|
|
|
1343
|
+
const conversationStopMatch = pathname.match(/^\/api\/conversations\/([^/]+)\/stop$/);
|
|
1344
|
+
if (conversationStopMatch && request.method === "POST") {
|
|
1345
|
+
const conversationId = decodeURIComponent(conversationStopMatch[1] ?? "");
|
|
1346
|
+
const body = (await readRequestBody(request)) as { runId?: string };
|
|
1347
|
+
const requestedRunId = typeof body.runId === "string" ? body.runId.trim() : "";
|
|
1348
|
+
const conversation = await conversationStore.get(conversationId);
|
|
1349
|
+
if (!conversation || conversation.ownerId !== ownerId) {
|
|
1350
|
+
writeJson(response, 404, {
|
|
1351
|
+
code: "CONVERSATION_NOT_FOUND",
|
|
1352
|
+
message: "Conversation not found",
|
|
1353
|
+
});
|
|
1354
|
+
return;
|
|
1355
|
+
}
|
|
1356
|
+
const activeRun = activeConversationRuns.get(conversationId);
|
|
1357
|
+
if (!activeRun || activeRun.ownerId !== ownerId) {
|
|
1358
|
+
writeJson(response, 200, {
|
|
1359
|
+
ok: true,
|
|
1360
|
+
stopped: false,
|
|
1361
|
+
});
|
|
1362
|
+
return;
|
|
1363
|
+
}
|
|
1364
|
+
if (activeRun.abortController.signal.aborted) {
|
|
1365
|
+
activeConversationRuns.delete(conversationId);
|
|
1366
|
+
writeJson(response, 200, {
|
|
1367
|
+
ok: true,
|
|
1368
|
+
stopped: false,
|
|
1369
|
+
});
|
|
1370
|
+
return;
|
|
1371
|
+
}
|
|
1372
|
+
if (requestedRunId && activeRun.runId !== requestedRunId) {
|
|
1373
|
+
writeJson(response, 200, {
|
|
1374
|
+
ok: true,
|
|
1375
|
+
stopped: false,
|
|
1376
|
+
runId: activeRun.runId ?? undefined,
|
|
1377
|
+
});
|
|
1378
|
+
return;
|
|
1379
|
+
}
|
|
1380
|
+
if (!requestedRunId) {
|
|
1381
|
+
writeJson(response, 200, {
|
|
1382
|
+
ok: true,
|
|
1383
|
+
stopped: false,
|
|
1384
|
+
runId: activeRun.runId ?? undefined,
|
|
1385
|
+
});
|
|
1386
|
+
return;
|
|
1387
|
+
}
|
|
1388
|
+
activeRun.abortController.abort();
|
|
1389
|
+
await clearPendingApprovalsForConversation(conversationId);
|
|
1390
|
+
writeJson(response, 200, {
|
|
1391
|
+
ok: true,
|
|
1392
|
+
stopped: true,
|
|
1393
|
+
runId: activeRun.runId ?? undefined,
|
|
1394
|
+
});
|
|
1395
|
+
return;
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1322
1398
|
const conversationMessageMatch = pathname.match(/^\/api\/conversations\/([^/]+)\/messages$/);
|
|
1323
1399
|
if (conversationMessageMatch && request.method === "POST") {
|
|
1324
1400
|
const conversationId = decodeURIComponent(conversationMessageMatch[1] ?? "");
|
|
@@ -1342,6 +1418,24 @@ export const createRequestHandler = async (options?: {
|
|
|
1342
1418
|
});
|
|
1343
1419
|
return;
|
|
1344
1420
|
}
|
|
1421
|
+
const activeRun = activeConversationRuns.get(conversationId);
|
|
1422
|
+
if (activeRun && activeRun.ownerId === ownerId) {
|
|
1423
|
+
if (activeRun.abortController.signal.aborted) {
|
|
1424
|
+
activeConversationRuns.delete(conversationId);
|
|
1425
|
+
} else {
|
|
1426
|
+
writeJson(response, 409, {
|
|
1427
|
+
code: "RUN_IN_PROGRESS",
|
|
1428
|
+
message: "A run is already active for this conversation",
|
|
1429
|
+
});
|
|
1430
|
+
return;
|
|
1431
|
+
}
|
|
1432
|
+
}
|
|
1433
|
+
const abortController = new AbortController();
|
|
1434
|
+
activeConversationRuns.set(conversationId, {
|
|
1435
|
+
ownerId,
|
|
1436
|
+
abortController,
|
|
1437
|
+
runId: null,
|
|
1438
|
+
});
|
|
1345
1439
|
if (
|
|
1346
1440
|
conversation.messages.length === 0 &&
|
|
1347
1441
|
(conversation.title === "New conversation" || conversation.title.trim().length === 0)
|
|
@@ -1360,6 +1454,7 @@ export const createRequestHandler = async (options?: {
|
|
|
1360
1454
|
const sections: Array<{ type: "text" | "tools"; content: string | string[] }> = [];
|
|
1361
1455
|
let currentText = "";
|
|
1362
1456
|
let currentTools: string[] = [];
|
|
1457
|
+
let runCancelled = false;
|
|
1363
1458
|
try {
|
|
1364
1459
|
// Persist the user turn immediately so refreshing mid-run keeps chat context.
|
|
1365
1460
|
conversation.messages = [...historyMessages, { role: "user", content: messageText }];
|
|
@@ -1426,11 +1521,19 @@ export const createRequestHandler = async (options?: {
|
|
|
1426
1521
|
__activeConversationId: conversationId,
|
|
1427
1522
|
},
|
|
1428
1523
|
messages: historyMessages,
|
|
1524
|
+
abortSignal: abortController.signal,
|
|
1429
1525
|
})) {
|
|
1430
1526
|
if (event.type === "run:started") {
|
|
1431
1527
|
latestRunId = event.runId;
|
|
1432
1528
|
runOwners.set(event.runId, ownerId);
|
|
1433
1529
|
runConversations.set(event.runId, conversationId);
|
|
1530
|
+
const active = activeConversationRuns.get(conversationId);
|
|
1531
|
+
if (active && active.abortController === abortController) {
|
|
1532
|
+
active.runId = event.runId;
|
|
1533
|
+
}
|
|
1534
|
+
}
|
|
1535
|
+
if (event.type === "run:cancelled") {
|
|
1536
|
+
runCancelled = true;
|
|
1434
1537
|
}
|
|
1435
1538
|
if (event.type === "model:chunk") {
|
|
1436
1539
|
// If we have tools accumulated and text starts again, push tools as a section
|
|
@@ -1502,26 +1605,60 @@ export const createRequestHandler = async (options?: {
|
|
|
1502
1605
|
if (currentText.length > 0) {
|
|
1503
1606
|
sections.push({ type: "text", content: currentText });
|
|
1504
1607
|
}
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1608
|
+
const hasAssistantContent =
|
|
1609
|
+
assistantResponse.length > 0 || toolTimeline.length > 0 || sections.length > 0;
|
|
1610
|
+
conversation.messages = hasAssistantContent
|
|
1611
|
+
? [
|
|
1612
|
+
...historyMessages,
|
|
1613
|
+
{ role: "user", content: messageText },
|
|
1614
|
+
{
|
|
1615
|
+
role: "assistant",
|
|
1616
|
+
content: assistantResponse,
|
|
1617
|
+
metadata:
|
|
1618
|
+
toolTimeline.length > 0 || sections.length > 0
|
|
1619
|
+
? ({
|
|
1620
|
+
toolActivity: toolTimeline,
|
|
1621
|
+
sections: sections.length > 0 ? sections : undefined,
|
|
1622
|
+
} as Message["metadata"])
|
|
1623
|
+
: undefined,
|
|
1624
|
+
},
|
|
1625
|
+
]
|
|
1626
|
+
: [...historyMessages, { role: "user", content: messageText }];
|
|
1520
1627
|
conversation.runtimeRunId = latestRunId || conversation.runtimeRunId;
|
|
1521
1628
|
conversation.pendingApprovals = [];
|
|
1522
1629
|
conversation.updatedAt = Date.now();
|
|
1523
1630
|
await conversationStore.update(conversation);
|
|
1524
1631
|
} catch (error) {
|
|
1632
|
+
if (abortController.signal.aborted || runCancelled) {
|
|
1633
|
+
const fallbackSections = [...sections];
|
|
1634
|
+
if (currentTools.length > 0) {
|
|
1635
|
+
fallbackSections.push({ type: "tools", content: [...currentTools] });
|
|
1636
|
+
}
|
|
1637
|
+
if (currentText.length > 0) {
|
|
1638
|
+
fallbackSections.push({ type: "text", content: currentText });
|
|
1639
|
+
}
|
|
1640
|
+
if (assistantResponse.length > 0 || toolTimeline.length > 0 || fallbackSections.length > 0) {
|
|
1641
|
+
conversation.messages = [
|
|
1642
|
+
...historyMessages,
|
|
1643
|
+
{ role: "user", content: messageText },
|
|
1644
|
+
{
|
|
1645
|
+
role: "assistant",
|
|
1646
|
+
content: assistantResponse,
|
|
1647
|
+
metadata:
|
|
1648
|
+
toolTimeline.length > 0 || fallbackSections.length > 0
|
|
1649
|
+
? ({
|
|
1650
|
+
toolActivity: [...toolTimeline],
|
|
1651
|
+
sections: fallbackSections.length > 0 ? fallbackSections : undefined,
|
|
1652
|
+
} as Message["metadata"])
|
|
1653
|
+
: undefined,
|
|
1654
|
+
},
|
|
1655
|
+
];
|
|
1656
|
+
conversation.updatedAt = Date.now();
|
|
1657
|
+
await conversationStore.update(conversation);
|
|
1658
|
+
}
|
|
1659
|
+
await clearPendingApprovalsForConversation(conversationId);
|
|
1660
|
+
return;
|
|
1661
|
+
}
|
|
1525
1662
|
try {
|
|
1526
1663
|
response.write(
|
|
1527
1664
|
formatSseEvent({
|
|
@@ -1563,6 +1700,10 @@ export const createRequestHandler = async (options?: {
|
|
|
1563
1700
|
}
|
|
1564
1701
|
}
|
|
1565
1702
|
} finally {
|
|
1703
|
+
const active = activeConversationRuns.get(conversationId);
|
|
1704
|
+
if (active && active.abortController === abortController) {
|
|
1705
|
+
activeConversationRuns.delete(conversationId);
|
|
1706
|
+
}
|
|
1566
1707
|
finishConversationStream(conversationId);
|
|
1567
1708
|
await persistConversationPendingApprovals(conversationId);
|
|
1568
1709
|
if (latestRunId) {
|
|
@@ -1654,6 +1795,10 @@ export const runOnce = async (
|
|
|
1654
1795
|
if (event.type === "run:completed") {
|
|
1655
1796
|
process.stdout.write("\n");
|
|
1656
1797
|
}
|
|
1798
|
+
if (event.type === "run:cancelled") {
|
|
1799
|
+
process.stdout.write("\n");
|
|
1800
|
+
process.stderr.write("Run cancelled.\n");
|
|
1801
|
+
}
|
|
1657
1802
|
}
|
|
1658
1803
|
};
|
|
1659
1804
|
|
|
@@ -355,6 +355,7 @@ export const runInteractiveInk = async ({
|
|
|
355
355
|
),
|
|
356
356
|
);
|
|
357
357
|
console.log(gray('Type "exit" to quit, "/help" for commands'));
|
|
358
|
+
console.log(gray("Press Ctrl+C during a run to stop streaming output."));
|
|
358
359
|
console.log(
|
|
359
360
|
gray("Conversation controls: /list /open <id> /new [title] /delete [id] /continue /reset [all]\n"),
|
|
360
361
|
);
|
|
@@ -376,6 +377,17 @@ export const runInteractiveInk = async ({
|
|
|
376
377
|
let turn = 1;
|
|
377
378
|
let activeConversationId: string | null = null;
|
|
378
379
|
let showToolPayloads = false;
|
|
380
|
+
let activeRunAbortController: AbortController | null = null;
|
|
381
|
+
|
|
382
|
+
rl.on("SIGINT", () => {
|
|
383
|
+
if (activeRunAbortController && !activeRunAbortController.signal.aborted) {
|
|
384
|
+
activeRunAbortController.abort();
|
|
385
|
+
process.stdout.write("\n");
|
|
386
|
+
console.log(gray("stop> cancelling current run..."));
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
rl.close();
|
|
390
|
+
});
|
|
379
391
|
|
|
380
392
|
// --- Main loop -------------------------------------------------------------
|
|
381
393
|
|
|
@@ -447,15 +459,18 @@ export const runInteractiveInk = async ({
|
|
|
447
459
|
let currentText = "";
|
|
448
460
|
let currentTools: string[] = [];
|
|
449
461
|
let runFailed = false;
|
|
462
|
+
let runCancelled = false;
|
|
450
463
|
let usage: TokenUsage | undefined;
|
|
451
464
|
let latestRunId = "";
|
|
452
465
|
const startedAt = Date.now();
|
|
466
|
+
activeRunAbortController = new AbortController();
|
|
453
467
|
|
|
454
468
|
try {
|
|
455
469
|
for await (const event of harness.run({
|
|
456
470
|
task: trimmed,
|
|
457
471
|
parameters: params,
|
|
458
472
|
messages,
|
|
473
|
+
abortSignal: activeRunAbortController.signal,
|
|
459
474
|
})) {
|
|
460
475
|
if (event.type === "run:started") {
|
|
461
476
|
latestRunId = event.runId;
|
|
@@ -555,6 +570,9 @@ export const runInteractiveInk = async ({
|
|
|
555
570
|
clearThinking();
|
|
556
571
|
runFailed = true;
|
|
557
572
|
console.log(red(`error> ${event.error.message}`));
|
|
573
|
+
} else if (event.type === "run:cancelled") {
|
|
574
|
+
clearThinking();
|
|
575
|
+
runCancelled = true;
|
|
558
576
|
} else if (event.type === "model:response") {
|
|
559
577
|
usage = event.usage;
|
|
560
578
|
} else if (event.type === "run:completed" && !sawChunk) {
|
|
@@ -569,18 +587,24 @@ export const runInteractiveInk = async ({
|
|
|
569
587
|
}
|
|
570
588
|
} catch (error) {
|
|
571
589
|
clearThinking();
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
590
|
+
if (activeRunAbortController.signal.aborted) {
|
|
591
|
+
runCancelled = true;
|
|
592
|
+
} else {
|
|
593
|
+
runFailed = true;
|
|
594
|
+
console.log(
|
|
595
|
+
red(
|
|
596
|
+
`error> ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
597
|
+
),
|
|
598
|
+
);
|
|
599
|
+
}
|
|
600
|
+
} finally {
|
|
601
|
+
activeRunAbortController = null;
|
|
578
602
|
}
|
|
579
603
|
|
|
580
604
|
// End the streaming line if needed
|
|
581
605
|
if (sawChunk && streamedText.length > 0) {
|
|
582
606
|
process.stdout.write("\n");
|
|
583
|
-
} else if (!sawChunk && !runFailed && responseText.length === 0) {
|
|
607
|
+
} else if (!sawChunk && !runFailed && !runCancelled && responseText.length === 0) {
|
|
584
608
|
clearThinking();
|
|
585
609
|
console.log(green("assistant> (no response)"));
|
|
586
610
|
}
|
|
@@ -621,17 +645,20 @@ export const runInteractiveInk = async ({
|
|
|
621
645
|
}
|
|
622
646
|
|
|
623
647
|
messages.push({ role: "user", content: trimmed });
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
648
|
+
const hasAssistantContent = responseText.length > 0 || toolTimeline.length > 0 || sections.length > 0;
|
|
649
|
+
if (hasAssistantContent) {
|
|
650
|
+
messages.push({
|
|
651
|
+
role: "assistant",
|
|
652
|
+
content: responseText,
|
|
653
|
+
metadata:
|
|
654
|
+
toolTimeline.length > 0 || sections.length > 0
|
|
655
|
+
? ({
|
|
656
|
+
toolActivity: toolTimeline,
|
|
657
|
+
sections: sections.length > 0 ? sections : undefined,
|
|
658
|
+
} as Message["metadata"])
|
|
659
|
+
: undefined,
|
|
660
|
+
});
|
|
661
|
+
}
|
|
635
662
|
turn = computeTurn(messages);
|
|
636
663
|
|
|
637
664
|
const conversation = await conversationStore.get(activeConversationId);
|
package/src/web-ui.ts
CHANGED
|
@@ -1074,9 +1074,9 @@ export const renderWebUiHtml = (options?: { agentName?: string }): string => {
|
|
|
1074
1074
|
.composer-shell {
|
|
1075
1075
|
background: #0a0a0a;
|
|
1076
1076
|
border: 1px solid rgba(255,255,255,0.1);
|
|
1077
|
-
border-radius:
|
|
1077
|
+
border-radius: 24px;
|
|
1078
1078
|
display: flex;
|
|
1079
|
-
align-items:
|
|
1079
|
+
align-items: end;
|
|
1080
1080
|
padding: 4px 6px 4px 18px;
|
|
1081
1081
|
transition: border-color 0.15s;
|
|
1082
1082
|
}
|
|
@@ -1093,7 +1093,7 @@ export const renderWebUiHtml = (options?: { agentName?: string }): string => {
|
|
|
1093
1093
|
padding: 10px 0 8px;
|
|
1094
1094
|
font-size: 14px;
|
|
1095
1095
|
line-height: 1.5;
|
|
1096
|
-
margin-top: -
|
|
1096
|
+
margin-top: -4px;
|
|
1097
1097
|
}
|
|
1098
1098
|
.composer-input::placeholder { color: #444; }
|
|
1099
1099
|
.send-btn {
|
|
@@ -1111,6 +1111,11 @@ export const renderWebUiHtml = (options?: { agentName?: string }): string => {
|
|
|
1111
1111
|
transition: background 0.15s, opacity 0.15s;
|
|
1112
1112
|
}
|
|
1113
1113
|
.send-btn:hover { background: #fff; }
|
|
1114
|
+
.send-btn.stop-mode {
|
|
1115
|
+
background: #4a4a4a;
|
|
1116
|
+
color: #fff;
|
|
1117
|
+
}
|
|
1118
|
+
.send-btn.stop-mode:hover { background: #565656; }
|
|
1114
1119
|
.send-btn:disabled { opacity: 0.2; cursor: default; }
|
|
1115
1120
|
.send-btn:disabled:hover { background: #ededed; }
|
|
1116
1121
|
.disclaimer {
|
|
@@ -1238,6 +1243,9 @@ export const renderWebUiHtml = (options?: { agentName?: string }): string => {
|
|
|
1238
1243
|
activeConversationId: null,
|
|
1239
1244
|
activeMessages: [],
|
|
1240
1245
|
isStreaming: false,
|
|
1246
|
+
activeStreamAbortController: null,
|
|
1247
|
+
activeStreamConversationId: null,
|
|
1248
|
+
activeStreamRunId: null,
|
|
1241
1249
|
isMessagesPinnedToBottom: true,
|
|
1242
1250
|
confirmDeleteId: null,
|
|
1243
1251
|
approvalRequestsInFlight: {}
|
|
@@ -1264,6 +1272,10 @@ export const renderWebUiHtml = (options?: { agentName?: string }): string => {
|
|
|
1264
1272
|
sidebarToggle: $("sidebar-toggle"),
|
|
1265
1273
|
sidebarBackdrop: $("sidebar-backdrop")
|
|
1266
1274
|
};
|
|
1275
|
+
const sendIconMarkup =
|
|
1276
|
+
'<svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M8 12V4M4 7l4-4 4 4" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>';
|
|
1277
|
+
const stopIconMarkup =
|
|
1278
|
+
'<svg width="16" height="16" viewBox="0 0 16 16" fill="none"><rect x="4" y="4" width="8" height="8" rx="2" fill="currentColor"/></svg>';
|
|
1267
1279
|
|
|
1268
1280
|
const pushConversationUrl = (conversationId) => {
|
|
1269
1281
|
const target = conversationId ? "/c/" + encodeURIComponent(conversationId) : "/";
|
|
@@ -1950,6 +1962,24 @@ export const renderWebUiHtml = (options?: { agentName?: string }): string => {
|
|
|
1950
1962
|
}
|
|
1951
1963
|
renderIfActiveConversation(false);
|
|
1952
1964
|
}
|
|
1965
|
+
if (eventName === "run:cancelled") {
|
|
1966
|
+
assistantMessage._activeActivities = [];
|
|
1967
|
+
if (assistantMessage._currentTools.length > 0) {
|
|
1968
|
+
assistantMessage._sections.push({
|
|
1969
|
+
type: "tools",
|
|
1970
|
+
content: assistantMessage._currentTools,
|
|
1971
|
+
});
|
|
1972
|
+
assistantMessage._currentTools = [];
|
|
1973
|
+
}
|
|
1974
|
+
if (assistantMessage._currentText.length > 0) {
|
|
1975
|
+
assistantMessage._sections.push({
|
|
1976
|
+
type: "text",
|
|
1977
|
+
content: assistantMessage._currentText,
|
|
1978
|
+
});
|
|
1979
|
+
assistantMessage._currentText = "";
|
|
1980
|
+
}
|
|
1981
|
+
renderIfActiveConversation(false);
|
|
1982
|
+
}
|
|
1953
1983
|
if (eventName === "run:error") {
|
|
1954
1984
|
assistantMessage._activeActivities = [];
|
|
1955
1985
|
const errMsg =
|
|
@@ -2023,7 +2053,15 @@ export const renderWebUiHtml = (options?: { agentName?: string }): string => {
|
|
|
2023
2053
|
|
|
2024
2054
|
const setStreaming = (value) => {
|
|
2025
2055
|
state.isStreaming = value;
|
|
2026
|
-
|
|
2056
|
+
const canStop = value && !!state.activeStreamRunId;
|
|
2057
|
+
elements.send.disabled = value ? !canStop : false;
|
|
2058
|
+
elements.send.innerHTML = value ? stopIconMarkup : sendIconMarkup;
|
|
2059
|
+
elements.send.classList.toggle("stop-mode", value);
|
|
2060
|
+
elements.send.setAttribute("aria-label", value ? "Stop response" : "Send message");
|
|
2061
|
+
elements.send.setAttribute(
|
|
2062
|
+
"title",
|
|
2063
|
+
value ? (canStop ? "Stop response" : "Starting response...") : "Send message",
|
|
2064
|
+
);
|
|
2027
2065
|
};
|
|
2028
2066
|
|
|
2029
2067
|
const pushToolActivity = (assistantMessage, line) => {
|
|
@@ -2204,6 +2242,36 @@ export const renderWebUiHtml = (options?: { agentName?: string }): string => {
|
|
|
2204
2242
|
el.style.overflowY = scrollHeight > 200 ? "auto" : "hidden";
|
|
2205
2243
|
};
|
|
2206
2244
|
|
|
2245
|
+
const stopActiveRun = async () => {
|
|
2246
|
+
const stopRunId = state.activeStreamRunId;
|
|
2247
|
+
if (!stopRunId) return;
|
|
2248
|
+
const conversationId = state.activeStreamConversationId || state.activeConversationId;
|
|
2249
|
+
if (!conversationId) return;
|
|
2250
|
+
// Disable the stop button immediately so the user sees feedback.
|
|
2251
|
+
state.activeStreamRunId = null;
|
|
2252
|
+
setStreaming(state.isStreaming);
|
|
2253
|
+
// Signal the server to cancel the run. The server will emit
|
|
2254
|
+
// run:cancelled through the still-open SSE stream, which
|
|
2255
|
+
// sendMessage() processes naturally – the stream ends on its own
|
|
2256
|
+
// and cleanup happens in one finally block. No fetch abort needed.
|
|
2257
|
+
try {
|
|
2258
|
+
await api(
|
|
2259
|
+
"/api/conversations/" + encodeURIComponent(conversationId) + "/stop",
|
|
2260
|
+
{
|
|
2261
|
+
method: "POST",
|
|
2262
|
+
body: JSON.stringify({ runId: stopRunId }),
|
|
2263
|
+
},
|
|
2264
|
+
);
|
|
2265
|
+
} catch (e) {
|
|
2266
|
+
console.warn("Failed to stop conversation run:", e);
|
|
2267
|
+
// Fallback: abort the local fetch so the UI at least stops.
|
|
2268
|
+
const abortController = state.activeStreamAbortController;
|
|
2269
|
+
if (abortController && !abortController.signal.aborted) {
|
|
2270
|
+
abortController.abort();
|
|
2271
|
+
}
|
|
2272
|
+
}
|
|
2273
|
+
};
|
|
2274
|
+
|
|
2207
2275
|
const sendMessage = async (text) => {
|
|
2208
2276
|
const messageText = (text || "").trim();
|
|
2209
2277
|
if (!messageText || state.isStreaming) {
|
|
@@ -2223,12 +2291,16 @@ export const renderWebUiHtml = (options?: { agentName?: string }): string => {
|
|
|
2223
2291
|
localMessages.push(assistantMessage);
|
|
2224
2292
|
state.activeMessages = localMessages;
|
|
2225
2293
|
renderMessages(localMessages, true, { forceScrollBottom: true });
|
|
2226
|
-
setStreaming(true);
|
|
2227
2294
|
let conversationId = state.activeConversationId;
|
|
2295
|
+
const streamAbortController = new AbortController();
|
|
2296
|
+
state.activeStreamAbortController = streamAbortController;
|
|
2297
|
+
state.activeStreamRunId = null;
|
|
2298
|
+
setStreaming(true);
|
|
2228
2299
|
try {
|
|
2229
2300
|
if (!conversationId) {
|
|
2230
2301
|
conversationId = await createConversation(messageText, { loadConversation: false });
|
|
2231
2302
|
}
|
|
2303
|
+
state.activeStreamConversationId = conversationId;
|
|
2232
2304
|
const streamConversationId = conversationId;
|
|
2233
2305
|
const renderIfActiveConversation = (streaming) => {
|
|
2234
2306
|
if (state.activeConversationId !== streamConversationId) {
|
|
@@ -2237,11 +2309,23 @@ export const renderWebUiHtml = (options?: { agentName?: string }): string => {
|
|
|
2237
2309
|
state.activeMessages = localMessages;
|
|
2238
2310
|
renderMessages(localMessages, streaming);
|
|
2239
2311
|
};
|
|
2312
|
+
const finalizeAssistantMessage = () => {
|
|
2313
|
+
assistantMessage._activeActivities = [];
|
|
2314
|
+
if (assistantMessage._currentTools.length > 0) {
|
|
2315
|
+
assistantMessage._sections.push({ type: "tools", content: assistantMessage._currentTools });
|
|
2316
|
+
assistantMessage._currentTools = [];
|
|
2317
|
+
}
|
|
2318
|
+
if (assistantMessage._currentText.length > 0) {
|
|
2319
|
+
assistantMessage._sections.push({ type: "text", content: assistantMessage._currentText });
|
|
2320
|
+
assistantMessage._currentText = "";
|
|
2321
|
+
}
|
|
2322
|
+
};
|
|
2240
2323
|
const response = await fetch("/api/conversations/" + encodeURIComponent(conversationId) + "/messages", {
|
|
2241
2324
|
method: "POST",
|
|
2242
2325
|
credentials: "include",
|
|
2243
2326
|
headers: { "Content-Type": "application/json", "x-csrf-token": state.csrfToken },
|
|
2244
|
-
body: JSON.stringify({ message: messageText })
|
|
2327
|
+
body: JSON.stringify({ message: messageText }),
|
|
2328
|
+
signal: streamAbortController.signal,
|
|
2245
2329
|
});
|
|
2246
2330
|
if (!response.ok || !response.body) {
|
|
2247
2331
|
throw new Error("Failed to stream response");
|
|
@@ -2268,6 +2352,10 @@ export const renderWebUiHtml = (options?: { agentName?: string }): string => {
|
|
|
2268
2352
|
assistantMessage._currentText += chunk;
|
|
2269
2353
|
renderIfActiveConversation(true);
|
|
2270
2354
|
}
|
|
2355
|
+
if (eventName === "run:started") {
|
|
2356
|
+
state.activeStreamRunId = typeof payload.runId === "string" ? payload.runId : null;
|
|
2357
|
+
setStreaming(state.isStreaming);
|
|
2358
|
+
}
|
|
2271
2359
|
if (eventName === "tool:started") {
|
|
2272
2360
|
const toolName = payload.tool || "tool";
|
|
2273
2361
|
const startedActivity = addActiveActivityFromToolStart(
|
|
@@ -2413,19 +2501,14 @@ export const renderWebUiHtml = (options?: { agentName?: string }): string => {
|
|
|
2413
2501
|
renderIfActiveConversation(true);
|
|
2414
2502
|
}
|
|
2415
2503
|
if (eventName === "run:completed") {
|
|
2416
|
-
|
|
2504
|
+
finalizeAssistantMessage();
|
|
2417
2505
|
if (!assistantMessage.content || assistantMessage.content.length === 0) {
|
|
2418
2506
|
assistantMessage.content = String(payload.result?.response || "");
|
|
2419
2507
|
}
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
}
|
|
2425
|
-
if (assistantMessage._currentText.length > 0) {
|
|
2426
|
-
assistantMessage._sections.push({ type: "text", content: assistantMessage._currentText });
|
|
2427
|
-
assistantMessage._currentText = "";
|
|
2428
|
-
}
|
|
2508
|
+
renderIfActiveConversation(false);
|
|
2509
|
+
}
|
|
2510
|
+
if (eventName === "run:cancelled") {
|
|
2511
|
+
finalizeAssistantMessage();
|
|
2429
2512
|
renderIfActiveConversation(false);
|
|
2430
2513
|
}
|
|
2431
2514
|
if (eventName === "run:error") {
|
|
@@ -2446,7 +2529,31 @@ export const renderWebUiHtml = (options?: { agentName?: string }): string => {
|
|
|
2446
2529
|
}
|
|
2447
2530
|
await loadConversations();
|
|
2448
2531
|
// Don't reload the conversation - we already have the latest state with tool chips
|
|
2532
|
+
} catch (error) {
|
|
2533
|
+
if (streamAbortController.signal.aborted) {
|
|
2534
|
+
assistantMessage._activeActivities = [];
|
|
2535
|
+
if (assistantMessage._currentTools.length > 0) {
|
|
2536
|
+
assistantMessage._sections.push({ type: "tools", content: assistantMessage._currentTools });
|
|
2537
|
+
assistantMessage._currentTools = [];
|
|
2538
|
+
}
|
|
2539
|
+
if (assistantMessage._currentText.length > 0) {
|
|
2540
|
+
assistantMessage._sections.push({ type: "text", content: assistantMessage._currentText });
|
|
2541
|
+
assistantMessage._currentText = "";
|
|
2542
|
+
}
|
|
2543
|
+
renderMessages(localMessages, false);
|
|
2544
|
+
} else {
|
|
2545
|
+
assistantMessage._activeActivities = [];
|
|
2546
|
+
assistantMessage._error = error instanceof Error ? error.message : "Something went wrong";
|
|
2547
|
+
renderMessages(localMessages, false);
|
|
2548
|
+
}
|
|
2449
2549
|
} finally {
|
|
2550
|
+
if (state.activeStreamAbortController === streamAbortController) {
|
|
2551
|
+
state.activeStreamAbortController = null;
|
|
2552
|
+
}
|
|
2553
|
+
if (state.activeStreamConversationId === conversationId) {
|
|
2554
|
+
state.activeStreamConversationId = null;
|
|
2555
|
+
}
|
|
2556
|
+
state.activeStreamRunId = null;
|
|
2450
2557
|
setStreaming(false);
|
|
2451
2558
|
elements.prompt.focus();
|
|
2452
2559
|
}
|
|
@@ -2549,6 +2656,13 @@ export const renderWebUiHtml = (options?: { agentName?: string }): string => {
|
|
|
2549
2656
|
|
|
2550
2657
|
elements.composer.addEventListener("submit", async (event) => {
|
|
2551
2658
|
event.preventDefault();
|
|
2659
|
+
if (state.isStreaming) {
|
|
2660
|
+
if (!state.activeStreamRunId) {
|
|
2661
|
+
return;
|
|
2662
|
+
}
|
|
2663
|
+
await stopActiveRun();
|
|
2664
|
+
return;
|
|
2665
|
+
}
|
|
2552
2666
|
const value = elements.prompt.value;
|
|
2553
2667
|
elements.prompt.value = "";
|
|
2554
2668
|
autoResizePrompt();
|