dsh-as-aistudio 0.2.8 → 0.2.11

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-as-aistudio",
3
- "version": "0.2.8",
3
+ "version": "0.2.11",
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; the four components are dependencies mounted by it. Each still installs alone and works alone.",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -52,10 +52,10 @@
52
52
  "verify:manifest": "node tools/verify-manifest.mjs"
53
53
  },
54
54
  "dependencies": {
55
- "dsh-delete-turn": "^0.1.8",
56
- "dsh-edit-turn": "^0.2.19",
55
+ "dsh-delete-turn": "^0.1.9",
56
+ "dsh-edit-turn": "^0.2.20",
57
57
  "dsh-markdown-bubble": "^0.1.4",
58
- "dsh-rerun-turn": "^0.1.25"
58
+ "dsh-rerun-turn": "^0.1.26"
59
59
  },
60
60
  "dsh": {
61
61
  "bundle": {
package/src/client.js CHANGED
@@ -40,7 +40,7 @@ window.__ModuleLoader__.load({
40
40
  const RERUN_PREFIX = '/dsh-rerun-turn'
41
41
 
42
42
  /** Keep in sync with package.json and lib/index.js. */
43
- const PLUGIN_VERSION = '0.2.19'
43
+ const PLUGIN_VERSION = '0.2.20'
44
44
 
45
45
  // --- copy -----------------------------------------------------------------
46
46
 
@@ -83,7 +83,6 @@ window.__ModuleLoader__.load({
83
83
  'error.prompt': '已回退,但改后的内容没能写进上下文。请重试。',
84
84
  'error.reply': '已回退,但替换没有落地。请重试。',
85
85
  'error.empty': '改写后的内容不能为空。',
86
- 'error.unchanged': '内容没有变化,未做任何修改。',
87
86
  'error.rerun-nothing': '已保存,但这一轮没有可重跑的作答。',
88
87
  'error.rerun-unavailable': '已保存,但重跑没有开始,请稍后重试。',
89
88
  'error.not-rerunnable': '这一轮没有可重跑的作答。',
@@ -131,7 +130,6 @@ window.__ModuleLoader__.load({
131
130
  'error.prompt': 'Rolled back, but the re-run did not start. Send the revised text manually.',
132
131
  'error.reply': 'Rolled back, but the replacement did not land. Try again.',
133
132
  'error.empty': 'The revised message cannot be empty.',
134
- 'error.unchanged': 'Nothing changed, so no edit was made.',
135
133
  'error.rerun-nothing': 'Saved, but this turn has no reply that can be re-run.',
136
134
  'error.rerun-unavailable': 'Saved, but the re-run did not start; try again.',
137
135
  'error.not-rerunnable': 'This turn has no reply that can be re-run.',
@@ -746,10 +744,18 @@ window.__ModuleLoader__.load({
746
744
  failure: null,
747
745
  })
748
746
  // `unchanged` means the host wrote NOTHING: the draft is (verbatim)
749
- // the text the message already carries. There is no failure to report -
750
- // the edit was simply not an edit - so it is noticed as such, and
751
- // nothing else about this pass changes: no row was shadowed, so none is
752
- // hidden, and the message stays on screen with the pencil it had.
747
+ // the text the message already carries. The flag is read here for one
748
+ // reason, and it is not the user's: a no-op is not a failure. The
749
+ // `applied === false` wording below would claim the rollback landed
750
+ // and the replacement did not, which is a lie about an edit that was
751
+ // never attempted.
752
+ //
753
+ // Nothing is announced for it, deliberately (0.2.20 removed the
754
+ // notice this used to draw): the edit was simply not an edit, so the
755
+ // editor closes like any save and the screen is exactly as it was -
756
+ // no row was shadowed, so none is hidden, and the message keeps the
757
+ // pencil it had. (The banner also landed on top of the composer's
758
+ // placeholder text, which is not a place to report anything from.)
753
759
  //
754
760
  // The re-run intent survives it. "Re-run" saves first only because the
755
761
  // sibling regenerates the turn from the prompt the surface shows; a
@@ -757,9 +763,7 @@ window.__ModuleLoader__.load({
757
763
  // wording moved, so the chain below is NOT gated on a write having
758
764
  // happened. (The decision itself is the host's - the text is parsed
759
765
  // there, once - and this half only reads the flag it sends back.)
760
- if (data.unchanged === true) {
761
- if (!rerunIntent) this.notify('unchanged')
762
- } else {
766
+ if (data.unchanged !== true) {
763
767
  // The rollback has landed, so the editor is gone with its row: a
764
768
  // failed second half has to be reported somewhere that survives that.
765
769
  if (data.kind === 'reply' && data.applied === false) this.notify('reply')
@@ -2643,7 +2647,7 @@ window.__ModuleLoader__.load({
2643
2647
  const ROUTE_PREFIX = '/dsh-rerun-turn'
2644
2648
 
2645
2649
  /** Keep in sync with package.json and lib/index.js. */
2646
- const PLUGIN_VERSION = '0.1.25'
2650
+ const PLUGIN_VERSION = '0.1.27'
2647
2651
 
2648
2652
  // --- copy -----------------------------------------------------------------
2649
2653
 
@@ -3175,11 +3179,19 @@ window.__ModuleLoader__.load({
3175
3179
  resolved.set(key, decision)
3176
3180
  return decision
3177
3181
  }
3178
- // The turn a row belongs to, from its key shape
3179
- // (`turn-process17`, `turn-tail17`, `assistant-step17:1`).
3180
- const keyTurnOf = (key) => {
3182
+ // The turn a row belongs to. The platform publishes it on the flow item
3183
+ // itself (`data-chat-turn`, the same anchor the prompt-row buttons read),
3184
+ // and a key sometimes spells it out as well
3185
+ // (`turn-process17`, `turn-tail17`, `assistant-step17:1`). Reading the
3186
+ // key alone is what left a bookkeeping turn's empty "completed" strip on
3187
+ // screen once DSH started keying those rows by node kind
3188
+ // (`turn-tail`, `["turn-tail","response"]`) with the turn only in the
3189
+ // data attribute.
3190
+ const keyTurnOf = (key, row) => {
3181
3191
  const match = /(?:turn-process|turn-tail)(\d+)|assistant-step(\d+):/.exec(key)
3182
- return match === null ? null : Number(match[1] ?? match[2])
3192
+ if (match !== null) return Number(match[1] ?? match[2])
3193
+ const fromRow = row && row.dataset ? Number(row.dataset.chatTurn) : Number.NaN
3194
+ return Number.isInteger(fromRow) ? fromRow : null
3183
3195
  }
3184
3196
  const nodeOf = (key) => {
3185
3197
  const direct = snapshot.nodes.get(key)
@@ -3209,7 +3221,7 @@ window.__ModuleLoader__.load({
3209
3221
  key,
3210
3222
  kind: node ? node.kind : null,
3211
3223
  seqs: node ? seqsFor(node) : [],
3212
- keyTurn: keyTurnOf(key),
3224
+ keyTurn: keyTurnOf(key, row),
3213
3225
  inTarget: false,
3214
3226
  inTail: false,
3215
3227
  fresh: false,
@@ -3649,12 +3661,15 @@ window.__ModuleLoader__.load({
3649
3661
 
3650
3662
  const zh = {
3651
3663
  'action.tooltip.message': '删除这条消息',
3664
+ 'action.tooltip.splice': '删除这一轮并接上后面',
3652
3665
  'action.tooltip.step': '删除这一步',
3653
3666
  'action.tooltip.reply': '删除这条回复',
3654
3667
  'dialog.title.message': '删除这条消息?',
3668
+ 'dialog.title.splice': '删除这一轮并接上后面?',
3655
3669
  'dialog.title.step': '删除这一步?',
3656
3670
  'dialog.title.reply': '删除这条回复?',
3657
3671
  'dialog.desc.message': '这条消息将从模型上下文中移除,并从当前转录中隐藏。原始会话日志保持不变。',
3672
+ 'dialog.desc.splice': '这一轮(含你的提问)会从模型上下文中移除;它之后的每一轮随后以新的连续轮号原样重放回来,所以对话视图里这一轮消失、后面的内容直接接上(轮号本身仍在日志里单调递增)。',
3658
3673
  'dialog.desc.step': '这一步的回复与它请求的工具结果将一并从模型上下文中移除,同一回合的其它步骤保留。',
3659
3674
  'dialog.desc.reply': '这条回复连同它的思考、工具调用与注入上下文将从模型上下文中移除,你的提问会保留。',
3660
3675
  'dialog.note': '删除只影响模型后续看到的内容,不会改写历史日志。',
@@ -3677,12 +3692,15 @@ window.__ModuleLoader__.load({
3677
3692
 
3678
3693
  const en = {
3679
3694
  'action.tooltip.message': 'Delete this message',
3695
+ 'action.tooltip.splice': 'Delete this turn and close the gap',
3680
3696
  'action.tooltip.step': 'Delete this step',
3681
3697
  'action.tooltip.reply': 'Delete this reply',
3682
3698
  'dialog.title.message': 'Delete this message?',
3699
+ 'dialog.title.splice': 'Delete this turn and close the gap?',
3683
3700
  'dialog.title.step': 'Delete this step?',
3684
3701
  'dialog.title.reply': 'Delete this reply?',
3685
3702
  'dialog.desc.message': 'This message leaves the model context and is hidden from the current transcript. The original session log stays untouched.',
3703
+ 'dialog.desc.splice': 'This turn (your prompt included) leaves the model context, and every later turn is replayed back unchanged under fresh consecutive turn numbers, so the transcript closes the gap where this turn stood (the log keeps its monotonic numbering).',
3686
3704
  'dialog.desc.step': 'This step and the tool results it requested leave the model context together; other steps in the same turn stay.',
3687
3705
  'dialog.desc.reply': 'This reply leaves the model context together with its reasoning, tool calls and injected context; your prompt stays.',
3688
3706
  'dialog.note': 'Deletion only changes what the model sees next; the append-only log is never rewritten.',
@@ -3720,6 +3738,16 @@ window.__ModuleLoader__.load({
3720
3738
  '[data-variant="think"]:hover .dshdt-think-action,.dshdt-think-action:focus-within{opacity:1}',
3721
3739
  '.dshdt-collapsing{overflow:hidden;transition:height .2s ease,opacity .14s ease,margin .2s ease,padding .2s ease}',
3722
3740
  '[data-dshdt-hidden="1"]{display:none!important}',
3741
+ // The second row action: delete the whole turn and splice the later turns
3742
+ // back. It is its own class so the trash action's selector, its counts and
3743
+ // the interop contract stay exactly as they were; the row attribute turns
3744
+ // it on only where the host can plan the operation.
3745
+ '.dshdt-splice{width:28px;height:28px;padding:6px;display:none;align-items:center;justify-content:center;border:none;border-radius:28px;background:transparent;color:var(--dsw-alias-label-tertiary,#8a8f98);cursor:pointer;transition:background-color .12s,color .12s}',
3746
+ '[data-dshdt-splice="1"] .dshdt-splice{display:inline-flex}',
3747
+ '.dshdt-splice:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover,rgba(127,127,127,.12));color:var(--dsw-alias-button-primary-fill,#4d6bfe)}',
3748
+ '.dshdt-splice:focus-visible{outline:2px solid var(--dsw-alias-button-primary-fill,#4d6bfe);outline-offset:2px}',
3749
+ '.dshdt-splice:disabled{cursor:default;opacity:.4}',
3750
+ '.dshdt-splice svg{width:15px;height:15px}',
3723
3751
  '[data-dshdt-no-target="1"] .dshdt-action{display:none!important}',
3724
3752
  '.dshdt-dialog-text{margin:0;color:var(--dsw-alias-label-primary,inherit);font-size:14px;line-height:22px}',
3725
3753
  '.dshdt-dialog-note{margin:10px 0 0;color:var(--dsw-alias-label-tertiary,#8a8f98);font-size:13px;line-height:20px}',
@@ -3755,6 +3783,23 @@ window.__ModuleLoader__.load({
3755
3783
  ).join('') +
3756
3784
  '</svg>'
3757
3785
 
3786
+ // Collapse glyph: a centre rail with one arrow above and one below, both
3787
+ // pointing at it - "the neighbours move up into this one's place".
3788
+ const SPLICE_ICON_PATHS = [
3789
+ 'M3.2 8h9.6',
3790
+ 'M8 2.4v4',
3791
+ 'M6.3 4.7L8 6.4l1.7-1.7',
3792
+ 'M8 13.6v-4',
3793
+ 'M6.3 11.3L8 9.6l1.7 1.7',
3794
+ ]
3795
+ const SPLICE_ICON_MARKUP =
3796
+ '<svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true">' +
3797
+ SPLICE_ICON_PATHS.map(
3798
+ (d) =>
3799
+ '<path d="' + d + '" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>',
3800
+ ).join('') +
3801
+ '</svg>'
3802
+
3758
3803
  function TrashIcon() {
3759
3804
  return jsx('svg', {
3760
3805
  width: 16,
@@ -3781,6 +3826,9 @@ window.__ModuleLoader__.load({
3781
3826
  // replacement seq -> the seq it was recorded as shadowing. See hiddenViaOf.
3782
3827
  hiddenVia: new Map(),
3783
3828
  surface: new Set(),
3829
+ // The seqs the host would accept a turn-delete for (advertised, not
3830
+ // guessed): the entry appears only on those rows.
3831
+ spliceSeqs: new Set(),
3784
3832
  replyTurns: new Set(),
3785
3833
  edits: new Map(),
3786
3834
  markerTurns: new Set(),
@@ -3847,6 +3895,8 @@ window.__ModuleLoader__.load({
3847
3895
  const hiddenVia = hiddenViaOf(data.hidden)
3848
3896
  const surface = new Set()
3849
3897
  for (const seq of Array.isArray(data.surface) ? data.surface : []) surface.add(seq)
3898
+ const spliceSeqs = new Set()
3899
+ for (const seq of Array.isArray(data.spliceSeqs) ? data.spliceSeqs : []) spliceSeqs.add(seq)
3850
3900
  const replyTurns = new Set()
3851
3901
  for (const turn of Array.isArray(data.replyTurns) ? data.replyTurns : []) replyTurns.add(turn)
3852
3902
  const edits = new Map()
@@ -3858,7 +3908,7 @@ window.__ModuleLoader__.load({
3858
3908
  if (typeof turn === 'number') markerTurns.add(turn)
3859
3909
  }
3860
3910
  const surfaceThrough = typeof data.lastSeq === 'number' ? data.lastSeq : -1
3861
- this.publish({ hidden, hiddenVia, surface, replyTurns, edits, markerTurns, surfaceReady: true, surfaceThrough, loaded: true, loadError: false })
3911
+ this.publish({ hidden, hiddenVia, surface, spliceSeqs, replyTurns, edits, markerTurns, surfaceReady: true, surfaceThrough, loaded: true, loadError: false })
3862
3912
  })
3863
3913
  .catch(() => {
3864
3914
  this.publish({ loadError: true })
@@ -3984,6 +4034,17 @@ window.__ModuleLoader__.load({
3984
4034
  }
3985
4035
  }
3986
4036
 
4037
+ // The second entry: delete the WHOLE turn the row belongs to - the prompt
4038
+ // included - and splice every later turn back under fresh turn numbers. Only
4039
+ // a human prompt anchors it; an injected context row or a reply row is not
4040
+ // the turn's own question.
4041
+ function spliceTargetFor(node) {
4042
+ if (node.kind !== 'user') return null
4043
+ const data = node.data || {}
4044
+ const seq = typeof data.seq === 'number' ? data.seq : node.anchorSeq
4045
+ return typeof seq === 'number' ? { mode: 'splice', seq, label: 'splice' } : null
4046
+ }
4047
+
3987
4048
  function seqsFor(node) {
3988
4049
  const data = node.data || {}
3989
4050
  const out = []
@@ -4106,6 +4167,7 @@ window.__ModuleLoader__.load({
4106
4167
  // does not, and a re-apply (HMR, plugin toggle, bundle-group reload) starts
4107
4168
  // with fresh module state over the very same page.
4108
4169
  const HOST_MARK = 'data-dshdt-action-host'
4170
+ const SPLICE_CLASS = 'dshdt-splice'
4109
4171
  const THINK_MARK = 'data-dshdt-think-action'
4110
4172
  const HOST_CLASS = 'dshdt-action-host'
4111
4173
  const THINK_CLASS = 'dshdt-think-action'
@@ -4148,7 +4210,23 @@ window.__ModuleLoader__.load({
4148
4210
  button.innerHTML = ICON_MARKUP
4149
4211
  host.appendChild(button)
4150
4212
  }
4151
- return { host, button }
4213
+ // The row host carries this plugin's second action as well: delete the
4214
+ // whole turn and splice the later turns back. It rides in the SAME host -
4215
+ // the trash action's namespace, its selector and its one-host-per-row
4216
+ // contract are untouched - under its own class, and the reasoning host
4217
+ // never gets one (a reasoning card owns no turn).
4218
+ let splice = null
4219
+ if (kind !== 'think') {
4220
+ splice = host.querySelector('.' + SPLICE_CLASS)
4221
+ if (splice === null) {
4222
+ splice = document.createElement('button')
4223
+ splice.type = 'button'
4224
+ splice.className = SPLICE_CLASS + ' dshdt-row-action'
4225
+ splice.innerHTML = SPLICE_ICON_MARKUP
4226
+ host.appendChild(splice)
4227
+ }
4228
+ }
4229
+ return { host, button, splice }
4152
4230
  }
4153
4231
 
4154
4232
  // Drop every node this plugin injected. Matched through this plugin's own
@@ -4237,7 +4315,7 @@ window.__ModuleLoader__.load({
4237
4315
  rowActions.delete(row)
4238
4316
  }
4239
4317
 
4240
- function injectRowAction(row, node, target, controller, t) {
4318
+ function injectRowAction(row, node, target, spliceTarget, controller, t) {
4241
4319
  const label = t(`action.tooltip.${target.label}`)
4242
4320
  let entry = rowActions.get(row)
4243
4321
  if (!entry) {
@@ -4246,7 +4324,7 @@ window.__ModuleLoader__.load({
4246
4324
  entry = adoptHost(findOwnedHost(row, false), 'row')
4247
4325
  rowActions.set(row, entry)
4248
4326
  }
4249
- const { host, button } = entry
4327
+ const { host, button, splice } = entry
4250
4328
  if (button.getAttribute('aria-label') !== label) {
4251
4329
  button.setAttribute('aria-label', label)
4252
4330
  button.setAttribute('title', label)
@@ -4256,6 +4334,24 @@ window.__ModuleLoader__.load({
4256
4334
  event.stopPropagation()
4257
4335
  controller.open(target)
4258
4336
  }
4337
+ // The second action is wired only where a splice target exists (and stays
4338
+ // hidden by the row attribute otherwise), so a row can never offer an
4339
+ // operation the host would refuse for the wrong reason.
4340
+ if (splice !== null && splice !== undefined) {
4341
+ const spliceLabel = t('action.tooltip.splice')
4342
+ if (splice.getAttribute('aria-label') !== spliceLabel) {
4343
+ splice.setAttribute('aria-label', spliceLabel)
4344
+ splice.setAttribute('title', spliceLabel)
4345
+ }
4346
+ splice.onclick =
4347
+ spliceTarget === null || spliceTarget === undefined
4348
+ ? null
4349
+ : (event) => {
4350
+ event.preventDefault()
4351
+ event.stopPropagation()
4352
+ controller.open(spliceTarget)
4353
+ }
4354
+ }
4259
4355
  const anchor = node.kind === 'user' || node.kind === 'steering' ? row.querySelector('[class*="_actions"]') : null
4260
4356
  if (anchor) {
4261
4357
  host.classList.remove('dshdt-floating')
@@ -4365,8 +4461,14 @@ window.__ModuleLoader__.load({
4365
4461
  const target = hidden ? null : liveTarget(targetFor(node), view.edits)
4366
4462
  const covered = target !== null || slotCoversRow(node)
4367
4463
  const deletable = !hidden && covered && rowDeletable(node, seqs, view)
4368
- if (deletable && target !== null) injectRowAction(row, node, target, controller, t)
4464
+ // The turn-delete entry rides on the same gates as the delete entry: a
4465
+ // row whose content already left the context offers neither.
4466
+ const spliceTarget =
4467
+ deletable && spliceTargetFor(node) !== null && view.spliceSeqs.has(target.seq) ? liveTarget(spliceTargetFor(node), view.edits) : null
4468
+ if (deletable && target !== null) injectRowAction(row, node, target, spliceTarget, controller, t)
4369
4469
  else removeRowAction(row)
4470
+ if (deletable && target !== null && spliceTarget !== null) row.dataset.dshdtSplice = '1'
4471
+ else delete row.dataset.dshdtSplice
4370
4472
  if (!hidden && covered && !rowDeletable(node, seqs, view)) row.dataset.dshdtNoTarget = '1'
4371
4473
  else delete row.dataset.dshdtNoTarget
4372
4474
  }
@@ -5465,7 +5567,7 @@ window.__ModuleLoader__.load({
5465
5567
  const NS = 'dsh-as-aistudio'
5466
5568
 
5467
5569
  /** Keep in sync with package.json and src/components.js. */
5468
- const PLUGIN_VERSION = '0.2.8'
5570
+ const PLUGIN_VERSION = '0.2.11'
5469
5571
 
5470
5572
  /** Presence marker the live verifier reads. */
5471
5573
  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.2.8'
29
+ export const STUDIO_VERSION = '0.2.11'
30
30
 
31
31
  /**
32
32
  * The AI Studio action strip this studio composes, in the order the user meets
package/src/studio.js CHANGED
@@ -48,7 +48,7 @@ window.__ModuleLoader__.load({
48
48
  const NS = 'dsh-as-aistudio'
49
49
 
50
50
  /** Keep in sync with package.json and src/components.js. */
51
- const PLUGIN_VERSION = '0.2.8'
51
+ const PLUGIN_VERSION = '0.2.11'
52
52
 
53
53
  /** Presence marker the live verifier reads. */
54
54
  const DEBUG_KEY = '__DSH_AS_AISTUDIO__'
@@ -1,6 +1,6 @@
1
1
  // dsh-delete-turn browser half, VENDORED by tools/vendor.mjs - DO NOT EDIT.
2
2
  //
3
- // Source: ../dsh-delete-turn/src/client.js (v0.1.8, sha256 09de24fd97ecd4ec)
3
+ // Source: ../dsh-delete-turn/src/client.js (v0.1.9, sha256 ea27911ab31c1647)
4
4
  // Regenerate with `npm run vendor`; test/vendor-sync.test.js fails when this is stale.
5
5
  //
6
6
  // The component registers its factory with the host module loader; inside this
@@ -25,12 +25,15 @@ export default (require) => {
25
25
 
26
26
  const zh = {
27
27
  'action.tooltip.message': '删除这条消息',
28
+ 'action.tooltip.splice': '删除这一轮并接上后面',
28
29
  'action.tooltip.step': '删除这一步',
29
30
  'action.tooltip.reply': '删除这条回复',
30
31
  'dialog.title.message': '删除这条消息?',
32
+ 'dialog.title.splice': '删除这一轮并接上后面?',
31
33
  'dialog.title.step': '删除这一步?',
32
34
  'dialog.title.reply': '删除这条回复?',
33
35
  'dialog.desc.message': '这条消息将从模型上下文中移除,并从当前转录中隐藏。原始会话日志保持不变。',
36
+ 'dialog.desc.splice': '这一轮(含你的提问)会从模型上下文中移除;它之后的每一轮随后以新的连续轮号原样重放回来,所以对话视图里这一轮消失、后面的内容直接接上(轮号本身仍在日志里单调递增)。',
34
37
  'dialog.desc.step': '这一步的回复与它请求的工具结果将一并从模型上下文中移除,同一回合的其它步骤保留。',
35
38
  'dialog.desc.reply': '这条回复连同它的思考、工具调用与注入上下文将从模型上下文中移除,你的提问会保留。',
36
39
  'dialog.note': '删除只影响模型后续看到的内容,不会改写历史日志。',
@@ -53,12 +56,15 @@ export default (require) => {
53
56
 
54
57
  const en = {
55
58
  'action.tooltip.message': 'Delete this message',
59
+ 'action.tooltip.splice': 'Delete this turn and close the gap',
56
60
  'action.tooltip.step': 'Delete this step',
57
61
  'action.tooltip.reply': 'Delete this reply',
58
62
  'dialog.title.message': 'Delete this message?',
63
+ 'dialog.title.splice': 'Delete this turn and close the gap?',
59
64
  'dialog.title.step': 'Delete this step?',
60
65
  'dialog.title.reply': 'Delete this reply?',
61
66
  'dialog.desc.message': 'This message leaves the model context and is hidden from the current transcript. The original session log stays untouched.',
67
+ 'dialog.desc.splice': 'This turn (your prompt included) leaves the model context, and every later turn is replayed back unchanged under fresh consecutive turn numbers, so the transcript closes the gap where this turn stood (the log keeps its monotonic numbering).',
62
68
  'dialog.desc.step': 'This step and the tool results it requested leave the model context together; other steps in the same turn stay.',
63
69
  'dialog.desc.reply': 'This reply leaves the model context together with its reasoning, tool calls and injected context; your prompt stays.',
64
70
  'dialog.note': 'Deletion only changes what the model sees next; the append-only log is never rewritten.',
@@ -96,6 +102,16 @@ export default (require) => {
96
102
  '[data-variant="think"]:hover .dshdt-think-action,.dshdt-think-action:focus-within{opacity:1}',
97
103
  '.dshdt-collapsing{overflow:hidden;transition:height .2s ease,opacity .14s ease,margin .2s ease,padding .2s ease}',
98
104
  '[data-dshdt-hidden="1"]{display:none!important}',
105
+ // The second row action: delete the whole turn and splice the later turns
106
+ // back. It is its own class so the trash action's selector, its counts and
107
+ // the interop contract stay exactly as they were; the row attribute turns
108
+ // it on only where the host can plan the operation.
109
+ '.dshdt-splice{width:28px;height:28px;padding:6px;display:none;align-items:center;justify-content:center;border:none;border-radius:28px;background:transparent;color:var(--dsw-alias-label-tertiary,#8a8f98);cursor:pointer;transition:background-color .12s,color .12s}',
110
+ '[data-dshdt-splice="1"] .dshdt-splice{display:inline-flex}',
111
+ '.dshdt-splice:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover,rgba(127,127,127,.12));color:var(--dsw-alias-button-primary-fill,#4d6bfe)}',
112
+ '.dshdt-splice:focus-visible{outline:2px solid var(--dsw-alias-button-primary-fill,#4d6bfe);outline-offset:2px}',
113
+ '.dshdt-splice:disabled{cursor:default;opacity:.4}',
114
+ '.dshdt-splice svg{width:15px;height:15px}',
99
115
  '[data-dshdt-no-target="1"] .dshdt-action{display:none!important}',
100
116
  '.dshdt-dialog-text{margin:0;color:var(--dsw-alias-label-primary,inherit);font-size:14px;line-height:22px}',
101
117
  '.dshdt-dialog-note{margin:10px 0 0;color:var(--dsw-alias-label-tertiary,#8a8f98);font-size:13px;line-height:20px}',
@@ -131,6 +147,23 @@ export default (require) => {
131
147
  ).join('') +
132
148
  '</svg>'
133
149
 
150
+ // Collapse glyph: a centre rail with one arrow above and one below, both
151
+ // pointing at it - "the neighbours move up into this one's place".
152
+ const SPLICE_ICON_PATHS = [
153
+ 'M3.2 8h9.6',
154
+ 'M8 2.4v4',
155
+ 'M6.3 4.7L8 6.4l1.7-1.7',
156
+ 'M8 13.6v-4',
157
+ 'M6.3 11.3L8 9.6l1.7 1.7',
158
+ ]
159
+ const SPLICE_ICON_MARKUP =
160
+ '<svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true">' +
161
+ SPLICE_ICON_PATHS.map(
162
+ (d) =>
163
+ '<path d="' + d + '" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>',
164
+ ).join('') +
165
+ '</svg>'
166
+
134
167
  function TrashIcon() {
135
168
  return jsx('svg', {
136
169
  width: 16,
@@ -157,6 +190,9 @@ export default (require) => {
157
190
  // replacement seq -> the seq it was recorded as shadowing. See hiddenViaOf.
158
191
  hiddenVia: new Map(),
159
192
  surface: new Set(),
193
+ // The seqs the host would accept a turn-delete for (advertised, not
194
+ // guessed): the entry appears only on those rows.
195
+ spliceSeqs: new Set(),
160
196
  replyTurns: new Set(),
161
197
  edits: new Map(),
162
198
  markerTurns: new Set(),
@@ -223,6 +259,8 @@ export default (require) => {
223
259
  const hiddenVia = hiddenViaOf(data.hidden)
224
260
  const surface = new Set()
225
261
  for (const seq of Array.isArray(data.surface) ? data.surface : []) surface.add(seq)
262
+ const spliceSeqs = new Set()
263
+ for (const seq of Array.isArray(data.spliceSeqs) ? data.spliceSeqs : []) spliceSeqs.add(seq)
226
264
  const replyTurns = new Set()
227
265
  for (const turn of Array.isArray(data.replyTurns) ? data.replyTurns : []) replyTurns.add(turn)
228
266
  const edits = new Map()
@@ -234,7 +272,7 @@ export default (require) => {
234
272
  if (typeof turn === 'number') markerTurns.add(turn)
235
273
  }
236
274
  const surfaceThrough = typeof data.lastSeq === 'number' ? data.lastSeq : -1
237
- this.publish({ hidden, hiddenVia, surface, replyTurns, edits, markerTurns, surfaceReady: true, surfaceThrough, loaded: true, loadError: false })
275
+ this.publish({ hidden, hiddenVia, surface, spliceSeqs, replyTurns, edits, markerTurns, surfaceReady: true, surfaceThrough, loaded: true, loadError: false })
238
276
  })
239
277
  .catch(() => {
240
278
  this.publish({ loadError: true })
@@ -360,6 +398,17 @@ export default (require) => {
360
398
  }
361
399
  }
362
400
 
401
+ // The second entry: delete the WHOLE turn the row belongs to - the prompt
402
+ // included - and splice every later turn back under fresh turn numbers. Only
403
+ // a human prompt anchors it; an injected context row or a reply row is not
404
+ // the turn's own question.
405
+ function spliceTargetFor(node) {
406
+ if (node.kind !== 'user') return null
407
+ const data = node.data || {}
408
+ const seq = typeof data.seq === 'number' ? data.seq : node.anchorSeq
409
+ return typeof seq === 'number' ? { mode: 'splice', seq, label: 'splice' } : null
410
+ }
411
+
363
412
  function seqsFor(node) {
364
413
  const data = node.data || {}
365
414
  const out = []
@@ -482,6 +531,7 @@ export default (require) => {
482
531
  // does not, and a re-apply (HMR, plugin toggle, bundle-group reload) starts
483
532
  // with fresh module state over the very same page.
484
533
  const HOST_MARK = 'data-dshdt-action-host'
534
+ const SPLICE_CLASS = 'dshdt-splice'
485
535
  const THINK_MARK = 'data-dshdt-think-action'
486
536
  const HOST_CLASS = 'dshdt-action-host'
487
537
  const THINK_CLASS = 'dshdt-think-action'
@@ -524,7 +574,23 @@ export default (require) => {
524
574
  button.innerHTML = ICON_MARKUP
525
575
  host.appendChild(button)
526
576
  }
527
- return { host, button }
577
+ // The row host carries this plugin's second action as well: delete the
578
+ // whole turn and splice the later turns back. It rides in the SAME host -
579
+ // the trash action's namespace, its selector and its one-host-per-row
580
+ // contract are untouched - under its own class, and the reasoning host
581
+ // never gets one (a reasoning card owns no turn).
582
+ let splice = null
583
+ if (kind !== 'think') {
584
+ splice = host.querySelector('.' + SPLICE_CLASS)
585
+ if (splice === null) {
586
+ splice = document.createElement('button')
587
+ splice.type = 'button'
588
+ splice.className = SPLICE_CLASS + ' dshdt-row-action'
589
+ splice.innerHTML = SPLICE_ICON_MARKUP
590
+ host.appendChild(splice)
591
+ }
592
+ }
593
+ return { host, button, splice }
528
594
  }
529
595
 
530
596
  // Drop every node this plugin injected. Matched through this plugin's own
@@ -613,7 +679,7 @@ export default (require) => {
613
679
  rowActions.delete(row)
614
680
  }
615
681
 
616
- function injectRowAction(row, node, target, controller, t) {
682
+ function injectRowAction(row, node, target, spliceTarget, controller, t) {
617
683
  const label = t(`action.tooltip.${target.label}`)
618
684
  let entry = rowActions.get(row)
619
685
  if (!entry) {
@@ -622,7 +688,7 @@ export default (require) => {
622
688
  entry = adoptHost(findOwnedHost(row, false), 'row')
623
689
  rowActions.set(row, entry)
624
690
  }
625
- const { host, button } = entry
691
+ const { host, button, splice } = entry
626
692
  if (button.getAttribute('aria-label') !== label) {
627
693
  button.setAttribute('aria-label', label)
628
694
  button.setAttribute('title', label)
@@ -632,6 +698,24 @@ export default (require) => {
632
698
  event.stopPropagation()
633
699
  controller.open(target)
634
700
  }
701
+ // The second action is wired only where a splice target exists (and stays
702
+ // hidden by the row attribute otherwise), so a row can never offer an
703
+ // operation the host would refuse for the wrong reason.
704
+ if (splice !== null && splice !== undefined) {
705
+ const spliceLabel = t('action.tooltip.splice')
706
+ if (splice.getAttribute('aria-label') !== spliceLabel) {
707
+ splice.setAttribute('aria-label', spliceLabel)
708
+ splice.setAttribute('title', spliceLabel)
709
+ }
710
+ splice.onclick =
711
+ spliceTarget === null || spliceTarget === undefined
712
+ ? null
713
+ : (event) => {
714
+ event.preventDefault()
715
+ event.stopPropagation()
716
+ controller.open(spliceTarget)
717
+ }
718
+ }
635
719
  const anchor = node.kind === 'user' || node.kind === 'steering' ? row.querySelector('[class*="_actions"]') : null
636
720
  if (anchor) {
637
721
  host.classList.remove('dshdt-floating')
@@ -741,8 +825,14 @@ export default (require) => {
741
825
  const target = hidden ? null : liveTarget(targetFor(node), view.edits)
742
826
  const covered = target !== null || slotCoversRow(node)
743
827
  const deletable = !hidden && covered && rowDeletable(node, seqs, view)
744
- if (deletable && target !== null) injectRowAction(row, node, target, controller, t)
828
+ // The turn-delete entry rides on the same gates as the delete entry: a
829
+ // row whose content already left the context offers neither.
830
+ const spliceTarget =
831
+ deletable && spliceTargetFor(node) !== null && view.spliceSeqs.has(target.seq) ? liveTarget(spliceTargetFor(node), view.edits) : null
832
+ if (deletable && target !== null) injectRowAction(row, node, target, spliceTarget, controller, t)
745
833
  else removeRowAction(row)
834
+ if (deletable && target !== null && spliceTarget !== null) row.dataset.dshdtSplice = '1'
835
+ else delete row.dataset.dshdtSplice
746
836
  if (!hidden && covered && !rowDeletable(node, seqs, view)) row.dataset.dshdtNoTarget = '1'
747
837
  else delete row.dataset.dshdtNoTarget
748
838
  }
@@ -1,6 +1,6 @@
1
1
  // dsh-edit-turn browser half, VENDORED by tools/vendor.mjs - DO NOT EDIT.
2
2
  //
3
- // Source: ../dsh-edit-turn/lib/client.js (v0.2.19, sha256 618c7394fb3880c4)
3
+ // Source: ../dsh-edit-turn/lib/client.js (v0.2.20, sha256 dd3f857d93cbdb0c)
4
4
  // Regenerate with `npm run vendor`; test/vendor-sync.test.js fails when this is stale.
5
5
  //
6
6
  // The component registers its factory with the host module loader; inside this
@@ -32,7 +32,7 @@ export default (require) => {
32
32
  const RERUN_PREFIX = '/dsh-rerun-turn'
33
33
 
34
34
  /** Keep in sync with package.json and lib/index.js. */
35
- const PLUGIN_VERSION = '0.2.19'
35
+ const PLUGIN_VERSION = '0.2.20'
36
36
 
37
37
  // --- copy -----------------------------------------------------------------
38
38
 
@@ -75,7 +75,6 @@ export default (require) => {
75
75
  'error.prompt': '已回退,但改后的内容没能写进上下文。请重试。',
76
76
  'error.reply': '已回退,但替换没有落地。请重试。',
77
77
  'error.empty': '改写后的内容不能为空。',
78
- 'error.unchanged': '内容没有变化,未做任何修改。',
79
78
  'error.rerun-nothing': '已保存,但这一轮没有可重跑的作答。',
80
79
  'error.rerun-unavailable': '已保存,但重跑没有开始,请稍后重试。',
81
80
  'error.not-rerunnable': '这一轮没有可重跑的作答。',
@@ -123,7 +122,6 @@ export default (require) => {
123
122
  'error.prompt': 'Rolled back, but the re-run did not start. Send the revised text manually.',
124
123
  'error.reply': 'Rolled back, but the replacement did not land. Try again.',
125
124
  'error.empty': 'The revised message cannot be empty.',
126
- 'error.unchanged': 'Nothing changed, so no edit was made.',
127
125
  'error.rerun-nothing': 'Saved, but this turn has no reply that can be re-run.',
128
126
  'error.rerun-unavailable': 'Saved, but the re-run did not start; try again.',
129
127
  'error.not-rerunnable': 'This turn has no reply that can be re-run.',
@@ -738,10 +736,18 @@ export default (require) => {
738
736
  failure: null,
739
737
  })
740
738
  // `unchanged` means the host wrote NOTHING: the draft is (verbatim)
741
- // the text the message already carries. There is no failure to report -
742
- // the edit was simply not an edit - so it is noticed as such, and
743
- // nothing else about this pass changes: no row was shadowed, so none is
744
- // hidden, and the message stays on screen with the pencil it had.
739
+ // the text the message already carries. The flag is read here for one
740
+ // reason, and it is not the user's: a no-op is not a failure. The
741
+ // `applied === false` wording below would claim the rollback landed
742
+ // and the replacement did not, which is a lie about an edit that was
743
+ // never attempted.
744
+ //
745
+ // Nothing is announced for it, deliberately (0.2.20 removed the
746
+ // notice this used to draw): the edit was simply not an edit, so the
747
+ // editor closes like any save and the screen is exactly as it was -
748
+ // no row was shadowed, so none is hidden, and the message keeps the
749
+ // pencil it had. (The banner also landed on top of the composer's
750
+ // placeholder text, which is not a place to report anything from.)
745
751
  //
746
752
  // The re-run intent survives it. "Re-run" saves first only because the
747
753
  // sibling regenerates the turn from the prompt the surface shows; a
@@ -749,9 +755,7 @@ export default (require) => {
749
755
  // wording moved, so the chain below is NOT gated on a write having
750
756
  // happened. (The decision itself is the host's - the text is parsed
751
757
  // there, once - and this half only reads the flag it sends back.)
752
- if (data.unchanged === true) {
753
- if (!rerunIntent) this.notify('unchanged')
754
- } else {
758
+ if (data.unchanged !== true) {
755
759
  // The rollback has landed, so the editor is gone with its row: a
756
760
  // failed second half has to be reported somewhere that survives that.
757
761
  if (data.kind === 'reply' && data.applied === false) this.notify('reply')
@@ -1,6 +1,6 @@
1
1
  // dsh-rerun-turn browser half, VENDORED by tools/vendor.mjs - DO NOT EDIT.
2
2
  //
3
- // Source: ../dsh-rerun-turn/lib/client.js (v0.1.25, sha256 7ce84517d66de598)
3
+ // Source: ../dsh-rerun-turn/lib/client.js (v0.1.27, sha256 1db55a5a2a12ac66)
4
4
  // Regenerate with `npm run vendor`; test/vendor-sync.test.js fails when this is stale.
5
5
  //
6
6
  // The component registers its factory with the host module loader; inside this
@@ -19,7 +19,7 @@ export default (require) => {
19
19
  const ROUTE_PREFIX = '/dsh-rerun-turn'
20
20
 
21
21
  /** Keep in sync with package.json and lib/index.js. */
22
- const PLUGIN_VERSION = '0.1.25'
22
+ const PLUGIN_VERSION = '0.1.27'
23
23
 
24
24
  // --- copy -----------------------------------------------------------------
25
25
 
@@ -551,11 +551,19 @@ export default (require) => {
551
551
  resolved.set(key, decision)
552
552
  return decision
553
553
  }
554
- // The turn a row belongs to, from its key shape
555
- // (`turn-process17`, `turn-tail17`, `assistant-step17:1`).
556
- const keyTurnOf = (key) => {
554
+ // The turn a row belongs to. The platform publishes it on the flow item
555
+ // itself (`data-chat-turn`, the same anchor the prompt-row buttons read),
556
+ // and a key sometimes spells it out as well
557
+ // (`turn-process17`, `turn-tail17`, `assistant-step17:1`). Reading the
558
+ // key alone is what left a bookkeeping turn's empty "completed" strip on
559
+ // screen once DSH started keying those rows by node kind
560
+ // (`turn-tail`, `["turn-tail","response"]`) with the turn only in the
561
+ // data attribute.
562
+ const keyTurnOf = (key, row) => {
557
563
  const match = /(?:turn-process|turn-tail)(\d+)|assistant-step(\d+):/.exec(key)
558
- return match === null ? null : Number(match[1] ?? match[2])
564
+ if (match !== null) return Number(match[1] ?? match[2])
565
+ const fromRow = row && row.dataset ? Number(row.dataset.chatTurn) : Number.NaN
566
+ return Number.isInteger(fromRow) ? fromRow : null
559
567
  }
560
568
  const nodeOf = (key) => {
561
569
  const direct = snapshot.nodes.get(key)
@@ -585,7 +593,7 @@ export default (require) => {
585
593
  key,
586
594
  kind: node ? node.kind : null,
587
595
  seqs: node ? seqsFor(node) : [],
588
- keyTurn: keyTurnOf(key),
596
+ keyTurn: keyTurnOf(key, row),
589
597
  inTarget: false,
590
598
  inTail: false,
591
599
  fresh: false,