@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 +113 -8
- package/dist/index.cjs +328 -14
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +97 -2
- package/dist/index.d.ts +97 -2
- package/dist/index.js +328 -17
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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.
|
|
20
|
-
> `useTerminalSession` are in place
|
|
21
|
-
>
|
|
22
|
-
>
|
|
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`),
|
|
109
|
-
(`data-fancy-terminal` +
|
|
110
|
-
|
|
111
|
-
|
|
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
|
-
|
|
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 (
|
|
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(
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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;
|