Skip to content

feat: open notes in temporary tab#304

Merged
vitonsky merged 115 commits into
masterfrom
251-feat-open-note-in-temporary-tab
May 18, 2026
Merged

feat: open notes in temporary tab#304
vitonsky merged 115 commits into
masterfrom
251-feat-open-note-in-temporary-tab

Conversation

@vitonsky

@vitonsky vitonsky commented May 18, 2026

Copy link
Copy Markdown
Member

Closes #251

Related to #269

In this PR I just to polish the code.

Summary by CodeRabbit

  • New Features

    • Notes can be opened in preview mode with distinctive tab styling.
    • Double-click a preview tab to convert it to a regular tab.
    • Preview tabs automatically switch to regular when you edit the note.
  • Behavior Changes

    • Newly created and restored notes open in regular (non-preview) mode.
    • Workspace restore now preserves which tab was a preview.

Review Change Stack

@coderabbitai

coderabbitai Bot commented May 18, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

This PR implements a preview tab feature allowing users to open notes temporarily. Notes open in preview mode by single-clicking (or via single-click in lists), automatically close when another preview tab opens, and promote to permanent tabs when edited or double-clicked. The Redux store is extended with previewTabId, and the note-opening API is updated to accept an options object with a preview flag.

Changes

Preview Tab Feature

Layer / File(s) Summary
State shape and schema updates
packages/app/src/features/App/Workspace/services/workspaceState.ts, packages/app/src/state/redux/vaults/vaults.ts
Workspace state schema and types now include previewTabId: NoteId | null field, initialized to null when creating new workspace objects.
Redux reducers for preview state management
packages/app/src/state/redux/vaults/vaults.ts
addOpenedNote reducer now accepts optional isPreview and isActive flags to manage preview tabs (ensuring only one open at a time), removeOpenedNote clears previewTabId when that note is removed, and setOpenedNotesState and togglePreviewTabToRegular replace/extend the opened-notes API to restore full workspace state including preview designation.
Redux selectors exposing preview state
packages/app/src/state/redux/vaults/selectors/notes.ts, packages/app/src/state/redux/vaults/selectors/selectWorkspaceState.ts
New selectPreviewTabId selector returns the current preview tab ID, and selectWorkspaceState now includes previewTabId in the workspace snapshot.
Workspace initialization with preview state
packages/app/src/features/App/Workspace/WorkspaceStateInitializer.tsx
Restore flow now dispatches setOpenedNotesState with activeNoteId and previewTabId from saved workspace state instead of two separate actions.
Note opening contract and implementation
packages/app/src/features/App/Workspace/WorkspaceProvider.tsx, packages/app/src/features/App/Workspace/index.tsx, packages/app/src/hooks/notes/useNoteActions.ts
NotesApi.openNote signature changes from (note, focus?: boolean) to (note, options?: { preview?: boolean }), implementation dispatches addOpenedNote with isPreview and isActive flags, and useNoteActions.click now accepts and propagates the options object through note fetching and activation.
Core user actions defaulting to non-preview
packages/app/src/hooks/notes/useCreateNote.ts, packages/app/src/hooks/notes/useNotesShortcutActions.ts
Newly created notes and restored closed notes explicitly open with preview: false to establish that user-initiated persistent actions do not use preview mode.
NotesList preview mode interactions
packages/app/src/features/MainScreen/NotesListPanel/NotesList.tsx
Single-click on a note preview opens it with preview: true, and double-click promotes it to regular mode via preview: false.
NoteEditor edit-to-promote automation
packages/app/src/features/NoteEditor/index.tsx, packages/app/src/features/NoteEditor/useTogglePreviewTabToRegularOnChange.ts
New useTogglePreviewTabToRegularOnChange hook monitors note content changes and automatically dispatches togglePreviewTabToRegular when the edited note is the current preview tab, wired into the Note component to watch text and title changes.
OpenedNotesPanel preview tab rendering and interactions
packages/app/src/features/NotesContainer/OpenedNotesPanel.tsx, packages/app/src/features/NotesContainer/index.tsx
Tab UI now selects and displays previewTabId, applies italic styling to preview tabs, wires double-click to promote preview to regular via preview: false, and forwards options through the click handler chain.
Redux test setup alignment
packages/app/src/state/redux/vaults/__tests__/redux.vault.activeNote.test.ts
Test fixtures updated to use setOpenedNotesState with explicit activeNoteId and previewTabId: null initialization across multiple test suites.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • DeepinkApp/deepink#217: Addresses workspace state persistence and restoration pipeline; this PR extends the persisted schema with previewTabId and updates the setOpenedNotessetOpenedNotesState migration introduced in that earlier PR.

Poem

🐰 A note flutters open, just for a peek,
No commitment made, just checking the facts.
Double-click to stay, edit to commit—
Preview tabs bow out when their turn is done.
One at a time, like rabbits through gates! ✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat: open notes in temporary tab' directly and specifically describes the main change - implementing temporary/preview tab functionality.
Linked Issues check ✅ Passed All four requirements from issue #251 are implemented: notes open as temporary tabs, double-click/edit converts to regular tabs, only one temporary tab allowed, and API accepts preview flag.
Out of Scope Changes check ✅ Passed All changes are directly related to implementing temporary/preview tab functionality. No unrelated modifications or scope creep detected.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch 251-feat-open-note-in-temporary-tab

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@packages/app/src/features/NoteEditor/useTogglePreviewTabToRegularOnChange.ts`:
- Around line 13-33: The hook useTogglePreviewTabToRegularOnChange currently
triggers promotion whenever any item in deps changes, allowing non-user updates
to promote preview tabs; change its signature to accept an explicit boolean flag
(e.g., shouldPromote) that callers set only from editor input handlers, then add
a guard at the top of the effect to return unless shouldPromote is true and
previewTabId === noteId, and keep deps limited to [shouldPromote, previewTabId,
noteId, workspaceData] so only user-originated edits (when callers pass
shouldPromote=true) can trigger
dispatch(workspacesApi.togglePreviewTabToRegular({ ...workspaceData })).

In `@packages/app/src/features/NotesContainer/OpenedNotesPanel.tsx`:
- Line 124: The double-click handler currently always calls onPick(note.id, {
preview: false }) which promotes every tab; change the handler in
OpenedNotesPanel to first check isPreviewTab (for the note/tab) and only call
onPick(note.id, { preview: false }) when isPreviewTab is true, otherwise do
nothing (or keep existing behavior for regular tabs). Update the onDoubleClick
invocation to conditionally call onPick based on isPreviewTab to avoid
unnecessary state updates and telemetry for non-preview tabs.

In `@packages/app/src/hooks/notes/useNoteActions.ts`:
- Around line 39-50: The async open flow can apply stale preview intent when
multiple rapid clicks resolve out of order; update the handler around
notesRegistry.getById([noteId]) to track the latest intent per noteId (e.g.,
store a pendingPromises map or a sequence token keyed by noteId) so that when
the promise resolves you verify the resolved result matches the most recent
requested intent before calling openNote(note, { preview }); also add rejection
handling for getById() (log or surface the error) so failures don’t silently
drop; reference notesRegistry.getById, noteId, preview, and openNote when
implementing the map/token and error handling.

In `@packages/app/src/state/redux/vaults/vaults.ts`:
- Around line 374-388: When replacing a preview tab in the vault reducer, ensure
workspace.activeNote doesn't end up pointing to a removed note: in the block
handling isPreview (where workspace.previewTabId and workspace.openedNotes are
updated), after removing the previous preview note check if
workspace.previewTabId === workspace.activeNote and, if so, set
workspace.activeNote to the new note.id (or clear it if you prefer) so
activeNote remains valid regardless of the isActive flag; update the logic
around workspace.previewTabId, workspace.openedNotes, and workspace.activeNote
to perform this fix.
- Around line 537-557: The reducer handling the setOpenedNotesState payload can
crash when notes is empty because it does notes[0].id; update the logic in the
setOpenedNotesState handler (the function that processes the PayloadAction with
WorkspaceScoped<{ notes: INote[]; activeNoteId: NoteId; previewTabId: NoteId |
null }>) to first check if notes.length === 0 and, in that case, set
workspace.openedNotes = [] and set workspace.activeNote (and
workspace.previewTab if applicable) to a safe value (null/undefined or leave
unchanged per your type constraints), then return early; otherwise proceed with
the existing Set lookup and fallback to notes[0].id as before.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: f6ea3c1e-93bc-4bec-aa3b-8d62e2e20025

📥 Commits

Reviewing files that changed from the base of the PR and between c53db8a and 428bd33.

📒 Files selected for processing (16)
  • packages/app/src/features/App/Workspace/WorkspaceProvider.tsx
  • packages/app/src/features/App/Workspace/WorkspaceStateInitializer.tsx
  • packages/app/src/features/App/Workspace/index.tsx
  • packages/app/src/features/App/Workspace/services/workspaceState.ts
  • packages/app/src/features/MainScreen/NotesListPanel/NotesList.tsx
  • packages/app/src/features/NoteEditor/index.tsx
  • packages/app/src/features/NoteEditor/useTogglePreviewTabToRegularOnChange.ts
  • packages/app/src/features/NotesContainer/OpenedNotesPanel.tsx
  • packages/app/src/features/NotesContainer/index.tsx
  • packages/app/src/hooks/notes/useCreateNote.ts
  • packages/app/src/hooks/notes/useNoteActions.ts
  • packages/app/src/hooks/notes/useNotesShortcutActions.ts
  • packages/app/src/state/redux/vaults/__tests__/redux.vault.activeNote.test.ts
  • packages/app/src/state/redux/vaults/selectors/notes.ts
  • packages/app/src/state/redux/vaults/selectors/selectWorkspaceState.ts
  • packages/app/src/state/redux/vaults/vaults.ts

Comment thread packages/app/src/features/NotesContainer/OpenedNotesPanel.tsx Outdated
Comment thread packages/app/src/hooks/notes/useNoteActions.ts
Comment thread packages/app/src/state/redux/vaults/vaults.ts
Comment thread packages/app/src/state/redux/vaults/vaults.ts Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/app/src/state/redux/vaults/__tests__/redux.vault.activeNote.test.ts (1)

112-144: 🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Incomplete test coverage: missing previewTabId state verification.

The test sets previewTabId: '3' in the initial state and dispatches addOpenedNote with isPreview: true, but does not verify that previewTabId is correctly updated to '6' after the action. Since the test explicitly exercises preview tab behavior, it should verify the complete preview state.

🧪 Proposed fix to add preview state verification

First, import the selector:

 import {
 	selectActiveNoteId,
 	selectOpenedNotes,
+	selectPreviewTabId,
 	selectRecentlyClosedNotes,
 	workspacesApi,
 } from '../vaults';

Then add an assertion after line 143:

 	expect(
 		selectOpenedNotes(selectors.workspace()).map((note) => note.id),
 	).toStrictEqual(['1', '2', '4', '5', '6']);
+	expect(selectPreviewTabId(selectors.workspace())).toStrictEqual('6');
 });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/app/src/state/redux/vaults/__tests__/redux.vault.activeNote.test.ts`
around lines 112 - 144, The test "Active note must be updated by close preview
tab" is missing verification that the preview tab state is updated when adding a
preview note; import and use the selector selectPreviewTabId and add an
assertion after the existing expectations to assert
selectPreviewTabId(selectors.workspace()) === '6' (this complements the call to
workspacesApi.addOpenedNote with isPreview: true and ensures previewTabId is
updated).
♻️ Duplicate comments (1)
packages/app/src/state/redux/vaults/vaults.ts (1)

539-568: 🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Improve type safety: allow activeNoteId to be nullable.

The past review concern about notes[0].id crashing has been fixed with optional chaining at Line 562. However, the payload type declares activeNoteId: NoteId (non-nullable), but the implementation treats it as potentially invalid by checking noteIdSet.has(activeNoteId) and falling back to notes[0]?.id ?? null. This mismatch can mislead callers about the contract.

📝 Proposed type fix
 		setOpenedNotesState: (
 			state,
 			{
 				payload: { vaultId, workspaceId, notes, activeNoteId, previewTabId },
 			}: PayloadAction<
 				WorkspaceScoped<{
 					notes: INote[];
-					activeNoteId: NoteId;
+					activeNoteId: NoteId | null;
 					previewTabId: NoteId | null;
 				}>
 			>,
 		) => {
 			const workspace = selectWorkspaceObject(state, { vaultId, workspaceId });
 			if (!workspace) return;

 			workspace.openedNotes = notes;

 			// Use Set for O(1) lookups
 			const noteIdSet = new Set(notes.map(({ id }) => id));

 			// Fall back to first note if active note not in list
-			workspace.activeNote = noteIdSet.has(activeNoteId)
+			workspace.activeNote = activeNoteId !== null && noteIdSet.has(activeNoteId)
 				? activeNoteId
 				: (notes[0]?.id ?? null);

 			workspace.previewTabId =
 				previewTabId !== null && noteIdSet.has(previewTabId)
 					? previewTabId
 					: null;
 		},
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/app/src/state/redux/vaults/vaults.ts` around lines 539 - 568, The
payload type for setOpenedNotesState is wrong: it declares activeNoteId as
non-nullable but the reducer treats it as possibly missing/invalid; update the
PayloadAction generic so activeNoteId is NoteId | null (and keep previewTabId as
NoteId | null), i.e., change the WorkspaceScoped payload shape referenced in
setOpenedNotesState to allow activeNoteId to be nullable (adjust any related
types like WorkspaceScoped and consumers if necessary) so the runtime fallback
logic using notes[0]?.id ?? null matches the TypeScript contract for
setOpenedNotesState, activeNoteId, and previewTabId.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In
`@packages/app/src/state/redux/vaults/__tests__/redux.vault.activeNote.test.ts`:
- Around line 112-144: The test "Active note must be updated by close preview
tab" is missing verification that the preview tab state is updated when adding a
preview note; import and use the selector selectPreviewTabId and add an
assertion after the existing expectations to assert
selectPreviewTabId(selectors.workspace()) === '6' (this complements the call to
workspacesApi.addOpenedNote with isPreview: true and ensures previewTabId is
updated).

---

Duplicate comments:
In `@packages/app/src/state/redux/vaults/vaults.ts`:
- Around line 539-568: The payload type for setOpenedNotesState is wrong: it
declares activeNoteId as non-nullable but the reducer treats it as possibly
missing/invalid; update the PayloadAction generic so activeNoteId is NoteId |
null (and keep previewTabId as NoteId | null), i.e., change the WorkspaceScoped
payload shape referenced in setOpenedNotesState to allow activeNoteId to be
nullable (adjust any related types like WorkspaceScoped and consumers if
necessary) so the runtime fallback logic using notes[0]?.id ?? null matches the
TypeScript contract for setOpenedNotesState, activeNoteId, and previewTabId.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 1819ffcf-a967-4d0a-a7a2-26da4d5552bf

📥 Commits

Reviewing files that changed from the base of the PR and between 428bd33 and 6e26826.

📒 Files selected for processing (3)
  • packages/app/src/features/NotesContainer/OpenedNotesPanel.tsx
  • packages/app/src/state/redux/vaults/__tests__/redux.vault.activeNote.test.ts
  • packages/app/src/state/redux/vaults/vaults.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/app/src/features/NotesContainer/OpenedNotesPanel.tsx

@vitonsky vitonsky merged commit b642142 into master May 18, 2026
6 checks passed
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.

Open a note in temporary tab

2 participants