@particle-academy/fancy-term 0.5.0 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -1
- package/docs/ShellSwitcher.md +57 -0
- package/docs/Terminal.md +117 -0
- package/package.json +6 -6
package/README.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# @particle-academy/fancy-term
|
|
2
2
|
|
|
3
|
+
[](https://particle.academy)
|
|
4
|
+
|
|
3
5
|
**Human+ Terminal for React** — a controlled, themeable `<Terminal>` wrapping
|
|
4
6
|
[xterm.js](https://xtermjs.org), with hooks and an MCP-bridgeable surface so
|
|
5
7
|
embedded agents read the buffer, write input, and run commands **without
|
|
@@ -16,7 +18,7 @@ Like every Fancy UI component it serves two surfaces at once:
|
|
|
16
18
|
Sexy by default via a Fancy dark theme drawn from the react-fancy Tailwind v4
|
|
17
19
|
tokens.
|
|
18
20
|
|
|
19
|
-
> **Status:**
|
|
21
|
+
> **Status:** pre-1.0. `<Terminal>` + `useTerminal` / `useTerminalFit` /
|
|
20
22
|
> `useTerminalSession` are in place, plus **shell / profile switching** (the
|
|
21
23
|
> `<ShellSwitcher>` component, controlled `shells` / `activeShell` props, and the
|
|
22
24
|
> session hook's `switchShell`). The `registerTerminalBridge` MCP bridge
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# ShellSwitcher
|
|
2
|
+
|
|
3
|
+
The shell/profile selector shown above a `<Terminal>`. fancy-term renders the
|
|
4
|
+
control and tracks the choice; **the host owns the list and does the work** —
|
|
5
|
+
reconnecting a PTY to the chosen shell is yours.
|
|
6
|
+
|
|
7
|
+
## Import
|
|
8
|
+
|
|
9
|
+
```tsx
|
|
10
|
+
import { ShellSwitcher, BUILTIN_SHELLS, resolveShell } from "@particle-academy/fancy-term";
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
Most hosts never render it directly — set `showShellBar` on `<Terminal>`:
|
|
16
|
+
|
|
17
|
+
```tsx
|
|
18
|
+
<Terminal
|
|
19
|
+
shells={BUILTIN_SHELLS}
|
|
20
|
+
showShellBar
|
|
21
|
+
onShellChange={(id, profile) => session.reconnect(profile)}
|
|
22
|
+
/>
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Render it yourself when it belongs somewhere else in your chrome:
|
|
26
|
+
|
|
27
|
+
```tsx
|
|
28
|
+
<ShellSwitcher shells={shells} active={id} onChange={setId} />
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## `ShellProfile`
|
|
32
|
+
|
|
33
|
+
A profile is plain data, so an agent can emit one and a host can store it:
|
|
34
|
+
|
|
35
|
+
| Field | Description |
|
|
36
|
+
|---|---|
|
|
37
|
+
| id | The id used by `activeShell`, `setShell` and `onShellChange`. |
|
|
38
|
+
| label | What the user sees. |
|
|
39
|
+
| … | Whatever else your backend needs to launch it. |
|
|
40
|
+
|
|
41
|
+
`BUILTIN_SHELLS` is a set of sensible presets to spread, not a fixed menu —
|
|
42
|
+
add, remove or replace freely.
|
|
43
|
+
|
|
44
|
+
## `resolveShell(shells, id)`
|
|
45
|
+
|
|
46
|
+
The pure helper that `setShell`, `<ShellSwitcher>` and the session hook all
|
|
47
|
+
agree on. Returns the matching `ShellProfile`, or `undefined` when the id is not
|
|
48
|
+
in the list — which is why an unknown id is a no-op rather than a crash.
|
|
49
|
+
|
|
50
|
+
## Controlled vs uncontrolled
|
|
51
|
+
|
|
52
|
+
Pass `activeShell` to control the selection. Omit it and `<Terminal>` tracks it
|
|
53
|
+
internally; `handle.setShell(id)` still works and still fires `onShellChange`.
|
|
54
|
+
|
|
55
|
+
## See also
|
|
56
|
+
|
|
57
|
+
- [Terminal](./Terminal.md)
|
package/docs/Terminal.md
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# Terminal
|
|
2
|
+
|
|
3
|
+
A controlled, themeable terminal over xterm.js. The output buffer is React
|
|
4
|
+
state, so a host can stream command output straight from its own data layer,
|
|
5
|
+
and an agent can read what the human sees.
|
|
6
|
+
|
|
7
|
+
## Import
|
|
8
|
+
|
|
9
|
+
```tsx
|
|
10
|
+
import { Terminal } from "@particle-academy/fancy-term";
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Basic Usage
|
|
14
|
+
|
|
15
|
+
```tsx
|
|
16
|
+
<Terminal output={log} onData={(d) => pty.write(d)} />
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Controlled output
|
|
20
|
+
|
|
21
|
+
`output` is diffed against the previous value and only the **appended delta** is
|
|
22
|
+
written, so re-rendering on every chunk is cheap — wire it to a stream and let
|
|
23
|
+
React do the rest. Replacing `output` with a string that does not extend the
|
|
24
|
+
previous one resets the terminal and rewrites it.
|
|
25
|
+
|
|
26
|
+
## Props
|
|
27
|
+
|
|
28
|
+
Extends `TerminalOptions` (below) and the native `<div>` attributes, minus
|
|
29
|
+
`onInput`, `onResize`, `onPaste`, `contextMenu` and `children`.
|
|
30
|
+
|
|
31
|
+
| Prop | Type | Default | Description |
|
|
32
|
+
|------|------|---------|-------------|
|
|
33
|
+
| output | `string` | — | The controlled buffer. Only the appended delta is written. |
|
|
34
|
+
| shells | `ShellProfile[]` | — | The shells/profiles you offer. Spread `BUILTIN_SHELLS` for presets; the host owns the list. |
|
|
35
|
+
| activeShell | `string` | — | Controlled active shell id. Omit for internal selection. |
|
|
36
|
+
| onShellChange | `(id, profile) => void` | — | Fired when the user or `setShell` switches. |
|
|
37
|
+
| showShellBar | `boolean` | `false` | Render the built-in `<ShellSwitcher>` above the surface. Opt-in so existing layout is unchanged. |
|
|
38
|
+
| contextMenu | `TerminalContextMenuConfig` | `true` | `false` disables; an array replaces the items; `(ctx, defaults) => items` lets you add/reorder. |
|
|
39
|
+
|
|
40
|
+
### `TerminalOptions`
|
|
41
|
+
|
|
42
|
+
| Option | Type | Default | Description |
|
|
43
|
+
|------|------|---------|-------------|
|
|
44
|
+
| theme | `TerminalTheme` | Fancy dark | xterm colour theme. |
|
|
45
|
+
| rows / cols | `number` | — | Fixed grid. Omit and leave `fit` on to size from the container. |
|
|
46
|
+
| fit | `boolean` | `true` | Auto-fit via the fit addon + `ResizeObserver`. |
|
|
47
|
+
| readOnly | `boolean` | `false` | Block stdin (display-only). |
|
|
48
|
+
| cursorBlink | `boolean` | `true` | |
|
|
49
|
+
| cursorStyle | `CursorStyle` | `"block"` | |
|
|
50
|
+
| fontFamily | `string` | — | Monospace stack. |
|
|
51
|
+
| fontSize | `number` | `13` | |
|
|
52
|
+
| scrollback | `number` | `1000` | |
|
|
53
|
+
| initialOutput | `string` | — | Written once on mount, before any controlled `output`. |
|
|
54
|
+
| onData | `(data: string) => void` | — | Keystrokes and paste data. Wire to your PTY. |
|
|
55
|
+
| onResize | `(size) => void` | — | |
|
|
56
|
+
| clipboard | `ClipboardOption` | `true` | See below. |
|
|
57
|
+
| osc52 | `Osc52Mode` | `"copy"` | See below. |
|
|
58
|
+
| copyPaste | `CopyPasteMode` | — | `"contextmenu"`, `"linux"` (highlight-to-copy, middle-click paste) or `"winmac"`. Ctrl+Shift+C always copies. |
|
|
59
|
+
| onReady | `(xterm: XTerm) => void` | — | Once opened and attached — the imperative twin of `handle.ready`. |
|
|
60
|
+
| onPaste | `(payload) => void \| boolean` | — | See "Pasted images" below. |
|
|
61
|
+
|
|
62
|
+
## Clipboard is injectable, and that matters in Electron
|
|
63
|
+
|
|
64
|
+
`clipboard` gates the copy chord, the paste interceptor, the context-menu
|
|
65
|
+
copy/paste and OSC 52:
|
|
66
|
+
|
|
67
|
+
- `true` / omitted — backed by `navigator.clipboard`.
|
|
68
|
+
- `false` — disabled. Native text paste still works.
|
|
69
|
+
- a `{ writeText, readText }` **provider** — every copy/paste path routes
|
|
70
|
+
through it.
|
|
71
|
+
|
|
72
|
+
Supply a provider in a sandboxed Electron renderer, where `navigator.clipboard`
|
|
73
|
+
**silently no-ops**, to bridge to the main-process clipboard over IPC. Silently
|
|
74
|
+
is the operative word: without it, copy appears to work and nothing lands.
|
|
75
|
+
|
|
76
|
+
## OSC 52 defaults to write-only, deliberately
|
|
77
|
+
|
|
78
|
+
Terminal programs (Claude Code, tmux, vim) can set or read the system clipboard
|
|
79
|
+
via `ESC ] 52`. `osc52` is `"copy"` by default — writes only. `"read"` and
|
|
80
|
+
`"both"` also answer read requests, which lets anything running in the terminal
|
|
81
|
+
exfiltrate the clipboard, so they are opt-in. `false` disables it entirely.
|
|
82
|
+
|
|
83
|
+
## Pasted images
|
|
84
|
+
|
|
85
|
+
`onPaste` receives `{ text, files, images }` on every paste. Plain text still
|
|
86
|
+
pastes natively; this is where a host receives pasted **images**, which a shell
|
|
87
|
+
cannot render — upload them, hand them to an agent, or write a path. Return
|
|
88
|
+
`false` to consume the paste entirely (for example to transform it, then call
|
|
89
|
+
`handle.paste(...)` yourself).
|
|
90
|
+
|
|
91
|
+
## Imperative handle
|
|
92
|
+
|
|
93
|
+
| Member | Type | Description |
|
|
94
|
+
|---|---|---|
|
|
95
|
+
| xterm | `XTerm \| null` | The instance. Null before mount. |
|
|
96
|
+
| ready | `Promise<XTerm>` | Resolves once opened and measured. `await` it instead of polling; re-armed if the terminal is recreated. |
|
|
97
|
+
| write / writeln | `(data: string) => void` | Raw write; ANSI honoured. |
|
|
98
|
+
| clear / reset | `() => void` | Viewport (keeps scrollback) / full reset. |
|
|
99
|
+
| fit | `() => void` | Re-fit. No-op on a 0-size box or with `fit` off. |
|
|
100
|
+
| focus | `() => void` | |
|
|
101
|
+
| getBuffer | `() => string` | The full buffer as plain text — **what an agent "sees"**. |
|
|
102
|
+
| getSelection | `() => string` | |
|
|
103
|
+
| copySelection | `() => Promise<boolean>` | False when nothing is selected or no clipboard. |
|
|
104
|
+
| paste | `(text?) => Promise<void>` | Reads the system clipboard with no argument. Honours bracketed paste. |
|
|
105
|
+
| selectAll / clearSelection | `() => void` | |
|
|
106
|
+
| setShell | `(id: string) => void` | Resolves the profile and fires `onShellChange`. No-op for an unknown id. |
|
|
107
|
+
| getShell | `() => string \| undefined` | |
|
|
108
|
+
|
|
109
|
+
`getBuffer()` is the Human+ affordance: an agent reads the same text the person
|
|
110
|
+
is looking at, rather than scraping the DOM.
|
|
111
|
+
|
|
112
|
+
## Headless hooks
|
|
113
|
+
|
|
114
|
+
`useTerminal`, `useTerminalFit` and `useTerminalSession` expose the engine layer
|
|
115
|
+
if you would rather build your own surface. `useTerminalSession` takes a
|
|
116
|
+
`TerminalSessionTransport` and manages connect/reconnect against your backend —
|
|
117
|
+
see [ShellSwitcher](./ShellSwitcher.md) for the shell-selection half.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@particle-academy/fancy-term",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.1",
|
|
4
4
|
"description": "Human+ Terminal for React — a controlled, themeable <Terminal> wrapping xterm.js, with hooks and an MCP-bridgeable surface so embedded agents read the buffer, write input, and run commands without DOM-scraping.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -53,18 +53,18 @@
|
|
|
53
53
|
"react-dom": "^19.0.0"
|
|
54
54
|
},
|
|
55
55
|
"devDependencies": {
|
|
56
|
-
"@types/react": "^19.2.
|
|
57
|
-
"@types/react-dom": "^19.2.
|
|
56
|
+
"@types/react": "^19.2.18",
|
|
57
|
+
"@types/react-dom": "^19.2.4",
|
|
58
58
|
"@xterm/addon-fit": "^0.10.0",
|
|
59
59
|
"@xterm/xterm": "^5.5.0",
|
|
60
60
|
"eslint": "^10.8.0",
|
|
61
61
|
"eslint-plugin-react-hooks": "^7.1.1",
|
|
62
|
-
"react": "^19.2.
|
|
63
|
-
"react-dom": "^19.2.
|
|
62
|
+
"react": "^19.2.8",
|
|
63
|
+
"react-dom": "^19.2.8",
|
|
64
64
|
"tsup": "^8.5.0",
|
|
65
65
|
"typescript": "^5.8.0",
|
|
66
66
|
"typescript-eslint": "^8.65.0",
|
|
67
|
-
"vitest": "^4.1.
|
|
67
|
+
"vitest": "^4.1.11"
|
|
68
68
|
},
|
|
69
69
|
"publishConfig": {
|
|
70
70
|
"access": "public"
|