From dc4a0d7b1cdc74f78bad4936f4998cbb015dcf16 Mon Sep 17 00:00:00 2001 From: huangruiteng <14976749+huangruiteng@users.noreply.github.com> Date: Wed, 16 Sep 2026 21:10:16 +0800 Subject: [PATCH] fix(dashboard): label steward answers as the LoopX Manager The manager channel's answers were titled with whatever the agent picker held, so a steward turn served on a managed host rendered as "Codex" in the conversation tray. The executor and its model belong to the machine-capability chip; the transcript should name the speaker, not the CLI brand that happened to run the turn. Goal channels keep naming the Goal's own Agent: only the manager context is re-labelled, through one `answerIdentityLabel` helper applied to the return-receipt, history-hydration, recovery, streaming, interruption and failure message paths. Signed-off-by: huangruiteng <14976749+huangruiteng@users.noreply.github.com> --- .../dashboard/src/views/dashboard-page.tsx | 52 +++++++++++++------ .../chat-recovery.mjs | 9 ++++ 2 files changed, 46 insertions(+), 15 deletions(-) diff --git a/apps/presentation/dashboard/src/views/dashboard-page.tsx b/apps/presentation/dashboard/src/views/dashboard-page.tsx index f0113d812c..3ef44b67d0 100644 --- a/apps/presentation/dashboard/src/views/dashboard-page.tsx +++ b/apps/presentation/dashboard/src/views/dashboard-page.tsx @@ -1516,6 +1516,14 @@ function PersonalGoalHome({ const managerQuickPrompts = ["我现在该做什么?", "哪些 Goal 在等我?", "Agent 在做什么?"]; const contextMessages = messagesByContext[contextId] ?? []; const contextProposals = proposalsByContext[contextId] ?? []; + + // Who is speaking in the transcript. The manager channel answers as the LoopX + // Manager: the executor that served the turn (and the model behind it) belongs + // to the machine-capability chip, so a person reading an answer is never told + // the CLI brand of whatever host happened to run it. Goal channels still name + // the Goal's own Agent. + const answerIdentityLabel = (targetContextId: string, goalFallback: string) => + targetContextId === "manager" ? t("header.manager") : goalFallback; const goalUserTodos = selectedGoal ? model.userTodos.filter((todo) => todo.goalId === selectedGoal.goalId) : model.userTodos; @@ -1607,7 +1615,8 @@ function PersonalGoalHome({ if (!fresh.length && !deliveryChanged) return current; return { ...current, [contextId]: [...updated, ...fresh.map((row) => ({ id: managerMessageId.current++, sourceMessageId: row.message_id, - role: "assistant" as const, agentLabel: selectedAgent.label, + role: "assistant" as const, + agentLabel: answerIdentityLabel(contextId, selectedAgent.label), sourceLabel: "管家交接回执", text: visibleAgentMessage(row.text), lines: [], returnDelivery: row.return_delivery, }))] }; @@ -1703,7 +1712,9 @@ function PersonalGoalHome({ ...current, [targetContextId]: history.messages.map((message) => ({ sourceMessageId: message.message_id, - agentLabel: message.role === "user" ? undefined : selectedAgent.label, + agentLabel: message.role === "user" + ? undefined + : answerIdentityLabel(targetContextId, selectedAgent.label), attachments: workspaceImageAttachments(message.attachments), id: managerMessageId.current++, lines: [], @@ -1713,7 +1724,9 @@ function PersonalGoalHome({ ? undefined : message.role === "error" ? "本地会话记录" - : `恢复的 ${selectedAgent.label} 会话`, + : targetContextId === "manager" + ? `恢复的${t("header.manager")}会话` + : `恢复的 ${selectedAgent.label} 会话`, text: message.role === "user" ? message.text : visibleAgentMessage(message.text), })), }; @@ -1778,10 +1791,12 @@ function PersonalGoalHome({ let streamedText = ""; const streamingMessageId = appendManagerAssistantMessage(targetContextId, { activity: ["正在恢复进行中的 Agent 回合"], - agentLabel: selectedAgent.label, + agentLabel: answerIdentityLabel(targetContextId, selectedAgent.label), lines: [], pending: true, - sourceLabel: `恢复的 ${selectedAgent.label} 会话`, + sourceLabel: targetContextId === "manager" + ? `恢复的${t("header.manager")}会话` + : `恢复的 ${selectedAgent.label} 会话`, text: "", }); try { @@ -1813,7 +1828,9 @@ function PersonalGoalHome({ ? [streamed.response.gate.summary, streamed.response.gate.next_action].filter(Boolean).slice(0, 2) : [], pending: false, - text: streamed.response.message || streamedText.trim() || `${selectedAgent.label} 已完成分析。`, + text: streamed.response.message + || streamedText.trim() + || `${answerIdentityLabel(targetContextId, selectedAgent.label)} 已完成分析。`, }); const recoveryGoal = model.goals.find((goal) => goal.goalId === activeSnapshot?.session.goal_id) ?? selectedGoal @@ -2136,13 +2153,13 @@ function PersonalGoalHome({ } let streamedText = ""; streamingMessageId = appendManagerAssistantMessage(targetContextId, { - activity: ["正在连接 Agent"], - agentLabel: selectedRoute.label, + activity: [targetContextId === "manager" ? "正在连接管家" : "正在连接 Agent"], + agentLabel: answerIdentityLabel(targetContextId, selectedRoute.label), lines: [], pending: true, sourceLabel: targetContextId !== "manager" ? `${selectedRoute.label} Agent · ${personalGoalTitle(targetGoal!.goalId)}` - : `${selectedRoute.label} 管家 · 跨 Goal`, + : `${t("header.manager")} · 跨 Goal`, text: "", }); const streamed = await sendChatTurnStreaming(sessionId, question, { @@ -2189,7 +2206,8 @@ function PersonalGoalHome({ updateManagerAssistantMessage(targetContextId, streamingMessageId, { lines: response.gate ? [response.gate.summary, response.gate.next_action].filter(Boolean).slice(0, 2) : [], pending: false, - text: visibleAgentMessage(response.message || streamedText.trim()) || `${selectedRoute.label} 已完成分析。`, + text: visibleAgentMessage(response.message || streamedText.trim()) + || `${answerIdentityLabel(targetContextId, selectedRoute.label)} 已完成分析。`, }); if (response.proposals.length > 0 && !targetGoal) { updateManagerAssistantMessage(targetContextId, streamingMessageId, { @@ -2223,10 +2241,12 @@ function PersonalGoalHome({ const userInterrupted = interruptedContexts.current.delete(targetContextId); if (userInterrupted) { const interruptedMessage = { - agentLabel: selectedRoute.label, + agentLabel: answerIdentityLabel(targetContextId, selectedRoute.label), lines: [], pending: false, - sourceLabel: `${selectedRoute.label} 会话`, + sourceLabel: targetContextId === "manager" + ? `${t("header.manager")}会话` + : `${selectedRoute.label} 会话`, text: "已中断。你可以在当前会话继续发送消息。", }; if (streamingMessageId === null) { @@ -2255,14 +2275,16 @@ function PersonalGoalHome({ ? String((gate as { summary?: unknown }).summary ?? "") : ""; const failureMessage = { - agentLabel: selectedRoute.label, + agentLabel: answerIdentityLabel(targetContextId, selectedRoute.label), lines: gateSummary ? [gateSummary] : [], pending: false, reconnect: payloadError?.reconnectable === true, sourceLabel: "LoopX Chat 本地后端", text: payloadError?.error_code === "resume_failed" - ? `原 ${selectedRoute.label} 会话无法恢复。本地历史已经保留,请在运行详情里选择“重试恢复”或“开始新 Session”。` - : error instanceof Error ? error.message : `${selectedRoute.label} 会话暂时不可用。`, + ? `原 ${answerIdentityLabel(targetContextId, selectedRoute.label)} 会话无法恢复。本地历史已经保留,请在运行详情里选择“重试恢复”或“开始新 Session”。` + : error instanceof Error + ? error.message + : `${answerIdentityLabel(targetContextId, selectedRoute.label)} 会话暂时不可用。`, }; if (streamingMessageId === null) { appendManagerAssistantMessage(targetContextId, failureMessage); diff --git a/examples/personal-workspace-browser/chat-recovery.mjs b/examples/personal-workspace-browser/chat-recovery.mjs index bdd2eb98cb..7cd51e00a8 100644 --- a/examples/personal-workspace-browser/chat-recovery.mjs +++ b/examples/personal-workspace-browser/chat-recovery.mjs @@ -44,6 +44,15 @@ export const chatRecoveryScenario = { await page.getByLabel("向 LoopX 发送消息").fill("我现在该做什么?只读回答,不要创建或修改任何状态。"); await page.getByRole("button", { name: "发送", exact: true }).click(); await page.getByText("管家已读取当前授权范围的 Goal 证据。", { exact: true }).waitFor({ state: "visible" }); + // The steward answers as the LoopX Manager. The executor that served the + // turn belongs to the machine-capability chip, so an answer must never be + // labelled with the CLI brand the agent picker happens to hold. + const answerIdentity = ( + await page.locator(".personal-manager-conversation-tray article.is-assistant strong").last().innerText() + ).trim(); + if (answerIdentity !== "LoopX 管家") { + throw new Error(`Manager answer was labelled as its executor instead of the steward: ${answerIdentity}`); + } if (!api.turnRequests.some((turn) => turn.message.startsWith("我现在该做什么?"))) throw new Error("Manager question bypassed the global runtime"); await page.getByText("查看完整对话", { exact: true }).waitFor({ state: "visible" }); if (page.url() !== managerUrlBefore) throw new Error(`Manager send navigated away from the overview: ${managerUrlBefore} -> ${page.url()}`);