Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion web/components/chat/PeriTaskList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ export const PeriTaskList = memo(function PeriTaskList({
)}

{!collapsed && (
<div id={BODY_ID} className="max-h-64 overflow-y-auto border-t border-border/50">
<div id={BODY_ID} className="max-h-64 overflow-y-auto overscroll-contain border-t border-border/50">
{!loaded ? (
// 加载态:Session Doc 快照尚未同步,任务列表不可用
<div className="px-3 py-2 text-[11px] text-text-muted" role="status">
Expand Down
2 changes: 1 addition & 1 deletion web/components/chat/SubAgentPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ export const SubAgentPanel = memo(function SubAgentPanel({ entries }: SubAgentPa
</button>
</CollapsibleTrigger>
<CollapsibleContent>
<div className="border-t border-border/70 bg-surface-1/30 px-3 py-3">
<div className="max-h-64 overflow-y-auto overscroll-contain border-t border-border/70 bg-surface-1/30 px-3 py-3">
<SubAgentTimeline entries={entries} />
</div>
</CollapsibleContent>
Expand Down
2 changes: 1 addition & 1 deletion web/components/chat/TodoChanges.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ export function TodoChanges({ changes }: TodoChangesProps) {
if (changes.length === 0) return null;

return (
<div className="mx-3 mb-2.5 mt-1 pt-1.5">
<div className="mx-3 mb-2.5 mt-1 max-h-64 overflow-y-auto overscroll-contain pt-1.5">
{changes.map((change) => {
const { Icon, iconClassName, labelClassName, itemClassName } = CHANGE_STYLES[change.kind];
const text =
Expand Down
2 changes: 1 addition & 1 deletion web/components/chat/TodoPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ export function TodoPanel({ todos, embedded = false, title }: TodoPanelProps) {

{/* Todo 列表 */}
{!collapsed && (
<div className="border-t border-border/50 px-3 py-1 divide-y divide-border/30">
<div className="max-h-64 overflow-y-auto overscroll-contain border-t border-border/50 px-3 py-1 divide-y divide-border/30">
{todos.map((todo) => (
<div key={todo.content} className="flex items-start gap-2 py-1">
{todo.status === "completed" ? (
Expand Down
2 changes: 1 addition & 1 deletion web/components/chat/ToolCallRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ export function ToolCallRow({ tool, envId }: ToolCallRowProps) {

{/* 子 agent 嵌套面板(保留) */}
{hasSubEntries && (
<div className="max-h-64 overflow-y-auto mx-1 mt-1 mb-1 rounded-md border border-border/40 bg-surface-0/50">
<div className="mx-1 mt-1 mb-1 rounded-md border border-border/40 bg-surface-0/50">
<div className="px-2 py-2">
<SubAgentPanel entries={tool.subEntries!} />
</div>
Expand Down
15 changes: 12 additions & 3 deletions web/components/chat/chat-status-panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,10 @@ function StatusTabButton({
function TodoRows({ todos }: { todos: TodoItem[] }) {
const { t } = useTranslation("components");
return (
<div className="chat-status-list" role="tabpanel">
<div
className="chat-status-list max-h-[min(16rem,35vh)] overflow-y-auto overscroll-contain [scrollbar-gutter:stable]"
role="tabpanel"
>
{todos.map((todo) => {
const Icon = todo.status === "completed" ? CheckCircle2 : todo.status === "in_progress" ? Clock3 : Circle;
return (
Expand Down Expand Up @@ -192,7 +195,10 @@ function TaskRows({
const { t } = useTranslation("components");
if (!loaded) return <p className="chat-status-note">{t("periTask.loading")}</p>;
return (
<div className="chat-status-list" role="tabpanel">
<div
className="chat-status-list max-h-[min(16rem,35vh)] overflow-y-auto overscroll-contain [scrollbar-gutter:stable]"
role="tabpanel"
>
{reconnecting && <p className="chat-status-note">{t("periTask.reconnecting")}</p>}
{tasks.map((task) => {
const Icon =
Expand Down Expand Up @@ -225,7 +231,10 @@ function TaskRows({
function ChangeRows({ files }: { files: ChangedFile[] }) {
const { t } = useTranslation("components");
return (
<div className="chat-status-list" role="tabpanel">
<div
className="chat-status-list max-h-[min(16rem,35vh)] overflow-y-auto overscroll-contain [scrollbar-gutter:stable]"
role="tabpanel"
>
{files.map((file) => (
<button
key={file.path}
Expand Down
50 changes: 49 additions & 1 deletion web/src/__tests__/chat-status-panel.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,26 @@
import { describe, expect, test } from "bun:test";
import { createElement } from "react";
import { renderToStaticMarkup } from "react-dom/server";
import { I18nextProvider } from "react-i18next";

import { fileNameFromPath } from "../../components/chat/chat-status-panel";
import { ChatStatusPanel, fileNameFromPath } from "../../components/chat/chat-status-panel";
import i18n from "../i18n";

function renderPanel(props: Partial<Parameters<typeof ChatStatusPanel>[0]> = {}): string {
return renderToStaticMarkup(
createElement(
I18nextProvider,
{ i18n },
createElement(ChatStatusPanel, {
todos: [],
tasks: [],
tasksLoaded: true,
changedFiles: [],
...props,
}),
),
);
}

describe("ChatStatusPanel 文件名称", () => {
// Changes 只展示文件名,完整绝对路径继续由状态面板保留用于预览定位。
Expand All @@ -17,4 +37,32 @@ describe("ChatStatusPanel 文件名称", () => {
test("忽略路径末尾斜杠", () => {
expect(fileNameFromPath("/opt/app/project/")).toBe("project");
});

// Todo、Subtasks 与 Changes 共用同一限高滚动容器,长列表不得继续抬高输入区。
test("三个状态列表都限制高度并启用内部滚动", () => {
const todoMarkup = renderPanel({ todos: [{ content: "检查待办", status: "pending" }] });
const taskMarkup = renderPanel({
tasks: [
{
taskId: "task-1",
title: "检查子任务",
kind: "subagent",
taskSubtype: null,
summary: null,
status: "running",
turnId: null,
isBackground: false,
startedAt: "2026-09-09T00:00:00.000Z",
completedAt: null,
updatedAt: "2026-09-09T00:00:00.000Z",
detailAvailability: "unavailable",
},
],
});
const changesMarkup = renderPanel({ changedFiles: [{ path: "/workspace/src/app.ts", type: "edit" }] });

for (const markup of [todoMarkup, taskMarkup, changesMarkup]) {
expect(markup).toContain("max-h-[min(16rem,35vh)] overflow-y-auto overscroll-contain [scrollbar-gutter:stable]");
}
});
});
20 changes: 20 additions & 0 deletions web/src/__tests__/tool-call-row-ssr.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,26 @@ describe("ToolCallRow 服务端渲染", () => {
expect(disabled).not.toContain('class="chat-tool-call-row-details-button"');
});

// TodoWrite 的变更记录必须在工具卡片内部限高滚动,避免长变更列表撑满会话。
test("TodoWrite 变更列表限制高度并内部滚动", () => {
const html = renderTool(
tool({
title: "TodoWrite",
kind: "unknown",
todoChanges: [
{
id: "todo-change-1",
kind: "added",
todo: { content: "检查长列表布局", status: "pending" },
},
],
}),
);

expect(html).toContain("max-h-64 overflow-y-auto overscroll-contain");
expect(html).toContain("检查长列表布局");
});

// 等待确认工具只保留状态,权限选项统一由输入框上方交互区域承载。
test("等待确认工具不重复渲染权限操作", () => {
const html = renderTool(
Expand Down
Loading