diff --git a/apps/desktop/src/components/TodoDock.tsx b/apps/desktop/src/components/TodoDock.tsx
index 63cfc561fd..905316d361 100644
--- a/apps/desktop/src/components/TodoDock.tsx
+++ b/apps/desktop/src/components/TodoDock.tsx
@@ -6,8 +6,6 @@ import { IconCheck, IconChevronDown, IconChevronUp } from "./icons";
import { useSessionTodosRecovery } from "../features/chat/todos/useSessionTodosRecovery";
import { useAppStore } from "../stores/app-store";
-const VISIBLE_LIMIT = 8;
-
function statusSymbol(status: TodoStatus): string {
switch (status) {
case "in_progress":
@@ -38,8 +36,6 @@ export function TodoDock({ sessionId }: { sessionId: string }) {
const finished = snapshot.todos.every(
(todo) => todo.status === "completed" || todo.status === "cancelled",
);
- const visible = snapshot.todos.slice(0, VISIBLE_LIMIT);
- const remaining = Math.max(0, snapshot.todos.length - VISIBLE_LIMIT);
const statusLabel = allCancelled
? t("chat.todo.status.cancelled")
: finished
@@ -65,15 +61,18 @@ export function TodoDock({ sessionId }: { sessionId: string }) {
-
- {visible.map((todo, index) => (
+ {/* The expanded dock lists every row (#1319): the list scrolls
+ inside its own box instead of truncating to a static "N more"
+ line, and a keyboard user can reach that scrollport only while
+ the disclosure is open. */}
+
+ {snapshot.todos.map((todo, index) => (
))}
- {remaining > 0 ? (
-
- {t("chat.todo.more", { count: remaining })}
-
- ) : null}
diff --git a/apps/desktop/src/styles/composer.css b/apps/desktop/src/styles/composer.css
index d707048540..51ac04000f 100644
--- a/apps/desktop/src/styles/composer.css
+++ b/apps/desktop/src/styles/composer.css
@@ -833,6 +833,22 @@
padding: 0 10px 10px;
}
+/* The expanded dock lists every row and scrolls inside its own box (#1319):
+ the Composer keeps its footprint, and reaching the list's end never scrolls
+ the transcript behind it. */
+.todo-dock.is-expanded .todo-dock-list {
+ max-height: min(280px, 30dvh);
+ overflow-y: auto;
+ overscroll-behavior: contain;
+}
+
+/* The scrollport is keyboard reachable while it is on screen; the inset ring
+ stays inside the clip layer that carries the disclosure animation. */
+.todo-dock-list:focus-visible {
+ outline: 2px solid color-mix(in oklab, var(--ds-accent) 72%, transparent);
+ outline-offset: -2px;
+}
+
.todo-dock-row {
display: flex;
min-width: 0;
@@ -876,11 +892,6 @@
white-space: nowrap;
}
-.todo-dock-more {
- color: var(--ds-text-muted);
- font-size: var(--text-2xs);
-}
-
@media (prefers-reduced-motion: reduce) {
.composer-mode-chip[data-planning="true"] svg {
animation: none;
diff --git a/apps/desktop/test/todo-dock-rendering.test.mjs b/apps/desktop/test/todo-dock-rendering.test.mjs
index 66d91d4557..adaae6d55a 100644
--- a/apps/desktop/test/todo-dock-rendering.test.mjs
+++ b/apps/desktop/test/todo-dock-rendering.test.mjs
@@ -54,7 +54,11 @@ test("TodoDock renders bounded session progress and cancelled state", async () =
assert.match(html, /class="todo-dock-content" aria-hidden="true"/);
assert.match(html, /class="todo-dock-content" aria-hidden="true">
/);
assert.match(html, /extra-0/);
- assert.match(html, /4 more items/);
+ // The expanded dock lists every row (#1319): the whole list is mounted, and
+ // the dock scrolls inside instead of printing a static overflow line.
+ assert.equal((html.match(/role="listitem"/g) ?? []).length, 12, "every ordered row renders");
+ assert.doesNotMatch(html, /more items/);
+ assert.match(html, /class="todo-dock-list" role="list" tabindex="-1"/);
assert.match(html, /lucide-check/);
Object.assign(useAppStore.getInitialState(), {
@@ -83,5 +87,13 @@ test("TodoDock uses a quiet disclosure animation and state surfaces", async () =
assert.match(css, /\.todo-dock-content\s*\{[\s\S]*?grid-template-rows:\s*0fr;[\s\S]*?opacity:\s*0;/);
assert.match(css, /\.todo-dock\.is-expanded \.todo-dock-content\s*\{[\s\S]*?grid-template-rows:\s*1fr;/);
assert.match(css, /\.todo-dock-row-completed\s*\{[\s\S]*?var\(--ds-success\)/);
+ // The expanded list is the scrollport (#1319): bounded height, internal
+ // scroll, no chaining into the transcript, and reachable from the keyboard.
+ assert.match(
+ css,
+ /\.todo-dock\.is-expanded \.todo-dock-list\s*\{[\s\S]*?max-height:[\s\S]*?overflow-y:\s*auto;[\s\S]*?overscroll-behavior:\s*contain;/,
+ );
+ assert.match(css, /\.todo-dock-list:focus-visible\s*\{[\s\S]*?outline:/);
+ assert.doesNotMatch(css, /\.todo-dock-more\s*\{/);
assert.match(css, /@media \(prefers-reduced-motion: reduce\)\s*\{[\s\S]*?\.todo-dock-content/);
});
diff --git a/docs/adr/0312-session-scoped-todo-checklist.md b/docs/adr/0312-session-scoped-todo-checklist.md
index 2ddaca72b8..5dfec9635e 100644
--- a/docs/adr/0312-session-scoped-todo-checklist.md
+++ b/docs/adr/0312-session-scoped-todo-checklist.md
@@ -27,8 +27,10 @@ forwards the notification through the existing IPC bridge, and the renderer
keeps snapshots keyed by session id while rejecting stale revisions.
The Composer TodoDock is a non-focusing, session-aware presentation surface.
-It shows bounded progress and at most eight ordered rows. Remote RACP sessions
-remain local-only for this vertical slice because RACP v1 has no Todo snapshot
+It shows bounded progress and the complete ordered list: the expanded dock
+scrolls inside its own box instead of truncating, so a long checklist stays
+readable without growing the Composer stack. Remote RACP sessions remain
+local-only for this vertical slice because RACP v1 has no Todo snapshot
operation; the renderer skips local recovery for those session ids rather than
reading the local database.
@@ -46,4 +48,5 @@ reading the local database.
Host-core tests cover migration, validation, transaction rollback, restart,
revision ordering, fork isolation, cascade deletion, and RPC authorization.
Renderer type checks and TodoDock interaction tests cover revision filtering,
-remote-session degradation, session switching, expansion, and bounded display.
+remote-session degradation, session switching, expansion, and full-list display
+through the dock's own scrollport.
diff --git a/docs/spec/04-ux/08-component-spec.md b/docs/spec/04-ux/08-component-spec.md
index a83fc27084..b2646cc9c0 100644
--- a/docs/spec/04-ux/08-component-spec.md
+++ b/docs/spec/04-ux/08-component-spec.md
@@ -2883,16 +2883,20 @@ reasoning-level control.
- The collapsed header shows completed/active progress and the current
`in_progress` content. A checklist whose items are all cancelled has a clear
cancelled label instead of a misleading `0/0 completed` count.
-- The disclosure is keyboard accessible, does not take focus on updates, resets
- closed when the active session changes, and shows at most eight ordered rows.
- The list stays mounted while collapsed so opening and closing can animate with
- a bounded height/opacity transition; collapsed content is `aria-hidden` and
- reduced-motion users receive an immediate state change. A collapsed dock
- reserves only its header row: the list's inset is clipped, never laid out
- below the header. Completed rows use a success-tinted tile with a check
- icon, in-progress rows use the accent tint, and cancelled rows are muted;
- each status symbol has a localized accessible
- name and each row renders plain text.
+- The disclosure is keyboard accessible, does not take focus on updates, and
+ resets closed when the active session changes. The expanded dock lists every
+ ordered row and scrolls inside its own box (`max-height: min(280px, 30dvh)`,
+ `overflow-y: auto`, `overscroll-behavior: contain`), so a long checklist stays
+ readable without growing the Composer stack and reaching the list's end never
+ scrolls the transcript behind it; that scrollport is keyboard reachable only
+ while the disclosure is open. The list stays mounted while collapsed so
+ opening and closing can animate with a bounded height/opacity transition;
+ collapsed content is `aria-hidden` and reduced-motion users receive an
+ immediate state change. A collapsed dock reserves only its header row: the
+ list's inset is clipped, never laid out below the header.
+ Completed rows use a success-tinted tile with a check icon, in-progress rows
+ use the accent tint, and cancelled rows are muted. Each status symbol has a
+ localized accessible name, and each row renders plain text.
- Renderer snapshots are keyed by session id. A `todos.changed` event with an
older or equal revision is ignored. Session activation and host recovery
re-read the authoritative snapshot, including already cached checklists; a
diff --git a/docs/spec/06-delivery/04-e2e-test-plan.md b/docs/spec/06-delivery/04-e2e-test-plan.md
index e7161ece62..be11b14972 100644
--- a/docs/spec/06-delivery/04-e2e-test-plan.md
+++ b/docs/spec/06-delivery/04-e2e-test-plan.md
@@ -9388,8 +9388,10 @@ must keep splitting are covered by `markdown-blocks.test.mjs`.
- **Steps**: Start a multi-step Agent turn that calls `TodoWrite` with ordered
pending and `in_progress` items. Observe the TodoDock above the Composer,
expand it, switch sessions, and confirm the checklist stays session-scoped.
- Complete and cancel items, confirm the bounded eight-row display and the
- all-cancelled label, then clear the checklist and reload/restart the host.
+ Expand a checklist longer than the dock's height cap: every row must render
+ and the list must scroll inside the dock without growing the Composer stack.
+ Complete and cancel items, confirm the all-cancelled label, then clear the
+ checklist and reload/restart the host.
Deliver an out-of-order older `todos.changed` event and confirm it cannot
replace the newer snapshot. Exercise invalid payload, Plan/Goal, delegated,
and remote-session paths.
@@ -9397,6 +9399,7 @@ must keep splitting are covered by `markdown-blocks.test.mjs`.
advances revision, including clear, and emits one committed `todos.changed`
snapshot. Invalid or unauthorized writes do not mutate or emit. TodoDock
renders plain text, does not take focus, resets expansion on session changes,
+ lists every ordered row with the dock's own list as the scrollport,
keeps a collapsed dock to its header height,
rejects stale events, and skips local recovery for `remote:` sessions because
RACP v1 has no Todo snapshot operation.
@@ -9410,8 +9413,9 @@ must keep splitting are covered by `markdown-blocks.test.mjs`.
the production renderer, and a real host/SQLite profile. Only the external
model stream and preload transport are fixtures; no live provider or user
profile is used. The scenario includes Unicode truncation with warning replay,
- single-active-item normalization, a
- failed initial read followed by host recovery without changing sessions,
+ single-active-item normalization, full-list rendering with the dock's
+ internal scroll, a failed initial read followed by host recovery without
+ changing sessions,
cached-snapshot reconciliation, and stale-event rejection. Runtime
`runtime-todos.test.ts` exercises Agent tool validation, overlong content
normalization, and continuation through a deterministic provider.
diff --git a/docs/zh-CN/spec/04-ux/08-component-spec.md b/docs/zh-CN/spec/04-ux/08-component-spec.md
index 61b4eb7c51..c6c58f33ab 100644
--- a/docs/zh-CN/spec/04-ux/08-component-spec.md
+++ b/docs/zh-CN/spec/04-ux/08-component-spec.md
@@ -2013,7 +2013,8 @@ MainChat 底部的输入区域,用于撰写和发送提示。支持多行输
- 当活动会话拥有非空的主机清单时,TodoDock 位于 Composer 栈中并显示在 Plan/Goal 批准表面上方;空清单不占布局空间。
- 收起标题显示完成/活动进度和当前 `in_progress` 内容;全部取消时显示明确的取消状态,不显示误导性的 `0/0 completed`。
-列表在收起时仍保持挂载,但收起状态只占标题行高度(列表内边距被裁剪,不会在标题下方留白),通过有界的高度/透明度过渡播放展开和收起动画;
+- 展开后列出全部条目,并在自身范围内滚动(`max-height: min(280px, 30dvh)`、`overflow-y: auto`、`overscroll-behavior: contain`):长清单因此保持可读,既不撑高 Composer 栈,滚到底也不会带动背后的转写;该滚动区仅在展开时可被键盘聚焦。
+- 列表在收起时仍保持挂载,但收起状态只占标题行高度(列表内边距被裁剪,不会在标题下方留白),通过有界的高度/透明度过渡播放展开和收起动画。
- 渲染器按 session id 保存快照并忽略旧或相同 revision。激活会话和主机恢复时重新读取权威快照,包括已缓存清单;首次读取失败不能导致主机恢复后清单持续隐藏。`remote:` 和 `native-pi:` 会话不拥有本地 Desktop 清单,跳过本地 `todos.get` 恢复;远程 Todo 对等能力留待新增 RACP 契约。
### 11.4 状态
diff --git a/docs/zh-CN/spec/06-delivery/04-e2e-test-plan.md b/docs/zh-CN/spec/06-delivery/04-e2e-test-plan.md
index fad21938fe..10a1e5641b 100644
--- a/docs/zh-CN/spec/06-delivery/04-e2e-test-plan.md
+++ b/docs/zh-CN/spec/06-delivery/04-e2e-test-plan.md
@@ -5555,12 +5555,12 @@ eleven-tool-round desktop paths are verified by
**E2E-CHAT-session-todo-checklist:TodoWrite 到按会话显示的 TodoDock**
- **前提:** 隔离的本地 Electron 配置、确定性的 Agent/Host fixture、两个 Desktop 会话,不使用真实 Provider 或付费 API。
-- **步骤:** 启动调用 `TodoWrite` 的多步骤 Agent 回合,观察 Composer 上方的 TodoDock,展开后切换会话并确认清单隔离。完成和取消条目,确认最多显示八条以及全部取消状态;清空清单后重载/重启 Host。发送乱序旧 `todos.changed` 事件,确认它不能覆盖新快照;再覆盖非法参数、Plan/Goal、委托和远程会话路径。
-切换会话时收起、收起状态只占标题行高度、拒绝旧事件
+- **步骤:** 启动调用 `TodoWrite` 的多步骤 Agent 回合,观察 Composer 上方的 TodoDock,展开后切换会话并确认清单隔离。展开一个超过 dock 高度上限的清单,确认渲染全部条目、列表在 dock 内滚动而不撑高 Composer 栈;完成和取消条目,确认全部取消状态;清空清单后重载/重启 Host。发送乱序旧 `todos.changed` 事件,确认它不能覆盖新快照;再覆盖非法参数、Plan/Goal、委托和远程会话路径。
+- **预期:** 切换会话时收起、收起状态只占标题行高度、列表渲染全部条目并以自身为滚动容器、拒绝旧事件。
- **链接规格:** `03-runtime/03-tools-and-permissions.md`、`03-runtime/04-data-storage.md`、`03-runtime/06-host-rpc-protocol.md`、`04-ux/08-component-spec.md`、ADR 0312。
- **验收:** C / E / F / Quality / Security。
- **里程碑:** M6+。
-- **自动化:** `pnpm test:e2e:todos` 从生产 Agent 的 ToolSearch/TodoWrite 路径进入,使用生产渲染器、真实 Host/SQLite 和隔离 Electron 配置验证清单旅程;仅替换外部模型流和 preload 传输,不使用真实 Provider 或用户配置。覆盖 Unicode 截断及警告重放、单一活动项归一化、首次读取失败后不切换会话的主机恢复、已缓存快照重新同步及旧事件拒绝。运行时 `runtime-todos.test.ts` 使用确定性 Provider 验证 Agent 工具校验、超长内容归一化和续跑。
+- **自动化:** `pnpm test:e2e:todos` 从生产 Agent 的 ToolSearch/TodoWrite 路径进入,使用生产渲染器、真实 Host/SQLite 和隔离 Electron 配置验证清单旅程;仅替换外部模型流和 preload 传输,不使用真实 Provider 或用户配置。覆盖 Unicode 截断及警告重放、单一活动项归一化、全量清单渲染与 dock 内滚动、首次读取失败后不切换会话的主机恢复、已缓存快照重新同步及旧事件拒绝。运行时 `runtime-todos.test.ts` 使用确定性 Provider 验证 Agent 工具校验、超长内容归一化和续跑。
- **状态:** 构建 Desktop 和 Host 后,在确切的请求候选中执行。Host-core 和渲染器定向测试是辅助检查,不能替代 Electron 用户旅程。
## 8. 可追溯性矩阵
diff --git a/packages/i18n/src/locales/de/index.ts b/packages/i18n/src/locales/de/index.ts
index e81a8344eb..5e5cedde74 100644
--- a/packages/i18n/src/locales/de/index.ts
+++ b/packages/i18n/src/locales/de/index.ts
@@ -284,7 +284,6 @@ export const de = {
"progress": "{{completed}}/{{total}} erledigt",
"current": "{{completed}}/{{total}} · Aktuell: {{content}}",
"completed": "{{completed}}/{{total}} erledigt",
- "more": "{{count}} weitere Elemente",
"updated": "Checkliste aktualisiert",
"updating": "Checkliste wird aktualisiert",
"status": { "pending": "Offen", "in_progress": "In Bearbeitung", "completed": "Erledigt", "cancelled": "Abgebrochen" }
diff --git a/packages/i18n/src/locales/en/index.ts b/packages/i18n/src/locales/en/index.ts
index b36374fee1..fff8171ff9 100644
--- a/packages/i18n/src/locales/en/index.ts
+++ b/packages/i18n/src/locales/en/index.ts
@@ -291,7 +291,6 @@ export const en = {
progress: "{{completed}}/{{total}} completed",
current: "{{completed}}/{{total}} · Current: {{content}}",
completed: "{{completed}}/{{total}} completed",
- more: "{{count}} more items",
updated: "Checklist updated",
updating: "Updating checklist",
status: {
diff --git a/packages/i18n/src/locales/es/index.ts b/packages/i18n/src/locales/es/index.ts
index 66fa3c9995..88ecbd6c81 100644
--- a/packages/i18n/src/locales/es/index.ts
+++ b/packages/i18n/src/locales/es/index.ts
@@ -284,7 +284,6 @@ export const es = {
"progress": "{{completed}}/{{total}} completadas",
"current": "{{completed}}/{{total}} · Actual: {{content}}",
"completed": "{{completed}}/{{total}} completadas",
- "more": "{{count}} elementos más",
"updated": "Lista actualizada",
"updating": "Actualizando lista",
"status": { "pending": "Pendiente", "in_progress": "En curso", "completed": "Completada", "cancelled": "Cancelada" }
diff --git a/packages/i18n/src/locales/fr/index.ts b/packages/i18n/src/locales/fr/index.ts
index e218a265a4..e7296977cf 100644
--- a/packages/i18n/src/locales/fr/index.ts
+++ b/packages/i18n/src/locales/fr/index.ts
@@ -284,7 +284,6 @@ export const fr = {
"progress": "{{completed}}/{{total}} terminées",
"current": "{{completed}}/{{total}} · Actuelle : {{content}}",
"completed": "{{completed}}/{{total}} terminées",
- "more": "{{count}} éléments supplémentaires",
"updated": "Liste mise à jour",
"updating": "Mise à jour de la liste",
"status": { "pending": "En attente", "in_progress": "En cours", "completed": "Terminée", "cancelled": "Annulée" }
diff --git a/packages/i18n/src/locales/ko/index.ts b/packages/i18n/src/locales/ko/index.ts
index 5674b9ba37..46df6c31aa 100644
--- a/packages/i18n/src/locales/ko/index.ts
+++ b/packages/i18n/src/locales/ko/index.ts
@@ -293,7 +293,6 @@ export const ko = {
progress: "{{completed}}/{{total}} 완료",
current: "{{completed}}/{{total}} · 현재: {{content}}",
completed: "{{completed}}/{{total}} 완료",
- more: "{{count}}개 더 있음",
updated: "체크리스트 업데이트됨",
updating: "체크리스트 업데이트 중",
status: { pending: "대기", in_progress: "진행 중", completed: "완료", cancelled: "취소됨" },
diff --git a/packages/i18n/src/locales/pt-BR/index.ts b/packages/i18n/src/locales/pt-BR/index.ts
index 479d407a6b..4e1e0050c9 100644
--- a/packages/i18n/src/locales/pt-BR/index.ts
+++ b/packages/i18n/src/locales/pt-BR/index.ts
@@ -283,7 +283,6 @@ export const ptBR = {
progress: "{{completed}}/{{total}} concluídos",
current: "{{completed}}/{{total}} · Atual: {{content}}",
completed: "{{completed}}/{{total}} concluídos",
- more: "Mais {{count}} itens",
updated: "Checklist atualizado",
updating: "Atualizando checklist",
status: { pending: "Pendente", in_progress: "Em andamento", completed: "Concluído", cancelled: "Cancelado" },
diff --git a/packages/i18n/src/locales/tr/index.ts b/packages/i18n/src/locales/tr/index.ts
index c10e713717..9e289f0d30 100644
--- a/packages/i18n/src/locales/tr/index.ts
+++ b/packages/i18n/src/locales/tr/index.ts
@@ -293,7 +293,6 @@ export const tr = {
progress: "{{completed}}/{{total}} tamamlandı",
current: "{{completed}}/{{total}} · Geçerli: {{content}}",
completed: "{{completed}}/{{total}} tamamlandı",
- more: "{{count}} öğe daha",
updated: "Kontrol listesi güncellendi",
updating: "Kontrol listesi güncelleniyor",
status: { pending: "Bekliyor", in_progress: "Devam ediyor", completed: "Tamamlandı", cancelled: "İptal edildi" },
diff --git a/packages/i18n/src/locales/zh-CN/index.ts b/packages/i18n/src/locales/zh-CN/index.ts
index c1249d6c84..4592e6edd3 100644
--- a/packages/i18n/src/locales/zh-CN/index.ts
+++ b/packages/i18n/src/locales/zh-CN/index.ts
@@ -286,7 +286,6 @@ export const zhCN = {
progress: "已完成 {{completed}}/{{total}}",
current: "{{completed}}/{{total}} · 当前:{{content}}",
completed: "已完成 {{completed}}/{{total}}",
- more: "还有 {{count}} 项",
updated: "清单已更新",
updating: "正在更新清单",
status: { pending: "待办", in_progress: "进行中", completed: "已完成", cancelled: "已取消" },
diff --git a/packages/i18n/src/locales/zh-TW/index.ts b/packages/i18n/src/locales/zh-TW/index.ts
index 5c6a787505..89f35eca03 100644
--- a/packages/i18n/src/locales/zh-TW/index.ts
+++ b/packages/i18n/src/locales/zh-TW/index.ts
@@ -286,7 +286,6 @@ export const zhTW = {
progress: "已完成 {{completed}}/{{total}}",
current: "{{completed}}/{{total}} · 目前:{{content}}",
completed: "已完成 {{completed}}/{{total}}",
- more: "還有 {{count}} 項",
updated: "清單已更新",
updating: "正在更新清單",
status: { pending: "待辦", in_progress: "進行中", completed: "已完成", cancelled: "已取消" },
diff --git a/scripts/e2e/todo-checklist.tsx b/scripts/e2e/todo-checklist.tsx
index ef6f579d9a..1aadd4d029 100644
--- a/scripts/e2e/todo-checklist.tsx
+++ b/scripts/e2e/todo-checklist.tsx
@@ -72,11 +72,21 @@ window.todoChecklistProbe = async () => {
checks.push("collapsed-dock-reserves-no-blank-height");
flushSync(() => header()!.click());
await until(() => header()?.getAttribute("aria-expanded") === "true", "Click must expand dock");
- assert(document.querySelectorAll(".todo-dock-row").length === 8, "Expanded list is capped at eight rows");
+ // The expanded dock lists every row and scrolls inside its own box (#1319).
+ assert(document.querySelectorAll(".todo-dock-row").length === 10, "Expanded list renders every row");
+ const list = document.querySelector(".todo-dock-list")!;
+ assert(getComputedStyle(list).overflowY === "auto", "The expanded list is the scroll container");
+ assert(list.scrollHeight > list.clientHeight, "A long checklist scrolls inside the dock instead of growing it");
+ assert(Math.round(list.clientHeight) <= 280, `The dock list keeps a bounded height (${list.clientHeight}px)`);
+ assert(getComputedStyle(list).overscrollBehaviorY === "contain",
+ "Reaching the list's end must not scroll the transcript");
+ assert(list.tabIndex === 0, "The expanded list is keyboard reachable");
await until(() => Math.round(box(".todo-dock-clip").height) === Math.round(box(".todo-dock-list").height),
"Expanded clip must match the listed rows once the disclosure transition settles");
- assert(document.querySelector(".todo-dock-more")?.textContent?.includes("2"), "Overflow count must render");
- checks.push("agent-prompt-tool-discovery-host-write-event-dock-expand-bounded-list");
+ assert(!document.querySelector(".todo-dock-more"), "No static overflow line remains");
+ assert(Math.round(box(".todo-dock").height) < 400,
+ `The dock keeps a bounded footprint (${dockGeometry()})`);
+ checks.push("agent-prompt-tool-discovery-host-write-event-dock-expand-full-scrolling-list");
const normalized = await write(first, [
{ content: "😀".repeat(501), status: "in_progress", priority: "high" },
{ content: "Second active item", status: "in_progress", priority: "medium" },