dsh-sidecard-ask 2.1.7 → 2.1.9
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 +86 -0
- package/NODE-HOST-CONTRACT.md +392 -0
- package/README.md +67 -0
- package/client.js +308 -38
- package/index.js +66 -11
- package/locale/en.json +3 -1
- package/locale/zh.json +3 -1
- package/package.json +3 -2
package/client.js
CHANGED
|
@@ -117,7 +117,7 @@ window.__ModuleLoader__.load({
|
|
|
117
117
|
* the Host and the module's `api.version` both read it, so a report can
|
|
118
118
|
* never claim a generation the browser is not actually running.
|
|
119
119
|
*/
|
|
120
|
-
const CLIENT_VERSION = '2.1.
|
|
120
|
+
const CLIENT_VERSION = '2.1.9'
|
|
121
121
|
|
|
122
122
|
/**
|
|
123
123
|
* Tab kind served by the DSH native right rail (also its implementation id).
|
|
@@ -206,6 +206,10 @@ window.__ModuleLoader__.load({
|
|
|
206
206
|
// tool-narrowing channel 0.2.1 honors (see the settings row help text).
|
|
207
207
|
sideAgentPreset: '',
|
|
208
208
|
floatMode: 'capsule',
|
|
209
|
+
// 2.1.9:答完自动把浮层从胶囊展开成卡片。胶囊只渲染一行状态,答案要
|
|
210
|
+
// 点一下 › 才看得到;用户明确要求「提问完成之后直接在侧边卡片显示回答内容」。
|
|
211
|
+
// 置 false 可恢复旧行为(始终停在胶囊)。
|
|
212
|
+
autoExpandOnDone: true,
|
|
209
213
|
floatMaxWidth: 340,
|
|
210
214
|
// Docked presentation (1.8.3): when the right rail is on screen, the
|
|
211
215
|
// answer paints inside the rail's rectangle instead of floating as a
|
|
@@ -463,6 +467,12 @@ window.__ModuleLoader__.load({
|
|
|
463
467
|
// where a page goes. It also works on builds whose rail markers we have
|
|
464
468
|
// never seen — including the leftover "card closed" tab, which still
|
|
465
469
|
// occupies a page body.
|
|
470
|
+
//
|
|
471
|
+
// 0.28.x moved the side-card plugin's tabs INTO this same rail, so this
|
|
472
|
+
// marker now also matches a DIFFERENT card's page. `findRail` refuses a
|
|
473
|
+
// body that declares another card id (`data-dsa-card-id`) instead of
|
|
474
|
+
// painting our card on top of it; unmarked bodies (older hosts, leftover
|
|
475
|
+
// closed-card faces) stay eligible.
|
|
466
476
|
'[data-dsa-rail-host]',
|
|
467
477
|
'[data-sidebar-right-panel][data-sidebar-right-open] [data-dockkit-pane]',
|
|
468
478
|
'[data-sidebar-right-panel][data-sidebar-right-open]',
|
|
@@ -539,17 +549,31 @@ window.__ModuleLoader__.load({
|
|
|
539
549
|
*
|
|
540
550
|
* @returns {{node: Element, box: {left: number, top: number, width: number, height: number}}|null}
|
|
541
551
|
*/
|
|
542
|
-
function findRail(scope) {
|
|
552
|
+
function findRail(scope, ownCardIds) {
|
|
543
553
|
if (scope === null || scope === undefined || typeof scope.querySelector !== 'function') return null
|
|
554
|
+
const owns = ownCardIds instanceof Set ? ownCardIds : null
|
|
555
|
+
let occupied = false
|
|
544
556
|
for (const selector of RAIL_SELECTORS) {
|
|
545
557
|
let node = null
|
|
546
558
|
try { node = scope.querySelector(selector) } catch { node = null }
|
|
547
559
|
if (node === null || typeof node.getBoundingClientRect !== 'function') continue
|
|
560
|
+
// A body that names a card this layer does not own belongs to another
|
|
561
|
+
// side card (or another session's page). Docking into it would cover
|
|
562
|
+
// that card, so it is skipped and remembered as "occupied" — the layer
|
|
563
|
+
// then stays floating and says why through `data-dsa-dock`.
|
|
564
|
+
let cardId = null
|
|
565
|
+
try {
|
|
566
|
+
cardId = typeof node.getAttribute === 'function' ? node.getAttribute('data-dsa-card-id') : null
|
|
567
|
+
} catch { cardId = null }
|
|
568
|
+
if (typeof cardId === 'string' && cardId !== '' && (owns === null || !owns.has(cardId))) {
|
|
569
|
+
occupied = true
|
|
570
|
+
continue
|
|
571
|
+
}
|
|
548
572
|
let box = null
|
|
549
573
|
try { box = dockedBodyRect(node) } catch { box = null }
|
|
550
|
-
if (railOnScreen(box)) return { node, box }
|
|
574
|
+
if (railOnScreen(box)) return { node, box, occupied }
|
|
551
575
|
}
|
|
552
|
-
return null
|
|
576
|
+
return occupied ? { node: null, box: null, occupied: true } : null
|
|
553
577
|
}
|
|
554
578
|
|
|
555
579
|
/**
|
|
@@ -627,6 +651,7 @@ window.__ModuleLoader__.load({
|
|
|
627
651
|
answerNonStreaming: '当前 DSH 版本未提供流式帧,答案一次性返回',
|
|
628
652
|
answerEmpty: '(没有内容)',
|
|
629
653
|
cardClosed: '该卡片已关闭',
|
|
654
|
+
closeThisTab: '关闭此页',
|
|
630
655
|
cardMovedToFlow: '该卡片已移至浮层显示',
|
|
631
656
|
cardDelete: '删除这条追问对话',
|
|
632
657
|
cardDeleteTitle: '删除这条追问对话?',
|
|
@@ -660,6 +685,7 @@ window.__ModuleLoader__.load({
|
|
|
660
685
|
errorBusy: '并发追问已达上限({n}),请稍后再试',
|
|
661
686
|
errorNoSideEngineBody: '这台 DSH 组合没有可用的子代理 provider',
|
|
662
687
|
errorNoParentBody: '当前没有活动的会话代理,无法发起独立作答',
|
|
688
|
+
errorNoReadonlyGuardBody: '这台宿主的工具注册表里认不出只读工具,为保证侧边作答不写工作区,本次已拒绝',
|
|
663
689
|
errorAborted: '作答被取消或超时',
|
|
664
690
|
errorUnreachableBody: '宿主接口不可达',
|
|
665
691
|
errorUnauthorizedBody: '需要先通过 DSH 登录(宿主会话未认证)',
|
|
@@ -689,6 +715,8 @@ window.__ModuleLoader__.load({
|
|
|
689
715
|
setFloatModeCapsule: '胶囊(不遮挡对话,可随时展开)',
|
|
690
716
|
setFloatModeFull: '直接展开成卡片',
|
|
691
717
|
setFloatModeHint: '浮层贴右侧边缘,只在空间不足时才可能压到对话区',
|
|
718
|
+
setAutoExpandOnDone: '答完自动展开卡片',
|
|
719
|
+
setAutoExpandOnDoneHint: '开启后,回答一结束就展开成卡片显示正文;关闭后保持一行胶囊,点一下才展开',
|
|
692
720
|
setFloatMaxWidth: '浮层宽度上限(px)',
|
|
693
721
|
setFloatMaxWidthHint: '空间不足时自动收窄,不会硬顶',
|
|
694
722
|
setDockRail: '右侧栏打开时占满它',
|
|
@@ -902,6 +930,7 @@ window.__ModuleLoader__.load({
|
|
|
902
930
|
answerNonStreaming: 'This DSH build publishes no stream frames; the answer arrived at once',
|
|
903
931
|
answerEmpty: '(empty)',
|
|
904
932
|
cardClosed: 'This card was closed',
|
|
933
|
+
closeThisTab: 'Close this tab',
|
|
905
934
|
cardMovedToFlow: 'This card moved to the floating stack',
|
|
906
935
|
cardDelete: 'Delete this follow-up conversation',
|
|
907
936
|
cardDeleteTitle: 'Delete this follow-up conversation?',
|
|
@@ -935,6 +964,7 @@ window.__ModuleLoader__.load({
|
|
|
935
964
|
errorBusy: 'Too many concurrent answers (limit {n}); try again shortly',
|
|
936
965
|
errorNoSideEngineBody: 'This DSH composition has no usable subagent provider',
|
|
937
966
|
errorNoParentBody: 'No active session agent to answer independently',
|
|
967
|
+
errorNoReadonlyGuardBody: 'This host\'s tool registry has no recognisable read-only tools, so the side answer was refused to keep the workspace untouched',
|
|
938
968
|
errorAborted: 'The answer was cancelled or timed out',
|
|
939
969
|
errorUnreachableBody: 'The host API is unreachable',
|
|
940
970
|
errorUnauthorizedBody: 'Sign in to DSH first (the host session is not authenticated)',
|
|
@@ -964,6 +994,8 @@ window.__ModuleLoader__.load({
|
|
|
964
994
|
setFloatModeCapsule: 'Capsule (never covers the conversation)',
|
|
965
995
|
setFloatModeFull: 'Expanded card stack',
|
|
966
996
|
setFloatModeHint: 'The layer is anchored to the right edge and only overlaps on very narrow frames',
|
|
997
|
+
setAutoExpandOnDone: 'Expand the card when the answer finishes',
|
|
998
|
+
setAutoExpandOnDoneHint: 'On: the answer appears in the card the moment it settles. Off: the one-line capsule stays until you click it',
|
|
967
999
|
setFloatMaxWidth: 'Floating layer max width (px)',
|
|
968
1000
|
setFloatMaxWidthHint: 'Narrows automatically when there is not enough room',
|
|
969
1001
|
setDockRail: 'Fill the right rail while it is open',
|
|
@@ -1215,6 +1247,9 @@ window.__ModuleLoader__.load({
|
|
|
1215
1247
|
busy: { key: 'errorBusy', vars: () => ({ n: store?.state?.config?.maxConcurrentAsks ?? 0 }) },
|
|
1216
1248
|
'no-side-engine': { key: 'errorNoSideEngineBody' },
|
|
1217
1249
|
'no-parent': { key: 'errorNoParentBody' },
|
|
1250
|
+
// 2.1.9: the read-only guard now fails closed. The code must be named
|
|
1251
|
+
// Client-side, or an English UI would show the Host's Chinese line.
|
|
1252
|
+
'no-readonly-guard': { key: 'errorNoReadonlyGuardBody' },
|
|
1218
1253
|
aborted: { key: 'errorAborted' },
|
|
1219
1254
|
unreachable: { key: 'errorUnreachableBody' },
|
|
1220
1255
|
// The Host now gates these routes on the DSH browser session, so both are
|
|
@@ -1606,6 +1641,23 @@ window.__ModuleLoader__.load({
|
|
|
1606
1641
|
store.set({ floatMode: mode })
|
|
1607
1642
|
}
|
|
1608
1643
|
|
|
1644
|
+
/**
|
|
1645
|
+
* Bring a finished card on screen.
|
|
1646
|
+
*
|
|
1647
|
+
* The floating layer defaults to the `capsule` pill (one line of status), so
|
|
1648
|
+
* an answer that finishes while collapsed never shows its text. Only a card
|
|
1649
|
+
* carried by the flow layer needs this: a card in the right rail already has
|
|
1650
|
+
* its own page, and an already-expanded card is left alone.
|
|
1651
|
+
* @param {string} id - the card that just finished.
|
|
1652
|
+
*/
|
|
1653
|
+
function revealFinishedCard(id) {
|
|
1654
|
+
if (store.state.config.autoExpandOnDone === false) return
|
|
1655
|
+
const card = store.state.cards.find(item => item.id === id)
|
|
1656
|
+
if (card === undefined || card.surface !== 'flow') return
|
|
1657
|
+
setFloatMode('full')
|
|
1658
|
+
patchCard(id, { open: true })
|
|
1659
|
+
}
|
|
1660
|
+
|
|
1609
1661
|
/** Replace one card by id (immutably; a no-op when it is gone). */
|
|
1610
1662
|
function patchCard(id, patch) {
|
|
1611
1663
|
const cards = store.state.cards.map(card => (card.id === id ? { ...card, ...patch } : card))
|
|
@@ -2332,21 +2384,34 @@ window.__ModuleLoader__.load({
|
|
|
2332
2384
|
* this id instead of guessing at a `tab.meta` that no longer exists.
|
|
2333
2385
|
*/
|
|
2334
2386
|
cardId: null,
|
|
2387
|
+
/** Per-session tab ownership; see `ownerKeyOf`. */
|
|
2388
|
+
owners: new Map(),
|
|
2335
2389
|
open(card) {
|
|
2336
2390
|
if (!this.available() || typeof this.controller.openTab !== 'function') return false
|
|
2337
2391
|
// 同一个 kind 在原生栏里只有一个地址(见 cardId 的注释):先把栏里
|
|
2338
2392
|
// 其它卡片迁回浮层,否则它们既不在栏里也不在浮层上,流式输出还会
|
|
2339
2393
|
// 继续往一张看不见的卡片里写。
|
|
2394
|
+
const openSession = typeof card.sessionId === 'string' && card.sessionId !== ''
|
|
2395
|
+
? card.sessionId
|
|
2396
|
+
: store.state.sessionId
|
|
2397
|
+
const openKey = ownerKeyOf(openSession)
|
|
2340
2398
|
for (const other of store.state.cards) {
|
|
2341
|
-
if (other.id
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2399
|
+
if (other.id === card.id || other.surface !== 'native-rightbar') continue
|
|
2400
|
+
// Only the SAME session's other card loses its address: every
|
|
2401
|
+
// session keeps its own tab, so migrating across sessions would
|
|
2402
|
+
// yank a card out of a rail the user is still looking at.
|
|
2403
|
+
const otherSession = typeof other.sessionId === 'string' && other.sessionId !== ''
|
|
2404
|
+
? other.sessionId
|
|
2405
|
+
: store.state.sessionId
|
|
2406
|
+
if (ownerKeyOf(otherSession) !== openKey) continue
|
|
2407
|
+
// `open: true` is load-bearing: the floating layer only mounts a
|
|
2408
|
+
// card whose `surface === 'flow'` AND `open` is true, so migrating
|
|
2409
|
+
// a card back to the flow while leaving it folded hid it on BOTH
|
|
2410
|
+
// surfaces — not in the rail, not on screen, still streaming.
|
|
2411
|
+
patchCard(other.id, { surface: 'flow', open: true })
|
|
2348
2412
|
}
|
|
2349
2413
|
this.cardId = card.id
|
|
2414
|
+
this.owners.set(openKey, card.id)
|
|
2350
2415
|
try {
|
|
2351
2416
|
this.controller.openTab(CARD_KIND, { params: { cardId: card.id }, revealIfOpened: true })
|
|
2352
2417
|
} catch (error) {
|
|
@@ -2372,12 +2437,20 @@ window.__ModuleLoader__.load({
|
|
|
2372
2437
|
// 却把另一种真实场景关在门外:页面重载后宿主会从布局里恢复出这张
|
|
2373
2438
|
// tab,而 `cardId` 是内存态、必然是 null —— 此时没有卡片认领它,
|
|
2374
2439
|
// 删除历史条目也永远关不掉,就是专家复现的"幽灵面板"。
|
|
2375
|
-
//
|
|
2376
|
-
|
|
2377
|
-
//
|
|
2440
|
+
// 规则改为:**本会话里有别的卡拥有它才拒绝**;本会话无主时按
|
|
2441
|
+
// kind + 会话定位(重载后的幽灵面板由此能关掉)。
|
|
2442
|
+
//
|
|
2443
|
+
// 2.1.9:归属按会话记。进程级单槽在跨会话时会误判「别的卡拥有它」,
|
|
2444
|
+
// 让 A 会话的 tab 永远关不掉(专家复现的跨会话残留)。
|
|
2378
2445
|
const ownerSession = typeof card.sessionId === 'string' && card.sessionId !== ''
|
|
2379
2446
|
? card.sessionId
|
|
2380
2447
|
: store.state.sessionId
|
|
2448
|
+
const knownSession = typeof card.sessionId === 'string' && card.sessionId !== ''
|
|
2449
|
+
const holderId = this.owners.get(ownerKeyOf(ownerSession))
|
|
2450
|
+
if (holderId !== undefined && holderId !== card.id) return false
|
|
2451
|
+
// 会话未知(历史条目/幽灵 id)时,退回进程级单槽:没有任何会话认领
|
|
2452
|
+
// 它,而栏里那张属于别的卡,就不能替它关。
|
|
2453
|
+
if (holderId === undefined && !knownSession && this.cardId !== null && this.cardId !== card.id) return false
|
|
2381
2454
|
let rows = []
|
|
2382
2455
|
try {
|
|
2383
2456
|
rows = typeof controller.openTabs?.getSnapshot === 'function'
|
|
@@ -2403,6 +2476,7 @@ window.__ModuleLoader__.load({
|
|
|
2403
2476
|
return false
|
|
2404
2477
|
}
|
|
2405
2478
|
if (this.cardId === card.id) this.cardId = null
|
|
2479
|
+
if (this.owners.get(ownerKeyOf(ownerSession)) === card.id) this.owners.delete(ownerKeyOf(ownerSession))
|
|
2406
2480
|
return true
|
|
2407
2481
|
} catch (error) {
|
|
2408
2482
|
console.warn(`[${PLUGIN_ID}] the right rail refused the close:`, error)
|
|
@@ -2425,6 +2499,8 @@ window.__ModuleLoader__.load({
|
|
|
2425
2499
|
* that is not this owner must not close someone else's tab.
|
|
2426
2500
|
*/
|
|
2427
2501
|
cardId: null,
|
|
2502
|
+
/** Per-session tab ownership; see `ownerKeyOf`. */
|
|
2503
|
+
owners: new Map(),
|
|
2428
2504
|
/**
|
|
2429
2505
|
* Why this adapter is (not) usable, in the settings page's own words.
|
|
2430
2506
|
* `null` means "usable"; a string explains the refusal.
|
|
@@ -2445,7 +2521,21 @@ window.__ModuleLoader__.load({
|
|
|
2445
2521
|
*/
|
|
2446
2522
|
available() {
|
|
2447
2523
|
if (this.service === null || this.error !== null) return false
|
|
2448
|
-
|
|
2524
|
+
if (!(Array.isArray(this.features) && this.features.includes('tabMeta'))) return false
|
|
2525
|
+
// 0.28.x: every registered type has its own on/off switch in the
|
|
2526
|
+
// side-card plugin's settings, and `openTab` on a disabled type
|
|
2527
|
+
// warns and returns instead of throwing. Treat it as "not usable"
|
|
2528
|
+
// here so `auto` goes straight to the floating card instead of
|
|
2529
|
+
// spending 600 ms proving a surface that deliberately refused.
|
|
2530
|
+
try {
|
|
2531
|
+
if (typeof this.service.isTabEnabled === 'function'
|
|
2532
|
+
&& this.service.isTabEnabled(CARD_KIND_BETTER) === false) {
|
|
2533
|
+
this.reason = 'tab-disabled'
|
|
2534
|
+
return false
|
|
2535
|
+
}
|
|
2536
|
+
} catch { /* the probe is optional */ }
|
|
2537
|
+
if (this.reason === 'tab-disabled') this.reason = null
|
|
2538
|
+
return true
|
|
2449
2539
|
},
|
|
2450
2540
|
open(card) {
|
|
2451
2541
|
if (!this.available() || typeof this.service.openTab !== 'function') return false
|
|
@@ -2468,21 +2558,25 @@ window.__ModuleLoader__.load({
|
|
|
2468
2558
|
// `dedupeKey` on our descriptor is what collapses repeat opens onto
|
|
2469
2559
|
// the same tab; both are part of the stable consumer contract.
|
|
2470
2560
|
this.service.openTab(
|
|
2471
|
-
{ type: CARD_KIND_BETTER, id: tabId, title:
|
|
2561
|
+
{ type: CARD_KIND_BETTER, id: tabId, title: t('answerTitle'), meta: { cardId: card.id } },
|
|
2472
2562
|
typeof targetSession === 'string' && targetSession !== '' ? { sessionId: targetSession } : undefined,
|
|
2473
2563
|
)
|
|
2474
2564
|
this.cardId = card.id
|
|
2565
|
+
this.owners.set(ownerKeyOf(targetSession), card.id)
|
|
2475
2566
|
return true
|
|
2476
2567
|
},
|
|
2477
2568
|
close(card) {
|
|
2478
2569
|
if (!this.available() || typeof this.service.closeTab !== 'function') return false
|
|
2479
|
-
// 单一 page
|
|
2480
|
-
//
|
|
2481
|
-
//
|
|
2482
|
-
if (this.cardId !== null && this.cardId !== card.id) return false
|
|
2570
|
+
// 单一 page 地址(每个会话一个):只有本会话里最后 open 的那张卡
|
|
2571
|
+
// 拥有栏里的 tab。同 native.close:本会话无主(页面重载后)也要
|
|
2572
|
+
// 放行,否则在历史管理器里删掉一条 better 卡,栏里那张 tab 永远留空。
|
|
2483
2573
|
const sessionId = typeof card.sessionId === 'string' && card.sessionId !== ''
|
|
2484
2574
|
? card.sessionId
|
|
2485
2575
|
: (store.state.sessionId ?? undefined)
|
|
2576
|
+
const knownSession = typeof card.sessionId === 'string' && card.sessionId !== ''
|
|
2577
|
+
const holderId = this.owners.get(ownerKeyOf(sessionId))
|
|
2578
|
+
if (holderId !== undefined && holderId !== card.id) return false
|
|
2579
|
+
if (holderId === undefined && !knownSession && this.cardId !== null && this.cardId !== card.id) return false
|
|
2486
2580
|
const scope = typeof sessionId === 'string' && sessionId !== '' ? { sessionId } : undefined
|
|
2487
2581
|
// 0.28.x 的原生 tab id 由宿主 mint(tab1…),不是我们传入的 seed id;
|
|
2488
2582
|
// CardHost 挂载时把真实 id 记进 embeddedTabIds。
|
|
@@ -2490,17 +2584,20 @@ window.__ModuleLoader__.load({
|
|
|
2490
2584
|
if (typeof mountedId === 'string' && mountedId !== '') {
|
|
2491
2585
|
this.service.closeTab(mountedId, scope)
|
|
2492
2586
|
this.cardId = null
|
|
2587
|
+
if (this.owners.get(ownerKeyOf(sessionId)) === card.id) this.owners.delete(ownerKeyOf(sessionId))
|
|
2493
2588
|
return true
|
|
2494
2589
|
}
|
|
2495
2590
|
// 卡片还没挂载过(或已从 store 移除):按 kind + 会话在栏的清单里
|
|
2496
2591
|
// 找到唯一那张 tab,经原生控制器关闭。
|
|
2497
2592
|
if (closeRailRow(CARD_KIND_BETTER, sessionId)) {
|
|
2498
2593
|
this.cardId = null
|
|
2594
|
+
if (this.owners.get(ownerKeyOf(sessionId)) === card.id) this.owners.delete(ownerKeyOf(sessionId))
|
|
2499
2595
|
return true
|
|
2500
2596
|
}
|
|
2501
2597
|
// 0.19 之前的 bottom workbench:seed id 就是 tab id。
|
|
2502
2598
|
this.service.closeTab(`${CARD_KIND_BETTER}:${card.id}`, scope)
|
|
2503
2599
|
this.cardId = null
|
|
2600
|
+
if (this.owners.get(ownerKeyOf(sessionId)) === card.id) this.owners.delete(ownerKeyOf(sessionId))
|
|
2504
2601
|
return true
|
|
2505
2602
|
},
|
|
2506
2603
|
},
|
|
@@ -2549,6 +2646,29 @@ window.__ModuleLoader__.load({
|
|
|
2549
2646
|
* @param {object} state - `store.state`.
|
|
2550
2647
|
* @returns {object} the report body (allow-listed again on the Host).
|
|
2551
2648
|
*/
|
|
2649
|
+
/**
|
|
2650
|
+
* How many rail rows currently belong to this plugin.
|
|
2651
|
+
*
|
|
2652
|
+
* 0.28.x's `available` only says the descriptor registered — it cannot say
|
|
2653
|
+
* the tab ever sat in the rail, because a disabled type or a missing guide
|
|
2654
|
+
* entry makes `openTab` return silently. Counting the host's own inventory
|
|
2655
|
+
* is the only first-hand signal, and it is what a bug report needs.
|
|
2656
|
+
* @returns {number}
|
|
2657
|
+
*/
|
|
2658
|
+
function clientRailTabCount() {
|
|
2659
|
+
const controller = surfaces.native === null ? null : surfaces.native.controller
|
|
2660
|
+
try {
|
|
2661
|
+
const rows = typeof controller?.openTabs?.getSnapshot === 'function'
|
|
2662
|
+
? controller.openTabs.getSnapshot()
|
|
2663
|
+
: []
|
|
2664
|
+
if (!Array.isArray(rows)) return 0
|
|
2665
|
+
return rows.filter(row => typeof row?.kind === 'string'
|
|
2666
|
+
&& (row.kind === CARD_KIND
|
|
2667
|
+
|| row.kind === CARD_KIND_BETTER
|
|
2668
|
+
|| row.kind === `dsh-better-sidebar:${CARD_KIND_BETTER}`)).length
|
|
2669
|
+
} catch { return 0 }
|
|
2670
|
+
}
|
|
2671
|
+
|
|
2552
2672
|
function buildClientReport(state) {
|
|
2553
2673
|
return {
|
|
2554
2674
|
// Read from the module's own version constant so the report can never
|
|
@@ -2567,6 +2687,10 @@ window.__ModuleLoader__.load({
|
|
|
2567
2687
|
...(surfaces.better.version === null ? {} : { version: surfaces.better.version }),
|
|
2568
2688
|
...(surfaces.better.reason === null ? {} : { reason: String(surfaces.better.reason) }),
|
|
2569
2689
|
features: surfaces.better.features,
|
|
2690
|
+
// First-hand proof that a page really exists in the rail, plus the
|
|
2691
|
+
// last host tab id a body actually mounted with.
|
|
2692
|
+
railTabs: clientRailTabCount(),
|
|
2693
|
+
mountedTabId: lastMountedHostTabId,
|
|
2570
2694
|
},
|
|
2571
2695
|
// §4.5 family singleton verdict (host-side diagnostics only).
|
|
2572
2696
|
family: {
|
|
@@ -2586,6 +2710,9 @@ window.__ModuleLoader__.load({
|
|
|
2586
2710
|
*/
|
|
2587
2711
|
let renderedCardId = null
|
|
2588
2712
|
|
|
2713
|
+
/** The host tab id the most recent embedded body mounted with. */
|
|
2714
|
+
let lastMountedHostTabId = null
|
|
2715
|
+
|
|
2589
2716
|
/**
|
|
2590
2717
|
* Host tab ids for cards mounted by an external embedder, keyed by card id.
|
|
2591
2718
|
*
|
|
@@ -2595,8 +2722,29 @@ window.__ModuleLoader__.load({
|
|
|
2595
2722
|
* open lands in DSH's native rail, so `closeTab(seed.id)` is a strict no-op.
|
|
2596
2723
|
* The body records the real id here and `surfaces.better.close` uses it.
|
|
2597
2724
|
*/
|
|
2725
|
+
/**
|
|
2726
|
+
* The key an adapter records tab ownership under.
|
|
2727
|
+
*
|
|
2728
|
+
* Ownership is per SESSION, never process-global: each session has its own
|
|
2729
|
+
* page address for a kind, so one slot could only ever describe the last
|
|
2730
|
+
* opened card and would deny every other session its own close.
|
|
2731
|
+
* @param {string|null|undefined} sessionId
|
|
2732
|
+
* @returns {string} '' for "unknown session"
|
|
2733
|
+
*/
|
|
2734
|
+
const ownerKeyOf = sessionId => (typeof sessionId === 'string' && sessionId !== '' ? sessionId : '')
|
|
2735
|
+
|
|
2598
2736
|
const embeddedTabIds = new Map()
|
|
2599
2737
|
|
|
2738
|
+
/**
|
|
2739
|
+
* Card ids this page has actually rendered a body for.
|
|
2740
|
+
*
|
|
2741
|
+
* Mount is the only thing that proves a tab belongs to THIS page: the host
|
|
2742
|
+
* persists the rail layout, so another window can restore the same tab
|
|
2743
|
+
* record while its card lives there. A restored-but-unmounted tab therefore
|
|
2744
|
+
* gets a close button, never an automatic close.
|
|
2745
|
+
*/
|
|
2746
|
+
const everMountedCards = new Set()
|
|
2747
|
+
|
|
2600
2748
|
/**
|
|
2601
2749
|
* Pick the surface for a new card.
|
|
2602
2750
|
* @param {string} wanted - `auto` | adapter kind.
|
|
@@ -4911,8 +5059,10 @@ body[data-ds-dark-theme] .dsa-seg button[data-on="true"]:hover{
|
|
|
4911
5059
|
} catch { return null }
|
|
4912
5060
|
}
|
|
4913
5061
|
const laneRect = measure(lane)
|
|
4914
|
-
const
|
|
4915
|
-
const
|
|
5062
|
+
const ownCards = new Set(store.state.cards.filter(item => item.surface === 'flow').map(item => item.id))
|
|
5063
|
+
const rail = findRail(document, ownCards)
|
|
5064
|
+
const railBox = rail === null || rail.box === null ? null : rail.box
|
|
5065
|
+
const railOccupied = rail !== null && rail.occupied === true && railBox === null
|
|
4916
5066
|
const laneRight = laneRect === null ? rootWidth : laneRect.right
|
|
4917
5067
|
const railLeft = railBox === null ? null : railBox.left
|
|
4918
5068
|
// 2.1.3: the composer occupies the bottom of the conversation column,
|
|
@@ -4965,7 +5115,9 @@ body[data-ds-dark-theme] .dsa-seg button[data-on="true"]:hover{
|
|
|
4965
5115
|
? 'dock'
|
|
4966
5116
|
: (mode !== 'full'
|
|
4967
5117
|
? 'mode'
|
|
4968
|
-
: (dockWanted
|
|
5118
|
+
: (dockWanted
|
|
5119
|
+
? (railBox !== null ? 'rail-too-small' : (railOccupied ? 'occupied' : 'no-rail'))
|
|
5120
|
+
: 'off'))
|
|
4969
5121
|
// The rail can appear AFTER this effect ran (opening it mounts new
|
|
4970
5122
|
// nodes), and it can be re-created later: keep the observer pointed at
|
|
4971
5123
|
// whichever node is currently accepted.
|
|
@@ -5722,6 +5874,14 @@ body[data-ds-dark-theme] .dsa-seg button[data-on="true"]:hover{
|
|
|
5722
5874
|
: notifyPerm === 'denied'
|
|
5723
5875
|
? t('notifyDenied')
|
|
5724
5876
|
: t('setNotifyOnDoneHint')),
|
|
5877
|
+
h('label', { className: 'dsa-check' },
|
|
5878
|
+
h('input', {
|
|
5879
|
+
type: 'checkbox',
|
|
5880
|
+
checked: draft.autoExpandOnDone !== false,
|
|
5881
|
+
onChange: event => setDraft({ ...draft, autoExpandOnDone: event.target.checked }),
|
|
5882
|
+
}),
|
|
5883
|
+
t('setAutoExpandOnDone')),
|
|
5884
|
+
h('span', { className: 'dsa-muted' }, t('setAutoExpandOnDoneHint')),
|
|
5725
5885
|
h('label', { className: 'dsa-check' },
|
|
5726
5886
|
h('input', {
|
|
5727
5887
|
type: 'checkbox',
|
|
@@ -6240,7 +6400,8 @@ body[data-ds-dark-theme] .dsa-seg button[data-on="true"]:hover{
|
|
|
6240
6400
|
: []
|
|
6241
6401
|
} catch { return false }
|
|
6242
6402
|
if (!Array.isArray(rows)) return false
|
|
6243
|
-
const candidates = rows.filter(entry => entry?.kind === kind
|
|
6403
|
+
const candidates = rows.filter(entry => (entry?.kind === kind
|
|
6404
|
+
|| entry?.kind === `dsh-better-sidebar:${kind}`)
|
|
6244
6405
|
&& typeof entry?.tabId === 'string' && entry.tabId !== '')
|
|
6245
6406
|
// 会话已知时按会话匹配;会话未知(旧历史没有 sessionId)时只有
|
|
6246
6407
|
// 全进程恰好一张该 kind 的 tab 才敢关,否则可能误关别的会话。
|
|
@@ -6276,15 +6437,19 @@ body[data-ds-dark-theme] .dsa-seg button[data-on="true"]:hover{
|
|
|
6276
6437
|
const better = surfaces.better
|
|
6277
6438
|
if (better === null || better === undefined) return
|
|
6278
6439
|
if (better.service === null || typeof better.service?.closeTab !== 'function') return
|
|
6279
|
-
// 单一 page
|
|
6280
|
-
//
|
|
6281
|
-
//
|
|
6282
|
-
|
|
6440
|
+
// 单一 page 地址(每个会话一个):只有本会话里最后 open 的那张卡
|
|
6441
|
+
// 拥有栏里的 tab;释放一张不是本会话所有者的历史条目,会把现在
|
|
6442
|
+
// 真正在栏里的卡片连带关掉。本会话无主时也放行(理由见 better.close)。
|
|
6443
|
+
const knownSession = typeof sessionId === 'string' && sessionId !== ''
|
|
6444
|
+
const holderId = better.owners.get(ownerKeyOf(sessionId))
|
|
6445
|
+
if (holderId !== undefined && holderId !== id) return
|
|
6446
|
+
if (holderId === undefined && !knownSession && better.cardId !== null && better.cardId !== id) return
|
|
6283
6447
|
const scope = typeof sessionId === 'string' && sessionId !== '' ? { sessionId } : undefined
|
|
6284
6448
|
const mountedId = embeddedTabIds.get(id)
|
|
6285
6449
|
if (typeof mountedId === 'string' && mountedId !== '') {
|
|
6286
6450
|
try { better.service.closeTab(mountedId, scope) } catch { /* the host may already be gone */ }
|
|
6287
6451
|
better.cardId = null
|
|
6452
|
+
if (better.owners.get(ownerKeyOf(sessionId)) === id) better.owners.delete(ownerKeyOf(sessionId))
|
|
6288
6453
|
return
|
|
6289
6454
|
}
|
|
6290
6455
|
if (closeRailRow(CARD_KIND_BETTER, sessionId)) {
|
|
@@ -6310,8 +6475,9 @@ body[data-ds-dark-theme] .dsa-seg button[data-on="true"]:hover{
|
|
|
6310
6475
|
const native = surfaces.native
|
|
6311
6476
|
if (native === null || native === undefined) return
|
|
6312
6477
|
if (native.controller === null || native.controller === undefined) return
|
|
6313
|
-
//
|
|
6314
|
-
|
|
6478
|
+
// 本会话无主时也放行:理由见 native.close。
|
|
6479
|
+
const holderId = native.owners.get(ownerKeyOf(sessionId))
|
|
6480
|
+
if (holderId !== undefined && holderId !== id) return
|
|
6315
6481
|
try {
|
|
6316
6482
|
native.close({ id, sessionId })
|
|
6317
6483
|
} catch { /* the rail owns its tab's lifetime */ }
|
|
@@ -6349,6 +6515,41 @@ body[data-ds-dark-theme] .dsa-seg button[data-on="true"]:hover{
|
|
|
6349
6515
|
}
|
|
6350
6516
|
|
|
6351
6517
|
const actions = {
|
|
6518
|
+
/**
|
|
6519
|
+
* Close the rail page that is rendering `props`, by its host tab id
|
|
6520
|
+
* when the host gave one, else by kind + session through the rail
|
|
6521
|
+
* inventory. Used by the orphan page's own close button.
|
|
6522
|
+
* @param {object} props - the body props the host handed `CardHost`.
|
|
6523
|
+
* @returns {boolean} whether a close was issued.
|
|
6524
|
+
*/
|
|
6525
|
+
closeRailTab(props) {
|
|
6526
|
+
const tabId = typeof props?.tab?.id === 'string' && props.tab.id !== '' ? props.tab.id : null
|
|
6527
|
+
const scopeId = typeof props?.scope?.sessionId === 'string' && props.scope.sessionId !== ''
|
|
6528
|
+
? props.scope.sessionId
|
|
6529
|
+
: (props?.tab?.sessionId ?? null)
|
|
6530
|
+
const controller = surfaces.native === null ? null : surfaces.native.controller
|
|
6531
|
+
try {
|
|
6532
|
+
if (tabId !== null && controller !== null && controller !== undefined) {
|
|
6533
|
+
if (typeof controller.closeIn === 'function' && typeof scopeId === 'string') {
|
|
6534
|
+
controller.closeIn(scopeId, tabId)
|
|
6535
|
+
return true
|
|
6536
|
+
}
|
|
6537
|
+
if (typeof controller.close === 'function') {
|
|
6538
|
+
controller.close(tabId)
|
|
6539
|
+
return true
|
|
6540
|
+
}
|
|
6541
|
+
}
|
|
6542
|
+
} catch { /* the rail owns its tab's lifetime */ }
|
|
6543
|
+
if (typeof scopeId === 'string' && closeRailRow(CARD_KIND, scopeId)) return true
|
|
6544
|
+
const better = surfaces.better
|
|
6545
|
+
try {
|
|
6546
|
+
if (tabId !== null && better?.service && typeof better.service.closeTab === 'function') {
|
|
6547
|
+
better.service.closeTab(tabId, typeof scopeId === 'string' ? { sessionId: scopeId } : undefined)
|
|
6548
|
+
return true
|
|
6549
|
+
}
|
|
6550
|
+
} catch { /* host may be gone */ }
|
|
6551
|
+
return false
|
|
6552
|
+
},
|
|
6352
6553
|
openPopover(candidate) {
|
|
6353
6554
|
// P2-2: the composer must be remembered BEFORE the popover's
|
|
6354
6555
|
// textarea takes focus, otherwise "insert into composer" has no
|
|
@@ -6503,7 +6704,7 @@ body[data-ds-dark-theme] .dsa-seg button[data-on="true"]:hover{
|
|
|
6503
6704
|
const config = store.state.config
|
|
6504
6705
|
const cut = truncateSelection(candidate.text, config.maxChars)
|
|
6505
6706
|
const card = {
|
|
6506
|
-
id: `card-${Date.now()}-${nextCardId++}`,
|
|
6707
|
+
id: `card-${Date.now().toString(36)}-${nextCardId++}-${Math.random().toString(36).slice(2, 8)}`,
|
|
6507
6708
|
question: payload.question,
|
|
6508
6709
|
selection: candidate.text,
|
|
6509
6710
|
selected: cut.text,
|
|
@@ -6589,6 +6790,15 @@ body[data-ds-dark-theme] .dsa-seg button[data-on="true"]:hover{
|
|
|
6589
6790
|
// what the "moved to the flow stack" body below covers.
|
|
6590
6791
|
const proof = setTimeout(() => {
|
|
6591
6792
|
pendingProofs.delete(card.id)
|
|
6793
|
+
// A card whose session is not the one on screen is queued by
|
|
6794
|
+
// the host, not refused: the rail only mounts on-screen tabs.
|
|
6795
|
+
// Judging it "unproven" here moved a perfectly live card to
|
|
6796
|
+
// the flow stack and closed the tab it had just opened.
|
|
6797
|
+
const ownSession = typeof card.sessionId === 'string' && card.sessionId !== ''
|
|
6798
|
+
? card.sessionId
|
|
6799
|
+
: null
|
|
6800
|
+
if (ownSession !== null && store.state.sessionId !== null
|
|
6801
|
+
&& ownSession !== store.state.sessionId) return
|
|
6592
6802
|
const live = store.state.cards.find(item => item.id === card.id)
|
|
6593
6803
|
if (live === undefined || live.surface === 'flow') return
|
|
6594
6804
|
if (renderedCardId === card.id) return
|
|
@@ -6874,7 +7084,11 @@ body[data-ds-dark-theme] .dsa-seg button[data-on="true"]:hover{
|
|
|
6874
7084
|
// and managed explicitly in the settings-page history manager.
|
|
6875
7085
|
},
|
|
6876
7086
|
async saveConfig(patch) {
|
|
6877
|
-
|
|
7087
|
+
// 宿主要求 inherit 显式确认(防误触/静默提权);用户点"保存"本身
|
|
7088
|
+
// 就是那个确认动作,恶意同源脚本仍可伪造 —— 它不是安全边界,
|
|
7089
|
+
// 只让这种变更必须是有意的。
|
|
7090
|
+
const body = patch?.sideTools === 'inherit' ? { ...patch, confirmInheritTools: true } : patch
|
|
7091
|
+
const value = await postJson('config', body)
|
|
6878
7092
|
store.set({
|
|
6879
7093
|
config: { ...CLIENT_DEFAULTS, ...(value.config ?? {}) },
|
|
6880
7094
|
problems: value.problems ?? [],
|
|
@@ -7046,6 +7260,8 @@ body[data-ds-dark-theme] .dsa-seg button[data-on="true"]:hover{
|
|
|
7046
7260
|
})
|
|
7047
7261
|
controllers.delete(cardId)
|
|
7048
7262
|
syncPersisted()
|
|
7263
|
+
// 2.1.9:答完就让答案出现在卡片里(胶囊态不再只显示一行状态)。
|
|
7264
|
+
revealFinishedCard(cardId)
|
|
7049
7265
|
// #5: background finish notification (guarded inside).
|
|
7050
7266
|
const finished = store.state.cards.find(item => item.id === cardId)
|
|
7051
7267
|
if (finished !== undefined) notifyCardFinish(finished)
|
|
@@ -7055,6 +7271,7 @@ body[data-ds-dark-theme] .dsa-seg button[data-on="true"]:hover{
|
|
|
7055
7271
|
flushStreamBuffers()
|
|
7056
7272
|
patchCard(cardId, { status: 'error', error, elapsedMs: Date.now() - startedAt })
|
|
7057
7273
|
controllers.delete(cardId)
|
|
7274
|
+
revealFinishedCard(cardId)
|
|
7058
7275
|
const failed = store.state.cards.find(item => item.id === cardId)
|
|
7059
7276
|
if (failed !== undefined) notifyCardFinish(failed)
|
|
7060
7277
|
},
|
|
@@ -7536,6 +7753,12 @@ body[data-ds-dark-theme] .dsa-seg button[data-on="true"]:hover{
|
|
|
7536
7753
|
title: () => t('answerTitle'),
|
|
7537
7754
|
description: () => t('settingsDesc'),
|
|
7538
7755
|
order: 120,
|
|
7756
|
+
// 0.28.x: a registered type is added to the + menu automatically,
|
|
7757
|
+
// and a guide entry opened with no card behind it renders this
|
|
7758
|
+
// plugin's "card closed" page — two dead entries per session
|
|
7759
|
+
// (native guide + this mirror). `hidden` keeps the type usable
|
|
7760
|
+
// through `openTab` while dropping it from the menu.
|
|
7761
|
+
hidden: true,
|
|
7539
7762
|
dedupeKey: tab => (tab?.meta?.cardId !== undefined ? String(tab.meta.cardId) : undefined),
|
|
7540
7763
|
component: props => h(CardHost, {
|
|
7541
7764
|
props: { tab: props?.tab, sessionId: props?.scope?.sessionId },
|
|
@@ -7587,7 +7810,16 @@ body[data-ds-dark-theme] .dsa-seg button[data-on="true"]:hover{
|
|
|
7587
7810
|
function CardHost({ props, surface }) {
|
|
7588
7811
|
const state = useStoreState()
|
|
7589
7812
|
const cardId = hostCardIdOf(props)
|
|
7590
|
-
|
|
7813
|
+
// A tab's `meta.cardId` is just a string the host persisted; it must
|
|
7814
|
+
// not route this body to a card that lives in ANOTHER session.
|
|
7815
|
+
const scopeId = typeof props?.scope?.sessionId === 'string' && props.scope.sessionId !== ''
|
|
7816
|
+
? props.scope.sessionId
|
|
7817
|
+
: null
|
|
7818
|
+
const card = state.cards.find(item => item.id === cardId
|
|
7819
|
+
&& (scopeId === null
|
|
7820
|
+
|| typeof item.sessionId !== 'string'
|
|
7821
|
+
|| item.sessionId === ''
|
|
7822
|
+
|| item.sessionId === scopeId))
|
|
7591
7823
|
// The embedder's REAL tab id (better-sidebar 0.28.x mints it and
|
|
7592
7824
|
// ignores the seed id we passed), recorded for close/delete.
|
|
7593
7825
|
const hostTabId = typeof props?.tab?.id === 'string' && props.tab.id !== '' ? props.tab.id : null
|
|
@@ -7596,7 +7828,11 @@ body[data-ds-dark-theme] .dsa-seg button[data-on="true"]:hover{
|
|
|
7596
7828
|
React.useEffect(() => {
|
|
7597
7829
|
if (typeof cardId !== 'string' || cardId === '') return undefined
|
|
7598
7830
|
renderedCardId = cardId
|
|
7599
|
-
|
|
7831
|
+
everMountedCards.add(cardId)
|
|
7832
|
+
if (hostTabId !== null) {
|
|
7833
|
+
embeddedTabIds.set(cardId, hostTabId)
|
|
7834
|
+
lastMountedHostTabId = hostTabId
|
|
7835
|
+
}
|
|
7600
7836
|
// Mounting settles the render proof, even when this host mounted
|
|
7601
7837
|
// late (a lazy tab panel slower than the 600ms timer) — the proof
|
|
7602
7838
|
// must never fire after the card is demonstrably on screen.
|
|
@@ -7605,6 +7841,13 @@ body[data-ds-dark-theme] .dsa-seg button[data-on="true"]:hover{
|
|
|
7605
7841
|
clearTimeout(proof)
|
|
7606
7842
|
pendingProofs.delete(cardId)
|
|
7607
7843
|
}
|
|
7844
|
+
// A body whose card is gone, for a tab THIS page mounted before:
|
|
7845
|
+
// release it now. A tab restored by another window was never
|
|
7846
|
+
// mounted here (`everMountedCards`), so it keeps its close button
|
|
7847
|
+
// instead of being stolen from that window.
|
|
7848
|
+
if (card === undefined && everMountedCards.has(cardId)) {
|
|
7849
|
+
try { actions.closeRailTab(props) } catch { /* host may be gone */ }
|
|
7850
|
+
}
|
|
7608
7851
|
return () => {
|
|
7609
7852
|
if (renderedCardId === cardId) renderedCardId = null
|
|
7610
7853
|
if (hostTabId !== null && embeddedTabIds.get(cardId) === hostTabId) {
|
|
@@ -7612,10 +7855,26 @@ body[data-ds-dark-theme] .dsa-seg button[data-on="true"]:hover{
|
|
|
7612
7855
|
}
|
|
7613
7856
|
}
|
|
7614
7857
|
}, [cardId, hostTabId])
|
|
7858
|
+
// No live card claims this body: a tab the host restored from its
|
|
7859
|
+
// persisted layout, or a card deleted before its body mounted. Give
|
|
7860
|
+
// the page an exit — the old build rendered one grey line and left
|
|
7861
|
+
// the user with no way to close it (A1 was fixed for the native
|
|
7862
|
+
// sweep, never for this face).
|
|
7615
7863
|
if (card === undefined) {
|
|
7616
|
-
return h('div', {
|
|
7864
|
+
return h('div', {
|
|
7865
|
+
className: 'dsa-settings',
|
|
7866
|
+
'data-dsa-root': '',
|
|
7867
|
+
'data-dsa-rail-host': '',
|
|
7868
|
+
'data-dsa-card-id': typeof cardId === 'string' ? cardId : '',
|
|
7869
|
+
},
|
|
7617
7870
|
h(Styles, null),
|
|
7618
|
-
h('div', { className: 'dsa-muted' }, t('cardClosed'))
|
|
7871
|
+
h('div', { className: 'dsa-muted' }, t('cardClosed')),
|
|
7872
|
+
h('button', {
|
|
7873
|
+
type: 'button',
|
|
7874
|
+
className: 'dsa-btn',
|
|
7875
|
+
'data-dsa-action': 'close-orphan-tab',
|
|
7876
|
+
onClick: () => { try { actions.closeRailTab(props) } catch { /* host may be gone */ } },
|
|
7877
|
+
}, t('closeThisTab')))
|
|
7619
7878
|
}
|
|
7620
7879
|
if (card.surface === 'flow') {
|
|
7621
7880
|
// The proof already moved this card to the flow stack and this
|
|
@@ -7624,11 +7883,22 @@ body[data-ds-dark-theme] .dsa-seg button[data-on="true"]:hover{
|
|
|
7624
7883
|
// note for a rail that keeps it.
|
|
7625
7884
|
try { surfaces.native.close(card) } catch { /* the rail owns its lifecycle */ }
|
|
7626
7885
|
try { surfaces.better.close(card) } catch { /* the host may already be gone */ }
|
|
7627
|
-
return h('div', {
|
|
7886
|
+
return h('div', {
|
|
7887
|
+
className: 'dsa-settings',
|
|
7888
|
+
'data-dsa-root': '',
|
|
7889
|
+
'data-dsa-rail-host': '',
|
|
7890
|
+
'data-dsa-card-id': typeof cardId === 'string' ? cardId : '',
|
|
7891
|
+
},
|
|
7628
7892
|
h(Styles, null),
|
|
7629
7893
|
h('div', { className: 'dsa-muted' }, t('cardMovedToFlow')))
|
|
7630
7894
|
}
|
|
7631
|
-
return h('div', {
|
|
7895
|
+
return h('div', {
|
|
7896
|
+
className: 'dsa-settings',
|
|
7897
|
+
'data-dsa-root': '',
|
|
7898
|
+
'data-dsa-rail-host': '',
|
|
7899
|
+
'data-dsa-card-id': typeof cardId === 'string' ? cardId : '',
|
|
7900
|
+
style: { padding: '10px' },
|
|
7901
|
+
},
|
|
7632
7902
|
h(Styles, null),
|
|
7633
7903
|
h(MemoAnswerCard, {
|
|
7634
7904
|
card,
|