diff --git a/FEATURES.md b/FEATURES.md index e3d1ea81..e4c440fc 100644 --- a/FEATURES.md +++ b/FEATURES.md @@ -181,7 +181,9 @@ Drafts stay in this browser and re-anchor to changed files on refresh. dragged to either edge. - The **Type** button opens an input dock with two modes: **Composer** (edit a draft with IME, dictation, and images; **Send** adds Enter) and **Direct** - (keys go straight to the terminal). + (keys go straight to the terminal). Its tap raises the keyboard in either + mode; afterwards, sends, scrolling, and shortcut actions leave a dismissed + keyboard closed. - The `>_` button in Composer offers slash commands for Claude Code, Codex, Pi, Kimi Code, Grok Build, and Antigravity CLI. - Install as a PWA for an app-style window; see diff --git a/scripts/check-web-assets.mjs b/scripts/check-web-assets.mjs index 41eafca3..0b51727d 100644 --- a/scripts/check-web-assets.mjs +++ b/scripts/check-web-assets.mjs @@ -6,7 +6,7 @@ import { fileURLToPath, URL } from "node:url"; const publicRoot = fileURLToPath(new URL("../server/public/", import.meta.url)); const maxFileCount = 160; const maxTotalBytes = 14 * 1024 * 1024; -const maxInitialJsBytes = 802 * 1024; +const maxInitialJsBytes = 803 * 1024; const maxInitialJsGzipBytes = 256 * 1024; const maxInitialCssBytes = 196 * 1024; diff --git a/web/src/App.tsx b/web/src/App.tsx index d20887cc..6f0e11ad 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -4090,10 +4090,17 @@ export default function App() { activateTerminalSurface(); setTerminalComposerOpen(true); }); - // Give the dock focus; Composer's editor opens the keyboard on tap. - document - .querySelector(".terminal-composer") - ?.focus({ preventScroll: true }); + // The tap is the request to type. The dock routes Direct to the + // terminal; in Composer, focus the draft within this gesture so + // phones raise the keyboard in one tap. Later dock focus (sends, + // commands, uploads) stays neutral. + const dock = + document.querySelector(".terminal-composer"); + dock?.focus({ preventScroll: true }); + if (dock?.dataset.inputMode === "composer") + dock + .querySelector(".terminal-composer-input") + ?.focus({ preventScroll: true }); }} > diff --git a/web/src/components/TerminalView.inputMode.test.ts b/web/src/components/TerminalView.inputMode.test.ts index e141c3bc..26882deb 100644 --- a/web/src/components/TerminalView.inputMode.test.ts +++ b/web/src/components/TerminalView.inputMode.test.ts @@ -298,6 +298,7 @@ async function registerDomTests() { mobileShortcuts = [[], []]; }); afterEach(async () => { + document.documentElement.classList.remove("keyboard-open"); if (root) await act(async () => root?.unmount()); root = null; clearTerminalComposerDraft(draftKey); @@ -379,7 +380,8 @@ async function registerDomTests() { } async function typeGesture() { // Match App's Type click: open+commit, then synchronously focus the dock - // root, whose handler routes Direct focus, all in one gesture. + // root, whose handler routes Direct focus, and in Composer the draft, + // all in one gesture. await act(async () => { flushSync(() => { open = true; @@ -388,6 +390,10 @@ async function registerDomTests() { const target = container.querySelector(".terminal-composer"); expect(target).not.toBeNull(); target!.focus({ preventScroll: true }); + if (target!.dataset.inputMode === "composer") + target! + .querySelector(".terminal-composer-input") + ?.focus({ preventScroll: true }); // Assert during the gesture, before act can flush any later work. if ( selectedMode() === "direct" && @@ -667,6 +673,9 @@ async function registerDomTests() { await mount(); if (mode !== "closed") await typeGesture(); if (mode === "direct") await choose("direct"); + // Type's tap raised the keyboard in Composer's draft; App marks it. + if (mode === "composer") + document.documentElement.classList.add("keyboard-open"); await frame(mouseReporting); const term = currentTerminal(); term.modes.mouseTrackingMode = mouseReporting ? "drag" : "none"; @@ -961,17 +970,13 @@ async function registerDomTests() { }, ); - test("Type keeps Composer neutral until its native draft is explicitly focused", async () => { + test("Type focuses Composer's draft in one tap, and later work stays neutral", async () => { await mount(); await typeGesture(); expect(selectedMode()).toBe("composer"); - expect(document.activeElement).toBe( - container.querySelector(".terminal-composer"), - ); const editor = container.querySelector( '[aria-label="Terminal input draft"]', )!; - act(() => editor.focus()); expect(document.activeElement).toBe(editor); act(() => editor.blur()); const focused = document.activeElement;