@yeaft/webchat-agent 1.0.414 → 1.0.415
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/connection/index.js +12 -0
- package/index.js +1 -1
- package/local-runtime/server/client-protocol.js +14 -0
- package/local-runtime/server/context.js +3 -2
- package/local-runtime/server/handlers/agent-file-terminal.js +185 -115
- package/local-runtime/server/handlers/agent-output.js +3 -0
- package/local-runtime/server/handlers/client-misc.js +21 -4
- package/local-runtime/server/handlers/client-workbench.js +222 -41
- package/local-runtime/server/workbench-correlation.js +184 -0
- package/local-runtime/server/workbench-route.js +180 -0
- package/local-runtime/server/ws-agent.js +4 -0
- package/local-runtime/server/ws-client.js +25 -3
- package/local-runtime/version.json +1 -1
- package/local-runtime/web/app.bundle.js +191 -135
- 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/terminal.js +167 -30
- package/workbench/file-ops.js +21 -20
- package/workbench/file-search.js +4 -3
- package/workbench/git-ops.js +23 -22
- package/workbench/request-routing.js +16 -0
- package/yeaft/cli.js +57 -1
- package/yeaft/sessions/session-manifest.js +114 -10
- package/yeaft/stdio-protocol.js +57 -0
|
Binary file
|
package/package.json
CHANGED
package/terminal.js
CHANGED
|
@@ -62,28 +62,122 @@ function terminalRoutingFields(source) {
|
|
|
62
62
|
return {
|
|
63
63
|
...(source?._requestUserId ? { _requestUserId: source._requestUserId } : {}),
|
|
64
64
|
...(source?._requestClientId ? { _requestClientId: source._requestClientId } : {}),
|
|
65
|
+
...(source?._workbenchRequestId ? { _workbenchRequestId: source._workbenchRequestId } : {}),
|
|
66
|
+
...(source?.workbenchRouteKey ? { workbenchRouteKey: source.workbenchRouteKey } : {}),
|
|
67
|
+
...(source?.workbenchWorkspaceGeneration
|
|
68
|
+
? { workbenchWorkspaceGeneration: source.workbenchWorkspaceGeneration }
|
|
69
|
+
: {}),
|
|
65
70
|
};
|
|
66
71
|
}
|
|
67
72
|
|
|
73
|
+
function terminalOwner(source) {
|
|
74
|
+
return {
|
|
75
|
+
conversationId: source?.conversationId || '',
|
|
76
|
+
workbenchRouteKey: source?.workbenchRouteKey || '',
|
|
77
|
+
workbenchWorkspaceGeneration: source?.workbenchWorkspaceGeneration || '',
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function terminalOwnerMatches(term, msg) {
|
|
82
|
+
if (!term || !msg) return false;
|
|
83
|
+
const owner = terminalOwner(term);
|
|
84
|
+
const request = terminalOwner(msg);
|
|
85
|
+
if (owner.workbenchRouteKey) {
|
|
86
|
+
return request.workbenchRouteKey === owner.workbenchRouteKey
|
|
87
|
+
&& request.workbenchWorkspaceGeneration === owner.workbenchWorkspaceGeneration
|
|
88
|
+
&& request.conversationId === owner.conversationId;
|
|
89
|
+
}
|
|
90
|
+
return !request.workbenchRouteKey
|
|
91
|
+
&& !request.workbenchWorkspaceGeneration
|
|
92
|
+
&& request.conversationId === owner.conversationId;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function rejectTerminalOwnerMismatch(msg, terminalId) {
|
|
96
|
+
ctx.sendToServer({
|
|
97
|
+
type: 'terminal_error',
|
|
98
|
+
conversationId: msg?.conversationId,
|
|
99
|
+
terminalId,
|
|
100
|
+
message: 'Terminal owner mismatch',
|
|
101
|
+
...terminalRoutingFields(msg),
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Fail closed when the Agent transport loses its Server-side owner state.
|
|
107
|
+
* Pending creates are cancelled before their backend resolves; established
|
|
108
|
+
* PTYs are removed from the owner map before kill so synchronous or delayed
|
|
109
|
+
* exit callbacks cannot affect a replacement terminal with the same id.
|
|
110
|
+
* No wire event is emitted because the transport is unavailable.
|
|
111
|
+
*/
|
|
112
|
+
export function cleanupTerminalsForDisconnect() {
|
|
113
|
+
const terminals = [...ctx.terminals.entries()];
|
|
114
|
+
if (terminals.length === 0) return 0;
|
|
115
|
+
|
|
116
|
+
for (const [, term] of terminals) {
|
|
117
|
+
term.cancelled = true;
|
|
118
|
+
if (term.timer) {
|
|
119
|
+
clearTimeout(term.timer);
|
|
120
|
+
term.timer = null;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
ctx.terminals.clear();
|
|
124
|
+
|
|
125
|
+
for (const [, term] of terminals) {
|
|
126
|
+
if (!term.pty) continue;
|
|
127
|
+
try { term.pty.kill(); } catch {}
|
|
128
|
+
}
|
|
129
|
+
return terminals.length;
|
|
130
|
+
}
|
|
131
|
+
|
|
68
132
|
export async function handleTerminalCreate(msg) {
|
|
69
133
|
const { conversationId, cols, rows } = msg;
|
|
70
134
|
const terminalId = msg.terminalId || conversationId;
|
|
71
135
|
const conv = ctx.conversations.get(conversationId);
|
|
72
|
-
const
|
|
136
|
+
const routeScoped = !!msg.workbenchRouteKey;
|
|
137
|
+
if (routeScoped && !msg.workbenchWorkspaceGeneration) {
|
|
138
|
+
rejectTerminalOwnerMismatch(msg, terminalId);
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
const workDir = routeScoped
|
|
142
|
+
? (msg.workDir || ctx.CONFIG.workDir)
|
|
143
|
+
: (conv?.workDir || ctx.CONFIG.workDir);
|
|
73
144
|
const routingFields = terminalRoutingFields(msg);
|
|
74
145
|
|
|
75
|
-
//
|
|
146
|
+
// A terminal id is an object capability. It may only be replaced by its
|
|
147
|
+
// immutable owner, never by another Session that guessed the id.
|
|
76
148
|
if (ctx.terminals.has(terminalId)) {
|
|
77
149
|
const existing = ctx.terminals.get(terminalId);
|
|
150
|
+
if (!terminalOwnerMatches(existing, msg)) {
|
|
151
|
+
rejectTerminalOwnerMismatch(msg, terminalId);
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
existing.cancelled = true;
|
|
155
|
+
if (existing.timer) {
|
|
156
|
+
clearTimeout(existing.timer);
|
|
157
|
+
existing.timer = null;
|
|
158
|
+
}
|
|
159
|
+
ctx.terminals.delete(terminalId);
|
|
78
160
|
if (existing.pty) {
|
|
79
161
|
try { existing.pty.kill(); } catch {}
|
|
80
162
|
}
|
|
81
|
-
if (existing.timer) clearTimeout(existing.timer);
|
|
82
|
-
ctx.terminals.delete(terminalId);
|
|
83
163
|
}
|
|
84
164
|
|
|
165
|
+
const pendingCreation = {
|
|
166
|
+
pty: null,
|
|
167
|
+
pending: true,
|
|
168
|
+
cancelled: false,
|
|
169
|
+
conversationId,
|
|
170
|
+
cols: cols || 80,
|
|
171
|
+
rows: rows || 24,
|
|
172
|
+
...terminalOwner(msg),
|
|
173
|
+
...routingFields,
|
|
174
|
+
};
|
|
175
|
+
ctx.terminals.set(terminalId, pendingCreation);
|
|
176
|
+
|
|
85
177
|
const pty = await loadNodePty();
|
|
178
|
+
if (ctx.terminals.get(terminalId) !== pendingCreation || pendingCreation.cancelled) return;
|
|
86
179
|
if (!pty) {
|
|
180
|
+
ctx.terminals.delete(terminalId);
|
|
87
181
|
ctx.sendToServer({
|
|
88
182
|
type: 'terminal_error',
|
|
89
183
|
conversationId,
|
|
@@ -120,40 +214,65 @@ export async function handleTerminalCreate(msg) {
|
|
|
120
214
|
env: terminalEnv
|
|
121
215
|
});
|
|
122
216
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
217
|
+
if (ctx.terminals.get(terminalId) !== pendingCreation || pendingCreation.cancelled) {
|
|
218
|
+
try { ptyProcess.kill(); } catch {}
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const terminalRecord = {
|
|
223
|
+
pty: ptyProcess,
|
|
224
|
+
cancelled: false,
|
|
225
|
+
conversationId,
|
|
226
|
+
cols: cols || 80,
|
|
227
|
+
rows: rows || 24,
|
|
228
|
+
buffer: '',
|
|
229
|
+
timer: null,
|
|
230
|
+
...terminalOwner(msg),
|
|
231
|
+
...routingFields,
|
|
232
|
+
};
|
|
233
|
+
ctx.terminals.set(terminalId, terminalRecord);
|
|
126
234
|
|
|
235
|
+
// Output callbacks are fenced to this exact record. A disconnect or
|
|
236
|
+
// same-id replacement removes it before kill, so stale data/exit events
|
|
237
|
+
// cannot leak or delete the replacement.
|
|
127
238
|
ptyProcess.onData(data => {
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
239
|
+
if (ctx.terminals.get(terminalId) !== terminalRecord || terminalRecord.cancelled) return;
|
|
240
|
+
terminalRecord.buffer += data;
|
|
241
|
+
if (!terminalRecord.timer) {
|
|
242
|
+
terminalRecord.timer = setTimeout(() => {
|
|
243
|
+
terminalRecord.timer = null;
|
|
244
|
+
if (ctx.terminals.get(terminalId) !== terminalRecord || terminalRecord.cancelled) {
|
|
245
|
+
terminalRecord.buffer = '';
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
131
248
|
ctx.sendToServer({
|
|
132
249
|
type: 'terminal_output',
|
|
133
250
|
conversationId,
|
|
134
251
|
terminalId,
|
|
135
|
-
data: buffer,
|
|
252
|
+
data: terminalRecord.buffer,
|
|
136
253
|
...routingFields,
|
|
137
254
|
});
|
|
138
|
-
buffer = '';
|
|
139
|
-
timer = null;
|
|
255
|
+
terminalRecord.buffer = '';
|
|
140
256
|
}, 16);
|
|
141
257
|
}
|
|
142
258
|
});
|
|
143
259
|
|
|
144
260
|
ptyProcess.onExit(({ exitCode }) => {
|
|
145
|
-
|
|
146
|
-
if (buffer) {
|
|
261
|
+
if (ctx.terminals.get(terminalId) !== terminalRecord || terminalRecord.cancelled) return;
|
|
262
|
+
if (terminalRecord.buffer) {
|
|
147
263
|
ctx.sendToServer({
|
|
148
264
|
type: 'terminal_output',
|
|
149
265
|
conversationId,
|
|
150
266
|
terminalId,
|
|
151
|
-
data: buffer,
|
|
267
|
+
data: terminalRecord.buffer,
|
|
152
268
|
...routingFields,
|
|
153
269
|
});
|
|
154
|
-
buffer = '';
|
|
270
|
+
terminalRecord.buffer = '';
|
|
271
|
+
}
|
|
272
|
+
if (terminalRecord.timer) {
|
|
273
|
+
clearTimeout(terminalRecord.timer);
|
|
274
|
+
terminalRecord.timer = null;
|
|
155
275
|
}
|
|
156
|
-
if (timer) clearTimeout(timer);
|
|
157
276
|
|
|
158
277
|
console.log(`[PTY] Process exited for ${terminalId}, code: ${exitCode}`);
|
|
159
278
|
ctx.terminals.delete(terminalId);
|
|
@@ -165,16 +284,6 @@ export async function handleTerminalCreate(msg) {
|
|
|
165
284
|
});
|
|
166
285
|
});
|
|
167
286
|
|
|
168
|
-
ctx.terminals.set(terminalId, {
|
|
169
|
-
pty: ptyProcess,
|
|
170
|
-
conversationId,
|
|
171
|
-
cols: cols || 80,
|
|
172
|
-
rows: rows || 24,
|
|
173
|
-
buffer: '',
|
|
174
|
-
timer: null,
|
|
175
|
-
...routingFields,
|
|
176
|
-
});
|
|
177
|
-
|
|
178
287
|
console.log(`[PTY] Created terminal ${terminalId} for ${conversationId} in ${workDir}`);
|
|
179
288
|
ctx.sendToServer({
|
|
180
289
|
type: 'terminal_created',
|
|
@@ -184,6 +293,7 @@ export async function handleTerminalCreate(msg) {
|
|
|
184
293
|
...routingFields,
|
|
185
294
|
});
|
|
186
295
|
} catch (e) {
|
|
296
|
+
if (ctx.terminals.get(terminalId) === pendingCreation) ctx.terminals.delete(terminalId);
|
|
187
297
|
console.error(`[PTY] Failed to create terminal:`, e.message);
|
|
188
298
|
ctx.sendToServer({
|
|
189
299
|
type: 'terminal_error',
|
|
@@ -199,6 +309,10 @@ export function handleTerminalInput(msg) {
|
|
|
199
309
|
const terminalId = msg.terminalId || msg.conversationId;
|
|
200
310
|
const term = ctx.terminals.get(terminalId);
|
|
201
311
|
if (term?.pty) {
|
|
312
|
+
if (!terminalOwnerMatches(term, msg)) {
|
|
313
|
+
rejectTerminalOwnerMismatch(msg, terminalId);
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
202
316
|
try {
|
|
203
317
|
term.pty.write(msg.data);
|
|
204
318
|
} catch (e) {
|
|
@@ -212,6 +326,10 @@ export function handleTerminalResize(msg) {
|
|
|
212
326
|
const { cols, rows } = msg;
|
|
213
327
|
const term = ctx.terminals.get(terminalId);
|
|
214
328
|
if (term?.pty && cols > 0 && rows > 0) {
|
|
329
|
+
if (!terminalOwnerMatches(term, msg)) {
|
|
330
|
+
rejectTerminalOwnerMismatch(msg, terminalId);
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
215
333
|
try {
|
|
216
334
|
term.pty.resize(cols, rows);
|
|
217
335
|
term.cols = cols;
|
|
@@ -226,11 +344,30 @@ export function handleTerminalClose(msg) {
|
|
|
226
344
|
const terminalId = msg.terminalId || msg.conversationId;
|
|
227
345
|
const term = ctx.terminals.get(terminalId);
|
|
228
346
|
if (term) {
|
|
347
|
+
if (!terminalOwnerMatches(term, msg)) {
|
|
348
|
+
rejectTerminalOwnerMismatch(msg, terminalId);
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
if (term.pending && !term.pty) {
|
|
352
|
+
term.cancelled = true;
|
|
353
|
+
ctx.terminals.delete(terminalId);
|
|
354
|
+
ctx.sendToServer({
|
|
355
|
+
type: 'terminal_closed',
|
|
356
|
+
conversationId: term.conversationId || msg.conversationId,
|
|
357
|
+
terminalId,
|
|
358
|
+
...terminalRoutingFields(term),
|
|
359
|
+
});
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
term.cancelled = true;
|
|
363
|
+
if (term.timer) {
|
|
364
|
+
clearTimeout(term.timer);
|
|
365
|
+
term.timer = null;
|
|
366
|
+
}
|
|
367
|
+
ctx.terminals.delete(terminalId);
|
|
229
368
|
if (term.pty) {
|
|
230
369
|
try { term.pty.kill(); } catch {}
|
|
231
370
|
}
|
|
232
|
-
if (term.timer) clearTimeout(term.timer);
|
|
233
|
-
ctx.terminals.delete(terminalId);
|
|
234
371
|
console.log(`[PTY] Closed terminal ${terminalId}`);
|
|
235
372
|
ctx.sendToServer({
|
|
236
373
|
type: 'terminal_closed',
|
package/workbench/file-ops.js
CHANGED
|
@@ -4,6 +4,7 @@ import { join, basename, dirname, extname } from 'path';
|
|
|
4
4
|
import { platform } from 'os';
|
|
5
5
|
import ctx from '../context.js';
|
|
6
6
|
import { resolveAndValidatePath, BINARY_EXTENSIONS } from './utils.js';
|
|
7
|
+
import { sendWorkbenchResult } from './request-routing.js';
|
|
7
8
|
|
|
8
9
|
export async function handleReadFile(msg) {
|
|
9
10
|
const { conversationId, filePath, requestId, _requestUserId, _requestClientId } = msg;
|
|
@@ -20,7 +21,7 @@ export async function handleReadFile(msg) {
|
|
|
20
21
|
// Binary file: read as Buffer, send base64
|
|
21
22
|
const buffer = await readFile(resolved);
|
|
22
23
|
console.log('[Agent] Sending binary file_content:', { filePath: resolved, size: buffer.length, mimeType, conversationId });
|
|
23
|
-
ctx
|
|
24
|
+
sendWorkbenchResult(ctx, msg, {
|
|
24
25
|
type: 'file_content',
|
|
25
26
|
conversationId,
|
|
26
27
|
requestId,
|
|
@@ -56,7 +57,7 @@ export async function handleReadFile(msg) {
|
|
|
56
57
|
};
|
|
57
58
|
|
|
58
59
|
console.log('[Agent] Sending file_content:', { filePath: resolved, contentLen: content.length, conversationId });
|
|
59
|
-
ctx
|
|
60
|
+
sendWorkbenchResult(ctx, msg, {
|
|
60
61
|
type: 'file_content',
|
|
61
62
|
conversationId,
|
|
62
63
|
requestId,
|
|
@@ -69,7 +70,7 @@ export async function handleReadFile(msg) {
|
|
|
69
70
|
});
|
|
70
71
|
}
|
|
71
72
|
} catch (e) {
|
|
72
|
-
ctx
|
|
73
|
+
sendWorkbenchResult(ctx, msg, {
|
|
73
74
|
type: 'file_content',
|
|
74
75
|
conversationId,
|
|
75
76
|
requestId,
|
|
@@ -92,7 +93,7 @@ export async function handleWriteFile(msg) {
|
|
|
92
93
|
const resolved = resolveAndValidatePath(filePath, workDir);
|
|
93
94
|
await writeFile(resolved, content, 'utf-8');
|
|
94
95
|
|
|
95
|
-
ctx
|
|
96
|
+
sendWorkbenchResult(ctx, msg, {
|
|
96
97
|
type: 'file_saved',
|
|
97
98
|
conversationId,
|
|
98
99
|
requestId,
|
|
@@ -103,7 +104,7 @@ export async function handleWriteFile(msg) {
|
|
|
103
104
|
success: true
|
|
104
105
|
});
|
|
105
106
|
} catch (e) {
|
|
106
|
-
ctx
|
|
107
|
+
sendWorkbenchResult(ctx, msg, {
|
|
107
108
|
type: 'file_saved',
|
|
108
109
|
conversationId,
|
|
109
110
|
requestId,
|
|
@@ -133,7 +134,7 @@ export async function handleListDirectory(msg) {
|
|
|
133
134
|
drives.push({ name: letter + ':', type: 'directory', size: 0 });
|
|
134
135
|
}
|
|
135
136
|
}
|
|
136
|
-
ctx
|
|
137
|
+
sendWorkbenchResult(ctx, msg, {
|
|
137
138
|
type: 'directory_listing',
|
|
138
139
|
conversationId,
|
|
139
140
|
requestId,
|
|
@@ -150,7 +151,7 @@ export async function handleListDirectory(msg) {
|
|
|
150
151
|
.filter(e => !(e.isDirectory() && SKIP_DIRS.has(e.name)))
|
|
151
152
|
.map(e => ({ name: e.name, type: e.isDirectory() ? 'directory' : 'file', size: 0 }))
|
|
152
153
|
.sort((a, b) => a.name.localeCompare(b.name));
|
|
153
|
-
ctx
|
|
154
|
+
sendWorkbenchResult(ctx, msg, {
|
|
154
155
|
type: 'directory_listing',
|
|
155
156
|
conversationId,
|
|
156
157
|
requestId,
|
|
@@ -161,7 +162,7 @@ export async function handleListDirectory(msg) {
|
|
|
161
162
|
});
|
|
162
163
|
}
|
|
163
164
|
} catch (e) {
|
|
164
|
-
ctx
|
|
165
|
+
sendWorkbenchResult(ctx, msg, {
|
|
165
166
|
type: 'directory_listing',
|
|
166
167
|
conversationId,
|
|
167
168
|
requestId,
|
|
@@ -209,7 +210,7 @@ export async function handleListDirectory(msg) {
|
|
|
209
210
|
return a.name.localeCompare(b.name);
|
|
210
211
|
});
|
|
211
212
|
|
|
212
|
-
ctx
|
|
213
|
+
sendWorkbenchResult(ctx, msg, {
|
|
213
214
|
type: 'directory_listing',
|
|
214
215
|
conversationId,
|
|
215
216
|
requestId,
|
|
@@ -219,7 +220,7 @@ export async function handleListDirectory(msg) {
|
|
|
219
220
|
entries: result
|
|
220
221
|
});
|
|
221
222
|
} catch (e) {
|
|
222
|
-
ctx
|
|
223
|
+
sendWorkbenchResult(ctx, msg, {
|
|
223
224
|
type: 'directory_listing',
|
|
224
225
|
conversationId,
|
|
225
226
|
requestId,
|
|
@@ -251,13 +252,13 @@ export async function handleCreateFile(msg) {
|
|
|
251
252
|
}
|
|
252
253
|
await writeFile(resolved, '', 'utf-8');
|
|
253
254
|
}
|
|
254
|
-
ctx
|
|
255
|
+
sendWorkbenchResult(ctx, msg, {
|
|
255
256
|
type: 'file_op_result', conversationId, _requestUserId,
|
|
256
257
|
operation: 'create', success: true,
|
|
257
258
|
message: (isDirectory ? 'Directory' : 'File') + ' created: ' + basename(resolved)
|
|
258
259
|
});
|
|
259
260
|
} catch (e) {
|
|
260
|
-
ctx
|
|
261
|
+
sendWorkbenchResult(ctx, msg, {
|
|
261
262
|
type: 'file_op_result', conversationId, _requestUserId,
|
|
262
263
|
operation: 'create', success: false, error: e.message
|
|
263
264
|
});
|
|
@@ -293,13 +294,13 @@ export async function handleDeleteFiles(msg) {
|
|
|
293
294
|
? 'Deleted: ' + deleted.join(', ') + (errors.length > 0 ? '; Errors: ' + errors.join(', ') : '')
|
|
294
295
|
: 'Failed: ' + errors.join(', ');
|
|
295
296
|
|
|
296
|
-
ctx
|
|
297
|
+
sendWorkbenchResult(ctx, msg, {
|
|
297
298
|
type: 'file_op_result', conversationId, _requestUserId,
|
|
298
299
|
operation: 'delete', success: deleted.length > 0,
|
|
299
300
|
message, deletedCount: deleted.length, errorCount: errors.length
|
|
300
301
|
});
|
|
301
302
|
} catch (e) {
|
|
302
|
-
ctx
|
|
303
|
+
sendWorkbenchResult(ctx, msg, {
|
|
303
304
|
type: 'file_op_result', conversationId, _requestUserId,
|
|
304
305
|
operation: 'delete', success: false, error: e.message
|
|
305
306
|
});
|
|
@@ -341,13 +342,13 @@ export async function handleMoveFiles(msg) {
|
|
|
341
342
|
? 'Moved: ' + moved.join(', ') + ' → ' + basename(destResolved) + (errors.length > 0 ? '; Errors: ' + errors.join(', ') : '')
|
|
342
343
|
: 'Failed: ' + errors.join(', ');
|
|
343
344
|
|
|
344
|
-
ctx
|
|
345
|
+
sendWorkbenchResult(ctx, msg, {
|
|
345
346
|
type: 'file_op_result', conversationId, _requestUserId,
|
|
346
347
|
operation: 'move', success: moved.length > 0,
|
|
347
348
|
message, movedCount: moved.length, errorCount: errors.length
|
|
348
349
|
});
|
|
349
350
|
} catch (e) {
|
|
350
|
-
ctx
|
|
351
|
+
sendWorkbenchResult(ctx, msg, {
|
|
351
352
|
type: 'file_op_result', conversationId, _requestUserId,
|
|
352
353
|
operation: 'move', success: false, error: e.message
|
|
353
354
|
});
|
|
@@ -402,13 +403,13 @@ export async function handleCopyFiles(msg) {
|
|
|
402
403
|
? 'Copied: ' + copied.join(', ') + (errors.length > 0 ? '; Errors: ' + errors.join(', ') : '')
|
|
403
404
|
: 'Failed: ' + errors.join(', ');
|
|
404
405
|
|
|
405
|
-
ctx
|
|
406
|
+
sendWorkbenchResult(ctx, msg, {
|
|
406
407
|
type: 'file_op_result', conversationId, _requestUserId,
|
|
407
408
|
operation: 'copy', success: copied.length > 0,
|
|
408
409
|
message, copiedCount: copied.length, errorCount: errors.length
|
|
409
410
|
});
|
|
410
411
|
} catch (e) {
|
|
411
|
-
ctx
|
|
412
|
+
sendWorkbenchResult(ctx, msg, {
|
|
412
413
|
type: 'file_op_result', conversationId, _requestUserId,
|
|
413
414
|
operation: 'copy', success: false, error: e.message
|
|
414
415
|
});
|
|
@@ -444,13 +445,13 @@ export async function handleUploadToDir(msg) {
|
|
|
444
445
|
? 'Uploaded: ' + saved.join(', ') + (errors.length > 0 ? '; Errors: ' + errors.join(', ') : '')
|
|
445
446
|
: 'Failed: ' + errors.join(', ');
|
|
446
447
|
|
|
447
|
-
ctx
|
|
448
|
+
sendWorkbenchResult(ctx, msg, {
|
|
448
449
|
type: 'file_op_result', conversationId, _requestUserId,
|
|
449
450
|
operation: 'upload', success: saved.length > 0,
|
|
450
451
|
message, uploadedCount: saved.length, errorCount: errors.length
|
|
451
452
|
});
|
|
452
453
|
} catch (e) {
|
|
453
|
-
ctx
|
|
454
|
+
sendWorkbenchResult(ctx, msg, {
|
|
454
455
|
type: 'file_op_result', conversationId, _requestUserId,
|
|
455
456
|
operation: 'upload', success: false, error: e.message
|
|
456
457
|
});
|
package/workbench/file-search.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { readdir, stat } from 'fs/promises';
|
|
2
2
|
import { join, relative, resolve } from 'path';
|
|
3
3
|
import ctx from '../context.js';
|
|
4
|
+
import { sendWorkbenchResult } from './request-routing.js';
|
|
4
5
|
|
|
5
6
|
export async function handleFileSearch(msg) {
|
|
6
7
|
const { conversationId, query, _requestUserId } = msg;
|
|
@@ -10,7 +11,7 @@ export async function handleFileSearch(msg) {
|
|
|
10
11
|
|
|
11
12
|
try {
|
|
12
13
|
if (!query || query.trim().length === 0) {
|
|
13
|
-
ctx
|
|
14
|
+
sendWorkbenchResult(ctx, msg, { type: 'file_search_result', conversationId, _requestUserId, query, results: [] });
|
|
14
15
|
return;
|
|
15
16
|
}
|
|
16
17
|
|
|
@@ -51,7 +52,7 @@ export async function handleFileSearch(msg) {
|
|
|
51
52
|
|
|
52
53
|
await walk(resolved, 0);
|
|
53
54
|
|
|
54
|
-
ctx
|
|
55
|
+
sendWorkbenchResult(ctx, msg, {
|
|
55
56
|
type: 'file_search_result',
|
|
56
57
|
conversationId,
|
|
57
58
|
_requestUserId,
|
|
@@ -60,6 +61,6 @@ export async function handleFileSearch(msg) {
|
|
|
60
61
|
truncated: results.length >= MAX_RESULTS
|
|
61
62
|
});
|
|
62
63
|
} catch (e) {
|
|
63
|
-
ctx
|
|
64
|
+
sendWorkbenchResult(ctx, msg, { type: 'file_search_result', conversationId, _requestUserId, query, results: [], error: e.message });
|
|
64
65
|
}
|
|
65
66
|
}
|
package/workbench/git-ops.js
CHANGED
|
@@ -2,6 +2,7 @@ import { readFile, writeFile } from 'fs/promises';
|
|
|
2
2
|
import { join, resolve } from 'path';
|
|
3
3
|
import ctx from '../context.js';
|
|
4
4
|
import { execAsync, resolveAndValidatePath, getGitRoot, validateGitPath } from './utils.js';
|
|
5
|
+
import { sendWorkbenchResult } from './request-routing.js';
|
|
5
6
|
|
|
6
7
|
export async function handleGitStatus(msg) {
|
|
7
8
|
const { conversationId, _requestUserId } = msg;
|
|
@@ -58,7 +59,7 @@ export async function handleGitStatus(msg) {
|
|
|
58
59
|
files.push({ path: displayPath, indexStatus, workTreeStatus });
|
|
59
60
|
}
|
|
60
61
|
|
|
61
|
-
ctx
|
|
62
|
+
sendWorkbenchResult(ctx, msg, {
|
|
62
63
|
type: 'git_status_result',
|
|
63
64
|
conversationId,
|
|
64
65
|
_requestUserId,
|
|
@@ -70,7 +71,7 @@ export async function handleGitStatus(msg) {
|
|
|
70
71
|
gitRoot
|
|
71
72
|
});
|
|
72
73
|
} catch (e) {
|
|
73
|
-
ctx
|
|
74
|
+
sendWorkbenchResult(ctx, msg, {
|
|
74
75
|
type: 'git_status_result',
|
|
75
76
|
conversationId,
|
|
76
77
|
_requestUserId,
|
|
@@ -88,7 +89,7 @@ export async function handleGitDiff(msg) {
|
|
|
88
89
|
try {
|
|
89
90
|
// 安全检查:验证 filePath 不包含 shell 注入字符
|
|
90
91
|
if (!filePath || /[`$;|&><!\n\r]/.test(filePath)) {
|
|
91
|
-
ctx
|
|
92
|
+
sendWorkbenchResult(ctx, msg, {
|
|
92
93
|
type: 'git_diff_result',
|
|
93
94
|
conversationId,
|
|
94
95
|
_requestUserId,
|
|
@@ -114,7 +115,7 @@ export async function handleGitDiff(msg) {
|
|
|
114
115
|
const fullPath = resolve(gitRoot, filePath);
|
|
115
116
|
const resolved = resolveAndValidatePath(fullPath, gitRoot);
|
|
116
117
|
const content = await readFile(resolved, 'utf-8');
|
|
117
|
-
ctx
|
|
118
|
+
sendWorkbenchResult(ctx, msg, {
|
|
118
119
|
type: 'git_diff_result',
|
|
119
120
|
conversationId,
|
|
120
121
|
_requestUserId,
|
|
@@ -148,7 +149,7 @@ export async function handleGitDiff(msg) {
|
|
|
148
149
|
windowsHide: true
|
|
149
150
|
});
|
|
150
151
|
if (cachedOut.trim()) {
|
|
151
|
-
ctx
|
|
152
|
+
sendWorkbenchResult(ctx, msg, {
|
|
152
153
|
type: 'git_diff_result',
|
|
153
154
|
conversationId,
|
|
154
155
|
_requestUserId,
|
|
@@ -167,7 +168,7 @@ export async function handleGitDiff(msg) {
|
|
|
167
168
|
windowsHide: true
|
|
168
169
|
});
|
|
169
170
|
if (wtOut.trim()) {
|
|
170
|
-
ctx
|
|
171
|
+
sendWorkbenchResult(ctx, msg, {
|
|
171
172
|
type: 'git_diff_result',
|
|
172
173
|
conversationId,
|
|
173
174
|
_requestUserId,
|
|
@@ -179,7 +180,7 @@ export async function handleGitDiff(msg) {
|
|
|
179
180
|
}
|
|
180
181
|
}
|
|
181
182
|
|
|
182
|
-
ctx
|
|
183
|
+
sendWorkbenchResult(ctx, msg, {
|
|
183
184
|
type: 'git_diff_result',
|
|
184
185
|
conversationId,
|
|
185
186
|
_requestUserId,
|
|
@@ -188,7 +189,7 @@ export async function handleGitDiff(msg) {
|
|
|
188
189
|
diff: stdout
|
|
189
190
|
});
|
|
190
191
|
} catch (e) {
|
|
191
|
-
ctx
|
|
192
|
+
sendWorkbenchResult(ctx, msg, {
|
|
192
193
|
type: 'git_diff_result',
|
|
193
194
|
conversationId,
|
|
194
195
|
_requestUserId,
|
|
@@ -210,15 +211,15 @@ export async function handleGitAdd(msg) {
|
|
|
210
211
|
await execAsync('git add -A', { cwd: gitRoot, timeout: 10000, windowsHide: true });
|
|
211
212
|
} else {
|
|
212
213
|
if (!validateGitPath(filePath)) {
|
|
213
|
-
ctx
|
|
214
|
+
sendWorkbenchResult(ctx, msg, { type: 'git_op_result', conversationId, _requestUserId, operation: 'add', success: false, error: 'Invalid file path' });
|
|
214
215
|
return;
|
|
215
216
|
}
|
|
216
217
|
await execAsync(`git add -- "${filePath}"`, { cwd: gitRoot, timeout: 10000, windowsHide: true });
|
|
217
218
|
}
|
|
218
219
|
|
|
219
|
-
ctx
|
|
220
|
+
sendWorkbenchResult(ctx, msg, { type: 'git_op_result', conversationId, _requestUserId, operation: 'add', success: true, message: addAll ? 'All files staged' : `Staged: ${filePath}` });
|
|
220
221
|
} catch (e) {
|
|
221
|
-
ctx
|
|
222
|
+
sendWorkbenchResult(ctx, msg, { type: 'git_op_result', conversationId, _requestUserId, operation: 'add', success: false, error: e.message });
|
|
222
223
|
}
|
|
223
224
|
}
|
|
224
225
|
|
|
@@ -234,15 +235,15 @@ export async function handleGitReset(msg) {
|
|
|
234
235
|
await execAsync('git reset HEAD', { cwd: gitRoot, timeout: 10000, windowsHide: true });
|
|
235
236
|
} else {
|
|
236
237
|
if (!validateGitPath(filePath)) {
|
|
237
|
-
ctx
|
|
238
|
+
sendWorkbenchResult(ctx, msg, { type: 'git_op_result', conversationId, _requestUserId, operation: 'reset', success: false, error: 'Invalid file path' });
|
|
238
239
|
return;
|
|
239
240
|
}
|
|
240
241
|
await execAsync(`git reset HEAD -- "${filePath}"`, { cwd: gitRoot, timeout: 10000, windowsHide: true });
|
|
241
242
|
}
|
|
242
243
|
|
|
243
|
-
ctx
|
|
244
|
+
sendWorkbenchResult(ctx, msg, { type: 'git_op_result', conversationId, _requestUserId, operation: 'reset', success: true, message: resetAll ? 'All files unstaged' : `Unstaged: ${filePath}` });
|
|
244
245
|
} catch (e) {
|
|
245
|
-
ctx
|
|
246
|
+
sendWorkbenchResult(ctx, msg, { type: 'git_op_result', conversationId, _requestUserId, operation: 'reset', success: false, error: e.message });
|
|
246
247
|
}
|
|
247
248
|
}
|
|
248
249
|
|
|
@@ -253,15 +254,15 @@ export async function handleGitRestore(msg) {
|
|
|
253
254
|
|
|
254
255
|
try {
|
|
255
256
|
if (!validateGitPath(filePath)) {
|
|
256
|
-
ctx
|
|
257
|
+
sendWorkbenchResult(ctx, msg, { type: 'git_op_result', conversationId, _requestUserId, operation: 'restore', success: false, error: 'Invalid file path' });
|
|
257
258
|
return;
|
|
258
259
|
}
|
|
259
260
|
|
|
260
261
|
const gitRoot = await getGitRoot(workDir);
|
|
261
262
|
await execAsync(`git restore -- "${filePath}"`, { cwd: gitRoot, timeout: 10000, windowsHide: true });
|
|
262
|
-
ctx
|
|
263
|
+
sendWorkbenchResult(ctx, msg, { type: 'git_op_result', conversationId, _requestUserId, operation: 'restore', success: true, message: `Restored: ${filePath}` });
|
|
263
264
|
} catch (e) {
|
|
264
|
-
ctx
|
|
265
|
+
sendWorkbenchResult(ctx, msg, { type: 'git_op_result', conversationId, _requestUserId, operation: 'restore', success: false, error: e.message });
|
|
265
266
|
}
|
|
266
267
|
}
|
|
267
268
|
|
|
@@ -272,7 +273,7 @@ export async function handleGitCommit(msg) {
|
|
|
272
273
|
|
|
273
274
|
try {
|
|
274
275
|
if (!commitMessage || !commitMessage.trim()) {
|
|
275
|
-
ctx
|
|
276
|
+
sendWorkbenchResult(ctx, msg, { type: 'git_op_result', conversationId, _requestUserId, operation: 'commit', success: false, error: 'Commit message is required' });
|
|
276
277
|
return;
|
|
277
278
|
}
|
|
278
279
|
|
|
@@ -286,13 +287,13 @@ export async function handleGitCommit(msg) {
|
|
|
286
287
|
const { stdout } = await execAsync(`git commit -F "${tmpFile}"`, {
|
|
287
288
|
cwd: gitRoot, timeout: 30000, windowsHide: true
|
|
288
289
|
});
|
|
289
|
-
ctx
|
|
290
|
+
sendWorkbenchResult(ctx, msg, { type: 'git_op_result', conversationId, _requestUserId, operation: 'commit', success: true, message: stdout.trim() });
|
|
290
291
|
} finally {
|
|
291
292
|
// Clean up temp file
|
|
292
293
|
try { await writeFile(tmpFile, '', 'utf8'); } catch {}
|
|
293
294
|
}
|
|
294
295
|
} catch (e) {
|
|
295
|
-
ctx
|
|
296
|
+
sendWorkbenchResult(ctx, msg, { type: 'git_op_result', conversationId, _requestUserId, operation: 'commit', success: false, error: e.stderr?.trim() || e.message });
|
|
296
297
|
}
|
|
297
298
|
}
|
|
298
299
|
|
|
@@ -306,8 +307,8 @@ export async function handleGitPush(msg) {
|
|
|
306
307
|
const { stdout, stderr } = await execAsync('git push', {
|
|
307
308
|
cwd: gitRoot, timeout: 60000, windowsHide: true
|
|
308
309
|
});
|
|
309
|
-
ctx
|
|
310
|
+
sendWorkbenchResult(ctx, msg, { type: 'git_op_result', conversationId, _requestUserId, operation: 'push', success: true, message: (stdout + '\n' + stderr).trim() || 'Push complete' });
|
|
310
311
|
} catch (e) {
|
|
311
|
-
ctx
|
|
312
|
+
sendWorkbenchResult(ctx, msg, { type: 'git_op_result', conversationId, _requestUserId, operation: 'push', success: false, error: e.stderr?.trim() || e.message });
|
|
312
313
|
}
|
|
313
314
|
}
|