chat: inject an Officer system prompt telling OpenCode its working directory (step 2)

OpenCode can't set a real per-session cwd (every session runs in the fixed server's
dir), so we tell the model its working directory via a system prompt appended to
OpenCode's own — sent as a system message, so it never appears in the visible chat
(verified against source + live). Claude doesn't need this (it honors cwd natively).

- client.postMessage(…, system?) forwards a `system` string on the message.
- send-opencode builds the Officer prompt from `workingDir` and sends it every turn.
- websocket: workingDir = the resolved cwd, except the general /chat (whose cwd is the
  claude_sessions grouping placeholder) uses the user's home.

Verified live: with the prompt, the model reports the injected dir as its cwd.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-25 11:06:17 +00:00
co-authored by Claude Opus 4.8
parent 7cd2be9fb9
commit 9a79a76b95
3 changed files with 35 additions and 6 deletions
+13 -3
View File
@@ -117,11 +117,21 @@ class ServerConnection {
return session.id;
}
async postMessage(sessionId: string, providerID: string, modelID: string, text: string): Promise<void> {
await this.postJson(`/session/${sessionId}/message`, {
async postMessage(
sessionId: string,
providerID: string,
modelID: string,
text: string,
system?: string,
): Promise<void> {
// `system` is appended to OpenCode's built-in system prompt (additive, not an override) and is
// sent to the LLM as a system message — it never appears as a visible chat part.
const body: Record<string, unknown> = {
model: { providerID, modelID },
parts: [{ type: 'text', text }],
});
};
if (system) body.system = system;
await this.postJson(`/session/${sessionId}/message`, body);
}
async abort(sessionId: string): Promise<void> {
+6 -1
View File
@@ -4,7 +4,7 @@ import type { ClientMessage, ServerMessage, Message, ChatEvent } from './types';
import { sessionManager } from './session-manager';
import { sendClaudeCodeStreaming } from '@@/channels/send-claude-code';
import { sendOpenCodeStreaming } from '@@/channels/send-opencode';
import { ensureClaudeSessionsCwd } from './claude-sessions';
import { ensureClaudeSessionsCwd, getClaudeSessionsCwd } from './claude-sessions';
import * as sidecar from '@@/sidecar-registry';
import { join } from 'path';
import { getHomeDirForRole, getEmailAccountsDir } from '../../../servers/data-path';
@@ -459,6 +459,10 @@ async function handleOpenCodeChat(
const onEvent = createEventHandler(sessionId, model, cwd);
// The dir the OpenCode model should treat as its cwd (via the Officer system prompt). For the general
// /chat, the resolved cwd is just the claude_sessions grouping placeholder, so use the user's home.
const workingDir = cwd === getClaudeSessionsCwd(email) ? getHomeDirForRole(email, ws.data.role) : cwd;
try {
const handle = await sendOpenCodeStreaming({
userId,
@@ -467,6 +471,7 @@ async function handleOpenCodeChat(
prompt: effectivePrompt,
sessionKey: sessionId,
cwd,
workingDir,
model,
role: ws.data.role,
resumeSessionId: msg.resumeSessionId,