dsh-composer-lists 1.0.0 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +42 -37
  2. package/client.js +78 -8
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -1,64 +1,69 @@
1
1
  # dsh-composer-lists
2
2
 
3
- DSH(DeepSeek Harness)聊天输入框的列表编排插件:自动续号与重排、标记感知的光标移动、层级升降,以及一个可撤销的 AI 提示词优化按钮。
3
+ [![CI](https://github.com/wangjianghu/dsh-composer-lists/actions/workflows/ci.yml/badge.svg)](https://github.com/wangjianghu/dsh-composer-lists/actions/workflows/ci.yml)
4
+ [![npm](https://img.shields.io/npm/v/dsh-composer-lists)](https://www.npmjs.com/package/dsh-composer-lists)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](./LICENSE)
4
6
 
5
- Ordered/unordered list editing for the DeepSeek Harness message composer: automatic numbering and renumbering, marker-aware caret moves, level promotion, and an undoable AI prompt-polish button.
7
+ **一键把粗糙的想法改写成结构化的 Agent 提示词**,再把输入框变成顺手的多级列表编辑器。Turn a rough idea into a structured agent prompt in one click — plus ordered/unordered list editing in the DSH composer.
6
8
 
7
- ## 安装 / Install
9
+ ## ✨ AI 提示词结构化改写(核心功能)
8
10
 
9
- ```sh
10
- dsh plugin add dsh-composer-lists
11
+ 写 Agent 提示词最耗时的不是措辞,是结构:目标、步骤、约束混成一团,模型只能猜。点一下输入框旁的 ✨,当前草稿会被改写为「**一句话任务目标 + 有序步骤列表(含子层级)+ 无序约束与验收标准**」的结构化提示词——正是 Agent 最容易准确执行的形态。
12
+
13
+ 改写效果示例 / What the rewrite looks like:
14
+
15
+ ```text
16
+ ✨ 改写前 改写后 ↩
17
+ 帮我做个周报,要包含这周做了什么, 1. 生成本周工作周报,按项目分组。
18
+ 下周的计划,还有些数据要整理一下, 1. 汇总本周各项目进展与关键结果;
19
+ 老板喜欢看图表,别太长 2. 整理下周计划,标注优先级。
20
+ - 输出包含进度图表,正文控制在 500 字内;
21
+ - 只使用草稿中提到的事实,不补充假设。
11
22
  ```
12
23
 
13
- Requires dsh 0.2.1+. 刷新页面后生效 / takes effect after a page refresh.
24
+ 工程上为「点一下就完事」做了完整闭环:
25
+
26
+ - **一键撤销**:改写完成后 ✨ 变 ↩,一键恢复原文;手动编辑过结果后撤销自动退场,绝不吞你的修改
27
+ - **跨模型自适应**:自动选用当前默认模型自己声明的**最轻思考档**(改写不需要推理,速度从十几秒降到 2–5 秒;模型未声明档位则自动跳过)
28
+ - **自愈与防护**:思考耗尽生成预算时自动加倍重试;请求 90 秒硬超时,优化中按 Esc 立即中止;草稿含引用/命令 chip 时自动禁用;无意义输入(过短、纯符号、复读)直接拦截
29
+ - **透明失败**:出错时明确提示原因(超时/模型拒绝/无可用模型),输入框永不锁死
14
30
 
15
- ## 行为 / Behavior
31
+ ## 列表编排(第二能力)
16
32
 
17
- 遵循 DSH 原生按键约定(Enter 发送、⌘Enter 加速发送均不改动):
33
+ 结构化提示词里全是列表——所以输入框本身也需要顺手的列表编辑。遵循 DSH 原生按键(Enter 发送、⌘Enter 加速发送不改动):
18
34
 
19
35
  | 手势 | 行为 |
20
36
  |---|---|
21
- | **Shift+Enter** | 列表行内换行并自动续号(沿用当前行样式:`1.`→`2.`、`a.`→`b.`、`①`→`②`);空列表项上按 = 退出列表并入上一行行尾(不留空行) |
22
- | **Backspace** | 光标紧跟标记后:二级及更深 → 升一级(如 `b. `→`2. `,光标停在新标记右侧);一级 → 整个标记一步删掉;空列表项 → 删整行并入上一行行尾 |
23
- | **← / →** | 标记前缀(缩进+编号/符号+空格)作为一步跨过:标记右侧按 ← 一次到行首,行首按 → 一次到标记右侧 |
24
- | **⌘← / Home** | 跳到行首;列表行落在标记右侧,再按一次到最左 |
25
- | **Tab / Shift+Tab** | 仅列表行:降 / 升一级,所在块按新层级重排样式与编号;多行选中批量 |
26
- | **⌘⇧8 / ⌘⇧7** | 当前行或选中行:加无序 / 有序标记;再按取消,可反复循环 |
27
- | **⌘⇧B** | 选中文本加 `**粗体**`(⌘B 被 DSH 菜单占用,无法到达页面) |
28
- | 输入/删除/粘贴 | 有序编号自动重排(多行块内顺序归一;手改编号不会被覆盖) |
37
+ | **Shift+Enter** | 列表行内换行并自动续号(沿用当前行样式);空项上按 = 退出列表 |
38
+ | **Backspace** | 光标紧跟标记后:二级及更深 → 升一级(`b. `→`2. `,光标停在当前行);一级 → 整个标记一步删掉;空项 → 退出 |
39
+ | **← / →** | 标记前缀作为一步跨过(标记右侧 ← 一次到行首,行首 → 一次到标记右侧) |
40
+ | **⌘← / Home** | 跳到行首;列表行落在标记右侧,再按到最左 |
41
+ | **Tab / Shift+Tab** | 降 / 升一级,所在块按新层级重排样式与编号;层级改变时光标保持原字符位置 |
42
+ | **⌘⇧8 / ⌘⇧7** | 当前行或选中行:加无序 / 有序标记;再按取消 |
43
+ | **⌘⇧B** | 选中文本加 `**粗体**`(⌘B 被 DSH 菜单占用) |
44
+ | 输入/删除/粘贴 | 有序编号自动重排;手改编号不会被覆盖 |
29
45
 
30
- ## 层级样式(默认,⚙ 中可自定义)
46
+ 默认六级样式:有序 `1.` → `a.` → `①` → `(1)` → `α.` → `1.`,无序 `- * + • ○ ▪`;每层可在 ⚙ 设置中自定义,缩进宽度 2/4/8 可选。手写 `a.` `α.` `(3)` `①` 同样被识别并参与重排。
31
47
 
32
- | 层级 | 有序 | 无序 |
33
- |---|---|---|
34
- | 一级 | `1. 2. 3.` | `-` |
35
- | 二级 | `a. b. c.` | `*` |
36
- | 三级 | `① ② ③` | `+` |
37
- | 四级 | `(1) (2) (3)` | `•` |
38
- | 五级 | `α. β. γ.` | `○` |
39
- | 六级 | `1. 2. 3.` | `▪` |
40
-
41
- 每层有序样式可选 `1.` / `a.` / `α.` / `(1)` / `①`,无序符号可选 `- * + • ○ ▪ –`,缩进宽度 2/4/8,另有自动重排与优化按钮开关。手写 `a.` `α.` `(3)` `①` 同样被识别并参与重排。设置存于浏览器 localStorage,即时生效。
42
-
43
- ## AI 提示词优化(✨ 按钮)
48
+ ## 安装 / Install
44
49
 
45
- 点击将当前草稿交由 Host 侧当前默认模型改写为「任务目标 + 有序步骤 + 无序约束」的结构化列表,完成后可一键 ↩ 撤销恢复原文;手动编辑过优化结果后撤销自动退场。请求 90 秒超时、优化中可按 Esc 中止,生成失败有明确提示。
50
+ ```sh
51
+ dsh plugin add dsh-composer-lists
52
+ ```
46
53
 
47
- ## 实现说明
54
+ Requires dsh 0.2.1+. 已收录于 [awesome-dsh-plugin](https://github.com/awesome-dsh-plugin/awesome-dsh-plugin) 市场目录 / listed in the plugin catalog. 刷新页面生效。
48
55
 
49
- - 浏览器半挂载于 `conversation.input.left` 槽位,所有编辑经会话 `inputActions.insertText / setDraft`(一次手势 = 一步撤销)
50
- - 含引用 chip / 命令 chip / 引用块时键盘编排自动停用;触发菜单(`/`、`@`)打开时不拦截按键;IME 组词期间不处理
51
- - Host 半注册 `POST /composer-lists/optimize` 路由:跨域精确校验、请求体 256KB 上限、思考档自动取模型最轻档、断连与卡流超时保护
56
+ AI 改写使用你当前配置的默认模型,经 Host 本地路由(`POST /composer-lists/optimize`)完成,不引入额外密钥或第三方服务。
52
57
 
53
58
  ## 开发 / Development
54
59
 
55
60
  ```sh
56
- node test-pure.cjs # 纯函数回归 + 确定性混沌测试(7000+ 断言)
61
+ node test-pure.cjs # 8000+ 断言:纯函数回归 + 确定性混沌测试
57
62
  node test-host.cjs # Host 路由集成测试(假 req/res 驱动)
58
- node bench-pure.cjs # 性能基准
63
+ node bench-pure.cjs # 性能基准(每按键路径 <1ms)
59
64
  ```
60
65
 
61
- 改 `client.js` 后用插件管理停用→启用一次即同步页面;改 `index.js`(Host 半)需重启 dsh。
66
+ 改 `client.js` 后停用→启用插件即同步页面;改 `index.js`(Host 半)需重启 dsh。
62
67
 
63
68
  ## License
64
69
 
package/client.js CHANGED
@@ -12,6 +12,10 @@
12
12
  * Tab / Shift+Tab → on list lines only: indent / outdent one level
13
13
  * Cmd/Ctrl+Shift+8 → toggle unordered list on the caret line / selection
14
14
  * Cmd/Ctrl+Shift+7 → toggle ordered list on the caret line / selection
15
+ * (a collapsed caret keeps its content offset; the
16
+ * renumbering the toggle implies on the surrounding
17
+ * block folds into the same edit, so the auto-renumber
18
+ * reaction never moves the caret afterwards)
15
19
  *
16
20
  * Ordered numbers are renumbered automatically after deletions or pastes.
17
21
  * Keyboard-only by design: the runtime renders no UI (a null component
@@ -313,12 +317,25 @@ window.__ModuleLoader__.load({
313
317
  /**
314
318
  * Toggle a list kind over the line range [a, b]: every line already of
315
319
  * that kind turns it off; otherwise lines convert, using each level's
316
- * configured style. Pressing repeatedly cycles on/off. The replacement
317
- * covers only marker prefixes so the caret lands right after the marker.
320
+ * configured style. Pressing repeatedly cycles on/off.
321
+ *
322
+ * The conversion is then converged to the auto-renumber state (numbers
323
+ * continue across the surrounding block, later siblings shift) inside
324
+ * THIS edit: the renumber reaction's own insertText would park the
325
+ * caret at ITS replacement end, dragging it away from the caret line.
326
+ * With a collapsed `caret` the plan ends exactly at the caret's content
327
+ * offset, so insertText parks it on the same content character (the
328
+ * Tab/Shift+Tab treatment); marker rewrites the convergence needs
329
+ * strictly AFTER the caret line are split into `pre`, which the caller
330
+ * applies first so the caret edit goes last. Rewrites BEFORE the caret
331
+ * line fold into the plan itself — its span may start earlier, the
332
+ * caret target is unaffected.
318
333
  * @param {'bullet'|'ordered'} kind
319
- * @returns {null|{start:number,end:number,text:string}}
334
+ * @param {number} [caret] collapsed caret offset in `text` coordinates
335
+ * @returns {{pre:{start:number,end:number,text:string}|null,
336
+ * plan:{start:number,end:number,text:string}}}
320
337
  */
321
- function togglePlan (text, a, b, kind, settings) {
338
+ function togglePlan (text, a, b, kind, settings, caret) {
322
339
  const unit = settings.indentUnit
323
340
  const lines = text.split('\n')
324
341
  const last = Math.min(b, lines.length - 1)
@@ -351,7 +368,43 @@ window.__ModuleLoader__.load({
351
368
  out[k] = withMarker(lines[k], renderOrderedCore(settings.orderedStyles[level], counters.get(lead)))
352
369
  }
353
370
  }
354
- return markerScopePlan(text, a, last, out)
371
+ // Converge to what the auto-renumber reaction would settle on; doing
372
+ // it here keeps the reaction idle after the edit.
373
+ const norm = renumberLines(out, 0, out.length - 1, settings, false).out
374
+ let from = a
375
+ for (let k = 0; k < a; k += 1) {
376
+ if (norm[k] !== lines[k]) { from = k; break }
377
+ }
378
+ let firstTrail = -1
379
+ let lastTrail = -1
380
+ for (let k = last + 1; k < lines.length; k += 1) {
381
+ if (norm[k] !== lines[k]) {
382
+ if (firstTrail < 0) firstTrail = k
383
+ lastTrail = k
384
+ }
385
+ }
386
+ if (caret === undefined) {
387
+ // Selection: one marker-scope replacement reaching back/forward to
388
+ // every line the convergence touched; the caret parks after the
389
+ // final marker as before.
390
+ return { pre: null, plan: markerScopePlan(text, from, Math.max(last, lastTrail), norm) }
391
+ }
392
+ const lastStart = lineRange(text, last, last).start
393
+ const lastEnd = lineRange(text, last, last).end
394
+ // The handler always passes a caret on line `last`; clamp anyway so
395
+ // an off-line caret can never pull the '\n' past the line end into
396
+ // the replacement (which would overlap `pre`).
397
+ const caretOnLine = Math.min(Math.max(caret, lastStart), lastEnd)
398
+ const oldEnd = lastStart + markerEndOf(lines[last])
399
+ const contentOffset = Math.max(0, caretOnLine - oldEnd)
400
+ const newEnd = markerEndOf(norm[last])
401
+ const pieces = []
402
+ for (let k = from; k < last; k++) pieces.push(norm[k])
403
+ pieces.push(norm[last].slice(0, Math.min(norm[last].length, newEnd + contentOffset)))
404
+ return {
405
+ pre: firstTrail >= 0 ? markerScopePlan(text, firstTrail, lastTrail, norm) : null,
406
+ plan: { start: lineRange(text, from, from).start, end: Math.max(caretOnLine, oldEnd), text: pieces.join('\n') },
407
+ }
355
408
  }
356
409
 
357
410
  /**
@@ -946,7 +999,19 @@ window.__ModuleLoader__.load({
946
999
  const sel = selectionLines(editable)
947
1000
  if (sel === null) return
948
1001
  const kind = e.code === 'Digit7' || e.code === 'Numpad7' ? 'ordered' : 'bullet'
949
- if (!applyPlan(togglePlan(sel.text, sel.a, sel.b, kind, settingsRef.current), editable)) return
1002
+ // Collapsed caret: the plan ends at the caret's content offset,
1003
+ // so the toggle parks it on the same content character.
1004
+ const caret = sel.collapsed ? Math.max(sel.span.start, sel.span.end) : undefined
1005
+ const { pre, plan } = togglePlan(sel.text, sel.a, sel.b, kind, settingsRef.current, caret)
1006
+ // Renumbering past the caret line goes FIRST — the caret edit
1007
+ // must be the last insertText, which is where the caret parks.
1008
+ const preApplied = pre !== null && applyPlan(pre, editable)
1009
+ if (!applyPlan(plan, editable)) {
1010
+ if (!preApplied) return // edit refused: keep the native key
1011
+ e.preventDefault()
1012
+ e.stopPropagation()
1013
+ return
1014
+ }
950
1015
  e.preventDefault()
951
1016
  e.stopPropagation()
952
1017
  return
@@ -1371,7 +1436,7 @@ window.__ModuleLoader__.load({
1371
1436
  }
1372
1437
  .clp-popover {
1373
1438
  position: fixed; z-index: 2147483000; width: 340px; height: min(432px, calc(100vh - 24px));
1374
- overflow: auto; padding: 10px 12px 10px; box-sizing: border-box;
1439
+ overflow: hidden; padding: 10px 12px 10px; box-sizing: border-box;
1375
1440
  background: var(--dsw-alias-bg-overlay); color: var(--dsw-alias-label-primary);
1376
1441
  border: 1px solid var(--dsw-alias-border-l1); border-radius: 10px;
1377
1442
  box-shadow: 0 8px 28px rgba(0,0,0,.22);
@@ -1393,7 +1458,12 @@ window.__ModuleLoader__.load({
1393
1458
  }
1394
1459
  .clp-tab + .clp-tab { border-left: 1px solid var(--dsw-alias-border-l1); }
1395
1460
  .clp-tab-on { background: var(--dsw-alias-brand-primary); color: #fff; font-weight: 600; }
1396
- .clp-tab-body { display: flex; flex-direction: column; gap: 8px; }
1461
+ /* The popover shell never scrolls; the title and the tab bar stay pinned
1462
+ and only this body scrolls (min-height:0 lets the flex child shrink). */
1463
+ .clp-tab-body {
1464
+ display: flex; flex-direction: column; gap: 8px;
1465
+ overflow-y: auto; min-height: 0; overscroll-behavior: contain;
1466
+ }
1397
1467
  .clp-keys-head { display: flex; align-items: baseline; gap: 8px; padding-top: 2px; }
1398
1468
  .clp-keys-title { font-size: 13px; font-weight: 600; color: var(--dsw-alias-label-primary); }
1399
1469
  .clp-keys-scope { color: var(--dsw-alias-label-secondary); font-size: 11px; }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-composer-lists",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "type": "module",
5
5
  "description": "Ordered and unordered list editing in the DeepSeek Harness message composer: auto numbering and renumbering, marker-aware caret moves, level promotion, and an AI prompt-polish button.",
6
6
  "author": "wangjianghu",