@yeaft/webchat-agent 1.0.413 → 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.
Files changed (58) hide show
  1. package/browser-runtime/browser-install.js +497 -0
  2. package/browser-runtime/cli.js +88 -0
  3. package/browser-runtime/config.js +116 -0
  4. package/browser-runtime/errors.js +8 -0
  5. package/browser-runtime/extension/manifest.json +18 -0
  6. package/browser-runtime/extension/offscreen.html +5 -0
  7. package/browser-runtime/extension/offscreen.js +101 -0
  8. package/browser-runtime/extension/popup.html +5 -0
  9. package/browser-runtime/extension/popup.js +1 -0
  10. package/browser-runtime/extension/service-worker.js +48 -0
  11. package/browser-runtime/extension.js +45 -0
  12. package/browser-runtime/index.js +5 -0
  13. package/browser-runtime/probe.js +427 -0
  14. package/browser-runtime/protocol.js +71 -0
  15. package/browser-runtime/service.js +132 -0
  16. package/browser-runtime/windows-version-job.ps1 +233 -0
  17. package/browser-runtime/windows-version-worker.js +75 -0
  18. package/browser-runtime/windows-version.js +85 -0
  19. package/cli.js +24 -7
  20. package/connection/index.js +12 -0
  21. package/context.js +1 -0
  22. package/index.js +19 -2
  23. package/llm-config-cli.js +24 -21
  24. package/local-runtime/server/client-protocol.js +14 -0
  25. package/local-runtime/server/context.js +3 -2
  26. package/local-runtime/server/handlers/agent-file-terminal.js +185 -115
  27. package/local-runtime/server/handlers/agent-output.js +3 -0
  28. package/local-runtime/server/handlers/client-misc.js +21 -4
  29. package/local-runtime/server/handlers/client-workbench.js +222 -41
  30. package/local-runtime/server/workbench-correlation.js +184 -0
  31. package/local-runtime/server/workbench-route.js +180 -0
  32. package/local-runtime/server/ws-agent.js +4 -0
  33. package/local-runtime/server/ws-client.js +25 -3
  34. package/local-runtime/version.json +1 -1
  35. package/local-runtime/web/app.bundle.js +191 -135
  36. package/local-runtime/web/app.bundle.js.gz +0 -0
  37. package/local-runtime/web/index.html +2 -2
  38. package/local-runtime/web/style.bundle.css +1 -1
  39. package/local-runtime/web/style.bundle.css.gz +0 -0
  40. package/package.json +5 -1
  41. package/service/config.js +23 -2
  42. package/service/index.js +1 -0
  43. package/service/linux.js +3 -2
  44. package/terminal.js +167 -30
  45. package/workbench/file-ops.js +21 -20
  46. package/workbench/file-search.js +4 -3
  47. package/workbench/git-ops.js +23 -22
  48. package/workbench/request-routing.js +16 -0
  49. package/yeaft/cli.js +57 -1
  50. package/yeaft/config-api.js +138 -192
  51. package/yeaft/config-store.js +192 -0
  52. package/yeaft/config.js +3 -0
  53. package/yeaft/init.js +20 -7
  54. package/yeaft/sessions/feature-flag.js +15 -33
  55. package/yeaft/sessions/session-manifest.js +114 -10
  56. package/yeaft/stdio-protocol.js +57 -0
  57. package/yeaft/storage/atomic.js +43 -17
  58. package/yeaft/tools/process-runner.js +86 -13
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yeaft/webchat-agent",
3
- "version": "1.0.413",
3
+ "version": "1.0.415",
4
4
  "description": "Remote worker agent for Yeaft Web Code Agent — connects the native Yeaft engine, CLI providers, and workbench tools",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -41,6 +41,8 @@
41
41
  "docker-entrypoint.sh",
42
42
  "yeaft/templates/**/*.md",
43
43
  "yeaft/dream/prompts/**/*.md",
44
+ "browser-runtime/extension/**/*",
45
+ "browser-runtime/*.ps1",
44
46
  "local-runtime/**/*"
45
47
  ],
46
48
  "license": "MIT",
@@ -62,6 +64,7 @@
62
64
  },
63
65
  "dependencies": {
64
66
  "@homebridge/node-pty-prebuilt-multiarch": "^0.13.1",
67
+ "@puppeteer/browsers": "2.13.0",
65
68
  "bcrypt": "^6.0.0",
66
69
  "compression": "^1.8.1",
67
70
  "dotenv": "^16.3.1",
@@ -69,6 +72,7 @@
69
72
  "jsonwebtoken": "^9.0.2",
70
73
  "multer": "^2.2.0",
71
74
  "nodemailer": "^9.0.3",
75
+ "puppeteer-core": "24.41.0",
72
76
  "qrcode": "^1.5.4",
73
77
  "speakeasy": "^2.0.0",
74
78
  "tweetnacl": "^1.0.3",
package/service/config.js CHANGED
@@ -113,7 +113,7 @@ export function resolveServiceInstanceId(args = [], env = process.env, options =
113
113
  }
114
114
 
115
115
  /** Resolve the CLI/env/default Yeaft data root for one Agent instance. */
116
- export function resolveYeaftDir(args = [], env = process.env, instanceId = DEFAULT_INSTANCE_ID) {
116
+ function explicitYeaftDirFromArgs(args = []) {
117
117
  let explicitYeaftDir = null;
118
118
  for (let index = 0; index < args.length; index += 1) {
119
119
  if (args[index] !== '--yeaft-dir') continue;
@@ -122,7 +122,28 @@ export function resolveYeaftDir(args = [], env = process.env, instanceId = DEFAU
122
122
  explicitYeaftDir = value;
123
123
  index += 1;
124
124
  }
125
- return explicitYeaftDir || env.YEAFT_DIR || getDefaultYeaftDir(instanceId);
125
+ return explicitYeaftDir;
126
+ }
127
+
128
+ export function resolveYeaftDir(args = [], env = process.env, instanceId = DEFAULT_INSTANCE_ID) {
129
+ return explicitYeaftDirFromArgs(args) || env.YEAFT_DIR || getDefaultYeaftDir(instanceId);
130
+ }
131
+
132
+ /** Resolve the persisted data root for an explicitly selected management target. */
133
+ export function resolveManagedYeaftDir(
134
+ args = [],
135
+ env = process.env,
136
+ instanceId = DEFAULT_INSTANCE_ID,
137
+ dependencies = {},
138
+ ) {
139
+ const explicitYeaftDir = explicitYeaftDirFromArgs(args);
140
+ if (explicitYeaftDir) return explicitYeaftDir;
141
+ const hasExplicitIdentity = args.includes('--name') || args.includes('--instance');
142
+ if (!hasExplicitIdentity && env.YEAFT_DIR) return env.YEAFT_DIR;
143
+ const readServiceConfig = dependencies.loadServiceConfig || loadServiceConfig;
144
+ const defaultYeaftDir = dependencies.getDefaultYeaftDir || getDefaultYeaftDir;
145
+ const persisted = readServiceConfig(instanceId)?.yeaftDir;
146
+ return persisted || defaultYeaftDir(instanceId);
126
147
  }
127
148
 
128
149
  /** Legacy alias for resolveServiceInstanceId(). */
package/service/index.js CHANGED
@@ -29,6 +29,7 @@ export {
29
29
  resolveDisplayName,
30
30
  resolveRuntimeIdentity,
31
31
  resolveServiceInstanceId,
32
+ resolveManagedYeaftDir,
32
33
  shouldLoadLegacyLocalConfig,
33
34
  normalizeInstanceId,
34
35
  isDefaultInstance,
package/service/linux.js CHANGED
@@ -12,7 +12,7 @@ export function getSystemdServicePath(instanceId = DEFAULT_INSTANCE_ID) {
12
12
  return join(homedir(), '.config', 'systemd', 'user', `${getServiceName(instanceId)}.service`);
13
13
  }
14
14
 
15
- function generateSystemdUnit(config) {
15
+ export function generateSystemdUnit(config) {
16
16
  const nodePath = getNodePath();
17
17
  const cliPath = getCliPath();
18
18
  const logDir = getLogDir(config.instanceId);
@@ -38,7 +38,8 @@ ExecStart=${nodePath} ${cliPath}
38
38
  WorkingDirectory=${config.workDir || homedir()}
39
39
  Restart=on-failure
40
40
  RestartSec=10
41
- KillMode=process
41
+ KillMode=mixed
42
+ TimeoutStopSec=15
42
43
  ${envLines.join('\n')}
43
44
  Environment=PATH=${nodeBinDir}:${homedir()}/.local/bin:${homedir()}/.npm-global/bin:/usr/local/bin:/usr/bin:/bin
44
45
 
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 workDir = conv?.workDir || ctx.CONFIG.workDir;
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
- // 输出缓冲 - 16ms 批量发送
124
- let buffer = '';
125
- let timer = null;
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
- buffer += data;
129
- if (!timer) {
130
- timer = setTimeout(() => {
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',
@@ -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.sendToServer({
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.sendToServer({
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.sendToServer({
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.sendToServer({
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.sendToServer({
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.sendToServer({
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.sendToServer({
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.sendToServer({
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.sendToServer({
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.sendToServer({
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.sendToServer({
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.sendToServer({
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.sendToServer({
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.sendToServer({
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.sendToServer({
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.sendToServer({
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.sendToServer({
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.sendToServer({
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.sendToServer({
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.sendToServer({
454
+ sendWorkbenchResult(ctx, msg, {
454
455
  type: 'file_op_result', conversationId, _requestUserId,
455
456
  operation: 'upload', success: false, error: e.message
456
457
  });
@@ -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.sendToServer({ type: 'file_search_result', conversationId, _requestUserId, query, results: [] });
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.sendToServer({
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.sendToServer({ type: 'file_search_result', conversationId, _requestUserId, query, results: [], error: e.message });
64
+ sendWorkbenchResult(ctx, msg, { type: 'file_search_result', conversationId, _requestUserId, query, results: [], error: e.message });
64
65
  }
65
66
  }