dsh-edit-turn 0.3.1 → 0.3.2

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,13 @@ ln -s /path/to/dsh-install/node_modules ./node_modules
250
250
 
251
251
  ### 更新日志
252
252
 
253
+ **0.3.2** —— 修复编辑器在长回答或页面底部时位置错误、按钮点不到的问题。
254
+
255
+ - **视口钳制**:编辑框优先放在行下方;放不下就翻到行上方;行比屏幕高、底部在视口外时钉在视口底部,不再跟着行跑到屏幕外。
256
+ - **自身滚动**:编辑框有 `max-height` 和 `overflow-y:auto`,保存/取消所在的 footer 用 `position:sticky` 固定,长草稿也能滚到按钮。
257
+ - **宽度/左边界**也按视口钳制,窄窗口不会被推出屏幕。
258
+ - **测试**:`npm test` **213/213**、`npm run verify:contract` **96** 项、`npm run verify:client` **184** 项。
259
+
253
260
  **0.3.1** —— 修复 0.3.0 的显示与跨插件清理问题。
254
261
 
255
262
  - **编辑后看不到变化**:`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.2'
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}',
@@ -1945,13 +1945,51 @@ window.__ModuleLoader__.load({
1945
1945
  // Put the box under the row it belongs to: same width, just below its lower
1946
1946
  // edge. Called on render and whenever the page scrolls or resizes, since
1947
1947
  // the layer is fixed to the viewport.
1948
+ function viewportSize() {
1949
+ const win = typeof window === 'undefined' ? undefined : window
1950
+ const doc = typeof document === 'undefined' ? undefined : document
1951
+ const width = (win !== undefined && win.innerWidth) ||
1952
+ (doc !== undefined && doc.documentElement && doc.documentElement.clientWidth) || 0
1953
+ const height = (win !== undefined && win.innerHeight) ||
1954
+ (doc !== undefined && doc.documentElement && doc.documentElement.clientHeight) || 0
1955
+ return { width, height }
1956
+ }
1957
+
1958
+ // Put the box under the row it belongs to: same width, just below its lower
1959
+ // edge. The layer is fixed to the viewport, so a row near the bottom (or one
1960
+ // taller than the screen) used to push the footer - and its save button -
1961
+ // past the bottom edge where nothing could reach it. Flip above the row when
1962
+ // there is room, otherwise pin the box to the bottom of the viewport and let
1963
+ // its own scrollbar carry the content; the footer stays sticky either way.
1948
1964
  function placeEditor(row, box) {
1949
1965
  const layer = ensureEditorLayer()
1950
1966
  if (box.parentElement !== layer) layer.appendChild(box)
1951
1967
  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`
1968
+ const viewport = viewportSize()
1969
+ const margin = 8
1970
+ const width = viewport.width > 0
1971
+ ? Math.max(1, Math.min(Math.max(200, rect.width || 0), viewport.width - margin * 2))
1972
+ : Math.max(200, rect.width || 0)
1973
+ box.style.width = `${Math.round(width)}px`
1974
+ const measured = box.getBoundingClientRect()
1975
+ const height = (measured && measured.height) || box.offsetHeight || 0
1976
+ let top = rect.bottom + 6
1977
+ if (viewport.height > 0 && height > 0) {
1978
+ const maxTop = Math.max(margin, viewport.height - height - margin)
1979
+ if (top + height > viewport.height - margin) {
1980
+ const above = rect.top - height - 6
1981
+ top = above >= margin ? above : maxTop
1982
+ }
1983
+ // A row taller than the screen has its bottom above the viewport after
1984
+ // the user scrolls to its action bar; without this clamp the box follows
1985
+ // it off-screen and the buttons become unreachable again.
1986
+ top = Math.max(margin, Math.min(top, maxTop))
1987
+ }
1988
+ let left = rect.left
1989
+ if (viewport.width > 0 && left + width > viewport.width - margin) left = viewport.width - width - margin
1990
+ if (left < margin) left = margin
1991
+ box.style.left = `${Math.round(left)}px`
1992
+ box.style.top = `${Math.round(top)}px`
1955
1993
  }
1956
1994
 
1957
1995
  // Grow the textarea to its content, the way a normal input box behaves.
@@ -2297,8 +2335,16 @@ window.__ModuleLoader__.load({
2297
2335
  // "eats" pinyin.
2298
2336
  if (event.isComposing === true) return
2299
2337
  fitEditorHeight(area)
2338
+ const row = controller.editorRow
2339
+ const box = controller.editorBox
2340
+ if (row !== null && box !== null && box.isConnected === true) placeEditor(row, box)
2341
+ })
2342
+ area.addEventListener('compositionend', () => {
2343
+ fitEditorHeight(area)
2344
+ const row = controller.editorRow
2345
+ const box = controller.editorBox
2346
+ if (row !== null && box !== null && box.isConnected === true) placeEditor(row, box)
2300
2347
  })
2301
- area.addEventListener('compositionend', () => fitEditorHeight(area))
2302
2348
  area.addEventListener('focus', () => {
2303
2349
  controller.editorFocus = true
2304
2350
  })
@@ -2408,11 +2454,11 @@ window.__ModuleLoader__.load({
2408
2454
  footer.appendChild(submit)
2409
2455
  box.appendChild(footer)
2410
2456
 
2411
- placeEditor(row, box)
2412
2457
  controller.editorBox = box
2413
2458
  controller.editorRow = row
2414
2459
  controller.editorElement = area
2415
2460
  fitEditorHeight(area)
2461
+ placeEditor(row, box)
2416
2462
  if (!view.confirming && !view.pending && (fresh || wasFocused)) {
2417
2463
  try {
2418
2464
  area.focus()
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.2'
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.2",
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",