feat(electrobun): load full session history from JSONL when resuming — conversation stays visible

This commit is contained in:
Hibryda 2026-03-27 04:09:00 +01:00
parent 1afbe66fd7
commit cb7fba6130
5 changed files with 190 additions and 5 deletions

View file

@ -146,3 +146,114 @@ export function listClaudeSessions(cwd: string): ClaudeSessionInfo[] {
return results;
}
// ── Message types for display ───────────────────────────────────────────────
export interface SessionMessage {
id: string;
role: 'user' | 'assistant' | 'tool_call' | 'tool_result' | 'system';
content: string;
timestamp: number;
model?: string;
toolName?: string;
}
/**
* Load conversation messages from a Claude JSONL session file.
* Extracts user prompts and assistant responses for display.
*/
export function loadClaudeSessionMessages(cwd: string, sdkSessionId: string): SessionMessage[] {
const encoded = encodeCwd(cwd);
const filePath = join(homedir(), ".claude", "projects", encoded, `${sdkSessionId}.jsonl`);
let content: string;
try {
content = readFileSync(filePath, "utf-8");
} catch {
return [];
}
const messages: SessionMessage[] = [];
const lines = content.split("\n");
for (const line of lines) {
if (!line.trim()) continue;
try {
const obj = JSON.parse(line);
// User messages
if (obj.type === "user" && obj.message?.content) {
const mc = obj.message.content;
let text = "";
if (typeof mc === "string") {
text = mc;
} else if (Array.isArray(mc)) {
const tb = mc.find((b: Record<string, unknown>) => b.type === "text");
if (tb?.text) text = String(tb.text);
}
if (text) {
messages.push({
id: obj.uuid || `user-${messages.length}`,
role: "user",
content: text,
timestamp: obj.timestamp ? new Date(obj.timestamp).getTime() : Date.now(),
});
}
}
// Assistant messages
if (obj.type === "assistant" && obj.message?.content) {
const mc = obj.message.content;
let text = "";
if (Array.isArray(mc)) {
for (const block of mc) {
if (block.type === "text" && block.text) {
text += block.text;
} else if (block.type === "tool_use") {
messages.push({
id: block.id || `tool-${messages.length}`,
role: "tool_call",
content: `${block.name}(${JSON.stringify(block.input || {}).slice(0, 200)})`,
timestamp: obj.timestamp ? new Date(obj.timestamp).getTime() : Date.now(),
toolName: block.name,
});
}
}
}
if (text) {
messages.push({
id: obj.uuid || obj.message?.id || `asst-${messages.length}`,
role: "assistant",
content: text,
timestamp: obj.timestamp ? new Date(obj.timestamp).getTime() : Date.now(),
model: obj.message?.model,
});
}
}
// Tool results
if (obj.type === "tool_result" || (obj.role === "tool" && obj.content)) {
const rc = obj.content;
let text = "";
if (typeof rc === "string") {
text = rc;
} else if (Array.isArray(rc)) {
const tb = rc.find((b: Record<string, unknown>) => b.type === "text");
if (tb?.text) text = String(tb.text);
}
if (text && text.length > 0) {
messages.push({
id: obj.uuid || `result-${messages.length}`,
role: "tool_result",
content: text.length > 500 ? text.slice(0, 497) + "..." : text,
timestamp: obj.timestamp ? new Date(obj.timestamp).getTime() : Date.now(),
});
}
}
} catch {
continue;
}
}
return messages;
}

View file

@ -179,5 +179,15 @@ export function createAgentHandlers(
return { sessions: [] };
}
},
"session.loadMessages": ({ cwd, sdkSessionId }: { cwd: string; sdkSessionId: string }) => {
try {
const { loadClaudeSessionMessages } = require("../claude-sessions.ts");
return { messages: loadClaudeSessionMessages(cwd, sdkSessionId) };
} catch (err) {
console.error("[session.loadMessages]", err);
return { messages: [] };
}
},
};
}

View file

@ -1,7 +1,7 @@
<script lang="ts">
import { onMount } from 'svelte';
import type { ClaudeSessionInfo } from './agent-store.svelte.ts';
import { listProjectSessions, setPendingResume, getPendingResume, clearPendingResume } from './agent-store.svelte.ts';
import { listProjectSessions, setPendingResume, getPendingResume, clearPendingResume, loadSessionHistory } from './agent-store.svelte.ts';
import { t } from './i18n.svelte.ts';
interface Props {
@ -72,16 +72,20 @@
return s.length > max ? s.slice(0, max - 3) + '...' : s;
}
function handleContinue() {
async function handleContinue() {
open = false;
setPendingResume(projectId, 'continue');
// User types their next prompt and hits Send — startAgent reads the pending resume
// Load the most recent session's messages for display
if (sessions.length > 0) {
await loadSessionHistory(projectId, sessions[0].sessionId, cwd);
}
}
function handleResume(sdkSessionId: string) {
async function handleResume(sdkSessionId: string) {
open = false;
setPendingResume(projectId, 'resume', sdkSessionId);
// User types their next prompt and hits Send — startAgent reads the pending resume
// Load the selected session's messages for display
await loadSessionHistory(projectId, sdkSessionId, cwd);
}
function handleNew() {

View file

@ -154,6 +154,51 @@ export function clearPendingResume(projectId: string): void {
bump();
}
/**
* Load full conversation history from a Claude JSONL session file into the store.
* Called when user selects a session from the picker shows the conversation
* BEFORE the user sends their next prompt.
*/
export async function loadSessionHistory(projectId: string, sdkSessionId: string, cwd: string): Promise<void> {
try {
const { messages } = await appRpc.request['session.loadMessages']({ cwd, sdkSessionId }) as {
messages: Array<{ id: string; role: string; content: string; timestamp: number; model?: string; toolName?: string }>;
};
if (!messages || messages.length === 0) return;
// Create a display-only session to show the history
const displaySessionId = `${projectId}-history-${Date.now()}`;
const converted: AgentMessage[] = messages.map((m, i) => ({
id: m.id || `hist-${i}`,
seqId: i,
role: m.role === 'user' ? 'user' as const
: m.role === 'assistant' ? 'assistant' as const
: m.role === 'tool_call' ? 'tool_call' as const
: m.role === 'tool_result' ? 'tool_result' as const
: 'system' as const,
content: m.content,
timestamp: m.timestamp,
toolName: m.toolName,
}));
sessions[displaySessionId] = {
sessionId: displaySessionId,
projectId,
provider: 'claude',
status: 'done',
messages: converted,
costUsd: 0,
inputTokens: 0,
outputTokens: 0,
model: messages.find(m => m.model)?.model || 'unknown',
};
projectSessionMap.set(projectId, displaySessionId);
bump();
} catch (err) {
console.error('[agent-store] loadSessionHistory error:', err);
}
}
// Map sessionId -> reactive session state
let sessions = $state<Record<string, AgentSession>>({});

View file

@ -496,6 +496,21 @@ export type PtyRPCRequests = {
};
};
/** Load full conversation messages from a Claude JSONL session file. */
"session.loadMessages": {
params: { cwd: string; sdkSessionId: string };
response: {
messages: Array<{
id: string;
role: 'user' | 'assistant' | 'tool_call' | 'tool_result' | 'system';
content: string;
timestamp: number;
model?: string;
toolName?: string;
}>;
};
};
// ── btmsg RPC ──────────────────────────────────────────────────────────
/** Register an agent in btmsg. */