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:
@@ -117,11 +117,21 @@ class ServerConnection {
|
|||||||
return session.id;
|
return session.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
async postMessage(sessionId: string, providerID: string, modelID: string, text: string): Promise<void> {
|
async postMessage(
|
||||||
await this.postJson(`/session/${sessionId}/message`, {
|
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 },
|
model: { providerID, modelID },
|
||||||
parts: [{ type: 'text', text }],
|
parts: [{ type: 'text', text }],
|
||||||
});
|
};
|
||||||
|
if (system) body.system = system;
|
||||||
|
await this.postJson(`/session/${sessionId}/message`, body);
|
||||||
}
|
}
|
||||||
|
|
||||||
async abort(sessionId: string): Promise<void> {
|
async abort(sessionId: string): Promise<void> {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import type { ClientMessage, ServerMessage, Message, ChatEvent } from './types';
|
|||||||
import { sessionManager } from './session-manager';
|
import { sessionManager } from './session-manager';
|
||||||
import { sendClaudeCodeStreaming } from '@@/channels/send-claude-code';
|
import { sendClaudeCodeStreaming } from '@@/channels/send-claude-code';
|
||||||
import { sendOpenCodeStreaming } from '@@/channels/send-opencode';
|
import { sendOpenCodeStreaming } from '@@/channels/send-opencode';
|
||||||
import { ensureClaudeSessionsCwd } from './claude-sessions';
|
import { ensureClaudeSessionsCwd, getClaudeSessionsCwd } from './claude-sessions';
|
||||||
import * as sidecar from '@@/sidecar-registry';
|
import * as sidecar from '@@/sidecar-registry';
|
||||||
import { join } from 'path';
|
import { join } from 'path';
|
||||||
import { getHomeDirForRole, getEmailAccountsDir } from '../../../servers/data-path';
|
import { getHomeDirForRole, getEmailAccountsDir } from '../../../servers/data-path';
|
||||||
@@ -459,6 +459,10 @@ async function handleOpenCodeChat(
|
|||||||
|
|
||||||
const onEvent = createEventHandler(sessionId, model, cwd);
|
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 {
|
try {
|
||||||
const handle = await sendOpenCodeStreaming({
|
const handle = await sendOpenCodeStreaming({
|
||||||
userId,
|
userId,
|
||||||
@@ -467,6 +471,7 @@ async function handleOpenCodeChat(
|
|||||||
prompt: effectivePrompt,
|
prompt: effectivePrompt,
|
||||||
sessionKey: sessionId,
|
sessionKey: sessionId,
|
||||||
cwd,
|
cwd,
|
||||||
|
workingDir,
|
||||||
model,
|
model,
|
||||||
role: ws.data.role,
|
role: ws.data.role,
|
||||||
resumeSessionId: msg.resumeSessionId,
|
resumeSessionId: msg.resumeSessionId,
|
||||||
|
|||||||
@@ -14,7 +14,8 @@ type OpenCodeStreamingParams = {
|
|||||||
username: string;
|
username: string;
|
||||||
prompt: string;
|
prompt: string;
|
||||||
sessionKey: 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;
|
model?: string;
|
||||||
role?: string;
|
role?: string;
|
||||||
resumeSessionId?: 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) };
|
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> {
|
export async function sendOpenCodeStreaming(params: OpenCodeStreamingParams): Promise<OpenCodeStreamingHandle> {
|
||||||
logger.info('OpenCode streaming exec', { sessionKey: params.sessionKey, model: params.model });
|
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);
|
unsub = conn.subscribe(sessionId, mapper);
|
||||||
|
|
||||||
const { providerID, modelID } = splitModel(params.model ?? '');
|
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.
|
// 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) });
|
logger.error('OpenCode postMessage failed', { sessionKey: params.sessionKey, error: String(err) });
|
||||||
params.onEvent({ type: 'error', message: 'Failed to send message to OpenCode' });
|
params.onEvent({ type: 'error', message: 'Failed to send message to OpenCode' });
|
||||||
unsub();
|
unsub();
|
||||||
|
|||||||
Reference in New Issue
Block a user