Skip to content

Refactor/react best practises#38

Merged
loco1842 merged 3 commits into
mainfrom
refactor/react-best-practises
Apr 27, 2026
Merged

Refactor/react best practises#38
loco1842 merged 3 commits into
mainfrom
refactor/react-best-practises

Conversation

@loco1842
Copy link
Copy Markdown
Owner

@loco1842 loco1842 commented Apr 27, 2026

Summary by CodeRabbit

  • Documentation

    • Expanded comprehensive guides covering API reference, architecture, configuration, deployment, development workflow, testing, and troubleshooting.
    • Updated contributing requirements and CI specifications.
    • Added common setup issues troubleshooting section.
  • Bug Fixes

    • Improved clipboard operation error handling with better user feedback.
  • Refactor

    • Consolidated theme, font, and density application logic for consistency.
    • Streamlined state management and event handling in UI components.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 27, 2026

📝 Walkthrough

Walkthrough

This PR comprehensively refactors the frontend architecture by extracting reusable React hooks, centralizing theme/font/density application logic into dedicated helpers, reorganizing utility functions from types.ts into focused modules (tab.ts, path.ts), and introducing the new CommandDetailTab component. Handler creation in App.tsx is simplified from per-tab factories with caching to explicit tabId-based async functions. Documentation is expanded across multiple areas: API bindings, architecture overview, configuration, deployment, development workflow, getting-started troubleshooting, and testing guidance. Build requirements and CI descriptions are refined in contribution guidelines.

Changes

Cohort / File(s) Summary
Documentation
docs/API.md, docs/ARCHITECTURE.md, docs/CONFIGURATION.md, docs/DEPLOYMENT.md, docs/DEVELOPMENT.md, docs/GETTING-STARTED.md, docs/TESTING.md
New and expanded documentation covering API reference bindings, architectural abstractions, configuration schemas and defaults, deployment procedures and platforms, development workflows and CI, setup troubleshooting, and testing frameworks/status. All additive changes except minor refinements to deployment and CI descriptions.
Hook Extraction
frontend/src/hooks/useSyncedRef.ts, frontend/src/hooks/useCopyToClipboard.ts, frontend/src/hooks/useResizable.ts
Three new custom React hooks: useSyncedRef for synchronizing ref values during render, useCopyToClipboard for clipboard operations with timed reset, and useResizable for drag-based size adjustment with localStorage persistence.
Theme/Styling Centralization
frontend/src/lib/theme-apply.ts, frontend/src/main.tsx
New centralized theme/font/density application module; refactored settings window theme handling to delegate DOM mutations to helper functions instead of inline effects.
App.tsx Handler Refactoring
frontend/src/App.tsx
Replaced per-tab handler factories and caching logic with single async handlers accepting explicit tabId parameter; synchronized state refs with useSyncedRef; delegated theme/font/density application to helper functions; integrated CommandDetailTab wrapper component.
Component Updates
frontend/src/components/CommandDetailTab.tsx, frontend/src/components/CommandDetail.tsx, frontend/src/components/OutputPane.tsx, frontend/src/components/ResizablePanel.tsx, frontend/src/components/SettingsPage.tsx
New CommandDetailTab component for conditional tab display and handler wiring; CommandDetail delegates clipboard operations to useCopyToClipboard hook; OutputPane and ResizablePanel replace manual drag/resize state with useResizable hook; SettingsPage uses centralized theme/font/density helpers.
Utility Reorganization
frontend/src/utils/tab.ts, frontend/src/utils/path.ts, frontend/src/types.ts
New tab.ts module exports tab ID and title utilities (moved from types.ts); new path.ts module exports OS/path utilities (moved from types.ts); types.ts removes these functions to reduce module coupling.
Import Updates
frontend/src/components/CommandPalette.tsx, frontend/src/components/Sidebar.tsx
Updated imports of getCommandDisplayTitle to source from ../utils/tab instead of ../types.
Localization & Contributing
frontend/src/locales/en.json, CONTRIBUTING.md, README.md
Added outputPane.copyFailed i18n key; updated Node.js version constraints to support ranges; refined CI platform matrix and frontend framework requirements; extended README documentation index.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐰 A rabbit hops through tangled code with glee,
Extracting hooks like carrots from the tree!
Themes now centralized, utilities aligned,
Docs bloom like clover—order redefined.

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 40.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title is vague and generic, using non-descriptive terminology ('Refactor' and 'best practises') that does not clearly convey what was actually changed in the changeset. Revise the title to be more specific and descriptive of the main changes, such as 'Extract UI utilities and refactor App component to use custom hooks' or 'Move utility functions and adopt custom hooks for state/UI management'.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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


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

❤️ Share

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

Copy link
Copy Markdown
Contributor

@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: 15

Caution

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

⚠️ Outside diff range comments (3)
docs/GETTING-STARTED.md (1)

12-12: ⚠️ Potential issue | 🟡 Minor

Node.js prerequisite is inconsistent with the rest of the PR.

This file still requires Node.js >= 25, but in this same PR CONTRIBUTING.md Line 40 and docs/DEVELOPMENT.md Line 16 were updated to >=20.19.0 || >=22.13.0 || >=24 (matching frontend/package.json engines). Please align this file too so contributors don't get conflicting requirements.

-- **Node.js** `>= 25`
+- **Node.js** `>=20.19.0 || >=22.13.0 || >=24`
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/GETTING-STARTED.md` at line 12, Update the Node.js prerequisite in the
GETTING-STARTED.md entry that currently reads "**Node.js** `>= 25`" to match the
project's unified engines constraint (use "`>=20.19.0 || >=22.13.0 || >=24`") so
it aligns with the changes made to CONTRIBUTING.md and docs/DEVELOPMENT.md and
frontend/package.json; simply replace the version string in that Node.js line to
the unified expression.
frontend/src/main.tsx (1)

37-62: ⚠️ Potential issue | 🟠 Major

Custom theme colors not applied on initial load.

applyTheme(t) at Line 51 is called without the second customColors argument, and the customThemes array isn't even parsed until lines 54–60. If the persisted s.theme is a custom theme ID, the custom CSS variables will not be set on initial render — the user would see an unstyled / default-theme UI until they re-pick the theme via handleThemeChange.

Consider parsing customThemes first and then resolving the custom colors before applying:

🔧 Suggested fix
             GetSettings().then(s => {
                 if (!s) return;
                 const t = s.theme || 'vscode-dark'
                 setTheme(t)
                 setDensity(s.density || 'comfortable')
                 setUiFont(s.uiFont || 'Inter')
                 setMonoFont(s.monoFont || 'JetBrains Mono')
                 setLocale(s.locale || 'en')
                 setTerminal(s.terminal || '')
                 if (s.windowX !== undefined) setWindowX(s.windowX)
                 if (s.windowY !== undefined) setWindowY(s.windowY)
                 if (s.windowWidth !== undefined) setWindowWidth(s.windowWidth)
                 if (s.windowHeight !== undefined) setWindowHeight(s.windowHeight)
-                applyTheme(t)
-                applyDensity(s.density || 'comfortable')
-                applyFonts(s.uiFont || 'Inter', s.monoFont || 'JetBrains Mono')
+                let parsedCustomThemes: CustomTheme[] = []
                 if (s.customThemes && s.customThemes !== '[]') {
                     try {
                         const parsed = JSON.parse(s.customThemes)
-                        setCustomThemes(Array.isArray(parsed) ? parsed : [])
+                        parsedCustomThemes = Array.isArray(parsed) ? parsed : []
+                        setCustomThemes(parsedCustomThemes)
                         customThemesStrRef.current = s.customThemes
                     } catch { /* ignore parse error */ }
                 }
+                const customMatch = parsedCustomThemes.find(c => c.id === t)
+                applyTheme(t, customMatch?.colors ?? null)
+                applyDensity(s.density || 'comfortable')
+                applyFonts(s.uiFont || 'Inter', s.monoFont || 'JetBrains Mono')
             }).catch(() => {})
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/main.tsx` around lines 37 - 62, The initial theme application
calls applyTheme(t) before parsing persisted customThemes, so custom theme CSS
variables are never applied on load; update the GetSettings().then handler in
main.tsx to parse s.customThemes (use JSON.parse with try/catch) and call
setCustomThemes(...) and set customThemesStrRef.current before calling
applyTheme; then, when resolving the theme ID t, derive the matching
customColors from the parsed customThemes array and pass those as the second
argument to applyTheme(t, customColors) (ensure applyTheme is still called with
defaults when no matching custom theme is found).
docs/TESTING.md (1)

9-16: ⚠️ Potential issue | 🟡 Minor

Section 1 contradicts the new Section 6 — Go tests do exist.

Section 1 still asserts "Cmdex currently has no automated tests.", "Go backend: No *_test.go files exist", and "CI ... do not run any test suites", but Section 6 (added in this PR) documents db_test.go with TestFreshDBMigrations, TestExistingDBIdempotent, and TestRollbackTo, and Section 7 even includes their expected go test output. Either pre-existing readers will trust Section 1 and miss the real tests, or skim Section 6 and conclude the doc is stale.

Recommend rewording Section 1 (and the "Verification is currently done entirely through manual QA" line) to reflect that Go tests now exist for the DB layer while frontend remains untested. The stale "Use a temporary SQLite file or :memory: database in tests" note in the priority-targets table at line 75 should also be updated since db_test.go already does this.

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

In `@docs/TESTING.md` around lines 9 - 16, Update the docs to remove the
contradictory blanket statement in Section 1 that "Cmdex currently has no
automated tests" and instead state that Go DB tests now exist; specifically
mention db_test.go and its tests TestFreshDBMigrations,
TestExistingDBIdempotent, and TestRollbackTo are present and exercised in the
doc, while frontend/unit tests are still missing and CI should be noted to not
run frontend tests. Also update the priority-targets table note about using a
temporary SQLite file or ":memory:" to reflect that db_test.go already uses an
in-memory/temporary SQLite setup so the guidance is no longer required.
🧹 Nitpick comments (14)
frontend/src/utils/path.ts (1)

37-43: Minor: separator detection on mixed-separator paths.

The split handles both / and \, but the rejoin separator is chosen by the presence of any \ in the input. For a mixed path like C:\Users/project/foo/bar (which can occur when users paste Windows paths into POSIX-style tooling), the output uses \ even though most segments were /-separated. Consider picking the separator by majority/first occurrence, or normalizing before split.

♻️ Optional refactor
-  const parts = path.split(/[\\/]/).filter(Boolean);
-  if (parts.length <= segments) return path;
-  const sep = path.includes('\\') ? '\\' : '/';
-  return '...' + sep + parts.slice(-segments).join(sep);
+  const parts = path.split(/[\\/]/).filter(Boolean);
+  if (parts.length <= segments) return path;
+  // Prefer the separator that appears first in the original path.
+  const firstSlash = path.indexOf('/');
+  const firstBackslash = path.indexOf('\\');
+  const sep =
+    firstBackslash !== -1 && (firstSlash === -1 || firstBackslash < firstSlash)
+      ? '\\'
+      : '/';
+  return '...' + sep + parts.slice(-segments).join(sep);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/utils/path.ts` around lines 37 - 43, The separator detection in
shortenPath is brittle for mixed-separator inputs (e.g.,
"C:\Users/project/..."): update shortenPath to choose the rejoin separator
deterministically by inspecting the path's separators (for example, count
occurrences of '/' vs '\' and pick the majority, or pick the first separator
character found) or normalize the input before splitting; change the logic
around const sep = ... in shortenPath to compute sep by
majority/first-occurrence (or normalize separators) so the returned '...' + sep
+ parts.slice(-segments).join(sep) uses the appropriate separator consistently.
frontend/src/utils/tab.ts (2)

27-29: Minor: trim before appending the ellipsis.

If character 50 lands on whitespace, the result is "…some text ...". A .trimEnd() on the slice keeps truncation visually tidy.

-  if (body.length <= 50) return body;
-  return body.slice(0, 50) + '...';
+  if (body.length <= 50) return body;
+  return body.slice(0, 50).trimEnd() + '…';

(Also worth considering the single‑char to match the ellipsis already used by scriptSnippet.)

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

In `@frontend/src/utils/tab.ts` around lines 27 - 29, In the truncation logic in
frontend/src/utils/tab.ts (the function that returns a snippet for body,
containing the lines that check body.length and slice to 50), replace the
current slice+ '...' with a trimmed slice to avoid trailing whitespace before
the ellipsis — e.g. use body.slice(0, 50).trimEnd() plus the ellipsis;
optionally switch the appended "..." to the single‑char "…" to match
scriptSnippet's style so update the return that currently does return
body.slice(0, 50) + '...' accordingly.

18-23: Shebang stripping is too narrow; use a generic regex for parity with scriptSnippet.

getCommandDisplayTitle only recognizes the literal #!/bin/bash shebang, so commands using #!/usr/bin/env bash, #!/bin/sh, #!/usr/bin/env python, etc. will surface their shebang line as the displayed title (and consume part of the 50‑char budget). Note that scriptSnippet in frontend/src/components/CommandPalette.tsx (line 52) already strips any shebang via /^#!.*\n?/, so the two derivations diverge for the same script.

♻️ Suggested generic stripping
-  let body = cmd.scriptContent;
-  if (body.startsWith('#!/bin/bash\n')) {
-    body = body.slice('#!/bin/bash\n'.length);
-  } else if (body.startsWith('#!/bin/bash')) {
-    body = body.slice('#!/bin/bash'.length);
-  }
-  
-  body = body.replace(/\n/g, ' ').trim();
+  const body = cmd.scriptContent
+    .replace(/^#!.*\r?\n?/, '')
+    .replace(/\n/g, ' ')
+    .trim();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/utils/tab.ts` around lines 18 - 23, The shebang stripping in
getCommandDisplayTitle is too specific (only '#!/bin/bash') and should use a
generic regex to match any shebang like scriptSnippet does; update the logic in
frontend/src/utils/tab.ts (inside getCommandDisplayTitle / where body is derived
from cmd.scriptContent) to remove a leading shebang using a regex such as
/^#!.*\n?/ so that any interpreter line (e.g., #!/usr/bin/env bash, #!/bin/sh,
#!/usr/bin/env python) is stripped before truncating/displaying the title.
frontend/src/hooks/useResizable.ts (2)

22-29: Replace the size→ref sync effect with the new useSyncedRef.

This file already pairs well with the new helper introduced in the same PR — the sizeRef + useEffect block is exactly what useSyncedRef was designed to eliminate.

-  const [size, setSize] = useState<number>(...);
-  ...
-  const sizeRef = useRef(size);
-
-  useEffect(() => {
-    sizeRef.current = size;
-  }, [size]);
+  const [size, setSize] = useState<number>(...);
+  ...
+  const sizeRef = useSyncedRef(size);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/hooks/useResizable.ts` around lines 22 - 29, Replace the manual
size→ref syncing logic by using the new helper: remove the useRef(size) +
useEffect block and instead create sizeRef with useSyncedRef(size) so sizeRef
always mirrors the latest size; update references that use sizeRef (symbol:
sizeRef) and ensure useSyncedRef is imported and used inside the useResizable
hook (symbols: useResizable, size).

12-13: Add a JSDoc block describing options and return shape.

As per coding guidelines: "JSDoc-style blocks must be used on non-obvious hooks". The axis/direction/storageKey semantics (and the appended -size suffix) are not obvious to consumers and warrant documentation.

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

In `@frontend/src/hooks/useResizable.ts` around lines 12 - 13, Add a JSDoc block
to the useResizable hook that documents the UseResizableOptions properties
(axis, direction, minSize, maxSize, defaultSize, storageKey) and the hook's
return shape; explicitly explain allowed values for axis and direction, the
semantics of storageKey (it is used as a prefix and has "-size" appended), and
the units/constraints for minSize/maxSize and defaultSize so callers understand
expected types and behavior; place the JSDoc immediately above the export
function useResizable and reference UseResizableOptions and the returned
tuple/object shape in the description.
frontend/src/hooks/useCopyToClipboard.ts (1)

3-3: Add a JSDoc block describing the hook’s behavior.

Per coding guideline frontend/src/hooks/**/*.ts: JSDoc-style blocks must be used on non-obvious hooks. Document the API contract (return shape, resetMs semantics, error handling), especially since the resolution of the error-handling question above will affect callers.

+/**
+ * Copy text to the system clipboard with a transient `copied` indicator.
+ * `@param` resetMs How long (ms) `copied` stays true after a successful copy. Default 1500.
+ * `@returns` `{ copied, copy(text) }`. `copy` resolves on success and rejects on failure.
+ */
 export function useCopyToClipboard(resetMs = 1500) {

As per coding guidelines: "JSDoc-style blocks must be used on non-obvious hooks (e.g., useKeyboardShortcuts behavior)".

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

In `@frontend/src/hooks/useCopyToClipboard.ts` at line 3, Add a JSDoc block for
the hook function useCopyToClipboard that documents the API contract: describe
the resetMs parameter (default 1500ms) and its effect (how long the success/err
state is cleared), the return shape (e.g., tuple or object containing the copy
function, a boolean success flag, and any error/message), and how errors are
surfaced/handled (whether the hook throws, returns an error value, or sets an
error state). Place the JSDoc immediately above the export function
useCopyToClipboard declaration and include types, parameter description, return
description, and any edge-case behavior callers must handle.
frontend/src/hooks/useSyncedRef.ts (1)

8-13: Consider an explicit return type for the public hook.

Adding : React.RefObject<T> (React 19 made RefObject mutable, replacing MutableRefObject) to the signature documents intent and prevents accidental inference drift if the body changes:

♻️ Suggested signature
-export function useSyncedRef<T>(value: T) {
-  const ref = useRef(value);
+export function useSyncedRef<T>(value: T): React.RefObject<T> {
+  const ref = useRef<T>(value);
   // eslint-disable-next-line react-hooks/refs -- render-phase sync is intentional and documented
   ref.current = value;
   return ref;
 }

Also worth confirming react-hooks/refs is the correct rule id used by eslint-plugin-react-hooks v6 — if it isn't, the disable is silently ineffective.

eslint-plugin-react-hooks v6 rule names list (react-hooks/refs render-phase mutation)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/hooks/useSyncedRef.ts` around lines 8 - 13, Add an explicit
return type to the public hook by changing the signature of useSyncedRef<T> to
return React.RefObject<T> (ensure React is imported or use import type if your
setup supports it), keep the intentional render-phase assignment to ref.current,
and update the ESLint disable comment: verify the exact rule id for render-phase
ref mutation in your eslint-plugin-react-hooks v6 (if the rule name is different
than react-hooks/refs then replace the comment with the correct rule id) so the
disable is effective.
frontend/src/main.tsx (1)

79-85: Direct CSS variable mutations remain alongside the helper.

Lines 81 and 84 still write --cmdex-last-dark-theme / --cmdex-last-light-theme directly on document.documentElement, while the rest of the theme application has been delegated to applyTheme. For consistency with the centralization goal of this PR, consider moving these two writes into the theme-apply helper (or a dedicated helper) so DOM mutations live in one place.

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

In `@frontend/src/main.tsx` around lines 79 - 85, The code directly sets CSS vars
'--cmdex-last-dark-theme' and '--cmdex-last-light-theme' in the if/else branch
alongside calling setLastDarkTheme/setLastLightTheme; move those DOM mutations
into the centralized theme application helper (e.g., the applyTheme or
theme-apply helper) so that only setLastDarkTheme/setLastLightTheme update state
here and the helper updates document.documentElement.style.setProperty for both
vars; update applyTheme (or create a small function in the theme-apply module)
to accept the newTheme and themeType and perform the two setProperty calls, then
remove the direct setProperty calls from main.tsx.
frontend/src/components/CommandDetailTab.tsx (2)

95-98: Unnecessary spread in boundDraftChange.

Spreading partial into a new object on every call adds allocations without behavior change — onDraftChange already receives a Partial<TabDraft>. You can pass partial through directly (or drop the wrapper entirely and forward onDraftChange from props).

♻️ Proposed simplification
-  const boundDraftChange = useCallback(
-    (partial: Partial<TabDraft>) => onDraftChange({ ...partial }),
-    [onDraftChange],
-  );
+  const boundDraftChange = useCallback(
+    (partial: Partial<TabDraft>) => onDraftChange(partial),
+    [onDraftChange],
+  );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/components/CommandDetailTab.tsx` around lines 95 - 98, The
boundDraftChange callback currently spreads the incoming Partial<TabDraft> into
a new object which creates unnecessary allocations; update the useCallback for
boundDraftChange to call onDraftChange(partial) directly (or remove
boundDraftChange and forward the onDraftChange prop instead) so that
Partial<TabDraft> is passed through without cloning; look for the
boundDraftChange declaration and the onDraftChange prop to implement the direct
pass-through.

128-128: Dead !draft guard.

draft is typed as a required TabDraft on CommandDetailTabProps, so !draft can never be truthy here. Drop it (or make draft optional in the props if undefined is actually expected).

-        saveDisabled={!draft || !draft.scriptBody.trim()}
+        saveDisabled={!draft.scriptBody.trim()}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/components/CommandDetailTab.tsx` at line 128, The expression
uses a redundant `!draft` guard in the `saveDisabled` prop of `CommandDetailTab`
because `draft` is declared required on `CommandDetailTabProps` (type
`TabDraft`); either remove `!draft` from the `saveDisabled` expression so it
becomes `saveDisabled={!draft.scriptBody.trim()}`-style (adjusting for
truthiness) or, if `draft` can legitimately be undefined, make `draft` optional
in the `CommandDetailTabProps` type (`draft?: TabDraft`) and add a runtime check
where `saveDisabled` is computed (e.g., `!draft || !draft.scriptBody.trim()`) so
`CommandDetailTab` and the `saveDisabled` prop remain correct; update the
`CommandDetailTabProps` type and the `saveDisabled` usage accordingly.
frontend/src/App.tsx (4)

1237-1271: handleSaveScript largely duplicates the update branch of handleSaveTab.

Both functions perform the same flow against UpdateCommand: trim the script body, run buildVariablesFromScript, run the var-removal-with-presets gate (skipVarRemovalCheckRef + pendingDirectSaveBodyRef), call UpdateCommand with all draft fields, refresh from GetScriptBody, update tabDrafts/tabBaselines, and conditionally setSelectedCommand. The only meaningful difference is that handleSaveScript uses an explicit scriptBody argument instead of d.scriptBody.

Worth extracting a shared saveExistingCommand(tabId, draft, scriptBodyOverride?) helper so the two stay in sync (e.g. if you ever change toast messages, error handling, or the post-save sync). Not blocking, but the duplicated pendingDirectSaveBodyRef/skipVarRemovalCheckRef orchestration in particular is easy to drift.

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

In `@frontend/src/App.tsx` around lines 1237 - 1271, handleSaveScript duplicates
the update flow found in handleSaveTab; extract the shared logic into a new
helper (e.g. saveExistingCommand(tabId, draft, scriptBodyOverride?)) that:
accepts the tabId and draft (and optional scriptBody override), trims and builds
vars (using buildVariablesFromScript), runs the skipVarRemovalCheckRef /
pendingDirectSaveBodyRef gate and returns early when needed, calls UpdateCommand
with draft fields, reloads data and refreshes the saved draft from
GetScriptBody, updates tabDrafts/tabBaselines and selected command, and emits
the toast and error handling; replace the duplicate bodies in handleSaveScript
and handleSaveTab to call this helper so both flows remain identical and avoid
drift.

1521-1533: Inline-arrow props recreate on every App render and defeat the new CommandDetailTab memoization.

onDraftChange, onSave, and onDiscard are passed as fresh arrow functions on every render of App. Combined with React.memo on CommandDetailTab (frontend/src/components/CommandDetailTab.tsx), this means each open command tab re-renders on every state change in App — losing the perf benefit the per-tab mounts + memo were intended to deliver.

Consider switching these three to the same tabId-injection pattern already used for onExecute/onSaveScript (parent passes the stable handler, child's useCallback injects tabId):

♻️ Proposed change
-                                                onDraftChange={(partial) => updateDraft(tab.id, partial)}
+                                                onDraftChange={updateDraft}
...
-                                                onSave={() => void handleSaveTab(tab.id)}
-                                                onDiscard={() => handleDiscardTab(tab.id)}
+                                                onSave={handleSaveTab}
+                                                onDiscard={handleDiscardTab}

Then in CommandDetailTab.tsx, add tabId injection and adjust the prop types to (tabId, ...)/(tabId).

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

In `@frontend/src/App.tsx` around lines 1521 - 1533, Replace the inline arrow
props that recreate each render (onDraftChange, onSave, onDiscard) by passing
stable handlers from App and letting CommandDetailTab inject the tab id;
specifically, stop calling updateDraft(tab.id, ...) and
handleSaveTab(tab.id)/handleDiscardTab(tab.id) inline and instead pass the
stable functions (e.g., updateDraft, handleSaveTab, handleDiscardTab) as props,
then modify CommandDetailTab (frontend/src/components/CommandDetailTab.tsx) to
accept those stable functions and use useCallback to wrap them with the local
tabId (matching the existing pattern used for onExecute/onSaveScript), and
update the prop types for CommandDetailTab accordingly so memoization is
effective.

1090-1110: handleFillVariables and handleFillVariablesByTab are near-identical.

handleFillVariables (line 1090) and handleFillVariablesByTab (line 1101) do the same thing modulo where the command id comes from. The first is only used by the Cmd/Ctrl+Enter shortcut at line 1323; you can drop it and call handleFillVariablesByTab(selectedCommand.id, currentResolvedValues) instead, after the existing selectedCommand && !isNewCommandTabId(...) guard at line 1317.

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

In `@frontend/src/App.tsx` around lines 1090 - 1110, Remove the duplicate handler
by deleting handleFillVariables and reuse handleFillVariablesByTab: update the
Cmd/Ctrl+Enter shortcut logic (where the code checks selectedCommand &&
!isNewCommandTabId(...)) to call handleFillVariablesByTab(selectedCommand.id,
currentResolvedValues) instead of the removed function; ensure
handleFillVariablesByTab remains exported/defined with the same signature
(tabId: string, initialValues: Record<string,string>) and keeps the
isNewCommandTabId(tabId) guard and GetVariables call so behavior is unchanged.

719-719: Prefer String.prototype.trim() over the equivalent regex.

s.replace(/^\s+|\s+$/g, '') is just s.trim(). Both occurrences (in handleSaveTab and handleSaveScript) can be simplified for readability with no behavior change.

Also applies to: 1241-1241

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

In `@frontend/src/App.tsx` at line 719, Replace the regex-based trimming with
String.prototype.trim() in the save handlers: change occurrences of
d.scriptBody.replace(/^\s+|\s+$/g, '') to d.scriptBody.trim() inside the
handleSaveTab and handleSaveScript code paths (also update the other occurrence
noted around the second location) to improve readability without changing
behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@CONTRIBUTING.md`:
- Line 40: The code span currently contains escaped pipes (`>=20.19.0 \|\|
>=22.13.0 \|\| >=24`) which will render the backslashes literally; update that
code span to use unescaped pipes (`>=20.19.0 || >=22.13.0 || >=24`) so the
Markdown renders correctly and matches the format used in docs/DEVELOPMENT.md
(see the `>=20.19.0 \|\| >=22.13.0 \|\| >=24` text to locate the span).

In `@docs/API.md`:
- Line 402: The per-service API sections use H4 for method headings (e.g., "####
GetVariables(commandID: string)") which skips heading levels and breaks the TOC;
update each service block (e.g., "## CommandService API", "## ExecutionService
API", "## SettingsService API", "## ImportExportService API", "## App Service
API", "## EventService API") so method entries are under a consistent H3 section
— either change each method heading (e.g., "GetVariables", "GetCategories",
etc.) from H4 to H3 or insert a "### Methods" subheading and keep the method
entries as H4 beneath it to match the pattern used by CommandService API and
satisfy markdownlint MD001.
- Around line 425-443: The import path for eventNames in the streaming example
is incorrect for typical component callers; update the import to match the
documented layout (use the relative path used elsewhere from components) — e.g.
replace the current "import { eventNames } from './wails/events';" with the
depth-correct import (from a components TSX consumer) such as "import {
eventNames } from '../wails/events';" and make the same change for the repeated
snippet that currently uses the wrong path; leave the rest of the example
(Events.On, RunCommand, cleanup) unchanged.

In `@docs/ARCHITECTURE.md`:
- Line 342: The document contains conflicting schema versions: the "Schema
version" paragraph (the earlier section mentioning "Schema version: 9, with
incremental migrations from v1 through v9") and the `DB` struct summary (which
says migrations "currently version 10"); choose one resolution and make the
texts consistent by either (A) bumping the earlier "Schema version" section to
v10 and adding a brief description of the v10 migration (what it changes/why)
and updating the incremental migrations list to include v10, or (B) reverting
the `DB` struct summary back to v9 so both sections state v9; update the "Schema
version" paragraph and the `DB` struct line together to match and ensure the
migration list and any mention of FTS5/WAL/migration version numbers reflect the
chosen version.

In `@docs/CONFIGURATION.md`:
- Around line 262-263: The table rows for `windowWidth` and `windowHeight` are
contradictory because they state minimums of 480/400 but cite `main.go`'s
`MinWidth: 900` / `MinHeight: 600` (which apply to the main window) while the
documented defaults (e.g., `windowWidth: 640`, `windowHeight: 520`) are for the
settings window; update the docs by either removing the misleading enforcement
reference or split into separate rows: one for the main window (referencing
`MinWidth`/`MinHeight` in `main.go` and the main-window minima) and one for the
settings window (showing the settings defaults and any re-centering logic), and
ensure the `windowWidth`/`windowHeight` labels and descriptions explicitly
indicate which window each constraint applies to.

In `@docs/DEPLOYMENT.md`:
- Line 228: Remove the leftover author/draft HTML comments like "<!-- VERIFY:
Server mode port and host defaults are from Dockerfile.server — confirm these
match production deployment expectations -->" (and the similar comments at the
other occurrences) from the published document; either delete them or move them
into a maintainer-only checklist (e.g., a separate MAINTAINERS.md or an internal
review TODO list) and replace any public-facing guidance with finalized
verification text, ensuring references such as the verification about server
mode port/host defaults are resolved and the docs contain only user-facing
content.

In `@docs/DEVELOPMENT.md`:
- Line 335: Update the broken Markdown link for "CI workflow" in the
documentation: change the link target currently written as
(.github/workflows/ci.yml) so it points to the repository root relative path
(../.github/workflows/ci.yml) so the "CI workflow" link resolves correctly from
the docs/DEVELOPMENT.md location.

In `@docs/GETTING-STARTED.md`:
- Around line 131-139: The docs currently reference the old Wails import path
"../wailsjs/go/main/App" which is stale for Wails v3; update the example text to
use the actual generated bindings path (e.g. "frontend/bindings" and examples
like "../bindings/cmdex/settingsservice" as used in frontend/src/main.tsx) so
the instructions and import example match the project's Wails v3 layout and the
regeneration command output.

In `@frontend/src/components/CommandDetail.tsx`:
- Around line 606-611: The copy-failure toast is unreachable because
useCopyToClipboard swallows promise rejections; update the hook
(useCopyToClipboard) so that the copy function it returns does not swallow
errors but either re-throws the caught error or returns a rejected Promise when
the underlying clipboard operation fails, allowing callers (like handleCopy in
CommandDetail.tsx) to catch and handle the error; ensure the exported copy
function preserves the original rejection behavior instead of swallowing it.

In `@frontend/src/components/CommandDetailTab.tsx`:
- Around line 126-131: CommandDetailTab wrapped in React.memo is being
invalidated because App.tsx passes fresh inline arrow props (onDraftChange,
onSave, onDiscard) each render; stabilize them by either: 1) stop passing
per-tab inline arrows and instead pass the memoized handlers (updateDraft,
handleSaveTab, handleDiscardTab) plus the tab id (tab.id) and let
CommandDetailTab bind those to produce stable callbacks internally (similar to
onExecute/onSaveScript), or 2) memoize the per-tab callbacks in App.tsx using
useCallback keyed on tab.id so the function identity is stable across renders;
update references to onDraftChange, onSave, and onDiscard accordingly so
React.memo can short-circuit re-renders.

In `@frontend/src/components/OutputPane.tsx`:
- Around line 106-114: Update handleCopy and handleCopyCommand to show a failure
toast when the copy promise rejects: wrap copyOutput(allOutputText) and
copyCommand(record.finalCmd) in try/catch (or await them) and on error call the
same toast pattern used in CommandDetail.tsx with the new i18n key
"outputPane.copyFailed". Keep early returns for missing data (allOutputText /
record?.finalCmd), and reference the existing functions handleCopy,
handleCopyCommand, copyOutput, copyCommand, and record.finalCmd so the error
path surfaces clipboard failures to the user.

In `@frontend/src/components/SettingsPage.tsx`:
- Around line 322-329: The cleanup currently runs on every dependency change
because useEffect returns a cleanup but lists
savedTheme/savedDensity/savedUiFont/savedMonoFont/customThemes as deps; change
this so the restore logic only runs on unmount by creating a synced ref of the
latest saved values (use the new useSyncedRef hook) that stores {savedTheme,
savedDensity, savedUiFont, savedMonoFont, customThemes}, then replace the effect
with useEffect(() => { return () => { const {savedTheme, savedDensity,
savedUiFont, savedMonoFont, customThemes} = syncedRef.current; const custom =
customThemes?.find(c => c.id === savedTheme); applyTheme(savedTheme,
custom?.colors ?? null); applyDensity(savedDensity); applyFonts(savedUiFont,
savedMonoFont); }; }, []); reference useSyncedRef, applyTheme, applyDensity,
applyFonts, customThemes, and the saved* variables to locate and update the
code.

In `@frontend/src/hooks/useCopyToClipboard.ts`:
- Around line 7-16: The copy function inside useCopyToClipboard (the async
callback created with useCallback) currently swallows errors in its try/catch so
callers like CommandDetail.tsx never see rejections; modify the copy
implementation to allow consumers to observe failures by either re-throwing the
caught error after logging (or removing the try/catch and letting the rejection
propagate), keeping the existing setCopied, timerRef, and resetMs logic intact
so the success path still behaves the same.

In `@frontend/src/lib/theme-apply.ts`:
- Around line 26-32: The applyFonts function currently interpolates uiFont and
monoFont directly into single-quoted CSS values which breaks if names contain
single quotes or backslashes and also misbehaves for empty strings; update
applyFonts to treat empty uiFont as 'System Default', and sanitize/escape any
single quotes or backslashes in uiFont and monoFont (or wrap values in double
quotes and escape any embedded double quotes/backslashes) before calling
document.documentElement.style.setProperty('--font-sans', ...) and
setProperty('--font-mono', ...), ensuring the final CSS token is always a valid
quoted font-family string.

In `@README.md`:
- Line 31: The README and DEPLOYMENT docs disagree on the macOS app bundle name:
README uses /Applications/CmDex.app while DEPLOYMENT.md references
/Applications/cmdex.app; align them to the canonical name from wails.json (name:
"CmDex") by updating the occurrence of "cmdex.app" in DEPLOYMENT.md to
"CmDex.app" so both docs use the exact same bundle path
(/Applications/CmDex.app).

---

Outside diff comments:
In `@docs/GETTING-STARTED.md`:
- Line 12: Update the Node.js prerequisite in the GETTING-STARTED.md entry that
currently reads "**Node.js** `>= 25`" to match the project's unified engines
constraint (use "`>=20.19.0 || >=22.13.0 || >=24`") so it aligns with the
changes made to CONTRIBUTING.md and docs/DEVELOPMENT.md and
frontend/package.json; simply replace the version string in that Node.js line to
the unified expression.

In `@docs/TESTING.md`:
- Around line 9-16: Update the docs to remove the contradictory blanket
statement in Section 1 that "Cmdex currently has no automated tests" and instead
state that Go DB tests now exist; specifically mention db_test.go and its tests
TestFreshDBMigrations, TestExistingDBIdempotent, and TestRollbackTo are present
and exercised in the doc, while frontend/unit tests are still missing and CI
should be noted to not run frontend tests. Also update the priority-targets
table note about using a temporary SQLite file or ":memory:" to reflect that
db_test.go already uses an in-memory/temporary SQLite setup so the guidance is
no longer required.

In `@frontend/src/main.tsx`:
- Around line 37-62: The initial theme application calls applyTheme(t) before
parsing persisted customThemes, so custom theme CSS variables are never applied
on load; update the GetSettings().then handler in main.tsx to parse
s.customThemes (use JSON.parse with try/catch) and call setCustomThemes(...) and
set customThemesStrRef.current before calling applyTheme; then, when resolving
the theme ID t, derive the matching customColors from the parsed customThemes
array and pass those as the second argument to applyTheme(t, customColors)
(ensure applyTheme is still called with defaults when no matching custom theme
is found).

---

Nitpick comments:
In `@frontend/src/App.tsx`:
- Around line 1237-1271: handleSaveScript duplicates the update flow found in
handleSaveTab; extract the shared logic into a new helper (e.g.
saveExistingCommand(tabId, draft, scriptBodyOverride?)) that: accepts the tabId
and draft (and optional scriptBody override), trims and builds vars (using
buildVariablesFromScript), runs the skipVarRemovalCheckRef /
pendingDirectSaveBodyRef gate and returns early when needed, calls UpdateCommand
with draft fields, reloads data and refreshes the saved draft from
GetScriptBody, updates tabDrafts/tabBaselines and selected command, and emits
the toast and error handling; replace the duplicate bodies in handleSaveScript
and handleSaveTab to call this helper so both flows remain identical and avoid
drift.
- Around line 1521-1533: Replace the inline arrow props that recreate each
render (onDraftChange, onSave, onDiscard) by passing stable handlers from App
and letting CommandDetailTab inject the tab id; specifically, stop calling
updateDraft(tab.id, ...) and handleSaveTab(tab.id)/handleDiscardTab(tab.id)
inline and instead pass the stable functions (e.g., updateDraft, handleSaveTab,
handleDiscardTab) as props, then modify CommandDetailTab
(frontend/src/components/CommandDetailTab.tsx) to accept those stable functions
and use useCallback to wrap them with the local tabId (matching the existing
pattern used for onExecute/onSaveScript), and update the prop types for
CommandDetailTab accordingly so memoization is effective.
- Around line 1090-1110: Remove the duplicate handler by deleting
handleFillVariables and reuse handleFillVariablesByTab: update the
Cmd/Ctrl+Enter shortcut logic (where the code checks selectedCommand &&
!isNewCommandTabId(...)) to call handleFillVariablesByTab(selectedCommand.id,
currentResolvedValues) instead of the removed function; ensure
handleFillVariablesByTab remains exported/defined with the same signature
(tabId: string, initialValues: Record<string,string>) and keeps the
isNewCommandTabId(tabId) guard and GetVariables call so behavior is unchanged.
- Line 719: Replace the regex-based trimming with String.prototype.trim() in the
save handlers: change occurrences of d.scriptBody.replace(/^\s+|\s+$/g, '') to
d.scriptBody.trim() inside the handleSaveTab and handleSaveScript code paths
(also update the other occurrence noted around the second location) to improve
readability without changing behavior.

In `@frontend/src/components/CommandDetailTab.tsx`:
- Around line 95-98: The boundDraftChange callback currently spreads the
incoming Partial<TabDraft> into a new object which creates unnecessary
allocations; update the useCallback for boundDraftChange to call
onDraftChange(partial) directly (or remove boundDraftChange and forward the
onDraftChange prop instead) so that Partial<TabDraft> is passed through without
cloning; look for the boundDraftChange declaration and the onDraftChange prop to
implement the direct pass-through.
- Line 128: The expression uses a redundant `!draft` guard in the `saveDisabled`
prop of `CommandDetailTab` because `draft` is declared required on
`CommandDetailTabProps` (type `TabDraft`); either remove `!draft` from the
`saveDisabled` expression so it becomes
`saveDisabled={!draft.scriptBody.trim()}`-style (adjusting for truthiness) or,
if `draft` can legitimately be undefined, make `draft` optional in the
`CommandDetailTabProps` type (`draft?: TabDraft`) and add a runtime check where
`saveDisabled` is computed (e.g., `!draft || !draft.scriptBody.trim()`) so
`CommandDetailTab` and the `saveDisabled` prop remain correct; update the
`CommandDetailTabProps` type and the `saveDisabled` usage accordingly.

In `@frontend/src/hooks/useCopyToClipboard.ts`:
- Line 3: Add a JSDoc block for the hook function useCopyToClipboard that
documents the API contract: describe the resetMs parameter (default 1500ms) and
its effect (how long the success/err state is cleared), the return shape (e.g.,
tuple or object containing the copy function, a boolean success flag, and any
error/message), and how errors are surfaced/handled (whether the hook throws,
returns an error value, or sets an error state). Place the JSDoc immediately
above the export function useCopyToClipboard declaration and include types,
parameter description, return description, and any edge-case behavior callers
must handle.

In `@frontend/src/hooks/useResizable.ts`:
- Around line 22-29: Replace the manual size→ref syncing logic by using the new
helper: remove the useRef(size) + useEffect block and instead create sizeRef
with useSyncedRef(size) so sizeRef always mirrors the latest size; update
references that use sizeRef (symbol: sizeRef) and ensure useSyncedRef is
imported and used inside the useResizable hook (symbols: useResizable, size).
- Around line 12-13: Add a JSDoc block to the useResizable hook that documents
the UseResizableOptions properties (axis, direction, minSize, maxSize,
defaultSize, storageKey) and the hook's return shape; explicitly explain allowed
values for axis and direction, the semantics of storageKey (it is used as a
prefix and has "-size" appended), and the units/constraints for minSize/maxSize
and defaultSize so callers understand expected types and behavior; place the
JSDoc immediately above the export function useResizable and reference
UseResizableOptions and the returned tuple/object shape in the description.

In `@frontend/src/hooks/useSyncedRef.ts`:
- Around line 8-13: Add an explicit return type to the public hook by changing
the signature of useSyncedRef<T> to return React.RefObject<T> (ensure React is
imported or use import type if your setup supports it), keep the intentional
render-phase assignment to ref.current, and update the ESLint disable comment:
verify the exact rule id for render-phase ref mutation in your
eslint-plugin-react-hooks v6 (if the rule name is different than
react-hooks/refs then replace the comment with the correct rule id) so the
disable is effective.

In `@frontend/src/main.tsx`:
- Around line 79-85: The code directly sets CSS vars '--cmdex-last-dark-theme'
and '--cmdex-last-light-theme' in the if/else branch alongside calling
setLastDarkTheme/setLastLightTheme; move those DOM mutations into the
centralized theme application helper (e.g., the applyTheme or theme-apply
helper) so that only setLastDarkTheme/setLastLightTheme update state here and
the helper updates document.documentElement.style.setProperty for both vars;
update applyTheme (or create a small function in the theme-apply module) to
accept the newTheme and themeType and perform the two setProperty calls, then
remove the direct setProperty calls from main.tsx.

In `@frontend/src/utils/path.ts`:
- Around line 37-43: The separator detection in shortenPath is brittle for
mixed-separator inputs (e.g., "C:\Users/project/..."): update shortenPath to
choose the rejoin separator deterministically by inspecting the path's
separators (for example, count occurrences of '/' vs '\' and pick the majority,
or pick the first separator character found) or normalize the input before
splitting; change the logic around const sep = ... in shortenPath to compute sep
by majority/first-occurrence (or normalize separators) so the returned '...' +
sep + parts.slice(-segments).join(sep) uses the appropriate separator
consistently.

In `@frontend/src/utils/tab.ts`:
- Around line 27-29: In the truncation logic in frontend/src/utils/tab.ts (the
function that returns a snippet for body, containing the lines that check
body.length and slice to 50), replace the current slice+ '...' with a trimmed
slice to avoid trailing whitespace before the ellipsis — e.g. use body.slice(0,
50).trimEnd() plus the ellipsis; optionally switch the appended "..." to the
single‑char "…" to match scriptSnippet's style so update the return that
currently does return body.slice(0, 50) + '...' accordingly.
- Around line 18-23: The shebang stripping in getCommandDisplayTitle is too
specific (only '#!/bin/bash') and should use a generic regex to match any
shebang like scriptSnippet does; update the logic in frontend/src/utils/tab.ts
(inside getCommandDisplayTitle / where body is derived from cmd.scriptContent)
to remove a leading shebang using a regex such as /^#!.*\n?/ so that any
interpreter line (e.g., #!/usr/bin/env bash, #!/bin/sh, #!/usr/bin/env python)
is stripped before truncating/displaying the title.
🪄 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: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: a1b288bd-ac43-4142-9481-761bf28b8b9d

📥 Commits

Reviewing files that changed from the base of the PR and between 882c7e4 and 461e50c.

📒 Files selected for processing (25)
  • CONTRIBUTING.md
  • README.md
  • docs/API.md
  • docs/ARCHITECTURE.md
  • docs/CONFIGURATION.md
  • docs/DEPLOYMENT.md
  • docs/DEVELOPMENT.md
  • docs/GETTING-STARTED.md
  • docs/TESTING.md
  • frontend/src/App.tsx
  • frontend/src/components/CommandDetail.tsx
  • frontend/src/components/CommandDetailTab.tsx
  • frontend/src/components/CommandPalette.tsx
  • frontend/src/components/OutputPane.tsx
  • frontend/src/components/ResizablePanel.tsx
  • frontend/src/components/SettingsPage.tsx
  • frontend/src/components/Sidebar.tsx
  • frontend/src/hooks/useCopyToClipboard.ts
  • frontend/src/hooks/useResizable.ts
  • frontend/src/hooks/useSyncedRef.ts
  • frontend/src/lib/theme-apply.ts
  • frontend/src/main.tsx
  • frontend/src/types.ts
  • frontend/src/utils/path.ts
  • frontend/src/utils/tab.ts
💤 Files with no reviewable changes (1)
  • frontend/src/types.ts

Comment thread CONTRIBUTING.md
Comment thread docs/API.md
Comment thread docs/API.md
Comment thread docs/ARCHITECTURE.md

| Abstraction | File | Description |
|---|---|---|
| `DB` struct | `db.go` | Database wrapper around `database/sql` with `modernc.org/sqlite` driver. Manages migrations (currently version 10), FTS5 full-text search triggers, WAL journal mode, and provides all CRUD query methods used by services. |
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Schema version conflicts with earlier section.

Line 342 states the DB struct manages migrations "currently version 10", but line 83 above in the same doc still says "Schema version: 9, with incremental migrations from v1 through v9." Please reconcile — either bump the earlier section to v10 (and add the v10 migration description) or correct line 342 back to v9.

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

In `@docs/ARCHITECTURE.md` at line 342, The document contains conflicting schema
versions: the "Schema version" paragraph (the earlier section mentioning "Schema
version: 9, with incremental migrations from v1 through v9") and the `DB` struct
summary (which says migrations "currently version 10"); choose one resolution
and make the texts consistent by either (A) bumping the earlier "Schema version"
section to v10 and adding a brief description of the v10 migration (what it
changes/why) and updating the incremental migrations list to include v10, or (B)
reverting the `DB` struct summary back to v9 so both sections state v9; update
the "Schema version" paragraph and the `DB` struct line together to match and
ensure the migration list and any mention of FTS5/WAL/migration version numbers
reflect the chosen version.

Comment thread docs/CONFIGURATION.md
Comment on lines +262 to +263
| `windowWidth` | Minimum 480px | Main window config in `main.go` (`MinWidth: 900`) and settings window re-centering logic |
| `windowHeight` | Minimum 400px | Main window config in `main.go` (`MinHeight: 600`) and settings window re-centering logic |
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Validation constraints contradict the cited values.

The windowWidth/windowHeight rows claim a minimum of 480/400 but cite main.go's MinWidth: 900 / MinHeight: 600 as the enforcement point. Those are also for the main window, while the documented windowWidth: 640 / windowHeight: 520 defaults are for the settings window. Either drop the contradictory enforcement reference or split into separate rows for main vs. settings windows so readers don't end up with conflicting numbers.

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

In `@docs/CONFIGURATION.md` around lines 262 - 263, The table rows for
`windowWidth` and `windowHeight` are contradictory because they state minimums
of 480/400 but cite `main.go`'s `MinWidth: 900` / `MinHeight: 600` (which apply
to the main window) while the documented defaults (e.g., `windowWidth: 640`,
`windowHeight: 520`) are for the settings window; update the docs by either
removing the misleading enforcement reference or split into separate rows: one
for the main window (referencing `MinWidth`/`MinHeight` in `main.go` and the
main-window minima) and one for the settings window (showing the settings
defaults and any re-centering logic), and ensure the
`windowWidth`/`windowHeight` labels and descriptions explicitly indicate which
window each constraint applies to.

Comment thread frontend/src/components/OutputPane.tsx Outdated
Comment thread frontend/src/components/SettingsPage.tsx Outdated
Comment thread frontend/src/hooks/useCopyToClipboard.ts
Comment thread frontend/src/lib/theme-apply.ts
Comment thread README.md
@loco1842
Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 27, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown
Contributor

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

🧹 Nitpick comments (3)
frontend/src/components/SettingsPage.tsx (1)

328-328: Nit: the inline eslint-disable-next-line react-hooks/exhaustive-deps on Line 328 is misplaced.

react-hooks/exhaustive-deps only fires on the dependency array (already suppressed at Line 335). Reading savedSettingsRef.current inside a cleanup function does not trigger that rule, so this comment is a no-op and just adds noise. Safe to drop:

♻️ Suggested cleanup
   useEffect(() => {
     return () => {
-      // eslint-disable-next-line react-hooks/exhaustive-deps -- syncedRef.current is intentionally read in cleanup for latest value
       const { savedTheme: st, savedDensity: sd, savedUiFont: suf, savedMonoFont: smf, customThemes: ct } = savedSettingsRef.current;
       const custom = ct?.find(c => c.id === st);
       applyTheme(st, custom?.colors ?? null);
       applyDensity(sd);
       applyFonts(suf, smf);
     };
   }, []); // eslint-disable-line react-hooks/exhaustive-deps
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/components/SettingsPage.tsx` at line 328, Remove the misplaced
inline eslint-disable-next-line react-hooks/exhaustive-deps that sits before the
cleanup reading savedSettingsRef.current in SettingsPage.tsx; the rule only
targets dependency arrays (the suppression already applied on the useEffect
dependency array) so drop the no-op disable comment to clean up noise while
leaving the existing suppression on the dependency array intact.
frontend/src/hooks/useResizable.ts (2)

12-12: Add a JSDoc block for this non-obvious hook.

Per the repo guideline for frontend/src/hooks/**/*.ts, non-obvious hooks should carry JSDoc. useResizable has several behavioral nuances worth documenting (axis/direction semantics, persisted bounds clamping on init, that handleStart must be wired to a mousedown and receives the axis-relevant client coordinate, persistence happens on mouseup).

📝 Suggested JSDoc
+/**
+ * Reusable resize hook backed by `localStorage`.
+ *
+ * Tracks a single dimension (`axis: 'x' | 'y'`) clamped to `[minSize, maxSize]`,
+ * initialized from `localStorage[storageKey]` (falling back to `defaultSize`).
+ * `direction` controls whether positive cursor movement grows (`1`) or shrinks
+ * (`-1`) the size — e.g. a top-edge resize handle uses `-1`.
+ *
+ * Wire `handleStart(e.clientX | e.clientY)` to a `mousedown` handler on the
+ * resize handle. The hook attaches `window` `mousemove`/`mouseup` listeners
+ * for the duration of the drag and persists the final size on mouseup.
+ */
 export function useResizable(options: UseResizableOptions) {

As per coding guidelines: "JSDoc-style blocks must be used on non-obvious hooks (e.g., useKeyboardShortcuts behavior)".

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

In `@frontend/src/hooks/useResizable.ts` at line 12, Add a JSDoc block above the
useResizable function documenting its semantics: explain axis vs direction
behavior, which coordinate is used for each axis, that persisted bounds are
clamped on initialization, and that persistence occurs on mouseup; also call out
that handleStart must be wired to a mousedown listener and that it receives the
axis-relevant client coordinate (e.g., clientX for horizontal, clientY for
vertical). Reference the exported useResizable function and its handleStart
handler in the comment, list the main options in UseResizableOptions and the
shape of the returned API, and include brief examples of wiring handleStart and
expected lifecycle (start → move → mouseup persistence).

49-56: Consider extracting localStorage persistence to an effect rather than using a state updater.

The current approach of reading localStorage inside a state updater works but triggers the side effect during the commit phase. Under React 19 StrictMode, state updaters intentionally run twice in development to detect impure functions. While the localStorage writes are idempotent today, this pattern invites footguns if logic changes.

The proposed fix using sizeRef.current would be incorrect: the ref lags behind the actual state during the event handler (effects run after the commit phase). A safer approach is to use an effect that depends on size and isDragging:

♻️ Suggested improvement
+  useEffect(() => {
+    if (!isDragging && size !== undefined) {
+      localStorage.setItem(storageKey, String(size));
+    }
+  }, [size, isDragging, storageKey]);

This removes I/O from the updater, runs after the commit phase, and eliminates side effects during re-render.

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

In `@frontend/src/hooks/useResizable.ts` around lines 49 - 56, The onMouseUp
handler currently writes to localStorage inside the setSize state updater (in
onMouseUp), which causes a side effect during the commit phase; remove
localStorage.setItem from the setSize updater so setSize only updates state, and
add an effect that persists size after commits: create a useEffect that watches
[size, isDragging] and when isDragging is false write
localStorage.setItem(storageKey, String(size)); keep the existing isDragging
state (setIsDragging) and isDraggingRef toggles in the handlers but move all
persistence out of setSize/onMouseUp into the effect to avoid I/O inside state
updaters.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@frontend/src/components/SettingsPage.tsx`:
- Line 328: Remove the misplaced inline eslint-disable-next-line
react-hooks/exhaustive-deps that sits before the cleanup reading
savedSettingsRef.current in SettingsPage.tsx; the rule only targets dependency
arrays (the suppression already applied on the useEffect dependency array) so
drop the no-op disable comment to clean up noise while leaving the existing
suppression on the dependency array intact.

In `@frontend/src/hooks/useResizable.ts`:
- Line 12: Add a JSDoc block above the useResizable function documenting its
semantics: explain axis vs direction behavior, which coordinate is used for each
axis, that persisted bounds are clamped on initialization, and that persistence
occurs on mouseup; also call out that handleStart must be wired to a mousedown
listener and that it receives the axis-relevant client coordinate (e.g., clientX
for horizontal, clientY for vertical). Reference the exported useResizable
function and its handleStart handler in the comment, list the main options in
UseResizableOptions and the shape of the returned API, and include brief
examples of wiring handleStart and expected lifecycle (start → move → mouseup
persistence).
- Around line 49-56: The onMouseUp handler currently writes to localStorage
inside the setSize state updater (in onMouseUp), which causes a side effect
during the commit phase; remove localStorage.setItem from the setSize updater so
setSize only updates state, and add an effect that persists size after commits:
create a useEffect that watches [size, isDragging] and when isDragging is false
write localStorage.setItem(storageKey, String(size)); keep the existing
isDragging state (setIsDragging) and isDraggingRef toggles in the handlers but
move all persistence out of setSize/onMouseUp into the effect to avoid I/O
inside state updaters.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: c9ed7978-d633-4f48-a121-089bbebc4757

📥 Commits

Reviewing files that changed from the base of the PR and between 461e50c and 5f8bb2b.

📒 Files selected for processing (5)
  • frontend/src/components/OutputPane.tsx
  • frontend/src/components/SettingsPage.tsx
  • frontend/src/hooks/useCopyToClipboard.ts
  • frontend/src/hooks/useResizable.ts
  • frontend/src/locales/en.json
✅ Files skipped from review due to trivial changes (1)
  • frontend/src/locales/en.json
🚧 Files skipped from review as they are similar to previous changes (1)
  • frontend/src/hooks/useCopyToClipboard.ts

@loco1842 loco1842 merged commit 7a70a3d into main Apr 27, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant