From 9d4129f593c17f423ad156ce4914026f941b3493 Mon Sep 17 00:00:00 2001 From: Paul Liu <20290410+Paulkm2006@users.noreply.github.com> Date: Fri, 7 Aug 2026 11:45:59 +0800 Subject: [PATCH 1/2] fix: race condition when installing multiple agents --- frontend/src/App.tsx | 2 ++ frontend/src/pages/ActivationPage.tsx | 10 +++--- frontend/src/pages/InstallTaskPage.tsx | 40 ++++++++++++++++++++++++ frontend/src/pages/ProfilesPage.tsx | 5 ++- frontend/src/state/TaskCenterContext.tsx | 33 +++++++++++++------ frontend/src/types/api.ts | 6 ++-- internal/app/desktopapp.go | 4 +++ internal/app/install.go | 1 + internal/process/process.go | 3 ++ 9 files changed, 85 insertions(+), 19 deletions(-) create mode 100644 frontend/src/pages/InstallTaskPage.tsx diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index cd488d38..e27fd2c6 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -3,6 +3,7 @@ import { Navigate, Route, Routes, useLocation } from "react-router-dom"; import { AppWindow } from "./components/AppWindow"; import { AppUpdater } from "./components/AppUpdater"; import { ActivationPage } from "./pages/ActivationPage"; +import { InstallTaskPage } from "./pages/InstallTaskPage"; import { AgentProfilePage } from "./pages/AgentProfilePage"; import { AgentSelectionPage } from "./pages/AgentSelectionPage"; import { EnvironmentOverviewPage } from "./pages/EnvironmentOverviewPage"; @@ -78,6 +79,7 @@ function WorkspaceRoutes() { } /> } /> } /> + } /> } /> } /> } /> diff --git a/frontend/src/pages/ActivationPage.tsx b/frontend/src/pages/ActivationPage.tsx index 22dbdd2c..e0959917 100644 --- a/frontend/src/pages/ActivationPage.tsx +++ b/frontend/src/pages/ActivationPage.tsx @@ -9,7 +9,7 @@ import { LogDisclosure } from "../components/LogDisclosure"; import { PageScaffold } from "../components/PageScaffold"; import { useI18n } from "../i18n"; import { desktopProtocol, profileAgentIdForDesktop, selectedDesktopApp } from "../state/desktopSetup"; -import { taskCanceller, taskKey, useTaskCenter, useTaskRoute, type TaskCanceller } from "../state/TaskCenterContext"; +import { installTaskRoute, taskCanceller, taskKey, useTaskCenter, useTaskRoute, type TaskCanceller } from "../state/TaskCenterContext"; import { useWizard } from "../state/WizardContext"; import type { AgentInstallResult, InstallRequest } from "../types/api"; @@ -78,7 +78,7 @@ export function ActivationPage() { kind: "install", target, title: t("安装 {name}", { name: selectedNames[target] || target }), - route, + route: installTaskRoute(target), group, })) { for (const started of startedAgents) finishTask(started, { kind: "failure", message: t("任务正在运行") }); @@ -174,6 +174,8 @@ export function ActivationPage() { return; } dispatch({ type: "ACTIVATION_LOADING", agentIds: state.selectedAgentIds }); + const firstAgent = startedTasks.agents[0]?.slice("install:".length); + if (firstAgent) navigate(installTaskRoute(firstAgent)); try { let response; if (isDesktop) { @@ -202,7 +204,7 @@ export function ActivationPage() { finishActivationTasks(startedTasks, [], false, message); dispatch({ type: "ACTIVATION_FAILED", message }); } - }, [dispatch, finishActivationTasks, installDesktop, isDesktop, refreshStatus, registerActivationCanceller, requestFor, startActivationTasks, state.selectedAgentIds, t]); + }, [dispatch, finishActivationTasks, installDesktop, isDesktop, navigate, refreshStatus, registerActivationCanceller, requestFor, startActivationTasks, state.selectedAgentIds, t]); useEffect( () => @@ -262,7 +264,7 @@ export function ActivationPage() { // The task card is also a recovery path after another setup run replaced the // wizard draft. Render the durable task directly instead of bouncing through // the setup guards with an empty selection. - const restoredTask = tasks.find((task) => task.kind === "install" && task.route.split("?", 1)[0] === "/setup/activation"); + const restoredTask = tasks.find((task) => task.kind === "install" && task.route.startsWith("/tasks/install/")); if (!state.selectedAgentIds.length && restoredTask) { const loading = restoredTask.state === "running"; const cancelled = restoredTask.state === "cancelled"; diff --git a/frontend/src/pages/InstallTaskPage.tsx b/frontend/src/pages/InstallTaskPage.tsx new file mode 100644 index 00000000..cc37771a --- /dev/null +++ b/frontend/src/pages/InstallTaskPage.tsx @@ -0,0 +1,40 @@ +import { useNavigate, useParams } from "react-router-dom"; + +import { DownloadProgress } from "../components/DownloadProgress"; +import { LogDisclosure } from "../components/LogDisclosure"; +import { PageScaffold } from "../components/PageScaffold"; +import { useI18n } from "../i18n"; +import { installTaskRoute, useTaskCenter } from "../state/TaskCenterContext"; + +export function InstallTaskPage() { + const { t } = useI18n(); + const navigate = useNavigate(); + const { agentId = "" } = useParams(); + const { tasks, cancelTask, dismissTask } = useTaskCenter(); + const target = decodeURIComponent(agentId); + const task = tasks.find((item) => item.kind === "install" && item.target === target); + if (!task) { + return navigate("/overview")} />; + } + const running = task.state === "running"; + const title = running ? t("正在安装") : task.state === "success" ? t("安装完成") : task.state === "cancelled" ? t("已取消") : t("需要处理部分问题"); + return ( + navigate("/overview")} + footerNote={running ? t("请保持此窗口打开") : undefined} + > + {task.progressTarget ? : null} + + {running ? ( + + ) : ( + + )} + + ); +} + +export { installTaskRoute }; diff --git a/frontend/src/pages/ProfilesPage.tsx b/frontend/src/pages/ProfilesPage.tsx index 24480914..d4cb5895 100644 --- a/frontend/src/pages/ProfilesPage.tsx +++ b/frontend/src/pages/ProfilesPage.tsx @@ -8,7 +8,7 @@ import { ProviderSegment } from "../components/ProviderSegment"; import { SelectField } from "../components/SelectField"; import { useI18n } from "../i18n"; import { byProviderCreatedAt } from "../state/ranking"; -import { taskCanceller, taskKey, useTaskCenter, useTaskRoute } from "../state/TaskCenterContext"; +import { installTaskRoute, taskCanceller, taskKey, useTaskCenter } from "../state/TaskCenterContext"; import { useWizard } from "../state/WizardContext"; import { PROTOCOL_LABELS, type ProfileSummary, type ProtocolId, type ProviderId } from "../types/api"; @@ -39,7 +39,6 @@ export function ProfilesPage() { const { locale, t } = useI18n(); const { state, refreshStatus } = useWizard(); const { startTask, finishTask, setTaskCanceller } = useTaskCenter(); - const route = useTaskRoute(); const status = state.status; const [editor, setEditor] = useState(null); const [busy, setBusy] = useState(false); @@ -139,7 +138,7 @@ export function ProfilesPage() { profile: profile.label || profile.id, agent: status.catalog.find((agent) => agent.id === agentId)?.name || agentId, }), - route, + route: installTaskRoute(agentId), progressTarget: status.capabilities.missingRuntime[agentId], group, })); diff --git a/frontend/src/state/TaskCenterContext.tsx b/frontend/src/state/TaskCenterContext.tsx index 4dad288d..75c4370a 100644 --- a/frontend/src/state/TaskCenterContext.tsx +++ b/frontend/src/state/TaskCenterContext.tsx @@ -59,6 +59,7 @@ export interface TaskRecord extends TaskInput { state: TaskState; progress?: TaskProgress; message?: string; + log?: string; startedAt: number; } @@ -143,6 +144,16 @@ function taskInputFor(value: TaskInput | string): TaskInput { return typeof value === "string" ? defaultTask(value) : value; } +export function installTaskRoute(target: string): string { + return `/tasks/install/${encodeURIComponent(target)}`; +} + +function outputText(output: InstallOutput): string { + if (output.kind === "progress") return ""; + if (output.kind === "command") return `$ ${output.args.join(" ")}\n`; + return output.text; +} + /** * The provider is mounted above the route content. A page can therefore unmount while * its Go request is still running without losing the card or its progress. @@ -162,16 +173,20 @@ export function TaskCenterProvider({ children }: PropsWithChildren) { useEffect( () => api.onInstallOutput((output: InstallOutput) => { - if (output.kind !== "progress") return; - const targetTasks = tasksRef.current.filter((task) => task.progressTarget === output.target || task.target === output.target); - if (targetTasks.length && !targetTasks.some((task) => task.state === "running")) return; - setProgress((current) => ({ - ...current, - [output.target]: { received: output.received, total: output.total }, - })); + const matchesTask = (task: TaskRecord) => task.state === "running" && (output.agent + ? task.target === output.agent + : output.kind === "progress" && (task.progressTarget === output.target || task.target === output.target)); + const targetTasks = tasksRef.current.filter(matchesTask); + if (output.kind === "progress") { + setProgress((current) => ({ + ...current, + [output.target]: { received: output.received, total: output.total }, + })); + } + if (!targetTasks.length) return; updateTasks((current) => current.map((task) => ( - task.state === "running" && (task.progressTarget === output.target || task.target === output.target) - ? { ...task, progress: { received: output.received, total: output.total } } + matchesTask(task) + ? { ...task, ...(output.kind === "progress" ? { progress: { received: output.received, total: output.total } } : {}), ...(outputText(output) ? { log: `${task.log || ""}${outputText(output)}` } : {}) } : task ))); }), diff --git a/frontend/src/types/api.ts b/frontend/src/types/api.ts index 47def289..7e975fa6 100644 --- a/frontend/src/types/api.ts +++ b/frontend/src/types/api.ts @@ -96,10 +96,10 @@ export type InstallResponse = Omit Date: Fri, 7 Aug 2026 11:49:01 +0800 Subject: [PATCH 2/2] feat: create update progress page --- frontend/src/App.tsx | 1 + frontend/src/components/AgentManageRow.tsx | 4 ++-- frontend/src/components/AppUpdater.tsx | 4 ++-- frontend/src/pages/InstallTaskPage.tsx | 9 ++++++--- frontend/src/state/TaskCenterContext.tsx | 4 ++++ internal/app/update.go | 10 ++++++++-- internal/binding/services.go | 2 +- 7 files changed, 24 insertions(+), 10 deletions(-) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index e27fd2c6..a7e24308 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -80,6 +80,7 @@ function WorkspaceRoutes() { } /> } /> } /> + } /> } /> } /> } /> diff --git a/frontend/src/components/AgentManageRow.tsx b/frontend/src/components/AgentManageRow.tsx index fa527744..eee487d2 100644 --- a/frontend/src/components/AgentManageRow.tsx +++ b/frontend/src/components/AgentManageRow.tsx @@ -4,7 +4,7 @@ import { Link } from "react-router-dom"; import { api, describeError } from "../backend/api"; import { sourceTranslate, type Translate, useI18n } from "../i18n"; -import { taskCanceller, taskKey, useTaskCenter, useTaskRoute } from "../state/TaskCenterContext"; +import { taskCanceller, taskKey, updateTaskRoute, useTaskCenter, useTaskRoute } from "../state/TaskCenterContext"; import type { AgentCatalogItem, AgentStatus, ProfileSummary, StatusResponse } from "../types/api"; import { AgentIcon, agentTagline } from "./icons/agents"; @@ -169,7 +169,7 @@ export function AgentManageRow({ kind: "update", target: agentId, title: t("更新 {name}", { name: catalog?.name || agentId }), - route, + route: updateTaskRoute(agentId), })) return; setLocalUpdating(true); setFailure(""); diff --git a/frontend/src/components/AppUpdater.tsx b/frontend/src/components/AppUpdater.tsx index 5b272c2b..71bc0136 100644 --- a/frontend/src/components/AppUpdater.tsx +++ b/frontend/src/components/AppUpdater.tsx @@ -4,7 +4,7 @@ import { useEffect, useRef } from "react"; import { api, describeError, isCancellationError } from "../backend/api"; import { OTA_PROGRESS_TARGET } from "../backend/wails"; import { useI18n } from "../i18n"; -import { taskCanceller, taskKey, useTaskCenter } from "../state/TaskCenterContext"; +import { taskCanceller, taskKey, updateTaskRoute, useTaskCenter } from "../state/TaskCenterContext"; const OTA_TASK_ID = taskKey("update", OTA_PROGRESS_TARGET); @@ -52,7 +52,7 @@ export function AppUpdater() { target: OTA_PROGRESS_TARGET, progressTarget: OTA_PROGRESS_TARGET, title: t("更新 OneAgent {version}", { version }), - route: "/overview", + route: updateTaskRoute(OTA_PROGRESS_TARGET), })) return; try { diff --git a/frontend/src/pages/InstallTaskPage.tsx b/frontend/src/pages/InstallTaskPage.tsx index cc37771a..6eb7c2bf 100644 --- a/frontend/src/pages/InstallTaskPage.tsx +++ b/frontend/src/pages/InstallTaskPage.tsx @@ -9,15 +9,18 @@ import { installTaskRoute, useTaskCenter } from "../state/TaskCenterContext"; export function InstallTaskPage() { const { t } = useI18n(); const navigate = useNavigate(); - const { agentId = "" } = useParams(); + const { agentId = "", kind = "install" } = useParams(); const { tasks, cancelTask, dismissTask } = useTaskCenter(); const target = decodeURIComponent(agentId); - const task = tasks.find((item) => item.kind === "install" && item.target === target); + const task = tasks.find((item) => item.kind === kind && item.target === target); if (!task) { return navigate("/overview")} />; } const running = task.state === "running"; - const title = running ? t("正在安装") : task.state === "success" ? t("安装完成") : task.state === "cancelled" ? t("已取消") : t("需要处理部分问题"); + const title = running + ? kind === "update" ? t("更新中") : t("正在安装") + : task.state === "success" ? kind === "update" ? t("更新完成") : t("安装完成") + : task.state === "cancelled" ? t("已取消") : t("需要处理部分问题"); return ( 0 && listeners[0] != nil { + base := listeners[0] + output = func(event process.Output) { event.Agent = agentID; base(event) } + } + runtime := u.installRuntime(output) npm, present := runtime.Runner.LookPath("npm") if !present || npm == "" { return AgentUpdateResult{}, oneerrors.New(oneerrors.PrerequisiteMissing, "npm is required to update "+agent.Name) diff --git a/internal/binding/services.go b/internal/binding/services.go index 77c087ac..b08599c3 100644 --- a/internal/binding/services.go +++ b/internal/binding/services.go @@ -296,7 +296,7 @@ func (s *AgentService) Update(ctx context.Context, request UpdateRequest) (app.A if s == nil || s.core == nil { return app.AgentUpdateResult{}, notReady("Agent update is not configured") } - return s.core.UpdateAgent(ctx, request.AgentID) + return s.core.UpdateAgent(ctx, request.AgentID, s.onOutput) } func (s *AgentService) Install(ctx context.Context, request InstallRequest) (InstallResponse, error) {