@poncho-ai/cli 0.30.0 → 0.30.2

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.
@@ -30,6 +30,8 @@ export const getWebUiClientScript = (markedSource: string): string => `
30
30
  parentConversationId: null,
31
31
  todos: [],
32
32
  todoPanelCollapsed: false,
33
+ cronSectionCollapsed: true,
34
+ cronShowAll: false,
33
35
  };
34
36
 
35
37
  const agentInitial = document.body.dataset.agentInitial || "A";
@@ -773,11 +775,86 @@ export const getWebUiClientScript = (markedSource: string): string => `
773
775
  }
774
776
  };
775
777
 
778
+ const cronCaretSvg = '<svg viewBox="0 0 12 12" fill="none" width="10" height="10"><path d="M4.5 2.75L8 6L4.5 9.25" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path></svg>';
779
+
780
+ const parseCronTitle = (title) => {
781
+ const rest = title.replace(/^\[cron\]\s*/, "");
782
+ const isoMatch = rest.match(/\s(\d{4}-\d{2}-\d{2}T[\d:.]+Z?)$/);
783
+ if (isoMatch) {
784
+ return { jobName: rest.slice(0, isoMatch.index).trim(), timestamp: isoMatch[1] };
785
+ }
786
+ return { jobName: rest, timestamp: "" };
787
+ };
788
+
789
+ const formatCronTimestamp = (isoStr) => {
790
+ if (!isoStr) return "";
791
+ try {
792
+ const d = new Date(isoStr);
793
+ if (isNaN(d.getTime())) return isoStr;
794
+ return d.toLocaleString(undefined, { month: "short", day: "numeric", hour: "numeric", minute: "2-digit" });
795
+ } catch { return isoStr; }
796
+ };
797
+
798
+ const CRON_PAGE_SIZE = 20;
799
+
800
+ const appendCronSection = (cronConvs, needsDivider) => {
801
+ if (needsDivider) {
802
+ const divider = document.createElement("div");
803
+ divider.className = "sidebar-section-divider";
804
+ elements.list.appendChild(divider);
805
+ }
806
+
807
+ cronConvs.sort((a, b) => (b.updatedAt || b.createdAt || 0) - (a.updatedAt || a.createdAt || 0));
808
+
809
+ const isOpen = !state.cronSectionCollapsed;
810
+ const header = document.createElement("div");
811
+ header.className = "cron-section-header";
812
+ header.innerHTML =
813
+ '<span class="cron-section-caret' + (isOpen ? ' open' : '') + '">' + cronCaretSvg + '</span>' +
814
+ '<span>Cron jobs</span>' +
815
+ '<span class="cron-section-count">' + cronConvs.length + '</span>';
816
+ header.onclick = () => {
817
+ state.cronSectionCollapsed = !state.cronSectionCollapsed;
818
+ state.cronShowAll = false;
819
+ renderConversationList();
820
+ };
821
+ elements.list.appendChild(header);
822
+
823
+ if (state.cronSectionCollapsed) return;
824
+
825
+ const limit = state.cronShowAll ? cronConvs.length : CRON_PAGE_SIZE;
826
+ const visible = cronConvs.slice(0, limit);
827
+
828
+ for (const c of visible) {
829
+ const { jobName, timestamp } = parseCronTitle(c.title);
830
+ const fmtTime = formatCronTimestamp(timestamp);
831
+ const displayTitle = fmtTime ? jobName + " \\u00b7 " + fmtTime : c.title;
832
+ elements.list.appendChild(buildConversationItem(Object.assign({}, c, { title: displayTitle })));
833
+ appendSubagentsIfActive(c.conversationId);
834
+ }
835
+
836
+ if (!state.cronShowAll && cronConvs.length > CRON_PAGE_SIZE) {
837
+ const remaining = cronConvs.length - CRON_PAGE_SIZE;
838
+ const viewMore = document.createElement("div");
839
+ viewMore.className = "cron-view-more";
840
+ viewMore.textContent = "View " + remaining + " more\\u2026";
841
+ viewMore.onclick = () => {
842
+ state.cronShowAll = true;
843
+ renderConversationList();
844
+ };
845
+ elements.list.appendChild(viewMore);
846
+ }
847
+ };
848
+
776
849
  const renderConversationList = () => {
777
850
  elements.list.innerHTML = "";
778
851
  const pending = state.conversations.filter(c => c.hasPendingApprovals);
779
852
  const rest = state.conversations.filter(c => !c.hasPendingApprovals);
780
853
 
854
+ const isCron = (c) => c.title && c.title.startsWith("[cron]");
855
+ const cronConvs = rest.filter(isCron);
856
+ const nonCron = rest.filter(c => !isCron(c));
857
+
781
858
  if (pending.length > 0) {
782
859
  const label = document.createElement("div");
783
860
  label.className = "sidebar-section-label";
@@ -796,7 +873,7 @@ export const getWebUiClientScript = (markedSource: string): string => `
796
873
  const latest = [];
797
874
  const previous7 = [];
798
875
  const older = [];
799
- for (const c of rest) {
876
+ for (const c of nonCron) {
800
877
  const ts = c.updatedAt || c.createdAt || 0;
801
878
  if (ts >= startOfToday) {
802
879
  latest.push(c);
@@ -808,6 +885,12 @@ export const getWebUiClientScript = (markedSource: string): string => `
808
885
  }
809
886
 
810
887
  let sectionRendered = pending.length > 0;
888
+
889
+ if (cronConvs.length > 0) {
890
+ appendCronSection(cronConvs, sectionRendered);
891
+ sectionRendered = true;
892
+ }
893
+
811
894
  const appendSection = (items, labelText) => {
812
895
  if (items.length === 0) return;
813
896
  if (sectionRendered) {
@@ -1261,6 +1344,101 @@ export const getWebUiClientScript = (markedSource: string): string => `
1261
1344
  });
1262
1345
  } else if (willStream) {
1263
1346
  setStreaming(true);
1347
+ } else if (payload.needsContinuation && !payload.conversation.parentConversationId) {
1348
+ console.log("[poncho] Detected orphaned continuation for", conversationId, "— auto-resuming");
1349
+ (async () => {
1350
+ try {
1351
+ setStreaming(true);
1352
+ var localMsgs = state.activeMessages || [];
1353
+ var contAssistant = {
1354
+ role: "assistant",
1355
+ content: "",
1356
+ _sections: [],
1357
+ _currentText: "",
1358
+ _currentTools: [],
1359
+ _toolImages: [],
1360
+ _activeActivities: [],
1361
+ _pendingApprovals: [],
1362
+ metadata: { toolActivity: [] }
1363
+ };
1364
+ localMsgs.push(contAssistant);
1365
+ state.activeMessages = localMsgs;
1366
+ state._activeStreamMessages = localMsgs;
1367
+ renderMessages(localMsgs, true);
1368
+ var contResp = await fetch(
1369
+ "/api/conversations/" + encodeURIComponent(conversationId) + "/messages",
1370
+ {
1371
+ method: "POST",
1372
+ credentials: "include",
1373
+ headers: { "Content-Type": "application/json", "x-csrf-token": state.csrfToken },
1374
+ body: JSON.stringify({ continuation: true }),
1375
+ },
1376
+ );
1377
+ if (!contResp.ok || !contResp.body) {
1378
+ contAssistant._error = "Failed to resume — reload to retry";
1379
+ setStreaming(false);
1380
+ renderMessages(localMsgs, false);
1381
+ return;
1382
+ }
1383
+ state.activeStreamConversationId = conversationId;
1384
+ var contReader = contResp.body.getReader();
1385
+ var contDecoder = new TextDecoder();
1386
+ var contBuffer = "";
1387
+ while (true) {
1388
+ var chunk = await contReader.read();
1389
+ if (chunk.done) break;
1390
+ contBuffer += contDecoder.decode(chunk.value, { stream: true });
1391
+ contBuffer = parseSseChunk(contBuffer, function(evtName, evtPayload) {
1392
+ if (evtName === "model:chunk" && evtPayload.content) {
1393
+ contAssistant.content = (contAssistant.content || "") + evtPayload.content;
1394
+ contAssistant._currentText += evtPayload.content;
1395
+ }
1396
+ if (evtName === "tool:started") {
1397
+ if (contAssistant._currentText) {
1398
+ contAssistant._sections.push({ type: "text", content: contAssistant._currentText });
1399
+ contAssistant._currentText = "";
1400
+ }
1401
+ contAssistant._currentTools.push("- start \`" + evtPayload.tool + "\`");
1402
+ }
1403
+ if (evtName === "tool:completed") {
1404
+ contAssistant._currentTools.push("- done \`" + evtPayload.tool + "\` (" + evtPayload.duration + "ms)");
1405
+ }
1406
+ if (evtName === "tool:error") {
1407
+ contAssistant._currentTools.push("- error \`" + evtPayload.tool + "\`: " + evtPayload.error);
1408
+ }
1409
+ if (evtName === "run:completed" || evtName === "run:error" || evtName === "run:cancelled") {
1410
+ if (contAssistant._currentTools.length > 0) {
1411
+ contAssistant._sections.push({ type: "tools", content: contAssistant._currentTools });
1412
+ contAssistant._currentTools = [];
1413
+ }
1414
+ if (contAssistant._currentText) {
1415
+ contAssistant._sections.push({ type: "text", content: contAssistant._currentText });
1416
+ contAssistant._currentText = "";
1417
+ }
1418
+ contAssistant._activeActivities = [];
1419
+ if (evtName === "run:error") {
1420
+ contAssistant._error = evtPayload.error?.message || "Something went wrong";
1421
+ }
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
+ }
1427
+ renderMessages(localMsgs, true);
1428
+ });
1429
+ }
1430
+ setStreaming(false);
1431
+ renderMessages(localMsgs, false);
1432
+ await loadConversations();
1433
+ } catch (contErr) {
1434
+ console.error("[poncho] Auto-continuation failed:", contErr);
1435
+ setStreaming(false);
1436
+ await loadConversation(conversationId).catch(function() {});
1437
+ } finally {
1438
+ state.activeStreamConversationId = null;
1439
+ state._activeStreamMessages = null;
1440
+ }
1441
+ })();
1264
1442
  }
1265
1443
  };
1266
1444
 
@@ -2344,6 +2522,7 @@ export const getWebUiClientScript = (markedSource: string): string => `
2344
2522
  let _totalSteps = 0;
2345
2523
  let _maxSteps = 0;
2346
2524
  let _isContinuation = false;
2525
+ let _receivedTerminalEvent = false;
2347
2526
  while (true) {
2348
2527
  let _shouldContinue = false;
2349
2528
  let fetchOpts;
@@ -2673,6 +2852,7 @@ export const getWebUiClientScript = (markedSource: string): string => `
2673
2852
  }
2674
2853
  }
2675
2854
  if (eventName === "run:completed") {
2855
+ _receivedTerminalEvent = true;
2676
2856
  _totalSteps += typeof payload.result?.steps === "number" ? payload.result.steps : 0;
2677
2857
  if (typeof payload.result?.maxSteps === "number") _maxSteps = payload.result.maxSteps;
2678
2858
  if (payload.result?.continuation === true && (_maxSteps <= 0 || _totalSteps < _maxSteps)) {
@@ -2689,10 +2869,12 @@ export const getWebUiClientScript = (markedSource: string): string => `
2689
2869
  }
2690
2870
  }
2691
2871
  if (eventName === "run:cancelled") {
2872
+ _receivedTerminalEvent = true;
2692
2873
  finalizeAssistantMessage();
2693
2874
  renderIfActiveConversation(false);
2694
2875
  }
2695
2876
  if (eventName === "run:error") {
2877
+ _receivedTerminalEvent = true;
2696
2878
  finalizeAssistantMessage();
2697
2879
  const errMsg = payload.error?.message || "Something went wrong";
2698
2880
  assistantMessage._error = errMsg;
@@ -2703,7 +2885,19 @@ export const getWebUiClientScript = (markedSource: string): string => `
2703
2885
  }
2704
2886
  });
2705
2887
  }
2888
+ if (!_shouldContinue && !_receivedTerminalEvent) {
2889
+ try {
2890
+ 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");
2894
+ }
2895
+ } catch (_recoverErr) {
2896
+ console.warn("[poncho] Recovery check failed after abrupt stream end");
2897
+ }
2898
+ }
2706
2899
  if (!_shouldContinue) break;
2900
+ _receivedTerminalEvent = false;
2707
2901
  _isContinuation = true;
2708
2902
  }
2709
2903
  // Update active state only if user is still on this conversation.
@@ -2977,7 +3171,7 @@ export const getWebUiClientScript = (markedSource: string): string => `
2977
3171
  state: "resolved",
2978
3172
  resolvedDecision: decision,
2979
3173
  }));
2980
- api("/api/approvals/" + encodeURIComponent(approvalId), {
3174
+ return api("/api/approvals/" + encodeURIComponent(approvalId), {
2981
3175
  method: "POST",
2982
3176
  body: JSON.stringify({ approved: decision === "approve" }),
2983
3177
  }).catch((error) => {
@@ -3025,16 +3219,54 @@ export const getWebUiClientScript = (markedSource: string): string => `
3025
3219
  if (pending.length === 0) return;
3026
3220
  const wasStreaming = state.isStreaming;
3027
3221
  if (!wasStreaming) setStreaming(true);
3028
- pending.forEach((aid) => submitApproval(aid, decision));
3222
+ // Mark all items as resolved in the UI immediately
3223
+ for (const aid of pending) {
3224
+ state.approvalRequestsInFlight[aid] = true;
3225
+ updatePendingApproval(aid, (request) => ({
3226
+ ...request,
3227
+ state: "resolved",
3228
+ resolvedDecision: decision,
3229
+ }));
3230
+ }
3029
3231
  renderMessages(state.activeMessages, state.isStreaming);
3030
3232
  loadConversations();
3031
- if (!wasStreaming && state.activeConversationId) {
3032
- const cid = state.activeConversationId;
3033
- await streamConversationEvents(cid, { liveOnly: true });
3034
- if (state.activeConversationId === cid) {
3035
- pollUntilRunIdle(cid);
3036
- }
3233
+ const streamCid = !wasStreaming && state.activeConversationId
3234
+ ? state.activeConversationId
3235
+ : null;
3236
+ if (streamCid) {
3237
+ streamConversationEvents(streamCid, { liveOnly: true }).finally(() => {
3238
+ if (state.activeConversationId === streamCid) {
3239
+ pollUntilRunIdle(streamCid);
3240
+ }
3241
+ });
3037
3242
  }
3243
+ // Send API calls sequentially so each store write completes
3244
+ // before the next read (avoids last-writer-wins in serverless).
3245
+ void (async () => {
3246
+ for (const aid of pending) {
3247
+ await api("/api/approvals/" + encodeURIComponent(aid), {
3248
+ method: "POST",
3249
+ body: JSON.stringify({ approved: decision === "approve" }),
3250
+ }).catch((error) => {
3251
+ const isStale = error && error.payload && error.payload.code === "APPROVAL_NOT_FOUND";
3252
+ if (isStale) {
3253
+ updatePendingApproval(aid, () => null);
3254
+ } else {
3255
+ const errMsg = error instanceof Error ? error.message : String(error);
3256
+ updatePendingApproval(aid, (request) => ({
3257
+ ...request,
3258
+ state: "pending",
3259
+ pendingDecision: null,
3260
+ resolvedDecision: null,
3261
+ _error: errMsg,
3262
+ }));
3263
+ }
3264
+ renderMessages(state.activeMessages, state.isStreaming);
3265
+ }).finally(() => {
3266
+ delete state.approvalRequestsInFlight[aid];
3267
+ });
3268
+ }
3269
+ })();
3038
3270
  return;
3039
3271
  }
3040
3272
 
@@ -401,6 +401,41 @@ export const WEB_UI_STYLES = `
401
401
  .conversation-item .delete-btn.confirming:hover {
402
402
  color: var(--error-alt);
403
403
  }
404
+ .cron-section-header {
405
+ display: flex;
406
+ align-items: center;
407
+ gap: 6px;
408
+ padding: 8px 10px 4px;
409
+ cursor: pointer;
410
+ font-size: 11px;
411
+ font-weight: 600;
412
+ color: var(--fg-7);
413
+ text-transform: uppercase;
414
+ letter-spacing: 0.04em;
415
+ user-select: none;
416
+ transition: color 0.15s;
417
+ }
418
+ .cron-section-header:hover { color: var(--fg-5); }
419
+ .cron-section-caret {
420
+ display: inline-flex;
421
+ transition: transform 0.15s;
422
+ }
423
+ .cron-section-caret.open { transform: rotate(90deg); }
424
+ .cron-section-count {
425
+ font-weight: 400;
426
+ color: var(--fg-8);
427
+ font-size: 11px;
428
+ }
429
+ .cron-view-more {
430
+ padding: 6px 10px;
431
+ font-size: 12px;
432
+ color: var(--fg-7);
433
+ cursor: pointer;
434
+ text-align: center;
435
+ transition: color 0.15s;
436
+ user-select: none;
437
+ }
438
+ .cron-view-more:hover { color: var(--fg-3); }
404
439
  .sidebar-footer {
405
440
  margin-top: auto;
406
441
  padding-top: 8px;
@@ -1572,6 +1607,8 @@ export const WEB_UI_STYLES = `
1572
1607
  line-height: 1.45;
1573
1608
  color: var(--fg-tool-code);
1574
1609
  width: 100%;
1610
+ min-width: 0;
1611
+ overflow: hidden;
1575
1612
  }
1576
1613
  .subagent-result-summary {
1577
1614
  list-style: none;
@@ -1616,6 +1653,19 @@ export const WEB_UI_STYLES = `
1616
1653
  display: grid;
1617
1654
  gap: 6px;
1618
1655
  padding: 0 12px 10px;
1656
+ min-width: 0;
1657
+ overflow-x: auto;
1658
+ overflow-wrap: break-word;
1659
+ word-break: break-word;
1660
+ }
1661
+ .subagent-result-body pre {
1662
+ max-width: 100%;
1663
+ overflow-x: auto;
1664
+ }
1665
+ .subagent-result-body table {
1666
+ max-width: 100%;
1667
+ overflow-x: auto;
1668
+ display: block;
1619
1669
  }
1620
1670
 
1621
1671
  /* Todo panel — inside composer-inner, above the input shell */