Skip to content

桌面版:点击会话 pane 非交互区域(含上方输出)应聚焦输入框 —— 原因定位 + 已验证修复 #812

Open

Description

@pjgmobile

反馈类别

桌面客户端 / 聊天输入框交互

环境

  • ZCode 桌面版 v3.14.3(Linux X11,源码仓库 zai-org/ZCode @ 29628c9)

问题描述

在会话 pane 里必须精确点中输入框本体的文字带才能打字。输入框外壳的 12px 内边距、输入框与工具栏之间的 12px 间隙、工具栏两侧按钮之间的空白区,以及上方整个时间线输出区域,点击后都不会把光标交还输入框 —— 这些区域都是纯展示 div,点击被吞掉。

日常影响:想先看看上面的输出再补充一句话时,每次都要重新瞄准输入框那一窄条,非常打击输入体验。

原因定位(源码分析)

  1. 可聚焦面只有 Lexical ContentEditable 本体(packages/ui/src/LexicalChatInput.tsx,min-h-10 max-h-40)。
  2. 壳层 div(packages/ui/src/prompt-editor/ChatPromptEditor.tsx,p-3 + gap-3)只有拖拽处理器,没有任何点击→聚焦转发;程序性聚焦只在切会话/草稿恢复时触发。
  3. pane 级别也没有兜底(packages/ui/src/v4/SessionPane.tsx 根节点无聚焦逻辑)。

建议方案(已在本地实现并验证)

  • ChatPromptEditor 壳层:onMouseDown 把非控件区域(padding/gap/工具栏空白)的按下转发为编辑器聚焦,preventDefault 后下一帧 editor.focus(),控件与编辑器本体保持原生行为。
  • ConversationComposer:暴露 focusHandleRef 句柄路由到现有 requestComposerFocus,复用 resolveComposerAutoFocus 门控(竖切后台 pane 不抢焦点、连接中 defer)。
  • SessionPane:pane 根节点点击兜底 —— 非交互目标一律聚焦输入框,跳过按钮/链接/表单/composer form 区域,且 selection 非折叠时跳过(保护拖选/双击选词复制)。
  • 不复用 draftFocusVersion store 通道:App.tsx 对该版本号有"收起右侧面板"副作用,pane 点击会误触发。

验证情况

dev 构建(pnpm dev:desktop:prod)手动验证:点击时间线空白/输出区域、壳层 padding、工具栏空白区均聚焦输入框;按钮/链接/拖选/双击选词行为不变。改动通过 pnpm typecheck、pnpm lint(0 errors)、pnpm architecture:check --changed(0 violations)。

如实说明:本问题由用户在日常使用中提出;上述源码定位、修复实现与验证均由 ZCode AI 编码代理(用 ZCode 修 ZCode)完成,用户在 dev 构建中人工确认了修复后的行为。

如果这个方向被接受,我可以提交 PR(改动 3 个文件 +74 行)。

Activity

  1. github-actions commented on Sep 24, 2026

    @github-actions

    👋 感谢你的反馈,我们已经收到。

    • 维护者看到后会尽快回复你。
    • 状态保持为 status: 待评估,你可以随时补充信息。
    • 信息不全时我们会打上 needs: 更多信息 标签并 @ 你。

    👋 Thanks — we've received your issue.

    • A maintainer will get back to you as soon as we can.
    • Status stays at status: 待评估 (Triage); feel free to add context.
    • If we need more details, we'll add needs: 更多信息 and ping you.
  2. pjgmobile commented on Sep 24, 2026

    @pjgmobile
    Author

    完整补丁与提交:

    完整 patch(git apply 可直接应用)
    diff --git a/packages/ui/src/prompt-editor/ChatPromptEditor.tsx b/packages/ui/src/prompt-editor/ChatPromptEditor.tsx
    index 6a65207..661cba4 100644
    --- a/packages/ui/src/prompt-editor/ChatPromptEditor.tsx
    +++ b/packages/ui/src/prompt-editor/ChatPromptEditor.tsx
    @@ -4,6 +4,7 @@ import type {
       KeyboardEventHandler,
       DragEventHandler,
       FormEventHandler,
    +  MouseEvent as ReactMouseEvent,
       MutableRefObject,
       ReactNode,
     } from "react";
    @@ -331,6 +332,32 @@ export function ChatPromptEditor({
           ? dragAttachmentHint
           : undefined;
     
    +  // 可聚焦面只有 ContentEditable 本体;壳的 p-3 padding、gap 和工具栏空白区都是纯展示 div,
    +  // 点击会落在壳上被吞掉,用户必须精确点中输入框。这里把壳内非控件区域的按下转发为聚焦。
    +  const handleShellMouseDown = useCallback(
    +    (event: ReactMouseEvent<HTMLDivElement>) => {
    +      if (event.button !== 0) return;
    +      const target = event.target as HTMLElement | null;
    +      if (!target || target.isContentEditable) return;
    +      // 控件与弹出层维持原生行为,不抢焦点。
    +      if (
    +        target.closest(
    +          "button, a, input, textarea, select, [role='button'], [role='menu'], [role='menuitem'], [data-composer-no-focus]",
    +        )
    +      ) {
    +        return;
    +      }
    +      // 阻止按下时把焦点交给 body,再统一聚焦到编辑器(与草稿回填同款下一帧时序)。
    +      event.preventDefault();
    +      if (typeof requestAnimationFrame === "function") {
    +        requestAnimationFrame(() => resolvedInputApiRef.current?.focus());
    +      } else {
    +        resolvedInputApiRef.current?.focus();
    +      }
    +    },
    +    [resolvedInputApiRef],
    +  );
    +
       return (
         <form onSubmit={handleSubmit} onKeyDown={handleKeyDown} className={cn("relative", className)}>
           {triggerPanelContainer ? null : (
    @@ -343,6 +370,7 @@ export function ChatPromptEditor({
             onDragOver={handleDragOver}
             onDragLeave={handleDragLeave}
             onDrop={handleDrop}
    +        onMouseDown={handleShellMouseDown}
             className={cn(
               "relative flex flex-col gap-3 overflow-hidden rounded-2xl border border-input-border bg-input p-3 transition-colors hover:border-input-border-hover focus-within:!border-input-border-focused focus-within:bg-input-focused",
               (isWorkspaceFileDropActive || isExternalFileDropActive) &&
    diff --git a/packages/ui/src/v4/ConversationComposer.tsx b/packages/ui/src/v4/ConversationComposer.tsx
    index 913ae0d..245407e 100644
    --- a/packages/ui/src/v4/ConversationComposer.tsx
    +++ b/packages/ui/src/v4/ConversationComposer.tsx
    @@ -24,6 +24,7 @@ import {
       useState,
       type DragEvent,
       type MouseEvent as ReactMouseEvent,
    +  type MutableRefObject,
       type ReactNode,
     } from "react";
     import { cn } from "@/components/lib/utils.js";
    @@ -190,6 +191,12 @@ export interface ConversationComposerSendOptions {
     }
     
     export type ConversationComposerSendResult = "sent" | "blocked" | "confirmationRequired";
    +
    +/** 宿主(SessionPane)经 ref 句柄请求聚焦输入框的最小 API。 */
    +export type ConversationComposerFocusHandle = {
    +  focus: () => void;
    +};
    +
     function getComposerAttachmentTypeLabel(filename: string, mimeType: string): string {
       const leaf = filename.split(/[\\/]/u).at(-1) ?? filename;
       const dotIndex = leaf.lastIndexOf(".");
    @@ -391,6 +398,11 @@ interface ConversationComposerProps {
        * 竖切多 pane 时由宿主传入 SessionPane.focused,仅焦点 pane 聚焦、后台 pane 不抢焦点。
        */
       autoFocusEnabled?: boolean;
    +  /**
    +   * 宿主经 ref 句柄主动请求聚焦输入框(如「点击 pane 空白处聚焦」)。
    +   * 复用 requestComposerFocus 的 autoFocus 三态门控,不旁路后台 pane / disabled 约束。
    +   */
    +  focusHandleRef?: MutableRefObject<ConversationComposerFocusHandle | null>;
       /** 当前 composer 是否运行在手机 Web 远控壳中。 */
       workspacePath: string;
       workspaceIdentity?: string;
    @@ -498,6 +510,7 @@ function ConversationComposerImpl({
       blockingRequestId = null,
       disabled = false,
       autoFocusEnabled = true,
    +  focusHandleRef,
       workspacePath,
       workspaceIdentity,
       remoteSessionId,
    @@ -852,6 +865,15 @@ function ConversationComposerImpl({
         flushPendingFocus();
       }, [flushPendingFocus]);
     
    +  // 把聚焦句柄交给宿主:SessionPane 的「点击 pane 空白处聚焦输入框」也走同一策略门控。
    +  useEffect(() => {
    +    if (!focusHandleRef) return;
    +    focusHandleRef.current = { focus: requestComposerFocus };
    +    return () => {
    +      focusHandleRef.current = null;
    +    };
    +  }, [focusHandleRef, requestComposerFocus]);
    +
       const handleCodeCommentRemoved = useCallback(
         (comment: Parameters<typeof removeCodeCommentPreview>[0]) => {
           removeCodeCommentPreview(comment, services?.broadcastService);
    diff --git a/packages/ui/src/v4/SessionPane.tsx b/packages/ui/src/v4/SessionPane.tsx
    index 2f9c33a..1826066 100644
    --- a/packages/ui/src/v4/SessionPane.tsx
    +++ b/packages/ui/src/v4/SessionPane.tsx
    @@ -13,6 +13,7 @@ import {
       useRef,
       useState,
       useSyncExternalStore,
    +  type MouseEvent as ReactMouseEvent,
       type ReactNode,
     } from "react";
     import { Hand } from "lucide-react";
    @@ -116,6 +117,7 @@ import type { GroupedDraftTaskState } from "@/store/zcodeSessionStoreTypes.js";
     import {
       ConversationComposer,
       type ComposerRestoreRequest,
    +  type ConversationComposerFocusHandle,
       type ConversationComposerSendOptions,
       type ConversationComposerSendResult,
     } from "@/v4/ConversationComposer.js";
    @@ -4361,6 +4363,29 @@ export function SessionPane({
           });
       }, [dispatchCommand, handleDraftSessionCreated, recoverableCommand, workspaceKey]);
     
    +  // 点击 pane 内任意非交互区域(含上方时间线输出)都把光标交还输入框。
    +  // 交互元素(按钮/链接/表单控件)维持原生行为;composer form 区域由
    +  // ChatPromptEditor 壳层自己的 mousedown 聚焦逻辑负责,这里跳过避免双重聚焦;
    +  // 拖选/双击选词时 selection 非折叠,跳过以保住复制体验。
    +  const composerFocusHandleRef = useRef<ConversationComposerFocusHandle | null>(null);
    +  const handlePaneClickFocusComposer = useCallback(
    +    (event: ReactMouseEvent<HTMLDivElement>) => {
    +      const target = event.target as HTMLElement | null;
    +      if (!target || target.isContentEditable) return;
    +      if (
    +        target.closest(
    +          "button, a, input, textarea, select, label, form, [role='button'], [role='menu'], [role='menuitem'], [role='tab'], [role='option'], [data-no-composer-focus]",
    +        )
    +      ) {
    +        return;
    +      }
    +      const selection = typeof window.getSelection === "function" ? window.getSelection() : null;
    +      if (selection && !selection.isCollapsed) return;
    +      composerFocusHandleRef.current?.focus();
    +    },
    +    [],
    +  );
    +
       // subagent 右侧 child tab 是观察视图;复用普通 SessionPane 时
       // 若仍创建 composer,会让用户误以为可以直接向 child session 继续输入。
       const composerNode = readOnly ? null : (
    @@ -4386,6 +4411,7 @@ export function SessionPane({
           externalTextInsertRequest={focused && sessionId === null ? composerTextInsertRequest : null}
           onExternalTextInsertApplied={handleExternalTextInsertApplied}
           autoFocusEnabled={focused}
    +      focusHandleRef={composerFocusHandleRef}
           disabled={
             connecting ||
             draftRuntimeRebuilding ||
    @@ -4594,6 +4620,7 @@ export function SessionPane({
           onDragOver={effectiveDropTargetController?.onDragOver}
           onDragLeave={effectiveDropTargetController?.onDragLeave}
           onDrop={effectiveDropTargetController?.onDrop}
    +      onClick={handlePaneClickFocusComposer}
           className="relative flex h-full min-h-0 flex-col"
         >
           {effectiveDropTargetController?.active ? (
  3. pjgmobile commented on Sep 24, 2026

    @pjgmobile
    Author

    相关 issue:#801、#242(权限/提问弹窗抢走输入焦点)—— 方向相反,但同属 composer 焦点体验问题。

    本 issue 的修复恰好补齐了「焦点被抢走后快速回焦」的路径:弹窗打断后,点击 pane 内任意空白处(包括上方输出区域)即可把光标交还输入框,无需精确瞄准输入带。

  4. pjgmobile commented on Sep 30, 2026

    @pjgmobile
    Author

    友好跟进一下状态 —— 这个 issue 提交已 6 天,目前仍在「待评估」,想确认是否在团队计划内。

    补充两点可能有用的信息:

    1. 稳定性证据:文中的修复补丁(3 个文件 +74 行)自 09-24 起在我本机日常使用,至今零问题,焦点行为完全稳定;当前最新 release v3.14.3 仍可复现原始问题。
    2. 随时可推进:如团队需要,我可以按仓库约定补充 E2E 场景,或直接发起 PR(分支与补丁均已就绪,见上方评论)。

    如需其他信息请随时告知,谢谢!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions