From 15a43894f9890da66e9d4a0131931a2ec5588a0a Mon Sep 17 00:00:00 2001 From: Codeman maintainer Date: Tue, 11 Aug 2026 19:36:19 +0200 Subject: [PATCH] chore: version packages --- CHANGELOG.md | 38 ++++++++++++++++++++++++++++++++++++++ CLAUDE.md | 4 +++- package-lock.json | 4 ++-- package.json | 2 +- 4 files changed, 44 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fed688df..9a6be28c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,43 @@ # aicodeman +## 1.16.5 + +### Patch Changes + +- Mobile keyboard dismissal, and a tidier Save/Close pair in the phone settings sheet. + + **The on-screen keyboard can finally be closed from inside the app.** The terminal + keeps focus on a hidden textarea and nothing ever released it, so once the keyboard + was up it covered roughly half the screen with no way out but the OS back gesture. + Two gestures now dismiss it: + - **A tap outside the terminal** (header, tab strip, empty page chrome). Deliberately + narrow: it only fires while the terminal input actually holds focus, never inside + the terminal (tap classification owns that decision), and never on a control, since + anything focusable is about to take focus itself and the keyboard accessory bar + exists to be used _while_ the keyboard is open. A scroll ends in `touchend` too, so + finger travel is tracked from `touchstart` and only a near-stationary gesture counts + as a tap, sharing the terminal's own 8px threshold so both agree on tap-vs-scroll. + Scrolling to read something mid-compose no longer drops the composer. + - **A second tap on inert transcript content.** Every terminal tap used to re-focus, + which left the accessory bar's chevron as the only way out. Scoped to inert rows on + purpose: the prompt row keeps focus-then-position, so a second tap there still + places the caret, and actionable rows (readbacks, `esc to interrupt` status rows, + menu selections) still blur as before. + + **Settings sheet header on phones.** Below 860px Save moves into the header, which + left the two ways out of the sheet as a fat accent pill beside a bare glyph. Save and + Close now share a recessed tray with matching 36px pill geometry, reading as one + 44px cluster the height of the phone header. Tray colors come from skin tokens, so + the light skins keep their look, and the tray stays off the sheets that carry a lone + close button. + + Also fixes a test that could never have caught a regression: the case asserting that + tapping a control does _not_ dismiss the keyboard was picking a button from the + hidden welcome overlay, whose rect still measures while the hit-test lands on the + terminal underneath, so it passed for the wrong reason and stayed green even with the + exemption deleted. All four guards in the dismiss handler are now individually + pinned. + ## 1.16.4 ### Patch Changes diff --git a/CLAUDE.md b/CLAUDE.md index 5a8802f6..57d1529b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -74,7 +74,7 @@ When user says "COM": CI runs `npm run check:lockfile` on every push/PR, so lockfile drift fails the build even if the `version-packages` script is bypassed. -**Version**: 1.16.4 (must match `package.json`) +**Version**: 1.16.5 (must match `package.json`) ## Project Overview @@ -280,6 +280,8 @@ Frontend JS modules have `@fileoverview` with `@dependency`/`@loadorder` tags. L **Shell keyboard accessory bar + one-shot Ctrl** (issue #262, `keyboard-accessory.js`): a **shell**-mode session automatically swaps the mobile accessory bar for terminal controls (Ctrl, Esc, Tab, four arrows, paste, dismiss); every other mode keeps the agent bar. `setMode()` now records the user's `extendedKeyboardBar` preference as the **base** layout and `refreshForActiveSession()` (called from `selectSession`) resolves base-vs-shell, so a settings save during a shell session cannot yank the bar away and switching back restores the user's choice. ⚠️ **Ctrl is a ONE-SHOT modifier applied in `terminal.onData`, not in a keydown handler**: a virtual keyboard emits no usable key events, so the character only exists as onData text. The hook sits AFTER `shouldSuppressTerminalQueryResponse` (xterm answers DA/CPR through onData too, and one of those would silently spend the modifier) and BEFORE every send path, so the control byte follows the normal control-char route. ⚠️ **Not every onData chunk is a keystroke**, and the query filter is not enough on its own: xterm ALSO emits mouse and focus reports on its own initiative, so the hook skips them via `isTerminalFocusOrMouseReport()` (they still reach the PTY, they just don't count as the next key). The mouse half is live — a shell session keeps the NARROW strip, so mouse DECSETs reach the browser and one tap while vim/htop runs spent the armed modifier silently (measured). The focus half is defense in depth: `FOCUS_ESCAPE_FILTER` in `session.ts` strips `\x1b[?1004h` from every PTY read, so `sendFocusMode` never turns on today; if it ever did, the bar's own post-key refocus would emit `\x1b[I` and eat the modifier before the user typed. ⚠️ It must disarm on ALL of: use, second tap, any other accessory key, session switch, keyboard dismissal, and a layout swap; a modifier left armed turns the next innocent keystroke into a control byte. ⚠️ **onData is not the only input path** — with `cjkInputEnabled` on, the CJK textarea owns the keyboard (onData returns early for everything it swallows, and the focus router sends `terminal.focus()` there, which is where the bar refocuses after every key), so `_handleCjkInput()` applies the modifier too. It is that module's single choke point to the PTY, so one call covers typed characters, IME flushes, Enter, backspace and arrows. Without it an armed modifier could neither fire NOR be spent, and survived to a later keystroke. Mapping is `ctrlByteFor()` (`code & 0x1f` over @A-Z[\]^_ and a-z, plus Ctrl+Space=NUL / Ctrl+?=DEL); characters with no control equivalent pass through unchanged, like a hardware keyboard. ⚠️ The armed style is `.accessory-btn.accessory-btn-ctrl.armed` (0,3,0) in BOTH stylesheets, and it cannot outrank mobile.css's light-skin repaint at **(0,3,1)** (`:is()` inherits its most specific argument, and that list holds `.btn-toolbar.btn-shell`) — so that rule excludes the state by hand as `.accessory-btn:not(.armed)`. Without the exclusion the armed button renders identically to a resting one on all four light skins, which is worse than no armed style at all. +**Dismissing the on-screen keyboard** (PRs #279/#280, `terminal-ui.js`): the terminal parks focus on a hidden textarea that nothing used to release, so TWO gestures now blur it, and they own different regions. **(1)** `_installMobileKeyboardDismiss()` — a document-level `touchend` that fires only while the terminal input actually holds focus, **never inside `#terminalContainer`** (tap classification owns that) and **never on a control** (`MOBILE_KEYBOARD_DISMISS_EXEMPT_SELECTOR`, matched with `closest()` so an icon inside a button counts). Session tabs are covered by the selector's `[tabindex]:not([tabindex="-1"])` arm, which is what stops a tab tap from blurring and then being re-focused by `selectSession()`. **(2)** In `_handleMobileTerminalTap`, a second tap on **inert `content`** (`startedWithTerminalFocus`) blurs instead of re-focusing. ⚠️ Scoped to `content` on purpose: the prompt row (`input`) keeps focus-then-position so a second tap still places the caret, and actionable rows blur earlier via `_isActionableMobileTerminalTap`. ⚠️ **A scroll ends in `touchend` too** — dismissing there closes the keyboard and drops the composer mid-read, so travel is tracked from `touchstart` and multi-touch is never a tap. Both classifiers MUST share one threshold: `initTerminal`'s `TAP_THRESHOLD` reads `MOBILE_KEYBOARD_DISMISS_TAP_SLOP`, since a gesture the terminal calls a scroll and the dismiss handler calls a tap is exactly that bug. ⚠️ **`test:ci` excludes `test/mobile/**`, so CI cannot see the only test covering (1)** — run `npm test -- test/mobile/keyboard.test.ts` by hand and diff the FAIL list against master. That blind spot is why merging the two PRs, which conflicted semantically but not textually, produced a red suite with two green CI checks. + **Phone toolbar: Enter replaces Shell** (post-1.8.0): inside `@media (max-width: 430px)` `btn-shell` is `display:none` and `btn-enter` takes its slot (`order: 4`); starting a shell moved into the Run dropdown (`Terminal / Shell` → `setRunMode('shell')` → `run()` → `runShell()`, button label "Run SH"). `runMode` is `z.string().max(20)` server-side, so new modes need no schema change. Desktop and tablet keep the green Run Shell button unchanged. ⚠️ **`sendEnterKey()` MUST go through `terminal._core.coreService.triggerDataEvent('\r', true)`** — not `sendInput()`, and never a raw POST to `/api/sessions/:id/input`. `localEchoEnabled` defaults to `MobileDetection.isTouchDevice()`, so on every phone the characters you type are buffered in the `LocalEchoOverlay` and have **never reached the PTY**; the `onData` Enter branch in terminal-ui.js is what flushes `pendingText` first and only then sends `\r` (after an 80ms delay so text lands first). Sending a bare `\r` submits an empty line and strands the typed text on screen, so the button looks dead. Replaying the keypress reuses the overlay flush, the flushed-offset cleanup and the ordering instead of reimplementing them. `KeyboardAccessory.sendKey()` is for escape sequences (arrows/Esc) and is the WRONG template to copy for input. diff --git a/package-lock.json b/package-lock.json index 41eeac82..553a6481 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "aicodeman", - "version": "1.16.4", + "version": "1.16.5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "aicodeman", - "version": "1.16.4", + "version": "1.16.5", "hasInstallScript": true, "license": "MIT", "workspaces": [ diff --git a/package.json b/package.json index 3a6c4c08..0fa75284 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "aicodeman", - "version": "1.16.4", + "version": "1.16.5", "description": "Mission control for AI coding agents - run 20 autonomous agents with real-time monitoring and session persistence", "type": "module", "main": "dist/index.js",