@keith9681/dsh-sidebar 0.15.2
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 +0 -0
- package/README_EN.md +0 -0
- package/cordis.patch.yml +49 -0
- package/lib/client-editor.js +34651 -0
- package/lib/client-mermaid.js +200629 -0
- package/lib/client-registry.js +13388 -0
- package/lib/client-terminal.js +8904 -0
- package/lib/client.js +13388 -0
- package/lib/index.js +3959 -0
- package/lib/invariant.js +22 -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 +22 -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 +12 -0
- package/lib/types/client/FileTree.d.ts +36 -0
- package/lib/types/client/GitView.d.ts +8 -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 +43 -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 +23 -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 +268 -0
- package/lib/types/client/binary-download.d.ts +12 -0
- package/lib/types/client/breakpoints.d.ts +12 -0
- package/lib/types/client/browser.d.ts +50 -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 +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/editable-state.d.ts +27 -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 +57 -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 +33 -0
- package/lib/types/client/lang.d.ts +12 -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.d.ts +366 -0
- package/lib/types/client/mermaid-blocks.d.ts +41 -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 +41 -0
- package/lib/types/client/paths.d.ts +35 -0
- package/lib/types/client/pdf-types.d.ts +2 -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 +23 -0
- package/lib/types/client/selection-payload.d.ts +27 -0
- package/lib/types/client/service.d.ts +440 -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 +30 -0
- package/lib/types/client/state.d.ts +359 -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 +33 -0
- package/lib/types/client/terminal-font.d.ts +20 -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 +603 -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 +82 -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/prefs-shared.d.ts +225 -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 +31 -0
- package/lib/types/trust-fence.d.ts +24 -0
- package/lib/types/wire.d.ts +38 -0
- package/package.json +173 -0
- package/scripts/install.ps1 +289 -0
- package/scripts/install.sh +287 -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 +253 -0
- package/src/client/DiffTab.tsx +110 -0
- package/src/client/DiffView.tsx +305 -0
- package/src/client/EditorHost.tsx +525 -0
- package/src/client/FileTree.tsx +653 -0
- package/src/client/GitView.tsx +558 -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 +1152 -0
- package/src/client/SideChatView.module.css +437 -0
- package/src/client/SideChatView.tsx +644 -0
- package/src/client/Sidebar.tsx +1148 -0
- package/src/client/SubagentView.module.css +547 -0
- package/src/client/SubagentView.tsx +883 -0
- package/src/client/TabBar.tsx +305 -0
- package/src/client/TerminalView.tsx +383 -0
- package/src/client/TextEditor.tsx +500 -0
- package/src/client/TreePanel.tsx +273 -0
- package/src/client/UploadOverlay.tsx +62 -0
- package/src/client/add-plugin-modal.tsx +199 -0
- package/src/client/api.ts +335 -0
- package/src/client/binary-download.tsx +23 -0
- package/src/client/breakpoints.ts +51 -0
- package/src/client/browser.ts +119 -0
- package/src/client/builtins/index.ts +37 -0
- package/src/client/builtins/tabs.tsx +331 -0
- package/src/client/builtins/viewers.tsx +125 -0
- package/src/client/chunk-loader.ts +351 -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/editable-state.ts +70 -0
- package/src/client/editor-load.ts +92 -0
- package/src/client/frame-batcher.ts +56 -0
- package/src/client/icons.tsx +161 -0
- package/src/client/image-types.ts +8 -0
- package/src/client/ime-guard.ts +47 -0
- package/src/client/index.tsx +369 -0
- package/src/client/intercept.tsx +110 -0
- package/src/client/lang.ts +103 -0
- package/src/client/layout.css +94 -0
- package/src/client/lazy-chunk.tsx +89 -0
- package/src/client/link-intercept.ts +73 -0
- package/src/client/locales.ts +734 -0
- package/src/client/mermaid-blocks.ts +110 -0
- package/src/client/mermaid-sanitize.ts +93 -0
- package/src/client/mermaid.tsx +401 -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 +62 -0
- package/src/client/paths.ts +47 -0
- package/src/client/pdf-types.ts +4 -0
- package/src/client/plugin-settings.ts +43 -0
- package/src/client/plugins-shared.ts +29 -0
- package/src/client/plugins-tabs.ts +87 -0
- package/src/client/plugins-viewers.ts +30 -0
- package/src/client/prefs.ts +207 -0
- package/src/client/produced-files.ts +91 -0
- package/src/client/selection-payload.ts +86 -0
- package/src/client/service.ts +838 -0
- package/src/client/settings-nav-icon.ts +45 -0
- package/src/client/shell-presets.ts +83 -0
- package/src/client/sidebar.module.css +2577 -0
- package/src/client/sidechat-transcript.ts +321 -0
- package/src/client/split-pane.tsx +322 -0
- package/src/client/state.ts +1175 -0
- package/src/client/subagent-detect.ts +137 -0
- package/src/client/subagent-jobs.ts +161 -0
- package/src/client/tab-content-memo.ts +46 -0
- package/src/client/terminal-font.ts +28 -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 +155 -0
- package/src/context-types.ts +610 -0
- package/src/fs-operations.ts +99 -0
- package/src/fs-search.ts +86 -0
- package/src/fs-tree.ts +158 -0
- package/src/git.ts +244 -0
- package/src/html-route.ts +106 -0
- package/src/index.ts +1132 -0
- package/src/invariant.ts +32 -0
- package/src/jobs-routes.ts +257 -0
- package/src/open-external.ts +90 -0
- package/src/prefs-shared.ts +268 -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 +479 -0
- package/src/trust-fence.ts +77 -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,46 @@
|
|
|
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
|
+
localeRevision: string
|
|
27
|
+
tabsVersion: number
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** True when the cell may skip a re-render (all render-affecting fields
|
|
31
|
+
* unchanged). Callback/context identities are deliberately ignored: their
|
|
32
|
+
* captured dependencies are stable or covered by the compared fields
|
|
33
|
+
* (onReferenceFile → sessionId/cwd, onSubagentJump/onToggleDir → stable
|
|
34
|
+
* refs/closures, onOpenDiff → paneId). */
|
|
35
|
+
export function tabContentCompare(prev: TabContentMemoKey, next: TabContentMemoKey): boolean {
|
|
36
|
+
return (
|
|
37
|
+
prev.tab === next.tab &&
|
|
38
|
+
prev.paneId === next.paneId &&
|
|
39
|
+
prev.sessionId === next.sessionId &&
|
|
40
|
+
prev.cwd === next.cwd &&
|
|
41
|
+
prev.visible === next.visible &&
|
|
42
|
+
prev.expanded === next.expanded &&
|
|
43
|
+
prev.localeRevision === next.localeRevision &&
|
|
44
|
+
prev.tabsVersion === next.tabsVersion
|
|
45
|
+
)
|
|
46
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
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
|
+
* Resolve the xterm font options for the given prefs.
|
|
14
|
+
* @param prefs - the current side card preferences.
|
|
15
|
+
* @param themeFontFamily - the app's theme code font (`--ds-font-family-code`
|
|
16
|
+
* token value, read live by the caller); undefined when the token is absent.
|
|
17
|
+
* @returns the `fontFamily` / `fontSize` xterm options.
|
|
18
|
+
*/
|
|
19
|
+
export function resolveTerminalFont(
|
|
20
|
+
prefs: SidebarPrefs,
|
|
21
|
+
themeFontFamily: string | undefined,
|
|
22
|
+
): { fontFamily: string; fontSize: number } {
|
|
23
|
+
const custom = prefs.terminalFontFamily.trim()
|
|
24
|
+
return {
|
|
25
|
+
fontFamily: custom !== '' ? custom : (themeFontFamily || DEFAULT_TERMINAL_FONT_FAMILY),
|
|
26
|
+
fontSize: clampTerminalFontSize(prefs.terminalFontSize),
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Live theme access for surfaces that cannot consume the token colors
|
|
3
|
+
* directly — xterm's palette and CodeMirror's theme extensions need concrete
|
|
4
|
+
* values, but the app's scheme flips at runtime (ui-layout's ThemePresenter
|
|
5
|
+
* projects prefers-color-scheme and the user's choice onto
|
|
6
|
+
* body[data-ds-dark-theme] and html { color-scheme }). This module reads the
|
|
7
|
+
* resolved scheme and token values, and notifies subscribers on flips, so
|
|
8
|
+
* the terminal and the editor re-theme in place instead of freezing in the
|
|
9
|
+
* scheme they happened to be created under.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/** Whether the app shell resolved to the dark scheme.
|
|
13
|
+
*
|
|
14
|
+
* The presenter sets `html { color-scheme }` together with the body palette
|
|
15
|
+
* attribute, so a set color-scheme means the decision is authoritative (an
|
|
16
|
+
* absent attribute is then LIGHT even when the OS prefers dark — the user
|
|
17
|
+
* chose light). Before the presenter has run, fall back to the OS media
|
|
18
|
+
* query as the best guess.
|
|
19
|
+
*/
|
|
20
|
+
export function isDarkScheme(): boolean {
|
|
21
|
+
if (typeof document === 'undefined') return true
|
|
22
|
+
const decided = document.documentElement.style.colorScheme !== ''
|
|
23
|
+
if (decided) return document.body.hasAttribute('data-ds-dark-theme')
|
|
24
|
+
return typeof matchMedia !== 'undefined' && matchMedia('(prefers-color-scheme: dark)').matches
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** One token's computed value on <body> ('' while the theme has not applied). */
|
|
28
|
+
export function tokenValue(name: string): string {
|
|
29
|
+
if (typeof document === 'undefined') return ''
|
|
30
|
+
return getComputedStyle(document.body).getPropertyValue(name).trim()
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Minimal alpha for a token color to count as effectively opaque. Skin
|
|
34
|
+
* systems turn `--dsw-alias-bg-base` translucent for glass panels (the
|
|
35
|
+
* dsh-web-ui skins use rgba 0.16–0.7; `transparent` is 0); below this
|
|
36
|
+
* floor a text surface (terminal, editor) would render over the skin's
|
|
37
|
+
* backdrop art, so callers fall back to an opaque color. Values at or
|
|
38
|
+
* above the floor (e.g. a skin's scoped 0.96 porcelain) pass through —
|
|
39
|
+
* the skin still controls the surface. */
|
|
40
|
+
const OPAQUE_ALPHA_MIN = 0.9
|
|
41
|
+
|
|
42
|
+
/** The alpha channel of a computed CSS color, or null when the format is
|
|
43
|
+
* not parseable (named colors, `color()`… — treated as opaque). Handles
|
|
44
|
+
* the shapes getComputedStyle actually returns: the rgb()/rgba() and
|
|
45
|
+
* hsl()/hsla() function forms (comma or space syntax, with or without the
|
|
46
|
+
* `/ alpha` slot) and the #rgb/#rgba/#rrggbb/#rrggbbaa hex family. */
|
|
47
|
+
export function colorAlpha(color: string): number | null {
|
|
48
|
+
const s = color.trim()
|
|
49
|
+
// #rgb / #rgba / #rrggbb / #rrggbbaa
|
|
50
|
+
const hex = /^#([0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8})$/i.exec(s)
|
|
51
|
+
if (hex !== null) {
|
|
52
|
+
const digits = hex[1]!
|
|
53
|
+
if (digits.length === 3 || digits.length === 4) {
|
|
54
|
+
const a = digits.length === 4 ? digits[3]! : 'f'
|
|
55
|
+
return parseInt(a + a, 16) / 255
|
|
56
|
+
}
|
|
57
|
+
const alphaHex = digits.length === 8 ? digits.slice(6) : 'ff'
|
|
58
|
+
return parseInt(alphaHex, 16) / 255
|
|
59
|
+
}
|
|
60
|
+
// rgb(r g b) / rgb(r g b / a) / rgba(r, g, b, a) — the trailing slot is alpha.
|
|
61
|
+
const fn = /^(rgba?|hsla?)\(([^)]+)\)$/i.exec(s)
|
|
62
|
+
if (fn !== null) {
|
|
63
|
+
const parts = fn[2]!.split(/[,\s/]+/).filter(Boolean)
|
|
64
|
+
const alphaPart = parts[3]
|
|
65
|
+
if (alphaPart === undefined) return 1
|
|
66
|
+
const alpha = Number.parseFloat(alphaPart)
|
|
67
|
+
return Number.isFinite(alpha) ? alpha : 1
|
|
68
|
+
}
|
|
69
|
+
return null
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* A token value that actually PAINTS something — the guard for text
|
|
74
|
+
* surfaces (issue #90). Skin systems routinely set global tokens to
|
|
75
|
+
* `transparent` (glass skins) or translucent glass values (`rgba(…,0.16–0.7)`,
|
|
76
|
+
* e.g. the dsh-web-ui skins) — both are truthy strings, so callers using
|
|
77
|
+
* `|| fallback` never fire and the terminal/editor goes see-through over
|
|
78
|
+
* the skin's backdrop. This returns '' for visually inert values (unset
|
|
79
|
+
* keywords, transparent, and any color below the opacity floor) so the
|
|
80
|
+
* caller's fallback chain engages; effectively opaque values pass through.
|
|
81
|
+
*/
|
|
82
|
+
export function effectiveTokenValue(name: string): string {
|
|
83
|
+
const raw = tokenValue(name)
|
|
84
|
+
switch (raw) {
|
|
85
|
+
case '':
|
|
86
|
+
case 'transparent':
|
|
87
|
+
case 'initial':
|
|
88
|
+
case 'inherit':
|
|
89
|
+
case 'unset':
|
|
90
|
+
return ''
|
|
91
|
+
default: {
|
|
92
|
+
const alpha = colorAlpha(raw)
|
|
93
|
+
if (alpha !== null && alpha < OPAQUE_ALPHA_MIN) return ''
|
|
94
|
+
return raw
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Subscribe to color-scheme flips (the presenter toggles the body
|
|
101
|
+
* attribute). The callback fires after the attribute changed; re-read the
|
|
102
|
+
* scheme inside it.
|
|
103
|
+
* @returns the disposer.
|
|
104
|
+
*/
|
|
105
|
+
export function subscribeColorScheme(callback: () => void): () => void {
|
|
106
|
+
if (typeof document === 'undefined') return () => {}
|
|
107
|
+
const observer = new MutationObserver(() => { callback() })
|
|
108
|
+
observer.observe(document.body, { attributes: true, attributeFilter: ['data-ds-dark-theme'] })
|
|
109
|
+
return () => { observer.disconnect() }
|
|
110
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The title-bar strip resolution chain — the ONE place that decides how
|
|
3
|
+
* many pixels the sidebar yields at the top. Standard signals first, then
|
|
4
|
+
* the user's chosen scheme; never a per-shell branch:
|
|
5
|
+
*
|
|
6
|
+
* 0. `web` scheme — EXPLICIT "DSH official web": never adapt, not even
|
|
7
|
+
* standard WCO geometry (the user declares the plain web UI).
|
|
8
|
+
* 1. Window Controls Overlay real geometry (standard API, authoritative
|
|
9
|
+
* when present — even 0, e.g. the overlay is hidden while maximized).
|
|
10
|
+
* 2. The `dsh-desktop-titlebar-inset` URL contract parameter (a shell
|
|
11
|
+
* declares the exact pixels it reserves).
|
|
12
|
+
* 3. The active shell preset's strip (scheme `preset` — opt-in data).
|
|
13
|
+
* 4. The legacy manual `titleBarStripPx` (scheme `custom`).
|
|
14
|
+
* 5. 0 — plain-browser semantics, nothing modified.
|
|
15
|
+
*
|
|
16
|
+
* The result drives `--dsh-title-bar-strip` + `body[data-dsh-title-bar-compat]`
|
|
17
|
+
* exactly like the legacy boolean did; only the VALUE source changed.
|
|
18
|
+
*/
|
|
19
|
+
import type { DesktopEnv } from './desktop-env.ts'
|
|
20
|
+
import type { TitleBarScheme } from '../prefs-shared.ts'
|
|
21
|
+
import type { WcoSnapshot } from './wco.ts'
|
|
22
|
+
import { presetStripFor, type ShellPreset } from './shell-presets.ts'
|
|
23
|
+
|
|
24
|
+
export function computeTitleBarStrip(
|
|
25
|
+
env: DesktopEnv,
|
|
26
|
+
wco: WcoSnapshot,
|
|
27
|
+
scheme: TitleBarScheme,
|
|
28
|
+
preset: ShellPreset | undefined,
|
|
29
|
+
customStripPx: number,
|
|
30
|
+
): number {
|
|
31
|
+
if (scheme === 'web') return 0
|
|
32
|
+
if (wco.present) return wco.height
|
|
33
|
+
if (env.titlebarInset > 0) return env.titlebarInset
|
|
34
|
+
if (scheme === 'preset') return presetStripFor(preset, env) ?? 0
|
|
35
|
+
if (scheme === 'custom') return customStripPx
|
|
36
|
+
return 0
|
|
37
|
+
}
|