dsh-remote-local-plugin 0.0.0-stage → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +85 -2
  3. package/assets/dsh-remote-agent-0.2.1.tgz +0 -0
  4. package/cordis.patch.yml +50 -0
  5. package/lib/client.js +1511 -0
  6. package/lib/client.js.map +7 -0
  7. package/lib/index.js +4565 -0
  8. package/lib/types/config.d.ts +94 -0
  9. package/lib/types/credentials/proxy.d.ts +40 -0
  10. package/lib/types/errors.d.ts +23 -0
  11. package/lib/types/host-api/contract.d.ts +217 -0
  12. package/lib/types/host-api/handler.d.ts +15 -0
  13. package/lib/types/host-api/terminal.d.ts +15 -0
  14. package/lib/types/hosts/id.d.ts +16 -0
  15. package/lib/types/hosts/store.d.ts +34 -0
  16. package/lib/types/hosts/types.d.ts +85 -0
  17. package/lib/types/index.d.ts +19 -0
  18. package/lib/types/mirror/service.d.ts +87 -0
  19. package/lib/types/mirror/test-visibility.d.ts +23 -0
  20. package/lib/types/remote/directory.d.ts +19 -0
  21. package/lib/types/remote/files.d.ts +14 -0
  22. package/lib/types/remote/local-directory.d.ts +11 -0
  23. package/lib/types/remote/provisioner.d.ts +32 -0
  24. package/lib/types/remote/terminal.d.ts +55 -0
  25. package/lib/types/rpc/contract.d.ts +114 -0
  26. package/lib/types/rpc/handler.d.ts +12 -0
  27. package/lib/types/service.d.ts +92 -0
  28. package/lib/types/ssh/probe.d.ts +33 -0
  29. package/lib/types/ssh/target.d.ts +24 -0
  30. package/lib/types/ssh/tunnel.d.ts +50 -0
  31. package/lib/types/tools.d.ts +19 -0
  32. package/node_modules/dsh-remote-terminal-controller/README.md +9 -0
  33. package/node_modules/dsh-remote-terminal-controller/cordis.patch.yml +8 -0
  34. package/node_modules/dsh-remote-terminal-controller/lib/client.js +1367 -0
  35. package/node_modules/dsh-remote-terminal-controller/lib/client.js.map +7 -0
  36. package/node_modules/dsh-remote-terminal-controller/lib/index.js +3 -0
  37. package/node_modules/dsh-remote-terminal-controller/package.json +59 -0
  38. package/package.json +146 -4
@@ -0,0 +1,1367 @@
1
+ window.__ModuleLoader__.load({
2
+ id: "dsh-remote-terminal-controller",
3
+ factory: (require) => {
4
+ var module = { exports: {} };
5
+ var exports = module.exports;
6
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
7
+ let _deepseek_ai_cordis = require("@deepseek-ai/cordis");
8
+ let _deepseek_ai_dsh_client_store = require("@deepseek-ai/dsh-client-store");
9
+ let _deepseek_ai_dsh_api_gateway_client = require("@deepseek-ai/dsh-api-gateway/client");
10
+ //#region ../../typert/protocol/lib/index.js
11
+ /** The one Remote failure class shared by owners, the Gateway, and consumers. */
12
+ /**
13
+ * One Remote call failure: a real Error carrying its stable code and typed
14
+ * details. Owners throw it at the failure point; the Host Gateway encodes it
15
+ * onto the wire unchanged; the Client face rebuilds an instance for the
16
+ * `RemoteResult` error branch, so `throw result.error` keeps throw semantics.
17
+ * Discrimination is always by `code`, never by instanceof.
18
+ */
19
+ var RemoteError = class extends Error {
20
+ code;
21
+ details;
22
+ /** Structural marker: cross-realm/bundle identification never uses instanceof. */
23
+ isDSHRemoteError = true;
24
+ /**
25
+ * @param code - stable failure code declared in {@link RemoteErrorDetailsMap}.
26
+ * @param message - human diagnostic carried across the wire.
27
+ * @param details - structured payload typed by the code.
28
+ * @param options - standard Error options (`cause` survives in-process only).
29
+ */
30
+ constructor(code, message, details, options) {
31
+ super(message, options);
32
+ this.code = code;
33
+ this.details = details;
34
+ this.name = "RemoteError";
35
+ }
36
+ };
37
+ /**
38
+ * Structurally identify a RemoteError thrown across module or realm copies of
39
+ * this class. Mechanism-internal: the Gateway and test assertions use it;
40
+ * business code receives typed failures and never needs it.
41
+ * @param value - a caught value.
42
+ * @returns the failure when the marker matches, otherwise undefined.
43
+ */
44
+ function remoteErrorOf(value) {
45
+ if (typeof value === "object" && value !== null && value.isDSHRemoteError === true && typeof value.code === "string") return value;
46
+ }
47
+ //#endregion
48
+ //#region lib/types/client/shell-preference.js
49
+ /** Browser-local shell preference; Host discovery decides whether the saved path is usable. */
50
+ const KEY = "dsh.terminal.shell";
51
+ /**
52
+ * Read the browser preference.
53
+ * @returns the last selected shell path, or null when storage is unavailable.
54
+ */
55
+ function preferredShell() {
56
+ try {
57
+ return typeof localStorage === "undefined" ? null : localStorage.getItem(KEY);
58
+ } catch (_storageUnavailable) {
59
+ return null;
60
+ }
61
+ }
62
+ /**
63
+ * Remember the selected shell without making storage a startup dependency.
64
+ * @param path - verified executable path offered by the Host.
65
+ */
66
+ function rememberShell(path) {
67
+ try {
68
+ if (typeof localStorage !== "undefined") localStorage.setItem(KEY, path);
69
+ } catch (_storageUnavailable) {}
70
+ }
71
+ //#endregion
72
+ //#region ../../util/deque/lib/index.js
73
+ /**
74
+ * Zero-dependency circular deque for queues that retain entries across asynchronous work.
75
+ * @module @deepseek-ai/dsh-deque
76
+ */
77
+ const MIN_CAPACITY = 16;
78
+ /**
79
+ * A circular deque with amortized constant-time insertion and removal.
80
+ * Removed entries are cleared immediately, and sparse storage shrinks after
81
+ * the live entry count reaches one quarter of its capacity.
82
+ */
83
+ var Deque = class {
84
+ buffer = new Array(MIN_CAPACITY);
85
+ head = 0;
86
+ count = 0;
87
+ /** Number of entries available to remove. */
88
+ get size() {
89
+ return this.count;
90
+ }
91
+ /**
92
+ * Append one entry after the current tail.
93
+ * @param value - entry to append.
94
+ */
95
+ pushBack(value) {
96
+ this.ensureCapacity();
97
+ const tail = this.head + this.count;
98
+ this.buffer[tail < this.buffer.length ? tail : tail - this.buffer.length] = value;
99
+ this.count += 1;
100
+ }
101
+ /**
102
+ * Insert one entry before the current head.
103
+ * @param value - entry to prepend.
104
+ */
105
+ pushFront(value) {
106
+ this.ensureCapacity();
107
+ this.head = this.head === 0 ? this.buffer.length - 1 : this.head - 1;
108
+ this.buffer[this.head] = value;
109
+ this.count += 1;
110
+ }
111
+ /**
112
+ * Remove the current head entry and clear its retained reference.
113
+ * Callers whose element type includes `undefined` use {@link size} to
114
+ * distinguish an empty deque from an `undefined` entry.
115
+ * @returns the removed entry, or `undefined` when the deque is empty.
116
+ */
117
+ popFront() {
118
+ if (this.count === 0) return void 0;
119
+ const value = this.buffer[this.head];
120
+ this.buffer[this.head] = void 0;
121
+ this.head += 1;
122
+ if (this.head === this.buffer.length) this.head = 0;
123
+ this.count -= 1;
124
+ this.compact();
125
+ return value;
126
+ }
127
+ /** Drop every entry and release the current backing storage. */
128
+ clear() {
129
+ this.buffer = new Array(MIN_CAPACITY);
130
+ this.head = 0;
131
+ this.count = 0;
132
+ }
133
+ ensureCapacity() {
134
+ if (this.count < this.buffer.length) return;
135
+ this.resize(this.buffer.length * 2);
136
+ }
137
+ compact() {
138
+ if (this.count === 0) {
139
+ this.head = 0;
140
+ return;
141
+ }
142
+ if (this.buffer.length > MIN_CAPACITY && this.count <= this.buffer.length / 4) this.resize(Math.max(MIN_CAPACITY, this.buffer.length / 2));
143
+ }
144
+ resize(capacity) {
145
+ const next = new Array(capacity);
146
+ let source = this.head;
147
+ for (let index = 0; index < this.count; index += 1) {
148
+ next[index] = this.buffer[source];
149
+ source += 1;
150
+ if (source === this.buffer.length) source = 0;
151
+ }
152
+ this.buffer = next;
153
+ this.head = 0;
154
+ }
155
+ };
156
+ //#endregion
157
+ //#region lib/types/client/model.js
158
+ /** React-free browser terminal state and reconnecting Remote-stream ownership. */
159
+ var TerminalViewError = class extends RemoteError {
160
+ constructor(issue, message = issue) {
161
+ super("terminal/view", message, { issue });
162
+ }
163
+ };
164
+ /** A view survives DOM unmount; its process only ends on explicit close. */
165
+ var TerminalView = class {
166
+ sessionId;
167
+ remote;
168
+ gateway;
169
+ inventory;
170
+ id;
171
+ createWhenMissing;
172
+ shellPath;
173
+ retain;
174
+ /** Observable controls, process metadata and the next screen update awaiting acknowledgement. */
175
+ state = (0, _deepseek_ai_dsh_client_store.createSnapshotStore)({
176
+ phase: "idle",
177
+ writable: false
178
+ });
179
+ lifetime = new AbortController();
180
+ stream;
181
+ mounted = false;
182
+ attachment;
183
+ pendingRender;
184
+ revision = 0;
185
+ creation;
186
+ loading;
187
+ closing;
188
+ detaching = /* @__PURE__ */ new Set();
189
+ /**
190
+ * @param sessionId - Session owning the terminal.
191
+ * @param remote - typed terminal Remote operations.
192
+ * @param gateway - reconnecting stream factory.
193
+ * @param inventory - Session-owned terminal list shared by all views.
194
+ * @param id - Host terminal identity, reused when recovering an item from its Session list.
195
+ * @param createWhenMissing - allow allocation only for a new tab, never a listed terminal.
196
+ * @param shellPath - explicit shell chosen at the guide; omission uses the remembered available shell.
197
+ * @param retain - window hold acknowledgement required before output attachment.
198
+ */
199
+ constructor(sessionId, remote, gateway, inventory, id, createWhenMissing = true, shellPath, retain) {
200
+ this.sessionId = sessionId;
201
+ this.remote = remote;
202
+ this.gateway = gateway;
203
+ this.inventory = inventory;
204
+ this.id = id;
205
+ this.createWhenMissing = createWhenMissing;
206
+ this.shellPath = shellPath;
207
+ this.retain = retain;
208
+ }
209
+ /**
210
+ * Attach the DOM lifetime, starting the chosen shell or reconnecting the saved process.
211
+ * @returns a detach callback that leaves the terminal process alive.
212
+ */
213
+ mount() {
214
+ this.mounted = true;
215
+ const state = this.state.getSnapshot();
216
+ if (state.phase === "idle") this.refresh();
217
+ else if (state.info !== void 0 && state.phase !== "failed" && state.phase !== "closed") this.connect();
218
+ return () => {
219
+ this.mounted = false;
220
+ this.detach();
221
+ };
222
+ }
223
+ /**
224
+ * Start or explicitly retry this tab, sharing Session discovery with other views.
225
+ * New identities allocate directly; confirmed or restored terminals cannot be replaced.
226
+ * @returns after environment lookup and creation or recovery settle.
227
+ */
228
+ refresh() {
229
+ if (this.creation !== void 0) return this.creation;
230
+ if (this.loading !== void 0) return this.loading;
231
+ if (this.closing !== void 0 || this.lifetime.signal.aborted) return Promise.resolve();
232
+ const retry = this.state.getSnapshot().phase !== "idle";
233
+ this.patch({
234
+ phase: "loading",
235
+ error: void 0,
236
+ issue: void 0
237
+ });
238
+ this.loading = (async () => {
239
+ const [environment, available] = await Promise.all([this.remote.environment(this.sessionId, this.lifetime.signal), this.createWhenMissing ? void 0 : this.inventory.read(retry)]);
240
+ if (this.stopped()) return;
241
+ this.patch({ environment: valueOf(environment) });
242
+ const info = available?.get(this.id);
243
+ if (info !== void 0) this.adopt(info);
244
+ else if (this.createWhenMissing) {
245
+ let path = this.shellPath;
246
+ if (path === void 0) {
247
+ const shells = valueOf(await this.remote.shells(this.sessionId, this.lifetime.signal));
248
+ const previous = preferredShell();
249
+ path = shells.find((shell) => shell.path === previous)?.path ?? shells[0]?.path;
250
+ }
251
+ if (this.stopped()) return;
252
+ if (path !== void 0) rememberShell(path);
253
+ await this.create(valueOf(environment), path);
254
+ } else throw new TerminalViewError("missingTerminal");
255
+ })().catch((error) => {
256
+ this.fail(error);
257
+ }).finally(() => {
258
+ this.loading = void 0;
259
+ });
260
+ return this.loading;
261
+ }
262
+ stopped() {
263
+ return this.lifetime.signal.aborted || this.closing !== void 0;
264
+ }
265
+ async create(environment, shellPath) {
266
+ this.patch({
267
+ phase: "creating",
268
+ error: void 0,
269
+ issue: void 0
270
+ });
271
+ this.creation = (async () => {
272
+ const info = valueOf(await this.remote.create(this.sessionId, {
273
+ id: this.id,
274
+ ...shellPath === void 0 ? {} : { shellPath },
275
+ cols: Math.min(80, environment.maxCols),
276
+ rows: Math.min(24, environment.maxRows)
277
+ }, this.lifetime.signal));
278
+ if (!this.lifetime.signal.aborted) {
279
+ this.inventory.update(info);
280
+ this.adopt(info);
281
+ }
282
+ })().catch((error) => {
283
+ this.fail(error);
284
+ }).finally(() => {
285
+ this.creation = void 0;
286
+ });
287
+ await this.creation;
288
+ }
289
+ adopt(info) {
290
+ this.createWhenMissing = false;
291
+ this.patch({
292
+ info,
293
+ title: info.title
294
+ });
295
+ if (this.retain === void 0) {
296
+ if (this.mounted && this.closing === void 0) this.connect();
297
+ return;
298
+ }
299
+ this.retain(this.lifetime.signal).then(() => {
300
+ if (this.mounted && this.closing === void 0) this.connect();
301
+ }).catch((error) => {
302
+ if (!this.stopped()) this.fail(error);
303
+ });
304
+ }
305
+ /** Reattach with a fresh screen and regain input control. */
306
+ connect() {
307
+ const info = this.state.getSnapshot().info;
308
+ if (info === void 0 || !this.mounted || this.closing !== void 0 || this.lifetime.signal.aborted) return;
309
+ this.detach();
310
+ const stream = this.gateway.$stream({
311
+ name: "Browser terminal output",
312
+ open: async function* (signal) {
313
+ await this.retain?.(signal);
314
+ signal.throwIfAborted();
315
+ const handle = this.remote.attach(this.sessionId, info.id, signal);
316
+ const attachment = {
317
+ handle,
318
+ signal,
319
+ sequence: 0,
320
+ acknowledged: 0,
321
+ pending: /* @__PURE__ */ new Map(),
322
+ queued: new Deque(),
323
+ window: void 0,
324
+ bytes: 0,
325
+ bufferedBytes: 0,
326
+ inFlightBytes: 0
327
+ };
328
+ this.attachment = attachment;
329
+ try {
330
+ for await (const frame of handle) {
331
+ if (frame.type === "snapshot") attachment.window = frame.inputWindow;
332
+ yield frame;
333
+ }
334
+ } finally {
335
+ if (this.attachment === attachment) this.attachment = void 0;
336
+ }
337
+ }.bind(this),
338
+ ended: () => new TerminalViewError("attachmentEnded"),
339
+ carrierFailed: () => {
340
+ if (this.stream === stream) this.patch({
341
+ phase: "disconnected",
342
+ writable: false
343
+ });
344
+ }
345
+ });
346
+ this.stream = stream;
347
+ this.patch({
348
+ phase: "connecting",
349
+ writable: false,
350
+ error: void 0,
351
+ issue: void 0,
352
+ render: void 0
353
+ });
354
+ this.consume(stream);
355
+ }
356
+ /**
357
+ * Release the next stream item after xterm has parsed this frame.
358
+ * @param revision - locally delivered render revision.
359
+ */
360
+ acknowledge(revision) {
361
+ if (this.pendingRender?.revision !== revision) return;
362
+ this.pendingRender.resolve();
363
+ this.pendingRender = void 0;
364
+ }
365
+ /**
366
+ * Queue input within the raw UTF-8 and encoded-frame budgets, waiting for carrier capacity.
367
+ * @param data - input from the terminal emulator; never replayed after disconnection.
368
+ */
369
+ write(data) {
370
+ const state = this.state.getSnapshot();
371
+ const attachment = this.attachment;
372
+ if (!state.writable || state.info === void 0 || attachment === void 0 || attachment.signal.aborted) return;
373
+ if (data.length === 0) return;
374
+ const bytes = new TextEncoder().encode(data).byteLength;
375
+ if (attachment.bytes + bytes > (state.environment?.maxInputBytes ?? 0)) {
376
+ this.failInput(new TerminalViewError("inputFull"));
377
+ return;
378
+ }
379
+ this.send(attachment, {
380
+ type: "write",
381
+ sequence: attachment.sequence + 1,
382
+ data
383
+ }, bytes);
384
+ }
385
+ /**
386
+ * Resize only from the currently writable view, ordered with keyboard input.
387
+ * @param cols - measured column count.
388
+ * @param rows - measured row count.
389
+ */
390
+ resize(cols, rows) {
391
+ const state = this.state.getSnapshot();
392
+ const attachment = this.attachment;
393
+ if (!state.writable || state.info === void 0 || attachment === void 0 || attachment.signal.aborted) return;
394
+ if (state.info.cols === cols && state.info.rows === rows) return;
395
+ cols = Math.min(cols, state.environment?.maxCols ?? cols);
396
+ rows = Math.min(rows, state.environment?.maxRows ?? rows);
397
+ this.send(attachment, {
398
+ type: "resize",
399
+ sequence: attachment.sequence + 1,
400
+ cols,
401
+ rows
402
+ }, 0);
403
+ }
404
+ send(attachment, command, bytes) {
405
+ const frameBytes = new TextEncoder().encode(JSON.stringify(command)).byteLength + (attachment.window?.frameOverheadBytes ?? 0);
406
+ if (attachment.bufferedBytes + frameBytes > (this.state.getSnapshot().environment?.maxBufferedBytes ?? 0) || attachment.window !== void 0 && frameBytes > attachment.window.maxBytes) {
407
+ this.failInput(new TerminalViewError("inputFull"));
408
+ return;
409
+ }
410
+ attachment.queued.pushBack({
411
+ command,
412
+ inputBytes: bytes,
413
+ frameBytes
414
+ });
415
+ attachment.sequence = command.sequence;
416
+ attachment.bytes += bytes;
417
+ attachment.bufferedBytes += frameBytes;
418
+ this.flushInput(attachment);
419
+ }
420
+ flushInput(attachment) {
421
+ while (this.attachment === attachment && !attachment.signal.aborted && this.state.getSnapshot().writable) {
422
+ const next = attachment.queued.popFront();
423
+ if (next === void 0) return;
424
+ if (attachment.window !== void 0 && attachment.inFlightBytes + next.frameBytes > attachment.window.maxBytes) {
425
+ attachment.queued.pushFront(next);
426
+ return;
427
+ }
428
+ try {
429
+ attachment.handle.send(next.command);
430
+ } catch (error) {
431
+ this.failInput(error);
432
+ return;
433
+ }
434
+ attachment.pending.set(next.command.sequence, {
435
+ inputBytes: next.inputBytes,
436
+ frameBytes: next.frameBytes
437
+ });
438
+ attachment.inFlightBytes += next.frameBytes;
439
+ }
440
+ }
441
+ failInput(error) {
442
+ this.fail(error);
443
+ this.detach();
444
+ }
445
+ /**
446
+ * Update the Host terminal's display name.
447
+ * @param title - user-entered terminal title.
448
+ * @returns after the rename settles and its result is reflected in view state.
449
+ */
450
+ async rename(title) {
451
+ if (title.trim() === this.state.getSnapshot().title || this.lifetime.signal.aborted) return;
452
+ try {
453
+ valueOf(await this.remote.rename(this.sessionId, this.id, title));
454
+ if (this.stopped()) return;
455
+ const current = this.state.getSnapshot().info;
456
+ if (current !== void 0) this.inventory.update({
457
+ ...current,
458
+ title: title.trim()
459
+ });
460
+ this.patch({
461
+ ...current === void 0 ? {} : { info: {
462
+ ...current,
463
+ title: title.trim()
464
+ } },
465
+ title: title.trim()
466
+ });
467
+ } catch (error) {
468
+ this.fail(error);
469
+ }
470
+ }
471
+ /**
472
+ * Explicitly terminate this view's process independently of its DOM lifetime.
473
+ * @returns after Host process cleanup succeeds; failures remain retryable by the owner.
474
+ */
475
+ close() {
476
+ if (this.closing !== void 0) return this.closing;
477
+ this.patch({
478
+ phase: "closing",
479
+ writable: false,
480
+ error: void 0,
481
+ issue: void 0
482
+ });
483
+ this.detach();
484
+ this.closing = (async () => {
485
+ await this.creation;
486
+ valueOf(await this.remote.close(this.sessionId, this.id));
487
+ this.inventory.remove(this.id);
488
+ this.detach();
489
+ this.patch({
490
+ phase: "closed",
491
+ writable: false
492
+ });
493
+ })().catch((error) => {
494
+ this.closing = void 0;
495
+ this.fail(error);
496
+ throw error;
497
+ });
498
+ return this.closing;
499
+ }
500
+ /**
501
+ * Stop Client work on plugin unload without closing Host terminals.
502
+ * @returns after active and previously detached stream iterators have closed.
503
+ */
504
+ async dispose() {
505
+ this.mounted = false;
506
+ this.lifetime.abort();
507
+ this.detach();
508
+ await Promise.all(this.detaching);
509
+ }
510
+ detach() {
511
+ const previous = this.stream;
512
+ this.stream = void 0;
513
+ this.attachment = void 0;
514
+ this.pendingRender?.resolve();
515
+ this.pendingRender = void 0;
516
+ if (previous !== void 0) {
517
+ const cleanup = previous.dispose().finally(() => {
518
+ this.detaching.delete(cleanup);
519
+ });
520
+ this.detaching.add(cleanup);
521
+ }
522
+ }
523
+ async consume(stream) {
524
+ let generation = 0;
525
+ let sequence = 0;
526
+ try {
527
+ for await (const item of stream) {
528
+ if (this.stream !== stream) return;
529
+ const frame = item.value;
530
+ if (generation !== item.generation) {
531
+ if (frame.type !== "snapshot") throw new TerminalViewError("invalidOutput", "Terminal output generation is missing its screen snapshot");
532
+ generation = item.generation;
533
+ sequence = frame.sequence;
534
+ item.accept();
535
+ } else if (frame.type === "output") {
536
+ if (frame.sequence !== sequence + 1) throw new TerminalViewError("invalidOutput", "Terminal output sequence has a gap");
537
+ sequence = frame.sequence;
538
+ } else if (frame.type === "snapshot") throw new TerminalViewError("invalidOutput", "Unexpected terminal screen snapshot");
539
+ if (frame.type === "ack") {
540
+ const attachment = this.attachment;
541
+ /* v8 ignore next -- the physical opener retains its handle until this consumer advances. */
542
+ if (attachment === void 0) throw new TerminalViewError("invalidOutput", "Terminal acknowledgement has no attachment");
543
+ const pending = attachment.pending.get(frame.sequence);
544
+ if (frame.sequence !== attachment.acknowledged + 1 || pending === void 0) throw new TerminalViewError("invalidOutput", "Terminal input acknowledgement is out of order");
545
+ attachment.acknowledged = frame.sequence;
546
+ attachment.bytes -= pending.inputBytes;
547
+ attachment.bufferedBytes -= pending.frameBytes;
548
+ attachment.inFlightBytes -= pending.frameBytes;
549
+ attachment.pending.delete(frame.sequence);
550
+ if (frame.refusal !== void 0) this.patch({ writable: false });
551
+ this.flushInput(attachment);
552
+ continue;
553
+ }
554
+ if (frame.type !== "output") {
555
+ this.inventory.update(frame.info);
556
+ this.patch({
557
+ info: frame.info,
558
+ title: frame.info.title,
559
+ phase: "connected",
560
+ writable: frame.info.state === "running" && frame.writable
561
+ });
562
+ }
563
+ if (frame.type !== "state") {
564
+ const revision = ++this.revision;
565
+ await new Promise((resolve) => {
566
+ this.pendingRender = {
567
+ revision,
568
+ resolve
569
+ };
570
+ const aborted = () => {
571
+ this.acknowledge(revision);
572
+ };
573
+ item.signal.addEventListener("abort", aborted, { once: true });
574
+ this.pendingRender.resolve = () => {
575
+ item.signal.removeEventListener("abort", aborted);
576
+ resolve();
577
+ };
578
+ this.patch({ render: {
579
+ revision,
580
+ frame
581
+ } });
582
+ if (item.signal.aborted) aborted();
583
+ });
584
+ }
585
+ }
586
+ } catch (error) {
587
+ if (this.stream === stream) if (this.state.getSnapshot().info?.state === "exited") this.patch({
588
+ phase: "closed",
589
+ writable: false
590
+ });
591
+ else this.fail(error);
592
+ }
593
+ }
594
+ patch(patch) {
595
+ if (this.lifetime.signal.aborted) return;
596
+ this.state.set({
597
+ ...this.state.getSnapshot(),
598
+ ...patch
599
+ });
600
+ }
601
+ fail(error) {
602
+ const failure = remoteErrorOf(error);
603
+ const issue = failure?.code === "terminal/view" ? failure.details.issue : failure?.code === "terminal/limit-reached" ? "terminalLimit" : failure?.code === "terminal/unavailable" ? "missingTerminal" : void 0;
604
+ if (!this.stopped() && issue === "missingTerminal") this.inventory.remove(this.id);
605
+ this.patch({
606
+ phase: error instanceof _deepseek_ai_dsh_api_gateway_client.RemoteStreamCarrierError ? "disconnected" : "failed",
607
+ writable: false,
608
+ issue,
609
+ error: error instanceof Error ? error.message : String(error)
610
+ });
611
+ }
612
+ };
613
+ function valueOf(result) {
614
+ if (!result.ok) throw result.error;
615
+ return result.value;
616
+ }
617
+ //#endregion
618
+ //#region ../../util/crypto/lib/index.js
619
+ /**
620
+ * Random v4 UUID, minted from `crypto.getRandomValues`.
621
+ * @returns the UUID string.
622
+ */
623
+ function randomUUID() {
624
+ const bytes = globalThis.crypto.getRandomValues(new Uint8Array(16));
625
+ const hex = Array.from(bytes, (byte, index) => {
626
+ return (index === 6 ? byte & 15 | 64 : index === 8 ? byte & 63 | 128 : byte).toString(16).padStart(2, "0");
627
+ }).join("");
628
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
629
+ }
630
+ //#endregion
631
+ //#region lib/types/client/close-requests.js
632
+ const PREFIX$1 = "dsh.terminal.close.v1.";
633
+ /** Each request has its own storage key, so other browser windows cannot overwrite its cleanup. */
634
+ var TerminalCloseRequests = class {
635
+ requests = /* @__PURE__ */ new Map();
636
+ constructor() {
637
+ try {
638
+ if (typeof localStorage === "undefined") return;
639
+ for (let index = 0; index < localStorage.length; index++) {
640
+ const key = localStorage.key(index);
641
+ if (key?.startsWith(PREFIX$1)) this.load(key);
642
+ }
643
+ } catch (error) {
644
+ console.error("Terminal cleanup recovery failed:", error);
645
+ }
646
+ }
647
+ /**
648
+ * Read cleanup work still awaiting Host confirmation.
649
+ * @returns unfinished requests owned by this browser instance.
650
+ */
651
+ pending() {
652
+ return [...this.requests.values()];
653
+ }
654
+ /**
655
+ * Retain cleanup across reload before removing a tab.
656
+ * @param request - close intent to save before removing its tab.
657
+ */
658
+ save(request) {
659
+ this.requests.set(request.id, request);
660
+ try {
661
+ if (typeof localStorage !== "undefined") localStorage.setItem(PREFIX$1 + request.id, JSON.stringify(request));
662
+ } catch (error) {
663
+ console.error("Terminal cleanup persistence failed:", error);
664
+ }
665
+ }
666
+ /**
667
+ * Forget confirmed cleanup in memory and browser storage.
668
+ * @param id - terminal whose Host cleanup succeeded.
669
+ */
670
+ remove(id) {
671
+ this.requests.delete(id);
672
+ try {
673
+ if (typeof localStorage !== "undefined") localStorage.removeItem(PREFIX$1 + id);
674
+ } catch (error) {
675
+ console.error("Terminal cleanup persistence failed:", error);
676
+ }
677
+ }
678
+ load(key) {
679
+ try {
680
+ const raw = localStorage.getItem(key);
681
+ if (raw === null) return;
682
+ const parsed = JSON.parse(raw);
683
+ if (!isRequest(parsed) || key !== PREFIX$1 + parsed.id) throw new Error("Invalid terminal cleanup request");
684
+ this.requests.set(parsed.id, parsed);
685
+ } catch (error) {
686
+ console.error("Terminal cleanup recovery failed:", error);
687
+ }
688
+ }
689
+ };
690
+ function isRequest(value) {
691
+ if (typeof value !== "object" || value === null) return false;
692
+ const request = value;
693
+ return typeof request.sessionId === "string" && request.sessionId.length > 0 && typeof request.id === "string" && /^[\w-]{1,128}$/u.test(request.id) && typeof request.title === "string";
694
+ }
695
+ //#endregion
696
+ //#region lib/types/client/retention.js
697
+ /** A stream acknowledgement gates output attachment for each physical connection. */
698
+ var TerminalWindowHold = class {
699
+ stream;
700
+ waiters = /* @__PURE__ */ new Set();
701
+ generation;
702
+ failure;
703
+ /**
704
+ * @param gateway - reconnecting stream owner.
705
+ * @param remote - typed terminal namespace.
706
+ * @param sessionId - saved layout's Session, without Agent activation.
707
+ * @param id - existing Host terminal.
708
+ */
709
+ constructor(gateway, remote, sessionId, id) {
710
+ this.stream = gateway.$stream({
711
+ name: "Browser terminal window hold",
712
+ open: (signal) => remote.retain(sessionId, id, signal),
713
+ ended: () => new RemoteError("terminal/unavailable", "Terminal hold ended", {})
714
+ });
715
+ this.consume();
716
+ }
717
+ /** Whether a terminal-domain failure ended this hold, allowing an explicit retry. */
718
+ get failed() {
719
+ return this.failure !== void 0;
720
+ }
721
+ /**
722
+ * Wait for an acknowledged current physical hold before following its screen.
723
+ * @param signal - output request or view lifetime.
724
+ * @returns after acknowledgement, or rejects on cancellation/unavailability.
725
+ */
726
+ async ready(signal) {
727
+ signal.throwIfAborted();
728
+ if (this.failure !== void 0) throw this.failure;
729
+ if (this.generation !== void 0 && !this.generation.aborted) return;
730
+ const waiting = Promise.withResolvers();
731
+ const abort = () => {
732
+ waiting.reject(signal.reason);
733
+ };
734
+ this.waiters.add(waiting);
735
+ signal.addEventListener("abort", abort, { once: true });
736
+ try {
737
+ await waiting.promise;
738
+ } finally {
739
+ this.waiters.delete(waiting);
740
+ signal.removeEventListener("abort", abort);
741
+ }
742
+ }
743
+ /**
744
+ * Release this window's stream and all acknowledgement waiters.
745
+ * @returns after the stream consumer closes.
746
+ */
747
+ dispose() {
748
+ this.reject(/* @__PURE__ */ new Error("Terminal window hold released"));
749
+ return this.stream.dispose();
750
+ }
751
+ async consume() {
752
+ try {
753
+ for await (const item of this.stream) {
754
+ item.accept();
755
+ this.generation = item.signal;
756
+ for (const waiter of this.waiters) waiter.resolve();
757
+ this.waiters.clear();
758
+ }
759
+ } catch (error) {
760
+ this.reject(error);
761
+ }
762
+ }
763
+ reject(error) {
764
+ this.failure = error instanceof Error ? error : new Error("Terminal window hold failed", { cause: error });
765
+ this.generation = void 0;
766
+ for (const waiter of this.waiters) waiter.reject(error);
767
+ this.waiters.clear();
768
+ }
769
+ };
770
+ //#endregion
771
+ //#region lib/types/client/bindings.js
772
+ const PREFIX = "dsh.terminal.binding.v1.";
773
+ /** Saved recovery targets keyed by Session and globally unique terminal content identity. */
774
+ var TerminalBindings = class {
775
+ memory = /* @__PURE__ */ new Map();
776
+ /**
777
+ * Read a saved target, retaining this window's value when storage is unavailable.
778
+ * @param sessionId - owning Session.
779
+ * @param contentId - terminal content identity, shared only by deliberate copies.
780
+ * @returns the existing Host identity, if one has been saved.
781
+ */
782
+ get(sessionId, contentId) {
783
+ const key = this.key(sessionId, contentId);
784
+ const known = this.memory.get(key);
785
+ if (known !== void 0) return known;
786
+ if (typeof localStorage === "undefined") return void 0;
787
+ try {
788
+ const raw = localStorage.getItem(key);
789
+ if (raw === null) return void 0;
790
+ const value = JSON.parse(raw);
791
+ if (typeof value !== "string" || !/^[\w-]{1,128}$/u.test(value)) return void 0;
792
+ this.memory.set(key, value);
793
+ return value;
794
+ } catch (_storageUnavailable) {
795
+ return;
796
+ }
797
+ }
798
+ /**
799
+ * Save an identity before its Host allocation begins.
800
+ * @param sessionId - owning Session.
801
+ * @param contentId - globally unique terminal content identity.
802
+ * @param id - existing or newly allocated Host identity.
803
+ */
804
+ set(sessionId, contentId, id) {
805
+ const key = this.key(sessionId, contentId);
806
+ this.memory.set(key, id);
807
+ if (typeof localStorage === "undefined") return;
808
+ try {
809
+ localStorage.setItem(key, JSON.stringify(id));
810
+ } catch (error) {
811
+ console.error("Terminal binding persistence failed:", error);
812
+ }
813
+ }
814
+ /**
815
+ * Remove this content's target after its close intent has been saved.
816
+ * @param sessionId - owning Session.
817
+ * @param contentId - closing terminal content identity.
818
+ */
819
+ delete(sessionId, contentId) {
820
+ const key = this.key(sessionId, contentId);
821
+ this.memory.delete(key);
822
+ if (typeof localStorage === "undefined") return;
823
+ try {
824
+ localStorage.removeItem(key);
825
+ } catch (error) {
826
+ console.error("Terminal binding cleanup failed:", error);
827
+ }
828
+ }
829
+ /** Release cached values when the Client service is disposed. */
830
+ clear() {
831
+ this.memory.clear();
832
+ }
833
+ key(sessionId, contentId) {
834
+ return PREFIX + JSON.stringify([sessionId, contentId]);
835
+ }
836
+ };
837
+ //#endregion
838
+ //#region lib/types/client/inventory.js
839
+ /** One Session's cached list, retained failures and in-flight mutation reconciliation. */
840
+ var TerminalInventory = class {
841
+ load;
842
+ state = { kind: "unloaded" };
843
+ items = /* @__PURE__ */ new Map();
844
+ pending = /* @__PURE__ */ new Set();
845
+ /** @param load - fetch the Session's retained Host terminals. */
846
+ constructor(load) {
847
+ this.load = load;
848
+ }
849
+ /**
850
+ * Read the cached list or join its single in-flight request.
851
+ * @param refresh - explicitly replace a settled result or failure; overlapping refreshes share one request.
852
+ * @returns current terminal metadata; failures remain cached until refresh or reset.
853
+ */
854
+ read(refresh = false) {
855
+ if (this.state.kind === "disposed") return Promise.reject(/* @__PURE__ */ new Error("Terminal inventory disposed"));
856
+ if (this.state.kind === "loading") return this.state.promise;
857
+ if (!refresh && this.state.kind === "ready") return Promise.resolve(this.items);
858
+ if (!refresh && this.state.kind === "failed") return Promise.reject(this.state.error);
859
+ const completed = Promise.withResolvers();
860
+ const request = {
861
+ kind: "loading",
862
+ promise: completed.promise,
863
+ updates: /* @__PURE__ */ new Map()
864
+ };
865
+ this.state = request;
866
+ this.pending.add(completed.promise);
867
+ this.loadRequest(request).then(completed.resolve, completed.reject);
868
+ completed.promise.then(() => {
869
+ this.pending.delete(completed.promise);
870
+ }, () => {
871
+ this.pending.delete(completed.promise);
872
+ });
873
+ return completed.promise;
874
+ }
875
+ async loadRequest(request) {
876
+ try {
877
+ const result = await this.load();
878
+ if (this.state !== request) return await this.read();
879
+ if (!result.ok) throw result.error;
880
+ const items = new Map(result.value.map((info) => [info.id, info]));
881
+ for (const [id, info] of request.updates) if (info === void 0) items.delete(id);
882
+ else items.set(id, info);
883
+ this.items = items;
884
+ this.state = { kind: "ready" };
885
+ return items;
886
+ } catch (error) {
887
+ if (this.state !== request) return this.read();
888
+ const failure = error instanceof Error ? error : new Error("Terminal discovery failed", { cause: error });
889
+ this.state = {
890
+ kind: "failed",
891
+ error: failure
892
+ };
893
+ throw failure;
894
+ }
895
+ }
896
+ /**
897
+ * Preserve acknowledged mutations and attachment metadata across a pending list response.
898
+ * @param info - latest confirmed terminal metadata.
899
+ */
900
+ update(info) {
901
+ if (this.state.kind === "disposed") return;
902
+ this.items.set(info.id, info);
903
+ if (this.state.kind === "loading") this.state.updates.set(info.id, info);
904
+ }
905
+ /**
906
+ * Remove a terminal after confirmed closure or unavailability.
907
+ * @param id - terminal identity.
908
+ */
909
+ remove(id) {
910
+ if (this.state.kind === "disposed") return;
911
+ this.items.delete(id);
912
+ if (this.state.kind === "loading") this.state.updates.set(id, void 0);
913
+ }
914
+ /**
915
+ * Replace discovery after a connection change; never discover a Session that has not needed a list.
916
+ * @returns the replacement lookup, or undefined for an unused or disposed inventory.
917
+ */
918
+ reset() {
919
+ if (this.state.kind === "unloaded" || this.state.kind === "disposed") return void 0;
920
+ this.state = { kind: "unloaded" };
921
+ return this.read();
922
+ }
923
+ /**
924
+ * Discard metadata and prevent late responses from publishing.
925
+ * @returns after all owned list requests settle.
926
+ */
927
+ async dispose() {
928
+ this.state = { kind: "disposed" };
929
+ this.items.clear();
930
+ await Promise.allSettled(this.pending);
931
+ }
932
+ };
933
+ //#endregion
934
+ //#region lib/types/client/index.js
935
+ /** Client terminal model service; views are keyed independently from Host terminal identities. */
936
+ /** Session and occurrence lookup, independent tab and terminal identities and background cleanup. */
937
+ var ClientTerminals = class extends _deepseek_ai_cordis.Service {
938
+ remote;
939
+ /** Failed cleanup tasks; successful and in-progress closes have no visible notification. */
940
+ closeFailures = (0, _deepseek_ai_dsh_client_store.createSnapshotStore)([]);
941
+ requests = new TerminalCloseRequests();
942
+ closing = /* @__PURE__ */ new Map();
943
+ closed = new Set(this.requests.pending().map((request) => request.id));
944
+ disposed = false;
945
+ views = /* @__PURE__ */ new Map();
946
+ inventories = /* @__PURE__ */ new Map();
947
+ bindings = new TerminalBindings();
948
+ holds = /* @__PURE__ */ new Map();
949
+ releasing = /* @__PURE__ */ new Set();
950
+ openTabs = [];
951
+ /**
952
+ * @param ctx - Client root Context with Gateway and terminal Remote namespace.
953
+ * @param remote - generated terminal namespace.
954
+ */
955
+ constructor(ctx, remote) {
956
+ super(ctx, "webTerminals");
957
+ this.remote = remote;
958
+ let connected = ctx.get("connection").generation.getSnapshot() !== void 0;
959
+ ctx.on("connection/reset", () => {
960
+ if (!connected) {
961
+ connected = true;
962
+ return;
963
+ }
964
+ for (const [sessionId, inventory] of this.inventories) inventory.reset()?.catch((error) => {
965
+ if (!this.disposed) this.ctx.logger.warn("Terminal list refresh failed; retry terminal recovery", {
966
+ sessionId,
967
+ error
968
+ });
969
+ });
970
+ });
971
+ ctx.effect(() => async () => {
972
+ this.disposed = true;
973
+ const listing = [...this.inventories.values()].map((inventory) => inventory.dispose());
974
+ this.inventories.clear();
975
+ const detaching = [...this.views.values()].flatMap((views) => [...views.values()].map((view) => view.dispose()));
976
+ this.views.clear();
977
+ this.bindings.clear();
978
+ const holds = [...this.holds.values()].flatMap((holds) => [...holds.values()].map((hold) => hold.dispose()));
979
+ this.holds.clear();
980
+ await Promise.all([
981
+ ...listing,
982
+ ...detaching,
983
+ ...holds,
984
+ ...this.releasing,
985
+ ...this.closing.values()
986
+ ]);
987
+ }, "terminal-controller.client.views");
988
+ for (const request of this.requests.pending()) this.cleanup(request);
989
+ }
990
+ /**
991
+ * Return the stable model for one sidebar occurrence.
992
+ * @param sessionId - owning Session.
993
+ * @param key - sidebar occurrence key.
994
+ * @param contentId - globally unique content identity; layout-local tab ids are not persistence keys.
995
+ * @param terminalId - existing Host identity when restoring a listed terminal; otherwise reuse the saved content identity.
996
+ * @param shellPath - explicit shell for a new terminal; restored terminals retain their own shell.
997
+ * @returns its observable state and terminal commands.
998
+ */
999
+ view(sessionId, key, contentId, terminalId, shellPath) {
1000
+ let views = this.views.get(sessionId);
1001
+ if (views === void 0) {
1002
+ views = /* @__PURE__ */ new Map();
1003
+ this.views.set(sessionId, views);
1004
+ }
1005
+ let view = views.get(key);
1006
+ if (view === void 0) {
1007
+ const saved = terminalId ?? this.bindings.get(sessionId, contentId);
1008
+ const id = saved ?? randomUUID();
1009
+ this.bindings.set(sessionId, contentId, id);
1010
+ view = new TerminalView(sessionId, this.remote, this.ctx.remote, this.inventory(sessionId), id, saved === void 0, shellPath, (signal) => this.hold(sessionId, id).ready(signal));
1011
+ views.set(key, view);
1012
+ this.reconcileHolds();
1013
+ view.refresh();
1014
+ }
1015
+ return view;
1016
+ }
1017
+ inventory(sessionId) {
1018
+ let inventory = this.inventories.get(sessionId);
1019
+ if (inventory === void 0) {
1020
+ inventory = new TerminalInventory(() => this.remote.list(sessionId));
1021
+ this.inventories.set(sessionId, inventory);
1022
+ }
1023
+ return inventory;
1024
+ }
1025
+ /**
1026
+ * Discover available launch choices on demand without allocating a PTY.
1027
+ * @param sessionId - target Session.
1028
+ * @param signal - the menu request lifetime.
1029
+ * @returns installed shells and the currently usable browser preference.
1030
+ */
1031
+ async launchShells(sessionId, signal) {
1032
+ const result = await this.remote.shells(sessionId, signal);
1033
+ if (!result.ok) throw new Error(result.error.message);
1034
+ const previous = preferredShell();
1035
+ return {
1036
+ shells: result.value,
1037
+ selectedShell: result.value.find((shell) => shell.path === previous)?.path ?? result.value[0]?.path
1038
+ };
1039
+ }
1040
+ /**
1041
+ * Remember the guide selection before allocating its terminal tab.
1042
+ * @param path - shell selected from Host discovery.
1043
+ */
1044
+ selectShell(path) {
1045
+ rememberShell(path);
1046
+ }
1047
+ /**
1048
+ * Save a close intent and release the tab immediately; cleanup outlives DOM unmount and reload.
1049
+ * @param sessionId - owning Session.
1050
+ * @param key - sidebar occurrence key, including an inactive restored tab.
1051
+ * @param contentId - globally unique content identity whose binding is removed.
1052
+ * @param terminalId - restored identity if the tab has no model yet.
1053
+ */
1054
+ close(sessionId, key, contentId, terminalId) {
1055
+ const views = this.views.get(sessionId);
1056
+ const view = views?.get(key);
1057
+ const id = view?.id ?? terminalId ?? this.bindings.get(sessionId, contentId);
1058
+ if (id === void 0) return;
1059
+ const request = {
1060
+ sessionId,
1061
+ id,
1062
+ title: view?.state.getSnapshot().title ?? key
1063
+ };
1064
+ this.closed.add(id);
1065
+ this.requests.save(request);
1066
+ this.bindings.delete(sessionId, contentId);
1067
+ views?.delete(key);
1068
+ if (views?.size === 0) this.views.delete(sessionId);
1069
+ this.cleanup(request, view);
1070
+ this.reconcileHolds();
1071
+ }
1072
+ /**
1073
+ * Reconcile this window's open terminal occurrences, including dormant saved Sessions.
1074
+ * @param tabs - terminal-kind membership supplied by the sidebar layout owner.
1075
+ */
1076
+ retainTabs(tabs) {
1077
+ this.openTabs = tabs;
1078
+ this.reconcileHolds();
1079
+ }
1080
+ hold(sessionId, id) {
1081
+ let holds = this.holds.get(sessionId);
1082
+ if (holds === void 0) {
1083
+ holds = /* @__PURE__ */ new Map();
1084
+ this.holds.set(sessionId, holds);
1085
+ }
1086
+ let hold = holds.get(id);
1087
+ if (hold === void 0 || hold.failed) {
1088
+ if (hold !== void 0) this.release(hold);
1089
+ hold = new TerminalWindowHold(this.ctx.remote, this.remote, sessionId, id);
1090
+ holds.set(id, hold);
1091
+ }
1092
+ return hold;
1093
+ }
1094
+ reconcileHolds() {
1095
+ if (this.disposed) return;
1096
+ const wanted = /* @__PURE__ */ new Map();
1097
+ for (const tab of this.openTabs) {
1098
+ const id = this.bindings.get(tab.sessionId, tab.contentId);
1099
+ if (id === void 0 || this.closed.has(id)) continue;
1100
+ let ids = wanted.get(tab.sessionId);
1101
+ if (ids === void 0) {
1102
+ ids = /* @__PURE__ */ new Set();
1103
+ wanted.set(tab.sessionId, ids);
1104
+ }
1105
+ ids.add(id);
1106
+ const view = this.views.get(tab.sessionId)?.get(tab.tabId);
1107
+ if (view === void 0 || view.state.getSnapshot().info !== void 0) {
1108
+ if (!this.holds.get(tab.sessionId)?.has(id)) this.hold(tab.sessionId, id);
1109
+ }
1110
+ }
1111
+ for (const [sessionId, holds] of this.holds) {
1112
+ for (const [id, hold] of holds) if (!wanted.get(sessionId)?.has(id)) {
1113
+ holds.delete(id);
1114
+ this.release(hold);
1115
+ }
1116
+ if (holds.size === 0) this.holds.delete(sessionId);
1117
+ }
1118
+ }
1119
+ release(hold) {
1120
+ const releasing = hold.dispose().finally(() => {
1121
+ this.releasing.delete(releasing);
1122
+ });
1123
+ this.releasing.add(releasing);
1124
+ releasing.catch((error) => {
1125
+ this.ctx.logger.warn("Terminal hold release failed", error);
1126
+ });
1127
+ }
1128
+ /**
1129
+ * Read shared Session discovery without a live view or unfinished close.
1130
+ * @param sessionId - Session being displayed.
1131
+ * @param refresh - explicitly replace settled discovery, including a previous failure.
1132
+ * @returns terminals available for opening as recovered tabs.
1133
+ */
1134
+ async recover(sessionId, refresh = false) {
1135
+ const available = await this.inventory(sessionId).read(refresh);
1136
+ const held = new Set([...this.views.get(sessionId)?.values() ?? []].map((view) => view.id));
1137
+ const closing = new Set(this.requests.pending().map((request) => request.id));
1138
+ return [...available.values()].filter((info) => !held.has(info.id) && !closing.has(info.id) && !this.closed.has(info.id));
1139
+ }
1140
+ /**
1141
+ * Retry a saved close request without reopening its tab.
1142
+ * @param id - failed terminal identity.
1143
+ */
1144
+ retryClose(id) {
1145
+ const record = this.requests.pending().find((item) => item.id === id);
1146
+ if (record !== void 0) this.cleanup(record);
1147
+ }
1148
+ cleanup(record, view) {
1149
+ if (this.closing.has(record.id) || this.disposed) return;
1150
+ this.closeFailures.set(this.closeFailures.getSnapshot().filter((failure) => failure.id !== record.id));
1151
+ const pending = (async () => {
1152
+ if (view !== void 0) await view.close();
1153
+ else {
1154
+ const result = await this.remote.close(record.sessionId, record.id);
1155
+ if (!result.ok) throw result.error;
1156
+ }
1157
+ this.inventories.get(record.sessionId)?.remove(record.id);
1158
+ this.requests.remove(record.id);
1159
+ })().catch((error) => {
1160
+ if (remoteErrorOf(error)?.code === "session/not-found") {
1161
+ this.requests.remove(record.id);
1162
+ return;
1163
+ }
1164
+ if (!this.disposed) this.closeFailures.set([...this.closeFailures.getSnapshot(), {
1165
+ id: record.id,
1166
+ title: record.title,
1167
+ message: error instanceof Error ? error.message : String(error)
1168
+ }]);
1169
+ }).then(async () => {
1170
+ await view?.dispose();
1171
+ this.closing.delete(record.id);
1172
+ });
1173
+ this.closing.set(record.id, pending);
1174
+ }
1175
+ };
1176
+ /** Required Client transport and terminal namespace. */
1177
+ const inject = [
1178
+ "remote",
1179
+ "remote.terminal",
1180
+ "connection"
1181
+ ];
1182
+ /**
1183
+ * Install the Client terminal models.
1184
+ * @param ctx - Client root Context.
1185
+ */
1186
+ function apply(ctx) {
1187
+ applyRouted(ctx);
1188
+ }
1189
+ /**
1190
+ * Session-routing terminal provider used inside the forked ClientTerminals
1191
+ * construction. Local Sessions call the stock remote face; mirrored Sessions
1192
+ * call the local plugin's SSH terminal route.
1193
+ */
1194
+ const TERMINAL_ROUTE = '/api/dsh-remote-terminal'
1195
+
1196
+ function applyRouted(ctx) {
1197
+ const original = ctx.remote.terminal
1198
+ const routed = routeTerminal(original)
1199
+ new ClientTerminals(ctx, routed)
1200
+ }
1201
+
1202
+ function routeTerminal(original) {
1203
+ const mirrored = new Map()
1204
+ const isMirror = sessionId => {
1205
+ let pending = mirrored.get(sessionId)
1206
+ if (pending === undefined) {
1207
+ pending = terminalPost({ action: 'context', sessionId })
1208
+ .then(value => value?.mirrored === true)
1209
+ .catch(() => false)
1210
+ mirrored.set(sessionId, pending)
1211
+ }
1212
+ return pending
1213
+ }
1214
+ const unary = name => async (...args) => {
1215
+ const sessionId = String(args[0])
1216
+ if (!await isMirror(sessionId)) return original[name](...args)
1217
+ const value = await terminalPost({ action: name, sessionId, ...terminalPayload(name, args) })
1218
+ if (value === undefined) throw new Error('remote terminal is unavailable; refusing to fall back to the local terminal')
1219
+ return { ok: true, value }
1220
+ }
1221
+ return {
1222
+ ...original,
1223
+ environment: unary('environment'),
1224
+ shells: unary('shells'),
1225
+ list: unary('list'),
1226
+ create: unary('create'),
1227
+ rename: unary('rename'),
1228
+ close: unary('close'),
1229
+ retain: (sessionId, id, signal) => (async function* () {
1230
+ if (await isMirror(sessionId)) {
1231
+ yield { type: 'retained' }
1232
+ return
1233
+ }
1234
+ yield* original.retain(sessionId, id, signal)
1235
+ })(),
1236
+ attach: (sessionId, id, signal) => new RoutedTerminalHandle(original, sessionId, id, signal, isMirror),
1237
+ }
1238
+ }
1239
+
1240
+ function terminalPayload(name, args) {
1241
+ if (name === 'create') {
1242
+ const request = args[1] ?? {}
1243
+ return { id: request.id, cols: request.cols, rows: request.rows, shellPath: request.shellPath }
1244
+ }
1245
+ if (name === 'rename') return { id: args[1], title: args[2] }
1246
+ if (name === 'close') return { id: args[1] }
1247
+ return {}
1248
+ }
1249
+
1250
+ async function terminalPost(payload) {
1251
+ const response = await fetch(TERMINAL_ROUTE, {
1252
+ method: 'POST',
1253
+ credentials: 'same-origin',
1254
+ headers: { 'content-type': 'application/json' },
1255
+ body: JSON.stringify(payload),
1256
+ })
1257
+ if (!response.ok) return undefined
1258
+ const body = await response.json()
1259
+ if (body.mirrored !== undefined) return body
1260
+ return body.ok === true ? body.value : undefined
1261
+ }
1262
+
1263
+ class RoutedTerminalHandle {
1264
+ #original
1265
+ #sessionId
1266
+ #id
1267
+ #signal
1268
+ #isMirror
1269
+ #handle
1270
+ #pending = []
1271
+ #controller = new AbortController()
1272
+ #opening
1273
+
1274
+ constructor(original, sessionId, id, signal, isMirror) {
1275
+ this.#original = original
1276
+ this.#sessionId = sessionId
1277
+ this.#id = id
1278
+ this.#signal = signal
1279
+ this.#isMirror = isMirror
1280
+ signal.addEventListener('abort', () => this.#controller.abort(), { once: true })
1281
+ }
1282
+
1283
+ async *[Symbol.asyncIterator]() {
1284
+ const handle = await this.#open()
1285
+ yield* handle
1286
+ }
1287
+
1288
+ send(item) {
1289
+ if (this.#handle !== undefined) this.#handle.send(item)
1290
+ else this.#pending.push(item)
1291
+ }
1292
+
1293
+ end() { this.#handle?.end() }
1294
+
1295
+ dispose() {
1296
+ this.#controller.abort()
1297
+ this.#handle?.dispose()
1298
+ }
1299
+
1300
+ async #open() {
1301
+ if (this.#handle !== undefined) return this.#handle
1302
+ this.#opening ??= (async () => {
1303
+ const remote = await this.#isMirror(this.#sessionId)
1304
+ const handle = remote
1305
+ ? remoteTerminalHandle(this.#sessionId, this.#id, this.#controller.signal)
1306
+ : this.#original.attach(this.#sessionId, this.#id, this.#signal)
1307
+ this.#handle = handle
1308
+ for (const item of this.#pending.splice(0)) handle.send(item)
1309
+ return handle
1310
+ })()
1311
+ return this.#opening
1312
+ }
1313
+ }
1314
+
1315
+ function remoteTerminalHandle(sessionId, id, signal) {
1316
+ let controller = new AbortController()
1317
+ signal.addEventListener('abort', () => controller.abort(), { once: true })
1318
+ return {
1319
+ async *[Symbol.asyncIterator]() {
1320
+ const response = await fetch(TERMINAL_ROUTE, {
1321
+ method: 'POST',
1322
+ credentials: 'same-origin',
1323
+ headers: { 'content-type': 'application/json', accept: 'text/event-stream' },
1324
+ body: JSON.stringify({ action: 'attach', sessionId, id }),
1325
+ signal: controller.signal,
1326
+ })
1327
+ if (!response.ok || response.body === null) throw new Error('remote terminal attach failed')
1328
+ const reader = response.body.getReader()
1329
+ const decoder = new TextDecoder()
1330
+ let buffer = ''
1331
+ try {
1332
+ for (;;) {
1333
+ const { done, value } = await reader.read()
1334
+ buffer += decoder.decode(value, { stream: !done })
1335
+ let newline = buffer.indexOf('\n')
1336
+ while (newline >= 0) {
1337
+ const line = buffer.slice(0, newline).replace(/\r$/u, '')
1338
+ buffer = buffer.slice(newline + 1)
1339
+ if (line.startsWith('data: ')) {
1340
+ const parsed = JSON.parse(line.slice(6).trim())
1341
+ if (parsed.message !== undefined && parsed.type === undefined) throw new Error(parsed.message)
1342
+ yield parsed
1343
+ }
1344
+ newline = buffer.indexOf('\n')
1345
+ }
1346
+ if (done) return
1347
+ }
1348
+ } finally {
1349
+ reader.releaseLock()
1350
+ }
1351
+ },
1352
+ send(item) {
1353
+ if (item.type === 'write') void terminalPost({ action: 'input', sessionId, id, data: item.data ?? '', sequence: item.sequence ?? 0 })
1354
+ else void terminalPost({ action: 'resize', sessionId, id, cols: item.cols ?? 80, rows: item.rows ?? 24, sequence: item.sequence ?? 0 })
1355
+ },
1356
+ end() {},
1357
+ dispose() { controller.abort() },
1358
+ }
1359
+ }
1360
+
1361
+ //#endregion
1362
+ exports.ClientTerminals = ClientTerminals;
1363
+ exports.apply = apply;
1364
+ exports.inject = inject;
1365
+ return module.exports;
1366
+ }
1367
+ });