@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,421 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-ui-tweaks — Git Web routes.
|
|
3
|
+
*
|
|
4
|
+
* Same-origin JSON endpoints backing the GitBar browser half. Every mutating
|
|
5
|
+
* action is a POST guarded by the same origin/same-site checks as the Settings
|
|
6
|
+
* route; the session id arrives as a query/body field and the backend resolves
|
|
7
|
+
* the working directory from the session header.
|
|
8
|
+
*
|
|
9
|
+
* The terminal panel rides two extra surfaces on the same prefix:
|
|
10
|
+
* - `GET /vendor/<file>` serves the vendored xterm.js UMD builds the browser
|
|
11
|
+
* half lazy-loads on first panel open (whitelist only — no path traversal).
|
|
12
|
+
* - `WS /terminal-ws` upgrades into the persistent PTY bridge: server→client
|
|
13
|
+
* frames are RAW pty output (plus one JSON `{"type":"exit"}` notice),
|
|
14
|
+
* client→server frames are JSON (`input` / `resize` / `close`) so a pasted
|
|
15
|
+
* brace can never be mistaken for a control frame.
|
|
16
|
+
* @module dsh-ui-tweaks/git-web
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import type { IncomingMessage, ServerResponse } from 'node:http'
|
|
20
|
+
import { readFileSync } from 'node:fs'
|
|
21
|
+
import type { Duplex } from 'node:stream'
|
|
22
|
+
import type { Context } from '@deepseek-ai/cordis'
|
|
23
|
+
// Type-only import activates the optional webServer Context declaration.
|
|
24
|
+
import type {} from '@deepseek-ai/dsh-host-webserver'
|
|
25
|
+
import { WebSocketServer, WebSocket } from 'ws'
|
|
26
|
+
import { sameOriginPost } from './web.ts'
|
|
27
|
+
import { GitBackend, type TerminalClient } from './git.ts'
|
|
28
|
+
|
|
29
|
+
/** Route prefix shared with the browser half. */
|
|
30
|
+
export const GIT_ROUTE = '/_dsh/ui-tweaks/git'
|
|
31
|
+
|
|
32
|
+
/** Exact path of the terminal WebSocket upgrade route. */
|
|
33
|
+
export const TERMINAL_WS_PATH = `${GIT_ROUTE}/terminal-ws`
|
|
34
|
+
|
|
35
|
+
const MAX_BODY_BYTES = 64 * 1024
|
|
36
|
+
|
|
37
|
+
/** Vendored browser assets served from the plugin's `vendor/` directory. */
|
|
38
|
+
const VENDOR_FILES: Record<string, { type: string }> = {
|
|
39
|
+
'xterm.js': { type: 'text/javascript; charset=utf-8' },
|
|
40
|
+
'addon-fit.js': { type: 'text/javascript; charset=utf-8' },
|
|
41
|
+
'xterm.css': { type: 'text/css; charset=utf-8' },
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function json<T>(res: ServerResponse, status: number, body: { ok: true; value: T } | { ok: false; error: { code: string; message: string } }): void {
|
|
45
|
+
const bytes = Buffer.from(JSON.stringify(body))
|
|
46
|
+
res.setHeader('Content-Type', 'application/json; charset=utf-8')
|
|
47
|
+
res.setHeader('Content-Length', String(bytes.length))
|
|
48
|
+
res.setHeader('Cache-Control', 'no-store')
|
|
49
|
+
res.setHeader('X-Content-Type-Options', 'nosniff')
|
|
50
|
+
res.writeHead(status)
|
|
51
|
+
res.end(bytes)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function ok<T>(res: ServerResponse, value: T): void {
|
|
55
|
+
json(res, 200, { ok: true, value })
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function fail(res: ServerResponse, status: number, code: string, message: string): void {
|
|
59
|
+
json(res, status, { ok: false, error: { code, message } })
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function messageOf(error: unknown): string {
|
|
63
|
+
if (error instanceof Error) return error.message
|
|
64
|
+
return String(error)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Read a bounded JSON POST body. */
|
|
68
|
+
async function readJson(req: IncomingMessage): Promise<unknown> {
|
|
69
|
+
const contentType = req.headers['content-type']?.split(';', 1)[0]?.trim().toLowerCase()
|
|
70
|
+
if (contentType !== 'application/json') throw new TypeError('Content-Type must be application/json')
|
|
71
|
+
const chunks: Buffer[] = []
|
|
72
|
+
let bytes = 0
|
|
73
|
+
for await (const chunk of req) {
|
|
74
|
+
const part = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)
|
|
75
|
+
bytes += part.length
|
|
76
|
+
if (bytes > MAX_BODY_BYTES) throw new RangeError('request body too large')
|
|
77
|
+
chunks.push(part)
|
|
78
|
+
}
|
|
79
|
+
if (chunks.length === 0) throw new TypeError('request body is empty')
|
|
80
|
+
return JSON.parse(Buffer.concat(chunks).toString('utf8')) as unknown
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
84
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function str(value: unknown): string | undefined {
|
|
88
|
+
return typeof value === 'string' && value !== '' ? value : undefined
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** URL-decoded session/workspace + file query params, with a sane file length cap. */
|
|
92
|
+
function queryParams(req: IncomingMessage): { session?: string | undefined; ws?: string | undefined; file?: string | undefined; mode?: string | undefined } {
|
|
93
|
+
const url = new URL(req.url ?? '/', 'http://localhost')
|
|
94
|
+
const file = url.searchParams.get('file')
|
|
95
|
+
return {
|
|
96
|
+
session: str(url.searchParams.get('session')),
|
|
97
|
+
ws: str(url.searchParams.get('ws')),
|
|
98
|
+
file: file !== null && file.length <= 4096 ? file : undefined,
|
|
99
|
+
mode: str(url.searchParams.get('mode')),
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Validate that a POST body carries the expected string fields. */
|
|
104
|
+
function requireFields<K extends string>(body: unknown, fields: readonly K[]): Record<K, string> {
|
|
105
|
+
if (!isRecord(body)) throw new TypeError('request body must be an object')
|
|
106
|
+
const out = {} as Record<K, string>
|
|
107
|
+
for (const field of fields) {
|
|
108
|
+
const value = body[field]
|
|
109
|
+
if (typeof value !== 'string' || value === '') throw new TypeError(`field "${field}" must be a non-empty string`)
|
|
110
|
+
out[field] = value
|
|
111
|
+
}
|
|
112
|
+
return out
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Same-origin Git endpoints. GETs are read-only; POSTs mutate the repository
|
|
117
|
+
* and reject cross-site requests.
|
|
118
|
+
*/
|
|
119
|
+
export class GitWebHandler {
|
|
120
|
+
constructor(private readonly backend: GitBackend) {}
|
|
121
|
+
|
|
122
|
+
async handle(req: IncomingMessage, res: ServerResponse): Promise<void> {
|
|
123
|
+
const url = new URL(req.url ?? '/', 'http://localhost')
|
|
124
|
+
const path = url.pathname
|
|
125
|
+
|
|
126
|
+
if (req.method === 'GET') {
|
|
127
|
+
// Vendored xterm builds for the terminal panel (exact-name whitelist).
|
|
128
|
+
if (path.startsWith(`${GIT_ROUTE}/vendor/`)) {
|
|
129
|
+
this.serveVendor(res, path.slice(`${GIT_ROUTE}/vendor/`.length))
|
|
130
|
+
return
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const { session, ws, file, mode } = queryParams(req)
|
|
134
|
+
|
|
135
|
+
try {
|
|
136
|
+
const cwd = this.backend.resolveTargetCwd({ session, ws })
|
|
137
|
+
if (cwd === undefined) {
|
|
138
|
+
// Distinguish "target not resolvable YET" (right after a restart the
|
|
139
|
+
// host has not materialized its session/workspace store, so even a
|
|
140
|
+
// valid session id has no cwd) from a genuine non-repo directory.
|
|
141
|
+
// The browser half chases the former with a short backoff instead of
|
|
142
|
+
// caching it as final and sitting blank for a whole poll period.
|
|
143
|
+
ok(res, { isRepo: false, unresolved: true })
|
|
144
|
+
return
|
|
145
|
+
}
|
|
146
|
+
if (path === `${GIT_ROUTE}/status`) {
|
|
147
|
+
ok(res, await this.backend.snapshot(cwd))
|
|
148
|
+
return
|
|
149
|
+
}
|
|
150
|
+
if (path === `${GIT_ROUTE}/branches`) {
|
|
151
|
+
ok(res, await this.backend.branches(cwd))
|
|
152
|
+
return
|
|
153
|
+
}
|
|
154
|
+
if (path === `${GIT_ROUTE}/diff`) {
|
|
155
|
+
if (file === undefined) throw new TypeError('file query parameter is required')
|
|
156
|
+
if (mode !== 'hunk' && mode !== 'full') throw new TypeError('mode must be "hunk" or "full"')
|
|
157
|
+
ok(res, await this.backend.diff(cwd, file, mode))
|
|
158
|
+
return
|
|
159
|
+
}
|
|
160
|
+
if (path === `${GIT_ROUTE}/graph`) {
|
|
161
|
+
const raw = url.searchParams.get('limit')
|
|
162
|
+
const limit = raw === null ? undefined : Number(raw)
|
|
163
|
+
ok(res, await this.backend.graph(cwd, limit))
|
|
164
|
+
return
|
|
165
|
+
}
|
|
166
|
+
} catch (error) {
|
|
167
|
+
fail(res, 400, 'git-request-failed', messageOf(error))
|
|
168
|
+
return
|
|
169
|
+
}
|
|
170
|
+
fail(res, 404, 'not-found', 'unknown git endpoint')
|
|
171
|
+
return
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (req.method !== 'POST') {
|
|
175
|
+
res.setHeader('Allow', 'GET, POST')
|
|
176
|
+
fail(res, 405, 'method-not-allowed', 'Use GET or POST')
|
|
177
|
+
return
|
|
178
|
+
}
|
|
179
|
+
if (!sameOriginPost(req)) {
|
|
180
|
+
fail(res, 403, 'origin-rejected', 'The request must originate from this ChatCode CLI Web application')
|
|
181
|
+
return
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
let body: unknown
|
|
185
|
+
try {
|
|
186
|
+
body = await readJson(req)
|
|
187
|
+
} catch (error) {
|
|
188
|
+
fail(res, error instanceof RangeError ? 413 : 400, 'invalid-request', messageOf(error))
|
|
189
|
+
return
|
|
190
|
+
}
|
|
191
|
+
const session = isRecord(body) ? str(body.session) : undefined
|
|
192
|
+
const ws = isRecord(body) ? str(body.ws) : undefined
|
|
193
|
+
|
|
194
|
+
try {
|
|
195
|
+
if (path === `${GIT_ROUTE}/open`) {
|
|
196
|
+
const fields = requireFields(body, ['target'])
|
|
197
|
+
if (fields.target !== 'explorer' && fields.target !== 'vscode' && fields.target !== 'idea'
|
|
198
|
+
&& fields.target !== 'goland' && fields.target !== 'webstorm' && fields.target !== 'pycharm') {
|
|
199
|
+
throw new TypeError('target must be "explorer", "vscode", "idea", "goland", "webstorm" or "pycharm"')
|
|
200
|
+
}
|
|
201
|
+
const cwd = this.requireCwd({ session, ws })
|
|
202
|
+
await this.backend.openFolder(cwd, fields.target as 'explorer' | 'vscode' | 'idea' | 'goland' | 'webstorm' | 'pycharm')
|
|
203
|
+
ok(res, { opened: fields.target })
|
|
204
|
+
return
|
|
205
|
+
}
|
|
206
|
+
if (path === `${GIT_ROUTE}/commit`) {
|
|
207
|
+
const fields = requireFields(body, ['message'])
|
|
208
|
+
const push = isRecord(body) && body.push === true
|
|
209
|
+
const exclude = isRecord(body) && Array.isArray(body.exclude)
|
|
210
|
+
? body.exclude.filter((entry): entry is string => typeof entry === 'string' && entry !== '')
|
|
211
|
+
: undefined
|
|
212
|
+
const cwd = this.requireCwd({ session, ws })
|
|
213
|
+
ok(res, await this.backend.commit(cwd, fields.message, {
|
|
214
|
+
push,
|
|
215
|
+
...(exclude !== undefined && exclude.length > 0 ? { exclude } : {}),
|
|
216
|
+
}))
|
|
217
|
+
return
|
|
218
|
+
}
|
|
219
|
+
if (path === `${GIT_ROUTE}/push`) {
|
|
220
|
+
const cwd = this.requireCwd({ session, ws })
|
|
221
|
+
await this.backend.push(cwd)
|
|
222
|
+
ok(res, { pushed: true })
|
|
223
|
+
return
|
|
224
|
+
}
|
|
225
|
+
if (path === `${GIT_ROUTE}/checkout`) {
|
|
226
|
+
const fields = requireFields(body, ['branch'])
|
|
227
|
+
const cwd = this.requireCwd({ session, ws })
|
|
228
|
+
await this.backend.checkout(cwd, fields.branch)
|
|
229
|
+
ok(res, { branch: fields.branch })
|
|
230
|
+
return
|
|
231
|
+
}
|
|
232
|
+
if (path === `${GIT_ROUTE}/create`) {
|
|
233
|
+
const fields = requireFields(body, ['name'])
|
|
234
|
+
const base = isRecord(body) ? str(body.base) : undefined
|
|
235
|
+
const pushRemote = isRecord(body) && body.push === true
|
|
236
|
+
const cwd = this.requireCwd({ session, ws })
|
|
237
|
+
await this.backend.createBranch(cwd, fields.name, base, pushRemote)
|
|
238
|
+
ok(res, { branch: fields.name, pushed: pushRemote })
|
|
239
|
+
return
|
|
240
|
+
}
|
|
241
|
+
if (path === `${GIT_ROUTE}/branch-delete`) {
|
|
242
|
+
const fields = requireFields(body, ['name'])
|
|
243
|
+
const cwd = this.requireCwd({ session, ws })
|
|
244
|
+
await this.backend.deleteBranch(cwd, fields.name)
|
|
245
|
+
ok(res, { branch: fields.name })
|
|
246
|
+
return
|
|
247
|
+
}
|
|
248
|
+
if (path === `${GIT_ROUTE}/remote-delete`) {
|
|
249
|
+
const fields = requireFields(body, ['name'])
|
|
250
|
+
const cwd = this.requireCwd({ session, ws })
|
|
251
|
+
await this.backend.deleteRemoteBranch(cwd, fields.name)
|
|
252
|
+
ok(res, { branch: fields.name })
|
|
253
|
+
return
|
|
254
|
+
}
|
|
255
|
+
if (path === `${GIT_ROUTE}/suggest`) {
|
|
256
|
+
const cwd = this.requireCwd({ session, ws })
|
|
257
|
+
// `via` / `reason` let the UI say when the offline heuristic wrote the
|
|
258
|
+
// message; `message` stays the first-class field.
|
|
259
|
+
ok(res, await this.backend.suggestDetailed(cwd))
|
|
260
|
+
return
|
|
261
|
+
}
|
|
262
|
+
} catch (error) {
|
|
263
|
+
fail(res, 400, 'git-action-failed', messageOf(error))
|
|
264
|
+
return
|
|
265
|
+
}
|
|
266
|
+
fail(res, 404, 'not-found', 'unknown git endpoint')
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
private requireCwd(target: { session?: string | undefined; ws?: string | undefined }): string {
|
|
270
|
+
const cwd = this.backend.resolveTargetCwd(target)
|
|
271
|
+
if (cwd === undefined) throw new Error('no working directory for this session')
|
|
272
|
+
return cwd
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/** Serve one whitelisted vendored asset; anything else is a 404. */
|
|
276
|
+
private serveVendor(res: ServerResponse, name: string): void {
|
|
277
|
+
const entry = VENDOR_FILES[name]
|
|
278
|
+
if (entry === undefined) {
|
|
279
|
+
fail(res, 404, 'not-found', 'unknown vendor asset')
|
|
280
|
+
return
|
|
281
|
+
}
|
|
282
|
+
try {
|
|
283
|
+
// import.meta.url = <packageRoot>/lib/git-web.js → ../vendor = package
|
|
284
|
+
// root's vendor/ directory (ships in the npm `files` list).
|
|
285
|
+
const body = readFileSync(new URL(`../vendor/${name}`, import.meta.url))
|
|
286
|
+
res.setHeader('Content-Type', entry.type)
|
|
287
|
+
res.setHeader('Content-Length', String(body.length))
|
|
288
|
+
res.setHeader('Cache-Control', 'public, max-age=86400')
|
|
289
|
+
res.setHeader('X-Content-Type-Options', 'nosniff')
|
|
290
|
+
res.writeHead(200)
|
|
291
|
+
res.end(body)
|
|
292
|
+
} catch (error) {
|
|
293
|
+
fail(res, 500, 'vendor-read-failed', messageOf(error))
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/** Registry key for one target's terminal: `session:<id>` or `ws:<name>`. */
|
|
299
|
+
function terminalKeyOf(session: string | undefined, ws: string | undefined): string | undefined {
|
|
300
|
+
if (session !== undefined) return `session:${session}`
|
|
301
|
+
if (ws !== undefined) return `ws:${ws}`
|
|
302
|
+
return undefined
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/** Clamp an integer query param into [min, max], falling back to `fallback`. */
|
|
306
|
+
function intParam(url: URL, name: string, fallback: number, min: number, max: number): number {
|
|
307
|
+
const raw = url.searchParams.get(name)
|
|
308
|
+
const parsed = raw === null ? Number.NaN : Number(raw)
|
|
309
|
+
if (!Number.isInteger(parsed) || parsed < min || parsed > max) return fallback
|
|
310
|
+
return parsed
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Attach the Git routes plus the terminal WebSocket whenever a webServer
|
|
315
|
+
* service is present.
|
|
316
|
+
* @param ctx - plugin context owning route effects.
|
|
317
|
+
* @param backend - Git backend.
|
|
318
|
+
*/
|
|
319
|
+
export function installGitWeb(ctx: Context, backend: GitBackend): void {
|
|
320
|
+
ctx.inject(['webServer'], (webCtx) => {
|
|
321
|
+
webCtx.effect(() => {
|
|
322
|
+
const handler = new GitWebHandler(backend)
|
|
323
|
+
return webCtx.webServer.register({
|
|
324
|
+
kind: 'prefix',
|
|
325
|
+
path: GIT_ROUTE,
|
|
326
|
+
handler: (req, res) => handler.handle(req, res),
|
|
327
|
+
})
|
|
328
|
+
}, 'dsh-ui-tweaks: Git routes')
|
|
329
|
+
|
|
330
|
+
// ── Terminal WebSocket ────────────────────────────────────────────────
|
|
331
|
+
// One upgrade endpoint bridges xterm.js in the browser to the target's
|
|
332
|
+
// persistent PTY. Wire protocol (mirrors dsh-better-sidebar):
|
|
333
|
+
// server → client: raw pty output chunks; one JSON {"type":"exit"} line
|
|
334
|
+
// when the shell dies.
|
|
335
|
+
// client → server: JSON frames only — {"type":"input","data":...},
|
|
336
|
+
// {"type":"resize",cols,rows}, {"type":"close"}.
|
|
337
|
+
// On connect the host replays the session's transcript ring before live
|
|
338
|
+
// data, so a panel reopen / page refresh restores the exact screen.
|
|
339
|
+
webCtx.effect(() => {
|
|
340
|
+
const wss = new WebSocketServer({ noServer: true })
|
|
341
|
+
return webCtx.webServer.registerUpgrade({
|
|
342
|
+
path: TERMINAL_WS_PATH,
|
|
343
|
+
handler: (req, socket, head) => {
|
|
344
|
+
const url = new URL(req.url ?? '/', 'http://localhost')
|
|
345
|
+
const session = str(url.searchParams.get('session'))
|
|
346
|
+
const wsName = str(url.searchParams.get('ws'))
|
|
347
|
+
const key = terminalKeyOf(session, wsName)
|
|
348
|
+
const cwd = key === undefined ? undefined : backend.resolveTargetCwd({ session, ws: wsName })
|
|
349
|
+
if (key === undefined || cwd === undefined) {
|
|
350
|
+
socket.destroy()
|
|
351
|
+
return
|
|
352
|
+
}
|
|
353
|
+
wss.handleUpgrade(req as IncomingMessage, socket as unknown as Duplex, head as Buffer, (ws) => {
|
|
354
|
+
attachTerminalSocket(backend, key, cwd, {
|
|
355
|
+
cols: intParam(url, 'cols', 80, 2, 500),
|
|
356
|
+
rows: intParam(url, 'rows', 24, 2, 200),
|
|
357
|
+
}, ws)
|
|
358
|
+
})
|
|
359
|
+
},
|
|
360
|
+
})
|
|
361
|
+
}, 'dsh-ui-tweaks: terminal WebSocket')
|
|
362
|
+
|
|
363
|
+
webCtx.effect(() => () => {
|
|
364
|
+
backend.terminals.disposeAll()
|
|
365
|
+
}, 'dsh-ui-tweaks: terminal teardown')
|
|
366
|
+
})
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/** Bridge one upgraded browser socket to the target's PTY session. */
|
|
370
|
+
function attachTerminalSocket(backend: GitBackend, key: string, cwd: string, dims: { cols: number; rows: number }, ws: WebSocket): void {
|
|
371
|
+
if (backend.terminals.unavailable) {
|
|
372
|
+
ws.close(1011, 'pty-unavailable')
|
|
373
|
+
return
|
|
374
|
+
}
|
|
375
|
+
const client: TerminalClient = {
|
|
376
|
+
send: (text) => { ws.send(text) },
|
|
377
|
+
get alive(): boolean { return ws.readyState === WebSocket.OPEN },
|
|
378
|
+
}
|
|
379
|
+
let session
|
|
380
|
+
try {
|
|
381
|
+
session = backend.terminals.attach(key, cwd, dims.cols, dims.rows, client)
|
|
382
|
+
} catch (error) {
|
|
383
|
+
ws.close(1011, messageOf(error).slice(0, 120))
|
|
384
|
+
return
|
|
385
|
+
}
|
|
386
|
+
// Replay what the shell printed before this socket attached (a fresh spawn
|
|
387
|
+
// has an empty ring). Synchronous, so it lands ahead of any live chunk.
|
|
388
|
+
if (session.transcript !== '') ws.send(session.transcript)
|
|
389
|
+
|
|
390
|
+
ws.on('message', (raw) => {
|
|
391
|
+
const text = typeof raw === 'string' ? raw : raw.toString('utf8')
|
|
392
|
+
if (text.startsWith('{')) {
|
|
393
|
+
try {
|
|
394
|
+
const frame = JSON.parse(text) as { type?: unknown; data?: unknown; cols?: unknown; rows?: unknown }
|
|
395
|
+
if (frame.type === 'input' && typeof frame.data === 'string') {
|
|
396
|
+
backend.terminals.input(key, frame.data)
|
|
397
|
+
return
|
|
398
|
+
}
|
|
399
|
+
if (frame.type === 'resize' && typeof frame.cols === 'number' && typeof frame.rows === 'number') {
|
|
400
|
+
backend.terminals.resize(key, frame.cols, frame.rows)
|
|
401
|
+
return
|
|
402
|
+
}
|
|
403
|
+
if (frame.type === 'close') {
|
|
404
|
+
backend.terminals.close(key)
|
|
405
|
+
ws.close(1000)
|
|
406
|
+
return
|
|
407
|
+
}
|
|
408
|
+
} catch {
|
|
409
|
+
// Malformed JSON that starts with '{' falls through as literal input —
|
|
410
|
+
// a user pasting braces into the shell must still work.
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
backend.terminals.input(key, text)
|
|
414
|
+
})
|
|
415
|
+
ws.on('close', () => {
|
|
416
|
+
backend.terminals.detach(key, client)
|
|
417
|
+
})
|
|
418
|
+
ws.on('error', () => {
|
|
419
|
+
// 'close' always follows; nothing else to clean here.
|
|
420
|
+
})
|
|
421
|
+
}
|