Skip to content

fix(desktop): crash in sidebar FileRow when sidebarFileLinks pref is missing#4045

Merged
Kitenite merged 6 commits into
mainfrom
fix-buildhint-shift-error
May 4, 2026
Merged

fix(desktop): crash in sidebar FileRow when sidebarFileLinks pref is missing#4045
Kitenite merged 6 commits into
mainfrom
fix-buildhint-shift-error

Conversation

@Kitenite
Copy link
Copy Markdown
Collaborator

@Kitenite Kitenite commented May 4, 2026

Summary

  • Older localStorage v2 user-preference rows predate sidebarFileLinks, so reading them back returned undefined for that key
  • useSidebarFilePolicy -> usePolicy -> buildHint then crashed with Cannot read properties of undefined (reading 'shift') when rendering any sidebar FileRow
  • Fix: shallow-merge DEFAULT_V2_USER_PREFERENCES over the persisted row so missing top-level fields fall back to defaults (also guards future schema additions)

Test plan

  • Reproduce: with a localStorage v2-user-preferences-* entry that lacks sidebarFileLinks, open a v2 workspace and confirm the sidebar no longer crashes
  • Open Settings -> Links and confirm the sidebar file links section shows the default tier mapping
  • Update a tier in Settings -> Links and confirm it persists across reloads

Summary by cubic

Fixes a desktop sidebar crash in FileRow when older v2 preferences lack sidebarFileLinks. Adds read-time healing for localStorage rows so preferences and workspace local state expose safe defaults and avoid undefined chains.

  • Refactors
    • Added withReadHeal to wrap localStorageCollectionOptions from @tanstack/react-db, healing each entry’s data during parse while preserving the {versionKey, data} envelope.
    • Applied to v2_user_preferences and v2_workspace_local_state via healV2UserPreferences (deep‑merge tier maps) and healWorkspaceLocalState (fill optional arrays, merge sidebarState defaults, preserve identity fields).
    • Type-safety and tests: use the library Parser type; annotate getKey param types at call sites; added unit tests for both heal functions and the wrapper, plus an end‑to‑end test with real localStorageCollectionOptions and a regression guard.

Written for commit d3b4a57. Summary will update on new commits.

Summary by CodeRabbit

  • Bug Fixes
    • Preference and workspace persisted state now auto-repair missing or malformed fields on load, preventing undefined UI values (sidebar tiers/links, viewed/recent files, sidebar state).
  • New Features
    • Read-time healing applied to local persisted collections so the app sees healed defaults without mutating stored JSON.
  • Tests
    • Added unit and integration tests covering healing behavior and regression protection.

…ed fields

Existing localStorage rows predate sidebarFileLinks, so reading them back
returned undefined for that key and crashed buildHint with
"Cannot read properties of undefined (reading 'shift')".
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 4, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Read-heal wrappers were added for two local collections: v2WorkspaceLocalState and v2UserPreferences. New healer functions (healWorkspaceLocalState, healV2UserPreferences) synthesize missing optional fields and deep-merge defaults; a generic withReadHeal parser wrapper applies healers on read. Collections were wired to use the wrapper; no public APIs changed.

Changes

Local Read-Healing for Workspace & Preferences

Layer / File(s) Summary
Healing Defaults & Helpers
apps/desktop/src/renderer/routes/_authenticated/providers/CollectionsProvider/dashboardSidebarLocal/schema.ts
Added export function healWorkspaceLocalState(raw: unknown): WorkspaceLocalStateRow and export function healV2UserPreferences(raw: unknown): V2UserPreferencesRow. Introduces SIDEBAR_STATE_DEFAULTS and WORKSPACE_LOCAL_STATE_OPTIONAL_DEFAULTS and implements coercion of non-objects plus deep/merged defaults for nested maps and optional arrays.
Generic Parser Wrapper
apps/desktop/src/renderer/routes/_authenticated/providers/CollectionsProvider/withReadHeal.ts
Added withReadHeal<T>(options: T, heal: (raw: unknown) => unknown): T which wraps a collection parser: delegates to base parser, detects object-stored envelope entries ({ versionKey, data }), replaces each data with heal(data) while preserving envelopes, returns non-object/array parsed values unchanged, and delegates stringify to the base parser.
Collection Integration / Wiring
apps/desktop/src/renderer/routes/_authenticated/providers/CollectionsProvider/collections.ts
Updated v2WorkspaceLocalState and v2UserPreferences local collection options to use localStorageCollectionOptions(withReadHeal({...}, healWorkspaceLocalState)) and localStorageCollectionOptions(withReadHeal({...}, healV2UserPreferences)) respectively; existing keys, schemas, and getKey logic preserved.
Tests (unit & E2E)
apps/desktop/src/renderer/routes/_authenticated/providers/CollectionsProvider/dashboardSidebarLocal/schema.test.ts, .../withReadHeal.test.ts
Added tests for healV2UserPreferences and healWorkspaceLocalState (defaults, historical/stale shapes, non-object inputs) and for withReadHeal (parser unit tests and E2E using in-memory storage): healing applies only to envelope data, healed reads do not mutate stored JSON, and regression tests show unwrapped reads remain unhealed.

Sequence Diagram

sequenceDiagram
    participant LS as Local Storage
    participant Parser as Parser (withReadHeal)
    participant Heal as Healer
    participant Consumer as Collection Consumer

    Consumer->>LS: request stored collection JSON
    LS-->>Parser: raw JSON string
    Parser->>Parser: baseParser.parse(raw)
    Parser->>Parser: detect object entries and envelopes
    Parser->>Heal: heal(entry.data)
    Heal->>Heal: merge with defaults / synthesize missing fields
    Heal-->>Parser: healed data
    Parser-->>Consumer: envelope with healed data
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐇 I hop through keys and mended seams,
Filling gaps with gentle beams.
Defaults tucked where data frays,
Healed rows wake to brighter days. 🥕

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 40.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The PR title accurately describes the main change: fixing a crash in sidebar FileRow when the sidebarFileLinks preference is missing from older localStorage entries.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description check ✅ Passed Pull request includes comprehensive description with summary, test plan, related issue context, and technical implementation details sufficient for review.

✏️ 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 fix-buildhint-shift-error

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.

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented May 4, 2026

Greptile Summary

This PR fixes a crash in the sidebar FileRow caused by sidebarFileLinks being undefined in older localStorage v2 preference rows that predate the field. The fix applies a shallow merge of DEFAULT_V2_USER_PREFERENCES over the persisted row, so any missing top-level fields fall back to defaults.

Confidence Score: 4/5

Safe to merge; fix is correct and well-scoped for the reported crash.

Single targeted change with a clear fix for a real crash. One P2 observation about shallow merge not covering nested schema evolution, but that is a future concern and does not affect the current bug fix.

No files require special attention beyond the single changed file.

Important Files Changed

Filename Overview
apps/desktop/src/renderer/hooks/useV2UserPreferences/useV2UserPreferences.ts Adds shallow default-merge for old persisted rows missing top-level fields (e.g. sidebarFileLinks); fixes the crash. Shallow merge is sufficient for the current schema but won't guard new keys added inside nested LinkTierMap objects.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[useLiveQuery rows] --> B{rows zero exists?}
    B -- No --> C[Use DEFAULT_V2_USER_PREFERENCES]
    B -- Yes --> D[Shallow merge defaults then rows zero]
    D --> E{Top-level field missing?}
    E -- Missing in rows zero --> F[Default value retained]
    E -- Present in rows zero --> G[Persisted value used]
    C --> H[preferences returned to consumers]
    F --> H
    G --> H
    H --> I[usePolicy and buildHint and FileRow render]
Loading
Prompt To Fix All With AI
Fix the following 1 code review issue. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 1
apps/desktop/src/renderer/hooks/useV2UserPreferences/useV2UserPreferences.ts:38-40
**Shallow merge doesn't guard nested schema additions**

The comment says this "guards future schema additions," but that only holds for top-level fields. If a new key is later added inside `linkTierMapSchema` (e.g. a new modifier tier), old rows will have `sidebarFileLinks: { plain, shift, meta, metaShift }` already present, so `...rows[0]` keeps that entire stale object and the new key will be `undefined` inside it — the same crash pattern would recur one level deeper.

Consider a recursive merge for `LinkTierMap` objects, or apply the Zod schema with its `.default()` values at deserialization time so both levels are covered automatically.

Reviews (1): Last reviewed commit: "fix(desktop): merge persisted v2 prefs o..." | Re-trigger Greptile

Comment on lines +38 to +40
const preferences: V2UserPreferencesRow = rows[0]
? { ...DEFAULT_V2_USER_PREFERENCES, ...rows[0] }
: DEFAULT_V2_USER_PREFERENCES;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Shallow merge doesn't guard nested schema additions

The comment says this "guards future schema additions," but that only holds for top-level fields. If a new key is later added inside linkTierMapSchema (e.g. a new modifier tier), old rows will have sidebarFileLinks: { plain, shift, meta, metaShift } already present, so ...rows[0] keeps that entire stale object and the new key will be undefined inside it — the same crash pattern would recur one level deeper.

Consider a recursive merge for LinkTierMap objects, or apply the Zod schema with its .default() values at deserialization time so both levels are covered automatically.

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/desktop/src/renderer/hooks/useV2UserPreferences/useV2UserPreferences.ts
Line: 38-40

Comment:
**Shallow merge doesn't guard nested schema additions**

The comment says this "guards future schema additions," but that only holds for top-level fields. If a new key is later added inside `linkTierMapSchema` (e.g. a new modifier tier), old rows will have `sidebarFileLinks: { plain, shift, meta, metaShift }` already present, so `...rows[0]` keeps that entire stale object and the new key will be `undefined` inside it — the same crash pattern would recur one level deeper.

Consider a recursive merge for `LinkTierMap` objects, or apply the Zod schema with its `.default()` values at deserialization time so both levels are covered automatically.

How can I resolve this? If you propose a fix, please make it concise.

Top-level shallow merge only healed missing fileLinks / urlLinks /
sidebarFileLinks entirely. A tier added inside an existing map (e.g. a
new modifier later) would still come back undefined and crash buildHint
the same way. Merge each tier map over its default too.
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
apps/desktop/src/renderer/hooks/useV2UserPreferences/useV2UserPreferences.ts (1)

42-59: ⚡ Quick win

preferences is not memoized — new object reference on every render

The merged preferences object is reconstructed on every call to useV2UserPreferences, not just when rows changes. Any consumer that passes preferences (or a sub-property) into a useCallback/useEffect/useMemo dependency array will re-subscribe on every render, even when the data hasn't changed. The old code (rows[0] ?? DEFAULT_V2_USER_PREFERENCES) returned a stable reference from TanStack DB or the static constant; the new code always produces a fresh object.

♻️ Proposed fix: wrap in `useMemo`
-	const stored = rows[0];
-	const preferences: V2UserPreferencesRow = stored
-		? {
-				...DEFAULT_V2_USER_PREFERENCES,
-				...stored,
-				fileLinks: {
-					...DEFAULT_V2_USER_PREFERENCES.fileLinks,
-					...stored.fileLinks,
-				},
-				urlLinks: {
-					...DEFAULT_V2_USER_PREFERENCES.urlLinks,
-					...stored.urlLinks,
-				},
-				sidebarFileLinks: {
-					...DEFAULT_V2_USER_PREFERENCES.sidebarFileLinks,
-					...stored.sidebarFileLinks,
-				},
-			}
-		: DEFAULT_V2_USER_PREFERENCES;
+	const preferences: V2UserPreferencesRow = useMemo(() => {
+		const stored = rows[0];
+		if (!stored) return DEFAULT_V2_USER_PREFERENCES;
+		return {
+			...DEFAULT_V2_USER_PREFERENCES,
+			...stored,
+			fileLinks: {
+				...DEFAULT_V2_USER_PREFERENCES.fileLinks,
+				...stored.fileLinks,
+			},
+			urlLinks: {
+				...DEFAULT_V2_USER_PREFERENCES.urlLinks,
+				...stored.urlLinks,
+			},
+			sidebarFileLinks: {
+				...DEFAULT_V2_USER_PREFERENCES.sidebarFileLinks,
+				...stored.sidebarFileLinks,
+			},
+		};
+	}, [rows]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/renderer/hooks/useV2UserPreferences/useV2UserPreferences.ts`
around lines 42 - 59, The merged preferences object in useV2UserPreferences
(variable preferences) is recreated every render; wrap the merge in a useMemo so
the object identity only changes when the source data changes (e.g. stored or
rows). Import React's useMemo if not present and replace the direct assignment
to preferences with: const preferences = useMemo(() => stored ? {
...DEFAULT_V2_USER_PREFERENCES, ...stored, fileLinks: {
...DEFAULT_V2_USER_PREFERENCES.fileLinks, ...stored.fileLinks }, urlLinks: {
...DEFAULT_V2_USER_PREFERENCES.urlLinks, ...stored.urlLinks }, sidebarFileLinks:
{ ...DEFAULT_V2_USER_PREFERENCES.sidebarFileLinks, ...stored.sidebarFileLinks }
} : DEFAULT_V2_USER_PREFERENCES, [stored]); ensuring the dependency correctly
tracks the source (stored or rows[0]) so consumers relying on preferences
identity won't re-run unnecessarily.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In
`@apps/desktop/src/renderer/hooks/useV2UserPreferences/useV2UserPreferences.ts`:
- Around line 42-59: The merged preferences object in useV2UserPreferences
(variable preferences) is recreated every render; wrap the merge in a useMemo so
the object identity only changes when the source data changes (e.g. stored or
rows). Import React's useMemo if not present and replace the direct assignment
to preferences with: const preferences = useMemo(() => stored ? {
...DEFAULT_V2_USER_PREFERENCES, ...stored, fileLinks: {
...DEFAULT_V2_USER_PREFERENCES.fileLinks, ...stored.fileLinks }, urlLinks: {
...DEFAULT_V2_USER_PREFERENCES.urlLinks, ...stored.urlLinks }, sidebarFileLinks:
{ ...DEFAULT_V2_USER_PREFERENCES.sidebarFileLinks, ...stored.sidebarFileLinks }
} : DEFAULT_V2_USER_PREFERENCES, [stored]); ensuring the dependency correctly
tracks the source (stored or rows[0]) so consumers relying on preferences
identity won't re-run unnecessarily.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 4a29168c-9ee9-46d1-92cd-1ddd7c9472da

📥 Commits

Reviewing files that changed from the base of the PR and between 3b5e156 and eaf7cd3.

📒 Files selected for processing (1)
  • apps/desktop/src/renderer/hooks/useV2UserPreferences/useV2UserPreferences.ts

…el merge

The previous fix patched useV2UserPreferences directly. The underlying
issue is that @tanstack/db's localStorageCollectionOptions only validates
the schema on writes — reads return whatever was last persisted, so any
collection here is one schema addition away from the same crash class.

Move the fix to the collection layer:
  - withReadHeal wraps localStorageCollectionOptions and installs a
    custom parser whose .parse runs each entry's data through a heal fn,
    preserving the {versionKey, data} envelope the library expects.
  - healV2UserPreferences lives next to DEFAULT_V2_USER_PREFERENCES in
    schema.ts and deep-merges stored rows over per-tier-map defaults
    (Zod alone can't cover this since per-tier defaults vary by map).
  - useV2UserPreferences goes back to a plain rows[0] ?? defaults read.

The wrapper is reusable for the other localStorage-backed collections
when they hit the same evolution problem.
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
apps/desktop/src/renderer/routes/_authenticated/providers/CollectionsProvider/withReadHeal.ts (1)

20-54: 💤 Low value

LGTM — approach is sound and the key library assumption is confirmed.

The parser option is a real, exported config field in @tanstack/react-db's localStorageCollectionOptions (confirmed in the library source at packages/db/src/local-storage.ts). The heal is cleanly read-only: writes go through baseParser.stringify unchanged, so the stored format is never altered until a normal mutation rewrites the healed shape.

One optional nit: the Parser interface is publicly documented by TanStack DB and exported from the library. You could import it instead of re-declaring locally to avoid potential drift, though the current definition is structurally identical.

♻️ Optional: import Parser from the library
-interface Parser {
-	parse: (raw: string) => unknown;
-	stringify: (value: unknown) => string;
-}
+import type { Parser } from "@tanstack/react-db";
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/desktop/src/renderer/routes/_authenticated/providers/CollectionsProvider/withReadHeal.ts`
around lines 20 - 54, Replace the locally redeclared Parser type with the
library's exported Parser: import the Parser interface from `@tanstack/react-db`
and use it in withReadHeal (affecting the baseParser and healingParser
declarations) so the function references the canonical Parser type (remove the
local Parser declaration to avoid drift).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In
`@apps/desktop/src/renderer/routes/_authenticated/providers/CollectionsProvider/withReadHeal.ts`:
- Around line 20-54: Replace the locally redeclared Parser type with the
library's exported Parser: import the Parser interface from `@tanstack/react-db`
and use it in withReadHeal (affecting the baseParser and healingParser
declarations) so the function references the canonical Parser type (remove the
local Parser declaration to avoid drift).

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: dec3bd22-caf4-48ab-ba78-d9f0d1c1d75a

📥 Commits

Reviewing files that changed from the base of the PR and between eaf7cd3 and 6dedd4b.

📒 Files selected for processing (3)
  • apps/desktop/src/renderer/routes/_authenticated/providers/CollectionsProvider/collections.ts
  • apps/desktop/src/renderer/routes/_authenticated/providers/CollectionsProvider/dashboardSidebarLocal/schema.ts
  • apps/desktop/src/renderer/routes/_authenticated/providers/CollectionsProvider/withReadHeal.ts

…Options

- healV2UserPreferences: empty/partial inputs, the original missing-
  sidebarFileLinks crash shape, and a missing-tier-inside-present-map case.
- withReadHeal parser: heals each entry's data while preserving the
  {versionKey, data} envelope; passes non-envelope values through.
- End-to-end through real createCollection + localStorageCollectionOptions
  with a Map-backed storage. Pre-populates storage with the buggy shape,
  asserts the collection exposes the healed row. Includes a regression
  guard that pins library behavior without the wrapper (sidebarFileLinks
  comes back undefined) so we know when the workaround is no longer needed.

Each test was verified to fail under a targeted impl mutation.
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@apps/desktop/src/renderer/routes/_authenticated/providers/CollectionsProvider/withReadHeal.test.ts`:
- Around line 38-49: withReadHeal({}) infers T = {} so opts is typed {} and
accessing opts.parser causes a TS2339; fix by casting the returned opts to the
correct extended interface that includes parser (i.e. the same cast used in the
first test) or by supplying an explicit generic to withReadHeal to preserve the
expected return type; apply that same cast/fix to the opts variable in the
second unit test so opts.parser is correctly typed.
🪄 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

Run ID: 3a7d8afa-ef02-436b-8b86-1a9b286a1545

📥 Commits

Reviewing files that changed from the base of the PR and between 6dedd4b and 24498fe.

📒 Files selected for processing (2)
  • apps/desktop/src/renderer/routes/_authenticated/providers/CollectionsProvider/dashboardSidebarLocal/schema.test.ts
  • apps/desktop/src/renderer/routes/_authenticated/providers/CollectionsProvider/withReadHeal.test.ts

Highest-risk localStorage collection: deeply nested shape (sidebarState
with discriminated unions, paneLayout, arrays of viewed files) and the
most consumers chaining into nested fields. One added field is one
undefined-property-chain away from the buildHint crash class.

healWorkspaceLocalState:
  - Preserves identity fields (workspaceId, projectId, paneLayout,
    createdAt) from the stored row — heal can't synthesize them.
  - Fills optional top-level defaults (viewedFiles, recentlyViewedFiles)
    via ?? so explicit null/undefined also fall back.
  - Deep-merges sidebarState against per-field defaults pulled next to
    the schema (SIDEBAR_STATE_DEFAULTS).
  - Never throws on null/non-object input — a parser throw silently
    zeros the entire collection.

Tests: 4 unit tests + an end-to-end through the real library with a
pre-evolution shape. Verified the nested-fill test fails when
SIDEBAR_STATE_DEFAULTS is dropped from the merge.
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

♻️ Duplicate comments (1)
apps/desktop/src/renderer/routes/_authenticated/providers/CollectionsProvider/withReadHeal.test.ts (1)

37-62: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

opts.parser causes TS2339 in both unit tests — typecheck CI will fail.

withReadHeal({}, heal) infers T = {}, so opts is typed as {}. TypeScript's optional chaining ?. does not suppress the TS2339 that fires at the property access step — TypeScript errors before the chain is evaluated. The same issue exists at line 58.

The fix is to cast the return value to a type that includes parser, or to use an explicit type argument:

🛠️ Proposed fix (lines 40 and 58)
-		const opts = withReadHeal({}, heal);
+		const opts = withReadHeal({}, heal) as {
+			parser?: { parse: (s: string) => unknown };
+		};

Apply the same change at line 58.

Alternatively, if withReadHeal's return type were widened to T & { parser: { parse: ... } } instead of just T, both the tests and the call sites in collections.ts would benefit from more accurate types without any casts here.

Run the following to confirm the current withReadHeal.ts signature hasn't changed:

#!/bin/bash
# Verify the return type of withReadHeal to confirm the TS2339 concern.
fd -t f "withReadHeal.ts" apps/desktop/src --exclude "*.test.ts" | xargs cat -n
🤖 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
`@apps/desktop/src/renderer/routes/_authenticated/providers/CollectionsProvider/withReadHeal.test.ts`
around lines 37 - 62, The tests fail TS2339 because withReadHeal({}, heal)
infers a bare {} so accessing opts.parser triggers a type error; fix by giving
withReadHeal an explicit return type or casting its result to include parser
(e.g. cast the call to a type with parser: { parse: (s: string) => unknown }) or
call withReadHeal with an explicit generic type that includes parser before
using opts.parser in both test cases (the calls that produce opts and the
subsequent opts.parser?.parse usages around the two parser checks).
🤖 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.

Duplicate comments:
In
`@apps/desktop/src/renderer/routes/_authenticated/providers/CollectionsProvider/withReadHeal.test.ts`:
- Around line 37-62: The tests fail TS2339 because withReadHeal({}, heal) infers
a bare {} so accessing opts.parser triggers a type error; fix by giving
withReadHeal an explicit return type or casting its result to include parser
(e.g. cast the call to a type with parser: { parse: (s: string) => unknown }) or
call withReadHeal with an explicit generic type that includes parser before
using opts.parser in both test cases (the calls that produce opts and the
subsequent opts.parser?.parse usages around the two parser checks).

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: e6675f0a-9882-4b58-adbf-042db52ab072

📥 Commits

Reviewing files that changed from the base of the PR and between 24498fe and f780542.

📒 Files selected for processing (4)
  • apps/desktop/src/renderer/routes/_authenticated/providers/CollectionsProvider/collections.ts
  • apps/desktop/src/renderer/routes/_authenticated/providers/CollectionsProvider/dashboardSidebarLocal/schema.test.ts
  • apps/desktop/src/renderer/routes/_authenticated/providers/CollectionsProvider/dashboardSidebarLocal/schema.ts
  • apps/desktop/src/renderer/routes/_authenticated/providers/CollectionsProvider/withReadHeal.test.ts

Address PR review feedback:
- Use the library-exported `Parser` type in `withReadHeal` instead of
  redeclaring locally (avoids future drift).
- Annotate `getKey` parameter types at the four `withReadHeal` call sites
  (collections.ts and the e2e tests) so the wrapper's passthrough generic
  preserves the schema/row-type linkage that downstream library overloads
  need to typecheck.
- Loosen the parser unit-test casts to a permissive `data` shape so
  bun:test's strict toEqual accepts the {a, healed} literal.
- Cast `paneLayout` reference compare via `Object.is` to satisfy bun:test's
  strict toBe types (the test fixture is a narrow stub of `WorkspaceState`).

CodeRabbit caught these — earlier filtered tsc grep had missed them.
Tests still pass; mutations still catch each regression.
@Kitenite Kitenite merged commit d745213 into main May 4, 2026
8 checks passed
@Kitenite Kitenite deleted the fix-buildhint-shift-error branch May 4, 2026 21:11
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 4, 2026

🧹 Preview Cleanup Complete

The following preview resources have been cleaned up:

  • ⚠️ Neon database branch

Thank you for your contribution! 🎉

MocA-Love pushed a commit to MocA-Love/superset that referenced this pull request May 21, 2026
…missing (superset-sh#4045)

* fix(desktop): merge persisted v2 prefs over defaults to avoid undefined fields

Existing localStorage rows predate sidebarFileLinks, so reading them back
returned undefined for that key and crashed buildHint with
"Cannot read properties of undefined (reading 'shift')".

* fix(desktop): also merge nested LinkTierMap defaults

Top-level shallow merge only healed missing fileLinks / urlLinks /
sidebarFileLinks entirely. A tier added inside an existing map (e.g. a
new modifier later) would still come back undefined and crash buildHint
the same way. Merge each tier map over its default too.

* refactor(desktop): heal localStorage rows at read time, drop hook-level merge

The previous fix patched useV2UserPreferences directly. The underlying
issue is that @tanstack/db's localStorageCollectionOptions only validates
the schema on writes — reads return whatever was last persisted, so any
collection here is one schema addition away from the same crash class.

Move the fix to the collection layer:
  - withReadHeal wraps localStorageCollectionOptions and installs a
    custom parser whose .parse runs each entry's data through a heal fn,
    preserving the {versionKey, data} envelope the library expects.
  - healV2UserPreferences lives next to DEFAULT_V2_USER_PREFERENCES in
    schema.ts and deep-merges stored rows over per-tier-map defaults
    (Zod alone can't cover this since per-tier defaults vary by map).
  - useV2UserPreferences goes back to a plain rows[0] ?? defaults read.

The wrapper is reusable for the other localStorage-backed collections
when they hit the same evolution problem.

* test(desktop): cover heal-on-read against real localStorageCollectionOptions

- healV2UserPreferences: empty/partial inputs, the original missing-
  sidebarFileLinks crash shape, and a missing-tier-inside-present-map case.
- withReadHeal parser: heals each entry's data while preserving the
  {versionKey, data} envelope; passes non-envelope values through.
- End-to-end through real createCollection + localStorageCollectionOptions
  with a Map-backed storage. Pre-populates storage with the buggy shape,
  asserts the collection exposes the healed row. Includes a regression
  guard that pins library behavior without the wrapper (sidebarFileLinks
  comes back undefined) so we know when the workaround is no longer needed.

Each test was verified to fail under a targeted impl mutation.

* fix(desktop): apply withReadHeal to v2WorkspaceLocalState preemptively

Highest-risk localStorage collection: deeply nested shape (sidebarState
with discriminated unions, paneLayout, arrays of viewed files) and the
most consumers chaining into nested fields. One added field is one
undefined-property-chain away from the buildHint crash class.

healWorkspaceLocalState:
  - Preserves identity fields (workspaceId, projectId, paneLayout,
    createdAt) from the stored row — heal can't synthesize them.
  - Fills optional top-level defaults (viewedFiles, recentlyViewedFiles)
    via ?? so explicit null/undefined also fall back.
  - Deep-merges sidebarState against per-field defaults pulled next to
    the schema (SIDEBAR_STATE_DEFAULTS).
  - Never throws on null/non-object input — a parser throw silently
    zeros the entire collection.

Tests: 4 unit tests + an end-to-end through the real library with a
pre-evolution shape. Verified the nested-fill test fails when
SIDEBAR_STATE_DEFAULTS is dropped from the merge.

* fix(desktop): typecheck cleanup for heal wrapper

Address PR review feedback:
- Use the library-exported `Parser` type in `withReadHeal` instead of
  redeclaring locally (avoids future drift).
- Annotate `getKey` parameter types at the four `withReadHeal` call sites
  (collections.ts and the e2e tests) so the wrapper's passthrough generic
  preserves the schema/row-type linkage that downstream library overloads
  need to typecheck.
- Loosen the parser unit-test casts to a permissive `data` shape so
  bun:test's strict toEqual accepts the {a, healed} literal.
- Cast `paneLayout` reference compare via `Object.is` to satisfy bun:test's
  strict toBe types (the test fixture is a narrow stub of `WorkspaceState`).

CodeRabbit caught these — earlier filtered tsc grep had missed them.
Tests still pass; mutations still catch each regression.
MocA-Love added a commit to MocA-Love/superset that referenced this pull request May 21, 2026
superset-sh#4037 NewProjectModal: drop fork-only OPTIONS multi-mode selector and adopt
upstream v2-only simplification (toast errors + Label + spinner). The
'empty'/'template' modes were 'coming soon' disabled entries with no
implementation behind them; no user-facing fork feature lost.

superset-sh#4191 DashboardSidebarDeleteDialog: keep fork's 'Checking…' confirm-button
label UX while incorporating upstream's fixed-height warning banner slot.
Restore isCheckingStatus destructure + prop pass; add to
DestroyConfirmPane interface so the deleting-button disabled state still
gates on it.

superset-sh#4045 schema.ts: backfill sidebarFileLinks field into v2UserPreferencesSchema
and DEFAULT_V2_USER_PREFERENCES (upstream introduced these in superset-sh#3988 which
fork will adopt in Phase 5). Also add 'shift' tier to linkTierMapSchema
(matches superset-sh#3988 LinkTier expansion) so superset-sh#4045's added schema tests pass.

superset-sh#4212 tray Quit Completely: replace upstream quitAppCompletely() with
fork's existing requestQuit('stop') which already performs full host-
service + pty-daemon teardown via prepareQuit('stop') + getTodoScheduler
stop + cleanupMainWindowResources. Tray menu entry retained.

superset-sh#4225 automations/page.tsx: drop iconUrl prop from ProjectThumbnail call
(fork ProjectThumbnail uses githubOwner, iconUrl arrives with superset-sh#3823 v2
project icon settings in cycle 41). Pass githubOwner={null} as transient
fallback.

superset-sh#4226 useBranchPickerController: fork's useWorkspaceCreates.submit returns
Promise<void> (in-flight tracker carries error state); upstream changed
it to Promise<SubmitResult>. Wrap submit in try/catch and navigate using
snapshot.id so the picker compiles. Note: foreign-worktree adopt path
with canonical workspaceId waiting is deferred to cycle 44b when
WorkspaceCreatesManager / submit API are upgraded.
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.

1 participant