@a-t-h-i/bot-lobby 0.6.49 → 0.6.50

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 (52) hide show
  1. package/README.md +2 -2
  2. package/THIRD_PARTY_NOTICES.md +4 -3
  3. package/package.json +9 -7
  4. package/src/classifier/classifier.ts +14 -2
  5. package/src/classifier/hosts.ts +12 -18
  6. package/src/classifier/instance.ts +7 -4
  7. package/src/execution/delegation.ts +2 -1
  8. package/src/execution/pi-runner.ts +11 -2
  9. package/src/lobby/host.ts +6 -0
  10. package/src/lobby/keys.ts +2 -1
  11. package/src/lobby/service.ts +71 -4
  12. package/src/lobby/sessions.ts +35 -0
  13. package/src/pi/auto-compaction.ts +46 -19
  14. package/src/pi/events.ts +2 -0
  15. package/src/pi/fresh-context.ts +10 -8
  16. package/src/pi/plan-checklist.ts +47 -15
  17. package/src/schemas/task.ts +14 -0
  18. package/src/state/persistence.ts +4 -1
  19. package/src/webui/api/index.ts +36 -2
  20. package/src/webui/api/lobby.ts +1 -1
  21. package/src/webui/api/plan-facts.ts +9 -4
  22. package/src/webui/api/sessions.ts +21 -0
  23. package/src/webui/api/tasks.ts +12 -1
  24. package/src/webui/api/terminal.ts +42 -0
  25. package/src/webui/dev/fake-service.ts +42 -1
  26. package/src/webui/protocol.ts +29 -2
  27. package/src/webui/server.ts +16 -1
  28. package/src/webui/terminal.ts +292 -0
  29. package/src/workflow/workflow.ts +3 -2
  30. package/webui/dist/assets/TerminalView-BrP-ENHg.css +1 -0
  31. package/webui/dist/assets/TerminalView-D_j85u4O.js +16 -0
  32. package/webui/dist/assets/dm-sans-latin-wght-normal-Xz1IZZA0.woff2 +0 -0
  33. package/webui/dist/assets/figtree-latin-wght-normal-D_ZTVpCC.woff2 +0 -0
  34. package/webui/dist/assets/fira-code-latin-wght-normal-CHoedHDv.woff2 +0 -0
  35. package/webui/dist/assets/geist-latin-wght-normal-BgDaEnEv.woff2 +0 -0
  36. package/webui/dist/assets/geist-mono-latin-wght-normal-XN7g48iV.woff2 +0 -0
  37. package/webui/dist/assets/ibm-plex-sans-latin-wght-normal-IvpUvPa2.woff2 +0 -0
  38. package/webui/dist/assets/index-BkM--P3x.js +115 -0
  39. package/webui/dist/assets/index-DrHXotS7.css +2 -0
  40. package/webui/dist/assets/jetbrains-mono-latin-wght-normal-B9CIFXIH.woff2 +0 -0
  41. package/webui/dist/assets/manrope-latin-wght-normal-DHIcAJRg.woff2 +0 -0
  42. package/webui/dist/assets/nunito-latin-wght-normal-BzFMHfZw.woff2 +0 -0
  43. package/webui/dist/assets/outfit-latin-wght-normal-Bc-8i84L.woff2 +0 -0
  44. package/webui/dist/assets/plus-jakarta-sans-latin-wght-normal-eXO_dkmS.woff2 +0 -0
  45. package/webui/dist/assets/roboto-mono-latin-wght-normal-CZtBPCCa.woff2 +0 -0
  46. package/webui/dist/assets/source-code-pro-latin-wght-normal-BP8Zz55n.woff2 +0 -0
  47. package/webui/dist/assets/source-sans-3-latin-wght-normal-Bo_j9asI.woff2 +0 -0
  48. package/webui/dist/assets/space-grotesk-latin-wght-normal-BhU9QXUp.woff2 +0 -0
  49. package/webui/dist/build.json +108 -92
  50. package/webui/dist/index.html +2 -2
  51. package/webui/dist/assets/index-C3p3zxoV.js +0 -114
  52. package/webui/dist/assets/index-DE9jHz-C.css +0 -2
@@ -0,0 +1,292 @@
1
+ /**
2
+ * Terminals in the web lobby: each one a real shell in the project's folder,
3
+ * on a pseudo-terminal so prompts, colours and full-screen programs work.
4
+ * The shell keeps running while no page watches it (a reload reattaches and
5
+ * replays what it printed), and ends when it exits, when the page closes it,
6
+ * or when the server stops.
7
+ *
8
+ * The pseudo-terminal comes from `node-pty` when it is installed (it is not a
9
+ * dependency: it needs a native build), and otherwise from a few lines of
10
+ * Python's standard `pty` module, which macOS and Linux ship. With neither
11
+ * (Windows without node-pty, or no python3) the terminal says why it is off.
12
+ *
13
+ * The page writes with `terminal.input` / `terminal.resize` and reads
14
+ * `GET /api/terminal.stream?id=…`: a `reset` with the scrollback first, then
15
+ * `data` as the shell prints and `exit` when it ends.
16
+ */
17
+ import { spawn, spawnSync, type ChildProcess } from "node:child_process";
18
+ import { basename } from "node:path";
19
+ import { StringDecoder } from "node:string_decoder";
20
+ import type { IncomingMessage, ServerResponse } from "node:http";
21
+ import type { TerminalInfo, TerminalStreamEvent } from "./protocol.ts";
22
+
23
+ /** Terminals open at once; one more is refused. */
24
+ export const MAX_TERMINALS = 8;
25
+ /** What a terminal keeps of its output for a page that attaches later, in characters. */
26
+ export const SCROLLBACK_CHARS = 256 * 1024;
27
+ /** Largest single write from the page. */
28
+ export const MAX_INPUT_CHARS = 64 * 1024;
29
+ const HEARTBEAT_MS = 15_000;
30
+
31
+ /**
32
+ * The Python side: fork a shell on a new pseudo-terminal, copy stdin to it and
33
+ * it to stdout, and read `cols rows` lines on fd 3 to resize it (the kernel
34
+ * then tells the shell). When stdin closes the terminal is hung up; the exit
35
+ * code is the shell's.
36
+ */
37
+ const PY_PTY = `
38
+ import os, sys, pty, select, struct, fcntl, termios, signal
39
+ cols, rows = int(sys.argv[1]), int(sys.argv[2])
40
+ argv = sys.argv[3:]
41
+ def size(fd, c, r):
42
+ fcntl.ioctl(fd, termios.TIOCSWINSZ, struct.pack("HHHH", r, c, 0, 0))
43
+ pid, fd = pty.fork()
44
+ if pid == 0:
45
+ size(0, cols, rows)
46
+ os.execvp(argv[0], argv)
47
+ def write_all(target, data):
48
+ while data:
49
+ try:
50
+ data = data[os.write(target, data):]
51
+ except InterruptedError:
52
+ pass
53
+ inputs, control = [0, fd, 3], b""
54
+ while True:
55
+ try:
56
+ ready = select.select(inputs, [], [])[0]
57
+ except InterruptedError:
58
+ continue
59
+ if fd in ready:
60
+ try:
61
+ data = os.read(fd, 65536)
62
+ except OSError:
63
+ data = b""
64
+ if not data:
65
+ break
66
+ write_all(1, data)
67
+ if 0 in ready:
68
+ data = os.read(0, 65536)
69
+ if not data:
70
+ os.close(fd)
71
+ try:
72
+ os.killpg(pid, signal.SIGHUP)
73
+ except OSError:
74
+ pass
75
+ break
76
+ write_all(fd, data)
77
+ if 3 in ready:
78
+ data = os.read(3, 1024)
79
+ if not data:
80
+ inputs.remove(3)
81
+ control += data
82
+ while b"\\n" in control:
83
+ line, control = control.split(b"\\n", 1)
84
+ try:
85
+ c, r = (int(part) for part in line.split())
86
+ size(fd, c, r)
87
+ except (ValueError, OSError):
88
+ pass
89
+ status = os.waitpid(pid, 0)[1]
90
+ sys.exit(os.waitstatus_to_exitcode(status) if hasattr(os, "waitstatus_to_exitcode") else status >> 8)
91
+ `;
92
+
93
+ /** A pseudo-terminal with a shell on it, whichever way it was made. */
94
+ interface Pty {
95
+ write(data: string): void;
96
+ resize(cols: number, rows: number): void;
97
+ kill(): void;
98
+ }
99
+
100
+ interface PtyHandlers {
101
+ onData: (text: string) => void;
102
+ onExit: (code: number | undefined) => void;
103
+ }
104
+
105
+ interface NodePtyModule {
106
+ spawn(file: string, args: string[], options: { name: string; cols: number; rows: number; cwd: string; env: NodeJS.ProcessEnv }): {
107
+ onData(listener: (data: string) => void): void;
108
+ onExit(listener: (event: { exitCode: number }) => void): void;
109
+ write(data: string): void;
110
+ resize(cols: number, rows: number): void;
111
+ kill(): void;
112
+ };
113
+ }
114
+
115
+ type Backend = { kind: "node-pty"; module: NodePtyModule } | { kind: "python" } | { kind: "none"; reason: string };
116
+
117
+ let backend: Promise<Backend> | undefined;
118
+
119
+ /** How this machine gets a pseudo-terminal, found once. */
120
+ export function terminalBackend(): Promise<Backend> {
121
+ backend ??= (async (): Promise<Backend> => {
122
+ // Named through a variable so the type checker and bundlers leave the optional module alone.
123
+ const optional = "node-pty";
124
+ try {
125
+ const loaded = (await import(optional)) as NodePtyModule & { default?: NodePtyModule };
126
+ const module = typeof loaded.spawn === "function" ? loaded : loaded.default;
127
+ if (module && typeof module.spawn === "function") return { kind: "node-pty", module };
128
+ } catch {
129
+ // Not installed: fall back to Python's pty.
130
+ }
131
+ if (process.platform === "win32") return { kind: "none", reason: "The terminal needs node-pty on Windows: run `npm install -g node-pty` next to bot-lobby." };
132
+ const probe = spawnSync("python3", ["-c", "import pty, termios, fcntl"], { stdio: "ignore", timeout: 5_000 });
133
+ if (probe.status === 0) return { kind: "python" };
134
+ return { kind: "none", reason: "The terminal needs python3 (or node-pty) on this machine." };
135
+ })();
136
+ return backend;
137
+ }
138
+
139
+ /** The user's shell and its arguments: a login shell where the shell knows the flag. */
140
+ export function shellCommand(env: NodeJS.ProcessEnv = process.env, platform: NodeJS.Platform = process.platform): string[] {
141
+ if (platform === "win32") return [env.COMSPEC || "powershell.exe"];
142
+ const shell = env.SHELL?.trim() || "/bin/bash";
143
+ return ["bash", "zsh", "fish", "ksh"].includes(basename(shell)) ? [shell, "-l"] : [shell];
144
+ }
145
+
146
+ function terminalEnv(): NodeJS.ProcessEnv {
147
+ return { ...process.env, TERM: "xterm-256color", COLORTERM: "truecolor", TERM_PROGRAM: "bot-lobby" };
148
+ }
149
+
150
+ function spawnPython(cwd: string, cols: number, rows: number, handlers: PtyHandlers): Pty {
151
+ const child: ChildProcess = spawn("python3", ["-c", PY_PTY, String(cols), String(rows), ...shellCommand()], {
152
+ cwd,
153
+ env: terminalEnv(),
154
+ stdio: ["pipe", "pipe", "pipe", "pipe"],
155
+ });
156
+ const decoder = new StringDecoder("utf8");
157
+ child.stdout!.on("data", (chunk: Buffer) => handlers.onData(decoder.write(chunk)));
158
+ child.stderr!.on("data", (chunk: Buffer) => handlers.onData(decoder.write(chunk)));
159
+ const control = child.stdio[3] as NodeJS.WritableStream;
160
+ for (const stream of [child.stdin!, control]) stream.on("error", () => {});
161
+ child.on("error", () => handlers.onExit(undefined));
162
+ child.on("exit", (code) => handlers.onExit(code ?? undefined));
163
+ return {
164
+ write: (data) => { if (child.stdin!.writable) child.stdin!.write(data); },
165
+ resize: (c, r) => { if (control.writable) control.write(`${c} ${r}\n`); },
166
+ kill: () => { child.stdin!.end(); setTimeout(() => { if (child.exitCode === null) child.kill("SIGKILL"); }, 2_000).unref(); },
167
+ };
168
+ }
169
+
170
+ function spawnNodePty(module: NodePtyModule, cwd: string, cols: number, rows: number, handlers: PtyHandlers): Pty {
171
+ const [file, ...args] = shellCommand();
172
+ const pty = module.spawn(file!, args, { name: "xterm-256color", cols, rows, cwd, env: terminalEnv() });
173
+ pty.onData(handlers.onData);
174
+ pty.onExit((event) => handlers.onExit(event.exitCode));
175
+ return { write: (data) => pty.write(data), resize: (c, r) => pty.resize(c, r), kill: () => pty.kill() };
176
+ }
177
+
178
+ interface Terminal {
179
+ info: TerminalInfo;
180
+ pty: Pty;
181
+ scrollback: string;
182
+ watchers: Set<ServerResponse>;
183
+ }
184
+
185
+ function frame(event: TerminalStreamEvent): string {
186
+ return `data: ${JSON.stringify(event)}\n\n`;
187
+ }
188
+
189
+ const clampSize = (value: number, max: number) => Math.max(2, Math.min(max, Math.floor(value)));
190
+
191
+ /** Every terminal this server runs. */
192
+ export class TerminalHost {
193
+ private terminals = new Map<string, Terminal>();
194
+ private count = 0;
195
+
196
+ list(): TerminalInfo[] {
197
+ return [...this.terminals.values()].map((terminal) => ({ ...terminal.info }));
198
+ }
199
+
200
+ /** Open a terminal in `cwd`; refused when the machine has no way to make one, or too many are open. */
201
+ async open(cwd: string, cols: number, rows: number): Promise<TerminalInfo> {
202
+ const found = await terminalBackend();
203
+ if (found.kind === "none") throw new TerminalError(found.reason);
204
+ if (this.terminals.size >= MAX_TERMINALS) throw new TerminalError(`at most ${MAX_TERMINALS} terminals; close one first`);
205
+ const id = `t${++this.count}`;
206
+ const shell = basename(shellCommand()[0]!);
207
+ const info: TerminalInfo = { id, title: `${shell} ${this.count}`, cwd, alive: true, startedAt: Date.now() };
208
+ const handlers: PtyHandlers = {
209
+ onData: (text) => {
210
+ if (!text) return;
211
+ const terminal = this.terminals.get(id);
212
+ if (!terminal) return;
213
+ terminal.scrollback = (terminal.scrollback + text).slice(-SCROLLBACK_CHARS);
214
+ this.send(terminal, { type: "data", data: text });
215
+ },
216
+ onExit: (code) => {
217
+ const terminal = this.terminals.get(id);
218
+ if (!terminal || !terminal.info.alive) return;
219
+ terminal.info.alive = false;
220
+ if (code !== undefined) terminal.info.exitCode = code;
221
+ this.send(terminal, { type: "exit", ...(code !== undefined ? { code } : {}) });
222
+ },
223
+ };
224
+ const c = clampSize(cols, 500);
225
+ const r = clampSize(rows, 300);
226
+ const pty = found.kind === "node-pty" ? spawnNodePty(found.module, cwd, c, r, handlers) : spawnPython(cwd, c, r, handlers);
227
+ this.terminals.set(id, { info, pty, scrollback: "", watchers: new Set() });
228
+ return { ...info };
229
+ }
230
+
231
+ input(id: string, data: string): void {
232
+ const terminal = this.need(id);
233
+ if (terminal.info.alive) terminal.pty.write(data);
234
+ }
235
+
236
+ resize(id: string, cols: number, rows: number): void {
237
+ const terminal = this.need(id);
238
+ if (terminal.info.alive) terminal.pty.resize(clampSize(cols, 500), clampSize(rows, 300));
239
+ }
240
+
241
+ /** End a terminal's shell (hung up, as when a terminal window closes) and forget it. */
242
+ close(id: string): void {
243
+ const terminal = this.need(id);
244
+ terminal.pty.kill();
245
+ for (const res of terminal.watchers) res.end();
246
+ this.terminals.delete(id);
247
+ }
248
+
249
+ /** `GET /api/terminal.stream?id=…`: the scrollback, then what the shell prints. */
250
+ stream(req: IncomingMessage, res: ServerResponse, id: string): boolean {
251
+ const terminal = this.terminals.get(id);
252
+ if (!terminal) return false;
253
+ res.writeHead(200, { "Content-Type": "text/event-stream; charset=utf-8", "Cache-Control": "no-store", Connection: "keep-alive", "X-Accel-Buffering": "no" });
254
+ res.write(`retry: 2000\n${frame({ type: "reset", data: terminal.scrollback, alive: terminal.info.alive, ...(terminal.info.exitCode !== undefined ? { code: terminal.info.exitCode } : {}) })}`);
255
+ terminal.watchers.add(res);
256
+ const beat = setInterval(() => res.write(":ping\n\n"), HEARTBEAT_MS);
257
+ beat.unref();
258
+ req.on("close", () => {
259
+ clearInterval(beat);
260
+ terminal.watchers.delete(res);
261
+ });
262
+ return true;
263
+ }
264
+
265
+ /** End every shell (the server is stopping). */
266
+ closeAll(): void {
267
+ for (const id of [...this.terminals.keys()]) this.close(id);
268
+ }
269
+
270
+ private send(terminal: Terminal, event: TerminalStreamEvent): void {
271
+ const text = frame(event);
272
+ for (const res of terminal.watchers) res.write(text);
273
+ }
274
+
275
+ private need(id: string): Terminal {
276
+ const terminal = this.terminals.get(id);
277
+ if (!terminal) throw new TerminalError(`no terminal ${id}`, true);
278
+ return terminal;
279
+ }
280
+ }
281
+
282
+ /** A terminal request that cannot be done: `missing` when the terminal is gone. */
283
+ export class TerminalError extends Error {
284
+ readonly missing: boolean;
285
+ constructor(message: string, missing = false) {
286
+ super(message);
287
+ this.missing = missing;
288
+ }
289
+ }
290
+
291
+ /** The server's terminals. */
292
+ export const terminals = new TerminalHost();
@@ -56,7 +56,7 @@ import { assertNoPendingApprovals, pendingApprovals, requestApproval, resolveApp
56
56
  import { withBlockingRequest } from "../state/blocking-requests.ts";
57
57
  import { pingApproval } from "../pi/notify.ts";
58
58
  import { describeRun, runLogEntry } from "../pi/run-summary.ts";
59
- import { planChecklist, planSteps, runSteps } from "../pi/plan-checklist.ts";
59
+ import { planChecklist, planSteps, runSteps, skippedStepsNote } from "../pi/plan-checklist.ts";
60
60
  import { STEP_LIMIT_MS, grantStepTime, reserveStepTime, stepTimeLeft } from "../state/step-limits.ts";
61
61
  import { noteWorkerRun } from "../state/work-time.ts";
62
62
  import { nextStates } from "./transitions.ts";
@@ -256,6 +256,7 @@ export function describeTask(task: Task): string {
256
256
  ? `Pending approvals: ${pendingApprovals(task).map((a) => `${a.id} ${a.kind} for ${a.domain}: ${truncate(a.detail, 120)}`).join("; ")}`
257
257
  : "",
258
258
  task.blockers.length > 0 ? `Blockers: ${task.blockers.map((b) => b.reason).join("; ")}` : "",
259
+ skippedStepsNote(task.plan, task.skippedSteps),
259
260
  `Next legal states: ${nextStates(task.state).join(", ")}`,
260
261
  ];
261
262
  return lines.filter((line) => line.length > 0).join("\n");
@@ -1858,7 +1859,7 @@ async function workerTimes(task: Task, deps: WorkflowDeps, assignments: readonly
1858
1859
  const now = budgetFor(task, deps);
1859
1860
  const slots = openSlots(task, assignments.map((entry) => entry.domain));
1860
1861
  const steps = planSteps(task.plan ?? "");
1861
- const completed = new Set(planChecklist(task.plan ?? "", (task.workerRuns ?? []).map((record) => ({ ...record, taskId: task.id, role: "worker" as const, output: "", attempts: 1 }))).flatMap((step, index) => step.status === "done" ? [index] : []));
1862
+ const completed = new Set(planChecklist(task.plan ?? "", (task.workerRuns ?? []).map((record) => ({ ...record, taskId: task.id, role: "worker" as const, output: "", attempts: 1 })), task.skippedSteps).flatMap((step, index) => step.status === "done" || step.status === "skipped" ? [index] : []));
1862
1863
  const prepared = [];
1863
1864
  for (const { domain, instruction, minutes } of assignments) {
1864
1865
  const covered = runSteps(steps, instruction, completed);
@@ -0,0 +1 @@
1
+ .xterm{cursor:text;-webkit-user-select:none;user-select:none;position:relative}.xterm.focus,.xterm:focus{outline:none}.xterm .xterm-helpers{z-index:5;position:absolute;top:0}.xterm .xterm-helper-textarea{opacity:0;z-index:-5;white-space:nowrap;resize:none;border:0;width:0;height:0;margin:0;padding:0;position:absolute;top:0;left:-9999em;overflow:hidden}.xterm .composition-view{color:#fff;white-space:nowrap;z-index:1;background:#000;display:none;position:absolute}.xterm .composition-view.active{display:block}.xterm .xterm-viewport{cursor:default;background-color:#000;position:absolute;inset:0;overflow-y:scroll}.xterm .xterm-screen{position:relative}.xterm .xterm-screen canvas{position:absolute;top:0;left:0}.xterm-char-measure-element{visibility:hidden;line-height:normal;display:inline-block;position:absolute;top:0;left:-9999em}.xterm.enable-mouse-events{cursor:default}.xterm.xterm-cursor-pointer,.xterm .xterm-cursor-pointer{cursor:pointer}.xterm.column-select.focus{cursor:crosshair}.xterm .xterm-accessibility:not(.debug),.xterm .xterm-message{z-index:10;color:#0000;pointer-events:none;position:absolute;inset:0}.xterm .xterm-accessibility-tree:not(.debug) ::selection{color:#0000}.xterm .xterm-accessibility-tree{-webkit-user-select:text;user-select:text;white-space:pre;font-family:monospace}.xterm .xterm-accessibility-tree>div{transform-origin:0;width:fit-content}.xterm .live-region{width:1px;height:1px;position:absolute;left:-9999px;overflow:hidden}.xterm-dim{opacity:1!important}.xterm-underline-1{text-decoration:underline}.xterm-underline-2{-webkit-text-decoration:underline double;text-decoration:underline double}.xterm-underline-3{-webkit-text-decoration:underline wavy;text-decoration:underline wavy}.xterm-underline-4{-webkit-text-decoration:underline dotted;text-decoration:underline dotted}.xterm-underline-5{-webkit-text-decoration:underline dashed;text-decoration:underline dashed}.xterm-overline{text-decoration:overline}.xterm-overline.xterm-underline-1{text-decoration:underline overline}.xterm-overline.xterm-underline-2{-webkit-text-decoration:overline double underline;text-decoration:overline double underline}.xterm-overline.xterm-underline-3{-webkit-text-decoration:overline wavy underline;text-decoration:overline wavy underline}.xterm-overline.xterm-underline-4{-webkit-text-decoration:overline dotted underline;text-decoration:overline dotted underline}.xterm-overline.xterm-underline-5{-webkit-text-decoration:overline dashed underline;text-decoration:overline dashed underline}.xterm-strikethrough{text-decoration:line-through}.xterm-screen .xterm-decoration-container .xterm-decoration{z-index:6;position:absolute}.xterm-screen .xterm-decoration-container .xterm-decoration.xterm-decoration-top-layer{z-index:7}.xterm-decoration-overview-ruler{z-index:8;pointer-events:none;position:absolute;top:0;right:0}.xterm-decoration-top{z-index:2;position:relative}.xterm .xterm-scrollable-element>.scrollbar{cursor:default}.xterm .xterm-scrollable-element>.scrollbar>.scra{cursor:pointer;font-size:11px!important}.xterm .xterm-scrollable-element>.visible{opacity:1;z-index:11;background:0 0;transition:opacity .1s linear}.xterm .xterm-scrollable-element>.invisible{opacity:0;pointer-events:none}.xterm .xterm-scrollable-element>.invisible.fade{transition:opacity .8s linear}.xterm .xterm-scrollable-element>.shadow{display:none;position:absolute}.xterm .xterm-scrollable-element>.shadow.top{width:100%;height:3px;box-shadow:var(--vscode-scrollbar-shadow,#000) 0 6px 6px -6px inset;display:block;top:0;left:3px}.xterm .xterm-scrollable-element>.shadow.left{width:3px;height:100%;box-shadow:var(--vscode-scrollbar-shadow,#000) 6px 0 6px -6px inset;display:block;top:3px;left:0}.xterm .xterm-scrollable-element>.shadow.top-left-corner{width:3px;height:3px;display:block;top:0;left:0}.xterm .xterm-scrollable-element>.shadow.top.left{box-shadow:var(--vscode-scrollbar-shadow,#000) 6px 0 6px -6px inset}