@bytespell/amux 0.0.1 → 0.0.2

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.
@@ -0,0 +1,1591 @@
1
+ import {
2
+ endTurn,
3
+ getEventsForSession,
4
+ startTurn,
5
+ storeEvent
6
+ } from "./chunk-SX7NC3ZM.js";
7
+ import {
8
+ isSessionUpdate
9
+ } from "./chunk-226DBKL3.js";
10
+ import {
11
+ debug,
12
+ warn
13
+ } from "./chunk-5IPYOXBE.js";
14
+ import {
15
+ agentConfigs,
16
+ db,
17
+ sessions
18
+ } from "./chunk-OQ5K5ON2.js";
19
+
20
+ // src/agents/manager.ts
21
+ import { EventEmitter } from "events";
22
+ import { eq } from "drizzle-orm";
23
+
24
+ // src/agents/backends/acp.ts
25
+ import { randomUUID } from "crypto";
26
+ import { spawn as nodeSpawn } from "child_process";
27
+ import { Readable, Writable } from "stream";
28
+ import * as fs from "fs/promises";
29
+ import {
30
+ ClientSideConnection,
31
+ ndJsonStream
32
+ } from "@agentclientprotocol/sdk";
33
+
34
+ // src/agents/process.ts
35
+ import { execa } from "execa";
36
+ import treeKill from "tree-kill";
37
+ function spawn(options) {
38
+ const { command, args = [], cwd, env, timeoutMs } = options;
39
+ const subprocess = execa(command, args, {
40
+ cwd,
41
+ env: { ...process.env, ...env },
42
+ stdin: "pipe",
43
+ stdout: "pipe",
44
+ stderr: "pipe",
45
+ timeout: timeoutMs,
46
+ cleanup: true,
47
+ // Kill on parent exit
48
+ windowsHide: true
49
+ // Hide console window on Windows
50
+ });
51
+ const pid = subprocess.pid;
52
+ if (!pid) {
53
+ throw new Error(`Failed to spawn process: ${command}`);
54
+ }
55
+ return {
56
+ pid,
57
+ stdin: subprocess.stdin,
58
+ stdout: subprocess.stdout,
59
+ stderr: subprocess.stderr,
60
+ kill: (signal = "SIGTERM") => killTree(pid, signal),
61
+ wait: async () => {
62
+ try {
63
+ const result = await subprocess;
64
+ return { exitCode: result.exitCode };
65
+ } catch (err) {
66
+ return { exitCode: err.exitCode ?? 1 };
67
+ }
68
+ }
69
+ };
70
+ }
71
+ function killTree(pid, signal) {
72
+ return new Promise((resolve) => {
73
+ treeKill(pid, signal, (err) => {
74
+ if (err && !err.message.includes("No such process")) {
75
+ console.error(`[process] kill error pid=${pid}:`, err.message);
76
+ }
77
+ resolve();
78
+ });
79
+ });
80
+ }
81
+
82
+ // src/agents/backends/acp.ts
83
+ var INIT_TIMEOUT_MS = 3e4;
84
+ function normalizeSessionUpdate(update) {
85
+ if (update.sessionUpdate !== "tool_call" && update.sessionUpdate !== "tool_call_update") {
86
+ return update;
87
+ }
88
+ const content = update.content;
89
+ if (!content || !Array.isArray(content)) {
90
+ return update;
91
+ }
92
+ const normalizedContent = content.map((item) => {
93
+ if (item.type !== "diff") return item;
94
+ if (typeof item.content === "string") return item;
95
+ const newText = item.newText;
96
+ const oldText = item.oldText;
97
+ const path2 = item.path;
98
+ if (newText === void 0) return item;
99
+ const filePath = path2 ?? "file";
100
+ const oldLines = oldText ? oldText.split("\n") : [];
101
+ const newLines = newText.split("\n");
102
+ let unifiedDiff = `Index: ${filePath}
103
+ `;
104
+ unifiedDiff += "===================================================================\n";
105
+ unifiedDiff += `--- ${filePath}
106
+ `;
107
+ unifiedDiff += `+++ ${filePath}
108
+ `;
109
+ unifiedDiff += `@@ -${oldLines.length > 0 ? 1 : 0},${oldLines.length} +1,${newLines.length} @@
110
+ `;
111
+ for (const line of oldLines) {
112
+ unifiedDiff += `-${line}
113
+ `;
114
+ }
115
+ for (const line of newLines) {
116
+ unifiedDiff += `+${line}
117
+ `;
118
+ }
119
+ return {
120
+ type: "diff",
121
+ content: unifiedDiff
122
+ };
123
+ });
124
+ return {
125
+ ...update,
126
+ content: normalizedContent
127
+ };
128
+ }
129
+ function withTimeout(promise, ms, operation) {
130
+ return Promise.race([
131
+ promise,
132
+ new Promise(
133
+ (_, reject) => setTimeout(() => reject(new Error(`${operation} timed out after ${ms}ms`)), ms)
134
+ )
135
+ ]);
136
+ }
137
+ var AcpBackend = class {
138
+ type = "acp";
139
+ instances = /* @__PURE__ */ new Map();
140
+ onSessionIdChanged;
141
+ matches(config2) {
142
+ return config2.command !== "__mock__" && config2.command !== "__stress__" && config2.command !== "__shell__";
143
+ }
144
+ async start(sessionId, config2, cwd, existingAcpSessionId, emit) {
145
+ if (this.instances.has(sessionId)) {
146
+ await this.stop(sessionId);
147
+ }
148
+ const args = config2.args ?? [];
149
+ const env = config2.env ?? {};
150
+ debug("acp", ` Spawning: ${config2.command} ${args.join(" ")} in ${cwd}`);
151
+ const proc = spawn({
152
+ command: config2.command,
153
+ args,
154
+ cwd,
155
+ env
156
+ });
157
+ const instance = {
158
+ process: proc,
159
+ connection: null,
160
+ sessionId: "",
161
+ pendingPermission: null,
162
+ permissionCallbacks: /* @__PURE__ */ new Map(),
163
+ emit,
164
+ terminals: /* @__PURE__ */ new Map()
165
+ };
166
+ proc.wait().then(({ exitCode }) => {
167
+ if (this.instances.has(sessionId)) {
168
+ emit({ amuxEvent: "error", message: `Agent process exited with code ${exitCode}` });
169
+ this.instances.delete(sessionId);
170
+ }
171
+ });
172
+ try {
173
+ const input = Writable.toWeb(proc.stdin);
174
+ const output = Readable.toWeb(proc.stdout);
175
+ const stream = ndJsonStream(input, output);
176
+ const client = this.createClient(sessionId, instance);
177
+ instance.connection = new ClientSideConnection(() => client, stream);
178
+ const initResult = await withTimeout(
179
+ instance.connection.initialize({
180
+ protocolVersion: 1,
181
+ clientCapabilities: {
182
+ fs: { readTextFile: true, writeTextFile: true },
183
+ terminal: true
184
+ }
185
+ }),
186
+ INIT_TIMEOUT_MS,
187
+ "Agent initialization"
188
+ );
189
+ debug("acp", ` Initialized agent: ${initResult.agentInfo?.name} v${initResult.agentInfo?.version}`);
190
+ const canResume = initResult.agentCapabilities?.sessionCapabilities?.resume !== void 0;
191
+ let acpSessionId;
192
+ let sessionResult;
193
+ if (existingAcpSessionId && canResume) {
194
+ let resumeSucceeded = false;
195
+ try {
196
+ debug("acp", ` Resuming ACP session ${existingAcpSessionId}...`);
197
+ sessionResult = await withTimeout(
198
+ instance.connection.unstable_resumeSession({
199
+ sessionId: existingAcpSessionId,
200
+ cwd,
201
+ mcpServers: []
202
+ }),
203
+ INIT_TIMEOUT_MS,
204
+ "Session resume"
205
+ );
206
+ await new Promise((resolve) => setTimeout(resolve, 100));
207
+ acpSessionId = existingAcpSessionId;
208
+ debug("acp", ` ACP session resumed successfully`);
209
+ resumeSucceeded = true;
210
+ } catch (resumeErr) {
211
+ debug("acp", ` Resume failed, creating new session:`, resumeErr);
212
+ }
213
+ if (!resumeSucceeded) {
214
+ sessionResult = await withTimeout(
215
+ instance.connection.newSession({ cwd, mcpServers: [] }),
216
+ INIT_TIMEOUT_MS,
217
+ "New session creation"
218
+ );
219
+ acpSessionId = sessionResult.sessionId;
220
+ debug("acp", ` New ACP session created: ${acpSessionId}`);
221
+ this.onSessionIdChanged?.(sessionId, acpSessionId);
222
+ }
223
+ } else {
224
+ debug("acp", ` Creating new ACP session in ${cwd}...`);
225
+ sessionResult = await withTimeout(
226
+ instance.connection.newSession({ cwd, mcpServers: [] }),
227
+ INIT_TIMEOUT_MS,
228
+ "New session creation"
229
+ );
230
+ acpSessionId = sessionResult.sessionId;
231
+ debug("acp", ` ACP session created: ${acpSessionId}`);
232
+ this.onSessionIdChanged?.(sessionId, acpSessionId);
233
+ }
234
+ instance.sessionId = acpSessionId;
235
+ this.instances.set(sessionId, instance);
236
+ const models = sessionResult?.models?.availableModels;
237
+ const modes = sessionResult?.modes?.availableModes;
238
+ if (sessionResult?.modes) {
239
+ emit({
240
+ sessionUpdate: "current_mode_update",
241
+ currentModeId: sessionResult.modes.currentModeId
242
+ });
243
+ }
244
+ return {
245
+ sessionId: acpSessionId,
246
+ models,
247
+ modes
248
+ };
249
+ } catch (err) {
250
+ console.error(`[acp] Error starting agent for session ${sessionId}:`, err);
251
+ await proc.kill();
252
+ throw err;
253
+ }
254
+ }
255
+ createClient(_sessionId, instance) {
256
+ return {
257
+ async requestPermission(params) {
258
+ const requestId = randomUUID();
259
+ const permission = {
260
+ requestId,
261
+ toolCallId: params.toolCall.toolCallId,
262
+ title: params.toolCall.title ?? "Permission Required",
263
+ options: params.options.map((o) => ({
264
+ optionId: o.optionId,
265
+ name: o.name,
266
+ kind: o.kind
267
+ }))
268
+ };
269
+ instance.pendingPermission = permission;
270
+ instance.emit({ amuxEvent: "permission_request", permission });
271
+ return new Promise((resolve, reject) => {
272
+ instance.permissionCallbacks.set(requestId, {
273
+ resolve: (optionId) => {
274
+ instance.pendingPermission = null;
275
+ instance.emit({ amuxEvent: "permission_cleared" });
276
+ resolve({ outcome: { outcome: "selected", optionId } });
277
+ },
278
+ reject
279
+ });
280
+ });
281
+ },
282
+ async sessionUpdate(params) {
283
+ debug("acp", ` sessionUpdate received:`, JSON.stringify(params));
284
+ const normalized = normalizeSessionUpdate(params.update);
285
+ instance.emit(normalized);
286
+ },
287
+ async readTextFile(params) {
288
+ const content = await fs.readFile(params.path, "utf-8");
289
+ return { content };
290
+ },
291
+ async writeTextFile(params) {
292
+ await fs.writeFile(params.path, params.content);
293
+ return {};
294
+ },
295
+ async createTerminal(params) {
296
+ debug("acp", ` createTerminal request:`, JSON.stringify(params));
297
+ const terminalId = randomUUID();
298
+ const outputByteLimit = params.outputByteLimit ?? 1024 * 1024;
299
+ const termProc = nodeSpawn(params.command, params.args ?? [], {
300
+ cwd: params.cwd ?? void 0,
301
+ env: params.env ? { ...process.env, ...Object.fromEntries(params.env.map((e) => [e.name, e.value])) } : process.env,
302
+ shell: true,
303
+ stdio: ["ignore", "pipe", "pipe"]
304
+ });
305
+ const terminal = {
306
+ process: termProc,
307
+ output: "",
308
+ exitCode: null,
309
+ signal: null,
310
+ truncated: false,
311
+ outputByteLimit
312
+ };
313
+ const appendOutput = (data) => {
314
+ terminal.output += data.toString();
315
+ if (terminal.output.length > terminal.outputByteLimit) {
316
+ terminal.output = terminal.output.slice(-terminal.outputByteLimit);
317
+ terminal.truncated = true;
318
+ }
319
+ };
320
+ termProc.stdout?.on("data", appendOutput);
321
+ termProc.stderr?.on("data", appendOutput);
322
+ termProc.on("exit", (code, signal) => {
323
+ debug("acp", ` Terminal ${terminalId} exited with code ${code}, signal ${signal}`);
324
+ terminal.exitCode = code ?? null;
325
+ terminal.signal = signal ?? null;
326
+ });
327
+ termProc.on("error", (err) => {
328
+ console.error(`[acp] Terminal ${terminalId} error:`, err.message);
329
+ terminal.output += `
330
+ Error: ${err.message}`;
331
+ terminal.exitCode = -1;
332
+ });
333
+ instance.terminals.set(terminalId, terminal);
334
+ debug("acp", ` Created terminal ${terminalId} for command: ${params.command}`);
335
+ return { terminalId };
336
+ },
337
+ async terminalOutput(params) {
338
+ debug("acp", ` terminalOutput request for terminal ${params.terminalId}`);
339
+ const terminal = instance.terminals.get(params.terminalId);
340
+ if (!terminal) {
341
+ throw new Error(`Terminal ${params.terminalId} not found`);
342
+ }
343
+ return {
344
+ output: terminal.output,
345
+ truncated: terminal.truncated,
346
+ exitStatus: terminal.exitCode !== null || terminal.signal !== null ? {
347
+ exitCode: terminal.exitCode,
348
+ signal: terminal.signal
349
+ } : void 0
350
+ };
351
+ },
352
+ async waitForTerminalExit(params) {
353
+ debug("acp", ` waitForTerminalExit request for terminal ${params.terminalId}`);
354
+ const terminal = instance.terminals.get(params.terminalId);
355
+ if (!terminal) {
356
+ throw new Error(`Terminal ${params.terminalId} not found`);
357
+ }
358
+ if (terminal.exitCode !== null || terminal.signal !== null) {
359
+ return {
360
+ exitCode: terminal.exitCode,
361
+ signal: terminal.signal
362
+ };
363
+ }
364
+ return new Promise((resolve) => {
365
+ terminal.process.on("exit", (code, signal) => {
366
+ resolve({
367
+ exitCode: code ?? null,
368
+ signal: signal ?? null
369
+ });
370
+ });
371
+ });
372
+ },
373
+ // Note: killTerminalCommand not in SDK Client interface yet, but we implement handlers
374
+ // for completeness when the SDK adds support
375
+ async killTerminal(params) {
376
+ debug("acp", ` killTerminal request for terminal ${params.terminalId}`);
377
+ const terminal = instance.terminals.get(params.terminalId);
378
+ if (!terminal) {
379
+ throw new Error(`Terminal ${params.terminalId} not found`);
380
+ }
381
+ terminal.process.kill("SIGTERM");
382
+ return {};
383
+ },
384
+ async releaseTerminal(params) {
385
+ debug("acp", ` releaseTerminal request for terminal ${params.terminalId}`);
386
+ const terminal = instance.terminals.get(params.terminalId);
387
+ if (terminal) {
388
+ if (terminal.exitCode === null) {
389
+ terminal.process.kill("SIGKILL");
390
+ }
391
+ instance.terminals.delete(params.terminalId);
392
+ }
393
+ return {};
394
+ }
395
+ };
396
+ }
397
+ async prompt(sessionId, content, _emit) {
398
+ const instance = this.instances.get(sessionId);
399
+ if (!instance) throw new Error(`No ACP instance for window ${sessionId}`);
400
+ debug("acp", ` Sending prompt to session ${instance.sessionId} with ${content.length} content block(s)...`);
401
+ const result = await instance.connection.prompt({
402
+ sessionId: instance.sessionId,
403
+ prompt: content
404
+ });
405
+ debug("acp", ` Prompt complete, stopReason: ${result.stopReason}`);
406
+ }
407
+ async stop(sessionId) {
408
+ const instance = this.instances.get(sessionId);
409
+ if (!instance) return;
410
+ for (const [, callback] of instance.permissionCallbacks) {
411
+ callback.reject(new Error("Agent stopped"));
412
+ }
413
+ instance.permissionCallbacks.clear();
414
+ instance.pendingPermission = null;
415
+ this.instances.delete(sessionId);
416
+ await instance.process.kill();
417
+ }
418
+ async stopAll() {
419
+ const sessionIds = [...this.instances.keys()];
420
+ await Promise.all(sessionIds.map((id) => this.stop(id)));
421
+ }
422
+ isRunning(sessionId) {
423
+ return this.instances.has(sessionId);
424
+ }
425
+ respondToPermission(sessionId, requestId, optionId) {
426
+ const instance = this.instances.get(sessionId);
427
+ const callback = instance?.permissionCallbacks.get(requestId);
428
+ if (!callback) {
429
+ throw new Error(`No pending permission request ${requestId}`);
430
+ }
431
+ callback.resolve(optionId);
432
+ instance.permissionCallbacks.delete(requestId);
433
+ }
434
+ getPendingPermission(sessionId) {
435
+ const instance = this.instances.get(sessionId);
436
+ return instance?.pendingPermission ?? null;
437
+ }
438
+ async cancel(sessionId) {
439
+ const instance = this.instances.get(sessionId);
440
+ if (!instance) return;
441
+ await instance.connection.cancel({ sessionId: instance.sessionId });
442
+ }
443
+ async setMode(sessionId, modeId) {
444
+ const instance = this.instances.get(sessionId);
445
+ if (!instance) throw new Error(`No ACP instance for window ${sessionId}`);
446
+ await instance.connection.setSessionMode({
447
+ sessionId: instance.sessionId,
448
+ modeId
449
+ });
450
+ }
451
+ async setModel(sessionId, modelId) {
452
+ const instance = this.instances.get(sessionId);
453
+ if (!instance) throw new Error(`No ACP instance for window ${sessionId}`);
454
+ await instance.connection.unstable_setSessionModel({
455
+ sessionId: instance.sessionId,
456
+ modelId
457
+ });
458
+ }
459
+ };
460
+
461
+ // src/agents/backends/mock.ts
462
+ import { randomUUID as randomUUID2 } from "crypto";
463
+ function delay(ms) {
464
+ return new Promise((resolve) => setTimeout(resolve, ms));
465
+ }
466
+ var MockBackend = class {
467
+ type = "mock";
468
+ running = /* @__PURE__ */ new Set();
469
+ matches(config2) {
470
+ return config2.command === "__mock__";
471
+ }
472
+ async start(sessionId, _config, _cwd, _existingAcpSessionId, emit) {
473
+ debug("mock", ` Starting mock agent for session ${sessionId}`);
474
+ emit({
475
+ sessionUpdate: "current_mode_update",
476
+ currentModeId: "mock"
477
+ });
478
+ debug("mock", ` Mock agent ready for session ${sessionId}`);
479
+ this.running.add(sessionId);
480
+ return {
481
+ sessionId: randomUUID2(),
482
+ models: [{ modelId: "mock-model", name: "Mock Model" }],
483
+ modes: [{ id: "mock", name: "Mock Mode" }]
484
+ };
485
+ }
486
+ async prompt(sessionId, content, emit) {
487
+ const text = content.filter((b) => b.type === "text").map((b) => b.text).join("");
488
+ debug("mock", ` Mock prompt for session ${sessionId}: "${text.slice(0, 50)}..."`);
489
+ const words = [
490
+ "This",
491
+ "is",
492
+ "a",
493
+ "mock",
494
+ "response",
495
+ "from",
496
+ "the",
497
+ "mock",
498
+ "agent.",
499
+ "It",
500
+ "simulates",
501
+ "streaming",
502
+ "text",
503
+ "chunks",
504
+ "for",
505
+ "performance",
506
+ "testing.",
507
+ "The",
508
+ "response",
509
+ "arrives",
510
+ "in",
511
+ "small",
512
+ "pieces",
513
+ "to",
514
+ "test",
515
+ "UI",
516
+ "rendering."
517
+ ];
518
+ for (let i = 0; i < words.length; i += 3) {
519
+ const chunk = words.slice(i, i + 3).join(" ") + " ";
520
+ emit({
521
+ sessionUpdate: "agent_message_chunk",
522
+ content: { type: "text", text: chunk }
523
+ });
524
+ await delay(50);
525
+ }
526
+ }
527
+ async stop(sessionId) {
528
+ debug("mock", ` Stopping mock agent for session ${sessionId}`);
529
+ this.running.delete(sessionId);
530
+ }
531
+ async stopAll() {
532
+ const sessionIds = [...this.running];
533
+ await Promise.all(sessionIds.map((id) => this.stop(id)));
534
+ }
535
+ isRunning(sessionId) {
536
+ return this.running.has(sessionId);
537
+ }
538
+ };
539
+
540
+ // src/agents/backends/stress.ts
541
+ import { randomUUID as randomUUID4 } from "crypto";
542
+
543
+ // src/stress/generators.ts
544
+ import { randomUUID as randomUUID3 } from "crypto";
545
+ var DEFAULT_STRESS_CONFIG = {
546
+ eventsPerTurn: 50,
547
+ toolCallProbability: 0.4,
548
+ thoughtProbability: 0.6,
549
+ planProbability: 0.3,
550
+ chunkSize: 50,
551
+ delayMs: 0
552
+ };
553
+ var MARKDOWN_SAMPLES = [
554
+ // Sample 1: Code block with TypeScript
555
+ `Here's a React component that handles user authentication:
556
+
557
+ \`\`\`typescript
558
+ import { useState, useEffect } from 'react';
559
+ import { useAuth } from '@/hooks/useAuth';
560
+
561
+ interface AuthProviderProps {
562
+ children: React.ReactNode;
563
+ }
564
+
565
+ export function AuthProvider({ children }: AuthProviderProps) {
566
+ const [isLoading, setIsLoading] = useState(true);
567
+ const { user, login, logout } = useAuth();
568
+
569
+ useEffect(() => {
570
+ // Check for existing session
571
+ const checkSession = async () => {
572
+ try {
573
+ await validateToken();
574
+ } finally {
575
+ setIsLoading(false);
576
+ }
577
+ };
578
+ checkSession();
579
+ }, []);
580
+
581
+ if (isLoading) {
582
+ return <LoadingSpinner />;
583
+ }
584
+
585
+ return (
586
+ <AuthContext.Provider value={{ user, login, logout }}>
587
+ {children}
588
+ </AuthContext.Provider>
589
+ );
590
+ }
591
+ \`\`\`
592
+
593
+ This component wraps your app and provides authentication context.`,
594
+ // Sample 2: Multiple code blocks
595
+ `Let me show you how to set up the API client:
596
+
597
+ First, install the dependencies:
598
+
599
+ \`\`\`bash
600
+ npm install @tanstack/react-query axios
601
+ \`\`\`
602
+
603
+ Then create the client:
604
+
605
+ \`\`\`typescript
606
+ // src/lib/api.ts
607
+ import axios from 'axios';
608
+
609
+ export const apiClient = axios.create({
610
+ baseURL: process.env.NEXT_PUBLIC_API_URL,
611
+ timeout: 10000,
612
+ headers: {
613
+ 'Content-Type': 'application/json',
614
+ },
615
+ });
616
+
617
+ // Add auth interceptor
618
+ apiClient.interceptors.request.use((config) => {
619
+ const token = localStorage.getItem('auth_token');
620
+ if (token) {
621
+ config.headers.Authorization = \`Bearer \${token}\`;
622
+ }
623
+ return config;
624
+ });
625
+ \`\`\`
626
+
627
+ And set up React Query:
628
+
629
+ \`\`\`typescript
630
+ // src/providers/QueryProvider.tsx
631
+ import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
632
+
633
+ const queryClient = new QueryClient({
634
+ defaultOptions: {
635
+ queries: {
636
+ staleTime: 5 * 60 * 1000,
637
+ retry: 1,
638
+ },
639
+ },
640
+ });
641
+
642
+ export function QueryProvider({ children }: { children: React.ReactNode }) {
643
+ return (
644
+ <QueryClientProvider client={queryClient}>
645
+ {children}
646
+ </QueryClientProvider>
647
+ );
648
+ }
649
+ \`\`\``,
650
+ // Sample 3: Lists and inline code
651
+ `Here's what I found in the codebase:
652
+
653
+ 1. **Components** - Located in \`src/components/\`
654
+ - \`Button.tsx\` - Primary button component
655
+ - \`Input.tsx\` - Form input with validation
656
+ - \`Modal.tsx\` - Accessible modal dialog
657
+
658
+ 2. **Hooks** - Custom React hooks in \`src/hooks/\`
659
+ - \`useDebounce\` - Debounces rapidly changing values
660
+ - \`useLocalStorage\` - Syncs state with localStorage
661
+ - \`useMediaQuery\` - Responsive breakpoint detection
662
+
663
+ 3. **Utils** - Helper functions in \`src/lib/\`
664
+ - \`cn()\` - Class name merger (tailwind-merge + clsx)
665
+ - \`formatDate()\` - Date formatting with Intl API
666
+ - \`validateEmail()\` - Email validation regex
667
+
668
+ The main entry point is \`src/App.tsx\` which sets up routing and providers.`,
669
+ // Sample 4: Table and code
670
+ `Here's a comparison of the state management options:
671
+
672
+ | Feature | Zustand | Redux | Jotai |
673
+ |---------|---------|-------|-------|
674
+ | Bundle size | 1.5kb | 7kb | 2kb |
675
+ | Boilerplate | Low | High | Low |
676
+ | DevTools | Yes | Yes | Yes |
677
+ | TypeScript | Excellent | Good | Excellent |
678
+
679
+ Based on your requirements, I recommend Zustand:
680
+
681
+ \`\`\`typescript
682
+ import { create } from 'zustand';
683
+
684
+ interface AppState {
685
+ count: number;
686
+ increment: () => void;
687
+ decrement: () => void;
688
+ }
689
+
690
+ export const useAppStore = create<AppState>((set) => ({
691
+ count: 0,
692
+ increment: () => set((state) => ({ count: state.count + 1 })),
693
+ decrement: () => set((state) => ({ count: state.count - 1 })),
694
+ }));
695
+ \`\`\``,
696
+ // Sample 5: Error explanation with code
697
+ `I found the issue! The error occurs because of a race condition:
698
+
699
+ \`\`\`typescript
700
+ // \u274C Bug: Race condition
701
+ useEffect(() => {
702
+ fetchData().then(setData);
703
+ }, [id]);
704
+
705
+ // \u2705 Fix: Cleanup function prevents stale updates
706
+ useEffect(() => {
707
+ let cancelled = false;
708
+
709
+ fetchData().then((result) => {
710
+ if (!cancelled) {
711
+ setData(result);
712
+ }
713
+ });
714
+
715
+ return () => {
716
+ cancelled = true;
717
+ };
718
+ }, [id]);
719
+ \`\`\`
720
+
721
+ The fix adds a cleanup function that sets a \`cancelled\` flag when the effect re-runs or the component unmounts.`
722
+ ];
723
+ var THOUGHT_PREFIXES = [
724
+ "Let me analyze",
725
+ "I should check",
726
+ "Looking at",
727
+ "Considering",
728
+ "The approach here is",
729
+ "Based on the code",
730
+ "I need to",
731
+ "First",
732
+ "Next",
733
+ "This suggests",
734
+ "The pattern shows",
735
+ "Examining"
736
+ ];
737
+ var LOREM_WORDS = [
738
+ "Lorem",
739
+ "ipsum",
740
+ "dolor",
741
+ "sit",
742
+ "amet",
743
+ "consectetur",
744
+ "adipiscing",
745
+ "elit",
746
+ "sed",
747
+ "do",
748
+ "eiusmod",
749
+ "tempor",
750
+ "incididunt",
751
+ "ut",
752
+ "labore",
753
+ "et",
754
+ "dolore",
755
+ "magna",
756
+ "aliqua",
757
+ "Ut",
758
+ "enim",
759
+ "ad",
760
+ "minim",
761
+ "veniam",
762
+ "quis",
763
+ "nostrud"
764
+ ];
765
+ var PLAN_TASKS = [
766
+ "Analyze the current implementation",
767
+ "Identify areas for improvement",
768
+ "Refactor the component structure",
769
+ "Update the test suite",
770
+ "Fix type errors",
771
+ "Add documentation",
772
+ "Optimize performance",
773
+ "Review dependencies"
774
+ ];
775
+ var RICH_TOOL_CALLS = [
776
+ // Bash tool with command and output
777
+ {
778
+ toolName: "Bash",
779
+ kind: "execute",
780
+ title: "List files in src directory",
781
+ input: {
782
+ command: "ls -la src/",
783
+ description: "List files in src directory"
784
+ },
785
+ output: `total 48
786
+ drwxr-xr-x 8 user user 4096 Jan 10 14:30 .
787
+ drwxr-xr-x 12 user user 4096 Jan 10 14:25 ..
788
+ drwxr-xr-x 4 user user 4096 Jan 10 14:30 components
789
+ drwxr-xr-x 2 user user 4096 Jan 10 14:28 hooks
790
+ drwxr-xr-x 2 user user 4096 Jan 10 14:27 lib
791
+ -rw-r--r-- 1 user user 1245 Jan 10 14:30 App.tsx
792
+ -rw-r--r-- 1 user user 892 Jan 10 14:25 main.tsx
793
+ -rw-r--r-- 1 user user 2341 Jan 10 14:29 index.css`
794
+ },
795
+ // Read tool with file contents
796
+ {
797
+ toolName: "Read",
798
+ kind: "read",
799
+ title: "Read ~/repos/project/src/utils.ts",
800
+ input: {
801
+ file_path: "/home/user/repos/project/src/utils.ts"
802
+ },
803
+ output: `export function cn(...classes: string[]): string {
804
+ return classes.filter(Boolean).join(' ');
805
+ }
806
+
807
+ export function formatDate(date: Date): string {
808
+ return new Intl.DateTimeFormat('en-US', {
809
+ year: 'numeric',
810
+ month: 'long',
811
+ day: 'numeric',
812
+ }).format(date);
813
+ }
814
+
815
+ export function debounce<T extends (...args: unknown[]) => void>(
816
+ fn: T,
817
+ delay: number
818
+ ): T {
819
+ let timeoutId: NodeJS.Timeout;
820
+ return ((...args) => {
821
+ clearTimeout(timeoutId);
822
+ timeoutId = setTimeout(() => fn(...args), delay);
823
+ }) as T;
824
+ }`
825
+ },
826
+ // Edit tool with diff
827
+ {
828
+ toolName: "Edit",
829
+ kind: "edit",
830
+ title: "Edit ~/repos/project/src/config.ts",
831
+ input: {
832
+ file_path: "/home/user/repos/project/src/config.ts",
833
+ old_string: "timeout: 5000",
834
+ new_string: "timeout: 10000"
835
+ },
836
+ diff: `--- a/src/config.ts
837
+ +++ b/src/config.ts
838
+ @@ -5,7 +5,7 @@ export const config = {
839
+ apiUrl: process.env.API_URL,
840
+ environment: process.env.NODE_ENV,
841
+ features: {
842
+ - timeout: 5000,
843
+ + timeout: 10000,
844
+ retries: 3,
845
+ cacheEnabled: true,
846
+ },`
847
+ },
848
+ // Glob tool
849
+ {
850
+ toolName: "Glob",
851
+ kind: "search",
852
+ title: 'Glob "**/*.test.ts"',
853
+ input: {
854
+ pattern: "**/*.test.ts",
855
+ path: "/home/user/repos/project"
856
+ },
857
+ output: `src/components/Button.test.ts
858
+ src/components/Input.test.ts
859
+ src/hooks/useAuth.test.ts
860
+ src/lib/utils.test.ts
861
+ src/lib/api.test.ts`
862
+ },
863
+ // Grep tool
864
+ {
865
+ toolName: "Grep",
866
+ kind: "search",
867
+ title: 'Grep "useState" in src/',
868
+ input: {
869
+ pattern: "useState",
870
+ path: "/home/user/repos/project/src"
871
+ },
872
+ output: `src/components/Counter.tsx:3:import { useState } from 'react';
873
+ src/components/Form.tsx:1:import { useState, useCallback } from 'react';
874
+ src/hooks/useToggle.ts:1:import { useState } from 'react';
875
+ src/App.tsx:2:import { useState, useEffect } from 'react';`
876
+ },
877
+ // Task/subagent tool
878
+ {
879
+ toolName: "Task",
880
+ kind: "think",
881
+ title: "Explore Task",
882
+ input: {
883
+ subagent_type: "Explore",
884
+ description: "Find authentication implementation",
885
+ prompt: "Search for authentication-related files and understand the auth flow"
886
+ },
887
+ output: `Found 3 relevant files:
888
+ - src/hooks/useAuth.ts (main auth hook)
889
+ - src/context/AuthContext.tsx (auth provider)
890
+ - src/lib/auth.ts (token management)
891
+
892
+ The app uses JWT tokens stored in localStorage with automatic refresh.`
893
+ },
894
+ // Write tool
895
+ {
896
+ toolName: "Write",
897
+ kind: "edit",
898
+ title: "Write ~/repos/project/src/newFile.ts",
899
+ input: {
900
+ file_path: "/home/user/repos/project/src/newFile.ts",
901
+ content: 'export const VERSION = "1.0.0";'
902
+ },
903
+ output: "File created successfully"
904
+ },
905
+ // WebFetch tool
906
+ {
907
+ toolName: "WebFetch",
908
+ kind: "fetch",
909
+ title: "Fetch https://api.example.com/docs",
910
+ input: {
911
+ url: "https://api.example.com/docs",
912
+ prompt: "Get the API documentation"
913
+ },
914
+ output: `# API Documentation
915
+
916
+ ## Authentication
917
+ All requests require a Bearer token in the Authorization header.
918
+
919
+ ## Endpoints
920
+ - GET /users - List all users
921
+ - POST /users - Create a user
922
+ - GET /users/:id - Get user by ID`
923
+ }
924
+ ];
925
+ function randomInt(min, max) {
926
+ return Math.floor(Math.random() * (max - min + 1)) + min;
927
+ }
928
+ function randomChoice(arr) {
929
+ return arr[Math.floor(Math.random() * arr.length)];
930
+ }
931
+ function generateText(length) {
932
+ const words = [];
933
+ while (words.join(" ").length < length) {
934
+ words.push(randomChoice(LOREM_WORDS));
935
+ }
936
+ return words.join(" ").slice(0, length);
937
+ }
938
+ function generateMessageChunks(totalLength, chunkSize, kind = "agent_message_chunk") {
939
+ const text = generateText(totalLength);
940
+ const chunks = [];
941
+ for (let i = 0; i < text.length; i += chunkSize) {
942
+ const chunk = text.slice(i, i + chunkSize);
943
+ chunks.push({
944
+ sessionUpdate: kind,
945
+ content: { type: "text", text: chunk }
946
+ });
947
+ }
948
+ return chunks;
949
+ }
950
+ function generateMarkdownChunks(chunkSize = 50) {
951
+ const markdown = randomChoice(MARKDOWN_SAMPLES);
952
+ const chunks = [];
953
+ for (let i = 0; i < markdown.length; i += chunkSize) {
954
+ const chunk = markdown.slice(i, i + chunkSize);
955
+ chunks.push({
956
+ sessionUpdate: "agent_message_chunk",
957
+ content: { type: "text", text: chunk }
958
+ });
959
+ }
960
+ return chunks;
961
+ }
962
+ function generateThoughtChunks(totalLength) {
963
+ const prefix = randomChoice(THOUGHT_PREFIXES);
964
+ const rest = generateText(totalLength - prefix.length);
965
+ const text = `${prefix} ${rest}`;
966
+ const chunkSize = randomInt(30, 60);
967
+ const chunks = [];
968
+ for (let i = 0; i < text.length; i += chunkSize) {
969
+ const chunk = text.slice(i, i + chunkSize);
970
+ chunks.push({
971
+ sessionUpdate: "agent_thought_chunk",
972
+ content: { type: "text", text: chunk }
973
+ });
974
+ }
975
+ return chunks;
976
+ }
977
+ function generateRichToolCall(config2) {
978
+ const toolCallId = randomUUID3();
979
+ const toolConfig = config2 ?? randomChoice(RICH_TOOL_CALLS);
980
+ const call = {
981
+ sessionUpdate: "tool_call",
982
+ toolCallId,
983
+ title: toolConfig.title,
984
+ status: "in_progress",
985
+ kind: toolConfig.kind,
986
+ rawInput: toolConfig.input,
987
+ _meta: {
988
+ claudeCode: {
989
+ toolName: toolConfig.toolName
990
+ }
991
+ }
992
+ };
993
+ const toolResponse = [];
994
+ if (toolConfig.output) {
995
+ toolResponse.push({ type: "text", text: toolConfig.output });
996
+ }
997
+ const content = [];
998
+ if (toolConfig.diff) {
999
+ content.push({ type: "diff", content: toolConfig.diff });
1000
+ }
1001
+ const update = {
1002
+ sessionUpdate: "tool_call_update",
1003
+ toolCallId,
1004
+ status: "completed",
1005
+ // Title update (sometimes tools update their title)
1006
+ title: toolConfig.title.replace("...", ""),
1007
+ _meta: {
1008
+ claudeCode: {
1009
+ toolName: toolConfig.toolName,
1010
+ toolResponse: toolResponse.length > 0 ? toolResponse : void 0
1011
+ }
1012
+ },
1013
+ ...content.length > 0 && { content }
1014
+ };
1015
+ return { call, update, toolCallId };
1016
+ }
1017
+ function generatePlan(entryCount) {
1018
+ const entries = [];
1019
+ const completedCount = randomInt(0, Math.floor(entryCount / 2));
1020
+ for (let i = 0; i < entryCount; i++) {
1021
+ const status = i < completedCount ? "completed" : i === completedCount ? "in_progress" : "pending";
1022
+ entries.push({
1023
+ content: randomChoice(PLAN_TASKS),
1024
+ priority: randomChoice(["high", "medium", "low"]),
1025
+ status
1026
+ });
1027
+ }
1028
+ return {
1029
+ sessionUpdate: "plan",
1030
+ entries
1031
+ };
1032
+ }
1033
+ function generateRealisticTurn(config2 = DEFAULT_STRESS_CONFIG) {
1034
+ const events = [];
1035
+ if (Math.random() < config2.thoughtProbability) {
1036
+ const thoughtLength = randomInt(100, 300);
1037
+ events.push(...generateThoughtChunks(thoughtLength));
1038
+ }
1039
+ if (Math.random() < config2.planProbability) {
1040
+ events.push(generatePlan(randomInt(3, 6)));
1041
+ }
1042
+ if (Math.random() < config2.toolCallProbability) {
1043
+ const toolCount = randomInt(1, 3);
1044
+ for (let i = 0; i < toolCount; i++) {
1045
+ const { call, update } = generateRichToolCall();
1046
+ events.push(call);
1047
+ if (Math.random() < 0.3) {
1048
+ events.push(...generateThoughtChunks(randomInt(50, 100)));
1049
+ }
1050
+ events.push(update);
1051
+ }
1052
+ }
1053
+ if (Math.random() < 0.7) {
1054
+ events.push(...generateMarkdownChunks(config2.chunkSize));
1055
+ } else {
1056
+ const responseLength = randomInt(100, config2.eventsPerTurn * config2.chunkSize);
1057
+ events.push(...generateMessageChunks(responseLength, config2.chunkSize));
1058
+ }
1059
+ return events;
1060
+ }
1061
+
1062
+ // src/stress/config.ts
1063
+ var config = {
1064
+ turnDelay: 5,
1065
+ eventsPerTurn: 50,
1066
+ eventDelay: 30,
1067
+ // 30ms between events for visible streaming
1068
+ chunkSize: 20
1069
+ // smaller chunks look more like streaming
1070
+ };
1071
+ function getStressRuntimeConfig() {
1072
+ return { ...config };
1073
+ }
1074
+ function setStressRuntimeConfig(updates) {
1075
+ if (updates.turnDelay !== void 0) {
1076
+ config.turnDelay = Math.max(0, Math.min(60, updates.turnDelay));
1077
+ }
1078
+ if (updates.eventsPerTurn !== void 0) {
1079
+ config.eventsPerTurn = Math.max(10, Math.min(500, updates.eventsPerTurn));
1080
+ }
1081
+ if (updates.eventDelay !== void 0) {
1082
+ config.eventDelay = Math.max(0, Math.min(200, updates.eventDelay));
1083
+ }
1084
+ if (updates.chunkSize !== void 0) {
1085
+ config.chunkSize = Math.max(5, Math.min(100, updates.chunkSize));
1086
+ }
1087
+ }
1088
+
1089
+ // src/agents/backends/stress.ts
1090
+ function delay2(ms) {
1091
+ return new Promise((resolve) => setTimeout(resolve, ms));
1092
+ }
1093
+ var StressBackend = class {
1094
+ type = "stress";
1095
+ running = /* @__PURE__ */ new Map();
1096
+ constructor() {
1097
+ }
1098
+ getConfig() {
1099
+ const runtime = getStressRuntimeConfig();
1100
+ return {
1101
+ eventsPerTurn: runtime.eventsPerTurn,
1102
+ toolCallProbability: 0.4,
1103
+ thoughtProbability: 0.6,
1104
+ planProbability: 0.3,
1105
+ chunkSize: runtime.chunkSize,
1106
+ delayMs: runtime.eventDelay
1107
+ };
1108
+ }
1109
+ matches(config2) {
1110
+ return config2.command === "__stress__";
1111
+ }
1112
+ async start(sessionId, _config, _cwd, _existingAcpSessionId, emit) {
1113
+ debug("stress", ` Starting stress agent for session ${sessionId}`);
1114
+ emit({
1115
+ sessionUpdate: "current_mode_update",
1116
+ currentModeId: "stress"
1117
+ });
1118
+ const agent = { emit, stopped: false };
1119
+ this.running.set(sessionId, agent);
1120
+ this.runEventLoop(sessionId, agent);
1121
+ debug("stress", ` Stress agent ready for session ${sessionId}`);
1122
+ return {
1123
+ sessionId: randomUUID4(),
1124
+ models: [{ modelId: "stress-model", name: "Stress Model" }],
1125
+ modes: [{ id: "stress", name: "Stress Mode" }]
1126
+ };
1127
+ }
1128
+ getTurnDelayMs() {
1129
+ const { turnDelay } = getStressRuntimeConfig();
1130
+ if (turnDelay === 0) {
1131
+ return 0;
1132
+ }
1133
+ const variance = turnDelay * 0.5;
1134
+ return (turnDelay - variance + Math.random() * variance * 2) * 1e3;
1135
+ }
1136
+ async runEventLoop(sessionId, agent) {
1137
+ await delay2(1e3);
1138
+ let turnCount = 0;
1139
+ while (!agent.stopped) {
1140
+ turnCount++;
1141
+ agent.emit({ amuxEvent: "turn_start" });
1142
+ agent.emit({
1143
+ sessionUpdate: "user_message_chunk",
1144
+ content: { type: "text", text: `[Stress test turn ${turnCount}]` }
1145
+ });
1146
+ const config2 = this.getConfig();
1147
+ const events = generateRealisticTurn(config2);
1148
+ for (const event of events) {
1149
+ if (agent.stopped) break;
1150
+ agent.emit(event);
1151
+ if (config2.delayMs > 0) {
1152
+ await delay2(config2.delayMs);
1153
+ }
1154
+ }
1155
+ if (!agent.stopped) {
1156
+ agent.emit({ amuxEvent: "turn_end" });
1157
+ }
1158
+ const turnDelay = this.getTurnDelayMs();
1159
+ await delay2(turnDelay);
1160
+ }
1161
+ debug("stress", ` Event loop stopped for session ${sessionId}`);
1162
+ }
1163
+ async prompt(sessionId, content, emit) {
1164
+ const text = content.filter((b) => b.type === "text").map((b) => b.text).join("");
1165
+ debug("stress", ` Prompt for session ${sessionId}: "${text.slice(0, 50)}..."`);
1166
+ const config2 = this.getConfig();
1167
+ const events = generateRealisticTurn(config2);
1168
+ for (const event of events) {
1169
+ emit(event);
1170
+ if (config2.delayMs > 0) {
1171
+ await delay2(config2.delayMs);
1172
+ }
1173
+ }
1174
+ }
1175
+ async stop(sessionId) {
1176
+ debug("stress", ` Stopping stress agent for session ${sessionId}`);
1177
+ const agent = this.running.get(sessionId);
1178
+ if (agent) {
1179
+ agent.stopped = true;
1180
+ this.running.delete(sessionId);
1181
+ }
1182
+ }
1183
+ async stopAll() {
1184
+ const sessionIds = [...this.running.keys()];
1185
+ await Promise.all(sessionIds.map((id) => this.stop(id)));
1186
+ }
1187
+ isRunning(sessionId) {
1188
+ return this.running.has(sessionId);
1189
+ }
1190
+ };
1191
+
1192
+ // src/agents/backends/shell.ts
1193
+ var pty = null;
1194
+ async function getPty() {
1195
+ if (!pty) {
1196
+ pty = await import("node-pty");
1197
+ }
1198
+ return pty;
1199
+ }
1200
+ var MAX_SCROLLBACK = 1e5;
1201
+ var ShellBackend = class {
1202
+ type = "shell";
1203
+ isInteractive = true;
1204
+ sessions = /* @__PURE__ */ new Map();
1205
+ matches(config2) {
1206
+ return config2.command === "__shell__";
1207
+ }
1208
+ async start(sessionId, _config, cwd, _existingAcpSessionId, emit) {
1209
+ const existing = this.sessions.get(sessionId);
1210
+ if (existing) {
1211
+ debug("shell", `Session ${sessionId} already running, reusing`);
1212
+ existing.emit = emit;
1213
+ return { sessionId };
1214
+ }
1215
+ const nodePty = await getPty();
1216
+ const shell = process.env.SHELL || "/bin/bash";
1217
+ debug("shell", `Spawning ${shell} in ${cwd}`);
1218
+ const p = nodePty.spawn(shell, [], {
1219
+ name: "xterm-256color",
1220
+ cols: 80,
1221
+ rows: 24,
1222
+ cwd,
1223
+ env: process.env
1224
+ });
1225
+ const session = {
1226
+ pty: p,
1227
+ scrollback: "",
1228
+ cwd,
1229
+ emit
1230
+ };
1231
+ p.onData((data) => {
1232
+ session.scrollback += data;
1233
+ if (session.scrollback.length > MAX_SCROLLBACK) {
1234
+ session.scrollback = session.scrollback.slice(-MAX_SCROLLBACK);
1235
+ }
1236
+ session.emit({ amuxEvent: "terminal_output", data });
1237
+ });
1238
+ p.onExit(({ exitCode, signal }) => {
1239
+ debug("shell", `PTY exited: code=${exitCode}, signal=${signal}`);
1240
+ session.emit({
1241
+ amuxEvent: "terminal_exit",
1242
+ exitCode: exitCode ?? null,
1243
+ signal: signal !== void 0 ? String(signal) : null
1244
+ });
1245
+ this.sessions.delete(sessionId);
1246
+ });
1247
+ this.sessions.set(sessionId, session);
1248
+ debug("shell", `Session ${sessionId} started`);
1249
+ return { sessionId };
1250
+ }
1251
+ /**
1252
+ * Get accumulated scrollback for replay on reconnect.
1253
+ */
1254
+ getScrollback(sessionId) {
1255
+ return this.sessions.get(sessionId)?.scrollback;
1256
+ }
1257
+ /**
1258
+ * Write raw input to terminal (keystrokes from client).
1259
+ */
1260
+ terminalWrite(sessionId, data) {
1261
+ const session = this.sessions.get(sessionId);
1262
+ if (!session) {
1263
+ warn("shell", `terminalWrite: session ${sessionId} not found`);
1264
+ return;
1265
+ }
1266
+ session.pty.write(data);
1267
+ }
1268
+ /**
1269
+ * Resize terminal dimensions.
1270
+ */
1271
+ terminalResize(sessionId, cols, rows) {
1272
+ const session = this.sessions.get(sessionId);
1273
+ if (!session) {
1274
+ warn("shell", `terminalResize: session ${sessionId} not found`);
1275
+ return;
1276
+ }
1277
+ session.pty.resize(cols, rows);
1278
+ }
1279
+ /**
1280
+ * Shell does not support conversational prompts.
1281
+ */
1282
+ async prompt() {
1283
+ throw new Error("Shell backend does not support prompt(). Use terminalWrite() instead.");
1284
+ }
1285
+ async stop(sessionId) {
1286
+ const session = this.sessions.get(sessionId);
1287
+ if (!session) return;
1288
+ debug("shell", `Stopping session ${sessionId}`);
1289
+ session.pty.kill();
1290
+ this.sessions.delete(sessionId);
1291
+ }
1292
+ async stopAll() {
1293
+ debug("shell", `Stopping all sessions (${this.sessions.size})`);
1294
+ for (const sessionId of this.sessions.keys()) {
1295
+ await this.stop(sessionId);
1296
+ }
1297
+ }
1298
+ isRunning(sessionId) {
1299
+ return this.sessions.has(sessionId);
1300
+ }
1301
+ };
1302
+
1303
+ // src/lib/mentions.ts
1304
+ import path from "path";
1305
+ function parseMessageToContentBlocks(message, workingDir) {
1306
+ const blocks = [];
1307
+ const mentionRegex = /@(\.{0,2}[\w\/\.\-]+)/g;
1308
+ let lastIndex = 0;
1309
+ for (const match of message.matchAll(mentionRegex)) {
1310
+ if (match.index > lastIndex) {
1311
+ const text = message.slice(lastIndex, match.index);
1312
+ if (text.trim()) {
1313
+ blocks.push({ type: "text", text });
1314
+ }
1315
+ }
1316
+ const relativePath = match[1];
1317
+ const absolutePath = path.resolve(workingDir, relativePath);
1318
+ blocks.push({
1319
+ type: "resource_link",
1320
+ uri: `file://${absolutePath}`,
1321
+ name: relativePath
1322
+ // Required per ACP spec - use the path the user typed
1323
+ });
1324
+ lastIndex = match.index + match[0].length;
1325
+ }
1326
+ if (lastIndex < message.length) {
1327
+ const text = message.slice(lastIndex);
1328
+ if (text.trim()) {
1329
+ blocks.push({ type: "text", text });
1330
+ }
1331
+ }
1332
+ return blocks.length > 0 ? blocks : [{ type: "text", text: message }];
1333
+ }
1334
+
1335
+ // src/agents/manager.ts
1336
+ function generateTitleFromMessage(message) {
1337
+ const cleaned = message.replace(/@[\w./~-]+/g, "").replace(/\s+/g, " ").trim();
1338
+ const firstLine = cleaned.split("\n")[0] || cleaned;
1339
+ if (firstLine.length <= 50) return firstLine;
1340
+ const truncated = firstLine.slice(0, 50);
1341
+ const lastSpace = truncated.lastIndexOf(" ");
1342
+ return lastSpace > 20 ? truncated.slice(0, lastSpace) + "..." : truncated + "...";
1343
+ }
1344
+ var AgentManager = class extends EventEmitter {
1345
+ backends;
1346
+ agents = /* @__PURE__ */ new Map();
1347
+ constructor() {
1348
+ super();
1349
+ const acpBackend = new AcpBackend();
1350
+ acpBackend.onSessionIdChanged = (sessionId, acpSessionId) => {
1351
+ db.update(sessions).set({ acpSessionId }).where(eq(sessions.id, sessionId)).run();
1352
+ };
1353
+ this.backends = [acpBackend, new ShellBackend(), new MockBackend(), new StressBackend()];
1354
+ }
1355
+ getBackendForConfig(config2) {
1356
+ const backend = this.backends.find((b) => b.matches(config2));
1357
+ if (!backend) {
1358
+ throw new Error(`No backend for agent config: ${config2.command}`);
1359
+ }
1360
+ return backend;
1361
+ }
1362
+ emitUpdate(sessionId, update) {
1363
+ storeEvent(sessionId, update);
1364
+ if (isSessionUpdate(update) && update.sessionUpdate === "session_info_update") {
1365
+ const title = update.title;
1366
+ if (title !== void 0) {
1367
+ db.update(sessions).set({ title }).where(eq(sessions.id, sessionId)).run();
1368
+ }
1369
+ }
1370
+ this.emit("update", { sessionId, update });
1371
+ }
1372
+ async startForSession(sessionId) {
1373
+ const existing = this.agents.get(sessionId);
1374
+ if (existing?.status === "ready") {
1375
+ const replayEvents2 = getEventsForSession(sessionId);
1376
+ const scrollback = this.getTerminalScrollback(sessionId);
1377
+ debug("agents", `startForSession ${sessionId} - existing ready, scrollback: ${scrollback?.length ?? 0} bytes`);
1378
+ return {
1379
+ acpSessionId: existing.session.sessionId,
1380
+ replayEvents: replayEvents2,
1381
+ scrollback,
1382
+ models: existing.session.models,
1383
+ modes: existing.session.modes
1384
+ };
1385
+ }
1386
+ if (existing?.status === "starting") {
1387
+ return new Promise((resolve, reject) => {
1388
+ const checkReady = () => {
1389
+ const agent = this.agents.get(sessionId);
1390
+ if (agent?.status === "ready") {
1391
+ const replayEvents2 = getEventsForSession(sessionId);
1392
+ const scrollback = this.getTerminalScrollback(sessionId);
1393
+ resolve({
1394
+ acpSessionId: agent.session.sessionId,
1395
+ replayEvents: replayEvents2,
1396
+ scrollback,
1397
+ models: agent.session.models,
1398
+ modes: agent.session.modes
1399
+ });
1400
+ } else if (agent?.status === "dead") {
1401
+ reject(new Error("Agent failed to start"));
1402
+ } else {
1403
+ setTimeout(checkReady, 100);
1404
+ }
1405
+ };
1406
+ checkReady();
1407
+ });
1408
+ }
1409
+ const session = db.select().from(sessions).where(eq(sessions.id, sessionId)).get();
1410
+ if (!session) throw new Error(`Session ${sessionId} not found`);
1411
+ const dbConfig = db.select().from(agentConfigs).where(eq(agentConfigs.id, session.agentConfigId)).get();
1412
+ if (!dbConfig) throw new Error(`Agent config ${session.agentConfigId} not found`);
1413
+ const config2 = {
1414
+ id: dbConfig.id,
1415
+ name: dbConfig.name,
1416
+ command: dbConfig.command,
1417
+ args: dbConfig.args ?? [],
1418
+ env: dbConfig.env ?? {}
1419
+ };
1420
+ const backend = this.getBackendForConfig(config2);
1421
+ const emit = (update) => this.emitUpdate(sessionId, update);
1422
+ this.agents.set(sessionId, {
1423
+ backend,
1424
+ session: { sessionId: "" },
1425
+ status: "starting"
1426
+ });
1427
+ const replayEvents = getEventsForSession(sessionId);
1428
+ try {
1429
+ const agentSession = await backend.start(sessionId, config2, session.directory, session.acpSessionId ?? null, emit);
1430
+ this.agents.set(sessionId, {
1431
+ backend,
1432
+ session: agentSession,
1433
+ status: "ready"
1434
+ });
1435
+ debug("agents", `Agent ready for session ${sessionId}`);
1436
+ const scrollback = this.getTerminalScrollback(sessionId);
1437
+ return {
1438
+ acpSessionId: agentSession.sessionId,
1439
+ replayEvents,
1440
+ scrollback,
1441
+ models: agentSession.models,
1442
+ modes: agentSession.modes
1443
+ };
1444
+ } catch (err) {
1445
+ this.agents.set(sessionId, {
1446
+ backend,
1447
+ session: { sessionId: "" },
1448
+ status: "dead"
1449
+ });
1450
+ throw err;
1451
+ }
1452
+ }
1453
+ // Legacy alias for compatibility during migration
1454
+ /** @deprecated Use startForSession instead */
1455
+ startForWindow = this.startForSession.bind(this);
1456
+ async prompt(sessionId, message) {
1457
+ debug("agents", `prompt() called for session ${sessionId}: "${message.slice(0, 50)}..."`);
1458
+ const agent = this.agents.get(sessionId);
1459
+ if (!agent || agent.status !== "ready") {
1460
+ throw new Error(`Agent not ready for session ${sessionId}`);
1461
+ }
1462
+ const session = db.select().from(sessions).where(eq(sessions.id, sessionId)).get();
1463
+ if (!session) throw new Error(`Session ${sessionId} not found`);
1464
+ if (!session.title) {
1465
+ const title = generateTitleFromMessage(message);
1466
+ if (title) {
1467
+ db.update(sessions).set({ title }).where(eq(sessions.id, sessionId)).run();
1468
+ this.emitUpdate(sessionId, {
1469
+ sessionUpdate: "session_info_update",
1470
+ title
1471
+ });
1472
+ }
1473
+ }
1474
+ startTurn(sessionId);
1475
+ this.emitUpdate(sessionId, { amuxEvent: "turn_start" });
1476
+ this.emitUpdate(sessionId, {
1477
+ sessionUpdate: "user_message_chunk",
1478
+ content: { type: "text", text: message }
1479
+ });
1480
+ const content = parseMessageToContentBlocks(message, session.directory);
1481
+ const emit = (update) => this.emitUpdate(sessionId, update);
1482
+ try {
1483
+ await agent.backend.prompt(sessionId, content, emit);
1484
+ } finally {
1485
+ endTurn(sessionId);
1486
+ this.emitUpdate(sessionId, { amuxEvent: "turn_end" });
1487
+ }
1488
+ }
1489
+ async setMode(sessionId, modeId) {
1490
+ const agent = this.agents.get(sessionId);
1491
+ if (!agent || agent.status !== "ready") {
1492
+ throw new Error(`Agent not ready for session ${sessionId}`);
1493
+ }
1494
+ if (!agent.backend.setMode) {
1495
+ throw new Error(`Backend ${agent.backend.type} does not support setMode`);
1496
+ }
1497
+ await agent.backend.setMode(sessionId, modeId);
1498
+ }
1499
+ async setModel(sessionId, modelId) {
1500
+ const agent = this.agents.get(sessionId);
1501
+ if (!agent || agent.status !== "ready") {
1502
+ throw new Error(`Agent not ready for session ${sessionId}`);
1503
+ }
1504
+ if (!agent.backend.setModel) {
1505
+ throw new Error(`Backend ${agent.backend.type} does not support setModel`);
1506
+ }
1507
+ await agent.backend.setModel(sessionId, modelId);
1508
+ }
1509
+ async cancel(sessionId) {
1510
+ const agent = this.agents.get(sessionId);
1511
+ if (!agent?.backend.cancel) return;
1512
+ await agent.backend.cancel(sessionId);
1513
+ }
1514
+ // Terminal-specific methods (for ShellBackend)
1515
+ terminalWrite(sessionId, data) {
1516
+ const agent = this.agents.get(sessionId);
1517
+ if (!agent) {
1518
+ throw new Error(`No agent running for session ${sessionId}`);
1519
+ }
1520
+ if (!agent.backend.terminalWrite) {
1521
+ throw new Error(`Backend ${agent.backend.type} does not support terminal input`);
1522
+ }
1523
+ agent.backend.terminalWrite(sessionId, data);
1524
+ }
1525
+ terminalResize(sessionId, cols, rows) {
1526
+ const agent = this.agents.get(sessionId);
1527
+ if (!agent) {
1528
+ throw new Error(`No agent running for session ${sessionId}`);
1529
+ }
1530
+ if (!agent.backend.terminalResize) {
1531
+ throw new Error(`Backend ${agent.backend.type} does not support terminal resize`);
1532
+ }
1533
+ agent.backend.terminalResize(sessionId, cols, rows);
1534
+ }
1535
+ getTerminalScrollback(sessionId) {
1536
+ const agent = this.agents.get(sessionId);
1537
+ if (!agent?.backend.getScrollback) return void 0;
1538
+ return agent.backend.getScrollback(sessionId);
1539
+ }
1540
+ isTerminalSession(sessionId) {
1541
+ const agent = this.agents.get(sessionId);
1542
+ return agent?.backend.isInteractive ?? false;
1543
+ }
1544
+ respondPermission(sessionId, requestId, optionId) {
1545
+ const agent = this.agents.get(sessionId);
1546
+ if (!agent) {
1547
+ console.warn(`[AgentManager] No agent running for session ${sessionId}, ignoring permission response`);
1548
+ return;
1549
+ }
1550
+ if (!agent.backend.respondToPermission) {
1551
+ throw new Error(`Backend ${agent.backend.type} does not support permissions`);
1552
+ }
1553
+ agent.backend.respondToPermission(sessionId, requestId, optionId);
1554
+ }
1555
+ async stopForSession(sessionId) {
1556
+ const agent = this.agents.get(sessionId);
1557
+ if (agent) {
1558
+ await agent.backend.stop(sessionId);
1559
+ }
1560
+ this.agents.delete(sessionId);
1561
+ }
1562
+ // Legacy alias for compatibility during migration
1563
+ /** @deprecated Use stopForSession instead */
1564
+ stopForWindow = this.stopForSession.bind(this);
1565
+ async stopAll() {
1566
+ const sessionIds = Array.from(this.agents.keys());
1567
+ await Promise.all(sessionIds.map((id) => this.stopForSession(id)));
1568
+ }
1569
+ getForSession(sessionId) {
1570
+ return this.agents.get(sessionId);
1571
+ }
1572
+ getPendingPermission(sessionId) {
1573
+ const agent = this.agents.get(sessionId);
1574
+ if (!agent?.backend.getPendingPermission) return null;
1575
+ return agent.backend.getPendingPermission(sessionId);
1576
+ }
1577
+ // Legacy alias for compatibility during migration
1578
+ /** @deprecated Use getForSession instead */
1579
+ getForWindow = this.getForSession.bind(this);
1580
+ registerBackend(backend) {
1581
+ this.backends.unshift(backend);
1582
+ }
1583
+ };
1584
+ var agentManager = new AgentManager();
1585
+
1586
+ export {
1587
+ getStressRuntimeConfig,
1588
+ setStressRuntimeConfig,
1589
+ agentManager
1590
+ };
1591
+ //# sourceMappingURL=chunk-RSLSN7F2.js.map