Repository navigation
Add focusOrOpenNode to the navigation service (#2432 phase 1) - #2441
Conversation
Pragmatic Code Review — PR #2441Review Type: Initial Review (no prior reviews on this PR) Requirements Check (phase 1 scope only)#2432's phase-1 section asks for one extracted navigation-service method plus migration of the listed call sites. Measured against that table:
The issue's explicit instruction — "The two variants need an optional I also verified the two non-migrated exclusions rather than taking the PR body's word:
Both exclusions are correct, not oversights. Phase 2 (the panel itself) is explicitly out of scope for this PR, per #2432's own "Land this as its own PR" instruction. Nothing here anticipates phase 2 speculatively, which is the right call — YAGNI applies to the seam as much as to the feature. Verification: I ran Code Review Findings🟡 Improvements📁 The PR body and commit message both state: "the sidebar's function getTargetPaneId(): string {
const currentState = navigationStore.state;
const paneExists = currentState.panes.some((p) => p.id === currentState.activePaneId);
if (paneExists) {
return currentState.activePaneId;
}
return currentState.panes[0]?.id ?? 'pane-1';
}I grepped the whole of To be fair on severity: the helper itself cannot simply be deleted, because Principle: DRY, and — more sharply here — the commit message is part of the change. A reviewer or future archaeologist reading "the fallback is now 📁 focusExistingNodeTab(nodeId: string): boolean {
return this.focusNodeTab(nodeId);
}The task brief asks specifically whether exposing two public methods is justified. My read: the split is justified, the wrapper is not. The split is justified because QueryNodeViewer's fall-through is What is unnecessary is the third method.
As it stands, three near-identically-named methods ( Principle: KISS / naming as the primary documentation. When two symbols differ only by a word that does not describe the difference, the reader must consult prose — a cost paid on every future read. 🟢 Nitpicks📁 Daily Journal's use of it is correct and necessary, and I want to be explicit about that since the brief asks. I confirmed the mechanism: The nit is that Nit — 📁 Areas verified cleanI checked these specifically and found no issues worth filing:
RecommendationAPPROVE This is a clean, well-scoped extraction that does exactly what #2432 phase 1 asks and stops there. The design judgment on the hard call — refusing to force QueryNodeViewer's cross-pane fall-through into a boolean flag — is correct, and the documentation of why each option exists is unusually good; it is the kind of comment that survives contact with a future reader. Neither finding is a blocker. The |
malibio
left a comment
There was a problem hiding this comment.
Automated pragmatic-code-review: APPROVE (posted as a comment — GitHub blocks self-approval). Full findings in the review comment above.
Three findings from the review of #2441. All addressed. **The commit message was wrong (🟡).** The previous commit claimed "the sidebar's 'pane-1' fallback is now DEFAULT_PANE_ID, which is that same value under its name." It was not — the sidebar's local getTargetPaneId survived untouched, literal and all, because openSearchTab still called it. So the PR removed two copies of that helper and left the third, which was the one the message described. Fixed by taking the reviewer's stronger option rather than the literal swap the message promised: NavigationService.targetPaneId() is now the canonical version of this logic, so it becomes public and openSearchTab calls it. That retires the last copy instead of correcting its constant, and 'pane-1' now appears in exactly one place in the frontend — its own definition in navigation.svelte.ts. **focusExistingNodeTab was a pass-through (🟡).** It called the private focusNodeTab and dropped its optional second parameter, leaving three near-identically-named methods in one class where the names did not carry the distinction — a reader had to consult the doc comments to tell focusNodeTab from focusExistingNodeTab. The split itself was upheld on review, and stays: QueryNodeViewer's fall-through is navigateToNodeInOtherPane, not addTab into the active pane, and folding that into an option would mean a boolean selecting which method runs, including ancestor resolution, pane creation and scrollToNode. Two names beat that flag. What went is the third method — focusNodeTab is now public with its already-optional nodeType parameter, so the QueryNodeViewer call site is unchanged apart from the name. **matchNodeType is a symptom (🟢).** Kept as-is; the real fix is date-node identity, which is well outside this PR. But the option's doc comment now says so explicitly, including that it should be deleted along with its only caller if date ids ever stop being bare YYYY-MM-DD strings. The reviewer's point was that the flag reads as general when it is not, and a comment is where a future caller will actually encounter that. Also collapsed a stray double blank line in the test file. No behavior change at any call site. 233 files / 4916 tests passed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PtzaT8FoAP9ut43eveHUdK
23789a1 to
9ef3240
Compare
Address Review — all three findings addressed✅ 🟡 The commit message was wrong about
|
|
Note on overlap with #2442: that PR fixes the same Leaving my commit in place per instruction. Once #2442 lands, the identical hunk resolves cleanly on rebase and my commit becomes an effective no-op; no conflict to hand-resolve. #2442 also fixes the root cause of something I hit here: the |
|
Correction — the My two previous comments described carrying that fixture fix here. That was wrong: #2442 is landing with the identical change plus the This PR is now frontend-only again — six files, zero Rust: The three review findings remain addressed as described above; that part is unchanged. Consequence: this branch can't be pushed again until #2442 lands, because the pre-push gate runs the full Rust suite against a Disregard the "Additional commit: repairing a break on |
Six call sites each hand-rolled the same open-or-focus behaviour: scan `state.tabs` for one whose `content.nodeId` matches, `setActiveTab` if found, otherwise mint a uuid and `addTab` into a pane resolved by a locally-duplicated `getTargetPaneId`. Three of them carried their own copy of that pane helper, two spelled its fallback as the literal 'pane-1'. Neither existing navigation method does this. `navigateToNode` re-points the *current* tab, replacing what the user was looking at, and `navigateToNodeInOtherPane` always calls `addTab`, so clicking the same node twice stacks duplicates. Tab reuse only existed as copies in the components. `focusOrOpenNode(nodeId, options)` is that behaviour, once. Its three options are the axes the call sites genuinely differ on, not generality for its own sake: - `nodeType` — written into the tab's content to select a viewer. Usually the node's own type, but deliberately an override where a tab routes elsewhere: a schema id opens as 'query' to reach QueryNodeViewer, a chat as 'ai-chat'. - `title` — defaults to 'Loading...', which viewers replace on mount. Only the collection viewer passes one, because its member row already holds the text. - `matchNodeType` — off by default, since a node id identifies one node and any tab showing it is the tab the user means. Daily Journal needs it: a date id is a date string, not a real node id, so nothing stops an unrelated tab from carrying the same string. Unlike the other two methods it does not resolve to a navigation ancestor. These callers pass ids they mean to open as tabs — a schema, a chat, a collection member — not child nodes to scroll to. QueryNodeViewer's row click is the one case that does not fit: its "not open yet" branch opens in the *other* pane, so the result list stays visible beside the node. Rather than bend the signature around it, the focus half is exposed separately as `focusExistingNodeTab`, and both share one private lookup so they cannot drift on what counts as already open. Two sites are deliberately left alone. `openSearchTab` opens a non-node tab (`type: 'search'`, no content) so the node lookup does not apply, and `pane-content.svelte:95` matches one specific tab by id as a staleness guard, not to focus it. QueryNodeViewer's `rerouteTab` also stays: it re-points a tab it identifies by pane, which is `updateTabContent`, not focus-or-open. Net -176 lines across five components. Behaviour is unchanged at every migrated site; the sidebar's 'pane-1' fallback is now DEFAULT_PANE_ID, which is that same value under its name. Phase 1 of #2432, landed separately from the two-pane panel because verifying tab and pane behaviour across the sidebar, search and two viewers is a different review job from reviewing a relationships panel. Tests: 16 new cases covering open, reuse, cross-pane focus, title defaulting, the nodeType override, both matchNodeType directions, and that no ancestor resolution occurs. Suite 233 files / 4916 passed (baseline 4900). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PtzaT8FoAP9ut43eveHUdK
Three findings from the review of #2441. All addressed. **The commit message was wrong (🟡).** The previous commit claimed "the sidebar's 'pane-1' fallback is now DEFAULT_PANE_ID, which is that same value under its name." It was not — the sidebar's local getTargetPaneId survived untouched, literal and all, because openSearchTab still called it. So the PR removed two copies of that helper and left the third, which was the one the message described. Fixed by taking the reviewer's stronger option rather than the literal swap the message promised: NavigationService.targetPaneId() is now the canonical version of this logic, so it becomes public and openSearchTab calls it. That retires the last copy instead of correcting its constant, and 'pane-1' now appears in exactly one place in the frontend — its own definition in navigation.svelte.ts. **focusExistingNodeTab was a pass-through (🟡).** It called the private focusNodeTab and dropped its optional second parameter, leaving three near-identically-named methods in one class where the names did not carry the distinction — a reader had to consult the doc comments to tell focusNodeTab from focusExistingNodeTab. The split itself was upheld on review, and stays: QueryNodeViewer's fall-through is navigateToNodeInOtherPane, not addTab into the active pane, and folding that into an option would mean a boolean selecting which method runs, including ancestor resolution, pane creation and scrollToNode. Two names beat that flag. What went is the third method — focusNodeTab is now public with its already-optional nodeType parameter, so the QueryNodeViewer call site is unchanged apart from the name. **matchNodeType is a symptom (🟢).** Kept as-is; the real fix is date-node identity, which is well outside this PR. But the option's doc comment now says so explicitly, including that it should be deleted along with its only caller if date ids ever stop being bare YYYY-MM-DD strings. The reviewer's point was that the flag reads as general when it is not, and a comment is where a future caller will actually encounter that. Also collapsed a stray double blank line in the test file. No behavior change at any call site. 233 files / 4916 tests passed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PtzaT8FoAP9ut43eveHUdK
9ef3240 to
f670dcc
Compare
Phase 1 of #2432. The two-pane relationships panel needs to open the node at the other end of an edge without stacking a duplicate tab — and no existing navigation method does that. This PR adds it and migrates the call sites that had each grown their own copy.
The problem
Six call sites hand-rolled the same behaviour: scan
state.tabsfor one whosecontent.nodeIdmatches,setActiveTabif found, otherwise mint a uuid andaddTabinto a pane resolved by a locally-duplicatedgetTargetPaneId. Three carried their own copy of that pane helper; two spelled its fallback as the bare literal'pane-1'.Neither existing method covers it:
navigateToNodere-points the current tab, replacing what the user was looking atnavigateToNodeInOtherPanealways callsaddTab, so clicking the same node twice stacks duplicatesTab reuse existed only as copies inside components.
The API
focusOrOpenNode(nodeId, options). The three options are the axes the call sites genuinely differ on, not generality for its own sake:nodeType'query'to reach QueryNodeViewer, a chat as'ai-chat'title'Loading...', which viewers replace on mount. Only the collection viewer passes one, because its member row already holds the textmatchNodeTypeUnlike the other two methods it does not resolve to a navigation ancestor. These callers pass ids they mean to open as tabs — a schema, a chat, a collection member — not child nodes to scroll to.
The one call site that doesn't fit
QueryNodeViewer's row click opens its "not yet open" case in the other pane, so the result list stays visible beside the node. Rather than bend the signature around it, the focus half is exposed separately as
focusExistingNodeTab, and both share one private lookup so they can't drift on what counts as "already open". #2432 explicitly says not to force every site into one signature.Deliberately not migrated
openSearchTab— opens a non-node tab (type: 'search', nocontent), so the node lookup doesn't applypane-content.svelte:95— matches one specific tab by id as a staleness guard, not to focus itrerouteTab(query-node-viewer) — re-points a tab it identifies by pane; that'supdateTabContent, not focus-or-openVerification
Unit: 16 new cases — open, reuse, cross-pane focus, title defaulting and preservation on reuse, the
nodeTypeoverride, bothmatchNodeTypedirections, and that no ancestor resolution occurs. Suite 233 files / 4916 passed (baseline 4900). Pre-push gate green: frontend + Rust + e2e.Browser (
dev:browseragainst an isolatedNODESPACE_HOME, real DB untouched):focusExistingNodeTabpreventsSearch's
openResultis covered by unit tests only:search-pane.sveltecalls Tauriinvokedirectly forsearch_roots, which doesn't work in browser proxy mode. That limitation is pre-existing onmainand untouched here.Net effect
−176 lines across five components, replaced by one service method. Behaviour is unchanged at every migrated site; the sidebar's
'pane-1'fallback is nowDEFAULT_PANE_ID, which is that same value under its name.Landed separately from the panel itself because verifying tab and pane behaviour across the sidebar, search and two viewers is a different review job from reviewing a relationships panel.
🤖 Generated with Claude Code
https://claude.ai/code/session_01PtzaT8FoAP9ut43eveHUdK