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.
Files changed (170) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +139 -0
  3. package/README_EN.md +139 -0
  4. package/lib/client-docx.js +7649 -0
  5. package/lib/client-editor.js +32677 -0
  6. package/lib/client-pptx.js +63438 -0
  7. package/lib/client-registry.js +8021 -0
  8. package/lib/client-terminal.js +8060 -0
  9. package/lib/client-xlsx.js +359755 -0
  10. package/lib/client.js +8021 -0
  11. package/lib/index.js +2604 -0
  12. package/lib/invariant.js +22 -0
  13. package/lib/types/agent-pty.d.ts +220 -0
  14. package/lib/types/browser-probe.d.ts +15 -0
  15. package/lib/types/bundle-route.d.ts +13 -0
  16. package/lib/types/client/BrowserView.d.ts +22 -0
  17. package/lib/types/client/DiffTab.d.ts +6 -0
  18. package/lib/types/client/DiffView.d.ts +51 -0
  19. package/lib/types/client/EditorHost.d.ts +10 -0
  20. package/lib/types/client/ExplorerView.d.ts +9 -0
  21. package/lib/types/client/GitView.d.ts +8 -0
  22. package/lib/types/client/OrphanedTab.d.ts +11 -0
  23. package/lib/types/client/PdfView.d.ts +6 -0
  24. package/lib/types/client/PptxView.d.ts +6 -0
  25. package/lib/types/client/SandboxStatusBar.d.ts +10 -0
  26. package/lib/types/client/SideCardSection.d.ts +27 -0
  27. package/lib/types/client/Sidebar.d.ts +6 -0
  28. package/lib/types/client/SubagentView.d.ts +16 -0
  29. package/lib/types/client/TabBar.d.ts +38 -0
  30. package/lib/types/client/TerminalView.d.ts +8 -0
  31. package/lib/types/client/TextEditor.d.ts +10 -0
  32. package/lib/types/client/api.d.ts +170 -0
  33. package/lib/types/client/binary-download.d.ts +12 -0
  34. package/lib/types/client/breakpoints.d.ts +12 -0
  35. package/lib/types/client/browser.d.ts +50 -0
  36. package/lib/types/client/builtins/index.d.ts +17 -0
  37. package/lib/types/client/builtins/tabs.d.ts +6 -0
  38. package/lib/types/client/builtins/viewers.d.ts +3 -0
  39. package/lib/types/client/chunk-loader.d.ts +74 -0
  40. package/lib/types/client/chunks/docx.d.ts +8 -0
  41. package/lib/types/client/chunks/editor.d.ts +9 -0
  42. package/lib/types/client/chunks/pptx.d.ts +8 -0
  43. package/lib/types/client/chunks/terminal.d.ts +9 -0
  44. package/lib/types/client/chunks/xlsx.d.ts +9 -0
  45. package/lib/types/client/cm-themes.d.ts +24 -0
  46. package/lib/types/client/conversation-draft.d.ts +14 -0
  47. package/lib/types/client/docx-view.d.ts +8 -0
  48. package/lib/types/client/editor-load.d.ts +66 -0
  49. package/lib/types/client/icons.d.ts +47 -0
  50. package/lib/types/client/image-types.d.ts +3 -0
  51. package/lib/types/client/ime-guard.d.ts +36 -0
  52. package/lib/types/client/index.d.ts +10 -0
  53. package/lib/types/client/intercept.d.ts +20 -0
  54. package/lib/types/client/lang.d.ts +12 -0
  55. package/lib/types/client/lazy-chunk.d.ts +25 -0
  56. package/lib/types/client/link-intercept.d.ts +35 -0
  57. package/lib/types/client/locales.d.ts +237 -0
  58. package/lib/types/client/office-shared.d.ts +33 -0
  59. package/lib/types/client/office-types.d.ts +36 -0
  60. package/lib/types/client/office-view.d.ts +12 -0
  61. package/lib/types/client/openpath-intercept.d.ts +41 -0
  62. package/lib/types/client/paths.d.ts +19 -0
  63. package/lib/types/client/pdf-types.d.ts +2 -0
  64. package/lib/types/client/prefs.d.ts +30 -0
  65. package/lib/types/client/produced-files.d.ts +27 -0
  66. package/lib/types/client/selection-payload.d.ts +27 -0
  67. package/lib/types/client/service.d.ts +214 -0
  68. package/lib/types/client/split-pane.d.ts +29 -0
  69. package/lib/types/client/state.d.ts +322 -0
  70. package/lib/types/client/subagent-activity.d.ts +35 -0
  71. package/lib/types/client/subagent-detect.d.ts +47 -0
  72. package/lib/types/client/subagent-jobs.d.ts +63 -0
  73. package/lib/types/client/theme.d.ts +28 -0
  74. package/lib/types/client/xlsx-to-univer.d.ts +33 -0
  75. package/lib/types/client/xlsx-view.d.ts +10 -0
  76. package/lib/types/config.d.ts +41 -0
  77. package/lib/types/context-types.d.ts +431 -0
  78. package/lib/types/fs-tree.d.ts +41 -0
  79. package/lib/types/git.d.ts +75 -0
  80. package/lib/types/html-route.d.ts +48 -0
  81. package/lib/types/index.d.ts +39 -0
  82. package/lib/types/invariant.d.ts +15 -0
  83. package/lib/types/jobs-routes.d.ts +45 -0
  84. package/lib/types/prefs-shared.d.ts +106 -0
  85. package/lib/types/pty-manager.d.ts +76 -0
  86. package/lib/types/tools.d.ts +28 -0
  87. package/lib/types/trust-fence.d.ts +24 -0
  88. package/lib/types/wire.d.ts +38 -0
  89. package/package.json +168 -0
  90. package/src/agent-pty.ts +519 -0
  91. package/src/browser-probe.ts +26 -0
  92. package/src/bundle-route.ts +130 -0
  93. package/src/client/BrowserView.tsx +253 -0
  94. package/src/client/DiffTab.tsx +110 -0
  95. package/src/client/DiffView.tsx +266 -0
  96. package/src/client/EditorHost.tsx +101 -0
  97. package/src/client/ExplorerView.tsx +283 -0
  98. package/src/client/GitView.tsx +540 -0
  99. package/src/client/OrphanedTab.tsx +27 -0
  100. package/src/client/PdfView.tsx +110 -0
  101. package/src/client/PptxView.tsx +131 -0
  102. package/src/client/SandboxStatusBar.tsx +60 -0
  103. package/src/client/SideCardSection.module.css +271 -0
  104. package/src/client/SideCardSection.tsx +426 -0
  105. package/src/client/Sidebar.tsx +861 -0
  106. package/src/client/SubagentView.module.css +547 -0
  107. package/src/client/SubagentView.tsx +866 -0
  108. package/src/client/TabBar.tsx +188 -0
  109. package/src/client/TerminalView.tsx +233 -0
  110. package/src/client/TextEditor.tsx +382 -0
  111. package/src/client/api.ts +215 -0
  112. package/src/client/binary-download.tsx +23 -0
  113. package/src/client/breakpoints.ts +51 -0
  114. package/src/client/browser.ts +119 -0
  115. package/src/client/builtins/index.ts +33 -0
  116. package/src/client/builtins/tabs.tsx +211 -0
  117. package/src/client/builtins/viewers.tsx +151 -0
  118. package/src/client/chunk-loader.ts +196 -0
  119. package/src/client/chunks/docx.tsx +8 -0
  120. package/src/client/chunks/editor.tsx +9 -0
  121. package/src/client/chunks/pptx.tsx +8 -0
  122. package/src/client/chunks/terminal.tsx +9 -0
  123. package/src/client/chunks/xlsx.tsx +9 -0
  124. package/src/client/cm-themes.ts +129 -0
  125. package/src/client/conversation-draft.ts +29 -0
  126. package/src/client/css-modules.d.ts +5 -0
  127. package/src/client/docx-view.tsx +113 -0
  128. package/src/client/editor-load.ts +92 -0
  129. package/src/client/icons.tsx +137 -0
  130. package/src/client/image-types.ts +8 -0
  131. package/src/client/ime-guard.ts +47 -0
  132. package/src/client/index.tsx +238 -0
  133. package/src/client/intercept.tsx +94 -0
  134. package/src/client/lang.ts +95 -0
  135. package/src/client/layout.css +54 -0
  136. package/src/client/lazy-chunk.tsx +89 -0
  137. package/src/client/link-intercept.ts +66 -0
  138. package/src/client/locales.ts +476 -0
  139. package/src/client/office-shared.tsx +40 -0
  140. package/src/client/office-types.ts +48 -0
  141. package/src/client/office-view.tsx +12 -0
  142. package/src/client/openpath-intercept.ts +62 -0
  143. package/src/client/paths.ts +28 -0
  144. package/src/client/pdf-types.ts +4 -0
  145. package/src/client/prefs.ts +100 -0
  146. package/src/client/produced-files.ts +87 -0
  147. package/src/client/selection-payload.ts +86 -0
  148. package/src/client/service.ts +434 -0
  149. package/src/client/sidebar.module.css +1917 -0
  150. package/src/client/split-pane.tsx +292 -0
  151. package/src/client/state.ts +1048 -0
  152. package/src/client/subagent-activity.ts +67 -0
  153. package/src/client/subagent-detect.ts +125 -0
  154. package/src/client/subagent-jobs.ts +161 -0
  155. package/src/client/theme.ts +44 -0
  156. package/src/client/xlsx-to-univer.ts +260 -0
  157. package/src/client/xlsx-view.tsx +116 -0
  158. package/src/config.ts +95 -0
  159. package/src/context-types.ts +445 -0
  160. package/src/fs-tree.ts +115 -0
  161. package/src/git.ts +236 -0
  162. package/src/html-route.ts +70 -0
  163. package/src/index.ts +897 -0
  164. package/src/invariant.ts +32 -0
  165. package/src/jobs-routes.ts +257 -0
  166. package/src/prefs-shared.ts +127 -0
  167. package/src/pty-manager.ts +199 -0
  168. package/src/tools.ts +477 -0
  169. package/src/trust-fence.ts +77 -0
  170. 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
+ }