+
-
Target Directory Path
-
+
+ Target Directory Path
+
+
+
+
+
+
+
setNewPresetPath(e.target.value)}
+ onKeyDown={(e) => {
+ if (e.key === "Enter" && newPresetPath.trim() && !pathError && !isValidating) {
+ handleSave();
+ }
+ }}
/>
-
- Browse
-
-
+
+
+ {newPresetPath && (
+
setNewPresetPath("")}
+ className="h-7 w-7 text-[var(--text-secondary)] hover:text-red-400 hover:bg-red-400/5 rounded-md transition-all"
+ >
+
+
+ )}
+
+
+
+ Browse
+
+
+
+
+ {pathError && (
+
+ )}
+ {newPresetPath && !pathError && !isValidating && (
+
+ Directory verified.
+
+ )}
-
- {newPresetPath ? (
- <>Auto-Labeling as:
{(newPresetPath.split(/[\\/]/).filter(Boolean).pop() || "ROOT").toUpperCase()} >
- ) : "Select a path to automatically generate a preset label."}
+
+ {newPresetPath && !pathError ? (
+ <>
+ Auto-Labeling as:{" "}
+
+ {(
+ newPresetPath.split(/[\\/]/).filter(Boolean).pop() ||
+ "ROOT"
+ ).toUpperCase()}
+
+ >
+ ) : (
+ "Select a path to automatically generate a preset label."
+ )}
- setIsAdding(false)} className="text-[11px] h-8 text-[var(--text-secondary)]">Cancel
+ setIsAdding(false)}
+ className="text-[11px] h-8 text-[var(--text-secondary)]"
+ >
+ Cancel
+
- Save Preset
+ {isValidating ? "Validating..." : "Save Preset"}
-
- )}
+ )}
-
+
-
+
Active ({activePresets.length})
-
+
Archived ({archivedPresets.length})
@@ -357,23 +562,34 @@ export function PresetsTab({
onClick={() => setIsAdding(!isAdding)}
className="h-8 px-4 text-[11px] font-bold bg-[var(--accent-primary)] text-[var(--accent-contrast)] hover:opacity-90 rounded-md transition-all flex gap-2"
>
-
{isAdding ? "Cancel" : "Add Preset"}
+
{" "}
+ {isAdding ? "Cancel" : "Add Preset"}
-
- {renderActiveContent()}
-
+ {renderActiveContent()}
-
- {renderArchivedContent()}
-
+ {renderArchivedContent()}
);
}
-function PresetCard({ preset, isSelected, onToggleSelection, onArchive, onRestore, onDelete }: {
+function PresetCard({
+ preset,
+ isSelected,
+ onToggleSelection,
+ onArchive,
+ onRestore,
+ onDelete,
+}: {
preset: DirectoryPreset;
isSelected?: boolean;
onToggleSelection?: () => void;
@@ -385,13 +601,15 @@ function PresetCard({ preset, isSelected, onToggleSelection, onArchive, onRestor
-
+
@@ -400,62 +618,85 @@ function PresetCard({ preset, isSelected, onToggleSelection, onArchive, onRestor
onClick={onToggleSelection}
className={cn(
"w-4 h-4 rounded border transition-all flex items-center justify-center cursor-pointer shrink-0",
- isSelected ? "border-[var(--accent-primary)] bg-[var(--accent-primary)]" : "border-[var(--border-color)] hover:border-[var(--accent-primary)]/50"
+ isSelected
+ ? "border-[var(--accent-primary)] bg-[var(--accent-primary)]"
+ : "border-[var(--border-color)] hover:border-[var(--accent-primary)]/50",
)}
>
- {isSelected &&
}
+ {isSelected && (
+
+ )}
)}
{preset.label}
-
+
- {truncatePath(preset.path, 35)}
+
+ {truncatePath(preset.path, 35)}
+
- Directory Preset
-
- {onArchive && (
-
-
-
-
-
-
- Archive Preset
-
- )}
- {onRestore && (
-
-
-
-
-
-
- Restore Preset
-
- )}
- {onDelete && (
-
-
- { e.stopPropagation(); onDelete(); }}
- >
-
-
-
- Delete Permanently
-
- )}
-
+
+ Directory Preset
+
+
+ {onArchive && (
+
+
+
+
+
+
+ Archive Preset
+
+ )}
+ {onRestore && (
+
+
+
+
+
+
+ Restore Preset
+
+ )}
+ {onDelete && (
+
+
+ {
+ e.stopPropagation();
+ onDelete();
+ }}
+ >
+
+
+
+
+ Delete Permanently
+
+
+ )}
+
);
diff --git a/src/features/cortex-library/components/SnippetsTab.tsx b/src/features/cortex-library/components/SnippetsTab.tsx
index 9eb48c0..bbc7875 100644
--- a/src/features/cortex-library/components/SnippetsTab.tsx
+++ b/src/features/cortex-library/components/SnippetsTab.tsx
@@ -1,12 +1,23 @@
import { useState, useMemo } from "react";
-import { ChevronRightSquare, Plus, Terminal, Trash2, Archive, RotateCcw } from "@/components/ui/icons";
+import {
+ ChevronRightSquare,
+ Plus,
+ Terminal,
+ Trash2,
+ Archive,
+ RotateCcw,
+} from "@/components/ui/icons";
import { Button } from "@/components/ui/button";
import { Card, CardHeader, CardTitle, CardFooter } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
-import { Snippet } from "@/types";
+import { Snippet } from "@/lib";
import { cn } from "@/lib/utils";
import { EmptyState } from "@/components/ui/empty-state";
-import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
+import {
+ Tooltip,
+ TooltipContent,
+ TooltipTrigger,
+} from "@/components/ui/tooltip";
import { ViewMode } from "@/components/ui/view-toggle";
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs";
import {
@@ -53,33 +64,41 @@ export function SnippetsTab({
isAdding,
setIsAdding,
activeSubTab,
- onSubTabChange
+ onSubTabChange,
}: SnippetsTabProps) {
// State lifted to CortexLibraryDialog to persist across sidebar navigation
const [newLabel, setNewLabel] = useState("");
const [newCommand, setNewCommand] = useState("");
- const activeSnippets = useMemo(() =>
- snippets.filter(s => !s.isArchived),
- [snippets]);
+ const activeSnippets = useMemo(
+ () => snippets.filter((s) => !s.isArchived),
+ [snippets],
+ );
- const archivedSnippets = useMemo(() =>
- snippets.filter(s => s.isArchived),
- [snippets]);
+ const archivedSnippets = useMemo(
+ () => snippets.filter((s) => s.isArchived),
+ [snippets],
+ );
- const filtered = useMemo(() =>
- activeSnippets.filter(s =>
- s.label.toLowerCase().includes(searchQuery.toLowerCase()) ||
- s.command.toLowerCase().includes(searchQuery.toLowerCase())
- ),
- [activeSnippets, searchQuery]);
+ const filtered = useMemo(
+ () =>
+ activeSnippets.filter(
+ (s) =>
+ s.label.toLowerCase().includes(searchQuery.toLowerCase()) ||
+ s.command.toLowerCase().includes(searchQuery.toLowerCase()),
+ ),
+ [activeSnippets, searchQuery],
+ );
- const archivedFiltered = useMemo(() =>
- archivedSnippets.filter(s =>
- s.label.toLowerCase().includes(searchQuery.toLowerCase()) ||
- s.command.toLowerCase().includes(searchQuery.toLowerCase())
- ),
- [archivedSnippets, searchQuery]);
+ const archivedFiltered = useMemo(
+ () =>
+ archivedSnippets.filter(
+ (s) =>
+ s.label.toLowerCase().includes(searchQuery.toLowerCase()) ||
+ s.command.toLowerCase().includes(searchQuery.toLowerCase()),
+ ),
+ [archivedSnippets, searchQuery],
+ );
const handleSave = () => {
if (newCommand.trim()) {
@@ -95,26 +114,33 @@ export function SnippetsTab({
return (
setIsAdding(true),
- icon: Plus
- } : undefined}
+ action={
+ !searchQuery
+ ? {
+ label: "Create New Snippet",
+ onClick: () => setIsAdding(true),
+ icon: Plus,
+ }
+ : undefined
+ }
compact
/>
);
}
- if (viewMode === 'card') {
+ if (viewMode === "card") {
return (
- {filtered.map(snippet => (
+ {filtered.map((snippet) => (
- {/* Select All Checkbox could go here but it needs to clear parent state */}
+ {/* Select All Checkbox could go here but it needs to clear parent state */}
+
+
+ Label
+
+
+ Command
- Label
- Command
- {filtered.map(snippet => (
+ {filtered.map((snippet) => (
{selectedIds.has(snippet.id) && (
@@ -161,7 +191,10 @@ export function SnippetsTab({
-
+
{snippet.label}
@@ -170,8 +203,11 @@ export function SnippetsTab({
-
-
+
+
{snippet.command}
@@ -179,7 +215,8 @@ export function SnippetsTab({
onExecute(snippet, false)}
>
@@ -195,7 +232,12 @@ export function SnippetsTab({
{onArchive && (
- onArchive(snippet.id)}>
+ onArchive(snippet.id)}
+ >
@@ -222,7 +264,7 @@ export function SnippetsTab({
if (archivedSelectedIds.size === archivedFiltered.length) {
setArchivedSelectedIds(new Set());
} else {
- setArchivedSelectedIds(new Set(archivedFiltered.map(s => s.id)));
+ setArchivedSelectedIds(new Set(archivedFiltered.map((s) => s.id)));
}
};
@@ -232,9 +274,10 @@ export function SnippetsTab({
- {viewMode === 'card' ? (
+ {viewMode === "card" ? (
{archivedFiltered.map((snippet) => (
toggleArchivedSelection(snippet.id)}
onArchive={undefined} // No archive button in archived view
onExecute={undefined} // No execute in archived view
- onRestore={onUnarchive ? () => onUnarchive(snippet.id) : undefined}
+ onRestore={
+ onUnarchive ? () => onUnarchive(snippet.id) : undefined
+ }
onDelete={() => onDelete(snippet.id)}
/>
))}
@@ -270,49 +315,63 @@ export function SnippetsTab({
"w-4 h-4 rounded border transition-all flex items-center justify-center cursor-pointer",
archivedSelectedIds.size > 0
? "border-[var(--accent-primary)] bg-[var(--accent-primary)]"
- : "border-[var(--border-color)] hover:border-[var(--accent-primary)]/50"
+ : "border-[var(--border-color)] hover:border-[var(--accent-primary)]/50",
)}
>
- {archivedSelectedIds.size === archivedFiltered.length && archivedSelectedIds.size > 0 && (
-
- )}
- {archivedSelectedIds.size > 0 && archivedSelectedIds.size < archivedFiltered.length && (
-
- )}
+ {archivedSelectedIds.size === archivedFiltered.length &&
+ archivedSelectedIds.size > 0 && (
+
+ )}
+ {archivedSelectedIds.size > 0 &&
+ archivedSelectedIds.size < archivedFiltered.length && (
+
+ )}
- Label
- Command
- Actions
+
+ Label
+
+
+ Command
+
+
+ Actions
+
- {archivedFiltered.map(snippet => (
-
-
- toggleArchivedSelection(snippet.id)}
+ {archivedFiltered.map((snippet) => (
+
- {archivedSelectedIds.has(snippet.id) && (
-
- )}
-
+
+ toggleArchivedSelection(snippet.id)}
+ className={cn(
+ "w-4 h-4 rounded border transition-all flex items-center justify-center cursor-pointer",
+ archivedSelectedIds.has(snippet.id)
+ ? "border-[var(--accent-primary)] bg-[var(--accent-primary)]"
+ : "border-[var(--border-color)] hover:border-[var(--accent-primary)]/50",
+ )}
+ >
+ {archivedSelectedIds.has(snippet.id) && (
+
+ )}
+
-
+
{snippet.label}
@@ -320,7 +379,7 @@ export function SnippetsTab({
-
+
{snippet.command}
@@ -328,7 +387,14 @@ export function SnippetsTab({
- onUnarchive && onUnarchive(snippet.id)}>
+
+ onUnarchive && onUnarchive(snippet.id)
+ }
+ >
@@ -336,7 +402,12 @@ export function SnippetsTab({
- onDelete(snippet.id)}>
+ onDelete(snippet.id)}
+ >
@@ -366,13 +437,23 @@ export function SnippetsTab({
/>
)}
-
+
-
+
Active ({activeSnippets.length})
-
+
Archived ({archivedSnippets.length})
@@ -381,23 +462,34 @@ export function SnippetsTab({
onClick={() => setIsAdding(!isAdding)}
className="h-8 px-4 text-[11px] font-bold bg-[var(--accent-primary)] text-[var(--accent-contrast)] hover:opacity-90 rounded-md transition-all flex gap-2"
>
-
{isAdding ? "Cancel" : "New Snippet"}
+
{" "}
+ {isAdding ? "Cancel" : "New Snippet"}
-
- {renderActiveContent()}
-
+ {renderActiveContent()}
-
- {renderArchivedContent()}
-
+ {renderArchivedContent()}
);
}
-function AddSnippetForm({ label, command, onLabelChange, onCommandChange, onSave, onCancel }: {
+function AddSnippetForm({
+ label,
+ command,
+ onLabelChange,
+ onCommandChange,
+ onSave,
+ onCancel,
+}: {
label: string;
command: string;
onLabelChange: (v: string) => void;
@@ -409,17 +501,21 @@ function AddSnippetForm({ label, command, onLabelChange, onCommandChange, onSave
- Terminal Command
+
+ Terminal Command
+
onCommandChange(e.target.value)}
/>
- Snippet Label
+
+ Snippet Label
+
- Cancel
- Save Snippet
+
+ Cancel
+
+
+ Save Snippet
+
);
}
-function SnippetCard({ snippet, isSelected, onToggleSelection, onArchive, onExecute, onRestore, onDelete }: {
+function SnippetCard({
+ snippet,
+ isSelected,
+ onToggleSelection,
+ onArchive,
+ onExecute,
+ onRestore,
+ onDelete,
+}: {
snippet: Snippet;
isSelected: boolean;
onToggleSelection: () => void;
@@ -449,13 +566,15 @@ function SnippetCard({ snippet, isSelected, onToggleSelection, onArchive, onExec
-
+
@@ -463,95 +582,119 @@ function SnippetCard({ snippet, isSelected, onToggleSelection, onArchive, onExec
onClick={onToggleSelection}
className={cn(
"w-4 h-4 rounded border transition-all flex items-center justify-center cursor-pointer shrink-0",
- isSelected ? "border-[var(--accent-primary)] bg-[var(--accent-primary)]" : "border-[var(--border-color)] hover:border-[var(--accent-primary)]/50"
+ isSelected
+ ? "border-[var(--accent-primary)] bg-[var(--accent-primary)]"
+ : "border-[var(--border-color)] hover:border-[var(--accent-primary)]/50",
)}
>
- {isSelected &&
}
+ {isSelected && (
+
+ )}
{snippet.label}
-
+
- {snippet.command}
+
+ {snippet.command}
+
-
- {onExecute && (
- <>
+
+ {onExecute && (
+ <>
+ onExecute(false)}
+ >
+ Inject
+
+ onExecute(true)}
+ >
+ Run
+
+ >
+ )}
+ {!onExecute && (
+
+ Terminal Snippet
+
+ )}
+
+
+
+ {onArchive && (
+
+
onExecute(false)}
+ variant="ghost"
+ size="icon"
+ className="h-6 w-6 text-[var(--text-secondary)]/60 opacity-0 group-hover:opacity-100 transition-all hover:bg-[var(--accent-primary)]/5 hover:text-[var(--accent-primary)] active:scale-95"
+ onClick={(e) => {
+ e.stopPropagation();
+ onArchive();
+ }}
>
- Inject
+
+
+
+ Archive Snippet
+
+
+ )}
+ {onRestore && (
+
+
onExecute(true)}
+ variant="ghost"
+ size="icon"
+ className="h-6 w-6 text-[var(--text-secondary)]/60 opacity-0 group-hover:opacity-100 transition-all hover:bg-[var(--accent-primary)]/5 hover:text-[var(--accent-primary)] active:scale-95"
+ onClick={(e) => {
+ e.stopPropagation();
+ onRestore();
+ }}
>
- Run
+
- >
- )}
- {!onExecute && (
- Terminal Snippet
- )}
-
-
-
- {onArchive && (
-
-
- { e.stopPropagation(); onArchive(); }}
- >
-
-
-
- Archive Snippet
-
- )}
- {onRestore && (
-
-
- { e.stopPropagation(); onRestore(); }}
- >
-
-
-
- Restore
-
- )}
- {onDelete && (
-
-
- { e.stopPropagation(); onDelete(); }}
- >
-
-
-
- Delete Permanently
-
- )}
-
+
+
+ Restore
+
+
+ )}
+ {onDelete && (
+
+
+ {
+ e.stopPropagation();
+ onDelete();
+ }}
+ >
+
+
+
+
+ Delete Permanently
+
+
+ )}
+
);
diff --git a/src/features/cortex-library/components/ThemesTab.tsx b/src/features/cortex-library/components/ThemesTab.tsx
new file mode 100644
index 0000000..c501a08
--- /dev/null
+++ b/src/features/cortex-library/components/ThemesTab.tsx
@@ -0,0 +1,536 @@
+import * as React from "react";
+import { useMemo } from "react";
+import { Card, CardContent } from "@/components/ui/card";
+import { Button } from "@/components/ui/button";
+import { Badge } from "@/components/ui/badge";
+import { Textarea } from "@/components/ui/textarea";
+import { Switch } from "@/components/ui/switch";
+import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
+import { Copy, Trash2, Check, Plus, Code, Palette } from "@/components/ui/icons";
+import { ThemeName, ThemeDefinition } from "@/hooks/useTheme";
+import { toast } from "sonner";
+
+import { ViewMode } from "@/components/ui/view-toggle";
+import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs";
+import { EmptyState } from "@/components/ui/empty-state";
+import {
+ Table,
+ TableHeader,
+ TableBody,
+ TableHead,
+ TableRow,
+ TableCell,
+} from "@/components/ui/table";
+import { cn } from "@/lib/utils";
+
+interface ThemesTabProps {
+ theme: ThemeName;
+ allThemes: ThemeDefinition[];
+ resolvedScheme: "light" | "dark";
+ setTheme: (theme: ThemeName) => void;
+ addCustomTheme: (theme: ThemeDefinition) => Promise
;
+ removeCustomTheme: (id: string) => Promise;
+ previewTheme: (config: ThemeDefinition) => void;
+ cancelPreview: () => void;
+ searchQuery: string;
+ viewMode: ViewMode;
+ isAdding: boolean;
+ setIsAdding: (adding: boolean) => void;
+ activeSubTab: string;
+ onSubTabChange: (tab: string) => void;
+}
+
+function normalizeThemeInput(parsed: any): ThemeDefinition {
+ if (!parsed || typeof parsed !== "object") {
+ throw new Error("Invalid theme object");
+ }
+ if (!parsed.id || typeof parsed.id !== "string") {
+ throw new Error("Missing or invalid 'id' property.");
+ }
+ if (!parsed.name || typeof parsed.name !== "string") {
+ throw new Error("Missing or invalid 'name' property.");
+ }
+
+ if (parsed.dark && typeof parsed.dark === "object") {
+ const requiredPalette = ["bg", "headerBg", "footerBg", "surface", "border", "textPrimary", "textSecondary", "accent"];
+ for (const key of requiredPalette) {
+ if (!parsed.dark[key]) {
+ throw new Error(`Missing required theme palette property under dark: ${key}`);
+ }
+ }
+ return {
+ id: parsed.id,
+ name: parsed.name,
+ dark: parsed.dark,
+ light: parsed.light,
+ isCustom: true,
+ };
+ } else if (parsed.light && typeof parsed.light === "object") {
+ const requiredPalette = ["bg", "headerBg", "footerBg", "surface", "border", "textPrimary", "textSecondary", "accent"];
+ for (const key of requiredPalette) {
+ if (!parsed.light[key]) {
+ throw new Error(`Missing required theme palette property under light: ${key}`);
+ }
+ }
+ return {
+ id: parsed.id,
+ name: parsed.name,
+ dark: parsed.dark,
+ light: parsed.light,
+ isCustom: true,
+ };
+ } else {
+ const required = ["bg", "headerBg", "footerBg", "surface", "border", "textPrimary", "textSecondary", "accent"];
+ for (const key of required) {
+ if (!parsed[key]) {
+ throw new Error(`Missing required theme property: ${key}`);
+ }
+ }
+ return {
+ id: parsed.id,
+ name: parsed.name,
+ dark: {
+ bg: parsed.bg,
+ headerBg: parsed.headerBg,
+ footerBg: parsed.footerBg,
+ surface: parsed.surface,
+ border: parsed.border,
+ textPrimary: parsed.textPrimary,
+ textSecondary: parsed.textSecondary,
+ accent: parsed.accent,
+ ansi: parsed.ansi,
+ },
+ light: parsed.light,
+ isCustom: true,
+ isLegacy: true,
+ };
+ }
+}
+
+const THEME_DESCRIPTIONS: Record = {
+ claude: "Anthropic's warm, organic paper-like aesthetic.",
+ cursor: "A deep, modern dark mode inspired by AI code editors.",
+ cortex: "The original high-contrast neon-pop experience.",
+ tokyo: "A vibrant, neon-lit theme inspired by Tokyo at night.",
+ nord: "An arctic, north-bluish clean and focused palette.",
+ catppuccin: "A soothing, high-productivity pastel color scheme.",
+ caffeine: "A warm, earthy workspace inspired by morning coffee.",
+};
+
+export function ThemesTab({
+ theme,
+ allThemes,
+ resolvedScheme,
+ setTheme,
+ addCustomTheme,
+ removeCustomTheme,
+ previewTheme,
+ cancelPreview,
+ searchQuery,
+ viewMode,
+ isAdding,
+ setIsAdding,
+ activeSubTab,
+ onSubTabChange,
+}: ThemesTabProps) {
+ const [jsonInput, setJsonInput] = React.useState("");
+ const [isPreviewing, setIsPreviewing] = React.useState(false);
+
+ // Live Preview Logic
+ React.useEffect(() => {
+ if (!isPreviewing || !jsonInput.trim()) {
+ if (!isPreviewing) cancelPreview();
+ return;
+ }
+
+ try {
+ const parsed = JSON.parse(jsonInput);
+ const normalized = normalizeThemeInput(parsed);
+ previewTheme(normalized);
+ } catch (e) {
+ // Silently fail preview for invalid JSON
+ }
+ }, [jsonInput, isPreviewing, previewTheme, cancelPreview]);
+
+ const handleImportTheme = async () => {
+ if (!jsonInput.trim()) return;
+
+ try {
+ const parsed = JSON.parse(jsonInput);
+ const normalized = normalizeThemeInput(parsed);
+
+ await addCustomTheme(normalized);
+ toast.success(`${normalized.name} created successfully`, {
+ description: "The theme has been added to your library.",
+ });
+ setJsonInput("");
+ setIsAdding(false);
+ setIsPreviewing(false);
+ } catch (err: any) {
+ toast.error("Failed to create Theme", {
+ description: "Check your JSON formatting and required keys.",
+ });
+ }
+ };
+
+ const handleCopyJson = (t: ThemeDefinition) => {
+ const { isCustom, ...rest } = t; // Strip internal flag
+ navigator.clipboard.writeText(JSON.stringify(rest, null, 2));
+ toast.success(`${t.name} copied successfully`, {
+ description: "The theme JSON is ready to paste.",
+ });
+ };
+
+ const activeThemes = useMemo(() => allThemes, [allThemes]);
+
+ const customThemes = useMemo(
+ () => allThemes.filter((t: ThemeDefinition) => t.isCustom),
+ [allThemes]
+ );
+
+ const filtered = useMemo(() => {
+ const list = activeSubTab === "custom" ? customThemes : activeThemes;
+ return list.filter(
+ (t: ThemeDefinition) =>
+ t.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
+ t.id.toLowerCase().includes(searchQuery.toLowerCase()) ||
+ (THEME_DESCRIPTIONS[t.id] || "User-created theme").toLowerCase().includes(searchQuery.toLowerCase())
+ );
+ }, [activeSubTab, activeThemes, customThemes, searchQuery]);
+
+ const renderContent = () => {
+ if (filtered.length === 0) {
+ return (
+ setIsAdding(true),
+ icon: Plus,
+ }
+ : undefined
+ }
+ compact
+ />
+ );
+ }
+
+ if (viewMode === "card") {
+ return (
+
+ {filtered.map((t: ThemeDefinition) => {
+ const isActive = theme === t.id;
+ return (
+
setTheme(t.id)}
+ className={cn(
+ "group/theme cursor-pointer transition-all hover:bg-[var(--text-primary)]/[0.04] text-left overflow-hidden border",
+ isActive
+ ? "border-[var(--accent-primary)] bg-[var(--text-primary)]/5 shadow-[0_0_20px_rgba(var(--accent-primary-rgb),0.05)]"
+ : "border-[var(--border-color)] bg-[var(--text-primary)]/[0.02]"
+ )}
+ >
+
+ {/* Visual Palette Representation */}
+
+
+ {(() => {
+ const previewPalette = (() => {
+ if (t.light && t.dark) {
+ return resolvedScheme === "light" ? t.light : t.dark;
+ }
+ if (t.light) return t.light;
+ if (t.dark) {
+ if (resolvedScheme === "light" && t.isLegacy) {
+ return {
+ bg: "#FAFAFA",
+ surface: "#FFFFFF",
+ accent: t.dark.accent,
+ };
+ }
+ return t.dark;
+ }
+ return {
+ bg: "#FAFAFA",
+ surface: "#FFFFFF",
+ accent: "#000000",
+ };
+ })();
+ return (
+ <>
+
+
+
+ >
+ );
+ })()}
+
+
+
+ {/* Name and Metadata */}
+
+
+
+ {t.name}
+
+ {isActive && (
+
+ Active
+
+ )}
+
+
+ {t.isCustom ? "User-created theme" : THEME_DESCRIPTIONS[t.id] || "Core Cortex preset"}
+
+
+
+ {/* Actions */}
+
+
+
+ {
+ e.stopPropagation();
+ handleCopyJson(t);
+ }}
+ className="w-7 h-7 rounded-md text-[var(--text-secondary)]/40 opacity-0 group-hover/theme:opacity-100 transition-all hover:text-[var(--accent-primary)] hover:bg-[var(--accent-primary)]/10"
+ >
+
+
+
+
+ Copy JSON
+
+
+ {t.isCustom && (
+
+
+ {
+ e.stopPropagation();
+ removeCustomTheme(t.id);
+ }}
+ className="w-7 h-7 text-[var(--text-secondary)]/60 opacity-0 group-hover/theme:opacity-100 transition-all hover:bg-red-500/10 hover:text-red-400 active:scale-95"
+ >
+
+
+
+
+ Delete Theme
+
+
+ )}
+
+
+
+ );
+ })}
+
+ );
+ }
+
+ return (
+
+
+
+
+ Name
+ Description
+ Type
+
+
+
+
+ {filtered.map((t: ThemeDefinition) => {
+ const isActive = theme === t.id;
+ return (
+ setTheme(t.id)}
+ className={cn(
+ "cursor-pointer transition-all",
+ isActive
+ ? "bg-[var(--accent-primary)]/[0.02] hover:bg-[var(--accent-primary)]/[0.04]"
+ : "text-[var(--text-secondary)]/70 hover:bg-[var(--text-primary)]/[0.02]"
+ )}
+ >
+
+
+
+
+
+ {t.name}
+
+
+
+
+ {t.isCustom ? "User-created theme" : THEME_DESCRIPTIONS[t.id] || "Core preset theme"}
+
+
+
+
+ {t.isCustom ? "Custom" : "Preset"}
+
+
+ e.stopPropagation()}>
+
+ handleCopyJson(t)}
+ >
+
+
+ {t.isCustom && (
+ removeCustomTheme(t.id)}
+ >
+
+
+ )}
+
+
+
+ );
+ })}
+
+
+ );
+ };
+
+ return (
+
+ {isAdding && (
+
+
+
+
+
+ Theme JSON Payload
+
+
+
+ Live Preview
+
+
+
+ Standard Schema
+
+
+
+
+
+ )}
+
+
+
+
+
+ All ({activeThemes.length})
+
+
+ Custom ({customThemes.length})
+
+
+
+
setIsAdding(!isAdding)}
+ className="h-8 px-4 text-[11px] font-bold bg-[var(--accent-primary)] text-[var(--accent-contrast)] hover:opacity-90 rounded-md transition-all flex gap-2"
+ >
+ {" "}
+ {isAdding ? "Cancel" : "Import Theme"}
+
+
+
+ {renderContent()}
+ {renderContent()}
+
+
+ );
+}
diff --git a/src/features/cortex-library/components/WorkspacesTab.tsx b/src/features/cortex-library/components/WorkspacesTab.tsx
index e705f54..094f801 100644
--- a/src/features/cortex-library/components/WorkspacesTab.tsx
+++ b/src/features/cortex-library/components/WorkspacesTab.tsx
@@ -1,10 +1,29 @@
import { useMemo } from "react";
-import { Clock, ExternalLink, Folder, Trash2, Rocket, Plus, Archive, RotateCcw } from "@/components/ui/icons";
+import {
+ Clock,
+ ExternalLink,
+ Folder,
+ Trash2,
+ Rocket,
+ Plus,
+ Archive,
+ RotateCcw,
+} from "@/components/ui/icons";
import { Button } from "@/components/ui/button";
-import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
-import { Card, CardHeader, CardTitle, CardFooter, CardContent } from "@/components/ui/card";
+import {
+ Tooltip,
+ TooltipContent,
+ TooltipTrigger,
+} from "@/components/ui/tooltip";
+import {
+ Card,
+ CardHeader,
+ CardTitle,
+ CardFooter,
+ CardContent,
+} from "@/components/ui/card";
import { LayoutPreviewIcon } from "@/components/ui/layout-preview-icon";
-import { SpaceTemplate } from "@/types";
+import { SpaceTemplate } from "@/lib";
import { EmptyState } from "@/components/ui/empty-state";
import { truncatePath } from "@/lib/utils";
import { ViewMode } from "@/components/ui/view-toggle";
@@ -60,33 +79,41 @@ export function WorkspacesTab({
onRestore,
onCapture,
activeSubTab,
- onSubTabChange
+ onSubTabChange,
}: WorkspacesTabProps) {
// State lifted to CortexLibraryDialog to persist across sidebar navigation
- const activeTemplates = useMemo(() =>
- templates.filter(t => !t.isArchived),
- [templates]);
+ const activeTemplates = useMemo(
+ () => templates.filter((t) => !t.isArchived),
+ [templates],
+ );
- const archivedTemplates = useMemo(() =>
- templates.filter(t => t.isArchived),
- [templates]);
+ const archivedTemplates = useMemo(
+ () => templates.filter((t) => t.isArchived),
+ [templates],
+ );
- const filtered = useMemo(() =>
- activeTemplates.filter(t =>
- t.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
- t.description?.toLowerCase().includes(searchQuery.toLowerCase()) ||
- t.rootPath.toLowerCase().includes(searchQuery.toLowerCase())
- ),
- [activeTemplates, searchQuery]);
+ const filtered = useMemo(
+ () =>
+ activeTemplates.filter(
+ (t) =>
+ t.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
+ t.description?.toLowerCase().includes(searchQuery.toLowerCase()) ||
+ t.rootPath.toLowerCase().includes(searchQuery.toLowerCase()),
+ ),
+ [activeTemplates, searchQuery],
+ );
- const archivedFiltered = useMemo(() =>
- archivedTemplates.filter(t =>
- t.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
- t.description?.toLowerCase().includes(searchQuery.toLowerCase()) ||
- t.rootPath.toLowerCase().includes(searchQuery.toLowerCase())
- ),
- [archivedTemplates, searchQuery]);
+ const archivedFiltered = useMemo(
+ () =>
+ archivedTemplates.filter(
+ (t) =>
+ t.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
+ t.description?.toLowerCase().includes(searchQuery.toLowerCase()) ||
+ t.rootPath.toLowerCase().includes(searchQuery.toLowerCase()),
+ ),
+ [archivedTemplates, searchQuery],
+ );
// Active view: no delete button
const renderActiveContent = () => {
@@ -94,10 +121,13 @@ export function WorkspacesTab({
return (
{filtered.map((template) => (
@@ -127,9 +157,15 @@ export function WorkspacesTab({
- Name
- Path
- Created
+
+ Name
+
+
+ Path
+
+
+ Created
+
@@ -147,7 +183,7 @@ export function WorkspacesTab({
"w-4 h-4 rounded border transition-all flex items-center justify-center cursor-pointer",
selectedIds.has(template.id)
? "border-[var(--accent-primary)] bg-[var(--accent-primary)]"
- : "border-[var(--border-color)] hover:border-[var(--accent-primary)]/50"
+ : "border-[var(--border-color)] hover:border-[var(--accent-primary)]/50",
)}
>
{selectedIds.has(template.id) && (
@@ -174,20 +210,28 @@ export function WorkspacesTab({
-
+
{truncatePath(template.rootPath, 25)}
-
+
{formatTimeAgo(template.createdAt)}
- e.stopPropagation()}>
+ e.stopPropagation()}
+ >
{onArchive && (
- onArchive(template.id)}>
+ onArchive(template.id)}
+ >
@@ -216,9 +260,10 @@ export function WorkspacesTab({
- {viewMode === 'card' ? (
+ {viewMode === "card" ? (
{archivedFiltered.map((template) => (
- Name
- Path
- Created
- Actions
+
+ Name
+
+
+ Path
+
+
+ Created
+
+
+ Actions
+
{archivedFiltered.map((template) => (
-
+
toggleArchivedSelection(template.id)}
@@ -269,7 +325,7 @@ export function WorkspacesTab({
"w-4 h-4 rounded border transition-all flex items-center justify-center cursor-pointer",
archivedSelectedIds.has(template.id)
? "border-[var(--accent-primary)] bg-[var(--accent-primary)]"
- : "border-[var(--border-color)] hover:border-[var(--accent-primary)]/50"
+ : "border-[var(--border-color)] hover:border-[var(--accent-primary)]/50",
)}
>
{archivedSelectedIds.has(template.id) && (
@@ -291,16 +347,25 @@ export function WorkspacesTab({
- {truncatePath(template.rootPath, 25)}
+
+ {truncatePath(template.rootPath, 25)}
+
- {formatTimeAgo(template.createdAt)}
+
+ {formatTimeAgo(template.createdAt)}
+
- onRestore && onRestore(template.id)}>
+ onRestore && onRestore(template.id)}
+ >
@@ -308,7 +373,12 @@ export function WorkspacesTab({
- onDelete(template.id)}>
+ onDelete(template.id)}
+ >
@@ -327,13 +397,23 @@ export function WorkspacesTab({
return (
-
+
-
+
Active ({activeTemplates.length})
-
+
Archived ({archivedTemplates.length})
@@ -348,19 +428,23 @@ export function WorkspacesTab({
)}
-
- {renderActiveContent()}
-
+ {renderActiveContent()}
-
- {renderArchivedContent()}
-
+ {renderArchivedContent()}
);
}
-function TemplateCard({ template, isSelected, onToggleSelection, onLaunch, onArchive, onRestore, onDelete }: {
+function TemplateCard({
+ template,
+ isSelected,
+ onToggleSelection,
+ onLaunch,
+ onArchive,
+ onRestore,
+ onDelete,
+}: {
template: SpaceTemplate;
isSelected: boolean;
onToggleSelection: () => void;
@@ -373,31 +457,45 @@ function TemplateCard({ template, isSelected, onToggleSelection, onLaunch, onArc
{ if (onLaunch) onLaunch(); }}
+ onClick={() => {
+ if (onLaunch) onLaunch();
+ }}
>
-
+
{ e.stopPropagation(); onToggleSelection(); }}
+ onClick={(e) => {
+ e.stopPropagation();
+ onToggleSelection();
+ }}
className={cn(
"w-4 h-4 rounded border transition-all flex items-center justify-center cursor-pointer shrink-0",
- isSelected ? "border-[var(--accent-primary)] bg-[var(--accent-primary)]" : "border-[var(--border-color)] hover:border-[var(--accent-primary)]/50"
+ isSelected
+ ? "border-[var(--accent-primary)] bg-[var(--accent-primary)]"
+ : "border-[var(--border-color)] hover:border-[var(--accent-primary)]/50",
)}
>
- {isSelected &&
}
+ {isSelected && (
+
+ )}
{template.name}
-
+
- {truncatePath(template.rootPath, 35)}
+
+ {truncatePath(template.rootPath, 35)}
+
@@ -416,55 +514,77 @@ function TemplateCard({ template, isSelected, onToggleSelection, onLaunch, onArc
- {formatTimeAgo(template.createdAt)}
-
- {onArchive && (
-
-
- { e.stopPropagation(); onArchive(); }}
- >
-
-
-
- Archive
-
- )}
- {onRestore && (
-
-
- { e.stopPropagation(); onRestore(); }}
- >
-
-
-
- Restore
-
- )}
- {onDelete && (
-
-
- { e.stopPropagation(); onDelete(); }}
- >
-
-
-
- Delete Permanently
-
- )}
- {onLaunch &&
}
-
+
+ {formatTimeAgo(template.createdAt)}
+
+
+ {onArchive && (
+
+
+ {
+ e.stopPropagation();
+ onArchive();
+ }}
+ >
+
+
+
+
+ Archive
+
+
+ )}
+ {onRestore && (
+
+
+ {
+ e.stopPropagation();
+ onRestore();
+ }}
+ >
+
+
+
+
+ Restore
+
+
+ )}
+ {onDelete && (
+
+
+ {
+ e.stopPropagation();
+ onDelete();
+ }}
+ >
+
+
+
+
+ Delete Permanently
+
+
+ )}
+ {onLaunch && (
+
+ )}
+
);
diff --git a/src/features/settings/SettingsDialog.tsx b/src/features/settings/SettingsDialog.tsx
index ba760f2..a5ed679 100644
--- a/src/features/settings/SettingsDialog.tsx
+++ b/src/features/settings/SettingsDialog.tsx
@@ -11,10 +11,8 @@ import {
Settings2,
Keyboard,
Terminal,
- Palette,
FlaskConical,
- Info,
- Cpu
+ Info
} from "@/components/ui/icons";
import { open } from "@tauri-apps/plugin-dialog";
@@ -34,7 +32,7 @@ import {
import { useTerminalSettings } from "@/hooks/useTerminalSettings";
import { ThemeName, ThemeDefinition } from "@/hooks/useTheme";
-import { useColorScheme } from "@/hooks/useColorScheme";
+
import { toast } from "sonner";
@@ -42,8 +40,7 @@ import { toast } from "sonner";
import { GeneralTab } from "./components/tabs/GeneralTab";
import { ShortcutsTab } from "./components/tabs/ShortcutsTab";
import { TerminalTab } from "./components/tabs/TerminalTab";
-import { ThemesTab } from "./components/tabs/ThemesTab";
-import { AgentsTab } from "./components/tabs/AgentsTab";
+
import { DemoTab } from "./components/tabs/DemoTab";
import { AboutTab } from "./components/tabs/AboutTab";
@@ -57,10 +54,6 @@ interface SettingsDialogProps {
theme: ThemeName;
setTheme: (theme: ThemeName) => void;
allThemes: ThemeDefinition[];
- addCustomTheme: (theme: ThemeDefinition) => Promise
;
- removeCustomTheme: (id: string) => Promise;
- previewTheme: (config: ThemeDefinition) => void;
- cancelPreview: () => void;
colorScheme: ColorScheme;
setColorScheme: (scheme: ColorScheme) => void;
uiFontScale: number;
@@ -69,6 +62,10 @@ interface SettingsDialogProps {
setZenPadding: (padding: number) => void;
reducedMotion: boolean;
setReducedMotion: (reduced: boolean) => void;
+ shimmerPreset: string;
+ setShimmerPreset: (preset: string) => void;
+ shimmerDuration: number;
+ setShimmerDuration: (v: number) => void;
onResetAppearance: () => void;
focusSettings: FocusSettings;
setFocusSetting: (key: K, value: FocusSettings[K]) => Promise;
@@ -85,10 +82,6 @@ export function SettingsDialog({
theme,
setTheme,
allThemes,
- addCustomTheme,
- removeCustomTheme,
- previewTheme,
- cancelPreview,
colorScheme,
setColorScheme,
uiFontScale,
@@ -97,6 +90,10 @@ export function SettingsDialog({
setZenPadding,
reducedMotion,
setReducedMotion,
+ shimmerPreset,
+ setShimmerPreset,
+ shimmerDuration,
+ setShimmerDuration,
onResetAppearance,
focusSettings,
setFocusSetting,
@@ -105,7 +102,7 @@ export function SettingsDialog({
setDemoSetting,
resetDemo,
}: SettingsDialogProps) {
- const { resolvedScheme } = useColorScheme();
+
const [defaultPath, setDefaultPath] = useState("");
const [activeTab, setActiveTab] = useState(initialTab);
@@ -118,7 +115,7 @@ export function SettingsDialog({
// Startup settings (local state, persisted on change)
const [showSplash, setShowSplash] = useState(true);
- const [startupBehavior, setStartupBehavior] = useState("modeSelector");
+ const [startupBehavior, setStartupBehavior] = useState("lastMode");
const [checkUpdates, setCheckUpdates] = useState(true);
const [confirmModeChange, setConfirmModeChange] = useState(true);
const [defaultShell, setDefaultShell] = useState("");
@@ -130,8 +127,6 @@ export function SettingsDialog({
settings: ts,
isLoaded,
updateSetting,
- updateSettingLive,
- commitSettings,
resetToDefaults: resetTerminal,
} = useTerminalSettings();
@@ -154,7 +149,7 @@ export function SettingsDialog({
] = await Promise.all([
getSetting("cortex_default_path", ""),
getSetting("startup.showSplashAnimation", true),
- getSetting("startup.behavior", "modeSelector"),
+ getSetting("startup.behavior", "lastMode"),
getSetting("startup.checkForUpdatesOnStartup", true),
getSetting("startup.confirmModeChange", true),
getSetting("startup.defaultShell", ""),
@@ -203,13 +198,13 @@ export function SettingsDialog({
const handleResetStartup = async () => {
setShowSplash(true);
- setStartupBehavior("modeSelector");
+ setStartupBehavior("lastMode");
setCheckUpdates(true);
setConfirmModeChange(true);
setDefaultShell("");
await Promise.all([
setSetting("startup.showSplashAnimation", true),
- setSetting("startup.behavior", "modeSelector"),
+ setSetting("startup.behavior", "lastMode"),
setSetting("startup.checkForUpdatesOnStartup", true),
setSetting("startup.confirmModeChange", true),
setSetting("startup.defaultShell", ""),
@@ -278,7 +273,7 @@ export function SettingsDialog({
onValueChange={setActiveTab}
className="w-full flex-1 flex flex-col overflow-hidden mt-4"
>
-
+
General
@@ -288,12 +283,6 @@ export function SettingsDialog({
Terminal
-
- Themes
-
-
- Agents
-
{import.meta.env.DEV && (
Demo
@@ -314,6 +303,10 @@ export function SettingsDialog({
setZenPadding={setZenPadding}
reducedMotion={reducedMotion}
setReducedMotion={setReducedMotion}
+ shimmerPreset={shimmerPreset}
+ setShimmerPreset={setShimmerPreset}
+ shimmerDuration={shimmerDuration}
+ setShimmerDuration={setShimmerDuration}
onResetAppearance={onResetAppearance}
showSplash={showSplash}
setShowSplash={(v) => handleStartupToggle("startup.showSplashAnimation", setShowSplash, v)}
@@ -326,21 +319,15 @@ export function SettingsDialog({
setCheckUpdates={(v) => handleStartupToggle("startup.checkForUpdatesOnStartup", setCheckUpdates, v)}
confirmModeChange={confirmModeChange}
setConfirmModeChange={(v) => handleStartupToggle("startup.confirmModeChange", setConfirmModeChange, v)}
- defaultShell={defaultShell}
- setDefaultShell={async (v) => {
- setDefaultShell(v);
- await setSetting("startup.defaultShell", v);
- window.dispatchEvent(new CustomEvent('cortex-settings-changed', {
- detail: { startup: { defaultShell: v } }
- }));
- }}
- defaultPath={defaultPath}
- onSetPath={handleSetPath}
+
onResetStartup={handleResetStartup}
focusSettings={focusSettings}
setFocusSetting={setFocusSetting}
onResetFocus={resetFocus}
onFactoryReset={handleFactoryReset}
+ theme={theme}
+ setTheme={setTheme}
+ allThemes={allThemes}
/>
@@ -358,29 +345,22 @@ export function SettingsDialog({
demo={demoSettings}
isLoaded={isLoaded}
updateSetting={updateSetting}
- updateSettingLive={updateSettingLive}
- commitSettings={commitSettings}
onResetTerminal={resetTerminal}
setDemoSetting={setDemoSetting}
+ defaultShell={defaultShell}
+ setDefaultShell={async (v) => {
+ setDefaultShell(v);
+ await setSetting("startup.defaultShell", v);
+ window.dispatchEvent(new CustomEvent('cortex-settings-changed', {
+ detail: { startup: { defaultShell: v } }
+ }));
+ }}
+ defaultPath={defaultPath}
+ onSetPath={handleSetPath}
/>
-
-
-
-
-
-
{import.meta.env.DEV && (
diff --git a/src/features/settings/components/tabs/AboutTab.tsx b/src/features/settings/components/tabs/AboutTab.tsx
index 87e1e11..5112d46 100644
--- a/src/features/settings/components/tabs/AboutTab.tsx
+++ b/src/features/settings/components/tabs/AboutTab.tsx
@@ -221,7 +221,7 @@ export function AboutTab() {
Cortex Space
-
+
Release v0.1.0-alpha
@@ -313,7 +313,7 @@ export function AboutTab() {
Update Available
-
+
{updateVersion}
@@ -321,7 +321,7 @@ export function AboutTab() {
{updateBody && (
Release Highlights
-
{updateBody}
+
{updateBody}
)}
@@ -351,7 +351,7 @@ export function AboutTab() {
{status === "downloading" ? "Downloading Update..." : "Installing Update..."}
- {progress}%
+ {progress}%
@@ -383,7 +383,7 @@ export function AboutTab() {
{errorMsg && (
-
{errorMsg}
+
{errorMsg}
)}
diff --git a/src/features/settings/components/tabs/AgentsTab.tsx b/src/features/settings/components/tabs/AgentsTab.tsx
deleted file mode 100644
index 0f14079..0000000
--- a/src/features/settings/components/tabs/AgentsTab.tsx
+++ /dev/null
@@ -1,410 +0,0 @@
-import { Cpu, Download, CheckCircle2, AlertCircle, Loader2, Plus, Trash2, ChevronDown } from "@/components/ui/icons";
-import { motion, AnimatePresence, Variants } from "framer-motion";
-import { useAgents } from "@/hooks/useAgents";
-import { Button } from "@/components/ui/button";
-import { useState } from "react";
-import { Input } from "@/components/ui/input";
-import { SettingsCard } from "../shared/SettingsUI";
-import { Card, CardContent } from "@/components/ui/card";
-import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
-import { cn, toTitleCase } from "@/lib/utils";
-
-function GeminiLogo() {
- return (
-
-
-
- );
-}
-
-function ClaudeLogo() {
- return (
-
-
-
-
- );
-}
-
-function CodexLogo() {
- return (
-
-
-
-
- );
-}
-
-function OpenCodeLogo() {
- return (
-
-
-
-
-
- );
-}
-
-function AntigravityLogo() {
- return (
-
-
-
-
-
- );
-}
-
-function FallbackLogo() {
- return (
-
-
-
-
-
- );
-}
-
-const AGENT_META: Record
= {
- 'agent-gemini': {
- logo: ,
- accent: '#8B5CF6',
- description: "Google's AI-powered command-line assistant for natural language interactions",
- },
- 'agent-claude': {
- logo: ,
- accent: '#F59E0B',
- description: "Anthropic's conversational AI agent designed for terminal workflows",
- },
- 'agent-codex': {
- logo: ,
- accent: '#10B981',
- description: "OpenAI's coding agent that translates natural language into terminal commands",
- },
- 'agent-opencode': {
- logo: ,
- accent: '#3B82F6',
- description: "Cortex's native AI agent for code generation and terminal assistance",
- },
- 'agent-antigravity': {
- logo: ,
- accent: '#EC4899',
- description: "Zero-configuration deployment agent for launching projects from the terminal",
- },
-};
-
-export function AgentsTab() {
- const { agents, installAgent, addAgent, deleteAgent } = useAgents();
- const [showAddForm, setShowAddForm] = useState(false);
- const [newLabel, setNewLabel] = useState("");
- const [newCommand, setNewCommand] = useState("");
- const [newInstallCommand, setNewInstallCommand] = useState("");
- const [newDownloadUrl, setNewDownloadUrl] = useState("");
- const [expandedErrors, setExpandedErrors] = useState>(new Set());
-
- const toggleError = (id: string) => {
- setExpandedErrors(prev => {
- const next = new Set(prev);
- if (next.has(id)) next.delete(id); else next.add(id);
- return next;
- });
- };
-
- const handleAddSubmit = (e: React.FormEvent) => {
- e.preventDefault();
- if (newCommand.trim()) {
- addAgent(newLabel, newCommand, newInstallCommand, newDownloadUrl);
- setNewLabel("");
- setNewCommand("");
- setNewInstallCommand("");
- setNewDownloadUrl("");
- setShowAddForm(false);
- }
- };
-
- const handleCancel = () => {
- setNewLabel("");
- setNewCommand("");
- setNewInstallCommand("");
- setNewDownloadUrl("");
- setShowAddForm(false);
- };
-
- const getAgentMeta = (agentId: string) => {
- const meta = AGENT_META[agentId];
- if (meta) return meta;
- const agent = agents.find(a => a.id === agentId);
- return {
- logo: ,
- accent: '#6B7280',
- description: `Custom agent for the "${agent?.command ?? 'unknown'}" terminal command`,
- };
- };
-
- const containerVariants: Variants = {
- hidden: { opacity: 0 },
- show: {
- opacity: 1,
- transition: {
- staggerChildren: 0.1
- }
- }
- };
-
- const itemVariants: Variants = {
- hidden: { opacity: 0, y: 15 },
- show: { opacity: 1, y: 0, transition: { duration: 0.4, ease: [0.22, 1, 0.36, 1] } }
- };
-
- return (
-
-
- }
- description="Configure and maintain your AI agent library. Agents can be used in any terminal pane."
- >
-
- {agents.map((agent) => {
- const meta = getAgentMeta(agent.id);
- return (
-
-
-
-
-
-
-
- {toTitleCase(agent.label)}
-
- {agent.isDefault ? (
-
- Default
-
- ) : (
-
- Custom
-
- )}
-
-
- {meta.description}
-
-
- {agent.command}
-
-
-
-
-
- {agent.status === 'installed' && (
-
-
- Active
-
- )}
- {agent.status === 'installing' && (
-
-
- Syncing
-
- )}
- {agent.status === 'error' && (
-
- )}
- {agent.status === 'not-installed' && (
-
- )}
-
-
-
- {(agent.status === 'not-installed' || agent.status === 'error') && agent.installCommand && (
- {
- if (expandedErrors.has(agent.id)) toggleError(agent.id);
- installAgent(agent.id);
- }}
- >
-
-
- )}
-
- {agent.status === 'error' && agent.errorMessage && (
- toggleError(agent.id)}
- className={cn(
- "w-7 h-7 rounded-md transition-all",
- expandedErrors.has(agent.id)
- ? "text-red-400 bg-red-500/10"
- : "text-[var(--text-secondary)]/40 hover:text-red-400 hover:bg-red-500/10"
- )}
- >
-
-
- )}
-
- {!agent.isDefault && (
-
-
- deleteAgent(agent.id)}
- >
-
-
-
-
- Remove Agent
-
-
- )}
-
-
-
-
-
- {agent.status === 'error' && expandedErrors.has(agent.id) && agent.errorMessage && (
-
-
-
Installation Error Output
-
{agent.errorMessage}
-
-
- )}
-
-
- );
- })}
-
-
- {!showAddForm ? (
- setShowAddForm(true)}
- >
-
-
- Register Custom Agent
-
-
- ) : (
-
-
-
-
-
New Custom Agent
-
Register a custom CLI agent for your workflow
-
-
-
-
-
-
-
- Installation Command (Optional)
-
- setNewInstallCommand(e.target.value)}
- className="h-8 text-[10px] font-mono bg-[var(--bg-color)]/50 border-[var(--border-color)]/20 focus:border-[var(--accent-primary)]/40"
- />
-
-
-
-
- Cancel
-
-
- Register Agent
-
-
-
- )}
-
-
-
-
-
- );
-}
diff --git a/src/features/settings/components/tabs/GeneralTab.tsx b/src/features/settings/components/tabs/GeneralTab.tsx
index 8b334e3..1699333 100644
--- a/src/features/settings/components/tabs/GeneralTab.tsx
+++ b/src/features/settings/components/tabs/GeneralTab.tsx
@@ -1,19 +1,15 @@
import { useState, useEffect } from "react";
import {
SettingsCard,
- SettingsRow,
- SegmentedControl
+ SettingsRow
} from "../shared/SettingsUI";
import { Slider } from "@/components/ui/slider";
import { Switch } from "@/components/ui/switch";
-import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import {
- FolderOpen,
Palette,
Rocket,
Target,
- Monitor,
AlertTriangle
} from "@/components/ui/icons";
import {
@@ -26,7 +22,281 @@ import {
import { ColorScheme, StartupBehavior, FocusSettings } from "@/lib/store";
import { motion, Variants } from "framer-motion";
import { ConfirmActionDialog } from "@/components/dialogs/ConfirmActionDialog";
-import { invoke } from "@tauri-apps/api/core";
+import { Label } from "@/components/ui/label";
+
+function DashboardMockup({ theme }: { theme: "light" | "dark" }) {
+ const isLight = theme === "light";
+ return (
+
+ {/* Top window controls */}
+
+
+ {/* Window Body */}
+
+ {/* Sidebar */}
+
+ {/* Logo / Profile */}
+
+ {/* Menu items */}
+
+
+
+ {/* Main Content Area */}
+
+ {/* Main Content Header */}
+
+
+ Your dashboard
+
+ {/* Top-right pills */}
+
+
+
+ {/* Subtitle line */}
+
+
+ {/* Main Card */}
+
+
+
+
+ );
+}
+
+interface ThemeCardProps {
+ value: ColorScheme;
+ label: string;
+ selected: boolean;
+ disabled?: boolean;
+ onClick: () => void;
+}
+
+function ThemeCard({ value, label, selected, disabled, onClick }: ThemeCardProps) {
+ const handleClick = () => {
+ if (!disabled) {
+ onClick();
+ }
+ };
+
+ return (
+
+ {/* Window Mockup Card */}
+
+ {value === "system" ? (
+
+ {/* Base Light theme */}
+
+ {/* Split Dark theme on the right */}
+
+
+ ) : (
+
+ )}
+
+
+ {/* Radio Label */}
+
+
+ {selected && (
+
+
+
+ )}
+
+
+ {label}
+
+ {disabled && (
+
+ Dark only
+
+ )}
+
+
+ );
+}
+
+function LayoutMockup({ type }: { type: "grid" | "count" }) {
+ const isGrid = type === "grid";
+ return (
+
+ {/* Top window controls */}
+
+
+ {/* Window Body */}
+
+ {isGrid ? (
+ /* Grid Mode: 2x2 symmetrical grid of panels */
+
+ ) : (
+ /* Flex Mode: Asymmetrical splits (1 wide left, 2 vertical stacked right) */
+
+ {/* Left wide pane */}
+
+ {/* Right column with two vertical splits */}
+
+
+ )}
+
+
+ );
+}
+
+interface LayoutModeCardProps {
+ value: "grid" | "count";
+ label: string;
+ selected: boolean;
+ onClick: () => void;
+}
+
+function LayoutModeCard({ value, label, selected, onClick }: LayoutModeCardProps) {
+ return (
+
+ {/* Window Mockup Card */}
+
+
+
+
+ {/* Radio Label */}
+
+
+ {selected && (
+
+
+
+ )}
+
+
+ {label}
+
+
+
+ );
+}
interface GeneralTabProps {
// Appearance
@@ -38,7 +308,14 @@ interface GeneralTabProps {
setZenPadding: (padding: number) => void;
reducedMotion: boolean;
setReducedMotion: (reduced: boolean) => void;
+ shimmerPreset: string;
+ setShimmerPreset: (preset: string) => void;
+ shimmerDuration: number;
+ setShimmerDuration: (v: number) => void;
onResetAppearance: () => void;
+ theme: string;
+ setTheme: (theme: any) => void;
+ allThemes: any[];
// Startup
showSplash: boolean;
@@ -56,11 +333,6 @@ interface GeneralTabProps {
setFocusSetting: (key: K, value: FocusSettings[K]) => Promise;
onResetFocus: () => Promise;
- // Environment
- defaultShell: string;
- setDefaultShell: (v: string) => void;
- defaultPath: string;
- onSetPath: () => void;
onFactoryReset: () => Promise;
}
@@ -73,7 +345,14 @@ export function GeneralTab({
setZenPadding,
reducedMotion,
setReducedMotion,
+ shimmerPreset,
+ setShimmerPreset,
+ shimmerDuration,
+ setShimmerDuration,
onResetAppearance,
+ theme,
+ setTheme,
+ allThemes,
showSplash,
setShowSplash,
startupBehavior,
@@ -86,23 +365,22 @@ export function GeneralTab({
focusSettings,
setFocusSetting,
onResetFocus,
- defaultShell,
- setDefaultShell,
- defaultPath,
- onSetPath,
onFactoryReset,
}: GeneralTabProps) {
const [isAppearanceResetOpen, setIsAppearanceResetOpen] = useState(false);
const [isStartupResetOpen, setIsStartupResetOpen] = useState(false);
const [isFocusResetOpen, setIsFocusResetOpen] = useState(false);
const [isFactoryResetOpen, setIsFactoryResetOpen] = useState(false);
- const [systemShell, setSystemShell] = useState("detecting...");
+ const activeThemeDef = allThemes?.find((t) => t.id === theme);
+ const hasLightMode = activeThemeDef ? (!!activeThemeDef.light || !!activeThemeDef.isLegacy) : true;
+
+ // Auto-fallback from Light scheme to Dark scheme if the active theme only supports Dark mode
useEffect(() => {
- invoke("get_default_shell")
- .then(setSystemShell)
- .catch(() => setSystemShell("unknown"));
- }, []);
+ if (!hasLightMode && colorScheme === "light") {
+ setColorScheme("dark");
+ }
+ }, [theme, hasLightMode, colorScheme, setColorScheme]);
const containerVariants: Variants = {
hidden: { opacity: 0 },
@@ -182,18 +460,19 @@ export function GeneralTab({
setStartupBehavior(v as StartupBehavior)}
+ size="sm"
>
- Mode Selector
- Resume Last Session
- Always Terminal
- Always AI Assisted
+ Mode Selector
+ Resume Last Session
+ Always AI Assisted
+ Always Terminal
@@ -241,19 +520,118 @@ export function GeneralTab({
description="Visual fidelity and scaling options."
onReset={() => setIsAppearanceResetOpen(true)}
>
+
+
+
+ Color Scheme
+
+
+ System sync or forced Light/Dark mode.
+
+
+
+
+ setColorScheme("system")}
+ />
+ setColorScheme("light")}
+ />
+ setColorScheme("dark")}
+ />
+
+
-
- value={colorScheme}
- onChange={setColorScheme}
- options={[
- { value: "system", label: "System" },
- { value: "dark", label: "Dark" },
- { value: "light", label: "Light" },
- ]}
- />
+ setTheme(v)}
+ size="sm"
+ >
+
+
+
+
+ {allThemes.map((t) => (
+
+ {t.name}
+
+ ))}
+
+
+
+
+ setShimmerPreset(v)}
+ size="sm"
+ >
+
+
+
+
+ Tonic
+ Sunrise
+ Bubble
+ Sunset
+ Peach
+ Mint
+ Spring
+ Twilight
+ Bay
+
+
+
+
+
+ setShimmerDuration(v)}
+ className="flex-1"
+ />
+
+ {(shimmerDuration || 1.45).toFixed(2)}s
+
+
setUiFontScale(v)}
className="flex-1"
/>
-
+
{uiFontScale}%
@@ -286,19 +664,34 @@ export function GeneralTab({
onCheckedChange={setReducedMotion}
/>
-
-
- value={(focusSettings.customLayoutMode as "grid" | "count") || "grid"}
- onChange={(v) => setFocusSetting("customLayoutMode", v)}
- options={[
- { value: "grid", label: "Grid Mode" },
- { value: "count", label: "Flex Mode" },
- ]}
- />
-
+
+
+
+ Layout Customization Mode
+
+
+ Choose the interface mode when configuring custom layouts.
+
+
+
+
+ setFocusSetting("customLayoutMode", "grid")}
+ />
+ setFocusSetting("customLayoutMode", "count")}
+ />
+
+
@@ -313,22 +706,27 @@ export function GeneralTab({
-
- setZenPadding(v)}
- className="flex-1"
- />
-
- {zenPadding}px
-
-
+ setZenPadding(Number(v))}
+ size="sm"
+ >
+
+
+
+
+ None (0px)
+ Compact (16px)
+ Default (32px)
+ Relaxed (64px)
+ Wide (96px)
+
+
- {/* 4. Environment & Shell */}
-
- }
- description="System-level paths and shell configurations."
- >
-
-
- setDefaultShell(e.target.value)}
- className="w-[180px] h-8 text-[11px] font-mono bg-[var(--bg-color)]/50 border-[var(--border-color)]/20 text-right"
- />
- {!defaultShell && (
-
- Detected: {systemShell}
-
- )}
-
-
-
-
-
- Default Workspace Path
-
-
-
-
-
- Browse
-
-
-
-
-
+
{/* 5. Maintenance / Reset */}
diff --git a/src/features/settings/components/tabs/ShortcutsTab.tsx b/src/features/settings/components/tabs/ShortcutsTab.tsx
index 101bc49..8ddacf4 100644
--- a/src/features/settings/components/tabs/ShortcutsTab.tsx
+++ b/src/features/settings/components/tabs/ShortcutsTab.tsx
@@ -93,29 +93,12 @@ export function ShortcutsTab({
onConfirm={onResetShortcuts}
/>
-
-
-
-
- Keyboard Shortcuts
-
-
Reconfigure the shortcuts for your workspace.
-
-
setIsConfirmOpen(true)}
- className="h-8 px-3 text-[10px] font-bold text-[var(--accent-primary)] hover:bg-[var(--accent-primary)]/10 hover:text-[var(--accent-primary)] transition-all border border-[var(--accent-primary)]/20"
- >
- Reset All
-
-
-
}
description="Manage workspace lifecycles and navigation flow."
+ onReset={() => setIsConfirmOpen(true)}
>
@@ -185,7 +168,7 @@ function StaticShortcutItem({ label, value }: { label: string; value: string })
{label}
{parseShortcutToKeys(value, isMac).map((key, idx) => (
-
+
{key}
))}
@@ -308,7 +291,7 @@ function ShortcutItem({ label, shortcutKey, currentValue, defaultValue, onChange
onKeyDown={handleKeyDown}
onBlur={stopRecording}
className={cn(
- "min-w-[110px] h-7 px-2.5 rounded-md border text-[10px] font-mono flex items-center justify-end gap-2 transition-all outline-none",
+ "min-w-[110px] h-7 px-2.5 rounded-md border text-[10px] flex items-center justify-end gap-2 transition-all outline-none",
isRecording
? "border-[var(--accent-primary)] bg-[var(--accent-primary)]/10 shadow-[0_0_15px_rgba(var(--accent-primary-rgb),0.2)]"
: hasConflict
@@ -330,7 +313,7 @@ function ShortcutItem({ label, shortcutKey, currentValue, defaultValue, onChange
@@ -397,7 +380,7 @@ function ShortcutItem({ label, shortcutKey, currentValue, defaultValue, onChange
{parseShortcutToKeys(pendingValue, isMac).map((key, idx) => (
-
+
{key}
))}
diff --git a/src/features/settings/components/tabs/TerminalTab.tsx b/src/features/settings/components/tabs/TerminalTab.tsx
index cb3990b..ac33127 100644
--- a/src/features/settings/components/tabs/TerminalTab.tsx
+++ b/src/features/settings/components/tabs/TerminalTab.tsx
@@ -1,11 +1,12 @@
+import { useState, useEffect } from "react";
import {
SettingsCard,
SettingsRow,
SegmentedControl
} from "../shared/SettingsUI";
-import { Slider } from "@/components/ui/slider";
import { Switch } from "@/components/ui/switch";
import { Input } from "@/components/ui/input";
+import { Button } from "@/components/ui/button";
import {
Select,
SelectContent,
@@ -14,18 +15,128 @@ import {
SelectValue
} from "@/components/ui/select";
import { TerminalSettings, DemoSettings } from "@/lib/store";
-import { Layout, Type, MousePointer2, History } from "@/components/ui/icons";
+import { Layout, Type, MousePointer2, History, FolderOpen, Monitor } from "@/components/ui/icons";
import { motion, Variants } from "framer-motion";
+import { Label } from "@/components/ui/label";
+import { invoke } from "@tauri-apps/api/core";
+
+function HeaderMockup({ type }: { type: "hover" | "always" }) {
+ const isHover = type === "hover";
+ return (
+
+ {/* Top window controls */}
+
+
+ {/* Window Body */}
+
+ {/* Mock Pane Header */}
+ {isHover ? (
+ /* Hover mode: faint dotted outline header showing it reveals on hover */
+
+ ) : (
+ /* Always Mode: solid visible header bar */
+
+ )}
+
+ {/* Terminal Content Mock */}
+
+
+ {/* Mock cursor arrow hovering near the top right for Hover Mode */}
+ {isHover && (
+
+ )}
+
+
+ );
+}
+
+interface HeaderVisibilityCardProps {
+ value: "hover" | "always";
+ label: string;
+ selected: boolean;
+ onClick: () => void;
+}
+
+function HeaderVisibilityCard({ value, label, selected, onClick }: HeaderVisibilityCardProps) {
+ return (
+
+ {/* Window Mockup Card */}
+
+
+
+
+ {/* Radio Label */}
+
+
+ {selected && (
+
+
+
+ )}
+
+
+ {label}
+
+
+
+ );
+}
interface TerminalTabProps {
ts: TerminalSettings;
demo: DemoSettings;
isLoaded: boolean;
updateSetting: (key: keyof TerminalSettings, value: any) => void;
- updateSettingLive: (key: keyof TerminalSettings, value: any) => void;
- commitSettings: (values: Partial) => Promise;
onResetTerminal: () => void;
setDemoSetting: (key: K, value: DemoSettings[K]) => Promise;
+ defaultShell: string;
+ setDefaultShell: (v: string) => void;
+ defaultPath: string;
+ onSetPath: () => void;
}
const fontFamilies = [
@@ -50,11 +161,21 @@ export function TerminalTab({
demo,
isLoaded,
updateSetting,
- updateSettingLive,
- commitSettings,
onResetTerminal,
setDemoSetting,
+ defaultShell,
+ setDefaultShell,
+ defaultPath,
+ onSetPath,
}: TerminalTabProps) {
+ const [systemShell, setSystemShell] = useState("detecting...");
+
+ useEffect(() => {
+ invoke("get_default_shell")
+ .then(setSystemShell)
+ .catch(() => setSystemShell("unknown"));
+ }, []);
+
if (!isLoaded) return null;
const containerVariants: Variants = {
@@ -96,19 +217,34 @@ export function TerminalTab({
onCheckedChange={(v) => setDemoSetting("showFloatingTerminalHeader", v)}
/>
-
-
- value={demo.terminalHeaderVisibility || 'hover'}
- onChange={(v) => setDemoSetting("terminalHeaderVisibility", v)}
- options={[
- { value: "hover", label: "Reveal on Hover" },
- { value: "always", label: "Always Visible" },
- ]}
- />
-
+
+
+
+ Header Visibility
+
+
+ Choose if the header should be always visible or reveal on hover.
+
+
+
+
+ setDemoSetting("terminalHeaderVisibility", "hover")}
+ />
+ setDemoSetting("terminalHeaderVisibility", "always")}
+ />
+
+
@@ -132,10 +268,11 @@ export function TerminalTab({
return "JetBrains Mono";
})()}
onValueChange={(v) => updateSetting("fontFamily", v)}
+ size="sm"
>
@@ -144,7 +281,7 @@ export function TerminalTab({
{f.label}
@@ -155,76 +292,127 @@ export function TerminalTab({
-
- updateSettingLive("fontSize", v)}
- onValueCommit={([v]) => commitSettings({ fontSize: v })}
- className="flex-1"
- />
-
- {ts.fontSize}px
-
-
+ {
+ const num = parseInt(v, 10);
+ updateSetting("fontSize", num);
+ }}
+ size="sm"
+ >
+
+
+
+
+ {(() => {
+ const standardSizes = [10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 24, 26, 28, 30, 32];
+ const fontSizes = standardSizes.includes(ts.fontSize)
+ ? standardSizes
+ : [...standardSizes, ts.fontSize].sort((a, b) => a - b);
+ return fontSizes.map((size) => (
+
+ {size}px
+
+ ));
+ })()}
+
+
+
+
+
+ updateSetting("fontWeight", v)}
+ size="sm"
+ >
+
+
+
+
+ Light (300)
+ Regular (400)
+ Medium (500)
+ Semibold (600)
+ Bold (700)
+
+
-
-
- updateSettingLive("lineHeight", Math.round(v * 10) / 10)
- }
- onValueCommit={([v]) =>
- commitSettings({ lineHeight: Math.round(v * 10) / 10 })
- }
- className="flex-1"
- />
-
- {ts.lineHeight.toFixed(1)}
-
-
+ updateSetting("lineHeight", parseFloat(v))}
+ size="sm"
+ >
+
+
+
+
+ {(() => {
+ const standardLineHeights = [1.0, 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7, 1.8, 1.9, 2.0];
+ const lineHeights = standardLineHeights.includes(ts.lineHeight)
+ ? standardLineHeights
+ : [...standardLineHeights, ts.lineHeight].sort((a, b) => a - b);
+ return lineHeights.map((lh) => (
+
+ {lh.toFixed(1)}
+
+ ));
+ })()}
+
+
-
-
- updateSettingLive("letterSpacing", v)
- }
- onValueCommit={([v]) =>
- commitSettings({ letterSpacing: v })
- }
- className="flex-1"
- />
-
- {ts.letterSpacing}px
-
-
+ updateSetting("letterSpacing", parseFloat(v))}
+ size="sm"
+ >
+
+
+
+
+ {(() => {
+ const standardSpacings = [-1.0, -0.5, 0.0, 0.5, 1.0, 1.5, 2.0, 2.5, 3.0, 4.0, 5.0];
+ const letterSpacings = standardSpacings.includes(ts.letterSpacing)
+ ? standardSpacings
+ : [...standardSpacings, ts.letterSpacing].sort((a, b) => a - b);
+ return letterSpacings.map((ls) => (
+
+ {ls >= 0 ? `+${ls.toFixed(1)}` : `${ls.toFixed(1)}`}px
+
+ ));
+ })()}
+
+
@@ -288,11 +476,74 @@ export function TerminalTab({
);
updateSetting("scrollbackLines", v);
}}
- className="w-[110px] h-8 text-[11px] font-mono text-right bg-[var(--bg-color)]/50 border-[var(--border-color)]/20"
+ className="w-[110px] h-8 text-[11px] text-right bg-[var(--bg-color)]/50 border-[var(--border-color)]/20"
/>
+
+
+ }
+ description="System-level paths and shell configurations."
+ >
+
+
+ {
+ if (v === "auto") {
+ setDefaultShell("");
+ } else {
+ setDefaultShell(v);
+ }
+ }}
+ size="sm"
+ >
+
+
+
+
+ Auto ({systemShell})
+ PowerShell
+ Windows PowerShell
+ Command Prompt
+ Git Bash
+
+
+
+
+
+
+
+ Default Workspace Path
+
+
+
+
+
+ Browse
+
+
+
+
+
);
}
diff --git a/src/features/settings/components/tabs/ThemesTab.tsx b/src/features/settings/components/tabs/ThemesTab.tsx
deleted file mode 100644
index 6cc327a..0000000
--- a/src/features/settings/components/tabs/ThemesTab.tsx
+++ /dev/null
@@ -1,349 +0,0 @@
-import * as React from "react";
-import { SettingsCard } from "../shared/SettingsUI";
-import { Card, CardContent } from "@/components/ui/card";
-import { Button } from "@/components/ui/button";
-import { Badge } from "@/components/ui/badge";
-import { Textarea } from "@/components/ui/textarea";
-import { Switch } from "@/components/ui/switch";
-import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
-import { Copy, Trash2, Check, Plus, Code, Palette } from "@/components/ui/icons";
-import { ThemeName, ThemeDefinition } from "@/hooks/useTheme";
-import { toast } from "sonner";
-import { motion, Variants } from "framer-motion";
-
-interface ThemesTabProps {
- theme: ThemeName;
- allThemes: ThemeDefinition[];
- resolvedScheme: 'light' | 'dark';
- setTheme: (theme: ThemeName) => void;
- addCustomTheme: (theme: ThemeDefinition) => Promise;
- removeCustomTheme: (id: string) => Promise;
- previewTheme: (config: ThemeDefinition) => void;
- cancelPreview: () => void;
-}
-
-function normalizeThemeInput(parsed: any): ThemeDefinition {
- if (!parsed || typeof parsed !== 'object') {
- throw new Error("Invalid theme object");
- }
- if (!parsed.id || typeof parsed.id !== 'string') {
- throw new Error("Missing or invalid 'id' property.");
- }
- if (!parsed.name || typeof parsed.name !== 'string') {
- throw new Error("Missing or invalid 'name' property.");
- }
-
- if (parsed.dark && typeof parsed.dark === 'object') {
- const requiredPalette = ['bg', 'headerBg', 'footerBg', 'surface', 'border', 'textPrimary', 'textSecondary', 'accent'];
- for (const key of requiredPalette) {
- if (!parsed.dark[key]) {
- throw new Error(`Missing required theme palette property under dark: ${key}`);
- }
- }
- return {
- id: parsed.id,
- name: parsed.name,
- dark: parsed.dark,
- light: parsed.light,
- isCustom: true
- };
- } else {
- const required = ['bg', 'headerBg', 'footerBg', 'surface', 'border', 'textPrimary', 'textSecondary', 'accent'];
- for (const key of required) {
- if (!parsed[key]) {
- throw new Error(`Missing required theme property: ${key}`);
- }
- }
- return {
- id: parsed.id,
- name: parsed.name,
- dark: {
- bg: parsed.bg,
- headerBg: parsed.headerBg,
- footerBg: parsed.footerBg,
- surface: parsed.surface,
- border: parsed.border,
- textPrimary: parsed.textPrimary,
- textSecondary: parsed.textSecondary,
- accent: parsed.accent,
- ansi: parsed.ansi
- },
- light: parsed.light,
- isCustom: true
- };
- }
-}
-
-const THEME_DESCRIPTIONS: Record = {
- claude: "Anthropic's warm, organic paper-like aesthetic.",
- cursor: "A deep, modern dark mode inspired by AI code editors.",
- cortex: "The original high-contrast neon-pop experience.",
- tokyo: "A vibrant, neon-lit theme inspired by Tokyo at night.",
- nord: "An arctic, north-bluish clean and focused palette.",
- catppuccin: "A soothing, high-productivity pastel color scheme.",
- caffeine: "A warm, earthy workspace inspired by morning coffee.",
-};
-
-export function ThemesTab({
- theme,
- allThemes,
- resolvedScheme,
- setTheme,
- addCustomTheme,
- removeCustomTheme,
- previewTheme,
- cancelPreview,
-}: ThemesTabProps) {
- const [jsonInput, setJsonInput] = React.useState("");
- const [isImporting, setIsImporting] = React.useState(false);
- const [isPreviewing, setIsPreviewing] = React.useState(false);
-
- const containerVariants: Variants = {
- hidden: { opacity: 0 },
- show: {
- opacity: 1,
- transition: {
- staggerChildren: 0.1
- }
- }
- };
-
- const itemVariants: Variants = {
- hidden: { opacity: 0, y: 15 },
- show: { opacity: 1, y: 0, transition: { duration: 0.4, ease: [0.22, 1, 0.36, 1] } }
- };
-
- // Live Preview Logic
- React.useEffect(() => {
- if (!isPreviewing || !jsonInput.trim()) {
- if (!isPreviewing) cancelPreview();
- return;
- }
-
- try {
- const parsed = JSON.parse(jsonInput);
- const normalized = normalizeThemeInput(parsed);
- previewTheme(normalized);
- } catch (e) {
- // Silently fail preview for invalid JSON
- }
- }, [jsonInput, isPreviewing, previewTheme, cancelPreview]);
-
- const handleImportTheme = async () => {
- if (!jsonInput.trim()) return;
-
- try {
- const parsed = JSON.parse(jsonInput);
- const normalized = normalizeThemeInput(parsed);
-
- await addCustomTheme(normalized);
- toast.success(`${normalized.name} created successfully`, { description: "The theme has been added to your library." });
- setJsonInput("");
- setIsImporting(false);
- setIsPreviewing(false);
- } catch (err: any) {
- toast.error("Failed to create Theme", { description: "Check your JSON formatting and required keys." });
- }
- };
-
- const handleCopyJson = (t: ThemeDefinition) => {
- const { isCustom, ...rest } = t; // Strip internal flag
- navigator.clipboard.writeText(JSON.stringify(rest, null, 2));
- toast.success(`${t.name} copied successfully`, { description: "The theme JSON is ready to paste." });
- };
-
- return (
-
-
- }
- description="Choose from our curated themes or create your own."
- >
-
-
-
- Library
-
-
setIsImporting(!isImporting)}
- className="h-7 text-[10px] gap-1.5 bg-[var(--accent-primary)]/5 hover:bg-[var(--accent-primary)]/10 text-[var(--accent-primary)] font-bold"
- >
-
- Create Theme
-
-
-
- {isImporting && (
-
-
-
-
-
- Theme JSON Payload
-
-
-
- Live Preview
-
-
-
Standard Schema
-
-
-
-
- )}
-
-
- {allThemes.map((t) => {
- const isActive = theme === t.id;
- return (
-
setTheme(t.id)}
- className={`group/theme cursor-pointer transition-all hover:bg-[var(--text-primary)]/[0.04] text-left overflow-hidden border ${
- isActive
- ? "border-[var(--accent-primary)] bg-[var(--text-primary)]/5 shadow-[0_0_20px_rgba(var(--accent-primary-rgb),0.05)]"
- : "border-[var(--border-color)] bg-[var(--text-primary)]/[0.02]"
- }`}
- >
-
- {/* The Icon Container */}
-
- {/* The Pills */}
-
- {(() => {
- const previewPalette = resolvedScheme === 'light'
- ? (t.light || {
- bg: "#FAFAFA",
- surface: "#FFFFFF",
- accent: t.dark.accent
- })
- : t.dark;
- return (
- <>
-
-
-
- >
- );
- })()}
-
-
-
- {/* The Text Block */}
-
-
-
- {t.name}
-
- {isActive && (
-
- Active
-
- )}
-
-
- {t.isCustom ? "User-created theme" : (THEME_DESCRIPTIONS[t.id] || "Core Cortex preset")}
-
-
-
- {/* Actions */}
-
-
-
- {
- e.stopPropagation();
- handleCopyJson(t);
- }}
- className="w-7 h-7 rounded-md text-[var(--text-secondary)]/40 opacity-0 group-hover/theme:opacity-100 transition-all hover:text-[var(--accent-primary)] hover:bg-[var(--accent-primary)]/10"
- >
-
-
-
-
- Copy JSON
-
-
- {t.isCustom && (
-
-
- {
- e.stopPropagation();
- removeCustomTheme(t.id);
- }}
- className="w-7 h-7 text-[var(--text-secondary)]/60 opacity-0 group-hover/theme:opacity-100 transition-all hover:bg-red-500/10 hover:text-red-400 active:scale-95"
- >
-
-
-
-
- Delete Theme
-
-
- )}
-
-
-
- );
- })}
-
-
-
-
-
- );
-}
diff --git a/src/features/setup/SetupView.tsx b/src/features/setup/SetupView.tsx
index 7d3c3c2..2f2945c 100644
--- a/src/features/setup/SetupView.tsx
+++ b/src/features/setup/SetupView.tsx
@@ -14,188 +14,210 @@ import { StepWorkspace } from "./components/steps/StepWorkspace";
import { StepConfigure } from "./components/steps/StepConfigure";
import { StepPreview } from "./components/steps/StepPreview";
import { configToLayoutNode } from "@/lib/setup-utils";
-import { LayoutNode } from "@/types";
+import { LayoutNode } from "@/lib";
import { PaneConfig } from "@/lib/setup-constants";
import { ScrollArea } from "@/components/ui/scroll-area";
interface SetupViewProps {
- mode: 'normal' | 'agents';
- onLaunch: (config: { rootPath: string; layout: LayoutNode; panes: PaneConfig[] }) => void;
+ mode: "normal" | "agents";
+ onLaunch: (config: {
+ rootPath: string;
+ layout: LayoutNode;
+ panes: PaneConfig[];
+ }) => void;
onBack: () => void;
}
-export const SetupView = React.memo(({ mode, onLaunch, onBack }: SetupViewProps) => {
- const [step, setStep] = useState(INITIAL_STEP);
-
- const {
- rootPath,
- setRootPath,
- isValidDir,
- handleBrowse,
- handleBreadcrumbClick,
- defaultDir
- } = useWorkspaceDirectory();
-
- const {
- presets,
- addPreset,
- removePreset
- } = usePresets(rootPath || defaultDir, isValidDir);
-
- const { snippets } = useSnippets();
- const { agents } = useAgents();
-
- const {
- layoutType,
- customLayout,
- setCustomLayout,
- savedLayouts,
- addSavedLayout,
- removeSavedLayout,
- currentLayout,
- activePanes,
- handleLayoutChange,
- updatePaneCommand,
- updatePaneName,
- updateAllPaneCommands,
- restoreDefaults,
- isInitialized
- } = useSetupPanes(agents);
-
- const activePresets = useMemo(() => presets.filter(p => !p.isArchived), [presets]);
- const activeSnippets = useMemo(() => snippets.filter(s => !s.isArchived), [snippets]);
- const activeSavedLayouts = useMemo(() => savedLayouts.filter(l => !l.isArchived), [savedLayouts]);
-
- const isStepValid = useMemo(() => {
- if (step === 1) return (rootPath || defaultDir).trim() !== "";
- if (step === 2) return true; // Allow proceeding even if command inputs are empty
- return true;
- }, [step, rootPath, defaultDir, activePanes]);
-
- const handleNext = () => {
- if (step === 1 && isValidDir === false && rootPath !== "") {
- toast.error("Failed to open directory", {
- description: "The provided path does not exist or is inaccessible.",
+export const SetupView = React.memo(
+ ({ mode, onLaunch, onBack }: SetupViewProps) => {
+ const [step, setStep] = useState(INITIAL_STEP);
+
+ const {
+ rootPath,
+ setRootPath,
+ isValidDir,
+ handleBrowse,
+ handleBreadcrumbClick,
+ defaultDir,
+ } = useWorkspaceDirectory();
+
+ const { presets, addPreset, removePreset } = usePresets(
+ rootPath || defaultDir,
+ isValidDir,
+ );
+
+ const { snippets } = useSnippets();
+ const { agents } = useAgents();
+
+ const {
+ layoutType,
+ customLayout,
+ setCustomLayout,
+ savedLayouts,
+ addSavedLayout,
+ removeSavedLayout,
+ currentLayout,
+ activePanes,
+ handleLayoutChange,
+ updatePaneCommand,
+ updatePaneName,
+ updateAllPaneCommands,
+ restoreDefaults,
+ isInitialized,
+ } = useSetupPanes(agents);
+
+ const activePresets = useMemo(
+ () => presets.filter((p) => !p.isArchived),
+ [presets],
+ );
+ const activeSnippets = useMemo(
+ () => snippets.filter((s) => !s.isArchived),
+ [snippets],
+ );
+ const activeSavedLayouts = useMemo(
+ () => savedLayouts.filter((l) => !l.isArchived),
+ [savedLayouts],
+ );
+
+ const isStepValid = useMemo(() => {
+ if (step === 1) return (rootPath || defaultDir).trim() !== "";
+ if (step === 2) return true; // Allow proceeding even if command inputs are empty
+ return true;
+ }, [step, rootPath, defaultDir, activePanes]);
+
+ const handleNext = () => {
+ if (step === 1 && isValidDir === false && rootPath !== "") {
+ toast.error("Failed to open directory", {
+ description: "The provided path does not exist or is inaccessible.",
+ });
+ return;
+ }
+ setStep((s) => Math.min(s + 1, MAX_STEP));
+ };
+
+ const prevStep = () => setStep((s) => Math.max(s - 1, INITIAL_STEP));
+
+ const handleLaunch = () => {
+ const layoutNode = configToLayoutNode(currentLayout, activePanes);
+ onLaunch({
+ rootPath: rootPath || defaultDir,
+ layout: layoutNode,
+ panes: activePanes,
});
- return;
- }
- setStep(s => Math.min(s + 1, MAX_STEP));
- };
-
- const prevStep = () => setStep(s => Math.max(s - 1, INITIAL_STEP));
-
- const handleLaunch = () => {
- const layoutNode = configToLayoutNode(currentLayout, activePanes);
- onLaunch({
- rootPath: rootPath || defaultDir,
- layout: layoutNode,
- panes: activePanes
- });
- };
-
- // Keyboard navigation shortcuts for the setup process
- useEffect(() => {
- const handleKeyDown = (e: KeyboardEvent) => {
- // 1. Next / Launch (Enter / Ctrl+Enter)
- if (e.key === 'Enter') {
- // Ctrl+Shift+Enter: Skip directly to launch if valid
- if (e.ctrlKey && e.shiftKey) {
- if (isStepValid) handleLaunch();
- return;
- }
+ };
- if (e.ctrlKey || e.metaKey || step === MAX_STEP) {
- if (isStepValid) handleLaunch();
+ // Keyboard navigation shortcuts for the setup process
+ useEffect(() => {
+ const handleKeyDown = (e: KeyboardEvent) => {
+ // 1. Next / Launch (Enter / Ctrl+Enter)
+ if (e.key === "Enter") {
+ // Ctrl+Shift+Enter: Skip directly to launch if valid
+ if (e.ctrlKey && e.shiftKey) {
+ if (isStepValid) handleLaunch();
+ return;
+ }
+
+ if (e.ctrlKey || e.metaKey || step === MAX_STEP) {
+ if (isStepValid) handleLaunch();
+ return;
+ }
+ if (isStepValid) handleNext();
return;
}
- if (isStepValid) handleNext();
- return;
- }
- // 2. Previous / Cancel (Esc)
- if (e.key === 'Escape') {
- if (step > 1) {
- prevStep();
- } else {
- onBack();
+ // 2. Previous / Cancel (Esc)
+ if (e.key === "Escape") {
+ if (step > 1) {
+ prevStep();
+ } else {
+ onBack();
+ }
}
- }
- };
-
- window.addEventListener('keydown', handleKeyDown);
- return () => window.removeEventListener('keydown', handleKeyDown);
- }, [step, isStepValid, isValidDir, rootPath, defaultDir, currentLayout, activePanes, onBack]);
-
-
- return (
-
-
-
-
-
-
- {step === 1 && (
-
- )}
-
- {step === 2 && (
-
- )}
-
- {step === 3 && (
-
- )}
-
-
-
-
-
-
- );
-});
+ };
+
+ window.addEventListener("keydown", handleKeyDown);
+ return () => window.removeEventListener("keydown", handleKeyDown);
+ }, [
+ step,
+ isStepValid,
+ isValidDir,
+ rootPath,
+ defaultDir,
+ currentLayout,
+ activePanes,
+ onBack,
+ ]);
+
+ return (
+
+
+
+
+
+
+ {step === 1 && (
+
+ )}
+
+ {step === 2 && (
+
+ )}
+
+ {step === 3 && (
+
+ )}
+
+
+
+
+
+
+ );
+ },
+);
diff --git a/src/features/setup/components/steps/StepConfigure.tsx b/src/features/setup/components/steps/StepConfigure.tsx
index 2e03e66..8a9651d 100644
--- a/src/features/setup/components/steps/StepConfigure.tsx
+++ b/src/features/setup/components/steps/StepConfigure.tsx
@@ -3,13 +3,13 @@ import { motion, Variants } from "framer-motion";
import { PaneConfig } from "@/lib/setup-constants";
import { PaneConfigCard } from "../ui-parts/PaneConfigCard";
import { useState } from "react";
-import { Snippet, Agent } from "@/types";
+import { Snippet, Agent } from "@/lib";
import { toTitleCase } from "@/lib/utils";
import { Combobox } from "@/components/ui/combobox";
import { Spotlight } from "@/components/ui/spotlight";
interface StepConfigureProps {
- mode: 'normal' | 'agents';
+ mode: "normal" | "agents";
activePanes: PaneConfig[];
updatePaneCommand: (id: number, command: string, isCustom?: boolean) => void;
updatePaneName: (id: number, name: string) => void;
@@ -18,7 +18,15 @@ interface StepConfigureProps {
agents?: Agent[];
}
-export function StepConfigure({ mode, activePanes, updatePaneCommand, updatePaneName, updateAllPaneCommands, snippets, agents = [] }: StepConfigureProps) {
+export function StepConfigure({
+ mode,
+ activePanes,
+ updatePaneCommand,
+ updatePaneName,
+ updateAllPaneCommands,
+ snippets,
+ agents = [],
+}: StepConfigureProps) {
const [globalValue, setGlobalValue] = useState("");
const containerVariants: Variants = {
@@ -26,26 +34,29 @@ export function StepConfigure({ mode, activePanes, updatePaneCommand, updatePane
visible: {
opacity: 1,
transition: {
- staggerChildren: 0.1
- }
- }
+ staggerChildren: 0.1,
+ },
+ },
};
const itemVariants: Variants = {
hidden: { opacity: 0, y: 15 },
- visible: {
- opacity: 1,
+ visible: {
+ opacity: 1,
y: 0,
- transition: { duration: 0.6, ease: [0.22, 1, 0.36, 1] as any }
- }
+ transition: { duration: 0.6, ease: [0.22, 1, 0.36, 1] as any },
+ },
};
- const globalItems = mode === 'agents'
- ? agents.filter(p => p.status === 'installed').map(p => ({ label: toTitleCase(p.label), value: p.command }))
- : snippets.map(s => ({ label: s.label, value: s.command }));
+ const globalItems =
+ mode === "agents"
+ ? agents
+ .filter((p) => p.status === "installed")
+ .map((p) => ({ label: toTitleCase(p.label), value: p.command }))
+ : snippets.map((s) => ({ label: s.label, value: s.command }));
return (
-
{/* Top Section: Context & Global Actions (Balanced) */}
-
+
- {mode === 'agents' ? 'Agent Assignment' : 'Startup Commands'}
+ {mode === "agents" ? "Agent Assignment" : "Startup Commands"}
- {mode === 'agents'
- ? 'Assign specific AI agents to each terminal. You can use one agent for everything or assign different ones to each window.'
- : 'Set up the commands that run when you open your workspace. Automatically initialize every terminal window.'}
+ {mode === "agents"
+ ? "Assign specific AI agents to each terminal. You can use one agent for everything or assign different ones to each window."
+ : "Set up the commands that run when you open your workspace. Automatically initialize every terminal window."}
- {mode === 'agents' && (
-
+ {mode === "agents" && (
+
@@ -82,16 +99,17 @@ export function StepConfigure({ mode, activePanes, updatePaneCommand, updatePane
Global Assignment
-
+
{
setGlobalValue(val);
- const isPreset = mode === 'agents'
- ? agents.some(p => p.command === val)
- : snippets.some(s => s.command === val);
+ const isPreset =
+ mode === "agents"
+ ? agents.some((p) => p.command === val)
+ : snippets.some((s) => s.command === val);
updateAllPaneCommands(val, !isPreset);
}}
placeholder="Assign agent to all windows..."
@@ -100,7 +118,9 @@ export function StepConfigure({ mode, activePanes, updatePaneCommand, updatePane
/>
@@ -110,15 +130,12 @@ export function StepConfigure({ mode, activePanes, updatePaneCommand, updatePane
{/* Interaction Section: Individual Pane Configurations (Wide & Balanced) */}
-
{activePanes.map((pane, index) => (
-
+
Terminal 0{pane.id}
-
+
{pane.command || "system default shell"}
@@ -163,7 +163,7 @@ export function StepPreview({ rootPath, defaultDir, layout, activePanes }: StepP
className="flex flex-col items-center justify-center rounded-lg border border-white/5 bg-white/[0.02] group-hover:border-[var(--accent-primary)]/20 group-hover:bg-[var(--accent-primary)]/[0.03] transition-all duration-500"
>
-
+
0{pane.id}
diff --git a/src/features/setup/components/steps/StepWorkspace.tsx b/src/features/setup/components/steps/StepWorkspace.tsx
index 44baf24..81ae8ad 100644
--- a/src/features/setup/components/steps/StepWorkspace.tsx
+++ b/src/features/setup/components/steps/StepWorkspace.tsx
@@ -1,4 +1,14 @@
-import { FolderOpen, Lock, X, Save, Database, Layout, Zap } from "@/components/ui/icons";
+import {
+ FolderOpen,
+ Lock,
+ X,
+ Save,
+ Database,
+ Layout,
+ Zap,
+ AlertCircle,
+ CheckCircle2,
+} from "@/components/ui/icons";
import { useState } from "react";
import { motion, Variants } from "framer-motion";
import { Button } from "@/components/ui/button";
@@ -10,7 +20,7 @@ import { cn } from "@/lib/utils";
import { EmptyState } from "@/components/ui/empty-state";
import { Spotlight } from "@/components/ui/spotlight";
-import { DirectoryPreset } from "@/types";
+import { DirectoryPreset } from "@/lib";
interface StepWorkspaceProps {
rootPath: string;
@@ -50,48 +60,49 @@ export function StepWorkspace({
savedLayouts,
addSavedLayout,
removeSavedLayout,
- onRestoreLayouts
+ onRestoreLayouts,
}: StepWorkspaceProps) {
const [layoutName, setLayoutName] = useState("");
const handleSaveLayout = () => {
- const finalName = layoutName.trim()
- ? layoutName
- : customLayout.type === 'grid'
+ const finalName = layoutName.trim()
+ ? layoutName
+ : customLayout.type === "grid"
? `${customLayout.rows}X${customLayout.cols}`
: `${customLayout.value} PANES`;
-
+
addSavedLayout(finalName, customLayout);
setLayoutName("");
};
const currentPath = rootPath || defaultDir;
- const isInvalid = customLayout.type === 'grid'
- ? (customLayout.rows < 1 || customLayout.cols < 1)
- : (customLayout.value < 1);
+ const isInvalid =
+ customLayout.type === "grid"
+ ? customLayout.rows < 1 || customLayout.cols < 1
+ : customLayout.value < 1;
const containerVariants: Variants = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: {
- staggerChildren: 0.1
- }
- }
+ staggerChildren: 0.1,
+ },
+ },
};
const itemVariants: Variants = {
hidden: { opacity: 0, y: 10 },
- visible: {
- opacity: 1,
+ visible: {
+ opacity: 1,
y: 0,
- transition: { duration: 0.6, ease: [0.22, 1, 0.36, 1] as any }
- }
+ transition: { duration: 0.6, ease: [0.22, 1, 0.36, 1] as any },
+ },
};
return (
-
- Choose the main folder for your project. All terminal sessions and agents will start in this folder.
+ Choose the main folder for your project. All terminal sessions and
+ agents will start in this folder.
{/* Bottom: Interaction (Full Width) */}
-
-
+
-
+
Folder Path
@@ -137,7 +162,8 @@ export function StepWorkspace({
value={rootPath}
onChange={(e) => setRootPath(e.target.value)}
placeholder={defaultDir || "Select a folder"}
- className="h-8 border-none bg-transparent px-0.5 font-mono text-xs text-[var(--text-primary)] shadow-none focus-visible:ring-0 placeholder:text-[var(--text-secondary)]/10 font-bold"
+ autoComplete="off"
+ className="h-8 border-none bg-transparent px-0.5 text-xs text-[var(--text-primary)] shadow-none focus-visible:ring-0 placeholder:text-[var(--text-secondary)]/10 font-bold"
/>
@@ -163,27 +189,53 @@ export function StepWorkspace({
+ {isValidDir === false && rootPath !== "" && (
+
+
+ Directory does not exist or is inaccessible.
+
+ )}
+
+ {isValidDir === true && rootPath !== "" && (
+
+
+ Directory verified.
+
+ )}
+
{currentPath && (
-
-
- {currentPath.split(/[\\/]/).filter(Boolean).map((part, i, arr) => (
-
- handleBreadcrumbClick(i)}
- className={cn(
- "rounded-md px-1.5 py-0.5 transition-all hover:bg-white/5 hover:text-[var(--text-primary)] border border-transparent",
- i === arr.length - 1 ? "text-[var(--accent-primary)] bg-[var(--accent-primary)]/10 font-bold" : "text-[var(--text-secondary)]"
+
+ {currentPath
+ .split(/[\\/]/)
+ .filter(Boolean)
+ .map((part, i, arr) => (
+
+ handleBreadcrumbClick(i)}
+ className={cn(
+ "rounded-md px-1.5 py-0.5 transition-all hover:bg-white/5 hover:text-[var(--text-primary)] border border-transparent",
+ i === arr.length - 1
+ ? "text-[var(--accent-primary)] bg-[var(--accent-primary)]/10 font-bold"
+ : "text-[var(--text-secondary)]",
+ )}
+ >
+ {part}
+
+ {i < arr.length - 1 && (
+
+ /
+
)}
- >
- {part}
-
- {i < arr.length - 1 && / }
-
- ))}
+
+ ))}
{!rootPath && (
System Default
@@ -219,16 +271,17 @@ export function StepWorkspace({
- Set up your workspace. Divide your screen into multiple terminal windows to work more efficiently.
+ Set up your workspace. Divide your screen into multiple terminal
+ windows to work more efficiently.
{/* Bottom: Interaction (Full Width) */}
-
+
- Layout Preview
-
- {layoutName.trim() ? layoutName : customLayout.type === 'grid' ? `${customLayout.rows}X${customLayout.cols}` : `${customLayout.value} PANES`}
+
+ Layout Preview
+
+
+ {layoutName.trim()
+ ? layoutName
+ : customLayout.type === "grid"
+ ? `${customLayout.rows}X${customLayout.cols}`
+ : `${customLayout.value} PANES`}
);
}
-
-
diff --git a/src/features/setup/components/ui-parts/LayoutSelector.tsx b/src/features/setup/components/ui-parts/LayoutSelector.tsx
index a928006..72af6ba 100644
--- a/src/features/setup/components/ui-parts/LayoutSelector.tsx
+++ b/src/features/setup/components/ui-parts/LayoutSelector.tsx
@@ -81,7 +81,7 @@ export function LayoutSelector({
{opt.name}
{!opt.isSystem && (
-
+
{opt.config.type === 'grid' ? `${opt.config.rows}X${opt.config.cols}` : `${opt.config.value} PANES`}
)}
@@ -171,7 +171,7 @@ function CustomLayoutForm({
max="4"
value={customLayout.rows === 0 ? "" : customLayout.rows}
onChange={(e) => handleNumericInput(e.target.value, 'rows')}
- className="h-9 w-14 bg-[var(--text-primary)]/5 px-2 font-mono text-xs text-center border-[var(--border-color)] focus:border-[var(--accent-primary)] font-bold rounded-lg transition-all"
+ className="h-9 w-14 bg-[var(--text-primary)]/5 px-2 text-xs text-center border-[var(--border-color)] focus:border-[var(--accent-primary)] font-bold rounded-lg transition-all"
/>
@@ -185,7 +185,7 @@ function CustomLayoutForm({
max="4"
value={customLayout.cols === 0 ? "" : customLayout.cols}
onChange={(e) => handleNumericInput(e.target.value, 'cols')}
- className="h-9 w-14 bg-[var(--text-primary)]/5 px-2 font-mono text-xs text-center border-[var(--border-color)] focus:border-[var(--accent-primary)] font-bold rounded-lg transition-all"
+ className="h-9 w-14 bg-[var(--text-primary)]/5 px-2 text-xs text-center border-[var(--border-color)] focus:border-[var(--accent-primary)] font-bold rounded-lg transition-all"
/>
>
@@ -198,7 +198,7 @@ function CustomLayoutForm({
max="16"
value={customLayout.value === 0 ? "" : customLayout.value}
onChange={(e) => handleNumericInput(e.target.value, 'value')}
- className="h-9 w-14 bg-[var(--text-primary)]/5 px-2 font-mono text-xs text-center border-[var(--border-color)] focus:border-[var(--accent-primary)] font-bold rounded-lg transition-all"
+ className="h-9 w-14 bg-[var(--text-primary)]/5 px-2 text-xs text-center border-[var(--border-color)] focus:border-[var(--accent-primary)] font-bold rounded-lg transition-all"
/>
)}
@@ -206,7 +206,7 @@ function CustomLayoutForm({
Live Preview
-
+
{getPaneCount(customLayout)} ACTIVE
diff --git a/src/features/setup/components/ui-parts/PaneConfigCard.tsx b/src/features/setup/components/ui-parts/PaneConfigCard.tsx
index 1a6522a..b038353 100644
--- a/src/features/setup/components/ui-parts/PaneConfigCard.tsx
+++ b/src/features/setup/components/ui-parts/PaneConfigCard.tsx
@@ -1,9 +1,14 @@
-import { Terminal, Code, Cpu, Library, X, CornerDownLeft } from "@/components/ui/icons";
-import { PaneConfig } from "@/lib/setup-constants";
-import { Snippet, Agent } from "@/types";
import {
- Combobox,
-} from "@/components/ui/combobox";
+ Terminal,
+ Code,
+ Cpu,
+ Library,
+ X,
+ CornerDownLeft,
+} from "@/components/ui/icons";
+import { PaneConfig } from "@/lib/setup-constants";
+import { Snippet, Agent } from "@/lib";
+import { Combobox } from "@/components/ui/combobox";
import { Input } from "@/components/ui/input";
import { motion, AnimatePresence } from "framer-motion";
import {
@@ -15,8 +20,31 @@ import { Button } from "@/components/ui/button";
import { useState, useRef } from "react";
import { Kbd } from "@/components/ui/kbd";
import { extractVariables, resolveVariables } from "@/lib/snippet-utils";
-import { toTitleCase } from "@/lib/utils";
+import { toTitleCase, cn } from "@/lib/utils";
import { Spotlight } from "@/components/ui/spotlight";
+import * as LobeIcons from "@lobehub/icons";
+
+function resolveLobeIcon(name: string | undefined): any {
+ if (!name || !name.trim()) return null;
+ const trimmed = name.trim();
+
+ // 1. Exact match
+ if ((LobeIcons as any)[trimmed]) return (LobeIcons as any)[trimmed];
+
+ // 2. Capitalized match (e.g. 'gemini' -> 'Gemini')
+ const capitalized = trimmed.charAt(0).toUpperCase() + trimmed.slice(1);
+ if ((LobeIcons as any)[capitalized]) return (LobeIcons as any)[capitalized];
+
+ // 3. Case-insensitive TOC lookup
+ const match = (LobeIcons.toc || []).find(
+ (item: any) => item.id.toLowerCase() === trimmed.toLowerCase()
+ );
+ if (match && (LobeIcons as any)[match.id]) {
+ return (LobeIcons as any)[match.id];
+ }
+
+ return null;
+}
interface PendingSnippet {
originalCommand: string;
@@ -28,21 +56,74 @@ interface PendingSnippet {
interface PaneConfigCardProps {
pane: PaneConfig;
index: number;
- mode: 'normal' | 'agents';
+ mode: "normal" | "agents";
onUpdate: (id: number, command: string, isCustom?: boolean) => void;
onNameUpdate?: (id: number, name: string) => void;
snippets: Snippet[];
agents?: Agent[];
}
-export function PaneConfigCard({ pane, index, mode, onUpdate, onNameUpdate, snippets, agents = [] }: PaneConfigCardProps) {
+export function PaneConfigCard({
+ pane,
+ index,
+ mode,
+ onUpdate,
+ onNameUpdate,
+ snippets,
+ agents = [],
+}: PaneConfigCardProps) {
const [isEditingName, setIsEditingName] = useState(false);
- const [pendingSnippet, setPendingSnippet] = useState
(null);
+ const [pendingSnippet, setPendingSnippet] = useState(
+ null,
+ );
const [currentVarValue, setCurrentVarValue] = useState("");
const promptInputRef = useRef(null);
-
+
const isPopulated = (pane.command || "").trim() !== "";
+ const getAgentIcon = () => {
+ if (!isPopulated) {
+ return mode === "agents" ? : ;
+ }
+ if (mode === "normal") {
+ return ;
+ }
+ const agent = agents.find((a) => a.command === pane.command);
+ if (!agent) {
+ return ;
+ }
+
+ // 0. Check if the icon is a base64 string
+ if (agent.icon && agent.icon.startsWith("data:image/")) {
+ return (
+
+ );
+ }
+
+ // 1. Try matching explicitly saved icon name
+ let IconComponent = resolveLobeIcon(agent.icon);
+
+ // 2. Try matching the label (e.g. "Gemini" -> Gemini)
+ if (!IconComponent) {
+ IconComponent = resolveLobeIcon(agent.label);
+ }
+
+ // 3. Try matching the command (e.g. "gemini" -> Gemini)
+ if (!IconComponent) {
+ IconComponent = resolveLobeIcon(agent.command);
+ }
+
+ if (IconComponent) {
+ return IconComponent.Color ? : ;
+ }
+
+ return ;
+ };
+
const handleSnippetSelect = (snippet: Snippet) => {
const variables = extractVariables(snippet.command);
if (variables.length > 0) {
@@ -50,7 +131,7 @@ export function PaneConfigCard({ pane, index, mode, onUpdate, onNameUpdate, snip
originalCommand: snippet.command,
variables,
resolvedValues: {},
- currentIndex: 0
+ currentIndex: 0,
});
setCurrentVarValue("");
} else {
@@ -62,19 +143,25 @@ export function PaneConfigCard({ pane, index, mode, onUpdate, onNameUpdate, snip
if (!pendingSnippet) return;
const currentVar = pendingSnippet.variables[pendingSnippet.currentIndex];
- const newResolved = { ...pendingSnippet.resolvedValues, [currentVar]: currentVarValue };
+ const newResolved = {
+ ...pendingSnippet.resolvedValues,
+ [currentVar]: currentVarValue,
+ };
const nextIndex = pendingSnippet.currentIndex + 1;
if (nextIndex < pendingSnippet.variables.length) {
setPendingSnippet({
...pendingSnippet,
resolvedValues: newResolved,
- currentIndex: nextIndex
+ currentIndex: nextIndex,
});
setCurrentVarValue("");
setTimeout(() => promptInputRef.current?.focus(), 10);
} else {
- const finalCommand = resolveVariables(pendingSnippet.originalCommand, newResolved);
+ const finalCommand = resolveVariables(
+ pendingSnippet.originalCommand,
+ newResolved,
+ );
onUpdate(pane.id, finalCommand, false);
setPendingSnippet(null);
setCurrentVarValue("");
@@ -90,35 +177,45 @@ export function PaneConfigCard({ pane, index, mode, onUpdate, onNameUpdate, snip
-
a.command === pane.command)
+ ? "bg-[var(--surface-color)] border border-[var(--border-color)]/30 shadow-sm"
+ : "bg-[var(--accent-primary)] text-[var(--accent-contrast)] shadow-md"
+ : "bg-[var(--text-primary)]/5 text-[var(--text-secondary)]"
+ )}
>
- {mode === 'agents' ? : }
+ {getAgentIcon()}
@@ -132,12 +229,12 @@ export function PaneConfigCard({ pane, index, mode, onUpdate, onNameUpdate, snip
onChange={(e) => onNameUpdate?.(pane.id, e.target.value)}
onBlur={() => setIsEditingName(false)}
onKeyDown={(e) => {
- if (e.key === 'Enter') setIsEditingName(false);
- if (e.key === 'Escape') setIsEditingName(false);
+ if (e.key === "Enter") setIsEditingName(false);
+ if (e.key === "Escape") setIsEditingName(false);
}}
/>
) : (
- setIsEditingName(true)}
>
@@ -146,7 +243,7 @@ export function PaneConfigCard({ pane, index, mode, onUpdate, onNameUpdate, snip
)}
-
+
{isPopulated && (
- Active
+
+ Active
+
)}
@@ -162,40 +261,48 @@ export function PaneConfigCard({ pane, index, mode, onUpdate, onNameUpdate, snip
- {mode === 'agents'
- ? (pane.isCustom ? 'Manual Command' : 'Agent Selection')
- : 'Startup Command'}
+ {mode === "agents"
+ ? pane.isCustom
+ ? "Manual Command"
+ : "Agent Selection"
+ : "Startup Command"}
- {mode === 'agents' && (
+ {mode === "agents" && (
onUpdate(pane.id, "", !pane.isCustom)}
className="h-6 px-2.5 gap-1.5 bg-white/5 text-[var(--text-secondary)] hover:text-[var(--accent-primary)] hover:bg-[var(--accent-primary)]/10 rounded-lg text-[9px] font-bold uppercase tracking-wider transition-all"
>
{pane.isCustom ? : }
- {pane.isCustom ? 'AGENT' : 'MANUAL'}
+ {pane.isCustom ? "AGENT" : "MANUAL"}
)}
- {mode === 'normal' && (
+ {mode === "normal" && (
-
+
Snippets
-
+
Library
{snippets.length === 0 ? (
-
- No Snippets Found
+
+
+ No Snippets Found
+
) : (
@@ -205,8 +312,12 @@ export function PaneConfigCard({ pane, index, mode, onUpdate, onNameUpdate, snip
onClick={() => handleSnippetSelect(snippet)}
className="w-full flex flex-col gap-0.5 text-left px-3 py-2 rounded-xl hover:bg-white/5 transition-all group/snippet"
>
- {snippet.label}
- {snippet.command}
+
+ {snippet.label}
+
+
+ {snippet.command}
+
))}
@@ -218,12 +329,17 @@ export function PaneConfigCard({ pane, index, mode, onUpdate, onNameUpdate, snip
- {mode === 'agents' && !pane.isCustom ? (
+ {mode === "agents" && !pane.isCustom ? (
p.status === 'installed').map(p => ({ label: toTitleCase(p.label), value: p.command }))}
+ items={agents
+ .filter((p) => p.status === "installed")
+ .map((p) => ({
+ label: toTitleCase(p.label),
+ value: p.command,
+ }))}
value={pane.command || ""}
onValueChange={(val) => {
- const isPreset = agents.some(p => p.command === val);
+ const isPreset = agents.some((p) => p.command === val);
onUpdate(pane.id, val, !isPreset);
}}
placeholder="Select agent..."
@@ -237,11 +353,13 @@ export function PaneConfigCard({ pane, index, mode, onUpdate, onNameUpdate, snip
value={pane.command || ""}
onChange={(e) => onUpdate(pane.id, e.target.value, true)}
placeholder={
- mode === 'agents' ? "Command..." :
- pane.id === 1 ? "npm run dev" :
- "Command..."
+ mode === "agents"
+ ? "Command..."
+ : pane.id === 1
+ ? "npm run dev"
+ : "Command..."
}
- className="w-full h-10 bg-white/5 border-transparent hover:bg-white/[0.08] focus-visible:bg-[var(--bg-color)] focus-visible:border-[var(--accent-primary)]/40 text-xs font-mono placeholder:text-[var(--text-secondary)]/20 transition-all rounded-lg shadow-none focus-visible:ring-0 font-bold"
+ className="w-full h-10 bg-white/5 border-transparent hover:bg-white/[0.08] focus-visible:bg-[var(--bg-color)] focus-visible:border-[var(--accent-primary)]/40 text-xs placeholder:text-[var(--text-secondary)]/20 transition-all rounded-lg shadow-none focus-visible:ring-0 font-bold"
/>
@@ -258,7 +376,7 @@ export function PaneConfigCard({ pane, index, mode, onUpdate, onNameUpdate, snip
exit={{ opacity: 0 }}
className="absolute inset-0 z-50 flex items-center justify-center p-4 bg-black/60 backdrop-blur-md"
>
-
- Variable Entry
-
+
+ Variable Entry
+
+
-
+
- Value for {pendingSnippet.variables[pendingSnippet.currentIndex]}
+ Value for{" "}
+
+ {pendingSnippet.variables[pendingSnippet.currentIndex]}
+
-
setCurrentVarValue(e.target.value)}
onKeyDown={(e) => {
- if (e.key === 'Enter') { e.stopPropagation(); handleVariableSubmit(); }
- if (e.key === 'Escape') { e.stopPropagation(); handleVariableCancel(); }
+ if (e.key === "Enter") {
+ e.stopPropagation();
+ handleVariableSubmit();
+ }
+ if (e.key === "Escape") {
+ e.stopPropagation();
+ handleVariableCancel();
+ }
}}
placeholder={`Enter ${pendingSnippet.variables[pendingSnippet.currentIndex].toLowerCase()}...`}
className="h-10 text-xs bg-[var(--text-primary)]/5 border-[var(--border-color)] pr-10 focus:border-[var(--accent-primary)]/50 rounded-lg"
@@ -297,17 +429,26 @@ export function PaneConfigCard({ pane, index, mode, onUpdate, onNameUpdate, snip
-
Enter Confirm
-
Esc Skip
+
+ Enter Confirm
+
+
+ Esc Skip
+
+
+
+ {pendingSnippet.currentIndex + 1} /{" "}
+ {pendingSnippet.variables.length}
-
{pendingSnippet.currentIndex + 1} / {pendingSnippet.variables.length}
-
diff --git a/src/features/setup/components/ui-parts/PresetManager.tsx b/src/features/setup/components/ui-parts/PresetManager.tsx
index 6ad23bb..31839a0 100644
--- a/src/features/setup/components/ui-parts/PresetManager.tsx
+++ b/src/features/setup/components/ui-parts/PresetManager.tsx
@@ -3,7 +3,7 @@ import { Button } from "@/components/ui/button";
import { EmptyState } from "@/components/ui/empty-state";
import { cn } from "@/lib/utils";
-import { DirectoryPreset } from "@/types";
+import { DirectoryPreset } from "@/lib";
interface PresetManagerProps {
presets: DirectoryPreset[];
@@ -13,8 +13,14 @@ interface PresetManagerProps {
rootPath: string;
}
-export function PresetManager({ presets, onSelect, onRemove, onAdd, rootPath }: PresetManagerProps) {
- const isDuplicate = presets.some(p => p.path === rootPath);
+export function PresetManager({
+ presets,
+ onSelect,
+ onRemove,
+ onAdd,
+ rootPath,
+}: PresetManagerProps) {
+ const isDuplicate = presets.some((p) => p.path === rootPath);
return (
@@ -22,7 +28,7 @@ export function PresetManager({ presets, onSelect, onRemove, onAdd, rootPath }:
Quick Access Library
- {(rootPath && !isDuplicate) && (
+ {rootPath && !isDuplicate && (
)}
-
+
{presets.length === 0 ? (
-
{
const isCurrent = preset.path === rootPath;
return (
-
@@ -59,7 +67,9 @@ export function PresetManager({ presets, onSelect, onRemove, onAdd, rootPath }:
onClick={() => onSelect(preset.path)}
className={cn(
"text-[12px] font-bold tracking-tight transition-colors pr-3 border-r border-[var(--border-color)]",
- isCurrent ? "text-[var(--accent-primary)]" : "text-[var(--text-primary)] opacity-60 group-hover:opacity-100"
+ isCurrent
+ ? "text-[var(--accent-primary)]"
+ : "text-[var(--text-primary)] opacity-60 group-hover:opacity-100",
)}
>
{preset.label}
diff --git a/src/features/space/SpaceView.tsx b/src/features/space/SpaceView.tsx
index eea4905..0a32962 100644
--- a/src/features/space/SpaceView.tsx
+++ b/src/features/space/SpaceView.tsx
@@ -1,11 +1,11 @@
import * as React from "react";
import { useState, useMemo, useEffect, useCallback } from "react";
-import {
- DndContext,
- DragEndEvent,
- DragStartEvent,
- PointerSensor,
- useSensor,
+import {
+ DndContext,
+ DragEndEvent,
+ DragStartEvent,
+ PointerSensor,
+ useSensor,
useSensors,
} from "@dnd-kit/core";
import { TerminalPane } from "../terminal/TerminalPane";
@@ -13,8 +13,12 @@ import { DropZone } from "./components/DropZone";
import { useIsMobile } from "@/hooks/useIsMobile";
import { findNeighborPane } from "@/lib/setup-utils";
import { ThemeName } from "@/hooks/useTheme";
-import { LayoutNode, PaneNode } from "@/types";
-import { getSettingsGroup, SHORTCUT_DEFAULTS, ShortcutSettings } from "@/lib/store";
+import { LayoutNode, PaneNode } from "@/lib";
+import {
+ getSettingsGroup,
+ SHORTCUT_DEFAULTS,
+ ShortcutSettings,
+} from "@/lib/store";
import { matchesShortcut } from "@/lib/shortcut-utils";
import {
ResizableHandle,
@@ -29,7 +33,7 @@ interface SpaceViewProps {
layout: string | LayoutNode;
panes?: any[];
};
- mode: 'normal' | 'agents';
+ mode: "normal" | "agents";
theme: string;
setTheme: (theme: ThemeName) => void;
onStop: () => void;
@@ -37,263 +41,315 @@ interface SpaceViewProps {
setIsZenMode: (val: boolean) => void;
zenPadding?: number;
showPaneHeaders?: boolean;
- onSplitPane?: (paneId: string, direction: 'horizontal' | 'vertical') => void;
- onMovePane?: (dragId: string, dropId: string, direction: 'top' | 'bottom' | 'left' | 'right') => void;
+ onSplitPane?: (paneId: string, direction: "horizontal" | "vertical") => void;
+ onMovePane?: (
+ dragId: string,
+ dropId: string,
+ direction: "top" | "bottom" | "left" | "right",
+ ) => void;
onKillPane?: (paneId: string) => void;
onRenamePane?: (paneId: string, newName: string) => void;
}
interface DropData {
- direction: 'top' | 'bottom' | 'left' | 'right';
+ direction: "top" | "bottom" | "left" | "right";
}
-export const SpaceView = React.memo(({
- workspaceId,
- config,
- isZenMode,
- zenPadding = 32,
- onSplitPane,
- onMovePane,
- onKillPane,
- onRenamePane,
- isCurrent
-}: SpaceViewProps & { isCurrent: boolean }) => {
- const isMobile = useIsMobile();
- const [activeDragId, setActiveDragId] = useState
(null);
- const [focusedPaneId, setFocusedPaneId] = useState(null);
- const [isMaximized, setIsMaximized] = useState(false);
- const [shortcuts, setShortcuts] = useState(SHORTCUT_DEFAULTS);
+export const SpaceView = React.memo(
+ ({
+ workspaceId,
+ config,
+ isZenMode,
+ zenPadding = 32,
+ showPaneHeaders = true,
+ onSplitPane,
+ onMovePane,
+ onKillPane,
+ onRenamePane,
+ isCurrent,
+ }: SpaceViewProps & { isCurrent: boolean }) => {
+ const isMobile = useIsMobile();
+ const [activeDragId, setActiveDragId] = useState(null);
+ const [focusedPaneId, setFocusedPaneId] = useState(null);
+ const [isMaximized, setIsMaximized] = useState(false);
+ const [shortcuts, setShortcuts] =
+ useState(SHORTCUT_DEFAULTS);
- useEffect(() => {
- getSettingsGroup('shortcuts', SHORTCUT_DEFAULTS).then(setShortcuts);
- }, []);
+ useEffect(() => {
+ getSettingsGroup("shortcuts", SHORTCUT_DEFAULTS).then(
+ setShortcuts,
+ );
+ }, []);
- const sensors = useSensors(
- useSensor(PointerSensor, {
- activationConstraint: {
- distance: 8,
- },
- })
- );
+ const sensors = useSensors(
+ useSensor(PointerSensor, {
+ activationConstraint: {
+ distance: 8,
+ },
+ }),
+ );
- const layoutTree = useMemo(() => {
- if (typeof config.layout === 'string') {
- try {
- return JSON.parse(config.layout) as LayoutNode;
- } catch (e) {
- console.error("Failed to parse layout JSON", e);
- return { type: 'pane', id: '1', name: 'Terminal', command: '' } as PaneNode;
+ const layoutTree = useMemo(() => {
+ if (typeof config.layout === "string") {
+ try {
+ return JSON.parse(config.layout) as LayoutNode;
+ } catch (e) {
+ console.error("Failed to parse layout JSON", e);
+ return {
+ type: "pane",
+ id: "1",
+ name: "Terminal",
+ command: "",
+ } as PaneNode;
+ }
}
- }
- return config.layout as LayoutNode;
- }, [config.layout]);
+ return config.layout as LayoutNode;
+ }, [config.layout]);
- const allPanes = useMemo(() => {
- const panes: PaneNode[] = [];
- const traverse = (node: LayoutNode) => {
- if (node.type === 'pane') {
- panes.push(node);
- } else {
- node.children.forEach(traverse);
+ const allPanes = useMemo(() => {
+ const panes: PaneNode[] = [];
+ const traverse = (node: LayoutNode) => {
+ if (node.type === "pane") {
+ panes.push(node);
+ } else {
+ node.children.forEach(traverse);
+ }
+ };
+ traverse(layoutTree);
+ return panes;
+ }, [layoutTree]);
+
+ // Set initial focus if none
+ useEffect(() => {
+ if (!focusedPaneId && allPanes.length > 0) {
+ setFocusedPaneId(allPanes[0].id);
}
- };
- traverse(layoutTree);
- return panes;
- }, [layoutTree]);
+ }, [allPanes, focusedPaneId]);
- // Set initial focus if none
- useEffect(() => {
- if (!focusedPaneId && allPanes.length > 0) {
- setFocusedPaneId(allPanes[0].id);
- }
- }, [allPanes, focusedPaneId]);
+ // Global active session workspace keyboard shortcuts
+ useEffect(() => {
+ if (!isCurrent) return; // ONLY attach listener if this is the visible workspace
- // Global active session workspace keyboard shortcuts
- useEffect(() => {
- if (!isCurrent) return; // ONLY attach listener if this is the visible workspace
+ const handleKeyDown = (e: KeyboardEvent) => {
+ // ...
+ // 1. Focus Pane (Ctrl/Cmd + [1-9])
+ const isNumKey = e.key >= "1" && e.key <= "9";
+ if ((e.ctrlKey || e.metaKey) && isNumKey && !e.shiftKey && !e.altKey) {
+ const paneNum = parseInt(e.key, 10);
+ const targetPane = allPanes[paneNum - 1];
+ if (targetPane) {
+ e.preventDefault();
+ setFocusedPaneId(targetPane.id);
+ }
+ }
- const handleKeyDown = (e: KeyboardEvent) => {
- // ...
- // 1. Focus Pane (Ctrl/Cmd + [1-9])
- const isNumKey = e.key >= '1' && e.key <= '9';
- if ((e.ctrlKey || e.metaKey) && isNumKey && !e.shiftKey && !e.altKey) {
- const paneNum = parseInt(e.key, 10);
- const targetPane = allPanes[paneNum - 1];
- if (targetPane) {
+ // 2. Toggle Maximize (Ctrl/Cmd + Shift + M)
+ const isM = e.key.toLowerCase() === "m";
+ if ((e.ctrlKey || e.metaKey) && e.shiftKey && isM) {
e.preventDefault();
- setFocusedPaneId(targetPane.id);
+ setIsMaximized((prev) => !prev);
}
- }
- // 2. Toggle Maximize (Ctrl/Cmd + Shift + M)
- const isM = e.key.toLowerCase() === 'm';
- if ((e.ctrlKey || e.metaKey) && e.shiftKey && isM) {
- e.preventDefault();
- setIsMaximized(prev => !prev);
- }
-
- // 3. Directional Navigation (Alt + Arrows)
- const isArrow = e.key.startsWith('Arrow');
- const isAlt = e.altKey && !e.ctrlKey && !e.metaKey && !e.shiftKey;
- if (isAlt && isArrow && focusedPaneId) {
- e.preventDefault();
- const dir = e.key.slice(5).toLowerCase() as 'up' | 'down' | 'left' | 'right';
- const neighborId = findNeighborPane(layoutTree, focusedPaneId, dir);
- if (neighborId) {
- setFocusedPaneId(neighborId);
+ // 3. Directional Navigation (Alt + Arrows)
+ const isArrow = e.key.startsWith("Arrow");
+ const isAlt = e.altKey && !e.ctrlKey && !e.metaKey && !e.shiftKey;
+ if (isAlt && isArrow && focusedPaneId) {
+ e.preventDefault();
+ const dir = e.key.slice(5).toLowerCase() as
+ | "up"
+ | "down"
+ | "left"
+ | "right";
+ const neighborId = findNeighborPane(layoutTree, focusedPaneId, dir);
+ if (neighborId) {
+ setFocusedPaneId(neighborId);
+ }
}
- }
- // 4. Pane Management (Reset / Close)
- if (focusedPaneId) {
- if (matchesShortcut(e, shortcuts.resetPane)) {
- // Reset is handled by the terminal itself via its own listener
- // but we can add it here if we had a way to trigger it.
- // Since it's handled in XtermTerminal, we don't necessarily need it here
- // UNLESS the terminal is not focused.
- } else if (matchesShortcut(e, shortcuts.closePane)) {
- e.preventDefault();
- onKillPane?.(focusedPaneId);
+ // 4. Pane Management (Reset / Close)
+ if (focusedPaneId) {
+ if (matchesShortcut(e, shortcuts.resetPane)) {
+ // Reset is handled by the terminal itself via its own listener
+ // but we can add it here if we had a way to trigger it.
+ // Since it's handled in XtermTerminal, we don't necessarily need it here
+ // UNLESS the terminal is not focused.
+ } else if (matchesShortcut(e, shortcuts.closePane)) {
+ e.preventDefault();
+ onKillPane?.(focusedPaneId);
+ }
}
- }
- };
+ };
- window.addEventListener('keydown', handleKeyDown);
- return () => window.removeEventListener('keydown', handleKeyDown);
- }, [allPanes, focusedPaneId, layoutTree, shortcuts, onKillPane]);
+ window.addEventListener("keydown", handleKeyDown);
+ return () => window.removeEventListener("keydown", handleKeyDown);
+ }, [allPanes, focusedPaneId, layoutTree, shortcuts, onKillPane]);
- const handleDragStart = (event: DragStartEvent) => {
- setActiveDragId(event.active.id as string);
- };
+ const handleDragStart = (event: DragStartEvent) => {
+ setActiveDragId(event.active.id as string);
+ };
- const handleDragEnd = (event: DragEndEvent) => {
- const { active, over } = event;
- setActiveDragId(null);
+ const handleDragEnd = (event: DragEndEvent) => {
+ const { active, over } = event;
+ setActiveDragId(null);
- if (over && active.id !== over.id) {
- const overData = over.data.current as unknown as DropData;
- const dropDirection = overData?.direction || 'right';
-
- onMovePane?.(active.id as string, over.id as string, dropDirection);
- }
- };
+ if (over && active.id !== over.id) {
+ const overData = over.data.current as unknown as DropData;
+ const dropDirection = overData?.direction || "right";
- const renderTerminalPane = useCallback((pane: PaneNode, isForcedFocus = false) => {
- const pIndex = allPanes.findIndex(p => p.id === pane.id);
- return (
-
- 1}
- onFocus={() => setFocusedPaneId(pane.id)}
- rootPath={config.rootPath}
- isZenMode={isZenMode}
- zenPadding={zenPadding}
- isMaximized={isMaximized}
- onMaximize={() => {
- setFocusedPaneId(pane.id);
- setIsMaximized(!isMaximized);
- }}
- onSplit={onSplitPane}
- onKill={onKillPane}
- onRename={onRenamePane}
- />
-
+ onMovePane?.(active.id as string, over.id as string, dropDirection);
+ }
+ };
+
+ const renderTerminalPane = useCallback(
+ (pane: PaneNode, isForcedFocus = false) => {
+ const pIndex = allPanes.findIndex((p) => p.id === pane.id);
+ return (
+
+ 1}
+ onFocus={() => setFocusedPaneId(pane.id)}
+ rootPath={config.rootPath}
+ isZenMode={isZenMode}
+ zenPadding={zenPadding}
+ showPaneHeaders={showPaneHeaders}
+ isMaximized={isMaximized}
+ onMaximize={() => {
+ setFocusedPaneId(pane.id);
+ setIsMaximized(!isMaximized);
+ }}
+ onSplit={onSplitPane}
+ onKill={onKillPane}
+ onRename={onRenamePane}
+ />
+
+ );
+ },
+ [
+ workspaceId,
+ activeDragId,
+ allPanes,
+ focusedPaneId,
+ config.rootPath,
+ isZenMode,
+ zenPadding,
+ isMaximized,
+ onSplitPane,
+ onKillPane,
+ onRenamePane,
+ ],
);
- }, [workspaceId, activeDragId, allPanes, focusedPaneId, config.rootPath, isZenMode, zenPadding, isMaximized, onSplitPane, onKillPane, onRenamePane]);
- // Recursive Layout Renderer
- const renderLayout = (node: LayoutNode): React.ReactNode => {
- if (node.type === 'pane') {
- return renderTerminalPane(node);
- }
+ // Recursive Layout Renderer
+ const renderLayout = (node: LayoutNode): React.ReactNode => {
+ if (node.type === "pane") {
+ return renderTerminalPane(node);
+ }
- const direction = node.direction;
- const orientation = direction === 'horizontal' ? 'horizontal' : 'vertical';
+ const direction = node.direction;
+ const orientation =
+ direction === "horizontal" ? "horizontal" : "vertical";
- const getFirstPaneId = (n: LayoutNode): string => {
- if (n.type === 'pane') return n.id;
- return getFirstPaneId(n.children[0]);
- };
+ const getFirstPaneId = (n: LayoutNode): string => {
+ if (n.type === "pane") return n.id;
+ return getFirstPaneId(n.children[0]);
+ };
+
+ const collectPanes = (
+ currentNode: LayoutNode,
+ targetDir: "horizontal" | "vertical",
+ ratioMultiplier: number = 1,
+ ): { node: LayoutNode; size: number }[] => {
+ if (currentNode.type === "pane") {
+ return [{ node: currentNode, size: ratioMultiplier * 100 }];
+ }
+
+ if (currentNode.direction === targetDir) {
+ return [
+ ...collectPanes(
+ currentNode.children[0],
+ targetDir,
+ ratioMultiplier * currentNode.ratio,
+ ),
+ ...collectPanes(
+ currentNode.children[1],
+ targetDir,
+ ratioMultiplier * (1 - currentNode.ratio),
+ ),
+ ];
+ }
- const collectPanes = (
- currentNode: LayoutNode,
- targetDir: 'horizontal' | 'vertical',
- ratioMultiplier: number = 1
- ): { node: LayoutNode; size: number }[] => {
- if (currentNode.type === 'pane') {
return [{ node: currentNode, size: ratioMultiplier * 100 }];
- }
+ };
- if (currentNode.direction === targetDir) {
- return [
- ...collectPanes(currentNode.children[0], targetDir, ratioMultiplier * currentNode.ratio),
- ...collectPanes(currentNode.children[1], targetDir, ratioMultiplier * (1 - currentNode.ratio))
- ];
- }
+ const flatPanes = collectPanes(node, direction);
- return [{ node: currentNode, size: ratioMultiplier * 100 }];
+ return (
+
+ {flatPanes.map((item, index) => {
+ const key =
+ item.node.type === "pane"
+ ? item.node.id
+ : `split-${getFirstPaneId(item.node)}`;
+ return (
+
+
+ {renderLayout(item.node)}
+
+ {index < flatPanes.length - 1 && }
+
+ );
+ })}
+
+ );
};
- const flatPanes = collectPanes(node, direction);
-
return (
-
- {flatPanes.map((item, index) => {
- const key = item.node.type === 'pane' ? item.node.id : `split-${getFirstPaneId(item.node)}`;
- return (
-
-
- {renderLayout(item.node)}
-
- {index < flatPanes.length - 1 && }
-
- );
- })}
-
- );
- };
-
- return (
-
-
-
- {isMobile ? (
-
+
+
+
+ {isMobile ? (
+
{allPanes.map((pane) => (
{renderTerminalPane(pane)}
))}
-
- ) : isMaximized && focusedPaneId ? (
+
+ ) : isMaximized && focusedPaneId ? (
// Maximized mode: render only the focused pane at full size, bypassing
// the resizable layout so handles and non-focused panes don't appear.
(() => {
- const focusedPane = allPanes.find(p => p.id === focusedPaneId);
+ const focusedPane = allPanes.find(
+ (p) => p.id === focusedPaneId,
+ );
if (focusedPane) {
return renderTerminalPane(focusedPane, true);
}
return renderLayout(layoutTree);
})()
) : (
- renderLayout(layoutTree)
- )}
-
+ renderLayout(layoutTree)
+ )}
+
+
-
- );
-});
+ );
+ },
+);
diff --git a/src/features/space/components/PaneElevator.tsx b/src/features/space/components/PaneElevator.tsx
index dec81f5..618d60c 100644
--- a/src/features/space/components/PaneElevator.tsx
+++ b/src/features/space/components/PaneElevator.tsx
@@ -70,7 +70,7 @@ export function PaneElevator({
{parseShortcutToKeys(shortcut, isMac).map((key, idx) => (
{key}
@@ -189,7 +189,7 @@ export function PaneElevator({
{/* Pane Index Counter (Stylized) */}
0{index + 1}
@@ -231,7 +231,7 @@ export function PaneElevator({
whileTap={{ scale: 0.95 }}
onClick={() => openUrl(dp.url)}
aria-label={`Connect to localhost:${dp.port}`}
- className="flex items-center gap-2 px-3 py-1 rounded-full border border-[var(--accent-primary)]/30 text-[10px] font-black cursor-pointer color-[var(--accent-primary)] bg-[var(--accent-primary)]/5 hover:bg-[var(--accent-primary)]/10 transition-all font-mono tracking-tight"
+ className="flex items-center gap-2 px-3 py-1 rounded-full border border-[var(--accent-primary)]/30 text-[10px] font-black cursor-pointer color-[var(--accent-primary)] bg-[var(--accent-primary)]/5 hover:bg-[var(--accent-primary)]/10 transition-all tracking-tight"
>
+{overflowPorts.length}
@@ -261,7 +261,7 @@ export function PaneElevator({
{overflowPorts.map((dp) => (
- openUrl(dp.url)} className="rounded-xl px-3 py-2 text-[11px] font-bold font-mono">
+ openUrl(dp.url)} className="rounded-xl px-3 py-2 text-[11px] font-bold">
localhost:{dp.port}
diff --git a/src/features/terminal/TerminalPane.tsx b/src/features/terminal/TerminalPane.tsx
index 37e83a0..7d12bae 100644
--- a/src/features/terminal/TerminalPane.tsx
+++ b/src/features/terminal/TerminalPane.tsx
@@ -13,6 +13,7 @@ interface TerminalPaneProps {
rootPath?: string;
isZenMode?: boolean;
zenPadding?: number;
+ showPaneHeaders?: boolean;
isMaximized?: boolean;
onMaximize?: () => void;
onSplit?: (id: string, direction: 'horizontal' | 'vertical') => void;
@@ -30,6 +31,7 @@ interface TerminalPaneProps {
rootPath,
isZenMode,
zenPadding,
+ showPaneHeaders,
isMaximized,
onMaximize,
onSplit,
@@ -141,6 +143,7 @@ interface TerminalPaneProps {
command={pane.command}
cwd={rootPath}
isZenMode={isZenMode}
+ showPaneHeaders={showPaneHeaders}
isMaximized={isMaximized}
onMaximize={onMaximize}
name={pane.name}
diff --git a/src/features/terminal/components/XtermTerminal.tsx b/src/features/terminal/components/XtermTerminal.tsx
index 6b9daeb..c94d4a0 100644
--- a/src/features/terminal/components/XtermTerminal.tsx
+++ b/src/features/terminal/components/XtermTerminal.tsx
@@ -6,7 +6,7 @@ import { openUrl } from '@tauri-apps/plugin-opener';
import { invoke } from '@tauri-apps/api/core';
import { motion, AnimatePresence } from 'framer-motion';
import { CornerDownLeft, X } from '@/components/ui/icons';
-import { useTheme } from '@/hooks/useTheme';
+import { useTheme, ThemePalette } from '@/hooks/useTheme';
import { useColorScheme } from '@/hooks/useColorScheme';
import { usePty } from '@/hooks/usePty';
import { Button } from "@/components/ui/button";
@@ -45,6 +45,7 @@ interface XtermTerminalProps {
command?: string;
cwd?: string;
isZenMode?: boolean;
+ showPaneHeaders?: boolean;
isMaximized?: boolean;
onMaximize?: () => void;
name?: string;
@@ -61,6 +62,7 @@ export function XtermTerminal({
command,
cwd,
isZenMode = false,
+ showPaneHeaders,
isMaximized = false,
onMaximize,
name,
@@ -82,6 +84,7 @@ export function XtermTerminal({
const [shortcuts, setShortcuts] = useState(SHORTCUT_DEFAULTS);
const [showFloatingHeader, setShowFloatingHeader] = useState(true);
const [headerVisibility, setHeaderVisibility] = useState<'hover' | 'always'>('hover');
+ const headerVisible = showPaneHeaders !== undefined ? showPaneHeaders : showFloatingHeader;
const [detectedPorts, setDetectedPorts] = useState([]);
const [pendingSnippet, setPendingSnippet] = useState(null);
@@ -134,39 +137,49 @@ export function XtermTerminal({
loadSettings();
}, []);
- const getThemePalette = useCallback((themeName: string, scheme: 'light' | 'dark') => {
+ const getThemePalette = useCallback((themeName: string, scheme: 'light' | 'dark'): ThemePalette => {
const themeDef = allThemes.find(t => t.id === themeName) || allThemes.find(t => t.id === 'cortex');
+ const defaultPalette: ThemePalette = {
+ bg: scheme === 'dark' ? '#09090b' : '#ffffff',
+ headerBg: scheme === 'dark' ? '#0f0f11' : '#f5f5f7',
+ footerBg: scheme === 'dark' ? '#09090b' : '#ffffff',
+ surface: scheme === 'dark' ? '#0f0f11' : '#ffffff',
+ border: scheme === 'dark' ? '#1d1d20' : '#d1d1d1',
+ textPrimary: scheme === 'dark' ? '#ffffff' : '#000000',
+ textSecondary: scheme === 'dark' ? '#A3A3A3' : '#525252',
+ accent: '#ffffff',
+ ansi: {}
+ };
+
if (!themeDef) {
- return {
- bg: scheme === 'dark' ? '#09090b' : '#ffffff',
- headerBg: scheme === 'dark' ? '#0f0f11' : '#f5f5f7',
- footerBg: scheme === 'dark' ? '#09090b' : '#ffffff',
- surface: scheme === 'dark' ? '#0f0f11' : '#ffffff',
- border: scheme === 'dark' ? '#1d1d20' : '#d1d1d1',
- textPrimary: scheme === 'dark' ? '#ffffff' : '#000000',
- textSecondary: scheme === 'dark' ? '#A3A3A3' : '#525252',
- accent: '#ffffff',
- ansi: {}
- };
+ return defaultPalette;
}
- if (scheme === 'light') {
- return themeDef.light || {
- bg: "#FAFAFA",
- headerBg: "#FFFFFF",
- footerBg: "#F0F0F0",
- surface: "#FFFFFF",
- border: "#E5E7EB",
- textPrimary: "#111827",
- textSecondary: "#4B5563",
- accent: themeDef.dark.accent,
- ansi: {
- ...themeDef.dark.ansi,
- black: '#111827',
- white: '#FFFFFF'
- }
- };
+
+ if (themeDef.light && themeDef.dark) {
+ return scheme === 'light' ? themeDef.light : themeDef.dark;
+ } else if (themeDef.light) {
+ return themeDef.light;
+ } else if (themeDef.dark) {
+ if (scheme === 'light' && themeDef.isLegacy) {
+ return {
+ bg: "#FAFAFA",
+ headerBg: "#FFFFFF",
+ footerBg: "#F0F0F0",
+ surface: "#FFFFFF",
+ border: "#E5E7EB",
+ textPrimary: "#111827",
+ textSecondary: "#4B5563",
+ accent: themeDef.dark.accent,
+ ansi: {
+ ...themeDef.dark.ansi,
+ black: '#111827',
+ white: '#FFFFFF'
+ }
+ };
+ }
+ return themeDef.dark;
}
- return themeDef.dark;
+ return defaultPalette;
}, [allThemes]);
const getActiveAnsiColors = useCallback((themeName: string, scheme: 'light' | 'dark') => {
@@ -493,6 +506,7 @@ export function XtermTerminal({
const initialFontFamily = getComputedStyle(root).getPropertyValue('--terminal-font-family').trim() || TERMINAL_DEFAULTS.fontFamily;
const initialLineHeight = parseFloat(getComputedStyle(root).getPropertyValue('--terminal-line-height').trim()) || TERMINAL_DEFAULTS.lineHeight;
const initialLetterSpacing = parseFloat(getComputedStyle(root).getPropertyValue('--terminal-letter-spacing').trim()) || TERMINAL_DEFAULTS.letterSpacing;
+ const initialFontWeight = getComputedStyle(root).getPropertyValue('--terminal-font-weight').trim() || TERMINAL_DEFAULTS.fontWeight || '400';
let initialSettings = { ...TERMINAL_DEFAULTS };
getSettingsGroup('terminal', TERMINAL_DEFAULTS).then((saved) => {
@@ -505,6 +519,7 @@ export function XtermTerminal({
cursorStyle: initialSettings.cursorStyle,
fontSize: initialFontSize,
fontFamily: initialFontFamily,
+ fontWeight: initialFontWeight as any,
lineHeight: initialLineHeight,
letterSpacing: initialLetterSpacing,
theme: {
@@ -734,6 +749,7 @@ export function XtermTerminal({
if (ts) {
xtermRef.current.options.fontSize = ts.fontSize;
xtermRef.current.options.fontFamily = `"${ts.fontFamily}", monospace`;
+ xtermRef.current.options.fontWeight = ts.fontWeight as any;
xtermRef.current.options.cursorBlink = false;
xtermRef.current.options.cursorStyle = ts.cursorStyle as 'block' | 'underline' | 'bar';
xtermRef.current.options.lineHeight = ts.lineHeight;
@@ -831,7 +847,7 @@ export function XtermTerminal({
el.removeEventListener('transitionend', onTransitionEnd);
clearTimeout(fallbackTimer);
};
- }, [isMaximized, isZenMode, showFloatingHeader, headerVisibility]);
+ }, [isMaximized, isZenMode, headerVisible, headerVisibility]);
useEffect(() => {
const handleFocus = () => { if (xtermRef.current && isFocused && isReady) xtermRef.current.focus(); };
@@ -919,7 +935,7 @@ export function XtermTerminal({
};
const getTerminalPaddingTop = () => {
- if (isZenMode || !showFloatingHeader) return '0px';
+ if (isZenMode || !headerVisible) return '0px';
return headerVisibility === 'always' ? '40px' : '0px';
};
@@ -936,7 +952,7 @@ export function XtermTerminal({
background: 'var(--bg-color)'
}}
>
- {showFloatingHeader && (
+ {headerVisible && (
void>();
function notify() {
- listeners.forEach(listener => listener(globalAgents, globalIsInitialized));
+ listeners.forEach((listener) => listener(globalAgents, globalIsInitialized));
}
async function writeToStore(agents: Agent[]) {
@@ -29,11 +29,11 @@ async function initializeGlobalAgents() {
try {
const saved = await getSetting("cortex_agents", DEFAULT_AGENTS);
-
+
// 1. Sync default properties from constants (now from JSON) to ensure updates reach users
- const initial = saved.map(agent => {
- const defaultAgent = DEFAULT_AGENTS.find(da => da.id === agent.id);
-
+ const initial = saved.map((agent) => {
+ const defaultAgent = DEFAULT_AGENTS.find((da) => da.id === agent.id);
+
// If it's a default agent, prioritize the latest installCommand and downloadUrl from the app bundle
if (defaultAgent) {
return {
@@ -50,26 +50,34 @@ async function initializeGlobalAgents() {
// 2. Add any NEW default agents that aren't in the saved list yet
const missingDefaults = DEFAULT_AGENTS.filter(
- da => !initial.some(ia => ia.id === da.id)
+ (da) => !initial.some((ia) => ia.id === da.id),
);
-
+
globalAgents = [...initial, ...missingDefaults];
globalIsInitialized = true;
notify();
// 3. Perform verification check asynchronously in the background
- const updated = await Promise.all(globalAgents.map(async (agent) => {
- try {
- const isInstalled = await invoke("check_command", { command: agent.command });
- return {
- ...agent,
- status: isInstalled ? 'installed' : agent.status === 'installed' ? 'not-installed' : agent.status
- } as Agent;
- } catch (e) {
- console.error("Verification failed for agent:", agent.label, e);
- return agent;
- }
- }));
+ const updated = await Promise.all(
+ globalAgents.map(async (agent) => {
+ try {
+ const isInstalled = await invoke("check_command", {
+ command: agent.command,
+ });
+ return {
+ ...agent,
+ status: isInstalled
+ ? "installed"
+ : agent.status === "installed"
+ ? "not-installed"
+ : agent.status,
+ } as Agent;
+ } catch (e) {
+ console.error("Verification failed for agent:", agent.label, e);
+ return agent;
+ }
+ }),
+ );
globalAgents = updated;
await writeToStore(updated);
@@ -84,8 +92,8 @@ async function initializeGlobalAgents() {
async function reloadFromStore() {
try {
const saved = await getSetting("cortex_agents", DEFAULT_AGENTS);
- const initial = saved.map(agent => {
- const defaultAgent = DEFAULT_AGENTS.find(da => da.id === agent.id);
+ const initial = saved.map((agent) => {
+ const defaultAgent = DEFAULT_AGENTS.find((da) => da.id === agent.id);
if (defaultAgent) {
return {
...agent,
@@ -99,9 +107,9 @@ async function reloadFromStore() {
});
const missingDefaults = DEFAULT_AGENTS.filter(
- da => !initial.some(ia => ia.id === da.id)
+ (da) => !initial.some((ia) => ia.id === da.id),
);
-
+
globalAgents = [...initial, ...missingDefaults];
notify();
} catch (e) {
@@ -110,9 +118,9 @@ async function reloadFromStore() {
}
// Global listener for settings changes
-if (typeof window !== 'undefined') {
- window.addEventListener('cortex-settings-changed', reloadFromStore);
- window.addEventListener('cortex:agents-updated', reloadFromStore);
+if (typeof window !== "undefined") {
+ window.addEventListener("cortex-settings-changed", reloadFromStore);
+ window.addEventListener("cortex:agents-updated", reloadFromStore);
}
export function useAgents() {
@@ -125,7 +133,7 @@ export function useAgents() {
setIsInitialized(nextInitialized);
};
listeners.add(listener);
-
+
// Trigger initialization if not started
if (!globalIsInitialized && !globalIsInitializing) {
initializeGlobalAgents();
@@ -136,143 +144,270 @@ export function useAgents() {
};
}, []);
- const updateAgentStatus = useCallback(async (id: string, status: Agent['status'], errorMessage?: string) => {
- globalAgents = globalAgents.map(a =>
- a.id === id ? { ...a, status, errorMessage: errorMessage ?? (status !== 'error' ? undefined : a.errorMessage) } : a
- );
- notify();
- await writeToStore(globalAgents);
- window.dispatchEvent(new Event('cortex:agents-updated'));
- }, []);
+ const updateAgentStatus = useCallback(
+ async (id: string, status: Agent["status"], errorMessage?: string) => {
+ globalAgents = globalAgents.map((a) =>
+ a.id === id
+ ? {
+ ...a,
+ status,
+ errorMessage:
+ errorMessage ??
+ (status !== "error" ? undefined : a.errorMessage),
+ }
+ : a,
+ );
+ notify();
+ await writeToStore(globalAgents);
+ window.dispatchEvent(new Event("cortex:agents-updated"));
+ },
+ [],
+ );
- const addAgent = useCallback(async (label: string, command: string, installCommand?: string, downloadUrl?: string) => {
- const trimmedLabel = label?.trim();
- const trimmedCommand = command?.trim();
- const trimmedInstallCommand = installCommand?.trim();
- const trimmedDownloadUrl = downloadUrl?.trim();
-
- if (!trimmedCommand) {
- toast.error("Failed to add agent", {
- description: "Enter a valid command to register the agent."
- });
- return;
- }
+ const installAgent = useCallback(
+ async (id: string) => {
+ const agent = globalAgents.find((a) => a.id === id);
+ if (!agent) return;
+
+ // Clear any previous error and mark as installing
+ await updateAgentStatus(id, "installing", undefined);
+
+ try {
+ if (agent.installCommand) {
+ await invoke("install_agent_cli", { command: agent.installCommand });
+ await new Promise((resolve) => setTimeout(resolve, 1500));
+ } else {
+ await new Promise((resolve) => setTimeout(resolve, 1000));
+ }
+
+ // Invoke succeeded — optimistically mark installed. PATH refresh needs new shell.
+ await updateAgentStatus(id, "installed", undefined);
+
+ try {
+ const confirmedInPath = await invoke("check_command", {
+ command: agent.command,
+ });
+ if (!confirmedInPath) {
+ toast.success(`${agent.label} installed successfully`, {
+ description:
+ "Restart Cortex or open a new terminal for the command to be available in PATH.",
+ duration: 7000,
+ });
+ } else {
+ toast.success(`${agent.label} installed successfully`, {
+ description: "The agent is ready for deployment.",
+ });
+ }
+ } catch {
+ toast.success(`${agent.label} installed successfully`, {
+ description: "The agent is ready for deployment.",
+ });
+ }
+ } catch (e: any) {
+ const detail =
+ typeof e === "string"
+ ? e
+ : (e?.message ?? "An unexpected error occurred.");
+ await updateAgentStatus(id, "error", detail);
+ toast.error(`Failed to install ${agent.label}`, {
+ description:
+ 'Click "View Error" on the agent card to see full details.',
+ duration: 6000,
+ });
+ }
+ },
+ [updateAgentStatus],
+ );
+
+ const addAgent = useCallback(
+ async (
+ label: string,
+ command: string,
+ installCommand?: string,
+ downloadUrl?: string,
+ icon?: string,
+ ) => {
+ const trimmedLabel = label?.trim();
+ const trimmedCommand = command?.trim();
+ const trimmedInstallCommand = installCommand?.trim();
+ const trimmedDownloadUrl = downloadUrl?.trim();
+ const trimmedIcon = icon?.trim();
+
+ if (!trimmedCommand) {
+ toast.error("Failed to add agent", {
+ description: "Enter a valid command to register the agent.",
+ });
+ return;
+ }
+
+ const isDuplicate = globalAgents.some(
+ (a) => a.command.trim() === trimmedCommand,
+ );
+
+ if (isDuplicate) {
+ toast.error("Agent cannot be added", {
+ description: "An agent with this command already exists.",
+ });
+ return;
+ }
+
+ let finalLabel = trimmedLabel || trimmedCommand.toUpperCase();
+
+ // Normalize standard names like 'freebuff' to 'Freebuff'
+ if (finalLabel.toLowerCase() === "freebuff") {
+ finalLabel = "Freebuff";
+ }
+
+ const newAgent: Agent = {
+ id: crypto.randomUUID(),
+ label: finalLabel,
+ command: trimmedCommand,
+ status: "not-installed",
+ downloadUrl: trimmedDownloadUrl || undefined,
+ installCommand: trimmedInstallCommand || undefined,
+ isDefault: false,
+ icon: trimmedIcon || undefined,
+ };
- const isDuplicate = globalAgents.some(a => a.command.trim() === trimmedCommand);
-
- if (isDuplicate) {
- toast.error("Agent cannot be added", {
- description: "An agent with this command already exists."
+ globalAgents = [...globalAgents, newAgent];
+ notify();
+ await writeToStore(globalAgents);
+ window.dispatchEvent(new Event("cortex:agents-updated"));
+
+ toast.success(`${newAgent.label} added successfully`, {
+ description: "The agent is now in your agent library.",
});
- return;
- }
- let finalLabel = trimmedLabel || trimmedCommand.toUpperCase();
-
- // Normalize standard names like 'freebuff' to 'Freebuff'
- if (finalLabel.toLowerCase() === 'freebuff') {
- finalLabel = 'Freebuff';
- }
+ // Check if it's already installed
+ try {
+ const isInstalled = await invoke("check_command", {
+ command: trimmedCommand,
+ });
+ if (isInstalled) {
+ await updateAgentStatus(newAgent.id, "installed");
+ } else if (trimmedInstallCommand) {
+ // Auto trigger installation!
+ installAgent(newAgent.id);
+ }
+ } catch (e) {
+ console.error("Verification failed for new agent:", newAgent.label, e);
+ }
+ },
+ [updateAgentStatus, installAgent],
+ );
- const newAgent: Agent = {
- id: crypto.randomUUID(),
- label: finalLabel,
- command: trimmedCommand,
- status: 'not-installed',
- downloadUrl: trimmedDownloadUrl || undefined,
- installCommand: trimmedInstallCommand || undefined,
- isDefault: false
- };
+ const editAgent = useCallback(
+ async (
+ id: string,
+ label: string,
+ command: string,
+ installCommand?: string,
+ downloadUrl?: string,
+ icon?: string,
+ ) => {
+ const trimmedLabel = label?.trim();
+ const trimmedCommand = command?.trim();
+ const trimmedInstallCommand = installCommand?.trim();
+ const trimmedDownloadUrl = downloadUrl?.trim();
+ const trimmedIcon = icon?.trim();
- globalAgents = [...globalAgents, newAgent];
- notify();
- await writeToStore(globalAgents);
- window.dispatchEvent(new Event('cortex:agents-updated'));
-
- toast.success(`${newAgent.label} added successfully`, {
- description: "The agent is now in your agent library."
- });
-
- // Check if it's already installed
- try {
- const isInstalled = await invoke("check_command", { command: trimmedCommand });
- if (isInstalled) {
- await updateAgentStatus(newAgent.id, 'installed');
+ if (!trimmedCommand) {
+ toast.error("Failed to update agent", {
+ description: "Enter a valid command to update the agent.",
+ });
+ return;
}
- } catch (e) {
- console.error("Verification failed for new agent:", newAgent.label, e);
- }
- }, [updateAgentStatus]);
+
+ const isDuplicate = globalAgents.some(
+ (a) => a.id !== id && a.command.trim() === trimmedCommand,
+ );
+
+ if (isDuplicate) {
+ toast.error("Agent cannot be updated", {
+ description: "An agent with this command already exists.",
+ });
+ return;
+ }
+
+ let finalLabel = trimmedLabel || trimmedCommand.toUpperCase();
+ if (finalLabel.toLowerCase() === "freebuff") {
+ finalLabel = "Freebuff";
+ }
+
+ const existingAgent = globalAgents.find((a) => a.id === id);
+ if (!existingAgent) return;
+
+ const isCommandChanged = existingAgent.command !== trimmedCommand;
+ const isInstallCommandChanged = existingAgent.installCommand !== trimmedInstallCommand;
+
+ globalAgents = globalAgents.map((a) =>
+ a.id === id
+ ? {
+ ...a,
+ label: finalLabel,
+ command: trimmedCommand,
+ installCommand: trimmedInstallCommand || undefined,
+ downloadUrl: trimmedDownloadUrl || undefined,
+ icon: trimmedIcon || undefined,
+ status: isCommandChanged ? "not-installed" : a.status,
+ }
+ : a,
+ );
+
+ notify();
+ await writeToStore(globalAgents);
+ window.dispatchEvent(new Event("cortex:agents-updated"));
+
+ toast.success(`${finalLabel} updated successfully`);
+
+ if (isCommandChanged) {
+ try {
+ const isInstalled = await invoke("check_command", {
+ command: trimmedCommand,
+ });
+ if (isInstalled) {
+ await updateAgentStatus(id, "installed");
+ } else if (trimmedInstallCommand) {
+ installAgent(id);
+ }
+ } catch (e) {
+ console.error("Verification failed for updated agent:", finalLabel, e);
+ }
+ } else if (isInstallCommandChanged && trimmedInstallCommand) {
+ const updatedAgent = globalAgents.find((a) => a.id === id);
+ if (updatedAgent && updatedAgent.status !== "installed" && updatedAgent.status !== "installing") {
+ installAgent(id);
+ }
+ }
+ },
+ [updateAgentStatus, installAgent],
+ );
const deleteAgent = useCallback(async (id: string) => {
- const agent = globalAgents.find(a => a.id === id);
+ const agent = globalAgents.find((a) => a.id === id);
if (agent?.isDefault) {
toast.error("Agent cannot be deleted", {
- description: "Default agents must remain in your library."
+ description: "Default agents must remain in your library.",
});
return;
}
-
- globalAgents = globalAgents.filter(a => a.id !== id);
+
+ globalAgents = globalAgents.filter((a) => a.id !== id);
notify();
await writeToStore(globalAgents);
- window.dispatchEvent(new Event('cortex:agents-updated'));
-
- toast.success(`${agent?.label || 'Agent'} removed successfully`, {
- description: "The agent has been removed from your library."
+ window.dispatchEvent(new Event("cortex:agents-updated"));
+
+ toast.success(`${agent?.label || "Agent"} removed successfully`, {
+ description: "The agent has been removed from your library.",
});
}, []);
- const installAgent = useCallback(async (id: string) => {
- const agent = globalAgents.find(a => a.id === id);
- if (!agent) return;
-
- // Clear any previous error and mark as installing
- await updateAgentStatus(id, 'installing', undefined);
-
- try {
- if (agent.installCommand) {
- await invoke("install_agent_cli", { command: agent.installCommand });
- await new Promise(resolve => setTimeout(resolve, 1500));
- } else {
- await new Promise(resolve => setTimeout(resolve, 1000));
- }
-
- // Invoke succeeded — optimistically mark installed. PATH refresh needs new shell.
- await updateAgentStatus(id, 'installed', undefined);
-
- try {
- const confirmedInPath = await invoke("check_command", { command: agent.command });
- if (!confirmedInPath) {
- toast.success(`${agent.label} installed successfully`, {
- description: "Restart Cortex or open a new terminal for the command to be available in PATH.",
- duration: 7000,
- });
- } else {
- toast.success(`${agent.label} installed successfully`, {
- description: "The agent is ready for deployment.",
- });
- }
- } catch {
- toast.success(`${agent.label} installed successfully`, {
- description: "The agent is ready for deployment.",
- });
- }
- } catch (e: any) {
- const detail = typeof e === 'string' ? e : (e?.message ?? "An unexpected error occurred.");
- await updateAgentStatus(id, 'error', detail);
- toast.error(`Failed to install ${agent.label}`, {
- description: "Click \"View Error\" on the agent card to see full details.",
- duration: 6000,
- });
- }
- }, [updateAgentStatus]);
-
return {
agents,
addAgent,
+ editAgent,
deleteAgent,
installAgent,
- isInitialized
+ isInitialized,
};
}
diff --git a/src/hooks/useAppShortcuts.ts b/src/hooks/useAppShortcuts.ts
index 2d18f16..e70d4b3 100644
--- a/src/hooks/useAppShortcuts.ts
+++ b/src/hooks/useAppShortcuts.ts
@@ -1,7 +1,11 @@
import { useEffect, useState } from "react";
import { toast } from "sonner";
-import { Workspace, Mode } from "../types";
-import { getSettingsGroup, SHORTCUT_DEFAULTS, ShortcutSettings } from "@/lib/store";
+import { Workspace, Mode } from "../lib";
+import {
+ getSettingsGroup,
+ SHORTCUT_DEFAULTS,
+ ShortcutSettings,
+} from "@/lib/store";
import { matchesShortcut } from "@/lib/shortcut-utils";
interface UseAppShortcutsProps {
@@ -31,13 +35,16 @@ export function useAppShortcuts({
onToggleSettings,
onToggleZenMode,
onToggleSwitcher,
- onSelectMode
+ onSelectMode,
}: UseAppShortcutsProps) {
- const [shortcuts, setShortcuts] = useState(SHORTCUT_DEFAULTS);
+ const [shortcuts, setShortcuts] =
+ useState(SHORTCUT_DEFAULTS);
useEffect(() => {
- getSettingsGroup('shortcuts', SHORTCUT_DEFAULTS).then(setShortcuts);
-
+ getSettingsGroup("shortcuts", SHORTCUT_DEFAULTS).then(
+ setShortcuts,
+ );
+
// Listen for setting changes
const handleSettingsChange = (e: Event) => {
const evt = e as CustomEvent<{ shortcuts?: ShortcutSettings }>;
@@ -45,8 +52,12 @@ export function useAppShortcuts({
setShortcuts(evt.detail.shortcuts);
}
};
- window.addEventListener('cortex-settings-changed', handleSettingsChange);
- return () => window.removeEventListener('cortex-settings-changed', handleSettingsChange);
+ window.addEventListener("cortex-settings-changed", handleSettingsChange);
+ return () =>
+ window.removeEventListener(
+ "cortex-settings-changed",
+ handleSettingsChange,
+ );
}, []);
useEffect(() => {
@@ -59,7 +70,7 @@ export function useAppShortcuts({
}
// 0.1 Escape to Exit Zen Mode
- if (e.key === 'Escape' && isZenMode) {
+ if (e.key === "Escape" && isZenMode) {
e.preventDefault();
onToggleZenMode();
return;
@@ -69,7 +80,9 @@ export function useAppShortcuts({
if (matchesShortcut(e, shortcuts.newWorkspace)) {
e.preventDefault();
onNewWorkspaceFlow();
- toast.success("New workspace initiated successfully", { description: "Configure your new separate workspace now." });
+ toast.success("New workspace initiated successfully", {
+ description: "Configure your new separate workspace now.",
+ });
return;
}
@@ -86,7 +99,9 @@ export function useAppShortcuts({
if (matchesShortcut(e, shortcuts.cycleNextWorkspace)) {
e.preventDefault();
if (workspaces.length <= 1) return;
- const currentIndex = workspaces.findIndex(w => w.id === activeWorkspaceId);
+ const currentIndex = workspaces.findIndex(
+ (w) => w.id === activeWorkspaceId,
+ );
const nextIndex = (currentIndex + 1) % workspaces.length;
onSwitchWorkspace(workspaces[nextIndex].id);
return;
@@ -96,8 +111,11 @@ export function useAppShortcuts({
if (matchesShortcut(e, shortcuts.cyclePrevWorkspace)) {
e.preventDefault();
if (workspaces.length <= 1) return;
- const currentIndex = workspaces.findIndex(w => w.id === activeWorkspaceId);
- const prevIndex = (currentIndex - 1 + workspaces.length) % workspaces.length;
+ const currentIndex = workspaces.findIndex(
+ (w) => w.id === activeWorkspaceId,
+ );
+ const prevIndex =
+ (currentIndex - 1 + workspaces.length) % workspaces.length;
onSwitchWorkspace(workspaces[prevIndex].id);
return;
}
@@ -132,25 +150,39 @@ export function useAppShortcuts({
// 8. Mode Selection (Ctrl+N, Ctrl+A)
if (matchesShortcut(e, shortcuts.switchNormalMode)) {
- const active = workspaces.find(w => w.id === activeWorkspaceId);
- if (active && active.status === 'mode-select') {
+ const active = workspaces.find((w) => w.id === activeWorkspaceId);
+ if (active && active.status === "mode-select") {
e.preventDefault();
- onSelectMode('normal');
+ onSelectMode("normal");
}
return;
}
if (matchesShortcut(e, shortcuts.switchAgentsMode)) {
- const active = workspaces.find(w => w.id === activeWorkspaceId);
- if (active && active.status === 'mode-select') {
+ const active = workspaces.find((w) => w.id === activeWorkspaceId);
+ if (active && active.status === "mode-select") {
e.preventDefault();
- onSelectMode('agents');
+ onSelectMode("agents");
}
return;
}
};
- window.addEventListener('keydown', handleKeyDown);
- return () => window.removeEventListener('keydown', handleKeyDown);
- }, [shortcuts, workspaces, activeWorkspaceId, isZenMode, onNewWorkspaceFlow, onCloseWorkspace, onSwitchWorkspace, onToggleShortcuts, onToggleTemplates, onToggleSettings, onToggleZenMode, onToggleSwitcher, onSelectMode]);
+ window.addEventListener("keydown", handleKeyDown);
+ return () => window.removeEventListener("keydown", handleKeyDown);
+ }, [
+ shortcuts,
+ workspaces,
+ activeWorkspaceId,
+ isZenMode,
+ onNewWorkspaceFlow,
+ onCloseWorkspace,
+ onSwitchWorkspace,
+ onToggleShortcuts,
+ onToggleTemplates,
+ onToggleSettings,
+ onToggleZenMode,
+ onToggleSwitcher,
+ onSelectMode,
+ ]);
}
diff --git a/src/hooks/useColorScheme.ts b/src/hooks/useColorScheme.ts
index 6a47a2a..7b1b63d 100644
--- a/src/hooks/useColorScheme.ts
+++ b/src/hooks/useColorScheme.ts
@@ -117,6 +117,18 @@ export function useColorScheme() {
await setSetting(`${PREFIX}.reducedMotion`, reduced);
}, []);
+ const setShimmerPreset = useCallback(async (preset: string) => {
+ globalSettings = { ...globalSettings, shimmerPreset: preset };
+ notifyListeners();
+ await setSetting(`${PREFIX}.shimmerPreset`, preset);
+ }, []);
+
+ const setShimmerDuration = useCallback(async (duration: number) => {
+ globalSettings = { ...globalSettings, shimmerDuration: duration };
+ notifyListeners();
+ await setSetting(`${PREFIX}.shimmerDuration`, duration);
+ }, []);
+
const resetToDefaults = useCallback(async () => {
globalSettings = APPEARANCE_DEFAULTS;
globalResolvedScheme = applyColorScheme(APPEARANCE_DEFAULTS.colorScheme);
@@ -125,5 +137,5 @@ export function useColorScheme() {
await setSettingsGroup(PREFIX, APPEARANCE_DEFAULTS);
}, []);
- return { settings, resolvedScheme, isLoaded, setColorScheme, setUiFontScale, setZenPadding, setReducedMotion, resetToDefaults };
+ return { settings, resolvedScheme, isLoaded, setColorScheme, setUiFontScale, setZenPadding, setReducedMotion, setShimmerPreset, setShimmerDuration, resetToDefaults };
}
diff --git a/src/hooks/usePresets.ts b/src/hooks/usePresets.ts
index dbcaabf..20e67a1 100644
--- a/src/hooks/usePresets.ts
+++ b/src/hooks/usePresets.ts
@@ -2,7 +2,7 @@ import { useState, useEffect, useCallback, useRef } from "react";
import { toast } from "sonner";
import { DEFAULT_PRESETS } from "@/lib/setup-constants";
import { getSetting, setSetting } from "@/lib/store";
-import { DirectoryPreset } from "@/types";
+import { DirectoryPreset } from "@/lib";
export function usePresets(rootPath: string, isValidDir: boolean | null) {
const [presets, setPresets] = useState([]);
@@ -11,11 +11,14 @@ export function usePresets(rootPath: string, isValidDir: boolean | null) {
useEffect(() => {
async function init() {
- const saved = await getSetting("cortex_presets", DEFAULT_PRESETS);
+ const saved = await getSetting(
+ "cortex_presets",
+ DEFAULT_PRESETS,
+ );
// Ensure all saved presets have IDs (migration for old data)
- const sanitized = saved.map(p => ({
+ const sanitized = saved.map((p) => ({
...p,
- id: p.id || crypto.randomUUID()
+ id: p.id || crypto.randomUUID(),
}));
setPresets(sanitized);
setIsInitialized(true);
@@ -23,8 +26,9 @@ export function usePresets(rootPath: string, isValidDir: boolean | null) {
init();
const handleSync = () => init();
- window.addEventListener('cortex:assets-updated', handleSync);
- return () => window.removeEventListener('cortex:assets-updated', handleSync);
+ window.addEventListener("cortex:assets-updated", handleSync);
+ return () =>
+ window.removeEventListener("cortex:assets-updated", handleSync);
}, []);
useEffect(() => {
@@ -37,7 +41,11 @@ export function usePresets(rootPath: string, isValidDir: boolean | null) {
const normalizePath = (p: string) => {
if (!p) return "";
// Remove trailing slashes, replace forward with backslash, and lowercase
- return p.replace(/[\\/]+$/, "").replace(/\//g, "\\").toLowerCase().trim();
+ return p
+ .replace(/[\\/]+$/, "")
+ .replace(/\//g, "\\")
+ .toLowerCase()
+ .trim();
};
const addPreset = useCallback(() => {
@@ -47,7 +55,7 @@ export function usePresets(rootPath: string, isValidDir: boolean | null) {
const now = Date.now();
if (now - lastAddRef.current < 400) return;
lastAddRef.current = now;
-
+
if (isValidDir === false) {
toast.error("Failed to save preset", {
id: "preset-invalid",
@@ -57,10 +65,13 @@ export function usePresets(rootPath: string, isValidDir: boolean | null) {
}
const normalizedTarget = normalizePath(targetPath);
- const name = targetPath.split(/[\\/]/).filter(Boolean).pop() || "NEW PRESET";
+ const name =
+ targetPath.split(/[\\/]/).filter(Boolean).pop() || "NEW PRESET";
- setPresets(prev => {
- const isDuplicate = prev.some(p => normalizePath(p.path) === normalizedTarget);
+ setPresets((prev) => {
+ const isDuplicate = prev.some(
+ (p) => normalizePath(p.path) === normalizedTarget,
+ );
if (isDuplicate) {
toast.error("Preset cannot be added", {
@@ -70,24 +81,24 @@ export function usePresets(rootPath: string, isValidDir: boolean | null) {
return prev;
}
- const newPreset: DirectoryPreset = {
+ const newPreset: DirectoryPreset = {
id: crypto.randomUUID(),
- label: name.toUpperCase(),
- path: targetPath
+ label: name.toUpperCase(),
+ path: targetPath,
};
-
+
toast.success(`${name.toUpperCase()} saved successfully`, {
id: `preset-save-${normalizedTarget}`,
description: "The directory has been added to your presets.",
});
-
+
return [...prev, newPreset];
});
}, [rootPath, isValidDir]);
const removePreset = useCallback((path: string) => {
- setPresets(prev => {
- const presetToRemove = prev.find(p => p.path === path);
+ setPresets((prev) => {
+ const presetToRemove = prev.find((p) => p.path === path);
if (!presetToRemove) return prev;
toast.info(`${presetToRemove.label} removed successfully`, {
@@ -95,15 +106,15 @@ export function usePresets(rootPath: string, isValidDir: boolean | null) {
description: "The preset has been deleted from your library.",
action: {
label: "Undo",
- onClick: () => setPresets(old => [...old, presetToRemove])
+ onClick: () => setPresets((old) => [...old, presetToRemove]),
},
cancel: {
label: "Dismiss",
- onClick: () => {}
+ onClick: () => {},
},
});
- return prev.filter(p => p.path !== path);
+ return prev.filter((p) => p.path !== path);
});
}, []);
@@ -111,6 +122,6 @@ export function usePresets(rootPath: string, isValidDir: boolean | null) {
presets,
setPresets,
addPreset,
- removePreset
+ removePreset,
};
}
diff --git a/src/hooks/useSetupPanes.ts b/src/hooks/useSetupPanes.ts
index ae7731b..252e276 100644
--- a/src/hooks/useSetupPanes.ts
+++ b/src/hooks/useSetupPanes.ts
@@ -1,29 +1,46 @@
import { useState, useMemo, useEffect, useCallback } from "react";
-import { LayoutType, LayoutConfig, SavedLayout, INITIAL_LAYOUTS, PaneConfig } from "@/lib/setup-constants";
+import {
+ LayoutType,
+ LayoutConfig,
+ SavedLayout,
+ INITIAL_LAYOUTS,
+ PaneConfig,
+} from "@/lib/setup-constants";
import { getPaneCount, derivePaneName } from "@/lib/setup-utils";
-import { getSetting, setSetting, SemanticsSettings, SEMANTICS_DEFAULTS } from "@/lib/store";
+import {
+ getSetting,
+ setSetting,
+ SemanticsSettings,
+ SEMANTICS_DEFAULTS,
+} from "@/lib/store";
import { toast } from "sonner";
-import { Agent } from "@/types";
+import { Agent } from "@/lib";
export function useSetupPanes(agents: Agent[] = []) {
const [layoutType, setLayoutType] = useState("2x2");
- const [customLayout, setCustomLayout] = useState({ type: 'grid', rows: 2, cols: 2 });
- const [savedLayouts, setSavedLayouts] = useState(INITIAL_LAYOUTS);
- const [semantics, setSemantics] = useState(SEMANTICS_DEFAULTS);
+ const [customLayout, setCustomLayout] = useState({
+ type: "grid",
+ rows: 2,
+ cols: 2,
+ });
+ const [savedLayouts, setSavedLayouts] =
+ useState(INITIAL_LAYOUTS);
+ const [semantics, setSemantics] =
+ useState