feat: add shell execution and kubeconfig management UI

Real-time approval modal, settings pages, tool calling auto-detect button,
and IPC command wrappers.

- Add ShellApprovalModal component for Tier 2 command approvals
- Add ShellExecution settings page
- Add KubeconfigManager settings page
- Update AIProviders page with tool calling detection button
- Add shell command wrappers to tauriCommands.ts
- Add routes for new settings pages

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
Shaun Arman 2026-06-05 08:14:03 -05:00
parent 117ab390a2
commit dd9e6c0d3d
6 changed files with 835 additions and 14 deletions

View File

@ -30,6 +30,9 @@ import Ollama from "@/pages/Settings/Ollama";
import Integrations from "@/pages/Settings/Integrations";
import MCPServers from "@/pages/Settings/MCPServers";
import Security from "@/pages/Settings/Security";
import ShellExecution from "@/pages/Settings/ShellExecution";
import KubeconfigManager from "@/pages/Settings/KubeconfigManager";
import { ShellApprovalModal } from "@/components/ShellApprovalModal";
const navItems = [
{ to: "/", icon: Home, label: "Dashboard" },
@ -177,9 +180,12 @@ export default function App() {
<Route path="/settings/integrations" element={<Integrations />} />
<Route path="/settings/mcp" element={<MCPServers />} />
<Route path="/settings/security" element={<Security />} />
<Route path="/settings/shell-execution" element={<ShellExecution />} />
<Route path="/settings/kubeconfig" element={<KubeconfigManager />} />
</Routes>
</main>
</div>
<ShellApprovalModal />
</div>
);
}

View File

@ -0,0 +1,166 @@
import { useEffect, useState } from 'react';
import { listen, UnlistenFn } from '@tauri-apps/api/event';
import { Button } from '@/components/ui';
import { Badge } from '@/components/ui';
import { AlertTriangle, Shield, Terminal, X } from 'lucide-react';
import { respondToShellApprovalCmd } from '@/lib/tauriCommands';
interface ShellApprovalRequest {
approval_id: string;
command: string;
tier: number;
reasoning: string;
risk_factors: string[];
}
export function ShellApprovalModal() {
const [request, setRequest] = useState<ShellApprovalRequest | null>(null);
const [isResponding, setIsResponding] = useState(false);
useEffect(() => {
let unlisten: UnlistenFn;
const setupListener = async () => {
unlisten = await listen<ShellApprovalRequest>(
'shell:approval-needed',
(event) => {
setRequest(event.payload);
}
);
};
setupListener();
return () => {
if (unlisten) {
unlisten();
}
};
}, []);
const handleResponse = async (decision: string) => {
if (!request) return;
setIsResponding(true);
try {
await respondToShellApprovalCmd(request.approval_id, decision);
setRequest(null);
} catch (error) {
console.error('Failed to respond to approval:', error);
} finally {
setIsResponding(false);
}
};
const handleDeny = () => handleResponse('deny');
const handleAllowOnce = () => handleResponse('allow_once');
const handleAllowSession = () => handleResponse('allow_session');
if (!request) return null;
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50">
<div className="relative bg-card border rounded-lg shadow-lg max-w-2xl w-full max-h-[90vh] overflow-y-auto m-4">
{/* Header */}
<div className="sticky top-0 bg-card border-b p-4 flex items-center justify-between">
<div className="flex items-center gap-2">
<Shield className="h-5 w-5 text-yellow-600" />
<h2 className="text-lg font-semibold">Command Approval Required</h2>
</div>
<button
onClick={() => !isResponding && setRequest(null)}
disabled={isResponding}
className="p-1 rounded hover:bg-accent text-muted-foreground"
>
<X className="h-4 w-4" />
</button>
</div>
{/* Content */}
<div className="p-6 space-y-4">
<p className="text-sm text-muted-foreground">
This command requires your approval before execution
</p>
{/* Command Display */}
<div className="rounded-lg bg-slate-950 p-4 font-mono text-sm">
<div className="flex items-center gap-2 mb-2">
<Terminal className="h-4 w-4 text-slate-400" />
<span className="text-slate-400">Command:</span>
</div>
<code className="text-green-400">{request.command}</code>
</div>
{/* Tier Badge */}
<div className="flex items-center gap-2">
<span className="text-sm text-muted-foreground">Safety Tier:</span>
<Badge variant="outline" className="bg-yellow-50 text-yellow-700 border-yellow-300">
Tier {request.tier}
</Badge>
</div>
{/* Reasoning */}
<div className="rounded-lg border border-yellow-300 bg-yellow-50 p-4">
<div className="flex gap-2">
<AlertTriangle className="h-4 w-4 text-yellow-700 flex-shrink-0 mt-0.5" />
<div>
<div className="font-medium text-yellow-900 mb-1">Why approval is needed:</div>
<div className="text-sm text-yellow-800">{request.reasoning}</div>
</div>
</div>
</div>
{/* Risk Factors */}
{request.risk_factors.length > 0 && (
<div className="space-y-2">
<div className="text-sm font-medium">Risk Factors:</div>
<ul className="list-disc list-inside text-sm text-muted-foreground space-y-1">
{request.risk_factors.map((factor, idx) => (
<li key={idx}>{factor}</li>
))}
</ul>
</div>
)}
{/* Safety Notice */}
<div className="rounded-lg bg-muted p-3 text-sm text-muted-foreground">
<div className="font-medium mb-1">Safety Controls:</div>
<ul className="list-disc list-inside space-y-1 text-xs">
<li>Command execution is logged and auditable</li>
<li>30-second timeout protection</li>
<li>PII detection before execution</li>
<li>Output is captured for review</li>
</ul>
</div>
</div>
{/* Footer */}
<div className="sticky bottom-0 bg-card border-t p-4 flex flex-col sm:flex-row gap-2 justify-end">
<Button
variant="destructive"
onClick={handleDeny}
disabled={isResponding}
className="w-full sm:w-auto"
>
Deny
</Button>
<Button
variant="outline"
onClick={handleAllowOnce}
disabled={isResponding}
className="w-full sm:w-auto"
>
Allow Once
</Button>
<Button
onClick={handleAllowSession}
disabled={isResponding}
className="w-full sm:w-auto"
>
Allow for Session
</Button>
</div>
</div>
</div>
);
}

View File

@ -17,6 +17,7 @@ export interface ProviderConfig {
session_id?: string;
user_id?: string;
use_datastore_upload?: boolean;
supports_tool_calling?: boolean;
}
export interface Message {
@ -333,6 +334,9 @@ export const applyRedactionsCmd = (logFileId: string, approvedSpanIds: string[])
export const testProviderConnectionCmd = (providerConfig: ProviderConfig) =>
invoke<ChatResponse>("test_provider_connection", { providerConfig });
export const detectToolCallingSupportCmd = (providerConfig: ProviderConfig) =>
invoke<boolean>("detect_tool_calling_support", { providerConfig });
export const createIssueCmd = (newIssue: NewIssue) =>
invoke<Issue>("create_issue", {
title: newIssue.title,
@ -683,3 +687,54 @@ export const listAllImageAttachmentsCmd = (search?: string, issueId?: string) =>
search: search ?? null,
issueId: issueId ?? null,
});
// ─── Shell Execution Commands ────────────────────────────────────────────────
export interface KubeconfigInfo {
id: string;
name: string;
context: string;
cluster_url?: string;
is_active: boolean;
}
export interface CommandExecution {
id: string;
command: string;
tier: number;
approval_status: string;
exit_code: number | null;
stdout: string | null;
stderr: string | null;
execution_time_ms: number | null;
executed_at: string;
}
export interface KubectlStatus {
installed: boolean;
path?: string;
version?: string;
}
export const uploadKubeconfigCmd = (name: string, content: string) =>
invoke<string>("upload_kubeconfig", { name, content });
export const listKubeconfigsCmd = () =>
invoke<KubeconfigInfo[]>("list_kubeconfigs");
export const activateKubeconfigCmd = (id: string) =>
invoke<void>("activate_kubeconfig", { id });
export const deleteKubeconfigCmd = (id: string) =>
invoke<void>("delete_kubeconfig", { id });
export const respondToShellApprovalCmd = (approvalId: string, decision: string) =>
invoke<void>("respond_to_shell_approval", { approvalId, decision });
export const listCommandExecutionsCmd = (issueId?: string) =>
invoke<CommandExecution[]>("list_command_executions", {
issueId: issueId ?? null,
});
export const checkKubectlInstalledCmd = () =>
invoke<KubectlStatus>("check_kubectl_installed");

View File

@ -19,10 +19,13 @@ import {
import { useSettingsStore } from "@/stores/settingsStore";
import {
testProviderConnectionCmd,
detectToolCallingSupportCmd,
saveAiProviderCmd,
loadAiProvidersCmd,
deleteAiProviderCmd,
listOllamaModelsCmd,
type ProviderConfig,
type OllamaModel,
} from "@/lib/tauriCommands";
export const CUSTOM_REST_MODELS = [
@ -64,6 +67,7 @@ const emptyProvider: ProviderConfig = {
api_format: undefined,
session_id: undefined,
user_id: undefined,
supports_tool_calling: false,
};
export default function AIProviders() {
@ -81,9 +85,11 @@ export default function AIProviders() {
const [isAdding, setIsAdding] = useState(false);
const [form, setForm] = useState<ProviderConfig>({ ...emptyProvider });
const [testResult, setTestResult] = useState<{ success: boolean; message: string } | null>(null);
const [isTesting, setIsTesting] = useState(false);
const [isTestingConnection, setIsTestingConnection] = useState(false);
const [isDetectingToolCalling, setIsDetectingToolCalling] = useState(false);
const [isCustomModel, setIsCustomModel] = useState(false);
const [customModelInput, setCustomModelInput] = useState("");
const [ollamaModels, setOllamaModels] = useState<OllamaModel[]>([]);
// Load providers from database on mount
// Note: Auto-testing of active provider is handled in App.tsx on startup
@ -99,6 +105,22 @@ export default function AIProviders() {
loadProviders();
}, [setProviders]);
// Load Ollama models when form provider type changes to ollama
useEffect(() => {
if (form.provider_type === "ollama") {
const loadOllamaModels = async () => {
try {
const models = await listOllamaModelsCmd();
setOllamaModels(models);
} catch (err) {
console.error("Failed to load Ollama models:", err);
setOllamaModels([]);
}
};
loadOllamaModels();
}
}, [form.provider_type]);
const startAdd = () => {
setForm({ ...emptyProvider });
setEditIndex(null);
@ -172,7 +194,7 @@ export default function AIProviders() {
};
const handleTest = async () => {
setIsTesting(true);
setIsTestingConnection(true);
setTestResult(null);
try {
const response = await testProviderConnectionCmd(form);
@ -180,7 +202,27 @@ export default function AIProviders() {
} catch (err) {
setTestResult({ success: false, message: String(err) });
} finally {
setIsTesting(false);
setIsTestingConnection(false);
}
};
const handleAutoDetectToolCalling = async () => {
setIsDetectingToolCalling(true);
setTestResult(null);
try {
const supportsTools = await detectToolCallingSupportCmd(form);
// Use functional update to avoid stale closure
setForm((prev) => ({ ...prev, supports_tool_calling: supportsTools }));
setTestResult({
success: supportsTools, // Align success with actual outcome
message: supportsTools
? "✅ Tool calling supported! Checkbox enabled automatically."
: "⚠️ Tool calling not supported. Checkbox disabled automatically.",
});
} catch (err) {
setTestResult({ success: false, message: `Auto-detect failed: ${String(err)}` });
} finally {
setIsDetectingToolCalling(false);
}
};
@ -289,12 +331,14 @@ export default function AIProviders() {
const type = v as ProviderConfig["provider_type"];
const defaults: Partial<ProviderConfig> =
type === "ollama"
? { api_url: "http://localhost:11434", api_key: "", model: "llama3.2:3b" }
? { api_url: "http://localhost:11434", api_key: "", model: "llama3.2:3b", supports_tool_calling: true }
: type === "openai"
? { api_url: "https://api.openai.com/v1" }
? { api_url: "https://api.openai.com/v1", supports_tool_calling: true }
: type === "anthropic"
? { api_url: "https://api.anthropic.com" }
: {};
? { api_url: "https://api.anthropic.com", supports_tool_calling: true }
: type === "azure"
? { supports_tool_calling: true }
: { supports_tool_calling: false }; // Custom providers default to false
setForm({ ...form, provider_type: type, ...defaults });
}}
>
@ -332,11 +376,35 @@ export default function AIProviders() {
{!(form.provider_type === "custom" && form.api_format === CUSTOM_REST_FORMAT) && (
<div className="space-y-2">
<Label>Model</Label>
<Input
value={form.model}
onChange={(e) => setForm({ ...form, model: e.target.value })}
placeholder="gpt-4o"
/>
{form.provider_type === "ollama" ? (
<Select
value={form.model}
onValueChange={(v) => setForm({ ...form, model: v })}
>
<SelectTrigger>
<SelectValue placeholder="Select installed model..." />
</SelectTrigger>
<SelectContent>
{ollamaModels.length > 0 ? (
ollamaModels.map((model) => (
<SelectItem key={model.name} value={model.name}>
{model.name}
</SelectItem>
))
) : (
<div className="px-2 py-1.5 text-sm text-muted-foreground">
No models installed. Go to Settings Ollama to pull models.
</div>
)}
</SelectContent>
</Select>
) : (
<Input
value={form.model}
onChange={(e) => setForm({ ...form, model: e.target.value })}
placeholder="gpt-4o"
/>
)}
</div>
)}
</div>
@ -506,6 +574,37 @@ export default function AIProviders() {
)}
</div>
)}
{/* Tool Calling Support Toggle */}
<div className="space-y-2 pt-4 border-t">
<div className="flex items-center justify-between">
<div className="space-y-0.5">
<Label htmlFor="supports-tool-calling" className="text-base">Tool Calling Support</Label>
<p className="text-xs text-muted-foreground">
Enable if this provider supports function/tool calling for shell execution and integrations
</p>
</div>
<input
id="supports-tool-calling"
type="checkbox"
checked={form.supports_tool_calling ?? false}
onChange={(e) =>
setForm({ ...form, supports_tool_calling: e.target.checked })
}
className="h-5 w-5 rounded border-gray-300 text-primary focus:ring-primary cursor-pointer"
/>
</div>
<Button
variant="outline"
size="sm"
onClick={handleAutoDetectToolCalling}
disabled={isTestingConnection || isDetectingToolCalling}
className="w-full"
>
<Zap className="w-4 h-4 mr-2" />
{isDetectingToolCalling ? "Detecting..." : "Auto-Detect Tool Calling Support"}
</Button>
</div>
</div>
</>
)}
@ -532,8 +631,8 @@ export default function AIProviders() {
<div className="flex items-center gap-2">
<Button onClick={handleSave}>Save</Button>
<Button variant="outline" onClick={handleTest} disabled={isTesting}>
{isTesting ? "Testing..." : "Test Connection"}
<Button variant="outline" onClick={handleTest} disabled={isTestingConnection || isDetectingToolCalling}>
{isTestingConnection ? "Testing..." : "Test Connection"}
</Button>
<Button variant="ghost" onClick={handleCancel}>
Cancel

View File

@ -0,0 +1,251 @@
import { useState, useEffect } from 'react';
import { Upload, Check, Trash2, FileCode } from 'lucide-react';
import { Button, Card, CardHeader, CardTitle, CardContent, Badge } from '@/components/ui';
import {
uploadKubeconfigCmd,
listKubeconfigsCmd,
activateKubeconfigCmd,
deleteKubeconfigCmd,
type KubeconfigInfo,
} from '@/lib/tauriCommands';
export default function KubeconfigManager() {
const [configs, setConfigs] = useState<KubeconfigInfo[]>([]);
const [isLoading, setIsLoading] = useState(false);
const [uploadContent, setUploadContent] = useState('');
const [uploadName, setUploadName] = useState('');
const [error, setError] = useState('');
const loadConfigs = async () => {
try {
const data = await listKubeconfigsCmd();
setConfigs(data);
} catch (err) {
setError(String(err));
}
};
useEffect(() => {
loadConfigs();
}, []);
const handleFileUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (!file) return;
const reader = new FileReader();
reader.onload = async (event) => {
const content = event.target?.result as string;
setUploadContent(content);
setUploadName(file.name.replace(/\.(yaml|yml)$/, ''));
};
reader.readAsText(file);
};
const handleUpload = async () => {
if (!uploadContent || !uploadName) {
setError('Please select a file and provide a name');
return;
}
setIsLoading(true);
setError('');
try {
await uploadKubeconfigCmd(uploadName, uploadContent);
setUploadContent('');
setUploadName('');
await loadConfigs();
} catch (err) {
setError(String(err));
} finally {
setIsLoading(false);
}
};
const handleActivate = async (id: string) => {
setIsLoading(true);
setError('');
try {
await activateKubeconfigCmd(id);
await loadConfigs();
} catch (err) {
setError(String(err));
} finally {
setIsLoading(false);
}
};
const handleDelete = async (id: string) => {
if (!confirm('Are you sure you want to delete this kubeconfig?')) return;
setIsLoading(true);
setError('');
try {
await deleteKubeconfigCmd(id);
await loadConfigs();
} catch (err) {
setError(String(err));
} finally {
setIsLoading(false);
}
};
return (
<div className="p-6 space-y-6">
<div>
<h1 className="text-3xl font-bold mb-2">Kubeconfig Manager</h1>
<p className="text-muted-foreground">
Upload and manage multiple Kubernetes cluster configurations for kubectl commands
</p>
</div>
{error && (
<div className="rounded-lg border border-red-300 bg-red-50 p-4 text-sm text-red-800">
{error}
</div>
)}
{/* Upload Section */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Upload className="h-5 w-5" />
Upload Kubeconfig
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div>
<label className="block text-sm font-medium mb-2">Select File</label>
<input
type="file"
accept=".yaml,.yml"
onChange={handleFileUpload}
className="block w-full text-sm text-foreground file:mr-4 file:py-2 file:px-4 file:rounded file:border file:border-input file:text-sm file:font-semibold file:bg-secondary file:text-secondary-foreground hover:file:bg-secondary/80 cursor-pointer"
/>
</div>
{uploadContent && (
<>
<div>
<label htmlFor="config-name" className="block text-sm font-medium mb-2">
Configuration Name
</label>
<input
id="config-name"
type="text"
value={uploadName}
onChange={(e) => setUploadName(e.target.value)}
placeholder="e.g., production-cluster"
className="w-full px-3 py-2 border rounded-md"
/>
</div>
<div className="rounded-lg bg-slate-950 p-4 font-mono text-xs text-slate-400 max-h-60 overflow-y-auto">
<pre>{uploadContent.substring(0, 500)}...</pre>
</div>
<Button onClick={handleUpload} disabled={isLoading} className="w-full">
Upload Kubeconfig
</Button>
</>
)}
</CardContent>
</Card>
{/* Configs List */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<FileCode className="h-5 w-5" />
Configured Clusters ({configs.length})
</CardTitle>
</CardHeader>
<CardContent>
{configs.length === 0 ? (
<p className="text-sm text-muted-foreground text-center py-8">
No kubeconfig files uploaded yet
</p>
) : (
<div className="space-y-3">
{configs.map((config) => (
<div
key={config.id}
className={`p-4 rounded-lg border ${
config.is_active
? 'border-primary bg-primary/5'
: 'border-border'
}`}
>
<div className="flex items-start justify-between">
<div className="space-y-1 flex-1">
<div className="flex items-center gap-2">
<h3 className="font-semibold">{config.name}</h3>
{config.is_active && (
<Badge variant="default" className="bg-green-600">
<Check className="h-3 w-3 mr-1" />
Active
</Badge>
)}
</div>
<div className="text-sm text-muted-foreground space-y-1">
<div>
<span className="font-medium">Context:</span> {config.context}
</div>
{config.cluster_url && (
<div>
<span className="font-medium">Cluster:</span> {config.cluster_url}
</div>
)}
</div>
</div>
<div className="flex gap-2">
{!config.is_active && (
<Button
variant="outline"
size="sm"
onClick={() => handleActivate(config.id)}
disabled={isLoading}
>
Activate
</Button>
)}
<Button
variant="ghost"
size="sm"
onClick={() => handleDelete(config.id)}
disabled={isLoading}
className="text-red-600 hover:text-red-700 hover:bg-red-50"
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
</div>
</div>
))}
</div>
)}
</CardContent>
</Card>
{/* Info Card */}
<Card>
<CardHeader>
<CardTitle>About Kubeconfig Files</CardTitle>
</CardHeader>
<CardContent className="space-y-2 text-sm text-muted-foreground">
<p>
Kubeconfig files contain authentication credentials and cluster connection details for
kubectl commands.
</p>
<ul className="list-disc list-inside space-y-1 ml-2">
<li>Upload your cluster's kubeconfig file (usually ~/.kube/config)</li>
<li>Multiple clusters can be configured and switched between</li>
<li>The active configuration is used for kubectl commands</li>
<li>All kubeconfig files are encrypted using AES-256-GCM</li>
</ul>
</CardContent>
</Card>
</div>
);
}

View File

@ -0,0 +1,244 @@
import { useState, useEffect } from 'react';
import { Terminal, CheckCircle, XCircle, Shield, History } from 'lucide-react';
import { Button, Card, CardHeader, CardTitle, CardContent, Badge } from '@/components/ui';
import { Link } from 'react-router-dom';
import {
checkKubectlInstalledCmd,
listCommandExecutionsCmd,
type KubectlStatus,
type CommandExecution,
} from '@/lib/tauriCommands';
export default function ShellExecution() {
const [kubectlStatus, setKubectlStatus] = useState<KubectlStatus | null>(null);
const [executions, setExecutions] = useState<CommandExecution[]>([]);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState('');
const loadKubectlStatus = async () => {
try {
const status = await checkKubectlInstalledCmd();
setKubectlStatus(status);
} catch (err) {
setError(String(err));
}
};
const loadExecutions = async () => {
setIsLoading(true);
try {
const data = await listCommandExecutionsCmd();
setExecutions(data);
} catch (err) {
setError(String(err));
} finally {
setIsLoading(false);
}
};
useEffect(() => {
loadKubectlStatus();
loadExecutions();
}, []);
const getTierBadge = (tier: number) => {
const colors = {
1: 'bg-green-100 text-green-700 border-green-300',
2: 'bg-yellow-100 text-yellow-700 border-yellow-300',
3: 'bg-red-100 text-red-700 border-red-300',
};
return colors[tier as keyof typeof colors] || colors[1];
};
const getStatusBadge = (status: string) => {
const config = {
auto: { label: 'Auto-executed', color: 'bg-blue-100 text-blue-700 border-blue-300' },
approved: { label: 'Approved', color: 'bg-green-100 text-green-700 border-green-300' },
denied: { label: 'Denied', color: 'bg-red-100 text-red-700 border-red-300' },
};
const statusConfig = config[status as keyof typeof config] || config.auto;
return statusConfig;
};
return (
<div className="p-6 space-y-6">
<div>
<h1 className="text-3xl font-bold mb-2">Shell Execution</h1>
<p className="text-muted-foreground">
Configure and monitor autonomous shell command execution with intelligent safety controls
</p>
</div>
{error && (
<div className="rounded-lg border border-red-300 bg-red-50 p-4 text-sm text-red-800">
{error}
</div>
)}
{/* kubectl Status */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Terminal className="h-5 w-5" />
kubectl Status
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
{kubectlStatus ? (
<>
<div className="flex items-center gap-3">
{kubectlStatus.installed ? (
<>
<CheckCircle className="h-5 w-5 text-green-600" />
<span className="font-medium text-green-700">kubectl is installed</span>
</>
) : (
<>
<XCircle className="h-5 w-5 text-red-600" />
<span className="font-medium text-red-700">kubectl is not installed</span>
</>
)}
</div>
{kubectlStatus.path && (
<div className="text-sm text-muted-foreground">
<span className="font-medium">Path:</span> {kubectlStatus.path}
</div>
)}
{kubectlStatus.version && (
<div className="rounded-lg bg-slate-950 p-3 font-mono text-xs text-slate-400 overflow-x-auto">
<pre>{kubectlStatus.version}</pre>
</div>
)}
</>
) : (
<p className="text-sm text-muted-foreground">Checking kubectl status...</p>
)}
<div className="pt-2">
<Link to="/settings/kubeconfig">
<Button variant="outline" className="w-full">
Manage Kubeconfig Files
</Button>
</Link>
</div>
</CardContent>
</Card>
{/* Safety Architecture */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Shield className="h-5 w-5" />
Safety Architecture
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<p className="text-sm text-muted-foreground">
Commands are automatically classified into three safety tiers:
</p>
<div className="space-y-3">
<div className="flex items-start gap-3 p-3 rounded-lg bg-green-50 border border-green-200">
<Badge className={getTierBadge(1)}>Tier 1</Badge>
<div className="space-y-1">
<div className="font-medium text-green-900">Auto-execute (Read-only)</div>
<div className="text-sm text-green-800">
kubectl get, describe, logs | cat, grep, ls
</div>
</div>
</div>
<div className="flex items-start gap-3 p-3 rounded-lg bg-yellow-50 border border-yellow-200">
<Badge className={getTierBadge(2)}>Tier 2</Badge>
<div className="space-y-1">
<div className="font-medium text-yellow-900">Require approval (Mutating)</div>
<div className="text-sm text-yellow-800">
kubectl apply, delete, scale | ssh, chmod, systemctl restart
</div>
</div>
</div>
<div className="flex items-start gap-3 p-3 rounded-lg bg-red-50 border border-red-200">
<Badge className={getTierBadge(3)}>Tier 3</Badge>
<div className="space-y-1">
<div className="font-medium text-red-900">Always deny (Destructive)</div>
<div className="text-sm text-red-800">
rm -rf, shutdown, mkfs, dd
</div>
</div>
</div>
</div>
</CardContent>
</Card>
{/* Command Execution History */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<History className="h-5 w-5" />
Recent Command Executions ({executions.length})
</CardTitle>
</CardHeader>
<CardContent>
{isLoading ? (
<p className="text-sm text-muted-foreground text-center py-8">Loading...</p>
) : executions.length === 0 ? (
<p className="text-sm text-muted-foreground text-center py-8">
No command executions yet
</p>
) : (
<div className="space-y-3">
{executions.slice(0, 10).map((exec) => {
const statusConfig = getStatusBadge(exec.approval_status);
return (
<div key={exec.id} className="p-3 rounded-lg border space-y-2">
<div className="flex items-start justify-between">
<div className="flex-1 min-w-0">
<code className="text-sm font-mono text-foreground break-all">
{exec.command}
</code>
</div>
<div className="flex gap-2 ml-3 flex-shrink-0">
<Badge className={getTierBadge(exec.tier)}>
T{exec.tier}
</Badge>
<Badge className={statusConfig.color}>
{statusConfig.label}
</Badge>
</div>
</div>
<div className="flex items-center gap-4 text-xs text-muted-foreground">
{exec.exit_code !== undefined && (
<span className={exec.exit_code === 0 ? 'text-green-600' : 'text-red-600'}>
Exit: {exec.exit_code}
</span>
)}
{exec.execution_time_ms !== undefined && (
<span>{exec.execution_time_ms}ms</span>
)}
<span>{new Date(exec.executed_at).toLocaleString()}</span>
</div>
{exec.stdout && (
<details className="text-xs">
<summary className="cursor-pointer text-muted-foreground hover:text-foreground">
Show output
</summary>
<pre className="mt-2 p-2 rounded bg-slate-950 text-slate-400 overflow-x-auto max-h-40">
{exec.stdout}
</pre>
</details>
)}
</div>
);
})}
</div>
)}
</CardContent>
</Card>
</div>
);
}