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
+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);