Skip to content

Commit cfcc838

Browse files
thalidaclaude
andauthored
Local repos: display the checked-out branch, drop the branch axis (#110)
* feat(source): drop the branch axis for local repos A local source scans whatever is checked out on disk, so a stored branch is a lie: it must not namespace the cache, the URL, or recents. Add identityBranch(src, branch) as the single "does this source carry a branch" rule (undefined for local, branch for remote) and route sourceKey, CURRENT_SOURCE, the deep-link URL, the fetch/overlay, and recents dedupe through it. - Recents now key + dedupe a local path by src alone, so switching its checkout no longer spawns a second row, and the row shows no @Branch identity pill. - The header branch pill still shows the live checked-out branch: it falls back to manifest.repo.branch via resolveBranch/SOURCE_INFO (display only). Backend already populates repo.branch from HEAD for every scan, so no change there. Closes #92 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(source): normalize local branch at the commit boundary only Follow-up to the local-branch-axis work: the "local has no branch" rule was being re-decided on every read (sourceKey, recents dedupe, the active match all called identityBranch(...) ?? ''). Collapse it to one boundary. - Add sourceIdentity(src, branch): the canonical identity string, one place the src\0branch join + empty-branch coercion lives. sourceKey hashes it; recents dedupe and the active match compare it. - identityBranch now applies only at the commit boundary (loadSource + setCurrentSource), so CURRENT_SOURCE and every stored recent are already branch-less for local. sourceIdentity trusts that and does a plain join — no per-read source-kind switch. - Bump the recents localStorage slot to recents.v2. Pre-v2 local entries stored a checkout branch; dropping the slot once is cheaper than migrating an MRU list, and it removes the only stale-data case the read guard existed for. - RecentsList matches active rows against CURRENT_SOURCE (the canonical applied source) instead of SOURCE_INFO (which exposes the manifest's live checkout for display). Post-v2 a recent's branch equals CURRENT_SOURCE's on both kinds, so the match is exact; a local row no longer needs the display branch normalized away. Drop the now-redundant !isLocal pill guard. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(source): share sameSourceIdentity for the active-row match The recents-dedupe predicate was private and RecentsList re-spelled the same identity comparison inline. Export it as sameSourceIdentity (it takes any two {src, branch?} refs, not just recents) and use it in the active-row match, so pushRecent, removeRecent, and isActive all go through one "are these the same source?" check. sourceIdentity (the string) still backs the React key. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(source): move pure source-identity helpers to utils/sources sourceKey (+ djb2), sourceIdentity, and sameSourceIdentity are pure functions on a source's (src, branch) identity with no signal dependency, so they belong beside their siblings identityBranch/resolveBranch in utils/sources, not in the stateful store. state/stores/source keeps the signals, the recents mutations, and the URL effect built on them. RecentsList now imports the two identity helpers from utils instead of reaching into the store for pure logic; excludes + useManifestSource import sourceKey from utils. Unit tests for the moved functions live in tests/utils/sources.test.ts (mirroring the new home), with focused sourceIdentity/sameSourceIdentity coverage added. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent 4d313cb commit cfcc838

11 files changed

Lines changed: 233 additions & 83 deletions

File tree

app/src/components/RecentsList/RecentsList.tsx

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
11
// components/RecentsList/RecentsList.tsx — recent projects: a heading and the
2-
// list. Active state derives from SOURCE_INFO (the resolved branch, matching
3-
// how recents store it — CURRENT_SOURCE keeps the raw submitted branch, which is
4-
// undefined for a local repo whose recent carries its resolved HEAD).
5-
// Remove is non-destructive: it forgets the entry only, it does not
6-
// clear the scan cache (that's the skip-cache control's job). Renders nothing
7-
// when there are no recents.
2+
// list. Active state matches each row against CURRENT_SOURCE by source identity
3+
// (the canonical applied source, not the manifest's display fields): a local
4+
// path is branch-less on both sides, so a checkout change never re-keys the row
5+
// or drops its active badge. Remove is non-destructive: it forgets the entry
6+
// only, it does not clear the scan cache (that's the skip-cache control's job).
7+
// Renders nothing when there are no recents.
88

99
import './RecentsList.css';
1010
import { useState } from 'preact/hooks';
11-
import { listRecents, removeRecent, SOURCE_INFO } from '@/state/stores/source';
11+
import { listRecents, removeRecent, CURRENT_SOURCE } from '@/state/stores/source';
1212
import { SERVER_CONFIG } from '@/state/stores/serverConfig';
13-
import { srcKind, SourceKind } from '@/utils/sources';
13+
import { srcKind, SourceKind, sourceIdentity, sameSourceIdentity } from '@/utils/sources';
1414
import type { SourcePayload } from '@/state/stores/ui';
1515
import { RecentRow } from './RecentRow';
1616

@@ -20,13 +20,12 @@ export interface RecentsListProps {
2020

2121
export function RecentsList({ onOpen }: RecentsListProps) {
2222
const recents = listRecents(); // reads RECENTS signal
23-
const si = SOURCE_INFO.value;
23+
const cur = CURRENT_SOURCE.value;
2424
const allowLocal = SERVER_CONFIG.value.allowLocalRepos;
2525
const [confirming, setConfirming] = useState<string | null>(null); // key of row
2626

27-
const keyOf = (r: { src: string; branch?: string }) => `${r.src}:${r.branch ?? ''}`;
28-
const isActive = (r: { src: string; branch?: string }) =>
29-
!!si.src && r.src === si.src && (r.branch ?? '') === (si.branch ?? '');
27+
const keyOf = (r: { src: string; branch?: string }) => sourceIdentity(r.src, r.branch);
28+
const isActive = (r: { src: string; branch?: string }) => !!cur && sameSourceIdentity(r, cur);
3029
// A local recent while local repos are off can't load; still clickable (the
3130
// server error explains why), just flagged with a hint glyph.
3231
const isUnavailable = (r: { src: string }) => srcKind(r.src) === SourceKind.Local && !allowLocal;

app/src/constants/storage.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,11 @@ export const STORAGE_PREFIX = 'cc.';
1313
* e.g. `cc.recents`. All persisted UI/session state flows through here; there
1414
* is no separate raw-localStorage key table. */
1515
export const PERSISTED_KEYS = {
16-
/** Recently-opened sources list (source-picker MRU). */
17-
RECENTS: 'recents',
16+
/** Recently-opened sources list (source-picker MRU). The `.v2` bump resets the
17+
* slot once: pre-v2 local entries stored a `branch` (their checkout), which a
18+
* local source no longer carries — dropping the stale slot is cheaper than
19+
* migrating an MRU convenience list. */
20+
RECENTS: 'recents.v2',
1821
/** Left (tree/info/controls) sidebar drag-handle width in px. */
1922
LEFT_SIDEBAR_WIDTH: 'leftSidebarWidth',
2023
/** Right (file/commit/street pane) sidebar drag-handle width in px. */

app/src/hooks/useManifestSource.ts

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,7 @@ import { SERVER_CONFIG } from '@/state/stores/serverConfig';
3939
import { MANIFEST, setManifest, markError } from '@/state/stores/manifest';
4040
import { SCAN_PROGRESS } from '@/state/stores/scanProgress';
4141
import { activeExcludePathsFor, ACTIVE_EXCLUDES } from '@/state/stores/excludes';
42-
import { sourceKey } from '@/state/stores/source';
43-
import { srcKind, SourceKind, srcNeedsBranch } from '@/utils/sources';
42+
import { srcKind, SourceKind, srcNeedsBranch, identityBranch, sourceKey } from '@/utils/sources';
4443
import { isEmptyManifest } from '@/utils/manifest';
4544
import { URL_PARAMS } from '@/constants/urlParams';
4645
import type { Manifest } from '@/types';
@@ -134,9 +133,13 @@ export async function loadSource(payload: SourcePayload): Promise<void> {
134133
loadController?.abort(); // supersede any in-flight load
135134
const controller = new AbortController();
136135
loadController = controller;
136+
// A local source has no branch axis — drop any branch (a stale deep-link or a
137+
// legacy recent could carry one) so the fetch URL, overlay, committed source,
138+
// and prefill all stay branch-less. The checked-out branch is display-only.
139+
const branch = identityBranch(payload.src, payload.branch);
137140
const meta = {
138141
kind: srcKind(payload.src),
139-
branch: payload.branch,
142+
branch,
140143
};
141144
SCAN_PROGRESS.value = { ...meta, phase: null }; // show overlay immediately
142145
// Snapshot the applied manifest so a cancel that lands after a skeleton was
@@ -147,7 +150,7 @@ export async function loadSource(payload: SourcePayload): Promise<void> {
147150
try {
148151
const url = manifestUrlFor({
149152
src: payload.src,
150-
branch: payload.branch,
153+
branch,
151154
noCache: !!payload.skipCache,
152155
exclude: activeExcludePathsFor(payload.src),
153156
});
@@ -174,7 +177,7 @@ export async function loadSource(payload: SourcePayload): Promise<void> {
174177
// camera-reframe reaction keys off CURRENT_SOURCE captured at apply-START, so
175178
// the new key must be live for the FINAL apply (the one to frame on) and NOT
176179
// for the preceding skeleton apply (which must not reframe).
177-
setCurrentSource(payload.src, payload.branch, manifest);
180+
setCurrentSource(payload.src, branch, manifest);
178181
setManifest(manifest);
179182
} catch (err) {
180183
if (myGen !== loadGeneration) return; // superseded — its error isn't current
@@ -184,7 +187,7 @@ export async function loadSource(payload: SourcePayload): Promise<void> {
184187
}
185188
SOURCE_ERROR.value = {
186189
error: err instanceof Error ? err.message : String(err),
187-
prefill: { src: payload.src, branch: payload.branch },
190+
prefill: { src: payload.src, branch },
188191
};
189192
} finally {
190193
// Only the authoritative load tears down the overlay; a superseded one must

app/src/state/stores/excludes.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@
88
import { computed, type ReadonlySignal } from '@preact/signals';
99
import { persistedSignal } from '@/state/persist';
1010
import { PERSISTED_KEYS } from '@/constants/storage';
11-
import { CURRENT_SOURCE, sourceKey } from '@/state/stores/source';
11+
import { CURRENT_SOURCE } from '@/state/stores/source';
12+
import { sourceKey } from '@/utils/sources';
1213

1314
/** repo key -> sorted, de-duped rel-paths. One localStorage slot for all repos.
1415
* Whole-object persistence: keys are runtime repo hashes, not in the default,

app/src/state/stores/source.ts

Lines changed: 25 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
// state/stores/source.ts — Everything about *which source is loaded*: the
2-
// current source's stable key + display info, the per-(src,branch) hash used to
3-
// namespace per-source localStorage slots, and the recently-opened list.
2+
// current source's stable key + display info and the recently-opened list. The
3+
// pure source-identity helpers (sourceKey/sourceIdentity/sameSourceIdentity)
4+
// live in utils/sources; this module owns the signals and persistence built on
5+
// them.
46
//
57
// CURRENT_SOURCE is session-scoped (set on every successful source apply);
68
// CURRENT_SOURCE_KEY + SOURCE_INFO derive from it (the latter also from
@@ -14,31 +16,17 @@ import { PERSISTED_KEYS } from '@/constants/storage';
1416
import { MAX_RECENT_SOURCES } from '@/constants/ui';
1517
import { URL_PARAMS } from '@/constants/urlParams';
1618
import { MANIFEST } from '@/state/stores/manifest';
17-
import { srcKind, SourceKind, resolveBranch } from '@/utils/sources';
19+
import {
20+
srcKind,
21+
SourceKind,
22+
resolveBranch,
23+
identityBranch,
24+
sourceKey,
25+
sameSourceIdentity,
26+
} from '@/utils/sources';
1827
import { isEmptyManifest } from '@/utils/manifest';
1928
import type { Manifest } from '@/types';
2029

21-
// ── sourceKey: stable short hash of (src, branch) ────────────────────
22-
23-
function djb2(s: string): string {
24-
let h = 5381;
25-
for (let i = 0; i < s.length; i++) {
26-
h = ((h << 5) + h + s.charCodeAt(i)) | 0;
27-
}
28-
return (h >>> 0).toString(36); // unsigned, base-36 — ~6-7 chars
29-
}
30-
31-
/**
32-
* Compute a short stable hash for a (src, branch) pair. Used to namespace
33-
* per-source state (selection, camera pose) in localStorage.
34-
*
35-
* The hash distinguishes (src, undefined) from (src, ""), but in practice
36-
* we treat empty-string branch as "no branch" — callers should pass undefined.
37-
*/
38-
export function sourceKey(src: string, branch?: string): string {
39-
return djb2(`${src}\0${branch ?? ''}`);
40-
}
41-
4230
// ── Currently-loaded source ──────────────────────────────────────────
4331

4432
/** The applied source ({src, branch}) or null when nothing is loaded
@@ -131,15 +119,13 @@ export function listRecents(): RecentSource[] {
131119
}
132120

133121
/**
134-
* Push (or update) an entry. Dedupes by (src, branch ?? ''). The pushed
135-
* entry becomes the most-recent. List is capped at MAX_RECENT_SOURCES
136-
* entries (oldest dropped).
122+
* Push (or update) an entry. Dedupes by source identity (src, plus branch for a
123+
* remote — a local path is one row regardless of checkout). The pushed entry
124+
* becomes the most-recent. List is capped at MAX_RECENT_SOURCES (oldest dropped).
137125
*/
138126
export function pushRecent(entry: Omit<RecentSource, 'lastOpenedAt'>): void {
139127
const now = Date.now();
140-
const filtered = RECENTS.value.filter(
141-
(r) => !(r.src === entry.src && (r.branch ?? '') === (entry.branch ?? ''))
142-
);
128+
const filtered = RECENTS.value.filter((r) => !sameSourceIdentity(r, entry));
143129
filtered.unshift({ ...entry, lastOpenedAt: now });
144130
RECENTS.value = filtered.slice(0, MAX_RECENT_SOURCES);
145131
}
@@ -155,20 +141,23 @@ export function setCurrentSource(
155141
branch: string | undefined,
156142
manifest: Manifest
157143
): void {
158-
CURRENT_SOURCE.value = { src, branch };
144+
// A local source carries no branch (identityBranch): its checkout is dynamic,
145+
// so CURRENT_SOURCE, the URL, the cache key, and the recent all omit it. The
146+
// checked-out branch is still shown in the header via SOURCE_INFO, which reads
147+
// it from the manifest — display only, not identity.
148+
const idBranch = identityBranch(src, branch);
149+
CURRENT_SOURCE.value = { src, branch: idBranch };
159150
pushRecent({
160151
src,
161152
// The server bakes the canonical owner/repo name into tree.name (a local
162153
// worktree's src basename would be the folder name, not the repo); keep the
163154
// raw src only as a defensive fallback.
164155
label: manifest.tree?.name || src,
165-
branch: resolveBranch(manifest, branch),
156+
branch: identityBranch(src, resolveBranch(manifest, branch)),
166157
});
167158
}
168159

169-
/** Drop the entry matching (src, branch). No-op if not present. */
160+
/** Drop the entry matching the given source identity. No-op if not present. */
170161
export function removeRecent(src: string, branch?: string): void {
171-
RECENTS.value = RECENTS.value.filter(
172-
(r) => !(r.src === src && (r.branch ?? '') === (branch ?? ''))
173-
);
162+
RECENTS.value = RECENTS.value.filter((r) => !sameSourceIdentity(r, { src, branch }));
174163
}

app/src/utils/sources.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,61 @@ export function resolveBranch(
3838
return requested ?? (looksReal ? mb : undefined);
3939
}
4040

41+
/**
42+
* The branch to commit for a source. A local source has no branch axis: it
43+
* scans whatever is checked out on disk, so a stored branch would be a lie and
44+
* must never namespace its cache, URL, or recents. It therefore commits
45+
* `undefined`; a remote source keeps its branch. Applied once at the commit
46+
* boundary (the source load + setCurrentSource) so everything downstream — the
47+
* URL, the cache key, recents identity — can trust a local source is branch-
48+
* less without re-checking the source kind on every read.
49+
*/
50+
export function identityBranch(src: string, branch?: string): string | undefined {
51+
return srcKind(src) === SourceKind.Local ? undefined : branch;
52+
}
53+
54+
// ── Source identity ──────────────────────────────────────────────────
55+
// A source's identity is (src + its identity branch). These derive a comparable
56+
// string, a boolean match, and a short hash from it — used for the localStorage
57+
// namespace, recents dedupe, and the active-row match. All pure: they trust the
58+
// branch to be normalized at the commit boundary (identityBranch), so a local
59+
// source is already branch-less by the time it reaches here.
60+
61+
/**
62+
* The canonical identity string for a source: its src joined with its branch.
63+
* Two sources with the same identity string are "the same source". NUL-separated
64+
* (can't appear in a path or URL) so src and branch can't collide across the
65+
* boundary.
66+
*/
67+
export function sourceIdentity(src: string, branch?: string): string {
68+
return `${src}\0${branch ?? ''}`;
69+
}
70+
71+
/** Whether two source refs are the same source (same identity string). Local
72+
* refs are committed branch-less, so a checkout change never splits them. */
73+
export function sameSourceIdentity(
74+
a: { src: string; branch?: string },
75+
b: { src: string; branch?: string }
76+
): boolean {
77+
return sourceIdentity(a.src, a.branch) === sourceIdentity(b.src, b.branch);
78+
}
79+
80+
function djb2(s: string): string {
81+
let h = 5381;
82+
for (let i = 0; i < s.length; i++) {
83+
h = ((h << 5) + h + s.charCodeAt(i)) | 0;
84+
}
85+
return (h >>> 0).toString(36); // unsigned, base-36 — ~6-7 chars
86+
}
87+
88+
/**
89+
* Compute a short stable hash for a source's identity. Used to namespace
90+
* per-source state (selection, camera pose) in localStorage.
91+
*/
92+
export function sourceKey(src: string, branch?: string): string {
93+
return djb2(sourceIdentity(src, branch));
94+
}
95+
4196
/**
4297
* True when a source can't be loaded without first choosing a branch: a remote
4398
* URL with no branch specified. The picker resolves the repo's branches and

app/tests/components/RecentsList.test.tsx

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,22 @@ describe('RecentsList', () => {
6161
expect(onOpen).toHaveBeenCalledWith({ src: 'https://github.com/o/alpha', branch: 'main' });
6262
});
6363

64+
it('renders a branch-less local recent with no @branch pill, matched active by path', async () => {
65+
SERVER_CONFIG.value = { allowLocalRepos: true };
66+
// A local recent is branch-less; CURRENT_SOURCE is too, so they match by src
67+
// even though the loaded manifest reports a checkout branch (display only).
68+
RECENTS.value = [{ src: '/Users/me/proj', label: 'proj', lastOpenedAt: 3 }];
69+
CURRENT_SOURCE.value = { src: '/Users/me/proj' };
70+
setManifest({ tree: { name: 'proj' }, repo: { branch: 'feat/x' } } as unknown as Manifest);
71+
render(<RecentsList onOpen={() => {}} />, container);
72+
await flush();
73+
74+
const rows = container.querySelectorAll('.recent-item');
75+
expect(rows).toHaveLength(1);
76+
expect(container.querySelector('.app-header-branch-pill')).toBeNull();
77+
expect(container.querySelector('.recent-row--active')).toBeTruthy();
78+
});
79+
6480
it('remove is non-destructive: forgets the entry, does not touch the cache', async () => {
6581
const spy = vi.spyOn(manifestApi, 'clearManifestCache');
6682
render(<RecentsList onOpen={() => {}} />, container);

app/tests/state/stores/activeSource.test.ts

Lines changed: 33 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,17 @@
11
import { describe, it, expect, afterEach } from 'vitest';
22
import {
3-
sourceKey,
43
CURRENT_SOURCE_KEY,
54
CURRENT_SOURCE,
65
SOURCE_INFO,
76
setCurrentSource,
87
listRecents,
98
RECENTS,
109
} from '@/state/stores/source';
10+
import { sourceKey } from '@/utils/sources';
1111
import { setManifest } from '@/state/stores/manifest';
1212
import { EMPTY_MANIFEST } from '@/constants/manifest';
1313
import type { Manifest } from '@/types';
1414

15-
describe('sourceKey', () => {
16-
it('is deterministic for the same (src, branch)', () => {
17-
expect(sourceKey('/foo', 'main')).toBe(sourceKey('/foo', 'main'));
18-
});
19-
20-
it('distinguishes branches', () => {
21-
expect(sourceKey('/foo', 'main')).not.toBe(sourceKey('/foo', 'develop'));
22-
});
23-
24-
it('distinguishes (src, undefined) from (src, "main")', () => {
25-
expect(sourceKey('/foo')).not.toBe(sourceKey('/foo', 'main'));
26-
});
27-
28-
it('produces a short alphanumeric string', () => {
29-
const k = sourceKey('/Users/example/repos/codecity');
30-
expect(k).toMatch(/^[a-z0-9]{1,10}$/);
31-
});
32-
});
33-
3415
describe('CURRENT_SOURCE → CURRENT_SOURCE_KEY (derived)', () => {
3516
afterEach(() => {
3617
CURRENT_SOURCE.value = null;
@@ -121,12 +102,41 @@ describe('setCurrentSource', () => {
121102
expect(listRecents()[0].branch).toBe('dev');
122103
});
123104

124-
it('records a local source with its working-tree checkout branch', () => {
125-
// A local worktree ignores any requested branch and reports its checkout.
105+
it('records a local source with no branch (branch is not part of its identity)', () => {
106+
// A local worktree scans whatever is checked out; storing that branch would
107+
// be a lie (it changes on disk), so the recent and CURRENT_SOURCE omit it.
126108
setCurrentSource('/Users/me/worktrees/feat-x', undefined, {
127109
tree: { name: 'owner/codecity' },
128110
repo: { branch: 'feat/issue-77' },
129111
} as unknown as Manifest);
130-
expect(listRecents()[0].branch).toBe('feat/issue-77');
112+
expect(CURRENT_SOURCE.value).toEqual({ src: '/Users/me/worktrees/feat-x', branch: undefined });
113+
expect(listRecents()[0].branch).toBeUndefined();
114+
});
115+
116+
it('dedupes a local path across checkouts into one recent', () => {
117+
// Opening the same local path at two different checkouts must not spawn a
118+
// second row: both commits store branch: undefined, so they dedupe by src.
119+
setCurrentSource('/proj', undefined, {
120+
tree: { name: 'proj' },
121+
repo: { branch: 'main' },
122+
} as unknown as Manifest);
123+
setCurrentSource('/proj', undefined, {
124+
tree: { name: 'proj' },
125+
repo: { branch: 'feat/x' },
126+
} as unknown as Manifest);
127+
expect(listRecents()).toHaveLength(1);
128+
expect(listRecents()[0].branch).toBeUndefined();
129+
});
130+
131+
it('drops the branch from CURRENT_SOURCE + the URL for a local source', () => {
132+
// Even if a stale branch is passed in (old deep-link, recents onOpen), a
133+
// local source never carries it: CURRENT_SOURCE and the page URL stay clean.
134+
setCurrentSource('/Users/me/proj', 'stale-branch', {
135+
tree: { name: 'proj' },
136+
repo: { branch: 'main' },
137+
} as unknown as Manifest);
138+
expect(CURRENT_SOURCE.value).toEqual({ src: '/Users/me/proj', branch: undefined });
139+
const u = new URL(window.location.href);
140+
expect(u.searchParams.has('branch')).toBe(false);
131141
});
132142
});

app/tests/state/stores/excludes.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { describe, it, expect, beforeEach } from 'vitest';
2-
import { CURRENT_SOURCE, sourceKey } from '@/state/stores/source';
2+
import { CURRENT_SOURCE } from '@/state/stores/source';
3+
import { sourceKey } from '@/utils/sources';
34
import {
45
EXCLUDES,
56
ACTIVE_EXCLUDES,

0 commit comments

Comments
 (0)