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
|
@@ -216,6 +216,51 @@ export interface ModelSelection {
|
|
|
216
216
|
model: string;
|
|
217
217
|
reasoningEffort?: string;
|
|
218
218
|
}
|
|
219
|
+
/**
|
|
220
|
+
* Remote session face(dsh >= 0.1.5 的模型读写照面;0.1.5 删除了
|
|
221
|
+
* `connection.api.sessions.*` 的 client 面后由它接管)。
|
|
222
|
+
* `remote`/`remote.session` 在宿主侧是可注入服务,但本插件不声明 inject
|
|
223
|
+
* (0.1.2 及更早无此服务,声明会让插件在旧宿主上永不激活)——调用点经
|
|
224
|
+
* `ctx.get` 惰性探测(cordis 的 get 不受 inject 门禁限制)。
|
|
225
|
+
* Authority:`@deepseek-ai/dsh-api-session-controller/lib/typert.remote-client.d.ts`
|
|
226
|
+
* (RemoteResult = `{ok: true, value} | {ok: false, error}`;error 实为
|
|
227
|
+
* RemoteError 实例,code 必有、message 继承 Error,这里弱化为可读取切片)。
|
|
228
|
+
*/
|
|
229
|
+
export interface RemoteSessionModelFace {
|
|
230
|
+
selectModel(request: {
|
|
231
|
+
sessionId: SessionId;
|
|
232
|
+
provider: string;
|
|
233
|
+
model: string;
|
|
234
|
+
reasoningEffort?: string;
|
|
235
|
+
}): Promise<{
|
|
236
|
+
ok: true;
|
|
237
|
+
value: unknown;
|
|
238
|
+
} | {
|
|
239
|
+
ok: false;
|
|
240
|
+
error: {
|
|
241
|
+
code?: string;
|
|
242
|
+
message?: string;
|
|
243
|
+
};
|
|
244
|
+
}>;
|
|
245
|
+
/** 宿主级模型目录(authority 同上,`ModelCatalog`)。 */
|
|
246
|
+
modelCatalog?(): Promise<{
|
|
247
|
+
ok: true;
|
|
248
|
+
value: ModelCatalogFace;
|
|
249
|
+
} | {
|
|
250
|
+
ok: false;
|
|
251
|
+
error: {
|
|
252
|
+
code?: string;
|
|
253
|
+
message?: string;
|
|
254
|
+
};
|
|
255
|
+
}>;
|
|
256
|
+
}
|
|
257
|
+
/** `remote.session.modelCatalog()` 的返回切片(authority: 同上 `ModelCatalog`)。 */
|
|
258
|
+
export interface ModelCatalogFace {
|
|
259
|
+
readonly default: ModelSelection;
|
|
260
|
+
readonly routableProviders: readonly string[];
|
|
261
|
+
readonly groups: SessionModelsResult['groups'];
|
|
262
|
+
readonly failures?: readonly unknown[];
|
|
263
|
+
}
|
|
219
264
|
export interface SessionModelsResult {
|
|
220
265
|
current: ModelSelection;
|
|
221
266
|
routable: boolean;
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* off-face 探测纪律:session.open()、store.update 均为运行时可达但契约不
|
|
7
7
|
* 保证的面——就地 feature-check + 吞错降级,并登记进 host/probes.ts。
|
|
8
8
|
*/
|
|
9
|
-
import type { Context, ConversationSnapshot, SessionBinding, SessionFace, SessionModelsResult } from '../host/contracts.ts';
|
|
9
|
+
import type { Context, ConversationSnapshot, ModelSelection, RemoteSessionModelFace, SessionBinding, SessionFace, SessionModelsResult } from '../host/contracts.ts';
|
|
10
10
|
import { type SideChatMeta } from './model.ts';
|
|
11
11
|
/**
|
|
12
12
|
* 首开编排:fork(全量历史快照)→ 登记 meta(先行,绝不丢登记)→
|
|
@@ -30,6 +30,36 @@ export declare function ensurePanelOpen(store: unknown): void;
|
|
|
30
30
|
* 运行时自动 open)。feature-check + 吞错,运行时若移除则降级为只发不收。
|
|
31
31
|
*/
|
|
32
32
|
export declare function openSessionWindow(session: SessionFace | undefined): void;
|
|
33
|
+
/**
|
|
34
|
+
* Current model selection of a session, read from its `modelSelection`
|
|
35
|
+
* projection (dsh >= 0.1.2): the durable selection is projected state, so no RPC
|
|
36
|
+
* is needed and the read is safe for any session id. Snapshot shape authority:
|
|
37
|
+
* `@deepseek-ai/dsh-api-session-controller/lib/types/types.d.ts`
|
|
38
|
+
* (`ModelSelectionProjection = { lastUsed, next }`,两者均可为 null——
|
|
39
|
+
* `next` 缺省时按投影文档回落 `lastUsed`)。
|
|
40
|
+
*/
|
|
41
|
+
export declare function projectedModelSelection(ctx: Context, sessionId: string): ModelSelection | undefined;
|
|
42
|
+
/**
|
|
43
|
+
* Remote session face carrying `selectModel`/`modelCatalog`(dsh 0.1.5 删除
|
|
44
|
+
* `connection.api.sessions.*` client 面后的模型 RPC 面;事实上 0.1.2-rc.1
|
|
45
|
+
* 起存在——authority: dsh-api-session-controller lib/typert.remote-client.d.ts)。
|
|
46
|
+
* 探测走 `ctx.get`(cordis 的 get 不受 inject 门禁限制,本插件为兼容旧宿主
|
|
47
|
+
* 不声明 inject 'remote.session';机制见 contracts.ts RemoteSessionModelFace)。
|
|
48
|
+
* `ctx.get('remote')` 返回命名空间容器时下钻其 `.session` 成员。
|
|
49
|
+
*/
|
|
50
|
+
export declare function remoteSessionFace(ctx: Context): RemoteSessionModelFace | undefined;
|
|
51
|
+
/**
|
|
52
|
+
* 读会话当前模型选择:投影面(0.1.2+,同步无 RPC)优先;旧
|
|
53
|
+
* `connection.api.sessions.models` RPC(<= 0.1.2 的 client 面,0.1.5 已删)
|
|
54
|
+
* 回退(双版本链,同 chatSourceOf 先例)。永不抛错。
|
|
55
|
+
*/
|
|
56
|
+
export declare function readModelSelection(ctx: Context, sessionId: string): Promise<ModelSelection | undefined>;
|
|
57
|
+
/**
|
|
58
|
+
* 写会话模型选择:`remote.session.selectModel`(新面)优先,旧
|
|
59
|
+
* `connection.api.sessions.selectModel` 回退。面在而调用失败(模型不可路由等)
|
|
60
|
+
* 属真实失败,不再落旧面重试。
|
|
61
|
+
*/
|
|
62
|
+
export declare function writeModelSelection(ctx: Context, sessionId: string, selection: ModelSelection): Promise<boolean>;
|
|
33
63
|
/** 读子会话当前模型名(composer 模型标签用);任何失败回退 null(默认文案)。 */
|
|
34
64
|
export declare function readModelName(ctx: Context, sessionId: string): Promise<string | null>;
|
|
35
65
|
/** 会话内容读取源(0.1.1/0.1.2 双兼容,feature-check 优先链)。 */
|
|
@@ -46,7 +76,11 @@ export interface ChatSource {
|
|
|
46
76
|
* 既有降级,不崩)。
|
|
47
77
|
*/
|
|
48
78
|
export declare function chatSourceOf(ctx: Context, binding: SessionBinding | undefined): ChatSource | undefined;
|
|
49
|
-
/**
|
|
79
|
+
/**
|
|
80
|
+
* 拉会话模型目录:0.1.5 走 `remote.session.modelCatalog()`(目录是宿主级,
|
|
81
|
+
* 菜单选中项所需的 current 由投影/旧面补上);旧面回退
|
|
82
|
+
* `connection.api.sessions.models`。失败 null → 菜单保持只读标签态。
|
|
83
|
+
*/
|
|
50
84
|
export declare function listModels(ctx: Context, sessionId: string): Promise<SessionModelsResult | null>;
|
|
51
85
|
/** 切换子会话模型;成功返回新模型展示名,失败 null(best-effort)。 */
|
|
52
86
|
export declare function switchModel(ctx: Context, sessionId: string, provider: string, model: string): Promise<string | null>;
|
|
@@ -0,0 +1,80 @@
|
|
|
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
|
+
/** Remember the plugin root context at activation (see module docstring). */
|
|
25
|
+
export declare function setRootContext(ctx: Context): void;
|
|
26
|
+
/** Plugin root context; falls back to the caller's context (legacy behaviour). */
|
|
27
|
+
export declare function rootContext(fallback: Context): Context;
|
|
28
|
+
/**
|
|
29
|
+
* True when the host keeps sidebar tabs outside the legacy layout store
|
|
30
|
+
* (better-sidebar >= 0.19). Version parse is best-effort: an unparsable
|
|
31
|
+
* version reports false, which keeps the pre-0.19 behaviour.
|
|
32
|
+
*/
|
|
33
|
+
export declare function nativeSidebarHost(ctx: Context): boolean;
|
|
34
|
+
interface LiveSideChat {
|
|
35
|
+
readonly tabId: string;
|
|
36
|
+
readonly sessionId: string;
|
|
37
|
+
/** Write text into the panel composer (draft delivery). */
|
|
38
|
+
seedDraft(text: string): void;
|
|
39
|
+
/** Current tab meta, read at call time (the panel re-renders on meta change). */
|
|
40
|
+
readMeta(): unknown;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Publish a mounted panel so programmatic entry points can focus it and seed
|
|
44
|
+
* its draft. Returns the disposer for the panel's effect cleanup.
|
|
45
|
+
*/
|
|
46
|
+
export declare function registerLiveSideChat(sessionId: string, tabId: string, seedDraft: (text: string) => void, readMeta: () => unknown): () => void;
|
|
47
|
+
interface OpeningMark {
|
|
48
|
+
readonly at: number;
|
|
49
|
+
/** 窗口内到达的待投递草稿(面板注册时经 seedDraft 回放)。 */
|
|
50
|
+
readonly drafts: string[];
|
|
51
|
+
}
|
|
52
|
+
/** 标记一次已发起、面板尚未注册的 native 打开(create/reopen 铸造后调用)。 */
|
|
53
|
+
export declare function markSideChatOpening(sessionId: string): void;
|
|
54
|
+
/** 进行中的打开;TTL 外视为 openTab 被拒/面板未挂载,清除并返回 undefined。 */
|
|
55
|
+
export declare function sideChatOpening(sessionId: string): OpeningMark | undefined;
|
|
56
|
+
/** Live panel for a native tab id (undefined for legacy layout tabs). */
|
|
57
|
+
export declare function liveSideChat(tabId: string): LiveSideChat | undefined;
|
|
58
|
+
/**
|
|
59
|
+
* Most recently registered live panel of a session — the open-or-focus target
|
|
60
|
+
* when the legacy snapshot cannot see the tab (mirrors "last opened wins").
|
|
61
|
+
*/
|
|
62
|
+
export declare function lastLiveSideChat(sessionId: string): LiveSideChat | undefined;
|
|
63
|
+
/** Native tab shell for reads that go through the layout snapshot (`readTab`). */
|
|
64
|
+
export declare function nativeTabShell(tabId: string, type: string): {
|
|
65
|
+
id: string;
|
|
66
|
+
type: string;
|
|
67
|
+
title: string;
|
|
68
|
+
meta: unknown;
|
|
69
|
+
} | undefined;
|
|
70
|
+
/**
|
|
71
|
+
* 聚焦一个 native tab:better-sidebar >= 0.19 的 `surface.activate` 是空操作
|
|
72
|
+
* (只返回记录存在性——authority: dsh-better-sidebar src/client/native/surface.ts),
|
|
73
|
+
* 真实聚焦面是 dsh 0.1.5 的 `ISidebarRight.focus`(authority:
|
|
74
|
+
* dsh-client-ui-sidebar-right lib/types/client/service.d.ts `focus(tabId)`)。
|
|
75
|
+
* focus 只标记活动 tab/pane,不展开折叠的栏——聚焦的语义包含「让用户看见」,
|
|
76
|
+
* 故折叠时补 toggleExpanded(authority: 同文件 `isExpanded/toggleExpanded`)。
|
|
77
|
+
* @returns true = 已通过 focus 聚焦;false = 面缺席(调用方回退 activateTab)。
|
|
78
|
+
*/
|
|
79
|
+
export declare function focusNativeTab(ctx: Context, tabId: string): boolean;
|
|
80
|
+
export {};
|
|
@@ -7,7 +7,11 @@
|
|
|
7
7
|
* 否则写 meta.pendingDraft 移交面板(面板在 composer 就绪后应用并清除)。
|
|
8
8
|
*/
|
|
9
9
|
import type { Context, SidebarTab } from '../host/contracts.ts';
|
|
10
|
-
/**
|
|
10
|
+
/**
|
|
11
|
+
* 从最新快照读一个 Tab(meta 合并写入前的读取面;布局即注册表)。
|
|
12
|
+
* better-sidebar >= 0.19: the open tab lives in the native right sidebar, not
|
|
13
|
+
* in the layout snapshot — fall back to the live-panel registry.
|
|
14
|
+
*/
|
|
11
15
|
export declare function readTab(ctx: Context, tabId: string): SidebarTab | undefined;
|
|
12
16
|
/**
|
|
13
17
|
* 为 sessionId 新建或聚焦一个侧边聊天 Tab;draftText 给出时写入其草稿。
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-sidenote",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.3",
|
|
4
4
|
"description": "DSH web plugin: Codex-style side chat (fork the current session into a persistent side panel) + selection annotations with numbered badges and a composer chip. A thin consumer of dsh-better-sidebar. / Codex 风格侧边聊天 + 划选注释的 DSH 插件。",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -227,6 +227,35 @@ export interface ModelSelection {
|
|
|
227
227
|
reasoningEffort?: string
|
|
228
228
|
}
|
|
229
229
|
|
|
230
|
+
/**
|
|
231
|
+
* Remote session face(dsh >= 0.1.5 的模型读写照面;0.1.5 删除了
|
|
232
|
+
* `connection.api.sessions.*` 的 client 面后由它接管)。
|
|
233
|
+
* `remote`/`remote.session` 在宿主侧是可注入服务,但本插件不声明 inject
|
|
234
|
+
* (0.1.2 及更早无此服务,声明会让插件在旧宿主上永不激活)——调用点经
|
|
235
|
+
* `ctx.get` 惰性探测(cordis 的 get 不受 inject 门禁限制)。
|
|
236
|
+
* Authority:`@deepseek-ai/dsh-api-session-controller/lib/typert.remote-client.d.ts`
|
|
237
|
+
* (RemoteResult = `{ok: true, value} | {ok: false, error}`;error 实为
|
|
238
|
+
* RemoteError 实例,code 必有、message 继承 Error,这里弱化为可读取切片)。
|
|
239
|
+
*/
|
|
240
|
+
export interface RemoteSessionModelFace {
|
|
241
|
+
selectModel(request: {
|
|
242
|
+
sessionId: SessionId
|
|
243
|
+
provider: string
|
|
244
|
+
model: string
|
|
245
|
+
reasoningEffort?: string
|
|
246
|
+
}): Promise<{ ok: true; value: unknown } | { ok: false; error: { code?: string; message?: string } }>
|
|
247
|
+
/** 宿主级模型目录(authority 同上,`ModelCatalog`)。 */
|
|
248
|
+
modelCatalog?(): Promise<{ ok: true; value: ModelCatalogFace } | { ok: false; error: { code?: string; message?: string } }>
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/** `remote.session.modelCatalog()` 的返回切片(authority: 同上 `ModelCatalog`)。 */
|
|
252
|
+
export interface ModelCatalogFace {
|
|
253
|
+
readonly default: ModelSelection
|
|
254
|
+
readonly routableProviders: readonly string[]
|
|
255
|
+
readonly groups: SessionModelsResult['groups']
|
|
256
|
+
readonly failures?: readonly unknown[]
|
|
257
|
+
}
|
|
258
|
+
|
|
230
259
|
export interface SessionModelsResult {
|
|
231
260
|
current: ModelSelection
|
|
232
261
|
routable: boolean
|
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
* 新增 off-face 依赖时:调用点照写就地探测,同时把条目加进 PROBES。
|
|
10
10
|
*/
|
|
11
11
|
import type { Context } from './contracts.ts'
|
|
12
|
+
import { nativeSidebarHost } from '../sidechat/native.ts'
|
|
13
|
+
import { remoteSessionFace } from '../sidechat/lifecycle.ts'
|
|
12
14
|
|
|
13
15
|
interface Probe {
|
|
14
16
|
/** 探测项名称(告警文案用)。 */
|
|
@@ -57,6 +59,31 @@ const PROBES: readonly Probe[] = [
|
|
|
57
59
|
},
|
|
58
60
|
fallback: 'ProducedFiles 等动态复用件降级为自绘',
|
|
59
61
|
},
|
|
62
|
+
{
|
|
63
|
+
name: 'sidebarRight.focus(native tab 程序化聚焦)',
|
|
64
|
+
check: (ctx) => {
|
|
65
|
+
// 仅 better-sidebar >= 0.19(native 右栏)需要的面;老宿主的
|
|
66
|
+
// activateTab 本就可用,缺失不算降级。
|
|
67
|
+
if (!nativeSidebarHost(ctx)) return true
|
|
68
|
+
const face = (ctx as { get?: (k: string) => unknown }).get?.('sidebarRight') as
|
|
69
|
+
| { focus?: unknown }
|
|
70
|
+
| undefined
|
|
71
|
+
return typeof face?.focus === 'function'
|
|
72
|
+
},
|
|
73
|
+
fallback: '聚焦既有侧边聊天 tab 不可用(activateTab 在 native 面是空操作)',
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
name: '会话模型面(remote.session / connection.api.sessions 双版本链)',
|
|
77
|
+
check: (ctx) => {
|
|
78
|
+
// 新面(0.1.5 的 remote.session)与旧面(<= 0.1.2 的 connection.api)
|
|
79
|
+
// 任一在列即能力在——双版本链的探测与调用点同序(lifecycle.ts)。
|
|
80
|
+
if (remoteSessionFace(ctx) !== undefined) return true
|
|
81
|
+
const legacy = (ctx as { connection?: { api?: { sessions?: { models?: unknown } } } })
|
|
82
|
+
.connection?.api?.sessions?.models
|
|
83
|
+
return typeof legacy === 'function'
|
|
84
|
+
},
|
|
85
|
+
fallback: 'fork 模型同步、模型标签与模型切换不可用(子会话用宿主默认模型)',
|
|
86
|
+
},
|
|
60
87
|
]
|
|
61
88
|
|
|
62
89
|
/** 激活时跑一遍探测清单;缺失项逐项 warn-once。返回缺失项数(测试可断言)。 */
|
package/src/client/index.tsx
CHANGED
|
@@ -13,10 +13,14 @@ import { attachLocale, type LocaleServiceLike } from './locales.ts'
|
|
|
13
13
|
import { createReflowStore } from './reflow.ts'
|
|
14
14
|
import { registerSideChat } from './sidechat/index.tsx'
|
|
15
15
|
import { registerAnnotations } from './annotate/index.tsx'
|
|
16
|
+
import { setRootContext } from './sidechat/native.ts'
|
|
16
17
|
|
|
17
18
|
export const inject = ['betterSidebar', 'sessions', 'workspaces', 'slots', 'connection', 'locale']
|
|
18
19
|
|
|
19
20
|
export function apply(ctx: Context): void {
|
|
21
|
+
// 侧栏 Tab 的原生面(better-sidebar >= 0.19)与宿主调用都要用插件根 ctx:
|
|
22
|
+
// 面板拿到的是 slot 注入的收缩 ctx(inject 里没有 workspaces),见 native.ts。
|
|
23
|
+
setRootContext(ctx)
|
|
20
24
|
// 跟随 DSH 通用设置里的语言(locale.preference,Host-backed,实时切换)。
|
|
21
25
|
attachLocale(ctx.locale as LocaleServiceLike | undefined)
|
|
22
26
|
// T2 off-face 全景探测:宿主升级后哪些能力降级了,开发者工具里一眼可见。
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* 模型选择器(WI-02):composer 脚部的模型 chip → 两级 Menu(provider 分组
|
|
3
|
-
* 标签 +
|
|
4
|
-
*
|
|
3
|
+
* 标签 + 型号行)。数据面走 lifecycle.ts 的双版本链(0.1.5 的
|
|
4
|
+
* remote.session.modelCatalog/selectModel 优先,<= 0.1.2 的
|
|
5
|
+
* connection.api.sessions 回退);切的是子会话自己的选择,不回写主会话。
|
|
5
6
|
*
|
|
6
|
-
* 懒加载:首次打开才拉目录(
|
|
7
|
-
* (selectModel 返回的 selected 为准)。
|
|
7
|
+
* 懒加载:首次打开才拉目录(catalog RPC 有成本);切换后本地标签立即更新。
|
|
8
8
|
*/
|
|
9
9
|
import { useState } from 'react'
|
|
10
10
|
import { IconCheckOutline16, IconChevronDownOutline14, Menu, type MenuEntry } from '@deepseek-ai/dsh-client-ui-primitives'
|
|
@@ -16,10 +16,12 @@ import { memo, useCallback, useEffect, useMemo, useRef, useState, useSyncExterna
|
|
|
16
16
|
import { IconNewChatOutline16 } from '@deepseek-ai/dsh-client-ui-primitives'
|
|
17
17
|
import type { Context, SessionFace, TabComponentProps } from '../host/contracts.ts'
|
|
18
18
|
import { useComposer, type Composer } from './composer.ts'
|
|
19
|
-
import { clearPendingDraft, parseSideChatMeta, phaseOf } from './model.ts'
|
|
19
|
+
import { appendDraftText, clearPendingDraft, parseSideChatMeta, phaseOf } from './model.ts'
|
|
20
20
|
import { transcriptOf } from '../chat/transcript.ts'
|
|
21
21
|
import { EmptyState, MessageList, StateScreen } from './rows.tsx'
|
|
22
22
|
import { chatSourceOf, ensurePanelOpen, forkAndRegister, openSessionWindow, readModelName, updateTabMeta } from './lifecycle.ts'
|
|
23
|
+
import { nativeSidebarHost, registerLiveSideChat } from './native.ts'
|
|
24
|
+
import { dropClosedSideChat, recordClosedSideChat } from './recentClosed.ts'
|
|
23
25
|
import { ToolCard } from '../chat/ToolCard.tsx'
|
|
24
26
|
import { ComposerBar } from './ComposerBar.tsx'
|
|
25
27
|
import { ReasoningRow } from '../chat/ReasoningRow.tsx'
|
|
@@ -187,6 +189,59 @@ export function SideChatPanel(props: TabComponentProps & { reflow: ReflowStore }
|
|
|
187
189
|
// pendingDraft/相位变化时真正动作(清除后 pendingDraft 为 undefined,幂等)。
|
|
188
190
|
}, [pendingDraft, phase, ctx, tab.id])
|
|
189
191
|
|
|
192
|
+
// Native right sidebar (better-sidebar >= 0.19): publish this panel so the
|
|
193
|
+
// programmatic entry points (/side, selection bridge) can reach its tab id and
|
|
194
|
+
// seed the draft — the layout snapshot cannot see native tabs (see native.ts).
|
|
195
|
+
// latest-ref:composer/meta/phase/title 每渲新引用,进 deps 会高频重注册。
|
|
196
|
+
const composerRef = useRef(composer)
|
|
197
|
+
composerRef.current = composer
|
|
198
|
+
const metaRef = useRef(tab.meta)
|
|
199
|
+
metaRef.current = tab.meta
|
|
200
|
+
const phaseRef = useRef(phase)
|
|
201
|
+
phaseRef.current = phase
|
|
202
|
+
const titleRef = useRef(tab.title)
|
|
203
|
+
titleRef.current = tab.title
|
|
204
|
+
useEffect(() => {
|
|
205
|
+
// 后悔药自愈(native):重挂载说明上次卸载是切会话/HMR 而非关闭,清掉误记。
|
|
206
|
+
const metaNow = parseSideChatMeta(metaRef.current)
|
|
207
|
+
if (nativeSidebarHost(ctx) && metaNow.childId !== undefined) {
|
|
208
|
+
dropClosedSideChat(scope.sessionId, metaNow.childId)
|
|
209
|
+
}
|
|
210
|
+
const dispose = registerLiveSideChat(
|
|
211
|
+
scope.sessionId,
|
|
212
|
+
tab.id,
|
|
213
|
+
(text) => {
|
|
214
|
+
// 相位门:未到 chat(forking/loading)时 composer 还是本地草稿,机器
|
|
215
|
+
// 绑定后本地草稿被丢弃——改走 meta.pendingDraft,由既有 pendingDraft
|
|
216
|
+
// effect 在相位就绪后应用(与 legacy 路径同机制)。
|
|
217
|
+
if (phaseRef.current !== 'chat') {
|
|
218
|
+
updateTabMeta(ctx, tab.id, (m) => ({ ...m, pendingDraft: appendDraftText(m.pendingDraft ?? '', text) }))
|
|
219
|
+
return
|
|
220
|
+
}
|
|
221
|
+
composerRef.current.appendDraft(text)
|
|
222
|
+
requestAnimationFrame(() => { rootRef.current?.querySelector('textarea')?.focus() })
|
|
223
|
+
},
|
|
224
|
+
() => metaRef.current,
|
|
225
|
+
)
|
|
226
|
+
return () => {
|
|
227
|
+
dispose()
|
|
228
|
+
// 后悔药数据源(native):用户 × 关 native tab 不经 descriptor.onClose
|
|
229
|
+
// (tab-adapter 卸载只 records.drop),以面板卸载补记;误记由挂载时的
|
|
230
|
+
// drop 自愈。legacy 由 descriptor.onClose 负责,不双写。
|
|
231
|
+
if (nativeSidebarHost(ctx)) {
|
|
232
|
+
const m = parseSideChatMeta(metaRef.current)
|
|
233
|
+
if (m.childId !== undefined && m.parentSessionId !== undefined) {
|
|
234
|
+
recordClosedSideChat(m.parentSessionId, {
|
|
235
|
+
childId: m.childId,
|
|
236
|
+
parentSessionId: m.parentSessionId,
|
|
237
|
+
title: titleRef.current,
|
|
238
|
+
closedAt: Date.now(),
|
|
239
|
+
})
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}, [scope.sessionId, tab.id, ctx])
|
|
244
|
+
|
|
190
245
|
// D3a 保存为正式会话:fork 子会话为独立主会话(无 unarchive API,
|
|
191
246
|
// fork 即转正——内置侧边对话同款路径)→ 主视图打开 → 关本 Tab →
|
|
192
247
|
// 全局 toast 确认去向(P4:Tab 关闭后面板树卸载,toast 挂在 body 级
|
|
@@ -6,9 +6,10 @@
|
|
|
6
6
|
* off-face 探测纪律:session.open()、store.update 均为运行时可达但契约不
|
|
7
7
|
* 保证的面——就地 feature-check + 吞错降级,并登记进 host/probes.ts。
|
|
8
8
|
*/
|
|
9
|
-
import type { Context, ConversationSnapshot, SessionBinding, SessionFace, SessionModelsResult, UiConversationLike } from '../host/contracts.ts'
|
|
9
|
+
import type { Context, ConversationSnapshot, ModelSelection, RemoteSessionModelFace, SessionBinding, SessionFace, SessionModelsResult, UiConversationLike } from '../host/contracts.ts'
|
|
10
10
|
import { parseSideChatMeta, type SideChatMeta } from './model.ts'
|
|
11
11
|
import { readTab } from './open.ts'
|
|
12
|
+
import { rootContext } from './native.ts'
|
|
12
13
|
|
|
13
14
|
/**
|
|
14
15
|
* 首开编排:fork(全量历史快照)→ 登记 meta(先行,绝不丢登记)→
|
|
@@ -34,6 +35,9 @@ export async function forkAndRegister(ctx: Context, parentSessionId: string, tab
|
|
|
34
35
|
// 边界缺失不阻断 fork。
|
|
35
36
|
}
|
|
36
37
|
const forked = await ctx.sessions.fork({ sessionId: parentSessionId })
|
|
38
|
+
// The slot-provided context a panel receives has a narrower inject list than the
|
|
39
|
+
// plugin root context, so host calls go through the root context (see native.ts).
|
|
40
|
+
const owner = rootContext(ctx)
|
|
37
41
|
// meta 先行:fork resolve 后立即登记 childId(会话切换导致组件卸载时
|
|
38
42
|
// updateTab 找不到 tab 也只是 no-op——否则 Tab 永远停在 forking 且下次
|
|
39
43
|
// 挂载重复 fork 出孤儿会话)。
|
|
@@ -45,22 +49,20 @@ export async function forkAndRegister(ctx: Context, parentSessionId: string, tab
|
|
|
45
49
|
}))
|
|
46
50
|
// 归档失败残留可见(无 unarchive API),不阻断面板。
|
|
47
51
|
try {
|
|
48
|
-
await
|
|
52
|
+
await owner.workspaces.archiveSession(forked)
|
|
49
53
|
} catch (error) {
|
|
50
54
|
console.warn('[dsh-sidenote] 归档侧边会话失败(会话列表可能短暂可见):', error)
|
|
51
55
|
}
|
|
52
56
|
// fork 继承 agent preset 但不继承模型选择——读主会话当前模型并同步到
|
|
53
57
|
// 子会话(best-effort,失败则子会话用宿主默认模型,面板标签如实回退)。
|
|
58
|
+
// 双版本链:投影 + remote.session(0.1.5)优先,connection.api(<= 0.1.2
|
|
59
|
+
// client 面)回退——面迁移不弃旧档(chatSourceOf 先例)。
|
|
54
60
|
try {
|
|
55
|
-
const
|
|
56
|
-
if (
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
provider: current.provider,
|
|
61
|
-
model: current.model,
|
|
62
|
-
...(current.reasoningEffort !== undefined ? { reasoningEffort: current.reasoningEffort } : {}),
|
|
63
|
-
})
|
|
61
|
+
const selection = await readModelSelection(owner, parentSessionId)
|
|
62
|
+
if (selection === undefined) {
|
|
63
|
+
console.warn('[dsh-sidenote] 同步主会话模型跳过:主机模型 API 不可用')
|
|
64
|
+
} else if (!(await writeModelSelection(owner, forked, selection))) {
|
|
65
|
+
console.warn('[dsh-sidenote] 同步主会话模型失败(子会话用默认模型)')
|
|
64
66
|
}
|
|
65
67
|
} catch (error) {
|
|
66
68
|
console.warn('[dsh-sidenote] 同步主会话模型失败(子会话用默认模型):', error)
|
|
@@ -104,14 +106,100 @@ export function openSessionWindow(session: SessionFace | undefined): void {
|
|
|
104
106
|
})
|
|
105
107
|
}
|
|
106
108
|
|
|
107
|
-
/**
|
|
108
|
-
|
|
109
|
+
/**
|
|
110
|
+
* Current model selection of a session, read from its `modelSelection`
|
|
111
|
+
* projection (dsh >= 0.1.2): the durable selection is projected state, so no RPC
|
|
112
|
+
* is needed and the read is safe for any session id. Snapshot shape authority:
|
|
113
|
+
* `@deepseek-ai/dsh-api-session-controller/lib/types/types.d.ts`
|
|
114
|
+
* (`ModelSelectionProjection = { lastUsed, next }`,两者均可为 null——
|
|
115
|
+
* `next` 缺省时按投影文档回落 `lastUsed`)。
|
|
116
|
+
*/
|
|
117
|
+
export function projectedModelSelection(ctx: Context, sessionId: string): ModelSelection | undefined {
|
|
118
|
+
try {
|
|
119
|
+
const session = ctx.sessions.binding(sessionId)?.session as
|
|
120
|
+
| { projections?: { faceOf?: (name: string) => { getSnapshot?: () => unknown } | undefined } }
|
|
121
|
+
| undefined
|
|
122
|
+
const snapshot = session?.projections?.faceOf?.('modelSelection')?.getSnapshot?.() as
|
|
123
|
+
| { next?: ModelSelection | null; lastUsed?: ModelSelection | null }
|
|
124
|
+
| undefined
|
|
125
|
+
return snapshot?.next ?? snapshot?.lastUsed ?? undefined
|
|
126
|
+
} catch {
|
|
127
|
+
return undefined
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Remote session face carrying `selectModel`/`modelCatalog`(dsh 0.1.5 删除
|
|
133
|
+
* `connection.api.sessions.*` client 面后的模型 RPC 面;事实上 0.1.2-rc.1
|
|
134
|
+
* 起存在——authority: dsh-api-session-controller lib/typert.remote-client.d.ts)。
|
|
135
|
+
* 探测走 `ctx.get`(cordis 的 get 不受 inject 门禁限制,本插件为兼容旧宿主
|
|
136
|
+
* 不声明 inject 'remote.session';机制见 contracts.ts RemoteSessionModelFace)。
|
|
137
|
+
* `ctx.get('remote')` 返回命名空间容器时下钻其 `.session` 成员。
|
|
138
|
+
*/
|
|
139
|
+
export function remoteSessionFace(ctx: Context): RemoteSessionModelFace | undefined {
|
|
140
|
+
for (const name of ['remote.session', 'remote']) {
|
|
141
|
+
let candidate: unknown
|
|
142
|
+
try {
|
|
143
|
+
candidate = ctx.get(name)
|
|
144
|
+
} catch {
|
|
145
|
+
continue // service absent
|
|
146
|
+
}
|
|
147
|
+
const face = candidate as { selectModel?: unknown; session?: { selectModel?: unknown } } | undefined
|
|
148
|
+
if (typeof face?.selectModel === 'function') return face as RemoteSessionModelFace
|
|
149
|
+
if (typeof face?.session?.selectModel === 'function') return face.session as RemoteSessionModelFace
|
|
150
|
+
}
|
|
151
|
+
return undefined
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* 读会话当前模型选择:投影面(0.1.2+,同步无 RPC)优先;旧
|
|
156
|
+
* `connection.api.sessions.models` RPC(<= 0.1.2 的 client 面,0.1.5 已删)
|
|
157
|
+
* 回退(双版本链,同 chatSourceOf 先例)。永不抛错。
|
|
158
|
+
*/
|
|
159
|
+
export async function readModelSelection(ctx: Context, sessionId: string): Promise<ModelSelection | undefined> {
|
|
160
|
+
const projected = projectedModelSelection(ctx, sessionId)
|
|
161
|
+
if (projected !== undefined) return projected
|
|
109
162
|
try {
|
|
110
163
|
const res = await ctx.connection.api.sessions.models({ sessionId })
|
|
111
|
-
|
|
164
|
+
if (res.result.ok) return res.result.value.current
|
|
112
165
|
} catch {
|
|
113
|
-
|
|
166
|
+
// 旧面缺席(0.1.5)→ undefined。
|
|
114
167
|
}
|
|
168
|
+
return undefined
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* 写会话模型选择:`remote.session.selectModel`(新面)优先,旧
|
|
173
|
+
* `connection.api.sessions.selectModel` 回退。面在而调用失败(模型不可路由等)
|
|
174
|
+
* 属真实失败,不再落旧面重试。
|
|
175
|
+
*/
|
|
176
|
+
export async function writeModelSelection(ctx: Context, sessionId: string, selection: ModelSelection): Promise<boolean> {
|
|
177
|
+
const request = {
|
|
178
|
+
sessionId,
|
|
179
|
+
provider: selection.provider,
|
|
180
|
+
model: selection.model,
|
|
181
|
+
...(selection.reasoningEffort !== undefined ? { reasoningEffort: selection.reasoningEffort } : {}),
|
|
182
|
+
}
|
|
183
|
+
const face = remoteSessionFace(ctx)
|
|
184
|
+
if (face !== undefined) {
|
|
185
|
+
try {
|
|
186
|
+
return (await face.selectModel(request)).ok
|
|
187
|
+
} catch {
|
|
188
|
+
return false
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
try {
|
|
192
|
+
const res = await ctx.connection.api.sessions.selectModel(request)
|
|
193
|
+
return res.result.ok
|
|
194
|
+
} catch {
|
|
195
|
+
return false
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** 读子会话当前模型名(composer 模型标签用);任何失败回退 null(默认文案)。 */
|
|
200
|
+
export async function readModelName(ctx: Context, sessionId: string): Promise<string | null> {
|
|
201
|
+
const selection = await readModelSelection(ctx, sessionId)
|
|
202
|
+
return selection?.model ?? null
|
|
115
203
|
}
|
|
116
204
|
|
|
117
205
|
/** 会话内容读取源(0.1.1/0.1.2 双兼容,feature-check 优先链)。 */
|
|
@@ -155,8 +243,30 @@ export function chatSourceOf(ctx: Context, binding: SessionBinding | undefined):
|
|
|
155
243
|
return undefined
|
|
156
244
|
}
|
|
157
245
|
|
|
158
|
-
/**
|
|
246
|
+
/**
|
|
247
|
+
* 拉会话模型目录:0.1.5 走 `remote.session.modelCatalog()`(目录是宿主级,
|
|
248
|
+
* 菜单选中项所需的 current 由投影/旧面补上);旧面回退
|
|
249
|
+
* `connection.api.sessions.models`。失败 null → 菜单保持只读标签态。
|
|
250
|
+
*/
|
|
159
251
|
export async function listModels(ctx: Context, sessionId: string): Promise<SessionModelsResult | null> {
|
|
252
|
+
const face = remoteSessionFace(ctx)
|
|
253
|
+
if (typeof face?.modelCatalog === 'function') {
|
|
254
|
+
try {
|
|
255
|
+
const result = await face.modelCatalog()
|
|
256
|
+
if (result.ok) {
|
|
257
|
+
const catalog = result.value
|
|
258
|
+
const current = (await readModelSelection(ctx, sessionId)) ?? catalog.default
|
|
259
|
+
return {
|
|
260
|
+
current,
|
|
261
|
+
routable: catalog.routableProviders.length > 0,
|
|
262
|
+
groups: catalog.groups,
|
|
263
|
+
...(catalog.failures !== undefined ? { failures: catalog.failures } : {}),
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
} catch {
|
|
267
|
+
// 落旧面。
|
|
268
|
+
}
|
|
269
|
+
}
|
|
160
270
|
try {
|
|
161
271
|
const res = await ctx.connection.api.sessions.models({ sessionId })
|
|
162
272
|
return res.result.ok ? res.result.value : null
|
|
@@ -167,10 +277,6 @@ export async function listModels(ctx: Context, sessionId: string): Promise<Sessi
|
|
|
167
277
|
|
|
168
278
|
/** 切换子会话模型;成功返回新模型展示名,失败 null(best-effort)。 */
|
|
169
279
|
export async function switchModel(ctx: Context, sessionId: string, provider: string, model: string): Promise<string | null> {
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
return res.result.ok ? model : null
|
|
173
|
-
} catch {
|
|
174
|
-
return null
|
|
175
|
-
}
|
|
280
|
+
const ok = await writeModelSelection(ctx, sessionId, { provider, model })
|
|
281
|
+
return ok ? model : null
|
|
176
282
|
}
|