dsh-gufa-code 0.0.0-stage → 1.0.1

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 +93 -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-ide.jpg +0 -0
  9. package/host/assets.js +31 -0
  10. package/host/chat.js +523 -0
  11. package/host/context.js +438 -0
  12. package/host/dev.js +35 -0
  13. package/host/fs.js +306 -0
  14. package/host/git.js +532 -0
  15. package/host/project.js +305 -0
  16. package/host/ssh.js +54 -0
  17. package/host/terminal-pty.js +237 -0
  18. package/host/terminal.js +947 -0
  19. package/icon.svg +18 -0
  20. package/index.js +63 -0
  21. package/locale/en.json +121 -0
  22. package/locale/zh.json +121 -0
  23. package/package.json +67 -4
  24. package/screenshots.json +3 -0
  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,662 @@
1
+ /**
2
+ * 60-dock:把官方 DSH 会话面板「停靠」进古法编程右侧的 AI 栏。
3
+ *
4
+ * 背景:右侧 AI 必须是真正的 DSH 会话本体(带工具调用 / 自动改文件 / 审批 / 差异审查),
5
+ * 而不是插件自造的轻量对话。官方会话渲染在 AppFrame 的 main 槽里,古法编程以
6
+ * `shell.overlay` 覆盖全屏后它仍挂载在页面中(只是被盖住)。本模块找到它的 DOM 根
7
+ * (ConversationRoot,`[data-phase]` + 内部 `[data-conversation-scroll]`),用
8
+ * `position:fixed` 把它浮到右侧停靠区,并随停靠区尺寸/会话切换持续校准;退出时还原。
9
+ *
10
+ * 要点:
11
+ * - 不改动官方组件树、不 reparent(避免 React 卸载/重排风险),只改根节点 inline style;
12
+ * - 会话切换 / 重渲染替换 DOM 根时自动重新停靠(MutationObserver + 250ms 轮询兜底);
13
+ * - 停靠区的坐标来自真实占位元素 getBoundingClientRect,支持窗口缩放与左右拖拽;
14
+ * - 找不到会话(例如没有选中会话)时降级为内置轻量对话,并给出明确提示与恢复入口。
15
+ */
16
+
17
+ /** 官方会话滚动容器(ConversationRoot 独有)。 */
18
+ const GUFA_CONVERSATION_SCROLL_SELECTOR = '[data-conversation-scroll]'
19
+
20
+ /** 找到官方会话的 DOM 根(ConversationRoot)。 */
21
+ function gufaFindConversationRoot() {
22
+ if (typeof document === 'undefined') return null
23
+ const scroller = document.querySelector(GUFA_CONVERSATION_SCROLL_SELECTOR)
24
+ if (scroller !== null && scroller !== undefined && typeof scroller.closest === 'function') {
25
+ const root = scroller.closest('[data-phase]')
26
+ if (root !== null && root !== undefined && typeof root.getAttribute === 'function') return root
27
+ }
28
+ // 回退启发:带 data-phase 且内部有 conversation.* 插槽的节点
29
+ const candidates = typeof document.querySelectorAll === 'function' ? Array.from(document.querySelectorAll('[data-phase]')) : []
30
+ for (const node of candidates) {
31
+ if (node === null || node === undefined || typeof node.querySelector !== 'function') continue
32
+ if (node.querySelector('[data-slot^="conversation"]') !== null) return node
33
+ }
34
+ return null
35
+ }
36
+
37
+ /** 记录根节点原始 inline 状态,便于退出时逐字节还原。 */
38
+ function gufaSnapshotDockTarget(node) {
39
+ return { style: node.getAttribute('style'), dock: node.getAttribute('data-gufa-dock') }
40
+ }
41
+
42
+ /** 还原根节点 inline 状态。 */
43
+ function gufaRestoreDockTarget(node, saved) {
44
+ try {
45
+ if (saved === null || saved === undefined) return
46
+ if (saved.style === null) node.removeAttribute('style')
47
+ else node.setAttribute('style', saved.style)
48
+ if (saved.dock === null) node.removeAttribute('data-gufa-dock')
49
+ else node.setAttribute('data-gufa-dock', saved.dock)
50
+ } catch {
51
+ /* 节点可能已被 React 移除,忽略 */
52
+ }
53
+ }
54
+
55
+ /**
56
+ * 把会话根浮到停靠区的位置与尺寸;返回是否成功。
57
+ *
58
+ * 窄栏适配:主视图会话的内容宽度默认 `clamp(680px, 列宽*0.64, 920px)`,直接在 360px 栏里
59
+ * 会横向溢出,因此覆盖 `--dsh-chat-user-width` / `--dsh-conversation-column-width`,
60
+ * 让消息与 composer 按栏宽排版。
61
+ *
62
+ * 若祖先存在 transform/contain 导致 position:fixed 相对于祖先而非视口,
63
+ * 用「测实际位置 → 补偿差值」迭代 2 次把会话对齐到停靠区。
64
+ */
65
+ function gufaPlaceDockedConversation(root, host, options) {
66
+ const rect = host.getBoundingClientRect()
67
+ if (!(rect.width >= 80 && rect.height >= 80)) return false
68
+ const target = { left: Math.round(rect.left), top: Math.round(rect.top), width: Math.round(rect.width), height: Math.round(rect.height) }
69
+ root.style.position = 'fixed'
70
+ root.style.left = `${target.left}px`
71
+ root.style.top = `${target.top}px`
72
+ root.style.width = `${target.width}px`
73
+ root.style.height = `${target.height}px`
74
+ root.style.margin = '0'
75
+ root.style.maxWidth = 'none'
76
+ root.style.zIndex = '2147483040'
77
+ let dockBackground = ''
78
+ try {
79
+ const hostStyle = window.getComputedStyle(host)
80
+ // 背景模式下停靠区容器是透明的,实色底色从覆盖层变量里取(--gufa-solid-bg)。
81
+ const solid = G.parseCssColor((hostStyle.getPropertyValue('--gufa-solid-bg') || '').trim())
82
+ const candidate = solid !== null && solid[3] >= 0.999 ? solid : G.parseCssColor(hostStyle.backgroundColor)
83
+ if (candidate !== null && candidate[3] >= 0.999) dockBackground = `rgb(${candidate[0]}, ${candidate[1]}, ${candidate[2]})`
84
+ } catch {
85
+ /* ignore */
86
+ }
87
+ const backdropMode = options !== null && options !== undefined && options.translucent === true
88
+ if (backdropMode && dockBackground !== '') root.style.background = `color-mix(in srgb, ${dockBackground} 34%, transparent)`
89
+ else root.style.background = dockBackground === '' ? 'var(--gufa-solid-bg, #1a1b1f)' : dockBackground
90
+ root.style.borderLeft = '1px solid var(--dsw-alias-border-l1)'
91
+ root.style.boxSizing = 'border-box'
92
+ root.style.setProperty('--dsh-chat-user-width', 'calc(100% - 24px)')
93
+ root.style.setProperty('--dsh-conversation-column-width', `${target.width}px`)
94
+ root.setAttribute('data-gufa-dock', '1')
95
+ // 容器变换修正(最多 2 次)
96
+ for (let attempt = 0; attempt < 2; attempt += 1) {
97
+ const actual = root.getBoundingClientRect()
98
+ const dx = Math.round(target.left - actual.left)
99
+ const dy = Math.round(target.top - actual.top)
100
+ if (Math.abs(dx) <= 1 && Math.abs(dy) <= 1) break
101
+ root.style.left = `${Math.round(parseFloat(root.style.left || '0') + dx)}px`
102
+ root.style.top = `${Math.round(parseFloat(root.style.top || '0') + dy)}px`
103
+ }
104
+ return true
105
+ }
106
+
107
+ /** 毫秒时间戳 → 简短相对时间(会话下拉用)。 */
108
+ function gufaRelativeTime(timestamp) {
109
+ const delta = Date.now() - timestamp
110
+ if (!Number.isFinite(delta) || delta < 0) return ''
111
+ const minute = 60_000
112
+ if (delta < minute) return '刚刚'
113
+ if (delta < 60 * minute) return `${Math.floor(delta / minute)} 分钟前`
114
+ if (delta < 24 * 60 * minute) return `${Math.floor(delta / (60 * minute))} 小时前`
115
+ if (delta < 30 * 24 * 60 * minute) return `${Math.floor(delta / (24 * 60 * minute))} 天前`
116
+ return ''
117
+ }
118
+
119
+ G.relativeTime = gufaRelativeTime
120
+
121
+ /**
122
+ * 全局弹层(MenuSurface)配色修正。
123
+ *
124
+ * 官方弹层背景来自 `--dsw-menu-surface-fill`(浅色 #f8f9fa94 + blur(40px) saturate(150%)),
125
+ * 滑动变祖皮肤并**不覆盖**这两个 token;当皮肤处于暗色调、而 DSH 基础主题标记仍是浅色时,
126
+ * 弹层会出现「浅色半透明底 + 皮肤浅色文字」→ 完全看不清。
127
+ *
128
+ * IDE 模式下把这两个 token 覆盖成我们合成的实色(跟随滑动变祖当前配色),退出 IDE 时还原。
129
+ * 只覆盖这一个表面 token + 关闭 blur,避免影响其他布局。
130
+ */
131
+ const GUFA_POPUP_TOKENS = [
132
+ '--dsw-menu-surface-fill',
133
+ '--dsw-menu-backdrop-filter',
134
+ '--dsw-specific-menu',
135
+ '--dsw-alias-bg-overlay',
136
+ '--dsw-alias-bg-module-platform',
137
+ ]
138
+ let gufaPopupSaved = null
139
+ let gufaBodyDarkSaved = null
140
+ let gufaPopupProbe = null
141
+
142
+ /** 各元素上被覆盖 token 的原始值(html 与 body 各存一份,皮肤在 body 上用了 !important)。 */
143
+ function gufaPopupTargets() {
144
+ const targets = []
145
+ if (typeof document !== 'undefined' && document.documentElement !== undefined) targets.push(document.documentElement)
146
+ if (typeof document !== 'undefined' && document.body !== null && document.body !== undefined) targets.push(document.body)
147
+ return targets
148
+ }
149
+
150
+ /** rgb(...) → #rrggbb(诊断探针 id 用)。 */
151
+ function gufaRgbToHex(value) {
152
+ const match = /rgba?\(\s*(\d+)[,\s]+(\d+)[,\s]+(\d+)/.exec(String(value ?? ''))
153
+ if (match === null) return 'unknown'
154
+ return `#${[match[1], match[2], match[3]].map((part) => Number(part).toString(16).padStart(2, '0')).join('')}`
155
+ }
156
+
157
+ function gufaRegisterPopupProbe(color) {
158
+ try {
159
+ if (typeof G.ctx?.inject !== 'function') return
160
+ if (typeof gufaPopupProbe === 'function') {
161
+ try {
162
+ gufaPopupProbe()
163
+ } catch {
164
+ /* ignore */
165
+ }
166
+ gufaPopupProbe = null
167
+ }
168
+ G.ctx.inject(['slots'], (scope) => {
169
+ try {
170
+ gufaPopupProbe = scope.slots.register({ name: 'shell.overlay', id: `gufa-code-popup-theme-${gufaRgbToHex(color)}`, order: 995 }, () => null)
171
+ } catch {
172
+ /* ignore */
173
+ }
174
+ })
175
+ } catch {
176
+ /* ignore */
177
+ }
178
+ }
179
+
180
+ G.applyPopupTheme = function applyPopupTheme(palette) {
181
+ try {
182
+ const targets = gufaPopupTargets()
183
+ if (targets.length === 0) return
184
+ if (gufaPopupSaved === null) gufaPopupSaved = new Map()
185
+ const values = {
186
+ '--dsw-menu-surface-fill': palette.overlay,
187
+ '--dsw-menu-backdrop-filter': 'none',
188
+ '--dsw-specific-menu': palette.overlay,
189
+ '--dsw-alias-bg-overlay': palette.overlay,
190
+ '--dsw-alias-bg-module-platform': palette.layer2,
191
+ }
192
+ for (const element of targets) {
193
+ if (!gufaPopupSaved.has(element)) gufaPopupSaved.set(element, new Map())
194
+ const saved = gufaPopupSaved.get(element)
195
+ for (const name of GUFA_POPUP_TOKENS) {
196
+ if (!saved.has(name)) saved.set(name, { value: element.style.getPropertyValue(name), priority: element.style.getPropertyPriority(name) })
197
+ }
198
+ for (const [name, value] of Object.entries(values)) element.style.setProperty(name, value, 'important')
199
+ }
200
+ document.documentElement.setAttribute('data-gufa-ide', '')
201
+ /* 深色皮肤下 DSH 自带组件(交付卡片等)依赖 body[data-ds-dark-theme] 切深色主题 */
202
+ try {
203
+ if (palette.isDark === true) {
204
+ if (gufaBodyDarkSaved === null) {
205
+ gufaBodyDarkSaved = { had: document.body.hasAttribute('data-ds-dark-theme'), value: document.body.getAttribute('data-ds-dark-theme') }
206
+ }
207
+ if (!document.body.hasAttribute('data-ds-dark-theme')) document.body.setAttribute('data-ds-dark-theme', '')
208
+ }
209
+ } catch {
210
+ /* ignore */
211
+ }
212
+ gufaRegisterPopupProbe(palette.overlay)
213
+ } catch (error) {
214
+ console.error('[gufa-code] 修正弹层配色失败', error)
215
+ }
216
+ }
217
+
218
+ G.restorePopupTheme = function restorePopupTheme() {
219
+ try {
220
+ if (typeof document === 'undefined' || document.documentElement === undefined) return
221
+ document.documentElement.removeAttribute('data-gufa-ide')
222
+ try {
223
+ if (gufaBodyDarkSaved !== null) {
224
+ if (gufaBodyDarkSaved.had) document.body.setAttribute('data-ds-dark-theme', gufaBodyDarkSaved.value ?? '')
225
+ else document.body.removeAttribute('data-ds-dark-theme')
226
+ gufaBodyDarkSaved = null
227
+ }
228
+ } catch {
229
+ gufaBodyDarkSaved = null
230
+ }
231
+ if (gufaPopupSaved === null) return
232
+ for (const [element, saved] of gufaPopupSaved) {
233
+ for (const [name, original] of saved) {
234
+ if (original.value === '') element.style.removeProperty(name)
235
+ else element.style.setProperty(name, original.value, original.priority)
236
+ }
237
+ }
238
+ gufaPopupSaved = null
239
+ if (typeof gufaPopupProbe === 'function') {
240
+ try {
241
+ gufaPopupProbe()
242
+ } catch {
243
+ /* ignore */
244
+ }
245
+ }
246
+ gufaPopupProbe = null
247
+ } catch (error) {
248
+ console.error('[gufa-code] 还原弹层配色失败', error)
249
+ }
250
+ }
251
+
252
+ /** 纯函数暴露:供自测脚本与浏览器控制台排查使用(不属于稳定 API)。 */
253
+ G.__dock = {
254
+ findConversationRoot: gufaFindConversationRoot,
255
+ placeDockedConversation: gufaPlaceDockedConversation,
256
+ snapshot: gufaSnapshotDockTarget,
257
+ restore: gufaRestoreDockTarget,
258
+ }
259
+
260
+ /**
261
+ * 停靠栏:官方会话本体(浮动) + 顶部工具条(新建会话 / 打开完整对话 / 收起)。
262
+ * props = { project, activeFile, selection, theme, width, onInsertCode, backdrop, active, sessionTitle, sessions, onOpenSession }
263
+ */
264
+ G.ChatDock = function ChatDock(props) {
265
+ const t = G.useT()
266
+ const theme = props.theme === 'light' ? 'light' : 'dark'
267
+ const hostRef = useRef(null)
268
+ const dockRef = useRef({ root: null, saved: null })
269
+ const tickRef = useRef(null)
270
+ const startedRef = useRef(0)
271
+ const [phase, setPhase] = useState('searching')
272
+ const [error, setError] = useState('')
273
+ const active = props.active !== false && props.paused !== true
274
+
275
+ const undock = useCallback(() => {
276
+ const current = dockRef.current
277
+ if (current.root !== null) gufaRestoreDockTarget(current.root, current.saved)
278
+ dockRef.current = { root: null, saved: null }
279
+ }, [])
280
+
281
+ const tick = useCallback(() => {
282
+ if (!active) return
283
+ const host = hostRef.current
284
+ if (host === null) return
285
+ const root = gufaFindConversationRoot()
286
+ if (root === null) {
287
+ if (dockRef.current.root !== null) undock()
288
+ setPhase((previous) => {
289
+ if (previous === 'docked') return 'searching'
290
+ if (startedRef.current !== 0 && Date.now() - startedRef.current > 5000) return 'missing'
291
+ return previous
292
+ })
293
+ return
294
+ }
295
+ if (dockRef.current.root !== root) {
296
+ undock()
297
+ dockRef.current = { root, saved: gufaSnapshotDockTarget(root) }
298
+ }
299
+ if (gufaPlaceDockedConversation(root, host, { translucent: props.backdrop === true })) {
300
+ setPhase((previous) => (previous === 'docked' ? previous : 'docked'))
301
+ }
302
+ }, [active, undock, props.backdrop])
303
+
304
+ useEffect(() => {
305
+ tickRef.current = tick
306
+ }, [tick])
307
+
308
+ // 生命周期:进入时轮询 + MutationObserver;退出时还原官方会话节点
309
+ useEffect(() => {
310
+ if (!active) {
311
+ undock()
312
+ return undefined
313
+ }
314
+ startedRef.current = Date.now()
315
+ setPhase((previous) => (previous === 'docked' ? previous : 'searching'))
316
+ let disposed = false
317
+ const run = () => {
318
+ if (!disposed) tickRef.current?.()
319
+ }
320
+ run()
321
+ const timer = window.setInterval(run, 250)
322
+ const observer = typeof MutationObserver === 'function' ? new MutationObserver(run) : null
323
+ if (observer !== null) observer.observe(document.body, { childList: true, subtree: true })
324
+ let resizeObserver = null
325
+ if (typeof ResizeObserver === 'function') {
326
+ resizeObserver = new ResizeObserver(run)
327
+ const host = hostRef.current
328
+ if (host !== null) resizeObserver.observe(host)
329
+ }
330
+ const onWindowResize = () => run()
331
+ window.addEventListener('resize', onWindowResize)
332
+ return () => {
333
+ disposed = true
334
+ window.clearInterval(timer)
335
+ observer?.disconnect()
336
+ resizeObserver?.disconnect()
337
+ window.removeEventListener('resize', onWindowResize)
338
+ undock()
339
+ }
340
+ }, [active, undock])
341
+
342
+ // 拖拽分割条改变宽度时立即重定位(轮询之外)
343
+ useEffect(() => {
344
+ tickRef.current?.()
345
+ }, [props.width, props.backdrop])
346
+
347
+ const newSession = useCallback(() => {
348
+ const ui = G.ctx === null || G.ctx === undefined ? undefined : G.ctx.get('uiWorkspace')
349
+ if (ui === undefined || ui === null || typeof ui.startSession !== 'function') {
350
+ setError('当前界面不支持新建会话')
351
+ return
352
+ }
353
+ try {
354
+ ui.startSession()
355
+ setError('')
356
+ startedRef.current = Date.now()
357
+ setPhase('searching')
358
+ } catch (failure) {
359
+ setError(G.errorText(failure))
360
+ }
361
+ }, [])
362
+
363
+ const retry = useCallback(() => {
364
+ startedRef.current = Date.now()
365
+ setError('')
366
+ setPhase('searching')
367
+ tickRef.current?.()
368
+ }, [])
369
+
370
+ const missing = phase === 'missing'
371
+
372
+ return h(
373
+ 'div',
374
+ { className: 'gufa-chatdock' },
375
+ h(
376
+ 'div',
377
+ { className: 'gufa-dock-toolbar' },
378
+ h(G.Icon, { name: 'sparkles', size: 14 }),
379
+ h('span', { className: 'gufa-dock-title' }, t('dock.title')),
380
+ h('span', { className: 'gufa-dock-sub' }, phase === 'docked' && typeof props.sessionTitle === 'string' && props.sessionTitle !== '' ? props.sessionTitle : t('dock.sub')),
381
+ phase === 'docked' ? h('span', { className: 'gufa-dock-live', title: t('dock.live') }) : null,
382
+ h('div', { className: 'gufa-dock-fill' }),
383
+ h(G.IconButton, { title: t('dock.sessions'), onClick: () => props.onOpenSessions?.() }, h(G.Icon, { name: 'history', size: 14 })),
384
+ h(G.IconButton, { title: t('dock.newSession'), onClick: newSession }, h(G.Icon, { name: 'plus', size: 14 })),
385
+ ),
386
+ h(
387
+ 'div',
388
+ { className: 'gufa-dock-host', ref: hostRef },
389
+ error !== '' ? h('div', { className: 'gufa-error-banner', style: { margin: '6px 8px' } }, h(G.Icon, { name: 'warn', size: 12 }), h('span', { style: { flex: '1 1 auto' } }, error), h(G.IconButton, { title: t('common.close'), onClick: () => setError('') }, h(G.Icon, { name: 'close', size: 11 }))) : null,
390
+ missing
391
+ ? h(
392
+ 'div',
393
+ { className: 'gufa-dock-notice' },
394
+ h(G.Icon, { name: 'warn', size: 12 }),
395
+ h('span', { style: { flex: '1 1 auto' } }, t('dock.missingNotice')),
396
+ h(G.IconButton, { title: t('dock.retry'), onClick: retry }, h(G.Icon, { name: 'refresh', size: 12 })),
397
+ )
398
+ : null,
399
+ missing
400
+ ? h(
401
+ 'div',
402
+ { className: 'gufa-dock-fallback' },
403
+ h(G.ChatPanel, {
404
+ project: props.project ?? null,
405
+ activeFile: props.activeFile ?? null,
406
+ selection: props.selection ?? null,
407
+ theme,
408
+ width: props.width,
409
+ onInsertCode: props.onInsertCode,
410
+ }),
411
+ )
412
+ : phase === 'searching'
413
+ ? h(
414
+ 'div',
415
+ { style: { flex: '1 1 auto', display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', gap: 10, color: 'var(--gufa-text-dim)', fontSize: 12, textAlign: 'center', padding: 18 } },
416
+ h(G.Spinner),
417
+ h('div', null, t('dock.missingHint')),
418
+ h(G.Button, { primary: true, icon: 'plus', onClick: newSession }, t('dock.newSession')),
419
+ )
420
+ : null,
421
+ ),
422
+ )
423
+ }
424
+
425
+ /**
426
+ * 把 DSH 侧栏底部整块 footer(含「今日消费」卡片 + 账号行 + 设置按钮 + 版本行)
427
+ * 停靠到古法编程左下角。
428
+ *
429
+ * 做法与官方会话停靠一致:不搬 DOM、不改插件 React 树,只给
430
+ * `[data-pane="sidebar"] / [class*="sidebarCol"]` 里的 `[class*="footArea"]`
431
+ * 加 position:fixed + 高于覆盖层的层级;位置取我们左侧面板的矩形,
432
+ * 并用实色背景补齐(footer 本身是透明的),退出 IDE 时逐字节还原。
433
+ * 找不到 footArea 时退化为只停靠用量卡片([data-dsh-usage-foot-card])。
434
+ */
435
+ G.UsageDock = function UsageDock(props) {
436
+ const active = props.active !== false
437
+ useEffect(() => {
438
+ if (!active) return undefined
439
+ let disposed = false
440
+ const state = { el: null, saved: null }
441
+
442
+ /** 优先整块 sidebar footArea;退化到用量卡片。 */
443
+ const findTarget = () => {
444
+ const pane = document.querySelector('[data-pane="sidebar"], [class*="sidebarCol"]')
445
+ if (pane !== null && pane !== undefined) {
446
+ const foot = pane.querySelector('[class*="footArea"]')
447
+ if (foot !== null && foot !== undefined) return foot
448
+ }
449
+ return document.querySelector('[data-dsh-usage-foot-card]')
450
+ }
451
+
452
+ /** 取覆盖层实色底(左面板配色),让 footer 不透明、盖住底下列表。 */
453
+ const solidBackground = () => {
454
+ const overlayRoot = document.querySelector('.gufa-overlay-root')
455
+ if (overlayRoot !== null && overlayRoot !== undefined) {
456
+ const inline = overlayRoot.style.getPropertyValue('--gufa-solid-bg').trim()
457
+ if (/^(#|rgb|hsl|color)/i.test(inline)) return inline
458
+ }
459
+ try {
460
+ const bodyColor = window.getComputedStyle(document.body).backgroundColor
461
+ if (bodyColor !== '' && bodyColor !== 'rgba(0, 0, 0, 0)' && bodyColor !== 'transparent') return bodyColor
462
+ } catch {
463
+ /* ignore */
464
+ }
465
+ return ''
466
+ }
467
+
468
+ const restore = () => {
469
+ document.documentElement.removeAttribute('data-gufa-usage')
470
+ document.documentElement.style.removeProperty('--gufa-usage-height')
471
+ const el = state.el
472
+ const saved = state.saved
473
+ state.el = null
474
+ state.saved = null
475
+ if (el === null || saved === null) return
476
+ try {
477
+ if (saved.style === null) el.removeAttribute('style')
478
+ else el.setAttribute('style', saved.style)
479
+ el.removeAttribute('data-gufa-foot-dock')
480
+ } catch {
481
+ /* 元素可能已被插件移除 */
482
+ }
483
+ }
484
+
485
+ const apply = () => {
486
+ if (disposed) return
487
+ const el = findTarget()
488
+ if (el === null || el === undefined) {
489
+ if (state.el !== null) restore()
490
+ return
491
+ }
492
+ if (state.el !== el) {
493
+ restore()
494
+ state.el = el
495
+ state.saved = { style: el.getAttribute('style') }
496
+ }
497
+ const sidepanel = document.querySelector('.gufa-sidepanel')
498
+ const statusbar = document.querySelector('.gufa-statusbar')
499
+ if (sidepanel === null || sidepanel === undefined) return
500
+ const sideRect = sidepanel.getBoundingClientRect()
501
+ if (!(sideRect.width >= 80)) return
502
+ const statusHeight = statusbar === null || statusbar === undefined ? 24 : statusbar.getBoundingClientRect().height
503
+ const background = solidBackground()
504
+ try {
505
+ el.style.setProperty('position', 'fixed', 'important')
506
+ el.style.setProperty('left', `${Math.round(sideRect.left)}px`, 'important')
507
+ el.style.setProperty('width', `${Math.round(sideRect.width)}px`, 'important')
508
+ el.style.setProperty('bottom', `${Math.round(statusHeight)}px`, 'important')
509
+ el.style.setProperty('z-index', '2147483050', 'important')
510
+ el.style.setProperty('margin', '0', 'important')
511
+ el.style.setProperty('pointer-events', 'auto', 'important')
512
+ el.style.setProperty('box-sizing', 'border-box', 'important')
513
+ if (background !== '') el.style.setProperty('background', background, 'important')
514
+ el.setAttribute('data-gufa-foot-dock', '1')
515
+ document.documentElement.setAttribute('data-gufa-usage', '')
516
+ const height = Math.round(el.getBoundingClientRect().height)
517
+ document.documentElement.style.setProperty('--gufa-usage-height', `${height > 0 ? height + 6 : 0}px`)
518
+ } catch (error) {
519
+ console.error('[gufa-code] 停靠侧栏底部失败', error)
520
+ }
521
+ }
522
+
523
+ apply()
524
+ const timer = window.setInterval(apply, 500)
525
+ const onResize = () => apply()
526
+ window.addEventListener('resize', onResize)
527
+ return () => {
528
+ disposed = true
529
+ window.clearInterval(timer)
530
+ window.removeEventListener('resize', onResize)
531
+ restore()
532
+ }
533
+ }, [active])
534
+ return null
535
+ }
536
+
537
+ /**
538
+ * 覆盖层宿主:模式打开时渲染完整 IDE(GufaApp);关闭时不渲染任何东西,
539
+ * 停靠栏的清理 effect 会把官方会话节点还原。
540
+ *
541
+ * 皮肤(如 liang-intensity-skin)会把 `--dsw-alias-bg-*` 改成半透明/透明以露出壁纸,
542
+ * 直接使用会变成「透明覆盖层 + 下面原始界面」的双层穿帮。这里把皮肤 token 与页面底色
543
+ * 合成成一套不透明实色,挂到覆盖层根节点上(--gufa-solid-*),子组件全部使用这套实色。
544
+ */
545
+ G.OverlayHost = function OverlayHost(props) {
546
+ const visible = G.useMode()
547
+ const theme = G.useTheme()
548
+ const rootRef = useRef(null)
549
+
550
+ useLayoutEffect(() => {
551
+ if (!visible) return undefined
552
+ /* 进入 IDE:无论是否有弹层,都标记 html[data-gufa-ide],编辑器透明/皮肤背景适配都依赖它 */
553
+ try {
554
+ document.documentElement.setAttribute('data-gufa-ide', '')
555
+ } catch {
556
+ /* ignore */
557
+ }
558
+ let applying = false
559
+ const apply = () => {
560
+ if (applying) return
561
+ const root = rootRef.current
562
+ if (root === null) return
563
+ applying = true
564
+ try {
565
+ applyPalette(root)
566
+ } finally {
567
+ applying = false
568
+ }
569
+ }
570
+ const applyPalette = (root) => {
571
+ const palette = G.resolveSolidPalette(root)
572
+ root.style.backgroundColor = palette.bg
573
+ root.style.setProperty('--gufa-solid-bg', palette.bg)
574
+ root.style.setProperty('--gufa-solid-layer', palette.layer)
575
+ root.style.setProperty('--gufa-solid-layer2', palette.layer2)
576
+ root.style.setProperty('--gufa-solid-overlay', palette.overlay)
577
+ root.style.setProperty('--gufa-solid-sidebar', palette.sidebar)
578
+ root.style.setProperty('--gufa-solid-text', palette.text)
579
+ root.style.setProperty('--gufa-solid-text-dim', palette.textDim)
580
+ if (typeof G.applyPopupTheme === 'function') G.applyPopupTheme(palette)
581
+ try {
582
+ if (window.__gufaApplyLogs === undefined) window.__gufaApplyLogs = 10
583
+ if (window.__gufaApplyLogs > 0) {
584
+ window.__gufaApplyLogs -= 1
585
+ void G.api('dev.log', { message: `overlay-palette-apply bg=${String(palette.bg)} isDark=${palette.isDark === true} store=${G.paletteStore !== null && G.paletteStore !== undefined}` }).catch(() => {})
586
+ }
587
+ } catch {
588
+ /* ignore */
589
+ }
590
+ if (G.paletteStore !== null && G.paletteStore !== undefined) G.paletteStore.set(palette)
591
+ /* 皮肤背景层是否真的挂进了覆盖层根:是→隐藏我们自己的复刻背景层,否→保留(避免全黑) */
592
+ /* 一次性几何诊断:找出聊天面板/主行与窗口右边缘的差距 */
593
+ try {
594
+ if (window.__gufaGeomLogged !== true) {
595
+ window.__gufaGeomLogged = true
596
+ const rect = (sel) => {
597
+ const node = document.querySelector(sel)
598
+ if (node === null) return 'none'
599
+ const r = node.getBoundingClientRect()
600
+ return `${sel}=[${Math.round(r.left)},${Math.round(r.top)},${Math.round(r.width)}x${Math.round(r.height)}]`
601
+ }
602
+ void G.api('dev.log', {
603
+ message: `geom inner=${innerWidth}x${innerHeight} body=${document.body.clientWidth} ${rect('.gufa-overlay-root')} ${rect('.gufa-root')} ${rect('.gufa-shell')} ${rect('.gufa-main-row')} ${rect('.gufa-chatpane')} ${rect('.gufa-chatdock')} ${rect('.gufa-editor-area')}`,
604
+ }).catch(() => {})
605
+ }
606
+ } catch {
607
+ /* ignore */
608
+ }
609
+ try {
610
+ const skinRoot = document.querySelector('.gufa-overlay-root > .liang-skin-backdrop')
611
+ if (skinRoot !== null) document.documentElement.setAttribute('data-gufa-skin-live', '1')
612
+ else document.documentElement.removeAttribute('data-gufa-skin-live')
613
+ } catch {
614
+ /* ignore */
615
+ }
616
+ }
617
+ apply()
618
+ const timer = window.setInterval(apply, 800)
619
+ let observer = null
620
+ if (typeof MutationObserver === 'function') {
621
+ /* 只监听语义属性;绝对不能监听 body 的 style/class ——
622
+ 滑动变祖皮肤在动画帧里持续写 body.style,会把这里变成 60Hz 回调风暴(进 IDE 即卡死)。 */
623
+ observer = new MutationObserver(apply)
624
+ observer.observe(document.body, { attributes: true, attributeFilter: ['data-liang-skin', 'data-liang-stage', 'data-ds-dark-theme'] })
625
+ observer.observe(document.documentElement, { attributes: true, attributeFilter: ['data-ds-dark-theme', 'data-platform'] })
626
+ }
627
+ return () => {
628
+ window.clearInterval(timer)
629
+ observer?.disconnect()
630
+ try {
631
+ document.documentElement.removeAttribute('data-gufa-ide')
632
+ document.documentElement.removeAttribute('data-gufa-skin-live')
633
+ } catch {
634
+ /* ignore */
635
+ }
636
+ if (typeof G.restorePopupTheme === 'function') G.restorePopupTheme()
637
+ }
638
+ }, [visible, theme])
639
+
640
+ if (!visible) return null
641
+ return h('div', { className: 'gufa-overlay-root', ref: rootRef }, h(G.GufaErrorBoundary, null, h(G.GufaApp, props)))
642
+ }
643
+
644
+ /**
645
+ * main key 安全网:若侧边栏仍然把主面板切到了 `gufa-code`(例如我们的点击拦截失效),
646
+ * 立刻进入覆盖层并切回会话主视图,避免官方会话被卸载。
647
+ */
648
+ G.MainRedirect = function MainRedirect() {
649
+ useEffect(() => {
650
+ G.enterGufa()
651
+ const layout = G.ctx === null || G.ctx === undefined ? undefined : G.ctx.get('layout')
652
+ const timer = window.setTimeout(() => {
653
+ try {
654
+ layout?.selectPanel?.(null)
655
+ } catch (error) {
656
+ console.error('[gufa-code] 还原会话主视图失败', error)
657
+ }
658
+ }, 0)
659
+ return () => window.clearTimeout(timer)
660
+ }, [])
661
+ return null
662
+ }