@colbymchenry/cmem 0.2.20 → 0.2.28

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.

Potentially problematic release.


This version of @colbymchenry/cmem might be problematic. Click here for more details.

package/dist/cli.js CHANGED
@@ -202,7 +202,7 @@ import { spawn as spawn2 } from "child_process";
202
202
 
203
203
  // src/ui/App.tsx
204
204
  import { useState as useState2, useEffect as useEffect2, useCallback as useCallback2 } from "react";
205
- import { Box as Box5, Text as Text5, useInput, useApp } from "ink";
205
+ import { Box as Box6, Text as Text6, useInput, useApp } from "ink";
206
206
  import TextInput2 from "ink-text-input";
207
207
  import Spinner from "ink-spinner";
208
208
  import { basename as basename4, dirname as dirname2 } from "path";
@@ -361,21 +361,23 @@ var SessionList = ({
361
361
  };
362
362
  var SessionItem = ({ session, isSelected }) => {
363
363
  const hasCustomTitle = !!session.customTitle;
364
- const displayTitle = truncate(session.customTitle || session.title, 40);
365
- const folderName = session.projectPath ? truncate(basename2(session.projectPath), 40) : "";
364
+ const displayTitle = truncate(session.customTitle || session.title, 38);
365
+ const folderName = session.projectPath ? truncate(basename2(session.projectPath), 38) : "";
366
366
  const msgs = String(session.messageCount).padStart(3);
367
367
  const updated = formatTimeAgo(session.updatedAt);
368
368
  const getTitleColor = () => {
369
369
  if (isSelected) return "cyan";
370
+ if (session.isFavorite) return "yellow";
370
371
  if (hasCustomTitle) return "magenta";
371
372
  return void 0;
372
373
  };
373
374
  return /* @__PURE__ */ jsxs3(Box3, { children: [
374
375
  /* @__PURE__ */ jsx3(Text3, { color: isSelected ? "cyan" : void 0, children: isSelected ? "\u25B8 " : " " }),
375
- /* @__PURE__ */ jsx3(Text3, { bold: isSelected, color: getTitleColor(), wrap: "truncate", children: displayTitle.padEnd(40) }),
376
+ /* @__PURE__ */ jsx3(Text3, { color: "yellow", children: session.isFavorite ? "\u2B50" : " " }),
377
+ /* @__PURE__ */ jsx3(Text3, { bold: isSelected, color: getTitleColor(), wrap: "truncate", children: displayTitle.padEnd(38) }),
376
378
  /* @__PURE__ */ jsxs3(Text3, { color: "blue", children: [
377
379
  " ",
378
- folderName.padEnd(40)
380
+ folderName.padEnd(38)
379
381
  ] }),
380
382
  /* @__PURE__ */ jsxs3(Text3, { dimColor: true, children: [
381
383
  " ",
@@ -386,16 +388,108 @@ var SessionItem = ({ session, isSelected }) => {
386
388
  ] });
387
389
  };
388
390
 
389
- // src/ui/components/Preview.tsx
391
+ // src/ui/components/ProjectList.tsx
390
392
  import { Box as Box4, Text as Text4 } from "ink";
391
393
  import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
394
+ var ProjectList = ({
395
+ projects,
396
+ selectedIndex
397
+ }) => {
398
+ if (projects.length === 0) {
399
+ return /* @__PURE__ */ jsxs4(
400
+ Box4,
401
+ {
402
+ flexDirection: "column",
403
+ borderStyle: "round",
404
+ borderColor: "gray",
405
+ paddingX: 1,
406
+ paddingY: 0,
407
+ children: [
408
+ /* @__PURE__ */ jsx4(Text4, { bold: true, children: "Projects" }),
409
+ /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "No projects found" }),
410
+ /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "Start using Claude Code in a project directory" })
411
+ ]
412
+ }
413
+ );
414
+ }
415
+ const visibleCount = 8;
416
+ let startIndex = Math.max(0, selectedIndex - Math.floor(visibleCount / 2));
417
+ const endIndex = Math.min(projects.length, startIndex + visibleCount);
418
+ if (endIndex - startIndex < visibleCount) {
419
+ startIndex = Math.max(0, endIndex - visibleCount);
420
+ }
421
+ const visibleProjects = projects.slice(startIndex, endIndex);
422
+ return /* @__PURE__ */ jsxs4(
423
+ Box4,
424
+ {
425
+ flexDirection: "column",
426
+ borderStyle: "round",
427
+ borderColor: "gray",
428
+ paddingX: 1,
429
+ paddingY: 0,
430
+ children: [
431
+ /* @__PURE__ */ jsxs4(Text4, { bold: true, children: [
432
+ "Projects (",
433
+ projects.length,
434
+ ")"
435
+ ] }),
436
+ visibleProjects.map((project, i) => {
437
+ const actualIndex = startIndex + i;
438
+ const isSelected = actualIndex === selectedIndex;
439
+ return /* @__PURE__ */ jsx4(
440
+ ProjectItem,
441
+ {
442
+ project,
443
+ isSelected
444
+ },
445
+ project.path
446
+ );
447
+ }),
448
+ projects.length > visibleCount && /* @__PURE__ */ jsxs4(Text4, { dimColor: true, children: [
449
+ startIndex > 0 ? "\u2191 more above" : "",
450
+ startIndex > 0 && endIndex < projects.length ? " | " : "",
451
+ endIndex < projects.length ? "\u2193 more below" : ""
452
+ ] })
453
+ ]
454
+ }
455
+ );
456
+ };
457
+ var ProjectItem = ({ project, isSelected }) => {
458
+ const displayName = truncate(project.name, 40);
459
+ const sessions = `${project.sessionCount} session${project.sessionCount !== 1 ? "s" : ""}`;
460
+ const messages = `${project.totalMessages} msgs`;
461
+ const updated = formatTimeAgo(project.lastUpdated);
462
+ const orderBadge = project.sortOrder !== null ? `${project.sortOrder + 1}.` : " ";
463
+ return /* @__PURE__ */ jsxs4(Box4, { children: [
464
+ /* @__PURE__ */ jsx4(Text4, { color: isSelected ? "cyan" : void 0, children: isSelected ? "\u25B8 " : " " }),
465
+ /* @__PURE__ */ jsxs4(Text4, { dimColor: true, children: [
466
+ orderBadge.padStart(3),
467
+ " "
468
+ ] }),
469
+ /* @__PURE__ */ jsx4(Text4, { color: "blue", children: "\u{1F4C1} " }),
470
+ /* @__PURE__ */ jsx4(Text4, { bold: isSelected, color: isSelected ? "cyan" : void 0, wrap: "truncate", children: displayName.padEnd(35) }),
471
+ /* @__PURE__ */ jsxs4(Text4, { dimColor: true, children: [
472
+ " ",
473
+ sessions.padEnd(12)
474
+ ] }),
475
+ /* @__PURE__ */ jsxs4(Text4, { dimColor: true, children: [
476
+ " ",
477
+ messages.padEnd(10)
478
+ ] }),
479
+ /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: updated.padStart(10) })
480
+ ] });
481
+ };
482
+
483
+ // src/ui/components/Preview.tsx
484
+ import { Box as Box5, Text as Text5 } from "ink";
485
+ import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
392
486
  var Preview = ({ session }) => {
393
487
  if (!session) {
394
488
  return null;
395
489
  }
396
490
  const summary = session.summary ? truncate(session.summary, 200) : "No summary available";
397
- return /* @__PURE__ */ jsxs4(
398
- Box4,
491
+ return /* @__PURE__ */ jsxs5(
492
+ Box5,
399
493
  {
400
494
  flexDirection: "column",
401
495
  borderStyle: "round",
@@ -404,15 +498,16 @@ var Preview = ({ session }) => {
404
498
  paddingY: 0,
405
499
  marginTop: 1,
406
500
  children: [
407
- /* @__PURE__ */ jsxs4(Box4, { children: [
408
- /* @__PURE__ */ jsx4(Text4, { bold: true, children: "Preview" }),
409
- session.projectPath && /* @__PURE__ */ jsxs4(Text4, { bold: true, color: "blue", children: [
501
+ /* @__PURE__ */ jsxs5(Box5, { children: [
502
+ /* @__PURE__ */ jsx5(Text5, { bold: true, children: "Preview" }),
503
+ session.isFavorite && /* @__PURE__ */ jsx5(Text5, { color: "yellow", children: " \u2B50" }),
504
+ session.projectPath && /* @__PURE__ */ jsxs5(Text5, { bold: true, color: "blue", children: [
410
505
  " \u{1F4C1} ",
411
506
  session.projectPath
412
507
  ] })
413
508
  ] }),
414
- /* @__PURE__ */ jsx4(Text4, { wrap: "wrap", children: summary }),
415
- /* @__PURE__ */ jsxs4(Text4, { dimColor: true, children: [
509
+ /* @__PURE__ */ jsx5(Text5, { wrap: "wrap", children: summary }),
510
+ /* @__PURE__ */ jsxs5(Text5, { dimColor: true, children: [
416
511
  "Messages: ",
417
512
  session.messageCount
418
513
  ] })
@@ -459,6 +554,24 @@ function ensureModelsDir() {
459
554
  // src/parser/index.ts
460
555
  import { readFileSync, readdirSync, existsSync as existsSync2, statSync } from "fs";
461
556
  import { join as join2 } from "path";
557
+ var AUTOMATED_TITLE_PATTERNS = [
558
+ /^<[a-z-]+>/i,
559
+ // XML-like tags: <project-instructions>, <command-message>, etc.
560
+ /^<[A-Z_]+>/,
561
+ // Uppercase tags: <SYSTEM>, <TOOL_USE>, etc.
562
+ /^\[system\]/i,
563
+ // [system] prefix
564
+ /^\/[a-z]+$/i
565
+ // Slash commands: /init, /help, etc.
566
+ ];
567
+ function isAutomatedByContent(title) {
568
+ for (const pattern of AUTOMATED_TITLE_PATTERNS) {
569
+ if (pattern.test(title.trim())) {
570
+ return true;
571
+ }
572
+ }
573
+ return false;
574
+ }
462
575
  function extractSessionMetadata(filepath) {
463
576
  const content = readFileSync(filepath, "utf-8");
464
577
  const metadata = {
@@ -702,6 +815,22 @@ function initSchema(database) {
702
815
  embedding FLOAT[${EMBEDDING_DIMENSIONS}]
703
816
  );
704
817
  `);
818
+ database.exec(`
819
+ CREATE TABLE IF NOT EXISTS favorites (
820
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
821
+ type TEXT NOT NULL CHECK (type IN ('session', 'folder')),
822
+ value TEXT NOT NULL,
823
+ created_at TEXT NOT NULL,
824
+ UNIQUE(type, value)
825
+ );
826
+ `);
827
+ database.exec(`
828
+ CREATE TABLE IF NOT EXISTS project_order (
829
+ path TEXT PRIMARY KEY,
830
+ sort_order INTEGER NOT NULL,
831
+ updated_at TEXT NOT NULL
832
+ );
833
+ `);
705
834
  database.exec(`
706
835
  CREATE INDEX IF NOT EXISTS idx_messages_session ON messages(session_id);
707
836
  `);
@@ -711,6 +840,9 @@ function initSchema(database) {
711
840
  database.exec(`
712
841
  CREATE INDEX IF NOT EXISTS idx_sessions_source ON sessions(source_file);
713
842
  `);
843
+ database.exec(`
844
+ CREATE INDEX IF NOT EXISTS idx_favorites_type ON favorites(type);
845
+ `);
714
846
  runMigrations(database);
715
847
  }
716
848
  function runMigrations(database) {
@@ -810,6 +942,10 @@ function updateSession(id, input) {
810
942
  updates.push("is_automated = ?");
811
943
  values.push(input.isAutomated ? 1 : 0);
812
944
  }
945
+ if (input.projectPath !== void 0) {
946
+ updates.push("project_path = ?");
947
+ values.push(input.projectPath);
948
+ }
813
949
  values.push(id);
814
950
  const transaction = db2.transaction(() => {
815
951
  db2.prepare(`UPDATE sessions SET ${updates.join(", ")} WHERE id = ?`).run(...values);
@@ -979,6 +1115,96 @@ function needsReembedding(sessionId, currentContentLength, threshold = 500) {
979
1115
  if (!state) return true;
980
1116
  return currentContentLength - state.contentLength >= threshold;
981
1117
  }
1118
+ function addFavorite(type, value) {
1119
+ const db2 = getDatabase();
1120
+ try {
1121
+ db2.prepare(`
1122
+ INSERT OR IGNORE INTO favorites (type, value, created_at)
1123
+ VALUES (?, ?, ?)
1124
+ `).run(type, value, (/* @__PURE__ */ new Date()).toISOString());
1125
+ return true;
1126
+ } catch {
1127
+ return false;
1128
+ }
1129
+ }
1130
+ function removeFavorite(type, value) {
1131
+ const db2 = getDatabase();
1132
+ const result = db2.prepare(`
1133
+ DELETE FROM favorites WHERE type = ? AND value = ?
1134
+ `).run(type, value);
1135
+ return result.changes > 0;
1136
+ }
1137
+ function toggleFavorite(type, value) {
1138
+ if (isFavorite(type, value)) {
1139
+ removeFavorite(type, value);
1140
+ return false;
1141
+ } else {
1142
+ addFavorite(type, value);
1143
+ return true;
1144
+ }
1145
+ }
1146
+ function isFavorite(type, value) {
1147
+ const db2 = getDatabase();
1148
+ const row = db2.prepare(`
1149
+ SELECT 1 FROM favorites WHERE type = ? AND value = ?
1150
+ `).get(type, value);
1151
+ return !!row;
1152
+ }
1153
+ function getFavorites(type) {
1154
+ const db2 = getDatabase();
1155
+ const rows = db2.prepare(`
1156
+ SELECT id, type, value, created_at as createdAt
1157
+ FROM favorites
1158
+ WHERE type = ?
1159
+ ORDER BY created_at DESC
1160
+ `).all(type);
1161
+ return rows;
1162
+ }
1163
+ function getFavoriteSessionIds() {
1164
+ const favorites = getFavorites("session");
1165
+ return new Set(favorites.map((f) => f.value));
1166
+ }
1167
+ function hasFavoriteSessions() {
1168
+ const db2 = getDatabase();
1169
+ const row = db2.prepare(`
1170
+ SELECT 1 FROM favorites WHERE type = 'session' LIMIT 1
1171
+ `).get();
1172
+ return !!row;
1173
+ }
1174
+ function getProjectOrders() {
1175
+ const db2 = getDatabase();
1176
+ const rows = db2.prepare(`
1177
+ SELECT path, sort_order as sortOrder
1178
+ FROM project_order
1179
+ ORDER BY sort_order ASC
1180
+ `).all();
1181
+ const map = /* @__PURE__ */ new Map();
1182
+ for (const row of rows) {
1183
+ map.set(row.path, row.sortOrder);
1184
+ }
1185
+ return map;
1186
+ }
1187
+ function updateProjectOrders(orders) {
1188
+ const db2 = getDatabase();
1189
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1190
+ const stmt = db2.prepare(`
1191
+ INSERT OR REPLACE INTO project_order (path, sort_order, updated_at)
1192
+ VALUES (?, ?, ?)
1193
+ `);
1194
+ const transaction = db2.transaction(() => {
1195
+ for (const order of orders) {
1196
+ stmt.run(order.path, order.sortOrder, now);
1197
+ }
1198
+ });
1199
+ transaction();
1200
+ }
1201
+ function hasCustomProjectOrder() {
1202
+ const db2 = getDatabase();
1203
+ const row = db2.prepare(`
1204
+ SELECT 1 FROM project_order LIMIT 1
1205
+ `).get();
1206
+ return !!row;
1207
+ }
982
1208
 
983
1209
  // src/db/vectors.ts
984
1210
  function storeEmbedding(sessionId, embedding) {
@@ -1090,18 +1316,81 @@ function useSessions(options = {}) {
1090
1316
  const [embeddingsReady, setEmbeddingsReady] = useState(false);
1091
1317
  const [isSearching, setIsSearching] = useState(false);
1092
1318
  const [projectFilter, setProjectFilter] = useState(options.projectFilter ?? null);
1319
+ const [favoriteSessionIds, setFavoriteSessionIds] = useState(/* @__PURE__ */ new Set());
1320
+ const [hasFavSessions, setHasFavSessions] = useState(false);
1321
+ const [projectOrderMap, setProjectOrderMap] = useState(/* @__PURE__ */ new Map());
1322
+ const [hasCustomOrder, setHasCustomOrder] = useState(false);
1323
+ const smartSort = useCallback((sessionList, favIds) => {
1324
+ const withFavorites = sessionList.map((s) => ({
1325
+ ...s,
1326
+ isFavorite: favIds.has(s.id)
1327
+ }));
1328
+ return withFavorites.sort((a, b) => {
1329
+ const aHasCustom = !!a.customTitle;
1330
+ const bHasCustom = !!b.customTitle;
1331
+ const aTier = a.isFavorite ? 0 : aHasCustom ? 1 : 2;
1332
+ const bTier = b.isFavorite ? 0 : bHasCustom ? 1 : 2;
1333
+ if (aTier !== bTier) return aTier - bTier;
1334
+ if (a.messageCount !== b.messageCount) return b.messageCount - a.messageCount;
1335
+ return new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime();
1336
+ });
1337
+ }, []);
1338
+ const [projects, setProjects] = useState([]);
1339
+ const calculateProjects = useCallback((sessionList, orderMap) => {
1340
+ const projectMap = /* @__PURE__ */ new Map();
1341
+ for (const session of sessionList) {
1342
+ if (!session.projectPath) continue;
1343
+ const existing = projectMap.get(session.projectPath);
1344
+ if (existing) {
1345
+ existing.sessionCount++;
1346
+ existing.totalMessages += session.messageCount;
1347
+ if (new Date(session.updatedAt) > new Date(existing.lastUpdated)) {
1348
+ existing.lastUpdated = session.updatedAt;
1349
+ }
1350
+ } else {
1351
+ const pathParts = session.projectPath.split("/");
1352
+ projectMap.set(session.projectPath, {
1353
+ path: session.projectPath,
1354
+ name: pathParts[pathParts.length - 1] || session.projectPath,
1355
+ sessionCount: 1,
1356
+ totalMessages: session.messageCount,
1357
+ sortOrder: orderMap.get(session.projectPath) ?? null,
1358
+ lastUpdated: session.updatedAt
1359
+ });
1360
+ }
1361
+ }
1362
+ return Array.from(projectMap.values()).sort((a, b) => {
1363
+ if (a.sortOrder !== null && b.sortOrder !== null) {
1364
+ return a.sortOrder - b.sortOrder;
1365
+ }
1366
+ if (a.sortOrder !== null && b.sortOrder === null) return -1;
1367
+ if (a.sortOrder === null && b.sortOrder !== null) return 1;
1368
+ if (a.totalMessages !== b.totalMessages) return b.totalMessages - a.totalMessages;
1369
+ return new Date(b.lastUpdated).getTime() - new Date(a.lastUpdated).getTime();
1370
+ });
1371
+ }, []);
1093
1372
  const loadSessions = useCallback(() => {
1094
1373
  try {
1095
1374
  const loaded = projectFilter ? listHumanSessionsByProject(projectFilter) : listHumanSessions();
1096
- setAllSessions(loaded);
1097
- setSessions(loaded);
1375
+ const favIds = getFavoriteSessionIds();
1376
+ const orderMap = getProjectOrders();
1377
+ setFavoriteSessionIds(favIds);
1378
+ setProjectOrderMap(orderMap);
1379
+ setHasFavSessions(hasFavoriteSessions());
1380
+ setHasCustomOrder(hasCustomProjectOrder());
1381
+ const sorted = smartSort(loaded, favIds);
1382
+ setAllSessions(sorted);
1383
+ setSessions(sorted);
1384
+ if (!projectFilter) {
1385
+ setProjects(calculateProjects(sorted, orderMap));
1386
+ }
1098
1387
  setError(null);
1099
1388
  } catch (err) {
1100
1389
  setError(String(err));
1101
1390
  } finally {
1102
1391
  setLoading(false);
1103
1392
  }
1104
- }, [projectFilter]);
1393
+ }, [projectFilter, smartSort, calculateProjects]);
1105
1394
  const initEmbeddings = useCallback(async () => {
1106
1395
  if (isReady()) {
1107
1396
  setEmbeddingsReady(true);
@@ -1131,7 +1420,7 @@ function useSessions(options = {}) {
1131
1420
  const filtered = allSessions.filter(
1132
1421
  (s) => s.title.toLowerCase().includes(query.toLowerCase()) || s.summary && s.summary.toLowerCase().includes(query.toLowerCase())
1133
1422
  );
1134
- setSessions(filtered);
1423
+ setSessions(smartSort(filtered, favoriteSessionIds));
1135
1424
  setIsSearching(true);
1136
1425
  return;
1137
1426
  }
@@ -1139,18 +1428,18 @@ function useSessions(options = {}) {
1139
1428
  setLoading(true);
1140
1429
  const queryEmbedding = await getEmbedding(query);
1141
1430
  const results = searchSessions(queryEmbedding, 20);
1142
- setSessions(results);
1431
+ setSessions(smartSort(results, favoriteSessionIds));
1143
1432
  setIsSearching(true);
1144
1433
  } catch (err) {
1145
1434
  setError(String(err));
1146
1435
  const filtered = allSessions.filter(
1147
1436
  (s) => s.title.toLowerCase().includes(query.toLowerCase()) || s.summary && s.summary.toLowerCase().includes(query.toLowerCase())
1148
1437
  );
1149
- setSessions(filtered);
1438
+ setSessions(smartSort(filtered, favoriteSessionIds));
1150
1439
  } finally {
1151
1440
  setLoading(false);
1152
1441
  }
1153
- }, [allSessions, embeddingsReady]);
1442
+ }, [allSessions, embeddingsReady, favoriteSessionIds, smartSort]);
1154
1443
  const clearSearch = useCallback(() => {
1155
1444
  setSessions(allSessions);
1156
1445
  setIsSearching(false);
@@ -1165,8 +1454,43 @@ function useSessions(options = {}) {
1165
1454
  const getSessionById = useCallback((id) => {
1166
1455
  return getSession(id);
1167
1456
  }, []);
1457
+ const toggleFavoriteHandler = useCallback((sessionId) => {
1458
+ const isNowFavorite = toggleFavorite("session", sessionId);
1459
+ const newFavIds = new Set(favoriteSessionIds);
1460
+ if (isNowFavorite) {
1461
+ newFavIds.add(sessionId);
1462
+ } else {
1463
+ newFavIds.delete(sessionId);
1464
+ }
1465
+ setFavoriteSessionIds(newFavIds);
1466
+ setHasFavSessions(newFavIds.size > 0);
1467
+ setAllSessions((prev) => smartSort(prev, newFavIds));
1468
+ setSessions((prev) => smartSort(prev, newFavIds));
1469
+ return isNowFavorite;
1470
+ }, [favoriteSessionIds, smartSort]);
1471
+ const moveProject = useCallback((fromIndex, toIndex) => {
1472
+ if (fromIndex === toIndex) return;
1473
+ if (fromIndex < 0 || toIndex < 0) return;
1474
+ if (fromIndex >= projects.length || toIndex >= projects.length) return;
1475
+ const newProjects = [...projects];
1476
+ const [movedProject] = newProjects.splice(fromIndex, 1);
1477
+ newProjects.splice(toIndex, 0, movedProject);
1478
+ const orders = newProjects.map((project, index) => ({
1479
+ path: project.path,
1480
+ sortOrder: index
1481
+ }));
1482
+ updateProjectOrders(orders);
1483
+ const newOrderMap = /* @__PURE__ */ new Map();
1484
+ for (const order of orders) {
1485
+ newOrderMap.set(order.path, order.sortOrder);
1486
+ }
1487
+ setProjectOrderMap(newOrderMap);
1488
+ setHasCustomOrder(true);
1489
+ setProjects(newProjects.map((p, i) => ({ ...p, sortOrder: i })));
1490
+ }, [projects]);
1168
1491
  return {
1169
1492
  sessions,
1493
+ projects,
1170
1494
  loading,
1171
1495
  error,
1172
1496
  embeddingsReady,
@@ -1176,16 +1500,22 @@ function useSessions(options = {}) {
1176
1500
  search,
1177
1501
  clearSearch,
1178
1502
  deleteSession: deleteSessionHandler,
1179
- getSessionById
1503
+ getSessionById,
1504
+ toggleFavorite: toggleFavoriteHandler,
1505
+ favoriteSessionIds,
1506
+ hasFavoriteSessions: hasFavSessions,
1507
+ moveProject,
1508
+ hasCustomProjectOrder: hasCustomOrder
1180
1509
  };
1181
1510
  }
1182
1511
 
1183
1512
  // src/ui/App.tsx
1184
- import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
1513
+ import { Fragment, jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
1185
1514
  var App = ({ onResume, projectFilter: initialProjectFilter }) => {
1186
1515
  const { exit } = useApp();
1187
1516
  const {
1188
1517
  sessions,
1518
+ projects,
1189
1519
  loading,
1190
1520
  embeddingsReady,
1191
1521
  projectFilter,
@@ -1193,18 +1523,34 @@ var App = ({ onResume, projectFilter: initialProjectFilter }) => {
1193
1523
  search,
1194
1524
  clearSearch,
1195
1525
  deleteSession: deleteSession2,
1196
- refresh
1526
+ refresh,
1527
+ toggleFavorite: toggleFavorite2,
1528
+ moveProject
1197
1529
  } = useSessions({ projectFilter: initialProjectFilter });
1198
1530
  const [selectedIndex, setSelectedIndex] = useState2(0);
1199
1531
  const [searchQuery, setSearchQuery] = useState2("");
1200
1532
  const [mode, setMode] = useState2("list");
1201
1533
  const [statusMessage, setStatusMessage] = useState2(null);
1202
1534
  const [renameValue, setRenameValue] = useState2("");
1535
+ const [currentTab, setCurrentTab] = useState2("global");
1536
+ const [selectedProjectPath, setSelectedProjectPath] = useState2(null);
1537
+ const getCurrentView = useCallback2(() => {
1538
+ if (currentTab === "projects") {
1539
+ return selectedProjectPath ? "project-sessions" : "projects";
1540
+ }
1541
+ return "sessions";
1542
+ }, [currentTab, selectedProjectPath]);
1543
+ const currentView = getCurrentView();
1544
+ const projectSessions = selectedProjectPath ? sessions.filter((s) => s.projectPath === selectedProjectPath) : [];
1545
+ useEffect2(() => {
1546
+ setSelectedIndex(0);
1547
+ }, [currentTab, selectedProjectPath]);
1203
1548
  useEffect2(() => {
1204
- if (selectedIndex >= sessions.length) {
1205
- setSelectedIndex(Math.max(0, sessions.length - 1));
1549
+ const maxIndex = currentView === "projects" ? projects.length - 1 : currentView === "project-sessions" ? projectSessions.length - 1 : sessions.length - 1;
1550
+ if (selectedIndex > maxIndex) {
1551
+ setSelectedIndex(Math.max(0, maxIndex));
1206
1552
  }
1207
- }, [sessions, selectedIndex]);
1553
+ }, [currentView, projects.length, projectSessions.length, sessions.length, selectedIndex]);
1208
1554
  useEffect2(() => {
1209
1555
  if (mode === "search" && searchQuery.length > 2) {
1210
1556
  const timer = setTimeout(() => {
@@ -1221,8 +1567,15 @@ var App = ({ onResume, projectFilter: initialProjectFilter }) => {
1221
1567
  return () => clearTimeout(timer);
1222
1568
  }
1223
1569
  }, [statusMessage]);
1570
+ const getCurrentSessions = useCallback2(() => {
1571
+ if (currentView === "project-sessions") {
1572
+ return projectSessions;
1573
+ }
1574
+ return sessions;
1575
+ }, [currentView, projectSessions, sessions]);
1224
1576
  const handleRestore = useCallback2(() => {
1225
- const session = sessions[selectedIndex];
1577
+ const currentSessions2 = getCurrentSessions();
1578
+ const session = currentSessions2[selectedIndex];
1226
1579
  if (!session) return;
1227
1580
  if (session.sourceFile) {
1228
1581
  const filename = basename4(session.sourceFile);
@@ -1261,17 +1614,30 @@ var App = ({ onResume, projectFilter: initialProjectFilter }) => {
1261
1614
  } else {
1262
1615
  setStatusMessage("No source file - cannot resume this session");
1263
1616
  }
1264
- }, [sessions, selectedIndex, onResume, exit]);
1617
+ }, [getCurrentSessions, selectedIndex, onResume, exit]);
1618
+ const handleEnterProject = useCallback2(() => {
1619
+ const project = projects[selectedIndex];
1620
+ if (project) {
1621
+ setSelectedProjectPath(project.path);
1622
+ setSelectedIndex(0);
1623
+ }
1624
+ }, [projects, selectedIndex]);
1625
+ const handleBackToProjects = useCallback2(() => {
1626
+ setSelectedProjectPath(null);
1627
+ setSelectedIndex(0);
1628
+ }, []);
1265
1629
  const handleDelete = useCallback2(() => {
1266
- const session = sessions[selectedIndex];
1630
+ const currentSessions2 = getCurrentSessions();
1631
+ const session = currentSessions2[selectedIndex];
1267
1632
  if (session) {
1268
1633
  deleteSession2(session.id);
1269
1634
  setStatusMessage(`Deleted: ${session.customTitle || session.title}`);
1270
1635
  setMode("list");
1271
1636
  }
1272
- }, [sessions, selectedIndex, deleteSession2]);
1637
+ }, [getCurrentSessions, selectedIndex, deleteSession2]);
1273
1638
  const handleRename = useCallback2(() => {
1274
- const session = sessions[selectedIndex];
1639
+ const currentSessions2 = getCurrentSessions();
1640
+ const session = currentSessions2[selectedIndex];
1275
1641
  if (session && renameValue.trim()) {
1276
1642
  renameSession(session.id, renameValue.trim());
1277
1643
  setStatusMessage(`Renamed to: ${renameValue.trim()}`);
@@ -1279,18 +1645,19 @@ var App = ({ onResume, projectFilter: initialProjectFilter }) => {
1279
1645
  }
1280
1646
  setMode("list");
1281
1647
  setRenameValue("");
1282
- }, [sessions, selectedIndex, renameValue, refresh]);
1648
+ }, [getCurrentSessions, selectedIndex, renameValue, refresh]);
1283
1649
  const handleClearRename = useCallback2(() => {
1284
- const session = sessions[selectedIndex];
1650
+ const currentSessions2 = getCurrentSessions();
1651
+ const session = currentSessions2[selectedIndex];
1285
1652
  if (session && session.customTitle) {
1286
1653
  renameSession(session.id, null);
1287
1654
  setStatusMessage(`Cleared custom name`);
1288
1655
  refresh();
1289
1656
  }
1290
1657
  setMode("list");
1291
- }, [sessions, selectedIndex, refresh]);
1658
+ }, [getCurrentSessions, selectedIndex, refresh]);
1292
1659
  useInput((input, key) => {
1293
- if (input === "q" && mode !== "search" && mode !== "rename") {
1660
+ if (input === "q" && mode !== "search" && mode !== "rename" && mode !== "sort-project") {
1294
1661
  exit();
1295
1662
  return;
1296
1663
  }
@@ -1327,54 +1694,99 @@ var App = ({ onResume, projectFilter: initialProjectFilter }) => {
1327
1694
  }
1328
1695
  return;
1329
1696
  }
1697
+ if (mode === "sort-project") {
1698
+ if (key.escape || key.return) {
1699
+ setMode("list");
1700
+ return;
1701
+ }
1702
+ if (key.upArrow || input === "k") {
1703
+ if (selectedIndex > 0) {
1704
+ moveProject(selectedIndex, selectedIndex - 1);
1705
+ setSelectedIndex(selectedIndex - 1);
1706
+ }
1707
+ return;
1708
+ }
1709
+ if (key.downArrow || input === "j") {
1710
+ if (selectedIndex < projects.length - 1) {
1711
+ moveProject(selectedIndex, selectedIndex + 1);
1712
+ setSelectedIndex(selectedIndex + 1);
1713
+ }
1714
+ return;
1715
+ }
1716
+ return;
1717
+ }
1330
1718
  if (input === "/") {
1331
1719
  setMode("search");
1332
1720
  return;
1333
1721
  }
1334
- if (input === "r") {
1335
- if (sessions.length > 0) {
1336
- const session = sessions[selectedIndex];
1722
+ if ((key.escape || key.backspace || key.delete) && currentView === "project-sessions") {
1723
+ handleBackToProjects();
1724
+ return;
1725
+ }
1726
+ if (input === "r" && currentView !== "projects") {
1727
+ const currentSessions2 = getCurrentSessions();
1728
+ if (currentSessions2.length > 0) {
1729
+ const session = currentSessions2[selectedIndex];
1337
1730
  setRenameValue(session?.customTitle || "");
1338
1731
  setMode("rename");
1339
1732
  }
1340
1733
  return;
1341
1734
  }
1342
- if (input === "R") {
1735
+ if (input === "R" && currentView !== "projects") {
1343
1736
  handleClearRename();
1344
1737
  return;
1345
1738
  }
1739
+ if (key.leftArrow || input === "h") {
1740
+ if (currentTab === "projects") {
1741
+ setCurrentTab("global");
1742
+ setSelectedProjectPath(null);
1743
+ }
1744
+ return;
1745
+ }
1746
+ if (key.rightArrow || input === "l") {
1747
+ if (currentTab === "global") {
1748
+ setCurrentTab("projects");
1749
+ }
1750
+ return;
1751
+ }
1346
1752
  if (key.upArrow || input === "k") {
1347
1753
  setSelectedIndex((prev) => Math.max(0, prev - 1));
1348
1754
  return;
1349
1755
  }
1350
1756
  if (key.downArrow || input === "j") {
1351
- setSelectedIndex((prev) => Math.min(sessions.length - 1, prev + 1));
1757
+ const maxIndex = currentView === "projects" ? projects.length - 1 : currentView === "project-sessions" ? projectSessions.length - 1 : sessions.length - 1;
1758
+ setSelectedIndex((prev) => Math.min(maxIndex, prev + 1));
1352
1759
  return;
1353
1760
  }
1354
1761
  if (key.return) {
1355
- handleRestore();
1762
+ if (currentView === "projects") {
1763
+ handleEnterProject();
1764
+ } else {
1765
+ handleRestore();
1766
+ }
1356
1767
  return;
1357
1768
  }
1358
- if (input === "d") {
1359
- if (sessions.length > 0) {
1769
+ if (input === "d" && currentView !== "projects") {
1770
+ const currentSessions2 = getCurrentSessions();
1771
+ if (currentSessions2.length > 0) {
1360
1772
  setMode("confirm-delete");
1361
1773
  }
1362
1774
  return;
1363
1775
  }
1364
- if (input === "f") {
1365
- if (projectFilter) {
1366
- setProjectFilter(null);
1367
- setStatusMessage("Filter cleared - showing all sessions");
1776
+ if (input === "s") {
1777
+ if (currentView === "projects") {
1778
+ if (projects.length > 0) {
1779
+ setMode("sort-project");
1780
+ setStatusMessage("Sort mode: use \u2191\u2193 to move, Enter to confirm");
1781
+ }
1368
1782
  } else {
1369
- const session = sessions[selectedIndex];
1370
- if (session?.projectPath) {
1371
- setProjectFilter(session.projectPath);
1372
- setStatusMessage(`Filtering to: ${basename4(session.projectPath)}`);
1373
- } else {
1374
- setStatusMessage("No folder for this session");
1783
+ const currentSessions2 = getCurrentSessions();
1784
+ const session = currentSessions2[selectedIndex];
1785
+ if (session) {
1786
+ const isNowFavorite = toggleFavorite2(session.id);
1787
+ setStatusMessage(isNowFavorite ? "\u2B50 Added to favorites" : "Removed from favorites");
1375
1788
  }
1376
1789
  }
1377
- setSelectedIndex(0);
1378
1790
  return;
1379
1791
  }
1380
1792
  });
@@ -1383,15 +1795,43 @@ var App = ({ onResume, projectFilter: initialProjectFilter }) => {
1383
1795
  setSelectedIndex(0);
1384
1796
  }, []);
1385
1797
  if (loading && sessions.length === 0) {
1386
- return /* @__PURE__ */ jsxs5(Box5, { padding: 1, children: [
1387
- /* @__PURE__ */ jsx5(Spinner, { type: "dots" }),
1388
- /* @__PURE__ */ jsx5(Text5, { children: " Loading sessions..." })
1798
+ return /* @__PURE__ */ jsxs6(Box6, { padding: 1, children: [
1799
+ /* @__PURE__ */ jsx6(Spinner, { type: "dots" }),
1800
+ /* @__PURE__ */ jsx6(Text6, { children: " Loading sessions..." })
1389
1801
  ] });
1390
1802
  }
1391
- const selectedSession = sessions[selectedIndex] || null;
1392
- return /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", padding: 1, children: [
1393
- /* @__PURE__ */ jsx5(Header, { embeddingsReady, projectFilter }),
1394
- /* @__PURE__ */ jsx5(
1803
+ const currentSessions = getCurrentSessions();
1804
+ const selectedSession = currentView !== "projects" ? currentSessions[selectedIndex] || null : null;
1805
+ const selectedProject = currentView === "projects" ? projects[selectedIndex] || null : null;
1806
+ return /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", padding: 1, children: [
1807
+ /* @__PURE__ */ jsx6(Header, { embeddingsReady, projectFilter: selectedProjectPath }),
1808
+ /* @__PURE__ */ jsxs6(Box6, { marginBottom: 0, children: [
1809
+ /* @__PURE__ */ jsx6(
1810
+ Text6,
1811
+ {
1812
+ color: currentTab === "global" ? "cyan" : void 0,
1813
+ bold: currentTab === "global",
1814
+ dimColor: currentTab !== "global",
1815
+ children: "Global"
1816
+ }
1817
+ ),
1818
+ /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: " | " }),
1819
+ /* @__PURE__ */ jsx6(
1820
+ Text6,
1821
+ {
1822
+ color: currentTab === "projects" ? "cyan" : void 0,
1823
+ bold: currentTab === "projects",
1824
+ dimColor: currentTab !== "projects",
1825
+ children: "Projects"
1826
+ }
1827
+ ),
1828
+ currentView === "project-sessions" && selectedProjectPath && /* @__PURE__ */ jsxs6(Fragment, { children: [
1829
+ /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: " \u2192 " }),
1830
+ /* @__PURE__ */ jsx6(Text6, { color: "blue", children: basename4(selectedProjectPath) })
1831
+ ] }),
1832
+ /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: " [\u2190\u2192] switch tabs" })
1833
+ ] }),
1834
+ currentTab === "global" && /* @__PURE__ */ jsx6(
1395
1835
  SearchInput,
1396
1836
  {
1397
1837
  value: searchQuery,
@@ -1399,22 +1839,62 @@ var App = ({ onResume, projectFilter: initialProjectFilter }) => {
1399
1839
  isFocused: mode === "search"
1400
1840
  }
1401
1841
  ),
1402
- /* @__PURE__ */ jsx5(
1842
+ currentView === "projects" ? /* @__PURE__ */ jsx6(
1843
+ ProjectList,
1844
+ {
1845
+ projects,
1846
+ selectedIndex,
1847
+ onSelect: setSelectedIndex
1848
+ }
1849
+ ) : /* @__PURE__ */ jsx6(
1403
1850
  SessionList,
1404
1851
  {
1405
- sessions,
1852
+ sessions: currentSessions,
1406
1853
  selectedIndex,
1407
1854
  onSelect: setSelectedIndex
1408
1855
  }
1409
1856
  ),
1410
- /* @__PURE__ */ jsx5(Preview, { session: selectedSession }),
1411
- /* @__PURE__ */ jsx5(Box5, { marginTop: 1, children: mode === "confirm-delete" ? /* @__PURE__ */ jsxs5(Text5, { color: "yellow", children: [
1857
+ selectedSession && /* @__PURE__ */ jsx6(Preview, { session: selectedSession }),
1858
+ selectedProject && /* @__PURE__ */ jsxs6(
1859
+ Box6,
1860
+ {
1861
+ flexDirection: "column",
1862
+ borderStyle: "round",
1863
+ borderColor: "gray",
1864
+ paddingX: 1,
1865
+ paddingY: 0,
1866
+ marginTop: 1,
1867
+ children: [
1868
+ /* @__PURE__ */ jsxs6(Box6, { children: [
1869
+ /* @__PURE__ */ jsx6(Text6, { bold: true, children: "Project Preview" }),
1870
+ selectedProject.sortOrder !== null && /* @__PURE__ */ jsxs6(Text6, { dimColor: true, children: [
1871
+ " (#",
1872
+ selectedProject.sortOrder + 1,
1873
+ ")"
1874
+ ] })
1875
+ ] }),
1876
+ /* @__PURE__ */ jsxs6(Text6, { color: "blue", children: [
1877
+ "\u{1F4C1} ",
1878
+ selectedProject.path
1879
+ ] }),
1880
+ /* @__PURE__ */ jsxs6(Text6, { dimColor: true, children: [
1881
+ selectedProject.sessionCount,
1882
+ " session",
1883
+ selectedProject.sessionCount !== 1 ? "s" : "",
1884
+ " \u2022 ",
1885
+ selectedProject.totalMessages,
1886
+ " messages"
1887
+ ] })
1888
+ ]
1889
+ }
1890
+ ),
1891
+ /* @__PURE__ */ jsx6(Box6, { marginTop: 1, children: mode === "confirm-delete" ? /* @__PURE__ */ jsxs6(Text6, { color: "yellow", children: [
1412
1892
  'Delete "',
1413
1893
  selectedSession?.customTitle || selectedSession?.title,
1414
1894
  '"? [y/n]'
1415
- ] }) : mode === "rename" ? /* @__PURE__ */ jsxs5(Box5, { children: [
1416
- /* @__PURE__ */ jsx5(Text5, { color: "magenta", children: "Rename: " }),
1417
- /* @__PURE__ */ jsx5(
1895
+ ] }) : mode === "rename" ? /* @__PURE__ */ jsxs6(Box6, { children: [
1896
+ /* @__PURE__ */ jsx6(Text6, { color: "magenta", children: "Rename: " }),
1897
+ /* @__PURE__ */ jsx6(
1418
1898
  TextInput2,
1419
1899
  {
1420
1900
  value: renameValue,
@@ -1422,8 +1902,8 @@ var App = ({ onResume, projectFilter: initialProjectFilter }) => {
1422
1902
  placeholder: "Enter new name..."
1423
1903
  }
1424
1904
  ),
1425
- /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: " [Enter] Save [Esc] Cancel" })
1426
- ] }) : statusMessage ? /* @__PURE__ */ jsx5(Text5, { color: "green", children: statusMessage }) : /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: "[\u2191\u2193/jk] Navigate [Enter] Resume [f] Filter folder [r] Rename [d] Delete [/] Search [q] Quit" }) })
1905
+ /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: " [Enter] Save [Esc] Cancel" })
1906
+ ] }) : mode === "sort-project" ? /* @__PURE__ */ jsx6(Text6, { color: "cyan", children: "Sort mode: [\u2191\u2193] Move project [Enter/Esc] Done" }) : statusMessage ? /* @__PURE__ */ jsx6(Text6, { color: "green", children: statusMessage }) : currentView === "projects" ? /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: "[\u2191\u2193] Navigate [Enter] Open [s] Sort [\u2190\u2192] Switch tabs [q] Quit" }) : currentView === "project-sessions" ? /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: "[\u2191\u2193] Navigate [Enter] Resume [s] Star [Esc] Back [r] Rename [d] Delete [q] Quit" }) : /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: "[\u2191\u2193] Navigate [Enter] Resume [s] Star [r] Rename [d] Delete [/] Search [q] Quit" }) })
1427
1907
  ] });
1428
1908
  };
1429
1909
 
@@ -1943,20 +2423,22 @@ async function processSessionFile(filePath, embeddingsReady, embedThreshold, ver
1943
2423
  const rawData = JSON.stringify({ filePath, messages, mtime: fileMtime });
1944
2424
  const projectPath = getProjectPathFromIndex(filePath, sessionId_from_file);
1945
2425
  const metadata = extractSessionMetadata(filePath);
1946
- const isAutomated = metadata.isSidechain || metadata.isMeta;
2426
+ const isAutomated = metadata.isSidechain || metadata.isMeta || isAutomatedByContent(title);
1947
2427
  let sessionId;
1948
2428
  let isNew = false;
1949
2429
  if (existingSession) {
1950
2430
  sessionId = existingSession.id;
1951
2431
  const needsMetadataUpdate = forceMetadataUpdate || !existingSession.isSidechain && !existingSession.isAutomated && isAutomated;
1952
- if (existingSession.messageCount !== messages.length || needsMetadataUpdate) {
2432
+ const needsProjectPathUpdate = !existingSession.projectPath && projectPath;
2433
+ if (existingSession.messageCount !== messages.length || needsMetadataUpdate || needsProjectPathUpdate) {
1953
2434
  updateSession(sessionId, {
1954
2435
  title,
1955
2436
  summary,
1956
2437
  rawData,
1957
2438
  messages,
1958
2439
  isSidechain: metadata.isSidechain,
1959
- isAutomated
2440
+ isAutomated,
2441
+ projectPath: projectPath || void 0
1960
2442
  });
1961
2443
  if (verbose) {
1962
2444
  const automatedTag = isAutomated ? chalk7.dim(" [auto]") : "";