Merge branch 'main' into various-improvements

This commit is contained in:
Cody Seibert
2025-12-10 14:39:07 -05:00
25 changed files with 4430 additions and 459 deletions

View File

@@ -65,9 +65,7 @@ import {
import { Button } from "@/components/ui/button";
import {
useKeyboardShortcuts,
NAV_SHORTCUTS,
UI_SHORTCUTS,
ACTION_SHORTCUTS,
useKeyboardShortcutsConfig,
KeyboardShortcut,
} from "@/hooks/use-keyboard-shortcuts";
import { getElectronAPI, Project, TrashedProject } from "@/lib/electron";
@@ -213,6 +211,9 @@ export function Sidebar() {
theme: globalTheme,
} = useAppStore();
// Get customizable keyboard shortcuts
const shortcuts = useKeyboardShortcutsConfig();
// State for project picker dropdown
const [isProjectPickerOpen, setIsProjectPickerOpen] = useState(false);
const [projectSearchQuery, setProjectSearchQuery] = useState("");
@@ -527,13 +528,13 @@ export function Sidebar() {
id: "board",
label: "Kanban Board",
icon: LayoutGrid,
shortcut: NAV_SHORTCUTS.board,
shortcut: shortcuts.board,
},
{
id: "agent",
label: "Agent Runner",
icon: Bot,
shortcut: NAV_SHORTCUTS.agent,
shortcut: shortcuts.agent,
},
],
},
@@ -544,25 +545,25 @@ export function Sidebar() {
id: "spec",
label: "Spec Editor",
icon: FileText,
shortcut: NAV_SHORTCUTS.spec,
shortcut: shortcuts.spec,
},
{
id: "context",
label: "Context",
icon: BookOpen,
shortcut: NAV_SHORTCUTS.context,
shortcut: shortcuts.context,
},
{
id: "tools",
label: "Agent Tools",
icon: Wrench,
shortcut: NAV_SHORTCUTS.tools,
shortcut: shortcuts.tools,
},
{
id: "profiles",
label: "AI Profiles",
icon: UserCircle,
shortcut: NAV_SHORTCUTS.profiles,
shortcut: shortcuts.profiles,
},
],
},
@@ -610,26 +611,26 @@ export function Sidebar() {
// Build keyboard shortcuts for navigation
const navigationShortcuts: KeyboardShortcut[] = useMemo(() => {
const shortcuts: KeyboardShortcut[] = [];
const shortcutsList: KeyboardShortcut[] = [];
// Sidebar toggle shortcut - always available
shortcuts.push({
key: UI_SHORTCUTS.toggleSidebar,
shortcutsList.push({
key: shortcuts.toggleSidebar,
action: () => toggleSidebar(),
description: "Toggle sidebar",
});
// Open project shortcut - opens the folder selection dialog directly
shortcuts.push({
key: ACTION_SHORTCUTS.openProject,
shortcutsList.push({
key: shortcuts.openProject,
action: () => handleOpenFolder(),
description: "Open folder selection dialog",
});
// Project picker shortcut - only when we have projects
if (projects.length > 0) {
shortcuts.push({
key: ACTION_SHORTCUTS.projectPicker,
shortcutsList.push({
key: shortcuts.projectPicker,
action: () => setIsProjectPickerOpen((prev) => !prev),
description: "Toggle project picker",
});
@@ -637,13 +638,13 @@ export function Sidebar() {
// Project cycling shortcuts - only when we have project history
if (projectHistory.length > 1) {
shortcuts.push({
key: ACTION_SHORTCUTS.cyclePrevProject,
shortcutsList.push({
key: shortcuts.cyclePrevProject,
action: () => cyclePrevProject(),
description: "Cycle to previous project (MRU)",
});
shortcuts.push({
key: ACTION_SHORTCUTS.cycleNextProject,
shortcutsList.push({
key: shortcuts.cycleNextProject,
action: () => cycleNextProject(),
description: "Cycle to next project (LRU)",
});
@@ -654,7 +655,7 @@ export function Sidebar() {
navSections.forEach((section) => {
section.items.forEach((item) => {
if (item.shortcut) {
shortcuts.push({
shortcutsList.push({
key: item.shortcut,
action: () => setCurrentView(item.id as any),
description: `Navigate to ${item.label}`,
@@ -664,15 +665,16 @@ export function Sidebar() {
});
// Add settings shortcut
shortcuts.push({
key: NAV_SHORTCUTS.settings,
shortcutsList.push({
key: shortcuts.settings,
action: () => setCurrentView("settings"),
description: "Navigate to Settings",
});
}
return shortcuts;
return shortcutsList;
}, [
shortcuts,
currentProject,
setCurrentView,
toggleSidebar,
@@ -681,6 +683,7 @@ export function Sidebar() {
projectHistory.length,
cyclePrevProject,
cycleNextProject,
navSections,
]);
// Register keyboard shortcuts
@@ -719,7 +722,7 @@ export function Sidebar() {
className="ml-1 px-1 py-0.5 bg-brand-500/10 border border-brand-500/30 rounded text-[10px] font-mono text-brand-400/70"
data-testid="sidebar-toggle-shortcut"
>
{UI_SHORTCUTS.toggleSidebar}
{shortcuts.toggleSidebar}
</span>
</div>
</button>
@@ -772,12 +775,12 @@ export function Sidebar() {
<button
onClick={handleOpenFolder}
className="group flex items-center justify-center flex-1 px-3 py-2.5 rounded-lg relative overflow-hidden transition-all text-muted-foreground hover:text-foreground hover:bg-sidebar-accent/50 border border-sidebar-border"
title={`Open Folder (${ACTION_SHORTCUTS.openProject})`}
title={`Open Folder (${shortcuts.openProject})`}
data-testid="open-project-button"
>
<FolderOpen className="w-4 h-4 shrink-0" />
<span className="hidden lg:flex items-center justify-center w-5 h-5 text-[10px] font-mono rounded bg-brand-500/10 border border-brand-500/30 text-brand-400/70 ml-2">
{ACTION_SHORTCUTS.openProject}
{shortcuts.openProject}
</span>
</button>
<button
@@ -819,7 +822,7 @@ export function Sidebar() {
className="hidden lg:flex items-center justify-center w-5 h-5 text-[10px] font-mono rounded bg-brand-500/10 border border-brand-500/30 text-brand-400/70"
data-testid="project-picker-shortcut"
>
{ACTION_SHORTCUTS.projectPicker}
{shortcuts.projectPicker}
</span>
<ChevronDown className="h-4 w-4 text-muted-foreground shrink-0" />
</div>
@@ -955,14 +958,14 @@ export function Sidebar() {
<Undo2 className="w-4 h-4 mr-2" />
<span className="flex-1">Previous</span>
<span className="text-[10px] font-mono text-muted-foreground ml-2">
{ACTION_SHORTCUTS.cyclePrevProject}
{shortcuts.cyclePrevProject}
</span>
</DropdownMenuItem>
<DropdownMenuItem onClick={cycleNextProject} data-testid="cycle-next-project">
<Redo2 className="w-4 h-4 mr-2" />
<span className="flex-1">Next</span>
<span className="text-[10px] font-mono text-muted-foreground ml-2">
{ACTION_SHORTCUTS.cycleNextProject}
{shortcuts.cycleNextProject}
</span>
</DropdownMenuItem>
<DropdownMenuItem onClick={clearProjectHistory} data-testid="clear-project-history">
@@ -1118,7 +1121,7 @@ export function Sidebar() {
)}
data-testid="shortcut-settings"
>
{NAV_SHORTCUTS.settings}
{shortcuts.settings}
</span>
)}
{!sidebarOpen && (

View File

@@ -25,19 +25,61 @@ import {
} from "lucide-react";
import { cn } from "@/lib/utils";
import type { SessionListItem } from "@/types/electron";
import { ACTION_SHORTCUTS } from "@/hooks/use-keyboard-shortcuts";
import { useKeyboardShortcutsConfig } from "@/hooks/use-keyboard-shortcuts";
import { useAppStore } from "@/store/app-store";
// Random session name generator
const adjectives = [
"Swift", "Bright", "Clever", "Dynamic", "Eager", "Focused", "Gentle", "Happy",
"Inventive", "Jolly", "Keen", "Lively", "Mighty", "Noble", "Optimal", "Peaceful",
"Quick", "Radiant", "Smart", "Tranquil", "Unique", "Vibrant", "Wise", "Zealous"
"Swift",
"Bright",
"Clever",
"Dynamic",
"Eager",
"Focused",
"Gentle",
"Happy",
"Inventive",
"Jolly",
"Keen",
"Lively",
"Mighty",
"Noble",
"Optimal",
"Peaceful",
"Quick",
"Radiant",
"Smart",
"Tranquil",
"Unique",
"Vibrant",
"Wise",
"Zealous",
];
const nouns = [
"Agent", "Builder", "Coder", "Developer", "Explorer", "Forge", "Garden", "Helper",
"Innovator", "Journey", "Kernel", "Lighthouse", "Mission", "Navigator", "Oracle",
"Project", "Quest", "Runner", "Spark", "Task", "Unicorn", "Voyage", "Workshop"
"Agent",
"Builder",
"Coder",
"Developer",
"Explorer",
"Forge",
"Garden",
"Helper",
"Innovator",
"Journey",
"Kernel",
"Lighthouse",
"Mission",
"Navigator",
"Oracle",
"Project",
"Quest",
"Runner",
"Spark",
"Task",
"Unicorn",
"Voyage",
"Workshop",
];
function generateRandomSessionName(): string {
@@ -62,13 +104,16 @@ export function SessionManager({
isCurrentSessionThinking = false,
onQuickCreateRef,
}: SessionManagerProps) {
const shortcuts = useKeyboardShortcutsConfig();
const [sessions, setSessions] = useState<SessionListItem[]>([]);
const [activeTab, setActiveTab] = useState<"active" | "archived">("active");
const [editingSessionId, setEditingSessionId] = useState<string | null>(null);
const [editingName, setEditingName] = useState("");
const [isCreating, setIsCreating] = useState(false);
const [newSessionName, setNewSessionName] = useState("");
const [runningSessions, setRunningSessions] = useState<Set<string>>(new Set());
const [runningSessions, setRunningSessions] = useState<Set<string>>(
new Set()
);
// Check running state for all sessions
const checkRunningSessions = async (sessionList: SessionListItem[]) => {
@@ -85,7 +130,10 @@ export function SessionManager({
}
} catch (err) {
// Ignore errors for individual session checks
console.warn(`[SessionManager] Failed to check running state for ${session.id}:`, err);
console.warn(
`[SessionManager] Failed to check running state for ${session.id}:`,
err
);
}
}
@@ -234,7 +282,8 @@ export function SessionManager({
const activeSessions = sessions.filter((s) => !s.isArchived);
const archivedSessions = sessions.filter((s) => s.isArchived);
const displayedSessions = activeTab === "active" ? activeSessions : archivedSessions;
const displayedSessions =
activeTab === "active" ? activeSessions : archivedSessions;
return (
<Card className="h-full flex flex-col">
@@ -246,10 +295,10 @@ export function SessionManager({
variant="default"
size="sm"
onClick={handleQuickCreateSession}
hotkey={ACTION_SHORTCUTS.newSession}
hotkey={shortcuts.newSession}
hotkeyActive={false}
data-testid="new-session-button"
title={`New Session (${ACTION_SHORTCUTS.newSession})`}
title={`New Session (${shortcuts.newSession})`}
>
<Plus className="w-4 h-4 mr-1" />
New
@@ -259,7 +308,9 @@ export function SessionManager({
<Tabs
value={activeTab}
onValueChange={(value) => setActiveTab(value as "active" | "archived")}
onValueChange={(value) =>
setActiveTab(value as "active" | "archived")
}
className="w-full"
>
<TabsList className="w-full">
@@ -275,7 +326,10 @@ export function SessionManager({
</Tabs>
</CardHeader>
<CardContent className="flex-1 overflow-y-auto space-y-2" data-testid="session-list">
<CardContent
className="flex-1 overflow-y-auto space-y-2"
data-testid="session-list"
>
{/* Create new session */}
{isCreating && (
<div className="p-3 border rounded-lg bg-muted/50">
@@ -330,8 +384,7 @@ export function SessionManager({
value={editingName}
onChange={(e) => setEditingName(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter")
handleRenameSession(session.id);
if (e.key === "Enter") handleRenameSession(session.id);
if (e.key === "Escape") {
setEditingSessionId(null);
setEditingName("");
@@ -368,13 +421,17 @@ export function SessionManager({
<>
<div className="flex items-center gap-2 mb-1">
{/* Show loading indicator if this session is running (either current session thinking or any session in runningSessions) */}
{((currentSessionId === session.id && isCurrentSessionThinking) || runningSessions.has(session.id)) ? (
{(currentSessionId === session.id &&
isCurrentSessionThinking) ||
runningSessions.has(session.id) ? (
<Loader2 className="w-4 h-4 text-primary animate-spin shrink-0" />
) : (
<MessageSquare className="w-4 h-4 text-muted-foreground shrink-0" />
)}
<h3 className="font-medium truncate">{session.name}</h3>
{((currentSessionId === session.id && isCurrentSessionThinking) || runningSessions.has(session.id)) && (
{((currentSessionId === session.id &&
isCurrentSessionThinking) ||
runningSessions.has(session.id)) && (
<span className="text-xs text-primary bg-primary/10 px-2 py-0.5 rounded-full">
thinking...
</span>
@@ -458,7 +515,9 @@ export function SessionManager({
<div className="text-center py-8 text-muted-foreground">
<MessageSquare className="w-12 h-12 mx-auto mb-2 opacity-50" />
<p className="text-sm">
{activeTab === "active" ? "No active sessions" : "No archived sessions"}
{activeTab === "active"
? "No active sessions"
: "No archived sessions"}
</p>
<p className="text-xs">
{activeTab === "active"

View File

@@ -203,8 +203,9 @@ export function HotkeyButton({
(event: KeyboardEvent) => {
if (!config || !hotkeyActive || disabled) return;
// Don't trigger when typing in inputs (unless explicitly scoped)
if (!scopeRef && isInputElement(document.activeElement)) {
// Don't trigger when typing in inputs (unless explicitly scoped or using cmdCtrl modifier)
// cmdCtrl shortcuts like Cmd+Enter should work even in inputs as they're intentional submit actions
if (!scopeRef && !config.cmdCtrl && isInputElement(document.activeElement)) {
return;
}

View File

@@ -26,12 +26,13 @@ import { Markdown } from "@/components/ui/markdown";
import type { ImageAttachment } from "@/store/app-store";
import {
useKeyboardShortcuts,
ACTION_SHORTCUTS,
useKeyboardShortcutsConfig,
KeyboardShortcut,
} from "@/hooks/use-keyboard-shortcuts";
export function AgentView() {
const { currentProject, setLastSelectedSession, getLastSelectedSession } = useAppStore();
const shortcuts = useKeyboardShortcutsConfig();
const [input, setInput] = useState("");
const [selectedImages, setSelectedImages] = useState<ImageAttachment[]>([]);
const [showImageDropZone, setShowImageDropZone] = useState(false);
@@ -417,12 +418,12 @@ export function AgentView() {
// Keyboard shortcuts for agent view
const agentShortcuts: KeyboardShortcut[] = useMemo(() => {
const shortcuts: KeyboardShortcut[] = [];
const shortcutsList: KeyboardShortcut[] = [];
// New session shortcut - only when in agent view with a project
if (currentProject) {
shortcuts.push({
key: ACTION_SHORTCUTS.newSession,
shortcutsList.push({
key: shortcuts.newSession,
action: () => {
if (quickCreateSessionRef.current) {
quickCreateSessionRef.current();
@@ -432,8 +433,8 @@ export function AgentView() {
});
}
return shortcuts;
}, [currentProject]);
return shortcutsList;
}, [currentProject, shortcuts]);
// Register keyboard shortcuts
useKeyboardShortcuts(agentShortcuts);
@@ -592,13 +593,16 @@ export function AgentView() {
<Card
className={cn(
"max-w-[80%]",
message.role === "user" &&
"bg-primary text-primary-foreground"
message.role === "user"
? "bg-primary text-primary-foreground"
: "border-l-4 border-primary bg-card"
)}
>
<CardContent className="p-3">
{message.role === "assistant" ? (
<Markdown className="text-sm">{message.content}</Markdown>
<Markdown className="text-sm text-primary prose-headings:text-primary prose-strong:text-primary prose-code:text-primary">
{message.content}
</Markdown>
) : (
<p className="text-sm whitespace-pre-wrap">
{message.content}
@@ -609,7 +613,7 @@ export function AgentView() {
"text-xs mt-2",
message.role === "user"
? "text-primary-foreground/70"
: "text-muted-foreground"
: "text-primary/70"
)}
>
{new Date(message.timestamp).toLocaleTimeString()}
@@ -624,11 +628,11 @@ export function AgentView() {
<div className="w-8 h-8 rounded-full bg-primary/10 flex items-center justify-center">
<Bot className="w-4 h-4 text-primary" />
</div>
<Card>
<Card className="border-l-4 border-primary bg-card">
<CardContent className="p-3">
<div className="flex items-center gap-2">
<Loader2 className="w-4 h-4 animate-spin" />
<span className="text-sm text-muted-foreground">
<Loader2 className="w-4 h-4 animate-spin text-primary" />
<span className="text-sm text-primary">
Thinking...
</span>
</div>

View File

@@ -98,7 +98,7 @@ import { Checkbox } from "@/components/ui/checkbox";
import { useAutoMode } from "@/hooks/use-auto-mode";
import {
useKeyboardShortcuts,
ACTION_SHORTCUTS,
useKeyboardShortcutsConfig,
KeyboardShortcut,
} from "@/hooks/use-keyboard-shortcuts";
import { useWindowState } from "@/hooks/use-window-state";
@@ -176,7 +176,10 @@ const CODEX_MODELS: ModelOption[] = [
];
// Profile icon mapping
const PROFILE_ICONS: Record<string, React.ComponentType<{ className?: string }>> = {
const PROFILE_ICONS: Record<
string,
React.ComponentType<{ className?: string }>
> = {
Brain,
Zap,
Scale,
@@ -203,6 +206,7 @@ export function BoardView() {
kanbanCardDetailLevel,
setKanbanCardDetailLevel,
} = useAppStore();
const shortcuts = useKeyboardShortcutsConfig();
const [activeFeature, setActiveFeature] = useState<Feature | null>(null);
const [editingFeature, setEditingFeature] = useState<Feature | null>(null);
const [showAddDialog, setShowAddDialog] = useState(false);
@@ -233,9 +237,8 @@ export function BoardView() {
DescriptionImagePath[]
>([]);
// Preview maps to persist image previews across tab switches
const [newFeaturePreviewMap, setNewFeaturePreviewMap] = useState<ImagePreviewMap>(
() => new Map()
);
const [newFeaturePreviewMap, setNewFeaturePreviewMap] =
useState<ImagePreviewMap>(() => new Map());
const [followUpPreviewMap, setFollowUpPreviewMap] = useState<ImagePreviewMap>(
() => new Map()
);
@@ -313,14 +316,14 @@ export function BoardView() {
// Keyboard shortcuts for this view
const boardShortcuts: KeyboardShortcut[] = useMemo(() => {
const shortcuts: KeyboardShortcut[] = [
const shortcutsList: KeyboardShortcut[] = [
{
key: ACTION_SHORTCUTS.addFeature,
key: shortcuts.addFeature,
action: () => setShowAddDialog(true),
description: "Add new feature",
},
{
key: ACTION_SHORTCUTS.startNext,
key: shortcuts.startNext,
action: () => startNextFeaturesRef.current(),
description: "Start next features from backlog",
},
@@ -335,7 +338,7 @@ export function BoardView() {
inProgressFeaturesForShortcuts.slice(0, 10).forEach((feature, index) => {
// Keys 1-9 for first 9 cards, 0 for 10th card
const key = index === 9 ? "0" : String(index + 1);
shortcuts.push({
shortcutsList.push({
key,
action: () => {
setOutputFeature(feature);
@@ -345,8 +348,8 @@ export function BoardView() {
});
});
return shortcuts;
}, [inProgressFeaturesForShortcuts]);
return shortcutsList;
}, [inProgressFeaturesForShortcuts, shortcuts]);
useKeyboardShortcuts(boardShortcuts);
// Prevent hydration issues
@@ -1510,7 +1513,10 @@ export function BoardView() {
const isSelected = selectedModel === option.id;
const isCodex = option.provider === "codex";
// Shorter display names for compact view
const shortName = option.label.replace("Claude ", "").replace("GPT-5.1 Codex ", "").replace("GPT-5.1 ", "");
const shortName = option.label
.replace("Claude ", "")
.replace("GPT-5.1 Codex ", "")
.replace("GPT-5.1 ", "");
return (
<button
key={option.id}
@@ -1626,7 +1632,7 @@ export function BoardView() {
<HotkeyButton
size="sm"
onClick={() => setShowAddDialog(true)}
hotkey={ACTION_SHORTCUTS.addFeature}
hotkey={shortcuts.addFeature}
hotkeyActive={false}
data-testid="add-feature-button"
>
@@ -1794,7 +1800,7 @@ export function BoardView() {
size="sm"
className="h-6 px-2 text-xs text-primary hover:text-primary hover:bg-primary/10"
onClick={handleStartNextFeatures}
hotkey={ACTION_SHORTCUTS.startNext}
hotkey={shortcuts.startNext}
hotkeyActive={false}
data-testid="start-next-button"
>
@@ -1867,26 +1873,29 @@ export function BoardView() {
</div>
{/* Add Feature Dialog */}
<Dialog open={showAddDialog} onOpenChange={(open) => {
setShowAddDialog(open);
// Clear preview map, validation error, and reset advanced options when dialog closes
if (!open) {
setNewFeaturePreviewMap(new Map());
setShowAdvancedOptions(false);
setDescriptionError(false);
}
}}>
<DialogContent
compact={!isMaximized}
data-testid="add-feature-dialog"
>
<Dialog
open={showAddDialog}
onOpenChange={(open) => {
setShowAddDialog(open);
// Clear preview map, validation error, and reset advanced options when dialog closes
if (!open) {
setNewFeaturePreviewMap(new Map());
setShowAdvancedOptions(false);
setDescriptionError(false);
}
}}
>
<DialogContent compact={!isMaximized} data-testid="add-feature-dialog">
<DialogHeader>
<DialogTitle>Add New Feature</DialogTitle>
<DialogDescription>
Create a new feature card for the Kanban board.
</DialogDescription>
</DialogHeader>
<Tabs defaultValue="prompt" className="py-4 flex-1 min-h-0 flex flex-col">
<Tabs
defaultValue="prompt"
className="py-4 flex-1 min-h-0 flex flex-col"
>
<TabsList className="w-full grid grid-cols-3 mb-4">
<TabsTrigger value="prompt" data-testid="tab-prompt">
<MessageSquare className="w-4 h-4 mr-2" />
@@ -1949,7 +1958,8 @@ export function BoardView() {
Simple Mode Active
</p>
<p className="text-xs text-muted-foreground">
Only showing AI profiles. Advanced model tweaking is hidden.
Only showing AI profiles. Advanced model tweaking is
hidden.
</p>
</div>
<Button
@@ -1959,7 +1969,7 @@ export function BoardView() {
data-testid="show-advanced-options-toggle"
>
<Settings2 className="w-4 h-4 mr-2" />
{showAdvancedOptions ? 'Hide' : 'Show'} Advanced
{showAdvancedOptions ? "Hide" : "Show"} Advanced
</Button>
</div>
)}
@@ -1978,9 +1988,12 @@ export function BoardView() {
</div>
<div className="grid grid-cols-2 gap-2">
{aiProfiles.slice(0, 6).map((profile) => {
const IconComponent = profile.icon ? PROFILE_ICONS[profile.icon] : Brain;
const IconComponent = profile.icon
? PROFILE_ICONS[profile.icon]
: Brain;
const isCodex = profile.provider === "codex";
const isSelected = newFeature.model === profile.model &&
const isSelected =
newFeature.model === profile.model &&
newFeature.thinkingLevel === profile.thinkingLevel;
return (
<button
@@ -1994,7 +2007,8 @@ export function BoardView() {
});
if (profile.thinkingLevel === "ultrathink") {
toast.warning("Ultrathink Selected", {
description: "Ultrathink uses extensive reasoning (45-180s, ~$0.48/task).",
description:
"Ultrathink uses extensive reasoning (45-180s, ~$0.48/task).",
duration: 4000,
});
}
@@ -2007,21 +2021,29 @@ export function BoardView() {
)}
data-testid={`profile-quick-select-${profile.id}`}
>
<div className={cn(
"w-7 h-7 rounded flex items-center justify-center flex-shrink-0",
isCodex ? "bg-emerald-500/10" : "bg-primary/10"
)}>
<div
className={cn(
"w-7 h-7 rounded flex items-center justify-center flex-shrink-0",
isCodex ? "bg-emerald-500/10" : "bg-primary/10"
)}
>
{IconComponent && (
<IconComponent className={cn(
"w-4 h-4",
isCodex ? "text-emerald-500" : "text-primary"
)} />
<IconComponent
className={cn(
"w-4 h-4",
isCodex ? "text-emerald-500" : "text-primary"
)}
/>
)}
</div>
<div className="min-w-0 flex-1">
<p className="text-sm font-medium truncate">{profile.name}</p>
<p className="text-sm font-medium truncate">
{profile.name}
</p>
<p className="text-[10px] text-muted-foreground truncate">
{profile.model}{profile.thinkingLevel !== "none" && ` + ${profile.thinkingLevel}`}
{profile.model}
{profile.thinkingLevel !== "none" &&
` + ${profile.thinkingLevel}`}
</p>
</div>
</button>
@@ -2045,107 +2067,122 @@ export function BoardView() {
)}
{/* Separator */}
{aiProfiles.length > 0 && (!showProfilesOnly || showAdvancedOptions) && <div className="border-t border-border" />}
{aiProfiles.length > 0 &&
(!showProfilesOnly || showAdvancedOptions) && (
<div className="border-t border-border" />
)}
{/* Claude Models Section - Hidden when showProfilesOnly is true and showAdvancedOptions is false */}
{(!showProfilesOnly || showAdvancedOptions) && (
<div className="space-y-3">
<div className="flex items-center justify-between">
<Label className="flex items-center gap-2">
<Brain className="w-4 h-4 text-primary" />
Claude (SDK)
</Label>
<span className="text-[11px] px-2 py-0.5 rounded-full border border-primary/40 text-primary">
Native
</span>
</div>
{renderModelOptions(
CLAUDE_MODELS,
newFeature.model,
(model) =>
setNewFeature({
...newFeature,
model,
thinkingLevel: modelSupportsThinking(model)
? newFeature.thinkingLevel
: "none",
})
)}
{/* Thinking Level - Only shown when Claude model is selected */}
{newModelAllowsThinking && (
<div className="space-y-2 pt-2 border-t border-border">
<Label className="flex items-center gap-2 text-sm">
<Brain className="w-3.5 h-3.5 text-muted-foreground" />
Thinking Level
<div className="space-y-3">
<div className="flex items-center justify-between">
<Label className="flex items-center gap-2">
<Brain className="w-4 h-4 text-primary" />
Claude (SDK)
</Label>
<div className="flex gap-2 flex-wrap">
{(["none", "low", "medium", "high", "ultrathink"] as ThinkingLevel[]).map((level) => (
<button
key={level}
type="button"
onClick={() => {
setNewFeature({ ...newFeature, thinkingLevel: level });
if (level === "ultrathink") {
toast.warning("Ultrathink Selected", {
description: "Ultrathink uses extensive reasoning (45-180s, ~$0.48/task). Best for complex architecture, migrations, or debugging.",
duration: 5000
});
}
}}
className={cn(
"flex-1 px-3 py-2 rounded-md border text-sm font-medium transition-colors min-w-[60px]",
newFeature.thinkingLevel === level
? "bg-primary text-primary-foreground border-primary"
: "bg-background hover:bg-accent border-input"
)}
data-testid={`thinking-level-${level}`}
>
{level === "none" && "None"}
{level === "low" && "Low"}
{level === "medium" && "Med"}
{level === "high" && "High"}
{level === "ultrathink" && "Ultra"}
</button>
))}
</div>
<p className="text-xs text-muted-foreground">
Higher levels give more time to reason through complex problems.
</p>
<span className="text-[11px] px-2 py-0.5 rounded-full border border-primary/40 text-primary">
Native
</span>
</div>
)}
</div>
{renderModelOptions(
CLAUDE_MODELS,
newFeature.model,
(model) =>
setNewFeature({
...newFeature,
model,
thinkingLevel: modelSupportsThinking(model)
? newFeature.thinkingLevel
: "none",
})
)}
{/* Thinking Level - Only shown when Claude model is selected */}
{newModelAllowsThinking && (
<div className="space-y-2 pt-2 border-t border-border">
<Label className="flex items-center gap-2 text-sm">
<Brain className="w-3.5 h-3.5 text-muted-foreground" />
Thinking Level
</Label>
<div className="flex gap-2 flex-wrap">
{(
[
"none",
"low",
"medium",
"high",
"ultrathink",
] as ThinkingLevel[]
).map((level) => (
<button
key={level}
type="button"
onClick={() => {
setNewFeature({
...newFeature,
thinkingLevel: level,
});
if (level === "ultrathink") {
toast.warning("Ultrathink Selected", {
description:
"Ultrathink uses extensive reasoning (45-180s, ~$0.48/task). Best for complex architecture, migrations, or debugging.",
duration: 5000,
});
}
}}
className={cn(
"flex-1 px-3 py-2 rounded-md border text-sm font-medium transition-colors min-w-[60px]",
newFeature.thinkingLevel === level
? "bg-primary text-primary-foreground border-primary"
: "bg-background hover:bg-accent border-input"
)}
data-testid={`thinking-level-${level}`}
>
{level === "none" && "None"}
{level === "low" && "Low"}
{level === "medium" && "Med"}
{level === "high" && "High"}
{level === "ultrathink" && "Ultra"}
</button>
))}
</div>
<p className="text-xs text-muted-foreground">
Higher levels give more time to reason through complex
problems.
</p>
</div>
)}
</div>
)}
{/* Separator */}
{(!showProfilesOnly || showAdvancedOptions) && <div className="border-t border-border" />}
{(!showProfilesOnly || showAdvancedOptions) && (
<div className="border-t border-border" />
)}
{/* Codex Models Section - Hidden when showProfilesOnly is true and showAdvancedOptions is false */}
{(!showProfilesOnly || showAdvancedOptions) && (
<div className="space-y-3">
<div className="flex items-center justify-between">
<Label className="flex items-center gap-2">
<Zap className="w-4 h-4 text-emerald-500" />
OpenAI via Codex CLI
</Label>
<span className="text-[11px] px-2 py-0.5 rounded-full border border-emerald-500/50 text-emerald-600 dark:text-emerald-300">
CLI
</span>
</div>
{renderModelOptions(
CODEX_MODELS,
newFeature.model,
(model) =>
<div className="space-y-3">
<div className="flex items-center justify-between">
<Label className="flex items-center gap-2">
<Zap className="w-4 h-4 text-emerald-500" />
OpenAI via Codex CLI
</Label>
<span className="text-[11px] px-2 py-0.5 rounded-full border border-emerald-500/50 text-emerald-600 dark:text-emerald-300">
CLI
</span>
</div>
{renderModelOptions(CODEX_MODELS, newFeature.model, (model) =>
setNewFeature({
...newFeature,
model,
thinkingLevel: "none",
})
)}
<p className="text-xs text-muted-foreground">
Codex models do not support thinking levels.
</p>
</div>
)}
<p className="text-xs text-muted-foreground">
Codex models do not support thinking levels.
</p>
</div>
)}
</TabsContent>
@@ -2156,12 +2193,18 @@ export function BoardView() {
id="skip-tests"
checked={newFeature.skipTests}
onCheckedChange={(checked) =>
setNewFeature({ ...newFeature, skipTests: checked === true })
setNewFeature({
...newFeature,
skipTests: checked === true,
})
}
data-testid="skip-tests-checkbox"
/>
<div className="flex items-center gap-2">
<Label htmlFor="skip-tests" className="text-sm cursor-pointer">
<Label
htmlFor="skip-tests"
className="text-sm cursor-pointer"
>
Skip automated testing
</Label>
<FlaskConical className="w-3.5 h-3.5 text-muted-foreground" />
@@ -2242,7 +2285,10 @@ export function BoardView() {
<DialogDescription>Modify the feature details.</DialogDescription>
</DialogHeader>
{editingFeature && (
<Tabs defaultValue="prompt" className="py-4 flex-1 min-h-0 flex flex-col">
<Tabs
defaultValue="prompt"
className="py-4 flex-1 min-h-0 flex flex-col"
>
<TabsList className="w-full grid grid-cols-3 mb-4">
<TabsTrigger value="prompt" data-testid="edit-tab-prompt">
<MessageSquare className="w-4 h-4 mr-2" />
@@ -2302,17 +2348,20 @@ export function BoardView() {
Simple Mode Active
</p>
<p className="text-xs text-muted-foreground">
Only showing AI profiles. Advanced model tweaking is hidden.
Only showing AI profiles. Advanced model tweaking is
hidden.
</p>
</div>
<Button
variant="outline"
size="sm"
onClick={() => setShowEditAdvancedOptions(!showEditAdvancedOptions)}
onClick={() =>
setShowEditAdvancedOptions(!showEditAdvancedOptions)
}
data-testid="edit-show-advanced-options-toggle"
>
<Settings2 className="w-4 h-4 mr-2" />
{showEditAdvancedOptions ? 'Hide' : 'Show'} Advanced
{showEditAdvancedOptions ? "Hide" : "Show"} Advanced
</Button>
</div>
)}
@@ -2331,10 +2380,14 @@ export function BoardView() {
</div>
<div className="grid grid-cols-2 gap-2">
{aiProfiles.slice(0, 6).map((profile) => {
const IconComponent = profile.icon ? PROFILE_ICONS[profile.icon] : Brain;
const IconComponent = profile.icon
? PROFILE_ICONS[profile.icon]
: Brain;
const isCodex = profile.provider === "codex";
const isSelected = editingFeature.model === profile.model &&
editingFeature.thinkingLevel === profile.thinkingLevel;
const isSelected =
editingFeature.model === profile.model &&
editingFeature.thinkingLevel ===
profile.thinkingLevel;
return (
<button
key={profile.id}
@@ -2347,7 +2400,8 @@ export function BoardView() {
});
if (profile.thinkingLevel === "ultrathink") {
toast.warning("Ultrathink Selected", {
description: "Ultrathink uses extensive reasoning (45-180s, ~$0.48/task).",
description:
"Ultrathink uses extensive reasoning (45-180s, ~$0.48/task).",
duration: 4000,
});
}
@@ -2360,21 +2414,31 @@ export function BoardView() {
)}
data-testid={`edit-profile-quick-select-${profile.id}`}
>
<div className={cn(
"w-7 h-7 rounded flex items-center justify-center flex-shrink-0",
isCodex ? "bg-emerald-500/10" : "bg-primary/10"
)}>
<div
className={cn(
"w-7 h-7 rounded flex items-center justify-center flex-shrink-0",
isCodex ? "bg-emerald-500/10" : "bg-primary/10"
)}
>
{IconComponent && (
<IconComponent className={cn(
"w-4 h-4",
isCodex ? "text-emerald-500" : "text-primary"
)} />
<IconComponent
className={cn(
"w-4 h-4",
isCodex
? "text-emerald-500"
: "text-primary"
)}
/>
)}
</div>
<div className="min-w-0 flex-1">
<p className="text-sm font-medium truncate">{profile.name}</p>
<p className="text-sm font-medium truncate">
{profile.name}
</p>
<p className="text-[10px] text-muted-foreground truncate">
{profile.model}{profile.thinkingLevel !== "none" && ` + ${profile.thinkingLevel}`}
{profile.model}
{profile.thinkingLevel !== "none" &&
` + ${profile.thinkingLevel}`}
</p>
</div>
</button>
@@ -2388,114 +2452,136 @@ export function BoardView() {
)}
{/* Separator */}
{aiProfiles.length > 0 && (!showProfilesOnly || showEditAdvancedOptions) && <div className="border-t border-border" />}
{aiProfiles.length > 0 &&
(!showProfilesOnly || showEditAdvancedOptions) && (
<div className="border-t border-border" />
)}
{/* Claude Models Section - Hidden when showProfilesOnly is true and showEditAdvancedOptions is false */}
{(!showProfilesOnly || showEditAdvancedOptions) && (
<div className="space-y-3">
<div className="flex items-center justify-between">
<Label className="flex items-center gap-2">
<Brain className="w-4 h-4 text-primary" />
Claude (SDK)
</Label>
<span className="text-[11px] px-2 py-0.5 rounded-full border border-primary/40 text-primary">
Native
</span>
</div>
{renderModelOptions(
CLAUDE_MODELS,
(editingFeature.model ?? "opus") as AgentModel,
(model) =>
setEditingFeature({
...editingFeature,
model,
thinkingLevel: modelSupportsThinking(model)
? editingFeature.thinkingLevel
: "none",
}),
"edit-model-select"
)}
{/* Thinking Level - Only shown when Claude model is selected */}
{editModelAllowsThinking && (
<div className="space-y-2 pt-2 border-t border-border">
<Label className="flex items-center gap-2 text-sm">
<Brain className="w-3.5 h-3.5 text-muted-foreground" />
Thinking Level
<div className="space-y-3">
<div className="flex items-center justify-between">
<Label className="flex items-center gap-2">
<Brain className="w-4 h-4 text-primary" />
Claude (SDK)
</Label>
<div className="flex gap-2 flex-wrap">
{(["none", "low", "medium", "high", "ultrathink"] as ThinkingLevel[]).map((level) => (
<button
key={level}
type="button"
onClick={() => {
setEditingFeature({ ...editingFeature, thinkingLevel: level });
if (level === "ultrathink") {
toast.warning("Ultrathink Selected", {
description: "Ultrathink uses extensive reasoning (45-180s, ~$0.48/task). Best for complex architecture, migrations, or debugging.",
duration: 5000
});
}
}}
className={cn(
"flex-1 px-3 py-2 rounded-md border text-sm font-medium transition-colors min-w-[60px]",
(editingFeature.thinkingLevel ?? "none") === level
? "bg-primary text-primary-foreground border-primary"
: "bg-background hover:bg-accent border-input"
)}
data-testid={`edit-thinking-level-${level}`}
>
{level === "none" && "None"}
{level === "low" && "Low"}
{level === "medium" && "Med"}
{level === "high" && "High"}
{level === "ultrathink" && "Ultra"}
</button>
))}
</div>
<p className="text-xs text-muted-foreground">
Higher levels give more time to reason through complex problems.
</p>
<span className="text-[11px] px-2 py-0.5 rounded-full border border-primary/40 text-primary">
Native
</span>
</div>
)}
</div>
{renderModelOptions(
CLAUDE_MODELS,
(editingFeature.model ?? "opus") as AgentModel,
(model) =>
setEditingFeature({
...editingFeature,
model,
thinkingLevel: modelSupportsThinking(model)
? editingFeature.thinkingLevel
: "none",
}),
"edit-model-select"
)}
{/* Thinking Level - Only shown when Claude model is selected */}
{editModelAllowsThinking && (
<div className="space-y-2 pt-2 border-t border-border">
<Label className="flex items-center gap-2 text-sm">
<Brain className="w-3.5 h-3.5 text-muted-foreground" />
Thinking Level
</Label>
<div className="flex gap-2 flex-wrap">
{(
[
"none",
"low",
"medium",
"high",
"ultrathink",
] as ThinkingLevel[]
).map((level) => (
<button
key={level}
type="button"
onClick={() => {
setEditingFeature({
...editingFeature,
thinkingLevel: level,
});
if (level === "ultrathink") {
toast.warning("Ultrathink Selected", {
description:
"Ultrathink uses extensive reasoning (45-180s, ~$0.48/task). Best for complex architecture, migrations, or debugging.",
duration: 5000,
});
}
}}
className={cn(
"flex-1 px-3 py-2 rounded-md border text-sm font-medium transition-colors min-w-[60px]",
(editingFeature.thinkingLevel ?? "none") ===
level
? "bg-primary text-primary-foreground border-primary"
: "bg-background hover:bg-accent border-input"
)}
data-testid={`edit-thinking-level-${level}`}
>
{level === "none" && "None"}
{level === "low" && "Low"}
{level === "medium" && "Med"}
{level === "high" && "High"}
{level === "ultrathink" && "Ultra"}
</button>
))}
</div>
<p className="text-xs text-muted-foreground">
Higher levels give more time to reason through complex
problems.
</p>
</div>
)}
</div>
)}
{/* Separator */}
{(!showProfilesOnly || showEditAdvancedOptions) && <div className="border-t border-border" />}
{(!showProfilesOnly || showEditAdvancedOptions) && (
<div className="border-t border-border" />
)}
{/* Codex Models Section - Hidden when showProfilesOnly is true and showEditAdvancedOptions is false */}
{(!showProfilesOnly || showEditAdvancedOptions) && (
<div className="space-y-3">
<div className="flex items-center justify-between">
<Label className="flex items-center gap-2">
<Zap className="w-4 h-4 text-emerald-500" />
OpenAI via Codex CLI
</Label>
<span className="text-[11px] px-2 py-0.5 rounded-full border border-emerald-500/50 text-emerald-600 dark:text-emerald-300">
CLI
</span>
<div className="space-y-3">
<div className="flex items-center justify-between">
<Label className="flex items-center gap-2">
<Zap className="w-4 h-4 text-emerald-500" />
OpenAI via Codex CLI
</Label>
<span className="text-[11px] px-2 py-0.5 rounded-full border border-emerald-500/50 text-emerald-600 dark:text-emerald-300">
CLI
</span>
</div>
{renderModelOptions(
CODEX_MODELS,
(editingFeature.model ?? "opus") as AgentModel,
(model) =>
setEditingFeature({
...editingFeature,
model,
thinkingLevel: "none",
}),
"edit-model-select"
)}
<p className="text-xs text-muted-foreground">
Codex models do not support thinking levels.
</p>
</div>
{renderModelOptions(
CODEX_MODELS,
(editingFeature.model ?? "opus") as AgentModel,
(model) =>
setEditingFeature({
...editingFeature,
model,
thinkingLevel: "none",
}),
"edit-model-select"
)}
<p className="text-xs text-muted-foreground">
Codex models do not support thinking levels.
</p>
</div>
)}
</TabsContent>
{/* Testing Tab */}
<TabsContent value="testing" className="space-y-4 overflow-y-auto">
<TabsContent
value="testing"
className="space-y-4 overflow-y-auto"
>
<div className="flex items-center space-x-2">
<Checkbox
id="edit-skip-tests"

View File

@@ -20,7 +20,7 @@ import {
} from "lucide-react";
import {
useKeyboardShortcuts,
ACTION_SHORTCUTS,
useKeyboardShortcutsConfig,
KeyboardShortcut,
} from "@/hooks/use-keyboard-shortcuts";
import {
@@ -44,6 +44,7 @@ interface ContextFile {
export function ContextView() {
const { currentProject } = useAppStore();
const shortcuts = useKeyboardShortcutsConfig();
const [contextFiles, setContextFiles] = useState<ContextFile[]>([]);
const [selectedFile, setSelectedFile] = useState<ContextFile | null>(null);
const [isLoading, setIsLoading] = useState(true);
@@ -64,12 +65,12 @@ export function ContextView() {
const contextShortcuts: KeyboardShortcut[] = useMemo(
() => [
{
key: ACTION_SHORTCUTS.addContextFile,
key: shortcuts.addContextFile,
action: () => setIsAddDialogOpen(true),
description: "Add new context file",
},
],
[]
[shortcuts]
);
useKeyboardShortcuts(contextShortcuts);
@@ -367,7 +368,7 @@ export function ContextView() {
<HotkeyButton
size="sm"
onClick={() => setIsAddDialogOpen(true)}
hotkey={ACTION_SHORTCUTS.addContextFile}
hotkey={shortcuts.addContextFile}
hotkeyActive={false}
data-testid="add-context-file"
>
@@ -501,7 +502,9 @@ export function ContextView() {
<div className="flex-1 flex items-center justify-center">
<div className="text-center">
<File className="w-12 h-12 text-muted-foreground mx-auto mb-3" />
<p className="text-foreground-secondary">Select a file to view or edit</p>
<p className="text-foreground-secondary">
Select a file to view or edit
</p>
<p className="text-muted-foreground text-sm mt-1">
Or drop files here to add them
</p>

View File

@@ -431,17 +431,22 @@ export function InterviewView() {
<Card
className={cn(
"max-w-[80%]",
message.role === "user" && "bg-primary text-primary-foreground"
message.role === "user"
? "bg-primary text-primary-foreground"
: "border-l-4 border-primary bg-card"
)}
>
<CardContent className="p-3">
<p className="text-sm whitespace-pre-wrap">{message.content}</p>
<p className={cn(
"text-sm whitespace-pre-wrap",
message.role === "assistant" && "text-primary"
)}>{message.content}</p>
<p
className={cn(
"text-xs mt-2",
message.role === "user"
? "text-primary-foreground/70"
: "text-muted-foreground"
: "text-primary/70"
)}
>
{message.timestamp.toLocaleTimeString()}
@@ -456,11 +461,11 @@ export function InterviewView() {
<div className="w-8 h-8 rounded-full bg-primary/10 flex items-center justify-center">
<Bot className="w-4 h-4 text-primary" />
</div>
<Card>
<Card className="border-l-4 border-primary bg-card">
<CardContent className="p-3">
<div className="flex items-center gap-2">
<Loader2 className="w-4 h-4 animate-spin" />
<span className="text-sm text-muted-foreground">
<Loader2 className="w-4 h-4 animate-spin text-primary" />
<span className="text-sm text-primary">
Generating specification...
</span>
</div>

View File

@@ -1,7 +1,13 @@
"use client";
import { useState, useMemo, useCallback, useEffect } from "react";
import { useAppStore, AIProfile, AgentModel, ThinkingLevel, ModelProvider } from "@/store/app-store";
import {
useAppStore,
AIProfile,
AgentModel,
ThinkingLevel,
ModelProvider,
} from "@/store/app-store";
import { Button } from "@/components/ui/button";
import { HotkeyButton } from "@/components/ui/hotkey-button";
import { Input } from "@/components/ui/input";
@@ -10,7 +16,7 @@ import { Textarea } from "@/components/ui/textarea";
import { cn, modelSupportsThinking } from "@/lib/utils";
import {
useKeyboardShortcuts,
ACTION_SHORTCUTS,
useKeyboardShortcutsConfig,
KeyboardShortcut,
} from "@/hooks/use-keyboard-shortcuts";
import {
@@ -53,7 +59,10 @@ import {
import { CSS } from "@dnd-kit/utilities";
// Icon mapping for profiles
const PROFILE_ICONS: Record<string, React.ComponentType<{ className?: string }>> = {
const PROFILE_ICONS: Record<
string,
React.ComponentType<{ className?: string }>
> = {
Brain,
Zap,
Scale,
@@ -446,8 +455,14 @@ function ProfileForm({
}
export function ProfilesView() {
const { aiProfiles, addAIProfile, updateAIProfile, removeAIProfile, reorderAIProfiles } =
useAppStore();
const {
aiProfiles,
addAIProfile,
updateAIProfile,
removeAIProfile,
reorderAIProfiles,
} = useAppStore();
const shortcuts = useKeyboardShortcutsConfig();
const [showAddDialog, setShowAddDialog] = useState(false);
const [editingProfile, setEditingProfile] = useState<AIProfile | null>(null);
@@ -516,17 +531,17 @@ export function ProfilesView() {
// Build keyboard shortcuts for profiles view
const profilesShortcuts: KeyboardShortcut[] = useMemo(() => {
const shortcuts: KeyboardShortcut[] = [];
const shortcutsList: KeyboardShortcut[] = [];
// Add profile shortcut - when in profiles view
shortcuts.push({
key: ACTION_SHORTCUTS.addProfile,
shortcutsList.push({
key: shortcuts.addProfile,
action: () => setShowAddDialog(true),
description: "Create new profile",
});
return shortcuts;
}, []);
return shortcutsList;
}, [shortcuts]);
// Register keyboard shortcuts for profiles view
useKeyboardShortcuts(profilesShortcuts);
@@ -555,7 +570,7 @@ export function ProfilesView() {
</div>
<HotkeyButton
onClick={() => setShowAddDialog(true)}
hotkey={ACTION_SHORTCUTS.addProfile}
hotkey={shortcuts.addProfile}
hotkeyActive={false}
data-testid="add-profile-button"
>

View File

@@ -1,7 +1,8 @@
"use client";
import { useState, useEffect, useRef, useCallback } from "react";
import { useAppStore } from "@/store/app-store";
import { useAppStore, DEFAULT_KEYBOARD_SHORTCUTS } from "@/store/app-store";
import type { KeyboardShortcuts } from "@/store/app-store";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
@@ -38,6 +39,8 @@ import {
GitBranch,
TestTube,
Settings2,
RefreshCw,
RotateCcw,
} from "lucide-react";
import { getElectronAPI } from "@/lib/electron";
import { Checkbox } from "@/components/ui/checkbox";
@@ -57,6 +60,7 @@ const NAV_ITEMS = [
{ id: "codex", label: "Codex", icon: Atom },
{ id: "appearance", label: "Appearance", icon: Palette },
{ id: "kanban", label: "Kanban Display", icon: LayoutGrid },
{ id: "keyboard", label: "Keyboard Shortcuts", icon: Settings2 },
{ id: "defaults", label: "Feature Defaults", icon: FlaskConical },
{ id: "danger", label: "Danger Zone", icon: Trash2 },
];
@@ -79,6 +83,9 @@ export function SettingsView() {
setShowProfilesOnly,
currentProject,
moveProjectToTrash,
keyboardShortcuts,
setKeyboardShortcut,
resetKeyboardShortcuts,
} = useAppStore();
// Compute the effective theme for the current project
@@ -147,6 +154,11 @@ export function SettingsView() {
} | null>(null);
const [activeSection, setActiveSection] = useState("api-keys");
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
const [isCheckingClaudeCli, setIsCheckingClaudeCli] = useState(false);
const [isCheckingCodexCli, setIsCheckingCodexCli] = useState(false);
const [editingShortcut, setEditingShortcut] = useState<string | null>(null);
const [shortcutValue, setShortcutValue] = useState("");
const [shortcutError, setShortcutError] = useState<string | null>(null);
const scrollContainerRef = useRef<HTMLDivElement>(null);
useEffect(() => {
@@ -367,6 +379,36 @@ export function SettingsView() {
}
};
const handleRefreshClaudeCli = useCallback(async () => {
setIsCheckingClaudeCli(true);
try {
const api = getElectronAPI();
if (api?.checkClaudeCli) {
const status = await api.checkClaudeCli();
setClaudeCliStatus(status);
}
} catch (error) {
console.error("Failed to refresh Claude CLI status:", error);
} finally {
setIsCheckingClaudeCli(false);
}
}, []);
const handleRefreshCodexCli = useCallback(async () => {
setIsCheckingCodexCli(true);
try {
const api = getElectronAPI();
if (api?.checkCodexCli) {
const status = await api.checkCodexCli();
setCodexCliStatus(status);
}
} catch (error) {
console.error("Failed to refresh Codex CLI status:", error);
} finally {
setIsCheckingCodexCli(false);
}
}, []);
const handleSave = () => {
setApiKeys({
anthropic: anthropicKey,
@@ -757,11 +799,27 @@ export function SettingsView() {
className="rounded-xl border border-border bg-card backdrop-blur-md overflow-hidden scroll-mt-6"
>
<div className="p-6 border-b border-border">
<div className="flex items-center gap-2 mb-2">
<Terminal className="w-5 h-5 text-brand-500" />
<h2 className="text-lg font-semibold text-foreground">
Claude Code CLI
</h2>
<div className="flex items-center justify-between mb-2">
<div className="flex items-center gap-2">
<Terminal className="w-5 h-5 text-brand-500" />
<h2 className="text-lg font-semibold text-foreground">
Claude Code CLI
</h2>
</div>
<Button
variant="ghost"
size="icon"
onClick={handleRefreshClaudeCli}
disabled={isCheckingClaudeCli}
data-testid="refresh-claude-cli"
title="Refresh Claude CLI detection"
>
<RefreshCw
className={`w-4 h-4 ${
isCheckingClaudeCli ? "animate-spin" : ""
}`}
/>
</Button>
</div>
<p className="text-sm text-muted-foreground">
Claude Code CLI provides better performance for long-running
@@ -881,11 +939,27 @@ export function SettingsView() {
className="rounded-xl border border-border bg-card backdrop-blur-md overflow-hidden scroll-mt-6"
>
<div className="p-6 border-b border-border">
<div className="flex items-center gap-2 mb-2">
<Terminal className="w-5 h-5 text-green-500" />
<h2 className="text-lg font-semibold text-foreground">
OpenAI Codex CLI
</h2>
<div className="flex items-center justify-between mb-2">
<div className="flex items-center gap-2">
<Terminal className="w-5 h-5 text-green-500" />
<h2 className="text-lg font-semibold text-foreground">
OpenAI Codex CLI
</h2>
</div>
<Button
variant="ghost"
size="icon"
onClick={handleRefreshCodexCli}
disabled={isCheckingCodexCli}
data-testid="refresh-codex-cli"
title="Refresh Codex CLI detection"
>
<RefreshCw
className={`w-4 h-4 ${
isCheckingCodexCli ? "animate-spin" : ""
}`}
/>
</Button>
</div>
<p className="text-sm text-muted-foreground">
Codex CLI enables GPT-5.1 Codex models for autonomous coding
@@ -1330,6 +1404,393 @@ export function SettingsView() {
</div>
</div>
{/* Keyboard Shortcuts Section */}
<div
id="keyboard"
className="rounded-xl border border-border bg-card backdrop-blur-md overflow-hidden scroll-mt-6"
>
<div className="p-6 border-b border-border">
<div className="flex items-center gap-2 mb-2">
<Settings2 className="w-5 h-5 text-brand-500" />
<h2 className="text-lg font-semibold text-foreground">
Keyboard Shortcuts
</h2>
</div>
<p className="text-sm text-muted-foreground">
Customize keyboard shortcuts for navigation and actions. Click
on any shortcut to edit it.
</p>
</div>
<div className="p-6 space-y-6">
{/* Navigation Shortcuts */}
<div className="space-y-3">
<div className="flex items-center justify-between">
<h3 className="text-sm font-semibold text-foreground">
Navigation
</h3>
<Button
variant="ghost"
size="sm"
onClick={() => resetKeyboardShortcuts()}
className="text-xs h-7"
data-testid="reset-shortcuts-button"
>
<RotateCcw className="w-3 h-3 mr-1" />
Reset All to Defaults
</Button>
</div>
<div className="space-y-2">
{[
{ key: "board" as keyof KeyboardShortcuts, label: "Kanban Board" },
{ key: "agent" as keyof KeyboardShortcuts, label: "Agent Runner" },
{ key: "spec" as keyof KeyboardShortcuts, label: "Spec Editor" },
{ key: "context" as keyof KeyboardShortcuts, label: "Context" },
{ key: "tools" as keyof KeyboardShortcuts, label: "Agent Tools" },
{ key: "profiles" as keyof KeyboardShortcuts, label: "AI Profiles" },
{ key: "settings" as keyof KeyboardShortcuts, label: "Settings" },
].map(({ key, label }) => (
<div
key={key}
className="flex items-center justify-between p-3 rounded-lg bg-sidebar-accent/10 border border-sidebar-border hover:bg-sidebar-accent/20 transition-colors"
>
<span className="text-sm text-foreground">{label}</span>
<div className="flex items-center gap-2">
{editingShortcut === key ? (
<>
<Input
value={shortcutValue}
onChange={(e) => {
const value = e.target.value.toUpperCase();
setShortcutValue(value);
// Check for conflicts
const conflict = Object.entries(keyboardShortcuts).find(
([k, v]) => k !== key && v.toUpperCase() === value
);
if (conflict) {
setShortcutError(`Already used by ${conflict[0]}`);
} else {
setShortcutError(null);
}
}}
onKeyDown={(e) => {
if (e.key === "Enter" && !shortcutError && shortcutValue) {
setKeyboardShortcut(key, shortcutValue);
setEditingShortcut(null);
setShortcutValue("");
setShortcutError(null);
} else if (e.key === "Escape") {
setEditingShortcut(null);
setShortcutValue("");
setShortcutError(null);
}
}}
className="w-24 h-8 text-center font-mono"
placeholder="Key"
maxLength={2}
autoFocus
data-testid={`edit-shortcut-${key}`}
/>
<Button
size="sm"
variant="ghost"
className="h-8 w-8 p-0"
onClick={() => {
if (!shortcutError && shortcutValue) {
setKeyboardShortcut(key, shortcutValue);
setEditingShortcut(null);
setShortcutValue("");
setShortcutError(null);
}
}}
disabled={!!shortcutError || !shortcutValue}
data-testid={`save-shortcut-${key}`}
>
<CheckCircle2 className="w-4 h-4" />
</Button>
<Button
size="sm"
variant="ghost"
className="h-8 w-8 p-0"
onClick={() => {
setEditingShortcut(null);
setShortcutValue("");
setShortcutError(null);
}}
data-testid={`cancel-shortcut-${key}`}
>
<AlertCircle className="w-4 h-4" />
</Button>
</>
) : (
<>
<button
onClick={() => {
setEditingShortcut(key);
setShortcutValue(keyboardShortcuts[key]);
setShortcutError(null);
}}
className={cn(
"px-3 py-1.5 text-sm font-mono rounded bg-sidebar-accent/20 border border-sidebar-border hover:bg-sidebar-accent/30 transition-colors",
keyboardShortcuts[key] !== DEFAULT_KEYBOARD_SHORTCUTS[key] &&
"border-brand-500/50 bg-brand-500/10 text-brand-400"
)}
data-testid={`shortcut-${key}`}
>
{keyboardShortcuts[key]}
</button>
{keyboardShortcuts[key] !== DEFAULT_KEYBOARD_SHORTCUTS[key] && (
<span className="text-xs text-brand-400">(modified)</span>
)}
</>
)}
</div>
</div>
))}
</div>
{shortcutError && (
<p className="text-xs text-red-400">{shortcutError}</p>
)}
</div>
{/* UI Shortcuts */}
<div className="space-y-3">
<h3 className="text-sm font-semibold text-foreground">
UI Controls
</h3>
<div className="space-y-2">
{[
{ key: "toggleSidebar" as keyof KeyboardShortcuts, label: "Toggle Sidebar" },
].map(({ key, label }) => (
<div
key={key}
className="flex items-center justify-between p-3 rounded-lg bg-sidebar-accent/10 border border-sidebar-border hover:bg-sidebar-accent/20 transition-colors"
>
<span className="text-sm text-foreground">{label}</span>
<div className="flex items-center gap-2">
{editingShortcut === key ? (
<>
<Input
value={shortcutValue}
onChange={(e) => {
const value = e.target.value;
setShortcutValue(value);
// Check for conflicts
const conflict = Object.entries(keyboardShortcuts).find(
([k, v]) => k !== key && v === value
);
if (conflict) {
setShortcutError(`Already used by ${conflict[0]}`);
} else {
setShortcutError(null);
}
}}
onKeyDown={(e) => {
if (e.key === "Enter" && !shortcutError && shortcutValue) {
setKeyboardShortcut(key, shortcutValue);
setEditingShortcut(null);
setShortcutValue("");
setShortcutError(null);
} else if (e.key === "Escape") {
setEditingShortcut(null);
setShortcutValue("");
setShortcutError(null);
}
}}
className="w-24 h-8 text-center font-mono"
placeholder="Key"
maxLength={2}
autoFocus
data-testid={`edit-shortcut-${key}`}
/>
<Button
size="sm"
variant="ghost"
className="h-8 w-8 p-0"
onClick={() => {
if (!shortcutError && shortcutValue) {
setKeyboardShortcut(key, shortcutValue);
setEditingShortcut(null);
setShortcutValue("");
setShortcutError(null);
}
}}
disabled={!!shortcutError || !shortcutValue}
>
<CheckCircle2 className="w-4 h-4" />
</Button>
<Button
size="sm"
variant="ghost"
className="h-8 w-8 p-0"
onClick={() => {
setEditingShortcut(null);
setShortcutValue("");
setShortcutError(null);
}}
>
<AlertCircle className="w-4 h-4" />
</Button>
</>
) : (
<>
<button
onClick={() => {
setEditingShortcut(key);
setShortcutValue(keyboardShortcuts[key]);
setShortcutError(null);
}}
className={cn(
"px-3 py-1.5 text-sm font-mono rounded bg-sidebar-accent/20 border border-sidebar-border hover:bg-sidebar-accent/30 transition-colors",
keyboardShortcuts[key] !== DEFAULT_KEYBOARD_SHORTCUTS[key] &&
"border-brand-500/50 bg-brand-500/10 text-brand-400"
)}
data-testid={`shortcut-${key}`}
>
{keyboardShortcuts[key]}
</button>
{keyboardShortcuts[key] !== DEFAULT_KEYBOARD_SHORTCUTS[key] && (
<span className="text-xs text-brand-400">(modified)</span>
)}
</>
)}
</div>
</div>
))}
</div>
</div>
{/* Action Shortcuts */}
<div className="space-y-3">
<h3 className="text-sm font-semibold text-foreground">
Actions
</h3>
<div className="space-y-2">
{[
{ key: "addFeature" as keyof KeyboardShortcuts, label: "Add Feature" },
{ key: "addContextFile" as keyof KeyboardShortcuts, label: "Add Context File" },
{ key: "startNext" as keyof KeyboardShortcuts, label: "Start Next Features" },
{ key: "newSession" as keyof KeyboardShortcuts, label: "New Session" },
{ key: "openProject" as keyof KeyboardShortcuts, label: "Open Project" },
{ key: "projectPicker" as keyof KeyboardShortcuts, label: "Project Picker" },
{ key: "cyclePrevProject" as keyof KeyboardShortcuts, label: "Previous Project" },
{ key: "cycleNextProject" as keyof KeyboardShortcuts, label: "Next Project" },
{ key: "addProfile" as keyof KeyboardShortcuts, label: "Add Profile" },
].map(({ key, label }) => (
<div
key={key}
className="flex items-center justify-between p-3 rounded-lg bg-sidebar-accent/10 border border-sidebar-border hover:bg-sidebar-accent/20 transition-colors"
>
<span className="text-sm text-foreground">{label}</span>
<div className="flex items-center gap-2">
{editingShortcut === key ? (
<>
<Input
value={shortcutValue}
onChange={(e) => {
const value = e.target.value.toUpperCase();
setShortcutValue(value);
// Check for conflicts
const conflict = Object.entries(keyboardShortcuts).find(
([k, v]) => k !== key && v.toUpperCase() === value
);
if (conflict) {
setShortcutError(`Already used by ${conflict[0]}`);
} else {
setShortcutError(null);
}
}}
onKeyDown={(e) => {
if (e.key === "Enter" && !shortcutError && shortcutValue) {
setKeyboardShortcut(key, shortcutValue);
setEditingShortcut(null);
setShortcutValue("");
setShortcutError(null);
} else if (e.key === "Escape") {
setEditingShortcut(null);
setShortcutValue("");
setShortcutError(null);
}
}}
className="w-24 h-8 text-center font-mono"
placeholder="Key"
maxLength={2}
autoFocus
data-testid={`edit-shortcut-${key}`}
/>
<Button
size="sm"
variant="ghost"
className="h-8 w-8 p-0"
onClick={() => {
if (!shortcutError && shortcutValue) {
setKeyboardShortcut(key, shortcutValue);
setEditingShortcut(null);
setShortcutValue("");
setShortcutError(null);
}
}}
disabled={!!shortcutError || !shortcutValue}
>
<CheckCircle2 className="w-4 h-4" />
</Button>
<Button
size="sm"
variant="ghost"
className="h-8 w-8 p-0"
onClick={() => {
setEditingShortcut(null);
setShortcutValue("");
setShortcutError(null);
}}
>
<AlertCircle className="w-4 h-4" />
</Button>
</>
) : (
<>
<button
onClick={() => {
setEditingShortcut(key);
setShortcutValue(keyboardShortcuts[key]);
setShortcutError(null);
}}
className={cn(
"px-3 py-1.5 text-sm font-mono rounded bg-sidebar-accent/20 border border-sidebar-border hover:bg-sidebar-accent/30 transition-colors",
keyboardShortcuts[key] !== DEFAULT_KEYBOARD_SHORTCUTS[key] &&
"border-brand-500/50 bg-brand-500/10 text-brand-400"
)}
data-testid={`shortcut-${key}`}
>
{keyboardShortcuts[key]}
</button>
{keyboardShortcuts[key] !== DEFAULT_KEYBOARD_SHORTCUTS[key] && (
<span className="text-xs text-brand-400">(modified)</span>
)}
</>
)}
</div>
</div>
))}
</div>
</div>
{/* Information */}
<div className="flex items-start gap-3 p-4 rounded-lg bg-blue-500/10 border border-blue-500/20">
<AlertCircle className="w-5 h-5 text-blue-500 mt-0.5 shrink-0" />
<div className="text-sm">
<p className="font-medium text-blue-400">
About Keyboard Shortcuts
</p>
<p className="text-blue-400/80 text-xs mt-1">
Shortcuts won&apos;t trigger when typing in input fields. Use
single keys (A-Z, 0-9) or special keys like ` (backtick).
Changes take effect immediately.
</p>
</div>
</div>
</div>
</div>
{/* Feature Defaults Section */}
<div
id="defaults"

File diff suppressed because it is too large Load Diff