Skip to content

Convert to React, add REST API#541

Open
johnste wants to merge 3 commits into
mainfrom
react
Open

Convert to React, add REST API#541
johnste wants to merge 3 commits into
mainfrom
react

Conversation

@johnste

@johnste johnste commented Jun 30, 2026

Copy link
Copy Markdown
Owner

Full rewrite the settings window and integration

  • UI is rewritten in React instead of Svelte. Because I know React way better than I do React.
  • The app and the window used to talk to each other through message-passing. This is replaced by a simple REST API instead to keep things more standard.

Nothing should feel different to end users.

Summary by CodeRabbit

  • New Features

    • Rebuilt the user interface with a React-based layout, including Start, Rules, About, Log Viewer, and URL testing pages.
    • Added real-time updates via an events stream for configuration, logs, and update availability.
    • Improved feedback with toasts, tooltips, updated icons, and a new footer/status area.
  • Bug Fixes

    • Improved reliability of configuration, update, and rules syncing so the UI refreshes more consistently.
    • Standardized rule matching output and improved log viewing/copying and debug filtering behavior.

@coderabbitai

coderabbitai Bot commented Jun 30, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR replaces the native WebView message bridge with an authenticated HTTP API and SSE event stream, and rewrites the finicky-ui package from Svelte to React. It also updates shared config/types, log and rules handling, and the UI pages and components that consume the new API.

Changes

Native app: SSE/API server and WebView bridge removal

Layer / File(s) Summary
SSE hub and HTTP API server
apps/finicky/src/window/server.go
Adds token-authenticated REST endpoints, CORS gating for the WebView origin, an SSE hub, and streamed config/update/log delivery.
main.go and logger wiring
apps/finicky/src/main.go, apps/finicky/src/logger/logger.go, apps/finicky/src/config/vm.go
Adds mutex-guarded shared state, switches config/update publication to SSE, removes the exported URL-test bridge and version message path, updates config option JSON tags, and sends raw log payloads to the UI stream.
Native WebView bridge cleanup
apps/finicky/src/window/window.go, window.h, window.m
Removes the old WebView message bridge, exposes API port/token access to the page, and simplifies window/menu setup and message handling.
Rule serialization and ignore list
apps/finicky/src/rules/rules.go, .gitignore
Serializes Rule.match consistently as an array and adds .fallow to ignored paths.

Estimated code review effort: 4 (Complex) | ~75 minutes

finicky-ui: Svelte to React rewrite

Layer / File(s) Summary
Tooling and React entry point
packages/finicky-ui/package.json, vite.config.ts, tsconfig.app.json, vite-env.d.ts, index.html, main.tsx, App.module.css
Switches build tooling and entrypoints from Svelte to React/Vite React and adds the app shell layout CSS.
App shell, store, API client, and types
packages/finicky-ui/src/App.tsx, lib/appStore.ts, lib/api/client.ts, lib/api/index.ts, lib/useSSE.ts, types.ts, lib/toast.ts, lib/useDebouncedCallback.ts, lib/useLogControls.ts, lib/useRulesSave.ts, lib/testUrlStore.ts
Adds the React app shell, in-memory store, authenticated API client, SSE hook, shared types, toast store rewrite, and debounced/log/rules helpers.
Layout and feedback components
packages/finicky-ui/src/components/TabBar.*, Footer.*, ToastContainer.*, Tooltip.*, PageContainer.*, pages/About.*, pages/LogViewer.*
Recreates navigation, footer status, toast UI, tooltips, page container layout, and the About and LogViewer pages in React.
Form, selector, and log components
packages/finicky-ui/src/components/OptionRow.*, BrowserProfileSelector.*, LogContent.*
Recreates the option toggle, browser/profile selector, and log content list as controlled React components.
Icon component migration
packages/finicky-ui/src/components/icons/*
Replaces the Svelte SVG icon markup with React icon components for the app icons.
Rules, StartPage, and TestUrl pages
packages/finicky-ui/src/pages/Rules.*, StartPage.*, TestUrl.*
Adds the drag-reorderable rules editor, the configuration/options page with debounced saving, and the URL testing page with normalized lookups and delayed loading state.

Estimated code review effort: 4 (Complex) | ~80 minutes

Possibly related PRs

  • johnste/finicky#512: Touches the same apps/finicky/src/rules/rules.go serialization path and Rule.MarshalJSON behavior for match.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 10.38% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main rewrite to React and the switch to a REST API.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch react

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 golangci-lint (2.12.2)

level=error msg="[linters_context] typechecking error: pattern ./...: directory prefix . does not contain main module or its selected dependencies"


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 20

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/finicky/src/main.go (1)

468-485: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Avoid dereferencing stale or nil configInfo.

cs is treated as nullable, but lastConfigPayload unconditionally reads configInfo. On the first nil state this can panic; on reload it can reuse stale config metadata.

Proposed fix
-	cs := newVM.GetConfigState()
+	ci := ConfigInfo{}
+	cs := newVM.GetConfigState()
 	if cs != nil {
-		configInfo = &ConfigInfo{
+		ci = ConfigInfo{
 			Handlers:       cs.Handlers,
 			Rewrites:       cs.Rewrites,
 			DefaultBrowser: cs.DefaultBrowser,
 			ConfigPath:     configPath,
 		}
 	}
+	configInfo = &ci
@@
 	lastConfigPayload = map[string]interface{}{
-		"handlers":       configInfo.Handlers,
-		"rewrites":       configInfo.Rewrites,
-		"defaultBrowser": configInfo.DefaultBrowser,
-		"configPath":     util.ShortenPath(configInfo.ConfigPath),
+		"handlers":       ci.Handlers,
+		"rewrites":       ci.Rewrites,
+		"defaultBrowser": ci.DefaultBrowser,
+		"configPath":     util.ShortenPath(ci.ConfigPath),
🤖 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 `@apps/finicky/src/main.go` around lines 468 - 485, The `lastConfigPayload`
block is unconditionally dereferencing `configInfo` even though
`GetConfigState()` can return nil, which can panic or reuse stale metadata.
Update the `main.go` config-building flow so `configInfo` is only read when it
has been freshly populated from `newVM.GetConfigState()`, and otherwise
initialize/reset `lastConfigPayload` safely from `cs` or skip the payload build
until valid config data exists. Use the `GetConfigState`, `ConfigInfo`, and
`lastConfigPayload` symbols to keep the fix localized and ensure reloads don’t
retain old values.
🧹 Nitpick comments (9)
apps/finicky/src/rules/rules.go (1)

39-51: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Correct fix; minor duplication in RuleAlias.

Logic is correct — normalizing nil to []string{} and always marshaling match as an array matches the stated goal and is consistent with getRulesData/writeJSON downstream in server.go. One nit: RuleAlias manually re-declares all of Rule's fields/tags instead of using the standard type Alias Rule idiom, which avoids duplication and the risk of silently dropping a future field if Rule changes.

♻️ Optional refactor using the alias-type idiom
 func (r Rule) MarshalJSON() ([]byte, error) {
-	type RuleAlias struct {
-		Match   []string `json:"match"`
-		Browser string   `json:"browser"`
-		Profile string   `json:"profile,omitempty"`
-	}
+	type RuleAlias Rule
 	match := r.Match
 	if match == nil {
 		match = []string{}
 	}
-	return json.Marshal(RuleAlias{Match: match, Browser: r.Browser, Profile: r.Profile})
+	alias := RuleAlias(r)
+	alias.Match = match
+	return json.Marshal(alias)
 }
🤖 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 `@apps/finicky/src/rules/rules.go` around lines 39 - 51, Refactor the
MarshalJSON implementation on Rule to avoid duplicating Rule’s fields in the
local RuleAlias struct. Keep the existing nil-to-empty-slice normalization for
Match, but switch to the standard alias-type pattern in Rule.MarshalJSON so the
serialization stays aligned automatically with Rule if fields are added or tags
change later.
packages/finicky-ui/src/components/TabBar.tsx (1)

32-41: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicated tab-rendering logic between top and bottom tabs.

The Link/className-building block is repeated almost verbatim for tabs and bottomTabs, differing only by the extra tabSmall class and the error badge. Consider extracting a small shared render helper (e.g. renderTab(path, label, Icon, extraClass?, badge?)) to avoid the duplication.

Also applies to: 44-56

🤖 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 `@packages/finicky-ui/src/components/TabBar.tsx` around lines 32 - 41, The tab
rendering in TabBar is duplicated between the main tabs and bottomTabs, so
extract the shared Link/className/icon/label markup into a small helper inside
TabBar.tsx, such as a renderTab function that takes path, label, Icon, and
optional extra class or badge content. Update both tabs.map and bottomTabs.map
to call the helper so the only differences remain the tabSmall styling and the
error badge.
packages/finicky-ui/src/components/icons/About.tsx (1)

1-9: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Duplicate SVG markup with InfoIcon.

AboutIcon (packages/finicky-ui/src/components/icons/About.tsx) and InfoIcon (packages/finicky-ui/src/components/icons/Info.tsx) render identical SVG paths (circle + vertical line + dot), differing only in width/height. Consider a single parameterized InfoCircleIcon({ size = 24 }) to avoid duplicated markup across icon components.

🤖 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 `@packages/finicky-ui/src/components/icons/About.tsx` around lines 1 - 9,
`AboutIcon` duplicates the same SVG markup as `InfoIcon`, so replace the
repeated icon implementation with a shared parameterized component such as
`InfoCircleIcon` and have `AboutIcon` delegate to it with the appropriate size.
Update the icon components in the `AboutIcon`/`InfoIcon` pair so the circle,
line, and dot paths live in one place and only the size prop varies.
packages/finicky-ui/src/components/LogContent.tsx (1)

21-22: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Using array index as React key for log entries.

If messageBuffer is ever trimmed from the front (common for capped log buffers), index-based keys cause React to misassociate DOM nodes with their previous entries during reconciliation. Consider keying by entry.time plus index, or a stable id if the buffer source provides one.

🤖 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 `@packages/finicky-ui/src/components/LogContent.tsx` around lines 21 - 22, The
LogContent log entry list is using the array index as the React key, which can
cause reconciliation issues when messageBuffer is trimmed or reordered. Update
the key in the LogContent component’s map over entries to use a stable
identifier from each entry, such as entry.time combined with a fallback index or
another unique id if available, so each li remains consistently associated with
the same log item.
packages/finicky-ui/src/lib/appStore.ts (1)

35-36: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

getNumErrors appears unused — error-count logic is duplicated at call sites instead.

Per cross-file graph context, StartPage.tsx computes messageBuffer.filter((m) => m.level.toLowerCase() === "error").length inline rather than calling this store method. That's the same expression duplicated in two places; if the error-detection rule ever changes (e.g., case-insensitivity tweak, additional levels), one of the two will silently drift out of sync.

Consider having consumers call appStore.getNumErrors() instead of re-implementing the filter.

🤖 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 `@packages/finicky-ui/src/lib/appStore.ts` around lines 35 - 36, `getNumErrors`
in `appStore` is duplicated at call sites instead of being reused. Update
consumers such as `StartPage.tsx` to call `appStore.getNumErrors()` rather than
re-implementing the `messageBuffer.filter(...).length` logic inline, so the
error-count rule stays centralized and consistent.
packages/finicky-ui/vite.config.ts (1)

7-16: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

assetsDir: "assets" is effectively a no-op given the custom filename patterns.

When assetFileNames/chunkFileNames/entryFileNames are fully overridden without an assetsDir-relative path segment, Rollup/Vite uses the patterns as-is instead of nesting under build.assetsDir (that default-nesting behavior only applies to the unmodified default pattern). Confirmed by Vite issue reports: When rollupOptions.output is an array, it defaults to '/[name]. the assetsDir prefix is only used by Vite's own default pattern, not by custom overrides like the ones here. So all entries/chunks/assets will land flat in the output root, making assetsDir: "assets" dead configuration. Not breaking (Vite still rewrites the generated index.html references correctly), but confusing/misleading.

♻️ Suggested cleanup
   build: {
-    assetsDir: "assets",
     rollupOptions: {
       output: {
         assetFileNames: "[name][extname]",
         chunkFileNames: "[name].js",
         entryFileNames: "[name].js",
       },
     },
   },
🤖 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 `@packages/finicky-ui/vite.config.ts` around lines 7 - 16, Remove the
misleading build.assetsDir setting in vite.config.ts since the custom
rollupOptions.output patterns in the Vite config (assetFileNames,
chunkFileNames, entryFileNames) already control the output paths and ignore the
default assetsDir nesting. Update the build configuration to keep only the
filename pattern overrides, or if a nested assets folder is desired, incorporate
that path directly into the rollupOptions.output patterns instead of relying on
assetsDir.
packages/finicky-ui/src/lib/useSSE.ts (1)

7-30: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

No error listener on the EventSource.

The native browser will auto-retry on a dropped connection, so this isn't broken, but there's currently zero visibility if the SSE stream to the local API server fails to (re)connect (e.g., app backend restarts) — logs/config/update events would just silently stop updating with no indication to the user or in the console.

   useEffect(() => {
     const es = new EventSource(api.eventsUrl());
+
+    es.addEventListener("error", (e) => {
+      console.warn("SSE connection error", e);
+    });
 
     es.addEventListener("log", (e) => {
🤖 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 `@packages/finicky-ui/src/lib/useSSE.ts` around lines 7 - 30, The EventSource
setup in useSSE currently handles only log, config, and updateInfo events, so
connection failures/reconnect problems are completely silent. Add an error
listener on the EventSource created in useEffect that reports SSE connection
issues to the console or appStore, and keep the existing cleanup with
es.close(). Use the existing symbols useSSE, appStore, and api.eventsUrl() so
the fix stays localized.
packages/finicky-ui/src/pages/TestUrl.module.css (1)

123-129: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Deprecated word-break: break-word value.

Stylelint flags break-word as deprecated for word-break. Both word-break and overflow-wrap are already set here; overflow-wrap: break-word (line 127) covers the wrapping behavior, so the deprecated word-break: break-word can be dropped or replaced.

Proposed fix
 .resultValue {
   color: var(--text-primary);
-  word-break: break-word;
   overflow-wrap: break-word;
   min-width: 0;
 }
🤖 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 `@packages/finicky-ui/src/pages/TestUrl.module.css` around lines 123 - 129, The
.resultValue rule in TestUrl.module.css uses the deprecated word-break:
break-word value. Update the .resultValue styles by removing that declaration or
replacing it with a non-deprecated word-break value, since overflow-wrap:
break-word already handles the wrapping behavior; keep the change localized to
the .resultValue selector.

Source: Linters/SAST tools

packages/finicky-ui/src/pages/TestUrl.tsx (1)

41-41: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Ineffective lint-suppression comment.

// fallow-ignore-next-line complexity isn't a recognized ESLint directive (typo for "follow"/wrong format), so it has no effect and is just dead text.

🤖 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 `@packages/finicky-ui/src/pages/TestUrl.tsx` at line 41, The lint suppression
comment in TestUrl is ineffective because the directive name is misspelled and
not recognized, so it does nothing. Replace the bad comment near the affected
complexity check with the correct ESLint directive syntax that matches the
intended rule, using the nearby TestUrl component/code block as the reference
point.
🤖 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 `@apps/finicky/src/main.go`:
- Line 65: The HTTP handlers and background actions are accessing shared app
state concurrently, which can race on vm, updateInfo, and lastConfigPayload
during config reloads, rule saves, and update checks. Serialize all access in
main.go by routing state mutations and reads through a single app-state
goroutine or protecting the shared globals with a mutex; make sure handlers like
the ones around the VM resolver, config update, and update status return copied
payloads instead of sharing mutable maps or structs directly.
- Around line 164-165: The API server startup path in main.go currently logs the
failure and continues, which leaves GetAPIPort() unusable for the settings UI.
Update the startup flow around window.StartAPIServer() so that a non-nil error
from that call stops execution immediately instead of falling through, and keep
the failure handling localized to this startup block so the React API URL is
only injected when the server has started successfully.

In `@apps/finicky/src/window/server.go`:
- Around line 155-158: The POST /api/rules flow in server.go returns success
before SaveRulesHandler has finished applying the new rules, which can leave vm
stale for follow-up requests. Update the save path around
SaveRulesHandler/getRulesData so the handler is completed and the new rules are
applied before writing the success response, or change the response to an
explicit pending state until the rebuild is done. Use the SaveRulesHandler hook
and the rule reload path in server.go/main.go as the key places to adjust.
- Around line 100-104: The current CORS setup in corsMiddleware exposes the
loopback API to any origin, so tighten access before setting headers. Add a
per-process secret and origin validation in the server flow around
corsMiddleware and the REST/SSE handlers, requiring the secret on requests
before allowing reads or writes. Update the relevant request handling symbols in
server.go so only trusted origins are reflected and unauthorized localhost calls
are rejected.

In `@packages/finicky-ui/src/App.tsx`:
- Around line 36-50: The initial data bootstrap in App.tsx is swallowing
failures in the load() effect, so the UI stays stuck in default state if
api.initialData() rejects. Update the load flow to handle errors explicitly in
the App component: capture the rejection, surface a visible error/loading state
or notification, and add a retry path (for example by retrying in the same
useEffect or exposing a refresh action) so users can recover when initialData()
is temporarily unavailable.

In `@packages/finicky-ui/src/components/BrowserProfileSelector.tsx`:
- Around line 144-155: The ProfileField render gate in BrowserProfileSelector
should not depend only on profileOptions.length, because that hides an
already-saved custom profile when the browser has no loaded profiles. Update the
conditional around ProfileField to also render when custom.profile is true (and
the browser/value.profile exists), so existing custom profile values remain
visible and editable. Keep the change localized to the BrowserProfileSelector
component and preserve the existing handleProfileSelect, onInput, onClear, and
onBlur behavior.

In `@packages/finicky-ui/src/components/Footer.tsx`:
- Line 29: The path text in Footer is using plain double-quoted strings, so
`${config.configPath}` and `${rulesFile.path}` are rendered literally instead of
interpolated. Update the `showPathToast` calls in `Footer.tsx` to use template
literals in the relevant click handlers so the `config.configPath` and
`rulesFile.path` values are inserted correctly.

In `@packages/finicky-ui/src/components/OptionRow.tsx`:
- Around line 44-52: The locked branch in OptionRow currently nests Inner (which
renders a label/input checkbox) inside a real button, creating invalid
interactive nesting. Update OptionRow’s locked rendering so the wrapper is
non-interactive or otherwise restructure Inner to avoid emitting the label/input
wrapper in the locked state, and keep onLockedClick behavior accessible without
using a button around the checkbox markup.
- Around line 16-39: The checkbox in Inner lacks an accessible name because the
visible option text is outside the toggle label. Fix this by giving the input in
OptionRow/Inner an accessible label, such as using the label prop for
aria-label, or by associating the visible optionLabel text with the checkbox so
assistive tech can announce it. Keep the locked and onChange behavior unchanged.

In `@packages/finicky-ui/src/lib/toast.ts`:
- Around line 42-58: The repeated-toast update path in show() is not refreshing
the extra field, so a later call with the same message/type but different
details leaves stale text in the UI. Update the existing-toast branch in show()
so the mapped toast object also replaces extra alongside duration, timeoutId,
and key, ensuring the latest payload is rendered when matching on message and
type.

In `@packages/finicky-ui/src/lib/useLogControls.ts`:
- Around line 29-40: The formatting work in copyLogs is happening before the
try/catch, so a bad log entry from formatEntry can throw outside the error
handler. Move the messageBuffer filtering and .map(formatEntry) logic inside the
try block (or otherwise wrap it in the same guard) so any RangeError from
Date.toISOString is caught and toast.error is shown. Keep the fix localized to
copyLogs and the formatEntry path it uses.

In `@packages/finicky-ui/src/lib/useRulesSave.ts`:
- Around line 10-20: The debounced save in useRulesSave clears isPending too
early, before api.saveRules finishes, creating a race with consumers like
Rules.tsx that rely on the flag. Move the isPending.current = false assignment
so it only happens after the save attempt has fully completed, ideally in a
finally block around the api.saveRules call, while keeping the existing
appStore.update flow in the useRulesSave callback.
- Around line 13-17: The save flow in useRulesSave is dropping fields from the
existing RulesFile because api.saveRules is called with only defaultBrowser,
defaultProfile, and rules. Update the save payload to preserve the current
rulesFile object (including options and any other existing properties) while
replacing only the rules data, and adjust the updated as any handling so the
returned shape matches the RulesFile model instead of masking the mismatch.

In `@packages/finicky-ui/src/pages/Rules.tsx`:
- Around line 52-65: The rule reordering flow in useDragSort and the draggable
rows in Rules.tsx only support mouse drag-and-drop, so add a keyboard-accessible
reorder path as well. Update the Rules page components that render the
reorderable list so users can move items with buttons or arrow-key handling, or
replace the custom drag logic in useDragSort/onDragStart/onDragOver/onDragEnd
with an accessible DnD solution such as `@dnd-kit/core` that includes keyboard
sensors. While touching the list rendering, replace key={i} usage with a stable
item identifier so reordered/editable rows keep consistent identity.
- Line 182: The save payload from Rules.tsx is dropping existing options, so
update the rules save flow to preserve the full RulesFile shape. In
useRulesSave, ensure the object built from the rules state includes options
alongside defaultBrowser, defaultProfile, and rules before it reaches
handleSaveRulesHTTP. Use the existing RulesFile/options fields so edits on the
Rules page do not clear keepRunning, hideIcon, logRequests, or checkForUpdates.
- Around line 193-199: The save guard in Rules.tsx is being cleared too early,
allowing the Rules effect to re-run before api.saveRules settles and overwrite
in-flight edits. Update the save flow around the relevant save handler and
isPending ref so the flag remains set for the full async round-trip, then reset
it in a finally block after api.saveRules completes; this keeps the useEffect
that depends on rulesFile.rules and installedBrowsers blocked until the save is
fully settled.

In `@packages/finicky-ui/src/pages/StartPage.tsx`:
- Around line 100-135: The StartPage resync effect is resetting local state from
rulesFile/config even while there are unsaved edits, which can overwrite pending
user changes before saveDebounced flushes. Add a pending/dirty guard in
StartPage’s useEffect (similar to the one used in Rules.tsx) so it skips the
reset while edits are in progress, and only resyncs when there are no pending
local changes.

In `@packages/finicky-ui/src/pages/TestUrl.tsx`:
- Around line 67-89: The loading state in TestUrl.tsx can be set after the
request has already finished because the loadingTimer runs in parallel with the
debounced request started in useEffect. Update the effect so loading only turns
on when the actual api.testUrl(normalizeUrl(testUrl)) call begins inside the
debounce callback, and remove the separate pre-request timer; keep
setLoading(false) in the finally path and in the invalid-URL early return so
TestUrl stays accurate.

In `@packages/finicky-ui/src/types.ts`:
- Around line 45-50: Update the TestUrlResult type so openInBackground is
optional instead of required, since TestUrl.tsx already defensively checks
typeof result.openInBackground before using it. Locate the TestUrlResult
interface in types.ts and change the field declaration to match the runtime
shape; keep the other properties unchanged so the type accurately reflects
consumer usage.

In `@packages/finicky-ui/tsconfig.app.json`:
- Around line 2-21: Add composite to the referenced TypeScript config so the
build mode works: both tsconfig.app.json and tsconfig.node.json need composite
enabled because tsconfig.json is being built with tsc -b and references them.
Update the compilerOptions in the referenced configs to set composite: true,
keeping the existing build setup in packages/finicky-ui compatible with
tsconfig.json and the tsc -b command.

---

Outside diff comments:
In `@apps/finicky/src/main.go`:
- Around line 468-485: The `lastConfigPayload` block is unconditionally
dereferencing `configInfo` even though `GetConfigState()` can return nil, which
can panic or reuse stale metadata. Update the `main.go` config-building flow so
`configInfo` is only read when it has been freshly populated from
`newVM.GetConfigState()`, and otherwise initialize/reset `lastConfigPayload`
safely from `cs` or skip the payload build until valid config data exists. Use
the `GetConfigState`, `ConfigInfo`, and `lastConfigPayload` symbols to keep the
fix localized and ensure reloads don’t retain old values.

---

Nitpick comments:
In `@apps/finicky/src/rules/rules.go`:
- Around line 39-51: Refactor the MarshalJSON implementation on Rule to avoid
duplicating Rule’s fields in the local RuleAlias struct. Keep the existing
nil-to-empty-slice normalization for Match, but switch to the standard
alias-type pattern in Rule.MarshalJSON so the serialization stays aligned
automatically with Rule if fields are added or tags change later.

In `@packages/finicky-ui/src/components/icons/About.tsx`:
- Around line 1-9: `AboutIcon` duplicates the same SVG markup as `InfoIcon`, so
replace the repeated icon implementation with a shared parameterized component
such as `InfoCircleIcon` and have `AboutIcon` delegate to it with the
appropriate size. Update the icon components in the `AboutIcon`/`InfoIcon` pair
so the circle, line, and dot paths live in one place and only the size prop
varies.

In `@packages/finicky-ui/src/components/LogContent.tsx`:
- Around line 21-22: The LogContent log entry list is using the array index as
the React key, which can cause reconciliation issues when messageBuffer is
trimmed or reordered. Update the key in the LogContent component’s map over
entries to use a stable identifier from each entry, such as entry.time combined
with a fallback index or another unique id if available, so each li remains
consistently associated with the same log item.

In `@packages/finicky-ui/src/components/TabBar.tsx`:
- Around line 32-41: The tab rendering in TabBar is duplicated between the main
tabs and bottomTabs, so extract the shared Link/className/icon/label markup into
a small helper inside TabBar.tsx, such as a renderTab function that takes path,
label, Icon, and optional extra class or badge content. Update both tabs.map and
bottomTabs.map to call the helper so the only differences remain the tabSmall
styling and the error badge.

In `@packages/finicky-ui/src/lib/appStore.ts`:
- Around line 35-36: `getNumErrors` in `appStore` is duplicated at call sites
instead of being reused. Update consumers such as `StartPage.tsx` to call
`appStore.getNumErrors()` rather than re-implementing the
`messageBuffer.filter(...).length` logic inline, so the error-count rule stays
centralized and consistent.

In `@packages/finicky-ui/src/lib/useSSE.ts`:
- Around line 7-30: The EventSource setup in useSSE currently handles only log,
config, and updateInfo events, so connection failures/reconnect problems are
completely silent. Add an error listener on the EventSource created in useEffect
that reports SSE connection issues to the console or appStore, and keep the
existing cleanup with es.close(). Use the existing symbols useSSE, appStore, and
api.eventsUrl() so the fix stays localized.

In `@packages/finicky-ui/src/pages/TestUrl.module.css`:
- Around line 123-129: The .resultValue rule in TestUrl.module.css uses the
deprecated word-break: break-word value. Update the .resultValue styles by
removing that declaration or replacing it with a non-deprecated word-break
value, since overflow-wrap: break-word already handles the wrapping behavior;
keep the change localized to the .resultValue selector.

In `@packages/finicky-ui/src/pages/TestUrl.tsx`:
- Line 41: The lint suppression comment in TestUrl is ineffective because the
directive name is misspelled and not recognized, so it does nothing. Replace the
bad comment near the affected complexity check with the correct ESLint directive
syntax that matches the intended rule, using the nearby TestUrl component/code
block as the reference point.

In `@packages/finicky-ui/vite.config.ts`:
- Around line 7-16: Remove the misleading build.assetsDir setting in
vite.config.ts since the custom rollupOptions.output patterns in the Vite config
(assetFileNames, chunkFileNames, entryFileNames) already control the output
paths and ignore the default assetsDir nesting. Update the build configuration
to keep only the filename pattern overrides, or if a nested assets folder is
desired, incorporate that path directly into the rollupOptions.output patterns
instead of relying on assetsDir.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: 61c7ff13-91b2-41ac-a177-6e62fc1b565f

📥 Commits

Reviewing files that changed from the base of the PR and between d4d62f8 and 1ae0992.

⛔ Files ignored due to path filters (1)
  • packages/finicky-ui/package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (91)
  • .gitignore
  • apps/finicky/src/logger/logger.go
  • apps/finicky/src/main.go
  • apps/finicky/src/rules/rules.go
  • apps/finicky/src/window/server.go
  • apps/finicky/src/window/window.go
  • apps/finicky/src/window/window.h
  • apps/finicky/src/window/window.m
  • packages/finicky-ui/index.html
  • packages/finicky-ui/package.json
  • packages/finicky-ui/src/App.module.css
  • packages/finicky-ui/src/App.svelte
  • packages/finicky-ui/src/App.tsx
  • packages/finicky-ui/src/components/BrowserProfileSelector.module.css
  • packages/finicky-ui/src/components/BrowserProfileSelector.svelte
  • packages/finicky-ui/src/components/BrowserProfileSelector.tsx
  • packages/finicky-ui/src/components/Footer.module.css
  • packages/finicky-ui/src/components/Footer.tsx
  • packages/finicky-ui/src/components/LogContent.module.css
  • packages/finicky-ui/src/components/LogContent.svelte
  • packages/finicky-ui/src/components/LogContent.tsx
  • packages/finicky-ui/src/components/OptionRow.module.css
  • packages/finicky-ui/src/components/OptionRow.svelte
  • packages/finicky-ui/src/components/OptionRow.tsx
  • packages/finicky-ui/src/components/PageContainer.module.css
  • packages/finicky-ui/src/components/PageContainer.svelte
  • packages/finicky-ui/src/components/PageContainer.tsx
  • packages/finicky-ui/src/components/TabBar.module.css
  • packages/finicky-ui/src/components/TabBar.svelte
  • packages/finicky-ui/src/components/TabBar.tsx
  • packages/finicky-ui/src/components/ToastContainer.module.css
  • packages/finicky-ui/src/components/ToastContainer.svelte
  • packages/finicky-ui/src/components/ToastContainer.tsx
  • packages/finicky-ui/src/components/Toggle.svelte
  • packages/finicky-ui/src/components/Tooltip.module.css
  • packages/finicky-ui/src/components/Tooltip.svelte
  • packages/finicky-ui/src/components/Tooltip.tsx
  • packages/finicky-ui/src/components/icons/About.svelte
  • packages/finicky-ui/src/components/icons/About.tsx
  • packages/finicky-ui/src/components/icons/External.svelte
  • packages/finicky-ui/src/components/icons/External.tsx
  • packages/finicky-ui/src/components/icons/Info.svelte
  • packages/finicky-ui/src/components/icons/Info.tsx
  • packages/finicky-ui/src/components/icons/Link.svelte
  • packages/finicky-ui/src/components/icons/Link.tsx
  • packages/finicky-ui/src/components/icons/Lock.svelte
  • packages/finicky-ui/src/components/icons/Lock.tsx
  • packages/finicky-ui/src/components/icons/Logs.svelte
  • packages/finicky-ui/src/components/icons/Logs.tsx
  • packages/finicky-ui/src/components/icons/Preferences.svelte
  • packages/finicky-ui/src/components/icons/Preferences.tsx
  • packages/finicky-ui/src/components/icons/Rules.svelte
  • packages/finicky-ui/src/components/icons/Rules.tsx
  • packages/finicky-ui/src/components/icons/Spinner.svelte
  • packages/finicky-ui/src/components/icons/Spinner.tsx
  • packages/finicky-ui/src/components/icons/Test.svelte
  • packages/finicky-ui/src/components/icons/Test.tsx
  • packages/finicky-ui/src/components/icons/Warning.svelte
  • packages/finicky-ui/src/components/icons/Warning.tsx
  • packages/finicky-ui/src/components/icons/X.svelte
  • packages/finicky-ui/src/components/icons/X.tsx
  • packages/finicky-ui/src/lib/api.ts
  • packages/finicky-ui/src/lib/appStore.ts
  • packages/finicky-ui/src/lib/testUrlStore.ts
  • packages/finicky-ui/src/lib/toast.ts
  • packages/finicky-ui/src/lib/useDebouncedCallback.ts
  • packages/finicky-ui/src/lib/useLogControls.ts
  • packages/finicky-ui/src/lib/useRulesSave.ts
  • packages/finicky-ui/src/lib/useSSE.ts
  • packages/finicky-ui/src/main.ts
  • packages/finicky-ui/src/main.tsx
  • packages/finicky-ui/src/pages/About.module.css
  • packages/finicky-ui/src/pages/About.svelte
  • packages/finicky-ui/src/pages/About.tsx
  • packages/finicky-ui/src/pages/LogViewer.module.css
  • packages/finicky-ui/src/pages/LogViewer.svelte
  • packages/finicky-ui/src/pages/LogViewer.tsx
  • packages/finicky-ui/src/pages/Rules.module.css
  • packages/finicky-ui/src/pages/Rules.svelte
  • packages/finicky-ui/src/pages/Rules.tsx
  • packages/finicky-ui/src/pages/StartPage.module.css
  • packages/finicky-ui/src/pages/StartPage.svelte
  • packages/finicky-ui/src/pages/StartPage.tsx
  • packages/finicky-ui/src/pages/TestUrl.module.css
  • packages/finicky-ui/src/pages/TestUrl.svelte
  • packages/finicky-ui/src/pages/TestUrl.tsx
  • packages/finicky-ui/src/types.ts
  • packages/finicky-ui/src/vite-env.d.ts
  • packages/finicky-ui/svelte.config.js
  • packages/finicky-ui/tsconfig.app.json
  • packages/finicky-ui/vite.config.ts
💤 Files with no reviewable changes (29)
  • packages/finicky-ui/svelte.config.js
  • packages/finicky-ui/src/components/icons/About.svelte
  • packages/finicky-ui/src/main.ts
  • packages/finicky-ui/src/components/icons/Warning.svelte
  • packages/finicky-ui/src/components/icons/Link.svelte
  • packages/finicky-ui/src/components/icons/X.svelte
  • packages/finicky-ui/src/vite-env.d.ts
  • packages/finicky-ui/src/components/OptionRow.svelte
  • packages/finicky-ui/src/components/icons/Logs.svelte
  • packages/finicky-ui/src/components/TabBar.svelte
  • packages/finicky-ui/src/components/LogContent.svelte
  • packages/finicky-ui/src/components/icons/External.svelte
  • packages/finicky-ui/src/pages/Rules.svelte
  • packages/finicky-ui/src/components/ToastContainer.svelte
  • packages/finicky-ui/src/components/icons/Rules.svelte
  • packages/finicky-ui/src/components/icons/Info.svelte
  • packages/finicky-ui/src/components/PageContainer.svelte
  • packages/finicky-ui/src/components/icons/Preferences.svelte
  • packages/finicky-ui/src/pages/LogViewer.svelte
  • packages/finicky-ui/src/components/icons/Test.svelte
  • packages/finicky-ui/src/pages/TestUrl.svelte
  • packages/finicky-ui/src/pages/About.svelte
  • packages/finicky-ui/src/lib/testUrlStore.ts
  • packages/finicky-ui/src/components/icons/Spinner.svelte
  • packages/finicky-ui/src/components/Tooltip.svelte
  • packages/finicky-ui/src/App.svelte
  • packages/finicky-ui/src/pages/StartPage.svelte
  • packages/finicky-ui/src/components/icons/Lock.svelte
  • packages/finicky-ui/src/components/BrowserProfileSelector.svelte

Comment thread apps/finicky/src/main.go Outdated
Comment thread apps/finicky/src/main.go Outdated
Comment thread apps/finicky/src/window/server.go Outdated
Comment thread apps/finicky/src/window/server.go Outdated
Comment thread packages/finicky-ui/src/App.tsx
Comment thread packages/finicky-ui/src/pages/Rules.tsx
Comment thread packages/finicky-ui/src/pages/StartPage.tsx
Comment thread packages/finicky-ui/src/pages/TestUrl.tsx
Comment thread packages/finicky-ui/src/types.ts
Comment on lines 2 to +21
"compilerOptions": {
"target": "ESNext",
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"resolveJsonModule": true,
/**
* Typecheck JS in `.svelte` and `.js` files by default.
* Disable checkJs if you'd like to use dynamic types in JS.
* Note that setting allowJs false does not prevent the use
* of JS in `.svelte` files.
*/
"allowJs": true,
"checkJs": true,
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force"
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"resolveJsonModule": true
},
"include": ["src/**/*.ts", "src/**/*.js", "src/**/*.svelte"]
"include": ["src"]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Inspect root tsconfig.json and tsconfig.node.json for references/composite setup
fd tsconfig.json --full-path packages/finicky-ui | xargs -I{} sh -c 'echo "== {} =="; cat -n {}'
fd tsconfig.node.json --full-path packages/finicky-ui | xargs -I{} sh -c 'echo "== {} =="; cat -n {}'

Repository: johnste/finicky

Length of output: 1167


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== packages/finicky-ui/package.json =="
cat -n packages/finicky-ui/package.json

echo
echo "== packages/finicky-ui/tsconfig.app.json =="
cat -n packages/finicky-ui/tsconfig.app.json

echo
echo "== TypeScript reference docs snippet from installed global docs? =="
python3 - <<'PY'
import os, glob
cands = glob.glob('/usr/local/lib/node_modules/typescript/lib/*.d.ts')
print("typescript lib files found:", len(cands))
PY

Repository: johnste/finicky

Length of output: 1843


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path
p = Path('packages/finicky-ui/package.json')
print(p.exists())
PY

Repository: johnste/finicky

Length of output: 158


Add composite: true to both referenced TS configs
packages/finicky-ui/package.json now runs tsc -b, and packages/finicky-ui/tsconfig.json references both tsconfig.app.json and tsconfig.node.json. Both referenced configs need composite: true, or tsc -b will fail with TS6306.

🤖 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 `@packages/finicky-ui/tsconfig.app.json` around lines 2 - 21, Add composite to
the referenced TypeScript config so the build mode works: both tsconfig.app.json
and tsconfig.node.json need composite enabled because tsconfig.json is being
built with tsc -b and references them. Update the compilerOptions in the
referenced configs to set composite: true, keeping the existing build setup in
packages/finicky-ui compatible with tsconfig.json and the tsc -b command.

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 4

🤖 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 `@apps/finicky/src/main.go`:
- Around line 565-572: The first nil config-state publish can still leave
publishedCI nil, so the payload construction in
publishConfigInfo/publishConfigInfo(nil) needs a nil guard before dereferencing
it. Update the logic around publishConfigInfo and the payload map in main.go so
that if publishConfigInfo(ci) returns nil you skip publishing or fall back
safely before accessing publishedCI. Use the publishConfigInfo symbol and the
payload assembly block to locate the fix.
- Around line 324-336: The VM reload path in main.go is publishing a nil value
before checking setupVM errors, which causes the app to lose the last valid VM
on config reload failure. Update the reload flow around setupVM, setVM, and
handleRuntimeError so the previous good VM remains active when setupErr is
non-nil, and only replace the stored VM after a successful setup; keep the
existing error handling and status updates tied to the failure case.
- Around line 256-259: The rules VM reset path in getVM/setVM handling leaves
lifecycle state stale when rf.DefaultBrowser, rf.Rules, and rf.Options are
cleared. Update the clearing branch in main.go so that when the VM is set to
nil, shouldKeepRunning is also reset to its default nil-VM behavior, rather than
preserving the previous value. Use the getVM(), setVM(nil), and
shouldKeepRunning symbols to locate and update the reset logic.

In `@packages/finicky-ui/src/lib/api.ts`:
- Around line 20-30: The API token handling in API_TOKEN is persisting a
per-process bearer secret into localStorage via the devApiToken key. Update this
flow so the token is taken only from the current query parameter or the injected
__FINICKY_API_TOKEN__ value, and remove the persistent fallback/storage path in
api.ts. If you keep any developer convenience for local testing, gate it so it
is not used in shipped code and does not write the token to persistent storage.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: 643334b4-7712-41e5-a7ac-b30cc6d39f7d

📥 Commits

Reviewing files that changed from the base of the PR and between 1ae0992 and c716e1d.

📒 Files selected for processing (6)
  • apps/finicky/src/main.go
  • apps/finicky/src/window/server.go
  • apps/finicky/src/window/window.go
  • apps/finicky/src/window/window.h
  • apps/finicky/src/window/window.m
  • packages/finicky-ui/src/lib/api.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • apps/finicky/src/window/window.go
  • apps/finicky/src/window/window.m

Comment thread apps/finicky/src/main.go
Comment thread apps/finicky/src/main.go
Comment thread apps/finicky/src/main.go Outdated
Comment thread packages/finicky-ui/src/lib/api.ts Outdated
Comment on lines +20 to +30
const API_TOKEN: string = (() => {
const token = new URLSearchParams(window.location.search).get("apiToken");
if (token) {
localStorage.setItem("devApiToken", token);
return token;
}
return (
(window as any).__FINICKY_API_TOKEN__ ??
localStorage.getItem("devApiToken") ??
""
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Keep the per-process API token out of persistent storage.

Persisting apiToken as devApiToken leaves the loopback API bearer secret readable by any script for this WebView origin and on disk after the process that generated it. Use only the injected token/current query param, or gate any dev-only convenience so it is not shipped persistently.

Suggested change
 const API_TOKEN: string = (() => {
   const token = new URLSearchParams(window.location.search).get("apiToken");
   if (token) {
-    localStorage.setItem("devApiToken", token);
     return token;
   }
   return (
     (window as any).__FINICKY_API_TOKEN__ ??
-    localStorage.getItem("devApiToken") ??
     ""
   );
 })();
📝 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.

Suggested change
const API_TOKEN: string = (() => {
const token = new URLSearchParams(window.location.search).get("apiToken");
if (token) {
localStorage.setItem("devApiToken", token);
return token;
}
return (
(window as any).__FINICKY_API_TOKEN__ ??
localStorage.getItem("devApiToken") ??
""
);
const API_TOKEN: string = (() => {
const token = new URLSearchParams(window.location.search).get("apiToken");
if (token) {
return token;
}
return (
(window as any).__FINICKY_API_TOKEN__ ??
""
);
🧰 Tools
🪛 ast-grep (0.44.0)

[warning] 22-22: Do not store sensitive data (credentials, tokens, PII) in localStorage or sessionStorage; it is readable by any script and persists on the device.
Context: localStorage.setItem("devApiToken", token)
Note: [CWE-312] Cleartext Storage of Sensitive Information.

(local-storage-sensitive-data-typescript)

🤖 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 `@packages/finicky-ui/src/lib/api.ts` around lines 20 - 30, The API token
handling in API_TOKEN is persisting a per-process bearer secret into
localStorage via the devApiToken key. Update this flow so the token is taken
only from the current query parameter or the injected __FINICKY_API_TOKEN__
value, and remove the persistent fallback/storage path in api.ts. If you keep
any developer convenience for local testing, gate it so it is not used in
shipped code and does not write the token to persistent storage.

Source: Linters/SAST tools

@coderabbitai coderabbitai Bot 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.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/finicky-ui/src/lib/api.ts (1)

37-56: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

get/post have no request timeout.

Neither wrapper passes an AbortSignal to fetch, so a hung or unresponsive local API server will leave callers (and the UI) waiting indefinitely with no way to recover. Since this contract change replaces the previous message-passing bridge with HTTP calls across the whole app, add a bounded timeout via AbortSignal.timeout(...).

Proposed fix
 async function get<T>(path: string): Promise<T> {
   const res = await fetch(`${base()}${path}`, {
     headers: { "X-Finicky-Token": API_TOKEN },
+    signal: AbortSignal.timeout(5000),
   });
   if (!res.ok) throw new Error(`GET ${path} failed: ${res.status}`);
   return res.json();
 }

 async function post<T>(path: string, body: unknown): Promise<T> {
   const res = await fetch(`${base()}${path}`, {
     method: "POST",
     headers: {
       "Content-Type": "application/json",
       "X-Finicky-Token": API_TOKEN,
     },
     body: JSON.stringify(body),
+    signal: AbortSignal.timeout(5000),
   });
   if (!res.ok) throw new Error(await res.text());
   return res.json();
 }
🤖 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 `@packages/finicky-ui/src/lib/api.ts` around lines 37 - 56, The get and post
wrappers in api.ts currently call fetch without any timeout, so a stalled local
API can hang the UI indefinitely. Update both get<T> and post<T> to pass an
AbortSignal created with AbortSignal.timeout(...) into fetch, using a bounded
shared timeout value so all HTTP calls fail predictably. Keep the existing
request headers and error handling, but ensure the timeout is applied in both
helper functions.
🤖 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.

Outside diff comments:
In `@packages/finicky-ui/src/lib/api.ts`:
- Around line 37-56: The get and post wrappers in api.ts currently call fetch
without any timeout, so a stalled local API can hang the UI indefinitely. Update
both get<T> and post<T> to pass an AbortSignal created with
AbortSignal.timeout(...) into fetch, using a bounded shared timeout value so all
HTTP calls fail predictably. Keep the existing request headers and error
handling, but ensure the timeout is applied in both helper functions.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 5db87c63-7d79-40c5-a73d-186952a503cb

📥 Commits

Reviewing files that changed from the base of the PR and between c716e1d and 2da05e7.

📒 Files selected for processing (16)
  • apps/finicky/src/main.go
  • apps/finicky/src/window/server.go
  • apps/finicky/src/window/window.go
  • apps/finicky/src/window/window.h
  • apps/finicky/src/window/window.m
  • packages/finicky-ui/src/App.tsx
  • packages/finicky-ui/src/components/BrowserProfileSelector.tsx
  • packages/finicky-ui/src/components/Footer.tsx
  • packages/finicky-ui/src/components/OptionRow.tsx
  • packages/finicky-ui/src/lib/api.ts
  • packages/finicky-ui/src/lib/toast.ts
  • packages/finicky-ui/src/lib/useLogControls.ts
  • packages/finicky-ui/src/lib/useRulesSave.ts
  • packages/finicky-ui/src/pages/StartPage.tsx
  • packages/finicky-ui/src/pages/TestUrl.tsx
  • packages/finicky-ui/src/types.ts
🚧 Files skipped from review as they are similar to previous changes (15)
  • packages/finicky-ui/src/lib/useRulesSave.ts
  • packages/finicky-ui/src/components/OptionRow.tsx
  • packages/finicky-ui/src/App.tsx
  • apps/finicky/src/window/window.h
  • packages/finicky-ui/src/components/Footer.tsx
  • packages/finicky-ui/src/components/BrowserProfileSelector.tsx
  • packages/finicky-ui/src/lib/useLogControls.ts
  • packages/finicky-ui/src/pages/TestUrl.tsx
  • apps/finicky/src/window/server.go
  • packages/finicky-ui/src/types.ts
  • packages/finicky-ui/src/pages/StartPage.tsx
  • apps/finicky/src/window/window.go
  • packages/finicky-ui/src/lib/toast.ts
  • apps/finicky/src/main.go
  • apps/finicky/src/window/window.m

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 1

🧹 Nitpick comments (2)
packages/finicky-ui/src/pages/StartPage.tsx (2)

105-105: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicate error-count logic.

numErrors re-implements the exact same filter that appStore.getNumErrors() already provides (packages/finicky-ui/src/lib/appStore.ts). Prefer calling the store method to keep the definition of "error" in one place.

-  const numErrors = messageBuffer.filter((m) => m.level.toLowerCase() === "error").length;
+  const numErrors = appStore.getNumErrors();
🤖 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 `@packages/finicky-ui/src/pages/StartPage.tsx` at line 105, The StartPage error
count is duplicating logic that already exists in appStore.getNumErrors(), so
keep the definition of “error” centralized. In StartPage.tsx, replace the local
messageBuffer.filter(...).length calculation with a call to the store method,
and use the existing appStore.getNumErrors() helper from appStore.ts instead of
re-implementing the filter inline.

113-119: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicated effect for fetching installed browsers.

This exact effect (including the comment) is duplicated verbatim in Rules.tsx. Consider extracting a shared useInstalledBrowsers() hook so the WebView-reload caveat and fetch logic live in one place.

🤖 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 `@packages/finicky-ui/src/pages/StartPage.tsx` around lines 113 - 119, The
installed-browser fetch effect in StartPage is duplicated verbatim in Rules.tsx,
so move the api.getBrowsers/appStore.update logic and the WebView reload caveat
into a shared useInstalledBrowsers() hook. Update StartPage and Rules to call
that hook instead of keeping separate useEffect blocks. Keep the browser-refresh
behavior and comment in one place so future changes only need to be made once.
🤖 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 `@packages/finicky-ui/src/lib/useRulesSave.ts`:
- Around line 18-29: Concurrent saves in useRulesSave can apply stale results
out of order, so add a monotonic request/version guard around the debounced save
flow. In debounced, save(), and scheduleSave(), track the latest request id
alongside isPending.current, increment it for each save trigger, and in the
async callback only call appStore.update({ rulesFile: updated as any }) if the
response still matches the newest id; ignore older responses and still clear
isPending.current in finally. Use the existing useDebouncedCallback,
api.saveRules, and appStore.update symbols to locate the save path.

---

Nitpick comments:
In `@packages/finicky-ui/src/pages/StartPage.tsx`:
- Line 105: The StartPage error count is duplicating logic that already exists
in appStore.getNumErrors(), so keep the definition of “error” centralized. In
StartPage.tsx, replace the local messageBuffer.filter(...).length calculation
with a call to the store method, and use the existing appStore.getNumErrors()
helper from appStore.ts instead of re-implementing the filter inline.
- Around line 113-119: The installed-browser fetch effect in StartPage is
duplicated verbatim in Rules.tsx, so move the api.getBrowsers/appStore.update
logic and the WebView reload caveat into a shared useInstalledBrowsers() hook.
Update StartPage and Rules to call that hook instead of keeping separate
useEffect blocks. Keep the browser-refresh behavior and comment in one place so
future changes only need to be made once.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: 13dbc05d-0895-469a-b6b8-770f61ace647

📥 Commits

Reviewing files that changed from the base of the PR and between edd9151 and 4f1bcd3.

⛔ Files ignored due to path filters (1)
  • packages/finicky-ui/package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (19)
  • apps/finicky/src/config/vm.go
  • apps/finicky/src/main.go
  • apps/finicky/src/window/server.go
  • packages/finicky-ui/package.json
  • packages/finicky-ui/src/components/BrowserProfileSelector.tsx
  • packages/finicky-ui/src/components/Footer.tsx
  • packages/finicky-ui/src/components/LogContent.tsx
  • packages/finicky-ui/src/components/OptionRow.tsx
  • packages/finicky-ui/src/components/PageContainer.tsx
  • packages/finicky-ui/src/components/TabBar.tsx
  • packages/finicky-ui/src/components/ToastContainer.tsx
  • packages/finicky-ui/src/components/Tooltip.tsx
  • packages/finicky-ui/src/lib/api/client.ts
  • packages/finicky-ui/src/lib/api/index.ts
  • packages/finicky-ui/src/lib/useRulesSave.ts
  • packages/finicky-ui/src/pages/Rules.tsx
  • packages/finicky-ui/src/pages/StartPage.tsx
  • packages/finicky-ui/src/pages/TestUrl.tsx
  • packages/finicky-ui/src/types.ts
🚧 Files skipped from review as they are similar to previous changes (16)
  • packages/finicky-ui/src/lib/api/index.ts
  • packages/finicky-ui/src/components/ToastContainer.tsx
  • packages/finicky-ui/src/components/PageContainer.tsx
  • packages/finicky-ui/src/components/LogContent.tsx
  • packages/finicky-ui/src/components/Tooltip.tsx
  • packages/finicky-ui/src/components/TabBar.tsx
  • packages/finicky-ui/src/types.ts
  • packages/finicky-ui/src/components/Footer.tsx
  • packages/finicky-ui/src/pages/TestUrl.tsx
  • packages/finicky-ui/src/components/OptionRow.tsx
  • packages/finicky-ui/package.json
  • packages/finicky-ui/src/lib/api/client.ts
  • packages/finicky-ui/src/components/BrowserProfileSelector.tsx
  • packages/finicky-ui/src/pages/Rules.tsx
  • apps/finicky/src/window/server.go
  • apps/finicky/src/main.go

Comment on lines +18 to +29
const isPending = useRef(false);

const debounced = useDebouncedCallback(async () => {
try {
const updated = await api.saveRules(buildPayload());
appStore.update({ rulesFile: updated as any });
} catch (err) {
toast.error(errorMessage, err instanceof Error ? err.message : String(err));
} finally {
isPending.current = false;
}
}, delay);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Concurrent flush() calls can race and overwrite newer state with a stale response.

save()/scheduleSave() set isPending.current = true and call debounced.flush()/schedule(), but flush() only clears the pending timer — it does not prevent a second call from starting a new api.saveRules request while a prior one is still in flight (e.g., quick successive blur events on adjacent fields, or drag-sort commit followed immediately by another edit). If the second request's response resolves before the first's, appStore.update({ rulesFile: updated }) from the first (stale) response can run last and clobber the newer state, silently discarding the most recent save's result.

🔒 Proposed fix: ignore stale responses via a monotonic request id
 export function useRulesSave(
   buildPayload: () => Partial<RulesFile>,
   delay: number,
   errorMessage = "Failed to save rules"
 ) {
   const isPending = useRef(false);
+  const requestId = useRef(0);
 
   const debounced = useDebouncedCallback(async () => {
+    const id = ++requestId.current;
     try {
       const updated = await api.saveRules(buildPayload());
-      appStore.update({ rulesFile: updated as any });
+      if (id === requestId.current) {
+        appStore.update({ rulesFile: updated as any });
+      }
     } catch (err) {
       toast.error(errorMessage, err instanceof Error ? err.message : String(err));
     } finally {
       isPending.current = false;
     }
   }, delay);
📝 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.

Suggested change
const isPending = useRef(false);
const debounced = useDebouncedCallback(async () => {
try {
const updated = await api.saveRules(buildPayload());
appStore.update({ rulesFile: updated as any });
} catch (err) {
toast.error(errorMessage, err instanceof Error ? err.message : String(err));
} finally {
isPending.current = false;
}
}, delay);
const isPending = useRef(false);
const requestId = useRef(0);
const debounced = useDebouncedCallback(async () => {
const id = ++requestId.current;
try {
const updated = await api.saveRules(buildPayload());
if (id === requestId.current) {
appStore.update({ rulesFile: updated as any });
}
} catch (err) {
toast.error(errorMessage, err instanceof Error ? err.message : String(err));
} finally {
isPending.current = false;
}
}, delay);
🤖 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 `@packages/finicky-ui/src/lib/useRulesSave.ts` around lines 18 - 29, Concurrent
saves in useRulesSave can apply stale results out of order, so add a monotonic
request/version guard around the debounced save flow. In debounced, save(), and
scheduleSave(), track the latest request id alongside isPending.current,
increment it for each save trigger, and in the async callback only call
appStore.update({ rulesFile: updated as any }) if the response still matches the
newest id; ignore older responses and still clear isPending.current in finally.
Use the existing useDebouncedCallback, api.saveRules, and appStore.update
symbols to locate the save path.

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.

1 participant