Update version to 0.39.0, add OverviewView component, and adjust navi…#26
Conversation
…gation paths in Sidebar and ViewTabs components to reflect new project structure.
📝 WalkthroughWalkthroughAdds a new per-project Overview dashboard route and UI: a draggable/resizable grid of mini-tiles (console, tunnel, logs, files, CPU, RAM, PIDs), persisted per-project layout, routing/nav updates, and a runtime dependency on Changes
Sequence DiagramsequenceDiagram
participant User
participant Router
participant OverviewView
participant GridLayout
participant localStorage
participant Atoms as StateAtoms
participant Tiles as TileComponents
participant API as ExternalAPIs
User->>Router: navigate /project/:id/overview
Router->>OverviewView: mount
OverviewView->>localStorage: load saved layout (key per project)
localStorage-->>OverviewView: layout or default
OverviewView->>GridLayout: init with layout
OverviewView->>Atoms: subscribe (stats, logs, tunnel, history)
Atoms-->>OverviewView: push state updates
OverviewView->>Tiles: render tiles with props
Tiles->>API: init (xterm, tunnel start/stop, openExternal)
User->>Tiles: interact (send command, start tunnel, open file)
Tiles->>API: call window.api / clipboard
API-->>Tiles: responses/logs
Tiles->>Atoms: update state (logs, status)
GridLayout->>localStorage: debounced save on layout change
localStorage-->>GridLayout: persisted
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
📝 Coding Plan
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (2)
src/components/OverviewView.jsx (1)
835-849: Consider simplifying the nested ternary with an object lookup.The deeply nested ternary for tile titles is functional but hard to read and maintain.
♻️ Suggested simplification
+const TILE_TITLES = { + console: "Console", + tunnel: "Tunnel", + tunnelLogs: "Tunnel Logs", + files: "Files", + cpu: "CPU", + ram: "RAM", + pids: "PIDs", +}; + {tiles.map((t) => ( <div key={t.i} className="h-full w-full"> <TileShell - title={ - t.i === "console" - ? "Console" - : t.i === "tunnel" - ? "Tunnel" - : t.i === "tunnelLogs" - ? "Tunnel Logs" - : t.i === "files" - ? "Files" - : t.i === "cpu" - ? "CPU" - : t.i === "ram" - ? "RAM" - : "PIDs" - } + title={TILE_TITLES[t.i] ?? t.i} right={t.headerRight ?? null} >🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/OverviewView.jsx` around lines 835 - 849, Replace the nested ternary used for the title prop in OverviewView.jsx with a simple lookup: define a mapping object (e.g., titleMap = { console: "Console", tunnel: "Tunnel", tunnelLogs: "Tunnel Logs", files: "Files", cpu: "CPU", ram: "RAM" }) near the render/functional component and then set title to titleMap[t.i] || "PIDs"; update the title prop usage that currently references t.i to use this lookup and remove the nested ternary to improve readability and maintainability.package.json (1)
118-118: Consider using the originalreact-grid-layoutpackage instead of this fork.While
@eleung/react-grid-layoutis a legitimate npm package, the originalreact-grid-layout(v2.2.2) is significantly more actively maintained and widely adopted (22k GitHub stars, 1.6M weekly downloads vs. ~310), with full TypeScript support and React 18+ compatibility. The fork is limited to React 16 & 17 and receives infrequent updates. Unless there is a specific reason this fork is required, switching to the original package would reduce maintenance risk and improve long-term compatibility.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@package.json` at line 118, Replace the forked dependency "@eleung/react-grid-layout" in package.json with the upstream "react-grid-layout" (suggested version v2.2.2), update your lockfile (npm install / yarn install), and run the app/tests to catch any API differences; then verify and, if necessary, update imports/usages (e.g., any references to "@eleung/react-grid-layout" to "react-grid-layout" and components like GridLayout/Responsive) so TypeScript picks up the upstream types and React 18+ compatibility.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/components/OverviewView.jsx`:
- Around line 89-105: The debounce timer set in onLayoutChange using
saveTimerRef.current may fire after the component unmounts; add a cleanup to
clear the pending timeout when the component unmounts by adding a useEffect with
a cleanup that checks saveTimerRef.current and calls
clearTimeout(saveTimerRef.current) (and optionally sets saveTimerRef.current =
null). Update the component that defines onLayoutChange/ layout/ saveTimerRef
(the OverviewView component) to ensure the timeout is cleared on unmount to
prevent the delayed localStorage write from running after unmount.
- Around line 108-109: The current logs assignment (const allLogs =
useAtomValue(logsAtom); const logs = allLogs?.[projectId] || []) creates a new
empty array each render when allLogs?.[projectId] is undefined, causing the
downstream useEffect to rerun; change this to memoize a stable fallback by
deriving logs with useMemo (depend on allLogs and projectId) so logs only
changes when the actual data changes (refer to logsAtom, allLogs, logs, and the
useEffect in OverviewView.jsx).
---
Nitpick comments:
In `@package.json`:
- Line 118: Replace the forked dependency "@eleung/react-grid-layout" in
package.json with the upstream "react-grid-layout" (suggested version v2.2.2),
update your lockfile (npm install / yarn install), and run the app/tests to
catch any API differences; then verify and, if necessary, update imports/usages
(e.g., any references to "@eleung/react-grid-layout" to "react-grid-layout" and
components like GridLayout/Responsive) so TypeScript picks up the upstream types
and React 18+ compatibility.
In `@src/components/OverviewView.jsx`:
- Around line 835-849: Replace the nested ternary used for the title prop in
OverviewView.jsx with a simple lookup: define a mapping object (e.g., titleMap =
{ console: "Console", tunnel: "Tunnel", tunnelLogs: "Tunnel Logs", files:
"Files", cpu: "CPU", ram: "RAM" }) near the render/functional component and then
set title to titleMap[t.i] || "PIDs"; update the title prop usage that currently
references t.i to use this lookup and remove the nested ternary to improve
readability and maintainability.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 627c2587-aaff-4c64-924a-410bd1fec58b
📒 Files selected for processing (5)
package.jsonsrc/App.jsxsrc/components/OverviewView.jsxsrc/components/Sidebar.jsxsrc/components/ViewTabs.jsx
| const onLayoutChange = (nextLayout) => { | ||
| setLayout(nextLayout); | ||
|
|
||
| if (saveTimerRef.current) clearTimeout(saveTimerRef.current); | ||
| saveTimerRef.current = setTimeout(() => { | ||
| if (projectId == null) return; | ||
| const key = `selfhost-overview-grid:${projectId}`; | ||
| try { | ||
| localStorage.setItem(key, JSON.stringify(nextLayout)); | ||
| } catch { | ||
| // Ignore storage errors (private mode / quota). | ||
| } | ||
| }, 250); | ||
| }; | ||
|
|
||
| return { layout, onLayoutChange }; | ||
| } |
There was a problem hiding this comment.
Missing cleanup for debounce timer on unmount.
If the component unmounts before the 250ms debounce completes, saveTimerRef.current callback may still execute. Clear the timer in a cleanup function.
🛠️ Proposed fix
+ useEffect(() => {
+ return () => {
+ if (saveTimerRef.current) clearTimeout(saveTimerRef.current);
+ };
+ }, []);
+
const onLayoutChange = (nextLayout) => {
setLayout(nextLayout);📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const onLayoutChange = (nextLayout) => { | |
| setLayout(nextLayout); | |
| if (saveTimerRef.current) clearTimeout(saveTimerRef.current); | |
| saveTimerRef.current = setTimeout(() => { | |
| if (projectId == null) return; | |
| const key = `selfhost-overview-grid:${projectId}`; | |
| try { | |
| localStorage.setItem(key, JSON.stringify(nextLayout)); | |
| } catch { | |
| // Ignore storage errors (private mode / quota). | |
| } | |
| }, 250); | |
| }; | |
| return { layout, onLayoutChange }; | |
| } | |
| useEffect(() => { | |
| return () => { | |
| if (saveTimerRef.current) clearTimeout(saveTimerRef.current); | |
| }; | |
| }, []); | |
| const onLayoutChange = (nextLayout) => { | |
| setLayout(nextLayout); | |
| if (saveTimerRef.current) clearTimeout(saveTimerRef.current); | |
| saveTimerRef.current = setTimeout(() => { | |
| if (projectId == null) return; | |
| const key = `selfhost-overview-grid:${projectId}`; | |
| try { | |
| localStorage.setItem(key, JSON.stringify(nextLayout)); | |
| } catch { | |
| // Ignore storage errors (private mode / quota). | |
| } | |
| }, 250); | |
| }; | |
| return { layout, onLayoutChange }; | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/components/OverviewView.jsx` around lines 89 - 105, The debounce timer
set in onLayoutChange using saveTimerRef.current may fire after the component
unmounts; add a cleanup to clear the pending timeout when the component unmounts
by adding a useEffect with a cleanup that checks saveTimerRef.current and calls
clearTimeout(saveTimerRef.current) (and optionally sets saveTimerRef.current =
null). Update the component that defines onLayoutChange/ layout/ saveTimerRef
(the OverviewView component) to ensure the timeout is cleared on unmount to
prevent the delayed localStorage write from running after unmount.
| const allLogs = useAtomValue(logsAtom); | ||
| const logs = allLogs?.[projectId] || []; |
There was a problem hiding this comment.
Unstable logs reference may cause unnecessary effect re-runs.
The || [] fallback creates a new array instance on every render when allLogs?.[projectId] is undefined, causing the useEffect at line 184 to re-execute unnecessarily.
🛠️ Proposed fix using useMemo
+const EMPTY_LOGS = [];
+
function ConsoleMiniTile({ projectId, status, onSendInput }) {
const allLogs = useAtomValue(logsAtom);
- const logs = allLogs?.[projectId] || [];
+ const logs = useMemo(() => allLogs?.[projectId] ?? EMPTY_LOGS, [allLogs, projectId]);🧰 Tools
🪛 GitHub Check: build-and-verify
[warning] 109-109:
The 'logs' logical expression could make the dependencies of useEffect Hook (at line 202) change on every render. To fix this, wrap the initialization of 'logs' in its own useMemo() Hook
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/components/OverviewView.jsx` around lines 108 - 109, The current logs
assignment (const allLogs = useAtomValue(logsAtom); const logs =
allLogs?.[projectId] || []) creates a new empty array each render when
allLogs?.[projectId] is undefined, causing the downstream useEffect to rerun;
change this to memoize a stable fallback by deriving logs with useMemo (depend
on allLogs and projectId) so logs only changes when the actual data changes
(refer to logsAtom, allLogs, logs, and the useEffect in OverviewView.jsx).
…agement and integrate reset layout functionality in ViewTabs. Introduce overviewLayoutResetSignalAtom for state management of the grid layout.
There was a problem hiding this comment.
Actionable comments posted: 6
🧹 Nitpick comments (1)
src/components/OverviewView.jsx (1)
22-702: Remove the stale inline overview implementation.
OverviewViewnow just delegates toOverviewGrid, andsrc/components/overview/OverviewGrid.jsxalready imports the standaloneTileShell,useProjectLayout, and tile components. Keeping a second copy here means fixes can land in the wrong implementation. Delete the in-file versions and keep/move only the CSS side-effect imports that the extracted components still need.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/OverviewView.jsx` around lines 22 - 702, This file contains a stale inline implementation of many overview pieces (MiniSparkline, TileShell, useProjectLayout, ConsoleMiniTile, TunnelMiniTile, TunnelLogsMiniTile, MiniFileExhibitorTile, ProcessIdsTile, CpuTile, RamTile) that now live in src/components/overview/OverviewGrid.jsx; delete these in-file component and hook definitions and any related helper duplicates, leaving only the CSS side-effect imports (if any) required by the extracted components and a simple OverviewView that imports/delegates to OverviewGrid; ensure you keep references to unique symbols (MiniSparkline, TileShell, useProjectLayout, ConsoleMiniTile, TunnelMiniTile, TunnelLogsMiniTile, MiniFileExhibitorTile, ProcessIdsTile, CpuTile, RamTile) removed here so future fixes land in the single canonical implementation.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/components/overview/OverviewGrid.jsx`:
- Around line 65-77: The ConsoleMiniTile instance is reused across projects
because the tile key/identifier is static ("console"), causing its local state
(input, history, historyIndex) to persist; update the tiles entry that renders
ConsoleMiniTile so its key depends on the current project (e.g., include
project.id in the tile key or use a composite key like `console-${project.id}`)
so React will unmount and remount ConsoleMiniTile when project changes, ensuring
its internal state is reset; locate the tiles array and the object with i:
"console" and change its key/identifier to incorporate project.id.
In `@src/components/overview/tiles/ConsoleMiniTile.jsx`:
- Around line 1-18: The logs variable in ConsoleMiniTile is recreating an empty
array each render (const logs = allLogs?.[projectId] || []) causing effects that
depend on logs to rerun; fix by defining a stable EMPTY_LOGS constant and use
useMemo or reuse that reference so logs becomes const logs =
allLogs?.[projectId] ?? EMPTY_LOGS (or memoize via useMemo(() =>
allLogs?.[projectId] ?? EMPTY_LOGS, [allLogs, projectId])) so the identity is
stable and the effect that depends on logs (the effect around line 93) only runs
when actual log data changes.
In `@src/components/overview/tiles/MiniFileExhibitorTile.jsx`:
- Around line 27-38: Replace the non-interactive clickable divs used for
directory toggles and file rows with proper interactive controls: change the row
container (the element with onClick that calls setExpanded((prev) => ({ ...prev,
[path]: !prev[path] })) and the child file row rendering in renderNode) to a
real button (type="button") or make them keyboard operable by adding tabIndex,
onKeyDown handling for Enter/Space, and appropriate ARIA (e.g., aria-expanded
for directory rows bound to isOpen). Ensure the label uses the existing
node.name, keep the visual styling, and preserve the children rendering logic
(children.map(renderNode, level + 1)) while updating those rows the same way so
they are focusable and usable by keyboard and assistive tech.
In `@src/components/overview/tiles/ProcessIdsTile.jsx`:
- Around line 24-30: The empty-state conditional uses stats?.activeProcesses
directly instead of the normalized value computed earlier
(activeProcesses/processCount), so replace the condition (pids.length === 0 &&
(stats?.activeProcesses ?? 0) > 0) with a check that uses the normalized process
count variable you already compute (activeProcesses or processCount) to
determine if active processes exist; update the JSX branch to reference that
normalized variable (activeProcesses) so the fallback path is honored when
stats.activeProcesses is missing but processCount/activeProcesses is present.
In `@src/components/overview/tiles/TunnelMiniTile.jsx`:
- Line 123: In TunnelMiniTile.jsx, update the status detail rendering that
currently uses the template fragment `: localhost:${portNum}` (inside the
TunnelMiniTile component where portNum is used) to remove the extra leading
colon so it reads `localhost:${portNum}`; locate the JSX expression that builds
the status/detail string and remove the stray ":" before "localhost" to fix the
formatting.
In `@src/components/overview/useProjectLayout.js`:
- Around line 39-49: Add an unmount cleanup that clears the pending debounce
timer so the queued save cannot run after the component unmounts: ensure the
effect or hook that sets saveTimerRef.current (the function containing the
setTimeout and localStorage.setItem) returns a cleanup which checks
saveTimerRef.current and calls clearTimeout(saveTimerRef.current) and resets it
(e.g., saveTimerRef.current = null). Reference the existing saveTimerRef.current
and the debounce block that calls setTimeout to locate where to add the cleanup.
---
Nitpick comments:
In `@src/components/OverviewView.jsx`:
- Around line 22-702: This file contains a stale inline implementation of many
overview pieces (MiniSparkline, TileShell, useProjectLayout, ConsoleMiniTile,
TunnelMiniTile, TunnelLogsMiniTile, MiniFileExhibitorTile, ProcessIdsTile,
CpuTile, RamTile) that now live in src/components/overview/OverviewGrid.jsx;
delete these in-file component and hook definitions and any related helper
duplicates, leaving only the CSS side-effect imports (if any) required by the
extracted components and a simple OverviewView that imports/delegates to
OverviewGrid; ensure you keep references to unique symbols (MiniSparkline,
TileShell, useProjectLayout, ConsoleMiniTile, TunnelMiniTile,
TunnelLogsMiniTile, MiniFileExhibitorTile, ProcessIdsTile, CpuTile, RamTile)
removed here so future fixes land in the single canonical implementation.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: eaed5e90-4c6d-4f33-8d6b-5044402cab16
📒 Files selected for processing (16)
src/components/OverviewView.jsxsrc/components/ViewTabs.jsxsrc/components/overview/OverviewGrid.jsxsrc/components/overview/TileShell.jsxsrc/components/overview/charts/CompactStatCard.jsxsrc/components/overview/charts/MiniSparkline.jsxsrc/components/overview/charts/PremiumSparklineChart.jsxsrc/components/overview/tiles/ConsoleMiniTile.jsxsrc/components/overview/tiles/CpuTile.jsxsrc/components/overview/tiles/MiniFileExhibitorTile.jsxsrc/components/overview/tiles/ProcessIdsTile.jsxsrc/components/overview/tiles/RamTile.jsxsrc/components/overview/tiles/TunnelLogsMiniTile.jsxsrc/components/overview/tiles/TunnelMiniTile.jsxsrc/components/overview/useProjectLayout.jssrc/store/atoms.js
✅ Files skipped from review due to trivial changes (3)
- src/store/atoms.js
- src/components/overview/TileShell.jsx
- src/components/overview/tiles/TunnelLogsMiniTile.jsx
🚧 Files skipped from review as they are similar to previous changes (1)
- src/components/ViewTabs.jsx
| const tiles = [ | ||
| { | ||
| i: "console", | ||
| headerRight: ( | ||
| <span | ||
| className={`w-1.5 h-1.5 rounded-full shrink-0 ${ | ||
| project.status === "running" ? "bg-green-500" : "bg-destructive" | ||
| }`} | ||
| /> | ||
| ), | ||
| render: ( | ||
| <ConsoleMiniTile projectId={project.id} status={project.status} onSendInput={onSendInput} /> | ||
| ), |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# First, let's look at the OverviewGrid.jsx file around the specified lines
cat -n src/components/overview/OverviewGrid.jsx | sed -n '60,80p'Repository: DevRoots-Studio/SelfHost-Helper
Length of output: 702
🏁 Script executed:
# Let's also see the grid rendering section (lines 140-163 mentioned in the comment)
cat -n src/components/overview/OverviewGrid.jsx | sed -n '135,165p'Repository: DevRoots-Studio/SelfHost-Helper
Length of output: 1337
🏁 Script executed:
# Now let's examine the ConsoleMiniTile component to understand its state
cat -n src/components/overview/tiles/ConsoleMiniTile.jsxRepository: DevRoots-Studio/SelfHost-Helper
Length of output: 7783
Remount the console tile per project to prevent command history leak.
ConsoleMiniTile stores local command state (input, history, historyIndex), but because the tile is keyed only by its type ("console" at line 141), the same component instance persists when projectId changes. The terminal itself is properly disposed and recreated via the projectId dependency (line 91), but the state variables are not reset. This allows command history from one project to be recalled and sent to another via ArrowUp/ArrowDown navigation.
Key the console tile by project.id to force unmount and remount when switching projects:
Suggested fix
render: (
- <ConsoleMiniTile projectId={project.id} status={project.status} onSendInput={onSendInput} />
+ <ConsoleMiniTile
+ key={`console:${project.id}`}
+ projectId={project.id}
+ status={project.status}
+ onSendInput={onSendInput}
+ />
),📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const tiles = [ | |
| { | |
| i: "console", | |
| headerRight: ( | |
| <span | |
| className={`w-1.5 h-1.5 rounded-full shrink-0 ${ | |
| project.status === "running" ? "bg-green-500" : "bg-destructive" | |
| }`} | |
| /> | |
| ), | |
| render: ( | |
| <ConsoleMiniTile projectId={project.id} status={project.status} onSendInput={onSendInput} /> | |
| ), | |
| const tiles = [ | |
| { | |
| i: "console", | |
| headerRight: ( | |
| <span | |
| className={`w-1.5 h-1.5 rounded-full shrink-0 ${ | |
| project.status === "running" ? "bg-green-500" : "bg-destructive" | |
| }`} | |
| /> | |
| ), | |
| render: ( | |
| <ConsoleMiniTile | |
| key={`console:${project.id}`} | |
| projectId={project.id} | |
| status={project.status} | |
| onSendInput={onSendInput} | |
| /> | |
| ), |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/components/overview/OverviewGrid.jsx` around lines 65 - 77, The
ConsoleMiniTile instance is reused across projects because the tile
key/identifier is static ("console"), causing its local state (input, history,
historyIndex) to persist; update the tiles entry that renders ConsoleMiniTile so
its key depends on the current project (e.g., include project.id in the tile key
or use a composite key like `console-${project.id}`) so React will unmount and
remount ConsoleMiniTile when project changes, ensuring its internal state is
reset; locate the tiles array and the object with i: "console" and change its
key/identifier to incorporate project.id.
| import React, { useEffect, useRef, useState } from "react"; | ||
| import { useAtomValue } from "jotai"; | ||
| import { Terminal as TerminalIcon, Send } from "lucide-react"; | ||
| import { toast } from "react-toastify"; | ||
| import { Terminal } from "@xterm/xterm"; | ||
| import { FitAddon } from "@xterm/addon-fit"; | ||
| import { ClipboardAddon } from "@xterm/addon-clipboard"; | ||
| import { WebLinksAddon } from "@xterm/addon-web-links"; | ||
|
|
||
| import { Button } from "@/components/ui/button"; | ||
| import { logsAtom } from "@/store/atoms"; | ||
|
|
||
| const API = window.api; | ||
|
|
||
| export default function ConsoleMiniTile({ projectId, status, onSendInput }) { | ||
| const allLogs = useAtomValue(logsAtom); | ||
| const logs = allLogs?.[projectId] || []; | ||
|
|
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# First, check if the file exists and get its size
ls -la src/components/overview/tiles/ConsoleMiniTile.jsx && wc -l src/components/overview/tiles/ConsoleMiniTile.jsxRepository: DevRoots-Studio/SelfHost-Helper
Length of output: 227
🏁 Script executed:
# Read the entire file to understand the context
cat -n src/components/overview/tiles/ConsoleMiniTile.jsxRepository: DevRoots-Studio/SelfHost-Helper
Length of output: 7783
🏁 Script executed:
# Look for useEffect hooks and their dependencies
rg "useEffect" src/components/overview/tiles/ConsoleMiniTile.jsx -A 10Repository: DevRoots-Studio/SelfHost-Helper
Length of output: 1106
Memoize the empty log fallback to prevent unnecessary effect reruns.
Line 17 creates a new [] on every render when allLogs?.[projectId] is missing. Since the effect at line 93 depends on logs, this causes unnecessary reruns even when no data has changed. Use useMemo with a stable EMPTY_LOGS reference to preserve identity.
🛠️ Minimal change
-import React, { useEffect, useRef, useState } from "react";
+import React, { useEffect, useMemo, useRef, useState } from "react";
import { useAtomValue } from "jotai";
import { Terminal as TerminalIcon, Send } from "lucide-react";
import { toast } from "react-toastify";
import { Terminal } from "@xterm/xterm";
import { FitAddon } from "@xterm/addon-fit";
import { ClipboardAddon } from "@xterm/addon-clipboard";
import { WebLinksAddon } from "@xterm/addon-web-links";
@@
const API = window.api;
+const EMPTY_LOGS = [];
export default function ConsoleMiniTile({ projectId, status, onSendInput }) {
const allLogs = useAtomValue(logsAtom);
- const logs = allLogs?.[projectId] || [];
+ const logs = useMemo(() => allLogs?.[projectId] ?? EMPTY_LOGS, [allLogs, projectId]);📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| import React, { useEffect, useRef, useState } from "react"; | |
| import { useAtomValue } from "jotai"; | |
| import { Terminal as TerminalIcon, Send } from "lucide-react"; | |
| import { toast } from "react-toastify"; | |
| import { Terminal } from "@xterm/xterm"; | |
| import { FitAddon } from "@xterm/addon-fit"; | |
| import { ClipboardAddon } from "@xterm/addon-clipboard"; | |
| import { WebLinksAddon } from "@xterm/addon-web-links"; | |
| import { Button } from "@/components/ui/button"; | |
| import { logsAtom } from "@/store/atoms"; | |
| const API = window.api; | |
| export default function ConsoleMiniTile({ projectId, status, onSendInput }) { | |
| const allLogs = useAtomValue(logsAtom); | |
| const logs = allLogs?.[projectId] || []; | |
| import React, { useEffect, useMemo, useRef, useState } from "react"; | |
| import { useAtomValue } from "jotai"; | |
| import { Terminal as TerminalIcon, Send } from "lucide-react"; | |
| import { toast } from "react-toastify"; | |
| import { Terminal } from "@xterm/xterm"; | |
| import { FitAddon } from "@xterm/addon-fit"; | |
| import { ClipboardAddon } from "@xterm/addon-clipboard"; | |
| import { WebLinksAddon } from "@xterm/addon-web-links"; | |
| import { Button } from "@/components/ui/button"; | |
| import { logsAtom } from "@/store/atoms"; | |
| const API = window.api; | |
| const EMPTY_LOGS = []; | |
| export default function ConsoleMiniTile({ projectId, status, onSendInput }) { | |
| const allLogs = useAtomValue(logsAtom); | |
| const logs = useMemo(() => allLogs?.[projectId] ?? EMPTY_LOGS, [allLogs, projectId]); |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/components/overview/tiles/ConsoleMiniTile.jsx` around lines 1 - 18, The
logs variable in ConsoleMiniTile is recreating an empty array each render (const
logs = allLogs?.[projectId] || []) causing effects that depend on logs to rerun;
fix by defining a stable EMPTY_LOGS constant and use useMemo or reuse that
reference so logs becomes const logs = allLogs?.[projectId] ?? EMPTY_LOGS (or
memoize via useMemo(() => allLogs?.[projectId] ?? EMPTY_LOGS, [allLogs,
projectId])) so the identity is stable and the effect that depends on logs (the
effect around line 93) only runs when actual log data changes.
| <div key={path} className="select-none"> | ||
| <div | ||
| className="flex items-center gap-2 px-2 py-1 hover:bg-white/5 cursor-pointer rounded-md" | ||
| style={{ paddingLeft: indent }} | ||
| onClick={() => setExpanded((prev) => ({ ...prev, [path]: !prev[path] }))} | ||
| > | ||
| <span className="text-muted-foreground/70">{isOpen ? "▾" : "▸"}</span> | ||
| <span className="text-xs text-foreground/85 truncate">{node.name}</span> | ||
| </div> | ||
| {isOpen && children.length > 0 && ( | ||
| <div className="pt-0.5">{children.map((child) => renderNode(child, level + 1))}</div> | ||
| )} |
There was a problem hiding this comment.
Use real controls for the file tree rows.
Both the directory toggles and file rows are clickable <div>s. They aren't focusable or keyboard-operable, so the preview can't be used without a mouse. Replace them with <button type="button"> elements, or add equivalent semantics and keyboard handling (aria-expanded for directories, Enter/Space activation, focusability).
Also applies to: 45-54
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/components/overview/tiles/MiniFileExhibitorTile.jsx` around lines 27 -
38, Replace the non-interactive clickable divs used for directory toggles and
file rows with proper interactive controls: change the row container (the
element with onClick that calls setExpanded((prev) => ({ ...prev, [path]:
!prev[path] })) and the child file row rendering in renderNode) to a real button
(type="button") or make them keyboard operable by adding tabIndex, onKeyDown
handling for Enter/Space, and appropriate ARIA (e.g., aria-expanded for
directory rows bound to isOpen). Ensure the label uses the existing node.name,
keep the visual styling, and preserve the children rendering logic
(children.map(renderNode, level + 1)) while updating those rows the same way so
they are focusable and usable by keyboard and assistive tech.
| if (pids.length === 0 && (stats?.activeProcesses ?? 0) > 0) { | ||
| return ( | ||
| <div className="h-full p-4 overflow-auto"> | ||
| <div className="text-xs text-muted-foreground/60"> | ||
| PID enumeration not available for this project (recovered Job Object). Active processes:{" "} | ||
| <span className="text-foreground font-mono">{activeProcesses}</span> | ||
| </div> |
There was a problem hiding this comment.
Use the normalized process count in the empty-state condition.
Line 24 bypasses the fallback you already computed at Line 10. If activeProcesses is absent but processCount is present, this can incorrectly fall through to “No processes detected.”
🔧 Proposed fix
- if (pids.length === 0 && (stats?.activeProcesses ?? 0) > 0) {
+ if (pids.length === 0 && activeProcesses > 0) {📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| if (pids.length === 0 && (stats?.activeProcesses ?? 0) > 0) { | |
| return ( | |
| <div className="h-full p-4 overflow-auto"> | |
| <div className="text-xs text-muted-foreground/60"> | |
| PID enumeration not available for this project (recovered Job Object). Active processes:{" "} | |
| <span className="text-foreground font-mono">{activeProcesses}</span> | |
| </div> | |
| if (pids.length === 0 && activeProcesses > 0) { | |
| return ( | |
| <div className="h-full p-4 overflow-auto"> | |
| <div className="text-xs text-muted-foreground/60"> | |
| PID enumeration not available for this project (recovered Job Object). Active processes:{" "} | |
| <span className="text-foreground font-mono">{activeProcesses}</span> | |
| </div> |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/components/overview/tiles/ProcessIdsTile.jsx` around lines 24 - 30, The
empty-state conditional uses stats?.activeProcesses directly instead of the
normalized value computed earlier (activeProcesses/processCount), so replace the
condition (pids.length === 0 && (stats?.activeProcesses ?? 0) > 0) with a check
that uses the normalized process count variable you already compute
(activeProcesses or processCount) to determine if active processes exist; update
the JSX branch to reference that normalized variable (activeProcesses) so the
fallback path is honored when stats.activeProcesses is missing but
processCount/activeProcesses is present.
| ? url | ||
| : error | ||
| ? String(error) | ||
| : `: localhost:${portNum}`} |
There was a problem hiding this comment.
Fix localhost fallback text formatting.
Line 123 renders an extra leading colon (: localhost:...). This appears to be a typo in the status detail line.
✏️ Proposed fix
- : `: localhost:${portNum}`}
+ : `localhost:${portNum}`}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| : `: localhost:${portNum}`} | |
| : `localhost:${portNum}`} |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/components/overview/tiles/TunnelMiniTile.jsx` at line 123, In
TunnelMiniTile.jsx, update the status detail rendering that currently uses the
template fragment `: localhost:${portNum}` (inside the TunnelMiniTile component
where portNum is used) to remove the extra leading colon so it reads
`localhost:${portNum}`; locate the JSX expression that builds the status/detail
string and remove the stray ":" before "localhost" to fix the formatting.
| if (saveTimerRef.current) clearTimeout(saveTimerRef.current); | ||
| saveTimerRef.current = setTimeout(() => { | ||
| if (projectId == null) return; | ||
| const key = `selfhost-overview-grid:${projectId}`; | ||
| try { | ||
| localStorage.setItem(key, JSON.stringify(nextLayout)); | ||
| } catch { | ||
| // Ignore storage errors (private mode / quota). | ||
| } | ||
| }, 250); | ||
| }; |
There was a problem hiding this comment.
Pending debounce timer should be cleaned up on unmount.
A queued save can still fire after unmount, causing stale writes. Add an unmount cleanup to clear saveTimerRef.current.
🧹 Proposed fix
export default function useProjectLayout({ projectId, defaultLayout }) {
const [layout, setLayout] = useState(defaultLayout);
const saveTimerRef = useRef(null);
const defaultLayoutRef = useRef(defaultLayout);
const projectIdRef = useRef(projectId);
+ useEffect(() => {
+ return () => {
+ if (saveTimerRef.current) clearTimeout(saveTimerRef.current);
+ };
+ }, []);
+
useEffect(() => {
defaultLayoutRef.current = defaultLayout;
}, [defaultLayout]);📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| if (saveTimerRef.current) clearTimeout(saveTimerRef.current); | |
| saveTimerRef.current = setTimeout(() => { | |
| if (projectId == null) return; | |
| const key = `selfhost-overview-grid:${projectId}`; | |
| try { | |
| localStorage.setItem(key, JSON.stringify(nextLayout)); | |
| } catch { | |
| // Ignore storage errors (private mode / quota). | |
| } | |
| }, 250); | |
| }; | |
| export default function useProjectLayout({ projectId, defaultLayout }) { | |
| const [layout, setLayout] = useState(defaultLayout); | |
| const saveTimerRef = useRef(null); | |
| const defaultLayoutRef = useRef(defaultLayout); | |
| const projectIdRef = useRef(projectId); | |
| useEffect(() => { | |
| return () => { | |
| if (saveTimerRef.current) clearTimeout(saveTimerRef.current); | |
| }; | |
| }, []); | |
| useEffect(() => { | |
| defaultLayoutRef.current = defaultLayout; | |
| }, [defaultLayout]); | |
| // ... rest of the hook implementation including the debounceSave logic |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/components/overview/useProjectLayout.js` around lines 39 - 49, Add an
unmount cleanup that clears the pending debounce timer so the queued save cannot
run after the component unmounts: ensure the effect or hook that sets
saveTimerRef.current (the function containing the setTimeout and
localStorage.setItem) returns a cleanup which checks saveTimerRef.current and
calls clearTimeout(saveTimerRef.current) and resets it (e.g.,
saveTimerRef.current = null). Reference the existing saveTimerRef.current and
the debounce block that calls setTimeout to locate where to add the cleanup.
…gation paths in Sidebar and ViewTabs components to reflect new project structure.
Summary by CodeRabbit
New Features
UI/UX