@ai-devkit/agent-manager 0.15.0 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (112) hide show
  1. package/.swcrc +27 -0
  2. package/dist/AgentManager.d.ts +4 -0
  3. package/dist/AgentManager.d.ts.map +1 -1
  4. package/dist/AgentManager.js +97 -64
  5. package/dist/AgentManager.js.map +1 -1
  6. package/dist/__tests__/AgentManager.test.js +564 -0
  7. package/dist/__tests__/AgentManager.test.js.map +1 -0
  8. package/dist/__tests__/adapters/ClaudeCodeAdapter.test.js +2157 -0
  9. package/dist/__tests__/adapters/ClaudeCodeAdapter.test.js.map +1 -0
  10. package/dist/__tests__/adapters/CodexAdapter.test.js +1354 -0
  11. package/dist/__tests__/adapters/CodexAdapter.test.js.map +1 -0
  12. package/dist/__tests__/adapters/CopilotAdapter.test.js +500 -0
  13. package/dist/__tests__/adapters/CopilotAdapter.test.js.map +1 -0
  14. package/dist/__tests__/adapters/GeminiCliAdapter.test.js +1229 -0
  15. package/dist/__tests__/adapters/GeminiCliAdapter.test.js.map +1 -0
  16. package/dist/__tests__/adapters/OpenCodeAdapter.test.js +626 -0
  17. package/dist/__tests__/adapters/OpenCodeAdapter.test.js.map +1 -0
  18. package/dist/__tests__/terminal/TmuxManager.test.js +159 -0
  19. package/dist/__tests__/terminal/TmuxManager.test.js.map +1 -0
  20. package/dist/__tests__/terminal/TtyWriter.test.js +162 -0
  21. package/dist/__tests__/terminal/TtyWriter.test.js.map +1 -0
  22. package/dist/__tests__/utils/AgentRegistry.test.js +282 -0
  23. package/dist/__tests__/utils/AgentRegistry.test.js.map +1 -0
  24. package/dist/__tests__/utils/ClaudeSessionParser.test.js +312 -0
  25. package/dist/__tests__/utils/ClaudeSessionParser.test.js.map +1 -0
  26. package/dist/__tests__/utils/matching.test.js +324 -0
  27. package/dist/__tests__/utils/matching.test.js.map +1 -0
  28. package/dist/__tests__/utils/process.test.js +177 -0
  29. package/dist/__tests__/utils/process.test.js.map +1 -0
  30. package/dist/__tests__/utils/session.test.js +158 -0
  31. package/dist/__tests__/utils/session.test.js.map +1 -0
  32. package/dist/adapters/AgentAdapter.d.ts +1 -1
  33. package/dist/adapters/AgentAdapter.d.ts.map +1 -1
  34. package/dist/adapters/AgentAdapter.js +8 -7
  35. package/dist/adapters/AgentAdapter.js.map +1 -1
  36. package/dist/adapters/ClaudeCodeAdapter.js +66 -77
  37. package/dist/adapters/ClaudeCodeAdapter.js.map +1 -1
  38. package/dist/adapters/CodexAdapter.d.ts +13 -1
  39. package/dist/adapters/CodexAdapter.d.ts.map +1 -1
  40. package/dist/adapters/CodexAdapter.js +208 -132
  41. package/dist/adapters/CodexAdapter.js.map +1 -1
  42. package/dist/adapters/CopilotAdapter.d.ts +44 -0
  43. package/dist/adapters/CopilotAdapter.d.ts.map +1 -0
  44. package/dist/adapters/CopilotAdapter.js +314 -0
  45. package/dist/adapters/CopilotAdapter.js.map +1 -0
  46. package/dist/adapters/GeminiCliAdapter.d.ts +6 -1
  47. package/dist/adapters/GeminiCliAdapter.d.ts.map +1 -1
  48. package/dist/adapters/GeminiCliAdapter.js +160 -149
  49. package/dist/adapters/GeminiCliAdapter.js.map +1 -1
  50. package/dist/adapters/OpenCodeAdapter.js +60 -69
  51. package/dist/adapters/OpenCodeAdapter.js.map +1 -1
  52. package/dist/adapters/index.d.ts +1 -0
  53. package/dist/adapters/index.d.ts.map +1 -1
  54. package/dist/adapters/index.js +2 -0
  55. package/dist/adapters/index.js.map +1 -1
  56. package/dist/index.d.ts +6 -0
  57. package/dist/index.d.ts.map +1 -1
  58. package/dist/index.js +5 -0
  59. package/dist/index.js.map +1 -1
  60. package/dist/terminal/TerminalFocusManager.js +62 -49
  61. package/dist/terminal/TerminalFocusManager.js.map +1 -1
  62. package/dist/terminal/TmuxManager.d.ts +28 -0
  63. package/dist/terminal/TmuxManager.d.ts.map +1 -0
  64. package/dist/terminal/TmuxManager.js +137 -0
  65. package/dist/terminal/TmuxManager.js.map +1 -0
  66. package/dist/terminal/TtyWriter.js +37 -16
  67. package/dist/terminal/TtyWriter.js.map +1 -1
  68. package/dist/terminal/index.js +1 -0
  69. package/dist/terminal/index.js.map +1 -1
  70. package/dist/utils/AgentRegistry.d.ts +35 -0
  71. package/dist/utils/AgentRegistry.d.ts.map +1 -0
  72. package/dist/utils/AgentRegistry.js +123 -0
  73. package/dist/utils/AgentRegistry.js.map +1 -0
  74. package/dist/utils/ClaudeSessionParser.d.ts.map +1 -1
  75. package/dist/utils/ClaudeSessionParser.js +62 -93
  76. package/dist/utils/ClaudeSessionParser.js.map +1 -1
  77. package/dist/utils/agents.d.ts +15 -0
  78. package/dist/utils/agents.d.ts.map +1 -0
  79. package/dist/utils/agents.js +41 -0
  80. package/dist/utils/agents.js.map +1 -0
  81. package/dist/utils/applescript.js +3 -6
  82. package/dist/utils/applescript.js.map +1 -1
  83. package/dist/utils/index.js +1 -0
  84. package/dist/utils/index.js.map +1 -1
  85. package/dist/utils/matching.js +21 -29
  86. package/dist/utils/matching.js.map +1 -1
  87. package/dist/utils/process.js +77 -56
  88. package/dist/utils/process.js.map +1 -1
  89. package/dist/utils/session.js +20 -34
  90. package/dist/utils/session.js.map +1 -1
  91. package/dist/utils/sortAgents.js +6 -8
  92. package/dist/utils/sortAgents.js.map +1 -1
  93. package/package.json +11 -7
  94. package/src/AgentManager.ts +40 -0
  95. package/src/__tests__/AgentManager.test.ts +135 -0
  96. package/src/__tests__/adapters/ClaudeCodeAdapter.test.ts +3 -2
  97. package/src/__tests__/adapters/CodexAdapter.test.ts +199 -0
  98. package/src/__tests__/adapters/CopilotAdapter.test.ts +411 -0
  99. package/src/__tests__/adapters/GeminiCliAdapter.test.ts +185 -1
  100. package/src/__tests__/terminal/TmuxManager.test.ts +175 -0
  101. package/src/__tests__/utils/AgentRegistry.test.ts +223 -0
  102. package/src/__tests__/utils/ClaudeSessionParser.test.ts +71 -0
  103. package/src/adapters/AgentAdapter.ts +1 -1
  104. package/src/adapters/CodexAdapter.ts +155 -8
  105. package/src/adapters/CopilotAdapter.ts +453 -0
  106. package/src/adapters/GeminiCliAdapter.ts +84 -6
  107. package/src/adapters/index.ts +1 -0
  108. package/src/index.ts +7 -0
  109. package/src/terminal/TmuxManager.ts +118 -0
  110. package/src/utils/AgentRegistry.ts +139 -0
  111. package/src/utils/ClaudeSessionParser.ts +21 -10
  112. package/src/utils/agents.ts +41 -0
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/__tests__/adapters/OpenCodeAdapter.test.ts"],"sourcesContent":["/**\n * Tests for OpenCodeAdapter\n */\n\nimport type { MockedFunction } from 'vitest';\nimport * as fs from 'fs';\nimport * as path from 'path';\n\nimport { OpenCodeAdapter } from '../../adapters/OpenCodeAdapter.js';\nimport type { ProcessInfo } from '../../adapters/AgentAdapter.js';\nimport { AgentStatus } from '../../adapters/AgentAdapter.js';\nimport { listAgentProcesses, enrichProcesses } from '../../utils/process.js';\nimport { generateAgentName } from '../../utils/matching.js';\nimport * as os from 'os';\n\nvi.mock('../../utils/process.js', () => ({\n listAgentProcesses: vi.fn(),\n enrichProcesses: vi.fn(),\n}));\n\nvi.mock('../../utils/matching.js', () => ({\n generateAgentName: vi.fn(),\n matchProcessesToSessions: vi.fn(),\n}));\n\nconst mockedListAgentProcesses = listAgentProcesses as MockedFunction<typeof listAgentProcesses>;\nconst mockedEnrichProcesses = enrichProcesses as MockedFunction<typeof enrichProcesses>;\nconst mockedGenerateAgentName = generateAgentName as MockedFunction<typeof generateAgentName>;\n\nfunction makeDb(queries: {\n session?: Array<{ id: string; directory: string; time_created: number }>;\n lastMessage?: { role: string; timeUpdated: number } | null;\n lastAssistant?: { completed: number | null; errored: number | null } | null;\n firstUserText?: { text: string } | null;\n parts?: Array<{ role: string; partData: string; timeCreated: number }>;\n}) {\n const prepareImpl = (sql: string) => {\n const normalized = sql.replace(/\\s+/g, ' ').trim().toLowerCase();\n\n if (normalized.includes('from session')) {\n return {\n all: () => (queries.session ?? []).map((r) => ({\n id: r.id, directory: r.directory, timeCreated: r.time_created,\n })),\n get: (dir: string) => {\n const match = (queries.session ?? []).find((r) => r.directory === dir);\n if (!match) return undefined;\n return { id: match.id, directory: match.directory, time_created: match.time_created };\n },\n };\n }\n\n if (normalized.includes('max(time_updated)')) {\n return {\n get: () => ({ maxUpdated: queries.lastMessage?.timeUpdated ?? 0 }),\n };\n }\n\n if (normalized.includes('from message') && !normalized.includes(\"'$.time.completed'\") && !normalized.includes('order by p.time_created')) {\n return {\n get: () => queries.lastMessage ?? undefined,\n };\n }\n\n if (normalized.includes(\"'$.time.completed'\")) {\n return {\n get: () => queries.lastAssistant === undefined\n ? undefined\n : queries.lastAssistant ?? undefined,\n };\n }\n\n if (normalized.includes(\"json_extract(m.data, '$.role') = 'user'\")) {\n return {\n get: () => queries.firstUserText === undefined\n ? undefined\n : queries.firstUserText ?? undefined,\n };\n }\n\n if (normalized.includes('order by p.time_created asc')) {\n return {\n all: () => queries.parts ?? [],\n };\n }\n\n return { all: () => [], get: () => undefined };\n };\n\n return { prepare: prepareImpl, close: vi.fn() };\n}\n\nfunction makeDbConstructor(db: ReturnType<typeof makeDb>) {\n return vi.fn().mockReturnValue(db);\n}\n\ndescribe('OpenCodeAdapter', () => {\n let adapter: OpenCodeAdapter;\n let tmpDir: string;\n let dbPath: string;\n\n beforeEach(() => {\n adapter = new OpenCodeAdapter();\n\n mockedListAgentProcesses.mockReset();\n mockedEnrichProcesses.mockReset();\n mockedGenerateAgentName.mockReset();\n\n mockedEnrichProcesses.mockImplementation((procs) => procs);\n mockedGenerateAgentName.mockImplementation((cwd, pid) => {\n const folder = path.basename(cwd) || 'unknown';\n return `${folder}-${pid}`;\n });\n\n tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'opencode-test-'));\n dbPath = path.join(tmpDir, 'opencode.db');\n (adapter as any).dbPath = dbPath;\n (adapter as any).db = null;\n });\n\n afterEach(() => {\n fs.rmSync(tmpDir, { recursive: true, force: true });\n vi.restoreAllMocks();\n });\n\n describe('type', () => {\n it('exposes opencode type', () => {\n expect(adapter.type).toBe('opencode');\n });\n });\n\n describe('canHandle', () => {\n it('returns true for opencode command', () => {\n expect(adapter.canHandle({ pid: 1, command: 'opencode', cwd: '/repo', tty: 'ttys001' })).toBe(true);\n });\n\n it('returns true for full path opencode', () => {\n expect(adapter.canHandle({ pid: 2, command: '/usr/local/bin/opencode serve', cwd: '/repo', tty: 'ttys002' })).toBe(true);\n });\n\n it('returns true for opencode.exe with unix-style path', () => {\n expect(adapter.canHandle({ pid: 3, command: '/usr/bin/opencode.exe', cwd: '/repo', tty: 'ttys003' })).toBe(true);\n });\n\n it('returns false for non-opencode processes', () => {\n expect(adapter.canHandle({ pid: 4, command: 'node server.js', cwd: '/repo', tty: 'ttys004' })).toBe(false);\n });\n\n it('returns false when opencode appears only in path args', () => {\n expect(adapter.canHandle({\n pid: 5,\n command: 'node /projects/opencode-plugin/index.js',\n cwd: '/repo',\n tty: 'ttys005',\n })).toBe(false);\n });\n });\n\n describe('detectAgents', () => {\n it('returns empty list when no opencode processes running', async () => {\n mockedListAgentProcesses.mockReturnValue([]);\n\n const agents = await adapter.detectAgents();\n\n expect(agents).toEqual([]);\n expect(mockedListAgentProcesses).toHaveBeenCalledWith('opencode');\n });\n\n it('returns process-only agent when DB does not exist', async () => {\n const procs: ProcessInfo[] = [\n { pid: 100, command: 'opencode', cwd: '/repo', tty: 'ttys001' },\n ];\n mockedListAgentProcesses.mockReturnValue(procs);\n mockedEnrichProcesses.mockReturnValue(procs);\n\n const agents = await adapter.detectAgents();\n\n expect(agents).toHaveLength(1);\n expect(agents[0]).toMatchObject({\n type: 'opencode',\n status: AgentStatus.RUNNING,\n pid: 100,\n projectPath: '/repo',\n sessionId: 'pid-100',\n summary: 'OpenCode process running',\n });\n });\n\n it('returns process-only agent when no session matches CWD', async () => {\n const procs: ProcessInfo[] = [\n { pid: 100, command: 'opencode', cwd: '/repo', tty: 'ttys001' },\n ];\n mockedListAgentProcesses.mockReturnValue(procs);\n mockedEnrichProcesses.mockReturnValue(procs);\n\n fs.writeFileSync(dbPath, ''); // file exists but empty → sqlite throws\n const db = makeDb({ session: [] }); // no matching session\n vi.doMock('better-sqlite3', () => makeDbConstructor(db));\n (adapter as any).db = db;\n\n const agents = await adapter.detectAgents();\n\n expect(agents).toHaveLength(1);\n expect(agents[0]).toMatchObject({\n type: 'opencode',\n status: AgentStatus.RUNNING,\n sessionId: 'pid-100',\n });\n });\n\n it('returns waiting when assistant turn has time.completed set', async () => {\n const now = Date.now();\n const procs: ProcessInfo[] = [\n { pid: 200, command: 'opencode', cwd: '/my-project', tty: 'ttys002' },\n ];\n mockedListAgentProcesses.mockReturnValue(procs);\n mockedEnrichProcesses.mockReturnValue(procs);\n\n const db = makeDb({\n session: [{ id: 'sess-001', directory: '/my-project', time_created: now - 60000 }],\n lastMessage: { role: 'assistant', timeUpdated: now - 60_000 },\n lastAssistant: { completed: now - 30_000, errored: null },\n firstUserText: { text: 'Refactor the auth module' },\n });\n (adapter as any).db = db;\n\n const agents = await adapter.detectAgents();\n\n expect(agents).toHaveLength(1);\n expect(agents[0]).toMatchObject({\n type: 'opencode',\n status: AgentStatus.WAITING,\n sessionId: 'sess-001',\n summary: 'Refactor the auth module',\n });\n });\n\n it('returns running when assistant turn has no time.completed (in-progress, any age)', async () => {\n const now = Date.now();\n const procs: ProcessInfo[] = [\n { pid: 250, command: 'opencode', cwd: '/proj', tty: 'ttys004' },\n ];\n mockedListAgentProcesses.mockReturnValue(procs);\n mockedEnrichProcesses.mockReturnValue(procs);\n\n const db = makeDb({\n session: [{ id: 'sess-tool', directory: '/proj', time_created: now - 180_000 }],\n lastMessage: { role: 'assistant', timeUpdated: now - 120_000 },\n lastAssistant: { completed: null, errored: null },\n firstUserText: { text: 'Run the build' },\n });\n (adapter as any).db = db;\n\n const agents = await adapter.detectAgents();\n\n expect(agents[0]).toMatchObject({\n status: AgentStatus.RUNNING,\n sessionId: 'sess-tool',\n });\n });\n\n it('returns running between steps (the bug fix) — no time.completed even during quiet moment', async () => {\n const now = Date.now();\n const procs: ProcessInfo[] = [\n { pid: 260, command: 'opencode', cwd: '/proj-b', tty: 'ttys005' },\n ];\n mockedListAgentProcesses.mockReturnValue(procs);\n mockedEnrichProcesses.mockReturnValue(procs);\n\n const db = makeDb({\n session: [{ id: 'sess-mid', directory: '/proj-b', time_created: now - 60_000 }],\n lastMessage: { role: 'assistant', timeUpdated: now - 45_000 },\n lastAssistant: { completed: null, errored: null },\n firstUserText: null,\n });\n (adapter as any).db = db;\n\n const agents = await adapter.detectAgents();\n expect(agents[0].status).toBe(AgentStatus.RUNNING);\n });\n\n it('returns waiting even when latest user message was metadata-updated after assistant completion', async () => {\n // Regression: OpenCode updates user.message.time_updated when appending\n // summary diffs after a turn finishes. Ordering by time_created (not\n // time_updated) keeps the assistant message correctly identified as latest.\n const now = Date.now();\n const procs: ProcessInfo[] = [\n { pid: 270, command: 'opencode', cwd: '/proj-c', tty: 'ttys006' },\n ];\n mockedListAgentProcesses.mockReturnValue(procs);\n mockedEnrichProcesses.mockReturnValue(procs);\n\n // makeDb returns lastMessage from the time_created-ordered query — supply the assistant.\n const db = makeDb({\n session: [{ id: 'sess-meta', directory: '/proj-c', time_created: now - 120_000 }],\n lastMessage: { role: 'assistant', timeUpdated: now - 30_000 },\n lastAssistant: { completed: now - 30_000, errored: null },\n firstUserText: null,\n });\n (adapter as any).db = db;\n\n const agents = await adapter.detectAgents();\n expect(agents[0].status).toBe(AgentStatus.WAITING);\n });\n\n it('returns running agent when last role is user (no assistant message yet)', async () => {\n const now = Date.now();\n const procs: ProcessInfo[] = [\n { pid: 300, command: 'opencode', cwd: '/work', tty: 'ttys003' },\n ];\n mockedListAgentProcesses.mockReturnValue(procs);\n mockedEnrichProcesses.mockReturnValue(procs);\n\n const db = makeDb({\n session: [{ id: 'sess-002', directory: '/work', time_created: now - 30000 }],\n lastMessage: { role: 'user', timeUpdated: now - 30_000 },\n lastAssistant: null,\n firstUserText: { text: 'Add unit tests' },\n });\n (adapter as any).db = db;\n\n const agents = await adapter.detectAgents();\n\n expect(agents[0]).toMatchObject({\n status: AgentStatus.RUNNING,\n sessionId: 'sess-002',\n });\n });\n\n it('returns idle agent when last activity exceeds threshold', async () => {\n const now = Date.now();\n const staleTime = now - 10 * 60 * 1000; // 10 minutes ago\n const procs: ProcessInfo[] = [\n { pid: 400, command: 'opencode', cwd: '/old-work', tty: 'ttys004' },\n ];\n mockedListAgentProcesses.mockReturnValue(procs);\n mockedEnrichProcesses.mockReturnValue(procs);\n\n const db = makeDb({\n session: [{ id: 'sess-003', directory: '/old-work', time_created: staleTime }],\n lastMessage: { role: 'assistant', timeUpdated: staleTime },\n lastAssistant: { completed: staleTime, errored: null },\n firstUserText: null,\n });\n (adapter as any).db = db;\n\n const agents = await adapter.detectAgents();\n\n expect(agents[0]).toMatchObject({\n status: AgentStatus.IDLE,\n sessionId: 'sess-003',\n });\n });\n });\n\n describe('getConversation', () => {\n it('returns empty array for invalid session ref', () => {\n const messages = adapter.getConversation('/no-separator-here');\n expect(messages).toEqual([]);\n });\n\n it('returns text messages from session parts', () => {\n const db = makeDb({\n parts: [\n { role: 'user', partData: JSON.stringify({ type: 'text', text: 'Hello agent' }), timeCreated: 1000 },\n { role: 'assistant', partData: JSON.stringify({ type: 'text', text: 'Hi, how can I help?' }), timeCreated: 2000 },\n ],\n });\n (adapter as any).db = db;\n\n const ref = `${dbPath}::sess-abc`;\n const messages = adapter.getConversation(ref);\n\n expect(messages).toHaveLength(2);\n expect(messages[0]).toEqual({ role: 'user', content: 'Hello agent' });\n expect(messages[1]).toEqual({ role: 'assistant', content: 'Hi, how can I help?' });\n });\n\n it('skips reasoning parts when verbose is false', () => {\n const db = makeDb({\n parts: [\n { role: 'assistant', partData: JSON.stringify({ type: 'reasoning', reasoning: 'internal thought' }), timeCreated: 1000 },\n { role: 'assistant', partData: JSON.stringify({ type: 'text', text: 'Answer' }), timeCreated: 2000 },\n ],\n });\n (adapter as any).db = db;\n\n const messages = adapter.getConversation(`${dbPath}::sess-x`);\n expect(messages).toHaveLength(1);\n expect(messages[0].content).toBe('Answer');\n });\n\n it('includes reasoning and tool parts when verbose is true', () => {\n const db = makeDb({\n parts: [\n { role: 'assistant', partData: JSON.stringify({ type: 'reasoning', reasoning: 'my thinking' }), timeCreated: 1000 },\n { role: 'assistant', partData: JSON.stringify({ type: 'tool', tool: 'read_file' }), timeCreated: 2000 },\n ],\n });\n (adapter as any).db = db;\n\n const messages = adapter.getConversation(`${dbPath}::sess-y`, { verbose: true });\n expect(messages).toHaveLength(2);\n expect(messages[0].content).toContain('my thinking');\n expect(messages[1].content).toContain('read_file');\n });\n\n it('returns empty array when DB cannot be opened', () => {\n (adapter as any).db = null;\n const messages = adapter.getConversation(`${dbPath}::sess-z`);\n expect(messages).toEqual([]);\n });\n });\n\n describe('listSessions', () => {\n it('returns empty array when DB does not exist', async () => {\n const sessions = await adapter.listSessions();\n expect(sessions).toEqual([]);\n });\n\n it('returns all sessions from DB', async () => {\n const now = Date.now();\n const db = makeDb({\n session: [\n { id: 'sess-a', directory: '/proj-a', time_created: now - 3000 },\n { id: 'sess-b', directory: '/proj-b', time_created: now - 6000 },\n ],\n lastMessage: null,\n lastAssistant: null,\n firstUserText: { text: 'Build the feature' },\n });\n (adapter as any).db = db;\n\n const sessions = await adapter.listSessions();\n\n expect(sessions).toHaveLength(2);\n expect(sessions[0]).toMatchObject({ type: 'opencode', sessionId: 'sess-a', cwd: '/proj-a' });\n expect(sessions[1]).toMatchObject({ type: 'opencode', sessionId: 'sess-b', cwd: '/proj-b' });\n expect(sessions[0].sessionFilePath).toContain('sess-a');\n });\n\n it('filters sessions by cwd when opts.cwd is set', async () => {\n const now = Date.now();\n const db = makeDb({\n session: [\n { id: 'sess-a', directory: '/proj-a', time_created: now - 3000 },\n { id: 'sess-b', directory: '/proj-b', time_created: now - 6000 },\n ],\n lastMessage: null,\n lastAssistant: null,\n firstUserText: null,\n });\n (adapter as any).db = db;\n\n const sessions = await adapter.listSessions({ cwd: '/proj-a' });\n\n expect(sessions).toHaveLength(1);\n expect(sessions[0].sessionId).toBe('sess-a');\n });\n\n it('uses session time_created as startedAt and lastActive when no parts exist', async () => {\n const timeCreated = Date.now() - 120000;\n const db = makeDb({\n session: [{ id: 'sess-c', directory: '/repo', time_created: timeCreated }],\n lastMessage: null,\n lastAssistant: null,\n firstUserText: null,\n });\n (adapter as any).db = db;\n\n const [session] = await adapter.listSessions();\n\n expect(session.startedAt.getTime()).toBeCloseTo(timeCreated, -2);\n expect(session.lastActive.getTime()).toBeCloseTo(timeCreated, -2);\n });\n });\n});\n"],"names":["fs","path","OpenCodeAdapter","AgentStatus","listAgentProcesses","enrichProcesses","generateAgentName","os","vi","mock","fn","matchProcessesToSessions","mockedListAgentProcesses","mockedEnrichProcesses","mockedGenerateAgentName","makeDb","queries","prepareImpl","sql","normalized","replace","trim","toLowerCase","includes","all","session","map","r","id","directory","timeCreated","time_created","get","dir","match","find","undefined","maxUpdated","lastMessage","timeUpdated","lastAssistant","firstUserText","parts","prepare","close","makeDbConstructor","db","mockReturnValue","describe","adapter","tmpDir","dbPath","beforeEach","mockReset","mockImplementation","procs","cwd","pid","folder","basename","mkdtempSync","join","tmpdir","afterEach","rmSync","recursive","force","restoreAllMocks","it","expect","type","toBe","canHandle","command","tty","agents","detectAgents","toEqual","toHaveBeenCalledWith","toHaveLength","toMatchObject","status","RUNNING","projectPath","sessionId","summary","writeFileSync","doMock","now","Date","role","completed","errored","text","WAITING","staleTime","IDLE","messages","getConversation","partData","JSON","stringify","ref","content","reasoning","tool","verbose","toContain","sessions","listSessions","sessionFilePath","startedAt","getTime","toBeCloseTo","lastActive"],"mappings":"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"}
@@ -0,0 +1,159 @@
1
+ import { execFile } from 'child_process';
2
+ import { TmuxManager } from '../../terminal/TmuxManager.js';
3
+ vi.mock('child_process', ()=>({
4
+ execFile: vi.fn()
5
+ }));
6
+ const mockedExecFile = execFile;
7
+ /** Drive the promisified execFile mock with a per-call handler. */ function setExecFileHandler(handler) {
8
+ mockedExecFile.mockImplementation((_cmd, _args, cb)=>{
9
+ const result = handler(_cmd, _args);
10
+ if (result instanceof Error) cb(result);
11
+ else cb(null, {
12
+ stdout: result,
13
+ stderr: ''
14
+ });
15
+ });
16
+ }
17
+ describe('TmuxManager', ()=>{
18
+ let tmux;
19
+ beforeEach(()=>{
20
+ tmux = new TmuxManager();
21
+ mockedExecFile.mockReset();
22
+ });
23
+ describe('isAvailable', ()=>{
24
+ it('returns true when `tmux -V` succeeds', async ()=>{
25
+ setExecFileHandler(()=>'tmux 3.4');
26
+ expect(await tmux.isAvailable()).toBe(true);
27
+ });
28
+ it('returns false when tmux is missing', async ()=>{
29
+ setExecFileHandler(()=>new Error('ENOENT'));
30
+ expect(await tmux.isAvailable()).toBe(false);
31
+ });
32
+ });
33
+ describe('sessionExists', ()=>{
34
+ it('returns true when has-session succeeds', async ()=>{
35
+ setExecFileHandler(()=>'');
36
+ expect(await tmux.sessionExists('foo')).toBe(true);
37
+ });
38
+ it('returns false when has-session fails', async ()=>{
39
+ setExecFileHandler(()=>new Error('no session'));
40
+ expect(await tmux.sessionExists('foo')).toBe(false);
41
+ });
42
+ });
43
+ describe('createSession', ()=>{
44
+ it('issues `tmux new-session -d -s <name> -c <cwd>`', async ()=>{
45
+ setExecFileHandler(()=>'');
46
+ await tmux.createSession('foo', '/work');
47
+ expect(mockedExecFile).toHaveBeenCalledWith('tmux', [
48
+ 'new-session',
49
+ '-d',
50
+ '-s',
51
+ 'foo',
52
+ '-c',
53
+ '/work'
54
+ ], expect.any(Function));
55
+ });
56
+ });
57
+ describe('sendKeys', ()=>{
58
+ it('appends Enter so the command runs', async ()=>{
59
+ setExecFileHandler(()=>'');
60
+ await tmux.sendKeys('foo', 'claude');
61
+ expect(mockedExecFile).toHaveBeenCalledWith('tmux', [
62
+ 'send-keys',
63
+ '-t',
64
+ 'foo',
65
+ 'claude',
66
+ 'Enter'
67
+ ], expect.any(Function));
68
+ });
69
+ });
70
+ describe('killSession', ()=>{
71
+ it('swallows errors when session is already gone', async ()=>{
72
+ setExecFileHandler(()=>new Error("can't find session"));
73
+ await expect(tmux.killSession('foo')).resolves.toBeUndefined();
74
+ });
75
+ });
76
+ describe('findAgentPid', ()=>{
77
+ const matchesClaude = (cmd)=>cmd.split(/\s+/)[0]?.endsWith('claude') ?? false;
78
+ it('returns null when the session has no pane', async ()=>{
79
+ setExecFileHandler((cmd, args)=>{
80
+ if (args[0] === 'list-panes') return new Error('no session');
81
+ return '';
82
+ });
83
+ expect(await tmux.findAgentPid('foo', matchesClaude)).toBeNull();
84
+ });
85
+ it('returns null when no descendant matches', async ()=>{
86
+ // pane PID 100; child 200 is "node /unrelated" — no match
87
+ setExecFileHandler((cmd, args)=>{
88
+ if (cmd === 'tmux' && args[0] === 'list-panes') return '100\n';
89
+ if (cmd === 'pgrep' && args[1] === '100') return '200\n';
90
+ if (cmd === 'pgrep') return new Error('no children');
91
+ if (cmd === 'ps' && args[1] === '200') return 'node /unrelated';
92
+ return '';
93
+ });
94
+ expect(await tmux.findAgentPid('foo', matchesClaude)).toBeNull();
95
+ });
96
+ it('returns the matching descendant when found', async ()=>{
97
+ // pane 100 → child 200 (claude) — no grandchildren
98
+ setExecFileHandler((cmd, args)=>{
99
+ if (cmd === 'tmux') return '100\n';
100
+ if (cmd === 'pgrep' && args[1] === '100') return '200\n';
101
+ if (cmd === 'pgrep') return new Error('no children');
102
+ if (cmd === 'ps' && args[1] === '200') return 'claude';
103
+ return '';
104
+ });
105
+ expect(await tmux.findAgentPid('foo', matchesClaude)).toBe(200);
106
+ });
107
+ it('prefers the deepest match (wrapper case)', async ()=>{
108
+ // pane 100 → 200 (claude wrapper, matches) → 300 (claude, matches, deeper)
109
+ setExecFileHandler((cmd, args)=>{
110
+ if (cmd === 'tmux') return '100\n';
111
+ if (cmd === 'pgrep' && args[1] === '100') return '200\n';
112
+ if (cmd === 'pgrep' && args[1] === '200') return '300\n';
113
+ if (cmd === 'pgrep') return new Error('no children');
114
+ if (cmd === 'ps' && args[1] === '200') return '/usr/local/bin/claude';
115
+ if (cmd === 'ps' && args[1] === '300') return '/usr/local/lib/claude';
116
+ return '';
117
+ });
118
+ expect(await tmux.findAgentPid('foo', matchesClaude)).toBe(300);
119
+ });
120
+ it('skips non-matching subprocesses (MCP child case)', async ()=>{
121
+ // pane 100 → 200 (claude, matches) → 300 (mcp-server, no match)
122
+ setExecFileHandler((cmd, args)=>{
123
+ if (cmd === 'tmux') return '100\n';
124
+ if (cmd === 'pgrep' && args[1] === '100') return '200\n';
125
+ if (cmd === 'pgrep' && args[1] === '200') return '300\n';
126
+ if (cmd === 'pgrep') return new Error('no children');
127
+ if (cmd === 'ps' && args[1] === '200') return 'claude';
128
+ if (cmd === 'ps' && args[1] === '300') return 'node mcp-server.js';
129
+ return '';
130
+ });
131
+ expect(await tmux.findAgentPid('foo', matchesClaude)).toBe(200);
132
+ });
133
+ it('handles the gemini Node-script shape via a token-scan matcher', async ()=>{
134
+ const matchesGemini = (cmd)=>cmd.split(/\s+/).some((t)=>t.endsWith('/gemini') || t === 'gemini');
135
+ setExecFileHandler((cmd, args)=>{
136
+ if (cmd === 'tmux') return '100\n';
137
+ if (cmd === 'pgrep' && args[1] === '100') return '200\n';
138
+ if (cmd === 'pgrep') return new Error('no children');
139
+ if (cmd === 'ps' && args[1] === '200') return 'node /opt/homebrew/bin/gemini --foo';
140
+ return '';
141
+ });
142
+ expect(await tmux.findAgentPid('foo', matchesGemini)).toBe(200);
143
+ });
144
+ it('handles multiple children at the same level', async ()=>{
145
+ // pane 100 → [200 (nope), 201 (claude)]
146
+ setExecFileHandler((cmd, args)=>{
147
+ if (cmd === 'tmux') return '100\n';
148
+ if (cmd === 'pgrep' && args[1] === '100') return '200\n201\n';
149
+ if (cmd === 'pgrep') return new Error('no children');
150
+ if (cmd === 'ps' && args[1] === '200') return 'bash';
151
+ if (cmd === 'ps' && args[1] === '201') return 'claude';
152
+ return '';
153
+ });
154
+ expect(await tmux.findAgentPid('foo', matchesClaude)).toBe(201);
155
+ });
156
+ });
157
+ });
158
+
159
+ //# sourceMappingURL=TmuxManager.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/__tests__/terminal/TmuxManager.test.ts"],"sourcesContent":["import { execFile } from 'child_process';\nimport type { MockedFunction } from 'vitest';\nimport { TmuxManager } from '../../terminal/TmuxManager.js';\n\nvi.mock('child_process', () => ({\n execFile: vi.fn(),\n}));\n\ntype ExecFileCb = (err: Error | null, result?: { stdout: string; stderr: string }) => void;\nconst mockedExecFile = execFile as unknown as MockedFunction<\n (cmd: string, args: string[], cb: ExecFileCb) => void\n>;\n\n/** Drive the promisified execFile mock with a per-call handler. */\nfunction setExecFileHandler(handler: (cmd: string, args: string[]) => string | Error) {\n mockedExecFile.mockImplementation((_cmd, _args, cb) => {\n const result = handler(_cmd, _args);\n if (result instanceof Error) cb(result);\n else cb(null, { stdout: result, stderr: '' });\n });\n}\n\ndescribe('TmuxManager', () => {\n let tmux: TmuxManager;\n\n beforeEach(() => {\n tmux = new TmuxManager();\n mockedExecFile.mockReset();\n });\n\n describe('isAvailable', () => {\n it('returns true when `tmux -V` succeeds', async () => {\n setExecFileHandler(() => 'tmux 3.4');\n expect(await tmux.isAvailable()).toBe(true);\n });\n\n it('returns false when tmux is missing', async () => {\n setExecFileHandler(() => new Error('ENOENT'));\n expect(await tmux.isAvailable()).toBe(false);\n });\n });\n\n describe('sessionExists', () => {\n it('returns true when has-session succeeds', async () => {\n setExecFileHandler(() => '');\n expect(await tmux.sessionExists('foo')).toBe(true);\n });\n\n it('returns false when has-session fails', async () => {\n setExecFileHandler(() => new Error('no session'));\n expect(await tmux.sessionExists('foo')).toBe(false);\n });\n });\n\n describe('createSession', () => {\n it('issues `tmux new-session -d -s <name> -c <cwd>`', async () => {\n setExecFileHandler(() => '');\n await tmux.createSession('foo', '/work');\n expect(mockedExecFile).toHaveBeenCalledWith(\n 'tmux',\n ['new-session', '-d', '-s', 'foo', '-c', '/work'],\n expect.any(Function),\n );\n });\n });\n\n describe('sendKeys', () => {\n it('appends Enter so the command runs', async () => {\n setExecFileHandler(() => '');\n await tmux.sendKeys('foo', 'claude');\n expect(mockedExecFile).toHaveBeenCalledWith(\n 'tmux',\n ['send-keys', '-t', 'foo', 'claude', 'Enter'],\n expect.any(Function),\n );\n });\n });\n\n describe('killSession', () => {\n it('swallows errors when session is already gone', async () => {\n setExecFileHandler(() => new Error(\"can't find session\"));\n await expect(tmux.killSession('foo')).resolves.toBeUndefined();\n });\n });\n\n describe('findAgentPid', () => {\n const matchesClaude = (cmd: string) => cmd.split(/\\s+/)[0]?.endsWith('claude') ?? false;\n\n it('returns null when the session has no pane', async () => {\n setExecFileHandler((cmd, args) => {\n if (args[0] === 'list-panes') return new Error('no session');\n return '';\n });\n expect(await tmux.findAgentPid('foo', matchesClaude)).toBeNull();\n });\n\n it('returns null when no descendant matches', async () => {\n // pane PID 100; child 200 is \"node /unrelated\" — no match\n setExecFileHandler((cmd, args) => {\n if (cmd === 'tmux' && args[0] === 'list-panes') return '100\\n';\n if (cmd === 'pgrep' && args[1] === '100') return '200\\n';\n if (cmd === 'pgrep') return new Error('no children');\n if (cmd === 'ps' && args[1] === '200') return 'node /unrelated';\n return '';\n });\n expect(await tmux.findAgentPid('foo', matchesClaude)).toBeNull();\n });\n\n it('returns the matching descendant when found', async () => {\n // pane 100 → child 200 (claude) — no grandchildren\n setExecFileHandler((cmd, args) => {\n if (cmd === 'tmux') return '100\\n';\n if (cmd === 'pgrep' && args[1] === '100') return '200\\n';\n if (cmd === 'pgrep') return new Error('no children');\n if (cmd === 'ps' && args[1] === '200') return 'claude';\n return '';\n });\n expect(await tmux.findAgentPid('foo', matchesClaude)).toBe(200);\n });\n\n it('prefers the deepest match (wrapper case)', async () => {\n // pane 100 → 200 (claude wrapper, matches) → 300 (claude, matches, deeper)\n setExecFileHandler((cmd, args) => {\n if (cmd === 'tmux') return '100\\n';\n if (cmd === 'pgrep' && args[1] === '100') return '200\\n';\n if (cmd === 'pgrep' && args[1] === '200') return '300\\n';\n if (cmd === 'pgrep') return new Error('no children');\n if (cmd === 'ps' && args[1] === '200') return '/usr/local/bin/claude';\n if (cmd === 'ps' && args[1] === '300') return '/usr/local/lib/claude';\n return '';\n });\n expect(await tmux.findAgentPid('foo', matchesClaude)).toBe(300);\n });\n\n it('skips non-matching subprocesses (MCP child case)', async () => {\n // pane 100 → 200 (claude, matches) → 300 (mcp-server, no match)\n setExecFileHandler((cmd, args) => {\n if (cmd === 'tmux') return '100\\n';\n if (cmd === 'pgrep' && args[1] === '100') return '200\\n';\n if (cmd === 'pgrep' && args[1] === '200') return '300\\n';\n if (cmd === 'pgrep') return new Error('no children');\n if (cmd === 'ps' && args[1] === '200') return 'claude';\n if (cmd === 'ps' && args[1] === '300') return 'node mcp-server.js';\n return '';\n });\n expect(await tmux.findAgentPid('foo', matchesClaude)).toBe(200);\n });\n\n it('handles the gemini Node-script shape via a token-scan matcher', async () => {\n const matchesGemini = (cmd: string) =>\n cmd.split(/\\s+/).some((t) => t.endsWith('/gemini') || t === 'gemini');\n setExecFileHandler((cmd, args) => {\n if (cmd === 'tmux') return '100\\n';\n if (cmd === 'pgrep' && args[1] === '100') return '200\\n';\n if (cmd === 'pgrep') return new Error('no children');\n if (cmd === 'ps' && args[1] === '200') return 'node /opt/homebrew/bin/gemini --foo';\n return '';\n });\n expect(await tmux.findAgentPid('foo', matchesGemini)).toBe(200);\n });\n\n it('handles multiple children at the same level', async () => {\n // pane 100 → [200 (nope), 201 (claude)]\n setExecFileHandler((cmd, args) => {\n if (cmd === 'tmux') return '100\\n';\n if (cmd === 'pgrep' && args[1] === '100') return '200\\n201\\n';\n if (cmd === 'pgrep') return new Error('no children');\n if (cmd === 'ps' && args[1] === '200') return 'bash';\n if (cmd === 'ps' && args[1] === '201') return 'claude';\n return '';\n });\n expect(await tmux.findAgentPid('foo', matchesClaude)).toBe(201);\n });\n });\n});\n"],"names":["execFile","TmuxManager","vi","mock","fn","mockedExecFile","setExecFileHandler","handler","mockImplementation","_cmd","_args","cb","result","Error","stdout","stderr","describe","tmux","beforeEach","mockReset","it","expect","isAvailable","toBe","sessionExists","createSession","toHaveBeenCalledWith","any","Function","sendKeys","killSession","resolves","toBeUndefined","matchesClaude","cmd","split","endsWith","args","findAgentPid","toBeNull","matchesGemini","some","t"],"mappings":"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"}
@@ -0,0 +1,162 @@
1
+ import { TtyWriter } from '../../terminal/TtyWriter.js';
2
+ import { TerminalType } from '../../terminal/TerminalFocusManager.js';
3
+ import { execFile } from 'child_process';
4
+ vi.mock('child_process', async ()=>{
5
+ const actual = await vi.importActual('child_process');
6
+ return {
7
+ ...actual,
8
+ execFile: vi.fn()
9
+ };
10
+ });
11
+ const mockedExecFile = execFile;
12
+ function mockExecFileSuccess(stdout = '') {
13
+ mockedExecFile.mockImplementation((...args)=>{
14
+ const cb = args[args.length - 1];
15
+ cb(null, {
16
+ stdout
17
+ }, '');
18
+ });
19
+ }
20
+ function mockExecFileError(message) {
21
+ mockedExecFile.mockImplementation((...args)=>{
22
+ const cb = args[args.length - 1];
23
+ cb(new Error(message), null, '');
24
+ });
25
+ }
26
+ describe('TtyWriter', ()=>{
27
+ beforeEach(()=>{
28
+ vi.clearAllMocks();
29
+ });
30
+ describe('tmux', ()=>{
31
+ const location = {
32
+ type: TerminalType.TMUX,
33
+ identifier: 'main:0.1',
34
+ tty: '/dev/ttys030'
35
+ };
36
+ it('sends message and Enter as separate tmux send-keys calls', async ()=>{
37
+ mockExecFileSuccess();
38
+ await TtyWriter.send(location, 'continue');
39
+ expect(mockedExecFile).toHaveBeenCalledWith('tmux', [
40
+ 'send-keys',
41
+ '-t',
42
+ 'main:0.1',
43
+ '-l',
44
+ 'continue'
45
+ ], expect.any(Function));
46
+ expect(mockedExecFile).toHaveBeenCalledWith('tmux', [
47
+ 'send-keys',
48
+ '-t',
49
+ 'main:0.1',
50
+ 'Enter'
51
+ ], expect.any(Function));
52
+ expect(mockedExecFile).toHaveBeenCalledTimes(2);
53
+ });
54
+ it('throws on tmux failure', async ()=>{
55
+ mockExecFileError('tmux not running');
56
+ await expect(TtyWriter.send(location, 'hello')).rejects.toThrow('tmux not running');
57
+ });
58
+ });
59
+ describe('iTerm2', ()=>{
60
+ const location = {
61
+ type: TerminalType.ITERM2,
62
+ identifier: '/dev/ttys030',
63
+ tty: '/dev/ttys030'
64
+ };
65
+ it('sends message via osascript with execFile (no shell)', async ()=>{
66
+ mockExecFileSuccess('ok');
67
+ await TtyWriter.send(location, 'hello');
68
+ // First call: send text without newline
69
+ expect(mockedExecFile).toHaveBeenCalledWith('osascript', [
70
+ '-e',
71
+ expect.stringContaining('write text "hello" newline no')
72
+ ], expect.any(Function));
73
+ // Second call: send Enter via separate write text with newline
74
+ expect(mockedExecFile).toHaveBeenCalledWith('osascript', [
75
+ '-e',
76
+ expect.stringContaining('write text "" newline yes')
77
+ ], expect.any(Function));
78
+ expect(mockedExecFile).toHaveBeenCalledTimes(2);
79
+ });
80
+ it('escapes special characters in message', async ()=>{
81
+ mockExecFileSuccess('ok');
82
+ await TtyWriter.send(location, 'say "hi" \\ there');
83
+ expect(mockedExecFile).toHaveBeenCalledWith('osascript', [
84
+ '-e',
85
+ expect.stringContaining('write text "say \\"hi\\" \\\\ there" newline no')
86
+ ], expect.any(Function));
87
+ });
88
+ it('escapes newlines in message', async ()=>{
89
+ mockExecFileSuccess('ok');
90
+ await TtyWriter.send(location, 'line1\nline2');
91
+ expect(mockedExecFile).toHaveBeenCalledWith('osascript', [
92
+ '-e',
93
+ expect.stringContaining('write text "line1\\nline2" newline no')
94
+ ], expect.any(Function));
95
+ });
96
+ it('throws when session not found', async ()=>{
97
+ mockExecFileSuccess('not_found');
98
+ await expect(TtyWriter.send(location, 'test')).rejects.toThrow('iTerm2 session not found');
99
+ });
100
+ it('throws when session disappears before Enter', async ()=>{
101
+ // First call succeeds (text sent), second returns not_found
102
+ let callCount = 0;
103
+ mockedExecFile.mockImplementation((...args)=>{
104
+ const cb = args[args.length - 1];
105
+ callCount++;
106
+ cb(null, {
107
+ stdout: callCount === 1 ? 'ok' : 'not_found'
108
+ }, '');
109
+ });
110
+ await expect(TtyWriter.send(location, 'test')).rejects.toThrow('iTerm2 session disappeared before Enter');
111
+ });
112
+ });
113
+ describe('Terminal.app', ()=>{
114
+ const location = {
115
+ type: TerminalType.TERMINAL_APP,
116
+ identifier: '/dev/ttys030',
117
+ tty: '/dev/ttys030'
118
+ };
119
+ it('sends message via do script (not System Events)', async ()=>{
120
+ mockExecFileSuccess('ok');
121
+ await TtyWriter.send(location, 'hello');
122
+ // First call: send text via do script
123
+ const firstCallArgs = mockedExecFile.mock.calls[0][1];
124
+ const textScript = firstCallArgs[1];
125
+ expect(textScript).toContain('do script "hello" in targetTab');
126
+ expect(textScript).not.toContain('keystroke');
127
+ expect(textScript).not.toContain('key code 36');
128
+ // Second call: send Enter via separate do script
129
+ const secondCallArgs = mockedExecFile.mock.calls[1][1];
130
+ const enterScript = secondCallArgs[1];
131
+ expect(enterScript).toContain('do script "" in targetTab');
132
+ expect(mockedExecFile).toHaveBeenCalledTimes(2);
133
+ });
134
+ it('throws when tab not found', async ()=>{
135
+ mockExecFileSuccess('not_found');
136
+ await expect(TtyWriter.send(location, 'test')).rejects.toThrow('Terminal.app tab not found');
137
+ });
138
+ it('throws when tab disappears before Enter', async ()=>{
139
+ let callCount = 0;
140
+ mockedExecFile.mockImplementation((...args)=>{
141
+ const cb = args[args.length - 1];
142
+ callCount++;
143
+ cb(null, {
144
+ stdout: callCount === 1 ? 'ok' : 'not_found'
145
+ }, '');
146
+ });
147
+ await expect(TtyWriter.send(location, 'test')).rejects.toThrow('Terminal.app tab disappeared before Enter');
148
+ });
149
+ });
150
+ describe('unsupported terminal', ()=>{
151
+ it('throws for unknown terminal type', async ()=>{
152
+ const location = {
153
+ type: TerminalType.UNKNOWN,
154
+ identifier: '',
155
+ tty: '/dev/ttys030'
156
+ };
157
+ await expect(TtyWriter.send(location, 'test')).rejects.toThrow('Cannot send input: unsupported terminal type');
158
+ });
159
+ });
160
+ });
161
+
162
+ //# sourceMappingURL=TtyWriter.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/__tests__/terminal/TtyWriter.test.ts"],"sourcesContent":["import type { Mock } from 'vitest';\n\nimport { TtyWriter } from '../../terminal/TtyWriter.js';\nimport { TerminalType } from '../../terminal/TerminalFocusManager.js';\nimport type { TerminalLocation } from '../../terminal/TerminalFocusManager.js';\nimport { execFile } from 'child_process';\n\nvi.mock('child_process', async () => {\n const actual = await vi.importActual<typeof import('child_process')>('child_process');\n return {\n ...actual,\n execFile: vi.fn(),\n };\n});\n\nconst mockedExecFile = execFile as unknown as Mock;\n\nfunction mockExecFileSuccess(stdout = '') {\n mockedExecFile.mockImplementation((...args: unknown[]) => {\n const cb = args[args.length - 1] as (err: Error | null, result: { stdout: string }, stderr: string) => void;\n cb(null, { stdout }, '');\n });\n}\n\nfunction mockExecFileError(message: string) {\n mockedExecFile.mockImplementation((...args: unknown[]) => {\n const cb = args[args.length - 1] as (err: Error | null, result: null, stderr: string) => void;\n cb(new Error(message), null, '');\n });\n}\n\ndescribe('TtyWriter', () => {\n beforeEach(() => {\n vi.clearAllMocks();\n });\n\n describe('tmux', () => {\n const location: TerminalLocation = {\n type: TerminalType.TMUX,\n identifier: 'main:0.1',\n tty: '/dev/ttys030',\n };\n\n it('sends message and Enter as separate tmux send-keys calls', async () => {\n mockExecFileSuccess();\n\n await TtyWriter.send(location, 'continue');\n\n expect(mockedExecFile).toHaveBeenCalledWith(\n 'tmux',\n ['send-keys', '-t', 'main:0.1', '-l', 'continue'],\n expect.any(Function),\n );\n expect(mockedExecFile).toHaveBeenCalledWith(\n 'tmux',\n ['send-keys', '-t', 'main:0.1', 'Enter'],\n expect.any(Function),\n );\n expect(mockedExecFile).toHaveBeenCalledTimes(2);\n });\n\n it('throws on tmux failure', async () => {\n mockExecFileError('tmux not running');\n\n await expect(TtyWriter.send(location, 'hello'))\n .rejects.toThrow('tmux not running');\n });\n });\n\n describe('iTerm2', () => {\n const location: TerminalLocation = {\n type: TerminalType.ITERM2,\n identifier: '/dev/ttys030',\n tty: '/dev/ttys030',\n };\n\n it('sends message via osascript with execFile (no shell)', async () => {\n mockExecFileSuccess('ok');\n\n await TtyWriter.send(location, 'hello');\n\n // First call: send text without newline\n expect(mockedExecFile).toHaveBeenCalledWith(\n 'osascript',\n ['-e', expect.stringContaining('write text \"hello\" newline no')],\n expect.any(Function),\n );\n // Second call: send Enter via separate write text with newline\n expect(mockedExecFile).toHaveBeenCalledWith(\n 'osascript',\n ['-e', expect.stringContaining('write text \"\" newline yes')],\n expect.any(Function),\n );\n expect(mockedExecFile).toHaveBeenCalledTimes(2);\n });\n\n it('escapes special characters in message', async () => {\n mockExecFileSuccess('ok');\n\n await TtyWriter.send(location, 'say \"hi\" \\\\ there');\n\n expect(mockedExecFile).toHaveBeenCalledWith(\n 'osascript',\n ['-e', expect.stringContaining('write text \"say \\\\\"hi\\\\\" \\\\\\\\ there\" newline no')],\n expect.any(Function),\n );\n });\n\n it('escapes newlines in message', async () => {\n mockExecFileSuccess('ok');\n\n await TtyWriter.send(location, 'line1\\nline2');\n\n expect(mockedExecFile).toHaveBeenCalledWith(\n 'osascript',\n ['-e', expect.stringContaining('write text \"line1\\\\nline2\" newline no')],\n expect.any(Function),\n );\n });\n\n it('throws when session not found', async () => {\n mockExecFileSuccess('not_found');\n\n await expect(TtyWriter.send(location, 'test'))\n .rejects.toThrow('iTerm2 session not found');\n });\n\n it('throws when session disappears before Enter', async () => {\n // First call succeeds (text sent), second returns not_found\n let callCount = 0;\n mockedExecFile.mockImplementation((...args: unknown[]) => {\n const cb = args[args.length - 1] as (err: Error | null, result: { stdout: string }, stderr: string) => void;\n callCount++;\n cb(null, { stdout: callCount === 1 ? 'ok' : 'not_found' }, '');\n });\n\n await expect(TtyWriter.send(location, 'test'))\n .rejects.toThrow('iTerm2 session disappeared before Enter');\n });\n });\n\n describe('Terminal.app', () => {\n const location: TerminalLocation = {\n type: TerminalType.TERMINAL_APP,\n identifier: '/dev/ttys030',\n tty: '/dev/ttys030',\n };\n\n it('sends message via do script (not System Events)', async () => {\n mockExecFileSuccess('ok');\n\n await TtyWriter.send(location, 'hello');\n\n // First call: send text via do script\n const firstCallArgs = (mockedExecFile.mock.calls[0] as unknown[])[1] as string[];\n const textScript = firstCallArgs[1];\n expect(textScript).toContain('do script \"hello\" in targetTab');\n expect(textScript).not.toContain('keystroke');\n expect(textScript).not.toContain('key code 36');\n\n // Second call: send Enter via separate do script\n const secondCallArgs = (mockedExecFile.mock.calls[1] as unknown[])[1] as string[];\n const enterScript = secondCallArgs[1];\n expect(enterScript).toContain('do script \"\" in targetTab');\n\n expect(mockedExecFile).toHaveBeenCalledTimes(2);\n });\n\n it('throws when tab not found', async () => {\n mockExecFileSuccess('not_found');\n\n await expect(TtyWriter.send(location, 'test'))\n .rejects.toThrow('Terminal.app tab not found');\n });\n\n it('throws when tab disappears before Enter', async () => {\n let callCount = 0;\n mockedExecFile.mockImplementation((...args: unknown[]) => {\n const cb = args[args.length - 1] as (err: Error | null, result: { stdout: string }, stderr: string) => void;\n callCount++;\n cb(null, { stdout: callCount === 1 ? 'ok' : 'not_found' }, '');\n });\n\n await expect(TtyWriter.send(location, 'test'))\n .rejects.toThrow('Terminal.app tab disappeared before Enter');\n });\n });\n\n describe('unsupported terminal', () => {\n it('throws for unknown terminal type', async () => {\n const location: TerminalLocation = {\n type: TerminalType.UNKNOWN,\n identifier: '',\n tty: '/dev/ttys030',\n };\n\n await expect(TtyWriter.send(location, 'test'))\n .rejects.toThrow('Cannot send input: unsupported terminal type');\n });\n });\n});\n"],"names":["TtyWriter","TerminalType","execFile","vi","mock","actual","importActual","fn","mockedExecFile","mockExecFileSuccess","stdout","mockImplementation","args","cb","length","mockExecFileError","message","Error","describe","beforeEach","clearAllMocks","location","type","TMUX","identifier","tty","it","send","expect","toHaveBeenCalledWith","any","Function","toHaveBeenCalledTimes","rejects","toThrow","ITERM2","stringContaining","callCount","TERMINAL_APP","firstCallArgs","calls","textScript","toContain","not","secondCallArgs","enterScript","UNKNOWN"],"mappings":"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"}