mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-02 13:39:41 +02:00
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:
@@ -451,6 +451,17 @@
|
||||
'Show Shortcuts': '显示快捷键',
|
||||
'Full shortcut reference': '完整快捷键参考',
|
||||
|
||||
// Mobile prompt composer (keyboard-accessory.js). The textarea's own
|
||||
// placeholder and label are looked up by the module at build time, since
|
||||
// the DOM translator skips <textarea> subtrees.
|
||||
'Compose prompt': '撰写提示词',
|
||||
'Compose prompt, draft saved': '撰写提示词,草稿已保存',
|
||||
'Resume saved prompt draft': '继续编辑已保存的提示词草稿',
|
||||
'Enter adds a new line': '按 Enter 换行',
|
||||
'Write your prompt…': '请输入提示词…',
|
||||
'Use terminal keyboard': '使用终端键盘',
|
||||
'Uploading…': '上传中…',
|
||||
|
||||
// Mobile overview (phone home screen)
|
||||
'Needs you': '需要你',
|
||||
'Current sessions': '当前会话',
|
||||
|
||||
@@ -636,6 +636,16 @@ function applyOneShotCtrl(data) {
|
||||
return { data, consumed: true };
|
||||
}
|
||||
|
||||
// The composer's Send goes out as ONE bracketed-paste frame on the WebSocket
|
||||
// input path, and ws-routes.ts drops a frame longer than MAX_INPUT_LENGTH
|
||||
// (config/terminal-limits.ts: 64 KiB, compared in UTF-16 code units) WITHOUT
|
||||
// an ACK, which would wedge the durable input queue. So the prompt budget is
|
||||
// that limit minus the two markers, derived here once so the refusal in
|
||||
// _sendComposedPrompt() and the toast that names the maximum cannot drift.
|
||||
const COMPOSER_INPUT_FRAME_LIMIT = 64 * 1024;
|
||||
const COMPOSER_PASTE_START = '\x1b[200~';
|
||||
const COMPOSER_PASTE_END = '\x1b[201~';
|
||||
|
||||
/**
|
||||
* KeyboardAccessoryBar - Quick action buttons shown above keyboard when typing.
|
||||
*/
|
||||
@@ -654,9 +664,8 @@ const KeyboardAccessoryBar = {
|
||||
_composerDrafts: new Map(),
|
||||
_composerUploads: new Map(),
|
||||
_composerOverlay: null,
|
||||
// Leave room for both six-character bracketed-paste markers under the
|
||||
// WebSocket input frame's 64 KiB character limit.
|
||||
_composerMaxLength: 65524,
|
||||
// Longest prompt Send accepts: the input frame limit minus both markers.
|
||||
_composerMaxLength: COMPOSER_INPUT_FRAME_LIMIT - COMPOSER_PASTE_START.length - COMPOSER_PASTE_END.length,
|
||||
|
||||
/** HTML for simple mode: arrows, commands, Compose, Esc, dismiss */
|
||||
_simpleButtons: `
|
||||
@@ -833,7 +842,8 @@ const KeyboardAccessoryBar = {
|
||||
* the next one. */
|
||||
refreshForActiveSession() {
|
||||
this.clearCtrl();
|
||||
if (this._composerOverlay && this._composerOverlay.dataset.sessionId !== app.activeSessionId) {
|
||||
const activeSessionId = typeof app !== 'undefined' ? app.activeSessionId : null;
|
||||
if (this._composerOverlay && this._composerOverlay.dataset.sessionId !== activeSessionId) {
|
||||
this._composerOverlay._closeComposer?.({ restoreFocus: false });
|
||||
}
|
||||
this._applyLayout(this._resolveMode());
|
||||
@@ -1206,12 +1216,11 @@ const KeyboardAccessoryBar = {
|
||||
// Match xterm's prepareTextForTerminal(): CR keeps embedded newlines inside
|
||||
// the single-line input transport and is what terminal.paste() emitted.
|
||||
const pasteText = text.replace(/\r?\n/g, '\r');
|
||||
const payload = `\x1b[200~${pasteText}\x1b[201~`;
|
||||
if (payload.length > 65536) {
|
||||
if (pasteText.length > this._composerMaxLength) {
|
||||
app.showToast?.(`Prompt is too long to send (maximum ${this._composerMaxLength.toLocaleString()} characters)`, 'error');
|
||||
return false;
|
||||
}
|
||||
app._sendInputAsync(sessionId, payload);
|
||||
app._sendInputAsync(sessionId, `${COMPOSER_PASTE_START}${pasteText}${COMPOSER_PASTE_END}`);
|
||||
setTimeout(() => app._sendInputAsync(sessionId, '\r', { useMux: true }), 120);
|
||||
return true;
|
||||
},
|
||||
@@ -1266,6 +1275,13 @@ const KeyboardAccessoryBar = {
|
||||
|
||||
this._composerOverlay = overlay;
|
||||
const textarea = overlay.querySelector('.prompt-composer-textarea');
|
||||
// i18n.js skips <textarea> subtrees (what is typed there is user content),
|
||||
// so the placeholder and label are translated here, when the dialog is built.
|
||||
const i18n = typeof window !== 'undefined' ? window.CodemanI18n : undefined;
|
||||
if (typeof i18n?.t === 'function') {
|
||||
textarea.placeholder = i18n.t('Write your prompt…');
|
||||
textarea.setAttribute('aria-label', i18n.t('Prompt'));
|
||||
}
|
||||
const fileInput = overlay.querySelector('.paste-file-input');
|
||||
const imageButton = overlay.querySelector('.paste-image');
|
||||
const sendButton = overlay.querySelector('.paste-send');
|
||||
@@ -1292,8 +1308,10 @@ const KeyboardAccessoryBar = {
|
||||
};
|
||||
overlay._closeComposer = close;
|
||||
const send = () => {
|
||||
// Whitespace-only counts as empty (it would submit blank lines), but the
|
||||
// text goes out untrimmed so deliberate leading or trailing lines survive.
|
||||
const text = textarea.value;
|
||||
if (!text || !this._sendComposedPrompt(sessionId, text)) return;
|
||||
if (!text.trim() || !this._sendComposedPrompt(sessionId, text)) return;
|
||||
app._echoPassthroughSessions?.delete(sessionId);
|
||||
this._composerDrafts.delete(sessionId);
|
||||
this._syncComposerDraftIndicator();
|
||||
|
||||
@@ -13809,7 +13809,10 @@ body.touch-device.cjk-input-visible .main {
|
||||
|
||||
.prompt-composer-dialog {
|
||||
max-width: 560px;
|
||||
max-height: calc(100dvh - min(15dvh, 72px) - 12px - env(safe-area-inset-bottom));
|
||||
/* The overlay's top inset and bottom gutter, plus the strip a folding device
|
||||
reserves below the dialog (0px everywhere else; see the fold rules at the
|
||||
end of this file). */
|
||||
max-height: calc(100dvh - min(15dvh, 72px) - 12px - env(safe-area-inset-bottom) - var(--fold-block-end));
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
@@ -18683,6 +18686,19 @@ html[data-session-list="sidebar"][data-sidebar="collapsed"] .btn-sidebar-toggle
|
||||
padding-bottom: var(--fold-block-end);
|
||||
}
|
||||
|
||||
/* The mobile prompt composer is a .paste-overlay with a gutter of its own: a
|
||||
three-value `padding` shorthand whose bottom is 12px plus the safe area. The
|
||||
generic rule above is a later longhand at the same specificity, so it ERASED
|
||||
that gutter (measured at 393x852: padding-bottom 0 instead of 12px, and with
|
||||
the fold variables set the hinge strip stood in for the gutter instead of
|
||||
adding to it). Restate the composer's bottom gutter on top of the strip; the
|
||||
side has no gutter of its own. The dialog's height cap subtracts the same
|
||||
strip where it is declared (.prompt-composer-dialog). */
|
||||
.prompt-composer-overlay {
|
||||
padding-right: var(--fold-inline-end);
|
||||
padding-bottom: calc(12px + env(safe-area-inset-bottom) + var(--fold-block-end));
|
||||
}
|
||||
|
||||
/* The response viewer is a bottom sheet, so a vertical hinge running through it
|
||||
is fine, since it is a wide surface like the terminal and inset dialogs are what
|
||||
the fold guidance is about. A horizontal hinge is not: in tabletop pose the
|
||||
|
||||
Reference in New Issue
Block a user