fix(ui): hide composer focus hint on mobile (#3795)
This commit is contained in:
@@ -1,17 +1,21 @@
|
||||
import { createRef } from "react";
|
||||
import { cleanup, render, screen } from "@testing-library/react";
|
||||
import type { Window as HappyDOMWindow } from "happy-dom";
|
||||
import { act, cleanup, render, screen } from "@testing-library/react";
|
||||
import { TooltipProvider } from "@kandev/ui/tooltip";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { ChatInputBody, type ChatInputBodyProps } from "./chat-input-body";
|
||||
import { shouldShowCancelAgent } from "./types";
|
||||
|
||||
const tipTapPropsMock = vi.hoisted(() => vi.fn());
|
||||
const MOCK_TIPTAP_INPUT_TEST_ID = "mock-tiptap-input";
|
||||
const initialWidth = window.innerWidth;
|
||||
const viewport = (window as unknown as HappyDOMWindow).happyDOM;
|
||||
const CHAT_INPUT_GLOW_TEST_ID = "chat-input-glow";
|
||||
|
||||
vi.mock("./tiptap-input", () => ({
|
||||
TipTapInput: (props: unknown) => {
|
||||
tipTapPropsMock(props);
|
||||
return <div data-testid="mock-tiptap-input" />;
|
||||
return <div data-testid={MOCK_TIPTAP_INPUT_TEST_ID} />;
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -25,6 +29,7 @@ vi.mock("./context-items/context-zone", () => ({
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
viewport.setWindowSize({ width: initialWidth });
|
||||
tipTapPropsMock.mockClear();
|
||||
});
|
||||
|
||||
@@ -152,7 +157,9 @@ describe("ChatInputBody", () => {
|
||||
expect.objectContaining({ entityReferencesEnabled: false }),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("ChatInputBody focus hint", () => {
|
||||
it("reserves right-side editable space while the focus hint is visible", () => {
|
||||
render(
|
||||
<TooltipProvider>
|
||||
@@ -162,7 +169,50 @@ describe("ChatInputBody", () => {
|
||||
|
||||
expect(screen.getByText("to focus")).toBeTruthy();
|
||||
expect(screen.getByTestId("chat-input-editor-shell").className).not.toContain("pr-28");
|
||||
expect(screen.getByTestId("mock-tiptap-input").parentElement?.className).toContain("pr-28");
|
||||
expect(screen.getByTestId(MOCK_TIPTAP_INPUT_TEST_ID).parentElement?.className).toContain(
|
||||
"pr-28",
|
||||
);
|
||||
});
|
||||
|
||||
// @covers AC-UI-COMPOSER-FOCUS-HINT-001.1, AC-UI-COMPOSER-FOCUS-HINT-001.2
|
||||
it.each([393, 767, 768, 1024])("matches focus hint and padding to viewport width %i", (width) => {
|
||||
viewport.setWindowSize({ width });
|
||||
render(
|
||||
<TooltipProvider>
|
||||
<ChatInputBody {...props({ showFocusHint: true })} />
|
||||
</TooltipProvider>,
|
||||
);
|
||||
|
||||
const hint = screen.queryByText("to focus");
|
||||
const editor = screen.getByTestId(MOCK_TIPTAP_INPUT_TEST_ID);
|
||||
expect(hint !== null).toBe(width >= 768);
|
||||
expect(editor.parentElement?.classList.contains("pr-28")).toBe(width >= 768);
|
||||
|
||||
act(() => viewport.setWindowSize({ width: width < 768 ? 768 : 767 }));
|
||||
expect(screen.getByTestId(MOCK_TIPTAP_INPUT_TEST_ID)).toBe(editor);
|
||||
expect(screen.queryByText("to focus") !== null).toBe(width < 768);
|
||||
expect(editor.parentElement?.classList.contains("pr-28")).toBe(width < 768);
|
||||
});
|
||||
|
||||
// @covers AC-UI-COMPOSER-FOCUS-HINT-001.3
|
||||
it("keeps the editor and draft when resizing across the phone boundary", () => {
|
||||
const inputProps = props();
|
||||
inputProps.editorAreaProps.value = "Draft to preserve";
|
||||
const body = (
|
||||
<TooltipProvider>
|
||||
<ChatInputBody {...inputProps} />
|
||||
</TooltipProvider>
|
||||
);
|
||||
viewport.setWindowSize({ width: 768 });
|
||||
render(body);
|
||||
const editor = screen.getByTestId(MOCK_TIPTAP_INPUT_TEST_ID);
|
||||
|
||||
act(() => viewport.setWindowSize({ width: 767 }));
|
||||
|
||||
expect(screen.getByTestId(MOCK_TIPTAP_INPUT_TEST_ID)).toBe(editor);
|
||||
expect(tipTapPropsMock).toHaveBeenLastCalledWith(
|
||||
expect.objectContaining({ value: "Draft to preserve" }),
|
||||
);
|
||||
});
|
||||
|
||||
it("does not reserve focus-hint space when the hint is hidden", () => {
|
||||
@@ -173,7 +223,9 @@ describe("ChatInputBody", () => {
|
||||
);
|
||||
|
||||
expect(screen.getByTestId("chat-input-editor-shell").className).not.toContain("pr-28");
|
||||
expect(screen.getByTestId("mock-tiptap-input").parentElement?.className).not.toContain("pr-28");
|
||||
expect(screen.getByTestId(MOCK_TIPTAP_INPUT_TEST_ID).parentElement?.className).not.toContain(
|
||||
"pr-28",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import { useTranslation } from "react-i18next";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@kandev/ui/tooltip";
|
||||
import { CompositorPulse } from "@kandev/ui/compositor-pulse";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useResponsiveBreakpoint } from "@/hooks/use-responsive-breakpoint";
|
||||
import { TipTapInput } from "./tiptap-input";
|
||||
import { ChatInputFocusHint } from "./chat-input-focus-hint";
|
||||
import { ResizeHandle } from "./resize-handle";
|
||||
@@ -382,6 +383,11 @@ function useChatInputDrop(addFiles: (files: File[]) => Promise<void>) {
|
||||
return { handleDragEnter, handleDragLeave, handleDragOver, handleDrop, isDragging };
|
||||
}
|
||||
|
||||
function useChatFocusHintVisibility(showFocusHint: boolean): boolean {
|
||||
const { isMobile } = useResponsiveBreakpoint();
|
||||
return showFocusHint && !isMobile;
|
||||
}
|
||||
|
||||
export function ChatInputBody({
|
||||
containerRef,
|
||||
height,
|
||||
@@ -399,6 +405,7 @@ export function ChatInputBody({
|
||||
editorAreaProps,
|
||||
promptResultRecovery,
|
||||
}: ChatInputBodyProps) {
|
||||
const focusHintVisible = useChatFocusHintVisibility(showFocusHint);
|
||||
const glowClass = chatInputGlowClass(isAgentBusy, isStarting);
|
||||
const hasGlow = Boolean(glowClass);
|
||||
const drop = useChatInputDrop(addFiles);
|
||||
@@ -431,7 +438,7 @@ export function ChatInputBody({
|
||||
onDragLeave={drop.handleDragLeave}
|
||||
onDrop={drop.handleDrop}
|
||||
>
|
||||
<ChatInputFocusHint visible={showFocusHint} />
|
||||
<ChatInputFocusHint visible={focusHintVisible} />
|
||||
<ChatInputContextArea {...contextAreaProps} />
|
||||
<div
|
||||
ref={containerRef}
|
||||
@@ -441,7 +448,7 @@ export function ChatInputBody({
|
||||
>
|
||||
<ChatInputEditorArea
|
||||
{...editorAreaProps}
|
||||
editorClassName={cn(editorAreaProps.editorClassName, showFocusHint && "pr-28")}
|
||||
editorClassName={cn(editorAreaProps.editorClassName, focusHintVisible && "pr-28")}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -51,6 +51,13 @@ test.describe("Mobile slash command composer", () => {
|
||||
// composerReady may reload to reconcile stale startup state. Seed the
|
||||
// client-side command list only after that recovery so it is not discarded.
|
||||
await seedAvailableCommands(testPage, task.session_id, [SLOW_COMMAND]);
|
||||
// @covers AC-UI-COMPOSER-FOCUS-HINT-001.1
|
||||
await editor.fill("");
|
||||
await editor.blur();
|
||||
const composer = session.chat.filter({ visible: true });
|
||||
await expect(composer.getByText("to focus", { exact: true })).not.toBeVisible();
|
||||
await expect(composer.locator(".pr-28")).toHaveCount(0);
|
||||
await testPage.screenshot({ path: test.info().outputPath("mobile-composer.png") });
|
||||
await editor.tap();
|
||||
await editor.fill("");
|
||||
await editor.pressSequentially("/s");
|
||||
|
||||
@@ -102,6 +102,13 @@ test.describe("Slash command composer", () => {
|
||||
await seedAvailableCommands(testPage, task.session_id, [SLOW_COMMAND]);
|
||||
|
||||
const editor = chatEditor(testPage);
|
||||
// @covers AC-UI-COMPOSER-FOCUS-HINT-001.2
|
||||
await editor.fill("");
|
||||
await editor.blur();
|
||||
const composer = session.chat.filter({ visible: true });
|
||||
await expect(composer.getByText("to focus", { exact: true })).toBeVisible();
|
||||
await editor.click();
|
||||
await expect(composer.getByText("to focus", { exact: true })).not.toBeVisible();
|
||||
await selectSlowCommandWithEnter(testPage, editor);
|
||||
|
||||
const chatList = session.chat.locator(".chat-message-list:visible");
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
---
|
||||
created: 2026-09-17
|
||||
status: done
|
||||
requirements:
|
||||
- REQ-UI-COMPOSER-FOCUS-HINT-001
|
||||
system_design:
|
||||
- ../../specs/ui/system-design/composer-focus-hint.md
|
||||
legacy_specs: []
|
||||
---
|
||||
|
||||
# Implementation Plan: Mobile Composer Focus Hint
|
||||
|
||||
## Overview
|
||||
|
||||
Hide the phone focus hint and reclaim its editor space in one sequential work
|
||||
order. Source inspection confirms that current eligibility ignores viewport
|
||||
width, and `ChatInputBody` uses that value for both hint and `pr-28` padding.
|
||||
An empty, unfocused phone composer reproduces the defect.
|
||||
|
||||
## Scope
|
||||
|
||||
### In scope
|
||||
|
||||
Shared composer hint presentation, responsive regression coverage, and draft
|
||||
preservation across the breakpoint.
|
||||
|
||||
### Out of scope
|
||||
|
||||
Executor configuration, cache cleanup, shortcut bindings, and tablet redesign.
|
||||
|
||||
## Technical approach
|
||||
|
||||
Apply the canonical `useResponsiveBreakpoint` mobile result in
|
||||
`ChatInputBody`. Share one effective visibility predicate between hint and
|
||||
padding. Preserve the state helper and all input handlers. UI owns this reusable
|
||||
presentation contract; no durable architectural boundary changes.
|
||||
|
||||
## ASCII UI preview
|
||||
|
||||
UI-01: Empty, unfocused shared composer. Hint text is illustrative English;
|
||||
existing localization remains authoritative. Toolbar structure is unchanged.
|
||||
|
||||
```text
|
||||
Desktop >=768px: [ Write a message... / to focus ]
|
||||
Phone before: [ Write a message... / to focus ]
|
||||
Phone after: [ Write a message... ]
|
||||
[ existing toolbar Send ]
|
||||
```
|
||||
|
||||
The phone removes both hint and its reserved space (AC-001.1). Desktop keeps
|
||||
existing eligibility (AC-001.2). The editor remains mounted across resize
|
||||
(AC-001.3). This is a local region preview, not a pixel layout specification.
|
||||
|
||||
## Tests
|
||||
|
||||
Extend `chat-input-body.test.tsx` with mobile absence, no reserved padding,
|
||||
wider visibility, and resize cases. The mobile case must fail on the existing
|
||||
implementation before the production change. Retain the state helper tests in
|
||||
`use-chat-input-container.test.ts` for existing eligibility.
|
||||
|
||||
## E2E tests
|
||||
|
||||
Extend `e2e/tests/chat/mobile-slash-command-composer.spec.ts` to check the hint
|
||||
before tapping the empty editor, then retain the slash-command/send flow
|
||||
(AC-001.1 and AC-001.3). Extend the desktop sibling with empty/unfocused hint
|
||||
visibility and focus dismissal (AC-001.2). Scope assertions to the active
|
||||
composer and inspect a phone screenshot. Use existing fixture readiness gates.
|
||||
|
||||
## Work orders
|
||||
|
||||
- [x] [Task 01: Gate composer focus hint on mobile](task-01-mobile-hint.md)
|
||||
|
||||
## Verification commands
|
||||
|
||||
```sh
|
||||
(cd apps/web && pnpm exec vitest run components/task/chat/chat-input-body.test.tsx components/task/chat/use-chat-input-container.test.ts)
|
||||
(cd apps/web && pnpm exec eslint components/task/chat/chat-input-body.tsx components/task/chat/chat-input-body.test.tsx e2e/tests/chat/mobile-slash-command-composer.spec.ts e2e/tests/chat/slash-command-composer.spec.ts)
|
||||
(cd apps/web && pnpm e2e:run --project=mobile-chrome mobile-slash-command-composer.spec.ts)
|
||||
(cd apps/web && pnpm e2e:run --project=chromium slash-command-composer.spec.ts)
|
||||
python3 scripts/list-docs.py validate
|
||||
python3 scripts/lint-spec-files.py --all
|
||||
git diff --check
|
||||
```
|
||||
|
||||
## Verification results
|
||||
|
||||
Completed on 2026-09-17.
|
||||
|
||||
- RED: the new 393px and 767px component cases failed because the hint was
|
||||
visible; 36 other tests passed before the production change.
|
||||
- GREEN: the two focused Vitest suites passed, 38 tests total. Coverage includes
|
||||
767px/768px boundaries, hint and padding changes in both resize directions,
|
||||
editor identity, draft preservation, and existing state eligibility.
|
||||
- ESLint passed with no warnings for the two component files and both modified
|
||||
browser specs. Prettier was applied to changed code.
|
||||
- Managed production-build E2E: mobile-chrome passed 1 test; chromium passed
|
||||
4 tests. Phone typing, slash-command selection, and sending work; desktop
|
||||
hint visibility and focus dismissal work.
|
||||
- The phone test passed again with `--no-build` to regenerate its screenshot
|
||||
after the desktop runner replaced the test output directory. Visual inspection
|
||||
confirmed the hint is absent, editing space is available, and toolbar/send
|
||||
controls remain visible. This reused the freshly verified production build.
|
||||
- Specification catalog validation, full specification lint, and `git diff
|
||||
--check` passed. The specification linter's 36 tests passed during design.
|
||||
|
||||
The initial browser attempts could not find Go. These successful E2E commands
|
||||
used `PATH=/usr/local/go/bin:$PATH` in this Pod. No production workaround or
|
||||
runner changes were needed. Unit tests supplied the defect-specific RED gate.
|
||||
|
||||
Public docs need no change: keyboard bindings, labels, and documented workflows
|
||||
are unchanged. Internal requirements, design, and delivery records are updated.
|
||||
|
||||
Fresh synthetic desktop and phone PR screenshots were captured and visually
|
||||
checked. Both capture runs passed. GitHub access and the user-approved commit
|
||||
identity were restored for publication.
|
||||
|
||||
|
||||
## Risks
|
||||
|
||||
Hiding only the hint leaves wasted padding. Using one effective predicate
|
||||
prevents that mismatch. Do not redefine tablet or coarse-pointer policy.
|
||||
|
||||
## Review follow-up
|
||||
|
||||
Greptile identified that forced component rerenders could mask a broken
|
||||
viewport subscription. Responsive cases now use native Happy DOM viewport
|
||||
changes without parent rerenders. All 38 focused tests pass; disabling the
|
||||
subscription makes the boundary-transition cases fail. ESLint passes.
|
||||
Production behavior and screenshots are unchanged.
|
||||
|
||||
CodeRabbit identified an incomplete recorded ESLint command. The command now
|
||||
lists both component files and both browser specs; rerunning it passed.
|
||||
@@ -0,0 +1,141 @@
|
||||
---
|
||||
id: "01-mobile-hint"
|
||||
title: "Gate composer focus hint on mobile"
|
||||
status: done
|
||||
wave: 1
|
||||
depends_on: []
|
||||
plan: "plan.md"
|
||||
requirements:
|
||||
- REQ-UI-COMPOSER-FOCUS-HINT-001
|
||||
acceptance_criteria:
|
||||
- AC-UI-COMPOSER-FOCUS-HINT-001.1
|
||||
- AC-UI-COMPOSER-FOCUS-HINT-001.2
|
||||
- AC-UI-COMPOSER-FOCUS-HINT-001.3
|
||||
system_design:
|
||||
- ../../specs/ui/system-design/composer-focus-hint.md
|
||||
---
|
||||
|
||||
# Task 01: Gate Composer Focus Hint on Mobile
|
||||
|
||||
## Summary
|
||||
|
||||
Suppress hint and reserved padding together below 768px. Keep desktop
|
||||
eligibility and phone editing functional.
|
||||
|
||||
## In scope
|
||||
|
||||
One responsive presentation predicate and focused component/browser coverage.
|
||||
|
||||
## Out of scope
|
||||
|
||||
Executor files, new copy, keyboard binding changes, and composer redesign.
|
||||
|
||||
## Acceptance
|
||||
|
||||
- Mobile component regression fails before implementation and passes after it.
|
||||
- Hint and padding disappear together on phone and recover on eligible wider
|
||||
viewports; draft survives resizing.
|
||||
- Phone slash-command and submission flow and desktop hint behavior pass.
|
||||
|
||||
## ASCII UI preview
|
||||
|
||||
UI-01: Empty, unfocused shared composer. Hint text is illustrative English;
|
||||
existing localization remains authoritative. Toolbar structure is unchanged.
|
||||
|
||||
```text
|
||||
Desktop >=768px: [ Write a message... / to focus ]
|
||||
Phone before: [ Write a message... / to focus ]
|
||||
Phone after: [ Write a message... ]
|
||||
[ existing toolbar Send ]
|
||||
```
|
||||
|
||||
The phone removes both hint and its reserved space (AC-001.1). Desktop keeps
|
||||
existing eligibility (AC-001.2). The editor remains mounted across resize
|
||||
(AC-001.3). This is a local region preview, not a pixel layout specification.
|
||||
|
||||
See the [full plan](plan.md#ascii-ui-preview).
|
||||
|
||||
## Verification
|
||||
|
||||
Run tests red first, apply the minimum fix, then run these checks sequentially.
|
||||
Install workspace dependencies from `apps/` with `pnpm install --frozen-lockfile`
|
||||
first if this checkout lacks them. Do not overlap browser suites.
|
||||
|
||||
```sh
|
||||
(cd apps/web && pnpm exec vitest run components/task/chat/chat-input-body.test.tsx components/task/chat/use-chat-input-container.test.ts)
|
||||
(cd apps/web && pnpm exec eslint components/task/chat/chat-input-body.tsx components/task/chat/chat-input-body.test.tsx e2e/tests/chat/mobile-slash-command-composer.spec.ts e2e/tests/chat/slash-command-composer.spec.ts)
|
||||
(cd apps/web && pnpm e2e:run --project=mobile-chrome mobile-slash-command-composer.spec.ts)
|
||||
(cd apps/web && pnpm e2e:run --project=chromium slash-command-composer.spec.ts)
|
||||
python3 scripts/list-docs.py validate
|
||||
python3 scripts/lint-spec-files.py --all
|
||||
git diff --check
|
||||
```
|
||||
|
||||
## Files likely touched
|
||||
|
||||
- `apps/web/components/task/chat/chat-input-body.tsx`
|
||||
- `apps/web/components/task/chat/chat-input-body.test.tsx`
|
||||
- `apps/web/e2e/tests/chat/mobile-slash-command-composer.spec.ts`
|
||||
- `apps/web/e2e/tests/chat/slash-command-composer.spec.ts`
|
||||
|
||||
## Dependencies
|
||||
|
||||
None.
|
||||
|
||||
## Risks
|
||||
|
||||
Assert both hint and padding. Reset responsive mocks between cases. Test
|
||||
767px and 768px, and preserve desktop eligibility without widening scope.
|
||||
|
||||
## Parallelism
|
||||
|
||||
`sequential`
|
||||
|
||||
## Inputs
|
||||
|
||||
- [Requirements](../../specs/ui/requirements/composer-focus-hint.md)
|
||||
- [System design](../../specs/ui/system-design/composer-focus-hint.md)
|
||||
- Existing `shouldShowChatFocusHint` tests and mobile slash-command fixture.
|
||||
|
||||
## Results
|
||||
|
||||
Completed on 2026-09-17.
|
||||
|
||||
- RED: the new 393px and 767px component cases failed because the hint was
|
||||
visible; 36 other tests passed before the production change.
|
||||
- GREEN: the two focused Vitest suites passed, 38 tests total. Coverage includes
|
||||
767px/768px boundaries, hint and padding changes in both resize directions,
|
||||
editor identity, draft preservation, and existing state eligibility.
|
||||
- ESLint passed with no warnings for the two component files and both modified
|
||||
browser specs. Prettier was applied to changed code.
|
||||
- Managed production-build E2E: mobile-chrome passed 1 test; chromium passed
|
||||
4 tests. Phone typing, slash-command selection, and sending work; desktop
|
||||
hint visibility and focus dismissal work.
|
||||
- The phone test passed again with `--no-build` to regenerate its screenshot
|
||||
after the desktop runner replaced the test output directory. Visual inspection
|
||||
confirmed the hint is absent, editing space is available, and toolbar/send
|
||||
controls remain visible. This reused the freshly verified production build.
|
||||
- Specification catalog validation, full specification lint, and `git diff
|
||||
--check` passed. The specification linter's 36 tests passed during design.
|
||||
|
||||
The initial browser attempts could not find Go. These successful E2E commands
|
||||
used `PATH=/usr/local/go/bin:$PATH` in this Pod. No production workaround or
|
||||
runner changes were needed. Unit tests supplied the defect-specific RED gate.
|
||||
|
||||
Public docs need no change: keyboard bindings, labels, and documented workflows
|
||||
are unchanged. Internal requirements, design, and delivery records are updated.
|
||||
|
||||
Fresh synthetic desktop and phone PR screenshots were captured and visually
|
||||
checked. Both capture runs passed. GitHub access and the user-approved commit
|
||||
identity were restored for publication.
|
||||
|
||||
## Review follow-up
|
||||
|
||||
Greptile identified that forced component rerenders could mask a broken
|
||||
viewport subscription. Responsive cases now use native Happy DOM viewport
|
||||
changes without parent rerenders. All 38 focused tests pass; disabling the
|
||||
subscription makes the boundary-transition cases fail. ESLint passes.
|
||||
Production behavior and screenshots are unchanged.
|
||||
|
||||
CodeRabbit identified an incomplete recorded ESLint command. The command now
|
||||
lists both component files and both browser specs; rerunning it passed.
|
||||
@@ -0,0 +1,40 @@
|
||||
---
|
||||
status: active
|
||||
system: ui
|
||||
created: 2026-09-17
|
||||
owners:
|
||||
- kandev
|
||||
---
|
||||
|
||||
# Composer Focus Hint Requirements
|
||||
|
||||
## Overview
|
||||
|
||||
UI owns the shared composer's responsive focus guidance, independent of agent
|
||||
and task lifecycle. Phone users focus the editor by tapping it.
|
||||
|
||||
## Requirements
|
||||
|
||||
### REQ-UI-COMPOSER-FOCUS-HINT-001: Responsive focus guidance
|
||||
|
||||
**Intent:** Keep keyboard guidance from consuming phone editing space.
|
||||
|
||||
#### Acceptance criteria
|
||||
|
||||
- **AC-UI-COMPOSER-FOCUS-HINT-001.1:** Below 768 CSS pixels, the composer shall
|
||||
omit the `/ to focus` hint and the editing space reserved solely for it,
|
||||
including when the editor is empty and unfocused.
|
||||
- **AC-UI-COMPOSER-FOCUS-HINT-001.2:** At widths of 768 CSS pixels or greater,
|
||||
the composer shall retain its existing hint eligibility: empty after trimming,
|
||||
unfocused, without an active clarification or pending review comments.
|
||||
- **AC-UI-COMPOSER-FOCUS-HINT-001.3:** Resizing across the phone boundary shall
|
||||
update hint visibility and reserved space without losing the draft. Tapping
|
||||
the phone editor, typing slash commands, and sending shall remain available.
|
||||
|
||||
## Out of scope
|
||||
|
||||
Keyboard bindings, tablet policy changes, executor caches, and new copy.
|
||||
|
||||
## Implementation Plans
|
||||
|
||||
- [Mobile composer focus hint](../../../plans/mobile-composer-focus-hint/plan.md)
|
||||
@@ -0,0 +1,55 @@
|
||||
---
|
||||
status: current
|
||||
system: ui
|
||||
requirements:
|
||||
- REQ-UI-COMPOSER-FOCUS-HINT-001
|
||||
---
|
||||
|
||||
# Composer Focus Hint System Design
|
||||
|
||||
## Purpose and boundaries
|
||||
|
||||
The shared composer owns presentation across task chat and quick chat. This
|
||||
change affects neither session state nor shortcut dispatch.
|
||||
|
||||
## Requirement mapping
|
||||
|
||||
| Requirement | Design section |
|
||||
| --- | --- |
|
||||
| REQ-UI-COMPOSER-FOCUS-HINT-001 | Components and control flow |
|
||||
|
||||
## Components and control flow
|
||||
|
||||
`shouldShowChatFocusHint` in `use-chat-input-container.ts` currently derives
|
||||
state eligibility without viewport information. Keep that state policy intact.
|
||||
In `ChatInputBody`, read `isMobile` from `useResponsiveBreakpoint` and derive
|
||||
one effective visibility value: `showFocusHint && !isMobile`. Use it for both
|
||||
`ChatInputFocusHint.visible` and the editor's conditional `pr-28` class.
|
||||
This keeps direct consumers and viewport transitions consistent and avoids
|
||||
hiding only the hint while retaining wasted editor space.
|
||||
|
||||
## Mobile composition
|
||||
|
||||
Use the existing inline composer and mobile slash-command flow as the nearest
|
||||
exemplar (`e2e/tests/chat/mobile-slash-command-composer.spec.ts`). The editor and
|
||||
send action retain their order, touch targets, scroll owner, dynamic viewport,
|
||||
and safe-area behavior. No additional drawer, navigation, or persisted state is
|
||||
needed. Width below 768px defines phone scope even with a fine pointer.
|
||||
|
||||
## Compatibility and recovery
|
||||
|
||||
The existing reactive breakpoint hook updates on boundary changes without
|
||||
remounting the editor. Wider touch devices retain current behavior. No new
|
||||
translation, API, persistence, permission, logging, or metrics contract is
|
||||
introduced. Existing editor focus and submission handlers are unchanged.
|
||||
|
||||
## Verification
|
||||
|
||||
Component tests drive the responsive hook through mobile and wider states,
|
||||
checking hint absence and removal of reserved padding together. Existing state
|
||||
predicate tests preserve wider eligibility. Playwright verifies phone tapping,
|
||||
slash-command entry, submission, and wider-screen hint behavior.
|
||||
|
||||
## Implementation Plans
|
||||
|
||||
- [Mobile composer focus hint](../../../plans/mobile-composer-focus-hint/plan.md)
|
||||
Reference in New Issue
Block a user