chore(release): v0.3.41

智能体抽屉读 ?open= 上下文、回复链接用抽屉地址;抽屉打开不亮焦点框;语句统计重复 key。

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Cxwgnn2pbzQ7rXv3rq7fei
This commit is contained in:
rootandClaude Opus 5.5 committed 2026-10-08 14:39:24 +08:00
1 parent 78a4d562f0
commit 1ade49fe8e
22 files changed
+62 -33

No files matched your search

@@ -40,6 +40,7 @@ import { Markdown } from "~/components/agentui/markdown.tsx";
import { MessageMarker, MessageTyping } from "~/components/agentui/message.tsx";
import { StreamingResponse } from "~/components/agentui/streaming-response.tsx";
import { consolePath } from "~/components/console/nav.ts";
import { recordPath } from "~/components/console/record-sheet.tsx";
import { StatusBadge } from "~/components/console/status-badge.tsx";
import { TechInfo } from "~/components/console/tech-info.tsx";
import { formatUsd } from "~/components/schedules/labels.ts";
@@ -673,14 +674,14 @@ export function resultFacts(report: GoalReport | null): ResultFact[] {
const only = r.downloads.length === 1 ? r.downloads[0] : undefined;
facts.push({
text: `提交了 ${downloads} 个下载`,
to: only ? `/console/downloads/${only.downloadTaskId}` : r.downloads.length ? "/console/downloads" : undefined,
to: only ? recordPath("downloads", only.downloadTaskId) : r.downloads.length ? "/console/downloads" : undefined,
});
}
if (done.length) {
const only = done.length === 1 ? done[0] : undefined;
facts.push({
text: `完成 ${done.length} 次整理`,
to: only ? `/console/organize/${only.recordId}` : "/console/organize",
to: only ? recordPath("organize", only.recordId) : "/console/organize",
});
}
if (r.imported && r.imported !== done.length) facts.push({ text: `已整理 ${r.imported} 个` });
@@ -109,7 +109,7 @@ export function AgentDrawer() {
const content = useRef<HTMLDivElement>(null);
// 记下最近的非智能体页面:会话页「在侧边打开」回到这里
useEffect(() => noteAgentReturnPath(pathname, search), [pathname, search]);
useEffect(() => syncAgentContextToPath(pathname), [pathname]);
useEffect(() => syncAgentContextToPath(pathname, search), [pathname, search]);
const name = useContextName(context);
const mock = agentMockEnabled();
// 目标前缀带名称和 ID:名称给人看,ID 给模型用
@@ -210,7 +210,7 @@ export function AgentDrawer() {
/** 顶栏胶囊里的智能体按钮(没有侧栏的窄屏):与快捷键、侧栏入口同一套切换(页面内按钮用 openAgentDrawer 传更完整的上下文) */
export function AgentDrawerTrigger({ className }: { className?: string }) {
const { pathname } = useLocation();
const { pathname, search } = useLocation();
return (
<Button
variant="ghost"
@@ -219,7 +219,7 @@ export function AgentDrawerTrigger({ className }: { className?: string }) {
title={`智能体(${agentShortcutLabel()})`}
className={className}
{...{ [AGENT_LAUNCHER_ATTR]: "" }}
onClick={() => toggleAgentDrawer(pathname)}
onClick={() => toggleAgentDrawer(pathname, search)}
>
<Bot />
</Button>
@@ -80,6 +80,11 @@ describe("侧栏上下文", () => {
expect(ctx?.taskId).toBe("5b4e76e0-f27f-4e7a-8e08-71486387df1d");
expect(contextFromPath("/console/sites/hdhome")?.siteId).toBe("hdhome");
expect(contextFromPath("/console/agent")).toBeNull();
// 列表页的记录抽屉(?open=)与日志页的任务抽屉(?run=)
expect(contextFromPath("/console/sites", "?open=hdhome&tab=rss")?.siteId).toBe("hdhome");
expect(contextFromPath("/console/organize", "?open=r1")?.recordId).toBe("r1");
expect(contextFromPath("/console/logs", "?type=task&run=t9")?.taskId).toBe("t9");
expect(contextFromPath("/console/sites", "?tab=rss")).toBeNull();
const goal = `${contextPrefix(ctx)}排查这个任务`;
expect(splitContextGoal(goal)).toEqual({ context: expect.stringContaining("taskId="), text: "排查这个任务" });
});
@@ -662,7 +662,7 @@ describe("实际结果行(FIX-9):只列系统核实的事实", () => {
createElement(ResultLine, { run: RUN, report: done, live: withText("提交了 2 个下载,整理了 1 个。") }),
);
expect(text(html).replace(/\s*、\s*/g, "、")).toContain("提交了 2 个下载、完成 1 次整理、1 个进行中 · 41 秒");
expect(html).toContain(`href="/console/organize/${ID(3)}"`);
expect(html).toContain(`href="/console/organize?open=${ID(3)}"`);
expect(html).toContain('href="/console/downloads"');
expect(html).not.toContain("与汇报不一致");
});
+12 -5
View File
@@ -87,9 +87,9 @@ export function newAgentConversation() {
* 抽屉开着时路由变了(浏览器前进后退、会话里的链接):还在新对话、上下文是按路由推断的,就换成新页面的对象;
* 页面按钮带进来的上下文、已经在看的会话不动。
*/
export function syncAgentContextToPath(pathname: string) {
export function syncAgentContextToPath(pathname: string, search = "") {
if (!state.open || state.runId || (state.context && !state.context.fromPath)) return;
const next = contextFromPath(pathname);
const next = contextFromPath(pathname, search);
if (JSON.stringify(next) === JSON.stringify(state.context)) return;
state = { ...state, context: next };
emit();
@@ -144,17 +144,24 @@ function focusAgentPageInput(): boolean {
}
/** 快捷键 / 侧栏主入口 / 顶栏胶囊里的按钮共用的切换 */
export function toggleAgentDrawer(pathname: string) {
export function toggleAgentDrawer(pathname: string, search = "") {
const action = toggleAction(state, pathname);
if (action === "close") setAgentDrawerOpen(false);
// 智能体页面本身就是会话:聚焦页面的输入框,找不到输入框才开抽屉
else if (action === "focus-page" && focusAgentPageInput()) return;
else if (state.runId) setAgentDrawerOpen(true);
else openAgentDrawer(contextFromPath(pathname));
else openAgentDrawer(contextFromPath(pathname, search));
}
/** 按当前路由推断页面上下文(顶栏按钮用;页面内按钮直接传更完整的上下文) */
export function contextFromPath(pathname: string): AgentContext | null {
export function contextFromPath(pathname: string, search = ""): AgentContext | null {
// 列表页上开着的记录抽屉(?open=,console-ia §9)等同于旧的详情页;日志页的任务抽屉是 ?run=
const q = new URLSearchParams(search);
const open = q.get("open");
const listPage = pathname.match(/^\/console\/(sites|downloads|organize)\/?$/);
if (listPage && open) return contextFromPath(`/console/${listPage[1]}/${open}`);
const run = q.get("run");
if (/^\/console\/logs\/?$/.test(pathname) && run) return contextFromPath(`/console/engine/${run}`);
const m = pathname.match(/^\/console\/(library|sites|tasks|downloads|organize|engine)\/([^/?#]+)/);
if (!m) return null;
const [, kind, id] = m as unknown as [string, string, string];
@@ -30,16 +30,16 @@ export function useActiveAgentRuns() {
/** 全局快捷键:Ctrl/⌘+J 切换抽屉(Esc 关闭由抽屉自身处理) */
function useAgentHotkey() {
const { pathname } = useLocation();
const { pathname, search } = useLocation();
useEffect(() => {
const onKey = (e: KeyboardEvent) => {
if (e.repeat || !isAgentShortcut(e)) return;
e.preventDefault();
toggleAgentDrawer(pathname);
toggleAgentDrawer(pathname, search);
};
window.addEventListener("keydown", onKey);
return () => window.removeEventListener("keydown", onKey);
}, [pathname]);
}, [pathname, search]);
}
/** 全局快捷键 Ctrl/⌘+J(没有界面,挂在 console layout 里一次) */
@@ -74,12 +74,12 @@ function CountBadge({ count, className }: { count: number; className?: string })
/** 侧栏最上方的 Agent 主入口:强调样式(棱镜环),显示进行中会话数 */
export function AgentNavEntry({ collapsed = false, onNavigate }: { collapsed?: boolean; onNavigate?: () => void }) {
const { pathname } = useLocation();
const { pathname, search } = useLocation();
const { count, summary } = useActiveAgentRuns();
const shortcut = agentShortcutLabel();
const activate = () => {
onNavigate?.();
toggleAgentDrawer(pathname);
toggleAgentDrawer(pathname, search);
};
const title = `智能体(${shortcut})${summary ? `\n${summary}` : ""}`;
@@ -77,6 +77,11 @@ function RootSheet({
<SheetContent
side="right"
className={cn("w-full gap-0 p-0", WIDTH[width], className)}
// 打开时焦点落在抽屉本身而不是第一个按钮 / 标签,避免一打开就亮焦点框;Tab 仍从抽屉内开始
onOpenAutoFocus={(e) => {
e.preventDefault();
(e.currentTarget as HTMLElement | null)?.focus({ preventScroll: true });
}}
onEscapeKeyDown={(e) => {
// 有子视图时 Esc 先回到上一层
const top = stack.current.at(-1);
@@ -207,10 +207,12 @@ const fmtMs = (ms: number) =>
ms >= 1000 ? `${(ms / 1000).toFixed(1)} 秒` : `${ms < 10 ? ms.toFixed(2) : Math.round(ms)} 毫秒`;
function StatementTable({ rows }: { rows: StatementRow[] }) {
// 规范化后的语句文本可能重复(不同角色 / 库各一行),用序号做 key
const keyed = rows.map((r, i) => ({ ...r, rowIndex: i }));
return (
<DataTable<StatementRow>
rows={rows}
rowKey={(r) => r.query}
<DataTable<StatementRow & { rowIndex: number }>
rows={keyed}
rowKey={(r) => String(r.rowIndex)}
empty={{ title: "还没有统计" }}
columns={[
{
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@umt/web",
"version": "0.3.40",
"version": "0.3.41",
"private": true,
"type": "module",
"scripts": {