Skip to content

Commit 565a158

Browse files
sanchitrkclaude
andcommitted
feat(acp): announce session titles to the editor
ACP and `codework run` create sessions without a placeholder title so the harness names them from their first prompt. ACP sends a session_info_update when a prompt changes the title. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 7c7c66c commit 565a158

4 files changed

Lines changed: 37 additions & 7 deletions

File tree

‎packages/codework/src/acp/handlers.ts‎

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ export const make = Effect.gen(function* () {
100100
newSession: ({ cwd }) =>
101101
run(
102102
Effect.gen(function* () {
103-
const handle = yield* Session.create({ directory: cwd, hostDir: cwd, title: "ACP" });
103+
const handle = yield* Session.create({ directory: cwd, hostDir: cwd });
104104
const current = yield* selection(handle.id, cwd);
105105
return { sessionId: handle.id, configOptions: yield* Config.options(current) };
106106
}),
@@ -208,9 +208,18 @@ export const make = Effect.gen(function* () {
208208
Stream.runHead,
209209
Effect.forkScoped({ startImmediately: true }),
210210
);
211+
const before = (yield* handle.info).title;
211212
yield* handle.run(text);
212213
const end = yield* Fiber.join(settled);
213214

215+
// The harness titles a session from its first prompt; tell the editor.
216+
const { title } = yield* handle.info;
217+
if (title !== before) {
218+
yield* client
219+
.sessionUpdate({ sessionId: id, update: { sessionUpdate: "session_info_update", title } })
220+
.pipe(Effect.ignore);
221+
}
222+
214223
if (Option.isNone(end) || isInterrupted(end.value)) return { stopReason: "cancelled" as const };
215224
if (isFailed(end.value)) return yield* AcpRequestError.internalError(end.value.data.error.message);
216225
return { stopReason: reason === "length" ? ("max_tokens" as const) : ("end_turn" as const) };

‎packages/codework/src/cli/cmd/handlers/run.ts‎

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,6 @@ export default Runtime.handler(
145145
const info = Option.isSome(session)
146146
? yield* rpc["session.configure"]({ sessionId: Session.SessionSchema.ID.make(session.value), runtime })
147147
: yield* rpc["session.create"]({
148-
title: "CLI",
149148
runtime,
150149
...(Option.isNone(cwd) ? {} : { directory: cwd.value }),
151150
hostDir,
@@ -176,7 +175,6 @@ export default Runtime.handler(
176175
} else {
177176
const selected = yield* selectSandbox(selection);
178177
handle = yield* Session.create({
179-
title: "CLI",
180178
...runtime,
181179
...(selected === undefined ? {} : { sandbox: selected }),
182180
...(Option.isNone(cwd) ? {} : { directory: cwd.value }),

‎packages/codework/test/__artifacts__/acp.live.json‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
{
22
"pong": "end_turn",
3+
"titled": [
4+
"Reply with exactly: PONG"
5+
],
36
"notices": [
47
1,
58
1
@@ -28,6 +31,7 @@
2831
],
2932
"failed": {
3033
"code": -32603,
31-
"hasMessage": true
34+
"hasMessage": true,
35+
"title": "Summarise how sessions, plugins and sandboxes…"
3236
}
3337
}

‎packages/codework/test/acp.test.ts‎

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,11 @@ describe("codework acp", () => {
168168
sessionId,
169169
prompt: [{ type: "text", text: "Reply with exactly: PONG" }],
170170
});
171-
const pongText = text(yield* Queue.clear(updates), "agent_message_chunk");
171+
const pongUpdates = yield* Queue.clear(updates);
172+
const pongText = text(pongUpdates, "agent_message_chunk");
173+
const titled = pongUpdates.flatMap((update) =>
174+
update.sessionUpdate === "session_info_update" ? [update.title] : [],
175+
);
172176
expect(pongText).toContain("PONG");
173177

174178
const tool = yield* acp.agent.prompt({
@@ -206,6 +210,8 @@ describe("codework acp", () => {
206210
sessionId,
207211
record: {
208212
pong: pong.stopReason,
213+
// The first prompt names the untitled session, and the editor is told once.
214+
titled,
209215
// Once per prompt, however many exchanges the prompt takes.
210216
notices: [pongText, toolText].map(
211217
(answer) =>
@@ -252,9 +258,22 @@ describe("codework acp", () => {
252258
const { sessionId } = yield* acp.agent.createSession({ cwd: project, mcpServers: [] });
253259
yield* acp.agent.setSessionConfigOption({ sessionId, configId: "model", value: LIVE_MODEL });
254260
const result = yield* settle(
255-
acp.agent.prompt({ sessionId, prompt: [{ type: "text", text: "hi" }] }),
261+
acp.agent.prompt({
262+
sessionId,
263+
prompt: [
264+
{
265+
type: "text",
266+
text: "Summarise how sessions, plugins and sandboxes fit together in the codework harness.\nKeep it short.",
267+
},
268+
],
269+
}),
256270
);
257-
return "code" in result ? { code: result.code, hasMessage: Boolean(result.message) } : result;
271+
// Titled at promotion, before the provider call fails: first line, cut to 48 characters.
272+
const { sessions } = yield* acp.agent.listSessions({ cwd: project });
273+
return {
274+
...("code" in result ? { code: result.code, hasMessage: Boolean(result.message) } : result),
275+
title: sessions.find((session) => session.sessionId === sessionId)?.title,
276+
};
258277
}),
259278
{ OPENROUTER_API_KEY: "sk-or-invalid" },
260279
);

0 commit comments

Comments
 (0)