feat: enhance ESLint configuration and improve component error handling

- Updated ESLint configuration to include support for `.mjs` and `.cjs` file types, adding necessary global variables for Node.js and browser environments.
- Introduced a new `vite-env.d.ts` file to define environment variables for Vite, improving type safety.
- Refactored error handling in `file-browser-dialog.tsx`, `description-image-dropzone.tsx`, and `feature-image-upload.tsx` to omit error parameters, simplifying the catch blocks.
- Removed unused bug report button functionality from the sidebar, streamlining the component structure.
- Adjusted various components to improve code readability and maintainability, including updates to type imports and component props.

These changes aim to enhance the development experience by improving linting support and simplifying error handling across components.
This commit is contained in:
Kacper
2025-12-21 23:08:08 +01:00
parent 43c93fe19a
commit 26236d3d5b
40 changed files with 2013 additions and 2587 deletions

View File

@@ -1,5 +1,4 @@
import React, { useState, useEffect, useCallback, useMemo, useRef } from "react";
import React, { useState, useEffect, useCallback, useRef } from 'react';
import {
Terminal as TerminalIcon,
Plus,
@@ -12,17 +11,13 @@ import {
RefreshCw,
X,
SquarePlus,
} from "lucide-react";
import { useAppStore, type TerminalPanelContent, type TerminalTab } from "@/store/app-store";
import { useKeyboardShortcutsConfig, type KeyboardShortcut } from "@/hooks/use-keyboard-shortcuts";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import {
Panel,
PanelGroup,
PanelResizeHandle,
} from "react-resizable-panels";
import { TerminalPanel } from "./terminal-view/terminal-panel";
} from 'lucide-react';
import { useAppStore, type TerminalPanelContent, type TerminalTab } from '@/store/app-store';
import { useKeyboardShortcutsConfig } from '@/hooks/use-keyboard-shortcuts';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Panel, PanelGroup, PanelResizeHandle } from 'react-resizable-panels';
import { TerminalPanel } from './terminal-view/terminal-panel';
import {
DndContext,
DragEndEvent,
@@ -34,8 +29,8 @@ import {
closestCenter,
DragOverlay,
useDroppable,
} from "@dnd-kit/core";
import { cn } from "@/lib/utils";
} from '@dnd-kit/core';
import { cn } from '@/lib/utils';
interface TerminalStatus {
enabled: boolean;
@@ -64,18 +59,18 @@ function TerminalTabButton({
}) {
const { setNodeRef, isOver } = useDroppable({
id: `tab-${tab.id}`,
data: { type: "tab", tabId: tab.id },
data: { type: 'tab', tabId: tab.id },
});
return (
<div
ref={setNodeRef}
className={cn(
"flex items-center gap-1 px-3 py-1.5 text-sm rounded-t-md border-b-2 cursor-pointer transition-colors",
'flex items-center gap-1 px-3 py-1.5 text-sm rounded-t-md border-b-2 cursor-pointer transition-colors',
isActive
? "bg-background border-brand-500 text-foreground"
: "bg-muted border-transparent text-muted-foreground hover:text-foreground hover:bg-accent",
isOver && isDropTarget && "ring-2 ring-green-500"
? 'bg-background border-brand-500 text-foreground'
: 'bg-muted border-transparent text-muted-foreground hover:text-foreground hover:bg-accent',
isOver && isDropTarget && 'ring-2 ring-green-500'
)}
onClick={onClick}
>
@@ -97,18 +92,18 @@ function TerminalTabButton({
// New tab drop zone
function NewTabDropZone({ isDropTarget }: { isDropTarget: boolean }) {
const { setNodeRef, isOver } = useDroppable({
id: "new-tab-zone",
data: { type: "new-tab" },
id: 'new-tab-zone',
data: { type: 'new-tab' },
});
return (
<div
ref={setNodeRef}
className={cn(
"flex items-center justify-center px-3 py-1.5 rounded-t-md border-2 border-dashed transition-all",
'flex items-center justify-center px-3 py-1.5 rounded-t-md border-2 border-dashed transition-all',
isOver && isDropTarget
? "border-green-500 bg-green-500/10 text-green-500"
: "border-transparent text-muted-foreground hover:border-border"
? 'border-green-500 bg-green-500/10 text-green-500'
: 'border-transparent text-muted-foreground hover:border-border'
)}
>
<SquarePlus className="h-4 w-4" />
@@ -135,7 +130,7 @@ export function TerminalView() {
const [status, setStatus] = useState<TerminalStatus | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [password, setPassword] = useState("");
const [password, setPassword] = useState('');
const [authLoading, setAuthLoading] = useState(false);
const [authError, setAuthError] = useState<string | null>(null);
const [activeDragId, setActiveDragId] = useState<string | null>(null);
@@ -143,7 +138,7 @@ export function TerminalView() {
const lastCreateTimeRef = useRef<number>(0);
const isCreatingRef = useRef<boolean>(false);
const serverUrl = import.meta.env.VITE_SERVER_URL || "http://localhost:3008";
const serverUrl = import.meta.env.VITE_SERVER_URL || 'http://localhost:3008';
const CREATE_COOLDOWN_MS = 500; // Prevent rapid terminal creation
// Helper to check if terminal creation should be debounced
@@ -159,7 +154,7 @@ export function TerminalView() {
};
// Get active tab
const activeTab = terminalState.tabs.find(t => t.id === terminalState.activeTabId);
const activeTab = terminalState.tabs.find((t) => t.id === terminalState.activeTabId);
// DnD sensors with activation constraint to avoid accidental drags
const sensors = useSensors(
@@ -178,43 +173,46 @@ export function TerminalView() {
// Handle drag over - track which tab we're hovering
const handleDragOver = useCallback((event: DragOverEvent) => {
const { over } = event;
if (over?.data?.current?.type === "tab") {
if (over?.data?.current?.type === 'tab') {
setDragOverTabId(over.data.current.tabId);
} else if (over?.data?.current?.type === "new-tab") {
setDragOverTabId("new");
} else if (over?.data?.current?.type === 'new-tab') {
setDragOverTabId('new');
} else {
setDragOverTabId(null);
}
}, []);
// Handle drag end
const handleDragEnd = useCallback((event: DragEndEvent) => {
const { active, over } = event;
setActiveDragId(null);
setDragOverTabId(null);
const handleDragEnd = useCallback(
(event: DragEndEvent) => {
const { active, over } = event;
setActiveDragId(null);
setDragOverTabId(null);
if (!over) return;
if (!over) return;
const activeId = active.id as string;
const overData = over.data?.current;
const activeId = active.id as string;
const overData = over.data?.current;
// If dropped on a tab, move terminal to that tab
if (overData?.type === "tab") {
moveTerminalToTab(activeId, overData.tabId);
return;
}
// If dropped on a tab, move terminal to that tab
if (overData?.type === 'tab') {
moveTerminalToTab(activeId, overData.tabId);
return;
}
// If dropped on new tab zone, create new tab with this terminal
if (overData?.type === "new-tab") {
moveTerminalToTab(activeId, "new");
return;
}
// If dropped on new tab zone, create new tab with this terminal
if (overData?.type === 'new-tab') {
moveTerminalToTab(activeId, 'new');
return;
}
// Otherwise, swap terminals within current tab
if (active.id !== over.id) {
swapTerminals(activeId, over.id as string);
}
}, [swapTerminals, moveTerminalToTab]);
// Otherwise, swap terminals within current tab
if (active.id !== over.id) {
swapTerminals(activeId, over.id as string);
}
},
[swapTerminals, moveTerminalToTab]
);
// Fetch terminal status
const fetchStatus = useCallback(async () => {
@@ -229,11 +227,11 @@ export function TerminalView() {
setTerminalUnlocked(true);
}
} else {
setError(data.error || "Failed to get terminal status");
setError(data.error || 'Failed to get terminal status');
}
} catch (err) {
setError("Failed to connect to server");
console.error("[Terminal] Status fetch error:", err);
setError('Failed to connect to server');
console.error('[Terminal] Status fetch error:', err);
} finally {
setLoading(false);
}
@@ -251,21 +249,21 @@ export function TerminalView() {
try {
const response = await fetch(`${serverUrl}/api/terminal/auth`, {
method: "POST",
headers: { "Content-Type": "application/json" },
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ password }),
});
const data = await response.json();
if (data.success) {
setTerminalUnlocked(true, data.data.token);
setPassword("");
setPassword('');
} else {
setAuthError(data.error || "Authentication failed");
setAuthError(data.error || 'Authentication failed');
}
} catch (err) {
setAuthError("Failed to authenticate");
console.error("[Terminal] Auth error:", err);
setAuthError('Failed to authenticate');
console.error('[Terminal] Auth error:', err);
} finally {
setAuthLoading(false);
}
@@ -273,21 +271,24 @@ export function TerminalView() {
// Create a new terminal session
// targetSessionId: the terminal to split (if splitting an existing terminal)
const createTerminal = async (direction?: "horizontal" | "vertical", targetSessionId?: string) => {
if (!canCreateTerminal("[Terminal] Debounced terminal creation")) {
const createTerminal = async (
direction?: 'horizontal' | 'vertical',
targetSessionId?: string
) => {
if (!canCreateTerminal('[Terminal] Debounced terminal creation')) {
return;
}
try {
const headers: Record<string, string> = {
"Content-Type": "application/json",
'Content-Type': 'application/json',
};
if (terminalState.authToken) {
headers["X-Terminal-Token"] = terminalState.authToken;
headers['X-Terminal-Token'] = terminalState.authToken;
}
const response = await fetch(`${serverUrl}/api/terminal/sessions`, {
method: "POST",
method: 'POST',
headers,
body: JSON.stringify({
cwd: currentProject?.path || undefined,
@@ -300,10 +301,10 @@ export function TerminalView() {
if (data.success) {
addTerminalToLayout(data.data.id, direction, targetSessionId);
} else {
console.error("[Terminal] Failed to create session:", data.error);
console.error('[Terminal] Failed to create session:', data.error);
}
} catch (err) {
console.error("[Terminal] Create session error:", err);
console.error('[Terminal] Create session error:', err);
} finally {
isCreatingRef.current = false;
}
@@ -311,21 +312,21 @@ export function TerminalView() {
// Create terminal in new tab
const createTerminalInNewTab = async () => {
if (!canCreateTerminal("[Terminal] Debounced terminal tab creation")) {
if (!canCreateTerminal('[Terminal] Debounced terminal tab creation')) {
return;
}
const tabId = addTerminalTab();
try {
const headers: Record<string, string> = {
"Content-Type": "application/json",
'Content-Type': 'application/json',
};
if (terminalState.authToken) {
headers["X-Terminal-Token"] = terminalState.authToken;
headers['X-Terminal-Token'] = terminalState.authToken;
}
const response = await fetch(`${serverUrl}/api/terminal/sessions`, {
method: "POST",
method: 'POST',
headers,
body: JSON.stringify({
cwd: currentProject?.path || undefined,
@@ -341,7 +342,7 @@ export function TerminalView() {
addTerminalToTab(data.data.id, tabId);
}
} catch (err) {
console.error("[Terminal] Create session error:", err);
console.error('[Terminal] Create session error:', err);
} finally {
isCreatingRef.current = false;
}
@@ -352,16 +353,16 @@ export function TerminalView() {
try {
const headers: Record<string, string> = {};
if (terminalState.authToken) {
headers["X-Terminal-Token"] = terminalState.authToken;
headers['X-Terminal-Token'] = terminalState.authToken;
}
await fetch(`${serverUrl}/api/terminal/sessions/${sessionId}`, {
method: "DELETE",
method: 'DELETE',
headers,
});
removeTerminalFromLayout(sessionId);
} catch (err) {
console.error("[Terminal] Kill session error:", err);
console.error('[Terminal] Kill session error:', err);
}
};
@@ -391,25 +392,20 @@ export function TerminalView() {
const shiftMatches = needsShift ? e.shiftKey : !e.shiftKey;
const altMatches = needsAlt ? e.altKey : !e.altKey;
return (
e.key.toLowerCase() === key &&
cmdMatches &&
shiftMatches &&
altMatches
);
return e.key.toLowerCase() === key && cmdMatches && shiftMatches && altMatches;
};
// Split terminal right (Cmd+D / Ctrl+D)
if (matchesShortcut(shortcuts.splitTerminalRight)) {
e.preventDefault();
createTerminal("horizontal", terminalState.activeSessionId);
createTerminal('horizontal', terminalState.activeSessionId);
return;
}
// Split terminal down (Cmd+Shift+D / Ctrl+Shift+D)
if (matchesShortcut(shortcuts.splitTerminalDown)) {
e.preventDefault();
createTerminal("vertical", terminalState.activeSessionId);
createTerminal('vertical', terminalState.activeSessionId);
return;
}
@@ -427,7 +423,7 @@ export function TerminalView() {
// Collect all terminal IDs from a panel tree in order
const getTerminalIds = (panel: TerminalPanelContent): string[] => {
if (panel.type === "terminal") {
if (panel.type === 'terminal') {
return [panel.sessionId];
}
return panel.panels.flatMap(getTerminalIds);
@@ -436,16 +432,16 @@ export function TerminalView() {
// Get a STABLE key for a panel - based only on terminal IDs, not tree structure
// This prevents unnecessary remounts when layout structure changes
const getPanelKey = (panel: TerminalPanelContent): string => {
if (panel.type === "terminal") {
if (panel.type === 'terminal') {
return panel.sessionId;
}
// Use joined terminal IDs - stable regardless of nesting depth
return `group-${getTerminalIds(panel).join("-")}`;
return `group-${getTerminalIds(panel).join('-')}`;
};
// Render panel content recursively
const renderPanelContent = (content: TerminalPanelContent): React.ReactNode => {
if (content.type === "terminal") {
if (content.type === 'terminal') {
// Use per-terminal fontSize or fall back to default
const terminalFontSize = content.fontSize ?? terminalState.defaultFontSize;
return (
@@ -456,8 +452,8 @@ export function TerminalView() {
isActive={terminalState.activeSessionId === content.sessionId}
onFocus={() => setActiveTerminalSession(content.sessionId)}
onClose={() => killTerminal(content.sessionId)}
onSplitHorizontal={() => createTerminal("horizontal", content.sessionId)}
onSplitVertical={() => createTerminal("vertical", content.sessionId)}
onSplitHorizontal={() => createTerminal('horizontal', content.sessionId)}
onSplitVertical={() => createTerminal('vertical', content.sessionId)}
isDragging={activeDragId === content.sessionId}
isDropTarget={activeDragId !== null && activeDragId !== content.sessionId}
fontSize={terminalFontSize}
@@ -466,15 +462,14 @@ export function TerminalView() {
);
}
const isHorizontal = content.direction === "horizontal";
const isHorizontal = content.direction === 'horizontal';
const defaultSizePerPanel = 100 / content.panels.length;
return (
<PanelGroup direction={content.direction}>
{content.panels.map((panel, index) => {
const panelSize = panel.type === "terminal" && panel.size
? panel.size
: defaultSizePerPanel;
const panelSize =
panel.type === 'terminal' && panel.size ? panel.size : defaultSizePerPanel;
const panelKey = getPanelKey(panel);
return (
@@ -484,8 +479,8 @@ export function TerminalView() {
key={`handle-${panelKey}`}
className={
isHorizontal
? "w-1 h-full bg-border hover:bg-brand-500 transition-colors data-[resize-handle-state=hover]:bg-brand-500 data-[resize-handle-state=drag]:bg-brand-500"
: "h-1 w-full bg-border hover:bg-brand-500 transition-colors data-[resize-handle-state=hover]:bg-brand-500 data-[resize-handle-state=drag]:bg-brand-500"
? 'w-1 h-full bg-border hover:bg-brand-500 transition-colors data-[resize-handle-state=hover]:bg-brand-500 data-[resize-handle-state=drag]:bg-brand-500'
: 'h-1 w-full bg-border hover:bg-brand-500 transition-colors data-[resize-handle-state=hover]:bg-brand-500 data-[resize-handle-state=drag]:bg-brand-500'
}
/>
)}
@@ -534,7 +529,9 @@ export function TerminalView() {
</div>
<h2 className="text-lg font-medium mb-2">Terminal Disabled</h2>
<p className="text-muted-foreground max-w-md">
Terminal access has been disabled. Set <code className="px-1.5 py-0.5 rounded bg-muted">TERMINAL_ENABLED=true</code> in your server .env file to enable it.
Terminal access has been disabled. Set{' '}
<code className="px-1.5 py-0.5 rounded bg-muted">TERMINAL_ENABLED=true</code> in your
server .env file to enable it.
</p>
</div>
);
@@ -561,9 +558,7 @@ export function TerminalView() {
disabled={authLoading}
autoFocus
/>
{authError && (
<p className="text-sm text-destructive">{authError}</p>
)}
{authError && <p className="text-sm text-destructive">{authError}</p>}
<Button type="submit" className="w-full" disabled={authLoading || !password}>
{authLoading ? (
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
@@ -577,8 +572,8 @@ export function TerminalView() {
{status.platform && (
<p className="text-xs text-muted-foreground mt-6">
Platform: {status.platform.platform}
{status.platform.isWSL && " (WSL)"}
{" | "}Shell: {status.platform.defaultShell}
{status.platform.isWSL && ' (WSL)'}
{' | '}Shell: {status.platform.defaultShell}
</p>
)}
</div>
@@ -597,7 +592,8 @@ export function TerminalView() {
Create a new terminal session to start executing commands.
{currentProject && (
<span className="block mt-2 text-sm">
Working directory: <code className="px-1.5 py-0.5 rounded bg-muted">{currentProject.path}</code>
Working directory:{' '}
<code className="px-1.5 py-0.5 rounded bg-muted">{currentProject.path}</code>
</span>
)}
</p>
@@ -610,8 +606,8 @@ export function TerminalView() {
{status?.platform && (
<p className="text-xs text-muted-foreground mt-6">
Platform: {status.platform.platform}
{status.platform.isWSL && " (WSL)"}
{" | "}Shell: {status.platform.defaultShell}
{status.platform.isWSL && ' (WSL)'}
{' | '}Shell: {status.platform.defaultShell}
</p>
)}
</div>
@@ -644,9 +640,7 @@ export function TerminalView() {
))}
{/* New tab drop zone (visible when dragging) */}
{activeDragId && (
<NewTabDropZone isDropTarget={true} />
)}
{activeDragId && <NewTabDropZone isDropTarget={true} />}
{/* New tab button */}
<button
@@ -664,7 +658,7 @@ export function TerminalView() {
variant="ghost"
size="sm"
className="h-7 px-2 text-muted-foreground hover:text-foreground"
onClick={() => createTerminal("horizontal")}
onClick={() => createTerminal('horizontal')}
title="Split Right"
>
<SplitSquareHorizontal className="h-4 w-4" />
@@ -673,7 +667,7 @@ export function TerminalView() {
variant="ghost"
size="sm"
className="h-7 px-2 text-muted-foreground hover:text-foreground"
onClick={() => createTerminal("vertical")}
onClick={() => createTerminal('vertical')}
title="Split Down"
>
<SplitSquareVertical className="h-4 w-4" />
@@ -688,11 +682,7 @@ export function TerminalView() {
) : (
<div className="flex-1 flex flex-col items-center justify-center text-center p-6">
<p className="text-muted-foreground mb-4">This tab is empty</p>
<Button
variant="outline"
size="sm"
onClick={() => createTerminal()}
>
<Button variant="outline" size="sm" onClick={() => createTerminal()}>
<Plus className="h-4 w-4 mr-2" />
New Terminal
</Button>
@@ -707,11 +697,7 @@ export function TerminalView() {
<div className="relative inline-flex items-center gap-2 px-3.5 py-2 bg-card border-2 border-brand-500 rounded-lg shadow-xl pointer-events-none overflow-hidden">
<TerminalIcon className="h-4 w-4 text-brand-500 shrink-0" />
<span className="text-sm font-medium text-foreground whitespace-nowrap">
{dragOverTabId === "new"
? "New tab"
: dragOverTabId
? "Move to tab"
: "Terminal"}
{dragOverTabId === 'new' ? 'New tab' : dragOverTabId ? 'Move to tab' : 'Terminal'}
</span>
</div>
) : null}