dsh-as-aistudio 0.2.11 → 0.2.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/client.js +187 -62
- package/src/components.js +1 -1
- package/src/studio.js +1 -1
- package/src/vendor/dsh-delete-turn.js +187 -62
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-as-aistudio",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.12",
|
|
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.
|
|
3723
|
+
'action.tooltip.turn': '删除这一轮',
|
|
3665
3724
|
'action.tooltip.step': '删除这一步',
|
|
3666
3725
|
'action.tooltip.reply': '删除这条回复',
|
|
3667
3726
|
'dialog.title.message': '删除这条消息?',
|
|
3668
|
-
'dialog.title.
|
|
3727
|
+
'dialog.title.turn': '删除这一轮?',
|
|
3669
3728
|
'dialog.title.step': '删除这一步?',
|
|
3670
3729
|
'dialog.title.reply': '删除这条回复?',
|
|
3671
3730
|
'dialog.desc.message': '这条消息将从模型上下文中移除,并从当前转录中隐藏。原始会话日志保持不变。',
|
|
3672
|
-
'dialog.desc.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
3742
|
-
//
|
|
3743
|
-
//
|
|
3744
|
-
//
|
|
3745
|
-
'.dshdt-
|
|
3746
|
-
'[data-dshdt-
|
|
3747
|
-
'.dshdt-
|
|
3748
|
-
'.dshdt-
|
|
3749
|
-
'.dshdt-
|
|
3750
|
-
'.dshdt-
|
|
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
|
|
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
|
|
3866
|
+
const TURN_ICON_MARKUP =
|
|
3796
3867
|
'<svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true">' +
|
|
3797
|
-
|
|
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
|
-
|
|
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
|
|
3899
|
-
for (const seq of Array.isArray(data.
|
|
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,
|
|
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
|
-
|
|
3935
|
-
|
|
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 -
|
|
4039
|
-
//
|
|
4040
|
-
|
|
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: '
|
|
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
|
|
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
|
|
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
|
|
4307
|
+
let turn = null
|
|
4219
4308
|
if (kind !== 'think') {
|
|
4220
|
-
|
|
4221
|
-
if (
|
|
4222
|
-
|
|
4223
|
-
|
|
4224
|
-
|
|
4225
|
-
|
|
4226
|
-
host.appendChild(
|
|
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,
|
|
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,
|
|
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,
|
|
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.
|
|
4424
|
+
controller.trigger(target)
|
|
4336
4425
|
}
|
|
4337
|
-
// The second action is wired only where a
|
|
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 (
|
|
4341
|
-
const
|
|
4342
|
-
if (
|
|
4343
|
-
|
|
4344
|
-
|
|
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
|
-
|
|
4347
|
-
|
|
4435
|
+
turn.onclick =
|
|
4436
|
+
turnTarget === null || turnTarget === undefined
|
|
4348
4437
|
? null
|
|
4349
4438
|
: (event) => {
|
|
4350
4439
|
event.preventDefault()
|
|
4351
4440
|
event.stopPropagation()
|
|
4352
|
-
controller.
|
|
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.
|
|
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
|
|
4467
|
-
deletable &&
|
|
4468
|
-
if (deletable && target !== null) injectRowAction(row, node, target,
|
|
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 &&
|
|
4471
|
-
else delete row.dataset.
|
|
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.
|
|
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.
|
|
5695
|
+
const PLUGIN_VERSION = '0.2.12'
|
|
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.
|
|
29
|
+
export const STUDIO_VERSION = '0.2.12'
|
|
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.
|
|
51
|
+
const PLUGIN_VERSION = '0.2.12'
|
|
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.
|
|
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.
|
|
87
|
+
'action.tooltip.turn': '删除这一轮',
|
|
29
88
|
'action.tooltip.step': '删除这一步',
|
|
30
89
|
'action.tooltip.reply': '删除这条回复',
|
|
31
90
|
'dialog.title.message': '删除这条消息?',
|
|
32
|
-
'dialog.title.
|
|
91
|
+
'dialog.title.turn': '删除这一轮?',
|
|
33
92
|
'dialog.title.step': '删除这一步?',
|
|
34
93
|
'dialog.title.reply': '删除这条回复?',
|
|
35
94
|
'dialog.desc.message': '这条消息将从模型上下文中移除,并从当前转录中隐藏。原始会话日志保持不变。',
|
|
36
|
-
'dialog.desc.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
106
|
-
//
|
|
107
|
-
//
|
|
108
|
-
//
|
|
109
|
-
'.dshdt-
|
|
110
|
-
'[data-dshdt-
|
|
111
|
-
'.dshdt-
|
|
112
|
-
'.dshdt-
|
|
113
|
-
'.dshdt-
|
|
114
|
-
'.dshdt-
|
|
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
|
|
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
|
|
230
|
+
const TURN_ICON_MARKUP =
|
|
160
231
|
'<svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true">' +
|
|
161
|
-
|
|
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
|
-
|
|
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
|
|
263
|
-
for (const seq of Array.isArray(data.
|
|
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,
|
|
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
|
-
|
|
299
|
-
|
|
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 -
|
|
403
|
-
//
|
|
404
|
-
|
|
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: '
|
|
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
|
|
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
|
|
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
|
|
671
|
+
let turn = null
|
|
583
672
|
if (kind !== 'think') {
|
|
584
|
-
|
|
585
|
-
if (
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
host.appendChild(
|
|
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,
|
|
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,
|
|
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,
|
|
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.
|
|
788
|
+
controller.trigger(target)
|
|
700
789
|
}
|
|
701
|
-
// The second action is wired only where a
|
|
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 (
|
|
705
|
-
const
|
|
706
|
-
if (
|
|
707
|
-
|
|
708
|
-
|
|
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
|
-
|
|
711
|
-
|
|
799
|
+
turn.onclick =
|
|
800
|
+
turnTarget === null || turnTarget === undefined
|
|
712
801
|
? null
|
|
713
802
|
: (event) => {
|
|
714
803
|
event.preventDefault()
|
|
715
804
|
event.stopPropagation()
|
|
716
|
-
controller.
|
|
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.
|
|
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
|
|
831
|
-
deletable &&
|
|
832
|
-
if (deletable && target !== null) injectRowAction(row, node, target,
|
|
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 &&
|
|
835
|
-
else delete row.dataset.
|
|
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.
|
|
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
|