mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-01-31 20:03:37 +00:00
feat: enhance terminal functionality and settings
- Added new endpoints for terminal settings: GET and PUT /settings to retrieve and update terminal configurations. - Implemented session limit checks during session creation, returning a 429 status when the limit is reached. - Introduced a new TerminalSection in settings view for customizing terminal appearance and behavior, including font family, default font size, line height, and screen reader mode. - Added support for new terminal features such as search functionality and improved error handling with a TerminalErrorBoundary component. - Updated terminal layout persistence to include session IDs for reconnection and enhanced terminal state management. - Introduced new keyboard shortcuts for terminal actions, including creating new terminal tabs. - Enhanced UI with scrollbar theming for terminal components.
This commit is contained in:
@@ -103,6 +103,7 @@ const SHORTCUT_LABELS: Record<keyof KeyboardShortcuts, string> = {
|
||||
splitTerminalRight: "Split Right",
|
||||
splitTerminalDown: "Split Down",
|
||||
closeTerminal: "Close Terminal",
|
||||
newTerminalTab: "New Tab",
|
||||
};
|
||||
|
||||
// Categorize shortcuts for color coding
|
||||
@@ -127,6 +128,7 @@ const SHORTCUT_CATEGORIES: Record<keyof KeyboardShortcuts, "navigation" | "ui" |
|
||||
splitTerminalRight: "action",
|
||||
splitTerminalDown: "action",
|
||||
closeTerminal: "action",
|
||||
newTerminalTab: "action",
|
||||
};
|
||||
|
||||
// Category colors
|
||||
|
||||
@@ -12,6 +12,7 @@ import { ApiKeysSection } from "./settings-view/api-keys/api-keys-section";
|
||||
import { ClaudeCliStatus } from "./settings-view/cli-status/claude-cli-status";
|
||||
import { AIEnhancementSection } from "./settings-view/ai-enhancement";
|
||||
import { AppearanceSection } from "./settings-view/appearance/appearance-section";
|
||||
import { TerminalSection } from "./settings-view/terminal/terminal-section";
|
||||
import { AudioSection } from "./settings-view/audio/audio-section";
|
||||
import { KeyboardShortcutsSection } from "./settings-view/keyboard-shortcuts/keyboard-shortcuts-section";
|
||||
import { FeatureDefaultsSection } from "./settings-view/feature-defaults/feature-defaults-section";
|
||||
@@ -108,6 +109,8 @@ export function SettingsView() {
|
||||
onThemeChange={handleSetTheme}
|
||||
/>
|
||||
);
|
||||
case "terminal":
|
||||
return <TerminalSection />;
|
||||
case "keyboard":
|
||||
return (
|
||||
<KeyboardShortcutsSection
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { LucideIcon } from "lucide-react";
|
||||
import {
|
||||
Key,
|
||||
Terminal,
|
||||
SquareTerminal,
|
||||
Palette,
|
||||
Settings2,
|
||||
Volume2,
|
||||
@@ -23,6 +24,7 @@ export const NAV_ITEMS: NavigationItem[] = [
|
||||
{ id: "claude", label: "Claude", icon: Terminal },
|
||||
{ id: "ai-enhancement", label: "AI Enhancement", icon: Sparkles },
|
||||
{ id: "appearance", label: "Appearance", icon: Palette },
|
||||
{ id: "terminal", label: "Terminal", icon: SquareTerminal },
|
||||
{ id: "keyboard", label: "Keyboard Shortcuts", icon: Settings2 },
|
||||
{ id: "audio", label: "Audio", icon: Volume2 },
|
||||
{ id: "defaults", label: "Feature Defaults", icon: FlaskConical },
|
||||
|
||||
@@ -5,6 +5,7 @@ export type SettingsViewId =
|
||||
| "claude"
|
||||
| "ai-enhancement"
|
||||
| "appearance"
|
||||
| "terminal"
|
||||
| "keyboard"
|
||||
| "audio"
|
||||
| "defaults"
|
||||
|
||||
@@ -0,0 +1,175 @@
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { Slider } from "@/components/ui/slider";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { SquareTerminal } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useAppStore } from "@/store/app-store";
|
||||
import { toast } from "sonner";
|
||||
import { TERMINAL_FONT_OPTIONS } from "@/config/terminal-themes";
|
||||
|
||||
export function TerminalSection() {
|
||||
const {
|
||||
terminalState,
|
||||
setTerminalDefaultRunScript,
|
||||
setTerminalScreenReaderMode,
|
||||
setTerminalFontFamily,
|
||||
setTerminalScrollbackLines,
|
||||
setTerminalLineHeight,
|
||||
setTerminalDefaultFontSize,
|
||||
} = useAppStore();
|
||||
|
||||
const {
|
||||
defaultRunScript,
|
||||
screenReaderMode,
|
||||
fontFamily,
|
||||
scrollbackLines,
|
||||
lineHeight,
|
||||
defaultFontSize,
|
||||
} = terminalState;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"rounded-2xl overflow-hidden",
|
||||
"border border-border/50",
|
||||
"bg-gradient-to-br from-card/90 via-card/70 to-card/80 backdrop-blur-xl",
|
||||
"shadow-sm shadow-black/5"
|
||||
)}
|
||||
>
|
||||
<div className="p-6 border-b border-border/50 bg-gradient-to-r from-transparent via-accent/5 to-transparent">
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<div className="w-9 h-9 rounded-xl bg-gradient-to-br from-green-500/20 to-green-600/10 flex items-center justify-center border border-green-500/20">
|
||||
<SquareTerminal className="w-5 h-5 text-green-500" />
|
||||
</div>
|
||||
<h2 className="text-lg font-semibold text-foreground tracking-tight">Terminal</h2>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground/80 ml-12">
|
||||
Customize terminal appearance and behavior. Theme follows your app theme in Appearance settings.
|
||||
</p>
|
||||
</div>
|
||||
<div className="p-6 space-y-6">
|
||||
{/* Font Family */}
|
||||
<div className="space-y-3">
|
||||
<Label className="text-foreground font-medium">Font Family</Label>
|
||||
<select
|
||||
value={fontFamily}
|
||||
onChange={(e) => {
|
||||
setTerminalFontFamily(e.target.value);
|
||||
toast.info("Font family changed", {
|
||||
description: "Restart terminal for changes to take effect",
|
||||
});
|
||||
}}
|
||||
className={cn(
|
||||
"w-full px-3 py-2 rounded-lg",
|
||||
"bg-accent/30 border border-border/50",
|
||||
"text-foreground text-sm",
|
||||
"focus:outline-none focus:ring-2 focus:ring-green-500/30"
|
||||
)}
|
||||
>
|
||||
{TERMINAL_FONT_OPTIONS.map((font) => (
|
||||
<option key={font.value} value={font.value}>
|
||||
{font.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Default Font Size */}
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label className="text-foreground font-medium">Default Font Size</Label>
|
||||
<span className="text-sm text-muted-foreground">{defaultFontSize}px</span>
|
||||
</div>
|
||||
<Slider
|
||||
value={[defaultFontSize]}
|
||||
min={8}
|
||||
max={32}
|
||||
step={1}
|
||||
onValueChange={([value]) => setTerminalDefaultFontSize(value)}
|
||||
className="flex-1"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Line Height */}
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label className="text-foreground font-medium">Line Height</Label>
|
||||
<span className="text-sm text-muted-foreground">{lineHeight.toFixed(1)}</span>
|
||||
</div>
|
||||
<Slider
|
||||
value={[lineHeight]}
|
||||
min={1.0}
|
||||
max={2.0}
|
||||
step={0.1}
|
||||
onValueChange={([value]) => {
|
||||
setTerminalLineHeight(value);
|
||||
}}
|
||||
onValueCommit={() => {
|
||||
toast.info("Line height changed", {
|
||||
description: "Restart terminal for changes to take effect",
|
||||
});
|
||||
}}
|
||||
className="flex-1"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Scrollback Lines */}
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label className="text-foreground font-medium">Scrollback Buffer</Label>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{(scrollbackLines / 1000).toFixed(0)}k lines
|
||||
</span>
|
||||
</div>
|
||||
<Slider
|
||||
value={[scrollbackLines]}
|
||||
min={1000}
|
||||
max={100000}
|
||||
step={1000}
|
||||
onValueChange={([value]) => setTerminalScrollbackLines(value)}
|
||||
onValueCommit={() => {
|
||||
toast.info("Scrollback changed", {
|
||||
description: "Restart terminal for changes to take effect",
|
||||
});
|
||||
}}
|
||||
className="flex-1"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Default Run Script */}
|
||||
<div className="space-y-3">
|
||||
<Label className="text-foreground font-medium">Default Run Script</Label>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Command to run automatically when opening a new terminal (e.g., "claude", "codex")
|
||||
</p>
|
||||
<Input
|
||||
value={defaultRunScript}
|
||||
onChange={(e) => setTerminalDefaultRunScript(e.target.value)}
|
||||
placeholder="e.g., claude, codex, npm run dev"
|
||||
className="bg-accent/30 border-border/50"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Screen Reader Mode */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="space-y-1">
|
||||
<Label className="text-foreground font-medium">Screen Reader Mode</Label>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Enable accessibility mode for screen readers
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
checked={screenReaderMode}
|
||||
onCheckedChange={(checked) => {
|
||||
setTerminalScreenReaderMode(checked);
|
||||
toast.success(checked ? "Screen reader mode enabled" : "Screen reader mode disabled", {
|
||||
description: "Restart terminal for changes to take effect",
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -4,14 +4,14 @@ interface TerminalOutputProps {
|
||||
|
||||
export function TerminalOutput({ lines }: TerminalOutputProps) {
|
||||
return (
|
||||
<div className="bg-zinc-900 rounded-lg p-4 font-mono text-sm max-h-48 overflow-y-auto">
|
||||
<div className="bg-card border border-border rounded-lg p-4 font-mono text-sm max-h-48 overflow-y-auto">
|
||||
{lines.map((line, index) => (
|
||||
<div key={index} className="text-zinc-400">
|
||||
<span className="text-green-500">$</span> {line}
|
||||
<div key={index} className="text-foreground">
|
||||
<span className="text-primary">$</span> {line}
|
||||
</div>
|
||||
))}
|
||||
{lines.length === 0 && (
|
||||
<div className="text-zinc-500 italic">Waiting for output...</div>
|
||||
<div className="text-muted-foreground italic">Waiting for output...</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,94 @@
|
||||
import React, { Component, ErrorInfo } from "react";
|
||||
import { AlertCircle, RefreshCw } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface Props {
|
||||
children: React.ReactNode;
|
||||
sessionId: string;
|
||||
onRestart?: () => void;
|
||||
}
|
||||
|
||||
interface State {
|
||||
hasError: boolean;
|
||||
error: Error | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* BUG-06 fix: Error boundary for terminal components
|
||||
* Catches xterm.js errors (WebGL context loss, canvas errors, etc.)
|
||||
* and displays a friendly recovery UI instead of crashing the app.
|
||||
*/
|
||||
export class TerminalErrorBoundary extends Component<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.state = { hasError: false, error: null };
|
||||
}
|
||||
|
||||
static getDerivedStateFromError(error: Error): State {
|
||||
return { hasError: true, error };
|
||||
}
|
||||
|
||||
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
|
||||
console.error("[TerminalErrorBoundary] Terminal crashed:", {
|
||||
sessionId: this.props.sessionId,
|
||||
error: error.message,
|
||||
stack: error.stack,
|
||||
componentStack: errorInfo.componentStack,
|
||||
});
|
||||
}
|
||||
|
||||
handleRestart = () => {
|
||||
this.setState({ hasError: false, error: null });
|
||||
this.props.onRestart?.();
|
||||
};
|
||||
|
||||
render() {
|
||||
if (this.state.hasError) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col items-center justify-center h-full w-full",
|
||||
"bg-background/95 backdrop-blur-sm",
|
||||
"p-6 text-center gap-4"
|
||||
)}
|
||||
>
|
||||
<div className="w-12 h-12 rounded-full bg-destructive/10 flex items-center justify-center">
|
||||
<AlertCircle className="w-6 h-6 text-destructive" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<h3 className="text-lg font-semibold text-foreground">
|
||||
Terminal Crashed
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground max-w-sm">
|
||||
{this.state.error?.message?.includes("WebGL")
|
||||
? "WebGL context was lost. This can happen with GPU driver issues."
|
||||
: "An unexpected error occurred in the terminal."}
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={this.handleRestart}
|
||||
className="gap-2"
|
||||
>
|
||||
<RefreshCw className="w-4 h-4" />
|
||||
Restart Terminal
|
||||
</Button>
|
||||
{this.state.error && (
|
||||
<details className="text-xs text-muted-foreground max-w-md">
|
||||
<summary className="cursor-pointer hover:text-foreground">
|
||||
Technical details
|
||||
</summary>
|
||||
<pre className="mt-2 p-2 bg-muted/50 rounded text-left overflow-auto max-h-32">
|
||||
{this.state.error.message}
|
||||
</pre>
|
||||
</details>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return this.props.children;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user