dsh-as-aistudio 0.3.0 → 0.4.0

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/CHANGELOG.md CHANGED
@@ -4,6 +4,15 @@
4
4
 
5
5
  ## 中文
6
6
 
7
+ **0.4.0** —— 少要一个平台包;四个组件跟进到最新。
8
+
9
+ - **不再要求 `dsh-tools`**:本包内置的宿主半部以前要 `@deepseek-ai/dsh-tools`,唯一原因是 `dsh-edit-turn` 向模型暴露了一个工具(`edit_turn_targets`)。那个工具已经删掉——编辑是靠 UI 面板做的,模型不需要知道这件事——于是整族插件都不再声明模型可见的工具,宿主半部的解析只需要 `dsh-session`。少一个平台包,就少一处跟宿主版本绑定的地方。
10
+
11
+ - **组件跟进**:`dsh-edit-turn` **0.5.6**、`dsh-rerun-turn` **0.2.6**;另两个不变(`dsh-delete-turn` 0.1.15、`dsh-markdown-bubble` 0.1.5)。
12
+
13
+ - `dsh-edit-turn` 0.5.5 把模型工具拿掉;0.5.6 给会话日志裁剪工具加了"先验后写"(那个工具在 `tools/` 下、不随 npm 包发布,本包也不含它,只是版本号跟着走)。
14
+ - `dsh-rerun-turn` 0.2.6 修 `sourceEventSeqs`:写入前摊平嵌套数组、去重、排序,并且**只接受真正的数字**——之前写出的形状(嵌套数组、缺项、倒序区间)会被宿主的校验器整条拒收,**整份会话日志都加载不了**。这是实测出来的:一个 29,497 事件的会话里有三条这样的记录,宿主三层校验全拒。
15
+
7
16
  **0.3.0** —— 设置页搬到插件行自己的配置箭头;组件设置卡片直接复用组件自己的那一个。
8
17
 
9
18
  - **设置页换了位置**:以前是「设置 → 插件 → AI Studio」里多一个标签页,和别的插件都不一样。现在注册到 `plugins.row.config`,键是 `dsh-as-aistudio#dsh-as-aistudio`(`<包名>#<行 id>` 的约定),于是**本包那一行**右侧多了配置箭头 —— 和这个 profile 里其它插件完全一致。点进去是整页:四个组件的实时状态,加下面的「组件设置」。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-as-aistudio",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Google-AI-Studio-style turn controls for DeepSeek Harness, in one install: edit a prompt and re-run it, re-run a reply in place, delete a turn from the model context, and render sent prompts as Markdown. Self-contained: the plugin list shows this one row, and the four components are vendored inside it - installing the studio installs nothing else.",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
package/src/client.js CHANGED
@@ -43,7 +43,7 @@ window.__ModuleLoader__.load({
43
43
  const RERUN_PREFIX = '/dsh-rerun-turn'
44
44
 
45
45
  /** Keep in sync with package.json and lib/index.js. */
46
- const PLUGIN_VERSION = '0.5.4'
46
+ const PLUGIN_VERSION = '0.5.6'
47
47
 
48
48
  // --- copy -----------------------------------------------------------------
49
49
 
@@ -58,6 +58,8 @@ window.__ModuleLoader__.load({
58
58
  'copy': '复制',
59
59
  'copied': '复制成功',
60
60
  'action.copied': '复制成功',
61
+ 'action.copy': '复制这条回答',
62
+ 'action.tail-actions': '这一轮的动作',
61
63
  'editor.title': '编辑这条消息',
62
64
  'editor.title-reply': '编辑这条回答',
63
65
  'editor.placeholder': '改成你希望自己当时说的内容。保存后就地替换这条消息,后面的对话保持不变。',
@@ -145,6 +147,8 @@ window.__ModuleLoader__.load({
145
147
  'copy': 'Copy',
146
148
  'copied': 'Copied',
147
149
  'action.copied': 'Copied',
150
+ 'action.copy': 'Copy this reply',
151
+ 'action.tail-actions': 'Actions for this turn',
148
152
  'editor.title': 'Edit this message',
149
153
  'editor.title-reply': 'Edit this reply',
150
154
  'editor.placeholder': 'Write what you want to have said. Saving replaces this message in place; the conversation after it stays as it is.',
@@ -422,6 +426,18 @@ window.__ModuleLoader__.load({
422
426
  // variable resolves on the action bar (and everywhere above it); the
423
427
  // fallback is the value it resolves to in the default theme.
424
428
  '.dshet-action{width:28px;height:28px;padding:6px;display:inline-flex;align-items:center;justify-content:center;border:none;border-radius:28px;background:transparent;color:var(--dsw-alias-label-tertiary,#81858c);cursor:pointer;transition:background-color .12s,color .12s}',
429
+ // The fallback strip for a turn with no closing body.
430
+ //
431
+ // The gate is the host's OWN marker: TurnTailNodeView renders
432
+ // `data-actions-reveal` on the tail container exactly on the branch that
433
+ // also draws the official action row, and never on the branch that has
434
+ // none. So one rule hides this strip wherever the official one exists -
435
+ // the two can never be drawn together, and no observer is needed.
436
+ '.dshet-tail{display:inline-flex;align-items:center;gap:2px;margin-left:4px}',
437
+ '[data-turn-tail][data-actions-reveal] .dshet-tail{display:none}',
438
+ '.dshet-tail[data-reveal=always]{opacity:1}',
439
+ '@media (hover:hover){.dshet-tail[data-reveal=hover]{opacity:0;transition:opacity 80ms}[data-turn-tail]:hover .dshet-tail[data-reveal=hover],[data-turn-tail]:focus-within .dshet-tail[data-reveal=hover]{opacity:1}}',
440
+ '.dshet-tail-action[data-copied=1]{color:var(--dshet-accent,#4d6bfe)}',
425
441
  '.dshet-action:hover:not(:disabled){background:var(--dshet-chip);box-shadow:inset 0 0 0 1px var(--dshet-line);color:var(--dsw-alias-label-primary,var(--dshet-ink))}',
426
442
  '.dshet-action:focus-visible{outline:2px solid var(--dsw-alias-button-primary-fill,var(--dshet-accent));outline-offset:2px}',
427
443
  '.dshet-action:disabled{cursor:default;opacity:.4}',
@@ -577,12 +593,18 @@ window.__ModuleLoader__.load({
577
593
  ].join('')
578
594
 
579
595
  const TAG_ID = 'dsh-edit-turn/edit-turn.css'
580
- if (typeof document !== 'undefined' && document.querySelector('style[data-plugin-css=' + JSON.stringify(TAG_ID) + ']') === null) {
581
- const tag = document.createElement('style')
582
- tag.dataset.plugin = NS
583
- tag.dataset.pluginCss = TAG_ID
584
- tag.textContent = CSS
585
- document.head.appendChild(tag)
596
+ if (typeof document !== 'undefined') {
597
+ // Reused, not skipped: an upgrade over a live page finds the tag the
598
+ // PREVIOUS build injected, and leaving it alone kept the new rules out of
599
+ // the page entirely.
600
+ let tag = document.querySelector('style[data-plugin-css=' + JSON.stringify(TAG_ID) + ']')
601
+ if (tag === null) {
602
+ tag = document.createElement('style')
603
+ tag.dataset.plugin = NS
604
+ tag.dataset.pluginCss = TAG_ID
605
+ document.head.appendChild(tag)
606
+ }
607
+ if (tag.textContent !== CSS) tag.textContent = CSS
586
608
  }
587
609
 
588
610
  // --- icon -----------------------------------------------------------------
@@ -3695,9 +3717,12 @@ window.__ModuleLoader__.load({
3695
3717
  error.className = 'dshet-error'
3696
3718
  error.setAttribute('role', 'status')
3697
3719
  const detail = typeof view.failureDetail === 'string' && view.failureDetail !== '' ? view.failureDetail : null
3698
- error.textContent = (failureCode === 'internal' || failureCode === 'generic') && detail !== null
3699
- ? `${errorText(t, failureCode)}(${detail.slice(0, 300)})`
3700
- : errorText(t, failureCode)
3720
+ // The host's own sentence is always shown: a code-only message leaves the
3721
+ // reader with nothing to act on, and it is what made a real 400 impossible
3722
+ // to diagnose.
3723
+ error.textContent = detail === null
3724
+ ? errorText(t, failureCode)
3725
+ : `${errorText(t, failureCode)}(${detail.slice(0, 300)})`
3701
3726
  box.appendChild(error)
3702
3727
  }
3703
3728
 
@@ -4531,6 +4556,211 @@ window.__ModuleLoader__.load({
4531
4556
  })
4532
4557
  }
4533
4558
 
4559
+ // --- the fallback strip ---------------------------------------------------
4560
+ //
4561
+ // TurnTailNodeView, when a turn's model reply has no closing body
4562
+ // (`closing === null`), returns the turnTail slot alone and never renders
4563
+ // `conversation.chat.assistant-actions`. Every action on that row goes with
4564
+ // it - this plugin's pencil, and the platform's own copy, branch, feedback
4565
+ // and usage buttons too. Nothing a plugin registers inside a strip that is
4566
+ // not rendered can bring those back, so the fallback lives in the one slot
4567
+ // that branch DOES render.
4568
+ //
4569
+ // It is not a second bar for every turn: the host puts `data-actions-reveal`
4570
+ // on the tail container only on the branch that draws its own strip, and one
4571
+ // CSS rule hides this one wherever that attribute is present.
4572
+
4573
+ /**
4574
+ * The reply this turn's fallback strip acts on.
4575
+ *
4576
+ * A turn runs several steps and the strip belongs at the end of the turn,
4577
+ * so the LAST rewriteable reply of that turn wins - the same row the host
4578
+ * would have hung its own strip on.
4579
+ *
4580
+ * @param view - the controller snapshot (see readReplyView).
4581
+ * @param turn - the TurnLocation the slot hands over.
4582
+ * @returns the reply entry, or undefined when this turn has none.
4583
+ */
4584
+ function replyForTurn(view, turn) {
4585
+ if (view === null || view === undefined || typeof turn !== 'number') return undefined
4586
+ const replies = view.replies
4587
+ if (replies === null || replies === undefined || typeof replies.values !== 'function') return undefined
4588
+ if (view.hidden === null || view.hidden === undefined || typeof view.hidden.has !== 'function') return undefined
4589
+ let best
4590
+ for (const reply of replies.values()) {
4591
+ if (reply === null || typeof reply !== 'object') continue
4592
+ if (typeof reply.seq !== 'number' || reply.turn !== turn) continue
4593
+ if (view.hidden.has(reply.seq)) continue
4594
+ if (best === undefined || reply.seq > best.seq) best = reply
4595
+ }
4596
+ return best
4597
+ }
4598
+
4599
+ /**
4600
+ * What a copy press on the fallback strip writes.
4601
+ *
4602
+ * The model's own answer when the turn has one, and its reasoning when the
4603
+ * whole reply went into thinking - the case this strip exists for. Blocks
4604
+ * are joined the way they read, blank ones dropped so a trailing empty
4605
+ * block does not become a trailing newline.
4606
+ *
4607
+ * @param entry - one reply entry.
4608
+ * @returns the text to copy; empty when there is nothing readable.
4609
+ */
4610
+ function replyCopyText(entry) {
4611
+ const blocks = entry !== null && entry !== undefined && Array.isArray(entry.blocks) ? entry.blocks : []
4612
+ const gather = (kind) =>
4613
+ blocks
4614
+ .filter((block) => block !== null && typeof block === 'object' && block.kind === kind)
4615
+ .map((block) => (typeof block.text === 'string' ? block.text : ''))
4616
+ .filter((text) => text.trim() !== '')
4617
+ .join('\n\n')
4618
+ const answer = gather('text')
4619
+ return answer !== '' ? answer : gather('reasoning')
4620
+ }
4621
+
4622
+ function CopyIcon() {
4623
+ return jsx('svg', {
4624
+ width: 16,
4625
+ height: 16,
4626
+ viewBox: '0 0 16 16',
4627
+ fill: 'none',
4628
+ 'aria-hidden': true,
4629
+ children: [
4630
+ jsx('rect', { x: 5.2, y: 5.2, width: 8, height: 8, rx: 1.6, stroke: 'currentColor', strokeWidth: 1.2 }, 0),
4631
+ jsx('path', { d: 'M10.8 5.2V3.6A1.6 1.6 0 0 0 9.2 2H3.6A1.6 1.6 0 0 0 2 3.6v5.6a1.6 1.6 0 0 0 1.6 1.6h1.6', stroke: 'currentColor', strokeWidth: 1.2, strokeLinecap: 'round' }, 1),
4632
+ ],
4633
+ })
4634
+ }
4635
+
4636
+ function CheckIcon() {
4637
+ return jsx('svg', {
4638
+ width: 16,
4639
+ height: 16,
4640
+ viewBox: '0 0 16 16',
4641
+ fill: 'none',
4642
+ 'aria-hidden': true,
4643
+ children: jsx('path', { d: 'M3.2 8.4l3.2 3.2 6.4-6.4', stroke: 'currentColor', strokeWidth: 1.4, strokeLinecap: 'round', strokeLinejoin: 'round' }, 0),
4644
+ })
4645
+ }
4646
+
4647
+ /**
4648
+ * The fallback strip for a turn the host draws no action row for.
4649
+ *
4650
+ * @param props - the slot's owner props ({ turn, seq, openFile }) plus the
4651
+ * injected controller, the standard `useChat` and this plugin's `t`.
4652
+ */
4653
+ function TailActionEntry(input) {
4654
+ const props = input !== null && typeof input === 'object' ? input : {}
4655
+ const turn = props.turn
4656
+ const controller = props.controller
4657
+ const t = typeof props.t === 'function' ? props.t : null
4658
+ const useChat = props.useChat
4659
+ const [view, setView] = react.useState(() => {
4660
+ try {
4661
+ return readReplyView(controller)
4662
+ } catch {
4663
+ return null
4664
+ }
4665
+ })
4666
+ const [copied, setCopied] = react.useState(false)
4667
+ react.useEffect(() => {
4668
+ let unsubscribe = null
4669
+ try {
4670
+ setView(readReplyView(controller))
4671
+ if (controller !== null && controller !== undefined && typeof controller.subscribe === 'function') {
4672
+ unsubscribe = controller.subscribe(() => setView(readReplyView(controller)))
4673
+ }
4674
+ } catch {
4675
+ setView(null)
4676
+ }
4677
+ return () => {
4678
+ if (typeof unsubscribe === 'function') unsubscribe()
4679
+ }
4680
+ }, [controller])
4681
+ // The host reveals the latest turn's row always and older ones on hover.
4682
+ // The same rule, from the same source it uses (the last turn in the
4683
+ // timeline), so the strip does not behave differently from the row it
4684
+ // stands in for.
4685
+ const lastTurn = typeof useChat === 'function'
4686
+ ? useChat((state) => {
4687
+ if (state === null || typeof state !== 'object') return undefined
4688
+ const timeline = state.timeline
4689
+ const order = timeline === null || timeline === undefined ? null : timeline.turnOrder
4690
+ if (!Array.isArray(order) || order.length === 0) return undefined
4691
+ return order[order.length - 1]
4692
+ })
4693
+ : undefined
4694
+ const entry = replyForTurn(view, turn)
4695
+ // Nothing this plugin can act on: the strip stays out of the way rather
4696
+ // than drawing an empty row.
4697
+ if (entry === undefined) return null
4698
+ const label = t === null ? '' : t('action.edit-reply')
4699
+ const copyLabel = t === null ? '' : t(copied ? 'action.copied' : 'action.copy')
4700
+ const open = (event) => {
4701
+ event.preventDefault()
4702
+ event.stopPropagation()
4703
+ controller.open({
4704
+ anchor: event.currentTarget,
4705
+ seq: entry.seq,
4706
+ mode: 'reply',
4707
+ turn: entry.turn,
4708
+ messageId: entry.messageId,
4709
+ text: typeof entry.text === 'string' ? entry.text : '',
4710
+ attachments: typeof entry.attachments === 'number' ? entry.attachments : 0,
4711
+ ...(Array.isArray(entry.blocks) ? { blocks: entry.blocks, patch: true } : { blocks: [] }),
4712
+ })
4713
+ }
4714
+ const copy = (event) => {
4715
+ event.preventDefault()
4716
+ event.stopPropagation()
4717
+ const text = replyCopyText(entry)
4718
+ if (text === '') return
4719
+ const write = primitives !== null && primitives !== undefined && typeof primitives.writeClipboard === 'function'
4720
+ ? primitives.writeClipboard
4721
+ : typeof navigator !== 'undefined' && navigator.clipboard && typeof navigator.clipboard.writeText === 'function'
4722
+ ? (value) => navigator.clipboard.writeText(value)
4723
+ : null
4724
+ if (write === null) return
4725
+ Promise.resolve()
4726
+ .then(() => write(text))
4727
+ .then(
4728
+ () => {
4729
+ setCopied(true)
4730
+ window.setTimeout(() => setCopied(false), 1200)
4731
+ },
4732
+ () => {},
4733
+ )
4734
+ }
4735
+ return jsxs('div', {
4736
+ className: 'dshet-tail',
4737
+ 'data-reveal': lastTurn === turn ? 'always' : 'hover',
4738
+ role: 'toolbar',
4739
+ 'aria-label': t === null ? undefined : t('action.tail-actions'),
4740
+ children: [
4741
+ jsx('button', {
4742
+ type: 'button',
4743
+ className: 'dshet-action dshet-tail-action',
4744
+ 'aria-label': label,
4745
+ title: label,
4746
+ onClick: open,
4747
+ key: 'edit',
4748
+ children: jsx(PencilIcon, {}),
4749
+ }),
4750
+ jsx('button', {
4751
+ type: 'button',
4752
+ className: 'dshet-action dshet-tail-action',
4753
+ 'data-copied': copied ? '1' : undefined,
4754
+ 'aria-label': copyLabel,
4755
+ title: copyLabel,
4756
+ onClick: copy,
4757
+ key: 'copy',
4758
+ children: copied ? jsx(CheckIcon, {}) : jsx(CopyIcon, {}),
4759
+ }),
4760
+ ],
4761
+ })
4762
+ }
4763
+
4534
4764
  // --- react entries --------------------------------------------------------
4535
4765
 
4536
4766
  function OverlayEntry({ useChat, useEditTurn, controller, t }) {
@@ -5012,6 +5242,23 @@ window.__ModuleLoader__.load({
5012
5242
  // The row's configure arrow in the plugin manager. Without this the
5013
5243
  // component page is a read-only overview (the card cannot be opened); the
5014
5244
  // key is the manager's `<bundle package>#<row id>` contract.
5245
+ // The fallback strip, for turns the host draws no action row for (see
5246
+ // TailActionEntry). Order 40 keeps it after the shipped contributors
5247
+ // (schedule sits at 20). It is hidden by CSS wherever the host's own strip
5248
+ // exists, so ordering only matters on the turns it appears at all.
5249
+ ctx.slots.inject('conversation.chat.turnTail', () =>
5250
+ ctx.slots.register(
5251
+ {
5252
+ name: 'conversation.chat.turnTail',
5253
+ id: 'edit-turn-tail',
5254
+ order: 40,
5255
+ locale: NS,
5256
+ inject: (sessionId) => ({ controller: controllerFor(sessionId) }),
5257
+ },
5258
+ TailActionEntry,
5259
+ ),
5260
+ )
5261
+
5015
5262
  ctx.slots.inject('plugins.row.config', () =>
5016
5263
  ctx.slots.register(
5017
5264
  {
@@ -5062,7 +5309,7 @@ window.__ModuleLoader__.load({
5062
5309
  const ROUTE_PREFIX = '/dsh-rerun-turn'
5063
5310
 
5064
5311
  /** Keep in sync with package.json and lib/index.js. */
5065
- const PLUGIN_VERSION = '0.2.5'
5312
+ const PLUGIN_VERSION = '0.2.6'
5066
5313
 
5067
5314
  // --- copy -----------------------------------------------------------------
5068
5315
 
@@ -8732,7 +8979,7 @@ window.__ModuleLoader__.load({
8732
8979
  const NS = 'dsh-as-aistudio'
8733
8980
 
8734
8981
  /** Keep in sync with package.json and src/components.js. */
8735
- const PLUGIN_VERSION = '0.3.0'
8982
+ const PLUGIN_VERSION = '0.4.0'
8736
8983
 
8737
8984
  /** Presence marker the live verifier reads. */
8738
8985
  const DEBUG_KEY = '__DSH_AS_AISTUDIO__'
package/src/components.js CHANGED
@@ -26,7 +26,7 @@
26
26
  export const STUDIO_ID = 'dsh-as-aistudio'
27
27
 
28
28
  /** Keep in sync with package.json and src/client.js. */
29
- export const STUDIO_VERSION = '0.3.0'
29
+ export const STUDIO_VERSION = '0.4.0'
30
30
 
31
31
  /**
32
32
  * The AI Studio action strip this studio composes, in the order the user meets
@@ -3,12 +3,12 @@
3
3
  // A component copied into this package must NOT get its own copy of
4
4
  // `@deepseek-ai/dsh-session`: two module instances mean two registries, and the
5
5
  // second copy's `appendPluginRecord` cannot touch a Session the host created
6
- // (the brand check inside it throws). The same goes for `dsh-tools`, whose
7
- // `defineTool` builds validators against schemas the host's own tool registry
8
- // expects.
6
+ // (the brand check inside it throws). So the copies import THIS file, and this
7
+ // file resolves that one package from the running harness.
9
8
  //
10
- // So the copies import THIS file, and this file resolves the two packages from
11
- // the running harness. The harness lives in the `dsh` installation, so the base
9
+ // (`@deepseek-ai/dsh-tools` used to be here as well, for the agent tool that
10
+ // dsh-edit-turn registered. That tool is gone - the family exposes no model-facing
11
+ // tools - so the studio no longer needs the package at all.) The harness lives in the `dsh` installation, so the base
12
12
  // is found by walking up from this file to the nearest node_modules that has it,
13
13
  // with the profile's own tree as the second try. A failure here is a failure of
14
14
  // the whole component row and says exactly which package was missing - silence
@@ -125,8 +125,5 @@ async function importPlatformOrThrow(spec) {
125
125
  }
126
126
  }
127
127
 
128
- /** `@deepseek-ai/dsh-tools`, resolved from the running harness. */
129
- export const dshTools = await importPlatformOrThrow('@deepseek-ai/dsh-tools')
130
-
131
128
  /** `@deepseek-ai/dsh-session`, resolved from the running harness. */
132
129
  export const dshSession = await importPlatformOrThrow('@deepseek-ai/dsh-session')
@@ -1,6 +1,6 @@
1
1
  // dsh-edit-turn host half (dsh-edit-turn.js), VENDORED by tools/vendor.mjs - DO NOT EDIT.
2
2
  //
3
- // Source: ../dsh-edit-turn/lib/index.js (v0.5.4, sha256 9efbde9d50f56073)
3
+ // Source: ../dsh-edit-turn/lib/index.js (v0.5.6, sha256 c4da00d187ff461d)
4
4
  // Regenerate with `npm run vendor`; test/sync.test.js fails when this is stale.
5
5
  //
6
6
  // src/shell.js imports the copy named after the component, which is why the
@@ -46,9 +46,8 @@
46
46
  // is why an `assistant/message` carrier is impossible (it forbids
47
47
  // `sourceEventSeqs` outright).
48
48
  //
49
- // The module resolves `@deepseek-ai/schemastery` defensively. `dsh-tools` is a
50
- // host-provided peer (the loader resolves it for every plugin), but the schema
51
- // package is a regular dependency: when it cannot be resolved - a pruned
49
+ // The module resolves `@deepseek-ai/schemastery` defensively. The schema package
50
+ // is a regular dependency: when it cannot be resolved - a pruned
52
51
  // package cache, an installation that skipped dependencies - a static import
53
52
  // takes the whole plugin down and the feature simply disappears from the UI
54
53
  // with nothing but a line on the host's stdout to explain it. A guarded require
@@ -57,7 +56,6 @@
57
56
  import { randomUUID } from 'node:crypto'
58
57
  import { readFile } from 'node:fs/promises'
59
58
  import { createRequire } from 'node:module'
60
- import { dshTools } from './_platform.js'
61
59
  import { dshSession as sessionApi } from './_platform.js'
62
60
  import {
63
61
  PATCH_TYPE,
@@ -70,9 +68,6 @@ import {
70
68
  projectedBlocks,
71
69
  } from './dsh-edit-turn.patch.js'
72
70
 
73
- const { defineTool } = dshTools
74
-
75
-
76
71
  const nodeRequire = createRequire(import.meta.url)
77
72
  const schemaPackage = (() => {
78
73
  try {
@@ -90,7 +85,7 @@ export const PLUGIN_ID = 'dsh-edit-turn'
90
85
  export const name = PLUGIN_ID
91
86
 
92
87
  /** Keep in sync with package.json and lib/client.js. */
93
- export const PLUGIN_VERSION = '0.5.4'
88
+ export const PLUGIN_VERSION = '0.5.6'
94
89
 
95
90
  // A ring of the last few requests, answering "did the browser half even ask, and
96
91
  // what did it get?" without needing its console. Read-only diagnostics: the
@@ -2345,62 +2340,6 @@ function sessionIdFromExec(exec) {
2345
2340
  return undefined
2346
2341
  }
2347
2342
 
2348
- function registerTool(ctx, config) {
2349
- const tools = ctx.get('tools')
2350
- if (!tools || typeof tools.register !== 'function') return
2351
- tools.register(
2352
- defineTool({
2353
- name: 'edit_turn_targets',
2354
- description:
2355
- 'List the user turns and model replies of a DeepSeek Harness session that the edit-turn plugin can rewrite, with each message\'s current text and, for a reply, every editable block (output text and reasoning). A prompt is replaced where it stands; a reply block is patched in place when the host supports projections, otherwise the reply correction is appended and the later turns are replayed behind it. Read-only: it changes nothing.',
2356
- parameters: {
2357
- sessionId: {
2358
- type: 'string',
2359
- description: 'Session id to inspect. Defaults to the session this agent is running in.',
2360
- },
2361
- limit: { type: 'number', description: 'Maximum number of turns to return (default 20).' },
2362
- },
2363
- output: {
2364
- schema: { type: 'string' },
2365
- render: (_args, value) => [{ type: 'text', text: value }],
2366
- },
2367
- async execute(args, exec) {
2368
- const sessionId = typeof args.sessionId === 'string' && args.sessionId !== '' ? args.sessionId : sessionIdFromExec(exec)
2369
- if (sessionId === undefined) {
2370
- throw new Error('no sessionId was supplied and the calling agent has no session')
2371
- }
2372
- const events = await readEvents(ctx, sessionId)
2373
- if (!events) throw new Error(`no session log for ${sessionId}`)
2374
- const nodes = surfaceOf(ctx, sessionId, events)
2375
- const patches = patchesOf(events)
2376
- const turns = editableTurns(events, nodes)
2377
- const replies = editableReplies(events, nodes, { patches, patch: config && config.patch === true })
2378
- const limit = typeof args.limit === 'number' && args.limit > 0 ? Math.floor(args.limit) : 20
2379
- const shown = turns.slice(-limit)
2380
- const shownReplies = replies.slice(-limit)
2381
- const lines = [
2382
- `session ${sessionId}: ${turns.length} editable turn(s), ${replies.length} editable reply/replies, ${isBusy(events) ? 'BUSY' : 'idle'}`,
2383
- ]
2384
- for (const turn of shown) {
2385
- const preview = turn.text.length > 160 ? `${turn.text.slice(0, 160)}...` : turn.text
2386
- lines.push(`- turn ${turn.turn} (seq ${turn.seq}, ${turn.attachments} attachment(s)): ${preview}`)
2387
- }
2388
- if (turns.length > shown.length) lines.push(`(${turns.length - shown.length} earlier turn(s) omitted)`)
2389
- for (const reply of shownReplies) {
2390
- const preview = reply.text.length > 160 ? `${reply.text.slice(0, 160)}...` : reply.text
2391
- const blocks = Array.isArray(reply.blocks)
2392
- ? ` [${reply.blocks.map((block) => `${block.index}:${block.kind}${block.patched ? '*' : ''}`).join(', ')}]`
2393
- : ''
2394
- lines.push(`- reply (seq ${reply.seq}, ${reply.attachments} non-text part(s))${blocks}: ${preview}`)
2395
- }
2396
- if (replies.length > shownReplies.length) lines.push(`(${replies.length - shownReplies.length} earlier reply/replies omitted)`)
2397
- lines.push('editing a turn replaces its wording in place (no model call); editing a reply block patches it in place when the host supports plugin-record projections, otherwise it replaces the reply and replays the tail. Re-running a turn is dsh-rerun-turn\'s button.')
2398
- return lines.join('\n')
2399
- },
2400
- }),
2401
- )
2402
- }
2403
-
2404
2343
  // --- http --------------------------------------------------------------------
2405
2344
 
2406
2345
  function isLoopbackAddress(address) {
@@ -2802,12 +2741,6 @@ export function apply(ctx, config) {
2802
2741
  ctx.inject(['webServer'], (sub) => registerRoutes(sub.webServer, sub))
2803
2742
  }
2804
2743
 
2805
- const tools = ctx.get('tools')
2806
- if (tools) {
2807
- registerTool(ctx, resolved)
2808
- } else {
2809
- ctx.inject(['tools'], (sub) => registerTool(sub, resolved))
2810
- }
2811
2744
  }
2812
2745
 
2813
2746
  export default apply
@@ -1,6 +1,6 @@
1
1
  // dsh-edit-turn host half (dsh-edit-turn.patch.js), VENDORED by tools/vendor.mjs - DO NOT EDIT.
2
2
  //
3
- // Source: ../dsh-edit-turn/lib/patch.js (v0.5.4, sha256 5e1e8cce1d168f6c)
3
+ // Source: ../dsh-edit-turn/lib/patch.js (v0.5.6, sha256 5e1e8cce1d168f6c)
4
4
  // Regenerate with `npm run vendor`; test/sync.test.js fails when this is stale.
5
5
  //
6
6
  // src/shell.js imports the copy named after the component, which is why the
@@ -1,6 +1,6 @@
1
1
  // dsh-rerun-turn host half (dsh-rerun-turn.js), VENDORED by tools/vendor.mjs - DO NOT EDIT.
2
2
  //
3
- // Source: ../dsh-rerun-turn/lib/index.js (v0.2.5, sha256 1955b5db5e5262ee)
3
+ // Source: ../dsh-rerun-turn/lib/index.js (v0.2.6, sha256 3e20b1a0a07cf213)
4
4
  // Regenerate with `npm run vendor`; test/sync.test.js fails when this is stale.
5
5
  //
6
6
  // src/shell.js imports the copy named after the component, which is why the
@@ -90,7 +90,7 @@ export const PLUGIN_ID = 'dsh-rerun-turn'
90
90
  export const name = PLUGIN_ID
91
91
 
92
92
  /** Keep in sync with package.json and lib/client.js. */
93
- export const PLUGIN_VERSION = '0.2.5'
93
+ export const PLUGIN_VERSION = '0.2.6'
94
94
 
95
95
  const ROUTE_PREFIX = '/dsh-rerun-turn'
96
96
  // A session id is an OPAQUE string to DSH: the host's own schema is
@@ -1182,6 +1182,35 @@ export const ADAPTER_DROPS_EMPTY_USER_CONTENT = probeAdapterDropsEmptyUserConten
1182
1182
  * injectable so the shape is testable without this machine's adapter cache.
1183
1183
  * @returns a one-element `{ type, data }` list for `session.append`.
1184
1184
  */
1185
+ /**
1186
+ * Normalize a shadowed-seq list to the shape the host's contract demands.
1187
+ *
1188
+ * `assertSourceEventReferences` requires a non-empty array of non-negative safe
1189
+ * integers, no duplicates, every entry earlier than the citing event. A list that
1190
+ * is nested (a range written as a pair), holds a non-number, or repeats a seq makes
1191
+ * the whole event unadoptable: the host's strict readers (`adoptSessionEvent`,
1192
+ * `snapshotSessionEvent`, `foldSurface`) reject it outright, so a log carrying one
1193
+ * can only be opened by the lenient path.
1194
+ *
1195
+ * @param shadowed - candidate list from a plan or a recovered record.
1196
+ * @returns a flat, ascending, de-duplicated list of seqs.
1197
+ */
1198
+ export function normalizeShadowed(shadowed) {
1199
+ const flat = []
1200
+ const visit = (value) => {
1201
+ if (Array.isArray(value)) {
1202
+ for (const inner of value) visit(inner)
1203
+ return
1204
+ }
1205
+ // 只认真正的数字:`Number(null)` 是 0,而 0 在 0 基日志里是个合法 seq,
1206
+ // 用 Number() 强转等于往引用列表里塞一条假引用。
1207
+ if (typeof value !== 'number' || !Number.isSafeInteger(value) || value < 0) return
1208
+ flat.push(value)
1209
+ }
1210
+ visit(shadowed)
1211
+ return [...new Set(flat)].sort((a, b) => a - b)
1212
+ }
1213
+
1185
1214
  export function buildShadowWrites(
1186
1215
  plan,
1187
1216
  rerunId,
@@ -1192,7 +1221,8 @@ export function buildShadowWrites(
1192
1221
  {
1193
1222
  type: 'user/message',
1194
1223
  surfaceOp: { op: 'replace', startSeq: plan.startSeq, endSeq: plan.endSeq },
1195
- sourceEventSeqs: plan.shadowed,
1224
+ // 写入前规范化:嵌套数组/重复项/非整数都会让宿主的严格校验器拒绝整条事件。
1225
+ sourceEventSeqs: normalizeShadowed(plan.shadowed),
1196
1226
  data: {
1197
1227
  id: randomUUID(),
1198
1228
  role: 'user',
@@ -1978,8 +2008,10 @@ async function performReplay(ctx, sessionId, rerunId, config) {
1978
2008
  logTo: record.logTo ?? record.endSeq,
1979
2009
  promptSeq: record.promptSeq ?? record.startSeq,
1980
2010
  // The replay copies exactly these surface nodes (and nothing the log
1981
- // happens to hold between them), so the shadowed list must travel.
1982
- shadowed: record.shadowed,
2011
+ // happens to hold between them), so the shadowed list must travel - and it
2012
+ // travels normalized, because a record read back from an older log may hold
2013
+ // the nested shape this plugin used to write.
2014
+ shadowed: normalizeShadowed(record.shadowed),
1983
2015
  }
1984
2016
  const startSeq = session.seq
1985
2017
  const writes = buildReplayWrites(events, plan, baseTurn, rerunId, startSeq, !promptLanded)
@@ -1,6 +1,6 @@
1
1
  {
2
- "dsh-edit-turn": "0.5.4",
3
- "dsh-rerun-turn": "0.2.5",
2
+ "dsh-edit-turn": "0.5.6",
3
+ "dsh-rerun-turn": "0.2.6",
4
4
  "dsh-delete-turn": "0.1.15",
5
5
  "dsh-markdown-bubble": "0.1.5"
6
6
  }