@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,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,103 @@
|
|
|
1
|
+
/** Browser lifecycle only; all account and credential decisions belong to the host service. */
|
|
2
|
+
import { useEffect, useRef, useState } from 'react'
|
|
3
|
+
import { api } from './api.ts'
|
|
4
|
+
import type { ChatCodeAuthState } from './api.ts'
|
|
5
|
+
|
|
6
|
+
export function useChatcodeAccount() {
|
|
7
|
+
const [chatcode, setChatcode] = useState<ChatCodeAuthState>()
|
|
8
|
+
const [busy, setBusy] = useState(false)
|
|
9
|
+
const [error, setError] = useState<'operation' | 'status'>()
|
|
10
|
+
const [loginUrl, setLoginUrl] = useState<string>()
|
|
11
|
+
const [revision, setRevision] = useState(0)
|
|
12
|
+
const busyRef = useRef(false)
|
|
13
|
+
const mounted = useRef(false)
|
|
14
|
+
const request = useRef<AbortController>()
|
|
15
|
+
const attempt = useRef<{ sessionId: string; popup: Window | null }>()
|
|
16
|
+
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
mounted.current = true
|
|
19
|
+
return () => { mounted.current = false }
|
|
20
|
+
}, [])
|
|
21
|
+
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
const controller = new AbortController()
|
|
24
|
+
request.current = controller
|
|
25
|
+
let timer: ReturnType<typeof setTimeout> | undefined
|
|
26
|
+
let running = false
|
|
27
|
+
const refresh = async (): Promise<void> => {
|
|
28
|
+
if (controller.signal.aborted || running || busyRef.current) return
|
|
29
|
+
clearTimeout(timer)
|
|
30
|
+
running = true
|
|
31
|
+
try {
|
|
32
|
+
const state = await api.chatcodeStatus(controller.signal)
|
|
33
|
+
if (controller.signal.aborted) return
|
|
34
|
+
setChatcode(state)
|
|
35
|
+
setError(current => current === 'status' ? undefined : current)
|
|
36
|
+
const local = attempt.current
|
|
37
|
+
if (local && state.login?.sessionId === local.sessionId && state.login.state === 'succeeded' && state.validation === 'valid') {
|
|
38
|
+
// COOP/browser policy may sever the handle. Returning focus is best effort.
|
|
39
|
+
try { local.popup?.close(); window.focus() } catch { /* Original DSH tab remains usable. */ }
|
|
40
|
+
attempt.current = undefined
|
|
41
|
+
setLoginUrl(undefined)
|
|
42
|
+
} else if (local && (state.login?.sessionId !== local.sessionId || state.login.state !== 'pending')) {
|
|
43
|
+
try { local.popup?.close() } catch { /* Detached window. */ }
|
|
44
|
+
attempt.current = undefined
|
|
45
|
+
setLoginUrl(undefined)
|
|
46
|
+
}
|
|
47
|
+
} catch {
|
|
48
|
+
if (!controller.signal.aborted) setError('status')
|
|
49
|
+
} finally {
|
|
50
|
+
running = false
|
|
51
|
+
if (!controller.signal.aborted) timer = setTimeout(() => { void refresh() }, 2_000)
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
void refresh()
|
|
55
|
+
const visible = (): void => { if (document.visibilityState === 'visible') void refresh() }
|
|
56
|
+
document.addEventListener('visibilitychange', visible)
|
|
57
|
+
return () => { controller.abort(); clearTimeout(timer); document.removeEventListener('visibilitychange', visible) }
|
|
58
|
+
}, [revision])
|
|
59
|
+
|
|
60
|
+
const operate = (action: () => Promise<void>): void => {
|
|
61
|
+
if (busyRef.current) return
|
|
62
|
+
busyRef.current = true
|
|
63
|
+
request.current?.abort() // Invalidate in-flight status BEFORE changing the account.
|
|
64
|
+
setBusy(true)
|
|
65
|
+
setError(undefined)
|
|
66
|
+
void action().catch(() => { if (mounted.current) setError('operation') }).finally(() => {
|
|
67
|
+
busyRef.current = false
|
|
68
|
+
if (mounted.current) { setBusy(false); setRevision(value => value + 1) }
|
|
69
|
+
})
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const startLogin = (): void => {
|
|
73
|
+
if (busyRef.current) return
|
|
74
|
+
// Never reuse a named cross-origin window. Retain the user gesture for popup permission.
|
|
75
|
+
let popup: Window | null = null
|
|
76
|
+
try { popup = window.open('', '_blank'); if (popup) popup.opener = null } catch { popup = null }
|
|
77
|
+
operate(async () => {
|
|
78
|
+
try {
|
|
79
|
+
const start = await api.chatcodeStart()
|
|
80
|
+
if (!mounted.current) { popup?.close(); return }
|
|
81
|
+
try { attempt.current?.popup?.close() } catch { /* Detached window. */ }
|
|
82
|
+
attempt.current = { sessionId: start.sessionId, popup }
|
|
83
|
+
setLoginUrl(start.url) // Always provide a real link when popups are blocked.
|
|
84
|
+
setChatcode(current => ({ ...current, available: true, login: { sessionId: start.sessionId, state: 'pending', expiresAtMs: 0 } }))
|
|
85
|
+
try { if (popup && !popup.closed) popup.location.href = start.url } catch { /* The explicit link remains available. */ }
|
|
86
|
+
} catch (error) { try { popup?.close() } catch { /* Detached window. */ }; throw error }
|
|
87
|
+
})
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const logout = (): void => operate(async () => {
|
|
91
|
+
await api.chatcodeLogout()
|
|
92
|
+
if (!mounted.current) return
|
|
93
|
+
try { attempt.current?.popup?.close() } catch { /* Detached window. */ }
|
|
94
|
+
attempt.current = undefined
|
|
95
|
+
setLoginUrl(undefined)
|
|
96
|
+
setChatcode({ available: true, configured: false, expired: false, validation: 'none' })
|
|
97
|
+
})
|
|
98
|
+
const cancelLogin = (): void => {
|
|
99
|
+
const sessionId = chatcode?.login?.sessionId
|
|
100
|
+
if (sessionId) operate(async () => { await api.chatcodeCancel(sessionId) })
|
|
101
|
+
}
|
|
102
|
+
return { chatcode, busy, error, loginUrl, startLogin, logout, cancelLogin }
|
|
103
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CodingPlan model-catalogue client. It deliberately runs only in the host
|
|
3
|
+
* half of the sidebar plugin: CodingPlan connection details and any API key
|
|
4
|
+
* present in its response must never cross the `/sidebar/api` boundary.
|
|
5
|
+
*/
|
|
6
|
+
import type { ResolvedCodingPlanConfig } from './config.ts'
|
|
7
|
+
|
|
8
|
+
/** The safe, display-only subset of one CodingPlan model. */
|
|
9
|
+
export interface CodingPlanModel {
|
|
10
|
+
logicalModelId: string
|
|
11
|
+
displayName: string
|
|
12
|
+
description?: string
|
|
13
|
+
accessType?: string
|
|
14
|
+
maxToken?: number
|
|
15
|
+
contextWindow?: number
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Browser-safe catalogue snapshot returned by the plugin route. */
|
|
19
|
+
export interface CodingPlanCatalogueSnapshot {
|
|
20
|
+
status: 'loading' | 'ready' | 'unavailable'
|
|
21
|
+
models: CodingPlanModel[]
|
|
22
|
+
fetchedAt?: string
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
type FetchLike = typeof fetch
|
|
26
|
+
|
|
27
|
+
const MAX_ID_LENGTH = 256
|
|
28
|
+
const MAX_TEXT_LENGTH = 1024
|
|
29
|
+
|
|
30
|
+
function text(value: unknown, maxLength = MAX_TEXT_LENGTH): string | undefined {
|
|
31
|
+
if (typeof value !== 'string') return undefined
|
|
32
|
+
const trimmed = value.trim()
|
|
33
|
+
return trimmed === '' ? undefined : trimmed.slice(0, maxLength)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function positiveInteger(value: unknown): number | undefined {
|
|
37
|
+
return typeof value === 'number' && Number.isSafeInteger(value) && value > 0 ? value : undefined
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Strip an untrusted server item down to fields safe to show in the UI. */
|
|
41
|
+
export function publicCodingPlanModel(value: unknown): CodingPlanModel | undefined {
|
|
42
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value)) return undefined
|
|
43
|
+
const record = value as Record<string, unknown>
|
|
44
|
+
const logicalModelId = text(record.logicalModelId, MAX_ID_LENGTH)
|
|
45
|
+
const displayName = text(record.displayName, MAX_TEXT_LENGTH)
|
|
46
|
+
if (logicalModelId === undefined || displayName === undefined) return undefined
|
|
47
|
+
return {
|
|
48
|
+
logicalModelId,
|
|
49
|
+
displayName,
|
|
50
|
+
...(text(record.description) !== undefined ? { description: text(record.description) } : {}),
|
|
51
|
+
...(text(record.accessType, 64) !== undefined ? { accessType: text(record.accessType, 64) } : {}),
|
|
52
|
+
...(positiveInteger(record.maxToken) !== undefined ? { maxToken: positiveInteger(record.maxToken) } : {}),
|
|
53
|
+
...(positiveInteger(record.contextWindow) !== undefined ? { contextWindow: positiveInteger(record.contextWindow) } : {}),
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function catalogueUrl(config: ResolvedCodingPlanConfig): URL {
|
|
58
|
+
const origin = new URL(config.baseUrl)
|
|
59
|
+
if (origin.protocol !== 'http:' && origin.protocol !== 'https:') {
|
|
60
|
+
throw new Error('CodingPlan endpoint must use HTTP(S)')
|
|
61
|
+
}
|
|
62
|
+
// The endpoint is fixed; deployments configure only its origin so a
|
|
63
|
+
// profile cannot accidentally turn this catalogue route into an arbitrary
|
|
64
|
+
// authenticated request target.
|
|
65
|
+
origin.pathname = '/api/cli/v1/model-runtime-configs'
|
|
66
|
+
origin.search = new URLSearchParams({
|
|
67
|
+
encryptApiKey: 'false',
|
|
68
|
+
userId: config.userId,
|
|
69
|
+
accountUuid: config.accountUuid,
|
|
70
|
+
userEmail: config.userEmail,
|
|
71
|
+
emailAddress: config.emailAddress,
|
|
72
|
+
}).toString()
|
|
73
|
+
return origin
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Host-side, single-flight CodingPlan catalogue cache. */
|
|
77
|
+
export class CodingPlanCatalogue {
|
|
78
|
+
private snapshotValue: CodingPlanCatalogueSnapshot = { status: 'loading', models: [] }
|
|
79
|
+
private inFlight: Promise<CodingPlanCatalogueSnapshot> | undefined
|
|
80
|
+
|
|
81
|
+
constructor(
|
|
82
|
+
private readonly config: ResolvedCodingPlanConfig,
|
|
83
|
+
private readonly request: FetchLike = fetch,
|
|
84
|
+
) {}
|
|
85
|
+
|
|
86
|
+
snapshot(): CodingPlanCatalogueSnapshot {
|
|
87
|
+
return {
|
|
88
|
+
...this.snapshotValue,
|
|
89
|
+
models: this.snapshotValue.models.map(model => ({ ...model })),
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Refresh once; concurrent callers share the same outgoing request. */
|
|
94
|
+
refresh(): Promise<CodingPlanCatalogueSnapshot> {
|
|
95
|
+
if (this.inFlight !== undefined) return this.inFlight
|
|
96
|
+
this.inFlight = this.load().finally(() => { this.inFlight = undefined })
|
|
97
|
+
return this.inFlight
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
private async load(): Promise<CodingPlanCatalogueSnapshot> {
|
|
101
|
+
const controller = new AbortController()
|
|
102
|
+
const timeout = setTimeout(() => controller.abort(), this.config.requestTimeoutMs)
|
|
103
|
+
try {
|
|
104
|
+
const response = await this.request(catalogueUrl(this.config), {
|
|
105
|
+
method: 'GET',
|
|
106
|
+
headers: { accept: 'application/json' },
|
|
107
|
+
signal: controller.signal,
|
|
108
|
+
})
|
|
109
|
+
if (!response.ok) throw new Error('CodingPlan catalogue request failed')
|
|
110
|
+
const payload: unknown = await response.json()
|
|
111
|
+
const record = payload !== null && typeof payload === 'object' && !Array.isArray(payload)
|
|
112
|
+
? payload as Record<string, unknown>
|
|
113
|
+
: undefined
|
|
114
|
+
const data = record?.data
|
|
115
|
+
const models = data !== null && typeof data === 'object' && !Array.isArray(data)
|
|
116
|
+
? (data as Record<string, unknown>).models
|
|
117
|
+
: undefined
|
|
118
|
+
if (record?.code !== '00000000' || !Array.isArray(models)) {
|
|
119
|
+
throw new Error('CodingPlan catalogue response is invalid')
|
|
120
|
+
}
|
|
121
|
+
const deduped = new Map<string, CodingPlanModel>()
|
|
122
|
+
for (const item of models) {
|
|
123
|
+
const model = publicCodingPlanModel(item)
|
|
124
|
+
if (model !== undefined) deduped.set(model.logicalModelId, model)
|
|
125
|
+
}
|
|
126
|
+
this.snapshotValue = {
|
|
127
|
+
status: 'ready',
|
|
128
|
+
models: [...deduped.values()],
|
|
129
|
+
fetchedAt: new Date().toISOString(),
|
|
130
|
+
}
|
|
131
|
+
return this.snapshot()
|
|
132
|
+
} catch {
|
|
133
|
+
// Keep a prior successful list usable during a temporary outage. No
|
|
134
|
+
// upstream response/message is preserved or surfaced: it could contain
|
|
135
|
+
// provider credentials or other deployment-only details.
|
|
136
|
+
this.snapshotValue = {
|
|
137
|
+
status: 'unavailable',
|
|
138
|
+
models: this.snapshotValue.models,
|
|
139
|
+
...(this.snapshotValue.fetchedAt !== undefined ? { fetchedAt: this.snapshotValue.fetchedAt } : {}),
|
|
140
|
+
}
|
|
141
|
+
return this.snapshot()
|
|
142
|
+
} finally {
|
|
143
|
+
clearTimeout(timeout)
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|