mixdog 0.9.47 → 0.9.49

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.
Files changed (83) hide show
  1. package/package.json +14 -4
  2. package/scripts/agent-parallel-smoke.mjs +4 -1
  3. package/scripts/agent-route-batch-test.mjs +40 -0
  4. package/scripts/code-graph-aggregate-cwd-test.mjs +154 -0
  5. package/scripts/code-graph-description-contract.mjs +185 -0
  6. package/scripts/code-graph-disk-hit-test.mjs +55 -0
  7. package/scripts/code-graph-dispatch-test.mjs +96 -0
  8. package/scripts/compact-pressure-test.mjs +40 -0
  9. package/scripts/deferred-tool-loading-test.mjs +233 -0
  10. package/scripts/execution-completion-dedup-test.mjs +48 -0
  11. package/scripts/explore-prompt-policy-test.mjs +88 -3
  12. package/scripts/live-worker-smoke.mjs +68 -16
  13. package/scripts/memory-core-input-test.mjs +33 -13
  14. package/scripts/native-edit-wire-test.mjs +152 -0
  15. package/scripts/openai-oauth-ws-1006-retry-test.mjs +294 -16
  16. package/scripts/patch-binary-cache-test.mjs +181 -0
  17. package/scripts/prompt-immediate-render-test.mjs +89 -0
  18. package/scripts/provider-toolcall-test.mjs +280 -15
  19. package/scripts/shell-failure-diagnostics-test.mjs +211 -0
  20. package/scripts/statusline-quota-hysteresis-test.mjs +26 -1
  21. package/scripts/streaming-tail-window-test.mjs +29 -0
  22. package/scripts/tool-failures.mjs +21 -3
  23. package/scripts/tool-smoke.mjs +263 -38
  24. package/scripts/tool-tui-presentation-test.mjs +17 -1
  25. package/scripts/tui-perf-run.ps1 +26 -0
  26. package/scripts/tui-transcript-perf-test.mjs +7 -1
  27. package/scripts/verify-release-assets-test.mjs +647 -0
  28. package/scripts/verify-release-assets.mjs +293 -0
  29. package/scripts/windows-hide-spawn-options-test.mjs +19 -0
  30. package/src/cli.mjs +1 -0
  31. package/src/runtime/agent/orchestrator/agent-trace-format.mjs +16 -5
  32. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +35 -11
  33. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +35 -11
  34. package/src/runtime/agent/orchestrator/providers/custom-tool-wire.mjs +28 -0
  35. package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +1 -1
  36. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +34 -34
  37. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +45 -38
  38. package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +36 -11
  39. package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +4 -4
  40. package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +10 -6
  41. package/src/runtime/agent/orchestrator/session/loop/deferred-call-through.mjs +4 -3
  42. package/src/runtime/agent/orchestrator/session/loop/recall-fasttrack.mjs +4 -3
  43. package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +16 -1
  44. package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +2 -2
  45. package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +0 -1
  46. package/src/runtime/agent/orchestrator/session/tool-batch.mjs +13 -8
  47. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +30 -16
  48. package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +25 -8
  49. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +69 -4
  50. package/src/runtime/agent/orchestrator/tools/code-graph-prewarm-worker.mjs +5 -4
  51. package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +1 -1
  52. package/src/runtime/agent/orchestrator/tools/graph-manifest.json +12 -12
  53. package/src/runtime/agent/orchestrator/tools/patch/native-server.mjs +12 -7
  54. package/src/runtime/agent/orchestrator/tools/patch-binary-fetcher.mjs +77 -18
  55. package/src/runtime/agent/orchestrator/tools/patch-manifest.json +11 -11
  56. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +99 -24
  57. package/src/runtime/memory/lib/memory-action-handlers.mjs +21 -11
  58. package/src/runtime/memory/tool-defs.mjs +1 -2
  59. package/src/session-runtime/context-status.mjs +25 -16
  60. package/src/session-runtime/model-route-api.mjs +2 -0
  61. package/src/session-runtime/runtime-core.mjs +2 -2
  62. package/src/session-runtime/session-turn-api.mjs +4 -1
  63. package/src/session-runtime/tool-catalog.mjs +113 -19
  64. package/src/session-runtime/tool-defs.mjs +1 -0
  65. package/src/standalone/agent-tool/helpers.mjs +25 -6
  66. package/src/standalone/agent-tool.mjs +75 -41
  67. package/src/tui/App.jsx +4 -0
  68. package/src/tui/app/input-parsers.mjs +8 -9
  69. package/src/tui/components/Markdown.jsx +6 -1
  70. package/src/tui/components/Message.jsx +11 -2
  71. package/src/tui/components/PromptInput.jsx +19 -21
  72. package/src/tui/components/Spinner.jsx +4 -4
  73. package/src/tui/components/StatusLine.jsx +6 -6
  74. package/src/tui/components/TranscriptItem.jsx +2 -2
  75. package/src/tui/components/prompt-input/immediate-render.mjs +47 -0
  76. package/src/tui/components/tool-execution/surface-detail.mjs +14 -9
  77. package/src/tui/dist/index.mjs +130 -45
  78. package/src/tui/engine/agent-job-feed.mjs +21 -2
  79. package/src/tui/engine/turn.mjs +12 -1
  80. package/src/tui/markdown/measure-rendered-rows.mjs +1 -1
  81. package/src/tui/markdown/streaming-markdown.mjs +20 -0
  82. package/src/ui/statusline.mjs +5 -5
  83. package/src/vendor/statusline/src/gateway/session-routes.mjs +22 -10
@@ -15,7 +15,7 @@
15
15
  * is mode-driven: up while requesting, down otherwise. Input totals hidden.
16
16
  * - elided duration formatting ("0:25" after 60s).
17
17
  * - mode prop: 'responding' | 'thinking' | 'tool-use' | 'tool-input' |
18
- * 'requesting' | 'compacting' | 'resuming' (default 'responding').
18
+ * 'requesting' | 'reconnecting' | 'compacting' | 'resuming' (default 'responding').
19
19
  */
20
20
  import React, { useRef } from 'react';
21
21
  import { Box, Text } from 'ink';
@@ -40,7 +40,7 @@ const SHOW_TOKENS_AFTER_MS = 30_000;
40
40
  const THINKING_DELAY_MS = 3000;
41
41
 
42
42
  // One-way shimmer. The tail runs past the final character before restarting.
43
- const GLIMMER_SPEED_MS = { requesting: 70, compacting: 120, 'auto-clear': 120, resuming: 120, 'tool-use': 120, responding: 120, thinking: 120, 'tool-input': 120 };
43
+ const GLIMMER_SPEED_MS = { requesting: 70, reconnecting: 70, compacting: 120, 'auto-clear': 120, resuming: 120, 'tool-use': 120, responding: 120, thinking: 120, 'tool-input': 120 };
44
44
  const GLIMMER_TRAIL = 4;
45
45
  const THINKING_GLIMMER_SPEED_MS = 120;
46
46
  const THINKING_GLIMMER_TRAIL = 4;
@@ -230,7 +230,7 @@ export function Spinner({ verb = 'Working', startedAt, outputTokens = 0, tokens
230
230
  : theme.spinnerGlyph;
231
231
 
232
232
  // --- Verb shimmer (traveling highlight) ---
233
- if (!displayVerbRef.current) {
233
+ if (!displayVerbRef.current || displayVerbModeRef.current !== mode) {
234
234
  displayVerbRef.current = stableModeVerb(mode, verb);
235
235
  nextVerbCheckRef.current = nextVerbCheckAt(now);
236
236
  }
@@ -240,7 +240,7 @@ export function Spinner({ verb = 'Working', startedAt, outputTokens = 0, tokens
240
240
  nextVerbCheckRef.current = nextVerbCheckAt(now);
241
241
  }
242
242
  const displayVerb = displayVerbRef.current;
243
- const messageText = `${displayVerb}…`;
243
+ const messageText = mode === 'reconnecting' ? displayVerb : `${displayVerb}…`;
244
244
  const messageLen = messageText.length;
245
245
 
246
246
  // Glimmer speed per mode.
@@ -152,18 +152,18 @@ function localContextPct({
152
152
  compactBoundaryTokens = 0,
153
153
  autoCompactTokenLimit = 0,
154
154
  } = {}) {
155
- const baseWindow = localNum(compactBoundaryTokens) > 0
155
+ const boundary = localNum(compactBoundaryTokens) > 0
156
156
  ? localNum(compactBoundaryTokens)
157
157
  : (localNum(displayContextWindow) > 0
158
158
  ? localNum(displayContextWindow)
159
159
  : (localNum(contextWindow) > 0
160
160
  ? localNum(contextWindow)
161
161
  : (localNum(rawContextWindow) > 0 ? localNum(rawContextWindow) : 200_000)));
162
- // Boundary-only denominator (mirrors ui/statusline.mjs resolveContextUsedPct):
163
- // context % is always measured against the single compaction boundary value,
164
- // never a separate auto-compact trigger — statusline and gateway stay on the
165
- // same denominator with no before/after trigger semantics.
166
- const window = baseWindow;
162
+ // The trigger is the denominator used by the pre-send auto-compact decision,
163
+ // so the local boot gauge reaches 100% at the exact same pressure.
164
+ const window = localNum(autoCompactTokenLimit) > 0
165
+ ? localNum(autoCompactTokenLimit)
166
+ : boundary;
167
167
  const s = stats && typeof stats === 'object' ? stats : {};
168
168
  const source = String(s.currentContextSource || '').toLowerCase();
169
169
  const estimated = localNum(s.currentEstimatedContextTokens);
@@ -61,7 +61,7 @@ export function ToolHookDenialCard({ item, columns = 80 }) {
61
61
  // transcript row (which reads theme.* directly) to re-render. Threading the
62
62
  // epoch through Item → AssistantMessage/UserMessage/ToolExecution breaks
63
63
  // React.memo's shallow equality on a theme change without a broad refactor.
64
- export const Item = React.memo(function Item({ item, prevKind, columns, toolOutputExpanded, rightMessage = '', rightTone = 'info', rightMessageWidth = 24, themeEpoch = 0 }) {
64
+ export const Item = React.memo(function Item({ item, prevKind, columns, toolOutputExpanded, rightMessage = '', rightTone = 'info', rightMessageWidth = 24, themeEpoch = 0, streamingWindowRows = 0 }) {
65
65
  const hintOnTurnDoneRow = item.kind === 'turndone' || item.kind === 'statusdone';
66
66
  let node = null;
67
67
  switch (item.kind) {
@@ -69,7 +69,7 @@ export const Item = React.memo(function Item({ item, prevKind, columns, toolOutp
69
69
  node = <UserMessage text={item.text} attached={prevKind === 'user'} columns={columns} themeEpoch={themeEpoch} />;
70
70
  break;
71
71
  case 'assistant':
72
- node = <AssistantMessage text={item.text} streaming={item.streaming} columns={columns} themeEpoch={themeEpoch} assistantId={item.id} />;
72
+ node = <AssistantMessage text={item.text} streaming={item.streaming} columns={columns} themeEpoch={themeEpoch} assistantId={item.id} streamingWindowRows={streamingWindowRows} />;
73
73
  break;
74
74
  case 'tool': {
75
75
  if (shouldSuppressFullyFailedToolItem(item)) return null;
@@ -0,0 +1,47 @@
1
+ function suppressed(value) {
2
+ return typeof value === 'function' ? value() === true : value === true;
3
+ }
4
+
5
+ export function cancelPromptImmediateFlush(throttle, clearTimer = clearTimeout) {
6
+ if (!throttle || throttle.timer === null) return false;
7
+ clearTimer(throttle.timer);
8
+ throttle.timer = null;
9
+ return true;
10
+ }
11
+
12
+ export function schedulePromptImmediateFlush({
13
+ throttle,
14
+ isSuppressed = false,
15
+ flush,
16
+ intervalMs = 16,
17
+ now = Date.now,
18
+ enqueue = queueMicrotask,
19
+ setTimer = setTimeout,
20
+ clearTimer = clearTimeout,
21
+ }) {
22
+ if (!throttle || typeof flush !== 'function') {
23
+ throw new TypeError('schedulePromptImmediateFlush requires throttle state and a flush function');
24
+ }
25
+ if (suppressed(isSuppressed)) {
26
+ cancelPromptImmediateFlush(throttle, clearTimer);
27
+ return false;
28
+ }
29
+
30
+ const current = now();
31
+ const elapsed = current - throttle.lastAt;
32
+ if (elapsed >= intervalMs) {
33
+ cancelPromptImmediateFlush(throttle, clearTimer);
34
+ throttle.lastAt = current;
35
+ enqueue(flush);
36
+ return true;
37
+ }
38
+ if (throttle.timer !== null) return false;
39
+
40
+ throttle.timer = setTimer(() => {
41
+ throttle.timer = null;
42
+ if (suppressed(isSuppressed)) return;
43
+ throttle.lastAt = now();
44
+ flush();
45
+ }, intervalMs - elapsed);
46
+ return true;
47
+ }
@@ -353,19 +353,24 @@ export function toolSearchLoadedSummary(resultText) {
353
353
  parsed = JSON.parse(String(resultText || ''));
354
354
  } catch {
355
355
  const text = String(resultText || '');
356
- const match = /^Loaded deferred tools:\s*(.+)$/m.exec(text);
357
- if (!match) return '';
358
- return [...new Set(match[1].split(',').map((name) => name.trim()).filter(Boolean))].join(', ');
356
+ const loaded = /^Loaded deferred tools:\s*(.+)$/m.exec(text)?.[1] || '';
357
+ const already = /^Already active:\s*(.+)$/m.exec(text)?.[1] || '';
358
+ return [
359
+ ...(loaded ? [`Loaded: ${loaded}`] : []),
360
+ ...(already ? [`Already active: ${already}`] : []),
361
+ ].join(' · ');
359
362
  }
360
363
  const tools = parsed?.selected?.tools;
361
364
  if (!tools || typeof tools !== 'object') return '';
362
- const names = [
363
- ...(Array.isArray(tools.added) ? tools.added : []),
364
- ...(Array.isArray(tools.already) ? tools.already : []),
365
- ]
365
+ const uniqueNames = (names) => [...new Set((Array.isArray(names) ? names : [])
366
366
  .map((name) => String(name || '').trim())
367
- .filter(Boolean);
368
- return [...new Set(names)].join(', ');
367
+ .filter(Boolean))];
368
+ const loaded = uniqueNames(tools.added);
369
+ const already = uniqueNames(tools.already);
370
+ return [
371
+ ...(loaded.length ? [`Loaded: ${loaded.join(', ')}`] : []),
372
+ ...(already.length ? [`Already active: ${already.join(', ')}`] : []),
373
+ ].join(' · ');
369
374
  }
370
375
 
371
376
  export function agentTerminalDetail(status, isError, elapsed, error = '') {
@@ -3512,7 +3512,7 @@ var STALL_TIMEOUT_MS = 3e3;
3512
3512
  var STALL_FADE_MS = 2e3;
3513
3513
  var SHOW_TOKENS_AFTER_MS = 3e4;
3514
3514
  var THINKING_DELAY_MS = 3e3;
3515
- var GLIMMER_SPEED_MS = { requesting: 70, compacting: 120, "auto-clear": 120, resuming: 120, "tool-use": 120, responding: 120, thinking: 120, "tool-input": 120 };
3515
+ var GLIMMER_SPEED_MS = { requesting: 70, reconnecting: 70, compacting: 120, "auto-clear": 120, resuming: 120, "tool-use": 120, responding: 120, thinking: 120, "tool-input": 120 };
3516
3516
  var GLIMMER_TRAIL = 4;
3517
3517
  var THINKING_GLIMMER_SPEED_MS = 120;
3518
3518
  var THINKING_GLIMMER_TRAIL = 4;
@@ -3649,7 +3649,7 @@ function Spinner({ verb = "Working", startedAt, outputTokens = 0, tokens = 0, th
3649
3649
  STALL_RGB,
3650
3650
  stalledIntensity
3651
3651
  )) : theme.spinnerGlyph;
3652
- if (!displayVerbRef.current) {
3652
+ if (!displayVerbRef.current || displayVerbModeRef.current !== mode) {
3653
3653
  displayVerbRef.current = stableModeVerb(mode, verb);
3654
3654
  nextVerbCheckRef.current = nextVerbCheckAt(now);
3655
3655
  }
@@ -3659,7 +3659,7 @@ function Spinner({ verb = "Working", startedAt, outputTokens = 0, tokens = 0, th
3659
3659
  nextVerbCheckRef.current = nextVerbCheckAt(now);
3660
3660
  }
3661
3661
  const displayVerb = displayVerbRef.current;
3662
- const messageText = `${displayVerb}\u2026`;
3662
+ const messageText = mode === "reconnecting" ? displayVerb : `${displayVerb}\u2026`;
3663
3663
  const messageLen = messageText.length;
3664
3664
  const glimmerSpeed = GLIMMER_SPEED_MS[mode] ?? 200;
3665
3665
  const shimmerSpan = Math.max(1, messageLen + GLIMMER_TRAIL);
@@ -4057,8 +4057,8 @@ function localContextPct({
4057
4057
  compactBoundaryTokens = 0,
4058
4058
  autoCompactTokenLimit = 0
4059
4059
  } = {}) {
4060
- const baseWindow = localNum(compactBoundaryTokens) > 0 ? localNum(compactBoundaryTokens) : localNum(displayContextWindow) > 0 ? localNum(displayContextWindow) : localNum(contextWindow) > 0 ? localNum(contextWindow) : localNum(rawContextWindow) > 0 ? localNum(rawContextWindow) : 2e5;
4061
- const window = baseWindow;
4060
+ const boundary = localNum(compactBoundaryTokens) > 0 ? localNum(compactBoundaryTokens) : localNum(displayContextWindow) > 0 ? localNum(displayContextWindow) : localNum(contextWindow) > 0 ? localNum(contextWindow) : localNum(rawContextWindow) > 0 ? localNum(rawContextWindow) : 2e5;
4061
+ const window = localNum(autoCompactTokenLimit) > 0 ? localNum(autoCompactTokenLimit) : boundary;
4062
4062
  const s = stats && typeof stats === "object" ? stats : {};
4063
4063
  const source = String(s.currentContextSource || "").toLowerCase();
4064
4064
  const estimated = localNum(s.currentEstimatedContextTokens);
@@ -4784,6 +4784,51 @@ function isAnyModifiedEnterSequence(input) {
4784
4784
  return Boolean(modifyOtherKeys && Number(modifyOtherKeys[1]) - 1 !== 0);
4785
4785
  }
4786
4786
 
4787
+ // src/tui/components/prompt-input/immediate-render.mjs
4788
+ function suppressed(value) {
4789
+ return typeof value === "function" ? value() === true : value === true;
4790
+ }
4791
+ function cancelPromptImmediateFlush(throttle, clearTimer = clearTimeout) {
4792
+ if (!throttle || throttle.timer === null) return false;
4793
+ clearTimer(throttle.timer);
4794
+ throttle.timer = null;
4795
+ return true;
4796
+ }
4797
+ function schedulePromptImmediateFlush({
4798
+ throttle,
4799
+ isSuppressed = false,
4800
+ flush,
4801
+ intervalMs = 16,
4802
+ now = Date.now,
4803
+ enqueue = queueMicrotask,
4804
+ setTimer = setTimeout,
4805
+ clearTimer = clearTimeout
4806
+ }) {
4807
+ if (!throttle || typeof flush !== "function") {
4808
+ throw new TypeError("schedulePromptImmediateFlush requires throttle state and a flush function");
4809
+ }
4810
+ if (suppressed(isSuppressed)) {
4811
+ cancelPromptImmediateFlush(throttle, clearTimer);
4812
+ return false;
4813
+ }
4814
+ const current = now();
4815
+ const elapsed = current - throttle.lastAt;
4816
+ if (elapsed >= intervalMs) {
4817
+ cancelPromptImmediateFlush(throttle, clearTimer);
4818
+ throttle.lastAt = current;
4819
+ enqueue(flush);
4820
+ return true;
4821
+ }
4822
+ if (throttle.timer !== null) return false;
4823
+ throttle.timer = setTimer(() => {
4824
+ throttle.timer = null;
4825
+ if (suppressed(isSuppressed)) return;
4826
+ throttle.lastAt = now();
4827
+ flush();
4828
+ }, intervalMs - elapsed);
4829
+ return true;
4830
+ }
4831
+
4787
4832
  // src/tui/components/PromptInput.jsx
4788
4833
  import { Fragment as Fragment2, jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
4789
4834
  var IME_LEFT_GUARD_COLUMNS = 0;
@@ -4832,6 +4877,7 @@ function PromptInput({
4832
4877
  });
4833
4878
  const [, bumpCursorAnchorEpoch] = useState3(0);
4834
4879
  const draftRef = useRef4(draft);
4880
+ const interruptActiveRef = useRef4(interruptActive);
4835
4881
  const lastReportedValueRef = useRef4(draft.value);
4836
4882
  const pasteGenerationRef = useRef4(0);
4837
4883
  if (valueRef) valueRef.current = draftRef.current.value;
@@ -4844,6 +4890,7 @@ function PromptInput({
4844
4890
  const undoRef = useRef4({ past: [], future: [], lastPushAt: 0, lastValue: null });
4845
4891
  const { value, cursor } = draft;
4846
4892
  draftRef.current = draft;
4893
+ interruptActiveRef.current = interruptActive;
4847
4894
  if (selectionRef) {
4848
4895
  const range = selectionRange(draft);
4849
4896
  selectionRef.current = range ? { range, text: mask ? "" : draft.value.slice(range.start, range.end) } : null;
@@ -4859,26 +4906,16 @@ function PromptInput({
4859
4906
  node = node.parentNode;
4860
4907
  }
4861
4908
  };
4862
- const FLUSH_INTERVAL_MS = 16;
4863
4909
  const scheduleImmediateFlush = () => {
4864
- const t = flushThrottleRef.current;
4865
- const now = Date.now();
4866
- const elapsed = now - t.lastAt;
4867
- if (elapsed >= FLUSH_INTERVAL_MS) {
4868
- if (t.timer !== null) {
4869
- clearTimeout(t.timer);
4870
- t.timer = null;
4871
- }
4872
- t.lastAt = now;
4873
- queueMicrotask(flushImmediate);
4874
- } else if (t.timer === null) {
4875
- t.timer = setTimeout(() => {
4876
- t.timer = null;
4877
- t.lastAt = Date.now();
4878
- flushImmediate();
4879
- }, FLUSH_INTERVAL_MS - elapsed);
4880
- }
4910
+ schedulePromptImmediateFlush({
4911
+ throttle: flushThrottleRef.current,
4912
+ isSuppressed: () => interruptActiveRef.current,
4913
+ flush: flushImmediate
4914
+ });
4881
4915
  };
4916
+ useEffect3(() => () => {
4917
+ cancelPromptImmediateFlush(flushThrottleRef.current);
4918
+ }, []);
4882
4919
  const commitDraft = (next, options = {}) => {
4883
4920
  const sameDraft = draftStateEqual(draftRef.current, next);
4884
4921
  if (!options.keepPreferredColumn) preferredColumnRef.current = null;
@@ -8114,14 +8151,14 @@ function parseMemoryCoreRows(text) {
8114
8151
  currentProjectId = label === "COMMON" ? null : label;
8115
8152
  return null;
8116
8153
  }
8117
- const match = raw.match(/^id=(\d+)\s+\[([^\]]*)\]\s+(.+?)(?:\s+—\s+(.+))?$/);
8154
+ const match = raw.match(/^id=(\d+)\s+(.+?)(?:\s+—\s+(.+))?$/);
8118
8155
  if (match) {
8119
- const [, id, category, element, summary = ""] = match;
8156
+ const [, id, element, summary = ""] = match;
8120
8157
  return {
8121
8158
  value: `core-${id}`,
8122
8159
  // Display is summary-first: session injection only ever uses the
8123
- // summary sentence (buildSessionCoreMemoryPayload), so id/category/
8124
- // element are UI noise. They stay in the hidden _fields for
8160
+ // summary sentence (buildSessionCoreMemoryPayload), so id/element
8161
+ // are UI noise. The element stays in the hidden fields for
8125
8162
  // edit/delete plumbing.
8126
8163
  label: `#${id}`,
8127
8164
  meta: currentProjectId || "",
@@ -8129,7 +8166,6 @@ function parseMemoryCoreRows(text) {
8129
8166
  _line: raw,
8130
8167
  _action: "core-entry",
8131
8168
  _id: Number(id),
8132
- _category: category,
8133
8169
  _element: element,
8134
8170
  _summary: summary || element,
8135
8171
  _projectId: currentProjectId,
@@ -9293,6 +9329,24 @@ var STABLE_PREFIX_LRU_MAX = 32;
9293
9329
  function streamingLayoutText(text) {
9294
9330
  return String(text ?? "").replace(/^\n+|\n+$/g, "");
9295
9331
  }
9332
+ function windowPlainStreamingText(text, columns, maxRows) {
9333
+ const value = streamingLayoutText(text);
9334
+ const rowBudget = Math.max(0, Math.floor(Number(maxRows) || 0));
9335
+ if (!value || rowBudget <= 0 || hasMarkdownSyntax(value)) return value;
9336
+ const lines = value.split("\n");
9337
+ const width = Math.max(1, Math.floor(Number(columns) || 80));
9338
+ let rows = 0;
9339
+ let start = lines.length;
9340
+ while (start > 0) {
9341
+ const line = lines[start - 1];
9342
+ const lineRows = Math.max(1, Math.ceil(displayWidth(line) / width));
9343
+ if (rows > 0 && rows + lineRows > rowBudget) break;
9344
+ rows += lineRows;
9345
+ start -= 1;
9346
+ if (rows >= rowBudget) break;
9347
+ }
9348
+ return start > 0 ? lines.slice(start).join("\n") : value;
9349
+ }
9296
9350
  function isWhitespaceOnlyText(text) {
9297
9351
  return !String(text ?? "").trim();
9298
9352
  }
@@ -9532,7 +9586,7 @@ function measureStreamingMarkdownRenderedRows(text, columns, streamKey) {
9532
9586
  if (!value) return 1;
9533
9587
  const parts = resolveStreamingMarkdownParts(value, streamKey);
9534
9588
  if (parts.plain) {
9535
- return measureMarkdownRenderedRows(value, columns, { trimPartialFences: false });
9589
+ return estimateWrappedRowsFallback(parts.unstableForRender, columns, 3);
9536
9590
  }
9537
9591
  let rows = 0;
9538
9592
  let childCount = 0;
@@ -18532,7 +18586,7 @@ function Markdown({ children, themeEpoch = 0, trimPartialFences = false, columns
18532
18586
  function StreamingMarkdown({ children, themeEpoch = 0, columns, streamKey }) {
18533
18587
  const parts = resolveStreamingMarkdownParts(children, streamKey);
18534
18588
  if (parts.plain) {
18535
- return /* @__PURE__ */ jsx13(Markdown, { themeEpoch, columns, children: parts.unstableForRender });
18589
+ return /* @__PURE__ */ jsx13(Text13, { color: theme.text, wrap: "wrap", children: parts.unstableForRender });
18536
18590
  }
18537
18591
  return /* @__PURE__ */ jsxs9(Box11, { flexDirection: "column", gap: 1, children: [
18538
18592
  parts.stablePrefix ? /* @__PURE__ */ jsx13(Markdown, { themeEpoch, columns, children: parts.stablePrefix }) : null,
@@ -18547,15 +18601,17 @@ var AssistantMessage = React14.memo(function AssistantMessage2({
18547
18601
  streaming = false,
18548
18602
  columns = 80,
18549
18603
  themeEpoch = 0,
18550
- assistantId
18604
+ assistantId,
18605
+ streamingWindowRows = 0
18551
18606
  }) {
18552
18607
  React14.useEffect(() => {
18553
18608
  if (!streaming && assistantId) resetStreamingMarkdownStablePrefix(assistantId);
18554
18609
  }, [streaming, assistantId]);
18555
18610
  const bodyWidth = assistantBodyWidth(columns);
18611
+ const renderText = streaming && streamingWindowRows > 0 ? windowPlainStreamingText(text, bodyWidth, streamingWindowRows) : text;
18556
18612
  return /* @__PURE__ */ jsxs10(Box12, { flexDirection: "row", marginTop: 1, children: [
18557
18613
  /* @__PURE__ */ jsx14(Box12, { flexShrink: 0, minWidth: 2, children: /* @__PURE__ */ jsx14(Text14, { color: theme.text, children: TURN_MARKER }) }),
18558
- /* @__PURE__ */ jsx14(Box12, { flexDirection: "column", flexShrink: 0, width: bodyWidth, children: streaming ? /* @__PURE__ */ jsx14(StreamingMarkdown, { themeEpoch, columns: bodyWidth, streamKey: assistantId, children: text }) : /* @__PURE__ */ jsx14(Markdown, { themeEpoch, columns: bodyWidth, children: text }) })
18614
+ /* @__PURE__ */ jsx14(Box12, { flexDirection: "column", flexShrink: 0, width: bodyWidth, children: streaming ? /* @__PURE__ */ jsx14(StreamingMarkdown, { themeEpoch, columns: bodyWidth, streamKey: assistantId, children: renderText }) : /* @__PURE__ */ jsx14(Markdown, { themeEpoch, columns: bodyWidth, children: text }) })
18559
18615
  ] });
18560
18616
  });
18561
18617
  var UserMessage = React14.memo(function UserMessage2({ text, attached = false, columns, themeEpoch = 0 }) {
@@ -19009,17 +19065,22 @@ function toolSearchLoadedSummary(resultText) {
19009
19065
  parsed = JSON.parse(String(resultText || ""));
19010
19066
  } catch {
19011
19067
  const text = String(resultText || "");
19012
- const match = /^Loaded deferred tools:\s*(.+)$/m.exec(text);
19013
- if (!match) return "";
19014
- return [...new Set(match[1].split(",").map((name) => name.trim()).filter(Boolean))].join(", ");
19068
+ const loaded2 = /^Loaded deferred tools:\s*(.+)$/m.exec(text)?.[1] || "";
19069
+ const already2 = /^Already active:\s*(.+)$/m.exec(text)?.[1] || "";
19070
+ return [
19071
+ ...loaded2 ? [`Loaded: ${loaded2}`] : [],
19072
+ ...already2 ? [`Already active: ${already2}`] : []
19073
+ ].join(" \xB7 ");
19015
19074
  }
19016
19075
  const tools = parsed?.selected?.tools;
19017
19076
  if (!tools || typeof tools !== "object") return "";
19018
- const names = [
19019
- ...Array.isArray(tools.added) ? tools.added : [],
19020
- ...Array.isArray(tools.already) ? tools.already : []
19021
- ].map((name) => String(name || "").trim()).filter(Boolean);
19022
- return [...new Set(names)].join(", ");
19077
+ const uniqueNames = (names) => [...new Set((Array.isArray(names) ? names : []).map((name) => String(name || "").trim()).filter(Boolean))];
19078
+ const loaded = uniqueNames(tools.added);
19079
+ const already = uniqueNames(tools.already);
19080
+ return [
19081
+ ...loaded.length ? [`Loaded: ${loaded.join(", ")}`] : [],
19082
+ ...already.length ? [`Already active: ${already.join(", ")}`] : []
19083
+ ].join(" \xB7 ");
19023
19084
  }
19024
19085
  function agentTerminalDetail(status, isError, elapsed, error = "") {
19025
19086
  const failureDetail = isError && error ? backgroundTaskFailureStatusLabel(status, error, { surface: "agent" }) : "";
@@ -19424,7 +19485,7 @@ function ToolHookDenialCard({ item, columns = 80 }) {
19424
19485
  ] }) : null
19425
19486
  ] });
19426
19487
  }
19427
- var Item = React19.memo(function Item2({ item, prevKind, columns, toolOutputExpanded, rightMessage = "", rightTone = "info", rightMessageWidth = 24, themeEpoch = 0 }) {
19488
+ var Item = React19.memo(function Item2({ item, prevKind, columns, toolOutputExpanded, rightMessage = "", rightTone = "info", rightMessageWidth = 24, themeEpoch = 0, streamingWindowRows = 0 }) {
19428
19489
  const hintOnTurnDoneRow = item.kind === "turndone" || item.kind === "statusdone";
19429
19490
  let node = null;
19430
19491
  switch (item.kind) {
@@ -19432,7 +19493,7 @@ var Item = React19.memo(function Item2({ item, prevKind, columns, toolOutputExpa
19432
19493
  node = /* @__PURE__ */ jsx19(UserMessage, { text: item.text, attached: prevKind === "user", columns, themeEpoch });
19433
19494
  break;
19434
19495
  case "assistant":
19435
- node = /* @__PURE__ */ jsx19(AssistantMessage, { text: item.text, streaming: item.streaming, columns, themeEpoch, assistantId: item.id });
19496
+ node = /* @__PURE__ */ jsx19(AssistantMessage, { text: item.text, streaming: item.streaming, columns, themeEpoch, assistantId: item.id, streamingWindowRows });
19436
19497
  break;
19437
19498
  case "tool": {
19438
19499
  if (shouldSuppressFullyFailedToolItem(item)) return null;
@@ -21712,7 +21773,8 @@ function App({ store, initialStatusLine = "", forceOnboarding = false }) {
21712
21773
  rightMessage: attachOverlayHint ? inputHint : "",
21713
21774
  rightTone: attachOverlayHint ? inputHintTone : "info",
21714
21775
  rightMessageWidth: attachOverlayHint ? guardHintWidth || transientStatusWidth || 24 : 24,
21715
- themeEpoch: state.themeEpoch || 0
21776
+ themeEpoch: state.themeEpoch || 0,
21777
+ streamingWindowRows: transcriptTailPinned && item.id === state.streamingTail?.id ? transcriptContentHeight + TRANSCRIPT_WINDOW_TAIL_OVERSCAN_ROWS : 0
21716
21778
  }
21717
21779
  );
21718
21780
  return measureRef ? /* @__PURE__ */ jsx20(Box18, { ref: measureRef, flexDirection: "column", flexShrink: 0, children: itemNode }, item.id) : /* @__PURE__ */ jsx20(React20.Fragment, { children: itemNode }, item.id);
@@ -23520,6 +23582,9 @@ function createAgentJobFeed({
23520
23582
  const terminalExecutionNotificationKeys = /* @__PURE__ */ new Set();
23521
23583
  const terminalExecutionResponseKeys = /* @__PURE__ */ new Set();
23522
23584
  const executionNotificationKeys = /* @__PURE__ */ new Map();
23585
+ const AGENT_STATUS_COALESCE_MS = 16;
23586
+ let agentStatusRefreshTimer = null;
23587
+ let agentStatusRefreshForce = false;
23523
23588
  const clearExecutionDedupState = () => {
23524
23589
  displayedExecutionNotificationKeys.clear();
23525
23590
  displayedExecutionResponseStates.clear();
@@ -23668,7 +23733,16 @@ function createAgentJobFeed({
23668
23733
  if (!parsed?.taskId) return;
23669
23734
  const status = String(parsed.status || "").toLowerCase();
23670
23735
  const terminal = /^(completed|complete|done|success|succeeded|ok|failed|error|timeout|killed|cancelled|canceled|denied)$/.test(status);
23671
- set(agentStatusState(terminal ? { force: true } : void 0));
23736
+ agentStatusRefreshForce = agentStatusRefreshForce || terminal;
23737
+ if (agentStatusRefreshTimer) return;
23738
+ agentStatusRefreshTimer = setTimeout(() => {
23739
+ agentStatusRefreshTimer = null;
23740
+ if (getDisposed()) return;
23741
+ const force = agentStatusRefreshForce;
23742
+ agentStatusRefreshForce = false;
23743
+ set(agentStatusState(force ? { force: true } : void 0));
23744
+ }, AGENT_STATUS_COALESCE_MS);
23745
+ agentStatusRefreshTimer.unref?.();
23672
23746
  }
23673
23747
  function subscribeRuntimeNotifications() {
23674
23748
  if (typeof runtime.onNotification !== "function") return null;
@@ -23780,6 +23854,9 @@ function createAgentJobFeed({
23780
23854
  try {
23781
23855
  unsubscribe?.();
23782
23856
  } finally {
23857
+ if (agentStatusRefreshTimer) clearTimeout(agentStatusRefreshTimer);
23858
+ agentStatusRefreshTimer = null;
23859
+ agentStatusRefreshForce = false;
23783
23860
  clearExecutionDedupState();
23784
23861
  }
23785
23862
  };
@@ -25460,8 +25537,9 @@ function createRunTurn(bag) {
25460
25537
  label: compactEventLabel(event),
25461
25538
  detail: compactEventDetail(event)
25462
25539
  });
25540
+ syncContextStats({ allowEstimated: true });
25463
25541
  },
25464
- onStageChange: async (stage) => {
25542
+ onStageChange: async (stage, detail = null) => {
25465
25543
  if (!markTurnProgress(`stage:${String(stage || "")}`)) return;
25466
25544
  if (!getState().spinner) return;
25467
25545
  const value = String(stage || "");
@@ -25484,6 +25562,13 @@ function createRunTurn(bag) {
25484
25562
  await yieldToRenderer();
25485
25563
  return;
25486
25564
  }
25565
+ if (value === "reconnecting") {
25566
+ compactingActive = false;
25567
+ const retryVerb = String(detail?.message || "Reconnecting");
25568
+ set({ spinner: { ...getState().spinner, mode: "reconnecting", verb: retryVerb } });
25569
+ await yieldToRenderer();
25570
+ return;
25571
+ }
25487
25572
  if (value === "requesting" || value === "streaming") compactingActive = false;
25488
25573
  const mode = value === "requesting" ? "requesting" : value === "streaming" ? getState().spinner.thinking ? "thinking" : "responding" : null;
25489
25574
  if (!mode || getState().spinner.mode === mode) return;
@@ -79,6 +79,9 @@ export function createAgentJobFeed({
79
79
  const terminalExecutionNotificationKeys = new Set();
80
80
  const terminalExecutionResponseKeys = new Set();
81
81
  const executionNotificationKeys = new Map();
82
+ const AGENT_STATUS_COALESCE_MS = 16;
83
+ let agentStatusRefreshTimer = null;
84
+ let agentStatusRefreshForce = false;
82
85
 
83
86
  const clearExecutionDedupState = () => {
84
87
  displayedExecutionNotificationKeys.clear();
@@ -261,7 +264,16 @@ export function createAgentJobFeed({
261
264
  if (!parsed?.taskId) return;
262
265
  const status = String(parsed.status || '').toLowerCase();
263
266
  const terminal = /^(completed|complete|done|success|succeeded|ok|failed|error|timeout|killed|cancelled|canceled|denied)$/.test(status);
264
- set(agentStatusState(terminal ? { force: true } : undefined));
267
+ agentStatusRefreshForce = agentStatusRefreshForce || terminal;
268
+ if (agentStatusRefreshTimer) return;
269
+ agentStatusRefreshTimer = setTimeout(() => {
270
+ agentStatusRefreshTimer = null;
271
+ if (getDisposed()) return;
272
+ const force = agentStatusRefreshForce;
273
+ agentStatusRefreshForce = false;
274
+ set(agentStatusState(force ? { force: true } : undefined));
275
+ }, AGENT_STATUS_COALESCE_MS);
276
+ agentStatusRefreshTimer.unref?.();
265
277
  }
266
278
 
267
279
  function subscribeRuntimeNotifications() {
@@ -399,7 +411,14 @@ export function createAgentJobFeed({
399
411
  return true;
400
412
  });
401
413
  return () => {
402
- try { unsubscribe?.(); } finally { clearExecutionDedupState(); }
414
+ try {
415
+ unsubscribe?.();
416
+ } finally {
417
+ if (agentStatusRefreshTimer) clearTimeout(agentStatusRefreshTimer);
418
+ agentStatusRefreshTimer = null;
419
+ agentStatusRefreshForce = false;
420
+ clearExecutionDedupState();
421
+ }
403
422
  };
404
423
  }
405
424
 
@@ -1007,8 +1007,12 @@ export function createRunTurn(bag) {
1007
1007
  label: compactEventLabel(event),
1008
1008
  detail: compactEventDetail(event),
1009
1009
  });
1010
+ // Compaction itself remains owned by the pre-provider-send pass.
1011
+ // This event only refreshes the gauge from the already-mutated
1012
+ // transcript before another render can show stale pressure.
1013
+ syncContextStats({ allowEstimated: true });
1010
1014
  },
1011
- onStageChange: async (stage) => {
1015
+ onStageChange: async (stage, detail = null) => {
1012
1016
  if (!markTurnProgress(`stage:${String(stage || '')}`)) return;
1013
1017
  if (!getState().spinner) return;
1014
1018
  const value = String(stage || '');
@@ -1031,6 +1035,13 @@ export function createRunTurn(bag) {
1031
1035
  await yieldToRenderer();
1032
1036
  return;
1033
1037
  }
1038
+ if (value === 'reconnecting') {
1039
+ compactingActive = false;
1040
+ const retryVerb = String(detail?.message || 'Reconnecting');
1041
+ set({ spinner: { ...getState().spinner, mode: 'reconnecting', verb: retryVerb } });
1042
+ await yieldToRenderer();
1043
+ return;
1044
+ }
1034
1045
  if (value === 'requesting' || value === 'streaming') compactingActive = false;
1035
1046
  const mode = value === 'requesting'
1036
1047
  ? 'requesting'
@@ -67,7 +67,7 @@ export function measureStreamingMarkdownRenderedRows(text, columns, streamKey) {
67
67
  if (!value) return 1;
68
68
  const parts = resolveStreamingMarkdownParts(value, streamKey);
69
69
  if (parts.plain) {
70
- return measureMarkdownRenderedRows(value, columns, { trimPartialFences: false });
70
+ return estimateWrappedRowsFallback(parts.unstableForRender, columns, 3);
71
71
  }
72
72
  let rows = 0;
73
73
  let childCount = 0;
@@ -5,6 +5,7 @@
5
5
  import { marked } from 'marked';
6
6
  import { configureMarked, hasMarkdownSyntax } from './render-ansi.mjs';
7
7
  import { trimPartialClosingFences, findOpenFenceStart } from './stream-fence.mjs';
8
+ import { displayWidth } from '../display-width.mjs';
8
9
 
9
10
  const stablePrefixByStreamKey = new Map();
10
11
  // Reuse the current normalized-text split across measure → render → harvest.
@@ -16,6 +17,25 @@ export function streamingLayoutText(text) {
16
17
  return String(text ?? '').replace(/^\n+|\n+$/g, '');
17
18
  }
18
19
 
20
+ export function windowPlainStreamingText(text, columns, maxRows) {
21
+ const value = streamingLayoutText(text);
22
+ const rowBudget = Math.max(0, Math.floor(Number(maxRows) || 0));
23
+ if (!value || rowBudget <= 0 || hasMarkdownSyntax(value)) return value;
24
+ const lines = value.split('\n');
25
+ const width = Math.max(1, Math.floor(Number(columns) || 80));
26
+ let rows = 0;
27
+ let start = lines.length;
28
+ while (start > 0) {
29
+ const line = lines[start - 1];
30
+ const lineRows = Math.max(1, Math.ceil(displayWidth(line) / width));
31
+ if (rows > 0 && rows + lineRows > rowBudget) break;
32
+ rows += lineRows;
33
+ start -= 1;
34
+ if (rows >= rowBudget) break;
35
+ }
36
+ return start > 0 ? lines.slice(start).join('\n') : value;
37
+ }
38
+
19
39
  function isWhitespaceOnlyText(text) {
20
40
  return !String(text ?? '').trim();
21
41
  }
@@ -189,7 +189,7 @@ function displayContextBoundary({
189
189
  return modelContextWindow('', '', boundarySeed);
190
190
  }
191
191
 
192
- function resolveContextUsedPct({
192
+ export function resolveContextUsedPct({
193
193
  provider = '',
194
194
  model = '',
195
195
  stats = null,
@@ -210,10 +210,10 @@ function resolveContextUsedPct({
210
210
  autoCompactTokenLimit,
211
211
  compact,
212
212
  });
213
- // Boundary-only denominator: context % is always measured against the single
214
- // compaction boundary value (shared with the gateway), never against a
215
- // separate auto-compact trigger. This keeps the statusline and gateway
216
- // display on the same denominator with no before/after trigger semantics.
213
+ const trigger = num(autoCompactTokenLimit);
214
+ // A resolved trigger means this is the runtime compaction gauge: keep both
215
+ // operands local so a gateway boundary percentage cannot replace it.
216
+ if (trigger > 0) return (numerator / trigger) * 100;
217
217
  const gatewayRawPct = gatewayStatus?.contextUsedPct;
218
218
  if (
219
219
  gatewayStatus