@chatcode/cco-better-sidebar 0.18.0-alpha.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.
- package/LICENSE +21 -0
- package/README.md +554 -0
- package/README_EN.md +563 -0
- package/assets/logo.svg +1 -0
- package/cordis.patch.yml +79 -0
- package/lib/client-editor.js +55744 -0
- package/lib/client-mermaid.js +208256 -0
- package/lib/client-registry.js +30378 -0
- package/lib/client-terminal.js +16894 -0
- package/lib/client.js +30378 -0
- package/lib/index.js +6897 -0
- package/lib/invariant.js +22 -0
- package/lib/types/agent-opens.d.ts +65 -0
- package/lib/types/agent-pty.d.ts +226 -0
- package/lib/types/browser-probe.d.ts +15 -0
- package/lib/types/bundle-route.d.ts +12 -0
- package/lib/types/chatcode-update.d.ts +47 -0
- package/lib/types/client/AccountMenu.d.ts +1 -0
- package/lib/types/client/AiRatioSection.d.ts +3 -0
- package/lib/types/client/BrowserView.d.ts +39 -0
- package/lib/types/client/ChatcodeGate.d.ts +6 -0
- package/lib/types/client/CodingPlanModelsSettings.d.ts +9 -0
- package/lib/types/client/DiffTab.d.ts +6 -0
- package/lib/types/client/DiffView.d.ts +51 -0
- package/lib/types/client/EditorHost.d.ts +13 -0
- package/lib/types/client/FenceErrorNotice.d.ts +5 -0
- package/lib/types/client/FileTree.d.ts +41 -0
- package/lib/types/client/FreeWindow.d.ts +29 -0
- package/lib/types/client/GitView.d.ts +12 -0
- package/lib/types/client/MarkdownHtml.d.ts +32 -0
- package/lib/types/client/OrphanedTab.d.ts +11 -0
- package/lib/types/client/PdfView.d.ts +6 -0
- package/lib/types/client/ReleaseNotesDialog.d.ts +11 -0
- package/lib/types/client/RenderBoundary.d.ts +32 -0
- package/lib/types/client/SandboxStatusBar.d.ts +10 -0
- package/lib/types/client/SideCardSection.d.ts +74 -0
- package/lib/types/client/Sidebar.d.ts +8 -0
- package/lib/types/client/TabBar.d.ts +54 -0
- package/lib/types/client/TerminalView.d.ts +24 -0
- package/lib/types/client/TextEditor.d.ts +10 -0
- package/lib/types/client/TreePanel.d.ts +27 -0
- package/lib/types/client/UpdateDialog.d.ts +10 -0
- package/lib/types/client/UpdateSettingsPanel.d.ts +1 -0
- package/lib/types/client/UpdateSettingsSection.d.ts +10 -0
- package/lib/types/client/UploadOverlay.d.ts +20 -0
- package/lib/types/client/UsageHeatmapSection.d.ts +4 -0
- package/lib/types/client/add-plugin-modal.d.ts +15 -0
- package/lib/types/client/api.d.ts +345 -0
- package/lib/types/client/binary-download.d.ts +12 -0
- package/lib/types/client/breakpoints.d.ts +17 -0
- package/lib/types/client/browser.d.ts +60 -0
- package/lib/types/client/builtins/index.d.ts +18 -0
- package/lib/types/client/builtins/tabs.d.ts +11 -0
- package/lib/types/client/builtins/viewers.d.ts +3 -0
- package/lib/types/client/chat-presentation.d.ts +13 -0
- package/lib/types/client/chunk-loader.d.ts +124 -0
- package/lib/types/client/chunks/editor.d.ts +9 -0
- package/lib/types/client/chunks/mermaid.d.ts +10 -0
- package/lib/types/client/chunks/terminal.d.ts +9 -0
- package/lib/types/client/cm-themes.d.ts +24 -0
- package/lib/types/client/conversation-draft.d.ts +14 -0
- package/lib/types/client/desktop-env.d.ts +35 -0
- package/lib/types/client/editor-load.d.ts +66 -0
- package/lib/types/client/frame-batcher.d.ts +24 -0
- package/lib/types/client/icons.d.ts +66 -0
- package/lib/types/client/image-types.d.ts +3 -0
- package/lib/types/client/ime-guard.d.ts +36 -0
- package/lib/types/client/index.d.ts +19 -0
- package/lib/types/client/intercept.d.ts +44 -0
- package/lib/types/client/lang.d.ts +14 -0
- package/lib/types/client/layout-push.d.ts +15 -0
- package/lib/types/client/lazy-chunk.d.ts +25 -0
- package/lib/types/client/link-intercept.d.ts +42 -0
- package/lib/types/client/locales-ar.d.ts +18 -0
- package/lib/types/client/locales-de.d.ts +4 -0
- package/lib/types/client/locales-fr.d.ts +10 -0
- package/lib/types/client/locales-hi.d.ts +17 -0
- package/lib/types/client/locales-id.d.ts +15 -0
- package/lib/types/client/locales-it.d.ts +8 -0
- package/lib/types/client/locales-ja.d.ts +17 -0
- package/lib/types/client/locales-ko.d.ts +9 -0
- package/lib/types/client/locales-nl.d.ts +16 -0
- package/lib/types/client/locales-pl.d.ts +19 -0
- package/lib/types/client/locales-pt.d.ts +1 -0
- package/lib/types/client/locales-ru.d.ts +13 -0
- package/lib/types/client/locales-sv.d.ts +1 -0
- package/lib/types/client/locales-th.d.ts +17 -0
- package/lib/types/client/locales-tr.d.ts +17 -0
- package/lib/types/client/locales-vi.d.ts +17 -0
- package/lib/types/client/locales-zh-HK.d.ts +32 -0
- package/lib/types/client/locales-zh-MO.d.ts +32 -0
- package/lib/types/client/locales-zh-TW.d.ts +32 -0
- package/lib/types/client/locales.d.ts +571 -0
- package/lib/types/client/markdown-html.d.ts +96 -0
- package/lib/types/client/markdown-images.d.ts +45 -0
- package/lib/types/client/markdown-labels.d.ts +21 -0
- package/lib/types/client/md-toc.d.ts +4 -0
- package/lib/types/client/mermaid-blocks.d.ts +47 -0
- package/lib/types/client/mermaid-sanitize.d.ts +2 -0
- package/lib/types/client/mermaid.d.ts +12 -0
- package/lib/types/client/open-when-sized.d.ts +20 -0
- package/lib/types/client/open-with-settings.d.ts +4 -0
- package/lib/types/client/open-with.d.ts +87 -0
- package/lib/types/client/openpath-intercept.d.ts +52 -0
- package/lib/types/client/paths.d.ts +43 -0
- package/lib/types/client/pdf-types.d.ts +2 -0
- package/lib/types/client/pinned.d.ts +86 -0
- package/lib/types/client/plugin-settings.d.ts +8 -0
- package/lib/types/client/plugins-shared.d.ts +27 -0
- package/lib/types/client/plugins-tabs.d.ts +3 -0
- package/lib/types/client/plugins-viewers.d.ts +3 -0
- package/lib/types/client/prefs.d.ts +41 -0
- package/lib/types/client/produced-files.d.ts +29 -0
- package/lib/types/client/selection-payload.d.ts +27 -0
- package/lib/types/client/service.d.ts +445 -0
- package/lib/types/client/session-pins.d.ts +14 -0
- package/lib/types/client/settings-nav-icon.d.ts +27 -0
- package/lib/types/client/shell-presets.d.ts +48 -0
- package/lib/types/client/split-pane.d.ts +38 -0
- package/lib/types/client/state.d.ts +480 -0
- package/lib/types/client/tab-content-memo.d.ts +40 -0
- package/lib/types/client/terminal-font.d.ts +78 -0
- package/lib/types/client/terminal-links.d.ts +119 -0
- package/lib/types/client/theme.d.ts +45 -0
- package/lib/types/client/titlebar-strip.d.ts +23 -0
- package/lib/types/client/ui-tweaks/appicons.d.ts +16 -0
- package/lib/types/client/ui-tweaks/archive.d.ts +42 -0
- package/lib/types/client/ui-tweaks/branding.d.ts +45 -0
- package/lib/types/client/ui-tweaks/cachehit.d.ts +41 -0
- package/lib/types/client/ui-tweaks/gitbar.d.ts +103 -0
- package/lib/types/client/ui-tweaks/index.d.ts +279 -0
- package/lib/types/client/ui-tweaks/mcp.d.ts +28 -0
- package/lib/types/client/ui-tweaks/notifier.d.ts +125 -0
- package/lib/types/client/ui-tweaks/whale.d.ts +55 -0
- package/lib/types/client/upload.d.ts +69 -0
- package/lib/types/client/useChatcodeAccount.d.ts +10 -0
- package/lib/types/client/wco.d.ts +47 -0
- package/lib/types/codingplan.d.ts +37 -0
- package/lib/types/config.d.ts +95 -0
- package/lib/types/context-types.d.ts +540 -0
- package/lib/types/fs-operations.d.ts +30 -0
- package/lib/types/fs-search.d.ts +24 -0
- package/lib/types/fs-tree.d.ts +51 -0
- package/lib/types/git.d.ts +124 -0
- package/lib/types/html-route.d.ts +59 -0
- package/lib/types/index.d.ts +47 -0
- package/lib/types/invariant.d.ts +15 -0
- package/lib/types/login-route.d.ts +2 -0
- package/lib/types/open-external.d.ts +24 -0
- package/lib/types/path-security.d.ts +26 -0
- package/lib/types/prefs-shared.d.ts +257 -0
- package/lib/types/pty-deps.d.ts +79 -0
- package/lib/types/pty-manager.d.ts +168 -0
- package/lib/types/tools.d.ts +34 -0
- package/lib/types/trust-fence.d.ts +24 -0
- package/lib/types/ui-tweaks/archive.d.ts +71 -0
- package/lib/types/ui-tweaks/config.d.ts +186 -0
- package/lib/types/ui-tweaks/git-web.d.ts +43 -0
- package/lib/types/ui-tweaks/git.d.ts +311 -0
- package/lib/types/ui-tweaks/index.d.ts +17 -0
- package/lib/types/ui-tweaks/mcp.d.ts +103 -0
- package/lib/types/ui-tweaks/turn-outcome.d.ts +57 -0
- package/lib/types/ui-tweaks/web.d.ts +55 -0
- package/lib/types/wire.d.ts +38 -0
- package/package.json +213 -0
- package/scripts/install.ps1 +310 -0
- package/scripts/install.sh +287 -0
- package/src/agent-opens.ts +265 -0
- package/src/agent-pty.ts +527 -0
- package/src/browser-probe.ts +26 -0
- package/src/bundle-route.ts +129 -0
- package/src/chatcode-update.ts +193 -0
- package/src/client/AccountMenu.tsx +177 -0
- package/src/client/AiRatioSection.module.css +329 -0
- package/src/client/AiRatioSection.tsx +292 -0
- package/src/client/BrowserView.tsx +289 -0
- package/src/client/ChatcodeGate.tsx +35 -0
- package/src/client/CodingPlanModelsSettings.module.css +118 -0
- package/src/client/CodingPlanModelsSettings.tsx +101 -0
- package/src/client/DiffTab.tsx +114 -0
- package/src/client/DiffView.tsx +305 -0
- package/src/client/EditorHost.tsx +563 -0
- package/src/client/FenceErrorNotice.tsx +40 -0
- package/src/client/FileTree.tsx +701 -0
- package/src/client/FreeWindow.tsx +337 -0
- package/src/client/GitView.tsx +722 -0
- package/src/client/MarkdownHtml.tsx +296 -0
- package/src/client/OrphanedTab.tsx +27 -0
- package/src/client/PdfView.tsx +110 -0
- package/src/client/ReleaseNotesDialog.tsx +91 -0
- package/src/client/RenderBoundary.tsx +49 -0
- package/src/client/SandboxStatusBar.tsx +60 -0
- package/src/client/SideCardSection.module.css +932 -0
- package/src/client/SideCardSection.tsx +1169 -0
- package/src/client/Sidebar.tsx +1857 -0
- package/src/client/TabBar.tsx +365 -0
- package/src/client/TerminalView.tsx +424 -0
- package/src/client/TextEditor.tsx +468 -0
- package/src/client/TreePanel.tsx +279 -0
- package/src/client/UpdateDialog.tsx +186 -0
- package/src/client/UpdateSettingsPanel.tsx +29 -0
- package/src/client/UpdateSettingsSection.tsx +22 -0
- package/src/client/UploadOverlay.tsx +62 -0
- package/src/client/UsageHeatmapSection.module.css +440 -0
- package/src/client/UsageHeatmapSection.tsx +301 -0
- package/src/client/add-plugin-modal.tsx +199 -0
- package/src/client/api.ts +433 -0
- package/src/client/binary-download.tsx +23 -0
- package/src/client/breakpoints.ts +61 -0
- package/src/client/browser.ts +167 -0
- package/src/client/builtins/index.ts +37 -0
- package/src/client/builtins/tabs.tsx +275 -0
- package/src/client/builtins/viewers.tsx +125 -0
- package/src/client/chat-presentation.ts +95 -0
- package/src/client/chunk-loader.ts +350 -0
- package/src/client/chunks/editor.tsx +9 -0
- package/src/client/chunks/mermaid.tsx +10 -0
- package/src/client/chunks/terminal.tsx +9 -0
- package/src/client/cm-themes.ts +129 -0
- package/src/client/conversation-draft.ts +29 -0
- package/src/client/css-modules.d.ts +5 -0
- package/src/client/desktop-env.ts +72 -0
- package/src/client/editor-load.ts +92 -0
- package/src/client/frame-batcher.ts +56 -0
- package/src/client/icons.tsx +170 -0
- package/src/client/image-types.ts +8 -0
- package/src/client/ime-guard.ts +47 -0
- package/src/client/index.tsx +501 -0
- package/src/client/intercept.tsx +172 -0
- package/src/client/lang.ts +225 -0
- package/src/client/layout-push.ts +37 -0
- package/src/client/layout.css +156 -0
- package/src/client/lazy-chunk.tsx +89 -0
- package/src/client/link-intercept.ts +73 -0
- package/src/client/locales-ar.ts +387 -0
- package/src/client/locales-de.ts +372 -0
- package/src/client/locales-fr.ts +379 -0
- package/src/client/locales-hi.ts +386 -0
- package/src/client/locales-id.ts +384 -0
- package/src/client/locales-it.ts +377 -0
- package/src/client/locales-ja.ts +486 -0
- package/src/client/locales-ko.ts +378 -0
- package/src/client/locales-nl.ts +384 -0
- package/src/client/locales-pl.ts +388 -0
- package/src/client/locales-pt.ts +369 -0
- package/src/client/locales-ru.ts +384 -0
- package/src/client/locales-sv.ts +369 -0
- package/src/client/locales-th.ts +386 -0
- package/src/client/locales-tr.ts +386 -0
- package/src/client/locales-vi.ts +386 -0
- package/src/client/locales-zh-HK.ts +401 -0
- package/src/client/locales-zh-MO.ts +401 -0
- package/src/client/locales-zh-TW.ts +401 -0
- package/src/client/locales.ts +1229 -0
- package/src/client/markdown-html.ts +331 -0
- package/src/client/markdown-images.ts +137 -0
- package/src/client/markdown-labels.tsx +31 -0
- package/src/client/md-toc.tsx +127 -0
- package/src/client/mermaid-blocks.ts +110 -0
- package/src/client/mermaid-sanitize.ts +93 -0
- package/src/client/mermaid.tsx +406 -0
- package/src/client/open-when-sized.ts +43 -0
- package/src/client/open-with-settings.tsx +135 -0
- package/src/client/open-with.ts +221 -0
- package/src/client/openpath-intercept.ts +79 -0
- package/src/client/paths.ts +63 -0
- package/src/client/pdf-types.ts +4 -0
- package/src/client/pinned.ts +184 -0
- package/src/client/plugin-settings.ts +43 -0
- package/src/client/plugins-shared.ts +29 -0
- package/src/client/plugins-tabs.ts +153 -0
- package/src/client/plugins-viewers.ts +44 -0
- package/src/client/prefs.ts +216 -0
- package/src/client/produced-files.ts +120 -0
- package/src/client/selection-payload.ts +86 -0
- package/src/client/service.ts +873 -0
- package/src/client/session-pins.tsx +334 -0
- package/src/client/settings-nav-icon.ts +55 -0
- package/src/client/shell-presets.ts +83 -0
- package/src/client/sidebar.module.css +3607 -0
- package/src/client/split-pane.tsx +333 -0
- package/src/client/state.ts +1646 -0
- package/src/client/tab-content-memo.ts +55 -0
- package/src/client/terminal-font.ts +224 -0
- package/src/client/terminal-links.ts +194 -0
- package/src/client/theme.ts +110 -0
- package/src/client/titlebar-strip.ts +37 -0
- package/src/client/ui-tweaks/appicons.ts +24 -0
- package/src/client/ui-tweaks/archive.tsx +281 -0
- package/src/client/ui-tweaks/branding.tsx +232 -0
- package/src/client/ui-tweaks/cachehit.tsx +122 -0
- package/src/client/ui-tweaks/gitbar.tsx +2559 -0
- package/src/client/ui-tweaks/index.tsx +1696 -0
- package/src/client/ui-tweaks/mcp.tsx +582 -0
- package/src/client/ui-tweaks/notifier.ts +514 -0
- package/src/client/ui-tweaks/whale.tsx +171 -0
- package/src/client/upload.ts +187 -0
- package/src/client/useChatcodeAccount.ts +103 -0
- package/src/client/wco.ts +120 -0
- package/src/codingplan.ts +146 -0
- package/src/config.ts +207 -0
- package/src/context-types.ts +559 -0
- package/src/fs-operations.ts +97 -0
- package/src/fs-search.ts +113 -0
- package/src/fs-tree.ts +158 -0
- package/src/git.ts +446 -0
- package/src/html-route.ts +106 -0
- package/src/index.ts +1524 -0
- package/src/invariant.ts +32 -0
- package/src/login-route.ts +26 -0
- package/src/open-external.ts +90 -0
- package/src/path-security.ts +81 -0
- package/src/prefs-shared.ts +303 -0
- package/src/pty-deps.ts +241 -0
- package/src/pty-manager.ts +376 -0
- package/src/tools.ts +482 -0
- package/src/trust-fence.ts +84 -0
- package/src/ui-tweaks/archive.ts +293 -0
- package/src/ui-tweaks/config.ts +257 -0
- package/src/ui-tweaks/git-web.ts +421 -0
- package/src/ui-tweaks/git.ts +1524 -0
- package/src/ui-tweaks/index.ts +61 -0
- package/src/ui-tweaks/mcp.ts +535 -0
- package/src/ui-tweaks/turn-outcome.ts +105 -0
- package/src/ui-tweaks/web.ts +225 -0
- package/src/wire.ts +110 -0
- package/vendor/addon-fit.js +2 -0
- package/vendor/xterm.css +218 -0
- package/vendor/xterm.js +5 -0
|
@@ -0,0 +1,433 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typed fetch wrapper over the /sidebar JSON API. Every call posts to
|
|
3
|
+
* `/sidebar/api/<method>` with the sessionId and — when known — the session's
|
|
4
|
+
* cwd from the client's own list summary. The host prefers its attached
|
|
5
|
+
* session header and uses the summary cwd only while the session is still
|
|
6
|
+
* hydrating at page load (a detached session would otherwise fail the
|
|
7
|
+
* request). Failures surface as {@link SidebarApiError} with the wire code.
|
|
8
|
+
*/
|
|
9
|
+
import { encodeHtmlUrl } from '../html-route.ts'
|
|
10
|
+
import type { BrowserProbeResult } from './browser.ts'
|
|
11
|
+
|
|
12
|
+
/** One wire failure. */
|
|
13
|
+
export class SidebarApiError extends Error {
|
|
14
|
+
constructor(
|
|
15
|
+
readonly code: string,
|
|
16
|
+
message: string,
|
|
17
|
+
) {
|
|
18
|
+
super(message)
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Whether a wire failure is the workspace fence refusing a path outside the
|
|
24
|
+
* session workspace (the host message reads `path "..." is outside
|
|
25
|
+
* workspace`). The request-trust fence answers code `forbidden` with the
|
|
26
|
+
* bare message 'forbidden', so the message fragment — not the code alone —
|
|
27
|
+
* identifies this case.
|
|
28
|
+
*/
|
|
29
|
+
export function isOutsideWorkspaceError(error: unknown): boolean {
|
|
30
|
+
return error instanceof SidebarApiError && isOutsideWorkspaceMessage(error.message)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Message-level variant for surfaces that stored the raw text (file-tree level errors). */
|
|
34
|
+
export function isOutsideWorkspaceMessage(message: string): boolean {
|
|
35
|
+
return message.includes('outside workspace')
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Explorer row (host fs-tree shape). */
|
|
39
|
+
export interface FsEntry {
|
|
40
|
+
name: string
|
|
41
|
+
path: string
|
|
42
|
+
isDir: boolean
|
|
43
|
+
hidden: boolean
|
|
44
|
+
/** Whether the row is a symlink; `isDir` then describes the link's target. */
|
|
45
|
+
isSymlink: boolean
|
|
46
|
+
/** For symlinks: the target is missing or unreadable (stat failed). */
|
|
47
|
+
broken: boolean
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Git status entry (host git shape). */
|
|
51
|
+
export interface GitStatusEntry {
|
|
52
|
+
path: string
|
|
53
|
+
xy: string
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Git status snapshot. */
|
|
57
|
+
export interface GitStatusResult {
|
|
58
|
+
isRepo: boolean
|
|
59
|
+
branch?: string
|
|
60
|
+
entries: GitStatusEntry[]
|
|
61
|
+
/** True when the host capped `entries` (huge untracked set); the panel
|
|
62
|
+
* shows a truncation notice instead of freezing (#369). */
|
|
63
|
+
truncated?: boolean
|
|
64
|
+
root?: string
|
|
65
|
+
repositories?: string[]
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** One linked Git checkout. */
|
|
69
|
+
export interface GitWorktree {
|
|
70
|
+
path: string
|
|
71
|
+
branch: string
|
|
72
|
+
current: boolean
|
|
73
|
+
changes: number
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** One git log row. */
|
|
77
|
+
export interface GitLogEntry {
|
|
78
|
+
/** Short hash (7+ chars, display). */
|
|
79
|
+
hash: string
|
|
80
|
+
/** Full 40-char hash (advanced operations). */
|
|
81
|
+
hashFull: string
|
|
82
|
+
subject: string
|
|
83
|
+
author: string
|
|
84
|
+
/** ISO 8601 author date (`%ai`). */
|
|
85
|
+
date: string
|
|
86
|
+
/** Ref decorations (--decorate=short), e.g. `HEAD -> main, origin/main`; '' when none. */
|
|
87
|
+
refs: string
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Text read result. */
|
|
91
|
+
export interface FsTextResult { kind: 'text'; content: string; truncated: boolean }
|
|
92
|
+
/** Binary read result (no content; images load through the media route).
|
|
93
|
+
* `head` carries the first bytes (base64) for viewer detect sniffing. */
|
|
94
|
+
export interface FsBinaryResult { kind: 'binary'; size: number; truncated: boolean; head: string }
|
|
95
|
+
|
|
96
|
+
/** Terminal dependency status (mirror of the host's depsStatus; issue #140). */
|
|
97
|
+
export type TerminalDepsStatus =
|
|
98
|
+
| { ok: true }
|
|
99
|
+
| {
|
|
100
|
+
ok: false
|
|
101
|
+
/** The require-time error message (module missing, native binding broken…). */
|
|
102
|
+
cause: string
|
|
103
|
+
/** The pasteable repair command (terminal/cmd). */
|
|
104
|
+
command: string
|
|
105
|
+
/** The detected profile name (null when undetected → the command defaults to web). */
|
|
106
|
+
profile: string | null
|
|
107
|
+
/** Optional supplementary hint (fallback command only). */
|
|
108
|
+
note?: string
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Display-only CodingPlan model returned by the sidebar host. Provider
|
|
112
|
+
* endpoints, protocol details, and API keys intentionally do not exist in
|
|
113
|
+
* this browser-facing type. */
|
|
114
|
+
export interface CodingPlanModel {
|
|
115
|
+
logicalModelId: string
|
|
116
|
+
displayName: string
|
|
117
|
+
description?: string
|
|
118
|
+
accessType?: string
|
|
119
|
+
maxToken?: number
|
|
120
|
+
contextWindow?: number
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Cached CodingPlan catalogue state. */
|
|
124
|
+
export interface CodingPlanCatalogue {
|
|
125
|
+
status: 'loading' | 'ready' | 'unavailable'
|
|
126
|
+
models: CodingPlanModel[]
|
|
127
|
+
fetchedAt?: string
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Browser-safe ChatCode account state returned by the optional host plugin. */
|
|
131
|
+
export interface ChatCodeAuthState {
|
|
132
|
+
required?: boolean
|
|
133
|
+
validation?: 'valid' | 'invalid' | 'unavailable' | 'none'
|
|
134
|
+
login?: { sessionId: string; state: 'pending' | 'succeeded' | 'failed' | 'timed-out' | 'cancelled'; expiresAtMs: number }
|
|
135
|
+
available: boolean
|
|
136
|
+
configured?: boolean
|
|
137
|
+
expired?: boolean
|
|
138
|
+
userName?: string
|
|
139
|
+
emailAddress?: string
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** One day of personal token usage (the platform userTokenTrend row). */
|
|
143
|
+
export interface ChatCodeTokenDay {
|
|
144
|
+
statDate: string
|
|
145
|
+
tokensIn: number
|
|
146
|
+
tokensOut: number
|
|
147
|
+
totalTokens: number
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** One personal commit record (the platform gitCommitRecord row; numeric
|
|
151
|
+
* fields are coerced defensively at render time). */
|
|
152
|
+
export interface ChatCodeCommitRecord {
|
|
153
|
+
projectName: string
|
|
154
|
+
name: string
|
|
155
|
+
email: string
|
|
156
|
+
authorEmail: string
|
|
157
|
+
additions: number
|
|
158
|
+
deletions: number
|
|
159
|
+
total: number
|
|
160
|
+
aiTotal: number
|
|
161
|
+
orgname: string
|
|
162
|
+
deptname: string
|
|
163
|
+
title: string
|
|
164
|
+
shortId: string
|
|
165
|
+
authoredDate: string
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** Exact release note for the running package version. */
|
|
169
|
+
export interface ReleaseNoteResult {
|
|
170
|
+
registered: boolean
|
|
171
|
+
release: null | {
|
|
172
|
+
id: number
|
|
173
|
+
packageName: string
|
|
174
|
+
versionNum: string
|
|
175
|
+
title?: string
|
|
176
|
+
contentMd: string
|
|
177
|
+
updatedAt?: string
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** The package the "update" section's action upgrades. */
|
|
182
|
+
export const CHATCODE_CLI_PKG = '@chatcode/chatcode-cli'
|
|
183
|
+
|
|
184
|
+
/** One package's version-check result (mirror of the host chatcode-update). */
|
|
185
|
+
export interface ChatCodeUpdateCheck {
|
|
186
|
+
packageName: string
|
|
187
|
+
current: string | null
|
|
188
|
+
latest: string | null
|
|
189
|
+
needsUpdate: boolean
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** One package's perform-update result (mirror of the host chatcode-update). */
|
|
193
|
+
export interface ChatCodeUpdateResult {
|
|
194
|
+
packageName: string
|
|
195
|
+
from: string | null
|
|
196
|
+
to: string | null
|
|
197
|
+
ok: boolean
|
|
198
|
+
message: string
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
async function call<T>(method: string, payload: Record<string, unknown>, signal?: AbortSignal): Promise<T> {
|
|
202
|
+
let response: Response
|
|
203
|
+
try {
|
|
204
|
+
response = await fetch(`/sidebar/api/${method}`, {
|
|
205
|
+
method: 'POST',
|
|
206
|
+
headers: { 'content-type': 'application/json' },
|
|
207
|
+
body: JSON.stringify(payload),
|
|
208
|
+
signal,
|
|
209
|
+
})
|
|
210
|
+
} catch (error) {
|
|
211
|
+
throw new SidebarApiError('network', error instanceof Error ? error.message : String(error))
|
|
212
|
+
}
|
|
213
|
+
const parsed: { ok?: boolean; value?: unknown; error?: { code?: string; message?: string } } | null
|
|
214
|
+
= await response.json().catch(() => null)
|
|
215
|
+
if (!response.ok || parsed === null || parsed.ok !== true || parsed.value === undefined) {
|
|
216
|
+
throw new SidebarApiError(
|
|
217
|
+
parsed?.error?.code ?? 'http',
|
|
218
|
+
parsed?.error?.message ?? `HTTP ${response.status}`,
|
|
219
|
+
)
|
|
220
|
+
}
|
|
221
|
+
return parsed.value as T
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Upload one file to the sidebar's raw upload route: the File goes straight
|
|
226
|
+
* into the POST body (no JSON/base64 re-encoding — the host streams it into
|
|
227
|
+
* the workspace). Failure surfaces as {@link SidebarApiError} with the wire
|
|
228
|
+
* code, exactly like every `/sidebar/api` call. An aborted `signal` rejects
|
|
229
|
+
* with the DOMException as-is (the caller decides whether that is an error).
|
|
230
|
+
*/
|
|
231
|
+
async function fetchUpload<T>(
|
|
232
|
+
scope: SessionScope,
|
|
233
|
+
dir: string,
|
|
234
|
+
relativePath: string,
|
|
235
|
+
body: Blob,
|
|
236
|
+
signal?: AbortSignal,
|
|
237
|
+
): Promise<T> {
|
|
238
|
+
const params = new URLSearchParams({ sessionId: scope.sessionId, dir, relativePath })
|
|
239
|
+
if (scope.cwd !== undefined && scope.cwd !== '') params.set('cwd', scope.cwd)
|
|
240
|
+
let response: Response
|
|
241
|
+
try {
|
|
242
|
+
response = await fetch(`/sidebar/upload?${params.toString()}`, {
|
|
243
|
+
method: 'POST',
|
|
244
|
+
headers: { 'content-type': 'application/octet-stream' },
|
|
245
|
+
body,
|
|
246
|
+
signal,
|
|
247
|
+
})
|
|
248
|
+
} catch (error) {
|
|
249
|
+
if (error instanceof DOMException && error.name === 'AbortError') throw error
|
|
250
|
+
throw new SidebarApiError('network', error instanceof Error ? error.message : String(error))
|
|
251
|
+
}
|
|
252
|
+
const parsed: { ok?: boolean; value?: unknown; error?: { code?: string; message?: string } } | null
|
|
253
|
+
= await response.json().catch(() => null)
|
|
254
|
+
if (!response.ok || parsed === null || parsed.ok !== true || parsed.value === undefined) {
|
|
255
|
+
throw new SidebarApiError(
|
|
256
|
+
parsed?.error?.code ?? 'http',
|
|
257
|
+
parsed?.error?.message ?? `HTTP ${response.status}`,
|
|
258
|
+
)
|
|
259
|
+
}
|
|
260
|
+
return parsed.value as T
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/** One request's session scope: the conversation id plus its cwd when known. */
|
|
264
|
+
export interface SessionScope {
|
|
265
|
+
sessionId: string
|
|
266
|
+
/** The session's working directory from the client list summary (optional). */
|
|
267
|
+
cwd?: string
|
|
268
|
+
/** Selected Git repository when cwd is a workspace container. */
|
|
269
|
+
repoRoot?: string
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/** Fold a scope into a JSON payload ({cwd} only when present). */
|
|
273
|
+
function scopePayload(scope: SessionScope, extra: Record<string, unknown>): Record<string, unknown> {
|
|
274
|
+
return {
|
|
275
|
+
sessionId: scope.sessionId,
|
|
276
|
+
...(scope.cwd !== undefined && scope.cwd !== '' ? { cwd: scope.cwd } : {}),
|
|
277
|
+
...(scope.repoRoot !== undefined && scope.repoRoot !== '' ? { repoRoot: scope.repoRoot } : {}),
|
|
278
|
+
...extra,
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/** Add a linked-worktree selection to a scoped Git request. The host validates
|
|
283
|
+
* membership before using it as a command cwd. */
|
|
284
|
+
function gitPayload(scope: SessionScope, worktree: string | undefined, extra: Record<string, unknown>): Record<string, unknown> {
|
|
285
|
+
return scopePayload(scope, { ...(worktree !== undefined && worktree !== '' ? { worktree } : {}), ...extra })
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/** The sidebar API surface (session scope threaded through every call). */
|
|
289
|
+
export const api = {
|
|
290
|
+
chatcodeStatus: (signal?: AbortSignal) => call<ChatCodeAuthState>('chatcode.status', {}, signal),
|
|
291
|
+
chatcodeStart: () => call<{ sessionId: string; url: string }>('chatcode.start', {}),
|
|
292
|
+
chatcodeCancel: (sessionId: string) => call<{ ok: true }>('chatcode.cancel', { sessionId }),
|
|
293
|
+
chatcodeLogout: () => call<{ ok: true }>('chatcode.logout', {}),
|
|
294
|
+
/** Check both ChatCode packages (web + CLI) against the npm registry. */
|
|
295
|
+
chatcodeUpdateCheck: (signal?: AbortSignal) =>
|
|
296
|
+
call<{ packages: ChatCodeUpdateCheck[] }>('chatcode.update.check', {}, signal),
|
|
297
|
+
/** Upgrade both ChatCode packages to their npm-latest versions. */
|
|
298
|
+
chatcodeUpdatePerform: (signal?: AbortSignal) =>
|
|
299
|
+
call<{ results: ChatCodeUpdateResult[] }>('chatcode.update.perform', {}, signal),
|
|
300
|
+
releaseNoteCurrent: (packageName: string, versionNum: string, signal?: AbortSignal) =>
|
|
301
|
+
call<ReleaseNoteResult>('release-note.current', { packageName, versionNum }, signal),
|
|
302
|
+
/** Per-day token usage of one day range (`YYYY-MM-DD`). */
|
|
303
|
+
chatcodeTokenTrend: (begin: string, end: string, signal?: AbortSignal) =>
|
|
304
|
+
call<ChatCodeTokenDay[]>('chatcode.token-trend', { begin, end }, signal),
|
|
305
|
+
/** Personal commit records of one day range (AI-ratio view). */
|
|
306
|
+
chatcodeGitCommitStats: (begin: string, end: string, signal?: AbortSignal) =>
|
|
307
|
+
call<ChatCodeCommitRecord[]>('chatcode.git-commit-stats', { begin, end }, signal),
|
|
308
|
+
sessionCwd: (scope: SessionScope, signal?: AbortSignal) =>
|
|
309
|
+
call<{ sessionId: string; cwd: string; root: string; parent: string | null }>('session.cwd', scopePayload(scope, {}), signal),
|
|
310
|
+
fsTree: (scope: SessionScope, path: string, signal?: AbortSignal) =>
|
|
311
|
+
call<{ path: string; entries: FsEntry[]; truncated: boolean }>('fs.tree', scopePayload(scope, { path }), signal),
|
|
312
|
+
/** Global recursive file-name search rooted at the session cwd (the editor
|
|
313
|
+
* side panel's search box); matches are cwd-relative '/'-separated paths. */
|
|
314
|
+
fsSearch: (scope: SessionScope, query: string, signal?: AbortSignal) =>
|
|
315
|
+
call<{ matches: string[]; truncated: boolean }>('fs.search', scopePayload(scope, { query }), signal),
|
|
316
|
+
fsRead: (scope: SessionScope, path: string, signal?: AbortSignal) =>
|
|
317
|
+
call<FsTextResult | FsBinaryResult>('fs.read', scopePayload(scope, { path }), signal),
|
|
318
|
+
fsWrite: (scope: SessionScope, path: string, content: string) =>
|
|
319
|
+
call<{ ok: true }>('fs.write', scopePayload(scope, { path, content })),
|
|
320
|
+
/** Upload one file's raw bytes into `dir` (keeps the folder tree via
|
|
321
|
+
* `relativePath`); the host streams it under the session workspace. */
|
|
322
|
+
uploadFile: (scope: SessionScope, dir: string, relativePath: string, body: Blob, signal?: AbortSignal) =>
|
|
323
|
+
fetchUpload<{ path: string; size: number }>(scope, dir, relativePath, body, signal),
|
|
324
|
+
gitWorktrees: (scope: SessionScope, signal?: AbortSignal) =>
|
|
325
|
+
call<GitWorktree[]>('git.worktrees', scopePayload(scope, {}), signal),
|
|
326
|
+
gitStatus: (scope: SessionScope, worktree?: string, signal?: AbortSignal) =>
|
|
327
|
+
call<GitStatusResult>('git.status', gitPayload(scope, worktree, {}), signal),
|
|
328
|
+
gitDiff: (scope: SessionScope, path: string | undefined, staged: boolean, worktree?: string, signal?: AbortSignal) =>
|
|
329
|
+
call<{ diff: string }>('git.diff', gitPayload(scope, worktree, { ...(path !== undefined ? { path } : {}), staged }), signal),
|
|
330
|
+
gitStage: (scope: SessionScope, path?: string, worktree?: string) =>
|
|
331
|
+
call<{ ok: true }>('git.stage', gitPayload(scope, worktree, { ...(path !== undefined ? { path } : {}) })),
|
|
332
|
+
gitUnstage: (scope: SessionScope, path?: string, worktree?: string) =>
|
|
333
|
+
call<{ ok: true }>('git.unstage', gitPayload(scope, worktree, { ...(path !== undefined ? { path } : {}) })),
|
|
334
|
+
gitCommit: (scope: SessionScope, message: string, worktree?: string) =>
|
|
335
|
+
call<{ ok: true }>('git.commit', gitPayload(scope, worktree, { message })),
|
|
336
|
+
gitBranch: (scope: SessionScope, worktree?: string, signal?: AbortSignal) =>
|
|
337
|
+
call<{ current: string; names: string[] }>('git.branch', gitPayload(scope, worktree, {}), signal),
|
|
338
|
+
gitCheckout: (scope: SessionScope, branch: string, worktree?: string) =>
|
|
339
|
+
call<{ ok: true }>('git.checkout', gitPayload(scope, worktree, { branch })),
|
|
340
|
+
/** Recent commit history, lazily pageable (skip/count; defaults 0/30). */
|
|
341
|
+
gitLog: (scope: SessionScope, count?: number, skip?: number, worktree?: string, signal?: AbortSignal) =>
|
|
342
|
+
call<GitLogEntry[]>('git.log', gitPayload(scope, worktree, {
|
|
343
|
+
...(count !== undefined ? { count } : {}),
|
|
344
|
+
...(skip !== undefined ? { skip } : {}),
|
|
345
|
+
}), signal),
|
|
346
|
+
/** Full patch text of one commit (diff display for the history rows). */
|
|
347
|
+
gitCommitDiff: (scope: SessionScope, hash: string, worktree?: string, signal?: AbortSignal) =>
|
|
348
|
+
call<{ diff: string }>('git.commit-diff', gitPayload(scope, worktree, { hash }), signal),
|
|
349
|
+
/** Discard the worktree changes of one file (the index is untouched). */
|
|
350
|
+
gitDiscard: (scope: SessionScope, path: string, worktree?: string) =>
|
|
351
|
+
call<{ ok: true }>('git.discard', gitPayload(scope, worktree, { path })),
|
|
352
|
+
/** Revert one commit onto the current branch. */
|
|
353
|
+
gitRevert: (scope: SessionScope, hash: string, worktree?: string) =>
|
|
354
|
+
call<{ ok: true }>('git.revert', gitPayload(scope, worktree, { hash })),
|
|
355
|
+
/** Cherry-pick one commit onto the current branch. */
|
|
356
|
+
gitCherryPick: (scope: SessionScope, hash: string, worktree?: string) =>
|
|
357
|
+
call<{ ok: true }>('git.cherry-pick', gitPayload(scope, worktree, { hash })),
|
|
358
|
+
/** Release a terminal's process immediately (tab closed; the WS close frame
|
|
359
|
+
* may be unreachable while the socket is down, so the host also accepts
|
|
360
|
+
* this explicit route). */
|
|
361
|
+
ptyClose: (scope: SessionScope, tab: string) =>
|
|
362
|
+
call<{ ok: true }>('pty.close', scopePayload(scope, { tab })),
|
|
363
|
+
/** Release an agent terminal by uuid (tab closed while WS was down). */
|
|
364
|
+
agentPtyClose: (uuid: string) =>
|
|
365
|
+
call<{ ok: true }>('agent-pty.close', { uuid }),
|
|
366
|
+
/** Terminal dependency status (issue #140): after a WS close 1011 with
|
|
367
|
+
* reason `pty-deps-missing` the view fetches the full repair details here
|
|
368
|
+
* (the close reason itself is capped at 123 bytes). */
|
|
369
|
+
terminalDeps: () =>
|
|
370
|
+
call<TerminalDepsStatus>('terminal.deps', {}),
|
|
371
|
+
/** The effective terminal shell and its display name (plugin-global). */
|
|
372
|
+
shellGet: () =>
|
|
373
|
+
call<{ shell: string; name: string }>('shell.get', {}),
|
|
374
|
+
/** The local display name used by the bottom-left account menu. */
|
|
375
|
+
accountGet: () =>
|
|
376
|
+
call<{ userName: string }>('account.get', {}),
|
|
377
|
+
/** Read the host-cached CodingPlan model catalogue. */
|
|
378
|
+
codingPlanModels: () =>
|
|
379
|
+
call<CodingPlanCatalogue>('codingplan.models', {}),
|
|
380
|
+
/** Refresh the host-cached CodingPlan catalogue. */
|
|
381
|
+
codingPlanRefresh: () =>
|
|
382
|
+
call<CodingPlanCatalogue>('codingplan.refresh', {}),
|
|
383
|
+
/** Read the side card preferences (plugin-global, no session scope). */
|
|
384
|
+
settingsGet: () =>
|
|
385
|
+
call<{ value?: unknown; revision?: number; externalDisable?: boolean }>('settings.get', {}),
|
|
386
|
+
/** Merge a patch into the side card preferences (revision-guarded). */
|
|
387
|
+
settingsUpdate: (patch: Record<string, unknown>, expectedRevision?: number) =>
|
|
388
|
+
call<{ value?: unknown; revision?: number }>('settings.update', {
|
|
389
|
+
patch,
|
|
390
|
+
...(expectedRevision !== undefined ? { expectedRevision } : {}),
|
|
391
|
+
}),
|
|
392
|
+
/** Probe a URL's response headers (the sidebar browser's embeddability
|
|
393
|
+
* check; see the host's browser.probe route). */
|
|
394
|
+
browserProbe: (url: string, signal?: AbortSignal) =>
|
|
395
|
+
call<BrowserProbeResult>('browser.probe', { url }, signal),
|
|
396
|
+
/** External open for the file tree's "open with" menu: reveal a path in
|
|
397
|
+
* the OS file manager, or hand a custom-scheme URL (vscode://, cursor://,
|
|
398
|
+
* zed://, custom editors) to its registered handler. The host launches
|
|
399
|
+
* the platform opener (argv, no shell). */
|
|
400
|
+
openExternal: (payload: { action: 'reveal'; path: string } | { action: 'url'; url: string }) =>
|
|
401
|
+
call<{ started: boolean }>('open.external', payload),
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/** Absolute URL of the media route for one path (images only). */
|
|
405
|
+
export function mediaUrl(scope: SessionScope, path: string): string {
|
|
406
|
+
return fileUrl(scope, path, false)
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/** Absolute URL of the download route: serves raw bytes (binary-safe) with
|
|
410
|
+
* `Content-Disposition: attachment`, so the browser saves the file. */
|
|
411
|
+
export function downloadUrl(scope: SessionScope, path: string): string {
|
|
412
|
+
return fileUrl(scope, path, true)
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/** Shared URL builder for the /sidebar/file route (media vs download). */
|
|
416
|
+
function fileUrl(scope: SessionScope, path: string, download: boolean): string {
|
|
417
|
+
const params = new URLSearchParams({ sessionId: scope.sessionId, path })
|
|
418
|
+
if (scope.cwd !== undefined && scope.cwd !== '') params.set('cwd', scope.cwd)
|
|
419
|
+
if (download) params.set('download', '1')
|
|
420
|
+
return `/sidebar/file?${params.toString()}`
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/**
|
|
424
|
+
* Absolute URL of the HTML preview route (see html-route.ts): the path is
|
|
425
|
+
* fully encoded so the previewed page's relative assets resolve back into
|
|
426
|
+
* the same route with the session scope intact. The UNC marker is
|
|
427
|
+
* platform-neutral — the host's requireAbsolute resolves the decoded
|
|
428
|
+
* forward-slash `//server/share/...` form on both win32 and POSIX — so no
|
|
429
|
+
* client-side platform signal is needed.
|
|
430
|
+
*/
|
|
431
|
+
export function htmlUrl(scope: SessionScope, path: string): string {
|
|
432
|
+
return encodeHtmlUrl(scope.sessionId, path)
|
|
433
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The "no preview — download instead" pane: shown by the editor host when no
|
|
3
|
+
* viewer can render a file (binary without a registered renderer, or the
|
|
4
|
+
* `binary-download` strategy) and registered as the `binary-download` viewer
|
|
5
|
+
* component so the declarative route and the host fallback share one UI.
|
|
6
|
+
*/
|
|
7
|
+
import type { ReactNode } from 'react'
|
|
8
|
+
import type { SessionScope } from './api.ts'
|
|
9
|
+
import { downloadUrl } from './api.ts'
|
|
10
|
+
import { t } from './locales.ts'
|
|
11
|
+
import css from './sidebar.module.css'
|
|
12
|
+
|
|
13
|
+
export function BinaryDownload(props: { scope: SessionScope; path: string }): ReactNode {
|
|
14
|
+
const { scope, path } = props
|
|
15
|
+
return (
|
|
16
|
+
<div className={css.editorBinary}>
|
|
17
|
+
<span className={css.editorBinaryNotice}>{t('binaryNoPreview')}</span>
|
|
18
|
+
<a className={css.editorDownloadLink} href={downloadUrl(scope, path)} download>
|
|
19
|
+
{t('downloadToView')}
|
|
20
|
+
</a>
|
|
21
|
+
</div>
|
|
22
|
+
)
|
|
23
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Narrow-viewport ("mobile") breakpoint for the sidebar. Width-based, shared
|
|
3
|
+
* by the layout logic (JS) and the style gates (CSS). The CSS side pairs
|
|
4
|
+
* with this file via `@media (max-width: 767px)` rules (sidebar.module.css)
|
|
5
|
+
* — 767px ≡ widths below NARROW_MAX_WIDTH, documented at both ends.
|
|
6
|
+
*
|
|
7
|
+
* "Real narrow" on purpose: the mobile layout (one full-screen drawer, the
|
|
8
|
+
* bottom panel's tabs merged into the right sidebar) is a phone / portrait
|
|
9
|
+
* tablet experience. The value is deliberately NOT aligned to the DSH app
|
|
10
|
+
* shell's own 1024px breakpoint — 1024px windows (small laptops, split
|
|
11
|
+
* panes) keep the desktop two-panel layout.
|
|
12
|
+
*/
|
|
13
|
+
import { useEffect, useState } from 'react'
|
|
14
|
+
|
|
15
|
+
/** Viewport widths strictly below this are "mobile" (paired CSS: max-width: 767px). */
|
|
16
|
+
export const NARROW_MAX_WIDTH = 768
|
|
17
|
+
|
|
18
|
+
/** Whether a viewport width is narrow (mobile). */
|
|
19
|
+
export function isNarrowWidth(width: number): boolean {
|
|
20
|
+
return width < NARROW_MAX_WIDTH
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface ViewportSize {
|
|
24
|
+
width: number
|
|
25
|
+
height: number
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Live narrow-viewport flag for components. Reads `window.innerWidth` and
|
|
30
|
+
* re-measures on resize (rAF-throttled, the repo's existing drag pattern).
|
|
31
|
+
* Deliberately avoids `matchMedia` (jsdom does not implement it) — the
|
|
32
|
+
* resize listener is equally exact for a breakpoint that never changes
|
|
33
|
+
* while the page is open.
|
|
34
|
+
*/
|
|
35
|
+
export function useViewportSize(): ViewportSize {
|
|
36
|
+
const [size, setSize] = useState<ViewportSize>(() => ({
|
|
37
|
+
width: typeof window === 'undefined' ? 0 : window.innerWidth,
|
|
38
|
+
height: typeof window === 'undefined' ? 0 : window.innerHeight,
|
|
39
|
+
}))
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
if (typeof window === 'undefined') return
|
|
42
|
+
let frame: number | null = null
|
|
43
|
+
const measure = (): void => {
|
|
44
|
+
frame = null
|
|
45
|
+
setSize({ width: window.innerWidth, height: window.innerHeight })
|
|
46
|
+
}
|
|
47
|
+
const onResize = (): void => {
|
|
48
|
+
if (frame === null) frame = requestAnimationFrame(measure)
|
|
49
|
+
}
|
|
50
|
+
window.addEventListener('resize', onResize)
|
|
51
|
+
return () => {
|
|
52
|
+
window.removeEventListener('resize', onResize)
|
|
53
|
+
if (frame !== null) cancelAnimationFrame(frame)
|
|
54
|
+
}
|
|
55
|
+
}, [])
|
|
56
|
+
return size
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function useNarrowViewport(): boolean {
|
|
60
|
+
return isNarrowWidth(useViewportSize().width)
|
|
61
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure URL policy for the built-in browser tab: normalize user input into
|
|
3
|
+
* an http(s) URL, and refuse destinations that would be dangerous to embed
|
|
4
|
+
* in the sidebar iframe. Kept dependency-free so it is unit-testable.
|
|
5
|
+
*
|
|
6
|
+
* The iframe sandbox (opaque origin, no allow-same-origin / top-navigation)
|
|
7
|
+
* is the primary security boundary; this module is the address-bar gate on
|
|
8
|
+
* top of it: only http/https may be navigated, and loopback addresses are
|
|
9
|
+
* refused so a browsed page cannot probe local services by user action.
|
|
10
|
+
* The GUI's OWN origin is explicitly ALLOWED — the user may open the GUI
|
|
11
|
+
* itself in the sidebar (debugging, mirroring); the sandbox still renders
|
|
12
|
+
* it in an opaque origin with no same-origin privileges, exactly like any
|
|
13
|
+
* other site.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/** Why a navigation attempt was refused. */
|
|
17
|
+
export type BrowserBlockReason = 'scheme' | 'loopback'
|
|
18
|
+
|
|
19
|
+
/** Result of normalizing one address-bar input. */
|
|
20
|
+
export type BrowserNavigateResult =
|
|
21
|
+
| { kind: 'ok'; url: string }
|
|
22
|
+
| { kind: 'blocked'; reason: BrowserBlockReason }
|
|
23
|
+
| { kind: 'invalid' }
|
|
24
|
+
|
|
25
|
+
/** One browser.probe wire result (host fetch of the target's headers). */
|
|
26
|
+
export interface BrowserProbeResult {
|
|
27
|
+
reachable: boolean
|
|
28
|
+
/** The final (post-redirect) URL; present when reachable. */
|
|
29
|
+
url?: string
|
|
30
|
+
status?: number
|
|
31
|
+
xFrameOptions?: string
|
|
32
|
+
/** The CSP frame-ancestors source list; present when the directive exists. */
|
|
33
|
+
frameAncestors?: string[]
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Embeddability verdict of one probe. */
|
|
37
|
+
export type Embeddability = 'embeddable' | 'blocked' | 'unknown'
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Decide whether a site can render inside the sidebar iframe. The signals
|
|
41
|
+
* are exactly the ones the BROWSER enforces when it refuses an iframe load:
|
|
42
|
+
* X-Frame-Options DENY/SAMEORIGIN, or a frame-ancestors directive that does
|
|
43
|
+
* not allow `*` ('self' here means the SITE's own origin — never ours, so
|
|
44
|
+
* it also blocks the sidebar). A site we could not reach yields 'unknown'
|
|
45
|
+
* and the plain iframe stays.
|
|
46
|
+
*/
|
|
47
|
+
export function embeddabilityOf(probe: BrowserProbeResult): Embeddability {
|
|
48
|
+
if (probe.reachable !== true) return 'unknown'
|
|
49
|
+
const xfo = probe.xFrameOptions?.trim().toUpperCase()
|
|
50
|
+
if (xfo === 'DENY' || xfo === 'SAMEORIGIN') return 'blocked'
|
|
51
|
+
if (probe.frameAncestors !== undefined && !probe.frameAncestors.some(source => source === '*')) return 'blocked'
|
|
52
|
+
return 'embeddable'
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** A loopback hostname (localhost, IPv6 ::1, 127.0.0.0/8, 0.0.0.0). */
|
|
56
|
+
export function isLoopbackHostname(hostname: string): boolean {
|
|
57
|
+
const host = hostname.replace(/^\[|\]$/g, '').toLowerCase()
|
|
58
|
+
if (host === 'localhost' || host === '::1' || host === '0.0.0.0') return true
|
|
59
|
+
const parts = host.split('.')
|
|
60
|
+
return parts.length === 4
|
|
61
|
+
&& parts[0] === '127'
|
|
62
|
+
&& parts.every(part => /^\d{1,3}$/.test(part) && Number(part) <= 255)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Normalize one address-bar input against the navigation policy.
|
|
67
|
+
* @param input - raw user text.
|
|
68
|
+
* @param selfOrigin - the GUI's own origin (window.location.origin). The GUI
|
|
69
|
+
* itself may be browsed in the sidebar (the sandbox keeps it opaque), so it
|
|
70
|
+
* is let through BEFORE the loopback check — its host is normally loopback.
|
|
71
|
+
* @param allowedLoopback - comma-separated loopback allowlist from the side
|
|
72
|
+
* card prefs (`browserAllowedLoopback`): bare hosts (`localhost`,
|
|
73
|
+
* `127.0.0.1`) allow every port, `host:port` entries allow exactly that
|
|
74
|
+
* authority. Entries are matched case-insensitively; portless entries match
|
|
75
|
+
* the host on any port. Empty allowlist keeps the default loopback block.
|
|
76
|
+
*/
|
|
77
|
+
/** Schemes that must never reach the iframe, even without `//` (javascript:,
|
|
78
|
+
* data:, file:, ...). Host:port lookalikes (example.com:8080) are NOT here —
|
|
79
|
+
* they parse as hosts below. */
|
|
80
|
+
const FORBIDDEN_SCHEMES = new Set([
|
|
81
|
+
'javascript', 'data', 'file', 'about', 'vbscript', 'blob',
|
|
82
|
+
'mailto', 'tel', 'ftp', 'ftps', 'ws', 'wss', 'sftp', 'ssh',
|
|
83
|
+
'chrome', 'chrome-extension', 'moz-extension', 'edge', 'opera', 'resource', 'view-source',
|
|
84
|
+
])
|
|
85
|
+
|
|
86
|
+
/** Parse the loopback allowlist into a matcher predicate over host:port. */
|
|
87
|
+
export function parseLoopbackAllowlist(allowlist: string): (host: string, port: string) => boolean {
|
|
88
|
+
const entries = allowlist.split(',').map(entry => entry.trim().toLowerCase()).filter(entry => entry !== '')
|
|
89
|
+
const exact = new Set(entries)
|
|
90
|
+
const hosts = new Set<string>()
|
|
91
|
+
for (const entry of entries) {
|
|
92
|
+
if (!entry.includes(':')) hosts.add(entry.replace(/^\[|\]$/g, ''))
|
|
93
|
+
}
|
|
94
|
+
return (host, port) => {
|
|
95
|
+
const key = `${host}:${port}`
|
|
96
|
+
if (exact.has(key) || exact.has(host)) return true
|
|
97
|
+
return port !== '' && hosts.has(host)
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Whether a loopback URL is explicitly allowlisted by the side card prefs
|
|
103
|
+
* (`browserAllowedLoopback`). Only allowlisted local addresses may run with
|
|
104
|
+
* `allow-same-origin` in the sidebar iframe — needed for local dev servers
|
|
105
|
+
* (Vite etc.) whose module/HMR/fetch pipeline requires a real origin, while
|
|
106
|
+
* the page stays cross-origin to the GUI and to every other site.
|
|
107
|
+
*/
|
|
108
|
+
export function isAllowedLoopbackUrl(url: string, allowlist: string): boolean {
|
|
109
|
+
if (allowlist.trim() === '') return false
|
|
110
|
+
let parsed: URL
|
|
111
|
+
try {
|
|
112
|
+
parsed = new URL(url)
|
|
113
|
+
} catch {
|
|
114
|
+
return false
|
|
115
|
+
}
|
|
116
|
+
if (!isLoopbackHostname(parsed.hostname)) return false
|
|
117
|
+
return parseLoopbackAllowlist(allowlist)(parsed.hostname, parsed.port)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function normalizeBrowserUrl(input: string, selfOrigin: string, allowedLoopback = ''): BrowserNavigateResult {
|
|
121
|
+
const trimmed = input.trim()
|
|
122
|
+
if (trimmed === '') return { kind: 'invalid' }
|
|
123
|
+
// Distinguish an explicit scheme from a bare host:port. "example.com:8080"
|
|
124
|
+
// would match a naive scheme regex (dots are legal in schemes), so a
|
|
125
|
+
// scheme prefix is only honored when it is http(s) or a known-forbidden
|
|
126
|
+
// scheme; anything else is treated as a host and gets https://.
|
|
127
|
+
const schemeMatch = /^([a-zA-Z][a-zA-Z0-9+.-]*):/.exec(trimmed)
|
|
128
|
+
let withScheme: string
|
|
129
|
+
if (schemeMatch === null) {
|
|
130
|
+
withScheme = `https://${trimmed}`
|
|
131
|
+
} else {
|
|
132
|
+
const scheme = schemeMatch[1]!.toLowerCase()
|
|
133
|
+
if (scheme === 'http' || scheme === 'https') withScheme = trimmed
|
|
134
|
+
else if (FORBIDDEN_SCHEMES.has(scheme)) return { kind: 'blocked', reason: 'scheme' }
|
|
135
|
+
else withScheme = `https://${trimmed}`
|
|
136
|
+
}
|
|
137
|
+
let url: URL
|
|
138
|
+
try {
|
|
139
|
+
url = new URL(withScheme)
|
|
140
|
+
} catch {
|
|
141
|
+
return { kind: 'invalid' }
|
|
142
|
+
}
|
|
143
|
+
// The protocol backstop: any URL that still parses to a non-http(s)
|
|
144
|
+
// scheme (e.g. ftp://, ws:// — which carry `//` and skip the list) is
|
|
145
|
+
// refused here.
|
|
146
|
+
if (url.protocol !== 'http:' && url.protocol !== 'https:') return { kind: 'blocked', reason: 'scheme' }
|
|
147
|
+
// The GUI's own origin is ALLOWED (the user may browse the GUI itself in
|
|
148
|
+
// the sidebar; the sandbox renders it in an opaque origin like any other
|
|
149
|
+
// site). It must be checked before the loopback gate because its host is
|
|
150
|
+
// normally loopback.
|
|
151
|
+
try {
|
|
152
|
+
if (url.origin === new URL(selfOrigin).origin) return { kind: 'ok', url: url.href }
|
|
153
|
+
} catch {
|
|
154
|
+
// Unparsable selfOrigin (never in practice): fall through to the loopback gate.
|
|
155
|
+
}
|
|
156
|
+
if (isLoopbackHostname(url.hostname)) {
|
|
157
|
+
// An explicit user allowlist (browserAllowedLoopback) can lift the
|
|
158
|
+
// loopback block for trusted local dev servers. The sandbox still
|
|
159
|
+
// renders them in an opaque origin — no GUI access, exactly like any
|
|
160
|
+
// other browsed site.
|
|
161
|
+
if (allowedLoopback.trim() !== '' && parseLoopbackAllowlist(allowedLoopback)(url.hostname, url.port)) {
|
|
162
|
+
return { kind: 'ok', url: url.href }
|
|
163
|
+
}
|
|
164
|
+
return { kind: 'blocked', reason: 'loopback' }
|
|
165
|
+
}
|
|
166
|
+
return { kind: 'ok', url: url.href }
|
|
167
|
+
}
|