@poncho-ai/cli 0.7.0 → 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-JBSQRXIR.js +5458 -0
- package/dist/chunk-JRIAQTMD.js +5548 -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-TBKHI5AK.js +5500 -0
- package/dist/chunk-TDPERUEG.js +5669 -0
- package/dist/chunk-TF3KCJAS.js +5458 -0
- package/dist/chunk-XPDWQPS2.js +5724 -0
- package/dist/chunk-XR3VAI5W.js +5458 -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-5OUK2NRE.js +494 -0
- package/dist/run-interactive-ink-7SLXGC72.js +517 -0
- package/dist/run-interactive-ink-C7TDMV7Z.js +517 -0
- package/dist/run-interactive-ink-IFKUSZIO.js +494 -0
- package/dist/run-interactive-ink-IP53WQ3O.js +517 -0
- package/dist/run-interactive-ink-KH2MJRH7.js +494 -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-S6MSMSX3.js +494 -0
- package/dist/run-interactive-ink-SVHTURD5.js +517 -0
- package/dist/run-interactive-ink-TSHABWSR.js +494 -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 +132 -23
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) : "/";
|
|
@@ -1745,12 +1757,7 @@ export const renderWebUiHtml = (options?: { agentName?: string }): string => {
|
|
|
1745
1757
|
);
|
|
1746
1758
|
}
|
|
1747
1759
|
}
|
|
1748
|
-
if (
|
|
1749
|
-
isStreaming &&
|
|
1750
|
-
isLastAssistant &&
|
|
1751
|
-
!hasPendingApprovals &&
|
|
1752
|
-
(!m._currentText || m._currentText.length === 0)
|
|
1753
|
-
) {
|
|
1760
|
+
if (isStreaming && isLastAssistant && !hasPendingApprovals) {
|
|
1754
1761
|
const waitIndicator = document.createElement("div");
|
|
1755
1762
|
waitIndicator.appendChild(createThinkingIndicator(getThinkingStatusLabel(m)));
|
|
1756
1763
|
content.appendChild(waitIndicator);
|
|
@@ -1955,6 +1962,24 @@ export const renderWebUiHtml = (options?: { agentName?: string }): string => {
|
|
|
1955
1962
|
}
|
|
1956
1963
|
renderIfActiveConversation(false);
|
|
1957
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
|
+
}
|
|
1958
1983
|
if (eventName === "run:error") {
|
|
1959
1984
|
assistantMessage._activeActivities = [];
|
|
1960
1985
|
const errMsg =
|
|
@@ -2028,7 +2053,15 @@ export const renderWebUiHtml = (options?: { agentName?: string }): string => {
|
|
|
2028
2053
|
|
|
2029
2054
|
const setStreaming = (value) => {
|
|
2030
2055
|
state.isStreaming = value;
|
|
2031
|
-
|
|
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
|
+
);
|
|
2032
2065
|
};
|
|
2033
2066
|
|
|
2034
2067
|
const pushToolActivity = (assistantMessage, line) => {
|
|
@@ -2197,7 +2230,7 @@ export const renderWebUiHtml = (options?: { agentName?: string }): string => {
|
|
|
2197
2230
|
}
|
|
2198
2231
|
}
|
|
2199
2232
|
}
|
|
2200
|
-
return "
|
|
2233
|
+
return "";
|
|
2201
2234
|
};
|
|
2202
2235
|
|
|
2203
2236
|
const autoResizePrompt = () => {
|
|
@@ -2209,6 +2242,36 @@ export const renderWebUiHtml = (options?: { agentName?: string }): string => {
|
|
|
2209
2242
|
el.style.overflowY = scrollHeight > 200 ? "auto" : "hidden";
|
|
2210
2243
|
};
|
|
2211
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
|
+
|
|
2212
2275
|
const sendMessage = async (text) => {
|
|
2213
2276
|
const messageText = (text || "").trim();
|
|
2214
2277
|
if (!messageText || state.isStreaming) {
|
|
@@ -2228,12 +2291,16 @@ export const renderWebUiHtml = (options?: { agentName?: string }): string => {
|
|
|
2228
2291
|
localMessages.push(assistantMessage);
|
|
2229
2292
|
state.activeMessages = localMessages;
|
|
2230
2293
|
renderMessages(localMessages, true, { forceScrollBottom: true });
|
|
2231
|
-
setStreaming(true);
|
|
2232
2294
|
let conversationId = state.activeConversationId;
|
|
2295
|
+
const streamAbortController = new AbortController();
|
|
2296
|
+
state.activeStreamAbortController = streamAbortController;
|
|
2297
|
+
state.activeStreamRunId = null;
|
|
2298
|
+
setStreaming(true);
|
|
2233
2299
|
try {
|
|
2234
2300
|
if (!conversationId) {
|
|
2235
2301
|
conversationId = await createConversation(messageText, { loadConversation: false });
|
|
2236
2302
|
}
|
|
2303
|
+
state.activeStreamConversationId = conversationId;
|
|
2237
2304
|
const streamConversationId = conversationId;
|
|
2238
2305
|
const renderIfActiveConversation = (streaming) => {
|
|
2239
2306
|
if (state.activeConversationId !== streamConversationId) {
|
|
@@ -2242,11 +2309,23 @@ export const renderWebUiHtml = (options?: { agentName?: string }): string => {
|
|
|
2242
2309
|
state.activeMessages = localMessages;
|
|
2243
2310
|
renderMessages(localMessages, streaming);
|
|
2244
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
|
+
};
|
|
2245
2323
|
const response = await fetch("/api/conversations/" + encodeURIComponent(conversationId) + "/messages", {
|
|
2246
2324
|
method: "POST",
|
|
2247
2325
|
credentials: "include",
|
|
2248
2326
|
headers: { "Content-Type": "application/json", "x-csrf-token": state.csrfToken },
|
|
2249
|
-
body: JSON.stringify({ message: messageText })
|
|
2327
|
+
body: JSON.stringify({ message: messageText }),
|
|
2328
|
+
signal: streamAbortController.signal,
|
|
2250
2329
|
});
|
|
2251
2330
|
if (!response.ok || !response.body) {
|
|
2252
2331
|
throw new Error("Failed to stream response");
|
|
@@ -2273,6 +2352,10 @@ export const renderWebUiHtml = (options?: { agentName?: string }): string => {
|
|
|
2273
2352
|
assistantMessage._currentText += chunk;
|
|
2274
2353
|
renderIfActiveConversation(true);
|
|
2275
2354
|
}
|
|
2355
|
+
if (eventName === "run:started") {
|
|
2356
|
+
state.activeStreamRunId = typeof payload.runId === "string" ? payload.runId : null;
|
|
2357
|
+
setStreaming(state.isStreaming);
|
|
2358
|
+
}
|
|
2276
2359
|
if (eventName === "tool:started") {
|
|
2277
2360
|
const toolName = payload.tool || "tool";
|
|
2278
2361
|
const startedActivity = addActiveActivityFromToolStart(
|
|
@@ -2418,19 +2501,14 @@ export const renderWebUiHtml = (options?: { agentName?: string }): string => {
|
|
|
2418
2501
|
renderIfActiveConversation(true);
|
|
2419
2502
|
}
|
|
2420
2503
|
if (eventName === "run:completed") {
|
|
2421
|
-
|
|
2504
|
+
finalizeAssistantMessage();
|
|
2422
2505
|
if (!assistantMessage.content || assistantMessage.content.length === 0) {
|
|
2423
2506
|
assistantMessage.content = String(payload.result?.response || "");
|
|
2424
2507
|
}
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
}
|
|
2430
|
-
if (assistantMessage._currentText.length > 0) {
|
|
2431
|
-
assistantMessage._sections.push({ type: "text", content: assistantMessage._currentText });
|
|
2432
|
-
assistantMessage._currentText = "";
|
|
2433
|
-
}
|
|
2508
|
+
renderIfActiveConversation(false);
|
|
2509
|
+
}
|
|
2510
|
+
if (eventName === "run:cancelled") {
|
|
2511
|
+
finalizeAssistantMessage();
|
|
2434
2512
|
renderIfActiveConversation(false);
|
|
2435
2513
|
}
|
|
2436
2514
|
if (eventName === "run:error") {
|
|
@@ -2451,7 +2529,31 @@ export const renderWebUiHtml = (options?: { agentName?: string }): string => {
|
|
|
2451
2529
|
}
|
|
2452
2530
|
await loadConversations();
|
|
2453
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
|
+
}
|
|
2454
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;
|
|
2455
2557
|
setStreaming(false);
|
|
2456
2558
|
elements.prompt.focus();
|
|
2457
2559
|
}
|
|
@@ -2554,6 +2656,13 @@ export const renderWebUiHtml = (options?: { agentName?: string }): string => {
|
|
|
2554
2656
|
|
|
2555
2657
|
elements.composer.addEventListener("submit", async (event) => {
|
|
2556
2658
|
event.preventDefault();
|
|
2659
|
+
if (state.isStreaming) {
|
|
2660
|
+
if (!state.activeStreamRunId) {
|
|
2661
|
+
return;
|
|
2662
|
+
}
|
|
2663
|
+
await stopActiveRun();
|
|
2664
|
+
return;
|
|
2665
|
+
}
|
|
2557
2666
|
const value = elements.prompt.value;
|
|
2558
2667
|
elements.prompt.value = "";
|
|
2559
2668
|
autoResizePrompt();
|