fix(web): 手机端会话详情折叠会话列表,避免会话记录被挤压看不全

小屏下会话列表固定占 10~15rem,会话记录只剩标题和输入框。改为 <lg
默认折叠列表(标题栏显示当前会话名+其它会话未读合计,点开选完自动
收起),并把高度预留从 13rem 收到 10rem。桌面端两栏布局不受影响。
This commit is contained in:
Autumn-27 committed 2026-08-29 09:08:10 -04:00
1 parent f3cfbafa8f
commit 5f0d91bbb3
1 file changed
+78 -8
@@ -5,6 +5,7 @@ import * as React from "react";
import {
ArrowUpIcon,
BrainIcon,
ChevronDownIcon,
CircleCheckIcon,
CircleSlashIcon,
CircleXIcon,
@@ -473,6 +474,10 @@ function WorkerAssetBadge({ assets }: { assets: IntentAsset[] }) {
export function SessionsTab({ taskId }: { taskId: string }) {
const [activeId, setActiveId] = React.useState(MAIN_ID);
// 手机端(<lg)会话列表默认折叠:屏幕高度本就紧张,列表若固定占掉 10~15rem,
// 下方的会话记录会被挤到只剩标题与输入框。折叠后记录区拿到几乎全部高度,
// 点标题栏可展开选会话,选完自动收起。桌面端不受影响(lg 起始终展开)。
const [listOpen, setListOpen] = React.useState(false);
// Per-session lazily-loaded caches, keyed by session_key (main | plan | intent:<id>).
const [store, setStore] = React.useState<SessionStore>({});
// Worker sessions derived from exploration intents (paged past the old 300 cap).
@@ -1130,6 +1135,13 @@ export function SessionsTab({ taskId }: { taskId: string }) {
const isSystem = active.role === "system";
const activeKey = keyForSession(active);
const activeState = store[activeKey];
// 折叠态(手机端)标题栏要替代整张列表:显示当前会话名 + 其它会话的未读合计,
// 否则收起后既不知道自己在看哪个会话,也看不到别处有新消息。
const activeDisplayTitle = (active.role === "worker" ? sessionMeta.get(active.id)?.title : "") || active.title;
const hiddenUnread = React.useMemo(
() => Object.entries(store).reduce((sum, [key, s]) => (key === activeKey ? sum : sum + s.unread), 0),
[store, activeKey],
);
// Keep the SSE dispatcher's notion of the active session current, and lazily load
// + clear unread whenever the active session changes.
@@ -1350,12 +1362,44 @@ export function SessionsTab({ taskId }: { taskId: string }) {
return (
<TooltipProvider delayDuration={300}>
<div className="grid h-[calc(100svh-13rem)] min-h-0 grid-cols-1 grid-rows-[minmax(10rem,15rem)_minmax(0,1fr)] gap-4 lg:grid-cols-[18rem_1fr] lg:grid-rows-[minmax(0,1fr)]">
{/* 高度预留:页面头部(标题行 + 目标 + Tabs ≈ 7.5rem)+ 内容内边距。手机端 p-4、
桌面端 lg:p-6,且桌面还要留出滚动余量,所以两档分别预留 10rem / 13rem —— 手机端
沿用 13rem 会白白吃掉 3rem 的记录高度。 */}
<div
className={cn(
"grid h-[calc(100svh-10rem)] min-h-0 grid-cols-1 gap-4",
listOpen ? "grid-rows-[minmax(0,40svh)_minmax(0,1fr)]" : "grid-rows-[auto_minmax(0,1fr)]",
"lg:h-[calc(100svh-13rem)] lg:grid-cols-[18rem_1fr] lg:grid-rows-[minmax(0,1fr)]",
)}
>
{/* Left: session list */}
<div className="flex flex-col overflow-hidden rounded-lg border bg-card">
<div className="flex min-h-0 flex-col overflow-hidden rounded-lg border bg-card">
<div className="border-b px-3 py-2">
<div className="flex items-center justify-between">
<div className="text-xs font-medium text-muted-foreground">会话列表</div>
<div className="flex items-center justify-between gap-2">
<button
type="button"
onClick={() => setListOpen((open) => !open)}
aria-expanded={listOpen}
aria-controls="session-list-panel"
className="flex min-w-0 flex-1 items-center gap-1 text-left text-xs font-medium text-muted-foreground lg:pointer-events-none"
>
<ChevronDownIcon
className={cn("size-3.5 shrink-0 transition-transform lg:hidden", !listOpen && "-rotate-90")}
/>
<span className="shrink-0">会话列表</span>
{!listOpen && (
<>
<span className="min-w-0 truncate text-foreground lg:hidden" title={activeDisplayTitle}>
· {activeDisplayTitle}
</span>
{hiddenUnread > 0 && (
<span className="inline-flex min-w-4 shrink-0 items-center justify-center rounded-full bg-blue-500/15 px-1 text-[10px] font-medium tabular-nums text-blue-600 lg:hidden dark:text-blue-400">
{hiddenUnread > 99 ? "99+" : hiddenUnread}
</span>
)}
</>
)}
</button>
{!MOCK && (
<span
className={cn(
@@ -1399,7 +1443,14 @@ export function SessionsTab({ taskId }: { taskId: string }) {
</Tooltip>
)}
</div>
<ScrollArea type="auto" className="min-h-0 flex-1 [&_[data-slot=scroll-area-viewport]>div]:block!">
<ScrollArea
id="session-list-panel"
type="auto"
className={cn(
"min-h-0 flex-1 [&_[data-slot=scroll-area-viewport]>div]:block!",
!listOpen && "max-lg:hidden",
)}
>
<div className="flex w-full flex-col gap-3 p-2">
{(["mainagent", "planner", "system", "worker"] as const).map((role) => {
const items = grouped[role];
@@ -1421,7 +1472,10 @@ export function SessionsTab({ taskId }: { taskId: string }) {
displayTitle={meta?.title ?? s.title}
hasPending={hasPendingForSession(s)}
unread={store[keyForSession(s)]?.unread}
onClick={() => setActiveId(s.id)}
onClick={() => {
setActiveId(s.id);
setListOpen(false); // 手机端选完即收起,把高度还给会话记录
}}
controlling={controllingIntent === s.intent_id}
deleted={meta?.deleted}
onPause={() => void controlWorker(s, "pause")}
@@ -1459,7 +1513,7 @@ export function SessionsTab({ taskId }: { taskId: string }) {
{/* Right: transcript */}
<div className="flex min-w-0 flex-col overflow-hidden rounded-lg border bg-card">
<div className="flex min-w-0 flex-wrap items-center gap-2 border-b px-4 py-2.5">
<div className="flex min-w-0 flex-wrap items-center gap-x-2 gap-y-1 border-b px-3 py-2 sm:px-4 sm:py-2.5">
{(() => {
const isWorker = active.role === "worker";
const meta = isWorker ? sessionMeta.get(active.id) : undefined;
@@ -1518,8 +1572,24 @@ export function SessionsTab({ taskId }: { taskId: string }) {
<div className="ml-auto flex min-w-0 max-w-full items-center justify-end gap-x-3 gap-y-1 text-xs text-muted-foreground max-sm:w-full max-sm:flex-wrap">
{tokenTotal.any && (
<Tooltip>
{/* 手机端用短标签(入/缓/出):长标签会把这一行撑成两行,进一步压缩记录区。 */}
<TooltipTrigger asChild>
<TokenMetrics input={tokenTotal.i} cache={tokenTotal.cr} output={tokenTotal.o} labels="long" />
<span className="inline-flex min-w-0 items-center">
<TokenMetrics
input={tokenTotal.i}
cache={tokenTotal.cr}
output={tokenTotal.o}
labels="short"
className="sm:hidden"
/>
<TokenMetrics
input={tokenTotal.i}
cache={tokenTotal.cr}
output={tokenTotal.o}
labels="long"
className="max-sm:hidden"
/>
</span>
</TooltipTrigger>
<TooltipContent>
输入 {activeTokens.input_tokens.toLocaleString()} · 输出{" "}