@particle-academy/fancy-term 0.1.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 +117 -0
- package/dist/index.cjs +237 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +165 -0
- package/dist/index.d.ts +165 -0
- package/dist/index.js +231 -0
- package/dist/index.js.map +1 -0
- package/package.json +70 -0
package/README.md
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# @particle-academy/fancy-term
|
|
2
|
+
|
|
3
|
+
**Human+ Terminal for React** — a controlled, themeable `<Terminal>` wrapping
|
|
4
|
+
[xterm.js](https://xtermjs.org), with hooks and an MCP-bridgeable surface so
|
|
5
|
+
embedded agents read the buffer, write input, and run commands **without
|
|
6
|
+
DOM-scraping**.
|
|
7
|
+
|
|
8
|
+
Like every Fancy UI component it serves two surfaces at once:
|
|
9
|
+
|
|
10
|
+
- **Authoring** — terse and controlled (`output` + `onData`), JSON-friendly props
|
|
11
|
+
(rows/cols, theme tokens, initial buffer), a stable `data-fancy-terminal`
|
|
12
|
+
handle, and a ref exposing the full `TerminalHandle`.
|
|
13
|
+
- **Inhabited** — that same handle is what an MCP bridge drives, so an agent reads
|
|
14
|
+
the visible buffer and writes input through stable affordances, never the DOM.
|
|
15
|
+
|
|
16
|
+
Sexy by default via a Fancy dark theme drawn from the react-fancy Tailwind v4
|
|
17
|
+
tokens.
|
|
18
|
+
|
|
19
|
+
> **Status:** 0.1.0. `<Terminal>` + `useTerminal` / `useTerminalFit` /
|
|
20
|
+
> `useTerminalSession` are in place. The `registerTerminalBridge` MCP bridge
|
|
21
|
+
> (`terminal_read` / `terminal_write` / `terminal_run`) and the trust‑but‑verify
|
|
22
|
+
> staged-command affordance ship next, in `@particle-academy/agent-integrations`.
|
|
23
|
+
|
|
24
|
+
## Install
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
npm install @particle-academy/fancy-term @xterm/xterm @xterm/addon-fit
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
`react`, `xterm`, and the fit addon are **peer dependencies** — the wrapper itself
|
|
31
|
+
is zero-runtime-dep (the same posture as `fancy-echarts` over ECharts). Import the
|
|
32
|
+
xterm stylesheet once in your app:
|
|
33
|
+
|
|
34
|
+
```ts
|
|
35
|
+
import "@xterm/xterm/css/xterm.css";
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## `<Terminal>`
|
|
39
|
+
|
|
40
|
+
The parent needs a height — the terminal fits its container (like any xterm
|
|
41
|
+
surface); a 0‑height parent collapses it.
|
|
42
|
+
|
|
43
|
+
```tsx
|
|
44
|
+
import { Terminal } from "@particle-academy/fancy-term";
|
|
45
|
+
|
|
46
|
+
function Console() {
|
|
47
|
+
const [out, setOut] = useState("$ ");
|
|
48
|
+
return (
|
|
49
|
+
<div style={{ height: 360 }}>
|
|
50
|
+
<Terminal output={out} onData={(d) => backend.send(d)} />
|
|
51
|
+
</div>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
`output` is a **controlled buffer**: the component writes only the appended delta
|
|
57
|
+
as it grows (replacing it wholesale resets + rewrites), so you can drive the
|
|
58
|
+
terminal straight from React state — e.g. streaming command output via
|
|
59
|
+
fancy-query's `useFancyStream`.
|
|
60
|
+
|
|
61
|
+
| prop | type | notes |
|
|
62
|
+
|---|---|---|
|
|
63
|
+
| `output` | `string` | controlled buffer (delta-appended) |
|
|
64
|
+
| `onData` | `(data: string) => void` | user keystrokes / paste → your PTY/command backend |
|
|
65
|
+
| `onResize` | `(size) => void` | cols/rows changed |
|
|
66
|
+
| `theme` | `TerminalTheme` | xterm color theme; omit for the Fancy dark theme |
|
|
67
|
+
| `rows` / `cols` | `number` | fixed grid; omit + keep `fit` to size from the container |
|
|
68
|
+
| `fit` | `boolean` | auto-fit via the fit addon + ResizeObserver (default `true`) |
|
|
69
|
+
| `readOnly` | `boolean` | block stdin (display-only) |
|
|
70
|
+
| `cursorBlink` / `cursorStyle` | | `"block" \| "underline" \| "bar"` |
|
|
71
|
+
| `fontFamily` / `fontSize` / `scrollback` | | |
|
|
72
|
+
| `initialOutput` | `string` | written once on mount (uncontrolled use) |
|
|
73
|
+
|
|
74
|
+
The ref exposes a `TerminalHandle`:
|
|
75
|
+
|
|
76
|
+
```tsx
|
|
77
|
+
const term = useRef<TerminalHandle>(null);
|
|
78
|
+
// term.current.write / writeln / clear / reset / fit / focus
|
|
79
|
+
// term.current.getBuffer() → the visible buffer as text (what an agent "sees")
|
|
80
|
+
// term.current.getSelection() → current selection
|
|
81
|
+
// term.current.xterm → the raw xterm.js instance (escape hatch)
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Hooks
|
|
85
|
+
|
|
86
|
+
```tsx
|
|
87
|
+
// Headless engine — the terminal without the component shell.
|
|
88
|
+
const handle = useTerminal(containerRef, { theme, onData });
|
|
89
|
+
|
|
90
|
+
// Auto-fit on resize, guarding the hidden-tab / late-mount 0×0 case.
|
|
91
|
+
useTerminalFit(containerRef, () => handle.fit());
|
|
92
|
+
|
|
93
|
+
// Bind to a streamed backend (PTY / SSH / command runner).
|
|
94
|
+
const session = useTerminalSession({
|
|
95
|
+
transport: {
|
|
96
|
+
send: (d) => echo.private(`pty.${id}`).whisper("stdin", { d }),
|
|
97
|
+
subscribe: (onChunk) => {
|
|
98
|
+
echo.private(`pty.${id}`).listen(".stdout", (e) => onChunk(e.chunk));
|
|
99
|
+
return () => echo.leave(`pty.${id}`);
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
});
|
|
103
|
+
<Terminal output={session.output} onData={session.sendData} />
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## Human+ contract
|
|
107
|
+
|
|
108
|
+
`<Terminal>` is **controlled** (`value`/`onData`), carries a **stable handle**
|
|
109
|
+
(`data-fancy-terminal` + the ref API), takes **JSON-friendly** props, and is
|
|
110
|
+
**bridgeable** — `registerTerminalBridge` (in `agent-integrations`, shipping next)
|
|
111
|
+
maps `terminal_read` / `terminal_write` / `terminal_run` onto the handle, wraps
|
|
112
|
+
mutations so every write broadcasts `AgentActivity`, and supports a staged
|
|
113
|
+
"agent proposes → human confirms" mode for destructive commands.
|
|
114
|
+
|
|
115
|
+
## License
|
|
116
|
+
|
|
117
|
+
MIT
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var react = require('react');
|
|
4
|
+
var xterm = require('@xterm/xterm');
|
|
5
|
+
var addonFit = require('@xterm/addon-fit');
|
|
6
|
+
var jsxRuntime = require('react/jsx-runtime');
|
|
7
|
+
|
|
8
|
+
// src/components/Terminal.tsx
|
|
9
|
+
|
|
10
|
+
// src/theme.ts
|
|
11
|
+
var fancyDarkTheme = {
|
|
12
|
+
background: "#09090b",
|
|
13
|
+
// zinc-950
|
|
14
|
+
foreground: "#e4e4e7",
|
|
15
|
+
// zinc-200
|
|
16
|
+
cursor: "#8b5cf6",
|
|
17
|
+
// violet-500
|
|
18
|
+
cursorAccent: "#09090b",
|
|
19
|
+
selectionBackground: "#3f3f46",
|
|
20
|
+
// zinc-700
|
|
21
|
+
black: "#18181b",
|
|
22
|
+
red: "#f43f5e",
|
|
23
|
+
green: "#10b981",
|
|
24
|
+
yellow: "#f59e0b",
|
|
25
|
+
blue: "#3b82f6",
|
|
26
|
+
magenta: "#8b5cf6",
|
|
27
|
+
cyan: "#22d3ee",
|
|
28
|
+
white: "#e4e4e7",
|
|
29
|
+
brightBlack: "#52525b",
|
|
30
|
+
brightRed: "#fb7185",
|
|
31
|
+
brightGreen: "#34d399",
|
|
32
|
+
brightYellow: "#fbbf24",
|
|
33
|
+
brightBlue: "#60a5fa",
|
|
34
|
+
brightMagenta: "#a78bfa",
|
|
35
|
+
brightCyan: "#67e8f9",
|
|
36
|
+
brightWhite: "#fafafa"
|
|
37
|
+
};
|
|
38
|
+
function useTerminalFit(ref, fit, enabled = true) {
|
|
39
|
+
const fitRef = react.useRef(fit);
|
|
40
|
+
fitRef.current = fit;
|
|
41
|
+
react.useEffect(() => {
|
|
42
|
+
if (!enabled) return;
|
|
43
|
+
const el = ref.current;
|
|
44
|
+
if (!el) return;
|
|
45
|
+
const run = () => {
|
|
46
|
+
if (el.clientWidth > 0 && el.clientHeight > 0) fitRef.current();
|
|
47
|
+
};
|
|
48
|
+
const observer = new ResizeObserver(run);
|
|
49
|
+
observer.observe(el);
|
|
50
|
+
run();
|
|
51
|
+
return () => observer.disconnect();
|
|
52
|
+
}, [ref, enabled]);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// src/hooks/use-terminal.ts
|
|
56
|
+
var DEFAULT_FONT = 'ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace';
|
|
57
|
+
function readBuffer(term) {
|
|
58
|
+
if (!term) return "";
|
|
59
|
+
const buf = term.buffer.active;
|
|
60
|
+
const lines = [];
|
|
61
|
+
for (let i = 0; i < buf.length; i++) {
|
|
62
|
+
const line = buf.getLine(i);
|
|
63
|
+
if (line) lines.push(line.translateToString(true));
|
|
64
|
+
}
|
|
65
|
+
return lines.join("\n").replace(/\n+$/, "");
|
|
66
|
+
}
|
|
67
|
+
function useTerminal(containerRef, options = {}) {
|
|
68
|
+
const xtermRef = react.useRef(null);
|
|
69
|
+
const fitRef = react.useRef(null);
|
|
70
|
+
const optsRef = react.useRef(options);
|
|
71
|
+
optsRef.current = options;
|
|
72
|
+
const handleRef = react.useRef(null);
|
|
73
|
+
if (handleRef.current === null) {
|
|
74
|
+
handleRef.current = {
|
|
75
|
+
get xterm() {
|
|
76
|
+
return xtermRef.current;
|
|
77
|
+
},
|
|
78
|
+
write: (d) => xtermRef.current?.write(d),
|
|
79
|
+
writeln: (d) => xtermRef.current?.writeln(d),
|
|
80
|
+
clear: () => xtermRef.current?.clear(),
|
|
81
|
+
reset: () => xtermRef.current?.reset(),
|
|
82
|
+
fit: () => {
|
|
83
|
+
try {
|
|
84
|
+
fitRef.current?.fit();
|
|
85
|
+
} catch {
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
focus: () => xtermRef.current?.focus(),
|
|
89
|
+
getBuffer: () => readBuffer(xtermRef.current),
|
|
90
|
+
getSelection: () => xtermRef.current?.getSelection() ?? ""
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
const handle = handleRef.current;
|
|
94
|
+
react.useEffect(() => {
|
|
95
|
+
const el = containerRef.current;
|
|
96
|
+
if (!el) return;
|
|
97
|
+
const o = optsRef.current;
|
|
98
|
+
const term = new xterm.Terminal({
|
|
99
|
+
theme: o.theme ?? fancyDarkTheme,
|
|
100
|
+
rows: o.rows,
|
|
101
|
+
cols: o.cols,
|
|
102
|
+
disableStdin: o.readOnly ?? false,
|
|
103
|
+
cursorBlink: o.cursorBlink ?? true,
|
|
104
|
+
cursorStyle: o.cursorStyle ?? "block",
|
|
105
|
+
fontFamily: o.fontFamily ?? DEFAULT_FONT,
|
|
106
|
+
fontSize: o.fontSize ?? 13,
|
|
107
|
+
scrollback: o.scrollback ?? 1e3,
|
|
108
|
+
allowProposedApi: true
|
|
109
|
+
});
|
|
110
|
+
const fitAddon = new addonFit.FitAddon();
|
|
111
|
+
term.loadAddon(fitAddon);
|
|
112
|
+
term.open(el);
|
|
113
|
+
xtermRef.current = term;
|
|
114
|
+
fitRef.current = fitAddon;
|
|
115
|
+
const dataSub = term.onData((d) => optsRef.current.onData?.(d));
|
|
116
|
+
const resizeSub = term.onResize(
|
|
117
|
+
({ cols, rows }) => optsRef.current.onResize?.({ cols, rows })
|
|
118
|
+
);
|
|
119
|
+
if (o.initialOutput) term.write(o.initialOutput);
|
|
120
|
+
if ((o.fit ?? true) && el.clientWidth > 0 && el.clientHeight > 0) {
|
|
121
|
+
try {
|
|
122
|
+
fitAddon.fit();
|
|
123
|
+
} catch {
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return () => {
|
|
127
|
+
dataSub.dispose();
|
|
128
|
+
resizeSub.dispose();
|
|
129
|
+
term.dispose();
|
|
130
|
+
xtermRef.current = null;
|
|
131
|
+
fitRef.current = null;
|
|
132
|
+
};
|
|
133
|
+
}, [containerRef]);
|
|
134
|
+
react.useEffect(() => {
|
|
135
|
+
const t = xtermRef.current;
|
|
136
|
+
if (!t) return;
|
|
137
|
+
if (options.theme) t.options.theme = options.theme;
|
|
138
|
+
if (options.readOnly !== void 0) t.options.disableStdin = options.readOnly;
|
|
139
|
+
if (options.cursorBlink !== void 0) t.options.cursorBlink = options.cursorBlink;
|
|
140
|
+
if (options.cursorStyle) t.options.cursorStyle = options.cursorStyle;
|
|
141
|
+
if (options.fontSize) t.options.fontSize = options.fontSize;
|
|
142
|
+
if (options.fontFamily) t.options.fontFamily = options.fontFamily;
|
|
143
|
+
}, [options.theme, options.readOnly, options.cursorBlink, options.cursorStyle, options.fontSize, options.fontFamily]);
|
|
144
|
+
useTerminalFit(containerRef, () => handle.fit(), options.fit ?? true);
|
|
145
|
+
return handle;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// src/output-diff.ts
|
|
149
|
+
function diffOutput(written, next) {
|
|
150
|
+
if (next === written) return null;
|
|
151
|
+
if (next.startsWith(written)) return { reset: false, write: next.slice(written.length) };
|
|
152
|
+
return { reset: true, write: next };
|
|
153
|
+
}
|
|
154
|
+
var Terminal = react.forwardRef(function Terminal2({
|
|
155
|
+
output,
|
|
156
|
+
theme,
|
|
157
|
+
rows,
|
|
158
|
+
cols,
|
|
159
|
+
fit = true,
|
|
160
|
+
readOnly,
|
|
161
|
+
cursorBlink,
|
|
162
|
+
cursorStyle,
|
|
163
|
+
fontFamily,
|
|
164
|
+
fontSize,
|
|
165
|
+
scrollback,
|
|
166
|
+
initialOutput,
|
|
167
|
+
onData,
|
|
168
|
+
onResize,
|
|
169
|
+
className,
|
|
170
|
+
style,
|
|
171
|
+
...rest
|
|
172
|
+
}, ref) {
|
|
173
|
+
const containerRef = react.useRef(null);
|
|
174
|
+
const handle = useTerminal(containerRef, {
|
|
175
|
+
theme,
|
|
176
|
+
rows,
|
|
177
|
+
cols,
|
|
178
|
+
fit,
|
|
179
|
+
readOnly,
|
|
180
|
+
cursorBlink,
|
|
181
|
+
cursorStyle,
|
|
182
|
+
fontFamily,
|
|
183
|
+
fontSize,
|
|
184
|
+
scrollback,
|
|
185
|
+
// `initialOutput` is for uncontrolled use; with a controlled `output` the
|
|
186
|
+
// diffing effect below owns the buffer, so don't double-write.
|
|
187
|
+
initialOutput: output === void 0 ? initialOutput : void 0,
|
|
188
|
+
onData,
|
|
189
|
+
onResize
|
|
190
|
+
});
|
|
191
|
+
react.useImperativeHandle(ref, () => handle, [handle]);
|
|
192
|
+
const written = react.useRef("");
|
|
193
|
+
react.useEffect(() => {
|
|
194
|
+
if (output === void 0) return;
|
|
195
|
+
const change = diffOutput(written.current, output);
|
|
196
|
+
if (!change) return;
|
|
197
|
+
if (change.reset) handle.reset();
|
|
198
|
+
handle.write(change.write);
|
|
199
|
+
written.current = output;
|
|
200
|
+
}, [output, handle]);
|
|
201
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
202
|
+
"div",
|
|
203
|
+
{
|
|
204
|
+
ref: containerRef,
|
|
205
|
+
"data-fancy-terminal": "",
|
|
206
|
+
"data-readonly": readOnly ? "" : void 0,
|
|
207
|
+
className,
|
|
208
|
+
style: { width: "100%", height: "100%", ...style },
|
|
209
|
+
...rest
|
|
210
|
+
}
|
|
211
|
+
);
|
|
212
|
+
});
|
|
213
|
+
function useTerminalSession(options) {
|
|
214
|
+
const [output, setOutput] = react.useState(options.initial ?? "");
|
|
215
|
+
const transportRef = react.useRef(options.transport);
|
|
216
|
+
transportRef.current = options.transport;
|
|
217
|
+
react.useEffect(() => {
|
|
218
|
+
const unsub = transportRef.current.subscribe((chunk) => setOutput((o) => o + chunk));
|
|
219
|
+
return () => {
|
|
220
|
+
if (typeof unsub === "function") unsub();
|
|
221
|
+
};
|
|
222
|
+
}, []);
|
|
223
|
+
const sendData = react.useCallback((data) => {
|
|
224
|
+
void transportRef.current.send(data);
|
|
225
|
+
}, []);
|
|
226
|
+
const append = react.useCallback((chunk) => setOutput((o) => o + chunk), []);
|
|
227
|
+
const clear = react.useCallback(() => setOutput(""), []);
|
|
228
|
+
return { output, sendData, append, clear };
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
exports.Terminal = Terminal;
|
|
232
|
+
exports.fancyDarkTheme = fancyDarkTheme;
|
|
233
|
+
exports.useTerminal = useTerminal;
|
|
234
|
+
exports.useTerminalFit = useTerminalFit;
|
|
235
|
+
exports.useTerminalSession = useTerminalSession;
|
|
236
|
+
//# sourceMappingURL=index.cjs.map
|
|
237
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +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","FitAddon","forwardRef","Terminal","useImperativeHandle","jsx","useState","useCallback"],"mappings":"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","file":"index.cjs","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"]}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
import { HTMLAttributes, RefObject } from 'react';
|
|
3
|
+
import { ITheme, Terminal as Terminal$1 } from '@xterm/xterm';
|
|
4
|
+
|
|
5
|
+
/** xterm.js color theme (re-exported for ergonomic typing). */
|
|
6
|
+
type TerminalTheme = ITheme;
|
|
7
|
+
type CursorStyle = "block" | "underline" | "bar";
|
|
8
|
+
/**
|
|
9
|
+
* Imperative handle exposed via the `<Terminal>` ref and returned by
|
|
10
|
+
* {@link useTerminal}. The same surface an MCP bridge drives so an embedded
|
|
11
|
+
* agent reads the buffer + writes input without DOM-scraping.
|
|
12
|
+
*/
|
|
13
|
+
interface TerminalHandle {
|
|
14
|
+
/** The underlying xterm.js instance — escape hatch for addons / advanced use. Null before mount. */
|
|
15
|
+
readonly xterm: Terminal$1 | null;
|
|
16
|
+
/** Write raw data (ANSI escape sequences honored) to the terminal. */
|
|
17
|
+
write: (data: string) => void;
|
|
18
|
+
/** Write data followed by CRLF. */
|
|
19
|
+
writeln: (data: string) => void;
|
|
20
|
+
/** Clear the viewport (keeps scrollback). */
|
|
21
|
+
clear: () => void;
|
|
22
|
+
/** Full reset — clears scrollback + terminal state. */
|
|
23
|
+
reset: () => void;
|
|
24
|
+
/** Re-fit to the container (no-op when the box is 0-size or fit is off). */
|
|
25
|
+
fit: () => void;
|
|
26
|
+
/** Focus the terminal input. */
|
|
27
|
+
focus: () => void;
|
|
28
|
+
/** The full buffer as plain text — what an agent "sees". */
|
|
29
|
+
getBuffer: () => string;
|
|
30
|
+
/** The current text selection, or "" when nothing is selected. */
|
|
31
|
+
getSelection: () => string;
|
|
32
|
+
}
|
|
33
|
+
/** Engine options shared by `<Terminal>` and {@link useTerminal}. */
|
|
34
|
+
interface TerminalOptions {
|
|
35
|
+
/** xterm color theme. Omit for the built-in Fancy dark theme. */
|
|
36
|
+
theme?: TerminalTheme;
|
|
37
|
+
/** Fixed grid size. Omit + leave `fit` on to size from the container. */
|
|
38
|
+
rows?: number;
|
|
39
|
+
cols?: number;
|
|
40
|
+
/** Auto-fit to the container via the fit addon + ResizeObserver. Default true. */
|
|
41
|
+
fit?: boolean;
|
|
42
|
+
/** Block stdin (display-only terminal). Default false. */
|
|
43
|
+
readOnly?: boolean;
|
|
44
|
+
/** Blink the cursor. Default true. */
|
|
45
|
+
cursorBlink?: boolean;
|
|
46
|
+
/** Cursor shape. Default "block". */
|
|
47
|
+
cursorStyle?: CursorStyle;
|
|
48
|
+
/** Monospace font stack. */
|
|
49
|
+
fontFamily?: string;
|
|
50
|
+
/** Font size in px. Default 13. */
|
|
51
|
+
fontSize?: number;
|
|
52
|
+
/** Scrollback lines. Default 1000. */
|
|
53
|
+
scrollback?: number;
|
|
54
|
+
/** Data written once on mount, before any controlled `output`. */
|
|
55
|
+
initialOutput?: string;
|
|
56
|
+
/** User keystrokes / paste data (xterm `onData`). Wire to your PTY/command backend. */
|
|
57
|
+
onData?: (data: string) => void;
|
|
58
|
+
/** Notified on terminal resize. */
|
|
59
|
+
onResize?: (size: {
|
|
60
|
+
cols: number;
|
|
61
|
+
rows: number;
|
|
62
|
+
}) => void;
|
|
63
|
+
}
|
|
64
|
+
interface TerminalProps extends TerminalOptions, Omit<HTMLAttributes<HTMLDivElement>, "onInput" | "onResize" | "children"> {
|
|
65
|
+
/**
|
|
66
|
+
* The controlled output buffer. The component diffs against the previous value
|
|
67
|
+
* and writes only the appended delta, so a host can drive the terminal from
|
|
68
|
+
* React state — e.g. streaming command output via fancy-query's
|
|
69
|
+
* `useFancyStream`. Replacing it with a non-extending string resets + rewrites.
|
|
70
|
+
*/
|
|
71
|
+
output?: string;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Human+ `<Terminal>` — a controlled, themeable xterm.js terminal.
|
|
76
|
+
*
|
|
77
|
+
* - **Authoring surface:** terse, controlled (`output` + `onData`), JSON-friendly
|
|
78
|
+
* props (rows/cols, theme tokens, initial buffer), a stable `data-fancy-terminal`
|
|
79
|
+
* handle, and a ref exposing {@link TerminalHandle} (write/clear/fit/getBuffer/…).
|
|
80
|
+
* - **Inhabited surface:** the same handle is what an MCP bridge drives, so an
|
|
81
|
+
* embedded agent reads the buffer + writes input without DOM-scraping.
|
|
82
|
+
*
|
|
83
|
+
* The parent must have a height — the terminal fits its container (like any xterm
|
|
84
|
+
* surface); a 0-height parent collapses it.
|
|
85
|
+
*
|
|
86
|
+
* ```tsx
|
|
87
|
+
* const [out, setOut] = useState("");
|
|
88
|
+
* <div style={{ height: 360 }}>
|
|
89
|
+
* <Terminal output={out} onData={(d) => backend.send(d)} />
|
|
90
|
+
* </div>
|
|
91
|
+
* ```
|
|
92
|
+
*/
|
|
93
|
+
declare const Terminal: react.ForwardRefExoticComponent<TerminalProps & react.RefAttributes<TerminalHandle>>;
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Create + own an xterm.js instance for a container, returning a stable
|
|
97
|
+
* {@link TerminalHandle}. The headless engine layer behind `<Terminal>` — use it
|
|
98
|
+
* directly when you want the terminal without the component shell (custom chrome,
|
|
99
|
+
* an agent bridge host, tests). Options are read live, so changing them re-applies
|
|
100
|
+
* without tearing down the terminal.
|
|
101
|
+
*/
|
|
102
|
+
declare function useTerminal(containerRef: RefObject<HTMLElement | null>, options?: TerminalOptions): TerminalHandle;
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Run `fit` whenever the container resizes — and once it first has a real,
|
|
106
|
+
* non-zero box. Guards the hidden-tab / late-mount 0×0 case: fitting a collapsed
|
|
107
|
+
* container yields garbage rows/cols (or throws), so we simply skip until there
|
|
108
|
+
* is layout to measure.
|
|
109
|
+
*/
|
|
110
|
+
declare function useTerminalFit(ref: RefObject<HTMLElement | null>, fit: () => void, enabled?: boolean): void;
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* A streamed terminal backend — a PTY, an SSH session, or a command runner.
|
|
114
|
+
* `subscribe` pushes output chunks (wire it to an Echo channel or a fancy-query
|
|
115
|
+
* `useFancyStream` feed); `send` forwards the user's keystrokes upstream.
|
|
116
|
+
*/
|
|
117
|
+
interface TerminalSessionTransport {
|
|
118
|
+
/** Forward user input / keystrokes to the backend. */
|
|
119
|
+
send: (data: string) => void | Promise<void>;
|
|
120
|
+
/** Subscribe to backend output chunks; return an unsubscribe function. */
|
|
121
|
+
subscribe: (onChunk: (chunk: string) => void) => (() => void) | void;
|
|
122
|
+
}
|
|
123
|
+
interface UseTerminalSessionOptions {
|
|
124
|
+
transport: TerminalSessionTransport;
|
|
125
|
+
/** Replayed history written before live output starts. */
|
|
126
|
+
initial?: string;
|
|
127
|
+
}
|
|
128
|
+
interface TerminalSessionApi {
|
|
129
|
+
/** Accumulated output — feed to `<Terminal output={output} />`. */
|
|
130
|
+
output: string;
|
|
131
|
+
/** Forward keystrokes upstream — wire to `<Terminal onData={sendData} />`. */
|
|
132
|
+
sendData: (data: string) => void;
|
|
133
|
+
/** Append a chunk locally (e.g. a client-side echo or banner). */
|
|
134
|
+
append: (chunk: string) => void;
|
|
135
|
+
/** Clear the local buffer. */
|
|
136
|
+
clear: () => void;
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Bind a `<Terminal>` to a streamed backend: accumulates incoming output into a
|
|
140
|
+
* controlled buffer and forwards keystrokes upstream — the glue between the
|
|
141
|
+
* display surface and a PTY/command channel.
|
|
142
|
+
*
|
|
143
|
+
* ```tsx
|
|
144
|
+
* const session = useTerminalSession({
|
|
145
|
+
* transport: {
|
|
146
|
+
* send: (d) => echo.private(`pty.${id}`).whisper("stdin", { d }),
|
|
147
|
+
* subscribe: (onChunk) => {
|
|
148
|
+
* const ch = echo.private(`pty.${id}`).listen(".stdout", (e) => onChunk(e.chunk));
|
|
149
|
+
* return () => echo.leave(`pty.${id}`);
|
|
150
|
+
* },
|
|
151
|
+
* },
|
|
152
|
+
* });
|
|
153
|
+
* <Terminal output={session.output} onData={session.sendData} />
|
|
154
|
+
* ```
|
|
155
|
+
*/
|
|
156
|
+
declare function useTerminalSession(options: UseTerminalSessionOptions): TerminalSessionApi;
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* The default "sexy" Fancy dark terminal theme — a zinc surface, violet cursor,
|
|
160
|
+
* and a balanced ANSI palette drawn from the react-fancy Tailwind v4 tokens.
|
|
161
|
+
* Pass a `theme` prop / option to override.
|
|
162
|
+
*/
|
|
163
|
+
declare const fancyDarkTheme: TerminalTheme;
|
|
164
|
+
|
|
165
|
+
export { type CursorStyle, Terminal, type TerminalHandle, type TerminalOptions, type TerminalProps, type TerminalSessionApi, type TerminalSessionTransport, type TerminalTheme, type UseTerminalSessionOptions, fancyDarkTheme, useTerminal, useTerminalFit, useTerminalSession };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
import { HTMLAttributes, RefObject } from 'react';
|
|
3
|
+
import { ITheme, Terminal as Terminal$1 } from '@xterm/xterm';
|
|
4
|
+
|
|
5
|
+
/** xterm.js color theme (re-exported for ergonomic typing). */
|
|
6
|
+
type TerminalTheme = ITheme;
|
|
7
|
+
type CursorStyle = "block" | "underline" | "bar";
|
|
8
|
+
/**
|
|
9
|
+
* Imperative handle exposed via the `<Terminal>` ref and returned by
|
|
10
|
+
* {@link useTerminal}. The same surface an MCP bridge drives so an embedded
|
|
11
|
+
* agent reads the buffer + writes input without DOM-scraping.
|
|
12
|
+
*/
|
|
13
|
+
interface TerminalHandle {
|
|
14
|
+
/** The underlying xterm.js instance — escape hatch for addons / advanced use. Null before mount. */
|
|
15
|
+
readonly xterm: Terminal$1 | null;
|
|
16
|
+
/** Write raw data (ANSI escape sequences honored) to the terminal. */
|
|
17
|
+
write: (data: string) => void;
|
|
18
|
+
/** Write data followed by CRLF. */
|
|
19
|
+
writeln: (data: string) => void;
|
|
20
|
+
/** Clear the viewport (keeps scrollback). */
|
|
21
|
+
clear: () => void;
|
|
22
|
+
/** Full reset — clears scrollback + terminal state. */
|
|
23
|
+
reset: () => void;
|
|
24
|
+
/** Re-fit to the container (no-op when the box is 0-size or fit is off). */
|
|
25
|
+
fit: () => void;
|
|
26
|
+
/** Focus the terminal input. */
|
|
27
|
+
focus: () => void;
|
|
28
|
+
/** The full buffer as plain text — what an agent "sees". */
|
|
29
|
+
getBuffer: () => string;
|
|
30
|
+
/** The current text selection, or "" when nothing is selected. */
|
|
31
|
+
getSelection: () => string;
|
|
32
|
+
}
|
|
33
|
+
/** Engine options shared by `<Terminal>` and {@link useTerminal}. */
|
|
34
|
+
interface TerminalOptions {
|
|
35
|
+
/** xterm color theme. Omit for the built-in Fancy dark theme. */
|
|
36
|
+
theme?: TerminalTheme;
|
|
37
|
+
/** Fixed grid size. Omit + leave `fit` on to size from the container. */
|
|
38
|
+
rows?: number;
|
|
39
|
+
cols?: number;
|
|
40
|
+
/** Auto-fit to the container via the fit addon + ResizeObserver. Default true. */
|
|
41
|
+
fit?: boolean;
|
|
42
|
+
/** Block stdin (display-only terminal). Default false. */
|
|
43
|
+
readOnly?: boolean;
|
|
44
|
+
/** Blink the cursor. Default true. */
|
|
45
|
+
cursorBlink?: boolean;
|
|
46
|
+
/** Cursor shape. Default "block". */
|
|
47
|
+
cursorStyle?: CursorStyle;
|
|
48
|
+
/** Monospace font stack. */
|
|
49
|
+
fontFamily?: string;
|
|
50
|
+
/** Font size in px. Default 13. */
|
|
51
|
+
fontSize?: number;
|
|
52
|
+
/** Scrollback lines. Default 1000. */
|
|
53
|
+
scrollback?: number;
|
|
54
|
+
/** Data written once on mount, before any controlled `output`. */
|
|
55
|
+
initialOutput?: string;
|
|
56
|
+
/** User keystrokes / paste data (xterm `onData`). Wire to your PTY/command backend. */
|
|
57
|
+
onData?: (data: string) => void;
|
|
58
|
+
/** Notified on terminal resize. */
|
|
59
|
+
onResize?: (size: {
|
|
60
|
+
cols: number;
|
|
61
|
+
rows: number;
|
|
62
|
+
}) => void;
|
|
63
|
+
}
|
|
64
|
+
interface TerminalProps extends TerminalOptions, Omit<HTMLAttributes<HTMLDivElement>, "onInput" | "onResize" | "children"> {
|
|
65
|
+
/**
|
|
66
|
+
* The controlled output buffer. The component diffs against the previous value
|
|
67
|
+
* and writes only the appended delta, so a host can drive the terminal from
|
|
68
|
+
* React state — e.g. streaming command output via fancy-query's
|
|
69
|
+
* `useFancyStream`. Replacing it with a non-extending string resets + rewrites.
|
|
70
|
+
*/
|
|
71
|
+
output?: string;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Human+ `<Terminal>` — a controlled, themeable xterm.js terminal.
|
|
76
|
+
*
|
|
77
|
+
* - **Authoring surface:** terse, controlled (`output` + `onData`), JSON-friendly
|
|
78
|
+
* props (rows/cols, theme tokens, initial buffer), a stable `data-fancy-terminal`
|
|
79
|
+
* handle, and a ref exposing {@link TerminalHandle} (write/clear/fit/getBuffer/…).
|
|
80
|
+
* - **Inhabited surface:** the same handle is what an MCP bridge drives, so an
|
|
81
|
+
* embedded agent reads the buffer + writes input without DOM-scraping.
|
|
82
|
+
*
|
|
83
|
+
* The parent must have a height — the terminal fits its container (like any xterm
|
|
84
|
+
* surface); a 0-height parent collapses it.
|
|
85
|
+
*
|
|
86
|
+
* ```tsx
|
|
87
|
+
* const [out, setOut] = useState("");
|
|
88
|
+
* <div style={{ height: 360 }}>
|
|
89
|
+
* <Terminal output={out} onData={(d) => backend.send(d)} />
|
|
90
|
+
* </div>
|
|
91
|
+
* ```
|
|
92
|
+
*/
|
|
93
|
+
declare const Terminal: react.ForwardRefExoticComponent<TerminalProps & react.RefAttributes<TerminalHandle>>;
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Create + own an xterm.js instance for a container, returning a stable
|
|
97
|
+
* {@link TerminalHandle}. The headless engine layer behind `<Terminal>` — use it
|
|
98
|
+
* directly when you want the terminal without the component shell (custom chrome,
|
|
99
|
+
* an agent bridge host, tests). Options are read live, so changing them re-applies
|
|
100
|
+
* without tearing down the terminal.
|
|
101
|
+
*/
|
|
102
|
+
declare function useTerminal(containerRef: RefObject<HTMLElement | null>, options?: TerminalOptions): TerminalHandle;
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Run `fit` whenever the container resizes — and once it first has a real,
|
|
106
|
+
* non-zero box. Guards the hidden-tab / late-mount 0×0 case: fitting a collapsed
|
|
107
|
+
* container yields garbage rows/cols (or throws), so we simply skip until there
|
|
108
|
+
* is layout to measure.
|
|
109
|
+
*/
|
|
110
|
+
declare function useTerminalFit(ref: RefObject<HTMLElement | null>, fit: () => void, enabled?: boolean): void;
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* A streamed terminal backend — a PTY, an SSH session, or a command runner.
|
|
114
|
+
* `subscribe` pushes output chunks (wire it to an Echo channel or a fancy-query
|
|
115
|
+
* `useFancyStream` feed); `send` forwards the user's keystrokes upstream.
|
|
116
|
+
*/
|
|
117
|
+
interface TerminalSessionTransport {
|
|
118
|
+
/** Forward user input / keystrokes to the backend. */
|
|
119
|
+
send: (data: string) => void | Promise<void>;
|
|
120
|
+
/** Subscribe to backend output chunks; return an unsubscribe function. */
|
|
121
|
+
subscribe: (onChunk: (chunk: string) => void) => (() => void) | void;
|
|
122
|
+
}
|
|
123
|
+
interface UseTerminalSessionOptions {
|
|
124
|
+
transport: TerminalSessionTransport;
|
|
125
|
+
/** Replayed history written before live output starts. */
|
|
126
|
+
initial?: string;
|
|
127
|
+
}
|
|
128
|
+
interface TerminalSessionApi {
|
|
129
|
+
/** Accumulated output — feed to `<Terminal output={output} />`. */
|
|
130
|
+
output: string;
|
|
131
|
+
/** Forward keystrokes upstream — wire to `<Terminal onData={sendData} />`. */
|
|
132
|
+
sendData: (data: string) => void;
|
|
133
|
+
/** Append a chunk locally (e.g. a client-side echo or banner). */
|
|
134
|
+
append: (chunk: string) => void;
|
|
135
|
+
/** Clear the local buffer. */
|
|
136
|
+
clear: () => void;
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Bind a `<Terminal>` to a streamed backend: accumulates incoming output into a
|
|
140
|
+
* controlled buffer and forwards keystrokes upstream — the glue between the
|
|
141
|
+
* display surface and a PTY/command channel.
|
|
142
|
+
*
|
|
143
|
+
* ```tsx
|
|
144
|
+
* const session = useTerminalSession({
|
|
145
|
+
* transport: {
|
|
146
|
+
* send: (d) => echo.private(`pty.${id}`).whisper("stdin", { d }),
|
|
147
|
+
* subscribe: (onChunk) => {
|
|
148
|
+
* const ch = echo.private(`pty.${id}`).listen(".stdout", (e) => onChunk(e.chunk));
|
|
149
|
+
* return () => echo.leave(`pty.${id}`);
|
|
150
|
+
* },
|
|
151
|
+
* },
|
|
152
|
+
* });
|
|
153
|
+
* <Terminal output={session.output} onData={session.sendData} />
|
|
154
|
+
* ```
|
|
155
|
+
*/
|
|
156
|
+
declare function useTerminalSession(options: UseTerminalSessionOptions): TerminalSessionApi;
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* The default "sexy" Fancy dark terminal theme — a zinc surface, violet cursor,
|
|
160
|
+
* and a balanced ANSI palette drawn from the react-fancy Tailwind v4 tokens.
|
|
161
|
+
* Pass a `theme` prop / option to override.
|
|
162
|
+
*/
|
|
163
|
+
declare const fancyDarkTheme: TerminalTheme;
|
|
164
|
+
|
|
165
|
+
export { type CursorStyle, Terminal, type TerminalHandle, type TerminalOptions, type TerminalProps, type TerminalSessionApi, type TerminalSessionTransport, type TerminalTheme, type UseTerminalSessionOptions, fancyDarkTheme, useTerminal, useTerminalFit, useTerminalSession };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import { forwardRef, useRef, useImperativeHandle, useEffect, useState, useCallback } from 'react';
|
|
2
|
+
import { Terminal as Terminal$1 } from '@xterm/xterm';
|
|
3
|
+
import { FitAddon } from '@xterm/addon-fit';
|
|
4
|
+
import { jsx } from 'react/jsx-runtime';
|
|
5
|
+
|
|
6
|
+
// src/components/Terminal.tsx
|
|
7
|
+
|
|
8
|
+
// src/theme.ts
|
|
9
|
+
var fancyDarkTheme = {
|
|
10
|
+
background: "#09090b",
|
|
11
|
+
// zinc-950
|
|
12
|
+
foreground: "#e4e4e7",
|
|
13
|
+
// zinc-200
|
|
14
|
+
cursor: "#8b5cf6",
|
|
15
|
+
// violet-500
|
|
16
|
+
cursorAccent: "#09090b",
|
|
17
|
+
selectionBackground: "#3f3f46",
|
|
18
|
+
// zinc-700
|
|
19
|
+
black: "#18181b",
|
|
20
|
+
red: "#f43f5e",
|
|
21
|
+
green: "#10b981",
|
|
22
|
+
yellow: "#f59e0b",
|
|
23
|
+
blue: "#3b82f6",
|
|
24
|
+
magenta: "#8b5cf6",
|
|
25
|
+
cyan: "#22d3ee",
|
|
26
|
+
white: "#e4e4e7",
|
|
27
|
+
brightBlack: "#52525b",
|
|
28
|
+
brightRed: "#fb7185",
|
|
29
|
+
brightGreen: "#34d399",
|
|
30
|
+
brightYellow: "#fbbf24",
|
|
31
|
+
brightBlue: "#60a5fa",
|
|
32
|
+
brightMagenta: "#a78bfa",
|
|
33
|
+
brightCyan: "#67e8f9",
|
|
34
|
+
brightWhite: "#fafafa"
|
|
35
|
+
};
|
|
36
|
+
function useTerminalFit(ref, fit, enabled = true) {
|
|
37
|
+
const fitRef = useRef(fit);
|
|
38
|
+
fitRef.current = fit;
|
|
39
|
+
useEffect(() => {
|
|
40
|
+
if (!enabled) return;
|
|
41
|
+
const el = ref.current;
|
|
42
|
+
if (!el) return;
|
|
43
|
+
const run = () => {
|
|
44
|
+
if (el.clientWidth > 0 && el.clientHeight > 0) fitRef.current();
|
|
45
|
+
};
|
|
46
|
+
const observer = new ResizeObserver(run);
|
|
47
|
+
observer.observe(el);
|
|
48
|
+
run();
|
|
49
|
+
return () => observer.disconnect();
|
|
50
|
+
}, [ref, enabled]);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// src/hooks/use-terminal.ts
|
|
54
|
+
var DEFAULT_FONT = 'ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace';
|
|
55
|
+
function readBuffer(term) {
|
|
56
|
+
if (!term) return "";
|
|
57
|
+
const buf = term.buffer.active;
|
|
58
|
+
const lines = [];
|
|
59
|
+
for (let i = 0; i < buf.length; i++) {
|
|
60
|
+
const line = buf.getLine(i);
|
|
61
|
+
if (line) lines.push(line.translateToString(true));
|
|
62
|
+
}
|
|
63
|
+
return lines.join("\n").replace(/\n+$/, "");
|
|
64
|
+
}
|
|
65
|
+
function useTerminal(containerRef, options = {}) {
|
|
66
|
+
const xtermRef = useRef(null);
|
|
67
|
+
const fitRef = useRef(null);
|
|
68
|
+
const optsRef = useRef(options);
|
|
69
|
+
optsRef.current = options;
|
|
70
|
+
const handleRef = useRef(null);
|
|
71
|
+
if (handleRef.current === null) {
|
|
72
|
+
handleRef.current = {
|
|
73
|
+
get xterm() {
|
|
74
|
+
return xtermRef.current;
|
|
75
|
+
},
|
|
76
|
+
write: (d) => xtermRef.current?.write(d),
|
|
77
|
+
writeln: (d) => xtermRef.current?.writeln(d),
|
|
78
|
+
clear: () => xtermRef.current?.clear(),
|
|
79
|
+
reset: () => xtermRef.current?.reset(),
|
|
80
|
+
fit: () => {
|
|
81
|
+
try {
|
|
82
|
+
fitRef.current?.fit();
|
|
83
|
+
} catch {
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
focus: () => xtermRef.current?.focus(),
|
|
87
|
+
getBuffer: () => readBuffer(xtermRef.current),
|
|
88
|
+
getSelection: () => xtermRef.current?.getSelection() ?? ""
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
const handle = handleRef.current;
|
|
92
|
+
useEffect(() => {
|
|
93
|
+
const el = containerRef.current;
|
|
94
|
+
if (!el) return;
|
|
95
|
+
const o = optsRef.current;
|
|
96
|
+
const term = new Terminal$1({
|
|
97
|
+
theme: o.theme ?? fancyDarkTheme,
|
|
98
|
+
rows: o.rows,
|
|
99
|
+
cols: o.cols,
|
|
100
|
+
disableStdin: o.readOnly ?? false,
|
|
101
|
+
cursorBlink: o.cursorBlink ?? true,
|
|
102
|
+
cursorStyle: o.cursorStyle ?? "block",
|
|
103
|
+
fontFamily: o.fontFamily ?? DEFAULT_FONT,
|
|
104
|
+
fontSize: o.fontSize ?? 13,
|
|
105
|
+
scrollback: o.scrollback ?? 1e3,
|
|
106
|
+
allowProposedApi: true
|
|
107
|
+
});
|
|
108
|
+
const fitAddon = new FitAddon();
|
|
109
|
+
term.loadAddon(fitAddon);
|
|
110
|
+
term.open(el);
|
|
111
|
+
xtermRef.current = term;
|
|
112
|
+
fitRef.current = fitAddon;
|
|
113
|
+
const dataSub = term.onData((d) => optsRef.current.onData?.(d));
|
|
114
|
+
const resizeSub = term.onResize(
|
|
115
|
+
({ cols, rows }) => optsRef.current.onResize?.({ cols, rows })
|
|
116
|
+
);
|
|
117
|
+
if (o.initialOutput) term.write(o.initialOutput);
|
|
118
|
+
if ((o.fit ?? true) && el.clientWidth > 0 && el.clientHeight > 0) {
|
|
119
|
+
try {
|
|
120
|
+
fitAddon.fit();
|
|
121
|
+
} catch {
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return () => {
|
|
125
|
+
dataSub.dispose();
|
|
126
|
+
resizeSub.dispose();
|
|
127
|
+
term.dispose();
|
|
128
|
+
xtermRef.current = null;
|
|
129
|
+
fitRef.current = null;
|
|
130
|
+
};
|
|
131
|
+
}, [containerRef]);
|
|
132
|
+
useEffect(() => {
|
|
133
|
+
const t = xtermRef.current;
|
|
134
|
+
if (!t) return;
|
|
135
|
+
if (options.theme) t.options.theme = options.theme;
|
|
136
|
+
if (options.readOnly !== void 0) t.options.disableStdin = options.readOnly;
|
|
137
|
+
if (options.cursorBlink !== void 0) t.options.cursorBlink = options.cursorBlink;
|
|
138
|
+
if (options.cursorStyle) t.options.cursorStyle = options.cursorStyle;
|
|
139
|
+
if (options.fontSize) t.options.fontSize = options.fontSize;
|
|
140
|
+
if (options.fontFamily) t.options.fontFamily = options.fontFamily;
|
|
141
|
+
}, [options.theme, options.readOnly, options.cursorBlink, options.cursorStyle, options.fontSize, options.fontFamily]);
|
|
142
|
+
useTerminalFit(containerRef, () => handle.fit(), options.fit ?? true);
|
|
143
|
+
return handle;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// src/output-diff.ts
|
|
147
|
+
function diffOutput(written, next) {
|
|
148
|
+
if (next === written) return null;
|
|
149
|
+
if (next.startsWith(written)) return { reset: false, write: next.slice(written.length) };
|
|
150
|
+
return { reset: true, write: next };
|
|
151
|
+
}
|
|
152
|
+
var Terminal = forwardRef(function Terminal2({
|
|
153
|
+
output,
|
|
154
|
+
theme,
|
|
155
|
+
rows,
|
|
156
|
+
cols,
|
|
157
|
+
fit = true,
|
|
158
|
+
readOnly,
|
|
159
|
+
cursorBlink,
|
|
160
|
+
cursorStyle,
|
|
161
|
+
fontFamily,
|
|
162
|
+
fontSize,
|
|
163
|
+
scrollback,
|
|
164
|
+
initialOutput,
|
|
165
|
+
onData,
|
|
166
|
+
onResize,
|
|
167
|
+
className,
|
|
168
|
+
style,
|
|
169
|
+
...rest
|
|
170
|
+
}, ref) {
|
|
171
|
+
const containerRef = useRef(null);
|
|
172
|
+
const handle = useTerminal(containerRef, {
|
|
173
|
+
theme,
|
|
174
|
+
rows,
|
|
175
|
+
cols,
|
|
176
|
+
fit,
|
|
177
|
+
readOnly,
|
|
178
|
+
cursorBlink,
|
|
179
|
+
cursorStyle,
|
|
180
|
+
fontFamily,
|
|
181
|
+
fontSize,
|
|
182
|
+
scrollback,
|
|
183
|
+
// `initialOutput` is for uncontrolled use; with a controlled `output` the
|
|
184
|
+
// diffing effect below owns the buffer, so don't double-write.
|
|
185
|
+
initialOutput: output === void 0 ? initialOutput : void 0,
|
|
186
|
+
onData,
|
|
187
|
+
onResize
|
|
188
|
+
});
|
|
189
|
+
useImperativeHandle(ref, () => handle, [handle]);
|
|
190
|
+
const written = useRef("");
|
|
191
|
+
useEffect(() => {
|
|
192
|
+
if (output === void 0) return;
|
|
193
|
+
const change = diffOutput(written.current, output);
|
|
194
|
+
if (!change) return;
|
|
195
|
+
if (change.reset) handle.reset();
|
|
196
|
+
handle.write(change.write);
|
|
197
|
+
written.current = output;
|
|
198
|
+
}, [output, handle]);
|
|
199
|
+
return /* @__PURE__ */ jsx(
|
|
200
|
+
"div",
|
|
201
|
+
{
|
|
202
|
+
ref: containerRef,
|
|
203
|
+
"data-fancy-terminal": "",
|
|
204
|
+
"data-readonly": readOnly ? "" : void 0,
|
|
205
|
+
className,
|
|
206
|
+
style: { width: "100%", height: "100%", ...style },
|
|
207
|
+
...rest
|
|
208
|
+
}
|
|
209
|
+
);
|
|
210
|
+
});
|
|
211
|
+
function useTerminalSession(options) {
|
|
212
|
+
const [output, setOutput] = useState(options.initial ?? "");
|
|
213
|
+
const transportRef = useRef(options.transport);
|
|
214
|
+
transportRef.current = options.transport;
|
|
215
|
+
useEffect(() => {
|
|
216
|
+
const unsub = transportRef.current.subscribe((chunk) => setOutput((o) => o + chunk));
|
|
217
|
+
return () => {
|
|
218
|
+
if (typeof unsub === "function") unsub();
|
|
219
|
+
};
|
|
220
|
+
}, []);
|
|
221
|
+
const sendData = useCallback((data) => {
|
|
222
|
+
void transportRef.current.send(data);
|
|
223
|
+
}, []);
|
|
224
|
+
const append = useCallback((chunk) => setOutput((o) => o + chunk), []);
|
|
225
|
+
const clear = useCallback(() => setOutput(""), []);
|
|
226
|
+
return { output, sendData, append, clear };
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export { Terminal, fancyDarkTheme, useTerminal, useTerminalFit, useTerminalSession };
|
|
230
|
+
//# sourceMappingURL=index.js.map
|
|
231
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +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":"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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 };\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"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@particle-academy/fancy-term",
|
|
3
|
+
"version": "0.1.0",
|
|
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
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "git+https://github.com/Particle-Academy/fancy-term.git"
|
|
8
|
+
},
|
|
9
|
+
"homepage": "https://github.com/Particle-Academy/fancy-term#readme",
|
|
10
|
+
"bugs": "https://github.com/Particle-Academy/fancy-term/issues",
|
|
11
|
+
"type": "module",
|
|
12
|
+
"main": "./dist/index.cjs",
|
|
13
|
+
"module": "./dist/index.js",
|
|
14
|
+
"types": "./dist/index.d.ts",
|
|
15
|
+
"exports": {
|
|
16
|
+
".": {
|
|
17
|
+
"import": {
|
|
18
|
+
"types": "./dist/index.d.ts",
|
|
19
|
+
"default": "./dist/index.js"
|
|
20
|
+
},
|
|
21
|
+
"require": {
|
|
22
|
+
"types": "./dist/index.d.cts",
|
|
23
|
+
"default": "./dist/index.cjs"
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
"files": [
|
|
28
|
+
"dist",
|
|
29
|
+
"docs",
|
|
30
|
+
"README.md"
|
|
31
|
+
],
|
|
32
|
+
"scripts": {
|
|
33
|
+
"build": "tsup",
|
|
34
|
+
"dev": "tsup --watch",
|
|
35
|
+
"lint": "tsc --noEmit",
|
|
36
|
+
"test": "vitest run",
|
|
37
|
+
"clean": "rm -rf dist",
|
|
38
|
+
"prepublishOnly": "tsup"
|
|
39
|
+
},
|
|
40
|
+
"keywords": [
|
|
41
|
+
"react",
|
|
42
|
+
"terminal",
|
|
43
|
+
"xterm",
|
|
44
|
+
"agents",
|
|
45
|
+
"mcp",
|
|
46
|
+
"human-plus-ux",
|
|
47
|
+
"fancy"
|
|
48
|
+
],
|
|
49
|
+
"peerDependencies": {
|
|
50
|
+
"react": "^18.0.0 || ^19.0.0",
|
|
51
|
+
"react-dom": "^18.0.0 || ^19.0.0",
|
|
52
|
+
"@xterm/xterm": ">=5.0.0",
|
|
53
|
+
"@xterm/addon-fit": ">=0.10.0"
|
|
54
|
+
},
|
|
55
|
+
"devDependencies": {
|
|
56
|
+
"@types/react": "^19.0.0",
|
|
57
|
+
"@types/react-dom": "^19.0.0",
|
|
58
|
+
"@xterm/xterm": "^5.5.0",
|
|
59
|
+
"@xterm/addon-fit": "^0.10.0",
|
|
60
|
+
"react": "^19.0.0",
|
|
61
|
+
"react-dom": "^19.0.0",
|
|
62
|
+
"tsup": "^8.5.0",
|
|
63
|
+
"typescript": "^5.8.0",
|
|
64
|
+
"vitest": "^4.1.8"
|
|
65
|
+
},
|
|
66
|
+
"publishConfig": {
|
|
67
|
+
"access": "public"
|
|
68
|
+
},
|
|
69
|
+
"license": "MIT"
|
|
70
|
+
}
|