Folder references settle on pick; descent moves to an explicit drill verb
文件夹引用 pick 即选定;下钻移交给显式 drill 动词
A directory row in the `@` menu had one verb doing two jobs. Picking it inserted literal `@dir/` text and kept the menu open — the descent path for reaching a file — so a user who wanted the folder *itself* as context never got a settled entity: the token kept its trigger character, stayed editable (typing `123` kept filtering children), and looked nothing like the atomic chip a file pick produces. Field feedback wit
English
Problem
A directory row in the @ menu had one verb doing two jobs. Picking it inserted literal @dir/ text and kept the menu open — the descent path for reaching a file — so a user who wanted the folder itself as context never got a settled entity: the token kept its trigger character, stayed editable (typing 123 kept filtering children), and looked nothing like the atomic chip a file pick produces. Field feedback with a competitor screenshot made the expectation concrete: a chosen folder should be as settled as a chosen file.
Decision
Split the two intents into two verbs on the same row, keyboard-mapped to shell-completion instincts:
- Settle (row click / Enter): the directory resolves as an atomic folder chip — the file chip's exact language: folder glyph,
dir/label, no trigger character, one deletable unit — whose serialized and clipboard form is the canonical@dir/mention. Implementation is the{ insert }arm the folder path had simply never taken;appearance: 'folder'was already supported end to end. - Drill (Tab / the row's trailing chevron): the previous behavior verbatim — literal editable
@dir/text, menu open on the children.
The plumbing is one new dimension, not a parallel path: InputTriggerCandidate.drill?: boolean advertises the second verb (only ui-reference directories set it), InputTriggerPick.action: 'pick' | 'drill' reports which one ran, ArbitrateKey gains 'tab', and the composer keymap registers KEY_TAB_COMMAND through the same arbitration helper as the arrows — 'consumed' prevents default, anything else leaves native focus traversal alone. MenuView renders the chevron only on drill rows (role="button" span inside the option, mousedown like the row so composer focus survives, stopPropagation so the row's settling pick stays out).
Alternatives considered
- Settle on menu close (auto-fold a literal
@dir/into a chip when the menu dismisses): rejected — the moment an editable token becomes an entity would be invisible and surprising; hand-typed mentions stay honest text. - Drill on click, settle via a dedicated row button (the inverse mapping): rejected — settling is the common intent and deserves the primary gesture; descent is the power-user refinement, which matches Tab.
- CSS-overpainting the trigger character on the literal text instead of introducing an entity: rejected earlier for the same reason it failed the folder-glyph fix — a Lexical text node cannot split its trigger character out, and the literal text is not a settled entity anyway.
Consequences
- A picked folder and a picked file are the same species: atomic, glyph-labeled, no
@, whole-unit deletion; hand-typed@dir/remains a plain-text reference with the glyph prefix. onPickimplementations that ignoreactionbehave exactly as before ('pick'is what every pre-existing path reports); the only behavioral change sits inui-reference's directory arm.- Tab is intercepted only while the menu highlights a drill row; everywhere else the browser keeps it, pinned by the keymap-routing spec.
- Coverage: controller arbitration (drill / plain / pick-action), MenuView chevron routing,
ui-referenceverb split, and a real-browser e2e driving all three gestures (Enter settle, Tab drill, chevron drill) against a real workspace directory.
中文
问题
@ 菜单里的目录行用一个动词干两件事。pick 它会插入字面 @dir/ 文本并保持菜单打开——那是抵达文件的下钻路径——于是想要文件夹本身作为上下文的用户永远得不到一个已选定的实体:token 保留触发字符、保持可编辑(继续输入 123 会继续筛选子项),与文件 pick 产出的原子 chip 完全不是一个物种。现场反馈附上竞品截图把期望说得很具体:选中的文件夹应当与选中的文件一样"定下来"。
决策
把两个意图拆成同一行上的两个动词,键位对齐 shell 补全直觉:
- 选定(点击行主体 / Enter):目录解析为原子 folder chip——与文件 chip 完全同语言:文件夹图标、
dir/标签、无触发字符、整体删除一个单位——序列化与剪贴板形式为规范@dir/mention。实现上就是文件夹路径从未走过的{ insert }分支;appearance: 'folder'端到端早已支持。 - 钻取(Tab / 行尾 chevron):原行为原样保留——字面可编辑的
@dir/文本,菜单对子项保持打开。
管线上是一个新维度而非平行通路:InputTriggerCandidate.drill?: boolean 声明第二动词(只有 ui-reference 的目录行设置),InputTriggerPick.action: 'pick' | 'drill' 报告实际执行的是哪一个,ArbitrateKey 增加 'tab',composer keymap 经与方向键相同的仲裁 helper 注册 KEY_TAB_COMMAND——'consumed' 才 preventDefault,其余情况原生焦点遍历不受影响。MenuView 只在 drill 行渲染 chevron(option 内的 role="button" span,与行同用 mousedown 保住 composer 焦点,stopPropagation 把行主体的选定 pick 挡在外面)。
曾考虑的替代方案
- 菜单关闭时选定(菜单消失时把字面
@dir/自动固化为 chip):否决——可编辑 token 变实体的时机不可见且令人意外;手敲的 mention 保持诚实文本。 - 点击钻取、行内专用按钮选定(反向映射):否决——选定是常见意图,应占据主手势;下钻是进阶细化,与 Tab 匹配。
- 对字面文本 CSS 覆盖触发字符而不引入实体:早先在文件夹图标修复中已因同一原因否决——Lexical 文本节点无法拆出触发字符,而且字面文本本来就不是已选定的实体。
后果
- pick 出的文件夹与文件是同一物种:原子、带图标标签、无
@、整体删除;手敲的@dir/仍是带图标前缀的纯文本引用。 - 忽略
action的onPick实现行为与从前完全一致(既有路径全部报告'pick');唯一的行为变化在ui-reference的目录分支。 - 只有菜单高亮在 drill 行时才拦截 Tab;其余场合浏览器保有该键,由 keymap-routing spec 钉住。
- 覆盖:controller 仲裁(drill / 普通行 / pick action)、MenuView chevron 路由、
ui-reference动词分流,以及在真实工作区目录上驱动全部三个手势(Enter 选定、Tab 钻取、chevron 钻取)的真浏览器 e2e。