feat: remove legacy OpenCode runtime

Cut product flows over to Coding/Pi and retain only the migration-owned v1 boundary. Promote supported native optional packages because electron-builder omitted pnpm transitive optional closure from the packaged ASAR.
This commit is contained in:
2026-08-24 12:17:43 +08:00
parent 61fede2b4d
commit 5a275b93a7
199 changed files with 1330 additions and 64725 deletions

View File

@@ -8,10 +8,8 @@ import {
ChevronDown,
ExternalLink,
FileText,
FolderPlus,
KeyRound,
RefreshCw,
RotateCw,
} from 'lucide-react';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
@@ -39,8 +37,6 @@ import {
} from '@/lib/telemetry';
import { hostApiFetch } from '@/lib/host-api';
import { cn } from '@/lib/utils';
import { useOpencodeStore } from '@/stores/opencode';
import { useProjectConfigStore } from '@/stores/project-config';
import { useSettingsStore } from '@/stores/settings';
import { useTranslation } from 'react-i18next';
import { toast } from 'sonner';
@@ -71,22 +67,6 @@ export function Settings() {
setTelemetryEnabled,
} = useSettingsStore();
const opencodeStatus = useOpencodeStore((state) => state.status);
const opencodeRuntimeConfigSummary = useOpencodeStore((state) => state.runtimeConfigSummary);
const opencodeProjects = useOpencodeStore((state) => state.projects);
const activeOpencodeProject = useOpencodeStore((state) => state.activeProject);
const opencodeLoading = useOpencodeStore((state) => state.loading);
const opencodeError = useOpencodeStore((state) => state.error);
const refreshOpencodeStatus = useOpencodeStore((state) => state.refreshStatus);
const restartOpencodeRuntime = useOpencodeStore((state) => state.restart);
const loadOpencodeRuntimeConfigSummary = useOpencodeStore(
(state) => state.loadRuntimeConfigSummary
);
const loadOpencodeProjects = useOpencodeStore((state) => state.loadProjects);
const pickAndOpenOpencodeProject = useOpencodeStore((state) => state.pickAndOpenProject);
const setActiveOpencodeProject = useOpencodeStore((state) => state.setActiveProject);
const loadProjectConfig = useProjectConfigStore((state) => state.load);
const [proxyServerDraft, setProxyServerDraft] = useState('');
const [proxyHttpServerDraft, setProxyHttpServerDraft] = useState('');
const [proxyHttpsServerDraft, setProxyHttpsServerDraft] = useState('');
@@ -103,7 +83,6 @@ export function Settings() {
const [adminPassword, setAdminPassword] = useState('');
const [adminPasswordError, setAdminPasswordError] = useState<string | null>(null);
const [adminUnlocking, setAdminUnlocking] = useState(false);
const [workspaceProjectError, setWorkspaceProjectError] = useState<string | null>(null);
useEffect(() => {
setProxyEnabledDraft(proxyEnabled);
@@ -144,20 +123,6 @@ export function Settings() {
return unsubscribe;
}, [devModeUnlocked]);
useEffect(() => {
void Promise.allSettled([
refreshOpencodeStatus(),
]);
}, [refreshOpencodeStatus]);
useEffect(() => {
if (!devModeUnlocked) return;
void Promise.allSettled([
loadOpencodeRuntimeConfigSummary(),
loadOpencodeProjects(),
]);
}, [devModeUnlocked, loadOpencodeProjects, loadOpencodeRuntimeConfigSummary]);
const proxySettingsDirty = useMemo(() => {
return (
proxyEnabledDraft !== proxyEnabled ||
@@ -212,14 +177,6 @@ export function Settings() {
}
};
const handleRefreshOpencodeSettings = () => {
void Promise.allSettled([
refreshOpencodeStatus(),
loadOpencodeRuntimeConfigSummary(),
loadOpencodeProjects(),
]);
};
const handleRevealAdditionalSettings = () => {
if (showAdditionalSettings) {
setShowAdditionalSettings(false);
@@ -257,30 +214,6 @@ export function Settings() {
setShowAdditionalSettings(false);
};
const handleAddOpencodeProject = async () => {
const project = await pickAndOpenOpencodeProject();
if (!project) return;
await activateVerifiedProject(project.id);
};
const activateVerifiedProject = async (projectId: string) => {
try {
setWorkspaceProjectError(null);
const result = await loadProjectConfig(projectId);
if (result.status === 'valid') {
await setActiveOpencodeProject(projectId);
return;
}
if (result.status === 'missing') {
setWorkspaceProjectError('Project configuration is missing. Create the project from the sidebar first.');
return;
}
setWorkspaceProjectError(result.error ?? 'Project configuration is invalid.');
} catch (error) {
setWorkspaceProjectError(error instanceof Error ? error.message : String(error));
}
};
const handleShowLogs = async () => {
try {
const logs = await hostApiFetch<{ content: string }>('/api/logs?tailLines=100');
@@ -359,48 +292,6 @@ export function Settings() {
</div>
</section>
<section className="space-y-5" data-testid="settings-runtime-summary">
<div className="flex items-start justify-between gap-4">
<div>
<h2 className="text-lg font-medium">Runtime</h2>
</div>
<Badge variant={opencodeStatus.state === 'running' ? 'default' : 'secondary'}>
{opencodeStatus.state}
</Badge>
</div>
<div className="flex flex-wrap items-center justify-between gap-4 rounded-lg border bg-card p-4">
<div className="flex min-w-0 flex-wrap items-center gap-x-6 gap-y-2 text-sm">
<div>
<div className="text-xs text-muted-foreground">Status</div>
<div className="font-medium">{opencodeStatus.state}</div>
</div>
<div>
<div className="text-xs text-muted-foreground">Port</div>
<div className="font-medium">{opencodeStatus.port}</div>
</div>
</div>
<Button
variant="outline"
onClick={() => void restartOpencodeRuntime()}
disabled={opencodeLoading}
aria-label="Restart runtime"
data-testid="settings-runtime-restart"
>
<RotateCw className={cn('mr-2 h-4 w-4', opencodeLoading && 'animate-spin')} />
Restart runtime
</Button>
</div>
{(opencodeStatus.error || opencodeError) && (
<div className="rounded-md border border-destructive/30 bg-destructive/10 px-3 py-2 text-xs text-destructive">
{opencodeStatus.error ?? opencodeError}
</div>
)}
</section>
<Separator />
<section className="space-y-5" data-testid="settings-updates">
<div>
<h2 className="text-lg font-medium">{t('updates.title')}</h2>
@@ -470,133 +361,16 @@ export function Settings() {
<DisclosureContent open innerClassName="space-y-8">
<Separator />
<section className="space-y-5" data-testid="settings-opencode-workspace">
<div className="flex items-start justify-between gap-4">
<div>
<h2 className="text-lg font-medium">Workspace</h2>
<p className="text-sm text-muted-foreground">
Configure the local runtime and project workspace.
</p>
</div>
<Button
variant="outline"
size="sm"
onClick={handleRefreshOpencodeSettings}
disabled={opencodeLoading}
aria-label="Refresh runtime settings"
>
<RefreshCw className={cn('mr-2 h-4 w-4', opencodeLoading && 'animate-spin')} />
Refresh
</Button>
</div>
<div
className="space-y-4 rounded-lg border bg-card p-4"
data-testid="settings-opencode-models"
>
<div className="flex items-start justify-between gap-3">
<div>
<Label className="text-sm font-medium">Models</Label>
<p className="text-sm text-muted-foreground">
Resolved from the runtime configuration.
</p>
</div>
<Button variant="outline" size="sm" onClick={() => navigate('/models')}>
Configure models
</Button>
</div>
<div className="space-y-3 text-sm">
<div>
<div className="text-xs text-muted-foreground">Default model</div>
<div className="break-all font-medium">
{opencodeRuntimeConfigSummary?.model ?? 'No configured model'}
</div>
</div>
<div>
<div className="text-xs text-muted-foreground">Small model</div>
<div className="break-all font-medium">
{opencodeRuntimeConfigSummary?.smallModel ?? 'No configured small model'}
</div>
</div>
<div className="flex items-center justify-between gap-3">
<span className="text-muted-foreground">Providers</span>
<span className="font-medium">
{`${opencodeRuntimeConfigSummary?.providerCount ?? 0} providers`}
</span>
</div>
</div>
</div>
<div className="space-y-4 rounded-lg border bg-card p-4">
<div className="flex items-start justify-between gap-4">
<div>
<Label className="text-sm font-medium">Projects</Label>
<p className="text-sm text-muted-foreground">
Folders opened as project workspaces.
</p>
</div>
<Button
variant="outline"
size="sm"
onClick={() => void handleAddOpencodeProject()}
disabled={opencodeLoading}
aria-label="Add project"
>
<FolderPlus className="mr-2 h-4 w-4" />
Add project
</Button>
</div>
<div className="rounded-md border bg-background/60 p-3">
<div className="text-xs text-muted-foreground">Active project</div>
<div className="mt-1 truncate text-sm font-medium">
{activeOpencodeProject?.name ?? 'No project selected'}
</div>
<div className="mt-1 truncate text-xs text-muted-foreground">
{activeOpencodeProject?.path ?? 'Add a project folder to start a session.'}
</div>
</div>
{workspaceProjectError && (
<div className="rounded-md border border-destructive/30 bg-destructive/10 px-3 py-2 text-xs text-destructive">
{workspaceProjectError}
</div>
)}
{opencodeProjects.length > 0 && (
<div className="max-h-56 space-y-2 overflow-auto pr-1">
{opencodeProjects.map((project) => {
const active = activeOpencodeProject?.id === project.id;
return (
<div
key={project.id}
className="flex items-center justify-between gap-3 rounded-md border bg-background/50 px-3 py-2"
>
<div className="min-w-0">
<div className="truncate text-sm font-medium">{project.name}</div>
<div className="truncate text-xs text-muted-foreground">
{project.path}
</div>
</div>
<Button
variant={active ? 'secondary' : 'outline'}
size="sm"
disabled={active || opencodeLoading}
onClick={() => void activateVerifiedProject(project.id)}
aria-label={
active
? `Active project ${project.name}`
: `Use project ${project.name}`
}
>
{active ? 'Active' : 'Use'}
</Button>
</div>
);
})}
</div>
)}
<section className="space-y-4" data-testid="settings-models">
<div>
<h2 className="text-lg font-medium">Models</h2>
<p className="text-sm text-muted-foreground">
Configure provider accounts and inspect local token usage.
</p>
</div>
<Button variant="outline" onClick={() => navigate('/models')}>
Configure models
</Button>
</section>
<Separator />