chat: unified session history across Claude + OpenCode (Phase 3)
/chat's session list, transcript load, delete, and rename now span both harnesses. - opencode-sessions.ts — REST-backed reader (OpenCode's SQLite via its HTTP API, never the DB): listOpenCodeSessions / loadOpenCodeSession / delete / rename, returning the same shapes as the Claude reader, tagged harness:'opencode'. A serve is directory- scoped, so listing a cwd = asking the serve rooted there. Transcript rebuild maps user/assistant/tool parts and drops reasoning (parity with the delta filter). - client.ts — adds listSessions/getMessages/deleteSession/renameSession over /session/*. - chat.ts — /sessions merges both (newest first); /sessions/:id, DELETE, and /title route by id shape (ses_ = OpenCode). ClaudeSessionSummary gains an optional `harness` tag. - send-opencode.ts — resuming from history: when the sessionKey is itself a ses_ id, reuse that OpenCode session instead of creating a new one. - SessionList.tsx — shows an "OpenCode" badge for OpenCode sessions. Verified end-to-end against a live serve: list (5 sessions, tagged), load (transcript rebuilt, reasoning filtered), and rename all work. Phases 1-3 complete; needs a restart. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -121,8 +121,57 @@ class ServerConnection {
|
||||
async abort(sessionId: string): Promise<void> {
|
||||
await this.postJson(`/session/${sessionId}/abort`, {}).catch(() => {});
|
||||
}
|
||||
|
||||
// ── Session history (REST; OpenCode's SQLite store is the source of truth) ──
|
||||
|
||||
async listSessions(): Promise<OpenCodeSessionInfo[]> {
|
||||
const res = await fetch(`${this.baseUrl}/session`);
|
||||
if (!res.ok) throw new Error(`opencode GET /session → ${res.status}`);
|
||||
return (await res.json()) as OpenCodeSessionInfo[];
|
||||
}
|
||||
|
||||
async getMessages(sessionId: string): Promise<OpenCodeStoredMessage[]> {
|
||||
const res = await fetch(`${this.baseUrl}/session/${sessionId}/message`);
|
||||
if (!res.ok) throw new Error(`opencode GET /session/${sessionId}/message → ${res.status}`);
|
||||
return (await res.json()) as OpenCodeStoredMessage[];
|
||||
}
|
||||
|
||||
async deleteSession(sessionId: string): Promise<boolean> {
|
||||
const res = await fetch(`${this.baseUrl}/session/${sessionId}`, { method: 'DELETE' });
|
||||
return res.ok;
|
||||
}
|
||||
|
||||
async renameSession(sessionId: string, title: string): Promise<boolean> {
|
||||
const res = await fetch(`${this.baseUrl}/session/${sessionId}`, {
|
||||
method: 'PATCH',
|
||||
headers: { 'content-type': 'application/json' },
|
||||
body: JSON.stringify({ title }),
|
||||
});
|
||||
return res.ok;
|
||||
}
|
||||
}
|
||||
|
||||
// Shapes returned by the `/session/*` read endpoints (verified against opencode 1.17.9).
|
||||
export type OpenCodeSessionInfo = {
|
||||
id: string;
|
||||
title?: string;
|
||||
time?: { created?: number; updated?: number };
|
||||
location?: { directory?: string | null };
|
||||
};
|
||||
|
||||
export type OpenCodeStoredPart = {
|
||||
type?: string;
|
||||
text?: string;
|
||||
tool?: string;
|
||||
callID?: string;
|
||||
state?: { input?: Record<string, unknown>; output?: string; error?: string };
|
||||
};
|
||||
|
||||
export type OpenCodeStoredMessage = {
|
||||
info?: { role?: string; modelID?: string; model?: { modelID?: string; providerID?: string } };
|
||||
parts?: OpenCodeStoredPart[];
|
||||
};
|
||||
|
||||
const connections = new Map<string, ServerConnection>();
|
||||
|
||||
export function getConnection(baseUrl: string): ServerConnection {
|
||||
|
||||
Reference in New Issue
Block a user