@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,514 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-ui-tweaks — task notifications (browser half).
|
|
3
|
+
*
|
|
4
|
+
* Watches the sessions list feed (the same store the sidebar's running flag,
|
|
5
|
+
* amber interaction dot and green "done" reminder project from) and raises a
|
|
6
|
+
* browser-side heads-up when a session finishes its turn or starts blocking
|
|
7
|
+
* on the user — so a backgrounded tab can call you back:
|
|
8
|
+
*
|
|
9
|
+
* - "Finished" = a session's `running` flag drops (true → false) without an
|
|
10
|
+
* interaction taking over, or its host-tracked `completed` reminder rises
|
|
11
|
+
* (finished while not selected). The host `dshTurnOutcome` session
|
|
12
|
+
* projection (`src/turn-outcome.ts`, folded from `turn/end` reasons) says
|
|
13
|
+
* WHY it finished, so the copy distinguishes a clean completion from a
|
|
14
|
+
* user abort and from a failed request (with the error text); when the
|
|
15
|
+
* projection is missing or stale, falls back to a plain-finish announce.
|
|
16
|
+
* - "Interaction" = a session gains `pendingInteraction`
|
|
17
|
+
* ('approval' | 'plan-review' | 'question') — exactly the sidebar's
|
|
18
|
+
* amber-dot classification.
|
|
19
|
+
*
|
|
20
|
+
* Three independent channels, each silently degrading when unavailable:
|
|
21
|
+
* - Title flash: alternates `(N) 🔔 <title>` ↔ `(N) <title>` until the page
|
|
22
|
+
* is visible and focused again, then restores the captured title. DSH may
|
|
23
|
+
* rewrite `document.title` independently; while flashing our ticks win,
|
|
24
|
+
* and on stop we restore the pre-flash capture (worst case a stale base).
|
|
25
|
+
* - System notification: the Web Notifications API; permission is requested
|
|
26
|
+
* from the settings toggle's user gesture (`requestNotifyPermission`).
|
|
27
|
+
* Clicking one focuses the window and opens that session. `silent` tracks
|
|
28
|
+
* the chime channel so the two never double-beep.
|
|
29
|
+
* - Chime: a tiny WebAudio two-note motif (rising = done, falling = needs
|
|
30
|
+
* you), synthesized in-process — no audio assets. Autoplay policy allows
|
|
31
|
+
* this once the user has interacted with the origin (sticky activation),
|
|
32
|
+
* which the settings-toggle clicks provide.
|
|
33
|
+
*
|
|
34
|
+
* Anti-spam: the first snapshot after install only arms the baseline (a page
|
|
35
|
+
* load never fires a burst); events fire on transitions only; a short
|
|
36
|
+
* per-session+kind cooldown absorbs reconnect re-pull flicker. Subagent child
|
|
37
|
+
* rows are skipped — their parent carries the turn, and mid-turn subagent
|
|
38
|
+
* completions would be noise.
|
|
39
|
+
* @module dsh-ui-tweaks/client/notifier
|
|
40
|
+
*/
|
|
41
|
+
|
|
42
|
+
import type { ISessions } from '@deepseek-ai/dsh-api-session-controller/client'
|
|
43
|
+
import type { SessionPendingInteractionSnapshot } from '@deepseek-ai/dsh-client-ui-session/client'
|
|
44
|
+
import type { HostObservable } from '@deepseek-ai/dsh-client-ui-slots'
|
|
45
|
+
import type { SessionId } from '@deepseek-ai/dsh-session/types'
|
|
46
|
+
import type { TurnOutcomeValue } from '../../ui-tweaks/turn-outcome.ts'
|
|
47
|
+
|
|
48
|
+
declare module '@deepseek-ai/dsh-session-projection/types' {
|
|
49
|
+
interface SessionProjectionMap {
|
|
50
|
+
dshTurnOutcome: TurnOutcomeValue | null
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** List-row key carrying the turn outcome; keep in sync with src/turn-outcome.ts. */
|
|
55
|
+
const TURN_OUTCOME_KEY = 'dshTurnOutcome'
|
|
56
|
+
|
|
57
|
+
/** Interaction kinds the host reports while a session blocks on the user. */
|
|
58
|
+
export type NotifyPendingKind = 'approval' | 'plan-review' | 'question'
|
|
59
|
+
|
|
60
|
+
/** Event classes the notifier can raise. */
|
|
61
|
+
type NotifyEventKind = 'complete' | 'aborted' | 'failed' | 'interaction'
|
|
62
|
+
|
|
63
|
+
/** Terminal-turn announcement kinds — they all answer "your task ended". */
|
|
64
|
+
const TERMINAL_KINDS = ['complete', 'aborted', 'failed'] as const
|
|
65
|
+
|
|
66
|
+
/** Localized copy snapshot the notifier needs (resolved once at install). */
|
|
67
|
+
export interface NotifierText {
|
|
68
|
+
/** System-notification heading for completion events. */
|
|
69
|
+
notifyTitleDone: string
|
|
70
|
+
/** System-notification heading for user-interrupted turns. */
|
|
71
|
+
notifyTitleAborted: string
|
|
72
|
+
/** System-notification heading for failed requests. */
|
|
73
|
+
notifyTitleFailed: string
|
|
74
|
+
/** System-notification heading for interaction events. */
|
|
75
|
+
notifyTitlePending: string
|
|
76
|
+
/** Completion body template; `{title}` becomes the session display title. */
|
|
77
|
+
bodyComplete: string
|
|
78
|
+
/** Interruption body template. */
|
|
79
|
+
bodyAborted: string
|
|
80
|
+
/** Failure body template; `{error}` becomes the truncated failure message. */
|
|
81
|
+
bodyFailed: string
|
|
82
|
+
/** Approval body template. */
|
|
83
|
+
bodyApproval: string
|
|
84
|
+
/** Plan-review body template. */
|
|
85
|
+
bodyPlan: string
|
|
86
|
+
/** Question body template. */
|
|
87
|
+
bodyQuestion: string
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Live-read behavior switches — read through {@link TaskNotifierInput.readState}
|
|
92
|
+
* on every feed tick so settings-panel flips apply without reinstalling.
|
|
93
|
+
*/
|
|
94
|
+
export interface NotifierOptions {
|
|
95
|
+
/** Raise alerts only while the page is hidden or unfocused. */
|
|
96
|
+
onlyWhenHidden: boolean
|
|
97
|
+
/** Alert when a session finishes its turn. */
|
|
98
|
+
onComplete: boolean
|
|
99
|
+
/** Alert when a session starts waiting on the user. */
|
|
100
|
+
onInteraction: boolean
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Output channels, each independently toggleable. */
|
|
104
|
+
export interface NotifierChannels {
|
|
105
|
+
titleFlash: boolean
|
|
106
|
+
systemNotification: boolean
|
|
107
|
+
sound: boolean
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Install-time wiring for {@link installTaskNotifier}. */
|
|
111
|
+
export interface TaskNotifierInput {
|
|
112
|
+
/** The client sessions service whose list feed carries the watch states. */
|
|
113
|
+
sessionsService: ISessions
|
|
114
|
+
/** Current Session-scoped interactions, published separately from list rows. */
|
|
115
|
+
pendingInteractions: HostObservable<SessionPendingInteractionSnapshot>
|
|
116
|
+
/** Localized copy snapshot used across the notifier's lifetime. */
|
|
117
|
+
text: NotifierText
|
|
118
|
+
/** Fresh behavior switches + channel toggles, read on every tick. */
|
|
119
|
+
readState(): { options: NotifierOptions; channels: NotifierChannels }
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Per-session state the transition detector compares against. */
|
|
123
|
+
interface WatchState {
|
|
124
|
+
running: boolean
|
|
125
|
+
pending: NotifyPendingKind | undefined
|
|
126
|
+
completed: boolean
|
|
127
|
+
outcome: TurnOutcomeValue | undefined
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Narrow the extensible host interaction roster to the notification copy we own. */
|
|
131
|
+
function pendingKind(
|
|
132
|
+
snapshot: SessionPendingInteractionSnapshot,
|
|
133
|
+
sessionId: SessionId,
|
|
134
|
+
): NotifyPendingKind | undefined {
|
|
135
|
+
switch (snapshot.get(sessionId)?.kind) {
|
|
136
|
+
case 'approval':
|
|
137
|
+
return 'approval'
|
|
138
|
+
case 'plan-review':
|
|
139
|
+
return 'plan-review'
|
|
140
|
+
case 'question':
|
|
141
|
+
return 'question'
|
|
142
|
+
default:
|
|
143
|
+
return undefined
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Map a logged `turn/end` reason onto an announcement kind. Unknown or absent
|
|
149
|
+
* outcomes (older host, projection lag) degrade to a plain completion.
|
|
150
|
+
*/
|
|
151
|
+
const classifyFinish = (outcome: TurnOutcomeValue | undefined): 'complete' | 'aborted' | 'failed' => {
|
|
152
|
+
switch (outcome?.kind) {
|
|
153
|
+
case 'aborted':
|
|
154
|
+
case 'interrupted':
|
|
155
|
+
return 'aborted'
|
|
156
|
+
case 'error':
|
|
157
|
+
return 'failed'
|
|
158
|
+
default:
|
|
159
|
+
return 'complete'
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** Same event kind for one session never repeats inside this window. */
|
|
164
|
+
const COOLDOWN_MS = 2000
|
|
165
|
+
/**
|
|
166
|
+
* A background session's finish usually announces via the running-drop; the
|
|
167
|
+
* host's green `completed` reminder often lands a re-pull later — treat it as
|
|
168
|
+
* the SAME completion inside this window instead of re-toasting it.
|
|
169
|
+
*/
|
|
170
|
+
const COMPLETE_RENOTIFY_MS = 30000
|
|
171
|
+
/** Title-flash blink period. */
|
|
172
|
+
const FLASH_INTERVAL_MS = 900
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* `NotificationOptions` plus Chrome's `renotify` (re-alert on tag replace).
|
|
176
|
+
* The option is real and supported at runtime, but this project's DOM lib
|
|
177
|
+
* predates its type declaration, so it is declared locally.
|
|
178
|
+
*/
|
|
179
|
+
interface RealertableNotificationOptions extends NotificationOptions {
|
|
180
|
+
renotify?: boolean
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** Replace the `{title}` / `{error}` placeholders (split/join — no regex escaping worries). */
|
|
184
|
+
function fill(template: string, title: string, error: string | undefined): string {
|
|
185
|
+
return template.split('{error}').join(error ?? '').split('{title}').join(title)
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Ask the browser for desktop-notification permission. Must run inside a user
|
|
190
|
+
* gesture (the settings toggle's click) to avoid a denied or ignored prompt;
|
|
191
|
+
* repeated calls are cheap no-ops once decided.
|
|
192
|
+
*/
|
|
193
|
+
export function requestNotifyPermission(): void {
|
|
194
|
+
try {
|
|
195
|
+
if (typeof Notification === 'undefined') return
|
|
196
|
+
if (Notification.permission === 'default') void Notification.requestPermission()
|
|
197
|
+
} catch {
|
|
198
|
+
// Notifications unsupported — the other channels carry on.
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Forced title-flash preview for the settings panel's 测试 button: the real
|
|
204
|
+
* flash auto-clears the moment its page is visible AND focused — which is
|
|
205
|
+
* exactly the tester's situation, so a plain preview would heal instantly and
|
|
206
|
+
* show nothing. Instead this blinks unconditionally for a few seconds, then
|
|
207
|
+
* restores whatever title was current at click time.
|
|
208
|
+
*/
|
|
209
|
+
let previewBlinkTimer: number | undefined
|
|
210
|
+
let previewStopTimer: number | undefined
|
|
211
|
+
|
|
212
|
+
export function previewTitleFlash(durationMs = 6000): void {
|
|
213
|
+
// Restart cleanly on rapid re-clicks.
|
|
214
|
+
if (previewBlinkTimer !== undefined) window.clearInterval(previewBlinkTimer)
|
|
215
|
+
if (previewStopTimer !== undefined) window.clearTimeout(previewStopTimer)
|
|
216
|
+
const base = document.title
|
|
217
|
+
let tick = false
|
|
218
|
+
const blink = (): void => {
|
|
219
|
+
tick = !tick
|
|
220
|
+
document.title = tick ? `(1) 🔔 ${base}` : `(1) ${base}`
|
|
221
|
+
}
|
|
222
|
+
blink()
|
|
223
|
+
previewBlinkTimer = window.setInterval(blink, FLASH_INTERVAL_MS)
|
|
224
|
+
previewStopTimer = window.setTimeout(() => {
|
|
225
|
+
if (previewBlinkTimer !== undefined) {
|
|
226
|
+
window.clearInterval(previewBlinkTimer)
|
|
227
|
+
previewBlinkTimer = undefined
|
|
228
|
+
}
|
|
229
|
+
previewStopTimer = undefined
|
|
230
|
+
document.title = base
|
|
231
|
+
}, durationMs)
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* One-shot self-test for the settings panel's 测试 button: previews every
|
|
236
|
+
* enabled channel — a forced ~6s title-flash blink (the real one would clear
|
|
237
|
+
* instantly on a focused page), the falling (needs-you) chime, and, when
|
|
238
|
+
* permission is already granted, one sample system notification.
|
|
239
|
+
*/
|
|
240
|
+
export function previewAlerts(channels: NotifierChannels, text: NotifierText): void {
|
|
241
|
+
requestNotifyPermission()
|
|
242
|
+
if (channels.titleFlash) previewTitleFlash()
|
|
243
|
+
if (channels.sound) playChime('down')
|
|
244
|
+
if (!channels.systemNotification || typeof Notification === 'undefined') return
|
|
245
|
+
if (Notification.permission !== 'granted') return
|
|
246
|
+
try {
|
|
247
|
+
const sample = fill(text.bodyComplete, 'ChatCode CLI', undefined)
|
|
248
|
+
new Notification(text.notifyTitleDone, { body: sample, tag: 'dsh-ui-tweaks:preview', silent: true })
|
|
249
|
+
} catch {
|
|
250
|
+
// Construction can throw on some platforms — degrade quietly.
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// ---------------------------------------------------------------------------
|
|
255
|
+
// Chime — WebAudio two-note motif, synthesized, no assets.
|
|
256
|
+
// ---------------------------------------------------------------------------
|
|
257
|
+
|
|
258
|
+
let audioCtx: AudioContext | undefined
|
|
259
|
+
|
|
260
|
+
function playChime(shape: 'up' | 'down'): void {
|
|
261
|
+
try {
|
|
262
|
+
audioCtx ??= new AudioContext()
|
|
263
|
+
const ctx = audioCtx
|
|
264
|
+
// Sticky activation from earlier clicks usually suffices; resume is best effort.
|
|
265
|
+
if (ctx.state === 'suspended') void ctx.resume().catch(() => {})
|
|
266
|
+
const now = ctx.currentTime
|
|
267
|
+
const notes = shape === 'up' ? [880, 1318.51] : [1174.66, 783.99]
|
|
268
|
+
// 240ms between notes with a long decay each: tight spacing read as ONE
|
|
269
|
+
// blip in listening tests; this wide a gap keeps the two tones distinct
|
|
270
|
+
// (rising = done, falling = needs you).
|
|
271
|
+
notes.forEach((freq, i) => {
|
|
272
|
+
const osc = ctx.createOscillator()
|
|
273
|
+
const gain = ctx.createGain()
|
|
274
|
+
osc.type = 'sine'
|
|
275
|
+
osc.frequency.value = freq
|
|
276
|
+
const t0 = now + i * 0.24
|
|
277
|
+
gain.gain.setValueAtTime(0.0001, t0)
|
|
278
|
+
gain.gain.exponentialRampToValueAtTime(0.22, t0 + 0.03)
|
|
279
|
+
gain.gain.exponentialRampToValueAtTime(0.0001, t0 + 0.2)
|
|
280
|
+
osc.connect(gain)
|
|
281
|
+
gain.connect(ctx.destination)
|
|
282
|
+
osc.start(t0)
|
|
283
|
+
osc.stop(t0 + 0.24)
|
|
284
|
+
})
|
|
285
|
+
} catch {
|
|
286
|
+
// Audio unavailable (autoplay policy, missing AudioContext) — stay silent.
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// ---------------------------------------------------------------------------
|
|
291
|
+
// Installer.
|
|
292
|
+
// ---------------------------------------------------------------------------
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Install the task notifier against the sessions list feed.
|
|
296
|
+
* @returns disposer — unsubscribes, restores the tab title and tears the channels down.
|
|
297
|
+
*/
|
|
298
|
+
export function installTaskNotifier(input: TaskNotifierInput): () => void {
|
|
299
|
+
const { sessionsService, pendingInteractions, text, readState } = input
|
|
300
|
+
const list = sessionsService.list
|
|
301
|
+
|
|
302
|
+
// --- detector state ------------------------------------------------------
|
|
303
|
+
const watch = new Map<SessionId, WatchState>()
|
|
304
|
+
const lastFired = new Map<string, number>()
|
|
305
|
+
|
|
306
|
+
// --- title flash ---------------------------------------------------------
|
|
307
|
+
let originalTitle: string | null = null
|
|
308
|
+
let flashTimer: number | undefined
|
|
309
|
+
let flashTick = false
|
|
310
|
+
let unread = 0
|
|
311
|
+
|
|
312
|
+
const visibleAndFocused = (): boolean => document.visibilityState === 'visible' && document.hasFocus()
|
|
313
|
+
|
|
314
|
+
const startTitleFlash = (): void => {
|
|
315
|
+
if (flashTimer !== undefined) return
|
|
316
|
+
originalTitle = document.title
|
|
317
|
+
flashTimer = window.setInterval(() => {
|
|
318
|
+
flashTick = !flashTick
|
|
319
|
+
const base = originalTitle ?? ''
|
|
320
|
+
document.title = flashTick ? `(${unread}) 🔔 ${base}` : `(${unread}) ${base}`
|
|
321
|
+
}, FLASH_INTERVAL_MS)
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const stopTitleFlash = (): void => {
|
|
325
|
+
if (flashTimer !== undefined) {
|
|
326
|
+
window.clearInterval(flashTimer)
|
|
327
|
+
flashTimer = undefined
|
|
328
|
+
}
|
|
329
|
+
if (originalTitle !== null) document.title = originalTitle
|
|
330
|
+
originalTitle = null
|
|
331
|
+
unread = 0
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/** Coming back to a visible, focused page acknowledges everything. */
|
|
335
|
+
const settleIfBack = (): void => {
|
|
336
|
+
if (visibleAndFocused()) stopTitleFlash()
|
|
337
|
+
}
|
|
338
|
+
const onVisibilityChange = (): void => {
|
|
339
|
+
settleIfBack()
|
|
340
|
+
}
|
|
341
|
+
window.addEventListener('focus', settleIfBack)
|
|
342
|
+
document.addEventListener('visibilitychange', onVisibilityChange)
|
|
343
|
+
|
|
344
|
+
// --- system notification -------------------------------------------------
|
|
345
|
+
const fireSystemNotification = (
|
|
346
|
+
kind: NotifyEventKind,
|
|
347
|
+
sessionId: SessionId,
|
|
348
|
+
heading: string,
|
|
349
|
+
body: string,
|
|
350
|
+
chimeOn: boolean,
|
|
351
|
+
): void => {
|
|
352
|
+
if (typeof Notification === 'undefined') return
|
|
353
|
+
if (Notification.permission !== 'granted') return
|
|
354
|
+
try {
|
|
355
|
+
// `silent` mirrors the chime channel: our motif is THE sound, so mute
|
|
356
|
+
// the OS bark while it will play; otherwise let the platform ring.
|
|
357
|
+
// `renotify` forces a fresh banner when this tag REPLACES an existing
|
|
358
|
+
// toast — without it Chrome swaps the old toast silently and Windows
|
|
359
|
+
// collapses it into the notification center.
|
|
360
|
+
const options: RealertableNotificationOptions = {
|
|
361
|
+
body,
|
|
362
|
+
tag: `dsh-ui-tweaks:${sessionId}:${kind}`,
|
|
363
|
+
silent: chimeOn,
|
|
364
|
+
renotify: true,
|
|
365
|
+
}
|
|
366
|
+
const n = new Notification(heading, options)
|
|
367
|
+
n.onclick = (): void => {
|
|
368
|
+
try { window.focus() } catch { /* ignore */ }
|
|
369
|
+
try { sessionsService.open(sessionId) } catch { /* session already gone — focus alone is fine */ }
|
|
370
|
+
n.close()
|
|
371
|
+
}
|
|
372
|
+
} catch {
|
|
373
|
+
// Construction can throw on some platforms — degrade quietly.
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// --- delivery ------------------------------------------------------------
|
|
378
|
+
const deliver = (
|
|
379
|
+
sessionId: SessionId,
|
|
380
|
+
displayTitle: string,
|
|
381
|
+
kind: NotifyEventKind,
|
|
382
|
+
pending: NotifyPendingKind | undefined,
|
|
383
|
+
detail: string | undefined,
|
|
384
|
+
channels: NotifierChannels,
|
|
385
|
+
now: number,
|
|
386
|
+
): void => {
|
|
387
|
+
const cooldownKey = `${sessionId}:${kind}`
|
|
388
|
+
const last = lastFired.get(cooldownKey)
|
|
389
|
+
if (last !== undefined && now - last < COOLDOWN_MS) return
|
|
390
|
+
lastFired.set(cooldownKey, now)
|
|
391
|
+
|
|
392
|
+
unread += 1
|
|
393
|
+
if (channels.titleFlash) startTitleFlash()
|
|
394
|
+
if (!channels.systemNotification && !channels.sound) return
|
|
395
|
+
|
|
396
|
+
const heading = kind === 'complete'
|
|
397
|
+
? text.notifyTitleDone
|
|
398
|
+
: kind === 'aborted'
|
|
399
|
+
? text.notifyTitleAborted
|
|
400
|
+
: kind === 'failed'
|
|
401
|
+
? text.notifyTitleFailed
|
|
402
|
+
: text.notifyTitlePending
|
|
403
|
+
let body = kind === 'aborted'
|
|
404
|
+
? text.bodyAborted
|
|
405
|
+
: kind === 'failed'
|
|
406
|
+
? text.bodyFailed
|
|
407
|
+
: text.bodyComplete
|
|
408
|
+
if (kind === 'interaction') {
|
|
409
|
+
body = pending === 'approval'
|
|
410
|
+
? text.bodyApproval
|
|
411
|
+
: pending === 'plan-review'
|
|
412
|
+
? text.bodyPlan
|
|
413
|
+
: text.bodyQuestion
|
|
414
|
+
}
|
|
415
|
+
fireSystemNotification(kind, sessionId, heading, fill(body, displayTitle, detail), channels.sound)
|
|
416
|
+
if (channels.sound) playChime(kind === 'complete' ? 'up' : 'down')
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// --- baseline arming -----------------------------------------------------
|
|
420
|
+
/** Seed the watch map without firing: fresh installs and reloads stay quiet. */
|
|
421
|
+
const armBaseline = (): void => {
|
|
422
|
+
watch.clear()
|
|
423
|
+
const interactions = pendingInteractions.getSnapshot()
|
|
424
|
+
for (const row of Object.values(list.getSnapshot().byId)) {
|
|
425
|
+
if (row.parentId !== undefined || row.blank) continue
|
|
426
|
+
const outcome = row.projectionValues?.[TURN_OUTCOME_KEY] ?? undefined
|
|
427
|
+
watch.set(row.id, {
|
|
428
|
+
running: row.running,
|
|
429
|
+
pending: pendingKind(interactions, row.id),
|
|
430
|
+
completed: row.completed === true,
|
|
431
|
+
outcome,
|
|
432
|
+
})
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// --- feed tick -----------------------------------------------------------
|
|
437
|
+
const check = (): void => {
|
|
438
|
+
const { options, channels } = readState()
|
|
439
|
+
const deliverable = !options.onlyWhenHidden || !visibleAndFocused()
|
|
440
|
+
const now = Date.now()
|
|
441
|
+
const snap = list.getSnapshot()
|
|
442
|
+
const interactions = pendingInteractions.getSnapshot()
|
|
443
|
+
const seen = new Set<SessionId>()
|
|
444
|
+
|
|
445
|
+
for (const row of Object.values(snap.byId)) {
|
|
446
|
+
if (row.parentId !== undefined || row.blank) continue
|
|
447
|
+
seen.add(row.id)
|
|
448
|
+
const prev = watch.get(row.id)
|
|
449
|
+
const outcome = row.projectionValues?.[TURN_OUTCOME_KEY] ?? undefined
|
|
450
|
+
const pending = pendingKind(interactions, row.id)
|
|
451
|
+
watch.set(row.id, { running: row.running, pending, completed: row.completed === true, outcome })
|
|
452
|
+
if (prev === undefined) continue
|
|
453
|
+
|
|
454
|
+
// Interaction outranks completion: a turn ending ON a question must
|
|
455
|
+
// announce the question, not a completion.
|
|
456
|
+
if (prev.pending === undefined && pending !== undefined) {
|
|
457
|
+
if (options.onInteraction && deliverable) {
|
|
458
|
+
deliver(row.id, row.displayTitle, 'interaction', pending, undefined, channels, now)
|
|
459
|
+
}
|
|
460
|
+
continue
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
const finished = prev.running && !row.running && pending === undefined
|
|
464
|
+
const completedReminder = !prev.completed && row.completed === true
|
|
465
|
+
if (finished || completedReminder) {
|
|
466
|
+
// The green `completed` reminder often arrives a re-pull AFTER the
|
|
467
|
+
// running-drop already announced the same finish — inside the
|
|
468
|
+
// re-notify window it is a duplicate, not a new completion.
|
|
469
|
+
let shouldFire = finished
|
|
470
|
+
if (!shouldFire) {
|
|
471
|
+
// Any recent terminal announcement (of any outcome kind) makes the
|
|
472
|
+
// green-dot reminder a duplicate of the same finish.
|
|
473
|
+
shouldFire = TERMINAL_KINDS.every((kind) => {
|
|
474
|
+
const last = lastFired.get(`${row.id}:${kind}`)
|
|
475
|
+
return last === undefined || now - last >= COMPLETE_RENOTIFY_MS
|
|
476
|
+
})
|
|
477
|
+
}
|
|
478
|
+
if (shouldFire && options.onComplete && deliverable) {
|
|
479
|
+
// Trust the outcome only when it CHANGED on this very transition —
|
|
480
|
+
// a stale value from an earlier turn would mislabel the announce.
|
|
481
|
+
const fresh = prev.outcome === undefined
|
|
482
|
+
|| outcome?.time !== prev.outcome.time
|
|
483
|
+
|| outcome?.kind !== prev.outcome.kind
|
|
484
|
+
const announcement = fresh && outcome !== undefined ? classifyFinish(outcome) : 'complete'
|
|
485
|
+
deliver(row.id, row.displayTitle, announcement, undefined, outcome?.errorMessage, channels, now)
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// Forget removed sessions so a re-added id arms fresh instead of comparing
|
|
491
|
+
// against stale pre-removal state.
|
|
492
|
+
for (const id of watch.keys()) {
|
|
493
|
+
if (!seen.has(id)) watch.delete(id)
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
armBaseline()
|
|
498
|
+
const disposeFeed = list.subscribe(check)
|
|
499
|
+
const disposeInteractions = pendingInteractions.subscribe(check)
|
|
500
|
+
|
|
501
|
+
return () => {
|
|
502
|
+
disposeFeed()
|
|
503
|
+
disposeInteractions()
|
|
504
|
+
stopTitleFlash()
|
|
505
|
+
window.removeEventListener('focus', settleIfBack)
|
|
506
|
+
document.removeEventListener('visibilitychange', onVisibilityChange)
|
|
507
|
+
watch.clear()
|
|
508
|
+
lastFired.clear()
|
|
509
|
+
if (audioCtx !== undefined) {
|
|
510
|
+
void audioCtx.close().catch(() => {})
|
|
511
|
+
audioCtx = undefined
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-ui-tweaks — whale working indicator (browser half).
|
|
3
|
+
*
|
|
4
|
+
* A compact China-knot brand mark perched on the composer card's top-right
|
|
5
|
+
* corner over drifting red waves. It is resident while the feature is on;
|
|
6
|
+
* idle it rests in brand red, and while the model works it floats and breathes
|
|
7
|
+
* through the red accent spectrum.
|
|
8
|
+
*
|
|
9
|
+
* - Mounted in `conversation.input.dock` (the full-width row stacked by the
|
|
10
|
+
* composer column right above the card), registered only while the
|
|
11
|
+
* `whaleIndicatorEnabled` toggle is on — off costs nothing. The dock row is
|
|
12
|
+
* WIDER than the card (the card is capped at
|
|
13
|
+
* `--dsh-composer-card-max-width` and centered with
|
|
14
|
+
* `--dsh-composer-side-clearance` side padding by the InputBar root), so
|
|
15
|
+
* the row replicates that exact geometry — same max-width, centered, same
|
|
16
|
+
* side padding — before right-aligning; a 6px `translateY` then straddles
|
|
17
|
+
* the card's top edge. The extra 14px right inset centers the whale over
|
|
18
|
+
* the send button (34px round button, 8px tool-row padding → its center
|
|
19
|
+
* sits 25px from the card's right edge; the 22px whale's right edge at
|
|
20
|
+
* 14px puts its center at 25px).
|
|
21
|
+
* - "Working" = the current session's `running` flag on the sessions list
|
|
22
|
+
* feed, OR the input machine's claimed/submitting phases (the window
|
|
23
|
+
* between pressing Enter and the agent actually starting to run).
|
|
24
|
+
* - The mark is self-contained rather than lifted from the host sidebar, so
|
|
25
|
+
* its branding remains consistent while DSH changes its own logo.
|
|
26
|
+
* @module dsh-ui-tweaks/client/whale
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import { useEffect, useMemo, useRef, useState, useSyncExternalStore } from 'react'
|
|
30
|
+
import type { ISessions } from '@deepseek-ai/dsh-api-session-controller/client'
|
|
31
|
+
import type { SessionId } from '@deepseek-ai/dsh-session/types'
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* China-knot-inspired geometry, painted in `currentColor`.
|
|
35
|
+
*/
|
|
36
|
+
const KNOT_PATHS = [
|
|
37
|
+
'M32 5c-8.5 0-15.5 6.9-15.5 15.5 0 8.6 6.1 12.8 15.5 18.8 9.4-6 15.5-10.2 15.5-18.8C47.5 11.9 40.5 5 32 5Z',
|
|
38
|
+
'M32 59c8.5 0 15.5-6.9 15.5-15.5 0-8.6-6.1-12.8-15.5-18.8-9.4 6-15.5 10.2-15.5 18.8C16.5 52.1 23.5 59 32 59Z',
|
|
39
|
+
'M5 32c0-8.5 6.9-15.5 15.5-15.5 8.6 0 12.8 6.1 18.8 15.5-6 9.4-10.2 15.5-18.8 15.5C11.9 47.5 5 40.5 5 32Z',
|
|
40
|
+
'M59 32c0 8.5-6.9 15.5-15.5 15.5-8.6 0-12.8-6.1-18.8-15.5 6-9.4 10.2-15.5 18.8-15.5C52.1 16.5 59 23.5 59 32Z',
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
/** Brand mark width in px; square geometry keeps it crisp beside Send. */
|
|
44
|
+
const KNOT_WIDTH = 22
|
|
45
|
+
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
// Styles — resident (translucent, still) vs working (opaque, swimming).
|
|
48
|
+
// ---------------------------------------------------------------------------
|
|
49
|
+
|
|
50
|
+
export const WHALE_CSS = `
|
|
51
|
+
.duwi-row{box-sizing:border-box;width:100%;max-width:calc(var(--dsh-composer-card-max-width) + 2 * var(--dsh-composer-side-clearance, 0px));margin:0 auto;padding:0 calc(var(--dsh-composer-side-clearance, 0px) + 14px) 0 var(--dsh-composer-side-clearance, 0px);display:flex;align-items:flex-end;justify-content:flex-end;height:27px;pointer-events:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}
|
|
52
|
+
.duwi-stack{display:flex;flex-direction:column;align-items:center;transform:translateY(6px)}
|
|
53
|
+
.duwi-whale{display:block;color:#9B0B09;transform-origin:50% 60%;pointer-events:auto;cursor:pointer}
|
|
54
|
+
.duwi-row.duwi-swim .duwi-whale{animation:duwi-swim 1.9s ease-in-out infinite,duwi-breathe 1.9s ease-in-out infinite}
|
|
55
|
+
.duwi-whale:hover{animation:duwi-swim 1.9s ease-in-out infinite}
|
|
56
|
+
.duwi-waves{display:block;width:26px;height:9px;margin-top:-3px;overflow:hidden}
|
|
57
|
+
.duwi-wave-back{stroke:#d96a65;opacity:.35}
|
|
58
|
+
.duwi-wave-front{stroke:#9B0B09;opacity:.8}
|
|
59
|
+
.duwi-wave-front,.duwi-wave-back{animation:duwi-drift-front 1.9s linear infinite}
|
|
60
|
+
.duwi-wave-back{animation-duration:3.4s;animation-direction:reverse}
|
|
61
|
+
@keyframes duwi-swim{
|
|
62
|
+
0%,100%{transform:translateY(0) rotate(-4deg)}
|
|
63
|
+
50%{transform:translateY(-3.5px) rotate(4deg)}
|
|
64
|
+
}
|
|
65
|
+
@keyframes duwi-breathe{
|
|
66
|
+
0%,100%{color:#9B0B09}
|
|
67
|
+
50%{color:#d94b45}
|
|
68
|
+
}
|
|
69
|
+
@keyframes duwi-drift-front{
|
|
70
|
+
from{transform:translateX(0)}
|
|
71
|
+
to{transform:translateX(-8px)}
|
|
72
|
+
}
|
|
73
|
+
@media (prefers-reduced-motion:reduce){
|
|
74
|
+
.duwi-row.duwi-swim .duwi-whale,.duwi-whale:hover{animation:none}
|
|
75
|
+
.duwi-wave-front,.duwi-wave-back{animation:none}
|
|
76
|
+
}
|
|
77
|
+
`
|
|
78
|
+
|
|
79
|
+
/** Idempotent installer for the indicator styles (same pattern as the other features). */
|
|
80
|
+
export function installWhaleStyles(): () => void {
|
|
81
|
+
const id = 'dsh-ui-tweaks-whale'
|
|
82
|
+
const existing = document.querySelector(`style[data-plugin-css="${id}"]`)
|
|
83
|
+
if (existing !== null) return () => {}
|
|
84
|
+
const style = document.createElement('style')
|
|
85
|
+
style.dataset.plugin = 'dsh-ui-tweaks'
|
|
86
|
+
style.dataset.pluginCss = id
|
|
87
|
+
style.textContent = WHALE_CSS
|
|
88
|
+
document.head.appendChild(style)
|
|
89
|
+
return () => { style.remove() }
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// ---------------------------------------------------------------------------
|
|
93
|
+
// Live whale geometry refresh.
|
|
94
|
+
// ---------------------------------------------------------------------------
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
// ---------------------------------------------------------------------------
|
|
98
|
+
// Component.
|
|
99
|
+
// ---------------------------------------------------------------------------
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Structural slice of the framework `InputState` (not re-exported by the
|
|
103
|
+
* package's client entry) — exactly the fields the indicator reads off the
|
|
104
|
+
* dock's owner share.
|
|
105
|
+
*/
|
|
106
|
+
interface InputPhaseSnapshot {
|
|
107
|
+
phase: 'plain' | 'adjudicating' | 'claimed' | 'submitting'
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export interface WhaleIndicatorProps {
|
|
111
|
+
/** Framework seat (PropsRuntime): current session id. */
|
|
112
|
+
sessionId: SessionId | undefined
|
|
113
|
+
/** Owner share (InputZone): live input-machine snapshot — covers the send → run window. */
|
|
114
|
+
input: InputPhaseSnapshot
|
|
115
|
+
/** Injected: the client sessions service (list feed carrying `running`). */
|
|
116
|
+
sessionsService: ISessions
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function WhaleIndicator({ sessionId, input, sessionsService }: WhaleIndicatorProps) {
|
|
120
|
+
// Idle easter egg: a click swims the whale for a couple of cycles.
|
|
121
|
+
const [peek, setPeek] = useState(false)
|
|
122
|
+
const peekTimer = useRef<number | undefined>(undefined)
|
|
123
|
+
useEffect(() => () => { window.clearTimeout(peekTimer.current) }, [])
|
|
124
|
+
const swimFor = (): void => {
|
|
125
|
+
setPeek(true)
|
|
126
|
+
window.clearTimeout(peekTimer.current)
|
|
127
|
+
peekTimer.current = window.setTimeout(() => { setPeek(false) }, 3800)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// The current session's `running` flag, straight off the sessions list feed.
|
|
131
|
+
const list = sessionsService.list
|
|
132
|
+
const subscribe = useMemo(() => list.subscribe.bind(list), [list])
|
|
133
|
+
const running = useSyncExternalStore(
|
|
134
|
+
subscribe,
|
|
135
|
+
() => {
|
|
136
|
+
const snapshot = list.getSnapshot()
|
|
137
|
+
const id = sessionId ?? snapshot.current
|
|
138
|
+
return id !== undefined && (snapshot.byId[id]?.running ?? false)
|
|
139
|
+
},
|
|
140
|
+
() => false,
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
// `claimed`/`submitting` bridge the gap between Enter and the agent start.
|
|
144
|
+
const submitting = input.phase === 'claimed' || input.phase === 'submitting'
|
|
145
|
+
const swim = running || submitting || peek
|
|
146
|
+
|
|
147
|
+
return (
|
|
148
|
+
<div className={swim ? 'duwi-row duwi-swim' : 'duwi-row'} aria-hidden="true">
|
|
149
|
+
<div className="duwi-stack">
|
|
150
|
+
<svg
|
|
151
|
+
className="duwi-whale"
|
|
152
|
+
width={KNOT_WIDTH}
|
|
153
|
+
height={KNOT_WIDTH}
|
|
154
|
+
viewBox="0 0 64 64"
|
|
155
|
+
fill="none"
|
|
156
|
+
onClick={swimFor}
|
|
157
|
+
>
|
|
158
|
+
<g stroke="currentColor" strokeWidth="5.2" strokeLinecap="round" strokeLinejoin="round">
|
|
159
|
+
{KNOT_PATHS.map((path) => <path key={path} d={path} />)}
|
|
160
|
+
</g>
|
|
161
|
+
<circle cx="32" cy="32" r="5.5" fill="currentColor" />
|
|
162
|
+
</svg>
|
|
163
|
+
<svg className="duwi-waves" viewBox="0 0 26 9" fill="none">
|
|
164
|
+
{/* Wavelength-8 sine strips drawn from -8 to +40 so the ±8px drift loop stays covered. */}
|
|
165
|
+
<path className="duwi-wave-back" d="M-8 4 Q-4 5.8 0 4 T8 4 T16 4 T24 4 T32 4 T40 4" strokeWidth="1.4" strokeLinecap="round" />
|
|
166
|
+
<path className="duwi-wave-front" d="M-8 5.5 Q-4 3.3 0 5.5 T8 5.5 T16 5.5 T24 5.5 T32 5.5 T40 5.5" strokeWidth="1.4" strokeLinecap="round" />
|
|
167
|
+
</svg>
|
|
168
|
+
</div>
|
|
169
|
+
</div>
|
|
170
|
+
)
|
|
171
|
+
}
|