diff --git a/src/main/services/discovery/ProjectScanner.ts b/src/main/services/discovery/ProjectScanner.ts
index 069af1bc..32e4b551 100644
--- a/src/main/services/discovery/ProjectScanner.ts
+++ b/src/main/services/discovery/ProjectScanner.ts
@@ -760,6 +760,7 @@ export class ProjectScanner {
projectPath,
todoData,
createdAt: Math.floor(createdAt),
+ updatedAt: Math.floor(effectiveMtime),
firstMessage: metadata.firstUserMessage?.text,
messageTimestamp: metadata.firstUserMessage?.timestamp,
hasSubagents,
@@ -830,6 +831,7 @@ export class ProjectScanner {
projectId,
projectPath,
createdAt: Math.floor(createdAt),
+ updatedAt: Math.floor(effectiveMtime),
firstMessage: metadata.firstUserMessage?.text,
messageTimestamp: metadata.firstUserMessage?.timestamp,
hasSubagents: false,
diff --git a/src/main/types/domain.ts b/src/main/types/domain.ts
index 6d2146b0..c50f1a44 100644
--- a/src/main/types/domain.ts
+++ b/src/main/types/domain.ts
@@ -89,6 +89,8 @@ export interface Session {
todoData?: unknown;
/** Unix timestamp when session file was created */
createdAt: number;
+ /** Unix timestamp of last update/activity */
+ updatedAt?: number;
/** First user message text (for preview) */
firstMessage?: string;
/** Timestamp of first user message (RFC3339) */
diff --git a/src/main/utils/jsonl.ts b/src/main/utils/jsonl.ts
index d998438f..8c90181e 100644
--- a/src/main/utils/jsonl.ts
+++ b/src/main/utils/jsonl.ts
@@ -440,7 +440,11 @@ export async function analyzeSessionFileMetadata(
gitBranch = entry.gitBranch;
}
- if (!firstUserMessage && entry.type === 'user') {
+ if (
+ !firstUserMessage &&
+ entry.type === 'user' &&
+ !('isMeta' in entry && entry.isMeta === true)
+ ) {
const content = entry.message?.content;
if (typeof content === 'string') {
if (isCommandOutputContent(content)) {
diff --git a/src/renderer/components/sidebar/SessionItem.tsx b/src/renderer/components/sidebar/SessionItem.tsx
index b2bba28f..03392ffb 100644
--- a/src/renderer/components/sidebar/SessionItem.tsx
+++ b/src/renderer/components/sidebar/SessionItem.tsx
@@ -285,7 +285,11 @@ export const SessionItem = React.memo(function SessionItem({
{session.messageCount}
·
- {formatShortTime(new Date(session.createdAt))}
+
+ {formatShortTime(
+ new Date(Math.max(session.updatedAt ?? session.createdAt, session.createdAt))
+ )}
+
{session.contextConsumption != null && session.contextConsumption > 0 && (
<>
·
diff --git a/src/renderer/store/index.ts b/src/renderer/store/index.ts
index 07aa12c5..7c2b1c23 100644
--- a/src/renderer/store/index.ts
+++ b/src/renderer/store/index.ts
@@ -249,15 +249,13 @@ export function initializeNotificationListeners(): () => void {
const isUnknownSessionInSidebar =
event.sessionId == null ||
!state.sessions.some((session) => session.id === event.sessionId);
- const shouldRefreshForPotentialNewSession =
+ const shouldRefreshSidebar =
isTopLevelSessionEvent &&
matchesSelectedProject &&
- isUnknownSessionInSidebar &&
- (event.type === 'add' || (state.connectionMode === 'local' && event.type === 'change'));
+ (isUnknownSessionInSidebar || event.type === 'change' || event.type === 'add');
- // Refresh sidebar session list only when a truly new top-level session appears.
- // Local fs.watch can report "change" before/without "add" for newly created files.
- if (shouldRefreshForPotentialNewSession) {
+ // Refresh sidebar session list when a new session appears or an existing session updates.
+ if (shouldRefreshSidebar) {
if (matchesSelectedProject && selectedProjectId) {
scheduleProjectRefresh(selectedProjectId);
}
diff --git a/src/renderer/store/slices/sessionSlice.ts b/src/renderer/store/slices/sessionSlice.ts
index 31f9596b..c0319d2c 100644
--- a/src/renderer/store/slices/sessionSlice.ts
+++ b/src/renderer/store/slices/sessionSlice.ts
@@ -109,8 +109,12 @@ export const createSessionSlice: StateCreator =
set({ sessionsLoading: true, sessionsError: null });
try {
const sessions = await api.getSessions(projectId);
- // Sort by createdAt (descending)
- const sorted = [...sessions].sort((a, b) => b.createdAt - a.createdAt);
+ // Sort by max of updatedAt/createdAt (descending)
+ const sorted = [...sessions].sort(
+ (a, b) =>
+ Math.max(b.updatedAt ?? b.createdAt, b.createdAt) -
+ Math.max(a.updatedAt ?? a.createdAt, a.createdAt)
+ );
set({ sessions: sorted, sessionsLoading: false });
} catch (error) {
set({
diff --git a/src/renderer/utils/dateGrouping.ts b/src/renderer/utils/dateGrouping.ts
index 718c0351..bfcfd630 100644
--- a/src/renderer/utils/dateGrouping.ts
+++ b/src/renderer/utils/dateGrouping.ts
@@ -12,11 +12,12 @@ import type { DateCategory, DateGroupedSessions } from '../types/tabs';
/**
* Groups sessions by relative date category.
- * Sessions are categorized based on their createdAt timestamp:
- * - Today: Sessions created today
- * - Yesterday: Sessions created yesterday
- * - Previous 7 Days: Sessions created 2-7 days ago
- * - Older: Sessions created more than 7 days ago
+ * Sessions are categorized based on their most recent timestamp
+ * (max(updatedAt, createdAt), with createdAt fallback):
+ * - Today: Sessions updated/created today
+ * - Yesterday: Sessions updated/created yesterday
+ * - Previous 7 Days: Sessions updated/created 2-7 days ago
+ * - Older: Sessions updated/created more than 7 days ago
*
* Within each category, sessions maintain their original sort order.
*
@@ -28,7 +29,9 @@ export function groupSessionsByDate(sessions: Session[]): DateGroupedSessions {
return sessions.reduce(
(acc, session) => {
- const sessionDate = new Date(session.createdAt);
+ // Use updatedAt if available, fallback to createdAt. Ensure we don't go backwards in time.
+ const timestamp = Math.max(session.updatedAt ?? session.createdAt, session.createdAt);
+ const sessionDate = new Date(timestamp);
if (isToday(sessionDate)) {
acc.Today.push(session);
diff --git a/test-debug.ts b/test-debug.ts
new file mode 100644
index 00000000..ad8fad2a
--- /dev/null
+++ b/test-debug.ts
@@ -0,0 +1,36 @@
+import * as fs from 'fs';
+import * as os from 'os';
+import * as path from 'path';
+import { ProjectScanner } from './src/main/services/discovery/ProjectScanner';
+
+async function run() {
+ const projectsDir = fs.mkdtempSync(path.join(os.tmpdir(), 'scanner-'));
+ const encodedName = '-Users-test-myproject';
+ const projectDir = path.join(projectsDir, encodedName);
+ fs.mkdirSync(projectDir);
+
+ const filePath = path.join(projectDir, 'session-timestamp-test.jsonl');
+
+ const oldDateMs = Date.now() - 30 * 24 * 60 * 60 * 1000;
+ const oldIsoString = new Date(oldDateMs).toISOString();
+
+ fs.writeFileSync(
+ filePath,
+ JSON.stringify({
+ uuid: 'test-uuid',
+ type: 'user',
+ message: { role: 'user', content: 'hello' },
+ timestamp: oldIsoString,
+ isMeta: false,
+ }) + '\n'
+ );
+
+ const nowMs = Date.now();
+ fs.utimesSync(filePath, new Date(nowMs), new Date(nowMs));
+
+ const scanner = new ProjectScanner(projectsDir);
+ const projects = await scanner.scanProject(encodedName);
+ const sessions = await scanner.listSessions(encodedName);
+ console.log(JSON.stringify(sessions, null, 2));
+}
+run();
diff --git a/test/main/services/discovery/ProjectScanner.updatedAt.test.ts b/test/main/services/discovery/ProjectScanner.updatedAt.test.ts
new file mode 100644
index 00000000..c64e501a
--- /dev/null
+++ b/test/main/services/discovery/ProjectScanner.updatedAt.test.ts
@@ -0,0 +1,70 @@
+import * as fs from 'fs';
+import * as os from 'os';
+import * as path from 'path';
+import { afterEach, describe, expect, it } from 'vitest';
+
+import { ProjectScanner } from '../../../../src/main/services/discovery/ProjectScanner';
+
+function createSessionLine(opts: { type?: string; timestamp?: string; role?: string }): string {
+ return JSON.stringify({
+ uuid: 'test-uuid',
+ type: opts.type ?? 'user',
+ message: { role: opts.role ?? 'user', content: 'hello' },
+ timestamp: opts.timestamp ?? new Date().toISOString(),
+ isMeta: false, // Must not be meta so the scanner recognizes it as the first real user message
+ });
+}
+
+describe('ProjectScanner updatedAt logic', () => {
+ const tempDirs: string[] = [];
+
+ afterEach(async () => {
+ await new Promise((resolve) => setTimeout(resolve, 50));
+ for (const dir of tempDirs) {
+ try {
+ fs.rmSync(dir, { recursive: true, force: true, maxRetries: 5, retryDelay: 200 });
+ } catch {
+ // Ignore cleanup failures
+ }
+ }
+ tempDirs.length = 0;
+ });
+
+ it('preserves old createdAt from first user message but uses recent mtime for updatedAt', async () => {
+ const projectsDir = fs.mkdtempSync(path.join(os.tmpdir(), 'scanner-'));
+ tempDirs.push(projectsDir);
+
+ const encodedName = '-Users-test-myproject';
+ const projectDir = path.join(projectsDir, encodedName);
+ fs.mkdirSync(projectDir);
+
+ const filePath = path.join(projectDir, 'session-timestamp-test.jsonl');
+
+ // Simulate an old first user message (weeks ago)
+ const oldDateMs = Date.now() - 30 * 24 * 60 * 60 * 1000;
+ const oldIsoString = new Date(oldDateMs).toISOString();
+
+ fs.writeFileSync(
+ filePath,
+ createSessionLine({ type: 'user', role: 'user', timestamp: oldIsoString }) + '\n'
+ );
+
+ // Force the file mtime to be exactly now
+ const nowMs = Date.now();
+ fs.utimesSync(filePath, new Date(nowMs), new Date(nowMs));
+
+ const scanner = new ProjectScanner(projectsDir);
+ const sessions = await scanner.listSessions(encodedName);
+
+ expect(sessions).toHaveLength(1);
+
+ const session = sessions[0];
+
+ // createdAt should strictly use the old message timestamp
+ expect(session.createdAt).toBe(Math.floor(oldDateMs));
+
+ // updatedAt should use the forced recent file mtime (within ~1 second variance depending on fs resolution)
+ expect(session.updatedAt).toBeDefined();
+ expect(Math.abs(session.updatedAt! - nowMs)).toBeLessThan(2000);
+ });
+});
diff --git a/test/renderer/utils/dateGrouping.test.ts b/test/renderer/utils/dateGrouping.test.ts
index 381f0656..bb388b9a 100644
--- a/test/renderer/utils/dateGrouping.test.ts
+++ b/test/renderer/utils/dateGrouping.test.ts
@@ -7,18 +7,15 @@ import {
import type { Session } from '../../../src/renderer/types/data';
// Helper to create a session with a specific date
-function createSession(id: string, createdAt: Date): Session {
+function createSession(id: string, createdAt: Date, updatedAt?: Date): Session {
return {
id,
- createdAt: createdAt.toISOString(),
- updatedAt: createdAt.toISOString(),
- displayName: `Session ${id}`,
- triggerCount: 1,
- ongoing: false,
- lastTriggerPreview: 'Test',
- cwd: '/test',
- todos: [],
- totalTokens: 0,
+ projectId: 'test-project',
+ projectPath: '/test/project',
+ createdAt: createdAt.getTime(),
+ updatedAt: updatedAt?.getTime(),
+ hasSubagents: false,
+ messageCount: 5,
};
}
@@ -119,6 +116,28 @@ describe('dateGrouping', () => {
expect(result.Today.map((s) => s.id)).toEqual(['first', 'second', 'third']);
});
+
+ it('should use updatedAt if available over createdAt', () => {
+ const createdAgo = new Date('2024-01-01T10:00:00Z'); // Older
+ const updatedToday = new Date('2024-01-15T10:00:00Z'); // Today
+ const sessions = [createSession('1', createdAgo, updatedToday)];
+
+ const result = groupSessionsByDate(sessions);
+
+ expect(result.Today).toHaveLength(1);
+ expect(result.Older).toHaveLength(0);
+ });
+
+ it('should not allow an older updatedAt to override a newer createdAt', () => {
+ const createdToday = new Date('2024-01-15T10:00:00Z'); // Today
+ const updatedOld = new Date('2024-01-01T10:00:00Z'); // Older
+ const sessions = [createSession('1', createdToday, updatedOld)];
+
+ const result = groupSessionsByDate(sessions);
+
+ expect(result.Today).toHaveLength(1);
+ expect(result.Older).toHaveLength(0);
+ });
});
describe('getNonEmptyCategories', () => {