[Init] Initial commit - NetMesh terminal manager
Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
This commit is contained in:
235
components/settings/tabs/ai/SafetySettings.tsx
Normal file
235
components/settings/tabs/ai/SafetySettings.tsx
Normal file
@@ -0,0 +1,235 @@
|
||||
import React, { useCallback, useState } from "react";
|
||||
import { Plus, X } from "lucide-react";
|
||||
import type { AIPermissionMode } from "../../../../infrastructure/ai/types";
|
||||
import {
|
||||
DEFAULT_COMMAND_BLOCKLIST,
|
||||
MAX_COMMAND_TIMEOUT_SECONDS,
|
||||
MAX_RESPONSE_IDLE_TIMEOUT_SECONDS,
|
||||
} from "../../../../infrastructure/ai/types";
|
||||
import { useI18n } from "../../../../application/i18n/I18nProvider";
|
||||
import { Button } from "../../../ui/button";
|
||||
import { Select, SettingCard, SettingRow, SettingsAnchor, SettingsSection } from "../../settings-ui";
|
||||
|
||||
export const SafetySettings: React.FC<{
|
||||
globalPermissionMode: AIPermissionMode;
|
||||
setGlobalPermissionMode: (mode: AIPermissionMode) => void;
|
||||
commandBlocklist: string[];
|
||||
setCommandBlocklist: (value: string[]) => void;
|
||||
commandTimeout: number;
|
||||
setCommandTimeout: (value: number) => void;
|
||||
responseIdleTimeout: number;
|
||||
setResponseIdleTimeout: (value: number) => void;
|
||||
maxIterations: number;
|
||||
setMaxIterations: (value: number) => void;
|
||||
}> = ({
|
||||
globalPermissionMode,
|
||||
setGlobalPermissionMode,
|
||||
commandBlocklist,
|
||||
setCommandBlocklist,
|
||||
commandTimeout,
|
||||
setCommandTimeout,
|
||||
responseIdleTimeout,
|
||||
setResponseIdleTimeout,
|
||||
maxIterations,
|
||||
setMaxIterations,
|
||||
}) => {
|
||||
const { t } = useI18n();
|
||||
const [regexErrors, setRegexErrors] = useState<Record<number, string>>({});
|
||||
|
||||
const validatePattern = useCallback((pattern: string, idx: number): boolean => {
|
||||
if (!pattern) {
|
||||
setRegexErrors((prev) => {
|
||||
const next = { ...prev };
|
||||
delete next[idx];
|
||||
return next;
|
||||
});
|
||||
return true;
|
||||
}
|
||||
try {
|
||||
new RegExp(pattern);
|
||||
setRegexErrors((prev) => {
|
||||
const next = { ...prev };
|
||||
delete next[idx];
|
||||
return next;
|
||||
});
|
||||
return true;
|
||||
} catch (e) {
|
||||
setRegexErrors((prev) => ({
|
||||
...prev,
|
||||
[idx]: e instanceof Error ? e.message : String(e),
|
||||
}));
|
||||
return false;
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handlePatternChange = useCallback((value: string, idx: number) => {
|
||||
const next = [...commandBlocklist];
|
||||
next[idx] = value;
|
||||
validatePattern(value, idx);
|
||||
setCommandBlocklist(next);
|
||||
}, [commandBlocklist, setCommandBlocklist, validatePattern]);
|
||||
|
||||
const permissionModeOptions = [
|
||||
{ value: "observer", label: t('ai.safety.permissionMode.observer') },
|
||||
{ value: "confirm", label: t('ai.safety.permissionMode.confirm') },
|
||||
{ value: "auto", label: t('ai.safety.permissionMode.auto') },
|
||||
];
|
||||
|
||||
return (
|
||||
<SettingsSection title={t('ai.safety.title')}>
|
||||
<div className="flex flex-col gap-4">
|
||||
<SettingCard divided>
|
||||
<SettingRow
|
||||
anchorId="ai-safety-permission-mode"
|
||||
label={t('ai.safety.permissionMode')}
|
||||
description={t('ai.safety.permissionMode.description')}
|
||||
>
|
||||
<Select
|
||||
value={globalPermissionMode}
|
||||
options={permissionModeOptions}
|
||||
onChange={(val) => setGlobalPermissionMode(val as AIPermissionMode)}
|
||||
className="w-64"
|
||||
/>
|
||||
</SettingRow>
|
||||
|
||||
<SettingRow
|
||||
anchorId="ai-safety-response-idle-timeout"
|
||||
label={t('ai.safety.responseIdleTimeout')}
|
||||
description={t('ai.safety.responseIdleTimeout.description')}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="number"
|
||||
aria-label={t('ai.safety.responseIdleTimeout')}
|
||||
value={responseIdleTimeout}
|
||||
onChange={(e) => {
|
||||
const val = parseInt(e.target.value, 10);
|
||||
if (!isNaN(val)) setResponseIdleTimeout(val);
|
||||
}}
|
||||
min={1}
|
||||
max={MAX_RESPONSE_IDLE_TIMEOUT_SECONDS}
|
||||
className="w-20 h-9 rounded-md border border-input bg-background px-3 text-sm text-right focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
|
||||
/>
|
||||
<span className="text-xs text-muted-foreground">{t('ai.safety.responseIdleTimeout.unit')}</span>
|
||||
</div>
|
||||
</SettingRow>
|
||||
|
||||
<SettingRow
|
||||
anchorId="ai-safety-command-timeout"
|
||||
label={t('ai.safety.commandTimeout')}
|
||||
description={t('ai.safety.commandTimeout.description')}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="number"
|
||||
value={commandTimeout}
|
||||
onChange={(e) => {
|
||||
const val = parseInt(e.target.value, 10);
|
||||
if (!isNaN(val)) setCommandTimeout(val);
|
||||
}}
|
||||
min={1}
|
||||
max={MAX_COMMAND_TIMEOUT_SECONDS}
|
||||
className="w-20 h-9 rounded-md border border-input bg-background px-3 text-sm text-right focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
|
||||
/>
|
||||
<span className="text-xs text-muted-foreground">{t('ai.safety.commandTimeout.unit')}</span>
|
||||
</div>
|
||||
</SettingRow>
|
||||
|
||||
<SettingRow
|
||||
label={t('ai.safety.maxIterations')}
|
||||
description={t('ai.safety.maxIterations.description')}
|
||||
>
|
||||
<input
|
||||
type="number"
|
||||
value={maxIterations}
|
||||
onChange={(e) => {
|
||||
const val = parseInt(e.target.value, 10);
|
||||
if (!isNaN(val) && val > 0) setMaxIterations(val);
|
||||
}}
|
||||
min={1}
|
||||
max={100}
|
||||
className="w-20 h-9 rounded-md border border-input bg-background px-3 text-sm text-right focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
|
||||
/>
|
||||
</SettingRow>
|
||||
</SettingCard>
|
||||
|
||||
{/* Command Blocklist */}
|
||||
<SettingsAnchor anchorId="ai-safety-blocklist">
|
||||
<SettingCard padded className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium">{t('ai.safety.blocklist')}</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t('ai.safety.blocklist.description')}
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="text-xs"
|
||||
onClick={() => { setCommandBlocklist([...DEFAULT_COMMAND_BLOCKLIST]); setRegexErrors({}); }}
|
||||
>
|
||||
{t('ai.safety.blocklist.reset')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
{commandBlocklist.map((pattern, idx) => (
|
||||
<div key={idx} className="space-y-0.5">
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="text"
|
||||
value={pattern}
|
||||
onChange={(e) => handlePatternChange(e.target.value, idx)}
|
||||
className={`flex-1 h-8 rounded-md border bg-background px-3 text-xs font-mono focus-visible:outline-none focus-visible:ring-1 ${
|
||||
regexErrors[idx]
|
||||
? 'border-destructive focus-visible:ring-destructive'
|
||||
: 'border-input focus-visible:ring-ring'
|
||||
}`}
|
||||
placeholder={t('ai.safety.blocklist.placeholder')}
|
||||
/>
|
||||
<button
|
||||
onClick={() => {
|
||||
const next = commandBlocklist.filter((_, i) => i !== idx);
|
||||
setCommandBlocklist(next);
|
||||
setRegexErrors((prev) => {
|
||||
const updated: Record<number, string> = {};
|
||||
for (const [k, v] of Object.entries(prev)) {
|
||||
const ki = Number(k);
|
||||
if (ki < idx) updated[ki] = v as string;
|
||||
else if (ki > idx) updated[ki - 1] = v as string;
|
||||
}
|
||||
return updated;
|
||||
});
|
||||
}}
|
||||
className="p-1 rounded hover:bg-destructive/10 text-muted-foreground hover:text-destructive transition-colors"
|
||||
>
|
||||
<X size={14} />
|
||||
</button>
|
||||
</div>
|
||||
{regexErrors[idx] && (
|
||||
<p className="text-[11px] text-destructive pl-1">{regexErrors[idx]}</p>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="text-xs"
|
||||
onClick={() => setCommandBlocklist([...commandBlocklist, ''])}
|
||||
>
|
||||
<Plus size={14} className="mr-1" />
|
||||
{t('ai.safety.blocklist.add')}
|
||||
</Button>
|
||||
</SettingCard>
|
||||
</SettingsAnchor>
|
||||
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t('ai.safety.note')}
|
||||
</p>
|
||||
</div>
|
||||
</SettingsSection>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user