Skip to content

UI Rules and lots of things to support it#512

Draft
johnste wants to merge 6 commits intomainfrom
feat/ui-rules
Draft

UI Rules and lots of things to support it#512
johnste wants to merge 6 commits intomainfrom
feat/ui-rules

Conversation

@johnste
Copy link
Copy Markdown
Owner

@johnste johnste commented Mar 15, 2026

Fixes #380

Summary by CodeRabbit

  • New Features

    • Rules-based browser routing via JSON configuration files as an alternative to JavaScript configs
    • Rules editor page with drag-to-reorder URL patterns and browser/profile selection
    • Browser profiles support with per-browser profile selection
    • Auto-detection of installed browsers and their available profiles
    • Safari browser detection
    • Configurable options (keep running, hide icon, etc.) with lock states for JavaScript configs
  • Improvements

    • Debounced file watching for responsive config reloads
    • Enhanced theme colors and UI styling
    • Better app launch handling

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Implements Issue #380 by introducing a separate JSON-backed “rules” configuration that can be edited from the UI, plus plumbing to load/save rules and merge them with an existing JS config (JS handlers take priority).

Changes:

  • Add rules.json model + persistence in the Go app, including conversion into finickyConfig-compatible handlers and VM rebuild when running without JS config.
  • Add a new Rules UI page with browser/profile selection and rule editing (including reordering) and wire it into app navigation and native messaging.
  • Extend config/runtime wiring (resolver, window messaging, installed browsers + profiles, config watcher debounce) to support the new UI configuration flow.

Reviewed changes

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

Show a summary per file
File Description
testdata/rules.json Example rules.json fixture for dev/testing scenarios.
testdata/config.js Example JS config fixture to test precedence vs rules.json.
scripts/test.sh Convenience script to run Go tests from apps/finicky/src.
scripts/dev.sh Dev runner for multiple config/rules scenarios + mock updates.
packages/finicky-ui/src/utils/text.ts Stops exporting internal helper used by log formatting.
packages/finicky-ui/src/types.ts Adds UI types for rules file + exposes isJSConfig in config info.
packages/finicky-ui/src/pages/TestUrl.svelte Persists test URL input via a store and updates styling.
packages/finicky-ui/src/pages/StartPage.svelte Adds editable preferences UI (default browser/profile + options) with JS-config locking.
packages/finicky-ui/src/pages/Rules.svelte New Rules page (edit rules, patterns, drag reorder, save debounce, profiles fetch).
packages/finicky-ui/src/pages/LogViewer.svelte UI tweak (checkbox accent color).
packages/finicky-ui/src/pages/About.svelte Layout/style refresh of About page content.
packages/finicky-ui/src/lib/testUrlStore.ts Adds store for test URL input persistence.
packages/finicky-ui/src/components/TabBar.svelte Adds Rules tab and reorganizes tab groups + nav styling.
packages/finicky-ui/src/components/PageContainer.svelte Allows HTML in descriptions and tweaks layout spacing/typography.
packages/finicky-ui/src/components/OptionRow.svelte New reusable toggle row with locked-state UX.
packages/finicky-ui/src/components/icons/Rules.svelte New Rules icon.
packages/finicky-ui/src/components/icons/Lock.svelte New Lock icon used for readonly/locked controls.
packages/finicky-ui/src/components/BrowserProfileSelector.svelte New browser + optional profile selector used by Rules/Preferences.
packages/finicky-ui/src/App.svelte Wires new routes/messages (rules, installed browsers, profiles) + footer config badges.
packages/finicky-ui/src/app.css Introduces new theme variables and updates overall styling.
apps/finicky/src/window/window.go Adds WebView message handlers for rules CRUD + installed browsers/profiles.
apps/finicky/src/version/version.go Supports FINICKY_MOCK_UPDATE env var for UI/dev testing.
apps/finicky/src/util/directories.go Adds ShortenPath helper for user-facing path display.
apps/finicky/src/rules/rules.go New rules package: load/save rules.json + JSON<->JS config conversion.
apps/finicky/src/rules/rules_test.go Unit tests for rules package behavior.
apps/finicky/src/resolver/resolver.go New resolver that merges cached JSON rules with JS config evaluation.
apps/finicky/src/resolver/resolver_test.go Tests for resolver behavior across no-config/JS-only/JSON-only/merged cases.
apps/finicky/src/main.m Ignores spurious openFile calls during launch (CLI flag paths).
apps/finicky/src/main.h Adds didFinishLaunching state for launch-time filtering.
apps/finicky/src/main.go Adds --rules/--no-config, VM/rules wiring, and switches URL evaluation to resolver.
apps/finicky/src/config/vm.go Refactors VM creation to accept API bytes + adds IsJSConfig + bulk option retrieval.
apps/finicky/src/config/configfiles.go Debounces config file watcher notifications.
apps/finicky/src/browser/launcher.go Adds browser profile enumeration and refactors profile parsing helpers.
apps/finicky/src/browser/detect.go Adds installed browser detection via Launch Services (Cgo/AppKit).
apps/finicky/src/browser/browsers.json Adds Safari entry (type recognized for completeness).

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

Comment on lines 19 to 23
<div class="header-section">
<h2>{title}</h2>
{#if description}
<p class="page-description">{description}</p>
<p class="page-description">{@html description}</p>
{/if}
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

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

{@html description} renders raw HTML. Since description is a plain string prop, this makes it easy for future call sites to accidentally pass user-controlled content and introduce XSS. Consider keeping description as text and adding a separate explicitly-named prop (e.g. descriptionHtml) or restricting/escaping input before rendering as HTML.

Copilot uses AI. Check for mistakes.
@@ -1,3 +1,22 @@
export interface Rule {
match: string[];
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

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

Rule.match is typed as string[], but the backend JSON format supports both a single string and an array (and rules.go will marshal a single pattern as a string). This forces unsafe casts in Rules.svelte and can hide bugs; update the type to string | string[] (or introduce a separate JSON DTO type) and normalize to an array at the UI boundary.

Suggested change
match: string[];
match: string | string[];

Copilot uses AI. Check for mistakes.
Comment on lines +21 to +26
func TestToJSHandlers_SkipsIncompleteRules(t *testing.T) {
rules := []Rule{
{Match: "", Browser: "Firefox"}, // no match
{Match: "example.com", Browser: ""}, // no browser
{Match: "example.com", Browser: "Safari"}, // valid
}
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

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

These tests construct Rule values using Match: "...", but Rule.Match is defined as []string in rules.go. This won’t compile; update the test data to use slices (e.g. Match: []string{"..."}) or adjust the struct/test helpers accordingly.

Copilot uses AI. Check for mistakes.
Comment on lines +33 to +37
func TestToJSHandlers_StringBrowser(t *testing.T) {
rules := []Rule{
{Match: "*github.com/*", Browser: "Firefox"},
}
result := ToJSHandlers(rules)
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

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

Same issue here: Rule.Match is a []string, but the test uses Match: "*github.com/*". Update to []string{"*github.com/*"} (and similarly for other rules in this test file) so it compiles and matches runtime behavior.

Copilot uses AI. Check for mistakes.
Comment on lines +88 to +92
func TestToJSConfigScript_DefaultBrowserFallback(t *testing.T) {
rf := RulesFile{Rules: []Rule{{Match: "example.com", Browser: "Firefox"}}}
script, err := ToJSConfigScript(rf, "finickyConfig")
if err != nil {
t.Fatal(err)
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

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

RulesFile{Rules: []Rule{{Match: "example.com", ...}}} uses Match as a string, but Rule.Match is a []string. This will fail to compile; use Match: []string{"example.com"}.

Copilot uses AI. Check for mistakes.
Comment on lines +136 to +137
{Match: "*github.com/*", Browser: "Google Chrome", Profile: "Personal"},
{Match: "https://linear.app/*", Browser: "Safari"},
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

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

In this round-trip test, rule literals use Match: "..." even though Rule.Match is []string. Update both entries to []string{...} so the test compiles and actually exercises the JSON single-string marshal/unmarshal behavior you want to verify.

Suggested change
{Match: "*github.com/*", Browser: "Google Chrome", Profile: "Personal"},
{Match: "https://linear.app/*", Browser: "Safari"},
{Match: []string{"*github.com/*"}, Browser: "Google Chrome", Profile: "Personal"},
{Match: []string{"https://linear.app/*"}, Browser: "Safari"},

Copilot uses AI. Check for mistakes.
Comment on lines +365 to +373
// Debounce: reset the timer so only the last event in a burst fires.
cfw.debounceMu.Lock()
if cfw.debounceTimer != nil {
cfw.debounceTimer.Stop()
}
notify := cfw.configChangeNotify
cfw.debounceTimer = time.AfterFunc(500*time.Millisecond, func() {
notify <- struct{}{}
})
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

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

The debounce timer callback sends on an unbuffered configChangeNotify channel from a timer goroutine. If the receiver is busy (e.g. resolving/launching a browser), this send can block and potentially accumulate stuck goroutines after successive file events. Consider making the channel buffered (size 1) and/or doing a non-blocking send (select with default) to coalesce bursts safely.

Copilot uses AI. Check for mistakes.
Comment on lines +140 to +165
// Set up rules save handler.
// When there is no JS config, rebuild the VM from the updated rules.
// When there is a JS config, JSON rules are loaded fresh in evaluateURL — nothing to do.
window.SaveRulesHandler = func(rf rules.RulesFile) {
slog.Debug("Rules updated", "count", len(rf.Rules))
resolver.SetCachedRules(rf)
if vm == nil || !vm.IsJSConfig() {
if rf.DefaultBrowser == "" && len(rf.Rules) == 0 {
vm = nil
return
}
script, err := rules.ToJSConfigScript(rf, namespace)
if err != nil {
slog.Error("Failed to generate config from rules", "error", err)
return
}
newVM, err := config.NewFromScript(finickyConfigAPIJS, namespace, script)
if err != nil {
slog.Error("Failed to rebuild VM from rules", "error", err)
return
}
vm = newVM
shouldKeepRunning = vm.GetAllConfigOptions().KeepRunning
go checkForUpdates()
}
}
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

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

window.SaveRulesHandler mutates the global vm (and triggers update checks) from the WebView message handler path, while vm is also read/used concurrently in the main event-loop goroutine (URL resolution and config reload). This introduces a real data race and can lead to crashes or inconsistent config evaluation. Route rule-save events through the existing select loop via a channel (or guard all vm reads/writes with a mutex) so VM rebuilds and shouldKeepRunning updates happen in one place.

Copilot uses AI. Check for mistakes.
@johnste
Copy link
Copy Markdown
Owner Author

johnste commented Apr 7, 2026

@coderabbitai full review

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 7, 2026

✅ Actions performed

Full review triggered.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 7, 2026

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: cf7c00e6-b828-497c-a241-97fdf71b834e

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This pull request introduces a JSON rules-based browser configuration system alongside JavaScript configs. It adds browser detection via macOS Launch Services, profile discovery, URL-to-browser resolution logic, debounced config file watching, and an interactive Rules editor UI with browser/profile selectors, alongside comprehensive test coverage.

Changes

Cohort / File(s) Summary
Browser Detection & Registration
apps/finicky/src/browser/browsers.json, apps/finicky/src/browser/detect.go
Added Safari entry to browser registry and implemented macOS Launch Services-based browser detection with Info.plist introspection for URL scheme support.
Profile Discovery
apps/finicky/src/browser/launcher.go
Refactored Firefox and Chromium profile discovery into reusable helpers; added GetProfilesForBrowser() to query profiles by browser identifier.
Rules System
apps/finicky/src/rules/rules.go, apps/finicky/src/rules/rules_test.go
New package for JSON rule definitions, serialization, disk persistence, and JS config script generation with normalized match patterns and optional profiles.
URL Resolution
apps/finicky/src/resolver/resolver.go, apps/finicky/src/resolver/resolver_test.go
New resolver package that evaluates URLs through JS configs or cached JSON rules, applies URL rewriting, selects browsers/profiles, and handles background open requests.
VM & Config Refactoring
apps/finicky/src/config/vm.go, apps/finicky/src/config/configfiles.go
Changed VM construction from embed.FS to byte-based API content; added config options query interface and debounced config file change notifications via resettable timer.
Main App Logic
apps/finicky/src/main.go, apps/finicky/src/main.h, apps/finicky/src/main.m
Refactored URL handling to use new resolver; added CLI flags for custom rules (-rules) and JS config bypass (-no-config); implemented app launch completion tracking to filter premature file open events.
Utilities & Version
apps/finicky/src/util/directories.go, apps/finicky/src/version/version.go
Added path shortening utility; added mock update environment variable support for testing.
Window Message Handlers
apps/finicky/src/window/window.go
Extended webview message handling for rules I/O (getRules, saveRules), browser querying (getInstalledBrowsers), and profile discovery (getBrowserProfiles).
Frontend Rules Editor
packages/finicky-ui/src/pages/Rules.svelte
New interactive page for editing browser match patterns, browser/profile assignment, with drag-to-reorder support and debounced auto-save.
Frontend Config & Options
packages/finicky-ui/src/pages/StartPage.svelte
Extended with rules file UI section, default browser/profile selector, and reusable option toggles; prevented modifications when JS config is active.
Frontend Components
packages/finicky-ui/src/components/BrowserProfileSelector.svelte, packages/finicky-ui/src/components/OptionRow.svelte, packages/finicky-ui/src/components/PageContainer.svelte
New browser/profile picker and option row components; updated page container styling for code elements.
Frontend Icons
packages/finicky-ui/src/components/icons/Rules.svelte, packages/finicky-ui/src/components/icons/Lock.svelte
Added Rules and Lock icon components.
Frontend App Shell
packages/finicky-ui/src/App.svelte, packages/finicky-ui/src/components/TabBar.svelte, packages/finicky-ui/src/lib/testUrlStore.ts
Added Rules route; restructured tab bar with bottom tabs; added testUrlInput store and rules-related message handlers.
Frontend Styling & Types
packages/finicky-ui/src/app.css, packages/finicky-ui/src/pages/TestUrl.svelte, packages/finicky-ui/src/pages/About.svelte, packages/finicky-ui/src/types.ts, packages/finicky-ui/src/utils/text.ts
Updated dark/light theme color system; added Rule and RulesFile TypeScript interfaces; updated ConfigInfo with isJSConfig flag; minor component styling adjustments.
Test & Dev Scripts
scripts/dev.sh, scripts/test.sh, testdata/config.js, testdata/rules.json
Added dev script with five config scenarios, test runner, and example JS and JSON config files.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant Safari as Safari/Browser
    participant App as Finicky App
    participant Resolver as Resolver
    participant Config as JS Config
    participant Rules as JSON Rules
    participant Browser as Launch Browser
    
    User->>Safari: Opens finicky:// link
    Safari->>App: Calls handleURL
    
    alt JS Config Enabled
        App->>Resolver: ResolveURL(vm, url, opener)
        Resolver->>Config: Evaluate JS handlers
        Config-->>Resolver: Selected browser + profile
    else JSON Rules Only
        App->>Resolver: ResolveURL(nil, url, opener)
        Resolver->>Rules: Check cached rules
        Rules-->>Resolver: Selected browser + profile
    end
    
    Resolver-->>App: BrowserConfig + error
    App->>Browser: Launch with profile
    Browser-->>User: Opens URL
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Poem

🐰 Whiskers twitch with delight,
Rules now dance in JSON light,
Browsers picked, profiles too,
Safari, Chrome—decisions brew!
The Finicky web finds its way,

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning The PR introduces significant changes beyond UI settings editing: a complete rules/JSON config system, browser detection via native code (cgo/AppKit), config prioritization logic, resolver patterns, and file management utilities—substantially expanding scope beyond issue #380's stated objectives. Consider splitting out-of-scope changes into separate PRs: (1) rules system with JSON parsing, (2) native browser detection, (3) config prioritization logic. This would align the PR more closely with issue #380's UI settings editing objective.
Docstring Coverage ⚠️ Warning Docstring coverage is 39.13% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Linked Issues check ✅ Passed The PR implements the core objectives of issue #380: editable UI settings, persistent storage in a separate rules file (distinct from JS config), and UI integration with the configuration system.
Title check ✅ Passed The title accurately summarizes the main change: adding UI support for rules editing and the underlying infrastructure needed to support it across multiple layers (backend rules handling, browser detection, resolver logic, and frontend UI components).

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/ui-rules

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

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 18

Caution

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

⚠️ Outside diff range comments (2)
packages/finicky-ui/src/pages/TestUrl.svelte (1)

61-71: ⚠️ Potential issue | 🟡 Minor

Subscription cleanup not handled on component destroy.

The testUrlResult.subscribe() call at line 62 returns an unsubscribe function that is never called. In Svelte, manual subscriptions outside of $: reactive statements or template $store syntax need explicit cleanup to prevent memory leaks when the component is destroyed.

🛠️ Proposed fix using onDestroy
 <script lang="ts">
+  import { onDestroy } from "svelte";
   import PageContainer from "../components/PageContainer.svelte";
   // ... other imports
 
   // Subscribe to test results
-  testUrlResult.subscribe(() => {
+  const unsubscribe = testUrlResult.subscribe(() => {
     clearTimeout(loadingTimer);
     loading = false;
   });
+
+  onDestroy(() => {
+    unsubscribe();
+    clearTimeout(debounceTimer);
+    clearTimeout(loadingTimer);
+  });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/finicky-ui/src/pages/TestUrl.svelte` around lines 61 - 71, The
subscription to testUrlResult is never cleaned up; capture the unsubscribe
function returned by testUrlResult.subscribe(...) (e.g., const unsubscribe =
testUrlResult.subscribe(...)) and register cleanup in Svelte's onDestroy by
calling unsubscribe() (and also clearTimeout(loadingTimer) if still pending) to
avoid memory leaks; modify the block around testUrlResult.subscribe, referencing
testUrlResult, loadingTimer, and loading, and add an onDestroy import/use to
call unsubscribe() and any timeout cleanup.
apps/finicky/src/main.go (1)

257-287: ⚠️ Potential issue | 🟠 Major

Preserve nil when Launch Services provides no opener.

This always enqueues &opener, so the resolver sees a truthy opener object with empty fields instead of null. JS handlers that distinguish “no opener” from “known opener” will misroute.

Suggested fix
 func HandleURL(url *C.char, name *C.char, bundleId *C.char, path *C.char, windowTitle *C.char, openInBackground C.bool) {
-	var opener resolver.OpenerInfo
+	var opener *resolver.OpenerInfo

 	if name != nil && bundleId != nil && path != nil {
-		opener = resolver.OpenerInfo{
+		opener = &resolver.OpenerInfo{
 			Name:     C.GoString(name),
 			BundleID: C.GoString(bundleId),
 			Path:     C.GoString(path),
 		}
 		if windowTitle != nil {
 			opener.WindowTitle = C.GoString(windowTitle)
@@
 	urlListener <- URLInfo{
 		URL:              urlString,
-		Opener:           &opener,
+		Opener:           opener,
 		OpenInBackground: bool(openInBackground),
 	}
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/finicky/src/main.go` around lines 257 - 287, The code always enqueues a
non-nil pointer to a zero-value resolver.OpenerInfo even when Launch Services
provided no opener; change the local opener to be a pointer (var opener
*resolver.OpenerInfo) and only allocate & populate it inside the existing if
(name != nil && bundleId != nil && path != nil) block (set WindowTitle on that
allocated struct if provided). Then pass opener (which will be nil when no
opener was provided) into URLInfo so JS can distinguish “no opener” from an
empty opener.
🧹 Nitpick comments (13)
packages/finicky-ui/src/types.ts (1)

7-18: Consider reusing ConfigOptions for options shapes.

RulesFile.options and ConfigInfo.options duplicate the same optional fields. Reusing Partial<ConfigOptions> reduces drift risk.

♻️ Suggested refactor
 export interface RulesFile {
   defaultBrowser: string;
   defaultProfile?: string;
-  options?: {
-    keepRunning?: boolean;
-    hideIcon?: boolean;
-    logRequests?: boolean;
-    checkForUpdates?: boolean;
-  };
+  options?: Partial<ConfigOptions>;
   rules: Rule[];
   path?: string;
 }
...
 export interface ConfigInfo {
   configPath: string;
   isJSConfig?: boolean;
   handlers?: number;
   rewrites?: number;
   defaultBrowser?: string;
-  options?: {
-    keepRunning?: boolean;
-    hideIcon?: boolean;
-    logRequests?: boolean;
-    checkForUpdates?: boolean;
-  };
+  options?: Partial<ConfigOptions>;
 }

Also applies to: 67-72

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/finicky-ui/src/types.ts` around lines 7 - 18, RulesFile defines an
inline options shape that duplicates ConfigInfo.options; change both
RulesFile.options and ConfigInfo.options to reuse the existing ConfigOptions
type by declaring them as Partial<ConfigOptions> (or ConfigOptions | undefined
if you prefer full type) to avoid duplicated field definitions—update the
interface references to use Partial<ConfigOptions> and remove the duplicated
inline object shape in RulesFile and the corresponding duplicated definition in
ConfigInfo so the shared ConfigOptions type is the single source of truth.
packages/finicky-ui/src/components/icons/Rules.svelte (1)

1-13: Apply the same SVG accessibility treatment here.

For decorative use, mark this SVG with aria-hidden="true" (and optionally focusable="false").

♿ Suggested tweak
 <svg
   xmlns="http://www.w3.org/2000/svg"
+  aria-hidden="true"
+  focusable="false"
   width="24"
   height="24"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/finicky-ui/src/components/icons/Rules.svelte` around lines 1 - 13,
The Rules.svelte SVG is missing accessibility attributes for decorative icons;
update the <svg> element in Rules.svelte (the SVG with class "lucide
lucide-list-filter") to include aria-hidden="true" and optionally
focusable="false" so it is ignored by assistive technologies when used
decoratively, keeping no change to visual rendering or other attributes.
packages/finicky-ui/src/components/icons/Lock.svelte (1)

1-15: Mark decorative icon SVG as non-accessible.

If this icon is decorative, add aria-hidden="true" (and optionally focusable="false") to avoid extra screen reader noise.

♿ Suggested tweak
 <svg
   xmlns="http://www.w3.org/2000/svg"
+  aria-hidden="true"
+  focusable="false"
   width="16"
   height="16"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/finicky-ui/src/components/icons/Lock.svelte` around lines 1 - 15,
The Lock.svelte SVG is decorative and should be hidden from assistive tech;
update the root <svg> element (the one with class "lucide lucide-lock" in
Lock.svelte) to include aria-hidden="true" and optionally focusable="false" so
screen readers skip it; if this icon ever becomes meaningful, replace those
attributes with an accessible title/role instead.
packages/finicky-ui/src/app.css (1)

1-44: Consider consolidating the two :root blocks.

There are two separate :root blocks (lines 1-15 and 17-44). While valid CSS, this split reduces maintainability. The first block sets base styles and explicit color values, while the second defines CSS custom properties. Consider merging them into a single :root block for clarity.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/finicky-ui/src/app.css` around lines 1 - 44, The file contains two
separate :root blocks—one with base font and color-scheme properties and another
with CSS custom properties (e.g., --bg-primary, --nav-text, --accent-color);
merge them into a single :root block by moving the base declarations
(font-family, line-height, font-weight, color-scheme, color, background-color,
font-synthesis, text-rendering, -webkit-font-smoothing, -moz-osx-font-smoothing)
together with all custom property definitions (the --* variables) and remove the
duplicate :root to improve maintainability and avoid fragmentation.
packages/finicky-ui/src/pages/TestUrl.svelte (1)

216-218: Hard-coded color values reduce theme consistency.

The .hint-message uses hard-coded rgba(245, 188, 28, ...) values instead of theme variables. Consider using var(--log-warning) with appropriate alpha for consistency with the rest of the theme system.

♻️ Suggested fix
   .hint-message {
     display: flex;
     align-items: center;
     gap: 10px;
     padding: 12px 16px;
-    background: rgba(245, 188, 28, 0.06);
-    border: 1px solid rgba(245, 188, 28, 0.25);
+    background: color-mix(in srgb, var(--log-warning) 6%, transparent);
+    border: 1px solid color-mix(in srgb, var(--log-warning) 25%, transparent);
     border-radius: 8px;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/finicky-ui/src/pages/TestUrl.svelte` around lines 216 - 218, Replace
the hard-coded rgba colors in the .hint-message style with the theme variable so
the component uses the shared color token; specifically, update the background
and border that currently use rgba(245, 188, 28, ...) to use var(--log-warning)
with appropriate alpha (e.g., via color-mix() or by using an RGB CSS variable
and applying alpha) in the .hint-message rule inside TestUrl.svelte so the hint
matches the app theme.
packages/finicky-ui/src/components/OptionRow.svelte (2)

120-143: Hard-coded colors in toggle styling reduce theme flexibility.

The toggle slider uses hard-coded colors (#666, #ddd, #111111) instead of CSS variables. This may cause visual inconsistency if the theme changes or in high-contrast modes.

♻️ Suggested theme-aware colors
   .toggle-slider {
     position: absolute;
     cursor: pointer;
     top: 0;
     left: 0;
     right: 0;
     bottom: 0;
-    background-color: `#666`;
+    background-color: var(--text-secondary);
     transition: 0.3s;
     border-radius: 24px;
   }

   .toggle-slider:before {
     position: absolute;
     content: "";
     height: 18px;
     width: 18px;
     left: 3px;
     bottom: 3px;
-    background-color: `#ddd`;
+    background-color: var(--bg-primary);
     transition: 0.3s;
     border-radius: 50%;
   }
   
   .toggle input:checked + .toggle-slider:before {
     transform: translateX(20px);
-    background-color: `#111111`;
+    background-color: var(--bg-primary);
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/finicky-ui/src/components/OptionRow.svelte` around lines 120 - 143,
Replace the three hard-coded color values used in the toggle CSS (.toggle-slider
background-color `#666`, .toggle-slider:before background-color `#ddd`, and
checked .toggle-slider:before background-color `#111111`) with theme-aware CSS
variables (e.g., use existing --toggle-active for the checked track and
add/choose variables such as --toggle-bg and --toggle-thumb for the track and
thumb defaults) so the rules in .toggle-slider, .toggle-slider:before, and
.toggle input:checked + .toggle-slider:before derive colors from variables
instead of fixed hex values; ensure sensible fallback values are provided in the
root or component :root so themes without those variables still render
predictably.

27-28: Verify onchange handler binding syntax for Svelte 5.

In Svelte 5, using {onchange} as a shorthand on native elements binds to the DOM event. However, the prop type is () => void while DOM onchange events pass an Event argument. If onchange is meant to be called on state change, this works, but if consumers expect event data, the signature mismatch could cause confusion.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/finicky-ui/src/components/OptionRow.svelte` around lines 27 - 28,
The current shorthand {onchange} on the input in OptionRow.svelte can mismatch
Svelte 5 DOM event typing: decide whether the exported prop onchange should
receive the DOM Event or be a no-arg callback; if it should receive the Event,
replace the shorthand with an explicit event binding on:change={onchange} and
update the prop type to (e: Event) => void, otherwise wrap the call to drop the
event payload (on:change={() => onchange?.()}) or change the prop name to
something like onToggle to make the no-arg intent explicit; ensure this change
is applied alongside the existing bind:checked and locked usage so consumers get
the expected signature.
packages/finicky-ui/src/pages/Rules.svelte (2)

176-183: Draggable list item missing keyboard accessibility.

The rule rows are draggable via mouse but lack keyboard support for reordering. Users relying on keyboard navigation cannot reorder rules. Consider adding keyboard event handlers (e.g., arrow keys with modifier) or ARIA live regions for accessibility compliance.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/finicky-ui/src/pages/Rules.svelte` around lines 176 - 183, The
draggable rule rows (element with class "rule-row", using dragIndex and handlers
onDragStart/onDragOver/onDragEnd) lack keyboard accessibility; add keyboard
support by making each rule-row focusable (tabindex="0"), manage an
"active/dragging" state via keyboard (e.g., onKeyDown onRuleKeyDown) that starts
a keyboard-reorder mode on a modifier key (Space or Enter) and then moves the
item with ArrowUp/ArrowDown to call the same reorder logic used by
onDragStart/onDragOver (or a shared moveRule(indexFrom, indexTo) helper), update
ARIA attributes (aria-grabbed, role="option" or "listitem",
aria-posinset/aria-setsize) while reordering, and announce changes via a small
ARIA live region when an item is picked up, moved, or dropped so keyboard users
receive feedback; wire cleanup to onKeyUp/Escape to cancel and ensure onDragEnd
behavior is mirrored.

162-167: Inline HTML in description prop bypasses content sanitization.

Using inline HTML with style attributes in the description (line 165) works because it's hardcoded, but this pattern could become a security risk if the description ever accepts dynamic content. Consider using a Svelte component or CSS class instead.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/finicky-ui/src/pages/Rules.svelte` around lines 162 - 167, The
description prop currently embeds inline HTML with a style attribute in the
PageContainer usage (PageContainer, description, isJSConfig) which bypasses
sanitization; replace that inline HTML by creating a small presentational Svelte
component (e.g., JsConfigBadge or DescriptionWithBadge) or add a CSS class
(e.g., .accent-badge) and use a plain text/element slot on PageContainer instead
of passing raw HTML in the description prop; update the PageContainer API (or
its consumer) to accept a description slot or component and revise the
Rules.svelte usage to render <JsConfigBadge/> or a <span class="accent-badge">JS
config is active</span> so styling is done via CSS and no inline style HTML is
injected.
packages/finicky-ui/src/components/BrowserProfileSelector.svelte (2)

204-207: Hardcoded color breaks the CSS variable pattern.

The clear button hover uses a hardcoded color #f44336 while the rest of the component uses CSS custom properties. Consider using a CSS variable for consistency with the design system.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/finicky-ui/src/components/BrowserProfileSelector.svelte` around
lines 204 - 207, The .clear-btn:hover rule in BrowserProfileSelector.svelte uses
a hardcoded color `#f44336`; change it to use the component/design-system CSS
variable (e.g., replace with var(--color-danger) or the project's equivalent
like var(--fg-danger)), and include a sensible fallback (e.g.,
var(--color-danger, `#f44336`)) to preserve behavior if the variable is missing;
update the .clear-btn:hover selector to reference that CSS custom property so
the component follows the existing variable pattern.

82-90: Consider adding accessibility attributes to form controls.

The <input> elements and <select> elements lack associated <label> elements or aria-label attributes. The clear buttons also lack accessible names. This could impact keyboard and screen reader users.

♿ Suggested accessibility improvements
     <input
       class="browser-input"
       type="text"
       placeholder="e.g. Firefox"
+      aria-label="Custom browser name"
       value={browser}
       oninput={handleBrowserInput}
       {disabled}
     />
-    <button class="clear-btn" onclick={handleClearBrowser}>✕</button>
+    <button class="clear-btn" onclick={handleClearBrowser} aria-label="Clear browser">✕</button>

Apply similar changes to the profile input and clear button as well.

Also applies to: 108-116

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/finicky-ui/src/components/BrowserProfileSelector.svelte` around
lines 82 - 90, The form controls in BrowserProfileSelector.svelte lack
accessible names; add explicit labels or aria-label attributes for the text
inputs (the element with class "browser-input" handled by handleBrowserInput and
the profile input referenced around lines 108-116) and for any <select>
controls, and give the clear buttons (the button invoking handleClearBrowser and
the profile clear button) accessible names via aria-label or aria-labelledby;
ensure labels are programmatically associated (for example using <label
for="..."> or matching id+aria-labelledby) so screen readers and keyboard users
can identify each control.
apps/finicky/src/rules/rules.go (2)

69-75: Global customPath is not thread-safe.

The package-level customPath variable is accessed without synchronization. If SetCustomPath and GetPath are ever called concurrently (e.g., during tests with parallel execution), this could lead to data races.

Since this is documented as intended for testing and CLI flags (typically set once at startup), the risk is low, but consider using sync.Once or a mutex if concurrent access becomes possible.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/finicky/src/rules/rules.go` around lines 69 - 75, The package-level
variable customPath is not protected against concurrent access by SetCustomPath
and readers (e.g., GetPath), risking data races; protect reads and writes by
introducing a sync.RWMutex (e.g., customPathMu) and use customPathMu.Lock() /
Unlock() in SetCustomPath and customPathMu.RLock() / RUnlock() around any
GetPath or other readers, ensuring all accesses to customPath are synchronized
(alternatively use sync/atomic for pointer updates or sync.Once if the value is
only set once).

129-141: Consider atomic file writes to prevent corruption.

os.WriteFile writes directly to the target file. If the process is interrupted mid-write (crash, power loss), the file could be left in a corrupted partial state. For user configuration files, an atomic write pattern (write to temp file, then rename) is safer.

🛡️ Suggested atomic write pattern
 func SaveToPath(rf RulesFile, path string) error {
 	if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
 		return err
 	}

 	data, err := json.MarshalIndent(rf, "", "  ")
 	if err != nil {
 		return err
 	}

-	return os.WriteFile(path, data, 0644)
+	// Write to temp file first, then atomically rename
+	tmpPath := path + ".tmp"
+	if err := os.WriteFile(tmpPath, data, 0644); err != nil {
+		return err
+	}
+	return os.Rename(tmpPath, path)
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/finicky/src/rules/rules.go` around lines 129 - 141, SaveToPath currently
writes directly with os.WriteFile which can leave a partial/corrupt file on
interruption; change it to perform an atomic write: after
json.MarshalIndent(rf...), create a temp file in the same directory (use
filepath.Dir(path)), write the data to that temp file, flush and sync the file
(fsync) and then atomically rename the temp file to the target path (os.Rename);
ensure you still call os.MkdirAll before creating the temp file and return any
errors from the temp write/sync/rename steps instead of calling os.WriteFile.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/finicky/src/browser/detect.go`:
- Around line 34-71: The helper getAllHttpsHandlerNames() uses AppKit APIs and
must run on the main thread; update the call site in didReceiveScriptMessage:
(the WKScriptMessageHandler callback) to marshal to the main thread by wrapping
the HandleWebViewMessage(...) invocation in
dispatch_sync(dispatch_get_main_queue(), ^{ ... }) so all
NSWorkspace/NSBundle/NSFileManager work happens on the main queue, or
alternatively perform the main-thread dispatch inside getAllHttpsHandlerNames()
itself to ensure its body runs on the main thread.

In `@apps/finicky/src/config/configfiles.go`:
- Around line 365-374: The debounce timer's callback (created in
cfw.debounceTimer via time.AfterFunc) performs a blocking send to
cfw.configChangeNotify while holding no lock, which lets timer goroutines pile
up and defeats coalescing; change the callback to perform a non-blocking send
(e.g., use a select with case notify <- struct{}{} and a default to drop the
signal if the channel is full) so pending timers won't block, or alternatively
make configChangeNotify a size-1 buffered channel and still use a non-blocking
send to ensure only one coalesced notification is delivered; adjust the timer
callback that references cfw.debounceTimer, cfw.debounceMu, and
configChangeNotify accordingly.

In `@apps/finicky/src/main.go`:
- Around line 147-149: The current early-return sets vm = nil when
rf.DefaultBrowser == "" && len(rf.Rules) == 0 which incorrectly treats
option-only rules files as empty; update the condition in the VM-initialization
logic (the block referencing rf, vm, DefaultBrowser and Rules) to instead detect
whether any persisted settings exist (e.g., check fields like rf.KeepRunning,
rf.HideIcon, rf.LogRequests, rf.CheckForUpdates, etc.) and only set vm = nil
when no rules and no other persisted settings are present; apply the same fix to
the analogous conditional later in the file (the block around the other rf/ vm
check).
- Around line 151-163: After swapping in newVM (from config.NewFromScript) you
must also apply the saved logRequests setting so the logger reflects the rebuilt
config immediately; after setting vm = newVM and updating shouldKeepRunning =
vm.GetAllConfigOptions().KeepRunning, call logger.SetupFile(...) (or the
existing logger initialization routine) with the new VM's logRequests value
(e.g., vm.GetAllConfigOptions().LogRequests or
newVM.GetAllConfigOptions().LogRequests) before launching checkForUpdates() so
the logRequests toggle takes effect without a restart.
- Around line 143-165: The SaveRulesHandler replaces the global vm and other
goroutines (TestURLInternal, the main event loop via resolver.ResolveURL and
evaluateURL) concurrently read/write the same goja.Runtime; make VM access
concurrency-safe by adding a package-level sync.RWMutex (e.g., vmMu) and use
vmMu.Lock() when replacing or mutating vm and vmMu.RLock() when reading/using it
(including in resolver.ResolveURL, evaluateURL, TestURLInternal, and any
main-loop accesses); ensure nil checks and assignments of vm and
shouldKeepRunning are done under the same lock and release promptly, and add
locking around calls that execute JS on the runtime so only one goroutine
accesses goja.Runtime at a time (apply same protection to the other spots noted
in the comment).

In `@apps/finicky/src/main.m`:
- Around line 112-119: The current application:openFile: early-return discards
legitimate pre-launch file-open events; change it to enqueue or handle only true
CLI flag values: add a mutable array property (e.g., pendingOpenFiles) on the
app delegate and when !self.didFinishLaunching, if filename looks like a CLI
flag (e.g., startsWith "-" or equals known flag tokens like "--config") ignore
it, otherwise append the filename to pendingOpenFiles for processing later in
applicationDidFinishLaunching (process and clear pendingOpenFiles there); ensure
application:openFile: no longer unconditionally returns false during launch so
legitimate file-open requests are preserved.

In `@apps/finicky/src/rules/rules.go`:
- Around line 178-199: ToJSConfigScript currently interpolates the namespace
directly into the generated JS ("var %s = ..."), allowing JS injection if
namespace is untrusted; fix by either validating the namespace parameter (in
ToJSConfigScript) against a safe JS identifier regex (e.g.
^[A-Za-z_$][A-Za-z0-9_$]*$) and returning an error for invalid values, or avoid
direct interpolation entirely by using a safe bracket assignment pattern with
the JSON-marshaled namespace (e.g. assign to
window[<json-stringified-namespace>] instead of "var <namespace>") so special
characters cannot break the generated script; update the fmt.Sprintf call that
emits the var assignment accordingly and add tests for both valid and invalid
namespace inputs.

In `@apps/finicky/src/util/directories.go`:
- Around line 27-29: The current prefix check using strings.HasPrefix(path,
home) can spuriously match directories like "/Users/alice2"; update the
condition in the path-shortening logic to only treat path as inside home when it
is exactly equal to home or when it has home followed by a path separator (e.g.
check path == home || strings.HasPrefix(path, home+string(os.PathSeparator))).
Apply this change where the code references path and home in the directories.go
shortening routine so "~" substitution only happens for true home-boundary
matches.

In `@apps/finicky/src/version/version.go`:
- Around line 239-245: The FINICKY_MOCK_UPDATE handling returns URLs by directly
embedding mockVersion which can double-up a leading "v" (e.g., "vv1.2.3");
update the block in the function that reads FINICKY_MOCK_UPDATE so you normalize
the value (e.g., strings.TrimSpace then strings.TrimPrefix(mockVersion, "v") and
TrimPrefix(..., "V")) before composing DownloadUrl and ReleaseUrl while keeping
LatestVersion set to the original or normalized form as desired; modify the code
around the ReleaseInfo construction (the block that sets
HasUpdate/LatestVersion/DownloadUrl/ReleaseUrl) to use the normalized tag string
for the URL fmt.Sprintf calls.

In `@apps/finicky/src/window/window.go`:
- Around line 267-274: handleGetBrowserProfiles silently proceeds when
msg["browser"] is missing or not a string, producing an empty browserName and
confusing output; validate the input and return an error message instead: check
msg["browser"] exists and is a non-empty string at the start of
handleGetBrowserProfiles, and if invalid send an error via SendMessageToWebView
(e.g., event "browserProfilesError" or include an "error" field) rather than
calling browser.GetProfilesForBrowser; reference the function
handleGetBrowserProfiles and the calls to browser.GetProfilesForBrowser and
SendMessageToWebView when making the change.
- Around line 223-260: The handler handleSaveRules currently logs errors from
rules.Save(rf) but never notifies the UI; modify the error branch after
rules.Save(rf) to send a failure message back to the webview (using
SendMessageToWebView) containing a status/error string (e.g.,
{"status":"error","error": err.Error()}) so the UI can show feedback, and keep
the existing success path (sending rulesResponse and invoking SaveRulesHandler)
unchanged; reference the rules.Save call, the SendMessageToWebView function, and
the rules.GetPath/SaveRulesHandler flow when implementing the error response.

In `@packages/finicky-ui/src/App.svelte`:
- Around line 104-107: The startup sendMessage calls may run before the webkit
message handler is ready; wrap the calls to window.finicky.sendMessage({ type:
"getRules" }) and window.finicky.sendMessage({ type: "getInstalledBrowsers" })
in a Svelte onMount handler (import onMount from "svelte") and/or add a
readiness check (e.g., guard that window.webkit?.messageHandlers?.finicky or
window.finicky exists) before sending so messages are deferred until the bridge
is available; update App.svelte to perform the sendMessage calls inside onMount
with the readiness check to avoid silent failures.
- Around line 153-160: Replace the Svelte 4 event syntax on native buttons with
standard DOM attributes for Svelte 5: change the two occurrences using on:click
that call showPathToast (the button rendering basename(config.configPath) and
the one rendering basename(rulesFile.path)) to use onclick instead, preserving
the same inline arrow function arguments (basename(...), the label
"Configuration loaded from", and the full path argument) and keep the
conditional blocks and className intact.

In `@packages/finicky-ui/src/components/PageContainer.svelte`:
- Around line 21-22: The component currently injects raw HTML via the
description prop using {`@html` description}; change the public API to make this a
consciously trusted sink by introducing a new prop name or slot (e.g.,
descriptionHtml prop or a named <slot name="description">) and stop calling
{`@html` description}. Update PageContainer.svelte to render plain text for the
existing description prop (e.g., {description}) and only use {`@html`
descriptionHtml} (or render the named slot) so the intent is explicit, and
update callers like Rules.svelte to pass hardcoded HTML into the new
descriptionHtml prop or into the named slot instead of the original description
prop.

In `@packages/finicky-ui/src/pages/Rules.svelte`:
- Around line 46-61: The debounce timer set by scheduleSave() (saveTimer) isn't
cleared on component teardown; add a Svelte onDestroy handler that calls
clearTimeout(saveTimer) (and clears or nulls saveTimer and pendingSave if
desired) to prevent the timeout from firing after the component is unmounted;
locate scheduleSave, saveTimer, and SAVE_DEBOUNCE in the file and register
onDestroy to perform the cleanup.
- Around line 127-131: The $effect block that calls window.finicky.sendMessage({
type: "getRules" }) and window.finicky.sendMessage({ type:
"getInstalledBrowsers" }) is being used for mount-only behavior but will re-run
on reactive updates; replace it with Svelte's onMount (import onMount from
"svelte") and move those sendMessage calls into an onMount callback (or
alternatively wrap them in untrack) so the messages are sent only once on
component mount; update the block that currently uses $effect to call onMount(()
=> { ... }) referencing the same window.finicky.sendMessage calls.

In `@packages/finicky-ui/src/pages/StartPage.svelte`:
- Around line 38-51: The reactive sync block currently assigns
defaultBrowserIsCustom and therefore depends on installedBrowsers, causing
backend updates to rerun the block and overwrite local edits; remove the
defaultBrowserIsCustom assignment from the big $: block (the block that sets
keepRunning, hideIcon, logRequests, checkForUpdates, defaultBrowser,
defaultProfile) and add a separate reactive statement like the existing
defaultProfileIsCustom one that computes defaultBrowserIsCustom = defaultBrowser
!== "" && !installedBrowsers.includes(defaultBrowser), so only that derived flag
reacts to installedBrowsers while defaultBrowser/defaultProfile stay stable
(references: defaultBrowserIsCustom, installedBrowsers, defaultProfileIsCustom,
profilesByBrowser, defaultBrowser, defaultProfile).

In `@scripts/dev.sh`:
- Around line 48-80: Default the scenario selector to "4" (both) before the case
block so an empty $1 runs scenario 4; modify the code around the existing case
"$1" in block (and the scenario labels 0|normal .. 4|both) to use a
pre-initialized value instead of raw $1 — e.g., if $1 is empty set a local
variable or reset positional parameters so the case tests a non-empty selector
(default "4") and ensure existing flags like --mock-update remain honored;
update references from case "$1" in to case "$SCENARIO" in (or set -- 4 ...) so
./scripts/dev.sh without args executes the "4|both" branch.

---

Outside diff comments:
In `@apps/finicky/src/main.go`:
- Around line 257-287: The code always enqueues a non-nil pointer to a
zero-value resolver.OpenerInfo even when Launch Services provided no opener;
change the local opener to be a pointer (var opener *resolver.OpenerInfo) and
only allocate & populate it inside the existing if (name != nil && bundleId !=
nil && path != nil) block (set WindowTitle on that allocated struct if
provided). Then pass opener (which will be nil when no opener was provided) into
URLInfo so JS can distinguish “no opener” from an empty opener.

In `@packages/finicky-ui/src/pages/TestUrl.svelte`:
- Around line 61-71: The subscription to testUrlResult is never cleaned up;
capture the unsubscribe function returned by testUrlResult.subscribe(...) (e.g.,
const unsubscribe = testUrlResult.subscribe(...)) and register cleanup in
Svelte's onDestroy by calling unsubscribe() (and also clearTimeout(loadingTimer)
if still pending) to avoid memory leaks; modify the block around
testUrlResult.subscribe, referencing testUrlResult, loadingTimer, and loading,
and add an onDestroy import/use to call unsubscribe() and any timeout cleanup.

---

Nitpick comments:
In `@apps/finicky/src/rules/rules.go`:
- Around line 69-75: The package-level variable customPath is not protected
against concurrent access by SetCustomPath and readers (e.g., GetPath), risking
data races; protect reads and writes by introducing a sync.RWMutex (e.g.,
customPathMu) and use customPathMu.Lock() / Unlock() in SetCustomPath and
customPathMu.RLock() / RUnlock() around any GetPath or other readers, ensuring
all accesses to customPath are synchronized (alternatively use sync/atomic for
pointer updates or sync.Once if the value is only set once).
- Around line 129-141: SaveToPath currently writes directly with os.WriteFile
which can leave a partial/corrupt file on interruption; change it to perform an
atomic write: after json.MarshalIndent(rf...), create a temp file in the same
directory (use filepath.Dir(path)), write the data to that temp file, flush and
sync the file (fsync) and then atomically rename the temp file to the target
path (os.Rename); ensure you still call os.MkdirAll before creating the temp
file and return any errors from the temp write/sync/rename steps instead of
calling os.WriteFile.

In `@packages/finicky-ui/src/app.css`:
- Around line 1-44: The file contains two separate :root blocks—one with base
font and color-scheme properties and another with CSS custom properties (e.g.,
--bg-primary, --nav-text, --accent-color); merge them into a single :root block
by moving the base declarations (font-family, line-height, font-weight,
color-scheme, color, background-color, font-synthesis, text-rendering,
-webkit-font-smoothing, -moz-osx-font-smoothing) together with all custom
property definitions (the --* variables) and remove the duplicate :root to
improve maintainability and avoid fragmentation.

In `@packages/finicky-ui/src/components/BrowserProfileSelector.svelte`:
- Around line 204-207: The .clear-btn:hover rule in
BrowserProfileSelector.svelte uses a hardcoded color `#f44336`; change it to use
the component/design-system CSS variable (e.g., replace with var(--color-danger)
or the project's equivalent like var(--fg-danger)), and include a sensible
fallback (e.g., var(--color-danger, `#f44336`)) to preserve behavior if the
variable is missing; update the .clear-btn:hover selector to reference that CSS
custom property so the component follows the existing variable pattern.
- Around line 82-90: The form controls in BrowserProfileSelector.svelte lack
accessible names; add explicit labels or aria-label attributes for the text
inputs (the element with class "browser-input" handled by handleBrowserInput and
the profile input referenced around lines 108-116) and for any <select>
controls, and give the clear buttons (the button invoking handleClearBrowser and
the profile clear button) accessible names via aria-label or aria-labelledby;
ensure labels are programmatically associated (for example using <label
for="..."> or matching id+aria-labelledby) so screen readers and keyboard users
can identify each control.

In `@packages/finicky-ui/src/components/icons/Lock.svelte`:
- Around line 1-15: The Lock.svelte SVG is decorative and should be hidden from
assistive tech; update the root <svg> element (the one with class "lucide
lucide-lock" in Lock.svelte) to include aria-hidden="true" and optionally
focusable="false" so screen readers skip it; if this icon ever becomes
meaningful, replace those attributes with an accessible title/role instead.

In `@packages/finicky-ui/src/components/icons/Rules.svelte`:
- Around line 1-13: The Rules.svelte SVG is missing accessibility attributes for
decorative icons; update the <svg> element in Rules.svelte (the SVG with class
"lucide lucide-list-filter") to include aria-hidden="true" and optionally
focusable="false" so it is ignored by assistive technologies when used
decoratively, keeping no change to visual rendering or other attributes.

In `@packages/finicky-ui/src/components/OptionRow.svelte`:
- Around line 120-143: Replace the three hard-coded color values used in the
toggle CSS (.toggle-slider background-color `#666`, .toggle-slider:before
background-color `#ddd`, and checked .toggle-slider:before background-color
`#111111`) with theme-aware CSS variables (e.g., use existing --toggle-active
for the checked track and add/choose variables such as --toggle-bg and
--toggle-thumb for the track and thumb defaults) so the rules in .toggle-slider,
.toggle-slider:before, and .toggle input:checked + .toggle-slider:before derive
colors from variables instead of fixed hex values; ensure sensible fallback
values are provided in the root or component :root so themes without those
variables still render predictably.
- Around line 27-28: The current shorthand {onchange} on the input in
OptionRow.svelte can mismatch Svelte 5 DOM event typing: decide whether the
exported prop onchange should receive the DOM Event or be a no-arg callback; if
it should receive the Event, replace the shorthand with an explicit event
binding on:change={onchange} and update the prop type to (e: Event) => void,
otherwise wrap the call to drop the event payload (on:change={() =>
onchange?.()}) or change the prop name to something like onToggle to make the
no-arg intent explicit; ensure this change is applied alongside the existing
bind:checked and locked usage so consumers get the expected signature.

In `@packages/finicky-ui/src/pages/Rules.svelte`:
- Around line 176-183: The draggable rule rows (element with class "rule-row",
using dragIndex and handlers onDragStart/onDragOver/onDragEnd) lack keyboard
accessibility; add keyboard support by making each rule-row focusable
(tabindex="0"), manage an "active/dragging" state via keyboard (e.g., onKeyDown
onRuleKeyDown) that starts a keyboard-reorder mode on a modifier key (Space or
Enter) and then moves the item with ArrowUp/ArrowDown to call the same reorder
logic used by onDragStart/onDragOver (or a shared moveRule(indexFrom, indexTo)
helper), update ARIA attributes (aria-grabbed, role="option" or "listitem",
aria-posinset/aria-setsize) while reordering, and announce changes via a small
ARIA live region when an item is picked up, moved, or dropped so keyboard users
receive feedback; wire cleanup to onKeyUp/Escape to cancel and ensure onDragEnd
behavior is mirrored.
- Around line 162-167: The description prop currently embeds inline HTML with a
style attribute in the PageContainer usage (PageContainer, description,
isJSConfig) which bypasses sanitization; replace that inline HTML by creating a
small presentational Svelte component (e.g., JsConfigBadge or
DescriptionWithBadge) or add a CSS class (e.g., .accent-badge) and use a plain
text/element slot on PageContainer instead of passing raw HTML in the
description prop; update the PageContainer API (or its consumer) to accept a
description slot or component and revise the Rules.svelte usage to render
<JsConfigBadge/> or a <span class="accent-badge">JS config is active</span> so
styling is done via CSS and no inline style HTML is injected.

In `@packages/finicky-ui/src/pages/TestUrl.svelte`:
- Around line 216-218: Replace the hard-coded rgba colors in the .hint-message
style with the theme variable so the component uses the shared color token;
specifically, update the background and border that currently use rgba(245, 188,
28, ...) to use var(--log-warning) with appropriate alpha (e.g., via color-mix()
or by using an RGB CSS variable and applying alpha) in the .hint-message rule
inside TestUrl.svelte so the hint matches the app theme.

In `@packages/finicky-ui/src/types.ts`:
- Around line 7-18: RulesFile defines an inline options shape that duplicates
ConfigInfo.options; change both RulesFile.options and ConfigInfo.options to
reuse the existing ConfigOptions type by declaring them as
Partial<ConfigOptions> (or ConfigOptions | undefined if you prefer full type) to
avoid duplicated field definitions—update the interface references to use
Partial<ConfigOptions> and remove the duplicated inline object shape in
RulesFile and the corresponding duplicated definition in ConfigInfo so the
shared ConfigOptions type is the single source of truth.
🪄 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: 2f612b7e-b0cc-479b-97d3-91cd15fa65fd

📥 Commits

Reviewing files that changed from the base of the PR and between 08eb1d3 and 213bf35.

📒 Files selected for processing (35)
  • apps/finicky/src/browser/browsers.json
  • apps/finicky/src/browser/detect.go
  • apps/finicky/src/browser/launcher.go
  • apps/finicky/src/config/configfiles.go
  • apps/finicky/src/config/vm.go
  • apps/finicky/src/main.go
  • apps/finicky/src/main.h
  • apps/finicky/src/main.m
  • apps/finicky/src/resolver/resolver.go
  • apps/finicky/src/resolver/resolver_test.go
  • apps/finicky/src/rules/rules.go
  • apps/finicky/src/rules/rules_test.go
  • apps/finicky/src/util/directories.go
  • apps/finicky/src/version/version.go
  • apps/finicky/src/window/window.go
  • packages/finicky-ui/src/App.svelte
  • packages/finicky-ui/src/app.css
  • packages/finicky-ui/src/components/BrowserProfileSelector.svelte
  • packages/finicky-ui/src/components/OptionRow.svelte
  • packages/finicky-ui/src/components/PageContainer.svelte
  • packages/finicky-ui/src/components/TabBar.svelte
  • packages/finicky-ui/src/components/icons/Lock.svelte
  • packages/finicky-ui/src/components/icons/Rules.svelte
  • packages/finicky-ui/src/lib/testUrlStore.ts
  • packages/finicky-ui/src/pages/About.svelte
  • packages/finicky-ui/src/pages/LogViewer.svelte
  • packages/finicky-ui/src/pages/Rules.svelte
  • packages/finicky-ui/src/pages/StartPage.svelte
  • packages/finicky-ui/src/pages/TestUrl.svelte
  • packages/finicky-ui/src/types.ts
  • packages/finicky-ui/src/utils/text.ts
  • scripts/dev.sh
  • scripts/test.sh
  • testdata/config.js
  • testdata/rules.json

Comment on lines +34 to +71
static char **getAllHttpsHandlerNames(int *count) {
@autoreleasepool {
NSURL *url = [NSURL URLWithString:@"https://example.com"];
NSArray<NSURL *> *appURLs = [[NSWorkspace sharedWorkspace] URLsForApplicationsToOpenURL:url];
if (!appURLs || appURLs.count == 0) {
*count = 0;
return NULL;
}

NSMutableSet<NSString *> *seen = [NSMutableSet set];
NSMutableArray<NSString *> *names = [NSMutableArray array];
NSSet *excludedBundleIDs = [NSSet setWithObjects:
@"se.johnste.finicky",
@"net.kassett.finicky",
nil];

for (NSURL *appURL in appURLs) {
NSBundle *bundle = [NSBundle bundleWithURL:appURL];
if ([excludedBundleIDs containsObject:bundle.bundleIdentifier]) continue;
if (!isLikelyBrowser(appURL)) continue;

NSString *name = [[NSFileManager defaultManager] displayNameAtPath:[appURL path]];
if ([name hasSuffix:@".app"]) {
name = [name substringToIndex:[name length] - 4];
}
if (![seen containsObject:name]) {
[seen addObject:name];
[names addObject:name];
}
}

*count = (int)names.count;
char **result = (char **)malloc(names.count * sizeof(char *));
for (NSInteger i = 0; i < (NSInteger)names.count; i++) {
result[i] = strdup([names[i] UTF8String]);
}
return result;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

find . -type f -name "*.go" | head -20

Repository: johnste/finicky

Length of output: 794


🏁 Script executed:

fd -t f "window\.go" --type f

Repository: johnste/finicky

Length of output: 92


🏁 Script executed:

fd -t f "detect\." --type f

Repository: johnste/finicky

Length of output: 93


🏁 Script executed:

git ls-files | grep -E "(browser|window)" | head -30

Repository: johnste/finicky

Length of output: 681


🏁 Script executed:

file apps/finicky/src/browser/detect.go

Repository: johnste/finicky

Length of output: 103


🏁 Script executed:

cat -n apps/finicky/src/browser/detect.go

Repository: johnste/finicky

Length of output: 3794


🏁 Script executed:

grep -n "GetInstalledBrowsers" apps/finicky/src/browser/*.go

Repository: johnste/finicky

Length of output: 249


🏁 Script executed:

sed -n '140,180p' apps/finicky/src/window/window.go

Repository: johnste/finicky

Length of output: 1093


🏁 Script executed:

sed -n '260,270p' apps/finicky/src/window/window.go

Repository: johnste/finicky

Length of output: 423


🏁 Script executed:

rg "HandleWebViewMessage" --type go --type m --type h -B 5 -A 5

Repository: johnste/finicky

Length of output: 84


🏁 Script executed:

grep -n "window.h\|window.m\|bridge\|WebView" apps/finicky/src/window/window.m | head -40

Repository: johnste/finicky

Length of output: 1446


🏁 Script executed:

sed -n '1,100p' apps/finicky/src/window/window.m

Repository: johnste/finicky

Length of output: 3711


🏁 Script executed:

rg "GetInstalledBrowsers\|dispatch.*main" apps/finicky/src/

Repository: johnste/finicky

Length of output: 41


🏁 Script executed:

grep -n "didReceiveScriptMessage\|WKScriptMessageHandler" apps/finicky/src/window.m

Repository: johnste/finicky

Length of output: 117


🏁 Script executed:

sed -n '165,195p' apps/finicky/src/window.m

Repository: johnste/finicky

Length of output: 127


🏁 Script executed:

rg "GetInstalledBrowsers\|dispatch.*main" apps/finicky/src/

Repository: johnste/finicky

Length of output: 41


🏁 Script executed:

grep -n "didReceiveScriptMessage\|WKScriptMessageHandler" apps/finicky/src/window/window.m

Repository: johnste/finicky

Length of output: 121


🏁 Script executed:

sed -n '165,195p' apps/finicky/src/window/window.m

Repository: johnste/finicky

Length of output: 1371


🌐 Web query:

WKScriptMessageHandler didReceiveScriptMessage main thread macOS documentation

💡 Result:

Apple’s WKScriptMessageHandler callback (userContentController(_:didReceive:) / Obj‑C userContentController:didReceiveScriptMessage:) is documented as being invoked when a script message is received, but the documentation does not promise it runs on the main thread. [1]

In practice, WebKit may call it off the main thread depending on how your handler is structured; for example, developers have observed it being on the main thread when the handler is a UIResponder subclass (iOS) but on “random threads” when it’s a plain NSObject, and the recommended fix is to force main-actor/main-thread handling (e.g., @MainActor or DispatchQueue.main.async). [2]

What to do (macOS included):

  • If you touch UI/AppKit from the callback, hop to the main thread/main actor yourself.
  • Swift (safe default):
    extension MyHandler: WKScriptMessageHandler {
      `@MainActor`
      func userContentController(_ userContentController: WKUserContentController,
                                 didReceive message: WKScriptMessage) {
        // UI-safe work here
      }
    }
    (Or wrap UI work in DispatchQueue.main.async { ... }.) [2]

Sources

  1. Apple-doc mirror showing the official description/signature (no thread guarantee stated). [1]
  2. Observed non-main-thread delivery + @MainActor workaround discussion. [2]

🏁 Script executed:

rg "GetInstalledBrowsers" --type go -B 3 -A 3

Repository: johnste/finicky

Length of output: 1491


🏁 Script executed:

rg "NSWorkspace\|NSBundle\|NSFileManager" apps/finicky/src/browser/detect.go

Repository: johnste/finicky

Length of output: 41


Run the browser enumeration on the main thread.

The Objective-C helper getAllHttpsHandlerNames() touches NSWorkspace, NSBundle, and NSFileManager—all AppKit APIs that require main-thread access. The call path from window/window.m line 173 (didReceiveScriptMessage:) invokes this code without marshalling to the main thread first. WKScriptMessageHandler callbacks are not guaranteed to run on the main thread, which makes this undefined behavior. This can cause crashes or intermittent browser detection failures.

Add dispatch_sync(dispatch_get_main_queue(), ^{ ... }) around the HandleWebViewMessage() call in the didReceiveScriptMessage: handler, or move the AppKit work onto the main thread inside the Objective-C helper itself.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/finicky/src/browser/detect.go` around lines 34 - 71, The helper
getAllHttpsHandlerNames() uses AppKit APIs and must run on the main thread;
update the call site in didReceiveScriptMessage: (the WKScriptMessageHandler
callback) to marshal to the main thread by wrapping the
HandleWebViewMessage(...) invocation in dispatch_sync(dispatch_get_main_queue(),
^{ ... }) so all NSWorkspace/NSBundle/NSFileManager work happens on the main
queue, or alternatively perform the main-thread dispatch inside
getAllHttpsHandlerNames() itself to ensure its body runs on the main thread.

Comment on lines +365 to +374
// Debounce: reset the timer so only the last event in a burst fires.
cfw.debounceMu.Lock()
if cfw.debounceTimer != nil {
cfw.debounceTimer.Stop()
}
notify := cfw.configChangeNotify
cfw.debounceTimer = time.AfterFunc(500*time.Millisecond, func() {
notify <- struct{}{}
})
cfw.debounceMu.Unlock()
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify unbuffered channel producer/consumer relationship and debounce send points.
rg -nP 'configChange\s*:=\s*make\(chan struct{}\)' apps/finicky/src/main.go
rg -nP 'time\.AfterFunc|configChangeNotify <- struct{}\{\}|notify <- struct{}\{\}' apps/finicky/src/config/configfiles.go

Repository: johnste/finicky

Length of output: 240


🏁 Script executed:

# Find all usages of configChangeNotify to understand initialization and consumption
rg -n 'configChangeNotify' apps/finicky/src/config/configfiles.go apps/finicky/src/main.go -B 2 -A 2

Repository: johnste/finicky

Length of output: 1970


🏁 Script executed:

# Check the main loop to see how it consumes the configChange channel
rg -n 'select' apps/finicky/src/main.go -B 5 -A 5

Repository: johnste/finicky

Length of output: 265


🏁 Script executed:

# Verify if there are other goroutines that might block the main loop
rg -n 'go func\(\)' apps/finicky/src/main.go -A 3 | head -50

Repository: johnste/finicky

Length of output: 322


🏁 Script executed:

# Get the full select statement in main.go to see all channel cases
sed -n '175,200p' apps/finicky/src/main.go

Repository: johnste/finicky

Length of output: 790


🏁 Script executed:

# Find where configChange is created and check how many receivers are on it
sed -n '110,130p' apps/finicky/src/main.go

Repository: johnste/finicky

Length of output: 631


🏁 Script executed:

# Check if there are multiple sends to configChange/configChangeNotify
rg -n '<-.*configChange|configChange.*<-' apps/finicky/src/main.go

Repository: johnste/finicky

Length of output: 86


🏁 Script executed:

# Get the complete select statement in the URL listener goroutine
sed -n '180,225p' apps/finicky/src/main.go

Repository: johnste/finicky

Length of output: 1456


🏁 Script executed:

# Check if there's a context deadline or timeout mechanism that could prevent blocking
rg -n 'context\.|timeout|Context' apps/finicky/src/main.go | head -20

Repository: johnste/finicky

Length of output: 306


🏁 Script executed:

# Check ResolveURL implementation to understand if it's a blocking operation
rg -n 'func.*ResolveURL' apps/finicky/src/ -A 20 | head -40

Repository: johnste/finicky

Length of output: 2738


🏁 Script executed:

# Check LaunchBrowser to see if it blocks
rg -n 'func.*LaunchBrowser' apps/finicky/src/ -A 10 | head -30

Repository: johnste/finicky

Length of output: 831


🏁 Script executed:

# Verify the debounce logic: are there multiple debounceTimer.Stop() calls?
sed -n '355,380p' apps/finicky/src/config/configfiles.go

Repository: johnste/finicky

Length of output: 812


Debounce callback can block and defeat event coalescing.

The timer goroutine sends to configChangeNotify on line 372. With an unbuffered channel, this send blocks if the main loop is busy processing a URL (lines 182–199 in main.go call ResolveURL and LaunchBrowser, which are synchronous operations). When file changes occur in rapid succession, multiple blocked timer goroutines accumulate waiting to send, defeating the debounce's purpose of producing a single coalesced notification.

💡 Safer coalescing pattern
  notify := cfw.configChangeNotify
  cfw.debounceTimer = time.AfterFunc(500*time.Millisecond, func() {
-   notify <- struct{}{}
+   select {
+   case notify <- struct{}{}:
+   default:
+     // Drop if a notification is already pending/being handled.
+   }
  })
📝 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
// Debounce: reset the timer so only the last event in a burst fires.
cfw.debounceMu.Lock()
if cfw.debounceTimer != nil {
cfw.debounceTimer.Stop()
}
notify := cfw.configChangeNotify
cfw.debounceTimer = time.AfterFunc(500*time.Millisecond, func() {
notify <- struct{}{}
})
cfw.debounceMu.Unlock()
// Debounce: reset the timer so only the last event in a burst fires.
cfw.debounceMu.Lock()
if cfw.debounceTimer != nil {
cfw.debounceTimer.Stop()
}
notify := cfw.configChangeNotify
cfw.debounceTimer = time.AfterFunc(500*time.Millisecond, func() {
select {
case notify <- struct{}{}:
default:
// Drop if a notification is already pending/being handled.
}
})
cfw.debounceMu.Unlock()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/finicky/src/config/configfiles.go` around lines 365 - 374, The debounce
timer's callback (created in cfw.debounceTimer via time.AfterFunc) performs a
blocking send to cfw.configChangeNotify while holding no lock, which lets timer
goroutines pile up and defeats coalescing; change the callback to perform a
non-blocking send (e.g., use a select with case notify <- struct{}{} and a
default to drop the signal if the channel is full) so pending timers won't
block, or alternatively make configChangeNotify a size-1 buffered channel and
still use a non-blocking send to ensure only one coalesced notification is
delivered; adjust the timer callback that references cfw.debounceTimer,
cfw.debounceMu, and configChangeNotify accordingly.

Comment on lines +143 to +165
window.SaveRulesHandler = func(rf rules.RulesFile) {
slog.Debug("Rules updated", "count", len(rf.Rules))
resolver.SetCachedRules(rf)
if vm == nil || !vm.IsJSConfig() {
if rf.DefaultBrowser == "" && len(rf.Rules) == 0 {
vm = nil
return
}
script, err := rules.ToJSConfigScript(rf, namespace)
if err != nil {
slog.Error("Failed to generate config from rules", "error", err)
return
}
newVM, err := config.NewFromScript(finickyConfigAPIJS, namespace, script)
if err != nil {
slog.Error("Failed to rebuild VM from rules", "error", err)
return
}
vm = newVM
shouldKeepRunning = vm.GetAllConfigOptions().KeepRunning
go checkForUpdates()
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# First, verify the file structure and read the main.go file to understand the context
git ls-files apps/finicky/src/main.go | head -5

Repository: johnste/finicky

Length of output: 83


🏁 Script executed:

# Get file info and line count
wc -l apps/finicky/src/main.go

Repository: johnste/finicky

Length of output: 87


🏁 Script executed:

# Read the SaveRulesHandler section (lines 143-165) and surrounding context
sed -n '135,170p' apps/finicky/src/main.go

Repository: johnste/finicky

Length of output: 1161


🏁 Script executed:

# Check lines 189-190 and context
sed -n '185,195p' apps/finicky/src/main.go

Repository: johnste/finicky

Length of output: 409


🏁 Script executed:

# Check lines 299-300 and context
sed -n '295,305p' apps/finicky/src/main.go

Repository: johnste/finicky

Length of output: 389


🏁 Script executed:

# Search for resolver.ResolveURL to understand what it does
rg "ResolveURL" apps/finicky/src/ -A 5 -B 2

Repository: johnste/finicky

Length of output: 14133


🏁 Script executed:

# Search for goja.Runtime usage and any mutex definitions
rg "goja\.Runtime|sync\.Mutex|sync\.RWMutex" apps/finicky/src/main.go

Repository: johnste/finicky

Length of output: 85


🏁 Script executed:

# Look at the vm definition and how it's used globally
rg "var vm|vm =" apps/finicky/src/main.go | head -20

Repository: johnste/finicky

Length of output: 140


🏁 Script executed:

# Read the evaluateURL function to see how vm/runtime is used
sed -n '1,150p' apps/finicky/src/resolver/resolver.go | tail -100

Repository: johnste/finicky

Length of output: 3213


🏁 Script executed:

# Search for evaluateURL full implementation
rg "func evaluateURL" apps/finicky/src/resolver/resolver.go -A 30

Repository: johnste/finicky

Length of output: 1065


🏁 Script executed:

# Check the full evaluateURL and runtime.Set calls
sed -n '1,200p' apps/finicky/src/resolver/resolver.go

Repository: johnste/finicky

Length of output: 4313


🏁 Script executed:

# Look for any synchronization primitives in main.go
rg "sync\.|mutex|lock|RWMutex|Mutex" apps/finicky/src/main.go -i

Repository: johnste/finicky

Length of output: 82


🏁 Script executed:

# Check if there are goroutines spawned that access vm
rg "go " apps/finicky/src/main.go | head -20

Repository: johnste/finicky

Length of output: 342


🏁 Script executed:

# Look at line 138-180 to see the full context around SaveRulesHandler
sed -n '138,180p' apps/finicky/src/main.go

Repository: johnste/finicky

Length of output: 1264


🏁 Script executed:

# Check if goja.Runtime is documented as thread-safe and look at lock usage
rg "LockOSThread|Lock|Unlock" apps/finicky/src/config/config.go -B 2 -A 2

Repository: johnste/finicky

Length of output: 132


🏁 Script executed:

# Look for the config.VM structure and Runtime() method
rg "type VM struct|func.*Runtime\(\)" apps/finicky/src/config/config.go -A 5

Repository: johnste/finicky

Length of output: 132


🏁 Script executed:

# Check how many goroutines actually access resolver.ResolveURL
rg "resolver\.ResolveURL|TestURLInternal" apps/finicky/src/main.go -B 2 -A 2

Repository: johnste/finicky

Length of output: 657


🏁 Script executed:

# Look at the main event loop to understand threading model
sed -n '165,250p' apps/finicky/src/main.go

Repository: johnste/finicky

Length of output: 2320


🏁 Script executed:

# Check if there's any synchronization in the main event loop where SaveRulesHandler might be called
rg "SaveRulesHandler|window\." apps/finicky/src/main.go | head -20

Repository: johnste/finicky

Length of output: 573


🏁 Script executed:

# Check the goja import and documentation
grep -n "goja" apps/finicky/src/config/config.go | head -10

Repository: johnste/finicky

Length of output: 125


🏁 Script executed:

# Find config package files
fd "config" apps/finicky/src --type f -name "*.go"

Repository: johnste/finicky

Length of output: 289


🏁 Script executed:

# Look for VM type definition
rg "type VM struct" apps/finicky/src -A 10

Repository: johnste/finicky

Length of output: 629


🏁 Script executed:

# Check for any mutex or synchronization in the config package
rg "Mutex|Lock|sync\." apps/finicky/src/config -A 2 -B 2

Repository: johnste/finicky

Length of output: 730


🏁 Script executed:

# Find the config.VM Runtime method
rg "func.*\(\s*vm\s*\*VM\s*\).*Runtime" apps/finicky/src -A 5

Repository: johnste/finicky

Length of output: 209


🏁 Script executed:

# Check if goja.Runtime is thread-safe by looking at documentation or comments
web_search
goja Runtime thread safety concurrency

Repository: johnste/finicky

Length of output: 152


🌐 Web query:

goja Runtime thread safety and goroutine safety

💡 Result:

  • goja.Runtime is not goroutine-safe / thread-safe. A single *goja.Runtime “can only be used by a single goroutine at a time.” If you need concurrency, create multiple runtimes, or serialize all access through one owning goroutine (e.g., an event-loop pattern). [1]
  • You generally can’t share JS object values between runtimes. The README notes it’s “not possible to pass object values between runtimes.” (Primitives are OK.) [1]
  • Promise resolve/reject funcs aren’t goroutine-safe either. Runtime.NewPromise() returns resolve/reject functions that “must not be called in parallel with VM running”; the docs show scheduling back onto the VM/event-loop goroutine. [2]
  • If you do share host objects across runtimes, the host side must be goroutine-safe. NewSharedDynamicObject / NewSharedDynamicArray can create objects sharable across multiple runtimes, but the provided DynamicObject/DynamicArray “must be goroutine-safe.” [3]

Sources:
[1] goja README / FAQ on pkg.go.dev (goroutine safety + cross-runtime value passing) (pkg.go.dev)
[2] Runtime.NewPromise docs (resolve/reject not goroutine-safe; schedule on loop) (pkg.go.dev)
[3] NewSharedDynamicObject / NewSharedDynamicArray docs (must be goroutine-safe) (pkg.go.dev)

Citations:


🏁 Script executed:

# Let me check the entire VM structure and its methods
sed -n '1,100p' apps/finicky/src/config/vm.go

Repository: johnste/finicky

Length of output: 2824


🏁 Script executed:

# Check all calls to vm from the main event loop
sed -n '165,300p' apps/finicky/src/main.go

Repository: johnste/finicky

Length of output: 3757


🏁 Script executed:

# Look for synchronization when vm is accessed
rg "vm\." apps/finicky/src/main.go | head -30

Repository: johnste/finicky

Length of output: 405


🏁 Script executed:

# Check how SaveRulesHandler is called and from what context
rg "SaveRulesHandler" apps/finicky/src -B 5 -A 5

Repository: johnste/finicky

Length of output: 2075


🏁 Script executed:

# Look at the event loop more carefully - understand all places where vm is accessed
sed -n '180,320p' apps/finicky/src/main.go

Repository: johnste/finicky

Length of output: 4023


🏁 Script executed:

# Verify TestURLInternal context - is it indeed spawned in a goroutine?
sed -n '135,145p' apps/finicky/src/main.go

Repository: johnste/finicky

Length of output: 491


🌐 Web query:

goja Runtime state isolation multiple evaluations concurrent

💡 Result:

  • State isolation: A goja.Runtime is a full JS VM instance with its own global object/heap. If you run multiple RunString/RunScript/eval() calls on the same Runtime, they share globals and mutated state across evaluations. For isolation between evaluations, create a new goja.Runtime per isolated context (or explicitly reset/recreate the global environment yourself). [1]

  • Concurrency: goja.Runtime is not goroutine-safe. A single runtime instance may only be used by one goroutine at a time; you must not evaluate concurrently on the same runtime. To run scripts concurrently, use one runtime per goroutine, or funnel all access to a runtime through a single “owner” goroutine / event-loop style scheduler. [1][2]

  • Common pattern for concurrent hosts: Implement an event loop (or use one like goja_nodejs) and only touch the runtime from the loop’s goroutine; other goroutines post tasks into the loop. [1][2][3]

Sources: [1] goja README (“Is it goroutine-safe?”) (github.com), [2] pkg.go.dev docs noting Runtime methods are not safe for concurrent use and recommending an event loop (pkg.go.dev), [3] example repo emphasizing all VM interactions must happen on the owning goroutine (github.com)

Citations:


🏁 Script executed:

# Final verification: check if there's any channel or synchronization mechanism that gates SaveRulesHandler or TestURLInternal
rg "SaveRulesHandler|TestURLInternal" apps/finicky/src/window -B 10 -A 2

Repository: johnste/finicky

Length of output: 1565


Protect shared goja.Runtime access with synchronization.

SaveRulesHandler replaces the global vm while TestURLInternal (spawned in its own goroutine) and the main event loop both read and mutate that same instance via resolver.ResolveURL. goja.Runtime is not goroutine-safe and can only be used by a single goroutine at a time. Each resolver.ResolveURL call invokes evaluateURL, which writes per-request globals like url, opener, and _jsonHandlers into the runtime, then executes JS that reads them. Concurrent access across the UI-triggered SaveRulesHandler, the spawned test-URL goroutine, and the main event loop will cause race conditions and state bleed. Guard all VM reads/writes with a mutex, or funnel all VM access through a single owning goroutine using channels.

Also applies to: lines 189–190, 301–302

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/finicky/src/main.go` around lines 143 - 165, The SaveRulesHandler
replaces the global vm and other goroutines (TestURLInternal, the main event
loop via resolver.ResolveURL and evaluateURL) concurrently read/write the same
goja.Runtime; make VM access concurrency-safe by adding a package-level
sync.RWMutex (e.g., vmMu) and use vmMu.Lock() when replacing or mutating vm and
vmMu.RLock() when reading/using it (including in resolver.ResolveURL,
evaluateURL, TestURLInternal, and any main-loop accesses); ensure nil checks and
assignments of vm and shouldKeepRunning are done under the same lock and release
promptly, and add locking around calls that execute JS on the runtime so only
one goroutine accesses goja.Runtime at a time (apply same protection to the
other spots noted in the comment).

Comment on lines +147 to +149
if rf.DefaultBrowser == "" && len(rf.Rules) == 0 {
vm = nil
return
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Don’t treat option-only rules.json files as empty.

These checks only look at DefaultBrowser and Rules. A saved rules file that only contains UI-editable settings like keepRunning, hideIcon, logRequests, or checkForUpdates will therefore produce vm = nil, so the new UI settings never take effect. Build a VM whenever any persisted rules-file setting is present.

Also applies to: 441-457

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/finicky/src/main.go` around lines 147 - 149, The current early-return
sets vm = nil when rf.DefaultBrowser == "" && len(rf.Rules) == 0 which
incorrectly treats option-only rules files as empty; update the condition in the
VM-initialization logic (the block referencing rf, vm, DefaultBrowser and Rules)
to instead detect whether any persisted settings exist (e.g., check fields like
rf.KeepRunning, rf.HideIcon, rf.LogRequests, rf.CheckForUpdates, etc.) and only
set vm = nil when no rules and no other persisted settings are present; apply
the same fix to the analogous conditional later in the file (the block around
the other rf/ vm check).

Comment on lines +151 to +163
script, err := rules.ToJSConfigScript(rf, namespace)
if err != nil {
slog.Error("Failed to generate config from rules", "error", err)
return
}
newVM, err := config.NewFromScript(finickyConfigAPIJS, namespace, script)
if err != nil {
slog.Error("Failed to rebuild VM from rules", "error", err)
return
}
vm = newVM
shouldKeepRunning = vm.GetAllConfigOptions().KeepRunning
go checkForUpdates()
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Apply logRequests after rebuilding from saved rules.

This branch swaps in newVM but only re-syncs KeepRunning and update checks. The UI now saves logRequests too, yet logger.SetupFile(...) is never re-run here, so that toggle won’t take effect until a later reload or restart.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/finicky/src/main.go` around lines 151 - 163, After swapping in newVM
(from config.NewFromScript) you must also apply the saved logRequests setting so
the logger reflects the rebuilt config immediately; after setting vm = newVM and
updating shouldKeepRunning = vm.GetAllConfigOptions().KeepRunning, call
logger.SetupFile(...) (or the existing logger initialization routine) with the
new VM's logRequests value (e.g., vm.GetAllConfigOptions().LogRequests or
newVM.GetAllConfigOptions().LogRequests) before launching checkForUpdates() so
the logRequests toggle takes effect without a restart.

Comment on lines +21 to +22
{#if description}
<p class="page-description">{description}</p>
<p class="page-description">{@html description}</p>
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# Check the PageContainer.svelte file
cat -n packages/finicky-ui/src/components/PageContainer.svelte

Repository: johnste/finicky

Length of output: 1809


🏁 Script executed:

# Find all usages of PageContainer component
rg "PageContainer" --type svelte --type ts --type js -B 2 -A 2

Repository: johnste/finicky

Length of output: 89


🏁 Script executed:

# Check imports to understand the component's scope
rg "import.*PageContainer" -B 1 -A 1

Repository: johnste/finicky

Length of output: 1551


🏁 Script executed:

# Check TestUrl.svelte for PageContainer usage
rg -A 10 -B 5 "PageContainer" packages/finicky-ui/src/pages/TestUrl.svelte

Repository: johnste/finicky

Length of output: 1221


🏁 Script executed:

# Check StartPage.svelte for PageContainer usage
rg -A 10 -B 5 "PageContainer" packages/finicky-ui/src/pages/StartPage.svelte

Repository: johnste/finicky

Length of output: 1499


🏁 Script executed:

# Check Rules.svelte for PageContainer usage
rg -A 10 -B 5 "PageContainer" packages/finicky-ui/src/pages/Rules.svelte

Repository: johnste/finicky

Length of output: 1327


🏁 Script executed:

# Check LogViewer.svelte for PageContainer usage
rg -A 10 -B 5 "PageContainer" packages/finicky-ui/src/pages/LogViewer.svelte

Repository: johnste/finicky

Length of output: 1154


🏁 Script executed:

# Check About.svelte for PageContainer usage
rg -A 10 -B 5 "PageContainer" packages/finicky-ui/src/pages/About.svelte

Repository: johnste/finicky

Length of output: 878


Make raw HTML in description an explicit trusted API.

The {@html} rendering on this string prop creates a safety boundary that isn't visible in the type signature. While the current callers (Rules.svelte) intentionally pass hardcoded HTML with styled elements and code tags, there's no mechanism to prevent future callers from passing user-controlled data into this unsafe sink. Use a separately named prop (e.g., descriptionHtml) or expose a named slot for rich content so the security boundary is explicit and clear to consumers.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/finicky-ui/src/components/PageContainer.svelte` around lines 21 -
22, The component currently injects raw HTML via the description prop using
{`@html` description}; change the public API to make this a consciously trusted
sink by introducing a new prop name or slot (e.g., descriptionHtml prop or a
named <slot name="description">) and stop calling {`@html` description}. Update
PageContainer.svelte to render plain text for the existing description prop
(e.g., {description}) and only use {`@html` descriptionHtml} (or render the named
slot) so the intent is explicit, and update callers like Rules.svelte to pass
hardcoded HTML into the new descriptionHtml prop or into the named slot instead
of the original description prop.

Comment on lines +46 to +61
function scheduleSave() {
clearTimeout(saveTimer);
pendingSave = true;
saveTimer = setTimeout(() => {
const filteredRules = rules.filter(
(r) => r.match.some((m) => m.trim() !== "") && r.browser.trim() !== ""
);
const payload: RulesFile = {
defaultBrowser: rulesFile.defaultBrowser,
defaultProfile: rulesFile.defaultProfile,
rules: filteredRules,
};
window.finicky.sendMessage({ type: "saveRules", payload });
pendingSave = false;
}, SAVE_DEBOUNCE);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Timer not cleared on component destroy could cause memory leak.

The saveTimer set by scheduleSave() is never cleaned up when the component is destroyed. If the user navigates away before the debounce completes, the timer will still fire and attempt to access unmounted component state.

🛠️ Proposed cleanup
+import { onDestroy, untrack, tick } from "svelte";

 let saveTimer: ReturnType<typeof setTimeout>;
 
+onDestroy(() => {
+  clearTimeout(saveTimer);
+});
📝 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
function scheduleSave() {
clearTimeout(saveTimer);
pendingSave = true;
saveTimer = setTimeout(() => {
const filteredRules = rules.filter(
(r) => r.match.some((m) => m.trim() !== "") && r.browser.trim() !== ""
);
const payload: RulesFile = {
defaultBrowser: rulesFile.defaultBrowser,
defaultProfile: rulesFile.defaultProfile,
rules: filteredRules,
};
window.finicky.sendMessage({ type: "saveRules", payload });
pendingSave = false;
}, SAVE_DEBOUNCE);
}
import { onDestroy } from "svelte";
let saveTimer: ReturnType<typeof setTimeout>;
onDestroy(() => {
clearTimeout(saveTimer);
});
function scheduleSave() {
clearTimeout(saveTimer);
pendingSave = true;
saveTimer = setTimeout(() => {
const filteredRules = rules.filter(
(r) => r.match.some((m) => m.trim() !== "") && r.browser.trim() !== ""
);
const payload: RulesFile = {
defaultBrowser: rulesFile.defaultBrowser,
defaultProfile: rulesFile.defaultProfile,
rules: filteredRules,
};
window.finicky.sendMessage({ type: "saveRules", payload });
pendingSave = false;
}, SAVE_DEBOUNCE);
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/finicky-ui/src/pages/Rules.svelte` around lines 46 - 61, The
debounce timer set by scheduleSave() (saveTimer) isn't cleared on component
teardown; add a Svelte onDestroy handler that calls clearTimeout(saveTimer) (and
clears or nulls saveTimer and pendingSave if desired) to prevent the timeout
from firing after the component is unmounted; locate scheduleSave, saveTimer,
and SAVE_DEBOUNCE in the file and register onDestroy to perform the cleanup.

Comment on lines +127 to +131
// Request data from native on mount
$effect(() => {
window.finicky.sendMessage({ type: "getRules" });
window.finicky.sendMessage({ type: "getInstalledBrowsers" });
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Mount-only effect will re-run on every reactive update.

In Svelte 5, $effect blocks run whenever their dependencies change. This effect has no explicit dependencies but will still re-run. For mount-only logic, use onMount instead, or wrap in untrack to prevent re-runs.

🛠️ Proposed fix using onMount
+import { onMount, untrack, tick } from "svelte";

-// Request data from native on mount
-$effect(() => {
-  window.finicky.sendMessage({ type: "getRules" });
-  window.finicky.sendMessage({ type: "getInstalledBrowsers" });
-});
+// Request data from native on mount
+onMount(() => {
+  window.finicky.sendMessage({ type: "getRules" });
+  window.finicky.sendMessage({ type: "getInstalledBrowsers" });
+});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/finicky-ui/src/pages/Rules.svelte` around lines 127 - 131, The
$effect block that calls window.finicky.sendMessage({ type: "getRules" }) and
window.finicky.sendMessage({ type: "getInstalledBrowsers" }) is being used for
mount-only behavior but will re-run on reactive updates; replace it with
Svelte's onMount (import onMount from "svelte") and move those sendMessage calls
into an onMount callback (or alternatively wrap them in untrack) so the messages
are sent only once on component mount; update the block that currently uses
$effect to call onMount(() => { ... }) referencing the same
window.finicky.sendMessage calls.

Comment on lines +38 to +51
// Sync when the rulesFile prop is updated from the backend.
// Does NOT read profilesByBrowser — keeps profile fetches from resetting local edits.
$: {
keepRunning = rulesFile.options?.keepRunning ?? config.options?.keepRunning ?? true;
hideIcon = rulesFile.options?.hideIcon ?? config.options?.hideIcon ?? false;
logRequests = rulesFile.options?.logRequests ?? config.options?.logRequests ?? false;
checkForUpdates = rulesFile.options?.checkForUpdates ?? config.options?.checkForUpdates ?? true;
defaultBrowser = isJSConfig ? (config.defaultBrowser ?? "") : (rulesFile.defaultBrowser ?? "");
defaultProfile = rulesFile.defaultProfile ?? "";
defaultBrowserIsCustom = defaultBrowser !== "" && !installedBrowsers.includes(defaultBrowser);
}
// Separate statement so profilesByBrowser changes only update this derived flag,
// not the browser/profile values above.
$: defaultProfileIsCustom = defaultProfile !== "" && !(profilesByBrowser[defaultBrowser] ?? []).includes(defaultProfile);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cat -n packages/finicky-ui/src/pages/StartPage.svelte | sed -n '35,55p'

Repository: johnste/finicky

Length of output: 1395


🏁 Script executed:

# Let's see more context around the reactive blocks to understand variable declarations
cat -n packages/finicky-ui/src/pages/StartPage.svelte | sed -n '1,80p'

Repository: johnste/finicky

Length of output: 4136


🏁 Script executed:

# Search for where installedBrowsers comes from and how it's used
rg -n "installedBrowsers" packages/finicky-ui/src/pages/StartPage.svelte -A 2 -B 2

Repository: johnste/finicky

Length of output: 801


🏁 Script executed:

# Check if there are imports or variable declarations for the key variables
rg -n "let.*installedBrowsers|let.*defaultBrowser|let.*profilesByBrowser" packages/finicky-ui/src/pages/StartPage.svelte

Repository: johnste/finicky

Length of output: 320


Move defaultBrowserIsCustom to a separate reactive statement to prevent browser-list updates from resetting local edits.

The sync block reads installedBrowsers (line 47), making it a reactive dependency. When async browser discovery completes and updates the installedBrowsers prop, the entire block reruns and overwrites defaultBrowser and defaultProfile with their prop values, wiping out any local changes. Move defaultBrowserIsCustom into its own $: statement like defaultProfileIsCustom, so derived flags update independently from backend sync.

Suggested fix
   $: {
     keepRunning = rulesFile.options?.keepRunning ?? config.options?.keepRunning ?? true;
     hideIcon = rulesFile.options?.hideIcon ?? config.options?.hideIcon ?? false;
     logRequests = rulesFile.options?.logRequests ?? config.options?.logRequests ?? false;
     checkForUpdates = rulesFile.options?.checkForUpdates ?? config.options?.checkForUpdates ?? true;
     defaultBrowser = isJSConfig ? (config.defaultBrowser ?? "") : (rulesFile.defaultBrowser ?? "");
     defaultProfile = rulesFile.defaultProfile ?? "";
-    defaultBrowserIsCustom = defaultBrowser !== "" && !installedBrowsers.includes(defaultBrowser);
   }
+  $: defaultBrowserIsCustom = defaultBrowser !== "" && !installedBrowsers.includes(defaultBrowser);
   $: defaultProfileIsCustom = defaultProfile !== "" && !(profilesByBrowser[defaultBrowser] ?? []).includes(defaultProfile);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/finicky-ui/src/pages/StartPage.svelte` around lines 38 - 51, The
reactive sync block currently assigns defaultBrowserIsCustom and therefore
depends on installedBrowsers, causing backend updates to rerun the block and
overwrite local edits; remove the defaultBrowserIsCustom assignment from the big
$: block (the block that sets keepRunning, hideIcon, logRequests,
checkForUpdates, defaultBrowser, defaultProfile) and add a separate reactive
statement like the existing defaultProfileIsCustom one that computes
defaultBrowserIsCustom = defaultBrowser !== "" &&
!installedBrowsers.includes(defaultBrowser), so only that derived flag reacts to
installedBrowsers while defaultBrowser/defaultProfile stay stable (references:
defaultBrowserIsCustom, installedBrowsers, defaultProfileIsCustom,
profilesByBrowser, defaultBrowser, defaultProfile).

scripts/dev.sh Outdated
Comment on lines +48 to +80
case "$1" in
0|normal)
echo "Scenario 0: normal (auto-detect configs)"
"$BINARY" --window
;;
1|none)
echo "Scenario 1: no JS config, no rules.json"
"$BINARY" --window --no-config --rules "$NO_RULES"
;;
2|js)
echo "Scenario 2: JS config only"
"$BINARY" --window --config "$JS_CONFIG" --rules "$NO_RULES"
;;
3|json)
echo "Scenario 3: rules.json only"
"$BINARY" --window --no-config --rules "$JSON_RULES"
;;
4|both)
echo "Scenario 4: JS config + rules.json"
"$BINARY" --window --config "$JS_CONFIG" --rules "$JSON_RULES"
;;
*)
echo "Usage: $0 [--mock-update [version]] <scenario>"
echo ""
echo " 0 normal Auto-detect configs (default behavior)"
echo " 1 none No JS config, no rules.json"
echo " 2 js JS config only ($JS_CONFIG)"
echo " 3 json rules.json only ($JSON_RULES)"
echo " 4 both JS config + rules.json"
echo ""
echo "Flags:"
echo " --mock-update [version] Show update banner (default: 1.99.0)"
exit 0
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Honor the documented default scenario.

The header says 4|both is the default, but an empty $1 falls through to the usage block. Default the selector before the case so ./scripts/dev.sh actually runs scenario 4.

Suggested fix
-case "$1" in
+SCENARIO="${1:-4}"
+case "$SCENARIO" in
📝 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
case "$1" in
0|normal)
echo "Scenario 0: normal (auto-detect configs)"
"$BINARY" --window
;;
1|none)
echo "Scenario 1: no JS config, no rules.json"
"$BINARY" --window --no-config --rules "$NO_RULES"
;;
2|js)
echo "Scenario 2: JS config only"
"$BINARY" --window --config "$JS_CONFIG" --rules "$NO_RULES"
;;
3|json)
echo "Scenario 3: rules.json only"
"$BINARY" --window --no-config --rules "$JSON_RULES"
;;
4|both)
echo "Scenario 4: JS config + rules.json"
"$BINARY" --window --config "$JS_CONFIG" --rules "$JSON_RULES"
;;
*)
echo "Usage: $0 [--mock-update [version]] <scenario>"
echo ""
echo " 0 normal Auto-detect configs (default behavior)"
echo " 1 none No JS config, no rules.json"
echo " 2 js JS config only ($JS_CONFIG)"
echo " 3 json rules.json only ($JSON_RULES)"
echo " 4 both JS config + rules.json"
echo ""
echo "Flags:"
echo " --mock-update [version] Show update banner (default: 1.99.0)"
exit 0
SCENARIO="${1:-4}"
case "$SCENARIO" in
0|normal)
echo "Scenario 0: normal (auto-detect configs)"
"$BINARY" --window
;;
1|none)
echo "Scenario 1: no JS config, no rules.json"
"$BINARY" --window --no-config --rules "$NO_RULES"
;;
2|js)
echo "Scenario 2: JS config only"
"$BINARY" --window --config "$JS_CONFIG" --rules "$NO_RULES"
;;
3|json)
echo "Scenario 3: rules.json only"
"$BINARY" --window --no-config --rules "$JSON_RULES"
;;
4|both)
echo "Scenario 4: JS config + rules.json"
"$BINARY" --window --config "$JS_CONFIG" --rules "$JSON_RULES"
;;
*)
echo "Usage: $0 [--mock-update [version]] <scenario>"
echo ""
echo " 0 normal Auto-detect configs (default behavior)"
echo " 1 none No JS config, no rules.json"
echo " 2 js JS config only ($JS_CONFIG)"
echo " 3 json rules.json only ($JSON_RULES)"
echo " 4 both JS config + rules.json"
echo ""
echo "Flags:"
echo " --mock-update [version] Show update banner (default: 1.99.0)"
exit 0
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/dev.sh` around lines 48 - 80, Default the scenario selector to "4"
(both) before the case block so an empty $1 runs scenario 4; modify the code
around the existing case "$1" in block (and the scenario labels 0|normal ..
4|both) to use a pre-initialized value instead of raw $1 — e.g., if $1 is empty
set a local variable or reset positional parameters so the case tests a
non-empty selector (default "4") and ensure existing flags like --mock-update
remain honored; update references from case "$1" in to case "$SCENARIO" in (or
set -- 4 ...) so ./scripts/dev.sh without args executes the "4|both" branch.

@johnste johnste changed the title Feat/UI rules UI Rules and lots of things to support it Apr 7, 2026
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.

UI Config

2 participants