mixdog 0.9.36 → 0.9.38
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 +3 -2
- package/scripts/abort-recovery-test.mjs +118 -0
- package/scripts/compact-smoke.mjs +7 -7
- package/scripts/dispatch-persist-recovery-test.mjs +141 -0
- package/scripts/explore-bench-tmp.mjs +19 -0
- package/scripts/explore-bench.mjs +101 -14
- package/scripts/explore-prompt-policy-test.mjs +31 -0
- package/scripts/ingest-pure-conversation-smoke.mjs +11 -11
- package/scripts/notify-completion-mirror-test.mjs +73 -0
- package/scripts/openai-ws-early-settle-test.mjs +176 -0
- package/scripts/output-style-smoke.mjs +17 -17
- package/scripts/provider-toolcall-test.mjs +333 -14
- package/scripts/recall-bench-cases.json +3 -3
- package/scripts/recall-bench.mjs +1 -1
- package/scripts/recall-quality-cases.json +4 -4
- package/scripts/recall-usecase-cases.json +2 -2
- package/scripts/session-bench.mjs +13 -13
- package/scripts/session-sweep.mjs +266 -0
- package/scripts/steering-drain-buckets-test.mjs +72 -11
- package/scripts/submit-commandbusy-race-test.mjs +114 -0
- package/scripts/tool-smoke.mjs +83 -10
- package/src/app.mjs +2 -1
- package/src/defaults/cycle3-review-prompt.md +9 -0
- package/src/defaults/skills/setup/SKILL.md +93 -293
- package/src/lib/rules-builder.cjs +3 -2
- package/src/output-styles/default.md +2 -2
- package/src/output-styles/extreme-minimal.md +1 -1
- package/src/output-styles/minimal.md +1 -1
- package/src/output-styles/simple.md +2 -3
- package/src/rules/agent/30-explorer.md +40 -14
- package/src/rules/lead/01-general.md +4 -0
- package/src/rules/shared/01-tool.md +7 -3
- package/src/runtime/agent/orchestrator/agent-trace-format.mjs +15 -3
- package/src/runtime/agent/orchestrator/config.mjs +1 -1
- package/src/runtime/agent/orchestrator/dispatch-persist.mjs +31 -8
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth-credentials.mjs +13 -7
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +6 -6
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +6 -6
- package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +35 -5
- package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +27 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +36 -37
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +2 -2
- package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +72 -1
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +26 -3
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +12 -28
- package/src/runtime/agent/orchestrator/providers/openai-transport-policy.mjs +18 -15
- package/src/runtime/agent/orchestrator/providers/openai-ws-delta.mjs +5 -2
- package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +119 -3
- package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +113 -2
- package/src/runtime/agent/orchestrator/providers/registry.mjs +44 -5
- package/src/runtime/agent/orchestrator/providers/tool-stream-state.mjs +78 -0
- package/src/runtime/agent/orchestrator/session/agent-loop.mjs +57 -31
- package/src/runtime/agent/orchestrator/session/cache/scoped-cache.mjs +8 -6
- package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +28 -2
- package/src/runtime/agent/orchestrator/session/loop/tool-classify.mjs +1 -3
- package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +15 -2
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +30 -55
- package/src/runtime/agent/orchestrator/session/manager/prompt-utils.mjs +2 -2
- package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +24 -16
- package/src/runtime/agent/orchestrator/session/result-classification.mjs +2 -0
- package/src/runtime/agent/orchestrator/session/store.mjs +116 -21
- package/src/runtime/agent/orchestrator/session/tool-batch.mjs +5 -2
- package/src/runtime/agent/orchestrator/stall-policy.mjs +55 -0
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +35 -20
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +9 -9
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +77 -31
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +25 -3
- package/src/runtime/agent/orchestrator/tools/builtin/search-builders.mjs +9 -2
- package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +73 -25
- package/src/runtime/agent/orchestrator/tools/builtin.mjs +2 -1
- package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +17 -7
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +14 -12
- package/src/runtime/agent/orchestrator/tools/code-graph/graph-model.mjs +5 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/search.mjs +56 -6
- package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +9 -0
- package/src/runtime/channels/lib/worker-main.mjs +5 -0
- package/src/runtime/memory/lib/core-memory-store.mjs +104 -0
- package/src/runtime/memory/lib/ko-morph.mjs +1 -1
- package/src/runtime/memory/lib/memory-cycle3.mjs +64 -4
- package/src/runtime/memory/lib/memory-text-utils.mjs +4 -4
- package/src/runtime/memory/lib/recall-format.mjs +3 -3
- package/src/runtime/shared/child-spawn-gate.mjs +15 -0
- package/src/runtime/shared/config.mjs +98 -12
- package/src/runtime/shared/process-listener-headroom.mjs +12 -0
- package/src/session-runtime/cwd-plugins.mjs +6 -3
- package/src/session-runtime/model-route-api.mjs +50 -2
- package/src/session-runtime/provider-auth-api.mjs +8 -1
- package/src/session-runtime/provider-models.mjs +3 -3
- package/src/session-runtime/resource-api.mjs +22 -0
- package/src/session-runtime/runtime-core.mjs +56 -10
- package/src/session-runtime/settings-api.mjs +11 -2
- package/src/session-runtime/warmup-schedulers.mjs +7 -1
- package/src/standalone/agent-tool.mjs +15 -9
- package/src/standalone/explore-tool.mjs +4 -1
- package/src/tui/App.jsx +6 -5
- package/src/tui/app/transcript-window.mjs +60 -10
- package/src/tui/app/use-transcript-window.mjs +2 -1
- package/src/tui/components/ContextPanel.jsx +4 -1
- package/src/tui/components/ToolExecution.jsx +15 -12
- package/src/tui/components/TranscriptItem.jsx +1 -1
- package/src/tui/components/tool-execution/surface-detail.mjs +8 -8
- package/src/tui/dist/index.mjs +482 -151
- package/src/tui/engine/agent-job-feed.mjs +36 -6
- package/src/tui/engine/notification-plan.mjs +3 -3
- package/src/tui/engine/queue-helpers.mjs +8 -0
- package/src/tui/engine/render-timing.mjs +104 -8
- package/src/tui/engine/session-api.mjs +58 -3
- package/src/tui/engine/session-flow.mjs +93 -29
- package/src/tui/engine/tool-card-results.mjs +28 -13
- package/src/tui/engine/turn.mjs +238 -157
- package/src/tui/engine.mjs +17 -8
- package/src/tui/index.jsx +10 -1
- package/src/workflows/default/WORKFLOW.md +8 -4
- package/src/workflows/solo/WORKFLOW.md +8 -4
|
@@ -306,6 +306,26 @@ export function __saveModelSettingsForTest(cfgMod, route, options = {}) {
|
|
|
306
306
|
return saveModelSettings(cfgMod, route, options);
|
|
307
307
|
}
|
|
308
308
|
|
|
309
|
+
// Decide whether notifyFnForSession should mirror a terminal tool completion
|
|
310
|
+
// into the pending-message queue. Only the TUI execution-ui path actually
|
|
311
|
+
// injects the model-visible twin of the completion into the active loop, and it
|
|
312
|
+
// signals that with an EXPLICIT ack (modelVisibleDelivered) — never a generic
|
|
313
|
+
// truthy onNotification return. Skipping the mirror on a bare `handled===true`
|
|
314
|
+
// would let a display-only / API listener suppress the sole model-visible copy,
|
|
315
|
+
// so the model would never see the completion body. Mirror UNLESS the body was
|
|
316
|
+
// explicitly delivered; unknown/generic-handled → keep the mirror.
|
|
317
|
+
export function shouldMirrorCompletionToPendingQueue({
|
|
318
|
+
callerSessionId,
|
|
319
|
+
modelVisibleDelivered,
|
|
320
|
+
hasEnqueue,
|
|
321
|
+
text,
|
|
322
|
+
meta = {},
|
|
323
|
+
} = {}) {
|
|
324
|
+
if (!callerSessionId || !hasEnqueue) return false;
|
|
325
|
+
if (modelVisibleDelivered) return false;
|
|
326
|
+
return shouldPersistModelVisibleToolCompletion(text, meta);
|
|
327
|
+
}
|
|
328
|
+
|
|
309
329
|
export async function createMixdogSessionRuntime({
|
|
310
330
|
provider,
|
|
311
331
|
model,
|
|
@@ -713,7 +733,7 @@ export async function createMixdogSessionRuntime({
|
|
|
713
733
|
// available / installs on quit" nag while the background stage runs
|
|
714
734
|
// silently. The wording lives in the notice surface (notification-plan):
|
|
715
735
|
// this emit only carries meta.version. TUI maps meta.kind 'update-notice'
|
|
716
|
-
// to a
|
|
736
|
+
// to a transient notice, never a model-visible message; tone 'info' =
|
|
717
737
|
// non-urgent, applies on the next launch.
|
|
718
738
|
const announceReady = () => {
|
|
719
739
|
emitRuntimeNotification('update ready', { kind: 'update-notice', version: ver, tone: 'info' });
|
|
@@ -742,7 +762,7 @@ export async function createMixdogSessionRuntime({
|
|
|
742
762
|
|
|
743
763
|
function emitRuntimeNotification(content, meta = {}) {
|
|
744
764
|
const text = String(content || '').trim();
|
|
745
|
-
if (!text) return false;
|
|
765
|
+
if (!text) return { handled: false, modelVisibleDelivered: false };
|
|
746
766
|
const event = { content: text, meta: meta && typeof meta === 'object' ? meta : {} };
|
|
747
767
|
let handled = false;
|
|
748
768
|
for (const listener of [...notificationListeners]) {
|
|
@@ -750,19 +770,34 @@ export async function createMixdogSessionRuntime({
|
|
|
750
770
|
if (listener(event) === true) handled = true;
|
|
751
771
|
} catch {}
|
|
752
772
|
}
|
|
753
|
-
|
|
773
|
+
// EXPLICIT model-visible-delivery ack: only the TUI execution-ui path sets
|
|
774
|
+
// event.modelVisibleDelivered when it enqueues the model-visible completion
|
|
775
|
+
// body into the active loop. A generic display-only / API listener that
|
|
776
|
+
// returns true does NOT set it, so `handled` alone must never be read as
|
|
777
|
+
// "the model saw the body".
|
|
778
|
+
const modelVisibleDelivered = event.modelVisibleDelivered === true;
|
|
779
|
+
return { handled, modelVisibleDelivered };
|
|
754
780
|
}
|
|
755
781
|
|
|
756
782
|
function notifyFnForSession(callerSessionId) {
|
|
757
783
|
return (text, meta = {}) => {
|
|
758
|
-
const handledByRuntimeListener = emitRuntimeNotification(text, meta);
|
|
784
|
+
const { handled: handledByRuntimeListener, modelVisibleDelivered } = emitRuntimeNotification(text, meta);
|
|
759
785
|
let enqueued = false;
|
|
760
786
|
// TUI sessions consume raw execution notifications for UI/task cards via
|
|
761
787
|
// onNotification, but those raw envelopes are internal-only in pending
|
|
762
|
-
// drain.
|
|
763
|
-
//
|
|
764
|
-
|
|
765
|
-
|
|
788
|
+
// drain. The TUI execution-ui path injects the model-visible twin of a
|
|
789
|
+
// terminal completion into the active loop and acks with
|
|
790
|
+
// modelVisibleDelivered, so mirroring it into the pending queue would
|
|
791
|
+
// double-inject the same completion — skip only on that EXPLICIT ack. A
|
|
792
|
+
// generic display-only / API listener returning true is NOT an ack: the
|
|
793
|
+
// mirror stays as the sole model-visible delivery.
|
|
794
|
+
if (shouldMirrorCompletionToPendingQueue({
|
|
795
|
+
callerSessionId,
|
|
796
|
+
modelVisibleDelivered,
|
|
797
|
+
hasEnqueue: typeof mgr.enqueuePendingMessage === 'function',
|
|
798
|
+
text,
|
|
799
|
+
meta,
|
|
800
|
+
})) {
|
|
766
801
|
try {
|
|
767
802
|
const visible = modelVisibleToolCompletionMessage(text, meta);
|
|
768
803
|
// Terminal completion (gated by shouldPersistModelVisibleToolCompletion)
|
|
@@ -973,7 +1008,7 @@ export async function createMixdogSessionRuntime({
|
|
|
973
1008
|
const meta = params.meta && typeof params.meta === 'object' ? params.meta : {};
|
|
974
1009
|
const content = channelNotificationModelContent(params);
|
|
975
1010
|
if (!content) return;
|
|
976
|
-
const handled = emitRuntimeNotification(content, meta);
|
|
1011
|
+
const { handled } = emitRuntimeNotification(content, meta);
|
|
977
1012
|
if (!handled && session?.id && shouldMirrorChannelNotificationToPending(meta)) {
|
|
978
1013
|
try { mgr.enqueuePendingMessage(session.id, content); } catch {}
|
|
979
1014
|
}
|
|
@@ -1228,7 +1263,15 @@ export async function createMixdogSessionRuntime({
|
|
|
1228
1263
|
if (!startupProviderCatalogRefreshStarted && !closeRequested) {
|
|
1229
1264
|
startupProviderCatalogRefreshStarted = true;
|
|
1230
1265
|
try {
|
|
1231
|
-
reg.refreshProviderCatalogsOnStartup()
|
|
1266
|
+
void Promise.resolve(reg.refreshProviderCatalogsOnStartup())
|
|
1267
|
+
.then(() => {
|
|
1268
|
+
invalidateProviderCaches();
|
|
1269
|
+
warmProviderModelCache();
|
|
1270
|
+
bootProfile('provider-catalogs:refresh-ready');
|
|
1271
|
+
})
|
|
1272
|
+
.catch((error) => {
|
|
1273
|
+
bootProfile('provider-catalogs:refresh-failed', { error: error?.message || String(error) });
|
|
1274
|
+
});
|
|
1232
1275
|
bootProfile('provider-catalogs:refresh-started');
|
|
1233
1276
|
} catch (error) {
|
|
1234
1277
|
bootProfile('provider-catalogs:refresh-failed', { error: error?.message || String(error) });
|
|
@@ -2102,6 +2145,9 @@ export async function createMixdogSessionRuntime({
|
|
|
2102
2145
|
scheduleStatuslineUsageRefresh,
|
|
2103
2146
|
invalidateContextStatusCache,
|
|
2104
2147
|
invalidateProviderCaches,
|
|
2148
|
+
createCurrentSession,
|
|
2149
|
+
invalidatePreSessionToolSurface,
|
|
2150
|
+
pushTranscriptRebind,
|
|
2105
2151
|
collectSearchProviderModels,
|
|
2106
2152
|
});
|
|
2107
2153
|
const workflowAgentsApi = createWorkflowAgentsApi({
|
|
@@ -122,7 +122,12 @@ export function createSettingsApi({
|
|
|
122
122
|
// language catalog entry and the full language list for the picker UI.
|
|
123
123
|
getProfile() {
|
|
124
124
|
const config = getConfig();
|
|
125
|
-
|
|
125
|
+
// In-memory config is flat: `config.profile` is what the save path
|
|
126
|
+
// (buildAgentSaveBuilder) persists into the on-disk `agent.profile`
|
|
127
|
+
// slot. Fall back to a nested `agent.profile` only for any stray
|
|
128
|
+
// nested snapshot.
|
|
129
|
+
const stored = config?.profile ?? config?.agent?.profile;
|
|
130
|
+
const profile = cfgMod.normalizeProfileConfig(stored);
|
|
126
131
|
return {
|
|
127
132
|
...profile,
|
|
128
133
|
languageEntry: cfgMod.profileLanguageEntry(profile.language),
|
|
@@ -152,7 +157,7 @@ export function createSettingsApi({
|
|
|
152
157
|
},
|
|
153
158
|
setProfile(input = {}) {
|
|
154
159
|
const config = getConfig();
|
|
155
|
-
const current = cfgMod.normalizeProfileConfig(config
|
|
160
|
+
const current = cfgMod.normalizeProfileConfig(config?.profile ?? config?.agent?.profile);
|
|
156
161
|
const next = { ...current };
|
|
157
162
|
if (hasOwn(input, 'title') || hasOwn(input, 'name')) {
|
|
158
163
|
next.title = input.title ?? input.name ?? '';
|
|
@@ -161,6 +166,10 @@ export function createSettingsApi({
|
|
|
161
166
|
next.language = input.language ?? input.lang ?? 'system';
|
|
162
167
|
}
|
|
163
168
|
const normalized = cfgMod.normalizeProfileConfig(next);
|
|
169
|
+
// Persist flat: buildAgentSaveBuilder (config.mjs saveConfig) reads
|
|
170
|
+
// `config.profile` and writes it into the on-disk `agent.profile`
|
|
171
|
+
// section, which the prompt builder (readAgentConfig) reads. Writing a
|
|
172
|
+
// nested `agent.profile` here would be dropped by the save path.
|
|
164
173
|
saveConfigAndAdopt({ ...config, profile: normalized });
|
|
165
174
|
return this.getProfile();
|
|
166
175
|
},
|
|
@@ -102,7 +102,13 @@ export function createWarmupSchedulers({
|
|
|
102
102
|
scheduleProviderModelWarmup(backgroundBusyRetryMs);
|
|
103
103
|
return;
|
|
104
104
|
}
|
|
105
|
-
|
|
105
|
+
if (!isFirstTurnCompleted() && !envFlag('MIXDOG_PROVIDER_WARMUP_BEFORE_FIRST_TURN')) {
|
|
106
|
+
bootProfile('provider-models:warm-deferred', { reason: 'first-turn-pending' });
|
|
107
|
+
warmProviderModelCache();
|
|
108
|
+
scheduleProviderModelWarmup(backgroundBusyRetryMs);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
warmProviderModelCache({ loadSecrets: true });
|
|
106
112
|
}, delayMs);
|
|
107
113
|
timers.providerModelWarmupTimer.unref?.();
|
|
108
114
|
}
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
import { presentErrorText, errorLine } from '../runtime/shared/err-text.mjs';
|
|
15
15
|
import { updateJsonAtomicSync } from '../runtime/shared/atomic-file.mjs';
|
|
16
16
|
import { normalizeAgentPermission } from '../runtime/shared/markdown-frontmatter.mjs';
|
|
17
|
+
import { ensureProcessListenerHeadroom } from '../runtime/shared/process-listener-headroom.mjs';
|
|
17
18
|
import { prepareAgentSession } from '../runtime/agent/orchestrator/agent-runtime/session-builder.mjs';
|
|
18
19
|
import {
|
|
19
20
|
abortAgentProgressWatchdog,
|
|
@@ -63,6 +64,8 @@ import { createNotify } from './agent-tool/notify.mjs';
|
|
|
63
64
|
// identical public surface (`import { AGENT_TOOL } from './agent-tool.mjs'`).
|
|
64
65
|
export { AGENT_TOOL };
|
|
65
66
|
|
|
67
|
+
ensureProcessListenerHeadroom(64);
|
|
68
|
+
|
|
66
69
|
const STANDALONE_SOURCE_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
|
67
70
|
|
|
68
71
|
// Grace window during which a terminated/idle worker row is kept around so the
|
|
@@ -98,15 +101,18 @@ const DEFAULT_SPAWN_PREP_TIMEOUT_MS = envTimeoutMs('MIXDOG_AGENT_SPAWN_PREP_TIME
|
|
|
98
101
|
|
|
99
102
|
// Global spawn-start stagger: unlimited-N parallel fan-out otherwise fires all
|
|
100
103
|
// first provider calls in the same instant, racing the server-side prompt-
|
|
101
|
-
// cache write/propagation window.
|
|
102
|
-
//
|
|
103
|
-
//
|
|
104
|
-
//
|
|
105
|
-
//
|
|
106
|
-
//
|
|
107
|
-
//
|
|
108
|
-
//
|
|
109
|
-
|
|
104
|
+
// cache write/propagation window. Default 0 (off): mirrors the explore fan-out
|
|
105
|
+
// finding — the first spawn's prompt-cache write only lands after its iter1
|
|
106
|
+
// completes (~seconds), so a sub-second stagger yields ~no cross-spawn cache
|
|
107
|
+
// reads while charging every later spawn the full delay, i.e. pure fan-out
|
|
108
|
+
// latency for negligible hit-rate gain. Kept as a knob for tuning: set
|
|
109
|
+
// MIXDOG_SPAWN_STAGGER_MS>0 to re-enable. When >0 it chains (not a fixed lane
|
|
110
|
+
// count) so it scales to any N: each new spawn's start is pushed to at least
|
|
111
|
+
// STAGGER_MS after the previous spawn's start; sequential/non-overlapping
|
|
112
|
+
// spawns pay zero added latency. Applied inside the deferred job body (see
|
|
113
|
+
// startDeferredSpawnJob) so the agent tool call itself still returns task_id
|
|
114
|
+
// immediately.
|
|
115
|
+
const SPAWN_STAGGER_MS = envTimeoutMs('MIXDOG_SPAWN_STAGGER_MS', 0);
|
|
110
116
|
let lastSpawnStartAt = 0;
|
|
111
117
|
async function waitForSpawnStagger() {
|
|
112
118
|
if (SPAWN_STAGGER_MS <= 0) return;
|
|
@@ -5,6 +5,9 @@ import { makeAgentDispatch } from '../runtime/agent/orchestrator/agent-runtime/a
|
|
|
5
5
|
import { loadConfig } from '../runtime/agent/orchestrator/config.mjs';
|
|
6
6
|
import { initProviders } from '../runtime/agent/orchestrator/providers/registry.mjs';
|
|
7
7
|
import { presentErrorText } from '../runtime/shared/err-text.mjs';
|
|
8
|
+
import { ensureProcessListenerHeadroom } from '../runtime/shared/process-listener-headroom.mjs';
|
|
9
|
+
|
|
10
|
+
ensureProcessListenerHeadroom(64);
|
|
8
11
|
|
|
9
12
|
// Ported from the original mixdog tool-defs.mjs `explore` entry.
|
|
10
13
|
// `aiWrapped` is dropped: in the standalone build there is no aiWrapped
|
|
@@ -89,7 +92,7 @@ function escapeXml(str) {
|
|
|
89
92
|
// reminder. The full no-verdict contract lives at system level
|
|
90
93
|
// (rules/agent/30-explorer.md).
|
|
91
94
|
export function buildExplorerPrompt(query) {
|
|
92
|
-
return `<query>${escapeXml(query)}</query>\nReminder: RULE ZERO is a binary gate after every tool result: >=1 path:line matching a SPECIFIC query token (product/library/domain name) -> answer NOW with those exact coordinates; zero -> one more batch if budget remains. A generic-word-only match (schema, handler, config, resolver, index, error...) while the query's specific tokens match nowhere counts as ZERO, not a hit. There is no third branch: "hits exist but I want better ones" IS branch one — answer now. A code_graph symbol hit (find_symbol/symbol_search returning path:line) IS an anchor — emit it directly, never re-locate it with grep. Credibility is mechanical (specific-token match), never judged: "is this the real implementation / final handler / just a wrapper" are caller questions, FORBIDDEN here; mark weak anchors ? and answer anyway. Flow/how/trigger queries: first matching definition/entry anchors ARE the complete answer —
|
|
95
|
+
return `<query>${escapeXml(query)}</query>\nReminder: BUDGET = TWO MESSAGES NORMALLY: message 1 = your multi-tool batch, message 2 = your answer text. A 3rd message (ANY extra tool call) is a DEFECT unless message 1 returned ZERO specific-token lines — extra code_graph/grep calls to confirm or upgrade an anchor you already hold are the single biggest source of overspend. TURN 1 IS ONE BATCH, NON-NEGOTIABLE: your FIRST tool message fires, in that single message together, grep (pattern[] packing 3-6 token variants) AND code_graph symbol_search (identifiers) AND find/glob (path/name fragments) — never emit grep alone and wait for its result before adding code_graph/find. Serial one-tool-per-turn is the #1 budget defect and forfeits the turn-1 -> answer path; a first turn that is not this multi-tool batch is malformed. After that batch returns, if ANY line carries a specific query token you MUST emit the ANSWER as your very next message: a further tool call to refine, confirm, or upgrade is the primary overspend defect — a second tool turn is legal ONLY when the batch returned ZERO specific-token lines. Then RULE ZERO is a binary gate after every tool result: >=1 path:line matching a SPECIFIC query token (product/library/domain name) -> STOP and answer NOW with those exact coordinates, this IS your final turn; zero -> one more batch if budget remains. Turns 2-3 exist SOLELY as zero-hit recovery (the previous turn matched ZERO specific tokens); spending a turn to confirm, refine, or upgrade an anchor you already hold is a defect. A generic-word-only match (schema, handler, config, resolver, index, error...) while the query's specific tokens match nowhere counts as ZERO, not a hit. There is no third branch: "hits exist but I want better ones" IS branch one — answer now. A code_graph symbol hit (find_symbol/symbol_search returning path:line) IS an anchor — emit it directly, never re-locate it with grep. Credibility is mechanical (specific-token match), never judged: "is this the real implementation / final handler / just a wrapper" are caller questions, FORBIDDEN here; mark weak anchors ? and answer anyway. Flow/how/trigger queries: first matching definition/entry anchors ARE the complete answer — trace no chains, one anchor per concept; the SOLE exception: a query that EXPLICITLY asks flow/default-resolution whose turn 1 yielded only an entry anchor (not the resolved value) may take ONE turn-2 hop to the resolving site, then stop. You locate WHERE, never WHY. Scope is ALWAYS the session working directory: omit path (project cwd default) or pass only a path seen in an earlier result; an unverified path/name fragment rides the SAME turn-1 find query[] batch (a find-only turn is a defect) and scopes grep/glob only through the exact path find returns; inventing a directory (/workspace/..., another repo's layout) is a defect — on zero hits change TOKENS, never guess paths. HARD max 3 tool turns; counter line (turn 1/3, turn 2/3, turn 3/3) on every tool message; expected shape is turn 1 -> answer, turns 2-3 are miss-recovery only (previous turn had ZERO matching lines), with changed tokens; after turn 3/3 you MUST answer best-so-far. Each turn = ONE maximal batch: a single grep whose pattern[] packs ALL token variants PLUS code_graph/find/glob in the SAME message; a single-tool turn is a wasted turn. Before emitting EXPLORATION_FAILED re-scan ALL earlier results: any line matching a specific query token -> answer with that anchor (? if weak) — a weak anchor beats a false miss; EXPLORATION_FAILED only when all 3 turns found zero token-matching lines. Output only anchor lines formatted as path:line — symbol/name — short reason, max 3 lines, or EXPLORATION_FAILED. For a CODE-location answer every line MUST carry :line (an explicit line number) — a bare filename with no :line is a DEFECT; with no line-anchored code evidence, return EXPLORATION_FAILED rather than a vague file-only or prose answer. EXCEPTION — file/dir-LOCATION queries (where does X store its config/logs/data on disk, which directory or file holds Y): an exact VERIFIED path (the file or directory itself, no :line) IS the valid answer — do not force a line number and do not FAIL. No preamble, bullets, numbering, headings, summary, code quotes, analysis, verdicts, ratings, or recommendations.`;
|
|
93
96
|
}
|
|
94
97
|
|
|
95
98
|
export function normalizeExploreQueries(rawQuery) {
|
package/src/tui/App.jsx
CHANGED
|
@@ -1494,7 +1494,8 @@ export function App({ store, initialStatusLine = '', forceOnboarding = false })
|
|
|
1494
1494
|
const n = Number(value || 0);
|
|
1495
1495
|
const d = Number(total || 0);
|
|
1496
1496
|
if (!d) return 'N/A';
|
|
1497
|
-
|
|
1497
|
+
const p = Math.max(0, Math.min(100, (n / d) * 100));
|
|
1498
|
+
return `${p > 0 && p < 1 ? p.toFixed(1) : Math.floor(p)}%`;
|
|
1498
1499
|
};
|
|
1499
1500
|
const fmt = (value) => {
|
|
1500
1501
|
const n = Number(value || 0);
|
|
@@ -2153,12 +2154,12 @@ export function App({ store, initialStatusLine = '', forceOnboarding = false })
|
|
|
2153
2154
|
}
|
|
2154
2155
|
}
|
|
2155
2156
|
if (!commandText) return false;
|
|
2156
|
-
if (state.commandBusy) {
|
|
2157
|
-
store.pushNotice('wait for the current command to finish', 'warn');
|
|
2158
|
-
return false;
|
|
2159
|
-
}
|
|
2160
2157
|
|
|
2161
2158
|
if (commandText.startsWith('/')) {
|
|
2159
|
+
if (state.commandBusy) {
|
|
2160
|
+
store.pushNotice('wait for the current command to finish', 'warn');
|
|
2161
|
+
return false;
|
|
2162
|
+
}
|
|
2162
2163
|
const [cmd, ...rest] = commandText.slice(1).split(/\s+/);
|
|
2163
2164
|
const accepted = runSlashCommand(cmd, rest.join(' ').trim());
|
|
2164
2165
|
if (accepted !== false) clearPastedImagesSnapshot();
|
|
@@ -537,6 +537,7 @@ function computeTranscriptItemVariantKey(item) {
|
|
|
537
537
|
const count = Number(item.count ?? 0);
|
|
538
538
|
const completed = item.completedCount === undefined ? 'u' : Number(item.completedCount);
|
|
539
539
|
const errors = item.errorCount === undefined ? 'u' : Number(item.errorCount);
|
|
540
|
+
const callErrors = item.callErrorCount === undefined ? 'u' : Number(item.callErrorCount);
|
|
540
541
|
const isError = item.isError ? 1 : 0;
|
|
541
542
|
const normalizedName = String(normalizeToolName(item.name) || '').toLowerCase();
|
|
542
543
|
const aggregate = item.aggregate ? 1 : 0;
|
|
@@ -547,7 +548,7 @@ function computeTranscriptItemVariantKey(item) {
|
|
|
547
548
|
const bgPrompt = textShapeFingerprint(bgArgs.prompt);
|
|
548
549
|
const bgMessage = textShapeFingerprint(bgArgs.message);
|
|
549
550
|
const bgError = textShapeFingerprint(bgArgs.error);
|
|
550
|
-
return `x${expanded}:n${normalizedName}:g${aggregate}:r${resultShape}:R${rawShape}:c${count}:d${completed}:e${errors}:E${isError}:bt${bgType}:bs${bgStatus}:bk${bgTaskId}:bp${bgPrompt}:bm${bgMessage}:be${bgError}`;
|
|
551
|
+
return `x${expanded}:n${normalizedName}:g${aggregate}:r${resultShape}:R${rawShape}:c${count}:d${completed}:e${errors}:ce${callErrors}:E${isError}:bt${bgType}:bs${bgStatus}:bk${bgTaskId}:bp${bgPrompt}:bm${bgMessage}:be${bgError}`;
|
|
551
552
|
}
|
|
552
553
|
return `x${expanded}:s${textShapeFingerprint(item.text ?? item.result ?? '')}`;
|
|
553
554
|
}
|
|
@@ -568,13 +569,43 @@ export const transcriptMeasuredRowsCache = new WeakMap();
|
|
|
568
569
|
// (its final settled height is then captured by the normal WeakMap path).
|
|
569
570
|
export const streamingMeasuredRowsById = new Map();
|
|
570
571
|
|
|
572
|
+
// High-water clamp for the STREAMING row ESTIMATE, keyed by stream item id.
|
|
573
|
+
// measureStreamingMarkdownRenderedRows (measure-rendered-rows.mjs) adds a +1
|
|
574
|
+
// gap row only while childCount===2 (stablePrefix box + unstableSuffix box).
|
|
575
|
+
// As the stable/unstable split moves per token — and when the suffix is
|
|
576
|
+
// momentarily whitespace-only, stablePrefix empties — childCount flips 1↔2
|
|
577
|
+
// frame-to-frame, so the estimate oscillates ±1. Streaming text only ever
|
|
578
|
+
// GROWS, so any per-frame DECREASE is spurious. Hold the max estimate seen this
|
|
579
|
+
// streaming run so streamingEstimateRows is NON-DECREASING, killing the -1 dip
|
|
580
|
+
// that shifts the transcript when a newline settles. Entry stores columns/
|
|
581
|
+
// toolExpanded so a real layout-basis change resets the water line (row count
|
|
582
|
+
// legitimately changes with width). Lifecycle mirrors streamingMeasuredRowsById
|
|
583
|
+
// exactly: pruned by pruneStreamingMeasuredRowsById and cleared at the same
|
|
584
|
+
// settle / invalidate delete sites in estimateTranscriptItemRowsCached.
|
|
585
|
+
const streamingEstimateHighWaterById = new Map();
|
|
586
|
+
|
|
587
|
+
/** True when either streaming id-keyed store holds entries, so the mount-prune
|
|
588
|
+
* call site fires even when only the estimate high-water map is populated (an
|
|
589
|
+
* item that never got a Yoga measurement but reached an estimate high-water,
|
|
590
|
+
* then was bulk-replaced, must still be pruned). */
|
|
591
|
+
export function hasStreamingRowStateToPrune() {
|
|
592
|
+
return streamingMeasuredRowsById.size > 0 || streamingEstimateHighWaterById.size > 0;
|
|
593
|
+
}
|
|
594
|
+
|
|
571
595
|
/** Drop streamingMeasuredRowsById entries for ids no longer mounted, so the
|
|
572
596
|
* store does not grow unbounded over a long session (mirrors the id→item /
|
|
573
597
|
* id→callback map pruning already done for the mounted set). */
|
|
574
598
|
export function pruneStreamingMeasuredRowsById(liveIds) {
|
|
575
|
-
if (!liveIds
|
|
576
|
-
|
|
577
|
-
|
|
599
|
+
if (!liveIds) return;
|
|
600
|
+
if (streamingMeasuredRowsById.size > 0) {
|
|
601
|
+
for (const id of streamingMeasuredRowsById.keys()) {
|
|
602
|
+
if (!liveIds.has(id)) streamingMeasuredRowsById.delete(id);
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
if (streamingEstimateHighWaterById.size > 0) {
|
|
606
|
+
for (const id of streamingEstimateHighWaterById.keys()) {
|
|
607
|
+
if (!liveIds.has(id)) streamingEstimateHighWaterById.delete(id);
|
|
608
|
+
}
|
|
578
609
|
}
|
|
579
610
|
}
|
|
580
611
|
|
|
@@ -627,7 +658,21 @@ export function streamingEstimateRows(item, columns, toolOutputExpanded) {
|
|
|
627
658
|
const trimmedText = assistantTextForStreamingRowEstimate(item.text);
|
|
628
659
|
const estimateItem = trimmedText === item.text ? item : { ...item, text: trimmedText };
|
|
629
660
|
const raw = Math.max(1, Math.ceil(estimateTranscriptItemRows(estimateItem, columns, toolOutputExpanded)));
|
|
630
|
-
|
|
661
|
+
const quantized = Math.ceil(raw / STREAMING_ROW_QUANTUM) * STREAMING_ROW_QUANTUM;
|
|
662
|
+
// High-water clamp: streaming text only grows, so never report fewer rows than
|
|
663
|
+
// this id already reached this run — absorbs the childCount 1↔2 gap flip that
|
|
664
|
+
// otherwise dips the estimate ±1 frame-to-frame. A columns/expanded change
|
|
665
|
+
// resets the water line (new width → legitimately new row count).
|
|
666
|
+
const id = item?.id;
|
|
667
|
+
if (id == null) return quantized;
|
|
668
|
+
const toolExpanded = toolOutputExpanded ? 1 : 0;
|
|
669
|
+
const prev = streamingEstimateHighWaterById.get(id);
|
|
670
|
+
if (!prev || prev.columns !== columns || prev.toolExpanded !== toolExpanded) {
|
|
671
|
+
streamingEstimateHighWaterById.set(id, { rows: quantized, columns, toolExpanded });
|
|
672
|
+
return quantized;
|
|
673
|
+
}
|
|
674
|
+
if (quantized > prev.rows) prev.rows = quantized;
|
|
675
|
+
return prev.rows;
|
|
631
676
|
}
|
|
632
677
|
|
|
633
678
|
// ── Same-frame streaming-tail growth compensation (scrolled-up only) ────────
|
|
@@ -705,15 +750,20 @@ export function estimateTranscriptItemRowsCached(item, columns, toolOutputExpand
|
|
|
705
750
|
// estimate frame.
|
|
706
751
|
return idEntry.rows;
|
|
707
752
|
}
|
|
708
|
-
if (idEntry)
|
|
753
|
+
if (idEntry) {
|
|
754
|
+
streamingMeasuredRowsById.delete(item.id);
|
|
755
|
+
streamingEstimateHighWaterById.delete(item.id);
|
|
756
|
+
}
|
|
709
757
|
// No confirmed measurement yet for this id (item just started streaming):
|
|
710
758
|
// nothing to defer against, so the first frame falls back to the estimate.
|
|
711
759
|
return streamingEstimateRows(item, columns, toolOutputExpanded);
|
|
712
760
|
}
|
|
713
|
-
if (item.kind === 'assistant'
|
|
714
|
-
// Item settled (no longer streaming): the id-keyed floor
|
|
715
|
-
// relevant
|
|
716
|
-
|
|
761
|
+
if (item.kind === 'assistant') {
|
|
762
|
+
// Item settled (no longer streaming): the id-keyed floor and the estimate
|
|
763
|
+
// high-water are no longer relevant — the normal WeakMap-measured path now
|
|
764
|
+
// owns its height. Clear both so a later id reuse / this run cannot leak.
|
|
765
|
+
if (streamingMeasuredRowsById.has(item.id)) streamingMeasuredRowsById.delete(item.id);
|
|
766
|
+
if (streamingEstimateHighWaterById.has(item.id)) streamingEstimateHighWaterById.delete(item.id);
|
|
717
767
|
}
|
|
718
768
|
const variantKey = transcriptItemVariantKey(item);
|
|
719
769
|
const toolExpanded = toolOutputExpanded ? 1 : 0;
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
transcriptMeasuredRowsCache,
|
|
16
16
|
streamingMeasuredRowsById,
|
|
17
17
|
pruneStreamingMeasuredRowsById,
|
|
18
|
+
hasStreamingRowStateToPrune,
|
|
18
19
|
transcriptItemVariantKey,
|
|
19
20
|
buildTranscriptRowIndexIncremental,
|
|
20
21
|
transcriptStructureSignature,
|
|
@@ -632,7 +633,7 @@ export function useTranscriptWindow({
|
|
|
632
633
|
if (!els.has(key)) refCache.delete(key);
|
|
633
634
|
}
|
|
634
635
|
}
|
|
635
|
-
if (
|
|
636
|
+
if (hasStreamingRowStateToPrune()) {
|
|
636
637
|
pruneStreamingMeasuredRowsById(new Set(liveItems.keys()));
|
|
637
638
|
}
|
|
638
639
|
});
|
|
@@ -48,7 +48,10 @@ function percent(value, total) {
|
|
|
48
48
|
function percentLabel(value, total) {
|
|
49
49
|
const pct = percent(value, total);
|
|
50
50
|
if (pct === null) return 'N/A';
|
|
51
|
-
|
|
51
|
+
// Match the footer/statusline display: sub-1% keeps one decimal, otherwise
|
|
52
|
+
// show the floored percentage so /context and the statusline do not disagree
|
|
53
|
+
// by 1% around half-percent boundaries.
|
|
54
|
+
return `${pct > 0 && pct < 1 ? pct.toFixed(1) : Math.floor(pct)}%`;
|
|
52
55
|
}
|
|
53
56
|
|
|
54
57
|
function usageColor(pct) {
|
|
@@ -73,7 +73,6 @@ export function displayToolName(name, args) {
|
|
|
73
73
|
}
|
|
74
74
|
|
|
75
75
|
const TOOL_BLINK_MS = 500;
|
|
76
|
-
const TOOL_BLINK_LIMIT_MS = 3000;
|
|
77
76
|
const TOOL_PENDING_SHOW_DELAY_MS = 1000;
|
|
78
77
|
// One shared-tick cadence covers both the 500ms blink and per-second elapsed;
|
|
79
78
|
// finer than either boundary so both stay crisp off a single timer.
|
|
@@ -87,7 +86,7 @@ function statusCopy(name, label, count, doneCount, pending, isError, args = {})
|
|
|
87
86
|
// dropping the pad just normalizes the spacing.
|
|
88
87
|
return formatToolActionHeader(name, args, { pending, count });
|
|
89
88
|
}
|
|
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 }) {
|
|
89
|
+
export function ToolExecution({ name, args, result, rawResult, isError, errorCount, callErrorCount, expanded, columns = 80, attached = false, count = 1, completedCount = 0, startedAt = 0, completedAt = 0, aggregate = false, categories = {}, doneCategories = null, headerFinalized = true, deferredDisplayReady = false }) {
|
|
91
90
|
const rowWidth = Math.max(1, Number(columns || 80));
|
|
92
91
|
const groupCount = Math.max(1, Number(count || 1));
|
|
93
92
|
const doneCount = Math.max(0, Math.min(groupCount, Number(completedCount || (result == null ? 0 : groupCount))));
|
|
@@ -116,13 +115,11 @@ export function ToolExecution({ name, args, result, rawResult, isError, errorCou
|
|
|
116
115
|
// land — no empty band.
|
|
117
116
|
const hasVisibleProgress = doneCount > 0 || Boolean(String(rt || '').trim());
|
|
118
117
|
const pendingDisplayReady = !pending || !startedAtMs || pendingDelayElapsed || pendingAgeMs >= TOOL_PENDING_SHOW_DELAY_MS || hasVisibleProgress || deferredDisplayReady;
|
|
119
|
-
// Derived blink (was two per-card setIntervals + a setTimeout):
|
|
120
|
-
//
|
|
121
|
-
//
|
|
122
|
-
// is identical to the old interval without owning a timer.
|
|
118
|
+
// Derived blink (was two per-card setIntervals + a setTimeout): while pending,
|
|
119
|
+
// the dot keeps blinking until the tool resolves. Phase comes from Date.now()
|
|
120
|
+
// so the cadence is identical to the old interval without owning a timer.
|
|
123
121
|
const blinkActive = pending && pendingDisplayReady;
|
|
124
|
-
const
|
|
125
|
-
const blinkOn = !blinkActive || blinkExpired
|
|
122
|
+
const blinkOn = !blinkActive
|
|
126
123
|
? true
|
|
127
124
|
: Math.floor(nowMs / TOOL_BLINK_MS) % 2 === 0;
|
|
128
125
|
// Keep the action verb in its active form until the engine explicitly seals
|
|
@@ -135,6 +132,12 @@ export function ToolExecution({ name, args, result, rawResult, isError, errorCou
|
|
|
135
132
|
const elapsedMs = startedAtMs ? Math.max(0, (pending ? nowMs : (completedAtMs || nowMs)) - startedAtMs) : 0;
|
|
136
133
|
const elapsed = elapsedMs >= 1000 ? formatElapsed(elapsedMs) : '';
|
|
137
134
|
const failedCount = clampFailureCount(errorCount, groupCount, isError);
|
|
135
|
+
// Real tool-call failures only (backend isError / error toolKind). Drives the
|
|
136
|
+
// ● dot color; command/result failures (shell exit, failed status) are counted
|
|
137
|
+
// in `failedCount`/L2 detail but never in `callFailedCount`, so they never
|
|
138
|
+
// paint the dot red. Fall back to 0 (never `isError`) when the engine did not
|
|
139
|
+
// supply a call-error count so a result failure can't leak into the dot.
|
|
140
|
+
const callFailedCount = clampFailureCount(callErrorCount, groupCount, false);
|
|
138
141
|
const displayGroupCount = groupCount;
|
|
139
142
|
const displayCategories = normalizeCountMap(categories || {});
|
|
140
143
|
// In the DONE state, count only successful calls: error-terminated calls are
|
|
@@ -209,8 +212,8 @@ export function ToolExecution({ name, args, result, rawResult, isError, errorCou
|
|
|
209
212
|
const aggregateTerminalStatus = pending
|
|
210
213
|
? 'running'
|
|
211
214
|
: (resultTerminalStatus(rt) || (isError || failedCount > 0 ? 'failed' : 'completed'));
|
|
212
|
-
const dotColor = toolStatusColor({ pending, groupCount,
|
|
213
|
-
const dotText = pending && !
|
|
215
|
+
const dotColor = toolStatusColor({ pending, groupCount, callFailedCount, terminalStatus: aggregateTerminalStatus });
|
|
216
|
+
const dotText = pending && !blinkOn ? ' ' : TURN_MARKER;
|
|
214
217
|
const gutter = 2;
|
|
215
218
|
const showHeaderExpandHint = hasRawResult;
|
|
216
219
|
const hintLabel = `ctrl+o ${expanded ? 'collapse' : 'expand'}`;
|
|
@@ -423,7 +426,7 @@ export function ToolExecution({ name, args, result, rawResult, isError, errorCou
|
|
|
423
426
|
const keepAgentDetail = (isError || agentFailureText) && !(agentHeaderFailure && !agentSurfaceBrief);
|
|
424
427
|
visibleDetailLines = keepAgentDetail ? [agentDetailLine] : [];
|
|
425
428
|
}
|
|
426
|
-
const finalStatusColor = toolStatusColor({ pending, groupCount,
|
|
429
|
+
const finalStatusColor = toolStatusColor({ pending, groupCount, callFailedCount, terminalStatus });
|
|
427
430
|
const dotColor = finalStatusColor;
|
|
428
431
|
// Agent surface cards use directional markers: `←` for requests going OUT
|
|
429
432
|
// (spawn/send/etc.) and `→` for the response coming back IN. Background
|
|
@@ -441,7 +444,7 @@ export function ToolExecution({ name, args, result, rawResult, isError, errorCou
|
|
|
441
444
|
// `●` turn marker is a true 1-cell glyph and keeps the padding-only gutter.
|
|
442
445
|
const isDirectionalMarker = isAgentResponse || isAgentSurfaceCard;
|
|
443
446
|
const markerText = isDirectionalMarker ? `${markerGlyph} ` : markerGlyph;
|
|
444
|
-
const dotText = pending && !
|
|
447
|
+
const dotText = pending && !blinkOn ? ' ' : markerText;
|
|
445
448
|
let labelText;
|
|
446
449
|
if (isAgentResponse) labelText = agentResponseTitle(parsedArgs);
|
|
447
450
|
else if (isBackgroundResponse) labelText = backgroundTaskResultTitle(normalizedName, backgroundMeta || parsedArgs);
|
|
@@ -77,7 +77,7 @@ export const Item = React.memo(function Item({ item, prevKind, columns, toolOutp
|
|
|
77
77
|
node = <ToolHookDenialCard item={item} columns={columns} />;
|
|
78
78
|
break;
|
|
79
79
|
}
|
|
80
|
-
node = <ToolExecution name={item.name} args={item.args} result={item.result} rawResult={item.rawResult} isError={item.isError} errorCount={item.errorCount} expanded={toolOutputExpanded || item.expanded} columns={columns} attached={false} count={item.count} completedCount={item.completedCount} startedAt={item.startedAt} completedAt={item.completedAt} aggregate={item.aggregate} categories={item.categories} doneCategories={item.doneCategories} headerFinalized={item.headerFinalized} deferredDisplayReady={item.deferredDisplayReady}
|
|
80
|
+
node = <ToolExecution name={item.name} args={item.args} result={item.result} rawResult={item.rawResult} isError={item.isError} errorCount={item.errorCount} callErrorCount={item.callErrorCount} expanded={toolOutputExpanded || item.expanded} columns={columns} attached={false} count={item.count} completedCount={item.completedCount} startedAt={item.startedAt} completedAt={item.completedAt} aggregate={item.aggregate} categories={item.categories} doneCategories={item.doneCategories} headerFinalized={item.headerFinalized} deferredDisplayReady={item.deferredDisplayReady} />;
|
|
81
81
|
break;
|
|
82
82
|
}
|
|
83
83
|
case 'notice':
|
|
@@ -390,16 +390,16 @@ export function clampFailureCount(errorCount, groupCount, isError) {
|
|
|
390
390
|
// partial failure -> mixdogOrange || warning (some, not all, of the group failed)
|
|
391
391
|
// all failed -> theme.error
|
|
392
392
|
// cancelled -> theme.warning
|
|
393
|
-
//
|
|
394
|
-
// (
|
|
395
|
-
//
|
|
396
|
-
//
|
|
397
|
-
//
|
|
398
|
-
export function toolStatusColor({ pending, groupCount,
|
|
393
|
+
// The RED/orange failure color is driven ONLY by real tool-call errors
|
|
394
|
+
// (`callFailedCount` — backend isError / error toolKind), NOT by command/result
|
|
395
|
+
// failures like a shell non-zero exit or a `[status: failed]` result. Those
|
|
396
|
+
// keep the card's L2 detail showing "Failed" but leave the dot on the success
|
|
397
|
+
// color. `terminalStatus` is still consulted so a cancelled card stays warning.
|
|
398
|
+
export function toolStatusColor({ pending, groupCount, callFailedCount = 0, terminalStatus = '' }) {
|
|
399
399
|
if (pending) return theme.text;
|
|
400
400
|
const status = normalizeTerminalStatus(terminalStatus);
|
|
401
401
|
if (status === 'cancelled') return theme.warning;
|
|
402
|
-
if (
|
|
403
|
-
if (groupCount > 1 &&
|
|
402
|
+
if (callFailedCount <= 0) return theme.success;
|
|
403
|
+
if (groupCount > 1 && callFailedCount < groupCount) return theme.mixdogOrange || theme.warning;
|
|
404
404
|
return theme.error;
|
|
405
405
|
}
|