@poncho-ai/cli 0.24.1 → 0.24.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.
@@ -1,5 +1,5 @@
1
1
 
2
- > @poncho-ai/cli@0.24.1 build /home/runner/work/poncho-ai/poncho-ai/packages/cli
2
+ > @poncho-ai/cli@0.24.2 build /home/runner/work/poncho-ai/poncho-ai/packages/cli
3
3
  > tsup src/index.ts src/cli.ts --format esm --dts
4
4
 
5
5
  CLI Building entry: src/cli.ts, src/index.ts
@@ -9,10 +9,10 @@
9
9
  ESM Build start
10
10
  ESM dist/cli.js 94.00 B
11
11
  ESM dist/index.js 857.00 B
12
- ESM dist/run-interactive-ink-IEB4MZ2C.js 56.74 KB
13
- ESM dist/chunk-3ETNDULB.js 400.75 KB
14
- ESM ⚡️ Build success in 57ms
12
+ ESM dist/run-interactive-ink-REIUGQ5X.js 56.74 KB
13
+ ESM dist/chunk-UTZB2CS7.js 404.26 KB
14
+ ESM ⚡️ Build success in 65ms
15
15
  DTS Build start
16
- DTS ⚡️ Build success in 3876ms
16
+ DTS ⚡️ Build success in 3948ms
17
17
  DTS dist/cli.d.ts 20.00 B
18
18
  DTS dist/index.d.ts 3.59 KB
package/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # @poncho-ai/cli
2
2
 
3
+ ## 0.24.2
4
+
5
+ ### Patch Changes
6
+
7
+ - [`70c4cfc`](https://github.com/cesr/poncho-ai/commit/70c4cfcb8d70e8b382157a82f2dc341bf526226b) Thanks [@cesr](https://github.com/cesr)! - Improve tool approval UX: optimistic approve/deny, fix browser panel not opening after approval, and restore real-time SSE streaming for resumed runs.
8
+
9
+ - [`ab4c1cb`](https://github.com/cesr/poncho-ai/commit/ab4c1cb0729a68ba0f296fd37380b5c228abfb5b) Thanks [@cesr](https://github.com/cesr)! - Fix browser hangs during long conversations in the web UI by throttling streaming renders with requestAnimationFrame and caching markdown parse output.
10
+
3
11
  ## 0.24.1
4
12
 
5
13
  ### Patch Changes
@@ -866,6 +866,25 @@ var WEB_UI_STYLES = `
866
866
  border-color: var(--deny-border);
867
867
  color: var(--deny);
868
868
  }
869
+ .approval-request-item.resolved {
870
+ opacity: 0.7;
871
+ }
872
+ .approval-resolved-status {
873
+ font-size: 12px;
874
+ font-weight: 600;
875
+ letter-spacing: 0.04em;
876
+ }
877
+ .approval-resolved-status code {
878
+ font-family: ui-monospace, "SF Mono", "Fira Code", monospace;
879
+ letter-spacing: 0;
880
+ color: var(--fg-strong);
881
+ }
882
+ .approval-resolved-status.approve {
883
+ color: var(--approve);
884
+ }
885
+ .approval-resolved-status.deny {
886
+ color: var(--deny);
887
+ }
869
888
  .user-bubble {
870
889
  background: var(--bg-elevated);
871
890
  border: 1px solid var(--border-2);
@@ -1735,15 +1754,23 @@ var getWebUiClientScript = (markedSource2) => `
1735
1754
  .replace(/"/g, """)
1736
1755
  .replace(/'/g, "'");
1737
1756
 
1757
+ const _mdCache = new Map();
1738
1758
  const renderAssistantMarkdown = (value) => {
1739
1759
  const source = String(value || "").trim();
1740
1760
  if (!source) return "<p></p>";
1741
1761
 
1762
+ const cached = _mdCache.get(source);
1763
+ if (cached !== undefined) return cached;
1764
+
1742
1765
  try {
1743
- return marked.parse(source);
1766
+ const result = marked.parse(source);
1767
+ _mdCache.set(source, result);
1768
+ if (_mdCache.size > 500) {
1769
+ _mdCache.delete(_mdCache.keys().next().value);
1770
+ }
1771
+ return result;
1744
1772
  } catch (error) {
1745
1773
  console.error("Markdown parsing error:", error);
1746
- // Fallback to escaped text
1747
1774
  return "<p>" + escapeHtml(source) + "</p>";
1748
1775
  }
1749
1776
  };
@@ -1834,15 +1861,26 @@ var getWebUiClientScript = (markedSource2) => `
1834
1861
  const approvalId = typeof req.approvalId === "string" ? req.approvalId : "";
1835
1862
  const tool = typeof req.tool === "string" ? req.tool : "tool";
1836
1863
  const input = req.input != null ? req.input : {};
1864
+ const subagentLabel = req._subagentLabel
1865
+ ? ' <span style="color: var(--text-3); font-size: 11px;">(from ' + escapeHtml(req._subagentLabel) + ')</span>'
1866
+ : "";
1867
+ if (req.state === "resolved") {
1868
+ const isApproved = req.resolvedDecision === "approve";
1869
+ const label = isApproved ? "Approved" : "Denied";
1870
+ const cls = isApproved ? "approve" : "deny";
1871
+ return (
1872
+ '<div class="approval-request-item resolved">' +
1873
+ '<div class="approval-resolved-status ' + cls + '">' + label + ': <code>' +
1874
+ escapeHtml(tool) + "</code>" + subagentLabel + "</div>" +
1875
+ "</div>"
1876
+ );
1877
+ }
1837
1878
  const submitting = req.state === "submitting";
1838
1879
  const approveLabel = submitting && req.pendingDecision === "approve" ? "Approving..." : "Approve";
1839
1880
  const denyLabel = submitting && req.pendingDecision === "deny" ? "Denying..." : "Deny";
1840
1881
  const errorHtml = req._error
1841
1882
  ? '<div style="color: var(--deny); font-size: 11px; margin-top: 4px;">Submit failed: ' + escapeHtml(req._error) + "</div>"
1842
1883
  : "";
1843
- const subagentLabel = req._subagentLabel
1844
- ? ' <span style="color: var(--text-3); font-size: 11px;">(from ' + escapeHtml(req._subagentLabel) + ')</span>'
1845
- : "";
1846
1884
  return (
1847
1885
  '<div class="approval-request-item">' +
1848
1886
  '<div class="approval-requests-label">Approval required: <code>' +
@@ -1870,10 +1908,11 @@ var getWebUiClientScript = (markedSource2) => `
1870
1908
  );
1871
1909
  })
1872
1910
  .join("");
1873
- const batchButtons = requests.length > 1
1911
+ const actionableCount = requests.filter((r) => r.state !== "resolved").length;
1912
+ const batchButtons = actionableCount > 1
1874
1913
  ? '<div class="approval-batch-actions">' +
1875
- '<button class="approval-batch-btn approve" data-approval-batch="approve">Approve all (' + requests.length + ')</button>' +
1876
- '<button class="approval-batch-btn deny" data-approval-batch="deny">Deny all (' + requests.length + ')</button>' +
1914
+ '<button class="approval-batch-btn approve" data-approval-batch="approve">Approve all (' + actionableCount + ')</button>' +
1915
+ '<button class="approval-batch-btn deny" data-approval-batch="deny">Deny all (' + actionableCount + ')</button>' +
1877
1916
  "</div>"
1878
1917
  : "";
1879
1918
  return (
@@ -2002,6 +2041,14 @@ var getWebUiClientScript = (markedSource2) => `
2002
2041
  return false;
2003
2042
  };
2004
2043
 
2044
+ const clearResolvedApprovals = (message) => {
2045
+ if (Array.isArray(message._pendingApprovals)) {
2046
+ message._pendingApprovals = message._pendingApprovals.filter(
2047
+ (req) => req.state !== "resolved",
2048
+ );
2049
+ }
2050
+ };
2051
+
2005
2052
  const toUiPendingApprovals = (pendingApprovals) => {
2006
2053
  if (!Array.isArray(pendingApprovals)) {
2007
2054
  return [];
@@ -2394,12 +2441,16 @@ var getWebUiClientScript = (markedSource2) => `
2394
2441
  );
2395
2442
  }
2396
2443
  });
2397
- // While streaming, show current tools if any
2398
- if (isStreaming && i === messages.length - 1 && m._currentTools && m._currentTools.length > 0) {
2399
- content.insertAdjacentHTML(
2400
- "beforeend",
2401
- renderToolActivity(m._currentTools, m._pendingApprovals || [], m._toolImages || []),
2402
- );
2444
+ // While streaming, show current tools and/or pending approvals
2445
+ if (isStreaming && i === messages.length - 1) {
2446
+ const hasCurrentTools = m._currentTools && m._currentTools.length > 0;
2447
+ const hasStreamApprovals = Array.isArray(m._pendingApprovals) && m._pendingApprovals.length > 0;
2448
+ if (hasCurrentTools || hasStreamApprovals) {
2449
+ content.insertAdjacentHTML(
2450
+ "beforeend",
2451
+ renderToolActivity(m._currentTools || [], m._pendingApprovals || [], m._toolImages || []),
2452
+ );
2453
+ }
2403
2454
  }
2404
2455
  // When reloading with unresolved approvals, show them even when not streaming
2405
2456
  if (!isStreaming && pendingApprovals.length > 0 && lastToolsSectionIndex < 0) {
@@ -2675,6 +2726,7 @@ var getWebUiClientScript = (markedSource2) => `
2675
2726
  renderMessages(state.activeMessages, payload.hasActiveRun);
2676
2727
  }
2677
2728
  if (payload.hasActiveRun) {
2729
+ if (window._connectBrowserStream) window._connectBrowserStream();
2678
2730
  setTimeout(poll, 2000);
2679
2731
  } else {
2680
2732
  setStreaming(false);
@@ -2692,12 +2744,24 @@ var getWebUiClientScript = (markedSource2) => `
2692
2744
  const liveOnly = options && options.liveOnly;
2693
2745
  return new Promise((resolve) => {
2694
2746
  const localMessages = state.activeMessages || [];
2747
+ let _rafId = 0;
2695
2748
  const renderIfActiveConversation = (streaming) => {
2696
2749
  if (state.activeConversationId !== conversationId) {
2697
2750
  return;
2698
2751
  }
2699
2752
  state.activeMessages = localMessages;
2700
- renderMessages(localMessages, streaming);
2753
+ if (!streaming) {
2754
+ if (_rafId) { cancelAnimationFrame(_rafId); _rafId = 0; }
2755
+ renderMessages(localMessages, false);
2756
+ return;
2757
+ }
2758
+ if (!_rafId) {
2759
+ _rafId = requestAnimationFrame(() => {
2760
+ _rafId = 0;
2761
+ if (state.activeConversationId !== conversationId) return;
2762
+ renderMessages(localMessages, true);
2763
+ });
2764
+ }
2701
2765
  };
2702
2766
  let assistantMessage = localMessages[localMessages.length - 1];
2703
2767
  if (!assistantMessage || assistantMessage.role !== "assistant") {
@@ -2766,6 +2830,7 @@ var getWebUiClientScript = (markedSource2) => `
2766
2830
  }
2767
2831
  if (eventName === "model:chunk") {
2768
2832
  const chunk = String(payload.content || "");
2833
+ if (chunk.length > 0) clearResolvedApprovals(assistantMessage);
2769
2834
  if (assistantMessage._currentTools.length > 0 && chunk.length > 0) {
2770
2835
  assistantMessage._sections.push({
2771
2836
  type: "tools",
@@ -2807,6 +2872,7 @@ var getWebUiClientScript = (markedSource2) => `
2807
2872
  renderIfActiveConversation(true);
2808
2873
  }
2809
2874
  if (eventName === "tool:started") {
2875
+ clearResolvedApprovals(assistantMessage);
2810
2876
  const toolName = payload.tool || "tool";
2811
2877
  removeActiveActivityForTool(assistantMessage, toolName);
2812
2878
  const startedActivity = addActiveActivityFromToolStart(
@@ -2961,7 +3027,7 @@ var getWebUiClientScript = (markedSource2) => `
2961
3027
  typeof payload.approvalId === "string" ? payload.approvalId : "";
2962
3028
  if (approvalId && Array.isArray(assistantMessage._pendingApprovals)) {
2963
3029
  assistantMessage._pendingApprovals = assistantMessage._pendingApprovals.filter(
2964
- (req) => req.approvalId !== approvalId,
3030
+ (req) => req.approvalId !== approvalId || req.state === "resolved",
2965
3031
  );
2966
3032
  }
2967
3033
  renderIfActiveConversation(true);
@@ -2974,7 +3040,7 @@ var getWebUiClientScript = (markedSource2) => `
2974
3040
  typeof payload.approvalId === "string" ? payload.approvalId : "";
2975
3041
  if (approvalId && Array.isArray(assistantMessage._pendingApprovals)) {
2976
3042
  assistantMessage._pendingApprovals = assistantMessage._pendingApprovals.filter(
2977
- (req) => req.approvalId !== approvalId,
3043
+ (req) => req.approvalId !== approvalId || req.state === "resolved",
2978
3044
  );
2979
3045
  }
2980
3046
  renderIfActiveConversation(true);
@@ -3511,12 +3577,24 @@ var getWebUiClientScript = (markedSource2) => `
3511
3577
  }
3512
3578
  state.activeStreamConversationId = conversationId;
3513
3579
  const streamConversationId = conversationId;
3580
+ let _rafId = 0;
3514
3581
  const renderIfActiveConversation = (streaming) => {
3515
3582
  if (state.activeConversationId !== streamConversationId) {
3516
3583
  return;
3517
3584
  }
3518
3585
  state.activeMessages = localMessages;
3519
- renderMessages(localMessages, streaming);
3586
+ if (!streaming) {
3587
+ if (_rafId) { cancelAnimationFrame(_rafId); _rafId = 0; }
3588
+ renderMessages(localMessages, false);
3589
+ return;
3590
+ }
3591
+ if (!_rafId) {
3592
+ _rafId = requestAnimationFrame(() => {
3593
+ _rafId = 0;
3594
+ if (state.activeConversationId !== streamConversationId) return;
3595
+ renderMessages(localMessages, true);
3596
+ });
3597
+ }
3520
3598
  };
3521
3599
  const finalizeAssistantMessage = () => {
3522
3600
  assistantMessage._activeActivities = [];
@@ -3577,7 +3655,7 @@ var getWebUiClientScript = (markedSource2) => `
3577
3655
  try {
3578
3656
  if (eventName === "model:chunk") {
3579
3657
  const chunk = String(payload.content || "");
3580
- // If we have tools accumulated and text starts again, push tools as a section
3658
+ if (chunk.length > 0) clearResolvedApprovals(assistantMessage);
3581
3659
  if (assistantMessage._currentTools.length > 0 && chunk.length > 0) {
3582
3660
  assistantMessage._sections.push({ type: "tools", content: assistantMessage._currentTools });
3583
3661
  assistantMessage._currentTools = [];
@@ -3621,6 +3699,7 @@ var getWebUiClientScript = (markedSource2) => `
3621
3699
  renderIfActiveConversation(true);
3622
3700
  }
3623
3701
  if (eventName === "tool:started") {
3702
+ clearResolvedApprovals(assistantMessage);
3624
3703
  const toolName = payload.tool || "tool";
3625
3704
  removeActiveActivityForTool(assistantMessage, toolName);
3626
3705
  const startedActivity = addActiveActivityFromToolStart(
@@ -3775,7 +3854,7 @@ var getWebUiClientScript = (markedSource2) => `
3775
3854
  typeof payload.approvalId === "string" ? payload.approvalId : "";
3776
3855
  if (approvalId && Array.isArray(assistantMessage._pendingApprovals)) {
3777
3856
  assistantMessage._pendingApprovals = assistantMessage._pendingApprovals.filter(
3778
- (req) => req.approvalId !== approvalId,
3857
+ (req) => req.approvalId !== approvalId || req.state === "resolved",
3779
3858
  );
3780
3859
  }
3781
3860
  renderIfActiveConversation(true);
@@ -3790,7 +3869,7 @@ var getWebUiClientScript = (markedSource2) => `
3790
3869
  typeof payload.approvalId === "string" ? payload.approvalId : "";
3791
3870
  if (approvalId && Array.isArray(assistantMessage._pendingApprovals)) {
3792
3871
  assistantMessage._pendingApprovals = assistantMessage._pendingApprovals.filter(
3793
- (req) => req.approvalId !== approvalId,
3872
+ (req) => req.approvalId !== approvalId || req.state === "resolved",
3794
3873
  );
3795
3874
  }
3796
3875
  renderIfActiveConversation(true);
@@ -4119,21 +4198,17 @@ var getWebUiClientScript = (markedSource2) => `
4119
4198
  openLightbox(img.src);
4120
4199
  });
4121
4200
 
4122
- const submitApproval = async (approvalId, decision, opts) => {
4123
- const wasStreaming = opts && opts.wasStreaming;
4201
+ const submitApproval = (approvalId, decision) => {
4124
4202
  state.approvalRequestsInFlight[approvalId] = true;
4125
4203
  updatePendingApproval(approvalId, (request) => ({
4126
4204
  ...request,
4127
- state: "submitting",
4128
- pendingDecision: decision,
4205
+ state: "resolved",
4206
+ resolvedDecision: decision,
4129
4207
  }));
4130
- try {
4131
- await api("/api/approvals/" + encodeURIComponent(approvalId), {
4132
- method: "POST",
4133
- body: JSON.stringify({ approved: decision === "approve" }),
4134
- });
4135
- updatePendingApproval(approvalId, () => null);
4136
- } catch (error) {
4208
+ api("/api/approvals/" + encodeURIComponent(approvalId), {
4209
+ method: "POST",
4210
+ body: JSON.stringify({ approved: decision === "approve" }),
4211
+ }).catch((error) => {
4137
4212
  const isStale = error && error.payload && error.payload.code === "APPROVAL_NOT_FOUND";
4138
4213
  if (isStale) {
4139
4214
  updatePendingApproval(approvalId, () => null);
@@ -4143,12 +4218,14 @@ var getWebUiClientScript = (markedSource2) => `
4143
4218
  ...request,
4144
4219
  state: "pending",
4145
4220
  pendingDecision: null,
4221
+ resolvedDecision: null,
4146
4222
  _error: errMsg,
4147
4223
  }));
4148
4224
  }
4149
- } finally {
4225
+ renderMessages(state.activeMessages, state.isStreaming);
4226
+ }).finally(() => {
4150
4227
  delete state.approvalRequestsInFlight[approvalId];
4151
- }
4228
+ });
4152
4229
  };
4153
4230
 
4154
4231
  elements.messages.addEventListener("click", async (event) => {
@@ -4167,7 +4244,7 @@ var getWebUiClientScript = (markedSource2) => `
4167
4244
  for (const m of messages) {
4168
4245
  if (Array.isArray(m._pendingApprovals)) {
4169
4246
  for (const req of m._pendingApprovals) {
4170
- if (req.approvalId && req.state !== "submitting" && !state.approvalRequestsInFlight[req.approvalId]) {
4247
+ if (req.approvalId && req.state !== "resolved" && !state.approvalRequestsInFlight[req.approvalId]) {
4171
4248
  pending.push(req.approvalId);
4172
4249
  }
4173
4250
  }
@@ -4176,8 +4253,7 @@ var getWebUiClientScript = (markedSource2) => `
4176
4253
  if (pending.length === 0) return;
4177
4254
  const wasStreaming = state.isStreaming;
4178
4255
  if (!wasStreaming) setStreaming(true);
4179
- renderMessages(state.activeMessages, state.isStreaming);
4180
- await Promise.all(pending.map((aid) => submitApproval(aid, decision, { wasStreaming })));
4256
+ pending.forEach((aid) => submitApproval(aid, decision));
4181
4257
  renderMessages(state.activeMessages, state.isStreaming);
4182
4258
  loadConversations();
4183
4259
  if (!wasStreaming && state.activeConversationId) {
@@ -4207,8 +4283,7 @@ var getWebUiClientScript = (markedSource2) => `
4207
4283
  if (!wasStreaming) {
4208
4284
  setStreaming(true);
4209
4285
  }
4210
- renderMessages(state.activeMessages, state.isStreaming);
4211
- await submitApproval(approvalId, decision, { wasStreaming });
4286
+ submitApproval(approvalId, decision);
4212
4287
  renderMessages(state.activeMessages, state.isStreaming);
4213
4288
  loadConversations();
4214
4289
  if (!wasStreaming && state.activeConversationId) {
@@ -8931,6 +9006,17 @@ data: ${JSON.stringify(data)}
8931
9006
  foundConversation.runStatus = "running";
8932
9007
  await conversationStore.update(foundConversation);
8933
9008
  const checkpointRef = allApprovals[0];
9009
+ const prevStream = conversationEventStreams.get(conversationId);
9010
+ if (prevStream) {
9011
+ prevStream.finished = false;
9012
+ prevStream.buffer = [];
9013
+ } else {
9014
+ conversationEventStreams.set(conversationId, {
9015
+ buffer: [],
9016
+ subscribers: /* @__PURE__ */ new Set(),
9017
+ finished: false
9018
+ });
9019
+ }
8934
9020
  const resumeWork = (async () => {
8935
9021
  try {
8936
9022
  const toolContext = {
@@ -8966,6 +9052,14 @@ data: ${JSON.stringify(data)}
8966
9052
  error: r.error
8967
9053
  })));
8968
9054
  }
9055
+ const bs = harness.browserSession;
9056
+ if (bs?.isActiveFor(conversationId)) {
9057
+ broadcastRawSse(conversationId, "browser:status", {
9058
+ active: true,
9059
+ url: bs.getUrl(conversationId) ?? null,
9060
+ interactionAllowed: true
9061
+ });
9062
+ }
8969
9063
  await resumeRunFromCheckpoint(
8970
9064
  conversationId,
8971
9065
  foundConversation,
@@ -10094,7 +10188,7 @@ var runInteractive = async (workingDir, params) => {
10094
10188
  await harness.initialize();
10095
10189
  const identity = await ensureAgentIdentity2(workingDir);
10096
10190
  try {
10097
- const { runInteractiveInk } = await import("./run-interactive-ink-IEB4MZ2C.js");
10191
+ const { runInteractiveInk } = await import("./run-interactive-ink-REIUGQ5X.js");
10098
10192
  await runInteractiveInk({
10099
10193
  harness,
10100
10194
  params,
package/dist/cli.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  main
4
- } from "./chunk-3ETNDULB.js";
4
+ } from "./chunk-UTZB2CS7.js";
5
5
 
6
6
  // src/cli.ts
7
7
  void main();
package/dist/index.js CHANGED
@@ -23,7 +23,7 @@ import {
23
23
  runTests,
24
24
  startDevServer,
25
25
  updateAgentGuidance
26
- } from "./chunk-3ETNDULB.js";
26
+ } from "./chunk-UTZB2CS7.js";
27
27
  export {
28
28
  addSkill,
29
29
  buildCli,
@@ -2,7 +2,7 @@ import {
2
2
  consumeFirstRunIntro,
3
3
  inferConversationTitle,
4
4
  resolveHarnessEnvironment
5
- } from "./chunk-3ETNDULB.js";
5
+ } from "./chunk-UTZB2CS7.js";
6
6
 
7
7
  // src/run-interactive-ink.ts
8
8
  import * as readline from "readline";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@poncho-ai/cli",
3
- "version": "0.24.1",
3
+ "version": "0.24.2",
4
4
  "description": "CLI for building and deploying AI agents",
5
5
  "repository": {
6
6
  "type": "git",
@@ -27,9 +27,9 @@
27
27
  "react": "^19.2.4",
28
28
  "react-devtools-core": "^6.1.5",
29
29
  "yaml": "^2.8.1",
30
+ "@poncho-ai/harness": "0.22.1",
30
31
  "@poncho-ai/messaging": "0.5.1",
31
- "@poncho-ai/sdk": "1.5.0",
32
- "@poncho-ai/harness": "0.22.1"
32
+ "@poncho-ai/sdk": "1.5.0"
33
33
  },
34
34
  "devDependencies": {
35
35
  "@types/busboy": "^1.5.4",
package/src/index.ts CHANGED
@@ -3103,6 +3103,20 @@ export const createRequestHandler = async (options?: {
3103
3103
  // Use the first approval as the checkpoint reference (all share the same checkpoint data)
3104
3104
  const checkpointRef = allApprovals[0]!;
3105
3105
 
3106
+ // Reset the event stream so new SSE subscribers can connect to the
3107
+ // resumed run (the previous run's stream was marked finished).
3108
+ const prevStream = conversationEventStreams.get(conversationId);
3109
+ if (prevStream) {
3110
+ prevStream.finished = false;
3111
+ prevStream.buffer = [];
3112
+ } else {
3113
+ conversationEventStreams.set(conversationId, {
3114
+ buffer: [],
3115
+ subscribers: new Set(),
3116
+ finished: false,
3117
+ });
3118
+ }
3119
+
3106
3120
  const resumeWork = (async () => {
3107
3121
  try {
3108
3122
  const toolContext = {
@@ -3145,6 +3159,16 @@ export const createRequestHandler = async (options?: {
3145
3159
  })));
3146
3160
  }
3147
3161
 
3162
+ // If approved tools activated the browser, notify connected clients
3163
+ const bs = harness.browserSession as BrowserSessionForStatus | undefined;
3164
+ if (bs?.isActiveFor(conversationId)) {
3165
+ broadcastRawSse(conversationId, "browser:status", {
3166
+ active: true,
3167
+ url: bs.getUrl(conversationId) ?? null,
3168
+ interactionAllowed: true,
3169
+ });
3170
+ }
3171
+
3148
3172
  await resumeRunFromCheckpoint(
3149
3173
  conversationId,
3150
3174
  foundConversation!,
@@ -151,15 +151,23 @@ export const getWebUiClientScript = (markedSource: string): string => `
151
151
  .replace(/"/g, "&quot;")
152
152
  .replace(/'/g, "&#39;");
153
153
 
154
+ const _mdCache = new Map();
154
155
  const renderAssistantMarkdown = (value) => {
155
156
  const source = String(value || "").trim();
156
157
  if (!source) return "<p></p>";
157
158
 
159
+ const cached = _mdCache.get(source);
160
+ if (cached !== undefined) return cached;
161
+
158
162
  try {
159
- return marked.parse(source);
163
+ const result = marked.parse(source);
164
+ _mdCache.set(source, result);
165
+ if (_mdCache.size > 500) {
166
+ _mdCache.delete(_mdCache.keys().next().value);
167
+ }
168
+ return result;
160
169
  } catch (error) {
161
170
  console.error("Markdown parsing error:", error);
162
- // Fallback to escaped text
163
171
  return "<p>" + escapeHtml(source) + "</p>";
164
172
  }
165
173
  };
@@ -250,15 +258,26 @@ export const getWebUiClientScript = (markedSource: string): string => `
250
258
  const approvalId = typeof req.approvalId === "string" ? req.approvalId : "";
251
259
  const tool = typeof req.tool === "string" ? req.tool : "tool";
252
260
  const input = req.input != null ? req.input : {};
261
+ const subagentLabel = req._subagentLabel
262
+ ? ' <span style="color: var(--text-3); font-size: 11px;">(from ' + escapeHtml(req._subagentLabel) + ')</span>'
263
+ : "";
264
+ if (req.state === "resolved") {
265
+ const isApproved = req.resolvedDecision === "approve";
266
+ const label = isApproved ? "Approved" : "Denied";
267
+ const cls = isApproved ? "approve" : "deny";
268
+ return (
269
+ '<div class="approval-request-item resolved">' +
270
+ '<div class="approval-resolved-status ' + cls + '">' + label + ': <code>' +
271
+ escapeHtml(tool) + "</code>" + subagentLabel + "</div>" +
272
+ "</div>"
273
+ );
274
+ }
253
275
  const submitting = req.state === "submitting";
254
276
  const approveLabel = submitting && req.pendingDecision === "approve" ? "Approving..." : "Approve";
255
277
  const denyLabel = submitting && req.pendingDecision === "deny" ? "Denying..." : "Deny";
256
278
  const errorHtml = req._error
257
279
  ? '<div style="color: var(--deny); font-size: 11px; margin-top: 4px;">Submit failed: ' + escapeHtml(req._error) + "</div>"
258
280
  : "";
259
- const subagentLabel = req._subagentLabel
260
- ? ' <span style="color: var(--text-3); font-size: 11px;">(from ' + escapeHtml(req._subagentLabel) + ')</span>'
261
- : "";
262
281
  return (
263
282
  '<div class="approval-request-item">' +
264
283
  '<div class="approval-requests-label">Approval required: <code>' +
@@ -286,10 +305,11 @@ export const getWebUiClientScript = (markedSource: string): string => `
286
305
  );
287
306
  })
288
307
  .join("");
289
- const batchButtons = requests.length > 1
308
+ const actionableCount = requests.filter((r) => r.state !== "resolved").length;
309
+ const batchButtons = actionableCount > 1
290
310
  ? '<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>' +
311
+ '<button class="approval-batch-btn approve" data-approval-batch="approve">Approve all (' + actionableCount + ')</button>' +
312
+ '<button class="approval-batch-btn deny" data-approval-batch="deny">Deny all (' + actionableCount + ')</button>' +
293
313
  "</div>"
294
314
  : "";
295
315
  return (
@@ -418,6 +438,14 @@ export const getWebUiClientScript = (markedSource: string): string => `
418
438
  return false;
419
439
  };
420
440
 
441
+ const clearResolvedApprovals = (message) => {
442
+ if (Array.isArray(message._pendingApprovals)) {
443
+ message._pendingApprovals = message._pendingApprovals.filter(
444
+ (req) => req.state !== "resolved",
445
+ );
446
+ }
447
+ };
448
+
421
449
  const toUiPendingApprovals = (pendingApprovals) => {
422
450
  if (!Array.isArray(pendingApprovals)) {
423
451
  return [];
@@ -810,12 +838,16 @@ export const getWebUiClientScript = (markedSource: string): string => `
810
838
  );
811
839
  }
812
840
  });
813
- // While streaming, show current tools if any
814
- if (isStreaming && i === messages.length - 1 && m._currentTools && m._currentTools.length > 0) {
815
- content.insertAdjacentHTML(
816
- "beforeend",
817
- renderToolActivity(m._currentTools, m._pendingApprovals || [], m._toolImages || []),
818
- );
841
+ // While streaming, show current tools and/or pending approvals
842
+ if (isStreaming && i === messages.length - 1) {
843
+ const hasCurrentTools = m._currentTools && m._currentTools.length > 0;
844
+ const hasStreamApprovals = Array.isArray(m._pendingApprovals) && m._pendingApprovals.length > 0;
845
+ if (hasCurrentTools || hasStreamApprovals) {
846
+ content.insertAdjacentHTML(
847
+ "beforeend",
848
+ renderToolActivity(m._currentTools || [], m._pendingApprovals || [], m._toolImages || []),
849
+ );
850
+ }
819
851
  }
820
852
  // When reloading with unresolved approvals, show them even when not streaming
821
853
  if (!isStreaming && pendingApprovals.length > 0 && lastToolsSectionIndex < 0) {
@@ -1091,6 +1123,7 @@ export const getWebUiClientScript = (markedSource: string): string => `
1091
1123
  renderMessages(state.activeMessages, payload.hasActiveRun);
1092
1124
  }
1093
1125
  if (payload.hasActiveRun) {
1126
+ if (window._connectBrowserStream) window._connectBrowserStream();
1094
1127
  setTimeout(poll, 2000);
1095
1128
  } else {
1096
1129
  setStreaming(false);
@@ -1108,12 +1141,24 @@ export const getWebUiClientScript = (markedSource: string): string => `
1108
1141
  const liveOnly = options && options.liveOnly;
1109
1142
  return new Promise((resolve) => {
1110
1143
  const localMessages = state.activeMessages || [];
1144
+ let _rafId = 0;
1111
1145
  const renderIfActiveConversation = (streaming) => {
1112
1146
  if (state.activeConversationId !== conversationId) {
1113
1147
  return;
1114
1148
  }
1115
1149
  state.activeMessages = localMessages;
1116
- renderMessages(localMessages, streaming);
1150
+ if (!streaming) {
1151
+ if (_rafId) { cancelAnimationFrame(_rafId); _rafId = 0; }
1152
+ renderMessages(localMessages, false);
1153
+ return;
1154
+ }
1155
+ if (!_rafId) {
1156
+ _rafId = requestAnimationFrame(() => {
1157
+ _rafId = 0;
1158
+ if (state.activeConversationId !== conversationId) return;
1159
+ renderMessages(localMessages, true);
1160
+ });
1161
+ }
1117
1162
  };
1118
1163
  let assistantMessage = localMessages[localMessages.length - 1];
1119
1164
  if (!assistantMessage || assistantMessage.role !== "assistant") {
@@ -1182,6 +1227,7 @@ export const getWebUiClientScript = (markedSource: string): string => `
1182
1227
  }
1183
1228
  if (eventName === "model:chunk") {
1184
1229
  const chunk = String(payload.content || "");
1230
+ if (chunk.length > 0) clearResolvedApprovals(assistantMessage);
1185
1231
  if (assistantMessage._currentTools.length > 0 && chunk.length > 0) {
1186
1232
  assistantMessage._sections.push({
1187
1233
  type: "tools",
@@ -1223,6 +1269,7 @@ export const getWebUiClientScript = (markedSource: string): string => `
1223
1269
  renderIfActiveConversation(true);
1224
1270
  }
1225
1271
  if (eventName === "tool:started") {
1272
+ clearResolvedApprovals(assistantMessage);
1226
1273
  const toolName = payload.tool || "tool";
1227
1274
  removeActiveActivityForTool(assistantMessage, toolName);
1228
1275
  const startedActivity = addActiveActivityFromToolStart(
@@ -1377,7 +1424,7 @@ export const getWebUiClientScript = (markedSource: string): string => `
1377
1424
  typeof payload.approvalId === "string" ? payload.approvalId : "";
1378
1425
  if (approvalId && Array.isArray(assistantMessage._pendingApprovals)) {
1379
1426
  assistantMessage._pendingApprovals = assistantMessage._pendingApprovals.filter(
1380
- (req) => req.approvalId !== approvalId,
1427
+ (req) => req.approvalId !== approvalId || req.state === "resolved",
1381
1428
  );
1382
1429
  }
1383
1430
  renderIfActiveConversation(true);
@@ -1390,7 +1437,7 @@ export const getWebUiClientScript = (markedSource: string): string => `
1390
1437
  typeof payload.approvalId === "string" ? payload.approvalId : "";
1391
1438
  if (approvalId && Array.isArray(assistantMessage._pendingApprovals)) {
1392
1439
  assistantMessage._pendingApprovals = assistantMessage._pendingApprovals.filter(
1393
- (req) => req.approvalId !== approvalId,
1440
+ (req) => req.approvalId !== approvalId || req.state === "resolved",
1394
1441
  );
1395
1442
  }
1396
1443
  renderIfActiveConversation(true);
@@ -1927,12 +1974,24 @@ export const getWebUiClientScript = (markedSource: string): string => `
1927
1974
  }
1928
1975
  state.activeStreamConversationId = conversationId;
1929
1976
  const streamConversationId = conversationId;
1977
+ let _rafId = 0;
1930
1978
  const renderIfActiveConversation = (streaming) => {
1931
1979
  if (state.activeConversationId !== streamConversationId) {
1932
1980
  return;
1933
1981
  }
1934
1982
  state.activeMessages = localMessages;
1935
- renderMessages(localMessages, streaming);
1983
+ if (!streaming) {
1984
+ if (_rafId) { cancelAnimationFrame(_rafId); _rafId = 0; }
1985
+ renderMessages(localMessages, false);
1986
+ return;
1987
+ }
1988
+ if (!_rafId) {
1989
+ _rafId = requestAnimationFrame(() => {
1990
+ _rafId = 0;
1991
+ if (state.activeConversationId !== streamConversationId) return;
1992
+ renderMessages(localMessages, true);
1993
+ });
1994
+ }
1936
1995
  };
1937
1996
  const finalizeAssistantMessage = () => {
1938
1997
  assistantMessage._activeActivities = [];
@@ -1993,7 +2052,7 @@ export const getWebUiClientScript = (markedSource: string): string => `
1993
2052
  try {
1994
2053
  if (eventName === "model:chunk") {
1995
2054
  const chunk = String(payload.content || "");
1996
- // If we have tools accumulated and text starts again, push tools as a section
2055
+ if (chunk.length > 0) clearResolvedApprovals(assistantMessage);
1997
2056
  if (assistantMessage._currentTools.length > 0 && chunk.length > 0) {
1998
2057
  assistantMessage._sections.push({ type: "tools", content: assistantMessage._currentTools });
1999
2058
  assistantMessage._currentTools = [];
@@ -2037,6 +2096,7 @@ export const getWebUiClientScript = (markedSource: string): string => `
2037
2096
  renderIfActiveConversation(true);
2038
2097
  }
2039
2098
  if (eventName === "tool:started") {
2099
+ clearResolvedApprovals(assistantMessage);
2040
2100
  const toolName = payload.tool || "tool";
2041
2101
  removeActiveActivityForTool(assistantMessage, toolName);
2042
2102
  const startedActivity = addActiveActivityFromToolStart(
@@ -2191,7 +2251,7 @@ export const getWebUiClientScript = (markedSource: string): string => `
2191
2251
  typeof payload.approvalId === "string" ? payload.approvalId : "";
2192
2252
  if (approvalId && Array.isArray(assistantMessage._pendingApprovals)) {
2193
2253
  assistantMessage._pendingApprovals = assistantMessage._pendingApprovals.filter(
2194
- (req) => req.approvalId !== approvalId,
2254
+ (req) => req.approvalId !== approvalId || req.state === "resolved",
2195
2255
  );
2196
2256
  }
2197
2257
  renderIfActiveConversation(true);
@@ -2206,7 +2266,7 @@ export const getWebUiClientScript = (markedSource: string): string => `
2206
2266
  typeof payload.approvalId === "string" ? payload.approvalId : "";
2207
2267
  if (approvalId && Array.isArray(assistantMessage._pendingApprovals)) {
2208
2268
  assistantMessage._pendingApprovals = assistantMessage._pendingApprovals.filter(
2209
- (req) => req.approvalId !== approvalId,
2269
+ (req) => req.approvalId !== approvalId || req.state === "resolved",
2210
2270
  );
2211
2271
  }
2212
2272
  renderIfActiveConversation(true);
@@ -2535,21 +2595,17 @@ export const getWebUiClientScript = (markedSource: string): string => `
2535
2595
  openLightbox(img.src);
2536
2596
  });
2537
2597
 
2538
- const submitApproval = async (approvalId, decision, opts) => {
2539
- const wasStreaming = opts && opts.wasStreaming;
2598
+ const submitApproval = (approvalId, decision) => {
2540
2599
  state.approvalRequestsInFlight[approvalId] = true;
2541
2600
  updatePendingApproval(approvalId, (request) => ({
2542
2601
  ...request,
2543
- state: "submitting",
2544
- pendingDecision: decision,
2602
+ state: "resolved",
2603
+ resolvedDecision: decision,
2545
2604
  }));
2546
- try {
2547
- await api("/api/approvals/" + encodeURIComponent(approvalId), {
2548
- method: "POST",
2549
- body: JSON.stringify({ approved: decision === "approve" }),
2550
- });
2551
- updatePendingApproval(approvalId, () => null);
2552
- } catch (error) {
2605
+ api("/api/approvals/" + encodeURIComponent(approvalId), {
2606
+ method: "POST",
2607
+ body: JSON.stringify({ approved: decision === "approve" }),
2608
+ }).catch((error) => {
2553
2609
  const isStale = error && error.payload && error.payload.code === "APPROVAL_NOT_FOUND";
2554
2610
  if (isStale) {
2555
2611
  updatePendingApproval(approvalId, () => null);
@@ -2559,12 +2615,14 @@ export const getWebUiClientScript = (markedSource: string): string => `
2559
2615
  ...request,
2560
2616
  state: "pending",
2561
2617
  pendingDecision: null,
2618
+ resolvedDecision: null,
2562
2619
  _error: errMsg,
2563
2620
  }));
2564
2621
  }
2565
- } finally {
2622
+ renderMessages(state.activeMessages, state.isStreaming);
2623
+ }).finally(() => {
2566
2624
  delete state.approvalRequestsInFlight[approvalId];
2567
- }
2625
+ });
2568
2626
  };
2569
2627
 
2570
2628
  elements.messages.addEventListener("click", async (event) => {
@@ -2583,7 +2641,7 @@ export const getWebUiClientScript = (markedSource: string): string => `
2583
2641
  for (const m of messages) {
2584
2642
  if (Array.isArray(m._pendingApprovals)) {
2585
2643
  for (const req of m._pendingApprovals) {
2586
- if (req.approvalId && req.state !== "submitting" && !state.approvalRequestsInFlight[req.approvalId]) {
2644
+ if (req.approvalId && req.state !== "resolved" && !state.approvalRequestsInFlight[req.approvalId]) {
2587
2645
  pending.push(req.approvalId);
2588
2646
  }
2589
2647
  }
@@ -2592,8 +2650,7 @@ export const getWebUiClientScript = (markedSource: string): string => `
2592
2650
  if (pending.length === 0) return;
2593
2651
  const wasStreaming = state.isStreaming;
2594
2652
  if (!wasStreaming) setStreaming(true);
2595
- renderMessages(state.activeMessages, state.isStreaming);
2596
- await Promise.all(pending.map((aid) => submitApproval(aid, decision, { wasStreaming })));
2653
+ pending.forEach((aid) => submitApproval(aid, decision));
2597
2654
  renderMessages(state.activeMessages, state.isStreaming);
2598
2655
  loadConversations();
2599
2656
  if (!wasStreaming && state.activeConversationId) {
@@ -2623,8 +2680,7 @@ export const getWebUiClientScript = (markedSource: string): string => `
2623
2680
  if (!wasStreaming) {
2624
2681
  setStreaming(true);
2625
2682
  }
2626
- renderMessages(state.activeMessages, state.isStreaming);
2627
- await submitApproval(approvalId, decision, { wasStreaming });
2683
+ submitApproval(approvalId, decision);
2628
2684
  renderMessages(state.activeMessages, state.isStreaming);
2629
2685
  loadConversations();
2630
2686
  if (!wasStreaming && state.activeConversationId) {
@@ -825,6 +825,25 @@ export const WEB_UI_STYLES = `
825
825
  border-color: var(--deny-border);
826
826
  color: var(--deny);
827
827
  }
828
+ .approval-request-item.resolved {
829
+ opacity: 0.7;
830
+ }
831
+ .approval-resolved-status {
832
+ font-size: 12px;
833
+ font-weight: 600;
834
+ letter-spacing: 0.04em;
835
+ }
836
+ .approval-resolved-status code {
837
+ font-family: ui-monospace, "SF Mono", "Fira Code", monospace;
838
+ letter-spacing: 0;
839
+ color: var(--fg-strong);
840
+ }
841
+ .approval-resolved-status.approve {
842
+ color: var(--approve);
843
+ }
844
+ .approval-resolved-status.deny {
845
+ color: var(--deny);
846
+ }
828
847
  .user-bubble {
829
848
  background: var(--bg-elevated);
830
849
  border: 1px solid var(--border-2);