dsh-sidenote 0.3.2 → 0.3.4
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/README.md +71 -40
- package/lib/client.js +960 -634
- package/lib/types/client/host/contracts.d.ts +45 -0
- package/lib/types/client/sidechat/lifecycle.d.ts +36 -2
- package/lib/types/client/sidechat/native.d.ts +87 -0
- package/lib/types/client/sidechat/open.d.ts +5 -1
- package/lib/types/client/sidechat/slash.d.ts +3 -1
- package/package.json +1 -1
- package/src/client/host/contracts.ts +29 -0
- package/src/client/host/probes.ts +27 -0
- package/src/client/index.tsx +8 -0
- package/src/client/sidechat/ModelMenu.tsx +4 -4
- package/src/client/sidechat/SideChatPanel.tsx +57 -1
- package/src/client/sidechat/index.tsx +13 -3
- package/src/client/sidechat/lifecycle.ts +128 -22
- package/src/client/sidechat/native.ts +192 -0
- package/src/client/sidechat/open.ts +63 -7
- package/src/client/sidechat/slash.ts +46 -13
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* better-sidebar >= 0.19 / DSH 0.1.5 bridge.
|
|
3
|
+
*
|
|
4
|
+
* From 0.19 on, better-sidebar no longer owns the right column: it hands it to
|
|
5
|
+
* DSH's native right sidebar (`dsh-client-ui-sidebar-right`) and routes tab
|
|
6
|
+
* lifecycle calls to that surface (`openTab`/`activateTab`/`updateTab`).
|
|
7
|
+
* Sidechat tabs opened that way never enter the legacy layout store
|
|
8
|
+
* (splits/bottomSplits/floats) that `collectSideTabs` walks, so the minted tab
|
|
9
|
+
* id cannot be recovered from the snapshot — after `openTab` there is no
|
|
10
|
+
* `updateTab` target until the panel mounts and publishes itself. Drafts must
|
|
11
|
+
* therefore ride the `openTab` seed.meta (which the native surface honors) or
|
|
12
|
+
* wait for the panel's registration.
|
|
13
|
+
*
|
|
14
|
+
* This module keeps that missing link: a registry of live side-chat panels
|
|
15
|
+
* (their native tab id, their draft sink, their current meta) plus a lazy
|
|
16
|
+
* root-context holder. The root context is the plugin's own fiber — unlike the
|
|
17
|
+
* slot-provided context a panel receives, its inject list contains
|
|
18
|
+
* `workspaces`, so host calls made from it keep working on 0.1.5.
|
|
19
|
+
*
|
|
20
|
+
* Authority: dsh-better-sidebar `src/client/service.ts` (>= 0.19 surface
|
|
21
|
+
* routing) and `@deepseek-ai/dsh-api-session-controller` (remote session face).
|
|
22
|
+
*/
|
|
23
|
+
import type { Context } from '../host/contracts.ts'
|
|
24
|
+
|
|
25
|
+
/** better-sidebar version that moved the right column to DSH's native sidebar. */
|
|
26
|
+
const NATIVE_SIDEBAR_MINOR = 19
|
|
27
|
+
|
|
28
|
+
let root: Context | undefined
|
|
29
|
+
|
|
30
|
+
/** Remember the plugin root context at activation (see module docstring). */
|
|
31
|
+
export function setRootContext(ctx: Context): void {
|
|
32
|
+
root = ctx
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Plugin root context; falls back to the caller's context (legacy behaviour). */
|
|
36
|
+
export function rootContext(fallback: Context): Context {
|
|
37
|
+
return root ?? fallback
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* True when the host keeps sidebar tabs outside the legacy layout store
|
|
42
|
+
* (better-sidebar >= 0.19). Version parse is best-effort: an unparsable
|
|
43
|
+
* version reports false, which keeps the pre-0.19 behaviour.
|
|
44
|
+
*/
|
|
45
|
+
export function nativeSidebarHost(ctx: Context): boolean {
|
|
46
|
+
try {
|
|
47
|
+
const [major = 0, minor = 0] = ctx.betterSidebar.version.split('.').map(part => Number.parseInt(part, 10))
|
|
48
|
+
return major > 0 || minor >= NATIVE_SIDEBAR_MINOR
|
|
49
|
+
} catch {
|
|
50
|
+
return false
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
interface LiveSideChat {
|
|
55
|
+
readonly tabId: string
|
|
56
|
+
readonly sessionId: string
|
|
57
|
+
/** Write text into the panel composer (draft delivery). */
|
|
58
|
+
seedDraft(text: string): void
|
|
59
|
+
/** Current tab meta, read at call time (the panel re-renders on meta change). */
|
|
60
|
+
readMeta(): unknown
|
|
61
|
+
/** Current tab title(聚焦列表/编号数据源的读取面)。 */
|
|
62
|
+
readTitle(): string
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Native tab id → live panel. Native layouts are memory-only, so is this. */
|
|
66
|
+
const liveSideChats = new Map<string, LiveSideChat>()
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* tabId → 首次注册序号(打开序的近似)。tab.id 跨重挂载稳定,重注册不改写
|
|
70
|
+
* ——遍历 Map 的插入序会随重挂漂移,「最近打开」只能锚在首次序号上。
|
|
71
|
+
*/
|
|
72
|
+
const birthOrder = new Map<string, number>()
|
|
73
|
+
let birthSeq = 0
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Publish a mounted panel so programmatic entry points can focus it and seed
|
|
77
|
+
* its draft. Returns the disposer for the panel's effect cleanup.
|
|
78
|
+
*/
|
|
79
|
+
export function registerLiveSideChat(
|
|
80
|
+
sessionId: string,
|
|
81
|
+
tabId: string,
|
|
82
|
+
seedDraft: (text: string) => void,
|
|
83
|
+
readMeta: () => unknown,
|
|
84
|
+
readTitle: () => string,
|
|
85
|
+
): () => void {
|
|
86
|
+
const entry: LiveSideChat = { tabId, sessionId, seedDraft, readMeta, readTitle }
|
|
87
|
+
liveSideChats.set(tabId, entry)
|
|
88
|
+
if (!birthOrder.has(tabId)) birthOrder.set(tabId, ++birthSeq)
|
|
89
|
+
// 挂载即回放在 openTab→注册窗口内暂存的草稿(seedDraft 内有相位门)。
|
|
90
|
+
const opening = openings.get(sessionId)
|
|
91
|
+
if (opening !== undefined) {
|
|
92
|
+
openings.delete(sessionId)
|
|
93
|
+
for (const draft of opening.drafts) seedDraft(draft)
|
|
94
|
+
}
|
|
95
|
+
return () => {
|
|
96
|
+
if (liveSideChats.get(tabId) === entry) liveSideChats.delete(tabId)
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// ── openTab→面板挂载窗口的 in-flight 标记 ────────────────────────────────────
|
|
101
|
+
// native tab 铸造后对布局快照与 registry 同时不可见,直到面板挂载登记;窗口内
|
|
102
|
+
// 的重复触发必须落进标记(草稿暂存、注册时回放),而不是再开一个 tab
|
|
103
|
+
// (双开 = 双 fork 出孤儿会话)。legacy 宿主无此窗口(openTab 同步落快照)。
|
|
104
|
+
|
|
105
|
+
/** 标记失效应力:openTab 被静默拒绝/面板未挂载时,超时后允许重试。 */
|
|
106
|
+
const OPENING_TTL_MS = 10_000
|
|
107
|
+
|
|
108
|
+
interface OpeningMark {
|
|
109
|
+
readonly at: number
|
|
110
|
+
/** 窗口内到达的待投递草稿(面板注册时经 seedDraft 回放)。 */
|
|
111
|
+
readonly drafts: string[]
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const openings = new Map<string, OpeningMark>()
|
|
115
|
+
|
|
116
|
+
/** 标记一次已发起、面板尚未注册的 native 打开(create/reopen 铸造后调用)。 */
|
|
117
|
+
export function markSideChatOpening(sessionId: string): void {
|
|
118
|
+
openings.set(sessionId, { at: Date.now(), drafts: [] })
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** 进行中的打开;TTL 外视为 openTab 被拒/面板未挂载,清除并返回 undefined。 */
|
|
122
|
+
export function sideChatOpening(sessionId: string): OpeningMark | undefined {
|
|
123
|
+
const mark = openings.get(sessionId)
|
|
124
|
+
if (mark === undefined) return undefined
|
|
125
|
+
if (Date.now() - mark.at > OPENING_TTL_MS) {
|
|
126
|
+
openings.delete(sessionId)
|
|
127
|
+
return undefined
|
|
128
|
+
}
|
|
129
|
+
return mark
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** Live panel for a native tab id (undefined for legacy layout tabs). */
|
|
133
|
+
export function liveSideChat(tabId: string): LiveSideChat | undefined {
|
|
134
|
+
return liveSideChats.get(tabId)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** 一个会话的全部存活面板(按打开序升序;native 下枚举 side tab 的唯一数据源)。 */
|
|
138
|
+
export function liveSideChatsOf(sessionId: string): LiveSideChat[] {
|
|
139
|
+
return [...liveSideChats.values()]
|
|
140
|
+
.filter(entry => entry.sessionId === sessionId)
|
|
141
|
+
.sort((a, b) => (birthOrder.get(a.tabId) ?? 0) - (birthOrder.get(b.tabId) ?? 0))
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Most recently OPENED live panel of a session — the open-or-focus target
|
|
146
|
+
* when the legacy snapshot cannot see the tab (mirrors "last opened wins").
|
|
147
|
+
* 排序锚在首次注册序号上:面板重挂载不翻序(见 birthOrder)。
|
|
148
|
+
*/
|
|
149
|
+
export function lastLiveSideChat(sessionId: string): LiveSideChat | undefined {
|
|
150
|
+
const all = liveSideChatsOf(sessionId)
|
|
151
|
+
return all[all.length - 1]
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** 插件卸载/重载时清场(apply 的 ctx.effect disposer 调用)。 */
|
|
155
|
+
export function clearNativeRuntime(): void {
|
|
156
|
+
root = undefined
|
|
157
|
+
liveSideChats.clear()
|
|
158
|
+
openings.clear()
|
|
159
|
+
birthOrder.clear()
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** Native tab shell for reads that go through the layout snapshot (`readTab`). */
|
|
163
|
+
export function nativeTabShell(tabId: string, type: string): { id: string; type: string; title: string; meta: unknown } | undefined {
|
|
164
|
+
const entry = liveSideChats.get(tabId)
|
|
165
|
+
if (entry === undefined) return undefined
|
|
166
|
+
return { id: entry.tabId, type, title: '', meta: entry.readMeta() }
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* 聚焦一个 native tab:better-sidebar >= 0.19 的 `surface.activate` 是空操作
|
|
171
|
+
* (只返回记录存在性——authority: dsh-better-sidebar src/client/native/surface.ts),
|
|
172
|
+
* 真实聚焦面是 dsh 0.1.5 的 `ISidebarRight.focus`(authority:
|
|
173
|
+
* dsh-client-ui-sidebar-right lib/types/client/service.d.ts `focus(tabId)`)。
|
|
174
|
+
* focus 只标记活动 tab/pane,不展开折叠的栏——聚焦的语义包含「让用户看见」,
|
|
175
|
+
* 故折叠时补 toggleExpanded(authority: 同文件 `isExpanded/toggleExpanded`)。
|
|
176
|
+
* @returns true = 已通过 focus 聚焦;false = 面缺席(调用方回退 activateTab)。
|
|
177
|
+
*/
|
|
178
|
+
export function focusNativeTab(ctx: Context, tabId: string): boolean {
|
|
179
|
+
try {
|
|
180
|
+
const face = ctx.get('sidebarRight') as {
|
|
181
|
+
focus?: (id: string) => void
|
|
182
|
+
isExpanded?: () => boolean
|
|
183
|
+
toggleExpanded?: () => void
|
|
184
|
+
} | undefined
|
|
185
|
+
if (typeof face?.focus !== 'function') return false
|
|
186
|
+
face.focus(tabId)
|
|
187
|
+
if (face.isExpanded?.() === false) face.toggleExpanded?.()
|
|
188
|
+
return true
|
|
189
|
+
} catch {
|
|
190
|
+
return false
|
|
191
|
+
}
|
|
192
|
+
}
|
|
@@ -16,10 +16,16 @@ import {
|
|
|
16
16
|
sideTabTitle,
|
|
17
17
|
} from './model.ts'
|
|
18
18
|
import { readInputDraft, resolveSessionInput } from './composer.ts'
|
|
19
|
+
import { focusNativeTab, lastLiveSideChat, liveSideChatsOf, markSideChatOpening, nativeSidebarHost, nativeTabShell, sideChatOpening } from './native.ts'
|
|
19
20
|
|
|
20
|
-
/**
|
|
21
|
+
/**
|
|
22
|
+
* 从最新快照读一个 Tab(meta 合并写入前的读取面;布局即注册表)。
|
|
23
|
+
* better-sidebar >= 0.19: the open tab lives in the native right sidebar, not
|
|
24
|
+
* in the layout snapshot — fall back to the live-panel registry.
|
|
25
|
+
*/
|
|
21
26
|
export function readTab(ctx: Context, tabId: string): SidebarTab | undefined {
|
|
22
|
-
|
|
27
|
+
const tab = collectTabs(ctx.betterSidebar.getSnapshot().state).find(candidate => candidate.id === tabId)
|
|
28
|
+
return tab ?? nativeTabShell(tabId, SIDE_TAB_TYPE)
|
|
23
29
|
}
|
|
24
30
|
|
|
25
31
|
/**
|
|
@@ -33,6 +39,17 @@ export function openOrFocusSideChat(ctx: Context, sessionId: string, draftText?:
|
|
|
33
39
|
if (snapshot.sessionId !== sessionId || snapshot.state === undefined) return false
|
|
34
40
|
const existing = collectSideTabs(snapshot.state)
|
|
35
41
|
|
|
42
|
+
// Native right sidebar (better-sidebar >= 0.19): the tab is invisible to the
|
|
43
|
+
// layout snapshot, so focus the live panel and hand it the draft directly.
|
|
44
|
+
const live = lastLiveSideChat(sessionId)
|
|
45
|
+
if (live !== undefined) {
|
|
46
|
+
if (draftText !== undefined && draftText !== '') live.seedDraft(draftText)
|
|
47
|
+
// native 宿主的 activateTab 是空操作(surface.activate 只查记录存在性),
|
|
48
|
+
// 聚焦走 ISidebarRight.focus 探测;面缺席(legacy)回退 activateTab。
|
|
49
|
+
if (!focusNativeTab(ctx, live.tabId)) ctx.betterSidebar.activateTab(live.tabId, { sessionId })
|
|
50
|
+
return true
|
|
51
|
+
}
|
|
52
|
+
|
|
36
53
|
if (existing.length > 0) {
|
|
37
54
|
const target = existing[existing.length - 1]!
|
|
38
55
|
if (draftText !== undefined && draftText !== '') {
|
|
@@ -50,6 +67,15 @@ export function openOrFocusSideChat(ctx: Context, sessionId: string, draftText?:
|
|
|
50
67
|
return true
|
|
51
68
|
}
|
|
52
69
|
|
|
70
|
+
// openTab→面板挂载窗口(native 特有):tab 已铸但对快照与 registry 均不可见,
|
|
71
|
+
// 重复触发落进 in-flight 标记(草稿暂存、注册时回放)而非再开一个
|
|
72
|
+
// (双开 = 双 fork 出孤儿会话)。legacy 无此窗口(openTab 同步落快照)。
|
|
73
|
+
const opening = sideChatOpening(sessionId)
|
|
74
|
+
if (opening !== undefined) {
|
|
75
|
+
if (draftText !== undefined && draftText !== '') opening.drafts.push(draftText)
|
|
76
|
+
return true
|
|
77
|
+
}
|
|
78
|
+
|
|
53
79
|
return createSideChat(ctx, sessionId, draftText)
|
|
54
80
|
} catch (error) {
|
|
55
81
|
console.warn('[dsh-sidenote] 打开侧边聊天失败:', error)
|
|
@@ -66,12 +92,26 @@ export function createSideChat(ctx: Context, sessionId: string, draftText?: stri
|
|
|
66
92
|
try {
|
|
67
93
|
const snapshot = ctx.betterSidebar.getSnapshot()
|
|
68
94
|
if (snapshot.sessionId !== sessionId || snapshot.state === undefined) return false
|
|
95
|
+
// openTab 的已知静默失败面先排除(类型在设置里被禁用 = 宿主 warn 后 return,
|
|
96
|
+
// 不抛错)——否则 native 分支的乐观返回会把失败报成成功。
|
|
97
|
+
if (!ctx.betterSidebar.isTabEnabled(SIDE_TAB_TYPE)) return false
|
|
69
98
|
// 新建:openTab 走 createTab 铸造(seed.meta 会被忽略),所以先记下既有
|
|
70
99
|
// id 集,openTab 同步落状态后找出新 Tab,再把 pendingDraft 写进它的 meta。
|
|
71
100
|
const before = new Set(collectTabs(snapshot.state).map(tab => tab.id))
|
|
72
|
-
|
|
101
|
+
// Native tabs (better-sidebar >= 0.19) take meta from the seed: the minted id
|
|
102
|
+
// only becomes known once the panel mounts and publishes itself.
|
|
103
|
+
const seed = draftText !== undefined && draftText !== ''
|
|
104
|
+
? { type: SIDE_TAB_TYPE, meta: { pendingDraft: draftText } }
|
|
105
|
+
: { type: SIDE_TAB_TYPE }
|
|
106
|
+
ctx.betterSidebar.openTab(seed, { sessionId })
|
|
73
107
|
const created = collectSideTabs(ctx.betterSidebar.getSnapshot().state).find(tab => !before.has(tab.id))
|
|
74
|
-
if (created === undefined)
|
|
108
|
+
if (created === undefined) {
|
|
109
|
+
if (!nativeSidebarHost(ctx)) return false
|
|
110
|
+
// native:tab 不进快照属预期——openTab 不抛错即视为成功;标记 in-flight
|
|
111
|
+
// 窗口供并发触发去重(窗口语意见 native.ts)。
|
|
112
|
+
markSideChatOpening(sessionId)
|
|
113
|
+
return true
|
|
114
|
+
}
|
|
75
115
|
if (draftText !== undefined && draftText !== '') {
|
|
76
116
|
ctx.betterSidebar.updateTab(created.id, { meta: { pendingDraft: draftText } })
|
|
77
117
|
}
|
|
@@ -93,6 +133,9 @@ export function sideChatTargetTitle(ctx: Context, sessionId: string): string | u
|
|
|
93
133
|
if (snapshot.sessionId !== sessionId || snapshot.state === undefined) return undefined
|
|
94
134
|
const existing = collectSideTabs(snapshot.state)
|
|
95
135
|
if (existing.length > 0) return existing[existing.length - 1]!.title
|
|
136
|
+
// native:布局快照看不到 tab,改读 live registry(最近打开者)。
|
|
137
|
+
const live = lastLiveSideChat(sessionId)
|
|
138
|
+
if (live !== undefined) return live.readTitle()
|
|
96
139
|
return sideTabTitle([])
|
|
97
140
|
} catch {
|
|
98
141
|
return undefined
|
|
@@ -109,13 +152,26 @@ export function reopenSideChat(ctx: Context, sessionId: string, childId: string,
|
|
|
109
152
|
try {
|
|
110
153
|
const snapshot = ctx.betterSidebar.getSnapshot()
|
|
111
154
|
if (snapshot.sessionId !== sessionId || snapshot.state === undefined) return false
|
|
155
|
+
if (!ctx.betterSidebar.isTabEnabled(SIDE_TAB_TYPE)) return false
|
|
156
|
+
// native 下同 kind 每 pane 单实例(dsh sidebar-right held 规则):有存活
|
|
157
|
+
// 实例时 openTab 必折叠为聚焦既有 tab 且 seed.meta 被丢弃——拒绝(弹层
|
|
158
|
+
// 侧此时本就不列重开项,这里是双保险)。
|
|
159
|
+
if (nativeSidebarHost(ctx) && liveSideChatsOf(sessionId).length > 0) return false
|
|
112
160
|
const before = new Set(collectTabs(snapshot.state).map(tab => tab.id))
|
|
113
|
-
|
|
161
|
+
// native(>= 0.19):seed.meta 携带恢复信息(native 面采纳 seed.meta;
|
|
162
|
+
// 面板挂载即走绑定恢复路径)。legacy 的 createTab 铸造忽略 seed.meta,
|
|
163
|
+
// 落快照后补写。
|
|
164
|
+
const meta = { childId, parentSessionId: sessionId }
|
|
165
|
+
ctx.betterSidebar.openTab({ type: SIDE_TAB_TYPE, ...(title !== undefined ? { title } : {}), meta }, { sessionId })
|
|
114
166
|
const created = collectSideTabs(ctx.betterSidebar.getSnapshot().state).find(tab => !before.has(tab.id))
|
|
115
|
-
if (created === undefined)
|
|
167
|
+
if (created === undefined) {
|
|
168
|
+
if (!nativeSidebarHost(ctx)) return false
|
|
169
|
+
markSideChatOpening(sessionId)
|
|
170
|
+
return true
|
|
171
|
+
}
|
|
116
172
|
ctx.betterSidebar.updateTab(created.id, {
|
|
117
173
|
...(title !== undefined ? { title } : {}),
|
|
118
|
-
meta
|
|
174
|
+
meta,
|
|
119
175
|
})
|
|
120
176
|
return true
|
|
121
177
|
} catch (error) {
|
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
* /side 斜杠命令(spike 落点):经 client 侧 `ctx.commandUi.register`
|
|
3
3
|
* (dsh-client-ui-commands 的 CommandContribution,ui 形态 popupSelect)
|
|
4
4
|
* 注册。popupSelect 是该注册面唯一的 UI 形态 —— 菜单行被选中后弹一个
|
|
5
|
-
* 选项壳,选项 = 新建 +
|
|
5
|
+
* 选项壳,选项 = 新建 + 当前会话已并存的侧边聊天(聚焦)+ 最近关闭(重开)。
|
|
6
|
+
* native 宿主(better-sidebar >= 0.19)受 dsh sidebar-right 的 held 规则约束
|
|
7
|
+
* (page kind 每 pane 单实例):有存活实例时不列「新建」与「重开」。
|
|
6
8
|
*
|
|
7
9
|
* 服务经 ctx.get 惰性解析(commandUi 不在 inject 清单里):服务缺失、
|
|
8
10
|
* 注册抛错(如与 host 命令撞名)都降级为「只有 Tab 入口」,绝不影响面板。
|
|
@@ -12,6 +14,7 @@
|
|
|
12
14
|
*/
|
|
13
15
|
import type { Context } from '../host/contracts.ts'
|
|
14
16
|
import { canForkFrom, collectSideTabs } from './model.ts'
|
|
17
|
+
import { focusNativeTab, liveSideChatsOf, nativeSidebarHost } from './native.ts'
|
|
15
18
|
import { createSideChat, reopenSideChat } from './open.ts'
|
|
16
19
|
import { dropClosedSideChat, listClosedSideChats } from './recentClosed.ts'
|
|
17
20
|
import { t } from '../locales.ts'
|
|
@@ -32,7 +35,11 @@ interface SelectOption {
|
|
|
32
35
|
interface CommandUiService {
|
|
33
36
|
register(contribution: {
|
|
34
37
|
readonly name: string
|
|
35
|
-
|
|
38
|
+
// 契约在 0.1.5 从 string 改为 () => string(authority:
|
|
39
|
+
// dsh-client-ui-commands 0.1.5-rc.2 contract.d.ts `readonly description: () => string`;
|
|
40
|
+
// 0.1.1~0.1.3 为 string)。传错形状会在候选合成时炸掉整个斜杠菜单
|
|
41
|
+
// (ui-input-trigger 的 contribution.description() 对 string 抛 TypeError)。
|
|
42
|
+
readonly description: string | (() => string)
|
|
36
43
|
available(session: CommandSession): boolean
|
|
37
44
|
readonly ui: {
|
|
38
45
|
readonly kind: 'popupSelect'
|
|
@@ -42,28 +49,52 @@ interface CommandUiService {
|
|
|
42
49
|
}): () => void
|
|
43
50
|
}
|
|
44
51
|
|
|
52
|
+
/** 0.1.5 信号:`connection.api.sessions` 面在 0.1.5 被删(与模型面迁移同判据)。 */
|
|
53
|
+
function isDsh015Plus(ctx: Context): boolean {
|
|
54
|
+
const legacy = (ctx as { connection?: { api?: { sessions?: unknown } } }).connection?.api?.sessions
|
|
55
|
+
return legacy === undefined || legacy === null
|
|
56
|
+
}
|
|
57
|
+
|
|
45
58
|
/** 单个命令名对应的完整贡献(side / 侧边 共用)。 */
|
|
46
59
|
function makeContribution(ctx: Context, name: string) {
|
|
47
60
|
return {
|
|
48
61
|
name,
|
|
49
|
-
description: t('cmdDesc'),
|
|
62
|
+
description: isDsh015Plus(ctx) ? () => t('cmdDesc') : t('cmdDesc'),
|
|
50
63
|
available: (session: CommandSession) => canForkFrom(ctx, session.sessionId),
|
|
51
64
|
ui: {
|
|
52
65
|
kind: 'popupSelect' as const,
|
|
53
66
|
options: (session: CommandSession) => {
|
|
54
|
-
const options: SelectOption[] = [
|
|
55
|
-
|
|
56
|
-
]
|
|
67
|
+
const options: SelectOption[] = []
|
|
68
|
+
const native = nativeSidebarHost(ctx)
|
|
69
|
+
const lives = native ? liveSideChatsOf(session.sessionId) : []
|
|
70
|
+
// native 下同 kind 每 pane 单实例(dsh-client-ui-sidebar-right 的 held
|
|
71
|
+
// 规则:page kind 的 contentId 恒为 sidebar://<kind>,openTab 必折叠为
|
|
72
|
+
// 聚焦既有 tab)——已有存活实例时「新建」名不副实,不列。
|
|
73
|
+
if (!native || lives.length === 0) {
|
|
74
|
+
options.push({ id: 'new', label: t('cmdNew'), detail: t('cmdNewDetail') })
|
|
75
|
+
}
|
|
57
76
|
// 已并存的侧边聊天列为聚焦项(命令弹层即多实例管理入口)。
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
77
|
+
// native(>= 0.19):tab 不进布局快照,枚举改读 live registry;
|
|
78
|
+
// legacy:读布局快照(现状)。
|
|
79
|
+
if (native) {
|
|
80
|
+
for (const live of lives) {
|
|
81
|
+
options.push({ id: `focus:${live.tabId}`, label: t('cmdFocus', { title: live.readTitle() }), detail: t('cmdFocusDetail') })
|
|
82
|
+
}
|
|
83
|
+
} else {
|
|
84
|
+
const snapshot = ctx.betterSidebar.getSnapshot()
|
|
85
|
+
if (snapshot.sessionId === session.sessionId && snapshot.state !== undefined) {
|
|
86
|
+
for (const tab of collectSideTabs(snapshot.state)) {
|
|
87
|
+
options.push({ id: `focus:${tab.id}`, label: t('cmdFocus', { title: tab.title }), detail: t('cmdFocusDetail') })
|
|
88
|
+
}
|
|
62
89
|
}
|
|
63
90
|
}
|
|
64
91
|
// D3 后悔药:最近关闭的可重开(Cmd+Shift+T 心智)。
|
|
65
|
-
|
|
66
|
-
|
|
92
|
+
// native 单实例约束下,有存活实例时 reopen 必被 held 折叠(seed.meta
|
|
93
|
+
// 丢弃且聚焦错对象)——此时不列重开项。
|
|
94
|
+
if (!(native && lives.length > 0)) {
|
|
95
|
+
for (const entry of listClosedSideChats(session.sessionId)) {
|
|
96
|
+
options.push({ id: `reopen:${entry.childId}`, label: t('cmdReopen', { title: entry.title }), detail: t('cmdReopenDetail') })
|
|
97
|
+
}
|
|
67
98
|
}
|
|
68
99
|
return Promise.resolve(options)
|
|
69
100
|
},
|
|
@@ -75,7 +106,9 @@ function makeContribution(ctx: Context, name: string) {
|
|
|
75
106
|
return
|
|
76
107
|
}
|
|
77
108
|
if (option.id.startsWith('focus:')) {
|
|
78
|
-
|
|
109
|
+
const tabId = option.id.slice('focus:'.length)
|
|
110
|
+
// native 宿主的 activateTab 是空操作——聚焦走 ISidebarRight.focus 探测。
|
|
111
|
+
if (!focusNativeTab(ctx, tabId)) ctx.betterSidebar.activateTab(tabId, { sessionId: session.sessionId })
|
|
79
112
|
return
|
|
80
113
|
}
|
|
81
114
|
if (option.id.startsWith('reopen:')) {
|