mixdog 0.9.46 → 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 (90) 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/defaults/cycle3-review-prompt.md +10 -4
  32. package/src/defaults/memory-promote-prompt.md +4 -3
  33. package/src/runtime/agent/orchestrator/agent-trace-format.mjs +16 -5
  34. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +35 -11
  35. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +35 -11
  36. package/src/runtime/agent/orchestrator/providers/custom-tool-wire.mjs +28 -0
  37. package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +1 -1
  38. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +34 -34
  39. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +45 -38
  40. package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +36 -11
  41. package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +4 -4
  42. package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +10 -6
  43. package/src/runtime/agent/orchestrator/session/loop/deferred-call-through.mjs +4 -3
  44. package/src/runtime/agent/orchestrator/session/loop/recall-fasttrack.mjs +4 -3
  45. package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +16 -1
  46. package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +2 -2
  47. package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +0 -1
  48. package/src/runtime/agent/orchestrator/session/tool-batch.mjs +13 -8
  49. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +30 -16
  50. package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +25 -8
  51. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +69 -4
  52. package/src/runtime/agent/orchestrator/tools/code-graph-prewarm-worker.mjs +5 -4
  53. package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +1 -1
  54. package/src/runtime/agent/orchestrator/tools/graph-manifest.json +12 -12
  55. package/src/runtime/agent/orchestrator/tools/patch/native-server.mjs +12 -7
  56. package/src/runtime/agent/orchestrator/tools/patch-binary-fetcher.mjs +77 -18
  57. package/src/runtime/agent/orchestrator/tools/patch-manifest.json +11 -11
  58. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +99 -24
  59. package/src/runtime/memory/lib/memory-action-handlers.mjs +21 -11
  60. package/src/runtime/memory/lib/memory-cycle2-gate.mjs +13 -5
  61. package/src/runtime/memory/lib/memory-cycle2.mjs +8 -5
  62. package/src/runtime/memory/lib/memory-cycle3.mjs +41 -6
  63. package/src/runtime/memory/lib/query-handlers.mjs +49 -6
  64. package/src/runtime/memory/lib/recall-format.mjs +21 -6
  65. package/src/runtime/memory/tool-defs.mjs +2 -3
  66. package/src/session-runtime/context-status.mjs +25 -16
  67. package/src/session-runtime/model-route-api.mjs +2 -0
  68. package/src/session-runtime/runtime-core.mjs +2 -2
  69. package/src/session-runtime/session-turn-api.mjs +4 -1
  70. package/src/session-runtime/tool-catalog.mjs +113 -19
  71. package/src/session-runtime/tool-defs.mjs +1 -0
  72. package/src/standalone/agent-tool/helpers.mjs +25 -6
  73. package/src/standalone/agent-tool.mjs +75 -41
  74. package/src/tui/App.jsx +4 -0
  75. package/src/tui/app/input-parsers.mjs +8 -9
  76. package/src/tui/components/Markdown.jsx +6 -1
  77. package/src/tui/components/Message.jsx +11 -2
  78. package/src/tui/components/PromptInput.jsx +19 -21
  79. package/src/tui/components/Spinner.jsx +4 -4
  80. package/src/tui/components/StatusLine.jsx +6 -6
  81. package/src/tui/components/TranscriptItem.jsx +2 -2
  82. package/src/tui/components/prompt-input/immediate-render.mjs +47 -0
  83. package/src/tui/components/tool-execution/surface-detail.mjs +14 -9
  84. package/src/tui/dist/index.mjs +130 -45
  85. package/src/tui/engine/agent-job-feed.mjs +21 -2
  86. package/src/tui/engine/turn.mjs +12 -1
  87. package/src/tui/markdown/measure-rendered-rows.mjs +1 -1
  88. package/src/tui/markdown/streaming-markdown.mjs +20 -0
  89. package/src/ui/statusline.mjs +5 -5
  90. package/src/vendor/statusline/src/gateway/session-routes.mjs +22 -10
@@ -28,6 +28,7 @@ export {
28
28
  resetStreamingMarkdownStablePrefix,
29
29
  resetAllStreamingMarkdownStablePrefixes,
30
30
  streamingLayoutText,
31
+ windowPlainStreamingText,
31
32
  } from '../markdown/streaming-markdown.mjs';
32
33
  export {
33
34
  measureMarkdownRenderedRows,
@@ -82,7 +83,11 @@ export function Markdown({ children, themeEpoch = 0, trimPartialFences = false,
82
83
  export function StreamingMarkdown({ children, themeEpoch = 0, columns, streamKey }) {
83
84
  const parts = resolveStreamingMarkdownParts(children, streamKey);
84
85
  if (parts.plain) {
85
- return <Markdown themeEpoch={themeEpoch} columns={columns}>{parts.unstableForRender}</Markdown>;
86
+ // Plain streaming text has no markdown tokens to style. Sending a growing
87
+ // multi-line tail through renderTokenAnsiSegments/marked on every delta
88
+ // reparses the whole response and dominates frame time; Ink can wrap the
89
+ // identical visible text directly.
90
+ return <Text color={theme.text} wrap="wrap">{parts.unstableForRender}</Text>;
86
91
  }
87
92
  return (
88
93
  <Box flexDirection="column" gap={1}>
@@ -15,7 +15,12 @@
15
15
  import React from 'react';
16
16
  import { Box, Text } from 'ink';
17
17
  import { theme, TURN_MARKER } from '../theme.mjs';
18
- import { Markdown, StreamingMarkdown, resetStreamingMarkdownStablePrefix } from './Markdown.jsx';
18
+ import {
19
+ Markdown,
20
+ StreamingMarkdown,
21
+ resetStreamingMarkdownStablePrefix,
22
+ windowPlainStreamingText,
23
+ } from './Markdown.jsx';
19
24
  import { assistantBodyWidth } from '../markdown/table-layout.mjs';
20
25
 
21
26
  // `themeEpoch` is a memo-busting prop (threaded from App): the active theme
@@ -28,6 +33,7 @@ export const AssistantMessage = React.memo(function AssistantMessage({
28
33
  columns = 80,
29
34
  themeEpoch = 0,
30
35
  assistantId,
36
+ streamingWindowRows = 0,
31
37
  }) {
32
38
  // The body column needs an EXPLICIT numeric width. Without it, ink/Yoga
33
39
  // measures the wrapped markdown body before the row's available width is
@@ -43,6 +49,9 @@ export const AssistantMessage = React.memo(function AssistantMessage({
43
49
  }, [streaming, assistantId]);
44
50
 
45
51
  const bodyWidth = assistantBodyWidth(columns);
52
+ const renderText = streaming && streamingWindowRows > 0
53
+ ? windowPlainStreamingText(text, bodyWidth, streamingWindowRows)
54
+ : text;
46
55
  return (
47
56
  <Box flexDirection="row" marginTop={1}>
48
57
  <Box flexShrink={0} minWidth={2}>
@@ -50,7 +59,7 @@ export const AssistantMessage = React.memo(function AssistantMessage({
50
59
  </Box>
51
60
  <Box flexDirection="column" flexShrink={0} width={bodyWidth}>
52
61
  {streaming
53
- ? <StreamingMarkdown themeEpoch={themeEpoch} columns={bodyWidth} streamKey={assistantId}>{text}</StreamingMarkdown>
62
+ ? <StreamingMarkdown themeEpoch={themeEpoch} columns={bodyWidth} streamKey={assistantId}>{renderText}</StreamingMarkdown>
54
63
  : <Markdown themeEpoch={themeEpoch} columns={bodyWidth}>{text}</Markdown>}
55
64
  </Box>
56
65
  </Box>
@@ -49,6 +49,10 @@ import {
49
49
  isModifiedEnterSequence,
50
50
  isAnyModifiedEnterSequence,
51
51
  } from './prompt-input/edit-helpers.mjs';
52
+ import {
53
+ cancelPromptImmediateFlush,
54
+ schedulePromptImmediateFlush,
55
+ } from './prompt-input/immediate-render.mjs';
52
56
 
53
57
  // Windows Terminal IME composition can clip a glyph that starts exactly at the
54
58
  // left edge of the editable text node. The rounded prompt box already adds a
@@ -108,6 +112,7 @@ export function PromptInput({
108
112
  });
109
113
  const [, bumpCursorAnchorEpoch] = useState(0);
110
114
  const draftRef = useRef(draft);
115
+ const interruptActiveRef = useRef(interruptActive);
111
116
  const lastReportedValueRef = useRef(draft.value);
112
117
  // Bumped on every submit/draftOverride replace/unmount so an async paste
113
118
  // (clipboard read or onPasteText promise) that resolves after the draft
@@ -132,6 +137,7 @@ export function PromptInput({
132
137
  const undoRef = useRef({ past: [], future: [], lastPushAt: 0, lastValue: null });
133
138
  const { value, cursor } = draft;
134
139
  draftRef.current = draft;
140
+ interruptActiveRef.current = interruptActive;
135
141
  if (selectionRef) {
136
142
  const range = selectionRange(draft);
137
143
  selectionRef.current = range
@@ -158,30 +164,22 @@ export function PromptInput({
158
164
  }
159
165
  };
160
166
 
161
- // commitDraft fires on every keystroke; a raw queueMicrotask(flushImmediate)
162
- // per commit bypassed ink's maxFps and forced an unthrottled render for each
163
- // char (a burst of paste/held-key edits = a render storm). Coalesce to a
164
- // frame budget: flush the first keystroke immediately (no caret lag) and
165
- // batch the rest to one flush per ~16ms, with a trailing flush so the final
166
- // char of a burst always lands.
167
- const FLUSH_INTERVAL_MS = 16;
167
+ // During an active turn, ink is already painting streaming updates at its
168
+ // 60fps budget. A second unthrottled input render competes with those frames
169
+ // and makes key echo slower, so busy input stays on ink's normal render path.
170
+ // Idle input keeps the leading+trailing immediate flush for crisp caret echo.
168
171
  const scheduleImmediateFlush = () => {
169
- const t = flushThrottleRef.current;
170
- const now = Date.now();
171
- const elapsed = now - t.lastAt;
172
- if (elapsed >= FLUSH_INTERVAL_MS) {
173
- if (t.timer !== null) { clearTimeout(t.timer); t.timer = null; }
174
- t.lastAt = now;
175
- queueMicrotask(flushImmediate);
176
- } else if (t.timer === null) {
177
- t.timer = setTimeout(() => {
178
- t.timer = null;
179
- t.lastAt = Date.now();
180
- flushImmediate();
181
- }, FLUSH_INTERVAL_MS - elapsed);
182
- }
172
+ schedulePromptImmediateFlush({
173
+ throttle: flushThrottleRef.current,
174
+ isSuppressed: () => interruptActiveRef.current,
175
+ flush: flushImmediate,
176
+ });
183
177
  };
184
178
 
179
+ useEffect(() => () => {
180
+ cancelPromptImmediateFlush(flushThrottleRef.current);
181
+ }, []);
182
+
185
183
  const commitDraft = (next, options = {}) => {
186
184
  const sameDraft = draftStateEqual(draftRef.current, next);
187
185
  if (!options.keepPreferredColumn) preferredColumnRef.current = null;
@@ -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;