From 8e83e00210e8e93587a3488dadb9f2f6a9f1fbf2 Mon Sep 17 00:00:00 2001 From: "IM.codes" Date: Fri, 8 May 2026 23:06:11 +0800 Subject: [PATCH 1/2] Remove main session fullscreen border --- web/src/app.tsx | 1 - web/src/styles.css | 1 - 2 files changed, 2 deletions(-) diff --git a/web/src/app.tsx b/web/src/app.tsx index 969139cc..3a959c0e 100644 --- a/web/src/app.tsx +++ b/web/src/app.tsx @@ -3477,7 +3477,6 @@ export function App() {
{/* Desktop local preview shortcut — available even before a session is active */} diff --git a/web/src/styles.css b/web/src/styles.css index 57ed7027..0a4c3096 100644 --- a/web/src/styles.css +++ b/web/src/styles.css @@ -1003,7 +1003,6 @@ body { .desktop-view-toggle { display: flex; justify-content: flex-end; padding: 2px 8px; background: #0f172a; gap: 4px; } .desktop-view-toggle .view-toggle { font-size: 12px; padding: 2px 8px; } .desktop-view-toggle .desktop-main-maximize-toggle { display: inline-flex; align-items: center; justify-content: center; min-width: 34px; } -.desktop-workspace-maximized { box-shadow: inset 0 0 0 2px #3b82f6, inset 0 0 0 3px rgba(96,165,250,0.35); } .desktop-fb-float { position: fixed; z-index: 1500; display: flex; flex-direction: column; background: #0f172a; border: 1px solid #334155; border-radius: 8px; box-shadow: 0 8px 32px rgba(0,0,0,0.5); overflow: hidden; } .desktop-fb-titlebar { display: flex; align-items: center; justify-content: space-between; padding: 6px 10px; border-bottom: 1px solid #334155; cursor: grab; flex-shrink: 0; user-select: none; } .desktop-fb-titlebar:active { cursor: grabbing; } From df9ce89496d2dbe143fd5010947c9c7bf12fb4f8 Mon Sep 17 00:00:00 2001 From: "IM.codes" Date: Fri, 8 May 2026 23:12:17 +0800 Subject: [PATCH 2/2] Fix timeline projection delete race --- src/daemon/timeline-projection-worker.ts | 4 ++++ test/daemon/timeline-projection.test.ts | 25 ++++++++++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/src/daemon/timeline-projection-worker.ts b/src/daemon/timeline-projection-worker.ts index e231805a..08a46f70 100644 --- a/src/daemon/timeline-projection-worker.ts +++ b/src/daemon/timeline-projection-worker.ts @@ -28,6 +28,7 @@ const dbPath = typeof workerData?.dbPath === 'string' && workerData.dbPath let db: DatabaseSyncInstance | null = null; const rebuildPromises = new Map>(); +const sessionMutationGenerations = new Map(); let writesSinceCheckpoint = 0; function sessionFilePath(sessionId: string): string { @@ -145,6 +146,7 @@ function upsertSessionMeta(sessionId: string, meta: { } function deleteSessionRows(sessionId: string): void { + sessionMutationGenerations.set(sessionId, (sessionMutationGenerations.get(sessionId) ?? 0) + 1); const database = ensureDb(); database.prepare('DELETE FROM timeline_projection_events WHERE session_id = ?').run(sessionId); database.prepare('DELETE FROM timeline_projection_sessions WHERE session_id = ?').run(sessionId); @@ -289,7 +291,9 @@ async function rebuildSessionInternal(sessionId: string): Promise { } function scheduleSessionRebuild(sessionId: string): void { + const scheduledGeneration = sessionMutationGenerations.get(sessionId) ?? 0; setImmediate(() => { + if ((sessionMutationGenerations.get(sessionId) ?? 0) !== scheduledGeneration) return; void rebuildSessionInternal(sessionId).catch(() => { // Query paths must stay fast and fail-open; an explicit rebuild request or // the next append/query can retry projection repair. diff --git a/test/daemon/timeline-projection.test.ts b/test/daemon/timeline-projection.test.ts index 66bc22e9..b03d7745 100644 --- a/test/daemon/timeline-projection.test.ts +++ b/test/daemon/timeline-projection.test.ts @@ -272,6 +272,31 @@ describe('timeline projection', () => { expect(explicitlyRebuilt?.map((event) => event.seq)).toEqual([2, 3]); }); + it('does not let a stale scheduled rebuild repopulate after delete', async () => { + const { timelineProjection, timelineStore } = await loadModules(); + const sessionId = 'projection_delete_cancels_scheduled_rebuild'; + const timelineFile = timelineStore.filePath(sessionId); + mkdirSync(join(tempHome!, '.imcodes', 'timeline'), { recursive: true }); + + timelineStore.append(makeEvent(sessionId, 1, 'assistant.text', { text: 'one' }, 1000)); + timelineStore.append(makeEvent(sessionId, 2, 'assistant.text', { text: 'two' }, 1001)); + await timelineProjection.rebuildSession(sessionId); + + appendFileSync(timelineFile, `${JSON.stringify(makeEvent(sessionId, 3, 'assistant.text', { text: 'three' }, 1002))}\n`); + const stale = await timelineProjection.queryHistory({ sessionId, limit: 10 }); + expect(stale?.map((event) => event.seq)).toEqual([1, 2]); + + await timelineProjection.deleteSession(sessionId); + await sleep(100); + + const afterDelete = await timelineProjection.queryHistory({ sessionId, limit: 10 }); + expect(afterDelete?.map((event) => event.seq)).toEqual([]); + + await timelineProjection.rebuildSession(sessionId); + const explicitlyRebuilt = await timelineProjection.queryHistory({ sessionId, limit: 10 }); + expect(explicitlyRebuilt?.map((event) => event.seq)).toEqual([1, 2, 3]); + }); + it('does not parse appended JSONL tails on the read path', async () => { const { timelineProjection, timelineStore } = await loadModules(); const sessionId = 'projection_incremental_tail';