Skip to content

[Bug] File search recents are shared across browse revisions #1528

Description

@Harsh23Kashyap

[Bug] File search recents are shared across browse revisions

Problem

The browse file-search dialog (mod+p) stores recently opened files in localStorage under a key scoped only by repoName — not by the current revisionName. When a user switches branches or revisions in the same repo, recents from another revision still appear in the dialog, and selecting one navigates with the current revision, which can point to a path that does not exist there.

Background

packages/web/src/app/(app)/browse/components/fileSearchCommandDialog.tsx:38:

const [recentlyOpened, setRecentlyOpened] = useLocalStorage<FileTreeItem[]>(
    `recentlyOpenedFiles-${repoName}`,
    []
);

The component already reads revisionName from useBrowseParams() on the line above and uses it to fetch the file list (getFiles({ repoName, revisionName: revisionName ?? 'HEAD' })), so the data is already revision-scoped. The localStorage key is the only thing that isn't.

Current behavior

A user opens /browse/<repo>/main, picks src/foo.ts from the file-search dialog (recorded under recentlyOpenedFiles-<repo>), then switches to /browse/<repo>/feature/branch. The dialog still shows src/foo.ts from the previous revision, even though the file may not exist on this branch. Selecting it navigates with the new revision and lands on a 404.

Expected behavior

Recently opened files should be scoped to the (repoName, revisionName) tuple. Switching revisions gives a fresh recents list scoped to the new revision. Within a single revision, recents behave as they do today (newest first, dedup by path).

Proposed solution

Change the localStorage key from recentlyOpenedFiles-${repoName} to recentlyOpenedFiles-${repoName}-${revisionName ?? 'HEAD'} so the recents are naturally scoped per-revision. The default of 'HEAD' matches the existing fallback used in the file fetch on the next line, so the key for the "no revision in URL" case matches the key the API uses to resolve the file list.

This is a one-line change. The existing setRecentlyOpened callback does not need to change — useLocalStorage from usehooks-ts writes to whatever key the current key string resolves to. Switching revisions automatically re-renders the dialog with the new key, which starts empty (or with whatever recents the user previously stored under that exact (repo, revision) tuple).

Old keys (recentlyOpenedFiles-<repo>) become orphaned entries in localStorage and are ignored. No migration is required — the worst case is the user loses the recents accumulated under the old key, which is the bug being fixed.

Alternatives considered

  • Single key with the current shape, plus a revisionName filter on the read side. Rejected: a stale localStorage entry from a different revision would still be returned, then filtered out, but the user would see the list shrink to zero on every revision switch, which is a worse UX than starting fresh.
  • A revision-agnostic "viewed across all branches" recents list. Rejected: the user explicitly filed the issue as "recents are shared across browse revisions", which says they want per-revision scoping, not a cross-revision view.
  • Move recents to the server (Prisma userRecentFile table). Punted: a localStorage key is the lowest-cost fix and matches the existing pattern. If a user clears their browser data they lose their recents, but they also lose their session, so this is fine.

Scope

One line changed in one file. One vitest case verifying the new key format. No schema, no migration, no new dependencies.

Acceptance criteria

  • The localStorage key is recentlyOpenedFiles-${repoName}-${revisionName ?? 'HEAD'} (or the HEAD default for the no-revision case).
  • Switching revisions in the same repo yields a fresh recents list (or the recents previously stored under that exact (repo, revision) tuple).
  • The existing setRecentlyOpened callback continues to work (no API change).
  • A vitest case asserts the new key format for a sample of (repoName, revisionName) tuples.

Backward compatibility

Pure behaviour change. The old key's data is left in place but ignored, so a user upgrading from a previous version simply starts with an empty recents list on their next branch switch. No data loss on the server side.

Risks

Minimal. The default 'HEAD' matches the file-fetch default on the next line, so the recents key and the file list key agree on the "no revision in URL" case. If a future change splits 'HEAD' and undefined semantics, both sides will need to be updated in lockstep — but that's already true for the file fetch.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions