@colbymchenry/cmem 0.2.23 → 0.2.29

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
  ] })
@@ -720,6 +815,22 @@ function initSchema(database) {
720
815
  embedding FLOAT[${EMBEDDING_DIMENSIONS}]
721
816
  );
722
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
+ `);
723
834
  database.exec(`
724
835
  CREATE INDEX IF NOT EXISTS idx_messages_session ON messages(session_id);
725
836
  `);
@@ -729,6 +840,9 @@ function initSchema(database) {
729
840
  database.exec(`
730
841
  CREATE INDEX IF NOT EXISTS idx_sessions_source ON sessions(source_file);
731
842
  `);
843
+ database.exec(`
844
+ CREATE INDEX IF NOT EXISTS idx_favorites_type ON favorites(type);
845
+ `);
732
846
  runMigrations(database);
733
847
  }
734
848
  function runMigrations(database) {
@@ -1001,6 +1115,96 @@ function needsReembedding(sessionId, currentContentLength, threshold = 500) {
1001
1115
  if (!state) return true;
1002
1116
  return currentContentLength - state.contentLength >= threshold;
1003
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
+ }
1004
1208
 
1005
1209
  // src/db/vectors.ts
1006
1210
  function storeEmbedding(sessionId, embedding) {
@@ -1112,18 +1316,81 @@ function useSessions(options = {}) {
1112
1316
  const [embeddingsReady, setEmbeddingsReady] = useState(false);
1113
1317
  const [isSearching, setIsSearching] = useState(false);
1114
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
+ }, []);
1115
1372
  const loadSessions = useCallback(() => {
1116
1373
  try {
1117
1374
  const loaded = projectFilter ? listHumanSessionsByProject(projectFilter) : listHumanSessions();
1118
- setAllSessions(loaded);
1119
- 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
+ }
1120
1387
  setError(null);
1121
1388
  } catch (err) {
1122
1389
  setError(String(err));
1123
1390
  } finally {
1124
1391
  setLoading(false);
1125
1392
  }
1126
- }, [projectFilter]);
1393
+ }, [projectFilter, smartSort, calculateProjects]);
1127
1394
  const initEmbeddings = useCallback(async () => {
1128
1395
  if (isReady()) {
1129
1396
  setEmbeddingsReady(true);
@@ -1153,7 +1420,7 @@ function useSessions(options = {}) {
1153
1420
  const filtered = allSessions.filter(
1154
1421
  (s) => s.title.toLowerCase().includes(query.toLowerCase()) || s.summary && s.summary.toLowerCase().includes(query.toLowerCase())
1155
1422
  );
1156
- setSessions(filtered);
1423
+ setSessions(smartSort(filtered, favoriteSessionIds));
1157
1424
  setIsSearching(true);
1158
1425
  return;
1159
1426
  }
@@ -1161,18 +1428,18 @@ function useSessions(options = {}) {
1161
1428
  setLoading(true);
1162
1429
  const queryEmbedding = await getEmbedding(query);
1163
1430
  const results = searchSessions(queryEmbedding, 20);
1164
- setSessions(results);
1431
+ setSessions(smartSort(results, favoriteSessionIds));
1165
1432
  setIsSearching(true);
1166
1433
  } catch (err) {
1167
1434
  setError(String(err));
1168
1435
  const filtered = allSessions.filter(
1169
1436
  (s) => s.title.toLowerCase().includes(query.toLowerCase()) || s.summary && s.summary.toLowerCase().includes(query.toLowerCase())
1170
1437
  );
1171
- setSessions(filtered);
1438
+ setSessions(smartSort(filtered, favoriteSessionIds));
1172
1439
  } finally {
1173
1440
  setLoading(false);
1174
1441
  }
1175
- }, [allSessions, embeddingsReady]);
1442
+ }, [allSessions, embeddingsReady, favoriteSessionIds, smartSort]);
1176
1443
  const clearSearch = useCallback(() => {
1177
1444
  setSessions(allSessions);
1178
1445
  setIsSearching(false);
@@ -1187,8 +1454,43 @@ function useSessions(options = {}) {
1187
1454
  const getSessionById = useCallback((id) => {
1188
1455
  return getSession(id);
1189
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]);
1190
1491
  return {
1191
1492
  sessions,
1493
+ projects,
1192
1494
  loading,
1193
1495
  error,
1194
1496
  embeddingsReady,
@@ -1198,16 +1500,22 @@ function useSessions(options = {}) {
1198
1500
  search,
1199
1501
  clearSearch,
1200
1502
  deleteSession: deleteSessionHandler,
1201
- getSessionById
1503
+ getSessionById,
1504
+ toggleFavorite: toggleFavoriteHandler,
1505
+ favoriteSessionIds,
1506
+ hasFavoriteSessions: hasFavSessions,
1507
+ moveProject,
1508
+ hasCustomProjectOrder: hasCustomOrder
1202
1509
  };
1203
1510
  }
1204
1511
 
1205
1512
  // src/ui/App.tsx
1206
- import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
1513
+ import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
1207
1514
  var App = ({ onResume, projectFilter: initialProjectFilter }) => {
1208
1515
  const { exit } = useApp();
1209
1516
  const {
1210
1517
  sessions,
1518
+ projects,
1211
1519
  loading,
1212
1520
  embeddingsReady,
1213
1521
  projectFilter,
@@ -1215,18 +1523,34 @@ var App = ({ onResume, projectFilter: initialProjectFilter }) => {
1215
1523
  search,
1216
1524
  clearSearch,
1217
1525
  deleteSession: deleteSession2,
1218
- refresh
1526
+ refresh,
1527
+ toggleFavorite: toggleFavorite2,
1528
+ moveProject
1219
1529
  } = useSessions({ projectFilter: initialProjectFilter });
1220
1530
  const [selectedIndex, setSelectedIndex] = useState2(0);
1221
1531
  const [searchQuery, setSearchQuery] = useState2("");
1222
1532
  const [mode, setMode] = useState2("list");
1223
1533
  const [statusMessage, setStatusMessage] = useState2(null);
1224
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]);
1225
1548
  useEffect2(() => {
1226
- if (selectedIndex >= sessions.length) {
1227
- 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));
1228
1552
  }
1229
- }, [sessions, selectedIndex]);
1553
+ }, [currentView, projects.length, projectSessions.length, sessions.length, selectedIndex]);
1230
1554
  useEffect2(() => {
1231
1555
  if (mode === "search" && searchQuery.length > 2) {
1232
1556
  const timer = setTimeout(() => {
@@ -1243,8 +1567,15 @@ var App = ({ onResume, projectFilter: initialProjectFilter }) => {
1243
1567
  return () => clearTimeout(timer);
1244
1568
  }
1245
1569
  }, [statusMessage]);
1570
+ const getCurrentSessions = useCallback2(() => {
1571
+ if (currentView === "project-sessions") {
1572
+ return projectSessions;
1573
+ }
1574
+ return sessions;
1575
+ }, [currentView, projectSessions, sessions]);
1246
1576
  const handleRestore = useCallback2(() => {
1247
- const session = sessions[selectedIndex];
1577
+ const currentSessions2 = getCurrentSessions();
1578
+ const session = currentSessions2[selectedIndex];
1248
1579
  if (!session) return;
1249
1580
  if (session.sourceFile) {
1250
1581
  const filename = basename4(session.sourceFile);
@@ -1283,17 +1614,30 @@ var App = ({ onResume, projectFilter: initialProjectFilter }) => {
1283
1614
  } else {
1284
1615
  setStatusMessage("No source file - cannot resume this session");
1285
1616
  }
1286
- }, [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
+ }, []);
1287
1629
  const handleDelete = useCallback2(() => {
1288
- const session = sessions[selectedIndex];
1630
+ const currentSessions2 = getCurrentSessions();
1631
+ const session = currentSessions2[selectedIndex];
1289
1632
  if (session) {
1290
1633
  deleteSession2(session.id);
1291
1634
  setStatusMessage(`Deleted: ${session.customTitle || session.title}`);
1292
1635
  setMode("list");
1293
1636
  }
1294
- }, [sessions, selectedIndex, deleteSession2]);
1637
+ }, [getCurrentSessions, selectedIndex, deleteSession2]);
1295
1638
  const handleRename = useCallback2(() => {
1296
- const session = sessions[selectedIndex];
1639
+ const currentSessions2 = getCurrentSessions();
1640
+ const session = currentSessions2[selectedIndex];
1297
1641
  if (session && renameValue.trim()) {
1298
1642
  renameSession(session.id, renameValue.trim());
1299
1643
  setStatusMessage(`Renamed to: ${renameValue.trim()}`);
@@ -1301,18 +1645,19 @@ var App = ({ onResume, projectFilter: initialProjectFilter }) => {
1301
1645
  }
1302
1646
  setMode("list");
1303
1647
  setRenameValue("");
1304
- }, [sessions, selectedIndex, renameValue, refresh]);
1648
+ }, [getCurrentSessions, selectedIndex, renameValue, refresh]);
1305
1649
  const handleClearRename = useCallback2(() => {
1306
- const session = sessions[selectedIndex];
1650
+ const currentSessions2 = getCurrentSessions();
1651
+ const session = currentSessions2[selectedIndex];
1307
1652
  if (session && session.customTitle) {
1308
1653
  renameSession(session.id, null);
1309
1654
  setStatusMessage(`Cleared custom name`);
1310
1655
  refresh();
1311
1656
  }
1312
1657
  setMode("list");
1313
- }, [sessions, selectedIndex, refresh]);
1658
+ }, [getCurrentSessions, selectedIndex, refresh]);
1314
1659
  useInput((input, key) => {
1315
- if (input === "q" && mode !== "search" && mode !== "rename") {
1660
+ if (input === "q" && mode !== "search" && mode !== "rename" && mode !== "sort-project") {
1316
1661
  exit();
1317
1662
  return;
1318
1663
  }
@@ -1349,54 +1694,99 @@ var App = ({ onResume, projectFilter: initialProjectFilter }) => {
1349
1694
  }
1350
1695
  return;
1351
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
+ }
1352
1718
  if (input === "/") {
1353
1719
  setMode("search");
1354
1720
  return;
1355
1721
  }
1356
- if (input === "r") {
1357
- if (sessions.length > 0) {
1358
- 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];
1359
1730
  setRenameValue(session?.customTitle || "");
1360
1731
  setMode("rename");
1361
1732
  }
1362
1733
  return;
1363
1734
  }
1364
- if (input === "R") {
1735
+ if (input === "R" && currentView !== "projects") {
1365
1736
  handleClearRename();
1366
1737
  return;
1367
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
+ }
1368
1752
  if (key.upArrow || input === "k") {
1369
1753
  setSelectedIndex((prev) => Math.max(0, prev - 1));
1370
1754
  return;
1371
1755
  }
1372
1756
  if (key.downArrow || input === "j") {
1373
- 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));
1374
1759
  return;
1375
1760
  }
1376
1761
  if (key.return) {
1377
- handleRestore();
1762
+ if (currentView === "projects") {
1763
+ handleEnterProject();
1764
+ } else {
1765
+ handleRestore();
1766
+ }
1378
1767
  return;
1379
1768
  }
1380
- if (input === "d") {
1381
- if (sessions.length > 0) {
1769
+ if (input === "d" && currentView !== "projects") {
1770
+ const currentSessions2 = getCurrentSessions();
1771
+ if (currentSessions2.length > 0) {
1382
1772
  setMode("confirm-delete");
1383
1773
  }
1384
1774
  return;
1385
1775
  }
1386
- if (input === "f") {
1387
- if (projectFilter) {
1388
- setProjectFilter(null);
1389
- 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
+ }
1390
1782
  } else {
1391
- const session = sessions[selectedIndex];
1392
- if (session?.projectPath) {
1393
- setProjectFilter(session.projectPath);
1394
- setStatusMessage(`Filtering to: ${basename4(session.projectPath)}`);
1395
- } else {
1396
- 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");
1397
1788
  }
1398
1789
  }
1399
- setSelectedIndex(0);
1400
1790
  return;
1401
1791
  }
1402
1792
  });
@@ -1405,15 +1795,21 @@ var App = ({ onResume, projectFilter: initialProjectFilter }) => {
1405
1795
  setSelectedIndex(0);
1406
1796
  }, []);
1407
1797
  if (loading && sessions.length === 0) {
1408
- return /* @__PURE__ */ jsxs5(Box5, { padding: 1, children: [
1409
- /* @__PURE__ */ jsx5(Spinner, { type: "dots" }),
1410
- /* @__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..." })
1411
1801
  ] });
1412
1802
  }
1413
- const selectedSession = sessions[selectedIndex] || null;
1414
- return /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", padding: 1, children: [
1415
- /* @__PURE__ */ jsx5(Header, { embeddingsReady, projectFilter }),
1416
- /* @__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
+ currentView === "project-sessions" && selectedProjectPath && /* @__PURE__ */ jsxs6(Box6, { marginBottom: 0, children: [
1809
+ /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: "Projects \u2192 " }),
1810
+ /* @__PURE__ */ jsx6(Text6, { color: "blue", children: basename4(selectedProjectPath) })
1811
+ ] }),
1812
+ currentTab === "global" && /* @__PURE__ */ jsx6(
1417
1813
  SearchInput,
1418
1814
  {
1419
1815
  value: searchQuery,
@@ -1421,22 +1817,62 @@ var App = ({ onResume, projectFilter: initialProjectFilter }) => {
1421
1817
  isFocused: mode === "search"
1422
1818
  }
1423
1819
  ),
1424
- /* @__PURE__ */ jsx5(
1820
+ currentView === "projects" ? /* @__PURE__ */ jsx6(
1821
+ ProjectList,
1822
+ {
1823
+ projects,
1824
+ selectedIndex,
1825
+ onSelect: setSelectedIndex
1826
+ }
1827
+ ) : /* @__PURE__ */ jsx6(
1425
1828
  SessionList,
1426
1829
  {
1427
- sessions,
1830
+ sessions: currentSessions,
1428
1831
  selectedIndex,
1429
1832
  onSelect: setSelectedIndex
1430
1833
  }
1431
1834
  ),
1432
- /* @__PURE__ */ jsx5(Preview, { session: selectedSession }),
1433
- /* @__PURE__ */ jsx5(Box5, { marginTop: 1, children: mode === "confirm-delete" ? /* @__PURE__ */ jsxs5(Text5, { color: "yellow", children: [
1835
+ selectedSession && /* @__PURE__ */ jsx6(Preview, { session: selectedSession }),
1836
+ selectedProject && /* @__PURE__ */ jsxs6(
1837
+ Box6,
1838
+ {
1839
+ flexDirection: "column",
1840
+ borderStyle: "round",
1841
+ borderColor: "gray",
1842
+ paddingX: 1,
1843
+ paddingY: 0,
1844
+ marginTop: 1,
1845
+ children: [
1846
+ /* @__PURE__ */ jsxs6(Box6, { children: [
1847
+ /* @__PURE__ */ jsx6(Text6, { bold: true, children: "Project Preview" }),
1848
+ selectedProject.sortOrder !== null && /* @__PURE__ */ jsxs6(Text6, { dimColor: true, children: [
1849
+ " (#",
1850
+ selectedProject.sortOrder + 1,
1851
+ ")"
1852
+ ] })
1853
+ ] }),
1854
+ /* @__PURE__ */ jsxs6(Text6, { color: "blue", children: [
1855
+ "\u{1F4C1} ",
1856
+ selectedProject.path
1857
+ ] }),
1858
+ /* @__PURE__ */ jsxs6(Text6, { dimColor: true, children: [
1859
+ selectedProject.sessionCount,
1860
+ " session",
1861
+ selectedProject.sessionCount !== 1 ? "s" : "",
1862
+ " \u2022 ",
1863
+ selectedProject.totalMessages,
1864
+ " messages"
1865
+ ] })
1866
+ ]
1867
+ }
1868
+ ),
1869
+ /* @__PURE__ */ jsx6(Box6, { marginTop: 1, children: mode === "confirm-delete" ? /* @__PURE__ */ jsxs6(Text6, { color: "yellow", children: [
1434
1870
  'Delete "',
1435
1871
  selectedSession?.customTitle || selectedSession?.title,
1436
1872
  '"? [y/n]'
1437
- ] }) : mode === "rename" ? /* @__PURE__ */ jsxs5(Box5, { children: [
1438
- /* @__PURE__ */ jsx5(Text5, { color: "magenta", children: "Rename: " }),
1439
- /* @__PURE__ */ jsx5(
1873
+ ] }) : mode === "rename" ? /* @__PURE__ */ jsxs6(Box6, { children: [
1874
+ /* @__PURE__ */ jsx6(Text6, { color: "magenta", children: "Rename: " }),
1875
+ /* @__PURE__ */ jsx6(
1440
1876
  TextInput2,
1441
1877
  {
1442
1878
  value: renameValue,
@@ -1444,8 +1880,32 @@ var App = ({ onResume, projectFilter: initialProjectFilter }) => {
1444
1880
  placeholder: "Enter new name..."
1445
1881
  }
1446
1882
  ),
1447
- /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: " [Enter] Save [Esc] Cancel" })
1448
- ] }) : 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" }) })
1883
+ /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: " [Enter] Save [Esc] Cancel" })
1884
+ ] }) : 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" }) }),
1885
+ /* @__PURE__ */ jsxs6(Box6, { marginTop: 1, children: [
1886
+ /* @__PURE__ */ jsx6(
1887
+ Text6,
1888
+ {
1889
+ backgroundColor: currentTab === "global" ? "green" : void 0,
1890
+ color: currentTab === "global" ? "white" : void 0,
1891
+ bold: currentTab === "global",
1892
+ dimColor: currentTab !== "global",
1893
+ children: currentTab === "global" ? " Global " : "Global"
1894
+ }
1895
+ ),
1896
+ /* @__PURE__ */ jsx6(Text6, { children: " " }),
1897
+ /* @__PURE__ */ jsx6(
1898
+ Text6,
1899
+ {
1900
+ backgroundColor: currentTab === "projects" ? "magenta" : void 0,
1901
+ color: currentTab === "projects" ? "white" : void 0,
1902
+ bold: currentTab === "projects",
1903
+ dimColor: currentTab !== "projects",
1904
+ children: currentTab === "projects" ? " Projects " : "Projects"
1905
+ }
1906
+ ),
1907
+ /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: " [\u2190\u2192] switch" })
1908
+ ] })
1449
1909
  ] });
1450
1910
  };
1451
1911