diff --git a/docs/【编辑器】画布Agent对话面板-2026-07-03.md b/docs/【编辑器】画布Agent对话面板-2026-07-03.md
index 4a1c4c217..c1cb6c4a7 100644
--- a/docs/【编辑器】画布Agent对话面板-2026-07-03.md
+++ b/docs/【编辑器】画布Agent对话面板-2026-07-03.md
@@ -61,7 +61,7 @@
- 对话框与左侧素材 / 图层侧栏**不互斥**,允许同时展开,便于在对话中选取和核对画布素材;左侧栏切换不改变 Agent 面板开关状态。
- 桌面端对话框固定宽约 360–400px;移动端抽屉式全宽覆盖;收起态为胶囊/圆形入口按钮。
- 底部消息输入框随输入内容从单行高度自动增长,最大高度为
- 128px;原生自适应和兼容降级统一由公共 `AutoGrowTextArea` 组件承载,`EditorAgentDraftTextarea` 只保留 Agent 专属视觉、粘贴和 Enter 提交语义。支持 `field-sizing: content` 的浏览器使用原生内容尺寸自适应,不支持该属性的旧 Safari / iOS WebView 使用前端测量降级,并在宽度变化时重新计算换行高度。内容超过最大高度后停止增长并启用内部纵向滚动,内容缩短或清空后同步收缩。内部滚动条使用浅灰窄滑块和透明轨道,上下留白不得溢出输入框圆角边界;输入框在窄屏下允许收缩且不产生横向滚动。
+ 128px;原生自适应和兼容降级统一由公共 `AutoGrowTextArea` 组件承载,`EditorAgentDraftTextarea` 只保留 Agent 专属视觉、粘贴和 Enter 提交语义。支持 `field-sizing: content` 的浏览器使用原生内容尺寸自适应;不支持该属性的旧 Safari / iOS WebView 使用前端测量降级,受控 `value` 更新、非受控输入事件和宽度变化都必须重新计算换行高度,同时继续转发调用方的 `onInput`。内容超过最大高度后停止增长并启用内部纵向滚动,内容缩短或清空后同步收缩。内部滚动条使用浅灰窄滑块和透明轨道,上下留白不得溢出输入框圆角边界;输入框在窄屏下允许收缩且不产生横向滚动。
- Enter 发送必须同时排除 `isComposing` 和旧 Safari / WebKit 候选词确认事件的 `keyCode === 229`,避免输入法选词时误发送。
- 用户消息必须包含去除首尾空白后的非空文本;附件只能随文本消息发送,前端发送门禁与后端 `module-editor-agent` 领域校验必须同时拒绝纯附件消息。
- 会话管理入口在对话框头部:当前会话标题 + 历史会话下拉(按更新时间倒序)+ 新建对话按钮,全部包在对话框内。
diff --git a/src/components/common/AutoGrowTextArea.test.tsx b/src/components/common/AutoGrowTextArea.test.tsx
index d912a7c94..e415ecdc9 100644
--- a/src/components/common/AutoGrowTextArea.test.tsx
+++ b/src/components/common/AutoGrowTextArea.test.tsx
@@ -129,4 +129,64 @@ describe('AutoGrowTextArea', () => {
unmount();
expect(disconnect).toHaveBeenCalledTimes(1);
});
+
+ it('remeasures uncontrolled input changes and forwards the consumer input handler', () => {
+ const onInput = vi.fn();
+ vi.stubGlobal('CSS', { supports: vi.fn().mockReturnValue(false) });
+ vi.stubGlobal(
+ 'ResizeObserver',
+ vi.fn().mockImplementation(() => ({
+ observe: vi.fn(),
+ unobserve: vi.fn(),
+ disconnect: vi.fn(),
+ })),
+ );
+
+ render(
+ ,
+ );
+ const input = screen.getByLabelText(
+ '非受控自动增长输入框',
+ ) as HTMLTextAreaElement;
+ let contentHeight = 90;
+ Object.defineProperty(input, 'offsetHeight', {
+ configurable: true,
+ get: () => {
+ const styleHeight = Number.parseFloat(input.style.height);
+ return Number.isNaN(styleHeight) ? 42 : styleHeight;
+ },
+ });
+ Object.defineProperty(input, 'clientHeight', {
+ configurable: true,
+ get: () => input.offsetHeight - 2,
+ });
+ Object.defineProperty(input, 'scrollHeight', {
+ configurable: true,
+ get: () => contentHeight,
+ });
+
+ fireEvent.input(input, {
+ target: { value: '第一行\n第二行\n第三行\n第四行' },
+ });
+ expect(input.style.height).toBe('92px');
+ expect(input.style.overflowY).toBe('hidden');
+
+ contentHeight = 180;
+ fireEvent.input(input, {
+ target: { value: '很多行内容'.repeat(30) },
+ });
+ expect(input.style.height).toBe('128px');
+ expect(input.style.overflowY).toBe('auto');
+
+ contentHeight = 20;
+ fireEvent.input(input, { target: { value: '短内容' } });
+ expect(input.style.height).toBe('40px');
+ expect(input.style.overflowY).toBe('hidden');
+ expect(onInput).toHaveBeenCalledTimes(3);
+ });
});
diff --git a/src/components/common/AutoGrowTextArea.tsx b/src/components/common/AutoGrowTextArea.tsx
index 0005511ad..93ee0bfa1 100644
--- a/src/components/common/AutoGrowTextArea.tsx
+++ b/src/components/common/AutoGrowTextArea.tsx
@@ -1,4 +1,5 @@
import {
+ type InputEventHandler,
type TextareaHTMLAttributes,
useLayoutEffect,
useRef,
@@ -50,6 +51,7 @@ function resizeFallbackTextArea(textarea: HTMLTextAreaElement) {
export function AutoGrowTextArea({
className,
+ onInput,
rows = 1,
value,
...textareaProps
@@ -103,12 +105,19 @@ export function AutoGrowTextArea({
]
.filter(Boolean)
.join(' ');
+ const handleInput: InputEventHandler = (event) => {
+ if (!supportsNativeFieldSizing()) {
+ resizeFallbackTextArea(event.currentTarget);
+ }
+ onInput?.(event);
+ };
return (