@jaychang1989/dsh-webchat 0.3.0 → 0.4.1

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 (44) hide show
  1. package/NOTICE +10 -3
  2. package/README.md +44 -39
  3. package/README.zh.md +43 -43
  4. package/cordis.patch.yml +8 -7
  5. package/lib/client.js +203 -1703
  6. package/lib/index.js +246 -3884
  7. package/package.json +3 -35
  8. package/lib/client.js.map +0 -1
  9. package/lib/types/client/api.d.ts +0 -74
  10. package/lib/types/client/controller.d.ts +0 -19
  11. package/lib/types/client/index.d.ts +0 -30
  12. package/lib/types/client/locales.d.ts +0 -169
  13. package/lib/types/client/mount.d.ts +0 -23
  14. package/lib/types/client/panel/Markdown.d.ts +0 -15
  15. package/lib/types/client/panel/WebChatPanel.d.ts +0 -28
  16. package/lib/types/client/sidebar-entry-core.d.ts +0 -60
  17. package/lib/types/client/sidebar-entry.d.ts +0 -22
  18. package/lib/types/engine/engine.d.ts +0 -247
  19. package/lib/types/engine/html-md.d.ts +0 -23
  20. package/lib/types/index.d.ts +0 -63
  21. package/lib/types/protocol.d.ts +0 -107
  22. package/lib/types/routes.d.ts +0 -22
  23. package/lib/types/store.d.ts +0 -69
  24. package/lib/types/tools.d.ts +0 -27
  25. package/lib/types/transfer.d.ts +0 -138
  26. package/src/client/api.ts +0 -90
  27. package/src/client/controller.ts +0 -43
  28. package/src/client/css-modules.d.ts +0 -5
  29. package/src/client/index.ts +0 -75
  30. package/src/client/locales.ts +0 -172
  31. package/src/client/mount.tsx +0 -125
  32. package/src/client/panel/Markdown.tsx +0 -251
  33. package/src/client/panel/WebChatPanel.tsx +0 -760
  34. package/src/client/panel/panel.module.css +0 -936
  35. package/src/client/sidebar-entry-core.ts +0 -207
  36. package/src/client/sidebar-entry.ts +0 -49
  37. package/src/engine/engine.ts +0 -1385
  38. package/src/engine/html-md.ts +0 -126
  39. package/src/index.ts +0 -212
  40. package/src/protocol.ts +0 -116
  41. package/src/routes.ts +0 -307
  42. package/src/store.ts +0 -217
  43. package/src/tools.ts +0 -257
  44. package/src/transfer.ts +0 -602
@@ -1,207 +0,0 @@
1
- /**
2
- * Shared sidebar entry injection core.
3
- *
4
- * dsh's sidebar shell exposes no slot an external plugin can register into,
5
- * so the entry row is injected between the shell's New Session button and the
6
- * workspace browser. The injection self-heals: a MutationObserver watches the
7
- * sidebar root and re-inserts the row whenever a React re-render displaces it
8
- * (re-insertion happens in the same frame, before paint, so no flicker).
9
- *
10
- * The row is plain DOM (no React tree) so it can never disturb the shell's
11
- * reconciliation; the view it toggles is a separate root owned by the caller.
12
- *
13
- * Packages receive this file as a generated copy via scripts/sync-shared.mjs;
14
- * edit the shared source and re-run the sync instead of editing a copy.
15
- */
16
-
17
- /** Per-package configuration for one sidebar entry row. */
18
- export interface SidebarEntryOptions {
19
- /** Full attribute name identifying the injected row (idempotency key), e.g. 'data-dsh-ssh-entry'. */
20
- rowAttribute: string
21
- /** CSS selector matching the injected row, e.g. '[data-dsh-ssh-entry]'. */
22
- rowSelector: string
23
- /**
24
- * L2 semantic-attribute plugin id (issue #506, enum table:
25
- * skins/skin-center/contracts/semantic-attrs-v1.md). When set, the row also
26
- * outputs data-dsh-plugin="<id>" and data-dsh-part="sidebar-entry"; unset
27
- * leaves the row without semantic attributes.
28
- */
29
- plugin?: string
30
- /** Inline icon markup (matches the shell's 16px nav-icon look). */
31
- icon: string
32
- /** CSS module class names for the row and its two spans (entry / entryIcon / entryLabel). */
33
- css: Record<string, string>
34
- /** Localized row label (aria-label + visible text). */
35
- label(): string
36
- /** Optional localized tooltip (title attribute). */
37
- tooltip?(): string
38
- /** Click action (open/toggle the owning panel). */
39
- onToggle(): void
40
- /** Family-block position: 'before' inserts ahead of sibling plugin rows, 'after' behind them. */
41
- position: 'before' | 'after'
42
- /**
43
- * Selectors of the sibling plugin entry rows this package orders against
44
- * (its own row included — the placement guard excludes a row that is
45
- * already inside the root). Each package passes the same list it used
46
- * before the consolidation so the rendered order stays stable.
47
- */
48
- familySelectors: readonly string[]
49
- /** Optional active-state bridge; highlights the row while the panel is open. */
50
- active?: {
51
- subscribe(listener: () => void): () => void
52
- isOpen(): boolean
53
- }
54
- }
55
-
56
- /** Find the sidebar shell root element, or undefined while not yet mounted. */
57
- function sidebarRoot(): HTMLElement | undefined {
58
- const column = document.querySelector<HTMLElement>('[data-pane="sidebar"], [class*="sidebarCol"]')
59
- if (column === null) return undefined
60
- // Current shells wrap the sidebar UI: column > wrapper > root(logoRow owner).
61
- // Prefer the element that owns the logo row — the real sidebar UI root —
62
- // and fall back to the column's first child for legacy shells.
63
- const logoOwner = column.querySelector<HTMLElement>('[class*="logoRow"]')?.parentElement
64
- return logoOwner ?? (column.firstElementChild as HTMLElement | undefined)
65
- }
66
-
67
- /** The New Session button: nested in the logo row on current shells, a direct child on legacy shells. */
68
- function newSessionButton(root: HTMLElement): HTMLButtonElement | undefined {
69
- const nested = root.querySelector<HTMLButtonElement>('button[class*="newSession"]')
70
- if (nested !== null) return nested
71
- for (const child of root.children) {
72
- if (child.tagName === 'BUTTON') return child as HTMLButtonElement
73
- }
74
- return undefined
75
- }
76
-
77
- /** Build the entry row (a detached button; insert once the shell is up). */
78
- function createEntry(options: SidebarEntryOptions): HTMLButtonElement {
79
- const entry = document.createElement('button')
80
- entry.type = 'button'
81
- entry.setAttribute(options.rowAttribute, '')
82
- if (options.plugin !== undefined) {
83
- entry.setAttribute('data-dsh-plugin', options.plugin)
84
- entry.setAttribute('data-dsh-part', 'sidebar-entry')
85
- }
86
- entry.className = options.css['entry'] ?? ''
87
- entry.setAttribute('aria-label', options.label())
88
- if (options.tooltip !== undefined) entry.setAttribute('title', options.tooltip())
89
- entry.innerHTML = '<span class="' + (options.css['entryIcon'] ?? '') + '">' + options.icon
90
- + '</span><span class="' + (options.css['entryLabel'] ?? '') + '">' + options.label() + '</span>'
91
- entry.addEventListener('click', options.onToggle)
92
- return entry
93
- }
94
-
95
- /** Re-insert the entry after the New Session row (before the browser region). */
96
- function placeEntry(root: HTMLElement, entry: HTMLButtonElement, options: SidebarEntryOptions): boolean {
97
- const button = newSessionButton(root)
98
- if (button === undefined) return false
99
- if (entry.parentElement !== root) {
100
- // Position relative to the family block (entries injected by sibling
101
- // plugins), never relative to transient logoRow geometry: every family
102
- // plugin that self-heals during a re-render then lands in the same
103
- // relative order, so the entries cannot swap positions regardless of
104
- // observer callback order or of shell wrapper changes. There is no
105
- // append-to-end fallback: appending at the end would randomly reorder
106
- // the block after a shell re-render.
107
- const row = button.closest('[class*="logoRow"]')
108
- const base = (row !== null && row.parentElement === root) ? row : button
109
- const family = Array.from(root.children).filter(
110
- (el): el is HTMLElement => el instanceof HTMLElement && el.matches(options.familySelectors.join(', ')),
111
- )
112
- const anchor = options.position === 'before'
113
- ? (family.length > 0 ? family[0] : base.nextElementSibling)
114
- : (family.length > 0 ? family[family.length - 1]!.nextElementSibling : base.nextElementSibling)
115
- root.insertBefore(entry, anchor)
116
- }
117
- return true
118
- }
119
-
120
- /**
121
- * Mount the sidebar entry, waiting for the shell to render and self-healing
122
- * on later React re-renders.
123
- * @param options - the row's attribute/icon/copy/action/ordering configuration.
124
- * @returns disposer removing the entry and its observers.
125
- */
126
- export function mountSidebarEntry(options: SidebarEntryOptions): () => void {
127
- // DOM-level idempotency: whatever path mounted an entry row before this
128
- // call (a duplicated apply, an HMR re-injection, a stale module still
129
- // alive), never mount a second one. The existing row keeps working; a full
130
- // page reload is the ultimate reset.
131
- if (typeof document !== 'undefined' && document.querySelector(options.rowSelector) !== null) {
132
- return () => {}
133
- }
134
- const entry = createEntry(options)
135
- let root: HTMLElement | undefined
136
- let placed = false
137
-
138
- const tryPlace = (): void => {
139
- if (root !== undefined && !root.isConnected) {
140
- // The shell rebuilt the sidebar pane (whole-tree teardown); the root
141
- // observer is gone with the old tree, so detach it and re-query from
142
- // scratch. The new pane is later noticed by the body-level watcher.
143
- rootObserver.disconnect()
144
- root = undefined
145
- placed = false
146
- }
147
- if (placed) {
148
- // Cheap short-circuit: entry still lives in a mountable subtree.
149
- if (document.body.contains(entry)) return
150
- // Entry was torn down together with the old tree; reset and re-place.
151
- rootObserver.disconnect()
152
- root = undefined
153
- placed = false
154
- }
155
- root ??= sidebarRoot()
156
- if (root === undefined) return
157
- placed = placeEntry(root, entry, options)
158
- if (placed) {
159
- rootObserver.observe(root, { childList: true, subtree: true })
160
- }
161
- }
162
-
163
- // Body-level watcher retained as the "whole rebuild" fallback: when the shell
164
- // tears down the whole sidebar pane, the root observer is gone with it and
165
- // only this body observation can notice the new pane mounting. It is no
166
- // longer disconnected after placement; the placed-and-still-mounted case
167
- // short-circuits through the cheap document.body.contains(entry) check, so
168
- // unrelated app mutations (e.g. chat streaming) cost one contains check
169
- // instead of churning the full re-query.
170
- const waitObserver = new MutationObserver(() => { tryPlace() })
171
- waitObserver.observe(document.body, { childList: true, subtree: true })
172
-
173
- // Self-heal: if a React re-render displaces the row, re-insert it in the
174
- // same frame (microtask before paint -> no visible flicker).
175
- const rootObserver = new MutationObserver(() => {
176
- if (root === undefined || !root.isConnected) {
177
- placed = false
178
- tryPlace()
179
- return
180
- }
181
- if (!root.contains(entry)) {
182
- placed = placeEntry(root, entry, options)
183
- }
184
- })
185
-
186
- // Reflect the panel's open state on the row (active highlight). Note: assigning
187
- // undefined to dataset.active materializes data-active="undefined" and keeps the
188
- // row permanently highlighted — delete the attribute instead.
189
- const unsubscribeActive = options.active === undefined ? undefined : (() => {
190
- const syncActive = (): void => {
191
- if (options.active!.isOpen()) entry.dataset.active = 'true'
192
- else delete entry.dataset.active
193
- }
194
- const unsubscribe = options.active.subscribe(syncActive)
195
- syncActive()
196
- return unsubscribe
197
- })()
198
-
199
- tryPlace()
200
-
201
- return () => {
202
- waitObserver.disconnect()
203
- rootObserver.disconnect()
204
- unsubscribeActive?.()
205
- entry.remove()
206
- }
207
- }
@@ -1,49 +0,0 @@
1
- /**
2
- * Sidebar entry injection — package-specific wiring over the shared core
3
- * (sidebar-entry-core.ts). The row is plain DOM so it can never disturb the
4
- * shell's reconciliation; the panel view it toggles is a separate React root
5
- * mounted in the center column (see mount.tsx).
6
- */
7
- import type { PanelController } from './controller.ts'
8
- import { mountSidebarEntry as mountSharedSidebarEntry } from './sidebar-entry-core.ts'
9
- import type { WebChatKey } from './locales.ts'
10
- // The hashed CSS module map (entry / entryIcon / entryLabel keys) — the same
11
- // pattern dsh-ssh uses, so the row matches the shell's nav-item look.
12
- import css from './panel/panel.module.css'
13
-
14
- /** Stable data attribute identifying the injected entry row. */
15
- export const ENTRY_SELECTOR = '[data-dsh-webchat-entry]'
16
-
17
- /** Inline icon (matches the shell's 16px nav-icon look): a chat bubble with a globe dot. */
18
- const ICON = '<svg viewBox="0 0 16 16" width="14" height="14" fill="none" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M8 2.5a5.5 5.5 0 0 0-4.7 8.3L2.5 13.5l2.8-.8A5.5 5.5 0 1 0 8 2.5z"/><circle cx="8" cy="8" r="1.1" fill="currentColor" stroke="none"/></svg>'
19
-
20
- export interface SidebarEntryDeps {
21
- controller: PanelController
22
- /** Locale accessor (spelled here to keep the core dependency-free). */
23
- tt: (key: WebChatKey) => string
24
- }
25
-
26
- /**
27
- * Mount the sidebar entry, waiting for the shell to render and self-healing
28
- * on later React re-renders.
29
- * @param deps - controller and copy accessor.
30
- * @returns disposer removing the entry and its observers.
31
- */
32
- export function mountSidebarEntry(deps: SidebarEntryDeps): () => void {
33
- return mountSharedSidebarEntry({
34
- rowAttribute: 'data-dsh-webchat-entry',
35
- rowSelector: ENTRY_SELECTOR,
36
- plugin: 'webchat',
37
- icon: ICON,
38
- css,
39
- label: () => deps.tt('entry.label'),
40
- tooltip: () => deps.tt('entry.tooltip'),
41
- onToggle: () => { deps.controller.toggle() },
42
- position: 'after',
43
- familySelectors: ['[data-dsh-taskboard-entry]', '[data-dsh-ssh-entry]', '[data-dsh-webchat-entry]'],
44
- active: {
45
- subscribe: (listener) => deps.controller.subscribe(listener),
46
- isOpen: () => deps.controller.getSnapshot().panelOpen,
47
- },
48
- })
49
- }