fix(web): keep composer menus above mobile keyboards (#2988)
This commit is contained in:
@@ -50,6 +50,32 @@ describe("popup menu geometry", () => {
|
||||
expect(style.transform).toBe("translateY(-100%)");
|
||||
});
|
||||
|
||||
// @covers AC-UI-COMPOSER-OVERLAY-001.1
|
||||
it("clamps an above-composer menu to a software-keyboard visual viewport", () => {
|
||||
const style = computePopupMenuStyle({
|
||||
position: { x: 16, y: 560 },
|
||||
placement: "above",
|
||||
viewport: { offsetLeft: 0, offsetTop: 0, width: 393, height: 420 },
|
||||
});
|
||||
|
||||
expect(style.top).toBe(412);
|
||||
expect(Number(style.top) - Number(style.maxHeight)).toBeGreaterThanOrEqual(8);
|
||||
});
|
||||
|
||||
it("preserves ordinary below-caret placement", () => {
|
||||
const style = computePopupMenuStyle({
|
||||
position: { x: 100, y: 100 },
|
||||
placement: "below",
|
||||
viewport: { offsetLeft: 0, offsetTop: 0, width: 1200, height: 800 },
|
||||
});
|
||||
|
||||
expect(style.top).toBe(108);
|
||||
expect(style.maxHeight).toBe(280);
|
||||
expect(style.transform).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("popup menu viewport updates", () => {
|
||||
it("reflows while the mobile visual viewport changes", () => {
|
||||
const viewport = Object.assign(new EventTarget(), {
|
||||
offsetLeft: 0,
|
||||
@@ -61,6 +87,7 @@ describe("popup menu geometry", () => {
|
||||
render(
|
||||
<PopupMenu
|
||||
isOpen
|
||||
testId="reflow-menu"
|
||||
position={null}
|
||||
clientRect={() => new DOMRect(16, 240, 1, 20)}
|
||||
title="References"
|
||||
@@ -70,15 +97,47 @@ describe("popup menu geometry", () => {
|
||||
Result
|
||||
</PopupMenu>,
|
||||
);
|
||||
const menu = screen.getByText("References").parentElement?.parentElement;
|
||||
expect(menu?.style.width).toBe("344px");
|
||||
const menu = screen.getByTestId("reflow-menu");
|
||||
expect(menu.style.width).toBe("344px");
|
||||
|
||||
act(() => {
|
||||
viewport.width = 300;
|
||||
viewport.dispatchEvent(new Event("resize"));
|
||||
});
|
||||
|
||||
expect(menu?.style.width).toBe("284px");
|
||||
expect(menu.style.width).toBe("284px");
|
||||
});
|
||||
|
||||
// @covers AC-UI-COMPOSER-OVERLAY-001.2
|
||||
it("reflows an open menu above the software keyboard", () => {
|
||||
const viewport = Object.assign(new EventTarget(), {
|
||||
offsetLeft: 0,
|
||||
offsetTop: 0,
|
||||
width: 393,
|
||||
height: 600,
|
||||
});
|
||||
Object.defineProperty(window, "visualViewport", { configurable: true, value: viewport });
|
||||
render(
|
||||
<PopupMenu
|
||||
isOpen
|
||||
testId="reflow-menu"
|
||||
position={{ x: 16, y: 560 }}
|
||||
title="References"
|
||||
selectedIndex={0}
|
||||
onClose={() => undefined}
|
||||
>
|
||||
Result
|
||||
</PopupMenu>,
|
||||
);
|
||||
const menu = screen.getByTestId("reflow-menu");
|
||||
expect(menu.style.top).toBe("552px");
|
||||
|
||||
act(() => {
|
||||
viewport.height = 420;
|
||||
viewport.dispatchEvent(new Event("resize"));
|
||||
});
|
||||
|
||||
expect(menu.style.top).toBe("412px");
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -27,16 +27,20 @@ export function computePopupMenuStyle(args: {
|
||||
const minLeft = args.viewport.offsetLeft + margin;
|
||||
const maxLeft = Math.max(minLeft, viewportRight - width - margin);
|
||||
const left = Math.min(Math.max(minLeft, args.position.x), maxLeft);
|
||||
const minVerticalEdge = args.viewport.offsetTop + margin;
|
||||
const maxVerticalEdge = Math.max(minVerticalEdge, viewportBottom - margin);
|
||||
const requestedVerticalEdge =
|
||||
args.placement === "above" ? args.position.y - margin : args.position.y + margin;
|
||||
const verticalEdge = Math.min(Math.max(minVerticalEdge, requestedVerticalEdge), maxVerticalEdge);
|
||||
const availableHeight =
|
||||
args.placement === "above"
|
||||
? Math.max(0, args.position.y - args.viewport.offsetTop - margin * 2)
|
||||
: Math.max(0, viewportBottom - args.position.y - margin * 2);
|
||||
? Math.max(0, verticalEdge - args.viewport.offsetTop - margin)
|
||||
: Math.max(0, viewportBottom - verticalEdge - margin);
|
||||
const maxHeight = Math.min(MENU_HEIGHT, availableHeight);
|
||||
const top = args.placement === "above" ? args.position.y - margin : args.position.y + margin;
|
||||
return {
|
||||
position: "fixed",
|
||||
left,
|
||||
top,
|
||||
top: verticalEdge,
|
||||
width,
|
||||
maxWidth: width,
|
||||
maxHeight,
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
import { type Page } from "@playwright/test";
|
||||
import { test, expect, type SeedData } from "../../fixtures/test-base";
|
||||
import type { ApiClient } from "../../helpers/api-client";
|
||||
import { SessionPage } from "../../pages/session-page";
|
||||
|
||||
const PROMPT_NAME = "mobile-prompt-mention-viewport";
|
||||
const PROMPT_CONTENT = "Mobile custom prompt content";
|
||||
|
||||
async function openReadyTask(page: Page, apiClient: ApiClient, seedData: SeedData) {
|
||||
const task = await apiClient.createTaskWithAgent(
|
||||
seedData.workspaceId,
|
||||
"Mobile Prompt Mention Viewport",
|
||||
seedData.agentProfileId,
|
||||
{
|
||||
description: "/e2e:simple-message",
|
||||
workflow_id: seedData.workflowId,
|
||||
workflow_step_id: seedData.startStepId,
|
||||
repository_ids: [seedData.repositoryId],
|
||||
},
|
||||
);
|
||||
await page.goto(`/t/${task.id}`);
|
||||
const session = new SessionPage(page);
|
||||
await session.waitForLoad();
|
||||
await session.waitForChatIdle({ timeout: 30_000 });
|
||||
return session;
|
||||
}
|
||||
|
||||
// @covers AC-UI-COMPOSER-OVERLAY-001.1
|
||||
// @covers AC-UI-COMPOSER-OVERLAY-001.3
|
||||
// @covers AC-UI-COMPOSER-OVERLAY-001.4
|
||||
// @covers AC-UI-COMPOSER-OVERLAY-001.5
|
||||
test("keeps custom prompts directly above a keyboard-resized mobile composer", async ({
|
||||
testPage,
|
||||
apiClient,
|
||||
seedData,
|
||||
prCapture,
|
||||
}) => {
|
||||
const prompt = await apiClient.createPrompt(PROMPT_NAME, PROMPT_CONTENT);
|
||||
try {
|
||||
const session = await openReadyTask(testPage, apiClient, seedData);
|
||||
const editor = await session.composerReady();
|
||||
await editor.tap();
|
||||
await editor.fill("");
|
||||
const viewport = testPage.viewportSize();
|
||||
expect(viewport).not.toBeNull();
|
||||
// Playwright cannot open an OS keyboard. Resize the page itself so the
|
||||
// mobile layout and focused composer reflow into the reduced visible area.
|
||||
// Overriding visualViewport alone would describe the composer as occluded.
|
||||
await testPage.setViewportSize({ width: viewport!.width, height: 420 });
|
||||
await editor.pressSequentially("@mobile-prompt");
|
||||
|
||||
const menu = testPage.getByRole("listbox", { name: /Mention tasks, files, prompts/i });
|
||||
const option = menu.getByRole("option").filter({ hasText: PROMPT_NAME });
|
||||
await expect(menu).toBeVisible();
|
||||
await expect(option).toBeVisible();
|
||||
|
||||
const [menuBox, composerBox] = await Promise.all([
|
||||
menu.locator("..").boundingBox(),
|
||||
session.activeChat().getByTestId("chat-input-editor-shell").boundingBox(),
|
||||
]);
|
||||
expect(menuBox).not.toBeNull();
|
||||
expect(composerBox).not.toBeNull();
|
||||
expect(Math.abs(menuBox!.y + menuBox!.height - composerBox!.y)).toBeLessThanOrEqual(8);
|
||||
|
||||
const geometry = await menu.evaluate((listbox) => {
|
||||
const surface = listbox.parentElement;
|
||||
const rect = surface?.getBoundingClientRect();
|
||||
return {
|
||||
top: rect?.top ?? -1,
|
||||
bottom: rect?.bottom ?? -1,
|
||||
viewportTop: window.visualViewport?.offsetTop ?? 0,
|
||||
viewportBottom: window.visualViewport
|
||||
? window.visualViewport.offsetTop + window.visualViewport.height
|
||||
: window.innerHeight,
|
||||
rowHeight: listbox.querySelector<HTMLElement>("[role='option']")?.getBoundingClientRect()
|
||||
.height,
|
||||
hasHorizontalOverflow:
|
||||
document.documentElement.scrollWidth > document.documentElement.clientWidth,
|
||||
};
|
||||
});
|
||||
expect(geometry.top).toBeGreaterThanOrEqual(geometry.viewportTop);
|
||||
expect(geometry.bottom).toBeLessThanOrEqual(geometry.viewportBottom + 1);
|
||||
expect(geometry.rowHeight).toBeGreaterThanOrEqual(44);
|
||||
expect(geometry.hasHorizontalOverflow).toBe(false);
|
||||
|
||||
await prCapture.screenshot("mobile-composer-prompt-menu", {
|
||||
caption:
|
||||
"Saved-prompt suggestions stay attached directly above the keyboard-resized composer",
|
||||
});
|
||||
|
||||
await option.tap();
|
||||
await expect(menu).toHaveCount(0);
|
||||
await expect(editor).toContainText(PROMPT_NAME);
|
||||
await expect(editor).toBeFocused();
|
||||
} finally {
|
||||
await apiClient.deletePrompt(prompt.id).catch(() => undefined);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,198 @@
|
||||
---
|
||||
spec: docs/specs/ui/requirements/composer-suggestion-overlays.md
|
||||
system_design: docs/specs/ui/system-design/composer-suggestion-overlays.md
|
||||
created: 2026-08-23
|
||||
status: implemented
|
||||
---
|
||||
|
||||
# Implementation Plan: Keep Mobile Composer Suggestions Visible
|
||||
|
||||
## Overview
|
||||
|
||||
Repair the shared popup geometry used by composer suggestion menus. A normal
|
||||
Pixel-sized browser run proved that typing `@mobile-prompt` opens the saved
|
||||
prompt menu and touch selection inserts the prompt chip. A second run modeled a
|
||||
software keyboard by shrinking `window.visualViewport.height` to 420 CSS
|
||||
pixels. The menu still rendered with its bottom at 551 pixels, leaving 131
|
||||
pixels behind the keyboard. The focused browser assertion failed as expected.
|
||||
The throwaway diagnostic spec was removed after recording this evidence.
|
||||
|
||||
The root cause is in `computePopupMenuStyle`. It constrains width and maximum
|
||||
height with visual-viewport dimensions, but it derives the vertical anchor from
|
||||
the unchanged layout-viewport caret coordinate. The existing visual-viewport
|
||||
resize listener therefore rerenders the same off-screen vertical position.
|
||||
|
||||
The default above placement is shared by chat `@` mentions (including custom
|
||||
prompts), `/` agent commands, `#` entity references, and the shared task/agent
|
||||
prompt composer. The plan editor's slash menu shares `PopupMenu` but requests
|
||||
below placement; it needs a geometry regression, not new behavior.
|
||||
|
||||
PR screenshot feedback exposed a separate test-model defect. The browser test
|
||||
replaced only `window.visualViewport.height`, so it declared the still-rendered
|
||||
composer occluded without causing layout reflow. The shared popup correctly
|
||||
used its containment fallback at the visible bottom edge, producing a screenshot
|
||||
with a 136-pixel gap to the composer. Task 02 replaces that impossible visual
|
||||
state with an actual keyboard-sized page viewport and makes direct
|
||||
menu-to-composer adjacency a permanent browser assertion.
|
||||
|
||||
## Requirement coverage
|
||||
|
||||
| Acceptance criterion | Work order coverage |
|
||||
| --- | --- |
|
||||
| `AC-UI-COMPOSER-OVERLAY-001.1` | Unit geometry regression and mobile saved-prompt E2E in Task 01 |
|
||||
| `AC-UI-COMPOSER-OVERLAY-001.2` | Visual-viewport resize component regression in Task 01 |
|
||||
| `AC-UI-COMPOSER-OVERLAY-001.3` | Phone geometry, row-height, and overflow assertions in Task 01 |
|
||||
| `AC-UI-COMPOSER-OVERLAY-001.4` | Touch insertion and focus assertions plus sibling mobile suites in Task 01 |
|
||||
| `AC-UI-COMPOSER-OVERLAY-001.5` | Keyboard-sized layout and menu-to-composer bounding-box assertion in Task 02 |
|
||||
|
||||
## Frontend
|
||||
|
||||
- Add a failing `computePopupMenuStyle` regression where an above-placement
|
||||
anchor remains below a shrunken visual viewport.
|
||||
- Normalize the rendered vertical edge against the current visual viewport
|
||||
before calculating available height.
|
||||
- Preserve the existing eight-pixel inset, 420-pixel focused width, 280-pixel
|
||||
height cap, short-list bottom anchoring, and body portal.
|
||||
- Keep the existing window and visual-viewport event subscriptions. No new
|
||||
hook, store state, or dependency is required.
|
||||
- Add ordinary above- and below-placement cases so the fix cannot shift desktop
|
||||
menus or the plan editor's below-caret menu.
|
||||
- Do not change mention queries, custom-prompt loading, entity-reference search,
|
||||
agent command handling, localization, or insertion logic.
|
||||
|
||||
## Tests
|
||||
|
||||
- Extend `apps/web/components/task/chat/popup-menu.test.tsx` first. Link the new
|
||||
cases to `AC-UI-COMPOSER-OVERLAY-001.1` and
|
||||
`AC-UI-COMPOSER-OVERLAY-001.2` in nearby comments.
|
||||
- Prove that a raw anchor at `y=560` with a 420-pixel visual viewport yields a
|
||||
rendered above-menu bottom no lower than the padded viewport bottom and a
|
||||
maximum height calculated from that normalized edge.
|
||||
- Preserve the current offset-viewport, focused-width, short-list anchor, and
|
||||
resize cases. Add a below-placement assertion for unchanged ordinary
|
||||
geometry.
|
||||
|
||||
## Mobile E2E
|
||||
|
||||
- Add
|
||||
`apps/web/e2e/tests/chat/mobile-prompt-mention-composer.spec.ts` using the
|
||||
managed E2E fixture and `mobile-chrome` project.
|
||||
- Create a uniquely named custom prompt through the API, open a ready task, and
|
||||
activate the real TipTap composer.
|
||||
- Keep the raw off-screen-anchor condition in deterministic geometry coverage.
|
||||
- For reviewer-visible browser evidence, resize the page to a keyboard-sized
|
||||
height so the mobile layout and composer reflow together, then type a matching
|
||||
`@` query.
|
||||
- Assert the popup surface is directly adjacent to the composer and inside the
|
||||
visual viewport, its result row is at least 44 pixels high, and the document
|
||||
has no horizontal overflow.
|
||||
- Tap the saved prompt and assert that the menu closes, the prompt appears in
|
||||
the draft, the composer retains focus, and no message is sent.
|
||||
- Delete the created prompt in `finally` so the test remains isolated.
|
||||
- Run the existing mobile entity-reference, slash-command, and task-create
|
||||
mention-menu scenarios as shared-consumer regressions.
|
||||
|
||||
## Backend
|
||||
|
||||
No backend, API, persistence, or migration change is required. The diagnostic
|
||||
run proved that the custom-prompt data reaches the menu when geometry is
|
||||
visible.
|
||||
|
||||
## Verification commands
|
||||
|
||||
Run the focused unit test during both red and green TDD steps:
|
||||
|
||||
```bash
|
||||
cd apps/web && pnpm test -- components/task/chat/popup-menu.test.tsx
|
||||
```
|
||||
|
||||
Run the rebuilt phone-browser scenarios after the unit regression passes:
|
||||
|
||||
```bash
|
||||
cd apps/web && pnpm e2e:run --project mobile-chrome \
|
||||
tests/chat/mobile-prompt-mention-composer.spec.ts \
|
||||
tests/chat/mobile-entity-reference-composer.spec.ts \
|
||||
tests/chat/mobile-slash-command-composer.spec.ts \
|
||||
tests/task/mobile-task-create-escape.spec.ts
|
||||
```
|
||||
|
||||
Run frontend static checks after implementation:
|
||||
|
||||
```bash
|
||||
cd apps/web && pnpm run typecheck
|
||||
cd apps && pnpm --filter @kandev/web lint
|
||||
```
|
||||
|
||||
## Verification results
|
||||
|
||||
- Unit RED: `pnpm test -- components/task/chat/popup-menu.test.tsx` failed the
|
||||
two new viewport regressions as expected (2 failed, 9 passed). Both received
|
||||
the stale `552px` top instead of the contained `412px` top.
|
||||
- Browser RED: the new `mobile-chrome` saved-prompt scenario failed with popup
|
||||
bottom `551px` beyond the simulated visual-viewport limit of `421px`.
|
||||
- Unit GREEN: the focused popup-menu suite passed (11 tests).
|
||||
- Browser GREEN: the rebuilt saved-prompt scenario passed (1 test), including
|
||||
visual-viewport containment, a 44-pixel touch row, no document horizontal
|
||||
overflow, touch insertion, and focus retention.
|
||||
- Shared-consumer GREEN: the rebuilt `mobile-chrome` command covering the new
|
||||
`@` scenario, existing `#` entity-reference scenario, existing `/` command
|
||||
scenario, and task-create mention-menu scenarios passed (5 tests).
|
||||
- `pnpm run typecheck` passed.
|
||||
- Full web lint initially reported the popup test callback over its line limit.
|
||||
Splitting geometry and viewport-update groups resolved it; the final full web
|
||||
lint passed with zero warnings.
|
||||
- PR review remediation qualified the legacy entity-reference anchoring text so
|
||||
it matches the authoritative off-screen-anchor clamp. Specification lint
|
||||
passed afterward.
|
||||
- Follow-up browser RED: the new adjacency assertion measured a 136-pixel gap
|
||||
when only `visualViewport.height` changed and the composer remained below the
|
||||
declared visible area.
|
||||
- Follow-up GREEN: resizing the page itself to 420 pixels reflowed the mobile
|
||||
layout and composer together. The saved-prompt surface remained within eight
|
||||
pixels of the composer while retaining viewport, touch-size, overflow,
|
||||
insertion, and focus coverage.
|
||||
- Follow-up shared-consumer run passed all five targeted mobile `@`, `#`, `/`,
|
||||
and task-create scenarios. The focused popup suite passed 11 tests; typecheck,
|
||||
full web lint, and specification lint also passed.
|
||||
- Fresh phone evidence was captured and visually inspected at
|
||||
`apps/web/.pr-assets/mobile-prompt-mention-composer--mobile-composer-prompt-menu.png`.
|
||||
|
||||
## Implementation wave
|
||||
|
||||
Execution stays in the primary conversation.
|
||||
|
||||
Wave 1:
|
||||
|
||||
- [x] [Task 01: Clamp composer suggestions to the visual viewport](task-01-clamp-composer-suggestions.md)
|
||||
|
||||
Wave 2:
|
||||
|
||||
- [x] [Task 02: Preserve visible composer adjacency](task-02-preserve-composer-adjacency.md)
|
||||
|
||||
No task is parallel-safe, and this plan does not authorize subagents.
|
||||
|
||||
## Risks
|
||||
|
||||
- Computing maximum height from the raw anchor after clamping only `top` would
|
||||
leave the surface internally inconsistent. Both values must use the same
|
||||
normalized edge.
|
||||
- Replacing the existing transform with a fixed full-height box would regress
|
||||
short-result bottom anchoring.
|
||||
- Browser E2E runners do not open a real operating-system keyboard. Raw
|
||||
visual-viewport occlusion stays in deterministic geometry coverage; visual
|
||||
browser evidence must resize page layout so the real composer and popup
|
||||
reflow together.
|
||||
- A shared primitive change can affect the plan editor's below placement even
|
||||
though the reported menus use above placement. Focused below-path coverage is
|
||||
required.
|
||||
|
||||
## Public documentation
|
||||
|
||||
None. The fix restores expected responsive behavior and changes no public
|
||||
command, configuration key, API, or workflow.
|
||||
|
||||
## Decisions
|
||||
|
||||
No ADR is required. The selected change corrects existing viewport containment
|
||||
inside one shared UI primitive and introduces no durable architectural
|
||||
alternative.
|
||||
@@ -0,0 +1,139 @@
|
||||
---
|
||||
id: "01-clamp-composer-suggestions"
|
||||
title: "Clamp composer suggestions to the visual viewport"
|
||||
status: done
|
||||
wave: 1
|
||||
depends_on: []
|
||||
plan: "plan.md"
|
||||
spec: "../../specs/ui/requirements/composer-suggestion-overlays.md"
|
||||
system_design: "../../specs/ui/system-design/composer-suggestion-overlays.md"
|
||||
---
|
||||
|
||||
# Task 01: Clamp Composer Suggestions to the Visual Viewport
|
||||
|
||||
## Intent
|
||||
|
||||
Keep mobile composer suggestion menus visible and selectable when a software
|
||||
keyboard shrinks the visual viewport but the editor anchor remains in layout
|
||||
coordinates.
|
||||
|
||||
## Acceptance
|
||||
|
||||
- `AC-UI-COMPOSER-OVERLAY-001.1`: An above-placement menu whose raw anchor is
|
||||
below the visual viewport renders its full surface inside the padded visible
|
||||
bounds when the header and a result row fit.
|
||||
- `AC-UI-COMPOSER-OVERLAY-001.2`: An already-open menu recomputes that contained
|
||||
geometry after visual-viewport resize or scroll without retriggering.
|
||||
- `AC-UI-COMPOSER-OVERLAY-001.3`: The phone browser keeps the popup horizontally
|
||||
contained, the document free of horizontal overflow, and each selectable row
|
||||
at least 44 CSS pixels tall.
|
||||
- `AC-UI-COMPOSER-OVERLAY-001.4`: Touching the custom-prompt result inserts it,
|
||||
closes the menu, preserves composer focus, and does not send the draft.
|
||||
- Ordinary desktop and offset-phone geometry remains unchanged. Below placement
|
||||
retains its explicit top-edge semantics.
|
||||
- The unit and browser regressions fail before the production change and pass
|
||||
after the smallest geometry correction.
|
||||
|
||||
## Files likely touched
|
||||
|
||||
- `apps/web/components/task/chat/popup-menu.tsx`
|
||||
- `apps/web/components/task/chat/popup-menu.test.tsx`
|
||||
- `apps/web/e2e/tests/chat/mobile-prompt-mention-composer.spec.ts`
|
||||
- `docs/plans/mobile-composer-suggestion-viewport/plan.md`
|
||||
- `docs/plans/mobile-composer-suggestion-viewport/task-01-clamp-composer-suggestions.md`
|
||||
|
||||
## Steps
|
||||
|
||||
1. Add the shrunken-visual-viewport unit case to
|
||||
`popup-menu.test.tsx`, annotate its acceptance-criterion coverage, and run it
|
||||
to record the expected failure.
|
||||
2. Add ordinary above- and below-placement assertions that pin existing
|
||||
geometry.
|
||||
3. Change `computePopupMenuStyle` so the rendered vertical edge and available
|
||||
height use the same visual-viewport-normalized coordinate. Keep the current
|
||||
width, margin, height cap, transform, and event subscriptions.
|
||||
4. Run the focused unit suite to green and refactor only if the geometry stays
|
||||
simpler and deterministic.
|
||||
5. Add the managed mobile E2E from the diagnostic scenario. Record its red
|
||||
geometry failure before relying on the production correction, then run it to
|
||||
green.
|
||||
6. Run the sibling mobile menu scenarios, typecheck, and lint. Record exact
|
||||
results in this work order and the plan.
|
||||
|
||||
## Verification
|
||||
|
||||
```bash
|
||||
cd apps/web && pnpm test -- components/task/chat/popup-menu.test.tsx
|
||||
cd apps/web && pnpm e2e:run --project mobile-chrome \
|
||||
tests/chat/mobile-prompt-mention-composer.spec.ts \
|
||||
tests/chat/mobile-entity-reference-composer.spec.ts \
|
||||
tests/chat/mobile-slash-command-composer.spec.ts \
|
||||
tests/task/mobile-task-create-escape.spec.ts
|
||||
cd apps/web && pnpm run typecheck
|
||||
cd apps && pnpm --filter @kandev/web lint
|
||||
```
|
||||
|
||||
## Dependencies
|
||||
|
||||
None.
|
||||
|
||||
## Parallelism
|
||||
|
||||
Sequential in the primary conversation. Production geometry and its unit and
|
||||
browser regressions form one TDD loop. This work order does not authorize
|
||||
subagents.
|
||||
|
||||
## Inputs
|
||||
|
||||
- `REQ-UI-COMPOSER-OVERLAY-001` and its four acceptance criteria.
|
||||
- `docs/specs/ui/system-design/composer-suggestion-overlays.md`.
|
||||
- Root-cause evidence: popup bottom 551 pixels with a 420-pixel visual viewport
|
||||
after typing a matching custom-prompt query.
|
||||
- Passing focused baseline: 23 tests across popup geometry, TipTap suggestion,
|
||||
and custom-prompt hooks.
|
||||
- Existing mobile browser coverage in
|
||||
`mobile-entity-reference-composer.spec.ts`,
|
||||
`mobile-slash-command-composer.spec.ts`, and
|
||||
`mobile-task-create-escape.spec.ts`.
|
||||
|
||||
## Risks
|
||||
|
||||
- Use visual-viewport offsets as well as height; an offset viewport can occur
|
||||
during zoom or browser chrome movement.
|
||||
- Calculate maximum height from the normalized rendered edge, not the stale
|
||||
caret anchor.
|
||||
- Preserve short-list anchoring and the plan editor's below placement.
|
||||
- Keep the E2E cleanup in `finally`; custom prompts are shared fixture data.
|
||||
- Do not add a hardcoded user-facing string. The production change needs no new
|
||||
copy or locale key.
|
||||
|
||||
## Output contract
|
||||
|
||||
Report the red failure, geometry formula, permanent regression coverage, exact
|
||||
verification results, affected shared consumers, blockers, and remaining risks.
|
||||
Then mark this work order done and update the plan checkbox and results.
|
||||
|
||||
## Results
|
||||
|
||||
- Added acceptance-criterion-linked unit coverage for an above-composer anchor
|
||||
left below a software-keyboard visual viewport, live viewport resize, and the
|
||||
unchanged below-caret placement path.
|
||||
- RED: the focused unit suite failed both new containment checks with `552px`
|
||||
instead of `412px` (2 failed, 9 passed).
|
||||
- RED: the permanent mobile saved-prompt E2E measured popup bottom `551px`
|
||||
against a `421px` limit and failed the containment assertion.
|
||||
- Updated `computePopupMenuStyle` to clamp the requested rendered vertical edge
|
||||
to the padded visual viewport, then calculate available height from that same
|
||||
normalized edge. Existing width, height cap, transform, portal, and viewport
|
||||
event subscriptions remain unchanged.
|
||||
- GREEN: the focused unit suite passed (11 tests).
|
||||
- GREEN: the rebuilt mobile saved-prompt scenario passed (1 test).
|
||||
- Shared regression: the rebuilt mobile `@`, `#`, `/`, and task-create mention
|
||||
command passed (5 tests).
|
||||
- `pnpm run typecheck` passed.
|
||||
- Final full web lint passed with zero warnings after splitting the test groups
|
||||
to satisfy the callback line limit.
|
||||
- PR review remediation qualified the legacy entity-reference anchoring text to
|
||||
cover the below-viewport clamp; specification lint passed afterward.
|
||||
- Managed browser runs removed their artifacts and task-owned instances during
|
||||
teardown. No blocker or known remaining risk remains in the work-order scope.
|
||||
+112
@@ -0,0 +1,112 @@
|
||||
---
|
||||
id: "02-preserve-composer-adjacency"
|
||||
title: "Preserve visible composer adjacency"
|
||||
status: done
|
||||
wave: 2
|
||||
depends_on: ["01-clamp-composer-suggestions"]
|
||||
plan: "plan.md"
|
||||
spec: "../../specs/ui/requirements/composer-suggestion-overlays.md"
|
||||
system_design: "../../specs/ui/system-design/composer-suggestion-overlays.md"
|
||||
---
|
||||
|
||||
# Task 02: Preserve Visible Composer Adjacency
|
||||
|
||||
## Intent
|
||||
|
||||
Keep suggestion lists directly above a visible mobile composer while retaining
|
||||
the off-screen-anchor containment fallback added in Task 01.
|
||||
|
||||
## Acceptance
|
||||
|
||||
- `AC-UI-COMPOSER-OVERLAY-001.1`: Raw anchors outside the visible viewport
|
||||
remain covered by deterministic containment tests.
|
||||
- `AC-UI-COMPOSER-OVERLAY-001.3`: A keyboard-sized phone layout keeps the menu
|
||||
contained, touch-safe, and free of horizontal overflow.
|
||||
- `AC-UI-COMPOSER-OVERLAY-001.4`: Touch selection still inserts the saved
|
||||
prompt and preserves focus.
|
||||
- `AC-UI-COMPOSER-OVERLAY-001.5`: A visible mobile composer and suggestion
|
||||
surface have no more than an eight-pixel vertical gap.
|
||||
|
||||
## Files likely touched
|
||||
|
||||
- `apps/web/e2e/tests/chat/mobile-prompt-mention-composer.spec.ts`
|
||||
- `docs/specs/ui/requirements/composer-suggestion-overlays.md`
|
||||
- `docs/specs/ui/system-design/composer-suggestion-overlays.md`
|
||||
- `docs/plans/mobile-composer-suggestion-viewport/plan.md`
|
||||
- `docs/plans/mobile-composer-suggestion-viewport/task-02-preserve-composer-adjacency.md`
|
||||
|
||||
## Steps
|
||||
|
||||
1. Add a menu-to-composer bounding-box assertion to the current mobile prompt
|
||||
scenario and record its failure against the occlusion-only viewport mock.
|
||||
2. Resize the page to the keyboard-sized height so layout, composer, and visual
|
||||
viewport reflow together. Keep the existing containment, touch-size,
|
||||
overflow, insertion, and focus assertions.
|
||||
3. Capture and inspect fresh phone evidence, then run the focused and sibling
|
||||
shared-menu checks.
|
||||
|
||||
## Verification
|
||||
|
||||
```bash
|
||||
cd apps/web && pnpm test -- components/task/chat/popup-menu.test.tsx
|
||||
cd apps/web && pnpm e2e:run --project mobile-chrome \
|
||||
tests/chat/mobile-prompt-mention-composer.spec.ts \
|
||||
tests/chat/mobile-entity-reference-composer.spec.ts \
|
||||
tests/chat/mobile-slash-command-composer.spec.ts \
|
||||
tests/task/mobile-task-create-escape.spec.ts
|
||||
cd apps/web && pnpm run typecheck
|
||||
cd apps && pnpm --filter @kandev/web lint
|
||||
python3 scripts/lint-spec-files.py --all
|
||||
```
|
||||
|
||||
## Dependencies
|
||||
|
||||
Task 01 supplies shared visual-viewport containment behavior and unit coverage.
|
||||
This remediation changes browser modeling and durable adjacency coverage, not
|
||||
production geometry.
|
||||
|
||||
## Parallelism
|
||||
|
||||
Sequential in the primary conversation. This work order does not authorize
|
||||
subagents.
|
||||
|
||||
## Inputs
|
||||
|
||||
- User screenshot feedback showing the list detached above the composer.
|
||||
- RED browser measurement: a 136-pixel menu-to-composer gap after replacing
|
||||
only `visualViewport.height` while leaving page layout unchanged.
|
||||
- Existing unit coverage for an anchor below a reduced visual viewport.
|
||||
|
||||
## Risks
|
||||
|
||||
- Do not remove the raw off-screen-anchor containment regression while fixing
|
||||
screenshot evidence.
|
||||
- Do not claim an operating-system keyboard is present in Playwright. The
|
||||
browser runner models its reduced available height by resizing the page.
|
||||
- Keep capture assets ignored and publish them through PR media, not feature
|
||||
branch history.
|
||||
|
||||
## Output contract
|
||||
|
||||
Report the RED gap, corrected viewport model, permanent adjacency assertion,
|
||||
fresh inspected screenshot, exact verification results, and PR status. Then
|
||||
mark this work order done and synchronize the plan.
|
||||
|
||||
## Results
|
||||
|
||||
- RED: the permanent bounding-box assertion measured a 136-pixel gap between
|
||||
the saved-prompt surface and composer under the old occlusion-only viewport
|
||||
mock.
|
||||
- Replaced that invalid visual model with `page.setViewportSize()` at a
|
||||
420-pixel height, which reflows the real mobile page, composer, and visual
|
||||
viewport together. Production containment geometry did not require change.
|
||||
- GREEN: the focused scenario passed with direct adjacency, visual-viewport
|
||||
containment, a 44-pixel result row, no horizontal overflow, touch insertion,
|
||||
and retained composer focus.
|
||||
- The full targeted mobile shared-consumer run passed five tests covering `@`,
|
||||
`#`, `/`, and task-create mention behavior.
|
||||
- Focused popup unit coverage passed 11 tests. Typecheck, full web lint, and
|
||||
specification lint passed.
|
||||
- Captured and visually inspected
|
||||
`apps/web/.pr-assets/mobile-prompt-mention-composer--mobile-composer-prompt-menu.png`;
|
||||
the menu is directly attached above the composer.
|
||||
@@ -37,8 +37,6 @@ contract that other capabilities can reuse.
|
||||
|
||||
### Requirements
|
||||
|
||||
|
||||
|
||||
- [ACP Model Configuration Summary](requirements/acp-model-configuration-summary.md)
|
||||
- [ACP Shell Command Output](requirements/acp-shell-command-output.md)
|
||||
- [Adaptive Kanban](requirements/adaptive-kanban.md)
|
||||
@@ -56,6 +54,7 @@ contract that other capabilities can reuse.
|
||||
- [Clarification submit feedback](requirements/clarification-submit-feedback.md)
|
||||
- [Command-panel Sidebar Task Reveal](requirements/command-panel-sidebar-task-reveal.md)
|
||||
- [Comment Markdown Rendering](requirements/comment-markdown.md)
|
||||
- [Composer Suggestion Overlays](requirements/composer-suggestion-overlays.md)
|
||||
- [Context Compaction Count](requirements/context-compaction-count.md)
|
||||
- [Context Window Reset Freshness](requirements/context-window-reset-freshness.md)
|
||||
- [Context Window Unmeasured State](requirements/context-window-unmeasured-state.md)
|
||||
@@ -143,6 +142,7 @@ contract that other capabilities can reuse.
|
||||
|
||||
- [Agent Todo List Panel](system-design/agent-todo-list-panel.md)
|
||||
- [App Status Bar](system-design/app-status-bar.md)
|
||||
- [Composer Suggestion Overlays](system-design/composer-suggestion-overlays.md)
|
||||
- [Per-workflow column visibility on the kanban board System Design Part 1](system-design/board-step-visibility-filter-01.md)
|
||||
- [Per-workflow column visibility on the kanban board System Design Part 2](system-design/board-step-visibility-filter-02.md)
|
||||
- [Task PR Automation Controls System Design Part 1](system-design/ci-pr-automation-01.md)
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
status: active
|
||||
system: ui
|
||||
created: 2026-08-02
|
||||
updated: 2026-08-23
|
||||
owners:
|
||||
- kandev
|
||||
---
|
||||
@@ -25,7 +26,7 @@ Starting another agent inside an existing task is another prompt-authoring flow,
|
||||
- **AC-UI-AGENT-LAUNCH-PROMPT-COMPOSER-001.4:** Copying the initial prompt, choosing blank context, or generating a session summary updates that same composer without losing its authoring tools.
|
||||
- **AC-UI-AGENT-LAUNCH-PROMPT-COMPOSER-001.5:** The Start Agent button and the existing Ctrl/Cmd+Enter shortcut launch with the current prompt and attachments. Enter used to select an open mention result never launches the agent.
|
||||
- **AC-UI-AGENT-LAUNCH-PROMPT-COMPOSER-001.6:** The Start Agent action remains disabled while the prompt is empty, while context is being summarized, while launch is in progress, or when no compatible agent profile is available.
|
||||
- **AC-UI-AGENT-LAUNCH-PROMPT-COMPOSER-001.7:** Desktop and phone layouts expose the same capabilities and launch result. On phone, the existing mobile sessions entry point opens the dialog; its prompt menu and controls remain touch reachable without horizontal document overflow.
|
||||
- **AC-UI-AGENT-LAUNCH-PROMPT-COMPOSER-001.7:** Desktop and phone layouts expose the same capabilities and launch result. On phone, the existing mobile sessions entry point opens the dialog; the prompt menu follows the shared [composer suggestion overlay requirements](composer-suggestion-overlays.md), and the controls remain touch reachable without horizontal document overflow.
|
||||
- **AC-UI-AGENT-LAUNCH-PROMPT-COMPOSER-001.8:** Agent launch continues to use the existing session launch contract, task environment, context selection, profile compatibility rules, and guarded enhancement delivery.
|
||||
|
||||
## Migrated source detail
|
||||
@@ -42,7 +43,7 @@ Starting another agent inside an existing task is another prompt-authoring flow,
|
||||
- Copying the initial prompt, choosing blank context, or generating a session summary updates that same composer without losing its authoring tools.
|
||||
- The Start Agent button and the existing Ctrl/Cmd+Enter shortcut launch with the current prompt and attachments. Enter used to select an open mention result never launches the agent.
|
||||
- The Start Agent action remains disabled while the prompt is empty, while context is being summarized, while launch is in progress, or when no compatible agent profile is available.
|
||||
- Desktop and phone layouts expose the same capabilities and launch result. On phone, the existing mobile sessions entry point opens the dialog; its prompt menu and controls remain touch reachable without horizontal document overflow.
|
||||
- Desktop and phone layouts expose the same capabilities and launch result. On phone, the existing mobile sessions entry point opens the dialog; the prompt menu follows the shared [composer suggestion overlay requirements](composer-suggestion-overlays.md), and the controls remain touch reachable without horizontal document overflow.
|
||||
- Agent launch continues to use the existing session launch contract, task environment, context selection, profile compatibility rules, and guarded enhancement delivery.
|
||||
|
||||
## Failure modes
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
---
|
||||
status: active
|
||||
system: ui
|
||||
created: 2026-08-23
|
||||
updated: 2026-08-24
|
||||
owners:
|
||||
- web
|
||||
---
|
||||
|
||||
# Composer Suggestion Overlay Requirements
|
||||
|
||||
## Overview
|
||||
|
||||
Kandev composers expose contextual suggestion menus for saved prompts, tasks,
|
||||
files, plans, agent commands, and external work items. On a phone, opening the
|
||||
software keyboard can shrink the browser's visual viewport without moving the
|
||||
composer's layout-viewport caret coordinates. The user must still be able to
|
||||
see and select suggestions without dismissing the keyboard.
|
||||
|
||||
## Terminology
|
||||
|
||||
- **Layout viewport:** The coordinate space used by the editor caret and fixed
|
||||
layout before mobile keyboard occlusion is applied.
|
||||
- **Visual viewport:** The currently visible browser region after zoom, browser
|
||||
chrome, and software keyboard occlusion.
|
||||
- **Suggestion overlay:** The shared popup surface opened by a composer trigger,
|
||||
including `@`, `#`, and `/` menus.
|
||||
|
||||
## Requirements
|
||||
|
||||
### REQ-UI-COMPOSER-OVERLAY-001: Keep composer suggestions usable in the visible viewport
|
||||
|
||||
**Intent:** Preserve suggestion discovery and selection when a mobile software
|
||||
keyboard changes the visible browser area.
|
||||
|
||||
**User story:** As a phone user composing a prompt, I want contextual
|
||||
suggestions to remain visible above the keyboard, so that I can insert an item
|
||||
without hiding the keyboard or abandoning my draft.
|
||||
|
||||
#### Acceptance criteria
|
||||
|
||||
- **AC-UI-COMPOSER-OVERLAY-001.1:** Given an open above-composer suggestion
|
||||
overlay, when the current visual viewport no longer contains the caret or
|
||||
direct anchor because a software keyboard or browser chrome changed the
|
||||
visible area, the complete rendered overlay surface shall be positioned and
|
||||
height-constrained inside the visual viewport whenever that viewport can fit
|
||||
the overlay header and one result row.
|
||||
- **AC-UI-COMPOSER-OVERLAY-001.2:** Given an open suggestion overlay, when the
|
||||
visual viewport resizes or scrolls, the overlay shall reflow without requiring
|
||||
the user to close, retrigger, or retype the active query. When the Visual
|
||||
Viewport API is unavailable, the layout viewport shall provide the fallback
|
||||
bounds.
|
||||
- **AC-UI-COMPOSER-OVERLAY-001.3:** Given a result list taller or wider than the
|
||||
available phone viewport, the overlay shall remain horizontally contained,
|
||||
keep result rows at least 44 CSS pixels tall, and scroll its results internally
|
||||
without introducing horizontal document overflow.
|
||||
- **AC-UI-COMPOSER-OVERLAY-001.4:** Given a visible saved-prompt, task, file,
|
||||
plan, command, or entity-reference result, when the user selects it by touch
|
||||
or keyboard, the existing feature-specific insertion behavior shall complete,
|
||||
the draft shall not submit implicitly, and composer focus shall remain
|
||||
available for continued editing.
|
||||
- **AC-UI-COMPOSER-OVERLAY-001.5:** Given the composer and its above-composer
|
||||
anchor remain inside the visual viewport, when a suggestion overlay opens or
|
||||
reflows, its rendered bottom edge shall remain directly adjacent to the
|
||||
composer rather than detach to another viewport edge.
|
||||
|
||||
## Out of scope
|
||||
|
||||
- Changing suggestion result sources, ranking, filtering, or empty states.
|
||||
- Changing saved-prompt, task, file, plan, command, or entity-reference data
|
||||
contracts.
|
||||
- Replacing the contextual popup with a drawer or full-screen picker.
|
||||
- Adding automatic placement-side flipping to the plan editor's below-caret
|
||||
block menu.
|
||||
- Changing message submission, task launch, or agent launch behavior.
|
||||
@@ -2,7 +2,7 @@
|
||||
status: active
|
||||
system: ui
|
||||
created: 2026-07-21
|
||||
updated: 2026-08-17
|
||||
updated: 2026-08-24
|
||||
owners:
|
||||
- kandev
|
||||
---
|
||||
@@ -21,7 +21,7 @@ Users discuss external work items in agent chat, but plain ticket keys and pull-
|
||||
#### Acceptance criteria
|
||||
|
||||
- **AC-UI-ENTITY-REFERENCE-COMPOSER-001.1:** Task chat and Quick Chat support a `#` entity-reference trigger in their shared TipTap composer. Passthrough, task creation, comments, plans, Office text inputs, and other editors remain unchanged.
|
||||
- **AC-UI-ENTITY-REFERENCE-COMPOSER-001.2:** Typing `#` at the start of a text block or after whitespace opens a search menu directly above the composer. Its rendered bottom edge stays anchored to the composer even when only a short result set is visible. A `#` inside another token or a code block remains literal text.
|
||||
- **AC-UI-ENTITY-REFERENCE-COMPOSER-001.2:** Typing `#` at the start of a text block or after whitespace opens a search menu directly above the composer. Its shared geometry and visible-viewport containment follow the [composer suggestion overlay requirements](composer-suggestion-overlays.md). Its rendered bottom edge stays anchored to a composer anchor inside the visible viewport even when only a short result set is visible; an anchor below that viewport is clamped to its padded bottom edge. A `#` inside another token or a code block remains literal text.
|
||||
- **AC-UI-ENTITY-REFERENCE-COMPOSER-001.3:** A trigger starts only when the user enters a new `#` character. Pasted, dropped, restored, or programmatically inserted text remains literal and does not open the menu.
|
||||
- **AC-UI-ENTITY-REFERENCE-COMPOSER-001.4:** After the user types at least one query character, search covers the active workspace's connected, searchable sources:
|
||||
- **AC-UI-ENTITY-REFERENCE-COMPOSER-001.5:** Jira tickets;
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
status: active
|
||||
system: ui
|
||||
created: 2026-07-02
|
||||
updated: 2026-08-23
|
||||
owners:
|
||||
- cfl
|
||||
---
|
||||
@@ -19,7 +20,7 @@ ACP agents can advertise slash commands during session setup. Kandev exposes tho
|
||||
|
||||
#### Acceptance criteria
|
||||
|
||||
- **AC-UI-SLASH-COMMAND-COMPOSER-001.1:** When a chat composer has advertised agent commands and the user types `/`, the composer shows matching slash commands from the active session.
|
||||
- **AC-UI-SLASH-COMMAND-COMPOSER-001.1:** When a chat composer has advertised agent commands and the user types `/`, the composer shows matching slash commands from the active session. The menu follows the shared [composer suggestion overlay requirements](composer-suggestion-overlays.md).
|
||||
- **AC-UI-SLASH-COMMAND-COMPOSER-001.2:** Selecting a slash command with Enter, Tab, or pointer/touch replaces only the active slash trigger range with a visually distinct inline command chip in the composer. It MUST NOT submit, queue, or otherwise send a chat message.
|
||||
- **AC-UI-SLASH-COMMAND-COMPOSER-001.3:** The inserted command chip remains part of the editable draft. The user can add text before or after it, delete it, or leave the composer without starting an agent turn.
|
||||
- **AC-UI-SLASH-COMMAND-COMPOSER-001.4:** The command chip serializes to the same plain slash command text when submitted, copied from the draft value, or saved as a draft.
|
||||
@@ -37,6 +38,7 @@ ACP agents can advertise slash commands during session setup. Kandev exposes tho
|
||||
## What
|
||||
|
||||
- When a chat composer has advertised agent commands and the user types `/`, the composer shows matching slash commands from the active session.
|
||||
- The menu's shared geometry and visible-viewport containment follow the [composer suggestion overlay requirements](composer-suggestion-overlays.md).
|
||||
- Selecting a slash command with Enter, Tab, or pointer/touch replaces only the active slash trigger range with a visually distinct inline command chip in the composer. It MUST NOT submit, queue, or otherwise send a chat message.
|
||||
- The inserted command chip remains part of the editable draft. The user can add text before or after it, delete it, or leave the composer without starting an agent turn.
|
||||
- The command chip serializes to the same plain slash command text when submitted, copied from the draft value, or saved as a draft.
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
---
|
||||
status: current
|
||||
system: ui
|
||||
updated: 2026-08-24
|
||||
requirements:
|
||||
- REQ-UI-COMPOSER-OVERLAY-001
|
||||
---
|
||||
|
||||
# Composer Suggestion Overlay System Design
|
||||
|
||||
## Purpose and boundaries
|
||||
|
||||
The shared `PopupMenu` primitive positions composer suggestion surfaces from a
|
||||
direct point or an editor-provided client rectangle. This design corrects its
|
||||
vertical geometry when mobile browsers retain layout-viewport caret coordinates
|
||||
below a software-keyboard-reduced visual viewport.
|
||||
|
||||
The primitive owns viewport containment, maximum size, reflow subscriptions,
|
||||
listbox structure, touch-row size, and focus-preserving pointer behavior. Each
|
||||
consumer continues to own trigger recognition, result loading, selection, and
|
||||
draft serialization.
|
||||
|
||||
## Requirement mapping
|
||||
|
||||
| Requirement | Design section |
|
||||
| --- | --- |
|
||||
| `REQ-UI-COMPOSER-OVERLAY-001` | [Geometry and reflow](#geometry-and-reflow), [Interaction contract](#interaction-contract) |
|
||||
|
||||
## Components and responsibilities
|
||||
|
||||
- `PopupMenu` in `apps/web/components/task/chat/popup-menu.tsx` resolves the
|
||||
anchor, reads viewport bounds, subscribes to viewport changes, portals the
|
||||
surface to `document.body`, and renders one labelled listbox.
|
||||
- `computePopupMenuStyle` performs deterministic horizontal and vertical
|
||||
geometry without reading the DOM. It is the unit-test boundary for viewport
|
||||
containment.
|
||||
- `MentionMenu`, `SlashCommandMenu`, and `EntityReferenceMenu` use the default
|
||||
above-anchor placement for chat and shared prompt-composer suggestions.
|
||||
- `PlanSlashMenu` uses the same primitive with below-anchor placement. Its
|
||||
placement semantics are not changed, but unit coverage must protect that path
|
||||
from a shared-geometry regression.
|
||||
|
||||
## Data and contracts
|
||||
|
||||
`PopupMenu` accepts one of two transient anchor contracts:
|
||||
|
||||
- a direct `{x, y}` fixed-position point; or
|
||||
- a `clientRect` callback whose top or bottom edge supplies the placement point.
|
||||
|
||||
The browser's `window.visualViewport`, when present, supplies `offsetLeft`,
|
||||
`offsetTop`, `width`, and `height`. Otherwise `window.innerWidth` and
|
||||
`window.innerHeight` supply a zero-offset fallback viewport. No value is stored
|
||||
or sent over an API.
|
||||
|
||||
The existing placement contract remains explicit: `above` renders the
|
||||
surface's bottom edge above its normalized anchor; `below` renders the surface's
|
||||
top edge below its normalized anchor. This repair does not introduce automatic
|
||||
side flipping. Normalization is a containment fallback, not the primary
|
||||
placement: a visible direct anchor remains unchanged so the overlay stays
|
||||
attached to its composer.
|
||||
|
||||
## Geometry and reflow
|
||||
|
||||
`computePopupMenuStyle` shall use the current viewport rectangle for both axes:
|
||||
|
||||
1. Derive padded viewport bounds with the existing eight-pixel margin.
|
||||
2. Clamp popup width and left position to the padded horizontal bounds.
|
||||
3. For `above`, derive the rendered bottom edge from `position.y - margin` and
|
||||
clamp that edge to the padded vertical bounds. Calculate available height
|
||||
from the clamped edge to the padded viewport top, not from the raw layout
|
||||
anchor. Keep the existing `translateY(-100%)` transform so short result sets
|
||||
stay bottom-anchored. When the requested edge is already inside the bounds,
|
||||
clamping must be an identity operation so the surface remains directly
|
||||
adjacent to the composer.
|
||||
4. For `below`, retain top-edge placement and calculate height from its
|
||||
normalized top edge to the padded viewport bottom. Cover this path so the
|
||||
above-placement repair cannot change its transform or ordinary geometry.
|
||||
5. Cap the outer surface at the existing menu-height limit, subtract the header
|
||||
height for the internally scrollable listbox, and never produce a negative
|
||||
size.
|
||||
|
||||
When the software keyboard leaves a composer anchor below the visual viewport,
|
||||
step 3 moves the rendered bottom edge to the visible viewport's padded bottom.
|
||||
The list then grows upward into visible space. Anchors already inside the
|
||||
viewport retain their current geometry and composer adjacency.
|
||||
|
||||
While open, `PopupMenu` continues to rerender on window resize and on visual
|
||||
viewport resize or scroll. The geometry helper then recomputes from the current
|
||||
bounds. No polling, global store, or new observer is required.
|
||||
|
||||
## Interaction contract
|
||||
|
||||
The popup remains a body portal above dialog content. Its title labels one
|
||||
`listbox`; each selectable row remains an `option` with a minimum 44-pixel touch
|
||||
height. Pointer-down continues to prevent composer blur, and selection remains
|
||||
owned by the invoking feature. This contextual, transient action list stays a
|
||||
popup on mobile; it is not a navigation flow that warrants a drawer.
|
||||
|
||||
## Control flow
|
||||
|
||||
1. A composer recognizes `@`, `#`, or `/` and supplies a direct point or live
|
||||
client rectangle.
|
||||
2. `PopupMenu` resolves the placement point and current viewport.
|
||||
3. `computePopupMenuStyle` returns the contained fixed-position rectangle.
|
||||
4. A viewport event increments the local revision and repeats steps 2 and 3.
|
||||
5. The consumer handles touch or keyboard selection and updates the draft.
|
||||
|
||||
## Failure and recovery
|
||||
|
||||
- Without Visual Viewport API support, the menu uses the layout viewport and
|
||||
preserves current desktop behavior.
|
||||
- If the visible viewport is too small for the header and one row, geometry
|
||||
remains non-negative and contained; the browser may expose less usable
|
||||
content until the viewport expands.
|
||||
- If the composer anchor itself is occluded, containment takes precedence over
|
||||
adjacency because no placement can be both attached to that hidden anchor and
|
||||
visible. Once layout reflow brings the anchor into view, direct adjacency is
|
||||
restored.
|
||||
- A missing anchor keeps the menu closed. A failed result lookup remains the
|
||||
invoking feature's empty or error state and does not affect geometry.
|
||||
- Closing and retriggering remains available, but viewport reflow must not
|
||||
require it.
|
||||
|
||||
## Persistence
|
||||
|
||||
None. Viewport bounds, menu state, selected index, and result scrolling are
|
||||
transient browser state.
|
||||
|
||||
## Security
|
||||
|
||||
The change introduces no new trust boundary. Consumer-provided labels and
|
||||
descriptions continue through React rendering, and the popup does not evaluate
|
||||
result data or URLs.
|
||||
|
||||
## Observability
|
||||
|
||||
No production telemetry is added. Deterministic geometry unit tests cover raw
|
||||
layout anchors outside a shrunken visual viewport. A phone-browser E2E covers a
|
||||
saved-prompt result after the page and composer reflow to a keyboard-sized
|
||||
viewport, including direct composer adjacency, containment, touch selection,
|
||||
and focus retention. Existing mobile `#` and `/` scenarios provide
|
||||
sibling-consumer regressions. Browser evidence must resize the page layout with
|
||||
the visible viewport; replacing only `visualViewport` describes an occluded
|
||||
composer and is not valid visual evidence for adjacency.
|
||||
|
||||
## Related decisions
|
||||
|
||||
None. This is a local correction to an existing shared presentation contract
|
||||
and does not establish a new architecture boundary.
|
||||
@@ -4,7 +4,7 @@ system: ui
|
||||
requirements:
|
||||
- REQ-UI-ENTITY-REFERENCE-COMPOSER-001
|
||||
created: 2026-07-21
|
||||
updated: 2026-08-17
|
||||
updated: 2026-08-24
|
||||
owners:
|
||||
- kandev
|
||||
---
|
||||
@@ -29,7 +29,7 @@ Users discuss external work items in agent chat, but plain ticket keys and pull-
|
||||
## What
|
||||
|
||||
- Task chat and Quick Chat support a `#` entity-reference trigger in their shared TipTap composer. Passthrough, task creation, comments, plans, Office text inputs, and other editors remain unchanged.
|
||||
- Typing `#` at the start of a text block or after whitespace opens a search menu directly above the composer. Its rendered bottom edge stays anchored to the composer even when only a short result set is visible. A `#` inside another token or a code block remains literal text.
|
||||
- Typing `#` at the start of a text block or after whitespace opens a search menu directly above the composer. Its shared geometry and visible-viewport containment follow the [composer suggestion overlay requirements](../requirements/composer-suggestion-overlays.md). Its rendered bottom edge stays anchored to a composer anchor inside the visible viewport even when only a short result set is visible; an anchor below that viewport is clamped to its padded bottom edge. A `#` inside another token or a code block remains literal text.
|
||||
- A trigger starts only when the user enters a new `#` character. Pasted, dropped, restored, or programmatically inserted text remains literal and does not open the menu.
|
||||
- After the user types at least one query character, search covers the active workspace's connected, searchable sources:
|
||||
- Jira tickets;
|
||||
|
||||
Reference in New Issue
Block a user