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