// 智能体页面的导航状态(纯函数 + 模块级记忆,单测覆盖): // - 会话链接、列表页链接的拼法(保留演示开关); // - 会话列表的筛选与滚动位置(列表页 ↔ 会话页之间保留,刷新页面才清)。 // 列表不再分「对话 / 自动巡检」两栏:旧链接里的 ?tab= 直接忽略。 function withQuery(path: string, q: Record): string { const p = new URLSearchParams(); for (const [k, v] of Object.entries(q)) if (v) p.set(k, v); const s = p.toString(); return s ? `${path}?${s}` : path; } /** 会话页链接 */ export function agentRunHref(id: string, mock = false): string { return withQuery(`/console/agent/${id}`, { mock: mock ? "1" : null }); } /** 列表页链接 */ export function agentListHref(mock = false): string { return withQuery("/console/agent", { mock: mock ? "1" : null }); } /** 会话列表的记忆:搜索、状态筛选、滚动位置 */ export type ListMemory = { q: string; view: "all" | "pending" | "active" | "failed"; scroll: number; }; export const newListMemory = (): ListMemory => ({ q: "", view: "all", scroll: 0 }); export const pageListMemory: ListMemory = newListMemory(); /** * 当前项要不要滚进可见区:完全看不见才滚,只滚到刚好露出(nearest),返回新的 scrollTop;不用滚时为 null。 * top / height 是当前项相对滚动内容顶部的位置。 */ export function revealScrollTop( item: { top: number; height: number }, viewport: { scrollTop: number; height: number }, ): number | null { const bottom = item.top + item.height; const viewBottom = viewport.scrollTop + viewport.height; if (item.top >= viewport.scrollTop && bottom <= viewBottom) return null; if (item.top < viewport.scrollTop) return Math.max(0, item.top); return Math.max(0, bottom - viewport.height); } // ── 输入草稿:按会话(和两处新对话输入框)分开记,切换会话、关抽屉不丢;发送成功后清掉。模块级,刷新页面才清 ── const drafts = new Map(); export const DRAFT_PAGE_GOAL = "goal:page"; export const DRAFT_DRAWER_GOAL = "goal:drawer"; export const runDraftKey = (runId: string) => `run:${runId}`; export function readDraft(key: string | undefined): string { return key ? (drafts.get(key) ?? "") : ""; } export function writeDraft(key: string | undefined, text: string): void { if (!key) return; if (text.trim()) drafts.set(key, text); else drafts.delete(key); }