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
package/lib/index.js ADDED
@@ -0,0 +1,6 @@
1
+ //#region src/index.ts
2
+ /** Plugin identity for cordis.yml rows. */
3
+ const name = "dsh-sidenote";
4
+ function apply(_ctx) {}
5
+ //#endregion
6
+ export { apply, name };
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Badge anchoring (Workitem 02): resolve a live DOM Range for an annotation.
3
+ *
4
+ * The range captured at creation time dies when React re-renders the message
5
+ * (session switch, streaming rebuilds, fork/delete). We therefore re-anchor
6
+ * on demand: find the message element by `data-chat-anchor-key`, locate the
7
+ * Nth occurrence of the anchor text in its text nodes, and rebuild the range.
8
+ * Anchor loss is acceptable (页面级临时语义) — the badge simply skips a
9
+ * render; nothing here may throw into the page.
10
+ */
11
+ import type { Annotation } from './model.ts';
12
+ /** The badge anchor point: right edge of the selection rects, first-line height. */
13
+ export interface BadgeAnchor {
14
+ /** max(rect.right) across the selection's client rects. */
15
+ readonly right: number;
16
+ /** Vertical center of the FIRST line's rect. */
17
+ readonly centerY: number;
18
+ }
19
+ /**
20
+ * Resolve a live range for an annotation: the cached range while its nodes
21
+ * are still connected, otherwise a re-anchored one (cached on success).
22
+ * Returns null when the anchor is gone — callers skip rendering, never throw.
23
+ */
24
+ export declare function resolveRange(annotation: Annotation, cache: Map<number, Range>): Range | null;
25
+ /**
26
+ * The badge anchor point of a range: 选区矩形右缘 (max right edge across
27
+ * client rects — the message column's right edge for full-line selections,
28
+ * the selection's text end for inline ones) at 选区首行高度 (first rect's
29
+ * vertical center). Null when the range lays out to nothing.
30
+ */
31
+ export declare function badgeAnchorOf(range: Range): BadgeAnchor | null;
32
+ /** Client rects of a range for the active-highlight overlay (empty on failure). */
33
+ export declare function highlightRectsOf(range: Range): readonly {
34
+ left: number;
35
+ top: number;
36
+ width: number;
37
+ height: number;
38
+ }[];
39
+ /** 角标碰撞半径(px):同点位的角标必须错开才可点击。 */
40
+ export declare const BADGE_COLLISION_RADIUS = 18;
41
+ /** 碰撞时向下错开的步长(px,大于角标直径)。 */
42
+ export declare const BADGE_SPREAD_STEP = 24;
43
+ /**
44
+ * 角标落点错开:与已放置角标(|Δx|、|Δy| 均小于碰撞半径)冲突时按
45
+ * BADGE_SPREAD_STEP 逐级下移——同一选区/相邻行的多个注释角标不再叠罗汉
46
+ * (叠放时上层角标拦截下层角标的点击)。
47
+ */
48
+ export declare function spreadBadgePoint(point: {
49
+ x: number;
50
+ y: number;
51
+ }, placed: readonly {
52
+ x: number;
53
+ y: number;
54
+ }[]): {
55
+ x: number;
56
+ y: number;
57
+ };
@@ -0,0 +1,11 @@
1
+ import type { ReactNode } from 'react';
2
+ import type { InputZone } from '../../context-types.ts';
3
+ import type { AnnotationStore } from './model.ts';
4
+ /** Props the dock skeleton hands us: the InputZone owner share (point-in-time). */
5
+ interface ChipProps {
6
+ readonly session: InputZone['session'];
7
+ readonly input: InputZone['input'];
8
+ }
9
+ /** Create the slot component bound to one store instance. */
10
+ export declare function createAnnotationChip(store: AnnotationStore): (props: ChipProps) => ReactNode;
11
+ export {};
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Managed draft prefix (Workitem 02 发送携带的降级方案): while a session has
3
+ * active annotations, their formatted quote block is maintained at the very
4
+ * top of the composer draft; the user's own text follows it, so sending
5
+ * naturally carries the context. The composer chip clears on the send edge
6
+ * (draft non-empty → empty), the annotations flip to 'sent', and the prefix
7
+ * dissolves with the already-cleared draft.
8
+ *
9
+ * insertReference spike conclusion: the U+FFFC chip path is NOT viable from a
10
+ * plugin — `SessionInput.insertReference` requires a span CAS'd against the
11
+ * live draftRev (a trigger-pipeline concept), and chip serialization routes
12
+ * through the source owner's ReferenceCodec, which is package-internal to
13
+ * ui-input-trigger with no plugin-facing registry. An unowned source would
14
+ * mark the occurrence invalid and fail the whole submit. Hence this file.
15
+ */
16
+ import type { Context, SessionId, SessionInput } from '../../context-types.ts';
17
+ import type { AnnotationStore } from './model.ts';
18
+ /** Resolve the per-session input facade, degrading to undefined (never throws). */
19
+ export declare function resolveInput(ctx: Context, sessionId: SessionId): SessionInput | undefined;
20
+ /**
21
+ * Rewrite one session's draft so its managed head matches the active
22
+ * annotations. A no-op when nothing would change (the common case while the
23
+ * user types their own message below the head).
24
+ */
25
+ export declare function syncSessionDraft(ctx: Context, store: AnnotationStore, sessionId: SessionId): void;
26
+ /** Re-sync every session holding annotations (store-change fan-out). */
27
+ export declare function syncAllDrafts(ctx: Context, store: AnnotationStore): void;
28
+ /** Forget all managed heads (page-level lifecycle; plugin teardown/testing). */
29
+ export declare function resetManagedHeads(): void;
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Quote-block formatting and the managed-draft math (Workitem 02). Pure and
3
+ * dependency-free for unit testing — the shapes here ARE the contract for
4
+ * 「发送给模型的数据形态」:
5
+ *
6
+ * > 原文片段 1
7
+ * 注解:xxx
8
+ *
9
+ * > 原文片段 2
10
+ * (无注解)
11
+ *
12
+ * 用户消息正文
13
+ */
14
+ /** Selected-text bound admitted into one annotation (超长截断 + 标记). */
15
+ export declare const SELECTION_LIMIT = 500;
16
+ /** Marker appended when a selection is truncated (参照 sidebar-qa 的省略号模式). */
17
+ export declare const TRUNCATION_MARK = "\u2026";
18
+ /** Bound a quote to {@link SELECTION_LIMIT} characters, marking truncation. */
19
+ export declare function truncateQuote(text: string, limit?: number): string;
20
+ /** Quote one (possibly multi-line) text as `> `-prefixed lines. */
21
+ export declare function quoteLines(text: string): string;
22
+ export interface QuoteItem {
23
+ readonly text: string;
24
+ readonly note: string;
25
+ }
26
+ /** One annotation's block: quoted original plus the note line. */
27
+ export declare function formatQuoteItem(item: QuoteItem): string;
28
+ /** The full context block for every active annotation, in creation order. */
29
+ export declare function buildQuoteBlock(items: readonly QuoteItem[]): string;
30
+ /** The 「在侧边聊天中提问」 seed: quote + optional note(与主对话注释同构). */
31
+ export declare function buildSideChatQuote(text: string, note?: string): string;
32
+ /**
33
+ * Recompute a session composer draft from its managed head.
34
+ *
35
+ * The managed head (quote block + one blank line) is maintained at the very
36
+ * top of the draft while annotations are active; the user's own text follows
37
+ * it untouched. `lastHead` is the head we last wrote: when the draft still
38
+ * starts with it we strip exactly that span; otherwise the user meddled with
39
+ * the head region and the whole draft is treated as user text (the fresh head
40
+ * is simply re-prepended — a documented, non-crashing degradation).
41
+ */
42
+ export declare function nextManagedDraft(draft: string, lastHead: string, block: string): {
43
+ head: string;
44
+ draft: string;
45
+ };
46
+ /**
47
+ * The send-edge predicate: the composer draft flipping non-empty → empty
48
+ * while annotations were attached is the reliable「发送后」signal (submit
49
+ * clears the draft as a commit; queueing does too). Whitespace-only drafts
50
+ * count as empty.
51
+ */
52
+ export declare function isSendEdge(previousDraft: string, nextDraft: string): boolean;
@@ -0,0 +1,2 @@
1
+ import type { Context } from '../../context-types.ts';
2
+ export declare function registerAnnotations(ctx: Context): void;
@@ -0,0 +1,59 @@
1
+ /**
2
+ * The annotation store: per-session numbered selection annotations. Pure
3
+ * (DOM-free, React-free) so the whole lifecycle is unit-testable. One instance
4
+ * per plugin activation; page-level lifecycle — nothing is persisted.
5
+ *
6
+ * Numbering: a per-session monotonic counter assigns each annotation its
7
+ * display number at creation (①②… rendered as plain digits). Numbers are
8
+ * never reused and never re-packed on delete — 删除不重排.
9
+ *
10
+ * State machine per annotation: 'active' (counts in the composer chip, rides
11
+ * the managed draft prefix) → 'sent' (the message it was attached to went
12
+ * out; badge stays on the message flow but the chip no longer counts it).
13
+ */
14
+ export interface Annotation {
15
+ /** Store-unique identity (monotonic across sessions). */
16
+ readonly id: number;
17
+ /** Per-session display number assigned at creation; never reused. */
18
+ readonly number: number;
19
+ readonly sessionId: string;
20
+ /** `data-chat-anchor-key` of the owning message (re-anchor key). */
21
+ readonly anchorKey: string | undefined;
22
+ /** Truncated quote text (what the model and the chip see). */
23
+ readonly text: string;
24
+ /** Full selection text (re-anchor needle; untruncated). */
25
+ readonly anchorText: string;
26
+ /** 0-based ordinal of the quote's occurrence inside the anchor element. */
27
+ readonly occurrence: number;
28
+ /** User-written note; '' means 纯引用. */
29
+ readonly note: string;
30
+ readonly state: 'active' | 'sent';
31
+ readonly createdAt: number;
32
+ }
33
+ export interface AnnotationDraft {
34
+ readonly sessionId: string;
35
+ readonly anchorKey: string | undefined;
36
+ readonly text: string;
37
+ readonly anchorText: string;
38
+ readonly occurrence: number;
39
+ readonly note: string;
40
+ }
41
+ export interface AnnotationStore {
42
+ /** Monotonic version, referentially stable between mutations (uSES-ready). */
43
+ getSnapshot(): number;
44
+ subscribe(fn: () => void): () => void;
45
+ /** Create an active annotation; returns it (id/number assigned). */
46
+ add(draft: AnnotationDraft): Annotation;
47
+ setNote(id: number, note: string): void;
48
+ remove(id: number): void;
49
+ /** Flip every active annotation of the session to 'sent' (send edge). */
50
+ markSessionSent(sessionId: string): void;
51
+ get(id: number): Annotation | undefined;
52
+ /** All annotations of a session in creation order (active + sent). */
53
+ list(sessionId: string): readonly Annotation[];
54
+ listActive(sessionId: string): readonly Annotation[];
55
+ countActive(sessionId: string): number;
56
+ /** Sessions currently holding any annotation (draft-sync fan-out). */
57
+ sessions(): readonly string[];
58
+ }
59
+ export declare function createAnnotationStore(now?: () => number): AnnotationStore;
@@ -0,0 +1,27 @@
1
+ import { Component } from 'react';
2
+ import type { ReactNode } from 'react';
3
+ import type { Context } from '../../context-types.ts';
4
+ import type { AnnotationStore } from './model.ts';
5
+ import type { SelectionController } from './selection.ts';
6
+ interface OverlayProps {
7
+ readonly ctx: Context;
8
+ readonly store: AnnotationStore;
9
+ readonly controller: SelectionController;
10
+ }
11
+ /** Crash guard: a render failure must never take the host page down. */
12
+ export declare class AnnotateErrorBoundary extends Component<{
13
+ children: ReactNode;
14
+ }, {
15
+ failed: boolean;
16
+ }> {
17
+ state: {
18
+ failed: boolean;
19
+ };
20
+ static getDerivedStateFromError(): {
21
+ failed: boolean;
22
+ };
23
+ componentDidCatch(error: unknown): void;
24
+ render(): ReactNode;
25
+ }
26
+ export declare function AnnotateOverlay(props: OverlayProps): ReactNode;
27
+ export {};
@@ -0,0 +1,49 @@
1
+ /** One validated selection, ready for the toolbar. */
2
+ export interface SelectionSnapshot {
3
+ /** Truncated quote (sent to the model / stored on the annotation). */
4
+ readonly text: string;
5
+ /** Full selection text (re-anchor needle). */
6
+ readonly anchorText: string;
7
+ /** `data-chat-anchor-key` of the owning message, when present. */
8
+ readonly anchorKey: string | undefined;
9
+ /** 0-based ordinal of this selection among identical texts in the anchor. */
10
+ readonly occurrence: number;
11
+ /** Bounding box for toolbar placement. */
12
+ readonly rect: {
13
+ readonly left: number;
14
+ readonly top: number;
15
+ readonly width: number;
16
+ };
17
+ /** A live clone of the selection range (badge anchor until re-render). */
18
+ readonly range: Range;
19
+ readonly sessionId: string;
20
+ }
21
+ export interface SelectionState {
22
+ readonly selection: SelectionSnapshot | null;
23
+ }
24
+ /** The assistant message node kind (v1 实测纠正值 — 勿引旧值 `assistant`). */
25
+ export declare const ASSISTANT_KIND = "assistant-step";
26
+ /** Pure validation of one candidate selection (unit-tested without DOM). */
27
+ export declare function isEligibleSelection(input: {
28
+ readonly blank: boolean;
29
+ readonly sameMessage: boolean;
30
+ readonly kind: string;
31
+ readonly streaming: boolean;
32
+ readonly excluded: boolean;
33
+ readonly hasSession: boolean;
34
+ }): boolean;
35
+ /**
36
+ * The 0-based ordinal of the selection's text among its occurrences in the
37
+ * anchor element — the disambiguator for repeated phrases (re-anchor key).
38
+ */
39
+ export declare function occurrenceOf(anchor: HTMLElement, range: Range, text: string): number;
40
+ /** Capture and validate the current window selection, or null when invalid. */
41
+ export declare function captureSelection(currentSessionId: string): SelectionSnapshot | null;
42
+ export interface SelectionController {
43
+ getSnapshot(): SelectionState;
44
+ subscribe(fn: () => void): () => void;
45
+ clear(): void;
46
+ dispose(): void;
47
+ }
48
+ /** Create one selection controller (call once per plugin activation). */
49
+ export declare function createSelectionController(getSessionId: () => string): SelectionController;
@@ -0,0 +1,19 @@
1
+ /**
2
+ * In-plugin bridge between the annotate module (Workitem 02/03) and the
3
+ * sidechat module (Workitem 01). The sidechat module installs an
4
+ * implementation on activation; annotate's 「在侧边聊天中提问」 button calls
5
+ * it when present. Module-level singleton by design: exactly one sidechat
6
+ * module and one annotate module exist per activation.
7
+ */
8
+ import type { SessionId } from '../context-types.ts';
9
+ export interface SideChatBridge {
10
+ /**
11
+ * Open (or focus) a side chat forked from `sessionId`, seeding its composer
12
+ * draft with `draftText` (quoted selection + annotation). Returns false
13
+ * when no side chat could be opened (caller keeps its own flow).
14
+ */
15
+ askInSideChat(sessionId: SessionId, draftText: string): boolean;
16
+ }
17
+ export declare const sideChatBridge: {
18
+ current: SideChatBridge | null;
19
+ };
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Client half of dsh-sidenote: the side-chat tabs (Workitem 01) and the
3
+ * selection annotations (Workitem 02). Thin consumer of dsh-better-sidebar —
4
+ * it builds no panel chrome (portal/resize/collapse/persistence); the panel
5
+ * container is entirely better-sidebar's.
6
+ *
7
+ * Requires the `betterSidebar` service (hard peer dependency): inject keeps
8
+ * the plugin inactive until better-sidebar provides it.
9
+ */
10
+ import type { Context } from '../context-types.ts';
11
+ export declare const inject: string[];
12
+ export declare function apply(ctx: Context): void;
@@ -0,0 +1,77 @@
1
+ /**
2
+ * dsh-sidenote 的双语层(zh/en),跟随 DSH 通用设置里的语言(`ctx.locale`,
3
+ * Host-backed locale.preference,实时切换)。模式照 better-sidebar
4
+ * `src/client/locales.ts`:attachLocale 挂服务,t() 读活动语言;
5
+ * 服务缺失(独立组合/测试)时回退浏览器语言。
6
+ */
7
+ export declare const LOCALE_NS = "dsh-sidenote";
8
+ /** 英文词典(key 的权威清单)。 */
9
+ export declare const en: {
10
+ menuTitle: string;
11
+ tabBaseTitle: string;
12
+ codeCopy: string;
13
+ codeCopied: string;
14
+ forkErrorTitle: string;
15
+ forkErrorHint: string;
16
+ missingTitle: string;
17
+ missingDetail: string;
18
+ missingHint: string;
19
+ preparing: string;
20
+ historyFailed: string;
21
+ emptyTitle: string;
22
+ emptyText: string;
23
+ thinking: string;
24
+ writing: string;
25
+ stopped: string;
26
+ toolLabel: string;
27
+ failed: string;
28
+ running: string;
29
+ inputPlaceholder: string;
30
+ modelLabel: string;
31
+ modelFollowsMain: string;
32
+ stopReply: string;
33
+ stopReplyTitle: string;
34
+ send: string;
35
+ cmdDesc: string;
36
+ cmdNew: string;
37
+ cmdNewDetail: string;
38
+ cmdFocus: string;
39
+ cmdFocusDetail: string;
40
+ addToConversation: string;
41
+ askInSideChat: string;
42
+ toolbarAria: string;
43
+ notePlaceholder: string;
44
+ confirmTitle: string;
45
+ saveNoteAria: string;
46
+ sideNotePlaceholder: string;
47
+ sideNoteAria: string;
48
+ confirmAskAria: string;
49
+ openSideFailed: string;
50
+ deleteNote: string;
51
+ cancel: string;
52
+ save: string;
53
+ chipOne: string;
54
+ chipMany: string;
55
+ removeTitle: string;
56
+ removeAria: string;
57
+ noteLine: string;
58
+ noNote: string;
59
+ };
60
+ export type CopyKey = keyof typeof en;
61
+ /** 中文词典(与 en 同 key)。 */
62
+ export declare const zh: Record<CopyKey, string>;
63
+ /** The DSH locale service face we consume (subset of LocaleRuntime). */
64
+ export interface LocaleServiceLike {
65
+ getSnapshot(): {
66
+ active: string;
67
+ };
68
+ subscribe(fn: () => void): () => void;
69
+ }
70
+ /** Attach the DSH locale service (call once from the client apply). */
71
+ export declare function attachLocale(service: LocaleServiceLike | undefined): void;
72
+ /** Subscribe to locale switches (component re-render driver). */
73
+ export declare function subscribeLocale(fn: () => void): () => void;
74
+ /** Translate a copy key; `{name}` placeholders interpolate from `params`. */
75
+ export declare function t(key: CopyKey, params?: Record<string, string | number>): string;
76
+ /** 语言切换时驱动组件重渲(useSyncExternalStore 标准接法)。 */
77
+ export declare function useLocaleTick(): void;
@@ -0,0 +1,2 @@
1
+ import type { TabComponentProps } from '../../context-types.ts';
2
+ export declare function SideChatPanel(props: TabComponentProps): import("react").JSX.Element;
@@ -0,0 +1,26 @@
1
+ import type { Context, SessionFace, SessionInput } from '../../context-types.ts';
2
+ /**
3
+ * 解析 childId 会话的 input 草稿机;任何一步缺失/抛错返回 null
4
+ * (调用方走降级路径)。
5
+ */
6
+ export declare function resolveSessionInput(ctx: Context, childId: string): SessionInput | null;
7
+ /** 读草稿机的当前草稿(容错)。 */
8
+ export declare function readInputDraft(input: SessionInput): string;
9
+ /** composer 对外脸:machine 标记实际落点(true = input 机器,false = 自绘降级)。 */
10
+ export interface Composer {
11
+ readonly machine: boolean;
12
+ readonly draft: string;
13
+ readonly sendError: string | null;
14
+ setDraft(text: string): void;
15
+ /** 拼接一段外部文本(桥接注入的引文草稿)。 */
16
+ appendDraft(text: string): void;
17
+ /** 发送当前草稿(空草稿 no-op)。 */
18
+ submit(): void;
19
+ }
20
+ /**
21
+ * 面板 composer hook。机器路径:草稿住在 input 机器的 store 里(随会话持久、
22
+ * 与主输入框同语义),submit 走机器事务(序列化/裁决/默认 sink 全在机器内)。
23
+ * 降级路径:草稿住本地 state,发送直连 session.prompt([...], 'queue'),
24
+ * 失败时回填草稿并报错。
25
+ */
26
+ export declare function useComposer(ctx: Context, session: SessionFace | undefined, childId: string | undefined): Composer;
@@ -0,0 +1,2 @@
1
+ import type { Context } from '../../context-types.ts';
2
+ export declare function registerSideChat(ctx: Context): void;
@@ -0,0 +1,101 @@
1
+ /**
2
+ * 侧边聊天的纯逻辑层:Tab 编号标题、meta 序列化/解析容错、状态树遍历、
3
+ * 会话快照 → 消息流折叠、面板相位判定。全部无副作用,单测直接覆盖。
4
+ *
5
+ * 数据源事实(权威注释):
6
+ * - SidebarState 真实形态是 splits/bottomSplits 两棵 split/leaf 树
7
+ * (dsh-better-sidebar src/client/state.ts),leaf.tabs 持有 SidebarTab。
8
+ * - ConversationSnapshot.nodes 是 ConversationNode 联合(kind 判别),
9
+ * partial/runningCalls 承载在途流式输出(dsh-client-runtime
10
+ * lib/types/client/sessions/conversation.d.ts)。
11
+ */
12
+ import type { ConversationSnapshot, Context, SidebarTab } from '../../context-types.ts';
13
+ /** Tab 类型 id(better-sidebar 注册表键;带包前缀防冲突)。 */
14
+ export declare const SIDE_TAB_TYPE = "dsh-sidenote:side";
15
+ /** 多实例 Tab id:`side:<uuid>`(terminal/browser 的 createTab 铸 id 先例)。 */
16
+ export declare function mintSideTabId(): string;
17
+ /**
18
+ * 标题编号:首个「侧边」;并存时新 Tab「侧边 N」(N = 既有最大编号 + 1,
19
+ * 从标题解析)——关掉「侧边」再新建不会重名复用「侧边 2」。
20
+ * @param existingTitles - 创建时刻同会话侧边聊天 Tab 的标题集。
21
+ */
22
+ export declare function sideTabTitle(existingTitles: readonly string[]): string;
23
+ /** 侧边聊天寄存在 SidebarTab.meta 上的插件自有 JSON。 */
24
+ export interface SideChatMeta {
25
+ /** fork 出的子会话 id;缺省 = 尚未 fork(刚创建)。 */
26
+ childId?: string;
27
+ /** fork 来源主会话 id(面板归属校验用;面板天然落在其 sidebar 状态里)。 */
28
+ parentSessionId?: string;
29
+ /** 桥接(WI-03 划选提问)写入的待注入草稿;面板应用后清除。 */
30
+ pendingDraft?: string;
31
+ }
32
+ /**
33
+ * 容错解析 tab.meta:布局持久化里的 meta 来自上一版本的自己,字段缺失或
34
+ * 类型漂移一律降级为缺省,绝不抛错(刷新恢复路径不许崩)。
35
+ */
36
+ export declare function parseSideChatMeta(meta: unknown): SideChatMeta;
37
+ /** 清除 meta.pendingDraft(其余字段原样保留)。 */
38
+ export declare function clearPendingDraft(meta: SideChatMeta): SideChatMeta;
39
+ /** 枚举侧栏状态(splits + bottomSplits 两棵树)里的全部 Tab。 */
40
+ export declare function collectTabs(state: unknown): SidebarTab[];
41
+ /** 枚举当前并存的侧边聊天 Tab(树顺序 = 打开顺序的近似)。 */
42
+ export declare function collectSideTabs(state: unknown): SidebarTab[];
43
+ /** 并存数(createTab 编号标题的输入)。 */
44
+ export declare function countSideTabs(state: unknown): number;
45
+ /**
46
+ * blank 着陆页会话没有已完成 turn,fork 必败(host 返回 fork-unavailable)
47
+ * ——+ 菜单与 /side 命令的 available 用它禁用入口;面板内错误态兜底。
48
+ * 摘要求知(列表未就绪)时放行,交给 fork 错误态。
49
+ */
50
+ export declare function canForkFrom(ctx: Context, sessionId: string): boolean;
51
+ /**
52
+ * 面板相位:
53
+ * - forking:无 childId,fork 编排进行中(或等待 effect 起跑);
54
+ * - fork-error:fork/归档失败(blank 会话无已完成 turn 等);
55
+ * - loading:有 childId,列表未就绪或 binding 尚未可解析;
56
+ * - missing:列表就绪后子会话不在列(会话已不存在);
57
+ * - chat:binding 解析成功,正常聊天。
58
+ */
59
+ export type PanelPhase = 'forking' | 'fork-error' | 'loading' | 'missing' | 'chat';
60
+ export declare function phaseOf(input: {
61
+ childId: string | undefined;
62
+ forkError: string | null;
63
+ bound: boolean;
64
+ listPhase: 'pending' | 'ready' | undefined;
65
+ listed: boolean;
66
+ }): PanelPhase;
67
+ /** 注入草稿的拼接规则:空草稿直接落文本,否则换行追加(引文+问题的自然形态)。 */
68
+ export declare function appendDraftText(draft: string, text: string): string;
69
+ /** 面板渲染用的消息视图(自绘;工具卡片等复杂节点降级为简洁块)。 */
70
+ export interface ChatMessage {
71
+ /** React key(节点 seq / 在途 callId 派生,稳定)。 */
72
+ key: string;
73
+ role: 'user' | 'assistant' | 'tool' | 'notice' | 'error';
74
+ /** markdown 正文(assistant)或纯文本(其他)。 */
75
+ text: string;
76
+ /** assistant 的思考内容(折叠渲染);缺省 = 无。 */
77
+ reasoning?: string;
78
+ /** tool 角色的工具名。 */
79
+ toolName?: string;
80
+ /** tool 角色的失败标记。 */
81
+ isError?: boolean;
82
+ /** 流式中(partial / runningCalls)。 */
83
+ streaming?: boolean;
84
+ /** 被打断冻结的 assistant 输出(渲染「已停止」标记)。 */
85
+ interrupted?: boolean;
86
+ }
87
+ /** 工具结果正文截断上限(面板是窄栏,超长输出不撑爆 DOM)。 */
88
+ export declare const TOOL_TEXT_LIMIT = 4000;
89
+ export declare function truncateText(text: string, max: number): string;
90
+ /** ContentBlock[] → 纯文本:text 块拼接;image 块降级占位;其余忽略。 */
91
+ export declare function contentTextOf(content: unknown): string;
92
+ /**
93
+ * 单个 ConversationNode → ChatMessage;返回 null = 面板不渲染
94
+ * (context 注入、未知面事件、已取消的重试等对读者无信息的节点)。
95
+ */
96
+ export declare function nodeToMessage(node: unknown): ChatMessage | null;
97
+ /**
98
+ * ConversationSnapshot → 渲染消息列表:终态节点 + 在途工具调用 + 流式部分。
99
+ * 快照缺省(未绑定)时为空列表。
100
+ */
101
+ export declare function transcriptOf(snapshot: ConversationSnapshot | undefined | null): ChatMessage[];
@@ -0,0 +1,16 @@
1
+ /**
2
+ * 「新建或聚焦一个侧边聊天 Tab」的共享编排:桥接(WI-03 划选提问
3
+ * askInSideChat)与 /side 斜杠命令共用。
4
+ *
5
+ * 聚焦策略:同一主会话并存多个时聚焦最后一个(最近打开的)。
6
+ * 草稿注入:目标 Tab 已 fork 且 input 机器可达 → 直接写机器草稿;
7
+ * 否则写 meta.pendingDraft 移交面板(面板在 composer 就绪后应用并清除)。
8
+ */
9
+ import type { Context, SidebarTab } from '../../context-types.ts';
10
+ /** 从最新快照读一个 Tab(meta 合并写入前的读取面;布局即注册表)。 */
11
+ export declare function readTab(ctx: Context, tabId: string): SidebarTab | undefined;
12
+ /**
13
+ * 为 sessionId 新建或聚焦一个侧边聊天 Tab;draftText 给出时写入其草稿。
14
+ * @returns false = 未能打开(调用方保持自己的流程)。
15
+ */
16
+ export declare function openOrFocusSideChat(ctx: Context, sessionId: string, draftText?: string): boolean;
@@ -0,0 +1,12 @@
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
+ /** 注册 /side 命令;不可行时静默降级(返回 undefined)。 */
12
+ export declare function registerSideCommand(ctx: Context): void;