dsh-sidenote 0.1.0

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.
Files changed (48) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +60 -0
  3. package/cordis.patch.yml +16 -0
  4. package/lib/client.js +2143 -0
  5. package/lib/index.js +6 -0
  6. package/lib/types/client/annotate/anchor.d.ts +57 -0
  7. package/lib/types/client/annotate/chip.d.ts +11 -0
  8. package/lib/types/client/annotate/draft.d.ts +29 -0
  9. package/lib/types/client/annotate/format.d.ts +52 -0
  10. package/lib/types/client/annotate/index.d.ts +2 -0
  11. package/lib/types/client/annotate/model.d.ts +59 -0
  12. package/lib/types/client/annotate/overlay.d.ts +27 -0
  13. package/lib/types/client/annotate/selection.d.ts +49 -0
  14. package/lib/types/client/bridge.d.ts +19 -0
  15. package/lib/types/client/index.d.ts +12 -0
  16. package/lib/types/client/locales.d.ts +77 -0
  17. package/lib/types/client/sidechat/SideChatPanel.d.ts +2 -0
  18. package/lib/types/client/sidechat/composer.d.ts +26 -0
  19. package/lib/types/client/sidechat/index.d.ts +2 -0
  20. package/lib/types/client/sidechat/model.d.ts +101 -0
  21. package/lib/types/client/sidechat/open.d.ts +16 -0
  22. package/lib/types/client/sidechat/slash.d.ts +12 -0
  23. package/lib/types/context-types.d.ts +335 -0
  24. package/lib/types/index.d.ts +9 -0
  25. package/package.json +96 -0
  26. package/src/client/annotate/anchor.ts +158 -0
  27. package/src/client/annotate/annotate.module.css +301 -0
  28. package/src/client/annotate/chip.tsx +115 -0
  29. package/src/client/annotate/css.d.ts +9 -0
  30. package/src/client/annotate/draft.ts +67 -0
  31. package/src/client/annotate/format.ts +83 -0
  32. package/src/client/annotate/index.tsx +66 -0
  33. package/src/client/annotate/model.ts +138 -0
  34. package/src/client/annotate/overlay.tsx +531 -0
  35. package/src/client/annotate/selection.ts +203 -0
  36. package/src/client/bridge.ts +19 -0
  37. package/src/client/index.tsx +22 -0
  38. package/src/client/locales.ts +175 -0
  39. package/src/client/sidechat/SideChatPanel.tsx +366 -0
  40. package/src/client/sidechat/composer.ts +129 -0
  41. package/src/client/sidechat/css-modules.d.ts +5 -0
  42. package/src/client/sidechat/index.tsx +56 -0
  43. package/src/client/sidechat/model.ts +331 -0
  44. package/src/client/sidechat/open.ts +66 -0
  45. package/src/client/sidechat/sidechat.module.css +230 -0
  46. package/src/client/sidechat/slash.ts +84 -0
  47. package/src/context-types.ts +369 -0
  48. package/src/index.ts +13 -0
@@ -0,0 +1,84 @@
1
+ /**
2
+ * /side 斜杠命令(spike 落点):经 client 侧 `ctx.commandUi.register`
3
+ * (dsh-client-ui-commands 的 CommandContribution,ui 形态 popupSelect)
4
+ * 注册。popupSelect 是该注册面唯一的 UI 形态 —— 菜单行被选中后弹一个
5
+ * 选项壳,选项 = 新建 + 当前会话已并存的侧边聊天(聚焦)。
6
+ *
7
+ * 服务经 ctx.get 惰性解析(commandUi 不在 inject 清单里):服务缺失、
8
+ * 注册抛错(如与 host 命令撞名)都降级为「只有 Tab 入口」,绝不影响面板。
9
+ */
10
+ import type { Context } from '../../context-types.ts'
11
+ import { canForkFrom, collectSideTabs } from './model.ts'
12
+ import { openOrFocusSideChat } from './open.ts'
13
+ import { t } from '../locales.ts'
14
+
15
+ /** dsh-client-ui-commands ClientSessionContext 的最小镜像(只有 sessionId)。 */
16
+ interface CommandSession {
17
+ readonly sessionId: string
18
+ }
19
+
20
+ /** popupSelect 选项行镜像。 */
21
+ interface SelectOption {
22
+ readonly id: string
23
+ readonly label: string
24
+ readonly detail?: string
25
+ }
26
+
27
+ /** ctx.commandUi 的最小镜像(CommandUiContract.register)。 */
28
+ interface CommandUiService {
29
+ register(contribution: {
30
+ readonly name: string
31
+ readonly description: string
32
+ available(session: CommandSession): boolean
33
+ readonly ui: {
34
+ readonly kind: 'popupSelect'
35
+ options(session: CommandSession, signal: AbortSignal): Promise<readonly SelectOption[]>
36
+ onSelect(option: SelectOption, session: CommandSession): void | Promise<void>
37
+ }
38
+ }): () => void
39
+ }
40
+
41
+ /** 注册 /side 命令;不可行时静默降级(返回 undefined)。 */
42
+ export function registerSideCommand(ctx: Context): void {
43
+ let commandUi: CommandUiService | undefined
44
+ try {
45
+ commandUi = ctx.get('commandUi') as CommandUiService | undefined
46
+ } catch {
47
+ return
48
+ }
49
+ if (commandUi === undefined || typeof commandUi.register !== 'function') return
50
+ try {
51
+ ctx.effect(() => commandUi.register({
52
+ name: 'side',
53
+ description: t('cmdDesc'),
54
+ available: (session) => canForkFrom(ctx, session.sessionId),
55
+ ui: {
56
+ kind: 'popupSelect',
57
+ options: (session) => {
58
+ const options: SelectOption[] = [
59
+ { id: 'new', label: t('cmdNew'), detail: t('cmdNewDetail') },
60
+ ]
61
+ // 已并存的侧边聊天列为聚焦项(命令弹层即多实例管理入口)。
62
+ const snapshot = ctx.betterSidebar.getSnapshot()
63
+ if (snapshot.sessionId === session.sessionId && snapshot.state !== undefined) {
64
+ for (const tab of collectSideTabs(snapshot.state)) {
65
+ options.push({ id: `focus:${tab.id}`, label: t('cmdFocus', { title: tab.title }), detail: t('cmdFocusDetail') })
66
+ }
67
+ }
68
+ return Promise.resolve(options)
69
+ },
70
+ onSelect: (option, session) => {
71
+ if (option.id === 'new') {
72
+ openOrFocusSideChat(ctx, session.sessionId)
73
+ return
74
+ }
75
+ if (option.id.startsWith('focus:')) {
76
+ ctx.betterSidebar.activateTab(option.id.slice('focus:'.length), { sessionId: session.sessionId })
77
+ }
78
+ },
79
+ },
80
+ }), 'dsh-sidenote: /side command')
81
+ } catch (error) {
82
+ console.warn('[dsh-sidenote] /side 命令注册失败(Tab 入口不受影响):', error)
83
+ }
84
+ }
@@ -0,0 +1,369 @@
1
+ /**
2
+ * Structural mirror of the runtime surfaces dsh-sidenote consumes.
3
+ *
4
+ * Third-party plugins resolve outside the DSH monorepo's single cordis
5
+ * instance, so upstream `declare module 'cordis'` augmentations never reach
6
+ * our `Context` type. We mirror the runtime shapes here instead — drift from
7
+ * upstream is contained to this file. Each section names its authority (the
8
+ * upstream .d.ts it mirrors); extend sections as features need more surface,
9
+ * keeping the mirror honest (only declare what actually exists at runtime).
10
+ *
11
+ * Authorities:
12
+ * - betterSidebar: dsh-better-sidebar `src/client/service.ts` (+ docs/external-plugin-guide.md)
13
+ * - sessions/workspaces: `@deepseek-ai/dsh-client-runtime` lib/types/client/contract/{sessions,session,workspaces}.d.ts
14
+ * (local checkout: <dsh install>/node_modules/@deepseek-ai/dsh-client-runtime/lib/types/client/...)
15
+ * - conversation input / slots: `@deepseek-ai/dsh-client-ui-conversation` / `dsh-client-ui-slots` type surfaces
16
+ */
17
+ import type { Context as CordisContext } from 'cordis'
18
+ import type { ReactNode } from 'react'
19
+
20
+ export type SessionId = string
21
+
22
+ /** Minimal observable snapshot shape (identity-stable, useSyncExternalStore-ready). */
23
+ export interface ObservableSnapshot<T> {
24
+ getSnapshot(): T
25
+ subscribe(listener: () => void): () => void
26
+ }
27
+
28
+ // ── betterSidebar (dsh-better-sidebar client-half service) ──────────────────
29
+
30
+ export interface SessionScope {
31
+ sessionId: SessionId
32
+ cwd?: string
33
+ }
34
+
35
+ /** A live sidebar tab instance. `meta` is plugin-owned JSON persisted with the layout. */
36
+ export interface SidebarTab {
37
+ id: string
38
+ type: string
39
+ title: string
40
+ path?: string
41
+ meta?: unknown
42
+ }
43
+
44
+ /**
45
+ * 侧栏状态 = splits/bottomSplits 两棵 split/leaf 树(权威:dsh-better-sidebar
46
+ * src/client/state.ts):leaf 持有 tabs;split 递归分栏。
47
+ */
48
+ export interface SidebarLeafNode {
49
+ kind: 'leaf'
50
+ id: string
51
+ tabs: SidebarTab[]
52
+ active: string | null
53
+ }
54
+
55
+ export interface SidebarSplitNode {
56
+ kind: 'split'
57
+ id: string
58
+ dir: 'row' | 'col'
59
+ sizes: number[]
60
+ children: SidebarTreeNode[]
61
+ }
62
+
63
+ export type SidebarTreeNode = SidebarLeafNode | SidebarSplitNode
64
+
65
+ export interface SidebarState {
66
+ /** 右侧面板的 split 树。 */
67
+ splits: SidebarTreeNode
68
+ /** 底部面板的 split 树。 */
69
+ bottomSplits: SidebarTreeNode
70
+ }
71
+
72
+ export interface SidebarSnapshot {
73
+ sessionId?: SessionId
74
+ state?: SidebarState
75
+ prefs?: Record<string, unknown>
76
+ }
77
+
78
+ /** better-sidebar's store handle (external tabs treat it as opaque). */
79
+ export type SidebarStore = unknown
80
+
81
+ export interface TabComponentProps {
82
+ ctx: Context
83
+ store: SidebarStore
84
+ scope: SessionScope
85
+ tab: SidebarTab
86
+ visible: boolean
87
+ }
88
+
89
+ export interface TabDescriptor {
90
+ id: string
91
+ title: string | (() => string)
92
+ icon?: ReactNode | ((size: number) => ReactNode)
93
+ order?: number
94
+ hidden?: boolean
95
+ available?: (ctx: Context, scope: SessionScope, state: SidebarState) => boolean
96
+ single?: boolean
97
+ dedupeKey?: (tab: SidebarTab) => string | undefined
98
+ createTab?: (state: SidebarState) => { tab: SidebarTab; patch?: Partial<SidebarState> } | null
99
+ badge?: (ctx: Context, scope: SessionScope, state: SidebarState) => string | number | null | undefined
100
+ onOpen?: (tab: SidebarTab, scope: SessionScope) => void
101
+ onActivate?: (tab: SidebarTab, scope: SessionScope) => void
102
+ onClose?: (tab: SidebarTab, scope: SessionScope) => void
103
+ component: (props: TabComponentProps) => ReactNode
104
+ }
105
+
106
+ export interface OpenTabSeed {
107
+ type: string
108
+ title?: string
109
+ path?: string
110
+ id?: string
111
+ url?: string
112
+ meta?: unknown
113
+ }
114
+
115
+ export interface BetterSidebarService {
116
+ readonly version: string
117
+ readonly features: readonly string[]
118
+ registerTab(descriptor: TabDescriptor): () => void
119
+ openTab(seed: OpenTabSeed, scope?: SessionScope): void
120
+ closeTab(tabId: string, scope?: SessionScope): void
121
+ activateTab(tabId: string, scope?: SessionScope): void
122
+ updateTab(tabId: string, patch: { title?: string; path?: string; meta?: unknown }): void
123
+ getTab(id: string): TabDescriptor | undefined
124
+ getTabs(): readonly TabDescriptor[]
125
+ isTabEnabled(id: string): boolean
126
+ getSnapshot(): SidebarSnapshot
127
+ subscribeState(listener: () => void): () => void
128
+ subscribe(listener: () => void): () => void
129
+ }
130
+
131
+ // ── sessions (dsh-client-runtime) ────────────────────────────────────────────
132
+
133
+ export interface SessionListSnapshot {
134
+ /** Currently selected session id. */
135
+ current?: SessionId
136
+ [key: string]: unknown
137
+ }
138
+
139
+ export interface ForkOptions {
140
+ sessionId: SessionId
141
+ atSeq?: number
142
+ increaseTitle?: boolean
143
+ }
144
+
145
+ /** Text plus browser-owned parts; text covers everything we send today. */
146
+ export type PromptContentPart = { type: 'text'; text: string } | { type: string; [key: string]: unknown }
147
+
148
+ /**
149
+ * Conversation read model (subset). Extend from
150
+ * dsh-client-runtime `lib/types/client/sessions/conversation.d.ts` as needed —
151
+ * the real shape has chat/nodes/partial/queue/running and more.
152
+ */
153
+ export interface ConversationSnapshot {
154
+ sessionId: SessionId
155
+ nodes: readonly unknown[]
156
+ running: boolean
157
+ [key: string]: unknown
158
+ }
159
+
160
+ export interface ISession {
161
+ readonly sessionId: SessionId
162
+ prompt(content: PromptContentPart[], mode: 'queue' | 'steer'): Promise<unknown>
163
+ cancel(): Promise<unknown>
164
+ rename(title: string): Promise<unknown>
165
+ loadOlder(): Promise<void>
166
+ command(line: string): Promise<unknown>
167
+ }
168
+
169
+ export type SessionFace = ISession & ObservableSnapshot<ConversationSnapshot>
170
+
171
+ export interface SessionBinding {
172
+ session: SessionFace
173
+ }
174
+
175
+ export interface SessionsService {
176
+ list: ObservableSnapshot<SessionListSnapshot>
177
+ fork(opts: ForkOptions): Promise<SessionId>
178
+ binding(id: SessionId): SessionBinding | undefined
179
+ scope(id: SessionId): Context | undefined
180
+ open(id: SessionId): void
181
+ }
182
+
183
+ // ── workspaces (dsh-client-runtime) ─────────────────────────────────────────
184
+
185
+ export interface WorkspacesService {
186
+ archiveSession(sessionId: SessionId): Promise<void>
187
+ }
188
+
189
+ // ── conversation input machine (dsh-client-ui-conversation; lazy via ctx.get) ─
190
+
191
+ export interface SessionInput {
192
+ setDraft(text: string): void
193
+ submit(mode?: string): void
194
+ notify(level: string, text: string): void
195
+ }
196
+
197
+ export interface ConversationService {
198
+ input: {
199
+ for(ctx: Context): SessionInput | undefined
200
+ }
201
+ }
202
+
203
+ // ── connection(dsh-client-connection;sessions RPC 直达面)──────────────────
204
+
205
+ /** 会话的当前模型选择(`session.models` 的 current 字段)。 */
206
+ export interface ModelSelection {
207
+ provider: string
208
+ model: string
209
+ reasoningEffort?: string
210
+ }
211
+
212
+ export interface SessionModelsResult {
213
+ current: ModelSelection
214
+ routable: boolean
215
+ groups: readonly unknown[]
216
+ }
217
+
218
+ export interface RpcEnvelope<T> {
219
+ result: { ok: true; value: T } | { ok: false; error: { message?: string } }
220
+ }
221
+
222
+ export interface ConnectionSessionsApi {
223
+ models(payload: { sessionId: SessionId }): Promise<RpcEnvelope<SessionModelsResult>>
224
+ selectModel(payload: { sessionId: SessionId; provider: string; model: string; reasoningEffort?: string }): Promise<RpcEnvelope<{ selected: ModelSelection }>>
225
+ }
226
+
227
+ export interface ConnectionService {
228
+ api: { sessions: ConnectionSessionsApi }
229
+ }
230
+
231
+ // ── Context ──────────────────────────────────────────────────────────────────
232
+
233
+ // ── locale(dsh-client-locale;DSH 通用设置的语言偏好,Host-backed)────────────
234
+
235
+ export interface LocaleService {
236
+ getSnapshot(): { active: string }
237
+ subscribe(fn: () => void): () => void
238
+ }
239
+
240
+ export interface Context extends CordisContext {
241
+ betterSidebar: BetterSidebarService
242
+ sessions: SessionsService
243
+ workspaces: WorkspacesService
244
+ connection: ConnectionService
245
+ locale: LocaleService
246
+ }
247
+
248
+ // ── sidechat 扩展(WI-01)───────────────────────────────────────────────────
249
+ // 下列声明经接口合并补进上方镜像;权威来源逐节标注。
250
+
251
+ /**
252
+ * 会话列表快照补全(权威:dsh-client-runtime
253
+ * lib/types/client/sessions/service.d.ts 的 SessionListState):
254
+ * phase 标「首次成功拉取」就绪边;byId 含已归档行(归档过滤在
255
+ * workspace UI 层,store 本身携带全部行)。
256
+ */
257
+ export interface SessionListSnapshot {
258
+ phase?: 'pending' | 'ready'
259
+ byId?: Record<string, { blank?: boolean } | undefined>
260
+ }
261
+
262
+ /**
263
+ * 会话快照补全(权威:dsh-client-runtime sessions/conversation.d.ts):
264
+ * openState 是历史窗口生命周期(cold = 未开窗口);partial/runningCalls
265
+ * 承载在途流式输出。面板折叠函数对三者全部容错(缺省按空处理)。
266
+ */
267
+ export interface ConversationSnapshot {
268
+ openState?: 'cold' | 'loading' | 'open' | 'error'
269
+ partial?: unknown
270
+ runningCalls?: readonly unknown[]
271
+ }
272
+
273
+ /**
274
+ * input 草稿机的 state store(权威:dsh-client-ui-conversation
275
+ * input/contract.d.ts 的 SessionInput.state)——完整声明见下方 annotate 扩展节
276
+ * (接口合并;draft 字段两侧都依赖)。
277
+ */
278
+
279
+ export interface Context {
280
+ /**
281
+ * 惰性非追踪服务读(运行时事实:context proxy 的 reflect.get)——
282
+ * 读取不在 inject 清单里的服务(conversation / commandUi)的唯一通道。
283
+ * (`ctx.effect` 无需镜像:公开 cordis 包的 fiber.d.ts 已通过接口合并
284
+ * 提供 `effect(execute, label?)`。)
285
+ */
286
+ get(name: string): unknown
287
+ }
288
+
289
+ // ── annotate 扩展 ────────────────────────────────────────────────────────────
290
+ // Mirrors consumed by src/client/annotate/** (Workitem 02). Authorities:
291
+ // - slots service: `@deepseek-ai/dsh-client-runtime` lib/types/client/slots.d.ts
292
+ // (SlotRegistry; register/inject) + `@deepseek-ai/dsh-client-ui-slots` index.d.ts
293
+ // - input.dock owner share: `@deepseek-ai/dsh-client-ui-conversation`
294
+ // lib/types/client/contract/slots.d.ts (`conversation.input.dock` → InputZone)
295
+ // - SessionInput: `.../lib/types/client/input/contract.d.ts`
296
+
297
+ /** Registration options subset passed to `ctx.slots.register` (same shape better-sidebar mirrors). */
298
+ export interface SlotRegisterOptions {
299
+ name: string
300
+ key?: string
301
+ id?: string
302
+ order?: number
303
+ label?: string | (() => string)
304
+ select?: (owner: unknown) => unknown
305
+ priority?: number
306
+ locale?: string
307
+ registrant?: string
308
+ inject?: (...args: any[]) => Record<string, unknown>
309
+ children?: Record<string, unknown>
310
+ }
311
+
312
+ /** The client slots service face (register returns the disposer). */
313
+ export interface SlotsService {
314
+ register(options: SlotRegisterOptions, component: unknown): () => void
315
+ /** Run a callback for each declaration lifetime of a slot (no-op while undeclared). */
316
+ inject(key: string, callback: () => (() => void) | void): () => void
317
+ }
318
+
319
+ /** Published composer input state (subset of the real InputState). */
320
+ export interface InputStateSnapshot {
321
+ readonly draft: string
322
+ readonly phase: string
323
+ readonly queue?: readonly unknown[]
324
+ }
325
+
326
+ /** Session input snapshot as exposed on `InputZone.input`. */
327
+ export interface ConversationSnapshotLite {
328
+ sessionId: SessionId
329
+ running: boolean
330
+ }
331
+
332
+ /** Owner share of the `conversation.input.dock` slot (point-in-time snapshots). */
333
+ export interface InputZone {
334
+ readonly session: ConversationSnapshotLite
335
+ readonly input: InputStateSnapshot
336
+ }
337
+
338
+ /**
339
+ * U+FFFC reference-chip insert (spike result: NOT used — serialization of the
340
+ * chip routes through the source owner's ReferenceCodec, which is
341
+ * package-internal to ui-input-trigger with no plugin-facing registry; an
342
+ * unowned source would mark the occurrence invalid and fail submit. Mirrored
343
+ * here only to document the probe target).
344
+ */
345
+ export interface ReferenceInsert {
346
+ readonly source: string
347
+ readonly ref: string
348
+ readonly label: string
349
+ readonly clipboardText: string
350
+ }
351
+
352
+ export interface TokenSpan {
353
+ readonly start: number
354
+ readonly end: number
355
+ readonly draftRev: number
356
+ }
357
+
358
+ export interface Context {
359
+ /** The slot registry (provided by dsh-client-runtime, mounted before this plugin). */
360
+ slots: SlotsService
361
+ }
362
+
363
+ /** SessionInput completion for annotate: the live state store + the (unused) chip insert face. */
364
+ export interface SessionInput {
365
+ /** Input state store (draft reads + subscribe for the send-edge watch). */
366
+ readonly state: ObservableSnapshot<InputStateSnapshot>
367
+ /** Spike-only mirror; see {@link ReferenceInsert}. Not called by annotate. */
368
+ insertReference(ref: ReferenceInsert, span: TokenSpan): boolean
369
+ }
package/src/index.ts ADDED
@@ -0,0 +1,13 @@
1
+ /**
2
+ * dsh-sidenote host half: currently empty by design — every feature is
3
+ * client-side (sessions RPC + the betterSidebar service). A host half would
4
+ * only appear for capabilities the client cannot reach (none today).
5
+ */
6
+ import type { Context } from './context-types.ts'
7
+
8
+ /** Plugin identity for cordis.yml rows. */
9
+ export const name = 'dsh-sidenote'
10
+
11
+ export function apply(_ctx: Context): void {
12
+ // No host services yet.
13
+ }