@langify-org/dsh-code-server 0.0.0-stage → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,708 @@
1
+ window.__ModuleLoader__.load({
2
+ id: "@langify-org/dsh-code-server",
3
+ factory: (require) => {
4
+ var module = { exports: {} };
5
+ var exports = module.exports;
6
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
7
+ let react = require("react");
8
+ let zod = require("zod");
9
+ let react_jsx_runtime = require("react/jsx-runtime");
10
+ // src/messages.ts
11
+ const en = {
12
+ title: "Code",
13
+ guide: "Edit the working folder in code-server",
14
+ frame: "code-server: {cwd}",
15
+ loading: "Starting code-server…",
16
+ opening: "Opening in Code…",
17
+ dismiss: "Dismiss",
18
+ reconnect: "Reconnect",
19
+ "reconnect.confirm": "Reload the editor? Save your changes first. Files you chose before reconnecting are not reopened.",
20
+ "notice.ended": "This editor connection has ended, and files are not sent to another window. Reconnect to continue.",
21
+ "queue.ended": "The editor connection has ended. Reconnect, then choose the file again.",
22
+ "queue.full": "Too many files are waiting. Choose them again once the editor is ready.",
23
+ "queue.expired": "A file waited too long and was dropped. Choose it again.",
24
+ "error.invalid_path": "Only regular files can be opened.",
25
+ "error.deadline_exceeded": "The editor did not open the file in time. Choose it again.",
26
+ "error.outcome_unknown": "It is unknown whether the editor opened the file; it may still appear later. Nothing is resent automatically.",
27
+ "error.open_failed": "The editor could not read the file: {message}",
28
+ "error.show_failed": "The editor could not show the file: {message}",
29
+ "error.invalid_request": "The request was invalid: {message}",
30
+ "error.session_not_found": "The session no longer exists.",
31
+ "error.session_migration_required": "Open this conversation once so DSH can migrate it, then try again.",
32
+ "error.not_a_directory": "The working folder does not exist.",
33
+ "error.not_a_file": "The file does not exist.",
34
+ "error.not_a_local_file": "Only local files open in Code.",
35
+ "error.different_host": "This session runs on another file system, which Code cannot open.",
36
+ "error.code_server_not_found": "code-server was not found. Install it, or set its command in the plugin settings.",
37
+ "error.code_server_failed": "code-server failed to start: {message}",
38
+ "error.unknown_binding": "This editor connection is gone. Reconnect.",
39
+ "error.not_ready": "The editor is still starting.",
40
+ "error.binding_disconnected": "The editor window disconnected. Reconnect.",
41
+ "error.binding_expired": "The editor connection expired. Reconnect.",
42
+ "error.binding_closed": "The editor connection was closed. Reconnect.",
43
+ "error.internal_error": "The plugin failed: {message}",
44
+ "error.unexpected": "Something went wrong: {message}",
45
+ "cwd.changed": "The session's working folder is now {cwd}. Reopen to edit it here.",
46
+ "cwd.reopen": "Reopen",
47
+ "reconnect.unsaved": "Some files have unsaved changes. Save them before reconnecting?",
48
+ "reconnect.saveAll": "Save All and Reconnect",
49
+ "status.name": "DeepSeek Harness connection",
50
+ "status.connecting": "Connecting to DeepSeek Harness…",
51
+ "status.connected": "Connected to DeepSeek Harness",
52
+ "status.disconnected": "Disconnected from DeepSeek Harness. Reconnect from the Code tab in DSH.",
53
+ "status.showLog": "Show the connection log"
54
+ };
55
+ const zh = {
56
+ title: "Code",
57
+ guide: "在 code-server 中编辑工作文件夹",
58
+ frame: "code-server:{cwd}",
59
+ loading: "正在启动 code-server…",
60
+ opening: "正在用 Code 打开…",
61
+ dismiss: "关闭",
62
+ reconnect: "重新连接",
63
+ "reconnect.confirm": "要重新加载编辑器吗?请先保存更改。重新连接前选择的文件不会重新打开。",
64
+ "notice.ended": "与此编辑器的连接已结束,文件不会发送到其他窗口。请重新连接以继续。",
65
+ "queue.ended": "与编辑器的连接已结束。请重新连接,然后再次选择该文件。",
66
+ "queue.full": "等待打开的文件过多。请在编辑器就绪后重新选择。",
67
+ "queue.expired": "有文件等待时间过长,已被取消。请重新选择。",
68
+ "error.invalid_path": "只能打开普通文件。",
69
+ "error.deadline_exceeded": "编辑器未能及时打开文件。请重新选择。",
70
+ "error.outcome_unknown": "无法确认编辑器是否打开了文件,它可能稍后才显示。不会自动重新发送。",
71
+ "error.open_failed": "编辑器无法读取该文件:{message}",
72
+ "error.show_failed": "编辑器无法显示该文件:{message}",
73
+ "error.invalid_request": "请求无效:{message}",
74
+ "error.session_not_found": "该会话已不存在。",
75
+ "error.session_migration_required": "请先打开一次此对话,让 DSH 完成迁移,然后重试。",
76
+ "error.not_a_directory": "工作文件夹不存在。",
77
+ "error.not_a_file": "文件不存在。",
78
+ "error.not_a_local_file": "只有本地文件可以在 Code 中打开。",
79
+ "error.different_host": "此会话运行在其他文件系统上,Code 无法打开。",
80
+ "error.code_server_not_found": "未找到 code-server。请安装它,或在插件设置中指定其命令。",
81
+ "error.code_server_failed": "code-server 启动失败:{message}",
82
+ "error.unknown_binding": "与此编辑器的连接已不存在。请重新连接。",
83
+ "error.not_ready": "编辑器仍在启动中。",
84
+ "error.binding_disconnected": "编辑器窗口已断开。请重新连接。",
85
+ "error.binding_expired": "与编辑器的连接已过期。请重新连接。",
86
+ "error.binding_closed": "与编辑器的连接已关闭。请重新连接。",
87
+ "error.internal_error": "插件出错:{message}",
88
+ "error.unexpected": "出现问题:{message}",
89
+ "cwd.changed": "会话的工作文件夹已变为 {cwd}。重新打开即可在此编辑。",
90
+ "cwd.reopen": "重新打开",
91
+ "reconnect.unsaved": "有文件尚未保存。要先保存再重新连接吗?",
92
+ "reconnect.saveAll": "全部保存并重新连接",
93
+ "status.name": "DeepSeek Harness 连接",
94
+ "status.connecting": "正在连接 DeepSeek Harness…",
95
+ "status.connected": "已连接 DeepSeek Harness",
96
+ "status.disconnected": "与 DeepSeek Harness 的连接已断开。请在 DSH 的 Code 标签页中重新连接。",
97
+ "status.showLog": "显示连接日志"
98
+ };
99
+ const ja = {
100
+ title: "Code",
101
+ guide: "作業フォルダーを code-server で編集する",
102
+ frame: "code-server: {cwd}",
103
+ loading: "code-server を起動しています…",
104
+ opening: "Code で開いています…",
105
+ dismiss: "閉じる",
106
+ reconnect: "再接続",
107
+ "reconnect.confirm": "エディターを再読み込みします。先に変更を保存してください。再接続前に選んだファイルは開き直しません。",
108
+ "notice.ended": "このエディターとの接続は終了しました。別の画面には送りません。続けるには再接続してください。",
109
+ "queue.ended": "エディターとの接続が終了しています。再接続してから、もう一度ファイルを選んでください。",
110
+ "queue.full": "待機中のファイルが多すぎます。エディターの準備ができてから選び直してください。",
111
+ "queue.expired": "待機が長すぎたファイルを取り消しました。もう一度選んでください。",
112
+ "error.invalid_path": "開けるのは通常のファイルだけです。",
113
+ "error.deadline_exceeded": "エディターが時間内にファイルを開けませんでした。もう一度選んでください。",
114
+ "error.outcome_unknown": "エディターがファイルを開いたか確認できません。後から表示される場合があります。自動では再送しません。",
115
+ "error.open_failed": "エディターがファイルを読めませんでした: {message}",
116
+ "error.show_failed": "エディターがファイルを表示できませんでした: {message}",
117
+ "error.invalid_request": "要求が不正です: {message}",
118
+ "error.session_not_found": "セッションがもう存在しません。",
119
+ "error.session_migration_required": "DSH が移行できるよう、この会話を一度開いてからやり直してください。",
120
+ "error.not_a_directory": "作業フォルダーが存在しません。",
121
+ "error.not_a_file": "ファイルが存在しません。",
122
+ "error.not_a_local_file": "Code で開けるのはローカルのファイルだけです。",
123
+ "error.different_host": "このセッションは別のファイルシステムで動いているため、Code では開けません。",
124
+ "error.code_server_not_found": "code-server が見つかりません。インストールするか、プラグインの設定でコマンドを指定してください。",
125
+ "error.code_server_failed": "code-server を起動できませんでした: {message}",
126
+ "error.unknown_binding": "このエディターとの接続はもうありません。再接続してください。",
127
+ "error.not_ready": "エディターはまだ起動中です。",
128
+ "error.binding_disconnected": "エディターの画面が切断されました。再接続してください。",
129
+ "error.binding_expired": "エディターとの接続が期限切れになりました。再接続してください。",
130
+ "error.binding_closed": "エディターとの接続が閉じられました。再接続してください。",
131
+ "error.internal_error": "プラグインでエラーが起きました: {message}",
132
+ "error.unexpected": "問題が起きました: {message}",
133
+ "cwd.changed": "セッションの作業フォルダーが {cwd} に変わりました。開き直すとここで編集できます。",
134
+ "cwd.reopen": "開き直す",
135
+ "reconnect.unsaved": "保存していない変更があります。保存してから再接続しますか?",
136
+ "reconnect.saveAll": "すべて保存して再接続",
137
+ "status.name": "DeepSeek Harness との接続",
138
+ "status.connecting": "DeepSeek Harness に接続しています…",
139
+ "status.connected": "DeepSeek Harness に接続済み",
140
+ "status.disconnected": "DeepSeek Harness との接続が切れました。DSH の Code タブから再接続してください。",
141
+ "status.showLog": "接続のログを表示"
142
+ };
143
+ /** Whether a computed key, such as `error.${code}` for a code from the wire, has a message. */
144
+ function isMessageKey(key) {
145
+ return Object.hasOwn(en, key);
146
+ }
147
+ // src/rpc.ts
148
+ /** DSH's shared RPC channel, and the prefix of the endpoints this plugin owns on it. */
149
+ const RPC_CHANNEL = "/api";
150
+ const RPC_PREFIX = "langify-code-server/";
151
+ const id = zod.z.string().min(1).max(256).refine((value) => !value.includes("\0"));
152
+ const position = zod.z.int().min(1).max(1e6);
153
+ const binding = {
154
+ sessionId: id,
155
+ bindingId: zod.z.uuid()
156
+ };
157
+ zod.z.strictObject({
158
+ sessionId: id,
159
+ ownerId: id
160
+ }), zod.z.strictObject({
161
+ ...binding,
162
+ revision: zod.z.int().min(-1)
163
+ }), zod.z.strictObject(binding), zod.z.strictObject({
164
+ ...binding,
165
+ address: zod.z.string().min(1).max(16384),
166
+ line: position.optional()
167
+ });
168
+ const responseSchemas = {
169
+ create: zod.z.strictObject({
170
+ bindingId: zod.z.uuid(),
171
+ cwd: zod.z.string(),
172
+ url: zod.z.string()
173
+ }),
174
+ watch: zod.z.strictObject({
175
+ state: zod.z.enum([
176
+ "pending",
177
+ "ready",
178
+ "disconnected",
179
+ "expired",
180
+ "closed"
181
+ ]),
182
+ revision: zod.z.int().nonnegative(),
183
+ reconnect: zod.z.boolean()
184
+ }),
185
+ close: zod.z.strictObject({ state: zod.z.literal("closed") }),
186
+ open: zod.z.strictObject({
187
+ path: zod.z.string(),
188
+ line: position
189
+ })
190
+ };
191
+ // src/client/session-bus.ts
192
+ /** After these the page must reconnect; files are never sent to another window. */
193
+ const FINAL_PHASES = /* @__PURE__ */ new Set([
194
+ "disconnected",
195
+ "expired",
196
+ "closed",
197
+ "error"
198
+ ]);
199
+ const QUEUE_LIMIT = 16;
200
+ const QUEUE_TTL_MS = 6e4;
201
+ const SEEN_LIMIT = 256;
202
+ /** One session's Code page state, shared by its Code tab and its file-link tabs. */
203
+ var SessionBus = class {
204
+ #state = {
205
+ phase: "idle",
206
+ queue: [],
207
+ notice: void 0,
208
+ owner: void 0
209
+ };
210
+ #listeners = /* @__PURE__ */ new Set();
211
+ /** Navigation keys already queued, so a re-rendered link is not opened twice. */
212
+ #seen = /* @__PURE__ */ new Set();
213
+ #now;
214
+ constructor(now = Date.now) {
215
+ this.#now = now;
216
+ }
217
+ getSnapshot = () => this.#state;
218
+ subscribe = (listener) => {
219
+ this.#listeners.add(listener);
220
+ return () => this.#listeners.delete(listener);
221
+ };
222
+ /** Make `tabId` the Code page, or return the tab that already is. */
223
+ claim(tabId) {
224
+ if (this.#state.owner !== void 0 && this.#state.owner !== tabId) return this.#state.owner;
225
+ if (this.#state.owner === void 0) this.#update({ owner: tabId });
226
+ }
227
+ release(tabId) {
228
+ if (this.#state.owner !== tabId) return;
229
+ this.#update({
230
+ owner: void 0,
231
+ phase: "idle",
232
+ queue: [],
233
+ notice: void 0
234
+ });
235
+ }
236
+ /** Queue a file once per `key` until the editor is ready. */
237
+ enqueue(key, request) {
238
+ if (this.#seen.has(key)) return;
239
+ this.#seen.add(key);
240
+ if (this.#seen.size > SEEN_LIMIT) this.#seen.delete(this.#seen.values().next().value ?? key);
241
+ if (FINAL_PHASES.has(this.#state.phase)) return this.#update({ notice: { key: "queue.ended" } });
242
+ if (this.#state.queue.length >= QUEUE_LIMIT) return this.#update({ notice: { key: "queue.full" } });
243
+ this.#update({ queue: [...this.#state.queue, {
244
+ ...request,
245
+ expiresAt: this.#now() + QUEUE_TTL_MS
246
+ }] });
247
+ }
248
+ /** The next queued file that has not waited too long. */
249
+ take() {
250
+ for (let [next, ...rest] = this.#state.queue; next !== void 0; [next, ...rest] = rest) {
251
+ this.#update({ queue: rest });
252
+ if (next.expiresAt >= this.#now()) return {
253
+ address: next.address,
254
+ line: next.line
255
+ };
256
+ this.#update({ notice: { key: "queue.expired" } });
257
+ }
258
+ }
259
+ phase(phase) {
260
+ this.#update(FINAL_PHASES.has(phase) ? {
261
+ phase,
262
+ queue: []
263
+ } : { phase });
264
+ }
265
+ notice(notice) {
266
+ this.#update({ notice });
267
+ }
268
+ clear() {
269
+ this.#update({ queue: [] });
270
+ }
271
+ #update(change) {
272
+ this.#state = {
273
+ ...this.#state,
274
+ ...change
275
+ };
276
+ for (const listener of this.#listeners) listener();
277
+ }
278
+ };
279
+ // src/client/face.ts
280
+ /** Tab kind of the Code page. */
281
+ const CODE_KIND = "langify-code-server";
282
+ const RPC_TIMEOUT_MS = 25e3;
283
+ /** A failed call; `code` is one of the host's error codes unless DSH's transport failed first. */
284
+ var RpcFailure = class extends Error {
285
+ code;
286
+ constructor(code, message) {
287
+ super(message);
288
+ this.code = code;
289
+ }
290
+ };
291
+ function createFace(ctx, sessionId) {
292
+ const isCurrent = () => ctx.sidebarRight.mounted.getSnapshot() === sessionId;
293
+ return {
294
+ bus: new SessionBus(),
295
+ isCurrent,
296
+ findCode: () => ctx.sidebarRight.openTabs.getSnapshot().find((tab) => tab.sessionId === sessionId && tab.kind === "langify-code-server"),
297
+ reveal: (tabId) => {
298
+ if (!isCurrent()) return;
299
+ if (!ctx.sidebarRight.isExpanded()) ctx.sidebarRight.toggleExpanded();
300
+ ctx.sidebarRight.focus(tabId);
301
+ },
302
+ async call(method, request) {
303
+ const result = await ctx.connection.rpc.call(RPC_CHANNEL, `${RPC_PREFIX}${method}`, {
304
+ ...request,
305
+ sessionId
306
+ }, AbortSignal.timeout(RPC_TIMEOUT_MS));
307
+ if (!result.ok) throw new RpcFailure(result.error.code, result.error.message);
308
+ return responseSchemas[method].parse(result.value);
309
+ }
310
+ };
311
+ }
312
+ // src/client/code-page.tsx
313
+ const SANDBOX = "allow-scripts allow-forms allow-same-origin allow-popups allow-popups-to-escape-sandbox allow-downloads";
314
+ /** After these, the binding is gone and the page must reconnect. */
315
+ const FINAL_ERRORS = /* @__PURE__ */ new Set([
316
+ "unknown_binding",
317
+ "binding_disconnected",
318
+ "binding_expired",
319
+ "binding_closed"
320
+ ]);
321
+ function CodePage({ bus, call, reveal, t, useTabInfo }) {
322
+ const { tab } = useTabInfo();
323
+ const state = (0, react.useSyncExternalStore)(bus.subscribe, bus.getSnapshot);
324
+ const [view, setView] = (0, react.useState)();
325
+ const [attempt, setAttempt] = (0, react.useState)(0);
326
+ const [drains, setDrains] = (0, react.useState)(0);
327
+ const generation = (0, react.useRef)(void 0);
328
+ const draining = (0, react.useRef)(false);
329
+ const frame = (0, react.useRef)(null);
330
+ const visible = (0, react.useRef)(tab.visible);
331
+ visible.current = tab.visible;
332
+ /** `confirm` is false when the editor asked: it has dealt with unsaved changes itself. */
333
+ const reconnect = (confirm = true) => {
334
+ if (confirm && view !== void 0 && !window.confirm(t("reconnect.confirm"))) return;
335
+ bus.clear();
336
+ bus.notice(void 0);
337
+ setAttempt((value) => value + 1);
338
+ };
339
+ const reconnectRef = (0, react.useRef)(reconnect);
340
+ reconnectRef.current = reconnect;
341
+ (0, react.useEffect)(() => tab.actions.bindCommands({ refresh: () => reconnectRef.current() }), [tab.actions]);
342
+ (0, react.useLayoutEffect)(() => {
343
+ const owner = bus.claim(tab.id);
344
+ if (owner !== void 0) {
345
+ reveal(owner);
346
+ tab.actions.close();
347
+ return;
348
+ }
349
+ let disposed = false;
350
+ let binding;
351
+ generation.current = void 0;
352
+ setView(void 0);
353
+ bus.phase("pending");
354
+ const fail = (error) => {
355
+ if (disposed) return;
356
+ bus.phase("error");
357
+ bus.notice(noticeOf(error));
358
+ };
359
+ const close = () => {
360
+ disposed = true;
361
+ generation.current = void 0;
362
+ if (binding !== void 0) call("close", { bindingId: binding.bindingId }).catch(() => {});
363
+ };
364
+ tab.signal.addEventListener("abort", close, { once: true });
365
+ const watch = async (bindingId) => {
366
+ let revision = -1;
367
+ for (;;) {
368
+ const status = await call("watch", {
369
+ bindingId,
370
+ revision
371
+ });
372
+ if (disposed) return;
373
+ if (status.reconnect) return reconnectRef.current(false);
374
+ bus.phase(status.state);
375
+ if (FINAL_PHASES.has(status.state)) return bus.notice({ key: "notice.ended" });
376
+ revision = status.revision;
377
+ }
378
+ };
379
+ (async () => {
380
+ try {
381
+ binding = await call("create", { ownerId: crypto.randomUUID() });
382
+ if (disposed) {
383
+ call("close", { bindingId: binding.bindingId }).catch(() => {});
384
+ return;
385
+ }
386
+ generation.current = binding.bindingId;
387
+ setView(binding);
388
+ await watch(binding.bindingId);
389
+ } catch (error) {
390
+ fail(error);
391
+ }
392
+ })();
393
+ return () => {
394
+ tab.signal.removeEventListener("abort", close);
395
+ close();
396
+ };
397
+ }, [
398
+ bus,
399
+ call,
400
+ reveal,
401
+ tab.id,
402
+ tab.signal,
403
+ attempt
404
+ ]);
405
+ (0, react.useEffect)(() => {
406
+ if (view === void 0 || state.phase !== "ready" || draining.current || state.queue.length === 0) return;
407
+ const { bindingId } = view;
408
+ draining.current = true;
409
+ const drain = async () => {
410
+ for (let file = bus.take(); file !== void 0; file = bus.take()) {
411
+ try {
412
+ await call("open", {
413
+ bindingId,
414
+ address: file.address,
415
+ line: file.line
416
+ });
417
+ if (generation.current !== bindingId) return;
418
+ if (visible.current) frame.current?.focus();
419
+ } catch (error) {
420
+ if (generation.current !== bindingId) return;
421
+ if (!(error instanceof RpcFailure)) bus.notice(noticeOf(error));
422
+ else if (FINAL_ERRORS.has(error.code)) {
423
+ bus.notice(noticeOf(error));
424
+ return bus.phase("error");
425
+ }
426
+ }
427
+ if (generation.current !== bindingId || bus.getSnapshot().phase !== "ready") return;
428
+ }
429
+ };
430
+ drain().finally(() => {
431
+ draining.current = false;
432
+ if (generation.current !== void 0) setDrains((value) => value + 1);
433
+ });
434
+ }, [
435
+ view,
436
+ state.phase,
437
+ state.queue,
438
+ bus,
439
+ call,
440
+ drains
441
+ ]);
442
+ const ended = FINAL_PHASES.has(state.phase);
443
+ const notice = state.notice === void 0 ? void 0 : t(state.notice.key, state.notice.params);
444
+ const reconnectButton = /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
445
+ type: "button",
446
+ style: styles.button,
447
+ onClick: () => reconnect(),
448
+ children: t("reconnect")
449
+ });
450
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
451
+ "data-dsh-code-server": state.phase,
452
+ style: styles.page,
453
+ children: view === void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
454
+ role: ended ? "alert" : "status",
455
+ style: styles.panel,
456
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: notice ?? t("loading") }), ended && reconnectButton]
457
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [notice !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
458
+ role: ended ? "alert" : "status",
459
+ style: styles.banner,
460
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
461
+ style: styles.text,
462
+ children: notice
463
+ }), ended ? reconnectButton : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
464
+ type: "button",
465
+ "aria-label": t("dismiss"),
466
+ title: t("dismiss"),
467
+ style: styles.button,
468
+ onClick: () => bus.notice(void 0),
469
+ children: "×"
470
+ })]
471
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("iframe", {
472
+ ref: frame,
473
+ src: view.url,
474
+ title: t("frame", { cwd: view.cwd }),
475
+ sandbox: SANDBOX,
476
+ allow: "clipboard-read; clipboard-write",
477
+ referrerPolicy: "no-referrer",
478
+ style: styles.frame
479
+ }, view.bindingId)] })
480
+ });
481
+ }
482
+ function noticeOf(error) {
483
+ const message = error instanceof Error ? error.message : String(error);
484
+ const key = error instanceof RpcFailure ? `error.${error.code}` : "error.unexpected";
485
+ return {
486
+ key: isMessageKey(key) ? key : "error.unexpected",
487
+ params: { message }
488
+ };
489
+ }
490
+ const styles = {
491
+ page: {
492
+ height: "100%",
493
+ minHeight: 0,
494
+ display: "flex",
495
+ flexDirection: "column",
496
+ overflow: "hidden",
497
+ padding: 4,
498
+ boxSizing: "border-box"
499
+ },
500
+ panel: {
501
+ flex: 1,
502
+ display: "flex",
503
+ flexDirection: "column",
504
+ alignItems: "center",
505
+ justifyContent: "center",
506
+ gap: 12,
507
+ padding: 16,
508
+ textAlign: "center",
509
+ overflowWrap: "anywhere"
510
+ },
511
+ banner: {
512
+ display: "flex",
513
+ alignItems: "center",
514
+ gap: 8,
515
+ padding: "4px 8px",
516
+ fontSize: 12,
517
+ borderBottom: "1px solid var(--dsw-alias-border-l3, #888)"
518
+ },
519
+ text: {
520
+ flex: 1,
521
+ minWidth: 0,
522
+ overflowWrap: "anywhere"
523
+ },
524
+ button: {
525
+ font: "inherit",
526
+ padding: "3px 8px",
527
+ border: "1px solid var(--dsw-alias-border-l3, #888)",
528
+ borderRadius: 5,
529
+ background: "transparent",
530
+ color: "inherit",
531
+ cursor: "pointer"
532
+ },
533
+ frame: {
534
+ display: "block",
535
+ flex: 1,
536
+ width: "100%",
537
+ minHeight: 0,
538
+ border: 0,
539
+ borderRadius: "var(--dsw-radius-sm, 8px)",
540
+ colorScheme: "normal"
541
+ }
542
+ };
543
+ // src/client/file-redirect.tsx
544
+ /**
545
+ * Claims text-file links and hands them to the session's Code page. DSH has no
546
+ * hook before a file tab opens, so this short-lived tab replaces itself with the
547
+ * Code page, or closes after revealing an open one.
548
+ */
549
+ function FileRedirect({ bus, findCode, reveal, isCurrent, t, useTabInfo }) {
550
+ const { tab } = useTabInfo();
551
+ (0, react.useLayoutEffect)(() => {
552
+ if (!isCurrent() || !tab.visible) return;
553
+ const { address, params, revision } = tab.navigation;
554
+ bus.enqueue(`${tab.id}:${revision}`, {
555
+ address,
556
+ line: params?.line
557
+ });
558
+ const code = findCode();
559
+ if (code === void 0) return tab.actions.openTab(CODE_KIND, { replaceTab: true });
560
+ reveal(code.tabId);
561
+ tab.actions.close();
562
+ }, [
563
+ bus,
564
+ findCode,
565
+ reveal,
566
+ isCurrent,
567
+ tab.id,
568
+ tab.navigation.revision,
569
+ tab.visible
570
+ ]);
571
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
572
+ style: { padding: 16 },
573
+ children: t("opening")
574
+ });
575
+ }
576
+ // src/client/locales.ts
577
+ /** DSH locale namespace of this plugin's messages (see ../messages.ts). */
578
+ const NAMESPACE = "langify.codeServer";
579
+ // ../../node_modules/.pnpm/@deepseek-ai+dsh-util-workspace-path@0.2.1-rc.3_@deepseek-ai+cordis@4.0.5/node_modules/@deepseek-ai/dsh-util-workspace-path/lib/index.js
580
+ /**
581
+ * The `dsh-resource://file/…` address grammar: how a file is named across the
582
+ * Sidebar and the resource model, built and parsed without touching a
583
+ * filesystem.
584
+ * @module
585
+ */
586
+ /** The scheme and type every file address opens with. */
587
+ const FILE_ADDRESS_PREFIX = "dsh-resource://file/";
588
+ /** Whether a decoded first path segment is a Windows drive (`C:`). */
589
+ function isDriveSegment(segment) {
590
+ return segment !== void 0 && /^[A-Za-z]:$/.test(segment);
591
+ }
592
+ /**
593
+ * Read a file address back into its parts without resolving `.` or `..`.
594
+ * Query and fragment suffixes are ignored; encoded path segments are decoded.
595
+ * @param address - a candidate address.
596
+ * @returns the parts, or `undefined` when the string is not a `dsh-resource://file/` URI in a known scope with a path, or a segment is not validly encoded.
597
+ */
598
+ function parseFileAddress(address) {
599
+ try {
600
+ if (!address.startsWith(FILE_ADDRESS_PREFIX)) return void 0;
601
+ const end = address.search(/[?#]/);
602
+ const [scope, ...rest] = address.slice(20, end === -1 ? void 0 : end).split("/");
603
+ if (scope === "session") {
604
+ const [id, ...segments] = rest;
605
+ if (id === void 0 || id === "" || segments.length === 0) return void 0;
606
+ return {
607
+ scope,
608
+ sessionId: decodeURIComponent(id),
609
+ path: segments.map(decodeURIComponent).join("/")
610
+ };
611
+ }
612
+ if (scope === "absolute") {
613
+ const unc = rest[0] === "" && rest.length > 1;
614
+ const segments = (unc ? rest.slice(1) : rest).map(decodeURIComponent);
615
+ if (segments.length === 0 || segments[0] === "") return void 0;
616
+ if (unc) return {
617
+ scope,
618
+ path: `//${segments.join("/")}`
619
+ };
620
+ return {
621
+ scope,
622
+ path: isDriveSegment(segments[0]) ? segments.join("/") : `/${segments.join("/")}`
623
+ };
624
+ }
625
+ return;
626
+ } catch {
627
+ return;
628
+ }
629
+ }
630
+ // src/client/text-files.ts
631
+ const TEXT_EXTENSIONS = new Set("txt text md mdx rst adoc log nix rs toml lock json jsonc json5 yaml yml xml html htm css scss sass less js jsx ts tsx mjs cjs mts cts py pyi sh bash zsh fish sql go java kt kts c h cpp cc cxx hpp cs rb php lua vim el clj cljs ex exs erl hrl ini conf cfg env properties service socket timer target path desktop diff patch csv tsv graphql proto".split(" "));
632
+ const TEXT_NAMES = new Set("readme license licence copying notice makefile gnumakefile dockerfile containerfile justfile gemfile rakefile procfile .gitignore .gitattributes .gitmodules .editorconfig .npmrc .nvmrc .env .bashrc .zshrc".split(" "));
633
+ /** Whether a `dsh-resource://file/...` address names a text file Code should open. */
634
+ function isTextFileAddress(address) {
635
+ const name = parseFileAddress(address)?.path.split("/").pop()?.toLowerCase();
636
+ if (!name) return false;
637
+ const extension = name.includes(".") ? name.slice(name.lastIndexOf(".") + 1) : "";
638
+ return TEXT_NAMES.has(name) || TEXT_EXTENSIONS.has(extension) || name.startsWith(".env.");
639
+ }
640
+ // src/client/index.tsx
641
+ const inject = [
642
+ "connection",
643
+ "locale",
644
+ "slots",
645
+ "sidebarRight",
646
+ "sidebarRightTabs"
647
+ ];
648
+ const PAGE_ID = "langify-code-server/page";
649
+ const FILE_ID = "langify-code-server/file";
650
+ function apply(ctx) {
651
+ ctx.effect(() => ctx.locale.register(NAMESPACE, {
652
+ en,
653
+ zh
654
+ }), "code-server: dictionaries");
655
+ ctx.effect(() => ctx.locale.register(NAMESPACE, "ja", ja), "code-server: Japanese dictionary");
656
+ const t = ctx.locale.bind(NAMESPACE);
657
+ const faces = /* @__PURE__ */ new Map();
658
+ const face = (sessionId) => {
659
+ let existing = faces.get(sessionId);
660
+ if (existing === void 0) {
661
+ existing = createFace(ctx, sessionId);
662
+ faces.set(sessionId, existing);
663
+ }
664
+ return existing;
665
+ };
666
+ ctx.effect(() => ctx.sidebarRightTabs.register({
667
+ id: PAGE_ID,
668
+ kind: CODE_KIND,
669
+ title: () => t("title"),
670
+ keepMounted: true,
671
+ guide: [{
672
+ id: "open-code",
673
+ order: 25,
674
+ title: () => t("title"),
675
+ description: () => t("guide")
676
+ }]
677
+ }), "code-server: page type");
678
+ ctx.effect(() => ctx.sidebarRightTabs.register({
679
+ id: FILE_ID,
680
+ kind: `${CODE_KIND}-file`,
681
+ patterns: ["dsh-resource://file/**"],
682
+ priority: "extension",
683
+ title: () => t("title"),
684
+ canOpen: isTextFileAddress
685
+ }), "code-server: file-link type");
686
+ ctx.effect(() => ctx.slots.inject("sidebar.right.pane.tab", () => ctx.slots.register({
687
+ name: "sidebar.right.pane.tab",
688
+ key: PAGE_ID,
689
+ inject: face,
690
+ locale: NAMESPACE
691
+ }, CodePage)), "code-server: page");
692
+ ctx.effect(() => ctx.slots.inject("sidebar.right.pane.tab", () => ctx.slots.register({
693
+ name: "sidebar.right.pane.tab",
694
+ key: FILE_ID,
695
+ inject: face,
696
+ locale: NAMESPACE
697
+ }, FileRedirect)), "code-server: file-link redirect");
698
+ ctx.effect(() => ctx.sidebarRight.registerCloseHandler(CODE_KIND, (sessionId, tab) => faces.get(sessionId)?.bus.release(tab.id)), "code-server: close handler");
699
+ ctx.effect(() => () => {
700
+ for (const { bus } of faces.values()) bus.phase("closed");
701
+ faces.clear();
702
+ }, "code-server: sessions");
703
+ }
704
+ exports.apply = apply;
705
+ exports.inject = inject;
706
+ return module.exports;
707
+ }
708
+ });