Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,11 @@ jobs:
- name: Hotkey recorder e2e
run: python tests/e2e/hotkey_recorder_e2e.py

# Real pointer input (mouse.down/move/up) — a fill()-style test would
# have passed while WebView2 refused every drag.
- name: Wheel mirror manual-sort drag e2e
run: python tests/e2e/wheel_sort_drag_e2e.py

# No __TAURI__ mock — the previous blind spot. Asserts the app fails
# LOUDLY (persistent banner / wheel error card) instead of silently.
- name: Missing-Tauri visibility e2e (no mock)
Expand Down
129 changes: 129 additions & 0 deletions .loom/design/task-brief-manual-sort-and-hide-fields.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
# Devin Brief: 手动拖拽排序失效 + 删除两个不该暴露的字段

## 项目
`/home/haa/sites/promptkey`,当前 `master`(HEAD `330e7c4` = PR #13 squash)。新建分支 `fix/wheel-manual-drag-and-hide-fields`。

## 背景

用户已安装 2.0.3 并实测。原话:

> 轮盘很好用,现在是那个排序时的手动排序,我看到你做的有拖拽啊改变排序但是我发现拖不动没法手动调整顺序,请你做调整...

> 另外我看到的每个提示词的这个,可用范围和那个内部的用编号排序的这个就删掉吧,不显示,这个不需要被调整就不要给出去

所以两个任务:

---

## Task 1:手动拖拽排序拖不动(必须修好,真机可用)

### 现状代码(已核实)

拖拽实现在 `src/js/views/prompts.js` 的 `bindMirrorDrag()`,绑定在**左侧「轮盘」镜像列表** `#wheelMirror` 上:

```js
handle.addEventListener('mousedown', () => { row.draggable = true; });
handle.addEventListener('mouseup', () => { row.draggable = false; });
row.addEventListener('dragstart', e => {
if (state.wheelSort !== 'manual') { e.preventDefault(); toast('info', t('wheel.sortSwitchToManual')); return; }
dragId = +row.dataset.wheelId;
row.classList.add('dragging');
e.dataTransfer.effectAllowed = 'move';
});
```

### 已知问题(我逐行读过,至少这些)

1. **`mousedown`/`mouseup` 设置 `draggable` 的做法在 Windows WebView2 上极不可靠**——`draggable` 属性的**时序**决定拖拽能否启动,某些情况下 `dragstart` 根本不触发。这是 HTML5 原生拖拽在 WebView2 上的长期坑。
2. **`row.dataset.wheelId` 若渲染时没写 `data-wheel-id`(而是 `data-id` 之类),`dragId` 恒为 `NaN`** —— 那么 `dragstart` 里 `dragId = NaN`,`drop` 里 `if (!dragId || dragId === overId) return;`(NaN 是 truthy 但 `NaN === NaN` 为 false,能过)但 `pins.indexOf(NaN)` = -1 → `if (from < 0 || to < 0) return;` → **完全静默失败**。**先核实 attribute 名字到底叫什么**。
3. **非 manual 模式时只 toast 不进入拖拽**,用户可能连"拖不动"和"不让拖"都分不清。
4. `mousedown` 才置 `draggable=true` 意味着**鼠标按下后到移动之间的窗口很短**,慢一点或先停顿就拖不起来。

### 要求

**直接换成不依赖 HTML5 原生拖拽的实现。** 推荐**指针事件(pointerdown/pointermove/pointerup)+ 手动视觉反馈**的自制排序:

- 按住行 → 行进入拖拽态(视觉:抬升/半透明/影子),可**上下移动**
- 拖动过程中给出**清晰的插入位置指示**(一条分割线或目标行高亮)
- 松手 → 计算目标 index → `ipc('set_pin_order', { ids })` → 重新加载
- **必须支持鼠标和触摸**(`pointerdown` 天然覆盖)
- 保留 `wheel` 镜像列表的**滚动**(拖拽时不能把滚动吞了):可约定"按住句柄区才拖"或"长按/位移超过阈值才进入拖拽"
- 必须**真的拖得动**。不要"看起来能拖但实现是 HTML5 原生那套"

顺带:

- **拖拽态下禁止 wheel 事件冒泡到行的点击**(拖完误触发预览/注入)
- 排序模式非 manual 时:**要么自动切到 manual 再拖,要么给出明确引导**(哪个体验好你论证后选一个,说明理由)
- 键盘可达性:方向键 + 空格/回车也能移动行(可选,但做更好)
- 视觉风格遵循既有设计系统,动效轻量不廉价

---

## Task 2:删掉两个不该给出去的字段(UI 层,用户明确要求)

用户要删的:

| 要删的 | 具体位置 |
|---|---|
| **可用范围** | `src/index.html:202` `<div class="field"><label data-i18n="f.apps">应用范围(可选)</label><input type="text" id="fApps" ...>` —— 编辑抽屉里的「应用范围」输入框 |
| **内部用编号排序** | `src/index.html:203` `<div class="field"><label data-i18n="f.order">轮盘位置(手动排序时生效)</label><input type="text" id="fOrder" ...>` —— 编辑抽屉里的「轮盘位置」编号输入框 |

用户原话:「**这个不需要被调整就不要给出去**」—— 意思是这是内部机制,不该裸露给用户,**UI 不显示即可**。

### 要求

- **从 UI 移除这两个字段**(抽屉不再显示)
- `src/js/views/prompts.js` 里对应的**读取/写入/监听**(`setV('#fApps', ...)`、`setV('#fOrder', ...)`、`payload` 里的 `app_scopes_json` / `inject_order`、`['fName','fContent','fTags','fApps','fOrder']` 的 dirty 监听数组)**全部清理干净**
- 同时清理 i18n 里不再使用的 key(`f.apps`、`f.appsPh`、`f.order` 等,中英双语都要删)
- **不要动后端/数据库/schema**:`app_scopes_json` 和 `inject_order` 字段**保留**(后端与其他流程可能仍依赖,尤其 `set_pin_order` 写的就是 pin order)。只是**前端不再让用户直接编辑它们**
- 注意:`src/js/store.js:92` 有 `inject_order` 的解析(`parseInt(p.inject_order, 10)`),那是内部排序逻辑,**保留**

---

# 硬约束

- 无 cargo,本机不编译。**CI 必须全绿**(cargo check/clippy/test + frontend static checks + playwright)
- Playwright:**新增拖拽排序的真实验证用例**(pointer 事件驱动,不是 `fill()` 那种假交互)。断言:按住→移动→松手后 DOM 顺序变化**且** `set_pin_order` 被调用
- 既有测试不得回归
- i18n 中英双语同步,删掉的 key 两侧一致
- 一个 commit `fix(ui): real pointer-based manual sort + hide internal fields from drawer`,push,PR `--base master`,**不合并**
- 遵守 LOOM 流程

---

## 最终交付

```
## 拖拽为什么拖不动
(先给逐条证据链:mousedown/draggable 时序问题、dataset attribute 名核实结果、以及任何你发现的其他断点。**先证明你找到了真根因再动手**)

## 实现方案
(为什么选 pointer 事件而非修 HTML5 原生拖拽;插入位置指示怎么做的;滚动冲突怎么解;非 manual 模式的处理策略及理由)

## 删除字段
(删了哪些 UI 元素 + 清理了哪些 JS/i18n;确认后端字段保留未被破坏)

## 测试
(CI 结果 + 新拖拽用例的断言细节 + 回归结果)

## 真机验证清单
(用户在 Windows 上按什么顺序试,每步预期)

## 未编译验证清单
```

```json
COMPLETION_NOTIFY
source: assistant
task: promptkey-manual-sort-fix
deliverables:
- root cause of "drag does not move" proven with evidence
- pointer-event based manual sort implemented and visually verified
- insertion indicator + scroll coexistence + click-suppression handled
- app scope & inject order fields removed from drawer UI, JS, and i18n
- backend fields preserved
- CI green incl. a real pointer-driven drag test
- branch fix/wheel-manual-drag-and-hide-fields + PR against master (not merged)
status: success
errors: none
```
2 changes: 1 addition & 1 deletion .loom/state.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"name": "promptkey",
"status": "complete",
"created_at": "2026-10-06T08:16:56.246Z",
"updated_at": "2026-10-07T04:30:31.807Z"
"updated_at": "2026-10-07T06:08:09.960Z"
},
"understanding": {
"confirmed": [
Expand Down
94 changes: 94 additions & 0 deletions .loom/tasks.json
Original file line number Diff line number Diff line change
Expand Up @@ -940,6 +940,100 @@
"created_at": "2026-10-07T04:17:40.992Z",
"updated_at": "2026-10-07T04:30:31.807Z",
"completed_at": "2026-10-07T04:30:31.807Z"
},
{
"id": "TASK-010",
"title": "Fix wheel mirror manual drag (pointer-based) + hide app-scope/order fields from drawer",
"outcome": "Wheel mirror list in the prompts view supports real pointer-driven reorder (mouse+touch) with insertion indicator, auto-switching to manual sort; the edit drawer no longer exposes app_scopes_json / inject_order inputs while backend fields stay intact.",
"acceptance": [
{
"criterion": "Dragging a wheel-mirror row by pointer reorders the DOM and calls set_pin_order with the new id sequence; verified by a Playwright test using real mouse.down/move/up events",
"verify_by": "python tests/e2e/wheel_sort_drag_e2e.py asserts DOM order change + set_pin_order payload",
"evidence": "tests/e2e/wheel_sort_drag_e2e.py: mouse.down on Delta row, move to above Beta, mouse.up -> __CALLS__ records set_pin_order ids=[1,4,2,3], DOM shows Alpha,Delta,Beta,Gamma. Mid-drag assertions verify .dragging lift + .wm-insert slot marker."
},
{
"criterion": "Drag works when wheelSort starts in 'auto' (default): first successful drag switches mode to manual, persists pk-wheel-sort, and the dropped order survives reload",
"verify_by": "e2e asserts wheelSortBtn label flips to manual + set_pin_order called once with final order",
"evidence": "e2e asserts localStorage pk-wheel-sort === 'manual' and sortBtn label 手动 after the drop; DOM order Alpha,Delta,Beta,Gamma is read back after loadPrompts re-render (mock writes inject_order per set_pin_order)."
},
{
"criterion": "Edit drawer shows no '应用范围'/'轮盘位置' inputs; saving an existing prompt preserves its app_scopes_json and inject_order instead of wiping them",
"verify_by": "e2e opens drawer: #fApps/#fOrder absent; update_prompt payload echoes stored values",
"evidence": "e2e: #fApps/#fOrder count 0, drawer text lacks both labels; update_prompt payload echoes app_scopes_json='[\"Code.exe\"]' and inject_order='1'."
},
{
"criterion": "i18n keys f.apps / f.order removed from both zh-CN.js and en-US.js; no dangling references; node --check clean on all JS",
"verify_by": "grep -rn 'f\\.apps\\|f\\.order' src/ returns nothing; find src -name '*.js' | node --check loop",
"evidence": "grep -rn 'f\\.apps\\|f\\.order\\|fApps\\|fOrder' src/ -> only a code comment remains; node --check loop clean."
}
],
"done_when": [],
"boundaries": [
"Does not change backend/schema: prompts.app_scopes_json and prompts.inject_order columns, set_pin_order IPC, and store.js pinOrder parsing stay untouched",
"Does not change wheel.html/wheel.js runtime wheel ordering behavior"
],
"depends_on": [],
"reads": [
".loom/design/task-brief-manual-sort-and-hide-fields.md",
"src/js/views/prompts.js",
"src/js/store.js",
"src/index.html",
"src/styles.css",
"src/js/i18n/zh-CN.js",
"src/js/i18n/en-US.js",
"tests/e2e/hotkey_status_e2e.py"
],
"touches": [
"src/js/views/prompts.js",
"src/index.html",
"src/styles.css",
"src/js/i18n/zh-CN.js",
"src/js/i18n/en-US.js",
"tests/e2e/wheel_sort_drag_e2e.py",
".github/workflows/ci.yml"
],
"implements": ".loom/design/task-brief-manual-sort-and-hide-fields.md",
"design_exemption": "",
"capability_hooks": [],
"capability_exemption": "Interaction fix within existing design-system tokens; no new professional field is engaged",
"integrity_version": 1,
"covers": [],
"status": "done",
"progress": {
"completed": [],
"current": "complete",
"next": ""
},
"evidence": [
"Pointer-based manual sort replaces HTML5 DnD in bindMirrorDrag (prompts.js): mouse drags from anywhere on a row after 5px vertical threshold; touch drags only from the grip (touch-action:none) so row presses still scroll .leftcol. Dragged row lifts via position:fixed + translate3d; dashed .wm-insert slot marks the landing position through plain flex reflow. Drop commits via set_pin_order; commits from auto mode flip wheelSort to manual only after the write succeeds.",
"Drawer no longer renders 应用范围/轮盘位置 inputs; update_prompt now passes app_scopes_json/inject_order through unchanged so a save cannot wipe set_pin_order output or stored scopes. i18n keys f.apps/f.order removed in zh-CN + en-US; wheel.sortAutoSwitch added on both sides.",
"Local e2e: wheel_sort_drag_e2e.py 17/17, hotkey_status 10/10, hotkey_recorder 22/22, no_tauri 13/13; node --check clean on all src js; check_capabilities OK. Visual verification screenshots mid-drag/after-drop/drawer taken via Playwright.",
{
"type": "acceptance_results",
"results": [
{
"criterion": "Dragging a wheel-mirror row by pointer reorders the DOM and calls set_pin_order with the new id sequence; verified by a Playwright test using real mouse.down/move/up events",
"evidence": "tests/e2e/wheel_sort_drag_e2e.py: mouse.down on Delta row, move to above Beta, mouse.up -> __CALLS__ records set_pin_order ids=[1,4,2,3], DOM shows Alpha,Delta,Beta,Gamma. Mid-drag assertions verify .dragging lift + .wm-insert slot marker."
},
{
"criterion": "Drag works when wheelSort starts in 'auto' (default): first successful drag switches mode to manual, persists pk-wheel-sort, and the dropped order survives reload",
"evidence": "e2e asserts localStorage pk-wheel-sort === 'manual' and sortBtn label 手动 after the drop; DOM order Alpha,Delta,Beta,Gamma is read back after loadPrompts re-render (mock writes inject_order per set_pin_order)."
},
{
"criterion": "Edit drawer shows no '应用范围'/'轮盘位置' inputs; saving an existing prompt preserves its app_scopes_json and inject_order instead of wiping them",
"evidence": "e2e: #fApps/#fOrder count 0, drawer text lacks both labels; update_prompt payload echoes app_scopes_json='[\"Code.exe\"]' and inject_order='1'."
},
{
"criterion": "i18n keys f.apps / f.order removed from both zh-CN.js and en-US.js; no dangling references; node --check clean on all JS",
"evidence": "grep -rn 'f\\.apps\\|f\\.order\\|fApps\\|fOrder' src/ -> only a code comment remains; node --check loop clean."
}
],
"at": "2026-10-07T06:08:09.959Z"
}
],
"created_at": "2026-10-07T05:55:04.721Z",
"updated_at": "2026-10-07T06:08:09.959Z",
"completed_at": "2026-10-07T06:08:09.959Z"
}
]
}
2 changes: 0 additions & 2 deletions src/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -199,8 +199,6 @@
<div class="var-hint" id="varHint"></div></div>
<div class="field"><label data-i18n="f.tags">标签</label><input type="text" id="fTags" data-i18n-ph="f.tagsPh" placeholder="dev, 写作…"></div>
<div class="switch-row"><span class="lbl"><span class="ico ico-sm" data-icon="pin"></span> <span data-i18n="f.pinned">加入轮盘</span></span><button class="switch" id="fPin" aria-label="pin"></button></div>
<div class="field"><label data-i18n="f.apps">应用范围(可选)</label><input type="text" id="fApps" placeholder="chrome.exe, Code.exe"></div>
<div class="field"><label data-i18n="f.order">轮盘位置(手动排序时生效)</label><input type="text" id="fOrder" inputmode="numeric" placeholder="1"></div>
<div class="drawer-meta" id="fMeta"></div>
</div>
<div class="drawer-foot">
Expand Down
5 changes: 2 additions & 3 deletions src/js/i18n/en-US.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@ export default {
"wheel.sortManual": "Order: manual",
"wheel.sortSwitchToManual": "Switch to manual order",
"wheel.sortSwitchToAuto": "Switch to auto order",
"wheel.dragHint": "Drag to reorder",
"wheel.sortAutoSwitch": "Switched to manual order",
"wheel.dragHint": "Drag to reorder (or focus and press ↑↓)",
"wh.esc": "Esc",
"wh.nomatch": "No match",
"wh.filtering": "Filter",
Expand Down Expand Up @@ -162,8 +163,6 @@ export default {
"f.tags": "Tags",
"f.tagsPh": "dev, writing…",
"f.pinned": "Pin to wheel",
"f.apps": "App scopes (optional)",
"f.order": "Wheel position (manual order only)",
"f.preview": "Preview",
"f.vars": "Variables",
"f.autoVars": "Auto: {{clipboard}} clipboard · {{date}} today",
Expand Down
5 changes: 2 additions & 3 deletions src/js/i18n/zh-CN.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@ export default {
"wheel.sortManual": "排序:手动",
"wheel.sortSwitchToManual": "切换为手动排序",
"wheel.sortSwitchToAuto": "切换为自动排序",
"wheel.dragHint": "拖动可调整顺序",
"wheel.sortAutoSwitch": "已切换为手动排序",
"wheel.dragHint": "拖动调整顺序(聚焦后可用 ↑↓ 键)",
"wh.esc": "Esc 关闭",
"wh.nomatch": "无匹配",
"wh.filtering": "筛选",
Expand Down Expand Up @@ -162,8 +163,6 @@ export default {
"f.tags": "标签",
"f.tagsPh": "dev, 写作…",
"f.pinned": "加入轮盘",
"f.apps": "应用范围(可选)",
"f.order": "轮盘位置(手动排序时生效)",
"f.preview": "预览",
"f.vars": "变量",
"f.autoVars": "自动:{{clipboard}} 剪贴板 · {{date}} 当前日期",
Expand Down
Loading
Loading