claude-starter 1.3.2 → 1.3.4

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.
Files changed (2) hide show
  1. package/index.js +83 -10
  2. package/package.json +1 -1
package/index.js CHANGED
@@ -202,17 +202,20 @@ function loadSessionQuick(filePath, projectName) {
202
202
  const sessionId = path.basename(filePath, '.jsonl');
203
203
  const stat = fs.statSync(filePath);
204
204
 
205
+ // Use 32KB head buffer (up from 8KB) to handle sessions whose first user
206
+ // message is very large (e.g. pasted code blocks, long queries).
207
+ const HEAD_SIZE = 32768;
205
208
  const fd = fs.openSync(filePath, 'r');
206
- const headBuf = Buffer.alloc(Math.min(8192, stat.size));
209
+ const headBuf = Buffer.alloc(Math.min(HEAD_SIZE, stat.size));
207
210
  fs.readSync(fd, headBuf, 0, headBuf.length, 0);
208
211
 
209
212
  // Read tail with progressive expansion: start at 32KB, grow up to 256KB
210
213
  // until we find a JSON line with a top-level timestamp (to get accurate lastTs).
211
214
  let tailStr = '';
212
- if (stat.size > 8192) {
215
+ if (stat.size > HEAD_SIZE) {
213
216
  const tailSizes = [32768, 65536, 131072, 262144];
214
217
  for (const ts of tailSizes) {
215
- const tailSize = Math.min(ts, stat.size - 8192);
218
+ const tailSize = Math.min(ts, stat.size - HEAD_SIZE);
216
219
  const tailBuf = Buffer.alloc(tailSize);
217
220
  fs.readSync(fd, tailBuf, 0, tailSize, stat.size - tailSize);
218
221
  tailStr = tailBuf.toString('utf-8');
@@ -221,7 +224,7 @@ function loadSessionQuick(filePath, projectName) {
221
224
  try { return !!JSON.parse(line).timestamp; } catch { return false; }
222
225
  });
223
226
  if (hasTopLevelTs) break;
224
- if (tailSize >= stat.size - 8192) break; // already read entire file
227
+ if (tailSize >= stat.size - HEAD_SIZE) break; // already read entire file
225
228
  }
226
229
  }
227
230
  fs.closeSync(fd);
@@ -250,7 +253,42 @@ function loadSessionQuick(filePath, projectName) {
250
253
  userMsgCount++;
251
254
  if (!firstUserMsg) firstUserMsg = extractUserText(d);
252
255
  }
253
- } catch (e) { /* partial line */ }
256
+ } catch (e) {
257
+ // The line was truncated by the head buffer. Try to salvage metadata
258
+ // via regex so we don't lose the session entirely.
259
+ if (!firstTs) {
260
+ const tsMatch = line.match(/"timestamp"\s*:\s*"([^"]+)"/);
261
+ if (tsMatch) firstTs = tsMatch[1];
262
+ }
263
+ if (!version) {
264
+ const vMatch = line.match(/"version"\s*:\s*"([^"]+)"/);
265
+ if (vMatch) version = vMatch[1];
266
+ }
267
+ if (!gitBranch) {
268
+ const bMatch = line.match(/"gitBranch"\s*:\s*"([^"]+)"/);
269
+ if (bMatch) gitBranch = bMatch[1];
270
+ }
271
+ if (!cwd) {
272
+ const cwdMatch = line.match(/"cwd"\s*:\s*"([^"]+)"/);
273
+ if (cwdMatch) cwd = cwdMatch[1];
274
+ }
275
+ // Try to extract user message text from the truncated JSON line.
276
+ // User messages have "type":"user" and text content embedded inside.
277
+ if (!firstUserMsg && /"type"\s*:\s*"user"/.test(line)) {
278
+ userMsgCount++;
279
+ // Match the text field inside message.content (handles both string
280
+ // content and array-of-objects content structures).
281
+ const textMatch = line.match(/"text"\s*:\s*"((?:[^"\\]|\\.)*)/) ||
282
+ line.match(/"content"\s*:\s*"((?:[^"\\]|\\.)*)/);
283
+ if (textMatch) {
284
+ let text = '';
285
+ try { text = JSON.parse('"' + textMatch[1] + '"'); } catch { text = textMatch[1]; }
286
+ if (!text.startsWith('<local-command') && !text.startsWith('<command-')) {
287
+ firstUserMsg = text.substring(0, 200);
288
+ }
289
+ }
290
+ }
291
+ }
254
292
  }
255
293
 
256
294
  if (tailStr) {
@@ -259,7 +297,12 @@ function loadSessionQuick(filePath, projectName) {
259
297
  try {
260
298
  const d = JSON.parse(line);
261
299
  if (d.timestamp) lastTs = d.timestamp;
262
- if (d.type === 'user') userMsgCount++;
300
+ if (d.type === 'user') {
301
+ userMsgCount++;
302
+ // If no real user message was found in the head (all were commands),
303
+ // try to pick one from the tail as a fallback topic.
304
+ if (!firstUserMsg) firstUserMsg = extractUserText(d);
305
+ }
263
306
  if (d.type === 'custom-title' && d.customTitle) customTitle = d.customTitle;
264
307
  } catch (e) { /* partial line */ }
265
308
  }
@@ -447,6 +490,16 @@ function runListMode(limit) {
447
490
  function createApp() {
448
491
  const allSessions = loadAllSessions();
449
492
  const meta = loadMeta();
493
+
494
+ // Apply meta customTitles — these take priority over JSONL titles
495
+ // so renames persist even after continuing a conversation
496
+ for (const session of allSessions) {
497
+ const sm = meta.sessions[session.sessionId];
498
+ if (sm && sm.customTitle) {
499
+ session.customTitle = sm.customTitle;
500
+ }
501
+ }
502
+
450
503
  let filteredSessions = [...allSessions];
451
504
  let selectedIndex = -1; // -1 = "New Session", 0+ = session index
452
505
  let filterText = '';
@@ -532,6 +585,16 @@ function createApp() {
532
585
  });
533
586
 
534
587
  function updateFooter() {
588
+ if (isSearchMode) {
589
+ const keys = [
590
+ '{#e0af68-fg}{bold}↵{/} {#e0af68-fg}Confirm{/}',
591
+ '{#7aa2f7-fg}{bold}↑↓{/} {#7aa2f7-fg}Navigate{/}',
592
+ '{#565f89-fg}{bold}⌫{/} {#565f89-fg}Delete char{/}',
593
+ '{#565f89-fg}{bold}Esc{/} {#565f89-fg}Clear{/}',
594
+ ];
595
+ footer.setContent(`\n ${keys.join(' {#414868-fg}│{/} ')}`);
596
+ return;
597
+ }
535
598
  const keys = [
536
599
  '{#9ece6a-fg}{bold}n{/} {#9ece6a-fg}New{/}',
537
600
  '{#7aa2f7-fg}{bold}↵{/} {#7aa2f7-fg}Resume{/}',
@@ -654,6 +717,10 @@ function createApp() {
654
717
  const session = filteredSessions[selectedIndex];
655
718
  loadSessionDetail(session);
656
719
 
720
+ // Meta customTitle takes priority over JSONL
721
+ const sm = meta.sessions[session.sessionId];
722
+ if (sm && sm.customTitle) session.customTitle = sm.customTitle;
723
+
657
724
  const color = getProjectColor(session.project, projectColorMap);
658
725
  let c = '';
659
726
  const sep = ` {#414868-fg}${'─'.repeat(44)}{/}`;
@@ -843,11 +910,13 @@ function createApp() {
843
910
  }
844
911
 
845
912
  screen.key(['down'], () => {
846
- if (renameMode || popupOpen || isSearchMode) return;
913
+ if (renameMode || popupOpen) return;
914
+ if (isSearchMode) { isSearchMode = false; updateHeader(); updateFooter(); screen.render(); }
847
915
  moveSelection(1);
848
916
  });
849
917
  screen.key(['up'], () => {
850
- if (renameMode || popupOpen || isSearchMode) return;
918
+ if (renameMode || popupOpen) return;
919
+ if (isSearchMode) { isSearchMode = false; updateHeader(); updateFooter(); screen.render(); }
851
920
  moveSelection(-1);
852
921
  });
853
922
  screen.key(['home'], () => {
@@ -880,7 +949,9 @@ function createApp() {
880
949
  // Search
881
950
  screen.key(['/'], () => {
882
951
  if (renameMode || isSearchMode) return;
883
- isSearchMode = true; filterText = ''; applyFilter();
952
+ isSearchMode = true;
953
+ if (!filterText) filterText = ''; // keep existing filterText if any
954
+ updateHeader(); updateFooter(); screen.render();
884
955
  });
885
956
 
886
957
  screen.on('keypress', (ch, key) => {
@@ -948,7 +1019,7 @@ function createApp() {
948
1019
  }
949
1020
 
950
1021
  if (!isSearchMode) return;
951
- if (key.name === 'return' || key.name === 'enter') { isSearchMode = false; renderAll(); return; }
1022
+ if (key.name === 'return' || key.name === 'enter') { isSearchMode = false; searchJustConfirmed = true; renderAll(); return; }
952
1023
  if (key.name === 'escape') { isSearchMode = false; filterText = ''; applyFilter(); return; }
953
1024
  // Only accept printable characters (exclude control chars like \r \n \t)
954
1025
  if (ch && ch.length === 1 && ch.charCodeAt(0) >= 32 && !key.ctrl && !key.meta) { filterText += ch; selectedIndex = -1; applyFilter(); }
@@ -1007,10 +1078,12 @@ function createApp() {
1007
1078
  // Track the rename confirm popup and its session for Enter handling
1008
1079
  let renameConfirmPopup = null;
1009
1080
  let renameConfirmSession = null;
1081
+ let searchJustConfirmed = false;
1010
1082
 
1011
1083
  screen.key(['enter'], () => {
1012
1084
  if (renameMode) return;
1013
1085
  if (renameJustFinished) return;
1086
+ if (searchJustConfirmed) { searchJustConfirmed = false; return; }
1014
1087
  // Handle rename confirm popup Enter
1015
1088
  if (renameConfirmPopup && popupOpen) {
1016
1089
  const session = renameConfirmSession;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-starter",
3
- "version": "1.3.2",
3
+ "version": "1.3.4",
4
4
  "description": "A beautiful terminal UI for managing Claude Code sessions — start new or resume past conversations",
5
5
  "main": "index.js",
6
6
  "bin": {