@agi-cli/server 0.1.81 → 0.1.83
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +3 -3
- package/src/index.ts +2 -2
- package/src/openapi/paths/git.ts +104 -0
- package/src/routes/config/agents.ts +44 -0
- package/src/routes/config/cwd.ts +21 -0
- package/src/routes/config/index.ts +14 -0
- package/src/routes/config/main.ts +68 -0
- package/src/routes/config/models.ts +109 -0
- package/src/routes/config/providers.ts +46 -0
- package/src/routes/config/utils.ts +107 -0
- package/src/routes/files.ts +60 -2
- package/src/routes/git/branch.ts +75 -0
- package/src/routes/git/commit.ts +159 -0
- package/src/routes/git/diff.ts +137 -0
- package/src/routes/git/index.ts +18 -0
- package/src/routes/git/push.ts +160 -0
- package/src/routes/git/schemas.ts +47 -0
- package/src/routes/git/staging.ts +208 -0
- package/src/routes/git/status.ts +76 -0
- package/src/routes/git/types.ts +19 -0
- package/src/routes/git/utils.ts +212 -0
- package/src/runtime/runner.ts +3 -6
- package/src/runtime/session-manager.ts +1 -1
- package/src/routes/config.ts +0 -387
- package/src/routes/git.ts +0 -980
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import type { Hono } from 'hono';
|
|
2
|
+
import { execFile } from 'node:child_process';
|
|
3
|
+
import { promisify } from 'node:util';
|
|
4
|
+
import {
|
|
5
|
+
gitStageSchema,
|
|
6
|
+
gitUnstageSchema,
|
|
7
|
+
gitRestoreSchema,
|
|
8
|
+
gitDeleteSchema,
|
|
9
|
+
} from './schemas.ts';
|
|
10
|
+
import { validateAndGetGitRoot } from './utils.ts';
|
|
11
|
+
|
|
12
|
+
const execFileAsync = promisify(execFile);
|
|
13
|
+
|
|
14
|
+
export function registerStagingRoutes(app: Hono) {
|
|
15
|
+
app.post('/v1/git/stage', async (c) => {
|
|
16
|
+
try {
|
|
17
|
+
const body = await c.req.json();
|
|
18
|
+
const { files, project } = gitStageSchema.parse(body);
|
|
19
|
+
|
|
20
|
+
const requestedPath = project || process.cwd();
|
|
21
|
+
|
|
22
|
+
const validation = await validateAndGetGitRoot(requestedPath);
|
|
23
|
+
if ('error' in validation) {
|
|
24
|
+
return c.json(
|
|
25
|
+
{ status: 'error', error: validation.error, code: validation.code },
|
|
26
|
+
400,
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const { gitRoot } = validation;
|
|
31
|
+
|
|
32
|
+
if (files.length === 0) {
|
|
33
|
+
return c.json(
|
|
34
|
+
{
|
|
35
|
+
status: 'error',
|
|
36
|
+
error: 'No files specified',
|
|
37
|
+
},
|
|
38
|
+
400,
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
await execFileAsync('git', ['add', ...files], { cwd: gitRoot });
|
|
43
|
+
|
|
44
|
+
return c.json({
|
|
45
|
+
status: 'ok',
|
|
46
|
+
data: {
|
|
47
|
+
staged: files,
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
} catch (error) {
|
|
51
|
+
return c.json(
|
|
52
|
+
{
|
|
53
|
+
status: 'error',
|
|
54
|
+
error:
|
|
55
|
+
error instanceof Error ? error.message : 'Failed to stage files',
|
|
56
|
+
},
|
|
57
|
+
500,
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
app.post('/v1/git/unstage', async (c) => {
|
|
63
|
+
try {
|
|
64
|
+
const body = await c.req.json();
|
|
65
|
+
const { files, project } = gitUnstageSchema.parse(body);
|
|
66
|
+
|
|
67
|
+
const requestedPath = project || process.cwd();
|
|
68
|
+
|
|
69
|
+
const validation = await validateAndGetGitRoot(requestedPath);
|
|
70
|
+
if ('error' in validation) {
|
|
71
|
+
return c.json(
|
|
72
|
+
{ status: 'error', error: validation.error, code: validation.code },
|
|
73
|
+
400,
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const { gitRoot } = validation;
|
|
78
|
+
|
|
79
|
+
if (files.length === 0) {
|
|
80
|
+
return c.json(
|
|
81
|
+
{
|
|
82
|
+
status: 'error',
|
|
83
|
+
error: 'No files specified',
|
|
84
|
+
},
|
|
85
|
+
400,
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
await execFileAsync('git', ['reset', 'HEAD', '--', ...files], {
|
|
90
|
+
cwd: gitRoot,
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
return c.json({
|
|
94
|
+
status: 'ok',
|
|
95
|
+
data: {
|
|
96
|
+
unstaged: files,
|
|
97
|
+
},
|
|
98
|
+
});
|
|
99
|
+
} catch (error) {
|
|
100
|
+
return c.json(
|
|
101
|
+
{
|
|
102
|
+
status: 'error',
|
|
103
|
+
error:
|
|
104
|
+
error instanceof Error ? error.message : 'Failed to unstage files',
|
|
105
|
+
},
|
|
106
|
+
500,
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
app.post('/v1/git/restore', async (c) => {
|
|
112
|
+
try {
|
|
113
|
+
const body = await c.req.json();
|
|
114
|
+
const { files, project } = gitRestoreSchema.parse(body);
|
|
115
|
+
|
|
116
|
+
const requestedPath = project || process.cwd();
|
|
117
|
+
|
|
118
|
+
const validation = await validateAndGetGitRoot(requestedPath);
|
|
119
|
+
if ('error' in validation) {
|
|
120
|
+
return c.json(
|
|
121
|
+
{ status: 'error', error: validation.error, code: validation.code },
|
|
122
|
+
400,
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const { gitRoot } = validation;
|
|
127
|
+
|
|
128
|
+
if (files.length === 0) {
|
|
129
|
+
return c.json(
|
|
130
|
+
{
|
|
131
|
+
status: 'error',
|
|
132
|
+
error: 'No files specified',
|
|
133
|
+
},
|
|
134
|
+
400,
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
await execFileAsync('git', ['restore', '--', ...files], {
|
|
139
|
+
cwd: gitRoot,
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
return c.json({
|
|
143
|
+
status: 'ok',
|
|
144
|
+
data: {
|
|
145
|
+
restored: files,
|
|
146
|
+
},
|
|
147
|
+
});
|
|
148
|
+
} catch (error) {
|
|
149
|
+
return c.json(
|
|
150
|
+
{
|
|
151
|
+
status: 'error',
|
|
152
|
+
error:
|
|
153
|
+
error instanceof Error ? error.message : 'Failed to restore files',
|
|
154
|
+
},
|
|
155
|
+
500,
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
app.post('/v1/git/delete', async (c) => {
|
|
161
|
+
try {
|
|
162
|
+
const body = await c.req.json();
|
|
163
|
+
const { files, project } = gitDeleteSchema.parse(body);
|
|
164
|
+
|
|
165
|
+
const requestedPath = project || process.cwd();
|
|
166
|
+
|
|
167
|
+
const validation = await validateAndGetGitRoot(requestedPath);
|
|
168
|
+
if ('error' in validation) {
|
|
169
|
+
return c.json(
|
|
170
|
+
{ status: 'error', error: validation.error, code: validation.code },
|
|
171
|
+
400,
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const { gitRoot } = validation;
|
|
176
|
+
|
|
177
|
+
if (files.length === 0) {
|
|
178
|
+
return c.json(
|
|
179
|
+
{
|
|
180
|
+
status: 'error',
|
|
181
|
+
error: 'No files specified',
|
|
182
|
+
},
|
|
183
|
+
400,
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
await execFileAsync('git', ['clean', '-f', '--', ...files], {
|
|
188
|
+
cwd: gitRoot,
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
return c.json({
|
|
192
|
+
status: 'ok',
|
|
193
|
+
data: {
|
|
194
|
+
deleted: files,
|
|
195
|
+
},
|
|
196
|
+
});
|
|
197
|
+
} catch (error) {
|
|
198
|
+
return c.json(
|
|
199
|
+
{
|
|
200
|
+
status: 'error',
|
|
201
|
+
error:
|
|
202
|
+
error instanceof Error ? error.message : 'Failed to delete files',
|
|
203
|
+
},
|
|
204
|
+
500,
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import type { Hono } from 'hono';
|
|
2
|
+
import { execFile } from 'node:child_process';
|
|
3
|
+
import { promisify } from 'node:util';
|
|
4
|
+
import { gitStatusSchema } from './schemas.ts';
|
|
5
|
+
import {
|
|
6
|
+
validateAndGetGitRoot,
|
|
7
|
+
parseGitStatus,
|
|
8
|
+
getAheadBehind,
|
|
9
|
+
getCurrentBranch,
|
|
10
|
+
} from './utils.ts';
|
|
11
|
+
|
|
12
|
+
const execFileAsync = promisify(execFile);
|
|
13
|
+
|
|
14
|
+
export function registerStatusRoute(app: Hono) {
|
|
15
|
+
app.get('/v1/git/status', async (c) => {
|
|
16
|
+
try {
|
|
17
|
+
const query = gitStatusSchema.parse({
|
|
18
|
+
project: c.req.query('project'),
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
const requestedPath = query.project || process.cwd();
|
|
22
|
+
|
|
23
|
+
const validation = await validateAndGetGitRoot(requestedPath);
|
|
24
|
+
if ('error' in validation) {
|
|
25
|
+
return c.json(
|
|
26
|
+
{ status: 'error', error: validation.error, code: validation.code },
|
|
27
|
+
400,
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const { gitRoot } = validation;
|
|
32
|
+
|
|
33
|
+
const { stdout: statusOutput } = await execFileAsync(
|
|
34
|
+
'git',
|
|
35
|
+
['status', '--porcelain=v2'],
|
|
36
|
+
{ cwd: gitRoot },
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
const { staged, unstaged, untracked } = parseGitStatus(
|
|
40
|
+
statusOutput,
|
|
41
|
+
gitRoot,
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
const { ahead, behind } = await getAheadBehind(gitRoot);
|
|
45
|
+
|
|
46
|
+
const branch = await getCurrentBranch(gitRoot);
|
|
47
|
+
|
|
48
|
+
const hasChanges =
|
|
49
|
+
staged.length > 0 || unstaged.length > 0 || untracked.length > 0;
|
|
50
|
+
|
|
51
|
+
return c.json({
|
|
52
|
+
status: 'ok',
|
|
53
|
+
data: {
|
|
54
|
+
branch,
|
|
55
|
+
ahead,
|
|
56
|
+
behind,
|
|
57
|
+
gitRoot,
|
|
58
|
+
workingDir: requestedPath,
|
|
59
|
+
staged,
|
|
60
|
+
unstaged,
|
|
61
|
+
untracked,
|
|
62
|
+
hasChanges,
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
} catch (error) {
|
|
66
|
+
return c.json(
|
|
67
|
+
{
|
|
68
|
+
status: 'error',
|
|
69
|
+
error:
|
|
70
|
+
error instanceof Error ? error.message : 'Failed to get status',
|
|
71
|
+
},
|
|
72
|
+
500,
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export interface GitFile {
|
|
2
|
+
path: string;
|
|
3
|
+
absPath: string;
|
|
4
|
+
status: 'modified' | 'added' | 'deleted' | 'renamed' | 'untracked';
|
|
5
|
+
staged: boolean;
|
|
6
|
+
insertions?: number;
|
|
7
|
+
deletions?: number;
|
|
8
|
+
oldPath?: string;
|
|
9
|
+
isNew: boolean;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface GitRoot {
|
|
13
|
+
gitRoot: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface GitError {
|
|
17
|
+
error: string;
|
|
18
|
+
code?: string;
|
|
19
|
+
}
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import { extname, join } from 'node:path';
|
|
3
|
+
import { promisify } from 'node:util';
|
|
4
|
+
import type { GitFile, GitRoot, GitError } from './types.ts';
|
|
5
|
+
|
|
6
|
+
const execFileAsync = promisify(execFile);
|
|
7
|
+
|
|
8
|
+
const LANGUAGE_MAP: Record<string, string> = {
|
|
9
|
+
js: 'javascript',
|
|
10
|
+
jsx: 'jsx',
|
|
11
|
+
ts: 'typescript',
|
|
12
|
+
tsx: 'tsx',
|
|
13
|
+
py: 'python',
|
|
14
|
+
rb: 'ruby',
|
|
15
|
+
go: 'go',
|
|
16
|
+
rs: 'rust',
|
|
17
|
+
java: 'java',
|
|
18
|
+
c: 'c',
|
|
19
|
+
cpp: 'cpp',
|
|
20
|
+
h: 'c',
|
|
21
|
+
hpp: 'cpp',
|
|
22
|
+
cs: 'csharp',
|
|
23
|
+
php: 'php',
|
|
24
|
+
sh: 'bash',
|
|
25
|
+
bash: 'bash',
|
|
26
|
+
zsh: 'bash',
|
|
27
|
+
sql: 'sql',
|
|
28
|
+
json: 'json',
|
|
29
|
+
yaml: 'yaml',
|
|
30
|
+
yml: 'yaml',
|
|
31
|
+
xml: 'xml',
|
|
32
|
+
html: 'html',
|
|
33
|
+
css: 'css',
|
|
34
|
+
scss: 'scss',
|
|
35
|
+
md: 'markdown',
|
|
36
|
+
txt: 'plaintext',
|
|
37
|
+
svelte: 'svelte',
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export function inferLanguage(filePath: string): string {
|
|
41
|
+
const extension = extname(filePath).toLowerCase().replace('.', '');
|
|
42
|
+
if (!extension) {
|
|
43
|
+
return 'plaintext';
|
|
44
|
+
}
|
|
45
|
+
return LANGUAGE_MAP[extension] ?? 'plaintext';
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function summarizeDiff(diff: string): {
|
|
49
|
+
insertions: number;
|
|
50
|
+
deletions: number;
|
|
51
|
+
binary: boolean;
|
|
52
|
+
} {
|
|
53
|
+
let insertions = 0;
|
|
54
|
+
let deletions = 0;
|
|
55
|
+
let binary = false;
|
|
56
|
+
|
|
57
|
+
for (const line of diff.split('\n')) {
|
|
58
|
+
if (line.startsWith('Binary files ') || line.includes('GIT binary patch')) {
|
|
59
|
+
binary = true;
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (line.startsWith('+') && !line.startsWith('+++')) {
|
|
64
|
+
insertions++;
|
|
65
|
+
} else if (line.startsWith('-') && !line.startsWith('---')) {
|
|
66
|
+
deletions++;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return { insertions, deletions, binary };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export async function validateAndGetGitRoot(
|
|
74
|
+
requestedPath: string,
|
|
75
|
+
): Promise<GitRoot | GitError> {
|
|
76
|
+
try {
|
|
77
|
+
const { stdout: gitRoot } = await execFileAsync(
|
|
78
|
+
'git',
|
|
79
|
+
['rev-parse', '--show-toplevel'],
|
|
80
|
+
{
|
|
81
|
+
cwd: requestedPath,
|
|
82
|
+
},
|
|
83
|
+
);
|
|
84
|
+
return { gitRoot: gitRoot.trim() };
|
|
85
|
+
} catch {
|
|
86
|
+
return {
|
|
87
|
+
error: 'Not a git repository',
|
|
88
|
+
code: 'NOT_A_GIT_REPO',
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export async function checkIfNewFile(
|
|
94
|
+
gitRoot: string,
|
|
95
|
+
file: string,
|
|
96
|
+
): Promise<boolean> {
|
|
97
|
+
try {
|
|
98
|
+
await execFileAsync('git', ['ls-files', '--error-unmatch', file], {
|
|
99
|
+
cwd: gitRoot,
|
|
100
|
+
});
|
|
101
|
+
return false;
|
|
102
|
+
} catch {
|
|
103
|
+
return true;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function getStatusFromCodeV2(code: string): GitFile['status'] {
|
|
108
|
+
switch (code) {
|
|
109
|
+
case 'M':
|
|
110
|
+
return 'modified';
|
|
111
|
+
case 'A':
|
|
112
|
+
return 'added';
|
|
113
|
+
case 'D':
|
|
114
|
+
return 'deleted';
|
|
115
|
+
case 'R':
|
|
116
|
+
return 'renamed';
|
|
117
|
+
case 'C':
|
|
118
|
+
return 'modified';
|
|
119
|
+
default:
|
|
120
|
+
return 'modified';
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function parseGitStatus(
|
|
125
|
+
statusOutput: string,
|
|
126
|
+
gitRoot: string,
|
|
127
|
+
): {
|
|
128
|
+
staged: GitFile[];
|
|
129
|
+
unstaged: GitFile[];
|
|
130
|
+
untracked: GitFile[];
|
|
131
|
+
} {
|
|
132
|
+
const lines = statusOutput.trim().split('\n').filter(Boolean);
|
|
133
|
+
const staged: GitFile[] = [];
|
|
134
|
+
const unstaged: GitFile[] = [];
|
|
135
|
+
const untracked: GitFile[] = [];
|
|
136
|
+
|
|
137
|
+
for (const line of lines) {
|
|
138
|
+
if (line.startsWith('1 ') || line.startsWith('2 ')) {
|
|
139
|
+
const parts = line.split(' ');
|
|
140
|
+
if (parts.length < 9) continue;
|
|
141
|
+
|
|
142
|
+
const xy = parts[1];
|
|
143
|
+
const x = xy[0];
|
|
144
|
+
const y = xy[1];
|
|
145
|
+
const path = parts.slice(8).join(' ');
|
|
146
|
+
const absPath = join(gitRoot, path);
|
|
147
|
+
|
|
148
|
+
if (x !== '.') {
|
|
149
|
+
staged.push({
|
|
150
|
+
path,
|
|
151
|
+
absPath,
|
|
152
|
+
status: getStatusFromCodeV2(x),
|
|
153
|
+
staged: true,
|
|
154
|
+
isNew: x === 'A',
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (y !== '.') {
|
|
159
|
+
unstaged.push({
|
|
160
|
+
path,
|
|
161
|
+
absPath,
|
|
162
|
+
status: getStatusFromCodeV2(y),
|
|
163
|
+
staged: false,
|
|
164
|
+
isNew: false,
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
} else if (line.startsWith('? ')) {
|
|
168
|
+
const path = line.slice(2);
|
|
169
|
+
const absPath = join(gitRoot, path);
|
|
170
|
+
untracked.push({
|
|
171
|
+
path,
|
|
172
|
+
absPath,
|
|
173
|
+
status: 'untracked',
|
|
174
|
+
staged: false,
|
|
175
|
+
isNew: true,
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return { staged, unstaged, untracked };
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export async function getAheadBehind(
|
|
184
|
+
gitRoot: string,
|
|
185
|
+
): Promise<{ ahead: number; behind: number }> {
|
|
186
|
+
try {
|
|
187
|
+
const { stdout } = await execFileAsync(
|
|
188
|
+
'git',
|
|
189
|
+
['rev-list', '--left-right', '--count', 'HEAD...@{upstream}'],
|
|
190
|
+
{ cwd: gitRoot },
|
|
191
|
+
);
|
|
192
|
+
const [ahead, behind] = stdout.trim().split(/\s+/).map(Number);
|
|
193
|
+
return { ahead: ahead || 0, behind: behind || 0 };
|
|
194
|
+
} catch {
|
|
195
|
+
return { ahead: 0, behind: 0 };
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export async function getCurrentBranch(gitRoot: string): Promise<string> {
|
|
200
|
+
try {
|
|
201
|
+
const { stdout } = await execFileAsync(
|
|
202
|
+
'git',
|
|
203
|
+
['branch', '--show-current'],
|
|
204
|
+
{
|
|
205
|
+
cwd: gitRoot,
|
|
206
|
+
},
|
|
207
|
+
);
|
|
208
|
+
return stdout.trim();
|
|
209
|
+
} catch {
|
|
210
|
+
return 'unknown';
|
|
211
|
+
}
|
|
212
|
+
}
|
package/src/runtime/runner.ts
CHANGED
|
@@ -22,10 +22,7 @@ import {
|
|
|
22
22
|
dequeueJob,
|
|
23
23
|
cleanupSession,
|
|
24
24
|
} from './session-queue.ts';
|
|
25
|
-
import {
|
|
26
|
-
setupToolContext,
|
|
27
|
-
type RunnerToolContext,
|
|
28
|
-
} from './tool-context-setup.ts';
|
|
25
|
+
import { setupToolContext } from './tool-context-setup.ts';
|
|
29
26
|
import {
|
|
30
27
|
updateSessionTokensIncremental,
|
|
31
28
|
updateMessageTokensIncremental,
|
|
@@ -218,12 +215,12 @@ async function runAssistant(opts: RunOpts) {
|
|
|
218
215
|
let accumulated = '';
|
|
219
216
|
let stepIndex = 0;
|
|
220
217
|
|
|
221
|
-
let
|
|
218
|
+
let _finishObserved = false;
|
|
222
219
|
const unsubscribeFinish = subscribe(opts.sessionId, (evt) => {
|
|
223
220
|
if (evt.type !== 'tool.result') return;
|
|
224
221
|
try {
|
|
225
222
|
const name = (evt.payload as { name?: string } | undefined)?.name;
|
|
226
|
-
if (name === 'finish')
|
|
223
|
+
if (name === 'finish') _finishObserved = true;
|
|
227
224
|
} catch {}
|
|
228
225
|
});
|
|
229
226
|
|