dsh-edit-turn 0.3.1 → 0.3.3

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.
package/README.md CHANGED
@@ -164,7 +164,7 @@ ctx.sessions.registerMessageProjection({
164
164
  | 运行中的实例是否真的挂载了本插件(只读路由守卫探针,无需 token) | `npm run probe:loaded [端口]` | 通过:`/state` 返 400、`/apply` 返 405 —— 这两个状态码只有本插件会返回 |
165
165
  | 浏览器半部到底问了什么、拿回了什么(只读诊断,含失败记录) | `GET /dsh-edit-turn/debug` | 通过:真实浏览器里确认客户端确实拿到 replies;并复现了旧 v3 会话的 session-not-found |
166
166
  | 官方 append **与读取**契约(真实校验器,进程内) | `npm run verify:contract` | 96 项通过(每段写入后都用加载器重读整份日志):替换事件被接受、派生历史真的收缩、日志 append-only、工具结果与调用同进同退、空 developer 载体不产生模型消息、**连续两次回退都被接受**、**编辑模型回答的完整机制被接受**、**回复编辑后的尾部重放(后续提问/回答/tool 调用与结果按新 turn 号回到修正回答之后,顺序正确、日志仍可加载)**、**跨插件依赖的不变量(单节点窗口 / 同事件类型 / 全窗口 sourceEventSeqs / 每个替换都带 `editedBy` 标记 / 改写后的 `source.kind` 不是 `user` / 修正回复不带 `sourceEventSeqs`)**,以及**就地补丁:一条 ignorable 记录、surface 不变、去掉投影后原文回来、后续轮次与 tool 结果不动、第二条记录可叠加** |
167
- | 纯逻辑 + 宿主集成 + 客户端 DOM 行为(真 HTTP、真校验器、桩服务、DOM 桩) | `npm test` | 210 项通过(含回复编辑的尾部重放用例:窗口/重放区间、新 turn 号与新 messageId、tool 调用与结果成对重映射、上一轮修正可再次编辑,以及与 `dsh-rerun-turn` builder 的逐字段对照;另含「文本没变 → 不写」的四条,以及块随消息一起改的 35 条:未动的块逐字节保留、删块少一块、文字位置不变、no-op 语义升级、附件服务缺失时降级且不写坏数据、自定义 `quote-card` 块的通用性、录用与拒绝、`/state` 的块描述与能力、附件字节路由、编辑器芯片/删除/添加/缩略图/降级警告) |
167
+ | 纯逻辑 + 宿主集成 + 客户端 DOM 行为(真 HTTP、真校验器、桩服务、DOM 桩) | `npm test` | 213 项通过(含回复编辑的尾部重放用例:窗口/重放区间、新 turn 号与新 messageId、tool 调用与结果成对重映射、上一轮修正可再次编辑,以及与 `dsh-rerun-turn` builder 的逐字段对照;另含「文本没变 → 不写」的四条,以及块随消息一起改的 35 条:未动的块逐字节保留、删块少一块、文字位置不变、no-op 语义升级、附件服务缺失时降级且不写坏数据、自定义 `quote-card` 块的通用性、录用与拒绝、`/state` 的块描述与能力、附件字节路由、编辑器芯片/删除/添加/缩略图/降级警告) |
168
168
  | 客户端半部静态检查(注册、i18n 完整性、样式、皮肤可读性、版本三处同步、线协议、块契约) | `npm run verify:client` | 184 项全部通过(含 12 条块契约检查:能力上报、服务现场探测、录用是唯一建块路径、录用先于 append、**两侧都不特判块类型**、块逐字节复制、文字归位、`dropped` 上报、只在无服务时警告、芯片靠 `error` 事件自证) |
169
169
  | 实机前端产物校验(运行中的 DSH 是否在下发当前代码) | `npm run verify:live -- --token-file ~/path/to/dsh.log` | 全部通过(含**宿主节点形状锚点**) |
170
170
  | **真实浏览器渲染冒烟**(CDP 驱动已开着的标签页,只读) | `npm run verify:ui` | 全部通过:下发的字节就是刚改的字节、无槽位崩溃留下的空占位、回复铅笔在平台动作条里、被回滚的行保住动作条、**被回滚的用户行一定显示替代文本(消息不会凭空消失)**、**替代气泡一定排在时间/复制之上(顺序与正常消息一致)**、**改写后那颗铅笔一定在还活着的动作条末尾(已在原始会话、切走的会话、切回后三处现场确认)**、**回复铅笔一定排在动作条最左(复制之前,已现场确认)**、气泡上没有 `.dshet-floating` 压着文字、切走再切回入口仍在 |
@@ -250,6 +250,19 @@ ln -s /path/to/dsh-install/node_modules ./node_modules
250
250
 
251
251
  ### 更新日志
252
252
 
253
+ **0.3.3** —— 编辑器锚定到刚按下的铅笔,位置不再跑到屏幕另一端。
254
+
255
+ - 回复的编辑按钮在 turn-tail 操作条里,而编辑器的锚点行可能是比屏幕还高的回答行;0.3.2 会把它钳到视口顶部,按钮可达但离点击处很远。现在把锚点记为按下的按钮本身:优先贴在按钮下方,放不下就贴在按钮上方,再按视口钳制。
256
+ - 长草稿仍由编辑器自身滚动、footer sticky,保存/取消始终可见。
257
+ - **测试**:`npm test` **213/213**、`npm run verify:contract` **96** 项、`npm run verify:client` **184** 项。
258
+
259
+ **0.3.2** —— 修复编辑器在长回答或页面底部时位置错误、按钮点不到的问题。
260
+
261
+ - **视口钳制**:编辑框优先放在行下方;放不下就翻到行上方;行比屏幕高、底部在视口外时钉在视口底部,不再跟着行跑到屏幕外。
262
+ - **自身滚动**:编辑框有 `max-height` 和 `overflow-y:auto`,保存/取消所在的 footer 用 `position:sticky` 固定,长草稿也能滚到按钮。
263
+ - **宽度/左边界**也按视口钳制,窄窗口不会被推出屏幕。
264
+ - **测试**:`npm test` **213/213**、`npm run verify:contract` **96** 项、`npm run verify:client` **184** 项。
265
+
253
266
  **0.3.1** —— 修复 0.3.0 的显示与跨插件清理问题。
254
267
 
255
268
  - **编辑后看不到变化**:`turn-process` 行和 `assistant-step` 行都覆盖同一个 seq,旧的 DOM 映射按文档顺序先拿到过程行,补丁落到没有正文的行上。现在优先 assistant 行;极短回答是裸文本节点时也有兜底。
package/lib/client.js CHANGED
@@ -42,7 +42,7 @@ window.__ModuleLoader__.load({
42
42
  const RERUN_PREFIX = '/dsh-rerun-turn'
43
43
 
44
44
  /** Keep in sync with package.json and lib/index.js. */
45
- const PLUGIN_VERSION = '0.3.1'
45
+ const PLUGIN_VERSION = '0.3.3'
46
46
 
47
47
  // --- copy -----------------------------------------------------------------
48
48
 
@@ -262,7 +262,7 @@ window.__ModuleLoader__.load({
262
262
  // positioned under its row by `placeEditor`.
263
263
  '.dshet-layer{position:fixed;left:0;top:0;right:0;bottom:0;pointer-events:none;z-index:40}',
264
264
  '.dshet-layer .dshet-editor{position:absolute;pointer-events:auto}',
265
- '.dshet-editor{margin:0;border:1px solid var(--dshet-line);border-radius:16px;background:var(--dshet-panel);backdrop-filter:blur(18px) saturate(1.2);box-shadow:var(--dshet-shadow);color:var(--dshet-ink);transition:border-color .12s}',
265
+ '.dshet-editor{display:flex;flex-direction:column;max-height:calc(100vh - 16px);overflow-y:auto;overscroll-behavior:contain;margin:0;border:1px solid var(--dshet-line);border-radius:16px;background:var(--dshet-panel);backdrop-filter:blur(18px) saturate(1.2);box-shadow:var(--dshet-shadow);color:var(--dshet-ink);transition:border-color .12s}',
266
266
  '.dshet-editor:focus-within{border-color:var(--dsw-alias-button-primary-fill,var(--dshet-accent))}',
267
267
  '.dshet-editor textarea{display:block;width:100%;min-height:47px;max-height:46vh;overflow-y:auto;resize:none;box-sizing:border-box;padding:11px 14px 0;border:0;background:transparent;color:inherit;font:inherit;font-size:var(--dsh-content-font-size,14px);line-height:calc(22px + var(--dsh-content-font-delta,0px));outline:none}',
268
268
  '.dshet-editor textarea::placeholder{color:var(--dshet-ink-dim)}',
@@ -290,7 +290,7 @@ window.__ModuleLoader__.load({
290
290
  '.dshet-note{color:var(--dshet-ink-dim)}',
291
291
  '.dshet-warn{color:var(--dsw-alias-state-warning-primary,#a2650a)}',
292
292
  '.dshet-error{color:var(--dsw-alias-state-error-primary,#c93a31)}',
293
- '.dshet-footer{display:flex;align-items:center;justify-content:flex-end;gap:8px;padding:4px 10px 10px}',
293
+ '.dshet-footer{position:sticky;bottom:0;z-index:1;display:flex;align-items:center;justify-content:flex-end;gap:8px;padding:4px 10px 10px;background:var(--dshet-panel);backdrop-filter:blur(18px) saturate(1.2)}',
294
294
  '.dshet-btn{padding:6px 16px;border:1px solid var(--dshet-line);border-radius:999px;background:var(--dshet-field);color:var(--dshet-ink);font:inherit;font-size:13px;line-height:20px;cursor:pointer;transition:background-color .12s,border-color .12s}',
295
295
  '.dshet-btn:hover:not(:disabled){background:var(--dshet-hover)}',
296
296
  '.dshet-btn:focus-visible{outline:2px solid var(--dsw-alias-button-primary-fill,var(--dshet-accent));outline-offset:2px}',
@@ -364,6 +364,7 @@ window.__ModuleLoader__.load({
364
364
  this.editorElement = null
365
365
  this.editorBox = null
366
366
  this.editorRow = null
367
+ this.editorAnchor = null
367
368
  this.editorFocus = false
368
369
  this.editorDraft = ''
369
370
  // The block list being edited: what the carrier will be rebuilt from.
@@ -563,6 +564,9 @@ window.__ModuleLoader__.load({
563
564
  }
564
565
  : target
565
566
  this.editor = normalized
567
+ this.editorAnchor = target.anchor !== undefined && target.anchor !== null && target.anchor.isConnected === true
568
+ ? target.anchor
569
+ : null
566
570
  this.editorBlocks = replyBlocks
567
571
  this.editorBlockIndex = chosen === undefined ? null : chosen.index
568
572
  this.editorBlockKind = chosen === undefined ? null : chosen.kind
@@ -644,6 +648,7 @@ window.__ModuleLoader__.load({
644
648
  this.editorElement = null
645
649
  this.editorBox = null
646
650
  this.editorRow = null
651
+ this.editorAnchor = null
647
652
  this.editorFocus = false
648
653
  this.editor = null
649
654
  this.editorDraft = ''
@@ -1197,9 +1202,9 @@ window.__ModuleLoader__.load({
1197
1202
  * at all and reads exactly like a dead pencil.
1198
1203
  */
1199
1204
  function wireAction(button) {
1200
- pressable(button, () => {
1205
+ pressable(button, (event) => {
1201
1206
  const activate = button[ACTIVATE]
1202
- if (typeof activate === 'function') activate()
1207
+ if (typeof activate === 'function') activate(event)
1203
1208
  })
1204
1209
  }
1205
1210
 
@@ -1815,7 +1820,14 @@ window.__ModuleLoader__.load({
1815
1820
  } else if (button.dataset !== undefined && button.dataset.dshetFallback !== undefined) {
1816
1821
  delete button.dataset.dshetFallback
1817
1822
  }
1818
- button[ACTIVATE] = () => controller.open(entry.target)
1823
+ button[ACTIVATE] = (event) => controller.open({
1824
+ ...entry.target,
1825
+ // The editor belongs beside the pencil the user just pressed, not at the
1826
+ // top of a row that may be taller than the screen.
1827
+ anchor: event !== undefined && event !== null && event.currentTarget !== undefined
1828
+ ? event.currentTarget
1829
+ : button,
1830
+ })
1819
1831
  liveHosts.add(host)
1820
1832
  if (button.getAttribute('aria-label') !== label) {
1821
1833
  button.setAttribute('aria-label', label)
@@ -1945,13 +1957,57 @@ window.__ModuleLoader__.load({
1945
1957
  // Put the box under the row it belongs to: same width, just below its lower
1946
1958
  // edge. Called on render and whenever the page scrolls or resizes, since
1947
1959
  // the layer is fixed to the viewport.
1948
- function placeEditor(row, box) {
1960
+ function viewportSize() {
1961
+ const win = typeof window === 'undefined' ? undefined : window
1962
+ const doc = typeof document === 'undefined' ? undefined : document
1963
+ const width = (win !== undefined && win.innerWidth) ||
1964
+ (doc !== undefined && doc.documentElement && doc.documentElement.clientWidth) || 0
1965
+ const height = (win !== undefined && win.innerHeight) ||
1966
+ (doc !== undefined && doc.documentElement && doc.documentElement.clientHeight) || 0
1967
+ return { width, height }
1968
+ }
1969
+
1970
+ // Put the box under the row it belongs to: same width, just below its lower
1971
+ // edge. The layer is fixed to the viewport, so a row near the bottom (or one
1972
+ // taller than the screen) used to push the footer - and its save button -
1973
+ // past the bottom edge where nothing could reach it. Flip above the row when
1974
+ // there is room, otherwise pin the box to the bottom of the viewport and let
1975
+ // its own scrollbar carry the content; the footer stays sticky either way.
1976
+ function placeEditor(row, box, anchor = null) {
1949
1977
  const layer = ensureEditorLayer()
1950
1978
  if (box.parentElement !== layer) layer.appendChild(box)
1951
1979
  const rect = row.getBoundingClientRect()
1952
- box.style.left = `${Math.round(rect.left)}px`
1953
- box.style.top = `${Math.round(rect.bottom + 6)}px`
1954
- box.style.width = `${Math.round(rect.width)}px`
1980
+ let anchorRect = null
1981
+ if (anchor !== null && anchor !== undefined && anchor.isConnected === true && typeof anchor.getBoundingClientRect === 'function') {
1982
+ const candidate = anchor.getBoundingClientRect()
1983
+ if (candidate.width > 0 && candidate.height > 0) anchorRect = candidate
1984
+ }
1985
+ const vertical = anchorRect === null ? rect : anchorRect
1986
+ const viewport = viewportSize()
1987
+ const margin = 8
1988
+ const width = viewport.width > 0
1989
+ ? Math.max(1, Math.min(Math.max(200, rect.width || 0), viewport.width - margin * 2))
1990
+ : Math.max(200, rect.width || 0)
1991
+ box.style.width = `${Math.round(width)}px`
1992
+ const measured = box.getBoundingClientRect()
1993
+ const height = (measured && measured.height) || box.offsetHeight || 0
1994
+ let top = vertical.bottom + 6
1995
+ if (viewport.height > 0 && height > 0) {
1996
+ const maxTop = Math.max(margin, viewport.height - height - margin)
1997
+ if (top + height > viewport.height - margin) {
1998
+ const above = vertical.top - height - 6
1999
+ top = above >= margin ? above : maxTop
2000
+ }
2001
+ // A row taller than the screen has its bottom above the viewport after
2002
+ // the user scrolls to its action bar; without this clamp the box follows
2003
+ // it off-screen and the buttons become unreachable again.
2004
+ top = Math.max(margin, Math.min(top, maxTop))
2005
+ }
2006
+ let left = rect.left
2007
+ if (viewport.width > 0 && left + width > viewport.width - margin) left = viewport.width - width - margin
2008
+ if (left < margin) left = margin
2009
+ box.style.left = `${Math.round(left)}px`
2010
+ box.style.top = `${Math.round(top)}px`
1955
2011
  }
1956
2012
 
1957
2013
  // Grow the textarea to its content, the way a normal input box behaves.
@@ -1981,7 +2037,7 @@ window.__ModuleLoader__.load({
1981
2037
  pressed = true
1982
2038
  event.preventDefault()
1983
2039
  event.stopPropagation()
1984
- action()
2040
+ action(event)
1985
2041
  }
1986
2042
  button.onclick = (event) => {
1987
2043
  event.preventDefault()
@@ -1990,7 +2046,7 @@ window.__ModuleLoader__.load({
1990
2046
  pressed = false
1991
2047
  return
1992
2048
  }
1993
- action()
2049
+ action(event)
1994
2050
  }
1995
2051
  }
1996
2052
 
@@ -2232,7 +2288,7 @@ window.__ModuleLoader__.load({
2232
2288
  ? controller.editorBox
2233
2289
  : null
2234
2290
  if (existing && existing.dataset.dshetFor === marker) {
2235
- placeEditor(row, existing)
2291
+ placeEditor(row, existing, controller.editorAnchor)
2236
2292
  return
2237
2293
  }
2238
2294
 
@@ -2297,8 +2353,16 @@ window.__ModuleLoader__.load({
2297
2353
  // "eats" pinyin.
2298
2354
  if (event.isComposing === true) return
2299
2355
  fitEditorHeight(area)
2356
+ const row = controller.editorRow
2357
+ const box = controller.editorBox
2358
+ if (row !== null && box !== null && box.isConnected === true) placeEditor(row, box, controller.editorAnchor)
2359
+ })
2360
+ area.addEventListener('compositionend', () => {
2361
+ fitEditorHeight(area)
2362
+ const row = controller.editorRow
2363
+ const box = controller.editorBox
2364
+ if (row !== null && box !== null && box.isConnected === true) placeEditor(row, box, controller.editorAnchor)
2300
2365
  })
2301
- area.addEventListener('compositionend', () => fitEditorHeight(area))
2302
2366
  area.addEventListener('focus', () => {
2303
2367
  controller.editorFocus = true
2304
2368
  })
@@ -2408,11 +2472,11 @@ window.__ModuleLoader__.load({
2408
2472
  footer.appendChild(submit)
2409
2473
  box.appendChild(footer)
2410
2474
 
2411
- placeEditor(row, box)
2412
2475
  controller.editorBox = box
2413
2476
  controller.editorRow = row
2414
2477
  controller.editorElement = area
2415
2478
  fitEditorHeight(area)
2479
+ placeEditor(row, box, controller.editorAnchor)
2416
2480
  if (!view.confirming && !view.pending && (fresh || wasFocused)) {
2417
2481
  try {
2418
2482
  area.focus()
@@ -2886,6 +2950,7 @@ window.__ModuleLoader__.load({
2886
2950
  event.preventDefault()
2887
2951
  event.stopPropagation()
2888
2952
  controller.open({
2953
+ anchor: event.currentTarget,
2889
2954
  seq: entry.seq,
2890
2955
  mode: 'reply',
2891
2956
  turn: entry.turn,
@@ -2939,7 +3004,7 @@ window.__ModuleLoader__.load({
2939
3004
  const keepPlaced = () => {
2940
3005
  const row = controller.editorRow
2941
3006
  const box = controller.editorBox
2942
- if (row !== null && box !== null && box.isConnected === true) placeEditor(row, box)
3007
+ if (row !== null && box !== null && box.isConnected === true) placeEditor(row, box, controller.editorAnchor)
2943
3008
  }
2944
3009
  document.addEventListener('pointerdown', onPointerDown, true)
2945
3010
  // Scroll events do not bubble; capture reaches the inner scroller too.
package/lib/index.js CHANGED
@@ -80,7 +80,7 @@ export const PLUGIN_ID = 'dsh-edit-turn'
80
80
  export const name = PLUGIN_ID
81
81
 
82
82
  /** Keep in sync with package.json and lib/client.js. */
83
- export const PLUGIN_VERSION = '0.3.1'
83
+ export const PLUGIN_VERSION = '0.3.3'
84
84
 
85
85
  // A ring of the last few requests, answering "did the browser half even ask, and
86
86
  // what did it get?" without needing its console. Read-only diagnostics: the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-edit-turn",
3
- "version": "0.3.1",
3
+ "version": "0.3.3",
4
4
  "description": "Edit any message in DeepSeek Harness in place: a prompt is replaced where it stands; a model reply block (answer text or reasoning) is patched through a plugin record and a message projection, so the turn stays where it is, later turns are untouched and the log grows by one small record. Falls back to the replay splice on hosts without the projection API. The append-only session log is never rewritten.",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",