Skip to content

refactor(x): collapse the 12 view booleans in App.tsx onto ViewState - #794

Open
kwp3 wants to merge 1 commit into
rowboatlabs:mainfrom
kwp3:refactor/app-view-state
Open

refactor(x): collapse the 12 view booleans in App.tsx onto ViewState#794
kwp3 wants to merge 1 commit into
rowboatlabs:mainfrom
kwp3:refactor/app-view-state

Conversation

@kwp3

@kwp3 kwp3 commented Jul 26, 2026

Copy link
Copy Markdown

What

App.tsx modelled "which view is showing" twice:

  1. Correctly, as the ViewState discriminated union — but only used for back/forward history and deep links.
  2. Badly, as 12 mutually-exclusive useState booleans, hand-synced across ~700 setter calls, plus 13 sites that enumerated the whole set in a &&/|| chain.

There is now one useState<ViewState>, and every is*Open flag plus selectedPath and selectedBackgroundTask is derived from it.

Why it was a bug class, not just noise

The render is a ternary chain. Two booleans true silently rendered the first match; all false rendered a blank pane. Neither errored. Deriving the flags makes both states unrepresentable rather than merely unlikely.

Shape of the change

  • Readers are untouched — the derived consts keep the same names, so ~290 call sites didn't move.
  • The setters collapse to 24 setView calls.
  • The 13 enumerating guards become three named predicates.
  • App.tsx: 7,704 → 6,729 lines.

Three decisions worth flagging for review, because each looks like an inconsistency but isn't:

  • isBrowserOpen stays a separate boolean. It overlays whatever view is showing rather than being one — handleToggleBrowser sets it without clearing anything.
  • isTabbedView and isTabbedViewExceptCode differ only by the Code view. Several guards predate Code and still exclude it; folding them together would change behaviour, so both sets are kept and commented.
  • workspaceInitialPath / knowledgeViewFolderPath / knowledgeViewMode stay separate state. They deliberately outlive the view — switching away from the workspace tab and back must land on the folder being browsed, not the root. I folded them in first, caught the regression, and reverted with a comment explaining why.

Latent bug fixed in passing

New viewForTabPath() replaces the 12-branch tab-path→view chain that switchFileTab and closeFileTab each hand-rolled. closeFileTab had no isCodeTabPath branch, so closing a tab beside the Code tab set selectedPath to the sentinel '__rowboat_code__'. It rendered correctly only because isCodeOpen won the ternary chain, while history recorded a bogus file path.

Also

Lifts the pure helpers App.tsx had accumulated into lib/knowledge-tree, view-state, copilot-prompts, plus additions to wiki-links and frontmatter. No behaviour change; extracted as-is.

Verification

  • npm run typecheck clean.
  • Renderer suite 134 passing, up from 119 (24 new tests across view-state.test.ts and wiki-links.test.ts).
  • npm run lint unchanged at the 11 pre-existing errors in packages/core and packages/shared — none in renderer, none introduced here.

The viewForTabPath tests specifically pin the failure mode the refactor exists to prevent: a sentinel tab path missing its branch falls through to { type: 'file' }, which renders a blank middle pane instead of erroring.

Not included

Converting the render ternary chain to switch (view.type). Large JSX diff for zero behaviour gain now that the flags are mutually exclusive by construction — worth doing whenever that JSX needs editing anyway.

App.tsx modelled "which view is showing" twice: correctly as the ViewState
union (used only for back/forward history and deep links), and badly as 12
mutually-exclusive useState booleans hand-synced across ~700 setter calls,
plus 13 sites that enumerated the whole set in a &&/|| chain. The render is
a ternary chain, so two-true silently rendered the first match and all-false
rendered a blank pane. Neither errored.

There is now one useState<ViewState>, and every is*Open flag plus
selectedPath and selectedBackgroundTask are derived from it, so both states
are unrepresentable. Readers are untouched; the setters collapse to 24
setView calls, and the 13 enumerating guards become three named predicates.
isTabbedView and isTabbedViewExceptCode differ only by the Code view, which
several guards predate - folding them together would change behaviour.

isBrowserOpen stays a separate boolean: it overlays whatever view is
showing rather than being one. workspaceInitialPath, knowledgeViewFolderPath
and knowledgeViewMode also stay separate, because they deliberately outlive
the view - switching away from the workspace tab and back must land on the
folder being browsed, not the root.

New viewForTabPath() replaces the 12-branch tab-path-to-view chain that
switchFileTab and closeFileTab each hand-rolled. That fixes a latent bug:
closeFileTab had no isCodeTabPath branch, so closing a tab beside the Code
tab set selectedPath to the sentinel '__rowboat_code__'. It rendered
correctly only because isCodeOpen won the ternary chain, while history
recorded a bogus file path.

Also lifts the pure helpers App.tsx had accumulated into lib/ (knowledge-tree,
view-state, copilot-prompts, and appended to wiki-links and frontmatter).
7,704 -> 6,729 lines. Typecheck clean; renderer suite 134 passing, from 119.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 26, 2026 16:54

Copilot AI 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.

Pull request overview

This PR refactors the Electron renderer’s App.tsx view-selection logic to use a single ViewState discriminated union as the source of truth, eliminating the prior set of mutually-exclusive boolean useState flags and the associated sync/guard complexity. It also extracts a set of pure helper modules from App.tsx into src/lib/ and adds targeted unit tests to pin behavior (notably sentinel tab-path → view mapping and wiki-link rewrite behavior).

Changes:

  • Collapse “which view is open” into useState<ViewState> and derive all is*Open flags and selected payloads from it.
  • Extract pure helpers into src/lib/ (view-state, knowledge-tree, copilot-prompts, plus wiki-link/frontmatter helpers).
  • Add Vitest coverage for viewForTabPath sentinel handling and wiki-link rewrite behavior.

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
apps/x/apps/renderer/src/App.tsx Replaces many view booleans with a single ViewState and updates navigation/guards to derive view flags from it.
apps/x/apps/renderer/src/lib/view-state.ts Introduces shared ViewState union + sentinel tab paths + deep-link parsing as a pure helper module.
apps/x/apps/renderer/src/lib/view-state.test.ts Adds tests that ensure every sentinel tab path maps to the correct ViewState variant.
apps/x/apps/renderer/src/lib/wiki-links.ts Exports a shared wiki-link token regex and extracts rename rewrite helpers from App.tsx.
apps/x/apps/renderer/src/lib/wiki-links.test.ts Adds unit tests for wiki-link rewriting on rename (full path, bare name, prefix/alias/anchor preservation).
apps/x/apps/renderer/src/lib/knowledge-tree.ts Extracts knowledge sidebar tree building/sorting/flattening helpers from App.tsx.
apps/x/apps/renderer/src/lib/frontmatter.ts Adds parseLinkedGoogleDocFrontmatter extraction for linked Google Doc metadata.
apps/x/apps/renderer/src/lib/copilot-prompts.ts Extracts prompt string builders used by UI affordances.
Comments suppressed due to low confidence (1)

apps/x/apps/renderer/src/lib/view-state.ts:100

  • The parseDeepLink JSDoc lists only a subset of supported type values, but the implementation supports additional targets (workspace, knowledge-view, chat-history, home, code, bg-tasks, apps). Keeping this list accurate helps prevent incorrect deep links.
 * Shape: rowboat://open?type=<file|chat|graph|task|suggested-topics|meetings|live-notes|email>&...

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +3716 to +3719
// Re-opens the remembered view directly rather than replaying navigation,
// so views that need side-effects on entry (a file tab, a workspace path,
// a knowledge-view mode) are deliberately not restorable: report the miss
// and let the caller fall back instead of leaving a blank full-screen chat.
if (a.type === 'workspace' && b.type === 'workspace') return (a.path ?? '') === (b.path ?? '')
if (a.type === 'knowledge-view' && b.type === 'knowledge-view') return (a.folderPath ?? '') === (b.folderPath ?? '') && (a.mode ?? '') === (b.mode ?? '')
if (a.type === 'email' && b.type === 'email') return (a.threadId ?? '') === (b.threadId ?? '') && (a.searchQuery ?? '') === (b.searchQuery ?? '')
return true // both graph
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.

2 participants