@ai-devkit/agent-manager 0.16.0 → 0.18.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 (105) hide show
  1. package/.swcrc +27 -0
  2. package/dist/AgentManager.js +72 -74
  3. package/dist/AgentManager.js.map +1 -1
  4. package/dist/__tests__/AgentManager.test.js +564 -0
  5. package/dist/__tests__/AgentManager.test.js.map +1 -0
  6. package/dist/__tests__/adapters/ClaudeCodeAdapter.test.js +2157 -0
  7. package/dist/__tests__/adapters/ClaudeCodeAdapter.test.js.map +1 -0
  8. package/dist/__tests__/adapters/CodexAdapter.test.js +1419 -0
  9. package/dist/__tests__/adapters/CodexAdapter.test.js.map +1 -0
  10. package/dist/__tests__/adapters/CopilotAdapter.test.js +500 -0
  11. package/dist/__tests__/adapters/CopilotAdapter.test.js.map +1 -0
  12. package/dist/__tests__/adapters/GeminiCliAdapter.test.js +1229 -0
  13. package/dist/__tests__/adapters/GeminiCliAdapter.test.js.map +1 -0
  14. package/dist/__tests__/adapters/OpenCodeAdapter.test.js +626 -0
  15. package/dist/__tests__/adapters/OpenCodeAdapter.test.js.map +1 -0
  16. package/dist/__tests__/adapters/PiAdapter.test.js +590 -0
  17. package/dist/__tests__/adapters/PiAdapter.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 -0
  39. package/dist/adapters/CodexAdapter.d.ts.map +1 -1
  40. package/dist/adapters/CodexAdapter.js +225 -141
  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 +2 -0
  47. package/dist/adapters/GeminiCliAdapter.d.ts.map +1 -1
  48. package/dist/adapters/GeminiCliAdapter.js +138 -156
  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/PiAdapter.d.ts +62 -0
  53. package/dist/adapters/PiAdapter.d.ts.map +1 -0
  54. package/dist/adapters/PiAdapter.js +450 -0
  55. package/dist/adapters/PiAdapter.js.map +1 -0
  56. package/dist/adapters/index.d.ts +2 -0
  57. package/dist/adapters/index.d.ts.map +1 -1
  58. package/dist/adapters/index.js +3 -0
  59. package/dist/adapters/index.js.map +1 -1
  60. package/dist/index.d.ts +2 -0
  61. package/dist/index.d.ts.map +1 -1
  62. package/dist/index.js +3 -0
  63. package/dist/index.js.map +1 -1
  64. package/dist/terminal/TerminalFocusManager.js +62 -49
  65. package/dist/terminal/TerminalFocusManager.js.map +1 -1
  66. package/dist/terminal/TmuxManager.js +59 -34
  67. package/dist/terminal/TmuxManager.js.map +1 -1
  68. package/dist/terminal/TtyWriter.js +37 -16
  69. package/dist/terminal/TtyWriter.js.map +1 -1
  70. package/dist/terminal/index.js +1 -0
  71. package/dist/terminal/index.js.map +1 -1
  72. package/dist/utils/AgentRegistry.js +41 -32
  73. package/dist/utils/AgentRegistry.js.map +1 -1
  74. package/dist/utils/ClaudeSessionParser.js +46 -87
  75. package/dist/utils/ClaudeSessionParser.js.map +1 -1
  76. package/dist/utils/agents.d.ts +1 -1
  77. package/dist/utils/agents.d.ts.map +1 -1
  78. package/dist/utils/agents.js +22 -11
  79. package/dist/utils/agents.js.map +1 -1
  80. package/dist/utils/applescript.js +3 -6
  81. package/dist/utils/applescript.js.map +1 -1
  82. package/dist/utils/index.js +1 -0
  83. package/dist/utils/index.js.map +1 -1
  84. package/dist/utils/matching.js +21 -29
  85. package/dist/utils/matching.js.map +1 -1
  86. package/dist/utils/process.js +77 -56
  87. package/dist/utils/process.js.map +1 -1
  88. package/dist/utils/session.js +20 -34
  89. package/dist/utils/session.js.map +1 -1
  90. package/dist/utils/sortAgents.js +6 -8
  91. package/dist/utils/sortAgents.js.map +1 -1
  92. package/package.json +11 -7
  93. package/src/__tests__/adapters/ClaudeCodeAdapter.test.ts +2 -0
  94. package/src/__tests__/adapters/CodexAdapter.test.ts +145 -0
  95. package/src/__tests__/adapters/CopilotAdapter.test.ts +411 -0
  96. package/src/__tests__/adapters/GeminiCliAdapter.test.ts +74 -1
  97. package/src/__tests__/adapters/PiAdapter.test.ts +435 -0
  98. package/src/adapters/AgentAdapter.ts +1 -1
  99. package/src/adapters/CodexAdapter.ts +164 -6
  100. package/src/adapters/CopilotAdapter.ts +453 -0
  101. package/src/adapters/GeminiCliAdapter.ts +42 -3
  102. package/src/adapters/PiAdapter.ts +597 -0
  103. package/src/adapters/index.ts +2 -0
  104. package/src/index.ts +2 -0
  105. package/src/utils/agents.ts +1 -1
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/__tests__/adapters/PiAdapter.test.ts"],"sourcesContent":["/**\n * Tests for PiAdapter\n */\n\nimport type { MockedFunction } from 'vitest';\nimport * as fs from 'fs';\nimport * as os from 'os';\nimport * as path from 'path';\n\nimport { PiAdapter } from '../../adapters/PiAdapter.js';\nimport type { ProcessInfo } from '../../adapters/AgentAdapter.js';\nimport { AgentStatus } from '../../adapters/AgentAdapter.js';\nimport { AgentRegistry } from '../../utils/AgentRegistry.js';\nimport { listAgentProcesses, enrichProcesses } from '../../utils/process.js';\nimport { matchProcessesToSessions, generateAgentName } from '../../utils/matching.js';\n\nvi.mock('../../utils/process.js', () => ({\n listAgentProcesses: vi.fn(),\n enrichProcesses: vi.fn(),\n}));\n\nvi.mock('../../utils/matching.js', () => ({\n matchProcessesToSessions: vi.fn(),\n generateAgentName: vi.fn(),\n}));\n\nconst mockedListAgentProcesses = listAgentProcesses as MockedFunction<typeof listAgentProcesses>;\nconst mockedEnrichProcesses = enrichProcesses as MockedFunction<typeof enrichProcesses>;\nconst mockedMatchProcessesToSessions = matchProcessesToSessions as MockedFunction<typeof matchProcessesToSessions>;\nconst mockedGenerateAgentName = generateAgentName as MockedFunction<typeof generateAgentName>;\n\ndescribe('PiAdapter', () => {\n let adapter: PiAdapter;\n let tmpHome: string;\n let sessionsDir: string;\n\n beforeEach(() => {\n tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), 'pi-adapter-test-'));\n process.env.HOME = tmpHome;\n sessionsDir = path.join(tmpHome, '.pi', 'agent', 'sessions');\n fs.mkdirSync(sessionsDir, { recursive: true });\n\n adapter = new PiAdapter(new AgentRegistry(path.join(tmpHome, 'agents.json')));\n mockedListAgentProcesses.mockReset();\n mockedEnrichProcesses.mockReset();\n mockedMatchProcessesToSessions.mockReset();\n mockedGenerateAgentName.mockReset();\n\n mockedEnrichProcesses.mockImplementation((procs) => procs);\n mockedMatchProcessesToSessions.mockReturnValue([]);\n mockedGenerateAgentName.mockImplementation((cwd: string, pid: number) => {\n const folder = path.basename(cwd) || 'unknown';\n return `${folder} (${pid})`;\n });\n });\n\n afterEach(() => {\n fs.rmSync(tmpHome, { recursive: true, force: true });\n });\n\n it('exposes pi type', () => {\n expect(adapter.type).toBe('pi');\n });\n\n it('identifies Pi commands without matching unrelated paths', () => {\n expect(adapter.canHandle({ pid: 1, command: 'pi', cwd: '/repo', tty: 'ttys001' })).toBe(true);\n expect(adapter.canHandle({ pid: 2, command: '/usr/local/bin/PI --model x', cwd: '/repo', tty: 'ttys002' })).toBe(true);\n expect(adapter.canHandle({ pid: 3, command: 'node /opt/pi/bin/pi.js', cwd: '/repo', tty: 'ttys003' })).toBe(true);\n expect(adapter.canHandle({ pid: 4, command: 'node /repo/feature-pi-adapter/script.js', cwd: '/repo', tty: 'ttys004' })).toBe(false);\n });\n\n it('maps a running Pi process to the tracker session for its PID', async () => {\n const cwd = '/repo/project-a';\n const proc = makeProcess({ pid: 101, cwd });\n const sessionFile = writePiSession(cwd, [\n { type: 'session_meta', timestamp: '2026-06-10T08:58:20.754Z', sessionId: 'sess-101', cwd },\n { role: 'user', timestamp: '2026-06-10T08:58:21.000Z', content: 'implement Pi adapter' },\n { role: 'assistant', timestamp: new Date().toISOString(), content: 'working on it' },\n ]);\n fs.writeFileSync(\n path.join(tmpHome, '.pi', 'agent', 'sessions.json'),\n JSON.stringify({ 101: sessionFile }),\n );\n mockedListAgentProcesses.mockReturnValue([proc]);\n\n const agents = await adapter.detectAgents();\n\n expect(agents).toHaveLength(1);\n expect(agents[0]).toMatchObject({\n type: 'pi',\n pid: 101,\n projectPath: cwd,\n sessionId: 'sess-101',\n summary: 'implement Pi adapter',\n status: AgentStatus.WAITING,\n sessionFilePath: sessionFile,\n });\n expect(mockedMatchProcessesToSessions).not.toHaveBeenCalled();\n });\n\n it('truncates long user prompts in detected agent summaries', async () => {\n const cwd = '/repo/project-long-summary';\n const proc = makeProcess({ pid: 112, cwd });\n const longPrompt = 'x'.repeat(140);\n const sessionFile = writePiSession(cwd, [\n { type: 'session', timestamp: '2026-06-10T08:58:20.754Z', id: 'sess-long', cwd },\n { role: 'user', timestamp: '2026-06-10T08:58:21.000Z', content: longPrompt },\n ]);\n fs.writeFileSync(\n path.join(tmpHome, '.pi', 'agent', 'sessions.json'),\n JSON.stringify({ 112: sessionFile }),\n );\n mockedListAgentProcesses.mockReturnValue([proc]);\n\n const agents = await adapter.detectAgents();\n\n expect(agents[0].summary).toHaveLength(120);\n expect(agents[0].summary.endsWith('...')).toBe(true);\n });\n\n it('uses the filename session id fallback and reports running when the latest message is from the user', async () => {\n const cwd = '/repo/project-filename-fallback';\n const proc = makeProcess({ pid: 113, cwd });\n const sessionFile = writePiSessionWithFileName(cwd, 'plain-session.jsonl', [\n { role: 'user', timestamp: new Date().toISOString(), content: 'still working' },\n ]);\n fs.writeFileSync(\n path.join(tmpHome, '.pi', 'agent', 'sessions.json'),\n JSON.stringify({ 113: sessionFile }),\n );\n mockedListAgentProcesses.mockReturnValue([proc]);\n\n const agents = await adapter.detectAgents();\n\n expect(agents[0]).toMatchObject({\n sessionId: 'plain-session',\n summary: 'still working',\n status: AgentStatus.RUNNING,\n });\n });\n\n it('falls back to legacy matching when sessions.json is missing', async () => {\n const cwd = '/repo/project-b';\n const proc = makeProcess({ pid: 202, cwd });\n const sessionFile = writePiSession(cwd, [\n { timestamp: '2026-06-10T08:58:20.754Z', sessionId: 'sess-202' },\n { role: 'user', timestamp: '2026-06-10T08:58:21.000Z', content: 'fallback matching please' },\n ]);\n mockedListAgentProcesses.mockReturnValue([proc]);\n mockedMatchProcessesToSessions.mockReturnValue([\n {\n process: proc,\n session: {\n sessionId: 'sess-202',\n filePath: sessionFile,\n projectDir: path.dirname(sessionFile),\n birthtimeMs: Date.now(),\n resolvedCwd: cwd,\n },\n deltaMs: 0,\n },\n ]);\n\n const agents = await adapter.detectAgents();\n\n expect(agents).toHaveLength(1);\n expect(agents[0]).toMatchObject({\n type: 'pi',\n pid: 202,\n projectPath: cwd,\n sessionId: 'sess-202',\n summary: 'fallback matching please',\n });\n expect(mockedMatchProcessesToSessions).toHaveBeenCalledWith(\n [proc],\n expect.arrayContaining([\n expect.objectContaining({ filePath: sessionFile, resolvedCwd: cwd }),\n ]),\n );\n });\n\n it('ignores malformed tracker metadata and still falls back to legacy matching', async () => {\n const cwd = '/repo/project-c';\n const proc = makeProcess({ pid: 303, cwd });\n const sessionFile = writePiSession(cwd, [\n { timestamp: '2026-06-10T08:58:20.754Z', sessionId: 'sess-303', cwd },\n { role: 'user', timestamp: '2026-06-10T08:58:21.000Z', content: 'recover from bad tracker' },\n ]);\n fs.writeFileSync(path.join(tmpHome, '.pi', 'agent', 'sessions.json'), '{bad json');\n mockedListAgentProcesses.mockReturnValue([proc]);\n mockedMatchProcessesToSessions.mockReturnValue([\n {\n process: proc,\n session: {\n sessionId: 'sess-303',\n filePath: sessionFile,\n projectDir: path.dirname(sessionFile),\n birthtimeMs: Date.now(),\n resolvedCwd: cwd,\n },\n deltaMs: 0,\n },\n ]);\n\n const agents = await adapter.detectAgents();\n\n expect(agents).toHaveLength(1);\n expect(agents[0].sessionId).toBe('sess-303');\n });\n\n it('falls back to legacy matching when a trusted tracker session is unparseable', async () => {\n const cwd = '/repo/project-bad-tracker-session';\n const proc = makeProcess({ pid: 304, cwd });\n const badSessionFile = writePiSessionWithFileName(cwd, 'bad.jsonl', ['{not json']);\n const fallbackSessionFile = writePiSession(cwd, [\n { timestamp: '2026-06-10T08:58:20.754Z', sessionId: 'sess-304', cwd },\n { role: 'user', timestamp: '2026-06-10T08:58:21.000Z', content: 'fallback after bad tracker session' },\n ]);\n fs.writeFileSync(\n path.join(tmpHome, '.pi', 'agent', 'sessions.json'),\n JSON.stringify({ 304: badSessionFile }),\n );\n mockedListAgentProcesses.mockReturnValue([proc]);\n mockedMatchProcessesToSessions.mockReturnValue([\n {\n process: proc,\n session: {\n sessionId: 'sess-304',\n filePath: fallbackSessionFile,\n projectDir: path.dirname(fallbackSessionFile),\n birthtimeMs: Date.now(),\n resolvedCwd: cwd,\n },\n deltaMs: 0,\n },\n ]);\n\n const agents = await adapter.detectAgents();\n\n expect(agents).toHaveLength(1);\n expect(agents[0]).toMatchObject({\n sessionId: 'sess-304',\n summary: 'fallback after bad tracker session',\n sessionFilePath: fallbackSessionFile,\n });\n expect(mockedMatchProcessesToSessions).toHaveBeenCalled();\n });\n\n it('does not trust tracker paths outside the Pi sessions directory', async () => {\n const cwd = '/repo/project-d';\n const proc = makeProcess({ pid: 404, cwd });\n const outside = path.join(tmpHome, 'outside.jsonl');\n fs.writeFileSync(outside, JSON.stringify({ role: 'user', content: 'nope' }));\n fs.writeFileSync(\n path.join(tmpHome, '.pi', 'agent', 'sessions.json'),\n JSON.stringify({ 404: outside }),\n );\n mockedListAgentProcesses.mockReturnValue([proc]);\n\n const agents = await adapter.detectAgents();\n\n expect(agents).toHaveLength(1);\n expect(agents[0]).toMatchObject({\n type: 'pi',\n pid: 404,\n sessionId: 'pid-404',\n summary: 'Pi process running',\n });\n });\n\n it('returns a process-only agent when no session can be matched', async () => {\n const proc = makeProcess({ pid: 505, cwd: '/repo/project-e' });\n mockedListAgentProcesses.mockReturnValue([proc]);\n\n const agents = await adapter.detectAgents();\n\n expect(agents).toHaveLength(1);\n expect(agents[0]).toMatchObject({\n type: 'pi',\n status: AgentStatus.RUNNING,\n pid: 505,\n projectPath: '/repo/project-e',\n sessionId: 'pid-505',\n summary: 'Pi process running',\n });\n });\n\n it('reads user and assistant conversation messages from JSONL', () => {\n const cwd = '/repo/project-f';\n const sessionFile = writePiSession(cwd, [\n { role: 'system', timestamp: '2026-06-10T08:58:20.000Z', content: 'hidden' },\n { role: 'user', timestamp: '2026-06-10T08:58:21.000Z', content: 'hello pi' },\n { type: 'assistant', timestamp: '2026-06-10T08:58:22.000Z', message: { content: 'hello human' } },\n '{not json',\n ]);\n\n expect(adapter.getConversation(sessionFile)).toEqual([\n { role: 'user', content: 'hello pi', timestamp: '2026-06-10T08:58:21.000Z' },\n { role: 'assistant', content: 'hello human', timestamp: '2026-06-10T08:58:22.000Z' },\n ]);\n });\n\n it('includes system entries only in verbose conversation mode', () => {\n const cwd = '/repo/project-verbose';\n const sessionFile = writePiSession(cwd, [\n { role: 'system', timestamp: '2026-06-10T08:58:20.000Z', content: 'model changed' },\n { role: 'user', timestamp: '2026-06-10T08:58:21.000Z', content: 'visible' },\n ]);\n\n expect(adapter.getConversation(sessionFile)).toEqual([\n { role: 'user', content: 'visible', timestamp: '2026-06-10T08:58:21.000Z' },\n ]);\n expect(adapter.getConversation(sessionFile, { verbose: true })).toEqual([\n { role: 'system', content: 'model changed', timestamp: '2026-06-10T08:58:20.000Z' },\n { role: 'user', content: 'visible', timestamp: '2026-06-10T08:58:21.000Z' },\n ]);\n });\n\n it('reads real Pi message entries with nested role and text parts', async () => {\n const cwd = '/repo/project-real';\n const proc = makeProcess({ pid: 606, cwd });\n const sessionFile = writePiSession(cwd, [\n { type: 'session', version: 3, id: 'sess-real', timestamp: '2026-06-10T13:27:17.581Z', cwd },\n { type: 'model_change', id: 'model-1', timestamp: '2026-06-10T13:27:17.655Z', modelId: 'claude-sonnet-4-6' },\n {\n type: 'message',\n id: 'msg-user',\n timestamp: '2026-06-10T13:27:37.975Z',\n message: {\n role: 'user',\n content: [{ type: 'text', text: 'hello' }],\n timestamp: 1781098057974,\n },\n },\n {\n type: 'message',\n id: 'msg-assistant',\n timestamp: '2026-06-10T13:27:40.161Z',\n message: {\n role: 'assistant',\n content: [{ type: 'text', text: 'Hello! How can I help you today?' }],\n provider: 'anthropic',\n model: 'claude-sonnet-4-6',\n timestamp: 1781098058012,\n },\n },\n ]);\n fs.writeFileSync(\n path.join(tmpHome, '.pi', 'agent', 'sessions.json'),\n JSON.stringify({ 606: sessionFile }),\n );\n mockedListAgentProcesses.mockReturnValue([proc]);\n\n expect(adapter.getConversation(sessionFile)).toEqual([\n { role: 'user', content: 'hello', timestamp: '2026-06-10T13:27:37.975Z' },\n { role: 'assistant', content: 'Hello! How can I help you today?', timestamp: '2026-06-10T13:27:40.161Z' },\n ]);\n\n const agents = await adapter.detectAgents();\n expect(agents[0]).toMatchObject({\n sessionId: 'sess-real',\n summary: 'hello',\n lastActive: new Date('2026-06-10T13:27:40.161Z'),\n });\n });\n\n it('lists historical sessions and applies cwd filtering', async () => {\n const matchingCwd = '/repo/project-g';\n const otherCwd = '/repo/project-h';\n const matchingSession = writePiSession(matchingCwd, [\n { timestamp: '2026-06-10T08:58:20.754Z', sessionId: 'sess-g', cwd: matchingCwd },\n { role: 'user', timestamp: '2026-06-10T08:58:21.000Z', content: 'first matching message' },\n ]);\n writePiSession(otherCwd, [\n { timestamp: '2026-06-10T08:58:20.754Z', sessionId: 'sess-h', cwd: otherCwd },\n { role: 'user', timestamp: '2026-06-10T08:58:21.000Z', content: 'other message' },\n ]);\n\n const sessions = await adapter.listSessions({ cwd: matchingCwd });\n\n expect(sessions).toEqual([\n expect.objectContaining({\n type: 'pi',\n sessionId: 'sess-g',\n cwd: matchingCwd,\n firstUserMessage: 'first matching message',\n sessionFilePath: matchingSession,\n }),\n ]);\n });\n\n function makeProcess(overrides: Partial<ProcessInfo>): ProcessInfo {\n return {\n pid: 1,\n command: 'pi',\n cwd: '/repo',\n tty: 'ttys001',\n startTime: new Date('2026-06-10T08:58:20.000Z'),\n ...overrides,\n };\n }\n\n function writePiSession(cwd: string, entries: Array<Record<string, unknown> | string>): string {\n const projectDir = path.join(sessionsDir, encodeProjectDir(cwd));\n fs.mkdirSync(projectDir, { recursive: true });\n const sessionId = entries\n .map((entry) => typeof entry === 'string' ? undefined : entry.sessionId)\n .find((value): value is string => typeof value === 'string') ?? cryptoRandomSessionId();\n const filePath = path.join(projectDir, `2026-06-10T08-58-20-754Z_${sessionId}.jsonl`);\n fs.writeFileSync(\n filePath,\n entries.map((entry) => typeof entry === 'string' ? entry : JSON.stringify(entry)).join('\\n'),\n );\n return filePath;\n }\n\n function writePiSessionWithFileName(cwd: string, fileName: string, entries: Array<Record<string, unknown> | string>): string {\n const projectDir = path.join(sessionsDir, encodeProjectDir(cwd));\n fs.mkdirSync(projectDir, { recursive: true });\n const filePath = path.join(projectDir, fileName);\n fs.writeFileSync(\n filePath,\n entries.map((entry) => typeof entry === 'string' ? entry : JSON.stringify(entry)).join('\\n'),\n );\n return filePath;\n }\n\n function encodeProjectDir(cwd: string): string {\n return cwd.replace(/\\//g, '-').replace(/^-?/, '--') + '--';\n }\n\n function cryptoRandomSessionId(): string {\n return `019eb0c1-06d2-71ed-90ee-${Math.random().toString(16).slice(2, 14).padEnd(12, '0')}`;\n }\n});\n"],"names":["fs","os","path","PiAdapter","AgentStatus","AgentRegistry","listAgentProcesses","enrichProcesses","matchProcessesToSessions","generateAgentName","vi","mock","fn","mockedListAgentProcesses","mockedEnrichProcesses","mockedMatchProcessesToSessions","mockedGenerateAgentName","describe","adapter","tmpHome","sessionsDir","beforeEach","mkdtempSync","join","tmpdir","process","env","HOME","mkdirSync","recursive","mockReset","mockImplementation","procs","mockReturnValue","cwd","pid","folder","basename","afterEach","rmSync","force","it","expect","type","toBe","canHandle","command","tty","proc","makeProcess","sessionFile","writePiSession","timestamp","sessionId","role","content","Date","toISOString","writeFileSync","JSON","stringify","agents","detectAgents","toHaveLength","toMatchObject","projectPath","summary","status","WAITING","sessionFilePath","not","toHaveBeenCalled","longPrompt","repeat","id","endsWith","writePiSessionWithFileName","RUNNING","session","filePath","projectDir","dirname","birthtimeMs","now","resolvedCwd","deltaMs","toHaveBeenCalledWith","arrayContaining","objectContaining","badSessionFile","fallbackSessionFile","outside","message","getConversation","toEqual","verbose","version","modelId","text","provider","model","lastActive","matchingCwd","otherCwd","matchingSession","sessions","listSessions","firstUserMessage","overrides","startTime","entries","encodeProjectDir","map","entry","undefined","find","value","cryptoRandomSessionId","fileName","replace","Math","random","toString","slice","padEnd"],"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":"AAEA,SAASA,SAAS,QAAQ,8BAA8B;AACxD,SAASC,YAAY,QAAQ,yCAAyC;AAEtE,SAASC,QAAQ,QAAQ,gBAAgB;AAEzCC,GAAGC,IAAI,CAAC,iBAAiB;IACrB,MAAMC,SAAS,MAAMF,GAAGG,YAAY,CAAiC;IACrE,OAAO;QACH,GAAGD,MAAM;QACTH,UAAUC,GAAGI,EAAE;IACnB;AACJ;AAEA,MAAMC,iBAAiBN;AAEvB,SAASO,oBAAoBC,SAAS,EAAE;IACpCF,eAAeG,kBAAkB,CAAC,CAAC,GAAGC;QAClC,MAAMC,KAAKD,IAAI,CAACA,KAAKE,MAAM,GAAG,EAAE;QAChCD,GAAG,MAAM;YAAEH;QAAO,GAAG;IACzB;AACJ;AAEA,SAASK,kBAAkBC,OAAe;IACtCR,eAAeG,kBAAkB,CAAC,CAAC,GAAGC;QAClC,MAAMC,KAAKD,IAAI,CAACA,KAAKE,MAAM,GAAG,EAAE;QAChCD,GAAG,IAAII,MAAMD,UAAU,MAAM;IACjC;AACJ;AAEAE,SAAS,aAAa;IAClBC,WAAW;QACPhB,GAAGiB,aAAa;IACpB;IAEAF,SAAS,QAAQ;QACb,MAAMG,WAA6B;YAC/BC,MAAMrB,aAAasB,IAAI;YACvBC,YAAY;YACZC,KAAK;QACT;QAEAC,GAAG,4DAA4D;YAC3DjB;YAEA,MAAMT,UAAU2B,IAAI,CAACN,UAAU;YAE/BO,OAAOpB,gBAAgBqB,oBAAoB,CACvC,QACA;gBAAC;gBAAa;gBAAM;gBAAY;gBAAM;aAAW,EACjDD,OAAOE,GAAG,CAACC;YAEfH,OAAOpB,gBAAgBqB,oBAAoB,CACvC,QACA;gBAAC;gBAAa;gBAAM;gBAAY;aAAQ,EACxCD,OAAOE,GAAG,CAACC;YAEfH,OAAOpB,gBAAgBwB,qBAAqB,CAAC;QACjD;QAEAN,GAAG,0BAA0B;YACzBX,kBAAkB;YAElB,MAAMa,OAAO5B,UAAU2B,IAAI,CAACN,UAAU,UACjCY,OAAO,CAACC,OAAO,CAAC;QACzB;IACJ;IAEAhB,SAAS,UAAU;QACf,MAAMG,WAA6B;YAC/BC,MAAMrB,aAAakC,MAAM;YACzBX,YAAY;YACZC,KAAK;QACT;QAEAC,GAAG,wDAAwD;YACvDjB,oBAAoB;YAEpB,MAAMT,UAAU2B,IAAI,CAACN,UAAU;YAE/B,wCAAwC;YACxCO,OAAOpB,gBAAgBqB,oBAAoB,CACvC,aACA;gBAAC;gBAAMD,OAAOQ,gBAAgB,CAAC;aAAiC,EAChER,OAAOE,GAAG,CAACC;YAEf,+DAA+D;YAC/DH,OAAOpB,gBAAgBqB,oBAAoB,CACvC,aACA;gBAAC;gBAAMD,OAAOQ,gBAAgB,CAAC;aAA6B,EAC5DR,OAAOE,GAAG,CAACC;YAEfH,OAAOpB,gBAAgBwB,qBAAqB,CAAC;QACjD;QAEAN,GAAG,yCAAyC;YACxCjB,oBAAoB;YAEpB,MAAMT,UAAU2B,IAAI,CAACN,UAAU;YAE/BO,OAAOpB,gBAAgBqB,oBAAoB,CACvC,aACA;gBAAC;gBAAMD,OAAOQ,gBAAgB,CAAC;aAAmD,EAClFR,OAAOE,GAAG,CAACC;QAEnB;QAEAL,GAAG,+BAA+B;YAC9BjB,oBAAoB;YAEpB,MAAMT,UAAU2B,IAAI,CAACN,UAAU;YAE/BO,OAAOpB,gBAAgBqB,oBAAoB,CACvC,aACA;gBAAC;gBAAMD,OAAOQ,gBAAgB,CAAC;aAAyC,EACxER,OAAOE,GAAG,CAACC;QAEnB;QAEAL,GAAG,iCAAiC;YAChCjB,oBAAoB;YAEpB,MAAMmB,OAAO5B,UAAU2B,IAAI,CAACN,UAAU,SACjCY,OAAO,CAACC,OAAO,CAAC;QACzB;QAEAR,GAAG,+CAA+C;YAC9C,4DAA4D;YAC5D,IAAIW,YAAY;YAChB7B,eAAeG,kBAAkB,CAAC,CAAC,GAAGC;gBAClC,MAAMC,KAAKD,IAAI,CAACA,KAAKE,MAAM,GAAG,EAAE;gBAChCuB;gBACAxB,GAAG,MAAM;oBAAEH,QAAQ2B,cAAc,IAAI,OAAO;gBAAY,GAAG;YAC/D;YAEA,MAAMT,OAAO5B,UAAU2B,IAAI,CAACN,UAAU,SACjCY,OAAO,CAACC,OAAO,CAAC;QACzB;IACJ;IAEAhB,SAAS,gBAAgB;QACrB,MAAMG,WAA6B;YAC/BC,MAAMrB,aAAaqC,YAAY;YAC/Bd,YAAY;YACZC,KAAK;QACT;QAEAC,GAAG,mDAAmD;YAClDjB,oBAAoB;YAEpB,MAAMT,UAAU2B,IAAI,CAACN,UAAU;YAE/B,sCAAsC;YACtC,MAAMkB,gBAAgB,AAAC/B,eAAeJ,IAAI,CAACoC,KAAK,CAAC,EAAE,AAAc,CAAC,EAAE;YACpE,MAAMC,aAAaF,aAAa,CAAC,EAAE;YACnCX,OAAOa,YAAYC,SAAS,CAAC;YAC7Bd,OAAOa,YAAYE,GAAG,CAACD,SAAS,CAAC;YACjCd,OAAOa,YAAYE,GAAG,CAACD,SAAS,CAAC;YAEjC,iDAAiD;YACjD,MAAME,iBAAiB,AAACpC,eAAeJ,IAAI,CAACoC,KAAK,CAAC,EAAE,AAAc,CAAC,EAAE;YACrE,MAAMK,cAAcD,cAAc,CAAC,EAAE;YACrChB,OAAOiB,aAAaH,SAAS,CAAC;YAE9Bd,OAAOpB,gBAAgBwB,qBAAqB,CAAC;QACjD;QAEAN,GAAG,6BAA6B;YAC5BjB,oBAAoB;YAEpB,MAAMmB,OAAO5B,UAAU2B,IAAI,CAACN,UAAU,SACjCY,OAAO,CAACC,OAAO,CAAC;QACzB;QAEAR,GAAG,2CAA2C;YAC1C,IAAIW,YAAY;YAChB7B,eAAeG,kBAAkB,CAAC,CAAC,GAAGC;gBAClC,MAAMC,KAAKD,IAAI,CAACA,KAAKE,MAAM,GAAG,EAAE;gBAChCuB;gBACAxB,GAAG,MAAM;oBAAEH,QAAQ2B,cAAc,IAAI,OAAO;gBAAY,GAAG;YAC/D;YAEA,MAAMT,OAAO5B,UAAU2B,IAAI,CAACN,UAAU,SACjCY,OAAO,CAACC,OAAO,CAAC;QACzB;IACJ;IAEAhB,SAAS,wBAAwB;QAC7BQ,GAAG,oCAAoC;YACnC,MAAML,WAA6B;gBAC/BC,MAAMrB,aAAa6C,OAAO;gBAC1BtB,YAAY;gBACZC,KAAK;YACT;YAEA,MAAMG,OAAO5B,UAAU2B,IAAI,CAACN,UAAU,SACjCY,OAAO,CAACC,OAAO,CAAC;QACzB;IACJ;AACJ"}