fix(mobile): merge-time fixes for the prompt composer (#444)

- styles.css: restate the composer overlay's own bottom gutter after the fold rules
  (the generic .paste-overlay longhand erased it: 0px flat, hinge strip replacing it
  folded) and subtract the fold strip from the dialog's max-height
- test/foldable-layout.test.ts: simulate the cascade for
  .paste-overlay.prompt-composer-overlay (fails without the CSS fix); pin the palette
  anchor by name instead of ELEMENTS.at(-1)
- keyboard-accessory.js: guard the app global in refreshForActiveSession() like the
  rest of the file
- keyboard-accessory.js: a whitespace-only draft is empty (Send no longer submits
  blank lines); the text still goes out untrimmed
- keyboard-accessory.js: derive _composerMaxLength and the frame refusal from one
  64 KiB frame limit minus both bracketed-paste markers so they cannot drift
- keyboard-accessory.js: translate the textarea placeholder and label at build time,
  since the DOM translator skips <textarea> subtrees
- i18n.js: zh-CN entries for the composer dialog copy
- docs/wiki/Mobile-Guide.md: describe the Compose key instead of a clipboard key
- CLAUDE.md: a "Mobile prompt composer" paragraph after the accessory bar one
- test/mobile-prompt-composer.test.ts: pin the whitespace rule and the derived budget

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
(cherry picked from commit f6725ba52da17b0bdbee8be3b5011e7cae514f69)
This commit is contained in:
Codeman maintainer
2026-09-21 04:39:28 +02:00
parent 0f955327b2
commit d8e85285c9
7 changed files with 102 additions and 17 deletions
+8 -2
View File
@@ -313,11 +313,17 @@ describe('fold reserved region: every centred overlay is covered', () => {
/**
* The elements whose padding cascade is simulated: every derived overlay as
* a bare element, plus the open command palette, which is a `.modal` wearing
* two more classes and the one overlay mobile.css pads with a shorthand.
* two more classes and the one overlay mobile.css pads with a shorthand, plus
* the mobile prompt composer, a `.paste-overlay` wearing a second class that
* carries its own `padding` shorthand. The derived list cannot see the
* composer (it inherits the centring declarations rather than declaring
* them), and simulating `['paste-overlay']` alone stayed green while the
* generic `.paste-overlay` fold rule erased the composer's bottom gutter.
*/
const ELEMENTS: { name: string; classes: string[] }[] = [
...CENTRED_OVERLAYS.map((o) => ({ name: o.selector, classes: classCompound(o.selector)! })),
{ name: '.modal.command-palette-modal.active', classes: ['modal', 'command-palette-modal', 'active'] },
{ name: '.paste-overlay.prompt-composer-overlay', classes: ['paste-overlay', 'prompt-composer-overlay'] },
];
it('simulates the cascade the browser measured', () => {
@@ -330,7 +336,7 @@ describe('fold reserved region: every centred overlay is covered', () => {
const picker = ['path-picker-overlay'];
expect(cascadedPadding(picker, 'right', 393, false)).toBe('0');
expect(cascadedPadding(picker, 'right', 626, false)).toBe('16px');
const palette = ELEMENTS.at(-1)!.classes;
const palette = ELEMENTS.find((e) => e.name === '.modal.command-palette-modal.active')!.classes;
expect(cascadedPadding(palette, 'right', 393, false)).toBeNull();
expect(cascadedPadding(palette, 'right', 626, false)).toBe('0.75rem');
expect(cascadedPadding(palette, 'bottom', 626, false)).toBe('0');
+26
View File
@@ -299,6 +299,32 @@ describe('mobile prompt composer', () => {
expect(bar._composerDrafts.get('session-1')).toHaveLength(65525);
});
it('treats a whitespace-only draft as empty instead of submitting blank lines', () => {
const { app, bar, document } = loadComposer();
bar.composePrompt();
textarea(document).value = ' \n\n ';
(document.querySelector('.paste-send') as HTMLButtonElement).click();
expect(app._sendInputAsync).not.toHaveBeenCalled();
expect(document.querySelector('.prompt-composer-overlay')).not.toBeNull();
});
it('derives the prompt budget from the 64 KiB input frame minus both paste markers', () => {
// ws-routes.ts drops a frame longer than MAX_INPUT_LENGTH without an ACK,
// so a prompt of exactly the budget must produce a frame of exactly 64 KiB.
const { app, bar, document } = loadComposer();
expect(bar._composerMaxLength).toBe(64 * 1024 - '\x1b[200~\x1b[201~'.length);
bar.composePrompt();
textarea(document).value = 'y'.repeat(bar._composerMaxLength);
(document.querySelector('.paste-send') as HTMLButtonElement).click();
expect(app._sendInputAsync).toHaveBeenCalledOnce();
expect((app._sendInputAsync.mock.calls[0][1] as string).length).toBe(64 * 1024);
expect(app.showToast).not.toHaveBeenCalled();
});
it('preserves the draft and focuses xterm when Use terminal keyboard is chosen', () => {
const { app, bar, document, localEcho, runTimers } = loadComposer();
const composeButton = mountComposeButton(bar, document);