@poncho-ai/cli 0.30.7 → 0.31.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.
@@ -1345,10 +1345,11 @@ export const getWebUiClientScript = (markedSource: string): string => `
1345
1345
  } else if (willStream) {
1346
1346
  setStreaming(true);
1347
1347
  } else if (payload.needsContinuation && !payload.conversation.parentConversationId) {
1348
- console.log("[poncho] Detected orphaned continuation for", conversationId, "— auto-resuming");
1348
+ console.log("[poncho] Detected orphaned continuation for", conversationId, "— auto-resuming via /continue");
1349
1349
  (async () => {
1350
1350
  try {
1351
1351
  setStreaming(true);
1352
+ state.activeStreamConversationId = conversationId;
1352
1353
  var localMsgs = state.activeMessages || [];
1353
1354
  var contAssistant = {
1354
1355
  role: "assistant",
@@ -1365,30 +1366,36 @@ export const getWebUiClientScript = (markedSource: string): string => `
1365
1366
  state.activeMessages = localMsgs;
1366
1367
  state._activeStreamMessages = localMsgs;
1367
1368
  renderMessages(localMsgs, true);
1369
+
1368
1370
  var contResp = await fetch(
1369
- "/api/conversations/" + encodeURIComponent(conversationId) + "/messages",
1371
+ "/api/conversations/" + encodeURIComponent(conversationId) + "/continue",
1370
1372
  {
1371
1373
  method: "POST",
1372
1374
  credentials: "include",
1373
1375
  headers: { "Content-Type": "application/json", "x-csrf-token": state.csrfToken },
1374
- body: JSON.stringify({ continuation: true }),
1375
1376
  },
1376
1377
  );
1377
1378
  if (!contResp.ok || !contResp.body) {
1378
- contAssistant._error = "Failed to resume reload to retry";
1379
+ // Server already claimed the continuation (safety net). Poll for completion.
1380
+ await pollUntilRunIdle(conversationId);
1379
1381
  setStreaming(false);
1380
1382
  renderMessages(localMsgs, false);
1381
1383
  return;
1382
1384
  }
1383
- state.activeStreamConversationId = conversationId;
1385
+
1384
1386
  var contReader = contResp.body.getReader();
1385
1387
  var contDecoder = new TextDecoder();
1386
1388
  var contBuffer = "";
1389
+ var gotStreamEnd = false;
1387
1390
  while (true) {
1388
1391
  var chunk = await contReader.read();
1389
1392
  if (chunk.done) break;
1390
1393
  contBuffer += contDecoder.decode(chunk.value, { stream: true });
1391
1394
  contBuffer = parseSseChunk(contBuffer, function(evtName, evtPayload) {
1395
+ if (evtName === "stream:end") {
1396
+ gotStreamEnd = true;
1397
+ return;
1398
+ }
1392
1399
  if (evtName === "model:chunk" && evtPayload.content) {
1393
1400
  contAssistant.content = (contAssistant.content || "") + evtPayload.content;
1394
1401
  contAssistant._currentText += evtPayload.content;
@@ -1419,14 +1426,14 @@ export const getWebUiClientScript = (markedSource: string): string => `
1419
1426
  if (evtName === "run:error") {
1420
1427
  contAssistant._error = evtPayload.error?.message || "Something went wrong";
1421
1428
  }
1422
- if (evtName === "run:completed" && evtPayload.result?.continuation === true) {
1423
- // Another continuation needed — reload to pick it up
1424
- loadConversation(conversationId).catch(function() {});
1425
- }
1426
1429
  }
1427
1430
  renderMessages(localMsgs, true);
1428
1431
  });
1429
1432
  }
1433
+ if (gotStreamEnd) {
1434
+ // Safety net already claimed it. Poll for completion.
1435
+ await pollUntilRunIdle(conversationId);
1436
+ }
1430
1437
  setStreaming(false);
1431
1438
  renderMessages(localMsgs, false);
1432
1439
  await loadConversations();
@@ -2521,59 +2528,30 @@ export const getWebUiClientScript = (markedSource: string): string => `
2521
2528
  };
2522
2529
  let _totalSteps = 0;
2523
2530
  let _maxSteps = 0;
2524
- let _isContinuation = false;
2525
2531
  let _receivedTerminalEvent = false;
2526
- while (true) {
2527
2532
  let _shouldContinue = false;
2528
- let fetchOpts;
2529
- if (_isContinuation) {
2530
- fetchOpts = {
2531
- method: "POST",
2532
- credentials: "include",
2533
- headers: { "Content-Type": "application/json", "x-csrf-token": state.csrfToken },
2534
- body: JSON.stringify({ continuation: true }),
2535
- signal: streamAbortController.signal,
2536
- };
2537
- } else if (filesToSend.length > 0) {
2538
- const formData = new FormData();
2539
- formData.append("message", messageText);
2540
- for (const f of filesToSend) {
2541
- formData.append("files", f, f.name);
2542
- }
2543
- fetchOpts = {
2544
- method: "POST",
2545
- credentials: "include",
2546
- headers: { "x-csrf-token": state.csrfToken },
2547
- body: formData,
2548
- signal: streamAbortController.signal,
2549
- };
2550
- } else {
2551
- fetchOpts = {
2552
- method: "POST",
2553
- credentials: "include",
2554
- headers: { "Content-Type": "application/json", "x-csrf-token": state.csrfToken },
2555
- body: JSON.stringify({ message: messageText }),
2556
- signal: streamAbortController.signal,
2557
- };
2558
- }
2559
- const response = await fetch(
2560
- "/api/conversations/" + encodeURIComponent(conversationId) + "/messages",
2561
- fetchOpts,
2562
- );
2563
- if (!response.ok || !response.body) {
2564
- throw new Error("Failed to stream response");
2565
- }
2566
- const reader = response.body.getReader();
2567
- const decoder = new TextDecoder();
2568
- let buffer = "";
2569
- while (true) {
2570
- const { value, done } = await reader.read();
2571
- if (done) {
2572
- break;
2533
+
2534
+ // Helper to read an SSE stream from a fetch response
2535
+ const readSseStream = async (response) => {
2536
+ _shouldContinue = false;
2537
+ const reader = response.body.getReader();
2538
+ const decoder = new TextDecoder();
2539
+ let buffer = "";
2540
+ while (true) {
2541
+ const { value, done } = await reader.read();
2542
+ if (done) break;
2543
+ buffer += decoder.decode(value, { stream: true });
2544
+ buffer = parseSseChunk(buffer, (eventName, payload) => {
2545
+ try {
2546
+ handleSseEvent(eventName, payload);
2547
+ } catch (error) {
2548
+ console.error("SSE event handling error:", eventName, error);
2549
+ }
2550
+ });
2573
2551
  }
2574
- buffer += decoder.decode(value, { stream: true });
2575
- buffer = parseSseChunk(buffer, (eventName, payload) => {
2576
- try {
2552
+ };
2553
+
2554
+ const handleSseEvent = (eventName, payload) => {
2577
2555
  if (eventName === "model:chunk") {
2578
2556
  const chunk = String(payload.content || "");
2579
2557
  if (chunk.length > 0) clearResolvedApprovals(assistantMessage);
@@ -2857,6 +2835,12 @@ export const getWebUiClientScript = (markedSource: string): string => `
2857
2835
  if (typeof payload.result?.maxSteps === "number") _maxSteps = payload.result.maxSteps;
2858
2836
  if (payload.result?.continuation === true && (_maxSteps <= 0 || _totalSteps < _maxSteps)) {
2859
2837
  _shouldContinue = true;
2838
+ if (assistantMessage._currentTools.length > 0) {
2839
+ assistantMessage._sections.push({ type: "tools", content: assistantMessage._currentTools });
2840
+ assistantMessage._currentTools = [];
2841
+ }
2842
+ assistantMessage._activeActivities = [];
2843
+ renderIfActiveConversation(true);
2860
2844
  } else {
2861
2845
  finalizeAssistantMessage();
2862
2846
  if (!assistantMessage.content || assistantMessage.content.length === 0) {
@@ -2880,26 +2864,77 @@ export const getWebUiClientScript = (markedSource: string): string => `
2880
2864
  assistantMessage._error = errMsg;
2881
2865
  renderIfActiveConversation(false);
2882
2866
  }
2883
- } catch (error) {
2884
- console.error("SSE event handling error:", eventName, error);
2885
- }
2886
- });
2867
+ if (eventName === "stream:end") {
2868
+ // no-op: server signals empty continuation
2869
+ }
2870
+ };
2871
+
2872
+ // Initial message POST
2873
+ let fetchOpts;
2874
+ if (filesToSend.length > 0) {
2875
+ const formData = new FormData();
2876
+ formData.append("message", messageText);
2877
+ for (const f of filesToSend) {
2878
+ formData.append("files", f, f.name);
2879
+ }
2880
+ fetchOpts = {
2881
+ method: "POST",
2882
+ credentials: "include",
2883
+ headers: { "x-csrf-token": state.csrfToken },
2884
+ body: formData,
2885
+ signal: streamAbortController.signal,
2886
+ };
2887
+ } else {
2888
+ fetchOpts = {
2889
+ method: "POST",
2890
+ credentials: "include",
2891
+ headers: { "Content-Type": "application/json", "x-csrf-token": state.csrfToken },
2892
+ body: JSON.stringify({ message: messageText }),
2893
+ signal: streamAbortController.signal,
2894
+ };
2895
+ }
2896
+ const response = await fetch(
2897
+ "/api/conversations/" + encodeURIComponent(conversationId) + "/messages",
2898
+ fetchOpts,
2899
+ );
2900
+ if (!response.ok || !response.body) {
2901
+ throw new Error("Failed to stream response");
2902
+ }
2903
+ await readSseStream(response);
2904
+
2905
+ // Continuation loop: POST to /continue while the server signals more work
2906
+ while (_shouldContinue) {
2907
+ _shouldContinue = false;
2908
+ _receivedTerminalEvent = false;
2909
+ const contResponse = await fetch(
2910
+ "/api/conversations/" + encodeURIComponent(conversationId) + "/continue",
2911
+ {
2912
+ method: "POST",
2913
+ credentials: "include",
2914
+ headers: { "Content-Type": "application/json", "x-csrf-token": state.csrfToken },
2915
+ signal: streamAbortController.signal,
2916
+ },
2917
+ );
2918
+ if (!contResponse.ok || !contResponse.body) {
2919
+ // Server may have already handled continuation (safety net claimed it).
2920
+ // Fall back to polling for idle state.
2921
+ await pollUntilRunIdle(conversationId);
2922
+ break;
2923
+ }
2924
+ await readSseStream(contResponse);
2887
2925
  }
2888
- if (!_shouldContinue && !_receivedTerminalEvent) {
2926
+
2927
+ // If stream ended without terminal event and no continuation, check server
2928
+ if (!_receivedTerminalEvent && !_shouldContinue) {
2889
2929
  try {
2890
2930
  const recoveryPayload = await api("/api/conversations/" + encodeURIComponent(conversationId));
2891
- if (recoveryPayload.needsContinuation) {
2892
- _shouldContinue = true;
2893
- console.log("[poncho] Stream ended without terminal event, server has continuation — resuming");
2931
+ if (recoveryPayload.hasActiveRun || recoveryPayload.needsContinuation) {
2932
+ await pollUntilRunIdle(conversationId);
2894
2933
  }
2895
2934
  } catch (_recoverErr) {
2896
2935
  console.warn("[poncho] Recovery check failed after abrupt stream end");
2897
2936
  }
2898
2937
  }
2899
- if (!_shouldContinue) break;
2900
- _receivedTerminalEvent = false;
2901
- _isContinuation = true;
2902
- }
2903
2938
  // Update active state only if user is still on this conversation.
2904
2939
  if (state.activeConversationId === streamConversationId) {
2905
2940
  state.activeMessages = localMessages;