diff --git a/apps/x/apps/renderer/src/App.tsx b/apps/x/apps/renderer/src/App.tsx
index fda27bf6..b6cd2e39 100644
--- a/apps/x/apps/renderer/src/App.tsx
+++ b/apps/x/apps/renderer/src/App.tsx
@@ -4727,6 +4727,25 @@ function App() {
}
return
}
+
+ // Ctrl+Tab — next tab, Ctrl+Shift+Tab — previous tab (browser-style).
+ // Bound to Ctrl specifically (Cmd+Tab is the OS app switcher on macOS).
+ if (e.ctrlKey && e.key === 'Tab') {
+ e.preventDefault()
+ const direction = e.shiftKey ? -1 : 1
+ if (inFileView) {
+ const currentIdx = fileTabs.findIndex(t => t.id === targetFileTabId)
+ if (currentIdx === -1) return
+ const nextIdx = (currentIdx + direction + fileTabs.length) % fileTabs.length
+ switchFileTab(fileTabs[nextIdx].id)
+ } else {
+ const currentIdx = chatTabs.findIndex(t => t.id === activeChatTabId)
+ if (currentIdx === -1) return
+ const nextIdx = (currentIdx + direction + chatTabs.length) % chatTabs.length
+ switchChatTab(chatTabs[nextIdx].id)
+ }
+ return
+ }
}
document.addEventListener('keydown', handleTabKeyDown)
return () => document.removeEventListener('keydown', handleTabKeyDown)
diff --git a/apps/x/apps/renderer/src/components/email-view.tsx b/apps/x/apps/renderer/src/components/email-view.tsx
index 266ae93c..8815723f 100644
--- a/apps/x/apps/renderer/src/components/email-view.tsx
+++ b/apps/x/apps/renderer/src/components/email-view.tsx
@@ -1398,12 +1398,12 @@ const ComposeBox = memo(function ComposeBox({
: (hasGenerated ? 'Edit' : 'Write')}
-
+
@@ -1413,7 +1413,7 @@ const ComposeBox = memo(function ComposeBox({
type="button"
variant="outline"
size="xs"
- className="rounded-full"
+ className="rounded-md"
onClick={() => { void runAi(preset.instruction, 'rewrite') }}
disabled={generating}
>{preset.label}
diff --git a/apps/x/packages/core/src/knowledge/sync_gmail.ts b/apps/x/packages/core/src/knowledge/sync_gmail.ts
index 7868a34e..e4d478e9 100644
--- a/apps/x/packages/core/src/knowledge/sync_gmail.ts
+++ b/apps/x/packages/core/src/knowledge/sync_gmail.ts
@@ -615,11 +615,29 @@ export function listEverythingElseThreads(opts: { cursor?: string; limit?: numbe
return listInboxPage({ section: 'other', ...opts });
}
+// In-memory index of parsed snapshots, keyed by cache filename and validated by
+// file mtime. listInboxPage runs on every inbox open, every "load more", and
+// every throttled live reload during a sync — without this it re-reads and
+// JSON.parses every cached thread (message bodies included) on each call. The
+// cache lets unchanged files skip the read+parse, so e.g. the "Everything else"
+// page right after "Important", reloads mid-sync, and re-opening the inbox cost
+// a cheap stat() per file instead of a full parse of the whole cache.
+interface ListCacheEntry {
+ mtimeMs: number;
+ dateMs: number;
+ section: InboxSection;
+ snapshot: GmailThreadSnapshot;
+}
+const listCache = new Map();
+
export function listInboxPage(opts: InboxPageOptions): InboxPageResult {
const limit = Math.max(1, Math.min(100, opts.limit ?? 25));
const cursor = parseCursor(opts.cursor);
- if (!fs.existsSync(CACHE_DIR)) return { threads: [], nextCursor: null };
+ if (!fs.existsSync(CACHE_DIR)) {
+ listCache.clear();
+ return { threads: [], nextCursor: null };
+ }
let names: string[];
try {
@@ -628,24 +646,56 @@ export function listInboxPage(opts: InboxPageOptions): InboxPageResult {
return { threads: [], nextCursor: null };
}
+ const seen = new Set();
const entries: IndexedEntry[] = [];
for (const name of names) {
if (!name.endsWith('.json')) continue;
+ seen.add(name);
const filePath = path.join(CACHE_DIR, name);
+
+ let mtimeMs: number;
try {
- const raw = fs.readFileSync(filePath, 'utf-8');
- const wrapper = JSON.parse(raw) as SnapshotCacheEntry;
- const snapshot = wrapper.snapshot;
- if (!snapshot) continue;
- if (snapshotImportance(snapshot) !== opts.section) continue;
- entries.push({
- threadId: snapshot.threadId,
- dateMs: snapshotDateMs(snapshot),
- snapshot,
- });
- } catch (err) {
- console.warn(`[Inbox lists] read failed for ${name}:`, err);
+ mtimeMs = fs.statSync(filePath).mtimeMs;
+ } catch {
+ listCache.delete(name);
+ continue;
}
+
+ let cached = listCache.get(name);
+ if (!cached || cached.mtimeMs !== mtimeMs) {
+ try {
+ const raw = fs.readFileSync(filePath, 'utf-8');
+ const wrapper = JSON.parse(raw) as SnapshotCacheEntry;
+ const snapshot = wrapper.snapshot;
+ if (!snapshot) {
+ listCache.delete(name);
+ continue;
+ }
+ cached = {
+ mtimeMs,
+ dateMs: snapshotDateMs(snapshot),
+ section: snapshotImportance(snapshot),
+ snapshot,
+ };
+ listCache.set(name, cached);
+ } catch (err) {
+ console.warn(`[Inbox lists] read failed for ${name}:`, err);
+ listCache.delete(name);
+ continue;
+ }
+ }
+
+ if (cached.section !== opts.section) continue;
+ entries.push({
+ threadId: cached.snapshot.threadId,
+ dateMs: cached.dateMs,
+ snapshot: cached.snapshot,
+ });
+ }
+
+ // Evict cache entries for files that are gone (archived/trashed/pruned).
+ for (const key of listCache.keys()) {
+ if (!seen.has(key)) listCache.delete(key);
}
// Newest first, threadId asc as tiebreak.