@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,281 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-ui-tweaks — Archive manager (browser half).
|
|
3
|
+
*
|
|
4
|
+
* Renders the "归档" settings section (shown in the Settings dialog; usable
|
|
5
|
+
* when the `archiveManagerEnabled` toggle in 界面调整 is on — otherwise it
|
|
6
|
+
* shows an invite card with a one-click enable). The section lists every
|
|
7
|
+
* archived session — enriched from the framework `useSessions` /
|
|
8
|
+
* `useWorkspaces` feeds — with per-row actions and batch actions that go
|
|
9
|
+
* through the same-origin archive route (`/_dsh/ui-tweaks/archive`):
|
|
10
|
+
*
|
|
11
|
+
* - **恢复** (Restore): remove the session from the archive set — its log and
|
|
12
|
+
* workspace slot are kept, so the conversation reappears in the normal
|
|
13
|
+
* sidebar list.
|
|
14
|
+
* - **删除** (Delete): PERMANENTLY delete the session — the server removes its
|
|
15
|
+
* durable log, workspace accounting, archive-set entry and caches; the list
|
|
16
|
+
* then refreshes so the row (and the session itself) disappear.
|
|
17
|
+
* - **全部恢复 / 全部删除**: the same actions for every archived session.
|
|
18
|
+
*
|
|
19
|
+
* Live (open/running) sessions are refused by the server; the client maps the
|
|
20
|
+
* `session-live` error code to localized copy.
|
|
21
|
+
* @module dsh-ui-tweaks/client/archive
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { useEffect, useMemo, useState, useSyncExternalStore } from 'react'
|
|
25
|
+
import type { ISessions, SessionListState, SessionSummary } from '@deepseek-ai/dsh-api-session-controller/client'
|
|
26
|
+
import type { WorkspaceSnapshot } from '@deepseek-ai/dsh-api-workspace-controller/client'
|
|
27
|
+
import type { SnapshotSelectorHook } from '@deepseek-ai/dsh-client-ui-slots'
|
|
28
|
+
import type { SettingsClient } from './index.tsx'
|
|
29
|
+
|
|
30
|
+
/** Route matching the host half (src/archive.ts). */
|
|
31
|
+
const ARCHIVE_ROUTE = '/_dsh/ui-tweaks/archive'
|
|
32
|
+
|
|
33
|
+
/** Locale keys the Archive manager reads off the `ui-tweaks` dictionary. */
|
|
34
|
+
type ArchiveLabelKey =
|
|
35
|
+
| 'archiveNav' | 'archiveTitle' | 'archiveEmpty' | 'archiveRestore' | 'archiveRestoring'
|
|
36
|
+
| 'archiveDelete' | 'archiveDeleteAll' | 'archiveRestoreAll'
|
|
37
|
+
| 'archiveCount' | 'archiveUnavailable' | 'archiveLiveError' | 'archiveDisabledHint'
|
|
38
|
+
| 'archiveEnable' | 'archiveRestored' | 'unavailable'
|
|
39
|
+
|
|
40
|
+
type Translate = (key: ArchiveLabelKey) => string
|
|
41
|
+
|
|
42
|
+
interface ApiSuccess<T> { ok: true; value: T }
|
|
43
|
+
interface ApiFailure { ok: false; error: { code: string; message: string } }
|
|
44
|
+
|
|
45
|
+
interface ArchiveSnapshot {
|
|
46
|
+
archivedSessionIds: string[]
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Fetch one archive route call, attaching the server error code when present. */
|
|
50
|
+
async function archiveRequest<T>(init?: RequestInit): Promise<T> {
|
|
51
|
+
const response = await fetch(ARCHIVE_ROUTE, { credentials: 'same-origin', ...init })
|
|
52
|
+
let body: ApiSuccess<T> | ApiFailure | undefined
|
|
53
|
+
try {
|
|
54
|
+
body = await response.json() as ApiSuccess<T> | ApiFailure
|
|
55
|
+
} catch {
|
|
56
|
+
// Non-JSON response (e.g. the route is not mounted yet) — fall through to
|
|
57
|
+
// a clean error instead of a JSON-parse exception.
|
|
58
|
+
body = undefined
|
|
59
|
+
}
|
|
60
|
+
if (!response.ok || body === undefined || !body.ok) {
|
|
61
|
+
const failure = body as ApiFailure | undefined
|
|
62
|
+
const error = new Error(failure?.error?.message ?? `UI Tweaks archive request failed with HTTP ${response.status}`) as Error & { code: string | undefined }
|
|
63
|
+
error.code = failure?.error?.code
|
|
64
|
+
throw error
|
|
65
|
+
}
|
|
66
|
+
return body.value
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Map a thrown archive error to user-facing copy. */
|
|
70
|
+
function errorMessage(error: unknown, t: Translate): string {
|
|
71
|
+
const code = (error as { code?: string } | null)?.code
|
|
72
|
+
if (code === 'session-live') return t('archiveLiveError')
|
|
73
|
+
return error instanceof Error ? error.message : String(error)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Re-pull the host session list after a permanent delete (the outward
|
|
77
|
+
* ISessions face omits refresh; the concrete runtime provides it). */
|
|
78
|
+
function refreshSessions(sessionsService: ISessions): void {
|
|
79
|
+
void (sessionsService as unknown as { refresh(): Promise<void> }).refresh()
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Workspace display title: the path's last non-empty segment. */
|
|
83
|
+
function basenameOf(path: string | undefined): string {
|
|
84
|
+
if (path === undefined || path === '') return ''
|
|
85
|
+
const cleaned = path.replace(/[\\/]+$/u, '')
|
|
86
|
+
const segments = cleaned.split(/[\\/]/u)
|
|
87
|
+
return segments.at(-1) ?? ''
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Locale-aware relative time for a past timestamp, e.g. "3小时前". */
|
|
91
|
+
function relativeTime(ms: number, now: number, formatter: Intl.RelativeTimeFormat): string {
|
|
92
|
+
const diffSeconds = Math.round((ms - now) / 1000)
|
|
93
|
+
const abs = Math.abs(diffSeconds)
|
|
94
|
+
if (abs < 60) return formatter.format(diffSeconds, 'second')
|
|
95
|
+
if (abs < 3600) return formatter.format(Math.round(diffSeconds / 60), 'minute')
|
|
96
|
+
if (abs < 86400) return formatter.format(Math.round(diffSeconds / 3600), 'hour')
|
|
97
|
+
if (abs < 604800) return formatter.format(Math.round(diffSeconds / 86400), 'day')
|
|
98
|
+
return formatter.format(Math.round(diffSeconds / 604800), 'week')
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export const ARCHIVE_CSS = `
|
|
102
|
+
.dut-arc{display:grid;gap:12px;max-width:680px;padding:6px 2px 36px}
|
|
103
|
+
.dut-arc-head{display:flex;align-items:center;gap:10px;padding:14px 16px 0}
|
|
104
|
+
.dut-arc-head h2{margin:0;font-size:15px;font-weight:600;letter-spacing:-.01em;color:var(--dsw-alias-label-primary)}
|
|
105
|
+
.dut-arc-count{font-size:11.5px;color:var(--dsw-alias-label-secondary);padding:2px 8px;border-radius:999px;background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l1)}
|
|
106
|
+
.dut-arc-spacer{flex:1}
|
|
107
|
+
.dut-arc-btn{display:inline-flex;align-items:center;height:28px;padding:0 12px;border-radius:999px;border:1px solid var(--dsw-alias-border-l1);background:transparent;color:var(--dsw-alias-label-secondary);font:inherit;font-size:12px;cursor:pointer;transition:background .15s ease,color .15s ease,border-color .15s ease}
|
|
108
|
+
.dut-arc-btn:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}
|
|
109
|
+
.dut-arc-btn:disabled{opacity:.5;cursor:default}
|
|
110
|
+
.dut-arc-btn.dut-arc-del:hover{border-color:color-mix(in srgb,var(--dsw-alias-state-error-primary) 55%,transparent);color:var(--dsw-alias-state-error-primary);background:color-mix(in srgb,var(--dsw-alias-state-error-primary) 10%,transparent)}
|
|
111
|
+
.dut-arc-btn.dut-arc-confirm{border-color:color-mix(in srgb,var(--dsw-alias-state-error-primary) 55%,transparent);color:var(--dsw-alias-state-error-primary);font-weight:600}
|
|
112
|
+
.dut-arc-body{display:grid;gap:2px;margin-top:4px}
|
|
113
|
+
.dut-arc-row{display:flex;align-items:center;gap:8px;padding:10px 16px;border-radius:12px}
|
|
114
|
+
.dut-arc-row:hover{background:var(--dsw-alias-interactive-bg-hover)}
|
|
115
|
+
.dut-arc-main{flex:1;min-width:0;display:flex;flex-direction:column;gap:2px;text-align:left;background:transparent;border:none;padding:0;cursor:default;color:var(--dsw-alias-label-primary);font:inherit}
|
|
116
|
+
.dut-arc-title{font-size:13.5px;font-weight:600;white-space:nowrap;text-overflow:ellipsis;overflow:hidden}
|
|
117
|
+
.dut-arc-sub{font-size:11.5px;color:var(--dsw-alias-label-secondary);white-space:nowrap;text-overflow:ellipsis;overflow:hidden}
|
|
118
|
+
.dut-arc-empty{padding:32px 16px;text-align:center;font-size:12.5px;color:var(--dsw-alias-label-secondary)}
|
|
119
|
+
.dut-arc-alert{margin:8px 16px 0;padding:8px 12px;border-radius:10px;font-size:12px;line-height:1.5;background:color-mix(in srgb,var(--dsw-alias-state-error-primary) 10%,transparent);color:var(--dsw-alias-state-error-primary)}
|
|
120
|
+
.dut-arc-off{padding:20px 16px;display:grid;gap:12px}
|
|
121
|
+
.dut-arc-off p{margin:0;font-size:12.5px;line-height:1.6;color:var(--dsw-alias-label-secondary)}
|
|
122
|
+
`
|
|
123
|
+
|
|
124
|
+
/** Install the Archive stylesheet once (idempotent); returns the disposer. */
|
|
125
|
+
export function installArchiveStyles(): () => void {
|
|
126
|
+
const id = 'dsh-ui-tweaks-archive'
|
|
127
|
+
const existing = document.querySelector(`style[data-plugin-css="${id}"]`)
|
|
128
|
+
if (existing !== null) return () => {}
|
|
129
|
+
const style = document.createElement('style')
|
|
130
|
+
style.dataset.plugin = 'dsh-ui-tweaks'
|
|
131
|
+
style.dataset.pluginCss = id
|
|
132
|
+
style.textContent = ARCHIVE_CSS
|
|
133
|
+
document.head.appendChild(style)
|
|
134
|
+
return () => { style.remove() }
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** One archived session row: the id plus the enriched session summary, when listed. */
|
|
138
|
+
interface ArchiveRow {
|
|
139
|
+
id: string
|
|
140
|
+
summary: SessionSummary | undefined
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export interface ArchiveSectionProps {
|
|
144
|
+
controller: SettingsClient
|
|
145
|
+
t: Translate
|
|
146
|
+
sessionsService: ISessions
|
|
147
|
+
/** Framework standard feeds: session list + workspace archive set. */
|
|
148
|
+
useSessions: SnapshotSelectorHook<SessionListState>
|
|
149
|
+
useWorkspaces: SnapshotSelectorHook<WorkspaceSnapshot>
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export function ArchiveSection({ controller, t, sessionsService, useSessions, useWorkspaces }: ArchiveSectionProps) {
|
|
153
|
+
const settingsState = useSyncExternalStore(controller.subscribe, controller.getSnapshot, controller.getSnapshot)
|
|
154
|
+
const enabled = settingsState.value?.archiveManagerEnabled === true
|
|
155
|
+
const sessions = useSessions(state => state)
|
|
156
|
+
const workspaces = useWorkspaces(state => state)
|
|
157
|
+
const [confirmId, setConfirmId] = useState<string | null>(null)
|
|
158
|
+
const [confirmAll, setConfirmAll] = useState(false)
|
|
159
|
+
const [busy, setBusy] = useState<string | null>(null)
|
|
160
|
+
const [error, setError] = useState<string | null>(null)
|
|
161
|
+
const timeFormatter = useMemo(() => new Intl.RelativeTimeFormat(navigator.language, { numeric: 'auto' }), [])
|
|
162
|
+
const now = Date.now()
|
|
163
|
+
|
|
164
|
+
useEffect(() => {
|
|
165
|
+
if (confirmId === null) return
|
|
166
|
+
const timer = window.setTimeout(() => { setConfirmId(current => (current === confirmId ? null : current)) }, 3000)
|
|
167
|
+
return () => { window.clearTimeout(timer) }
|
|
168
|
+
}, [confirmId])
|
|
169
|
+
useEffect(() => {
|
|
170
|
+
if (!confirmAll) return
|
|
171
|
+
const timer = window.setTimeout(() => { setConfirmAll(false) }, 3000)
|
|
172
|
+
return () => { window.clearTimeout(timer) }
|
|
173
|
+
}, [confirmAll])
|
|
174
|
+
|
|
175
|
+
const rows: ArchiveRow[] = useMemo(() => {
|
|
176
|
+
const byId = sessions.byId
|
|
177
|
+
return workspaces.archivedSessionIds.map(id => ({ id, summary: byId[id] })).sort((a, b) => {
|
|
178
|
+
const ta = a.summary?.updatedAt ?? 0
|
|
179
|
+
const tb = b.summary?.updatedAt ?? 0
|
|
180
|
+
return tb - ta
|
|
181
|
+
})
|
|
182
|
+
}, [workspaces.archivedSessionIds, sessions.byId])
|
|
183
|
+
|
|
184
|
+
const run = (key: string, body: unknown, after?: () => void): void => {
|
|
185
|
+
setBusy(key)
|
|
186
|
+
setError(null)
|
|
187
|
+
void archiveRequest<ArchiveSnapshot>({
|
|
188
|
+
method: 'POST',
|
|
189
|
+
headers: { 'Content-Type': 'application/json' },
|
|
190
|
+
body: JSON.stringify(body),
|
|
191
|
+
}).then(() => { after?.() }).catch((reason: unknown) => {
|
|
192
|
+
setError(errorMessage(reason, t))
|
|
193
|
+
}).finally(() => { setBusy(current => (current === key ? null : current)) })
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const restore = (id: string): void => {
|
|
197
|
+
run(`restore:${id}`, { action: 'restore', sessionId: id })
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const remove = (id: string): void => {
|
|
201
|
+
if (confirmId !== id) {
|
|
202
|
+
setConfirmId(id)
|
|
203
|
+
return
|
|
204
|
+
}
|
|
205
|
+
setConfirmId(null)
|
|
206
|
+
run(`delete:${id}`, { action: 'delete', sessionId: id }, () => { refreshSessions(sessionsService) })
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const restoreAll = (): void => {
|
|
210
|
+
run('restore-all', { action: 'restore-all' })
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const removeAll = (): void => {
|
|
214
|
+
if (!confirmAll) {
|
|
215
|
+
setConfirmAll(true)
|
|
216
|
+
return
|
|
217
|
+
}
|
|
218
|
+
setConfirmAll(false)
|
|
219
|
+
run('delete-all', { action: 'delete-all' }, () => { refreshSessions(sessionsService) })
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (!enabled) {
|
|
223
|
+
return (
|
|
224
|
+
<div className="dut-arc">
|
|
225
|
+
<div className="dut-panel dut-arc-off">
|
|
226
|
+
<p>{t('archiveDisabledHint')}</p>
|
|
227
|
+
<div>
|
|
228
|
+
<button type="button" className="dut-btn dut-btn-active" onClick={() => {
|
|
229
|
+
void controller.set('archiveManagerEnabled', true).catch(() => { setError(t('unavailable')) })
|
|
230
|
+
}}>{t('archiveEnable')}</button>
|
|
231
|
+
</div>
|
|
232
|
+
</div>
|
|
233
|
+
</div>
|
|
234
|
+
)
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return (
|
|
238
|
+
<div className="dut-arc">
|
|
239
|
+
<div className="dut-panel">
|
|
240
|
+
<div className="dut-arc-head">
|
|
241
|
+
<h2>{t('archiveTitle')}</h2>
|
|
242
|
+
<span className="dut-arc-count">{rows.length} {t('archiveCount')}</span>
|
|
243
|
+
<span className="dut-arc-spacer" />
|
|
244
|
+
{rows.length > 1 ? (
|
|
245
|
+
<>
|
|
246
|
+
<button type="button" className="dut-arc-btn" disabled={busy !== null} onClick={restoreAll}>{busy === 'restore-all' ? t('archiveRestoring') : t('archiveRestoreAll')}</button>
|
|
247
|
+
<button type="button" className={'dut-arc-btn dut-arc-del' + (confirmAll ? ' dut-arc-confirm' : '')} disabled={busy !== null} onClick={removeAll}>{busy === 'delete-all' ? t('archiveDeleteAll') + '…' : confirmAll ? t('archiveDeleteAll') + '?' : t('archiveDeleteAll')}</button>
|
|
248
|
+
</>
|
|
249
|
+
) : null}
|
|
250
|
+
</div>
|
|
251
|
+
{error !== null ? <div className="dut-arc-alert">{error}</div> : null}
|
|
252
|
+
<div className="dut-arc-body">
|
|
253
|
+
{rows.length === 0 ? (
|
|
254
|
+
<div className="dut-arc-empty">{t('archiveEmpty')}</div>
|
|
255
|
+
) : rows.map(row => {
|
|
256
|
+
const summary = row.summary
|
|
257
|
+
const title = summary?.displayTitle ?? row.id
|
|
258
|
+
const sub = [basenameOf(summary?.cwd), summary !== undefined ? relativeTime(summary.updatedAt, now, timeFormatter) : ''].filter(Boolean).join(' · ')
|
|
259
|
+
return (
|
|
260
|
+
<div className="dut-arc-row" key={row.id}>
|
|
261
|
+
<div className="dut-arc-main" title={title}>
|
|
262
|
+
<span className="dut-arc-title">{title}</span>
|
|
263
|
+
{sub !== '' ? <span className="dut-arc-sub">{sub}</span> : null}
|
|
264
|
+
</div>
|
|
265
|
+
<button type="button" className="dut-arc-btn" disabled={busy !== null} onClick={() => { restore(row.id) }}>{busy === `restore:${row.id}` ? t('archiveRestoring') : t('archiveRestore')}</button>
|
|
266
|
+
<button
|
|
267
|
+
type="button"
|
|
268
|
+
className={'dut-arc-btn dut-arc-del' + (confirmId === row.id ? ' dut-arc-confirm' : '')}
|
|
269
|
+
disabled={busy !== null}
|
|
270
|
+
onClick={() => { remove(row.id) }}
|
|
271
|
+
>
|
|
272
|
+
{busy === `delete:${row.id}` ? t('archiveDelete') + '…' : confirmId === row.id ? t('archiveDelete') + '?' : t('archiveDelete')}
|
|
273
|
+
</button>
|
|
274
|
+
</div>
|
|
275
|
+
)
|
|
276
|
+
})}
|
|
277
|
+
</div>
|
|
278
|
+
</div>
|
|
279
|
+
</div>
|
|
280
|
+
)
|
|
281
|
+
}
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ChatCode CLI visual identity.
|
|
3
|
+
*
|
|
4
|
+
* The host owns its component tree, so this module deliberately uses stable
|
|
5
|
+
* semantics (roles, composer data attributes and visible labels) instead of
|
|
6
|
+
* the host's CSS-module hashes. A small observer reapplies the few DOM
|
|
7
|
+
* affordances after the host swaps a session or rerenders its sidebar.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { ReactNode } from 'react'
|
|
11
|
+
|
|
12
|
+
declare module '@deepseek-ai/dsh-client-ui-slots' {
|
|
13
|
+
interface SlotMap {
|
|
14
|
+
'sidebar.brand.mark': {
|
|
15
|
+
kind: 'single'
|
|
16
|
+
scope: 'root'
|
|
17
|
+
owner: { size: number }
|
|
18
|
+
}
|
|
19
|
+
'sidebar.brand.name': {
|
|
20
|
+
kind: 'single'
|
|
21
|
+
scope: 'root'
|
|
22
|
+
owner: Record<never, never>
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Browser URL for the project-owned `assets/logo.svg` file. */
|
|
28
|
+
export const PROJECT_LOGO_URL = '/_dsh/ui-tweaks/assets/logo.svg'
|
|
29
|
+
const PROJECT_NAME = 'ChatCode CLI'
|
|
30
|
+
const LEGACY_HOST_BRAND_TEXT = /DSH\s*(?:Local\s*Build|本地构建)|ChatCode\s*CLI/giu
|
|
31
|
+
const HOST_BRAND_LABEL_TEXT = /^(?:DSH\s*(?:Local\s*Build|本地构建)|ChatCode\s*CLI)$/iu
|
|
32
|
+
const HOST_BUILD_VERSION_TEXT = /^v?\d+\.\d+\.\d+(?:[-+][0-9a-z.-]+)*$/iu
|
|
33
|
+
const HERO_HEADLINE = /探索未至之境/u
|
|
34
|
+
const HERO_PREVIEW_TEXT = /^(?:预览版|Preview)$/iu
|
|
35
|
+
const FAVICON_MARKER = 'data-dut-project-favicon'
|
|
36
|
+
|
|
37
|
+
/** Reusable rendering of the packaged project logo. */
|
|
38
|
+
export function ProjectLogoIcon({ className = '' }: { className?: string }): ReactNode {
|
|
39
|
+
return (
|
|
40
|
+
<img className={className} src={PROJECT_LOGO_URL} alt="" aria-hidden="true" />
|
|
41
|
+
)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** New-session brand-mark slot supplied by the conversation package. */
|
|
45
|
+
export function HeroProjectLogo({ size, className }: { size: number, className?: string | undefined }): ReactNode {
|
|
46
|
+
return <img className={`${className ?? ''} dut-project-logo dut-hero-logo`} src={PROJECT_LOGO_URL} width={size} height={size} alt="" aria-hidden="true" />
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Sidebar mark shared by the expanded header and the collapsed rail. */
|
|
50
|
+
export function SidebarProjectLogo({ size }: { size: number }): ReactNode {
|
|
51
|
+
return <img className="dut-project-logo dut-sidebar-logo" src={PROJECT_LOGO_URL} width={size} height={size} alt="" aria-hidden="true" />
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Expanded-sidebar wordmark supplied through the host's dedicated slot. */
|
|
55
|
+
export function SidebarProjectName(): ReactNode {
|
|
56
|
+
return <span className="dut-sidebar-brand-name">ChatCode CLI</span>
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export const BRANDING_CSS = `
|
|
60
|
+
/* Keep the light-theme type hierarchy in ChatCode's restrained red family,
|
|
61
|
+
while leaving every surface and interaction state (including workspace /
|
|
62
|
+
sidebar hover and selected fills, menus, the composer and shadows) owned
|
|
63
|
+
by the host theme. */
|
|
64
|
+
body:not([data-ds-dark-theme]){
|
|
65
|
+
--dsw-alias-label-primary:#301111;
|
|
66
|
+
--dsw-alias-label-secondary:#754745;
|
|
67
|
+
--dsw-alias-label-tertiary:#a77471;
|
|
68
|
+
}
|
|
69
|
+
/* Saturated brand red remains exclusive to explicitly marked primary actions. */
|
|
70
|
+
body:not([data-ds-dark-theme]) .dut-unicom-primary-action{background:linear-gradient(135deg,#9B0B09,#b71916) !important;border-color:#9B0B09 !important;color:#fff !important;box-shadow:0 7px 18px rgba(155,11,9,.24) !important}
|
|
71
|
+
body:not([data-ds-dark-theme]) .dut-unicom-primary-action:hover{background:linear-gradient(135deg,#b71916,#cf302c) !important;border-color:#b71916 !important;color:#fff !important;box-shadow:0 10px 24px rgba(155,11,9,.30) !important}
|
|
72
|
+
body:not([data-ds-dark-theme]) .dut-unicom-primary-action:active{transform:translateY(1px);box-shadow:0 4px 11px rgba(155,11,9,.22) !important}
|
|
73
|
+
/* In dark mode, alter only the resting background of primary buttons. Once
|
|
74
|
+
hovered, the host owns the complete interaction treatment again. */
|
|
75
|
+
body[data-ds-dark-theme] .dut-unicom-primary-action:not(:hover){background:#9B0B09 !important}
|
|
76
|
+
.dut-project-logo{display:block;object-fit:contain}
|
|
77
|
+
.dut-left-logo{width:44px !important;height:44px !important}
|
|
78
|
+
.dut-brand-title{font-size:18px !important;font-weight:600 !important}
|
|
79
|
+
.dut-sidebar-logo{width:44px !important;height:44px !important}
|
|
80
|
+
/* The alpha sidebar keeps its slotted identity at 24px and clips the brand
|
|
81
|
+
button. Give only our expanded identity the full artwork height so the
|
|
82
|
+
44px knot is not cropped by that host-owned overflow boundary. */
|
|
83
|
+
[class*="brandIdentity"]:has(.dut-sidebar-logo){height:44px !important}
|
|
84
|
+
.dut-sidebar-brand-name{font-size:18px !important;line-height:22px !important;font-weight:600 !important;letter-spacing:0 !important;white-space:nowrap}
|
|
85
|
+
.dut-brand-version{display:none !important}
|
|
86
|
+
.dut-hero-preview{display:none !important}
|
|
87
|
+
.dut-hero-logo{width:60px !important;height:60px !important}
|
|
88
|
+
[class*="headline"]:has(.dut-hero-logo){grid-template-columns:60px auto !important;font-size:26px !important;line-height:34px !important}
|
|
89
|
+
.dut-settings-logo{width:19px;height:19px;object-fit:contain}
|
|
90
|
+
`
|
|
91
|
+
|
|
92
|
+
function hasPrimaryLabel(button: HTMLButtonElement): boolean {
|
|
93
|
+
const label = `${button.textContent ?? ''} ${button.getAttribute('aria-label') ?? ''} ${button.title}`.trim().toLowerCase()
|
|
94
|
+
return /(?:^|\s)(new session|send|install)(?:\s|$)|新会话|发送|(?:^|\s)安装(?:\s|$)/u.test(label)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Replace one host SVG while retaining its generated layout class names. */
|
|
98
|
+
function replaceHostLogo(svg: SVGElement): void {
|
|
99
|
+
if (svg.classList.contains('dut-project-logo')) return
|
|
100
|
+
const image = document.createElement('img')
|
|
101
|
+
image.className = `${svg.getAttribute('class') ?? ''} dut-project-logo dut-left-logo`.trim()
|
|
102
|
+
image.src = PROJECT_LOGO_URL
|
|
103
|
+
image.alt = ''
|
|
104
|
+
image.setAttribute('aria-hidden', 'true')
|
|
105
|
+
const width = svg.getAttribute('width')
|
|
106
|
+
const height = svg.getAttribute('height')
|
|
107
|
+
if (width !== null) image.setAttribute('width', width)
|
|
108
|
+
if (height !== null) image.setAttribute('height', height)
|
|
109
|
+
svg.replaceWith(image)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Locate the mark adjacent to an otherwise unstructured sidebar brand label. */
|
|
113
|
+
function replaceLogoBesideBrandLabel(label: HTMLElement): void {
|
|
114
|
+
const nearby = [
|
|
115
|
+
label.previousElementSibling,
|
|
116
|
+
label.parentElement?.querySelector('svg') ?? null,
|
|
117
|
+
]
|
|
118
|
+
for (const candidate of nearby) {
|
|
119
|
+
if (candidate instanceof SVGElement) {
|
|
120
|
+
replaceHostLogo(candidate)
|
|
121
|
+
return
|
|
122
|
+
}
|
|
123
|
+
const svg = candidate?.querySelector('svg')
|
|
124
|
+
if (svg instanceof SVGElement) {
|
|
125
|
+
replaceHostLogo(svg)
|
|
126
|
+
return
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** Hide the host build suffix that shares the sidebar wordmark container. */
|
|
132
|
+
function hideVersionBesideBrandLabel(label: HTMLElement): void {
|
|
133
|
+
const container = label.parentElement
|
|
134
|
+
if (container === null) return
|
|
135
|
+
for (const sibling of container.children) {
|
|
136
|
+
if (sibling === label || !(sibling instanceof HTMLElement)) continue
|
|
137
|
+
if (!HOST_BUILD_VERSION_TEXT.test(sibling.textContent?.trim() ?? '')) continue
|
|
138
|
+
sibling.classList.add('dut-brand-version')
|
|
139
|
+
sibling.setAttribute('aria-hidden', 'true')
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function syncBranding(): void {
|
|
144
|
+
document.title = document.title.replace(LEGACY_HOST_BRAND_TEXT, PROJECT_NAME)
|
|
145
|
+
|
|
146
|
+
// The host deliberately renders the blank-session preview badge as a
|
|
147
|
+
// third headline column. ChatCode is the production identity here, so
|
|
148
|
+
// suppress that host-owned badge and collapse the headline back to two
|
|
149
|
+
// columns (brand mark + product name).
|
|
150
|
+
document.querySelectorAll<HTMLElement>('[class*="previewBadge"]').forEach((badge) => {
|
|
151
|
+
if (!HERO_PREVIEW_TEXT.test(badge.textContent?.trim() ?? '')) return
|
|
152
|
+
badge.classList.add('dut-hero-preview')
|
|
153
|
+
badge.setAttribute('aria-hidden', 'true')
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
// The sidebar label identifies the precise icon to its left. Process it
|
|
157
|
+
// before the generic text pass so the sibling relationship is still intact.
|
|
158
|
+
document.querySelectorAll<HTMLElement>('body *').forEach((element) => {
|
|
159
|
+
if (element.childElementCount !== 0 || !HOST_BRAND_LABEL_TEXT.test(element.textContent?.trim() ?? '')) return
|
|
160
|
+
if (element.textContent !== PROJECT_NAME) element.textContent = PROJECT_NAME
|
|
161
|
+
element.classList.add('dut-brand-title')
|
|
162
|
+
replaceLogoBesideBrandLabel(element)
|
|
163
|
+
hideVersionBesideBrandLabel(element)
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT)
|
|
167
|
+
let node: Text | null
|
|
168
|
+
while ((node = walker.nextNode() as Text | null) !== null) {
|
|
169
|
+
const parent = node.parentElement
|
|
170
|
+
if (parent?.closest('script,style') !== null) continue
|
|
171
|
+
if (LEGACY_HOST_BRAND_TEXT.test(node.data)) {
|
|
172
|
+
LEGACY_HOST_BRAND_TEXT.lastIndex = 0
|
|
173
|
+
node.data = node.data.replace(LEGACY_HOST_BRAND_TEXT, PROJECT_NAME)
|
|
174
|
+
LEGACY_HOST_BRAND_TEXT.lastIndex = 0
|
|
175
|
+
}
|
|
176
|
+
if (HERO_HEADLINE.test(node.data)) node.data = node.data.replace(HERO_HEADLINE, PROJECT_NAME)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Older host builds expose a brand-mark wrapper but no text-bearing sibling.
|
|
180
|
+
document.querySelectorAll<HTMLElement>('[class*="brandMark"]').forEach((host) => {
|
|
181
|
+
const svg = host.matches('svg') ? host : host.querySelector('svg')
|
|
182
|
+
if (svg instanceof SVGElement) replaceHostLogo(svg)
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
document.querySelectorAll<HTMLButtonElement>('button').forEach((button) => {
|
|
186
|
+
button.classList.toggle('dut-unicom-primary-action', hasPrimaryLabel(button))
|
|
187
|
+
})
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** Install a last-declared SVG favicon so browsers prefer it to DSH's whale. */
|
|
191
|
+
function installProjectFavicon(): HTMLLinkElement {
|
|
192
|
+
const existing = document.head.querySelector<HTMLLinkElement>(`link[${FAVICON_MARKER}]`)
|
|
193
|
+
if (existing !== null) return existing
|
|
194
|
+
const icon = document.createElement('link')
|
|
195
|
+
icon.setAttribute(FAVICON_MARKER, '')
|
|
196
|
+
icon.rel = 'icon'
|
|
197
|
+
icon.type = 'image/svg+xml'
|
|
198
|
+
icon.href = PROJECT_LOGO_URL
|
|
199
|
+
document.head.appendChild(icon)
|
|
200
|
+
return icon
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** Install the brand theme and retain it across host navigation/rerenders. */
|
|
204
|
+
export function installBranding(): () => void {
|
|
205
|
+
const id = 'dsh-ui-tweaks-branding'
|
|
206
|
+
const existing = document.querySelector<HTMLStyleElement>(`style[data-plugin-css="${id}"]`)
|
|
207
|
+
if (existing !== null) return () => {}
|
|
208
|
+
const style = document.createElement('style')
|
|
209
|
+
style.dataset.plugin = 'dsh-ui-tweaks'
|
|
210
|
+
style.dataset.pluginCss = id
|
|
211
|
+
style.textContent = BRANDING_CSS
|
|
212
|
+
document.head.appendChild(style)
|
|
213
|
+
const favicon = installProjectFavicon()
|
|
214
|
+
|
|
215
|
+
let frame: number | undefined
|
|
216
|
+
const schedule = (): void => {
|
|
217
|
+
if (frame !== undefined) return
|
|
218
|
+
frame = window.requestAnimationFrame(() => {
|
|
219
|
+
frame = undefined
|
|
220
|
+
syncBranding()
|
|
221
|
+
})
|
|
222
|
+
}
|
|
223
|
+
syncBranding()
|
|
224
|
+
const observer = new MutationObserver(schedule)
|
|
225
|
+
observer.observe(document.body, { childList: true, characterData: true, subtree: true })
|
|
226
|
+
return () => {
|
|
227
|
+
observer.disconnect()
|
|
228
|
+
if (frame !== undefined) window.cancelAnimationFrame(frame)
|
|
229
|
+
favicon.remove()
|
|
230
|
+
style.remove()
|
|
231
|
+
}
|
|
232
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-ui-tweaks — precise cache-hit readout (browser half).
|
|
3
|
+
*
|
|
4
|
+
* Rewrites the cache-hit figure in the composer stats line under the input
|
|
5
|
+
* box ("缓存命中 96%" / "Cache hit 96%") to two decimal places ("96.35%").
|
|
6
|
+
* DSH rounds the share to an integer before display (adding decimals only to
|
|
7
|
+
* avoid a false 100%); the exact figure needs the raw token buckets, which
|
|
8
|
+
* this seat reads through the framework's per-session projection hook
|
|
9
|
+
* (`useProjection('tokenUsage')`) — the same durable whole-log totals the
|
|
10
|
+
* stock number is computed from.
|
|
11
|
+
*
|
|
12
|
+
* - Mounted in `conversation.composer.dock` (the band under the composer card
|
|
13
|
+
* that hosts the shipped stats line), registered only while the
|
|
14
|
+
* `preciseCacheHitEnabled` toggle is on — off costs nothing. The entry
|
|
15
|
+
* renders nothing itself: it is a live reader that patches the stock span's
|
|
16
|
+
* text in place, so layout, truncation and tooltip behavior stay DSH's own.
|
|
17
|
+
* - A MutationObserver re-applies the precise figure whenever React repaints
|
|
18
|
+
* the line (token updates, group churn); writes are idempotent — a rewrite
|
|
19
|
+
* only happens when the text actually differs — so the loop settles after
|
|
20
|
+
* one pass. Toggling off restores the original texts.
|
|
21
|
+
* @module dsh-ui-tweaks/client/cachehit
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { useEffect } from 'react'
|
|
25
|
+
import type { UseProjection } from '@deepseek-ai/dsh-api-session-controller/client'
|
|
26
|
+
|
|
27
|
+
/** Client-side slice of dsh-token-meter's cumulative projection. */
|
|
28
|
+
interface TokenUsageProjection {
|
|
29
|
+
uncachedInputTokens: number
|
|
30
|
+
cacheReadTokens: number
|
|
31
|
+
cacheWriteTokens: number
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
declare module '@deepseek-ai/dsh-session-projection/types' {
|
|
35
|
+
interface SessionProjectionMap {
|
|
36
|
+
tokenUsage: TokenUsageProjection | undefined
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Whole-span shape of the stock cache-hit group (zh + en locales). */
|
|
41
|
+
const CACHE_HIT_SPAN_PATTERN = /^\s*(?:缓存命中|Cache hit)\s+\d+(?:\.\d+)?%\s*$/
|
|
42
|
+
/** The trailing percentage inside that label — the only part rewritten. */
|
|
43
|
+
const PERCENT_IN_SPAN_PATTERN = /\d+(?:\.\d+)?%(?=\s*$)/
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Sum the three disjoint prompt-side billing buckets — DSH's own denominator
|
|
47
|
+
* for the cache-hit share (`uncachedInput + cacheRead + cacheWrite`).
|
|
48
|
+
*/
|
|
49
|
+
function billedInputTokens(usage: TokenUsageProjection): number {
|
|
50
|
+
return usage.uncachedInputTokens + usage.cacheReadTokens + usage.cacheWriteTokens
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface PreciseCacheHitEntryProps {
|
|
54
|
+
/** Framework seat (PropsRuntime): key-addressed per-session projection reader. */
|
|
55
|
+
useProjection: UseProjection
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Session-scoped composer-dock seat that keeps the stock cache-hit figure at two decimals. */
|
|
59
|
+
export function PreciseCacheHitEntry({ useProjection }: PreciseCacheHitEntryProps) {
|
|
60
|
+
// The raw cumulative usage; undefined until the host's baseline/frame
|
|
61
|
+
// carries the key — exactly when the stock line first shows the group.
|
|
62
|
+
const usage = useProjection('tokenUsage')
|
|
63
|
+
|
|
64
|
+
useEffect(() => {
|
|
65
|
+
const bands = document.querySelectorAll<HTMLElement>('[data-slot="conversation.composer.dock"]')
|
|
66
|
+
if (bands.length === 0) return
|
|
67
|
+
|
|
68
|
+
/** Original texts of spans we rewrote, restored on cleanup. */
|
|
69
|
+
const patched = new Map<HTMLSpanElement, string>()
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Two-decimal share over the raw buckets; null while there is nothing
|
|
73
|
+
* precise to show (projection absent, or no billed input — the stock line
|
|
74
|
+
* omits the group in that case too).
|
|
75
|
+
*/
|
|
76
|
+
const precisePercent = (): string | null => {
|
|
77
|
+
if (usage === undefined) return null
|
|
78
|
+
const billed = billedInputTokens(usage)
|
|
79
|
+
if (!(billed > 0)) return null
|
|
80
|
+
return ((usage.cacheReadTokens / billed) * 100).toFixed(2)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const apply = (): void => {
|
|
84
|
+
const precise = precisePercent()
|
|
85
|
+
for (const band of bands) {
|
|
86
|
+
for (const el of band.querySelectorAll<HTMLSpanElement>('span')) {
|
|
87
|
+
const current = el.textContent ?? ''
|
|
88
|
+
if (!CACHE_HIT_SPAN_PATTERN.test(current)) continue
|
|
89
|
+
if (precise === null) {
|
|
90
|
+
// Projection gone (session switch race): undo any rewrite so the
|
|
91
|
+
// stock integer shows again instead of a stale figure.
|
|
92
|
+
const original = patched.get(el)
|
|
93
|
+
if (original !== undefined && original !== current) el.textContent = original
|
|
94
|
+
patched.delete(el)
|
|
95
|
+
continue
|
|
96
|
+
}
|
|
97
|
+
if (!patched.has(el)) patched.set(el, current)
|
|
98
|
+
const rewritten = current.replace(PERCENT_IN_SPAN_PATTERN, `${precise}%`)
|
|
99
|
+
if (rewritten !== current) el.textContent = rewritten
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
apply()
|
|
105
|
+
// React owns these text nodes: every repaint of the line re-triggers us,
|
|
106
|
+
// and our own idempotent write settles after a single extra pass.
|
|
107
|
+
const observer = new MutationObserver(apply)
|
|
108
|
+
for (const band of bands) {
|
|
109
|
+
observer.observe(band, { childList: true, characterData: true, subtree: true })
|
|
110
|
+
}
|
|
111
|
+
return () => {
|
|
112
|
+
observer.disconnect()
|
|
113
|
+
// Toggle-off/unmount must not leave a stale precise figure behind.
|
|
114
|
+
for (const [el, original] of patched) {
|
|
115
|
+
if ((el.textContent ?? '') !== original) el.textContent = original
|
|
116
|
+
}
|
|
117
|
+
patched.clear()
|
|
118
|
+
}
|
|
119
|
+
}, [usage])
|
|
120
|
+
|
|
121
|
+
return null
|
|
122
|
+
}
|