@mp-lb/termkit 0.2.2 → 0.2.4

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 (74) hide show
  1. package/README.md +19 -110
  2. package/dist/Terminal.d.ts +10 -1
  3. package/dist/Terminal.d.ts.map +1 -1
  4. package/dist/Terminal.js +100 -15
  5. package/dist/TermkitContext.d.ts +8 -0
  6. package/dist/TermkitContext.d.ts.map +1 -0
  7. package/dist/TermkitContext.js +2 -0
  8. package/dist/TermkitProvider.d.ts +8 -0
  9. package/dist/TermkitProvider.d.ts.map +1 -0
  10. package/dist/{context.js → TermkitProvider.js} +11 -18
  11. package/dist/createTerminalWriteQueue.d.ts.map +1 -1
  12. package/dist/createTerminalWriteQueue.js +1 -2
  13. package/dist/{store.d.ts → createTermkitStore.d.ts} +1 -1
  14. package/dist/createTermkitStore.d.ts.map +1 -0
  15. package/dist/{store.js → createTermkitStore.js} +11 -5
  16. package/dist/fitTerminal.d.ts.map +1 -1
  17. package/dist/fitTerminal.js +6 -3
  18. package/dist/index.d.ts +16 -5
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +12 -3
  21. package/dist/terminalColors.d.ts +11 -0
  22. package/dist/terminalColors.d.ts.map +1 -0
  23. package/dist/terminalColors.js +57 -0
  24. package/dist/types.d.ts +4 -0
  25. package/dist/types.d.ts.map +1 -1
  26. package/dist/types.js +0 -1
  27. package/dist/useDanglingSessions.d.ts +3 -0
  28. package/dist/useDanglingSessions.d.ts.map +1 -0
  29. package/dist/useDanglingSessions.js +6 -0
  30. package/dist/useFocusedSession.d.ts +2 -0
  31. package/dist/useFocusedSession.d.ts.map +1 -0
  32. package/dist/useFocusedSession.js +6 -0
  33. package/dist/useIsFocused.d.ts +2 -0
  34. package/dist/useIsFocused.d.ts.map +1 -0
  35. package/dist/useIsFocused.js +4 -0
  36. package/dist/useIsRinging.d.ts +2 -0
  37. package/dist/useIsRinging.d.ts.map +1 -0
  38. package/dist/useIsRinging.js +4 -0
  39. package/dist/useRingingSessions.d.ts +2 -0
  40. package/dist/useRingingSessions.d.ts.map +1 -0
  41. package/dist/useRingingSessions.js +6 -0
  42. package/dist/useSession.d.ts +3 -0
  43. package/dist/useSession.d.ts.map +1 -0
  44. package/dist/useSession.js +6 -0
  45. package/dist/useSessionStatus.d.ts +3 -0
  46. package/dist/useSessionStatus.d.ts.map +1 -0
  47. package/dist/useSessionStatus.js +6 -0
  48. package/dist/useSessions.d.ts +3 -0
  49. package/dist/useSessions.d.ts.map +1 -0
  50. package/dist/useSessions.js +6 -0
  51. package/dist/useSlots.d.ts +3 -0
  52. package/dist/useSlots.d.ts.map +1 -0
  53. package/dist/useSlots.js +6 -0
  54. package/dist/useTermkitActions.d.ts +10 -0
  55. package/dist/useTermkitActions.d.ts.map +1 -0
  56. package/dist/useTermkitActions.js +20 -0
  57. package/dist/useTermkitContext.d.ts +3 -0
  58. package/dist/useTermkitContext.d.ts.map +1 -0
  59. package/dist/useTermkitContext.js +8 -0
  60. package/package.json +9 -7
  61. package/dist/Terminal.js.map +0 -1
  62. package/dist/context.d.ts +0 -15
  63. package/dist/context.d.ts.map +0 -1
  64. package/dist/context.js.map +0 -1
  65. package/dist/createTerminalWriteQueue.js.map +0 -1
  66. package/dist/fitTerminal.js.map +0 -1
  67. package/dist/hooks.d.ts +0 -22
  68. package/dist/hooks.d.ts.map +0 -1
  69. package/dist/hooks.js +0 -83
  70. package/dist/hooks.js.map +0 -1
  71. package/dist/index.js.map +0 -1
  72. package/dist/store.d.ts.map +0 -1
  73. package/dist/store.js.map +0 -1
  74. package/dist/types.js.map +0 -1
package/README.md CHANGED
@@ -1,122 +1,31 @@
1
1
  # @mp-lb/termkit
2
2
 
3
- Easily orchestrate terminal sessions in React. Render a `<Terminal />`, and manage
4
- all your live sessions — including their attention bells — with a small set of
5
- global hooks.
6
-
7
- ## Mental model
8
-
9
- - A **session** is a long-lived shell (a PTY) living in the **daemon**
10
- (`@mp-lb/termkit-server`), addressed by a stable id you choose.
11
- - A `<Terminal session={id} />` is just a **view** onto a session. Mounting it
12
- attaches (reconnecting to the existing shell if there is one, via a replayed
13
- screen snapshot); unmounting detaches but leaves the shell running.
14
- - The `<TermkitProvider>` holds the **global registry** of sessions and polls the
15
- daemon for the live list. So there is **no one-to-one** requirement between the
16
- terminals on screen and the sessions that are alive — you can have background
17
- sessions with no terminal, and you can see/manage them from anywhere.
18
-
19
- ## Two ways to use a session
20
-
21
- - **Local:** drop `<Terminal session="abc" />` somewhere. Rendering it creates or
22
- reattaches session `abc`. Its lifecycle reads as component-local — but the
23
- session itself outlives the component.
24
- - **Global:** use the hooks (`useSessions`, `useRingingSessions`,
25
- `useTermkitActions`, …) anywhere under the provider to list every session, watch
26
- which ones are **belling** for attention, and act on them — whether or not a
27
- terminal is mounted for them.
28
-
29
- ## Setup
3
+ Build React terminal experiences backed by persistent PTY sessions. This workspace package is private while its API is being designed; use it through the workspace dependency `workspace:*`.
30
4
 
31
5
  ```tsx
32
- import { TermkitProvider } from "@mp-lb/termkit"
6
+ import { Terminal, TermkitProvider, type TermkitConfig } from "@mp-lb/termkit";
33
7
 
34
- const config = {
35
- // Where the byte stream lives. `params` is forwarded to the daemon as session
36
- // metadata and drives its resolveCwd.
8
+ const config: TermkitConfig = {
37
9
  connectionUrl: ({ sessionId, params }) =>
38
- `ws://${location.host}/_terminal?` +
39
- new URLSearchParams({ session: sessionId, ...params }),
40
- // The control plane: list / kill / clear-bell. Wire to your backend.
10
+ `wss://example.com/_terminal?${new URLSearchParams({ ...params, session: sessionId })}`,
41
11
  transport: {
42
- list: () => trpc.terminal.list.query(),
43
- kill: (sessionId) => trpc.terminal.kill.mutate({ sessionId }),
44
- seen: (sessionId) => trpc.terminal.seen.mutate({ sessionId }),
12
+ list: () => api.terminal.list(),
13
+ kill: (sessionId) => api.terminal.kill(sessionId),
14
+ seen: (sessionId) => api.terminal.seen(sessionId),
45
15
  },
46
- }
47
-
48
- <TermkitProvider config={config}>
49
- <App />
50
- </TermkitProvider>
16
+ };
17
+
18
+ export const Shell = () => (
19
+ <TermkitProvider config={config}>
20
+ <div style={{ height: 400 }}>
21
+ <Terminal session="workspace" autoFocus />
22
+ </div>
23
+ </TermkitProvider>
24
+ );
51
25
  ```
52
26
 
53
- ## Rendering a terminal
54
-
55
- ```tsx
56
- import { Terminal } from "@mp-lb/termkit"
57
-
58
- <Terminal session="global" autoFocus />
59
- <Terminal session={tab.id} params={{ slug: "myproject" }} />
60
- ```
61
-
62
- Props: `session` (required, stable id), `params`, `autoFocus`, `dismissOnView`
63
- (default `true` — clear this session's bell when you view it), `pendingCommand`
64
- (auto-typed once after the prompt settles), `onExitFocus` (called on
65
- Ctrl+Shift+Enter, after termkit blurs the terminal), `background`, `className`,
66
- `style`.
67
-
68
- ## Bells & session hooks
69
-
70
- ```tsx
71
- const sessions = useSessions() // LiveSession[] — the global list
72
- const ringing = useRingingSessions() // Set<string> of ids belling for attention
73
- const isRinging = useIsRinging(id) // one session
74
- const status = useSessionStatus(id) // connecting | ready | disconnected | dead
75
- const { focus, kill, dismissBell, dismissAllBells, setPendingCommand } =
76
- useTermkitActions()
77
- ```
78
-
79
- Bells clear automatically when you view a session (`dismissOnView`), or explicitly
80
- via `dismissBell(id)` / `dismissAllBells()`.
81
-
82
- ## Theming
83
-
84
- `<Terminal>` renders xterm on a transparent surface inside a full-size wrapper.
85
- Set the wrapper background with Tailwind/classes, an explicit colour, a CSS
86
- variable, or a provider default:
87
-
88
- ```tsx
89
- <Terminal session="global" className="bg-zinc-950" />
90
- <Terminal session="global" background="var(--background)" />
91
- <Terminal session="global" style={{ backgroundColor: theme.terminal }} />
92
-
93
- const config = {
94
- ...baseConfig,
95
- terminalOptions: { background: "var(--termkit-bg)" },
96
- }
97
- ```
98
-
99
- Because the terminal itself is transparent, any leftover space that cannot fit a
100
- full row or column uses the same background as the terminal.
101
-
102
- ## Migrating dash onto termkit
103
-
104
- termkit is a superset of what `dash` does today; the mapping is mechanical:
27
+ `api` represents your host application's control-plane adapter. Keep the provider configuration stable. Unmounting a terminal detaches the view; the shell keeps running in the daemon. Remounting the same session ID reattaches to its current screen.
105
28
 
106
- | dash | termkit |
107
- | --- | --- |
108
- | `<Terminal slot=… slug=… instanceId=… />` | `<Terminal session=… params={{ slug, instanceId }} />` |
109
- | `useTerminalStore` (zustand) | the provider store + `useTermkitActions` |
110
- | `live-sessions.ts` poller + `useLiveSessions` | built into `<TermkitProvider>` + `useSessions` |
111
- | `useRingingSessionIds` | `useRingingSessions` |
112
- | `markTerminalSeen` on focus/view | `dismissOnView` (default on) + `dismissBell` |
113
- | `LiveSession.slug` / `.instanceId` | `LiveSession.meta.slug` / `.meta.instanceId` |
114
- | exit-chord → focus `[data-session-tab]` | `onExitFocus` callback |
115
- | `useKnownSessionIds` / `useSessionSummaries` | **stay in dash** — built on `useSessions()` (project/slug grouping is dash domain) |
116
- | daemon (`server/terminal-daemon.ts`) | `createTerminalDaemon({ resolveCwd })` from `@mp-lb/termkit-server` |
117
- | tRPC `terminal` router | `createTerminalRouter(client)` from `@mp-lb/termkit-trpc`, or keep dash's |
29
+ TermKit includes IBM Plex Mono and Noto Sans Symbols 2 for its default terminal font stack. Applications can provide their own font family through the terminal options.
118
30
 
119
- The dash-specific seams — the `~/Code` working-directory resolution and the
120
- project/instance groupings — are deliberately **not** baked in: the daemon takes a
121
- `resolveCwd` callback, and per-project rollups stay in dash, layered over the
122
- generic `useSessions()` list.
31
+ See the [guide](../../docs/public/index.mdx) and [generated reference](../../docs/public/api/index.mdx). The Node daemon and optional tRPC adapter live in sibling packages and are kept out of the browser entrypoint.
@@ -1,5 +1,10 @@
1
1
  import { type CSSProperties } from "react";
2
+ import { type ITheme } from "@xterm/xterm";
2
3
  import "@xterm/xterm/css/xterm.css";
4
+ import "@fontsource/ibm-plex-mono/400.css";
5
+ import "@fontsource/ibm-plex-mono/700.css";
6
+ import "@fontsource/noto-sans-symbols-2/400.css";
7
+ import { type TerminalColors } from "./terminalColors.js";
3
8
  export interface TerminalProps {
4
9
  session: string;
5
10
  params?: Record<string, string>;
@@ -7,9 +12,13 @@ export interface TerminalProps {
7
12
  dismissOnView?: boolean;
8
13
  pendingCommand?: string;
9
14
  onExitFocus?: () => void;
15
+ exitFocusChord?: "ctrl-enter" | "ctrl-shift-enter";
10
16
  background?: string;
17
+ colors?: TerminalColors;
18
+ theme?: ITheme;
19
+ visible?: boolean;
11
20
  className?: string;
12
21
  style?: CSSProperties;
13
22
  }
14
- export declare function Terminal({ session, params, autoFocus, dismissOnView, pendingCommand, onExitFocus, background, className, style, }: TerminalProps): import("react/jsx-runtime").JSX.Element;
23
+ export declare function Terminal({ session, params, autoFocus, dismissOnView, pendingCommand, onExitFocus, exitFocusChord, background, colors, theme, visible, className, style, }: TerminalProps): import("react").JSX.Element;
15
24
  //# sourceMappingURL=Terminal.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"Terminal.d.ts","sourceRoot":"","sources":["../src/Terminal.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAqB,KAAK,aAAa,EAAE,MAAM,OAAO,CAAA;AAG7D,OAAO,4BAA4B,CAAA;AAWnC,MAAM,WAAW,aAAa;IAK5B,OAAO,EAAE,MAAM,CAAA;IAIf,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAE/B,SAAS,CAAC,EAAE,OAAO,CAAA;IAKnB,aAAa,CAAC,EAAE,OAAO,CAAA;IAGvB,cAAc,CAAC,EAAE,MAAM,CAAA;IAIvB,WAAW,CAAC,EAAE,MAAM,IAAI,CAAA;IAIxB,UAAU,CAAC,EAAE,MAAM,CAAA;IAGnB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,KAAK,CAAC,EAAE,aAAa,CAAA;CACtB;AAED,wBAAgB,QAAQ,CAAC,EACvB,OAAO,EACP,MAAM,EACN,SAAiB,EACjB,aAAoB,EACpB,cAAc,EACd,WAAW,EACX,UAAU,EACV,SAAS,EACT,KAAK,GACN,EAAE,aAAa,2CA2Sf"}
1
+ {"version":3,"file":"Terminal.d.ts","sourceRoot":"","sources":["../src/Terminal.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAqB,KAAK,aAAa,EAAE,MAAM,OAAO,CAAC;AAC9D,OAAO,EAAqB,KAAK,MAAM,EAAE,MAAM,cAAc,CAAC;AAE9D,OAAO,4BAA4B,CAAC;AACpC,OAAO,mCAAmC,CAAC;AAC3C,OAAO,mCAAmC,CAAC;AAC3C,OAAO,yCAAyC,CAAC;AAUjD,OAAO,EAEL,KAAK,cAAc,EACpB,MAAM,qBAAqB,CAAC;AAE7B,MAAM,WAAW,aAAa;IAK5B,OAAO,EAAE,MAAM,CAAC;IAIhB,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAEhC,SAAS,CAAC,EAAE,OAAO,CAAC;IAKpB,aAAa,CAAC,EAAE,OAAO,CAAC;IAGxB,cAAc,CAAC,EAAE,MAAM,CAAC;IAIxB,WAAW,CAAC,EAAE,MAAM,IAAI,CAAC;IAGzB,cAAc,CAAC,EAAE,YAAY,GAAG,kBAAkB,CAAC;IAInD,UAAU,CAAC,EAAE,MAAM,CAAC;IAEpB,MAAM,CAAC,EAAE,cAAc,CAAC;IAIxB,KAAK,CAAC,EAAE,MAAM,CAAC;IAGf,OAAO,CAAC,EAAE,OAAO,CAAC;IAGlB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,aAAa,CAAC;CACvB;AAED,wBAAgB,QAAQ,CAAC,EACvB,OAAO,EACP,MAAM,EACN,SAAiB,EACjB,aAAoB,EACpB,cAAc,EACd,WAAW,EACX,cAAmC,EACnC,UAAU,EACV,MAAM,EACN,KAAK,EACL,OAAc,EACd,SAAS,EACT,KAAK,GACN,EAAE,aAAa,+BA+cf"}
package/dist/Terminal.js CHANGED
@@ -3,13 +3,18 @@ import { useEffect, useRef } from "react";
3
3
  import { Terminal as XTerm } from "@xterm/xterm";
4
4
  import { FitAddon } from "@xterm/addon-fit";
5
5
  import "@xterm/xterm/css/xterm.css";
6
+ import "@fontsource/ibm-plex-mono/400.css";
7
+ import "@fontsource/ibm-plex-mono/700.css";
8
+ import "@fontsource/noto-sans-symbols-2/400.css";
6
9
  import { CLOSE_SESSION_GONE, HEARTBEAT_INTERVAL_MS, HEARTBEAT_PONG_GRACE_MS, } from "@mp-lb/termkit-core";
7
- import { useTermkitContext } from "./context.js";
10
+ import { useTermkitContext } from "./useTermkitContext.js";
8
11
  import { createTerminalWriteQueue } from "./createTerminalWriteQueue.js";
9
12
  import { fitTerminal } from "./fitTerminal.js";
10
- export function Terminal({ session, params, autoFocus = false, dismissOnView = true, pendingCommand, onExitFocus, background, className, style, }) {
13
+ import { resolveTerminalColors, } from "./terminalColors.js";
14
+ export function Terminal({ session, params, autoFocus = false, dismissOnView = true, pendingCommand, onExitFocus, exitFocusChord = "ctrl-shift-enter", background, colors, theme, visible = true, className, style, }) {
11
15
  const { store, config } = useTermkitContext();
12
16
  const containerRef = useRef(null);
17
+ const refreshLayoutBurstRef = useRef(() => { });
13
18
  const containerStyle = {
14
19
  width: "100%",
15
20
  height: "100%",
@@ -18,7 +23,7 @@ export function Terminal({ session, params, autoFocus = false, dismissOnView = t
18
23
  };
19
24
  useEffect(() => {
20
25
  const sessionId = session;
21
- const { attach, detach, focus, blur, setStatus, setPendingCommand, consumePendingCommand } = store.getState();
26
+ const { attach, detach, focus, blur, setStatus, setPendingCommand, consumePendingCommand, } = store.getState();
22
27
  attach(sessionId);
23
28
  if (pendingCommand !== undefined)
24
29
  setPendingCommand(sessionId, pendingCommand);
@@ -32,15 +37,38 @@ export function Terminal({ session, params, autoFocus = false, dismissOnView = t
32
37
  // if the ref is repointed before teardown runs.
33
38
  const container = containerRef.current;
34
39
  const opts = config.terminalOptions ?? {};
40
+ const terminalTheme = {
41
+ ...resolveTerminalColors(opts.colors),
42
+ ...opts.theme,
43
+ ...resolveTerminalColors(colors),
44
+ ...theme,
45
+ background: theme?.background ??
46
+ (typeof colors === "object" ? colors.background : undefined) ??
47
+ opts.theme?.background ??
48
+ (typeof opts.colors === "object"
49
+ ? opts.colors.background
50
+ : undefined) ??
51
+ background ??
52
+ opts.background,
53
+ };
35
54
  const term = new XTerm({
36
- fontFamily: opts.fontFamily ?? '"IBM Plex Mono", monospace',
55
+ fontFamily: opts.fontFamily ?? '"IBM Plex Mono", "Noto Sans Symbols 2", monospace',
37
56
  fontSize: opts.fontSize ?? 13,
38
57
  cursorBlink: opts.cursorBlink ?? true,
39
- theme: { background: "transparent" },
58
+ theme: terminalTheme,
40
59
  });
41
60
  const fitAddon = new FitAddon();
42
61
  term.loadAddon(fitAddon);
43
62
  term.open(container);
63
+ for (const selector of [
64
+ ".xterm-viewport",
65
+ ".xterm-scrollable-element",
66
+ ".xterm-screen",
67
+ ]) {
68
+ container
69
+ .querySelector(selector)
70
+ ?.style.setProperty("background-color", "transparent", "important");
71
+ }
44
72
  let lastFitSize = fitTerminal(container, term, fitAddon, null);
45
73
  let lastSentSize = null;
46
74
  // If a bell lands while you're actively looking at this session — its terminal
@@ -58,6 +86,7 @@ export function Terminal({ session, params, autoFocus = false, dismissOnView = t
58
86
  else if (term.textarea) {
59
87
  term.textarea.tabIndex = -1;
60
88
  }
89
+ let scheduleLayoutRefreshBurst = () => { };
61
90
  // Subscribe to store focus changes so external callers (focus/blur actions) work.
62
91
  const unsubFocus = store.subscribe((state, prev) => {
63
92
  const isFocused = state.focusedSlot === sessionId;
@@ -67,6 +96,7 @@ export function Terminal({ session, params, autoFocus = false, dismissOnView = t
67
96
  if (term.textarea)
68
97
  term.textarea.tabIndex = 0;
69
98
  term.focus();
99
+ scheduleLayoutRefreshBurst();
70
100
  }
71
101
  else {
72
102
  term.blur();
@@ -79,15 +109,21 @@ export function Terminal({ session, params, autoFocus = false, dismissOnView = t
79
109
  const onFocus = () => {
80
110
  focus(sessionId);
81
111
  markSeen();
112
+ scheduleLayoutRefreshBurst();
82
113
  };
83
114
  const onBlur = () => blur(sessionId);
84
115
  term.textarea?.addEventListener("focus", onFocus);
85
116
  term.textarea?.addEventListener("blur", onBlur);
86
- // Ctrl+Shift+Enter exits terminal focus. Handled here (capture on the container,
117
+ // The configured chord exits terminal focus. Handled here (capture on the container,
87
118
  // before xterm sees it) because global capture listeners don't reliably fire for
88
119
  // Enter when xterm has focus. We blur and hand control back to the host.
89
120
  const onExitChord = (e) => {
90
- if (e.key === "Enter" && e.shiftKey && e.ctrlKey) {
121
+ const isExitChord = e.key === "Enter" &&
122
+ e.ctrlKey &&
123
+ !e.altKey &&
124
+ !e.metaKey &&
125
+ (exitFocusChord === "ctrl-shift-enter" ? e.shiftKey : !e.shiftKey);
126
+ if (isExitChord) {
91
127
  e.preventDefault();
92
128
  e.stopPropagation();
93
129
  blur(sessionId);
@@ -103,6 +139,7 @@ export function Terminal({ session, params, autoFocus = false, dismissOnView = t
103
139
  let socket = null;
104
140
  let reconnectTimer = null;
105
141
  let refreshFrame = null;
142
+ const refreshTimers = new Set();
106
143
  let disposed = false;
107
144
  let attachedBefore = false;
108
145
  // App-level heartbeat. A half-open socket (laptop sleep/wake, Wi-Fi change, a
@@ -137,7 +174,9 @@ export function Terminal({ session, params, autoFocus = false, dismissOnView = t
137
174
  try {
138
175
  socket?.close();
139
176
  }
140
- catch { /* already closing */ }
177
+ catch {
178
+ /* already closing */
179
+ }
141
180
  }, HEARTBEAT_PONG_GRACE_MS);
142
181
  }
143
182
  }, HEARTBEAT_INTERVAL_MS);
@@ -197,7 +236,10 @@ export function Terminal({ session, params, autoFocus = false, dismissOnView = t
197
236
  if (nextSize === null)
198
237
  return;
199
238
  lastFitSize = nextSize;
200
- if (lastSentSize?.cols === nextSize.cols && lastSentSize.rows === nextSize.rows)
239
+ if (lastSentSize?.cols === nextSize.cols &&
240
+ lastSentSize.rows === nextSize.rows)
241
+ return;
242
+ if (socket?.readyState !== WebSocket.OPEN)
201
243
  return;
202
244
  lastSentSize = nextSize;
203
245
  send({ type: "resize", cols: nextSize.cols, rows: nextSize.rows });
@@ -215,9 +257,26 @@ export function Terminal({ session, params, autoFocus = false, dismissOnView = t
215
257
  return;
216
258
  refreshFrame = requestAnimationFrame(refreshVisibleRows);
217
259
  };
260
+ const refreshAfterDelay = (ms) => {
261
+ const timer = setTimeout(() => {
262
+ refreshTimers.delete(timer);
263
+ refreshAfterLayout();
264
+ }, ms);
265
+ refreshTimers.add(timer);
266
+ };
267
+ const refreshLayoutBurst = () => {
268
+ refreshAfterLayout();
269
+ refreshAfterDelay(50);
270
+ refreshAfterDelay(150);
271
+ refreshAfterDelay(500);
272
+ refreshAfterDelay(1000);
273
+ refreshAfterDelay(2000);
274
+ };
275
+ scheduleLayoutRefreshBurst = refreshLayoutBurst;
276
+ refreshLayoutBurstRef.current = refreshLayoutBurst;
218
277
  const refreshWhenVisible = () => {
219
278
  if (document.visibilityState === "visible")
220
- refreshAfterLayout();
279
+ refreshLayoutBurst();
221
280
  };
222
281
  const terminalWriteQueue = createTerminalWriteQueue(term, refreshAfterLayout);
223
282
  const connect = () => {
@@ -240,6 +299,7 @@ export function Terminal({ session, params, autoFocus = false, dismissOnView = t
240
299
  attachedBefore = true;
241
300
  setStatus(sessionId, "ready");
242
301
  fitAndSendResize();
302
+ refreshLayoutBurst();
243
303
  // Run the queued command once, on the very first attach. Wait for the prompt
244
304
  // to finish painting (see noteOutput/flushPendingCmd) so it lands on the real
245
305
  // prompt instead of racing shell startup. The cap timer is a backstop.
@@ -262,7 +322,9 @@ export function Terminal({ session, params, autoFocus = false, dismissOnView = t
262
322
  if (msg.type === "pong")
263
323
  onPong();
264
324
  }
265
- catch { /* unknown control frame — ignore */ }
325
+ catch {
326
+ /* unknown control frame — ignore */
327
+ }
266
328
  }
267
329
  return;
268
330
  }
@@ -288,20 +350,39 @@ export function Terminal({ session, params, autoFocus = false, dismissOnView = t
288
350
  };
289
351
  };
290
352
  connect();
291
- term.onData((data) => send({ type: "input", data }));
353
+ term.onData((data) => {
354
+ fitAndSendResize();
355
+ send({ type: "input", data });
356
+ });
292
357
  const observer = new ResizeObserver(refreshAfterLayout);
293
358
  observer.observe(container);
294
359
  container.addEventListener("pointerdown", refreshAfterLayout, true);
295
- container.addEventListener("wheel", refreshAfterLayout, { capture: true, passive: true });
360
+ container.addEventListener("wheel", refreshAfterLayout, {
361
+ capture: true,
362
+ passive: true,
363
+ });
296
364
  window.addEventListener("focus", refreshAfterLayout);
297
365
  window.addEventListener("pageshow", refreshAfterLayout);
298
366
  document.addEventListener("visibilitychange", refreshWhenVisible);
367
+ document.fonts?.ready
368
+ .then(() => {
369
+ if (disposed)
370
+ return;
371
+ // xterm measures cells when opened; loading a web font does not remeasure
372
+ // them. A harmless font-family change asks xterm to measure again.
373
+ term.options.fontFamily = `${term.options.fontFamily} `;
374
+ refreshLayoutBurst();
375
+ })
376
+ .catch(() => { });
299
377
  return () => {
300
378
  disposed = true;
301
379
  if (reconnectTimer !== null)
302
380
  clearTimeout(reconnectTimer);
303
381
  if (refreshFrame !== null)
304
382
  cancelAnimationFrame(refreshFrame);
383
+ for (const timer of refreshTimers)
384
+ clearTimeout(timer);
385
+ refreshTimers.clear();
305
386
  stopHeartbeat();
306
387
  clearAutoType();
307
388
  blur(sessionId);
@@ -323,8 +404,12 @@ export function Terminal({ session, params, autoFocus = false, dismissOnView = t
323
404
  term.textarea?.removeEventListener("blur", onBlur);
324
405
  terminalWriteQueue.dispose();
325
406
  term.dispose();
407
+ refreshLayoutBurstRef.current = () => { };
326
408
  };
327
409
  }, [session]); // eslint-disable-line react-hooks/exhaustive-deps
328
- return _jsx("div", { ref: containerRef, className: className, style: containerStyle });
410
+ useEffect(() => {
411
+ if (visible)
412
+ refreshLayoutBurstRef.current();
413
+ }, [visible]);
414
+ return (_jsx("div", { ref: containerRef, className: className, style: containerStyle }));
329
415
  }
330
- //# sourceMappingURL=Terminal.js.map
@@ -0,0 +1,8 @@
1
+ import type { TermkitStore } from "./createTermkitStore.js";
2
+ import type { TermkitConfig } from "./types.js";
3
+ export interface TermkitContextValue {
4
+ store: TermkitStore;
5
+ config: TermkitConfig;
6
+ }
7
+ export declare const TermkitContext: import("react").Context<TermkitContextValue | null>;
8
+ //# sourceMappingURL=TermkitContext.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"TermkitContext.d.ts","sourceRoot":"","sources":["../src/TermkitContext.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAC;AAC5D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAEhD,MAAM,WAAW,mBAAmB;IAClC,KAAK,EAAE,YAAY,CAAC;IACpB,MAAM,EAAE,aAAa,CAAC;CACvB;AAED,eAAO,MAAM,cAAc,qDAAkD,CAAC"}
@@ -0,0 +1,2 @@
1
+ import { createContext } from "react";
2
+ export const TermkitContext = createContext(null);
@@ -0,0 +1,8 @@
1
+ import { type ReactNode } from "react";
2
+ import type { TermkitConfig } from "./types.js";
3
+ export interface TermkitProviderProps {
4
+ config: TermkitConfig;
5
+ children: ReactNode;
6
+ }
7
+ export declare const TermkitProvider: ({ config, children }: TermkitProviderProps) => import("react").JSX.Element;
8
+ //# sourceMappingURL=TermkitProvider.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"TermkitProvider.d.ts","sourceRoot":"","sources":["../src/TermkitProvider.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAuB,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAG5D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAEhD,MAAM,WAAW,oBAAoB;IACnC,MAAM,EAAE,aAAa,CAAC;IACtB,QAAQ,EAAE,SAAS,CAAC;CACrB;AAOD,eAAO,MAAM,eAAe,GAAI,sBAAsB,oBAAoB,gCAuCzE,CAAC"}
@@ -1,20 +1,20 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
- import { createContext, useContext, useEffect, useRef } from "react";
3
- import { createTermkitStore } from "./store.js";
4
- const TermkitContext = createContext(null);
2
+ import { useEffect, useState } from "react";
3
+ import { createTermkitStore } from "./createTermkitStore.js";
4
+ import { TermkitContext } from "./TermkitContext.js";
5
5
  // Holds the global session registry and config for everything below it. Mount it
6
6
  // once near your app root. The registry lives here (not in each <Terminal>), which
7
7
  // is what makes sessions outlive the terminals that render them: a <Terminal> is
8
8
  // just a view onto a session, and the provider's poller keeps the live list fresh
9
9
  // whether or not any terminal is currently mounted for it.
10
- export function TermkitProvider({ config, children }) {
10
+ export const TermkitProvider = ({ config, children }) => {
11
11
  // Create the store once. Config is captured at first render; treat it as stable
12
12
  // (define it outside render or memoize it host-side, like any context value).
13
- const valueRef = useRef(null);
14
- if (valueRef.current === null) {
15
- valueRef.current = { store: createTermkitStore(config.transport), config };
16
- }
17
- const { store } = valueRef.current;
13
+ const [value] = useState(() => ({
14
+ store: createTermkitStore(config.transport),
15
+ config,
16
+ }));
17
+ const { store } = value;
18
18
  // The single poller: one fetch loop publishes the live snapshot every consumer
19
19
  // reads. Cheap on the daemon (an in-memory map read), so a couple of seconds
20
20
  // keeps bells and counts responsive without being chatty.
@@ -39,12 +39,5 @@ export function TermkitProvider({ config, children }) {
39
39
  };
40
40
  // config.transport is captured once with the store; re-poll only if cadence changes.
41
41
  }, [intervalMs, store, config.transport]);
42
- return _jsx(TermkitContext.Provider, { value: valueRef.current, children: children });
43
- }
44
- export function useTermkitContext() {
45
- const ctx = useContext(TermkitContext);
46
- if (!ctx)
47
- throw new Error("useTermkit must be used within a <TermkitProvider>");
48
- return ctx;
49
- }
50
- //# sourceMappingURL=context.js.map
42
+ return (_jsx(TermkitContext.Provider, { value: value, children: children }));
43
+ };
@@ -1 +1 @@
1
- {"version":3,"file":"createTerminalWriteQueue.d.ts","sourceRoot":"","sources":["../src/createTerminalWriteQueue.ts"],"names":[],"mappings":"AAAA,UAAU,gBAAgB;IACxB,KAAK,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,IAAI,KAAK,IAAI,CAAA;CACrD;AAED,eAAO,MAAM,wBAAwB,GAAI,MAAM,gBAAgB,EAAE,aAAa,MAAM,IAAI;gBAmBxE,MAAM;;;CAarB,CAAA"}
1
+ {"version":3,"file":"createTerminalWriteQueue.d.ts","sourceRoot":"","sources":["../src/createTerminalWriteQueue.ts"],"names":[],"mappings":"AAAA,UAAU,gBAAgB;IACxB,KAAK,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,IAAI,KAAK,IAAI,CAAC;CACtD;AAED,eAAO,MAAM,wBAAwB,GACnC,MAAM,gBAAgB,EACtB,aAAa,MAAM,IAAI;gBAoBT,MAAM;;;CAarB,CAAC"}
@@ -9,8 +9,8 @@ export const createTerminalWriteQueue = (term, afterWrite) => {
9
9
  chunks = [];
10
10
  isWriting = true;
11
11
  term.write(data, () => {
12
- afterWrite?.();
13
12
  isWriting = false;
13
+ afterWrite?.();
14
14
  drain();
15
15
  });
16
16
  };
@@ -30,4 +30,3 @@ export const createTerminalWriteQueue = (term, afterWrite) => {
30
30
  },
31
31
  };
32
32
  };
33
- //# sourceMappingURL=createTerminalWriteQueue.js.map
@@ -23,4 +23,4 @@ export interface TermkitState {
23
23
  }
24
24
  export type TermkitStore = StoreApi<TermkitState>;
25
25
  export declare function createTermkitStore(transport: TermkitTransport): TermkitStore;
26
- //# sourceMappingURL=store.d.ts.map
26
+ //# sourceMappingURL=createTermkitStore.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"createTermkitStore.d.ts","sourceRoot":"","sources":["../src/createTermkitStore.ts"],"names":[],"mappings":"AAAA,OAAO,EAAe,KAAK,QAAQ,EAAE,MAAM,iBAAiB,CAAC;AAC7D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AACvD,OAAO,KAAK,EAAE,aAAa,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAOlE,MAAM,WAAW,YAAY;IAC3B,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,aAAa,CAAC;CACvB;AAED,MAAM,WAAW,YAAY;IAE3B,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;IAEpC,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAI3B,eAAe,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAIxC,IAAI,EAAE,WAAW,EAAE,CAAC;IAGpB,MAAM,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,CAAC;IACpC,MAAM,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,CAAC;IACpC,SAAS,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,aAAa,KAAK,IAAI,CAAC;IAG9D,KAAK,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,CAAC;IACnC,IAAI,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,CAAC;IAGlC,iBAAiB,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IAChE,qBAAqB,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,MAAM,GAAG,IAAI,CAAC;IAG5D,IAAI,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,CAAC;IAClC,WAAW,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,CAAC;IAGzC,OAAO,EAAE,CAAC,IAAI,EAAE,WAAW,EAAE,KAAK,IAAI,CAAC;CACxC;AAED,MAAM,MAAM,YAAY,GAAG,QAAQ,CAAC,YAAY,CAAC,CAAC;AAElD,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,gBAAgB,GAAG,YAAY,CAkG5E"}
@@ -19,7 +19,10 @@ export function createTermkitStore(transport) {
19
19
  return s;
20
20
  const slots = { ...s.slots };
21
21
  delete slots[sessionId];
22
- return { slots, focusedSlot: s.focusedSlot === sessionId ? null : s.focusedSlot };
22
+ return {
23
+ slots,
24
+ focusedSlot: s.focusedSlot === sessionId ? null : s.focusedSlot,
25
+ };
23
26
  });
24
27
  },
25
28
  setStatus(sessionId, status) {
@@ -34,10 +37,14 @@ export function createTermkitStore(transport) {
34
37
  set({ focusedSlot: sessionId });
35
38
  },
36
39
  blur(sessionId) {
37
- set((s) => ({ focusedSlot: s.focusedSlot === sessionId ? null : s.focusedSlot }));
40
+ set((s) => ({
41
+ focusedSlot: s.focusedSlot === sessionId ? null : s.focusedSlot,
42
+ }));
38
43
  },
39
44
  setPendingCommand(sessionId, command) {
40
- set((s) => ({ pendingCommands: { ...s.pendingCommands, [sessionId]: command } }));
45
+ set((s) => ({
46
+ pendingCommands: { ...s.pendingCommands, [sessionId]: command },
47
+ }));
41
48
  },
42
49
  consumePendingCommand(sessionId) {
43
50
  const command = get().pendingCommands[sessionId];
@@ -74,7 +81,7 @@ export function createTermkitStore(transport) {
74
81
  // Optimistically clear attention so the badge goes away immediately; the
75
82
  // next poll confirms it.
76
83
  set((s) => ({
77
- live: s.live.map((l) => (l.sessionId === sessionId ? { ...l, attention: false } : l)),
84
+ live: s.live.map((l) => l.sessionId === sessionId ? { ...l, attention: false } : l),
78
85
  }));
79
86
  },
80
87
  setLive(live) {
@@ -82,4 +89,3 @@ export function createTermkitStore(transport) {
82
89
  },
83
90
  }));
84
91
  }
85
- //# sourceMappingURL=store.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"fitTerminal.d.ts","sourceRoot":"","sources":["../src/fitTerminal.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAA;AAChD,OAAO,KAAK,EAAE,QAAQ,IAAI,KAAK,EAAE,MAAM,cAAc,CAAA;AAErD,UAAU,YAAY;IACpB,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,MAAM,CAAA;CACb;AAED,eAAO,MAAM,WAAW,GACtB,WAAW,WAAW,EACtB,MAAM,KAAK,EACX,UAAU,QAAQ,EAClB,UAAU,YAAY,GAAG,IAAI,KAC5B,YAAY,GAAG,IASjB,CAAA"}
1
+ {"version":3,"file":"fitTerminal.d.ts","sourceRoot":"","sources":["../src/fitTerminal.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AACjD,OAAO,KAAK,EAAE,QAAQ,IAAI,KAAK,EAAE,MAAM,cAAc,CAAC;AAEtD,UAAU,YAAY;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;CACd;AAED,eAAO,MAAM,WAAW,GACtB,WAAW,WAAW,EACtB,MAAM,KAAK,EACX,UAAU,QAAQ,EAClB,UAAU,YAAY,GAAG,IAAI,KAC5B,YAAY,GAAG,IAejB,CAAC"}
@@ -1,10 +1,13 @@
1
1
  export const fitTerminal = (container, term, fitAddon, lastSize) => {
2
2
  if (container.clientWidth === 0 || container.clientHeight === 0)
3
3
  return lastSize;
4
- fitAddon.fit();
5
- const nextSize = { cols: term.cols, rows: term.rows };
4
+ const nextSize = fitAddon.proposeDimensions();
5
+ if (!nextSize)
6
+ return lastSize;
7
+ if (term.cols !== nextSize.cols || term.rows !== nextSize.rows) {
8
+ term.resize(nextSize.cols, nextSize.rows);
9
+ }
6
10
  if (lastSize?.cols === nextSize.cols && lastSize.rows === nextSize.rows)
7
11
  return lastSize;
8
12
  return nextSize;
9
13
  };
10
- //# sourceMappingURL=fitTerminal.js.map
package/dist/index.d.ts CHANGED
@@ -1,10 +1,21 @@
1
- export { TermkitProvider } from "./context.js";
2
- export type { TermkitProviderProps } from "./context.js";
1
+ export { TermkitProvider } from "./TermkitProvider.js";
2
+ export type { TermkitProviderProps } from "./TermkitProvider.js";
3
3
  export { Terminal } from "./Terminal.js";
4
4
  export type { TerminalProps } from "./Terminal.js";
5
- export { useSessions, useSession, useRingingSessions, useIsRinging, useSessionStatus, useFocusedSession, useIsFocused, useSlots, useDanglingSessions, useTermkitActions, } from "./hooks.js";
6
- export type { TermkitActions } from "./hooks.js";
7
- export type { TerminalSlot } from "./store.js";
5
+ export { useSessions } from "./useSessions.js";
6
+ export { useSession } from "./useSession.js";
7
+ export { useRingingSessions } from "./useRingingSessions.js";
8
+ export { useIsRinging } from "./useIsRinging.js";
9
+ export { useSessionStatus } from "./useSessionStatus.js";
10
+ export { useFocusedSession } from "./useFocusedSession.js";
11
+ export { useIsFocused } from "./useIsFocused.js";
12
+ export { useSlots } from "./useSlots.js";
13
+ export { useDanglingSessions } from "./useDanglingSessions.js";
14
+ export { useTermkitActions } from "./useTermkitActions.js";
15
+ export type { TermkitActions } from "./useTermkitActions.js";
16
+ export type { TerminalSlot } from "./createTermkitStore.js";
8
17
  export type { TermkitConfig, TermkitTransport, TermkitTerminalOptions, SessionStatus, LiveSession, } from "./types.js";
18
+ export { terminalColorSchemes } from "./terminalColors.js";
19
+ export type { TerminalColors, TerminalColorSchemeName, } from "./terminalColors.js";
9
20
  export { generateSessionName, SESSION_NAMES } from "@mp-lb/termkit-core";
10
21
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAQA,OAAO,EAAE,eAAe,EAAE,MAAM,cAAc,CAAA;AAC9C,YAAY,EAAE,oBAAoB,EAAE,MAAM,cAAc,CAAA;AACxD,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAA;AACxC,YAAY,EAAE,aAAa,EAAE,MAAM,eAAe,CAAA;AAElD,OAAO,EACL,WAAW,EACX,UAAU,EACV,kBAAkB,EAClB,YAAY,EACZ,gBAAgB,EAChB,iBAAiB,EACjB,YAAY,EACZ,QAAQ,EACR,mBAAmB,EACnB,iBAAiB,GAClB,MAAM,YAAY,CAAA;AACnB,YAAY,EAAE,cAAc,EAAE,MAAM,YAAY,CAAA;AAChD,YAAY,EAAE,YAAY,EAAE,MAAM,YAAY,CAAA;AAE9C,YAAY,EACV,aAAa,EACb,gBAAgB,EAChB,sBAAsB,EACtB,aAAa,EACb,WAAW,GACZ,MAAM,YAAY,CAAA;AAGnB,OAAO,EAAE,mBAAmB,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAQA,OAAO,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AACvD,YAAY,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AACjE,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AACzC,YAAY,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAEnD,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAC/C,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAC7C,OAAO,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAC;AAC7D,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACjD,OAAO,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AACzD,OAAO,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAC3D,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACjD,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AACzC,OAAO,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAC;AAC/D,OAAO,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAC3D,YAAY,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAC7D,YAAY,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAC;AAE5D,YAAY,EACV,aAAa,EACb,gBAAgB,EAChB,sBAAsB,EACtB,aAAa,EACb,WAAW,GACZ,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAC;AAC3D,YAAY,EACV,cAAc,EACd,uBAAuB,GACxB,MAAM,qBAAqB,CAAC;AAG7B,OAAO,EAAE,mBAAmB,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC"}