Turn Ghostwriter's flat message list into a tree structure, enabling
Claude.ai/ChatGPT-style branching conversations.
**Data model**: Add `parentMessageId` and `activeChildId` to messages,
`activeRootMessageId` to threads. Migration backfills existing messages
into a linear chain and links regenerated messages as siblings.
**Backend**: Tree-walking queries (getActivePathFromRoot, getAncestorPath,
getSiblingsOf), rewritten history builder that follows the ancestor path,
new editMessage and switchBranch services, regenerate now works on any
assistant message (not just the latest).
**Frontend**: BranchNavigator component (← 2/3 → arrows), inline edit on
user messages, per-message regenerate on assistant messages, regenerate
button removed from composer (now per-message).
**Infra**: Pin Node 22 via Volta to prevent ABI mismatches with
better-sqlite3 across environments.
When switching between job tabs (e.g. Discovered → Applied), the detail
panel would briefly show the previously selected job with the new tab's
action buttons — confusing and error-prone.
Three coordinated fixes:
- setActiveTab now checks if the selected job's status fits the target
tab, keeping it when valid (e.g. Discovered → All Jobs) and clearing
it otherwise.
- New visibleSelectedJob useMemo guard synchronously nulls out the
selected job when it doesn't belong to the active tab, eliminating
the one-frame flash caused by the data hook's useEffect lag.
- Auto-select effect now handles the case where a job passes the status
check but gets filtered out by source/sponsor/salary filters.