mixdog 0.9.117 → 0.9.118

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mixdog",
3
- "version": "0.9.117",
3
+ "version": "0.9.118",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Standalone mixdog coding-agent CLI/TUI workspace.",
@@ -11,6 +11,7 @@ import { fileURLToPath } from 'node:url';
11
11
  import { executeBuiltinTool } from '../src/runtime/agent/orchestrator/tools/builtin.mjs';
12
12
  import { executeCodeGraphTool } from '../src/runtime/agent/orchestrator/tools/code-graph.mjs';
13
13
  import { executePatchTool } from '../src/runtime/agent/orchestrator/tools/patch.mjs';
14
+ import { normalizeToolEnvelope } from '../src/runtime/agent/orchestrator/session/tool-envelope.mjs';
14
15
 
15
16
  const root = join(fileURLToPath(new URL('.', import.meta.url)), '..');
16
17
  const SESSIONS = 8;
@@ -30,7 +31,8 @@ function record(tool, ms, result, expectRe) {
30
31
  async function timed(tool, expectRe, fn) {
31
32
  const t0 = Date.now();
32
33
  try {
33
- const out = await fn();
34
+ const raw = await fn();
35
+ const out = normalizeToolEnvelope(raw).result;
34
36
  record(tool, Date.now() - t0, out, expectRe);
35
37
  return out;
36
38
  } catch (err) {
@@ -357,19 +357,17 @@ export async function lstatPathsForMtime(paths, workDir, concurrency = Infinity,
357
357
  return out;
358
358
  }
359
359
 
360
- // Extra invalidation listeners: sibling modules with their own derived caches
361
- // (e.g. the broad find-enumeration cache in list-tool) register a clear
362
- // callback here so every write-invalidation event that drops the result/stat/
363
- // raw caches also drops theirs. Full clear is intentional — those entries are
364
- // cheap to rebuild and a path-scoped diff is not worth the coupling.
360
+ // Extra invalidation listeners: sibling modules with derived caches receive
361
+ // the normalized affected paths, or null for a full clear, so unrelated
362
+ // project inventories survive writes in another temporary/project root.
365
363
  const EXTRA_INVALIDATION_LISTENERS = new Set();
366
364
  export function registerCacheInvalidationListener(fn) {
367
365
  if (typeof fn === 'function') EXTRA_INVALIDATION_LISTENERS.add(fn);
368
366
  return () => EXTRA_INVALIDATION_LISTENERS.delete(fn);
369
367
  }
370
- function runExtraInvalidationListeners() {
368
+ function runExtraInvalidationListeners(affectedPaths = null) {
371
369
  for (const fn of EXTRA_INVALIDATION_LISTENERS) {
372
- try { fn(); } catch { /* best-effort: one listener must not block others */ }
370
+ try { fn(affectedPaths); } catch { /* best-effort: one listener must not block others */ }
373
371
  }
374
372
  }
375
373
 
@@ -411,9 +409,7 @@ function cacheInvalidatePaths(paths) {
411
409
  deleteReadRangeIndexForPath(affected);
412
410
  bumpPathMutationGeneration(affected);
413
411
  }
414
- // Broad enumeration entries are not path-scoped, so any partial
415
- // invalidation still fully drops them (cheap to rebuild).
416
- runExtraInvalidationListeners();
412
+ runExtraInvalidationListeners(affectedPaths);
417
413
  }
418
414
 
419
415
  export function invalidateBuiltinResultCache(paths = null) {
@@ -1,5 +1,5 @@
1
1
  import { readdirSync } from 'fs';
2
- import { basename, relative } from 'path';
2
+ import { basename, isAbsolute, relative } from 'path';
3
3
  import {
4
4
  coerceReadFamilyPathArg,
5
5
  extractGlobBaseDirectory,
@@ -415,17 +415,52 @@ export async function executeTreeTool(args, workDir, options = {}) {
415
415
  // known-incomplete and are NEVER cached.
416
416
  const FIND_ENUM_CACHE = new Map(); // key -> { files, expiresAt, gen }
417
417
  const FIND_ENUM_INFLIGHT = new Map(); // key -> { promise, controller, subscribers }
418
+ const FIND_TARGETED_BATCHES_BY_RUNNER = new WeakMap();
419
+ const FIND_ENUM_ROOT_GEN = new Map();
418
420
  let FIND_ENUM_GEN = 0;
419
421
 
420
422
  // The broad enumeration is a DERIVED cache the scope/path invalidation layer
421
- // does not otherwise know about a file created/renamed after a sweep would
422
- // stay invisible to broad find reuse for the whole TTL. Drop all entries on any
423
- // write-invalidation event (TTL remains the secondary bound). Full clear is
424
- // fine: entries are cheap to rebuild.
425
- registerCacheInvalidationListener(() => {
426
- FIND_ENUM_GEN += 1;
427
- FIND_ENUM_CACHE.clear();
428
- FIND_ENUM_INFLIGHT.clear();
423
+ // does not otherwise know about. Invalidate only inventories whose roots
424
+ // overlap the written paths; a patch in an isolated temp root must not force
425
+ // every active Project to rescan.
426
+ function findEnumerationPathsOverlap(left, right) {
427
+ const contains = (base, target) => {
428
+ const rel = relative(base, target);
429
+ return rel === '' || (!isAbsolute(rel) && !/^\.\.(?:[\\/]|$)/.test(rel));
430
+ };
431
+ return contains(left, right) || contains(right, left);
432
+ }
433
+ function findEnumerationRootFromKey(key) {
434
+ return String(key).split('\u0000', 1)[0];
435
+ }
436
+ function findEnumerationRootGeneration(root) {
437
+ return FIND_ENUM_ROOT_GEN.get(root) || 0;
438
+ }
439
+ registerCacheInvalidationListener((affectedPaths) => {
440
+ if (!Array.isArray(affectedPaths) || affectedPaths.length === 0) {
441
+ FIND_ENUM_GEN += 1;
442
+ FIND_ENUM_ROOT_GEN.clear();
443
+ FIND_ENUM_CACHE.clear();
444
+ FIND_ENUM_INFLIGHT.clear();
445
+ return;
446
+ }
447
+ const keys = new Set([...FIND_ENUM_CACHE.keys(), ...FIND_ENUM_INFLIGHT.keys()]);
448
+ const affectedRoots = new Set();
449
+ for (const key of keys) {
450
+ const root = findEnumerationRootFromKey(key);
451
+ if (affectedPaths.some((affected) => findEnumerationPathsOverlap(root, affected))) {
452
+ affectedRoots.add(root);
453
+ }
454
+ }
455
+ for (const root of affectedRoots) {
456
+ FIND_ENUM_ROOT_GEN.set(root, findEnumerationRootGeneration(root) + 1);
457
+ for (const key of [...FIND_ENUM_CACHE.keys()]) {
458
+ if (findEnumerationRootFromKey(key) === root) FIND_ENUM_CACHE.delete(key);
459
+ }
460
+ for (const key of [...FIND_ENUM_INFLIGHT.keys()]) {
461
+ if (findEnumerationRootFromKey(key) === root) FIND_ENUM_INFLIGHT.delete(key);
462
+ }
463
+ }
429
464
  });
430
465
 
431
466
  function findEnumTtlMs() {
@@ -503,9 +538,10 @@ function subscribeToFindEnumeration(key, entry, signal = null) {
503
538
  async function getBroadEnumeration({ root, hidden, depth, includeNoise, ignoreMode, rgArgs, cwd, runRgImpl = runRg, bestEffort = false, signal = null }) {
504
539
  const ttl = findEnumTtlMs();
505
540
  const key = findEnumKey({ root, hidden, depth, includeNoise, ignoreMode });
541
+ const rootGen = findEnumerationRootGeneration(root);
506
542
  if (ttl > 0) {
507
543
  const hit = FIND_ENUM_CACHE.get(key);
508
- if (hit && hit.gen === FIND_ENUM_GEN && hit.expiresAt > Date.now()) {
544
+ if (hit && hit.gen === FIND_ENUM_GEN && hit.rootGen === rootGen && hit.expiresAt > Date.now()) {
509
545
  return { files: hit.files, truncated: false, partial: false };
510
546
  }
511
547
  if (hit) FIND_ENUM_CACHE.delete(key); // expired
@@ -525,6 +561,7 @@ async function getBroadEnumeration({ root, hidden, depth, includeNoise, ignoreMo
525
561
  return { files: [], truncated: false, partial: true };
526
562
  }
527
563
  const genAtStart = FIND_ENUM_GEN;
564
+ const rootGenAtStart = rootGen;
528
565
  const controller = new AbortController();
529
566
  const entry = { promise: null, controller, subscribers: new Set() };
530
567
  entry.promise = (async () => {
@@ -536,8 +573,15 @@ async function getBroadEnumeration({ root, hidden, depth, includeNoise, ignoreMo
536
573
  // later query with a larger head_limit must re-run the enumeration.
537
574
  // Also never let an in-flight prewarm/real sweep repopulate after a
538
575
  // write invalidation cleared the cache during the sweep.
539
- if (ttl > 0 && !truncated && !partial && FIND_ENUM_GEN === genAtStart) {
540
- FIND_ENUM_CACHE.set(key, { files, expiresAt: Date.now() + ttl, gen: genAtStart });
576
+ if (ttl > 0 && !truncated && !partial
577
+ && FIND_ENUM_GEN === genAtStart
578
+ && findEnumerationRootGeneration(root) === rootGenAtStart) {
579
+ FIND_ENUM_CACHE.set(key, {
580
+ files,
581
+ expiresAt: Date.now() + ttl,
582
+ gen: genAtStart,
583
+ rootGen: rootGenAtStart,
584
+ });
541
585
  }
542
586
  return { files, truncated, partial };
543
587
  })();
@@ -585,21 +629,82 @@ async function getTargetedFindEnumeration({
585
629
  const key = JSON.stringify([root, hidden, depth ?? '', includeNoise, terms]);
586
630
  const runs = context?.targetedRuns;
587
631
  if (runs?.has(key)) return runs.get(key);
588
- const run = (async () => {
589
- const rgArgs = ['--files', '--no-ignore'];
590
- if (hidden) rgArgs.push('--hidden');
591
- if (depth != null) rgArgs.push('--max-depth', String(depth));
592
- for (const query of terms) rgArgs.push('--iglob', `*${escapeFindGlobLiteral(query)}*`);
593
- rgArgs.push('.');
594
- const stdout = await runRgImpl(rgArgs, { cwd: root, signal });
595
- const paths = parseRgFileList(stdout).filter((path) =>
596
- includeNoise || !path.split('/').some((segment) => NOISE_DIR_NAMES.has(segment)));
597
- return {
598
- files: paths,
599
- truncated: Boolean(stdout && typeof stdout === 'object' && stdout.truncated),
600
- partial: Boolean(stdout && typeof stdout === 'object' && stdout.partial),
632
+ let batches = FIND_TARGETED_BATCHES_BY_RUNNER.get(runRgImpl);
633
+ if (!batches) {
634
+ batches = new Map();
635
+ FIND_TARGETED_BATCHES_BY_RUNNER.set(runRgImpl, batches);
636
+ }
637
+ const batchKey = JSON.stringify([root, hidden, depth ?? '', includeNoise]);
638
+ let batch = batches.get(batchKey);
639
+ if (!batch) {
640
+ batch = {
641
+ terms: new Set(),
642
+ waiters: new Set(),
643
+ controller: new AbortController(),
601
644
  };
602
- })();
645
+ batches.set(batchKey, batch);
646
+ setImmediate(async () => {
647
+ if (batches.get(batchKey) === batch) batches.delete(batchKey);
648
+ if (batch.waiters.size === 0) return;
649
+ const rgArgs = ['--files', '--no-ignore'];
650
+ if (hidden) rgArgs.push('--hidden');
651
+ if (depth != null) rgArgs.push('--max-depth', String(depth));
652
+ for (const query of batch.terms) {
653
+ rgArgs.push('--iglob', `*${escapeFindGlobLiteral(query)}*`);
654
+ }
655
+ if (!includeNoise) {
656
+ for (const ex of DEFAULT_IGNORE_GLOBS) rgArgs.push('--glob', ex);
657
+ }
658
+ rgArgs.push('.');
659
+ try {
660
+ const stdout = await runRgImpl(rgArgs, {
661
+ cwd: root,
662
+ signal: batch.controller.signal,
663
+ });
664
+ const paths = parseRgFileList(stdout).filter((path) =>
665
+ includeNoise || !path.split('/').some((segment) => NOISE_DIR_NAMES.has(segment)));
666
+ const result = {
667
+ files: paths,
668
+ truncated: Boolean(stdout && typeof stdout === 'object' && stdout.truncated),
669
+ partial: Boolean(stdout && typeof stdout === 'object' && stdout.partial),
670
+ };
671
+ for (const waiter of [...batch.waiters]) waiter.resolve(result);
672
+ } catch (error) {
673
+ for (const waiter of [...batch.waiters]) waiter.reject(error);
674
+ }
675
+ });
676
+ }
677
+ for (const term of terms) batch.terms.add(term);
678
+ const run = new Promise((resolve, reject) => {
679
+ let settled = false;
680
+ const waiter = {
681
+ resolve(value) {
682
+ if (settled) return;
683
+ settled = true;
684
+ batch.waiters.delete(waiter);
685
+ if (signal instanceof AbortSignal) signal.removeEventListener('abort', onAbort);
686
+ resolve(value);
687
+ },
688
+ reject(error) {
689
+ if (settled) return;
690
+ settled = true;
691
+ batch.waiters.delete(waiter);
692
+ if (signal instanceof AbortSignal) signal.removeEventListener('abort', onAbort);
693
+ reject(error);
694
+ },
695
+ };
696
+ const onAbort = () => {
697
+ waiter.reject(findEnumerationAbortError(signal));
698
+ if (batch.waiters.size === 0) {
699
+ try { batch.controller.abort(findEnumerationAbortError(signal)); } catch {}
700
+ }
701
+ };
702
+ batch.waiters.add(waiter);
703
+ if (signal instanceof AbortSignal) {
704
+ if (signal.aborted) onAbort();
705
+ else signal.addEventListener('abort', onAbort, { once: true });
706
+ }
707
+ });
603
708
  if (runs) runs.set(key, run);
604
709
  return run;
605
710
  }
@@ -113,6 +113,42 @@ export async function warmNativeSearchServer() {
113
113
  }
114
114
  }
115
115
 
116
+ /** Fast Windows process table snapshot served by the resident native helper.
117
+ * Returns [{pid,parentPid,identity}] or null; callers stay conservative when
118
+ * the binary is unavailable or the request misses its short deadline. */
119
+ export async function tryNativeProcessSnapshot({ timeoutMs = 750 } = {}) {
120
+ if (process.platform !== 'win32' || process.env.MIXDOG_SEARCH_SERVER === '0') return null;
121
+ let server = _ensureServer();
122
+ if (!server && await warmNativeSearchServer()) server = _ensureServer();
123
+ if (!server) return null;
124
+ const id = ++server.sequence;
125
+ _setServerReferenced(server, true);
126
+ const response = await new Promise((resolve) => {
127
+ let settled = false;
128
+ const settle = (value) => {
129
+ if (settled) return;
130
+ settled = true;
131
+ clearTimeout(timer);
132
+ resolve(value);
133
+ if (server.pending.size === 0) _setServerReferenced(server, false);
134
+ };
135
+ const timer = setTimeout(() => {
136
+ server.pending.delete(id);
137
+ settle(null);
138
+ }, Math.max(1, Number(timeoutMs) || 750));
139
+ timer.unref?.();
140
+ server.pending.set(id, { resolve: settle, reject: () => settle(null) });
141
+ try {
142
+ server.child.stdin.write(`${JSON.stringify({ id, processSnapshot: true })}\n`);
143
+ } catch {
144
+ server.pending.delete(id);
145
+ settle(null);
146
+ }
147
+ });
148
+ if (!response || response.error || !Array.isArray(response.rows)) return null;
149
+ return response.rows;
150
+ }
151
+
116
152
  export async function tryServeSearch(argsList, execOptions = {}, opts = {}) {
117
153
  if (process.env.MIXDOG_SEARCH_SERVER === '0') return null;
118
154
  const server = _ensureServer();
@@ -1,4 +1,5 @@
1
1
  import { spawn, spawnSync } from 'child_process';
2
+ import { tryNativeProcessSnapshot } from './native-search-client.mjs';
2
3
 
3
4
  // Process/pid lifecycle helpers for background shell jobs: liveness probing,
4
5
  // tree-kill, the module-level live-job pid registry, and the CLI-shutdown exit
@@ -31,75 +32,25 @@ function isProcessTreeAlive(pid) {
31
32
  }
32
33
  }
33
34
 
34
- const windowsSnapshotCaches = new WeakMap();
35
- function windowsProcessSnapshot({ fresh = false, spawnFn = spawn } = {}) {
36
- let windowsSnapshotCache = windowsSnapshotCaches.get(spawnFn);
37
- if (!windowsSnapshotCache) {
38
- windowsSnapshotCache = { at: 0, rows: null, inFlight: null };
39
- windowsSnapshotCaches.set(spawnFn, windowsSnapshotCache);
40
- }
35
+ const windowsSnapshotCache = { at: 0, rows: null, inFlight: null };
36
+ function windowsProcessSnapshot({ fresh = false } = {}) {
41
37
  const now = Date.now();
42
38
  if (!fresh && windowsSnapshotCache.rows && now - windowsSnapshotCache.at < 750) {
43
39
  return Promise.resolve(windowsSnapshotCache.rows);
44
40
  }
45
41
  if (windowsSnapshotCache.inFlight) return windowsSnapshotCache.inFlight;
46
- const command = [
47
- "$ErrorActionPreference='Stop'",
48
- 'Get-CimInstance Win32_Process -Property ProcessId,ParentProcessId,CreationDate | ForEach-Object {',
49
- ' [Console]::Out.WriteLine(("{0}`t{1}`t{2}" -f $_.ProcessId,$_.ParentProcessId,$_.CreationDate))',
50
- '}',
51
- ].join('; ');
52
- windowsSnapshotCache.inFlight = new Promise((resolve) => {
53
- let child;
54
- let stdout = '';
55
- let settled = false;
56
- let timeout = null;
57
- const finish = (rows) => {
58
- if (settled) return;
59
- settled = true;
60
- if (timeout) clearTimeout(timeout);
61
- windowsSnapshotCache.inFlight = null;
42
+ windowsSnapshotCache.inFlight = Promise.resolve(tryNativeProcessSnapshot()).then(
43
+ (snapshot) => {
44
+ const rows = normalizeWindowsSnapshot(snapshot);
62
45
  if (rows) {
63
46
  windowsSnapshotCache.at = Date.now();
64
47
  windowsSnapshotCache.rows = rows;
65
48
  }
66
- resolve(rows);
67
- };
68
- try {
69
- child = spawnFn('powershell.exe', [
70
- '-NoLogo', '-NoProfile', '-NonInteractive', '-Command', command,
71
- ], {
72
- windowsHide: true,
73
- stdio: ['ignore', 'pipe', 'ignore'],
74
- });
75
- child.stdout.setEncoding('utf8');
76
- child.stdout.on('data', (chunk) => {
77
- if (stdout.length <= 4 * 1024 * 1024) stdout += chunk;
78
- });
79
- child.once('error', () => finish(null));
80
- child.once('close', (code) => {
81
- if (code !== 0 || !stdout) {
82
- finish(null);
83
- return;
84
- }
85
- const rows = new Map();
86
- for (const line of stdout.split(/\r?\n/)) {
87
- const [pidText, parentText, identity = ''] = line.split('\t');
88
- const rowPid = Number(pidText);
89
- const parentPid = Number(parentText);
90
- if (!Number.isFinite(rowPid) || rowPid <= 0 || !Number.isFinite(parentPid)) continue;
91
- rows.set(rowPid, { pid: rowPid, parentPid, identity });
92
- }
93
- finish(rows);
94
- });
95
- timeout = setTimeout(() => {
96
- try { child.kill(); } catch {}
97
- finish(null);
98
- }, 2000);
99
- if (typeof timeout.unref === 'function') timeout.unref();
100
- } catch {
101
- finish(null);
102
- }
49
+ return rows;
50
+ },
51
+ () => null,
52
+ ).finally(() => {
53
+ windowsSnapshotCache.inFlight = null;
103
54
  });
104
55
  return windowsSnapshotCache.inFlight;
105
56
  }
@@ -117,28 +68,39 @@ function normalizeWindowsSnapshot(snapshot) {
117
68
  return rows;
118
69
  }
119
70
 
120
- // Standby-pool support: does `pid` currently own live descendant processes?
121
- // FRESH snapshot on purpose the 750ms cache could miss a just-spawned
122
- // grandchild and wrongly certify a polluted standby shell as clean. Unknown
123
- // snapshots answer true (conservative: the pool discards instead of reusing).
124
- export async function windowsPidHasDescendants(pid, { spawnFn = spawn } = {}) {
125
- if (!Number.isFinite(pid) || pid <= 0) return false;
126
- if (process.platform !== 'win32') return false;
71
+ // Standby-pool support: capture every current descendant with a creation-time
72
+ // identity so a PID reused after the baseline snapshot cannot be allowlisted.
73
+ // Unknown snapshots stay conservative: callers discard instead of reusing.
74
+ export async function windowsPidDescendants(pid) {
75
+ if (!Number.isFinite(pid) || pid <= 0) return new Map();
76
+ if (process.platform !== 'win32') return new Map();
127
77
  let rows = null;
128
- try { rows = await windowsProcessSnapshot({ fresh: true, spawnFn }); } catch { rows = null; }
129
- if (!rows) return true;
78
+ try { rows = await windowsProcessSnapshot({ fresh: true }); } catch { rows = null; }
79
+ if (!rows) return null;
130
80
  const owned = new Set([pid]);
81
+ const descendants = new Map();
131
82
  let discovered = true;
132
83
  while (discovered) {
133
84
  discovered = false;
134
85
  for (const row of rows.values()) {
135
86
  if (!owned.has(row.pid) && owned.has(row.parentPid)) {
136
87
  owned.add(row.pid);
88
+ descendants.set(row.pid, row.identity);
137
89
  discovered = true;
138
90
  }
139
91
  }
140
92
  }
141
- return owned.size > 1;
93
+ return descendants;
94
+ }
95
+
96
+ export async function windowsPidHasDescendants(pid, { allowedDescendants = null } = {}) {
97
+ const descendants = await windowsPidDescendants(pid);
98
+ if (!descendants) return true;
99
+ if (!(allowedDescendants instanceof Map)) return descendants.size > 0;
100
+ for (const [descendantPid, identity] of descendants) {
101
+ if (allowedDescendants.get(descendantPid) !== identity) return true;
102
+ }
103
+ return false;
142
104
  }
143
105
 
144
106
  // Invoke onQuiescent exactly once, synchronously when already quiescent or
@@ -165,7 +127,7 @@ export function trackProcessTreeQuiescence(
165
127
  // The PID returned by spawn is durable pre-exit ownership evidence.
166
128
  // Seed it before the first asynchronous snapshot so a child whose
167
129
  // parent is this PID remains attributable even if the root is already
168
- // absent when CIM returns.
130
+ // absent when the native snapshot returns.
169
131
  ownedWindowsProcesses.set(pid, null);
170
132
  }
171
133
  const finish = () => {
@@ -2,9 +2,10 @@
2
2
  // resolveSymbolReadSpan, executeCodeGraphTool (entry with cwd re-rooting +
3
3
  // batch fan-out + abort race), isCodeGraphTool. Extracted verbatim from
4
4
  // code-graph.mjs.
5
- import { resolve as pathResolve, isAbsolute, relative as pathRelative, basename as pathBasename } from 'node:path';
5
+ import { resolve as pathResolve, isAbsolute, relative as pathRelative, basename as pathBasename, extname } from 'node:path';
6
6
  import { homedir as osHomedir } from 'node:os';
7
7
  import { existsSync, statSync } from 'node:fs';
8
+ import { readFile } from 'node:fs/promises';
8
9
  import { normalizeInputPath, toDisplayPath } from '../builtin.mjs';
9
10
  import { findFileByBasename } from '../builtin/path-diagnostics.mjs';
10
11
  import { markScopedCacheIncomplete } from '../../session/cache/scoped-cache-outcome.mjs';
@@ -55,6 +56,32 @@ const CODE_GRAPH_BATCHABLE_MODES = new Set(['symbol', 'find_symbol', 'symbol_sea
55
56
  const CODE_GRAPH_FILE_BATCHABLE_MODES = new Set(['imports', 'dependents', 'related', 'impact', 'symbols', 'overview']);
56
57
  const CODE_GRAPH_BATCH_CONCURRENCY = 20;
57
58
 
59
+ function _outlineLanguageForPath(file) {
60
+ const ext = extname(String(file || '')).slice(1);
61
+ if (['js', 'mjs', 'cjs', 'jsx'].includes(ext)) return 'javascript';
62
+ if (['ts', 'tsx', 'mts', 'cts'].includes(ext)) return 'typescript';
63
+ if (ext === 'py') return 'python';
64
+ if (ext === 'go') return 'go';
65
+ if (ext === 'rs') return 'rust';
66
+ if (ext === 'java') return 'java';
67
+ if (ext === 'kt' || ext === 'kts') return 'kotlin';
68
+ if (ext === 'cs') return 'csharp';
69
+ if (ext === 'rb') return 'ruby';
70
+ if (ext === 'php') return 'php';
71
+ if (ext === 'swift') return 'swift';
72
+ if (ext === 'c' || ext === 'h') return 'c';
73
+ if (['cpp', 'cc', 'cxx', 'hpp', 'hxx'].includes(ext)) return 'cpp';
74
+ if (ext === 'scala' || ext === 'sc') return 'scala';
75
+ if (ext === 'sh' || ext === 'bash' || ext === 'zsh') return 'bash';
76
+ if (ext === 'lua') return 'lua';
77
+ if (ext === 'dart') return 'dart';
78
+ if (ext === 'm' || ext === 'mm') return 'objc';
79
+ if (ext === 'ex' || ext === 'exs') return 'elixir';
80
+ if (ext === 'zig') return 'zig';
81
+ if (ext === 'r' || ext === 'R') return 'r';
82
+ return null;
83
+ }
84
+
58
85
  async function _mapWithConcurrency(values, mapper) {
59
86
  const out = new Array(values.length);
60
87
  let cursor = 0;
@@ -265,6 +292,28 @@ async function codeGraph(args, cwd, signal = null, options = {}) {
265
292
  return `prewarm scheduled: cwd=${cwd} symbols=${symbols.length}${symbols.length ? ` (${symbols.slice(0, 5).join(',')}${symbols.length > 5 ? `,+${symbols.length - 5}` : ''})` : ''}`;
266
293
  }
267
294
 
295
+ // A file outline is source-local: it needs neither imports nor reverse
296
+ // edges. Read the explicit file directly instead of waiting for a cold
297
+ // whole-project graph build. Relationship and name-search modes keep the
298
+ // full graph path below.
299
+ if (mode === 'symbols') {
300
+ const normFile = normalizeInputPath(args?.file);
301
+ const abs = normFile
302
+ ? (isAbsolute(normFile) ? pathResolve(normFile) : pathResolve(cwd, normFile))
303
+ : null;
304
+ const lang = abs ? _outlineLanguageForPath(abs) : null;
305
+ if (abs && lang) {
306
+ if (signal?.aborted) throw new Error('aborted');
307
+ try {
308
+ const text = await readFile(abs, { encoding: 'utf8', signal: signal || undefined });
309
+ return _extractSymbolsCheap(text, lang);
310
+ } catch (error) {
311
+ if (signal?.aborted) throw new Error('aborted');
312
+ if (error?.code !== 'ENOENT' && error?.code !== 'EISDIR') throw error;
313
+ }
314
+ }
315
+ }
316
+
268
317
  const graph = await buildCodeGraphAsync(cwd, signal, {
269
318
  excludedProjectRoots: options?.excludedProjectRoots,
270
319
  });
@@ -1,26 +1,26 @@
1
1
  {
2
- "version": "0.1.4",
2
+ "version": "0.1.5",
3
3
  "_comment": "Synced from immutable graph-v release assets.",
4
4
  "assets": {
5
5
  "darwin-arm64": {
6
- "url": "https://github.com/tribgames/mixdog/releases/download/graph-v0.1.4/mixdog-graph-darwin-arm64",
7
- "sha256": "1531098011a3c32ad9a6d002e7240b829352fda60afc5708699dd0d335405201"
6
+ "url": "https://github.com/tribgames/mixdog/releases/download/graph-v0.1.5/mixdog-graph-darwin-arm64",
7
+ "sha256": "8124df97acdac619126c0686896c8b419fca9c3f2fc605547c21459b840deb3b"
8
8
  },
9
9
  "darwin-x64": {
10
- "url": "https://github.com/tribgames/mixdog/releases/download/graph-v0.1.4/mixdog-graph-darwin-x64",
11
- "sha256": "ead5684bebc2ef001724b2d71f23f3e7fc6e52d60d6deb5cea17b73fc3768a1b"
10
+ "url": "https://github.com/tribgames/mixdog/releases/download/graph-v0.1.5/mixdog-graph-darwin-x64",
11
+ "sha256": "64a4f2d5bb4e769373beb4aec7d355bcf53882af9ab8a401338f99441ee2e99b"
12
12
  },
13
13
  "linux-arm64": {
14
- "url": "https://github.com/tribgames/mixdog/releases/download/graph-v0.1.4/mixdog-graph-linux-arm64",
15
- "sha256": "8d6d24490dfff31f66967b8384f4afe8f85051f9352c3866a2da32e6ef52d644"
14
+ "url": "https://github.com/tribgames/mixdog/releases/download/graph-v0.1.5/mixdog-graph-linux-arm64",
15
+ "sha256": "ac9aae52012267b37139eeae45f6d2de780ec8f3e128a06a3843884f34434fa4"
16
16
  },
17
17
  "linux-x64": {
18
- "url": "https://github.com/tribgames/mixdog/releases/download/graph-v0.1.4/mixdog-graph-linux-x64",
19
- "sha256": "ed26c40956d90d9863d18c028cc7766e9abf5af33405a9cab02f7cbc4a3c7eac"
18
+ "url": "https://github.com/tribgames/mixdog/releases/download/graph-v0.1.5/mixdog-graph-linux-x64",
19
+ "sha256": "12314eaf7d2168ff203edf0d45dff315fba00d49704183f61f0eba9cfc1cab72"
20
20
  },
21
21
  "win32-x64": {
22
- "url": "https://github.com/tribgames/mixdog/releases/download/graph-v0.1.4/mixdog-graph-win32-x64.exe",
23
- "sha256": "f87bd63e70ecc0a7639f0143b5954a2b673d84e6c59e1ca8f7a8e5cdcc50a21a"
22
+ "url": "https://github.com/tribgames/mixdog/releases/download/graph-v0.1.5/mixdog-graph-win32-x64.exe",
23
+ "sha256": "b7d724ec01e5af4bee1764a08796bf0e1cefc2e2b841e6285f7b669a54774d10"
24
24
  }
25
25
  }
26
26
  }
@@ -27,7 +27,11 @@ import { spawn } from 'node:child_process';
27
27
  import { basename } from 'node:path';
28
28
  import { createHash, randomBytes } from 'node:crypto';
29
29
  import { availableParallelism, freemem } from 'node:os';
30
- import { killProcessTree, windowsPidHasDescendants } from '../builtin/shell-job-process.mjs';
30
+ import {
31
+ killProcessTree,
32
+ windowsPidDescendants,
33
+ windowsPidHasDescendants,
34
+ } from '../builtin/shell-job-process.mjs';
31
35
 
32
36
  const MB = 1024 * 1024;
33
37
 
@@ -269,6 +273,8 @@ function _spawnStandby(shell, env, sig) {
269
273
  sig,
270
274
  nonce,
271
275
  ready: false,
276
+ baselinePending: false,
277
+ baselineDescendants: null,
272
278
  taken: false,
273
279
  consumed: false,
274
280
  idleTimer: null,
@@ -281,9 +287,27 @@ function _spawnStandby(shell, env, sig) {
281
287
  if (readyTail.length > readyMarker.length * 2) readyTail = readyTail.slice(-readyMarker.length * 2);
282
288
  return;
283
289
  }
284
- entry.ready = true;
285
290
  try { child.stdout.removeListener('data', onReadyData); } catch {}
286
- _signalStandbyChange();
291
+ if (entry.baselinePending) return;
292
+ entry.baselinePending = true;
293
+ // pwsh owns a persistent conhost child before it becomes READY. Record
294
+ // that clean baseline once; recycle rejects only NEW descendants, with
295
+ // creation identities preventing PID-reuse false negatives.
296
+ void windowsPidDescendants(child.pid).then((baseline) => {
297
+ entry.baselinePending = false;
298
+ if (!baseline || child.exitCode !== null || child.signalCode !== null) {
299
+ _discardIdle(entry);
300
+ _killEntry(entry);
301
+ return;
302
+ }
303
+ entry.baselineDescendants = baseline;
304
+ entry.ready = true;
305
+ _signalStandbyChange();
306
+ }, () => {
307
+ entry.baselinePending = false;
308
+ _discardIdle(entry);
309
+ _killEntry(entry);
310
+ });
287
311
  };
288
312
  child.stdout.setEncoding('utf8');
289
313
  child.stdout.on('data', onReadyData);
@@ -428,7 +452,11 @@ export function takePwshStandby({ shell, shellArgs, env }) {
428
452
  const alive = () => c.exitCode === null && c.signalCode === null;
429
453
  let dirty = true;
430
454
  if (alive() && c.stdin && !c.stdin.destroyed) {
431
- try { dirty = await windowsPidHasDescendants(c.pid); } catch { dirty = true; }
455
+ try {
456
+ dirty = await windowsPidHasDescendants(c.pid, {
457
+ allowedDescendants: entry.baselineDescendants,
458
+ });
459
+ } catch { dirty = true; }
432
460
  }
433
461
  if (dirty || !alive() || !c.stdin || c.stdin.destroyed
434
462
  || _idle.length >= POOL_TARGET || _envSig !== entry.sig) {
@@ -79,6 +79,31 @@ export function normalizeTerminalStatus(value) {
79
79
  return normalizeToolTerminalStatus(value);
80
80
  }
81
81
 
82
+ // Semantic outcome shared by TUI and desktop. A returned command/task failure
83
+ // means the tool transport worked, so it is warning-yellow; red is reserved
84
+ // for a failed invocation. A failed mutation that still committed a diff is
85
+ // likewise partial success, not a total failure.
86
+ export function deriveToolOutcomeTone({
87
+ pending = false,
88
+ groupCount = 1,
89
+ callFailedCount = 0,
90
+ exitFailedCount = 0,
91
+ terminalStatus = '',
92
+ partialMutation = false,
93
+ } = {}) {
94
+ if (pending) return 'running';
95
+ const status = normalizeTerminalStatus(terminalStatus);
96
+ if (status === 'cancelled' || status === 'denied') return 'warning';
97
+ const count = Math.max(1, Number(groupCount) || 1);
98
+ const callFailures = Math.max(0, Number(callFailedCount) || 0);
99
+ if (callFailures > 0) {
100
+ if (partialMutation || (count > 1 && callFailures < count)) return 'warning';
101
+ return 'error';
102
+ }
103
+ if (status === 'failed' || Number(exitFailedCount) > 0) return 'warning';
104
+ return 'success';
105
+ }
106
+
82
107
  export function displayTerminalStatus(value) {
83
108
  // 'exit' is a shell-only pseudo-status (command RAN but exited non-zero); it
84
109
  // is intentionally NOT a normalized terminal status so it never colors red.
@@ -50,7 +50,7 @@ const TOOL_PENDING_SHOW_DELAY_MS = 1000;
50
50
  // One shared-tick cadence covers both the 500ms blink and per-second elapsed;
51
51
  // finer than either boundary so both stay crisp off a single timer.
52
52
  const TOOL_ANIM_TICK_MS = TOOL_BLINK_MS;
53
- export function ToolExecution({ name, args, result, rawResult, isError, errorCount, callErrorCount, exitErrorCount, expanded, columns = 80, attached = false, count = 1, completedCount = 0, startedAt = 0, completedAt = 0, aggregate = false, categories = {}, doneCategories = null, headerFinalized = true, deferredDisplayReady = false, agentResponseAggregate = false }) {
53
+ export function ToolExecution({ name, args, result, rawResult, uiDiff, isError, errorCount, callErrorCount, exitErrorCount, expanded, columns = 80, attached = false, count = 1, completedCount = 0, startedAt = 0, completedAt = 0, aggregate = false, categories = {}, doneCategories = null, headerFinalized = true, deferredDisplayReady = false, agentResponseAggregate = false }) {
54
54
  const rowWidth = Math.max(1, Number(columns || 80));
55
55
  const groupCount = Math.max(1, Number(count || 1));
56
56
  const doneCount = Math.max(0, Math.min(groupCount, Number(completedCount || (result == null ? 0 : groupCount))));
@@ -105,6 +105,12 @@ export function ToolExecution({ name, args, result, rawResult, isError, errorCou
105
105
  // Shell command-exits (ran, non-zero exit). Counted separately so the dot
106
106
  // paints the neutral warning "Exit" color instead of red or green success.
107
107
  const exitFailedCount = clampFailureCount(exitErrorCount, groupCount, false);
108
+ // apply_patch can commit an ordered prefix before a later section fails.
109
+ // The runtime-provided uiDiff is authoritative evidence of that partial
110
+ // mutation; an empty/missing diff means the invocation failed completely.
111
+ const partialMutation = callFailedCount > 0
112
+ && typeof uiDiff === 'string'
113
+ && Boolean(uiDiff.trim());
108
114
  const displayGroupCount = groupCount;
109
115
  const displayCategories = normalizeCountMap(categories || {});
110
116
  // In the DONE state the engine-supplied doneCategories map counts ATTEMPTS
@@ -178,7 +184,7 @@ export function ToolExecution({ name, args, result, rawResult, isError, errorCou
178
184
  const aggregateTerminalStatus = pending
179
185
  ? 'running'
180
186
  : (resultTerminalStatus(rt) || (isError || failedCount > 0 ? 'failed' : 'completed'));
181
- const dotColor = toolStatusColor({ pending, groupCount, callFailedCount, exitFailedCount, terminalStatus: aggregateTerminalStatus });
187
+ const dotColor = toolStatusColor({ pending, groupCount, callFailedCount, exitFailedCount, terminalStatus: aggregateTerminalStatus, partialMutation });
182
188
  const dotText = pending && !blinkOn ? ' ' : TURN_MARKER;
183
189
  const gutter = 2;
184
190
  const showHeaderExpandHint = hasRawResult;
@@ -306,7 +312,7 @@ export function ToolExecution({ name, args, result, rawResult, isError, errorCou
306
312
  const detailColor = isPendingPlaceholderDetail ? theme.subtle : theme.text;
307
313
  // Skill/agent collapsed gating lives in the shared model (detailLine).
308
314
  const visibleDetailLines = detailLines;
309
- const finalStatusColor = toolStatusColor({ pending, groupCount, callFailedCount, exitFailedCount, terminalStatus });
315
+ const finalStatusColor = toolStatusColor({ pending, groupCount, callFailedCount, exitFailedCount, terminalStatus, partialMutation });
310
316
  const dotColor = finalStatusColor;
311
317
  // Agent surface cards use directional markers: `←` for requests going OUT
312
318
  // (spawn/send/etc.) and `→` for the response coming back IN. Background
@@ -80,7 +80,7 @@ export const Item = React.memo(function Item({ item, prevKind, columns, toolOutp
80
80
  // Every tool card keeps its one-row gap above (user reverted the earlier
81
81
  // "stack consecutive cards flush" experiment: attached rows read broken).
82
82
  // Keep transcript-window.mjs row estimation in sync (attachedTool=false).
83
- node = <ToolExecution name={item.name} args={item.args} result={item.result} rawResult={item.rawResult} isError={item.isError} errorCount={item.errorCount} callErrorCount={item.callErrorCount} exitErrorCount={item.exitErrorCount} 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} agentResponseAggregate={item.agentResponseAggregate} />;
83
+ node = <ToolExecution name={item.name} args={item.args} result={item.result} rawResult={item.rawResult} uiDiff={item.uiDiff} isError={item.isError} errorCount={item.errorCount} callErrorCount={item.callErrorCount} exitErrorCount={item.exitErrorCount} 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} agentResponseAggregate={item.agentResponseAggregate} />;
84
84
  break;
85
85
  }
86
86
  case 'notice':
@@ -6,7 +6,7 @@
6
6
  * existing TUI imports and keeps only the theme-dependent dot color.
7
7
  */
8
8
  import { theme } from '../../theme.mjs';
9
- import { normalizeTerminalStatus } from '../../../runtime/shared/tool-card-model.mjs';
9
+ import { deriveToolOutcomeTone } from '../../../runtime/shared/tool-card-model.mjs';
10
10
 
11
11
  export {
12
12
  isShellTool,
@@ -35,29 +35,12 @@ export {
35
35
  clampFailureCount,
36
36
  } from '../../../runtime/shared/tool-card-model.mjs';
37
37
 
38
- // Single source of truth for the tool-card dot (●) color. Both the aggregate
39
- // and normal (single-tool) render paths must call this with a resolved
40
- // `terminalStatus` do not recompute color inline elsewhere.
41
- // running/pending -> theme.text (white; blink handled by caller)
42
- // success -> theme.success
43
- // partial failure -> mixdogOrange || warning (some, not all, of the group failed)
44
- // all failed -> theme.error
45
- // cancelled -> theme.warning
46
- // The RED/orange failure color is driven ONLY by real tool-call errors
47
- // (`callFailedCount` — provider isError / error toolKind), NOT by command/result
48
- // failures like a `[status: failed]` result. A shell command-exit
49
- // (`exitFailedCount`) is its own distinct neutral state: warning color, never
50
- // red. `terminalStatus` is still consulted so a cancelled card stays warning.
51
- export function toolStatusColor({ pending, groupCount, callFailedCount = 0, exitFailedCount = 0, terminalStatus = '' }) {
52
- if (pending) return theme.text;
53
- const status = normalizeTerminalStatus(terminalStatus);
54
- if (status === 'cancelled') return theme.warning;
55
- if (status === 'denied') return theme.warning;
56
- if (callFailedCount > 0) {
57
- if (groupCount > 1 && callFailedCount < groupCount) return theme.mixdogOrange || theme.warning;
58
- return theme.error;
59
- }
60
- // Command-exit(s) with no real tool-call failure: distinct warning state.
61
- if (exitFailedCount > 0) return theme.warning;
38
+ // Theme binding only; semantic outcome lives in the shared card model so the
39
+ // TUI and desktop cannot disagree about success, warning, and failure.
40
+ export function toolStatusColor(input) {
41
+ const tone = deriveToolOutcomeTone(input);
42
+ if (tone === 'running') return theme.text;
43
+ if (tone === 'warning') return theme.warning;
44
+ if (tone === 'error') return theme.error;
62
45
  return theme.success;
63
46
  }
@@ -15172,6 +15172,26 @@ function shellResultStatus(value) {
15172
15172
  function normalizeTerminalStatus(value) {
15173
15173
  return normalizeToolTerminalStatus(value);
15174
15174
  }
15175
+ function deriveToolOutcomeTone({
15176
+ pending = false,
15177
+ groupCount = 1,
15178
+ callFailedCount = 0,
15179
+ exitFailedCount = 0,
15180
+ terminalStatus = "",
15181
+ partialMutation = false
15182
+ } = {}) {
15183
+ if (pending) return "running";
15184
+ const status = normalizeTerminalStatus(terminalStatus);
15185
+ if (status === "cancelled" || status === "denied") return "warning";
15186
+ const count = Math.max(1, Number(groupCount) || 1);
15187
+ const callFailures = Math.max(0, Number(callFailedCount) || 0);
15188
+ if (callFailures > 0) {
15189
+ if (partialMutation || count > 1 && callFailures < count) return "warning";
15190
+ return "error";
15191
+ }
15192
+ if (status === "failed" || Number(exitFailedCount) > 0) return "warning";
15193
+ return "success";
15194
+ }
15175
15195
  function displayTerminalStatus(value) {
15176
15196
  if (String(value || "").trim().toLowerCase() === "exit") return "Exit";
15177
15197
  const status = normalizeTerminalStatus(value);
@@ -15771,16 +15791,11 @@ function truncateToWidth(text, maxWidth) {
15771
15791
  }
15772
15792
 
15773
15793
  // src/tui/components/tool-execution/surface-detail.mjs
15774
- function toolStatusColor({ pending, groupCount, callFailedCount = 0, exitFailedCount = 0, terminalStatus = "" }) {
15775
- if (pending) return theme.text;
15776
- const status = normalizeTerminalStatus(terminalStatus);
15777
- if (status === "cancelled") return theme.warning;
15778
- if (status === "denied") return theme.warning;
15779
- if (callFailedCount > 0) {
15780
- if (groupCount > 1 && callFailedCount < groupCount) return theme.mixdogOrange || theme.warning;
15781
- return theme.error;
15782
- }
15783
- if (exitFailedCount > 0) return theme.warning;
15794
+ function toolStatusColor(input) {
15795
+ const tone = deriveToolOutcomeTone(input);
15796
+ if (tone === "running") return theme.text;
15797
+ if (tone === "warning") return theme.warning;
15798
+ if (tone === "error") return theme.error;
15784
15799
  return theme.success;
15785
15800
  }
15786
15801
 
@@ -15809,7 +15824,7 @@ import { jsx as jsx16, jsxs as jsxs12 } from "react/jsx-runtime";
15809
15824
  var TOOL_BLINK_MS = 500;
15810
15825
  var TOOL_PENDING_SHOW_DELAY_MS = 1e3;
15811
15826
  var TOOL_ANIM_TICK_MS = TOOL_BLINK_MS;
15812
- function ToolExecution({ name, args, result, rawResult, isError, errorCount, callErrorCount, exitErrorCount, expanded, columns = 80, attached = false, count = 1, completedCount = 0, startedAt = 0, completedAt = 0, aggregate = false, categories = {}, doneCategories = null, headerFinalized = true, deferredDisplayReady = false, agentResponseAggregate = false }) {
15827
+ function ToolExecution({ name, args, result, rawResult, uiDiff, isError, errorCount, callErrorCount, exitErrorCount, expanded, columns = 80, attached = false, count = 1, completedCount = 0, startedAt = 0, completedAt = 0, aggregate = false, categories = {}, doneCategories = null, headerFinalized = true, deferredDisplayReady = false, agentResponseAggregate = false }) {
15813
15828
  const rowWidth = Math.max(1, Number(columns || 80));
15814
15829
  const groupCount = Math.max(1, Number(count || 1));
15815
15830
  const doneCount = Math.max(0, Math.min(groupCount, Number(completedCount || (result == null ? 0 : groupCount))));
@@ -15834,6 +15849,7 @@ function ToolExecution({ name, args, result, rawResult, isError, errorCount, cal
15834
15849
  const failedCount = clampFailureCount(errorCount, groupCount, isError);
15835
15850
  const callFailedCount = clampFailureCount(callErrorCount, groupCount, false);
15836
15851
  const exitFailedCount = clampFailureCount(exitErrorCount, groupCount, false);
15852
+ const partialMutation = callFailedCount > 0 && typeof uiDiff === "string" && Boolean(uiDiff.trim());
15837
15853
  const displayGroupCount = groupCount;
15838
15854
  const displayCategories = normalizeCountMap(categories || {});
15839
15855
  const normalizedDoneCategories = doneCategories ? normalizeCountMap(doneCategories) : displayCategories;
@@ -15859,7 +15875,7 @@ function ToolExecution({ name, args, result, rawResult, isError, errorCount, cal
15859
15875
  detailText = "";
15860
15876
  }
15861
15877
  const aggregateTerminalStatus = pending ? "running" : resultTerminalStatus(rt) || (isError || failedCount > 0 ? "failed" : "completed");
15862
- const dotColor2 = toolStatusColor({ pending, groupCount, callFailedCount, exitFailedCount, terminalStatus: aggregateTerminalStatus });
15878
+ const dotColor2 = toolStatusColor({ pending, groupCount, callFailedCount, exitFailedCount, terminalStatus: aggregateTerminalStatus, partialMutation });
15863
15879
  const dotText2 = pending && !blinkOn ? " " : TURN_MARKER;
15864
15880
  const gutter2 = 2;
15865
15881
  const showHeaderExpandHint2 = hasRawResult;
@@ -15943,7 +15959,7 @@ function ToolExecution({ name, args, result, rawResult, isError, errorCount, cal
15943
15959
  const isPendingPlaceholderDetail = !showRawResult && detailIsPlaceholder;
15944
15960
  const detailColor = isPendingPlaceholderDetail ? theme.subtle : theme.text;
15945
15961
  const visibleDetailLines = detailLines;
15946
- const finalStatusColor = toolStatusColor({ pending, groupCount, callFailedCount, exitFailedCount, terminalStatus });
15962
+ const finalStatusColor = toolStatusColor({ pending, groupCount, callFailedCount, exitFailedCount, terminalStatus, partialMutation });
15947
15963
  const dotColor = finalStatusColor;
15948
15964
  const markerGlyph = isAgentResponse ? AGENT_RESPONSE_MARKER : isAgentSurfaceCard ? AGENT_CALL_MARKER : TURN_MARKER;
15949
15965
  const isDirectionalMarker = isAgentResponse || isAgentSurfaceCard;
@@ -16149,7 +16165,7 @@ var Item = React19.memo(function Item2({ item, prevKind, columns, toolOutputExpa
16149
16165
  node = /* @__PURE__ */ jsx19(ToolHookDenialCard, { item, columns });
16150
16166
  break;
16151
16167
  }
16152
- node = /* @__PURE__ */ jsx19(ToolExecution, { name: item.name, args: item.args, result: item.result, rawResult: item.rawResult, isError: item.isError, errorCount: item.errorCount, callErrorCount: item.callErrorCount, exitErrorCount: item.exitErrorCount, expanded: toolOutputExpanded || item.expanded, 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, agentResponseAggregate: item.agentResponseAggregate });
16168
+ node = /* @__PURE__ */ jsx19(ToolExecution, { name: item.name, args: item.args, result: item.result, rawResult: item.rawResult, uiDiff: item.uiDiff, isError: item.isError, errorCount: item.errorCount, callErrorCount: item.callErrorCount, exitErrorCount: item.exitErrorCount, expanded: toolOutputExpanded || item.expanded, 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, agentResponseAggregate: item.agentResponseAggregate });
16153
16169
  break;
16154
16170
  }
16155
16171
  case "notice":