dsh-gufa-code 0.0.0-stage → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (158) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +96 -2
  3. package/client.js +11548 -0
  4. package/cordis.patch.yml +8 -0
  5. package/docs/icon-180.png +0 -0
  6. package/docs/icon.svg +18 -0
  7. package/docs/market-submission.md +32 -0
  8. package/docs/screenshot-editor.jpg +0 -0
  9. package/docs/screenshot-ide.jpg +0 -0
  10. package/host/assets.js +31 -0
  11. package/host/chat.js +523 -0
  12. package/host/context.js +438 -0
  13. package/host/dev.js +35 -0
  14. package/host/fs.js +306 -0
  15. package/host/git.js +532 -0
  16. package/host/project.js +305 -0
  17. package/host/ssh.js +54 -0
  18. package/host/terminal-pty.js +237 -0
  19. package/host/terminal.js +947 -0
  20. package/icon.svg +18 -0
  21. package/index.js +63 -0
  22. package/locale/en.json +121 -0
  23. package/locale/zh.json +121 -0
  24. package/package.json +66 -4
  25. package/scripts/build-client.mjs +38 -0
  26. package/scripts/selftest-client.mjs +457 -0
  27. package/scripts/selftest-host.mjs +197 -0
  28. package/scripts/selftest-http.mjs +284 -0
  29. package/scripts/selftest-router.mjs +101 -0
  30. package/scripts/selftest-terminal-client.mjs +314 -0
  31. package/scripts/selftest-terminal-pty.mjs +190 -0
  32. package/scripts/selftest-terminal.mjs +351 -0
  33. package/scripts/test-chat-render.mjs +702 -0
  34. package/scripts/test-chat.mjs +761 -0
  35. package/scripts/test-editor.mjs +516 -0
  36. package/scripts/verify-workspace-sessions.mjs +135 -0
  37. package/src/client/00-core.js +1686 -0
  38. package/src/client/10-icons.js +205 -0
  39. package/src/client/20-editor.js +1318 -0
  40. package/src/client/30-terminal.js +1510 -0
  41. package/src/client/40-chat.js +1184 -0
  42. package/src/client/46-gitgraph.js +278 -0
  43. package/src/client/50-panels.js +916 -0
  44. package/src/client/55-backdrop.js +508 -0
  45. package/src/client/60-dock.js +662 -0
  46. package/src/client/85-shortcuts.js +516 -0
  47. package/src/client/90-app.js +2272 -0
  48. package/src/client/99-main.js +449 -0
  49. package/vendor/monaco/LICENSE +21 -0
  50. package/vendor/monaco/ThirdPartyNotices.txt +448 -0
  51. package/vendor/monaco/vs/base/browser/ui/codicons/codicon/codicon.ttf +0 -0
  52. package/vendor/monaco/vs/base/worker/workerMain.js +31 -0
  53. package/vendor/monaco/vs/basic-languages/abap/abap.js +10 -0
  54. package/vendor/monaco/vs/basic-languages/apex/apex.js +10 -0
  55. package/vendor/monaco/vs/basic-languages/azcli/azcli.js +10 -0
  56. package/vendor/monaco/vs/basic-languages/bat/bat.js +10 -0
  57. package/vendor/monaco/vs/basic-languages/bicep/bicep.js +11 -0
  58. package/vendor/monaco/vs/basic-languages/cameligo/cameligo.js +10 -0
  59. package/vendor/monaco/vs/basic-languages/clojure/clojure.js +10 -0
  60. package/vendor/monaco/vs/basic-languages/coffee/coffee.js +10 -0
  61. package/vendor/monaco/vs/basic-languages/cpp/cpp.js +10 -0
  62. package/vendor/monaco/vs/basic-languages/csharp/csharp.js +10 -0
  63. package/vendor/monaco/vs/basic-languages/csp/csp.js +10 -0
  64. package/vendor/monaco/vs/basic-languages/css/css.js +12 -0
  65. package/vendor/monaco/vs/basic-languages/cypher/cypher.js +10 -0
  66. package/vendor/monaco/vs/basic-languages/dart/dart.js +10 -0
  67. package/vendor/monaco/vs/basic-languages/dockerfile/dockerfile.js +10 -0
  68. package/vendor/monaco/vs/basic-languages/ecl/ecl.js +10 -0
  69. package/vendor/monaco/vs/basic-languages/elixir/elixir.js +10 -0
  70. package/vendor/monaco/vs/basic-languages/flow9/flow9.js +10 -0
  71. package/vendor/monaco/vs/basic-languages/freemarker2/freemarker2.js +12 -0
  72. package/vendor/monaco/vs/basic-languages/fsharp/fsharp.js +10 -0
  73. package/vendor/monaco/vs/basic-languages/go/go.js +10 -0
  74. package/vendor/monaco/vs/basic-languages/graphql/graphql.js +10 -0
  75. package/vendor/monaco/vs/basic-languages/handlebars/handlebars.js +10 -0
  76. package/vendor/monaco/vs/basic-languages/hcl/hcl.js +10 -0
  77. package/vendor/monaco/vs/basic-languages/html/html.js +10 -0
  78. package/vendor/monaco/vs/basic-languages/ini/ini.js +10 -0
  79. package/vendor/monaco/vs/basic-languages/java/java.js +10 -0
  80. package/vendor/monaco/vs/basic-languages/javascript/javascript.js +10 -0
  81. package/vendor/monaco/vs/basic-languages/julia/julia.js +10 -0
  82. package/vendor/monaco/vs/basic-languages/kotlin/kotlin.js +10 -0
  83. package/vendor/monaco/vs/basic-languages/less/less.js +11 -0
  84. package/vendor/monaco/vs/basic-languages/lexon/lexon.js +10 -0
  85. package/vendor/monaco/vs/basic-languages/liquid/liquid.js +10 -0
  86. package/vendor/monaco/vs/basic-languages/lua/lua.js +10 -0
  87. package/vendor/monaco/vs/basic-languages/m3/m3.js +10 -0
  88. package/vendor/monaco/vs/basic-languages/markdown/markdown.js +10 -0
  89. package/vendor/monaco/vs/basic-languages/mdx/mdx.js +10 -0
  90. package/vendor/monaco/vs/basic-languages/mips/mips.js +10 -0
  91. package/vendor/monaco/vs/basic-languages/msdax/msdax.js +10 -0
  92. package/vendor/monaco/vs/basic-languages/mysql/mysql.js +10 -0
  93. package/vendor/monaco/vs/basic-languages/objective-c/objective-c.js +10 -0
  94. package/vendor/monaco/vs/basic-languages/pascal/pascal.js +10 -0
  95. package/vendor/monaco/vs/basic-languages/pascaligo/pascaligo.js +10 -0
  96. package/vendor/monaco/vs/basic-languages/perl/perl.js +10 -0
  97. package/vendor/monaco/vs/basic-languages/pgsql/pgsql.js +10 -0
  98. package/vendor/monaco/vs/basic-languages/php/php.js +10 -0
  99. package/vendor/monaco/vs/basic-languages/pla/pla.js +10 -0
  100. package/vendor/monaco/vs/basic-languages/postiats/postiats.js +10 -0
  101. package/vendor/monaco/vs/basic-languages/powerquery/powerquery.js +10 -0
  102. package/vendor/monaco/vs/basic-languages/powershell/powershell.js +10 -0
  103. package/vendor/monaco/vs/basic-languages/protobuf/protobuf.js +11 -0
  104. package/vendor/monaco/vs/basic-languages/pug/pug.js +10 -0
  105. package/vendor/monaco/vs/basic-languages/python/python.js +10 -0
  106. package/vendor/monaco/vs/basic-languages/qsharp/qsharp.js +10 -0
  107. package/vendor/monaco/vs/basic-languages/r/r.js +10 -0
  108. package/vendor/monaco/vs/basic-languages/razor/razor.js +10 -0
  109. package/vendor/monaco/vs/basic-languages/redis/redis.js +10 -0
  110. package/vendor/monaco/vs/basic-languages/redshift/redshift.js +10 -0
  111. package/vendor/monaco/vs/basic-languages/restructuredtext/restructuredtext.js +10 -0
  112. package/vendor/monaco/vs/basic-languages/ruby/ruby.js +10 -0
  113. package/vendor/monaco/vs/basic-languages/rust/rust.js +10 -0
  114. package/vendor/monaco/vs/basic-languages/sb/sb.js +10 -0
  115. package/vendor/monaco/vs/basic-languages/scala/scala.js +10 -0
  116. package/vendor/monaco/vs/basic-languages/scheme/scheme.js +10 -0
  117. package/vendor/monaco/vs/basic-languages/scss/scss.js +12 -0
  118. package/vendor/monaco/vs/basic-languages/shell/shell.js +10 -0
  119. package/vendor/monaco/vs/basic-languages/solidity/solidity.js +10 -0
  120. package/vendor/monaco/vs/basic-languages/sophia/sophia.js +10 -0
  121. package/vendor/monaco/vs/basic-languages/sparql/sparql.js +10 -0
  122. package/vendor/monaco/vs/basic-languages/sql/sql.js +10 -0
  123. package/vendor/monaco/vs/basic-languages/st/st.js +10 -0
  124. package/vendor/monaco/vs/basic-languages/swift/swift.js +13 -0
  125. package/vendor/monaco/vs/basic-languages/systemverilog/systemverilog.js +10 -0
  126. package/vendor/monaco/vs/basic-languages/tcl/tcl.js +10 -0
  127. package/vendor/monaco/vs/basic-languages/twig/twig.js +10 -0
  128. package/vendor/monaco/vs/basic-languages/typescript/typescript.js +10 -0
  129. package/vendor/monaco/vs/basic-languages/typespec/typespec.js +10 -0
  130. package/vendor/monaco/vs/basic-languages/vb/vb.js +10 -0
  131. package/vendor/monaco/vs/basic-languages/wgsl/wgsl.js +307 -0
  132. package/vendor/monaco/vs/basic-languages/xml/xml.js +10 -0
  133. package/vendor/monaco/vs/basic-languages/yaml/yaml.js +10 -0
  134. package/vendor/monaco/vs/editor/editor.main.css +8 -0
  135. package/vendor/monaco/vs/editor/editor.main.js +798 -0
  136. package/vendor/monaco/vs/language/css/cssMode.js +13 -0
  137. package/vendor/monaco/vs/language/css/cssWorker.js +77 -0
  138. package/vendor/monaco/vs/language/html/htmlMode.js +13 -0
  139. package/vendor/monaco/vs/language/html/htmlWorker.js +454 -0
  140. package/vendor/monaco/vs/language/json/jsonMode.js +19 -0
  141. package/vendor/monaco/vs/language/json/jsonWorker.js +42 -0
  142. package/vendor/monaco/vs/language/typescript/tsMode.js +20 -0
  143. package/vendor/monaco/vs/language/typescript/tsWorker.js +51328 -0
  144. package/vendor/monaco/vs/loader.js +11 -0
  145. package/vendor/monaco/vs/nls.messages.de.js +21 -0
  146. package/vendor/monaco/vs/nls.messages.es.js +21 -0
  147. package/vendor/monaco/vs/nls.messages.fr.js +19 -0
  148. package/vendor/monaco/vs/nls.messages.it.js +19 -0
  149. package/vendor/monaco/vs/nls.messages.ja.js +21 -0
  150. package/vendor/monaco/vs/nls.messages.ko.js +19 -0
  151. package/vendor/monaco/vs/nls.messages.ru.js +21 -0
  152. package/vendor/monaco/vs/nls.messages.zh-cn.js +21 -0
  153. package/vendor/monaco/vs/nls.messages.zh-tw.js +19 -0
  154. package/vendor/xterm/LICENSE +21 -0
  155. package/vendor/xterm/LICENSE.addon-fit +19 -0
  156. package/vendor/xterm/addon-fit.js +2 -0
  157. package/vendor/xterm/xterm.css +285 -0
  158. package/vendor/xterm/xterm.js +2 -0
@@ -0,0 +1,1510 @@
1
+ /**
2
+ * 30-terminal:内嵌真实终端面板(xterm + WebSocket 连接 Host PTY)。
3
+ *
4
+ * 能力:
5
+ * - 离线加载 vendor/xterm(UMD → window.Terminal、addon-fit → window.FitAddon.FitAddon);
6
+ * 加载失败自动降级为「纯文本输出 + 输入行」,仍然可交互。
7
+ * - 多标签(新建/切换/关闭)、ResizeObserver + fit() 自适应并上报 resize。
8
+ * - 键盘输入直接经 WS 发给 PTY;暗/亮主题跟随 props.theme。
9
+ * - 断线自动重连:用已收字节数(since)请求补发缺失输出,服务端缓冲滑出窗口时先清屏再回放。
10
+ * - 监听 G.bus 'terminal:send'(SSH 快捷连接)把文本写入当前终端;终端未创建或未连上时排队。
11
+ *
12
+ * props: { root, visible, height, theme, onToggleHeight }(高度由外部拖拽条控制,组件内不改)
13
+ */
14
+ /** 客户端诊断日志:写入 Host 侧 /tmp/gufa-code-debug.log(排查桌面壳连接问题用)。 */
15
+ function gufaTermDebug(stage, detail) {
16
+ try {
17
+ const message = `${stage} | ${typeof detail === 'string' ? detail : JSON.stringify(detail)}`
18
+ void G.api('dev.log', { message }).catch(() => {})
19
+ } catch {
20
+ /* 诊断失败不影响功能 */
21
+ }
22
+ }
23
+
24
+ /* ── xterm 离线资产加载(模块级、幂等、失败可重试) ─────────────────────── */
25
+ const GUFA_TERM_CSS_ID = 'gufa-code/xterm.css'
26
+ const GUFA_TERM_XTERM_ID = 'gufa-code/xterm.js'
27
+ const GUFA_TERM_FIT_ID = 'gufa-code/xterm-addon-fit.js'
28
+ /** @type {Promise<{ Terminal: Function, FitAddon: Function }> | null} */
29
+ let gufaTermAssetsPromise = null
30
+
31
+ /** 页面里可能被其他插件注入全局 `module` / `exports`,UMD 会误判成 Node 环境而不挂 window。 */
32
+ function gufaHideNodeGlobals() {
33
+ const g = globalThis
34
+ const hadModule = Object.prototype.hasOwnProperty.call(g, 'module')
35
+ const hadExports = Object.prototype.hasOwnProperty.call(g, 'exports')
36
+ const hadDefine = Object.prototype.hasOwnProperty.call(g, 'define')
37
+ const savedModule = g.module
38
+ const savedExports = g.exports
39
+ const savedDefine = g.define
40
+ try {
41
+ delete g.module
42
+ } catch {
43
+ /* ignore */
44
+ }
45
+ try {
46
+ delete g.exports
47
+ } catch {
48
+ /* ignore */
49
+ }
50
+ try {
51
+ if (typeof g.module !== 'undefined') g.module = undefined
52
+ } catch {
53
+ /* ignore */
54
+ }
55
+ try {
56
+ if (typeof g.exports !== 'undefined') g.exports = undefined
57
+ } catch {
58
+ /* ignore */
59
+ }
60
+ /* 关键:Monaco 的 AMD loader 会挂全局 define,UMD 会优先走 AMD 分支而不挂 window */
61
+ try {
62
+ delete g.define
63
+ } catch {
64
+ /* ignore */
65
+ }
66
+ try {
67
+ if (typeof g.define !== 'undefined') g.define = undefined
68
+ } catch {
69
+ /* ignore */
70
+ }
71
+ return () => {
72
+ try {
73
+ if (hadModule) g.module = savedModule
74
+ else delete g.module
75
+ } catch {
76
+ /* ignore */
77
+ }
78
+ try {
79
+ if (hadExports) g.exports = savedExports
80
+ else delete g.exports
81
+ } catch {
82
+ /* ignore */
83
+ }
84
+ try {
85
+ if (hadDefine) g.define = savedDefine
86
+ else delete g.define
87
+ } catch {
88
+ /* ignore */
89
+ }
90
+ }
91
+ }
92
+
93
+ /** 从可能被污染的全局 module.exports 里抢救 UMD 导出。 */
94
+ function gufaRecoverUmd(name) {
95
+ try {
96
+ const pollution = globalThis.module
97
+ if (pollution === null || pollution === undefined || typeof pollution !== 'object') return undefined
98
+ const exported = pollution.exports
99
+ if (exported === null || exported === undefined) return undefined
100
+ if (typeof exported === 'object') return exported[name]
101
+ return undefined
102
+ } catch {
103
+ return undefined
104
+ }
105
+ }
106
+
107
+ /** 在「隐藏 module/exports」的环境里注入并执行一个 UMD 脚本。 */
108
+ function gufaTermLoadUmdScript(id, src) {
109
+ return new Promise((resolve, reject) => {
110
+ const restore = gufaHideNodeGlobals()
111
+ gufaTermLoadScript(id, src).then(
112
+ () => {
113
+ restore()
114
+ resolve()
115
+ },
116
+ (error) => {
117
+ restore()
118
+ reject(error)
119
+ },
120
+ )
121
+ })
122
+ }
123
+
124
+ /** 注入并等待一个 <script>;已成功加载过的直接复用。 */
125
+ function gufaTermLoadScript(id, src) {
126
+ const existing = document.querySelector(`script[data-gufa="${id}"]`)
127
+ if (existing !== null) {
128
+ if (existing.dataset.loaded === '1') return Promise.resolve()
129
+ return new Promise((resolve, reject) => {
130
+ existing.addEventListener('load', () => resolve(), { once: true })
131
+ existing.addEventListener('error', () => reject(new Error(`无法加载脚本:${src}`)), { once: true })
132
+ })
133
+ }
134
+ return new Promise((resolve, reject) => {
135
+ const script = document.createElement('script')
136
+ script.src = src
137
+ script.async = true
138
+ script.dataset.gufa = id
139
+ script.addEventListener('load', () => {
140
+ script.dataset.loaded = '1'
141
+ resolve()
142
+ }, { once: true })
143
+ script.addEventListener('error', () => {
144
+ script.remove()
145
+ reject(new Error(`无法加载脚本:${src}`))
146
+ }, { once: true })
147
+ document.head.append(script)
148
+ })
149
+ }
150
+
151
+ /** 注入 xterm.css 并等待样式加载完成(失败也继续,xterm 仍可用)。 */
152
+ function gufaTermIncludeCss() {
153
+ const existing = document.querySelector(`link[data-gufa="${GUFA_TERM_CSS_ID}"]`)
154
+ if (existing !== null) return Promise.resolve()
155
+ return new Promise((resolve) => {
156
+ const link = document.createElement('link')
157
+ link.rel = 'stylesheet'
158
+ link.href = G.asset('xterm/xterm.css')
159
+ link.dataset.gufa = GUFA_TERM_CSS_ID
160
+ link.addEventListener('load', () => resolve(), { once: true })
161
+ link.addEventListener('error', () => resolve(), { once: true })
162
+ document.head.append(link)
163
+ })
164
+ }
165
+
166
+ /** 读取 window.Terminal(含从被污染 module.exports 抢救)。 */
167
+ function gufaReadTerminalCtor() {
168
+ if (typeof window.Terminal === 'function') return window.Terminal
169
+ const recovered = gufaRecoverUmd('Terminal')
170
+ if (typeof recovered === 'function') {
171
+ window.Terminal = recovered
172
+ return recovered
173
+ }
174
+ return undefined
175
+ }
176
+
177
+ /** 读取 window.FitAddon.FitAddon(含抢救)。 */
178
+ function gufaReadFitCtor() {
179
+ const fit = window.FitAddon
180
+ if (fit !== undefined && fit !== null) {
181
+ if (typeof fit.FitAddon === 'function') return fit.FitAddon
182
+ if (typeof fit === 'function') return fit
183
+ }
184
+ const recovered = gufaRecoverUmd('FitAddon')
185
+ if (recovered !== undefined && recovered !== null) {
186
+ if (typeof recovered.FitAddon === 'function') {
187
+ window.FitAddon = recovered
188
+ return recovered.FitAddon
189
+ }
190
+ if (typeof recovered === 'function') {
191
+ window.FitAddon = { FitAddon: recovered }
192
+ return recovered
193
+ }
194
+ }
195
+ return undefined
196
+ }
197
+
198
+ /** 首次需要时注入 xterm.css / xterm.js / addon-fit.js。 */
199
+ function gufaTermLoadAssets() {
200
+ if (gufaTermAssetsPromise !== null) return gufaTermAssetsPromise
201
+ gufaTermAssetsPromise = (async () => {
202
+ await gufaTermIncludeCss()
203
+ // 清掉旧实例可能残留/误加载的脚本节点,在隐藏 module/exports 的环境下重新执行一次。
204
+ for (const id of [GUFA_TERM_XTERM_ID, GUFA_TERM_FIT_ID]) {
205
+ for (const node of Array.from(document.querySelectorAll(`script[data-gufa="${id}"]`))) node.remove()
206
+ }
207
+ await gufaTermLoadUmdScript(GUFA_TERM_XTERM_ID, G.asset('xterm/xterm.js'))
208
+ await gufaTermLoadUmdScript(GUFA_TERM_FIT_ID, G.asset('xterm/addon-fit.js'))
209
+ let TerminalCtor = gufaReadTerminalCtor()
210
+ let FitCtor = gufaReadFitCtor()
211
+ if (typeof TerminalCtor !== 'function') {
212
+ // HMR/重复加载时偶发「脚本已执行但全局未挂上」:延迟后重新注入一次
213
+ for (const node of Array.from(document.querySelectorAll(`script[data-gufa="${GUFA_TERM_XTERM_ID}"]`))) node.remove()
214
+ await gufaTermLoadUmdScript(GUFA_TERM_XTERM_ID, G.asset('xterm/xterm.js'))
215
+ TerminalCtor = gufaReadTerminalCtor()
216
+ }
217
+ if (typeof FitCtor !== 'function') {
218
+ for (const node of Array.from(document.querySelectorAll(`script[data-gufa="${GUFA_TERM_FIT_ID}"]`))) node.remove()
219
+ await gufaTermLoadUmdScript(GUFA_TERM_FIT_ID, G.asset('xterm/addon-fit.js'))
220
+ FitCtor = gufaReadFitCtor()
221
+ }
222
+ if (typeof TerminalCtor !== 'function') throw new Error('xterm.js 已加载,但 window.Terminal 不可用')
223
+ if (typeof FitCtor !== 'function') throw new Error('addon-fit.js 已加载,但 window.FitAddon.FitAddon 不可用')
224
+ return { Terminal: TerminalCtor, FitAddon: FitCtor }
225
+ })().catch((error) => {
226
+ // 失败后允许下次挂载重试(脚本节点已在失败时移除)
227
+ gufaTermAssetsPromise = null
228
+ throw error
229
+ })
230
+ return gufaTermAssetsPromise
231
+ }
232
+
233
+ /* ── 主题 / 文本工具 ────────────────────────────────────────────────────── */
234
+ /** 解析 'rgb(...)' / 'rgba(...)' → [r,g,b,a]。 */
235
+ function gufaTermParseColor(value) {
236
+ const match = /^rgba?\(\s*(\d+)[,\s]+(\d+)[,\s]+(\d+)(?:[,\s/]+([\d.]+))?\s*\)$/.exec(String(value ?? '').trim())
237
+ if (match === null) return null
238
+ return [Number(match[1]), Number(match[2]), Number(match[3]), match[4] === undefined ? 1 : Number(match[4])]
239
+ }
240
+
241
+ function gufaTermHex(color) {
242
+ const c = color ?? [0, 0, 0]
243
+ return `#${[c[0], c[1], c[2]].map((part) => Math.max(0, Math.min(255, Math.round(part))).toString(16).padStart(2, '0')).join('')}`
244
+ }
245
+
246
+ function gufaTermHexAlpha(color, alpha) {
247
+ const a = Math.max(0, Math.min(1, alpha))
248
+ return `${gufaTermHex(color)}${Math.round(a * 255).toString(16).padStart(2, '0')}`
249
+ }
250
+
251
+ /** 可选的皮肤调色板(跟随滑动变祖)。 */
252
+ function gufaTermPalette(props) {
253
+ const palette = props.palette
254
+ return palette !== null && palette !== undefined && typeof palette === 'object' ? palette : null
255
+ }
256
+
257
+ /**
258
+ * xterm 的暗/亮主题调色板。
259
+ * @param {string} mode 'dark' | 'light'
260
+ * @param {object|null} palette 插件合成的实色调色板(优先于 mode)
261
+ */
262
+ function gufaTermTheme(mode, palette) {
263
+ const dark = palette !== null && palette !== undefined && typeof palette.isDark === 'boolean' ? palette.isDark : mode === 'dark'
264
+ if (dark) {
265
+ const darkTheme = {
266
+ background: '#1e1e1e',
267
+ foreground: '#d4d4d4',
268
+ cursor: '#d4d4d4',
269
+ cursorAccent: '#1e1e1e',
270
+ selectionBackground: '#264f78',
271
+ black: '#000000',
272
+ red: '#cd3131',
273
+ green: '#0dbc79',
274
+ yellow: '#e5e510',
275
+ blue: '#2472c8',
276
+ magenta: '#bc3fbc',
277
+ cyan: '#11a8cd',
278
+ white: '#e5e5e5',
279
+ brightBlack: '#666666',
280
+ brightRed: '#f14c4c',
281
+ brightGreen: '#23d18b',
282
+ brightYellow: '#f5f543',
283
+ brightBlue: '#3b8eea',
284
+ brightMagenta: '#d670d6',
285
+ brightCyan: '#29b8db',
286
+ brightWhite: '#e5e5e5',
287
+ }
288
+ return gufaApplyPaletteToTermTheme(darkTheme, palette)
289
+ }
290
+ const light = {
291
+ background: '#ffffff',
292
+ foreground: '#333333',
293
+ cursor: '#333333',
294
+ cursorAccent: '#ffffff',
295
+ selectionBackground: '#add6ff',
296
+ black: '#000000',
297
+ red: '#cd3131',
298
+ green: '#00bc00',
299
+ yellow: '#949800',
300
+ blue: '#0451a5',
301
+ magenta: '#bc05bc',
302
+ cyan: '#0598bc',
303
+ white: '#555555',
304
+ brightBlack: '#666666',
305
+ brightRed: '#cd3131',
306
+ brightGreen: '#14ce14',
307
+ brightYellow: '#b5ba00',
308
+ brightBlue: '#0451a5',
309
+ brightMagenta: '#bc05bc',
310
+ brightCyan: '#0598bc',
311
+ brightWhite: '#a5a5a5',
312
+ }
313
+ return gufaApplyPaletteToTermTheme(light, palette)
314
+ }
315
+
316
+ /** 把皮肤实色调色板叠加到 xterm 主题(背景/前景/光标/选区跟随滑动变祖)。 */
317
+ function gufaApplyPaletteToTermTheme(theme, palette) {
318
+ if (palette === null || palette === undefined || typeof palette !== 'object') return theme
319
+ const bg = gufaTermParseColor(palette.bg)
320
+ const fg = gufaTermParseColor(palette.text)
321
+ const accent = gufaTermParseColor(palette.accent !== undefined ? palette.accent : palette.text)
322
+ if (bg !== null) {
323
+ theme.background = gufaTermHex(bg)
324
+ theme.cursorAccent = gufaTermHex(bg)
325
+ }
326
+ if (fg !== null) {
327
+ theme.foreground = gufaTermHex(fg)
328
+ theme.cursor = gufaTermHex(fg)
329
+ }
330
+ if (accent !== null) {
331
+ theme.cursor = gufaTermHex(accent)
332
+ theme.selectionBackground = gufaTermHexAlpha(accent, 0.34)
333
+ }
334
+ return theme
335
+ }
336
+
337
+ /** 降级模式下去掉常见 ANSI 控制序列,保留可读文本。 */
338
+ function gufaTermStripEscapes(text) {
339
+ return String(text)
340
+ .replace(/\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)/g, '')
341
+ .replace(/\x1b[PX^_][\s\S]*?\x1b\\/g, '')
342
+ .replace(/\x1b\[[0-?]*[ -/]*[@-~]/g, '')
343
+ .replace(/\x1b[@-Z\\-_]/g, '')
344
+ .replace(/\r/g, '')
345
+ .replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g, '')
346
+ }
347
+
348
+ /** 文本的 UTF-8 字节数(用于 since 偏移计数;纯 ASCII 直接取长度,避免频繁分配)。 */
349
+ function gufaTermByteLength(text) {
350
+ if (!/[^\x00-\x7f]/.test(text)) return text.length
351
+ try {
352
+ return new TextEncoder().encode(text).length
353
+ } catch {
354
+ return text.length
355
+ }
356
+ }
357
+
358
+ /** xterm 在回放历史输出时可能自动应答查询序列;回放窗口内忽略这些应答,避免污染 shell 输入。 */
359
+ function gufaTermIsProtocolReply(data) {
360
+ return /^\x1b\](?:10|11|12);(?:rgb:[\da-f]{4}\/[\da-f]{4}\/[\da-f]{4}|#[\da-f]{6})(?:\x07|\x1b\\)$/i.test(data)
361
+ || /^\x1b\[\??[\d;]*[cR]$/.test(data)
362
+ }
363
+
364
+ /** 根据容器像素尺寸估算初始 cols/rows(xterm 挂载前的创建请求用)。 */
365
+ function gufaTermEstimateSize(width, height) {
366
+ const cols = Math.floor((Math.max(0, width) - 12) / 7.5)
367
+ const rows = Math.floor((Math.max(0, height) - 12) / 17)
368
+ return {
369
+ cols: Math.max(20, Math.min(300, cols)),
370
+ rows: Math.max(5, Math.min(120, rows)),
371
+ }
372
+ }
373
+
374
+ /** 终端标签页。 */
375
+ function GufaTerminalTab(props) {
376
+ const session = props.session
377
+ return h(
378
+ 'div',
379
+ {
380
+ role: 'tab',
381
+ 'aria-selected': props.active ? 'true' : 'false',
382
+ className: 'gufa-tab',
383
+ 'data-active': props.active ? 'true' : undefined,
384
+ title: session.title,
385
+ onClick: props.onSelect,
386
+ style: { flex: '0 0 auto', height: 30, maxWidth: 190, opacity: session.exited ? 0.62 : 1 },
387
+ },
388
+ h(G.Icon, { name: 'terminal', size: 12 }),
389
+ h('span', { className: 'gufa-tab-name', style: { fontSize: 11.5 } }, session.title || G.translate('terminal.session')),
390
+ h(
391
+ 'button',
392
+ {
393
+ type: 'button',
394
+ className: 'gufa-tab-close',
395
+ title: G.translate('terminal.close'),
396
+ 'aria-label': G.translate('terminal.close'),
397
+ onClick: (event) => {
398
+ event.stopPropagation()
399
+ props.onClose()
400
+ },
401
+ },
402
+ h(G.Icon, { name: 'close', size: 10 }),
403
+ ),
404
+ )
405
+ }
406
+
407
+ /**
408
+ * 单个终端视图:xterm 或降级纯文本;每个视图自己持有 xterm 实例与 WS 连接。
409
+ * 父组件始终渲染所有标签,非活动标签 display:none,以保留各自滚动位置与状态。
410
+ */
411
+ function GufaTerminalView(props) {
412
+ const t = G.useT()
413
+ const session = props.session
414
+ const hostRef = useRef(null)
415
+ const preRef = useRef(null)
416
+ const inputRef = useRef(null)
417
+ const termRef = useRef(null)
418
+ const fitRef = useRef(null)
419
+ const openerRef = useRef(null)
420
+ const channelRef = useRef(null)
421
+ const ticketRef = useRef(null)
422
+ const activeRef = useRef(props.active === true)
423
+ const themeRef = useRef(props.theme)
424
+ const palette = gufaTermPalette(props)
425
+ const paletteRef = useRef(palette)
426
+ paletteRef.current = palette
427
+ const paletteKey = palette === null
428
+ ? ''
429
+ : [palette.bg, palette.text, palette.layer, palette.accent, palette.isDark].join('|')
430
+ const [fallback, setFallback] = useState(false)
431
+ const [output, setOutput] = useState('')
432
+ const [notice, setNotice] = useState('')
433
+ const [connected, setConnected] = useState(false)
434
+ const [exited, setExited] = useState(session.exited === true)
435
+ const [inputValue, setInputValue] = useState('')
436
+
437
+ useEffect(() => {
438
+ themeRef.current = props.theme
439
+ const term = termRef.current
440
+ if (term === null) return
441
+ const theme = gufaTermTheme(props.theme, palette)
442
+ gufaTermDebug('terminal-theme', `apply palette=${palette !== null} bg=${String(theme.background)}`)
443
+ try {
444
+ term.options.theme = theme
445
+ } catch {
446
+ try {
447
+ if (typeof term.setOption === 'function') term.setOption('theme', theme)
448
+ } catch {
449
+ /* 老版本 xterm 不支持即时换主题时忽略 */
450
+ }
451
+ }
452
+ }, [props.theme, paletteKey])
453
+
454
+ useEffect(() => {
455
+ if (props.assets.status === 'loading') return undefined
456
+ const fallbackMode = props.assets.status !== 'ready'
457
+ setFallback(fallbackMode)
458
+ const root = props.root
459
+ const terminalId = session.id
460
+ let disposed = false
461
+ /** 'xterm' | 'fallback':运行期若 xterm 创建失败会切到 fallback。 */
462
+ let mode = fallbackMode ? 'fallback' : 'xterm'
463
+ let term = null
464
+ let fit = null
465
+ let streamAbort = null
466
+ let streamEnded = false
467
+ let inputChain = Promise.resolve()
468
+ let retryTimer = null
469
+ let openFrame = 0
470
+ let openRetries = 0
471
+ let fitFrame = 0
472
+ let observer = null
473
+ let replayTimer = null
474
+ let replaying = false
475
+ let attempts = 0
476
+ let settled = false
477
+ let fatal = false
478
+ let receivedBytes = 0
479
+ let pendingOutput = ''
480
+ let loggedFirstOutput = false
481
+ let loggedFirstInput = false
482
+ let loggedFirstMessage = false
483
+ let loggedRawMessages = 0
484
+ const outbox = []
485
+
486
+ const setNoticeSafe = (message) => {
487
+ if (!disposed) setNotice(message)
488
+ }
489
+
490
+ const postInput = (data) => {
491
+ if (typeof data !== 'string' || data === '') return
492
+ const body = { root: root === null ? '' : root, id: terminalId, data }
493
+ inputChain = inputChain
494
+ .then(() => G.api('terminal.input', body))
495
+ .catch((error) => {
496
+ gufaTermDebug('terminal-io', `input-failed ${G.errorText(error)}`)
497
+ scheduleReconnect()
498
+ })
499
+ }
500
+
501
+ const postResize = (cols, rows) => {
502
+ if (typeof cols !== 'number' || typeof rows !== 'number') return
503
+ const body = { root: root === null ? '' : root, id: terminalId, cols, rows }
504
+ inputChain = inputChain.then(() => G.api('terminal.resize', body)).catch(() => {})
505
+ }
506
+
507
+ const writeTerm = (data) => {
508
+ if (term !== null) {
509
+ try {
510
+ term.write(data)
511
+ } catch {
512
+ /* xterm 可能正在销毁 */
513
+ }
514
+ return
515
+ }
516
+ pendingOutput += data
517
+ if (pendingOutput.length > 512 * 1024) pendingOutput = pendingOutput.slice(-512 * 1024)
518
+ }
519
+
520
+ const appendFallback = (data) => {
521
+ const text = gufaTermStripEscapes(data)
522
+ if (text === '') return
523
+ setOutput((current) => {
524
+ const next = current + text
525
+ return next.length > 200 * 1024 ? next.slice(-200 * 1024) : next
526
+ })
527
+ }
528
+
529
+ const writeOutput = (data) => {
530
+ if (mode === 'xterm') writeTerm(data)
531
+ else appendFallback(data)
532
+ }
533
+
534
+ const writeNoticeLine = (message) => {
535
+ setNoticeSafe(message)
536
+ const line = `\r\n\x1b[31m${message}\x1b[0m\r\n`
537
+ if (mode === 'xterm') writeTerm(line)
538
+ else appendFallback(line)
539
+ }
540
+
541
+ const fitNow = () => {
542
+ if (fit === null || term === null || disposed) return
543
+ const host = hostRef.current
544
+ if (host === null || host.clientWidth <= 0 || host.clientHeight <= 0) return
545
+ try {
546
+ fit.fit()
547
+ } catch {
548
+ /* 布局切换的中间帧可能失败,等下一次 ResizeObserver */
549
+ }
550
+ }
551
+
552
+ const sendCurrentSize = () => {
553
+ if (term !== null) postResize(term.cols, term.rows)
554
+ }
555
+
556
+ const handleValue = (value) => {
557
+ if (disposed) return
558
+ if (value === null || typeof value !== 'object') return
559
+ if (!loggedFirstMessage) {
560
+ loggedFirstMessage = true
561
+ gufaTermDebug('terminal-io', `first-message type=${value.type}`)
562
+ }
563
+ if (value.type === 'output') {
564
+ if (!loggedFirstOutput) {
565
+ loggedFirstOutput = true
566
+ gufaTermDebug('terminal-io', `first-output len=${typeof value.data === 'string' ? value.data.length : 0}`)
567
+ }
568
+ if (typeof value.data !== 'string' || value.data === '') return
569
+ const bytes = gufaTermByteLength(value.data)
570
+ if (typeof value.offset === 'number' && Number.isFinite(value.offset)) receivedBytes = value.offset + bytes
571
+ else receivedBytes += bytes
572
+ if (value.replay === true) {
573
+ replaying = true
574
+ if (replayTimer !== null) clearTimeout(replayTimer)
575
+ replayTimer = window.setTimeout(() => {
576
+ replayTimer = null
577
+ replaying = false
578
+ }, 600)
579
+ }
580
+ writeOutput(value.data)
581
+ return
582
+ }
583
+ if (value.type === 'reset') {
584
+ receivedBytes = 0
585
+ pendingOutput = ''
586
+ if (term !== null) {
587
+ try {
588
+ term.reset()
589
+ } catch {
590
+ /* ignore */
591
+ }
592
+ } else {
593
+ setOutput('')
594
+ }
595
+ return
596
+ }
597
+ if (value.type === 'hello' || value.type === 'hello2' || value.type === 'pong' || value.type === 'ready') {
598
+ return
599
+ }
600
+ if (value.type === 'exit') {
601
+ settled = true
602
+ const code = typeof value.code === 'number' ? value.code : null
603
+ const label = code === null ? t('terminal.exited') : `${t('terminal.exited')} (${code})`
604
+ writeOutput(`\r\n\x1b[2m[${label}]\x1b[0m\r\n`)
605
+ setExited(true)
606
+ if (term !== null) {
607
+ try {
608
+ term.options.disableStdin = true
609
+ } catch {
610
+ /* ignore */
611
+ }
612
+ }
613
+ props.onExit(terminalId, code)
614
+ if (streamAbort !== null) {
615
+ try {
616
+ streamAbort()
617
+ } catch {
618
+ /* ignore */
619
+ }
620
+ }
621
+ return
622
+ }
623
+ if (value.type === 'error') {
624
+ gufaTermDebug('terminal-ws', `server-error ${typeof value.message === 'string' ? value.message : 'unknown'}`)
625
+ fatal = true
626
+ settled = true
627
+ writeNoticeLine(typeof value.message === 'string' && value.message !== '' ? value.message : t('common.error'))
628
+ setExited(true)
629
+ if (term !== null) {
630
+ try {
631
+ term.options.disableStdin = true
632
+ } catch {
633
+ /* ignore */
634
+ }
635
+ }
636
+ props.onExit(terminalId, null)
637
+ }
638
+ }
639
+
640
+ const scheduleReconnect = () => {
641
+ if (disposed || settled || fatal || streamEnded) return
642
+ attempts += 1
643
+ if (attempts > 8) {
644
+ writeNoticeLine(t('common.error'))
645
+ return
646
+ }
647
+ const delay = Math.min(5000, 400 * attempts)
648
+ retryTimer = window.setTimeout(() => {
649
+ retryTimer = null
650
+ startStream()
651
+ }, delay)
652
+ }
653
+
654
+ /* ── 传输:直连真实 Host 端口的 SSE(带票据)──────────────────────────
655
+ * 桌面壳页面源是 dsh-app://app:
656
+ * - 相对路径 fetch 会走自定义协议,响应只能整体缓冲 → 不适合流;
657
+ * - 直连 ws:// 时 Chromium 上行可发、下行帧不达;
658
+ * 于是输出用「真实 Host 基址 + 票据」的 SSE,输入/尺寸仍走相对路径 POST。 */
659
+ const handleStreamEvent = (event) => {
660
+ if (disposed) return
661
+ if (event === null || typeof event !== 'object') return
662
+ if (loggedRawMessages < 6) {
663
+ loggedRawMessages += 1
664
+ gufaTermDebug('terminal-io', `event#${loggedRawMessages} type=${event.type}`)
665
+ }
666
+ handleValue(event)
667
+ if (event.type === 'ready') {
668
+ attempts = 0
669
+ setConnected(true)
670
+ fitNow()
671
+ if (term !== null) postResize(term.cols, term.rows)
672
+ if (activeRef.current === true && term !== null) {
673
+ try {
674
+ term.focus()
675
+ } catch {
676
+ /* ignore */
677
+ }
678
+ }
679
+ }
680
+ }
681
+
682
+ const openStream = (ticket, since) => {
683
+ if (disposed) return
684
+ let base = location.href
685
+ try {
686
+ base = typeof G.serverBase === 'function' ? G.serverBase() : location.href
687
+ const url = new URL('gufa-code/terminal-stream', base)
688
+ url.searchParams.set('ticket', ticket)
689
+ url.searchParams.set('root', root === null ? '' : root)
690
+ url.searchParams.set('id', terminalId)
691
+ if (since !== undefined) url.searchParams.set('since', String(since))
692
+ const controller = new AbortController()
693
+ streamAbort = () => controller.abort()
694
+ gufaTermDebug('terminal-stream', `open ${url.origin} since=${since === undefined ? 'none' : since}`)
695
+ void (async () => {
696
+ try {
697
+ const response = await fetch(url.href, { method: 'GET', mode: 'cors', signal: controller.signal })
698
+ if (!response.ok || response.body === null) {
699
+ gufaTermDebug('terminal-stream', `http ${response.status}`)
700
+ if (!controller.signal.aborted) {
701
+ setConnected(false)
702
+ scheduleReconnect()
703
+ }
704
+ return
705
+ }
706
+ const reader = response.body.getReader()
707
+ const decoder = new TextDecoder()
708
+ let buffer = ''
709
+ for (;;) {
710
+ const { value, done } = await reader.read()
711
+ if (done) break
712
+ buffer += decoder.decode(value, { stream: true })
713
+ let index
714
+ while ((index = buffer.indexOf('\n\n')) >= 0) {
715
+ const chunk = buffer.slice(0, index)
716
+ buffer = buffer.slice(index + 2)
717
+ for (const line of chunk.split('\n')) {
718
+ if (!line.startsWith('data: ')) continue
719
+ let event
720
+ try {
721
+ event = JSON.parse(line.slice(6))
722
+ } catch {
723
+ continue
724
+ }
725
+ handleStreamEvent(event)
726
+ }
727
+ }
728
+ }
729
+ if (!controller.signal.aborted) {
730
+ setConnected(false)
731
+ scheduleReconnect()
732
+ }
733
+ } catch (error) {
734
+ if (controller.signal.aborted) return
735
+ gufaTermDebug('terminal-stream', `failed ${G.errorText(error)}`)
736
+ setConnected(false)
737
+ scheduleReconnect()
738
+ }
739
+ })()
740
+ } catch (error) {
741
+ gufaTermDebug('terminal-stream', `url-failed ${G.errorText(error)}`)
742
+ scheduleReconnect()
743
+ }
744
+ }
745
+
746
+ const startStream = () => {
747
+ if (disposed) return
748
+ if (streamAbort !== null) {
749
+ try {
750
+ streamAbort()
751
+ } catch {
752
+ /* ignore */
753
+ }
754
+ streamAbort = null
755
+ }
756
+ streamEnded = false
757
+ const since = receivedBytes > 0 ? receivedBytes : undefined
758
+ G.api('terminal.wsTicket', {})
759
+ .then((value) => {
760
+ if (disposed) return
761
+ const ticket = value !== null && value !== undefined && typeof value.ticket === 'string' ? value.ticket : ''
762
+ if (ticket === '') {
763
+ scheduleReconnect()
764
+ return
765
+ }
766
+ openStream(ticket, since)
767
+ })
768
+ .catch((error) => {
769
+ gufaTermDebug('terminal-stream', `ticket-failed ${G.errorText(error)}`)
770
+ scheduleReconnect()
771
+ })
772
+ }
773
+
774
+ /* xterm 容器可能因为父面板隐藏而暂时 0 尺寸:等布局可用后再 open。 */
775
+ const tryOpen = () => {
776
+ openFrame = 0
777
+ if (disposed || mode !== 'xterm' || term !== null) return
778
+ const host = hostRef.current
779
+ if (host === null) return
780
+ if (host.clientWidth <= 0 || host.clientHeight <= 0) {
781
+ if (openRetries < 600) {
782
+ openRetries += 1
783
+ openFrame = window.requestAnimationFrame(tryOpen)
784
+ }
785
+ return
786
+ }
787
+ gufaTermDebug('terminal-xterm', `open host=${host.clientWidth}x${host.clientHeight} palette=${palette !== null} isDark=${palette === null ? '?' : palette.isDark === true}`)
788
+ try {
789
+ const terminal = new props.assets.Terminal({
790
+ allowProposedApi: false,
791
+ convertEol: false,
792
+ cursorBlink: true,
793
+ cursorStyle: 'bar',
794
+ fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace',
795
+ fontSize: 12.5,
796
+ lineHeight: 1.15,
797
+ scrollback: 5000,
798
+ theme: gufaTermTheme(themeRef.current, paletteRef.current),
799
+ })
800
+ term = terminal
801
+ termRef.current = terminal
802
+ try {
803
+ fit = new props.assets.FitAddon()
804
+ term.loadAddon(fit)
805
+ fitRef.current = fit
806
+ } catch {
807
+ fit = null
808
+ fitRef.current = null
809
+ }
810
+ term.open(host)
811
+ if (pendingOutput !== '') {
812
+ const data = pendingOutput
813
+ pendingOutput = ''
814
+ terminal.write(data)
815
+ }
816
+ terminal.onData((data) => {
817
+ if (replaying && gufaTermIsProtocolReply(data)) return
818
+ if (!loggedFirstInput) {
819
+ loggedFirstInput = true
820
+ gufaTermDebug('terminal-io', `first-key len=${data.length}`)
821
+ }
822
+ const channel = channelRef.current
823
+ if (channel !== null) channel.send(data)
824
+ })
825
+ terminal.onResize(({ cols, rows }) => {
826
+ if (replaying) return
827
+ postResize(cols, rows)
828
+ })
829
+ try {
830
+ if (typeof terminal.onTitleChange === 'function') {
831
+ terminal.onTitleChange((title) => {
832
+ if (typeof title === 'string' && title.trim() !== '') props.onTitle(terminalId, title.trim())
833
+ })
834
+ }
835
+ } catch {
836
+ /* 标题同步失败不影响使用 */
837
+ }
838
+ const onPointerDown = () => {
839
+ try {
840
+ terminal.focus()
841
+ } catch {
842
+ /* ignore */
843
+ }
844
+ gufaTermDebug('terminal-focus', `pointerdown active=${document.activeElement === null ? 'null' : (document.activeElement.tagName ?? '?')}`)
845
+ }
846
+ host.addEventListener('pointerdown', onPointerDown, true)
847
+ try {
848
+ terminal.focus()
849
+ } catch {
850
+ /* ignore */
851
+ }
852
+ gufaTermDebug('terminal-focus', `focused active=${document.activeElement === null ? 'null' : (document.activeElement.tagName ?? '?')}`)
853
+ observer = new ResizeObserver(() => {
854
+ if (fitFrame !== 0) return
855
+ fitFrame = window.requestAnimationFrame(() => {
856
+ fitFrame = 0
857
+ fitNow()
858
+ })
859
+ })
860
+ observer.observe(host)
861
+ fitNow()
862
+ sendCurrentSize()
863
+ } catch (error) {
864
+ gufaTermDebug('terminal-xterm', `open-failed ${G.errorText(error)}`)
865
+ mode = 'fallback'
866
+ if (term !== null) {
867
+ try {
868
+ term.dispose()
869
+ } catch {
870
+ /* ignore */
871
+ }
872
+ term = null
873
+ termRef.current = null
874
+ fit = null
875
+ fitRef.current = null
876
+ }
877
+ if (observer !== null) {
878
+ try {
879
+ observer.disconnect()
880
+ } catch {
881
+ /* ignore */
882
+ }
883
+ observer = null
884
+ }
885
+ setFallback(true)
886
+ setNoticeSafe(G.errorText(error))
887
+ }
888
+ }
889
+
890
+ const offClearRequest = G.bus === null || G.bus === undefined ? () => {} : G.bus.on(`terminal:clear:${terminalId}`, () => {
891
+ pendingOutput = ''
892
+ if (term !== null) {
893
+ try {
894
+ term.clear()
895
+ } catch {
896
+ try {
897
+ term.reset()
898
+ } catch {
899
+ /* ignore */
900
+ }
901
+ }
902
+ if (activeRef.current === true) {
903
+ try {
904
+ term.focus()
905
+ } catch {
906
+ /* ignore */
907
+ }
908
+ }
909
+ }
910
+ setOutput('')
911
+ })
912
+
913
+ const channel = {
914
+ /** 写入当前终端(WS 未连接时先排队)。 */
915
+ send(data) {
916
+ postInput(data)
917
+ },
918
+ fit() {
919
+ fitNow()
920
+ sendCurrentSize()
921
+ },
922
+ focus() {
923
+ if (term !== null) {
924
+ try {
925
+ term.focus()
926
+ } catch {
927
+ /* ignore */
928
+ }
929
+ } else if (inputRef.current !== null) {
930
+ inputRef.current.focus()
931
+ }
932
+ },
933
+ }
934
+ channelRef.current = channel
935
+ /** 隐藏标签首次挂载时容器为 0 尺寸,切为活动标签后由这里补一次 open 尝试。 */
936
+ openerRef.current = () => {
937
+ if (term !== null || mode !== 'xterm' || disposed) return
938
+ if (openFrame !== 0) return
939
+ openRetries = 0
940
+ openFrame = window.requestAnimationFrame(tryOpen)
941
+ }
942
+ props.register(terminalId, channel)
943
+
944
+ startStream()
945
+ if (mode === 'xterm') tryOpen()
946
+
947
+ return () => {
948
+ disposed = true
949
+ if (retryTimer !== null) clearTimeout(retryTimer)
950
+ if (replayTimer !== null) clearTimeout(replayTimer)
951
+ if (openFrame !== 0) window.cancelAnimationFrame(openFrame)
952
+ if (fitFrame !== 0) window.cancelAnimationFrame(fitFrame)
953
+ if (observer !== null) {
954
+ try {
955
+ observer.disconnect()
956
+ } catch {
957
+ /* ignore */
958
+ }
959
+ }
960
+ if (streamAbort !== null) {
961
+ try {
962
+ streamAbort()
963
+ } catch {
964
+ /* ignore */
965
+ }
966
+ streamAbort = null
967
+ }
968
+ try {
969
+ offClearRequest()
970
+ } catch {
971
+ /* ignore */
972
+ }
973
+ channelRef.current = null
974
+ openerRef.current = null
975
+ props.unregister(terminalId)
976
+ if (term !== null) {
977
+ try {
978
+ term.dispose()
979
+ } catch {
980
+ /* ignore */
981
+ }
982
+ }
983
+ term = null
984
+ termRef.current = null
985
+ fit = null
986
+ fitRef.current = null
987
+ }
988
+ // assets/root/session.id 变化时重建连接与终端;回调由父组件保证稳定
989
+ // eslint-disable-next-line react-hooks/exhaustive-deps
990
+ }, [props.assets.status, props.root, session.id])
991
+
992
+ /* 切到活动标签:重新 fit(父容器此时可见)并聚焦;隐藏时不打扰。 */
993
+ useEffect(() => {
994
+ activeRef.current = props.active === true
995
+ if (props.active !== true || props.visible === false) return undefined
996
+ if (fallback) {
997
+ if (inputRef.current !== null) inputRef.current.focus()
998
+ return undefined
999
+ }
1000
+ const frame = window.requestAnimationFrame(() => {
1001
+ if (openerRef.current !== null) openerRef.current()
1002
+ const fit = fitRef.current
1003
+ if (fit !== null) {
1004
+ try {
1005
+ fit.fit()
1006
+ } catch {
1007
+ /* ignore */
1008
+ }
1009
+ }
1010
+ const term = termRef.current
1011
+ if (term !== null) {
1012
+ try {
1013
+ term.focus()
1014
+ } catch {
1015
+ /* ignore */
1016
+ }
1017
+ }
1018
+ })
1019
+ return () => window.cancelAnimationFrame(frame)
1020
+ }, [props.active, props.visible, fallback, props.assets.status])
1021
+
1022
+ /* 降级模式输出自动滚到底部(用户向上翻阅时不打扰)。 */
1023
+ useEffect(() => {
1024
+ const node = preRef.current
1025
+ if (node === null) return
1026
+ const pinned = node.scrollTop + node.clientHeight >= node.scrollHeight - 48
1027
+ if (pinned) node.scrollTop = node.scrollHeight
1028
+ }, [output])
1029
+
1030
+ const monoFont = 'ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace'
1031
+ const fallbackTheme = gufaTermTheme(props.theme, palette)
1032
+ const background = fallbackTheme.background
1033
+ const foreground = fallbackTheme.foreground
1034
+
1035
+ const body = fallback
1036
+ ? h(
1037
+ React.Fragment,
1038
+ null,
1039
+ h(
1040
+ 'pre',
1041
+ {
1042
+ ref: preRef,
1043
+ className: 'gufa-scroll',
1044
+ style: {
1045
+ flex: '1 1 auto',
1046
+ minHeight: 0,
1047
+ margin: 0,
1048
+ padding: '8px 10px',
1049
+ overflow: 'auto',
1050
+ whiteSpace: 'pre-wrap',
1051
+ wordBreak: 'break-word',
1052
+ fontFamily: monoFont,
1053
+ fontSize: 12.5,
1054
+ lineHeight: 1.5,
1055
+ color: foreground,
1056
+ background,
1057
+ },
1058
+ },
1059
+ output,
1060
+ ),
1061
+ h(
1062
+ 'div',
1063
+ { style: { display: 'flex', alignItems: 'center', gap: 6, padding: '6px 8px', borderTop: '1px solid var(--gufa-border)', background } },
1064
+ h('span', { style: { color: foreground, opacity: 0.6, fontFamily: monoFont, fontSize: 12 } }, '$'),
1065
+ h('input', {
1066
+ ref: inputRef,
1067
+ className: 'gufa-input',
1068
+ value: inputValue,
1069
+ spellCheck: false,
1070
+ autoComplete: 'off',
1071
+ autoCapitalize: 'off',
1072
+ placeholder: t('terminal.session'),
1073
+ onChange: (event) => setInputValue(event.target.value),
1074
+ onKeyDown: (event) => {
1075
+ if (event.key !== 'Enter') return
1076
+ event.preventDefault()
1077
+ const value = inputValue
1078
+ setInputValue('')
1079
+ const channel = channelRef.current
1080
+ if (channel !== null) channel.send(value === '' ? '\r' : `${value}\r`)
1081
+ },
1082
+ style: { flex: '1 1 auto', fontFamily: monoFont, fontSize: 12, height: 26 },
1083
+ }),
1084
+ h(
1085
+ G.IconButton,
1086
+ {
1087
+ title: t('chat.send'),
1088
+ onClick: () => {
1089
+ const value = inputValue
1090
+ setInputValue('')
1091
+ const channel = channelRef.current
1092
+ if (channel !== null) channel.send(value === '' ? '\r' : `${value}\r`)
1093
+ },
1094
+ },
1095
+ h(G.Icon, { name: 'arrow-right', size: 13 }),
1096
+ ),
1097
+ ),
1098
+ )
1099
+ : h('div', { ref: hostRef, style: { position: 'absolute', top: 4, right: 2, bottom: 2, left: 6 } })
1100
+
1101
+ return h(
1102
+ 'div',
1103
+ {
1104
+ style: {
1105
+ display: props.active === true ? 'flex' : 'none',
1106
+ flexDirection: 'column',
1107
+ flex: '1 1 auto',
1108
+ minHeight: 0,
1109
+ minWidth: 0,
1110
+ position: 'relative',
1111
+ background,
1112
+ },
1113
+ },
1114
+ body,
1115
+ notice !== ''
1116
+ ? h(
1117
+ 'div',
1118
+ {
1119
+ style: {
1120
+ position: 'absolute',
1121
+ top: 6,
1122
+ left: 8,
1123
+ right: 8,
1124
+ zIndex: 2,
1125
+ display: 'flex',
1126
+ alignItems: 'center',
1127
+ gap: 6,
1128
+ padding: '4px 8px',
1129
+ borderRadius: 6,
1130
+ fontSize: 11.5,
1131
+ color: 'var(--dsw-alias-state-error-primary)',
1132
+ background: 'color-mix(in srgb, var(--dsw-alias-state-error-primary) 12%, var(--dsw-alias-bg-overlay, #fff))',
1133
+ border: '1px solid color-mix(in srgb, var(--dsw-alias-state-error-primary) 38%, transparent)',
1134
+ },
1135
+ },
1136
+ h(G.Icon, { name: 'warn', size: 12 }),
1137
+ h('span', { style: { flex: '1 1 auto', minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } }, notice),
1138
+ h(
1139
+ G.IconButton,
1140
+ { title: t('common.close'), onClick: () => setNotice(''), style: { width: 20, height: 20 } },
1141
+ h(G.Icon, { name: 'close', size: 11 }),
1142
+ ),
1143
+ )
1144
+ : null,
1145
+ !connected && !fallback && !exited && notice === ''
1146
+ ? h(
1147
+ 'div',
1148
+ { style: { position: 'absolute', right: 10, bottom: 8, display: 'flex', alignItems: 'center', gap: 6, color: foreground, opacity: 0.55, fontSize: 11, pointerEvents: 'none' } },
1149
+ h(G.Spinner),
1150
+ t('common.loading'),
1151
+ )
1152
+ : null,
1153
+ )
1154
+ }
1155
+
1156
+ /* ── 终端面板 ───────────────────────────────────────────────────────────── */
1157
+ G.TerminalPane = function TerminalPane(props) {
1158
+ const t = G.useT()
1159
+ const root = typeof props.root === 'string' && props.root !== '' ? props.root : null
1160
+ const theme = props.theme === 'light' ? 'light' : 'dark'
1161
+ const visible = props.visible !== false
1162
+ const [assets, setAssets] = useState({ status: 'loading', Terminal: null, FitAddon: null, error: '' })
1163
+ const [sessions, setSessions] = useState([])
1164
+ const [activeId, setActiveId] = useState(null)
1165
+ const [error, setError] = useState('')
1166
+ const [creating, setCreating] = useState(false)
1167
+ const rootRef = useRef(root)
1168
+ const sizeRef = useRef(null)
1169
+ const tabStripRef = useRef(null)
1170
+ const channelsRef = useRef(new Map())
1171
+ const pendingInputRef = useRef([])
1172
+ const sessionsRef = useRef(sessions)
1173
+ const activeRef = useRef(activeId)
1174
+ const creatingRef = useRef(false)
1175
+ const bootRef = useRef(0)
1176
+ const aliveRef = useRef(true)
1177
+ rootRef.current = root
1178
+
1179
+ useEffect(() => {
1180
+ sessionsRef.current = sessions
1181
+ }, [sessions])
1182
+ useEffect(() => {
1183
+ activeRef.current = activeId
1184
+ }, [activeId])
1185
+ useEffect(() => () => {
1186
+ aliveRef.current = false
1187
+ }, [])
1188
+
1189
+ /* xterm 资产:首次挂载按需加载,失败降级。 */
1190
+ useEffect(() => {
1191
+ let cancelled = false
1192
+ gufaTermLoadAssets()
1193
+ .then((value) => {
1194
+ if (cancelled) return
1195
+ setAssets({ status: 'ready', Terminal: value.Terminal, FitAddon: value.FitAddon, error: '' })
1196
+ })
1197
+ .catch((loadError) => {
1198
+ gufaTermDebug('terminal-assets', `failed ${G.errorText(loadError)}`)
1199
+ if (cancelled) return
1200
+ setAssets({ status: 'failed', Terminal: null, FitAddon: null, error: G.errorText(loadError) })
1201
+ })
1202
+ return () => {
1203
+ cancelled = true
1204
+ }
1205
+ }, [])
1206
+
1207
+ /** 把队列里的输入写入当前活动终端。 */
1208
+ const drainPending = () => {
1209
+ if (pendingInputRef.current.length === 0) return
1210
+ const id = activeRef.current
1211
+ const channel = id === null ? undefined : channelsRef.current.get(id)
1212
+ if (channel === undefined) return
1213
+ const queue = pendingInputRef.current
1214
+ pendingInputRef.current = []
1215
+ for (const text of queue) channel.send(text)
1216
+ }
1217
+
1218
+ const createSession = useCallback(async (generation = bootRef.current) => {
1219
+ const targetRoot = rootRef.current
1220
+ if (targetRoot === null || creatingRef.current) return null
1221
+ creatingRef.current = true
1222
+ setCreating(true)
1223
+ try {
1224
+ const box = sizeRef.current
1225
+ const dims = gufaTermEstimateSize(box === null ? 0 : box.clientWidth, box === null ? 0 : box.clientHeight)
1226
+ const value = await G.api('terminal.create', { root: targetRoot, cols: dims.cols, rows: dims.rows })
1227
+ // 组件已卸载 / 创建期间项目或挂载代次已变:回收这个孤儿终端
1228
+ if (!aliveRef.current || bootRef.current !== generation || rootRef.current !== targetRoot) {
1229
+ void G.api('terminal.close', { root: targetRoot, id: value.id }).catch(() => {})
1230
+ return null
1231
+ }
1232
+ const created = {
1233
+ id: value.id,
1234
+ title: typeof value.title === 'string' && value.title !== '' ? value.title : G.translate('terminal.session'),
1235
+ exited: false,
1236
+ exitCode: null,
1237
+ }
1238
+ setSessions((current) => (current.some((item) => item.id === created.id) ? current : [...current, created]))
1239
+ setActiveId((current) => current ?? created.id)
1240
+ setError('')
1241
+ return created
1242
+ } catch (createError) {
1243
+ gufaTermDebug('terminal-create', `failed ${G.errorText(createError)}`)
1244
+ if (aliveRef.current) setError(G.errorText(createError))
1245
+ return null
1246
+ } finally {
1247
+ creatingRef.current = false
1248
+ if (aliveRef.current) setCreating(false)
1249
+ }
1250
+ }, [])
1251
+
1252
+ /* 根目录变化:恢复该项目的终端列表,没有则建第一个。 */
1253
+ useEffect(() => {
1254
+ const generation = ++bootRef.current
1255
+ const stale = () => !aliveRef.current || bootRef.current !== generation || rootRef.current !== root
1256
+ setError('')
1257
+ if (root === null) {
1258
+ setSessions([])
1259
+ setActiveId(null)
1260
+ return undefined
1261
+ }
1262
+ setSessions([])
1263
+ setActiveId(null)
1264
+ ;(async () => {
1265
+ try {
1266
+ const value = await G.api('terminal.list', { root })
1267
+ if (stale()) return
1268
+ const list = Array.isArray(value?.terminals) ? value.terminals : []
1269
+ if (list.length === 0) {
1270
+ await createSession(generation)
1271
+ return
1272
+ }
1273
+ const restored = []
1274
+ for (const item of list) {
1275
+ if (item === null || typeof item !== 'object' || typeof item.id !== 'string') continue
1276
+ restored.push({
1277
+ id: item.id,
1278
+ title: typeof item.title === 'string' && item.title !== '' ? item.title : G.translate('terminal.session'),
1279
+ exited: item.alive !== true,
1280
+ exitCode: typeof item.exitCode === 'number' ? item.exitCode : null,
1281
+ })
1282
+ }
1283
+ setSessions(restored)
1284
+ if (restored.length > 0) setActiveId(restored[0].id)
1285
+ } catch (listError) {
1286
+ if (stale()) return
1287
+ setError(G.errorText(listError))
1288
+ }
1289
+ })()
1290
+ return undefined
1291
+ }, [root, createSession])
1292
+
1293
+ /* 快捷键:清除当前终端内容 / 新建终端。 */
1294
+ useEffect(() => {
1295
+ if (G.bus === null || G.bus === undefined) return undefined
1296
+ const offClear = G.bus.on('terminal:clear', () => {
1297
+ const id = activeRef.current
1298
+ if (id === null) return
1299
+ G.bus.emit(`terminal:clear:${id}`)
1300
+ })
1301
+ const offNew = G.bus.on('terminal:new', () => {
1302
+ void createSession()
1303
+ })
1304
+ return () => {
1305
+ try {
1306
+ offClear()
1307
+ } catch {
1308
+ /* ignore */
1309
+ }
1310
+ try {
1311
+ offNew()
1312
+ } catch {
1313
+ /* ignore */
1314
+ }
1315
+ }
1316
+ }, [createSession])
1317
+
1318
+ /* G.bus 'terminal:send'(SSH 快捷连接等):写入当前终端;没有终端时先建一个。 */
1319
+ useEffect(() => {
1320
+ return G.bus.on('terminal:send', (detail) => {
1321
+ const text = detail !== null && typeof detail === 'object' && typeof detail.text === 'string' ? detail.text : ''
1322
+ if (text === '') return
1323
+ pendingInputRef.current.push(text)
1324
+ drainPending()
1325
+ if (rootRef.current !== null && sessionsRef.current.length === 0 && !creatingRef.current) void createSession()
1326
+ })
1327
+ // drainPending 只依赖 ref,无需列为依赖
1328
+ // eslint-disable-next-line react-hooks/exhaustive-deps
1329
+ }, [createSession])
1330
+
1331
+ /* 标签过多时把活动标签滚入可视区。 */
1332
+ useEffect(() => {
1333
+ const node = tabStripRef.current
1334
+ if (node === null || typeof node.querySelector !== 'function') return
1335
+ const activeTab = node.querySelector('[data-active="true"]')
1336
+ if (activeTab !== null && typeof activeTab.scrollIntoView === 'function') {
1337
+ try {
1338
+ activeTab.scrollIntoView({ block: 'nearest', inline: 'nearest' })
1339
+ } catch {
1340
+ /* 老浏览器忽略 */
1341
+ }
1342
+ }
1343
+ }, [activeId, sessions.length])
1344
+
1345
+ /* 活动终端变化后补发排队输入。 */
1346
+ useEffect(() => {
1347
+ activeRef.current = activeId
1348
+ drainPending()
1349
+ // eslint-disable-next-line react-hooks/exhaustive-deps
1350
+ }, [activeId, sessions.length])
1351
+
1352
+ const registerChannel = useCallback((id, channel) => {
1353
+ channelsRef.current.set(id, channel)
1354
+ if (activeRef.current === null || activeRef.current === id) drainPending()
1355
+ }, [])
1356
+ const unregisterChannel = useCallback((id) => {
1357
+ channelsRef.current.delete(id)
1358
+ }, [])
1359
+
1360
+ const closeSession = useCallback((id) => {
1361
+ const targetRoot = rootRef.current
1362
+ const remaining = sessionsRef.current.filter((item) => item.id !== id)
1363
+ setSessions(remaining)
1364
+ setActiveId((current) => {
1365
+ if (current !== id) return current
1366
+ return remaining.length === 0 ? null : remaining[remaining.length - 1].id
1367
+ })
1368
+ if (targetRoot !== null) {
1369
+ void G.api('terminal.close', { root: targetRoot, id }).catch((closeError) => {
1370
+ if (aliveRef.current) setError(G.errorText(closeError))
1371
+ })
1372
+ }
1373
+ }, [])
1374
+
1375
+ const handleExit = useCallback((id, code) => {
1376
+ setSessions((current) => current.map((item) => (item.id === id ? { ...item, exited: true, exitCode: typeof code === 'number' ? code : null } : item)))
1377
+ }, [])
1378
+
1379
+ const handleTitle = useCallback((id, title) => {
1380
+ setSessions((current) => {
1381
+ let changed = false
1382
+ const next = current.map((item) => {
1383
+ if (item.id !== id) return item
1384
+ if (item.title === title) return item
1385
+ changed = true
1386
+ return { ...item, title }
1387
+ })
1388
+ return changed ? next : current
1389
+ })
1390
+ }, [])
1391
+
1392
+ const height = typeof props.height === 'number' && props.height > 0 ? props.height : 230
1393
+ const tabStrip = []
1394
+ for (const session of sessions) {
1395
+ tabStrip.push(
1396
+ h(GufaTerminalTab, {
1397
+ key: session.id,
1398
+ session,
1399
+ active: session.id === activeId,
1400
+ onSelect: () => setActiveId(session.id),
1401
+ onClose: () => closeSession(session.id),
1402
+ }),
1403
+ )
1404
+ }
1405
+
1406
+ const body = (() => {
1407
+ if (root === null) {
1408
+ return h(G.Empty, { icon: 'terminal', title: t('explorer.openProjectFirst'), hint: t('app.noProjectHint') })
1409
+ }
1410
+ if (assets.status === 'loading') {
1411
+ return h('div', { style: { display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 8, height: '100%', color: 'var(--gufa-text-dim)', fontSize: 12 } }, h(G.Spinner), t('common.loading'))
1412
+ }
1413
+ if (sessions.length === 0) {
1414
+ return h(
1415
+ 'div',
1416
+ { style: { display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', gap: 8, height: '100%' } },
1417
+ h(G.Button, { icon: 'plus', disabled: creating, onClick: () => void createSession() }, t('terminal.new')),
1418
+ )
1419
+ }
1420
+ const views = []
1421
+ for (const session of sessions) {
1422
+ views.push(
1423
+ h(GufaTerminalView, {
1424
+ key: session.id,
1425
+ session,
1426
+ root,
1427
+ active: session.id === activeId,
1428
+ visible,
1429
+ theme,
1430
+ palette: props.palette,
1431
+ assets,
1432
+ register: registerChannel,
1433
+ unregister: unregisterChannel,
1434
+ onExit: handleExit,
1435
+ onTitle: handleTitle,
1436
+ }),
1437
+ )
1438
+ }
1439
+ return views
1440
+ })()
1441
+
1442
+ const canCreate = root !== null && assets.status !== 'loading' && sessions.length < 8 && creating !== true
1443
+
1444
+ return h(
1445
+ 'div',
1446
+ {
1447
+ style: {
1448
+ height,
1449
+ flex: '0 0 auto',
1450
+ display: 'flex',
1451
+ flexDirection: 'column',
1452
+ minHeight: 0,
1453
+ minWidth: 0,
1454
+ borderTop: '1px solid var(--gufa-border)',
1455
+ background: 'var(--gufa-bg)',
1456
+ },
1457
+ },
1458
+ h(
1459
+ 'div',
1460
+ {
1461
+ style: {
1462
+ display: 'flex',
1463
+ alignItems: 'center',
1464
+ gap: 4,
1465
+ height: 32,
1466
+ padding: '0 6px 0 8px',
1467
+ flex: '0 0 auto',
1468
+ borderBottom: '1px solid var(--gufa-border)',
1469
+ background: 'var(--gufa-layer)',
1470
+ minWidth: 0,
1471
+ },
1472
+ },
1473
+ h(G.Icon, { name: 'terminal', size: 13 }),
1474
+ h('span', { style: { fontSize: 11, fontWeight: 600, letterSpacing: '0.06em', color: 'var(--gufa-text-dim)', whiteSpace: 'nowrap', marginRight: 2 } }, t('terminal.title')),
1475
+ h('div', { ref: tabStripRef, style: { display: 'flex', alignItems: 'stretch', gap: 2, flex: '1 1 auto', minWidth: 0, overflowX: 'auto', overflowY: 'hidden', scrollbarWidth: 'none' } }, tabStrip),
1476
+ h(
1477
+ G.IconButton,
1478
+ { title: t('terminal.new'), disabled: canCreate !== true, onClick: () => void createSession() },
1479
+ h(G.Icon, { name: 'plus', size: 13 }),
1480
+ ),
1481
+ h(
1482
+ G.IconButton,
1483
+ { title: t('common.close'), onClick: () => props.onToggleHeight?.() },
1484
+ h(G.Icon, { name: 'chevron-down', size: 12 }),
1485
+ ),
1486
+ ),
1487
+ error !== ''
1488
+ ? h(
1489
+ 'div',
1490
+ { className: 'gufa-error-banner', style: { margin: '4px 8px', flex: '0 0 auto' } },
1491
+ h(G.Icon, { name: 'warn', size: 12 }),
1492
+ h('span', { style: { flex: '1 1 auto', minWidth: 0 } }, error),
1493
+ h(G.IconButton, { title: t('common.close'), onClick: () => setError('') }, h(G.Icon, { name: 'close', size: 11 })),
1494
+ )
1495
+ : null,
1496
+ assets.status === 'failed'
1497
+ ? h(
1498
+ 'div',
1499
+ { className: 'gufa-error-banner', style: { margin: '4px 8px', flex: '0 0 auto' } },
1500
+ h(G.Icon, { name: 'warn', size: 12 }),
1501
+ h('span', { style: { flex: '1 1 auto', minWidth: 0 } }, `${t('common.error')}: ${assets.error}`),
1502
+ )
1503
+ : null,
1504
+ h(
1505
+ 'div',
1506
+ { ref: sizeRef, style: { position: 'relative', flex: '1 1 auto', display: 'flex', flexDirection: 'column', minHeight: 0, minWidth: 0, background: theme === 'dark' ? '#1e1e1e' : '#ffffff' } },
1507
+ body,
1508
+ ),
1509
+ )
1510
+ }