dsh-better-sidebar 0.10.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 +139 -0
- package/README_EN.md +139 -0
- package/lib/client-docx.js +7649 -0
- package/lib/client-editor.js +32677 -0
- package/lib/client-pptx.js +63438 -0
- package/lib/client-registry.js +8021 -0
- package/lib/client-terminal.js +8060 -0
- package/lib/client-xlsx.js +359755 -0
- package/lib/client.js +8021 -0
- package/lib/index.js +2604 -0
- package/lib/invariant.js +22 -0
- package/lib/types/agent-pty.d.ts +220 -0
- package/lib/types/browser-probe.d.ts +15 -0
- package/lib/types/bundle-route.d.ts +13 -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 +10 -0
- package/lib/types/client/ExplorerView.d.ts +9 -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/PptxView.d.ts +6 -0
- package/lib/types/client/SandboxStatusBar.d.ts +10 -0
- package/lib/types/client/SideCardSection.d.ts +27 -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 +38 -0
- package/lib/types/client/TerminalView.d.ts +8 -0
- package/lib/types/client/TextEditor.d.ts +10 -0
- package/lib/types/client/api.d.ts +170 -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 +17 -0
- package/lib/types/client/builtins/tabs.d.ts +6 -0
- package/lib/types/client/builtins/viewers.d.ts +3 -0
- package/lib/types/client/chunk-loader.d.ts +74 -0
- package/lib/types/client/chunks/docx.d.ts +8 -0
- package/lib/types/client/chunks/editor.d.ts +9 -0
- package/lib/types/client/chunks/pptx.d.ts +8 -0
- package/lib/types/client/chunks/terminal.d.ts +9 -0
- package/lib/types/client/chunks/xlsx.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/docx-view.d.ts +8 -0
- package/lib/types/client/editor-load.d.ts +66 -0
- package/lib/types/client/icons.d.ts +47 -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 +10 -0
- package/lib/types/client/intercept.d.ts +20 -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 +35 -0
- package/lib/types/client/locales.d.ts +237 -0
- package/lib/types/client/office-shared.d.ts +33 -0
- package/lib/types/client/office-types.d.ts +36 -0
- package/lib/types/client/office-view.d.ts +12 -0
- package/lib/types/client/openpath-intercept.d.ts +41 -0
- package/lib/types/client/paths.d.ts +19 -0
- package/lib/types/client/pdf-types.d.ts +2 -0
- package/lib/types/client/prefs.d.ts +30 -0
- package/lib/types/client/produced-files.d.ts +27 -0
- package/lib/types/client/selection-payload.d.ts +27 -0
- package/lib/types/client/service.d.ts +214 -0
- package/lib/types/client/split-pane.d.ts +29 -0
- package/lib/types/client/state.d.ts +322 -0
- package/lib/types/client/subagent-activity.d.ts +35 -0
- package/lib/types/client/subagent-detect.d.ts +47 -0
- package/lib/types/client/subagent-jobs.d.ts +63 -0
- package/lib/types/client/theme.d.ts +28 -0
- package/lib/types/client/xlsx-to-univer.d.ts +33 -0
- package/lib/types/client/xlsx-view.d.ts +10 -0
- package/lib/types/config.d.ts +41 -0
- package/lib/types/context-types.d.ts +431 -0
- package/lib/types/fs-tree.d.ts +41 -0
- package/lib/types/git.d.ts +75 -0
- package/lib/types/html-route.d.ts +48 -0
- package/lib/types/index.d.ts +39 -0
- package/lib/types/invariant.d.ts +15 -0
- package/lib/types/jobs-routes.d.ts +45 -0
- package/lib/types/prefs-shared.d.ts +106 -0
- package/lib/types/pty-manager.d.ts +76 -0
- package/lib/types/tools.d.ts +28 -0
- package/lib/types/trust-fence.d.ts +24 -0
- package/lib/types/wire.d.ts +38 -0
- package/package.json +168 -0
- package/src/agent-pty.ts +519 -0
- package/src/browser-probe.ts +26 -0
- package/src/bundle-route.ts +130 -0
- package/src/client/BrowserView.tsx +253 -0
- package/src/client/DiffTab.tsx +110 -0
- package/src/client/DiffView.tsx +266 -0
- package/src/client/EditorHost.tsx +101 -0
- package/src/client/ExplorerView.tsx +283 -0
- package/src/client/GitView.tsx +540 -0
- package/src/client/OrphanedTab.tsx +27 -0
- package/src/client/PdfView.tsx +110 -0
- package/src/client/PptxView.tsx +131 -0
- package/src/client/SandboxStatusBar.tsx +60 -0
- package/src/client/SideCardSection.module.css +271 -0
- package/src/client/SideCardSection.tsx +426 -0
- package/src/client/Sidebar.tsx +861 -0
- package/src/client/SubagentView.module.css +547 -0
- package/src/client/SubagentView.tsx +866 -0
- package/src/client/TabBar.tsx +188 -0
- package/src/client/TerminalView.tsx +233 -0
- package/src/client/TextEditor.tsx +382 -0
- package/src/client/api.ts +215 -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 +33 -0
- package/src/client/builtins/tabs.tsx +211 -0
- package/src/client/builtins/viewers.tsx +151 -0
- package/src/client/chunk-loader.ts +196 -0
- package/src/client/chunks/docx.tsx +8 -0
- package/src/client/chunks/editor.tsx +9 -0
- package/src/client/chunks/pptx.tsx +8 -0
- package/src/client/chunks/terminal.tsx +9 -0
- package/src/client/chunks/xlsx.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/docx-view.tsx +113 -0
- package/src/client/editor-load.ts +92 -0
- package/src/client/icons.tsx +137 -0
- package/src/client/image-types.ts +8 -0
- package/src/client/ime-guard.ts +47 -0
- package/src/client/index.tsx +238 -0
- package/src/client/intercept.tsx +94 -0
- package/src/client/lang.ts +95 -0
- package/src/client/layout.css +54 -0
- package/src/client/lazy-chunk.tsx +89 -0
- package/src/client/link-intercept.ts +66 -0
- package/src/client/locales.ts +476 -0
- package/src/client/office-shared.tsx +40 -0
- package/src/client/office-types.ts +48 -0
- package/src/client/office-view.tsx +12 -0
- package/src/client/openpath-intercept.ts +62 -0
- package/src/client/paths.ts +28 -0
- package/src/client/pdf-types.ts +4 -0
- package/src/client/prefs.ts +100 -0
- package/src/client/produced-files.ts +87 -0
- package/src/client/selection-payload.ts +86 -0
- package/src/client/service.ts +434 -0
- package/src/client/sidebar.module.css +1917 -0
- package/src/client/split-pane.tsx +292 -0
- package/src/client/state.ts +1048 -0
- package/src/client/subagent-activity.ts +67 -0
- package/src/client/subagent-detect.ts +125 -0
- package/src/client/subagent-jobs.ts +161 -0
- package/src/client/theme.ts +44 -0
- package/src/client/xlsx-to-univer.ts +260 -0
- package/src/client/xlsx-view.tsx +116 -0
- package/src/config.ts +95 -0
- package/src/context-types.ts +445 -0
- package/src/fs-tree.ts +115 -0
- package/src/git.ts +236 -0
- package/src/html-route.ts +70 -0
- package/src/index.ts +897 -0
- package/src/invariant.ts +32 -0
- package/src/jobs-routes.ts +257 -0
- package/src/prefs-shared.ts +127 -0
- package/src/pty-manager.ts +199 -0
- package/src/tools.ts +477 -0
- package/src/trust-fence.ts +77 -0
- package/src/wire.ts +94 -0
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure derivation of the compact LIVE line shown on a running subagent card:
|
|
3
|
+
* the last text output and the last tool call of the child's history tail
|
|
4
|
+
* (`subagent.history` events, oldest → newest). Renders nothing itself — the
|
|
5
|
+
* SubagentLiveLine component turns this into the card's status lines.
|
|
6
|
+
* Kept framework-free so the parser is unit-testable in the node environment.
|
|
7
|
+
*/
|
|
8
|
+
import type { SidebarHistoryEntry } from '../context-types.ts'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Extract the concatenated plain text of a content-block list (the durable
|
|
12
|
+
* `ContentBlock[]` shape, structurally: blocks with `type: 'text'` carry
|
|
13
|
+
* `text`; anything else — tool_use, image, … — contributes nothing).
|
|
14
|
+
* @param content - the raw `content` field of a message event.
|
|
15
|
+
* @returns the joined text, or undefined when the message carries no text.
|
|
16
|
+
*/
|
|
17
|
+
export function contentText(content: unknown): string | undefined {
|
|
18
|
+
if (!Array.isArray(content)) return undefined
|
|
19
|
+
const parts: string[] = []
|
|
20
|
+
for (const block of content) {
|
|
21
|
+
if (block === null || typeof block !== 'object') continue
|
|
22
|
+
const candidate = block as { type?: unknown; text?: unknown }
|
|
23
|
+
if (candidate.type === 'text' && typeof candidate.text === 'string') {
|
|
24
|
+
parts.push(candidate.text)
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return parts.length > 0 ? parts.join('\n') : undefined
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** The live status of one subagent card (both fields optional). */
|
|
31
|
+
export interface LastActivity {
|
|
32
|
+
/** The latest assembled assistant text output in the tail. */
|
|
33
|
+
text?: string
|
|
34
|
+
/** The latest tool call in the tail. */
|
|
35
|
+
tool?: { name: string; args: string }
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Fold a history tail into the last text output + last tool call (each is
|
|
40
|
+
* the LAST occurrence in event order). Lifecycle events and raw
|
|
41
|
+
* `assistant/chunk` rows are ignored — the card shows what the subagent is
|
|
42
|
+
* doing right now, not its plumbing.
|
|
43
|
+
* @param entries - the tail page from `subagent.history` (oldest → newest).
|
|
44
|
+
* @returns the last text and/or tool call; an empty object when the tail has neither.
|
|
45
|
+
*/
|
|
46
|
+
export function lastActivity(entries: SidebarHistoryEntry[]): LastActivity {
|
|
47
|
+
let text: string | undefined
|
|
48
|
+
let tool: { name: string; args: string } | undefined
|
|
49
|
+
for (const entry of entries) {
|
|
50
|
+
const { type, data } = entry.event
|
|
51
|
+
if (type === 'assistant/message') {
|
|
52
|
+
const message = data.message as { content?: unknown } | undefined
|
|
53
|
+
const extracted = contentText(message?.content)
|
|
54
|
+
if (extracted !== undefined) text = extracted
|
|
55
|
+
} else if (type === 'tool/call') {
|
|
56
|
+
tool = {
|
|
57
|
+
name: typeof data.name === 'string' ? data.name : 'tool',
|
|
58
|
+
args: typeof data.arguments === 'string' ? data.arguments : '',
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
if (text === undefined && tool === undefined) return {}
|
|
63
|
+
return {
|
|
64
|
+
...(text === undefined ? {} : { text }),
|
|
65
|
+
...(tool === undefined ? {} : { tool }),
|
|
66
|
+
}
|
|
67
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
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
|
+
|
|
19
|
+
/** Count the direct subagent children of one session (durable `origin` rows). */
|
|
20
|
+
export function directSubagentCount(
|
|
21
|
+
byId: SidebarSessionList['byId'],
|
|
22
|
+
sessionId: string,
|
|
23
|
+
): number {
|
|
24
|
+
let count = 0
|
|
25
|
+
for (const summary of Object.values(byId)) {
|
|
26
|
+
if (summary.origin === 'subagent' && summary.parentId === sessionId) count += 1
|
|
27
|
+
}
|
|
28
|
+
return count
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* The main agent of the current session's tree: walk the durable parent
|
|
33
|
+
* chain upward until the first non-subagent session. The Subagent page shows
|
|
34
|
+
* THIS root's full topology regardless of how deep the current selection is
|
|
35
|
+
* (a session whose row is still hydrating, or a broken chain, degrades to
|
|
36
|
+
* the session itself).
|
|
37
|
+
*/
|
|
38
|
+
export function rootAncestor(
|
|
39
|
+
byId: SidebarSessionList['byId'],
|
|
40
|
+
sessionId: string | undefined,
|
|
41
|
+
): string | undefined {
|
|
42
|
+
if (sessionId === undefined) return undefined
|
|
43
|
+
const seen = new Set<string>()
|
|
44
|
+
let current: SidebarSessionSummary | undefined = byId[sessionId]
|
|
45
|
+
while (current !== undefined && current.origin === 'subagent'
|
|
46
|
+
&& current.parentId !== undefined && !seen.has(current.id)) {
|
|
47
|
+
seen.add(current.id)
|
|
48
|
+
current = byId[current.parentId]
|
|
49
|
+
}
|
|
50
|
+
return current?.id ?? sessionId
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Collect every catalog branch (an entry with `hasChildren`) reachable from
|
|
55
|
+
* the root — the set of catalogs the always-expanded topology consumes.
|
|
56
|
+
* Cycles fail soft.
|
|
57
|
+
*/
|
|
58
|
+
export function collectBranchIds(
|
|
59
|
+
catalogs: Readonly<Record<string, SidebarSubagentCatalog>>,
|
|
60
|
+
rootId: string | undefined,
|
|
61
|
+
): string[] {
|
|
62
|
+
const out: string[] = []
|
|
63
|
+
const seen = new Set<string>()
|
|
64
|
+
const visit = (parentId: string): void => {
|
|
65
|
+
if (seen.has(parentId)) return
|
|
66
|
+
seen.add(parentId)
|
|
67
|
+
for (const entry of catalogs[parentId]?.entries ?? []) {
|
|
68
|
+
if (entry.kind === 'child' && entry.hasChildren) {
|
|
69
|
+
out.push(entry.id)
|
|
70
|
+
visit(entry.id)
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
if (rootId !== undefined) visit(rootId)
|
|
75
|
+
return out
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Whether a new direct subagent appeared under `sessionId` between two
|
|
80
|
+
* consecutive list snapshots (the count crossed 0 → >0). Switching to a
|
|
81
|
+
* session that already has subagents yields `false` (its baseline starts at
|
|
82
|
+
* the current count), so the auto-open never fights an existing layout.
|
|
83
|
+
*/
|
|
84
|
+
export function detectNewDirectSubagent(
|
|
85
|
+
prev: SidebarSessionList,
|
|
86
|
+
next: SidebarSessionList,
|
|
87
|
+
sessionId: string,
|
|
88
|
+
): boolean {
|
|
89
|
+
return directSubagentCount(prev.byId, sessionId) === 0
|
|
90
|
+
&& directSubagentCount(next.byId, sessionId) > 0
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Descendant totals of one session through an uninterrupted subagent-origin chain. */
|
|
94
|
+
export interface SubagentDescendantTotals {
|
|
95
|
+
count: number
|
|
96
|
+
runningCount: number
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Index every subagent descendant under each ancestor it reaches through an
|
|
101
|
+
* uninterrupted subagent-origin chain (same semantics as the official
|
|
102
|
+
* `indexSubagentDescendants`; cycles fail soft).
|
|
103
|
+
*/
|
|
104
|
+
export function countSubagentDescendants(
|
|
105
|
+
byId: SidebarSessionList['byId'],
|
|
106
|
+
sessionId: string,
|
|
107
|
+
): SubagentDescendantTotals {
|
|
108
|
+
const totals: SubagentDescendantTotals = { count: 0, runningCount: 0 }
|
|
109
|
+
for (const descendant of Object.values(byId)) {
|
|
110
|
+
if (descendant.origin !== 'subagent') continue
|
|
111
|
+
const seen = new Set<string>()
|
|
112
|
+
let current: SidebarSessionSummary | undefined = descendant
|
|
113
|
+
while (current?.origin === 'subagent' && current.parentId !== undefined
|
|
114
|
+
&& !seen.has(current.id)) {
|
|
115
|
+
seen.add(current.id)
|
|
116
|
+
if (current.parentId === sessionId) {
|
|
117
|
+
totals.count += 1
|
|
118
|
+
if (descendant.running === true) totals.runningCount += 1
|
|
119
|
+
break
|
|
120
|
+
}
|
|
121
|
+
current = byId[current.parentId]
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return totals
|
|
125
|
+
}
|
|
@@ -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,44 @@
|
|
|
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
|
+
/**
|
|
34
|
+
* Subscribe to color-scheme flips (the presenter toggles the body
|
|
35
|
+
* attribute). The callback fires after the attribute changed; re-read the
|
|
36
|
+
* scheme inside it.
|
|
37
|
+
* @returns the disposer.
|
|
38
|
+
*/
|
|
39
|
+
export function subscribeColorScheme(callback: () => void): () => void {
|
|
40
|
+
if (typeof document === 'undefined') return () => {}
|
|
41
|
+
const observer = new MutationObserver(() => { callback() })
|
|
42
|
+
observer.observe(document.body, { attributes: true, attributeFilter: ['data-ds-dark-theme'] })
|
|
43
|
+
return () => { observer.disconnect() }
|
|
44
|
+
}
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Convert a SheetJS (xlsx) workbook into Univer's {@link IWorkbookData} so
|
|
3
|
+
* {@link XlsxView} can render it through the Univer sheets preset.
|
|
4
|
+
*
|
|
5
|
+
* v1 scope (matches docs/plans/2026-08-10-office-preview-design.md §3.6):
|
|
6
|
+
* - Sheet names + order
|
|
7
|
+
* - Cell values, typed (string / number / boolean / error → force-string)
|
|
8
|
+
* - Cell formulas (text form; Univer's formula engine computes them)
|
|
9
|
+
* - Merged cells (!merges → mergeData)
|
|
10
|
+
* - Column widths (!cols → columnData) and row heights (!rows → rowData)
|
|
11
|
+
*
|
|
12
|
+
* Out of scope for v1 (SheetJS community edition does not parse them and the
|
|
13
|
+
* Pro edition is commercial): cell styles (font/fill/border/alignment),
|
|
14
|
+
* conditional formatting, charts. Users who need style fidelity get the
|
|
15
|
+
* download button on the binary placeholder.
|
|
16
|
+
*
|
|
17
|
+
* The function is pure (no DOM, no Univer imports) so it is unit-testable
|
|
18
|
+
* without jsdom or canvas.
|
|
19
|
+
*/
|
|
20
|
+
import type * as XLSX from 'xlsx'
|
|
21
|
+
// Types come from @univerjs/presets (which re-exports @univerjs/core's type
|
|
22
|
+
// surface). The enum values BooleanNumber / CellValueType are imported as
|
|
23
|
+
// runtime values from the same module — the bundler inlines them, and the
|
|
24
|
+
// test environment has @univerjs/presets available as a dependency.
|
|
25
|
+
import { BooleanNumber, CellValueType, type LocaleType, type ICellData, type IColumnData, type IRowData, type IWorkbookData, type IWorksheetData, type IObjectArrayPrimitiveType, type IObjectMatrixPrimitiveType, type IRange } from '@univerjs/presets'
|
|
26
|
+
|
|
27
|
+
/** SheetJS workbook (the slice of the module we use). */
|
|
28
|
+
type XLSXWorkbook = XLSX.WorkBook
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Build a Univer workbook snapshot from a SheetJS workbook.
|
|
32
|
+
*
|
|
33
|
+
* @param wb - the parsed workbook from `XLSX.read(buf, { type: 'array' })`
|
|
34
|
+
* @param appVersion - the Univer package version, written into `appVersion`
|
|
35
|
+
* (Univer requires the field but does not gate on it).
|
|
36
|
+
* @param locale - the locale Univer should render in.
|
|
37
|
+
*/
|
|
38
|
+
export function xlsxWorkbookToUniver(
|
|
39
|
+
wb: XLSXWorkbook,
|
|
40
|
+
appVersion: string,
|
|
41
|
+
locale: LocaleType,
|
|
42
|
+
): IWorkbookData {
|
|
43
|
+
const sheetOrder: string[] = []
|
|
44
|
+
const sheets: Record<string, Partial<IWorksheetData>> = {}
|
|
45
|
+
|
|
46
|
+
wb.SheetNames.forEach((name, index) => {
|
|
47
|
+
const sheetId = `sheet-${index}`
|
|
48
|
+
sheetOrder.push(sheetId)
|
|
49
|
+
sheets[sheetId] = worksheetToUniver(wb.Sheets[name] ?? {}, name, sheetId)
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
// Univer requires at least one sheet; an empty workbook gets a placeholder.
|
|
53
|
+
if (sheetOrder.length === 0) {
|
|
54
|
+
const fallbackId = 'sheet-0'
|
|
55
|
+
sheetOrder.push(fallbackId)
|
|
56
|
+
sheets[fallbackId] = {
|
|
57
|
+
id: fallbackId,
|
|
58
|
+
name: 'Sheet1',
|
|
59
|
+
rowCount: 20,
|
|
60
|
+
columnCount: 26,
|
|
61
|
+
cellData: {},
|
|
62
|
+
rowData: {},
|
|
63
|
+
columnData: {},
|
|
64
|
+
mergeData: [],
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
id: 'workbook',
|
|
70
|
+
name: wb.Props?.Title || 'Workbook',
|
|
71
|
+
appVersion,
|
|
72
|
+
locale,
|
|
73
|
+
styles: {},
|
|
74
|
+
sheetOrder,
|
|
75
|
+
sheets,
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Convert one SheetJS worksheet to a Univer IWorksheetData. */
|
|
80
|
+
function worksheetToUniver(
|
|
81
|
+
ws: XLSX.WorkSheet,
|
|
82
|
+
name: string,
|
|
83
|
+
sheetId: string,
|
|
84
|
+
): Partial<IWorksheetData> {
|
|
85
|
+
const cellData: IObjectMatrixPrimitiveType<ICellData> = {}
|
|
86
|
+
const merges: IRange[] = []
|
|
87
|
+
|
|
88
|
+
// Cell values + formulas: iterate every cell address (skip the !-prefixed
|
|
89
|
+
// special keys SheetJS uses for sheet-level metadata).
|
|
90
|
+
for (const addr of Object.keys(ws)) {
|
|
91
|
+
if (addr.startsWith('!')) continue
|
|
92
|
+
const cell = ws[addr] as XLSX.CellObject | undefined
|
|
93
|
+
if (cell === undefined) continue
|
|
94
|
+
const { r, c } = decodeAddr(addr)
|
|
95
|
+
const converted = convertCell(cell)
|
|
96
|
+
if (converted === null) continue
|
|
97
|
+
let row = cellData[r]
|
|
98
|
+
if (row === undefined) {
|
|
99
|
+
row = {}
|
|
100
|
+
cellData[r] = row
|
|
101
|
+
}
|
|
102
|
+
row[c] = converted
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Merged cells: SheetJS's Range uses { s: { r, c }, e: { r, c } } (0-indexed),
|
|
106
|
+
// which is exactly Univer's IRange shape — copy verbatim.
|
|
107
|
+
if (Array.isArray(ws['!merges'])) {
|
|
108
|
+
for (const m of ws['!merges']!) {
|
|
109
|
+
if (m === undefined || m === null) continue
|
|
110
|
+
merges.push({ startRow: m.s.r, endRow: m.e.r, startColumn: m.s.c, endColumn: m.e.c })
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const { rowCount, columnCount } = dimensions(ws, cellData)
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
id: sheetId,
|
|
118
|
+
name,
|
|
119
|
+
tabColor: '',
|
|
120
|
+
hidden: BooleanNumber.FALSE,
|
|
121
|
+
freeze: { startRow: -1, startColumn: -1, xSplit: 0, ySplit: 0 },
|
|
122
|
+
rowCount,
|
|
123
|
+
columnCount,
|
|
124
|
+
defaultColumnWidth: 96,
|
|
125
|
+
defaultRowHeight: 22,
|
|
126
|
+
mergeData: merges,
|
|
127
|
+
cellData,
|
|
128
|
+
rowData: convertRows(ws['!rows']),
|
|
129
|
+
columnData: convertCols(ws['!cols']),
|
|
130
|
+
rowHeader: { width: 46 },
|
|
131
|
+
columnHeader: { height: 20 },
|
|
132
|
+
showGridlines: BooleanNumber.TRUE,
|
|
133
|
+
rightToLeft: BooleanNumber.FALSE,
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** Map a SheetJS CellObject onto Univer's ICellData (or null for empty). */
|
|
138
|
+
function convertCell(cell: XLSX.CellObject): ICellData | null {
|
|
139
|
+
const out: ICellData = {}
|
|
140
|
+
// Type mapping: b→BOOLEAN, n→NUMBER, s→STRING, d→STRING, e→FORCE_STRING, z→skip
|
|
141
|
+
switch (cell.t) {
|
|
142
|
+
case 'b':
|
|
143
|
+
out.t = CellValueType.BOOLEAN
|
|
144
|
+
out.v = cell.v === true
|
|
145
|
+
break
|
|
146
|
+
case 'n':
|
|
147
|
+
out.t = CellValueType.NUMBER
|
|
148
|
+
out.v = typeof cell.v === 'number' ? cell.v : Number(cell.v)
|
|
149
|
+
break
|
|
150
|
+
case 'e':
|
|
151
|
+
// Excel error strings like #REF! — display verbatim, no formula engine.
|
|
152
|
+
out.t = CellValueType.FORCE_STRING
|
|
153
|
+
out.v = String(cell.w ?? cell.v ?? '')
|
|
154
|
+
break
|
|
155
|
+
case 's':
|
|
156
|
+
case 'd':
|
|
157
|
+
case 'z':
|
|
158
|
+
default:
|
|
159
|
+
// String / date / stub. SheetJS renders dates as ISO strings in `w`
|
|
160
|
+
// when cellDates is on; otherwise `v` is the serial number. Use `w`
|
|
161
|
+
// (formatted text) when present so users see what Excel showed.
|
|
162
|
+
out.t = CellValueType.STRING
|
|
163
|
+
out.v = cell.w ?? (cell.v !== undefined ? String(cell.v) : '')
|
|
164
|
+
break
|
|
165
|
+
}
|
|
166
|
+
// Formula: keep as text; Univer's formula engine computes on load.
|
|
167
|
+
if (typeof cell.f === 'string' && cell.f !== '') {
|
|
168
|
+
out.f = cell.f
|
|
169
|
+
}
|
|
170
|
+
return out
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** Convert SheetJS `!cols` (ColInfo[]) to Univer columnData (sparse, by index). */
|
|
174
|
+
function convertCols(cols: XLSX.ColInfo[] | undefined): IObjectArrayPrimitiveType<Partial<IColumnData>> {
|
|
175
|
+
if (!Array.isArray(cols)) return {}
|
|
176
|
+
const out: IObjectArrayPrimitiveType<Partial<IColumnData>> = {}
|
|
177
|
+
cols.forEach((info, index) => {
|
|
178
|
+
if (info === undefined || info === null) return
|
|
179
|
+
const w = info.wpx ?? (info.width !== undefined ? Math.round(info.width * 7) : undefined)
|
|
180
|
+
out[index] = {
|
|
181
|
+
...(w !== undefined ? { w } : {}),
|
|
182
|
+
...(info.hidden === true ? { hd: BooleanNumber.TRUE } : {}),
|
|
183
|
+
}
|
|
184
|
+
})
|
|
185
|
+
return out
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** Convert SheetJS `!rows` (RowInfo[]) to Univer rowData (sparse, by index). */
|
|
189
|
+
function convertRows(rows: XLSX.RowInfo[] | undefined): IObjectArrayPrimitiveType<Partial<IRowData>> {
|
|
190
|
+
if (!Array.isArray(rows)) return {}
|
|
191
|
+
const out: IObjectArrayPrimitiveType<Partial<IRowData>> = {}
|
|
192
|
+
rows.forEach((info, index) => {
|
|
193
|
+
if (info === undefined || info === null) return
|
|
194
|
+
const h = info.hpx ?? info.hpt
|
|
195
|
+
out[index] = {
|
|
196
|
+
...(h !== undefined ? { h: Math.round(h) } : {}),
|
|
197
|
+
...(info.hidden === true ? { hd: BooleanNumber.TRUE } : {}),
|
|
198
|
+
}
|
|
199
|
+
})
|
|
200
|
+
return out
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Resolve the worksheet dimensions. SheetJS's `!ref` is authoritative when
|
|
205
|
+
* present; otherwise fall back to the max row/col we actually wrote, with a
|
|
206
|
+
* small floor so the canvas has somewhere to paint.
|
|
207
|
+
*/
|
|
208
|
+
function dimensions(
|
|
209
|
+
ws: XLSX.WorkSheet,
|
|
210
|
+
cellData: IObjectMatrixPrimitiveType<ICellData>,
|
|
211
|
+
): { rowCount: number; columnCount: number } {
|
|
212
|
+
const ref = ws['!ref']
|
|
213
|
+
if (typeof ref === 'string' && ref !== '') {
|
|
214
|
+
const range = decodeRange(ref)
|
|
215
|
+
return {
|
|
216
|
+
rowCount: Math.max(range.endRow + 1, 20),
|
|
217
|
+
columnCount: Math.max(range.endColumn + 1, 10),
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
let maxRow = -1
|
|
221
|
+
let maxCol = -1
|
|
222
|
+
for (const r of Object.keys(cellData)) {
|
|
223
|
+
const rowNum = Number(r)
|
|
224
|
+
if (rowNum > maxRow) maxRow = rowNum
|
|
225
|
+
const rowObj = cellData[rowNum]!
|
|
226
|
+
for (const c of Object.keys(rowObj)) {
|
|
227
|
+
const colNum = Number(c)
|
|
228
|
+
if (colNum > maxCol) maxCol = colNum
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
return {
|
|
232
|
+
rowCount: Math.max(maxRow + 1, 20),
|
|
233
|
+
columnCount: Math.max(maxCol + 1, 10),
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/** Decode an A1-style address (e.g. "AB12") to 0-indexed {row, col}. */
|
|
238
|
+
function decodeAddr(addr: string): { r: number; c: number } {
|
|
239
|
+
let col = 0
|
|
240
|
+
let i = 0
|
|
241
|
+
while (i < addr.length) {
|
|
242
|
+
const ch = addr.charCodeAt(i)
|
|
243
|
+
if (ch >= 65 && ch <= 90) {
|
|
244
|
+
col = col * 26 + (ch - 64)
|
|
245
|
+
i += 1
|
|
246
|
+
} else {
|
|
247
|
+
break
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
const row = Number(addr.slice(i)) - 1
|
|
251
|
+
return { r: row, c: col - 1 }
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/** Decode an A1-style range (e.g. "A1:AB12") to 0-indexed bounds. */
|
|
255
|
+
function decodeRange(range: string): { startRow: number; endRow: number; startColumn: number; endColumn: number } {
|
|
256
|
+
const at = range.indexOf(':')
|
|
257
|
+
const start = decodeAddr(at === -1 ? range : range.slice(0, at))
|
|
258
|
+
const end = at === -1 ? start : decodeAddr(range.slice(at + 1))
|
|
259
|
+
return { startRow: start.r, endRow: end.r, startColumn: start.c, endColumn: end.c }
|
|
260
|
+
}
|