mixdog 0.9.26 → 0.9.27
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.
- package/package.json +2 -1
- package/scripts/arg-guard-test.mjs +68 -0
- package/scripts/compacted-placeholder-scrub-test.mjs +77 -0
- package/src/cli.mjs +40 -4
- package/src/rules/shared/01-tool.md +3 -0
- package/src/runtime/agent/orchestrator/agent-trace-format.mjs +3 -1
- package/src/runtime/agent/orchestrator/session/loop/stored-tool-args.mjs +30 -0
- package/src/runtime/agent/orchestrator/session/loop/transcript-repair.mjs +5 -0
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +14 -1
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +1 -1
- package/src/runtime/memory/tool-defs.mjs +2 -2
- package/src/runtime/shared/staged-install-worker.mjs +14 -0
- package/src/runtime/shared/staged-update.mjs +530 -0
- package/src/runtime/shared/update-checker.mjs +1 -1
- package/src/session-runtime/lifecycle-api.mjs +9 -11
- package/src/session-runtime/runtime-core.mjs +35 -36
- package/src/standalone/agent-tool/tool-def.mjs +1 -1
- package/src/tui/app/transcript-window.mjs +26 -6
- package/src/tui/components/Spinner.jsx +5 -2
- package/src/tui/components/StatusLine.jsx +7 -7
- package/src/tui/components/ToolExecution.jsx +24 -58
- package/src/tui/display-width.mjs +8 -4
- package/src/tui/dist/index.mjs +373 -269
- package/src/tui/hooks/useSharedTick.mjs +103 -0
- package/src/tui/markdown/format-token.mjs +46 -8
- package/src/tui/markdown/render-ansi.mjs +24 -1
- package/src/tui/markdown/stream-fence.mjs +60 -0
- package/src/tui/markdown/streaming-markdown.mjs +22 -1
- package/vendor/ink/build/display-width.js +5 -4
|
@@ -15,7 +15,8 @@ import { writeLastSessionCwd } from '../runtime/shared/user-cwd.mjs';
|
|
|
15
15
|
import { cancelBackgroundTasks } from '../runtime/shared/background-tasks.mjs';
|
|
16
16
|
import { createTranscriptWriter } from '../runtime/shared/transcript-writer.mjs';
|
|
17
17
|
import { mixdogHome } from '../runtime/shared/plugin-paths.mjs';
|
|
18
|
-
import { checkLatestVersion,
|
|
18
|
+
import { checkLatestVersion, localPackageVersion, isDevInstall } from '../runtime/shared/update-checker.mjs';
|
|
19
|
+
import { spawnStagedInstall, runStagedInstall } from '../runtime/shared/staged-update.mjs';
|
|
19
20
|
import {
|
|
20
21
|
modelVisibleToolCompletionMessage,
|
|
21
22
|
shouldPersistModelVisibleToolCompletion,
|
|
@@ -634,12 +635,14 @@ export async function createMixdogSessionRuntime({
|
|
|
634
635
|
};
|
|
635
636
|
// phase: 'idle' | 'checking' | 'installing' | 'installed' | 'failed'
|
|
636
637
|
let updateProcessState = { phase: 'idle', version: null, error: null };
|
|
637
|
-
// Boot detects an available update
|
|
638
|
-
//
|
|
639
|
-
//
|
|
640
|
-
//
|
|
641
|
-
//
|
|
642
|
-
|
|
638
|
+
// Boot detects an available update and STAGES it in the background (a hidden,
|
|
639
|
+
// detached npm install into ~/.mixdog/data/staging/<ver>). The staged package
|
|
640
|
+
// is swapped into the global dir on the next clean launch (cli.mjs
|
|
641
|
+
// pre-import), never while a session is live — so npm never overwrites the
|
|
642
|
+
// .mjs files node currently holds (the old shutdown `npm install -g` did, and
|
|
643
|
+
// caused Windows TAR_ENTRY_ERROR / ENOENT). The live-session refcount that a
|
|
644
|
+
// concurrent launch consults to defer its swap is registered earlier, in
|
|
645
|
+
// cli.mjs (pre-import), so this process is visible before any runtime loads.
|
|
643
646
|
|
|
644
647
|
function autoUpdateEnabled() {
|
|
645
648
|
return config?.update?.auto !== false;
|
|
@@ -668,14 +671,23 @@ export async function createMixdogSessionRuntime({
|
|
|
668
671
|
if (updateProcessState.phase === 'installing') {
|
|
669
672
|
return { ...updateProcessState, alreadyInstalling: true, error: 'update already in progress' };
|
|
670
673
|
}
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
674
|
+
if (isDevInstall()) {
|
|
675
|
+
updateProcessState = { phase: 'failed', version: null, error: 'dev install — update skipped' };
|
|
676
|
+
return updateProcessState;
|
|
677
|
+
}
|
|
678
|
+
const ver = updateCheckState.latestVersion;
|
|
679
|
+
if (!ver || !updateCheckState.updateAvailable) {
|
|
680
|
+
updateProcessState = { phase: 'idle', version: null, error: null };
|
|
681
|
+
return { ...updateProcessState, error: 'no update available' };
|
|
682
|
+
}
|
|
683
|
+
// "Update now" stages the new version (verified, self-contained) rather than
|
|
684
|
+
// installing over the live global dir; the swap applies on the next launch.
|
|
685
|
+
// phase 'installed' here means "staged & ready — restart to apply".
|
|
686
|
+
updateProcessState = { phase: 'installing', version: ver, error: null };
|
|
675
687
|
try {
|
|
676
|
-
const result = await
|
|
688
|
+
const result = await runStagedInstall(ver);
|
|
677
689
|
if (result?.ok) {
|
|
678
|
-
updateProcessState = { phase: 'installed', version: result.version ||
|
|
690
|
+
updateProcessState = { phase: 'installed', version: result.version || ver, error: null };
|
|
679
691
|
} else {
|
|
680
692
|
updateProcessState = { phase: 'failed', version: null, error: result?.error || 'update failed' };
|
|
681
693
|
}
|
|
@@ -689,43 +701,31 @@ export async function createMixdogSessionRuntime({
|
|
|
689
701
|
// constructed (setTimeout(0) defers past the synchronous return), so a
|
|
690
702
|
// slow/hanging registry request can never delay session boot. The check
|
|
691
703
|
// ALWAYS runs (populates updateCheckState for the maintenance picker), but
|
|
692
|
-
//
|
|
693
|
-
//
|
|
694
|
-
//
|
|
704
|
+
// when an update is available it kicks off a hidden BACKGROUND staging
|
|
705
|
+
// install (spawnStagedInstall) into ~/.mixdog/data/staging/<ver>. The actual
|
|
706
|
+
// swap into the global dir happens on the next clean launch (cli.mjs
|
|
707
|
+
// pre-import), so npm never overwrites files this live process holds.
|
|
695
708
|
// force:true — always hit the registry at boot (the 24h disk cache went
|
|
696
709
|
// stale-visible: it kept reporting an older "latest" than the installed
|
|
697
710
|
// version). checkLatestVersion() still falls back to the cache offline.
|
|
698
711
|
// isDevInstall() gate: a git checkout / clone (or non-node_modules install)
|
|
699
|
-
// must never self-update —
|
|
712
|
+
// must never self-update — staging + swap would fight the working tree.
|
|
700
713
|
const updateBootTimer = setTimeout(() => {
|
|
701
714
|
void (async () => {
|
|
702
715
|
await checkForUpdateInternal({ force: true });
|
|
703
716
|
if (autoUpdateEnabled() && !isDevInstall() && updateCheckState.updateAvailable) {
|
|
704
|
-
|
|
705
|
-
|
|
717
|
+
const ver = updateCheckState.latestVersion;
|
|
718
|
+
try { spawnStagedInstall(ver); } catch { /* best-effort background stage */ }
|
|
719
|
+
const v = ver ? `v${ver}` : 'latest';
|
|
706
720
|
// UI-only notice (TUI maps meta.kind 'update-notice' to a transcript
|
|
707
721
|
// notice, never a model-visible message). tone 'info': non-urgent,
|
|
708
|
-
//
|
|
709
|
-
emitRuntimeNotification(`mixdog ${v}
|
|
722
|
+
// staging runs quietly and applies on the next launch.
|
|
723
|
+
emitRuntimeNotification(`mixdog ${v} staging in background — applies on next launch.`, { kind: 'update-notice', tone: 'info' });
|
|
710
724
|
}
|
|
711
725
|
})().catch(() => {});
|
|
712
726
|
}, 0);
|
|
713
727
|
updateBootTimer.unref?.();
|
|
714
728
|
|
|
715
|
-
// Invoked from the graceful shutdown path (lifecycle close() on exit/quit).
|
|
716
|
-
// Spawns the deferred npm install fire-and-forget: runGlobalUpdate() detaches
|
|
717
|
-
// (POSIX) / unrefs its child, so we neither await it nor block teardown —
|
|
718
|
-
// the parent exits, releasing file handles, while npm installs in the hidden
|
|
719
|
-
// background child. No-op unless an update was armed at boot.
|
|
720
|
-
function flushPendingUpdate() {
|
|
721
|
-
if (!pendingUpdateInstall) return;
|
|
722
|
-
pendingUpdateInstall = false;
|
|
723
|
-
// Re-check at flush time: the user may have turned update.auto off after
|
|
724
|
-
// the boot notice armed the install — respect the current setting.
|
|
725
|
-
if (!autoUpdateEnabled()) return;
|
|
726
|
-
try { void runGlobalUpdate(); } catch { /* best-effort: exit must not wedge */ }
|
|
727
|
-
}
|
|
728
|
-
|
|
729
729
|
function emitRuntimeNotification(content, meta = {}) {
|
|
730
730
|
const text = String(content || '').trim();
|
|
731
731
|
if (!text) return false;
|
|
@@ -2003,7 +2003,6 @@ export async function createMixdogSessionRuntime({
|
|
|
2003
2003
|
resolveRoute,
|
|
2004
2004
|
applyDeferredToolSurface,
|
|
2005
2005
|
standaloneTools,
|
|
2006
|
-
flushPendingUpdate,
|
|
2007
2006
|
});
|
|
2008
2007
|
const resourceApi = createResourceApi({
|
|
2009
2008
|
getConfig: () => config,
|
|
@@ -40,7 +40,7 @@ export const AGENT_TOOL = {
|
|
|
40
40
|
type: 'object',
|
|
41
41
|
properties: {
|
|
42
42
|
type: { type: 'string', enum: ['spawn', 'send', 'list', 'close', 'cancel', 'status', 'read', 'cleanup'], description: 'Action. Default spawn.' },
|
|
43
|
-
task_id: { type: 'string', description: 'Manual recovery task ID.' },
|
|
43
|
+
task_id: { type: 'string', description: 'Manual recovery task ID. Required for read/status.' },
|
|
44
44
|
agent: { type: 'string', description: 'Workflow agent id.' },
|
|
45
45
|
tag: { type: 'string', description: 'Stable scope handle. Reuse the same tag for follow-up on the same scope; use distinct tags only for independent scopes.' },
|
|
46
46
|
sessionId: { type: 'string', description: 'Raw sess_ id.' },
|
|
@@ -50,6 +50,16 @@ export const TRANSCRIPT_WINDOW_OVERSCAN_ROWS = positiveIntEnv('MIXDOG_TUI_TRANSC
|
|
|
50
50
|
// tail of off-screen rows, so lower it to a value that still comfortably covers
|
|
51
51
|
// viewport + overscan on a large terminal. Env-tunable for A/B / revert.
|
|
52
52
|
export const TRANSCRIPT_WINDOW_MAX_ITEMS = positiveIntEnv('MIXDOG_TUI_TRANSCRIPT_WINDOW_ITEMS', 80);
|
|
53
|
+
// When the transcript is anchored to the bottom (live tail / streaming view,
|
|
54
|
+
// effectiveScrollOffset === 0), everything above the viewport is off-screen and
|
|
55
|
+
// cannot be revealed without a scroll action — which sets scrollOffset > 0 and
|
|
56
|
+
// re-renders through the FULL overscan/cap constants above, i.e. scrolled-up
|
|
57
|
+
// history is byte-for-byte unchanged. So in the at-bottom case we mount only
|
|
58
|
+
// viewport + a small overscan, sharply cutting ink's per-frame serialize cost
|
|
59
|
+
// during streaming (bench: 80→20 mounted rows ≈ 4.07ms/render, 109 CPU-ms/s).
|
|
60
|
+
// Env-tunable for A/B / revert.
|
|
61
|
+
export const TRANSCRIPT_WINDOW_TAIL_OVERSCAN_ROWS = positiveIntEnv('MIXDOG_TUI_TRANSCRIPT_TAIL_OVERSCAN_ROWS', 4);
|
|
62
|
+
export const TRANSCRIPT_WINDOW_TAIL_MAX_ITEMS = positiveIntEnv('MIXDOG_TUI_TRANSCRIPT_WINDOW_TAIL_ITEMS', 20);
|
|
53
63
|
export const SELECTION_PAINT_INTERVAL_MS = positiveIntEnv('MIXDOG_TUI_SELECTION_PAINT_MS', 24);
|
|
54
64
|
// Frame-coalesce edge-drag auto-scroll + wheel scroll: both paths accumulate
|
|
55
65
|
// deltas into one pending total and flush via a single scrollTranscriptRows
|
|
@@ -904,12 +914,16 @@ export function transcriptRenderWindow(items, { scrollOffset = 0, viewportHeight
|
|
|
904
914
|
}
|
|
905
915
|
|
|
906
916
|
const minItems = Math.min(TRANSCRIPT_WINDOW_MIN_ITEMS, itemCount);
|
|
907
|
-
|
|
917
|
+
// At-bottom (live tail) mounts only viewport + a small overscan; scrolled-up
|
|
918
|
+
// views keep the full overscan/cap so history renders exactly as before.
|
|
919
|
+
const atTail = effectiveScrollOffset === 0;
|
|
920
|
+
const overscanRows = atTail ? TRANSCRIPT_WINDOW_TAIL_OVERSCAN_ROWS : TRANSCRIPT_WINDOW_OVERSCAN_ROWS;
|
|
921
|
+
const maxItems = Math.max(minItems, atTail ? TRANSCRIPT_WINDOW_TAIL_MAX_ITEMS : TRANSCRIPT_WINDOW_MAX_ITEMS);
|
|
908
922
|
const prefixRows = fallbackIndex.prefixRows;
|
|
909
923
|
const visibleTop = Math.max(0, totalRows - effectiveScrollOffset - viewRows);
|
|
910
924
|
const visibleBottom = Math.min(totalRows, totalRows - effectiveScrollOffset);
|
|
911
|
-
const desiredTop = Math.max(0, visibleTop -
|
|
912
|
-
const desiredBottom = Math.min(totalRows, visibleBottom +
|
|
925
|
+
const desiredTop = Math.max(0, visibleTop - overscanRows);
|
|
926
|
+
const desiredBottom = Math.min(totalRows, visibleBottom + overscanRows);
|
|
913
927
|
|
|
914
928
|
let startIndex = Math.max(0, upperBound(prefixRows, desiredTop) - 1);
|
|
915
929
|
let endIndex = Math.min(itemCount, Math.max(startIndex + 1, lowerBound(prefixRows, Math.max(desiredBottom, desiredTop + 1))));
|
|
@@ -920,9 +934,15 @@ export function transcriptRenderWindow(items, { scrollOffset = 0, viewportHeight
|
|
|
920
934
|
if (endIndex - startIndex > maxItems) {
|
|
921
935
|
const visibleStartIndex = Math.max(0, upperBound(prefixRows, visibleTop) - 1);
|
|
922
936
|
const visibleEndIndex = Math.min(itemCount, Math.max(visibleStartIndex + 1, lowerBound(prefixRows, Math.max(visibleBottom, visibleTop + 1))));
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
937
|
+
// The cap must never cut into rows needed to fill the viewport: floor it at
|
|
938
|
+
// the item span the visible viewport actually covers, so a run of many short
|
|
939
|
+
// (e.g. one-line) items can't leave the top of the viewport unmounted under
|
|
940
|
+
// the small tail cap. Full-view (scrolled-up) behavior is unchanged: there
|
|
941
|
+
// maxItems already exceeds the visible span, so the floor is a no-op.
|
|
942
|
+
const effectiveMaxItems = Math.max(maxItems, visibleEndIndex - visibleStartIndex);
|
|
943
|
+
startIndex = Math.max(0, Math.min(visibleStartIndex, itemCount - effectiveMaxItems));
|
|
944
|
+
endIndex = Math.min(itemCount, Math.max(visibleEndIndex, startIndex + effectiveMaxItems));
|
|
945
|
+
if (endIndex - startIndex > effectiveMaxItems) startIndex = Math.max(0, endIndex - effectiveMaxItems);
|
|
926
946
|
}
|
|
927
947
|
|
|
928
948
|
const bottomSpacerRows = Math.max(0, totalRows - (prefixRows[endIndex] || totalRows));
|
|
@@ -18,7 +18,8 @@
|
|
|
18
18
|
* 'requesting' | 'compacting' | 'resuming' (default 'responding').
|
|
19
19
|
*/
|
|
20
20
|
import React, { useRef } from 'react';
|
|
21
|
-
import { Box, Text
|
|
21
|
+
import { Box, Text } from 'ink';
|
|
22
|
+
import { useSharedTick } from '../hooks/useSharedTick.mjs';
|
|
22
23
|
import { theme } from '../theme.mjs';
|
|
23
24
|
import { SPINNER_FRAMES } from '../spinner-verbs.mjs';
|
|
24
25
|
import { DOWN_ARROW, UP_ARROW } from '../figures.mjs';
|
|
@@ -170,7 +171,9 @@ function tokenModeGlyph(mode) {
|
|
|
170
171
|
}
|
|
171
172
|
|
|
172
173
|
export function Spinner({ verb = 'Working', startedAt, outputTokens = 0, tokens = 0, thinking = false, thinkingActiveSince = 0, mode = 'responding', columns = 80, marginTop = 1 }) {
|
|
173
|
-
|
|
174
|
+
// Re-render at the frame cadence off the shared tick (no dedicated timer).
|
|
175
|
+
// Glyph/shimmer/token/elapsed values are all derived from Date.now() below.
|
|
176
|
+
useSharedTick(FRAME_MS);
|
|
174
177
|
const { TEXT_RGB, SHIMMER_RGB, SPINNER_GLYPH_RGB, THINKING_INACTIVE, THINKING_SHIMMER, STALL_RGB } = spinnerRgb();
|
|
175
178
|
const now = Date.now();
|
|
176
179
|
const elapsedMs = startedAt ? Math.max(0, now - startedAt) : 0;
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
statuslineFooterCacheKey,
|
|
16
16
|
statuslineFooterIdentityChanged,
|
|
17
17
|
} from '../statusline-ansi-bridge.mjs';
|
|
18
|
+
import { useSharedTick } from '../hooks/useSharedTick.mjs';
|
|
18
19
|
|
|
19
20
|
// Loaded at RUNTIME (not bundled) so its vendored statusline-lib relative
|
|
20
21
|
// imports resolve from the real src/ui location, not the dist/ bundle dir.
|
|
@@ -514,13 +515,12 @@ function StatusLineView({ sessionId, clientHostPid, provider, model, effort, fas
|
|
|
514
515
|
scheduleStatuslineModulePrewarm();
|
|
515
516
|
}, []);
|
|
516
517
|
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
}, [refreshMs]);
|
|
518
|
+
// Refresh cadence rides the shared animation tick (no dedicated interval).
|
|
519
|
+
// The callback bumps refreshTick so the async full-render effect below still
|
|
520
|
+
// re-fires on schedule; the tick timer stops when nothing is subscribed.
|
|
521
|
+
useSharedTick(refreshMs, true, () => {
|
|
522
|
+
setRefreshTick((tick) => (tick + 1) % 1_000_000);
|
|
523
|
+
});
|
|
524
524
|
|
|
525
525
|
useEffect(() => {
|
|
526
526
|
let alive = true;
|
|
@@ -8,8 +8,9 @@
|
|
|
8
8
|
* - The result hangs under a single dim ` ⎿ ` gutter — the gutter is placed
|
|
9
9
|
* once, not repeated per wrapped line.
|
|
10
10
|
*/
|
|
11
|
-
import React
|
|
11
|
+
import React from 'react';
|
|
12
12
|
import { Box, Text } from 'ink';
|
|
13
|
+
import { useSharedTick } from '../hooks/useSharedTick.mjs';
|
|
13
14
|
import stringWidth from 'string-width';
|
|
14
15
|
import { theme, TURN_MARKER, AGENT_CALL_MARKER, AGENT_RESPONSE_MARKER } from '../theme.mjs';
|
|
15
16
|
import { formatElapsed } from '../time-format.mjs';
|
|
@@ -74,6 +75,9 @@ export function displayToolName(name, args) {
|
|
|
74
75
|
const TOOL_BLINK_MS = 500;
|
|
75
76
|
const TOOL_BLINK_LIMIT_MS = 3000;
|
|
76
77
|
const TOOL_PENDING_SHOW_DELAY_MS = 1000;
|
|
78
|
+
// One shared-tick cadence covers both the 500ms blink and per-second elapsed;
|
|
79
|
+
// finer than either boundary so both stay crisp off a single timer.
|
|
80
|
+
const TOOL_ANIM_TICK_MS = TOOL_BLINK_MS;
|
|
77
81
|
function statusCopy(name, label, count, doneCount, pending, isError, args = {}) {
|
|
78
82
|
// No stableVerbWidth padding: it padded the done verb to the active ("-ing")
|
|
79
83
|
// width, which Ink trims at the line END (vendor output trimEnd) so it never
|
|
@@ -85,10 +89,6 @@ function statusCopy(name, label, count, doneCount, pending, isError, args = {})
|
|
|
85
89
|
}
|
|
86
90
|
export function ToolExecution({ name, args, result, rawResult, isError, errorCount, expanded, columns = 80, attached = false, count = 1, completedCount = 0, startedAt = 0, completedAt = 0, aggregate = false, categories = {}, doneCategories = null, headerFinalized = true, deferredDisplayReady = false }) {
|
|
87
91
|
const rowWidth = Math.max(1, Number(columns || 80));
|
|
88
|
-
const [blinkOn, setBlinkOn] = useState(true);
|
|
89
|
-
const [blinkExpired, setBlinkExpired] = useState(false);
|
|
90
|
-
const [pendingDelayElapsed, setPendingDelayElapsed] = useState(false);
|
|
91
|
-
const [, setElapsedTick] = useState(0);
|
|
92
92
|
const groupCount = Math.max(1, Number(count || 1));
|
|
93
93
|
const doneCount = Math.max(0, Math.min(groupCount, Number(completedCount || (result == null ? 0 : groupCount))));
|
|
94
94
|
const rt = result == null ? null : String(result).replace(/\s+$/, '');
|
|
@@ -96,7 +96,15 @@ export function ToolExecution({ name, args, result, rawResult, isError, errorCou
|
|
|
96
96
|
const pending = doneCount < groupCount;
|
|
97
97
|
const startedAtMs = Number(startedAt || 0);
|
|
98
98
|
const completedAtMs = Number(completedAt || 0);
|
|
99
|
-
const
|
|
99
|
+
const nowMs = Date.now();
|
|
100
|
+
// Single shared tick drives the blink + elapsed re-renders while pending; all
|
|
101
|
+
// phase/elapsed values below are derived from nowMs, so no per-card timers.
|
|
102
|
+
useSharedTick(TOOL_ANIM_TICK_MS, pending);
|
|
103
|
+
const pendingAgeMs = pending && startedAtMs ? Math.max(0, nowMs - startedAtMs) : 0;
|
|
104
|
+
// Derived (was a per-card setTimeout): the pending-show delay has elapsed.
|
|
105
|
+
const pendingDelayElapsed = pending
|
|
106
|
+
? (!startedAtMs || pendingAgeMs >= TOOL_PENDING_SHOW_DELAY_MS)
|
|
107
|
+
: false;
|
|
100
108
|
// A card that is still pending but already has something to paint (a result
|
|
101
109
|
// landed, or at least one of an aggregate's parallel calls completed) must
|
|
102
110
|
// SKIP the blank placeholder: it was pushed early (engine ensureVisible on a
|
|
@@ -108,6 +116,15 @@ export function ToolExecution({ name, args, result, rawResult, isError, errorCou
|
|
|
108
116
|
// land — no empty band.
|
|
109
117
|
const hasVisibleProgress = doneCount > 0 || Boolean(String(rt || '').trim());
|
|
110
118
|
const pendingDisplayReady = !pending || !startedAtMs || pendingDelayElapsed || pendingAgeMs >= TOOL_PENDING_SHOW_DELAY_MS || hasVisibleProgress || deferredDisplayReady;
|
|
119
|
+
// Derived blink (was two per-card setIntervals + a setTimeout): the dot blinks
|
|
120
|
+
// at TOOL_BLINK_MS while a display-ready pending card is fresh, then goes solid
|
|
121
|
+
// once TOOL_BLINK_LIMIT_MS elapses. Phase comes from Date.now() so the cadence
|
|
122
|
+
// is identical to the old interval without owning a timer.
|
|
123
|
+
const blinkActive = pending && pendingDisplayReady;
|
|
124
|
+
const blinkExpired = blinkActive && startedAtMs > 0 && (nowMs - startedAtMs) >= TOOL_BLINK_LIMIT_MS;
|
|
125
|
+
const blinkOn = !blinkActive || blinkExpired
|
|
126
|
+
? true
|
|
127
|
+
: Math.floor(nowMs / TOOL_BLINK_MS) % 2 === 0;
|
|
111
128
|
// Keep the action verb in its active form until the engine explicitly seals
|
|
112
129
|
// the tool block. Fast tool batches often complete before the next provider
|
|
113
130
|
// iteration decides whether to call more tools or emit assistant text; flipping
|
|
@@ -115,7 +132,7 @@ export function ToolExecution({ name, args, result, rawResult, isError, errorCou
|
|
|
115
132
|
const headerPending = pending || headerFinalized === false;
|
|
116
133
|
const hasResult = result != null && Boolean(String(rt || '').trim());
|
|
117
134
|
const hasRawResult = rawResult != null && Boolean(String(rawRt || '').trim());
|
|
118
|
-
const elapsedMs = startedAtMs ? Math.max(0, (pending ?
|
|
135
|
+
const elapsedMs = startedAtMs ? Math.max(0, (pending ? nowMs : (completedAtMs || nowMs)) - startedAtMs) : 0;
|
|
119
136
|
const elapsed = elapsedMs >= 1000 ? formatElapsed(elapsedMs) : '';
|
|
120
137
|
const failedCount = clampFailureCount(errorCount, groupCount, isError);
|
|
121
138
|
const displayGroupCount = groupCount;
|
|
@@ -133,57 +150,6 @@ export function ToolExecution({ name, args, result, rawResult, isError, errorCou
|
|
|
133
150
|
);
|
|
134
151
|
const displayDoneCategories = hasDoneCounts ? normalizedDoneCategories : displayCategories;
|
|
135
152
|
|
|
136
|
-
useEffect(() => {
|
|
137
|
-
if (!pending) {
|
|
138
|
-
setPendingDelayElapsed(false);
|
|
139
|
-
return undefined;
|
|
140
|
-
}
|
|
141
|
-
const started = Number(startedAt || 0);
|
|
142
|
-
if (!started) {
|
|
143
|
-
setPendingDelayElapsed(true);
|
|
144
|
-
return undefined;
|
|
145
|
-
}
|
|
146
|
-
const remaining = TOOL_PENDING_SHOW_DELAY_MS - Math.max(0, Date.now() - started);
|
|
147
|
-
if (remaining <= 0) {
|
|
148
|
-
setPendingDelayElapsed(true);
|
|
149
|
-
return undefined;
|
|
150
|
-
}
|
|
151
|
-
setPendingDelayElapsed(false);
|
|
152
|
-
const timer = setTimeout(() => setPendingDelayElapsed(true), remaining);
|
|
153
|
-
return () => clearTimeout(timer);
|
|
154
|
-
}, [pending, startedAt]);
|
|
155
|
-
|
|
156
|
-
useEffect(() => {
|
|
157
|
-
if (!pending || !pendingDisplayReady || blinkExpired) {
|
|
158
|
-
setBlinkOn(true);
|
|
159
|
-
return undefined;
|
|
160
|
-
}
|
|
161
|
-
const timer = setInterval(() => setBlinkOn((on) => !on), TOOL_BLINK_MS);
|
|
162
|
-
return () => clearInterval(timer);
|
|
163
|
-
}, [pending, pendingDisplayReady, blinkExpired]);
|
|
164
|
-
|
|
165
|
-
useEffect(() => {
|
|
166
|
-
if (!pending || !pendingDisplayReady) {
|
|
167
|
-
setBlinkExpired(false);
|
|
168
|
-
return undefined;
|
|
169
|
-
}
|
|
170
|
-
const started = Number(startedAt || 0);
|
|
171
|
-
const remaining = TOOL_BLINK_LIMIT_MS - (started ? Math.max(0, Date.now() - started) : 0);
|
|
172
|
-
if (remaining <= 0) {
|
|
173
|
-
setBlinkExpired(true);
|
|
174
|
-
return undefined;
|
|
175
|
-
}
|
|
176
|
-
setBlinkExpired(false);
|
|
177
|
-
const timer = setTimeout(() => setBlinkExpired(true), remaining);
|
|
178
|
-
return () => clearTimeout(timer);
|
|
179
|
-
}, [pending, pendingDisplayReady, startedAt]);
|
|
180
|
-
|
|
181
|
-
useEffect(() => {
|
|
182
|
-
if (!pending || !pendingDisplayReady || !startedAtMs) return undefined;
|
|
183
|
-
const timer = setInterval(() => setElapsedTick((tick) => (tick + 1) % 1000000), 1000);
|
|
184
|
-
return () => clearInterval(timer);
|
|
185
|
-
}, [pending, pendingDisplayReady, startedAtMs]);
|
|
186
|
-
|
|
187
153
|
// While a freshly-started tool is still inside its pending-show delay we used
|
|
188
154
|
// to `return null` (0 rendered rows). But estimateTranscriptItemRows() in
|
|
189
155
|
// App.jsx counts a collapsed tool item from the moment it is pushed (1 row for
|
|
@@ -32,18 +32,22 @@ const graphemeSegmenter = new Intl.Segmenter(undefined, { granularity: 'grapheme
|
|
|
32
32
|
/**
|
|
33
33
|
* True for code points we treat as wide (2 cells) when the policy is ON:
|
|
34
34
|
* - U+2460–U+24FF Enclosed Alphanumerics (① ② ③ …)
|
|
35
|
-
* - U+
|
|
36
|
-
* Deliberately excludes box-drawing / blocks / figures
|
|
35
|
+
* - U+2194–U+21FF Arrows (↔ ⇒ ⇧ …)
|
|
36
|
+
* Deliberately excludes box-drawing / blocks / figures — including the four
|
|
37
|
+
* Basic Arrows U+2190–U+2193 (← ↑ → ↓) that figures.mjs uses as 1-cell
|
|
38
|
+
* markers (agent card ←/→, history ↑/↓): WT draws them 1 cell in
|
|
39
|
+
* Cascadia, and widening them ate the marker's gutter padding space
|
|
40
|
+
* ("←Spawn" rendered glued / shifted vs the ● rows).
|
|
37
41
|
*/
|
|
38
42
|
export function isProblemCodePoint(cp) {
|
|
39
|
-
return (cp >= 0x2460 && cp <= 0x24ff) || (cp >=
|
|
43
|
+
return (cp >= 0x2460 && cp <= 0x24ff) || (cp >= 0x2194 && cp <= 0x21ff);
|
|
40
44
|
}
|
|
41
45
|
|
|
42
46
|
// Fast precheck for the problem ranges above. Lets the hot path bail before
|
|
43
47
|
// the per-grapheme segmenter loop when a string (the overwhelmingly common
|
|
44
48
|
// ASCII/status-text case) contains no widenable glyph. Kept identical to
|
|
45
49
|
// vendor/ink/build/display-width.js.
|
|
46
|
-
const PROBLEM_RE = /[\
|
|
50
|
+
const PROBLEM_RE = /[\u2194-\u21ff\u2460-\u24ff]/;
|
|
47
51
|
|
|
48
52
|
/**
|
|
49
53
|
* Resolve the wide policy from env. Override wins over the default; the
|