fix(threads): allow scrolling through long clarification footers (#3655)

This commit is contained in:
José Almeida
2026-09-14 11:43:38 +02:00
committed by GitHub
parent 9475abe480
commit c092acac8b
12 changed files with 5352 additions and 5 deletions
@@ -5,6 +5,7 @@ import { IconChevronDown, IconChevronUp, IconMessageQuestion } from "@tabler/ico
import { Button } from "@kandev/ui/button";
import { useTranslation } from "react-i18next";
import { ClarificationInputOverlay } from "./clarification-input-overlay";
import { useComposerDisclosureContext } from "./composer-disclosure";
import { ResizeHandle } from "./resize-handle";
import { useResizableClarificationOverlay } from "@/hooks/use-resizable-clarification-overlay";
import type { ClarificationRequestMetadata, Message } from "@/lib/types/http";
@@ -64,6 +65,7 @@ export function ClarificationPanelSection({
onOutcome,
}: ClarificationPanelSectionProps) {
const { t } = useTranslation();
const disclosure = useComposerDisclosureContext();
const pendingId = pendingIdFromMessages(messages);
const [collapsed, setCollapsed] = useCollapsedForBundle(pendingId);
const contentId = useId();
@@ -101,7 +103,12 @@ export function ClarificationPanelSection({
style={
compact
? undefined
: { maxHeight: `${maxHeightVh}vh`, ...(height !== null ? { height } : {}) }
: {
maxHeight: `${maxHeightVh}vh`,
// Threads owns the outer vertical scroll boundary in its bounded footer.
overscrollBehaviorY: disclosure ? "auto" : undefined,
...(height !== null ? { height } : {}),
}
}
>
{compact && (
@@ -23,8 +23,6 @@ function seedTaskAndWaitForIdle(
}
test.describe("Mock agent clarification slash commands", () => {
test.describe.configure({ retries: 1 });
// Smoke test that the /ask-single alias routes to the clarification scenario.
// The underlying clarification behaviour (option click, multi-question carousel,
// submit, etc.) is exhaustively covered by clarification.spec.ts — this only
@@ -2,6 +2,12 @@ import { test, expect } from "../../fixtures/test-base";
import { assertNoHorizontalOverflow } from "../../helpers/session-stream-overload";
import { seedSecondaryClarificationTask } from "../../helpers/clarification";
import { waitForFiniteAnimations } from "../../helpers/animations";
import {
answerLongThreadQuestion,
expectThreadQuestionSubmitted,
seedLongThreadQuestion,
threadDeckGeometry,
} from "./threads-clarification-helpers";
import {
captureThreadSettings,
capturePresentation,
@@ -9,6 +15,60 @@ import {
startPresentationThread,
} from "./threads-presentation-helpers";
for (const short of [false, true]) {
// @covers AC-UI-THREADS-DECK-004.6, AC-UI-THREADS-DECK-004.7, AC-UI-THREADS-DECK-005.5, AC-UI-THREADS-DECK-005.7, AC-UI-THREADS-DECK-005.8
test(`scrolls and submits a long required question by touch (${short ? "short phone" : "Pixel 5"})`, async ({
testPage,
apiClient,
seedData,
}, testInfo) => {
test.setTimeout(180_000);
const original = await captureThreadSettings(apiClient);
try {
if (short) await testPage.setViewportSize({ width: 393, height: 500 });
const { task } = await seedLongThreadQuestion(apiClient, seedData);
await seedThreadPresentation(apiClient, { layout: "grid", autoHideComposer: true });
await testPage.goto(`/threads?taskId=${task.id}`);
const board = testPage.getByTestId("threads-board");
const tile = testPage.getByTestId(`thread-column-${task.id}`);
await expect(board).toHaveAttribute("data-layout", "columns");
await expect(board.getByTestId("session-chat")).toHaveCount(1);
await expect(tile.getByTestId("clarification-option")).toHaveCount(3);
await expect(tile.getByTestId("chat-input-editor")).toBeVisible();
await expect(board.getByTestId("collapse-composer")).toHaveCount(0);
expect(
(await tile.locator(".chat-message-list").boundingBox())!.height,
).toBeGreaterThanOrEqual(79);
const baseline = await threadDeckGeometry(testPage);
await capturePresentation(testPage, testInfo, "phone-long-question-before");
await answerLongThreadQuestion(testPage, tile, "touch", testInfo);
await expectThreadQuestionSubmitted(apiClient, task.session_id!, tile);
expect(await threadDeckGeometry(testPage)).toEqual(baseline);
await expect(board.getByTestId("session-chat")).toHaveCount(1);
await expect(tile.getByTestId("chat-input-editor")).toBeVisible();
const tileBox = (await tile.boundingBox())!;
expect(tileBox.y + tileBox.height).toBeLessThanOrEqual(testPage.viewportSize()!.height);
await assertNoHorizontalOverflow(testPage, "phone long question submitted");
await capturePresentation(testPage, testInfo, "phone-long-question-submitted");
await testPage.setViewportSize({ width: 767, height: 1100 });
await expect(board).toHaveAttribute("data-layout", "columns");
await expect(board.getByTestId("session-chat")).toHaveCount(1);
await testPage.setViewportSize({ width: 768, height: 1100 });
await expect(board).toHaveAttribute("data-layout", "grid");
await expect(board.getByTestId("session-chat")).toHaveCount(2);
await expect(board.getByTestId("collapse-composer")).toHaveCount(0);
expect((await captureThreadSettings(apiClient)).thread_views[0]).toMatchObject({
layout: "grid",
auto_hide_composer: true,
});
await assertNoHorizontalOverflow(testPage, "phone to tablet boundary");
} finally {
await apiClient.saveUserSettings(original);
}
});
}
// @covers AC-UI-THREADS-DECK-004.6, AC-UI-THREADS-DECK-005.7, AC-UI-THREADS-DECK-005.8, AC-UI-THREADS-DECK-005.10
test("keeps the touch composer visible and restores drafts through single-chat navigation", async ({
testPage,
@@ -0,0 +1,352 @@
import { chromium, expect, type Locator, type Page, type TestInfo } from "@playwright/test";
import { mkdtemp, mkdir, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import type { SeedData } from "../../fixtures/test-base";
import type { ApiClient } from "../../helpers/api-client";
import { waitForFiniteAnimations } from "../../helpers/animations";
import { waitForAgentMessage, waitForSessionState } from "../../helpers/session";
import { PrAssetCapture } from "../../helpers/pr-asset-capture";
import { capturePresentation, threadBoxes } from "./threads-presentation-helpers";
export async function attachQuestionRecord(info: TestInfo, name: string, record: unknown) {
const path = info.outputPath(`${name}.json`);
await writeFile(path, JSON.stringify(record, null, 2));
await info.attach(name, { path, contentType: "application/json" });
}
export async function seedLongThreadQuestion(api: ApiClient, seed: SeedData) {
const tasks = [];
for (const [title, scenario] of [
["A long required question", "clarification-multi"],
["B neighboring question", "clarification"],
]) {
const task = await api.createTaskWithAgent(seed.workspaceId, title, seed.agentProfileId, {
description: `/e2e:${scenario}`,
workflow_id: seed.workflowId,
workflow_step_id: seed.startStepId,
repository_ids: [seed.repositoryId],
executor_profile_id: seed.worktreeExecutorProfileId,
});
if (!task.session_id) throw new Error("Required question has no session");
await waitForSessionState(api, {
taskId: task.id,
sessionId: task.session_id,
expectedState: "WAITING_FOR_INPUT",
message: `${title} must block on its real clarification request`,
timeout: 60_000,
});
tasks.push(task);
}
return { task: tasks[0], sibling: tasks[1] };
}
/** Native tab zoom, with neither viewport/device-scale emulation nor CSS zoom. */
export async function withNativeThreadZoom(
baseURL: string,
zoom: number,
info: TestInfo,
run: (page: Page) => Promise<void>,
) {
const root = await mkdtemp(join(tmpdir(), "threads-question-zoom-"));
const extension = join(root, "extension");
await mkdir(extension);
await writeFile(
join(extension, "manifest.json"),
JSON.stringify({
manifest_version: 3,
name: "Threads question native zoom test",
version: "1.0",
permissions: ["tabs"],
background: { service_worker: "background.js" },
}),
);
await writeFile(
join(extension, "background.js"),
"chrome.runtime.onInstalled.addListener(() => {});",
);
let context;
try {
context = await chromium.launchPersistentContext(join(root, "profile"), {
channel: "chromium",
headless: true,
baseURL,
viewport: null,
deviceScaleFactor: undefined,
args: [
`--disable-extensions-except=${extension}`,
`--load-extension=${extension}`,
"--window-size=1366,855",
],
});
await context.addInitScript(() => {
localStorage.setItem("kandev.onboarding.completed", "true");
});
const page = context.pages()[0];
const worker = context.serviceWorkers()[0] ?? (await context.waitForEvent("serviceworker"));
await page.goto(baseURL);
const initialScale = await page.evaluate(() => devicePixelRatio);
const actualZoom = await worker.evaluate(
async ({ baseURL, zoom }) => {
const { tabs } = (
globalThis as unknown as {
chrome: {
tabs: {
query: (query: { url: string }) => Promise<{ id: number }[]>;
setZoom: (id: number, factor: number) => Promise<void>;
getZoom: (id: number) => Promise<number>;
};
};
}
).chrome;
const [tab] = await tabs.query({ url: `${new URL(baseURL).origin}/*` });
await tabs.setZoom(tab.id, zoom);
return tabs.getZoom(tab.id);
},
{ baseURL, zoom },
);
expect(actualZoom).toBe(zoom);
await expect
.poll(() => page.evaluate(() => devicePixelRatio))
.toBeCloseTo(initialScale * zoom, 5);
await attachQuestionRecord(info, "native-browser-zoom", {
browser: context.browser()!.version(),
zoom: actualZoom,
...(await page.evaluate(() => ({
width: innerWidth,
height: innerHeight,
devicePixelRatio,
visualScale: visualViewport?.scale,
}))),
});
try {
await run(page);
} finally {
await capturePresentation(page, info, "native-question-final-state");
}
} finally {
try {
await context?.close();
} finally {
await rm(root, { recursive: true, force: true });
}
}
}
/** Intersect every clipping ancestor, including the inner question and outer footer. */
export function questionTargetGeometry(target: Locator) {
return target.evaluate((element) => {
const box = element.getBoundingClientRect();
const clip = { top: 0, bottom: innerHeight, left: 0, right: innerWidth };
for (let parent = element.parentElement; parent; parent = parent.parentElement) {
const style = getComputedStyle(parent);
const bounds = parent.getBoundingClientRect();
if (/(auto|scroll|hidden|clip)/.test(style.overflowY)) {
clip.top = Math.max(clip.top, bounds.top);
clip.bottom = Math.min(clip.bottom, bounds.bottom);
}
if (/(auto|scroll|hidden|clip)/.test(style.overflowX)) {
clip.left = Math.max(clip.left, bounds.left);
clip.right = Math.min(clip.right, bounds.right);
}
}
const x = box.x + box.width / 2;
const y = box.y + box.height / 2;
const hit = document.elementFromPoint(x, y);
const footer = element.closest('[data-testid="thread-footer-allocation"]')!;
const inner = element.closest('[data-testid="clarification-scroll-region"]')!;
return {
x,
y,
width: box.width,
height: box.height,
top: box.top,
bottom: box.bottom,
clip,
contained:
box.top >= clip.top - 1 &&
box.bottom <= clip.bottom + 1 &&
box.left >= clip.left - 1 &&
box.right <= clip.right + 1,
hit: !!hit && element.contains(hit),
footerScrollTop: footer.scrollTop,
innerScrollTop: inner.scrollTop,
};
});
}
type QuestionInput = "wheel" | "touch" | "keyboard";
async function scrollQuestion(page: Page, tile: Locator, direction: number, touch: boolean) {
const area = await tile.getByTestId("clarification-scroll-region").evaluate((inner) => {
const box = inner.getBoundingClientRect();
const footer = inner
.closest('[data-testid="thread-footer-allocation"]')!
.getBoundingClientRect();
const top = Math.max(box.top, footer.top, 0);
const bottom = Math.min(box.bottom, footer.bottom, innerHeight);
return { x: box.x + box.width / 2, y: (top + bottom) / 2, height: bottom - top };
});
expect(area.height).toBeGreaterThan(40);
if (!touch) {
await page.mouse.move(area.x, area.y);
await page.mouse.wheel(0, direction * 40);
return;
}
const client = await page.context().newCDPSession(page);
try {
await client.send("Input.dispatchTouchEvent", {
type: "touchStart",
touchPoints: [{ x: area.x, y: area.y + direction * 20 }],
});
for (let step = 1; step <= 8; step++) {
await client.send("Input.dispatchTouchEvent", {
type: "touchMove",
touchPoints: [{ x: area.x, y: area.y + direction * (20 - step * 5) }],
});
}
} finally {
try {
await client.send("Input.dispatchTouchEvent", { type: "touchEnd", touchPoints: [] });
} finally {
await client.detach();
}
}
}
export async function reachQuestionTarget(
page: Page,
tile: Locator,
target: Locator,
input: QuestionInput,
reverseTab = false,
) {
await waitForFiniteAnimations(tile);
if (input === "keyboard") {
await expect
.poll(
async () => {
if (await target.evaluate((el) => el === document.activeElement)) return true;
await page.keyboard.press(reverseTab ? "Shift+Tab" : "Tab");
return false;
},
{ intervals: [50], timeout: 10_000, message: "Native tab navigation reaches the action" },
)
.toBe(true);
} else {
await expect
.poll(
async () => {
const geometry = await questionTargetGeometry(target);
if (!geometry.contained || !geometry.hit) {
await scrollQuestion(
page,
tile,
geometry.bottom > geometry.clip.bottom + 1 ? 1 : -1,
input === "touch",
);
}
return geometry;
},
{
intervals: [100],
timeout: 10_000,
message: `${input} reaches the complete question action through the footer`,
},
)
.toMatchObject({ contained: true, hit: true });
}
const geometry = await questionTargetGeometry(target);
expect(geometry).toMatchObject({ contained: true, hit: true });
return geometry;
}
export async function answerLongThreadQuestion(
page: Page,
tile: Locator,
input: QuestionInput,
info: TestInfo,
beforeSubmit?: () => Promise<void>,
) {
const records = [];
const capture = new PrAssetCapture(page, info.file, { captureKey: info.title });
const option = (label: string) =>
tile.getByTestId("clarification-option").filter({
has: page.getByTestId("clarification-option-label").getByText(label, { exact: true }),
});
const activate = async (target: Locator, key = "Enter", reverse = false) => {
const geometry = await reachQuestionTarget(page, tile, target, input, reverse);
records.push({ target: await target.innerText(), ...geometry });
if ((await target.getAttribute("data-testid")) === "clarification-submit") {
await beforeSubmit?.();
await capture.screenshot("submit-reachable");
}
if (input === "keyboard") await page.keyboard.press(key);
else if (input === "touch") await page.touchscreen.tap(geometry.x, geometry.y);
else await page.mouse.click(geometry.x, geometry.y);
};
try {
// Prove the last option is reachable before any activation can focus/scroll it.
await reachQuestionTarget(page, tile, option("SQLite"), input);
await capture.screenshot("last-option-reachable");
await activate(tile.getByTestId("clarification-next"));
await expect(tile.getByTestId("clarification-step").nth(1)).toHaveAttribute(
"data-active",
"true",
);
await expect(tile.getByTestId("clarification-step").nth(0)).toHaveAttribute(
"data-answered",
"false",
);
await activate(tile.getByTestId("clarification-prev"), "Enter", true);
await expect(tile.getByTestId("clarification-step").nth(0)).toHaveAttribute(
"data-active",
"true",
);
for (const [index, label] of ["SQLite", "Rust", "Bare metal"].entries()) {
await activate(option(label), index === 1 ? "Enter" : "Space");
await expect(tile.getByTestId("clarification-step").nth(index)).toHaveAttribute(
"data-answered",
"true",
);
}
await activate(tile.getByTestId("clarification-submit"), "Enter", true);
} finally {
await attachQuestionRecord(info, `${input}-question-hit-targets`, records);
capture.flush();
}
if (input === "touch") {
for (const record of records.filter((record) =>
/SQLite|Rust|Bare metal|Submit/.test(record.target),
)) {
expect(record.height).toBeGreaterThanOrEqual(44);
expect(record.width).toBeGreaterThanOrEqual(44);
}
}
}
export async function expectThreadQuestionSubmitted(
api: ApiClient,
sessionId: string,
tile: Locator,
) {
await waitForAgentMessage(api, sessionId, "You answered:");
const { messages } = await api.listSessionMessages(sessionId);
const receipt = messages
.filter((message) => message.author_type === "agent")
.map((message) => message.content)
.join("\n");
expect(receipt).toMatch(/"db":\s*{\s*"selected_option":\s*"q1_opt3"/);
expect(receipt).toMatch(/"language":\s*{\s*"selected_option":\s*"q2_opt3"/);
expect(receipt).toMatch(/"deploy":\s*{\s*"selected_option":\s*"q3_opt2"/);
await expect(tile.getByTestId("clarification-overlay-container")).toHaveCount(0);
await expect(tile.locator(".chat-message-list")).toContainText("You answered:");
}
export async function threadDeckGeometry(page: Page) {
const board = page.getByTestId("threads-board");
return {
boxes: await threadBoxes(board),
scroll: await board.evaluate((el) => ({ left: el.scrollLeft, top: el.scrollTop })),
};
}
@@ -3,6 +3,15 @@ import { test, expect } from "../../fixtures/test-base";
import { dwell } from "../../helpers/causal-waits";
import { seedSecondaryClarificationTask } from "../../helpers/clarification";
import { waitForLatestSessionDone } from "../../helpers/session";
import {
answerLongThreadQuestion,
attachQuestionRecord,
expectThreadQuestionSubmitted,
questionTargetGeometry,
seedLongThreadQuestion,
threadDeckGeometry,
withNativeThreadZoom,
} from "./threads-clarification-helpers";
import {
captureThreadSettings,
capturePresentation,
@@ -13,6 +22,123 @@ import {
let original: ThreadPresentationSettings;
for (const zoom of [1, 0.9]) {
for (const autoHideComposer of [false, true]) {
// @covers AC-UI-THREADS-DECK-005.5, AC-UI-THREADS-DECK-005.6, AC-UI-THREADS-DECK-004.1
test(`scrolls long required questions through the Grid footer (zoom ${zoom}, auto-hide ${autoHideComposer})`, async ({
apiClient,
seedData,
backend,
}, testInfo) => {
test.setTimeout(180_000);
const { task } = await seedLongThreadQuestion(apiClient, seedData);
await apiClient.seedAgentMessages(task.session_id!, 35, "Question history");
await seedThreadPresentation(apiClient, { layout: "grid", autoHideComposer });
await withNativeThreadZoom(backend.frontendUrl, zoom, testInfo, async (page) => {
await page.goto(`/threads?taskId=${task.id}`);
const board = page.getByTestId("threads-board");
const tile = page.getByTestId(`thread-column-${task.id}`);
await expect(board).toHaveAttribute("data-layout", "grid");
await expect(tile.getByTestId("clarification-option")).toHaveCount(3);
await expect(tile.getByTestId("chat-input-editor")).toBeVisible();
const list = tile.locator(".chat-message-list");
await expect(list.getByText("Question history 35", { exact: true })).toHaveCount(1);
expect((await list.boundingBox())!.height).toBeGreaterThanOrEqual(79);
await expect
.poll(() => list.evaluate((el) => el.scrollHeight - el.scrollTop - el.clientHeight))
.toBeLessThan(3);
if (autoHideComposer) {
const freeze = tile.getByTestId("auto-scroll-toggle-button");
await freeze.click();
await expect(freeze).toHaveAttribute("aria-pressed", "false");
// Return from the composer control to the question's original allocation.
const footer = tile.getByTestId("thread-footer-allocation");
const box = (await footer.boundingBox())!;
await page.mouse.move(box.x + 2, box.y + box.height - 10);
await page.mouse.wheel(0, -3000);
await expect.poll(() => footer.evaluate((el) => el.scrollTop)).toBe(0);
}
// Frozen positions only adopt real reader gestures, never synthetic scrollTop writes.
await list.evaluate((el) => {
el.setAttribute("data-history-scrolling", "true");
el.addEventListener("scrollend", () => el.removeAttribute("data-history-scrolling"), {
once: true,
});
});
const listBox = (await list.boundingBox())!;
await page.mouse.move(listBox.x + listBox.width / 2, listBox.y + listBox.height / 2);
await page.mouse.wheel(0, -600);
await expect(list).not.toHaveAttribute("data-history-scrolling", "true");
const anchor = await list.evaluate((el) => {
const top = el.getBoundingClientRect().top;
const paragraph = Array.from(el.querySelectorAll("p")).find(
(p) =>
p.textContent?.startsWith("Question history") &&
p.getBoundingClientRect().top > top + 5,
)!;
return {
text: paragraph.textContent!,
y: paragraph.getBoundingClientRect().y,
scrollTop: el.scrollTop,
};
});
const baseline = await threadDeckGeometry(page);
await attachQuestionRecord(testInfo, "question-before-wheel", {
...(await questionTargetGeometry(tile.getByTestId("clarification-option").last())),
tile: await tile.boundingBox(),
footer: await tile.getByTestId("thread-footer-allocation").boundingBox(),
});
await answerLongThreadQuestion(page, tile, "wheel", testInfo, async () => {
expect((await list.getByText(anchor.text, { exact: true }).boundingBox())!.y).toBeCloseTo(
anchor.y,
0,
);
expect(await threadDeckGeometry(page)).toEqual(baseline);
});
await expectThreadQuestionSubmitted(apiClient, task.session_id!, tile);
expect(await threadDeckGeometry(page)).toEqual(baseline);
if (autoHideComposer) {
expect(await list.evaluate((el) => el.scrollTop)).toBeCloseTo(anchor.scrollTop, 0);
} else {
// Resuming agent work follows the bottom when auto-scroll is enabled.
await expect
.poll(() => list.evaluate((el) => el.scrollHeight - el.scrollTop - el.clientHeight))
.toBeLessThan(3);
}
});
});
}
}
for (const scenario of [
{
name: "answers a long required question with keyboard navigation",
layout: "grid",
input: "keyboard",
},
{ name: "keeps long required answers usable in Columns", layout: "columns", input: "wheel" },
] as const) {
// @covers AC-UI-THREADS-DECK-005.5, AC-UI-THREADS-DECK-005.6, AC-UI-THREADS-DECK-005.9
test(scenario.name, async ({ testPage, apiClient, seedData }, testInfo) => {
test.setTimeout(180_000);
await testPage.setViewportSize({ width: 1366, height: 768 });
const { task } = await seedLongThreadQuestion(apiClient, seedData);
await seedThreadPresentation(apiClient, { layout: scenario.layout, autoHideComposer: true });
await testPage.goto(`/threads?taskId=${task.id}`);
const tile = testPage.getByTestId(`thread-column-${task.id}`);
await expect(testPage.getByTestId("threads-board")).toHaveAttribute(
"data-layout",
scenario.layout,
);
await expect(tile.getByTestId("clarification-option")).toHaveCount(3);
const baseline = await threadDeckGeometry(testPage);
await answerLongThreadQuestion(testPage, tile, scenario.input, testInfo);
await expectThreadQuestionSubmitted(apiClient, task.session_id!, tile);
expect(await threadDeckGeometry(testPage)).toEqual(baseline);
await capturePresentation(testPage, testInfo, `question-${scenario.input}-${scenario.layout}`);
});
}
async function observeDisclosureMotion(tile: Locator) {
await tile.evaluate((root) => {
const samples: { early: number; late: number; duration: number; ciVisible: boolean }[] = [];
@@ -0,0 +1,210 @@
# Threads Grid Clarification Overflow: Diagnostic Evidence
Recorded 2026-09-13 in task `f8e3db36-7fe5-492a-b9da-7cf72d3b5077`, session
`0c4b868a-694a-4de2-8d5b-48d554076e23`. This is diagnosis and a browser-only
causal probe, not an implemented or verified production fix.
## User report and base
> when i'm on grid view i can't seem to scroll down to see all the options on a clarification (on my laptop at 90% zoom. I think we need to make it scrollable in that situation.
Clean starting branch: `feature/fix-grid-clarificati-zmg`. A fresh
`git fetch origin main` returned `d62daa9d70437e1a46f7dd31f6138f90ebd54b3f`,
matching HEAD and origin/main. `git merge-base --is-ancestor` confirmed that
`f718c50666eb7175e39087294afabc70ac39f3a7` is included.
`gh pr view 3626 --repo kdlbs/kandev --json state,mergedAt,mergeCommit,baseRefName`
confirmed MERGED, main, that same merge commit, and `2026-09-13T12:13:10Z`.
No obsolete parent branch was checked out or written to.
## Smallest faithful reproduction
1. Use the standard disposable E2E backend/worktree fixture. Create two tasks
with live primary mock-agent sessions, one using `/e2e:clarification-multi`
and its neighbor using `/e2e:clarification`. Wait for WAITING_FOR_INPUT.
2. Save a test-owned Threads view with Grid and auto-hide false or true. Open
`/threads` focused on the multi-question session. Two tasks are essential:
a single admitted task fills the available height by design.
3. Use a 1366x768 content viewport at 100%, or the same browser window at real
90% zoom. Place the pointer inside the visible part of the question and
scroll down. The inner content reaches its end while the outer footer
stays at scrollTop 0. SQLite (last option) and Next remain clipped.
4. Do not use a locator click/focus/scrollIntoView to reveal the option. Native
focus can scroll the outer footer and would conceal the wheel regression.
The existing three-question fixture is already long enough: context, question,
three described options, custom input, and navigation total about 475 CSS px.
No fake question DOM or modified backend protocol was needed.
## Native zoom
Browser: bundled headless Chromium `149.0.7827.55`, Linux. A temporary persistent
profile and local MV3 extension with only `tabs` permission called
`chrome.tabs.setZoom(tabId, 0.9)` and verified `chrome.tabs.getZoom(tabId) === 0.9`.
The browser window used `--window-size=1366,855` and no Playwright viewport or
device-scale emulation. At 100%, the content area was 1366x768; at 90%,
`innerWidth/innerHeight` were 1517x853, devicePixelRatio was
0.8999999761581421, and visualViewport.scale stayed 1. At 100%, DPR and visual
scale were both 1. CSS zoom was not applied.
The setup used `chromium.launchPersistentContext(ownedProfile, { channel:
"chromium", headless: true, viewport: null, deviceScaleFactor: undefined,
args: ["--disable-extensions-except=<owned-extension>",
"--load-extension=<owned-extension>", "--window-size=1366,855"] })`.
Explicitly clearing deviceScaleFactor matters inside Playwright Test because
its desktop project otherwise supplies a default. The extension has a minimal
background service worker; obtain that worker through `context.serviceWorkers`
and invoke the tab zoom API there. Close the context in `finally`.
This follows the primary [Playwright extension setup](https://playwright.dev/docs/chrome-extensions)
and [Chrome tabs zoom API](https://developer.chrome.com/docs/extensions/reference/api/tabs#method-setZoom).
The completed probe verifies native browser zoom, not CSS zoom, a larger
emulated viewport, deviceScaleFactor, or pinch zoom. Wheel events hit the actual
clarification at both zooms; at 90% a requested delta of 900 was reported as
about 1000 CSS px. This is desktop automation, not physical laptop/trackpad
hardware evidence. No exact user device/browser information was needed to
establish a faithful reproduction.
## Measured geometry
All numbers below are CSS px, rounded to two decimals. The raw
[geometry record](evidence/geometry.json) retains full precision, before/after
ancestor styles, wheel targets and keyboard focus trails.
| Zoom | Auto-hide | Tile height | Footer height | Footer content height | Question viewport height | Inner content height | Inner scrollTop after wheel | Footer scrollTop after wheel |
| --- | --- | --- | --- | --- | --- | --- | --- | --- |
| 100% | Off | 346 | 198.50 | 541 | 384 | 475 | 90 | 0 |
| 100% | On | 346 | 198.50 | 573 | 384 | 475 | 90 | 0 |
| 90% | Off | 388.66 | 240.87 | 584 | 426.67 | 475 | 48.89 | 0 |
| 90% | On | 388.66 | 240.87 | 616 | 426.67 | 475 | 48.89 | 0 |
At 90%, the tile bottom was y=440.66 and the footer ended at y=439.55.
After three downward wheel events over the question, SQLite began at y=461.39
and Next at y=588.07. Both center hit-tests failed. The header Submit had
scrolled above the visible footer (y=160.95), while the outer footer remained
unmoved. At 100%, the final option began at y=419.75 below a y=397 footer end.
The native transcript stayed 80px high in all four cases.
![Real 90% zoom: final option clipped after wheel scrolling](https://raw.githubusercontent.com/kdlbs/kandev/e0c9e9c77703f7a2efc2729f5c26fb883c29be0a/diagnosis-grid-90-before.png)
These are Playwright screenshots taken at native zoom; use the raw DOM metrics
for CSS geometry, not the exported bitmap dimensions as physical screen size.
## Root cause and scope
The chain is:
```text
ThreadColumn: fixed grid tile, min-height 0, overflow hidden
TaskChatPanel: full-height flex column
transcript: independent native scroll owner
ComposerFooterAllocation: max-height calc(100% - 80px), overflow-y auto,
overscroll contain
ClarificationPanelSection
overlay container: max-height 50vh, overflow hidden, overscroll contain
clarification-scroll-region: flex child, min-height 0, overflow-y auto
question, options, custom input, Next/Back and header Submit
ChatFooter / ChatInputArea / composer disclosure region
```
`ClarificationPanelSection`'s clipped intermediate container blocks scroll
chaining at the end of the inner question scroll range. Its 50vh viewport is
larger than the tile's footer, so the inner scroller alone cannot expose the
bottom of its own viewport inside the outer clipping boundary. The footer has
ample remaining scroll range, but wheel input never reaches it.
Pending state is correct: the real selected session owns the clarification;
`ChatInputArea` reports required activity and disclosure remains expanded.
The question is outside the animated composer wrapper. Auto-hide changes
routine footer content height but not this failure. No new pending-state or
disclosure controller behavior is needed.
Fourteen native Tab steps reached the last option, custom input, Next, and
composer. Focus scrolled the outer footer to 306px at 100% and 327.78px at
90%, confirming a usable programmatic scroll range. This records navigation
only; full keyboard answering/submission remains an implementation acceptance
check. Pointer-based test helpers that implicitly focus/scroll can mask the bug.
## Browser-only causal probe
After preserving all four failing baseline measurements, change only
`clarification-overlay-container.style.overscrollBehaviorY = "auto"` in the
isolated browser at 90%, auto-hide on. Reset the scrollers to their baseline,
then use small wheel events, center hit-tests, and real mouse clicks.
- The footer moved to 66.67px; SQLite and then Rust became clickable.
- The final question's Bare metal option was clickable with footer scrollTop
115.56px. Scrolling back exposed header Submit, which passed the hit-test.
- Submit completed the real pending question exchange. The overlay cleared,
and the mock agent received `db=q1_opt3`, `language=q2_opt3`,
`deploy=q3_opt2`. No full-task navigation or direct submission API was used.
The [causal probe record](evidence/scroll-chain-probe.json) contains the click
coordinates, ancestor geometry, pending-clear observation, and agent receipt.
The probe proves the scroll-chain cause and feasibility of a narrow correction;
it is not production GREEN or a complete final containment/keyboard/mobile audit.
![Real submission after the temporary DOM-only change](https://raw.githubusercontent.com/kdlbs/kandev/e0c9e9c77703f7a2efc2729f5c26fb883c29be0a/diagnosis-grid-90-dom-probe.png)
## Coverage gap
The current desktop `only the selected session's required question forces its
composer open` scenario admits one task (full height even with Grid selected),
switches sessions and checks required disclosure; it does not answer a long
question. It passed on the affected main. The existing short-phone scenario
taps the first PostgreSQL option in a single question. Neither proves wheel/
touch access to a final option and carousel action in a bounded short tile.
## Validation
| Command/check | Exact result |
| --- | --- |
| `cd apps && pnpm install --frozen-lockfile` | Passed, 903 packages; frozen lockfile unchanged |
| Managed current-assets baseline command below | Passed: backend, Vite E2E assets, fixture plugin built; 1 selected-session test passed (11.5s test body) |
| Temporary diagnostic discovery | 1 test in 1 file |
| First native-zoom diagnostic attempt | 1 setup failure: inherited deviceScaleFactor incompatible with null viewport; no app behavior tested |
| Corrected temporary diagnostic command below | 1 instrumentation test passed (35.3s test body; 46.6s total), four baseline failures recorded and one successful DOM-only causal probe; workers 1, retries 0, strict WS |
| Screenshots | Before/after inspected; data and actions match the measured root cause |
| Production fix and full affected desktop/mobile acceptance | Not run; pending explicit implementation request |
Exact commands, from `apps/web`:
```bash
pnpm e2e:run --host --shards 1 --project chromium tests/task/threads-composer-disclosure.spec.ts --grep 'only the selected session' --workers=1 --retries=0
pnpm e2e:raw --project=chromium e2e/tests/task/grid-clarification-native-zoom-diagnostic.spec.ts --list --workers=1 --retries=0
pnpm e2e:run --host --no-build --shards 1 --project chromium tests/task/grid-clarification-native-zoom-diagnostic.spec.ts --workers=1 --retries=0
```
The last two commands used a temporary diagnostic file, removed at the handoff.
They document executed evidence, not a permanent suite command. Permanent
regression test paths/commands are defined in Task 01.
The initial sandboxed build failed to write the shared Go cache. The authorized
retry completed outside that filesystem restriction. Native Chromium's first
standalone probe also needed the authorized socket-capable execution context.
Neither environment setup failure is counted as behavioral RED. The default
managed build compiled all remote helpers on the cold worktree; the work order
uses the existing lean host build plus fresh web assets for these focused paths.
## Isolation and cleanup
The successful diagnostic used backend PID `3185664`, port `18100`, and owned
root `/tmp/kandev-e2e-0-28DMRc`. Its recorded fallback teardown command was:
```bash
scripts/kandev-kill 18100 --yes
```
That command was valid only while this fixture owned port 18100. The normal
fixture teardown already removed that root and stopped that PID; both absence
checks passed. Do not run a historical port command against a future occupant.
Persistent diagnostic browsers closed via `context.close()`. Temporary test
files and only this task's disposable browser profiles/extensions are removed
before the handoff. The synthetic measurements/screenshots above are retained.
The baseline fixture `/tmp/kandev-e2e-0-s4S96m` and setup-failure fixture
`/tmp/kandev-e2e-0-hDHSWa` are likewise test-owned and automatically torn down.
Developer :9998 and parent playground :48431 were not used or modified. No
parent branch/PR writes, merge/queue changes, or task/session/agent spawning
occurred. Public documentation needs no change for this diagnosis/plan, and
the implementation restores the behavior already described in the existing
Threads guide.
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,895 @@
{
"units": "CSS px",
"desktop": [
{
"case": "task-threads-composer-disc-21925-er-zoom-0-9-auto-hide-true--chromium",
"native_zoom": {
"browser": "149.0.7827.55",
"zoom": 0.9,
"width": 1517,
"height": 853,
"devicePixelRatio": 0.8999999761581421,
"visualScale": 1
},
"before": {
"x": 918.8889465332031,
"y": 538.6371803283691,
"width": 1131.5972900390625,
"height": 56.68402862548828,
"top": 510.295166015625,
"bottom": 566.9791946411133,
"clip": {
"top": 199.7916717529297,
"bottom": 439.54864501953125,
"left": 333.107666015625,
"right": 1504.670166015625
},
"contained": false,
"hit": false,
"footerScrollTop": 0,
"innerScrollTop": 0,
"tile": {
"x": 331.9965515136719,
"y": 51.99652862548828,
"width": 1173.7848205566406,
"height": 388.66320037841797
},
"footer": {
"x": 333.107666015625,
"y": 198.68057250976562,
"width": 1171.5625,
"height": 240.86807250976562
}
},
"targets": [
{
"target": "Next",
"x": 1453.6372623443604,
"y": 423.3941249847412,
"width": 62.1006965637207,
"height": 26.197917938232422,
"top": 410.295166015625,
"bottom": 436.4930839538574,
"clip": {
"top": 198.68057250976562,
"bottom": 439.54864501953125,
"left": 333.107666015625,
"right": 1504.670166015625
},
"contained": true,
"hit": true,
"footerScrollTop": 177.7777862548828,
"innerScrollTop": 48.88888931274414
},
{
"target": "Back",
"x": 384.7309284210205,
"y": 423.3941249847412,
"width": 63.281253814697266,
"height": 26.197917938232422,
"top": 410.295166015625,
"bottom": 436.4930839538574,
"clip": {
"top": 198.68057250976562,
"bottom": 439.54864501953125,
"left": 333.107666015625,
"right": 1504.670166015625
},
"contained": true,
"hit": true,
"footerScrollTop": 177.7777862548828,
"innerScrollTop": 48.88888931274414
},
{
"target": "3\nSQLite\nEmbedded database for simplicity",
"x": 918.8889465332031,
"y": 311.9704933166504,
"width": 1131.5972900390625,
"height": 56.68402862548828,
"top": 283.62847900390625,
"bottom": 340.31250762939453,
"clip": {
"top": 198.68057250976562,
"bottom": 439.54864501953125,
"left": 333.107666015625,
"right": 1504.670166015625
},
"contained": true,
"hit": true,
"footerScrollTop": 177.7777862548828,
"innerScrollTop": 48.88888931274414
},
{
"target": "3\nRust\nBest for systems work",
"x": 918.8889465332031,
"y": 311.9704933166504,
"width": 1131.5972900390625,
"height": 56.68402862548828,
"top": 283.62847900390625,
"bottom": 340.31250762939453,
"clip": {
"top": 198.68057250976562,
"bottom": 439.54864501953125,
"left": 333.107666015625,
"right": 1504.670166015625
},
"contained": true,
"hit": true,
"footerScrollTop": 177.7777862548828,
"innerScrollTop": 48.88888931274414
},
{
"target": "2\nBare metal\nRun directly on hosts",
"x": 918.8889465332031,
"y": 249.29687881469727,
"width": 1131.5972900390625,
"height": 56.68402862548828,
"top": 220.95486450195312,
"bottom": 277.6388931274414,
"clip": {
"top": 198.68057250976562,
"bottom": 385.8159770965576,
"left": 333.107666015625,
"right": 1504.670166015625
},
"contained": true,
"hit": true,
"footerScrollTop": 226.6666717529297,
"innerScrollTop": 0
},
{
"target": "Submit",
"x": 1379.4879035949707,
"y": 217.33507633209229,
"width": 82.4826431274414,
"height": 23.97569465637207,
"top": 205.34722900390625,
"bottom": 229.32292366027832,
"clip": {
"top": 198.68057250976562,
"bottom": 439.54864501953125,
"left": 333.107666015625,
"right": 1504.670166015625
},
"contained": true,
"hit": true,
"footerScrollTop": 4.44444465637207,
"innerScrollTop": 0
}
]
},
{
"case": "task-threads-composer-disc-4addc-oter-zoom-1-auto-hide-true--chromium",
"native_zoom": {
"browser": "149.0.7827.55",
"zoom": 1,
"width": 1366,
"height": 768,
"devicePixelRatio": 1,
"visualScale": 1
},
"before": {
"x": 843,
"y": 538,
"width": 980,
"height": 56.5,
"top": 509.75,
"bottom": 566.25,
"clip": {
"top": 199.5,
"bottom": 397,
"left": 333,
"right": 1353
},
"contained": false,
"hit": false,
"footerScrollTop": 0,
"innerScrollTop": 0,
"tile": {
"x": 332,
"y": 52,
"width": 1022,
"height": 346
},
"footer": {
"x": 333,
"y": 198.5,
"width": 1020,
"height": 198.5
}
},
"targets": [
{
"target": "Next",
"x": 1302.015625,
"y": 359.25,
"width": 61.96875,
"height": 26,
"top": 346.25,
"bottom": 372.25,
"clip": {
"top": 198.5,
"bottom": 383.5,
"left": 333,
"right": 1353
},
"contained": true,
"hit": true,
"footerScrollTop": 200,
"innerScrollTop": 90
},
{
"target": "Back",
"x": 384.5703125,
"y": 359.25,
"width": 63.140625,
"height": 26,
"top": 346.25,
"bottom": 372.25,
"clip": {
"top": 198.5,
"bottom": 383.5,
"left": 333,
"right": 1353
},
"contained": true,
"hit": true,
"footerScrollTop": 200,
"innerScrollTop": 90
},
{
"target": "3\nSQLite\nEmbedded database for simplicity",
"x": 843,
"y": 248,
"width": 980,
"height": 56.5,
"top": 219.75,
"bottom": 276.25,
"clip": {
"top": 198.5,
"bottom": 383.5,
"left": 333,
"right": 1353
},
"contained": true,
"hit": true,
"footerScrollTop": 200,
"innerScrollTop": 90
},
{
"target": "3\nRust\nBest for systems work",
"x": 843,
"y": 248,
"width": 980,
"height": 56.5,
"top": 219.75,
"bottom": 276.25,
"clip": {
"top": 198.5,
"bottom": 383.5,
"left": 333,
"right": 1353
},
"contained": true,
"hit": true,
"footerScrollTop": 200,
"innerScrollTop": 90
},
{
"target": "2\nBare metal\nRun directly on hosts",
"x": 843,
"y": 253.5,
"width": 980,
"height": 56.5,
"top": 225.25,
"bottom": 281.75,
"clip": {
"top": 198.5,
"bottom": 361.5,
"left": 333,
"right": 1353
},
"contained": true,
"hit": true,
"footerScrollTop": 222,
"innerScrollTop": 0
},
{
"target": "Submit",
"x": 1227.7109375,
"y": 221.5,
"width": 82.578125,
"height": 24,
"top": 209.5,
"bottom": 233.5,
"clip": {
"top": 199.5,
"bottom": 397,
"left": 333,
"right": 1353
},
"contained": true,
"hit": true,
"footerScrollTop": 0,
"innerScrollTop": 0
}
]
},
{
"case": "task-threads-composer-disc-acf88-ter-zoom-1-auto-hide-false--chromium",
"native_zoom": {
"browser": "149.0.7827.55",
"zoom": 1,
"width": 1366,
"height": 768,
"devicePixelRatio": 1,
"visualScale": 1
},
"before": {
"x": 843,
"y": 538,
"width": 980,
"height": 56.5,
"top": 509.75,
"bottom": 566.25,
"clip": {
"top": 199.5,
"bottom": 397,
"left": 333,
"right": 1353
},
"contained": false,
"hit": false,
"footerScrollTop": 0,
"innerScrollTop": 0,
"tile": {
"x": 332,
"y": 52,
"width": 1022,
"height": 346
},
"footer": {
"x": 333,
"y": 198.5,
"width": 1020,
"height": 198.5
}
},
"targets": [
{
"target": "Next",
"x": 1302.015625,
"y": 359.25,
"width": 61.96875,
"height": 26,
"top": 346.25,
"bottom": 372.25,
"clip": {
"top": 198.5,
"bottom": 383.5,
"left": 333,
"right": 1353
},
"contained": true,
"hit": true,
"footerScrollTop": 200,
"innerScrollTop": 90
},
{
"target": "Back",
"x": 384.5703125,
"y": 359.25,
"width": 63.140625,
"height": 26,
"top": 346.25,
"bottom": 372.25,
"clip": {
"top": 198.5,
"bottom": 383.5,
"left": 333,
"right": 1353
},
"contained": true,
"hit": true,
"footerScrollTop": 200,
"innerScrollTop": 90
},
{
"target": "3\nSQLite\nEmbedded database for simplicity",
"x": 843,
"y": 248,
"width": 980,
"height": 56.5,
"top": 219.75,
"bottom": 276.25,
"clip": {
"top": 198.5,
"bottom": 383.5,
"left": 333,
"right": 1353
},
"contained": true,
"hit": true,
"footerScrollTop": 200,
"innerScrollTop": 90
},
{
"target": "3\nRust\nBest for systems work",
"x": 843,
"y": 248,
"width": 980,
"height": 56.5,
"top": 219.75,
"bottom": 276.25,
"clip": {
"top": 198.5,
"bottom": 383.5,
"left": 333,
"right": 1353
},
"contained": true,
"hit": true,
"footerScrollTop": 200,
"innerScrollTop": 90
},
{
"target": "2\nBare metal\nRun directly on hosts",
"x": 843,
"y": 253.5,
"width": 980,
"height": 56.5,
"top": 225.25,
"bottom": 281.75,
"clip": {
"top": 198.5,
"bottom": 361.5,
"left": 333,
"right": 1353
},
"contained": true,
"hit": true,
"footerScrollTop": 222,
"innerScrollTop": 0
},
{
"target": "Submit",
"x": 1227.7109375,
"y": 221.5,
"width": 82.578125,
"height": 24,
"top": 209.5,
"bottom": 233.5,
"clip": {
"top": 199.5,
"bottom": 397,
"left": 333,
"right": 1353
},
"contained": true,
"hit": true,
"footerScrollTop": 0,
"innerScrollTop": 0
}
]
},
{
"case": "task-threads-composer-disc-d0198-r-zoom-0-9-auto-hide-false--chromium",
"native_zoom": {
"browser": "149.0.7827.55",
"zoom": 0.9,
"width": 1517,
"height": 853,
"devicePixelRatio": 0.8999999761581421,
"visualScale": 1
},
"before": {
"x": 918.8889465332031,
"y": 538.6371803283691,
"width": 1131.5972900390625,
"height": 56.68402862548828,
"top": 510.295166015625,
"bottom": 566.9791946411133,
"clip": {
"top": 199.7916717529297,
"bottom": 439.54864501953125,
"left": 333.107666015625,
"right": 1504.670166015625
},
"contained": false,
"hit": false,
"footerScrollTop": 0,
"innerScrollTop": 0,
"tile": {
"x": 331.9965515136719,
"y": 51.99652862548828,
"width": 1173.7848205566406,
"height": 388.66320037841797
},
"footer": {
"x": 333.107666015625,
"y": 198.68057250976562,
"width": 1171.5625,
"height": 240.86807250976562
}
},
"targets": [
{
"target": "Next",
"x": 1453.6372623443604,
"y": 423.3941249847412,
"width": 62.1006965637207,
"height": 26.197917938232422,
"top": 410.295166015625,
"bottom": 436.4930839538574,
"clip": {
"top": 198.68057250976562,
"bottom": 439.54864501953125,
"left": 333.107666015625,
"right": 1504.670166015625
},
"contained": true,
"hit": true,
"footerScrollTop": 177.7777862548828,
"innerScrollTop": 48.88888931274414
},
{
"target": "Back",
"x": 384.7309284210205,
"y": 423.3941249847412,
"width": 63.281253814697266,
"height": 26.197917938232422,
"top": 410.295166015625,
"bottom": 436.4930839538574,
"clip": {
"top": 198.68057250976562,
"bottom": 439.54864501953125,
"left": 333.107666015625,
"right": 1504.670166015625
},
"contained": true,
"hit": true,
"footerScrollTop": 177.7777862548828,
"innerScrollTop": 48.88888931274414
},
{
"target": "3\nSQLite\nEmbedded database for simplicity",
"x": 918.8889465332031,
"y": 311.9704933166504,
"width": 1131.5972900390625,
"height": 56.68402862548828,
"top": 283.62847900390625,
"bottom": 340.31250762939453,
"clip": {
"top": 198.68057250976562,
"bottom": 439.54864501953125,
"left": 333.107666015625,
"right": 1504.670166015625
},
"contained": true,
"hit": true,
"footerScrollTop": 177.7777862548828,
"innerScrollTop": 48.88888931274414
},
{
"target": "3\nRust\nBest for systems work",
"x": 918.8889465332031,
"y": 311.9704933166504,
"width": 1131.5972900390625,
"height": 56.68402862548828,
"top": 283.62847900390625,
"bottom": 340.31250762939453,
"clip": {
"top": 198.68057250976562,
"bottom": 439.54864501953125,
"left": 333.107666015625,
"right": 1504.670166015625
},
"contained": true,
"hit": true,
"footerScrollTop": 177.7777862548828,
"innerScrollTop": 48.88888931274414
},
{
"target": "2\nBare metal\nRun directly on hosts",
"x": 918.8889465332031,
"y": 249.29687881469727,
"width": 1131.5972900390625,
"height": 56.68402862548828,
"top": 220.95486450195312,
"bottom": 277.6388931274414,
"clip": {
"top": 198.68057250976562,
"bottom": 385.8159770965576,
"left": 333.107666015625,
"right": 1504.670166015625
},
"contained": true,
"hit": true,
"footerScrollTop": 226.6666717529297,
"innerScrollTop": 0
},
{
"target": "Submit",
"x": 1379.4879035949707,
"y": 217.33507633209229,
"width": 82.4826431274414,
"height": 23.97569465637207,
"top": 205.34722900390625,
"bottom": 229.32292366027832,
"clip": {
"top": 198.68057250976562,
"bottom": 439.54864501953125,
"left": 333.107666015625,
"right": 1504.670166015625
},
"contained": true,
"hit": true,
"footerScrollTop": 4.44444465637207,
"innerScrollTop": 0
}
]
}
],
"mobile": [
{
"case": "task-mobile-threads-compos-d673a--question-by-touch-Pixel-5--mobile-chrome",
"input": "CDP touch start/move/end and touchscreen tap",
"targets": [
{
"target": "Next",
"x": 342.015625,
"y": 568.75,
"width": 61.96875,
"height": 26,
"top": 555.75,
"bottom": 581.75,
"clip": {
"top": 230,
"bottom": 593.5,
"left": 0,
"right": 393
},
"contained": true,
"hit": true,
"footerScrollTop": 0,
"innerScrollTop": 159
},
{
"target": "Back",
"x": 51.5703125,
"y": 568.75,
"width": 63.140625,
"height": 26,
"top": 555.75,
"bottom": 581.75,
"clip": {
"top": 230,
"bottom": 593.5,
"left": 0,
"right": 393
},
"contained": true,
"hit": true,
"footerScrollTop": 0,
"innerScrollTop": 159
},
{
"target": "3\nSQLite\nEmbedded database for simplicity",
"x": 196.5,
"y": 457.5,
"width": 353,
"height": 56.5,
"top": 429.25,
"bottom": 485.75,
"clip": {
"top": 230,
"bottom": 593.5,
"left": 0,
"right": 393
},
"contained": true,
"hit": true,
"footerScrollTop": 0,
"innerScrollTop": 159
},
{
"target": "3\nRust\nBest for systems work",
"x": 196.5,
"y": 457.5,
"width": 353,
"height": 56.5,
"top": 429.25,
"bottom": 485.75,
"clip": {
"top": 230,
"bottom": 593.5,
"left": 0,
"right": 393
},
"contained": true,
"hit": true,
"footerScrollTop": 0,
"innerScrollTop": 159
},
{
"target": "2\nBare metal\nRun directly on hosts",
"x": 196.5,
"y": 458,
"width": 353,
"height": 56.5,
"top": 429.75,
"bottom": 486.25,
"clip": {
"top": 230,
"bottom": 593.5,
"left": 0,
"right": 393
},
"contained": true,
"hit": true,
"footerScrollTop": 0,
"innerScrollTop": 96
},
{
"target": "Submit",
"x": 144.5,
"y": 252,
"width": 257,
"height": 44,
"top": 230,
"bottom": 274,
"clip": {
"top": 230,
"bottom": 593.5,
"left": 0,
"right": 393
},
"contained": true,
"hit": true,
"footerScrollTop": 0,
"innerScrollTop": 40
}
],
"viewport": {
"width": 393,
"height": 727
},
"screen": {
"width": 393,
"height": 851
},
"deviceScaleFactor": 2.75,
"viewport_source": "Viewport confirmed by captured PNG dimensions; screen metrics confirmed in a separate browser probe of the same configuration."
},
{
"case": "task-mobile-threads-compos-d64ef-stion-by-touch-short-phone--mobile-chrome",
"input": "CDP touch start/move/end and touchscreen tap",
"targets": [
{
"target": "Next",
"x": 342.015625,
"y": 454.75,
"width": 61.96875,
"height": 26,
"top": 441.75,
"bottom": 467.75,
"clip": {
"top": 230,
"bottom": 480,
"left": 0,
"right": 393
},
"contained": true,
"hit": true,
"footerScrollTop": 0,
"innerScrollTop": 273
},
{
"target": "Back",
"x": 51.5703125,
"y": 454.75,
"width": 63.140625,
"height": 26,
"top": 441.75,
"bottom": 467.75,
"clip": {
"top": 230,
"bottom": 480,
"left": 0,
"right": 393
},
"contained": true,
"hit": true,
"footerScrollTop": 0,
"innerScrollTop": 273
},
{
"target": "3\nSQLite\nEmbedded database for simplicity",
"x": 196.5,
"y": 343.5,
"width": 353,
"height": 56.5,
"top": 315.25,
"bottom": 371.75,
"clip": {
"top": 230,
"bottom": 480,
"left": 0,
"right": 393
},
"contained": true,
"hit": true,
"footerScrollTop": 0,
"innerScrollTop": 273
},
{
"target": "3\nRust\nBest for systems work",
"x": 196.5,
"y": 343.5,
"width": 353,
"height": 56.5,
"top": 315.25,
"bottom": 371.75,
"clip": {
"top": 230,
"bottom": 480,
"left": 0,
"right": 393
},
"contained": true,
"hit": true,
"footerScrollTop": 0,
"innerScrollTop": 273
},
{
"target": "2\nBare metal\nRun directly on hosts",
"x": 196.5,
"y": 344,
"width": 353,
"height": 56.5,
"top": 315.75,
"bottom": 372.25,
"clip": {
"top": 230,
"bottom": 480,
"left": 0,
"right": 393
},
"contained": true,
"hit": true,
"footerScrollTop": 0,
"innerScrollTop": 210
},
{
"target": "Submit",
"x": 144.5,
"y": 254,
"width": 257,
"height": 44,
"top": 232,
"bottom": 276,
"clip": {
"top": 230,
"bottom": 480,
"left": 0,
"right": 393
},
"contained": true,
"hit": true,
"footerScrollTop": 0,
"innerScrollTop": 38
}
],
"viewport": {
"width": 393,
"height": 500
},
"screen": {
"width": 393,
"height": 500
},
"deviceScaleFactor": 2.75,
"viewport_source": "Viewport confirmed by captured PNG dimensions; screen metrics confirmed in a separate browser probe of the same configuration."
}
]
}
@@ -0,0 +1,627 @@
{
"change": "Temporary DOM overscrollBehaviorY:auto on clarification-overlay-container",
"clicks": [
{
"label": "SQLite",
"point": {
"x": 918.8889465332031,
"y": 423.0816078186035,
"hit": true
},
"geometry": {
"wheelEvents": [
{
"x": 918,
"y": 268,
"deltaY": 1000.0000264909539,
"target": "clarification-overlay"
},
{
"x": 918,
"y": 268,
"deltaY": 1000.0000264909539,
"target": "clarification-overlay"
},
{
"x": 918,
"y": 268,
"deltaY": 1000.0000264909539,
"target": "clarification-overlay"
},
{
"x": 918,
"y": 319,
"deltaY": 66.66666843273026,
"target": "clarification-question-card"
},
{
"x": 918,
"y": 319,
"deltaY": 66.66666843273026,
"target": "clarification-option"
}
],
"viewport": {
"innerWidth": 1517,
"innerHeight": 853,
"dpr": 0.8999999761581421,
"visualScale": 1
},
"layout": "grid",
"nodes": [
{
"id": "thread-column-10344556-5ab4-4b1a-bbdc-eb43a65ef72d",
"x": 331.9965515136719,
"y": 51.99652862548828,
"width": 1173.7847900390625,
"height": 388.6632080078125,
"bottom": 440.6597366333008,
"clientHeight": 386,
"scrollHeight": 386,
"scrollTop": 0,
"overflowY": "hidden",
"overscrollY": "auto",
"maxHeight": "none",
"minHeight": "0px"
},
{
"id": "session-chat",
"x": 333.107666015625,
"y": 118.6805648803711,
"width": 1171.5625,
"height": 320.8680725097656,
"bottom": 439.5486373901367,
"clientHeight": 321,
"scrollHeight": 321,
"scrollTop": 0,
"overflowY": "visible",
"overscrollY": "auto",
"maxHeight": "none",
"minHeight": "0px"
},
{
"id": "h-full overflow-y-auto overflow-x-hidden relative chat-message-list p-0 [overflow-anchor:auto]",
"x": 333.107666015625,
"y": 118.6805648803711,
"width": 1171.5625,
"height": 80,
"bottom": 198.6805648803711,
"clientHeight": 80,
"scrollHeight": 206,
"scrollTop": 125.55555725097656,
"overflowY": "auto",
"overscrollY": "auto",
"maxHeight": "none",
"minHeight": "0px"
},
{
"id": "thread-footer-allocation",
"x": 333.107666015625,
"y": 198.68057250976562,
"width": 1171.5625,
"height": 240.86807250976562,
"bottom": 439.54864501953125,
"clientHeight": 241,
"scrollHeight": 616,
"scrollTop": 66.66667175292969,
"overflowY": "auto",
"overscrollY": "contain",
"maxHeight": "calc(100% - 80px)",
"minHeight": "0px"
},
{
"id": "clarification-overlay-container",
"x": 333.107666015625,
"y": 133.125,
"width": 1171.5625,
"height": 426.66668701171875,
"bottom": 559.7916870117188,
"clientHeight": 427,
"scrollHeight": 427,
"scrollTop": 0,
"overflowY": "hidden",
"overscrollY": "auto",
"maxHeight": "426.667px",
"minHeight": "120px"
},
{
"id": "clarification-scroll-region",
"x": 333.107666015625,
"y": 133.125,
"width": 1171.5625,
"height": 426.66668701171875,
"bottom": 559.7916870117188,
"clientHeight": 427,
"scrollHeight": 475,
"scrollTop": 48.88888931274414,
"overflowY": "auto",
"overscrollY": "auto",
"maxHeight": "none",
"minHeight": "0px"
},
{
"id": "chat-input-area",
"x": 333.107666015625,
"y": 559.7916870117188,
"width": 1171.5625,
"height": 188.15972900390625,
"bottom": 747.951416015625,
"clientHeight": 188,
"scrollHeight": 188,
"scrollTop": 0,
"overflowY": "visible",
"overscrollY": "auto",
"maxHeight": "none",
"minHeight": "0px"
}
],
"targets": [
{
"id": "clarification-option",
"text": "1PostgreSQLRelational database with strong consistency",
"x": 353.0903015136719,
"y": 269.3923645019531,
"width": 1131.5972900390625,
"height": 56.68402862548828,
"bottom": 326.0763931274414,
"hit": true
},
{
"id": "clarification-option",
"text": "2MongoDBDocument database for flexible schemas",
"x": 353.0903015136719,
"y": 332.06597900390625,
"width": 1131.5972900390625,
"height": 56.68402862548828,
"bottom": 388.75000762939453,
"hit": true
},
{
"id": "clarification-option",
"text": "3SQLiteEmbedded database for simplicity",
"x": 353.0903015136719,
"y": 394.7395935058594,
"width": 1131.5972900390625,
"height": 56.68402862548828,
"bottom": 451.42362213134766,
"hit": true
},
{
"id": "clarification-next",
"text": "Next",
"x": 1422.5869140625,
"y": 521.40625,
"width": 62.1006965637207,
"height": 26.197917938232422,
"bottom": 547.6041679382324,
"hit": false
},
{
"id": "clarification-submit",
"text": "Submit",
"x": 1338.24658203125,
"y": 94.23611450195312,
"width": 82.4826431274414,
"height": 23.97569465637207,
"bottom": 118.2118091583252,
"hit": false
}
]
}
},
{
"label": "Rust",
"point": {
"x": 918.8889465332031,
"y": 423.0816078186035,
"hit": true
},
"geometry": {
"wheelEvents": [
{
"x": 918,
"y": 268,
"deltaY": 1000.0000264909539,
"target": "clarification-overlay"
},
{
"x": 918,
"y": 268,
"deltaY": 1000.0000264909539,
"target": "clarification-overlay"
},
{
"x": 918,
"y": 268,
"deltaY": 1000.0000264909539,
"target": "clarification-overlay"
},
{
"x": 918,
"y": 319,
"deltaY": 66.66666843273026,
"target": "clarification-question-card"
},
{
"x": 918,
"y": 319,
"deltaY": 66.66666843273026,
"target": "clarification-option"
}
],
"viewport": {
"innerWidth": 1517,
"innerHeight": 853,
"dpr": 0.8999999761581421,
"visualScale": 1
},
"layout": "grid",
"nodes": [
{
"id": "thread-column-10344556-5ab4-4b1a-bbdc-eb43a65ef72d",
"x": 331.9965515136719,
"y": 51.99652862548828,
"width": 1173.7847900390625,
"height": 388.6632080078125,
"bottom": 440.6597366333008,
"clientHeight": 386,
"scrollHeight": 386,
"scrollTop": 0,
"overflowY": "hidden",
"overscrollY": "auto",
"maxHeight": "none",
"minHeight": "0px"
},
{
"id": "session-chat",
"x": 333.107666015625,
"y": 118.6805648803711,
"width": 1171.5625,
"height": 320.8680725097656,
"bottom": 439.5486373901367,
"clientHeight": 321,
"scrollHeight": 321,
"scrollTop": 0,
"overflowY": "visible",
"overscrollY": "auto",
"maxHeight": "none",
"minHeight": "0px"
},
{
"id": "h-full overflow-y-auto overflow-x-hidden relative chat-message-list p-0 [overflow-anchor:auto]",
"x": 333.107666015625,
"y": 118.6805648803711,
"width": 1171.5625,
"height": 80,
"bottom": 198.6805648803711,
"clientHeight": 80,
"scrollHeight": 206,
"scrollTop": 125.55555725097656,
"overflowY": "auto",
"overscrollY": "auto",
"maxHeight": "none",
"minHeight": "0px"
},
{
"id": "thread-footer-allocation",
"x": 333.107666015625,
"y": 198.68057250976562,
"width": 1171.5625,
"height": 240.86807250976562,
"bottom": 439.54864501953125,
"clientHeight": 241,
"scrollHeight": 616,
"scrollTop": 66.66667175292969,
"overflowY": "auto",
"overscrollY": "contain",
"maxHeight": "calc(100% - 80px)",
"minHeight": "0px"
},
{
"id": "clarification-overlay-container",
"x": 333.107666015625,
"y": 133.125,
"width": 1171.5625,
"height": 426.66668701171875,
"bottom": 559.7916870117188,
"clientHeight": 427,
"scrollHeight": 427,
"scrollTop": 0,
"overflowY": "hidden",
"overscrollY": "auto",
"maxHeight": "426.667px",
"minHeight": "120px"
},
{
"id": "clarification-scroll-region",
"x": 333.107666015625,
"y": 133.125,
"width": 1171.5625,
"height": 426.66668701171875,
"bottom": 559.7916870117188,
"clientHeight": 427,
"scrollHeight": 475,
"scrollTop": 48.88888931274414,
"overflowY": "auto",
"overscrollY": "auto",
"maxHeight": "none",
"minHeight": "0px"
},
{
"id": "chat-input-area",
"x": 333.107666015625,
"y": 559.7916870117188,
"width": 1171.5625,
"height": 188.15972900390625,
"bottom": 747.951416015625,
"clientHeight": 188,
"scrollHeight": 188,
"scrollTop": 0,
"overflowY": "visible",
"overscrollY": "auto",
"maxHeight": "none",
"minHeight": "0px"
}
],
"targets": [
{
"id": "clarification-option",
"text": "1GoStrong typing, fast compile",
"x": 353.0903015136719,
"y": 269.3923645019531,
"width": 1131.5972900390625,
"height": 56.68402862548828,
"bottom": 326.0763931274414,
"hit": true
},
{
"id": "clarification-option",
"text": "2TypeScriptFamiliar to the team",
"x": 353.0903015136719,
"y": 332.06597900390625,
"width": 1131.5972900390625,
"height": 56.68402862548828,
"bottom": 388.75000762939453,
"hit": true
},
{
"id": "clarification-option",
"text": "3RustBest for systems work",
"x": 353.0903015136719,
"y": 394.7395935058594,
"width": 1131.5972900390625,
"height": 56.68402862548828,
"bottom": 451.42362213134766,
"hit": true
},
{
"id": "clarification-next",
"text": "Next",
"x": 1422.5869140625,
"y": 521.40625,
"width": 62.1006965637207,
"height": 26.197917938232422,
"bottom": 547.6041679382324,
"hit": false
},
{
"id": "clarification-submit",
"text": "Submit",
"x": 1338.24658203125,
"y": 94.23611450195312,
"width": 82.4826431274414,
"height": 23.97569465637207,
"bottom": 118.2118091583252,
"hit": false
}
]
}
},
{
"label": "Bare metal",
"point": {
"x": 918.8889465332031,
"y": 360.4079933166504,
"hit": true
},
"geometry": {
"wheelEvents": [
{
"x": 918,
"y": 268,
"deltaY": 1000.0000264909539,
"target": "clarification-overlay"
},
{
"x": 918,
"y": 268,
"deltaY": 1000.0000264909539,
"target": "clarification-overlay"
},
{
"x": 918,
"y": 268,
"deltaY": 1000.0000264909539,
"target": "clarification-overlay"
},
{
"x": 918,
"y": 319,
"deltaY": 66.66666843273026,
"target": "clarification-question-card"
},
{
"x": 918,
"y": 319,
"deltaY": 66.66666843273026,
"target": "clarification-option"
}
],
"viewport": {
"innerWidth": 1517,
"innerHeight": 853,
"dpr": 0.8999999761581421,
"visualScale": 1
},
"layout": "grid",
"nodes": [
{
"id": "thread-column-10344556-5ab4-4b1a-bbdc-eb43a65ef72d",
"x": 331.9965515136719,
"y": 51.99652862548828,
"width": 1173.7847900390625,
"height": 388.6632080078125,
"bottom": 440.6597366333008,
"clientHeight": 386,
"scrollHeight": 386,
"scrollTop": 0,
"overflowY": "hidden",
"overscrollY": "auto",
"maxHeight": "none",
"minHeight": "0px"
},
{
"id": "session-chat",
"x": 333.107666015625,
"y": 118.6805648803711,
"width": 1171.5625,
"height": 320.8680725097656,
"bottom": 439.5486373901367,
"clientHeight": 321,
"scrollHeight": 321,
"scrollTop": 0,
"overflowY": "visible",
"overscrollY": "auto",
"maxHeight": "none",
"minHeight": "0px"
},
{
"id": "h-full overflow-y-auto overflow-x-hidden relative chat-message-list p-0 [overflow-anchor:auto]",
"x": 333.107666015625,
"y": 118.6805648803711,
"width": 1171.5625,
"height": 80,
"bottom": 198.6805648803711,
"clientHeight": 80,
"scrollHeight": 206,
"scrollTop": 125.55555725097656,
"overflowY": "auto",
"overscrollY": "auto",
"maxHeight": "none",
"minHeight": "0px"
},
{
"id": "thread-footer-allocation",
"x": 333.107666015625,
"y": 198.68057250976562,
"width": 1171.5625,
"height": 240.86807250976562,
"bottom": 439.54864501953125,
"clientHeight": 241,
"scrollHeight": 602,
"scrollTop": 115.55555725097656,
"overflowY": "auto",
"overscrollY": "contain",
"maxHeight": "calc(100% - 80px)",
"minHeight": "0px"
},
{
"id": "clarification-overlay-container",
"x": 333.107666015625,
"y": 84.23611450195312,
"width": 1171.5625,
"height": 412.69097900390625,
"bottom": 496.9270935058594,
"clientHeight": 413,
"scrollHeight": 413,
"scrollTop": 0,
"overflowY": "hidden",
"overscrollY": "auto",
"maxHeight": "426.667px",
"minHeight": "120px"
},
{
"id": "clarification-scroll-region",
"x": 333.107666015625,
"y": 84.23611450195312,
"width": 1171.5625,
"height": 412.69097900390625,
"bottom": 496.9270935058594,
"clientHeight": 413,
"scrollHeight": 413,
"scrollTop": 0,
"overflowY": "auto",
"overscrollY": "auto",
"maxHeight": "none",
"minHeight": "0px"
},
{
"id": "chat-input-area",
"x": 333.107666015625,
"y": 496.9270935058594,
"width": 1171.5625,
"height": 188.15972900390625,
"bottom": 685.0868225097656,
"clientHeight": 188,
"scrollHeight": 188,
"scrollTop": 0,
"overflowY": "visible",
"overscrollY": "auto",
"maxHeight": "none",
"minHeight": "0px"
}
],
"targets": [
{
"id": "clarification-option",
"text": "1DockerContainerized deploy",
"x": 353.0903015136719,
"y": 269.3923645019531,
"width": 1131.5972900390625,
"height": 56.68402862548828,
"bottom": 326.0763931274414,
"hit": true
},
{
"id": "clarification-option",
"text": "2Bare metalRun directly on hosts",
"x": 353.0903015136719,
"y": 332.06597900390625,
"width": 1131.5972900390625,
"height": 56.68402862548828,
"bottom": 388.75000762939453,
"hit": true
},
{
"id": "clarification-next",
"text": "Next",
"x": 1422.5869140625,
"y": 458.732666015625,
"width": 62.1006965637207,
"height": 26.197917938232422,
"bottom": 484.9305839538574,
"hit": false
},
{
"id": "clarification-submit",
"text": "Submit",
"x": 1338.24658203125,
"y": 94.23611450195312,
"width": 82.4826431274414,
"height": 23.97569465637207,
"bottom": 118.2118091583252,
"hit": false
}
]
}
}
],
"submitPoint": {
"x": 1379.4879035949707,
"y": 221.77951908111572,
"hit": true
},
"resolved": false,
"resolvedAfterWait": true,
"agent_answer": "Let me ask you a few questions about the project setup.You answered: {\n \"db\": {\n \"selected_option\": \"q1_opt3\"\n },\n \"deploy\": {\n \"selected_option\": \"q3_opt2\"\n },\n \"language\": {\n \"selected_option\": \"q2_opt3\"\n }"
}
@@ -0,0 +1,238 @@
---
created: 2026-09-13
status: implemented
requirements:
- REQ-UI-THREADS-DECK-004
- REQ-UI-THREADS-DECK-005
system_design:
- ../../specs/ui/system-design/threads-conversation-deck.md
- ../../specs/ui/system-design/threads-saved-views.md
legacy_specs: []
---
# Implementation Plan: Threads Grid Clarification Overflow
## Overview
Restore wheel and touch access to every required question option and action
inside a bounded Threads footer. One sequential work order covers the narrow
containment correction and its desktop/mobile regression evidence.
This is a conformance repair to the reviewed [Threads layouts package](../threads-layouts/plan.md),
particularly [Task 02](../threads-layouts/task-02-render-grid.md),
[Task 03](../threads-layouts/task-03-composer-disclosure.md), and
[Task 06](../threads-layouts/task-06-presentation-polish.md). Their recorded
results remain historical; this package does not reopen or replace them.
Diagnosis and this package are complete. The user authorized implementation
after the handoff on 2026-09-13. Task 01 is implemented and verified; no merge or queue action
belongs to this package.
## Scope
### In scope
- Let vertical scrolling pass from the existing clarification scroll region
through its clipped wrapper to the existing bounded Threads footer.
- Cover auto-hide on and off, actual option/Next/Submit activation, keyboard
access, stable sibling tiles, and existing transcript/CI behavior.
- Preserve Columns, normal task chat, clarification resizing/collapse, and the
phone's one-conversation composition, touch input, and safe areas.
### Out of scope
Grid sizing/fallback redesign, new scroll containers, extra response renderers,
new state or APIs, persisted preferences, arbitrary tile resizing, mobile
composition changes, agent lifecycle changes, parent PR/branch writes, and
additional tasks, sessions, or delegated agents.
## Evidence and requirement conformance
The [diagnostic report](evidence.md) records the original user wording, current
main/merge verification, browser setup, raw geometry, and a browser-only causal
probe. The defect reproduces at 100% and real 90% Chromium browser zoom with
both auto-hide states. The implementation violates active
`AC-UI-THREADS-DECK-005.5` and the short-tile reachability outcome in `005.6`.
The existing design's [Composer geometry](../../specs/ui/system-design/threads-conversation-deck.md#composer-geometry)
already requires bounded scrolling and reachable question actions, including
auto-hide off. No requirement, acceptance ID, design boundary, or ADR needs to
be created or changed.
Preservation criteria are `AC-UI-THREADS-DECK-004.1`, `004.6`, `004.7`, and
`005.1`, `005.3`, `005.6`, `005.7`, `005.8`, `005.9`, `005.11`.
Saved-view presentation semantics remain owned by the existing saved-views
requirement/design pair; no settings writes are part of the correction.
## Technical approach
`TaskChatPanel` places `ClarificationPanelSection` and `ChatFooter` inside
`ComposerFooterAllocation`. The footer is correctly bounded to the tile body
minus an 80px transcript floor. The question's existing inner scroll region
is capped at 50vh, which can exceed a half-height tile's footer.
The intermediate `clarification-overlay-container` has `overflow: hidden` and
`overscroll-behavior: contain`. Once the inner scroller reaches its own bottom,
this clipped wrapper blocks input from reaching the still-scrollable footer.
The footer remains at scrollTop 0, so the final option and Next stay clipped.
In `apps/web/components/task/chat/clarification-panel-section.tsx`, use the
existing optional `useComposerDisclosureContext` to identify a Threads host.
Scope a vertical-only scroll-chaining allowance to that host. Check context
presence, not `disclosure.enabled`: auto-hide-off and touch Threads also have
the bounded footer and reproduced/implicated path. Keep non-Threads behavior
unchanged. Do not change the 50vh cap, drag clamp, minimum height, collapse
state, or question renderer.
`ComposerFooterAllocation` remains the tile's outer vertical containment
boundary (`overflow-y-auto`, `overscroll-contain`, and the 80px reserve). The
existing resizable question retains its local scroll region; its input must
continue into the footer at its boundary. This restores the existing scroll
chain without a new scroll owner, wheel handler, resize observer, or transcript
scroll loop. Leave horizontal containment and deck navigation unchanged.
Required-action reporting already forces the native composer open. The
question is a sibling of `ChatFooter`, outside its disclosure animation. Do
not change pending-action state, disclosure timing, CI placement, or input
mounting to repair a vertical scroll-chain defect.
## Mobile design contract
Entry remains `/threads` through the shared listing navigation. The nearest
shipped exemplar is the current Threads phone composition in
`ThreadColumn`/`ThreadsBoard`, using the same focused-conversation approach as
`MobileColumnTabs` and the existing thread/session picker sheets. Title and
status precede one transcript; required questions and the normal composer
remain inline. Primary outcome is answering the question inside that surface.
Retain this full-height composition because answering is part of the active
conversation. No extra drawer or task navigation is required. The existing
dynamic-height app shell, safe-area handling, visible touch composer, and
single active transcript remain authoritative. The bounded footer owns the
outer allocation/containment; the existing resizable question's local scroll
region must chain into it. Reuse all question state and submission logic.
Phone/coarse-pointer targets retain their current sizing; assert at least
44px for option rows and primary Submit, plus actual hit targets and viewport
containment for every exercised action. Do not enlarge desktop controls.
## ASCII UI preview
UI-01: Short desktop Grid, long pending question, auto-hide on or off.
```text
BEFORE AFTER
+-------------------------+ +-------------------------+
| Task A [Open]| | Task A [Open]|
| transcript (80px floor) | | transcript (80px floor) |
| Question / options | | Question / options |
| inner scroll stops | | scroll continues into |
| [final option clipped] | | bounded footer |
| [Next unreachable] | | [last option] [Next] |
+-------------------------+ +-------------------------+
| Task B: stable tile | | Task B: stable tile |
+-------------------------+ +-------------------------+
```
Header/tile geometry is fixed. The footer scrolls vertically within its
allocation; options, Next, and the existing header Submit can each be scrolled
into view. They need not all fit simultaneously. The question header is not
made sticky and no action moves. The drawing's spacing is illustrative; the
scroll path and stable containment are required (`005.5`, `005.6`, `004.1`).
UI-02: Phone with Grid saved; one active conversation remains.
```text
[Threads / View] [1/2] [Menu]
[Task A v] [Open]
[one transcript ]
[inline required question ]
[scroll to option / Submit ]
[normal composer ]
[safe-area clearance ]
```
Keep swipe/picker navigation and touch targets. Vertical question input reaches
the same bounded footer; there is no desktop Grid mounted behind this surface
(`004.6`, `005.7`, `005.8`). Tests cover a normal Pixel 5 viewport, short 393x500
viewport, and the existing coarse-pointer tablet path.
## Tests
Existing clarification panel/resize, composer disclosure, and transcript resize
unit tests remain the compatibility checks. The regression is rendered browser
scroll behavior, so its RED evidence must come from actual wheel input and hit
geometry, not a class-name assertion or an auto-scrolling locator click.
## E2E tests
| Flow | Acceptance criteria | File/project |
| --- | --- | --- |
| Short Grid, long multi-question bundle, auto-hide on/off; wheel reaches final option and Next; all answers submitted | `005.5`, `005.6`, `005.9` | `threads-composer-disclosure.spec.ts`, chromium |
| Keyboard navigation/activation; Columns compatibility; current CI, history/bottom/frozen scrolling, sibling geometry and disclosure scenarios retained | `004.1`, `005.1`, `005.3`, `005.6`, `005.9`, `005.11` | Same desktop file |
| Phone long question via touch scroll, final-option/Submit hit containment; short viewport and single detail; touch composer/settings preserved | `004.6`, `005.7`, `005.8`, `005.9` | `mobile-threads-composer-disclosure.spec.ts`, mobile-chrome |
| Normal task question/carousel and 50vh drag/reset behavior | `005.9` | `chat/clarification-resize.spec.ts`, chromium |
Keep the existing scenario coverage and add to it. Reuse `/e2e:clarification-multi`
with a second admitted task: one task deliberately fills the available height
and would miss the half-height Grid regression. The current three-question
fixture already exceeds the affected allocation. Shared setup and geometry/
gesture helpers belong in `threads-presentation-helpers.ts` or a narrowly
scoped sibling helper if its size warrants it. Task 01 assigns the shared
question/gesture/native-zoom helper to `threads-clarification-helpers.ts`.
The work order defines exact commands and the native zoom recheck. A viewport
approximation may supplement permanent automation but must be labelled and
cannot be reported as browser zoom. No deviceScaleFactor or CSS zoom substitute
is accepted as proof of the user's 90% condition.
## Work orders
- [x] [Task 01: Restore required-question scrolling](task-01-restore-question-scrolling.md)
One sequential work order; no dependencies beyond the already merged parent.
No delegation is authorized.
## Verification results
Planning evidence only: fresh backend/Vite/plugin fixture build passed;
existing selected-session clarification baseline passed 1 test; the temporary
native-zoom diagnostic passed 1 instrumentation scenario containing four
baseline cases and one causal probe. The first diagnostic attempt failed at
browser configuration before application behavior (inherited deviceScaleFactor
with viewport null); the corrected run used native zoom without that emulation.
See [exact commands/results](evidence.md#validation).
Implementation on refreshed main `4e4b29b29` is complete. The production change
only overrides vertical overscroll behavior when the existing Threads context
is present. All four native-zoom wheel cases failed before the change. Final
coverage passes for all 17 affected desktop cases and five mobile cases, with
46 compatibility unit tests, typecheck, scoped lint, and the i18n ratchet also
passing. Every browser run used one worker, zero retries, and current built
assets. No suites overlapped.
The first GREEN attempt exposed an incorrect test expectation about native
work-start auto-follow; a second exposed a synthetic frozen-scroll setup.
Both test-only corrections, their scoped reruns, exact commands and results
are recorded in [Task 01 Results](task-01-restore-question-scrolling.md#results).
The production correction did not change during those iterations.
[Final geometry](evidence/implementation-geometry.json) records successful
click/tap targets and native browser zoom. Desktop and phone screenshots were
visually checked against the previews above. Isolated runtimes and temporary
native-browser profiles were removed by fixture cleanup.
Internal documentation is updated. Existing public documentation already
describes answering questions inside Threads and independent conversation
scrolling; this repair adds no setting, navigation, label or public contract.
The normal commit-hook receipt and publication state are in the task handoff.
## Risks
- Gating by auto-hide enabled would leave auto-hide-off and phone cases broken.
- Removing containment globally would change other chat hosts. Override only
vertical chaining within Threads; keep the outer footer containment.
- Locator click/focus/scrollIntoView can scroll ancestors and mask this defect.
Require actual gesture input and geometry before activation.
- A large wheel delta may overshoot an option after chaining works. Use bounded
gestures until the entire target is within its visible owner, then click.
- Native zoom has fractional geometry. Use sensible CSS-pixel tolerances and
record actual zoom/viewport; physical trackpad and mobile keyboard behavior
still require hardware validation if a platform-specific failure remains.
@@ -0,0 +1,391 @@
---
id: "01-restore-question-scrolling"
title: "Restore required-question scrolling"
status: done
wave: 1
depends_on: []
plan: "plan.md"
requirements:
- REQ-UI-THREADS-DECK-004
- REQ-UI-THREADS-DECK-005
acceptance_criteria:
- AC-UI-THREADS-DECK-004.1
- AC-UI-THREADS-DECK-004.6
- AC-UI-THREADS-DECK-004.7
- AC-UI-THREADS-DECK-005.1
- AC-UI-THREADS-DECK-005.3
- AC-UI-THREADS-DECK-005.5
- AC-UI-THREADS-DECK-005.6
- AC-UI-THREADS-DECK-005.7
- AC-UI-THREADS-DECK-005.8
- AC-UI-THREADS-DECK-005.9
- AC-UI-THREADS-DECK-005.11
system_design:
- ../../specs/ui/system-design/threads-conversation-deck.md
- ../../specs/ui/system-design/threads-saved-views.md
---
# Task 01: Restore Required-Question Scrolling
## Summary
Allow the existing clarification scroller to chain vertical input through its
clipped wrapper into the bounded Threads footer. Preserve all other question,
composer, transcript, layout, and submission behavior. The user explicitly
authorized implementation on 2026-09-13 after the design handoff.
## In scope
- Use `/tdd` and `/e2e`: add a rendered behavioral RED before production edits,
then the smallest Threads-scoped vertical containment correction.
- In `ClarificationPanelSection`, identify Threads by the existing disclosure
context's presence, including `enabled: false`. Allow vertical chaining on
`clarification-overlay-container`; retain its horizontal behavior and the
outer `ComposerFooterAllocation` bound/containment.
- Add the desktop/phone cases below beside the existing disclosure scenarios;
keep all current test coverage. Record fresh commands, geometry, screenshots,
and results here, and synchronize `plan.md` at completion.
## Out of scope
New scroll owners/handlers, tile sizing or fallback changes, moving/sticking
question actions, composer/disclosure state changes, new backend fixtures or
protocols, translation changes without new copy, normal-chat containment
changes, public-doc redesign, publishing to the parent PR, and delegation.
## Acceptance
1. With two admitted tasks at a 1366x768 content viewport and both auto-hide
settings, Grid remains Grid and wheel input over the question reaches the
final option and Next. Every target is fully within the footer's visible
bounds, its center passes `elementFromPoint`, and real activation succeeds.
Answer all three questions and submit; the pending overlay clears and the
mock agent receives the exact selected answers. Repeat at real 90% browser
zoom, recording the API zoom value and effective CSS viewport.
2. Keyboard-only Tab/Shift+Tab and Enter/Space can select and submit the same
bundle; Next/Back navigation is exercised without implicitly answering.
Neighbor tile boxes and board scroll offsets stay unchanged, the transcript
retains its existing follow/history/frozen policy and 80px floor, and the
existing CI, disclosure, normal task chat, and Columns scenarios still pass.
3. Pixel 5 and short 393x500 phone views retain one active conversation and
an inline visible composer. Real vertical touch input reaches final options
and Submit; tap completes the pending request. Verify actual hit geometry,
option/primary-action touch targets, safe-area/viewport clearance, and no
document horizontal overflow. Retain the existing coarse-pointer tablet
and saved-preference assertions, plus the 767/768px composition boundary.
## ASCII UI preview
UI-01 excerpt from [the combined preview](plan.md#ascii-ui-preview), covering
`005.5`, `005.6`, `004.1`:
```text
+----------------------------+
| Grid tile A [Open] |
| transcript (80px minimum) |
| question / options |
| inner scroll -> footer |
| [last option] [Next] |
+----------------------------+
| Grid tile B: same bounds |
+----------------------------+
```
Keep both tile bounds fixed. The header Submit remains in its existing place;
scroll back to it for final submission. UI-02 phone keeps the existing
composition (`004.6`, `005.7`, `005.8`):
```text
[Threads / View] [1/2] [Menu]
[Task A v] [Open]
[one transcript ]
[inline question / actions ]
[normal composer ]
[safe-area clearance ]
```
Spacing is illustrative. Do not add a drawer, sticky action row, or new phone
navigation. Retain the existing local question scroller and its outer footer
boundary; remove the input trap between them.
## TDD and browser mechanics
Add a test named `scrolls long required questions through the Grid footer`
parameterized by auto-hide true/false and native zoom 1/0.9. Use the existing
`/e2e:clarification-multi` scenario through `createTaskWithAgent`, wait for real
`WAITING_FOR_INPUT`, and create a second admitted task. Keep its native pending
request and sender; do not replace the question with DOM-only options or a
summary-store fake. The one-task layout is full height and misses this bug.
Use a scoped helper in `threads-clarification-helpers.ts` for shared seeding,
geometry/gesture operations and the native-zoom setup when needed. For native
zoom, create an owned temporary Chromium persistent context with `channel:
"chromium"`, `headless: true`, `viewport: null`, and `deviceScaleFactor:
undefined` to override the project's inherited emulation. Create an ephemeral
MV3 extension with only `tabs` permission, call `chrome.tabs.setZoom`, verify
`getZoom`, and record `innerWidth`, `innerHeight`, devicePixelRatio and
visualViewport.scale. Use `--window-size=1366,855` for the verified 1366x768
baseline content area. This produced 1517x853 at 90% on this host. Reuse the
same isolated fixture backend; close the browser and delete only its temporary
profile/extension in `finally`. No shared Playwright config or dependencies
need changing. The [evidence recipe](evidence.md#native-zoom) records the API
and setup details. If a runner lacks regular Chromium, report that limitation
and retain the 100% behavioral gate; never silently call an approximation real
zoom or skip all scroll coverage.
The RED assertion must fail because the outer footer stays at scrollTop 0 and
the final target remains clipped after genuine wheel events over the question.
Use small bounded wheel deltas and recompute the target's full containment,
not only its center; a working scroll chain can overshoot with a large delta.
Do not use `scrollIntoView`, direct scrollTop assignment, `.focus()`, or an
auto-scrolling locator `.click()` to make an unreachable option pass. Read
geometry and use `page.mouse` before activation. After proven containment and
hit-testing, activate the option and assert the actual carousel/backend result.
Exercise Next and Back before completing all answers, then scroll to the
existing header Submit. Wait for the agent's answer receipt/pending-clear
state, not only disappearance caused by navigation.
Add `answers a long required question with keyboard navigation` and
`keeps long required answers usable in Columns` in the desktop file. Native
keyboard focus may scroll the footer, which is existing useful behavior;
verify selection and submission, not just focusability. Preserve all current
CI, history/bottom/frozen-scroll, draft, cancellation, and session-ownership
scenarios. For the new gesture case record sibling boxes and board offsets
before/after question scroll and submission. Preserve reader anchoring checks
with a transcript taller than the viewport.
Add `scrolls and submits a long required question by touch` in the existing
mobile disclosure file. Use the mobile-chrome project's Pixel 5 device; do not
replace it with desktop dimensions/deviceScaleFactor. Use real CDP touch
start/move/end for vertical scrolling and `.tap()` for actions. Follow the
cleanup pattern in `mobile-threads-swipe-helpers.ts`; always end the gesture
and detach CDP in `finally`. Cover canonical/short phone sizes, 767/768px
composition, and the existing 900px coarse-pointer tablet path without
changing saved Grid/auto-hide preferences. Assert Submit/option targets are
at least 44px, full containment, center hit-test, single detail, and successful
submission. Native hardware keyboard/safe-area behavior remains a separately
labelled limitation of emulation.
## Verification
From the repository root. This workspace already ran the frozen install;
a fresh worktree must first run `(cd apps && pnpm install --frozen-lockfile)`.
Run one command at a time. No retries, additional workers, or overlapping full
suites. Build after production changes; reuse only those just-built assets.
The lean host target suffices for these local/worktree executor tests and
avoids rebuilding unrelated remote-platform helpers on a cold machine.
```bash
(cd apps/web && pnpm exec vitest run components/task/chat/clarification-panel-section.test.tsx hooks/use-resizable-clarification-overlay.test.ts components/task/chat/composer-disclosure.test.tsx components/task/chat/use-composer-disclosure.test.ts components/task/chat/transcript-viewport-resize.test.ts)
(cd apps/web && pnpm run typecheck)
(cd apps/web && pnpm exec eslint components/task/chat/clarification-panel-section.tsx e2e/tests/task/threads-composer-disclosure.spec.ts e2e/tests/task/mobile-threads-composer-disclosure.spec.ts e2e/tests/task/threads-clarification-helpers.ts)
(cd apps/web && pnpm run i18n:ratchet)
make -C apps/backend build-dev e2e-plugin-package
make build-web-e2e
(cd apps/web && pnpm e2e:raw --project=chromium e2e/tests/task/threads-composer-disclosure.spec.ts e2e/tests/chat/clarification-resize.spec.ts --list --workers=1 --retries=0)
(cd apps/web && pnpm e2e:run --host --no-build --shards 1 --project chromium tests/task/threads-composer-disclosure.spec.ts tests/chat/clarification-resize.spec.ts --workers=1 --retries=0)
(cd apps/web && pnpm e2e:raw --project=mobile-chrome e2e/tests/task/mobile-threads-composer-disclosure.spec.ts --list --workers=1 --retries=0)
(cd apps/web && pnpm e2e:run --host --no-build --shards 1 --project mobile-chrome tests/task/mobile-threads-composer-disclosure.spec.ts --workers=1 --retries=0)
git diff --check
git status --short -- docs/plans/threads-grid-clarification-overflow
```
Before the production correction, run the new desktop regression against the
current built assets using the same managed command with `--grep 'scrolls long
required questions through the Grid footer'`. Record its behavioral RED, then
rebuild and run the complete command block for GREEN. List/discovery counts,
actual zoom/viewport, geometry, input mode, and all failed setup attempts belong
in Results. Do not claim the planning-stage DOM probe as GREEN.
## Files likely touched
- `apps/web/components/task/chat/clarification-panel-section.tsx`.
- `apps/web/e2e/tests/task/threads-composer-disclosure.spec.ts`.
- `apps/web/e2e/tests/task/mobile-threads-composer-disclosure.spec.ts`.
- New `apps/web/e2e/tests/task/threads-clarification-helpers.ts`, using existing
`threads-presentation-helpers.ts`, clarification/session API helpers and
native mobile gesture conventions.
- Existing clarification panel unit test only if needed to cover an added
behavioral seam; browser tests own the layout regression.
- This plan, work order, and their evidence/results artifacts.
No changes to `TaskChatPanel`, `ComposerFooterAllocation`, disclosure CSS,
transcript scroll owners, backend, locale catalogs, or shared configuration
are expected. Reassess the plan if evidence requires any of those boundaries
to change materially.
## Dependencies
None. Verified base: `d62daa9d70437e1a46f7dd31f6138f90ebd54b3f`; parent PR
#3626 is merged at `f718c50666eb7175e39087294afabc70ac39f3a7`. Reconcile current
main and preserve concurrent edits before implementation/delivery. Work only
in this task workspace and its branch.
## Risks
A context `enabled` check would miss auto-hide off and phones. Global overscroll
changes could alter normal task chat. Focus/locator auto-scrolling can mask RED.
Native zoom has fractional CSS bounds and inherits Playwright context defaults;
keep that setup explicit. Never touch developer :9998 or parent playground
:48431; fixture teardown owns only its isolated backend/data/browser.
## Parallelism
`sequential`; no delegated agents or platform task/session creation authorized.
## Inputs
- [Plan and mobile contract](plan.md).
- [Measured reproduction and causal probe](evidence.md).
- Existing deck requirements: `004` layout and `005` composer criteria above.
- Deck system design: Composer disclosure, Composer geometry, Responsive behavior.
- Saved-view system design: Presentation preferences (preserve only).
- Reviewed Threads layouts Tasks 02, 03, 06 and current scoped AGENTS.md.
## Results
Implementation and task-defined verification are complete. Refreshed main is
`4e4b29b29e7680c0ed6c5de003a8dfe7e8b2fb94`; its one intervening Office fix has no
overlap with this work. The unpushed documentation commit was rebased onto that
main before edits. Planning evidence remains in `evidence.md`.
### Change and conformance
`ClarificationPanelSection` now overrides only `overscrollBehaviorY` to `auto`
when `useComposerDisclosureContext()` is non-null. This covers auto-hide on,
auto-hide off, Columns and touch Threads; normal task/Quick Chat keep their
existing containment. No height limit, resize behavior, disclosure state,
transcript owner, question action, or phone composition changed.
Permanent coverage adds four native-zoom Grid cases, one keyboard case, one
Columns case and two phone touch cases. Existing scenarios remain intact.
The normal-chat resize spec's local `retries: 1` override was removed so the
explicit zero-retry run applies to all affected cases.
`AC-UI-THREADS-DECK-005.5` and `005.6` are restored. All mapped preservation
criteria use the existing deck requirements and design; no new AC or contract
was introduced. Internal docs are updated; public docs need no change because
the existing independent-scroll and in-Threads answer behavior is restored.
### Validation receipt
The frozen install from diagnosis remained valid: the intervening main commit
changed neither package manifest nor lockfile. Commands ran from the locations
specified in Verification. Every browser run was headless, strict WS, one
worker and retries zero; no browser suites overlapped.
| Check | Result |
| --- | --- |
| Five named unit files | 5 files, 46 tests passed (17.23s) |
| `pnpm run typecheck` | Passed |
| Named scoped ESLint command, also including `e2e/tests/chat/clarification-resize.spec.ts` | Passed; rerun on the final changed test code |
| `pnpm run i18n:ratchet` | Passed, one modified production file clean; 644 guard entries intact |
| `make -C apps/backend build-dev e2e-plugin-package` | Passed on refreshed main |
| `make build-web-e2e` | Passed; Vite completed in 9.13s after the final production edit |
| Desktop discovery | 17 tests in 2 files |
| Desktop behavioral RED, Grid grep | All 4 failed at full-containment/hit assertions after actual wheel input |
| First full desktop GREEN attempt | 13 passed, 4 failed (4.7m); all question submissions succeeded, but the added post-submit history assertion contradicted native work-start auto-follow |
| Six-case desktop correction/capture run | 4 passed, 2 failed (1.7m); synthetic scrollTop after disabling auto-scroll did not update the native frozen offset |
| Final four-case Grid rerun | 4 passed (1.2m); uses real transcript wheel input, preserves history before Submit, verifies follow when enabled and the frozen offset when disabled |
| Mobile discovery | 5 tests in 1 file |
| Documentation catalog/spec lint and diff checks | Passed: 267 decisions, 873 specifications; all spec files clean |
| Full mobile command with `CAPTURE_PR_ASSETS=1` | 5 passed (1.2m), including both new touch cases |
The full desktop command is the one listed in Verification. Its 11 unchanged
compatibility cases all passed. The two new keyboard/Columns cases passed in
both the full run and the six-case rerun. Only the four edited Grid cases
needed the last rerun. Thus all 17 affected desktop cases pass on the final
production implementation, with every changed regression run after its last
edit. Exact supplemental commands, from `apps/web`:
```bash
pnpm e2e:run --host --no-build --shards 1 --project chromium tests/task/threads-composer-disclosure.spec.ts --grep 'scrolls long required questions through the Grid footer' --workers=1 --retries=0
CAPTURE_PR_ASSETS=1 pnpm e2e:run --host --no-build --shards 1 --project chromium tests/task/threads-composer-disclosure.spec.ts --grep 'scrolls long required questions|answers a long required question|keeps long required answers' --workers=1 --retries=0
CAPTURE_PR_ASSETS=1 pnpm e2e:run --host --no-build --shards 1 --project chromium tests/task/threads-composer-disclosure.spec.ts --grep 'scrolls long required questions' --workers=1 --retries=0
CAPTURE_PR_ASSETS=1 pnpm e2e:run --host --no-build --shards 1 --project mobile-chrome tests/task/mobile-threads-composer-disclosure.spec.ts --workers=1 --retries=0
```
### Final behavior and geometry
The [raw geometry](evidence/implementation-geometry.json) records each actual
activation. Chromium `149.0.7827.55` used a 1366x768 content viewport at native
zoom 1 and 1517x853 at native zoom 0.9, with visualViewport scale 1. Native tab
zoom was verified through `chrome.tabs.getZoom`; device scale and CSS zoom
were not substitutes.
The Grid footer remains 198.50 CSS px at 100% and 240.87 CSS px at 90%; the
transcript retains its 80px floor. Every final option, Next, Back and Submit
had its full box inside all clipping ancestors and passed center hit-testing
before mouse activation. The agent received exactly `db=q1_opt3`,
`language=q2_opt3`, `deploy=q3_opt2`; the pending overlay cleared. Tab,
Shift+Tab, Enter and Space completed the same bundle in the keyboard case.
Neighbor boxes and deck scroll offsets remained unchanged.
Pixel 5 (393x727 content viewport, 393x851 emulated screen) and short 393x500
phone emulation completed that same exchange
with CDP touch start/move/end and real taps. Option rows measured 56.5px high,
Submit 44px; full containment and real hits passed. One active conversation,
the visible composer, viewport clearance, no document horizontal overflow,
the 767/768px transition and saved Grid/auto-hide preferences passed. Existing
900px coarse-pointer tablet, draft, model, attachment and cancellation cases
also passed. Screenshots were inspected against UI-01/UI-02.
Phone verification uses emulation's safe-area values and a resized viewport;
it does not simulate an OS keyboard, physical display cutout, Safari, or a
hardware trackpad. No failure remains in the exercised browser paths.
### Isolation and teardown
The final Grid run owned `/tmp/kandev-e2e-0-JmMopy`; the phone run owned
`/tmp/kandev-e2e-0-CdFlQo`. Both used the worker's isolated port 18100 and were
fully removed by fixture teardown; absence checks passed. While such a fixture
owns that port, the exact fallback is `scripts/kandev-kill 18100 --yes`.
Do not apply that historical command to a future occupant. Temporary
`threads-question-zoom-*` profiles/extensions close in `finally`; none remain.
Developer :9998 and parent playground :48431 were never touched.
Screenshots are retained on an orphan media commit, rather than as binaries in
the fix branch. Final rendered examples: [Grid at 90%](https://raw.githubusercontent.com/kdlbs/kandev/e0c9e9c77703f7a2efc2729f5c26fb883c29be0a/grid-90-last-option.png), [Grid Submit](https://raw.githubusercontent.com/kdlbs/kandev/e0c9e9c77703f7a2efc2729f5c26fb883c29be0a/grid-100-submit.png), [short phone](https://raw.githubusercontent.com/kdlbs/kandev/e0c9e9c77703f7a2efc2729f5c26fb883c29be0a/phone-short-last-option.png), [phone Submit](https://raw.githubusercontent.com/kdlbs/kandev/e0c9e9c77703f7a2efc2729f5c26fb883c29be0a/phone-submit.png).
### PR review correction
PR #3655's documentation finding corrected the parent package's stale
"pending results" reference to "completed results", matching this plan's
implemented status. The historical parent evidence remains intact. This
documentation-only correction passed catalog validation, specification lint
and diff checks; production code, tests and screenshots are unchanged.
### Compatibility with concurrent main
Main advanced to `89bf7657a28fd1ed58452d57abe631c175e08a05` with provider
diagnostics changes. No files overlap this fix, but the agent lifecycle is a
shared contract. PR head `d1ad7d66ce5eaf721455640ce7f513735af3e353` and that
base produced conflict-free synthetic merge
`d72049ea50c95fe7c78a28638aad69f61b498f00`, tested in a detached disposable
worktree. The frontend runtime, packages, manifest and lockfile are identical
to the tested fix; the only additional web change is an upstream transport
recovery E2E spec. The PR branch was not rebased or merged.
After `cd apps && pnpm install --frozen-lockfile` (903 packages, 4.1s), current
backend/plugin and Vite assets were built. The first managed build stopped
before tests because an existing `/tmp/.git` marker confused Go's VCS lookup.
An environment-only override was replaced by an explicit Make override, since
the Makefile owns `GOFLAGS`. The successful commands from the disposable root
were `make -C apps/backend GOFLAGS='-v -buildvcs=false' build-dev e2e-plugin-package`
and `make build-web-e2e` (8.41s). Only optional Go VCS metadata was disabled;
the application's build version/commit still identified the synthetic merge.
Sequential commands from its `apps/web`, each using current assets, strict WS,
one worker and zero retries:
```bash
E2E_PORT_OFFSET=20 pnpm e2e:run --host --no-build --shards 1 --project chromium tests/task/threads-composer-disclosure.spec.ts --grep 'zoom 0.9' --workers=1 --retries=0
E2E_PORT_OFFSET=20 pnpm e2e:run --host --no-build --shards 1 --project mobile-chrome tests/task/mobile-threads-composer-disclosure.spec.ts --grep 'short phone' --workers=1 --retries=0
```
Both native 90% Grid cases passed (32.4s total), followed by the short-phone
touch/submission case (17.9s total). Fixture roots
`/tmp/kandev-e2e-0-yU6fwf` and `/tmp/kandev-e2e-0-4CMTkQ` were removed by teardown.
The separate evidence correction records Pixel 5's actual 393x727 viewport
and 393x851 screen, verified against its PNG dimensions at device scale 2.75;
all 36 recorded activation targets remain fully contained and hit-test true.
+6 -2
View File
@@ -21,8 +21,12 @@ conversation at a time with swipe/picker navigation and the normal composer.
The user approved implementation on 2026-09-11. The initial five orders are done.
The scoped polish requested on 2026-09-12 is also complete in Task 06.
The feature is committed and open for review in PR #3626. Review remediation
retains the existing implementation boundaries and is recorded below.
The feature merged in PR #3626 on 2026-09-13 at
`f718c50666eb7175e39087294afabc70ac39f3a7`. Review remediation retains the
existing implementation boundaries and is recorded below. The focused
[Grid clarification overflow follow-up](../threads-grid-clarification-overflow/plan.md)
records a later conformance repair; its completed results do not replace this
package's historical evidence.
Read the previews first, then the work order
for the implementation boundary being changed.