dsh-edit-turn 0.5.10 → 0.5.12

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
@@ -277,6 +277,20 @@ ln -s /path/to/dsh-install/node_modules ./node_modules
277
277
 
278
278
  ### 更新日志
279
279
 
280
+ **0.5.12** —— `/state` 只折一次表面;临时诊断撤掉。
281
+
282
+ - **复制已确认修好**(根因是 `/state` 太大太慢:25 MB / 13 秒,那段时间里浏览器半部没有任何编辑条目,按下复制不会被接管)。所以 0.5.10 的自报诊断按计划撤掉。
283
+
284
+ - **再提速**:`rollbackLedger` 自己又折了一遍表面,而 `stateOf` 已经折过一次 —— 31k 事件的会话等于把整份日志走了两遍,还各自重建了 `bySeq`、`turnIndex`。现在折一次、索引复用,四个遍历共用同一份。实测同一份日志:`editableReplies` 的轻量路径 **2,227 ms → 29 ms**(0.5.11),这一版再省掉重复的那一遍折叠。
285
+
286
+ - **缓存试过又撤了**:按"会话 + 事件数 + 配置"缓存 state 能把重复打开变成瞬时,但测试里暴露出陈旧风险(状态快照没跟上新事件时,键不变、结果却是旧的)。宁可慢一点,也不要给编辑器一份过期的状态。这条以后要做得从宿主的 seq 严格判定失效。
287
+
288
+ **0.5.11** —— `/state` 的回复列表加轻量路径:不再为每条回复构建分块。
289
+
290
+ - 0.5.9 把回复全文从 `/state` 拿掉了,但服务端**仍然为每条回复构建了全部分块**(还要为每条回复遍历它那一整轮的所有事件做合并)。实测一个会话 290 条回复:**2,227 ms、22.4 MB 的中间数据** —— 而 `/state` 只需要身份。
291
+
292
+ - 新增 `light` 选项:跳过分块构建与"整轮合并",只保留判定所需的字段。同一份日志:**29 ms、1.1 MB**(77 倍)。`/reply`(按需取)仍走完整路径。
293
+
280
294
  **0.5.10** —— 临时诊断:复制没被接管时,把原因写进标签页标题。
281
295
 
282
296
  - 这台机器上没有可用的浏览器(浏览器工具找不到 Chrome),拿不到真实 DOM,而两次按代码推断的修法都没命中。所以加一条**自报**:如果这次复制按下没被接管,标签页标题会变成 `[复制诊断] <原因>`(例如「这一行没登记文本(登记 3 行)」「按钮不在行内」「写入没匹配上」);接管成功时标题自动恢复。不污染剪贴板、不改页面内容。
package/lib/client.js CHANGED
@@ -45,7 +45,7 @@ window.__ModuleLoader__.load({
45
45
  const RERUN_PREFIX = '/dsh-rerun-turn'
46
46
 
47
47
  /** Keep in sync with package.json and lib/index.js. */
48
- const PLUGIN_VERSION = '0.5.10'
48
+ const PLUGIN_VERSION = '0.5.12'
49
49
 
50
50
  // --- copy -----------------------------------------------------------------
51
51
 
@@ -2001,24 +2001,6 @@ window.__ModuleLoader__.load({
2001
2001
  })()
2002
2002
  const rowCopyText = copyRegistry.text
2003
2003
 
2004
- // TEMP DIAG (删除时机:复制修好后). 复制按下没被接管时,把原因写进标签页标题:
2005
- // 不污染剪贴板、不改页面内容,用户念一眼标题即可;成功接管时自动恢复原标题。
2006
- const copyDiag = (page) => {
2007
- try {
2008
- if (page.__dshetTitle === undefined) page.__dshetTitle = document.title
2009
- const base = String(page.__dshetTitle ?? '')
2010
- const show = (reason) => {
2011
- document.title = `[复制诊断] ${reason}${base === '' ? '' : ' · ' + base}`
2012
- }
2013
- const clear = () => {
2014
- if (String(document.title).startsWith('[复制诊断]')) document.title = base
2015
- }
2016
- return { show, clear }
2017
- } catch {
2018
- return { show: () => {}, clear: () => {} }
2019
- }
2020
- }
2021
- const titleDiag = typeof globalThis !== 'undefined' ? copyDiag(globalThis) : { show: () => {}, clear: () => {} }
2022
2004
  const rowHostText = copyRegistry.host
2023
2005
 
2024
2006
  /**
@@ -2150,11 +2132,6 @@ window.__ModuleLoader__.load({
2150
2132
  const native = clipboard.writeText.bind(clipboard)
2151
2133
  const wrapped = (value) => {
2152
2134
  const rewritten = rewriteCopiedText(String(value ?? ''))
2153
- if (rewritten === null && rowCopyText.size > 0) {
2154
- titleDiag.show(`写入没匹配上(登记 ${rowCopyText.size} 行,长度 ${String(value ?? '').length})`)
2155
- } else if (rewritten !== null) {
2156
- titleDiag.clear()
2157
- }
2158
2135
  return rewritten === null ? native(value) : native(rewritten)
2159
2136
  }
2160
2137
  wrapped[CLIPBOARD_WRAPPED] = true
@@ -4995,15 +4972,11 @@ window.__ModuleLoader__.load({
4995
4972
  // Our own buttons sit in the same bar: theirs is not this press.
4996
4973
  if (pluginOwned(button)) return
4997
4974
  const row = button.closest('[data-chat-flow-key]')
4998
- if (row === null) { titleDiag.show('按钮不在行内'); return }
4975
+ if (row === null) return
4999
4976
  const key = row.getAttribute('data-chat-flow-key')
5000
- if (typeof key !== 'string' || key === '') { titleDiag.show('行没有 key'); return }
4977
+ if (typeof key !== 'string' || key === '') return
5001
4978
  const text = rowCopyText.get(key)
5002
- if (typeof text !== 'string') {
5003
- titleDiag.show(`这一行没登记文本(登记 ${rowCopyText.size} 行)`)
5004
- return
5005
- }
5006
- titleDiag.clear()
4979
+ if (typeof text !== 'string') return
5007
4980
  const write = primitives !== null && primitives !== undefined && typeof primitives.writeClipboard === 'function'
5008
4981
  ? primitives.writeClipboard
5009
4982
  : null
package/lib/index.js CHANGED
@@ -78,7 +78,7 @@ export const PLUGIN_ID = 'dsh-edit-turn'
78
78
  export const name = PLUGIN_ID
79
79
 
80
80
  /** Keep in sync with package.json and lib/client.js. */
81
- export const PLUGIN_VERSION = '0.5.10'
81
+ export const PLUGIN_VERSION = '0.5.12'
82
82
 
83
83
  // A ring of the last few requests, answering "did the browser half even ask, and
84
84
  // what did it get?" without needing its console. Read-only diagnostics: the
@@ -699,10 +699,13 @@ function copyBlock(block) {
699
699
  * @returns `{ hidden, edits }`: one hidden entry per shadowed seq, and one
700
700
  * record per landed rollback.
701
701
  */
702
- export function rollbackLedger(events) {
703
- const folded = foldSurface(events)
704
- const bySeq = new Map(events.map((event) => [event.seq, event]))
705
- const turnOf = turnIndex(events)
702
+ export function rollbackLedger(events, options = {}) {
703
+ // A caller that already folded the surface (the state route does) hands it in:
704
+ // folding twice over a long session is the single most expensive thing this
705
+ // route did - 31k events, and every fold walks all of them.
706
+ const folded = options.folded ?? foldSurface(events)
707
+ const bySeq = options.bySeq ?? new Map(events.map((event) => [event.seq, event]))
708
+ const turnOf = options.turnOf ?? turnIndex(events)
706
709
  const hidden = []
707
710
  const edits = []
708
711
  for (const replacement of folded.replacements) {
@@ -1244,8 +1247,8 @@ export function buildReplayWrites(events, plan, baseTurn, replayId, startSeq, pr
1244
1247
  * @returns one entry per editable prompt, in conversation order.
1245
1248
  */
1246
1249
  export function editableTurns(events, surfaceNodes, options = {}) {
1247
- const eventBySeq = new Map(events.map((event) => [event.seq, event]))
1248
- const turnOf = turnIndex(events)
1250
+ const eventBySeq = options.bySeq ?? new Map(events.map((event) => [event.seq, event]))
1251
+ const turnOf = options.turnOf ?? turnIndex(events)
1249
1252
  const head = surfaceNodes[0]
1250
1253
  const out = []
1251
1254
  // Surface order, not log order: a prompt rewritten in place travels as a
@@ -1301,10 +1304,15 @@ export function editableTurns(events, surfaceNodes, options = {}) {
1301
1304
  */
1302
1305
  export function editableReplies(events, surfaceNodes, options = {}) {
1303
1306
  const nodeIndex = new Map(surfaceNodes.map((seq, index) => [seq, index]))
1304
- const turnOf = turnIndex(events)
1307
+ const turnOf = options.turnOf ?? turnIndex(events)
1305
1308
  const head = surfaceNodes[0]
1306
1309
  const patches = options.patches instanceof Map ? options.patches : new Map()
1307
1310
  const patch = options.patch === true
1311
+ // `/state` publishes identities only, so the blocks never leave this function.
1312
+ // Skipping them also skips the per-turn widening below, which walks every reply
1313
+ // of the same turn for every reply: that widening is what made one refresh of a
1314
+ // long session take seconds.
1315
+ const light = options.light === true
1308
1316
  const out = []
1309
1317
  for (const event of events) {
1310
1318
  if (!isAssistantReply(event)) continue
@@ -1324,7 +1332,7 @@ export function editableReplies(events, surfaceNodes, options = {}) {
1324
1332
  // text of that turn is offered, each annotated with the seq it lives in.
1325
1333
  let listed = blocks.map((block) => ({ ...block, seq: event.seq }))
1326
1334
  const carriesAnswer = blocks.some((block) => block.kind === 'text' && block.text.trim() !== '')
1327
- if (patch && carriesAnswer && typeof turn === 'number') {
1335
+ if (!light && patch && carriesAnswer && typeof turn === 'number') {
1328
1336
  const acrossTurn = []
1329
1337
  for (const other of events) {
1330
1338
  if (!isAssistantReply(other) || turnOf.get(other.seq) !== turn) continue
@@ -1338,6 +1346,7 @@ export function editableReplies(events, surfaceNodes, options = {}) {
1338
1346
  if (acrossTurn.length > listed.length) listed = acrossTurn
1339
1347
  }
1340
1348
  out.push({
1349
+ ...(light ? {} : { blocks: listed }),
1341
1350
  seq: event.seq,
1342
1351
  turn: typeof turn === 'number' ? turn : null,
1343
1352
  messageId: messageIdOf(event) ?? null,
@@ -1763,7 +1772,9 @@ async function stateOf(ctx, sessionId, config) {
1763
1772
  }
1764
1773
  }
1765
1774
  const folded = foldSurface(events)
1766
- const ledger = rollbackLedger(events)
1775
+ const bySeq = new Map(events.map((event) => [event.seq, event]))
1776
+ const turnOf = turnIndex(events)
1777
+ const ledger = rollbackLedger(events, { folded, bySeq, turnOf })
1767
1778
  const patches = patchesOf(events)
1768
1779
  // A patch whose target has been retired (a later rerun shadowed it) is inert:
1769
1780
  // the projection skips it and no row can show it. Reporting it in /state made
@@ -1777,8 +1788,8 @@ async function stateOf(ctx, sessionId, config) {
1777
1788
  // now, so a store that is unmounted (or not yet mounted) cannot leave the
1778
1789
  // editor promising to keep blocks the host would have to drop.
1779
1790
  const store = resolveAttachmentStore(ctx)
1780
- const turns = editableTurns(events, folded.nodes, { preview: canPreviewAttachments(store) })
1781
- const replies = editableReplies(events, folded.nodes, { patches, patch: config.patch === true }).map(lightReply)
1791
+ const turns = editableTurns(events, folded.nodes, { preview: canPreviewAttachments(store), bySeq, turnOf })
1792
+ const replies = editableReplies(events, folded.nodes, { patches, patch: config.patch === true, light: true, bySeq, turnOf }).map(lightReply)
1782
1793
  noteRequest({
1783
1794
  kind: 'state',
1784
1795
  sessionId,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-edit-turn",
3
- "version": "0.5.10",
3
+ "version": "0.5.12",
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",
@@ -45,14 +45,17 @@
45
45
  "verify:ui": "node tools/verify-live-ui.mjs"
46
46
  },
47
47
  "dependencies": {
48
+ "@deepseek-ai/cordis": "^4.0.5",
49
+ "@deepseek-ai/dsh-scope": "^0.2.1-rc.5",
48
50
  "@deepseek-ai/schemastery": "^3.18.1"
49
51
  },
50
52
  "devDependencies": {
51
- "@deepseek-ai/dsh-session": "^0.2.1-rc.2"
53
+ "@deepseek-ai/dsh-session": "^0.2.1-rc.2",
54
+ "@deepseek-ai/dsh-session-format-catalog": "^0.2.1-rc.5"
52
55
  },
53
56
  "peerDependencies": {
54
- "@deepseek-ai/dsh-settings": "^0.1.0-rc.7 || ^0.1.1-rc.2 || ^0.1.2-alpha.2 || ^0.1.3-alpha.2 || ^0.1.5-alpha.1 || ^0.2.0-rc.1",
55
- "@deepseek-ai/dsh-session": "^0.2.0-rc.1 || ^0.2.1-rc.2"
57
+ "@deepseek-ai/dsh-session": "^0.2.0-rc.1 || ^0.2.1-rc.2",
58
+ "@deepseek-ai/dsh-settings": "^0.1.0-rc.7 || ^0.1.1-rc.2 || ^0.1.2-alpha.2 || ^0.1.3-alpha.2 || ^0.1.5-alpha.1 || ^0.2.0-rc.1"
56
59
  },
57
60
  "dsh": {
58
61
  "bundle": {