fix(agent): 会话列表分栏跟随当前会话并进 URL,保留滚动位置
- 打开巡检会话(含巡检的子会话)时左栏自动切到「自动巡检」并选中当前项; 手动切换分栏写进 ?tab=,刷新、窄屏「全部会话」返回都保持 - 列表页 / 会话页之间保留搜索、筛选和各分栏的滚动位置;当前会话看不见时滚进可见区 - 跳到子会话时展开父会话 - 演示模式的分栏筛选与服务端一致:子会话随父会话 Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Cxwgnn2pbzQ7rXv3rq7fei
This commit is contained in:
1 parent
177d21c26b
commit
f5234cbcb9
7 files changed
+257
-30
No files matched your search
@@ -141,22 +141,34 @@ export const TRIGGER_LABEL: Record<AgentTrigger, string> = {
|
||||
export type AgentRunGroup = "conversation" | "auto";
|
||||
const isAutoTrigger = (t: AgentTrigger) => t === "schedule" || t === "event";
|
||||
|
||||
/** 会话所在分栏:子会话随父会话(与服务端 group 筛选一致);父会话未知时子会话归「对话」 */
|
||||
export function runGroup(
|
||||
run: Pick<AgentRunView, "trigger">,
|
||||
parent?: Pick<AgentRunView, "trigger"> | null,
|
||||
): AgentRunGroup {
|
||||
const t = run.trigger === "subagent" && parent ? parent.trigger : run.trigger;
|
||||
return isAutoTrigger(t) ? "auto" : "conversation";
|
||||
}
|
||||
|
||||
export function useAgentRuns(status?: AgentRunStatus, trigger?: AgentTrigger, group?: AgentRunGroup) {
|
||||
const mock = useAgentMockGate();
|
||||
return useInfiniteQuery({
|
||||
queryKey: [...agentKeys.runs(status, trigger, group), mock],
|
||||
initialPageParam: undefined as string | undefined,
|
||||
queryFn: async ({ pageParam, signal }) => {
|
||||
if (mock)
|
||||
if (mock) {
|
||||
const all = MOCK_RUNS();
|
||||
const byId = new Map(all.map((r) => [r.id, r] as const));
|
||||
return {
|
||||
items: MOCK_RUNS().filter(
|
||||
items: all.filter(
|
||||
(r) =>
|
||||
(!status || r.status === status) &&
|
||||
(!trigger || r.trigger === trigger) &&
|
||||
(!group || isAutoTrigger(r.trigger) === (group === "auto")),
|
||||
(!group || runGroup(r, r.parentRunId ? byId.get(r.parentRunId) : null) === group),
|
||||
),
|
||||
nextCursor: null,
|
||||
};
|
||||
}
|
||||
return apiParsed(RunPage, AGENT_BASE, {
|
||||
params: cleanParams({ status, trigger, group, cursor: pageParam, limit: 30 }),
|
||||
signal,
|
||||
|
||||
@@ -14,7 +14,7 @@ import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle } from "
|
||||
import { errorMessage } from "~/lib/api.ts";
|
||||
import { capabilityMeta, isDenied, RunStatusBadge } from "./agent-conversation.tsx";
|
||||
import { AgentRunPanel, GoalComposer } from "./agent-run-panel.tsx";
|
||||
import { AgentSessionList } from "./agent-session-list.tsx";
|
||||
import { AgentSessionList, drawerListMemory } from "./agent-session-list.tsx";
|
||||
import {
|
||||
type AgentContext,
|
||||
agentShortcutLabel,
|
||||
@@ -71,7 +71,7 @@ function HistoryPopover({ activeId }: { activeId: string | null }) {
|
||||
</PopoverTrigger>
|
||||
<PopoverContent align="end" className="flex h-[min(32rem,70dvh)] w-[min(22rem,calc(100vw-2rem))] flex-col p-2">
|
||||
<AgentSessionList
|
||||
remember
|
||||
memory={drawerListMemory}
|
||||
limit={20}
|
||||
activeId={activeId ?? undefined}
|
||||
onOpen={(id) => {
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { runGroup } from "~/api/agent.ts";
|
||||
import { agentListHref, agentRunHref, groupFromSearch, resolveGroup, revealScrollTop } from "./agent-nav.ts";
|
||||
import { isAgentPage, outletKey } from "./agent-store.ts";
|
||||
|
||||
// 智能体页面切换的状态逻辑:整页重挂载 key、分栏与 URL、草稿、滚动策略、快捷键在智能体页面的行为。
|
||||
@@ -15,3 +17,46 @@ describe("主内容区重挂载 key", () => {
|
||||
expect(isAgentPage("/console/agentx")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("分栏:对话 / 自动巡检", () => {
|
||||
it("会话所在分栏:巡检 / 事件在自动巡检,子会话随父会话,父会话未知归对话", () => {
|
||||
expect(runGroup({ trigger: "schedule" })).toBe("auto");
|
||||
expect(runGroup({ trigger: "event" })).toBe("auto");
|
||||
expect(runGroup({ trigger: "user" })).toBe("conversation");
|
||||
expect(runGroup({ trigger: "subagent" }, { trigger: "schedule" })).toBe("auto");
|
||||
expect(runGroup({ trigger: "subagent" }, { trigger: "user" })).toBe("conversation");
|
||||
expect(runGroup({ trigger: "subagent" })).toBe("conversation");
|
||||
});
|
||||
it("URL 的 tab 只认两个取值", () => {
|
||||
expect(groupFromSearch("?tab=auto&mock=1")).toBe("auto");
|
||||
expect(groupFromSearch("?tab=conversation")).toBe("conversation");
|
||||
expect(groupFromSearch("?tab=xx")).toBeNull();
|
||||
expect(groupFromSearch("")).toBeNull();
|
||||
});
|
||||
it("取值顺序:URL > 当前会话 > 上次 > 对话", () => {
|
||||
expect(resolveGroup({ fromUrl: "conversation", activeRun: "auto", remembered: "auto" })).toBe("conversation");
|
||||
expect(resolveGroup({ fromUrl: null, activeRun: "auto", remembered: "conversation" })).toBe("auto");
|
||||
// 打开巡检会话、快照还没到:沿用点它时所在的分栏,不先闪到「对话」
|
||||
expect(resolveGroup({ fromUrl: null, activeRun: null, remembered: "auto" })).toBe("auto");
|
||||
expect(resolveGroup({ fromUrl: null })).toBe("conversation");
|
||||
});
|
||||
it("链接:会话页不带 tab(由会话决定),列表页只在自动巡检时带,保留演示开关", () => {
|
||||
expect(agentRunHref("r1")).toBe("/console/agent/r1");
|
||||
expect(agentRunHref("r1", true)).toBe("/console/agent/r1?mock=1");
|
||||
expect(agentListHref()).toBe("/console/agent");
|
||||
expect(agentListHref("auto")).toBe("/console/agent?tab=auto");
|
||||
expect(agentListHref("auto", true)).toBe("/console/agent?tab=auto&mock=1");
|
||||
expect(agentListHref("conversation", true)).toBe("/console/agent?mock=1");
|
||||
});
|
||||
});
|
||||
|
||||
describe("当前会话滚进可见区", () => {
|
||||
const vp = { scrollTop: 200, height: 300 };
|
||||
it("已经完整可见不滚", () => {
|
||||
expect(revealScrollTop({ top: 250, height: 40 }, vp)).toBeNull();
|
||||
});
|
||||
it("在上方:滚到顶边对齐;在下方:滚到底边刚好露出", () => {
|
||||
expect(revealScrollTop({ top: 120, height: 40 }, vp)).toBe(120);
|
||||
expect(revealScrollTop({ top: 600, height: 40 }, vp)).toBe(340);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,110 @@
|
||||
import { useEffect } from "react";
|
||||
import { useSearchParams } from "react-router";
|
||||
import { type AgentRunGroup, runGroup, useAgentRun } from "~/api/agent.ts";
|
||||
|
||||
// 智能体页面的导航状态(纯函数 + 模块级记忆,单测覆盖):
|
||||
// - 分栏「对话 / 自动巡检」:URL 的 ?tab= 优先,其次当前会话所在分栏,再次上次看的分栏;
|
||||
// - 会话链接、列表页链接的拼法(保留演示开关);
|
||||
// - 页面会话列表的筛选与各分栏滚动位置(列表页 ↔ 会话页之间保留,刷新页面才清)。
|
||||
|
||||
export const TAB_PARAM = "tab";
|
||||
|
||||
/** URL 里的分栏;没写或写错时为 null */
|
||||
export function groupFromSearch(search: string | URLSearchParams): AgentRunGroup | null {
|
||||
const v = (typeof search === "string" ? new URLSearchParams(search) : search).get(TAB_PARAM);
|
||||
return v === "auto" || v === "conversation" ? v : null;
|
||||
}
|
||||
|
||||
/** 分栏取值:URL 显式指定 > 当前会话所在分栏 > 上次看的分栏 > 对话 */
|
||||
export function resolveGroup(o: {
|
||||
fromUrl: AgentRunGroup | null;
|
||||
activeRun?: AgentRunGroup | null;
|
||||
remembered?: AgentRunGroup | null;
|
||||
}): AgentRunGroup {
|
||||
return o.fromUrl ?? o.activeRun ?? o.remembered ?? "conversation";
|
||||
}
|
||||
|
||||
function withQuery(path: string, q: Record<string, string | null>): 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;
|
||||
}
|
||||
|
||||
/** 会话页链接:分栏由会话本身决定,不带 tab */
|
||||
export function agentRunHref(id: string, mock = false): string {
|
||||
return withQuery(`/console/agent/${id}`, { mock: mock ? "1" : null });
|
||||
}
|
||||
|
||||
/** 列表页链接:「对话」是缺省,不写进 URL */
|
||||
export function agentListHref(group: AgentRunGroup = "conversation", mock = false): string {
|
||||
return withQuery("/console/agent", { [TAB_PARAM]: group === "auto" ? "auto" : null, mock: mock ? "1" : null });
|
||||
}
|
||||
|
||||
/** 页面会话列表的记忆:上次看的分栏、搜索与筛选、各分栏的滚动位置 */
|
||||
export type ListMemory = {
|
||||
group: AgentRunGroup;
|
||||
q: string;
|
||||
view: "all" | "pending" | "failed";
|
||||
scroll: Record<AgentRunGroup, number>;
|
||||
};
|
||||
export const newListMemory = (): ListMemory => ({
|
||||
group: "conversation",
|
||||
q: "",
|
||||
view: "all",
|
||||
scroll: { conversation: 0, auto: 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);
|
||||
}
|
||||
|
||||
/** 会话所在分栏(子会话按父会话);快照没到之前为 null */
|
||||
export function useRunGroup(runId: string | undefined): AgentRunGroup | null {
|
||||
const run = useAgentRun(runId);
|
||||
const r = run.data?.run;
|
||||
const parentId = r?.trigger === "subagent" ? (r.parentRunId ?? undefined) : undefined;
|
||||
const parent = useAgentRun(parentId);
|
||||
if (!r) return null;
|
||||
if (parentId && !parent.data) return null;
|
||||
return runGroup(r, parent.data?.run ?? null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 页面会话列表的分栏:URL 的 ?tab= 优先;会话页再按当前会话所在分栏,快照没到时沿用上次的分栏(避免闪一下)。
|
||||
* 列表页没有 tab 就是「对话」,URL 与选中项一致。切换分栏只改 URL(replace,不进历史)。
|
||||
*/
|
||||
export function usePageListGroup(activeRun?: AgentRunGroup | null): [AgentRunGroup, (g: AgentRunGroup) => void] {
|
||||
const [params, setParams] = useSearchParams();
|
||||
const onRunPage = activeRun !== undefined;
|
||||
const group = resolveGroup({
|
||||
fromUrl: groupFromSearch(params),
|
||||
activeRun: onRunPage ? activeRun : null,
|
||||
remembered: onRunPage ? pageListMemory.group : null,
|
||||
});
|
||||
useEffect(() => {
|
||||
pageListMemory.group = group;
|
||||
}, [group]);
|
||||
const change = (g: AgentRunGroup) =>
|
||||
setParams(
|
||||
(prev) => {
|
||||
const next = new URLSearchParams(prev);
|
||||
next.set(TAB_PARAM, g);
|
||||
return next;
|
||||
},
|
||||
{ replace: true, preventScrollReset: true },
|
||||
);
|
||||
return [group, change];
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Bot, ChevronRight } from "lucide-react";
|
||||
import { type ReactNode, useLayoutEffect, useRef, useState } from "react";
|
||||
import { type ReactNode, useEffect, useLayoutEffect, useRef, useState } from "react";
|
||||
import { Link } from "react-router";
|
||||
import { type AgentRunGroup, type AgentRunView, TRIGGER_LABEL, useAgentRuns } from "~/api/agent.ts";
|
||||
import { CHEVRON_CLASS } from "~/components/agentui/disclosure.tsx";
|
||||
@@ -11,6 +11,7 @@ import { Tabs, TabsList, TabsTrigger } from "~/components/ui/tabs.tsx";
|
||||
import { relTime } from "~/components/workbench/release-fields.ts";
|
||||
import { cn } from "~/lib/utils.ts";
|
||||
import { contextLabel, RunStatusBadge } from "./agent-conversation.tsx";
|
||||
import { type ListMemory, newListMemory, revealScrollTop } from "./agent-nav.ts";
|
||||
import { shortGoalTitle, splitContextGoal } from "./agent-store.ts";
|
||||
|
||||
// 会话列表(AGUI-3):会话页左栏与抽屉的「历史」弹层共用。
|
||||
@@ -80,8 +81,8 @@ export function arrangeHistory(
|
||||
return { pinned, rows };
|
||||
}
|
||||
|
||||
/** 抽屉里跨开关保留的筛选与滚动位置(模块级,刷新页面才清) */
|
||||
const drawerMemory = { filters: EMPTY_FILTERS, scroll: 0 };
|
||||
/** 抽屉历史弹层跨开关保留的分栏、筛选与滚动位置(模块级,刷新页面才清) */
|
||||
export const drawerListMemory: ListMemory = newListMemory();
|
||||
|
||||
function RunRow({ r, current, trailing }: { r: AgentRunView; current: boolean; trailing?: ReactNode }) {
|
||||
const goal = splitContextGoal(r.goal);
|
||||
@@ -134,7 +135,12 @@ function Row({
|
||||
hrefFor?: (id: string) => string;
|
||||
onOpen?: (id: string) => void;
|
||||
}) {
|
||||
const [open, setOpen] = useState(row.children.some((c) => c.id === activeId));
|
||||
const childActive = row.children.some((c) => c.id === activeId);
|
||||
const [open, setOpen] = useState(childActive);
|
||||
// 列表不随换会话重挂载:跳到子会话(会话内链接、概况条)时把父会话展开,当前项可见
|
||||
useEffect(() => {
|
||||
if (childActive) setOpen(true);
|
||||
}, [childActive]);
|
||||
const item = (r: AgentRunView) => {
|
||||
const current = r.id === activeId;
|
||||
const cls = cn(
|
||||
@@ -142,12 +148,13 @@ function Row({
|
||||
current && "pill-selected",
|
||||
);
|
||||
return onOpen ? (
|
||||
<button type="button" className={cls} onClick={() => onOpen(r.id)}>
|
||||
<button type="button" data-run-id={r.id} className={cls} onClick={() => onOpen(r.id)}>
|
||||
<RunRow r={r} current={current} />
|
||||
</button>
|
||||
) : (
|
||||
<Link
|
||||
to={hrefFor?.(r.id) ?? `/console/agent/${r.id}`}
|
||||
data-run-id={r.id}
|
||||
aria-current={current ? "page" : undefined}
|
||||
className={cls}
|
||||
>
|
||||
@@ -183,33 +190,45 @@ function Row({
|
||||
}
|
||||
|
||||
/**
|
||||
* 会话列表:搜索 + 全部 / 等你处理 / 失败 + 按需加载的统一分页。
|
||||
* - hrefFor:会话页左栏,点一条跳到该会话页。
|
||||
* - onOpen:抽屉历史弹层,点一条在抽屉内打开;remember 时筛选与滚动位置下次打开时恢复。
|
||||
* - limit:历史弹层的默认每页条数,可调节并翻页查看更早的会话。
|
||||
* 会话列表:分栏 + 搜索 + 全部 / 等你处理 / 失败 + 按需加载的分页。
|
||||
* - hrefFor:列表页 / 会话页左栏,点一条跳到该会话页。
|
||||
* - onOpen:抽屉历史弹层,点一条在抽屉内打开。
|
||||
* - group / onGroupChange:分栏受控(页面把分栏放在 URL 里);不传时列表自己记。
|
||||
* - memory:筛选与各分栏滚动位置记在这里,列表重新挂载(下次打开弹层、列表页 ↔ 会话页)时恢复。
|
||||
* - activeId:当前会话,高亮;第一次出现在列表里且看不见时滚进可见区,是子会话时展开父会话。
|
||||
* - limit:历史弹层的默认每页条数。
|
||||
*/
|
||||
export function AgentSessionList({
|
||||
activeId,
|
||||
hrefFor,
|
||||
onOpen,
|
||||
remember = false,
|
||||
group: controlledGroup,
|
||||
onGroupChange,
|
||||
memory,
|
||||
limit,
|
||||
className,
|
||||
}: {
|
||||
activeId?: string;
|
||||
hrefFor?: (id: string) => string;
|
||||
onOpen?: (id: string) => void;
|
||||
remember?: boolean;
|
||||
group?: AgentRunGroup;
|
||||
onGroupChange?: (group: AgentRunGroup) => void;
|
||||
memory?: ListMemory;
|
||||
limit?: number;
|
||||
className?: string;
|
||||
}) {
|
||||
const [f, setF] = useState<HistoryFilters>(remember ? drawerMemory.filters : EMPTY_FILTERS);
|
||||
const set = (patch: Partial<HistoryFilters>) =>
|
||||
setF((prev) => {
|
||||
const [own, setOwn] = useState<HistoryFilters>(
|
||||
memory ? { q: memory.q, view: memory.view, group: memory.group } : EMPTY_FILTERS,
|
||||
);
|
||||
const f: HistoryFilters = controlledGroup ? { ...own, group: controlledGroup } : own;
|
||||
const set = (patch: Partial<HistoryFilters>) => {
|
||||
if (patch.group && controlledGroup) onGroupChange?.(patch.group);
|
||||
setOwn((prev) => {
|
||||
const next = { ...prev, ...patch };
|
||||
if (remember) drawerMemory.filters = next;
|
||||
if (memory) Object.assign(memory, { q: next.q, view: next.view, group: next.group });
|
||||
return next;
|
||||
});
|
||||
};
|
||||
const runs = useAgentRuns(undefined, undefined, f.group);
|
||||
// 等你处理的会话可能是很早以前建的:单独按 waiting 取一页,保证置顶
|
||||
const waiting = useAgentRuns("waiting", undefined, f.group);
|
||||
@@ -231,12 +250,29 @@ export function AgentSessionList({
|
||||
const rows = f.view === "all" ? visible.filter((row) => !awaitingUser(row.run)) : visible;
|
||||
const filtered = !!f.q.trim() || f.view !== "all";
|
||||
|
||||
// 滚动:每个分栏第一次有内容时恢复记住的位置,再把当前会话滚进可见区(各只做一次,之后不跟着用户的滚动抢位置)
|
||||
const scroller = useRef<HTMLDivElement>(null);
|
||||
const restored = useRef(!remember);
|
||||
const restoredGroup = useRef<AgentRunGroup | null>(null);
|
||||
const revealed = useRef<string | null>(null);
|
||||
const ready = !runs.isPending && visible.length > 0;
|
||||
useLayoutEffect(() => {
|
||||
if (restored.current || !rows.length || !scroller.current) return;
|
||||
restored.current = true;
|
||||
scroller.current.scrollTop = drawerMemory.scroll;
|
||||
const el = scroller.current;
|
||||
if (!el || !ready) return;
|
||||
if (restoredGroup.current !== f.group) {
|
||||
restoredGroup.current = f.group;
|
||||
el.scrollTop = memory?.scroll[f.group] ?? 0;
|
||||
}
|
||||
const key = `${f.group}|${activeId ?? ""}`;
|
||||
if (!activeId || revealed.current === key) return;
|
||||
const item = el.querySelector<HTMLElement>(`[data-run-id="${activeId}"]`);
|
||||
if (!item) return;
|
||||
revealed.current = key;
|
||||
const top = item.getBoundingClientRect().top - el.getBoundingClientRect().top + el.scrollTop;
|
||||
const next = revealScrollTop(
|
||||
{ top, height: item.offsetHeight },
|
||||
{ scrollTop: el.scrollTop, height: el.clientHeight },
|
||||
);
|
||||
if (next !== null) el.scrollTop = next;
|
||||
});
|
||||
const rowProps = { activeId, hrefFor, onOpen };
|
||||
return (
|
||||
@@ -279,7 +315,8 @@ export function AgentSessionList({
|
||||
<div
|
||||
ref={scroller}
|
||||
onScroll={(e) => {
|
||||
if (remember) drawerMemory.scroll = e.currentTarget.scrollTop;
|
||||
// 换栏后还没恢复位置前的滚动(内容变短被浏览器夹回)不记
|
||||
if (memory && restoredGroup.current === f.group) memory.scroll[f.group] = e.currentTarget.scrollTop;
|
||||
}}
|
||||
className="min-h-0 flex-1 overflow-y-auto"
|
||||
>
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
import { ArrowLeft } from "lucide-react";
|
||||
import { Link, useParams } from "react-router";
|
||||
import { agentMockEnabled } from "~/api/agent.ts";
|
||||
import {
|
||||
agentListHref,
|
||||
agentRunHref,
|
||||
pageListMemory,
|
||||
usePageListGroup,
|
||||
useRunGroup,
|
||||
} from "~/components/agent/agent-nav.ts";
|
||||
import { AgentRunPanel } from "~/components/agent/agent-run-panel.tsx";
|
||||
import { AgentSessionList } from "~/components/agent/agent-session-list.tsx";
|
||||
import { Button } from "~/components/ui/button.tsx";
|
||||
@@ -10,17 +17,25 @@ import { Button } from "~/components/ui/button.tsx";
|
||||
export default function AgentRunPage() {
|
||||
const { runId = "" } = useParams();
|
||||
const mock = agentMockEnabled();
|
||||
const suffix = mock ? "?mock=1" : "";
|
||||
// 左栏分栏跟随当前会话(巡检会话及其子会话在「自动巡检」),也可以手动切换(记在 URL)
|
||||
const [group, setGroup] = usePageListGroup(useRunGroup(runId));
|
||||
return (
|
||||
<>
|
||||
<h1 className="sr-only">智能体会话</h1>
|
||||
<div className="grid grid-cols-1 gap-4 xl:grid-cols-[minmax(0,17rem)_minmax(0,1fr)]">
|
||||
<aside className="pg-slab hidden rounded-xl p-3 xl:block xl:h-[calc(100dvh-7rem)] xl:overflow-hidden">
|
||||
<AgentSessionList activeId={runId} hrefFor={(id) => `/console/agent/${id}${suffix}`} className="h-full" />
|
||||
<AgentSessionList
|
||||
activeId={runId}
|
||||
hrefFor={(id) => agentRunHref(id, mock)}
|
||||
group={group}
|
||||
onGroupChange={setGroup}
|
||||
memory={pageListMemory}
|
||||
className="h-full"
|
||||
/>
|
||||
</aside>
|
||||
<div className="pg-slab flex h-[calc(100dvh-7rem)] min-h-[28rem] flex-col gap-2 rounded-xl p-3 md:p-4">
|
||||
<Button asChild variant="ghost" size="sm" className="-mt-1 -ml-2 self-start text-muted-foreground xl:hidden">
|
||||
<Link to={`/console/agent${suffix}`}>
|
||||
<Link to={agentListHref(group, mock)}>
|
||||
<ArrowLeft />
|
||||
全部会话
|
||||
</Link>
|
||||
|
||||
@@ -2,6 +2,7 @@ import { FlaskConical } from "lucide-react";
|
||||
import { useNavigate } from "react-router";
|
||||
import { toast } from "sonner";
|
||||
import { agentMockEnabled, useCreateAgentRun } from "~/api/agent.ts";
|
||||
import { agentRunHref, pageListMemory, usePageListGroup } from "~/components/agent/agent-nav.ts";
|
||||
import { GoalComposer } from "~/components/agent/agent-run-panel.tsx";
|
||||
import { AgentSessionList } from "~/components/agent/agent-session-list.tsx";
|
||||
import { quickGoalsFor } from "~/components/agent/quick-goals.ts";
|
||||
@@ -14,6 +15,7 @@ export default function AgentPage() {
|
||||
const create = useCreateAgentRun();
|
||||
const navigate = useNavigate();
|
||||
const mock = agentMockEnabled();
|
||||
const [group, setGroup] = usePageListGroup();
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -29,7 +31,13 @@ export default function AgentPage() {
|
||||
) : null}
|
||||
<div className="grid grid-cols-1 gap-4 lg:grid-cols-[minmax(0,20rem)_minmax(0,1fr)] lg:items-start">
|
||||
<div className="pg-slab order-2 rounded-xl p-3 lg:order-1 lg:h-[calc(100dvh-12rem)] lg:overflow-hidden">
|
||||
<AgentSessionList hrefFor={(id) => `/console/agent/${id}${mock ? "?mock=1" : ""}`} className="h-full" />
|
||||
<AgentSessionList
|
||||
hrefFor={(id) => agentRunHref(id, mock)}
|
||||
group={group}
|
||||
onGroupChange={setGroup}
|
||||
memory={pageListMemory}
|
||||
className="h-full"
|
||||
/>
|
||||
</div>
|
||||
<div className="pg-slab order-1 mx-auto w-full max-w-[88ch] rounded-xl p-4 lg:order-2">
|
||||
<GoalComposer
|
||||
@@ -39,7 +47,7 @@ export default function AgentPage() {
|
||||
create.mutate(
|
||||
{ goal, ...opts },
|
||||
{
|
||||
onSuccess: (run) => navigate(`/console/agent/${run.id}${mock ? "?mock=1" : ""}`),
|
||||
onSuccess: (run) => navigate(agentRunHref(run.id, mock)),
|
||||
onError: (e) => toast.error(errorMessage(e)),
|
||||
},
|
||||
)
|
||||
|
||||
Reference in new issue
Block a user