Skip to content

Feat/separate command editor windows#35

Merged
loco1842 merged 15 commits into
mainfrom
feat/separate-command-editor-windows
Apr 24, 2026
Merged

Feat/separate command editor windows#35
loco1842 merged 15 commits into
mainfrom
feat/separate-command-editor-windows

Conversation

@loco1842
Copy link
Copy Markdown
Owner

@loco1842 loco1842 commented Apr 24, 2026

Summary by CodeRabbit

  • New Features

    • Per-tab command view: each open tab preserves its DOM state and saves/refreshes independently.
    • Tab-scoped actions: run/save/delete and presets operate per tab to avoid cross-tab interference.
  • Bug Fixes

    • Prevented inactive tabs from overwriting current selection or variables.
    • Execution indicator shows only the actively executing tab.
  • Performance

    • Reduced unnecessary re-renders of command detail for smoother responsiveness.

…memo

- Export CommandDetail wrapped in React.memo to prevent inactive tab re-renders
- Rely on default shallow prop comparison (no custom compare function)
- React DevTools display name preserved automatically
…ories

- Add makeHandleExecute, makeHandleRunInTerminal factories
- Add makeHandleFillVariables, makeHandleDelete factories
- Add makeHandleAddPreset, makeHandleRenamePreset, makeHandleDeletePreset factories
- Add makeHandleReorderPresets, makeHandleSavePresetValues factories
- Add makeHandleSaveScript factory with tabId parameter
- Replace refreshSelectedCommand with parameterized refreshCommand
- Update CommandDetail JSX to use factory functions keyed by activeTabId
- Keep legacy handlers (handleExecute, handleFillVariables, handleDeleteCommand,
  handleSaveScriptDirect) for keyboard shortcuts, Sidebar, and AlertDialog
- Remove dead code: handleManagePresets, handleAddPresetFromDetail,
  handleRenamePresetFromDetail, handleDeletePresetFromDetail,
  handleReorderPresetsFromDetail, handleSavePresetValuesFromDetail
…ctory refactor plan

- Add 01-02-SUMMARY.md with execution details, decisions, and self-check
- Replace single CommandDetail mount with iteration over openTabs
- Each tab gets its own .command-tab-shell wrapper with display toggle
- Inactive tabs hidden via inline style display:none (DOM persists)
- Welcome and loading views preserved as separate conditional blocks
- Gate onResolvedValuesChange to active tab only (undefined for inactive)
- Inactive tabs use draft-based variableDefinitionsToPrompts fallback
- Per-tab FloatingSaveBar with tab-specific dirty state
- Remove dead code: isNew and saveDisabled variables (now computed per-tab)
- Add comment explaining animation applies to entire main-content area
- Note that inactive shells are display:none during fade
- Verified no breaking :only-child or :first-child CSS selectors exist
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 24, 2026

📝 Walkthrough

Walkthrough

Reworks tab handling to mount a CommandDetail per open tab, introduces per-tab handler factories cached in tabHandlerCacheRef, ensures only the active tab shows executing state via executingTabIdState, stabilizes inactive-tab variables with tabVariablesMap, and memoizes CommandDetail with ref-based resolved-value change detection.

Changes

Cohort / File(s) Summary
App — per-tab mounting & handlers
frontend/src/App.tsx
Mount one CommandDetail (and FloatingSaveBar) per open tab (inactive tabs display:none); replace shared handlers with tab-scoped makeHandle* factories; add tabHandlerCacheRef, selectedCommandRef, executingTabIdState; guard async ops to update only originating tab; memoize/cleanup handler cache; introduce tabVariablesMap for inactive-tab variable stability.
CommandDetail — render and resolved-values
frontend/src/components/CommandDetail.tsx
Track last resolvedValues in a ref and call onResolvedValuesChange only when stringified key values change; default export wrapped with React.memo to reduce re-renders.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant App as App.tsx
    participant Detail as CommandDetail
    participant API as Backend API
    participant Toast

    User->>App: Trigger action in tab (execute/save/run/fill/delete)
    App->>Detail: Lookup per-tab handler from tabHandlerCacheRef and invoke
    Detail->>API: Call backend with tabId
    API-->>Detail: Respond success/error
    Detail-->>App: Return result (tab-scoped)
    App->>App: Update executingTabIdState (start/clear) and refresh only that tab's data
    App->>Toast: Show success or error
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Poem

🐰
Each tab gets its own little den to keep,
Handlers hop in, tidy code in a heap,
Refs hold the whispers of values so small,
Memos hush re-renders until the call,
I thump my foot — the app stands neat and tall! 🎉

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Feat/separate command editor windows' directly aligns with the PR's main objective of implementing separate command editor windows/tabs with stable per-tab behavior.
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: 3

🧹 Nitpick comments (2)
frontend/src/App.tsx (1)

1036-1047: Unused t in makeHandleFillVariables deps.

The callback body doesn't reference t, so including it in the dep array needlessly recreates the factory on every language change and invalidates React.memo on downstream CommandDetail more often than required. Drop t (and the surrounding eslint disable isn't needed here since the deps would then be empty).

♻️ Proposed fix
     const makeHandleFillVariables = useCallback((tabId: string) => {
         return async (initialValues: Record<string, string>) => {
             if (isNewCommandTabId(tabId)) return;
             const vars = await GetVariables(tabId);
             setModal({
                 type: 'fillVariables',
                 variables: vars || [],
                 commandId: tabId,
                 initialValues,
             });
         };
-    }, [t]);
+    }, []);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/App.tsx` around lines 1036 - 1047, The useCallback factory
makeHandleFillVariables unnecessarily lists the translation hook `t` in its
dependency array even though the callback body doesn't reference it; remove `t`
from the deps (use an empty array or only real dependencies) so
makeHandleFillVariables is stable across language changes, and also remove the
surrounding eslint-disable comment since it will no longer be needed; locate
makeHandleFillVariables, which calls isNewCommandTabId, GetVariables, and
setModal, and update its dependency array accordingly.
frontend/src/components/CommandDetail.tsx (1)

1355-1355: React.memo effectiveness is limited by unstable callback props.

The memoization will only skip renders when all props are shallowly equal. However, App.tsx passes freshly-created callbacks on every render (e.g. onDraftChange={(partial) => updateDraft(tab.id, partial)} inline arrow, plus makeHandleExecute(tab.id), makeHandleRunInTerminal(tab.id), etc.—each makeHandle* factory returns a new closure per invocation). As a result, CommandDetail will re-render on virtually every App render regardless of React.memo.

The DOM-persistence benefit from display: none per-tab mounts is still achieved, but if you want React.memo to actually skip renders, consider either:

  • Caching the per-tab handler bundles in a useRef<Map<string, Handlers>> keyed by tabId, or
  • Passing tabId as a prop and lifting the per-tab factory calls inside CommandDetail (so the child closes over a stable updateDraft/factory function received once).
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/components/CommandDetail.tsx` at line 1355, CommandDetail is
wrapped with React.memo but still re-renders because App.tsx passes
freshly-created callbacks (e.g., onDraftChange={(partial) => updateDraft(tab.id,
partial)}, makeHandleExecute(tab.id), makeHandleRunInTerminal(tab.id)), so
memoization is ineffective; fix by creating a stable per-tab handlers cache in
App using a useRef<Map<string, Handlers>> keyed by tab.id (store/reuse the
closures for makeHandleExecute, makeHandleRunInTerminal, onDraftChange, etc.)
and pass the cached handler object reference to CommandDetail (or alternatively
pass only tabId and move the makeHandle* calls into CommandDetail so it closes
over stable updateDraft/factory functions), ensuring the props received by
CommandDetail are referentially stable so React.memo can skip renders.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@frontend/src/App.tsx`:
- Around line 1296-1300: handleSaveScriptDirect currently reads activeTabId but
the confirm dialog flow stores the intended tab in modal.tabId and the body in
pendingDirectSaveBodyRef, so a tab switch causes the wrong target; change
handleSaveScriptDirect signature to accept a tabId (e.g. handleSaveScriptDirect
= useCallback(async (tabId: string, scriptBody: string) => { ... }) ), use that
tabId to validate (isNewCommandTabId(tabId)) and to call
makeHandleSaveScript(tabId), and update the confirm dialog action (where
pendingDirectSaveBodyRef.current is used) to call
handleSaveScriptDirect(modal.tabId, pendingDirectSaveBodyRef.current); ensure
any other callers are updated accordingly and keep the existing
handleSaveTab(modal.tabId) branch consistent.
- Around line 1192-1249: Wrap all async Wails binding calls in the preset
factory functions (makeHandleAddPreset, makeHandleRenamePreset,
makeHandleDeletePreset, makeHandleReorderPresets, makeHandleSavePresetValues)
with try/catch and surface failures to the user via toast.error; specifically,
in each returned async handler, perform the
SavePreset/UpdatePreset/DeletePreset/ReorderPresets call inside try { await ...;
await refreshCommand(tabId); (any success toasts) } catch (err) {
console.error(..., err); toast.error(t('toast.someErrorKey') or err.message); }
(for reorder/others keep refreshCommand in finally or call it in both paths as
appropriate) so that backend rejections no longer silently fail and users get
feedback.
- Around line 1183-1190: refreshCommand currently unconditionally calls
setSelectedCommand(refreshed) which can overwrite whatever tab the user is
viewing; change it to only update selectedCommand when the refreshed command
matches the currently selected one (compare refreshed.id to the current
selectedCommand.id) so background refreshes don't clobber another tab, and
likewise update makeHandleReorderPresets so its setSelectedCommand((prev) =>
...) only applies the optimistic reordered presets when prev && prev.id ===
tabId (no-op otherwise); use the existing allCommandsRef/current selectedCommand
accessor to perform the id checks before calling setSelectedCommand.

---

Nitpick comments:
In `@frontend/src/App.tsx`:
- Around line 1036-1047: The useCallback factory makeHandleFillVariables
unnecessarily lists the translation hook `t` in its dependency array even though
the callback body doesn't reference it; remove `t` from the deps (use an empty
array or only real dependencies) so makeHandleFillVariables is stable across
language changes, and also remove the surrounding eslint-disable comment since
it will no longer be needed; locate makeHandleFillVariables, which calls
isNewCommandTabId, GetVariables, and setModal, and update its dependency array
accordingly.

In `@frontend/src/components/CommandDetail.tsx`:
- Line 1355: CommandDetail is wrapped with React.memo but still re-renders
because App.tsx passes freshly-created callbacks (e.g., onDraftChange={(partial)
=> updateDraft(tab.id, partial)}, makeHandleExecute(tab.id),
makeHandleRunInTerminal(tab.id)), so memoization is ineffective; fix by creating
a stable per-tab handlers cache in App using a useRef<Map<string, Handlers>>
keyed by tab.id (store/reuse the closures for makeHandleExecute,
makeHandleRunInTerminal, onDraftChange, etc.) and pass the cached handler object
reference to CommandDetail (or alternatively pass only tabId and move the
makeHandle* calls into CommandDetail so it closes over stable
updateDraft/factory functions), ensuring the props received by CommandDetail are
referentially stable so React.memo can skip renders.
🪄 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: 2430a785-f1f2-4553-9298-ad676fa96bda

📥 Commits

Reviewing files that changed from the base of the PR and between 1fd21ea and df793e2.

⛔ Files ignored due to path filters (2)
  • .planning/PROJECT.md is excluded by !.planning/*
  • .planning/ROADMAP.md is excluded by !.planning/*
📒 Files selected for processing (2)
  • frontend/src/App.tsx
  • frontend/src/components/CommandDetail.tsx

Comment thread frontend/src/App.tsx
Comment thread frontend/src/App.tsx
Comment thread frontend/src/App.tsx Outdated
- handleSaveScriptDirect now accepts explicit tabId to prevent wrong-target saves during tab switches
- Add try/catch + toast.error to all preset factory functions (add/rename/delete/reorder/save-values)
- Gate refreshCommand and optimistic reorder updates to only affect matching selectedCommand
- Remove unnecessary 't' dependency from makeHandleFillVariables
- Add tabHandlerCacheRef to provide stable callback references for React.memo effectiveness
…rm dialog

Gates setSelectedCommand inside makeHandleSaveScript so it only updates
when the saved tab is still active. Without this, switching tabs while
the variable-removal confirm dialog is open would cause the UI to snap
back to the saved tab after the dialog confirms.
…nt UI jumps on tab switch

When saving a command (new or existing), if the user switches tabs while
the async save is in flight, the UI was snapping back to the saved command
after the operation completed. This gates both setActiveTabId and
setSelectedCommand with activeTabIdRef.current === tabId so the UI only
updates when the user is still viewing the tab that was saved.
…uesChange

The resolvedValues useMemo in CommandDetail depends on the 'variables' prop,
which is recreated on every App.tsx render. This caused onResolvedValuesChange
to fire with a new object reference each render, triggering App re-render and
creating an infinite loop (Maximum update depth exceeded).

Added a shallow-comparison guard using a ref to only call onResolvedValuesChange
when the actual key/value pairs change, breaking the loop while preserving
correct state synchronization.
@loco1842
Copy link
Copy Markdown
Owner Author

@CodeRabbit review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 24, 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.

@loco1842
Copy link
Copy Markdown
Owner Author

@coderabbitai help

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 24, 2026

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

CodeRabbit commands

These commands are invoked using PR/Issue comments.

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai evaluate custom pre-merge check --instructions <custom-checks-instructions> --name <custom-checks-title> [--mode <error|warning>] to validate the custom pre-merge checks instructions. Defaults to error when --mode is omitted.
  • @coderabbitai ignore pre-merge checks to override pre-merge checks and get an approval on PR.
  • @coderabbitai run pre-merge checks to run pre-merge checks on the pull request.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai resolve merge conflict to automatically resolve merge conflicts.
  • @coderabbitai autofix to automatically fix issues identified in unresolved review comments.
  • @coderabbitai resolve to resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai generate configuration to create a PR that adds the current resolved configuration as .coderabbit.yaml (or show it if already present).
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore or @coderabbit ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit configuration file (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • You can also validate your configuration using the online YAML validator.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, support, documentation and community

  • Visit our status page to check the current availability of CodeRabbit.
  • Create a ticket on our support page for assistance with any issues or questions.
  • Visit our documentation site for detailed information on how to use CodeRabbit.
  • Join our Discord community to connect with other users and get help from the community.
  • Follow us on X/Twitter for updates and announcements.

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: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@frontend/src/App.tsx`:
- Around line 1368-1373: The post-commit useEffect invalidates handlerGenRef and
tabHandlerCacheRef too late, so renders that triggered factory-dep changes
(e.g., `t`) still forward stale handlers; fix by making invalidation synchronous
during render: create a memoized generation key (useMemo) based on the factory
deps (`makeHandleExecute`, `makeHandleRunInTerminal`, `makeHandleFillVariables`,
`makeHandleDelete`, `makeHandleRenamePreset`, `makeHandleDeletePreset`,
`makeHandleAddPreset`, `makeHandleSavePresetValues`, `makeHandleReorderPresets`,
`makeHandleSaveScript`) and compare it to a `lastHandlerGenRef` to clear
`tabHandlerCacheRef.current` and update `lastHandlerGenRef.current` when they
differ (or alternately refactor each tab into a child component that computes
handlers with useMemo keyed by `tabId` + factories); this ensures the cache is
invalidated synchronously so the loop that looks up handlers (and components
using `React.memo`) never sees stale closures.
- Around line 1606-1608: The executingTabId is incorrectly derived from
activeTabId causing isExecuting to follow tab switches; change executingTabId to
be driven by a state variable (e.g., executingTabIdState via useState) instead
of deriving it from activeTabId, and update the places that currently set/clear
executingTabIdRef (where execution starts/stops) to set/clear this new state as
well (you can keep executingTabIdRef for imperative uses but ensure the JSX and
CommandDetail props read the state value); ensure isExecuting ?
executingTabIdState : null is used in the render so the executing tab remains
pinned during execution and React.memo optimizations are preserved.
🪄 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: 5a274c6d-1d56-4139-a8bb-d66a81c4941e

📥 Commits

Reviewing files that changed from the base of the PR and between df793e2 and aeab031.

📒 Files selected for processing (2)
  • frontend/src/App.tsx
  • frontend/src/components/CommandDetail.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • frontend/src/components/CommandDetail.tsx

Comment thread frontend/src/App.tsx Outdated
Comment thread frontend/src/App.tsx Outdated
…ingTabId state

- WR-01: Replace useEffect-based handler cache invalidation with
  synchronous render-time comparison of factory refs via prevFactoriesRef,
  ensuring stale closures are never returned between factory-dep changes
  and effect firing
- WR-03: Add executingTabIdState (useState) alongside executingTabIdRef
  so the executing tab remains pinned during execution even when user
  switches tabs, instead of incorrectly following activeTabId
@loco1842
Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 24, 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.

@loco1842
Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 24, 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.

Actionable comments posted: 1

🧹 Nitpick comments (1)
frontend/src/App.tsx (1)

1371-1408: Consider reducing boilerplate in factory-generation bookkeeping.

The hand-rolled 10-field object + 10-condition identity check is verbose and easy to get out of sync when a new makeHandle* factory is added. Using an array simplifies the logic while maintaining the same effect:

♻️ Terser encoding
-    const prevFactoriesRef = useRef<Record<string, ((...args: unknown[]) => unknown) | null>>({
-        makeHandleExecute: null,
-        makeHandleRunInTerminal: null,
-        makeHandleFillVariables: null,
-        makeHandleDelete: null,
-        makeHandleRenamePreset: null,
-        makeHandleDeletePreset: null,
-        makeHandleAddPreset: null,
-        makeHandleSavePresetValues: null,
-        makeHandleReorderPresets: null,
-        makeHandleSaveScript: null,
-    });
-
-    const pf = prevFactoriesRef.current;
-    if (
-        pf.makeHandleExecute !== makeHandleExecute ||
-        pf.makeHandleRunInTerminal !== makeHandleRunInTerminal ||
-        /* ...8 more... */
-        pf.makeHandleSaveScript !== makeHandleSaveScript
-    ) {
-        tabHandlerCacheRef.current.clear();
-        pf.makeHandleExecute = makeHandleExecute;
-        /* ...9 more assignments... */
-    }
+    const factories = [
+        makeHandleExecute, makeHandleRunInTerminal, makeHandleFillVariables,
+        makeHandleDelete, makeHandleRenamePreset, makeHandleDeletePreset,
+        makeHandleAddPreset, makeHandleSavePresetValues, makeHandleReorderPresets,
+        makeHandleSaveScript,
+    ] as const;
+    const prevFactoriesRef = useRef<readonly unknown[]>([]);
+    if (
+        prevFactoriesRef.current.length !== factories.length ||
+        factories.some((f, i) => f !== prevFactoriesRef.current[i])
+    ) {
+        tabHandlerCacheRef.current.clear();
+        prevFactoriesRef.current = factories;
+    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/App.tsx` around lines 1371 - 1408, Replace the hand-rolled
10-field object and long equality check with a compact array-driven approach:
turn prevFactoriesRef.current (and local pf) into a map updated by iterating a
string array of factory names (e.g.
["makeHandleExecute","makeHandleRunInTerminal",..., "makeHandleSaveScript"]),
check if any current function identity differs via Array.prototype.some
comparing pf[name] vs the corresponding variable (e.g. using (name: string) =>
pf[name] !== (eval? or better map name->variable)), and if any changed call
tabHandlerCacheRef.current.clear() then update pf[name] = the new factory for
each name in the same loop; keep the same symbols (prevFactoriesRef, pf,
tabHandlerCacheRef, and the makeHandle* identifiers) so behavior is identical
but boilerplate is removed and adding new makeHandle* entries requires only
adding the name to the array.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@frontend/src/App.tsx`:
- Around line 1022-1034: makeHandleDelete currently closes over a stale closeTab
because its deps are only [loadData, t]; update the factory to avoid the stale
closure by either (a) adding closeTab (and any functions it closes over like
finalizeCloseTab) to the useCallback dependency array for makeHandleDelete, or
(b) ref-forwarding closeTab into makeHandleDelete (change its signature to
makeHandleDelete(closeTab) or makeHandleDelete(tabId, closeTab)) so the current
closeTab is used when the handler is created; also ensure DeleteCommand errors
surface to the user by invoking toast.error(...) inside the catch in
makeHandleDelete (in addition to console.error) to match the app’s Wails async
call guidelines.

---

Nitpick comments:
In `@frontend/src/App.tsx`:
- Around line 1371-1408: Replace the hand-rolled 10-field object and long
equality check with a compact array-driven approach: turn
prevFactoriesRef.current (and local pf) into a map updated by iterating a string
array of factory names (e.g. ["makeHandleExecute","makeHandleRunInTerminal",...,
"makeHandleSaveScript"]), check if any current function identity differs via
Array.prototype.some comparing pf[name] vs the corresponding variable (e.g.
using (name: string) => pf[name] !== (eval? or better map name->variable)), and
if any changed call tabHandlerCacheRef.current.clear() then update pf[name] =
the new factory for each name in the same loop; keep the same symbols
(prevFactoriesRef, pf, tabHandlerCacheRef, and the makeHandle* identifiers) so
behavior is identical but boilerplate is removed and adding new makeHandle*
entries requires only adding the name to the array.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: b0b74989-e94f-41a6-a117-89f057f1fe3c

📥 Commits

Reviewing files that changed from the base of the PR and between aeab031 and 57f731c.

📒 Files selected for processing (1)
  • frontend/src/App.tsx

Comment thread frontend/src/App.tsx Outdated
@loco1842
Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 24, 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.

Actionable comments posted: 1

🧹 Nitpick comments (2)
frontend/src/App.tsx (2)

1384-1407: Type erasure on factories record weakens safety for no runtime benefit.

The Record<(typeof factoryNames)[number], (...args: unknown[]) => unknown> annotation forces every factory to be treated as (...args: unknown[]) => unknown. The only operation actually performed here is reference equality on function identities — no arguments are applied — so an opaque object/unknown keyed map would retain identity comparison while skipping the misleading callable signature.

♻️ Proposed tightening
-    const factories: Record<(typeof factoryNames)[number], (...args: unknown[]) => unknown> = {
+    const factories: Record<(typeof factoryNames)[number], unknown> = {
         makeHandleExecute,
         makeHandleRunInTerminal,
         makeHandleFillVariables,
         makeHandleDelete,
         makeHandleRenamePreset,
         makeHandleDeletePreset,
         makeHandleAddPreset,
         makeHandleSavePresetValues,
         makeHandleReorderPresets,
         makeHandleSaveScript,
     };

-    const prevFactoriesRef = useRef<Record<string, ((...args: unknown[]) => unknown) | null>>(
+    const prevFactoriesRef = useRef<Record<string, unknown>>(
         Object.fromEntries(factoryNames.map((name) => [name, null]))
     );

Only reference equality is used downstream, so unknown is sufficient and avoids implying these can be called with unknown[].

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

In `@frontend/src/App.tsx` around lines 1384 - 1407, The factories Record is
overly specific about callable signatures; change its type from Record<(typeof
factoryNames)[number], (...args: unknown[]) => unknown> to a non-callable
identity-preserving type (e.g., Record<(typeof factoryNames)[number], unknown>)
and update prevFactoriesRef to match (e.g., Record<string, unknown | null>) so
the identity comparison pf[name] !== factories[name] remains valid; touch the
symbols factoryNames, factories, prevFactoriesRef, and tabHandlerCacheRef to
keep typings consistent without implying these entries are invocable functions.

1709-1747: New-command tab placeholder recreated every App render — defeats React.memo on that tab.

For new-command tabs, command = makePlaceholderCommand(tab.id, draft?.categoryId) produces a brand‑new object on every App render, so the command prop handed to the memoized CommandDetail changes identity every time (even when tab.id and draft.categoryId are unchanged). This undoes the memoization win the surrounding handler cache and tabVariablesMap are designed to preserve — any unrelated App state change (e.g., executingTabIdState toggling on a different tab, selectedRecord updates, pane-open toggles) will cause the new-command tab's CommandDetail to re-render.

Existing (saved) tabs are fine because allCommandsRef.current.find(...) returns the same object reference until loadData() actually produces a new array.

♻️ Proposed refactor — memoize placeholder commands per tab
     const tabVariablesMap = useMemo(() => {
         const map: Record<string, VarPromptType[]> = {};
         for (const tab of openTabs) {
             if (tab.id === '__welcome__') continue;
             const draft = tabDrafts[tab.id];
             if (draft && !isNewCommandTabId(tab.id)) {
                 map[tab.id] = variableDefinitionsToPrompts(draft.variables);
             }
         }
         return map;
     }, [tabDrafts, openTabs]);
+
+    // Stable placeholder commands for new-command tabs so React.memo isn't bypassed
+    // on every App render.
+    const tabPlaceholderMap = useMemo(() => {
+        const map: Record<string, Command> = {};
+        for (const tab of openTabs) {
+            if (!isNewCommandTabId(tab.id)) continue;
+            const draft = tabDrafts[tab.id];
+            map[tab.id] = makePlaceholderCommand(tab.id, draft?.categoryId);
+        }
+        return map;
+    }, [openTabs, tabDrafts]);
-                                        const command = isTabNew
-                                            ? makePlaceholderCommand(tab.id, draft?.categoryId)
-                                            : allCommandsRef.current.find((c) => c.id === tab.id) ?? null;
+                                        const command = isTabNew
+                                            ? tabPlaceholderMap[tab.id] ?? null
+                                            : allCommandsRef.current.find((c) => c.id === tab.id) ?? null;

If makePlaceholderCommand only actually depends on categoryId, you can further narrow the memo dep to draft?.categoryId per tab.

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

In `@frontend/src/App.tsx` around lines 1709 - 1747, The placeholder command
object created in the render (`command = makePlaceholderCommand(tab.id,
draft?.categoryId)` for isTabNew tabs) is re-created every render breaking
React.memo; fix by memoizing the placeholder per tab id (e.g., store in a ref
map like placeholderCommandRef.current keyed by tab.id) and only recreate it
when the inputs change (draft?.categoryId or tab.id), then use that cached
object as `command`; keep existing logic for saved tabs
(allCommandsRef.current.find(...)) and ensure cache is updated/invalidated when
categoryId changes or the tab is closed.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@frontend/src/App.tsx`:
- Around line 1338-1356: The catch block in makeHandleSaveScript currently only
logs errors to console and should show a user-facing toast like the other
handlers; update the catch to call toast.error(...) with a localized message
(e.g., t('toast.commandSaveFailed') if available, or String(err)) and keep
console.error for debugging, ensuring failures from UpdateCommand/GetScriptBody
surface to the user; locate makeHandleSaveScript and modify its try/catch to
mirror the other factories' error handling (use toast.error and the err value).

---

Nitpick comments:
In `@frontend/src/App.tsx`:
- Around line 1384-1407: The factories Record is overly specific about callable
signatures; change its type from Record<(typeof factoryNames)[number], (...args:
unknown[]) => unknown> to a non-callable identity-preserving type (e.g.,
Record<(typeof factoryNames)[number], unknown>) and update prevFactoriesRef to
match (e.g., Record<string, unknown | null>) so the identity comparison pf[name]
!== factories[name] remains valid; touch the symbols factoryNames, factories,
prevFactoriesRef, and tabHandlerCacheRef to keep typings consistent without
implying these entries are invocable functions.
- Around line 1709-1747: The placeholder command object created in the render
(`command = makePlaceholderCommand(tab.id, draft?.categoryId)` for isTabNew
tabs) is re-created every render breaking React.memo; fix by memoizing the
placeholder per tab id (e.g., store in a ref map like
placeholderCommandRef.current keyed by tab.id) and only recreate it when the
inputs change (draft?.categoryId or tab.id), then use that cached object as
`command`; keep existing logic for saved tabs (allCommandsRef.current.find(...))
and ensure cache is updated/invalidated when categoryId changes or the tab is
closed.
🪄 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: d1c8f1c7-2e29-4167-949b-4996dfef9237

📥 Commits

Reviewing files that changed from the base of the PR and between 57f731c and 07c7032.

📒 Files selected for processing (1)
  • frontend/src/App.tsx

Comment thread frontend/src/App.tsx
Comment on lines +1338 to 1356
try {
await UpdateCommand(tabId, d.title.trim(), d.description.trim(), strippedBody, d.categoryId, d.tags.map(tag => tag.trim()).filter(Boolean), vars, d.workingDir);
await loadData();
const cmd = allCommandsRef.current.find(c => c.id === tabId);
if (cmd) {
const body = await GetScriptBody(cmd.id);
const saved = draftFromCommand(cmd, body);
setTabDrafts(prev => ({ ...prev, [tabId]: saved }));
setTabBaselines(prev => ({ ...prev, [tabId]: cloneDraft(saved) }));
// Only update selectedCommand if this tab is still active,
// so tab switches during confirm dialogs don't clobber the UI.
if (activeTabIdRef.current === tabId) {
setSelectedCommand(cmd);
}
}
toast.success(t('toast.commandSaved'));
} catch (err) {
console.error('Failed to save script:', err);
}
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

Missing toast.error in makeHandleSaveScript catch — inconsistent with peer preset factories.

All other newly added factories (makeHandleAddPreset, makeHandleRenamePreset, makeHandleDeletePreset, makeHandleReorderPresets, makeHandleSavePresetValues, makeHandleDelete, makeHandleRunInTerminal) surface failures via toast.error. makeHandleSaveScript only logs to console.error, so a backend rejection of UpdateCommand/GetScriptBody during the preview-panel "save script" path is silent to the user — no pending changes are cleared, but no feedback is given either.

As per coding guidelines: "Async Wails calls in React must use .then/.catch or try/catch patterns with user feedback via toast from sonner."

🛡 Proposed fix
             } catch (err) {
                 console.error('Failed to save script:', err);
+                toast.error(t('toast.commandSaveFailed') ?? String(err));
             }

(Uses an existing toast.commandSaveFailed key if available; otherwise String(err) — align with whatever key the i18n bundle exposes for save failures.)

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

In `@frontend/src/App.tsx` around lines 1338 - 1356, The catch block in
makeHandleSaveScript currently only logs errors to console and should show a
user-facing toast like the other handlers; update the catch to call
toast.error(...) with a localized message (e.g., t('toast.commandSaveFailed') if
available, or String(err)) and keep console.error for debugging, ensuring
failures from UpdateCommand/GetScriptBody surface to the user; locate
makeHandleSaveScript and modify its try/catch to mirror the other factories'
error handling (use toast.error and the err value).

@loco1842 loco1842 merged commit 39e26e3 into main Apr 24, 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