dsh-sidecard-ask 1.5.1 → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +65 -0
- package/README.md +34 -11
- package/client.js +791 -26
- package/index.js +183 -4
- package/package.json +1 -1
package/client.js
CHANGED
|
@@ -47,7 +47,7 @@ window.__ModuleLoader__.load({
|
|
|
47
47
|
* the Host and the module's `api.version` both read it, so a report can
|
|
48
48
|
* never claim a generation the browser is not actually running.
|
|
49
49
|
*/
|
|
50
|
-
const CLIENT_VERSION = '1.
|
|
50
|
+
const CLIENT_VERSION = '1.7.0'
|
|
51
51
|
|
|
52
52
|
/**
|
|
53
53
|
* Tab kind served by the DSH native right rail (also its implementation id).
|
|
@@ -98,6 +98,42 @@ window.__ModuleLoader__.load({
|
|
|
98
98
|
sideProvider: 'auto',
|
|
99
99
|
floatMode: 'capsule',
|
|
100
100
|
floatMaxWidth: 340,
|
|
101
|
+
// Mirrors the Host's DEFAULT_CONFIG.quickActions (contract-asserted):
|
|
102
|
+
// `builtin:*` entries are resolved through QUICK_ACTION_BUILTINS, while
|
|
103
|
+
// custom entries (no builtin prefix) carry their own label/question.
|
|
104
|
+
quickActions: [
|
|
105
|
+
{ id: 'builtin:explain', enabled: true, shortcut: '' },
|
|
106
|
+
{ id: 'builtin:translate', enabled: true, shortcut: '' },
|
|
107
|
+
{ id: 'builtin:summarize', enabled: true, shortcut: '' },
|
|
108
|
+
{ id: 'builtin:polish', enabled: true, shortcut: '' },
|
|
109
|
+
{ id: 'builtin:fix', enabled: true, shortcut: '' },
|
|
110
|
+
],
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Bounds for quick-action editing, mirrored by the Host validator. The
|
|
115
|
+
* settings page refuses anything outside these before it ever posts.
|
|
116
|
+
*/
|
|
117
|
+
const QUICK_ACTION_LIMITS = {
|
|
118
|
+
maxEntries: 12,
|
|
119
|
+
idLength: 40,
|
|
120
|
+
labelLength: 20,
|
|
121
|
+
questionLength: 500,
|
|
122
|
+
shortcutLength: 40,
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Built-in quick actions. The config list only carries `builtin:<id>`;
|
|
127
|
+
* the localized label and question live HERE so the wire/config layers
|
|
128
|
+
* stay language-neutral. Questions are instructions only — the Host
|
|
129
|
+
* fences the selection separately, so none of them needs to quote text.
|
|
130
|
+
*/
|
|
131
|
+
const QUICK_ACTION_BUILTINS = {
|
|
132
|
+
'builtin:explain': { labelKey: 'qaExplain', questionKey: 'qaExplainQ' },
|
|
133
|
+
'builtin:translate': { labelKey: 'qaTranslate', questionKey: 'qaTranslateQ' },
|
|
134
|
+
'builtin:summarize': { labelKey: 'qaSummarize', questionKey: 'qaSummarizeQ' },
|
|
135
|
+
'builtin:polish': { labelKey: 'qaPolish', questionKey: 'qaPolishQ' },
|
|
136
|
+
'builtin:fix': { labelKey: 'qaFix', questionKey: 'qaFixQ' },
|
|
101
137
|
}
|
|
102
138
|
|
|
103
139
|
/** Zone classification: slot-key patterns the harness itself renders. */
|
|
@@ -294,6 +330,43 @@ window.__ModuleLoader__.load({
|
|
|
294
330
|
none: '无',
|
|
295
331
|
unsupportedTrigger: '无法识别该快捷键,请用形如 Alt+Q / Ctrl+Shift+K 的写法',
|
|
296
332
|
surfaceUnavailable: '所选侧边承载面不可用',
|
|
333
|
+
familySuppressed: '检测到同系列更新版本({version})已在本页运行,本实例已自动停用划词触发与快捷键,界面由更新版本接管。建议在插件页卸载本旧版本。',
|
|
334
|
+
familyLegacyToast: '检测到旧版 dsh-selection-followup 同时启用,已由本版本接管;建议卸载旧包',
|
|
335
|
+
familyLegacyDiag: '旧版共载',
|
|
336
|
+
familyPeers: '同页实例',
|
|
337
|
+
retryEdit: '改问',
|
|
338
|
+
editQuestionPlaceholder: '修改问题后重新提问…',
|
|
339
|
+
retryAskSubmit: '重新提问',
|
|
340
|
+
copyCode: '复制代码',
|
|
341
|
+
qaExplain: '解释',
|
|
342
|
+
qaExplainQ: '请用简洁的语言解释这段内容的含义。',
|
|
343
|
+
qaTranslate: '翻译',
|
|
344
|
+
qaTranslateQ: '请把这段内容翻译成中文;如果原文已经是中文,则翻译成英文。只输出译文,不要加解释。',
|
|
345
|
+
qaSummarize: '总结',
|
|
346
|
+
qaSummarizeQ: '请用简短的要点总结这段内容。',
|
|
347
|
+
qaPolish: '润色',
|
|
348
|
+
qaPolishQ: '请润色这段文字:保持原意、修正语病与不通顺之处,只输出润色后的内容。',
|
|
349
|
+
qaFix: '纠错',
|
|
350
|
+
qaFixQ: '请检查这段内容中的事实性、逻辑性与文字错误,逐条列出问题并给出修正建议。',
|
|
351
|
+
setQuickActions: '快捷动作',
|
|
352
|
+
setQuickActionsHint: '选中文字后显示的一键动作。自定义问题中的 {selection} 会替换为选中内容;不写占位符时,原文会随问题自动附上。',
|
|
353
|
+
qaEnabled: '启用',
|
|
354
|
+
qaAdd: '+ 添加动作',
|
|
355
|
+
qaDelete: '删除',
|
|
356
|
+
qaBuiltinBadge: '内置',
|
|
357
|
+
qaLabelPlaceholder: '动作名称(如:英译中)',
|
|
358
|
+
qaQuestionPlaceholder: '问题指令,可用 {selection} 引用选中内容',
|
|
359
|
+
qaShortcutPlaceholder: '如 Alt+T,留空表示无快捷键',
|
|
360
|
+
qaTooMany: '快捷动作最多 {n} 条',
|
|
361
|
+
qaMalformed: '存在格式不正确的动作',
|
|
362
|
+
qaBadId: '动作 id 缺失或超长',
|
|
363
|
+
qaDuplicateId: '存在重复的动作 id',
|
|
364
|
+
qaBadLabel: '动作名称必填,且不超过 {n} 个字符',
|
|
365
|
+
qaBadQuestion: '问题指令必填,且不超过 {n} 个字符',
|
|
366
|
+
qaBadShortcut: '“{label}”的快捷键格式不正确,请用 Alt+Q 这样的写法',
|
|
367
|
+
qaDuplicateShortcut: '“{label}”的快捷键与“{other}”冲突',
|
|
368
|
+
familyLegacyDiag: '旧版共载',
|
|
369
|
+
familyPeers: '同页实例',
|
|
297
370
|
},
|
|
298
371
|
en: {
|
|
299
372
|
triggerLabel: 'Ask about selection',
|
|
@@ -430,6 +503,41 @@ window.__ModuleLoader__.load({
|
|
|
430
503
|
none: 'none',
|
|
431
504
|
unsupportedTrigger: 'Unrecognized shortcut — use a form like Alt+Q / Ctrl+Shift+K',
|
|
432
505
|
surfaceUnavailable: 'The chosen side surface is unavailable',
|
|
506
|
+
familySuppressed: 'A newer family build ({version}) is already running on this page; this copy has disabled its trigger and shortcut so only one UI answers. Uninstall this older package.',
|
|
507
|
+
familyLegacyToast: 'Legacy dsh-selection-followup detected alongside this plugin; this build took over. Uninstall the old package.',
|
|
508
|
+
familyLegacyDiag: 'Legacy co-load',
|
|
509
|
+
familyPeers: 'Copies on page',
|
|
510
|
+
retryEdit: 'Edit & retry',
|
|
511
|
+
editQuestionPlaceholder: 'Edit the question, then ask again…',
|
|
512
|
+
retryAskSubmit: 'Ask again',
|
|
513
|
+
copyCode: 'Copy code',
|
|
514
|
+
qaExplain: 'Explain',
|
|
515
|
+
qaExplainQ: 'Explain what this passage means, concisely.',
|
|
516
|
+
qaTranslate: 'Translate',
|
|
517
|
+
qaTranslateQ: 'Translate this passage into English; if it is already in English, translate it into Chinese. Output the translation only, with no explanation.',
|
|
518
|
+
qaSummarize: 'Summarize',
|
|
519
|
+
qaSummarizeQ: 'Summarize the key points of this passage briefly.',
|
|
520
|
+
qaPolish: 'Polish',
|
|
521
|
+
qaPolishQ: 'Polish this text: keep the original meaning, fix grammar and awkward phrasing, and output only the polished version.',
|
|
522
|
+
qaFix: 'Proofread',
|
|
523
|
+
qaFixQ: 'Check this passage for factual, logical, and writing errors. List each issue with a suggested fix.',
|
|
524
|
+
setQuickActions: 'Quick actions',
|
|
525
|
+
setQuickActionsHint: 'One-click actions shown after selecting text. In a custom question, {selection} is replaced by the selected text; without it, the source text is attached automatically.',
|
|
526
|
+
qaEnabled: 'On',
|
|
527
|
+
qaAdd: '+ Add action',
|
|
528
|
+
qaDelete: 'Delete',
|
|
529
|
+
qaBuiltinBadge: 'built-in',
|
|
530
|
+
qaLabelPlaceholder: 'Action name (e.g. EN→ZH)',
|
|
531
|
+
qaQuestionPlaceholder: 'Question; use {selection} for the selected text',
|
|
532
|
+
qaShortcutPlaceholder: 'e.g. Alt+T; blank for none',
|
|
533
|
+
qaTooMany: 'At most {n} quick actions',
|
|
534
|
+
qaMalformed: 'A malformed action exists',
|
|
535
|
+
qaBadId: 'An action id is missing or too long',
|
|
536
|
+
qaDuplicateId: 'Duplicate action ids exist',
|
|
537
|
+
qaBadLabel: 'Action names are required and at most {n} characters',
|
|
538
|
+
qaBadQuestion: 'Questions are required and at most {n} characters',
|
|
539
|
+
qaBadShortcut: 'The shortcut for "{label}" is invalid; use a form like Alt+Q',
|
|
540
|
+
qaDuplicateShortcut: 'The shortcut for "{label}" conflicts with "{other}"',
|
|
433
541
|
},
|
|
434
542
|
}
|
|
435
543
|
|
|
@@ -659,6 +767,12 @@ window.__ModuleLoader__.load({
|
|
|
659
767
|
zoneAnchors: null,
|
|
660
768
|
/** Which ladder rung each registration actually landed on. */
|
|
661
769
|
slots: {},
|
|
770
|
+
/** §4.5 family election: this copy owns the interaction surface. */
|
|
771
|
+
familyLeader: true,
|
|
772
|
+
/** Other cooperating copies currently live in this page. */
|
|
773
|
+
familyPeers: [],
|
|
774
|
+
/** A non-cooperating legacy build (≤1.0.x) is present and suppressed. */
|
|
775
|
+
familyLegacy: false,
|
|
662
776
|
revision: 0,
|
|
663
777
|
},
|
|
664
778
|
listeners: new Set(),
|
|
@@ -686,6 +800,94 @@ window.__ModuleLoader__.load({
|
|
|
686
800
|
return store.state
|
|
687
801
|
}
|
|
688
802
|
|
|
803
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
804
|
+
// §4.5 family singleton
|
|
805
|
+
//
|
|
806
|
+
// The plugin shipped under two npm names (`dsh-selection-followup` ≤1.0.1
|
|
807
|
+
// and `dsh-sidecard-ask` ≥1.1.0), and a profile can accidentally enable
|
|
808
|
+
// BOTH bundles — or the same bundle twice. Two live copies used to mean
|
|
809
|
+
// two trigger buttons stacked on the selection and two Alt+Q popovers.
|
|
810
|
+
//
|
|
811
|
+
// Cooperating copies (any build with this section) share one registry on
|
|
812
|
+
// `window` and elect a single leader by version (ties → first loaded);
|
|
813
|
+
// non-leaders keep their settings page but mount NO interaction surface.
|
|
814
|
+
//
|
|
815
|
+
// The old 1.0.x build cannot cooperate: it is detected structurally (its
|
|
816
|
+
// overlay carries the same `dsa-layer` class without an owner marker), the
|
|
817
|
+
// leader hides its UI with one stylesheet rule, and the user is told to
|
|
818
|
+
// uninstall the old package.
|
|
819
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
820
|
+
|
|
821
|
+
const FAMILY_WINDOW_KEY = '__dshSidecardFamily'
|
|
822
|
+
const INSTANCE_ID = `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`
|
|
823
|
+
const OWNER_TOKEN = `${PLUGIN_ID}@${CLIENT_VERSION}#${INSTANCE_ID}`
|
|
824
|
+
|
|
825
|
+
/** Compare dotted numeric versions; non-numeric parts sort first. */
|
|
826
|
+
function compareVersions(a, b) {
|
|
827
|
+
const parts = v => String(v).split(/[.+-]/).map(part => (/^\d+$/.test(part) ? Number(part) : -1))
|
|
828
|
+
const pa = parts(a)
|
|
829
|
+
const pb = parts(b)
|
|
830
|
+
for (let i = 0; i < Math.max(pa.length, pb.length); i += 1) {
|
|
831
|
+
const x = pa[i] ?? -1
|
|
832
|
+
const y = pb[i] ?? -1
|
|
833
|
+
if (x !== y) return x < y ? -1 : 1
|
|
834
|
+
}
|
|
835
|
+
return 0
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
/**
|
|
839
|
+
* Elect the leader: highest version wins; a tie goes to the member that
|
|
840
|
+
* joined first (Map insertion order). Pure so the contract test can pin it.
|
|
841
|
+
* @returns {string} the winning member id.
|
|
842
|
+
*/
|
|
843
|
+
function electFamilyLeader(members, selfId) {
|
|
844
|
+
let winner = null
|
|
845
|
+
for (const member of members) {
|
|
846
|
+
// Strictly greater version replaces; an equal version keeps the member
|
|
847
|
+
// that joined earlier (Map insertion order = load order).
|
|
848
|
+
if (winner === null || compareVersions(member.version, winner.version) > 0) {
|
|
849
|
+
winner = member
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
return winner?.id ?? selfId
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
/** Fetch-or-create the cross-copy registry (survives HMR on `window`). */
|
|
856
|
+
function familyTable(win) {
|
|
857
|
+
const existing = win[FAMILY_WINDOW_KEY]
|
|
858
|
+
if (existing !== undefined && existing !== null && typeof existing.subscribe === 'function') {
|
|
859
|
+
return existing
|
|
860
|
+
}
|
|
861
|
+
const members = new Map()
|
|
862
|
+
const listeners = new Set()
|
|
863
|
+
const emit = () => { for (const fn of [...listeners]) { try { fn() } catch { /* peer listener */ } } }
|
|
864
|
+
const table = {
|
|
865
|
+
join(member) { members.set(member.id, member); emit() },
|
|
866
|
+
leave(id) { members.delete(id); emit() },
|
|
867
|
+
list: () => [...members.values()],
|
|
868
|
+
subscribe(fn) { listeners.add(fn); return () => listeners.delete(fn) },
|
|
869
|
+
}
|
|
870
|
+
try {
|
|
871
|
+
Object.defineProperty(win, FAMILY_WINDOW_KEY, { value: table, configurable: true, writable: true })
|
|
872
|
+
} catch {
|
|
873
|
+
win[FAMILY_WINDOW_KEY] = table
|
|
874
|
+
}
|
|
875
|
+
return table
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
/**
|
|
879
|
+
* Find legacy (≤1.0.x, non-cooperating) family UI: a `dsa-layer` carrying
|
|
880
|
+
* a trigger button but no owner marker. Every layer this build renders
|
|
881
|
+
* stamps `data-dsa-owner`, so an unmarked layer can only be the old build.
|
|
882
|
+
*/
|
|
883
|
+
function findLegacyLayers(root) {
|
|
884
|
+
if (typeof root.querySelectorAll !== 'function') return []
|
|
885
|
+
try {
|
|
886
|
+
return [...root.querySelectorAll('.dsa-layer:not([data-dsa-owner])')]
|
|
887
|
+
.filter(node => node.querySelector?.('.dsa-trigger, .dsa-pop') !== null)
|
|
888
|
+
} catch { return [] }
|
|
889
|
+
}
|
|
890
|
+
|
|
689
891
|
/**
|
|
690
892
|
* In-session override of the configured floating-layer shape. Deliberately
|
|
691
893
|
* NOT persisted: the settings page owns the durable default, and a reload
|
|
@@ -926,6 +1128,134 @@ window.__ModuleLoader__.load({
|
|
|
926
1128
|
&& event.metaKey === facts.meta
|
|
927
1129
|
}
|
|
928
1130
|
|
|
1131
|
+
/**
|
|
1132
|
+
* Resolve the configured quick actions into renderable, ENABLED entries.
|
|
1133
|
+
* The Host validator keeps a saved list well-formed; this is the
|
|
1134
|
+
* render-time guard for an in-memory/edited draft: unknown `builtin:*`
|
|
1135
|
+
* ids and custom entries missing label/question are skipped.
|
|
1136
|
+
* @param {object|Array} configOrList - the effective config or a raw list.
|
|
1137
|
+
* @returns {Array<{id:string,label:string,question:string,shortcut:string,builtin:boolean}>}
|
|
1138
|
+
*/
|
|
1139
|
+
function quickActionsOf(configOrList) {
|
|
1140
|
+
const list = Array.isArray(configOrList) ? configOrList : configOrList?.quickActions
|
|
1141
|
+
if (!Array.isArray(list)) return []
|
|
1142
|
+
const out = []
|
|
1143
|
+
for (const entry of list) {
|
|
1144
|
+
if (entry === null || typeof entry !== 'object') continue
|
|
1145
|
+
if (entry.enabled === false) continue
|
|
1146
|
+
const id = String(entry.id ?? '')
|
|
1147
|
+
const meta = QUICK_ACTION_BUILTINS[id]
|
|
1148
|
+
if (meta !== undefined) {
|
|
1149
|
+
out.push({
|
|
1150
|
+
id,
|
|
1151
|
+
label: t(meta.labelKey),
|
|
1152
|
+
question: t(meta.questionKey),
|
|
1153
|
+
shortcut: typeof entry.shortcut === 'string' ? entry.shortcut : '',
|
|
1154
|
+
builtin: true,
|
|
1155
|
+
})
|
|
1156
|
+
continue
|
|
1157
|
+
}
|
|
1158
|
+
// An unknown builtin id resolves to nothing the client knows.
|
|
1159
|
+
if (id.startsWith('builtin:')) continue
|
|
1160
|
+
if (typeof entry.label === 'string' && entry.label.trim() !== ''
|
|
1161
|
+
&& typeof entry.question === 'string' && entry.question.trim() !== '') {
|
|
1162
|
+
out.push({
|
|
1163
|
+
id,
|
|
1164
|
+
label: entry.label,
|
|
1165
|
+
question: entry.question,
|
|
1166
|
+
shortcut: typeof entry.shortcut === 'string' ? entry.shortcut : '',
|
|
1167
|
+
builtin: false,
|
|
1168
|
+
})
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
return out
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
/**
|
|
1175
|
+
* The question actually sent for one quick action. A template containing
|
|
1176
|
+
* `{selection}` embeds the (truncated) selection inline; a template
|
|
1177
|
+
* without it is sent as-is, because the Host fences the selection in
|
|
1178
|
+
* every prompt regardless.
|
|
1179
|
+
* @param {{question?: string}} action - a resolved quick action.
|
|
1180
|
+
* @param {string} selection - the raw selected text.
|
|
1181
|
+
* @param {number} maxChars - the selection truncation budget.
|
|
1182
|
+
* @returns {string}
|
|
1183
|
+
*/
|
|
1184
|
+
function renderActionQuestion(action, selection, maxChars) {
|
|
1185
|
+
const template = String(action?.question ?? '')
|
|
1186
|
+
if (!template.includes('{selection}')) return template
|
|
1187
|
+
const cut = truncateSelection(String(selection ?? ''), maxChars)
|
|
1188
|
+
return template.split('{selection}').join(cut.text)
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
/** Canonical fingerprint of an accelerator, for duplicate detection. */
|
|
1192
|
+
function shortcutFingerprint(accelerator) {
|
|
1193
|
+
const facts = parseShortcut(accelerator)
|
|
1194
|
+
if (facts === null) return null
|
|
1195
|
+
return `${facts.ctrl ? 1 : 0}|${facts.alt ? 1 : 0}|${facts.shift ? 1 : 0}|${facts.meta ? 1 : 0}|${facts.key}`
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
/**
|
|
1199
|
+
* Validate a settings-page quick-action draft BEFORE posting it, so the
|
|
1200
|
+
* Host's all-or-nothing refusal never comes back as a surprise. Also
|
|
1201
|
+
* checks that no action shortcut collides with another action or with the
|
|
1202
|
+
* main popover shortcut.
|
|
1203
|
+
* @param {unknown} list - the draft `quickActions` array.
|
|
1204
|
+
* @param {string} mainShortcut - the configured popover shortcut.
|
|
1205
|
+
* @returns {{ok: true}|{ok: false, key: string, vars?: object}}
|
|
1206
|
+
*/
|
|
1207
|
+
function validateQuickActionsDraft(list, mainShortcut) {
|
|
1208
|
+
if (!Array.isArray(list) || list.length > QUICK_ACTION_LIMITS.maxEntries) {
|
|
1209
|
+
return { ok: false, key: 'qaTooMany' }
|
|
1210
|
+
}
|
|
1211
|
+
const ids = new Set()
|
|
1212
|
+
const taken = new Map()
|
|
1213
|
+
const mainFingerprint = shortcutFingerprint(mainShortcut)
|
|
1214
|
+
if (mainFingerprint !== null) taken.set(mainFingerprint, '')
|
|
1215
|
+
const labelOf = (entry) => {
|
|
1216
|
+
const meta = QUICK_ACTION_BUILTINS[String(entry.id ?? '')]
|
|
1217
|
+
return meta !== undefined ? t(meta.labelKey) : String(entry.label ?? '')
|
|
1218
|
+
}
|
|
1219
|
+
for (const entry of list) {
|
|
1220
|
+
if (entry === null || typeof entry !== 'object' || Array.isArray(entry)) {
|
|
1221
|
+
return { ok: false, key: 'qaMalformed' }
|
|
1222
|
+
}
|
|
1223
|
+
const id = String(entry.id ?? '')
|
|
1224
|
+
if (id === '' || id.length > QUICK_ACTION_LIMITS.idLength) return { ok: false, key: 'qaBadId' }
|
|
1225
|
+
if (ids.has(id)) return { ok: false, key: 'qaDuplicateId' }
|
|
1226
|
+
ids.add(id)
|
|
1227
|
+
const meta = QUICK_ACTION_BUILTINS[id]
|
|
1228
|
+
const label = labelOf(entry)
|
|
1229
|
+
if (meta === undefined && !id.startsWith('builtin:')) {
|
|
1230
|
+
const customLabel = String(entry.label ?? '')
|
|
1231
|
+
const customQuestion = String(entry.question ?? '')
|
|
1232
|
+
if (customLabel.trim() === '' || customLabel.length > QUICK_ACTION_LIMITS.labelLength) {
|
|
1233
|
+
return { ok: false, key: 'qaBadLabel', vars: { n: QUICK_ACTION_LIMITS.labelLength } }
|
|
1234
|
+
}
|
|
1235
|
+
if (customQuestion.trim() === '' || customQuestion.length > QUICK_ACTION_LIMITS.questionLength) {
|
|
1236
|
+
return { ok: false, key: 'qaBadQuestion', vars: { n: QUICK_ACTION_LIMITS.questionLength } }
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1239
|
+
const shortcut = String(entry.shortcut ?? '').trim()
|
|
1240
|
+
if (shortcut !== '') {
|
|
1241
|
+
if (parseShortcut(shortcut) === null) {
|
|
1242
|
+
return { ok: false, key: 'qaBadShortcut', vars: { label } }
|
|
1243
|
+
}
|
|
1244
|
+
const fingerprint = shortcutFingerprint(shortcut)
|
|
1245
|
+
if (taken.has(fingerprint)) {
|
|
1246
|
+
const owner = taken.get(fingerprint)
|
|
1247
|
+
return {
|
|
1248
|
+
ok: false,
|
|
1249
|
+
key: 'qaDuplicateShortcut',
|
|
1250
|
+
vars: { label, other: owner === '' ? t('setShortcut') : owner },
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
taken.set(fingerprint, label)
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1256
|
+
return { ok: true }
|
|
1257
|
+
}
|
|
1258
|
+
|
|
929
1259
|
// ─────────────────────────────────────────────────────────────────────
|
|
930
1260
|
// §6 carriers
|
|
931
1261
|
// ─────────────────────────────────────────────────────────────────────
|
|
@@ -1210,6 +1540,12 @@ window.__ModuleLoader__.load({
|
|
|
1210
1540
|
...(surfaces.better.reason === null ? {} : { reason: String(surfaces.better.reason) }),
|
|
1211
1541
|
features: surfaces.better.features,
|
|
1212
1542
|
},
|
|
1543
|
+
// §4.5 family singleton verdict (host-side diagnostics only).
|
|
1544
|
+
family: {
|
|
1545
|
+
leader: state.familyLeader !== false,
|
|
1546
|
+
peers: Array.isArray(state.familyPeers) ? state.familyPeers : [],
|
|
1547
|
+
legacy: state.familyLegacy === true,
|
|
1548
|
+
},
|
|
1213
1549
|
}
|
|
1214
1550
|
}
|
|
1215
1551
|
|
|
@@ -1255,7 +1591,7 @@ window.__ModuleLoader__.load({
|
|
|
1255
1591
|
(\`html[data-platform] body>[data-dsh-better-sidebar]{-webkit-app-region:initial}\`
|
|
1256
1592
|
plus \`[data-dsh-panel-host]>*{-webkit-app-region:no-drag}\`).
|
|
1257
1593
|
Every surface we draw declares it. The property is inert in a browser. */
|
|
1258
|
-
.dsa-layer,.dsa-trigger,.dsa-pop,.dsa-float,.dsa-stack,.dsa-card,.dsa-toast,.dsa-settings{-webkit-app-region:no-drag}
|
|
1594
|
+
.dsa-layer,.dsa-trigger,.dsa-quickbar,.dsa-pop,.dsa-float,.dsa-stack,.dsa-card,.dsa-toast,.dsa-settings{-webkit-app-region:no-drag}
|
|
1259
1595
|
.dsa-layer{pointer-events:none;position:relative;z-index:60}
|
|
1260
1596
|
.dsa-trigger{pointer-events:auto;position:fixed;display:flex;align-items:center;gap:4px;
|
|
1261
1597
|
padding:4px 8px;border-radius:8px;border:1px solid var(--dsw-alias-border-l2, rgba(127,127,127,.35));
|
|
@@ -1264,6 +1600,26 @@ window.__ModuleLoader__.load({
|
|
|
1264
1600
|
font-family:inherit;white-space:nowrap}
|
|
1265
1601
|
.dsa-trigger:hover{border-color:var(--dsw-alias-brand-primary, #4c8dff)}
|
|
1266
1602
|
.dsa-trigger:focus-visible{outline:2px solid var(--dsw-alias-brand-primary, #4c8dff);outline-offset:1px}
|
|
1603
|
+
/* The one-click action bar: the trigger button plus compact chips, laid out
|
|
1604
|
+
in one wrapping row under the selection. The container is positioned by the
|
|
1605
|
+
component; chips keep their own pointer-events because .dsa-layer is a
|
|
1606
|
+
click-through layer. */
|
|
1607
|
+
.dsa-quickbar{pointer-events:auto;position:fixed;display:flex;align-items:center;gap:6px;
|
|
1608
|
+
max-width:min(620px, 92vw);flex-wrap:wrap;z-index:60}
|
|
1609
|
+
.dsa-chip{pointer-events:auto;padding:3px 10px;border-radius:999px;
|
|
1610
|
+
border:1px solid var(--dsw-alias-border-l2, rgba(127,127,127,.35));
|
|
1611
|
+
background:var(--dsw-alias-bg-overlay, #fff);color:var(--dsw-alias-label-primary, #111);
|
|
1612
|
+
font:inherit;font-size:12px;line-height:16px;cursor:pointer;white-space:nowrap;
|
|
1613
|
+
box-shadow:0 2px 8px rgba(0,0,0,.14)}
|
|
1614
|
+
.dsa-chip:hover{border-color:var(--dsw-alias-brand-primary, #4c8dff)}
|
|
1615
|
+
.dsa-chip:focus-visible{outline:2px solid var(--dsw-alias-brand-primary, #4c8dff);outline-offset:1px}
|
|
1616
|
+
/* Chips rendered INSIDE the popover need no floating shadow. */
|
|
1617
|
+
.dsa-pop-chips{display:flex;gap:6px;flex-wrap:wrap}
|
|
1618
|
+
.dsa-pop-chips .dsa-chip{box-shadow:none}
|
|
1619
|
+
/* Inside the trigger bar the main button and chips are one flex row: the
|
|
1620
|
+
trigger drops its own fixed positioning, chips join the row directly. */
|
|
1621
|
+
.dsa-trigger-embedded{position:static;box-shadow:0 2px 10px rgba(0,0,0,.18)}
|
|
1622
|
+
.dsa-quickbar-chips{display:contents}
|
|
1267
1623
|
.dsa-pop{pointer-events:auto;position:fixed;width:min(420px, 92vw);display:flex;flex-direction:column;gap:8px;
|
|
1268
1624
|
padding:12px;border-radius:12px;border:1px solid var(--dsw-alias-border-l2, rgba(127,127,127,.35));
|
|
1269
1625
|
background:var(--dsw-alias-bg-overlay, #fff);color:var(--dsw-alias-label-primary, #111);
|
|
@@ -1348,6 +1704,11 @@ window.__ModuleLoader__.load({
|
|
|
1348
1704
|
.dsa-table th,.dsa-table td{border:1px solid var(--dsw-alias-border-l2, rgba(127,127,127,.35));
|
|
1349
1705
|
padding:4px 8px;text-align:left;word-break:break-word;vertical-align:top}
|
|
1350
1706
|
.dsa-table th{background:var(--dsw-alias-bg-layer-2, rgba(127,127,127,.12));font-weight:600}
|
|
1707
|
+
.dsa-pre-wrap{position:relative;margin:0 0 6px}
|
|
1708
|
+
.dsa-pre-wrap .dsa-pre{margin:0}
|
|
1709
|
+
.dsa-code-copy{position:absolute;top:4px;right:4px;padding:1px 7px;line-height:16px;
|
|
1710
|
+
font-size:12px;opacity:.72;background:var(--dsw-alias-bg-overlay, #fff)}
|
|
1711
|
+
.dsa-code-copy:hover,.dsa-code-copy:focus-visible{opacity:1}
|
|
1351
1712
|
.dsa-pre{margin:0 0 6px;padding:8px;border-radius:8px;overflow:auto;
|
|
1352
1713
|
background:var(--dsw-alias-bg-layer-2, rgba(127,127,127,.12));font-size:12px;line-height:17px;
|
|
1353
1714
|
font-family:ui-monospace,SFMono-Regular,Menlo,monospace}
|
|
@@ -1366,8 +1727,25 @@ window.__ModuleLoader__.load({
|
|
|
1366
1727
|
background:var(--dsw-alias-bg-layer-1, transparent);color:inherit}
|
|
1367
1728
|
.dsa-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:10px}
|
|
1368
1729
|
.dsa-check{display:flex;align-items:center;gap:6px;font-size:12px}
|
|
1730
|
+
/* Quick-action editor (settings page): one wrapped row per action. */
|
|
1731
|
+
.dsa-qa{display:flex;flex-direction:column;gap:8px}
|
|
1732
|
+
.dsa-qa-row{display:flex;align-items:center;gap:8px;flex-wrap:wrap;
|
|
1733
|
+
padding:8px;border:1px solid var(--dsw-alias-border-l1, rgba(127,127,127,.3));border-radius:8px}
|
|
1734
|
+
.dsa-qa-row .dsa-input{flex:1;min-width:120px}
|
|
1735
|
+
.dsa-qa-question{flex-basis:100%!important;min-width:0!important}
|
|
1736
|
+
.dsa-qa-head{display:flex;align-items:center;gap:8px;font-size:12px;
|
|
1737
|
+
color:var(--dsw-alias-label-secondary, #555)}
|
|
1369
1738
|
.dsa-diag{display:flex;flex-direction:column;gap:2px;font-size:12px;
|
|
1370
1739
|
color:var(--dsw-alias-label-secondary, #555)}
|
|
1740
|
+
/* §4.5 legacy coexistence: an unmarked dsa-layer can only be the old
|
|
1741
|
+
dsh-selection-followup ≤1.0.x build. When this build is the elected
|
|
1742
|
+
leader it hides the old build's whole interaction surface so exactly
|
|
1743
|
+
one trigger button / popover answers the selection. Our own layer always
|
|
1744
|
+
carries data-dsa-owner, so this never matches current-generation UI. */
|
|
1745
|
+
.dsa-layer:not([data-dsa-owner]){display:none!important}
|
|
1746
|
+
.dsa-family-notice{padding:8px 10px;border-radius:8px;font-size:12px;
|
|
1747
|
+
border:1px solid var(--dsw-alias-state-warn-primary, #b8860b);
|
|
1748
|
+
color:var(--dsw-alias-state-warn-primary, #b8860b)}
|
|
1371
1749
|
`
|
|
1372
1750
|
|
|
1373
1751
|
/** Render the stylesheet as a React element (removed with its component). */
|
|
@@ -1442,7 +1820,33 @@ window.__ModuleLoader__.load({
|
|
|
1442
1820
|
i += 1
|
|
1443
1821
|
}
|
|
1444
1822
|
i += 1
|
|
1445
|
-
|
|
1823
|
+
const code = body.join('\n')
|
|
1824
|
+
// A per-block copy button: answers are code-heavy, and dragging a
|
|
1825
|
+
// selection across a scrollable <pre> used to miss lines. The
|
|
1826
|
+
// click closes over the code text (never re-read from the DOM), and
|
|
1827
|
+
// reuses the same clipboard API → selection fallback as card copy.
|
|
1828
|
+
nodes.push(h('div', { key: `f${key++}`, className: 'dsa-pre-wrap' },
|
|
1829
|
+
h('pre', { className: 'dsa-pre' }, h('code', null, code)),
|
|
1830
|
+
h('button', {
|
|
1831
|
+
type: 'button',
|
|
1832
|
+
className: 'dsa-btn dsa-btn-icon dsa-code-copy',
|
|
1833
|
+
title: t('copyCode'),
|
|
1834
|
+
'aria-label': t('copyCode'),
|
|
1835
|
+
onClick: (event) => {
|
|
1836
|
+
try {
|
|
1837
|
+
const host = event?.currentTarget ?? event?.target
|
|
1838
|
+
const wrap = typeof host?.closest === 'function' ? host.closest('.dsa-pre-wrap') : null
|
|
1839
|
+
const codeNode = wrap !== null && typeof wrap?.querySelector === 'function'
|
|
1840
|
+
? wrap.querySelector('code')
|
|
1841
|
+
: null
|
|
1842
|
+
void copyText(code, codeNode).then((result) => {
|
|
1843
|
+
toast(result === 'copied'
|
|
1844
|
+
? t('copied')
|
|
1845
|
+
: result === 'selected' ? t('copySelectedHint') : t('copyFailed'))
|
|
1846
|
+
})
|
|
1847
|
+
} catch { /* a clipboard failure must not break the card */ }
|
|
1848
|
+
},
|
|
1849
|
+
}, '⧉')))
|
|
1446
1850
|
continue
|
|
1447
1851
|
}
|
|
1448
1852
|
if (line.includes('|') && i + 1 < lines.length && isTableSplit(lines[i + 1])) {
|
|
@@ -1589,15 +1993,15 @@ window.__ModuleLoader__.load({
|
|
|
1589
1993
|
// ─────────────────────────────────────────────────────────────────────
|
|
1590
1994
|
|
|
1591
1995
|
/** The in-place trigger button floating at the selection. */
|
|
1592
|
-
function TriggerButton({ candidate, onOpen }) {
|
|
1996
|
+
function TriggerButton({ candidate, onOpen, embedded = false }) {
|
|
1593
1997
|
// The clamp width follows the live label: icon + gap + padding + border
|
|
1594
1998
|
// (~38px of chrome) plus a safety margin, over the estimated label width.
|
|
1595
1999
|
const width = Math.ceil(estimateLabelWidth(t('triggerLabel')) + 62)
|
|
1596
|
-
const box = clampBox(candidate.rect, width, 30, 'below')
|
|
2000
|
+
const box = embedded ? null : clampBox(candidate.rect, width, 30, 'below')
|
|
1597
2001
|
return h('button', {
|
|
1598
2002
|
type: 'button',
|
|
1599
|
-
className: 'dsa-trigger',
|
|
1600
|
-
style: { left: `${box.left}px`, top: `${box.top}px` },
|
|
2003
|
+
className: embedded ? 'dsa-trigger dsa-trigger-embedded' : 'dsa-trigger',
|
|
2004
|
+
style: embedded ? undefined : { left: `${box.left}px`, top: `${box.top}px` },
|
|
1601
2005
|
title: t('triggerHint'),
|
|
1602
2006
|
'aria-label': `${t('triggerLabel')}:${zoneLabel(candidate)}`,
|
|
1603
2007
|
onMouseDown: (event) => { event.preventDefault() },
|
|
@@ -1609,14 +2013,64 @@ window.__ModuleLoader__.load({
|
|
|
1609
2013
|
}, h('span', { 'aria-hidden': true }, '💬'), h('span', null, t('triggerLabel')))
|
|
1610
2014
|
}
|
|
1611
2015
|
|
|
2016
|
+
/**
|
|
2017
|
+
* Compact quick-action chips, shared by the in-place trigger bar and the
|
|
2018
|
+
* popover. The trigger bar lays chips out directly (`display:contents`);
|
|
2019
|
+
* the popover wraps them in its own borderless chip row.
|
|
2020
|
+
*/
|
|
2021
|
+
function QuickChips({ actions, onPick, inline = false }) {
|
|
2022
|
+
if (!Array.isArray(actions) || actions.length === 0) return null
|
|
2023
|
+
return h('div', { className: inline ? 'dsa-quickbar-chips' : 'dsa-pop-chips' },
|
|
2024
|
+
actions.map(action => h('button', {
|
|
2025
|
+
key: action.id,
|
|
2026
|
+
type: 'button',
|
|
2027
|
+
className: 'dsa-chip',
|
|
2028
|
+
title: typeof action.shortcut === 'string' && action.shortcut.trim() !== ''
|
|
2029
|
+
? `${action.question}\n${action.shortcut}`
|
|
2030
|
+
: action.question,
|
|
2031
|
+
'aria-label': `${action.label}:${action.question}`,
|
|
2032
|
+
onMouseDown: (event) => { event.preventDefault() },
|
|
2033
|
+
onClick: (event) => {
|
|
2034
|
+
event.preventDefault()
|
|
2035
|
+
event.stopPropagation()
|
|
2036
|
+
onPick(action)
|
|
2037
|
+
},
|
|
2038
|
+
}, action.label)))
|
|
2039
|
+
}
|
|
2040
|
+
|
|
2041
|
+
/**
|
|
2042
|
+
* The in-place trigger bar: the main "ask" button followed by every
|
|
2043
|
+
* enabled quick action. A chip click skips the popover entirely — the
|
|
2044
|
+
* question is fixed and the answer goes straight to the side card.
|
|
2045
|
+
*/
|
|
2046
|
+
function TriggerBar({ candidate, config, onOpen, onQuick }) {
|
|
2047
|
+
const actions = quickActionsOf(config)
|
|
2048
|
+
const mainWidth = Math.ceil(estimateLabelWidth(t('triggerLabel')) + 62)
|
|
2049
|
+
// Rough row width for viewport clamping: main button + 6px gap + an
|
|
2050
|
+
// estimate per chip (label + ~24px padding/chrome). Wrapped rows cost
|
|
2051
|
+
// height, not width, so the clamp stays usable even when it wraps.
|
|
2052
|
+
const chipsWidth = actions.reduce(
|
|
2053
|
+
(sum, action) => sum + Math.ceil(estimateLabelWidth(action.label) + 24), 0)
|
|
2054
|
+
const rowWidth = Math.min(620, mainWidth + (actions.length > 0 ? 6 + chipsWidth : 0))
|
|
2055
|
+
const box = clampBox(candidate.rect, rowWidth, 34, 'below')
|
|
2056
|
+
return h('div', {
|
|
2057
|
+
className: 'dsa-quickbar',
|
|
2058
|
+
style: { left: `${box.left}px`, top: `${box.top}px` },
|
|
2059
|
+
onMouseDown: event => event.stopPropagation(),
|
|
2060
|
+
},
|
|
2061
|
+
h(TriggerButton, { candidate, onOpen, embedded: true }),
|
|
2062
|
+
h(QuickChips, { actions, onPick: onQuick, inline: true }))
|
|
2063
|
+
}
|
|
2064
|
+
|
|
1612
2065
|
/** The question popover. */
|
|
1613
|
-
function AskPopover({ candidate, config, onClose, onSubmit }) {
|
|
2066
|
+
function AskPopover({ candidate, config, onClose, onSubmit, onQuick }) {
|
|
1614
2067
|
const [question, setQuestion] = React.useState('')
|
|
1615
2068
|
const [carrier, setCarrier] = React.useState(config.defaultCarrier)
|
|
1616
2069
|
const [busy, setBusy] = React.useState(false)
|
|
1617
2070
|
const areaRef = React.useRef(null)
|
|
1618
2071
|
const cut = truncateSelection(candidate.text, config.maxChars)
|
|
1619
|
-
const box = clampBox(candidate.rect, 420,
|
|
2072
|
+
const box = clampBox(candidate.rect, 420, 300, 'below')
|
|
2073
|
+
const quickActions = quickActionsOf(config)
|
|
1620
2074
|
|
|
1621
2075
|
React.useEffect(() => {
|
|
1622
2076
|
const area = areaRef.current
|
|
@@ -1663,6 +2117,10 @@ window.__ModuleLoader__.load({
|
|
|
1663
2117
|
codePointLength(candidate.text) > 400
|
|
1664
2118
|
? `${Array.from(candidate.text).slice(0, 400).join('')}…`
|
|
1665
2119
|
: candidate.text),
|
|
2120
|
+
// Same chips as the in-place bar: shortcut-only users reach them via
|
|
2121
|
+
// Alt+Q, and a click here submits directly (the popover closes because
|
|
2122
|
+
// submit/quickAsk both clear `popover`).
|
|
2123
|
+
h(QuickChips, { actions: quickActions, onPick: action => onQuick(action) }),
|
|
1666
2124
|
h('textarea', {
|
|
1667
2125
|
ref: areaRef,
|
|
1668
2126
|
className: 'dsa-textarea',
|
|
@@ -1737,8 +2195,8 @@ window.__ModuleLoader__.load({
|
|
|
1737
2195
|
reasoning, renderRichText(card.text))
|
|
1738
2196
|
}
|
|
1739
2197
|
|
|
1740
|
-
/** Card footer: status, copy, follow-up, retry, close. */
|
|
1741
|
-
function CardActions({ card, onCopy, onFollowUp, onRetry, onClose, onCancel, onSendToMain }) {
|
|
2198
|
+
/** Card footer: status, copy, follow-up, retry/edit-retry, close. */
|
|
2199
|
+
function CardActions({ card, onCopy, onFollowUp, onRetry, onRetryEdit, onClose, onCancel, onSendToMain }) {
|
|
1742
2200
|
const terminal = card.status === 'done' || card.status === 'error' || card.status === 'stopped'
|
|
1743
2201
|
return h('div', { className: 'dsa-row' },
|
|
1744
2202
|
h('span', { className: 'dsa-muted' },
|
|
@@ -1748,9 +2206,15 @@ window.__ModuleLoader__.load({
|
|
|
1748
2206
|
card.status === 'streaming'
|
|
1749
2207
|
? h('button', { type: 'button', className: 'dsa-btn', onClick: onCancel }, t('cancelAnswer'))
|
|
1750
2208
|
: null,
|
|
1751
|
-
terminal && card.status !== 'done'
|
|
2209
|
+
terminal && card.status !== 'done' && card.carrier === 'side'
|
|
1752
2210
|
? h('button', { type: 'button', className: 'dsa-btn', onClick: onRetry }, t('retry'))
|
|
1753
2211
|
: null,
|
|
2212
|
+
// Edit & retry is available for EVERY terminal side card: a typo in
|
|
2213
|
+
// the question used to invalidate the whole card (plain retry only
|
|
2214
|
+
// resends verbatim), and an error card can be corrected the same way.
|
|
2215
|
+
terminal && card.carrier === 'side'
|
|
2216
|
+
? h('button', { type: 'button', className: 'dsa-btn', onClick: onRetryEdit }, t('retryEdit'))
|
|
2217
|
+
: null,
|
|
1754
2218
|
card.status === 'error' && card.carrier === 'side'
|
|
1755
2219
|
? h('button', { type: 'button', className: 'dsa-btn', onClick: onSendToMain }, t('sendToMain'))
|
|
1756
2220
|
: null,
|
|
@@ -1776,9 +2240,14 @@ window.__ModuleLoader__.load({
|
|
|
1776
2240
|
* (the layer is the only surface that can cover the conversation). A host
|
|
1777
2241
|
* tab that embeds the same card passes no callback and gets no buttons.
|
|
1778
2242
|
*/
|
|
1779
|
-
function AnswerCard({ card, surfaceLabel, onCopy, onFollowUp, onRetry, onClose, onCancel, onSendToMain, onFloatMode }) {
|
|
2243
|
+
function AnswerCard({ card, surfaceLabel, onCopy, onFollowUp, onRetry, onRetryEdit, onClose, onCancel, onSendToMain, onFloatMode }) {
|
|
1780
2244
|
const [followOpen, setFollowOpen] = React.useState(false)
|
|
1781
2245
|
const [draft, setDraft] = React.useState('')
|
|
2246
|
+
// Edit-&-retry editor: prefilled with the card's CURRENT question and
|
|
2247
|
+
// kept separate from the follow-up draft (follow-up appends a turn;
|
|
2248
|
+
// edit-retry regenerates THIS turn, keeping the card's earlier turns).
|
|
2249
|
+
const [editOpen, setEditOpen] = React.useState(false)
|
|
2250
|
+
const [editDraft, setEditDraft] = React.useState('')
|
|
1782
2251
|
const answerRef = React.useRef(null)
|
|
1783
2252
|
const config = store.state.config
|
|
1784
2253
|
// Icon-only, so the header row keeps one line: the full labels
|
|
@@ -1830,12 +2299,60 @@ window.__ModuleLoader__.load({
|
|
|
1830
2299
|
},
|
|
1831
2300
|
}))
|
|
1832
2301
|
: null,
|
|
2302
|
+
editOpen
|
|
2303
|
+
? h('div', { className: 'dsa-field' },
|
|
2304
|
+
h('textarea', {
|
|
2305
|
+
className: 'dsa-textarea',
|
|
2306
|
+
value: editDraft,
|
|
2307
|
+
placeholder: t('editQuestionPlaceholder'),
|
|
2308
|
+
autoFocus: true,
|
|
2309
|
+
onChange: event => setEditDraft(event.target.value),
|
|
2310
|
+
onKeyDown: (event) => {
|
|
2311
|
+
if (event.key === 'Enter' && !event.shiftKey && !event.isComposing) {
|
|
2312
|
+
event.preventDefault()
|
|
2313
|
+
const text = editDraft.trim()
|
|
2314
|
+
if (text === '') return
|
|
2315
|
+
setEditDraft('')
|
|
2316
|
+
setEditOpen(false)
|
|
2317
|
+
onRetryEdit(text)
|
|
2318
|
+
}
|
|
2319
|
+
if (event.key === 'Escape') {
|
|
2320
|
+
event.stopPropagation()
|
|
2321
|
+
setEditOpen(false)
|
|
2322
|
+
}
|
|
2323
|
+
},
|
|
2324
|
+
}),
|
|
2325
|
+
h('div', { className: 'dsa-row', style: { justifyContent: 'flex-end' } },
|
|
2326
|
+
h('button', {
|
|
2327
|
+
type: 'button',
|
|
2328
|
+
className: 'dsa-btn',
|
|
2329
|
+
onClick: () => setEditOpen(false),
|
|
2330
|
+
}, t('askCancel')),
|
|
2331
|
+
h('button', {
|
|
2332
|
+
type: 'button',
|
|
2333
|
+
className: 'dsa-btn dsa-btn-primary',
|
|
2334
|
+
disabled: editDraft.trim() === '',
|
|
2335
|
+
onClick: () => {
|
|
2336
|
+
const text = editDraft.trim()
|
|
2337
|
+
if (text === '') return
|
|
2338
|
+
setEditDraft('')
|
|
2339
|
+
setEditOpen(false)
|
|
2340
|
+
onRetryEdit(text)
|
|
2341
|
+
},
|
|
2342
|
+
}, t('retryAskSubmit'))))
|
|
2343
|
+
: null,
|
|
1833
2344
|
h(CardActions, {
|
|
1834
2345
|
card,
|
|
1835
2346
|
onCopy: () => onCopy(answerRef.current),
|
|
1836
2347
|
onRetry,
|
|
1837
2348
|
onCancel,
|
|
1838
2349
|
onSendToMain,
|
|
2350
|
+
onRetryEdit: () => {
|
|
2351
|
+
// Open with the live question, so reopening after an earlier edit
|
|
2352
|
+
// re-edits the question actually asked, not the card's first one.
|
|
2353
|
+
setEditDraft(card.question)
|
|
2354
|
+
setEditOpen(open => !open)
|
|
2355
|
+
},
|
|
1839
2356
|
onFollowUp: () => setFollowOpen(open => !open),
|
|
1840
2357
|
onClose,
|
|
1841
2358
|
}),
|
|
@@ -1967,6 +2484,7 @@ window.__ModuleLoader__.load({
|
|
|
1967
2484
|
onCopy: element => actions.copy(card, element),
|
|
1968
2485
|
onFollowUp: text => actions.followUp(card, text),
|
|
1969
2486
|
onRetry: () => actions.retry(card),
|
|
2487
|
+
onRetryEdit: text => actions.retryEdit(card, text),
|
|
1970
2488
|
onCancel: () => actions.cancel(card),
|
|
1971
2489
|
onSendToMain: () => actions.sendToMain(card),
|
|
1972
2490
|
onClose: () => actions.close(card),
|
|
@@ -1977,13 +2495,27 @@ window.__ModuleLoader__.load({
|
|
|
1977
2495
|
/** The `shell.overlay` entry: styles + trigger + popover + cards + toast. */
|
|
1978
2496
|
function OverlayRoot({ actions }) {
|
|
1979
2497
|
const state = useStoreState()
|
|
2498
|
+
// §4.5 a non-leader copy mounts no interaction surface at all (its
|
|
2499
|
+
// settings page stays available to explain the situation).
|
|
2500
|
+
if (state.familyLeader === false) return null
|
|
1980
2501
|
const config = state.config
|
|
1981
2502
|
const candidate = state.trigger
|
|
1982
2503
|
const popover = state.popover
|
|
1983
|
-
return h('div', {
|
|
2504
|
+
return h('div', {
|
|
2505
|
+
className: 'dsa-layer',
|
|
2506
|
+
'data-dsa-root': '',
|
|
2507
|
+
// §4.5 stamps ownership so the legacy-suppression stylesheet can
|
|
2508
|
+
// distinguish this build's layer from an unmarked ≤1.0.x layer.
|
|
2509
|
+
'data-dsa-owner': OWNER_TOKEN,
|
|
2510
|
+
},
|
|
1984
2511
|
h(Styles, null),
|
|
1985
2512
|
candidate !== null && popover === null && config.trigger !== 'shortcut'
|
|
1986
|
-
? h(
|
|
2513
|
+
? h(TriggerBar, {
|
|
2514
|
+
candidate,
|
|
2515
|
+
config,
|
|
2516
|
+
onOpen: () => actions.openPopover(candidate),
|
|
2517
|
+
onQuick: action => actions.quickAsk(candidate, action),
|
|
2518
|
+
})
|
|
1987
2519
|
: null,
|
|
1988
2520
|
popover !== null
|
|
1989
2521
|
? h(AskPopover, {
|
|
@@ -1991,6 +2523,7 @@ window.__ModuleLoader__.load({
|
|
|
1991
2523
|
config,
|
|
1992
2524
|
onClose: () => actions.closePopover(),
|
|
1993
2525
|
onSubmit: payload => actions.submit(popover.candidate, payload),
|
|
2526
|
+
onQuick: action => actions.quickAsk(popover.candidate, action),
|
|
1994
2527
|
})
|
|
1995
2528
|
: null,
|
|
1996
2529
|
h(FloatingLayer, { cards: state.cards, actions }),
|
|
@@ -2046,11 +2579,95 @@ window.__ModuleLoader__.load({
|
|
|
2046
2579
|
}, options.map(name => h('option', { key: name, value: name }, name)))
|
|
2047
2580
|
}
|
|
2048
2581
|
|
|
2582
|
+
// ── quick-action list editor ───────────────────────────────────────
|
|
2583
|
+
// The draft always carries the FULL list (the user layer replaces
|
|
2584
|
+
// defaults wholesale): built-in rows are editable only in
|
|
2585
|
+
// enabled/shortcut, custom rows also in label/question.
|
|
2586
|
+
const qaList = () => (Array.isArray(draft.quickActions) ? draft.quickActions : [])
|
|
2587
|
+
const patchAction = (index, patch) => {
|
|
2588
|
+
const quickActions = qaList().map((entry, i) => (i === index ? { ...entry, ...patch } : entry))
|
|
2589
|
+
setDraft({ ...draft, quickActions })
|
|
2590
|
+
}
|
|
2591
|
+
const addAction = () => {
|
|
2592
|
+
const list = qaList()
|
|
2593
|
+
// Host cap is enforced on save; do not even append beyond it.
|
|
2594
|
+
if (list.length >= QUICK_ACTION_LIMITS.maxEntries) return
|
|
2595
|
+
const id = `custom:${Date.now().toString(36)}-${list.length + 1}`
|
|
2596
|
+
setDraft({ ...draft, quickActions: [...list, { id, label: '', question: '', enabled: true, shortcut: '' }] })
|
|
2597
|
+
}
|
|
2598
|
+
const removeAction = (index) => {
|
|
2599
|
+
setDraft({ ...draft, quickActions: qaList().filter((_, i) => i !== index) })
|
|
2600
|
+
}
|
|
2601
|
+
const quickActionsEditor = h('div', { className: 'dsa-field' },
|
|
2602
|
+
h('label', null, t('setQuickActions')),
|
|
2603
|
+
h('div', { className: 'dsa-qa' },
|
|
2604
|
+
qaList().map((entry, index) => {
|
|
2605
|
+
const meta = QUICK_ACTION_BUILTINS[String(entry.id)]
|
|
2606
|
+
const isBuiltin = meta !== undefined
|
|
2607
|
+
return h('div', { key: `${entry.id}-${index}`, className: 'dsa-qa-row' },
|
|
2608
|
+
h('label', { className: 'dsa-check', title: t('qaEnabled') },
|
|
2609
|
+
h('input', {
|
|
2610
|
+
type: 'checkbox',
|
|
2611
|
+
checked: entry.enabled !== false,
|
|
2612
|
+
onChange: event => patchAction(index, { enabled: event.target.checked }),
|
|
2613
|
+
}),
|
|
2614
|
+
t('qaEnabled')),
|
|
2615
|
+
isBuiltin ? h('span', { className: 'dsa-badge' }, t('qaBuiltinBadge')) : null,
|
|
2616
|
+
isBuiltin
|
|
2617
|
+
? h('span', { className: 'dsa-title' }, t(meta.labelKey))
|
|
2618
|
+
: h('input', {
|
|
2619
|
+
className: 'dsa-input',
|
|
2620
|
+
value: String(entry.label ?? ''),
|
|
2621
|
+
maxLength: QUICK_ACTION_LIMITS.labelLength,
|
|
2622
|
+
placeholder: t('qaLabelPlaceholder'),
|
|
2623
|
+
onChange: event => patchAction(index, { label: event.target.value }),
|
|
2624
|
+
}),
|
|
2625
|
+
h('input', {
|
|
2626
|
+
className: 'dsa-input',
|
|
2627
|
+
style: { maxWidth: '150px' },
|
|
2628
|
+
value: String(entry.shortcut ?? ''),
|
|
2629
|
+
maxLength: QUICK_ACTION_LIMITS.shortcutLength,
|
|
2630
|
+
placeholder: t('qaShortcutPlaceholder'),
|
|
2631
|
+
onChange: event => patchAction(index, { shortcut: event.target.value }),
|
|
2632
|
+
}),
|
|
2633
|
+
isBuiltin
|
|
2634
|
+
? null
|
|
2635
|
+
: h('button', {
|
|
2636
|
+
type: 'button',
|
|
2637
|
+
className: 'dsa-btn',
|
|
2638
|
+
onClick: () => removeAction(index),
|
|
2639
|
+
}, t('qaDelete')),
|
|
2640
|
+
isBuiltin
|
|
2641
|
+
? h('div', { className: 'dsa-muted dsa-qa-question' }, t(meta.questionKey))
|
|
2642
|
+
: h('input', {
|
|
2643
|
+
className: 'dsa-input dsa-qa-question',
|
|
2644
|
+
value: String(entry.question ?? ''),
|
|
2645
|
+
maxLength: QUICK_ACTION_LIMITS.questionLength,
|
|
2646
|
+
placeholder: t('qaQuestionPlaceholder'),
|
|
2647
|
+
onChange: event => patchAction(index, { question: event.target.value }),
|
|
2648
|
+
}))
|
|
2649
|
+
}),
|
|
2650
|
+
h('div', null, h('button', {
|
|
2651
|
+
type: 'button',
|
|
2652
|
+
className: 'dsa-btn',
|
|
2653
|
+
disabled: qaList().length >= QUICK_ACTION_LIMITS.maxEntries,
|
|
2654
|
+
onClick: addAction,
|
|
2655
|
+
}, t('qaAdd')))),
|
|
2656
|
+
h('span', { className: 'dsa-muted' }, t('setQuickActionsHint')))
|
|
2657
|
+
|
|
2049
2658
|
const save = async () => {
|
|
2050
2659
|
if (parseShortcut(draft.shortcut) === null) {
|
|
2051
2660
|
setStatus(`error:${t('unsupportedTrigger')}`)
|
|
2052
2661
|
return
|
|
2053
2662
|
}
|
|
2663
|
+
// Pre-flight the quick-action list: the Host would all-or-nothing
|
|
2664
|
+
// refuse it anyway, but a local check names the offending row instead
|
|
2665
|
+
// of surfacing a wire error after the save.
|
|
2666
|
+
const qaResult = validateQuickActionsDraft(draft.quickActions, draft.shortcut)
|
|
2667
|
+
if (qaResult.ok === false) {
|
|
2668
|
+
setStatus(`error:${t(qaResult.key, qaResult.vars)}`)
|
|
2669
|
+
return
|
|
2670
|
+
}
|
|
2054
2671
|
setStatus('saving')
|
|
2055
2672
|
try {
|
|
2056
2673
|
await actions.saveConfig(draft)
|
|
@@ -2060,11 +2677,20 @@ window.__ModuleLoader__.load({
|
|
|
2060
2677
|
}
|
|
2061
2678
|
}
|
|
2062
2679
|
|
|
2680
|
+
// §4.5 a non-leader copy explains why its interaction surface is gone.
|
|
2681
|
+
const familyBanner = state.familyLeader === false
|
|
2682
|
+
? h('div', { className: 'dsa-family-notice', role: 'status' },
|
|
2683
|
+
t('familySuppressed', { version: (state.familyPeers[0]?.version) ?? '?' }))
|
|
2684
|
+
: (state.familyLegacy === true
|
|
2685
|
+
? h('div', { className: 'dsa-family-notice', role: 'status' }, t('familyLegacyToast'))
|
|
2686
|
+
: null)
|
|
2687
|
+
|
|
2063
2688
|
return h('div', { className: 'dsa-settings', 'data-dsa-root': '' },
|
|
2064
2689
|
h(Styles, null),
|
|
2065
2690
|
h('div', null,
|
|
2066
2691
|
h('div', { className: 'dsa-title' }, t('settingsTitle')),
|
|
2067
2692
|
h('div', { className: 'dsa-muted' }, t('settingsDesc'))),
|
|
2693
|
+
familyBanner,
|
|
2068
2694
|
h('div', { className: 'dsa-grid' },
|
|
2069
2695
|
field(t('setTrigger'), select('trigger', [
|
|
2070
2696
|
['selection', t('setTriggerSelection')],
|
|
@@ -2107,6 +2733,7 @@ window.__ModuleLoader__.load({
|
|
|
2107
2733
|
])),
|
|
2108
2734
|
field(t('setSideTimeout'), number('sideTimeoutMs', 5000, 3600000), t('setSideTimeoutHint')),
|
|
2109
2735
|
field(t('setSideProvider'), providerSelect(), t('setSideProviderHint'))),
|
|
2736
|
+
quickActionsEditor,
|
|
2110
2737
|
h('label', { className: 'dsa-check' },
|
|
2111
2738
|
h('input', {
|
|
2112
2739
|
type: 'checkbox',
|
|
@@ -2447,6 +3074,18 @@ window.__ModuleLoader__.load({
|
|
|
2447
3074
|
}
|
|
2448
3075
|
void runSideCard(card.id)
|
|
2449
3076
|
},
|
|
3077
|
+
/**
|
|
3078
|
+
* One quick-action chip: resolve the template ({selection} is the
|
|
3079
|
+
* only placeholder), then take the SAME path as a popover submit
|
|
3080
|
+
* fixed to the side card — surface picking, render proof and the
|
|
3081
|
+
* stream loop all live in submit(), so this stays a thin adapter.
|
|
3082
|
+
*/
|
|
3083
|
+
quickAsk(candidate, action) {
|
|
3084
|
+
const config = store.state.config
|
|
3085
|
+
const question = renderActionQuestion(action, candidate.text, config.maxChars)
|
|
3086
|
+
if (question.trim() === '') return
|
|
3087
|
+
void actions.submit(candidate, { question, carrier: 'side' })
|
|
3088
|
+
},
|
|
2450
3089
|
copy(card, element) {
|
|
2451
3090
|
void copyText(card.text, element).then((result) => {
|
|
2452
3091
|
toast(result === 'copied' ? t('copied') : result === 'selected' ? t('copySelectedHint') : t('copyFailed'))
|
|
@@ -2487,6 +3126,25 @@ window.__ModuleLoader__.load({
|
|
|
2487
3126
|
patchCard(card.id, { status: 'pending', text: '', reasoning: '', error: null, streaming: null })
|
|
2488
3127
|
if (card.carrier === 'side') void runSideCard(card.id)
|
|
2489
3128
|
},
|
|
3129
|
+
/**
|
|
3130
|
+
* Regenerate the CURRENT turn with an edited question. Unlike
|
|
3131
|
+
* followUp, this does NOT push the old Q/A into `history`: the
|
|
3132
|
+
* earlier turns on the card stay, the question text is replaced,
|
|
3133
|
+
* and the answer streams again from scratch.
|
|
3134
|
+
*/
|
|
3135
|
+
retryEdit(card, question) {
|
|
3136
|
+
const trimmed = String(question ?? '').trim()
|
|
3137
|
+
if (trimmed === '' || card.carrier !== 'side') return
|
|
3138
|
+
patchCard(card.id, {
|
|
3139
|
+
question: trimmed,
|
|
3140
|
+
status: 'pending',
|
|
3141
|
+
text: '',
|
|
3142
|
+
reasoning: '',
|
|
3143
|
+
error: null,
|
|
3144
|
+
streaming: null,
|
|
3145
|
+
})
|
|
3146
|
+
void runSideCard(card.id)
|
|
3147
|
+
},
|
|
2490
3148
|
cancel(card) {
|
|
2491
3149
|
const controller = controllers.get(card.id)
|
|
2492
3150
|
if (controller !== undefined) {
|
|
@@ -2671,20 +3329,30 @@ window.__ModuleLoader__.load({
|
|
|
2671
3329
|
// ── selection listeners ──────────────────────────────────────────
|
|
2672
3330
|
const evaluate = () => {
|
|
2673
3331
|
pendingTimer = null
|
|
3332
|
+
// §4.5 a non-leader copy must not surface its trigger, even if a
|
|
3333
|
+
// stale selection event arrives.
|
|
3334
|
+
if (store.state.familyLeader === false) return
|
|
2674
3335
|
const config = store.state.config
|
|
2675
3336
|
const candidate = readSelection()
|
|
2676
3337
|
// Zone anchors: the harness stamps every slot outlet with
|
|
2677
|
-
// `data-slot="<key>"`.
|
|
2678
|
-
// selection
|
|
2679
|
-
//
|
|
2680
|
-
//
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
3338
|
+
// `data-slot="<key>"`. This is a property of the BUILD, not of the
|
|
3339
|
+
// current selection — a selection on a body-mounted node (an
|
|
3340
|
+
// onboarding modal, another plugin's raw-DOM panel) has an empty
|
|
3341
|
+
// slot path even though the frame is full of anchors. Probe the
|
|
3342
|
+
// whole document instead, or one such selection would flip the
|
|
3343
|
+
// global verdict to "missing" and silently disable zone filtering.
|
|
3344
|
+
if (candidate !== null && store.state.zoneAnchors !== true) {
|
|
3345
|
+
const buildHasAnchors = candidate.slotPath.length > 0
|
|
3346
|
+
|| (typeof document !== 'undefined' && document.querySelector('[data-slot]') !== null)
|
|
3347
|
+
if (buildHasAnchors) {
|
|
3348
|
+
if (store.state.zoneAnchors !== true) scheduleReport(0)
|
|
3349
|
+
store.set({ zoneAnchors: true })
|
|
3350
|
+
} else if (store.state.zoneAnchors === null && candidate.slotPath.length === 0) {
|
|
3351
|
+
// Only the build-wide miss (and never a single unanchored
|
|
3352
|
+
// selection) flips the verdict to unavailable.
|
|
3353
|
+
store.set({ zoneAnchors: false })
|
|
3354
|
+
scheduleReport(0)
|
|
3355
|
+
console.info(`[${PLUGIN_ID}] 该版本没有 data-slot 锚点,区域过滤不可用,已按"全部区域"工作`)
|
|
2688
3356
|
}
|
|
2689
3357
|
}
|
|
2690
3358
|
const decision = shouldOffer(candidate, config, { anchors: store.state.zoneAnchors !== false })
|
|
@@ -2715,6 +3383,17 @@ window.__ModuleLoader__.load({
|
|
|
2715
3383
|
}
|
|
2716
3384
|
|
|
2717
3385
|
const onKeyDown = (event) => {
|
|
3386
|
+
// §4.5 only the leader answers the shortcut; otherwise two copies
|
|
3387
|
+
// would each preventDefault and each open their own popover.
|
|
3388
|
+
if (store.state.familyLeader === false) return
|
|
3389
|
+
// Never steal keys while the user is typing in our own popover or
|
|
3390
|
+
// card editors (Enter/Escape there are handled by the components).
|
|
3391
|
+
const target = elementOf(event.target)
|
|
3392
|
+
if (target !== null && target.closest('[data-dsa-root]') !== null
|
|
3393
|
+
&& (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA'
|
|
3394
|
+
|| target.closest('[contenteditable=""],[contenteditable="true"]') !== null)) {
|
|
3395
|
+
return
|
|
3396
|
+
}
|
|
2718
3397
|
const config = store.state.config
|
|
2719
3398
|
const facts = parseShortcut(config.shortcut)
|
|
2720
3399
|
if (matchesShortcut(event, facts)) {
|
|
@@ -2725,6 +3404,18 @@ window.__ModuleLoader__.load({
|
|
|
2725
3404
|
return
|
|
2726
3405
|
}
|
|
2727
3406
|
}
|
|
3407
|
+
// Per-action shortcuts: the main shortcut wins on a collision, and
|
|
3408
|
+
// the candidate falls back to the last raised trigger, exactly like
|
|
3409
|
+
// the main shortcut. A chip then submits straight to a side card.
|
|
3410
|
+
for (const action of quickActionsOf(config)) {
|
|
3411
|
+
if (action.shortcut === '' || !matchesShortcut(event, parseShortcut(action.shortcut))) continue
|
|
3412
|
+
const candidate = readSelection() ?? store.state.trigger
|
|
3413
|
+
if (candidate !== null) {
|
|
3414
|
+
event.preventDefault()
|
|
3415
|
+
actions.quickAsk(candidate, action)
|
|
3416
|
+
return
|
|
3417
|
+
}
|
|
3418
|
+
}
|
|
2728
3419
|
if (event.key === 'Escape') {
|
|
2729
3420
|
if (store.state.popover !== null) store.set({ popover: null })
|
|
2730
3421
|
}
|
|
@@ -3140,6 +3831,7 @@ window.__ModuleLoader__.load({
|
|
|
3140
3831
|
onCopy: element => actions.copy(card, element),
|
|
3141
3832
|
onFollowUp: text => actions.followUp(card, text),
|
|
3142
3833
|
onRetry: () => actions.retry(card),
|
|
3834
|
+
onRetryEdit: text => actions.retryEdit(card, text),
|
|
3143
3835
|
onCancel: () => actions.cancel(card),
|
|
3144
3836
|
onSendToMain: () => { void actions.sendToMain(card) },
|
|
3145
3837
|
onClose: () => actions.close(card),
|
|
@@ -3153,6 +3845,61 @@ window.__ModuleLoader__.load({
|
|
|
3153
3845
|
// Corrupt-but-parseable history must never block startup.
|
|
3154
3846
|
console.warn(`[${PLUGIN_ID}] card history restore failed:`, error)
|
|
3155
3847
|
}
|
|
3848
|
+
|
|
3849
|
+
// §4.5 family singleton: register this copy, (re)elect on every join
|
|
3850
|
+
// or leave, and gate the interaction surface on the verdict.
|
|
3851
|
+
const family = familyTable(window)
|
|
3852
|
+
family.join({ id: INSTANCE_ID, version: CLIENT_VERSION, at: Date.now() })
|
|
3853
|
+
const reelect = () => {
|
|
3854
|
+
const members = family.list()
|
|
3855
|
+
const leaderId = electFamilyLeader(members, INSTANCE_ID)
|
|
3856
|
+
const leader = leaderId === INSTANCE_ID
|
|
3857
|
+
const peers = members
|
|
3858
|
+
.filter(member => member.id !== INSTANCE_ID)
|
|
3859
|
+
.map(member => ({ id: member.id, version: member.version }))
|
|
3860
|
+
store.set({ familyLeader: leader, familyPeers: peers })
|
|
3861
|
+
scheduleReport(0)
|
|
3862
|
+
if (leader) detectLegacy()
|
|
3863
|
+
}
|
|
3864
|
+
const unsubscribeFamily = family.subscribe(reelect)
|
|
3865
|
+
|
|
3866
|
+
// Legacy (≤1.0.x) detection: it never joins the registry, so watch
|
|
3867
|
+
// the overlay tree for an unmarked dsa-layer carrying family UI. The
|
|
3868
|
+
// suppression stylesheet then hides it; we only need detection to
|
|
3869
|
+
// raise the toast/report. Repeated matches are a no-op.
|
|
3870
|
+
let legacyAnnounced = false
|
|
3871
|
+
let legacyObserver = null
|
|
3872
|
+
const detectLegacy = () => {
|
|
3873
|
+
if (store.state.familyLeader !== true || legacyAnnounced === true) return
|
|
3874
|
+
const root = typeof document !== 'undefined' ? document : null
|
|
3875
|
+
if (root === null) return
|
|
3876
|
+
const found = findLegacyLayers(root).length > 0
|
|
3877
|
+
if (found === false) return
|
|
3878
|
+
legacyAnnounced = true
|
|
3879
|
+
// Inject the suppression rule directly: the React stylesheet only
|
|
3880
|
+
// exists while our overlay is mounted, but the legacy layer must
|
|
3881
|
+
// stay hidden even during its own re-renders.
|
|
3882
|
+
try {
|
|
3883
|
+
if (root.getElementById('dsa-family-legacy-style') === null) {
|
|
3884
|
+
const style = root.createElement('style')
|
|
3885
|
+
style.id = 'dsa-family-legacy-style'
|
|
3886
|
+
style.textContent = '.dsa-layer:not([data-dsa-owner]){display:none!important}'
|
|
3887
|
+
root.head?.appendChild(style)
|
|
3888
|
+
}
|
|
3889
|
+
} catch { /* React stylesheet already carries the same rule */ }
|
|
3890
|
+
store.set({ familyLegacy: true })
|
|
3891
|
+
toast(t('familyLegacyToast'))
|
|
3892
|
+
scheduleReport(0)
|
|
3893
|
+
}
|
|
3894
|
+
const legacyTimers = [300, 1200, 3000].map(delay => setTimeout(detectLegacy, delay))
|
|
3895
|
+
if (typeof document !== 'undefined' && typeof MutationObserver === 'function') {
|
|
3896
|
+
legacyObserver = new MutationObserver(() => detectLegacy())
|
|
3897
|
+
try {
|
|
3898
|
+
legacyObserver.observe(document.documentElement ?? document, { childList: true, subtree: true })
|
|
3899
|
+
} catch { /* document without a root yet — timers still cover it */ }
|
|
3900
|
+
}
|
|
3901
|
+
|
|
3902
|
+
reelect()
|
|
3156
3903
|
void refreshState().catch(() => { /* the settings page shows the failure */ })
|
|
3157
3904
|
// One report after the slot ladders have had time to settle, so the
|
|
3158
3905
|
// Host ends up holding the CLIENT's real registration state.
|
|
@@ -3170,6 +3917,15 @@ window.__ModuleLoader__.load({
|
|
|
3170
3917
|
pendingProofs.clear()
|
|
3171
3918
|
if (toastTimer !== null) clearTimeout(toastTimer)
|
|
3172
3919
|
if (reportTimer !== null) clearTimeout(reportTimer)
|
|
3920
|
+
for (const timer of legacyTimers) clearTimeout(timer)
|
|
3921
|
+
try {
|
|
3922
|
+
if (typeof document !== 'undefined' && document.getElementById) {
|
|
3923
|
+
document.getElementById('dsa-family-legacy-style')?.remove()
|
|
3924
|
+
}
|
|
3925
|
+
} catch { /* torn down */ }
|
|
3926
|
+
try { legacyObserver?.disconnect() } catch { /* already gone */ }
|
|
3927
|
+
try { unsubscribeFamily() } catch { /* registry torn down */ }
|
|
3928
|
+
try { family.leave(INSTANCE_ID) } catch { /* registry torn down */ }
|
|
3173
3929
|
store.set({
|
|
3174
3930
|
trigger: null,
|
|
3175
3931
|
popover: null,
|
|
@@ -3211,6 +3967,12 @@ window.__ModuleLoader__.load({
|
|
|
3211
3967
|
truncateSelection,
|
|
3212
3968
|
parseShortcut,
|
|
3213
3969
|
matchesShortcut,
|
|
3970
|
+
quickActionsOf,
|
|
3971
|
+
renderActionQuestion,
|
|
3972
|
+
validateQuickActionsDraft,
|
|
3973
|
+
shortcutFingerprint,
|
|
3974
|
+
QUICK_ACTION_BUILTINS,
|
|
3975
|
+
QUICK_ACTION_LIMITS,
|
|
3214
3976
|
classifyZone,
|
|
3215
3977
|
slotPathOf,
|
|
3216
3978
|
shouldOffer,
|
|
@@ -3222,6 +3984,9 @@ window.__ModuleLoader__.load({
|
|
|
3222
3984
|
renderRichText,
|
|
3223
3985
|
pickSurface,
|
|
3224
3986
|
floatPlacement,
|
|
3987
|
+
compareVersions,
|
|
3988
|
+
electFamilyLeader,
|
|
3989
|
+
findLegacyLayers,
|
|
3225
3990
|
localizeError,
|
|
3226
3991
|
errorCodes: ERROR_CODES,
|
|
3227
3992
|
resolveLanguage,
|