@teddysc/claude-run 0.12.0 → 0.13.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
@@ -36,6 +36,11 @@ See [spec.md](spec.md)
36
36
 
37
37
  ## Changelog
38
38
 
39
+ ### 0.13.0
40
+ - Display message count, duration, and JSONL size in conversation header
41
+ - Sidebar shows compact message count, duration, and JSONL size per session
42
+ - Add sorting options for sessions (by duration or JSONL size, asc/desc)
43
+
39
44
  ### 0.12.0
40
45
  - Rename sessions directly from the web UI
41
46
  - Searchable project dropdown with fuzzy matching on name and path
@@ -99,7 +104,9 @@ See [spec.md](spec.md)
99
104
  - **Filter by project** - Searchable dropdown with fuzzy matching on project name and path
100
105
  - **Resume sessions** - Copy the resume command to continue any conversation in your terminal
101
106
  - **Copy messages** - Click the copy button on any message to copy its text content
102
- - **Session metadata** - See model and start/end times in the conversation header
107
+ - **Session metadata** - See model, message count, duration, and JSONL size in the conversation header
108
+ - **Sidebar stats** - Compact message count, duration, and JSONL size per session
109
+ - **Sidebar sorting** - Sort sessions by duration or JSONL size (asc/desc)
103
110
  - **Rename sessions** - Click the conversation title to rename a session
104
111
  - **Collapsible sidebar** - Maximize your viewing area
105
112
  - **Dark mode** - Easy on the eyes
package/dist/index.js CHANGED
@@ -89,6 +89,7 @@ var claudeDir = join(homedir(), ".claude");
89
89
  var projectsDir = join(claudeDir, "projects");
90
90
  var fileIndex = /* @__PURE__ */ new Map();
91
91
  var summaryCache = /* @__PURE__ */ new Map();
92
+ var sessionStatsCache = /* @__PURE__ */ new Map();
92
93
  var historyCache = null;
93
94
  var pendingRequests = /* @__PURE__ */ new Map();
94
95
  function initStorage(dir) {
@@ -104,6 +105,80 @@ function invalidateHistoryCache() {
104
105
  function addToFileIndex(sessionId, filePath) {
105
106
  fileIndex.set(sessionId, filePath);
106
107
  summaryCache.delete(sessionId);
108
+ sessionStatsCache.delete(sessionId);
109
+ }
110
+ function invalidateSessionStats(sessionId) {
111
+ sessionStatsCache.delete(sessionId);
112
+ }
113
+ function parseMessageTimestamp(rawTimestamp) {
114
+ if (typeof rawTimestamp === "string") {
115
+ const parsed = Date.parse(rawTimestamp);
116
+ return Number.isNaN(parsed) ? null : parsed;
117
+ }
118
+ if (typeof rawTimestamp === "number" && Number.isFinite(rawTimestamp)) {
119
+ return rawTimestamp;
120
+ }
121
+ return null;
122
+ }
123
+ async function getSessionStats(sessionId) {
124
+ if (sessionStatsCache.has(sessionId)) {
125
+ return sessionStatsCache.get(sessionId);
126
+ }
127
+ const filePath = await findSessionFile(sessionId);
128
+ if (!filePath) {
129
+ const empty = {
130
+ messageCount: 0,
131
+ startTime: null,
132
+ endTime: null,
133
+ fileSizeBytes: null
134
+ };
135
+ sessionStatsCache.set(sessionId, empty);
136
+ return empty;
137
+ }
138
+ let fileSizeBytes = null;
139
+ try {
140
+ const fileStat = await stat(filePath);
141
+ fileSizeBytes = fileStat.size;
142
+ } catch {
143
+ fileSizeBytes = null;
144
+ }
145
+ let messageCount = 0;
146
+ let startTime = null;
147
+ let endTime = null;
148
+ const stream = createReadStream(filePath, { encoding: "utf-8" });
149
+ const rl = createInterface({ input: stream, crlfDelay: Infinity });
150
+ try {
151
+ for await (const line of rl) {
152
+ if (!line.trim()) {
153
+ continue;
154
+ }
155
+ try {
156
+ const msg = JSON.parse(line);
157
+ if (msg.type !== "user" && msg.type !== "assistant") {
158
+ continue;
159
+ }
160
+ messageCount += 1;
161
+ const parsed = parseMessageTimestamp(msg.timestamp);
162
+ if (parsed !== null) {
163
+ startTime = startTime === null ? parsed : Math.min(startTime, parsed);
164
+ endTime = endTime === null ? parsed : Math.max(endTime, parsed);
165
+ }
166
+ } catch {
167
+ continue;
168
+ }
169
+ }
170
+ } finally {
171
+ rl.close();
172
+ stream.close();
173
+ }
174
+ const stats = {
175
+ messageCount,
176
+ startTime,
177
+ endTime,
178
+ fileSizeBytes
179
+ };
180
+ sessionStatsCache.set(sessionId, stats);
181
+ return stats;
107
182
  }
108
183
  async function readLastNonEmptyLine(filePath) {
109
184
  try {
@@ -370,12 +445,17 @@ async function getSessions() {
370
445
  }
371
446
  seenIds.add(sessionId);
372
447
  const summary = await getSessionSummary(sessionId);
448
+ const stats = await getSessionStats(sessionId);
373
449
  sessions.push({
374
450
  id: sessionId,
375
451
  display: summary ?? entry.display,
376
452
  timestamp: entry.timestamp,
377
453
  project: entry.project,
378
- projectName: getProjectName(entry.project)
454
+ projectName: getProjectName(entry.project),
455
+ messageCount: stats.messageCount,
456
+ startTime: stats.startTime,
457
+ endTime: stats.endTime,
458
+ fileSizeBytes: stats.fileSizeBytes
379
459
  });
380
460
  }
381
461
  return sessions.sort((a, b) => b.timestamp - a.timestamp);
@@ -424,6 +504,7 @@ async function deleteSession(sessionId) {
424
504
  }
425
505
  fileIndex.delete(sessionId);
426
506
  summaryCache.delete(sessionId);
507
+ sessionStatsCache.delete(sessionId);
427
508
  const historyPath = join(claudeDir, "history.jsonl");
428
509
  let historyChanged = false;
429
510
  try {
@@ -1096,6 +1177,7 @@ function createServer(options) {
1096
1177
  });
1097
1178
  onSessionChange((sessionId, filePath) => {
1098
1179
  addToFileIndex(sessionId, filePath);
1180
+ invalidateSessionStats(sessionId);
1099
1181
  });
1100
1182
  startWatcher();
1101
1183
  let httpServer = null;