Skip to content

Conversation

@devin-ai-integration
Copy link
Contributor

@devin-ai-integration devin-ai-integration bot commented Jan 23, 2026

feat: implement lazy loading for session content

Summary

This PR implements hybrid lazy loading for sessions to improve app startup time. At startup, only session metadata (title, created_at, participants, etc.) is loaded into TinyBase. Session content (transcripts, notes, raw_md) is loaded on-demand when a session is opened.

Key changes:

  • Added LoadMode type ("metadata" | "full") to control what gets loaded during startup
  • Modified loadAllSessionData to use "metadata" mode at startup, skipping transcript and note file reads
  • Added ensureSessionContentLoaded() function that loads content into TinyBase when a session is opened
  • Added useSessionContentLoader hook for components to trigger content loading
  • Shows "Loading session..." while content is being fetched
  • Newly created sessions are marked as loaded to avoid unnecessary disk reads

The existing TinyBase persister infrastructure, editing, undo/redo, and auto-save logic remain unchanged - content is still stored in TinyBase once loaded.

Review & Testing Checklist for Human

  • Test startup time improvement - With many sessions, startup should be noticeably faster since only _meta.json files are read
  • Test opening existing sessions - Content should load correctly when opening a session, verify transcripts and notes appear
  • Test creating new sessions - New sessions should work immediately without loading delay (they're marked as loaded on creation)
  • Test rapid session switching - Open multiple sessions quickly, verify no race conditions or missing content
  • Test editing after lazy load - After content loads, verify editing, undo/redo, and auto-save still work correctly

Recommended test plan:

  1. Create several test sessions with transcripts and notes
  2. Restart the app and measure startup time
  3. Open each session and verify content loads correctly
  4. Create a new session and start recording - verify it works without loading delay
  5. Edit a lazy-loaded session and verify changes persist

Notes

There's module-level state (contentLoadState) tracking which sessions have content loaded. This is intentional to prevent duplicate loads, but worth noting that this state is per-window and resets on hot reload.

Link to Devin run: https://app.devin.ai/sessions/7dddac540af2421da06531009e8acdf9
Requested by: yujonglee (@yujonglee)


Open with Devin

- Load only metadata at startup for faster app launch
- Load session content (transcripts, notes) on-demand when session is opened
- Add useSessionContentLoader hook for components to trigger content loading
- Show loading state while content is being fetched
- Mark newly created sessions as loaded to avoid unnecessary loading
- Clear content load state when sessions are reloaded

Co-Authored-By: yujonglee <yujonglee.dev@gmail.com>
@devin-ai-integration
Copy link
Contributor Author

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR that start with 'DevinAI' or '@devin'.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment and CI monitoring

@netlify
Copy link

netlify bot commented Jan 23, 2026

Deploy Preview for hyprnote-storybook canceled.

Name Link
🔨 Latest commit 82b3a62
🔍 Latest deploy log https://app.netlify.com/projects/hyprnote-storybook/deploys/69737968ccf7f70008b0f665

@netlify
Copy link

netlify bot commented Jan 23, 2026

Deploy Preview for howto-fix-macos-audio-selection canceled.

Name Link
🔨 Latest commit 82b3a62
🔍 Latest deploy log https://app.netlify.com/projects/howto-fix-macos-audio-selection/deploys/697379688dd6bf000890c9a4

@netlify
Copy link

netlify bot commented Jan 23, 2026

Deploy Preview for hyprnote canceled.

Name Link
🔨 Latest commit 82b3a62
🔍 Latest deploy log https://app.netlify.com/projects/hyprnote/deploys/6973796815af2f0008ec11e4

Co-Authored-By: yujonglee <yujonglee.dev@gmail.com>
Copy link
Contributor Author

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 2 potential issues.

View issues and 4 additional flags in Devin Review.

Open in Devin Review

Comment on lines +96 to +102
if (contentLoading) {
return (
<div className="flex items-center justify-center h-full">
<div className="text-sm text-muted-foreground">Loading session...</div>
</div>
);
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 React hooks called after conditional early return

The TabContentNote component has an early return at lines 96-102 that occurs before hooks are called, violating React's rules of hooks.

Click to expand

Problem

In TabContentNote, hooks (useEffect at line 104 and useQuery at line 135) are called after a conditional early return:

if (contentLoading) {
  return (
    <div className="flex items-center justify-center h-full">
      <div className="text-sm text-muted-foreground">Loading session...</div>
    </div>
  );
}

useEffect(() => { // This hook is called conditionally!
  ...
}, [...]);

React requires hooks to be called in the same order on every render. When contentLoading is true, the hooks after the return statement are not called, but when contentLoading becomes false, they suddenly are. This can cause:

  • React errors/warnings about hooks being called in a different order
  • Unpredictable behavior and potential crashes
  • State inconsistencies

Fix

Move all hooks before the early return, or move the loading check into the JSX return.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +132 to 174
export async function loadSessionContent(
dataDir: string,
sessionId: string,
): Promise<LoadResult<LoadedSessionData>> {
const result = createEmptyLoadedSessionData();
const sessionsDir = [dataDir, "sessions"].join(sep());

const scanResult = await fsSyncCommands.scanAndRead(
sessionsDir,
[SESSION_TRANSCRIPT_FILE, `*${SESSION_NOTE_EXTENSION}`],
true,
`/${sessionId}/`,
);

if (scanResult.status === "error") {
if (isDirectoryNotFoundError(scanResult.error)) {
return ok(result);
}
console.error(
`[${LABEL}] loadSessionContent scan error:`,
scanResult.error,
);
return err(scanResult.error);
}

for (const [path, content] of Object.entries(scanResult.data.files)) {
if (!content) continue;
if (path.endsWith(SESSION_TRANSCRIPT_FILE)) {
processTranscriptFile(path, content, result);
}
}

const mdPromises: Promise<void>[] = [];
for (const [path, content] of Object.entries(scanResult.data.files)) {
if (!content) continue;
if (path.endsWith(SESSION_NOTE_EXTENSION)) {
mdPromises.push(processMdFile(path, content, result));
}
}
await Promise.all(mdPromises);

return ok(result);
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Session raw_md content is never loaded during lazy loading

The loadSessionContent function fails to load raw_md content from _memo.md files because result.sessions is empty when processing note files.

Click to expand

Problem

In loadSessionContent (apps/desktop/src/store/tinybase/persister/session/load/index.ts:132-174):

  1. It creates an empty result with result.sessions = {}
  2. It scans for *.md files including _memo.md
  3. When processMdFile processes _memo.md, it checks if (result.sessions[fm.session_id]) at note.ts:37 before setting raw_md
  4. Since no meta file is loaded, result.sessions is empty, so the check fails and raw_md is never set
// In note.ts:36-39
if (path.endsWith(SESSION_MEMO_FILE)) {
  if (result.sessions[fm.session_id]) {  // Always false since sessions is empty!
    result.sessions[fm.session_id].raw_md = tiptapContent;
  }
}

Then in ops.ts:98-101, the code tries to use this never-populated data:

const session = result.data.sessions[sessionId]; // undefined
if (session?.raw_md) {
  store.setCell("sessions", sessionId, "raw_md", session.raw_md);
}

Impact

User notes stored in _memo.md files will not be loaded when opening existing sessions. Users will see empty notes even though they have saved content.

Fix

In loadSessionContent, initialize result.sessions[sessionId] before processing note files, or change processMdFile to create the session entry if it doesn't exist for memo files.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +77 to +84
if (result.status === "error") {
console.error(
`[SessionOps] Failed to load content for session ${sessionId}:`,
result.error,
);
contentLoadState.loading.delete(sessionId);
contentLoadState.loaded.add(sessionId);
return false;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Error handling breaks retry logic: When content loading fails, the session is still marked as loaded (line 83). This prevents any future retry attempts, permanently leaving the session without its content.

if (result.status === "error") {
  console.error(
    `[SessionOps] Failed to load content for session ${sessionId}:`,
    result.error,
  );
  contentLoadState.loading.delete(sessionId);
  // Don't mark as loaded on error - allow retry
  return false;
}

Remove line 83 (contentLoadState.loaded.add(sessionId)) from the error handler to allow retries on subsequent session opens.

Suggested change
if (result.status === "error") {
console.error(
`[SessionOps] Failed to load content for session ${sessionId}:`,
result.error,
);
contentLoadState.loading.delete(sessionId);
contentLoadState.loaded.add(sessionId);
return false;
if (result.status === "error") {
console.error(
`[SessionOps] Failed to load content for session ${sessionId}:`,
result.error,
);
contentLoadState.loading.delete(sessionId);
return false;

Spotted by Graphite Agent

Fix in Graphite


Is this helpful? React 👍 or 👎 to let us know.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants