@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 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** - Focus on specific projects
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 {