dsh-as-aistudio 0.2.11 → 0.2.13

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.11",
3
+ "version": "0.2.13",
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",
package/src/client.js CHANGED
@@ -3657,19 +3657,78 @@ window.__ModuleLoader__.load({
3657
3657
  // Minimum gap between automatic snapshot refreshes while a transcript grows.
3658
3658
  const REFRESH_INTERVAL_MS = 3000
3659
3659
 
3660
+ // --- preferences ----------------------------------------------------------
3661
+ //
3662
+ // One client-side preference: whether a deletion opens the confirmation
3663
+ // dialog first. It lives in localStorage - the same store the official
3664
+ // client plugins keep their view options in - so it survives reloads with no
3665
+ // host round-trip, and a subscription keeps the settings page and the delete
3666
+ // paths on one value.
3667
+
3668
+ const PREFS_KEY = 'dsh-delete-turn:prefs'
3669
+ const DEFAULT_ASK = true
3670
+
3671
+ function readStoredAsk() {
3672
+ try {
3673
+ if (typeof window === 'undefined' || !window.localStorage) return DEFAULT_ASK
3674
+ const raw = window.localStorage.getItem(PREFS_KEY)
3675
+ if (raw === null) return DEFAULT_ASK
3676
+ const parsed = JSON.parse(raw)
3677
+ return parsed && typeof parsed.ask === 'boolean' ? parsed.ask : DEFAULT_ASK
3678
+ } catch {
3679
+ // A missing or malformed entry falls back to the shipped default rather
3680
+ // than breaking the host: the dialog path is the safe behaviour.
3681
+ return DEFAULT_ASK
3682
+ }
3683
+ }
3684
+
3685
+ let askBeforeDelete = readStoredAsk()
3686
+ const prefListeners = new Set()
3687
+
3688
+ function getPrefs() {
3689
+ return { ask: askBeforeDelete }
3690
+ }
3691
+
3692
+ function setAskBeforeDelete(value) {
3693
+ const ask = value === true
3694
+ if (askBeforeDelete === ask) return
3695
+ askBeforeDelete = ask
3696
+ try {
3697
+ if (typeof window !== 'undefined' && window.localStorage) {
3698
+ window.localStorage.setItem(PREFS_KEY, JSON.stringify({ ask }))
3699
+ }
3700
+ } catch {
3701
+ // Storage that refuses writes (private mode) must not break the toggle.
3702
+ }
3703
+ for (const listener of prefListeners) {
3704
+ try {
3705
+ listener()
3706
+ } catch (error) {
3707
+ console.error('[dsh-delete-turn] preference subscriber threw:', error)
3708
+ }
3709
+ }
3710
+ }
3711
+
3712
+ function subscribePrefs(listener) {
3713
+ prefListeners.add(listener)
3714
+ return () => {
3715
+ prefListeners.delete(listener)
3716
+ }
3717
+ }
3718
+
3660
3719
  // --- copy -----------------------------------------------------------------
3661
3720
 
3662
3721
  const zh = {
3663
3722
  'action.tooltip.message': '删除这条消息',
3664
- 'action.tooltip.splice': '删除这一轮并接上后面',
3723
+ 'action.tooltip.turn': '删除这一轮',
3665
3724
  'action.tooltip.step': '删除这一步',
3666
3725
  'action.tooltip.reply': '删除这条回复',
3667
3726
  'dialog.title.message': '删除这条消息?',
3668
- 'dialog.title.splice': '删除这一轮并接上后面?',
3727
+ 'dialog.title.turn': '删除这一轮?',
3669
3728
  'dialog.title.step': '删除这一步?',
3670
3729
  'dialog.title.reply': '删除这条回复?',
3671
3730
  'dialog.desc.message': '这条消息将从模型上下文中移除,并从当前转录中隐藏。原始会话日志保持不变。',
3672
- 'dialog.desc.splice': '这一轮(含你的提问)会从模型上下文中移除;它之后的每一轮随后以新的连续轮号原样重放回来,所以对话视图里这一轮消失、后面的内容直接接上(轮号本身仍在日志里单调递增)。',
3731
+ 'dialog.desc.turn': '这一轮(含你的提问、注入上下文、全部回复与工具结果)将从模型上下文中移除;它之后的轮次原样保留,在对话视图里直接接上来。',
3673
3732
  'dialog.desc.step': '这一步的回复与它请求的工具结果将一并从模型上下文中移除,同一回合的其它步骤保留。',
3674
3733
  'dialog.desc.reply': '这条回复连同它的思考、工具调用与注入上下文将从模型上下文中移除,你的提问会保留。',
3675
3734
  'dialog.note': '删除只影响模型后续看到的内容,不会改写历史日志。',
@@ -3677,6 +3736,11 @@ window.__ModuleLoader__.load({
3677
3736
  'dialog.confirm': '删除',
3678
3737
  'dialog.pending': '删除中…',
3679
3738
  'dialog.retry': '重试',
3739
+ 'config.ask': '删除时询问',
3740
+ 'config.hint.ask': '每次删除前都会弹出确认框。',
3741
+ 'config.hint.auto': '删除会直接执行,不再询问。',
3742
+ 'config.summary.ask': '删除前询问',
3743
+ 'config.summary.auto': '直接删除',
3680
3744
  'error.invalid': '请求无效,请刷新后重试。',
3681
3745
  'error.session-not-active': '这个会话当前未激活,请先打开该会话再删除。',
3682
3746
  'error.session-not-found': '找不到该会话的日志。',
@@ -3692,15 +3756,15 @@ window.__ModuleLoader__.load({
3692
3756
 
3693
3757
  const en = {
3694
3758
  'action.tooltip.message': 'Delete this message',
3695
- 'action.tooltip.splice': 'Delete this turn and close the gap',
3759
+ 'action.tooltip.turn': 'Delete this turn',
3696
3760
  'action.tooltip.step': 'Delete this step',
3697
3761
  'action.tooltip.reply': 'Delete this reply',
3698
3762
  'dialog.title.message': 'Delete this message?',
3699
- 'dialog.title.splice': 'Delete this turn and close the gap?',
3763
+ 'dialog.title.turn': 'Delete this turn?',
3700
3764
  'dialog.title.step': 'Delete this step?',
3701
3765
  'dialog.title.reply': 'Delete this reply?',
3702
3766
  '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).',
3767
+ 'dialog.desc.turn': 'This turn (your prompt, injected context, every reply step and tool result) leaves the model context. Later turns stay exactly where they are and simply move up in the transcript.',
3704
3768
  'dialog.desc.step': 'This step and the tool results it requested leave the model context together; other steps in the same turn stay.',
3705
3769
  'dialog.desc.reply': 'This reply leaves the model context together with its reasoning, tool calls and injected context; your prompt stays.',
3706
3770
  'dialog.note': 'Deletion only changes what the model sees next; the append-only log is never rewritten.',
@@ -3708,6 +3772,11 @@ window.__ModuleLoader__.load({
3708
3772
  'dialog.confirm': 'Delete',
3709
3773
  'dialog.pending': 'Deleting...',
3710
3774
  'dialog.retry': 'Retry',
3775
+ 'config.ask': 'Ask before deleting',
3776
+ 'config.hint.ask': 'Show a confirmation dialog before every deletion.',
3777
+ 'config.hint.auto': 'Delete right away without asking.',
3778
+ 'config.summary.ask': 'Ask before deleting',
3779
+ 'config.summary.auto': 'Delete without asking',
3711
3780
  'error.invalid': 'Invalid request; refresh and try again.',
3712
3781
  'error.session-not-active': 'This session is not open in DSH; open it first.',
3713
3782
  'error.session-not-found': 'No session log was found for this id.',
@@ -3738,20 +3807,22 @@ window.__ModuleLoader__.load({
3738
3807
  '[data-variant="think"]:hover .dshdt-think-action,.dshdt-think-action:focus-within{opacity:1}',
3739
3808
  '.dshdt-collapsing{overflow:hidden;transition:height .2s ease,opacity .14s ease,margin .2s ease,padding .2s ease}',
3740
3809
  '[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}',
3810
+ // The second row action: delete the whole turn (prompt included). It is
3811
+ // its own class so the trash action's selector, its counts and the
3812
+ // interop contract stay exactly as they were; the row attribute turns it
3813
+ // on only where the host can plan the operation.
3814
+ '.dshdt-turn{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}',
3815
+ '[data-dshdt-turn="1"] .dshdt-turn{display:inline-flex}',
3816
+ '.dshdt-turn:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover,rgba(127,127,127,.12));color:var(--dsw-alias-button-primary-fill,#4d6bfe)}',
3817
+ '.dshdt-turn:focus-visible{outline:2px solid var(--dsw-alias-button-primary-fill,#4d6bfe);outline-offset:2px}',
3818
+ '.dshdt-turn:disabled{cursor:default;opacity:.4}',
3819
+ '.dshdt-turn svg{width:15px;height:15px}',
3751
3820
  '[data-dshdt-no-target="1"] .dshdt-action{display:none!important}',
3752
3821
  '.dshdt-dialog-text{margin:0;color:var(--dsw-alias-label-primary,inherit);font-size:14px;line-height:22px}',
3753
3822
  '.dshdt-dialog-note{margin:10px 0 0;color:var(--dsw-alias-label-tertiary,#8a8f98);font-size:13px;line-height:20px}',
3754
3823
  '.dshdt-dialog-error{margin:10px 0 0;color:var(--dsw-alias-state-error-primary,#d54941);font-size:13px;line-height:20px}',
3824
+ '.dshdt-config{display:flex;flex-direction:column;gap:8px}',
3825
+ '.dshdt-config-hint{margin:0;color:var(--dsw-alias-label-tertiary,#8a8f98);font-size:13px;line-height:20px}',
3755
3826
  '.dshdt-danger{background:var(--dsw-alias-state-error-primary,#d54941)!important;color:var(--dsw-alias-label-primary-foreground,#fff)!important}',
3756
3827
  '@media (prefers-reduced-motion:reduce){.dshdt-collapsing{transition:none}.dshdt-floating{transition:none}}',
3757
3828
  ].join('')
@@ -3785,16 +3856,16 @@ window.__ModuleLoader__.load({
3785
3856
 
3786
3857
  // Collapse glyph: a centre rail with one arrow above and one below, both
3787
3858
  // pointing at it - "the neighbours move up into this one's place".
3788
- const SPLICE_ICON_PATHS = [
3859
+ const TURN_ICON_PATHS = [
3789
3860
  'M3.2 8h9.6',
3790
3861
  'M8 2.4v4',
3791
3862
  'M6.3 4.7L8 6.4l1.7-1.7',
3792
3863
  'M8 13.6v-4',
3793
3864
  'M6.3 11.3L8 9.6l1.7 1.7',
3794
3865
  ]
3795
- const SPLICE_ICON_MARKUP =
3866
+ const TURN_ICON_MARKUP =
3796
3867
  '<svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true">' +
3797
- SPLICE_ICON_PATHS.map(
3868
+ TURN_ICON_PATHS.map(
3798
3869
  (d) =>
3799
3870
  '<path d="' + d + '" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>',
3800
3871
  ).join('') +
@@ -3828,7 +3899,7 @@ window.__ModuleLoader__.load({
3828
3899
  surface: new Set(),
3829
3900
  // The seqs the host would accept a turn-delete for (advertised, not
3830
3901
  // guessed): the entry appears only on those rows.
3831
- spliceSeqs: new Set(),
3902
+ turnSeqs: new Set(),
3832
3903
  replyTurns: new Set(),
3833
3904
  edits: new Map(),
3834
3905
  markerTurns: new Set(),
@@ -3895,8 +3966,8 @@ window.__ModuleLoader__.load({
3895
3966
  const hiddenVia = hiddenViaOf(data.hidden)
3896
3967
  const surface = new Set()
3897
3968
  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)
3969
+ const turnSeqs = new Set()
3970
+ for (const seq of Array.isArray(data.turnSeqs) ? data.turnSeqs : []) turnSeqs.add(seq)
3900
3971
  const replyTurns = new Set()
3901
3972
  for (const turn of Array.isArray(data.replyTurns) ? data.replyTurns : []) replyTurns.add(turn)
3902
3973
  const edits = new Map()
@@ -3908,7 +3979,7 @@ window.__ModuleLoader__.load({
3908
3979
  if (typeof turn === 'number') markerTurns.add(turn)
3909
3980
  }
3910
3981
  const surfaceThrough = typeof data.lastSeq === 'number' ? data.lastSeq : -1
3911
- this.publish({ hidden, hiddenVia, surface, spliceSeqs, replyTurns, edits, markerTurns, surfaceReady: true, surfaceThrough, loaded: true, loadError: false })
3982
+ this.publish({ hidden, hiddenVia, surface, turnSeqs, replyTurns, edits, markerTurns, surfaceReady: true, surfaceThrough, loaded: true, loadError: false })
3912
3983
  })
3913
3984
  .catch(() => {
3914
3985
  this.publish({ loadError: true })
@@ -3931,8 +4002,25 @@ window.__ModuleLoader__.load({
3931
4002
  }
3932
4003
 
3933
4004
  async confirm() {
3934
- const requested = this.view.dialog
3935
- if (requested === null || this.view.pending) return
4005
+ await this.perform(this.view.dialog)
4006
+ }
4007
+
4008
+ // The entry every action uses. With "ask" on, it opens the confirmation
4009
+ // dialog as before. With it off, it deletes straight away - and since a
4010
+ // silent failure would have nowhere to surface, a failed attempt opens the
4011
+ // dialog carrying its error so the user can retry or cancel.
4012
+ async trigger(target) {
4013
+ if (target === null || target === undefined || this.view.pending) return
4014
+ if (getPrefs().ask) {
4015
+ this.open(target)
4016
+ return
4017
+ }
4018
+ const result = await this.perform(target)
4019
+ if (result.failure !== null) this.publish({ dialog: target, pending: false, failure: result.failure })
4020
+ }
4021
+
4022
+ async perform(requested) {
4023
+ if (requested === null || this.view.pending) return { failure: null }
3936
4024
  this.publish({ pending: true, failure: null })
3937
4025
  try {
3938
4026
  // The live node can move while the dialog sits open: another producer
@@ -3953,7 +4041,7 @@ window.__ModuleLoader__.load({
3953
4041
  }
3954
4042
  if (!result.ok) {
3955
4043
  this.publish({ pending: false, failure: result.code })
3956
- return
4044
+ return { failure: result.code }
3957
4045
  }
3958
4046
  const data = result.data
3959
4047
  const hidden = new Map(this.view.hidden)
@@ -3964,8 +4052,10 @@ window.__ModuleLoader__.load({
3964
4052
  for (const [via, seq] of hiddenViaOf(data.hidden)) hiddenVia.set(via, seq)
3965
4053
  this.animateOnce = true
3966
4054
  this.publish({ pending: false, dialog: null, hidden, hiddenVia, loaded: true, loadError: false })
4055
+ return { failure: null }
3967
4056
  } catch {
3968
4057
  this.publish({ pending: false, failure: 'generic' })
4058
+ return { failure: 'generic' }
3969
4059
  }
3970
4060
  }
3971
4061
 
@@ -4035,14 +4125,13 @@ window.__ModuleLoader__.load({
4035
4125
  }
4036
4126
 
4037
4127
  // 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) {
4128
+ // included - as one range replace. Only a human prompt anchors it; an
4129
+ // injected context row or a reply row is not the turn's own question.
4130
+ function turnTargetFor(node) {
4042
4131
  if (node.kind !== 'user') return null
4043
4132
  const data = node.data || {}
4044
4133
  const seq = typeof data.seq === 'number' ? data.seq : node.anchorSeq
4045
- return typeof seq === 'number' ? { mode: 'splice', seq, label: 'splice' } : null
4134
+ return typeof seq === 'number' ? { mode: 'turn', seq, label: 'turn' } : null
4046
4135
  }
4047
4136
 
4048
4137
  function seqsFor(node) {
@@ -4167,7 +4256,7 @@ window.__ModuleLoader__.load({
4167
4256
  // does not, and a re-apply (HMR, plugin toggle, bundle-group reload) starts
4168
4257
  // with fresh module state over the very same page.
4169
4258
  const HOST_MARK = 'data-dshdt-action-host'
4170
- const SPLICE_CLASS = 'dshdt-splice'
4259
+ const TURN_CLASS = 'dshdt-turn'
4171
4260
  const THINK_MARK = 'data-dshdt-think-action'
4172
4261
  const HOST_CLASS = 'dshdt-action-host'
4173
4262
  const THINK_CLASS = 'dshdt-think-action'
@@ -4211,22 +4300,22 @@ window.__ModuleLoader__.load({
4211
4300
  host.appendChild(button)
4212
4301
  }
4213
4302
  // 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 -
4303
+ // whole turn. It rides in the SAME host -
4215
4304
  // the trash action's namespace, its selector and its one-host-per-row
4216
4305
  // contract are untouched - under its own class, and the reasoning host
4217
4306
  // never gets one (a reasoning card owns no turn).
4218
- let splice = null
4307
+ let turn = null
4219
4308
  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)
4309
+ turn = host.querySelector('.' + TURN_CLASS)
4310
+ if (turn === null) {
4311
+ turn = document.createElement('button')
4312
+ turn.type = 'button'
4313
+ turn.className = TURN_CLASS + ' dshdt-row-action'
4314
+ turn.innerHTML = TURN_ICON_MARKUP
4315
+ host.appendChild(turn)
4227
4316
  }
4228
4317
  }
4229
- return { host, button, splice }
4318
+ return { host, button, turn }
4230
4319
  }
4231
4320
 
4232
4321
  // Drop every node this plugin injected. Matched through this plugin's own
@@ -4315,7 +4404,7 @@ window.__ModuleLoader__.load({
4315
4404
  rowActions.delete(row)
4316
4405
  }
4317
4406
 
4318
- function injectRowAction(row, node, target, spliceTarget, controller, t) {
4407
+ function injectRowAction(row, node, target, turnTarget, controller, t) {
4319
4408
  const label = t(`action.tooltip.${target.label}`)
4320
4409
  let entry = rowActions.get(row)
4321
4410
  if (!entry) {
@@ -4324,7 +4413,7 @@ window.__ModuleLoader__.load({
4324
4413
  entry = adoptHost(findOwnedHost(row, false), 'row')
4325
4414
  rowActions.set(row, entry)
4326
4415
  }
4327
- const { host, button, splice } = entry
4416
+ const { host, button, turn } = entry
4328
4417
  if (button.getAttribute('aria-label') !== label) {
4329
4418
  button.setAttribute('aria-label', label)
4330
4419
  button.setAttribute('title', label)
@@ -4332,24 +4421,24 @@ window.__ModuleLoader__.load({
4332
4421
  button.onclick = (event) => {
4333
4422
  event.preventDefault()
4334
4423
  event.stopPropagation()
4335
- controller.open(target)
4424
+ controller.trigger(target)
4336
4425
  }
4337
- // The second action is wired only where a splice target exists (and stays
4426
+ // The second action is wired only where a turn target exists (and stays
4338
4427
  // hidden by the row attribute otherwise), so a row can never offer an
4339
4428
  // 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)
4429
+ if (turn !== null && turn !== undefined) {
4430
+ const turnLabel = t('action.tooltip.turn')
4431
+ if (turn.getAttribute('aria-label') !== turnLabel) {
4432
+ turn.setAttribute('aria-label', turnLabel)
4433
+ turn.setAttribute('title', turnLabel)
4345
4434
  }
4346
- splice.onclick =
4347
- spliceTarget === null || spliceTarget === undefined
4435
+ turn.onclick =
4436
+ turnTarget === null || turnTarget === undefined
4348
4437
  ? null
4349
4438
  : (event) => {
4350
4439
  event.preventDefault()
4351
4440
  event.stopPropagation()
4352
- controller.open(spliceTarget)
4441
+ controller.trigger(turnTarget)
4353
4442
  }
4354
4443
  }
4355
4444
  const anchor = node.kind === 'user' || node.kind === 'steering' ? row.querySelector('[class*="_actions"]') : null
@@ -4391,7 +4480,7 @@ window.__ModuleLoader__.load({
4391
4480
  button.onclick = (event) => {
4392
4481
  event.preventDefault()
4393
4482
  event.stopPropagation()
4394
- controller.open(target)
4483
+ controller.trigger(target)
4395
4484
  }
4396
4485
  if (host.parentElement !== think) think.appendChild(host)
4397
4486
  }
@@ -4463,12 +4552,12 @@ window.__ModuleLoader__.load({
4463
4552
  const deletable = !hidden && covered && rowDeletable(node, seqs, view)
4464
4553
  // The turn-delete entry rides on the same gates as the delete entry: a
4465
4554
  // 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)
4555
+ const turnTarget =
4556
+ deletable && turnTargetFor(node) !== null && view.turnSeqs.has(target.seq) ? liveTarget(turnTargetFor(node), view.edits) : null
4557
+ if (deletable && target !== null) injectRowAction(row, node, target, turnTarget, controller, t)
4469
4558
  else removeRowAction(row)
4470
- if (deletable && target !== null && spliceTarget !== null) row.dataset.dshdtSplice = '1'
4471
- else delete row.dataset.dshdtSplice
4559
+ if (deletable && target !== null && turnTarget !== null) row.dataset.dshdtTurn = '1'
4560
+ else delete row.dataset.dshdtTurn
4472
4561
  if (!hidden && covered && !rowDeletable(node, seqs, view)) row.dataset.dshdtNoTarget = '1'
4473
4562
  else delete row.dataset.dshdtNoTarget
4474
4563
  }
@@ -4538,7 +4627,7 @@ window.__ModuleLoader__.load({
4538
4627
  'aria-label': label,
4539
4628
  title: label,
4540
4629
  disabled: view.pending,
4541
- onClick: () => controller.open({ mode: 'reply', messageId }),
4630
+ onClick: () => controller.trigger({ mode: 'reply', messageId }),
4542
4631
  children: jsx(TrashIcon, {}),
4543
4632
  })
4544
4633
  }
@@ -4583,6 +4672,27 @@ window.__ModuleLoader__.load({
4583
4672
  })
4584
4673
  }
4585
4674
 
4675
+ // The plugin page entry (the component row's "configure" arrow, slot
4676
+ // `plugins.row.config`, key `<bundle>#<row id>`). It renders the row's
4677
+ // one-liner for the manager's summary slot and the toggle card for the page.
4678
+ function AskBeforeDeleteOption({ view, t }) {
4679
+ const tr = typeof t === 'function' ? t : (key) => key
4680
+ const [ask, setAsk] = react.useState(getPrefs().ask)
4681
+ react.useEffect(() => subscribePrefs(() => setAsk(getPrefs().ask)), [])
4682
+ if (view === 'summary') return tr(ask ? 'config.summary.ask' : 'config.summary.auto')
4683
+ return jsxs('div', {
4684
+ className: 'dshdt-config',
4685
+ children: [
4686
+ jsx(primitives.Checkbox, {
4687
+ checked: ask,
4688
+ label: tr('config.ask'),
4689
+ onChange: (checked) => setAskBeforeDelete(checked),
4690
+ }),
4691
+ jsx('p', { className: 'dshdt-config-hint', children: tr(ask ? 'config.hint.ask' : 'config.hint.auto') }),
4692
+ ],
4693
+ })
4694
+ }
4695
+
4586
4696
  function OverlayEntry({ useChat, useDeletion, controller, t }) {
4587
4697
  const snapshot = typeof useChat === 'function' ? useChat((state) => state) : undefined
4588
4698
  const view = useDeletion((state) => state)
@@ -4677,6 +4787,21 @@ window.__ModuleLoader__.load({
4677
4787
  OverlayEntry,
4678
4788
  ),
4679
4789
  )
4790
+
4791
+ // The plugin's own page in the manager: the row this bundle inserts
4792
+ // (`cordis.patch.yml`, id `dsh-delete-turn`) gains a configure arrow that
4793
+ // opens this card. The key is `<bundle package>#<row id>`, the contract of
4794
+ // ui-plugin-manager's rowConfigKey.
4795
+ ctx.slots.inject('plugins.row.config', () =>
4796
+ ctx.slots.register(
4797
+ {
4798
+ name: 'plugins.row.config',
4799
+ key: 'dsh-delete-turn#dsh-delete-turn',
4800
+ locale: NS,
4801
+ },
4802
+ AskBeforeDeleteOption,
4803
+ ),
4804
+ )
4680
4805
  }
4681
4806
 
4682
4807
  exports.apply = apply
@@ -5567,7 +5692,7 @@ window.__ModuleLoader__.load({
5567
5692
  const NS = 'dsh-as-aistudio'
5568
5693
 
5569
5694
  /** Keep in sync with package.json and src/components.js. */
5570
- const PLUGIN_VERSION = '0.2.11'
5695
+ const PLUGIN_VERSION = '0.2.13'
5571
5696
 
5572
5697
  /** Presence marker the live verifier reads. */
5573
5698
  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.11'
29
+ export const STUDIO_VERSION = '0.2.13'
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.11'
51
+ const PLUGIN_VERSION = '0.2.13'
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.9, sha256 ea27911ab31c1647)
3
+ // Source: ../dsh-delete-turn/src/client.js (v0.1.11, sha256 7d14658143d66eb2)
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
@@ -21,19 +21,78 @@ export default (require) => {
21
21
  // Minimum gap between automatic snapshot refreshes while a transcript grows.
22
22
  const REFRESH_INTERVAL_MS = 3000
23
23
 
24
+ // --- preferences ----------------------------------------------------------
25
+ //
26
+ // One client-side preference: whether a deletion opens the confirmation
27
+ // dialog first. It lives in localStorage - the same store the official
28
+ // client plugins keep their view options in - so it survives reloads with no
29
+ // host round-trip, and a subscription keeps the settings page and the delete
30
+ // paths on one value.
31
+
32
+ const PREFS_KEY = 'dsh-delete-turn:prefs'
33
+ const DEFAULT_ASK = true
34
+
35
+ function readStoredAsk() {
36
+ try {
37
+ if (typeof window === 'undefined' || !window.localStorage) return DEFAULT_ASK
38
+ const raw = window.localStorage.getItem(PREFS_KEY)
39
+ if (raw === null) return DEFAULT_ASK
40
+ const parsed = JSON.parse(raw)
41
+ return parsed && typeof parsed.ask === 'boolean' ? parsed.ask : DEFAULT_ASK
42
+ } catch {
43
+ // A missing or malformed entry falls back to the shipped default rather
44
+ // than breaking the host: the dialog path is the safe behaviour.
45
+ return DEFAULT_ASK
46
+ }
47
+ }
48
+
49
+ let askBeforeDelete = readStoredAsk()
50
+ const prefListeners = new Set()
51
+
52
+ function getPrefs() {
53
+ return { ask: askBeforeDelete }
54
+ }
55
+
56
+ function setAskBeforeDelete(value) {
57
+ const ask = value === true
58
+ if (askBeforeDelete === ask) return
59
+ askBeforeDelete = ask
60
+ try {
61
+ if (typeof window !== 'undefined' && window.localStorage) {
62
+ window.localStorage.setItem(PREFS_KEY, JSON.stringify({ ask }))
63
+ }
64
+ } catch {
65
+ // Storage that refuses writes (private mode) must not break the toggle.
66
+ }
67
+ for (const listener of prefListeners) {
68
+ try {
69
+ listener()
70
+ } catch (error) {
71
+ console.error('[dsh-delete-turn] preference subscriber threw:', error)
72
+ }
73
+ }
74
+ }
75
+
76
+ function subscribePrefs(listener) {
77
+ prefListeners.add(listener)
78
+ return () => {
79
+ prefListeners.delete(listener)
80
+ }
81
+ }
82
+
24
83
  // --- copy -----------------------------------------------------------------
25
84
 
26
85
  const zh = {
27
86
  'action.tooltip.message': '删除这条消息',
28
- 'action.tooltip.splice': '删除这一轮并接上后面',
87
+ 'action.tooltip.turn': '删除这一轮',
29
88
  'action.tooltip.step': '删除这一步',
30
89
  'action.tooltip.reply': '删除这条回复',
31
90
  'dialog.title.message': '删除这条消息?',
32
- 'dialog.title.splice': '删除这一轮并接上后面?',
91
+ 'dialog.title.turn': '删除这一轮?',
33
92
  'dialog.title.step': '删除这一步?',
34
93
  'dialog.title.reply': '删除这条回复?',
35
94
  'dialog.desc.message': '这条消息将从模型上下文中移除,并从当前转录中隐藏。原始会话日志保持不变。',
36
- 'dialog.desc.splice': '这一轮(含你的提问)会从模型上下文中移除;它之后的每一轮随后以新的连续轮号原样重放回来,所以对话视图里这一轮消失、后面的内容直接接上(轮号本身仍在日志里单调递增)。',
95
+ 'dialog.desc.turn': '这一轮(含你的提问、注入上下文、全部回复与工具结果)将从模型上下文中移除;它之后的轮次原样保留,在对话视图里直接接上来。',
37
96
  'dialog.desc.step': '这一步的回复与它请求的工具结果将一并从模型上下文中移除,同一回合的其它步骤保留。',
38
97
  'dialog.desc.reply': '这条回复连同它的思考、工具调用与注入上下文将从模型上下文中移除,你的提问会保留。',
39
98
  'dialog.note': '删除只影响模型后续看到的内容,不会改写历史日志。',
@@ -41,6 +100,11 @@ export default (require) => {
41
100
  'dialog.confirm': '删除',
42
101
  'dialog.pending': '删除中…',
43
102
  'dialog.retry': '重试',
103
+ 'config.ask': '删除时询问',
104
+ 'config.hint.ask': '每次删除前都会弹出确认框。',
105
+ 'config.hint.auto': '删除会直接执行,不再询问。',
106
+ 'config.summary.ask': '删除前询问',
107
+ 'config.summary.auto': '直接删除',
44
108
  'error.invalid': '请求无效,请刷新后重试。',
45
109
  'error.session-not-active': '这个会话当前未激活,请先打开该会话再删除。',
46
110
  'error.session-not-found': '找不到该会话的日志。',
@@ -56,15 +120,15 @@ export default (require) => {
56
120
 
57
121
  const en = {
58
122
  'action.tooltip.message': 'Delete this message',
59
- 'action.tooltip.splice': 'Delete this turn and close the gap',
123
+ 'action.tooltip.turn': 'Delete this turn',
60
124
  'action.tooltip.step': 'Delete this step',
61
125
  'action.tooltip.reply': 'Delete this reply',
62
126
  'dialog.title.message': 'Delete this message?',
63
- 'dialog.title.splice': 'Delete this turn and close the gap?',
127
+ 'dialog.title.turn': 'Delete this turn?',
64
128
  'dialog.title.step': 'Delete this step?',
65
129
  'dialog.title.reply': 'Delete this reply?',
66
130
  '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).',
131
+ 'dialog.desc.turn': 'This turn (your prompt, injected context, every reply step and tool result) leaves the model context. Later turns stay exactly where they are and simply move up in the transcript.',
68
132
  'dialog.desc.step': 'This step and the tool results it requested leave the model context together; other steps in the same turn stay.',
69
133
  'dialog.desc.reply': 'This reply leaves the model context together with its reasoning, tool calls and injected context; your prompt stays.',
70
134
  'dialog.note': 'Deletion only changes what the model sees next; the append-only log is never rewritten.',
@@ -72,6 +136,11 @@ export default (require) => {
72
136
  'dialog.confirm': 'Delete',
73
137
  'dialog.pending': 'Deleting...',
74
138
  'dialog.retry': 'Retry',
139
+ 'config.ask': 'Ask before deleting',
140
+ 'config.hint.ask': 'Show a confirmation dialog before every deletion.',
141
+ 'config.hint.auto': 'Delete right away without asking.',
142
+ 'config.summary.ask': 'Ask before deleting',
143
+ 'config.summary.auto': 'Delete without asking',
75
144
  'error.invalid': 'Invalid request; refresh and try again.',
76
145
  'error.session-not-active': 'This session is not open in DSH; open it first.',
77
146
  'error.session-not-found': 'No session log was found for this id.',
@@ -102,20 +171,22 @@ export default (require) => {
102
171
  '[data-variant="think"]:hover .dshdt-think-action,.dshdt-think-action:focus-within{opacity:1}',
103
172
  '.dshdt-collapsing{overflow:hidden;transition:height .2s ease,opacity .14s ease,margin .2s ease,padding .2s ease}',
104
173
  '[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}',
174
+ // The second row action: delete the whole turn (prompt included). It is
175
+ // its own class so the trash action's selector, its counts and the
176
+ // interop contract stay exactly as they were; the row attribute turns it
177
+ // on only where the host can plan the operation.
178
+ '.dshdt-turn{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}',
179
+ '[data-dshdt-turn="1"] .dshdt-turn{display:inline-flex}',
180
+ '.dshdt-turn:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover,rgba(127,127,127,.12));color:var(--dsw-alias-button-primary-fill,#4d6bfe)}',
181
+ '.dshdt-turn:focus-visible{outline:2px solid var(--dsw-alias-button-primary-fill,#4d6bfe);outline-offset:2px}',
182
+ '.dshdt-turn:disabled{cursor:default;opacity:.4}',
183
+ '.dshdt-turn svg{width:15px;height:15px}',
115
184
  '[data-dshdt-no-target="1"] .dshdt-action{display:none!important}',
116
185
  '.dshdt-dialog-text{margin:0;color:var(--dsw-alias-label-primary,inherit);font-size:14px;line-height:22px}',
117
186
  '.dshdt-dialog-note{margin:10px 0 0;color:var(--dsw-alias-label-tertiary,#8a8f98);font-size:13px;line-height:20px}',
118
187
  '.dshdt-dialog-error{margin:10px 0 0;color:var(--dsw-alias-state-error-primary,#d54941);font-size:13px;line-height:20px}',
188
+ '.dshdt-config{display:flex;flex-direction:column;gap:8px}',
189
+ '.dshdt-config-hint{margin:0;color:var(--dsw-alias-label-tertiary,#8a8f98);font-size:13px;line-height:20px}',
119
190
  '.dshdt-danger{background:var(--dsw-alias-state-error-primary,#d54941)!important;color:var(--dsw-alias-label-primary-foreground,#fff)!important}',
120
191
  '@media (prefers-reduced-motion:reduce){.dshdt-collapsing{transition:none}.dshdt-floating{transition:none}}',
121
192
  ].join('')
@@ -149,16 +220,16 @@ export default (require) => {
149
220
 
150
221
  // Collapse glyph: a centre rail with one arrow above and one below, both
151
222
  // pointing at it - "the neighbours move up into this one's place".
152
- const SPLICE_ICON_PATHS = [
223
+ const TURN_ICON_PATHS = [
153
224
  'M3.2 8h9.6',
154
225
  'M8 2.4v4',
155
226
  'M6.3 4.7L8 6.4l1.7-1.7',
156
227
  'M8 13.6v-4',
157
228
  'M6.3 11.3L8 9.6l1.7 1.7',
158
229
  ]
159
- const SPLICE_ICON_MARKUP =
230
+ const TURN_ICON_MARKUP =
160
231
  '<svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true">' +
161
- SPLICE_ICON_PATHS.map(
232
+ TURN_ICON_PATHS.map(
162
233
  (d) =>
163
234
  '<path d="' + d + '" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>',
164
235
  ).join('') +
@@ -192,7 +263,7 @@ export default (require) => {
192
263
  surface: new Set(),
193
264
  // The seqs the host would accept a turn-delete for (advertised, not
194
265
  // guessed): the entry appears only on those rows.
195
- spliceSeqs: new Set(),
266
+ turnSeqs: new Set(),
196
267
  replyTurns: new Set(),
197
268
  edits: new Map(),
198
269
  markerTurns: new Set(),
@@ -259,8 +330,8 @@ export default (require) => {
259
330
  const hiddenVia = hiddenViaOf(data.hidden)
260
331
  const surface = new Set()
261
332
  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)
333
+ const turnSeqs = new Set()
334
+ for (const seq of Array.isArray(data.turnSeqs) ? data.turnSeqs : []) turnSeqs.add(seq)
264
335
  const replyTurns = new Set()
265
336
  for (const turn of Array.isArray(data.replyTurns) ? data.replyTurns : []) replyTurns.add(turn)
266
337
  const edits = new Map()
@@ -272,7 +343,7 @@ export default (require) => {
272
343
  if (typeof turn === 'number') markerTurns.add(turn)
273
344
  }
274
345
  const surfaceThrough = typeof data.lastSeq === 'number' ? data.lastSeq : -1
275
- this.publish({ hidden, hiddenVia, surface, spliceSeqs, replyTurns, edits, markerTurns, surfaceReady: true, surfaceThrough, loaded: true, loadError: false })
346
+ this.publish({ hidden, hiddenVia, surface, turnSeqs, replyTurns, edits, markerTurns, surfaceReady: true, surfaceThrough, loaded: true, loadError: false })
276
347
  })
277
348
  .catch(() => {
278
349
  this.publish({ loadError: true })
@@ -295,8 +366,25 @@ export default (require) => {
295
366
  }
296
367
 
297
368
  async confirm() {
298
- const requested = this.view.dialog
299
- if (requested === null || this.view.pending) return
369
+ await this.perform(this.view.dialog)
370
+ }
371
+
372
+ // The entry every action uses. With "ask" on, it opens the confirmation
373
+ // dialog as before. With it off, it deletes straight away - and since a
374
+ // silent failure would have nowhere to surface, a failed attempt opens the
375
+ // dialog carrying its error so the user can retry or cancel.
376
+ async trigger(target) {
377
+ if (target === null || target === undefined || this.view.pending) return
378
+ if (getPrefs().ask) {
379
+ this.open(target)
380
+ return
381
+ }
382
+ const result = await this.perform(target)
383
+ if (result.failure !== null) this.publish({ dialog: target, pending: false, failure: result.failure })
384
+ }
385
+
386
+ async perform(requested) {
387
+ if (requested === null || this.view.pending) return { failure: null }
300
388
  this.publish({ pending: true, failure: null })
301
389
  try {
302
390
  // The live node can move while the dialog sits open: another producer
@@ -317,7 +405,7 @@ export default (require) => {
317
405
  }
318
406
  if (!result.ok) {
319
407
  this.publish({ pending: false, failure: result.code })
320
- return
408
+ return { failure: result.code }
321
409
  }
322
410
  const data = result.data
323
411
  const hidden = new Map(this.view.hidden)
@@ -328,8 +416,10 @@ export default (require) => {
328
416
  for (const [via, seq] of hiddenViaOf(data.hidden)) hiddenVia.set(via, seq)
329
417
  this.animateOnce = true
330
418
  this.publish({ pending: false, dialog: null, hidden, hiddenVia, loaded: true, loadError: false })
419
+ return { failure: null }
331
420
  } catch {
332
421
  this.publish({ pending: false, failure: 'generic' })
422
+ return { failure: 'generic' }
333
423
  }
334
424
  }
335
425
 
@@ -399,14 +489,13 @@ export default (require) => {
399
489
  }
400
490
 
401
491
  // 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) {
492
+ // included - as one range replace. Only a human prompt anchors it; an
493
+ // injected context row or a reply row is not the turn's own question.
494
+ function turnTargetFor(node) {
406
495
  if (node.kind !== 'user') return null
407
496
  const data = node.data || {}
408
497
  const seq = typeof data.seq === 'number' ? data.seq : node.anchorSeq
409
- return typeof seq === 'number' ? { mode: 'splice', seq, label: 'splice' } : null
498
+ return typeof seq === 'number' ? { mode: 'turn', seq, label: 'turn' } : null
410
499
  }
411
500
 
412
501
  function seqsFor(node) {
@@ -531,7 +620,7 @@ export default (require) => {
531
620
  // does not, and a re-apply (HMR, plugin toggle, bundle-group reload) starts
532
621
  // with fresh module state over the very same page.
533
622
  const HOST_MARK = 'data-dshdt-action-host'
534
- const SPLICE_CLASS = 'dshdt-splice'
623
+ const TURN_CLASS = 'dshdt-turn'
535
624
  const THINK_MARK = 'data-dshdt-think-action'
536
625
  const HOST_CLASS = 'dshdt-action-host'
537
626
  const THINK_CLASS = 'dshdt-think-action'
@@ -575,22 +664,22 @@ export default (require) => {
575
664
  host.appendChild(button)
576
665
  }
577
666
  // 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 -
667
+ // whole turn. It rides in the SAME host -
579
668
  // the trash action's namespace, its selector and its one-host-per-row
580
669
  // contract are untouched - under its own class, and the reasoning host
581
670
  // never gets one (a reasoning card owns no turn).
582
- let splice = null
671
+ let turn = null
583
672
  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)
673
+ turn = host.querySelector('.' + TURN_CLASS)
674
+ if (turn === null) {
675
+ turn = document.createElement('button')
676
+ turn.type = 'button'
677
+ turn.className = TURN_CLASS + ' dshdt-row-action'
678
+ turn.innerHTML = TURN_ICON_MARKUP
679
+ host.appendChild(turn)
591
680
  }
592
681
  }
593
- return { host, button, splice }
682
+ return { host, button, turn }
594
683
  }
595
684
 
596
685
  // Drop every node this plugin injected. Matched through this plugin's own
@@ -679,7 +768,7 @@ export default (require) => {
679
768
  rowActions.delete(row)
680
769
  }
681
770
 
682
- function injectRowAction(row, node, target, spliceTarget, controller, t) {
771
+ function injectRowAction(row, node, target, turnTarget, controller, t) {
683
772
  const label = t(`action.tooltip.${target.label}`)
684
773
  let entry = rowActions.get(row)
685
774
  if (!entry) {
@@ -688,7 +777,7 @@ export default (require) => {
688
777
  entry = adoptHost(findOwnedHost(row, false), 'row')
689
778
  rowActions.set(row, entry)
690
779
  }
691
- const { host, button, splice } = entry
780
+ const { host, button, turn } = entry
692
781
  if (button.getAttribute('aria-label') !== label) {
693
782
  button.setAttribute('aria-label', label)
694
783
  button.setAttribute('title', label)
@@ -696,24 +785,24 @@ export default (require) => {
696
785
  button.onclick = (event) => {
697
786
  event.preventDefault()
698
787
  event.stopPropagation()
699
- controller.open(target)
788
+ controller.trigger(target)
700
789
  }
701
- // The second action is wired only where a splice target exists (and stays
790
+ // The second action is wired only where a turn target exists (and stays
702
791
  // hidden by the row attribute otherwise), so a row can never offer an
703
792
  // 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)
793
+ if (turn !== null && turn !== undefined) {
794
+ const turnLabel = t('action.tooltip.turn')
795
+ if (turn.getAttribute('aria-label') !== turnLabel) {
796
+ turn.setAttribute('aria-label', turnLabel)
797
+ turn.setAttribute('title', turnLabel)
709
798
  }
710
- splice.onclick =
711
- spliceTarget === null || spliceTarget === undefined
799
+ turn.onclick =
800
+ turnTarget === null || turnTarget === undefined
712
801
  ? null
713
802
  : (event) => {
714
803
  event.preventDefault()
715
804
  event.stopPropagation()
716
- controller.open(spliceTarget)
805
+ controller.trigger(turnTarget)
717
806
  }
718
807
  }
719
808
  const anchor = node.kind === 'user' || node.kind === 'steering' ? row.querySelector('[class*="_actions"]') : null
@@ -755,7 +844,7 @@ export default (require) => {
755
844
  button.onclick = (event) => {
756
845
  event.preventDefault()
757
846
  event.stopPropagation()
758
- controller.open(target)
847
+ controller.trigger(target)
759
848
  }
760
849
  if (host.parentElement !== think) think.appendChild(host)
761
850
  }
@@ -827,12 +916,12 @@ export default (require) => {
827
916
  const deletable = !hidden && covered && rowDeletable(node, seqs, view)
828
917
  // The turn-delete entry rides on the same gates as the delete entry: a
829
918
  // 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)
919
+ const turnTarget =
920
+ deletable && turnTargetFor(node) !== null && view.turnSeqs.has(target.seq) ? liveTarget(turnTargetFor(node), view.edits) : null
921
+ if (deletable && target !== null) injectRowAction(row, node, target, turnTarget, controller, t)
833
922
  else removeRowAction(row)
834
- if (deletable && target !== null && spliceTarget !== null) row.dataset.dshdtSplice = '1'
835
- else delete row.dataset.dshdtSplice
923
+ if (deletable && target !== null && turnTarget !== null) row.dataset.dshdtTurn = '1'
924
+ else delete row.dataset.dshdtTurn
836
925
  if (!hidden && covered && !rowDeletable(node, seqs, view)) row.dataset.dshdtNoTarget = '1'
837
926
  else delete row.dataset.dshdtNoTarget
838
927
  }
@@ -902,7 +991,7 @@ export default (require) => {
902
991
  'aria-label': label,
903
992
  title: label,
904
993
  disabled: view.pending,
905
- onClick: () => controller.open({ mode: 'reply', messageId }),
994
+ onClick: () => controller.trigger({ mode: 'reply', messageId }),
906
995
  children: jsx(TrashIcon, {}),
907
996
  })
908
997
  }
@@ -947,6 +1036,27 @@ export default (require) => {
947
1036
  })
948
1037
  }
949
1038
 
1039
+ // The plugin page entry (the component row's "configure" arrow, slot
1040
+ // `plugins.row.config`, key `<bundle>#<row id>`). It renders the row's
1041
+ // one-liner for the manager's summary slot and the toggle card for the page.
1042
+ function AskBeforeDeleteOption({ view, t }) {
1043
+ const tr = typeof t === 'function' ? t : (key) => key
1044
+ const [ask, setAsk] = react.useState(getPrefs().ask)
1045
+ react.useEffect(() => subscribePrefs(() => setAsk(getPrefs().ask)), [])
1046
+ if (view === 'summary') return tr(ask ? 'config.summary.ask' : 'config.summary.auto')
1047
+ return jsxs('div', {
1048
+ className: 'dshdt-config',
1049
+ children: [
1050
+ jsx(primitives.Checkbox, {
1051
+ checked: ask,
1052
+ label: tr('config.ask'),
1053
+ onChange: (checked) => setAskBeforeDelete(checked),
1054
+ }),
1055
+ jsx('p', { className: 'dshdt-config-hint', children: tr(ask ? 'config.hint.ask' : 'config.hint.auto') }),
1056
+ ],
1057
+ })
1058
+ }
1059
+
950
1060
  function OverlayEntry({ useChat, useDeletion, controller, t }) {
951
1061
  const snapshot = typeof useChat === 'function' ? useChat((state) => state) : undefined
952
1062
  const view = useDeletion((state) => state)
@@ -1041,6 +1151,21 @@ export default (require) => {
1041
1151
  OverlayEntry,
1042
1152
  ),
1043
1153
  )
1154
+
1155
+ // The plugin's own page in the manager: the row this bundle inserts
1156
+ // (`cordis.patch.yml`, id `dsh-delete-turn`) gains a configure arrow that
1157
+ // opens this card. The key is `<bundle package>#<row id>`, the contract of
1158
+ // ui-plugin-manager's rowConfigKey.
1159
+ ctx.slots.inject('plugins.row.config', () =>
1160
+ ctx.slots.register(
1161
+ {
1162
+ name: 'plugins.row.config',
1163
+ key: 'dsh-delete-turn#dsh-delete-turn',
1164
+ locale: NS,
1165
+ },
1166
+ AskBeforeDeleteOption,
1167
+ ),
1168
+ )
1044
1169
  }
1045
1170
 
1046
1171
  exports.apply = apply