@claude-sessions/core 0.4.7 → 0.4.8-beta.1

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/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { P as Project, M as MessagePayload$1, a as Message, S as SummaryInfo, b as SessionTodos, c as MoveSessionResult, A as AgentInfo, d as SessionSortOptions, e as ProjectTreeData, C as CompressSessionOptions, f as SummarizeSessionOptions, g as ConversationLine, h as SearchResult, F as FileChange, i as SessionsIndex, j as SessionIndexEntry } from './types-BMQErZZE.js';
2
- export { p as CleanupPreview, o as ClearSessionsResult, v as CompressSessionResult, k as ContentItem, D as DeleteSessionResult, w as ProjectKnowledge, R as RenameSessionResult, r as ResumeSessionOptions, s as ResumeSessionResult, u as SessionAnalysis, m as SessionFilesSummary, l as SessionMeta, y as SessionSortField, z as SessionSortOrder, q as SessionTreeData, n as SplitSessionResult, x as SummarizeSessionResult, T as TodoItem, t as ToolUsageStats } from './types-BMQErZZE.js';
1
+ import { P as Project, M as MessagePayload$1, a as Message, T as TitleDisplayMode, S as SummaryInfo, b as SessionSortField, c as SessionTodos, d as MoveSessionResult, A as AgentInfo, e as SessionSortOptions, f as ProjectTreeData, C as CompressSessionOptions, g as SummarizeSessionOptions, h as ConversationLine, i as SearchResult, F as FileChange, j as SessionsIndex, k as SessionIndexEntry } from './types-CoLRUgPk.js';
2
+ export { r as CleanupPreview, q as ClearSessionsResult, x as CompressSessionResult, l as ContentItem, D as DeleteSessionResult, y as ProjectKnowledge, R as RenameSessionResult, t as ResumeSessionOptions, u as ResumeSessionResult, w as SessionAnalysis, o as SessionFilesSummary, m as SessionMeta, B as SessionSortOrder, s as SessionTreeData, p as SplitSessionResult, z as SummarizeSessionResult, n as TodoItem, v as ToolUsageStats } from './types-CoLRUgPk.js';
3
3
  import * as effect_Cause from 'effect/Cause';
4
4
  import { Effect } from 'effect';
5
5
 
@@ -71,7 +71,8 @@ declare const findProjectByWorkspacePath: (workspacePath: string, projectNames:
71
71
  * Sort projects with priority:
72
72
  * 1. Current project (if specified)
73
73
  * 2. Current user's home directory subpaths
74
- * 3. Others (alphabetically by displayName)
74
+ * 3. Most recently modified first (by newest session file mtime)
75
+ * 4. Alphabetically by displayName as tiebreaker
75
76
  */
76
77
  declare const sortProjects: (projects: Project[], options?: {
77
78
  currentProjectName?: string | null;
@@ -94,25 +95,60 @@ declare const parseCommandMessage: (content?: string) => {
94
95
  declare const extractTitle: (input: MessagePayload$1 | string | undefined) => string;
95
96
  declare const isInvalidApiKeyMessage: (msg: Message) => boolean;
96
97
  declare const isContinuationSummary: (msg: Message) => boolean;
98
+ /**
99
+ * Options for getDisplayTitle when using the options-based signature
100
+ */
101
+ interface DisplayTitleOptions {
102
+ customTitle?: string;
103
+ currentSummary?: string;
104
+ title?: string;
105
+ createdAt?: string;
106
+ maxLength?: number;
107
+ fallback?: string;
108
+ /** 'message' = first user message (default), 'datetime' = relative date/time */
109
+ mode?: TitleDisplayMode;
110
+ /** Locale for date formatting (e.g. 'de-DE', 'en-GB'). Defaults to system locale. */
111
+ locale?: string;
112
+ }
97
113
  /**
98
114
  * Get display title with fallback logic
99
- * Priority: customTitle > currentSummary (truncated) > title > fallback
115
+ * Priority: customTitle > currentSummary (truncated) > title/datetime > fallback
100
116
  * Also handles slash command format in title
117
+ *
118
+ * Supports two call signatures:
119
+ * - Legacy: getDisplayTitle(customTitle, currentSummary, title, maxLength?, fallback?)
120
+ * - Options: getDisplayTitle(options)
101
121
  */
102
- declare const getDisplayTitle: (customTitle: string | undefined, currentSummary: string | undefined, title: string | undefined, maxLength?: number, fallback?: string) => string;
122
+ declare function getDisplayTitle(options: DisplayTitleOptions): string;
123
+ declare function getDisplayTitle(customTitle: string | undefined, currentSummary: string | undefined, title: string | undefined, maxLength?: number, fallback?: string): string;
103
124
  /**
104
125
  * Mask home directory path with ~
105
126
  * Only masks the specified homeDir, not other users' paths
106
127
  */
107
128
  declare const maskHomePath: (text: string, homeDir: string) => string;
108
129
  /**
109
- * Get sort timestamp for session (Unix timestamp ms)
110
- * Priority: summaries[0].timestamp > createdAt > 0
130
+ * Get summary-based sort timestamp for session (Unix timestamp ms)
131
+ * Used for 'summary' sort field. Priority: summaries[0].timestamp > createdAt > 0
111
132
  */
133
+ declare const getSummarySortTimestamp: (session: {
134
+ summaries?: SummaryInfo[];
135
+ createdAt?: string;
136
+ }) => number;
137
+ /** @deprecated Use getSummarySortTimestamp instead */
112
138
  declare const getSessionSortTimestamp: (session: {
113
139
  summaries?: SummaryInfo[];
114
140
  createdAt?: string;
115
141
  }) => number;
142
+ /**
143
+ * Get display-ready sort timestamp based on active sort field.
144
+ * Returns the timestamp matching what the user sees as the sort order.
145
+ */
146
+ declare const getDisplaySortTimestamp: (session: {
147
+ summaries?: SummaryInfo[];
148
+ createdAt?: string;
149
+ updatedAt?: string;
150
+ fileMtime?: number;
151
+ }, sortField: SessionSortField) => number;
116
152
  /**
117
153
  * Try to parse a single JSON line, returning null on failure with optional warning log
118
154
  * Use this when you want to skip invalid lines instead of throwing
@@ -139,7 +175,7 @@ declare const readJsonlFile: <T = Record<string, unknown>>(filePath: string, opt
139
175
  * @param timestamp - Unix timestamp (ms) or ISO date string
140
176
  * @returns Relative time string
141
177
  */
142
- declare const formatRelativeTime: (timestamp: number | string) => string;
178
+ declare const formatRelativeTime: (timestamp: number | string, locale?: string) => string;
143
179
  /**
144
180
  * Calculate total todo count from session todos
145
181
  * Includes both session-level and agent-level todos
@@ -483,6 +519,7 @@ declare const previewCleanup: (projectName?: string) => Effect.Effect<{
483
519
  emptyWithTodosCount: number;
484
520
  orphanAgentCount: number;
485
521
  orphanTodoCount: number;
522
+ isStale: boolean;
486
523
  }[], effect_Cause.UnknownException, never>;
487
524
  declare const clearSessions: (options: {
488
525
  projectName?: string;
@@ -491,12 +528,15 @@ declare const clearSessions: (options: {
491
528
  skipWithTodos?: boolean;
492
529
  clearOrphanAgents?: boolean;
493
530
  clearOrphanTodos?: boolean;
531
+ clearStale?: boolean;
532
+ staleProjects?: string[];
494
533
  }) => Effect.Effect<{
495
534
  success: true;
496
535
  deletedCount: number;
497
536
  removedMessageCount: number;
498
537
  deletedOrphanAgentCount: number;
499
538
  deletedOrphanTodoCount: number;
539
+ deletedStaleProjectCount: number;
500
540
  }, effect_Cause.UnknownException, never>;
501
541
 
502
542
  declare const searchSessions: (query: string, options?: {
@@ -573,9 +613,7 @@ declare function validateChain(messages: readonly GenericMessage[]): ValidationR
573
613
  errors: ChainError[];
574
614
  };
575
615
  /**
576
- * Validate for unwanted progress messages (hook outputs)
577
- *
578
- * Only 'Stop' hookEvent is treated as an error (should be removed)
616
+ * Validate for unwanted cleanup artifacts that should not remain in sessions.
579
617
  */
580
618
  declare function validateProgressMessages(messages: readonly GenericMessage[]): ValidationResult & {
581
619
  errors: ProgressError[];
@@ -670,4 +708,4 @@ declare const getLogger: () => Logger;
670
708
  */
671
709
  declare const createLogger: (namespace: string) => Logger;
672
710
 
673
- export { AgentInfo, type ChainError, CompressSessionOptions, ConversationLine, FileChange, type GenericMessage, type Logger, Message, MessagePayload$1 as MessagePayload, MoveSessionResult, type ProgressError, Project, ProjectTreeData, SearchResult, SessionIndexEntry, SessionSortOptions, SessionTodos, SessionsIndex, SummarizeSessionOptions, SummaryInfo, TREE_ICONS, type ToolUseResultError, type TreeItemType, type ValidationResult, analyzeSession, autoRepairChain, canMoveSession, clearSessions, compressSession, createLogger, deleteLinkedTodos, deleteMessage, deleteMessageWithChainRepair, deleteOrphanAgents, deleteOrphanTodos, deleteSession, expandHomePath, extractProjectKnowledge, extractTextContent, extractTitle, findLinkedAgents, findLinkedTodos, findOrphanAgents, findOrphanTodos, findProjectByWorkspacePath, folderNameToDisplayPath, folderNameToPath, formatRelativeTime, generateTreeNodeId, getCachePath, getDisplayTitle, getIndexEntryDisplayTitle, getLogger, getRealPathFromSession, getSessionFiles, getSessionSortTimestamp, getSessionTooltip, getSessionsDir, getTodoIcon, getTodosDir, getTotalTodoCount, hasSessionsIndex, isContinuationSummary, isInvalidApiKeyMessage, listProjects, listSessions, loadAgentMessages, loadProjectTreeData, loadSessionTreeData, loadSessionsIndex, maskHomePath, moveSession, parseCommandMessage, parseJsonlLines, parseTreeNodeId, pathToFolderName, previewCleanup, readJsonlFile, readSession, renameSession, repairChain, repairParentUuidChain, restoreMessage, searchSessions, sessionHasSubItems, sessionHasTodos, setLogger, sortIndexEntriesByModified, sortProjects, splitSession, summarizeSession, tryParseJsonLine, updateSessionSummary, validateChain, validateProgressMessages, validateToolUseResult };
711
+ export { AgentInfo, type ChainError, CompressSessionOptions, ConversationLine, type DisplayTitleOptions, FileChange, type GenericMessage, type Logger, Message, MessagePayload$1 as MessagePayload, MoveSessionResult, type ProgressError, Project, ProjectTreeData, SearchResult, SessionIndexEntry, SessionSortField, SessionSortOptions, SessionTodos, SessionsIndex, SummarizeSessionOptions, SummaryInfo, TREE_ICONS, TitleDisplayMode, type ToolUseResultError, type TreeItemType, type ValidationResult, analyzeSession, autoRepairChain, canMoveSession, clearSessions, compressSession, createLogger, deleteLinkedTodos, deleteMessage, deleteMessageWithChainRepair, deleteOrphanAgents, deleteOrphanTodos, deleteSession, expandHomePath, extractProjectKnowledge, extractTextContent, extractTitle, findLinkedAgents, findLinkedTodos, findOrphanAgents, findOrphanTodos, findProjectByWorkspacePath, folderNameToDisplayPath, folderNameToPath, formatRelativeTime, generateTreeNodeId, getCachePath, getDisplaySortTimestamp, getDisplayTitle, getIndexEntryDisplayTitle, getLogger, getRealPathFromSession, getSessionFiles, getSessionSortTimestamp, getSessionTooltip, getSessionsDir, getSummarySortTimestamp, getTodoIcon, getTodosDir, getTotalTodoCount, hasSessionsIndex, isContinuationSummary, isInvalidApiKeyMessage, listProjects, listSessions, loadAgentMessages, loadProjectTreeData, loadSessionTreeData, loadSessionsIndex, maskHomePath, moveSession, parseCommandMessage, parseJsonlLines, parseTreeNodeId, pathToFolderName, previewCleanup, readJsonlFile, readSession, renameSession, repairChain, repairParentUuidChain, restoreMessage, searchSessions, sessionHasSubItems, sessionHasTodos, setLogger, sortIndexEntriesByModified, sortProjects, splitSession, summarizeSession, tryParseJsonLine, updateSessionSummary, validateChain, validateProgressMessages, validateToolUseResult };
package/dist/index.js CHANGED
@@ -95,11 +95,31 @@ var isContinuationSummary = (msg) => {
95
95
  const text = extractTextContent(msg.message);
96
96
  return text.startsWith("This session is being continued from");
97
97
  };
98
- var getDisplayTitle = (customTitle, currentSummary, title, maxLength = 60, fallback = "Untitled") => {
98
+ function getDisplayTitle(customTitleOrOptions, currentSummary, title, maxLength = 60, fallback = "Untitled") {
99
+ let mode = "message";
100
+ let createdAt;
101
+ let customTitle;
102
+ let locale;
103
+ if (typeof customTitleOrOptions === "object" && customTitleOrOptions !== null) {
104
+ const opts = customTitleOrOptions;
105
+ customTitle = opts.customTitle;
106
+ currentSummary = opts.currentSummary;
107
+ title = opts.title;
108
+ createdAt = opts.createdAt;
109
+ maxLength = opts.maxLength ?? 60;
110
+ fallback = opts.fallback ?? "Untitled";
111
+ mode = opts.mode ?? "message";
112
+ locale = opts.locale;
113
+ } else {
114
+ customTitle = customTitleOrOptions;
115
+ }
99
116
  if (customTitle) return customTitle;
100
117
  if (currentSummary) {
101
118
  return currentSummary.length > maxLength ? currentSummary.slice(0, maxLength - 3) + "..." : currentSummary;
102
119
  }
120
+ if (mode === "datetime" && createdAt) {
121
+ return formatRelativeTime(createdAt, locale);
122
+ }
103
123
  if (title && title !== "Untitled") {
104
124
  const firstParagraph = title.includes("\n\n") ? title.split("\n\n")[0] : title;
105
125
  if (firstParagraph.includes("<command-name>")) {
@@ -109,7 +129,7 @@ var getDisplayTitle = (customTitle, currentSummary, title, maxLength = 60, fallb
109
129
  return firstParagraph;
110
130
  }
111
131
  return fallback;
112
- };
132
+ }
113
133
  var replaceMessageContent = (msg, text) => ({
114
134
  ...msg,
115
135
  message: {
@@ -143,10 +163,27 @@ var maskHomePath = (text, homeDir) => {
143
163
  const regex = new RegExp(`${escapedHome}(?=[/\\\\]|$)`, "g");
144
164
  return text.replace(regex, "~");
145
165
  };
146
- var getSessionSortTimestamp = (session) => {
166
+ var getSummarySortTimestamp = (session) => {
147
167
  const timestampStr = session.summaries?.[0]?.timestamp ?? session.createdAt;
148
168
  return timestampStr ? new Date(timestampStr).getTime() : 0;
149
169
  };
170
+ var getSessionSortTimestamp = getSummarySortTimestamp;
171
+ var getDisplaySortTimestamp = (session, sortField) => {
172
+ switch (sortField) {
173
+ case "updated": {
174
+ if (session.updatedAt) return new Date(session.updatedAt).getTime();
175
+ return getSummarySortTimestamp(session);
176
+ }
177
+ case "created": {
178
+ if (session.createdAt) return new Date(session.createdAt).getTime();
179
+ return getSummarySortTimestamp(session);
180
+ }
181
+ case "modified":
182
+ return session.fileMtime ?? getSummarySortTimestamp(session);
183
+ default:
184
+ return getSummarySortTimestamp(session);
185
+ }
186
+ };
150
187
  var tryParseJsonLine = (line, lineNumber, filePath) => {
151
188
  try {
152
189
  return JSON.parse(line);
@@ -179,7 +216,7 @@ var readJsonlFile = (filePath, options) => Effect.gen(function* () {
179
216
  const lines = content.trim().split("\n").filter(Boolean);
180
217
  return parseJsonlLines(lines, filePath, options);
181
218
  });
182
- var formatRelativeTime = (timestamp) => {
219
+ var formatRelativeTime = (timestamp, locale) => {
183
220
  const date = typeof timestamp === "string" ? new Date(timestamp) : new Date(timestamp);
184
221
  const now = /* @__PURE__ */ new Date();
185
222
  const diff = now.getTime() - date.getTime();
@@ -190,7 +227,7 @@ var formatRelativeTime = (timestamp) => {
190
227
  if (minutes < 60) return `${minutes}m ago`;
191
228
  if (hours < 24) return `${hours}h ago`;
192
229
  if (days < 7) return `${days}d ago`;
193
- return date.toLocaleDateString();
230
+ return date.toLocaleDateString(locale, { day: "numeric", month: "short", year: "numeric" });
194
231
  };
195
232
  var getTotalTodoCount = (todos) => {
196
233
  return todos.sessionTodos.length + todos.agentTodos.reduce((sum, a) => sum + a.todos.length, 0);
@@ -390,6 +427,9 @@ var sortProjects = (projects, options = {}) => {
390
427
  if (aIsUserHome && !bIsUserHome) return -1;
391
428
  if (!aIsUserHome && bIsUserHome) return 1;
392
429
  }
430
+ const aMtime = a.lastModified ?? 0;
431
+ const bMtime = b.lastModified ?? 0;
432
+ if (aMtime !== bMtime) return bMtime - aMtime;
393
433
  return a.displayName.localeCompare(b.displayName);
394
434
  });
395
435
  };
@@ -499,8 +539,8 @@ var findOrphanAgentsWithPaths = (projectName) => Effect2.gen(function* () {
499
539
  }
500
540
  for (const entry of files) {
501
541
  const entryPath = path2.join(projectPath, entry);
502
- const stat4 = yield* Effect2.tryPromise(() => fs3.stat(entryPath).catch(() => null));
503
- if (stat4?.isDirectory() && !entry.startsWith(".")) {
542
+ const stat6 = yield* Effect2.tryPromise(() => fs3.stat(entryPath).catch(() => null));
543
+ if (stat6?.isDirectory() && !entry.startsWith(".")) {
504
544
  const subagentsPath = path2.join(entryPath, "subagents");
505
545
  const subagentsExists = yield* Effect2.tryPromise(
506
546
  () => fs3.stat(subagentsPath).then(() => true).catch(() => false)
@@ -818,11 +858,24 @@ var listProjects = Effect4.gen(function* () {
818
858
  const files = yield* Effect4.tryPromise(() => fs5.readdir(projectPath));
819
859
  const sessionFiles = files.filter((f) => f.endsWith(".jsonl") && !f.startsWith("agent-"));
820
860
  const displayName = yield* Effect4.tryPromise(() => folderNameToPath(entry.name));
861
+ let lastModified = 0;
862
+ if (sessionFiles.length > 0) {
863
+ const stats = yield* Effect4.all(
864
+ sessionFiles.map(
865
+ (f) => Effect4.tryPromise(
866
+ () => fs5.stat(path4.join(projectPath, f)).then((s) => s.mtimeMs)
867
+ ).pipe(Effect4.orElseSucceed(() => 0))
868
+ ),
869
+ { concurrency: 20 }
870
+ );
871
+ lastModified = stats.reduce((max, value) => value > max ? value : max, 0);
872
+ }
821
873
  return {
822
874
  name: entry.name,
823
875
  displayName,
824
876
  path: projectPath,
825
- sessionCount: sessionFiles.length
877
+ sessionCount: sessionFiles.length,
878
+ lastModified
826
879
  };
827
880
  })
828
881
  ),
@@ -897,19 +950,7 @@ function validateProgressMessages(messages) {
897
950
  const errors = [];
898
951
  for (let i = 0; i < messages.length; i++) {
899
952
  const msg = messages[i];
900
- if (msg.type === "progress") {
901
- const progressMsg = msg;
902
- const hookEvent = progressMsg.hookEvent ?? progressMsg.data?.hookEvent;
903
- const hookName = progressMsg.hookName ?? progressMsg.data?.hookName;
904
- if (hookEvent === "Stop") {
905
- errors.push({
906
- type: "unwanted_progress",
907
- line: i + 1,
908
- hookEvent,
909
- hookName
910
- });
911
- }
912
- } else if (msg.type === "saved_hook_context") {
953
+ if (msg.type === "saved_hook_context") {
913
954
  errors.push({
914
955
  type: "unwanted_progress",
915
956
  line: i + 1,
@@ -1199,8 +1240,8 @@ var deleteSession = (projectName, sessionId) => Effect5.gen(function* () {
1199
1240
  const projectPath = path5.join(sessionsDir, projectName);
1200
1241
  const filePath = path5.join(projectPath, `${sessionId}.jsonl`);
1201
1242
  const linkedAgents = yield* findLinkedAgents(projectName, sessionId);
1202
- const stat4 = yield* Effect5.tryPromise(() => fs6.stat(filePath));
1203
- if (stat4.size === 0) {
1243
+ const stat6 = yield* Effect5.tryPromise(() => fs6.stat(filePath));
1244
+ if (stat6.size === 0) {
1204
1245
  yield* Effect5.tryPromise(() => fs6.unlink(filePath));
1205
1246
  const agentBackupDir2 = path5.join(projectPath, ".bak");
1206
1247
  yield* Effect5.tryPromise(() => fs6.mkdir(agentBackupDir2, { recursive: true }));
@@ -1590,7 +1631,7 @@ var loadSessionTreeDataInternal = (projectName, sessionId, summariesByTargetSess
1590
1631
  }
1591
1632
  const todos = yield* findLinkedTodos(sessionId, linkedAgentIds);
1592
1633
  const createdAt = firstMessage?.timestamp ?? void 0;
1593
- const sortTimestamp = getSessionSortTimestamp({ summaries, createdAt });
1634
+ const sortTimestamp = getSummarySortTimestamp({ summaries, createdAt });
1594
1635
  return {
1595
1636
  id: sessionId,
1596
1637
  projectName,
@@ -1689,12 +1730,16 @@ var buildProjectTreeResult = (project, sessions, sort) => {
1689
1730
  if (isErrorSessionTitle(s.currentSummary)) return false;
1690
1731
  return true;
1691
1732
  });
1733
+ const displaySessions = filteredSessions.map((s) => ({
1734
+ ...s,
1735
+ sortTimestamp: getDisplaySortTimestamp(s, sort.field)
1736
+ }));
1692
1737
  return {
1693
1738
  name: project.name,
1694
1739
  displayName: project.displayName,
1695
1740
  path: project.path,
1696
- sessionCount: filteredSessions.length,
1697
- sessions: filteredSessions
1741
+ sessionCount: displaySessions.length,
1742
+ sessions: displaySessions
1698
1743
  };
1699
1744
  };
1700
1745
  var buildTreeCache = (globalUuidMap, allSummaries, sessions, fileMtimes) => {
@@ -1721,7 +1766,7 @@ var updateSessionSummaries = (cached, summariesByTargetSession) => {
1721
1766
  const oldJson = JSON.stringify(cached.summaries);
1722
1767
  const newJson = JSON.stringify(newSummaries);
1723
1768
  if (oldJson === newJson) return cached;
1724
- const newSortTimestamp = getSessionSortTimestamp({
1769
+ const newSortTimestamp = getSummarySortTimestamp({
1725
1770
  summaries: newSummaries,
1726
1771
  createdAt: cached.createdAt
1727
1772
  });
@@ -1750,8 +1795,8 @@ var loadProjectTreeData = (projectName, sortOptions) => Effect6.gen(function* ()
1750
1795
  (file) => Effect6.gen(function* () {
1751
1796
  const filePath = path7.join(projectPath, file);
1752
1797
  try {
1753
- const stat4 = yield* Effect6.tryPromise(() => fs8.stat(filePath));
1754
- fileMtimes.set(file.replace(".jsonl", ""), stat4.mtimeMs);
1798
+ const stat6 = yield* Effect6.tryPromise(() => fs8.stat(filePath));
1799
+ fileMtimes.set(file.replace(".jsonl", ""), stat6.mtimeMs);
1755
1800
  } catch {
1756
1801
  }
1757
1802
  })
@@ -2007,9 +2052,12 @@ var compressSession = (projectName, sessionId, options = {}) => Effect7.gen(func
2007
2052
  const messagesToRemove = [];
2008
2053
  const filteredMessages = messages.filter((msg, idx) => {
2009
2054
  if (msg.type === "progress") {
2010
- removedProgress++;
2011
- messagesToRemove.push(msg);
2012
- return false;
2055
+ const hookEvent = (typeof msg.hookEvent === "string" ? msg.hookEvent : void 0) ?? (typeof msg.data === "object" && msg.data !== null && "hookEvent" in msg.data ? msg.data.hookEvent : void 0);
2056
+ if (hookEvent !== "Stop") {
2057
+ removedProgress++;
2058
+ messagesToRemove.push(msg);
2059
+ return false;
2060
+ }
2013
2061
  }
2014
2062
  if (msg.type === "custom-title") {
2015
2063
  if (customTitleIndices.length > 1 && idx !== customTitleIndices[customTitleIndices.length - 1]) {
@@ -2196,6 +2244,7 @@ var summarizeSession = (projectName, sessionId, options = {}) => Effect7.gen(fun
2196
2244
  import { Effect as Effect8 } from "effect";
2197
2245
  import * as fs10 from "fs/promises";
2198
2246
  import * as path9 from "path";
2247
+ import * as os2 from "os";
2199
2248
  var cleanInvalidMessages = (projectName, sessionId) => Effect8.gen(function* () {
2200
2249
  const filePath = path9.join(getSessionsDir(), projectName, `${sessionId}.jsonl`);
2201
2250
  const content = yield* Effect8.tryPromise(() => fs10.readFile(filePath, "utf-8"));
@@ -2243,9 +2292,32 @@ var previewCleanup = (projectName) => Effect8.gen(function* () {
2243
2292
  const targetProjects = projectName ? projects.filter((p) => p.name === projectName) : projects;
2244
2293
  const orphanTodos = yield* findOrphanTodos();
2245
2294
  const orphanTodoCount = orphanTodos.length;
2295
+ const homeDir = os2.homedir();
2296
+ const staleSet = /* @__PURE__ */ new Set();
2297
+ yield* Effect8.all(
2298
+ targetProjects.map(
2299
+ (project) => Effect8.tryPromise(async () => {
2300
+ const displayPath = await folderNameToPath(project.name);
2301
+ const absPath = expandHomePath(displayPath, homeDir);
2302
+ try {
2303
+ const stats = await fs10.stat(absPath);
2304
+ if (!stats.isDirectory()) {
2305
+ staleSet.add(project.name);
2306
+ }
2307
+ } catch (error) {
2308
+ const err = error;
2309
+ if (err.code === "ENOENT") {
2310
+ staleSet.add(project.name);
2311
+ }
2312
+ }
2313
+ })
2314
+ ),
2315
+ { concurrency: 10 }
2316
+ );
2246
2317
  const results = yield* Effect8.all(
2247
2318
  targetProjects.map(
2248
2319
  (project) => Effect8.gen(function* () {
2320
+ const isStale = staleSet.has(project.name);
2249
2321
  const sessions = yield* listSessions(project.name);
2250
2322
  const emptySessions = sessions.filter((s) => s.messageCount === 0);
2251
2323
  const invalidSessions = sessions.filter(
@@ -2266,8 +2338,9 @@ var previewCleanup = (projectName) => Effect8.gen(function* () {
2266
2338
  invalidSessions,
2267
2339
  emptyWithTodosCount,
2268
2340
  orphanAgentCount: orphanAgents.length,
2269
- orphanTodoCount: 0
2341
+ orphanTodoCount: 0,
2270
2342
  // Will set for first project only
2343
+ isStale
2271
2344
  };
2272
2345
  })
2273
2346
  ),
@@ -2285,7 +2358,9 @@ var clearSessions = (options) => Effect8.gen(function* () {
2285
2358
  clearInvalid = true,
2286
2359
  skipWithTodos = true,
2287
2360
  clearOrphanAgents = true,
2288
- clearOrphanTodos = false
2361
+ clearOrphanTodos = false,
2362
+ clearStale = false,
2363
+ staleProjects = []
2289
2364
  } = options;
2290
2365
  const projects = yield* listProjects;
2291
2366
  const targetProjects = projectName ? projects.filter((p) => p.name === projectName) : projects;
@@ -2343,12 +2418,24 @@ var clearSessions = (options) => Effect8.gen(function* () {
2343
2418
  const result = yield* deleteOrphanTodos();
2344
2419
  deletedOrphanTodoCount = result.deletedCount;
2345
2420
  }
2421
+ let deletedStaleProjectCount = 0;
2422
+ if (clearStale && staleProjects.length > 0) {
2423
+ const sessionsDir = getSessionsDir();
2424
+ for (const staleProjectName of staleProjects) {
2425
+ const projectSessionsPath = path9.join(sessionsDir, staleProjectName);
2426
+ const deleted = yield* Effect8.tryPromise(
2427
+ () => fs10.rm(projectSessionsPath, { recursive: true, force: true }).then(() => true)
2428
+ ).pipe(Effect8.orElse(() => Effect8.succeed(false)));
2429
+ if (deleted) deletedStaleProjectCount++;
2430
+ }
2431
+ }
2346
2432
  return {
2347
2433
  success: true,
2348
2434
  deletedCount: deletedSessionCount,
2349
2435
  removedMessageCount,
2350
2436
  deletedOrphanAgentCount,
2351
- deletedOrphanTodoCount
2437
+ deletedOrphanTodoCount,
2438
+ deletedStaleProjectCount
2352
2439
  };
2353
2440
  });
2354
2441
 
@@ -2575,6 +2662,7 @@ export {
2575
2662
  formatRelativeTime,
2576
2663
  generateTreeNodeId,
2577
2664
  getCachePath,
2665
+ getDisplaySortTimestamp,
2578
2666
  getDisplayTitle,
2579
2667
  getIndexEntryDisplayTitle,
2580
2668
  getLogger,
@@ -2583,6 +2671,7 @@ export {
2583
2671
  getSessionSortTimestamp,
2584
2672
  getSessionTooltip,
2585
2673
  getSessionsDir,
2674
+ getSummarySortTimestamp,
2586
2675
  getTodoIcon,
2587
2676
  getTodosDir,
2588
2677
  getTotalTodoCount,