[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

This commit is contained in:
2026-09-13 18:24:01 +08:00
commit 3c72efcb7f
3255 changed files with 907009 additions and 0 deletions

View 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>
);
};