From d8e0dc79efe6278d478e3627fb5b9b280a405e2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Padez?= Date: Sat, 25 Jul 2026 12:26:41 +0000 Subject: [PATCH] chat: send cwd on every message instead of reading it back from session metadata Simpler + more efficient than the previous per-turn GET /session: the client already has the selected cwd, so it now sends it on every message (it's constant for a session). The server uses msg.cwd directly for OpenCode's per-turn working-directory system prompt, and still tags the cwd at creation for listing. Drops the getSession round-trip. Co-Authored-By: Claude Opus 4.8 --- src/servers/api/chat/opencode/client.ts | 6 ----- src/servers/channels/send-opencode.ts | 26 +++++++------------ .../Chat/EmbeddableChat/useEmbeddableChat.ts | 26 +++++++++++++++---- 3 files changed, 30 insertions(+), 28 deletions(-) diff --git a/src/servers/api/chat/opencode/client.ts b/src/servers/api/chat/opencode/client.ts index 999fb10a..83f80e73 100644 --- a/src/servers/api/chat/opencode/client.ts +++ b/src/servers/api/chat/opencode/client.ts @@ -146,12 +146,6 @@ class ServerConnection { return (await res.json()) as OpenCodeSessionInfo[]; } - async getSession(sessionId: string): Promise { - const res = await fetch(`${this.baseUrl}/session/${sessionId}`); - if (!res.ok) return null; - return (await res.json()) as OpenCodeSessionInfo; - } - async getMessages(sessionId: string): Promise { const res = await fetch(`${this.baseUrl}/session/${sessionId}/message`); if (!res.ok) throw new Error(`opencode GET /session/${sessionId}/message → ${res.status}`); diff --git a/src/servers/channels/send-opencode.ts b/src/servers/channels/send-opencode.ts index 3297bcab..b36b17a3 100644 --- a/src/servers/channels/send-opencode.ts +++ b/src/servers/channels/send-opencode.ts @@ -1,7 +1,7 @@ import type { ChatEvent } from '@@/api/chat/types'; import { logger } from '@@/api/chat/logger'; import { ensureServer } from '@@/api/chat/opencode/server-manager'; -import { getConnection, officerMeta } from '@@/api/chat/opencode/client'; +import { getConnection } from '@@/api/chat/opencode/client'; import { createEventMapper } from '@@/api/chat/opencode/event-mapper'; import { getOpenCodeSession, setOpenCodeSession } from '@@/api/chat/opencode/state'; @@ -51,25 +51,17 @@ export async function sendOpenCodeStreaming(params: OpenCodeStreamingParams): Pr const conn = getConnection(baseUrl); // Resolve the OpenCode session: a known mapping, or — when resuming from history — the sessionKey is - // itself the OpenCode session id (`ses_…`); otherwise create a new one. - const existingId = + // itself the OpenCode session id (`ses_…`); otherwise create a new one, tagged with its cwd (for + // listing). The cwd rides every message from the client, so the system prompt below stays consistent. + let opencodeSessionId = getOpenCodeSession(params.sessionKey) ?? (params.sessionKey.startsWith('ses_') ? params.sessionKey : undefined) ?? params.resumeSessionId; - - // The session's working directory is bound at creation (metadata.officer.cwd). msg.cwd only rides - // the first message, so for an existing/resumed session read the bound cwd back and reuse it — this - // keeps the system prompt (and session tagging) consistent across every turn. - let sessionId: string; - let cwd = params.cwd; - if (existingId) { - sessionId = existingId; - const bound = officerMeta((await conn.getSession(existingId))?.metadata).cwd; - if (bound) cwd = bound; - } else { - sessionId = await conn.createSession(cwd ? { officer: { cwd } } : undefined); + if (!opencodeSessionId) { + opencodeSessionId = await conn.createSession(params.cwd ? { officer: { cwd: params.cwd } } : undefined); } - setOpenCodeSession(params.sessionKey, sessionId); + setOpenCodeSession(params.sessionKey, opencodeSessionId); + const sessionId = opencodeSessionId; let unsub = () => {}; const mapper = createEventMapper((event: ChatEvent) => { @@ -79,7 +71,7 @@ export async function sendOpenCodeStreaming(params: OpenCodeStreamingParams): Pr unsub = conn.subscribe(sessionId, mapper); const { providerID, modelID } = splitModel(params.model ?? ''); - const system = cwd ? officerSystemPrompt(cwd) : undefined; + const system = params.cwd ? officerSystemPrompt(params.cwd) : undefined; // Fire the turn; assistant tokens + tool calls stream back over the SSE subscription above. conn.postMessage(sessionId, providerID, modelID, params.prompt, system).catch((err) => { diff --git a/src/workspaces/officerdev/src/apps/Chat/EmbeddableChat/useEmbeddableChat.ts b/src/workspaces/officerdev/src/apps/Chat/EmbeddableChat/useEmbeddableChat.ts index 0de7291e..1e82bc63 100644 --- a/src/workspaces/officerdev/src/apps/Chat/EmbeddableChat/useEmbeddableChat.ts +++ b/src/workspaces/officerdev/src/apps/Chat/EmbeddableChat/useEmbeddableChat.ts @@ -26,9 +26,21 @@ type UseEmbeddableChatParams = { }; export function useEmbeddableChat(params: UseEmbeddableChatParams, onMessageComplete?: () => void) { - const { initialMessage, defaultInput = '', promptPrefix, cwd, sandboxed, autoSend = false, chat: externalChat } = params; + const { + initialMessage, + defaultInput = '', + promptPrefix, + cwd, + sandboxed, + autoSend = false, + chat: externalChat, + } = params; - const internalChat = useChat(params.sessionId, params.initialModel, { replaceUrl: params.replaceUrl ?? false, context: params.context, contextId: params.contextId }); + const internalChat = useChat(params.sessionId, params.initialModel, { + replaceUrl: params.replaceUrl ?? false, + context: params.context, + contextId: params.contextId, + }); const chat = externalChat ?? internalChat; const { @@ -90,13 +102,14 @@ export function useEmbeddableChat(params: UseEmbeddableChatParams, onMessageComp let prompt = promptPrefix ? `${promptPrefix}\n\n${text}` : text; if (prefix) prompt = `${prefix}${prompt}`; - const cwdForFirst = !sessionId ? cwd : undefined; + // Send the cwd on every message (not just the first): OpenCode rebuilds its working-directory + // system prompt each turn, so it needs the cwd every time. It stays constant for a session. const displayText = promptPrefix ? text : undefined; sendPrompt( prompt, !sessionId && ids.length > 0 ? ids : undefined, images.length > 0 ? images : undefined, - cwdForFirst, + cwd, undefined, sandboxed, thinkingLevel, @@ -149,7 +162,10 @@ export function useEmbeddableChat(params: UseEmbeddableChatParams, onMessageComp rafId = requestAnimationFrame(resizeTextarea); }); observer.observe(textarea); - return () => { observer.disconnect(); cancelAnimationFrame(rafId); }; + return () => { + observer.disconnect(); + cancelAnimationFrame(rafId); + }; }, []); // Auto-scroll to bottom on new messages