From 8caafced18830f9632080cbc1807f2a6b97705cf Mon Sep 17 00:00:00 2001 From: keliangliang Date: Tue, 27 Oct 2026 19:39:10 +0800 Subject: [PATCH] =?UTF-8?q?Task:=20=E5=B0=86=E8=81=8A=E5=A4=A9=E8=BE=93?= =?UTF-8?q?=E5=85=A5=E6=A1=86=E7=9A=84=E6=A8=A1=E5=9E=8B=E4=B8=8E=E6=80=9D?= =?UTF-8?q?=E8=80=83=E7=AD=89=E7=BA=A7=E9=80=89=E6=8B=A9=E5=99=A8=E6=95=B4?= =?UTF-8?q?=E4=BD=93=E5=8F=B3=E7=A7=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/chat-composer-right-controls/change.md | 21 +++ src/features/chat/ChatComposer.test.tsx | 61 +++++++- src/features/chat/ChatComposer.tsx | 148 ++++++++++---------- src/styles/index.css | 16 ++- 4 files changed, 167 insertions(+), 79 deletions(-) create mode 100644 docs/chat-composer-right-controls/change.md diff --git a/docs/chat-composer-right-controls/change.md b/docs/chat-composer-right-controls/change.md new file mode 100644 index 0000000..785e236 --- /dev/null +++ b/docs/chat-composer-right-controls/change.md @@ -0,0 +1,21 @@ +# Chat composer right-side controls + +## Intent + +Move the model and thinking-level selectors out of the left side of the chat composer footer and into the right-aligned action cluster, without changing any composer behavior. + +## Spec + +- Keep model selection before thinking-level selection. +- Place both selectors in the right-side action group before the optional byte counter and the send, follow-up, or stop action. +- Preserve selector, send, follow-up, stop, disabled, search, and accessibility behavior. +- Keep the action group wrappable and constrained to the composer width so narrow layouts do not overflow or hide controls. +- Do not change model controls outside `ChatComposer`. + +## Plan + +1. Add a layout regression test that asserts the right-side group membership and DOM order, including the byte counter. +2. Group the existing composer footer controls in a right-aligned wrapping container and adjust only the related CSS flex rules. +3. Run the focused `ChatComposer` tests, formatting, lint, type checking, and the project verification gate. + +Risk: standard, because this is a localized user-visible Renderer layout change with multiple acceptance criteria and no API, persistence, or security impact. diff --git a/src/features/chat/ChatComposer.test.tsx b/src/features/chat/ChatComposer.test.tsx index c5a6950..9505f2b 100644 --- a/src/features/chat/ChatComposer.test.tsx +++ b/src/features/chat/ChatComposer.test.tsx @@ -154,6 +154,54 @@ describe("ChatComposer", () => { expect(onThinkingLevelChange).toHaveBeenCalledWith("xhigh"); }); + it("orders selectors, byte count, and submit within the right action group", () => { + const { container } = render( + , + ); + + const actions = container.querySelector( + ".chat-composer-actions", + ); + const rightActions = container.querySelector( + ".chat-composer-right-actions", + ); + const config = container.querySelector( + ".chat-composer-config", + ); + const count = container.querySelector(".chat-composer-count"); + const submit = screen.getByRole("button", { name: "Send" }); + + expect(actions).toContainElement(rightActions); + expect(rightActions).toContainElement(config); + expect(config?.querySelectorAll('[role="combobox"]')).toHaveLength(2); + expect(rightActions).toContainElement(count); + expect(rightActions).toContainElement(submit); + expect(Array.from(rightActions?.children ?? [])).toEqual([ + config, + count, + submit, + ]); + }); + it("filters and completes runtime slash commands with keyboard controls", async () => { render( { const actions = container.querySelector( ".chat-composer-actions", ); + const rightActions = container.querySelector( + ".chat-composer-right-actions", + ); const config = container.querySelector( ".chat-composer-config", ); - expect(actions).toContainElement(config); + expect(actions).toContainElement(rightActions); + expect(rightActions).toContainElement(config); expect(config?.querySelectorAll('[role="combobox"]')).toHaveLength(2); - expect(actions?.lastElementChild).toHaveClass("chat-composer-submit"); + expect(rightActions?.lastElementChild).toHaveClass("chat-composer-submit"); const css = readFileSync("src/styles/index.css", "utf8"); expect(css).toMatch(/\.chat-composer-actions\s*\{[^}]*flex-wrap:\s*wrap;/s); expect(css).toMatch( - /\.chat-composer-config\s*\{[^}]*min-width:\s*0;[^}]*flex:\s*1 1 [^;]+;/s, + /\.chat-composer-right-actions\s*\{[^}]*max-width:\s*100%;[^}]*flex-wrap:\s*wrap;[^}]*justify-content:\s*flex-end;/s, + ); + expect(css).toMatch( + /\.chat-composer-config\s*\{[^}]*min-width:\s*0;[^}]*flex:\s*0 1 auto;/s, ); expect(css).toMatch( /\.chat-composer-config \.searchable-select\s*\{[^}]*max-width:\s*min\(220px, 100%\);[^}]*flex:\s*0 1 auto;/s, diff --git a/src/features/chat/ChatComposer.tsx b/src/features/chat/ChatComposer.tsx index 3ce0fa9..3aba489 100644 --- a/src/features/chat/ChatComposer.tsx +++ b/src/features/chat/ChatComposer.tsx @@ -272,80 +272,82 @@ export function ChatComposer({ )}
-
- { - const separator = value.indexOf("/"); - if (separator < 1 || !onModelChange) return; - void runMutation(() => - onModelChange( - value.slice(0, separator), - value.slice(separator + 1), - ), - ); - }} - /> - ({ - value: level, - label: t(THINKING_KEYS[level]), - }), - )} - onChange={(value) => { - if (!onThinkingLevelChange) return; - void runMutation(() => - onThinkingLevelChange(value as ChatThinkingLevel), - ); - }} - /> +
+
+ { + const separator = value.indexOf("/"); + if (separator < 1 || !onModelChange) return; + void runMutation(() => + onModelChange( + value.slice(0, separator), + value.slice(separator + 1), + ), + ); + }} + /> + ({ + value: level, + label: t(THINKING_KEYS[level]), + }), + )} + onChange={(value) => { + if (!onThinkingLevelChange) return; + void runMutation(() => + onThinkingLevelChange(value as ChatThinkingLevel), + ); + }} + /> +
+ {(byteLength > MAX_INPUT_BYTES * 0.8 || tooLarge) && ( + + {formatNumber(byteLength)} / {formatNumber(MAX_INPUT_BYTES)} + + )} + {showStop && ( + + )} + {(!running || hasDraft) && ( + + )}
- {(byteLength > MAX_INPUT_BYTES * 0.8 || tooLarge) && ( - - {formatNumber(byteLength)} / {formatNumber(MAX_INPUT_BYTES)} - - )} - {showStop && ( - - )} - {(!running || hasDraft) && ( - - )}
diff --git a/src/styles/index.css b/src/styles/index.css index d3c5e35..2fb100b 100644 --- a/src/styles/index.css +++ b/src/styles/index.css @@ -4221,16 +4221,26 @@ details[open] > summary > .chat-disclosure-icon { flex-wrap: wrap; align-items: center; justify-content: flex-end; - gap: 8px; padding-top: 7px; } +.chat-composer-right-actions { + max-width: 100%; + margin-left: auto; + display: flex; + flex: 0 1 auto; + flex-wrap: wrap; + align-items: center; + justify-content: flex-end; + gap: 8px; +} .chat-composer-config { min-width: 0; - margin-right: auto; + max-width: 100%; display: flex; - flex: 1 1 auto; + flex: 0 1 auto; flex-wrap: wrap; align-items: center; + justify-content: flex-end; gap: 4px; } .chat-composer-config .searchable-select {