feat: stream real-time progress for agent SDK install/upgrade/uninstall
Replace the spinner-only UX with live log output during agent SDK operations, matching the existing OpenCode plugin install experience. Backend: emit structured events (started/log/completed/failed) via EventEmitter during npm install and binary download. npm commands now run with piped stdio for line-by-line streaming; binary downloads report chunked progress every 1 MB. Frontend: subscribe to `app://agent-install` events through a new `useAgentInstallStream` hook and render a theme-aware log terminal below the preflight checks panel. Also fixes the install log container in both agent settings and the OpenCode plugins modal: auto-scroll no longer shifts the outer page, and colours now follow the active light/dark theme. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -85,6 +85,7 @@ import type {
|
||||
ModelProviderInfo,
|
||||
PreflightResult,
|
||||
} from "@/lib/types"
|
||||
import { useAgentInstallStream } from "@/hooks/use-agent-install-stream"
|
||||
import { OpencodePluginsModal } from "./opencode-plugins-modal"
|
||||
|
||||
interface AgentCheckState {
|
||||
@@ -2624,6 +2625,9 @@ export function AcpAgentSettings() {
|
||||
const busyActionRef = useRef<Set<AgentType>>(new Set())
|
||||
const handledSearchAgentRef = useRef<string | null>(null)
|
||||
const agentListRef = useRef<HTMLDivElement | null>(null)
|
||||
const installStream = useAgentInstallStream()
|
||||
const [streamAgentType, setStreamAgentType] = useState<AgentType | null>(null)
|
||||
const installLogEndRef = useRef<HTMLDivElement | null>(null)
|
||||
|
||||
const sortedAgents = useMemo(
|
||||
() =>
|
||||
@@ -2748,6 +2752,30 @@ export function AcpAgentSettings() {
|
||||
[runPreflight]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
return () => installStream.reset()
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
const container = installLogEndRef.current?.parentElement
|
||||
if (container) {
|
||||
container.scrollTop = container.scrollHeight
|
||||
}
|
||||
}, [installStream.logs])
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
installStream.status === "success" ||
|
||||
installStream.status === "failed"
|
||||
) {
|
||||
if (streamAgentType) {
|
||||
runPreflight(streamAgentType).catch(() => {})
|
||||
}
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [installStream.status])
|
||||
|
||||
useEffect(() => {
|
||||
refreshAgents().catch((err) => {
|
||||
console.error("[Settings] refresh agents failed:", err)
|
||||
@@ -2940,11 +2968,14 @@ export function AcpAgentSettings() {
|
||||
[agent.agent_type]:
|
||||
kind ?? (mode === "download" ? "download_binary" : "upgrade_binary"),
|
||||
}))
|
||||
const taskId = crypto.randomUUID()
|
||||
setStreamAgentType(agent.agent_type)
|
||||
await installStream.start(taskId)
|
||||
try {
|
||||
if (mode === "upgrade") {
|
||||
await acpClearBinaryCache(agent.agent_type)
|
||||
}
|
||||
await acpDownloadAgentBinary(agent.agent_type)
|
||||
await acpDownloadAgentBinary(agent.agent_type, taskId)
|
||||
await runPreflight(agent.agent_type)
|
||||
const detectedVersion = await acpDetectAgentLocalVersion(
|
||||
agent.agent_type
|
||||
@@ -2990,7 +3021,8 @@ export function AcpAgentSettings() {
|
||||
}))
|
||||
}
|
||||
},
|
||||
[runPreflight, t]
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[runPreflight, t, installStream.start]
|
||||
)
|
||||
|
||||
const runNpxAction = useCallback(
|
||||
@@ -3002,10 +3034,14 @@ export function AcpAgentSettings() {
|
||||
...prev,
|
||||
[agent.agent_type]: mode === "install" ? "install_npx" : "upgrade_npx",
|
||||
}))
|
||||
const taskId = crypto.randomUUID()
|
||||
setStreamAgentType(agent.agent_type)
|
||||
await installStream.start(taskId)
|
||||
try {
|
||||
const installedVersion = await acpPrepareNpxAgent(
|
||||
agent.agent_type,
|
||||
agent.registry_version
|
||||
agent.registry_version,
|
||||
taskId
|
||||
)
|
||||
setAgents((prev) =>
|
||||
prev.map((item) =>
|
||||
@@ -3062,7 +3098,8 @@ export function AcpAgentSettings() {
|
||||
}))
|
||||
}
|
||||
},
|
||||
[runPreflight, t]
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[runPreflight, t, installStream.start]
|
||||
)
|
||||
|
||||
const runUninstallAction = useCallback(
|
||||
@@ -3077,8 +3114,11 @@ export function AcpAgentSettings() {
|
||||
? "uninstall_binary"
|
||||
: "uninstall_npx",
|
||||
}))
|
||||
const taskId = crypto.randomUUID()
|
||||
setStreamAgentType(agent.agent_type)
|
||||
await installStream.start(taskId)
|
||||
try {
|
||||
await acpUninstallAgent(agent.agent_type)
|
||||
await acpUninstallAgent(agent.agent_type, taskId)
|
||||
setAgents((prev) =>
|
||||
prev.map((item) =>
|
||||
item.agent_type === agent.agent_type
|
||||
@@ -3105,7 +3145,8 @@ export function AcpAgentSettings() {
|
||||
}))
|
||||
}
|
||||
},
|
||||
[runPreflight, t]
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[runPreflight, t, installStream.start]
|
||||
)
|
||||
|
||||
const handleFixAction = async (agent: AcpAgentInfo, action: UiFixAction) => {
|
||||
@@ -5003,6 +5044,24 @@ export function AcpAgentSettings() {
|
||||
{t("preflight.notRun")}
|
||||
</div>
|
||||
)}
|
||||
{installStream.status !== "idle" &&
|
||||
streamAgentType === selectedAgent.agent_type && (
|
||||
<div className="mt-2 rounded-md border bg-muted/50 text-muted-foreground p-3 max-h-[200px] overflow-y-auto font-mono text-[11px] leading-relaxed">
|
||||
{installStream.logs.map((line, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className={
|
||||
line.startsWith("ERROR:")
|
||||
? "text-destructive"
|
||||
: ""
|
||||
}
|
||||
>
|
||||
{line}
|
||||
</div>
|
||||
))}
|
||||
<div ref={installLogEndRef} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
|
||||
@@ -60,7 +60,10 @@ export function OpencodePluginsModal({
|
||||
}, [open])
|
||||
|
||||
useEffect(() => {
|
||||
logEndRef.current?.scrollIntoView({ behavior: "smooth" })
|
||||
const container = logEndRef.current?.parentElement
|
||||
if (container) {
|
||||
container.scrollTop = container.scrollHeight
|
||||
}
|
||||
}, [stream.logs])
|
||||
|
||||
useEffect(() => {
|
||||
@@ -240,11 +243,13 @@ export function OpencodePluginsModal({
|
||||
)}
|
||||
|
||||
{stream.status !== "idle" && (
|
||||
<div className="rounded-md border bg-black/80 text-green-400 p-3 max-h-[200px] overflow-y-auto font-mono text-[11px] leading-relaxed">
|
||||
<div className="rounded-md border bg-muted/50 text-muted-foreground p-3 max-h-[200px] overflow-y-auto font-mono text-[11px] leading-relaxed">
|
||||
{stream.logs.map((line, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className={line.startsWith("ERROR:") ? "text-red-400" : ""}
|
||||
className={
|
||||
line.startsWith("ERROR:") ? "text-destructive" : ""
|
||||
}
|
||||
>
|
||||
{line}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user