fix: synchronize task collaboration session titles
Apply creation titles and repaired history names to cached sessions. Keep messages and reading state intact and preserve title edits received while a collaboration request is in flight. Validation: pnpm lint; pnpm typecheck; 604 unit tests; two browser regressions.
This commit is contained in:
@@ -34,6 +34,8 @@ Agent 相关弹窗、面板及菜单统一保持本地 shadcn/ui 设计,具体
|
|||||||
|
|
||||||
普通用户的阅读路径为:确认当前项目,提出问题,阅读回答,核查来源,再按需要定位地图或补充资料。审批和队列在有实际内容时出现,历史与处理过程按需展开。
|
普通用户的阅读路径为:确认当前项目,提出问题,阅读回答,核查来源,再按需要定位地图或补充资料。审批和队列在有实际内容时出现,历史与处理过程按需展开。
|
||||||
|
|
||||||
|
任务协作以对应任务名称作为初始标题,随后由 Agent 根据任务名称与对话内容生成具体标题;生成失败或仅返回通用名称时保留现有标题。“任务协作”仅作为历史列表中的类型标记。旧的通用标题随历史分页和详情读取逐步补齐,保留手动重命名及原有顺序;标题更新不重载消息或打断当前对话。
|
||||||
|
|
||||||
运行中的输入区使用一个外框:顶部显示待处理消息,正文最多预览两行,溢出时提供“展开全文”;每条消息只保留正文、简短的处理方式和一个菜单。编辑、处理方式切换、关闭排队统一进入该菜单。底栏在生成期间用“引导 / 排队”替代暂时不能切换的模型选择,生成完成后恢复模型选择。右下角的同一个按钮在运行且输入为空时停止,有内容时发送追加消息,空闲时发送新消息。输入正文默认高 64px,随多行内容自动增高至视口高度的 24%,最多 160px;超出后在输入区内部滚动,清空后恢复 64px;队列单独限制滚动高度,工作台支持的窄屏和短窗口保留可见的输入操作。
|
运行中的输入区使用一个外框:顶部显示待处理消息,正文最多预览两行,溢出时提供“展开全文”;每条消息只保留正文、简短的处理方式和一个菜单。编辑、处理方式切换、关闭排队统一进入该菜单。底栏在生成期间用“引导 / 排队”替代暂时不能切换的模型选择,生成完成后恢复模型选择。右下角的同一个按钮在运行且输入为空时停止,有内容时发送追加消息,空闲时发送新消息。输入正文默认高 64px,随多行内容自动增高至视口高度的 24%,最多 160px;超出后在输入区内部滚动,清空后恢复 64px;队列单独限制滚动高度,工作台支持的窄屏和短窗口保留可见的输入操作。
|
||||||
|
|
||||||
## 1. 交互参考
|
## 1. 交互参考
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ import { agentRuntimes, createAgentRuntime, mergeHistoryMessages, resumeAgentRun
|
|||||||
type AgentReadingPosition, type AgentSessionRuntime } from "./agent-session-runtime";
|
type AgentReadingPosition, type AgentSessionRuntime } from "./agent-session-runtime";
|
||||||
|
|
||||||
const AGENT_PANEL_COLLAPSE_MS = 180;
|
const AGENT_PANEL_COLLAPSE_MS = 180;
|
||||||
|
const LEGACY_COLLABORATION_TITLES = new Set(["任务协作", "任务协作问答", "新对话"]);
|
||||||
|
|
||||||
type UseWorkbenchAgentOptions = {
|
type UseWorkbenchAgentOptions = {
|
||||||
onUiEnvelope: (payload: UIEnvelopePayload, sessionId: string) => Promise<void> | void;
|
onUiEnvelope: (payload: UIEnvelopePayload, sessionId: string) => Promise<void> | void;
|
||||||
@@ -334,6 +335,20 @@ export function useWorkbenchAgent({
|
|||||||
// Ownership changes only when selecting a different runtime, never on token updates.
|
// Ownership changes only when selecting a different runtime, never on token updates.
|
||||||
if (resources.activeAgent !== runtime) activateRuntime(runtime);
|
if (resources.activeAgent !== runtime) activateRuntime(runtime);
|
||||||
}, [activateRuntime, resources, runtime]);
|
}, [activateRuntime, resources, runtime]);
|
||||||
|
useEffect(() => {
|
||||||
|
let changed = false;
|
||||||
|
const summaries = new Map(history.sessions.map((summary) => [summary.id, summary]));
|
||||||
|
for (const entry of agentRuntimes(resources)) {
|
||||||
|
if (entry.scope !== scopeKey || entry.purpose !== "task_collaboration"
|
||||||
|
|| !LEGACY_COLLABORATION_TITLES.has(entry.title)) continue;
|
||||||
|
const summary = entry.sessionId ? summaries.get(entry.sessionId) : undefined;
|
||||||
|
if (!summary || LEGACY_COLLABORATION_TITLES.has(summary.title)) continue;
|
||||||
|
entry.title = summary.title;
|
||||||
|
if (resources.activeAgent === entry) setSessionTitle(summary.title);
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
if (changed) resources.notify();
|
||||||
|
}, [history.sessions, resources, scopeKey]);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
chatRef.current = chat;
|
chatRef.current = chat;
|
||||||
}, [chat]);
|
}, [chat]);
|
||||||
@@ -642,6 +657,7 @@ export function useWorkbenchAgent({
|
|||||||
if (!normalizedTaskId) throw new Error("工况任务 ID 不能为空。");
|
if (!normalizedTaskId) throw new Error("工况任务 ID 不能为空。");
|
||||||
const context = agentTaskDiscussionContextSchema.parse(discussion?.context ?? {});
|
const context = agentTaskDiscussionContextSchema.parse(discussion?.context ?? {});
|
||||||
const sourceDraft = resources.activeAgent?.draft ?? promptDraftRef.current;
|
const sourceDraft = resources.activeAgent?.draft ?? promptDraftRef.current;
|
||||||
|
const titlesBeforeOpen = new Map(agentRuntimes(resources).filter((entry) => entry.scope === projectScopeKey).map((entry) => [entry, entry.title]));
|
||||||
const projectClient = createAgentApiClient(undefined, { getAccessToken: getWorkbenchTokenProvider(getAccessToken), getProjectId });
|
const projectClient = createAgentApiClient(undefined, { getAccessToken: getWorkbenchTokenProvider(getAccessToken), getProjectId });
|
||||||
const collaboration = await projectClient.createTaskCollaborationSession(normalizedTaskId);
|
const collaboration = await projectClient.createTaskCollaborationSession(normalizedTaskId);
|
||||||
const id = collaboration.session_id || collaboration.id;
|
const id = collaboration.session_id || collaboration.id;
|
||||||
@@ -654,6 +670,10 @@ export function useWorkbenchAgent({
|
|||||||
createWorkbenchAgentTransport(projectClient, getAccessToken, getProjectId), loaded);
|
createWorkbenchAgentTransport(projectClient, getAccessToken, getProjectId), loaded);
|
||||||
if (loaded.isStreaming || loaded.runStatus === "running") resumeAgentRuntime(resources, target);
|
if (loaded.isStreaming || loaded.runStatus === "running") resumeAgentRuntime(resources, target);
|
||||||
}
|
}
|
||||||
|
const titleBeforeOpen = titlesBeforeOpen.get(target);
|
||||||
|
const canApplyTitle = titleBeforeOpen === undefined
|
||||||
|
? LEGACY_COLLABORATION_TITLES.has(target.title) : target.title === titleBeforeOpen;
|
||||||
|
if (collaboration.title?.trim() && canApplyTitle) target.title = collaboration.title.trim();
|
||||||
target.taskDiscussion = { context, reportGeneration: discussion?.reportGeneration };
|
target.taskDiscussion = { context, reportGeneration: discussion?.reportGeneration };
|
||||||
if (!target.draft.trim() && target !== resources.activeAgent) target.draft = sourceDraft;
|
if (!target.draft.trim() && target !== resources.activeAgent) target.draft = sourceDraft;
|
||||||
if (discussion?.prompt?.trim()) target.draft = target.draft.trim() ? `${target.draft}\n\n${discussion.prompt.trim()}` : discussion.prompt.trim();
|
if (discussion?.prompt?.trim()) target.draft = target.draft.trim() ? `${target.draft}\n\n${discussion.prompt.trim()}` : discussion.prompt.trim();
|
||||||
|
|||||||
@@ -5,6 +5,33 @@ import { createServer, type ServerResponse } from "node:http";
|
|||||||
import { responseFixtureWriter } from "./support/responses-fixture";
|
import { responseFixtureWriter } from "./support/responses-fixture";
|
||||||
import { expectThemeColor } from "./support/theme-colors";
|
import { expectThemeColor } from "./support/theme-colors";
|
||||||
|
|
||||||
|
test("history repairs a cached collaboration title without reloading its conversation", async ({ page }, testInfo) => {
|
||||||
|
await page.emulateMedia({ reducedMotion: "reduce" });
|
||||||
|
await mockWorkbenchContext(page);
|
||||||
|
const session = { session_id: "collaboration-50000000-0000-4000-8000-000000000001",
|
||||||
|
purpose: "task_collaboration", title: "任务协作", created_at: Date.now(), updated_at: Date.now() };
|
||||||
|
await mockAgentApi(page, { sessions: [session] });
|
||||||
|
let loads = 0;
|
||||||
|
page.on("request", (request) => {
|
||||||
|
if (request.method() === "GET" && new URL(request.url()).pathname.endsWith(`/session/${session.session_id}`)) loads += 1;
|
||||||
|
});
|
||||||
|
await page.goto("/", { waitUntil: "domcontentloaded" });
|
||||||
|
await page.getByRole("button", { name: /展开 Agent 助手面板/ }).click();
|
||||||
|
const panel = page.getByRole("complementary", { name: "Agent 命令面板" });
|
||||||
|
await panel.getByRole("button", { name: "打开 Agent 历史记录" }).click();
|
||||||
|
const dialog = page.getByRole("dialog", { name: "历史对话" });
|
||||||
|
await dialog.locator("button.agent-history-session-trigger").filter({ hasText: "任务协作" }).click();
|
||||||
|
await expect(panel.locator(".agent-chat-context-bar > p")).toHaveText("任务协作");
|
||||||
|
session.title = "南桥管道爆管调查";
|
||||||
|
await panel.getByRole("button", { name: "打开 Agent 历史记录" }).click();
|
||||||
|
await dialog.getByRole("button", { name: "刷新 Agent 历史记录" }).click();
|
||||||
|
await expect(dialog.locator("button.agent-history-session-trigger").filter({ hasText: session.title })).toBeVisible();
|
||||||
|
await page.keyboard.press("Escape");
|
||||||
|
await expect(panel.locator(".agent-chat-context-bar > p")).toHaveText(session.title);
|
||||||
|
expect(loads).toBe(1);
|
||||||
|
await panel.screenshot({ path: testInfo.outputPath("collaboration-title-repaired.png"), animations: "disabled" });
|
||||||
|
});
|
||||||
|
|
||||||
async function prepare(page: Page) {
|
async function prepare(page: Page) {
|
||||||
await page.emulateMedia({ reducedMotion: "reduce" });
|
await page.emulateMedia({ reducedMotion: "reduce" });
|
||||||
await mockWorkbenchContext(page);
|
await mockWorkbenchContext(page);
|
||||||
|
|||||||
@@ -102,6 +102,22 @@ test("单一协作入口展示任务引用,保留草稿且仅手动发送,
|
|||||||
await page.emulateMedia({ reducedMotion: "reduce" });
|
await page.emulateMedia({ reducedMotion: "reduce" });
|
||||||
await mockWorkbenchContext(page);
|
await mockWorkbenchContext(page);
|
||||||
await mockAgentApi(page, { sessions: [{ session_id: "ordinary-main", title: "供水核查主聊天", purpose: "ordinary_chat", created_at: now, updated_at: now }] });
|
await mockAgentApi(page, { sessions: [{ session_id: "ordinary-main", title: "供水核查主聊天", purpose: "ordinary_chat", created_at: now, updated_at: now }] });
|
||||||
|
let collaborationOpens = 0;
|
||||||
|
const renamedCollaborationTitle = "异味投诉现场复核记录";
|
||||||
|
let finishReopen!: () => void;
|
||||||
|
const reopenGate = new Promise<void>((resolve) => { finishReopen = resolve; });
|
||||||
|
await page.route(`**/task/${taskId}/collaboration/session`, async (route) => {
|
||||||
|
collaborationOpens += 1;
|
||||||
|
if (collaborationOpens === 2) await reopenGate;
|
||||||
|
return route.fulfill({ headers: { "Access-Control-Allow-Origin": "*" }, json: {
|
||||||
|
session_id: `collaboration-${taskId}`, purpose: "task_collaboration", task_id: taskId,
|
||||||
|
title: collaborationOpens <= 2 ? task.title : renamedCollaborationTitle,
|
||||||
|
created: collaborationOpens === 1, status: "active",
|
||||||
|
} });
|
||||||
|
});
|
||||||
|
await page.route(`**/session/collaboration-${taskId}/title`, (route) => route.fulfill({
|
||||||
|
headers: { "Access-Control-Allow-Origin": "*" }, json: { id: `collaboration-${taskId}`, title: route.request().postDataJSON().title },
|
||||||
|
}));
|
||||||
let submitted = 0;
|
let submitted = 0;
|
||||||
let inboxReads = 0;
|
let inboxReads = 0;
|
||||||
let incoming = false;
|
let incoming = false;
|
||||||
@@ -146,7 +162,7 @@ test("单一协作入口展示任务引用,保留草稿且仅手动发送,
|
|||||||
await expect(feed.getByRole("button", { name: "Agent 协作", exact: true })).toHaveAttribute("title", "打开调查协作对话,引用此任务,不会直接发送。");
|
await expect(feed.getByRole("button", { name: "Agent 协作", exact: true })).toHaveAttribute("title", "打开调查协作对话,引用此任务,不会直接发送。");
|
||||||
await feed.screenshot({ path: testInfo.outputPath("investigation-entry-desktop.png") });
|
await feed.screenshot({ path: testInfo.outputPath("investigation-entry-desktop.png") });
|
||||||
await feed.getByRole("button", { name: "Agent 协作", exact: true }).click();
|
await feed.getByRole("button", { name: "Agent 协作", exact: true }).click();
|
||||||
await expect(agent.getByText("任务协作问答", { exact: true }).first()).toBeVisible();
|
await expect(agent.locator(".agent-chat-context-bar > p")).toHaveText(task.title);
|
||||||
const reference = agent.getByRole("group", { name: "对话引用" });
|
const reference = agent.getByRole("group", { name: "对话引用" });
|
||||||
await expect(reference).toContainText("居民异味投诉调查");
|
await expect(reference).toContainText("居民异味投诉调查");
|
||||||
await expect(reference).toContainText("第 1 轮 · 报告第 3 版");
|
await expect(reference).toContainText("第 1 轮 · 报告第 3 版");
|
||||||
@@ -156,6 +172,18 @@ test("单一协作入口展示任务引用,保留草稿且仅手动发送,
|
|||||||
await expect(prompt).toBeFocused();
|
await expect(prompt).toBeFocused();
|
||||||
expect(submitted).toBe(0);
|
expect(submitted).toBe(0);
|
||||||
await feed.getByRole("button", { name: "Agent 协作", exact: true }).click();
|
await feed.getByRole("button", { name: "Agent 协作", exact: true }).click();
|
||||||
|
await expect.poll(() => collaborationOpens).toBe(2);
|
||||||
|
await agent.getByRole("button", { name: "打开 Agent 历史记录" }).click();
|
||||||
|
const renameDialog = page.getByRole("dialog", { name: "历史对话" });
|
||||||
|
await renameDialog.getByRole("button", { name: `会话操作:${task.title}` }).click();
|
||||||
|
await page.getByRole("menuitem", { name: "重命名对话", exact: true }).click();
|
||||||
|
await renameDialog.getByRole("textbox", { name: "会话标题", exact: true }).fill(renamedCollaborationTitle);
|
||||||
|
await renameDialog.getByRole("button", { name: "保存对话标题" }).click();
|
||||||
|
await expect(renameDialog.locator("button.agent-history-session-trigger").filter({ hasText: renamedCollaborationTitle })).toBeVisible();
|
||||||
|
await page.keyboard.press("Escape");
|
||||||
|
finishReopen();
|
||||||
|
await expect(prompt).toBeFocused();
|
||||||
|
await expect(agent.locator(".agent-chat-context-bar > p")).toHaveText(renamedCollaborationTitle);
|
||||||
await expect(prompt).toBeFocused();
|
await expect(prompt).toBeFocused();
|
||||||
await expect(prompt).toHaveValue("保留的现场补充草稿");
|
await expect(prompt).toHaveValue("保留的现场补充草稿");
|
||||||
await feed.getByLabel("主要依据").getByRole("button", { name: "围绕此证据提问" }).click();
|
await feed.getByLabel("主要依据").getByRole("button", { name: "围绕此证据提问" }).click();
|
||||||
@@ -200,6 +228,7 @@ test("单一协作入口展示任务引用,保留草稿且仅手动发送,
|
|||||||
await expect(mobileAgent.getByRole("button", { name: "停止 Agent 分析" })).toHaveCount(0);
|
await expect(mobileAgent.getByRole("button", { name: "停止 Agent 分析" })).toHaveCount(0);
|
||||||
incoming = true;
|
incoming = true;
|
||||||
await mobileAgent.getByRole("button", { name: "打开 Agent 历史记录" }).click();
|
await mobileAgent.getByRole("button", { name: "打开 Agent 历史记录" }).click();
|
||||||
|
await expect(page.getByRole("dialog", { name: "历史对话" }).locator("button.agent-history-session-trigger").filter({ hasText: renamedCollaborationTitle })).toBeVisible();
|
||||||
await page.getByRole("dialog", { name: "历史对话" }).getByRole("button", { name: /^供水核查主聊天/ }).click();
|
await page.getByRole("dialog", { name: "历史对话" }).getByRole("button", { name: /^供水核查主聊天/ }).click();
|
||||||
await expect(mobileAgent.getByRole("group", { name: "对话引用" })).toHaveCount(0);
|
await expect(mobileAgent.getByRole("group", { name: "对话引用" })).toHaveCount(0);
|
||||||
await expect(mobileAgent.getByText("已核对上游监测,请补充现场采样记录。")).toBeVisible();
|
await expect(mobileAgent.getByText("已核对上游监测,请补充现场采样记录。")).toBeVisible();
|
||||||
|
|||||||
Reference in New Issue
Block a user