@keith9681/dsh-sidebar 0.15.2

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 (220) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +0 -0
  3. package/README_EN.md +0 -0
  4. package/cordis.patch.yml +49 -0
  5. package/lib/client-editor.js +34651 -0
  6. package/lib/client-mermaid.js +200629 -0
  7. package/lib/client-registry.js +13388 -0
  8. package/lib/client-terminal.js +8904 -0
  9. package/lib/client.js +13388 -0
  10. package/lib/index.js +3959 -0
  11. package/lib/invariant.js +22 -0
  12. package/lib/types/agent-pty.d.ts +226 -0
  13. package/lib/types/browser-probe.d.ts +15 -0
  14. package/lib/types/bundle-route.d.ts +12 -0
  15. package/lib/types/client/BrowserView.d.ts +22 -0
  16. package/lib/types/client/DiffTab.d.ts +6 -0
  17. package/lib/types/client/DiffView.d.ts +51 -0
  18. package/lib/types/client/EditorHost.d.ts +12 -0
  19. package/lib/types/client/FileTree.d.ts +36 -0
  20. package/lib/types/client/GitView.d.ts +8 -0
  21. package/lib/types/client/OrphanedTab.d.ts +11 -0
  22. package/lib/types/client/PdfView.d.ts +6 -0
  23. package/lib/types/client/RenderBoundary.d.ts +32 -0
  24. package/lib/types/client/SandboxStatusBar.d.ts +10 -0
  25. package/lib/types/client/SideCardSection.d.ts +74 -0
  26. package/lib/types/client/SideChatView.d.ts +16 -0
  27. package/lib/types/client/Sidebar.d.ts +6 -0
  28. package/lib/types/client/SubagentView.d.ts +16 -0
  29. package/lib/types/client/TabBar.d.ts +43 -0
  30. package/lib/types/client/TerminalView.d.ts +24 -0
  31. package/lib/types/client/TextEditor.d.ts +10 -0
  32. package/lib/types/client/TreePanel.d.ts +23 -0
  33. package/lib/types/client/UploadOverlay.d.ts +20 -0
  34. package/lib/types/client/add-plugin-modal.d.ts +15 -0
  35. package/lib/types/client/api.d.ts +268 -0
  36. package/lib/types/client/binary-download.d.ts +12 -0
  37. package/lib/types/client/breakpoints.d.ts +12 -0
  38. package/lib/types/client/browser.d.ts +50 -0
  39. package/lib/types/client/builtins/index.d.ts +18 -0
  40. package/lib/types/client/builtins/tabs.d.ts +11 -0
  41. package/lib/types/client/builtins/viewers.d.ts +3 -0
  42. package/lib/types/client/chunk-loader.d.ts +124 -0
  43. package/lib/types/client/chunks/editor.d.ts +9 -0
  44. package/lib/types/client/chunks/mermaid.d.ts +10 -0
  45. package/lib/types/client/chunks/terminal.d.ts +9 -0
  46. package/lib/types/client/cm-themes.d.ts +24 -0
  47. package/lib/types/client/conversation-draft.d.ts +14 -0
  48. package/lib/types/client/desktop-env.d.ts +35 -0
  49. package/lib/types/client/editable-state.d.ts +27 -0
  50. package/lib/types/client/editor-load.d.ts +66 -0
  51. package/lib/types/client/frame-batcher.d.ts +24 -0
  52. package/lib/types/client/icons.d.ts +57 -0
  53. package/lib/types/client/image-types.d.ts +3 -0
  54. package/lib/types/client/ime-guard.d.ts +36 -0
  55. package/lib/types/client/index.d.ts +19 -0
  56. package/lib/types/client/intercept.d.ts +33 -0
  57. package/lib/types/client/lang.d.ts +12 -0
  58. package/lib/types/client/lazy-chunk.d.ts +25 -0
  59. package/lib/types/client/link-intercept.d.ts +42 -0
  60. package/lib/types/client/locales.d.ts +366 -0
  61. package/lib/types/client/mermaid-blocks.d.ts +41 -0
  62. package/lib/types/client/mermaid-sanitize.d.ts +2 -0
  63. package/lib/types/client/mermaid.d.ts +12 -0
  64. package/lib/types/client/open-when-sized.d.ts +20 -0
  65. package/lib/types/client/open-with-settings.d.ts +4 -0
  66. package/lib/types/client/open-with.d.ts +87 -0
  67. package/lib/types/client/openpath-intercept.d.ts +41 -0
  68. package/lib/types/client/paths.d.ts +35 -0
  69. package/lib/types/client/pdf-types.d.ts +2 -0
  70. package/lib/types/client/plugin-settings.d.ts +8 -0
  71. package/lib/types/client/plugins-shared.d.ts +27 -0
  72. package/lib/types/client/plugins-tabs.d.ts +3 -0
  73. package/lib/types/client/plugins-viewers.d.ts +3 -0
  74. package/lib/types/client/prefs.d.ts +41 -0
  75. package/lib/types/client/produced-files.d.ts +23 -0
  76. package/lib/types/client/selection-payload.d.ts +27 -0
  77. package/lib/types/client/service.d.ts +440 -0
  78. package/lib/types/client/settings-nav-icon.d.ts +19 -0
  79. package/lib/types/client/shell-presets.d.ts +48 -0
  80. package/lib/types/client/sidechat-transcript.d.ts +103 -0
  81. package/lib/types/client/split-pane.d.ts +30 -0
  82. package/lib/types/client/state.d.ts +359 -0
  83. package/lib/types/client/subagent-detect.d.ts +54 -0
  84. package/lib/types/client/subagent-jobs.d.ts +63 -0
  85. package/lib/types/client/tab-content-memo.d.ts +33 -0
  86. package/lib/types/client/terminal-font.d.ts +20 -0
  87. package/lib/types/client/theme.d.ts +45 -0
  88. package/lib/types/client/titlebar-strip.d.ts +23 -0
  89. package/lib/types/client/upload.d.ts +69 -0
  90. package/lib/types/client/wco.d.ts +47 -0
  91. package/lib/types/config.d.ts +64 -0
  92. package/lib/types/context-types.d.ts +603 -0
  93. package/lib/types/fs-operations.d.ts +28 -0
  94. package/lib/types/fs-search.d.ts +24 -0
  95. package/lib/types/fs-tree.d.ts +51 -0
  96. package/lib/types/git.d.ts +82 -0
  97. package/lib/types/html-route.d.ts +59 -0
  98. package/lib/types/index.d.ts +47 -0
  99. package/lib/types/invariant.d.ts +15 -0
  100. package/lib/types/jobs-routes.d.ts +45 -0
  101. package/lib/types/open-external.d.ts +24 -0
  102. package/lib/types/prefs-shared.d.ts +225 -0
  103. package/lib/types/pty-deps.d.ts +78 -0
  104. package/lib/types/pty-manager.d.ts +168 -0
  105. package/lib/types/sidechat-core.d.ts +172 -0
  106. package/lib/types/sidechat-routes.d.ts +30 -0
  107. package/lib/types/subagent-activity.d.ts +44 -0
  108. package/lib/types/subagent-live-route.d.ts +44 -0
  109. package/lib/types/tools.d.ts +31 -0
  110. package/lib/types/trust-fence.d.ts +24 -0
  111. package/lib/types/wire.d.ts +38 -0
  112. package/package.json +173 -0
  113. package/scripts/install.ps1 +289 -0
  114. package/scripts/install.sh +287 -0
  115. package/src/agent-pty.ts +527 -0
  116. package/src/browser-probe.ts +26 -0
  117. package/src/bundle-route.ts +129 -0
  118. package/src/client/BrowserView.tsx +253 -0
  119. package/src/client/DiffTab.tsx +110 -0
  120. package/src/client/DiffView.tsx +305 -0
  121. package/src/client/EditorHost.tsx +525 -0
  122. package/src/client/FileTree.tsx +653 -0
  123. package/src/client/GitView.tsx +558 -0
  124. package/src/client/OrphanedTab.tsx +27 -0
  125. package/src/client/PdfView.tsx +110 -0
  126. package/src/client/RenderBoundary.tsx +49 -0
  127. package/src/client/SandboxStatusBar.tsx +60 -0
  128. package/src/client/SideCardSection.module.css +960 -0
  129. package/src/client/SideCardSection.tsx +1152 -0
  130. package/src/client/SideChatView.module.css +437 -0
  131. package/src/client/SideChatView.tsx +644 -0
  132. package/src/client/Sidebar.tsx +1148 -0
  133. package/src/client/SubagentView.module.css +547 -0
  134. package/src/client/SubagentView.tsx +883 -0
  135. package/src/client/TabBar.tsx +305 -0
  136. package/src/client/TerminalView.tsx +383 -0
  137. package/src/client/TextEditor.tsx +500 -0
  138. package/src/client/TreePanel.tsx +273 -0
  139. package/src/client/UploadOverlay.tsx +62 -0
  140. package/src/client/add-plugin-modal.tsx +199 -0
  141. package/src/client/api.ts +335 -0
  142. package/src/client/binary-download.tsx +23 -0
  143. package/src/client/breakpoints.ts +51 -0
  144. package/src/client/browser.ts +119 -0
  145. package/src/client/builtins/index.ts +37 -0
  146. package/src/client/builtins/tabs.tsx +331 -0
  147. package/src/client/builtins/viewers.tsx +125 -0
  148. package/src/client/chunk-loader.ts +351 -0
  149. package/src/client/chunks/editor.tsx +9 -0
  150. package/src/client/chunks/mermaid.tsx +10 -0
  151. package/src/client/chunks/terminal.tsx +9 -0
  152. package/src/client/cm-themes.ts +129 -0
  153. package/src/client/conversation-draft.ts +29 -0
  154. package/src/client/css-modules.d.ts +5 -0
  155. package/src/client/desktop-env.ts +72 -0
  156. package/src/client/editable-state.ts +70 -0
  157. package/src/client/editor-load.ts +92 -0
  158. package/src/client/frame-batcher.ts +56 -0
  159. package/src/client/icons.tsx +161 -0
  160. package/src/client/image-types.ts +8 -0
  161. package/src/client/ime-guard.ts +47 -0
  162. package/src/client/index.tsx +369 -0
  163. package/src/client/intercept.tsx +110 -0
  164. package/src/client/lang.ts +103 -0
  165. package/src/client/layout.css +94 -0
  166. package/src/client/lazy-chunk.tsx +89 -0
  167. package/src/client/link-intercept.ts +73 -0
  168. package/src/client/locales.ts +734 -0
  169. package/src/client/mermaid-blocks.ts +110 -0
  170. package/src/client/mermaid-sanitize.ts +93 -0
  171. package/src/client/mermaid.tsx +401 -0
  172. package/src/client/open-when-sized.ts +43 -0
  173. package/src/client/open-with-settings.tsx +135 -0
  174. package/src/client/open-with.ts +221 -0
  175. package/src/client/openpath-intercept.ts +62 -0
  176. package/src/client/paths.ts +47 -0
  177. package/src/client/pdf-types.ts +4 -0
  178. package/src/client/plugin-settings.ts +43 -0
  179. package/src/client/plugins-shared.ts +29 -0
  180. package/src/client/plugins-tabs.ts +87 -0
  181. package/src/client/plugins-viewers.ts +30 -0
  182. package/src/client/prefs.ts +207 -0
  183. package/src/client/produced-files.ts +91 -0
  184. package/src/client/selection-payload.ts +86 -0
  185. package/src/client/service.ts +838 -0
  186. package/src/client/settings-nav-icon.ts +45 -0
  187. package/src/client/shell-presets.ts +83 -0
  188. package/src/client/sidebar.module.css +2577 -0
  189. package/src/client/sidechat-transcript.ts +321 -0
  190. package/src/client/split-pane.tsx +322 -0
  191. package/src/client/state.ts +1175 -0
  192. package/src/client/subagent-detect.ts +137 -0
  193. package/src/client/subagent-jobs.ts +161 -0
  194. package/src/client/tab-content-memo.ts +46 -0
  195. package/src/client/terminal-font.ts +28 -0
  196. package/src/client/theme.ts +110 -0
  197. package/src/client/titlebar-strip.ts +37 -0
  198. package/src/client/upload.ts +187 -0
  199. package/src/client/wco.ts +120 -0
  200. package/src/config.ts +155 -0
  201. package/src/context-types.ts +610 -0
  202. package/src/fs-operations.ts +99 -0
  203. package/src/fs-search.ts +86 -0
  204. package/src/fs-tree.ts +158 -0
  205. package/src/git.ts +244 -0
  206. package/src/html-route.ts +106 -0
  207. package/src/index.ts +1132 -0
  208. package/src/invariant.ts +32 -0
  209. package/src/jobs-routes.ts +257 -0
  210. package/src/open-external.ts +90 -0
  211. package/src/prefs-shared.ts +268 -0
  212. package/src/pty-deps.ts +240 -0
  213. package/src/pty-manager.ts +376 -0
  214. package/src/sidechat-core.ts +468 -0
  215. package/src/sidechat-routes.ts +344 -0
  216. package/src/subagent-activity.ts +91 -0
  217. package/src/subagent-live-route.ts +93 -0
  218. package/src/tools.ts +479 -0
  219. package/src/trust-fence.ts +77 -0
  220. package/src/wire.ts +100 -0
@@ -0,0 +1,187 @@
1
+ /**
2
+ * File-upload plumbing for the files window: turn a file picker or a drag-drop
3
+ * into per-file raw-byte uploads through the sidebar's `/sidebar/upload` route.
4
+ *
5
+ * Folders keep their tree in both flows: the picker's `webkitdirectory`
6
+ * selection arrives as Files with `webkitRelativePath` filled, and dropped
7
+ * folders — which never surface in `dataTransfer.files` — are traversed via
8
+ * `webkitGetAsEntry`, so the relative path is preserved for every nested file
9
+ * and the host recreates the tree under the chosen directory. The File is
10
+ * streamed straight into the POST body (no base64 inflation); uploads run
11
+ * sequentially so one slow file cannot starve the others, and each result
12
+ * reports its own outcome (the tree keeps going after a failure). An
13
+ * optional `AbortSignal` stops the queue at the next item boundary and
14
+ * aborts the in-flight request; the host cleans up its temp file when the
15
+ * request stream dies.
16
+ */
17
+ import { api, SidebarApiError, type SessionScope } from './api.ts'
18
+ import { isAbsolutePath } from './paths.ts'
19
+ import type { CopyKey } from './locales.ts'
20
+
21
+ /** One pending file: the browser File plus the workspace-relative target path. */
22
+ export interface UploadItem {
23
+ file: File
24
+ relativePath: string
25
+ }
26
+
27
+ /** One settled upload. */
28
+ export interface UploadResult {
29
+ relativePath: string
30
+ ok: boolean
31
+ path?: string
32
+ /** Wire error code when the host refused the upload ('too-large', ...). */
33
+ code?: string
34
+ /** The host's error message (English wire text; localize via `code`). */
35
+ error?: string
36
+ }
37
+
38
+ /** Sanitize a relative target: absolute paths, traversal, and empty segments
39
+ * are rejected (the host enforces the same rules with a 400). */
40
+ function sanitizeRelativePath(rel: string): string | undefined {
41
+ if (rel === '' || isAbsolutePath(rel)) return undefined
42
+ if (rel.split(/[\\/]/).some((s) => s === '' || s === '.' || s === '..')) return undefined
43
+ return rel
44
+ }
45
+
46
+ /** The picker's relative path: webkitRelativePath when present, else the name. */
47
+ function relativePathOf(file: File): string | undefined {
48
+ return sanitizeRelativePath(file.webkitRelativePath || file.name || '')
49
+ }
50
+
51
+ /** Collect a picker selection (webkitdirectory folders carry relative paths). */
52
+ export function uploadItemsFromFiles(files: FileList | readonly File[]): UploadItem[] {
53
+ const items: UploadItem[] = []
54
+ for (const file of files) {
55
+ const rel = relativePathOf(file)
56
+ if (rel !== undefined) items.push({ file, relativePath: rel })
57
+ }
58
+ return items
59
+ }
60
+
61
+ /** Read one dropped file-system entry into upload items; directories
62
+ * recurse, prefixing their name onto every descendant's relative path. */
63
+ async function itemsFromEntry(entry: FileSystemEntry, prefix: string): Promise<UploadItem[]> {
64
+ if (entry.isFile) {
65
+ const file = await new Promise<File>((resolve, reject) => {
66
+ ;(entry as FileSystemFileEntry).file(resolve, reject)
67
+ })
68
+ const rel = sanitizeRelativePath(prefix + file.name)
69
+ return rel === undefined ? [] : [{ file, relativePath: rel }]
70
+ }
71
+ if (entry.isDirectory) {
72
+ const reader = (entry as FileSystemDirectoryEntry).createReader()
73
+ const entries: FileSystemEntry[] = []
74
+ // readEntries returns one BATCH per call; drain until an empty batch.
75
+ for (;;) {
76
+ const batch = await new Promise<FileSystemEntry[]>((resolve, reject) => {
77
+ reader.readEntries(resolve, reject)
78
+ })
79
+ if (batch.length === 0) break
80
+ entries.push(...batch)
81
+ }
82
+ const nested = await Promise.all(entries.map(child => itemsFromEntry(child, `${prefix}${entry.name}/`)))
83
+ return nested.flat()
84
+ }
85
+ return []
86
+ }
87
+
88
+ /**
89
+ * Collect a drag-drop payload. Dropped folders do NOT surface in
90
+ * `dataTransfer.files` — they arrive as directory items, so entries are
91
+ * captured via `webkitGetAsEntry` and traversed (draining readEntries
92
+ * batches), keeping each nested file's relative path. MUST be invoked
93
+ * synchronously from the drop handler: the dataTransfer enters protected
94
+ * mode once the event dispatch ends, while the captured entry handles stay
95
+ * readable asynchronously. Falls back to the flat file list when the entry
96
+ * API is unavailable; an entry that fails to read is skipped, not fatal.
97
+ */
98
+ export async function uploadItemsFromDrop(data: DataTransfer | undefined): Promise<UploadItem[]> {
99
+ if (data === undefined) return []
100
+ // Synchronous section: capture every entry handle before returning.
101
+ const entries = [...data.items]
102
+ .map(item => (item.kind === 'file' ? item.webkitGetAsEntry() : null))
103
+ .filter((entry): entry is FileSystemEntry => entry !== null)
104
+ if (entries.length === 0) return uploadItemsFromFiles(data.files)
105
+ const nested = await Promise.all(entries.map(entry => itemsFromEntry(entry, '').catch(() => [])))
106
+ return nested.flat()
107
+ }
108
+
109
+ /**
110
+ * Client-side pre-check cap. Mirrors the host's default (config `uploadLimit`)
111
+ * only — the host enforces the real, configurable limit, so a lowered host
112
+ * limit surfaces as a `too-large` wire error, never as a silent pass.
113
+ */
114
+ export const MAX_UPLOAD_BYTES = 128 * 1024 * 1024
115
+
116
+ /** How long a success hint stays before fading (failures stay until the next action). */
117
+ export const UPLOAD_HINT_MS = 3500
118
+
119
+ /**
120
+ * One-line upload progress text: 'Uploading into {dir}…' while no file is in
121
+ * flight, then 'Uploading {done}/{total}: {name}' per file. Shared by the tree
122
+ * hint and the full-window upload overlay.
123
+ */
124
+ export function uploadHintText(
125
+ done: number,
126
+ total: number,
127
+ current: string,
128
+ dir: string,
129
+ t: (key: CopyKey, params?: Record<string, string | number>) => string,
130
+ ): string {
131
+ return current === '' ? t('uploadingTo', { dir }) : t('uploadProgress', { done, total, name: current })
132
+ }
133
+
134
+ /**
135
+ * Upload every item into `dir` (absolute, inside the session workspace),
136
+ * sequentially, reporting progress as `(done, total, currentRelativePath)`.
137
+ * Resolves with one result per item — never rejects; `signal.aborted` stops
138
+ * the queue at the next item boundary (completed items stay uploaded).
139
+ */
140
+ export async function uploadToDir(
141
+ scope: SessionScope,
142
+ dir: string,
143
+ items: UploadItem[],
144
+ onProgress?: (done: number, total: number, current: string) => void,
145
+ signal?: AbortSignal,
146
+ ): Promise<UploadResult[]> {
147
+ const results: UploadResult[] = []
148
+ let done = 0
149
+ for (const item of items) {
150
+ if (signal?.aborted) break
151
+ onProgress?.(done, items.length, item.relativePath)
152
+ try {
153
+ if (item.file.size > MAX_UPLOAD_BYTES) {
154
+ results.push({ relativePath: item.relativePath, ok: false, code: 'too-large' })
155
+ } else {
156
+ const res = await api.uploadFile(scope, dir, item.relativePath, item.file, signal)
157
+ results.push({ relativePath: item.relativePath, ok: true, path: res.path })
158
+ }
159
+ } catch (error) {
160
+ // An aborted in-flight request is a cancel, not a failure: stop quietly.
161
+ if (error instanceof DOMException && error.name === 'AbortError') break
162
+ results.push({
163
+ relativePath: item.relativePath,
164
+ ok: false,
165
+ code: error instanceof SidebarApiError ? error.code : undefined,
166
+ error: error instanceof Error ? error.message : String(error),
167
+ })
168
+ }
169
+ done++
170
+ }
171
+ onProgress?.(done, items.length, '')
172
+ return results
173
+ }
174
+
175
+ /** Fold a result list into a one-line status for the tree hint. */
176
+ export function summarizeResults(
177
+ results: UploadResult[],
178
+ t: (key: CopyKey, params?: Record<string, string | number>) => string,
179
+ ): string {
180
+ const okCount = results.filter((r) => r.ok).length
181
+ const failed = results.find((r) => !r.ok)
182
+ if (failed !== undefined) {
183
+ const detail = failed.code === 'too-large' ? t('uploadTooLarge') : (failed.error ?? t('uploadFailedUnknown'))
184
+ return t('uploadFailed', { error: detail })
185
+ }
186
+ return t('uploadDone', { count: okCount })
187
+ }
@@ -0,0 +1,120 @@
1
+ /**
2
+ * Reactive Window Controls Overlay geometry — the STANDARD web mechanism
3
+ * for frameless Chromium shells that draw the native caption buttons
4
+ * (minimize / maximize / close) OVER the web content (Electron
5
+ * `titleBarOverlay` on Windows; issue #257). Feature-detected: absent in
6
+ * plain browsers, macOS, Tauri etc., where the snapshot stays `NONE` and
7
+ * the sidebar adapts nothing — this is the conservative "auto" signal.
8
+ *
9
+ * The API reports the real titlebar rect (CSS px), which differs between
10
+ * shells and Electron versions (32 / 36 …), and fires `geometrychange`
11
+ * when the window maximizes / restores / moves — so the strip must be
12
+ * reactive, not memoized like the URL stamps.
13
+ *
14
+ * Module-level store so the Sidebar shell can subscribe via
15
+ * `useSyncExternalStore`; `setWcoSourceForTests` swaps the source for unit
16
+ * tests (no real API exists in jsdom/headless).
17
+ */
18
+ export interface WcoSource {
19
+ readonly visible: boolean
20
+ getTitlebarAreaRect(): { readonly x: number; readonly y: number; readonly width: number; readonly height: number }
21
+ addEventListener(type: 'geometrychange', listener: () => void): void
22
+ removeEventListener(type: 'geometrychange', listener: () => void): void
23
+ }
24
+
25
+ /** The current overlay geometry (present=false = API unavailable). */
26
+ export interface WcoSnapshot {
27
+ readonly present: boolean
28
+ readonly height: number
29
+ }
30
+
31
+ /** Snapshot when the API is unavailable (plain browser / non-overlay shell). */
32
+ export const WCO_NONE: WcoSnapshot = Object.freeze({ present: false, height: 0 })
33
+
34
+ type Listener = () => void
35
+
36
+ let source: WcoSource | undefined
37
+ let snapshot: WcoSnapshot = WCO_NONE
38
+ let attached = false
39
+ let sourceListener: (() => void) | undefined
40
+ const listeners = new Set<Listener>()
41
+
42
+ function read(): WcoSnapshot {
43
+ if (source === undefined) return WCO_NONE
44
+ try {
45
+ // A present-but-NOT-visible API is a PHANTOM: headless Chromium and
46
+ // macOS builds expose the interface but draw no overlay (empty rect,
47
+ // visible=false). Only a genuinely visible overlay is authoritative —
48
+ // otherwise the resolution chain would trust a zero rect and silently
49
+ // skip the shell's real reserved space.
50
+ if (source.visible !== true) return { present: false, height: 0 }
51
+ const rect = source.getTitlebarAreaRect()
52
+ const height = Math.round(rect.height)
53
+ return Number.isFinite(height) && height > 0
54
+ ? { present: true, height }
55
+ : { present: true, height: 0 }
56
+ } catch {
57
+ // A hostile/racy API must never break the layout — treat as absent.
58
+ return { present: false, height: 0 }
59
+ }
60
+ }
61
+
62
+ function onGeometryChange(): void {
63
+ snapshot = read()
64
+ emit()
65
+ }
66
+
67
+ function emit(): void {
68
+ for (const listener of listeners) listener()
69
+ }
70
+
71
+ /** Attach the native geometrychange listener (once). */
72
+ function attach(): void {
73
+ if (attached) return
74
+ attached = true
75
+ // The test hook may have installed a source already; otherwise attach to
76
+ // the real navigator API (absent in plain browsers → stay NONE).
77
+ const candidate = source ?? (navigator as unknown as { windowControlsOverlay?: WcoSource }).windowControlsOverlay
78
+ if (candidate === undefined) return
79
+ source = candidate
80
+ sourceListener = onGeometryChange
81
+ snapshot = read()
82
+ source.addEventListener('geometrychange', sourceListener)
83
+ }
84
+
85
+ /** Detach the native listener (last subscriber left or source swapped). */
86
+ function detach(): void {
87
+ if (source !== undefined && sourceListener !== undefined) {
88
+ source.removeEventListener('geometrychange', sourceListener)
89
+ }
90
+ sourceListener = undefined
91
+ attached = false
92
+ }
93
+
94
+ /** Read the current snapshot (returns the frozen NONE when unavailable). */
95
+ export function getWcoSnapshot(): WcoSnapshot {
96
+ return snapshot
97
+ }
98
+
99
+ /**
100
+ * Subscribe to overlay geometry changes. Attaches to the real
101
+ * `navigator.windowControlsOverlay` on first subscribe; the disposer
102
+ * detaches the native listener when the last subscriber leaves.
103
+ */
104
+ export function subscribeWco(onChange: Listener): () => void {
105
+ listeners.add(onChange)
106
+ attach()
107
+ return () => {
108
+ listeners.delete(onChange)
109
+ if (listeners.size === 0) detach()
110
+ }
111
+ }
112
+
113
+ /** Test hook: swap the geometry source (undefined = API unavailable). */
114
+ export function setWcoSourceForTests(next: WcoSource | undefined): void {
115
+ detach()
116
+ source = next
117
+ snapshot = next === undefined ? WCO_NONE : read()
118
+ if (next !== undefined && listeners.size > 0) attach()
119
+ emit()
120
+ }
package/src/config.ts ADDED
@@ -0,0 +1,155 @@
1
+ /**
2
+ * Serializable configuration and defaults for the sidebar host half. Loader
3
+ * schema validation normally fills defaults; {@link resolveSidebarConfig}
4
+ * applies the same defaults for direct callers that bypass the Loader.
5
+ * @module dsh-better-sidebar/config
6
+ */
7
+
8
+ import z from 'schemastery'
9
+ import {
10
+ SIDEBAR_PREFS_DEFAULTS,
11
+ SIDEBAR_PREFS_NS,
12
+ TERMINAL_FONT_SIZE_DEFAULT,
13
+ TERMINAL_FONT_SIZE_MAX,
14
+ TERMINAL_FONT_SIZE_MIN,
15
+ TITLE_BAR_STRIP_DEFAULT,
16
+ TITLE_BAR_STRIP_MAX,
17
+ TITLE_BAR_STRIP_MIN,
18
+ WIDTH_PERCENT_DEFAULT,
19
+ WIDTH_PERCENT_MAX,
20
+ WIDTH_PERCENT_MIN,
21
+ type SidebarPrefs,
22
+ } from './prefs-shared.ts'
23
+
24
+ export {
25
+ SIDEBAR_PREFS_DEFAULTS,
26
+ SIDEBAR_PREFS_NS,
27
+ TERMINAL_FONT_SIZE_DEFAULT,
28
+ TERMINAL_FONT_SIZE_MAX,
29
+ TERMINAL_FONT_SIZE_MIN,
30
+ TITLE_BAR_STRIP_DEFAULT,
31
+ TITLE_BAR_STRIP_MAX,
32
+ TITLE_BAR_STRIP_MIN,
33
+ WIDTH_PERCENT_DEFAULT,
34
+ WIDTH_PERCENT_MAX,
35
+ WIDTH_PERCENT_MIN,
36
+ type SidebarPrefs,
37
+ } from './prefs-shared.ts'
38
+
39
+ /** Tunable sidebar host limits (every field optional; defaults fill in). */
40
+ export interface SidebarConfig {
41
+ /** Read cap of one text file (bytes); larger files return truncated. */
42
+ readLimit?: number
43
+ /** Media route cap (bytes); larger binaries are refused. */
44
+ mediaLimit?: number
45
+ /** Upload route cap (bytes); larger files are refused without touching disk. */
46
+ uploadLimit?: number
47
+ /** Explorer row bound of one level. */
48
+ listLimit?: number
49
+ /** Terminals per session. */
50
+ terminalsPerSession?: number
51
+ /** How long a disconnected terminal process survives awaiting a reconnect. */
52
+ reconnectGraceMs?: number
53
+ /**
54
+ * Terminal shell (absolute path or bare executable name) for BOTH the UI
55
+ * terminal tabs and the model-facing `terminal_*` tools. Empty = auto:
56
+ * POSIX follows `$SHELL` then the account login shell; Windows follows
57
+ * `DSH_SIDEBAR_SHELL`, then probes for `pwsh.exe`, then falls back to the
58
+ * inbox `powershell.exe` (5.1). Set it from `cordis.patch.yml` / profile
59
+ * plugin config, e.g. `config: { shell: /bin/zsh }`.
60
+ */
61
+ shell?: string
62
+ /**
63
+ * Optional arguments passed to the shell executable. When non-empty these
64
+ * REPLACE the automatic platform defaults (POSIX `-l` / Windows none), so
65
+ * the deployment has full control over how the shell starts. When omitted
66
+ * the existing default behavior is kept.
67
+ */
68
+ shellArgs?: string[]
69
+ }
70
+
71
+ /** Schemastery schema for the plugin configuration. */
72
+ export const Config: z<SidebarConfig> = z.object({
73
+ readLimit: z.number().step(1).min(1).default(512 * 1024),
74
+ mediaLimit: z.number().step(1).min(1).default(20 * 1024 * 1024),
75
+ uploadLimit: z.number().step(1).min(1).default(128 * 1024 * 1024),
76
+ listLimit: z.number().step(1).min(1).default(1000),
77
+ terminalsPerSession: z.number().step(1).min(1).default(3),
78
+ reconnectGraceMs: z.number().step(1).min(0).default(30_000),
79
+ shell: z.string().default(''),
80
+ shellArgs: z.array(z.string()).default([]),
81
+ })
82
+
83
+ /** Fully defaulted sidebar host settings. */
84
+ export interface ResolvedSidebarConfig {
85
+ readLimit: number
86
+ mediaLimit: number
87
+ uploadLimit: number
88
+ listLimit: number
89
+ terminalsPerSession: number
90
+ reconnectGraceMs: number
91
+ /** The configured terminal shell; empty means the host auto-resolves it. */
92
+ shell: string
93
+ /** Explicit shell arguments; empty means use the platform defaults. */
94
+ shellArgs: string[]
95
+ }
96
+
97
+ /**
98
+ * Apply direct-call defaults after Loader schema validation has normally run.
99
+ *
100
+ * @param config - Deployment-provided sidebar host settings.
101
+ * @returns Complete settings consumed by the host half.
102
+ */
103
+ export function resolveSidebarConfig(config: SidebarConfig | undefined): ResolvedSidebarConfig {
104
+ return {
105
+ readLimit: config?.readLimit ?? 512 * 1024,
106
+ mediaLimit: config?.mediaLimit ?? 20 * 1024 * 1024,
107
+ uploadLimit: config?.uploadLimit ?? 128 * 1024 * 1024,
108
+ listLimit: config?.listLimit ?? 1000,
109
+ terminalsPerSession: config?.terminalsPerSession ?? 3,
110
+ reconnectGraceMs: config?.reconnectGraceMs ?? 30_000,
111
+ shell: config?.shell?.trim() ?? '',
112
+ shellArgs: config?.shellArgs ?? [],
113
+ }
114
+ }
115
+
116
+ // ── User-facing "Side card" preferences ─────────────────────────────────────
117
+
118
+ /** Schemastery schema for the user-facing preferences (validated by the settings service). */
119
+ export const PrefsSchema: z<SidebarPrefs> = z.object({
120
+ openByDefault: z.boolean().default(false),
121
+ defaultWidthPercent: z.number().step(1).min(WIDTH_PERCENT_MIN).max(WIDTH_PERCENT_MAX).default(WIDTH_PERCENT_DEFAULT),
122
+ autoOpenSubagent: z.boolean().default(true),
123
+ autoOpenJobs: z.boolean().default(true),
124
+ agentTerminalTools: z.boolean().default(false),
125
+ // The bottom panel was removed from this fork; the pref stays in the
126
+ // schema only to tolerate old persisted documents (never defaults on).
127
+ bottomPanelAutoTerminal: z.boolean().default(false),
128
+ terminalFontFamily: z.string().default(''),
129
+ terminalFontSize: z.number().step(1).min(TERMINAL_FONT_SIZE_MIN).max(TERMINAL_FONT_SIZE_MAX).default(TERMINAL_FONT_SIZE_DEFAULT),
130
+ interceptOpenPath: z.boolean().default(true),
131
+ editorExplorer: z.boolean().default(false),
132
+ terminalShell: z.string().default(''),
133
+ terminalShellArgs: z.string().default(''),
134
+ titleBarScheme: z.union([z.const('auto'), z.const('web'), z.const('preset'), z.const('custom')]),
135
+ titleBarPresetId: z.string(),
136
+ customCss: z.string(),
137
+ titleBarCompat: z.boolean().default(false),
138
+ titleBarStripPx: z.number().step(1).min(TITLE_BAR_STRIP_MIN).max(TITLE_BAR_STRIP_MAX).default(TITLE_BAR_STRIP_DEFAULT),
139
+ htmlViewerNoSandbox: z.boolean().default(false),
140
+ htmlViewerDefaultUnsafe: z.boolean().default(false),
141
+ browserNoSandbox: z.boolean().default(false),
142
+ browserInterceptLinks: z.boolean().default(true),
143
+ browserInterceptHttp: z.boolean().default(true),
144
+ browserInterceptHttps: z.boolean().default(false),
145
+ // Per-feature enable switches are OPEN maps (any tab/viewer id, built-in or
146
+ // external): an absent key means enabled, so old documents resolve to {}
147
+ // (everything on) with no migration. Non-boolean values fail validation.
148
+ tabsEnabled: z.dict(z.boolean()).default({}),
149
+ viewersEnabled: z.dict(z.boolean()).default({}),
150
+ // Plugin-owned settings blobs (v0.12.0+) are an OPEN nested map: any
151
+ // descriptor id may carry any JSON-serializable values. This is the
152
+ // "settings seam" opening — without it the seam would drop third-party
153
+ // keys as unknown schema fields.
154
+ pluginSettings: z.dict(z.dict(z.any())).default({}),
155
+ })