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
+16 -2
View File
@@ -14,7 +14,8 @@ type OpenCodeStreamingParams = {
username: string;
prompt: string;
sessionKey: string;
cwd?: string;
cwd?: string; // logical cwd for session tagging/listing (metadata.officer.cwd)
workingDir?: string; // the dir the model should treat as its cwd (Officer system prompt)
model?: string;
role?: string;
resumeSessionId?: string;
@@ -32,6 +33,18 @@ function splitModel(model: string): { providerID: string; modelID: string } {
return { providerID: model.slice(0, slash), modelID: model.slice(slash + 1) };
}
// 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 an appended, non-visible system prompt. Sent on every turn.
function officerSystemPrompt(cwd: string): string {
return [
`Session working directory: ${cwd}`,
'',
`For this session, treat \`${cwd}\` as your current working directory. Resolve relative paths, globs, and file references against it, and run shell commands from inside it (\`cd\` into it, or use absolute paths beneath it). When the user says "here", "this directory", or gives a relative path, they mean a location within \`${cwd}\`.`,
'',
`Unless the user explicitly directs you elsewhere, keep your work focused on \`${cwd}\`, its files, and its subdirectories.`,
].join('\n');
}
export async function sendOpenCodeStreaming(params: OpenCodeStreamingParams): Promise<OpenCodeStreamingHandle> {
logger.info('OpenCode streaming exec', { sessionKey: params.sessionKey, model: params.model });
@@ -60,9 +73,10 @@ export async function sendOpenCodeStreaming(params: OpenCodeStreamingParams): Pr
unsub = conn.subscribe(sessionId, mapper);
const { providerID, modelID } = splitModel(params.model ?? '');
const system = params.workingDir ? officerSystemPrompt(params.workingDir) : undefined;
// Fire the turn; assistant tokens + tool calls stream back over the SSE subscription above.
conn.postMessage(sessionId, providerID, modelID, params.prompt).catch((err) => {
conn.postMessage(sessionId, providerID, modelID, params.prompt, system).catch((err) => {
logger.error('OpenCode postMessage failed', { sessionKey: params.sessionKey, error: String(err) });
params.onEvent({ type: 'error', message: 'Failed to send message to OpenCode' });
unsub();