v2.1.0-provider-picker: BooCoder systemd migration + provider picker

- BooCoder moves from Docker to host systemd service (boocoder.service)
- Agent dispatch (ACP + PTY) switches from SSH to direct spawn/exec
- SSH helpers marked @deprecated (kept for one release cycle)
- Provider registry (5 providers: boocode, opencode, goose, claude, qwen)
- Agent probe with direct which/exec + model discovery (qwen settings, static claude models)
- GET /api/providers route with installed status, models, transport fallback
- ProviderPicker frontend component in CoderPane header
- External provider messages route through tasks row instead of inference enqueue
- Smart scroll: MessageList only auto-scrolls when near bottom (150px threshold)
- DB: available_agents gets models, label, transport columns
- Bug fix: loadContext SELECT includes allowed_read_paths
- Bug fix: cap hit sentinel inserted before buildMessagesPayload
- docker-compose.yml: boocoder service commented out, BOOCODER_URL env var added
- CLAUDE.md: updated docs for systemd, provider registry, JSONB gotcha, loadContext
This commit is contained in:
2026-05-25 19:20:53 +00:00
parent e423579e99
commit d8ffee1950
21 changed files with 687 additions and 222 deletions

View File

@@ -10,6 +10,7 @@
import { useCallback, useEffect, useRef, useState } from 'react';
import { Code, Send, Check, X, RefreshCw } from 'lucide-react';
import { MarkdownRenderer } from '@/components/MarkdownRenderer';
import { ProviderPicker } from '@/components/ProviderPicker';
import { cn } from '@/lib/utils';
// ---------------------------------------------------------------------------
@@ -300,6 +301,8 @@ export function CoderPane({ sessionId }: Props) {
const { changes, loading, refresh, approve, reject } = usePendingChanges(sessionId);
const [input, setInput] = useState('');
const [sending, setSending] = useState(false);
const [provider, setProvider] = useState('boocode');
const [model, setModel] = useState('qwen3.6-35b-a3b-mxfp4');
const messagesEndRef = useRef<HTMLDivElement>(null);
const inputRef = useRef<HTMLTextAreaElement>(null);
@@ -331,7 +334,11 @@ export function CoderPane({ sessionId }: Props) {
const res = await fetch(`/api/coder/sessions/${sessionId}/messages`, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ content: text }),
body: JSON.stringify({
content: text,
provider: provider !== 'boocode' ? provider : undefined,
model: model || undefined,
}),
});
if (res.ok) {
const data = await res.json();
@@ -347,7 +354,7 @@ export function CoderPane({ sessionId }: Props) {
} finally {
setSending(false);
}
}, [input, sending, sessionId, setMessages]);
}, [input, sending, sessionId, provider, model, setMessages]);
const handleKeyDown = useCallback(
(e: React.KeyboardEvent) => {
@@ -363,11 +370,18 @@ export function CoderPane({ sessionId }: Props) {
<div className="flex flex-col h-full bg-background">
{/* Header */}
<div className="flex items-center gap-2 px-3 py-1.5 border-b border-border bg-muted/30 shrink-0">
<Code size={14} className="text-muted-foreground" />
<span className="text-xs font-medium text-muted-foreground">BooCoder</span>
<Code size={14} className="text-muted-foreground shrink-0" />
<ProviderPicker
provider={provider}
model={model}
onChange={(prov, mod) => {
setProvider(prov);
setModel(mod);
}}
/>
<span
className={cn(
'inline-block w-1.5 h-1.5 rounded-full ml-auto',
'inline-block w-1.5 h-1.5 rounded-full ml-auto shrink-0',
connected ? 'bg-green-500' : 'bg-red-500'
)}
title={connected ? 'Connected' : 'Disconnected'}