dsh-as-aistudio 0.3.0 → 0.4.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.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,21 @@
4
4
 
5
5
  ## 中文
6
6
 
7
+ **0.4.1** —— 编辑过的消息,复制按钮被宿主重画之后仍然复制改写后的文本。
8
+
9
+ - **组件跟进**:`dsh-edit-turn` **0.5.7**(另三个不变:`dsh-rerun-turn` 0.2.6、`dsh-delete-turn` 0.1.15、`dsh-markdown-bubble` 0.1.5)。
10
+
11
+ - **复制拿到的是改写前的原文**:编辑一条消息后,界面上显示的是改写后的文本,但点那一排的复制按钮,粘出来的是原文。原因是这次按下原本由**挂在宿主那个按钮节点上**的捕获期监听回答,而动作条是宿主 React 画的——任何一次重渲染都可能换掉那个按钮节点,监听就跟着旧节点一起消失,按下于是落到宿主自己的处理上(它复制"这一行被画出来时的那条消息")。现在改由**文档级捕获期监听按行**回答:与按钮是不是同一个节点无关,捕获期在 React 根节点之前,宿主自己的处理看不到这次按下。本包已把修好的 `dsh-edit-turn` 0.5.7 重新 vendor 进来并重建了浏览器 bundle。
12
+
13
+ **0.4.0** —— 少要一个平台包;四个组件跟进到最新。
14
+
15
+ - **不再要求 `dsh-tools`**:本包内置的宿主半部以前要 `@deepseek-ai/dsh-tools`,唯一原因是 `dsh-edit-turn` 向模型暴露了一个工具(`edit_turn_targets`)。那个工具已经删掉——编辑是靠 UI 面板做的,模型不需要知道这件事——于是整族插件都不再声明模型可见的工具,宿主半部的解析只需要 `dsh-session`。少一个平台包,就少一处跟宿主版本绑定的地方。
16
+
17
+ - **组件跟进**:`dsh-edit-turn` **0.5.6**、`dsh-rerun-turn` **0.2.6**;另两个不变(`dsh-delete-turn` 0.1.15、`dsh-markdown-bubble` 0.1.5)。
18
+
19
+ - `dsh-edit-turn` 0.5.5 把模型工具拿掉;0.5.6 给会话日志裁剪工具加了"先验后写"(那个工具在 `tools/` 下、不随 npm 包发布,本包也不含它,只是版本号跟着走)。
20
+ - `dsh-rerun-turn` 0.2.6 修 `sourceEventSeqs`:写入前摊平嵌套数组、去重、排序,并且**只接受真正的数字**——之前写出的形状(嵌套数组、缺项、倒序区间)会被宿主的校验器整条拒收,**整份会话日志都加载不了**。这是实测出来的:一个 29,497 事件的会话里有三条这样的记录,宿主三层校验全拒。
21
+
7
22
  **0.3.0** —— 设置页搬到插件行自己的配置箭头;组件设置卡片直接复用组件自己的那一个。
8
23
 
9
24
  - **设置页换了位置**:以前是「设置 → 插件 → 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.1",
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.7'
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 -----------------------------------------------------------------
@@ -1913,6 +1935,17 @@ window.__ModuleLoader__.load({
1913
1935
  const COPY_TIMER = 'dshetCopyTimer'
1914
1936
  /** The host draws its own check for a second; this is the same second. */
1915
1937
  const COPY_FEEDBACK_MS = 1000
1938
+ /**
1939
+ * Row key -> the text that row's copy button must hand over.
1940
+ *
1941
+ * The host's copy button copies the message the row was drawn for, so on a row
1942
+ * this plugin rewrote it would copy the wording the user just replaced. The
1943
+ * press is answered from a document-level listener keyed by ROW rather than by
1944
+ * button node: the host re-renders its action bar, and a listener planted on
1945
+ * the button it drew goes away with that node (the button it draws next is a
1946
+ * different one).
1947
+ */
1948
+ const rowCopyText = new Map()
1916
1949
 
1917
1950
  /**
1918
1951
  * A button another plugin put in this bar.
@@ -1977,6 +2010,13 @@ window.__ModuleLoader__.load({
1977
2010
  * takeover that cannot copy is worse than none.
1978
2011
  */
1979
2012
  function takeOverRowCopy(row, text, t) {
2013
+ // Registered before the button lookup on purpose: the handover has to work
2014
+ // even when this pass cannot find the button to mark (the next one may, and
2015
+ // the document-level press answers from this table either way).
2016
+ {
2017
+ const key = row.getAttribute('data-chat-flow-key')
2018
+ if (typeof key === 'string' && key !== '') rowCopyText.set(key, text)
2019
+ }
1980
2020
  const button = nativeCopyButton(row, t)
1981
2021
  if (button === null) return
1982
2022
  const write = primitives !== null && primitives !== undefined && typeof primitives.writeClipboard === 'function'
@@ -2105,6 +2145,8 @@ window.__ModuleLoader__.load({
2105
2145
 
2106
2146
  /** The copy handover of a row, wherever the last pass left it. */
2107
2147
  function releaseRowCopy(row) {
2148
+ const key = row.getAttribute('data-chat-flow-key')
2149
+ if (typeof key === 'string' && key !== '') rowCopyText.delete(key)
2108
2150
  const button = nativeCopyButton(row)
2109
2151
  if (button !== null) releaseCopyButton(button)
2110
2152
  }
@@ -3695,9 +3737,12 @@ window.__ModuleLoader__.load({
3695
3737
  error.className = 'dshet-error'
3696
3738
  error.setAttribute('role', 'status')
3697
3739
  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)
3740
+ // The host's own sentence is always shown: a code-only message leaves the
3741
+ // reader with nothing to act on, and it is what made a real 400 impossible
3742
+ // to diagnose.
3743
+ error.textContent = detail === null
3744
+ ? errorText(t, failureCode)
3745
+ : `${errorText(t, failureCode)}(${detail.slice(0, 300)})`
3701
3746
  box.appendChild(error)
3702
3747
  }
3703
3748
 
@@ -4531,6 +4576,211 @@ window.__ModuleLoader__.load({
4531
4576
  })
4532
4577
  }
4533
4578
 
4579
+ // --- the fallback strip ---------------------------------------------------
4580
+ //
4581
+ // TurnTailNodeView, when a turn's model reply has no closing body
4582
+ // (`closing === null`), returns the turnTail slot alone and never renders
4583
+ // `conversation.chat.assistant-actions`. Every action on that row goes with
4584
+ // it - this plugin's pencil, and the platform's own copy, branch, feedback
4585
+ // and usage buttons too. Nothing a plugin registers inside a strip that is
4586
+ // not rendered can bring those back, so the fallback lives in the one slot
4587
+ // that branch DOES render.
4588
+ //
4589
+ // It is not a second bar for every turn: the host puts `data-actions-reveal`
4590
+ // on the tail container only on the branch that draws its own strip, and one
4591
+ // CSS rule hides this one wherever that attribute is present.
4592
+
4593
+ /**
4594
+ * The reply this turn's fallback strip acts on.
4595
+ *
4596
+ * A turn runs several steps and the strip belongs at the end of the turn,
4597
+ * so the LAST rewriteable reply of that turn wins - the same row the host
4598
+ * would have hung its own strip on.
4599
+ *
4600
+ * @param view - the controller snapshot (see readReplyView).
4601
+ * @param turn - the TurnLocation the slot hands over.
4602
+ * @returns the reply entry, or undefined when this turn has none.
4603
+ */
4604
+ function replyForTurn(view, turn) {
4605
+ if (view === null || view === undefined || typeof turn !== 'number') return undefined
4606
+ const replies = view.replies
4607
+ if (replies === null || replies === undefined || typeof replies.values !== 'function') return undefined
4608
+ if (view.hidden === null || view.hidden === undefined || typeof view.hidden.has !== 'function') return undefined
4609
+ let best
4610
+ for (const reply of replies.values()) {
4611
+ if (reply === null || typeof reply !== 'object') continue
4612
+ if (typeof reply.seq !== 'number' || reply.turn !== turn) continue
4613
+ if (view.hidden.has(reply.seq)) continue
4614
+ if (best === undefined || reply.seq > best.seq) best = reply
4615
+ }
4616
+ return best
4617
+ }
4618
+
4619
+ /**
4620
+ * What a copy press on the fallback strip writes.
4621
+ *
4622
+ * The model's own answer when the turn has one, and its reasoning when the
4623
+ * whole reply went into thinking - the case this strip exists for. Blocks
4624
+ * are joined the way they read, blank ones dropped so a trailing empty
4625
+ * block does not become a trailing newline.
4626
+ *
4627
+ * @param entry - one reply entry.
4628
+ * @returns the text to copy; empty when there is nothing readable.
4629
+ */
4630
+ function replyCopyText(entry) {
4631
+ const blocks = entry !== null && entry !== undefined && Array.isArray(entry.blocks) ? entry.blocks : []
4632
+ const gather = (kind) =>
4633
+ blocks
4634
+ .filter((block) => block !== null && typeof block === 'object' && block.kind === kind)
4635
+ .map((block) => (typeof block.text === 'string' ? block.text : ''))
4636
+ .filter((text) => text.trim() !== '')
4637
+ .join('\n\n')
4638
+ const answer = gather('text')
4639
+ return answer !== '' ? answer : gather('reasoning')
4640
+ }
4641
+
4642
+ function CopyIcon() {
4643
+ return jsx('svg', {
4644
+ width: 16,
4645
+ height: 16,
4646
+ viewBox: '0 0 16 16',
4647
+ fill: 'none',
4648
+ 'aria-hidden': true,
4649
+ children: [
4650
+ jsx('rect', { x: 5.2, y: 5.2, width: 8, height: 8, rx: 1.6, stroke: 'currentColor', strokeWidth: 1.2 }, 0),
4651
+ 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),
4652
+ ],
4653
+ })
4654
+ }
4655
+
4656
+ function CheckIcon() {
4657
+ return jsx('svg', {
4658
+ width: 16,
4659
+ height: 16,
4660
+ viewBox: '0 0 16 16',
4661
+ fill: 'none',
4662
+ 'aria-hidden': true,
4663
+ 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),
4664
+ })
4665
+ }
4666
+
4667
+ /**
4668
+ * The fallback strip for a turn the host draws no action row for.
4669
+ *
4670
+ * @param props - the slot's owner props ({ turn, seq, openFile }) plus the
4671
+ * injected controller, the standard `useChat` and this plugin's `t`.
4672
+ */
4673
+ function TailActionEntry(input) {
4674
+ const props = input !== null && typeof input === 'object' ? input : {}
4675
+ const turn = props.turn
4676
+ const controller = props.controller
4677
+ const t = typeof props.t === 'function' ? props.t : null
4678
+ const useChat = props.useChat
4679
+ const [view, setView] = react.useState(() => {
4680
+ try {
4681
+ return readReplyView(controller)
4682
+ } catch {
4683
+ return null
4684
+ }
4685
+ })
4686
+ const [copied, setCopied] = react.useState(false)
4687
+ react.useEffect(() => {
4688
+ let unsubscribe = null
4689
+ try {
4690
+ setView(readReplyView(controller))
4691
+ if (controller !== null && controller !== undefined && typeof controller.subscribe === 'function') {
4692
+ unsubscribe = controller.subscribe(() => setView(readReplyView(controller)))
4693
+ }
4694
+ } catch {
4695
+ setView(null)
4696
+ }
4697
+ return () => {
4698
+ if (typeof unsubscribe === 'function') unsubscribe()
4699
+ }
4700
+ }, [controller])
4701
+ // The host reveals the latest turn's row always and older ones on hover.
4702
+ // The same rule, from the same source it uses (the last turn in the
4703
+ // timeline), so the strip does not behave differently from the row it
4704
+ // stands in for.
4705
+ const lastTurn = typeof useChat === 'function'
4706
+ ? useChat((state) => {
4707
+ if (state === null || typeof state !== 'object') return undefined
4708
+ const timeline = state.timeline
4709
+ const order = timeline === null || timeline === undefined ? null : timeline.turnOrder
4710
+ if (!Array.isArray(order) || order.length === 0) return undefined
4711
+ return order[order.length - 1]
4712
+ })
4713
+ : undefined
4714
+ const entry = replyForTurn(view, turn)
4715
+ // Nothing this plugin can act on: the strip stays out of the way rather
4716
+ // than drawing an empty row.
4717
+ if (entry === undefined) return null
4718
+ const label = t === null ? '' : t('action.edit-reply')
4719
+ const copyLabel = t === null ? '' : t(copied ? 'action.copied' : 'action.copy')
4720
+ const open = (event) => {
4721
+ event.preventDefault()
4722
+ event.stopPropagation()
4723
+ controller.open({
4724
+ anchor: event.currentTarget,
4725
+ seq: entry.seq,
4726
+ mode: 'reply',
4727
+ turn: entry.turn,
4728
+ messageId: entry.messageId,
4729
+ text: typeof entry.text === 'string' ? entry.text : '',
4730
+ attachments: typeof entry.attachments === 'number' ? entry.attachments : 0,
4731
+ ...(Array.isArray(entry.blocks) ? { blocks: entry.blocks, patch: true } : { blocks: [] }),
4732
+ })
4733
+ }
4734
+ const copy = (event) => {
4735
+ event.preventDefault()
4736
+ event.stopPropagation()
4737
+ const text = replyCopyText(entry)
4738
+ if (text === '') return
4739
+ const write = primitives !== null && primitives !== undefined && typeof primitives.writeClipboard === 'function'
4740
+ ? primitives.writeClipboard
4741
+ : typeof navigator !== 'undefined' && navigator.clipboard && typeof navigator.clipboard.writeText === 'function'
4742
+ ? (value) => navigator.clipboard.writeText(value)
4743
+ : null
4744
+ if (write === null) return
4745
+ Promise.resolve()
4746
+ .then(() => write(text))
4747
+ .then(
4748
+ () => {
4749
+ setCopied(true)
4750
+ window.setTimeout(() => setCopied(false), 1200)
4751
+ },
4752
+ () => {},
4753
+ )
4754
+ }
4755
+ return jsxs('div', {
4756
+ className: 'dshet-tail',
4757
+ 'data-reveal': lastTurn === turn ? 'always' : 'hover',
4758
+ role: 'toolbar',
4759
+ 'aria-label': t === null ? undefined : t('action.tail-actions'),
4760
+ children: [
4761
+ jsx('button', {
4762
+ type: 'button',
4763
+ className: 'dshet-action dshet-tail-action',
4764
+ 'aria-label': label,
4765
+ title: label,
4766
+ onClick: open,
4767
+ key: 'edit',
4768
+ children: jsx(PencilIcon, {}),
4769
+ }),
4770
+ jsx('button', {
4771
+ type: 'button',
4772
+ className: 'dshet-action dshet-tail-action',
4773
+ 'data-copied': copied ? '1' : undefined,
4774
+ 'aria-label': copyLabel,
4775
+ title: copyLabel,
4776
+ onClick: copy,
4777
+ key: 'copy',
4778
+ children: copied ? jsx(CheckIcon, {}) : jsx(CopyIcon, {}),
4779
+ }),
4780
+ ],
4781
+ })
4782
+ }
4783
+
4534
4784
  // --- react entries --------------------------------------------------------
4535
4785
 
4536
4786
  function OverlayEntry({ useChat, useEditTurn, controller, t }) {
@@ -4545,6 +4795,45 @@ window.__ModuleLoader__.load({
4545
4795
  // left". Blur cannot be that signal: a host re-render replaces the node
4546
4796
  // (blur without a related target) and a save temporarily disables it -
4547
4797
  // both used to drop the caret for good.
4798
+ // The press that matters: the host's own copy button on a row this plugin
4799
+ // rewrote. Capture phase, so the host's handler (which listens at the React
4800
+ // root, below the document) never sees the press; by row key, so a button
4801
+ // the host re-rendered is still answered.
4802
+ react.useEffect(() => {
4803
+ if (typeof document.addEventListener !== 'function' || typeof document.removeEventListener !== 'function') {
4804
+ return undefined
4805
+ }
4806
+ const onCopyPress = (event) => {
4807
+ const target = event.target
4808
+ if (target === null || target === undefined || typeof target.closest !== 'function') return
4809
+ const button = target.closest('button')
4810
+ if (button === null) return
4811
+ // Our own buttons sit in the same bar: theirs is not this press.
4812
+ if (pluginOwned(button)) return
4813
+ const row = button.closest('[data-chat-flow-key]')
4814
+ if (row === null) return
4815
+ const key = row.getAttribute('data-chat-flow-key')
4816
+ if (typeof key !== 'string' || key === '') return
4817
+ const text = rowCopyText.get(key)
4818
+ if (typeof text !== 'string') return
4819
+ const write = primitives !== null && primitives !== undefined && typeof primitives.writeClipboard === 'function'
4820
+ ? primitives.writeClipboard
4821
+ : null
4822
+ if (write === null) return
4823
+ if (typeof event.preventDefault === 'function') event.preventDefault()
4824
+ if (typeof event.stopPropagation === 'function') event.stopPropagation()
4825
+ if (typeof event.stopImmediatePropagation === 'function') event.stopImmediatePropagation()
4826
+ Promise.resolve(write(text)).then(
4827
+ (ok) => {
4828
+ if (ok === true) flashRowCopy(button, typeof t === 'function' ? t('action.copied') : null)
4829
+ },
4830
+ () => {},
4831
+ )
4832
+ }
4833
+ document.addEventListener('click', onCopyPress, true)
4834
+ return () => document.removeEventListener('click', onCopyPress, true)
4835
+ }, [t])
4836
+
4548
4837
  react.useEffect(() => {
4549
4838
  if (typeof document.addEventListener !== 'function' || typeof document.removeEventListener !== 'function') {
4550
4839
  return undefined
@@ -5012,6 +5301,23 @@ window.__ModuleLoader__.load({
5012
5301
  // The row's configure arrow in the plugin manager. Without this the
5013
5302
  // component page is a read-only overview (the card cannot be opened); the
5014
5303
  // key is the manager's `<bundle package>#<row id>` contract.
5304
+ // The fallback strip, for turns the host draws no action row for (see
5305
+ // TailActionEntry). Order 40 keeps it after the shipped contributors
5306
+ // (schedule sits at 20). It is hidden by CSS wherever the host's own strip
5307
+ // exists, so ordering only matters on the turns it appears at all.
5308
+ ctx.slots.inject('conversation.chat.turnTail', () =>
5309
+ ctx.slots.register(
5310
+ {
5311
+ name: 'conversation.chat.turnTail',
5312
+ id: 'edit-turn-tail',
5313
+ order: 40,
5314
+ locale: NS,
5315
+ inject: (sessionId) => ({ controller: controllerFor(sessionId) }),
5316
+ },
5317
+ TailActionEntry,
5318
+ ),
5319
+ )
5320
+
5015
5321
  ctx.slots.inject('plugins.row.config', () =>
5016
5322
  ctx.slots.register(
5017
5323
  {
@@ -5062,7 +5368,7 @@ window.__ModuleLoader__.load({
5062
5368
  const ROUTE_PREFIX = '/dsh-rerun-turn'
5063
5369
 
5064
5370
  /** Keep in sync with package.json and lib/index.js. */
5065
- const PLUGIN_VERSION = '0.2.5'
5371
+ const PLUGIN_VERSION = '0.2.6'
5066
5372
 
5067
5373
  // --- copy -----------------------------------------------------------------
5068
5374
 
@@ -8732,7 +9038,7 @@ window.__ModuleLoader__.load({
8732
9038
  const NS = 'dsh-as-aistudio'
8733
9039
 
8734
9040
  /** Keep in sync with package.json and src/components.js. */
8735
- const PLUGIN_VERSION = '0.3.0'
9041
+ const PLUGIN_VERSION = '0.4.1'
8736
9042
 
8737
9043
  /** Presence marker the live verifier reads. */
8738
9044
  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.1'
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.7, sha256 32dc3ed4d66bb063)
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.7'
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.7, 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.7",
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
  }