feat(electrobun): load full session history from JSONL when resuming — conversation stays visible
This commit is contained in:
parent
1afbe66fd7
commit
cb7fba6130
5 changed files with 190 additions and 5 deletions
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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: [] };
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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>>({});
|
||||
|
||||
|
|
|
|||
|
|
@ -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. */
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue