@yeaft/webchat-agent 1.0.317 → 1.0.320
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/local-runtime/version.json +1 -1
- package/local-runtime/web/app.bundle.js +92 -92
- package/local-runtime/web/app.bundle.js.gz +0 -0
- package/local-runtime/web/index.html +2 -2
- package/local-runtime/web/style.bundle.css +1 -1
- package/local-runtime/web/style.bundle.css.gz +0 -0
- package/package.json +1 -1
- package/yeaft/tools/disk-usage.js +20 -6
- package/yeaft/tools/glob.js +19 -7
- package/yeaft/tools/grep.js +31 -8
- package/yeaft/tools/search-paths.js +27 -6
- package/yeaft/web-bridge.js +34 -1
|
Binary file
|
package/package.json
CHANGED
|
@@ -4,12 +4,18 @@ import { basename, join, relative, resolve } from 'node:path';
|
|
|
4
4
|
import { defineTool } from './types.js';
|
|
5
5
|
import { managedCliToolReady, resolveManagedCliCommand } from '../managed-cli.js';
|
|
6
6
|
import { runProcess } from './process-runner.js';
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
isAbortError,
|
|
9
|
+
SearchBackendLimitError,
|
|
10
|
+
throwIfAborted,
|
|
11
|
+
waitForAbortable,
|
|
12
|
+
} from './search-paths.js';
|
|
8
13
|
|
|
9
14
|
const FALLBACK_CONCURRENCY = 16;
|
|
10
15
|
const MAX_LIMIT = 200;
|
|
11
16
|
const MAX_DEPTH = 10;
|
|
12
17
|
const MAX_OUTPUT_BYTES = 512 * 1024;
|
|
18
|
+
const DUST_RESULT_ROWS = MAX_LIMIT;
|
|
13
19
|
|
|
14
20
|
function formatSize(bytes) {
|
|
15
21
|
if (bytes < 1024) return `${bytes}B`;
|
|
@@ -40,10 +46,16 @@ function flattenDustTree(root, baseDir, depth, limit) {
|
|
|
40
46
|
return [total, ...rows.slice(0, Math.max(0, limit - 1))].filter(Boolean);
|
|
41
47
|
}
|
|
42
48
|
|
|
43
|
-
async function runDust(
|
|
44
|
-
|
|
49
|
+
export async function runDust(
|
|
50
|
+
command,
|
|
51
|
+
baseDir,
|
|
52
|
+
{ depth, limit, signal },
|
|
53
|
+
processRunner = runProcess,
|
|
54
|
+
) {
|
|
55
|
+
const result = await processRunner(command, [
|
|
45
56
|
'--output-json',
|
|
46
57
|
'--depth', String(depth),
|
|
58
|
+
'--number-of-lines', String(DUST_RESULT_ROWS),
|
|
47
59
|
'--apparent-size',
|
|
48
60
|
'--only-dir',
|
|
49
61
|
'--no-progress',
|
|
@@ -56,8 +68,10 @@ async function runDust(command, baseDir, { depth, limit, signal }) {
|
|
|
56
68
|
maxBytes: MAX_OUTPUT_BYTES,
|
|
57
69
|
env: { ...process.env, NO_COLOR: '1' },
|
|
58
70
|
});
|
|
59
|
-
if (result.timedOut) throw new
|
|
60
|
-
if (result.truncated)
|
|
71
|
+
if (result.timedOut) throw new SearchBackendLimitError('dust timed out');
|
|
72
|
+
if (result.truncated) {
|
|
73
|
+
throw new SearchBackendLimitError('dust output exceeded the tool limit');
|
|
74
|
+
}
|
|
61
75
|
if (result.code !== 0) throw new Error(result.stderr.trim() || `dust exited with code ${result.code}`);
|
|
62
76
|
return flattenDustTree(JSON.parse(result.stdout), baseDir, depth, limit);
|
|
63
77
|
}
|
|
@@ -229,7 +243,7 @@ symlink is scanned like dust; descendant symlinks are not followed.`,
|
|
|
229
243
|
try {
|
|
230
244
|
rows = await runDust(dustCommand, baseDir, { depth, limit, signal: ctx?.signal });
|
|
231
245
|
} catch (error) {
|
|
232
|
-
if (isAbortError(error)) throw error;
|
|
246
|
+
if (isAbortError(error) || error instanceof SearchBackendLimitError) throw error;
|
|
233
247
|
rows = null;
|
|
234
248
|
}
|
|
235
249
|
}
|
package/yeaft/tools/glob.js
CHANGED
|
@@ -12,9 +12,11 @@ import { resolve, join, relative } from 'path';
|
|
|
12
12
|
import { managedCliToolReady, resolveManagedCliCommand } from '../managed-cli.js';
|
|
13
13
|
import { runProcess } from './process-runner.js';
|
|
14
14
|
import {
|
|
15
|
+
createFdPathRegex,
|
|
15
16
|
createSearchPathMatcher,
|
|
16
17
|
isAbortError,
|
|
17
18
|
isSkippedSearchDirectory,
|
|
19
|
+
SearchBackendLimitError,
|
|
18
20
|
SEARCH_SKIP_DIRS,
|
|
19
21
|
throwIfAborted,
|
|
20
22
|
waitForAbortable,
|
|
@@ -68,9 +70,16 @@ async function* walkDir(dir, baseDir, signal, maxDepth = 10, depth = 0) {
|
|
|
68
70
|
}
|
|
69
71
|
}
|
|
70
72
|
|
|
71
|
-
async function listFilesWithFd(
|
|
73
|
+
export async function listFilesWithFd(
|
|
74
|
+
fdCommand,
|
|
75
|
+
baseDir,
|
|
76
|
+
pattern,
|
|
77
|
+
signal,
|
|
78
|
+
processRunner = runProcess,
|
|
79
|
+
) {
|
|
72
80
|
const args = [
|
|
73
|
-
'
|
|
81
|
+
'--full-path',
|
|
82
|
+
'--case-sensitive',
|
|
74
83
|
'--type', 'file',
|
|
75
84
|
'--type', 'symlink',
|
|
76
85
|
'--hidden',
|
|
@@ -81,15 +90,18 @@ async function listFilesWithFd(fdCommand, baseDir, signal) {
|
|
|
81
90
|
];
|
|
82
91
|
for (const skipped of SEARCH_SKIP_DIRS) args.push('--exclude', skipped);
|
|
83
92
|
args.push('--exclude', '.yeaft/worktrees', '--exclude', '**/.yeaft/worktrees/**');
|
|
84
|
-
|
|
93
|
+
args.push('--', createFdPathRegex(pattern), '.');
|
|
94
|
+
const result = await processRunner(fdCommand, args, {
|
|
85
95
|
cwd: baseDir,
|
|
86
96
|
signal,
|
|
87
97
|
timeoutMs: 120_000,
|
|
88
98
|
maxBytes: 16 * 1024 * 1024,
|
|
89
99
|
preserveCarriageReturns: true,
|
|
90
100
|
});
|
|
91
|
-
if (result.timedOut) throw new
|
|
92
|
-
if (result.truncated)
|
|
101
|
+
if (result.timedOut) throw new SearchBackendLimitError('fd timed out');
|
|
102
|
+
if (result.truncated) {
|
|
103
|
+
throw new SearchBackendLimitError('fd output exceeded the tool limit');
|
|
104
|
+
}
|
|
93
105
|
if (result.code !== 0) {
|
|
94
106
|
throw new Error(result.stderr.trim() || `fd exited with code ${result.code}`);
|
|
95
107
|
}
|
|
@@ -176,10 +188,10 @@ Guidelines:
|
|
|
176
188
|
}
|
|
177
189
|
if (fdCommand) {
|
|
178
190
|
try {
|
|
179
|
-
paths = (await listFilesWithFd(fdCommand, baseDir, ctx?.signal))
|
|
191
|
+
paths = (await listFilesWithFd(fdCommand, baseDir, pattern, ctx?.signal))
|
|
180
192
|
.filter(path => matchGlob(pattern, path));
|
|
181
193
|
} catch (error) {
|
|
182
|
-
if (isAbortError(error)) throw error;
|
|
194
|
+
if (isAbortError(error) || error instanceof SearchBackendLimitError) throw error;
|
|
183
195
|
paths = null;
|
|
184
196
|
}
|
|
185
197
|
}
|
package/yeaft/tools/grep.js
CHANGED
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
SEARCH_SKIP_GLOBS,
|
|
19
19
|
isAbortError,
|
|
20
20
|
isSkippedSearchDirectory,
|
|
21
|
+
SearchBackendLimitError,
|
|
21
22
|
throwIfAborted,
|
|
22
23
|
waitForAbortable,
|
|
23
24
|
} from './search-paths.js';
|
|
@@ -348,7 +349,6 @@ export function runRipgrep(pattern, searchPath, options, spawnProcess = spawn, c
|
|
|
348
349
|
if (options.after) args.push('-A', String(options.after));
|
|
349
350
|
if (options.context || options.before || options.after) args.push('--no-context-separator');
|
|
350
351
|
if (options.multiline) args.push('-U', '--multiline-dotall');
|
|
351
|
-
args.push('--sort', 'path');
|
|
352
352
|
args.push('--', pattern);
|
|
353
353
|
if (searchTarget) args.push(searchTarget);
|
|
354
354
|
|
|
@@ -517,7 +517,12 @@ export function runRipgrep(pattern, searchPath, options, spawnProcess = spawn, c
|
|
|
517
517
|
});
|
|
518
518
|
}
|
|
519
519
|
|
|
520
|
-
async function listRipgrepCandidatePaths(
|
|
520
|
+
export async function listRipgrepCandidatePaths(
|
|
521
|
+
command,
|
|
522
|
+
searchPath,
|
|
523
|
+
options,
|
|
524
|
+
processRunner = runProcess,
|
|
525
|
+
) {
|
|
521
526
|
throwIfAborted(options.signal);
|
|
522
527
|
const searchStat = await lstat(searchPath);
|
|
523
528
|
const baseDir = searchStat.isDirectory() ? searchPath : dirname(searchPath);
|
|
@@ -532,8 +537,8 @@ async function listRipgrepCandidatePaths(command, searchPath, options) {
|
|
|
532
537
|
];
|
|
533
538
|
if (options.fixedStrings) args.push('-F');
|
|
534
539
|
for (const skipGlob of SEARCH_SKIP_GLOBS) args.push('--glob', skipGlob);
|
|
535
|
-
args.push('--
|
|
536
|
-
const result = await
|
|
540
|
+
args.push('--max-filesize', String(MAX_TEXT_FILE_BYTES), '--', options.pattern, target);
|
|
541
|
+
const result = await processRunner(command, args, {
|
|
537
542
|
cwd: baseDir,
|
|
538
543
|
env: createRipgrepEnv(),
|
|
539
544
|
signal: options.signal,
|
|
@@ -541,8 +546,10 @@ async function listRipgrepCandidatePaths(command, searchPath, options) {
|
|
|
541
546
|
maxBytes: MAX_CANDIDATE_BYTES,
|
|
542
547
|
preserveCarriageReturns: true,
|
|
543
548
|
});
|
|
544
|
-
if (result.timedOut) throw new
|
|
545
|
-
if (result.truncated)
|
|
549
|
+
if (result.timedOut) throw new SearchBackendLimitError('rg timed out');
|
|
550
|
+
if (result.truncated) {
|
|
551
|
+
throw new SearchBackendLimitError('rg candidate output exceeded the tool limit');
|
|
552
|
+
}
|
|
546
553
|
if (result.code !== 0 && result.code !== 1) {
|
|
547
554
|
throw new Error(result.stderr.trim() || `rg exited with code ${result.code}`);
|
|
548
555
|
}
|
|
@@ -761,7 +768,23 @@ export async function nodeGrep(pattern, searchPath, options) {
|
|
|
761
768
|
}
|
|
762
769
|
|
|
763
770
|
throwIfAborted(options.signal);
|
|
764
|
-
if (
|
|
771
|
+
if (candidatePaths) {
|
|
772
|
+
const orderedCandidates = [...candidatePaths]
|
|
773
|
+
.sort((left, right) => compareSearchPaths(
|
|
774
|
+
relative(searchBase, left).replace(/\\/g, '/'),
|
|
775
|
+
relative(searchBase, right).replace(/\\/g, '/'),
|
|
776
|
+
));
|
|
777
|
+
for (let index = 0; index < orderedCandidates.length && !stopped; index += FALLBACK_CONCURRENCY) {
|
|
778
|
+
throwIfAborted(options.signal);
|
|
779
|
+
const batches = await Promise.all(
|
|
780
|
+
orderedCandidates.slice(index, index + FALLBACK_CONCURRENCY).map(collectFileRecords),
|
|
781
|
+
);
|
|
782
|
+
for (const batch of batches) {
|
|
783
|
+
addFileRecords(batch);
|
|
784
|
+
if (stopped) break;
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
} else if (rootStat.isDirectory()) await searchDir(searchPath);
|
|
765
788
|
else addFileRecords(await collectFileRecords(searchPath));
|
|
766
789
|
const result = output.toString();
|
|
767
790
|
return options.structured
|
|
@@ -960,7 +983,7 @@ Guidelines:
|
|
|
960
983
|
});
|
|
961
984
|
result = await nodeGrep(pattern, absPath, { ...options, candidatePaths });
|
|
962
985
|
} catch (error) {
|
|
963
|
-
if (isAbortError(error)) throw error;
|
|
986
|
+
if (isAbortError(error) || error instanceof SearchBackendLimitError) throw error;
|
|
964
987
|
result = await nodeGrep(pattern, absPath, options);
|
|
965
988
|
}
|
|
966
989
|
} else {
|
|
@@ -34,7 +34,7 @@ function expandBraces(pattern) {
|
|
|
34
34
|
));
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
function
|
|
37
|
+
function globToRegExpSource(pattern, separator = '/') {
|
|
38
38
|
let source = '';
|
|
39
39
|
for (let index = 0; index < pattern.length; index += 1) {
|
|
40
40
|
const char = pattern[index];
|
|
@@ -42,19 +42,33 @@ function globToRegExp(pattern) {
|
|
|
42
42
|
index += 1;
|
|
43
43
|
if (pattern[index + 1] === '/') {
|
|
44
44
|
index += 1;
|
|
45
|
-
source +=
|
|
45
|
+
source += `(?:[\\s\\S]*${separator})?`;
|
|
46
46
|
} else {
|
|
47
|
-
source += '
|
|
47
|
+
source += '[\\s\\S]*';
|
|
48
48
|
}
|
|
49
49
|
} else if (char === '*') {
|
|
50
|
-
source += '[^/]*';
|
|
50
|
+
source += separator === '/' ? '[^/]*' : '[^\\\\/]*';
|
|
51
51
|
} else if (char === '?') {
|
|
52
|
-
source += '[^/]';
|
|
52
|
+
source += separator === '/' ? '[^/]' : '[^\\\\/]';
|
|
53
|
+
} else if (char === '/') {
|
|
54
|
+
source += separator;
|
|
53
55
|
} else {
|
|
54
56
|
source += char.replace(/[|\\{}()[\]^$+?.]/g, '\\$&');
|
|
55
57
|
}
|
|
56
58
|
}
|
|
57
|
-
return
|
|
59
|
+
return source;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function globToRegExp(pattern) {
|
|
63
|
+
return new RegExp(`^${globToRegExpSource(pattern)}$`);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Compile the public glob dialect for fd --full-path on POSIX and Windows. */
|
|
67
|
+
export function createFdPathRegex(glob) {
|
|
68
|
+
const normalizedGlob = String(glob || '').replace(/\\/g, '/');
|
|
69
|
+
const alternatives = expandBraces(normalizedGlob || '**/*')
|
|
70
|
+
.map(pattern => globToRegExpSource(pattern, '[\\\\/]'));
|
|
71
|
+
return `(?:^|[\\\\/])(?:${alternatives.join('|')})$`;
|
|
58
72
|
}
|
|
59
73
|
|
|
60
74
|
export function createSearchPathMatcher({ glob, type } = {}) {
|
|
@@ -106,3 +120,10 @@ export function waitForAbortable(promise, signal) {
|
|
|
106
120
|
);
|
|
107
121
|
});
|
|
108
122
|
}
|
|
123
|
+
|
|
124
|
+
export class SearchBackendLimitError extends Error {
|
|
125
|
+
constructor(message) {
|
|
126
|
+
super(message);
|
|
127
|
+
this.name = 'SearchBackendLimitError';
|
|
128
|
+
}
|
|
129
|
+
}
|
package/yeaft/web-bridge.js
CHANGED
|
@@ -86,7 +86,8 @@ import { listMcpServers, upsertMcpServer, removeMcpServer } from './config-api.j
|
|
|
86
86
|
import { buildMcpFlattenedTools } from './tools/mcp-tools.js';
|
|
87
87
|
import { getAgentRegistry, agentBelongsToScope } from './tools/agent.js';
|
|
88
88
|
import { enqueueSubAgentPrompt } from './sub-agent/prompt-queue.js';
|
|
89
|
-
import { isPromptableAgentStatus } from './sub-agent/status.js';
|
|
89
|
+
import { isPromptableAgentStatus, isTerminalAgentStatus, STATUS } from './sub-agent/status.js';
|
|
90
|
+
import { consumeNotificationForAgent } from './sub-agent/notifications.js';
|
|
90
91
|
import { perfNowMs, recordAgentPerfTrace } from './perf-trace.js';
|
|
91
92
|
import { recordAgentSessionCreated, recordAgentTurn } from '../metrics.js';
|
|
92
93
|
import { TASK_RESULT_DELIVERY, isTerminalTaskStatus, taskResultDeliveryFor } from './tasks/store.js';
|
|
@@ -6577,6 +6578,38 @@ export function handleYeaftTaskCancel(msg) {
|
|
|
6577
6578
|
return;
|
|
6578
6579
|
}
|
|
6579
6580
|
|
|
6581
|
+
const existingTask = session.taskManager.getTask?.(sessionId, taskId) || null;
|
|
6582
|
+
if (existingTask?.kind === 'sub_agent' && existingTask.status === 'running') {
|
|
6583
|
+
const subAgentId = existingTask.runtime?.subAgentId || '';
|
|
6584
|
+
const agent = subAgentId ? getAgentRegistry().get(subAgentId) : null;
|
|
6585
|
+
const scope = {
|
|
6586
|
+
sessionId,
|
|
6587
|
+
parentVpId: existingTask.ownerVpId || null,
|
|
6588
|
+
parentThreadId: existingTask.source?.threadId || 'main',
|
|
6589
|
+
};
|
|
6590
|
+
if (!agent || !agentBelongsToScope(agent, scope)) {
|
|
6591
|
+
fail('sub-agent not found', existingTask);
|
|
6592
|
+
return;
|
|
6593
|
+
}
|
|
6594
|
+
if (!isTerminalAgentStatus(agent.status)) {
|
|
6595
|
+
agent.status = STATUS.CLOSED;
|
|
6596
|
+
if (agent.abortController && !agent.abortController.signal.aborted) {
|
|
6597
|
+
try { agent.abortController.abort('stopped_by_user'); } catch { /* best effort */ }
|
|
6598
|
+
}
|
|
6599
|
+
}
|
|
6600
|
+
try { consumeNotificationForAgent(agent.id); } catch { /* best effort */ }
|
|
6601
|
+
const task = session.taskManager.completeTask(sessionId, taskId, { status: 'cancelled' });
|
|
6602
|
+
sendSessionEvent({
|
|
6603
|
+
type: 'yeaft_task_cancel_result',
|
|
6604
|
+
success: true,
|
|
6605
|
+
taskId,
|
|
6606
|
+
clientRequestId: clientRequestId || null,
|
|
6607
|
+
pending: false,
|
|
6608
|
+
task,
|
|
6609
|
+
}, { sessionId, vpId: task?.ownerVpId || null, threadId: task?.source?.threadId || null });
|
|
6610
|
+
return;
|
|
6611
|
+
}
|
|
6612
|
+
|
|
6580
6613
|
let result;
|
|
6581
6614
|
try {
|
|
6582
6615
|
result = session.taskManager.cancelTask(sessionId, taskId);
|