Skip to content

Commit 3ddc452

Browse files
committed
feat(server): /status endpoint for display-only plugin messages in the TUI
Creates an external ToolPart tied to a parent message so plugins can surface status text in the TUI without sending content to the LLM.
1 parent 601813d commit 3ddc452

File tree

1 file changed

+84
-0
lines changed

1 file changed

+84
-0
lines changed
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import { afterEach, describe, expect, mock, test } from "bun:test"
2+
import { Instance } from "../../src/project/instance"
3+
import { Server } from "../../src/server/server"
4+
import { Session } from "../../src/session"
5+
import { Log } from "../../src/util/log"
6+
import { tmpdir } from "../fixture/fixture"
7+
8+
Log.init({ print: false })
9+
10+
afterEach(async () => {
11+
mock.restore()
12+
await Instance.disposeAll()
13+
})
14+
15+
describe("session status route", () => {
16+
test("accepts status with messageID", async () => {
17+
await using tmp = await tmpdir({ git: true })
18+
await Instance.provide({
19+
directory: tmp.path,
20+
fn: async () => {
21+
const session = await Session.create({})
22+
const app = Server.Default().app
23+
24+
const res = await app.request(`/session/${session.id}/status`, {
25+
method: "POST",
26+
headers: { "Content-Type": "application/json" },
27+
body: JSON.stringify({ message: "working", messageID: "msg-123" }),
28+
})
29+
30+
expect(res.status).toBe(200)
31+
expect(await res.text()).toBe("ok")
32+
33+
await Session.remove(session.id)
34+
},
35+
})
36+
})
37+
38+
// Regression: messageID was required in the zod validator but oc status
39+
// only sends it when OPENCODE_MESSAGE_ID is set. Without this fix,
40+
// oc status calls without a messageID would 400.
41+
test("accepts status without messageID", async () => {
42+
await using tmp = await tmpdir({ git: true })
43+
await Instance.provide({
44+
directory: tmp.path,
45+
fn: async () => {
46+
const session = await Session.create({})
47+
const app = Server.Default().app
48+
49+
const res = await app.request(`/session/${session.id}/status`, {
50+
method: "POST",
51+
headers: { "Content-Type": "application/json" },
52+
body: JSON.stringify({ message: "working" }),
53+
})
54+
55+
// Without messageID the emitter is a no-op, but the route must not 400
56+
expect(res.status).toBe(200)
57+
expect(await res.text()).toBe("ok")
58+
59+
await Session.remove(session.id)
60+
},
61+
})
62+
})
63+
64+
test("rejects status without message field", async () => {
65+
await using tmp = await tmpdir({ git: true })
66+
await Instance.provide({
67+
directory: tmp.path,
68+
fn: async () => {
69+
const session = await Session.create({})
70+
const app = Server.Default().app
71+
72+
const res = await app.request(`/session/${session.id}/status`, {
73+
method: "POST",
74+
headers: { "Content-Type": "application/json" },
75+
body: JSON.stringify({}),
76+
})
77+
78+
expect(res.status).toBe(400)
79+
80+
await Session.remove(session.id)
81+
},
82+
})
83+
})
84+
})

0 commit comments

Comments
 (0)