@teddysc/claude-run 0.11.0 → 0.12.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 +11 -1
- package/dist/index.js +168 -1
- package/dist/web/assets/index-CrTCiHsZ.js +328 -0
- package/dist/web/assets/index-_XHzPMzO.css +1 -0
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/dist/web/assets/index-BgICkv1W.js +0 -318
- package/dist/web/assets/index-DM8xkSFM.css +0 -1
package/README.md
CHANGED
|
@@ -30,8 +30,17 @@ bun install -g @teddysc/claude-run@latest
|
|
|
30
30
|
|
|
31
31
|
The browser will open automatically at http://localhost:12001.
|
|
32
32
|
|
|
33
|
+
## Specifications
|
|
34
|
+
|
|
35
|
+
See [spec.md](spec.md)
|
|
36
|
+
|
|
33
37
|
## Changelog
|
|
34
38
|
|
|
39
|
+
### 0.12.0
|
|
40
|
+
- Rename sessions directly from the web UI
|
|
41
|
+
- Searchable project dropdown with fuzzy matching on name and path
|
|
42
|
+
- Session summary caching for improved performance
|
|
43
|
+
|
|
35
44
|
### 0.11.0
|
|
36
45
|
- Display session metadata (model, start/end times) in conversation header
|
|
37
46
|
- Show full project path with ~ shorthand in header and sidebar tooltips
|
|
@@ -87,10 +96,11 @@ The browser will open automatically at http://localhost:12001.
|
|
|
87
96
|
- **Truncation options** - Limit long tool outputs by line count or character count
|
|
88
97
|
- **Batch export** - Export multiple conversations at once
|
|
89
98
|
- **Real-time streaming** - Watch conversations update live as Claude responds
|
|
90
|
-
- **Filter by project** -
|
|
99
|
+
- **Filter by project** - Searchable dropdown with fuzzy matching on project name and path
|
|
91
100
|
- **Resume sessions** - Copy the resume command to continue any conversation in your terminal
|
|
92
101
|
- **Copy messages** - Click the copy button on any message to copy its text content
|
|
93
102
|
- **Session metadata** - See model and start/end times in the conversation header
|
|
103
|
+
- **Rename sessions** - Click the conversation title to rename a session
|
|
94
104
|
- **Collapsible sidebar** - Maximize your viewing area
|
|
95
105
|
- **Dark mode** - Easy on the eyes
|
|
96
106
|
- **Clean UI** - Familiar chat interface with collapsible tool calls
|
package/dist/index.js
CHANGED
|
@@ -88,6 +88,7 @@ function mergeSidechainText(messages, sidechainMap) {
|
|
|
88
88
|
var claudeDir = join(homedir(), ".claude");
|
|
89
89
|
var projectsDir = join(claudeDir, "projects");
|
|
90
90
|
var fileIndex = /* @__PURE__ */ new Map();
|
|
91
|
+
var summaryCache = /* @__PURE__ */ new Map();
|
|
91
92
|
var historyCache = null;
|
|
92
93
|
var pendingRequests = /* @__PURE__ */ new Map();
|
|
93
94
|
function initStorage(dir) {
|
|
@@ -102,6 +103,58 @@ function invalidateHistoryCache() {
|
|
|
102
103
|
}
|
|
103
104
|
function addToFileIndex(sessionId, filePath) {
|
|
104
105
|
fileIndex.set(sessionId, filePath);
|
|
106
|
+
summaryCache.delete(sessionId);
|
|
107
|
+
}
|
|
108
|
+
async function readLastNonEmptyLine(filePath) {
|
|
109
|
+
try {
|
|
110
|
+
const fileStat = await stat(filePath);
|
|
111
|
+
const fileSize = fileStat.size;
|
|
112
|
+
if (fileSize === 0) {
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
const chunkSize = Math.min(8192, fileSize);
|
|
116
|
+
const start = Math.max(0, fileSize - chunkSize);
|
|
117
|
+
const fileHandle = await open(filePath, "r");
|
|
118
|
+
try {
|
|
119
|
+
const buffer = Buffer.alloc(chunkSize);
|
|
120
|
+
await fileHandle.read(buffer, 0, chunkSize, start);
|
|
121
|
+
const text = buffer.toString("utf-8");
|
|
122
|
+
const lines = text.split("\n").filter((line) => line.trim().length > 0);
|
|
123
|
+
if (lines.length === 0) {
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
return lines[lines.length - 1] ?? null;
|
|
127
|
+
} finally {
|
|
128
|
+
await fileHandle.close();
|
|
129
|
+
}
|
|
130
|
+
} catch {
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
async function getSessionSummary(sessionId) {
|
|
135
|
+
if (summaryCache.has(sessionId)) {
|
|
136
|
+
return summaryCache.get(sessionId) ?? null;
|
|
137
|
+
}
|
|
138
|
+
const filePath = await findSessionFile(sessionId);
|
|
139
|
+
if (!filePath) {
|
|
140
|
+
summaryCache.set(sessionId, null);
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
const lastLine = await readLastNonEmptyLine(filePath);
|
|
144
|
+
if (!lastLine) {
|
|
145
|
+
summaryCache.set(sessionId, null);
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
try {
|
|
149
|
+
const msg = JSON.parse(lastLine);
|
|
150
|
+
if (msg.type === "summary" && typeof msg.summary === "string" && msg.summary.trim().length > 0) {
|
|
151
|
+
summaryCache.set(sessionId, msg.summary);
|
|
152
|
+
return msg.summary;
|
|
153
|
+
}
|
|
154
|
+
} catch {
|
|
155
|
+
}
|
|
156
|
+
summaryCache.set(sessionId, null);
|
|
157
|
+
return null;
|
|
105
158
|
}
|
|
106
159
|
function encodeProjectPath(path) {
|
|
107
160
|
return path.replace(/[/.]/g, "-");
|
|
@@ -316,9 +369,10 @@ async function getSessions() {
|
|
|
316
369
|
continue;
|
|
317
370
|
}
|
|
318
371
|
seenIds.add(sessionId);
|
|
372
|
+
const summary = await getSessionSummary(sessionId);
|
|
319
373
|
sessions.push({
|
|
320
374
|
id: sessionId,
|
|
321
|
-
display: entry.display,
|
|
375
|
+
display: summary ?? entry.display,
|
|
322
376
|
timestamp: entry.timestamp,
|
|
323
377
|
project: entry.project,
|
|
324
378
|
projectName: getProjectName(entry.project)
|
|
@@ -369,6 +423,7 @@ async function deleteSession(sessionId) {
|
|
|
369
423
|
console.log(`[deleteSession] No file found for session: ${sessionId}`);
|
|
370
424
|
}
|
|
371
425
|
fileIndex.delete(sessionId);
|
|
426
|
+
summaryCache.delete(sessionId);
|
|
372
427
|
const historyPath = join(claudeDir, "history.jsonl");
|
|
373
428
|
let historyChanged = false;
|
|
374
429
|
try {
|
|
@@ -405,6 +460,87 @@ async function deleteSession(sessionId) {
|
|
|
405
460
|
}
|
|
406
461
|
return fileDeleted || historyChanged;
|
|
407
462
|
}
|
|
463
|
+
async function getSessionFilePath(sessionId) {
|
|
464
|
+
return findSessionFile(sessionId);
|
|
465
|
+
}
|
|
466
|
+
async function renameSessionDisplay(params) {
|
|
467
|
+
const { sessionId, display, project, timestamp } = params;
|
|
468
|
+
const historyPath = join(claudeDir, "history.jsonl");
|
|
469
|
+
let changed = false;
|
|
470
|
+
let summaryChanged = false;
|
|
471
|
+
try {
|
|
472
|
+
const content = await readFile(historyPath, "utf-8");
|
|
473
|
+
const lines = content.split("\n");
|
|
474
|
+
const updated = [];
|
|
475
|
+
for (const line of lines) {
|
|
476
|
+
if (!line.trim()) {
|
|
477
|
+
continue;
|
|
478
|
+
}
|
|
479
|
+
try {
|
|
480
|
+
const entry = JSON.parse(line);
|
|
481
|
+
const matchesId = entry.sessionId && entry.sessionId === sessionId;
|
|
482
|
+
const matchesFallback = !entry.sessionId && project && typeof timestamp === "number" && entry.project === project && entry.timestamp === timestamp;
|
|
483
|
+
if (matchesId || matchesFallback) {
|
|
484
|
+
if (entry.display !== display) {
|
|
485
|
+
entry.display = display;
|
|
486
|
+
changed = true;
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
updated.push(JSON.stringify(entry));
|
|
490
|
+
} catch {
|
|
491
|
+
updated.push(line);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
if (changed) {
|
|
495
|
+
await writeFile(historyPath, updated.join("\n") + "\n", "utf-8");
|
|
496
|
+
if (historyCache) {
|
|
497
|
+
historyCache = historyCache.map((entry) => {
|
|
498
|
+
const matchesId = entry.sessionId && entry.sessionId === sessionId;
|
|
499
|
+
const matchesFallback = !entry.sessionId && project && typeof timestamp === "number" && entry.project === project && entry.timestamp === timestamp;
|
|
500
|
+
if (matchesId || matchesFallback) {
|
|
501
|
+
return { ...entry, display };
|
|
502
|
+
}
|
|
503
|
+
return entry;
|
|
504
|
+
});
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
} catch (err) {
|
|
508
|
+
console.error(`[renameSessionDisplay] Failed to update history.jsonl for ${sessionId}:`, err);
|
|
509
|
+
return false;
|
|
510
|
+
}
|
|
511
|
+
const sessionFile = await findSessionFile(sessionId);
|
|
512
|
+
if (sessionFile) {
|
|
513
|
+
try {
|
|
514
|
+
const content = await readFile(sessionFile, "utf-8");
|
|
515
|
+
const lines = content.split("\n");
|
|
516
|
+
let summaryUpdated = false;
|
|
517
|
+
const updatedLines = lines.map((line) => {
|
|
518
|
+
if (!line.trim()) {
|
|
519
|
+
return line;
|
|
520
|
+
}
|
|
521
|
+
try {
|
|
522
|
+
const msg = JSON.parse(line);
|
|
523
|
+
if (msg.type === "summary") {
|
|
524
|
+
msg.summary = display;
|
|
525
|
+
summaryUpdated = true;
|
|
526
|
+
return JSON.stringify(msg);
|
|
527
|
+
}
|
|
528
|
+
} catch {
|
|
529
|
+
}
|
|
530
|
+
return line;
|
|
531
|
+
});
|
|
532
|
+
if (!summaryUpdated) {
|
|
533
|
+
updatedLines.push(JSON.stringify({ type: "summary", summary: display }));
|
|
534
|
+
}
|
|
535
|
+
await writeFile(sessionFile, updatedLines.join("\n") + "\n", "utf-8");
|
|
536
|
+
summaryCache.set(sessionId, display);
|
|
537
|
+
summaryChanged = true;
|
|
538
|
+
} catch (err) {
|
|
539
|
+
console.error(`[renameSessionDisplay] Failed to update session file for ${sessionId}:`, err);
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
return changed || summaryChanged;
|
|
543
|
+
}
|
|
408
544
|
async function getConversation(sessionId) {
|
|
409
545
|
return dedupe(`getConversation:${sessionId}`, async () => {
|
|
410
546
|
const filePath = await findSessionFile(sessionId);
|
|
@@ -771,6 +907,37 @@ function createServer(options) {
|
|
|
771
907
|
console.log(`[/api/sessions/delete] Failed:`, failed);
|
|
772
908
|
return c.json({ deleted, failed });
|
|
773
909
|
});
|
|
910
|
+
app.post("/api/sessions/rename", async (c) => {
|
|
911
|
+
let body;
|
|
912
|
+
try {
|
|
913
|
+
body = await c.req.json();
|
|
914
|
+
} catch {
|
|
915
|
+
return c.json({ error: "Invalid JSON body" }, 400);
|
|
916
|
+
}
|
|
917
|
+
const id = body?.id?.trim();
|
|
918
|
+
const display = body?.display?.trim();
|
|
919
|
+
if (!id || !display) {
|
|
920
|
+
return c.json({ error: "Missing id/display" }, 400);
|
|
921
|
+
}
|
|
922
|
+
const ok = await renameSessionDisplay({
|
|
923
|
+
sessionId: id,
|
|
924
|
+
display,
|
|
925
|
+
project: body.project,
|
|
926
|
+
timestamp: body.timestamp
|
|
927
|
+
});
|
|
928
|
+
if (!ok) {
|
|
929
|
+
return c.json({ error: "Session not found" }, 404);
|
|
930
|
+
}
|
|
931
|
+
return c.json({ ok: true });
|
|
932
|
+
});
|
|
933
|
+
app.get("/api/sessions/:id/path", async (c) => {
|
|
934
|
+
const sessionId = c.req.param("id");
|
|
935
|
+
if (!sessionId) {
|
|
936
|
+
return c.json({ error: "Missing session id" }, 400);
|
|
937
|
+
}
|
|
938
|
+
const path = await getSessionFilePath(sessionId);
|
|
939
|
+
return c.json({ path });
|
|
940
|
+
});
|
|
774
941
|
app.post("/api/search", async (c) => {
|
|
775
942
|
let body;
|
|
776
943
|
try {
|