@volter/supercode-terminal 0.2.30

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/ui.mjs ADDED
@@ -0,0 +1,892 @@
1
+ // src/ui.jsx
2
+ import { useEffect, useMemo, useRef, useState } from "preact/hooks";
3
+ import { Fragment, jsx, jsxs } from "preact/jsx-runtime";
4
+ var DEFAULT_LABELS = Object.freeze({
5
+ back: "Back to terminals",
6
+ control: "Control",
7
+ empty: "No terminal sessions are running.",
8
+ open: "Open",
9
+ refresh: "Refresh",
10
+ // what a session with no observed cwd or command is
11
+ session: "tmux session",
12
+ stop: "Stop",
13
+ subtitle: "Local tmux sessions",
14
+ title: "Terminals",
15
+ view: "View"
16
+ });
17
+ var WORKSPACE_LABELS = Object.freeze({
18
+ ...DEFAULT_LABELS,
19
+ attach: "Open session",
20
+ back: "Back to chats",
21
+ closeTab: "Detach terminal",
22
+ newTerminal: "New terminal",
23
+ noTabs: "Open a local session or start a new coding agent.",
24
+ open: "Open locally",
25
+ sessions: "Sessions",
26
+ fitViewport: "Fit terminal to window",
27
+ scaleViewport: "Keep terminal grid scaled",
28
+ stop: "Stop session",
29
+ terminal: "Terminal"
30
+ });
31
+ function record(value) {
32
+ return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
33
+ }
34
+ function normalizeTerminalUiState(value) {
35
+ const source = record(value) ?? {};
36
+ const attachment = record(source.attachment);
37
+ const normalizedAttachment = attachment && typeof attachment.id === "string" && typeof attachment.baseUrl === "string" && (attachment.mode === "observe" || attachment.mode === "control") ? {
38
+ id: attachment.id,
39
+ baseUrl: attachment.baseUrl,
40
+ mode: attachment.mode,
41
+ ...typeof attachment.sessionId === "string" ? { sessionId: attachment.sessionId } : {}
42
+ } : null;
43
+ const sessions = Array.isArray(source.sessions) ? source.sessions.flatMap((candidate) => {
44
+ const session = record(candidate);
45
+ if (!session || typeof session.id !== "string" || typeof session.label !== "string") return [];
46
+ return [{
47
+ id: session.id,
48
+ label: session.label,
49
+ cwd: typeof session.cwd === "string" ? session.cwd : null,
50
+ activeCommand: typeof session.activeCommand === "string" ? session.activeCommand : null,
51
+ owned: session.owned === true,
52
+ size: record(session.size) && Number.isInteger(session.size.columns) && session.size.columns > 0 && Number.isInteger(session.size.rows) && session.size.rows > 0 ? { columns: session.size.columns, rows: session.size.rows } : null
53
+ }];
54
+ }) : [];
55
+ return {
56
+ available: source.available === true,
57
+ canOpenLocal: source.canOpenLocal === true,
58
+ error: typeof source.error === "string" ? source.error : null,
59
+ sessions,
60
+ attachment: normalizedAttachment
61
+ };
62
+ }
63
+ function terminalPath(value) {
64
+ const complete = value.replaceAll("\\", "/");
65
+ const boundary = complete.lastIndexOf("/");
66
+ if (boundary < 0) return { leading: complete, separator: "", trailing: "" };
67
+ return {
68
+ leading: complete.slice(0, boundary),
69
+ separator: "/",
70
+ trailing: complete.slice(boundary + 1)
71
+ };
72
+ }
73
+ function TerminalPath({ value }) {
74
+ const path = terminalPath(value);
75
+ return /* @__PURE__ */ jsxs("small", { class: "sctui-path", title: value, children: [
76
+ /* @__PURE__ */ jsx("span", { children: path.leading }),
77
+ path.separator ? /* @__PURE__ */ jsx("i", { children: path.separator }) : null,
78
+ path.trailing ? /* @__PURE__ */ jsx("b", { children: path.trailing }) : null
79
+ ] });
80
+ }
81
+ var TERMINAL_THEME_SLOTS = ["foreground", "cursor", "black", "red", "green", "yellow", "blue", "magenta", "cyan", "white", "brightBlack", "brightRed", "brightGreen", "brightYellow", "brightBlue", "brightMagenta", "brightCyan", "brightWhite"];
82
+ function terminalTheme(element) {
83
+ const probe = element.ownerDocument.createElement("span");
84
+ probe.hidden = true;
85
+ element.append(probe);
86
+ const theme = { background: "rgba(0, 0, 0, 0)" };
87
+ for (const slot of TERMINAL_THEME_SLOTS) {
88
+ const name = `--sctui-terminal-${slot.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`)}`;
89
+ probe.style.color = `var(${name}, var(${name}-default))`;
90
+ theme[slot] = getComputedStyle(probe).color;
91
+ }
92
+ probe.remove();
93
+ return theme;
94
+ }
95
+ function TerminalViewer(props) {
96
+ const [connector, setConnector] = useState(() => props.connect ?? null);
97
+ const [error, setError] = useState(null);
98
+ useEffect(() => {
99
+ if (props.connect) {
100
+ setConnector(() => props.connect);
101
+ return;
102
+ }
103
+ let disposed = false;
104
+ import("@volter/supercode-terminal/client").then((module) => {
105
+ if (!disposed) setConnector(() => module.connectTerminal);
106
+ }, (cause) => {
107
+ if (!disposed) setError(String(cause));
108
+ });
109
+ return () => {
110
+ disposed = true;
111
+ };
112
+ }, [props.connect]);
113
+ return connector ? /* @__PURE__ */ jsx(TerminalStreamViewer, { ...props, connect: connector }) : /* @__PURE__ */ jsx("div", { role: error ? "alert" : "status", children: error ?? "Loading terminal\u2026" });
114
+ }
115
+ function TerminalStreamViewer({ active = true, attachment, connect, convertEol = false, onReady, onActivity, onBell, onOpenLink, onRetry, onStatus, onTitle }) {
116
+ const root = useRef(null);
117
+ const runtime = useRef(null);
118
+ const searchInput = useRef(null);
119
+ const pasteDialog = useRef(null);
120
+ const bellTimer = useRef(null);
121
+ const activeRef = useRef(active);
122
+ const callbacks = useRef({});
123
+ const [status, setStatus] = useState("Loading terminal\u2026");
124
+ const [bellVisible, setBellVisible] = useState(false);
125
+ const [findOpen, setFindOpen] = useState(false);
126
+ const [findQuery, setFindQuery] = useState("");
127
+ const [findFound, setFindFound] = useState(null);
128
+ const [helpOpen, setHelpOpen] = useState(false);
129
+ const [historyOpen, setHistoryOpen] = useState(false);
130
+ const [pasteHistory, setPasteHistory] = useState([]);
131
+ const [pendingPaste, setPendingPaste] = useState(null);
132
+ activeRef.current = active;
133
+ callbacks.current = { onActivity, onBell, onOpenLink, onRetry, onStatus, onTitle };
134
+ const publishStatus = (next) => {
135
+ setStatus(next);
136
+ callbacks.current.onStatus?.(next);
137
+ };
138
+ const focusTerminal = () => runtime.current?.terminal.focus();
139
+ const closeFind = () => {
140
+ runtime.current?.search.clearDecorations();
141
+ setFindOpen(false);
142
+ queueMicrotask(focusTerminal);
143
+ };
144
+ const find = (direction = "next", query = findQuery) => {
145
+ if (!query || !runtime.current) return;
146
+ const found = runtime.current.search[direction === "previous" ? "findPrevious" : "findNext"](query, {
147
+ incremental: direction === "next"
148
+ });
149
+ setFindFound(found);
150
+ };
151
+ const openFind = () => {
152
+ setHelpOpen(false);
153
+ setHistoryOpen(false);
154
+ setFindOpen(true);
155
+ queueMicrotask(() => searchInput.current?.focus());
156
+ };
157
+ const sendPaste = (value, remember = true) => {
158
+ runtime.current?.terminal.paste(value);
159
+ if (!remember || value.length > 5e4) return;
160
+ setPasteHistory((current) => [value, ...current.filter((item) => item !== value)].slice(0, 20));
161
+ };
162
+ const proposePaste = (value) => {
163
+ setHistoryOpen(false);
164
+ if (value.length > 1e3 || /[\r\n]/.test(value)) setPendingPaste(value);
165
+ else sendPaste(value);
166
+ };
167
+ const saveContents = () => {
168
+ const terminal = runtime.current?.terminal;
169
+ if (!terminal) return;
170
+ const buffer = terminal.buffer.normal;
171
+ const lines = [];
172
+ for (let index = 0; index < buffer.length; index += 1) {
173
+ lines.push(buffer.getLine(index)?.translateToString(true) ?? "");
174
+ }
175
+ const blob = new Blob([`${lines.join("\n")}
176
+ `], { type: "text/plain;charset=utf-8" });
177
+ const url = URL.createObjectURL(blob);
178
+ const link = document.createElement("a");
179
+ const name = (attachment.sessionId ?? "session").replaceAll(/[^a-z0-9_-]/gi, "-").slice(0, 80);
180
+ link.download = `terminal-${name}.txt`;
181
+ link.href = url;
182
+ link.click();
183
+ queueMicrotask(() => URL.revokeObjectURL(url));
184
+ };
185
+ useEffect(() => {
186
+ if (!root.current) return;
187
+ let disposed = false;
188
+ let disposeRuntime = () => void 0;
189
+ void Promise.all([
190
+ import("@xterm/addon-fit"),
191
+ import("@xterm/addon-search"),
192
+ import("@xterm/addon-web-links"),
193
+ import("@xterm/xterm")
194
+ ]).then(([{ FitAddon }, { SearchAddon }, { WebLinksAddon }, { Terminal }]) => {
195
+ if (disposed || !root.current) return;
196
+ const terminal = new Terminal({
197
+ allowTransparency: true,
198
+ convertEol,
199
+ cursorBlink: attachment.mode === "control",
200
+ disableStdin: attachment.mode !== "control",
201
+ fontFamily: "ui-monospace, SFMono-Regular, Menlo, Consolas, monospace",
202
+ fontSize: 12,
203
+ scrollback: 5e3,
204
+ theme: terminalTheme(root.current)
205
+ });
206
+ const fit = new FitAddon();
207
+ const search = new SearchAddon();
208
+ terminal.loadAddon(fit);
209
+ terminal.loadAddon(search);
210
+ terminal.loadAddon(new WebLinksAddon((_event, uri) => {
211
+ if (callbacks.current.onOpenLink) callbacks.current.onOpenLink(uri);
212
+ else globalThis.open(uri, "_blank", "noopener,noreferrer");
213
+ }));
214
+ terminal.open(root.current);
215
+ fit.fit();
216
+ runtime.current = { search, terminal };
217
+ const title = terminal.onTitleChange((value) => {
218
+ const normalized = value.replaceAll(/[\u0000-\u001f\u007f]/g, " ").trim().slice(0, 160);
219
+ if (normalized) callbacks.current.onTitle?.(normalized);
220
+ });
221
+ const bell = terminal.onBell(() => {
222
+ clearTimeout(bellTimer.current);
223
+ setBellVisible(true);
224
+ bellTimer.current = setTimeout(() => setBellVisible(false), 350);
225
+ callbacks.current.onBell?.();
226
+ });
227
+ let connected = false;
228
+ const handlers = {
229
+ onOpen() {
230
+ connected = true;
231
+ publishStatus(attachment.mode === "control" ? "Live \xB7 interactive" : "Live \xB7 read-only");
232
+ },
233
+ onOutput(data) {
234
+ if (disposed) return;
235
+ terminal.write(data);
236
+ if (!activeRef.current) callbacks.current.onActivity?.();
237
+ },
238
+ onResize(cols, rows) {
239
+ if (!disposed) terminal.resize(cols, rows);
240
+ },
241
+ onError(message) {
242
+ if (!disposed) {
243
+ publishStatus(message);
244
+ terminal.writeln(`\r
245
+ ${message}`);
246
+ }
247
+ },
248
+ onExit() {
249
+ connected = false;
250
+ if (!disposed) publishStatus("Terminal exited");
251
+ },
252
+ onClose() {
253
+ connected = false;
254
+ if (!disposed) setStatus((current) => {
255
+ const next = current === "Terminal exited" ? current : "Terminal disconnected";
256
+ callbacks.current.onStatus?.(next);
257
+ return next;
258
+ });
259
+ }
260
+ };
261
+ const size = { cols: terminal.cols, rows: terminal.rows };
262
+ let connection;
263
+ try {
264
+ connection = connect(attachment, handlers, size);
265
+ } catch (error) {
266
+ bell.dispose();
267
+ title.dispose();
268
+ terminal.dispose();
269
+ runtime.current = null;
270
+ throw error;
271
+ }
272
+ let resizeFrame = 0;
273
+ let sentSize = size;
274
+ const refit = () => {
275
+ if (disposed || !connected || attachment.mode !== "control") return;
276
+ fit.fit();
277
+ if (terminal.cols === sentSize.cols && terminal.rows === sentSize.rows) return;
278
+ sentSize = { cols: terminal.cols, rows: terminal.rows };
279
+ connection.resize(terminal.cols, terminal.rows);
280
+ };
281
+ const resizeObserver = attachment.mode === "control" && globalThis.ResizeObserver ? new ResizeObserver(() => {
282
+ cancelAnimationFrame(resizeFrame);
283
+ resizeFrame = requestAnimationFrame(refit);
284
+ }) : null;
285
+ resizeObserver?.observe(root.current);
286
+ const input = attachment.mode === "control" ? terminal.onData((data) => connection.write(data)) : null;
287
+ onReady?.({ focus() {
288
+ refit();
289
+ terminal.focus();
290
+ } });
291
+ const focus = () => {
292
+ if (activeRef.current && attachment.mode === "control") terminal.focus();
293
+ };
294
+ root.current.addEventListener("pointerdown", focus);
295
+ const paste = (event) => {
296
+ if (attachment.mode !== "control") return;
297
+ const value = event.clipboardData?.getData("text/plain");
298
+ if (!value) return;
299
+ event.preventDefault();
300
+ proposePaste(value);
301
+ };
302
+ root.current.addEventListener("paste", paste, true);
303
+ if (active) requestAnimationFrame(focus);
304
+ disposeRuntime = () => {
305
+ root.current?.removeEventListener("pointerdown", focus);
306
+ root.current?.removeEventListener("paste", paste, true);
307
+ input?.dispose();
308
+ resizeObserver?.disconnect();
309
+ cancelAnimationFrame(resizeFrame);
310
+ bell.dispose();
311
+ title.dispose();
312
+ connection.close();
313
+ terminal.dispose();
314
+ runtime.current = null;
315
+ clearTimeout(bellTimer.current);
316
+ };
317
+ }).catch((error) => {
318
+ if (!disposed) publishStatus(error instanceof Error ? error.message : "Could not load the terminal viewer");
319
+ });
320
+ return () => {
321
+ disposed = true;
322
+ disposeRuntime();
323
+ };
324
+ }, [attachment.id, attachment.mode, connect, convertEol]);
325
+ useEffect(() => {
326
+ if (!active || attachment.mode !== "control") return;
327
+ const focus = () => root.current?.querySelector(".xterm-helper-textarea")?.focus({ preventScroll: true });
328
+ focus();
329
+ requestAnimationFrame(focus);
330
+ }, [active, attachment.mode]);
331
+ useEffect(() => {
332
+ if (!pendingPaste) return;
333
+ queueMicrotask(() => pasteDialog.current?.querySelector("button")?.focus({ preventScroll: true }));
334
+ }, [pendingPaste]);
335
+ const retryable = status === "Terminal disconnected" || status === "Terminal connection failed";
336
+ return /* @__PURE__ */ jsxs(
337
+ "div",
338
+ {
339
+ class: "sctui-viewer",
340
+ "data-bell": String(bellVisible),
341
+ onKeyDownCapture: (event) => {
342
+ if (pendingPaste) {
343
+ if (event.key === "Escape") {
344
+ event.preventDefault();
345
+ setPendingPaste(null);
346
+ queueMicrotask(focusTerminal);
347
+ } else if (event.key === "Tab") {
348
+ const buttons = [...pasteDialog.current?.querySelectorAll("button") ?? []];
349
+ const first = buttons.at(0);
350
+ const last = buttons.at(-1);
351
+ if (event.shiftKey && document.activeElement === first) {
352
+ event.preventDefault();
353
+ last?.focus();
354
+ } else if (!event.shiftKey && document.activeElement === last) {
355
+ event.preventDefault();
356
+ first?.focus();
357
+ }
358
+ }
359
+ return;
360
+ }
361
+ if (event.key === "Escape" && findOpen) {
362
+ event.preventDefault();
363
+ closeFind();
364
+ } else if (event.key === "Escape" && (helpOpen || historyOpen)) {
365
+ event.preventDefault();
366
+ setHelpOpen(false);
367
+ setHistoryOpen(false);
368
+ queueMicrotask(focusTerminal);
369
+ } else if (event.altKey && event.shiftKey && !event.ctrlKey && !event.metaKey && event.code === "KeyF") {
370
+ event.preventDefault();
371
+ openFind();
372
+ }
373
+ },
374
+ children: [
375
+ /* @__PURE__ */ jsxs("div", { class: "sctui-viewerbar", children: [
376
+ /* @__PURE__ */ jsxs("div", { class: "sctui-status", "data-live": String(status.startsWith("Live")), role: "status", children: [
377
+ /* @__PURE__ */ jsx("span", { "aria-hidden": "true" }),
378
+ status,
379
+ retryable && onRetry ? /* @__PURE__ */ jsx("button", { type: "button", onClick: onRetry, children: "Reconnect" }) : null
380
+ ] }),
381
+ /* @__PURE__ */ jsxs("div", { class: "sctui-tools", "aria-label": "Terminal tools", children: [
382
+ /* @__PURE__ */ jsx("button", { type: "button", "aria-keyshortcuts": "Alt+Shift+F", "aria-label": "Find in terminal", title: `Find (${terminalShortcutLabel("F")})`, onClick: openFind, children: /* @__PURE__ */ jsx(SearchGlyph, {}) }),
383
+ /* @__PURE__ */ jsx("button", { type: "button", "aria-label": "Paste history", "aria-expanded": historyOpen, disabled: !pasteHistory.length, title: "Paste history (memory only)", onClick: () => {
384
+ setHelpOpen(false);
385
+ setHistoryOpen((open) => !open);
386
+ }, children: /* @__PURE__ */ jsx(HistoryGlyph, {}) }),
387
+ /* @__PURE__ */ jsx("button", { type: "button", "aria-label": "Save terminal contents", title: "Save terminal contents", onClick: saveContents, children: /* @__PURE__ */ jsx(DownloadGlyph, {}) }),
388
+ /* @__PURE__ */ jsx("button", { type: "button", "aria-label": "Terminal shortcut help", "aria-expanded": helpOpen, title: "Keyboard shortcuts", onClick: () => {
389
+ setHistoryOpen(false);
390
+ setHelpOpen((open) => !open);
391
+ }, children: /* @__PURE__ */ jsx(HelpGlyph, {}) })
392
+ ] })
393
+ ] }),
394
+ findOpen ? /* @__PURE__ */ jsxs("form", { class: "sctui-find", onSubmit: (event) => {
395
+ event.preventDefault();
396
+ find(event.shiftKey ? "previous" : "next");
397
+ }, children: [
398
+ /* @__PURE__ */ jsx(SearchGlyph, {}),
399
+ /* @__PURE__ */ jsx("input", { ref: searchInput, value: findQuery, "aria-label": "Find", placeholder: "Find", onInput: (event) => {
400
+ const query = event.currentTarget.value;
401
+ setFindQuery(query);
402
+ if (query) find("next", query);
403
+ else {
404
+ runtime.current?.search.clearDecorations();
405
+ setFindFound(null);
406
+ }
407
+ } }),
408
+ /* @__PURE__ */ jsx("small", { children: findFound === null ? "" : findFound ? "Match" : "No results" }),
409
+ /* @__PURE__ */ jsx("button", { type: "button", "aria-label": "Previous match", onClick: () => find("previous"), children: /* @__PURE__ */ jsx(UpGlyph, {}) }),
410
+ /* @__PURE__ */ jsx("button", { type: "button", "aria-label": "Next match", onClick: () => find("next"), children: /* @__PURE__ */ jsx(DownGlyph, {}) }),
411
+ /* @__PURE__ */ jsx("button", { type: "button", "aria-label": "Close find", onClick: closeFind, children: /* @__PURE__ */ jsx(CloseGlyph, {}) })
412
+ ] }) : null,
413
+ historyOpen ? /* @__PURE__ */ jsxs("div", { class: "sctui-popover sctui-paste-history", role: "menu", "aria-label": "Paste history", children: [
414
+ /* @__PURE__ */ jsxs("strong", { children: [
415
+ "Paste history ",
416
+ /* @__PURE__ */ jsxs("span", { children: [
417
+ /* @__PURE__ */ jsx("small", { children: "memory only" }),
418
+ /* @__PURE__ */ jsx("button", { type: "button", onClick: () => {
419
+ setPasteHistory([]);
420
+ setHistoryOpen(false);
421
+ }, children: "Clear" })
422
+ ] })
423
+ ] }),
424
+ pasteHistory.map((value, index) => /* @__PURE__ */ jsxs("button", { type: "button", role: "menuitem", onClick: () => proposePaste(value), children: [
425
+ /* @__PURE__ */ jsx("span", { children: value.replaceAll(/\s+/g, " ").slice(0, 90) }),
426
+ /* @__PURE__ */ jsxs("small", { children: [
427
+ value.length,
428
+ " chars"
429
+ ] })
430
+ ] }, `${index}:${value.length}`))
431
+ ] }) : null,
432
+ helpOpen ? /* @__PURE__ */ jsxs("div", { class: "sctui-popover sctui-shortcuts", role: "dialog", "aria-label": "Terminal keyboard shortcuts", children: [
433
+ /* @__PURE__ */ jsx("strong", { children: "Terminal shortcuts" }),
434
+ /* @__PURE__ */ jsxs("dl", { children: [
435
+ /* @__PURE__ */ jsxs("div", { children: [
436
+ /* @__PURE__ */ jsx("dt", { children: "New or attach" }),
437
+ /* @__PURE__ */ jsx("dd", { children: terminalShortcutLabel("T") })
438
+ ] }),
439
+ /* @__PURE__ */ jsxs("div", { children: [
440
+ /* @__PURE__ */ jsx("dt", { children: "Find" }),
441
+ /* @__PURE__ */ jsx("dd", { children: terminalShortcutLabel("F") })
442
+ ] }),
443
+ /* @__PURE__ */ jsxs("div", { children: [
444
+ /* @__PURE__ */ jsx("dt", { children: "Detach tab" }),
445
+ /* @__PURE__ */ jsx("dd", { children: terminalShortcutLabel("D") })
446
+ ] }),
447
+ /* @__PURE__ */ jsxs("div", { children: [
448
+ /* @__PURE__ */ jsx("dt", { children: "Previous / next tab" }),
449
+ /* @__PURE__ */ jsx("dd", { children: terminalShortcutLabel("[ / ]") })
450
+ ] }),
451
+ /* @__PURE__ */ jsxs("div", { children: [
452
+ /* @__PURE__ */ jsx("dt", { children: "Select tab" }),
453
+ /* @__PURE__ */ jsx("dd", { children: terminalShortcutLabel("1\u20139") })
454
+ ] })
455
+ ] }),
456
+ /* @__PURE__ */ jsx("small", { children: "Command and Control shortcuts stay with the browser and terminal." })
457
+ ] }) : null,
458
+ pendingPaste ? /* @__PURE__ */ jsxs("div", { class: "sctui-paste-confirm", ref: pasteDialog, role: "alertdialog", "aria-modal": "true", "aria-label": "Confirm terminal paste", children: [
459
+ /* @__PURE__ */ jsx("strong", { children: "Paste into this terminal?" }),
460
+ /* @__PURE__ */ jsxs("span", { children: [
461
+ pendingPaste.split(/\r?\n/).length,
462
+ " lines \xB7 ",
463
+ pendingPaste.length.toLocaleString(),
464
+ " characters"
465
+ ] }),
466
+ /* @__PURE__ */ jsx("pre", { children: pendingPaste.replaceAll(/\s+/g, " ").slice(0, 180) }),
467
+ /* @__PURE__ */ jsxs("div", { children: [
468
+ /* @__PURE__ */ jsx("button", { type: "button", onClick: () => {
469
+ setPendingPaste(null);
470
+ queueMicrotask(focusTerminal);
471
+ }, children: "Cancel" }),
472
+ /* @__PURE__ */ jsx("button", { type: "button", onClick: () => {
473
+ sendPaste(pendingPaste);
474
+ setPendingPaste(null);
475
+ queueMicrotask(focusTerminal);
476
+ }, children: "Paste" })
477
+ ] })
478
+ ] }) : null,
479
+ /* @__PURE__ */ jsx("div", { class: "sctui-screen", "data-active": String(active), ref: root })
480
+ ]
481
+ }
482
+ );
483
+ }
484
+ function TerminalGlyph() {
485
+ return /* @__PURE__ */ jsx("svg", { "aria-hidden": "true", viewBox: "0 0 16 16", children: /* @__PURE__ */ jsx("path", { d: "m3 4 3.5 4L3 12M8 12h5" }) });
486
+ }
487
+ function CloseGlyph() {
488
+ return /* @__PURE__ */ jsx("svg", { "aria-hidden": "true", viewBox: "0 0 16 16", children: /* @__PURE__ */ jsx("path", { d: "m4.5 4.5 7 7m0-7-7 7" }) });
489
+ }
490
+ function BackGlyph() {
491
+ return /* @__PURE__ */ jsx("svg", { "aria-hidden": "true", viewBox: "0 0 16 16", children: /* @__PURE__ */ jsx("path", { d: "m9.5 3.5-4.5 4.5 4.5 4.5M5 8h7" }) });
492
+ }
493
+ function PlusGlyph() {
494
+ return /* @__PURE__ */ jsx("svg", { "aria-hidden": "true", viewBox: "0 0 16 16", children: /* @__PURE__ */ jsx("path", { d: "M8 3v10M3 8h10" }) });
495
+ }
496
+ function SearchGlyph() {
497
+ return /* @__PURE__ */ jsxs("svg", { "aria-hidden": "true", viewBox: "0 0 16 16", children: [
498
+ /* @__PURE__ */ jsx("circle", { cx: "7", cy: "7", r: "3.5" }),
499
+ /* @__PURE__ */ jsx("path", { d: "m9.7 9.7 3 3" })
500
+ ] });
501
+ }
502
+ function HistoryGlyph() {
503
+ return /* @__PURE__ */ jsx("svg", { "aria-hidden": "true", viewBox: "0 0 16 16", children: /* @__PURE__ */ jsx("path", { d: "M3.2 5.3A5 5 0 1 1 3 10M3.2 5.3V2.7m0 2.6h2.6M8 4.8V8l2 1.3" }) });
504
+ }
505
+ function DownloadGlyph() {
506
+ return /* @__PURE__ */ jsx("svg", { "aria-hidden": "true", viewBox: "0 0 16 16", children: /* @__PURE__ */ jsx("path", { d: "M8 2.5v7m-2.5-2L8 10l2.5-2.5M3 12.5h10" }) });
507
+ }
508
+ function HelpGlyph() {
509
+ return /* @__PURE__ */ jsxs("svg", { "aria-hidden": "true", viewBox: "0 0 16 16", children: [
510
+ /* @__PURE__ */ jsx("circle", { cx: "8", cy: "8", r: "5.5" }),
511
+ /* @__PURE__ */ jsx("path", { d: "M6.7 6.2A1.5 1.5 0 0 1 8.2 5c1 0 1.7.6 1.7 1.5 0 1.4-1.8 1.4-1.8 2.6M8.1 11.4h.01" })
512
+ ] });
513
+ }
514
+ function UpGlyph() {
515
+ return /* @__PURE__ */ jsx("svg", { "aria-hidden": "true", viewBox: "0 0 16 16", children: /* @__PURE__ */ jsx("path", { d: "m4 10 4-4 4 4" }) });
516
+ }
517
+ function DownGlyph() {
518
+ return /* @__PURE__ */ jsx("svg", { "aria-hidden": "true", viewBox: "0 0 16 16", children: /* @__PURE__ */ jsx("path", { d: "m4 6 4 4 4-4" }) });
519
+ }
520
+ function ViewportGlyph({ mode }) {
521
+ return mode === "fit" ? /* @__PURE__ */ jsx("svg", { "aria-hidden": "true", viewBox: "0 0 16 16", children: /* @__PURE__ */ jsx("path", { d: "M6 3H3v3m7-3h3v3M6 13H3v-3m7 3h3v-3M3.5 3.5 6 6m6.5-2.5L10 6m-6.5 6.5L6 10m6.5 2.5L10 10" }) }) : /* @__PURE__ */ jsxs("svg", { "aria-hidden": "true", viewBox: "0 0 16 16", children: [
522
+ /* @__PURE__ */ jsx("rect", { x: "3", y: "4", width: "10", height: "8", rx: "1" }),
523
+ /* @__PURE__ */ jsx("path", { d: "M6 2.5h4M6 13.5h4" })
524
+ ] });
525
+ }
526
+ function terminalWorkspaceShortcut(event) {
527
+ if (!event.altKey || !event.shiftKey || event.ctrlKey || event.metaKey) return null;
528
+ if (/^Digit[1-9]$/.test(event.code)) return { action: "tab", index: Number(event.code.slice(-1)) - 1 };
529
+ if (event.code === "KeyD") return { action: "detach" };
530
+ if (event.code === "KeyT") return { action: "new" };
531
+ if (event.code === "BracketLeft") return { action: "previous" };
532
+ if (event.code === "BracketRight") return { action: "next" };
533
+ return null;
534
+ }
535
+ function terminalShortcutLabel(key) {
536
+ return /Mac|iPhone|iPad|iPod/i.test(globalThis.navigator?.platform ?? "") ? `\u2325\u21E7${key}` : `Alt+Shift+${key}`;
537
+ }
538
+ function TerminalWorkspace({
539
+ className = "",
540
+ createActions = [],
541
+ labels: labelOverrides,
542
+ onAttach,
543
+ onClose,
544
+ onCreate,
545
+ onOpen,
546
+ onRefresh,
547
+ onStop,
548
+ onViewportModeChange,
549
+ renderViewer,
550
+ state,
551
+ viewportMode = "scale"
552
+ }) {
553
+ const labels = { ...WORKSPACE_LABELS, ...labelOverrides };
554
+ const [tabs, setTabs] = useState([]);
555
+ const [activeSessionId, setActiveSessionId] = useState(null);
556
+ const [menuOpen, setMenuOpen] = useState(false);
557
+ const [pending, setPending] = useState(false);
558
+ const [tabMetadata, setTabMetadata] = useState({});
559
+ const [tabStatuses, setTabStatuses] = useState({});
560
+ const menu = useRef(null);
561
+ const sessionById = useMemo(() => new Map(state.sessions.map((session) => [session.id, session])), [state.sessions]);
562
+ useEffect(() => {
563
+ const attachment = state.attachment;
564
+ if (!attachment?.sessionId) return;
565
+ setTabs((current) => {
566
+ const existing = current.findIndex((tab) => tab.sessionId === attachment.sessionId);
567
+ if (existing < 0) return [...current, { attachment, sessionId: attachment.sessionId }];
568
+ if (current[existing].attachment.id === attachment.id) return current;
569
+ const next = [...current];
570
+ next[existing] = { attachment, sessionId: attachment.sessionId };
571
+ return next;
572
+ });
573
+ setActiveSessionId(attachment.sessionId);
574
+ }, [state.attachment?.id, state.attachment?.sessionId]);
575
+ useEffect(() => {
576
+ if (!menuOpen) return;
577
+ const closeOutside = (event) => {
578
+ if (!menu.current?.contains(event.target)) setMenuOpen(false);
579
+ };
580
+ document.addEventListener("pointerdown", closeOutside, true);
581
+ return () => document.removeEventListener("pointerdown", closeOutside, true);
582
+ }, [menuOpen]);
583
+ const run = async (operation) => {
584
+ setPending(true);
585
+ try {
586
+ await operation();
587
+ } finally {
588
+ setPending(false);
589
+ setMenuOpen(false);
590
+ }
591
+ };
592
+ const attach = (session) => run(() => onAttach(session.id, session.owned ? "control" : "observe"));
593
+ const activate = (sessionId) => {
594
+ setActiveSessionId(sessionId);
595
+ setTabMetadata((current) => current[sessionId]?.attention ? { ...current, [sessionId]: { ...current[sessionId], attention: null } } : current);
596
+ };
597
+ const markAttention = (sessionId, attention) => setTabMetadata((current) => {
598
+ const existing = current[sessionId] ?? {};
599
+ if (existing.attention === "bell" || existing.attention === attention) return current;
600
+ return { ...current, [sessionId]: { ...existing, attention } };
601
+ });
602
+ const closeTab = (sessionId) => {
603
+ const index = tabs.findIndex((tab) => tab.sessionId === sessionId);
604
+ const next = tabs.filter((tab) => tab.sessionId !== sessionId);
605
+ setTabs(next);
606
+ if (activeSessionId === sessionId) {
607
+ setActiveSessionId(next[Math.min(index, next.length - 1)]?.sessionId ?? null);
608
+ }
609
+ };
610
+ const activeSession = activeSessionId ? sessionById.get(activeSessionId) : null;
611
+ const activeTab = activeSessionId ? tabs.find((tab) => tab.sessionId === activeSessionId) : null;
612
+ return /* @__PURE__ */ jsxs(
613
+ "section",
614
+ {
615
+ class: `sctui-workspace ${className}`.trim(),
616
+ "aria-label": labels.title,
617
+ onKeyDownCapture: (event) => {
618
+ if (event.key === "Escape" && menuOpen) {
619
+ event.preventDefault();
620
+ setMenuOpen(false);
621
+ menu.current?.querySelector(".sctui-session-trigger")?.focus();
622
+ return;
623
+ }
624
+ const shortcut = terminalWorkspaceShortcut(event);
625
+ if (!shortcut) return;
626
+ if ((shortcut.action === "previous" || shortcut.action === "next") && tabs.length) {
627
+ event.preventDefault();
628
+ const current = Math.max(0, tabs.findIndex((tab) => tab.sessionId === activeSessionId));
629
+ const step = shortcut.action === "next" ? 1 : -1;
630
+ activate(tabs[(current + step + tabs.length) % tabs.length].sessionId);
631
+ return;
632
+ }
633
+ if (shortcut.action === "tab") {
634
+ const tab = tabs[shortcut.index];
635
+ if (tab) {
636
+ event.preventDefault();
637
+ activate(tab.sessionId);
638
+ }
639
+ return;
640
+ }
641
+ if (shortcut.action === "detach" && activeSessionId) {
642
+ event.preventDefault();
643
+ closeTab(activeSessionId);
644
+ return;
645
+ }
646
+ if (shortcut.action === "new") {
647
+ event.preventDefault();
648
+ setMenuOpen(true);
649
+ queueMicrotask(() => menu.current?.querySelector('[role="menuitem"]')?.focus());
650
+ }
651
+ },
652
+ children: [
653
+ /* @__PURE__ */ jsxs("nav", { class: "sctui-tabbar", "aria-label": "Terminal tabs", children: [
654
+ /* @__PURE__ */ jsx("button", { class: "sctui-back", type: "button", "aria-label": labels.back, onClick: onClose, children: /* @__PURE__ */ jsx(BackGlyph, {}) }),
655
+ /* @__PURE__ */ jsx("div", { class: "sctui-tabs", role: "tablist", children: tabs.map((tab, index) => {
656
+ const session = sessionById.get(tab.sessionId);
657
+ const active = tab.sessionId === activeSessionId;
658
+ const metadata = tabMetadata[tab.sessionId] ?? {};
659
+ const status = tabStatuses[tab.sessionId] ?? "Loading terminal\u2026";
660
+ const title = metadata.title || session?.label || labels.terminal;
661
+ return /* @__PURE__ */ jsxs("div", { class: "sctui-tab", "data-active": String(active), children: [
662
+ /* @__PURE__ */ jsxs(
663
+ "button",
664
+ {
665
+ type: "button",
666
+ role: "tab",
667
+ "aria-keyshortcuts": `Alt+Shift+${index + 1}`,
668
+ "aria-selected": active,
669
+ title: `${session?.cwd ?? title} \xB7 ${terminalShortcutLabel(String(index + 1))}`,
670
+ onClick: () => activate(tab.sessionId),
671
+ children: [
672
+ /* @__PURE__ */ jsx(TerminalGlyph, {}),
673
+ /* @__PURE__ */ jsx("span", { children: title }),
674
+ /* @__PURE__ */ jsx("i", { "aria-label": `${status}${metadata.attention ? ` \xB7 ${metadata.attention}` : ""}`, "data-attention": metadata.attention ?? "none", "data-live": String(status.startsWith("Live")) })
675
+ ]
676
+ }
677
+ ),
678
+ /* @__PURE__ */ jsx(
679
+ "button",
680
+ {
681
+ type: "button",
682
+ "aria-keyshortcuts": active ? "Alt+Shift+D" : void 0,
683
+ "aria-label": `${labels.closeTab}: ${session?.label ?? labels.terminal}`,
684
+ title: active ? `${labels.closeTab} (${terminalShortcutLabel("D")})` : labels.closeTab,
685
+ onClick: () => closeTab(tab.sessionId),
686
+ children: /* @__PURE__ */ jsx(CloseGlyph, {})
687
+ }
688
+ )
689
+ ] }, tab.sessionId);
690
+ }) }),
691
+ /* @__PURE__ */ jsxs("div", { class: "sctui-session-menu", ref: menu, children: [
692
+ /* @__PURE__ */ jsx(
693
+ "button",
694
+ {
695
+ class: "sctui-session-trigger",
696
+ type: "button",
697
+ "aria-expanded": menuOpen,
698
+ "aria-haspopup": "menu",
699
+ "aria-keyshortcuts": "Alt+Shift+T",
700
+ "aria-label": labels.newTerminal,
701
+ title: `${labels.newTerminal} (${terminalShortcutLabel("T")})`,
702
+ onClick: () => setMenuOpen((open) => !open),
703
+ children: /* @__PURE__ */ jsx(PlusGlyph, {})
704
+ }
705
+ ),
706
+ menuOpen ? /* @__PURE__ */ jsxs("div", { role: "menu", "aria-label": labels.newTerminal, children: [
707
+ createActions.length ? /* @__PURE__ */ jsx("strong", { children: labels.newTerminal }) : null,
708
+ createActions.map((action) => /* @__PURE__ */ jsxs("button", { role: "menuitem", type: "button", disabled: pending || !state.available || !onCreate, onClick: () => run(() => onCreate?.(action.id)), children: [
709
+ action.icon ? /* @__PURE__ */ jsx("span", { class: "sctui-action-icon", children: action.icon }) : /* @__PURE__ */ jsx(TerminalGlyph, {}),
710
+ /* @__PURE__ */ jsx("span", { children: action.label })
711
+ ] }, action.id)),
712
+ /* @__PURE__ */ jsxs("span", { class: "sctui-menu-heading", children: [
713
+ /* @__PURE__ */ jsx("strong", { children: labels.sessions }),
714
+ /* @__PURE__ */ jsx("button", { role: "menuitem", type: "button", disabled: pending, onClick: () => run(onRefresh), children: labels.refresh })
715
+ ] }),
716
+ state.sessions.map((session) => /* @__PURE__ */ jsxs("button", { role: "menuitem", type: "button", disabled: pending, onClick: () => attach(session), children: [
717
+ /* @__PURE__ */ jsx(TerminalGlyph, {}),
718
+ /* @__PURE__ */ jsxs("span", { children: [
719
+ /* @__PURE__ */ jsx("b", { children: session.label }),
720
+ /* @__PURE__ */ jsx(TerminalPath, { value: session.cwd ?? session.activeCommand ?? labels.session })
721
+ ] }),
722
+ /* @__PURE__ */ jsx("small", { children: session.owned ? labels.control : labels.view })
723
+ ] }, session.id))
724
+ ] }) : null
725
+ ] }),
726
+ activeTab?.attachment.mode === "control" && onViewportModeChange ? /* @__PURE__ */ jsx(
727
+ "button",
728
+ {
729
+ class: "sctui-viewport-action",
730
+ type: "button",
731
+ "aria-label": labels.fitViewport,
732
+ "aria-pressed": viewportMode === "fit",
733
+ title: viewportMode === "scale" ? labels.fitViewport : labels.scaleViewport,
734
+ onClick: () => onViewportModeChange(viewportMode === "scale" ? "fit" : "scale"),
735
+ children: /* @__PURE__ */ jsx(ViewportGlyph, { mode: viewportMode })
736
+ }
737
+ ) : null,
738
+ activeSession?.owned && state.canOpenLocal && onOpen ? /* @__PURE__ */ jsx("button", { class: "sctui-toolbar-action", type: "button", onClick: () => run(() => onOpen(activeSession.id)), children: labels.open }) : null,
739
+ activeSession?.owned ? /* @__PURE__ */ jsx("button", { class: "sctui-toolbar-action sctui-danger", type: "button", onClick: () => run(async () => {
740
+ await onStop(activeSession.id);
741
+ closeTab(activeSession.id);
742
+ }), children: labels.stop }) : null
743
+ ] }),
744
+ state.error ? /* @__PURE__ */ jsx("div", { class: "sctui-error", role: "alert", children: state.error }) : null,
745
+ /* @__PURE__ */ jsxs("div", { class: "sctui-terminals", children: [
746
+ tabs.map((tab) => {
747
+ const active = tab.sessionId === activeSessionId;
748
+ const viewer = renderViewer ? renderViewer(tab.attachment, {
749
+ active,
750
+ onActivity: () => {
751
+ if (!active) markAttention(tab.sessionId, "activity");
752
+ },
753
+ onBell: () => {
754
+ if (!active) markAttention(tab.sessionId, "bell");
755
+ },
756
+ onStatus: (status) => setTabStatuses((current) => current[tab.sessionId] === status ? current : { ...current, [tab.sessionId]: status }),
757
+ onTitle: (title) => setTabMetadata((current) => current[tab.sessionId]?.title === title ? current : { ...current, [tab.sessionId]: { ...current[tab.sessionId], title } }),
758
+ retry: () => {
759
+ const session = sessionById.get(tab.sessionId);
760
+ if (session) void attach(session);
761
+ }
762
+ }) : /* @__PURE__ */ jsx(
763
+ TerminalViewer,
764
+ {
765
+ active,
766
+ attachment: tab.attachment,
767
+ onActivity: () => {
768
+ if (!active) markAttention(tab.sessionId, "activity");
769
+ },
770
+ onBell: () => {
771
+ if (!active) markAttention(tab.sessionId, "bell");
772
+ },
773
+ onRetry: () => {
774
+ const session = sessionById.get(tab.sessionId);
775
+ if (session) void attach(session);
776
+ },
777
+ onStatus: (status) => setTabStatuses((current) => current[tab.sessionId] === status ? current : { ...current, [tab.sessionId]: status }),
778
+ onTitle: (title) => setTabMetadata((current) => current[tab.sessionId]?.title === title ? current : { ...current, [tab.sessionId]: { ...current[tab.sessionId], title } })
779
+ }
780
+ );
781
+ return /* @__PURE__ */ jsx("div", { class: "sctui-terminal", "data-active": String(active), children: viewer }, tab.sessionId);
782
+ }),
783
+ !tabs.length ? /* @__PURE__ */ jsxs("div", { class: "sctui-terminal-empty", children: [
784
+ /* @__PURE__ */ jsx(TerminalGlyph, {}),
785
+ /* @__PURE__ */ jsx("strong", { children: labels.noTabs }),
786
+ /* @__PURE__ */ jsx("button", { type: "button", onClick: () => setMenuOpen(true), children: labels.attach })
787
+ ] }) : null
788
+ ] })
789
+ ]
790
+ }
791
+ );
792
+ }
793
+ function TerminalPanel({
794
+ className = "",
795
+ createActions = [],
796
+ labels: labelOverrides,
797
+ onAttach,
798
+ onClose,
799
+ onCreate,
800
+ onDismiss,
801
+ onOpen,
802
+ onRefresh,
803
+ onStop,
804
+ renderViewer,
805
+ state
806
+ }) {
807
+ const [pending, setPending] = useState(false);
808
+ const leadingButton = useRef(null);
809
+ const panel = useRef(null);
810
+ const labels = { ...DEFAULT_LABELS, ...labelOverrides };
811
+ const attached = state.attachment !== null;
812
+ useEffect(() => leadingButton.current?.focus({ preventScroll: true }), []);
813
+ const run = (operation) => {
814
+ setPending(true);
815
+ void Promise.resolve().then(operation).finally(() => setPending(false));
816
+ };
817
+ return /* @__PURE__ */ jsxs(
818
+ "section",
819
+ {
820
+ ref: panel,
821
+ class: `sctui-panel ${className}`.trim(),
822
+ "data-attached": String(attached),
823
+ "aria-label": labels.title,
824
+ "aria-modal": "true",
825
+ role: "dialog",
826
+ onKeyDown: (event) => {
827
+ if (event.key === "Escape" && !state.attachment) {
828
+ event.preventDefault();
829
+ event.stopPropagation();
830
+ onClose();
831
+ return;
832
+ }
833
+ if (event.key !== "Tab") return;
834
+ const focusable = [...panel.current?.querySelectorAll('button:not(:disabled), [href], input:not(:disabled), textarea:not(:disabled), [tabindex]:not([tabindex="-1"])') ?? []];
835
+ const first = focusable.at(0);
836
+ const last = focusable.at(-1);
837
+ if (!first || !last) return;
838
+ if (event.shiftKey && document.activeElement === first) {
839
+ event.preventDefault();
840
+ last.focus();
841
+ } else if (!event.shiftKey && document.activeElement === last) {
842
+ event.preventDefault();
843
+ first.focus();
844
+ }
845
+ },
846
+ children: [
847
+ /* @__PURE__ */ jsxs("header", { children: [
848
+ /* @__PURE__ */ jsx(
849
+ "button",
850
+ {
851
+ ref: leadingButton,
852
+ type: "button",
853
+ "aria-label": attached ? labels.back : `Close ${labels.title.toLowerCase()}`,
854
+ onClick: attached ? () => run(onDismiss) : onClose,
855
+ children: "\u2190"
856
+ }
857
+ ),
858
+ /* @__PURE__ */ jsxs("span", { children: [
859
+ /* @__PURE__ */ jsx("strong", { children: labels.title }),
860
+ /* @__PURE__ */ jsx("small", { children: labels.subtitle })
861
+ ] }),
862
+ attached ? /* @__PURE__ */ jsx("button", { type: "button", "aria-label": `Close ${labels.title.toLowerCase()}`, onClick: onClose, children: "\xD7" }) : /* @__PURE__ */ jsx("button", { type: "button", disabled: pending, onClick: () => run(onRefresh), children: labels.refresh })
863
+ ] }),
864
+ attached ? /* @__PURE__ */ jsx("div", { class: "sctui-live", children: renderViewer ? renderViewer(state.attachment) : /* @__PURE__ */ jsx(TerminalViewer, { attachment: state.attachment }) }) : /* @__PURE__ */ jsxs(Fragment, { children: [
865
+ createActions.length ? /* @__PURE__ */ jsx("div", { class: "sctui-create", children: createActions.map((action) => /* @__PURE__ */ jsx("button", { type: "button", disabled: pending || !state.available || !onCreate, onClick: () => run(() => onCreate?.(action.id)), children: action.label }, action.id)) }) : null,
866
+ state.error ? /* @__PURE__ */ jsx("div", { class: "sctui-error", role: "alert", children: state.error }) : null,
867
+ /* @__PURE__ */ jsx("div", { class: "sctui-list", children: state.sessions.map((session) => /* @__PURE__ */ jsxs("article", { children: [
868
+ /* @__PURE__ */ jsxs("span", { children: [
869
+ /* @__PURE__ */ jsx("strong", { children: session.label }),
870
+ /* @__PURE__ */ jsx(TerminalPath, { value: session.cwd ?? session.activeCommand ?? labels.session })
871
+ ] }),
872
+ /* @__PURE__ */ jsx("button", { type: "button", disabled: pending, onClick: () => run(() => onAttach(session.id, "observe")), children: labels.view }),
873
+ session.owned ? /* @__PURE__ */ jsx("button", { type: "button", disabled: pending, onClick: () => run(() => onAttach(session.id, "control")), children: labels.control }) : null,
874
+ session.owned && state.canOpenLocal && onOpen ? /* @__PURE__ */ jsx("button", { type: "button", disabled: pending, onClick: () => run(() => onOpen(session.id)), children: labels.open }) : null,
875
+ session.owned ? /* @__PURE__ */ jsx("button", { type: "button", class: "sctui-danger", disabled: pending, onClick: () => run(() => onStop(session.id)), children: labels.stop }) : null
876
+ ] }, session.id)) }),
877
+ !state.sessions.length && state.available ? /* @__PURE__ */ jsx("p", { class: "sctui-empty", children: labels.empty }) : null
878
+ ] })
879
+ ]
880
+ }
881
+ );
882
+ }
883
+ export {
884
+ TerminalPanel,
885
+ TerminalPath,
886
+ TerminalStreamViewer,
887
+ TerminalViewer,
888
+ TerminalWorkspace,
889
+ normalizeTerminalUiState,
890
+ terminalPath,
891
+ terminalWorkspaceShortcut
892
+ };