Skip to content

Commit fb02f91

Browse files
author
weiesky.wangc
committed
ui(chat): PC composer polish — 1000px cap, themed strip, white card + shadow, focus-expand animation
- cap the chat column at 1400px and the composer card at 1000px (centered, responsive below) - new --bg-input-strip / --bg-composer / --shadow-composer tokens: light theme gets a light-gray strip (#F9F9F9) with a pure-white card and soft 8px shadow; dark theme keeps the strip at the page color (#0d0d0d) with the elevated card (#1e1e1e) - desktop textarea focus animates to two lines (height 0.2s ease), blurs back to content height (min one line); shared resizeChatTextarea helper (content-box clamp) replaces 7 raw autosize sites; mobile metrics/behavior unchanged
1 parent f3a8413 commit fb02f91

6 files changed

Lines changed: 92 additions & 24 deletions

File tree

history.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# Changelog
22

3+
## Unreleased
4+
5+
- ui(chat): **PC composer polish** — the composer card is capped at 1000px and centered on wide screens (shrinks responsively below); the card sits on its own input strip (`--bg-input-strip`, light gray `#F9F9F9` in the light theme) as a pure-white surface with a soft 8px drop shadow; and focusing the desktop textarea animates it to two lines (`height 0.2s ease`), collapsing back to content height (min one line) on blur. Autosize moved to a shared content-box helper (`resizeChatTextarea`) so an empty input snaps to exactly one line; mobile metrics/behavior unchanged.
6+
37
## 1.7.17 (2026-08-05)
48

59
- feat(v2): **persist agent identity from the wire header** — new `server/lib/v2/agent-id.js` parses `x-claude-code-agent-id` (`name@session-…` named / pure-hex anonymous) and the writer stores it as an optional `agent` field on journal req lines. Teammate display names no longer depend on the frontend's window-scoped heuristic registry, so names survive cold loads and mid-session gaps (new captures only).

src/components/chat/ChatInputBar.jsx

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,28 @@ const SpeechRec = typeof window !== 'undefined' && window.isSecureContext
1616
? (window.SpeechRecognition || window.webkitSpeechRecognition)
1717
: null;
1818

19+
// Desktop textarea vertical metrics (must stay in sync with ChatInputBar.module.css):
20+
// padding 12px top + 4px bottom, line-height 21px (14 * 1.5).
21+
const FOCUS_VPAD = 16;
22+
const LINE_H = 21;
23+
const MIN_LINES = 1;
24+
const MAX_LINES = 6;
25+
const FOCUS_LINES = 2;
26+
27+
// Shared autosize helper: measures the content box (scrollHeight minus vertical padding)
28+
// so an empty textarea snaps to exactly one line; `focused` pads short content up to two lines.
29+
// Mobile (isMobile && !isPad) keeps the legacy total-box behavior (different padding/font metrics).
30+
export function resizeChatTextarea(ta, { focused = false } = {}) {
31+
if (!ta) return;
32+
ta.style.height = 'auto';
33+
if (isMobile && !isPad) {
34+
ta.style.height = Math.min(ta.scrollHeight, 160) + 'px';
35+
return;
36+
}
37+
const content = Math.min(Math.max(ta.scrollHeight - FOCUS_VPAD, LINE_H * MIN_LINES), LINE_H * MAX_LINES);
38+
ta.style.height = (focused ? Math.max(content, LINE_H * FOCUS_LINES) : content) + 'px';
39+
}
40+
1941
const SPEECH_LANG_MAP = {
2042
zh: 'zh-CN', 'zh-TW': 'zh-TW', en: 'en-US', ko: 'ko-KR',
2143
ja: 'ja-JP', de: 'de-DE', es: 'es-ES', fr: 'fr-FR',
@@ -46,6 +68,20 @@ function ChatInputBar({ inputRef, inputEmpty, inputSuggestion, terminalVisible,
4668
const recRef = useRef(null);
4769
const anchorRef = useRef({ prefix: '', suffix: '' });
4870
const rootRef = useRef(null);
71+
// Desktop-only focus expand: focused empty/one-line input grows to two lines (animated via CSS).
72+
const [focusExpand, setFocusExpand] = useState(false);
73+
const focusExpandRef = useRef(false);
74+
75+
const resizeInput = (focused) => {
76+
const ta = inputRef?.current;
77+
if (!ta) return;
78+
resizeChatTextarea(ta, { focused });
79+
};
80+
const applyFocusExpand = (on) => {
81+
focusExpandRef.current = on;
82+
setFocusExpand(on);
83+
resizeInput(on);
84+
};
4985

5086
useEffect(() => () => {
5187
const rec = recRef.current;
@@ -173,8 +209,7 @@ function ChatInputBar({ inputRef, inputEmpty, inputSuggestion, terminalVisible,
173209
else interim += transcript;
174210
}
175211
t2.value = prefix + finalAcc + suffix;
176-
t2.style.height = 'auto';
177-
t2.style.height = Math.min(t2.scrollHeight, 120) + 'px';
212+
resizeChatTextarea(t2, { focused: focusExpandRef.current });
178213
setInterimText(interim);
179214
onChange?.({ target: t2 });
180215
};
@@ -338,12 +373,14 @@ function ChatInputBar({ inputRef, inputEmpty, inputSuggestion, terminalVisible,
338373
<div className={styles.textareaWithGhost}>
339374
<textarea
340375
ref={inputRef}
341-
className={styles.chatTextarea}
376+
className={`${styles.chatTextarea}${focusExpand ? ` ${styles.chatTextareaFocused}` : ''}`}
342377
placeholder={inputSuggestion ? '' : t('ui.chatInput.placeholder')}
343378
rows={1}
344379
onKeyDown={onKeyDown}
345380
onInput={handleTextareaInput}
346381
onPaste={handlePaste}
382+
onFocus={() => applyFocusExpand(true)}
383+
onBlur={() => applyFocusExpand(false)}
347384
/>
348385
{inputSuggestion && inputEmpty && (
349386
<div className={styles.ghostText}>{inputSuggestion}</div>

src/components/chat/ChatInputBar.module.css

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,18 @@
11
.chatInputBar {
2-
padding: 8px 16px 12px;
3-
background: var(--bg-input-bar);
2+
/* 宽屏下用 max() 把卡片限宽 1000px 并居中,窄屏退化为 16px 边距。
3+
外圈用 --bg-input-strip(亮 #F9F9F9 / 暗 #1e1e1e)托住白卡片 + 浅阴影。 */
4+
padding: 8px max(16px, calc((100% - 1000px) / 2)) 12px;
5+
background: var(--bg-input-strip);
46
flex-shrink: 0;
57
}
68

79
.chatInputWrapper {
810
display: flex;
911
flex-direction: column;
10-
background: var(--bg-elevated);
12+
background: var(--bg-composer);
1113
border: 1px solid var(--border-secondary);
1214
border-radius: 16px;
15+
box-shadow: var(--shadow-composer);
1316
transition: border-color 0.2s;
1417
overflow: visible;
1518
}
@@ -31,8 +34,9 @@
3134

3235
.chatTextarea {
3336
width: 100%;
34-
min-height: 22px;
35-
max-height: 120px;
37+
/* 行高 21px(14*1.5) + 上下 padding 16px:min 一行 37px,max 六行 136px */
38+
min-height: 37px;
39+
max-height: 136px;
3640
padding: 12px 14px 4px;
3741
background: #00000000;
3842
color: var(--text-primary);
@@ -43,6 +47,13 @@
4347
line-height: 1.5;
4448
font-family: var(--font-ui);
4549
box-sizing: border-box;
50+
/* 聚焦扩展/失焦回收由 JS 写 style.height,这里负责过渡动画 */
51+
transition: height 0.2s ease;
52+
}
53+
54+
/* 桌面聚焦态:不足两行时补到两行总高 58px(42 内容 + 16 padding) */
55+
.chatTextareaFocused {
56+
min-height: 58px;
4657
}
4758

4859
.chatTextarea::placeholder {
@@ -532,6 +543,8 @@
532543
max-height: 208px;
533544
padding: 18px 21px 8px;
534545
font-size: 22px;
546+
/* 移动端关闭高度动画,避免软键盘弹出时抖动 */
547+
transition: none;
535548
}
536549

537550
.ghostText {
@@ -627,9 +640,9 @@
627640
}
628641

629642
/* ─── iPad/Pad 模式:恢复桌面默认值 ─── */
630-
:global(html.pad-mode) .chatInputBar { padding: 8px 16px 12px; }
643+
:global(html.pad-mode) .chatInputBar { padding: 8px max(16px, calc((100% - 1000px) / 2)) 12px; }
631644
:global(html.pad-mode) .chatInputWrapper { border-radius: 16px; }
632-
:global(html.pad-mode) .chatTextarea { min-height: 22px; max-height: 120px; padding: 12px 14px 4px; font-size: 14px; }
645+
:global(html.pad-mode) .chatTextarea { min-height: 37px; max-height: 136px; padding: 12px 14px 4px; font-size: 14px; transition: height 0.2s ease; }
633646
:global(html.pad-mode) .ghostText { padding: 12px 14px 4px; font-size: 14px; }
634647
:global(html.pad-mode) .chatInputBottom { padding: 4px 6px 6px; gap: 4px; }
635648
:global(html.pad-mode) .plusBtn,

src/components/chat/ChatView.jsx

Lines changed: 9 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ import { TeamButton, TeamModal } from '../dashboard/TeamSessionPanel';
3131
import { WorkflowButton, WorkflowRunsModal } from '../dashboard/WorkflowRunsPanel';
3232
import SnapLineOverlay from '../common/SnapLineOverlay';
3333
import RoleFilterBar from './RoleFilterBar';
34-
import ChatInputBar from './ChatInputBar';
34+
import ChatInputBar, { resizeChatTextarea } from './ChatInputBar';
3535
import WorkflowLiveHud from '../viewers/WorkflowLiveHud';
3636
import PresetModal from '../terminal/PresetModal';
3737
import UltraPlanModal from '../terminal/UltraPlanModal';
@@ -2175,8 +2175,7 @@ class ChatView extends React.Component {
21752175
const textarea = this._inputRef.current;
21762176
if (textarea) {
21772177
textarea.value = description;
2178-
textarea.style.height = 'auto';
2179-
textarea.style.height = Math.min(textarea.scrollHeight, (isMobile && !isPad) ? 160 : 120) + 'px';
2178+
resizeChatTextarea(textarea, { focused: true });
21802179
this.setState({ inputEmpty: false });
21812180
textarea.focus();
21822181
} else if (this._inputWs && this._inputWs.readyState === WebSocket.OPEN) {
@@ -2215,8 +2214,7 @@ class ChatView extends React.Component {
22152214
const ta = this._inputRef.current;
22162215
if (ta) {
22172216
ta.value = assembled;
2218-
ta.style.height = 'auto';
2219-
ta.style.height = Math.min(ta.scrollHeight, 120) + 'px';
2217+
resizeChatTextarea(ta, { focused: document.activeElement === ta });
22202218
}
22212219
this.setState({
22222220
ultraplanModalOpen: false,
@@ -2644,7 +2642,8 @@ class ChatView extends React.Component {
26442642
}
26452643
if (textareaToReset) {
26462644
textareaToReset.value = '';
2647-
textareaToReset.style.height = 'auto';
2645+
// 发送后 textarea 仍保持聚焦(现有行为):聚焦态回落到两行,非聚焦态收回一行
2646+
resizeChatTextarea(textareaToReset, { focused: document.activeElement === textareaToReset });
26482647
}
26492648
if (!skipUiState) {
26502649
this._clearPendingImages();
@@ -2726,7 +2725,7 @@ class ChatView extends React.Component {
27262725
const askId = this.state.pendingAsk.id;
27272726
// 先把 textarea 清空 + 收 pending images(与正常路径一致的视觉反馈)
27282727
textarea.value = '';
2729-
textarea.style.height = 'auto';
2728+
resizeChatTextarea(textarea, { focused: document.activeElement === textarea });
27302729
this._clearPendingImages();
27312730
this.setState({ inputEmpty: true, pendingInput: userText || imagePaths, inputSuggestion: null }, () => this.scrollToBottom());
27322731
this.handleAskCancel(askId, 'Interrupted by user');
@@ -2756,8 +2755,7 @@ class ChatView extends React.Component {
27562755
const textarea = this._inputRef.current;
27572756
if (textarea) {
27582757
textarea.value = this.state.inputSuggestion;
2759-
textarea.style.height = 'auto';
2760-
textarea.style.height = Math.min(textarea.scrollHeight, 120) + 'px';
2758+
resizeChatTextarea(textarea, { focused: document.activeElement === textarea });
27612759
}
27622760
this.setState({ inputSuggestion: null, inputEmpty: false });
27632761
return;
@@ -2770,8 +2768,7 @@ class ChatView extends React.Component {
27702768

27712769
handleInputChange = (e) => {
27722770
const textarea = e.target;
2773-
textarea.style.height = 'auto';
2774-
textarea.style.height = Math.min(textarea.scrollHeight, 120) + 'px';
2771+
resizeChatTextarea(textarea, { focused: document.activeElement === textarea });
27752772
const empty = !textarea.value.trim();
27762773
this.setState({ inputEmpty: empty });
27772774
if (this.state.inputSuggestion && !empty) {
@@ -2910,8 +2907,7 @@ class ChatView extends React.Component {
29102907
if (!textarea) return;
29112908
const cur = textarea.value;
29122909
textarea.value = cur ? `${cur} ${quoted}` : quoted;
2913-
textarea.style.height = 'auto';
2914-
textarea.style.height = Math.min(textarea.scrollHeight, 120) + 'px';
2910+
resizeChatTextarea(textarea, { focused: true });
29152911
this.setState({ inputEmpty: false });
29162912
textarea.focus();
29172913
};

src/components/chat/ChatView.module.css

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -363,12 +363,18 @@
363363
flex-direction: column;
364364
overflow: hidden;
365365
position: relative;
366+
/* 宽屏下对话内容列限宽 1400px 并居中,窄屏自适应收缩 */
367+
max-width: 1400px;
368+
margin: 0 auto;
369+
width: 100%;
366370
}
367371

368-
/* 审批面板 + ChatInputBar 的共享定位容器。面板用 absolute bottom:100% 贴在输入栏之上。 */
372+
/* 审批面板 + ChatInputBar 的共享定位容器。面板用 absolute bottom:100% 贴在输入栏之上。
373+
背景与外圈输入条(--bg-input-strip)一致,避免容器间露出页面底色接缝。 */
369374
.inputStack {
370375
position: relative;
371376
flex-shrink: 0;
377+
background: var(--bg-input-strip);
372378
}
373379

374380
.overlayPanel {

src/global.css

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,12 @@
1414
--bg-base-alt: #0d0d0d;
1515
--bg-container: #111;
1616
--bg-elevated: #1e1e1e;
17+
/* Composer card fill: dark theme keeps the elevated tone */
18+
--bg-composer: #1e1e1e;
19+
/* Composer card drop shadow: subtle lift, 8px spread */
20+
--shadow-composer: 0 2px 8px rgba(0, 0, 0, 0.35);
21+
/* Strip behind the composer card: page color in dark, light gray in light */
22+
--bg-input-strip: #0d0d0d;
1723
--bg-surface: #2a2a2a;
1824
--bg-code: #14141F;
1925
--bg-code-dark: #0d1117;
@@ -172,6 +178,12 @@
172178
--bg-base-alt: #F5F5F5;
173179
--bg-container: #FFFFFF;
174180
--bg-elevated: #F9F9F9;
181+
/* Composer card fill: light theme uses pure white for contrast against the page */
182+
--bg-composer: #FFFFFF;
183+
/* Composer card drop shadow: subtle lift, 8px spread */
184+
--shadow-composer: 0 2px 8px rgba(0, 0, 0, 0.08);
185+
/* Strip behind the composer card: page color in dark, light gray in light */
186+
--bg-input-strip: #F9F9F9;
175187
--bg-surface: #F0F0F0;
176188
--bg-code: #F5F5F5;
177189
--bg-code-dark: #F5F5F5;

0 commit comments

Comments
 (0)