dsh-code-server-app 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.js ADDED
@@ -0,0 +1,945 @@
1
+ // dsh-code-server — client bundle(侧栏按钮 + 内部浮动窗口 iframe)
2
+ // 格式:window.__ModuleLoader__.load({ id, factory });factory(require) 的
3
+ // require 解析浏览器端冻结模块表(react 等官方共享模块)。
4
+ //
5
+ // 数据通道:同源 fetch DSH webServer 上的 /code-server JSON API
6
+ // GET /code-server/status → { ok, running, status, port, host, pid, cwd, url, version, error, logTail, adopted }
7
+ // POST /code-server/start → { cwd? } → status
8
+ // POST /code-server/stop → status
9
+ //
10
+ // UI 结构(参照 dsh-univer-office 的 WorktreeWindow):
11
+ // - sidebar.footer.action(id code-server-panel):"Code Server"按钮;点击打开窗口
12
+ // - shell.overlay(id code-server):内部浮动窗口——固定定位 + 空转根容器,
13
+ // 窗口自身接管 pointer-events,可拖动/8 向缩放/最大化/折叠/关闭
14
+ //
15
+ // 窗口几何与手势完全复刻 univer-office 模式:
16
+ // - 根容器 position:fixed inset:0 pointer-events:none(点击穿透到底层)
17
+ // - 窗口 pointer-events:auto,标题栏 pointerdown 拖动(setPointerCapture),
18
+ // 双击最大化,8 个 resize handle 缩放,min 尺寸约束,viewport 边缘 clamp。
19
+ window.__ModuleLoader__.load({
20
+ id: 'dsh-code-server-app',
21
+ factory: (require) => {
22
+ var module = { exports: {} }
23
+ var exports = module.exports
24
+ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' })
25
+ let React = require('react')
26
+ // ReactDOM 为必需:窗口经 createPortal 挂到 document.body(跨越 shell.overlay 的
27
+ // stacking context)。react-dom 在冻结模块表内(官方 feedback bundle 同款),
28
+ // 缺失时在此显式失败(而非静默降级导致窗口被输入框遮挡)。
29
+ let ReactDOM = require('react-dom')
30
+
31
+ // ---------- 模块级共享 store(两个 occupant 之间同步 open/status) ----------
32
+ var listeners = new Set()
33
+ var state = { open: false, status: null, busy: false }
34
+ function setState(patch) {
35
+ state = Object.assign({}, state, patch)
36
+ listeners.forEach(function (fn) { fn() })
37
+ }
38
+ function subscribe(fn) {
39
+ listeners.add(fn)
40
+ return function () { listeners.delete(fn) }
41
+ }
42
+ function getState() { return state }
43
+ function useStore() {
44
+ return React.useSyncExternalStore(subscribe, getState)
45
+ }
46
+
47
+ // ---------- /code-server API ----------
48
+ async function api(path, body) {
49
+ var options = { method: body === undefined ? 'GET' : 'POST', headers: {} }
50
+ if (body !== undefined) {
51
+ options.headers['content-type'] = 'application/json'
52
+ options.body = JSON.stringify(body)
53
+ }
54
+ var res = await fetch(path, options)
55
+ var text = await res.text()
56
+ var data = null
57
+ try { data = text === '' ? null : JSON.parse(text) } catch (e) { data = null }
58
+ if (!res.ok) return { ok: false, error: 'HTTP ' + res.status + (data !== null && data.error !== undefined ? ': ' + data.error : '') }
59
+ return data !== null ? data : { ok: false, error: 'invalid JSON response' }
60
+ }
61
+
62
+ // 解析当前活动工作区目录,优先级:
63
+ // 1. 当前会话的 cwd(useSessions.byId[current].cwd)
64
+ // 2. 当前会话所属 workspace 的 path(workspace.sessionIds 含 current)
65
+ // 3. recentWorkspaceId 对应 workspace 的 path
66
+ // 4. 第一个 workspace 的 path
67
+ // 均缺失时返回 undefined(调用方不传 cwd,由 host 保留当前目录)。
68
+ function activeWorkspaceCwd(useSessions, useWorkspaces) {
69
+ try {
70
+ var list = useSessions(function (s) { return s })
71
+ var wsList = useWorkspaces === null ? null : (typeof useWorkspaces === 'function' ? useWorkspaces(function (s) { return s }) : null)
72
+ var current = list != null ? list.current : undefined
73
+ if (current !== undefined && list != null && list.byId != null) {
74
+ var cur = list.byId[current]
75
+ if (cur != null && typeof cur.cwd === 'string' && cur.cwd !== '') return cur.cwd
76
+ }
77
+ var items = wsList != null && Array.isArray(wsList.items) ? wsList.items : []
78
+ if (current !== undefined) {
79
+ for (var i = 0; i < items.length; i++) {
80
+ var w = items[i]
81
+ if (w.sessionIds != null && w.sessionIds.indexOf(current) !== -1 && typeof w.path === 'string' && w.path !== '') return w.path
82
+ }
83
+ }
84
+ var recentId = wsList != null ? wsList.recentWorkspaceId : undefined
85
+ if (recentId !== undefined) {
86
+ for (var j = 0; j < items.length; j++) {
87
+ if (items[j].workspaceId === recentId && typeof items[j].path === 'string' && items[j].path !== '') return items[j].path
88
+ }
89
+ }
90
+ if (items.length > 0 && typeof items[0].path === 'string' && items[0].path !== '') return items[0].path
91
+ } catch (e) { /* props 未提供时静默 */ }
92
+ return undefined
93
+ }
94
+
95
+ // ---------- 窗口几何(复刻 univer-office 的 fit/move/resize) ----------
96
+ // 底部预留输入栏高度(可在设置卡片切换):true 时窗口初始/缩放/最大化
97
+ // 都止于输入框上方,不遮挡 composer;false 时允许盖住输入框(最大化到视口底)。
98
+ var RESIZE_DIRECTIONS = ['nw', 'n', 'ne', 'w', 'e', 'sw', 's', 'se']
99
+ var VIEWPORT_GUTTER = 12
100
+ var DEFAULT_WIDTH = 720
101
+ var DEFAULT_HEIGHT = 520
102
+ var MIN_WIDTH = 360
103
+ var MIN_HEIGHT = 260
104
+ var CASCADE_OFFSET = 24
105
+ var COMPOSER_RESERVE_DEFAULT = 148 // 输入框 + 下发 dock 的近似高度(测量失败时的回退)
106
+
107
+ /** 输入区预留:flag=false(用户允许)不保留 → 0。
108
+ * flag=true(默认)时:若 composer 贴住视口底(会话页/对话中),让最大化窗口
109
+ * 底边贴到 composer 输入卡顶部——窗口压住消息区,下面只露出输入框;
110
+ * 若 composer 不在底部(hero 页/新会话描述画面),输入框在页面中间,
111
+ * 最大化直接全屏(0),避免在输入框下方露出页面背景渐变。 */
112
+ function reserveOf(flag) {
113
+ if (flag === false) return 0
114
+ try {
115
+ if (typeof document === 'undefined') return 0
116
+ var seats = document.querySelectorAll('[data-composer-seat]')
117
+ if (seats === null || seats.length === 0) return 0 // 无 composer:全屏
118
+ var top = Number.POSITIVE_INFINITY
119
+ var bottom = 0
120
+ for (var i = 0; i < seats.length; i++) {
121
+ var rect = seats[i].getBoundingClientRect()
122
+ if (rect === null || rect.width <= 0) continue
123
+ if (rect.top < top) top = rect.top
124
+ if (rect.bottom > bottom) bottom = rect.bottom
125
+ }
126
+ if (!isFinite(top) || bottom <= 0) return 0
127
+ var viewportBottom = window.innerHeight
128
+ // composer 未贴底(hero 页,seat 底沿离视口底 > 24px)→ 全屏
129
+ if (viewportBottom - bottom > 24) return 0
130
+ // 会话页:窗口底边 = 输入卡顶部,下面只留输入框(+ 8px 呼吸边距)
131
+ return Math.min(Math.max(1, viewportBottom - top + 8), 420)
132
+ } catch (e) {
133
+ return 0
134
+ }
135
+ }
136
+ function viewportSize() {
137
+ return { width: Math.max(1, window.innerWidth), height: Math.max(1, window.innerHeight) }
138
+ }
139
+ /** 窗口可用的最大高度:视口高 - 上下 gutter - 输入栏预留(reserve 设为 0 时不预留)。 */
140
+ function usableViewport(viewport, reserve) {
141
+ return {
142
+ width: Math.max(1, viewport.width - VIEWPORT_GUTTER * 2),
143
+ height: Math.max(1, viewport.height - VIEWPORT_GUTTER * 2 - reserve),
144
+ }
145
+ }
146
+ function clamp(value, min, max) {
147
+ return Math.min(max, Math.max(min, value))
148
+ }
149
+ function fitRect(rect, viewport, reserve) {
150
+ const available = usableViewport(viewport, reserve)
151
+ const width = clamp(rect.width, Math.min(MIN_WIDTH, available.width), available.width)
152
+ const height = clamp(rect.height, Math.min(MIN_HEIGHT, available.height), available.height)
153
+ return {
154
+ x: clamp(rect.x, VIEWPORT_GUTTER, Math.max(VIEWPORT_GUTTER, viewport.width - VIEWPORT_GUTTER - width)),
155
+ y: clamp(rect.y, VIEWPORT_GUTTER, Math.max(VIEWPORT_GUTTER, viewport.height - VIEWPORT_GUTTER - reserve - height)),
156
+ width, height,
157
+ }
158
+ }
159
+ function initialRect(stackIndex, viewport, reserve) {
160
+ const available = usableViewport(viewport, reserve)
161
+ const width = Math.min(DEFAULT_WIDTH, available.width)
162
+ const height = Math.min(DEFAULT_HEIGHT, available.height)
163
+ // 初始位置:输入框上方、靠右(右下偏上),级联偏移。
164
+ // reserve=0(允许盖住)时初始仍在输入框上方(有效下沿 = composer 顶部,
165
+ // 若 composer 贴底则留输入框高度,避免开局就压住输入框);用户想盖住
166
+ // 输入框时点"最大化"(或拖到底部)即可,最大化才允许占满视口底。
167
+ const composerTop = composerTopOf()
168
+ const bottomLimit = composerTop > 0
169
+ ? composerTop - 8
170
+ : viewport.height - VIEWPORT_GUTTER - reserve
171
+ return fitRect({
172
+ x: viewport.width - VIEWPORT_GUTTER - width - stackIndex * CASCADE_OFFSET,
173
+ y: Math.max(VIEWPORT_GUTTER, bottomLimit - height - stackIndex * CASCADE_OFFSET),
174
+ width, height,
175
+ }, viewport, reserve)
176
+ }
177
+ /** composer 输入区顶部(视口坐标);无 composer 时返回 -1。 */
178
+ function composerTopOf() {
179
+ try {
180
+ if (typeof document === 'undefined') return -1
181
+ var seats = document.querySelectorAll('[data-composer-seat]')
182
+ if (seats === null || seats.length === 0) return -1
183
+ var top = Number.POSITIVE_INFINITY
184
+ for (var i = 0; i < seats.length; i++) {
185
+ var rect = seats[i].getBoundingClientRect()
186
+ if (rect !== null && rect.width > 0 && rect.top < top) top = rect.top
187
+ }
188
+ return isFinite(top) ? top : -1
189
+ } catch (e) {
190
+ return -1
191
+ }
192
+ }
193
+ function moveRect(start, dx, dy, viewport, reserve) {
194
+ return fitRect({ ...start, x: start.x + dx, y: start.y + dy }, viewport, reserve)
195
+ }
196
+ function resizeRect(start, direction, dx, dy, viewport, reserve) {
197
+ const fitted = fitRect(start, viewport, reserve)
198
+ const available = usableViewport(viewport, reserve)
199
+ const minWidth = Math.min(MIN_WIDTH, available.width)
200
+ const minHeight = Math.min(MIN_HEIGHT, available.height)
201
+ const bottomLimit = viewport.height - VIEWPORT_GUTTER - reserve
202
+ let left = fitted.x
203
+ let right = fitted.x + fitted.width
204
+ let top = fitted.y
205
+ let bottom = fitted.y + fitted.height
206
+ if (direction.includes('w')) left = clamp(fitted.x + dx, VIEWPORT_GUTTER, right - minWidth)
207
+ if (direction.includes('e')) right = clamp(right + dx, left + minWidth, viewport.width - VIEWPORT_GUTTER)
208
+ if (direction.includes('n')) top = clamp(fitted.y + dy, VIEWPORT_GUTTER, bottom - minHeight)
209
+ if (direction.includes('s')) bottom = clamp(bottom + dy, top + minHeight, bottomLimit)
210
+ return { x: left, y: top, width: right - left, height: bottom - top }
211
+ }
212
+ /** 最大化矩形:占满"输入栏上方"区域(reserve=0 时到视口底)。 */
213
+ function maximizedRect(viewport, reserve) {
214
+ const available = usableViewport(viewport, reserve)
215
+ return {
216
+ x: VIEWPORT_GUTTER,
217
+ y: VIEWPORT_GUTTER,
218
+ width: available.width,
219
+ height: available.height,
220
+ }
221
+ }
222
+
223
+ // ---------- 样式(主题变量 + 兜底值;仅覆盖窗口自身结构) ----------
224
+ var CSS =
225
+ '.dshcs-root{position:fixed;inset:0;z-index:2147483000;pointer-events:none}' +
226
+ '.dshcs-win{--dshcs-accent:#5b6cff;pointer-events:auto;position:fixed;isolation:isolate;z-index:2147483000;display:flex;flex-direction:column;min-width:1px;min-height:1px;overflow:hidden;color:var(--dsw-alias-label-primary,#172033);background:var(--dsw-alias-bg-base,#fff);border:1px solid var(--dsw-alias-border-l2,#dfe3eb);border-radius:16px;box-shadow:0 1px 2px rgba(13,22,38,.08),0 18px 48px rgba(13,22,38,.2),0 0 0 1px rgba(255,255,255,.4) inset;transition:border-color .16s ease,box-shadow .16s ease}' +
227
+ '.dshcs-win:hover{border-color:color-mix(in srgb,var(--dshcs-accent) 28%,var(--dsw-alias-border-l2,#dfe3eb));box-shadow:0 2px 5px rgba(13,22,38,.1),0 22px 58px rgba(13,22,38,.24),0 0 0 1px rgba(255,255,255,.5) inset}' +
228
+ '.dshcs-win[data-interaction]{transition:none;user-select:none}' +
229
+ '.dshcs-win[data-interaction]::after{content:"";position:absolute;inset:0;z-index:18;background:transparent}' +
230
+ '.dshcs-win-folded{height:48px!important}' +
231
+ '.dshcs-win-max{border-radius:18px;z-index:2147483001;box-shadow:0 1px 2px rgba(13,22,38,.06),0 8px 24px rgba(13,22,38,.12)}' +
232
+ '.dshcs-win-max:hover{box-shadow:0 1px 2px rgba(13,22,38,.06),0 10px 28px rgba(13,22,38,.14)}' +
233
+ '.dshcs-header{position:relative;z-index:10;display:flex;align-items:center;gap:10px;height:48px;padding:0 8px 0 10px;flex:none;cursor:grab;touch-action:none;user-select:none;background:linear-gradient(180deg,color-mix(in srgb,var(--dsw-alias-bg-base,#fff) 96%,#7583ff),color-mix(in srgb,var(--dsw-alias-bg-base,#fff) 92%,#dfe3f5));border-bottom:1px solid color-mix(in srgb,var(--dsw-alias-border-l2,#dfe3eb) 78%,transparent)}' +
234
+ '.dshcs-win[data-interaction=move] .dshcs-header{cursor:grabbing}.dshcs-win-max .dshcs-header{cursor:default}.dshcs-win-folded .dshcs-header{border-bottom:0}' +
235
+ '.dshcs-glyph{display:grid;place-items:center;width:22px;height:22px;flex:none;border-radius:6px;color:#fff;background:transparent}' +
236
+ '.dshcs-identity{display:flex;flex-direction:column;justify-content:center;min-width:0;flex:1;line-height:1.2}' +
237
+ '.dshcs-title,.dshcs-sub{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.dshcs-title{font-size:12.5px;font-weight:650;letter-spacing:.01em;color:var(--dsw-alias-label-primary,#172033)}.dshcs-sub{margin-top:2px;font-size:10.5px;color:var(--dsw-alias-label-tertiary,#7d8798)}' +
238
+ '.dshcs-chip{display:inline-flex;align-items:center;gap:6px;flex:none;padding:3px 8px;border:1px solid var(--dsw-alias-border-l2,#dfe3eb);border-radius:999px;font-size:10.5px;font-weight:600;line-height:16px;color:var(--dsw-alias-label-secondary,#566174);background:color-mix(in srgb,var(--dsw-alias-bg-base,#fff) 75%,transparent)}' +
239
+ '.dshcs-chip[data-status=running]{color:#16784d;border-color:color-mix(in srgb,#20a66a 30%,var(--dsw-alias-border-l2,#dfe3eb));background:color-mix(in srgb,#20a66a 8%,var(--dsw-alias-bg-base,#fff))}' +
240
+ '.dshcs-chip[data-status=starting]{color:#9a6114;border-color:color-mix(in srgb,#d78b25 34%,var(--dsw-alias-border-l2,#dfe3eb));background:color-mix(in srgb,#d78b25 9%,var(--dsw-alias-bg-base,#fff))}' +
241
+ '.dshcs-chip[data-status=error]{color:#b42318;border-color:color-mix(in srgb,#f85149 30%,var(--dsw-alias-border-l2,#dfe3eb));background:color-mix(in srgb,#f85149 8%,var(--dsw-alias-bg-base,#fff))}' +
242
+ '.dshcs-dot,.dshcs-pulse{position:relative;width:7px;height:7px;border-radius:50%;background:#20a66a;flex:none}.dshcs-pulse::after{content:"";position:absolute;inset:-3px;border-radius:50%;border:1px solid currentColor;opacity:.24;animation:dshcs-pulse 2s ease-out infinite}' +
243
+ '.dshcs-chip[data-status=starting] .dshcs-dot{background:#d78b25}.dshcs-chip[data-status=starting] .dshcs-dot::after{content:"";position:absolute;inset:-3px;border-radius:50%;border:1px solid currentColor;opacity:.24;animation:dshcs-pulse 2s ease-out infinite}' +
244
+ '.dshcs-chip[data-status=error] .dshcs-dot{background:#f85149}.dshcs-chip[data-status=idle] .dshcs-dot{background:#7d8798}' +
245
+ '@keyframes dshcs-pulse{0%{transform:scale(.7);opacity:.35}70%,100%{transform:scale(1.65);opacity:0}}' +
246
+ '.dshcs-controls{display:flex;align-items:center;gap:3px;flex:none}' +
247
+ '.dshcs-ctl{display:grid;place-items:center;width:28px;height:28px;padding:0;border:0;border-radius:8px;color:var(--dsw-alias-label-secondary,#566174);background:transparent;cursor:pointer;transition:color .12s ease,background .12s ease,transform .12s ease}' +
248
+ '.dshcs-ctl:hover{color:var(--dsw-alias-label-primary,#172033);background:color-mix(in srgb,var(--dsw-alias-label-primary,#172033) 8%,transparent)}' +
249
+ '.dshcs-ctl:active{transform:scale(.92)}.dshcs-ctl:focus-visible{outline:2px solid color-mix(in srgb,var(--dshcs-accent) 65%,transparent);outline-offset:1px}' +
250
+ '.dshcs-ctl[data-danger]:hover{color:#d94242;background:rgba(217,66,66,.1)}' +
251
+ '.dshcs-ctl svg{width:15px;height:15px;fill:none;stroke:currentColor;stroke-width:1.4;stroke-linecap:round;stroke-linejoin:round}' +
252
+ '.dshcs-body{display:flex;flex:1;min-width:0;min-height:0;flex-direction:column;background:var(--dsw-alias-bg-base,#fff)}.dshcs-body[hidden]{display:none}' +
253
+ '.dshcs-frame{display:block;flex:1;min-width:0;min-height:0;width:100%;height:100%;border:0;background:var(--dsw-alias-bg-base,#fff)}' +
254
+ '.dshcs-empty{flex:1;display:flex;align-items:center;justify-content:center;padding:24px}' +
255
+ '.dshcs-emptybox{max-width:560px;text-align:left;background:var(--dsw-alias-bg-layer-2,#f7f8fb);border:1px solid var(--dsw-alias-border-l2,#dfe3eb);border-radius:12px;padding:16px 18px}' +
256
+ '.dshcs-error{color:var(--dsw-alias-label-error,#d92d20);white-space:pre-wrap;word-break:break-all;font-family:ui-monospace,Consolas,monospace;font-size:12px;margin:8px 0 0}' +
257
+ '.dshcs-hint{color:var(--dsw-alias-label-tertiary,#7d8798);font-size:12px;margin:6px 0 0}' +
258
+ '.dshcs-resize{position:absolute;z-index:20;display:block;touch-action:none}' +
259
+ '.dshcs-resize-n,.dshcs-resize-s{left:16px;right:16px;height:10px;cursor:ns-resize}.dshcs-resize-n{top:0}.dshcs-resize-s{bottom:0}' +
260
+ '.dshcs-resize-w,.dshcs-resize-e{top:16px;bottom:16px;width:10px;cursor:ew-resize}.dshcs-resize-w{left:0}.dshcs-resize-e{right:0}' +
261
+ '.dshcs-resize-nw,.dshcs-resize-ne,.dshcs-resize-sw,.dshcs-resize-se{width:18px;height:18px}' +
262
+ '.dshcs-resize-nw{left:0;top:0;cursor:nwse-resize}.dshcs-resize-ne{right:0;top:0;cursor:nesw-resize}.dshcs-resize-sw{left:0;bottom:0;cursor:nesw-resize}.dshcs-resize-se{right:0;bottom:0;cursor:nwse-resize}' +
263
+ '.dshcs-resize-se::after{content:"";position:absolute;right:4px;bottom:4px;width:7px;height:7px;border-right:1.5px solid color-mix(in srgb,var(--dsw-alias-label-tertiary,#7d8798) 58%,transparent);border-bottom:1.5px solid color-mix(in srgb,var(--dsw-alias-label-tertiary,#7d8798) 58%,transparent);border-radius:0 0 2px}'
264
+ var CSS_TAG = 'dsh-code-server/styles'
265
+ if (typeof document !== 'undefined' && document.querySelector('style[data-dshcs=' + JSON.stringify(CSS_TAG) + ']') === null) {
266
+ var tag = document.createElement('style')
267
+ tag.setAttribute('data-dshcs', CSS_TAG)
268
+ tag.textContent = CSS
269
+ document.head.appendChild(tag)
270
+ }
271
+
272
+ // ---------- 小图标 ----------
273
+ // code-server 官方徽标:经 _static 路径从运行中的服务取(favicon-dark-support.svg
274
+ // 为官方 SVG,支持深色模式);运行 URL 为空时回退自绘格子图标。
275
+ function Glyph(props) {
276
+ var baseUrl = props && props.baseUrl
277
+ var official = typeof baseUrl === 'string' && baseUrl !== ''
278
+ ? baseUrl + '_static/src/browser/media/favicon-dark-support.svg'
279
+ : null
280
+ if (official !== null) {
281
+ return React.createElement('img', {
282
+ src: official, alt: '', 'aria-hidden': true,
283
+ style: { width: 18, height: 18, display: 'block', objectFit: 'contain' },
284
+ })
285
+ }
286
+ // 无运行时 URL(未运行):自绘格子徽标
287
+ return React.createElement('svg', { viewBox: '0 0 18 18', 'aria-hidden': true },
288
+ React.createElement('rect', { x: '3', y: '3', width: '12', height: '12', rx: '2' }),
289
+ React.createElement('path', { d: 'M3 7h12M7 3v12' })
290
+ )
291
+ }
292
+ function FoldIcon(props) {
293
+ return React.createElement('svg', { viewBox: '0 0 16 16', 'aria-hidden': true },
294
+ props.expanded
295
+ ? React.createElement('path', { d: 'm4 10 4-4 4 4' })
296
+ : React.createElement('path', { d: 'M4 9h8' })
297
+ )
298
+ }
299
+ function MaximizeIcon(props) {
300
+ return React.createElement('svg', { viewBox: '0 0 16 16', 'aria-hidden': true },
301
+ props.restored
302
+ ? React.createElement(React.Fragment, null,
303
+ React.createElement('rect', { x: '3', y: '5', width: '8', height: '8', rx: '1' }),
304
+ React.createElement('path', { d: 'M5 5V3h8v8h-2' })
305
+ )
306
+ : React.createElement('rect', { x: '3', y: '3', width: '10', height: '10', rx: '1.5' })
307
+ )
308
+ }
309
+ function CloseIcon() {
310
+ return React.createElement('svg', { viewBox: '0 0 16 16', 'aria-hidden': true },
311
+ React.createElement('path', { d: 'm4 4 8 8m0-8-8 8' })
312
+ )
313
+ }
314
+
315
+ // ---------- 侧栏按钮 ----------
316
+ // 环境检测不通过时不显示(检测未返回或 ok=false 均隐藏);
317
+ // 首次状态由 apply 里的预拉取填充,避免按钮闪烁。
318
+ function SidebarAction(props) {
319
+ var store = useStore()
320
+ var env = store.status != null ? store.status.env : null
321
+ if (env == null || env.ok !== true) return null
322
+ function openPanel() {
323
+ setState({ open: true })
324
+ }
325
+ var label = store.open ? 'Code Server…' : 'Code Server'
326
+ return React.createElement(
327
+ 'button',
328
+ { type: 'button', onClick: openPanel, title: '在 DSH 内打开 code-server(VS Code 网页版)', style: { display: 'flex', alignItems: 'center', gap: 8 } },
329
+ React.createElement('span', { 'aria-hidden': true, style: { fontSize: 14 } }, '⌘'),
330
+ props && props.wide === true ? React.createElement('span', null, label) : null
331
+ )
332
+ }
333
+
334
+ // ---------- 内部浮动窗口(参照 univer-office WorktreeWindow) ----------
335
+ function Window(props) {
336
+ var store = useStore()
337
+ var status = store.status
338
+ var running = status != null && status.ok === true && status.running === true
339
+ var starting = status != null && status.status === 'starting'
340
+ var errored = status != null && status.ok === false
341
+ var cwd = activeWorkspaceCwd(props && props.useSessions, props && props.useWorkspaces)
342
+ // 上次同步(或打开服务)时的工作区;变化时自动重启并刷新 iframe
343
+ var lastSyncedCwdRef = React.useRef(undefined)
344
+ var [reloadTick, setReloadTick] = React.useState(0)
345
+
346
+ // ---- 窗口状态与手势(复刻 univer-office 的 WorktreeWindow) ----
347
+ // reserveComposer(设置卡片):true 不盖输入框;false 允许盖住(最大化到视口底)。
348
+ var reserve = reserveOf(status != null ? status.reserveComposer : true)
349
+ var [folded, setFolded] = React.useState(false)
350
+ var [maximized, setMaximized] = React.useState(false)
351
+ var [interaction, setInteraction] = React.useState(null)
352
+ var [rect, setRect] = React.useState(function () { return initialRect(0, viewportSize(), reserve) })
353
+ var rectRef = React.useRef(rect)
354
+ var cancelPointerSessionRef = React.useRef(function () {})
355
+ rectRef.current = rect
356
+ React.useEffect(function () {
357
+ var onViewportResize = function () {
358
+ setRect(function (current) {
359
+ // 最大化时保持占满"输入栏上方"区域;否则仅 fit 防止越界
360
+ return maximized ? maximizedRect(viewportSize(), reserve) : fitRect(current, viewportSize(), reserve)
361
+ })
362
+ }
363
+ window.addEventListener('resize', onViewportResize)
364
+ return function () { window.removeEventListener('resize', onViewportResize) }
365
+ }, [maximized, reserve])
366
+ // 设置变化(reserve 切换)时重新 fit 当前 rect(立即生效,不遮挡/不越界)
367
+ React.useEffect(function () {
368
+ setRect(function (current) { return fitRect(current, viewportSize(), reserve) })
369
+ }, [reserve])
370
+ React.useEffect(function () {
371
+ return function () { cancelPointerSessionRef.current() }
372
+ }, [])
373
+
374
+ var beginPointerSession = function (event, kind) {
375
+ if (event.button !== 0 || maximized) return
376
+ event.preventDefault()
377
+ event.stopPropagation()
378
+ cancelPointerSessionRef.current()
379
+ var view = event.currentTarget.ownerDocument.defaultView
380
+ if (view === null) return
381
+ var pointerId = event.pointerId
382
+ var origin = { x: event.clientX, y: event.clientY }
383
+ var start = rectRef.current
384
+ var element = event.currentTarget
385
+ setInteraction(kind)
386
+ try { element.setPointerCapture(pointerId) } catch (e) { /* capture 失败仍可跟随 */ }
387
+ var move = function (next) {
388
+ if (next.pointerId !== pointerId) return
389
+ var dx = next.clientX - origin.x
390
+ var dy = next.clientY - origin.y
391
+ setRect(kind === 'move' ? moveRect(start, dx, dy, viewportSize(), reserve) : resizeRect(start, kind, dx, dy, viewportSize(), reserve))
392
+ }
393
+ var cleanup = function () {
394
+ view.removeEventListener('pointermove', move)
395
+ view.removeEventListener('pointerup', finish)
396
+ view.removeEventListener('pointercancel', finish)
397
+ cancelPointerSessionRef.current = function () {}
398
+ try { element.releasePointerCapture(pointerId) } catch (e) { /* ignore */ }
399
+ }
400
+ var finish = function (next) {
401
+ if (next.pointerId !== pointerId) return
402
+ cleanup()
403
+ setInteraction(null)
404
+ }
405
+ cancelPointerSessionRef.current = cleanup
406
+ view.addEventListener('pointermove', move)
407
+ view.addEventListener('pointerup', finish)
408
+ view.addEventListener('pointercancel', finish)
409
+ }
410
+
411
+ var toggleFolded = function () {
412
+ setMaximized(false)
413
+ setFolded(function (current) { return !current })
414
+ }
415
+ var toggleMaximized = function () {
416
+ setFolded(false)
417
+ setMaximized(function (current) { return !current })
418
+ }
419
+ var onHeaderPointerDown = function (event) {
420
+ if (event.target.closest('[data-window-control]') !== null) return
421
+ beginPointerSession(event, 'move')
422
+ }
423
+ var onHeaderDoubleClick = function (event) {
424
+ if (event.target.closest('[data-window-control]') === null) toggleMaximized()
425
+ }
426
+
427
+ // ---- 生命周期/数据同步(与原全屏浮层一致) ----
428
+ // 打开时:拉状态;未运行则尝试启动(带当前会话 cwd)
429
+ React.useEffect(function () {
430
+ if (!store.open) return
431
+ var cancelled = false
432
+ async function boot() {
433
+ setState({ busy: true })
434
+ var s = await api('/code-server/status')
435
+ if (cancelled) return
436
+ if (s != null && s.ok === true && s.running === true) {
437
+ setState({ status: s, busy: false })
438
+ return
439
+ }
440
+ var started = await api('/code-server/start', typeof cwd === 'string' ? { cwd: cwd } : {})
441
+ if (cancelled) return
442
+ setState({ status: started, busy: false })
443
+ }
444
+ boot()
445
+ return function () { cancelled = true }
446
+ // eslint-disable-next-line react-hooks/exhaustive-deps
447
+ }, [store.open])
448
+
449
+ // 跟随活动工作区:快照变化(切换会话/工作区)自动重启到新目录,成功后刷新 iframe
450
+ React.useEffect(function () {
451
+ if (!store.open) return
452
+ if (typeof cwd !== 'string' || cwd === '') return
453
+ if (lastSyncedCwdRef.current === cwd) return
454
+ var changed = lastSyncedCwdRef.current !== undefined && lastSyncedCwdRef.current !== cwd
455
+ lastSyncedCwdRef.current = cwd
456
+ var cancelled = false
457
+ async function sync() {
458
+ setState({ busy: true })
459
+ var s = await api('/code-server/start', { cwd: cwd })
460
+ if (cancelled) return
461
+ setState({ status: s, busy: false })
462
+ if (changed && s != null && s.ok === true && s.running === true) {
463
+ setReloadTick(function (t) { return t + 1 })
464
+ }
465
+ }
466
+ sync()
467
+ return function () { cancelled = true }
468
+ }, [store.open, cwd])
469
+
470
+ // 窗口关闭时重置跟随基线,下次打开重新对齐
471
+ React.useEffect(function () {
472
+ if (!store.open) lastSyncedCwdRef.current = undefined
473
+ }, [store.open])
474
+
475
+ // 打开期间每 3s 轮询状态
476
+ React.useEffect(function () {
477
+ if (!store.open) return
478
+ var timer = window.setInterval(function () {
479
+ api('/code-server/status').then(function (s) { setState({ status: s }) })
480
+ }, 3000)
481
+ return function () { window.clearInterval(timer) }
482
+ }, [store.open])
483
+
484
+ // Esc 关闭
485
+ React.useEffect(function () {
486
+ if (!store.open) return
487
+ function onKey(e) {
488
+ if (e.key === 'Escape') setState({ open: false })
489
+ }
490
+ window.addEventListener('keydown', onKey)
491
+ return function () { window.removeEventListener('keydown', onKey) }
492
+ }, [store.open])
493
+
494
+ if (!store.open) return null
495
+
496
+ var statusKind = running ? 'running' : starting ? 'starting' : errored ? 'error' : 'idle'
497
+ var statusLabel = running ? '运行中' : starting ? '启动中…' : errored ? '错误' : '未运行'
498
+
499
+ var baseUrl = status != null && typeof status.url === 'string' ? status.url : null
500
+ // code-server 前端记住"最近工作区"并自行恢复;必须用 ?folder= 显式指定
501
+ // 打开目录,否则 iframe 裸根 URL 会显示上一次的工具区(不跟随切换)。
502
+ //
503
+ // folder 参数格式(实测):VS Code web 要求以 "/" 开头的平台路径——Windows
504
+ // 盘符路径必须统一为正斜杠并加前导 "/"(/C:/Users/...)。裸 Windows 路径
505
+ // (C:\Users\...) 会被前端当 URI scheme 解析而剥掉盘符,file:/// 形式则报
506
+ // "Workspace does not exist"。
507
+ var activeDir = running && status != null && typeof status.cwd === 'string' && status.cwd !== '' ? status.cwd : null
508
+ var folderParam = null
509
+ if (activeDir !== null) {
510
+ var normalized = activeDir.replace(/\\/g, '/')
511
+ folderParam = normalized
512
+ if (/^[A-Za-z]:\//.test(normalized)) folderParam = '/' + normalized
513
+ else if (normalized.charCodeAt(0) !== 47) folderParam = '/' + normalized
514
+ }
515
+ var pageUrl = baseUrl !== null
516
+ ? baseUrl + (folderParam !== null ? '?folder=' + encodeURIComponent(folderParam) : '')
517
+ : null
518
+
519
+ var metaBits = []
520
+ if (status != null) {
521
+ if (status.host != null) metaBits.push(String(status.host) + ':' + String(status.port))
522
+ if (status.cwd != null) metaBits.push('cwd: ' + status.cwd)
523
+ if (status.adopted === true) metaBits.push('接管既有实例')
524
+ }
525
+ if (typeof cwd === 'string' && cwd !== '') {
526
+ metaBits.push(running && status != null && typeof status.cwd === 'string' && status.cwd !== cwd
527
+ ? '目标: ' + cwd + '(切换即重启)'
528
+ : '跟随: ' + cwd)
529
+ }
530
+
531
+ var body
532
+ if (running && pageUrl !== null) {
533
+ body = React.createElement('iframe', {
534
+ key: reloadTick,
535
+ className: 'dshcs-frame',
536
+ src: pageUrl,
537
+ title: 'code-server',
538
+ sandbox: 'allow-scripts allow-same-origin allow-forms allow-modals allow-popups allow-pointer-lock',
539
+ })
540
+ } else {
541
+ var errText = errored && status != null && status.error
542
+ ? status.error
543
+ : (starting ? '正在启动 code-server…' : 'code-server 未运行')
544
+ body = React.createElement('div', { className: 'dshcs-empty' },
545
+ React.createElement('div', { className: 'dshcs-emptybox' },
546
+ React.createElement('div', null, starting ? '正在启动 code-server…' : errored ? 'code-server 启动失败' : 'code-server 未运行'),
547
+ React.createElement('pre', { className: 'dshcs-error' }, errText),
548
+ React.createElement('p', { className: 'dshcs-hint' },
549
+ '安装命令(Windows 原生):npm install -g code-server@latest(需配套最新版 node-gyp 与 VS Spectre 缓解库);' +
550
+ '或在 cordis.patch.yml 的 code-server config 中设置 bin 指向已安装位置。' +
551
+ '端口 ' + (status != null ? status.port : '8090') + ' 被占用时请释放或修改 port 配置。')
552
+ ))
553
+ }
554
+
555
+ var className = ['dshcs-win', folded ? 'dshcs-win-folded' : '', maximized ? 'dshcs-win-max' : ''].filter(Boolean).join(' ')
556
+ // 最大化由 rect 驱动(maximizedRect 占满"输入栏上方"区域),不用 CSS inset;
557
+ // 还原时回到拖动前的位置。
558
+ var shownRect = maximized ? maximizedRect(viewportSize(), reserve) : rect
559
+ var style = { left: shownRect.x, top: shownRect.y, width: shownRect.width, height: shownRect.height }
560
+
561
+ var windowTree = React.createElement('section', {
562
+ className: className,
563
+ style: style,
564
+ 'data-interaction': interaction !== null ? interaction : undefined,
565
+ role: 'dialog',
566
+ 'aria-label': 'Code Server',
567
+ },
568
+ React.createElement('header', {
569
+ className: 'dshcs-header',
570
+ onPointerDown: onHeaderPointerDown,
571
+ onDoubleClick: onHeaderDoubleClick,
572
+ },
573
+ React.createElement('span', { className: 'dshcs-glyph', 'aria-hidden': true }, React.createElement(Glyph, { baseUrl: baseUrl })),
574
+ React.createElement('span', { className: 'dshcs-identity' },
575
+ React.createElement('span', { className: 'dshcs-title' }, 'Code Server'),
576
+ React.createElement('span', { className: 'dshcs-sub' }, metaBits.length > 0 ? metaBits.join(' · ') : 'VS Code 网页版')
577
+ ),
578
+ React.createElement('span', { className: 'dshcs-chip', 'data-status': statusKind },
579
+ React.createElement('span', { className: 'dshcs-dot', 'aria-hidden': true }),
580
+ statusLabel
581
+ ),
582
+ React.createElement('span', { className: 'dshcs-controls' },
583
+ React.createElement('button', {
584
+ type: 'button', className: 'dshcs-ctl', 'data-window-control': '', title: folded ? '展开' : '折叠',
585
+ 'aria-label': folded ? '展开' : '折叠', onClick: toggleFolded,
586
+ }, React.createElement(FoldIcon, { expanded: folded })),
587
+ React.createElement('button', {
588
+ type: 'button', className: 'dshcs-ctl', 'data-window-control': '', title: maximized ? '还原' : '最大化',
589
+ 'aria-label': maximized ? '还原' : '最大化', onClick: toggleMaximized,
590
+ }, React.createElement(MaximizeIcon, { restored: maximized })),
591
+ React.createElement('button', {
592
+ type: 'button', className: 'dshcs-ctl', 'data-window-control': '', 'data-danger': '', title: '关闭(Esc)',
593
+ 'aria-label': '关闭', onClick: function () { setState({ open: false }) },
594
+ }, React.createElement(CloseIcon, null))
595
+ )
596
+ ),
597
+ React.createElement('div', { className: 'dshcs-body', hidden: folded },
598
+ body
599
+ ),
600
+ !folded && !maximized
601
+ ? RESIZE_DIRECTIONS.map(function (direction) {
602
+ return React.createElement('span', {
603
+ key: direction,
604
+ className: 'dshcs-resize dshcs-resize-' + direction,
605
+ 'data-direction': direction,
606
+ onPointerDown: function (event) { beginPointerSession(event, direction) },
607
+ })
608
+ })
609
+ : null
610
+ )
611
+ // portal 到 document.body:脱离 shell.overlay(z-index 20)的 stacking context,
612
+ // 让窗口真正覆盖到 DSH 输入框区(输入框不再透出)。
613
+ return ReactDOM.createPortal(windowTree, document.body)
614
+ }
615
+
616
+ // ---------- 设置卡片(参照 auto-open-web 的自绘卡片模式) ----------
617
+ // 数据通道:settingsScope(官方 settings 域,命名空间 code-server);
618
+ // 插槽:settings.plugin.item(keyed 注册,卡片自绘,观感对齐官方设计令牌)。
619
+ var CARD_CSS =
620
+ '.dshcs-card{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:12px;list-style:none;transition:border-color .16s,background .16s}' +
621
+ '.dshcs-card:hover{border-color:var(--dsw-alias-label-dimmed,var(--dsw-alias-border-l2))}' +
622
+ '.dshcs-cardOpen{background:var(--dsw-alias-bg-layer-2);border-color:var(--dsw-alias-label-dimmed,var(--dsw-alias-border-l2))}' +
623
+ '.dshcs-cardHeader{appearance:none;width:100%;font:inherit;color:inherit;text-align:left;cursor:pointer;background:0 0;border:0;border-radius:12px;align-items:center;gap:12px;padding:14px 16px;display:flex}' +
624
+ '.dshcs-cardHeader:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:-2px}' +
625
+ '.dshcs-cardHeadText{flex-direction:column;flex:1;gap:4px;min-width:0;display:flex}' +
626
+ '.dshcs-cardName{color:var(--dsw-alias-label-primary);font-size:15px;font-weight:600;line-height:1.4}' +
627
+ '.dshcs-cardDescription{color:var(--dsw-alias-label-tertiary,var(--dsw-alias-label-secondary));font-size:13px;line-height:1.5}' +
628
+ '.dshcs-cardChevron{color:var(--dsw-alias-label-tertiary,var(--dsw-alias-label-secondary));flex:none;transition:transform .16s;display:inline-flex}' +
629
+ '.dshcs-cardChevronOpen{transform:rotate(180deg)}' +
630
+ '.dshcs-cardBody{border-top:1px solid var(--dsw-alias-border-l2);margin:0 16px;padding-bottom:8px}' +
631
+ '.dshcs-cardReadOnly{color:var(--dsw-alias-label-tertiary,var(--dsw-alias-label-secondary));margin:12px 0 0;font-size:12px;line-height:1.5}' +
632
+ '.dshcs-pending{white-space:nowrap;background:var(--dsw-alias-bg-module-platform,var(--dsw-alias-bg-layer-2));color:var(--dsw-alias-label-secondary);border-radius:999px;flex:none;padding:1px 8px;font-size:11px;font-weight:500;line-height:17px}' +
633
+ '.dshcs-cardFooter{border-top:1px solid var(--dsw-alias-border-l2);justify-content:flex-end;align-items:center;gap:8px;padding:12px 0 4px;display:flex}' +
634
+ '.dshcs-cardFailed{min-width:0;color:var(--dsw-alias-label-error,var(--dsw-alias-state-error-primary));flex:1;margin:0;font-size:12px;line-height:1.5}' +
635
+ '.dshcs-cbtn{appearance:none;font:inherit;cursor:pointer;border-radius:8px;padding:5px 14px;font-size:13px;line-height:1.5;border:1px solid transparent;transition:color .12s ease,background .12s ease}' +
636
+ '.dshcs-cbtn:disabled{opacity:.4;cursor:default}' +
637
+ '.dshcs-cbtn:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:1px}' +
638
+ '.dshcs-cbtnOutline{border-color:var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);background:0 0}' +
639
+ '.dshcs-cbtnOutline:hover:not(:disabled){color:var(--dsw-alias-label-primary);background:var(--dsw-alias-interactive-bg-hover,transparent)}' +
640
+ '.dshcs-cbtnPrimary{background:var(--dsw-alias-label-primary);color:var(--dsw-alias-bg-layer-3);border-color:var(--dsw-alias-label-primary);font-weight:600}' +
641
+ '.dshcs-cbtnPrimary:hover:not(:disabled){filter:brightness(.95)}' +
642
+ '.dshcs-cbtnPrimary:active:not(:disabled){filter:brightness(.9)}' +
643
+ '.dshcs-field{flex-direction:column;gap:6px;padding:12px 0;display:flex}' +
644
+ '.dshcs-field+.dshcs-field{border-top:1px solid var(--dsw-alias-border-l2)}' +
645
+ '.dshcs-fieldHead{align-items:center;gap:8px;display:flex}' +
646
+ '.dshcs-fieldLabel{min-width:0;color:var(--dsw-alias-label-primary);flex:1;font-size:13px;font-weight:500;line-height:1.5}' +
647
+ '.dshcs-hint{color:var(--dsw-alias-label-tertiary,var(--dsw-alias-label-secondary));margin:0;font-size:12px;line-height:1.5}' +
648
+ '.dshcs-check{display:flex;align-items:center;gap:8px;cursor:pointer}' +
649
+ '.dshcs-check input{accent-color:var(--dsw-alias-brand-primary);width:15px;height:15px;margin:0;flex:none}' +
650
+ '.dshcs-check input:disabled{cursor:default}' +
651
+ '.dshcs-badges{align-items:center;gap:8px;display:inline-flex}' +
652
+ '.dshcs-badge{white-space:nowrap;background:var(--dsw-alias-bg-module-platform,var(--dsw-alias-bg-layer-2));color:var(--dsw-alias-label-secondary);border-radius:999px;padding:1px 8px;font-size:11px;font-weight:500;line-height:17px}' +
653
+ '.dshcs-reset{font:inherit;color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border:none;padding:0;font-size:12px;line-height:1.5}' +
654
+ '.dshcs-reset:hover:not(:disabled){color:var(--dsw-alias-label-primary)}' +
655
+ '.dshcs-reset:disabled{cursor:default}'
656
+ var CARD_TAG = 'dsh-code-server/Card.module.css'
657
+ function injectCss(tagId, css) {
658
+ if (typeof document === 'undefined') return
659
+ if (document.querySelector('style[data-plugin-css=' + JSON.stringify(tagId) + ']') !== null) return
660
+ var tag = document.createElement('style')
661
+ tag.dataset.plugin = 'dsh-code-server'
662
+ tag.dataset.pluginCss = tagId
663
+ tag.textContent = css
664
+ document.head.appendChild(tag)
665
+ }
666
+ injectCss(CARD_TAG, CARD_CSS)
667
+
668
+ function csIconChevron(props) {
669
+ return React.createElement('svg', {
670
+ width: 14, height: 14, viewBox: '0 0 14 14', fill: 'none', 'aria-hidden': true, className: props.className,
671
+ },
672
+ React.createElement('path', {
673
+ d: 'M3.5 5.25L7 8.75L10.5 5.25', stroke: 'currentColor', strokeWidth: 1.5, strokeLinecap: 'round', strokeLinejoin: 'round',
674
+ })
675
+ )
676
+ }
677
+ function csBtn(props) {
678
+ return React.createElement('button', {
679
+ type: 'button',
680
+ className: 'dshcs-cbtn ' + (props.variant === 'primary' ? 'dshcs-cbtnPrimary' : 'dshcs-cbtnOutline'),
681
+ disabled: props.disabled === true,
682
+ onClick: props.onClick,
683
+ }, props.children)
684
+ }
685
+ function csCheck(props) {
686
+ return React.createElement('label', { className: 'dshcs-check' },
687
+ React.createElement('input', {
688
+ type: 'checkbox', checked: props.checked === true, disabled: props.disabled === true,
689
+ onChange: function (event) { props.onChange(event.target.checked) },
690
+ }),
691
+ React.createElement('span', { className: 'dshcs-hint' }, props.children)
692
+ )
693
+ }
694
+ function csBadges(props) {
695
+ var children = []
696
+ if (props.overridden === true) {
697
+ children.push(React.createElement('span', { className: 'dshcs-badge' }, props.overriddenLabel))
698
+ children.push(React.createElement('button', { type: 'button', className: 'dshcs-reset', disabled: props.disabled === true, onClick: props.onReset }, props.resetLabel))
699
+ }
700
+ if (children.length === 0) return null
701
+ return React.createElement('span', { className: 'dshcs-badges' }, ...children)
702
+ }
703
+ function csCard(props) {
704
+ var [open, setOpen] = React.useState(false)
705
+ var state = props.state
706
+ if (state === null || state === undefined || state.available !== true) return null
707
+ var title = props.title
708
+ var saveDisabled = state.dirty !== true || state.invalid === true || state.saving === true
709
+ return React.createElement('li', { className: 'dshcs-card' + (open ? ' dshcs-cardOpen' : '') },
710
+ React.createElement('button', {
711
+ type: 'button', className: 'dshcs-cardHeader', 'aria-expanded': open,
712
+ onClick: function () { setOpen(!open) },
713
+ },
714
+ React.createElement('span', { className: 'dshcs-cardHeadText' },
715
+ React.createElement('span', { className: 'dshcs-cardName' }, title),
716
+ React.createElement('span', { className: 'dshcs-cardDescription' }, props.description)
717
+ ),
718
+ state.dirty === true ? React.createElement('span', { className: 'dshcs-pending' }, props.unsavedLabel) : null,
719
+ React.createElement(csIconChevron, { className: 'dshcs-cardChevron' + (open ? ' dshcs-cardChevronOpen' : '') })
720
+ ),
721
+ open === true ? React.createElement('div', { className: 'dshcs-cardBody' },
722
+ state.writable !== true ? React.createElement('p', { className: 'dshcs-cardReadOnly', role: 'status' }, props.readOnlyLabel) : null,
723
+ props.children,
724
+ React.createElement('div', { className: 'dshcs-cardFooter' },
725
+ state.failed === true ? React.createElement('p', { className: 'dshcs-cardFailed', role: 'status' }, props.saveFailedLabel) : null,
726
+ React.createElement(csBtn, { disabled: state.dirty !== true || state.saving === true, onClick: props.onDiscard }, props.discardLabel),
727
+ React.createElement(csBtn, { variant: 'primary', disabled: saveDisabled, onClick: props.onSave }, state.saving === true ? props.savingLabel : props.saveLabel)
728
+ )
729
+ ) : null
730
+ )
731
+ }
732
+
733
+ /** 设置卡片页面:useState+useEffect 订阅 settingsScope(不投机 useSyncExternalStore
734
+ * 对 snapshot 引用稳定性的要求;不建中间 store)。 */
735
+ function csSettingsPage(props) {
736
+ try {
737
+ var scope = props.scope
738
+ var [snapshot, setSnapshot] = React.useState(function () { return scope !== undefined ? scope.getSnapshot() : null })
739
+ var [draft, setDraft] = React.useState(null) // null | boolean(未保存草稿)
740
+ var [saving, setSaving] = React.useState(false)
741
+ var [failed, setFailed] = React.useState(false)
742
+ React.useEffect(function () {
743
+ if (scope === undefined || typeof scope.subscribe !== 'function') return
744
+ function onUpdate() {
745
+ try { setSnapshot(scope.getSnapshot()) } catch (e) { /* ignore */ }
746
+ }
747
+ var dispose = scope.subscribe(onUpdate)
748
+ return function () { if (typeof dispose === 'function') dispose() }
749
+ }, [scope])
750
+ if (scope === undefined) {
751
+ console.error('[code-server] card scope missing')
752
+ return null
753
+ }
754
+ if (snapshot === null || snapshot === undefined || snapshot.status !== 'ready') {
755
+ // 尚未就绪(连接/镜像加载中):静默留空,官方插槽边界接管;不显示占位
756
+ if (snapshot !== null && snapshot !== undefined && snapshot.status === 'unavailable') {
757
+ console.warn('[code-server] settings scope unavailable:', snapshot.status)
758
+ }
759
+ return null
760
+ }
761
+ var value = snapshot.value !== undefined && snapshot.value !== null ? snapshot.value : {}
762
+ var user = snapshot.user
763
+ var loaded = value.reserveComposer === true
764
+ var overridden = user !== undefined && user !== null && Object.prototype.hasOwnProperty.call(user, 'reserveComposer')
765
+ var dirty = draft !== null && draft !== loaded
766
+ var saveDisabled = !dirty || saving
767
+ var state = {
768
+ available: true,
769
+ writable: snapshot.writable,
770
+ dirty: dirty,
771
+ invalid: false,
772
+ saving: saving,
773
+ failed: failed,
774
+ reserveComposer: { text: draft !== null ? draft : loaded, overridden: overridden },
775
+ }
776
+ async function doSave() {
777
+ if (dirty && !saving) {
778
+ setSaving(true); setFailed(false)
779
+ try {
780
+ await props.scope.set('reserveComposer', draft === true)
781
+ setDraft(null)
782
+ } catch (e) {
783
+ setFailed(true)
784
+ }
785
+ setSaving(false)
786
+ }
787
+ }
788
+ async function doReset() {
789
+ if (saving || snapshot.writable !== true) return
790
+ setSaving(true); setFailed(false)
791
+ try {
792
+ // unset 清除 user 覆盖层 → 回到 base 默认值;成功后镜像同步
793
+ if (typeof props.scope.unset === 'function') await props.scope.unset('reserveComposer')
794
+ else await props.scope.set('reserveComposer', value.reserveComposer === undefined || value.reserveComposer === null ? true : value.reserveComposer)
795
+ setDraft(null)
796
+ } catch (e) {
797
+ setFailed(true)
798
+ }
799
+ setSaving(false)
800
+ }
801
+ // ---- 环境检测/安装(host /code-server/status.env + /code-server/setup) ----
802
+ var [envInfo, setEnvInfo] = React.useState(null) // null=待检测 | { ok, entry, native, vscodeInner, pathToSetup } | { error }
803
+ var [setupBusy, setSetupBusy] = React.useState(false)
804
+ var [setupMsg, setSetupMsg] = React.useState(null)
805
+ var envAliveRef = React.useRef(true) // 组件卸载后停止轮询(不向已卸载组件 setState)
806
+ React.useEffect(function () {
807
+ envAliveRef.current = true
808
+ return function () { envAliveRef.current = false }
809
+ }, [])
810
+ async function doInstallEnv() {
811
+ if (setupBusy) return
812
+ setSetupBusy(true); setSetupMsg(null)
813
+ // api 无 body 时发 GET;setup 是 POST 路由 — 传空对象强制 POST
814
+ var s = await api('/code-server/setup', {})
815
+ if (s == null || s.ok !== true) {
816
+ setSetupMsg(s != null && s.error ? s.error : '启动安装失败')
817
+ setSetupBusy(false)
818
+ return
819
+ }
820
+ setSetupMsg('环境安装已在后台开始,完成后自动刷新…')
821
+ // 轮询 status.setup 直到安装结束:成功后刷新 env 检测,失败显示日志尾部
822
+ function poll() {
823
+ if (envAliveRef.current !== true) return
824
+ api('/code-server/status').then(function (st) {
825
+ if (envAliveRef.current !== true) return
826
+ if (st == null || st.setup == null) { setSetupBusy(false); return }
827
+ if (st.setup.running === true) { setTimeout(poll, 2000); return }
828
+ setSetupBusy(false)
829
+ setEnvInfo(st.env != null ? st.env : { error: 'status 未返回 env' })
830
+ setSetupMsg(st.setup.ok === true
831
+ ? '环境安装完成'
832
+ : '环境安装失败:\n' + (st.setup.logTail != null ? st.setup.logTail.slice(-600) : '(无日志)'))
833
+ })
834
+ }
835
+ poll()
836
+ }
837
+ var envSection = React.createElement('div', { className: 'dshcs-field' },
838
+ React.createElement('div', { className: 'dshcs-fieldHead' },
839
+ React.createElement('span', { className: 'dshcs-fieldLabel' }, '环境检测'),
840
+ React.createElement('span', { className: 'dshcs-badges' },
841
+ React.createElement(csBtn, { variant: 'primary', disabled: setupBusy, onClick: async function () {
842
+ setSetupBusy(true); setSetupMsg(null)
843
+ var s = await api('/code-server/status')
844
+ setEnvInfo(s != null && s.env != null ? s.env : { error: 'status 未返回 env' })
845
+ setSetupBusy(false)
846
+ } }, '检测环境'),
847
+ React.createElement(csBtn, { disabled: setupBusy, onClick: function () { doInstallEnv() } }, setupBusy ? '安装中…' : '安装环境')
848
+ )
849
+ ),
850
+ envInfo != null
851
+ ? React.createElement('div', { className: 'dshcs-hint', style: { marginTop: 6 } },
852
+ envInfo.error != null
853
+ ? React.createElement('span', null, '检测失败: ' + envInfo.error)
854
+ : React.createElement('span', null,
855
+ '状态: ' + (envInfo.ok === true ? '✅ 就绪' : '❌ 不通过') +
856
+ (envInfo.entry != null ? ' · 入口: ' + envInfo.entry : ' · 入口缺失') +
857
+ ' · native: ' + (envInfo.native === true ? '✅' : '❌') +
858
+ ' · VS Code 内部依赖: ' + (envInfo.vscodeInner === true ? '✅' : '❌'))
859
+ )
860
+ : React.createElement('div', { className: 'dshcs-hint', style: { marginTop: 6 } }, '点击"检测环境"查看 code-server 安装状态')
861
+ ,
862
+ setupMsg != null
863
+ ? React.createElement('div', { className: 'dshcs-hint', style: { marginTop: 6 } }, setupMsg)
864
+ : null,
865
+ React.createElement('div', { className: 'dshcs-hint', style: { marginTop: 10, color: 'var(--dsw-alias-label-tertiary,var(--dsw-alias-label-secondary))' } },
866
+ '安装位置: ' + (envInfo != null && envInfo.entry != null ? envInfo.entry : '(profile)\\.code-server-app(待安装后可查看)') +
867
+ '\n卸载: 先手动删除 .code-server-app 目录,再 dsh plugin --profile web remove dsh-code-server-app'
868
+ )
869
+ )
870
+ return React.createElement(csCard, {
871
+ title: 'Code Server',
872
+ description: '窗口行为:是否保留输入框上方空间(不遮挡 composer)',
873
+ state: state,
874
+ unsavedLabel: '未保存', readOnlyLabel: '本部署的设置为只读。',
875
+ saveFailedLabel: '本部署没有接受这些值,已保留供你修改。',
876
+ discardLabel: '放弃修改', saveLabel: '保存', savingLabel: '保存中…',
877
+ onSave: doSave, onDiscard: function () { setDraft(null); setFailed(false) },
878
+ },
879
+ React.createElement('div', { className: 'dshcs-field' },
880
+ React.createElement('div', { className: 'dshcs-fieldHead' },
881
+ React.createElement('span', { className: 'dshcs-fieldLabel' }, '保留输入框上方空间'),
882
+ overridden === true
883
+ ? React.createElement(csBadges, {
884
+ overridden: true, disabled: snapshot.writable !== true,
885
+ overriddenLabel: '已覆盖', resetLabel: '恢复默认',
886
+ onReset: function () { doReset() },
887
+ })
888
+ : null
889
+ ),
890
+ React.createElement(csCheck, {
891
+ checked: draft !== null ? draft : loaded,
892
+ disabled: snapshot.writable !== true,
893
+ onChange: function (v) { setDraft(v === true); setFailed(false) },
894
+ }, '开启时窗口初始/缩放/最大化止于输入框上方;关闭后允许盖住输入框(最大化到视口底)')
895
+ ),
896
+ envSection
897
+ )
898
+ } catch (e) {
899
+ // 渲染异常:记日志不打断;官方插槽对异常有边界,卡片留空即可
900
+ console.error('[code-server] card render error:', e !== null && e !== undefined && e.message !== undefined ? e.message : String(e))
901
+ return null
902
+ }
903
+ }
904
+
905
+ // ---------- 插件注册 ----------
906
+ function apply(ctx) {
907
+ var slots = ctx.get('slots')
908
+ if (slots === undefined) {
909
+ console.error('[code-server] slots service unavailable')
910
+ return
911
+ }
912
+ // 预拉取 status:让侧栏按钮首帧就有 env(检测通过才显示,不闪烁)
913
+ api('/code-server/status').then(function (s) {
914
+ setState({ status: s })
915
+ }).catch(function () { /* 首次失败由后续轮询补救 */ })
916
+ slots.inject('sidebar.footer.action', () => slots.register(
917
+ { name: 'sidebar.footer.action', id: 'code-server-panel', order: 60, label: 'Code Server' },
918
+ (props) => React.createElement(SidebarAction, props)
919
+ ))
920
+ slots.inject('shell.overlay', () => slots.register(
921
+ { name: 'shell.overlay', id: 'code-server', order: 70, label: 'Code Server' },
922
+ (props) => React.createElement(Window, props)
923
+ ))
924
+
925
+ // ---- 设置卡片(settings.plugin.item 插槽 + settingsScope 命名空间) ----
926
+ var settingsScope = ctx.settingsScope
927
+ if (settingsScope === undefined || typeof settingsScope.bind !== 'function') {
928
+ console.error('[code-server] settingsScope unavailable (inject missing?); settings card disabled')
929
+ return
930
+ }
931
+ var scope = settingsScope.bind({ namespace: 'code-server' })
932
+ slots.inject('settings.plugin.item', () => slots.register(
933
+ { name: 'settings.plugin.item', key: 'code-server', label: 'Code Server', inject: function () { return { scope: scope } } },
934
+ csSettingsPage
935
+ ))
936
+
937
+ console.log('[code-server] client bundle registered (sidebar action + floating window + settings card)')
938
+ }
939
+
940
+ exports.apply = apply
941
+ exports.inject = ['slots', 'settingsScope']
942
+ exports.name = 'code-server'
943
+ return module.exports
944
+ }
945
+ })