From 2054dd44c3b613d51240a8c6b7cf5a2d3078b2bc Mon Sep 17 00:00:00 2001 From: Yaswanth Narvaneni Date: Fri, 16 Oct 2026 12:33:25 +0100 Subject: [PATCH] Raise the keyboard from Type in one tap On phones, Type opened the input dock and left Composer's draft unfocused, so writing a message took a second tap on the editor. Type's tap is the request to type: within the same gesture, after focusing the dock (which still routes Direct to the terminal), focus Composer's draft so mobile browsers raise the keyboard at once. Everything else from #359 stays: sends, command picks, uploads and scrolling leave a dismissed keyboard closed, and a touch outside the editor still retires residual focus. The input-mode test mirrors the new gesture, expects the draft focused after Type, and models the open keyboard in the Composer scroll case. The initial JS budget rises from 802 to 803 KiB. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AWodzpMyqdBcX25EE9HhDG --- FEATURES.md | 4 +++- scripts/check-web-assets.mjs | 2 +- web/src/App.tsx | 15 +++++++++++---- .../components/TerminalView.inputMode.test.ts | 17 +++++++++++------ 4 files changed, 26 insertions(+), 12 deletions(-) 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;