Skip to content

Commit 91a1fdf

Browse files
authored
feat: terminal session exit by sending a message with the same agent used in the session (#24)
1 parent 82e1034 commit 91a1fdf

File tree

6 files changed

+84
-0
lines changed

6 files changed

+84
-0
lines changed

src/plugin/pty/notification-manager.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ export class NotificationManager {
2020
path: { id: session.parentSessionId },
2121
body: {
2222
parts: [{ type: 'text', text: message }],
23+
...(session.parentAgent ? { agent: session.parentAgent } : {}),
2324
},
2425
})
2526
} catch {

src/plugin/pty/session-lifecycle.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ export class SessionLifecycleManager {
3636
pid: 0, // will be set after spawn
3737
createdAt: moment(),
3838
parentSessionId: opts.parentSessionId,
39+
parentAgent: opts.parentAgent,
3940
notifyOnExit: opts.notifyOnExit ?? false,
4041
buffer,
4142
process: null, // will be set

src/plugin/pty/tools/spawn.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ export const ptySpawn = tool({
4040
title: args.title,
4141
description: args.description,
4242
parentSessionId: sessionId,
43+
parentAgent: ctx.agent,
4344
notifyOnExit: args.notifyOnExit,
4445
})
4546

src/plugin/pty/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ export interface PTYSession {
1818
pid: number
1919
createdAt: moment.Moment
2020
parentSessionId: string
21+
parentAgent?: string
2122
notifyOnExit: boolean
2223
buffer: RingBuffer
2324
process: IPty | null
@@ -46,6 +47,7 @@ export interface SpawnOptions {
4647
title?: string
4748
description?: string
4849
parentSessionId: string
50+
parentAgent?: string
4951
notifyOnExit?: boolean
5052
}
5153

test/notification-manager.test.ts

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import { describe, expect, it, mock } from 'bun:test'
2+
import type { OpencodeClient } from '@opencode-ai/sdk'
3+
import moment from 'moment'
4+
import { RingBuffer } from '../src/plugin/pty/buffer.ts'
5+
import { NotificationManager } from '../src/plugin/pty/notification-manager.ts'
6+
import type { PTYSession } from '../src/plugin/pty/types.ts'
7+
8+
type PromptPayload = {
9+
path: { id: string }
10+
body: {
11+
parts: Array<{ type: string; text: string }>
12+
agent?: string
13+
}
14+
}
15+
16+
function createSession(overrides: Partial<PTYSession> = {}): PTYSession {
17+
const buffer = new RingBuffer()
18+
buffer.append('line 1\nline 2\n')
19+
20+
return {
21+
id: 'pty_test',
22+
title: 'Test Session',
23+
description: 'Test session description',
24+
command: 'echo',
25+
args: ['hello'],
26+
workdir: '/tmp',
27+
status: 'running',
28+
pid: 12345,
29+
createdAt: moment(),
30+
parentSessionId: 'parent-session-id',
31+
parentAgent: 'agent-two',
32+
notifyOnExit: true,
33+
buffer,
34+
process: null,
35+
...overrides,
36+
}
37+
}
38+
39+
describe('NotificationManager', () => {
40+
it('includes body.agent when originating agent is present', async () => {
41+
const promptAsync = mock(async (_payload: PromptPayload) => {})
42+
const manager = new NotificationManager()
43+
44+
manager.init({ session: { promptAsync } } as unknown as OpencodeClient)
45+
46+
await manager.sendExitNotification(createSession({ parentAgent: 'agent-two' }), 0)
47+
48+
expect(promptAsync).toHaveBeenCalledTimes(1)
49+
const payload = promptAsync.mock.calls[0]![0]
50+
51+
expect(payload.path).toEqual({ id: 'parent-session-id' })
52+
expect(payload.body.agent).toBe('agent-two')
53+
expect(payload.body.parts).toHaveLength(1)
54+
expect(payload.body.parts[0]?.text).toContain('<pty_exited>')
55+
expect(payload.body.parts[0]?.text).toContain('Use pty_read to check the full output.')
56+
})
57+
58+
it('omits body.agent when originating agent is missing', async () => {
59+
const promptAsync = mock(async (_payload: PromptPayload) => {})
60+
const manager = new NotificationManager()
61+
62+
manager.init({ session: { promptAsync } } as unknown as OpencodeClient)
63+
64+
await manager.sendExitNotification(createSession({ parentAgent: undefined }), 1)
65+
66+
expect(promptAsync).toHaveBeenCalledTimes(1)
67+
const payload = promptAsync.mock.calls[0]![0]
68+
69+
expect(payload.path).toEqual({ id: 'parent-session-id' })
70+
expect(Object.hasOwn(payload.body, 'agent')).toBe(false)
71+
expect(payload.body.parts).toHaveLength(1)
72+
expect(payload.body.parts[0]?.text).toContain('<pty_exited>')
73+
expect(payload.body.parts[0]?.text).toContain(
74+
'Process failed. Use pty_read with the pattern parameter to search for errors in the output.'
75+
)
76+
})
77+
})

test/pty-tools.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ describe('PTY Tools', () => {
4949
args: ['hello'],
5050
description: 'Test session',
5151
parentSessionId: 'parent-session-id',
52+
parentAgent: 'test-agent',
5253
workdir: undefined,
5354
env: undefined,
5455
title: undefined,
@@ -92,6 +93,7 @@ describe('PTY Tools', () => {
9293
title: 'My Node Session',
9394
description: 'Running Node.js script',
9495
parentSessionId: 'parent-session-id',
96+
parentAgent: 'test-agent',
9597
notifyOnExit: true,
9698
})
9799

0 commit comments

Comments
 (0)