dsh-sidenote 0.3.2 → 0.3.3
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 +68 -40
- package/lib/client.js +834 -553
- 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 +80 -0
- package/lib/types/client/sidechat/open.d.ts +5 -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 +4 -0
- package/src/client/sidechat/ModelMenu.tsx +4 -4
- package/src/client/sidechat/SideChatPanel.tsx +56 -1
- package/src/client/sidechat/lifecycle.ts +128 -22
- package/src/client/sidechat/native.ts +168 -0
- package/src/client/sidechat/open.ts +56 -7
- package/src/client/sidechat/slash.ts +12 -2
|
@@ -0,0 +1,168 @@
|
|
|
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
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Native tab id → live panel. Native layouts are memory-only, so is this. */
|
|
64
|
+
const liveSideChats = new Map<string, LiveSideChat>()
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Publish a mounted panel so programmatic entry points can focus it and seed
|
|
68
|
+
* its draft. Returns the disposer for the panel's effect cleanup.
|
|
69
|
+
*/
|
|
70
|
+
export function registerLiveSideChat(
|
|
71
|
+
sessionId: string,
|
|
72
|
+
tabId: string,
|
|
73
|
+
seedDraft: (text: string) => void,
|
|
74
|
+
readMeta: () => unknown,
|
|
75
|
+
): () => void {
|
|
76
|
+
const entry: LiveSideChat = { tabId, sessionId, seedDraft, readMeta }
|
|
77
|
+
liveSideChats.set(tabId, entry)
|
|
78
|
+
// 挂载即回放在 openTab→注册窗口内暂存的草稿(seedDraft 内有相位门)。
|
|
79
|
+
const opening = openings.get(sessionId)
|
|
80
|
+
if (opening !== undefined) {
|
|
81
|
+
openings.delete(sessionId)
|
|
82
|
+
for (const draft of opening.drafts) seedDraft(draft)
|
|
83
|
+
}
|
|
84
|
+
return () => {
|
|
85
|
+
if (liveSideChats.get(tabId) === entry) liveSideChats.delete(tabId)
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ── openTab→面板挂载窗口的 in-flight 标记 ────────────────────────────────────
|
|
90
|
+
// native tab 铸造后对布局快照与 registry 同时不可见,直到面板挂载登记;窗口内
|
|
91
|
+
// 的重复触发必须落进标记(草稿暂存、注册时回放),而不是再开一个 tab
|
|
92
|
+
// (双开 = 双 fork 出孤儿会话)。legacy 宿主无此窗口(openTab 同步落快照)。
|
|
93
|
+
|
|
94
|
+
/** 标记失效应力:openTab 被静默拒绝/面板未挂载时,超时后允许重试。 */
|
|
95
|
+
const OPENING_TTL_MS = 10_000
|
|
96
|
+
|
|
97
|
+
interface OpeningMark {
|
|
98
|
+
readonly at: number
|
|
99
|
+
/** 窗口内到达的待投递草稿(面板注册时经 seedDraft 回放)。 */
|
|
100
|
+
readonly drafts: string[]
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const openings = new Map<string, OpeningMark>()
|
|
104
|
+
|
|
105
|
+
/** 标记一次已发起、面板尚未注册的 native 打开(create/reopen 铸造后调用)。 */
|
|
106
|
+
export function markSideChatOpening(sessionId: string): void {
|
|
107
|
+
openings.set(sessionId, { at: Date.now(), drafts: [] })
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** 进行中的打开;TTL 外视为 openTab 被拒/面板未挂载,清除并返回 undefined。 */
|
|
111
|
+
export function sideChatOpening(sessionId: string): OpeningMark | undefined {
|
|
112
|
+
const mark = openings.get(sessionId)
|
|
113
|
+
if (mark === undefined) return undefined
|
|
114
|
+
if (Date.now() - mark.at > OPENING_TTL_MS) {
|
|
115
|
+
openings.delete(sessionId)
|
|
116
|
+
return undefined
|
|
117
|
+
}
|
|
118
|
+
return mark
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Live panel for a native tab id (undefined for legacy layout tabs). */
|
|
122
|
+
export function liveSideChat(tabId: string): LiveSideChat | undefined {
|
|
123
|
+
return liveSideChats.get(tabId)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Most recently registered live panel of a session — the open-or-focus target
|
|
128
|
+
* when the legacy snapshot cannot see the tab (mirrors "last opened wins").
|
|
129
|
+
*/
|
|
130
|
+
export function lastLiveSideChat(sessionId: string): LiveSideChat | undefined {
|
|
131
|
+
let found: LiveSideChat | undefined
|
|
132
|
+
for (const entry of liveSideChats.values()) {
|
|
133
|
+
if (entry.sessionId === sessionId) found = entry
|
|
134
|
+
}
|
|
135
|
+
return found
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** Native tab shell for reads that go through the layout snapshot (`readTab`). */
|
|
139
|
+
export function nativeTabShell(tabId: string, type: string): { id: string; type: string; title: string; meta: unknown } | undefined {
|
|
140
|
+
const entry = liveSideChats.get(tabId)
|
|
141
|
+
if (entry === undefined) return undefined
|
|
142
|
+
return { id: entry.tabId, type, title: '', meta: entry.readMeta() }
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* 聚焦一个 native tab:better-sidebar >= 0.19 的 `surface.activate` 是空操作
|
|
147
|
+
* (只返回记录存在性——authority: dsh-better-sidebar src/client/native/surface.ts),
|
|
148
|
+
* 真实聚焦面是 dsh 0.1.5 的 `ISidebarRight.focus`(authority:
|
|
149
|
+
* dsh-client-ui-sidebar-right lib/types/client/service.d.ts `focus(tabId)`)。
|
|
150
|
+
* focus 只标记活动 tab/pane,不展开折叠的栏——聚焦的语义包含「让用户看见」,
|
|
151
|
+
* 故折叠时补 toggleExpanded(authority: 同文件 `isExpanded/toggleExpanded`)。
|
|
152
|
+
* @returns true = 已通过 focus 聚焦;false = 面缺席(调用方回退 activateTab)。
|
|
153
|
+
*/
|
|
154
|
+
export function focusNativeTab(ctx: Context, tabId: string): boolean {
|
|
155
|
+
try {
|
|
156
|
+
const face = ctx.get('sidebarRight') as {
|
|
157
|
+
focus?: (id: string) => void
|
|
158
|
+
isExpanded?: () => boolean
|
|
159
|
+
toggleExpanded?: () => void
|
|
160
|
+
} | undefined
|
|
161
|
+
if (typeof face?.focus !== 'function') return false
|
|
162
|
+
face.focus(tabId)
|
|
163
|
+
if (face.isExpanded?.() === false) face.toggleExpanded?.()
|
|
164
|
+
return true
|
|
165
|
+
} catch {
|
|
166
|
+
return false
|
|
167
|
+
}
|
|
168
|
+
}
|
|
@@ -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, 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
|
}
|
|
@@ -109,13 +149,22 @@ export function reopenSideChat(ctx: Context, sessionId: string, childId: string,
|
|
|
109
149
|
try {
|
|
110
150
|
const snapshot = ctx.betterSidebar.getSnapshot()
|
|
111
151
|
if (snapshot.sessionId !== sessionId || snapshot.state === undefined) return false
|
|
152
|
+
if (!ctx.betterSidebar.isTabEnabled(SIDE_TAB_TYPE)) return false
|
|
112
153
|
const before = new Set(collectTabs(snapshot.state).map(tab => tab.id))
|
|
113
|
-
|
|
154
|
+
// native(>= 0.19):seed.meta 携带恢复信息(native 面采纳 seed.meta;
|
|
155
|
+
// 面板挂载即走绑定恢复路径)。legacy 的 createTab 铸造忽略 seed.meta,
|
|
156
|
+
// 落快照后补写。
|
|
157
|
+
const meta = { childId, parentSessionId: sessionId }
|
|
158
|
+
ctx.betterSidebar.openTab({ type: SIDE_TAB_TYPE, ...(title !== undefined ? { title } : {}), meta }, { sessionId })
|
|
114
159
|
const created = collectSideTabs(ctx.betterSidebar.getSnapshot().state).find(tab => !before.has(tab.id))
|
|
115
|
-
if (created === undefined)
|
|
160
|
+
if (created === undefined) {
|
|
161
|
+
if (!nativeSidebarHost(ctx)) return false
|
|
162
|
+
markSideChatOpening(sessionId)
|
|
163
|
+
return true
|
|
164
|
+
}
|
|
116
165
|
ctx.betterSidebar.updateTab(created.id, {
|
|
117
166
|
...(title !== undefined ? { title } : {}),
|
|
118
|
-
meta
|
|
167
|
+
meta,
|
|
119
168
|
})
|
|
120
169
|
return true
|
|
121
170
|
} catch (error) {
|
|
@@ -32,7 +32,11 @@ interface SelectOption {
|
|
|
32
32
|
interface CommandUiService {
|
|
33
33
|
register(contribution: {
|
|
34
34
|
readonly name: string
|
|
35
|
-
|
|
35
|
+
// 契约在 0.1.5 从 string 改为 () => string(authority:
|
|
36
|
+
// dsh-client-ui-commands 0.1.5-rc.2 contract.d.ts `readonly description: () => string`;
|
|
37
|
+
// 0.1.1~0.1.3 为 string)。传错形状会在候选合成时炸掉整个斜杠菜单
|
|
38
|
+
// (ui-input-trigger 的 contribution.description() 对 string 抛 TypeError)。
|
|
39
|
+
readonly description: string | (() => string)
|
|
36
40
|
available(session: CommandSession): boolean
|
|
37
41
|
readonly ui: {
|
|
38
42
|
readonly kind: 'popupSelect'
|
|
@@ -42,11 +46,17 @@ interface CommandUiService {
|
|
|
42
46
|
}): () => void
|
|
43
47
|
}
|
|
44
48
|
|
|
49
|
+
/** 0.1.5 信号:`connection.api.sessions` 面在 0.1.5 被删(与模型面迁移同判据)。 */
|
|
50
|
+
function isDsh015Plus(ctx: Context): boolean {
|
|
51
|
+
const legacy = (ctx as { connection?: { api?: { sessions?: unknown } } }).connection?.api?.sessions
|
|
52
|
+
return legacy === undefined || legacy === null
|
|
53
|
+
}
|
|
54
|
+
|
|
45
55
|
/** 单个命令名对应的完整贡献(side / 侧边 共用)。 */
|
|
46
56
|
function makeContribution(ctx: Context, name: string) {
|
|
47
57
|
return {
|
|
48
58
|
name,
|
|
49
|
-
description: t('cmdDesc'),
|
|
59
|
+
description: isDsh015Plus(ctx) ? () => t('cmdDesc') : t('cmdDesc'),
|
|
50
60
|
available: (session: CommandSession) => canForkFrom(ctx, session.sessionId),
|
|
51
61
|
ui: {
|
|
52
62
|
kind: 'popupSelect' as const,
|