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;
|
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: [] };
|
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">
|
<script lang="ts">
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import type { ClaudeSessionInfo } from './agent-store.svelte.ts';
|
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';
|
import { t } from './i18n.svelte.ts';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
|
@ -72,16 +72,20 @@
|
||||||
return s.length > max ? s.slice(0, max - 3) + '...' : s;
|
return s.length > max ? s.slice(0, max - 3) + '...' : s;
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleContinue() {
|
async function handleContinue() {
|
||||||
open = false;
|
open = false;
|
||||||
setPendingResume(projectId, 'continue');
|
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;
|
open = false;
|
||||||
setPendingResume(projectId, 'resume', sdkSessionId);
|
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() {
|
function handleNew() {
|
||||||
|
|
|
||||||
|
|
@ -154,6 +154,51 @@ export function clearPendingResume(projectId: string): void {
|
||||||
bump();
|
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
|
// Map sessionId -> reactive session state
|
||||||
let sessions = $state<Record<string, AgentSession>>({});
|
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 ──────────────────────────────────────────────────────────
|
// ── btmsg RPC ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
/** Register an agent in btmsg. */
|
/** Register an agent in btmsg. */
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue