@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 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"]}
@@ -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 };
@@ -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
+ }