@slopus/ghostty-web 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 (39) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +85 -0
  3. package/dist/GhosttyRemoteTerminal.d.ts +69 -0
  4. package/dist/GhosttyRemoteTerminal.d.ts.map +1 -0
  5. package/dist/GhosttyRemoteTerminal.js +212 -0
  6. package/dist/RemoteTerminalProtocolClient.d.ts +30 -0
  7. package/dist/RemoteTerminalProtocolClient.d.ts.map +1 -0
  8. package/dist/RemoteTerminalProtocolClient.js +350 -0
  9. package/dist/RemoteTerminalProtocolServer.d.ts +75 -0
  10. package/dist/RemoteTerminalProtocolServer.d.ts.map +1 -0
  11. package/dist/RemoteTerminalProtocolServer.js +794 -0
  12. package/dist/WirePacket.d.ts +29 -0
  13. package/dist/WirePacket.d.ts.map +1 -0
  14. package/dist/WirePacket.js +24 -0
  15. package/dist/WirePacketDecoder.d.ts +8 -0
  16. package/dist/WirePacketDecoder.d.ts.map +1 -0
  17. package/dist/WirePacketDecoder.js +130 -0
  18. package/dist/applyGridPatch.d.ts +3 -0
  19. package/dist/applyGridPatch.d.ts.map +1 -0
  20. package/dist/applyGridPatch.js +19 -0
  21. package/dist/diffGridState.d.ts +3 -0
  22. package/dist/diffGridState.d.ts.map +1 -0
  23. package/dist/diffGridState.js +26 -0
  24. package/dist/encodeWirePacket.d.ts +7 -0
  25. package/dist/encodeWirePacket.d.ts.map +1 -0
  26. package/dist/encodeWirePacket.js +25 -0
  27. package/dist/index.d.ts +13 -0
  28. package/dist/index.d.ts.map +1 -0
  29. package/dist/index.js +9 -0
  30. package/dist/jsonPayload.d.ts +3 -0
  31. package/dist/jsonPayload.d.ts.map +1 -0
  32. package/dist/jsonPayload.js +8 -0
  33. package/dist/testing/ThrottledTcpProxy.d.ts +15 -0
  34. package/dist/testing/ThrottledTcpProxy.d.ts.map +1 -0
  35. package/dist/testing/ThrottledTcpProxy.js +83 -0
  36. package/dist/types.d.ts +105 -0
  37. package/dist/types.d.ts.map +1 -0
  38. package/dist/types.js +1 -0
  39. package/package.json +32 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 rig Contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,85 @@
1
+ # @slopus/ghostty-web
2
+
3
+ Client/server protocol for efficiently remoting a Ghostty-backed terminal. Clients implement the
4
+ protocol by importing this library:
5
+
6
+ ```ts
7
+ import { RemoteTerminalProtocolClient, GhosttyRemoteTerminalReplica } from "@slopus/ghostty-web";
8
+ ```
9
+
10
+ The implementation is isolated: it does not depend on Rig sessions or agent execution, and the
11
+ Ghostty adapters use structural typing so any compatible emulator (WASM or native) can plug in.
12
+
13
+ The protocol has two server-selected display modes:
14
+
15
+ - **VT replay** sends ordered raw PTY bytes to clients with a compatible terminal parser. A
16
+ Ghostty-backed client applies the bytes locally and acknowledges only after its emulator has
17
+ consumed them.
18
+ - **Semantic grid** sends a keyframe followed by patches containing changed rows. Slow clients may
19
+ skip transient states. A VT attachment switches permanently to this mode when its unacknowledged
20
+ window grows beyond its configured credit or when reconnect replay is no longer available.
21
+
22
+ Every binary packet is length-bounded, versioned, optionally deflate-compressed, and carries a
23
+ monotonic byte offset or grid revision. Input and resize operations have independent monotonic IDs.
24
+ Reconnects include a terminal epoch and server-issued lease so offsets from a previous process or
25
+ another replica cannot be reused.
26
+
27
+ The important ordering rules are part of the wire contract:
28
+
29
+ - A semantic keyframe declares `coversOutputOffset`; the server cannot use it to replace newer VT
30
+ bytes.
31
+ - Resize drains canonical parsing, resizes the PTY and canonical Ghostty, broadcasts an output
32
+ barrier, and only then releases output produced by the resize.
33
+ - Resize events carry an independent revision and are acknowledged by replicas. A fresh attachment
34
+ after any resize, or a reconnect that missed one, uses a semantic keyframe instead of replaying
35
+ bytes into the wrong geometry.
36
+ - Exit is retained by the server and is sent only after the client acknowledges display state that
37
+ covers the exit barrier. Attaching after exit produces the same final display and exit event.
38
+ - Output, grid, input, and resize acknowledgements are monotonic and bounded by state the server
39
+ actually sent. Input deduplication uses bounded, single-attachment leases.
40
+ - Scrollback pages carry a history epoch, revision, absolute base row, and optional palette/style
41
+ tables so a client can render cells and detect an evicted or shifting paging basis.
42
+ - Replay, per-client VT backlog, canonical parsing, resize-held output, inflated frames, and packet
43
+ batches all have explicit byte or item caps. Output is split into fixed server-sized chunks,
44
+ encoded once for fanout, and clients must advertise at least one chunk of credit, so hostile tiny
45
+ credit cannot trigger per-client packet explosions.
46
+
47
+ `createGhosttyRemoteTerminalServer` and `GhosttyRemoteTerminalReplica` are the concrete adapters.
48
+ The server driver serializes canonical Ghostty parsing, snapshots, and resize. A Ghostty replica is
49
+ raw-VT-only because a semantic grid cannot reconstruct Ghostty's hidden parser state; web or native
50
+ grid renderers can advertise the semantic capability for recovery. The driver also owns ordered
51
+ exit and the optional canonical terminal-response sink; replies generated by a replica are never
52
+ forwarded to the PTY.
53
+
54
+ ## Measurements and tests
55
+
56
+ `pnpm --filter @slopus/ghostty-web test` runs the protocol independently over real TCP. The
57
+ suite covers byte-split and malformed frames, bounded control floods, real Ghostty on both ends,
58
+ reconnects, resize races and rejection, stale-grid prevention, durable exit, one slow client,
59
+ 16-client encode-once fanout, 1 MiB output through a 96-byte credit window, hard pressure caps,
60
+ render-faithful semantic recovery, terminal-generated replies, lost input acknowledgements, Ctrl-C
61
+ during input flood, and stable scrollback paging.
62
+
63
+ The constrained-link scenario uses 1 Mbps, 150 ms RTT, deterministic jitter, and 137-byte network
64
+ fragments. It reports p50/p95 handshake, first-byte, input-to-PTY, input-to-render, Ghostty
65
+ render-ready, dense convergence, CPU, RSS, and wire bytes. Values are printed rather than treated as
66
+ portable performance promises; broad ceilings catch regressions without turning host load into
67
+ flaky microbenchmarks.
68
+
69
+ ## Design sources
70
+
71
+ - [Mosh](https://mosh.org/) synchronizes terminal state instead of queueing every intermediate
72
+ frame and demonstrates local echo for high-latency paths.
73
+ - [RFB / VNC](https://www.rfc-editor.org/rfc/rfc6143) uses client-demanded incremental updates so a
74
+ slow client can skip transient framebuffer states.
75
+ - [RDP graphics acknowledgements](https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdprfx/e4d498fd-822b-408d-b8b3-1c216f21265b)
76
+ advertise client capacity and bound frames in flight.
77
+ - [tmux control mode](https://github.com/tmux/tmux/wiki/Control-Mode) pauses lagging pane output and
78
+ recovers through an authoritative pane capture.
79
+ - [xterm.js flow control](https://xtermjs.org/docs/guides/flowcontrol/) recommends end-to-end
80
+ acknowledgements tied to emulator consumption and high/low watermarks.
81
+ - [`libghostty-vt` RenderState](https://github.com/ghostty-org/ghostty) tracks global and per-row
82
+ dirty state, which maps directly to semantic row patches.
83
+
84
+ The transport is a Node `Duplex`; TCP, Unix sockets, TLS, or a binary WebSocket adapter can supply
85
+ that boundary. Authentication and encryption belong to the enclosing transport.
@@ -0,0 +1,69 @@
1
+ import { RemoteTerminalProtocolServer } from "./RemoteTerminalProtocolServer.js";
2
+ import type { RemoteTerminalGridState, RemoteTerminalReplica, RemoteTerminalServerOptions } from "./types.js";
3
+ export interface GhosttySnapshotCell {
4
+ background: unknown;
5
+ blink?: boolean;
6
+ bold: boolean;
7
+ dim: boolean;
8
+ foreground: unknown;
9
+ invisible?: boolean;
10
+ inverse?: boolean;
11
+ italic: boolean;
12
+ overline?: boolean;
13
+ strikethrough?: boolean;
14
+ text: string;
15
+ underline?: string;
16
+ underlineColor?: unknown;
17
+ width?: 1 | 2;
18
+ x: number;
19
+ y: number;
20
+ }
21
+ export interface GhosttySnapshot {
22
+ cells: readonly GhosttySnapshotCell[];
23
+ cursor: {
24
+ visible: boolean;
25
+ x: number;
26
+ y: number;
27
+ };
28
+ palette?: readonly string[];
29
+ rows: readonly string[];
30
+ scroll: {
31
+ offset: number;
32
+ totalRows: number;
33
+ visibleRows: number;
34
+ };
35
+ title: string;
36
+ wrappedRows?: readonly boolean[];
37
+ }
38
+ export interface GhosttyTerminalLike {
39
+ onPtyWrite?(handler: (data: string) => void): () => void;
40
+ resize(cols: number, rows: number): void | Promise<void>;
41
+ snapshot(): GhosttySnapshot | Promise<GhosttySnapshot>;
42
+ writeBytes(data: Uint8Array): void | Promise<void>;
43
+ }
44
+ export declare class GhosttyRemoteTerminalReplica implements RemoteTerminalReplica {
45
+ #private;
46
+ constructor(terminal: GhosttyTerminalLike);
47
+ applyGrid(): never;
48
+ applyVt(data: Uint8Array): void | Promise<void>;
49
+ resize(cols: number, rows: number): void | Promise<void>;
50
+ }
51
+ export declare class GhosttyRemoteTerminalServerDriver {
52
+ #private;
53
+ constructor(protocol: RemoteTerminalProtocolServer, terminal: GhosttyTerminalLike, cols?: number, maxBufferedBytes?: number, onTerminalResponse?: (data: Uint8Array) => void | Promise<void>);
54
+ publishOutput(data: Uint8Array): Promise<void>;
55
+ prepareResize(): Promise<void>;
56
+ performResize(cols: number, rows: number, resizePty?: (cols: number, rows: number) => void | Promise<void>): Promise<void>;
57
+ publishExit(exitCode: number | null): Promise<void>;
58
+ close(): void;
59
+ settled(): Promise<void>;
60
+ }
61
+ export declare function createGhosttyRemoteTerminalServer(terminal: GhosttyTerminalLike, options: Omit<RemoteTerminalServerOptions, "onResize"> & {
62
+ onResize?: RemoteTerminalServerOptions["onResize"];
63
+ onTerminalResponse?: (data: Uint8Array) => void | Promise<void>;
64
+ }): {
65
+ driver: GhosttyRemoteTerminalServerDriver;
66
+ protocol: RemoteTerminalProtocolServer;
67
+ };
68
+ export declare function ghosttySnapshotToGrid(snapshot: GhosttySnapshot, cols?: number): Omit<RemoteTerminalGridState, "coversOutputOffset" | "revision">;
69
+ //# sourceMappingURL=GhosttyRemoteTerminal.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"GhosttyRemoteTerminal.d.ts","sourceRoot":"","sources":["../sources/GhosttyRemoteTerminal.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,212 @@
1
+ import { RemoteTerminalProtocolServer } from "./RemoteTerminalProtocolServer.js";
2
+ export class GhosttyRemoteTerminalReplica {
3
+ #terminal;
4
+ constructor(terminal) {
5
+ this.#terminal = terminal;
6
+ }
7
+ applyGrid() {
8
+ throw new Error("A Ghostty VT replica cannot be seeded from a semantic grid.");
9
+ }
10
+ applyVt(data) {
11
+ return this.#terminal.writeBytes(data);
12
+ }
13
+ resize(cols, rows) {
14
+ return this.#terminal.resize(cols, rows);
15
+ }
16
+ }
17
+ export class GhosttyRemoteTerminalServerDriver {
18
+ #cols;
19
+ #exiting = false;
20
+ #heldOutput = [];
21
+ #heldOutputBytes = 0;
22
+ #maxBufferedBytes;
23
+ #operation = Promise.resolve();
24
+ #queuedOutputBytes = 0;
25
+ #resizeSettled = Promise.resolve();
26
+ #resolveResizeSettled;
27
+ #protocol;
28
+ #terminal;
29
+ #unsubscribePtyWrite;
30
+ #waitingForResize = false;
31
+ constructor(protocol, terminal, cols = 80, maxBufferedBytes = 4 * 1024 * 1024, onTerminalResponse) {
32
+ this.#protocol = protocol;
33
+ this.#terminal = terminal;
34
+ this.#cols = cols;
35
+ this.#maxBufferedBytes = maxBufferedBytes;
36
+ this.#unsubscribePtyWrite =
37
+ onTerminalResponse === undefined
38
+ ? undefined
39
+ : terminal.onPtyWrite?.((data) => {
40
+ try {
41
+ void Promise.resolve(onTerminalResponse(Buffer.from(data))).catch((error) => this.#protocol.fail(error));
42
+ }
43
+ catch (error) {
44
+ this.#protocol.fail(error);
45
+ }
46
+ });
47
+ }
48
+ publishOutput(data) {
49
+ if (this.#exiting)
50
+ return Promise.reject(new Error("Terminal output has already exited."));
51
+ const bytes = Buffer.from(data);
52
+ if (this.#queuedOutputBytes + bytes.length > this.#maxBufferedBytes) {
53
+ const error = new Error("Canonical terminal output buffer is full.");
54
+ this.#protocol.fail(error);
55
+ return Promise.reject(error);
56
+ }
57
+ this.#queuedOutputBytes += bytes.length;
58
+ if (this.#waitingForResize) {
59
+ if (this.#heldOutputBytes + bytes.length > this.#maxBufferedBytes) {
60
+ const error = new Error("Canonical resize output buffer is full.");
61
+ this.#queuedOutputBytes -= bytes.length;
62
+ this.#protocol.fail(error);
63
+ return Promise.reject(error);
64
+ }
65
+ this.#heldOutputBytes += bytes.length;
66
+ return new Promise((resolve, reject) => {
67
+ this.#heldOutput.push({ bytes, reject, resolve });
68
+ });
69
+ }
70
+ return this.#enqueueOutput(bytes, true);
71
+ }
72
+ async prepareResize() {
73
+ if (this.#waitingForResize)
74
+ throw new Error("A canonical terminal resize is already pending.");
75
+ this.#waitingForResize = true;
76
+ this.#resizeSettled = new Promise((resolve) => {
77
+ this.#resolveResizeSettled = resolve;
78
+ });
79
+ await this.#operation;
80
+ }
81
+ async performResize(cols, rows, resizePty) {
82
+ if (!this.#waitingForResize)
83
+ throw new Error("Canonical resize was not prepared.");
84
+ let failure;
85
+ try {
86
+ await resizePty?.(cols, rows);
87
+ await this.#terminal.resize(cols, rows);
88
+ this.#cols = cols;
89
+ const snapshot = await this.#terminal.snapshot();
90
+ this.#protocol.publishGrid({
91
+ ...ghosttySnapshotToGrid(snapshot, this.#cols),
92
+ coversOutputOffset: this.#protocol.outputOffset(),
93
+ });
94
+ }
95
+ catch (error) {
96
+ failure = error;
97
+ this.#protocol.fail(error);
98
+ throw error;
99
+ }
100
+ finally {
101
+ this.#waitingForResize = false;
102
+ for (const held of this.#heldOutput.splice(0)) {
103
+ this.#heldOutputBytes -= held.bytes.length;
104
+ if (failure === undefined)
105
+ void this.#enqueueOutput(held.bytes, true).then(held.resolve, held.reject);
106
+ else {
107
+ this.#queuedOutputBytes -= held.bytes.length;
108
+ held.reject(failure);
109
+ }
110
+ }
111
+ this.#resolveResizeSettled?.();
112
+ this.#resolveResizeSettled = undefined;
113
+ }
114
+ }
115
+ async publishExit(exitCode) {
116
+ this.#exiting = true;
117
+ await this.#resizeSettled;
118
+ await this.#operation;
119
+ this.#protocol.publishExit(exitCode);
120
+ }
121
+ close() {
122
+ this.#unsubscribePtyWrite?.();
123
+ }
124
+ settled() {
125
+ return this.#operation;
126
+ }
127
+ #enqueueOutput(bytes, reserved = false) {
128
+ if (!reserved)
129
+ this.#queuedOutputBytes += bytes.length;
130
+ const operation = this.#operation.then(async () => {
131
+ try {
132
+ await this.#terminal.writeBytes(bytes);
133
+ const snapshot = await this.#terminal.snapshot();
134
+ this.#protocol.publishUpdate(bytes, ghosttySnapshotToGrid(snapshot, this.#cols));
135
+ }
136
+ finally {
137
+ this.#queuedOutputBytes -= bytes.length;
138
+ }
139
+ });
140
+ this.#operation = operation.catch((error) => {
141
+ this.#protocol.fail(error);
142
+ });
143
+ return operation;
144
+ }
145
+ }
146
+ export function createGhosttyRemoteTerminalServer(terminal, options) {
147
+ let driver;
148
+ const protocol = new RemoteTerminalProtocolServer({
149
+ ...options,
150
+ async onBeforeResize() {
151
+ await options.onBeforeResize?.();
152
+ await driver.prepareResize();
153
+ },
154
+ async onResize(cols, rows) {
155
+ await driver.performResize(cols, rows, options.onResize);
156
+ },
157
+ });
158
+ driver = new GhosttyRemoteTerminalServerDriver(protocol, terminal, options.initialCols ?? 80, options.maxBufferedBytes ?? options.maxReplayBytes ?? 4 * 1024 * 1024, options.onTerminalResponse);
159
+ return { driver, protocol };
160
+ }
161
+ export function ghosttySnapshotToGrid(snapshot, cols = Math.max(1, snapshot.rows.reduce((maximum, row) => Math.max(maximum, row.length), 0))) {
162
+ const styleIds = new Map();
163
+ const styles = [];
164
+ const cellsByRow = new Map();
165
+ for (const cell of snapshot.cells) {
166
+ const row = cellsByRow.get(cell.y) ?? [];
167
+ row.push(cell);
168
+ cellsByRow.set(cell.y, row);
169
+ }
170
+ const rows = snapshot.rows.map((_text, y) => {
171
+ const source = (cellsByRow.get(y) ?? []).sort((left, right) => left.x - right.x);
172
+ return {
173
+ cells: source.map((cell, index) => {
174
+ const style = {
175
+ background: cell.background,
176
+ blink: cell.blink ?? false,
177
+ bold: cell.bold,
178
+ dim: cell.dim,
179
+ foreground: cell.foreground,
180
+ invisible: cell.invisible ?? false,
181
+ inverse: cell.inverse ?? false,
182
+ italic: cell.italic,
183
+ overline: cell.overline ?? false,
184
+ strikethrough: cell.strikethrough ?? false,
185
+ underline: cell.underline ?? "none",
186
+ underlineColor: cell.underlineColor ?? null,
187
+ };
188
+ const key = JSON.stringify(style);
189
+ let styleId = styleIds.get(key);
190
+ if (styleId === undefined) {
191
+ styleId = styles.length;
192
+ styleIds.set(key, styleId);
193
+ styles.push(style);
194
+ }
195
+ const nextX = source[index + 1]?.x;
196
+ const width = cell.width ?? (nextX === cell.x + 2 ? 2 : 1);
197
+ return { styleId, text: cell.text, width, x: cell.x };
198
+ }),
199
+ wrapped: snapshot.wrappedRows?.[y] ?? false,
200
+ };
201
+ });
202
+ return {
203
+ cols,
204
+ cursor: snapshot.cursor,
205
+ palette: snapshot.palette ?? [],
206
+ rows,
207
+ startRow: snapshot.scroll.offset,
208
+ styles: styles.length === 0 ? [{}] : styles,
209
+ title: snapshot.title,
210
+ totalRows: snapshot.scroll.totalRows,
211
+ };
212
+ }
@@ -0,0 +1,30 @@
1
+ import type { RemoteTerminalClientOptions, RemoteTerminalGridState, RemoteTerminalMode, RemoteTerminalScrollbackPage } from "./types.js";
2
+ export interface RemoteTerminalReconnectState {
3
+ epoch: string | undefined;
4
+ inputLease: string | undefined;
5
+ pendingInputs: readonly {
6
+ data: Uint8Array;
7
+ sequence: number;
8
+ }[];
9
+ resumeInputSequence: number;
10
+ resumeOutputOffset: number;
11
+ }
12
+ export declare class RemoteTerminalProtocolClient {
13
+ #private;
14
+ appliedOutputOffset: number;
15
+ epoch: string | undefined;
16
+ grid: RemoteTerminalGridState | undefined;
17
+ inputLease: string | undefined;
18
+ mode: RemoteTerminalMode | undefined;
19
+ readonly ready: Promise<void>;
20
+ constructor(options: RemoteTerminalClientOptions);
21
+ close(): void;
22
+ reconnectState(): RemoteTerminalReconnectState;
23
+ requestScrollback(start: number, count: number, basis?: {
24
+ historyEpoch: string;
25
+ historyRevision: number;
26
+ }): Promise<RemoteTerminalScrollbackPage>;
27
+ resize(cols: number, rows: number): Promise<void>;
28
+ writeInput(data: Uint8Array | string): number;
29
+ }
30
+ //# sourceMappingURL=RemoteTerminalProtocolClient.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"RemoteTerminalProtocolClient.d.ts","sourceRoot":"","sources":["../sources/RemoteTerminalProtocolClient.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EACR,2BAA2B,EAE3B,uBAAuB,EACvB,kBAAkB,EAClB,4BAA4B,EAC/B,MAAM,YAAY,CAAC;AAepB,MAAM,WAAW,4BAA4B;IACzC,KAAK,EAAE,MAAM,GAAG,SAAS,CAAC;IAC1B,UAAU,EAAE,MAAM,GAAG,SAAS,CAAC;IAC/B,aAAa,EAAE,SAAS;QAAE,IAAI,EAAE,UAAU,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IACjE,mBAAmB,EAAE,MAAM,CAAC;IAC5B,kBAAkB,EAAE,MAAM,CAAC;CAC9B;AAED,qBAAa,4BAA4B;;IACrC,mBAAmB,EAAE,MAAM,CAAC;IAC5B,KAAK,EAAE,MAAM,GAAG,SAAS,CAAC;IAC1B,IAAI,EAAE,uBAAuB,GAAG,SAAS,CAAC;IAC1C,UAAU,EAAE,MAAM,GAAG,SAAS,CAAC;IAC/B,IAAI,EAAE,kBAAkB,GAAG,SAAS,CAAC;IACrC,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC;gBAsBlB,OAAO,EAAE,2BAA2B;IAuDhD,KAAK,IAAI,IAAI;IAIb,cAAc,IAAI,4BAA4B;IAa9C,iBAAiB,CACb,KAAK,EAAE,MAAM,EACb,KAAK,EAAE,MAAM,EACb,KAAK,CAAC,EAAE;QAAE,YAAY,EAAE,MAAM,CAAC;QAAC,eAAe,EAAE,MAAM,CAAA;KAAE,GAC1D,OAAO,CAAC,4BAA4B,CAAC;IA0BxC,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAejD,UAAU,CAAC,IAAI,EAAE,UAAU,GAAG,MAAM,GAAG,MAAM;CA6KhD"}