@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/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { forwardRef, useRef, useImperativeHandle, useEffect,
|
|
1
|
+
import { forwardRef, useRef, useState, useCallback, useImperativeHandle, useEffect, useId } from 'react';
|
|
2
2
|
import { Terminal as Terminal$1 } from '@xterm/xterm';
|
|
3
3
|
import { FitAddon } from '@xterm/addon-fit';
|
|
4
|
-
import { jsx } from 'react/jsx-runtime';
|
|
4
|
+
import { jsx, jsxs } from 'react/jsx-runtime';
|
|
5
5
|
|
|
6
6
|
// src/components/Terminal.tsx
|
|
7
7
|
|
|
@@ -85,7 +85,13 @@ function useTerminal(containerRef, options = {}) {
|
|
|
85
85
|
},
|
|
86
86
|
focus: () => xtermRef.current?.focus(),
|
|
87
87
|
getBuffer: () => readBuffer(xtermRef.current),
|
|
88
|
-
getSelection: () => xtermRef.current?.getSelection() ?? ""
|
|
88
|
+
getSelection: () => xtermRef.current?.getSelection() ?? "",
|
|
89
|
+
// Shell selection is owned by the <Terminal> component layer (it needs the
|
|
90
|
+
// `shells` list + onShellChange). The headless engine is shell-agnostic, so
|
|
91
|
+
// these are no-ops here and get overridden when <Terminal> composes them.
|
|
92
|
+
setShell: () => {
|
|
93
|
+
},
|
|
94
|
+
getShell: () => void 0
|
|
89
95
|
};
|
|
90
96
|
}
|
|
91
97
|
const handle = handleRef.current;
|
|
@@ -143,12 +149,231 @@ function useTerminal(containerRef, options = {}) {
|
|
|
143
149
|
return handle;
|
|
144
150
|
}
|
|
145
151
|
|
|
152
|
+
// src/types.ts
|
|
153
|
+
function resolveShell(shells, id) {
|
|
154
|
+
if (!shells || id === void 0) return void 0;
|
|
155
|
+
return shells.find((s) => s.id === id);
|
|
156
|
+
}
|
|
157
|
+
var BUILTIN_SHELLS = [
|
|
158
|
+
{ id: "cmd", label: "Command Prompt", icon: ">_", command: "cmd.exe" },
|
|
159
|
+
{ id: "powershell", label: "Windows PowerShell", icon: "PS", command: "powershell.exe" },
|
|
160
|
+
{ id: "pwsh", label: "PowerShell", icon: "PS", command: "pwsh" },
|
|
161
|
+
{
|
|
162
|
+
id: "git-bash",
|
|
163
|
+
label: "Git Bash",
|
|
164
|
+
icon: "",
|
|
165
|
+
command: "C:\\Program Files\\Git\\bin\\bash.exe",
|
|
166
|
+
args: ["--login", "-i"]
|
|
167
|
+
},
|
|
168
|
+
{ id: "bash", label: "Bash", icon: "$", command: "bash" },
|
|
169
|
+
{ id: "zsh", label: "Zsh", icon: "%", command: "zsh" }
|
|
170
|
+
];
|
|
171
|
+
var PANEL_BG = "#18181b";
|
|
172
|
+
var BORDER = "#3f3f46";
|
|
173
|
+
var FG = "#e4e4e7";
|
|
174
|
+
var MUTED = "#a1a1aa";
|
|
175
|
+
var ACCENT = "#8b5cf6";
|
|
176
|
+
var HOVER_BG = "#27272a";
|
|
177
|
+
function ShellSwitcher({
|
|
178
|
+
shells,
|
|
179
|
+
value,
|
|
180
|
+
onChange,
|
|
181
|
+
className,
|
|
182
|
+
style,
|
|
183
|
+
disabled
|
|
184
|
+
}) {
|
|
185
|
+
const [open, setOpen] = useState(false);
|
|
186
|
+
const [activeIndex, setActiveIndex] = useState(0);
|
|
187
|
+
const rootRef = useRef(null);
|
|
188
|
+
const listId = useId();
|
|
189
|
+
const selected = resolveShell(shells, value);
|
|
190
|
+
const selectedIndex = selected ? shells.indexOf(selected) : -1;
|
|
191
|
+
useEffect(() => {
|
|
192
|
+
if (!open) return;
|
|
193
|
+
const onDocClick = (e) => {
|
|
194
|
+
if (rootRef.current && !rootRef.current.contains(e.target)) setOpen(false);
|
|
195
|
+
};
|
|
196
|
+
document.addEventListener("mousedown", onDocClick);
|
|
197
|
+
return () => document.removeEventListener("mousedown", onDocClick);
|
|
198
|
+
}, [open]);
|
|
199
|
+
const openMenu = useCallback(() => {
|
|
200
|
+
if (disabled) return;
|
|
201
|
+
setActiveIndex(selectedIndex >= 0 ? selectedIndex : 0);
|
|
202
|
+
setOpen(true);
|
|
203
|
+
}, [disabled, selectedIndex]);
|
|
204
|
+
const pick = useCallback(
|
|
205
|
+
(i) => {
|
|
206
|
+
const profile = shells[i];
|
|
207
|
+
if (!profile) return;
|
|
208
|
+
onChange(profile.id, profile);
|
|
209
|
+
setOpen(false);
|
|
210
|
+
},
|
|
211
|
+
[shells, onChange]
|
|
212
|
+
);
|
|
213
|
+
const onButtonKeyDown = (e) => {
|
|
214
|
+
if (disabled) return;
|
|
215
|
+
if (e.key === "ArrowDown" || e.key === "ArrowUp" || e.key === "Enter" || e.key === " ") {
|
|
216
|
+
e.preventDefault();
|
|
217
|
+
openMenu();
|
|
218
|
+
}
|
|
219
|
+
};
|
|
220
|
+
const onListKeyDown = (e) => {
|
|
221
|
+
if (e.key === "Escape") {
|
|
222
|
+
e.preventDefault();
|
|
223
|
+
setOpen(false);
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
if (e.key === "ArrowDown") {
|
|
227
|
+
e.preventDefault();
|
|
228
|
+
setActiveIndex((i) => Math.min(shells.length - 1, i + 1));
|
|
229
|
+
} else if (e.key === "ArrowUp") {
|
|
230
|
+
e.preventDefault();
|
|
231
|
+
setActiveIndex((i) => Math.max(0, i - 1));
|
|
232
|
+
} else if (e.key === "Home") {
|
|
233
|
+
e.preventDefault();
|
|
234
|
+
setActiveIndex(0);
|
|
235
|
+
} else if (e.key === "End") {
|
|
236
|
+
e.preventDefault();
|
|
237
|
+
setActiveIndex(shells.length - 1);
|
|
238
|
+
} else if (e.key === "Enter" || e.key === " ") {
|
|
239
|
+
e.preventDefault();
|
|
240
|
+
pick(activeIndex);
|
|
241
|
+
}
|
|
242
|
+
};
|
|
243
|
+
const buttonStyle = {
|
|
244
|
+
display: "inline-flex",
|
|
245
|
+
alignItems: "center",
|
|
246
|
+
gap: 6,
|
|
247
|
+
height: 26,
|
|
248
|
+
padding: "0 8px",
|
|
249
|
+
fontSize: 12,
|
|
250
|
+
lineHeight: 1,
|
|
251
|
+
fontFamily: "inherit",
|
|
252
|
+
color: FG,
|
|
253
|
+
background: PANEL_BG,
|
|
254
|
+
border: `1px solid ${BORDER}`,
|
|
255
|
+
borderRadius: 6,
|
|
256
|
+
cursor: disabled ? "default" : "pointer",
|
|
257
|
+
opacity: disabled ? 0.6 : 1,
|
|
258
|
+
userSelect: "none"
|
|
259
|
+
};
|
|
260
|
+
return /* @__PURE__ */ jsxs(
|
|
261
|
+
"div",
|
|
262
|
+
{
|
|
263
|
+
ref: rootRef,
|
|
264
|
+
"data-fancy-shell-switcher": "",
|
|
265
|
+
"data-active-shell": value ?? void 0,
|
|
266
|
+
className,
|
|
267
|
+
style: { position: "relative", display: "inline-block", ...style },
|
|
268
|
+
children: [
|
|
269
|
+
/* @__PURE__ */ jsxs(
|
|
270
|
+
"button",
|
|
271
|
+
{
|
|
272
|
+
type: "button",
|
|
273
|
+
"aria-haspopup": "listbox",
|
|
274
|
+
"aria-expanded": open,
|
|
275
|
+
"aria-label": "Select shell",
|
|
276
|
+
disabled,
|
|
277
|
+
style: buttonStyle,
|
|
278
|
+
onClick: () => open ? setOpen(false) : openMenu(),
|
|
279
|
+
onKeyDown: onButtonKeyDown,
|
|
280
|
+
children: [
|
|
281
|
+
selected?.icon ? /* @__PURE__ */ jsx("span", { "aria-hidden": "true", style: { color: ACCENT }, children: selected.icon }) : null,
|
|
282
|
+
/* @__PURE__ */ jsx("span", { children: selected?.label ?? "Select shell" }),
|
|
283
|
+
/* @__PURE__ */ jsx("span", { "aria-hidden": "true", style: { color: MUTED, fontSize: 9 }, children: "\u25BC" })
|
|
284
|
+
]
|
|
285
|
+
}
|
|
286
|
+
),
|
|
287
|
+
open ? /* @__PURE__ */ jsx(
|
|
288
|
+
"ul",
|
|
289
|
+
{
|
|
290
|
+
role: "listbox",
|
|
291
|
+
id: listId,
|
|
292
|
+
"aria-label": "Shells",
|
|
293
|
+
tabIndex: -1,
|
|
294
|
+
autoFocus: true,
|
|
295
|
+
onKeyDown: onListKeyDown,
|
|
296
|
+
ref: (el) => el?.focus(),
|
|
297
|
+
style: {
|
|
298
|
+
position: "absolute",
|
|
299
|
+
zIndex: 50,
|
|
300
|
+
top: "calc(100% + 4px)",
|
|
301
|
+
left: 0,
|
|
302
|
+
minWidth: "100%",
|
|
303
|
+
margin: 0,
|
|
304
|
+
padding: 4,
|
|
305
|
+
listStyle: "none",
|
|
306
|
+
background: PANEL_BG,
|
|
307
|
+
border: `1px solid ${BORDER}`,
|
|
308
|
+
borderRadius: 8,
|
|
309
|
+
boxShadow: "0 8px 24px rgba(0,0,0,0.45)",
|
|
310
|
+
outline: "none"
|
|
311
|
+
},
|
|
312
|
+
children: shells.map((s, i) => {
|
|
313
|
+
const isSelected = s.id === value;
|
|
314
|
+
const isActive = i === activeIndex;
|
|
315
|
+
return /* @__PURE__ */ jsxs(
|
|
316
|
+
"li",
|
|
317
|
+
{
|
|
318
|
+
role: "option",
|
|
319
|
+
"aria-selected": isSelected,
|
|
320
|
+
"data-shell-id": s.id,
|
|
321
|
+
onMouseEnter: () => setActiveIndex(i),
|
|
322
|
+
onClick: () => pick(i),
|
|
323
|
+
style: {
|
|
324
|
+
display: "flex",
|
|
325
|
+
alignItems: "center",
|
|
326
|
+
gap: 8,
|
|
327
|
+
padding: "6px 8px",
|
|
328
|
+
fontSize: 12,
|
|
329
|
+
borderRadius: 5,
|
|
330
|
+
color: FG,
|
|
331
|
+
cursor: "pointer",
|
|
332
|
+
background: isActive ? HOVER_BG : "transparent"
|
|
333
|
+
},
|
|
334
|
+
children: [
|
|
335
|
+
/* @__PURE__ */ jsx(
|
|
336
|
+
"span",
|
|
337
|
+
{
|
|
338
|
+
"aria-hidden": "true",
|
|
339
|
+
style: {
|
|
340
|
+
width: 18,
|
|
341
|
+
textAlign: "center",
|
|
342
|
+
color: isSelected ? ACCENT : MUTED
|
|
343
|
+
},
|
|
344
|
+
children: s.icon ?? ""
|
|
345
|
+
}
|
|
346
|
+
),
|
|
347
|
+
/* @__PURE__ */ jsx("span", { style: { flex: 1 }, children: s.label }),
|
|
348
|
+
isSelected ? /* @__PURE__ */ jsx("span", { "aria-hidden": "true", style: { color: ACCENT, fontSize: 11 }, children: "\u2713" }) : null
|
|
349
|
+
]
|
|
350
|
+
},
|
|
351
|
+
s.id
|
|
352
|
+
);
|
|
353
|
+
})
|
|
354
|
+
}
|
|
355
|
+
) : null
|
|
356
|
+
]
|
|
357
|
+
}
|
|
358
|
+
);
|
|
359
|
+
}
|
|
360
|
+
|
|
146
361
|
// src/output-diff.ts
|
|
147
362
|
function diffOutput(written, next) {
|
|
148
363
|
if (next === written) return null;
|
|
149
364
|
if (next.startsWith(written)) return { reset: false, write: next.slice(written.length) };
|
|
150
365
|
return { reset: true, write: next };
|
|
151
366
|
}
|
|
367
|
+
|
|
368
|
+
// src/shell-select.ts
|
|
369
|
+
function decideShellSelect(shells, current, id) {
|
|
370
|
+
const profile = resolveShell(shells, id);
|
|
371
|
+
if (!profile) return null;
|
|
372
|
+
return { profile, changed: current !== id };
|
|
373
|
+
}
|
|
374
|
+
function decideShellSwitch(current, next) {
|
|
375
|
+
return { changed: current !== next };
|
|
376
|
+
}
|
|
152
377
|
var Terminal = forwardRef(function Terminal2({
|
|
153
378
|
output,
|
|
154
379
|
theme,
|
|
@@ -164,11 +389,27 @@ var Terminal = forwardRef(function Terminal2({
|
|
|
164
389
|
initialOutput,
|
|
165
390
|
onData,
|
|
166
391
|
onResize,
|
|
392
|
+
shells,
|
|
393
|
+
activeShell,
|
|
394
|
+
onShellChange,
|
|
395
|
+
showShellBar = false,
|
|
167
396
|
className,
|
|
168
397
|
style,
|
|
169
398
|
...rest
|
|
170
399
|
}, ref) {
|
|
171
400
|
const containerRef = useRef(null);
|
|
401
|
+
const isShellControlled = activeShell !== void 0;
|
|
402
|
+
const [internalShell, setInternalShell] = useState(void 0);
|
|
403
|
+
const shellId = isShellControlled ? activeShell : internalShell;
|
|
404
|
+
const shellStateRef = useRef({ shells, shellId, isShellControlled, onShellChange });
|
|
405
|
+
shellStateRef.current = { shells, shellId, isShellControlled, onShellChange };
|
|
406
|
+
const selectShell = useCallback((id) => {
|
|
407
|
+
const s = shellStateRef.current;
|
|
408
|
+
const decision = decideShellSelect(s.shells, s.shellId, id);
|
|
409
|
+
if (!decision) return;
|
|
410
|
+
if (!s.isShellControlled) setInternalShell(id);
|
|
411
|
+
s.onShellChange?.(id, decision.profile);
|
|
412
|
+
}, []);
|
|
172
413
|
const handle = useTerminal(containerRef, {
|
|
173
414
|
theme,
|
|
174
415
|
rows,
|
|
@@ -186,7 +427,15 @@ var Terminal = forwardRef(function Terminal2({
|
|
|
186
427
|
onData,
|
|
187
428
|
onResize
|
|
188
429
|
});
|
|
189
|
-
useImperativeHandle(
|
|
430
|
+
useImperativeHandle(
|
|
431
|
+
ref,
|
|
432
|
+
() => ({
|
|
433
|
+
...handle,
|
|
434
|
+
setShell: selectShell,
|
|
435
|
+
getShell: () => shellStateRef.current.shellId
|
|
436
|
+
}),
|
|
437
|
+
[handle, selectShell]
|
|
438
|
+
);
|
|
190
439
|
const written = useRef("");
|
|
191
440
|
useEffect(() => {
|
|
192
441
|
if (output === void 0) return;
|
|
@@ -196,36 +445,97 @@ var Terminal = forwardRef(function Terminal2({
|
|
|
196
445
|
handle.write(change.write);
|
|
197
446
|
written.current = output;
|
|
198
447
|
}, [output, handle]);
|
|
199
|
-
|
|
448
|
+
const surface = /* @__PURE__ */ jsx(
|
|
200
449
|
"div",
|
|
201
450
|
{
|
|
202
451
|
ref: containerRef,
|
|
203
452
|
"data-fancy-terminal": "",
|
|
204
453
|
"data-readonly": readOnly ? "" : void 0,
|
|
454
|
+
"data-fancy-terminal-shell": shellId ?? void 0,
|
|
455
|
+
style: showShellBar && shells ? { width: "100%", flex: 1, minHeight: 0 } : { width: "100%", height: "100%", ...style },
|
|
456
|
+
...showShellBar && shells ? {} : rest
|
|
457
|
+
}
|
|
458
|
+
);
|
|
459
|
+
if (!showShellBar || !shells) return surface;
|
|
460
|
+
return /* @__PURE__ */ jsxs(
|
|
461
|
+
"div",
|
|
462
|
+
{
|
|
463
|
+
"data-fancy-terminal-shell": shellId ?? void 0,
|
|
205
464
|
className,
|
|
206
|
-
style: { width: "100%", height: "100%", ...style },
|
|
207
|
-
...rest
|
|
465
|
+
style: { display: "flex", flexDirection: "column", width: "100%", height: "100%", ...style },
|
|
466
|
+
...rest,
|
|
467
|
+
children: [
|
|
468
|
+
/* @__PURE__ */ jsx(
|
|
469
|
+
"div",
|
|
470
|
+
{
|
|
471
|
+
style: {
|
|
472
|
+
display: "flex",
|
|
473
|
+
alignItems: "center",
|
|
474
|
+
gap: 8,
|
|
475
|
+
padding: "4px 6px",
|
|
476
|
+
background: "#09090b",
|
|
477
|
+
borderBottom: "1px solid #27272a"
|
|
478
|
+
},
|
|
479
|
+
children: /* @__PURE__ */ jsx(
|
|
480
|
+
ShellSwitcher,
|
|
481
|
+
{
|
|
482
|
+
shells,
|
|
483
|
+
value: shellId,
|
|
484
|
+
onChange: (id) => selectShell(id),
|
|
485
|
+
disabled: readOnly
|
|
486
|
+
}
|
|
487
|
+
)
|
|
488
|
+
}
|
|
489
|
+
),
|
|
490
|
+
surface
|
|
491
|
+
]
|
|
208
492
|
}
|
|
209
493
|
);
|
|
210
494
|
});
|
|
211
495
|
function useTerminalSession(options) {
|
|
212
|
-
const
|
|
496
|
+
const initial = options.initial ?? "";
|
|
497
|
+
const [output, setOutput] = useState(initial);
|
|
498
|
+
const [internalShell, setInternalShell] = useState(options.shell);
|
|
499
|
+
const shell = options.shell !== void 0 ? options.shell : internalShell;
|
|
213
500
|
const transportRef = useRef(options.transport);
|
|
214
501
|
transportRef.current = options.transport;
|
|
502
|
+
const initialRef = useRef(initial);
|
|
503
|
+
initialRef.current = initial;
|
|
215
504
|
useEffect(() => {
|
|
216
505
|
const unsub = transportRef.current.subscribe((chunk) => setOutput((o) => o + chunk));
|
|
217
506
|
return () => {
|
|
218
507
|
if (typeof unsub === "function") unsub();
|
|
219
508
|
};
|
|
220
509
|
}, []);
|
|
510
|
+
const connectedShell = useRef(void 0);
|
|
511
|
+
const mounted = useRef(false);
|
|
512
|
+
useEffect(() => {
|
|
513
|
+
if (!mounted.current) {
|
|
514
|
+
mounted.current = true;
|
|
515
|
+
connectedShell.current = shell;
|
|
516
|
+
void transportRef.current.connect?.(shell);
|
|
517
|
+
return;
|
|
518
|
+
}
|
|
519
|
+
if (!decideShellSwitch(connectedShell.current, shell).changed) return;
|
|
520
|
+
connectedShell.current = shell;
|
|
521
|
+
setOutput(initialRef.current);
|
|
522
|
+
void transportRef.current.connect?.(shell);
|
|
523
|
+
}, [shell]);
|
|
221
524
|
const sendData = useCallback((data) => {
|
|
222
525
|
void transportRef.current.send(data);
|
|
223
526
|
}, []);
|
|
224
527
|
const append = useCallback((chunk) => setOutput((o) => o + chunk), []);
|
|
225
528
|
const clear = useCallback(() => setOutput(""), []);
|
|
226
|
-
|
|
529
|
+
const switchShell = useCallback((id) => {
|
|
530
|
+
setInternalShell((cur) => cur === id ? cur : id);
|
|
531
|
+
if (!decideShellSwitch(connectedShell.current, id).changed) return;
|
|
532
|
+
connectedShell.current = id;
|
|
533
|
+
setOutput(initialRef.current);
|
|
534
|
+
void transportRef.current.connect?.(id);
|
|
535
|
+
}, []);
|
|
536
|
+
return { output, sendData, append, clear, shell, switchShell };
|
|
227
537
|
}
|
|
228
538
|
|
|
229
|
-
export { Terminal, fancyDarkTheme, useTerminal, useTerminalFit, useTerminalSession };
|
|
539
|
+
export { BUILTIN_SHELLS, ShellSwitcher, Terminal, fancyDarkTheme, resolveShell, useTerminal, useTerminalFit, useTerminalSession };
|
|
230
540
|
//# sourceMappingURL=index.js.map
|
|
231
541
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/theme.ts","../src/hooks/use-terminal-fit.ts","../src/hooks/use-terminal.ts","../src/output-diff.ts","../src/components/Terminal.tsx","../src/hooks/use-terminal-session.ts"],"names":["useRef","useEffect","XTerm","Terminal"],"mappings":";;;;;;;;AAOO,IAAM,cAAA,GAAgC;AAAA,EAC3C,UAAA,EAAY,SAAA;AAAA;AAAA,EACZ,UAAA,EAAY,SAAA;AAAA;AAAA,EACZ,MAAA,EAAQ,SAAA;AAAA;AAAA,EACR,YAAA,EAAc,SAAA;AAAA,EACd,mBAAA,EAAqB,SAAA;AAAA;AAAA,EACrB,KAAA,EAAO,SAAA;AAAA,EACP,GAAA,EAAK,SAAA;AAAA,EACL,KAAA,EAAO,SAAA;AAAA,EACP,MAAA,EAAQ,SAAA;AAAA,EACR,IAAA,EAAM,SAAA;AAAA,EACN,OAAA,EAAS,SAAA;AAAA,EACT,IAAA,EAAM,SAAA;AAAA,EACN,KAAA,EAAO,SAAA;AAAA,EACP,WAAA,EAAa,SAAA;AAAA,EACb,SAAA,EAAW,SAAA;AAAA,EACX,WAAA,EAAa,SAAA;AAAA,EACb,YAAA,EAAc,SAAA;AAAA,EACd,UAAA,EAAY,SAAA;AAAA,EACZ,aAAA,EAAe,SAAA;AAAA,EACf,UAAA,EAAY,SAAA;AAAA,EACZ,WAAA,EAAa;AACf;ACrBO,SAAS,cAAA,CACd,GAAA,EACA,GAAA,EACA,OAAA,GAAU,IAAA,EACJ;AACN,EAAA,MAAM,MAAA,GAAS,OAAO,GAAG,CAAA;AACzB,EAAA,MAAA,CAAO,OAAA,GAAU,GAAA;AAEjB,EAAA,SAAA,CAAU,MAAM;AACd,IAAA,IAAI,CAAC,OAAA,EAAS;AACd,IAAA,MAAM,KAAK,GAAA,CAAI,OAAA;AACf,IAAA,IAAI,CAAC,EAAA,EAAI;AAET,IAAA,MAAM,MAAM,MAAM;AAChB,MAAA,IAAI,GAAG,WAAA,GAAc,CAAA,IAAK,GAAG,YAAA,GAAe,CAAA,SAAU,OAAA,EAAQ;AAAA,IAChE,CAAA;AAEA,IAAA,MAAM,QAAA,GAAW,IAAI,cAAA,CAAe,GAAG,CAAA;AACvC,IAAA,QAAA,CAAS,QAAQ,EAAE,CAAA;AACnB,IAAA,GAAA,EAAI;AAEJ,IAAA,OAAO,MAAM,SAAS,UAAA,EAAW;AAAA,EACnC,CAAA,EAAG,CAAC,GAAA,EAAK,OAAO,CAAC,CAAA;AACnB;;;ACxBA,IAAM,YAAA,GACJ,wFAAA;AAGF,SAAS,WAAW,IAAA,EAA4B;AAC9C,EAAA,IAAI,CAAC,MAAM,OAAO,EAAA;AAClB,EAAA,MAAM,GAAA,GAAM,KAAK,MAAA,CAAO,MAAA;AACxB,EAAA,MAAM,QAAkB,EAAC;AACzB,EAAA,KAAA,IAAS,CAAA,GAAI,CAAA,EAAG,CAAA,GAAI,GAAA,CAAI,QAAQ,CAAA,EAAA,EAAK;AACnC,IAAA,MAAM,IAAA,GAAO,GAAA,CAAI,OAAA,CAAQ,CAAC,CAAA;AAC1B,IAAA,IAAI,MAAM,KAAA,CAAM,IAAA,CAAK,IAAA,CAAK,iBAAA,CAAkB,IAAI,CAAC,CAAA;AAAA,EACnD;AACA,EAAA,OAAO,MAAM,IAAA,CAAK,IAAI,CAAA,CAAE,OAAA,CAAQ,QAAQ,EAAE,CAAA;AAC5C;AASO,SAAS,WAAA,CACd,YAAA,EACA,OAAA,GAA2B,EAAC,EACZ;AAChB,EAAA,MAAM,QAAA,GAAWA,OAAqB,IAAI,CAAA;AAC1C,EAAA,MAAM,MAAA,GAASA,OAAwB,IAAI,CAAA;AAC3C,EAAA,MAAM,OAAA,GAAUA,OAAO,OAAO,CAAA;AAC9B,EAAA,OAAA,CAAQ,OAAA,GAAU,OAAA;AAGlB,EAAA,MAAM,SAAA,GAAYA,OAA8B,IAAI,CAAA;AACpD,EAAA,IAAI,SAAA,CAAU,YAAY,IAAA,EAAM;AAC9B,IAAA,SAAA,CAAU,OAAA,GAAU;AAAA,MAClB,IAAI,KAAA,GAAQ;AACV,QAAA,OAAO,QAAA,CAAS,OAAA;AAAA,MAClB,CAAA;AAAA,MACA,OAAO,CAAC,CAAA,KAAM,QAAA,CAAS,OAAA,EAAS,MAAM,CAAC,CAAA;AAAA,MACvC,SAAS,CAAC,CAAA,KAAM,QAAA,CAAS,OAAA,EAAS,QAAQ,CAAC,CAAA;AAAA,MAC3C,KAAA,EAAO,MAAM,QAAA,CAAS,OAAA,EAAS,KAAA,EAAM;AAAA,MACrC,KAAA,EAAO,MAAM,QAAA,CAAS,OAAA,EAAS,KAAA,EAAM;AAAA,MACrC,KAAK,MAAM;AACT,QAAA,IAAI;AACF,UAAA,MAAA,CAAO,SAAS,GAAA,EAAI;AAAA,QACtB,CAAA,CAAA,MAAQ;AAAA,QAER;AAAA,MACF,CAAA;AAAA,MACA,KAAA,EAAO,MAAM,QAAA,CAAS,OAAA,EAAS,KAAA,EAAM;AAAA,MACrC,SAAA,EAAW,MAAM,UAAA,CAAW,QAAA,CAAS,OAAO,CAAA;AAAA,MAC5C,YAAA,EAAc,MAAM,QAAA,CAAS,OAAA,EAAS,cAAa,IAAK;AAAA,KAC1D;AAAA,EACF;AACA,EAAA,MAAM,SAAS,SAAA,CAAU,OAAA;AAGzB,EAAAC,UAAU,MAAM;AACd,IAAA,MAAM,KAAK,YAAA,CAAa,OAAA;AACxB,IAAA,IAAI,CAAC,EAAA,EAAI;AACT,IAAA,MAAM,IAAI,OAAA,CAAQ,OAAA;AAElB,IAAA,MAAM,IAAA,GAAO,IAAIC,UAAA,CAAM;AAAA,MACrB,KAAA,EAAO,EAAE,KAAA,IAAS,cAAA;AAAA,MAClB,MAAM,CAAA,CAAE,IAAA;AAAA,MACR,MAAM,CAAA,CAAE,IAAA;AAAA,MACR,YAAA,EAAc,EAAE,QAAA,IAAY,KAAA;AAAA,MAC5B,WAAA,EAAa,EAAE,WAAA,IAAe,IAAA;AAAA,MAC9B,WAAA,EAAa,EAAE,WAAA,IAAe,OAAA;AAAA,MAC9B,UAAA,EAAY,EAAE,UAAA,IAAc,YAAA;AAAA,MAC5B,QAAA,EAAU,EAAE,QAAA,IAAY,EAAA;AAAA,MACxB,UAAA,EAAY,EAAE,UAAA,IAAc,GAAA;AAAA,MAC5B,gBAAA,EAAkB;AAAA,KACnB,CAAA;AACD,IAAA,MAAM,QAAA,GAAW,IAAI,QAAA,EAAS;AAC9B,IAAA,IAAA,CAAK,UAAU,QAAQ,CAAA;AACvB,IAAA,IAAA,CAAK,KAAK,EAAE,CAAA;AACZ,IAAA,QAAA,CAAS,OAAA,GAAU,IAAA;AACnB,IAAA,MAAA,CAAO,OAAA,GAAU,QAAA;AAEjB,IAAA,MAAM,OAAA,GAAU,KAAK,MAAA,CAAO,CAAC,MAAM,OAAA,CAAQ,OAAA,CAAQ,MAAA,GAAS,CAAC,CAAC,CAAA;AAC9D,IAAA,MAAM,YAAY,IAAA,CAAK,QAAA;AAAA,MAAS,CAAC,EAAE,IAAA,EAAM,IAAA,EAAK,KAC5C,OAAA,CAAQ,OAAA,CAAQ,QAAA,GAAW,EAAE,IAAA,EAAM,IAAA,EAAM;AAAA,KAC3C;AAEA,IAAA,IAAI,CAAA,CAAE,aAAA,EAAe,IAAA,CAAK,KAAA,CAAM,EAAE,aAAa,CAAA;AAC/C,IAAA,IAAA,CAAK,CAAA,CAAE,OAAO,IAAA,KAAS,EAAA,CAAG,cAAc,CAAA,IAAK,EAAA,CAAG,eAAe,CAAA,EAAG;AAChE,MAAA,IAAI;AACF,QAAA,QAAA,CAAS,GAAA,EAAI;AAAA,MACf,CAAA,CAAA,MAAQ;AAAA,MAER;AAAA,IACF;AAEA,IAAA,OAAO,MAAM;AACX,MAAA,OAAA,CAAQ,OAAA,EAAQ;AAChB,MAAA,SAAA,CAAU,OAAA,EAAQ;AAClB,MAAA,IAAA,CAAK,OAAA,EAAQ;AACb,MAAA,QAAA,CAAS,OAAA,GAAU,IAAA;AACnB,MAAA,MAAA,CAAO,OAAA,GAAU,IAAA;AAAA,IACnB,CAAA;AAAA,EAEF,CAAA,EAAG,CAAC,YAAY,CAAC,CAAA;AAGjB,EAAAD,UAAU,MAAM;AACd,IAAA,MAAM,IAAI,QAAA,CAAS,OAAA;AACnB,IAAA,IAAI,CAAC,CAAA,EAAG;AACR,IAAA,IAAI,OAAA,CAAQ,KAAA,EAAO,CAAA,CAAE,OAAA,CAAQ,QAAQ,OAAA,CAAQ,KAAA;AAC7C,IAAA,IAAI,QAAQ,QAAA,KAAa,MAAA,EAAW,CAAA,CAAE,OAAA,CAAQ,eAAe,OAAA,CAAQ,QAAA;AACrE,IAAA,IAAI,QAAQ,WAAA,KAAgB,MAAA,EAAW,CAAA,CAAE,OAAA,CAAQ,cAAc,OAAA,CAAQ,WAAA;AACvE,IAAA,IAAI,OAAA,CAAQ,WAAA,EAAa,CAAA,CAAE,OAAA,CAAQ,cAAc,OAAA,CAAQ,WAAA;AACzD,IAAA,IAAI,OAAA,CAAQ,QAAA,EAAU,CAAA,CAAE,OAAA,CAAQ,WAAW,OAAA,CAAQ,QAAA;AACnD,IAAA,IAAI,OAAA,CAAQ,UAAA,EAAY,CAAA,CAAE,OAAA,CAAQ,aAAa,OAAA,CAAQ,UAAA;AAAA,EACzD,CAAA,EAAG,CAAC,OAAA,CAAQ,KAAA,EAAO,QAAQ,QAAA,EAAU,OAAA,CAAQ,WAAA,EAAa,OAAA,CAAQ,WAAA,EAAa,OAAA,CAAQ,QAAA,EAAU,OAAA,CAAQ,UAAU,CAAC,CAAA;AAGpH,EAAA,cAAA,CAAe,cAAc,MAAM,MAAA,CAAO,KAAI,EAAG,OAAA,CAAQ,OAAO,IAAI,CAAA;AAEpE,EAAA,OAAO,MAAA;AACT;;;ACzHO,SAAS,UAAA,CACd,SACA,IAAA,EAC0C;AAC1C,EAAA,IAAI,IAAA,KAAS,SAAS,OAAO,IAAA;AAC7B,EAAA,IAAI,IAAA,CAAK,UAAA,CAAW,OAAO,CAAA,EAAG,OAAO,EAAE,KAAA,EAAO,KAAA,EAAO,KAAA,EAAO,IAAA,CAAK,KAAA,CAAM,OAAA,CAAQ,MAAM,CAAA,EAAE;AACvF,EAAA,OAAO,EAAE,KAAA,EAAO,IAAA,EAAM,KAAA,EAAO,IAAA,EAAK;AACpC;ACWO,IAAM,QAAA,GAAW,UAAA,CAA0C,SAASE,SAAAA,CACzE;AAAA,EACE,MAAA;AAAA,EACA,KAAA;AAAA,EACA,IAAA;AAAA,EACA,IAAA;AAAA,EACA,GAAA,GAAM,IAAA;AAAA,EACN,QAAA;AAAA,EACA,WAAA;AAAA,EACA,WAAA;AAAA,EACA,UAAA;AAAA,EACA,QAAA;AAAA,EACA,UAAA;AAAA,EACA,aAAA;AAAA,EACA,MAAA;AAAA,EACA,QAAA;AAAA,EACA,SAAA;AAAA,EACA,KAAA;AAAA,EACA,GAAG;AACL,CAAA,EACA,GAAA,EACA;AACA,EAAA,MAAM,YAAA,GAAeH,OAAuB,IAAI,CAAA;AAEhD,EAAA,MAAM,MAAA,GAAS,YAAY,YAAA,EAAc;AAAA,IACvC,KAAA;AAAA,IACA,IAAA;AAAA,IACA,IAAA;AAAA,IACA,GAAA;AAAA,IACA,QAAA;AAAA,IACA,WAAA;AAAA,IACA,WAAA;AAAA,IACA,UAAA;AAAA,IACA,QAAA;AAAA,IACA,UAAA;AAAA;AAAA;AAAA,IAGA,aAAA,EAAe,MAAA,KAAW,MAAA,GAAY,aAAA,GAAgB,MAAA;AAAA,IACtD,MAAA;AAAA,IACA;AAAA,GACD,CAAA;AAED,EAAA,mBAAA,CAAoB,GAAA,EAAK,MAAM,MAAA,EAAQ,CAAC,MAAM,CAAC,CAAA;AAI/C,EAAA,MAAM,OAAA,GAAUA,OAAO,EAAE,CAAA;AACzB,EAAAC,UAAU,MAAM;AACd,IAAA,IAAI,WAAW,MAAA,EAAW;AAC1B,IAAA,MAAM,MAAA,GAAS,UAAA,CAAW,OAAA,CAAQ,OAAA,EAAS,MAAM,CAAA;AACjD,IAAA,IAAI,CAAC,MAAA,EAAQ;AACb,IAAA,IAAI,MAAA,CAAO,KAAA,EAAO,MAAA,CAAO,KAAA,EAAM;AAC/B,IAAA,MAAA,CAAO,KAAA,CAAM,OAAO,KAAK,CAAA;AACzB,IAAA,OAAA,CAAQ,OAAA,GAAU,MAAA;AAAA,EACpB,CAAA,EAAG,CAAC,MAAA,EAAQ,MAAM,CAAC,CAAA;AAEnB,EAAA,uBACE,GAAA;AAAA,IAAC,KAAA;AAAA,IAAA;AAAA,MACC,GAAA,EAAK,YAAA;AAAA,MACL,qBAAA,EAAoB,EAAA;AAAA,MACpB,eAAA,EAAe,WAAW,EAAA,GAAK,MAAA;AAAA,MAC/B,SAAA;AAAA,MACA,OAAO,EAAE,KAAA,EAAO,QAAQ,MAAA,EAAQ,MAAA,EAAQ,GAAG,KAAA,EAAM;AAAA,MAChD,GAAG;AAAA;AAAA,GACN;AAEJ,CAAC;ACzCM,SAAS,mBAAmB,OAAA,EAAwD;AACzF,EAAA,MAAM,CAAC,MAAA,EAAQ,SAAS,IAAI,QAAA,CAAS,OAAA,CAAQ,WAAW,EAAE,CAAA;AAE1D,EAAA,MAAM,YAAA,GAAeD,MAAAA,CAAO,OAAA,CAAQ,SAAS,CAAA;AAC7C,EAAA,YAAA,CAAa,UAAU,OAAA,CAAQ,SAAA;AAE/B,EAAAC,UAAU,MAAM;AACd,IAAA,MAAM,KAAA,GAAQ,YAAA,CAAa,OAAA,CAAQ,SAAA,CAAU,CAAC,KAAA,KAAU,SAAA,CAAU,CAAC,CAAA,KAAM,CAAA,GAAI,KAAK,CAAC,CAAA;AACnF,IAAA,OAAO,MAAM;AACX,MAAA,IAAI,OAAO,KAAA,KAAU,UAAA,EAAY,KAAA,EAAM;AAAA,IACzC,CAAA;AAAA,EAEF,CAAA,EAAG,EAAE,CAAA;AAEL,EAAA,MAAM,QAAA,GAAW,WAAA,CAAY,CAAC,IAAA,KAAiB;AAC7C,IAAA,KAAK,YAAA,CAAa,OAAA,CAAQ,IAAA,CAAK,IAAI,CAAA;AAAA,EACrC,CAAA,EAAG,EAAE,CAAA;AAEL,EAAA,MAAM,MAAA,GAAS,WAAA,CAAY,CAAC,KAAA,KAAkB,SAAA,CAAU,CAAC,CAAA,KAAM,CAAA,GAAI,KAAK,CAAA,EAAG,EAAE,CAAA;AAC7E,EAAA,MAAM,QAAQ,WAAA,CAAY,MAAM,UAAU,EAAE,CAAA,EAAG,EAAE,CAAA;AAEjD,EAAA,OAAO,EAAE,MAAA,EAAQ,QAAA,EAAU,MAAA,EAAQ,KAAA,EAAM;AAC3C","file":"index.js","sourcesContent":["import type { TerminalTheme } from \"./types\";\n\n/**\n * The default \"sexy\" Fancy dark terminal theme — a zinc surface, violet cursor,\n * and a balanced ANSI palette drawn from the react-fancy Tailwind v4 tokens.\n * Pass a `theme` prop / option to override.\n */\nexport const fancyDarkTheme: TerminalTheme = {\n background: \"#09090b\", // zinc-950\n foreground: \"#e4e4e7\", // zinc-200\n cursor: \"#8b5cf6\", // violet-500\n cursorAccent: \"#09090b\",\n selectionBackground: \"#3f3f46\", // zinc-700\n black: \"#18181b\",\n red: \"#f43f5e\",\n green: \"#10b981\",\n yellow: \"#f59e0b\",\n blue: \"#3b82f6\",\n magenta: \"#8b5cf6\",\n cyan: \"#22d3ee\",\n white: \"#e4e4e7\",\n brightBlack: \"#52525b\",\n brightRed: \"#fb7185\",\n brightGreen: \"#34d399\",\n brightYellow: \"#fbbf24\",\n brightBlue: \"#60a5fa\",\n brightMagenta: \"#a78bfa\",\n brightCyan: \"#67e8f9\",\n brightWhite: \"#fafafa\",\n};\n","import { useEffect, useRef, type RefObject } from \"react\";\n\n/**\n * Run `fit` whenever the container resizes — and once it first has a real,\n * non-zero box. Guards the hidden-tab / late-mount 0×0 case: fitting a collapsed\n * container yields garbage rows/cols (or throws), so we simply skip until there\n * is layout to measure.\n */\nexport function useTerminalFit(\n ref: RefObject<HTMLElement | null>,\n fit: () => void,\n enabled = true,\n): void {\n const fitRef = useRef(fit);\n fitRef.current = fit;\n\n useEffect(() => {\n if (!enabled) return;\n const el = ref.current;\n if (!el) return;\n\n const run = () => {\n if (el.clientWidth > 0 && el.clientHeight > 0) fitRef.current();\n };\n\n const observer = new ResizeObserver(run);\n observer.observe(el);\n run(); // initial — fires once the box has layout\n\n return () => observer.disconnect();\n }, [ref, enabled]);\n}\n","import { useEffect, useRef, type RefObject } from \"react\";\nimport { Terminal as XTerm } from \"@xterm/xterm\";\nimport { FitAddon } from \"@xterm/addon-fit\";\nimport { fancyDarkTheme } from \"../theme\";\nimport { useTerminalFit } from \"./use-terminal-fit\";\nimport type { TerminalHandle, TerminalOptions } from \"../types\";\n\nconst DEFAULT_FONT =\n 'ui-monospace, SFMono-Regular, \"SF Mono\", Menlo, Consolas, \"Liberation Mono\", monospace';\n\n/** Read the whole xterm buffer as plain text — what an agent \"sees\". */\nfunction readBuffer(term: XTerm | null): string {\n if (!term) return \"\";\n const buf = term.buffer.active;\n const lines: string[] = [];\n for (let i = 0; i < buf.length; i++) {\n const line = buf.getLine(i);\n if (line) lines.push(line.translateToString(true));\n }\n return lines.join(\"\\n\").replace(/\\n+$/, \"\");\n}\n\n/**\n * Create + own an xterm.js instance for a container, returning a stable\n * {@link TerminalHandle}. The headless engine layer behind `<Terminal>` — use it\n * directly when you want the terminal without the component shell (custom chrome,\n * an agent bridge host, tests). Options are read live, so changing them re-applies\n * without tearing down the terminal.\n */\nexport function useTerminal(\n containerRef: RefObject<HTMLElement | null>,\n options: TerminalOptions = {},\n): TerminalHandle {\n const xtermRef = useRef<XTerm | null>(null);\n const fitRef = useRef<FitAddon | null>(null);\n const optsRef = useRef(options);\n optsRef.current = options;\n\n // Stable handle — its methods always operate on the live xterm via the ref.\n const handleRef = useRef<TerminalHandle | null>(null);\n if (handleRef.current === null) {\n handleRef.current = {\n get xterm() {\n return xtermRef.current;\n },\n write: (d) => xtermRef.current?.write(d),\n writeln: (d) => xtermRef.current?.writeln(d),\n clear: () => xtermRef.current?.clear(),\n reset: () => xtermRef.current?.reset(),\n fit: () => {\n try {\n fitRef.current?.fit();\n } catch {\n /* container not measurable yet — ignore */\n }\n },\n focus: () => xtermRef.current?.focus(),\n getBuffer: () => readBuffer(xtermRef.current),\n getSelection: () => xtermRef.current?.getSelection() ?? \"\",\n };\n }\n const handle = handleRef.current;\n\n // Create the engine once for the container.\n useEffect(() => {\n const el = containerRef.current;\n if (!el) return;\n const o = optsRef.current;\n\n const term = new XTerm({\n theme: o.theme ?? fancyDarkTheme,\n rows: o.rows,\n cols: o.cols,\n disableStdin: o.readOnly ?? false,\n cursorBlink: o.cursorBlink ?? true,\n cursorStyle: o.cursorStyle ?? \"block\",\n fontFamily: o.fontFamily ?? DEFAULT_FONT,\n fontSize: o.fontSize ?? 13,\n scrollback: o.scrollback ?? 1000,\n allowProposedApi: true,\n });\n const fitAddon = new FitAddon();\n term.loadAddon(fitAddon);\n term.open(el);\n xtermRef.current = term;\n fitRef.current = fitAddon;\n\n const dataSub = term.onData((d) => optsRef.current.onData?.(d));\n const resizeSub = term.onResize(({ cols, rows }) =>\n optsRef.current.onResize?.({ cols, rows }),\n );\n\n if (o.initialOutput) term.write(o.initialOutput);\n if ((o.fit ?? true) && el.clientWidth > 0 && el.clientHeight > 0) {\n try {\n fitAddon.fit();\n } catch {\n /* ignore */\n }\n }\n\n return () => {\n dataSub.dispose();\n resizeSub.dispose();\n term.dispose();\n xtermRef.current = null;\n fitRef.current = null;\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [containerRef]);\n\n // Re-apply reactive options without tearing the terminal down.\n useEffect(() => {\n const t = xtermRef.current;\n if (!t) return;\n if (options.theme) t.options.theme = options.theme;\n if (options.readOnly !== undefined) t.options.disableStdin = options.readOnly;\n if (options.cursorBlink !== undefined) t.options.cursorBlink = options.cursorBlink;\n if (options.cursorStyle) t.options.cursorStyle = options.cursorStyle;\n if (options.fontSize) t.options.fontSize = options.fontSize;\n if (options.fontFamily) t.options.fontFamily = options.fontFamily;\n }, [options.theme, options.readOnly, options.cursorBlink, options.cursorStyle, options.fontSize, options.fontFamily]);\n\n // Auto-fit on container resize (0×0-guarded).\n useTerminalFit(containerRef, () => handle.fit(), options.fit ?? true);\n\n return handle;\n}\n","/**\n * Decide what to push to the terminal when the controlled `output` changes.\n * Terminals are append-only streams, so the common case (output grew) writes\n * only the delta; a value that no longer extends what we've written means a\n * wholesale replace, so reset + rewrite. Returns null when nothing changed.\n */\nexport function diffOutput(\n written: string,\n next: string,\n): { reset: boolean; write: string } | null {\n if (next === written) return null;\n if (next.startsWith(written)) return { reset: false, write: next.slice(written.length) };\n return { reset: true, write: next };\n}\n","import { forwardRef, useEffect, useImperativeHandle, useRef } from \"react\";\nimport { useTerminal } from \"../hooks/use-terminal\";\nimport { diffOutput } from \"../output-diff\";\nimport type { TerminalHandle, TerminalProps } from \"../types\";\n\n/**\n * Human+ `<Terminal>` — a controlled, themeable xterm.js terminal.\n *\n * - **Authoring surface:** terse, controlled (`output` + `onData`), JSON-friendly\n * props (rows/cols, theme tokens, initial buffer), a stable `data-fancy-terminal`\n * handle, and a ref exposing {@link TerminalHandle} (write/clear/fit/getBuffer/…).\n * - **Inhabited surface:** the same handle is what an MCP bridge drives, so an\n * embedded agent reads the buffer + writes input without DOM-scraping.\n *\n * The parent must have a height — the terminal fits its container (like any xterm\n * surface); a 0-height parent collapses it.\n *\n * ```tsx\n * const [out, setOut] = useState(\"\");\n * <div style={{ height: 360 }}>\n * <Terminal output={out} onData={(d) => backend.send(d)} />\n * </div>\n * ```\n */\nexport const Terminal = forwardRef<TerminalHandle, TerminalProps>(function Terminal(\n {\n output,\n theme,\n rows,\n cols,\n fit = true,\n readOnly,\n cursorBlink,\n cursorStyle,\n fontFamily,\n fontSize,\n scrollback,\n initialOutput,\n onData,\n onResize,\n className,\n style,\n ...rest\n },\n ref,\n) {\n const containerRef = useRef<HTMLDivElement>(null);\n\n const handle = useTerminal(containerRef, {\n theme,\n rows,\n cols,\n fit,\n readOnly,\n cursorBlink,\n cursorStyle,\n fontFamily,\n fontSize,\n scrollback,\n // `initialOutput` is for uncontrolled use; with a controlled `output` the\n // diffing effect below owns the buffer, so don't double-write.\n initialOutput: output === undefined ? initialOutput : undefined,\n onData,\n onResize,\n });\n\n useImperativeHandle(ref, () => handle, [handle]);\n\n // Controlled output: write only the appended delta; reset + rewrite if the\n // value diverges from what we've already written (a wholesale replace).\n const written = useRef(\"\");\n useEffect(() => {\n if (output === undefined) return;\n const change = diffOutput(written.current, output);\n if (!change) return;\n if (change.reset) handle.reset();\n handle.write(change.write);\n written.current = output;\n }, [output, handle]);\n\n return (\n <div\n ref={containerRef}\n data-fancy-terminal=\"\"\n data-readonly={readOnly ? \"\" : undefined}\n className={className}\n style={{ width: \"100%\", height: \"100%\", ...style }}\n {...rest}\n />\n );\n});\n","import { useCallback, useEffect, useRef, useState } from \"react\";\n\n/**\n * A streamed terminal backend — a PTY, an SSH session, or a command runner.\n * `subscribe` pushes output chunks (wire it to an Echo channel or a fancy-query\n * `useFancyStream` feed); `send` forwards the user's keystrokes upstream.\n */\nexport interface TerminalSessionTransport {\n /** Forward user input / keystrokes to the backend. */\n send: (data: string) => void | Promise<void>;\n /** Subscribe to backend output chunks; return an unsubscribe function. */\n subscribe: (onChunk: (chunk: string) => void) => (() => void) | void;\n}\n\nexport interface UseTerminalSessionOptions {\n transport: TerminalSessionTransport;\n /** Replayed history written before live output starts. */\n initial?: string;\n}\n\nexport interface TerminalSessionApi {\n /** Accumulated output — feed to `<Terminal output={output} />`. */\n output: string;\n /** Forward keystrokes upstream — wire to `<Terminal onData={sendData} />`. */\n sendData: (data: string) => void;\n /** Append a chunk locally (e.g. a client-side echo or banner). */\n append: (chunk: string) => void;\n /** Clear the local buffer. */\n clear: () => void;\n}\n\n/**\n * Bind a `<Terminal>` to a streamed backend: accumulates incoming output into a\n * controlled buffer and forwards keystrokes upstream — the glue between the\n * display surface and a PTY/command channel.\n *\n * ```tsx\n * const session = useTerminalSession({\n * transport: {\n * send: (d) => echo.private(`pty.${id}`).whisper(\"stdin\", { d }),\n * subscribe: (onChunk) => {\n * const ch = echo.private(`pty.${id}`).listen(\".stdout\", (e) => onChunk(e.chunk));\n * return () => echo.leave(`pty.${id}`);\n * },\n * },\n * });\n * <Terminal output={session.output} onData={session.sendData} />\n * ```\n */\nexport function useTerminalSession(options: UseTerminalSessionOptions): TerminalSessionApi {\n const [output, setOutput] = useState(options.initial ?? \"\");\n\n const transportRef = useRef(options.transport);\n transportRef.current = options.transport;\n\n useEffect(() => {\n const unsub = transportRef.current.subscribe((chunk) => setOutput((o) => o + chunk));\n return () => {\n if (typeof unsub === \"function\") unsub();\n };\n // subscribe once; the transport is read live via the ref.\n }, []);\n\n const sendData = useCallback((data: string) => {\n void transportRef.current.send(data);\n }, []);\n\n const append = useCallback((chunk: string) => setOutput((o) => o + chunk), []);\n const clear = useCallback(() => setOutput(\"\"), []);\n\n return { output, sendData, append, clear };\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/theme.ts","../src/hooks/use-terminal-fit.ts","../src/hooks/use-terminal.ts","../src/types.ts","../src/components/ShellSwitcher.tsx","../src/output-diff.ts","../src/shell-select.ts","../src/components/Terminal.tsx","../src/hooks/use-terminal-session.ts"],"names":["useRef","useEffect","XTerm","Terminal","useState","useCallback","jsx","jsxs"],"mappings":"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","file":"index.js","sourcesContent":["import type { TerminalTheme } from \"./types\";\n\n/**\n * The default \"sexy\" Fancy dark terminal theme — a zinc surface, violet cursor,\n * and a balanced ANSI palette drawn from the react-fancy Tailwind v4 tokens.\n * Pass a `theme` prop / option to override.\n */\nexport const fancyDarkTheme: TerminalTheme = {\n background: \"#09090b\", // zinc-950\n foreground: \"#e4e4e7\", // zinc-200\n cursor: \"#8b5cf6\", // violet-500\n cursorAccent: \"#09090b\",\n selectionBackground: \"#3f3f46\", // zinc-700\n black: \"#18181b\",\n red: \"#f43f5e\",\n green: \"#10b981\",\n yellow: \"#f59e0b\",\n blue: \"#3b82f6\",\n magenta: \"#8b5cf6\",\n cyan: \"#22d3ee\",\n white: \"#e4e4e7\",\n brightBlack: \"#52525b\",\n brightRed: \"#fb7185\",\n brightGreen: \"#34d399\",\n brightYellow: \"#fbbf24\",\n brightBlue: \"#60a5fa\",\n brightMagenta: \"#a78bfa\",\n brightCyan: \"#67e8f9\",\n brightWhite: \"#fafafa\",\n};\n","import { useEffect, useRef, type RefObject } from \"react\";\n\n/**\n * Run `fit` whenever the container resizes — and once it first has a real,\n * non-zero box. Guards the hidden-tab / late-mount 0×0 case: fitting a collapsed\n * container yields garbage rows/cols (or throws), so we simply skip until there\n * is layout to measure.\n */\nexport function useTerminalFit(\n ref: RefObject<HTMLElement | null>,\n fit: () => void,\n enabled = true,\n): void {\n const fitRef = useRef(fit);\n fitRef.current = fit;\n\n useEffect(() => {\n if (!enabled) return;\n const el = ref.current;\n if (!el) return;\n\n const run = () => {\n if (el.clientWidth > 0 && el.clientHeight > 0) fitRef.current();\n };\n\n const observer = new ResizeObserver(run);\n observer.observe(el);\n run(); // initial — fires once the box has layout\n\n return () => observer.disconnect();\n }, [ref, enabled]);\n}\n","import { useEffect, useRef, type RefObject } from \"react\";\nimport { Terminal as XTerm } from \"@xterm/xterm\";\nimport { FitAddon } from \"@xterm/addon-fit\";\nimport { fancyDarkTheme } from \"../theme\";\nimport { useTerminalFit } from \"./use-terminal-fit\";\nimport type { TerminalHandle, TerminalOptions } from \"../types\";\n\nconst DEFAULT_FONT =\n 'ui-monospace, SFMono-Regular, \"SF Mono\", Menlo, Consolas, \"Liberation Mono\", monospace';\n\n/** Read the whole xterm buffer as plain text — what an agent \"sees\". */\nfunction readBuffer(term: XTerm | null): string {\n if (!term) return \"\";\n const buf = term.buffer.active;\n const lines: string[] = [];\n for (let i = 0; i < buf.length; i++) {\n const line = buf.getLine(i);\n if (line) lines.push(line.translateToString(true));\n }\n return lines.join(\"\\n\").replace(/\\n+$/, \"\");\n}\n\n/**\n * Create + own an xterm.js instance for a container, returning a stable\n * {@link TerminalHandle}. The headless engine layer behind `<Terminal>` — use it\n * directly when you want the terminal without the component shell (custom chrome,\n * an agent bridge host, tests). Options are read live, so changing them re-applies\n * without tearing down the terminal.\n */\nexport function useTerminal(\n containerRef: RefObject<HTMLElement | null>,\n options: TerminalOptions = {},\n): TerminalHandle {\n const xtermRef = useRef<XTerm | null>(null);\n const fitRef = useRef<FitAddon | null>(null);\n const optsRef = useRef(options);\n optsRef.current = options;\n\n // Stable handle — its methods always operate on the live xterm via the ref.\n const handleRef = useRef<TerminalHandle | null>(null);\n if (handleRef.current === null) {\n handleRef.current = {\n get xterm() {\n return xtermRef.current;\n },\n write: (d) => xtermRef.current?.write(d),\n writeln: (d) => xtermRef.current?.writeln(d),\n clear: () => xtermRef.current?.clear(),\n reset: () => xtermRef.current?.reset(),\n fit: () => {\n try {\n fitRef.current?.fit();\n } catch {\n /* container not measurable yet — ignore */\n }\n },\n focus: () => xtermRef.current?.focus(),\n getBuffer: () => readBuffer(xtermRef.current),\n getSelection: () => xtermRef.current?.getSelection() ?? \"\",\n // Shell selection is owned by the <Terminal> component layer (it needs the\n // `shells` list + onShellChange). The headless engine is shell-agnostic, so\n // these are no-ops here and get overridden when <Terminal> composes them.\n setShell: () => {},\n getShell: () => undefined,\n };\n }\n const handle = handleRef.current;\n\n // Create the engine once for the container.\n useEffect(() => {\n const el = containerRef.current;\n if (!el) return;\n const o = optsRef.current;\n\n const term = new XTerm({\n theme: o.theme ?? fancyDarkTheme,\n rows: o.rows,\n cols: o.cols,\n disableStdin: o.readOnly ?? false,\n cursorBlink: o.cursorBlink ?? true,\n cursorStyle: o.cursorStyle ?? \"block\",\n fontFamily: o.fontFamily ?? DEFAULT_FONT,\n fontSize: o.fontSize ?? 13,\n scrollback: o.scrollback ?? 1000,\n allowProposedApi: true,\n });\n const fitAddon = new FitAddon();\n term.loadAddon(fitAddon);\n term.open(el);\n xtermRef.current = term;\n fitRef.current = fitAddon;\n\n const dataSub = term.onData((d) => optsRef.current.onData?.(d));\n const resizeSub = term.onResize(({ cols, rows }) =>\n optsRef.current.onResize?.({ cols, rows }),\n );\n\n if (o.initialOutput) term.write(o.initialOutput);\n if ((o.fit ?? true) && el.clientWidth > 0 && el.clientHeight > 0) {\n try {\n fitAddon.fit();\n } catch {\n /* ignore */\n }\n }\n\n return () => {\n dataSub.dispose();\n resizeSub.dispose();\n term.dispose();\n xtermRef.current = null;\n fitRef.current = null;\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [containerRef]);\n\n // Re-apply reactive options without tearing the terminal down.\n useEffect(() => {\n const t = xtermRef.current;\n if (!t) return;\n if (options.theme) t.options.theme = options.theme;\n if (options.readOnly !== undefined) t.options.disableStdin = options.readOnly;\n if (options.cursorBlink !== undefined) t.options.cursorBlink = options.cursorBlink;\n if (options.cursorStyle) t.options.cursorStyle = options.cursorStyle;\n if (options.fontSize) t.options.fontSize = options.fontSize;\n if (options.fontFamily) t.options.fontFamily = options.fontFamily;\n }, [options.theme, options.readOnly, options.cursorBlink, options.cursorStyle, options.fontSize, options.fontFamily]);\n\n // Auto-fit on container resize (0×0-guarded).\n useTerminalFit(containerRef, () => handle.fit(), options.fit ?? true);\n\n return handle;\n}\n","import type { HTMLAttributes } from \"react\";\nimport type { ITheme, Terminal as XTerm } from \"@xterm/xterm\";\n\n/** xterm.js color theme (re-exported for ergonomic typing). */\nexport type TerminalTheme = ITheme;\n\nexport type CursorStyle = \"block\" | \"underline\" | \"bar\";\n\n/**\n * A selectable shell / profile. Fully JSON-friendly (primitives only) so an agent\n * can emit it. fancy-term owns the *selected-shell state + UI + events*; the\n * `command` / `args` / `cwd` fields are **host hints** — fancy-term never spawns a\n * shell, the host reconnects its PTY/command backend to the chosen profile.\n */\nexport interface ShellProfile {\n /** Stable key, e.g. `\"powershell\"`. */\n id: string;\n /** Display label, e.g. `\"PowerShell\"`. */\n label: string;\n /** Optional short glyph / emoji / single char (kept JSON-friendly — no ReactNode). */\n icon?: string;\n /** Host hint — the executable, e.g. `\"pwsh\"` / `\"cmd.exe\"`. */\n command?: string;\n /** Host hint — launch args. */\n args?: string[];\n /** Host hint — working directory. */\n cwd?: string;\n}\n\n/**\n * Imperative handle exposed via the `<Terminal>` ref and returned by\n * {@link useTerminal}. The same surface an MCP bridge drives so an embedded\n * agent reads the buffer + writes input without DOM-scraping.\n */\nexport interface TerminalHandle {\n /** The underlying xterm.js instance — escape hatch for addons / advanced use. Null before mount. */\n readonly xterm: XTerm | null;\n /** Write raw data (ANSI escape sequences honored) to the terminal. */\n write: (data: string) => void;\n /** Write data followed by CRLF. */\n writeln: (data: string) => void;\n /** Clear the viewport (keeps scrollback). */\n clear: () => void;\n /** Full reset — clears scrollback + terminal state. */\n reset: () => void;\n /** Re-fit to the container (no-op when the box is 0-size or fit is off). */\n fit: () => void;\n /** Focus the terminal input. */\n focus: () => void;\n /** The full buffer as plain text — what an agent \"sees\". */\n getBuffer: () => string;\n /** The current text selection, or \"\" when nothing is selected. */\n getSelection: () => string;\n /**\n * Switch the active shell by id. Resolves the matching {@link ShellProfile}\n * from `shells` and fires `onShellChange`; in uncontrolled mode it also updates\n * the internal selection. No-op when the id isn't in `shells`.\n */\n setShell: (id: string) => void;\n /** The active shell id, or `undefined` when no shell is selected. */\n getShell: () => string | undefined;\n}\n\n/** Engine options shared by `<Terminal>` and {@link useTerminal}. */\nexport interface TerminalOptions {\n /** xterm color theme. Omit for the built-in Fancy dark theme. */\n theme?: TerminalTheme;\n /** Fixed grid size. Omit + leave `fit` on to size from the container. */\n rows?: number;\n cols?: number;\n /** Auto-fit to the container via the fit addon + ResizeObserver. Default true. */\n fit?: boolean;\n /** Block stdin (display-only terminal). Default false. */\n readOnly?: boolean;\n /** Blink the cursor. Default true. */\n cursorBlink?: boolean;\n /** Cursor shape. Default \"block\". */\n cursorStyle?: CursorStyle;\n /** Monospace font stack. */\n fontFamily?: string;\n /** Font size in px. Default 13. */\n fontSize?: number;\n /** Scrollback lines. Default 1000. */\n scrollback?: number;\n /** Data written once on mount, before any controlled `output`. */\n initialOutput?: string;\n /** User keystrokes / paste data (xterm `onData`). Wire to your PTY/command backend. */\n onData?: (data: string) => void;\n /** Notified on terminal resize. */\n onResize?: (size: { cols: number; rows: number }) => void;\n}\n\nexport interface TerminalProps extends TerminalOptions, Omit<HTMLAttributes<HTMLDivElement>, \"onInput\" | \"onResize\" | \"children\"> {\n /**\n * The controlled output buffer. The component diffs against the previous value\n * and writes only the appended delta, so a host can drive the terminal from\n * React state — e.g. streaming command output via fancy-query's\n * `useFancyStream`. Replacing it with a non-extending string resets + rewrites.\n */\n output?: string;\n\n /**\n * The shells / profiles a host offers. fancy-term renders the selector and\n * tracks the choice; the host reacts (reconnects its PTY to the chosen shell).\n * Spread {@link BUILTIN_SHELLS} for sensible presets, but the host owns the list.\n */\n shells?: ShellProfile[];\n /** Controlled active-shell id. Omit for uncontrolled (internal) selection. */\n activeShell?: string;\n /** Fired when the user (or `setShell`) switches shells. */\n onShellChange?: (id: string, profile: ShellProfile) => void;\n /**\n * Render the built-in `<ShellSwitcher>` toolbar above the xterm surface when\n * `shells` is provided. Default `false` — opt-in so existing layout is unchanged.\n */\n showShellBar?: boolean;\n}\n\n/**\n * Resolve a shell id to its {@link ShellProfile} within a list. Pure helper —\n * the single place `setShell` / `<ShellSwitcher>` / the session hook agree on\n * what \"switch to id X\" means.\n */\nexport function resolveShell(\n shells: ShellProfile[] | undefined,\n id: string | undefined,\n): ShellProfile | undefined {\n if (!shells || id === undefined) return undefined;\n return shells.find((s) => s.id === id);\n}\n\n/**\n * A small set of sensible built-in shell presets a host can spread / filter.\n * `command` / `args` are host hints only — fancy-term never spawns them.\n */\nexport const BUILTIN_SHELLS: readonly ShellProfile[] = [\n { id: \"cmd\", label: \"Command Prompt\", icon: \">_\", command: \"cmd.exe\" },\n { id: \"powershell\", label: \"Windows PowerShell\", icon: \"PS\", command: \"powershell.exe\" },\n { id: \"pwsh\", label: \"PowerShell\", icon: \"PS\", command: \"pwsh\" },\n {\n id: \"git-bash\",\n label: \"Git Bash\",\n icon: \"\",\n command: \"C:\\\\Program Files\\\\Git\\\\bin\\\\bash.exe\",\n args: [\"--login\", \"-i\"],\n },\n { id: \"bash\", label: \"Bash\", icon: \"$\", command: \"bash\" },\n { id: \"zsh\", label: \"Zsh\", icon: \"%\", command: \"zsh\" },\n] as const;\n","import {\n useCallback,\n useEffect,\n useId,\n useRef,\n useState,\n type CSSProperties,\n type KeyboardEvent,\n} from \"react\";\nimport { resolveShell, type ShellProfile } from \"../types\";\n\nexport interface ShellSwitcherProps {\n /** The available shells / profiles. */\n shells: ShellProfile[];\n /** Controlled selected shell id. */\n value?: string;\n /** Fired when a shell is picked. */\n onChange: (id: string, profile: ShellProfile) => void;\n className?: string;\n style?: CSSProperties;\n /** Disable the control. */\n disabled?: boolean;\n}\n\nconst PANEL_BG = \"#18181b\"; // zinc-900\nconst BORDER = \"#3f3f46\"; // zinc-700\nconst FG = \"#e4e4e7\"; // zinc-200\nconst MUTED = \"#a1a1aa\"; // zinc-400\nconst ACCENT = \"#8b5cf6\"; // violet-500\nconst HOVER_BG = \"#27272a\"; // zinc-800\n\n/**\n * Compact, accessible shell picker styled to the Fancy dark theme. Self-contained\n * (zero third-party deps) — a button that toggles a listbox of `shells`.\n *\n * Stable handles for agents: the root carries `data-fancy-shell-switcher` and each\n * option carries `data-shell-id`, so a bridge can pick a shell without DOM-guessing.\n *\n * Controlled: pass `value` + `onChange`. Keyboard accessible (Arrow / Enter / Esc).\n */\nexport function ShellSwitcher({\n shells,\n value,\n onChange,\n className,\n style,\n disabled,\n}: ShellSwitcherProps) {\n const [open, setOpen] = useState(false);\n const [activeIndex, setActiveIndex] = useState(0);\n const rootRef = useRef<HTMLDivElement>(null);\n const listId = useId();\n\n const selected = resolveShell(shells, value);\n const selectedIndex = selected ? shells.indexOf(selected) : -1;\n\n // Close on outside click.\n useEffect(() => {\n if (!open) return;\n const onDocClick = (e: MouseEvent) => {\n if (rootRef.current && !rootRef.current.contains(e.target as Node)) setOpen(false);\n };\n document.addEventListener(\"mousedown\", onDocClick);\n return () => document.removeEventListener(\"mousedown\", onDocClick);\n }, [open]);\n\n const openMenu = useCallback(() => {\n if (disabled) return;\n setActiveIndex(selectedIndex >= 0 ? selectedIndex : 0);\n setOpen(true);\n }, [disabled, selectedIndex]);\n\n const pick = useCallback(\n (i: number) => {\n const profile = shells[i];\n if (!profile) return;\n onChange(profile.id, profile);\n setOpen(false);\n },\n [shells, onChange],\n );\n\n const onButtonKeyDown = (e: KeyboardEvent) => {\n if (disabled) return;\n if (e.key === \"ArrowDown\" || e.key === \"ArrowUp\" || e.key === \"Enter\" || e.key === \" \") {\n e.preventDefault();\n openMenu();\n }\n };\n\n const onListKeyDown = (e: KeyboardEvent) => {\n if (e.key === \"Escape\") {\n e.preventDefault();\n setOpen(false);\n return;\n }\n if (e.key === \"ArrowDown\") {\n e.preventDefault();\n setActiveIndex((i) => Math.min(shells.length - 1, i + 1));\n } else if (e.key === \"ArrowUp\") {\n e.preventDefault();\n setActiveIndex((i) => Math.max(0, i - 1));\n } else if (e.key === \"Home\") {\n e.preventDefault();\n setActiveIndex(0);\n } else if (e.key === \"End\") {\n e.preventDefault();\n setActiveIndex(shells.length - 1);\n } else if (e.key === \"Enter\" || e.key === \" \") {\n e.preventDefault();\n pick(activeIndex);\n }\n };\n\n const buttonStyle: CSSProperties = {\n display: \"inline-flex\",\n alignItems: \"center\",\n gap: 6,\n height: 26,\n padding: \"0 8px\",\n fontSize: 12,\n lineHeight: 1,\n fontFamily: \"inherit\",\n color: FG,\n background: PANEL_BG,\n border: `1px solid ${BORDER}`,\n borderRadius: 6,\n cursor: disabled ? \"default\" : \"pointer\",\n opacity: disabled ? 0.6 : 1,\n userSelect: \"none\",\n };\n\n return (\n <div\n ref={rootRef}\n data-fancy-shell-switcher=\"\"\n data-active-shell={value ?? undefined}\n className={className}\n style={{ position: \"relative\", display: \"inline-block\", ...style }}\n >\n <button\n type=\"button\"\n aria-haspopup=\"listbox\"\n aria-expanded={open}\n aria-label=\"Select shell\"\n disabled={disabled}\n style={buttonStyle}\n onClick={() => (open ? setOpen(false) : openMenu())}\n onKeyDown={onButtonKeyDown}\n >\n {selected?.icon ? (\n <span aria-hidden=\"true\" style={{ color: ACCENT }}>\n {selected.icon}\n </span>\n ) : null}\n <span>{selected?.label ?? \"Select shell\"}</span>\n <span aria-hidden=\"true\" style={{ color: MUTED, fontSize: 9 }}>\n ▼\n </span>\n </button>\n\n {open ? (\n <ul\n role=\"listbox\"\n id={listId}\n aria-label=\"Shells\"\n tabIndex={-1}\n // eslint-disable-next-line jsx-a11y/no-autofocus\n autoFocus\n onKeyDown={onListKeyDown}\n ref={(el) => el?.focus()}\n style={{\n position: \"absolute\",\n zIndex: 50,\n top: \"calc(100% + 4px)\",\n left: 0,\n minWidth: \"100%\",\n margin: 0,\n padding: 4,\n listStyle: \"none\",\n background: PANEL_BG,\n border: `1px solid ${BORDER}`,\n borderRadius: 8,\n boxShadow: \"0 8px 24px rgba(0,0,0,0.45)\",\n outline: \"none\",\n }}\n >\n {shells.map((s, i) => {\n const isSelected = s.id === value;\n const isActive = i === activeIndex;\n return (\n <li\n key={s.id}\n role=\"option\"\n aria-selected={isSelected}\n data-shell-id={s.id}\n onMouseEnter={() => setActiveIndex(i)}\n onClick={() => pick(i)}\n style={{\n display: \"flex\",\n alignItems: \"center\",\n gap: 8,\n padding: \"6px 8px\",\n fontSize: 12,\n borderRadius: 5,\n color: FG,\n cursor: \"pointer\",\n background: isActive ? HOVER_BG : \"transparent\",\n }}\n >\n <span\n aria-hidden=\"true\"\n style={{\n width: 18,\n textAlign: \"center\",\n color: isSelected ? ACCENT : MUTED,\n }}\n >\n {s.icon ?? \"\"}\n </span>\n <span style={{ flex: 1 }}>{s.label}</span>\n {isSelected ? (\n <span aria-hidden=\"true\" style={{ color: ACCENT, fontSize: 11 }}>\n ✓\n </span>\n ) : null}\n </li>\n );\n })}\n </ul>\n ) : null}\n </div>\n );\n}\n","/**\n * Decide what to push to the terminal when the controlled `output` changes.\n * Terminals are append-only streams, so the common case (output grew) writes\n * only the delta; a value that no longer extends what we've written means a\n * wholesale replace, so reset + rewrite. Returns null when nothing changed.\n */\nexport function diffOutput(\n written: string,\n next: string,\n): { reset: boolean; write: string } | null {\n if (next === written) return null;\n if (next.startsWith(written)) return { reset: false, write: next.slice(written.length) };\n return { reset: true, write: next };\n}\n","import { resolveShell, type ShellProfile } from \"./types\";\n\n/**\n * Pure decision for \"select shell `id`\": resolves the profile and reports whether\n * the selection actually changed. The single source of truth shared by\n * `<Terminal>`'s `setShell`, `<ShellSwitcher>`, and `useTerminalSession`, so the\n * interactive behavior is unit-testable without a DOM.\n *\n * Returns `null` when the id isn't in `shells` (a no-op — unknown shell).\n */\nexport function decideShellSelect(\n shells: ShellProfile[] | undefined,\n current: string | undefined,\n id: string,\n): { profile: ShellProfile; changed: boolean } | null {\n const profile = resolveShell(shells, id);\n if (!profile) return null;\n return { profile, changed: current !== id };\n}\n\n/**\n * Pure decision for a session shell switch. The session hook holds the buffer +\n * the transport, so it only needs to know: did the shell change? If so it must\n * reset the buffer (to `initial`) and reconnect the transport. Extracted so the\n * reset-and-reconnect contract is unit-testable without rendering the hook.\n */\nexport function decideShellSwitch(\n current: string | undefined,\n next: string | undefined,\n): { changed: boolean } {\n return { changed: current !== next };\n}\n","import {\n forwardRef,\n useCallback,\n useEffect,\n useImperativeHandle,\n useRef,\n useState,\n} from \"react\";\nimport { useTerminal } from \"../hooks/use-terminal\";\nimport { ShellSwitcher } from \"./ShellSwitcher\";\nimport { diffOutput } from \"../output-diff\";\nimport { decideShellSelect } from \"../shell-select\";\nimport type { TerminalHandle, TerminalProps } from \"../types\";\n\n/**\n * Human+ `<Terminal>` — a controlled, themeable xterm.js terminal.\n *\n * - **Authoring surface:** terse, controlled (`output` + `onData`), JSON-friendly\n * props (rows/cols, theme tokens, initial buffer), a stable `data-fancy-terminal`\n * handle, and a ref exposing {@link TerminalHandle} (write/clear/fit/getBuffer/…).\n * - **Inhabited surface:** the same handle is what an MCP bridge drives, so an\n * embedded agent reads the buffer + writes input without DOM-scraping.\n *\n * The parent must have a height — the terminal fits its container (like any xterm\n * surface); a 0-height parent collapses it.\n *\n * ```tsx\n * const [out, setOut] = useState(\"\");\n * <div style={{ height: 360 }}>\n * <Terminal output={out} onData={(d) => backend.send(d)} />\n * </div>\n * ```\n */\nexport const Terminal = forwardRef<TerminalHandle, TerminalProps>(function Terminal(\n {\n output,\n theme,\n rows,\n cols,\n fit = true,\n readOnly,\n cursorBlink,\n cursorStyle,\n fontFamily,\n fontSize,\n scrollback,\n initialOutput,\n onData,\n onResize,\n shells,\n activeShell,\n onShellChange,\n showShellBar = false,\n className,\n style,\n ...rest\n },\n ref,\n) {\n const containerRef = useRef<HTMLDivElement>(null);\n\n // Selected shell: controlled by `activeShell` when provided, else internal.\n const isShellControlled = activeShell !== undefined;\n const [internalShell, setInternalShell] = useState<string | undefined>(undefined);\n const shellId = isShellControlled ? activeShell : internalShell;\n\n // Read shell state live so the stable handle's setShell/getShell never go stale.\n const shellStateRef = useRef({ shells, shellId, isShellControlled, onShellChange });\n shellStateRef.current = { shells, shellId, isShellControlled, onShellChange };\n\n const selectShell = useCallback((id: string) => {\n const s = shellStateRef.current;\n const decision = decideShellSelect(s.shells, s.shellId, id);\n if (!decision) return; // unknown id — no-op\n if (!s.isShellControlled) setInternalShell(id);\n s.onShellChange?.(id, decision.profile);\n }, []);\n\n const handle = useTerminal(containerRef, {\n theme,\n rows,\n cols,\n fit,\n readOnly,\n cursorBlink,\n cursorStyle,\n fontFamily,\n fontSize,\n scrollback,\n // `initialOutput` is for uncontrolled use; with a controlled `output` the\n // diffing effect below owns the buffer, so don't double-write.\n initialOutput: output === undefined ? initialOutput : undefined,\n onData,\n onResize,\n });\n\n useImperativeHandle(\n ref,\n (): TerminalHandle => ({\n ...handle,\n setShell: selectShell,\n getShell: () => shellStateRef.current.shellId,\n }),\n [handle, selectShell],\n );\n\n // Controlled output: write only the appended delta; reset + rewrite if the\n // value diverges from what we've already written (a wholesale replace).\n const written = useRef(\"\");\n useEffect(() => {\n if (output === undefined) return;\n const change = diffOutput(written.current, output);\n if (!change) return;\n if (change.reset) handle.reset();\n handle.write(change.write);\n written.current = output;\n }, [output, handle]);\n\n const surface = (\n <div\n ref={containerRef}\n data-fancy-terminal=\"\"\n data-readonly={readOnly ? \"\" : undefined}\n data-fancy-terminal-shell={shellId ?? undefined}\n // When the shell bar is shown the wrapper owns the box; the surface flexes.\n style={\n showShellBar && shells\n ? { width: \"100%\", flex: 1, minHeight: 0 }\n : { width: \"100%\", height: \"100%\", ...style }\n }\n {...(showShellBar && shells ? {} : rest)}\n />\n );\n\n if (!showShellBar || !shells) return surface;\n\n return (\n <div\n data-fancy-terminal-shell={shellId ?? undefined}\n className={className}\n style={{ display: \"flex\", flexDirection: \"column\", width: \"100%\", height: \"100%\", ...style }}\n {...rest}\n >\n <div\n style={{\n display: \"flex\",\n alignItems: \"center\",\n gap: 8,\n padding: \"4px 6px\",\n background: \"#09090b\",\n borderBottom: \"1px solid #27272a\",\n }}\n >\n <ShellSwitcher\n shells={shells}\n value={shellId}\n onChange={(id) => selectShell(id)}\n disabled={readOnly}\n />\n </div>\n {surface}\n </div>\n );\n});\n","import { useCallback, useEffect, useRef, useState } from \"react\";\nimport { decideShellSwitch } from \"../shell-select\";\n\n/**\n * A streamed terminal backend — a PTY, an SSH session, or a command runner.\n * `subscribe` pushes output chunks (wire it to an Echo channel or a fancy-query\n * `useFancyStream` feed); `send` forwards the user's keystrokes upstream.\n */\nexport interface TerminalSessionTransport {\n /** Forward user input / keystrokes to the backend. */\n send: (data: string) => void | Promise<void>;\n /** Subscribe to backend output chunks; return an unsubscribe function. */\n subscribe: (onChunk: (chunk: string) => void) => (() => void) | void;\n /**\n * Optional — called when the active shell changes (and once on mount if a\n * `shell` is provided). The host (re)connects its PTY/command backend to the\n * chosen shell here. Transports that don't care about shells can omit it; the\n * hook stays fully backward compatible.\n */\n connect?: (shell: string | undefined) => void | Promise<void>;\n}\n\nexport interface UseTerminalSessionOptions {\n transport: TerminalSessionTransport;\n /** Replayed history written before live output starts. */\n initial?: string;\n /** The active shell id. Changing it resets the buffer + calls `transport.connect`. */\n shell?: string;\n}\n\nexport interface TerminalSessionApi {\n /** Accumulated output — feed to `<Terminal output={output} />`. */\n output: string;\n /** Forward keystrokes upstream — wire to `<Terminal onData={sendData} />`. */\n sendData: (data: string) => void;\n /** Append a chunk locally (e.g. a client-side echo or banner). */\n append: (chunk: string) => void;\n /** Clear the local buffer. */\n clear: () => void;\n /** The active shell id (controlled by `options.shell` when provided). */\n shell: string | undefined;\n /**\n * Switch shells: resets the buffer (back to `initial`) and tells the transport\n * to reconnect via `transport.connect(id)`. In controlled mode (`options.shell`\n * set) this still fires `connect`; pass the new id down via `options.shell` to\n * reflect it. No-op when the id matches the current shell.\n */\n switchShell: (id: string | undefined) => void;\n}\n\n/**\n * Bind a `<Terminal>` to a streamed backend: accumulates incoming output into a\n * controlled buffer and forwards keystrokes upstream — the glue between the\n * display surface and a PTY/command channel.\n *\n * ```tsx\n * const session = useTerminalSession({\n * transport: {\n * send: (d) => echo.private(`pty.${id}`).whisper(\"stdin\", { d }),\n * subscribe: (onChunk) => {\n * const ch = echo.private(`pty.${id}`).listen(\".stdout\", (e) => onChunk(e.chunk));\n * return () => echo.leave(`pty.${id}`);\n * },\n * },\n * });\n * <Terminal output={session.output} onData={session.sendData} />\n * ```\n */\nexport function useTerminalSession(options: UseTerminalSessionOptions): TerminalSessionApi {\n const initial = options.initial ?? \"\";\n const [output, setOutput] = useState(initial);\n\n // In uncontrolled mode `switchShell` drives this; in controlled mode the\n // `options.shell` effect keeps it in sync.\n const [internalShell, setInternalShell] = useState<string | undefined>(options.shell);\n const shell = options.shell !== undefined ? options.shell : internalShell;\n\n const transportRef = useRef(options.transport);\n transportRef.current = options.transport;\n const initialRef = useRef(initial);\n initialRef.current = initial;\n\n useEffect(() => {\n const unsub = transportRef.current.subscribe((chunk) => setOutput((o) => o + chunk));\n return () => {\n if (typeof unsub === \"function\") unsub();\n };\n // subscribe once; the transport is read live via the ref.\n }, []);\n\n // Connect on mount + whenever the active shell changes. Resetting the buffer\n // is owned by switchShell (uncontrolled) / this effect (controlled).\n const connectedShell = useRef<string | undefined>(undefined);\n const mounted = useRef(false);\n useEffect(() => {\n // On mount: connect once (even if shell is undefined, for parity).\n if (!mounted.current) {\n mounted.current = true;\n connectedShell.current = shell;\n void transportRef.current.connect?.(shell);\n return;\n }\n if (!decideShellSwitch(connectedShell.current, shell).changed) return;\n connectedShell.current = shell;\n setOutput(initialRef.current); // reset the buffer for the new shell\n void transportRef.current.connect?.(shell);\n }, [shell]);\n\n const sendData = useCallback((data: string) => {\n void transportRef.current.send(data);\n }, []);\n\n const append = useCallback((chunk: string) => setOutput((o) => o + chunk), []);\n const clear = useCallback(() => setOutput(\"\"), []);\n\n const switchShell = useCallback((id: string | undefined) => {\n // Uncontrolled: flip internal state, the effect above does reset + connect.\n // Controlled (options.shell set): still drive a reset + connect now so the\n // host sees the switch immediately even before it echoes options.shell back.\n setInternalShell((cur) => (cur === id ? cur : id));\n if (!decideShellSwitch(connectedShell.current, id).changed) return;\n connectedShell.current = id;\n setOutput(initialRef.current);\n void transportRef.current.connect?.(id);\n }, []);\n\n return { output, sendData, append, clear, shell, switchShell };\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@particle-academy/fancy-term",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Human+ Terminal for React — a controlled, themeable <Terminal> wrapping xterm.js, with hooks and an MCP-bridgeable surface so embedded agents read the buffer, write input, and run commands without DOM-scraping.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|