dsh-coding-sidebar 1.0.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 +524 -0
- package/cordis.patch.yml +49 -0
- package/lib/client-editor.js +48229 -0
- package/lib/client-mermaid.js +200745 -0
- package/lib/client-registry.js +21867 -0
- package/lib/client-terminal.js +9361 -0
- package/lib/client.js +21867 -0
- package/lib/index.js +4556 -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/client/BrowserView.d.ts +39 -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/FileTree.d.ts +38 -0
- package/lib/types/client/FreeWindow.d.ts +29 -0
- package/lib/types/client/GitView.d.ts +10 -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/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/SideChatView.d.ts +16 -0
- package/lib/types/client/Sidebar.d.ts +6 -0
- package/lib/types/client/SubagentView.d.ts +16 -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 +24 -0
- package/lib/types/client/UploadOverlay.d.ts +20 -0
- package/lib/types/client/add-plugin-modal.d.ts +15 -0
- package/lib/types/client/api.d.ts +275 -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/chunk-loader.d.ts +131 -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 +59 -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 +25 -0
- package/lib/types/client/intercept.d.ts +46 -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 +452 -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 +38 -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 +85 -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 +439 -0
- package/lib/types/client/settings-nav-icon.d.ts +19 -0
- package/lib/types/client/shell-presets.d.ts +48 -0
- package/lib/types/client/sidechat-transcript.d.ts +103 -0
- package/lib/types/client/split-pane.d.ts +36 -0
- package/lib/types/client/state.d.ts +432 -0
- package/lib/types/client/subagent-detect.d.ts +54 -0
- package/lib/types/client/subagent-jobs.d.ts +63 -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/upload.d.ts +69 -0
- package/lib/types/client/wco.d.ts +47 -0
- package/lib/types/config.d.ts +64 -0
- package/lib/types/context-types.d.ts +605 -0
- package/lib/types/fs-operations.d.ts +28 -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/jobs-routes.d.ts +45 -0
- package/lib/types/open-external.d.ts +24 -0
- package/lib/types/path-security.d.ts +20 -0
- package/lib/types/prefs-shared.d.ts +236 -0
- package/lib/types/pty-deps.d.ts +78 -0
- package/lib/types/pty-manager.d.ts +168 -0
- package/lib/types/sidechat-core.d.ts +172 -0
- package/lib/types/sidechat-routes.d.ts +30 -0
- package/lib/types/subagent-activity.d.ts +44 -0
- package/lib/types/subagent-live-route.d.ts +44 -0
- package/lib/types/tools.d.ts +34 -0
- package/lib/types/trust-fence.d.ts +24 -0
- package/lib/types/wire.d.ts +38 -0
- package/package.json +172 -0
- package/scripts/install.ps1 +306 -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/client/BrowserView.tsx +289 -0
- package/src/client/DiffTab.tsx +114 -0
- package/src/client/DiffView.tsx +305 -0
- package/src/client/EditorHost.tsx +558 -0
- package/src/client/FileTree.tsx +669 -0
- package/src/client/FreeWindow.tsx +337 -0
- package/src/client/GitView.tsx +717 -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/RenderBoundary.tsx +49 -0
- package/src/client/SandboxStatusBar.tsx +60 -0
- package/src/client/SideCardSection.module.css +960 -0
- package/src/client/SideCardSection.tsx +1163 -0
- package/src/client/SideChatView.module.css +437 -0
- package/src/client/SideChatView.tsx +645 -0
- package/src/client/Sidebar.tsx +1370 -0
- package/src/client/SubagentView.module.css +547 -0
- package/src/client/SubagentView.tsx +883 -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 +275 -0
- package/src/client/UploadOverlay.tsx +62 -0
- package/src/client/add-plugin-modal.tsx +199 -0
- package/src/client/api.ts +357 -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 +340 -0
- package/src/client/builtins/viewers.tsx +125 -0
- package/src/client/chunk-loader.ts +358 -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 +152 -0
- package/src/client/image-types.ts +8 -0
- package/src/client/ime-guard.ts +47 -0
- package/src/client/index.tsx +427 -0
- package/src/client/intercept.tsx +188 -0
- package/src/client/lang.ts +225 -0
- package/src/client/layout-push.ts +24 -0
- package/src/client/layout.css +90 -0
- package/src/client/lazy-chunk.tsx +89 -0
- package/src/client/link-intercept.ts +73 -0
- package/src/client/locales-ar.ts +368 -0
- package/src/client/locales-de.ts +353 -0
- package/src/client/locales-fr.ts +360 -0
- package/src/client/locales-hi.ts +367 -0
- package/src/client/locales-id.ts +365 -0
- package/src/client/locales-it.ts +358 -0
- package/src/client/locales-ja.ts +367 -0
- package/src/client/locales-ko.ts +359 -0
- package/src/client/locales-nl.ts +365 -0
- package/src/client/locales-pl.ts +369 -0
- package/src/client/locales-pt.ts +350 -0
- package/src/client/locales-ru.ts +365 -0
- package/src/client/locales-sv.ts +350 -0
- package/src/client/locales-th.ts +367 -0
- package/src/client/locales-tr.ts +367 -0
- package/src/client/locales-vi.ts +367 -0
- package/src/client/locales-zh-HK.ts +382 -0
- package/src/client/locales-zh-MO.ts +382 -0
- package/src/client/locales-zh-TW.ts +382 -0
- package/src/client/locales.ts +894 -0
- package/src/client/markdown-html.ts +331 -0
- package/src/client/markdown-images.ts +137 -0
- package/src/client/markdown-labels.tsx +52 -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 +143 -0
- package/src/client/paths.ts +63 -0
- package/src/client/pdf-types.ts +4 -0
- package/src/client/pinned.ts +183 -0
- package/src/client/plugin-settings.ts +43 -0
- package/src/client/plugins-shared.ts +29 -0
- package/src/client/plugins-tabs.ts +96 -0
- package/src/client/plugins-viewers.ts +30 -0
- package/src/client/prefs.ts +210 -0
- package/src/client/produced-files.ts +120 -0
- package/src/client/selection-payload.ts +86 -0
- package/src/client/service.ts +859 -0
- package/src/client/settings-nav-icon.ts +45 -0
- package/src/client/shell-presets.ts +83 -0
- package/src/client/sidebar.module.css +2788 -0
- package/src/client/sidechat-transcript.ts +321 -0
- package/src/client/split-pane.tsx +331 -0
- package/src/client/state.ts +1497 -0
- package/src/client/subagent-detect.ts +137 -0
- package/src/client/subagent-jobs.ts +161 -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/upload.ts +187 -0
- package/src/client/wco.ts +120 -0
- package/src/config.ts +154 -0
- package/src/context-types.ts +611 -0
- package/src/fs-operations.ts +95 -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 +1291 -0
- package/src/invariant.ts +32 -0
- package/src/jobs-routes.ts +257 -0
- package/src/open-external.ts +90 -0
- package/src/path-security.ts +75 -0
- package/src/prefs-shared.ts +280 -0
- package/src/pty-deps.ts +240 -0
- package/src/pty-manager.ts +376 -0
- package/src/sidechat-core.ts +468 -0
- package/src/sidechat-routes.ts +344 -0
- package/src/subagent-activity.ts +91 -0
- package/src/subagent-live-route.ts +93 -0
- package/src/tools.ts +482 -0
- package/src/trust-fence.ts +84 -0
- package/src/wire.ts +100 -0
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure subagent-membership helpers over the sessions list feed (structural
|
|
3
|
+
* mirror world — no runtime imports). Used by the sidebar's auto-activation
|
|
4
|
+
* effect and the Subagent page:
|
|
5
|
+
*
|
|
6
|
+
* - {@link directSubagentCount}: direct durable children of one session,
|
|
7
|
+
* - {@link detectNewDirectSubagent}: the 0 → N transition that means "a new
|
|
8
|
+
* subagent just spawned under the current session" (the auto-open trigger),
|
|
9
|
+
* - {@link countSubagentDescendants}: uninterrupted subagent-origin lineage
|
|
10
|
+
* totals (mirror of the official `indexSubagentDescendants` over the
|
|
11
|
+
* plugin's own summary rows).
|
|
12
|
+
*/
|
|
13
|
+
import type {
|
|
14
|
+
SidebarSessionList,
|
|
15
|
+
SidebarSessionSummary,
|
|
16
|
+
SidebarSubagentCatalog,
|
|
17
|
+
} from '../context-types.ts'
|
|
18
|
+
import { SIDE_LABEL_PREFIX } from '../sidechat-core.ts'
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Side Chat threads ride the subagent origin (main-list hiding + the RPC
|
|
22
|
+
* ownership fence) but they are NOT subagent topology: they carry the
|
|
23
|
+
* durable 'Side: ' label and live as sidebar tabs. Excluding them here
|
|
24
|
+
* keeps the auto-open trigger and the Subagent page counts clean.
|
|
25
|
+
*/
|
|
26
|
+
export function isSideThreadSummary(summary: SidebarSessionSummary): boolean {
|
|
27
|
+
return summary.origin === 'subagent' && summary.displayTitle.startsWith(SIDE_LABEL_PREFIX)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Count the direct subagent children of one session (durable `origin` rows). */
|
|
31
|
+
export function directSubagentCount(
|
|
32
|
+
byId: SidebarSessionList['byId'],
|
|
33
|
+
sessionId: string,
|
|
34
|
+
): number {
|
|
35
|
+
let count = 0
|
|
36
|
+
for (const summary of Object.values(byId)) {
|
|
37
|
+
if (summary.origin === 'subagent' && summary.parentId === sessionId
|
|
38
|
+
&& !isSideThreadSummary(summary)) count += 1
|
|
39
|
+
}
|
|
40
|
+
return count
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* The main agent of the current session's tree: walk the durable parent
|
|
45
|
+
* chain upward until the first non-subagent session. The Subagent page shows
|
|
46
|
+
* THIS root's full topology regardless of how deep the current selection is
|
|
47
|
+
* (a session whose row is still hydrating, or a broken chain, degrades to
|
|
48
|
+
* the session itself).
|
|
49
|
+
*/
|
|
50
|
+
export function rootAncestor(
|
|
51
|
+
byId: SidebarSessionList['byId'],
|
|
52
|
+
sessionId: string | undefined,
|
|
53
|
+
): string | undefined {
|
|
54
|
+
if (sessionId === undefined) return undefined
|
|
55
|
+
const seen = new Set<string>()
|
|
56
|
+
let current: SidebarSessionSummary | undefined = byId[sessionId]
|
|
57
|
+
while (current !== undefined && current.origin === 'subagent'
|
|
58
|
+
&& current.parentId !== undefined && !seen.has(current.id)) {
|
|
59
|
+
seen.add(current.id)
|
|
60
|
+
current = byId[current.parentId]
|
|
61
|
+
}
|
|
62
|
+
return current?.id ?? sessionId
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Collect every catalog branch (an entry with `hasChildren`) reachable from
|
|
67
|
+
* the root — the set of catalogs the always-expanded topology consumes.
|
|
68
|
+
* Cycles fail soft.
|
|
69
|
+
*/
|
|
70
|
+
export function collectBranchIds(
|
|
71
|
+
catalogs: Readonly<Record<string, SidebarSubagentCatalog>>,
|
|
72
|
+
rootId: string | undefined,
|
|
73
|
+
): string[] {
|
|
74
|
+
const out: string[] = []
|
|
75
|
+
const seen = new Set<string>()
|
|
76
|
+
const visit = (parentId: string): void => {
|
|
77
|
+
if (seen.has(parentId)) return
|
|
78
|
+
seen.add(parentId)
|
|
79
|
+
for (const entry of catalogs[parentId]?.entries ?? []) {
|
|
80
|
+
if (entry.kind === 'child' && entry.hasChildren) {
|
|
81
|
+
out.push(entry.id)
|
|
82
|
+
visit(entry.id)
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
if (rootId !== undefined) visit(rootId)
|
|
87
|
+
return out
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Whether a new direct subagent appeared under `sessionId` between two
|
|
92
|
+
* consecutive list snapshots (the count crossed 0 → >0). Switching to a
|
|
93
|
+
* session that already has subagents yields `false` (its baseline starts at
|
|
94
|
+
* the current count), so the auto-open never fights an existing layout.
|
|
95
|
+
*/
|
|
96
|
+
export function detectNewDirectSubagent(
|
|
97
|
+
prev: SidebarSessionList,
|
|
98
|
+
next: SidebarSessionList,
|
|
99
|
+
sessionId: string,
|
|
100
|
+
): boolean {
|
|
101
|
+
return directSubagentCount(prev.byId, sessionId) === 0
|
|
102
|
+
&& directSubagentCount(next.byId, sessionId) > 0
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Descendant totals of one session through an uninterrupted subagent-origin chain. */
|
|
106
|
+
export interface SubagentDescendantTotals {
|
|
107
|
+
count: number
|
|
108
|
+
runningCount: number
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Index every subagent descendant under each ancestor it reaches through an
|
|
113
|
+
* uninterrupted subagent-origin chain (same semantics as the official
|
|
114
|
+
* `indexSubagentDescendants`; cycles fail soft).
|
|
115
|
+
*/
|
|
116
|
+
export function countSubagentDescendants(
|
|
117
|
+
byId: SidebarSessionList['byId'],
|
|
118
|
+
sessionId: string,
|
|
119
|
+
): SubagentDescendantTotals {
|
|
120
|
+
const totals: SubagentDescendantTotals = { count: 0, runningCount: 0 }
|
|
121
|
+
for (const descendant of Object.values(byId)) {
|
|
122
|
+
if (descendant.origin !== 'subagent' || isSideThreadSummary(descendant)) continue
|
|
123
|
+
const seen = new Set<string>()
|
|
124
|
+
let current: SidebarSessionSummary | undefined = descendant
|
|
125
|
+
while (current?.origin === 'subagent' && current.parentId !== undefined
|
|
126
|
+
&& !seen.has(current.id)) {
|
|
127
|
+
seen.add(current.id)
|
|
128
|
+
if (current.parentId === sessionId) {
|
|
129
|
+
totals.count += 1
|
|
130
|
+
if (descendant.running === true) totals.runningCount += 1
|
|
131
|
+
break
|
|
132
|
+
}
|
|
133
|
+
current = byId[current.parentId]
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return totals
|
|
137
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure derivations for the Subagent page's background-job section. Kept
|
|
3
|
+
* framework-free so the node test environment can unit-test them: the job
|
|
4
|
+
* rows arrive through the harness `session/jobs` push mirror
|
|
5
|
+
* (`jobsBySession` in the sessions list feed) — nothing here issues
|
|
6
|
+
* requests, and the row ordering / status mapping mirror the official
|
|
7
|
+
* ui-jobs header list.
|
|
8
|
+
*/
|
|
9
|
+
import type {
|
|
10
|
+
SidebarSessionList,
|
|
11
|
+
SidebarSessionSummary,
|
|
12
|
+
SidebarJobStatus,
|
|
13
|
+
SidebarJobView,
|
|
14
|
+
} from '../context-types.ts'
|
|
15
|
+
import type { CopyKey } from './locales.ts'
|
|
16
|
+
|
|
17
|
+
/** One row of the jobs section: the job plus its owning session's title. */
|
|
18
|
+
export interface TreeJob {
|
|
19
|
+
ownerSessionId: string
|
|
20
|
+
ownerTitle: string
|
|
21
|
+
job: SidebarJobView
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Whether the registry still holds the job open (its duration ticks). */
|
|
25
|
+
export function isJobLive(job: SidebarJobView): boolean {
|
|
26
|
+
return job.status === 'running' || job.status === 'stopping'
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Every session id of the topology tree rooted at `rootId` (the root plus
|
|
31
|
+
* each session whose uninterrupted subagent-origin chain reaches it — same
|
|
32
|
+
* lineage semantics as {@link countSubagentDescendants}; cycles fail soft).
|
|
33
|
+
* Sessions outside the tree (orphans, other trees) are excluded, so the
|
|
34
|
+
* jobs section never shows foreign work.
|
|
35
|
+
*/
|
|
36
|
+
export function treeSessionIds(
|
|
37
|
+
byId: SidebarSessionList['byId'],
|
|
38
|
+
rootId: string | undefined,
|
|
39
|
+
): Set<string> {
|
|
40
|
+
const ids = new Set<string>()
|
|
41
|
+
if (rootId === undefined) return ids
|
|
42
|
+
for (const summary of Object.values(byId)) {
|
|
43
|
+
const seen = new Set<string>()
|
|
44
|
+
let current: SidebarSessionSummary | undefined = summary
|
|
45
|
+
let reachesRoot = false
|
|
46
|
+
while (current !== undefined && !seen.has(current.id)) {
|
|
47
|
+
seen.add(current.id)
|
|
48
|
+
if (current.id === rootId) {
|
|
49
|
+
reachesRoot = true
|
|
50
|
+
break
|
|
51
|
+
}
|
|
52
|
+
if (current.origin !== 'subagent' || current.parentId === undefined) break
|
|
53
|
+
current = byId[current.parentId]
|
|
54
|
+
}
|
|
55
|
+
if (reachesRoot) ids.add(summary.id)
|
|
56
|
+
}
|
|
57
|
+
return ids
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Whether a NEW background job appeared for one session between two
|
|
62
|
+
* consecutive list snapshots (a job id the previous snapshot lacked).
|
|
63
|
+
* Unlike the subagent auto-open (0 → N only), ANY new job id triggers: the
|
|
64
|
+
* agent may start several jobs over a session, and each new one should
|
|
65
|
+
* surface the Jobs page (a fresh page load never triggers — its baseline
|
|
66
|
+
* starts at the current snapshot).
|
|
67
|
+
*/
|
|
68
|
+
export function detectNewJob(
|
|
69
|
+
prev: SidebarSessionList,
|
|
70
|
+
next: SidebarSessionList,
|
|
71
|
+
sessionId: string,
|
|
72
|
+
): boolean {
|
|
73
|
+
const prevIds = new Set((prev.jobsBySession?.[sessionId] ?? []).map(job => job.id))
|
|
74
|
+
return (next.jobsBySession?.[sessionId] ?? []).some(job => !prevIds.has(job.id))
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Collect the background jobs of the whole current tree, owner-labeled.
|
|
79
|
+
* Sessions without a mirror entry contribute nothing; an absent mirror
|
|
80
|
+
* (runtime older than the jobs feed) yields an empty list.
|
|
81
|
+
*/
|
|
82
|
+
export function collectTreeJobs(
|
|
83
|
+
byId: SidebarSessionList['byId'],
|
|
84
|
+
jobsBySession: Readonly<Record<string, readonly SidebarJobView[]>> | undefined,
|
|
85
|
+
rootId: string | undefined,
|
|
86
|
+
): TreeJob[] {
|
|
87
|
+
const rows: TreeJob[] = []
|
|
88
|
+
if (jobsBySession === undefined) return rows
|
|
89
|
+
for (const sessionId of treeSessionIds(byId, rootId)) {
|
|
90
|
+
const jobs = jobsBySession[sessionId]
|
|
91
|
+
if (jobs === undefined || jobs.length === 0) continue
|
|
92
|
+
const ownerTitle = byId[sessionId]?.displayTitle ?? sessionId
|
|
93
|
+
for (const job of jobs) rows.push({ ownerSessionId: sessionId, ownerTitle, job })
|
|
94
|
+
}
|
|
95
|
+
return rows
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Live rows first in start order, then settled rows newest-first (mirror of
|
|
100
|
+
* the official ui-jobs ordering); a tie falls back to start order so the
|
|
101
|
+
* sort never depends on the host's map iteration.
|
|
102
|
+
*/
|
|
103
|
+
export function orderJobs(rows: readonly TreeJob[]): TreeJob[] {
|
|
104
|
+
return [...rows].sort((left, right) => {
|
|
105
|
+
const liveLeft = isJobLive(left.job)
|
|
106
|
+
if (liveLeft !== isJobLive(right.job)) return liveLeft ? -1 : 1
|
|
107
|
+
if (liveLeft) return left.job.startedAt - right.job.startedAt
|
|
108
|
+
const finished = (right.job.finishedAt ?? right.job.startedAt) - (left.job.finishedAt ?? left.job.startedAt)
|
|
109
|
+
return finished !== 0 ? finished : left.job.startedAt - right.job.startedAt
|
|
110
|
+
})
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** The sidebar's StateDot states for the five wire statuses. */
|
|
114
|
+
export type JobDotState = 'ongoing' | 'warning' | 'done' | 'error'
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Status marker semantics. `stopping` and `killed` share the attention
|
|
118
|
+
* color: both mean the work ended (or is ending) on request rather than on
|
|
119
|
+
* its own.
|
|
120
|
+
*/
|
|
121
|
+
export function jobDotState(status: SidebarJobStatus): JobDotState {
|
|
122
|
+
switch (status) {
|
|
123
|
+
case 'running': return 'ongoing'
|
|
124
|
+
case 'stopping': return 'warning'
|
|
125
|
+
case 'completed': return 'done'
|
|
126
|
+
case 'killed': return 'warning'
|
|
127
|
+
case 'failed': return 'error'
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** Human status word of one wire status (localized through the passed translator). */
|
|
132
|
+
export function jobStatusLabel(
|
|
133
|
+
status: SidebarJobStatus,
|
|
134
|
+
t: (key: CopyKey, params?: Record<string, string | number>) => string,
|
|
135
|
+
): string {
|
|
136
|
+
switch (status) {
|
|
137
|
+
case 'running': return t('jobStatusRunning')
|
|
138
|
+
case 'stopping': return t('jobStatusStopping')
|
|
139
|
+
case 'completed': return t('jobStatusCompleted')
|
|
140
|
+
case 'killed': return t('jobStatusKilled')
|
|
141
|
+
case 'failed': return t('jobStatusFailed')
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Elapsed time in at most two adjacent units (mirror of the official
|
|
147
|
+
* ui-jobs duration wording). A background job that outlives an hour is
|
|
148
|
+
* already exceptional, so hours is the widest unit.
|
|
149
|
+
*/
|
|
150
|
+
export function formatJobDuration(
|
|
151
|
+
elapsedMs: number,
|
|
152
|
+
t: (key: CopyKey, params?: Record<string, string | number>) => string,
|
|
153
|
+
): string {
|
|
154
|
+
const total = Math.max(0, Math.floor(elapsedMs / 1_000))
|
|
155
|
+
const seconds = total % 60
|
|
156
|
+
const minutes = Math.floor(total / 60) % 60
|
|
157
|
+
const hours = Math.floor(total / 3_600)
|
|
158
|
+
if (hours > 0) return t('jobDurationHours', { hours, minutes })
|
|
159
|
+
if (minutes > 0) return t('jobDurationMinutes', { minutes, seconds })
|
|
160
|
+
return t('jobDurationSeconds', { seconds })
|
|
161
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The memo comparator for one tab's content cell (TabContent in Sidebar.tsx).
|
|
3
|
+
*
|
|
4
|
+
* Kept as a pure module so the "what re-renders a tab cell" contract is unit
|
|
5
|
+
* testable: geometry/store re-renders of the Sidebar shell must NOT reconcile
|
|
6
|
+
* every mounted tab (xterm/CodeMirror/FileTree subtrees — issue #315), while
|
|
7
|
+
* locale switches (localeRevision), tab-registry updates (tabsVersion), and
|
|
8
|
+
* a tab MOVING BETWEEN PANES (paneId — onOpenDiff closes over the pane id,
|
|
9
|
+
* and moveTab reuses the SAME tab object, so only the pane id identifies the
|
|
10
|
+
* move) must re-render the cell.
|
|
11
|
+
*/
|
|
12
|
+
import type { SidebarTab } from './state.ts'
|
|
13
|
+
|
|
14
|
+
/** The fields whose change invalidates a tab cell's memo. Anything a cell's
|
|
15
|
+
* render (or its callbacks) depends on must be listed here. */
|
|
16
|
+
export interface TabContentMemoKey {
|
|
17
|
+
tab: SidebarTab
|
|
18
|
+
/** The pane this tab currently lives in. onOpenDiff closes over it, and
|
|
19
|
+
* moveTab reuses the same tab object when the tab moves panes — without
|
|
20
|
+
* comparing paneId the cell would keep the source pane's callback. */
|
|
21
|
+
paneId: string
|
|
22
|
+
sessionId: string
|
|
23
|
+
cwd: string | undefined
|
|
24
|
+
visible: boolean
|
|
25
|
+
expanded: string[]
|
|
26
|
+
/** Files highlighted in the file tree (the "Show in folder" reveal). */
|
|
27
|
+
revealed: string[]
|
|
28
|
+
localeRevision: string
|
|
29
|
+
tabsVersion: number
|
|
30
|
+
/** When the tab is a pinned virtual tab (injected from another session),
|
|
31
|
+
* this overrides `tab.id` passed to the tab descriptor's component so
|
|
32
|
+
* TerminalView connects to the home session's PTY by the original id.
|
|
33
|
+
* Undefined for regular tabs (no override). */
|
|
34
|
+
effectiveTabId: string | undefined
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** True when the cell may skip a re-render (all render-affecting fields
|
|
38
|
+
* unchanged). Callback/context identities are deliberately ignored: their
|
|
39
|
+
* captured dependencies are stable or covered by the compared fields
|
|
40
|
+
* (onReferenceFile → sessionId/cwd, onSubagentJump/onToggleDir → stable
|
|
41
|
+
* refs/closures, onOpenDiff → paneId). */
|
|
42
|
+
export function tabContentCompare(prev: TabContentMemoKey, next: TabContentMemoKey): boolean {
|
|
43
|
+
return (
|
|
44
|
+
prev.tab === next.tab &&
|
|
45
|
+
prev.paneId === next.paneId &&
|
|
46
|
+
prev.sessionId === next.sessionId &&
|
|
47
|
+
prev.cwd === next.cwd &&
|
|
48
|
+
prev.visible === next.visible &&
|
|
49
|
+
prev.expanded === next.expanded &&
|
|
50
|
+
prev.revealed === next.revealed &&
|
|
51
|
+
prev.localeRevision === next.localeRevision &&
|
|
52
|
+
prev.tabsVersion === next.tabsVersion &&
|
|
53
|
+
prev.effectiveTabId === next.effectiveTabId
|
|
54
|
+
)
|
|
55
|
+
}
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Terminal font resolution: the user's custom font prefs (SidebarPrefs,
|
|
3
|
+
* configured under the terminal card's secondary settings) turned into the
|
|
4
|
+
* xterm options. Kept as a pure module (no DOM, no xterm) so the fallback
|
|
5
|
+
* chain and clamping are unit-testable without mounting a terminal.
|
|
6
|
+
*/
|
|
7
|
+
import { clampTerminalFontSize, type SidebarPrefs } from '../prefs-shared.ts'
|
|
8
|
+
|
|
9
|
+
/** The built-in fallback stack when neither the user nor the theme sets one. */
|
|
10
|
+
export const DEFAULT_TERMINAL_FONT_FAMILY = '"SF Mono", Menlo, Consolas, "Liberation Mono", monospace'
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Icon fonts appended to whichever base stack wins, so shell prompts that
|
|
14
|
+
* draw glyphs from the Nerd Font Private Use Areas resolve to a real glyph
|
|
15
|
+
* instead of the missing-glyph box (aka tofu).
|
|
16
|
+
*
|
|
17
|
+
* Why this is needed even though the OS "should" fall back automatically:
|
|
18
|
+
* prompt frameworks (starship, powerlevel10k, oh-my-posh) take their icons
|
|
19
|
+
* from the PUA. Chromium's implicit system fallback reliably covers the
|
|
20
|
+
* *BMP* PUA (U+E000–U+F8FF — e.g. the U+E0B0 powerline separator) but NOT
|
|
21
|
+
* the *supplementary-plane* PUA-B (U+F0000+) where Nerd Fonts v3 relocated
|
|
22
|
+
* the Material Design icon set. Naming the families explicitly makes the
|
|
23
|
+
* browser consult them per character, which covers both planes.
|
|
24
|
+
*
|
|
25
|
+
* Deliberately NOT listed: color-emoji families. Chromium routes genuine
|
|
26
|
+
* emoji code points through a dedicated emoji fallback path (which is why
|
|
27
|
+
* emoji already render), so naming them buys nothing here — while placing a
|
|
28
|
+
* color font ahead of the generic family risks capturing BMP symbols the
|
|
29
|
+
* prompt expects in monospace (U+26A0 ⚠, U+2714 ✔ …) and rendering them as
|
|
30
|
+
* wide color glyphs that break the cell grid.
|
|
31
|
+
*
|
|
32
|
+
* Ordering rationale: the symbols-only patches ship glyphs without Latin,
|
|
33
|
+
* so they can never hijack ASCII metrics — the safest first hop. The
|
|
34
|
+
* fully-patched distributions follow for users who installed one of those
|
|
35
|
+
* instead. Both the `… Mono` and proportional family names are listed
|
|
36
|
+
* because the Nerd Fonts installers register them as distinct families.
|
|
37
|
+
*
|
|
38
|
+
* These are strictly *appended*, never prepended: xterm derives its cell
|
|
39
|
+
* metrics from the first entry, so the base font must stay in front or the
|
|
40
|
+
* whole grid would be re-measured against an icon font.
|
|
41
|
+
*/
|
|
42
|
+
export const ICON_FONT_FALLBACKS: readonly string[] = [
|
|
43
|
+
// Symbols-only patches (glyph coverage without Latin) — ideal fallbacks.
|
|
44
|
+
'"Symbols Nerd Font Mono"',
|
|
45
|
+
'"Symbols Nerd Font"',
|
|
46
|
+
// Common fully-patched Nerd Font distributions.
|
|
47
|
+
'"Hack Nerd Font Mono"',
|
|
48
|
+
'"Hack Nerd Font"',
|
|
49
|
+
'"JetBrainsMono Nerd Font Mono"',
|
|
50
|
+
'"JetBrainsMono Nerd Font"',
|
|
51
|
+
'"FiraCode Nerd Font Mono"',
|
|
52
|
+
'"FiraCode Nerd Font"',
|
|
53
|
+
'"CaskaydiaCove Nerd Font Mono"',
|
|
54
|
+
'"CaskaydiaCove Nerd Font"',
|
|
55
|
+
'"SauceCodePro Nerd Font Mono"',
|
|
56
|
+
'"UbuntuMono Nerd Font Mono"',
|
|
57
|
+
'"Iosevka Nerd Font Mono"',
|
|
58
|
+
'"MesloLGS Nerd Font Mono"',
|
|
59
|
+
'"MesloLGS NF"',
|
|
60
|
+
]
|
|
61
|
+
|
|
62
|
+
/** The symbols-only patch count at the head of {@link ICON_FONT_FALLBACKS}
|
|
63
|
+
* (these carry no Latin glyphs, so they can never hijack ASCII metrics). */
|
|
64
|
+
const SYMBOLS_ONLY_COUNT = 2
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* CSS generic font families. A generic is a catch-all that always resolves,
|
|
68
|
+
* so icon fonts must be spliced in *before* the first one to stay reachable.
|
|
69
|
+
*/
|
|
70
|
+
const GENERIC_FAMILIES = new Set([
|
|
71
|
+
'monospace', 'serif', 'sans-serif', 'cursive', 'fantasy',
|
|
72
|
+
'system-ui', 'ui-monospace', 'ui-serif', 'ui-sans-serif', 'ui-rounded',
|
|
73
|
+
'math', 'emoji', 'fangsong',
|
|
74
|
+
])
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* CSS-wide keywords. These are only valid as an *entire* declaration value,
|
|
78
|
+
* so a stack cannot be appended to one.
|
|
79
|
+
*/
|
|
80
|
+
const CSS_WIDE_KEYWORDS = new Set(['inherit', 'initial', 'unset', 'revert', 'revert-layer'])
|
|
81
|
+
|
|
82
|
+
/** Normalize one family name for comparison: unquote, collapse runs of
|
|
83
|
+
* whitespace, casefold. */
|
|
84
|
+
function normalizeFamily(family: string): string {
|
|
85
|
+
return family
|
|
86
|
+
.trim()
|
|
87
|
+
.replace(/^["']|["']$/g, '')
|
|
88
|
+
.replace(/\s+/g, ' ')
|
|
89
|
+
.trim()
|
|
90
|
+
.toLowerCase()
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Split a CSS font-family stack on its top-level commas.
|
|
95
|
+
*
|
|
96
|
+
* Naive `split(',')` would corrupt quoted family names containing a comma
|
|
97
|
+
* and function values such as `var(--x, monospace)`, so quotes (with
|
|
98
|
+
* backslash escapes) and parentheses are tracked.
|
|
99
|
+
*
|
|
100
|
+
* @param stack - a CSS font-family stack.
|
|
101
|
+
* @returns the trimmed, non-empty family entries in source order.
|
|
102
|
+
*/
|
|
103
|
+
function splitFamilies(stack: string): string[] {
|
|
104
|
+
const entries: string[] = []
|
|
105
|
+
let buffer = ''
|
|
106
|
+
let quote: string | null = null
|
|
107
|
+
let depth = 0
|
|
108
|
+
for (let i = 0; i < stack.length; i += 1) {
|
|
109
|
+
const char = stack[i] as string
|
|
110
|
+
if (quote !== null) {
|
|
111
|
+
// Escapes are copied verbatim so `\"` does not close the quote.
|
|
112
|
+
if (char === '\\' && i + 1 < stack.length) {
|
|
113
|
+
buffer += char + (stack[i + 1] as string)
|
|
114
|
+
i += 1
|
|
115
|
+
continue
|
|
116
|
+
}
|
|
117
|
+
buffer += char
|
|
118
|
+
if (char === quote) quote = null
|
|
119
|
+
continue
|
|
120
|
+
}
|
|
121
|
+
if (char === '"' || char === "'") {
|
|
122
|
+
quote = char
|
|
123
|
+
buffer += char
|
|
124
|
+
continue
|
|
125
|
+
}
|
|
126
|
+
if (char === '(') depth += 1
|
|
127
|
+
else if (char === ')') depth = Math.max(0, depth - 1)
|
|
128
|
+
else if (char === ',' && depth === 0) {
|
|
129
|
+
entries.push(buffer)
|
|
130
|
+
buffer = ''
|
|
131
|
+
continue
|
|
132
|
+
}
|
|
133
|
+
buffer += char
|
|
134
|
+
}
|
|
135
|
+
entries.push(buffer)
|
|
136
|
+
return entries.map(entry => entry.trim()).filter(entry => entry !== '')
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Append {@link ICON_FONT_FALLBACKS} to a CSS font-family stack, keeping
|
|
141
|
+
* the caller's own entries and order intact.
|
|
142
|
+
*
|
|
143
|
+
* - Families already named in `stack` are not duplicated (quote-, case- and
|
|
144
|
+
* whitespace-insensitive), so a user who already lists their Nerd Font
|
|
145
|
+
* keeps their exact priority.
|
|
146
|
+
* - The icon fonts are spliced in ahead of the *first* generic family
|
|
147
|
+
* (`monospace` etc.), because a generic always resolves: anything after it
|
|
148
|
+
* would never be consulted. A stack that OPENS with a generic is the one
|
|
149
|
+
* exception: only the symbols-only patches (no Latin) may precede it — a
|
|
150
|
+
* fully-patched Nerd Font there would become xterm's measuring base font
|
|
151
|
+
* and override the user/theme family precedence, so it is placed after the
|
|
152
|
+
* generic instead.
|
|
153
|
+
* - Idempotent — re-applying to an already-topped-up stack is a no-op, which
|
|
154
|
+
* matters because `TerminalView` diffs the resolved value against the live
|
|
155
|
+
* `term.options.fontFamily` before reflowing.
|
|
156
|
+
*
|
|
157
|
+
* @param stack - a CSS font-family stack (base font first).
|
|
158
|
+
* @returns the stack with icon fallbacks merged in.
|
|
159
|
+
*/
|
|
160
|
+
export function withIconFontFallbacks(stack: string): string {
|
|
161
|
+
const entries = splitFamilies(stack)
|
|
162
|
+
if (entries.length === 0) return ICON_FONT_FALLBACKS.join(', ')
|
|
163
|
+
|
|
164
|
+
const present = new Set(entries.map(normalizeFamily))
|
|
165
|
+
const notPresent = (family: string): boolean => !present.has(normalizeFamily(family))
|
|
166
|
+
const symbolsOnly = ICON_FONT_FALLBACKS.slice(0, SYMBOLS_ONLY_COUNT).filter(notPresent)
|
|
167
|
+
const patched = ICON_FONT_FALLBACKS.slice(SYMBOLS_ONLY_COUNT).filter(notPresent)
|
|
168
|
+
if (symbolsOnly.length === 0 && patched.length === 0) return entries.join(', ')
|
|
169
|
+
|
|
170
|
+
const firstGeneric = entries.findIndex(entry => GENERIC_FAMILIES.has(normalizeFamily(entry)))
|
|
171
|
+
const cut = firstGeneric === -1 ? entries.length : firstGeneric
|
|
172
|
+
// A fully-patched distribution ships Latin glyphs: placed ahead of a
|
|
173
|
+
// LEADING generic (cut === 0) it would become xterm's measuring/base font
|
|
174
|
+
// and override the user/theme family precedence. Only the symbols-only
|
|
175
|
+
// patches — which carry no Latin — may precede it; the patched fonts go
|
|
176
|
+
// after the generic (still reachable per character, never the base).
|
|
177
|
+
if (cut === 0) {
|
|
178
|
+
return [...symbolsOnly, entries[0]!, ...patched, ...entries.slice(1)].join(', ')
|
|
179
|
+
}
|
|
180
|
+
return [...entries.slice(0, cut), ...symbolsOnly, ...patched, ...entries.slice(cut)].join(', ')
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Reduce one link of the base-family chain to a usable stack, or `''` when
|
|
185
|
+
* it cannot carry appended fallbacks so the next link should win.
|
|
186
|
+
*
|
|
187
|
+
* Rejecting CSS-wide keywords matters because the theme font arrives as a
|
|
188
|
+
* raw token value (`tokenValue('--ds-font-family-code')`); skins do set
|
|
189
|
+
* tokens to `initial`/`inherit`/`unset` (see `effectiveTokenValue` in
|
|
190
|
+
* `theme.ts`, which guards the color tokens for the same reason). Appending
|
|
191
|
+
* to such a value yields an invalid `font-family`, which the CSSOM discards
|
|
192
|
+
* silently — the terminal would lose the theme font *and* the icon fonts.
|
|
193
|
+
*/
|
|
194
|
+
function usableBase(value: string | undefined): string {
|
|
195
|
+
const trimmed = (value ?? '').trim()
|
|
196
|
+
if (trimmed === '') return ''
|
|
197
|
+
if (CSS_WIDE_KEYWORDS.has(trimmed.toLowerCase())) return ''
|
|
198
|
+
return trimmed
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Resolve the xterm font options for the given prefs.
|
|
203
|
+
*
|
|
204
|
+
* The base family keeps its existing precedence — user pref > theme code
|
|
205
|
+
* font > built-in stack — and then {@link withIconFontFallbacks} tops it up
|
|
206
|
+
* so prompt icons resolve regardless of which base won.
|
|
207
|
+
*
|
|
208
|
+
* @param prefs - the current side card preferences.
|
|
209
|
+
* @param themeFontFamily - the app's theme code font (`--ds-font-family-code`
|
|
210
|
+
* token value, read live by the caller); undefined when the token is absent.
|
|
211
|
+
* @returns the `fontFamily` / `fontSize` xterm options.
|
|
212
|
+
*/
|
|
213
|
+
export function resolveTerminalFont(
|
|
214
|
+
prefs: SidebarPrefs,
|
|
215
|
+
themeFontFamily: string | undefined,
|
|
216
|
+
): { fontFamily: string; fontSize: number } {
|
|
217
|
+
const base = usableBase(prefs.terminalFontFamily)
|
|
218
|
+
|| usableBase(themeFontFamily)
|
|
219
|
+
|| DEFAULT_TERMINAL_FONT_FAMILY
|
|
220
|
+
return {
|
|
221
|
+
fontFamily: withIconFontFallbacks(base),
|
|
222
|
+
fontSize: clampTerminalFontSize(prefs.terminalFontSize),
|
|
223
|
+
}
|
|
224
|
+
}
|