diff --git a/ui/src/components/AgentConfigForm.tsx b/ui/src/components/AgentConfigForm.tsx index 35c50055..5467ac97 100644 --- a/ui/src/components/AgentConfigForm.tsx +++ b/ui/src/components/AgentConfigForm.tsx @@ -31,6 +31,7 @@ import { help, adapterLabels, } from "./agent-config-primitives"; +import { defaultCreateValues } from "./agent-config-defaults"; import { getUIAdapter } from "../adapters"; import { ClaudeLocalAdvancedFields } from "../adapters/claude-local/config-fields"; import { MarkdownEditor } from "./MarkdownEditor"; @@ -210,8 +211,10 @@ export function AgentConfigForm(props: AgentConfigFormProps) { } if (overlay.adapterType !== undefined) { patch.adapterType = overlay.adapterType; - } - if (Object.keys(overlay.adapterConfig).length > 0) { + // When adapter type changes, send only the new config — don't merge + // with old config since old adapter fields are meaningless for the new type + patch.adapterConfig = overlay.adapterConfig; + } else if (Object.keys(overlay.adapterConfig).length > 0) { const existing = (agent.adapterConfig ?? {}) as Record; patch.adapterConfig = { ...existing, ...overlay.adapterConfig }; } @@ -432,12 +435,20 @@ export function AgentConfigForm(props: AgentConfigFormProps) { value={adapterType} onChange={(t) => { if (isCreate) { - set!({ adapterType: t, model: "", thinkingEffort: "" }); + // Reset all adapter-specific fields to defaults when switching adapter type + const { adapterType: _at, ...defaults } = defaultCreateValues; + set!({ ...defaults, adapterType: t }); } else { + // Clear all adapter config and explicitly blank out model + both effort keys + // so the old adapter's values don't bleed through via eff() setOverlay((prev) => ({ ...prev, adapterType: t, - adapterConfig: {}, // clear adapter config when type changes + adapterConfig: { + model: "", + effort: "", + modelReasoningEffort: "", + }, })); } }} @@ -794,10 +805,10 @@ function AdapterEnvironmentResult({ result }: { result: AdapterEnvironmentTestRe result.status === "pass" ? "Passed" : result.status === "warn" ? "Warnings" : "Failed"; const statusClass = result.status === "pass" - ? "text-green-300 border-green-500/40 bg-green-500/10" + ? "text-green-700 dark:text-green-300 border-green-300 dark:border-green-500/40 bg-green-50 dark:bg-green-500/10" : result.status === "warn" - ? "text-amber-300 border-amber-500/40 bg-amber-500/10" - : "text-red-300 border-red-500/40 bg-red-500/10"; + ? "text-amber-700 dark:text-amber-300 border-amber-300 dark:border-amber-500/40 bg-amber-50 dark:bg-amber-500/10" + : "text-red-700 dark:text-red-300 border-red-300 dark:border-red-500/40 bg-red-50 dark:bg-red-500/10"; return (
@@ -1154,37 +1165,39 @@ function ModelDropdown({ onChange={(e) => setModelSearch(e.target.value)} autoFocus /> - - {filteredModels.map((m) => ( +
- ))} - {filteredModels.length === 0 && ( -

No models found.

- )} + {filteredModels.map((m) => ( + + ))} + {filteredModels.length === 0 && ( +

No models found.

+ )} +
diff --git a/ui/src/components/AsciiArtAnimation.tsx b/ui/src/components/AsciiArtAnimation.tsx new file mode 100644 index 00000000..f46941e2 --- /dev/null +++ b/ui/src/components/AsciiArtAnimation.tsx @@ -0,0 +1,199 @@ +import { useEffect, useRef } from "react"; + +const CHARS = "░▒▓█▄▀■□▪▫●○◆◇◈◉★☆✦✧·."; + +interface Particle { + x: number; + y: number; + vx: number; + vy: number; + char: string; + life: number; + maxLife: number; + phase: number; +} + +function measureChar(container: HTMLElement): { w: number; h: number } { + const span = document.createElement("span"); + span.textContent = "M"; + span.style.cssText = + "position:absolute;visibility:hidden;white-space:pre;font-size:11px;font-family:monospace;line-height:1;"; + container.appendChild(span); + const rect = span.getBoundingClientRect(); + container.removeChild(span); + return { w: rect.width, h: rect.height }; +} + +export function AsciiArtAnimation() { + const preRef = useRef(null); + const frameRef = useRef(0); + const particlesRef = useRef([]); + + useEffect(() => { + if (!preRef.current) return; + const preEl: HTMLPreElement = preRef.current; + + const charSize = measureChar(preEl); + let charW = charSize.w; + let charH = charSize.h; + let cols = Math.ceil(preEl.clientWidth / charW); + let rows = Math.ceil(preEl.clientHeight / charH); + let particles = particlesRef.current; + + function spawnParticle() { + const edge = Math.random(); + let x: number, y: number, vx: number, vy: number; + if (edge < 0.5) { + x = -1; + y = Math.random() * rows; + vx = 0.3 + Math.random() * 0.5; + vy = (Math.random() - 0.5) * 0.2; + } else { + x = Math.random() * cols; + y = rows + 1; + vx = (Math.random() - 0.5) * 0.2; + vy = -(0.2 + Math.random() * 0.4); + } + const maxLife = 60 + Math.random() * 120; + particles.push({ + x, y, vx, vy, + char: CHARS[Math.floor(Math.random() * CHARS.length)], + life: 0, + maxLife, + phase: Math.random() * Math.PI * 2, + }); + } + + function render(time: number) { + const t = time * 0.001; + + // Spawn particles + const targetCount = Math.floor((cols * rows) / 12); + while (particles.length < targetCount) { + spawnParticle(); + } + + // Build grid + const grid: string[][] = Array.from({ length: rows }, () => + Array.from({ length: cols }, () => " ") + ); + const opacity: number[][] = Array.from({ length: rows }, () => + Array.from({ length: cols }, () => 0) + ); + + // Background wave pattern + for (let r = 0; r < rows; r++) { + for (let c = 0; c < cols; c++) { + const wave = + Math.sin(c * 0.08 + t * 0.7 + r * 0.04) * + Math.sin(r * 0.06 - t * 0.5) * + Math.cos((c + r) * 0.03 + t * 0.3); + if (wave > 0.65) { + grid[r][c] = wave > 0.85 ? "·" : "."; + opacity[r][c] = Math.min(1, (wave - 0.65) * 3); + } + } + } + + // Update and render particles + for (let i = particles.length - 1; i >= 0; i--) { + const p = particles[i]; + p.life++; + + // Flow field influence + const angle = + Math.sin(p.x * 0.05 + t * 0.3) * Math.cos(p.y * 0.07 - t * 0.2) * + Math.PI; + p.vx += Math.cos(angle) * 0.02; + p.vy += Math.sin(angle) * 0.02; + + // Damping + p.vx *= 0.98; + p.vy *= 0.98; + + p.x += p.vx; + p.y += p.vy; + + // Life fade + const lifeFrac = p.life / p.maxLife; + const alpha = lifeFrac < 0.1 + ? lifeFrac / 0.1 + : lifeFrac > 0.8 + ? (1 - lifeFrac) / 0.2 + : 1; + + // Remove dead or out-of-bounds particles + if ( + p.life >= p.maxLife || + p.x < -2 || p.x > cols + 2 || + p.y < -2 || p.y > rows + 2 + ) { + particles.splice(i, 1); + continue; + } + + const col = Math.round(p.x); + const row = Math.round(p.y); + if (row >= 0 && row < rows && col >= 0 && col < cols) { + if (alpha > opacity[row][col]) { + // Cycle through characters based on life + const charIdx = Math.floor( + (lifeFrac + Math.sin(p.phase + t)) * CHARS.length + ) % CHARS.length; + grid[row][col] = CHARS[Math.abs(charIdx)]; + opacity[row][col] = alpha; + } + } + } + + // Render to string + let output = ""; + for (let r = 0; r < rows; r++) { + for (let c = 0; c < cols; c++) { + const a = opacity[r][c]; + if (a > 0 && grid[r][c] !== " ") { + const o = Math.round(a * 60 + 40); + output += `${grid[r][c]}`; + } else { + output += " "; + } + } + if (r < rows - 1) output += "\n"; + } + + preEl.innerHTML = output; + frameRef.current = requestAnimationFrame(render); + } + + // Handle resize + const observer = new ResizeObserver(() => { + const size = measureChar(preEl); + charW = size.w; + charH = size.h; + cols = Math.ceil(preEl.clientWidth / charW); + rows = Math.ceil(preEl.clientHeight / charH); + // Cull out-of-bounds particles on resize + particles = particles.filter( + (p) => p.x >= -2 && p.x <= cols + 2 && p.y >= -2 && p.y <= rows + 2 + ); + particlesRef.current = particles; + }); + observer.observe(preEl); + + frameRef.current = requestAnimationFrame(render); + + return () => { + cancelAnimationFrame(frameRef.current); + observer.disconnect(); + }; + }, []); + + return ( +