dsh-sidecard-ask 2.1.5 → 2.1.6
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 +35 -0
- package/client.js +217 -33
- package/index.js +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,40 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 2.1.6
|
|
4
|
+
|
|
5
|
+
**主题:修复桌面端 0.2.1-rc.3 / 网页端 0.2.1-rc.4 与 0.2.2-alpha.2 上的 4 个承载面与会话归属缺陷。**
|
|
6
|
+
|
|
7
|
+
### 修复
|
|
8
|
+
|
|
9
|
+
- **S1(高)默认 better 承载面开第二张卡时第一张消失**。better-sidebar 0.28.2 把每个
|
|
10
|
+
描述符镜像到 DSH 原生右栏,而该 tab 类型没有 `multiple`:同一 kind、同一会话只有
|
|
11
|
+
一个 page 地址,第二次 `openTab` 会把这个地址导航到新卡。插件过去只在 native
|
|
12
|
+
适配器做「迁回浮层」,better 适配器没有,于是第一张卡既不在栏里也不在浮层上,
|
|
13
|
+
流式输出继续写进看不见的卡片。现在 `surfaces.better.open()` 与 native 一样,先把
|
|
14
|
+
同会话其它 better 卡片迁回浮层并置 `open:true`。覆盖原生右栏为唯一地址的 0.28.x。
|
|
15
|
+
- **S2(高)关闭/删除卡片不会关闭 better 承载面的 tab**。0.28.x 的原生 tab id 由宿主
|
|
16
|
+
mint(`tab1`…),插件传入的 seed id 被忽略,`closeTab(seed.id)` 是严格 no-op。
|
|
17
|
+
`CardHost` 现在从宿主传入的 `props.tab.id` 记录真实 tab id,关闭/删除用它;未挂载
|
|
18
|
+
过的卡片(刷新后恢复的栏 tab)按 kind + 会话在 `sidebarRight.openTabs` 清单里定位。
|
|
19
|
+
`deleteHistoryCard` / `clearHistory` 也会释放没有活跃卡片的 better tab。
|
|
20
|
+
- **S3(中)显式 `native-rightbar` 下关闭/删除失效(2.1.5 R2 回归)**。宿主的 page tab
|
|
21
|
+
把 `contentId` 固定写成 `sidebar://<kind>`,不是卡片 id;R2 用
|
|
22
|
+
`entry.contentId === card.id` 判归属,匹配恒失败,关闭流程永远返回 false。现在
|
|
23
|
+
归属只看适配器持有的 `surfaces.native.cardId`,定位按 kind + 会话;`releaseNativeTab`
|
|
24
|
+
与删除路径随之恢复生效。测试替身也改为真实 `contentId` 语义。
|
|
25
|
+
- **S4(中)切会话后停止/改投用错会话**。卡片过去不记录发起会话;用户在会话 A 追问、
|
|
26
|
+
切到 B 后点停止,`/cancel` 带的是 B,宿主 R6 归属校验按「没找到」处理,子代理继续
|
|
27
|
+
跑到超时;「改到主对话」也会投进 B。卡片现在在创建时记录 `sessionId`,停止、关闭、
|
|
28
|
+
删除、追问、重试、改投全部使用卡片自己的会话,并随 v3 历史持久化(旧条目无该字段
|
|
29
|
+
时回退当前会话)。
|
|
30
|
+
|
|
31
|
+
### 验证
|
|
32
|
+
|
|
33
|
+
`verify 57 / contract 1475 / float-placement 100 / smoke 279 = 1911 全绿`。净增 21 条
|
|
34
|
+
回归断言:better 第二张卡迁移、真实宿主 tab id 关闭、非所有者不得替他人关 tab、
|
|
35
|
+
未挂载 tab 的删除释放、原生 `contentId=sidebar://kind` 下的关闭与删除、切会话后
|
|
36
|
+
`/cancel` 仍带原会话、v3 条目持久化与重放 `sessionId`。
|
|
37
|
+
|
|
3
38
|
## 2.1.5
|
|
4
39
|
|
|
5
40
|
**主题:修完「技术方案评审团」在 0.2.1-rc.3(桌面 Electron 44.7.0 / 网页端同源)上复核出的 8 条缺陷。**
|
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.6'
|
|
121
121
|
|
|
122
122
|
/**
|
|
123
123
|
* Tab kind served by the DSH native right rail (also its implementation id).
|
|
@@ -2264,6 +2264,23 @@ window.__ModuleLoader__.load({
|
|
|
2264
2264
|
throw Object.assign(new Error(t('noMainCarrier')), { code: 'no-main-carrier' })
|
|
2265
2265
|
}
|
|
2266
2266
|
|
|
2267
|
+
/**
|
|
2268
|
+
* The session a card belongs to, falling back to the active one.
|
|
2269
|
+
*
|
|
2270
|
+
* Cards outlive session switches (the overlay is root-scoped), so every
|
|
2271
|
+
* follow-up transport must name the card's own session: the Host's `/cancel`
|
|
2272
|
+
* treats a known-but-different session as "not found", and a prompt sent to
|
|
2273
|
+
* the wrong session quotes the wrong conversation.
|
|
2274
|
+
* @param {object|undefined} card - the card being acted on.
|
|
2275
|
+
* @param {object} ctx - the client context (for the active-session fallback).
|
|
2276
|
+
* @returns {string|null} the owning session, or the active one.
|
|
2277
|
+
*/
|
|
2278
|
+
function cardSessionId(card, ctx) {
|
|
2279
|
+
const own = card?.sessionId
|
|
2280
|
+
if (typeof own === 'string' && own !== '') return own
|
|
2281
|
+
return currentSessionId(ctx)
|
|
2282
|
+
}
|
|
2283
|
+
|
|
2267
2284
|
/** Best-effort active session id from every sanctioned source. */
|
|
2268
2285
|
function currentSessionId(ctx) {
|
|
2269
2286
|
const captured = store.state.sessionId
|
|
@@ -2347,10 +2364,15 @@ window.__ModuleLoader__.load({
|
|
|
2347
2364
|
// and lets CardHost show its closed-card face.
|
|
2348
2365
|
const controller = this.controller
|
|
2349
2366
|
if (controller === null || controller === undefined) return false
|
|
2350
|
-
//
|
|
2351
|
-
//
|
|
2352
|
-
//
|
|
2353
|
-
|
|
2367
|
+
// 归属校验:只有"本卡就是适配器最后打开的那张"才能关。宿主的 page
|
|
2368
|
+
// tab 把 contentId 固定写成 `sidebar://<kind>`(不是卡片 id),所以
|
|
2369
|
+
// 过去用 `contentId === card.id` 判归属永远匹配失败、关闭恒失效
|
|
2370
|
+
// (2.1.5 R2 的回归)。kind 单地址模型下 `cardId` 才是唯一归属信号。
|
|
2371
|
+
if (this.cardId !== card.id) return false
|
|
2372
|
+
// 卡片自己的会话优先:用户切走会话后关闭 A 的 tab,仍应落在 A。
|
|
2373
|
+
const ownerSession = typeof card.sessionId === 'string' && card.sessionId !== ''
|
|
2374
|
+
? card.sessionId
|
|
2375
|
+
: store.state.sessionId
|
|
2354
2376
|
let rows = []
|
|
2355
2377
|
try {
|
|
2356
2378
|
rows = typeof controller.openTabs?.getSnapshot === 'function'
|
|
@@ -2360,18 +2382,11 @@ window.__ModuleLoader__.load({
|
|
|
2360
2382
|
const row = Array.isArray(rows)
|
|
2361
2383
|
? rows.find(entry => entry?.kind === CARD_KIND
|
|
2362
2384
|
&& typeof entry?.tabId === 'string' && entry.tabId !== ''
|
|
2363
|
-
//
|
|
2364
|
-
//
|
|
2365
|
-
// `params.cardId`, which the rail publishes as `contentId`), or
|
|
2366
|
-
// when the host reported no id at all AND this adapter still holds
|
|
2367
|
-
// that card's address. Claiming a tab merely because `contentId`
|
|
2368
|
-
// was empty made "close A" close B's tab — and with it B's body.
|
|
2369
|
-
&& (entry.contentId === card.id
|
|
2370
|
-
|| ((entry.contentId === undefined || entry.contentId === null || entry.contentId === '')
|
|
2371
|
-
&& this.cardId === card.id))
|
|
2385
|
+
// 该 kind 只有一个 page 地址(不是 multiple),所以 kind +
|
|
2386
|
+
// 会话就唯一定位本卡那张 tab;归属由上面的 adapter.cardId 保证。
|
|
2372
2387
|
&& (typeof entry?.sessionId !== 'string'
|
|
2373
|
-
||
|
|
2374
|
-
|| entry.sessionId ===
|
|
2388
|
+
|| ownerSession === null
|
|
2389
|
+
|| entry.sessionId === ownerSession))
|
|
2375
2390
|
: undefined
|
|
2376
2391
|
if (row === undefined) return false
|
|
2377
2392
|
try {
|
|
@@ -2397,6 +2412,14 @@ window.__ModuleLoader__.load({
|
|
|
2397
2412
|
error: null,
|
|
2398
2413
|
dispose: null,
|
|
2399
2414
|
features: [],
|
|
2415
|
+
/**
|
|
2416
|
+
* The card whose open last took the single better tab.
|
|
2417
|
+
*
|
|
2418
|
+
* 0.28.x hosts every better tab in the native rail under one page
|
|
2419
|
+
* address per kind, so only the newest open owns it. Closing a card
|
|
2420
|
+
* that is not this owner must not close someone else's tab.
|
|
2421
|
+
*/
|
|
2422
|
+
cardId: null,
|
|
2400
2423
|
/**
|
|
2401
2424
|
* Why this adapter is (not) usable, in the settings page's own words.
|
|
2402
2425
|
* `null` means "usable"; a string explains the refusal.
|
|
@@ -2421,20 +2444,56 @@ window.__ModuleLoader__.load({
|
|
|
2421
2444
|
},
|
|
2422
2445
|
open(card) {
|
|
2423
2446
|
if (!this.available() || typeof this.service.openTab !== 'function') return false
|
|
2447
|
+
// 0.28.x 把 better 的每个描述符镜像成原生右栏类型,而该类型没有
|
|
2448
|
+
// `multiple`:同一 kind 在一个会话里只有一个 page 地址。再开一张卡
|
|
2449
|
+
// 会把这个地址导航到新卡,上一张既不在栏里也没有回到浮层 —— 和
|
|
2450
|
+
// native 适配器 2.1.5 R1 修的是同一个缺陷。这里同样先把同会话的
|
|
2451
|
+
// 其它 better 卡片迁回浮层。
|
|
2452
|
+
const targetSession = card.sessionId ?? store.state.sessionId ?? null
|
|
2453
|
+
for (const other of store.state.cards) {
|
|
2454
|
+
if (other.id === card.id || other.surface !== 'better-sidebar') continue
|
|
2455
|
+
const otherSession = typeof other.sessionId === 'string' && other.sessionId !== ''
|
|
2456
|
+
? other.sessionId
|
|
2457
|
+
: null
|
|
2458
|
+
if (targetSession !== null && otherSession !== null && otherSession !== targetSession) continue
|
|
2459
|
+
patchCard(other.id, { surface: 'flow', open: true })
|
|
2460
|
+
}
|
|
2424
2461
|
const tabId = `${CARD_KIND_BETTER}:${card.id}`
|
|
2425
2462
|
// `meta` is the transport for the card id (feature `tabMeta`), and
|
|
2426
2463
|
// `dedupeKey` on our descriptor is what collapses repeat opens onto
|
|
2427
|
-
// the same tab; both are part of the stable consumer contract
|
|
2428
|
-
// (`lib/types/client/service.d.ts`, unchanged across 0.22.0 → 0.22.1).
|
|
2464
|
+
// the same tab; both are part of the stable consumer contract.
|
|
2429
2465
|
this.service.openTab(
|
|
2430
2466
|
{ type: CARD_KIND_BETTER, id: tabId, title: card.question.slice(0, 32), meta: { cardId: card.id } },
|
|
2431
|
-
|
|
2467
|
+
typeof targetSession === 'string' && targetSession !== '' ? { sessionId: targetSession } : undefined,
|
|
2432
2468
|
)
|
|
2469
|
+
this.cardId = card.id
|
|
2433
2470
|
return true
|
|
2434
2471
|
},
|
|
2435
2472
|
close(card) {
|
|
2436
2473
|
if (!this.available() || typeof this.service.closeTab !== 'function') return false
|
|
2437
|
-
|
|
2474
|
+
// 单一 page 地址:只有最后 open 的那张卡拥有栏里的 tab。
|
|
2475
|
+
if (this.cardId !== card.id) return false
|
|
2476
|
+
const sessionId = typeof card.sessionId === 'string' && card.sessionId !== ''
|
|
2477
|
+
? card.sessionId
|
|
2478
|
+
: (store.state.sessionId ?? undefined)
|
|
2479
|
+
const scope = typeof sessionId === 'string' && sessionId !== '' ? { sessionId } : undefined
|
|
2480
|
+
// 0.28.x 的原生 tab id 由宿主 mint(tab1…),不是我们传入的 seed id;
|
|
2481
|
+
// CardHost 挂载时把真实 id 记进 embeddedTabIds。
|
|
2482
|
+
const mountedId = embeddedTabIds.get(card.id)
|
|
2483
|
+
if (typeof mountedId === 'string' && mountedId !== '') {
|
|
2484
|
+
this.service.closeTab(mountedId, scope)
|
|
2485
|
+
this.cardId = null
|
|
2486
|
+
return true
|
|
2487
|
+
}
|
|
2488
|
+
// 卡片还没挂载过(或已从 store 移除):按 kind + 会话在栏的清单里
|
|
2489
|
+
// 找到唯一那张 tab,经原生控制器关闭。
|
|
2490
|
+
if (closeRailRow(CARD_KIND_BETTER, sessionId)) {
|
|
2491
|
+
this.cardId = null
|
|
2492
|
+
return true
|
|
2493
|
+
}
|
|
2494
|
+
// 0.19 之前的 bottom workbench:seed id 就是 tab id。
|
|
2495
|
+
this.service.closeTab(`${CARD_KIND_BETTER}:${card.id}`, scope)
|
|
2496
|
+
this.cardId = null
|
|
2438
2497
|
return true
|
|
2439
2498
|
},
|
|
2440
2499
|
},
|
|
@@ -2520,6 +2579,17 @@ window.__ModuleLoader__.load({
|
|
|
2520
2579
|
*/
|
|
2521
2580
|
let renderedCardId = null
|
|
2522
2581
|
|
|
2582
|
+
/**
|
|
2583
|
+
* Host tab ids for cards mounted by an external embedder, keyed by card id.
|
|
2584
|
+
*
|
|
2585
|
+
* better-sidebar's descriptor component receives the host `SidebarTab`, whose
|
|
2586
|
+
* `id` is the tab id the native rail minted (`tab1`, …) — NOT the internal
|
|
2587
|
+
* `sidecard-ask-better:<cardId>` seed id. Since 0.28.x every better-sidebar
|
|
2588
|
+
* open lands in DSH's native rail, so `closeTab(seed.id)` is a strict no-op.
|
|
2589
|
+
* The body records the real id here and `surfaces.better.close` uses it.
|
|
2590
|
+
*/
|
|
2591
|
+
const embeddedTabIds = new Map()
|
|
2592
|
+
|
|
2523
2593
|
/**
|
|
2524
2594
|
* Pick the surface for a new card.
|
|
2525
2595
|
* @param {string} wanted - `auto` | adapter kind.
|
|
@@ -5980,6 +6050,8 @@ body[data-ds-dark-theme] .dsa-seg button[data-on="true"]:hover{
|
|
|
5980
6050
|
truncated: card.truncated === true,
|
|
5981
6051
|
droppedChars: Number(card.droppedChars ?? 0),
|
|
5982
6052
|
createdAt: Number.isFinite(card.createdAt) ? card.createdAt : Date.now(),
|
|
6053
|
+
// 卡片归属的会话,随历史一起持久化;旧条目没有该字段时按 null 读。
|
|
6054
|
+
sessionId: typeof card.sessionId === 'string' && card.sessionId !== '' ? card.sessionId : null,
|
|
5983
6055
|
provider: typeof card.provider === 'string' ? card.provider : '',
|
|
5984
6056
|
elapsedMs: Number.isFinite(card.elapsedMs) ? card.elapsedMs : null,
|
|
5985
6057
|
// 持久化的历史轮次与上下文预算同一个上限(HISTORY_TURNS):
|
|
@@ -6073,6 +6145,8 @@ body[data-ds-dark-theme] .dsa-seg button[data-on="true"]:hover{
|
|
|
6073
6145
|
provider: typeof entry.provider === 'string' && entry.provider !== '' ? entry.provider : null,
|
|
6074
6146
|
elapsedMs: Number.isFinite(entry.elapsedMs) ? entry.elapsedMs : null,
|
|
6075
6147
|
createdAt: Number.isFinite(entry.createdAt) ? entry.createdAt : 0,
|
|
6148
|
+
// 刷新后仍要能把追问/改投送回原来的会话;旧条目没有该字段。
|
|
6149
|
+
sessionId: typeof entry.sessionId === 'string' && entry.sessionId !== '' ? entry.sessionId : null,
|
|
6076
6150
|
// P2-5b: replayed versions stay switchable after a reload.
|
|
6077
6151
|
versions: replayVersions(entry.versions),
|
|
6078
6152
|
viewingVersion: null,
|
|
@@ -6123,6 +6197,8 @@ body[data-ds-dark-theme] .dsa-seg button[data-on="true"]:hover{
|
|
|
6123
6197
|
provider: typeof entry.provider === 'string' && entry.provider !== '' ? entry.provider : null,
|
|
6124
6198
|
elapsedMs: Number.isFinite(entry.elapsedMs) ? entry.elapsedMs : null,
|
|
6125
6199
|
createdAt: Number.isFinite(entry.createdAt) ? entry.createdAt : 0,
|
|
6200
|
+
// 旧历史没有这个字段:留 null,调用方回退到当前会话。
|
|
6201
|
+
sessionId: typeof entry.sessionId === 'string' && entry.sessionId !== '' ? entry.sessionId : null,
|
|
6126
6202
|
notified: true,
|
|
6127
6203
|
versions: replayVersions(entry.versions),
|
|
6128
6204
|
viewingVersion: null,
|
|
@@ -6136,6 +6212,81 @@ body[data-ds-dark-theme] .dsa-seg button[data-on="true"]:hover{
|
|
|
6136
6212
|
: [],
|
|
6137
6213
|
})
|
|
6138
6214
|
|
|
6215
|
+
/**
|
|
6216
|
+
* Close the one rail tab a kind owns in one session.
|
|
6217
|
+
*
|
|
6218
|
+
* The rail keeps one page tab per kind (no `multiple`), and its
|
|
6219
|
+
* `contentId` is `sidebar://<kind>` rather than any card id. Both
|
|
6220
|
+
* external surfaces therefore locate a tab by kind + session, never by
|
|
6221
|
+
* the id a body was opened with.
|
|
6222
|
+
* @param {string} kind - the tab kind registered with the rail.
|
|
6223
|
+
* @param {string|null|undefined} sessionId - the owning session, when known.
|
|
6224
|
+
* @returns {boolean} whether a tab was found and the rail accepted the close.
|
|
6225
|
+
*/
|
|
6226
|
+
function closeRailRow(kind, sessionId) {
|
|
6227
|
+
const controller = surfaces.native === null ? null : surfaces.native.controller
|
|
6228
|
+
if (controller === null || controller === undefined) return false
|
|
6229
|
+
let rows = []
|
|
6230
|
+
try {
|
|
6231
|
+
rows = typeof controller.openTabs?.getSnapshot === 'function'
|
|
6232
|
+
? controller.openTabs.getSnapshot()
|
|
6233
|
+
: []
|
|
6234
|
+
} catch { return false }
|
|
6235
|
+
if (!Array.isArray(rows)) return false
|
|
6236
|
+
const candidates = rows.filter(entry => entry?.kind === kind
|
|
6237
|
+
&& typeof entry?.tabId === 'string' && entry.tabId !== '')
|
|
6238
|
+
// 会话已知时按会话匹配;会话未知(旧历史没有 sessionId)时只有
|
|
6239
|
+
// 全进程恰好一张该 kind 的 tab 才敢关,否则可能误关别的会话。
|
|
6240
|
+
const row = typeof sessionId === 'string' && sessionId !== ''
|
|
6241
|
+
? candidates.find(entry => typeof entry?.sessionId !== 'string'
|
|
6242
|
+
|| entry.sessionId === sessionId)
|
|
6243
|
+
: (candidates.length === 1 ? candidates[0] : undefined)
|
|
6244
|
+
if (row === undefined) return false
|
|
6245
|
+
try {
|
|
6246
|
+
if (typeof controller.closeIn === 'function' && typeof row.sessionId === 'string') {
|
|
6247
|
+
controller.closeIn(row.sessionId, row.tabId)
|
|
6248
|
+
} else if (typeof controller.close === 'function') {
|
|
6249
|
+
controller.close(row.tabId)
|
|
6250
|
+
} else {
|
|
6251
|
+
return false
|
|
6252
|
+
}
|
|
6253
|
+
return true
|
|
6254
|
+
} catch {
|
|
6255
|
+
return false
|
|
6256
|
+
}
|
|
6257
|
+
}
|
|
6258
|
+
|
|
6259
|
+
/**
|
|
6260
|
+
* Release a better-sidebar tab that no longer has a live card body.
|
|
6261
|
+
*
|
|
6262
|
+
* The descriptor component records the host's real tab id on mount;
|
|
6263
|
+
* without that (a tab restored from a previous page load, or a delete
|
|
6264
|
+
* before the body mounted) the rail inventory locates it by kind.
|
|
6265
|
+
* @param {string} id - the card id whose tab should go away.
|
|
6266
|
+
* @param {string|null|undefined} sessionId - the card's own session.
|
|
6267
|
+
*/
|
|
6268
|
+
function releaseBetterTab(id, sessionId) {
|
|
6269
|
+
const better = surfaces.better
|
|
6270
|
+
if (better === null || better === undefined) return
|
|
6271
|
+
if (better.service === null || typeof better.service?.closeTab !== 'function') return
|
|
6272
|
+
// 单一 page 地址:只有最后 open 的那张卡拥有栏里的 tab;释放一张
|
|
6273
|
+
// 不是当前所有者的历史条目,会把现在真正在栏里的卡片连带关掉。
|
|
6274
|
+
if (better.cardId !== id) return
|
|
6275
|
+
const scope = typeof sessionId === 'string' && sessionId !== '' ? { sessionId } : undefined
|
|
6276
|
+
const mountedId = embeddedTabIds.get(id)
|
|
6277
|
+
if (typeof mountedId === 'string' && mountedId !== '') {
|
|
6278
|
+
try { better.service.closeTab(mountedId, scope) } catch { /* the host may already be gone */ }
|
|
6279
|
+
better.cardId = null
|
|
6280
|
+
return
|
|
6281
|
+
}
|
|
6282
|
+
if (closeRailRow(CARD_KIND_BETTER, sessionId)) {
|
|
6283
|
+
better.cardId = null
|
|
6284
|
+
return
|
|
6285
|
+
}
|
|
6286
|
+
try { better.service.closeTab(`${CARD_KIND_BETTER}:${id}`, scope) } catch { /* legacy no-op */ }
|
|
6287
|
+
better.cardId = null
|
|
6288
|
+
}
|
|
6289
|
+
|
|
6139
6290
|
/**
|
|
6140
6291
|
* Close the right-rail tab when it is showing this card.
|
|
6141
6292
|
*
|
|
@@ -6145,14 +6296,15 @@ body[data-ds-dark-theme] .dsa-seg button[data-on="true"]:hover{
|
|
|
6145
6296
|
* no longer exists, until the user closed it by hand. Only the tab this
|
|
6146
6297
|
* adapter actually opened is touched; `close` re-checks ownership.
|
|
6147
6298
|
* @param {string} id - the card id that is going away.
|
|
6299
|
+
* @param {string|null|undefined} sessionId - the card's own session.
|
|
6148
6300
|
*/
|
|
6149
|
-
function releaseNativeTab(id) {
|
|
6301
|
+
function releaseNativeTab(id, sessionId) {
|
|
6150
6302
|
const native = surfaces.native
|
|
6151
6303
|
if (native === null || native === undefined) return
|
|
6152
6304
|
if (native.controller === null || native.controller === undefined) return
|
|
6153
6305
|
if (native.cardId !== id) return
|
|
6154
6306
|
try {
|
|
6155
|
-
native.close({ id })
|
|
6307
|
+
native.close({ id, sessionId })
|
|
6156
6308
|
} catch { /* the rail owns its tab's lifetime */ }
|
|
6157
6309
|
}
|
|
6158
6310
|
|
|
@@ -6191,12 +6343,21 @@ body[data-ds-dark-theme] .dsa-seg button[data-on="true"]:hover{
|
|
|
6191
6343
|
},
|
|
6192
6344
|
/** Delete one entry from durable history (and its live card). */
|
|
6193
6345
|
deleteHistoryCard(id) {
|
|
6346
|
+
const entry = store.state.history.find(item => item.id === id)
|
|
6194
6347
|
const next = store.state.history
|
|
6195
6348
|
.filter(item => item.id !== id)
|
|
6196
6349
|
.slice(0, historyCap())
|
|
6197
6350
|
// A deleted entry must not keep a right-rail tab alive: the tab is
|
|
6198
6351
|
// not part of the store, so nothing else would ever close it.
|
|
6199
|
-
|
|
6352
|
+
// The live card (when the manager is deleting one) carries the
|
|
6353
|
+
// session the entry itself may predate.
|
|
6354
|
+
const live = entry === undefined
|
|
6355
|
+
? store.state.cards.find(card => card.id === id)
|
|
6356
|
+
: undefined
|
|
6357
|
+
if (entry !== undefined || live !== undefined) {
|
|
6358
|
+
releaseBetterTab(id, entry?.sessionId ?? live?.sessionId)
|
|
6359
|
+
}
|
|
6360
|
+
releaseNativeTab(id, entry?.sessionId ?? live?.sessionId)
|
|
6200
6361
|
writePersisted(next)
|
|
6201
6362
|
store.set({
|
|
6202
6363
|
history: next,
|
|
@@ -6224,7 +6385,7 @@ body[data-ds-dark-theme] .dsa-seg button[data-on="true"]:hover{
|
|
|
6224
6385
|
const controller = controllers.get(id)
|
|
6225
6386
|
if (controller !== undefined) {
|
|
6226
6387
|
controller.abort()
|
|
6227
|
-
void postJson('cancel', { id, sessionId:
|
|
6388
|
+
void postJson('cancel', { id, sessionId: cardSessionId(card, ctx) }).catch(() => {})
|
|
6228
6389
|
}
|
|
6229
6390
|
controllers.delete(id)
|
|
6230
6391
|
pendingDeltas.delete(id)
|
|
@@ -6244,7 +6405,7 @@ body[data-ds-dark-theme] .dsa-seg button[data-on="true"]:hover{
|
|
|
6244
6405
|
// A card restored from durable history has no live `surface`, so the
|
|
6245
6406
|
// branch above cannot see its tab; this checks the adapter's own
|
|
6246
6407
|
// address instead and is a no-op when nothing is on the rail.
|
|
6247
|
-
releaseNativeTab(id)
|
|
6408
|
+
releaseNativeTab(id, card.sessionId)
|
|
6248
6409
|
const next = store.state.history
|
|
6249
6410
|
.filter(item => item.id !== id)
|
|
6250
6411
|
.slice(0, historyCap())
|
|
@@ -6278,7 +6439,14 @@ body[data-ds-dark-theme] .dsa-seg button[data-on="true"]:hover{
|
|
|
6278
6439
|
const railCard = surfaces.native?.cardId
|
|
6279
6440
|
if (typeof railCard === 'string' && railCard !== ''
|
|
6280
6441
|
&& !store.state.cards.some(card => card.id === railCard)) {
|
|
6281
|
-
|
|
6442
|
+
const railSession = store.state.history.find(item => item.id === railCard)?.sessionId
|
|
6443
|
+
releaseNativeTab(railCard, railSession)
|
|
6444
|
+
}
|
|
6445
|
+
// better 承载面同理:历史条目被清空后,只为「没有活跃卡片」的
|
|
6446
|
+
// 条目释放它可能留下的栏 tab。
|
|
6447
|
+
for (const entry of store.state.history) {
|
|
6448
|
+
if (store.state.cards.some(card => card.id === entry.id)) continue
|
|
6449
|
+
releaseBetterTab(entry.id, entry.sessionId)
|
|
6282
6450
|
}
|
|
6283
6451
|
try {
|
|
6284
6452
|
for (const key of [PERSIST_KEY, PERSIST_V2_KEY, PERSIST_LEGACY_KEY]) {
|
|
@@ -6303,6 +6471,8 @@ body[data-ds-dark-theme] .dsa-seg button[data-on="true"]:hover{
|
|
|
6303
6471
|
droppedChars: cut.droppedChars,
|
|
6304
6472
|
zone: candidate.zone,
|
|
6305
6473
|
carrier: payload.carrier,
|
|
6474
|
+
// 发起会话:卡片可能活过会话切换,停止/改投/追问必须回到它。
|
|
6475
|
+
sessionId: currentSessionId(ctx),
|
|
6306
6476
|
status: 'pending',
|
|
6307
6477
|
text: '',
|
|
6308
6478
|
reasoning: '',
|
|
@@ -6330,7 +6500,7 @@ body[data-ds-dark-theme] .dsa-seg button[data-on="true"]:hover{
|
|
|
6330
6500
|
try {
|
|
6331
6501
|
const result = await askInMainConversation({
|
|
6332
6502
|
ctx,
|
|
6333
|
-
sessionId:
|
|
6503
|
+
sessionId: cardSessionId(card, ctx),
|
|
6334
6504
|
selection: candidate.text,
|
|
6335
6505
|
question: payload.question,
|
|
6336
6506
|
zone: candidate.zone,
|
|
@@ -6517,7 +6687,7 @@ body[data-ds-dark-theme] .dsa-seg button[data-on="true"]:hover{
|
|
|
6517
6687
|
else {
|
|
6518
6688
|
void askInMainConversation({
|
|
6519
6689
|
ctx,
|
|
6520
|
-
sessionId:
|
|
6690
|
+
sessionId: cardSessionId(next, ctx),
|
|
6521
6691
|
selection: next.selection,
|
|
6522
6692
|
question: text,
|
|
6523
6693
|
zone: next.zone,
|
|
@@ -6585,7 +6755,7 @@ body[data-ds-dark-theme] .dsa-seg button[data-on="true"]:hover{
|
|
|
6585
6755
|
// so POST /cancel too. Fire-and-forget on purpose — the card is
|
|
6586
6756
|
// already stopped locally, and a dead host must not turn the
|
|
6587
6757
|
// Stop button into a failure.
|
|
6588
|
-
void postJson('cancel', { id: card.id, sessionId:
|
|
6758
|
+
void postJson('cancel', { id: card.id, sessionId: cardSessionId(card, ctx) }).catch(() => {})
|
|
6589
6759
|
}
|
|
6590
6760
|
patchCard(card.id, { status: 'stopped' })
|
|
6591
6761
|
},
|
|
@@ -6593,7 +6763,7 @@ body[data-ds-dark-theme] .dsa-seg button[data-on="true"]:hover{
|
|
|
6593
6763
|
try {
|
|
6594
6764
|
const result = await askInMainConversation({
|
|
6595
6765
|
ctx,
|
|
6596
|
-
sessionId:
|
|
6766
|
+
sessionId: cardSessionId(card, ctx),
|
|
6597
6767
|
selection: card.selection,
|
|
6598
6768
|
question: card.question,
|
|
6599
6769
|
zone: card.zone,
|
|
@@ -6634,7 +6804,7 @@ body[data-ds-dark-theme] .dsa-seg button[data-on="true"]:hover{
|
|
|
6634
6804
|
controller.abort()
|
|
6635
6805
|
// Same host-side release as the Stop button: closing a card
|
|
6636
6806
|
// mid-stream must not leave the child running server-side.
|
|
6637
|
-
void postJson('cancel', { id: card.id, sessionId:
|
|
6807
|
+
void postJson('cancel', { id: card.id, sessionId: cardSessionId(card, ctx) }).catch(() => {})
|
|
6638
6808
|
}
|
|
6639
6809
|
controllers.delete(card.id)
|
|
6640
6810
|
// A closed card's buffered-but-unflushed deltas belong to nobody:
|
|
@@ -6767,6 +6937,13 @@ body[data-ds-dark-theme] .dsa-seg button[data-on="true"]:hover{
|
|
|
6767
6937
|
const runSideCard = async (cardId) => {
|
|
6768
6938
|
const card = store.state.cards.find(item => item.id === cardId)
|
|
6769
6939
|
if (card === undefined) return
|
|
6940
|
+
// 追问/重试沿用卡片自己的会话,而不是用户此刻切到的会话。
|
|
6941
|
+
const ownerSession = typeof card.sessionId === 'string' && card.sessionId !== ''
|
|
6942
|
+
? card.sessionId
|
|
6943
|
+
: currentSessionId(ctx)
|
|
6944
|
+
if (typeof ownerSession === 'string' && ownerSession !== '' && card.sessionId !== ownerSession) {
|
|
6945
|
+
patchCard(cardId, { sessionId: ownerSession })
|
|
6946
|
+
}
|
|
6770
6947
|
const controller = new AbortController()
|
|
6771
6948
|
controllers.set(cardId, controller)
|
|
6772
6949
|
// #9: wall time from the (re)start of THIS attempt to its terminal
|
|
@@ -6788,7 +6965,7 @@ body[data-ds-dark-theme] .dsa-seg button[data-on="true"]:hover{
|
|
|
6788
6965
|
selection: card.selected,
|
|
6789
6966
|
zone: card.zone,
|
|
6790
6967
|
carrier: 'side',
|
|
6791
|
-
sessionId:
|
|
6968
|
+
sessionId: ownerSession,
|
|
6792
6969
|
history: card.history.slice(-HISTORY_TURNS).map(turn => ({
|
|
6793
6970
|
question: String(turn?.question ?? '').slice(0, HISTORY_TURN_Q_CHARS),
|
|
6794
6971
|
answer: String(turn?.answer ?? '').slice(0, HISTORY_TURN_A_CHARS),
|
|
@@ -7357,11 +7534,15 @@ body[data-ds-dark-theme] .dsa-seg button[data-on="true"]:hover{
|
|
|
7357
7534
|
const state = useStoreState()
|
|
7358
7535
|
const cardId = hostCardIdOf(props)
|
|
7359
7536
|
const card = state.cards.find(item => item.id === cardId)
|
|
7537
|
+
// The embedder's REAL tab id (better-sidebar 0.28.x mints it and
|
|
7538
|
+
// ignores the seed id we passed), recorded for close/delete.
|
|
7539
|
+
const hostTabId = typeof props?.tab?.id === 'string' && props.tab.id !== '' ? props.tab.id : null
|
|
7360
7540
|
// The proof hook MUST run before any early return (hook order), and
|
|
7361
7541
|
// it is what lets `submit` detect a host that never rendered us.
|
|
7362
7542
|
React.useEffect(() => {
|
|
7363
7543
|
if (typeof cardId !== 'string' || cardId === '') return undefined
|
|
7364
7544
|
renderedCardId = cardId
|
|
7545
|
+
if (hostTabId !== null) embeddedTabIds.set(cardId, hostTabId)
|
|
7365
7546
|
// Mounting settles the render proof, even when this host mounted
|
|
7366
7547
|
// late (a lazy tab panel slower than the 600ms timer) — the proof
|
|
7367
7548
|
// must never fire after the card is demonstrably on screen.
|
|
@@ -7372,8 +7553,11 @@ body[data-ds-dark-theme] .dsa-seg button[data-on="true"]:hover{
|
|
|
7372
7553
|
}
|
|
7373
7554
|
return () => {
|
|
7374
7555
|
if (renderedCardId === cardId) renderedCardId = null
|
|
7556
|
+
if (hostTabId !== null && embeddedTabIds.get(cardId) === hostTabId) {
|
|
7557
|
+
embeddedTabIds.delete(cardId)
|
|
7558
|
+
}
|
|
7375
7559
|
}
|
|
7376
|
-
}, [cardId])
|
|
7560
|
+
}, [cardId, hostTabId])
|
|
7377
7561
|
if (card === undefined) {
|
|
7378
7562
|
return h('div', { className: 'dsa-settings', 'data-dsa-root': '', 'data-dsa-rail-host': '' },
|
|
7379
7563
|
h(Styles, null),
|
package/index.js
CHANGED
|
@@ -45,7 +45,7 @@ export const name = 'dsh-sidecard-ask'
|
|
|
45
45
|
export const inject = ['webServer']
|
|
46
46
|
|
|
47
47
|
/** Version of this plugin (kept in step with package.json by test/verify.mjs). */
|
|
48
|
-
export const PLUGIN_VERSION = '2.1.
|
|
48
|
+
export const PLUGIN_VERSION = '2.1.6'
|
|
49
49
|
|
|
50
50
|
/** Route prefix of the plugin's own API. */
|
|
51
51
|
export const ROUTE_PREFIX = '/sidecard-ask/api'
|