diff --git a/src/plugin/pty/notification-manager.ts b/src/plugin/pty/notification-manager.ts index 73e4173..9821534 100644 --- a/src/plugin/pty/notification-manager.ts +++ b/src/plugin/pty/notification-manager.ts @@ -20,6 +20,7 @@ export class NotificationManager { path: { id: session.parentSessionId }, body: { parts: [{ type: 'text', text: message }], + ...(session.parentAgent ? { agent: session.parentAgent } : {}), }, }) } catch { diff --git a/src/plugin/pty/session-lifecycle.ts b/src/plugin/pty/session-lifecycle.ts index ed294c8..9ee0e17 100644 --- a/src/plugin/pty/session-lifecycle.ts +++ b/src/plugin/pty/session-lifecycle.ts @@ -36,6 +36,7 @@ export class SessionLifecycleManager { pid: 0, // will be set after spawn createdAt: moment(), parentSessionId: opts.parentSessionId, + parentAgent: opts.parentAgent, notifyOnExit: opts.notifyOnExit ?? false, buffer, process: null, // will be set diff --git a/src/plugin/pty/tools/spawn.ts b/src/plugin/pty/tools/spawn.ts index c4f57f8..82d9e72 100644 --- a/src/plugin/pty/tools/spawn.ts +++ b/src/plugin/pty/tools/spawn.ts @@ -40,6 +40,7 @@ export const ptySpawn = tool({ title: args.title, description: args.description, parentSessionId: sessionId, + parentAgent: ctx.agent, notifyOnExit: args.notifyOnExit, }) diff --git a/src/plugin/pty/types.ts b/src/plugin/pty/types.ts index bf0c9d5..41cd263 100644 --- a/src/plugin/pty/types.ts +++ b/src/plugin/pty/types.ts @@ -18,6 +18,7 @@ export interface PTYSession { pid: number createdAt: moment.Moment parentSessionId: string + parentAgent?: string notifyOnExit: boolean buffer: RingBuffer process: IPty | null @@ -46,6 +47,7 @@ export interface SpawnOptions { title?: string description?: string parentSessionId: string + parentAgent?: string notifyOnExit?: boolean } diff --git a/test/notification-manager.test.ts b/test/notification-manager.test.ts new file mode 100644 index 0000000..2b4ad09 --- /dev/null +++ b/test/notification-manager.test.ts @@ -0,0 +1,77 @@ +import { describe, expect, it, mock } from 'bun:test' +import type { OpencodeClient } from '@opencode-ai/sdk' +import moment from 'moment' +import { RingBuffer } from '../src/plugin/pty/buffer.ts' +import { NotificationManager } from '../src/plugin/pty/notification-manager.ts' +import type { PTYSession } from '../src/plugin/pty/types.ts' + +type PromptPayload = { + path: { id: string } + body: { + parts: Array<{ type: string; text: string }> + agent?: string + } +} + +function createSession(overrides: Partial = {}): PTYSession { + const buffer = new RingBuffer() + buffer.append('line 1\nline 2\n') + + return { + id: 'pty_test', + title: 'Test Session', + description: 'Test session description', + command: 'echo', + args: ['hello'], + workdir: '/tmp', + status: 'running', + pid: 12345, + createdAt: moment(), + parentSessionId: 'parent-session-id', + parentAgent: 'agent-two', + notifyOnExit: true, + buffer, + process: null, + ...overrides, + } +} + +describe('NotificationManager', () => { + it('includes body.agent when originating agent is present', async () => { + const promptAsync = mock(async (_payload: PromptPayload) => {}) + const manager = new NotificationManager() + + manager.init({ session: { promptAsync } } as unknown as OpencodeClient) + + await manager.sendExitNotification(createSession({ parentAgent: 'agent-two' }), 0) + + expect(promptAsync).toHaveBeenCalledTimes(1) + const payload = promptAsync.mock.calls[0]![0] + + expect(payload.path).toEqual({ id: 'parent-session-id' }) + expect(payload.body.agent).toBe('agent-two') + expect(payload.body.parts).toHaveLength(1) + expect(payload.body.parts[0]?.text).toContain('') + expect(payload.body.parts[0]?.text).toContain('Use pty_read to check the full output.') + }) + + it('omits body.agent when originating agent is missing', async () => { + const promptAsync = mock(async (_payload: PromptPayload) => {}) + const manager = new NotificationManager() + + manager.init({ session: { promptAsync } } as unknown as OpencodeClient) + + await manager.sendExitNotification(createSession({ parentAgent: undefined }), 1) + + expect(promptAsync).toHaveBeenCalledTimes(1) + const payload = promptAsync.mock.calls[0]![0] + + expect(payload.path).toEqual({ id: 'parent-session-id' }) + expect(Object.hasOwn(payload.body, 'agent')).toBe(false) + expect(payload.body.parts).toHaveLength(1) + expect(payload.body.parts[0]?.text).toContain('') + expect(payload.body.parts[0]?.text).toContain( + 'Process failed. Use pty_read with the pattern parameter to search for errors in the output.' + ) + }) +}) diff --git a/test/pty-tools.test.ts b/test/pty-tools.test.ts index 80ed4c5..2ba4989 100644 --- a/test/pty-tools.test.ts +++ b/test/pty-tools.test.ts @@ -49,6 +49,7 @@ describe('PTY Tools', () => { args: ['hello'], description: 'Test session', parentSessionId: 'parent-session-id', + parentAgent: 'test-agent', workdir: undefined, env: undefined, title: undefined, @@ -92,6 +93,7 @@ describe('PTY Tools', () => { title: 'My Node Session', description: 'Running Node.js script', parentSessionId: 'parent-session-id', + parentAgent: 'test-agent', notifyOnExit: true, })