diff options
| author | Adam Malczewski <[email protected]> | 2026-07-01 03:30:42 +0900 |
|---|---|---|
| committer | Adam Malczewski <[email protected]> | 2026-07-01 03:30:42 +0900 |
| commit | 6bca0a8b65506239b0ce72d7f86ba96f825152b1 (patch) | |
| tree | 7b6b3d0edeb2239acbaa605ba898abaf1711a12c /packages/transport-http/src | |
| parent | 566c64033ad79538f9208fc3ef9477cd8a58f7da (diff) | |
| parent | a2e3bad36fd236835423e3e9124b8496b6d9c795 (diff) | |
| download | dispatch-6bca0a8b65506239b0ce72d7f86ba96f825152b1.tar.gz dispatch-6bca0a8b65506239b0ce72d7f86ba96f825152b1.zip | |
Merge branch 'feature/summon-title' into predev
Diffstat (limited to 'packages/transport-http/src')
| -rw-r--r-- | packages/transport-http/src/app.test.ts | 90 | ||||
| -rw-r--r-- | packages/transport-http/src/app.ts | 17 | ||||
| -rw-r--r-- | packages/transport-http/src/logic.ts | 6 |
3 files changed, 35 insertions, 78 deletions
diff --git a/packages/transport-http/src/app.test.ts b/packages/transport-http/src/app.test.ts index 9b1480d..0876cdb 100644 --- a/packages/transport-http/src/app.test.ts +++ b/packages/transport-http/src/app.test.ts @@ -800,17 +800,11 @@ describe("POST /chat", () => { expect(cap.received?.cwd).toBeUndefined(); }); - it("sets the conversation title from the request before the turn", async () => { - const calls: { conversationId: string; title: string }[] = []; - const store: ConversationStore = { - ...createFakeConversationStore(), - async setConversationTitle(conversationId, title) { - calls.push({ conversationId, title }); - }, - }; + it("forwards the title to the orchestrator", async () => { + const cap = createCapturingOrchestrator(); const app = createApp({ - conversationStore: store, - orchestrator: createFakeOrchestrator([]), + conversationStore: createFakeConversationStore(), + orchestrator: cap, credentialStore: createFakeCredentialStore([]), }); @@ -821,20 +815,15 @@ describe("POST /chat", () => { }); expect(res.status).toBe(200); - expect(calls).toEqual([{ conversationId: "conv1", title: "My Task" }]); + expect(cap.received).toBeDefined(); + expect(cap.received?.title).toBe("My Task"); }); - it("forwards a trimmed title to setConversationTitle", async () => { - const calls: { conversationId: string; title: string }[] = []; - const store: ConversationStore = { - ...createFakeConversationStore(), - async setConversationTitle(conversationId, title) { - calls.push({ conversationId, title }); - }, - }; + it("forwards a trimmed title to the orchestrator", async () => { + const cap = createCapturingOrchestrator(); const app = createApp({ - conversationStore: store, - orchestrator: createFakeOrchestrator([]), + conversationStore: createFakeConversationStore(), + orchestrator: cap, credentialStore: createFakeCredentialStore([]), }); @@ -845,20 +834,14 @@ describe("POST /chat", () => { }); expect(res.status).toBe(200); - expect(calls).toEqual([{ conversationId: "conv1", title: "spaced" }]); + expect(cap.received?.title).toBe("spaced"); }); - it("does not call setConversationTitle when title is omitted", async () => { - let setTitleCalled = false; - const store: ConversationStore = { - ...createFakeConversationStore(), - async setConversationTitle() { - setTitleCalled = true; - }, - }; + it("does not forward a title when omitted", async () => { + const cap = createCapturingOrchestrator(); const app = createApp({ - conversationStore: store, - orchestrator: createFakeOrchestrator([]), + conversationStore: createFakeConversationStore(), + orchestrator: cap, credentialStore: createFakeCredentialStore([]), }); @@ -869,20 +852,14 @@ describe("POST /chat", () => { }); expect(res.status).toBe(200); - expect(setTitleCalled).toBe(false); + expect(cap.received?.title).toBeUndefined(); }); - it("does not call setConversationTitle for a whitespace-only title", async () => { - let setTitleCalled = false; - const store: ConversationStore = { - ...createFakeConversationStore(), - async setConversationTitle() { - setTitleCalled = true; - }, - }; + it("does not forward a title for a whitespace-only title", async () => { + const cap = createCapturingOrchestrator(); const app = createApp({ - conversationStore: store, - orchestrator: createFakeOrchestrator([]), + conversationStore: createFakeConversationStore(), + orchestrator: cap, credentialStore: createFakeCredentialStore([]), }); @@ -893,19 +870,12 @@ describe("POST /chat", () => { }); expect(res.status).toBe(200); - expect(setTitleCalled).toBe(false); + expect(cap.received?.title).toBeUndefined(); }); it("returns 400 when title is not a string", async () => { - let setTitleCalled = false; - const store: ConversationStore = { - ...createFakeConversationStore(), - async setConversationTitle() { - setTitleCalled = true; - }, - }; const app = createApp({ - conversationStore: store, + conversationStore: createFakeConversationStore(), orchestrator: createFakeOrchestrator([]), credentialStore: createFakeCredentialStore([]), }); @@ -919,21 +889,19 @@ describe("POST /chat", () => { expect(res.status).toBe(400); const body = (await res.json()) as { error: string }; expect(body.error).toContain("title"); - expect(setTitleCalled).toBe(false); }); - it("proceeds with the turn even if setConversationTitle throws", async () => { + it("does not call setConversationTitle itself (the orchestrator owns it)", async () => { + let setTitleCalled = false; const store: ConversationStore = { ...createFakeConversationStore(), async setConversationTitle() { - throw new Error("store unavailable"); + setTitleCalled = true; }, }; const app = createApp({ conversationStore: store, - orchestrator: createFakeOrchestrator([ - { type: "done", conversationId: "conv1", turnId: "t1", reason: "stop" }, - ]), + orchestrator: createFakeOrchestrator([]), credentialStore: createFakeCredentialStore([]), }); @@ -944,8 +912,10 @@ describe("POST /chat", () => { }); expect(res.status).toBe(200); - const text = await res.text(); - expect(text.trim().split("\n")).toHaveLength(1); + // The route must NOT pre-create the meta — that would bypass the + // orchestrator's new-conversation workspace/system-prompt init. The + // orchestrator sets the title after workspace setup instead. + expect(setTitleCalled).toBe(false); }); }); diff --git a/packages/transport-http/src/app.ts b/packages/transport-http/src/app.ts index 03b6ec2..6e0748b 100644 --- a/packages/transport-http/src/app.ts +++ b/packages/transport-http/src/app.ts @@ -469,22 +469,6 @@ export function createApp(opts: CreateServerOptions): Hono { imageCount: images?.length ?? 0, }); - // Persist an explicit title BEFORE the turn starts so the tab shows it - // immediately (and before `--open` signals the frontend to open it). The - // store creates the conversation meta if none exists yet; a subsequent - // append preserves a non-"Untitled" title. A title-set failure is logged - // but never blocks the turn — the title is a nicety, the answer is not. - if (title !== undefined) { - try { - await opts.conversationStore.setConversationTitle(conversationId, title); - log.info("chat: title set", { conversationId }); - } catch (err) { - log.warn("chat: title set failure", { - error: err instanceof Error ? err.message : String(err), - }); - } - } - const events: AgentEvent[] = []; let controllerRef: ReadableStreamDefaultController<Uint8Array> | undefined; let streamClosed = false; @@ -534,6 +518,7 @@ export function createApp(opts: CreateServerOptions): Hono { ...(reasoningEffort !== undefined ? { reasoningEffort } : {}), ...(workspaceId !== undefined ? { workspaceId } : {}), ...(images !== undefined ? { images } : {}), + ...(title !== undefined ? { title } : {}), }; opts.orchestrator diff --git a/packages/transport-http/src/logic.ts b/packages/transport-http/src/logic.ts index 5cf96cc..c703049 100644 --- a/packages/transport-http/src/logic.ts +++ b/packages/transport-http/src/logic.ts @@ -59,8 +59,10 @@ export interface ChatCommand { * A human-readable title for the conversation tab, set at creation time. * Parsed from the `ChatRequest.title` field; trimmed server-side. A * whitespace-only value is treated as absent (omitted) so the auto-derived - * title applies. Forwarded to the `/chat` route which persists it via the - * conversation store's `setConversationTitle` before the turn starts. + * title applies. Forwarded to the orchestrator, which persists it via the + * conversation store's `setConversationTitle` AFTER the new-conversation + * workspace setup (so workspace assignment / first-turn system-prompt + * construction are not skipped) and before the first message append. */ readonly title?: string; /** |
