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/README.md +150 -0
- package/cordis.patch.yml +31 -0
- package/lib/client.js +945 -0
- package/lib/index.js +666 -0
- package/package.json +47 -0
- package/scripts/setup-code-server.mjs +157 -0
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
|
+
})
|