Răsfoiți Sursa

docs(web): define two-stage draft editor isolation

imccyu 1 săptămână în urmă
părinte
comite
218db70b75

+ 6 - 0
.agents/notes/proposed/architecture/2026-09-14-composer-model-and-draft-editor.i18n.yaml

@@ -0,0 +1,6 @@
+# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
+# side as of the last confirmed-consistent state. Both languages carry equal authority;
+# after editing either side, bring the other along and re-record with:
+#   pnpm run verify-translation-pairing --write .agents/notes/proposed/architecture/2026-09-14-composer-model-and-draft-editor.md
+2026-09-14-composer-model-and-draft-editor.md: 3f66ab2c73da0a8f66bf43a821cbe5f85363867a
+2026-09-14-composer-model-and-draft-editor.zh.md: aa040b0fa68961d4d5884d73791c4986b8bfab50

+ 116 - 0
.agents/notes/proposed/architecture/2026-09-14-composer-model-and-draft-editor.md

@@ -0,0 +1,116 @@
+# Agent Note: Two-stage Composer and DraftEditor isolation
+
+Status: proposed
+
+English | [中文](2026-09-14-composer-model-and-draft-editor.zh.md)
+
+## Problem
+
+One Client needs to edit the same Session's draft and pending attachments in multiple views. A Lexical editor binds only one DOM root; multiple presentation locations need multiple editor instances, but must not own unrelated drafts or upload tasks, or make the Session Controller understand carets, composition, or DOM state.
+
+The current [SessionInputShell](../../../../packages/client/ui-conversation/src/client/input/facade.ts) combines Lexical operations, draft projection, the submission state machine, and failure recovery. [InputBar](../../../../packages/client/ui-conversation/src/client/skeleton/InputBar.tsx) combines editor presentation, DOM bindings, attachment intake, and submission controls. Implementing multiple instances directly in these files would mix code extraction with behavior changes.
+
+[ConversationController](../../../../packages/client/ui-conversation/src/client/service.ts) already owns attachment entities and upload tasks centrally; the shell retains only ordered attachment IDs. Selecting a skill inserts ordinary `/name` text whose highlighting derives from a lexicon; atomic file and Session references use chips carrying source identity. A shared draft must not lose these references by synchronizing text alone, and does not require copying attachment entities.
+
+This proposal details editor isolation for [#3951](https://github.com/deepseek-ai/deepseek-harness/pull/3951), following [Client Session and UI ownership](../../implemented/architecture/2026-08-20-client-session-conversation-ownership.md). The Session activity view, residency states, and eviction policy are designed independently; [#4138](https://github.com/deepseek-ai/deepseek-harness/pull/4138) is only a Host lifecycle reference. This proposal implements none of those features and does not repeat the Conversation component decomposition in #3984.
+
+## Proposal
+
+Use two independent PRs. Stage one concentrates existing editor implementation into explicit locations for behavior changes; stage two changes behavior only. `DraftEditor` names the draft-editing area, while Composer names the complete writing area including attachments and submission controls. Keep `input/`, `skeleton/`, `InputBar`, `InputHub`, and `SessionInputShell`; directory moves and renames are not refactoring deliverables.
+
+### Stage one: five mechanical responsibility extractions
+
+The ui-conversation paths below are relative to `packages/client/ui-conversation/src/client/`. Every new file must contain logic already executed today, not placeholder interfaces or future features.
+
+| Original location | Extraction destination | Location for later behavior changes |
+|---|---|---|
+| Lexical creation, registration, projection, node operations, and cleanup in `input/facade.ts` | `input/editor/runtime.ts` | One editor's implementation and its creation, binding, and disposal |
+| Text-area JSX in `skeleton/InputBar.tsx` | `input/editor/DraftEditor.tsx` | One editor's presentation, excluding the attachment rail and submission orchestration |
+| Focus, selection reveal, wheel, keymap, and picker binding functions in `InputBar.tsx` | `input/editor/view-binding.ts` | DOM interaction and editor bindings for one mounted view |
+| Range, reference, and keyboard interface types in `contract/input.ts` | `contract/draft-editor.ts` | Editor-facing data and operation types; submission and shared state stay in the original file |
+| Document drop effect implementation in `ui-attachment/src/client/ComposerAttachments.tsx` | `ui-attachment/src/client/drop-events.ts` | Document drag-and-drop registration, routing, and cleanup |
+
+The existing shell creates and delegates to the internal object in `runtime.ts`. That object retains the original editor, NodeKey map, projection, and Lexical registrations; it neither copies those states nor independently decides whether editing or submission is permitted. The shell retains SubmitMachine, draft revision, attachment IDs, notices, attempts, serialization, and success/failure recovery decisions.
+
+Methods combining guards and node operations retain their guards at the original location. For example, beginCommand keeps its span/phase checks, node replacement, and machine dispatch in the same order; failure recovery preserves batch ordering, revision guards, restoration flags, and history cleanup timing. Editor updates still call the shell synchronously at the original publication point, without another Promise, effect, or notification turn.
+
+`DraftEditor.tsx` extracts presentation without adding a DOM wrapper. All existing React hooks, refs, dependency arrays, and relative effect order remain in InputBar; effects delegate to ordinary functions at their original call sites. CSS files, class keys, React keys, placeholder order, and decorator order remain unchanged. The new component does not take over editor creation or hold another draft.
+
+`contract/draft-editor.ts` receives `TokenSpan`, `ReferenceInsert`, `ArbitrateKey`, `ArbitrateOutcome`, `ComposerKeyboard`, `EditSelection`, and `Occurrence`. Names and members remain unchanged, consumers import from the actual declaration owner, and existing public exports retain their names and visibility. `ComposerKeyboard` temporarily still depends on shared `InputState`; this is not an independent controlled-editor protocol.
+
+#### Stage-one invariants
+
+- InputHub still creates one shell and one editor per Session, with unchanged creation, reuse, and disposal timing and counts.
+- Lexical remains the draft authority; Undo/Redo, NodeKey identity, span checks, and revision rules remain unchanged, without a second document or store.
+- `useInput`, `inputActions`, Slots, events, inject declarations, and public APIs retain their names, payloads, and behavior; Host protocols and persistence formats do not change.
+- Attachment selection, upload timing, image previews, submission batches, success clearing, failure restoration, and notice rules remain unchanged.
+- Each original component still registers document drop listeners in the same effect; the single picker, duplicate drop, and single editor/root limitations remain.
+- Tests change only type imports that actually need updating; test filenames, assertions, recorded Sessions, and expected outputs remain unchanged, without snapshot refreshes.
+- No existing file moves, existing private-name changes, CSS changes, new packages, dependencies, renderer scopes, or general state framework.
+
+#### Isolation actually achieved in stage one
+
+| Subject | Result |
+|---|---|
+| Editor implementation | Node and projection operations belong to runtime, presentation to DraftEditor, and DOM bindings to view-binding |
+| Submission and attachment orchestration | Still owned by the original shell and ConversationController, not DraftEditor |
+| State across different Sessions | Remains isolated under existing rules |
+| Shared state within one Session | Still reuses the original shell, without duplicate attachments or uploads |
+| Independent editors within one Session | Not implemented; views still share one Lexical editor |
+| Independent selection, IME, Undo, and menu origins | Not implemented; separating code does not change runtime ownership |
+| Multi-view picker, focus, and document drop routing | Not implemented; binding code has a separate location for modification |
+
+### Stage two: behavior changes only
+
+Stage two implements a shared draft and multiple editors directly in the locations above. It must not move existing files or directories, perform pure renames or helper/class/component extractions, reorder existing tests, or clean up formatting or comments. New types, implementations, and tests required by new behavior may be added, but copying old code into a new file and deleting its original does not evade this restriction.
+
+If behavior implementation still needs structural preparation, complete stage one first: amend its PR before merge, or add a separate mechanical prerequisite PR after merge. The behavior PR uses that mechanical result as its base and cannot include the preparation.
+
+#### Final state ownership
+
+The shared Composer model evolves the responsibilities of the existing SessionInputShell without requiring another rename. The Session Controller continues to own only Session business state and does not import DraftEditor, Lexical, or the shared draft document.
+
+| State | Final owner | Multi-view requirement |
+|---|---|---|
+| Draft text, semantic references, and content revision | Session-associated shared Composer model | Publish edits from either view to every view through one reactive source |
+| Ordered attachment IDs, claims, submission attempts, and failure recovery | Shared Composer model | Settle each submission once; operations in either view affect the same pending input |
+| File, Blob URL, upload tasks, progress, and receipts | Existing attachment owner | Do not copy per view; unmounting one view does not cancel resources used by another |
+| Lexical, DOM, NodeKey mappings, selection, and IME preedit | Each DraftEditor instance | Two independent editors/roots; unmounting one does not detach the other |
+| Menu anchor, file dialog, and focus | Initiating view | Route by operation origin, not a single Session picker |
+| Session history, running, and queue | Session Controller | Keep reading existing sources instead of copying them into the draft model |
+
+The renderer still binds React hooks from bare observables, and business components read and write through existing standard props. The shared model accepts neither DOM, Lexical NodeKeys, nor composition intermediate state; DraftEditor receives neither Session/Context nor upload services, only draft data, presentation data, and editing/intent callbacks.
+
+#### Shared content and synchronization requirements
+
+Draft content must represent ordinary text, newlines, and atomic references with complete `ReferenceInsert` information independently of Lexical. Shared reference identity must not depend on one editor's NodeKey; each instance privately maps it to its own nodes. Skills remain ordinary `/name` text, with both views deriving highlights from the same text and lexicon, without an extra selected-skill list or changes to Host recognition.
+
+Draft text is small, so synchronization may use complete semantic documents without requiring a collaborative-editing algorithm. The shared model accepts edits, assigns revisions, and publishes; editors distinguish local changes from external rendering to avoid feedback loops. Callbacks from stale revisions, prior model lifetimes, or unmounted views must not overwrite current content. Submission freezing, success clearing, failure restoration, and attachment changes must reach all views through the same shared source.
+
+IME preedit belongs to the local instance, and updates from another view must not directly disrupt text under composition. Stage two must define and verify how another view's edits, submission clearing, and model release interact with composition. Undo/Redo must also operate on one logical draft, rather than letting two Lexical histories restore stale whole documents over each other; synchronization and history implementation are outside the mechanical stage.
+
+Programmatic insertion, menu selection, file selection, and focus restoration need the initiating view's temporary identity. Closing that view must not redirect late UI actions into another view of the same Session. Document drop must select one explicit target and process the drop exactly once; origin routing and deduplication are stage-two behavior.
+
+Existing text-draft restoration after refresh must remain, without implicitly promising persistence for structured references, File objects, or cross-browser collaboration. The Session activity view and LRU/timeout policy remain independent of this editing protocol.
+
+## Alternatives considered
+
+**Only rename input or relocate it to composer.** This does not separate Lexical operations, view bindings, and submission decisions; behavior implementation would still need to extract old code from large files, so it is not a stage-one deliverable.
+
+**Bind one Lexical editor to two DOM roots.** This conflicts with Lexical's single-root model; copying React presentation does not create two independently interactive editors.
+
+**Give each Composer an independent draft and attachments.** This fails the same-Session shared-editing requirement and introduces conflicting attachment and submission ownership.
+
+**Implement a shared DraftDocument, Undo, or drop deduplication in the mechanical stage.** This changes authority, lifecycle, or event-processing counts and cannot be reviewed as behavior-preserving preparation.
+
+## Acceptance criteria
+
+Stage one completes the five extractions and required imports, JSDoc, and README updates; review compares original method bodies, branches, callback order, hooks, DOM, and cleanup. Existing editing, reference, claim, attachment, submission, failure-restoration, and unmount tests continue to pass; focused browser regressions run against built artifacts with unchanged expected output. Type and documentation checks cover relocated declarations and bilingual pairs. New dual-instance functionality is not a stage-one acceptance condition.
+
+Stage two uses two genuinely mounted Composers for one Session to verify bidirectional text and chip synchronization, skill highlights, shared attachments and progress, submission clearing/failure restoration, IME/Undo, origin routing, and continued operation after either view unmounts. Its diff contains behavior implementation and corresponding tests only, without mechanical cleanup.
+
+## Risks
+
+Even stateless JSX extraction can alter ref or effect timing; therefore hooks and refs retain their host, and DOM gains no wrapper. Lexical extraction can alter nested updates, projection caching, or history cleanup order; therefore retain original operation bodies and compare execution order instead of rewriting algorithms.
+
+Stage one still cannot mount two editors for one Session and retains the existing picker/drop limitations. Confusing directory isolation with state isolation could cause roots to detach each other, duplicate attachment intake, or misroute focus; stage-two dual-instance behavior tests must close these gaps.

+ 116 - 0
.agents/notes/proposed/architecture/2026-09-14-composer-model-and-draft-editor.zh.md

@@ -0,0 +1,116 @@
+# Agent Note: Composer 与 DraftEditor 隔离的两阶段重构
+
+Status: proposed
+
+[English](2026-09-14-composer-model-and-draft-editor.md) | 中文
+
+## 问题
+
+同一个 Client 需要在不同视图中编辑同一个 Session 的草稿和待发送附件。Lexical 的一个 editor 只能绑定一个 DOM root;多个呈现位置需要多个编辑实例,但不能各自拥有互不相干的草稿和上传任务,也不能让 Session Controller 理解光标、输入法或 DOM。
+
+当前 [SessionInputShell](../../../../packages/client/ui-conversation/src/client/input/facade.ts) 同时包含 Lexical 操作、草稿投影、提交状态机和失败恢复。[InputBar](../../../../packages/client/ui-conversation/src/client/skeleton/InputBar.tsx) 同时包含编辑区呈现、DOM 绑定、附件入口和发送控件。直接在这两个文件中实现多实例会让代码提取与行为差异混在一起。
+
+附件实体和上传任务已经由 [ConversationController](../../../../packages/client/ui-conversation/src/client/service.ts) 集中管理,shell 只保留有序附件 IDs。skill(技能)选择插入普通 `/name` 文本,高亮由词表派生;文件和 Session 的原子引用则使用带来源身份的 chip。共享草稿不能只同步文字而丢失这些引用,也不需要复制附件实体。
+
+本提案细化 [#3951](https://github.com/deepseek-ai/deepseek-harness/pull/3951) 的编辑器隔离,遵循 [Client Session 与 UI 所有权](../../implemented/architecture/2026-08-20-client-session-conversation-ownership.zh.md)。Session 活跃视图、驻留状态与回收策略独立设计;[#4138](https://github.com/deepseek-ai/deepseek-harness/pull/4138) 仅作为 Host 生命周期参考。本提案不实现这些功能,也不重复 #3984 的 Conversation 组件拆分。
+
+## 提案
+
+采用两个独立 PR(Pull Request)。第一阶段集中既有编辑实现,为行为改动准备明确位置;第二阶段只修改行为。`DraftEditor` 专指草稿编辑区,Composer 指包含附件和发送控件的完整编写区域。保留 `input/`、`skeleton/`、`InputBar`、`InputHub` 和 `SessionInputShell`,不以改目录或改名作为重构成果。
+
+### 第一阶段:五处机械职责提取
+
+以下 ui-conversation 路径相对 `packages/client/ui-conversation/src/client/`。每个新文件必须承载当前已经执行的逻辑,不建立占位接口或未来功能。
+
+| 原位置 | 提取位置 | 后续行为修改的落点 |
+|---|---|---|
+| `input/facade.ts` 的 Lexical 创建、注册、投影、节点操作和清理 | `input/editor/runtime.ts` | 单个 editor 的实现及其创建、绑定和释放 |
+| `skeleton/InputBar.tsx` 的文字区域 JSX | `input/editor/DraftEditor.tsx` | 单份编辑区的呈现,不包含附件栏和提交编排 |
+| `InputBar.tsx` 的 focus、selection reveal、wheel、keymap、picker 绑定函数 | `input/editor/view-binding.ts` | 单个挂载视图的 DOM 交互和编辑器绑定 |
+| `contract/input.ts` 的范围、引用、键盘接口类型 | `contract/draft-editor.ts` | 编辑器对外数据与操作类型;提交和共享状态仍留原文件 |
+| `ui-attachment/src/client/ComposerAttachments.tsx` 的 document drop effect 实现 | `ui-attachment/src/client/drop-events.ts` | document 拖放监听的注册、路由和清理 |
+
+`runtime.ts` 内部对象由现有 shell 创建并委托调用。它保管原 editor、NodeKey 映射、投影和 Lexical 注册;不复制这些状态,也不独立决定能否编辑或提交。shell 继续持有 SubmitMachine、draft revision、附件 IDs、通知、attempt、序列化以及成功/失败恢复决策。
+
+同时涉及判断和节点操作的方法在原位置保留判断。例如 beginCommand 的 span/phase 检查、节点替换、machine dispatch 的顺序不变;失败恢复的批次排序、revision 保护、恢复标志和 history 清理时点不变。editor 更新仍在原同步位置回调 shell 发布状态,不增加 Promise、effect 或通知轮次。
+
+`DraftEditor.tsx` 是无额外 DOM 包装的呈现提取。所有既有 React 钩子、refs、依赖数组和 effect 相对顺序仍留在 InputBar;effect 只在原调用位置委托普通函数。CSS 文件、class keys、React keys、placeholder 与 decorator 顺序均不变。新组件不接管 editor 创建或持有另一份草稿。
+
+`contract/draft-editor.ts` 移入 `TokenSpan`、`ReferenceInsert`、`ArbitrateKey`、`ArbitrateOutcome`、`ComposerKeyboard`、`EditSelection` 和 `Occurrence`。名称和成员不变,所有消费方从实际声明处导入;既有公开出口保持原名称和可见集合。`ComposerKeyboard` 仍暂时依赖共享 `InputState`,这不是独立受控编辑协议。
+
+#### 第一阶段不变项
+
+- InputHub 仍按 Session 创建一个 shell 和一个 editor,创建、复用、dispose(资源释放)的时点及次数不变。
+- 草稿真值仍在 Lexical,Undo/Redo、NodeKey 身份、span 检查和 revision 规则不变;不增加第二份文档或存储。
+- `useInput`、`inputActions`、Slot、事件、inject 及公开 API 的名称、载荷和行为不变;不改 Host 协议和持久化格式。
+- 附件选择、上传时机、图片预览、提交批次、成功清空、失败恢复及通知规则不变。
+- document drop 仍在每个原组件的同一 effect 中注册;单 picker、重复 drop、单 editor/root 的限制原样保留。
+- 测试只修改实际需要的类型导入;不改测试文件名、断言、录制 Session 或预期输出,不刷新快照。
+- 不搬已有文件,不改已有私有名字,不改 CSS,不增包、依赖、renderer scope 或通用状态框架。
+
+#### 第一阶段实际隔离程度
+
+| 内容 | 完成后的状态 |
+|---|---|
+| 编辑器实现 | 节点和投影操作归 runtime,呈现归 DraftEditor,DOM 绑定归 view-binding |
+| 提交与附件编排 | 仍由原 shell 和 ConversationController 管理,不落入 DraftEditor |
+| 不同 Session 的状态 | 继续按原规则隔离 |
+| 同 Session 的共享状态 | 继续复用原 shell;没有双份附件或上传任务 |
+| 同 Session 的独立编辑实例 | 未实现,仍共享一个 Lexical editor |
+| 独立 selection、IME、Undo 和菜单来源 | 未实现;代码位置分开不代表运行时归属已改变 |
+| picker、focus、document drop 的多视图路由 | 未实现;绑定代码已有单独修改位置 |
+
+### 第二阶段:只调整行为
+
+第二阶段直接在上述位置实现共享草稿和多编辑实例。禁止移动已有文件或目录、纯改名、纯提取 helper/class/component、重排已有测试,以及格式或注释清理。新增行为需要的新类型、实现和测试可以增加,但不得复制旧代码到新文件后删除原文来规避约束。
+
+如果行为实现仍需要结构准备,必须先补第一阶段:未合入时修改第一阶段 PR;已合入时单独增加机械前置 PR。行为 PR 以该机械结果为 base,不能夹带机械准备。
+
+#### 最终状态归属
+
+共享 Composer 模型是现有 SessionInputShell 的职责演进,不要求再次改名。Session Controller 继续只管理会话业务,不 import DraftEditor、Lexical 或共享草稿文档。
+
+| 状态 | 最终 owner | 多视图要求 |
+|---|---|---|
+| 草稿正文、语义引用、内容 revision | Session 关联的共享 Composer 模型 | 任一处编辑后,通过同一个响应式来源发布给所有视图 |
+| 有序附件 IDs、认领、提交 attempt 和失败恢复 | 共享 Composer 模型 | 每个提交只结算一次,任一视图的操作作用于同一批输入 |
+| File、Blob URL、上传任务、进度和凭证 | 既有附件管理 owner | 不随视图复制;卸载一个视图不取消其他视图所用资源 |
+| Lexical、DOM、NodeKey 映射、selection、IME preedit | 各 DraftEditor 实例 | 两个独立 editor/root;卸载一处不解绑另一处 |
+| 菜单锚点、文件对话框和焦点 | 发起操作的视图 | 按操作来源路由,不以 Session 唯一 picker 代替来源 |
+| Session 历史、running、queue | Session Controller | 继续读取现有来源,不复制进草稿模型 |
+
+React 钩子仍由 renderer 从裸 observable 绑定,业务组件通过现有标准 props 读写。共享模型不接收 DOM、Lexical NodeKey 或输入法中间态;DraftEditor 不接收 Session/Context 或上传服务,只接收草稿数据、显示数据与编辑/意图回调。
+
+#### 共享内容和同步要求
+
+草稿内容必须能独立于 Lexical 表示普通文本、换行和带完整 `ReferenceInsert` 信息的原子引用。引用的共享身份不能依赖某个 editor 的 NodeKey;各实例私有映射到自己的节点。skill 保持普通 `/name` 文本,两处从同一文本和词表派生高亮,不引入额外的已选 skill 列表或改变 Host 识别规则。
+
+草稿文本量小,可以使用完整语义文档同步,不要求协同编辑算法。共享模型负责接受编辑、分配 revision 和发布,编辑器区分本地产生的变化与外部呈现,避免回声循环。旧 revision、旧模型生命周期和已卸载视图的回调不能覆盖新内容。提交冻结、成功清空、失败恢复和附件变化必须通过同一共享来源到达所有视图。
+
+IME preedit 属于本地实例,远端视图更新不能直接破坏正在组合的文本。来自另一视图的修改、发送清空和模型释放如何与组合态相遇,必须在第二阶段定义并验证。Undo/Redo 也必须作用于同一份逻辑草稿,不能让两份 Lexical history 互相恢复陈旧整篇文档;具体同步与历史实现不属于机械阶段。
+
+程序化插入、菜单选择、文件选择器和焦点恢复要携带发起视图的临时身份。视图关闭后不能把迟到的 UI 操作转发给另一个同 Session 视图。document drop 必须明确一次拖放选哪个目标并保证仅处理一次;来源路由和去重均是第二阶段行为。
+
+刷新后的既有文字草稿恢复应保留,但不默认新增结构化引用、File 或跨浏览器协作的持久化承诺。Session 活跃视图及 LRU/时限策略不与这份编辑协议绑定。
+
+## 考虑过的替代方案
+
+**仅把 input 改名或平移到 composer。** 不能分离 Lexical 操作、视图绑定和提交决策,后续行为实现仍需从大文件中提取旧代码,因此不作为第一阶段成果。
+
+**让一个 Lexical editor 同时挂两个 DOM root。** 与 Lexical 的单 root 模型冲突,不能用 React 复制呈现来获得两个可独立交互的编辑器。
+
+**每个 Composer 独立草稿和附件。** 不满足同一 Session 共享编辑的要求,还会引入附件和提交所有权分歧。
+
+**机械阶段直接实现共享 DraftDocument、Undo 或 drop 去重。** 改变真值、生命周期或事件处理次数,无法作为行为不变的前置改动审查。
+
+## 验收标准
+
+第一阶段必须完成五处提取及必要导入、JSDoc 和 README 更新;逐项核对原方法体、分支、回调次序、Hooks、DOM 和清理。现有编辑、引用、认领、附件、提交、失败恢复和卸载测试继续通过;用构建产物运行针对性浏览器回归,预期输出不变。类型和文档检查覆盖移动后的声明与双语配对。不以新双实例功能作为第一阶段验收条件。
+
+第二阶段必须用同一 Session 的两个真实挂载 Composer 验证双向文字和 chip 同步、skill 高亮、共享附件和进度、发送清空/失败恢复、IME/Undo、来源路由,以及任一视图卸载后另一处继续工作。其差异必须仅包含行为实现及相应测试,不包含机械整理。
+
+## 风险
+
+无状态 JSX 提取仍可能改变 ref 或 effect 时序;因此 Hook 和 ref 的宿主保持不变,DOM 不增加包装。Lexical 提取可能改变嵌套 update、projection 缓存或 history 清理顺序;因此保留原操作体并对照执行次序,而非重写算法。
+
+第一阶段仍不能同时挂载同 Session 的两个编辑器,且保留原 picker/drop 限制。后续若误把目录隔离当成状态隔离,会造成 root 相互解绑、重复附件接收或错误焦点路由;这些限制必须由第二阶段的双实例行为测试关闭。

+ 2 - 2
packages/client/ui-attachment/README.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write packages/client/ui-attachment/README.md
-README.md: c2b429b5ea2f190ab113697aa97eb35c7adcaa29
-README.zh.md: e2d6b3d45eaee7d652c33f223cfb8f061c69c72a
+README.md: 3c69b387914c631f50d783c4a35089ab8db1668f
+README.zh.md: c21b2acdbeb7ced6e9a8733a861c5324121ad194

+ 1 - 0
packages/client/ui-attachment/README.md

@@ -52,6 +52,7 @@ The plugin waits for `conversation.input.attachments`, `conversation.message.ima
 | File | Role |
 |---|---|
 | [`src/client/ComposerAttachments.tsx`](src/client/ComposerAttachments.tsx) | Ordered image/file rail + drop overlay assembly |
+| [`src/client/drop-events.ts`](src/client/drop-events.ts) | Document drag-and-drop listeners installed by each mounted attachment view's effect |
 | [`src/AttachmentRail.tsx`](src/AttachmentRail.tsx) | Horizontal attachment overflow, wheel translation, edge arrows |
 | [`src/client/MessageImages.tsx`](src/client/MessageImages.tsx) | Per-message gallery + lightbox assembly |
 | [`src/MessageImage.tsx`](src/MessageImage.tsx) | Single image sizing, load/retry, click-to-open; local submission-echo previews render their object URL directly |

+ 1 - 0
packages/client/ui-attachment/README.zh.md

@@ -52,6 +52,7 @@ Chat 中的一条用户消息把文件与图片放在同一个靠右、可换行
 | 文件 | 职责 |
 |---|---|
 | [`src/client/ComposerAttachments.tsx`](src/client/ComposerAttachments.tsx) | 有序图片/文件栏+拖放遮罩的组装 |
+| [`src/client/drop-events.ts`](src/client/drop-events.ts) | 每个已挂载附件视图的 effect 安装的 document 拖放监听 |
 | [`src/AttachmentRail.tsx`](src/AttachmentRail.tsx) | 附件横向溢出、滚轮转换、边缘箭头 |
 | [`src/client/MessageImages.tsx`](src/client/MessageImages.tsx) | 每消息画廊+灯箱的组装 |
 | [`src/MessageImage.tsx`](src/MessageImage.tsx) | 单图尺寸、加载/重试、点击打开;本地提交回显预览直接显示其 object URL |

+ 2 - 2
packages/client/ui-conversation/README.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write packages/client/ui-conversation/README.md
-README.md: 8d68dfdb61943c7c45b38d9244134edecc86a593
-README.zh.md: b1d548ba8fc9276a6864548802a45796e74c0c7a
+README.md: 5b94862d830d27ae02e60832294c7595537b47c0
+README.zh.md: 6d7bb72ba582f90356f4bc08a78b2cdfdf211db7

+ 2 - 0
packages/client/ui-conversation/README.md

@@ -38,6 +38,8 @@ Target packages declaration-merge their snapshot and Location data maps, then re
 
 The composer registers the File command action and owns its label, availability, and native file-dialog callback. Menu availability and invocation both consult the mounted composer's current attachment-intake policy. Unmounting or locking the composer disables that action; disposing the plugin removes its registration. The callback binding stays inside the input module.
 
+`SessionInputShell` owns one Lexical editor per Session through its private [DraftEditorRuntime](src/client/input/editor/runtime.ts), while retaining submission, attachment selection, and recovery decisions. [DraftEditor](src/client/input/editor/DraftEditor.tsx) renders the borrowed editor; InputBar retains its Hooks and refs and installs DOM behavior through [view-binding](src/client/input/editor/view-binding.ts). Editor-facing types live in [draft-editor.ts](src/client/contract/draft-editor.ts), with shared input and submission types in [input.ts](src/client/contract/input.ts). This separation does not support simultaneous editable roots for one Session; [the two-stage isolation proposal](../../../.agents/notes/proposed/architecture/2026-09-14-composer-model-and-draft-editor.md) defines the remaining work.
+
 Claimed commands retain their identity and highlight when only their arguments and trailing separator are deleted; editing the command name releases the claim. The same rules apply to every command and locale, including `/goal`, `/目标`, `/plan`, and `/计划`. Command hints and ordinary placeholders remain hidden throughout IME composition and reappear only after the editor commits the final text and the corresponding input is empty.
 
 Workspace selection uses `uiWorkspace.openWorkspace` to prepare the target and commit navigation. Draft text and attachments move in its synchronous preparation callback only while that request is current; later navigation or owner disposal leaves the original draft intact.

+ 2 - 0
packages/client/ui-conversation/README.zh.md

@@ -38,6 +38,8 @@ target package 通过 declaration merge 扩展 snapshot 与 Location data map,
 
 输入框注册「文件」命令动作,负责其标题、可用性和原生文件选择器回调。菜单可用性与实际调用都读取已挂载输入框当前的附件接收策略。输入框卸载或锁定后该动作不可用,插件 dispose(资源释放)时移除注册。回调绑定留在输入模块内部。
 
+`SessionInputShell` 通过私有 [DraftEditorRuntime](src/client/input/editor/runtime.ts) 为每个 Session 持有一个 Lexical editor,同时保留提交、附件选择和恢复决策。[DraftEditor](src/client/input/editor/DraftEditor.tsx) 呈现借用的 editor;InputBar 保留钩子与 refs,并通过 [view-binding](src/client/input/editor/view-binding.ts) 安装 DOM 行为。编辑器类型位于 [draft-editor.ts](src/client/contract/draft-editor.ts),共享输入和提交类型位于 [input.ts](src/client/contract/input.ts)。这一拆分不支持同一 Session 同时挂载多个可编辑 root;[两阶段隔离提案](../../../.agents/notes/proposed/architecture/2026-09-14-composer-model-and-draft-editor.zh.md) 定义剩余工作。
+
 已认领的命令在仅删除参数和末尾分隔空格时保留身份与高亮,改动命令名才会释放认领。所有命令和语言使用相同规则,包括 `/goal`、`/目标`、`/plan` 和 `/计划`。输入法组合输入期间,命令提示和普通占位文字持续隐藏,直到编辑器提交最终文字且对应输入为空时才重新显示。
 
 工作区选择使用 `uiWorkspace.openWorkspace` 准备目标并提交导航。草稿文字和附件仅在该请求仍为当前请求时,通过它的同步准备回调搬移;后续导航或所有者释放会保留原草稿。