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 <noreply@anthropic.com>
This commit is contained in:
2026-07-25 12:26:41 +00:00
co-authored by Claude Opus 4.8
parent 2a940de76a
commit d8e0dc79ef
3 changed files with 30 additions and 28 deletions
-6
View File
@@ -146,12 +146,6 @@ class ServerConnection {
return (await res.json()) as OpenCodeSessionInfo[]; return (await res.json()) as OpenCodeSessionInfo[];
} }
async getSession(sessionId: string): Promise<OpenCodeSessionInfo | null> {
const res = await fetch(`${this.baseUrl}/session/${sessionId}`);
if (!res.ok) return null;
return (await res.json()) as OpenCodeSessionInfo;
}
async getMessages(sessionId: string): Promise<OpenCodeStoredMessage[]> { async getMessages(sessionId: string): Promise<OpenCodeStoredMessage[]> {
const res = await fetch(`${this.baseUrl}/session/${sessionId}/message`); const res = await fetch(`${this.baseUrl}/session/${sessionId}/message`);
if (!res.ok) throw new Error(`opencode GET /session/${sessionId}/message → ${res.status}`); if (!res.ok) throw new Error(`opencode GET /session/${sessionId}/message → ${res.status}`);
+9 -17
View File
@@ -1,7 +1,7 @@
import type { ChatEvent } from '@@/api/chat/types'; import type { ChatEvent } from '@@/api/chat/types';
import { logger } from '@@/api/chat/logger'; import { logger } from '@@/api/chat/logger';
import { ensureServer } from '@@/api/chat/opencode/server-manager'; 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 { createEventMapper } from '@@/api/chat/opencode/event-mapper';
import { getOpenCodeSession, setOpenCodeSession } from '@@/api/chat/opencode/state'; import { getOpenCodeSession, setOpenCodeSession } from '@@/api/chat/opencode/state';
@@ -51,25 +51,17 @@ export async function sendOpenCodeStreaming(params: OpenCodeStreamingParams): Pr
const conn = getConnection(baseUrl); const conn = getConnection(baseUrl);
// Resolve the OpenCode session: a known mapping, or — when resuming from history — the sessionKey is // 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. // itself the OpenCode session id (`ses_…`); otherwise create a new one, tagged with its cwd (for
const existingId = // listing). The cwd rides every message from the client, so the system prompt below stays consistent.
let opencodeSessionId =
getOpenCodeSession(params.sessionKey) ?? getOpenCodeSession(params.sessionKey) ??
(params.sessionKey.startsWith('ses_') ? params.sessionKey : undefined) ?? (params.sessionKey.startsWith('ses_') ? params.sessionKey : undefined) ??
params.resumeSessionId; params.resumeSessionId;
if (!opencodeSessionId) {
// The session's working directory is bound at creation (metadata.officer.cwd). msg.cwd only rides opencodeSessionId = await conn.createSession(params.cwd ? { officer: { cwd: params.cwd } } : undefined);
// 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);
} }
setOpenCodeSession(params.sessionKey, sessionId); setOpenCodeSession(params.sessionKey, opencodeSessionId);
const sessionId = opencodeSessionId;
let unsub = () => {}; let unsub = () => {};
const mapper = createEventMapper((event: ChatEvent) => { const mapper = createEventMapper((event: ChatEvent) => {
@@ -79,7 +71,7 @@ export async function sendOpenCodeStreaming(params: OpenCodeStreamingParams): Pr
unsub = conn.subscribe(sessionId, mapper); unsub = conn.subscribe(sessionId, mapper);
const { providerID, modelID } = splitModel(params.model ?? ''); 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. // Fire the turn; assistant tokens + tool calls stream back over the SSE subscription above.
conn.postMessage(sessionId, providerID, modelID, params.prompt, system).catch((err) => { conn.postMessage(sessionId, providerID, modelID, params.prompt, system).catch((err) => {
@@ -26,9 +26,21 @@ type UseEmbeddableChatParams = {
}; };
export function useEmbeddableChat(params: UseEmbeddableChatParams, onMessageComplete?: () => void) { 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 chat = externalChat ?? internalChat;
const { const {
@@ -90,13 +102,14 @@ export function useEmbeddableChat(params: UseEmbeddableChatParams, onMessageComp
let prompt = promptPrefix ? `${promptPrefix}\n\n${text}` : text; let prompt = promptPrefix ? `${promptPrefix}\n\n${text}` : text;
if (prefix) prompt = `${prefix}${prompt}`; 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; const displayText = promptPrefix ? text : undefined;
sendPrompt( sendPrompt(
prompt, prompt,
!sessionId && ids.length > 0 ? ids : undefined, !sessionId && ids.length > 0 ? ids : undefined,
images.length > 0 ? images : undefined, images.length > 0 ? images : undefined,
cwdForFirst, cwd,
undefined, undefined,
sandboxed, sandboxed,
thinkingLevel, thinkingLevel,
@@ -149,7 +162,10 @@ export function useEmbeddableChat(params: UseEmbeddableChatParams, onMessageComp
rafId = requestAnimationFrame(resizeTextarea); rafId = requestAnimationFrame(resizeTextarea);
}); });
observer.observe(textarea); observer.observe(textarea);
return () => { observer.disconnect(); cancelAnimationFrame(rafId); }; return () => {
observer.disconnect();
cancelAnimationFrame(rafId);
};
}, []); }, []);
// Auto-scroll to bottom on new messages // Auto-scroll to bottom on new messages