feat(ui): SkillPlans Timeline + merged Profiles page; six shared primitives - #26
Conversation
Adds Timeline as the third segmented option (Timeline | Matrix | By plan), sets 'timeline' as the default view, and replaces inline page chrome with PageShell + FilterBar. Preserves existing handleCopy deduplication logic.
… Select Replaces the two inline MUI Select blocks and StatusDot/arrow glue in SyncProfileRow with the PairSelect primitive (Task 1.6), so the closed right-hand select displays the user's name instead of the bare userId. Updates the Sync page column header from 7 columns to match the new 4-column row layout.
Wire Profiles page into Routes.jsx as /profiles; add bookmark-preserving Navigate redirects for /mapping and /sync. Consolidate nav entries in HeaderNav from separate Mapping + Sync items into a single Profiles item.
Forward refs through MapAccountCard (to header row) and MapCharacterCard (to outer row), then compute mtime-matched pairs in MappingView via ResizeObserver + window events and render SwatchBridge as an overlay between the columns.
…; recompute bridge on filter change
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (1)
📝 WalkthroughWalkthroughConsolidates Mapping and Sync into a single Profiles page, redirects /mapping and /sync to /profiles?view=..., and adds MappingView and SyncView. Introduces multiple UI primitives (MatrixShell, ProgressRail, PairSelect, FilterBar, PageShell, Connector, EveTypeIcon), hooks (useMappingDerivations, usePlanReadiness, useSyncAction), tests, and nav/routing updates. ChangesProfiles consolidation, mapping & sync
Skill Plans modernization & UI primitives
Sequence Diagram(s)sequenceDiagram
participant User
participant Profiles as Profiles.jsx
participant MappingView
participant AccountsAPI
participant LocalStorage
User->>Profiles: Open /profiles?view=mapping or click Profiles→Mapping
Profiles->>LocalStorage: read persisted view/filter/mapSort
Profiles->>MappingView: render with subDirs, associations, filter, view, sort
MappingView->>MappingView: computeMappingDerivations(subDirs, associations)
User->>MappingView: drag character onto account
MappingView->>User: show confirm dialog
User->>MappingView: confirm
MappingView->>AccountsAPI: associateCharacter(userId, charId)
AccountsAPI-->>MappingView: success
MappingView->>Profiles: refreshData / update associations
sequenceDiagram
participant User
participant Profiles as Profiles.jsx
participant SyncView
participant SyncAPI
participant Toast
User->>Profiles: Open /profiles?view=sync or click Profiles→Sync
Profiles->>SyncView: render with settingsData, associations, userSelections
SyncView->>SyncView: initialize per-profile selections
User->>SyncView: select character/user, click Sync
SyncView->>SyncView: show confirm dialog
User->>SyncView: confirm
SyncView->>SyncAPI: syncSubdirectory(profile, userId, charId)
SyncAPI-->>SyncView: success
SyncView->>Toast: show success message
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
✨ Simplify code
Comment |
There was a problem hiding this comment.
Actionable comments posted: 13
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@renderer/src/components/profiles/MappingView.jsx`:
- Line 109: The effect in MappingView.jsx has an unnecessary dependency on
filter causing extra recomputations; remove filter from the dependency array of
the useEffect that currently lists [accounts, availableCharacters, mtimeToColor,
filter] so it becomes [accounts, availableCharacters, mtimeToColor]; ensure this
effect only uses values internal to it (accounts, availableCharacters,
mtimeToColor) and that filter-driven reactivity remains implemented where
filteredAccounts/filteredCharacters are computed elsewhere so behavior is
unchanged.
- Around line 132-137: handleDrop and handleUnassociate currently assume
associateCharacter/unassociateCharacter succeed and show no feedback on failure;
wrap the calls to associateCharacter and unassociateCharacter in try/catch,
check result?.success and on false or caught errors call toast.error with
result.message or the caught error message, and ensure state updates
(setAssociations) and refreshData only happen on success; reference the existing
functions handleDrop/handleUnassociate, associateCharacter,
unassociateCharacter, setAssociations and refreshData when making these changes.
In `@renderer/src/components/profiles/SwatchBridge.jsx`:
- Around line 4-6: The empty-state branch in the SwatchBridge component returns
a plain <svg> which omits the absolute overlay styling and pointer-events-none
used by the non-empty rendering; update the early return (the if (!pairs ||
pairs.length === 0) branch) to render the same overlay svg as the non-empty case
by including the absolute overlay classes (e.g., "absolute inset-0
pointer-events-none") merged with the existing className and using the same
width/height props so layout and interactivity remain consistent while pairs are
loading.
In `@renderer/src/components/profiles/SyncProfileRow.jsx`:
- Around line 23-27: The charOptions useMemo assumes each item has a name and
will throw or render empty when name is missing; update the sort and map to
fallback to charId when name is falsy by comparing (a.name || a.charId) and
(b.name || b.charId) in the localeCompare call and setting primary to (c.name ||
c.charId) while keeping secondary as c.charId so labels and sorting remain
stable even if name is absent; modify the charOptions computation that
references subDir.availableCharFiles, sort, and map accordingly.
In `@renderer/src/components/profiles/SyncView.jsx`:
- Around line 35-44: The state-updater in handleSelectionChange currently calls
persistSelections(next) inside setSelections(prev => ...), which breaks purity
and can cause duplicate writes; change handleSelectionChange to compute the
updated "next" selections outside the updater, call setSelections with either
setSelections(prev => ({...prev, ...})) but do NOT invoke persistSelections
inside that updater — instead call persistSelections(next) after setSelections
returns (or in a .then callback if using the functional update result), and
ensure persistSelections errors are caught/handled; reference
handleSelectionChange, setSelections, and persistSelections when making this
change.
In `@renderer/src/components/skillplan/MatrixShell.jsx`:
- Around line 19-24: The frozen top-left cell container (the div rendering
{frozenHeader}) must be exposed as a column header for screen readers: add
role="columnheader" (and aria-colindex if you track column indices) to the div
that renders frozenHeader in MatrixShell.jsx; likewise, mark the frozen row
label elements (the elements rendering the left-side row labels around lines
where frozen row labels are rendered) with role="rowheader" (and aria-rowindex
as appropriate) so assistive tech can associate headers with table cells.
In `@renderer/src/components/ui/EveTypeIcon.jsx`:
- Line 11: EveTypeIcon.jsx currently builds Tailwind classes at runtime via the
sizeClass = `h-${size} w-${size}` expression which Tailwind cannot detect;
instead, create a static mapping object (e.g., sizeToClass) inside the
EveTypeIcon component mapping allowed size prop values (constrain the size prop
to those values) to full class strings like "h-4 w-4", "h-6 w-6", etc., replace
sizeClass with a lookup (sizeToClass[size] || defaultClass), and update the prop
type/validation for size to accept only the mapped keys so Tailwind can pick up
the classes at build time.
In `@renderer/src/components/ui/FilterBar.jsx`:
- Around line 45-47: In FilterBar.jsx change the conditional that currently
hides the counter for falsy values to a nullish check so zero still renders:
replace the truthy check around {count ? (<span className="text-meta text-ink-3
tabular">{count}</span>) : null} with a null/undefined guard (e.g., check count
!= null or count !== undefined) so the <span> renders for 0 but remains hidden
when count is null/undefined; update the JSX in the FilterBar component where
the count rendering occurs.
In `@renderer/src/hooks/useMappingDerivations.js`:
- Around line 20-24: The roundToMinute function and surrounding mapping logic
must be hardened against malformed payloads: in roundToMinute(mtime) validate
that mtime produces a valid Date (e.g., new Date(mtime) is not "Invalid Date")
and return a safe fallback (null or empty string) instead of calling toISOString
when invalid; additionally, wherever the code assumes array fields exist (the
mapping logic that iterates fields around the usages at/near lines 33 and 49),
guard with Array.isArray checks before iterating or map/forEach and skip or
default missing/invalid entries so a single malformed file entry cannot throw
(update the functions/blocks referencing roundToMinute and the array fields to
perform these validations and handle fallbacks).
In `@renderer/src/hooks/usePlanReadiness.js`:
- Line 17: The ETA calculation is incorrectly turning a valid 0 into null by
using the || operator; update the assignment that sets eta (the line that calls
calculateDaysFromToday in usePlanReadiness.js) to use the nullish coalescing
operator so that 0 is preserved (i.e., replace the `|| null` behavior with `??
null`), then run the hook's unit/UI tests to confirm "finishes today" is
represented as 0 rather than null.
In `@renderer/src/hooks/useSyncAction.js`:
- Around line 11-31: The isLoading boolean can be flipped false by one
overlapping run() call while others are still in flight; change the loading
tracking to a counter (e.g., activeCount via useRef or useState) and increment
it at the start of run() and decrement in the finally block, then derive/set
isLoading as activeCount > 0 (update setIsLoading or replace it with derived
boolean) so controls remain disabled until all concurrent run() invocations have
completed; update references to isLoading/setIsLoading and the run function to
use this counter-based logic.
In `@renderer/src/pages/Profiles.jsx`:
- Around line 111-117: The effect is using a stale closure over params; change
the setParams call inside the useEffect so it uses the callback form to read
current search params instead of the closed-over params. In the useEffect that
currently calls writeLS(LS.view, view) and creates new URLSearchParams(params)
then setParams(...), replace that with a call to setParams(prev => { create a
new URLSearchParams from prev, set('view', view) and return it }, { replace:
true }) so the handler uses the latest params; keep writeLS and the view
dependency unchanged and remove the eslint-disable if you want the hook rules to
be satisfied.
In `@renderer/src/Routes.jsx`:
- Around line 69-75: Normalize the lastBackupDir prop before passing it to
Profiles so it is always a string; replace the current prop usage
(lastBackupDir) with a normalized value (e.g., typeof lastBackupDir === 'string'
? lastBackupDir : '' or String(lastBackupDir) with an Array.isArray check) so
Profiles receives '' instead of an empty array and
window.electronAPI.chooseDirectory(...) is never called with a truthy array.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: 70c528f4-ef0d-4dee-8fff-7fb13ef3b867
📒 Files selected for processing (48)
renderer/src/Routes.jsxrenderer/src/components/common/HeaderNav.jsxrenderer/src/components/profiles/MapAccountCard.jsxrenderer/src/components/profiles/MapAccountCard.test.jsxrenderer/src/components/profiles/MapCharacterCard.jsxrenderer/src/components/profiles/MapCharacterCard.test.jsxrenderer/src/components/profiles/MappingView.jsxrenderer/src/components/profiles/MappingView.test.jsxrenderer/src/components/profiles/MtimeSwatch.jsxrenderer/src/components/profiles/SwatchBridge.jsxrenderer/src/components/profiles/SwatchBridge.test.jsxrenderer/src/components/profiles/SyncProfileRow.jsxrenderer/src/components/profiles/SyncView.jsxrenderer/src/components/profiles/SyncView.test.jsxrenderer/src/components/skillplan/MatrixShell.jsxrenderer/src/components/skillplan/MatrixShell.test.jsxrenderer/src/components/skillplan/MissingSkillsPopover.jsxrenderer/src/components/skillplan/MissingSkillsPopover.test.jsxrenderer/src/components/skillplan/PlanList.jsxrenderer/src/components/skillplan/PlanList.test.jsxrenderer/src/components/skillplan/PlanMatrix.jsxrenderer/src/components/skillplan/PlanTimeline.jsxrenderer/src/components/skillplan/PlanTimeline.test.jsxrenderer/src/components/ui/Connector.jsxrenderer/src/components/ui/Connector.test.jsxrenderer/src/components/ui/EveTypeIcon.jsxrenderer/src/components/ui/EveTypeIcon.test.jsxrenderer/src/components/ui/FilterBar.jsxrenderer/src/components/ui/FilterBar.test.jsxrenderer/src/components/ui/PageShell.jsxrenderer/src/components/ui/PageShell.test.jsxrenderer/src/components/ui/PairSelect.jsxrenderer/src/components/ui/PairSelect.test.jsxrenderer/src/components/ui/ProgressRail.jsxrenderer/src/components/ui/ProgressRail.test.jsxrenderer/src/hooks/useMappingDerivations.jsrenderer/src/hooks/useMappingDerivations.test.jsrenderer/src/hooks/usePlanReadiness.jsrenderer/src/hooks/usePlanReadiness.test.jsrenderer/src/hooks/useSyncAction.jsrenderer/src/hooks/useSyncAction.test.jsrenderer/src/pages/Mapping.jsxrenderer/src/pages/Mapping.test.jsxrenderer/src/pages/Profiles.jsxrenderer/src/pages/Profiles.test.jsxrenderer/src/pages/SkillPlans.jsxrenderer/src/pages/Sync.jsxrenderer/src/pages/Sync.test.jsx
💤 Files with no reviewable changes (4)
- renderer/src/pages/Mapping.test.jsx
- renderer/src/pages/Mapping.jsx
- renderer/src/pages/Sync.jsx
- renderer/src/pages/Sync.test.jsx
| if (c.PendingPlans?.[planName]) { | ||
| const queue = c.SkillQueue; | ||
| const training = Array.isArray(queue) && queue.length > 0 && Boolean(character.MCT); | ||
| const eta = calculateDaysFromToday(c.PendingFinishDates?.[planName]) || null; |
There was a problem hiding this comment.
Preserve a valid ETA of 0 days.
Line 17 uses || null, which converts a valid 0 return into null. That hides “finishes today” state.
Suggested fix
- const eta = calculateDaysFromToday(c.PendingFinishDates?.[planName]) || null;
+ const eta = calculateDaysFromToday(c.PendingFinishDates?.[planName]) ?? null;📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const eta = calculateDaysFromToday(c.PendingFinishDates?.[planName]) || null; | |
| const eta = calculateDaysFromToday(c.PendingFinishDates?.[planName]) ?? null; |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@renderer/src/hooks/usePlanReadiness.js` at line 17, The ETA calculation is
incorrectly turning a valid 0 into null by using the || operator; update the
assignment that sets eta (the line that calls calculateDaysFromToday in
usePlanReadiness.js) to use the nullish coalescing operator so that 0 is
preserved (i.e., replace the `|| null` behavior with `?? null`), then run the
hook's unit/UI tests to confirm "finishes today" is represented as 0 rather than
null.
| const [isLoading, setIsLoading] = useState(false); | ||
|
|
||
| const run = useCallback(async (operation, options = {}) => { | ||
| const { successMessage, errorContext = 'sync action' } = options; | ||
| try { | ||
| setIsLoading(true); | ||
| const result = await operation(); | ||
| if (result?.success) { | ||
| if (successMessage) toast.success(successMessage); | ||
| else if (result.message) toast.success(result.message); | ||
| } | ||
| return result; | ||
| } catch (err) { | ||
| logger.error(`${errorContext} failed`, err); | ||
| const userMessage = err?.message || `${errorContext} failed`; | ||
| toast.error(userMessage); | ||
| throw err; | ||
| } finally { | ||
| setIsLoading(false); | ||
| } | ||
| }, []); |
There was a problem hiding this comment.
isLoading is incorrect for overlapping run() calls
If run() is triggered twice before the first call finishes, Line 29 can set loading to false while one operation is still in flight. That can re-enable controls too early.
Suggested fix
export function useSyncAction() {
- const [isLoading, setIsLoading] = useState(false);
+ const [pendingCount, setPendingCount] = useState(0);
+ const isLoading = pendingCount > 0;
const run = useCallback(async (operation, options = {}) => {
const { successMessage, errorContext = 'sync action' } = options;
try {
- setIsLoading(true);
+ setPendingCount((n) => n + 1);
const result = await operation();
if (result?.success) {
if (successMessage) toast.success(successMessage);
else if (result.message) toast.success(result.message);
}
return result;
} catch (err) {
logger.error(`${errorContext} failed`, err);
const userMessage = err?.message || `${errorContext} failed`;
toast.error(userMessage);
throw err;
} finally {
- setIsLoading(false);
+ setPendingCount((n) => Math.max(0, n - 1));
}
}, []);📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const [isLoading, setIsLoading] = useState(false); | |
| const run = useCallback(async (operation, options = {}) => { | |
| const { successMessage, errorContext = 'sync action' } = options; | |
| try { | |
| setIsLoading(true); | |
| const result = await operation(); | |
| if (result?.success) { | |
| if (successMessage) toast.success(successMessage); | |
| else if (result.message) toast.success(result.message); | |
| } | |
| return result; | |
| } catch (err) { | |
| logger.error(`${errorContext} failed`, err); | |
| const userMessage = err?.message || `${errorContext} failed`; | |
| toast.error(userMessage); | |
| throw err; | |
| } finally { | |
| setIsLoading(false); | |
| } | |
| }, []); | |
| const [pendingCount, setPendingCount] = useState(0); | |
| const isLoading = pendingCount > 0; | |
| const run = useCallback(async (operation, options = {}) => { | |
| const { successMessage, errorContext = 'sync action' } = options; | |
| try { | |
| setPendingCount((n) => n + 1); | |
| const result = await operation(); | |
| if (result?.success) { | |
| if (successMessage) toast.success(successMessage); | |
| else if (result.message) toast.success(result.message); | |
| } | |
| return result; | |
| } catch (err) { | |
| logger.error(`${errorContext} failed`, err); | |
| const userMessage = err?.message || `${errorContext} failed`; | |
| toast.error(userMessage); | |
| throw err; | |
| } finally { | |
| setPendingCount((n) => Math.max(0, n - 1)); | |
| } | |
| }, []); |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@renderer/src/hooks/useSyncAction.js` around lines 11 - 31, The isLoading
boolean can be flipped false by one overlapping run() call while others are
still in flight; change the loading tracking to a counter (e.g., activeCount via
useRef or useState) and increment it at the start of run() and decrement in the
finally block, then derive/set isLoading as activeCount > 0 (update setIsLoading
or replace it with derived boolean) so controls remain disabled until all
concurrent run() invocations have completed; update references to
isLoading/setIsLoading and the run function to use this counter-based logic.
- Routes: lastBackupDir always normalized to a string (was [] when config.LastBackupDir was missing — broke chooseDirectory contract). - Profiles: setParams uses functional form; drops stale closure on params. - MappingView: associate/unassociate wrapped in try/catch with toast on failure (was silent on rejection); bridge effect drops redundant filter dep (ResizeObserver covers re-render). - SwatchBridge: empty-state branch carries the same absolute overlay styling as the populated branch, so a 0-pair render doesn't push layout when containerSize is non-zero. - SyncProfileRow: charOptions falls back to charId when name is missing (sort + display) so a malformed file entry no longer throws. - SyncView: persistSelections moved out of the setSelections updater (was impure under StrictMode); errors now surface via toast. - MatrixShell: frozen header gets role=columnheader, frozen row cell gets role=rowheader so screen readers can associate headers with cells in the dense matrix. - EveTypeIcon: size prop maps to a static class table (was dynamic template — Tailwind v4 JIT couldn't see it, so the size prop had no visual effect in production builds). Constrained to oneOf 4/5/6/8. - FilterBar: count rendering switched to nullish guard so a literal zero count would still display. - useMappingDerivations: roundToMinute returns null for invalid Date inputs; iteration guards with Array.isArray and skips malformed entries so a single bad payload row no longer throws. Skipped (logged in PR comments): - usePlanReadiness `?? null` change — calculateDaysFromToday returns strings, never numeric 0, so the distinction is moot here. - useSyncAction concurrent-counter — useSyncAction is one instance per hook usage and buttons disable while loading; no current call site triggers concurrent run() invocations to defend against.
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@renderer/src/components/skillplan/MatrixShell.jsx`:
- Around line 69-80: MatrixShell.propTypes (and similar assignments across ~48
function components) are inert under React 19; replace runtime propTypes with a
codebase migration: either convert MatrixShell to TypeScript by adding a Props
interface for MatrixShell (defining ariaLabel, frozenHeader, columnHeaders,
rows, frozenWidth, colWidth) and update the MatrixShell function signature to
use that type, or add a development-only runtime validator (e.g., a
validateProps utility called inside MatrixShell that performs the same checks as
the old propTypes) and remove MatrixShell.propTypes to avoid misleading usage;
additionally update renderer/eslint.config.js to report React version 19.2.5 so
lint rules are accurate.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: 71cd843f-ca20-4412-aaef-aafb93f436c9
📒 Files selected for processing (10)
renderer/src/Routes.jsxrenderer/src/components/profiles/MappingView.jsxrenderer/src/components/profiles/SwatchBridge.jsxrenderer/src/components/profiles/SyncProfileRow.jsxrenderer/src/components/profiles/SyncView.jsxrenderer/src/components/skillplan/MatrixShell.jsxrenderer/src/components/ui/EveTypeIcon.jsxrenderer/src/components/ui/FilterBar.jsxrenderer/src/hooks/useMappingDerivations.jsrenderer/src/pages/Profiles.jsx
Summary
Three-phase frontend redesign per
docs/superpowers/plans/2026-05-04-canifly-ui-redesign.md.renderer/src/components/ui/:EveTypeIcon,FilterBar,PageShell,Connector,ProgressRail,PairSelect. Each has a colocated test.PlanTimelineview (default),usePlanReadinesshook unifying readiness math,MatrixShellextraction,MissingSkillsPopoverdrill-down, and thePlanList.handleCopybug fix (was passing the Skills array asnewPlanName)./profilespage replacing/mappingand/sync. Smart default mode (mapping when unmatched chars exist, else sync). NewMappingView+SyncViewbodies;useMappingDerivations+useSyncActionhooks.SwatchBridgeSVG overlay encodes mtime pairings as informational hairlines between paired rows./mappingand/syncredirect to/profiles?view=...to preserve bookmarks.26 commits, 49 files (+2498/-1603). 30 test files / 117 tests passing locally; production build succeeds.
Aesthetic constraints honored
No glow, no glass, no decorative animation. The
ConnectorSVG primitive is the one signature visual addition and is used informationally only (Mapping bridge, Sync flow indicator, Skill Plans timeline rail). Inter + JetBrains Mono stay; existing OKLCH token palette unchanged.Notable behavior
/profiles: query string > localStorage > smart default. A persisted'mapping'preference is overridden if all characters are now matched (so users don't get stuck in mapping mode after setup).SkillPlansdefault view is now Timeline; Matrix and By plan remain available via the segmented control.PlanListcopy now derives a unique target name (Hurricane (copy),Hurricane (copy 2), ...) instead of crashing the API call with a Skills payload.<Select>now displays the file name (was bare userId) via the newPairSelectrenderValuelogic.Code-review fixes applied pre-PR
A final-pass reviewer flagged three issues, all fixed in commit
d26cdd9:useSyncActionwas throwing silently — addedtoast.errorin the catch (matchesuseAsyncOperation).MappingViewlocal association state was diverging from the prop afterrefreshData— added a syncuseEffect.SwatchBridgerects went stale after filter changes — addedfilterto the recompute effect's deps.Known follow-ups (deferred, low impact)
Profiles.isDefaultDirinitial state isfalseregardless of whethercurrentSettingsDirmatches the Tranquility default. Reset-to-default button shows when it shouldn't on first load. Cosmetic; clicking is safe.PlanList.jsxstill has an inline plan-icon<img>; could be replaced withEveTypeIconfor consistency. Behaviorally identical.Test plan
cd renderer && npm testpasses (currently 30/30 files, 117/117 tests)cd renderer && npm run buildsucceeds/skill-plans— Timeline renders by default; segmented control switches to Matrix and By plan; filter input takes/focus andEscclears<name> (copy)appears/profiles— defaults to Mapping if there are unmatched chars, Sync otherwise/mappingand/syncdirectly — both redirect to/profileswith the right mode🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Improvements