@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,292 @@
|
|
|
1
|
+
import { useEffect, useMemo, useRef, useState } from 'react'
|
|
2
|
+
import { Input } from '@deepseek-ai/dsh-client-ui-primitives'
|
|
3
|
+
import type {} from '@deepseek-ai/dsh-client-ui-settings/client'
|
|
4
|
+
import type { PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'
|
|
5
|
+
import clsx from 'clsx'
|
|
6
|
+
import { api, SidebarApiError, type ChatCodeCommitRecord } from './api.ts'
|
|
7
|
+
import { t } from './locales.ts'
|
|
8
|
+
import css from './AiRatioSection.module.css'
|
|
9
|
+
|
|
10
|
+
/** Commit rows per page (the portal's page size). */
|
|
11
|
+
const PAGE_SIZE = 10
|
|
12
|
+
|
|
13
|
+
export type AiRatioSectionProps = PropsRuntime<'settings.section'>
|
|
14
|
+
|
|
15
|
+
/** One query's outcome. */
|
|
16
|
+
type QueryState =
|
|
17
|
+
| { kind: 'loading' }
|
|
18
|
+
| { kind: 'ready'; records: ChatCodeCommitRecord[] }
|
|
19
|
+
| { kind: 'signin' }
|
|
20
|
+
| { kind: 'error'; message: string }
|
|
21
|
+
|
|
22
|
+
const isoDay = (date: Date): string =>
|
|
23
|
+
`${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`
|
|
24
|
+
|
|
25
|
+
/** The current month's first/last day (the query row's default range). */
|
|
26
|
+
const monthRange = (): { begin: string; end: string } => {
|
|
27
|
+
const now = new Date()
|
|
28
|
+
return {
|
|
29
|
+
begin: isoDay(new Date(now.getFullYear(), now.getMonth(), 1)),
|
|
30
|
+
end: isoDay(new Date(now.getFullYear(), now.getMonth() + 1, 0)),
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Coerce one platform number defensively (absent/negative/garbage → 0). */
|
|
35
|
+
const num = (value: unknown): number => {
|
|
36
|
+
const parsed = typeof value === 'number' ? value : Number(value)
|
|
37
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : 0
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** The portal's per-record ratio: additions=0 → 0, else round(ai/added*100). */
|
|
41
|
+
const ratioOf = (record: ChatCodeCommitRecord): number => {
|
|
42
|
+
const added = num(record.additions)
|
|
43
|
+
return added === 0 ? 0 : Math.round((num(record.aiTotal) / added) * 100)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Ratio ink: ≥70 success, ≥50 business, else warn (the portal's green/blue/orange). */
|
|
47
|
+
const ratioColor = (ratio: number): string =>
|
|
48
|
+
ratio >= 70
|
|
49
|
+
? 'var(--dsw-alias-state-success-primary)'
|
|
50
|
+
: ratio >= 50
|
|
51
|
+
? 'var(--dsw-alias-state-business-primary)'
|
|
52
|
+
: 'var(--dsw-alias-state-warn-primary)'
|
|
53
|
+
|
|
54
|
+
const intOf = (value: number): string => value.toLocaleString()
|
|
55
|
+
|
|
56
|
+
const textOf = (value: unknown): string => (typeof value === 'string' ? value : value === undefined || value === null ? '' : String(value))
|
|
57
|
+
|
|
58
|
+
export function AiRatioSection(_props: AiRatioSectionProps) {
|
|
59
|
+
const initial = useMemo(monthRange, [])
|
|
60
|
+
const [begin, setBegin] = useState(initial.begin)
|
|
61
|
+
const [end, setEnd] = useState(initial.end)
|
|
62
|
+
const [query, setQuery] = useState<QueryState>({ kind: 'loading' })
|
|
63
|
+
const [page, setPage] = useState(1)
|
|
64
|
+
const controllerRef = useRef<AbortController | null>(null)
|
|
65
|
+
|
|
66
|
+
const runQuery = (rangeBegin: string, rangeEnd: string): void => {
|
|
67
|
+
controllerRef.current?.abort()
|
|
68
|
+
const controller = new AbortController()
|
|
69
|
+
controllerRef.current = controller
|
|
70
|
+
setQuery({ kind: 'loading' })
|
|
71
|
+
setPage(1)
|
|
72
|
+
api.chatcodeGitCommitStats(rangeBegin, rangeEnd, controller.signal)
|
|
73
|
+
.then(records => {
|
|
74
|
+
if (controller.signal.aborted) return
|
|
75
|
+
setQuery({ kind: 'ready', records })
|
|
76
|
+
})
|
|
77
|
+
.catch((error: unknown) => {
|
|
78
|
+
if (controller.signal.aborted) return
|
|
79
|
+
const code = error instanceof SidebarApiError ? error.code : ''
|
|
80
|
+
if (code === 'chatcode-unavailable' || code === 'chatcode-unauthorized') {
|
|
81
|
+
setQuery({ kind: 'signin' })
|
|
82
|
+
return
|
|
83
|
+
}
|
|
84
|
+
setQuery({ kind: 'error', message: error instanceof Error ? error.message : String(error) })
|
|
85
|
+
})
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
useEffect(() => {
|
|
89
|
+
runQuery(initial.begin, initial.end)
|
|
90
|
+
return () => { controllerRef.current?.abort() }
|
|
91
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps -- mount-only default query
|
|
92
|
+
}, [])
|
|
93
|
+
|
|
94
|
+
const rangeValid = begin !== '' && end !== '' && begin <= end
|
|
95
|
+
|
|
96
|
+
const overview = useMemo(() => {
|
|
97
|
+
const records = query.kind === 'ready' ? query.records : []
|
|
98
|
+
let added = 0
|
|
99
|
+
let ai = 0
|
|
100
|
+
for (const record of records) {
|
|
101
|
+
added += num(record.additions)
|
|
102
|
+
ai += num(record.aiTotal)
|
|
103
|
+
}
|
|
104
|
+
return { added, ai, ratio: added > 0 ? Math.round((ai / added) * 100) : 0, count: records.length }
|
|
105
|
+
}, [query])
|
|
106
|
+
|
|
107
|
+
const pages = query.kind === 'ready' ? Math.max(1, Math.ceil(query.records.length / PAGE_SIZE)) : 1
|
|
108
|
+
const current = Math.min(page, pages)
|
|
109
|
+
const rows = query.kind === 'ready'
|
|
110
|
+
? query.records.slice((current - 1) * PAGE_SIZE, current * PAGE_SIZE)
|
|
111
|
+
: []
|
|
112
|
+
|
|
113
|
+
const exportCsv = (): void => {
|
|
114
|
+
if (query.kind !== 'ready' || query.records.length === 0) return
|
|
115
|
+
const header = [
|
|
116
|
+
t('aiRatioColProject'), t('aiRatioColName'), t('aiRatioColEmail'), t('aiRatioColAuthorEmail'),
|
|
117
|
+
t('aiRatioColAdditions'), t('aiRatioColDeletions'), t('aiRatioColTotal'), t('aiRatioColAi'),
|
|
118
|
+
t('aiRatioColRatio'), t('aiRatioColOrg'), t('aiRatioColDept'), t('aiRatioColTitle'),
|
|
119
|
+
t('aiRatioColShortId'), t('aiRatioColDate'),
|
|
120
|
+
]
|
|
121
|
+
const lines = query.records.map(record => [
|
|
122
|
+
textOf(record.projectName), textOf(record.name), textOf(record.email), textOf(record.authorEmail),
|
|
123
|
+
num(record.additions), num(record.deletions), num(record.total), num(record.aiTotal),
|
|
124
|
+
`${ratioOf(record)}%`, textOf(record.orgname), textOf(record.deptname), textOf(record.title),
|
|
125
|
+
textOf(record.shortId), textOf(record.authoredDate),
|
|
126
|
+
])
|
|
127
|
+
const csv = [header, ...lines]
|
|
128
|
+
.map(cells => cells.map(cell => `"${String(cell).replace(/"/g, '""')}"`).join(','))
|
|
129
|
+
.join('\r\n')
|
|
130
|
+
const blob = new Blob([`\ufeff${csv}`], { type: 'text/csv;charset=utf-8' })
|
|
131
|
+
const url = URL.createObjectURL(blob)
|
|
132
|
+
const anchor = document.createElement('a')
|
|
133
|
+
anchor.href = url
|
|
134
|
+
anchor.download = `ai-code-ratio_${begin}_${end}.csv`
|
|
135
|
+
document.body.appendChild(anchor)
|
|
136
|
+
anchor.click()
|
|
137
|
+
anchor.remove()
|
|
138
|
+
URL.revokeObjectURL(url)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return (
|
|
142
|
+
<section className={css.section} aria-label={t('aiRatioTitle')}>
|
|
143
|
+
<div className={css.queryRow}>
|
|
144
|
+
<Input
|
|
145
|
+
type="date"
|
|
146
|
+
className={css.dateInput}
|
|
147
|
+
value={begin}
|
|
148
|
+
aria-label={t('aiRatioDateStart')}
|
|
149
|
+
onChange={event => { setBegin(event.currentTarget.value) }}
|
|
150
|
+
/>
|
|
151
|
+
<span className={css.dash}>–</span>
|
|
152
|
+
<Input
|
|
153
|
+
type="date"
|
|
154
|
+
className={css.dateInput}
|
|
155
|
+
value={end}
|
|
156
|
+
aria-label={t('aiRatioDateEnd')}
|
|
157
|
+
onChange={event => { setEnd(event.currentTarget.value) }}
|
|
158
|
+
/>
|
|
159
|
+
<button
|
|
160
|
+
type="button"
|
|
161
|
+
className={css.primaryButton}
|
|
162
|
+
disabled={!rangeValid || query.kind === 'loading'}
|
|
163
|
+
onClick={() => { runQuery(begin, end) }}
|
|
164
|
+
>
|
|
165
|
+
{query.kind === 'loading' ? t('aiRatioQuerying') : t('aiRatioQuery')}
|
|
166
|
+
</button>
|
|
167
|
+
<button
|
|
168
|
+
type="button"
|
|
169
|
+
className={css.exportButton}
|
|
170
|
+
disabled={query.kind !== 'ready' || query.records.length === 0}
|
|
171
|
+
onClick={exportCsv}
|
|
172
|
+
>
|
|
173
|
+
{t('aiRatioExport')}
|
|
174
|
+
</button>
|
|
175
|
+
</div>
|
|
176
|
+
|
|
177
|
+
<div className={css.overview}>
|
|
178
|
+
<div className={css.card}>
|
|
179
|
+
<span className={css.cardLabel}>{t('aiRatioOverviewAdded')}</span>
|
|
180
|
+
<span className={clsx(css.cardValue, css.addedValue)}>{intOf(overview.added)}</span>
|
|
181
|
+
</div>
|
|
182
|
+
<div className={css.card}>
|
|
183
|
+
<span className={css.cardLabel}>{t('aiRatioOverviewAi')}</span>
|
|
184
|
+
<span className={clsx(css.cardValue, css.adoptedValue)}>{intOf(overview.ai)}</span>
|
|
185
|
+
</div>
|
|
186
|
+
<div className={clsx(css.card, overview.ratio >= 50 ? css.ratioCardHigh : css.ratioCardLow)}>
|
|
187
|
+
<span className={css.cardLabel}>{t('aiRatioOverviewRatio')}</span>
|
|
188
|
+
<span className={clsx(css.cardValue, css.cardRatio)}>{overview.ratio}%</span>
|
|
189
|
+
</div>
|
|
190
|
+
<div className={css.card}>
|
|
191
|
+
<span className={css.cardLabel}>{t('aiRatioOverviewCommits')}</span>
|
|
192
|
+
<span className={clsx(css.cardValue, css.commitsValue)}>{intOf(overview.count)}</span>
|
|
193
|
+
</div>
|
|
194
|
+
</div>
|
|
195
|
+
|
|
196
|
+
<div className={css.tableHeading}>{t('aiRatioTableTitle')}</div>
|
|
197
|
+
|
|
198
|
+
{query.kind === 'loading' ? (
|
|
199
|
+
<div className={css.state}>{t('aiRatioLoading')}</div>
|
|
200
|
+
) : query.kind === 'signin' ? (
|
|
201
|
+
<div className={css.state}>{t('aiRatioLoginRequired')}</div>
|
|
202
|
+
) : query.kind === 'error' ? (
|
|
203
|
+
<div className={css.stateError}>
|
|
204
|
+
<span>{query.message === '' ? t('aiRatioError') : `${t('aiRatioError')}:${query.message}`}</span>
|
|
205
|
+
<button type="button" className={css.exportButton} disabled={!rangeValid} onClick={() => { runQuery(begin, end) }}>
|
|
206
|
+
{t('aiRatioRetry')}
|
|
207
|
+
</button>
|
|
208
|
+
</div>
|
|
209
|
+
) : query.records.length === 0 ? (
|
|
210
|
+
<div className={css.state}>{t('aiRatioEmpty')}</div>
|
|
211
|
+
) : (
|
|
212
|
+
<>
|
|
213
|
+
<div className={css.tableWrap}>
|
|
214
|
+
<table className={css.table}>
|
|
215
|
+
<thead>
|
|
216
|
+
<tr>
|
|
217
|
+
<th>{t('aiRatioColProject')}</th>
|
|
218
|
+
<th>{t('aiRatioColName')}</th>
|
|
219
|
+
<th>{t('aiRatioColEmail')}</th>
|
|
220
|
+
<th>{t('aiRatioColAuthorEmail')}</th>
|
|
221
|
+
<th className={css.num}>{t('aiRatioColAdditions')}</th>
|
|
222
|
+
<th className={css.num}>{t('aiRatioColDeletions')}</th>
|
|
223
|
+
<th className={css.num}>{t('aiRatioColTotal')}</th>
|
|
224
|
+
<th className={css.num}>{t('aiRatioColAi')}</th>
|
|
225
|
+
<th>{t('aiRatioColRatio')}</th>
|
|
226
|
+
<th>{t('aiRatioColOrg')}</th>
|
|
227
|
+
<th>{t('aiRatioColDept')}</th>
|
|
228
|
+
<th>{t('aiRatioColTitle')}</th>
|
|
229
|
+
<th>{t('aiRatioColShortId')}</th>
|
|
230
|
+
<th>{t('aiRatioColDate')}</th>
|
|
231
|
+
</tr>
|
|
232
|
+
</thead>
|
|
233
|
+
<tbody>
|
|
234
|
+
{rows.map((record, index) => {
|
|
235
|
+
const ratio = ratioOf(record)
|
|
236
|
+
return (
|
|
237
|
+
<tr key={`${textOf(record.shortId)}-${index}`}>
|
|
238
|
+
<td title={textOf(record.projectName)}>{textOf(record.projectName)}</td>
|
|
239
|
+
<td>{textOf(record.name)}</td>
|
|
240
|
+
<td title={textOf(record.email)}>{textOf(record.email)}</td>
|
|
241
|
+
<td title={textOf(record.authorEmail)}>{textOf(record.authorEmail)}</td>
|
|
242
|
+
<td className={css.num}>{intOf(num(record.additions))}</td>
|
|
243
|
+
<td className={css.num}>{intOf(num(record.deletions))}</td>
|
|
244
|
+
<td className={css.num}>{intOf(num(record.total))}</td>
|
|
245
|
+
<td className={clsx(css.num, num(record.aiTotal) > 0 && css.aiValue)}>
|
|
246
|
+
{intOf(num(record.aiTotal))}
|
|
247
|
+
</td>
|
|
248
|
+
<td>
|
|
249
|
+
<span className={css.ratioValue} style={{ color: ratioColor(ratio) }}>{ratio}%</span>
|
|
250
|
+
<span className={css.ratioBar} aria-hidden="true">
|
|
251
|
+
<span style={{ width: `${Math.min(ratio, 100)}%`, background: ratioColor(ratio) }} />
|
|
252
|
+
</span>
|
|
253
|
+
</td>
|
|
254
|
+
<td title={textOf(record.orgname)}>{textOf(record.orgname)}</td>
|
|
255
|
+
<td title={textOf(record.deptname)}>{textOf(record.deptname)}</td>
|
|
256
|
+
<td title={textOf(record.title)}>{textOf(record.title)}</td>
|
|
257
|
+
<td>{textOf(record.shortId)}</td>
|
|
258
|
+
<td className={css.dateCell}>{textOf(record.authoredDate)}</td>
|
|
259
|
+
</tr>
|
|
260
|
+
)
|
|
261
|
+
})}
|
|
262
|
+
</tbody>
|
|
263
|
+
</table>
|
|
264
|
+
</div>
|
|
265
|
+
<div className={css.pager}>
|
|
266
|
+
<button
|
|
267
|
+
type="button"
|
|
268
|
+
className={css.exportButton}
|
|
269
|
+
aria-label={t('aiRatioPrevPage')}
|
|
270
|
+
disabled={current <= 1}
|
|
271
|
+
onClick={() => { setPage(current - 1) }}
|
|
272
|
+
>
|
|
273
|
+
‹
|
|
274
|
+
</button>
|
|
275
|
+
<span className={css.pageInfo}>
|
|
276
|
+
{t('aiRatioPageInfo', { page: current, pages, count: query.records.length })}
|
|
277
|
+
</span>
|
|
278
|
+
<button
|
|
279
|
+
type="button"
|
|
280
|
+
className={css.exportButton}
|
|
281
|
+
aria-label={t('aiRatioNextPage')}
|
|
282
|
+
disabled={current >= pages}
|
|
283
|
+
onClick={() => { setPage(current + 1) }}
|
|
284
|
+
>
|
|
285
|
+
›
|
|
286
|
+
</button>
|
|
287
|
+
</div>
|
|
288
|
+
</>
|
|
289
|
+
)}
|
|
290
|
+
</section>
|
|
291
|
+
)
|
|
292
|
+
}
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The built-in browser tab: an address bar plus a sandboxed iframe.
|
|
3
|
+
*
|
|
4
|
+
* Security model (see browser.ts + the sandbox tokens below): the iframe is
|
|
5
|
+
* ALWAYS sandboxed without `allow-same-origin` (opaque origin — the visited
|
|
6
|
+
* page can never sit on the GUI's origin, read its storage, or reach
|
|
7
|
+
* /sidebar/api) and without `allow-top-navigation` (a page must not hijack
|
|
8
|
+
* the GUI). The address bar only accepts http(s) and refuses loopback /
|
|
9
|
+
* the GUI's own origin. The side card setting "关闭浏览器沙箱" drops the
|
|
10
|
+
* sandbox attribute entirely for fully trusted sites — the visited page then
|
|
11
|
+
* runs with the GUI's own origin and full session access, so a persistent
|
|
12
|
+
* warning bar renders while it is off.
|
|
13
|
+
*
|
|
14
|
+
* The URL is persisted onto the tab (path/title via the patchTab reducer)
|
|
15
|
+
* so a reload restores the visited page; the back/forward stack only tracks
|
|
16
|
+
* address-bar navigations (in-frame link clicks are cross-origin and
|
|
17
|
+
* invisible — a documented limitation).
|
|
18
|
+
*/
|
|
19
|
+
import { useEffect, useState } from 'react'
|
|
20
|
+
import {
|
|
21
|
+
IconChevronLeftOutline14,
|
|
22
|
+
IconChevronRightOutline14,
|
|
23
|
+
IconLinkOutline14,
|
|
24
|
+
IconRefreshOutline14,
|
|
25
|
+
IconWarningOutline16,
|
|
26
|
+
} from '@deepseek-ai/dsh-client-ui-primitives'
|
|
27
|
+
import { VscLinkExternal } from 'react-icons/vsc'
|
|
28
|
+
import { api } from './api.ts'
|
|
29
|
+
import { embeddabilityOf, isAllowedLoopbackUrl, normalizeBrowserUrl } from './browser.ts'
|
|
30
|
+
import { patchTab } from './state.ts'
|
|
31
|
+
import { SandboxStatusBar } from './SandboxStatusBar.tsx'
|
|
32
|
+
import { t } from './locales.ts'
|
|
33
|
+
import type { TabComponentProps } from './service.ts'
|
|
34
|
+
import css from './sidebar.module.css'
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* The browser iframe sandbox tokens. NO allow-same-origin (opaque origin —
|
|
38
|
+
* no GUI storage/API access), NO allow-top-navigation (a browsed page must
|
|
39
|
+
* not hijack the GUI). allow-forms/allow-popups/allow-downloads/allow-modals
|
|
40
|
+
* keep login flows working; allow-popups-to-escape-sandbox lets OAuth
|
|
41
|
+
* popups open as normal tabs (they are cross-origin to the GUI either way).
|
|
42
|
+
*/
|
|
43
|
+
export const BROWSER_IFRAME_SANDBOX =
|
|
44
|
+
'allow-scripts allow-forms allow-popups allow-downloads allow-modals allow-popups-to-escape-sandbox'
|
|
45
|
+
|
|
46
|
+
/** allow-same-origin appended for explicitly allowlisted local addresses. */
|
|
47
|
+
const BROWSER_IFRAME_SANDBOX_SAME_ORIGIN =
|
|
48
|
+
`${BROWSER_IFRAME_SANDBOX} allow-same-origin`
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* The sandbox tokens for one URL: allowlisted loopback addresses (local dev
|
|
52
|
+
* servers the user explicitly trusts) additionally get `allow-same-origin`
|
|
53
|
+
* so Vite/module/HMR pipelines that need a real origin work; every other
|
|
54
|
+
* site keeps the opaque-origin sandbox. `allow-same-origin` does NOT give
|
|
55
|
+
* the page access to the GUI — it stays cross-origin to it and to every
|
|
56
|
+
* other site — but it does give it its OWN origin privileges (localStorage,
|
|
57
|
+
* fetch without CORS), so it is only granted for the explicit allowlist.
|
|
58
|
+
*
|
|
59
|
+
* The GUI itself is the one hard exception: even when its own host is
|
|
60
|
+
* allowlisted (a bare-host entry covers every port, so the GUI origin
|
|
61
|
+
* matches), a page at the GUI's exact origin must never get
|
|
62
|
+
* `allow-same-origin` — that would make it same-origin with its parent and
|
|
63
|
+
* hand it the GUI's storage/API (and the ability to shed the sandbox). The
|
|
64
|
+
* GUI keeps the opaque-origin sandbox no matter what the allowlist says.
|
|
65
|
+
*/
|
|
66
|
+
export function iframeSandboxFor(url: string | undefined, allowedLoopback: string, selfOrigin?: string): string | undefined {
|
|
67
|
+
if (url === undefined) return undefined
|
|
68
|
+
if (selfOrigin !== undefined) {
|
|
69
|
+
let parsed: URL
|
|
70
|
+
try {
|
|
71
|
+
parsed = new URL(url)
|
|
72
|
+
} catch {
|
|
73
|
+
return BROWSER_IFRAME_SANDBOX
|
|
74
|
+
}
|
|
75
|
+
if (parsed.origin === selfOrigin) return BROWSER_IFRAME_SANDBOX
|
|
76
|
+
}
|
|
77
|
+
return isAllowedLoopbackUrl(url, allowedLoopback)
|
|
78
|
+
? BROWSER_IFRAME_SANDBOX_SAME_ORIGIN
|
|
79
|
+
: BROWSER_IFRAME_SANDBOX
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function BrowserView(props: TabComponentProps) {
|
|
83
|
+
const { store, tab } = props
|
|
84
|
+
// The current address (initialized from the persisted tab.path so a
|
|
85
|
+
// reload restores the visited page).
|
|
86
|
+
const [url, setUrl] = useState<string | undefined>(tab.path)
|
|
87
|
+
const [input, setInput] = useState<string>(tab.path ?? '')
|
|
88
|
+
/** Blocked/invalid hint shown under the address bar (null = none). */
|
|
89
|
+
const [message, setMessage] = useState<string | null>(null)
|
|
90
|
+
/** Address-bar navigation history (in-frame clicks are not tracked). */
|
|
91
|
+
const [history, setHistory] = useState<string[]>(tab.path !== undefined ? [tab.path] : [])
|
|
92
|
+
const [cursor, setCursor] = useState<number>(tab.path !== undefined ? 0 : -1)
|
|
93
|
+
/** Bumped on reload to remount the iframe (also remounts on sandbox flip). */
|
|
94
|
+
const [reloadKey, setReloadKey] = useState(0)
|
|
95
|
+
/** TEMPORARY sandbox unlock for THIS surface only (never writes the global
|
|
96
|
+
* side card setting; lasts until the tab unmounts or the user restores). */
|
|
97
|
+
const [localUnlock, setLocalUnlock] = useState(false)
|
|
98
|
+
const noSandbox = store.getPrefs().browserNoSandbox === true || localUnlock
|
|
99
|
+
/** A site that refuses to be embedded (X-Frame-Options / frame-ancestors):
|
|
100
|
+
* the probe verdict shown instead of the blank iframe. */
|
|
101
|
+
const [embedBlocked, setEmbedBlocked] = useState<string | null>(null)
|
|
102
|
+
/** The user asked to load the refused site anyway (keeps the plain iframe). */
|
|
103
|
+
const [forceEmbed, setForceEmbed] = useState(false)
|
|
104
|
+
|
|
105
|
+
// Probe every navigation (address bar, history, restored path): when the
|
|
106
|
+
// target forbids embedding, show the reason + open-in-browser instead of
|
|
107
|
+
// the browser's cryptic "refused to connect" blank frame. A failed probe
|
|
108
|
+
// (unreachable) keeps the plain iframe.
|
|
109
|
+
useEffect(() => {
|
|
110
|
+
if (url === undefined) return
|
|
111
|
+
let cancelled = false
|
|
112
|
+
setEmbedBlocked(null)
|
|
113
|
+
setForceEmbed(false)
|
|
114
|
+
void api.browserProbe(url).then((probe) => {
|
|
115
|
+
if (!cancelled && embeddabilityOf(probe) === 'blocked') setEmbedBlocked(url)
|
|
116
|
+
}).catch(() => { /* unreachable: keep the plain iframe */ })
|
|
117
|
+
return () => { cancelled = true }
|
|
118
|
+
}, [url])
|
|
119
|
+
|
|
120
|
+
const persist = (nextUrl: string): void => {
|
|
121
|
+
let host = nextUrl
|
|
122
|
+
try { host = new URL(nextUrl).hostname } catch { /* keep the URL as title */ }
|
|
123
|
+
store.reduce(state => patchTab(state, tab.id, { path: nextUrl, title: host }))
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const navigateTo = (raw: string): void => {
|
|
127
|
+
const result = normalizeBrowserUrl(raw, window.location.origin, store.getPrefs().browserAllowedLoopback)
|
|
128
|
+
if (result.kind === 'ok') {
|
|
129
|
+
const next = result.url
|
|
130
|
+
setUrl(next)
|
|
131
|
+
setInput(next)
|
|
132
|
+
setMessage(null)
|
|
133
|
+
// Push onto the stack, dropping any stale forward entries.
|
|
134
|
+
setHistory(previous => [...previous.slice(0, cursor + 1), next])
|
|
135
|
+
setCursor(previous => previous + 1)
|
|
136
|
+
setReloadKey(key => key + 1)
|
|
137
|
+
persist(next)
|
|
138
|
+
return
|
|
139
|
+
}
|
|
140
|
+
setMessage(result.kind === 'invalid'
|
|
141
|
+
? t('browserInvalid')
|
|
142
|
+
: result.reason === 'scheme' ? t('browserBlockedScheme')
|
|
143
|
+
: t('browserBlockedLoopback'))
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const goBack = (): void => {
|
|
147
|
+
if (cursor <= 0) return
|
|
148
|
+
const next = history[cursor - 1]!
|
|
149
|
+
setCursor(cursor - 1)
|
|
150
|
+
setUrl(next)
|
|
151
|
+
setInput(next)
|
|
152
|
+
setReloadKey(key => key + 1)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const goForward = (): void => {
|
|
156
|
+
if (cursor >= history.length - 1) return
|
|
157
|
+
const next = history[cursor + 1]!
|
|
158
|
+
setCursor(cursor + 1)
|
|
159
|
+
setUrl(next)
|
|
160
|
+
setInput(next)
|
|
161
|
+
setReloadKey(key => key + 1)
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return (
|
|
165
|
+
<div className={css.browser}>
|
|
166
|
+
<div className={css.browserBar}>
|
|
167
|
+
<button
|
|
168
|
+
type="button"
|
|
169
|
+
className={css.iconButton}
|
|
170
|
+
aria-label={t('browserBack')}
|
|
171
|
+
title={t('browserBack')}
|
|
172
|
+
disabled={cursor <= 0}
|
|
173
|
+
onClick={goBack}
|
|
174
|
+
>
|
|
175
|
+
<IconChevronLeftOutline14 />
|
|
176
|
+
</button>
|
|
177
|
+
<button
|
|
178
|
+
type="button"
|
|
179
|
+
className={css.iconButton}
|
|
180
|
+
aria-label={t('browserForward')}
|
|
181
|
+
title={t('browserForward')}
|
|
182
|
+
disabled={cursor >= history.length - 1}
|
|
183
|
+
onClick={goForward}
|
|
184
|
+
>
|
|
185
|
+
<IconChevronRightOutline14 />
|
|
186
|
+
</button>
|
|
187
|
+
<button
|
|
188
|
+
type="button"
|
|
189
|
+
className={css.iconButton}
|
|
190
|
+
aria-label={t('refresh')}
|
|
191
|
+
title={t('refresh')}
|
|
192
|
+
onClick={() => { setReloadKey(key => key + 1) }}
|
|
193
|
+
>
|
|
194
|
+
<IconRefreshOutline14 />
|
|
195
|
+
</button>
|
|
196
|
+
<input
|
|
197
|
+
className={css.browserInput}
|
|
198
|
+
value={input}
|
|
199
|
+
placeholder={t('browserPlaceholder')}
|
|
200
|
+
spellCheck={false}
|
|
201
|
+
onChange={event => { setInput(event.target.value) }}
|
|
202
|
+
onKeyDown={event => {
|
|
203
|
+
if (event.key === 'Enter') navigateTo(input)
|
|
204
|
+
}}
|
|
205
|
+
/>
|
|
206
|
+
<button
|
|
207
|
+
type="button"
|
|
208
|
+
className={css.iconButton}
|
|
209
|
+
aria-label={t('browserGo')}
|
|
210
|
+
title={t('browserGo')}
|
|
211
|
+
onClick={() => { navigateTo(input) }}
|
|
212
|
+
>
|
|
213
|
+
<IconLinkOutline14 />
|
|
214
|
+
</button>
|
|
215
|
+
<button
|
|
216
|
+
type="button"
|
|
217
|
+
className={css.iconButton}
|
|
218
|
+
aria-label={t('browserOpenExternal')}
|
|
219
|
+
title={t('browserOpenExternal')}
|
|
220
|
+
disabled={url === undefined}
|
|
221
|
+
onClick={() => {
|
|
222
|
+
if (url !== undefined) window.open(url, '_blank', 'noopener')
|
|
223
|
+
}}
|
|
224
|
+
>
|
|
225
|
+
<VscLinkExternal size={15} />
|
|
226
|
+
</button>
|
|
227
|
+
</div>
|
|
228
|
+
{message !== null && <div className={css.browserMessage}>{message}</div>}
|
|
229
|
+
<SandboxStatusBar
|
|
230
|
+
sandboxed={!noSandbox}
|
|
231
|
+
local={localUnlock}
|
|
232
|
+
dangerCopy={t('browserNoSandboxWarning')}
|
|
233
|
+
onUnlock={() => { setLocalUnlock(true) }}
|
|
234
|
+
onRestore={() => { setLocalUnlock(false) }}
|
|
235
|
+
/>
|
|
236
|
+
{url === undefined ? (
|
|
237
|
+
<div className={css.browserStart}>{t('browserStart')}</div>
|
|
238
|
+
) : embedBlocked !== null && !forceEmbed ? (
|
|
239
|
+
<BrowserEmbedBlocked
|
|
240
|
+
url={embedBlocked}
|
|
241
|
+
onOpenInBrowser={() => { window.open(embedBlocked, '_blank', 'noopener') }}
|
|
242
|
+
onLoadAnyway={() => { setForceEmbed(true) }}
|
|
243
|
+
/>
|
|
244
|
+
) : (
|
|
245
|
+
<iframe
|
|
246
|
+
key={`${reloadKey}:${noSandbox ? 'ns' : 'sb'}`}
|
|
247
|
+
className={css.browserFrame}
|
|
248
|
+
src={url}
|
|
249
|
+
sandbox={noSandbox ? undefined : iframeSandboxFor(url, store.getPrefs().browserAllowedLoopback, window.location.origin)}
|
|
250
|
+
referrerPolicy="no-referrer"
|
|
251
|
+
allow=""
|
|
252
|
+
title={url}
|
|
253
|
+
/>
|
|
254
|
+
)}
|
|
255
|
+
</div>
|
|
256
|
+
)
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* The embed-refusal panel: shown when the probed site forbids being
|
|
261
|
+
* displayed inside other pages (X-Frame-Options / frame-ancestors) — the
|
|
262
|
+
* iframe would only show the browser's "refused to connect" blank. Explains
|
|
263
|
+
* the reason and offers the real-browser open plus a load-anyway escape.
|
|
264
|
+
* Exported so the copy and the actions are testable without a DOM.
|
|
265
|
+
*/
|
|
266
|
+
export function BrowserEmbedBlocked(props: {
|
|
267
|
+
url: string
|
|
268
|
+
onOpenInBrowser: () => void
|
|
269
|
+
onLoadAnyway: () => void
|
|
270
|
+
}) {
|
|
271
|
+
const { url, onOpenInBrowser, onLoadAnyway } = props
|
|
272
|
+
let host = url
|
|
273
|
+
try { host = new URL(url).hostname } catch { /* keep the raw URL */ }
|
|
274
|
+
return (
|
|
275
|
+
<div className={css.browserBlocked}>
|
|
276
|
+
<IconWarningOutline16 size={16} />
|
|
277
|
+
<div className={css.browserBlockedTitle}>{t('browserEmbedBlocked', { host })}</div>
|
|
278
|
+
<div className={css.browserBlockedDesc}>{t('browserEmbedBlockedDesc')}</div>
|
|
279
|
+
<div className={css.browserBlockedActions}>
|
|
280
|
+
<button type="button" className={css.browserBlockedButton} onClick={onOpenInBrowser}>
|
|
281
|
+
{t('browserOpenExternal')}
|
|
282
|
+
</button>
|
|
283
|
+
<button type="button" className={css.browserBlockedButton} onClick={onLoadAnyway}>
|
|
284
|
+
{t('browserEmbedAnyway')}
|
|
285
|
+
</button>
|
|
286
|
+
</div>
|
|
287
|
+
</div>
|
|
288
|
+
)
|
|
289
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/** Mandatory Web sign-in surface. The host remains the authority for every decision. */
|
|
2
|
+
import { useEffect, useRef } from 'react'
|
|
3
|
+
import type { ChatCodeAuthState } from './api.ts'
|
|
4
|
+
import { useChatcodeAccount } from './useChatcodeAccount.ts'
|
|
5
|
+
|
|
6
|
+
type GateDecision = 'allow' | 'login' | 'wait'
|
|
7
|
+
|
|
8
|
+
/** Decide whether this browser visit has enough verified ChatCode state to enter the workbench. */
|
|
9
|
+
export function chatcodeGateDecision(chatcode: ChatCodeAuthState | undefined, verifiedThisVisit: boolean): GateDecision {
|
|
10
|
+
if (chatcode?.available === false || chatcode?.required !== true) return 'allow'
|
|
11
|
+
if (chatcode.validation === 'valid' && chatcode.expired !== true) return 'allow'
|
|
12
|
+
if (chatcode.validation === 'unavailable') return verifiedThisVisit ? 'allow' : 'login'
|
|
13
|
+
if (chatcode.expired === true || chatcode.validation === 'invalid'
|
|
14
|
+
|| (chatcode.configured === false && chatcode.validation === 'none')) return 'login'
|
|
15
|
+
return 'wait'
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function ChatcodeGate(): JSX.Element | null {
|
|
19
|
+
const { chatcode } = useChatcodeAccount()
|
|
20
|
+
const verifiedThisVisit = useRef(false)
|
|
21
|
+
// The auth bundle is optional for standalone better-sidebar consumers.
|
|
22
|
+
// Its route always answers available=false when no auth bundle is mounted,
|
|
23
|
+
// so keep the first frame covered until that answer arrives. This prevents
|
|
24
|
+
// the application flashing usable controls before mandatory auth is known.
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
if (chatcode?.validation === 'valid' && chatcode.expired !== true) verifiedThisVisit.current = true
|
|
27
|
+
// A new browser visit must observe a successful remote validation. Once
|
|
28
|
+
// this visit has passed that check, a later company-network outage does
|
|
29
|
+
// not interrupt ongoing work.
|
|
30
|
+
if (chatcodeGateDecision(chatcode, verifiedThisVisit.current) !== 'login') return
|
|
31
|
+
const back = `${window.location.pathname}${window.location.search}${window.location.hash}`
|
|
32
|
+
window.location.replace(`/sidebar/login?return=${encodeURIComponent(back)}`)
|
|
33
|
+
}, [chatcode])
|
|
34
|
+
return null
|
|
35
|
+
}
|