chat: point OpenCode at a fixed pm2-managed server (fixes empty model picker)
The per-cwd `opencode serve` spawning is replaced by a single fixed server (http://127.0.0.1:4096, OPENCODE_SERVER_URL) managed by pm2 — added as `officer-opencode` in ecosystem.config.cjs (cwd = home). Root-cause fix for the empty model selector: list-models shelled out to `opencode models`, which failed at runtime on the deployed server (the picker got only Claude tiers). It now reads the fixed server's GET /config/providers over HTTP — 11ms and reliable — so the curated OpenCode models (Big Pickle, Claude Haiku) show up. - server-manager.ts — drops spawning; exposes OPENCODE_SERVER_URL + a health check. - client.ts — createSession no longer binds a directory (sessions live in the one server's project). - send-opencode.ts / opencode-sessions.ts / chat.ts — use the fixed server; drop the cwd/home plumbing. Session list/load/delete/rename now hit :4096. Verified end-to-end against the live server: model list, streaming turn, session list, and transcript load all work with no spawning. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -3,15 +3,15 @@ import { ensureServer } from './opencode/server-manager';
|
||||
import { getConnection } from './opencode/client';
|
||||
import { logger } from './logger';
|
||||
|
||||
// The OpenCode analog of claude-sessions.ts. OpenCode's own SQLite store is the source of truth, read
|
||||
// via its HTTP API (never the DB directly). A `serve` is directory-scoped, so listing sessions for a
|
||||
// cwd just means asking the serve rooted at that cwd. Returns the same shapes as the Claude reader,
|
||||
// The OpenCode analog of claude-sessions.ts. OpenCode's own store is the source of truth, read via the
|
||||
// fixed pm2-managed server's HTTP API (never the DB directly). All OpenCode /chat sessions live in that
|
||||
// one server's project, so listing is just GET /session. Returns the same shapes as the Claude reader,
|
||||
// tagged harness:'opencode', so chat.ts can merge both harnesses transparently.
|
||||
|
||||
/** List OpenCode sessions for a working directory. Never throws — returns [] if OpenCode is unavailable. */
|
||||
export async function listOpenCodeSessions(cwd: string, home: string): Promise<ClaudeSessionSummary[]> {
|
||||
/** List OpenCode sessions. Never throws — returns [] if the server is unavailable. */
|
||||
export async function listOpenCodeSessions(): Promise<ClaudeSessionSummary[]> {
|
||||
try {
|
||||
const { baseUrl } = await ensureServer(cwd, home);
|
||||
const { baseUrl } = await ensureServer();
|
||||
const sessions = await getConnection(baseUrl).listSessions();
|
||||
return sessions.map((s) => {
|
||||
const created = s.time?.created ?? Date.now();
|
||||
@@ -19,7 +19,7 @@ export async function listOpenCodeSessions(cwd: string, home: string): Promise<C
|
||||
return {
|
||||
id: s.id,
|
||||
title: s.title || '(untitled)',
|
||||
cwd,
|
||||
cwd: s.location?.directory ?? '',
|
||||
createdAt: new Date(created).toISOString(),
|
||||
updatedAt: new Date(updated).toISOString(),
|
||||
messageCount: 0, // the session list endpoint doesn't include a turn count
|
||||
@@ -27,15 +27,15 @@ export async function listOpenCodeSessions(cwd: string, home: string): Promise<C
|
||||
} satisfies ClaudeSessionSummary;
|
||||
});
|
||||
} catch (err) {
|
||||
logger.warn('Failed to list OpenCode sessions', { cwd, error: String(err) });
|
||||
logger.warn('Failed to list OpenCode sessions', { error: String(err) });
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/** Load one OpenCode session's transcript, rebuilt into the shared display message shape. */
|
||||
export async function loadOpenCodeSession(cwd: string, home: string, sessionId: string): Promise<ClaudeSessionDetail | null> {
|
||||
export async function loadOpenCodeSession(sessionId: string): Promise<ClaudeSessionDetail | null> {
|
||||
try {
|
||||
const { baseUrl } = await ensureServer(cwd, home);
|
||||
const { baseUrl } = await ensureServer();
|
||||
const stored = await getConnection(baseUrl).getMessages(sessionId);
|
||||
|
||||
const messages: ClaudeChatMessage[] = [];
|
||||
@@ -70,21 +70,21 @@ export async function loadOpenCodeSession(cwd: string, home: string, sessionId:
|
||||
isError: Boolean(p.state?.error),
|
||||
});
|
||||
}
|
||||
// text-part deltas already excluded reasoning; skip reasoning/step-* parts here too.
|
||||
// skip reasoning/step-* parts (parity with the streaming delta filter).
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { id: sessionId, model: modelId ? `opencode/${modelId}` : 'opencode', cwd, messages };
|
||||
return { id: sessionId, model: modelId ? `opencode/${modelId}` : 'opencode', cwd: '', messages };
|
||||
} catch (err) {
|
||||
logger.warn('Failed to load OpenCode session', { sessionId, error: String(err) });
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function deleteOpenCodeSession(cwd: string, home: string, sessionId: string): Promise<boolean> {
|
||||
export async function deleteOpenCodeSession(sessionId: string): Promise<boolean> {
|
||||
try {
|
||||
const { baseUrl } = await ensureServer(cwd, home);
|
||||
const { baseUrl } = await ensureServer();
|
||||
return await getConnection(baseUrl).deleteSession(sessionId);
|
||||
} catch (err) {
|
||||
logger.warn('Failed to delete OpenCode session', { sessionId, error: String(err) });
|
||||
@@ -92,9 +92,9 @@ export async function deleteOpenCodeSession(cwd: string, home: string, sessionId
|
||||
}
|
||||
}
|
||||
|
||||
export async function renameOpenCodeSession(cwd: string, home: string, sessionId: string, title: string): Promise<boolean> {
|
||||
export async function renameOpenCodeSession(sessionId: string, title: string): Promise<boolean> {
|
||||
try {
|
||||
const { baseUrl } = await ensureServer(cwd, home);
|
||||
const { baseUrl } = await ensureServer();
|
||||
return await getConnection(baseUrl).renameSession(sessionId, title);
|
||||
} catch (err) {
|
||||
logger.warn('Failed to rename OpenCode session', { sessionId, error: String(err) });
|
||||
|
||||
Reference in New Issue
Block a user