claude-code-rust 0.5.1 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,13 +2,15 @@
2
2
 
3
3
  A native Rust terminal interface for Claude Code. Drop-in replacement for Anthropic's stock Node.js/React Ink TUI, built for performance and a better user experience.
4
4
 
5
+ [![npm version](https://img.shields.io/npm/v/claude-code-rust)](https://www.npmjs.com/package/claude-code-rust)
6
+ [![npm downloads](https://img.shields.io/npm/dm/claude-code-rust)](https://www.npmjs.com/package/claude-code-rust)
5
7
  [![CI](https://github.com/srothgan/claude-code-rust/actions/workflows/ci.yml/badge.svg)](https://github.com/srothgan/claude-code-rust/actions/workflows/ci.yml)
6
8
  [![License: AGPL-3.0-or-later](https://img.shields.io/badge/License-AGPL--3.0--or--later-blue.svg)](https://www.gnu.org/licenses/agpl-3.0)
7
- [![MSRV](https://img.shields.io/badge/MSRV-1.88.0-blue.svg)](https://blog.rust-lang.org/)
9
+ [![Node.js](https://img.shields.io/badge/Node.js-%3E%3D18-green.svg)](https://nodejs.org/)
8
10
 
9
11
  ## About
10
12
 
11
- Claude Code Rust replaces the stock Claude Code terminal interface with a native Rust binary built on [Ratatui](https://ratatui.rs/). It connects to the same Claude API through a local Agent SDK bridge (`agent-sdk/dist/bridge.js`). Core Claude Code functionality - tool calls, file editing, terminal commands, and permissions - works unchanged.
13
+ Claude Code Rust replaces the stock Claude Code terminal interface with a native Rust binary built on [Ratatui](https://ratatui.rs/). It connects to the same Claude API through a local Agent SDK bridge. Core Claude Code functionality - tool calls, file editing, terminal commands, and permissions - works unchanged.
12
14
 
13
15
  ## Requisites
14
16
 
@@ -17,13 +19,13 @@ Claude Code Rust replaces the stock Claude Code terminal interface with a native
17
19
 
18
20
  ## Install
19
21
 
20
- ### npm (global, recommended)
22
+ ### pnpm (global, recommended)
21
23
 
22
24
  ```bash
23
- npm install -g claude-code-rust
25
+ pnpm add -g claude-code-rust
24
26
  ```
25
27
 
26
- The npm package installs a `claude-rs` command and downloads the matching
28
+ The published package installs a `claude-rs` command and downloads the matching
27
29
  prebuilt release binary for your platform during `postinstall`.
28
30
 
29
31
  ## Usage
@@ -56,9 +58,7 @@ Three-layer design:
56
58
 
57
59
  ## Known Limitations
58
60
 
59
- - Token usage and cost tracking can be partial when upstream runtime events do not include full usage data.
60
- - Session resume via `--resume` depends on runtime support and account/session availability.
61
- - `/login` and `/logout` are intentionally not offered in command discovery for this release.
61
+ - The config view includes the Settings tab but the Status, Usage, and MCP tabs are not yet implemented.
62
62
 
63
63
  ## Status
64
64
 
@@ -0,0 +1,75 @@
1
+ import { emitSessionUpdate } from "./events.js";
2
+ function availableAgentsSignature(agents) {
3
+ return JSON.stringify(agents);
4
+ }
5
+ function normalizeAvailableAgentName(value) {
6
+ if (typeof value !== "string") {
7
+ return "";
8
+ }
9
+ return value.trim();
10
+ }
11
+ export function mapAvailableAgents(value) {
12
+ if (!Array.isArray(value)) {
13
+ return [];
14
+ }
15
+ const byName = new Map();
16
+ for (const entry of value) {
17
+ if (!entry || typeof entry !== "object") {
18
+ continue;
19
+ }
20
+ const record = entry;
21
+ const name = normalizeAvailableAgentName(record.name);
22
+ if (!name) {
23
+ continue;
24
+ }
25
+ const description = typeof record.description === "string" ? record.description : "";
26
+ const model = typeof record.model === "string" && record.model.trim().length > 0 ? record.model : undefined;
27
+ const existing = byName.get(name);
28
+ if (!existing) {
29
+ byName.set(name, { name, description, model });
30
+ continue;
31
+ }
32
+ if (existing.description.trim().length === 0 && description.trim().length > 0) {
33
+ existing.description = description;
34
+ }
35
+ if (!existing.model && model) {
36
+ existing.model = model;
37
+ }
38
+ }
39
+ return [...byName.values()].sort((a, b) => a.name.localeCompare(b.name));
40
+ }
41
+ export function mapAvailableAgentsFromNames(value) {
42
+ if (!Array.isArray(value)) {
43
+ return [];
44
+ }
45
+ const byName = new Map();
46
+ for (const entry of value) {
47
+ const name = normalizeAvailableAgentName(entry);
48
+ if (!name || byName.has(name)) {
49
+ continue;
50
+ }
51
+ byName.set(name, { name, description: "" });
52
+ }
53
+ return [...byName.values()].sort((a, b) => a.name.localeCompare(b.name));
54
+ }
55
+ export function emitAvailableAgentsIfChanged(session, agents) {
56
+ const signature = availableAgentsSignature(agents);
57
+ if (session.lastAvailableAgentsSignature === signature) {
58
+ return;
59
+ }
60
+ session.lastAvailableAgentsSignature = signature;
61
+ emitSessionUpdate(session.sessionId, { type: "available_agents_update", agents });
62
+ }
63
+ export function refreshAvailableAgents(session) {
64
+ if (typeof session.query.supportedAgents !== "function") {
65
+ return;
66
+ }
67
+ void session.query
68
+ .supportedAgents()
69
+ .then((agents) => {
70
+ emitAvailableAgentsIfChanged(session, mapAvailableAgents(agents));
71
+ })
72
+ .catch(() => {
73
+ // Best-effort only.
74
+ });
75
+ }
@@ -25,13 +25,6 @@ function expectString(record, key, context) {
25
25
  }
26
26
  return value;
27
27
  }
28
- function expectBoolean(record, key, context) {
29
- const value = record[key];
30
- if (typeof value !== "boolean") {
31
- throw new Error(`${context}.${key} must be a boolean`);
32
- }
33
- return value;
34
- }
35
28
  function optionalString(record, key, context) {
36
29
  const value = record[key];
37
30
  if (value === undefined || value === null) {
@@ -49,6 +42,45 @@ function optionalMetadata(record, key) {
49
42
  }
50
43
  return asRecord(value, `${key} metadata`);
51
44
  }
45
+ function optionalLaunchSettings(record, key, context) {
46
+ const value = record[key];
47
+ if (value === undefined || value === null) {
48
+ return {};
49
+ }
50
+ const parsed = asRecord(value, `${context}.${key}`);
51
+ const model = optionalString(parsed, "model", `${context}.${key}`);
52
+ const language = optionalString(parsed, "language", `${context}.${key}`);
53
+ const permissionMode = optionalString(parsed, "permission_mode", `${context}.${key}`);
54
+ const thinkingMode = optionalThinkingMode(parsed, "thinking_mode", `${context}.${key}`);
55
+ const effortLevel = optionalEffortLevel(parsed, "effort_level", `${context}.${key}`);
56
+ return {
57
+ ...(model ? { model } : {}),
58
+ ...(language ? { language } : {}),
59
+ ...(permissionMode ? { permission_mode: permissionMode } : {}),
60
+ ...(thinkingMode ? { thinking_mode: thinkingMode } : {}),
61
+ ...(effortLevel ? { effort_level: effortLevel } : {}),
62
+ };
63
+ }
64
+ function optionalThinkingMode(record, key, context) {
65
+ const value = optionalString(record, key, context);
66
+ if (value === undefined) {
67
+ return undefined;
68
+ }
69
+ if (value === "adaptive" || value === "disabled") {
70
+ return value;
71
+ }
72
+ throw new Error(`${context}.${key} must be "adaptive" or "disabled" when provided`);
73
+ }
74
+ function optionalEffortLevel(record, key, context) {
75
+ const value = optionalString(record, key, context);
76
+ if (value === undefined) {
77
+ return undefined;
78
+ }
79
+ if (value === "low" || value === "medium" || value === "high") {
80
+ return value;
81
+ }
82
+ throw new Error(`${context}.${key} must be "low", "medium", or "high" when provided`);
83
+ }
52
84
  function parsePromptChunks(record, context) {
53
85
  const rawChunks = record.chunks;
54
86
  if (!Array.isArray(rawChunks)) {
@@ -76,23 +108,22 @@ export function parseCommandEnvelope(line) {
76
108
  return {
77
109
  command: "create_session",
78
110
  cwd: expectString(raw, "cwd", "create_session"),
79
- yolo: expectBoolean(raw, "yolo", "create_session"),
80
- model: optionalString(raw, "model", "create_session"),
81
111
  resume: optionalString(raw, "resume", "create_session"),
112
+ launch_settings: optionalLaunchSettings(raw, "launch_settings", "create_session"),
82
113
  metadata: optionalMetadata(raw, "metadata"),
83
114
  };
84
- case "load_session":
115
+ case "resume_session":
85
116
  return {
86
- command: "load_session",
87
- session_id: expectString(raw, "session_id", "load_session"),
117
+ command: "resume_session",
118
+ session_id: expectString(raw, "session_id", "resume_session"),
119
+ launch_settings: optionalLaunchSettings(raw, "launch_settings", "resume_session"),
88
120
  metadata: optionalMetadata(raw, "metadata"),
89
121
  };
90
122
  case "new_session":
91
123
  return {
92
124
  command: "new_session",
93
125
  cwd: expectString(raw, "cwd", "new_session"),
94
- yolo: expectBoolean(raw, "yolo", "new_session"),
95
- model: optionalString(raw, "model", "new_session"),
126
+ launch_settings: optionalLaunchSettings(raw, "launch_settings", "new_session"),
96
127
  };
97
128
  case "prompt":
98
129
  return {
@@ -0,0 +1,55 @@
1
+ import { looksLikeAuthRequired } from "./auth.js";
2
+ import { writeEvent } from "./events.js";
3
+ import { emitSessionUpdate } from "./events.js";
4
+ import { parseFastModeState } from "./state_parsing.js";
5
+ export function emitAuthRequired(session, detail) {
6
+ if (session.authHintSent) {
7
+ return;
8
+ }
9
+ session.authHintSent = true;
10
+ writeEvent({
11
+ event: "auth_required",
12
+ method_name: "Claude Login",
13
+ method_description: detail && detail.trim().length > 0
14
+ ? detail
15
+ : "Type /login to authenticate.",
16
+ });
17
+ }
18
+ export function looksLikePlanLimitError(input) {
19
+ const normalized = input.toLowerCase();
20
+ return (normalized.includes("rate limit") ||
21
+ normalized.includes("rate-limit") ||
22
+ normalized.includes("max turns") ||
23
+ normalized.includes("max budget") ||
24
+ normalized.includes("quota") ||
25
+ normalized.includes("plan limit") ||
26
+ normalized.includes("too many requests") ||
27
+ normalized.includes("insufficient quota") ||
28
+ normalized.includes("429"));
29
+ }
30
+ export function classifyTurnErrorKind(subtype, errors, assistantError) {
31
+ const combined = errors.join("\n");
32
+ if (subtype === "error_max_turns" ||
33
+ subtype === "error_max_budget_usd" ||
34
+ assistantError === "billing_error" ||
35
+ assistantError === "rate_limit" ||
36
+ (combined.length > 0 && looksLikePlanLimitError(combined))) {
37
+ return "plan_limit";
38
+ }
39
+ if (assistantError === "authentication_failed" ||
40
+ errors.some((entry) => looksLikeAuthRequired(entry))) {
41
+ return "auth_required";
42
+ }
43
+ if (assistantError === "server_error") {
44
+ return "internal";
45
+ }
46
+ return "other";
47
+ }
48
+ export function emitFastModeUpdateIfChanged(session, value) {
49
+ const next = parseFastModeState(value);
50
+ if (!next || next === session.fastModeState) {
51
+ return;
52
+ }
53
+ session.fastModeState = next;
54
+ emitSessionUpdate(session.sessionId, { type: "fast_mode_update", fast_mode_state: next });
55
+ }
@@ -0,0 +1,83 @@
1
+ import { listSessions } from "@anthropic-ai/claude-agent-sdk";
2
+ import { buildModeState } from "./commands.js";
3
+ import { mapSdkSessions } from "./history.js";
4
+ const SESSION_LIST_LIMIT = 50;
5
+ export function writeEvent(event, requestId) {
6
+ const envelope = {
7
+ ...(requestId ? { request_id: requestId } : {}),
8
+ ...event,
9
+ };
10
+ process.stdout.write(`${JSON.stringify(envelope)}\n`);
11
+ }
12
+ export function failConnection(message, requestId) {
13
+ writeEvent({ event: "connection_failed", message }, requestId);
14
+ }
15
+ export function slashError(sessionId, message, requestId) {
16
+ writeEvent({ event: "slash_error", session_id: sessionId, message }, requestId);
17
+ }
18
+ export function emitSessionUpdate(sessionId, update) {
19
+ writeEvent({ event: "session_update", session_id: sessionId, update });
20
+ }
21
+ export function emitConnectEvent(session) {
22
+ const historyUpdates = session.resumeUpdates;
23
+ const connectEvent = session.connectEvent === "session_replaced"
24
+ ? {
25
+ event: "session_replaced",
26
+ session_id: session.sessionId,
27
+ cwd: session.cwd,
28
+ model_name: session.model,
29
+ available_models: session.availableModels,
30
+ mode: session.mode ? buildModeState(session.mode) : null,
31
+ ...(historyUpdates && historyUpdates.length > 0 ? { history_updates: historyUpdates } : {}),
32
+ }
33
+ : {
34
+ event: "connected",
35
+ session_id: session.sessionId,
36
+ cwd: session.cwd,
37
+ model_name: session.model,
38
+ available_models: session.availableModels,
39
+ mode: session.mode ? buildModeState(session.mode) : null,
40
+ ...(historyUpdates && historyUpdates.length > 0 ? { history_updates: historyUpdates } : {}),
41
+ };
42
+ writeEvent(connectEvent, session.connectRequestId);
43
+ session.connectRequestId = undefined;
44
+ session.connected = true;
45
+ session.authHintSent = false;
46
+ session.resumeUpdates = undefined;
47
+ const staleSessions = session.sessionsToCloseAfterConnect;
48
+ session.sessionsToCloseAfterConnect = undefined;
49
+ if (!staleSessions || staleSessions.length === 0) {
50
+ refreshSessionsList();
51
+ return;
52
+ }
53
+ void (async () => {
54
+ // Lazy import to break circular dependency at module-evaluation time.
55
+ const { sessions, closeSession } = await import("./session_lifecycle.js");
56
+ for (const stale of staleSessions) {
57
+ if (stale === session) {
58
+ continue;
59
+ }
60
+ if (sessions.get(stale.sessionId) === stale) {
61
+ sessions.delete(stale.sessionId);
62
+ }
63
+ await closeSession(stale);
64
+ }
65
+ refreshSessionsList();
66
+ })();
67
+ }
68
+ export async function emitSessionsList(requestId) {
69
+ try {
70
+ const sdkSessions = await listSessions({ limit: SESSION_LIST_LIMIT });
71
+ writeEvent({ event: "sessions_listed", sessions: mapSdkSessions(sdkSessions, SESSION_LIST_LIMIT) }, requestId);
72
+ }
73
+ catch (error) {
74
+ const message = error instanceof Error ? error.message : String(error);
75
+ console.error(`[sdk warn] listSessions failed: ${message}`);
76
+ writeEvent({ event: "sessions_listed", sessions: [] }, requestId);
77
+ }
78
+ }
79
+ export function refreshSessionsList() {
80
+ void emitSessionsList().catch(() => {
81
+ // Defensive no-op.
82
+ });
83
+ }
@@ -1,224 +1,21 @@
1
- import fs from "node:fs";
2
- import os from "node:os";
3
- import path from "node:path";
4
- import { TOOL_RESULT_TYPES, buildToolResultFields, createToolCall, isToolUseBlockType } from "./tooling.js";
5
1
  import { asRecordOrNull } from "./shared.js";
6
- import { buildUsageUpdateFromResult } from "./usage.js";
7
- function normalizeUserPromptText(raw) {
8
- let text = raw.replace(/<context[\s\S]*/gi, " ");
9
- text = text.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1");
10
- text = text.replace(/\s+/g, " ").trim();
11
- return text;
12
- }
13
- function truncateTextByChars(text, maxChars) {
14
- const chars = Array.from(text);
15
- if (chars.length <= maxChars) {
16
- return text;
17
- }
18
- return chars.slice(0, maxChars).join("");
19
- }
20
- function firstUserMessageTitleFromRecord(record) {
21
- if (record.type !== "user") {
22
- return undefined;
23
- }
24
- const message = asRecordOrNull(record.message);
25
- if (!message || message.role !== "user" || !Array.isArray(message.content)) {
26
- return undefined;
27
- }
28
- const parts = [];
29
- for (const item of message.content) {
30
- const block = asRecordOrNull(item);
31
- if (!block || block.type !== "text" || typeof block.text !== "string") {
32
- continue;
33
- }
34
- const cleaned = normalizeUserPromptText(block.text);
35
- if (!cleaned) {
36
- continue;
37
- }
38
- parts.push(cleaned);
39
- const combined = parts.join(" ");
40
- if (Array.from(combined).length >= 180) {
41
- return truncateTextByChars(combined, 180);
42
- }
43
- }
44
- if (parts.length === 0) {
2
+ import { TOOL_RESULT_TYPES, buildToolResultFields, createToolCall, isToolUseBlockType } from "./tooling.js";
3
+ function nonEmptyTrimmed(value) {
4
+ if (typeof value !== "string") {
45
5
  return undefined;
46
6
  }
47
- return truncateTextByChars(parts.join(" "), 180);
48
- }
49
- function extractSessionPreviewFromJsonl(filePath) {
50
- let text;
51
- try {
52
- text = fs.readFileSync(filePath, "utf8");
53
- }
54
- catch {
55
- return {};
56
- }
57
- let cwd;
58
- let title;
59
- const lines = text.split(/\r?\n/);
60
- for (const rawLine of lines) {
61
- const line = rawLine.trim();
62
- if (line.length === 0) {
63
- continue;
64
- }
65
- let parsed;
66
- try {
67
- parsed = JSON.parse(line);
68
- }
69
- catch {
70
- continue;
71
- }
72
- const record = asRecordOrNull(parsed);
73
- if (!record) {
74
- continue;
75
- }
76
- if (!cwd && typeof record.cwd === "string" && record.cwd.trim().length > 0) {
77
- cwd = record.cwd;
78
- }
79
- if (!title) {
80
- title = firstUserMessageTitleFromRecord(record);
81
- }
82
- if (cwd && title) {
83
- break;
84
- }
85
- }
86
- return {
87
- ...(cwd ? { cwd } : {}),
88
- ...(title ? { title } : {}),
89
- };
7
+ const trimmed = value.trim();
8
+ return trimmed.length > 0 ? trimmed : undefined;
90
9
  }
91
- export function listRecentPersistedSessions(limit = 8) {
92
- const root = path.join(os.homedir(), ".claude", "projects");
93
- if (!fs.existsSync(root)) {
94
- return [];
95
- }
10
+ function messageCandidates(raw) {
96
11
  const candidates = [];
97
- let projectDirs;
98
- try {
99
- projectDirs = fs.readdirSync(root, { withFileTypes: true }).filter((dirent) => dirent.isDirectory());
100
- }
101
- catch {
102
- return [];
103
- }
104
- for (const dirent of projectDirs) {
105
- const projectDir = path.join(root, dirent.name);
106
- let sessionFiles;
107
- try {
108
- sessionFiles = fs
109
- .readdirSync(projectDir, { withFileTypes: true })
110
- .filter((entry) => entry.isFile() && entry.name.endsWith(".jsonl"));
111
- }
112
- catch {
113
- sessionFiles = [];
114
- }
115
- for (const sessionFile of sessionFiles) {
116
- const sessionId = sessionFile.name.slice(0, -".jsonl".length);
117
- if (!sessionId) {
118
- continue;
119
- }
120
- let mtimeMs = 0;
121
- try {
122
- mtimeMs = fs.statSync(path.join(projectDir, sessionFile.name)).mtimeMs;
123
- }
124
- catch {
125
- continue;
126
- }
127
- if (!Number.isFinite(mtimeMs) || mtimeMs <= 0) {
128
- continue;
129
- }
130
- candidates.push({
131
- session_id: sessionId,
132
- cwd: "",
133
- file_path: path.join(projectDir, sessionFile.name),
134
- updated_at: new Date(mtimeMs).toISOString(),
135
- sort_ms: mtimeMs,
136
- });
137
- }
138
- }
139
- candidates.sort((a, b) => b.sort_ms - a.sort_ms);
140
- const deduped = [];
141
- const seen = new Set();
142
- for (const candidate of candidates) {
143
- if (seen.has(candidate.session_id)) {
144
- continue;
145
- }
146
- seen.add(candidate.session_id);
147
- const preview = extractSessionPreviewFromJsonl(candidate.file_path);
148
- const cwd = preview.cwd?.trim();
149
- if (!cwd) {
150
- continue;
151
- }
152
- deduped.push({
153
- session_id: candidate.session_id,
154
- cwd,
155
- file_path: candidate.file_path,
156
- ...(preview.title ? { title: preview.title } : {}),
157
- ...(candidate.updated_at ? { updated_at: candidate.updated_at } : {}),
158
- sort_ms: candidate.sort_ms,
159
- });
160
- if (deduped.length >= limit) {
161
- break;
162
- }
163
- }
164
- return deduped;
165
- }
166
- export function resolvePersistedSessionEntry(sessionId) {
167
- if (sessionId.trim().length === 0 ||
168
- sessionId.includes("/") ||
169
- sessionId.includes("\\") ||
170
- sessionId.includes("..")) {
171
- return null;
172
- }
173
- const root = path.join(os.homedir(), ".claude", "projects");
174
- if (!fs.existsSync(root)) {
175
- return null;
176
- }
177
- let projectDirs;
178
- try {
179
- projectDirs = fs.readdirSync(root, { withFileTypes: true }).filter((dirent) => dirent.isDirectory());
180
- }
181
- catch {
182
- return null;
183
- }
184
- let best = null;
185
- for (const dirent of projectDirs) {
186
- const filePath = path.join(root, dirent.name, `${sessionId}.jsonl`);
187
- if (!fs.existsSync(filePath)) {
188
- continue;
189
- }
190
- const preview = extractSessionPreviewFromJsonl(filePath);
191
- const cwd = preview.cwd?.trim();
192
- if (!cwd) {
193
- continue;
194
- }
195
- let mtimeMs = 0;
196
- try {
197
- mtimeMs = fs.statSync(filePath).mtimeMs;
198
- }
199
- catch {
200
- mtimeMs = 0;
201
- }
202
- if (!best || mtimeMs >= best.sort_ms) {
203
- best = {
204
- session_id: sessionId,
205
- cwd,
206
- file_path: filePath,
207
- sort_ms: mtimeMs,
208
- };
209
- }
210
- }
211
- return best;
212
- }
213
- function persistedMessageCandidates(record) {
214
- const candidates = [];
215
- const topLevel = asRecordOrNull(record.message);
12
+ const topLevel = asRecordOrNull(raw);
216
13
  if (topLevel) {
217
14
  candidates.push(topLevel);
218
- }
219
- const nested = asRecordOrNull(asRecordOrNull(asRecordOrNull(record.data)?.message)?.message);
220
- if (nested) {
221
- candidates.push(nested);
15
+ const nested = asRecordOrNull(topLevel.message);
16
+ if (nested) {
17
+ candidates.push(nested);
18
+ }
222
19
  }
223
20
  return candidates;
224
21
  }
@@ -264,53 +61,48 @@ function pushResumeToolResult(updates, toolCalls, block) {
264
61
  base.content = fields.content;
265
62
  }
266
63
  }
267
- function pushResumeUsageUpdate(updates, message, emittedUsageMessageIds) {
268
- const messageId = typeof message.id === "string" ? message.id : "";
269
- if (messageId && emittedUsageMessageIds.has(messageId)) {
270
- return;
271
- }
272
- const usageUpdate = buildUsageUpdateFromResult(message);
273
- if (!usageUpdate) {
274
- return;
275
- }
276
- updates.push(usageUpdate);
277
- if (messageId) {
278
- emittedUsageMessageIds.add(messageId);
279
- }
64
+ function summaryFromSession(info) {
65
+ return (nonEmptyTrimmed(info.summary) ??
66
+ nonEmptyTrimmed(info.customTitle) ??
67
+ nonEmptyTrimmed(info.firstPrompt) ??
68
+ info.sessionId);
280
69
  }
281
- export function extractSessionHistoryUpdatesFromJsonl(filePath) {
282
- let text;
283
- try {
284
- text = fs.readFileSync(filePath, "utf8");
285
- }
286
- catch {
287
- return [];
288
- }
289
- const updates = [];
290
- const toolCalls = new Map();
291
- const emittedUsageMessageIds = new Set();
292
- const lines = text.split(/\r?\n/);
293
- for (const rawLine of lines) {
294
- const line = rawLine.trim();
295
- if (line.length === 0) {
296
- continue;
297
- }
298
- let parsed;
299
- try {
300
- parsed = JSON.parse(line);
301
- }
302
- catch {
70
+ export function mapSdkSessionInfo(info) {
71
+ return {
72
+ session_id: info.sessionId,
73
+ summary: summaryFromSession(info),
74
+ last_modified_ms: info.lastModified,
75
+ file_size_bytes: info.fileSize,
76
+ ...(nonEmptyTrimmed(info.cwd) ? { cwd: info.cwd?.trim() } : {}),
77
+ ...(nonEmptyTrimmed(info.gitBranch) ? { git_branch: info.gitBranch?.trim() } : {}),
78
+ ...(nonEmptyTrimmed(info.customTitle) ? { custom_title: info.customTitle?.trim() } : {}),
79
+ ...(nonEmptyTrimmed(info.firstPrompt) ? { first_prompt: info.firstPrompt?.trim() } : {}),
80
+ };
81
+ }
82
+ export function mapSdkSessions(infos, limit = 50) {
83
+ const sorted = [...infos].sort((a, b) => b.lastModified - a.lastModified);
84
+ const entries = [];
85
+ const seen = new Set();
86
+ for (const info of sorted) {
87
+ if (!info.sessionId || seen.has(info.sessionId)) {
303
88
  continue;
304
89
  }
305
- const record = asRecordOrNull(parsed);
306
- if (!record) {
307
- continue;
90
+ seen.add(info.sessionId);
91
+ entries.push(mapSdkSessionInfo(info));
92
+ if (entries.length >= limit) {
93
+ break;
308
94
  }
309
- for (const message of persistedMessageCandidates(record)) {
310
- const role = message.role;
311
- if (role !== "user" && role !== "assistant") {
312
- continue;
313
- }
95
+ }
96
+ return entries;
97
+ }
98
+ export function mapSessionMessagesToUpdates(messages) {
99
+ const updates = [];
100
+ const toolCalls = new Map();
101
+ for (const entry of messages) {
102
+ const fallbackRole = entry.type === "assistant" ? "assistant" : "user";
103
+ for (const message of messageCandidates(entry.message)) {
104
+ const roleCandidate = message.role;
105
+ const role = roleCandidate === "assistant" || roleCandidate === "user" ? roleCandidate : fallbackRole;
314
106
  const content = Array.isArray(message.content) ? message.content : [];
315
107
  for (const item of content) {
316
108
  const block = asRecordOrNull(item);
@@ -337,7 +129,6 @@ export function extractSessionHistoryUpdatesFromJsonl(filePath) {
337
129
  pushResumeTextChunk(updates, role, "[image]");
338
130
  }
339
131
  }
340
- pushResumeUsageUpdate(updates, message, emittedUsageMessageIds);
341
132
  }
342
133
  }
343
134
  return updates;