Files
platform/src/servers/channels/send-opencode.ts
T
pastilhasandClaude Opus 4.8 cbe83f39a9 chat: run the /email chat from the email account's storage dir
The email chat's working directory now resolves to
DATA_PATH/<owner>/email_accounts/<accountEmail> (created if missing), so the agent
operates in the selected account's dir (emails.db, attachment_cache, …).

- websocket.ts: new resolveChatCwd — context 'email' → the account dir (via a new
  resolveEmailCwd), 'chat' → the pwd/claude_sessions dir, else the given cwd. Both the
  Claude and OpenCode handlers use it. The account defaults to the owner's first enabled
  account for now; the account selector will pass it as contextId later.
- OpenCode honors the cwd again: send-opencode passes it as the session `directory`
  (client.createSession(directory?)) for context-scoped chats; the general /chat still
  omits it and uses the fixed server's default project. Verified against the live server
  that directory-bound sessions create + list.

No frontend change — the /email panel already sends context:'email'.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-24 17:37:48 +00:00

76 lines
2.8 KiB
TypeScript

import type { ChatEvent } from '@@/api/chat/types';
import { logger } from '@@/api/chat/logger';
import { ensureServer } from '@@/api/chat/opencode/server-manager';
import { getConnection } from '@@/api/chat/opencode/client';
import { createEventMapper } from '@@/api/chat/opencode/event-mapper';
import { getOpenCodeSession, setOpenCodeSession } from '@@/api/chat/opencode/state';
// The OpenCode analog of send-claude-code.ts's streaming path. Drives a turn against a warm
// `opencode serve` over HTTP + SSE, mapping events to the shared ChatEvent contract.
type OpenCodeStreamingParams = {
userId: number;
email: string;
username: string;
prompt: string;
sessionKey: string;
cwd?: string;
model?: string;
role?: string;
resumeSessionId?: string;
onEvent: (event: ChatEvent) => void;
};
type OpenCodeStreamingHandle = {
kill: () => void;
};
/** Split an OpenCode model id (`providerID/modelID`, e.g. `opencode/claude-opus-4-8`). */
function splitModel(model: string): { providerID: string; modelID: string } {
const slash = model.indexOf('/');
if (slash <= 0) return { providerID: 'opencode', modelID: model };
return { providerID: model.slice(0, slash), modelID: model.slice(slash + 1) };
}
export async function sendOpenCodeStreaming(params: OpenCodeStreamingParams): Promise<OpenCodeStreamingHandle> {
logger.info('OpenCode streaming exec', { sessionKey: params.sessionKey, model: params.model });
const { baseUrl } = await ensureServer();
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.
let opencodeSessionId =
getOpenCodeSession(params.sessionKey) ??
(params.sessionKey.startsWith('ses_') ? params.sessionKey : undefined) ??
params.resumeSessionId;
if (!opencodeSessionId) {
opencodeSessionId = await conn.createSession(params.cwd);
}
setOpenCodeSession(params.sessionKey, opencodeSessionId);
const sessionId = opencodeSessionId;
let unsub = () => {};
const mapper = createEventMapper((event: ChatEvent) => {
params.onEvent(event);
if (event.type === 'result' || event.type === 'error') unsub();
});
unsub = conn.subscribe(sessionId, mapper);
const { providerID, modelID } = splitModel(params.model ?? '');
// Fire the turn; assistant tokens + tool calls stream back over the SSE subscription above.
conn.postMessage(sessionId, providerID, modelID, params.prompt).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();
});
return {
kill: () => {
void conn.abort(sessionId);
unsub();
},
};
}