@kdonev/termscape 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 (139) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +288 -0
  3. package/dist/agents/env.d.ts +36 -0
  4. package/dist/agents/env.d.ts.map +1 -0
  5. package/dist/agents/env.js +61 -0
  6. package/dist/agents/env.js.map +1 -0
  7. package/dist/agents/profiles.d.ts +45 -0
  8. package/dist/agents/profiles.d.ts.map +1 -0
  9. package/dist/agents/profiles.js +123 -0
  10. package/dist/agents/profiles.js.map +1 -0
  11. package/dist/agents/resolve.d.ts +16 -0
  12. package/dist/agents/resolve.d.ts.map +1 -0
  13. package/dist/agents/resolve.js +71 -0
  14. package/dist/agents/resolve.js.map +1 -0
  15. package/dist/agents/router.d.ts +29 -0
  16. package/dist/agents/router.d.ts.map +1 -0
  17. package/dist/agents/router.js +83 -0
  18. package/dist/agents/router.js.map +1 -0
  19. package/dist/agents/tokens.d.ts +21 -0
  20. package/dist/agents/tokens.d.ts.map +1 -0
  21. package/dist/agents/tokens.js +39 -0
  22. package/dist/agents/tokens.js.map +1 -0
  23. package/dist/agents/wiring.d.ts +30 -0
  24. package/dist/agents/wiring.d.ts.map +1 -0
  25. package/dist/agents/wiring.js +110 -0
  26. package/dist/agents/wiring.js.map +1 -0
  27. package/dist/browser.d.ts +13 -0
  28. package/dist/browser.d.ts.map +1 -0
  29. package/dist/browser.js +34 -0
  30. package/dist/browser.js.map +1 -0
  31. package/dist/cli.d.ts +3 -0
  32. package/dist/cli.d.ts.map +1 -0
  33. package/dist/cli.js +200 -0
  34. package/dist/cli.js.map +1 -0
  35. package/dist/db/index.d.ts +14 -0
  36. package/dist/db/index.d.ts.map +1 -0
  37. package/dist/db/index.js +24 -0
  38. package/dist/db/index.js.map +1 -0
  39. package/dist/db/migrations.d.ts +15 -0
  40. package/dist/db/migrations.d.ts.map +1 -0
  41. package/dist/db/migrations.js +176 -0
  42. package/dist/db/migrations.js.map +1 -0
  43. package/dist/db/store.d.ts +71 -0
  44. package/dist/db/store.d.ts.map +1 -0
  45. package/dist/db/store.js +341 -0
  46. package/dist/db/store.js.map +1 -0
  47. package/dist/hub.d.ts +188 -0
  48. package/dist/hub.d.ts.map +1 -0
  49. package/dist/hub.js +636 -0
  50. package/dist/hub.js.map +1 -0
  51. package/dist/hub.tgz +0 -0
  52. package/dist/mcp/server.d.ts +31 -0
  53. package/dist/mcp/server.d.ts.map +1 -0
  54. package/dist/mcp/server.js +66 -0
  55. package/dist/mcp/server.js.map +1 -0
  56. package/dist/paths.d.ts +27 -0
  57. package/dist/paths.d.ts.map +1 -0
  58. package/dist/paths.js +31 -0
  59. package/dist/paths.js.map +1 -0
  60. package/dist/protocol/address.d.ts +15 -0
  61. package/dist/protocol/address.d.ts.map +1 -0
  62. package/dist/protocol/address.js +39 -0
  63. package/dist/protocol/address.js.map +1 -0
  64. package/dist/protocol/domain.d.ts +151 -0
  65. package/dist/protocol/domain.d.ts.map +1 -0
  66. package/dist/protocol/domain.js +104 -0
  67. package/dist/protocol/domain.js.map +1 -0
  68. package/dist/protocol/index.d.ts +7 -0
  69. package/dist/protocol/index.d.ts.map +1 -0
  70. package/dist/protocol/index.js +7 -0
  71. package/dist/protocol/index.js.map +1 -0
  72. package/dist/protocol/inject.d.ts +29 -0
  73. package/dist/protocol/inject.d.ts.map +1 -0
  74. package/dist/protocol/inject.js +40 -0
  75. package/dist/protocol/inject.js.map +1 -0
  76. package/dist/protocol/mcp-tools.d.ts +45 -0
  77. package/dist/protocol/mcp-tools.d.ts.map +1 -0
  78. package/dist/protocol/mcp-tools.js +46 -0
  79. package/dist/protocol/mcp-tools.js.map +1 -0
  80. package/dist/protocol/peer.d.ts +304 -0
  81. package/dist/protocol/peer.d.ts.map +1 -0
  82. package/dist/protocol/peer.js +183 -0
  83. package/dist/protocol/peer.js.map +1 -0
  84. package/dist/protocol/ws.d.ts +416 -0
  85. package/dist/protocol/ws.d.ts.map +1 -0
  86. package/dist/protocol/ws.js +119 -0
  87. package/dist/protocol/ws.js.map +1 -0
  88. package/dist/remote/deployer.d.ts +78 -0
  89. package/dist/remote/deployer.d.ts.map +1 -0
  90. package/dist/remote/deployer.js +216 -0
  91. package/dist/remote/deployer.js.map +1 -0
  92. package/dist/remote/enroll.d.ts +14 -0
  93. package/dist/remote/enroll.d.ts.map +1 -0
  94. package/dist/remote/enroll.js +244 -0
  95. package/dist/remote/enroll.js.map +1 -0
  96. package/dist/remote/join-script.d.ts +29 -0
  97. package/dist/remote/join-script.d.ts.map +1 -0
  98. package/dist/remote/join-script.js +709 -0
  99. package/dist/remote/join-script.js.map +1 -0
  100. package/dist/remote/join.d.ts +19 -0
  101. package/dist/remote/join.d.ts.map +1 -0
  102. package/dist/remote/join.js +195 -0
  103. package/dist/remote/join.js.map +1 -0
  104. package/dist/remote/lan.d.ts +45 -0
  105. package/dist/remote/lan.d.ts.map +1 -0
  106. package/dist/remote/lan.js +105 -0
  107. package/dist/remote/lan.js.map +1 -0
  108. package/dist/remote/peer-serve.d.ts +27 -0
  109. package/dist/remote/peer-serve.d.ts.map +1 -0
  110. package/dist/remote/peer-serve.js +274 -0
  111. package/dist/remote/peer-serve.js.map +1 -0
  112. package/dist/remote/peer.d.ts +62 -0
  113. package/dist/remote/peer.d.ts.map +1 -0
  114. package/dist/remote/peer.js +250 -0
  115. package/dist/remote/peer.js.map +1 -0
  116. package/dist/remote/registry.d.ts +104 -0
  117. package/dist/remote/registry.d.ts.map +1 -0
  118. package/dist/remote/registry.js +359 -0
  119. package/dist/remote/registry.js.map +1 -0
  120. package/dist/remote/tarball.d.ts +9 -0
  121. package/dist/remote/tarball.d.ts.map +1 -0
  122. package/dist/remote/tarball.js +22 -0
  123. package/dist/remote/tarball.js.map +1 -0
  124. package/dist/server.d.ts +54 -0
  125. package/dist/server.d.ts.map +1 -0
  126. package/dist/server.js +438 -0
  127. package/dist/server.js.map +1 -0
  128. package/dist/session/manager.d.ts +104 -0
  129. package/dist/session/manager.d.ts.map +1 -0
  130. package/dist/session/manager.js +421 -0
  131. package/dist/session/manager.js.map +1 -0
  132. package/dist/session/pty.d.ts +114 -0
  133. package/dist/session/pty.d.ts.map +1 -0
  134. package/dist/session/pty.js +300 -0
  135. package/dist/session/pty.js.map +1 -0
  136. package/package.json +49 -0
  137. package/web/assets/index-B2JtCtHa.css +1 -0
  138. package/web/assets/index-BOqZOoep.js +107 -0
  139. package/web/index.html +13 -0
@@ -0,0 +1,300 @@
1
+ import { EventEmitter } from 'node:events';
2
+ import { createRequire } from 'node:module';
3
+ import { basename } from 'node:path';
4
+ import { platform } from 'node:process';
5
+ import { buildAgentEnv } from '../agents/env.js';
6
+ /*
7
+ * node-pty, @xterm/headless and @xterm/addon-serialize are CommonJS, and their
8
+ * typings use `declare module` with named exports. That combination is a trap:
9
+ * `import { Terminal } from '@xterm/headless'` typechecks cleanly and then
10
+ * throws at runtime, because Node's CJS named-export detection cannot see
11
+ * through their bundled output. createRequire gets us the real exports while
12
+ * `typeof import(...)` keeps full type information.
13
+ */
14
+ const require = createRequire(import.meta.url);
15
+ const pty = require('node-pty');
16
+ const { Terminal } = require('@xterm/headless');
17
+ const { SerializeAddon } = require('@xterm/addon-serialize');
18
+ /** How much history the hub keeps in memory for live reattach. Never persisted. */
19
+ const SCROLLBACK_LINES = 2000;
20
+ /** Scrollback included when replaying to a browser that just attached. */
21
+ const ATTACH_SCROLLBACK = 1000;
22
+ /**
23
+ * How long a `heuristic` profile must be quiet before its last line is worth
24
+ * testing against the prompt pattern.
25
+ */
26
+ const HEURISTIC_QUIET_MS = 400;
27
+ /**
28
+ * How long a `hooks` profile whose hooks have never arrived must be quiet
29
+ * before it is called idle. Longer, because this path has no prompt pattern to
30
+ * check and only silence to go on - and an agent mid-turn is animating a
31
+ * spinner, which is not silence.
32
+ */
33
+ const HOOK_FALLBACK_QUIET_MS = 2000;
34
+ /**
35
+ * One PTY plus a headless xterm that mirrors it.
36
+ *
37
+ * The headless terminal earns its keep twice: it produces the serialized
38
+ * screen we persist to SQLite, and it is the in-memory scrollback that backs
39
+ * LOD detach/reattach. Feeding one parser instead of keeping a separate raw
40
+ * ring buffer means the replayed screen is always consistent with what the
41
+ * program actually drew, escape sequences and all.
42
+ */
43
+ export class PtySession extends EventEmitter {
44
+ id;
45
+ cols;
46
+ rows;
47
+ readyHint;
48
+ statusMode;
49
+ proc = null;
50
+ term;
51
+ serializer;
52
+ lastOutputAt = 0;
53
+ idleTimer = null;
54
+ _status = 'unknown';
55
+ /**
56
+ * Whether this session has ever reported a turn boundary itself. Until it
57
+ * has, a `hooks` profile is indistinguishable from one whose hooks cannot
58
+ * run at all, and the dot has to come from somewhere.
59
+ */
60
+ hooksSeen = false;
61
+ /** Last title this program set, to keep repeats off the wire. */
62
+ lastTitle = null;
63
+ /** argv[0], to recognise the one title the program did not choose. */
64
+ launchedFile = '';
65
+ disposed = false;
66
+ constructor(id, cols, rows, readyHint, statusMode) {
67
+ super();
68
+ this.id = id;
69
+ this.cols = cols;
70
+ this.rows = rows;
71
+ this.readyHint = readyHint;
72
+ this.statusMode = statusMode;
73
+ this.term = new Terminal({
74
+ cols,
75
+ rows,
76
+ scrollback: SCROLLBACK_LINES,
77
+ allowProposedApi: true,
78
+ });
79
+ this.serializer = new SerializeAddon();
80
+ this.term.loadAddon(this.serializer);
81
+ // OSC 0 and OSC 2, read off the mirror terminal rather than with a regex
82
+ // over the stream. The parser already handles a sequence split across two
83
+ // reads and either terminator, and nothing has to be stripped: the
84
+ // browser's own terminal is entitled to the same bytes.
85
+ this.term.onTitleChange((title) => {
86
+ const next = title.trim();
87
+ if (next === this.lastTitle || this.isLaunchNoise(next))
88
+ return;
89
+ this.lastTitle = next;
90
+ this.emit('title', next);
91
+ });
92
+ }
93
+ /**
94
+ * ConPTY announces the child's image path as the terminal title the moment
95
+ * it starts, before the program has said anything at all. That is Windows
96
+ * talking, not the agent, and `C:...node.exe` in a window header is worse
97
+ * than the canvas address it would replace.
98
+ */
99
+ isLaunchNoise(title) {
100
+ if (!this.launchedFile)
101
+ return false;
102
+ const same = (a, b) => platform === 'win32' ? a.toLowerCase() === b.toLowerCase() : a === b;
103
+ return (same(title, this.launchedFile) || same(title, basename(this.launchedFile)));
104
+ }
105
+ get status() {
106
+ return this._status;
107
+ }
108
+ get running() {
109
+ return this.proc !== null;
110
+ }
111
+ get pid() {
112
+ return this.proc?.pid ?? null;
113
+ }
114
+ start(opts) {
115
+ if (this.proc)
116
+ throw new Error(`session ${this.id} already running`);
117
+ const [file, ...args] = opts.argv;
118
+ if (!file)
119
+ throw new Error('empty argv');
120
+ this.launchedFile = file;
121
+ this.cols = opts.cols;
122
+ this.rows = opts.rows;
123
+ this.term.resize(opts.cols, opts.rows);
124
+ this.proc = pty.spawn(file, args, {
125
+ name: 'xterm-256color',
126
+ cols: opts.cols,
127
+ rows: opts.rows,
128
+ cwd: opts.cwd,
129
+ // Strips the parent agent CLI's own session state; see agents/env.ts for
130
+ // why inheriting it wholesale breaks resume and leaks an IPC channel.
131
+ env: buildAgentEnv(process.env, opts.env),
132
+ // ConPTY is the only way to get a real PTY on Windows; node-pty falls
133
+ // back to winpty on older builds, which we do not support.
134
+ useConpty: platform === 'win32' ? true : undefined,
135
+ });
136
+ this.proc.onData((data) => this.ingest(data));
137
+ this.proc.onExit(({ exitCode, signal }) => {
138
+ this.proc = null;
139
+ this.clearIdleTimer();
140
+ this._status = 'unknown';
141
+ this.emit('exit', { exitCode, signal });
142
+ });
143
+ this.markBusy();
144
+ }
145
+ /** Feed output to subscribers and to the mirror terminal. */
146
+ ingest(data) {
147
+ if (this.disposed)
148
+ return;
149
+ this.lastOutputAt = Date.now();
150
+ this.term.write(data);
151
+ this.emit('data', data);
152
+ // A hooked session that has reported a boundary is left to its hooks.
153
+ // One that never has is watched like any other, or it would sit on
154
+ // whatever status it started with for the life of the process.
155
+ if (this.statusMode === 'heuristic' || !this.hooksSeen)
156
+ this.scheduleIdleCheck();
157
+ }
158
+ /**
159
+ * Idle detection from output alone. Drives the status chip only — never
160
+ * gates delivery, because delivery is immediate by design.
161
+ *
162
+ * Two callers, with different standards of proof. A `heuristic` profile has
163
+ * a prompt pattern, so a short quiet spell plus a matching last line is
164
+ * enough. A `hooks` profile only reaches here while its hooks have never
165
+ * arrived, and has nothing to match against, so it waits out a longer
166
+ * silence and takes that as the answer.
167
+ */
168
+ scheduleIdleCheck() {
169
+ this.markBusy();
170
+ this.clearIdleTimer();
171
+ const fallback = this.statusMode === 'hooks';
172
+ const quiet = fallback ? HOOK_FALLBACK_QUIET_MS : HEURISTIC_QUIET_MS;
173
+ this.idleTimer = setTimeout(() => {
174
+ if (Date.now() - this.lastOutputAt < quiet)
175
+ return;
176
+ if (fallback) {
177
+ // A hook that arrived while this was pending outranks it.
178
+ if (!this.hooksSeen)
179
+ this.setStatus('idle');
180
+ return;
181
+ }
182
+ const tail = this.lastNonEmptyLine();
183
+ const looksIdle = this.readyHint ? this.readyHint.test(tail) : true;
184
+ this.setStatus(looksIdle ? 'idle' : 'busy');
185
+ }, quiet + 100);
186
+ }
187
+ clearIdleTimer() {
188
+ if (this.idleTimer) {
189
+ clearTimeout(this.idleTimer);
190
+ this.idleTimer = null;
191
+ }
192
+ }
193
+ markBusy() {
194
+ this.setStatus('busy');
195
+ }
196
+ /**
197
+ * A turn boundary the agent reported itself. Exact, where reading the output
198
+ * is a guess — so the first one to arrive retires the fallback for good and
199
+ * this session's status is whatever its hooks say from then on.
200
+ */
201
+ noteHook(s) {
202
+ this.hooksSeen = true;
203
+ this.clearIdleTimer();
204
+ this.setStatus(s);
205
+ }
206
+ setStatus(s) {
207
+ if (this._status === s)
208
+ return;
209
+ this._status = s;
210
+ this.emit('status', s);
211
+ }
212
+ write(data) {
213
+ if (!this.proc)
214
+ throw new Error(`session ${this.id} is not running`);
215
+ this.proc.write(data);
216
+ }
217
+ resize(cols, rows) {
218
+ if (cols < 1 || rows < 1)
219
+ return;
220
+ this.cols = cols;
221
+ this.rows = rows;
222
+ this.term.resize(cols, rows);
223
+ // A dead session still tracks its geometry so the restored window is right.
224
+ this.proc?.resize(cols, rows);
225
+ }
226
+ /** Full-ish history, for a browser that just attached to this window. */
227
+ serializeForAttach() {
228
+ return this.serializer.serialize({ scrollback: ATTACH_SCROLLBACK });
229
+ }
230
+ /**
231
+ * Screen only. This is what goes in SQLite: a restart preserves the last
232
+ * screenful, deliberately not the scrollback above it.
233
+ */
234
+ serializeForPersist() {
235
+ return this.serializer.serialize({ scrollback: 0 });
236
+ }
237
+ /**
238
+ * The last line the program actually drew, wherever it is on the screen.
239
+ *
240
+ * Not the bottom row: a CLI sitting at its prompt after printing one banner
241
+ * has its prompt at the top and twenty-three blank rows under it, and
242
+ * testing the bottom row against a prompt pattern matched nothing, ever.
243
+ * That is what kept a freshly started window amber for the life of the
244
+ * process.
245
+ */
246
+ lastNonEmptyLine() {
247
+ const drawn = this.tailLines(this.rows);
248
+ return drawn.slice(drawn.lastIndexOf('\n') + 1).trim();
249
+ }
250
+ /**
251
+ * Trailing rendered lines, for the read_screen MCP tool.
252
+ *
253
+ * Rows the terminal wrapped are rejoined. A wrap is a property of how
254
+ * wide the window happens to be, not of the text, so breaking a line
255
+ * there hands the reader a word split down the middle at a column that
256
+ * depends on someone else's terminal size - and the reader here is an
257
+ * agent trying to make sense of what another agent is doing.
258
+ */
259
+ tailLines(n) {
260
+ const buf = this.term.buffer.active;
261
+ const end = buf.baseY + this.term.rows;
262
+ const start = Math.max(0, end - n);
263
+ const out = [];
264
+ for (let i = start; i < end; i++) {
265
+ const line = buf.getLine(i);
266
+ const text = line?.translateToString(true) ?? '';
267
+ if (line?.isWrapped && out.length > 0)
268
+ out[out.length - 1] += text;
269
+ else
270
+ out.push(text);
271
+ }
272
+ while (out.length > 0 && out[out.length - 1].trim() === '')
273
+ out.pop();
274
+ return out.join('\n');
275
+ }
276
+ /** Restore a persisted screen into the mirror so reattach shows it. */
277
+ restore(serialized) {
278
+ if (serialized)
279
+ this.term.write(serialized);
280
+ }
281
+ kill() {
282
+ this.clearIdleTimer();
283
+ if (!this.proc)
284
+ return;
285
+ try {
286
+ this.proc.kill();
287
+ }
288
+ catch {
289
+ // Already gone; onExit has fired or will not fire. Nothing to do.
290
+ }
291
+ this.proc = null;
292
+ }
293
+ dispose() {
294
+ this.disposed = true;
295
+ this.kill();
296
+ this.term.dispose();
297
+ this.removeAllListeners();
298
+ }
299
+ }
300
+ //# sourceMappingURL=pty.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pty.js","sourceRoot":"","sources":["../../src/session/pty.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "@kdonev/termscape",
3
+ "version": "0.1.0",
4
+ "description": "Run a crew of AI coding agents on one infinite canvas, and let them talk to each other.",
5
+ "license": "MIT",
6
+ "author": "kdonev",
7
+ "homepage": "https://github.com/kdonev/termscape#readme",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/kdonev/termscape.git",
11
+ "directory": "packages/hub"
12
+ },
13
+ "bugs": {
14
+ "url": "https://github.com/kdonev/termscape/issues"
15
+ },
16
+ "keywords": [
17
+ "ai",
18
+ "agents",
19
+ "terminal",
20
+ "canvas",
21
+ "mcp",
22
+ "claude-code",
23
+ "pty",
24
+ "tui"
25
+ ],
26
+ "type": "module",
27
+ "bin": {
28
+ "termscape": "./dist/cli.js"
29
+ },
30
+ "engines": {
31
+ "node": ">=22"
32
+ },
33
+ "dependencies": {
34
+ "@fastify/static": "^10.1.3",
35
+ "@fastify/websocket": "^11.0.1",
36
+ "@modelcontextprotocol/sdk": "^1.30.0",
37
+ "@xterm/addon-serialize": "^0.13.0",
38
+ "@xterm/headless": "^5.5.0",
39
+ "better-sqlite3": "^13.0.3",
40
+ "fastify": "^5.2.0",
41
+ "node-pty": "1.2.0-beta.15",
42
+ "smol-toml": "^1.3.1",
43
+ "ssh2": "^1.16.0",
44
+ "zod": "^4.5.4"
45
+ },
46
+ "scripts": {
47
+ "start": "node dist/cli.js"
48
+ }
49
+ }
@@ -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 .xterm-scroll-area{visibility:hidden}.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}.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}:root{--bg:#0a0c10;--grid:#151a23;--panel:#12161e;--panel-2:#171c26;--border:#262d3b;--text:#d5dae2;--muted:#7c8596;--accent:#7c9cf5;--danger:#e06c75;--ok:#88c07a;--warn:#d8b271;--lightningcss-light: ;--lightningcss-dark:initial;color-scheme:dark}*{box-sizing:border-box}html,body,#root{height:100%;margin:0}body{background:var(--bg);color:var(--text);font:13px/1.45 ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto,sans-serif;overflow:hidden}.app{flex-direction:column;height:100%;display:flex}.toolbar{background:var(--panel);border-bottom:1px solid var(--border);z-index:20;flex:none;align-items:center;gap:10px;padding:8px 12px;display:flex;position:relative}.brand{letter-spacing:.2px;align-items:baseline;gap:8px;font-weight:650;display:flex}.conn{font-size:11px;font-weight:400}.conn.on{color:var(--muted)}.conn.off{color:var(--warn)}.group{align-items:center;gap:6px;display:flex}.spacer{flex:1}.input{background:var(--panel-2);border:1px solid var(--border);color:var(--text);font:inherit;border-radius:6px;min-width:120px;padding:5px 8px}.input.wide{min-width:260px}.input:focus{border-color:var(--accent);outline:none}.btn{background:var(--panel-2);border:1px solid var(--border);color:var(--text);font:inherit;cursor:pointer;white-space:nowrap;border-radius:6px;padding:5px 9px}.btn:hover:not(:disabled){border-color:var(--accent)}.btn:disabled{opacity:.45;cursor:default}.btn.primary{background:#1e2a45;border-color:#33406a}.btn.danger:hover{border-color:var(--danger);color:var(--danger)}.btn.wide{min-width:56px}.canvas{cursor:default;background-color:var(--bg);background-image:linear-gradient(var(--grid) 1px, transparent 1px), linear-gradient(90deg, var(--grid) 1px, transparent 1px);touch-action:none;background-size:40px 40px;flex:1;position:relative;overflow:hidden}.canvas.panning{cursor:grabbing}.world{transform-origin:0 0;position:absolute;inset:0}.world.interacting{will-change:transform}.edges{pointer-events:none;position:absolute;overflow:visible}.ws-group{opacity:.4;pointer-events:none;border:1px dashed;border-radius:14px;position:absolute;top:0;left:0}.ws-label{white-space:nowrap;align-items:baseline;gap:8px;font-size:12px;font-weight:600;display:flex;position:absolute;top:-22px;left:12px}.ws-path{color:var(--muted);font-size:11px;font-weight:400}.window{background:var(--panel);border:1px solid var(--border);border-radius:10px;flex-direction:column;display:flex;position:absolute;top:0;left:0;overflow:hidden;box-shadow:0 8px 28px #00000073}.window.selected{box-shadow:0 0 0 1px var(--accent), 0 12px 36px #0000008c}.window.stopped{opacity:.82}.window-bar{background:var(--panel-2);border-bottom:1px solid var(--border);cursor:grab;-webkit-user-select:none;user-select:none;flex:none;align-items:center;gap:8px;padding:6px 8px;display:flex}.window-bar:active{cursor:grabbing}.window-bar .dot{border-radius:50%;flex:none;width:8px;height:8px}.window-bar .addr{font-size:12px;font-weight:600}.window-bar .meta{color:var(--muted);font-size:11px}.window-bar .btn{padding:2px 7px;font-size:11px}.window-body{background:#0e1117;flex:1;min-height:0;position:relative}.term-host{position:absolute;inset:4px 6px;overflow:hidden}.term-scale{transform-origin:0 0;position:absolute;top:0;left:0}.term-scale .xterm{width:100%;height:100%}.term-scale .xterm-viewport{scrollbar-width:none}.term-scale .xterm-viewport::-webkit-scrollbar{width:0;height:0;display:none}.resize-handle{cursor:nwse-resize;background:linear-gradient(135deg, transparent 50%, var(--border) 50%);width:16px;height:16px;position:absolute;bottom:0;right:0}.lod{text-align:center;height:100%;color:var(--muted);flex-direction:column;justify-content:center;align-items:center;gap:6px;padding:16px;display:flex}.lod-title{color:var(--text);font-size:20px;font-weight:650}.lod-sub{font-size:13px}.lod-status{color:var(--warn);font-size:13px}.lod-hint{opacity:.6;margin-top:6px;font-size:11px}.msg-log{background:var(--panel);border:1px solid var(--border);border-radius:10px;width:420px;max-height:60vh;padding:8px;position:absolute;top:100%;right:12px;overflow-y:auto;box-shadow:0 12px 32px #00000080}.msg-empty{color:var(--muted);text-align:center;padding:10px}.msg{border-left:2px solid var(--ok);background:var(--panel-2);border-radius:7px;margin-bottom:6px;padding:7px 9px}.msg.failed{border-left-color:var(--danger)}.msg-head{align-items:baseline;gap:6px;font-size:11px;display:flex}.msg-head .from,.msg-head .to{font-weight:600}.msg-head .arrow,.msg-head .when{color:var(--muted)}.msg-head .when{margin-left:auto}.msg-body{white-space:pre-wrap;word-break:break-word;max-height:90px;margin-top:4px;font-family:ui-monospace,Menlo,Consolas,monospace;font-size:11px;overflow:hidden}.msg-error{color:var(--danger);margin-top:4px;font-size:11px}.empty-state{text-align:center;max-width:460px;color:var(--muted);pointer-events:none;position:absolute;top:50%;left:50%;transform:translate(-50%,-50%)}.empty-state h1{color:var(--text);margin:0 0 8px;font-size:20px}.empty-state p{margin:0 0 10px}.empty-state .hint{opacity:.7;font-size:11px}.empty-state .btn{pointer-events:auto}.errors{z-index:30;flex-direction:column;gap:6px;max-width:460px;display:flex;position:absolute;bottom:14px;left:14px}.error{border:1px solid var(--danger);color:#f0b6bb;background:#2a1a1e;border-radius:7px;align-items:flex-start;gap:8px;padding:7px 10px;font-size:12px;display:flex}.error-body{word-break:break-word;flex:1;min-width:0}.error-close{color:inherit;opacity:.7;font:inherit;cursor:pointer;background:0 0;border:0;flex:none;padding:0 2px;line-height:1}.error-close:hover{opacity:1}.panel{background:var(--panel);border-left:1px solid var(--border);z-index:40;flex-direction:column;width:420px;max-width:92vw;transition:transform .18s;display:flex;position:absolute;top:0;bottom:0;right:0;transform:translate(100%);box-shadow:-18px 0 40px #00000073}.panel.open{transform:translate(0)}.panel[aria-hidden=true]{visibility:hidden}.panel.open[aria-hidden=false]{visibility:visible}.panel-head{border-bottom:1px solid var(--border);flex:none;align-items:center;gap:8px;padding:9px 12px;display:flex}.panel-title{font-weight:650}.panel-body{flex:1;padding:8px;overflow-y:auto}.node{border-radius:6px;align-items:center;gap:7px;min-width:0;padding:5px 7px;display:flex}.node .dot{border-radius:50%;flex:none;width:8px;height:8px}.node-label{white-space:nowrap;text-overflow:ellipsis;flex:0 auto;font-weight:600;overflow:hidden}.node-sub{color:var(--muted);white-space:nowrap;text-overflow:ellipsis;flex:0 auto;min-width:0;font-size:11px;overflow:hidden}.node.machine{background:var(--panel-2);margin-top:6px}.node.session{cursor:pointer}.node.session:hover{background:var(--panel-2)}.node.session.on{box-shadow:inset 0 0 0 1px var(--accent);background:#1b2540}.node .btn{padding:2px 7px;font-size:11px}.node-children{border-left:1px solid var(--border);margin-left:11px;padding-left:9px}.node-empty{color:var(--muted);padding:4px 8px;font-size:11px}.node-error{color:var(--danger);word-break:break-word;padding:2px 8px 4px 26px;font-size:11px}.node-log{color:var(--muted);word-break:break-word;padding:2px 8px 4px 26px;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:10.5px;line-height:1.35}.node-form{flex-wrap:wrap;gap:5px;padding:5px 7px 8px;display:flex}.node-form .input{flex:120px;min-width:90px}.node-form .input.wide{flex:3 200px;min-width:140px}.twisty{color:var(--muted);cursor:pointer;font:inherit;background:0 0;border:0;flex:none;width:14px;padding:0}.twisty:hover{color:var(--text)}.add-machine{margin:10px 0 0 7px}.add-machine-panel{border:1px solid var(--border);border-radius:8px;margin-top:10px;padding:8px}.host-tabs{border-bottom:1px solid var(--border);align-items:center;gap:4px;margin-bottom:4px;padding-bottom:8px;display:flex}.host-tabs .tab{font:inherit;color:var(--muted);cursor:pointer;background:0 0;border:1px solid #0000;border-radius:6px;flex:1;padding:5px 8px;font-size:11px}.host-tabs .tab:hover{color:var(--text)}.host-tabs .tab.on{color:var(--text);background:var(--panel-2);border-color:var(--border)}.join-url{background:var(--panel-2);border:1px solid var(--border);border-radius:7px;flex:1 0 100%;align-items:center;gap:6px;padding:7px 8px;display:flex}.join-url code{white-space:nowrap;min-width:0;color:var(--accent);flex:1;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:11.5px;overflow-x:auto}.host-note code{color:var(--text);font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace}.host-form{flex-wrap:wrap;gap:6px;padding-top:8px;display:flex}.host-form .input{flex:1;min-width:90px}.host-form .input.tiny{flex:none;min-width:56px;max-width:64px}.host-form .input.wide{flex:1 0 100%}.host-note{color:var(--muted);flex:1 0 100%;margin:2px 0 0;font-size:11px;line-height:1.4}