@particle-academy/fancy-term 0.1.0 → 0.2.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.
- package/README.md +113 -8
- package/dist/index.cjs +320 -7
- 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 +320 -10
- 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
|
@@ -87,7 +87,13 @@ function useTerminal(containerRef, options = {}) {
|
|
|
87
87
|
},
|
|
88
88
|
focus: () => xtermRef.current?.focus(),
|
|
89
89
|
getBuffer: () => readBuffer(xtermRef.current),
|
|
90
|
-
getSelection: () => xtermRef.current?.getSelection() ?? ""
|
|
90
|
+
getSelection: () => xtermRef.current?.getSelection() ?? "",
|
|
91
|
+
// Shell selection is owned by the <Terminal> component layer (it needs the
|
|
92
|
+
// `shells` list + onShellChange). The headless engine is shell-agnostic, so
|
|
93
|
+
// these are no-ops here and get overridden when <Terminal> composes them.
|
|
94
|
+
setShell: () => {
|
|
95
|
+
},
|
|
96
|
+
getShell: () => void 0
|
|
91
97
|
};
|
|
92
98
|
}
|
|
93
99
|
const handle = handleRef.current;
|
|
@@ -145,12 +151,231 @@ function useTerminal(containerRef, options = {}) {
|
|
|
145
151
|
return handle;
|
|
146
152
|
}
|
|
147
153
|
|
|
154
|
+
// src/types.ts
|
|
155
|
+
function resolveShell(shells, id) {
|
|
156
|
+
if (!shells || id === void 0) return void 0;
|
|
157
|
+
return shells.find((s) => s.id === id);
|
|
158
|
+
}
|
|
159
|
+
var BUILTIN_SHELLS = [
|
|
160
|
+
{ id: "cmd", label: "Command Prompt", icon: ">_", command: "cmd.exe" },
|
|
161
|
+
{ id: "powershell", label: "Windows PowerShell", icon: "PS", command: "powershell.exe" },
|
|
162
|
+
{ id: "pwsh", label: "PowerShell", icon: "PS", command: "pwsh" },
|
|
163
|
+
{
|
|
164
|
+
id: "git-bash",
|
|
165
|
+
label: "Git Bash",
|
|
166
|
+
icon: "",
|
|
167
|
+
command: "C:\\Program Files\\Git\\bin\\bash.exe",
|
|
168
|
+
args: ["--login", "-i"]
|
|
169
|
+
},
|
|
170
|
+
{ id: "bash", label: "Bash", icon: "$", command: "bash" },
|
|
171
|
+
{ id: "zsh", label: "Zsh", icon: "%", command: "zsh" }
|
|
172
|
+
];
|
|
173
|
+
var PANEL_BG = "#18181b";
|
|
174
|
+
var BORDER = "#3f3f46";
|
|
175
|
+
var FG = "#e4e4e7";
|
|
176
|
+
var MUTED = "#a1a1aa";
|
|
177
|
+
var ACCENT = "#8b5cf6";
|
|
178
|
+
var HOVER_BG = "#27272a";
|
|
179
|
+
function ShellSwitcher({
|
|
180
|
+
shells,
|
|
181
|
+
value,
|
|
182
|
+
onChange,
|
|
183
|
+
className,
|
|
184
|
+
style,
|
|
185
|
+
disabled
|
|
186
|
+
}) {
|
|
187
|
+
const [open, setOpen] = react.useState(false);
|
|
188
|
+
const [activeIndex, setActiveIndex] = react.useState(0);
|
|
189
|
+
const rootRef = react.useRef(null);
|
|
190
|
+
const listId = react.useId();
|
|
191
|
+
const selected = resolveShell(shells, value);
|
|
192
|
+
const selectedIndex = selected ? shells.indexOf(selected) : -1;
|
|
193
|
+
react.useEffect(() => {
|
|
194
|
+
if (!open) return;
|
|
195
|
+
const onDocClick = (e) => {
|
|
196
|
+
if (rootRef.current && !rootRef.current.contains(e.target)) setOpen(false);
|
|
197
|
+
};
|
|
198
|
+
document.addEventListener("mousedown", onDocClick);
|
|
199
|
+
return () => document.removeEventListener("mousedown", onDocClick);
|
|
200
|
+
}, [open]);
|
|
201
|
+
const openMenu = react.useCallback(() => {
|
|
202
|
+
if (disabled) return;
|
|
203
|
+
setActiveIndex(selectedIndex >= 0 ? selectedIndex : 0);
|
|
204
|
+
setOpen(true);
|
|
205
|
+
}, [disabled, selectedIndex]);
|
|
206
|
+
const pick = react.useCallback(
|
|
207
|
+
(i) => {
|
|
208
|
+
const profile = shells[i];
|
|
209
|
+
if (!profile) return;
|
|
210
|
+
onChange(profile.id, profile);
|
|
211
|
+
setOpen(false);
|
|
212
|
+
},
|
|
213
|
+
[shells, onChange]
|
|
214
|
+
);
|
|
215
|
+
const onButtonKeyDown = (e) => {
|
|
216
|
+
if (disabled) return;
|
|
217
|
+
if (e.key === "ArrowDown" || e.key === "ArrowUp" || e.key === "Enter" || e.key === " ") {
|
|
218
|
+
e.preventDefault();
|
|
219
|
+
openMenu();
|
|
220
|
+
}
|
|
221
|
+
};
|
|
222
|
+
const onListKeyDown = (e) => {
|
|
223
|
+
if (e.key === "Escape") {
|
|
224
|
+
e.preventDefault();
|
|
225
|
+
setOpen(false);
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
if (e.key === "ArrowDown") {
|
|
229
|
+
e.preventDefault();
|
|
230
|
+
setActiveIndex((i) => Math.min(shells.length - 1, i + 1));
|
|
231
|
+
} else if (e.key === "ArrowUp") {
|
|
232
|
+
e.preventDefault();
|
|
233
|
+
setActiveIndex((i) => Math.max(0, i - 1));
|
|
234
|
+
} else if (e.key === "Home") {
|
|
235
|
+
e.preventDefault();
|
|
236
|
+
setActiveIndex(0);
|
|
237
|
+
} else if (e.key === "End") {
|
|
238
|
+
e.preventDefault();
|
|
239
|
+
setActiveIndex(shells.length - 1);
|
|
240
|
+
} else if (e.key === "Enter" || e.key === " ") {
|
|
241
|
+
e.preventDefault();
|
|
242
|
+
pick(activeIndex);
|
|
243
|
+
}
|
|
244
|
+
};
|
|
245
|
+
const buttonStyle = {
|
|
246
|
+
display: "inline-flex",
|
|
247
|
+
alignItems: "center",
|
|
248
|
+
gap: 6,
|
|
249
|
+
height: 26,
|
|
250
|
+
padding: "0 8px",
|
|
251
|
+
fontSize: 12,
|
|
252
|
+
lineHeight: 1,
|
|
253
|
+
fontFamily: "inherit",
|
|
254
|
+
color: FG,
|
|
255
|
+
background: PANEL_BG,
|
|
256
|
+
border: `1px solid ${BORDER}`,
|
|
257
|
+
borderRadius: 6,
|
|
258
|
+
cursor: disabled ? "default" : "pointer",
|
|
259
|
+
opacity: disabled ? 0.6 : 1,
|
|
260
|
+
userSelect: "none"
|
|
261
|
+
};
|
|
262
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
263
|
+
"div",
|
|
264
|
+
{
|
|
265
|
+
ref: rootRef,
|
|
266
|
+
"data-fancy-shell-switcher": "",
|
|
267
|
+
"data-active-shell": value ?? void 0,
|
|
268
|
+
className,
|
|
269
|
+
style: { position: "relative", display: "inline-block", ...style },
|
|
270
|
+
children: [
|
|
271
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
272
|
+
"button",
|
|
273
|
+
{
|
|
274
|
+
type: "button",
|
|
275
|
+
"aria-haspopup": "listbox",
|
|
276
|
+
"aria-expanded": open,
|
|
277
|
+
"aria-label": "Select shell",
|
|
278
|
+
disabled,
|
|
279
|
+
style: buttonStyle,
|
|
280
|
+
onClick: () => open ? setOpen(false) : openMenu(),
|
|
281
|
+
onKeyDown: onButtonKeyDown,
|
|
282
|
+
children: [
|
|
283
|
+
selected?.icon ? /* @__PURE__ */ jsxRuntime.jsx("span", { "aria-hidden": "true", style: { color: ACCENT }, children: selected.icon }) : null,
|
|
284
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { children: selected?.label ?? "Select shell" }),
|
|
285
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { "aria-hidden": "true", style: { color: MUTED, fontSize: 9 }, children: "\u25BC" })
|
|
286
|
+
]
|
|
287
|
+
}
|
|
288
|
+
),
|
|
289
|
+
open ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
290
|
+
"ul",
|
|
291
|
+
{
|
|
292
|
+
role: "listbox",
|
|
293
|
+
id: listId,
|
|
294
|
+
"aria-label": "Shells",
|
|
295
|
+
tabIndex: -1,
|
|
296
|
+
autoFocus: true,
|
|
297
|
+
onKeyDown: onListKeyDown,
|
|
298
|
+
ref: (el) => el?.focus(),
|
|
299
|
+
style: {
|
|
300
|
+
position: "absolute",
|
|
301
|
+
zIndex: 50,
|
|
302
|
+
top: "calc(100% + 4px)",
|
|
303
|
+
left: 0,
|
|
304
|
+
minWidth: "100%",
|
|
305
|
+
margin: 0,
|
|
306
|
+
padding: 4,
|
|
307
|
+
listStyle: "none",
|
|
308
|
+
background: PANEL_BG,
|
|
309
|
+
border: `1px solid ${BORDER}`,
|
|
310
|
+
borderRadius: 8,
|
|
311
|
+
boxShadow: "0 8px 24px rgba(0,0,0,0.45)",
|
|
312
|
+
outline: "none"
|
|
313
|
+
},
|
|
314
|
+
children: shells.map((s, i) => {
|
|
315
|
+
const isSelected = s.id === value;
|
|
316
|
+
const isActive = i === activeIndex;
|
|
317
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
318
|
+
"li",
|
|
319
|
+
{
|
|
320
|
+
role: "option",
|
|
321
|
+
"aria-selected": isSelected,
|
|
322
|
+
"data-shell-id": s.id,
|
|
323
|
+
onMouseEnter: () => setActiveIndex(i),
|
|
324
|
+
onClick: () => pick(i),
|
|
325
|
+
style: {
|
|
326
|
+
display: "flex",
|
|
327
|
+
alignItems: "center",
|
|
328
|
+
gap: 8,
|
|
329
|
+
padding: "6px 8px",
|
|
330
|
+
fontSize: 12,
|
|
331
|
+
borderRadius: 5,
|
|
332
|
+
color: FG,
|
|
333
|
+
cursor: "pointer",
|
|
334
|
+
background: isActive ? HOVER_BG : "transparent"
|
|
335
|
+
},
|
|
336
|
+
children: [
|
|
337
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
338
|
+
"span",
|
|
339
|
+
{
|
|
340
|
+
"aria-hidden": "true",
|
|
341
|
+
style: {
|
|
342
|
+
width: 18,
|
|
343
|
+
textAlign: "center",
|
|
344
|
+
color: isSelected ? ACCENT : MUTED
|
|
345
|
+
},
|
|
346
|
+
children: s.icon ?? ""
|
|
347
|
+
}
|
|
348
|
+
),
|
|
349
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { style: { flex: 1 }, children: s.label }),
|
|
350
|
+
isSelected ? /* @__PURE__ */ jsxRuntime.jsx("span", { "aria-hidden": "true", style: { color: ACCENT, fontSize: 11 }, children: "\u2713" }) : null
|
|
351
|
+
]
|
|
352
|
+
},
|
|
353
|
+
s.id
|
|
354
|
+
);
|
|
355
|
+
})
|
|
356
|
+
}
|
|
357
|
+
) : null
|
|
358
|
+
]
|
|
359
|
+
}
|
|
360
|
+
);
|
|
361
|
+
}
|
|
362
|
+
|
|
148
363
|
// src/output-diff.ts
|
|
149
364
|
function diffOutput(written, next) {
|
|
150
365
|
if (next === written) return null;
|
|
151
366
|
if (next.startsWith(written)) return { reset: false, write: next.slice(written.length) };
|
|
152
367
|
return { reset: true, write: next };
|
|
153
368
|
}
|
|
369
|
+
|
|
370
|
+
// src/shell-select.ts
|
|
371
|
+
function decideShellSelect(shells, current, id) {
|
|
372
|
+
const profile = resolveShell(shells, id);
|
|
373
|
+
if (!profile) return null;
|
|
374
|
+
return { profile, changed: current !== id };
|
|
375
|
+
}
|
|
376
|
+
function decideShellSwitch(current, next) {
|
|
377
|
+
return { changed: current !== next };
|
|
378
|
+
}
|
|
154
379
|
var Terminal = react.forwardRef(function Terminal2({
|
|
155
380
|
output,
|
|
156
381
|
theme,
|
|
@@ -166,11 +391,27 @@ var Terminal = react.forwardRef(function Terminal2({
|
|
|
166
391
|
initialOutput,
|
|
167
392
|
onData,
|
|
168
393
|
onResize,
|
|
394
|
+
shells,
|
|
395
|
+
activeShell,
|
|
396
|
+
onShellChange,
|
|
397
|
+
showShellBar = false,
|
|
169
398
|
className,
|
|
170
399
|
style,
|
|
171
400
|
...rest
|
|
172
401
|
}, ref) {
|
|
173
402
|
const containerRef = react.useRef(null);
|
|
403
|
+
const isShellControlled = activeShell !== void 0;
|
|
404
|
+
const [internalShell, setInternalShell] = react.useState(void 0);
|
|
405
|
+
const shellId = isShellControlled ? activeShell : internalShell;
|
|
406
|
+
const shellStateRef = react.useRef({ shells, shellId, isShellControlled, onShellChange });
|
|
407
|
+
shellStateRef.current = { shells, shellId, isShellControlled, onShellChange };
|
|
408
|
+
const selectShell = react.useCallback((id) => {
|
|
409
|
+
const s = shellStateRef.current;
|
|
410
|
+
const decision = decideShellSelect(s.shells, s.shellId, id);
|
|
411
|
+
if (!decision) return;
|
|
412
|
+
if (!s.isShellControlled) setInternalShell(id);
|
|
413
|
+
s.onShellChange?.(id, decision.profile);
|
|
414
|
+
}, []);
|
|
174
415
|
const handle = useTerminal(containerRef, {
|
|
175
416
|
theme,
|
|
176
417
|
rows,
|
|
@@ -188,7 +429,15 @@ var Terminal = react.forwardRef(function Terminal2({
|
|
|
188
429
|
onData,
|
|
189
430
|
onResize
|
|
190
431
|
});
|
|
191
|
-
react.useImperativeHandle(
|
|
432
|
+
react.useImperativeHandle(
|
|
433
|
+
ref,
|
|
434
|
+
() => ({
|
|
435
|
+
...handle,
|
|
436
|
+
setShell: selectShell,
|
|
437
|
+
getShell: () => shellStateRef.current.shellId
|
|
438
|
+
}),
|
|
439
|
+
[handle, selectShell]
|
|
440
|
+
);
|
|
192
441
|
const written = react.useRef("");
|
|
193
442
|
react.useEffect(() => {
|
|
194
443
|
if (output === void 0) return;
|
|
@@ -198,38 +447,102 @@ var Terminal = react.forwardRef(function Terminal2({
|
|
|
198
447
|
handle.write(change.write);
|
|
199
448
|
written.current = output;
|
|
200
449
|
}, [output, handle]);
|
|
201
|
-
|
|
450
|
+
const surface = /* @__PURE__ */ jsxRuntime.jsx(
|
|
202
451
|
"div",
|
|
203
452
|
{
|
|
204
453
|
ref: containerRef,
|
|
205
454
|
"data-fancy-terminal": "",
|
|
206
455
|
"data-readonly": readOnly ? "" : void 0,
|
|
456
|
+
"data-fancy-terminal-shell": shellId ?? void 0,
|
|
457
|
+
style: showShellBar && shells ? { width: "100%", flex: 1, minHeight: 0 } : { width: "100%", height: "100%", ...style },
|
|
458
|
+
...showShellBar && shells ? {} : rest
|
|
459
|
+
}
|
|
460
|
+
);
|
|
461
|
+
if (!showShellBar || !shells) return surface;
|
|
462
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
463
|
+
"div",
|
|
464
|
+
{
|
|
465
|
+
"data-fancy-terminal-shell": shellId ?? void 0,
|
|
207
466
|
className,
|
|
208
|
-
style: { width: "100%", height: "100%", ...style },
|
|
209
|
-
...rest
|
|
467
|
+
style: { display: "flex", flexDirection: "column", width: "100%", height: "100%", ...style },
|
|
468
|
+
...rest,
|
|
469
|
+
children: [
|
|
470
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
471
|
+
"div",
|
|
472
|
+
{
|
|
473
|
+
style: {
|
|
474
|
+
display: "flex",
|
|
475
|
+
alignItems: "center",
|
|
476
|
+
gap: 8,
|
|
477
|
+
padding: "4px 6px",
|
|
478
|
+
background: "#09090b",
|
|
479
|
+
borderBottom: "1px solid #27272a"
|
|
480
|
+
},
|
|
481
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
482
|
+
ShellSwitcher,
|
|
483
|
+
{
|
|
484
|
+
shells,
|
|
485
|
+
value: shellId,
|
|
486
|
+
onChange: (id) => selectShell(id),
|
|
487
|
+
disabled: readOnly
|
|
488
|
+
}
|
|
489
|
+
)
|
|
490
|
+
}
|
|
491
|
+
),
|
|
492
|
+
surface
|
|
493
|
+
]
|
|
210
494
|
}
|
|
211
495
|
);
|
|
212
496
|
});
|
|
213
497
|
function useTerminalSession(options) {
|
|
214
|
-
const
|
|
498
|
+
const initial = options.initial ?? "";
|
|
499
|
+
const [output, setOutput] = react.useState(initial);
|
|
500
|
+
const [internalShell, setInternalShell] = react.useState(options.shell);
|
|
501
|
+
const shell = options.shell !== void 0 ? options.shell : internalShell;
|
|
215
502
|
const transportRef = react.useRef(options.transport);
|
|
216
503
|
transportRef.current = options.transport;
|
|
504
|
+
const initialRef = react.useRef(initial);
|
|
505
|
+
initialRef.current = initial;
|
|
217
506
|
react.useEffect(() => {
|
|
218
507
|
const unsub = transportRef.current.subscribe((chunk) => setOutput((o) => o + chunk));
|
|
219
508
|
return () => {
|
|
220
509
|
if (typeof unsub === "function") unsub();
|
|
221
510
|
};
|
|
222
511
|
}, []);
|
|
512
|
+
const connectedShell = react.useRef(void 0);
|
|
513
|
+
const mounted = react.useRef(false);
|
|
514
|
+
react.useEffect(() => {
|
|
515
|
+
if (!mounted.current) {
|
|
516
|
+
mounted.current = true;
|
|
517
|
+
connectedShell.current = shell;
|
|
518
|
+
void transportRef.current.connect?.(shell);
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
if (!decideShellSwitch(connectedShell.current, shell).changed) return;
|
|
522
|
+
connectedShell.current = shell;
|
|
523
|
+
setOutput(initialRef.current);
|
|
524
|
+
void transportRef.current.connect?.(shell);
|
|
525
|
+
}, [shell]);
|
|
223
526
|
const sendData = react.useCallback((data) => {
|
|
224
527
|
void transportRef.current.send(data);
|
|
225
528
|
}, []);
|
|
226
529
|
const append = react.useCallback((chunk) => setOutput((o) => o + chunk), []);
|
|
227
530
|
const clear = react.useCallback(() => setOutput(""), []);
|
|
228
|
-
|
|
531
|
+
const switchShell = react.useCallback((id) => {
|
|
532
|
+
setInternalShell((cur) => cur === id ? cur : id);
|
|
533
|
+
if (!decideShellSwitch(connectedShell.current, id).changed) return;
|
|
534
|
+
connectedShell.current = id;
|
|
535
|
+
setOutput(initialRef.current);
|
|
536
|
+
void transportRef.current.connect?.(id);
|
|
537
|
+
}, []);
|
|
538
|
+
return { output, sendData, append, clear, shell, switchShell };
|
|
229
539
|
}
|
|
230
540
|
|
|
541
|
+
exports.BUILTIN_SHELLS = BUILTIN_SHELLS;
|
|
542
|
+
exports.ShellSwitcher = ShellSwitcher;
|
|
231
543
|
exports.Terminal = Terminal;
|
|
232
544
|
exports.fancyDarkTheme = fancyDarkTheme;
|
|
545
|
+
exports.resolveShell = resolveShell;
|
|
233
546
|
exports.useTerminal = useTerminal;
|
|
234
547
|
exports.useTerminalFit = useTerminalFit;
|
|
235
548
|
exports.useTerminalSession = useTerminalSession;
|