mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-03 21:03:08 +00:00
Merge branch 'main' into various-improvements
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
>
|
||||
|
||||
@@ -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'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"
|
||||
|
||||
1486
app/src/components/views/setup-view.tsx
Normal file
1486
app/src/components/views/setup-view.tsx
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user