@achasoft/dsh-advanced-sidebar 0.1.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 (58) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +205 -0
  3. package/cordis.patch.yml +158 -0
  4. package/lib/client.js +24890 -0
  5. package/lib/client.js.map +1 -0
  6. package/lib/host.js +3156 -0
  7. package/lib/index.js +20 -0
  8. package/lib/remote.js +1994 -0
  9. package/lib/typert.host.js +2000 -0
  10. package/package.json +136 -0
  11. package/types/client/ActionMenu.d.ts +36 -0
  12. package/types/client/Glyphs.d.ts +49 -0
  13. package/types/client/PanelHost.d.ts +24 -0
  14. package/types/client/Seats.d.ts +16 -0
  15. package/types/client/SettingsCard.d.ts +22 -0
  16. package/types/client/contract.d.ts +358 -0
  17. package/types/client/controller.d.ts +224 -0
  18. package/types/client/cx.d.ts +16 -0
  19. package/types/client/index.d.ts +39 -0
  20. package/types/client/locales.d.ts +474 -0
  21. package/types/client/panels/ChangesPanel.d.ts +22 -0
  22. package/types/client/panels/FilesPanel.d.ts +16 -0
  23. package/types/client/panels/PreviewPanel.d.ts +27 -0
  24. package/types/client/panels/TasksPanel.d.ts +28 -0
  25. package/types/client/panels/TerminalPanel.d.ts +40 -0
  26. package/types/client/panels/shared.d.ts +65 -0
  27. package/types/client/target.d.ts +23 -0
  28. package/types/client/terminal-screen.d.ts +95 -0
  29. package/types/client/ui/Alert.d.ts +30 -0
  30. package/types/client/ui/Badge.d.ts +24 -0
  31. package/types/client/ui/Button.d.ts +28 -0
  32. package/types/client/ui/Calendar.d.ts +65 -0
  33. package/types/client/ui/DatePicker.d.ts +41 -0
  34. package/types/client/ui/Dialog.d.ts +61 -0
  35. package/types/client/ui/DropdownMenu.d.ts +98 -0
  36. package/types/client/ui/Input.d.ts +25 -0
  37. package/types/client/ui/Layer.d.ts +56 -0
  38. package/types/client/ui/Select.d.ts +49 -0
  39. package/types/client/ui/Separator.d.ts +15 -0
  40. package/types/client/ui/Tabs.d.ts +49 -0
  41. package/types/client/ui/Toggle.d.ts +57 -0
  42. package/types/client/ui/Tooltip.d.ts +22 -0
  43. package/types/client/ui/anchor.d.ts +92 -0
  44. package/types/client/ui/index.d.ts +42 -0
  45. package/types/client/use-capability.d.ts +27 -0
  46. package/types/host/deletion.d.ts +57 -0
  47. package/types/host/files.d.ts +43 -0
  48. package/types/host/git.d.ts +198 -0
  49. package/types/host/index.d.ts +210 -0
  50. package/types/host/open-in.d.ts +93 -0
  51. package/types/host/paths.d.ts +55 -0
  52. package/types/host/porcelain.d.ts +51 -0
  53. package/types/host/preview.d.ts +185 -0
  54. package/types/host/run.d.ts +72 -0
  55. package/types/host/tasks.d.ts +80 -0
  56. package/types/host/terminals.d.ts +86 -0
  57. package/types/host/types.d.ts +877 -0
  58. package/types/index.d.ts +17 -0
package/lib/host.js ADDED
@@ -0,0 +1,3156 @@
1
+ import z from "@deepseek-ai/schemastery";
2
+ import { installSettingsSection, settingsNamespace } from "@deepseek-ai/dsh-settings";
3
+ import { Remote, TypertRemoteService } from "@deepseek-ai/dsh-typert-protocol";
4
+ import { rm } from "node:fs/promises";
5
+ import { dirname, isAbsolute, join, resolve } from "node:path";
6
+ import { createUserMessage } from "@deepseek-ai/dsh-llm";
7
+ import { randomUUID } from "node:crypto";
8
+ import { connect } from "node:net";
9
+ import { JobId } from "@deepseek-ai/dsh-jobs";
10
+
11
+ //#region tsbuild/host/deletion.js
12
+ /**
13
+ * Commits Delete for the sidebar menu. Stateless apart from the context and settings it reads.
14
+ */
15
+ var SessionDeleter = class {
16
+ ctx;
17
+ source;
18
+ /**
19
+ * @param ctx - Host context carrying the workspace registry and session persistence.
20
+ * @param source - reads the current settings section; called per request.
21
+ */
22
+ constructor(ctx, source) {
23
+ this.ctx = ctx;
24
+ this.source = source;
25
+ }
26
+ /**
27
+ * Report whether the durable log can be removed at all.
28
+ * @returns the capability, with a reason when purging is impossible.
29
+ */
30
+ describe() {
31
+ const persistence = this.ctx.get("sessionPersistence");
32
+ if (persistence === void 0) return {
33
+ canPurge: false,
34
+ reason: "no session-persistence backend is mounted; nothing is written to remove"
35
+ };
36
+ if (!persistence.supportsRawArtifacts) return {
37
+ canPurge: false,
38
+ reason: "this session-persistence backend keeps no per-session artifact, so Delete can only archive"
39
+ };
40
+ return { canPurge: true };
41
+ }
42
+ /**
43
+ * Hide one session, and remove its durable artifact when the mode and the Host allow it.
44
+ * @param request - the session to delete.
45
+ * @param signal - cancellation for the persistence listing.
46
+ * @returns what was actually done, or a classified failure.
47
+ */
48
+ async delete(request, signal) {
49
+ const settings = this.source();
50
+ if (!settings.showDelete) return {
51
+ ok: false,
52
+ code: "disabled",
53
+ message: "Delete is switched off in the advanced-sidebar settings"
54
+ };
55
+ const registry = this.ctx.get("workspaceRegistry");
56
+ if (registry === void 0) return {
57
+ ok: false,
58
+ code: "no-registry",
59
+ message: "no workspace registry is mounted"
60
+ };
61
+ const sessionId = request.sessionId;
62
+ const artifact = settings.deleteMode === "purge" ? await this.locateArtifact(sessionId, signal) : void 0;
63
+ try {
64
+ await registry.archiveSession(sessionId);
65
+ } catch (error) {
66
+ return {
67
+ ok: false,
68
+ code: "archive-failed",
69
+ message: error instanceof Error ? error.message : String(error)
70
+ };
71
+ }
72
+ if (settings.deleteMode === "archive") return {
73
+ ok: true,
74
+ archived: true,
75
+ purged: false
76
+ };
77
+ if (artifact === void 0 || "reason" in artifact) return {
78
+ ok: true,
79
+ archived: true,
80
+ purged: false,
81
+ purgeSkippedReason: artifact?.reason ?? this.describe().reason ?? "the session has no materialized artifact to remove"
82
+ };
83
+ try {
84
+ await rm(artifact.path, { force: true });
85
+ } catch (error) {
86
+ return {
87
+ ok: false,
88
+ code: "remove-failed",
89
+ message: `archived, but ${artifact.path} could not be removed: ${error instanceof Error ? error.message : String(error)}`
90
+ };
91
+ }
92
+ return {
93
+ ok: true,
94
+ archived: true,
95
+ purged: true,
96
+ artifactPath: artifact.path
97
+ };
98
+ }
99
+ /**
100
+ * Find the backend artifact for one session, or say why it will not be removed.
101
+ * @param sessionId - the session to look up.
102
+ * @param signal - cancellation for the persistence listing.
103
+ * @returns the artifact path, or the reason purging is skipped.
104
+ */
105
+ async locateArtifact(sessionId, signal) {
106
+ const capability = this.describe();
107
+ if (!capability.canPurge) return { reason: capability.reason ?? "purging is unavailable on this Host" };
108
+ if (this.ctx.agents.get(sessionId) !== void 0) return { reason: "the session is live, so its log was kept; delete it again after it stops" };
109
+ const persistence = this.ctx.sessionPersistence;
110
+ let headers;
111
+ try {
112
+ headers = await persistence.list(signal);
113
+ } catch (error) {
114
+ return { reason: `the durable session list could not be read: ${error instanceof Error ? error.message : String(error)}` };
115
+ }
116
+ const header = headers.find((entry) => entry.id === sessionId);
117
+ if (header === void 0) return { reason: "the session has no durable log on this Host" };
118
+ const location = persistence.locate(header);
119
+ if (location === void 0) return { reason: "this backend keeps no per-session artifact for that session" };
120
+ return { path: location.path };
121
+ }
122
+ };
123
+
124
+ //#endregion
125
+ //#region tsbuild/host/paths.js
126
+ /**
127
+ * Path resolution and containment for every endpoint that takes a path from the browser.
128
+ *
129
+ * A browser-supplied path is untrusted input at a process boundary, so each one is resolved through
130
+ * `ctx.fs` and proved to sit inside the workspace it claims to belong to before any command, read,
131
+ * or launch sees it. `..` and symlinks are handled by the filesystem's own canonicalization rather
132
+ * than by string arithmetic here.
133
+ * @module @achasoft/dsh-advanced-sidebar/host/paths
134
+ */
135
+ /**
136
+ * Resolve one absolute directory as a workspace root.
137
+ * @param ctx - Host context carrying the optional filesystem capability.
138
+ * @param path - absolute directory path supplied by the browser.
139
+ * @param signal - cancellation for the backend round-trip.
140
+ * @returns the canonical directory, or the reason it was refused.
141
+ */
142
+ async function resolveWorkspace(ctx, path, signal) {
143
+ const fs = ctx.get("fs");
144
+ if (fs === void 0) return {
145
+ ok: false,
146
+ rejection: {
147
+ code: "no-filesystem",
148
+ message: "no filesystem capability is mounted: this deployment composes no @deepseek-ai/dsh-fs provider"
149
+ }
150
+ };
151
+ let target;
152
+ try {
153
+ target = await fs.resolve(path, signal === void 0 ? {} : { signal });
154
+ } catch (error) {
155
+ return {
156
+ ok: false,
157
+ rejection: {
158
+ code: "path-denied",
159
+ message: describe(error, path)
160
+ }
161
+ };
162
+ }
163
+ const info = await fs.stat(target, signal);
164
+ if (info === void 0 || info.type !== "directory") return {
165
+ ok: false,
166
+ rejection: {
167
+ code: "path-denied",
168
+ message: `${path} is not a directory`
169
+ }
170
+ };
171
+ return {
172
+ ok: true,
173
+ value: {
174
+ target,
175
+ processPath: fs.processPath(target)
176
+ }
177
+ };
178
+ }
179
+ /**
180
+ * Resolve one path and prove it sits inside an already-resolved workspace.
181
+ * @param ctx - Host context carrying the optional filesystem capability.
182
+ * @param workspace - the canonical workspace directory the path must stay within.
183
+ * @param path - absolute path supplied by the browser.
184
+ * @param signal - cancellation for the backend round-trip.
185
+ * @returns the canonical path, or the reason it was refused.
186
+ */
187
+ async function resolveInside(ctx, workspace, path, signal) {
188
+ const fs = ctx.get("fs");
189
+ /* v8 ignore next 4 -- the caller resolved `workspace` through the same service moments earlier. */
190
+ if (fs === void 0) return {
191
+ ok: false,
192
+ rejection: {
193
+ code: "no-filesystem",
194
+ message: "filesystem capability withdrawn mid-request"
195
+ }
196
+ };
197
+ let target;
198
+ try {
199
+ target = await fs.resolve(path, signal === void 0 ? {} : { signal });
200
+ } catch (error) {
201
+ return {
202
+ ok: false,
203
+ rejection: {
204
+ code: "path-denied",
205
+ message: describe(error, path)
206
+ }
207
+ };
208
+ }
209
+ if (!fs.contains(workspace.target, target)) return {
210
+ ok: false,
211
+ rejection: {
212
+ code: "path-denied",
213
+ message: `${path} is outside ${workspace.target.displayPath}`
214
+ }
215
+ };
216
+ return {
217
+ ok: true,
218
+ value: {
219
+ target,
220
+ processPath: fs.processPath(target)
221
+ }
222
+ };
223
+ }
224
+ /**
225
+ * Phrase one resolution failure without leaking a stack.
226
+ * @param error - whatever the backend threw.
227
+ * @param path - the path that was being resolved.
228
+ * @returns a single-line operator diagnostic.
229
+ */
230
+ function describe(error, path) {
231
+ return `cannot resolve ${path}: ${error instanceof Error ? error.message : String(error)}`;
232
+ }
233
+
234
+ //#endregion
235
+ //#region tsbuild/host/files.js
236
+ /**
237
+ * How much of a file's head decides whether it is text. A NUL byte anywhere in this window is the
238
+ * same test `git` uses, and it is cheap enough to apply to every preview.
239
+ */
240
+ const BINARY_PROBE_BYTES = 8e3;
241
+ /** Reads file previews for the Files panel. */
242
+ var FileReader = class {
243
+ ctx;
244
+ source;
245
+ /**
246
+ * @param ctx - Host context carrying the optional filesystem capability.
247
+ * @param source - reads the current settings section; called per request.
248
+ */
249
+ constructor(ctx, source) {
250
+ this.ctx = ctx;
251
+ this.source = source;
252
+ }
253
+ /**
254
+ * Report whether a preview can be read on this Host.
255
+ * @returns availability, with the reason when there is no filesystem.
256
+ */
257
+ describe() {
258
+ if (this.ctx.get("fs") === void 0) return {
259
+ available: false,
260
+ reason: "no filesystem capability is mounted: this deployment composes no @deepseek-ai/dsh-fs provider"
261
+ };
262
+ return { available: true };
263
+ }
264
+ /**
265
+ * List one directory level inside a workspace.
266
+ *
267
+ * The Web Client's own `listDirectory` cannot serve this panel: the Host's browse capability
268
+ * returns directories only, because its one caller is a workspace picker. A file browser needs
269
+ * the files.
270
+ * @param request - the directory and the workspace it must stay inside.
271
+ * @param signal - cancellation for the listing.
272
+ * @returns the level, or a classified failure.
273
+ */
274
+ async list(request, signal) {
275
+ const fs = this.ctx.get("fs");
276
+ if (fs === void 0) return {
277
+ ok: false,
278
+ code: "no-filesystem",
279
+ message: "no filesystem capability is mounted"
280
+ };
281
+ const workspace = await resolveWorkspace(this.ctx, request.workspacePath, signal);
282
+ if (!workspace.ok) return {
283
+ ok: false,
284
+ code: workspace.rejection.code === "no-filesystem" ? "no-filesystem" : "path-denied",
285
+ message: workspace.rejection.message
286
+ };
287
+ const directory = await resolveInside(this.ctx, workspace.value, request.path, signal);
288
+ if (!directory.ok) return {
289
+ ok: false,
290
+ code: directory.rejection.code === "no-filesystem" ? "no-filesystem" : "path-denied",
291
+ message: directory.rejection.message
292
+ };
293
+ const info = await fs.stat(directory.value.target, signal);
294
+ if (info === void 0 || info.type !== "directory") return {
295
+ ok: false,
296
+ code: "not-a-file",
297
+ message: `${request.path} is not a directory`
298
+ };
299
+ let children;
300
+ try {
301
+ children = await fs.listDir(directory.value.target, signal);
302
+ } catch (error) {
303
+ return {
304
+ ok: false,
305
+ code: "read-failed",
306
+ message: error instanceof Error ? error.message : String(error)
307
+ };
308
+ }
309
+ const settings = this.source();
310
+ const sorted = children.filter((child) => settings.filesShowHidden || !child.name.startsWith(".")).map((child) => ({
311
+ name: child.name,
312
+ path: fs.processPath(child.target),
313
+ kind: child.type,
314
+ ...child.size === void 0 ? {} : { size: child.size }
315
+ })).sort((left, right) => {
316
+ const leftDirectory = left.kind === "directory";
317
+ if (leftDirectory !== (right.kind === "directory")) return leftDirectory ? -1 : 1;
318
+ return left.name.localeCompare(right.name);
319
+ });
320
+ const truncated = sorted.length > settings.filesMaxEntries;
321
+ const parent = fs.contains(directory.value.target, workspace.value.target) ? void 0 : dirname(directory.value.processPath);
322
+ return {
323
+ ok: true,
324
+ path: directory.value.processPath,
325
+ ...parent === void 0 ? {} : { parent },
326
+ entries: truncated ? sorted.slice(0, settings.filesMaxEntries) : sorted,
327
+ truncated
328
+ };
329
+ }
330
+ /**
331
+ * Read one file, bounded by `filesMaxPreviewBytes`.
332
+ * @param request - the file and the workspace it must stay inside.
333
+ * @param signal - cancellation for the read.
334
+ * @returns the preview, or a classified failure.
335
+ */
336
+ async read(request, signal) {
337
+ const fs = this.ctx.get("fs");
338
+ if (fs === void 0) return {
339
+ ok: false,
340
+ code: "no-filesystem",
341
+ message: "no filesystem capability is mounted"
342
+ };
343
+ const workspace = await resolveWorkspace(this.ctx, request.workspacePath, signal);
344
+ if (!workspace.ok) return {
345
+ ok: false,
346
+ code: workspace.rejection.code === "no-filesystem" ? "no-filesystem" : "path-denied",
347
+ message: workspace.rejection.message
348
+ };
349
+ const file = await resolveInside(this.ctx, workspace.value, request.path, signal);
350
+ if (!file.ok) return {
351
+ ok: false,
352
+ code: file.rejection.code === "no-filesystem" ? "no-filesystem" : "path-denied",
353
+ message: file.rejection.message
354
+ };
355
+ const info = await fs.stat(file.value.target, signal);
356
+ if (info === void 0 || info.type !== "file") return {
357
+ ok: false,
358
+ code: "not-a-file",
359
+ message: `${request.path} is not a regular file`
360
+ };
361
+ const max = this.source().filesMaxPreviewBytes;
362
+ let bytes;
363
+ try {
364
+ bytes = await fs.readBytes(file.value.target, signal, max);
365
+ } catch (error) {
366
+ return {
367
+ ok: false,
368
+ code: "read-failed",
369
+ message: error instanceof Error ? error.message : String(error)
370
+ };
371
+ }
372
+ const size = info.size ?? bytes.byteLength;
373
+ if (isBinary(bytes)) return {
374
+ ok: true,
375
+ path: request.path,
376
+ text: "",
377
+ binary: true,
378
+ truncated: false,
379
+ bytes: size
380
+ };
381
+ return {
382
+ ok: true,
383
+ path: request.path,
384
+ text: new TextDecoder("utf-8", { fatal: false }).decode(bytes),
385
+ binary: false,
386
+ truncated: size > bytes.byteLength,
387
+ bytes: size
388
+ };
389
+ }
390
+ };
391
+ /**
392
+ * Whether a file's leading bytes look like something other than text.
393
+ * @param bytes - the read window.
394
+ * @returns true when a NUL appears in the probe window.
395
+ */
396
+ function isBinary(bytes) {
397
+ const limit = Math.min(bytes.byteLength, BINARY_PROBE_BYTES);
398
+ for (let index = 0; index < limit; index += 1) if (bytes[index] === 0) return true;
399
+ return false;
400
+ }
401
+
402
+ //#endregion
403
+ //#region tsbuild/host/porcelain.js
404
+ /**
405
+ * Pure parser for `git status --porcelain=v2 --branch -z`.
406
+ *
407
+ * Kept apart from the command runner so the format — which is the only part of the git integration
408
+ * with edge cases worth testing (renames carry two paths, `-z` terminates every line including the
409
+ * headers, and a path may contain any byte but NUL) — is exercised without a subprocess.
410
+ * @module @achasoft/dsh-advanced-sidebar/host/porcelain
411
+ */
412
+ /**
413
+ * `--porcelain=v2` status letters. `.` is git's spelling of "nothing to report in this index",
414
+ * which is why {@link GitFileState} carries `unmodified` rather than leaving the field absent.
415
+ */
416
+ const STATE_BY_LETTER = {
417
+ ".": "unmodified",
418
+ "M": "modified",
419
+ "T": "typechange",
420
+ "A": "added",
421
+ "D": "deleted",
422
+ "R": "renamed",
423
+ "C": "copied",
424
+ "U": "conflicted"
425
+ };
426
+ /**
427
+ * Map one status letter.
428
+ * @param letter - a single `XY` character.
429
+ * @returns the state, or `modified` for a letter a newer git introduced after this parser.
430
+ */
431
+ function state(letter) {
432
+ if (letter === void 0) return "unmodified";
433
+ return STATE_BY_LETTER[letter] ?? "modified";
434
+ }
435
+ /**
436
+ * Split one `1`/`2`/`u` record into its space-separated fields plus the trailing path.
437
+ *
438
+ * The path is the remainder after a fixed field count, never a split result: a path may contain
439
+ * spaces, and splitting it would silently truncate every such file to its first word.
440
+ * @param record - the record text without its NUL terminator.
441
+ * @param fields - how many space-separated fields precede the path (including the leading type).
442
+ * @returns the fields and the untouched path remainder, or undefined for a malformed record.
443
+ */
444
+ function splitRecord(record, fields) {
445
+ const head = [];
446
+ let at = 0;
447
+ for (let index = 0; index < fields; index += 1) {
448
+ const space = record.indexOf(" ", at);
449
+ if (space < 0) return void 0;
450
+ head.push(record.slice(at, space));
451
+ at = space + 1;
452
+ }
453
+ const path = record.slice(at);
454
+ return path === "" ? void 0 : {
455
+ head,
456
+ path
457
+ };
458
+ }
459
+ /**
460
+ * Parse the `# branch.ab +A -B` header.
461
+ * @param value - the header's value text.
462
+ * @returns ahead and behind counts; zeros when the header is malformed.
463
+ */
464
+ function parseAheadBehind(value) {
465
+ const match = /^\+(\d+)\s+-(\d+)$/u.exec(value.trim());
466
+ if (match === null) return {
467
+ ahead: 0,
468
+ behind: 0
469
+ };
470
+ return {
471
+ ahead: Number(match[1]),
472
+ behind: Number(match[2])
473
+ };
474
+ }
475
+ /**
476
+ * Parse one complete `--porcelain=v2 --branch -z` payload.
477
+ *
478
+ * Unknown record types are skipped rather than rejected: git adds record kinds over time, and a
479
+ * reading that lists every change it understood is more useful than one that refuses the whole
480
+ * repository because a future git emitted a line this parser has not met.
481
+ * @param payload - git's stdout, verbatim.
482
+ * @returns the branch facts and every parsed change.
483
+ */
484
+ function parsePorcelainV2(payload) {
485
+ const branch = {
486
+ ahead: 0,
487
+ behind: 0,
488
+ detached: false
489
+ };
490
+ const changes = [];
491
+ const chunks = payload.split("\0");
492
+ for (let index = 0; index < chunks.length; index += 1) {
493
+ const chunk = chunks[index];
494
+ if (chunk === void 0 || chunk === "") continue;
495
+ if (chunk.startsWith("# ")) {
496
+ const space = chunk.indexOf(" ", 2);
497
+ const key = space < 0 ? chunk.slice(2) : chunk.slice(2, space);
498
+ const value = space < 0 ? "" : chunk.slice(space + 1);
499
+ if (key === "branch.head") if (value === "(detached)") branch.detached = true;
500
+ else branch.branch = value;
501
+ else if (key === "branch.upstream") branch.upstream = value;
502
+ else if (key === "branch.ab") {
503
+ const { ahead, behind } = parseAheadBehind(value);
504
+ branch.ahead = ahead;
505
+ branch.behind = behind;
506
+ }
507
+ continue;
508
+ }
509
+ if (chunk.startsWith("? ")) {
510
+ const path = chunk.slice(2);
511
+ if (path !== "") changes.push({
512
+ path,
513
+ index: "unmodified",
514
+ worktree: "untracked",
515
+ untracked: true,
516
+ conflicted: false
517
+ });
518
+ continue;
519
+ }
520
+ if (chunk.startsWith("! ")) continue;
521
+ if (chunk.startsWith("1 ")) {
522
+ const parsed = splitRecord(chunk, 8);
523
+ if (parsed === void 0) continue;
524
+ const xy = parsed.head[1] ?? "..";
525
+ changes.push({
526
+ path: parsed.path,
527
+ index: state(xy[0]),
528
+ worktree: state(xy[1]),
529
+ untracked: false,
530
+ conflicted: false
531
+ });
532
+ continue;
533
+ }
534
+ if (chunk.startsWith("2 ")) {
535
+ const parsed = splitRecord(chunk, 9);
536
+ const original = chunks[index + 1];
537
+ index += 1;
538
+ if (parsed === void 0) continue;
539
+ const xy = parsed.head[1] ?? "..";
540
+ changes.push({
541
+ path: parsed.path,
542
+ ...original === void 0 || original === "" ? {} : { oldPath: original },
543
+ index: state(xy[0]),
544
+ worktree: state(xy[1]),
545
+ untracked: false,
546
+ conflicted: false
547
+ });
548
+ continue;
549
+ }
550
+ if (chunk.startsWith("u ")) {
551
+ const parsed = splitRecord(chunk, 10);
552
+ if (parsed === void 0) continue;
553
+ changes.push({
554
+ path: parsed.path,
555
+ index: "conflicted",
556
+ worktree: "conflicted",
557
+ untracked: false,
558
+ conflicted: true
559
+ });
560
+ }
561
+ }
562
+ return {
563
+ branch,
564
+ changes
565
+ };
566
+ }
567
+ /**
568
+ * Whether a change belongs in the staged group — its index state differs from HEAD.
569
+ * @param change - one parsed change.
570
+ * @returns true when a commit right now would record something for this path.
571
+ */
572
+ function isStaged(change) {
573
+ return !change.untracked && !change.conflicted && change.index !== "unmodified";
574
+ }
575
+ /**
576
+ * Whether a change belongs in the unstaged group — its working tree differs from the index.
577
+ * @param change - one parsed change.
578
+ * @returns true when the path has edits no commit would record yet.
579
+ */
580
+ function isUnstaged(change) {
581
+ return !change.untracked && !change.conflicted && change.worktree !== "unmodified";
582
+ }
583
+
584
+ //#endregion
585
+ //#region tsbuild/host/run.js
586
+ /**
587
+ * One-shot command execution over `ctx.subprocess`, shared by the git reader and the Open in
588
+ * launcher.
589
+ *
590
+ * The subprocess seam applies no defaults, so every disposition, bound, and grace period is stated
591
+ * here from this plugin's own settings — which is also what keeps the two callers' behavior
592
+ * configurable from cordis.yml rather than from constants buried in a runner.
593
+ * @module @achasoft/dsh-advanced-sidebar/host/run
594
+ */
595
+ /** No filesystem/subprocess capability, or a spawn that never produced a process. */
596
+ var CommandUnavailableError = class extends Error {
597
+ /**
598
+ * @param message - operator diagnostic naming what was missing.
599
+ */
600
+ constructor(message) {
601
+ super(message);
602
+ this.name = "CommandUnavailableError";
603
+ }
604
+ };
605
+ /**
606
+ * Read one collect-mode stream in full.
607
+ * @param reader - the stream's offset reader, present by the spawn's own disposition.
608
+ * @returns the retained text and whether the head was dropped to fit the cap.
609
+ */
610
+ function drain(reader) {
611
+ /* v8 ignore next -- both streams are spawned in collect mode, so both readers exist. */
612
+ if (reader === void 0) return {
613
+ text: "",
614
+ lossy: false
615
+ };
616
+ const read = reader.readFrom(0);
617
+ return {
618
+ text: read.text,
619
+ lossy: read.lossy
620
+ };
621
+ }
622
+ /**
623
+ * Resolve one executable, returning undefined rather than throwing when it is absent.
624
+ * @param ctx - Host context carrying the optional subprocess capability.
625
+ * @param command - executable name or absolute path.
626
+ * @param signal - cancellation for the lookup.
627
+ * @returns the canonical executable path, or undefined when it does not resolve.
628
+ * @throws {CommandUnavailableError} when no subprocess capability is mounted.
629
+ */
630
+ async function resolveCommand(ctx, command, signal) {
631
+ const subprocess = ctx.get("subprocess");
632
+ if (subprocess === void 0) throw new CommandUnavailableError("no subprocess capability is mounted: this deployment composes no @deepseek-ai/dsh-subprocess provider");
633
+ try {
634
+ return await subprocess.resolveExecutable(command, void 0, signal);
635
+ } catch {
636
+ return;
637
+ }
638
+ }
639
+ /**
640
+ * Run one command to completion under a timeout.
641
+ * @param ctx - Host context carrying the optional subprocess capability.
642
+ * @param spec - the fully specified invocation.
643
+ * @param signal - the caller's cancellation, distinguished from the timeout in the outcome.
644
+ * @returns exit facts and collected output.
645
+ * @throws {CommandUnavailableError} when no subprocess capability is mounted.
646
+ */
647
+ async function runCommand(ctx, spec, signal) {
648
+ const subprocess = ctx.get("subprocess");
649
+ if (subprocess === void 0) throw new CommandUnavailableError("no subprocess capability is mounted: this deployment composes no @deepseek-ai/dsh-subprocess provider");
650
+ const timeout = AbortSignal.timeout(spec.timeoutMs);
651
+ const combined = signal === void 0 ? timeout : AbortSignal.any([signal, timeout]);
652
+ const handle = subprocess.spawn({
653
+ argv: spec.argv,
654
+ cwd: spec.cwd,
655
+ stdio: {
656
+ stdin: "ignore",
657
+ stdout: { maxBytes: spec.maxBytes },
658
+ stderr: { maxBytes: spec.maxBytes }
659
+ },
660
+ graceMs: spec.graceMs,
661
+ signal: combined,
662
+ ...spec.env === void 0 ? {} : { env: spec.env }
663
+ });
664
+ const outcome = await handle.done;
665
+ const stdout = drain(handle.collected.stdout);
666
+ return {
667
+ exitCode: outcome.exitCode,
668
+ signal: outcome.signal,
669
+ stdout: stdout.text,
670
+ stderr: drain(handle.collected.stderr).text,
671
+ timedOut: timeout.aborted,
672
+ aborted: signal?.aborted === true && !timeout.aborted,
673
+ stdoutLossy: stdout.lossy
674
+ };
675
+ }
676
+
677
+ //#endregion
678
+ //#region tsbuild/host/git.js
679
+ /**
680
+ * `git`'s own separator between the two paths of a rename record and between status lines.
681
+ * Requested explicitly (`-z`) so a path containing a newline or a quote cannot be misread — git's
682
+ * default output would C-quote such a path and this parser would then compare a quoted spelling
683
+ * against the unquoted one a later diff request repeats.
684
+ */
685
+ const NUL_ARGS = ["-z"];
686
+ /** Exit code of the synthetic outcome produced when the `git` binary does not resolve. */
687
+ const MISSING_BINARY_EXIT = 127;
688
+ /** The remote a branch with no upstream is published to when nothing else names one. */
689
+ const DEFAULT_REMOTE = "origin";
690
+ /**
691
+ * What the model is told a commit message is, when the settings section supplies no prompt of its
692
+ * own.
693
+ *
694
+ * It states the format because a model asked for "a commit message" writes a paragraph as readily
695
+ * as a subject line, and the panel puts the answer straight into the box a person then edits.
696
+ */
697
+ const DEFAULT_COMMIT_PROMPT = [
698
+ "Write a git commit message for the supplied diff.",
699
+ "Answer with the message alone: no preamble, no explanation, no code fences, no quotes.",
700
+ "First line: imperative mood, under 72 characters, no trailing period.",
701
+ "Then, only if the change needs it, a blank line and a short body explaining WHY rather than restating the diff. Wrap the body at 72 columns.",
702
+ "Describe what the diff actually does. Do not invent a motive it does not show."
703
+ ].join("\n");
704
+ /** Compose one classified failure. */
705
+ function fail$4(code, message) {
706
+ return {
707
+ ok: false,
708
+ code,
709
+ message
710
+ };
711
+ }
712
+ /** First non-empty line of git's stderr, which is the part worth showing. */
713
+ function stderrLine(outcome) {
714
+ return outcome.stderr.split("\n").map((part) => part.trim()).find((part) => part !== "") ?? `git exited with code ${String(outcome.exitCode)}`;
715
+ }
716
+ /**
717
+ * Turn a non-zero exit into the right classified failure.
718
+ * @param outcome - the finished command.
719
+ * @returns the failure a caller should return.
720
+ */
721
+ function classify(outcome) {
722
+ if (outcome.timedOut) return fail$4("timeout", "git did not finish within gitTimeoutMs");
723
+ if (outcome.aborted) return fail$4("cancelled", "the request was abandoned before git finished");
724
+ if (outcome.exitCode === MISSING_BINARY_EXIT) return fail$4("no-git", "git is not installed, or not on this Host process PATH");
725
+ const message = stderrLine(outcome);
726
+ if (/not a git repository/iu.test(message)) return fail$4("not-a-repository", message);
727
+ return fail$4("git-failed", message);
728
+ }
729
+ /**
730
+ * Take a model's answer down to the message itself.
731
+ *
732
+ * A model asked for plain text still fences it often enough that the panel would otherwise put
733
+ * ```` ``` ```` into a commit; the wrapper is removed only when it wraps the WHOLE answer, so a
734
+ * message that legitimately quotes a fenced block keeps it.
735
+ * @param text - what the model streamed.
736
+ * @returns the message, trimmed.
737
+ */
738
+ function stripFence(text) {
739
+ const trimmed = text.trim();
740
+ return (/^```[^\n]*\n([\s\S]*)\n```$/u.exec(trimmed)?.[1] ?? trimmed).trim();
741
+ }
742
+ /**
743
+ * Reads one workspace's git state. One instance serves every request; the resolved `git` path is
744
+ * cached across calls and dropped whenever a lookup fails, so installing git later needs no restart.
745
+ */
746
+ var GitReader = class {
747
+ ctx;
748
+ source;
749
+ executable;
750
+ version;
751
+ /**
752
+ * @param ctx - Host context carrying the subprocess and filesystem capabilities.
753
+ * @param source - reads the current settings section; called per request so a committed change
754
+ * reaches the next reading with no registration to rebuild.
755
+ */
756
+ constructor(ctx, source) {
757
+ this.ctx = ctx;
758
+ this.source = source;
759
+ }
760
+ /**
761
+ * Report whether git can answer on this Host.
762
+ * @param signal - cancellation for the lookup.
763
+ * @returns availability plus the version string when one was read.
764
+ */
765
+ async describe(signal) {
766
+ let executable;
767
+ try {
768
+ executable = await this.locate(signal);
769
+ } catch (error) {
770
+ /* v8 ignore next -- only a missing subprocess capability reaches here. */
771
+ return {
772
+ available: false,
773
+ reason: error instanceof Error ? error.message : String(error)
774
+ };
775
+ }
776
+ if (executable === void 0) return {
777
+ available: false,
778
+ reason: "git is not installed, or not on this Host process PATH"
779
+ };
780
+ return {
781
+ available: true,
782
+ ...this.version === void 0 ? {} : { detail: this.version }
783
+ };
784
+ }
785
+ /**
786
+ * Read one workspace's changed paths.
787
+ * @param request - the workspace directory to read.
788
+ * @param signal - cancellation for the reading.
789
+ * @returns the reading, or a classified failure.
790
+ */
791
+ async status(request, signal) {
792
+ const prepared = await this.prepare(request.workspacePath, signal);
793
+ if ("failure" in prepared) return prepared.failure;
794
+ const { repository, workspace } = prepared;
795
+ const outcome = await this.git(workspace.value.processPath, [
796
+ "status",
797
+ "--porcelain=v2",
798
+ "--branch",
799
+ "--untracked-files=all",
800
+ ...NUL_ARGS
801
+ ], signal);
802
+ if (outcome.exitCode !== 0) return classify(outcome);
803
+ const parsed = parsePorcelainV2(outcome.stdout);
804
+ const limit = this.source().gitMaxFiles;
805
+ const truncated = outcome.stdoutLossy || parsed.changes.length > limit;
806
+ const changes = truncated ? parsed.changes.slice(0, limit) : parsed.changes;
807
+ const conflicted = changes.filter((change) => change.conflicted);
808
+ return {
809
+ ok: true,
810
+ write: await this.writeCapability(repository.root, signal),
811
+ repositoryRoot: repository.root,
812
+ prefix: repository.prefix,
813
+ ...parsed.branch.branch === void 0 ? {} : { branch: parsed.branch.branch },
814
+ ...parsed.branch.upstream === void 0 ? {} : { upstream: parsed.branch.upstream },
815
+ ahead: parsed.branch.ahead,
816
+ behind: parsed.branch.behind,
817
+ detached: parsed.branch.detached,
818
+ staged: changes.filter(isStaged),
819
+ unstaged: changes.filter(isUnstaged),
820
+ untracked: changes.filter((change) => change.untracked),
821
+ conflicted,
822
+ truncated,
823
+ readAt: Date.now()
824
+ };
825
+ }
826
+ /**
827
+ * Read one path's patch.
828
+ * @param request - which path, and which of the two indexes to compare.
829
+ * @param signal - cancellation for the reading.
830
+ * @returns the patch, or a classified failure.
831
+ */
832
+ async diff(request, signal) {
833
+ const prepared = await this.prepare(request.workspacePath, signal);
834
+ if ("failure" in prepared) return prepared.failure;
835
+ const { repository } = prepared;
836
+ const contained = await this.contain(repository.root, [request.path], signal);
837
+ if (contained !== void 0) return contained;
838
+ const common = [
839
+ "--no-pager",
840
+ "diff",
841
+ "--no-color",
842
+ "--no-ext-diff"
843
+ ];
844
+ const argv = request.untracked ? [
845
+ ...common,
846
+ "--no-index",
847
+ "--",
848
+ devNull(),
849
+ request.path
850
+ ] : [
851
+ ...common,
852
+ ...request.staged ? ["--cached"] : [],
853
+ "--",
854
+ request.path
855
+ ];
856
+ const max = this.source().gitDiffMaxBytes;
857
+ const outcome = await this.git(repository.root, argv, signal, max);
858
+ const differed = request.untracked && outcome.exitCode === 1 && outcome.stderr.trim() === "";
859
+ if (outcome.exitCode !== 0 && !differed) return classify(outcome);
860
+ const patch = outcome.stdout;
861
+ const truncated = outcome.stdoutLossy || patch.length > max;
862
+ return {
863
+ ok: true,
864
+ path: request.path,
865
+ patch: patch.length > max ? patch.slice(0, max) : patch,
866
+ binary: /^Binary files .* differ$/mu.test(patch),
867
+ truncated
868
+ };
869
+ }
870
+ /**
871
+ * Stage paths.
872
+ * @param request - the workspace and the repository-relative paths to add.
873
+ * @param signal - cancellation for the write and the reading that follows it.
874
+ * @returns the reading after the write, or a classified failure.
875
+ */
876
+ stage(request, signal) {
877
+ if (!this.source().allowGitStaging) return Promise.resolve(fail$4("disabled", "staging is switched off in the advanced-sidebar settings"));
878
+ return this.write(request, signal, (paths) => [
879
+ "add",
880
+ "--",
881
+ ...paths
882
+ ]);
883
+ }
884
+ /**
885
+ * Unstage paths, leaving the working tree untouched.
886
+ * @param request - the workspace and the repository-relative paths to restore.
887
+ * @param signal - cancellation for the write and the reading that follows it.
888
+ * @returns the reading after the write, or a classified failure.
889
+ */
890
+ unstage(request, signal) {
891
+ if (!this.source().allowGitStaging) return Promise.resolve(fail$4("disabled", "staging is switched off in the advanced-sidebar settings"));
892
+ return this.write(request, signal, (paths) => [
893
+ "restore",
894
+ "--staged",
895
+ "--",
896
+ ...paths
897
+ ]);
898
+ }
899
+ /**
900
+ * Record the staged changes.
901
+ *
902
+ * The message crosses as one argv element, so no shell ever sees it and nothing in it can become
903
+ * an option. Hooks run: a `pre-commit` that refuses is a real answer, and its stderr is returned
904
+ * verbatim rather than summarized.
905
+ * @param request - the workspace, the message, and whether to replace the previous commit.
906
+ * @param signal - cancellation for the commit and the reading that follows it.
907
+ * @returns the new commit and the reading after it, or a classified failure.
908
+ */
909
+ async commit(request, signal) {
910
+ const settings = this.source();
911
+ if (!settings.allowGitCommit) return fail$4("disabled", "committing is switched off in the advanced-sidebar settings");
912
+ const message = request.message.trim();
913
+ if (message === "") return fail$4("empty-message", "a commit needs a message");
914
+ const prepared = await this.prepare(request.workspacePath, signal);
915
+ if ("failure" in prepared) return prepared.failure;
916
+ const { repository } = prepared;
917
+ if (await this.author(repository.root, signal) === void 0) return fail$4("no-identity", "git has no user.name and user.email, so it has no author to record; set them with `git config --global user.name` and `git config --global user.email`");
918
+ if (!request.amend && !await this.hasStaged(repository.root, signal)) return fail$4("nothing-staged", "nothing is staged, so there is nothing to commit");
919
+ const outcome = await this.git(repository.root, [
920
+ "commit",
921
+ ...request.amend ? ["--amend"] : [],
922
+ "-m",
923
+ message
924
+ ], signal, void 0, settings.gitCommitTimeoutMs);
925
+ if (outcome.exitCode !== 0) return classify(outcome);
926
+ const [commit, subject] = (await this.git(repository.root, [
927
+ "log",
928
+ "-1",
929
+ "--format=%h%n%s"
930
+ ], signal)).stdout.split("\n");
931
+ const status = await this.status({ workspacePath: request.workspacePath }, signal);
932
+ if (!status.ok) return status;
933
+ return {
934
+ ok: true,
935
+ commit: (commit ?? "").trim(),
936
+ subject: (subject ?? "").trim(),
937
+ status,
938
+ notes: outcome.stderr.trim()
939
+ };
940
+ }
941
+ /**
942
+ * Send the current branch's commits to its remote.
943
+ *
944
+ * Only the current branch, and only to its own upstream: a refspec assembled from browser text
945
+ * would let one button push anything anywhere, and `git push` with no arguments already means
946
+ * exactly what the panel offers. A branch with no upstream is published only when the caller asks
947
+ * for it, because choosing a remote is a decision rather than a default.
948
+ * @param request - the workspace, and whether an unpublished branch may be published.
949
+ * @param signal - cancellation; the network wait runs under `gitPushTimeoutMs`.
950
+ * @returns the push, the reading after it, or a classified failure.
951
+ */
952
+ async push(request, signal) {
953
+ const settings = this.source();
954
+ if (!settings.allowGitPush) return fail$4("disabled", "pushing is switched off in the advanced-sidebar settings");
955
+ const prepared = await this.prepare(request.workspacePath, signal);
956
+ if ("failure" in prepared) return prepared.failure;
957
+ const { repository } = prepared;
958
+ const before = await this.status({ workspacePath: request.workspacePath }, signal);
959
+ if (!before.ok) return before;
960
+ if (before.detached || before.branch === void 0) return fail$4("detached-head", "HEAD names a commit rather than a branch, so there is nothing to push");
961
+ const branch = before.branch;
962
+ const upstream = before.upstream;
963
+ if (upstream === void 0 && !request.setUpstream) return fail$4("no-upstream", `${branch} has no upstream; publish it to a remote first, or use Publish to record one`);
964
+ const remote = upstream === void 0 ? await this.defaultRemote(repository.root, signal) : upstream.split("/")[0] ?? DEFAULT_REMOTE;
965
+ if (remote === void 0) return fail$4("no-upstream", "this repository has no remote to push to");
966
+ const outcome = await this.git(repository.root, upstream === void 0 ? [
967
+ "push",
968
+ "--set-upstream",
969
+ remote,
970
+ branch
971
+ ] : ["push"], signal, void 0, settings.gitPushTimeoutMs);
972
+ if (outcome.exitCode !== 0) return classify(outcome);
973
+ const status = await this.status({ workspacePath: request.workspacePath }, signal);
974
+ if (!status.ok) return status;
975
+ return {
976
+ ok: true,
977
+ branch,
978
+ remote,
979
+ published: upstream === void 0,
980
+ status,
981
+ notes: `${outcome.stdout}\n${outcome.stderr}`.trim()
982
+ };
983
+ }
984
+ /**
985
+ * Ask the deployment's own model to write a commit message for what is staged.
986
+ *
987
+ * The model sees the staged patch and nothing else — not the working tree, not the repository's
988
+ * history, not the session. It is the same model the composer is set to, so this needs no second
989
+ * credential and no second provider; a Host with no model reports the verb unavailable instead.
990
+ * @param request - the workspace, and whether the message is for an amend.
991
+ * @param signal - cancellation for the readings and the model call.
992
+ * @returns the drafted message, or a classified failure.
993
+ */
994
+ async draftCommitMessage(request, signal) {
995
+ const settings = this.source();
996
+ if (!settings.allowGitCommit || !settings.allowCommitMessageDraft) return fail$4("disabled", "the drafted commit message is switched off in the advanced-sidebar settings");
997
+ const llm = this.ctx.get("llm");
998
+ const models = this.ctx.get("agentDefaultModel");
999
+ if (llm === void 0 || models === void 0) return fail$4("no-model", "no model is configured for this deployment");
1000
+ const prepared = await this.prepare(request.workspacePath, signal);
1001
+ if ("failure" in prepared) return prepared.failure;
1002
+ const { repository } = prepared;
1003
+ const patch = await this.stagedPatch(repository.root, request.amend, signal);
1004
+ if ("failure" in patch) return patch.failure;
1005
+ if (patch.text.trim() === "") return fail$4("nothing-staged", "nothing is staged, so there is nothing to describe");
1006
+ const selection = models.currentSelection();
1007
+ try {
1008
+ let drafted = "";
1009
+ for await (const chunk of llm.stream({
1010
+ provider: selection.provider,
1011
+ model: selection.model,
1012
+ ...selection.reasoningEffort === void 0 ? {} : { reasoningEffort: selection.reasoningEffort },
1013
+ system: settings.commitMessagePrompt.trim() === "" ? DEFAULT_COMMIT_PROMPT : settings.commitMessagePrompt,
1014
+ messages: [createUserMessage({
1015
+ content: [{
1016
+ type: "text",
1017
+ text: patch.text
1018
+ }],
1019
+ source: {
1020
+ kind: "plugin",
1021
+ plugin: "dsh-advanced-sidebar"
1022
+ }
1023
+ })],
1024
+ ...signal === void 0 ? {} : { signal }
1025
+ })) if (chunk.type === "text-delta") drafted += chunk.text;
1026
+ const message = stripFence(drafted);
1027
+ if (message === "") return fail$4("llm-failed", "the model returned no message");
1028
+ return {
1029
+ ok: true,
1030
+ message,
1031
+ model: `${selection.provider}/${selection.model}`,
1032
+ truncated: patch.truncated
1033
+ };
1034
+ } catch (error) {
1035
+ if (signal?.aborted === true) return fail$4("cancelled", "the request was abandoned");
1036
+ return fail$4("llm-failed", error instanceof Error ? error.message : "the model request failed");
1037
+ }
1038
+ }
1039
+ /**
1040
+ * The patch a drafted message describes, bounded so a large change cannot become a large request.
1041
+ * @param root - absolute repository root.
1042
+ * @param amend - describe the previous commit's content as well as the index.
1043
+ * @param signal - cancellation for the invocations.
1044
+ * @returns the patch and whether it was cut, or the failure to return.
1045
+ */
1046
+ async stagedPatch(root, amend, signal) {
1047
+ const settings = this.source();
1048
+ const base = amend && await this.hasParent(root, signal) ? ["HEAD~1"] : [];
1049
+ const stat = await this.git(root, [
1050
+ "diff",
1051
+ "--cached",
1052
+ "--stat",
1053
+ ...base
1054
+ ], signal);
1055
+ if (stat.exitCode !== 0) return { failure: classify(stat) };
1056
+ const cap = settings.commitMessageMaxBytes;
1057
+ const patch = await this.git(root, [
1058
+ "diff",
1059
+ "--cached",
1060
+ "--no-color",
1061
+ ...base
1062
+ ], signal, cap);
1063
+ if (patch.exitCode !== 0) return { failure: classify(patch) };
1064
+ const truncated = patch.stdoutLossy || patch.stdout.length >= cap;
1065
+ return {
1066
+ text: [
1067
+ stat.stdout.trim(),
1068
+ "",
1069
+ truncated ? patch.stdout.slice(0, cap) : patch.stdout,
1070
+ truncated ? "\n[the patch was truncated here]" : ""
1071
+ ].join("\n").trim(),
1072
+ truncated
1073
+ };
1074
+ }
1075
+ /**
1076
+ * Whether HEAD has a parent commit.
1077
+ * @param cwd - absolute repository root.
1078
+ * @param signal - cancellation for the invocation.
1079
+ * @returns true when `HEAD~1` resolves.
1080
+ */
1081
+ async hasParent(cwd, signal) {
1082
+ return (await this.git(cwd, [
1083
+ "rev-parse",
1084
+ "--verify",
1085
+ "--quiet",
1086
+ "HEAD~1"
1087
+ ], signal)).exitCode === 0;
1088
+ }
1089
+ /**
1090
+ * The remote an unpublished branch would be published to.
1091
+ * @param cwd - absolute repository root.
1092
+ * @param signal - cancellation for the invocation.
1093
+ * @returns `origin` when it exists, else the first remote, else undefined.
1094
+ */
1095
+ async defaultRemote(cwd, signal) {
1096
+ const outcome = await this.git(cwd, ["remote"], signal);
1097
+ if (outcome.exitCode !== 0) return void 0;
1098
+ const remotes = outcome.stdout.split("\n").map((line) => line.trim()).filter((line) => line !== "");
1099
+ return remotes.includes(DEFAULT_REMOTE) ? DEFAULT_REMOTE : remotes[0];
1100
+ }
1101
+ /**
1102
+ * Run one index write over contained paths, then re-read the repository.
1103
+ * @param request - the workspace and the paths.
1104
+ * @param signal - cancellation for both invocations.
1105
+ * @param argv - builds the git arguments from the accepted paths.
1106
+ * @returns the reading after the write, or a classified failure.
1107
+ */
1108
+ async write(request, signal, argv) {
1109
+ if (request.paths.length === 0) return fail$4("path-denied", "no paths were given");
1110
+ const prepared = await this.prepare(request.workspacePath, signal);
1111
+ if ("failure" in prepared) return prepared.failure;
1112
+ const { repository } = prepared;
1113
+ const contained = await this.contain(repository.root, request.paths, signal);
1114
+ if (contained !== void 0) return contained;
1115
+ const outcome = await this.git(repository.root, argv(request.paths), signal);
1116
+ if (outcome.exitCode !== 0) return classify(outcome);
1117
+ const status = await this.status({ workspacePath: request.workspacePath }, signal);
1118
+ return status.ok ? {
1119
+ ok: true,
1120
+ status
1121
+ } : status;
1122
+ }
1123
+ /**
1124
+ * Prove every path sits inside the repository before git is handed any of them.
1125
+ *
1126
+ * A path from the browser is untrusted input at a process boundary. `git add` and
1127
+ * `git restore` both accept absolute paths and `..`, and `git diff --no-index` will read any file
1128
+ * at all — so an unchecked path is a write outside the repository in one direction and an
1129
+ * exfiltration route in the other.
1130
+ * @param root - absolute repository root.
1131
+ * @param paths - repository-relative paths, as the browser sent them.
1132
+ * @param signal - cancellation for the resolutions.
1133
+ * @returns the failure to return, or undefined when every path is inside.
1134
+ */
1135
+ async contain(root, paths, signal) {
1136
+ const resolved = await resolveWorkspace(this.ctx, root, signal);
1137
+ if (!resolved.ok) return fail$4(resolved.rejection.code, resolved.rejection.message);
1138
+ for (const path of paths) {
1139
+ const inside = await resolveInside(this.ctx, resolved.value, resolve(root, path), signal);
1140
+ if (!inside.ok) return fail$4(inside.rejection.code, inside.rejection.message);
1141
+ }
1142
+ }
1143
+ /**
1144
+ * The author `git commit` would record.
1145
+ * @param cwd - the repository root.
1146
+ * @param signal - cancellation for the invocation.
1147
+ * @returns `Name <email>`, or undefined when git has no identity configured.
1148
+ */
1149
+ async author(cwd, signal) {
1150
+ const outcome = await this.git(cwd, ["var", "GIT_AUTHOR_IDENT"], signal);
1151
+ if (outcome.exitCode !== 0) return void 0;
1152
+ const ident = outcome.stdout.trim();
1153
+ const at = ident.lastIndexOf(">");
1154
+ return at < 0 ? void 0 : ident.slice(0, at + 1);
1155
+ }
1156
+ /**
1157
+ * Whether the index differs from HEAD.
1158
+ * @param cwd - the repository root.
1159
+ * @param signal - cancellation for the invocation.
1160
+ * @returns true when a commit would record something.
1161
+ */
1162
+ async hasStaged(cwd, signal) {
1163
+ return (await this.git(cwd, [
1164
+ "diff",
1165
+ "--cached",
1166
+ "--quiet"
1167
+ ], signal)).exitCode !== 0;
1168
+ }
1169
+ /**
1170
+ * Report what the panel may do to this repository.
1171
+ * @param cwd - the repository root.
1172
+ * @param signal - cancellation for the identity lookup.
1173
+ * @returns the write capability.
1174
+ */
1175
+ async writeCapability(cwd, signal) {
1176
+ const settings = this.source();
1177
+ const canStage = settings.allowGitStaging;
1178
+ const canCommit = canStage && settings.allowGitCommit;
1179
+ const canDraftMessage = canCommit && settings.allowCommitMessageDraft && this.ctx.get("llm") !== void 0 && this.ctx.get("agentDefaultModel") !== void 0;
1180
+ const author = canCommit ? await this.author(cwd, signal) : void 0;
1181
+ return {
1182
+ canStage,
1183
+ canCommit,
1184
+ canPush: settings.allowGitPush,
1185
+ canDraftMessage,
1186
+ ...author === void 0 ? {} : { author }
1187
+ };
1188
+ }
1189
+ /**
1190
+ * Resolve the workspace and its repository once for both endpoints.
1191
+ * @param workspacePath - the browser-supplied directory.
1192
+ * @param signal - cancellation for the resolution and the `rev-parse`.
1193
+ * @returns the repository and the resolved workspace, or the failure to return.
1194
+ */
1195
+ async prepare(workspacePath, signal) {
1196
+ const workspace = await resolveWorkspace(this.ctx, workspacePath, signal);
1197
+ if (!workspace.ok) return { failure: fail$4(workspace.rejection.code, workspace.rejection.message) };
1198
+ const repository = await this.locateRepository(workspace.value.processPath, signal);
1199
+ if ("failure" in repository) return { failure: repository.failure };
1200
+ return {
1201
+ repository: repository.repository,
1202
+ workspace
1203
+ };
1204
+ }
1205
+ /**
1206
+ * Ask git where the repository containing a directory begins.
1207
+ * @param cwd - the canonical workspace directory.
1208
+ * @param signal - cancellation for the invocation.
1209
+ * @returns the repository, or the failure to return.
1210
+ */
1211
+ async locateRepository(cwd, signal) {
1212
+ const outcome = await this.git(cwd, [
1213
+ "rev-parse",
1214
+ "--show-toplevel",
1215
+ "--show-prefix"
1216
+ ], signal);
1217
+ if (outcome.exitCode !== 0) return { failure: classify(outcome) };
1218
+ const [root, prefix] = outcome.stdout.split("\n");
1219
+ if (root === void 0 || root.trim() === "") return { failure: fail$4("not-a-repository", `${cwd} is not inside a git repository`) };
1220
+ return { repository: {
1221
+ root: root.trim(),
1222
+ prefix: (prefix ?? "").trim().replace(/\/$/u, "")
1223
+ } };
1224
+ }
1225
+ /**
1226
+ * Run one git invocation with this plugin's own bounds.
1227
+ * @param cwd - directory to run in.
1228
+ * @param args - arguments after the executable.
1229
+ * @param signal - the caller's cancellation.
1230
+ * @returns the finished command.
1231
+ */
1232
+ async git(cwd, args, signal, maxBytes, timeoutMs) {
1233
+ const executable = await this.locate(signal);
1234
+ if (executable === void 0) return {
1235
+ exitCode: MISSING_BINARY_EXIT,
1236
+ signal: null,
1237
+ stdout: "",
1238
+ stderr: "git is not installed, or not on this Host process PATH",
1239
+ timedOut: false,
1240
+ aborted: false,
1241
+ stdoutLossy: false
1242
+ };
1243
+ const settings = this.source();
1244
+ return runCommand(this.ctx, {
1245
+ argv: [executable, ...args],
1246
+ cwd,
1247
+ timeoutMs: timeoutMs ?? settings.gitTimeoutMs,
1248
+ maxBytes: maxBytes ?? Math.max(settings.gitDiffMaxBytes, 1 << 20),
1249
+ graceMs: 1e3,
1250
+ env: {
1251
+ GIT_PAGER: "cat",
1252
+ GIT_OPTIONAL_LOCKS: "0",
1253
+ LC_ALL: "C",
1254
+ GIT_EDITOR: "true",
1255
+ GIT_TERMINAL_PROMPT: "0"
1256
+ }
1257
+ }, signal);
1258
+ }
1259
+ /**
1260
+ * Resolve `git` once and remember it.
1261
+ * @param signal - cancellation for the lookup.
1262
+ * @returns the executable path, or undefined when git is absent.
1263
+ * @throws {CommandUnavailableError} when no subprocess capability is mounted.
1264
+ */
1265
+ async locate(signal) {
1266
+ if (this.executable !== void 0) return this.executable;
1267
+ const found = await resolveCommand(this.ctx, "git", signal);
1268
+ if (found === void 0) return void 0;
1269
+ this.executable = found;
1270
+ try {
1271
+ const outcome = await runCommand(this.ctx, {
1272
+ argv: [found, "--version"],
1273
+ cwd: process.cwd(),
1274
+ timeoutMs: 5e3,
1275
+ maxBytes: 4096,
1276
+ graceMs: 1e3
1277
+ }, signal);
1278
+ if (outcome.exitCode === 0) this.version = outcome.stdout.trim();
1279
+ } catch (error) {
1280
+ /* v8 ignore next -- runCommand only throws for a withdrawn subprocess capability. */
1281
+ if (!(error instanceof CommandUnavailableError)) throw error;
1282
+ }
1283
+ return found;
1284
+ }
1285
+ };
1286
+ /**
1287
+ * The empty left-hand side of an untracked file's synthesized patch.
1288
+ * @returns the platform's null device path.
1289
+ */
1290
+ function devNull() {
1291
+ return process.platform === "win32" ? "NUL" : "/dev/null";
1292
+ }
1293
+
1294
+ //#endregion
1295
+ //#region tsbuild/host/open-in.js
1296
+ /** Id of the built-in file-manager target; no configured editor may claim it. */
1297
+ const REVEAL_TARGET_ID = "reveal";
1298
+ /** Wall-clock bound on a launch. A desktop opener returns immediately or is broken. */
1299
+ const LAUNCH_TIMEOUT_MS = 15e3;
1300
+ /** TERM-to-KILL grace for a launcher that ignored its deadline. */
1301
+ const LAUNCH_GRACE_MS = 2e3;
1302
+ /** Compose one classified failure. */
1303
+ function fail$3(code, message) {
1304
+ return {
1305
+ ok: false,
1306
+ code,
1307
+ message
1308
+ };
1309
+ }
1310
+ /** The file manager's name on this platform, used as the target's menu text. */
1311
+ function revealLabel(platform) {
1312
+ if (platform === "darwin") return "Finder";
1313
+ if (platform === "win32") return "File Explorer";
1314
+ return "File manager";
1315
+ }
1316
+ /**
1317
+ * Launches external applications for the Open in submenu. One instance serves every request and
1318
+ * caches each target's resolved executable.
1319
+ */
1320
+ var OpenInLauncher = class {
1321
+ ctx;
1322
+ source;
1323
+ resolved = /* @__PURE__ */ new Map();
1324
+ /**
1325
+ * @param ctx - Host context carrying the subprocess and filesystem capabilities.
1326
+ * @param source - reads the current settings section; called per request so an edited target list
1327
+ * reaches the next menu with no registration to rebuild.
1328
+ */
1329
+ constructor(ctx, source) {
1330
+ this.ctx = ctx;
1331
+ this.source = source;
1332
+ }
1333
+ /**
1334
+ * List every target in menu order with its availability.
1335
+ *
1336
+ * Unavailable targets are listed rather than hidden: a person who configured an editor and does
1337
+ * not see it cannot tell a typo in `command` from a menu that simply has no such feature.
1338
+ * @param signal - cancellation for the executable lookups.
1339
+ * @returns the targets, file manager first.
1340
+ */
1341
+ async describe(signal) {
1342
+ const platform = process.platform;
1343
+ const views = [{
1344
+ id: REVEAL_TARGET_ID,
1345
+ label: revealLabel(platform),
1346
+ available: await this.available(this.revealCommand(platform).argv[0] ?? "", signal),
1347
+ kind: "reveal"
1348
+ }];
1349
+ for (const editor of this.source().editors) views.push({
1350
+ id: editor.id,
1351
+ label: editor.label,
1352
+ available: await this.available(editor.command, signal),
1353
+ kind: "command"
1354
+ });
1355
+ return views;
1356
+ }
1357
+ /**
1358
+ * Hand one path to a target.
1359
+ * @param request - which target, and which absolute path.
1360
+ * @param signal - cancellation for the launch.
1361
+ * @returns settlement, or a classified failure.
1362
+ */
1363
+ async open(request, signal) {
1364
+ const resolvedPath = await resolveWorkspace(this.ctx, request.path, signal);
1365
+ const target = resolvedPath.ok ? resolvedPath.value.processPath : await this.resolveFile(request.path, signal);
1366
+ if (target === void 0) return fail$3("path-denied", `${request.path} does not exist on this Host`);
1367
+ const directory = resolvedPath.ok;
1368
+ if (request.targetId === REVEAL_TARGET_ID) return this.reveal(target, directory, signal);
1369
+ const editor = this.source().editors.find((entry) => entry.id === request.targetId);
1370
+ if (editor === void 0) return fail$3("unknown-target", `no Open in target "${request.targetId}"`);
1371
+ const executable = await this.locate(editor.command, signal);
1372
+ if (executable === void 0) return fail$3("unavailable", `${editor.label}: "${editor.command}" does not resolve on this Host`);
1373
+ return this.launch([
1374
+ executable,
1375
+ ...editor.args,
1376
+ target
1377
+ ], signal);
1378
+ }
1379
+ /**
1380
+ * Show a path in the operating system's file manager.
1381
+ * @param target - the canonical path.
1382
+ * @param directory - whether the path is a directory (opened) or a file (selected).
1383
+ * @param signal - cancellation for the launch.
1384
+ * @returns settlement, or a classified failure.
1385
+ */
1386
+ async reveal(target, directory, signal) {
1387
+ const platform = process.platform;
1388
+ const { argv, tolerateExit } = this.revealCommand(platform, target, directory);
1389
+ const command = argv[0];
1390
+ if (command === void 0) return fail$3("unavailable", `no file manager is known for ${platform}`);
1391
+ const executable = await this.locate(command, signal);
1392
+ if (executable === void 0) return fail$3("unavailable", `${command} does not resolve on this Host`);
1393
+ return this.launch([executable, ...argv.slice(1)], signal, tolerateExit);
1394
+ }
1395
+ /**
1396
+ * The platform's file-manager invocation.
1397
+ * @param platform - the Host platform.
1398
+ * @param target - the canonical path; omitted while only the executable name is needed.
1399
+ * @param directory - whether the path is a directory.
1400
+ * @returns the argv and whether a non-zero exit is normal for it.
1401
+ */
1402
+ revealCommand(platform, target = "", directory = true) {
1403
+ if (platform === "darwin") return {
1404
+ argv: directory ? ["open", target] : [
1405
+ "open",
1406
+ "-R",
1407
+ target
1408
+ ],
1409
+ tolerateExit: false
1410
+ };
1411
+ if (platform === "win32") return {
1412
+ argv: directory ? ["explorer.exe", target] : ["explorer.exe", `/select,${target}`],
1413
+ tolerateExit: true
1414
+ };
1415
+ return {
1416
+ argv: ["xdg-open", directory ? target : dirname(target)],
1417
+ tolerateExit: false
1418
+ };
1419
+ }
1420
+ /**
1421
+ * Run one launcher and classify its outcome.
1422
+ * @param argv - resolved executable and arguments.
1423
+ * @param signal - cancellation for the launch.
1424
+ * @param tolerateExit - accept a non-zero exit as success (Windows Explorer).
1425
+ * @returns settlement, or a classified failure.
1426
+ */
1427
+ async launch(argv, signal, tolerateExit = false) {
1428
+ let outcome;
1429
+ try {
1430
+ outcome = await runCommand(this.ctx, {
1431
+ argv,
1432
+ cwd: process.cwd(),
1433
+ timeoutMs: LAUNCH_TIMEOUT_MS,
1434
+ maxBytes: 8192,
1435
+ graceMs: LAUNCH_GRACE_MS
1436
+ }, signal);
1437
+ } catch (error) {
1438
+ if (error instanceof CommandUnavailableError) return fail$3("unavailable", error.message);
1439
+ throw error;
1440
+ }
1441
+ if (outcome.timedOut) return fail$3("timeout", `${argv[0] ?? "launcher"} did not return within ${String(LAUNCH_TIMEOUT_MS)}ms`);
1442
+ if (outcome.exitCode === 0 || tolerateExit) return { ok: true };
1443
+ const detail = outcome.stderr.trim();
1444
+ return fail$3("launch-failed", detail === "" ? `${argv[0] ?? "launcher"} exited with code ${String(outcome.exitCode)}` : detail);
1445
+ }
1446
+ /**
1447
+ * Resolve a file path that is not a directory.
1448
+ * @param path - the browser-supplied path.
1449
+ * @param signal - cancellation for the resolution.
1450
+ * @returns the canonical path, or undefined when nothing is there.
1451
+ */
1452
+ async resolveFile(path, signal) {
1453
+ const fs = this.ctx.get("fs");
1454
+ if (fs === void 0) return void 0;
1455
+ try {
1456
+ const resolved = await fs.resolve(path, signal === void 0 ? {} : { signal });
1457
+ return await fs.stat(resolved, signal) === void 0 ? void 0 : fs.processPath(resolved);
1458
+ } catch {
1459
+ return;
1460
+ }
1461
+ }
1462
+ /**
1463
+ * Whether one command resolves, without reporting why it does not.
1464
+ * @param command - executable name or path.
1465
+ * @param signal - cancellation for the lookup.
1466
+ * @returns true when the command resolves.
1467
+ */
1468
+ async available(command, signal) {
1469
+ if (command === "") return false;
1470
+ try {
1471
+ return await this.locate(command, signal) !== void 0;
1472
+ } catch (error) {
1473
+ /* v8 ignore next -- only a missing subprocess capability reaches here. */
1474
+ if (error instanceof CommandUnavailableError) return false;
1475
+ /* v8 ignore next */
1476
+ throw error;
1477
+ }
1478
+ }
1479
+ /**
1480
+ * Resolve one command once and remember the answer, negative answers included.
1481
+ * @param command - executable name or path.
1482
+ * @param signal - cancellation for the lookup.
1483
+ * @returns the executable path, or undefined when the command is absent.
1484
+ * @throws {CommandUnavailableError} when no subprocess capability is mounted.
1485
+ */
1486
+ async locate(command, signal) {
1487
+ if (this.resolved.has(command)) return this.resolved.get(command);
1488
+ const found = await resolveCommand(this.ctx, command, signal);
1489
+ this.resolved.set(command, found);
1490
+ return found;
1491
+ }
1492
+ /** Drop every cached lookup, so an edited target list or a newly installed editor is re-probed. */
1493
+ forget() {
1494
+ this.resolved.clear();
1495
+ }
1496
+ };
1497
+
1498
+ //#endregion
1499
+ //#region tsbuild/host/preview.js
1500
+ /** Claude Code's launch file, read relative to the workspace root. */
1501
+ const LAUNCH_FILE = ".claude/launch.json";
1502
+ /** How long one readiness probe waits for the socket before retrying. */
1503
+ const PROBE_TIMEOUT_MS = 500;
1504
+ /** How long to wait between readiness probes. */
1505
+ const PROBE_INTERVAL_MS = 250;
1506
+ /** In-memory cap per collected stream, in bytes. Generous: a dev server's startup banner is small
1507
+ * and its request log is what a person scrolls back through. */
1508
+ const STREAM_MAX_BYTES = 4 * 1024 * 1024;
1509
+ /** Compose one classified failure. */
1510
+ function fail$2(code, message) {
1511
+ return {
1512
+ ok: false,
1513
+ code,
1514
+ message
1515
+ };
1516
+ }
1517
+ /**
1518
+ * Where a configuration's frame should point.
1519
+ * @param launch - the configuration.
1520
+ * @returns the URL, or undefined when neither a url nor a port was given.
1521
+ */
1522
+ function urlOf(launch) {
1523
+ if (launch.url !== void 0 && launch.url !== "") return launch.url;
1524
+ if (launch.port === void 0 || launch.port <= 0) return void 0;
1525
+ return `http://127.0.0.1:${String(launch.port)}`;
1526
+ }
1527
+ /**
1528
+ * Whether a socket accepts a connection on one port.
1529
+ * @param port - the port to probe.
1530
+ * @param signal - cancellation of the whole readiness wait.
1531
+ * @returns true when the connection was accepted.
1532
+ */
1533
+ function accepts(port, signal) {
1534
+ return new Promise((resolve$1) => {
1535
+ const socket = connect({
1536
+ port,
1537
+ host: "127.0.0.1"
1538
+ });
1539
+ const settle = (value) => {
1540
+ socket.destroy();
1541
+ signal.removeEventListener("abort", onAbort);
1542
+ resolve$1(value);
1543
+ };
1544
+ const onAbort = () => {
1545
+ settle(false);
1546
+ };
1547
+ socket.setTimeout(PROBE_TIMEOUT_MS);
1548
+ socket.once("connect", () => {
1549
+ settle(true);
1550
+ });
1551
+ socket.once("timeout", () => {
1552
+ settle(false);
1553
+ });
1554
+ socket.once("error", () => {
1555
+ settle(false);
1556
+ });
1557
+ signal.addEventListener("abort", onAbort, { once: true });
1558
+ });
1559
+ }
1560
+ /** Sleep, resolving early when the wait is cancelled. */
1561
+ function pause(ms, signal) {
1562
+ return new Promise((resolve$1) => {
1563
+ const timer = setTimeout(() => {
1564
+ signal.removeEventListener("abort", onAbort);
1565
+ resolve$1();
1566
+ }, ms);
1567
+ const onAbort = () => {
1568
+ clearTimeout(timer);
1569
+ resolve$1();
1570
+ };
1571
+ signal.addEventListener("abort", onAbort, { once: true });
1572
+ });
1573
+ }
1574
+ /**
1575
+ * Read one launch file's configurations.
1576
+ *
1577
+ * A malformed file is reported rather than thrown: the panel still lists the settings rows and says
1578
+ * why the file was ignored, which is more useful than a Preview entry that refuses to open.
1579
+ * @param text - the file's contents.
1580
+ * @returns the configurations, or the reason the file was ignored.
1581
+ */
1582
+ function parseLaunchFile(text) {
1583
+ let document;
1584
+ try {
1585
+ document = JSON.parse(text);
1586
+ } catch (error) {
1587
+ return { error: `not valid JSON: ${error instanceof Error ? error.message : String(error)}` };
1588
+ }
1589
+ if (typeof document !== "object" || document === null) return { error: "the top level is not an object" };
1590
+ const configurations = document.configurations;
1591
+ if (!Array.isArray(configurations)) return { error: "it carries no `configurations` array" };
1592
+ const launches = [];
1593
+ for (const entry of configurations) {
1594
+ if (typeof entry !== "object" || entry === null) continue;
1595
+ const row = entry;
1596
+ const name = row.name;
1597
+ if (typeof name !== "string" || name === "") continue;
1598
+ const args = Array.isArray(row.runtimeArgs) ? row.runtimeArgs.filter((value) => typeof value === "string") : [];
1599
+ launches.push({
1600
+ name,
1601
+ ...typeof row.runtimeExecutable === "string" && row.runtimeExecutable !== "" ? { runtimeExecutable: row.runtimeExecutable } : {},
1602
+ runtimeArgs: args,
1603
+ ...typeof row.port === "number" && Number.isInteger(row.port) && row.port > 0 ? { port: row.port } : {},
1604
+ ...typeof row.url === "string" && row.url !== "" ? { url: row.url } : {},
1605
+ ...typeof row.cwd === "string" && row.cwd !== "" ? { cwd: row.cwd } : {},
1606
+ ...typeof row.env === "object" && row.env !== null ? { env: Object.fromEntries(Object.entries(row.env).filter((pair) => typeof pair[1] === "string")) } : {}
1607
+ });
1608
+ }
1609
+ return { launches };
1610
+ }
1611
+ /**
1612
+ * Merge a workspace's launch file with the settings rows.
1613
+ *
1614
+ * The file wins a name collision: a repository stating how to run itself outranks a deployment-wide
1615
+ * default that happens to use the same name.
1616
+ * @param fromFile - configurations read from `.claude/launch.json`.
1617
+ * @param fromSettings - configurations from the `previews` settings rows.
1618
+ * @returns the merged list, file rows first.
1619
+ */
1620
+ function mergeLaunches(fromFile, fromSettings) {
1621
+ const merged = fromFile.map((launch) => ({
1622
+ launch,
1623
+ origin: "launch-json"
1624
+ }));
1625
+ const claimed = new Set(fromFile.map((launch) => launch.name));
1626
+ for (const launch of fromSettings) {
1627
+ if (claimed.has(launch.name)) continue;
1628
+ claimed.add(launch.name);
1629
+ merged.push({
1630
+ launch,
1631
+ origin: "settings"
1632
+ });
1633
+ }
1634
+ return merged;
1635
+ }
1636
+ /**
1637
+ * Owns every preview server in the process. One instance is created by the service and disposed
1638
+ * with it, which is what guarantees no dev server outlives the plugin.
1639
+ */
1640
+ var PreviewServers = class {
1641
+ ctx;
1642
+ source;
1643
+ records = /* @__PURE__ */ new Map();
1644
+ /**
1645
+ * Configurations with a start already in flight, keyed `<workspace>\u0000<name>`.
1646
+ *
1647
+ * `start()` awaits a file read, a path resolution, and an executable lookup before it registers
1648
+ * anything, so two starts of one row would both find no predecessor and both spawn — leaving a
1649
+ * dev server on the port that no `serverId` any panel holds can ever stop.
1650
+ */
1651
+ starting = /* @__PURE__ */ new Set();
1652
+ closing = false;
1653
+ /**
1654
+ * @param ctx - Host context carrying the subprocess and filesystem capabilities.
1655
+ * @param source - reads the current settings section; called per request.
1656
+ */
1657
+ constructor(ctx, source) {
1658
+ this.ctx = ctx;
1659
+ this.source = source;
1660
+ }
1661
+ /**
1662
+ * Report whether a preview server can be started on this Host.
1663
+ * @returns availability plus how many servers are already running.
1664
+ */
1665
+ describe() {
1666
+ const running = [...this.records.values()].filter((record) => record.state !== "exited" && record.state !== "failed").length;
1667
+ if (this.ctx.get("subprocess") === void 0) return {
1668
+ available: false,
1669
+ reason: "no subprocess capability is mounted: this deployment composes no @deepseek-ai/dsh-subprocess provider",
1670
+ running
1671
+ };
1672
+ if (this.ctx.get("fs") === void 0) return {
1673
+ available: false,
1674
+ reason: "no filesystem capability is mounted: a launch configuration has no directory to run in",
1675
+ running
1676
+ };
1677
+ return {
1678
+ available: true,
1679
+ running
1680
+ };
1681
+ }
1682
+ /**
1683
+ * List one workspace's configurations, each with its current state.
1684
+ * @param request - the workspace to read.
1685
+ * @param signal - cancellation for the file read.
1686
+ * @returns the list, or a classified failure.
1687
+ */
1688
+ async list(request, signal) {
1689
+ const workspace = await resolveWorkspace(this.ctx, request.workspacePath, signal);
1690
+ if (!workspace.ok) return fail$2(workspace.rejection.code === "no-filesystem" ? "no-filesystem" : "path-denied", workspace.rejection.message);
1691
+ const file = await this.readLaunchFile(workspace, signal);
1692
+ return {
1693
+ ok: true,
1694
+ servers: mergeLaunches(file.launches, this.settingsLaunches()).map((resolved) => this.view(resolved, workspace.value.processPath)),
1695
+ ...file.path === void 0 ? {} : { launchFile: file.path },
1696
+ ...file.error === void 0 ? {} : { launchFileError: file.error }
1697
+ };
1698
+ }
1699
+ /**
1700
+ * Start one configuration.
1701
+ * @param request - the workspace and the configuration name.
1702
+ * @param signal - cancellation of the start itself; a started server owns its later lifetime.
1703
+ * @returns the started row, or a classified failure.
1704
+ */
1705
+ async start(request, signal) {
1706
+ if (this.closing) return fail$2("closed", "the plugin is unloading");
1707
+ if (this.ctx.get("subprocess") === void 0) return fail$2("no-subprocess", "no subprocess capability is mounted");
1708
+ const workspace = await resolveWorkspace(this.ctx, request.workspacePath, signal);
1709
+ if (!workspace.ok) return fail$2(workspace.rejection.code === "no-filesystem" ? "no-filesystem" : "path-denied", workspace.rejection.message);
1710
+ const resolved = mergeLaunches((await this.readLaunchFile(workspace, signal)).launches, this.settingsLaunches()).find((entry) => entry.launch.name === request.name);
1711
+ if (resolved === void 0) return fail$2("unknown-server", `no launch configuration named "${request.name}"`);
1712
+ const launch = resolved.launch;
1713
+ if (launch.runtimeExecutable === void 0 || launch.runtimeExecutable === "") return fail$2("not-startable", `"${launch.name}" names no command; it opens its url and starts nothing`);
1714
+ const key = `${workspace.value.processPath}\u0000${launch.name}`;
1715
+ if (this.starting.has(key)) return fail$2("limit-reached", `"${launch.name}" is already starting`);
1716
+ this.starting.add(key);
1717
+ try {
1718
+ return await this.spawn(workspace, launch, resolved.origin, signal);
1719
+ } finally {
1720
+ this.starting.delete(key);
1721
+ }
1722
+ }
1723
+ /**
1724
+ * Start one validated configuration, holding its in-flight claim.
1725
+ * @param workspace - the resolved workspace.
1726
+ * @param launch - the configuration, already known to name a command.
1727
+ * @param origin - which file it came from.
1728
+ * @param signal - cancellation of the start.
1729
+ * @returns the started row, or a classified failure.
1730
+ */
1731
+ async spawn(workspace, launch, origin, signal) {
1732
+ const subprocess = this.ctx.get("subprocess");
1733
+ /* v8 ignore next -- the caller resolved the same service moments earlier. */
1734
+ if (subprocess === void 0) return fail$2("no-subprocess", "no subprocess capability is mounted");
1735
+ const settings = this.source();
1736
+ const previous = this.find(workspace.value.processPath, launch.name);
1737
+ if (previous !== void 0) await this.terminate(previous);
1738
+ if ([...this.records.values()].filter((record$1) => record$1.state === "starting" || record$1.state === "ready").length >= settings.maxPreviews) return fail$2("limit-reached", `${String(settings.maxPreviews)} preview servers are already running`);
1739
+ const directory = await this.launchDirectory(workspace, launch, signal);
1740
+ if ("failure" in directory) return directory.failure;
1741
+ let executable;
1742
+ try {
1743
+ executable = await resolveCommand(this.ctx, launch.runtimeExecutable ?? "", signal);
1744
+ } catch (error) {
1745
+ return fail$2("no-subprocess", error instanceof CommandUnavailableError ? error.message : String(error));
1746
+ }
1747
+ if (executable === void 0) return fail$2("unavailable", `"${launch.runtimeExecutable ?? ""}" does not resolve on this Host`);
1748
+ if (this.closing) return fail$2("closed", "the plugin is unloading");
1749
+ let handle;
1750
+ try {
1751
+ handle = subprocess.spawn({
1752
+ argv: [executable, ...launch.runtimeArgs ?? []],
1753
+ cwd: directory.path,
1754
+ stdio: {
1755
+ stdin: "ignore",
1756
+ stdout: { maxBytes: STREAM_MAX_BYTES },
1757
+ stderr: { maxBytes: STREAM_MAX_BYTES }
1758
+ },
1759
+ graceMs: settings.previewGraceMs,
1760
+ env: {
1761
+ NO_COLOR: "1",
1762
+ FORCE_COLOR: "0",
1763
+ ...launch.env ?? {},
1764
+ ...launch.port === void 0 ? {} : { PORT: String(launch.port) }
1765
+ }
1766
+ });
1767
+ } catch (error) {
1768
+ return fail$2("spawn-failed", error instanceof Error ? error.message : String(error));
1769
+ }
1770
+ const record = {
1771
+ serverId: randomUUID(),
1772
+ name: launch.name,
1773
+ workspace: workspace.value.processPath,
1774
+ origin,
1775
+ handle,
1776
+ url: urlOf(launch),
1777
+ port: launch.port,
1778
+ startedAt: Date.now(),
1779
+ base: 0,
1780
+ buffer: "",
1781
+ read: {
1782
+ stdout: 0,
1783
+ stderr: 0
1784
+ },
1785
+ state: launch.port === void 0 ? "ready" : "starting",
1786
+ exitCode: null,
1787
+ detail: void 0,
1788
+ abort: new AbortController()
1789
+ };
1790
+ this.records.set(record.serverId, record);
1791
+ if (this.closing) {
1792
+ await this.terminate(record);
1793
+ return fail$2("closed", "the plugin is unloading");
1794
+ }
1795
+ this.watch(record);
1796
+ if (record.state === "starting") this.awaitReady(record);
1797
+ return {
1798
+ ok: true,
1799
+ server: this.viewOf(record)
1800
+ };
1801
+ }
1802
+ /**
1803
+ * Stop one server and forget it.
1804
+ * @param request - the handle.
1805
+ * @returns settlement, or a classified failure.
1806
+ */
1807
+ async stop(request) {
1808
+ const record = this.records.get(request.serverId);
1809
+ if (record === void 0) return fail$2("unknown-server", `no preview server ${request.serverId}`);
1810
+ this.records.delete(record.serverId);
1811
+ await this.terminate(record);
1812
+ return { ok: true };
1813
+ }
1814
+ /**
1815
+ * Read one server's output from a caller-owned offset, with its state at read time.
1816
+ * @param request - the handle and the offset already rendered.
1817
+ * @returns the delta and the state, or a classified failure.
1818
+ */
1819
+ logs(request) {
1820
+ const record = this.records.get(request.serverId);
1821
+ if (record === void 0) return fail$2("unknown-server", `no preview server ${request.serverId}`);
1822
+ this.drain(record);
1823
+ const total = record.base + record.buffer.length;
1824
+ const from = Number.isFinite(request.fromOffset) ? Math.max(0, Math.floor(request.fromOffset)) : 0;
1825
+ const lossy = from < record.base;
1826
+ const text = lossy ? record.buffer : record.buffer.slice(Math.min(from - record.base, record.buffer.length));
1827
+ return {
1828
+ ok: true,
1829
+ serverId: record.serverId,
1830
+ text,
1831
+ nextOffset: total,
1832
+ lossy,
1833
+ server: this.viewOf(record)
1834
+ };
1835
+ }
1836
+ /**
1837
+ * Terminate every server and refuse new ones. Called from the service's teardown effect.
1838
+ * @returns after every process tree has exited.
1839
+ */
1840
+ async disposeAll() {
1841
+ this.closing = true;
1842
+ const live = [...this.records.values()];
1843
+ this.records.clear();
1844
+ await Promise.all(live.map((record) => this.terminate(record)));
1845
+ }
1846
+ /**
1847
+ * The `previews` settings rows, normalized into the shape the merge reads.
1848
+ * @returns the configured launches.
1849
+ */
1850
+ settingsLaunches() {
1851
+ return this.source().previews.map((row) => ({
1852
+ name: row.name,
1853
+ ...row.runtimeExecutable === "" ? {} : { runtimeExecutable: row.runtimeExecutable },
1854
+ runtimeArgs: row.runtimeArgs,
1855
+ ...row.port <= 0 ? {} : { port: row.port },
1856
+ ...row.url === "" ? {} : { url: row.url },
1857
+ ...row.cwd === "" ? {} : { cwd: row.cwd }
1858
+ }));
1859
+ }
1860
+ /**
1861
+ * Read and parse the workspace's launch file.
1862
+ * @param workspace - the resolved workspace.
1863
+ * @param signal - cancellation for the read.
1864
+ * @returns the configurations, the file's path when it existed, and why it was ignored when it was.
1865
+ */
1866
+ async readLaunchFile(workspace, signal) {
1867
+ if (!this.source().previewsFromLaunchFile) return { launches: [] };
1868
+ const fs = this.ctx.get("fs");
1869
+ if (fs === void 0) return { launches: [] };
1870
+ const path = join(workspace.value.processPath, LAUNCH_FILE);
1871
+ let text;
1872
+ try {
1873
+ const target = await fs.resolve(path, signal === void 0 ? {} : { signal });
1874
+ const info = await fs.stat(target, signal);
1875
+ if (info === void 0 || info.type !== "file") return { launches: [] };
1876
+ text = await fs.readText(target, signal);
1877
+ } catch {
1878
+ return { launches: [] };
1879
+ }
1880
+ const parsed = parseLaunchFile(text);
1881
+ if ("error" in parsed) return {
1882
+ launches: [],
1883
+ path,
1884
+ error: `${LAUNCH_FILE} was ignored: ${parsed.error}`
1885
+ };
1886
+ return {
1887
+ launches: parsed.launches,
1888
+ path
1889
+ };
1890
+ }
1891
+ /**
1892
+ * Resolve the directory one configuration runs in, proving it stays inside the workspace.
1893
+ * @param workspace - the resolved workspace.
1894
+ * @param launch - the configuration.
1895
+ * @param signal - cancellation for the resolution.
1896
+ * @returns the directory, or the failure to return.
1897
+ */
1898
+ async launchDirectory(workspace, launch, signal) {
1899
+ if (launch.cwd === void 0 || launch.cwd === "") return { path: workspace.value.processPath };
1900
+ const candidate = isAbsolute(launch.cwd) ? launch.cwd : join(workspace.value.processPath, launch.cwd);
1901
+ const resolved = await resolveInside(this.ctx, workspace.value, candidate, signal);
1902
+ if (!resolved.ok) return { failure: fail$2(resolved.rejection.code === "no-filesystem" ? "no-filesystem" : "path-denied", resolved.rejection.message) };
1903
+ return { path: resolved.value.processPath };
1904
+ }
1905
+ /**
1906
+ * Watch one process for its exit.
1907
+ * @param record - the started server.
1908
+ */
1909
+ watch(record) {
1910
+ record.handle.done.then((outcome) => {
1911
+ record.abort.abort();
1912
+ record.exitCode = outcome.exitCode;
1913
+ record.state = record.state === "ready" ? "exited" : "failed";
1914
+ if (record.state === "failed" && record.detail === void 0) record.detail = outcome.signal === null ? `the process exited with code ${String(outcome.exitCode)} before its port accepted` : `the process ended on ${outcome.signal} before its port accepted`;
1915
+ }, (error) => {
1916
+ record.abort.abort();
1917
+ record.state = "failed";
1918
+ record.detail = error instanceof Error ? error.message : String(error);
1919
+ });
1920
+ }
1921
+ /**
1922
+ * Probe one server's port until it accepts or the deadline passes.
1923
+ * @param record - the started server.
1924
+ */
1925
+ async awaitReady(record) {
1926
+ const port = record.port;
1927
+ /* v8 ignore next -- only a row WITH a port enters the starting state. */
1928
+ if (port === void 0) return;
1929
+ const deadline = Date.now() + this.source().previewReadyTimeoutMs;
1930
+ while (!record.abort.signal.aborted && Date.now() < deadline) {
1931
+ if (await accepts(port, record.abort.signal)) {
1932
+ if (record.state === "starting") record.state = "ready";
1933
+ return;
1934
+ }
1935
+ await pause(PROBE_INTERVAL_MS, record.abort.signal);
1936
+ }
1937
+ if (record.state === "starting") {
1938
+ record.state = "failed";
1939
+ record.detail = `port ${String(port)} did not accept a connection within previewReadyTimeoutMs`;
1940
+ }
1941
+ }
1942
+ /**
1943
+ * Move whatever the collected streams hold into the retained buffer.
1944
+ *
1945
+ * Both streams share one buffer and one offset, because the panel shows one log: a dev server
1946
+ * prints its banner on one and its errors on the other, and two independently scrolling views of
1947
+ * the same startup would be harder to read, not easier.
1948
+ * @param record - the server to drain.
1949
+ */
1950
+ drain(record) {
1951
+ const take = (reader, from) => {
1952
+ /* v8 ignore next -- both streams are spawned in collect mode, so both readers exist. */
1953
+ if (reader === void 0) return {
1954
+ text: "",
1955
+ next: from
1956
+ };
1957
+ const read = reader.readFrom(from);
1958
+ return {
1959
+ text: read.lossy ? "" : read.text,
1960
+ next: read.nextOffset
1961
+ };
1962
+ };
1963
+ const out = take(record.handle.collected.stdout, record.read.stdout);
1964
+ const err = take(record.handle.collected.stderr, record.read.stderr);
1965
+ record.read = {
1966
+ stdout: out.next,
1967
+ stderr: err.next
1968
+ };
1969
+ const added = out.text + err.text;
1970
+ if (added === "") return;
1971
+ record.buffer += added;
1972
+ const max = Math.max(this.source().previewScrollback, 1024);
1973
+ if (record.buffer.length > max) {
1974
+ const drop = record.buffer.length - max;
1975
+ record.base += drop;
1976
+ record.buffer = record.buffer.slice(drop);
1977
+ }
1978
+ }
1979
+ /**
1980
+ * Find a live record for one workspace and configuration name.
1981
+ * @param workspace - canonical workspace path.
1982
+ * @param name - configuration name.
1983
+ * @returns the record, when one exists.
1984
+ */
1985
+ find(workspace, name) {
1986
+ for (const record of this.records.values()) if (record.workspace === workspace && record.name === name) return record;
1987
+ }
1988
+ /**
1989
+ * Stop one process tree without letting a cleanup fault escape.
1990
+ * @param record - the server to stop.
1991
+ * @returns after the tree has exited.
1992
+ */
1993
+ async terminate(record) {
1994
+ record.abort.abort();
1995
+ this.records.delete(record.serverId);
1996
+ try {
1997
+ record.handle.terminate();
1998
+ await record.handle.waitForExit();
1999
+ } catch {}
2000
+ }
2001
+ /**
2002
+ * Project one configuration into its view, attaching a live record when one exists.
2003
+ * @param resolved - the configuration and its origin.
2004
+ * @param workspace - canonical workspace path.
2005
+ * @returns the view.
2006
+ */
2007
+ view(resolved, workspace) {
2008
+ const record = this.find(workspace, resolved.launch.name);
2009
+ if (record !== void 0) return this.viewOf(record);
2010
+ const startable = resolved.launch.runtimeExecutable !== void 0 && resolved.launch.runtimeExecutable !== "";
2011
+ const url = urlOf(resolved.launch);
2012
+ return {
2013
+ name: resolved.launch.name,
2014
+ origin: resolved.origin,
2015
+ startable,
2016
+ state: startable ? "stopped" : "ready",
2017
+ ...url === void 0 ? {} : { url },
2018
+ ...resolved.launch.port === void 0 ? {} : { port: resolved.launch.port },
2019
+ ...startable ? {} : { detail: "this configuration opens its url and starts nothing" }
2020
+ };
2021
+ }
2022
+ /**
2023
+ * Project one live record into its view.
2024
+ * @param record - the server.
2025
+ * @returns the view.
2026
+ */
2027
+ viewOf(record) {
2028
+ return {
2029
+ serverId: record.serverId,
2030
+ name: record.name,
2031
+ origin: record.origin,
2032
+ startable: true,
2033
+ state: record.state,
2034
+ ...record.url === void 0 ? {} : { url: record.url },
2035
+ ...record.port === void 0 ? {} : { port: record.port },
2036
+ ...record.handle.pid < 0 ? {} : { pid: record.handle.pid },
2037
+ ...record.state === "exited" || record.state === "failed" ? { exitCode: record.exitCode } : {},
2038
+ ...record.detail === void 0 ? {} : { detail: record.detail },
2039
+ startedAt: record.startedAt
2040
+ };
2041
+ }
2042
+ };
2043
+
2044
+ //#endregion
2045
+ //#region tsbuild/host/terminals.js
2046
+ /** Compose one classified failure. */
2047
+ function fail$1(code, message) {
2048
+ return {
2049
+ ok: false,
2050
+ code,
2051
+ message
2052
+ };
2053
+ }
2054
+ /** Terminal geometry the substrate will accept, whatever the panel measured. */
2055
+ function clampGeometry(cols, rows) {
2056
+ const bound = (value, low, high) => Number.isFinite(value) ? Math.min(Math.max(Math.round(value), low), high) : low;
2057
+ return {
2058
+ cols: bound(cols, 20, 500),
2059
+ rows: bound(rows, 5, 200)
2060
+ };
2061
+ }
2062
+ /**
2063
+ * Owns every panel terminal in the process. One instance is created by the service and disposed
2064
+ * with it, which is what guarantees no shell outlives the plugin.
2065
+ */
2066
+ var PanelTerminals = class {
2067
+ ctx;
2068
+ source;
2069
+ records = /* @__PURE__ */ new Map();
2070
+ closing = false;
2071
+ /**
2072
+ * @param ctx - Host context carrying the subprocess and filesystem capabilities.
2073
+ * @param source - reads the current settings section; called per request.
2074
+ */
2075
+ constructor(ctx, source) {
2076
+ this.ctx = ctx;
2077
+ this.source = source;
2078
+ }
2079
+ /**
2080
+ * Report whether a panel terminal can be allocated on this Host.
2081
+ * @returns availability plus the shell that would answer.
2082
+ */
2083
+ describe() {
2084
+ if (this.ctx.get("subprocess") === void 0) return {
2085
+ available: false,
2086
+ reason: "no subprocess capability is mounted: this deployment composes no @deepseek-ai/dsh-subprocess provider"
2087
+ };
2088
+ if (this.ctx.get("fs") === void 0) return {
2089
+ available: false,
2090
+ reason: "no filesystem capability is mounted: the terminal has no way to resolve its working directory"
2091
+ };
2092
+ return {
2093
+ available: true,
2094
+ detail: this.shell()
2095
+ };
2096
+ }
2097
+ /**
2098
+ * Allocate one terminal in a workspace.
2099
+ * @param request - the directory and the panel's measured geometry.
2100
+ * @param signal - cancellation of the allocation; a published terminal owns its later lifetime.
2101
+ * @returns the handle, or a classified failure.
2102
+ */
2103
+ async open(request, signal) {
2104
+ if (this.closing) return fail$1("closed", "the plugin is unloading");
2105
+ const subprocess = this.ctx.get("subprocess");
2106
+ if (subprocess === void 0) return fail$1("no-subprocess", "no subprocess capability is mounted");
2107
+ const settings = this.source();
2108
+ if ([...this.records.values()].filter((record$1) => record$1.running).length >= settings.maxTerminals) return fail$1("limit-reached", `${String(settings.maxTerminals)} panel terminals are already open`);
2109
+ const workspace = await resolveWorkspace(this.ctx, request.workspacePath, signal);
2110
+ if (!workspace.ok) return fail$1(workspace.rejection.code === "no-filesystem" ? "no-filesystem" : "path-denied", workspace.rejection.message);
2111
+ const shell = this.shell();
2112
+ const { cols, rows } = clampGeometry(request.cols, request.rows);
2113
+ let handle;
2114
+ try {
2115
+ handle = await subprocess.spawnTerminal({
2116
+ argv: [shell],
2117
+ cwd: workspace.value.processPath,
2118
+ rows,
2119
+ cols,
2120
+ graceMs: settings.terminalGraceMs,
2121
+ signal,
2122
+ env: {
2123
+ TERM: "xterm-256color",
2124
+ COLUMNS: String(cols),
2125
+ LINES: String(rows)
2126
+ }
2127
+ });
2128
+ } catch (error) {
2129
+ return fail$1("spawn-failed", error instanceof Error ? error.message : String(error));
2130
+ }
2131
+ const record = {
2132
+ id: randomUUID(),
2133
+ handle,
2134
+ shell,
2135
+ cwd: workspace.value.processPath,
2136
+ base: 0,
2137
+ buffer: "",
2138
+ running: true,
2139
+ exitCode: null,
2140
+ signal: null
2141
+ };
2142
+ this.records.set(record.id, record);
2143
+ this.collect(record);
2144
+ return {
2145
+ ok: true,
2146
+ terminalId: record.id,
2147
+ shell,
2148
+ cwd: record.cwd,
2149
+ pid: handle.pid
2150
+ };
2151
+ }
2152
+ /**
2153
+ * Read output from a caller-owned offset.
2154
+ * @param request - the handle and the offset already rendered.
2155
+ * @returns the delta and the process state, or a classified failure.
2156
+ */
2157
+ read(request) {
2158
+ const record = this.records.get(request.terminalId);
2159
+ if (record === void 0) return fail$1("unknown-terminal", `no panel terminal ${request.terminalId}`);
2160
+ const total = record.base + record.buffer.length;
2161
+ const from = Number.isFinite(request.fromOffset) ? Math.max(0, Math.floor(request.fromOffset)) : 0;
2162
+ const lossy = from < record.base;
2163
+ const text = lossy ? record.buffer : record.buffer.slice(Math.min(from - record.base, record.buffer.length));
2164
+ return {
2165
+ ok: true,
2166
+ terminalId: record.id,
2167
+ text,
2168
+ nextOffset: total,
2169
+ lossy,
2170
+ running: record.running,
2171
+ ...record.running ? {} : {
2172
+ exitCode: record.exitCode,
2173
+ signal: record.signal
2174
+ }
2175
+ };
2176
+ }
2177
+ /**
2178
+ * Send keystrokes.
2179
+ * @param request - the handle and the text to deliver verbatim.
2180
+ * @returns settlement, or a classified failure.
2181
+ */
2182
+ async write(request) {
2183
+ const record = this.records.get(request.terminalId);
2184
+ if (record === void 0) return fail$1("unknown-terminal", `no panel terminal ${request.terminalId}`);
2185
+ if (!record.running) return fail$1("unknown-terminal", `panel terminal ${record.id} has exited`);
2186
+ try {
2187
+ await record.handle.write(request.data);
2188
+ } catch (error) {
2189
+ return fail$1("spawn-failed", error instanceof Error ? error.message : String(error));
2190
+ }
2191
+ return { ok: true };
2192
+ }
2193
+ /**
2194
+ * Deliver a signal to the terminal's foreground process group.
2195
+ * @param request - the handle and the signal.
2196
+ * @returns settlement, or a classified failure.
2197
+ */
2198
+ async signal(request) {
2199
+ const record = this.records.get(request.terminalId);
2200
+ if (record === void 0) return fail$1("unknown-terminal", `no panel terminal ${request.terminalId}`);
2201
+ if (!record.running) return fail$1("unknown-terminal", `panel terminal ${record.id} has exited`);
2202
+ try {
2203
+ await record.handle.signalForeground(request.signal);
2204
+ } catch {}
2205
+ return { ok: true };
2206
+ }
2207
+ /**
2208
+ * Close one terminal and forget it.
2209
+ * @param request - the handle.
2210
+ * @returns settlement, or a classified failure.
2211
+ */
2212
+ async close(request) {
2213
+ const record = this.records.get(request.terminalId);
2214
+ if (record === void 0) return fail$1("unknown-terminal", `no panel terminal ${request.terminalId}`);
2215
+ this.records.delete(record.id);
2216
+ await terminateQuietly(record.handle);
2217
+ return { ok: true };
2218
+ }
2219
+ /**
2220
+ * Terminate every terminal and refuse new ones. Called from the service's teardown effect.
2221
+ * @returns after every terminal session has settled.
2222
+ */
2223
+ async disposeAll() {
2224
+ this.closing = true;
2225
+ const live = [...this.records.values()];
2226
+ this.records.clear();
2227
+ await Promise.all(live.map((record) => terminateQuietly(record.handle)));
2228
+ }
2229
+ /**
2230
+ * The shell a new terminal starts: the configured one, then `$SHELL`, then the platform default.
2231
+ * @returns an executable name or path.
2232
+ */
2233
+ shell() {
2234
+ const configured = this.source().terminalShell.trim();
2235
+ if (configured !== "") return configured;
2236
+ if (process.platform === "win32") return process.env.COMSPEC ?? "powershell.exe";
2237
+ const login = process.env.SHELL;
2238
+ return login !== void 0 && login !== "" ? login : "/bin/sh";
2239
+ }
2240
+ /**
2241
+ * Drain one terminal's output into its retained scrollback and record its settlement.
2242
+ *
2243
+ * Attached at allocation rather than at first read: a shell prints its prompt immediately, and a
2244
+ * stream nobody is reading would otherwise apply backpressure until the panel polled.
2245
+ * @param record - the freshly registered terminal.
2246
+ */
2247
+ collect(record) {
2248
+ const decoder = new TextDecoder("utf-8", { fatal: false });
2249
+ record.handle.output.on("data", (chunk) => {
2250
+ const text = typeof chunk === "string" ? chunk : decoder.decode(chunk, { stream: true });
2251
+ if (text === "") return;
2252
+ record.buffer += text;
2253
+ const max = Math.max(this.source().terminalScrollback, 1024);
2254
+ if (record.buffer.length > max) {
2255
+ const drop = record.buffer.length - max;
2256
+ record.base += drop;
2257
+ record.buffer = record.buffer.slice(drop);
2258
+ }
2259
+ });
2260
+ record.handle.done.then((outcome) => {
2261
+ record.running = false;
2262
+ record.exitCode = outcome.exitCode;
2263
+ record.signal = outcome.signal;
2264
+ }, (error) => {
2265
+ record.running = false;
2266
+ record.buffer += `\r\n[terminal transport failed: ${error instanceof Error ? error.message : String(error)}]\r\n`;
2267
+ });
2268
+ }
2269
+ };
2270
+ /**
2271
+ * Terminate one terminal session without letting a cleanup fault escape.
2272
+ * @param handle - the terminal to close.
2273
+ * @returns after the session tree has settled.
2274
+ */
2275
+ async function terminateQuietly(handle) {
2276
+ try {
2277
+ await handle.terminate();
2278
+ } catch {}
2279
+ }
2280
+
2281
+ //#endregion
2282
+ //#region tsbuild/host/tasks.js
2283
+ /** Compose one classified failure. */
2284
+ function fail(code, message) {
2285
+ return {
2286
+ ok: false,
2287
+ code,
2288
+ message
2289
+ };
2290
+ }
2291
+ /** Whether the registry still holds the record open. */
2292
+ function isLive(snapshot) {
2293
+ return snapshot.status === "running" || snapshot.status === "stopping";
2294
+ }
2295
+ /**
2296
+ * Stops and reads background tasks on behalf of the Tasks panel. One instance serves every request
2297
+ * and retains the output it has already drained.
2298
+ */
2299
+ var TaskController = class {
2300
+ ctx;
2301
+ source;
2302
+ /** Accumulated output per job id, so a second panel open is not an empty read. */
2303
+ collected = /* @__PURE__ */ new Map();
2304
+ /**
2305
+ * Tasks whose output has already been taken.
2306
+ *
2307
+ * `read()` is CONSUMING for a stream job and IDEMPOTENT for a final-output one, and the snapshot
2308
+ * does not say which kind a job is. Appending every read would therefore duplicate a
2309
+ * final-output job's text once per poll, so each task is drained exactly once — after settlement,
2310
+ * when there is nothing further to come.
2311
+ */
2312
+ drained = /* @__PURE__ */ new Set();
2313
+ /**
2314
+ * @param ctx - Host context carrying the optional job registry and the agent registry.
2315
+ * @param source - reads the current settings section; called per request.
2316
+ */
2317
+ constructor(ctx, source) {
2318
+ this.ctx = ctx;
2319
+ this.source = source;
2320
+ }
2321
+ /**
2322
+ * Report what the Tasks panel may do on this Host.
2323
+ * @returns availability plus the two per-verb permissions.
2324
+ */
2325
+ describe() {
2326
+ const settings = this.source();
2327
+ if (this.ctx.get("jobs") === void 0) return {
2328
+ available: false,
2329
+ reason: "no job registry is mounted: this deployment composes no @deepseek-ai/dsh-jobs provider",
2330
+ canKill: false,
2331
+ canReadOutput: false
2332
+ };
2333
+ return {
2334
+ available: true,
2335
+ canKill: settings.allowTaskKill,
2336
+ canReadOutput: settings.showTaskOutput
2337
+ };
2338
+ }
2339
+ /**
2340
+ * Stop one live task.
2341
+ *
2342
+ * The registry marks a killed record reported, which suppresses the completion notice its
2343
+ * producer would otherwise open a model turn to deliver. That is the correct trade for a person
2344
+ * pressing Stop — the work is being cancelled on their authority, not the model's — and it is why
2345
+ * this verb is gated by `allowTaskKill` rather than always on.
2346
+ * @param request - the owning session and the task id.
2347
+ * @returns what the registry did, or a classified failure.
2348
+ */
2349
+ async kill(request) {
2350
+ if (!this.source().allowTaskKill) return fail("disabled", "stopping a background task is switched off in the advanced-sidebar settings");
2351
+ const bound = this.bind(request.sessionId, request.taskId);
2352
+ if ("failure" in bound) return bound.failure;
2353
+ try {
2354
+ const outcome = bound.jobs.kill(bound.id, bound.agent, "stopped from the DeepSeek Harness sidebar");
2355
+ await this.absorb(request.sessionId, request.taskId);
2356
+ return {
2357
+ ok: true,
2358
+ outcome
2359
+ };
2360
+ } catch (error) {
2361
+ return fail("registry-refused", error instanceof Error ? error.message : String(error));
2362
+ }
2363
+ }
2364
+ /**
2365
+ * Read one task's output, or say why it is withheld.
2366
+ * @param request - the owning session and the task id.
2367
+ * @returns the accumulated output, or a classified failure.
2368
+ */
2369
+ async output(request) {
2370
+ if (!this.source().showTaskOutput) return fail("disabled", "task output is switched off in the advanced-sidebar settings");
2371
+ const bound = this.bind(request.sessionId, request.taskId);
2372
+ if ("failure" in bound) return bound.failure;
2373
+ let snapshot;
2374
+ try {
2375
+ snapshot = bound.jobs.get(bound.id, bound.agent);
2376
+ } catch (error) {
2377
+ return fail("unknown-task", error instanceof Error ? error.message : String(error));
2378
+ }
2379
+ const retained = this.collected.get(request.taskId) ?? "";
2380
+ if (isLive(snapshot) || !snapshot.reported) return {
2381
+ ok: true,
2382
+ taskId: request.taskId,
2383
+ readable: retained !== "",
2384
+ text: retained,
2385
+ reason: isLive(snapshot) ? "the task is still running; its output stream belongs to the model until the task settles" : "the task has settled but its completion has not been reported to the model yet"
2386
+ };
2387
+ await this.absorb(request.sessionId, request.taskId);
2388
+ return {
2389
+ ok: true,
2390
+ taskId: request.taskId,
2391
+ readable: true,
2392
+ text: this.collected.get(request.taskId) ?? ""
2393
+ };
2394
+ }
2395
+ /** Drop retained output. Called from the service's teardown effect. */
2396
+ dispose() {
2397
+ this.collected.clear();
2398
+ this.drained.clear();
2399
+ }
2400
+ /**
2401
+ * Drain whatever the registry will still hand over and append it to the retained text.
2402
+ * @param sessionId - the owning session.
2403
+ * @param taskId - the task id.
2404
+ */
2405
+ async absorb(sessionId, taskId) {
2406
+ if (this.drained.has(taskId)) return;
2407
+ const bound = this.bind(sessionId, taskId);
2408
+ if ("failure" in bound) return;
2409
+ this.drained.add(taskId);
2410
+ try {
2411
+ const read = bound.jobs.read(bound.id, bound.agent);
2412
+ if (read.text !== "") this.collected.set(taskId, (this.collected.get(taskId) ?? "") + read.text);
2413
+ } catch {}
2414
+ await Promise.resolve();
2415
+ }
2416
+ /**
2417
+ * Resolve the registry, the owning agent, and the branded job id together.
2418
+ * @param sessionId - the owning session, as the browser spelled it.
2419
+ * @param taskId - the task id, as the browser spelled it.
2420
+ * @returns the bound handles, or the failure to return.
2421
+ */
2422
+ bind(sessionId, taskId) {
2423
+ const jobs = this.ctx.get("jobs");
2424
+ if (jobs === void 0) return { failure: fail("no-registry", "no job registry is mounted") };
2425
+ const agent = this.ctx.agents.get(sessionId);
2426
+ if (agent === void 0) return { failure: fail("unknown-session", `no live agent answers for session ${sessionId}`) };
2427
+ return {
2428
+ jobs,
2429
+ agent,
2430
+ id: JobId(taskId)
2431
+ };
2432
+ }
2433
+ };
2434
+
2435
+ //#endregion
2436
+ //#region tsbuild/host/index.js
2437
+ /**
2438
+ * The advanced-sidebar plugin's node half: one Remote namespace serving every operation the browser
2439
+ * structurally cannot perform, plus the `advanced-sidebar` settings section both halves address.
2440
+ *
2441
+ * What is here and what is not follows one rule — the Host owns only what a browser cannot do. The
2442
+ * session list, the workspace list, the background-task list, archiving, and directory listing all
2443
+ * already reach the Web Client through capabilities it holds, so this endpoint adds no second copy
2444
+ * of any of them. It answers for git (a subprocess), panel terminals (a pseudo-terminal), file
2445
+ * previews (a filesystem read), external applications (a launch), stopping a background task
2446
+ * (an owner-fenced registry), and deletion (a durable artifact).
2447
+ *
2448
+ * Nothing here is model-facing: no tool, no prompt section, no session event. Every result is a
2449
+ * discriminated value rather than a throw, because the RPC gateway erases a business exception's
2450
+ * classification and each panel's next move depends on which class it was.
2451
+ * @module @achasoft/dsh-advanced-sidebar/host
2452
+ */
2453
+ var __runInitializers = void 0 && (void 0).__runInitializers || function(thisArg, initializers, value) {
2454
+ var useValue = arguments.length > 2;
2455
+ for (var i = 0; i < initializers.length; i++) value = useValue ? initializers[i].call(thisArg, value) : initializers[i].call(thisArg);
2456
+ return useValue ? value : void 0;
2457
+ };
2458
+ var __esDecorate = void 0 && (void 0).__esDecorate || function(ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) {
2459
+ function accept(f) {
2460
+ if (f !== void 0 && typeof f !== "function") throw new TypeError("Function expected");
2461
+ return f;
2462
+ }
2463
+ var kind = contextIn.kind, key = kind === "getter" ? "get" : kind === "setter" ? "set" : "value";
2464
+ var target = !descriptorIn && ctor ? contextIn["static"] ? ctor : ctor.prototype : null;
2465
+ var descriptor = descriptorIn || (target ? Object.getOwnPropertyDescriptor(target, contextIn.name) : {});
2466
+ var _, done = false;
2467
+ for (var i = decorators.length - 1; i >= 0; i--) {
2468
+ var context = {};
2469
+ for (var p in contextIn) context[p] = p === "access" ? {} : contextIn[p];
2470
+ for (var p in contextIn.access) context.access[p] = contextIn.access[p];
2471
+ context.addInitializer = function(f) {
2472
+ if (done) throw new TypeError("Cannot add initializers after decoration has completed");
2473
+ extraInitializers.push(accept(f || null));
2474
+ };
2475
+ var result = (0, decorators[i])(kind === "accessor" ? {
2476
+ get: descriptor.get,
2477
+ set: descriptor.set
2478
+ } : descriptor[key], context);
2479
+ if (kind === "accessor") {
2480
+ if (result === void 0) continue;
2481
+ if (result === null || typeof result !== "object") throw new TypeError("Object expected");
2482
+ if (_ = accept(result.get)) descriptor.get = _;
2483
+ if (_ = accept(result.set)) descriptor.set = _;
2484
+ if (_ = accept(result.init)) initializers.unshift(_);
2485
+ } else if (_ = accept(result)) if (kind === "field") initializers.unshift(_);
2486
+ else descriptor[key] = _;
2487
+ }
2488
+ if (target) Object.defineProperty(target, contextIn.name, descriptor);
2489
+ done = true;
2490
+ };
2491
+ /**
2492
+ * The settings namespace both halves address; the browser card joins the plugin tab on it.
2493
+ *
2494
+ * Kebab-case, unlike the Remote namespace below: a settings namespace is a kebab-case grammar the
2495
+ * Host validates, while a Remote namespace is read as `ctx.remote.advancedSidebar.…` and so must be
2496
+ * an identifier.
2497
+ */
2498
+ const ADVANCED_SIDEBAR_SETTINGS_NAMESPACE = settingsNamespace("advanced-sidebar");
2499
+ /** An Open in target id must be usable in a menu and on the wire. */
2500
+ const TARGET_ID_PATTERN = /^[a-z][a-z0-9-]*$/u;
2501
+ /**
2502
+ * Reject a section this service could not act on, for the constraints the schema cannot express.
2503
+ *
2504
+ * Called from the constructor as well as from the settings hook: the settings seam is optional, so
2505
+ * a composition without it never runs the hook — and these constraints come straight off the
2506
+ * composition file, where being wrong is a load-time mistake rather than a running deployment.
2507
+ * @param value - the resolved section, schema-valid by construction.
2508
+ */
2509
+ function validateConfig(value) {
2510
+ const seen = new Set(["reveal"]);
2511
+ for (const editor of value.editors) {
2512
+ if (!TARGET_ID_PATTERN.test(editor.id)) throw new TypeError(`advanced-sidebar: editor id "${editor.id}" must match ${String(TARGET_ID_PATTERN)}`);
2513
+ if (seen.has(editor.id)) throw new TypeError(`advanced-sidebar: editor id "${editor.id}" is used twice (or collides with the built-in file-manager target)`);
2514
+ seen.add(editor.id);
2515
+ if (editor.command.trim() === "") throw new TypeError(`advanced-sidebar: editor "${editor.id}" has an empty command`);
2516
+ }
2517
+ const names = /* @__PURE__ */ new Set();
2518
+ for (const preview of value.previews) {
2519
+ if (preview.name.trim() === "") throw new TypeError("advanced-sidebar: a preview row has an empty name");
2520
+ if (names.has(preview.name)) throw new TypeError(`advanced-sidebar: preview name "${preview.name}" is used twice`);
2521
+ names.add(preview.name);
2522
+ if (preview.runtimeExecutable === "" && preview.url === "" && preview.port <= 0) throw new TypeError(`advanced-sidebar: preview "${preview.name}" names no command, no url, and no port, so there is nothing to start and nowhere to point the frame`);
2523
+ }
2524
+ if (value.deleteMode === "purge" && !value.confirmDelete) throw new TypeError("advanced-sidebar: deleteMode \"purge\" removes a session log irreversibly, so confirmDelete cannot be false");
2525
+ }
2526
+ /** Schemastery shape of one preview launch row. */
2527
+ const PreviewSchema = z.object({
2528
+ name: z.string().required(),
2529
+ runtimeExecutable: z.string().required(),
2530
+ runtimeArgs: z.array(z.string()).required(),
2531
+ port: z.number().step(1).min(0).max(65535).required(),
2532
+ url: z.string().required(),
2533
+ cwd: z.string().required()
2534
+ });
2535
+ /** Schemastery shape of one Open in target row. */
2536
+ const EditorSchema = z.object({
2537
+ id: z.string().required(),
2538
+ label: z.string().required(),
2539
+ command: z.string().required(),
2540
+ args: z.array(z.string()).required()
2541
+ });
2542
+ /** Host endpoint for the sidebar's advanced operations, and owner of the settings section. */
2543
+ let AdvancedSidebarService = (() => {
2544
+ let _classSuper = TypertRemoteService;
2545
+ let _instanceExtraInitializers = [];
2546
+ let _describe_decorators;
2547
+ let _gitStatus_decorators;
2548
+ let _gitDiff_decorators;
2549
+ let _gitStage_decorators;
2550
+ let _gitUnstage_decorators;
2551
+ let _gitCommit_decorators;
2552
+ let _gitPush_decorators;
2553
+ let _gitCommitMessage_decorators;
2554
+ let _terminalOpen_decorators;
2555
+ let _terminalRead_decorators;
2556
+ let _terminalWrite_decorators;
2557
+ let _terminalSignal_decorators;
2558
+ let _terminalClose_decorators;
2559
+ let _listEntries_decorators;
2560
+ let _previewList_decorators;
2561
+ let _previewStart_decorators;
2562
+ let _previewStop_decorators;
2563
+ let _previewLogs_decorators;
2564
+ let _readFile_decorators;
2565
+ let _openIn_decorators;
2566
+ let _taskKill_decorators;
2567
+ let _taskOutput_decorators;
2568
+ let _deleteSession_decorators;
2569
+ return class AdvancedSidebarService$1 extends _classSuper {
2570
+ static {
2571
+ const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(_classSuper[Symbol.metadata] ?? null) : void 0;
2572
+ _describe_decorators = [Remote("describe")];
2573
+ _gitStatus_decorators = [Remote("gitStatus")];
2574
+ _gitDiff_decorators = [Remote("gitDiff")];
2575
+ _gitStage_decorators = [Remote("gitStage")];
2576
+ _gitUnstage_decorators = [Remote("gitUnstage")];
2577
+ _gitCommit_decorators = [Remote("gitCommit")];
2578
+ _gitPush_decorators = [Remote("gitPush")];
2579
+ _gitCommitMessage_decorators = [Remote("gitCommitMessage")];
2580
+ _terminalOpen_decorators = [Remote("terminalOpen")];
2581
+ _terminalRead_decorators = [Remote("terminalRead")];
2582
+ _terminalWrite_decorators = [Remote("terminalWrite")];
2583
+ _terminalSignal_decorators = [Remote("terminalSignal")];
2584
+ _terminalClose_decorators = [Remote("terminalClose")];
2585
+ _listEntries_decorators = [Remote("listEntries")];
2586
+ _previewList_decorators = [Remote("previewList")];
2587
+ _previewStart_decorators = [Remote("previewStart")];
2588
+ _previewStop_decorators = [Remote("previewStop")];
2589
+ _previewLogs_decorators = [Remote("previewLogs")];
2590
+ _readFile_decorators = [Remote("readFile")];
2591
+ _openIn_decorators = [Remote("openIn")];
2592
+ _taskKill_decorators = [Remote("taskKill")];
2593
+ _taskOutput_decorators = [Remote("taskOutput")];
2594
+ _deleteSession_decorators = [Remote("deleteSession")];
2595
+ __esDecorate(this, null, _describe_decorators, {
2596
+ kind: "method",
2597
+ name: "describe",
2598
+ static: false,
2599
+ private: false,
2600
+ access: {
2601
+ has: (obj) => "describe" in obj,
2602
+ get: (obj) => obj.describe
2603
+ },
2604
+ metadata: _metadata
2605
+ }, null, _instanceExtraInitializers);
2606
+ __esDecorate(this, null, _gitStatus_decorators, {
2607
+ kind: "method",
2608
+ name: "gitStatus",
2609
+ static: false,
2610
+ private: false,
2611
+ access: {
2612
+ has: (obj) => "gitStatus" in obj,
2613
+ get: (obj) => obj.gitStatus
2614
+ },
2615
+ metadata: _metadata
2616
+ }, null, _instanceExtraInitializers);
2617
+ __esDecorate(this, null, _gitDiff_decorators, {
2618
+ kind: "method",
2619
+ name: "gitDiff",
2620
+ static: false,
2621
+ private: false,
2622
+ access: {
2623
+ has: (obj) => "gitDiff" in obj,
2624
+ get: (obj) => obj.gitDiff
2625
+ },
2626
+ metadata: _metadata
2627
+ }, null, _instanceExtraInitializers);
2628
+ __esDecorate(this, null, _gitStage_decorators, {
2629
+ kind: "method",
2630
+ name: "gitStage",
2631
+ static: false,
2632
+ private: false,
2633
+ access: {
2634
+ has: (obj) => "gitStage" in obj,
2635
+ get: (obj) => obj.gitStage
2636
+ },
2637
+ metadata: _metadata
2638
+ }, null, _instanceExtraInitializers);
2639
+ __esDecorate(this, null, _gitUnstage_decorators, {
2640
+ kind: "method",
2641
+ name: "gitUnstage",
2642
+ static: false,
2643
+ private: false,
2644
+ access: {
2645
+ has: (obj) => "gitUnstage" in obj,
2646
+ get: (obj) => obj.gitUnstage
2647
+ },
2648
+ metadata: _metadata
2649
+ }, null, _instanceExtraInitializers);
2650
+ __esDecorate(this, null, _gitCommit_decorators, {
2651
+ kind: "method",
2652
+ name: "gitCommit",
2653
+ static: false,
2654
+ private: false,
2655
+ access: {
2656
+ has: (obj) => "gitCommit" in obj,
2657
+ get: (obj) => obj.gitCommit
2658
+ },
2659
+ metadata: _metadata
2660
+ }, null, _instanceExtraInitializers);
2661
+ __esDecorate(this, null, _gitPush_decorators, {
2662
+ kind: "method",
2663
+ name: "gitPush",
2664
+ static: false,
2665
+ private: false,
2666
+ access: {
2667
+ has: (obj) => "gitPush" in obj,
2668
+ get: (obj) => obj.gitPush
2669
+ },
2670
+ metadata: _metadata
2671
+ }, null, _instanceExtraInitializers);
2672
+ __esDecorate(this, null, _gitCommitMessage_decorators, {
2673
+ kind: "method",
2674
+ name: "gitCommitMessage",
2675
+ static: false,
2676
+ private: false,
2677
+ access: {
2678
+ has: (obj) => "gitCommitMessage" in obj,
2679
+ get: (obj) => obj.gitCommitMessage
2680
+ },
2681
+ metadata: _metadata
2682
+ }, null, _instanceExtraInitializers);
2683
+ __esDecorate(this, null, _terminalOpen_decorators, {
2684
+ kind: "method",
2685
+ name: "terminalOpen",
2686
+ static: false,
2687
+ private: false,
2688
+ access: {
2689
+ has: (obj) => "terminalOpen" in obj,
2690
+ get: (obj) => obj.terminalOpen
2691
+ },
2692
+ metadata: _metadata
2693
+ }, null, _instanceExtraInitializers);
2694
+ __esDecorate(this, null, _terminalRead_decorators, {
2695
+ kind: "method",
2696
+ name: "terminalRead",
2697
+ static: false,
2698
+ private: false,
2699
+ access: {
2700
+ has: (obj) => "terminalRead" in obj,
2701
+ get: (obj) => obj.terminalRead
2702
+ },
2703
+ metadata: _metadata
2704
+ }, null, _instanceExtraInitializers);
2705
+ __esDecorate(this, null, _terminalWrite_decorators, {
2706
+ kind: "method",
2707
+ name: "terminalWrite",
2708
+ static: false,
2709
+ private: false,
2710
+ access: {
2711
+ has: (obj) => "terminalWrite" in obj,
2712
+ get: (obj) => obj.terminalWrite
2713
+ },
2714
+ metadata: _metadata
2715
+ }, null, _instanceExtraInitializers);
2716
+ __esDecorate(this, null, _terminalSignal_decorators, {
2717
+ kind: "method",
2718
+ name: "terminalSignal",
2719
+ static: false,
2720
+ private: false,
2721
+ access: {
2722
+ has: (obj) => "terminalSignal" in obj,
2723
+ get: (obj) => obj.terminalSignal
2724
+ },
2725
+ metadata: _metadata
2726
+ }, null, _instanceExtraInitializers);
2727
+ __esDecorate(this, null, _terminalClose_decorators, {
2728
+ kind: "method",
2729
+ name: "terminalClose",
2730
+ static: false,
2731
+ private: false,
2732
+ access: {
2733
+ has: (obj) => "terminalClose" in obj,
2734
+ get: (obj) => obj.terminalClose
2735
+ },
2736
+ metadata: _metadata
2737
+ }, null, _instanceExtraInitializers);
2738
+ __esDecorate(this, null, _listEntries_decorators, {
2739
+ kind: "method",
2740
+ name: "listEntries",
2741
+ static: false,
2742
+ private: false,
2743
+ access: {
2744
+ has: (obj) => "listEntries" in obj,
2745
+ get: (obj) => obj.listEntries
2746
+ },
2747
+ metadata: _metadata
2748
+ }, null, _instanceExtraInitializers);
2749
+ __esDecorate(this, null, _previewList_decorators, {
2750
+ kind: "method",
2751
+ name: "previewList",
2752
+ static: false,
2753
+ private: false,
2754
+ access: {
2755
+ has: (obj) => "previewList" in obj,
2756
+ get: (obj) => obj.previewList
2757
+ },
2758
+ metadata: _metadata
2759
+ }, null, _instanceExtraInitializers);
2760
+ __esDecorate(this, null, _previewStart_decorators, {
2761
+ kind: "method",
2762
+ name: "previewStart",
2763
+ static: false,
2764
+ private: false,
2765
+ access: {
2766
+ has: (obj) => "previewStart" in obj,
2767
+ get: (obj) => obj.previewStart
2768
+ },
2769
+ metadata: _metadata
2770
+ }, null, _instanceExtraInitializers);
2771
+ __esDecorate(this, null, _previewStop_decorators, {
2772
+ kind: "method",
2773
+ name: "previewStop",
2774
+ static: false,
2775
+ private: false,
2776
+ access: {
2777
+ has: (obj) => "previewStop" in obj,
2778
+ get: (obj) => obj.previewStop
2779
+ },
2780
+ metadata: _metadata
2781
+ }, null, _instanceExtraInitializers);
2782
+ __esDecorate(this, null, _previewLogs_decorators, {
2783
+ kind: "method",
2784
+ name: "previewLogs",
2785
+ static: false,
2786
+ private: false,
2787
+ access: {
2788
+ has: (obj) => "previewLogs" in obj,
2789
+ get: (obj) => obj.previewLogs
2790
+ },
2791
+ metadata: _metadata
2792
+ }, null, _instanceExtraInitializers);
2793
+ __esDecorate(this, null, _readFile_decorators, {
2794
+ kind: "method",
2795
+ name: "readFile",
2796
+ static: false,
2797
+ private: false,
2798
+ access: {
2799
+ has: (obj) => "readFile" in obj,
2800
+ get: (obj) => obj.readFile
2801
+ },
2802
+ metadata: _metadata
2803
+ }, null, _instanceExtraInitializers);
2804
+ __esDecorate(this, null, _openIn_decorators, {
2805
+ kind: "method",
2806
+ name: "openIn",
2807
+ static: false,
2808
+ private: false,
2809
+ access: {
2810
+ has: (obj) => "openIn" in obj,
2811
+ get: (obj) => obj.openIn
2812
+ },
2813
+ metadata: _metadata
2814
+ }, null, _instanceExtraInitializers);
2815
+ __esDecorate(this, null, _taskKill_decorators, {
2816
+ kind: "method",
2817
+ name: "taskKill",
2818
+ static: false,
2819
+ private: false,
2820
+ access: {
2821
+ has: (obj) => "taskKill" in obj,
2822
+ get: (obj) => obj.taskKill
2823
+ },
2824
+ metadata: _metadata
2825
+ }, null, _instanceExtraInitializers);
2826
+ __esDecorate(this, null, _taskOutput_decorators, {
2827
+ kind: "method",
2828
+ name: "taskOutput",
2829
+ static: false,
2830
+ private: false,
2831
+ access: {
2832
+ has: (obj) => "taskOutput" in obj,
2833
+ get: (obj) => obj.taskOutput
2834
+ },
2835
+ metadata: _metadata
2836
+ }, null, _instanceExtraInitializers);
2837
+ __esDecorate(this, null, _deleteSession_decorators, {
2838
+ kind: "method",
2839
+ name: "deleteSession",
2840
+ static: false,
2841
+ private: false,
2842
+ access: {
2843
+ has: (obj) => "deleteSession" in obj,
2844
+ get: (obj) => obj.deleteSession
2845
+ },
2846
+ metadata: _metadata
2847
+ }, null, _instanceExtraInitializers);
2848
+ if (_metadata) Object.defineProperty(this, Symbol.metadata, {
2849
+ enumerable: true,
2850
+ configurable: true,
2851
+ writable: true,
2852
+ value: _metadata
2853
+ });
2854
+ }
2855
+ /** Loader validation for every deployment-varying choice this plugin makes. */
2856
+ static Config = z.object({
2857
+ showInSessionHeader: z.boolean().required(),
2858
+ showChanges: z.boolean().required(),
2859
+ showTerminal: z.boolean().required(),
2860
+ showFiles: z.boolean().required(),
2861
+ showTasks: z.boolean().required(),
2862
+ showOpenIn: z.boolean().required(),
2863
+ showArchive: z.boolean().required(),
2864
+ showDelete: z.boolean().required(),
2865
+ showPreview: z.boolean().required(),
2866
+ panelWidth: z.number().step(1).min(280).max(1400).required(),
2867
+ confirmDelete: z.boolean().required(),
2868
+ deleteMode: z.union(["archive", "purge"]).required(),
2869
+ allowTaskKill: z.boolean().required(),
2870
+ showTaskOutput: z.boolean().required(),
2871
+ gitMaxFiles: z.number().step(1).min(1).max(1e4).required(),
2872
+ gitDiffMaxBytes: z.number().step(1).min(1024).max(16 * 1024 * 1024).required(),
2873
+ gitTimeoutMs: z.number().step(1).min(1e3).max(6e5).required(),
2874
+ gitCommitTimeoutMs: z.number().step(1).min(1e3).max(18e5).required(),
2875
+ allowGitStaging: z.boolean().required(),
2876
+ allowGitCommit: z.boolean().required(),
2877
+ allowGitPush: z.boolean().required(),
2878
+ gitPushTimeoutMs: z.number().step(1).min(1e3).max(18e5).required(),
2879
+ allowCommitMessageDraft: z.boolean().required(),
2880
+ commitMessagePrompt: z.string(),
2881
+ commitMessageMaxBytes: z.number().step(1).min(1024).max(4 * 1024 * 1024).required(),
2882
+ terminalShell: z.string(),
2883
+ terminalScrollback: z.number().step(1).min(1024).max(4 * 1024 * 1024).required(),
2884
+ maxTerminals: z.number().step(1).min(1).max(32).required(),
2885
+ terminalGraceMs: z.number().step(1).min(100).max(6e4).required(),
2886
+ filesMaxPreviewBytes: z.number().step(1).min(1024).max(16 * 1024 * 1024).required(),
2887
+ filesMaxEntries: z.number().step(1).min(1).max(2e4).required(),
2888
+ filesShowHidden: z.boolean().required(),
2889
+ editors: z.array(EditorSchema).required(),
2890
+ previews: z.array(PreviewSchema).required(),
2891
+ previewsFromLaunchFile: z.boolean().required(),
2892
+ maxPreviews: z.number().step(1).min(1).max(16).required(),
2893
+ previewReadyTimeoutMs: z.number().step(1).min(1e3).max(6e5).required(),
2894
+ previewScrollback: z.number().step(1).min(1024).max(4 * 1024 * 1024).required(),
2895
+ previewGraceMs: z.number().step(1).min(100).max(6e4).required()
2896
+ });
2897
+ source = __runInitializers(this, _instanceExtraInitializers);
2898
+ git;
2899
+ terminals;
2900
+ launcher;
2901
+ files;
2902
+ tasks;
2903
+ deleter;
2904
+ preview;
2905
+ /**
2906
+ * @param ctx - Host context; every capability this service uses is resolved optionally, so a
2907
+ * deployment missing one still serves a view that explains which panel is dark and why.
2908
+ * @param config - the composition-layer preferences, used as the section's base layer.
2909
+ */
2910
+ constructor(ctx, config) {
2911
+ super(ctx, "advancedSidebar");
2912
+ validateConfig(config);
2913
+ this.source = () => config;
2914
+ const read = () => this.source();
2915
+ this.git = new GitReader(ctx, read);
2916
+ this.terminals = new PanelTerminals(ctx, read);
2917
+ this.launcher = new OpenInLauncher(ctx, read);
2918
+ this.files = new FileReader(ctx, read);
2919
+ this.tasks = new TaskController(ctx, read);
2920
+ this.deleter = new SessionDeleter(ctx, read);
2921
+ this.preview = new PreviewServers(ctx, read);
2922
+ installSettingsSection(ctx, ADVANCED_SIDEBAR_SETTINGS_NAMESPACE, AdvancedSidebarService$1.Config, config, {
2923
+ setSource: (current) => {
2924
+ this.source = current;
2925
+ },
2926
+ onChange: () => {
2927
+ this.launcher.forget();
2928
+ },
2929
+ validate: validateConfig
2930
+ });
2931
+ ctx.effect(() => async () => {
2932
+ this.tasks.dispose();
2933
+ await Promise.all([this.terminals.disposeAll(), this.preview.disposeAll()]);
2934
+ }, "advanced-sidebar: panel terminals, preview servers, retained task output");
2935
+ }
2936
+ /**
2937
+ * Describe which operations this Host can serve, so the menu can disable an entry with a reason
2938
+ * instead of offering one that fails when it is pressed.
2939
+ * @param signal - gateway-supplied cancellation for the executable probes.
2940
+ * @returns the capability view.
2941
+ */
2942
+ async describe(signal) {
2943
+ const settings = this.source();
2944
+ const deletion = this.deleter.describe();
2945
+ return {
2946
+ git: await this.git.describe(signal),
2947
+ terminal: this.terminals.describe(),
2948
+ files: this.files.describe(),
2949
+ preview: this.preview.describe(),
2950
+ tasks: this.tasks.describe(),
2951
+ openIn: await this.launcher.describe(signal),
2952
+ settings,
2953
+ deletion: {
2954
+ canPurge: deletion.canPurge,
2955
+ mode: settings.deleteMode === "purge" && deletion.canPurge ? "purge" : "archive",
2956
+ ...deletion.reason === void 0 ? {} : { reason: deletion.reason }
2957
+ },
2958
+ readAt: Date.now()
2959
+ };
2960
+ }
2961
+ /**
2962
+ * Read one workspace's git status.
2963
+ * @param request - the workspace directory.
2964
+ * @param signal - gateway-supplied cancellation for the caller's abandoned request.
2965
+ * @returns the reading, or a classified failure.
2966
+ */
2967
+ gitStatus(request, signal) {
2968
+ return this.git.status(request, signal);
2969
+ }
2970
+ /**
2971
+ * Read one path's patch.
2972
+ * @param request - the path and which index to compare.
2973
+ * @param signal - gateway-supplied cancellation for the caller's abandoned request.
2974
+ * @returns the patch, or a classified failure.
2975
+ */
2976
+ gitDiff(request, signal) {
2977
+ return this.git.diff(request, signal);
2978
+ }
2979
+ /**
2980
+ * Stage paths into the index.
2981
+ * @param request - the workspace and the repository-relative paths.
2982
+ * @param signal - gateway-supplied cancellation for the caller's abandoned request.
2983
+ * @returns the reading after the write, or a classified failure.
2984
+ */
2985
+ gitStage(request, signal) {
2986
+ return this.git.stage(request, signal);
2987
+ }
2988
+ /**
2989
+ * Take paths back out of the index, leaving the working tree alone.
2990
+ * @param request - the workspace and the repository-relative paths.
2991
+ * @param signal - gateway-supplied cancellation for the caller's abandoned request.
2992
+ * @returns the reading after the write, or a classified failure.
2993
+ */
2994
+ gitUnstage(request, signal) {
2995
+ return this.git.unstage(request, signal);
2996
+ }
2997
+ /**
2998
+ * Record the staged changes.
2999
+ * @param request - the workspace, the message, and whether to amend.
3000
+ * @param signal - gateway-supplied cancellation; hooks run under `gitCommitTimeoutMs`.
3001
+ * @returns the new commit and the reading after it, or a classified failure.
3002
+ */
3003
+ gitCommit(request, signal) {
3004
+ return this.git.commit(request, signal);
3005
+ }
3006
+ /**
3007
+ * Send the current branch's commits to its remote.
3008
+ * @param request - the workspace, and whether an unpublished branch may be published.
3009
+ * @param signal - gateway-supplied cancellation; the network wait runs under `gitPushTimeoutMs`.
3010
+ * @returns the push and the reading after it, or a classified failure.
3011
+ */
3012
+ gitPush(request, signal) {
3013
+ return this.git.push(request, signal);
3014
+ }
3015
+ /**
3016
+ * Ask the deployment's own model to write a commit message for what is staged.
3017
+ * @param request - the workspace, and whether the message is for an amend.
3018
+ * @param signal - gateway-supplied cancellation for the readings and the model call.
3019
+ * @returns the drafted message, or a classified failure.
3020
+ */
3021
+ gitCommitMessage(request, signal) {
3022
+ return this.git.draftCommitMessage(request, signal);
3023
+ }
3024
+ /**
3025
+ * Allocate a panel terminal.
3026
+ * @param request - the workspace directory and the panel's measured geometry.
3027
+ * @param signal - gateway-supplied cancellation of the allocation.
3028
+ * @returns the handle, or a classified failure.
3029
+ */
3030
+ terminalOpen(request, signal) {
3031
+ return this.terminals.open(request, signal);
3032
+ }
3033
+ /**
3034
+ * Read a panel terminal's output from a caller-owned offset.
3035
+ * @param request - the handle and the offset already rendered.
3036
+ * @returns the delta and the process state, or a classified failure.
3037
+ */
3038
+ terminalRead(request) {
3039
+ return Promise.resolve(this.terminals.read(request));
3040
+ }
3041
+ /**
3042
+ * Send keystrokes to a panel terminal.
3043
+ * @param request - the handle and the text to deliver verbatim.
3044
+ * @returns settlement, or a classified failure.
3045
+ */
3046
+ terminalWrite(request) {
3047
+ return this.terminals.write(request);
3048
+ }
3049
+ /**
3050
+ * Deliver a signal to a panel terminal's foreground process group.
3051
+ * @param request - the handle and the signal.
3052
+ * @returns settlement, or a classified failure.
3053
+ */
3054
+ terminalSignal(request) {
3055
+ return this.terminals.signal(request);
3056
+ }
3057
+ /**
3058
+ * Close a panel terminal.
3059
+ * @param request - the handle.
3060
+ * @returns settlement, or a classified failure.
3061
+ */
3062
+ terminalClose(request) {
3063
+ return this.terminals.close(request);
3064
+ }
3065
+ /**
3066
+ * List one directory level for the Files panel.
3067
+ * @param request - the directory and the workspace it must stay inside.
3068
+ * @param signal - gateway-supplied cancellation for the caller's abandoned request.
3069
+ * @returns the level, or a classified failure.
3070
+ */
3071
+ listEntries(request, signal) {
3072
+ return this.files.list(request, signal);
3073
+ }
3074
+ /**
3075
+ * List one workspace's preview launch configurations, each with its current state.
3076
+ * @param request - the workspace to read.
3077
+ * @param signal - gateway-supplied cancellation for the caller's abandoned request.
3078
+ * @returns the list, or a classified failure.
3079
+ */
3080
+ previewList(request, signal) {
3081
+ return this.preview.list(request, signal);
3082
+ }
3083
+ /**
3084
+ * Start one preview configuration.
3085
+ * @param request - the workspace and the configuration name.
3086
+ * @param signal - gateway-supplied cancellation of the start.
3087
+ * @returns the started row, or a classified failure.
3088
+ */
3089
+ previewStart(request, signal) {
3090
+ return this.preview.start(request, signal);
3091
+ }
3092
+ /**
3093
+ * Stop one running preview server.
3094
+ * @param request - the handle.
3095
+ * @returns settlement, or a classified failure.
3096
+ */
3097
+ previewStop(request) {
3098
+ return this.preview.stop(request);
3099
+ }
3100
+ /**
3101
+ * Read one preview server's output from a caller-owned offset, with its state at read time.
3102
+ * @param request - the handle and the offset already rendered.
3103
+ * @returns the delta and the state, or a classified failure.
3104
+ */
3105
+ previewLogs(request) {
3106
+ return Promise.resolve(this.preview.logs(request));
3107
+ }
3108
+ /**
3109
+ * Read one file for the Files panel preview.
3110
+ * @param request - the file and the workspace it must stay inside.
3111
+ * @param signal - gateway-supplied cancellation for the caller's abandoned request.
3112
+ * @returns the preview, or a classified failure.
3113
+ */
3114
+ readFile(request, signal) {
3115
+ return this.files.read(request, signal);
3116
+ }
3117
+ /**
3118
+ * Hand one path to an external application or to the operating system's file manager.
3119
+ * @param request - the target and the path.
3120
+ * @param signal - gateway-supplied cancellation for the launch.
3121
+ * @returns settlement, or a classified failure.
3122
+ */
3123
+ openIn(request, signal) {
3124
+ return this.launcher.open(request, signal);
3125
+ }
3126
+ /**
3127
+ * Stop one live background task.
3128
+ * @param request - the owning session and the task id.
3129
+ * @returns what the registry did, or a classified failure.
3130
+ */
3131
+ taskKill(request) {
3132
+ return this.tasks.kill(request);
3133
+ }
3134
+ /**
3135
+ * Read one settled background task's output.
3136
+ * @param request - the owning session and the task id.
3137
+ * @returns the accumulated output, or a classified failure.
3138
+ */
3139
+ taskOutput(request) {
3140
+ return this.tasks.output(request);
3141
+ }
3142
+ /**
3143
+ * Delete one session: archive it, and remove its durable artifact when the mode and Host allow.
3144
+ * @param request - the session to delete.
3145
+ * @param signal - gateway-supplied cancellation for the persistence listing.
3146
+ * @returns what was actually done, or a classified failure.
3147
+ */
3148
+ deleteSession(request, signal) {
3149
+ return this.deleter.delete(request, signal);
3150
+ }
3151
+ };
3152
+ })();
3153
+ var host_default = AdvancedSidebarService;
3154
+
3155
+ //#endregion
3156
+ export { ADVANCED_SIDEBAR_SETTINGS_NAMESPACE, AdvancedSidebarService, REVEAL_TARGET_ID, host_default as default };