@particle-academy/fancy-term 0.1.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -16,10 +16,13 @@ Like every Fancy UI component it serves two surfaces at once:
16
16
  Sexy by default via a Fancy dark theme drawn from the react-fancy Tailwind v4
17
17
  tokens.
18
18
 
19
- > **Status:** 0.1.0. `<Terminal>` + `useTerminal` / `useTerminalFit` /
20
- > `useTerminalSession` are in place. The `registerTerminalBridge` MCP bridge
21
- > (`terminal_read` / `terminal_write` / `terminal_run`) and the trust‑but‑verify
22
- > staged-command affordance ship next, in `@particle-academy/agent-integrations`.
19
+ > **Status:** 0.2.0. `<Terminal>` + `useTerminal` / `useTerminalFit` /
20
+ > `useTerminalSession` are in place, plus **shell / profile switching** (the
21
+ > `<ShellSwitcher>` component, controlled `shells` / `activeShell` props, and the
22
+ > session hook's `switchShell`). The `registerTerminalBridge` MCP bridge
23
+ > (`terminal_read` / `terminal_write` / `terminal_run` / `terminal_set_shell`) and
24
+ > the trust‑but‑verify staged-command affordance ship in
25
+ > `@particle-academy/agent-integrations`.
23
26
 
24
27
  ## Install
25
28
 
@@ -70,6 +73,10 @@ fancy-query's `useFancyStream`.
70
73
  | `cursorBlink` / `cursorStyle` | | `"block" \| "underline" \| "bar"` |
71
74
  | `fontFamily` / `fontSize` / `scrollback` | | |
72
75
  | `initialOutput` | `string` | written once on mount (uncontrolled use) |
76
+ | `shells` | `ShellProfile[]` | the shells / profiles the host offers (see [Switching shells](#switching-shells)) |
77
+ | `activeShell` | `string` | controlled selected shell id (omit for uncontrolled) |
78
+ | `onShellChange` | `(id, profile) => void` | fired when the user / `setShell` switches |
79
+ | `showShellBar` | `boolean` | render the `<ShellSwitcher>` toolbar above the surface (default `false`) |
73
80
 
74
81
  The ref exposes a `TerminalHandle`:
75
82
 
@@ -78,9 +85,105 @@ const term = useRef<TerminalHandle>(null);
78
85
  // term.current.write / writeln / clear / reset / fit / focus
79
86
  // term.current.getBuffer() → the visible buffer as text (what an agent "sees")
80
87
  // term.current.getSelection() → current selection
88
+ // term.current.setShell("pwsh") → switch the active shell (fires onShellChange)
89
+ // term.current.getShell() → the active shell id
81
90
  // term.current.xterm → the raw xterm.js instance (escape hatch)
82
91
  ```
83
92
 
93
+ ## Switching shells
94
+
95
+ fancy-term is a **frontend wrapper** — it never spawns a shell. It owns the
96
+ *selected-shell state, the UI, and the change events*; the **host reacts** by
97
+ reconnecting its PTY / command backend to the chosen profile. That separation
98
+ keeps the wrapper portable across any backend.
99
+
100
+ A `ShellProfile` is fully JSON-friendly (an agent can emit one):
101
+
102
+ ```ts
103
+ interface ShellProfile {
104
+ id: string; // stable key, e.g. "powershell"
105
+ label: string; // display, e.g. "PowerShell"
106
+ icon?: string; // optional short glyph / emoji / single char
107
+ command?: string; // host hint, e.g. "pwsh" / "cmd.exe"
108
+ args?: string[]; // host hint
109
+ cwd?: string; // host hint
110
+ }
111
+ ```
112
+
113
+ `command` / `args` / `cwd` are **host hints only** — fancy-term passes the choice
114
+ along; your backend decides what to launch. Spread the built-in presets and
115
+ filter to what your host actually offers:
116
+
117
+ ```ts
118
+ import { BUILTIN_SHELLS } from "@particle-academy/fancy-term";
119
+ // cmd · powershell · pwsh · git-bash · bash · zsh
120
+ const shells = BUILTIN_SHELLS.filter((s) => ["pwsh", "git-bash"].includes(s.id));
121
+ ```
122
+
123
+ ### On the `<Terminal>` (controlled)
124
+
125
+ ```tsx
126
+ const [shell, setShell] = useState("pwsh");
127
+
128
+ <div style={{ height: 360 }}>
129
+ <Terminal
130
+ output={out}
131
+ onData={(d) => backend.send(d)}
132
+ shells={shells}
133
+ activeShell={shell}
134
+ onShellChange={(id) => {
135
+ setShell(id);
136
+ backend.reconnect(id); // host reconnects its PTY to the chosen shell
137
+ }}
138
+ showShellBar // opt-in: render the built-in switcher toolbar
139
+ />
140
+ </div>
141
+ ```
142
+
143
+ The root carries `data-fancy-terminal-shell="<id>"` so an agent (or an MCP bridge)
144
+ can read the active shell without DOM-guessing. Omit `activeShell` for an
145
+ uncontrolled terminal that tracks the selection internally.
146
+
147
+ ### Standalone `<ShellSwitcher>`
148
+
149
+ Place the picker anywhere — it's a self-contained, accessible (Arrow / Enter /
150
+ Esc), Fancy-dark-themed dropdown with zero third-party deps:
151
+
152
+ ```tsx
153
+ import { ShellSwitcher } from "@particle-academy/fancy-term";
154
+
155
+ <ShellSwitcher
156
+ shells={shells}
157
+ value={shell}
158
+ onChange={(id, profile) => backend.reconnect(profile)}
159
+ />
160
+ ```
161
+
162
+ Stable handles: the root carries `data-fancy-shell-switcher`, each option carries
163
+ `data-shell-id`.
164
+
165
+ ### In the session hook
166
+
167
+ `useTerminalSession` accepts a `shell` and exposes `switchShell(id)`. Switching
168
+ **resets the buffer** and calls the transport's optional `connect(shell)` so the
169
+ host can (re)wire its backend. Transports that don't care about shells just omit
170
+ `connect` — fully backward compatible.
171
+
172
+ ```tsx
173
+ const session = useTerminalSession({
174
+ shell: "pwsh",
175
+ transport: {
176
+ send: (d) => backend.send(d),
177
+ subscribe: (onChunk) => backend.onOutput(onChunk),
178
+ connect: (shell) => backend.connect(shell), // (re)connect to the chosen shell
179
+ },
180
+ });
181
+
182
+ // later — reset + reconnect to a new shell:
183
+ session.switchShell("git-bash");
184
+ <Terminal output={session.output} onData={session.sendData} />
185
+ ```
186
+
84
187
  ## Hooks
85
188
 
86
189
  ```tsx
@@ -105,10 +208,12 @@ const session = useTerminalSession({
105
208
 
106
209
  ## Human+ contract
107
210
 
108
- `<Terminal>` is **controlled** (`value`/`onData`), carries a **stable handle**
109
- (`data-fancy-terminal` + the ref API), takes **JSON-friendly** props, and is
110
- **bridgeable** — `registerTerminalBridge` (in `agent-integrations`, shipping next)
111
- maps `terminal_read` / `terminal_write` / `terminal_run` onto the handle, wraps
211
+ `<Terminal>` is **controlled** (`value`/`onData`, `activeShell`/`onShellChange`),
212
+ carries **stable handles** (`data-fancy-terminal` + `data-fancy-terminal-shell`,
213
+ `data-fancy-shell-switcher` / `data-shell-id`, plus the ref API), takes
214
+ **JSON-friendly** props (including `ShellProfile[]`), and is **bridgeable** —
215
+ `registerTerminalBridge` (in `agent-integrations`) maps `terminal_read` /
216
+ `terminal_write` / `terminal_run` / `terminal_set_shell` onto the handle, wraps
112
217
  mutations so every write broadcasts `AgentActivity`, and supports a staged
113
218
  "agent proposes → human confirms" mode for destructive commands.
114
219
 
package/dist/index.cjs CHANGED
@@ -80,14 +80,26 @@ function useTerminal(containerRef, options = {}) {
80
80
  clear: () => xtermRef.current?.clear(),
81
81
  reset: () => xtermRef.current?.reset(),
82
82
  fit: () => {
83
+ const addon = fitRef.current;
84
+ if (!addon) return;
83
85
  try {
84
- fitRef.current?.fit();
86
+ const dims = addon.proposeDimensions();
87
+ if (!dims || !Number.isFinite(dims.cols) || !Number.isFinite(dims.rows) || dims.cols < 1 || dims.rows < 1) {
88
+ return;
89
+ }
90
+ addon.fit();
85
91
  } catch {
86
92
  }
87
93
  },
88
94
  focus: () => xtermRef.current?.focus(),
89
95
  getBuffer: () => readBuffer(xtermRef.current),
90
- getSelection: () => xtermRef.current?.getSelection() ?? ""
96
+ getSelection: () => xtermRef.current?.getSelection() ?? "",
97
+ // Shell selection is owned by the <Terminal> component layer (it needs the
98
+ // `shells` list + onShellChange). The headless engine is shell-agnostic, so
99
+ // these are no-ops here and get overridden when <Terminal> composes them.
100
+ setShell: () => {
101
+ },
102
+ getShell: () => void 0
91
103
  };
92
104
  }
93
105
  const handle = handleRef.current;
@@ -117,12 +129,7 @@ function useTerminal(containerRef, options = {}) {
117
129
  ({ cols, rows }) => optsRef.current.onResize?.({ cols, rows })
118
130
  );
119
131
  if (o.initialOutput) term.write(o.initialOutput);
120
- if ((o.fit ?? true) && el.clientWidth > 0 && el.clientHeight > 0) {
121
- try {
122
- fitAddon.fit();
123
- } catch {
124
- }
125
- }
132
+ if (o.fit ?? true) handle.fit();
126
133
  return () => {
127
134
  dataSub.dispose();
128
135
  resizeSub.dispose();
@@ -145,12 +152,231 @@ function useTerminal(containerRef, options = {}) {
145
152
  return handle;
146
153
  }
147
154
 
155
+ // src/types.ts
156
+ function resolveShell(shells, id) {
157
+ if (!shells || id === void 0) return void 0;
158
+ return shells.find((s) => s.id === id);
159
+ }
160
+ var BUILTIN_SHELLS = [
161
+ { id: "cmd", label: "Command Prompt", icon: ">_", command: "cmd.exe" },
162
+ { id: "powershell", label: "Windows PowerShell", icon: "PS", command: "powershell.exe" },
163
+ { id: "pwsh", label: "PowerShell", icon: "PS", command: "pwsh" },
164
+ {
165
+ id: "git-bash",
166
+ label: "Git Bash",
167
+ icon: "",
168
+ command: "C:\\Program Files\\Git\\bin\\bash.exe",
169
+ args: ["--login", "-i"]
170
+ },
171
+ { id: "bash", label: "Bash", icon: "$", command: "bash" },
172
+ { id: "zsh", label: "Zsh", icon: "%", command: "zsh" }
173
+ ];
174
+ var PANEL_BG = "#18181b";
175
+ var BORDER = "#3f3f46";
176
+ var FG = "#e4e4e7";
177
+ var MUTED = "#a1a1aa";
178
+ var ACCENT = "#8b5cf6";
179
+ var HOVER_BG = "#27272a";
180
+ function ShellSwitcher({
181
+ shells,
182
+ value,
183
+ onChange,
184
+ className,
185
+ style,
186
+ disabled
187
+ }) {
188
+ const [open, setOpen] = react.useState(false);
189
+ const [activeIndex, setActiveIndex] = react.useState(0);
190
+ const rootRef = react.useRef(null);
191
+ const listId = react.useId();
192
+ const selected = resolveShell(shells, value);
193
+ const selectedIndex = selected ? shells.indexOf(selected) : -1;
194
+ react.useEffect(() => {
195
+ if (!open) return;
196
+ const onDocClick = (e) => {
197
+ if (rootRef.current && !rootRef.current.contains(e.target)) setOpen(false);
198
+ };
199
+ document.addEventListener("mousedown", onDocClick);
200
+ return () => document.removeEventListener("mousedown", onDocClick);
201
+ }, [open]);
202
+ const openMenu = react.useCallback(() => {
203
+ if (disabled) return;
204
+ setActiveIndex(selectedIndex >= 0 ? selectedIndex : 0);
205
+ setOpen(true);
206
+ }, [disabled, selectedIndex]);
207
+ const pick = react.useCallback(
208
+ (i) => {
209
+ const profile = shells[i];
210
+ if (!profile) return;
211
+ onChange(profile.id, profile);
212
+ setOpen(false);
213
+ },
214
+ [shells, onChange]
215
+ );
216
+ const onButtonKeyDown = (e) => {
217
+ if (disabled) return;
218
+ if (e.key === "ArrowDown" || e.key === "ArrowUp" || e.key === "Enter" || e.key === " ") {
219
+ e.preventDefault();
220
+ openMenu();
221
+ }
222
+ };
223
+ const onListKeyDown = (e) => {
224
+ if (e.key === "Escape") {
225
+ e.preventDefault();
226
+ setOpen(false);
227
+ return;
228
+ }
229
+ if (e.key === "ArrowDown") {
230
+ e.preventDefault();
231
+ setActiveIndex((i) => Math.min(shells.length - 1, i + 1));
232
+ } else if (e.key === "ArrowUp") {
233
+ e.preventDefault();
234
+ setActiveIndex((i) => Math.max(0, i - 1));
235
+ } else if (e.key === "Home") {
236
+ e.preventDefault();
237
+ setActiveIndex(0);
238
+ } else if (e.key === "End") {
239
+ e.preventDefault();
240
+ setActiveIndex(shells.length - 1);
241
+ } else if (e.key === "Enter" || e.key === " ") {
242
+ e.preventDefault();
243
+ pick(activeIndex);
244
+ }
245
+ };
246
+ const buttonStyle = {
247
+ display: "inline-flex",
248
+ alignItems: "center",
249
+ gap: 6,
250
+ height: 26,
251
+ padding: "0 8px",
252
+ fontSize: 12,
253
+ lineHeight: 1,
254
+ fontFamily: "inherit",
255
+ color: FG,
256
+ background: PANEL_BG,
257
+ border: `1px solid ${BORDER}`,
258
+ borderRadius: 6,
259
+ cursor: disabled ? "default" : "pointer",
260
+ opacity: disabled ? 0.6 : 1,
261
+ userSelect: "none"
262
+ };
263
+ return /* @__PURE__ */ jsxRuntime.jsxs(
264
+ "div",
265
+ {
266
+ ref: rootRef,
267
+ "data-fancy-shell-switcher": "",
268
+ "data-active-shell": value ?? void 0,
269
+ className,
270
+ style: { position: "relative", display: "inline-block", ...style },
271
+ children: [
272
+ /* @__PURE__ */ jsxRuntime.jsxs(
273
+ "button",
274
+ {
275
+ type: "button",
276
+ "aria-haspopup": "listbox",
277
+ "aria-expanded": open,
278
+ "aria-label": "Select shell",
279
+ disabled,
280
+ style: buttonStyle,
281
+ onClick: () => open ? setOpen(false) : openMenu(),
282
+ onKeyDown: onButtonKeyDown,
283
+ children: [
284
+ selected?.icon ? /* @__PURE__ */ jsxRuntime.jsx("span", { "aria-hidden": "true", style: { color: ACCENT }, children: selected.icon }) : null,
285
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: selected?.label ?? "Select shell" }),
286
+ /* @__PURE__ */ jsxRuntime.jsx("span", { "aria-hidden": "true", style: { color: MUTED, fontSize: 9 }, children: "\u25BC" })
287
+ ]
288
+ }
289
+ ),
290
+ open ? /* @__PURE__ */ jsxRuntime.jsx(
291
+ "ul",
292
+ {
293
+ role: "listbox",
294
+ id: listId,
295
+ "aria-label": "Shells",
296
+ tabIndex: -1,
297
+ autoFocus: true,
298
+ onKeyDown: onListKeyDown,
299
+ ref: (el) => el?.focus(),
300
+ style: {
301
+ position: "absolute",
302
+ zIndex: 50,
303
+ top: "calc(100% + 4px)",
304
+ left: 0,
305
+ minWidth: "100%",
306
+ margin: 0,
307
+ padding: 4,
308
+ listStyle: "none",
309
+ background: PANEL_BG,
310
+ border: `1px solid ${BORDER}`,
311
+ borderRadius: 8,
312
+ boxShadow: "0 8px 24px rgba(0,0,0,0.45)",
313
+ outline: "none"
314
+ },
315
+ children: shells.map((s, i) => {
316
+ const isSelected = s.id === value;
317
+ const isActive = i === activeIndex;
318
+ return /* @__PURE__ */ jsxRuntime.jsxs(
319
+ "li",
320
+ {
321
+ role: "option",
322
+ "aria-selected": isSelected,
323
+ "data-shell-id": s.id,
324
+ onMouseEnter: () => setActiveIndex(i),
325
+ onClick: () => pick(i),
326
+ style: {
327
+ display: "flex",
328
+ alignItems: "center",
329
+ gap: 8,
330
+ padding: "6px 8px",
331
+ fontSize: 12,
332
+ borderRadius: 5,
333
+ color: FG,
334
+ cursor: "pointer",
335
+ background: isActive ? HOVER_BG : "transparent"
336
+ },
337
+ children: [
338
+ /* @__PURE__ */ jsxRuntime.jsx(
339
+ "span",
340
+ {
341
+ "aria-hidden": "true",
342
+ style: {
343
+ width: 18,
344
+ textAlign: "center",
345
+ color: isSelected ? ACCENT : MUTED
346
+ },
347
+ children: s.icon ?? ""
348
+ }
349
+ ),
350
+ /* @__PURE__ */ jsxRuntime.jsx("span", { style: { flex: 1 }, children: s.label }),
351
+ isSelected ? /* @__PURE__ */ jsxRuntime.jsx("span", { "aria-hidden": "true", style: { color: ACCENT, fontSize: 11 }, children: "\u2713" }) : null
352
+ ]
353
+ },
354
+ s.id
355
+ );
356
+ })
357
+ }
358
+ ) : null
359
+ ]
360
+ }
361
+ );
362
+ }
363
+
148
364
  // src/output-diff.ts
149
365
  function diffOutput(written, next) {
150
366
  if (next === written) return null;
151
367
  if (next.startsWith(written)) return { reset: false, write: next.slice(written.length) };
152
368
  return { reset: true, write: next };
153
369
  }
370
+
371
+ // src/shell-select.ts
372
+ function decideShellSelect(shells, current, id) {
373
+ const profile = resolveShell(shells, id);
374
+ if (!profile) return null;
375
+ return { profile, changed: current !== id };
376
+ }
377
+ function decideShellSwitch(current, next) {
378
+ return { changed: current !== next };
379
+ }
154
380
  var Terminal = react.forwardRef(function Terminal2({
155
381
  output,
156
382
  theme,
@@ -166,11 +392,27 @@ var Terminal = react.forwardRef(function Terminal2({
166
392
  initialOutput,
167
393
  onData,
168
394
  onResize,
395
+ shells,
396
+ activeShell,
397
+ onShellChange,
398
+ showShellBar = false,
169
399
  className,
170
400
  style,
171
401
  ...rest
172
402
  }, ref) {
173
403
  const containerRef = react.useRef(null);
404
+ const isShellControlled = activeShell !== void 0;
405
+ const [internalShell, setInternalShell] = react.useState(void 0);
406
+ const shellId = isShellControlled ? activeShell : internalShell;
407
+ const shellStateRef = react.useRef({ shells, shellId, isShellControlled, onShellChange });
408
+ shellStateRef.current = { shells, shellId, isShellControlled, onShellChange };
409
+ const selectShell = react.useCallback((id) => {
410
+ const s = shellStateRef.current;
411
+ const decision = decideShellSelect(s.shells, s.shellId, id);
412
+ if (!decision) return;
413
+ if (!s.isShellControlled) setInternalShell(id);
414
+ s.onShellChange?.(id, decision.profile);
415
+ }, []);
174
416
  const handle = useTerminal(containerRef, {
175
417
  theme,
176
418
  rows,
@@ -188,7 +430,15 @@ var Terminal = react.forwardRef(function Terminal2({
188
430
  onData,
189
431
  onResize
190
432
  });
191
- react.useImperativeHandle(ref, () => handle, [handle]);
433
+ react.useImperativeHandle(
434
+ ref,
435
+ () => ({
436
+ ...handle,
437
+ setShell: selectShell,
438
+ getShell: () => shellStateRef.current.shellId
439
+ }),
440
+ [handle, selectShell]
441
+ );
192
442
  const written = react.useRef("");
193
443
  react.useEffect(() => {
194
444
  if (output === void 0) return;
@@ -198,38 +448,102 @@ var Terminal = react.forwardRef(function Terminal2({
198
448
  handle.write(change.write);
199
449
  written.current = output;
200
450
  }, [output, handle]);
201
- return /* @__PURE__ */ jsxRuntime.jsx(
451
+ const surface = /* @__PURE__ */ jsxRuntime.jsx(
202
452
  "div",
203
453
  {
204
454
  ref: containerRef,
205
455
  "data-fancy-terminal": "",
206
456
  "data-readonly": readOnly ? "" : void 0,
457
+ "data-fancy-terminal-shell": shellId ?? void 0,
458
+ style: showShellBar && shells ? { width: "100%", flex: 1, minHeight: 0 } : { width: "100%", height: "100%", ...style },
459
+ ...showShellBar && shells ? {} : rest
460
+ }
461
+ );
462
+ if (!showShellBar || !shells) return surface;
463
+ return /* @__PURE__ */ jsxRuntime.jsxs(
464
+ "div",
465
+ {
466
+ "data-fancy-terminal-shell": shellId ?? void 0,
207
467
  className,
208
- style: { width: "100%", height: "100%", ...style },
209
- ...rest
468
+ style: { display: "flex", flexDirection: "column", width: "100%", height: "100%", ...style },
469
+ ...rest,
470
+ children: [
471
+ /* @__PURE__ */ jsxRuntime.jsx(
472
+ "div",
473
+ {
474
+ style: {
475
+ display: "flex",
476
+ alignItems: "center",
477
+ gap: 8,
478
+ padding: "4px 6px",
479
+ background: "#09090b",
480
+ borderBottom: "1px solid #27272a"
481
+ },
482
+ children: /* @__PURE__ */ jsxRuntime.jsx(
483
+ ShellSwitcher,
484
+ {
485
+ shells,
486
+ value: shellId,
487
+ onChange: (id) => selectShell(id),
488
+ disabled: readOnly
489
+ }
490
+ )
491
+ }
492
+ ),
493
+ surface
494
+ ]
210
495
  }
211
496
  );
212
497
  });
213
498
  function useTerminalSession(options) {
214
- const [output, setOutput] = react.useState(options.initial ?? "");
499
+ const initial = options.initial ?? "";
500
+ const [output, setOutput] = react.useState(initial);
501
+ const [internalShell, setInternalShell] = react.useState(options.shell);
502
+ const shell = options.shell !== void 0 ? options.shell : internalShell;
215
503
  const transportRef = react.useRef(options.transport);
216
504
  transportRef.current = options.transport;
505
+ const initialRef = react.useRef(initial);
506
+ initialRef.current = initial;
217
507
  react.useEffect(() => {
218
508
  const unsub = transportRef.current.subscribe((chunk) => setOutput((o) => o + chunk));
219
509
  return () => {
220
510
  if (typeof unsub === "function") unsub();
221
511
  };
222
512
  }, []);
513
+ const connectedShell = react.useRef(void 0);
514
+ const mounted = react.useRef(false);
515
+ react.useEffect(() => {
516
+ if (!mounted.current) {
517
+ mounted.current = true;
518
+ connectedShell.current = shell;
519
+ void transportRef.current.connect?.(shell);
520
+ return;
521
+ }
522
+ if (!decideShellSwitch(connectedShell.current, shell).changed) return;
523
+ connectedShell.current = shell;
524
+ setOutput(initialRef.current);
525
+ void transportRef.current.connect?.(shell);
526
+ }, [shell]);
223
527
  const sendData = react.useCallback((data) => {
224
528
  void transportRef.current.send(data);
225
529
  }, []);
226
530
  const append = react.useCallback((chunk) => setOutput((o) => o + chunk), []);
227
531
  const clear = react.useCallback(() => setOutput(""), []);
228
- return { output, sendData, append, clear };
532
+ const switchShell = react.useCallback((id) => {
533
+ setInternalShell((cur) => cur === id ? cur : id);
534
+ if (!decideShellSwitch(connectedShell.current, id).changed) return;
535
+ connectedShell.current = id;
536
+ setOutput(initialRef.current);
537
+ void transportRef.current.connect?.(id);
538
+ }, []);
539
+ return { output, sendData, append, clear, shell, switchShell };
229
540
  }
230
541
 
542
+ exports.BUILTIN_SHELLS = BUILTIN_SHELLS;
543
+ exports.ShellSwitcher = ShellSwitcher;
231
544
  exports.Terminal = Terminal;
232
545
  exports.fancyDarkTheme = fancyDarkTheme;
546
+ exports.resolveShell = resolveShell;
233
547
  exports.useTerminal = useTerminal;
234
548
  exports.useTerminalFit = useTerminalFit;
235
549
  exports.useTerminalSession = useTerminalSession;