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 +1 -1
- package/scripts/tool-stress.mjs +3 -1
- package/src/runtime/agent/orchestrator/tools/builtin/cache-layers.mjs +6 -10
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +131 -26
- package/src/runtime/agent/orchestrator/tools/builtin/native-search-client.mjs +36 -0
- package/src/runtime/agent/orchestrator/tools/builtin/shell-job-process.mjs +33 -71
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +50 -1
- package/src/runtime/agent/orchestrator/tools/graph-manifest.json +11 -11
- package/src/runtime/agent/orchestrator/tools/lib/pwsh-standby-pool.mjs +32 -4
- package/src/runtime/shared/tool-card-model.mjs +25 -0
- package/src/tui/components/ToolExecution.jsx +9 -3
- package/src/tui/components/TranscriptItem.jsx +1 -1
- package/src/tui/components/tool-execution/surface-detail.mjs +8 -25
- package/src/tui/dist/index.mjs +30 -14
package/package.json
CHANGED
package/scripts/tool-stress.mjs
CHANGED
|
@@ -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
|
|
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
|
|
361
|
-
//
|
|
362
|
-
//
|
|
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
|
-
|
|
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
|
|
422
|
-
//
|
|
423
|
-
//
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
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
|
|
540
|
-
|
|
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
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
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
|
|
35
|
-
function windowsProcessSnapshot({ fresh = false
|
|
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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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:
|
|
121
|
-
//
|
|
122
|
-
//
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
if (
|
|
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
|
|
129
|
-
if (!rows) return
|
|
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
|
|
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
|
|
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.
|
|
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.
|
|
7
|
-
"sha256": "
|
|
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.
|
|
11
|
-
"sha256": "
|
|
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.
|
|
15
|
-
"sha256": "
|
|
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.
|
|
19
|
-
"sha256": "
|
|
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.
|
|
23
|
-
"sha256": "
|
|
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 {
|
|
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
|
-
|
|
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 {
|
|
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 {
|
|
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
|
-
//
|
|
39
|
-
// and
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
}
|
package/src/tui/dist/index.mjs
CHANGED
|
@@ -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(
|
|
15775
|
-
|
|
15776
|
-
|
|
15777
|
-
if (
|
|
15778
|
-
if (
|
|
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":
|