fix(threads): allow scrolling through long clarification footers (#3655)
This commit is contained in:
@@ -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.
|
||||
|
||||

|
||||
|
||||
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.
|
||||
|
||||

|
||||
|
||||
## 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.
|
||||
@@ -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.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user