@particle-academy/fancy-term 0.5.0 → 0.6.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/CHANGELOG.md +181 -0
- package/LICENSE +21 -0
- package/README.md +22 -3
- package/docs/ShellSwitcher.md +57 -0
- package/docs/Terminal.md +117 -0
- package/package.json +16 -11
- package/xterm.css +28 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to `@particle-academy/fancy-term` are documented here.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
6
|
+
|
|
7
|
+
> **Pre-1.0:** breaking changes land in MINOR releases. Until 1.0 the minor
|
|
8
|
+
> number is not a compatibility promise — read the entry, not the version.
|
|
9
|
+
|
|
10
|
+
> This file starts here. Earlier releases predate it and were never written up;
|
|
11
|
+
> `git log` is the record for those. It is not backfilled rather than
|
|
12
|
+
> guessed-at, because a changelog that invents its own history is worse than one
|
|
13
|
+
> that admits where it begins.
|
|
14
|
+
|
|
15
|
+
## [Unreleased]
|
|
16
|
+
|
|
17
|
+
## 0.6.0 — 2026-10-10
|
|
18
|
+
|
|
19
|
+
### Added
|
|
20
|
+
|
|
21
|
+
- **`@particle-academy/fancy-term/xterm.css` — load the stylesheet without naming xterm.**
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
import "@particle-academy/fancy-term/xterm.css";
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
The terminal has never rendered without xterm's stylesheet, so every consumer
|
|
28
|
+
had to write `import "@xterm/xterm/css/xterm.css"` in their own source — which
|
|
29
|
+
made xterm the one third-party package a Fancy-only app still had to name.
|
|
30
|
+
|
|
31
|
+
**Nothing to do.** The old import is identical in effect and still works; this
|
|
32
|
+
is an additional way in, not a replacement. The new subpath is an `@import` of
|
|
33
|
+
xterm's own stylesheet rather than a copy of it, so it resolves to the single
|
|
34
|
+
copy already in your tree and there is no vendored third-party file to drift.
|
|
35
|
+
|
|
36
|
+
### Changed
|
|
37
|
+
|
|
38
|
+
- **The `@xterm/*` peer ranges are now bounded above** — `@xterm/xterm` moves
|
|
39
|
+
from `>=5.0.0` to `>=5.0.0 <6`, and `@xterm/addon-fit` from `>=0.10.0` to
|
|
40
|
+
`>=0.10.0 <0.11`.
|
|
41
|
+
|
|
42
|
+
**This was live, not theoretical.** `@xterm/xterm@6.0.0` and
|
|
43
|
+
`@xterm/addon-fit@0.11.0` are both `latest` on npm as of today, so the old
|
|
44
|
+
ranges admitted a MAJOR this package has never been built against — and
|
|
45
|
+
nothing reported it, because a resolver quietly picking an untested version
|
|
46
|
+
looks exactly like success. The pairing made it worse rather than safer:
|
|
47
|
+
`addon-fit@0.10.0` declares `peerDependencies: {"@xterm/xterm": "^5.0.0"}` and
|
|
48
|
+
accidentally held the line, while **`0.11.0` declares no peer at all** — so
|
|
49
|
+
`>=0.10.0` plus `>=5.0.0` allowed xterm 6 with addon-fit 0.11, an untested
|
|
50
|
+
combination, silently.
|
|
51
|
+
|
|
52
|
+
**What you must do: almost certainly nothing.** If you are on xterm 5.x and
|
|
53
|
+
addon-fit 0.10.x — what `npm install` has been resolving all along, and what
|
|
54
|
+
this package is built and tested against — the range still admits your
|
|
55
|
+
version and the upgrade is invisible. **If you have explicitly moved to xterm
|
|
56
|
+
6 or addon-fit 0.11, this release will now fail at install with a peer
|
|
57
|
+
conflict instead of running untested code.** That is the intended behaviour:
|
|
58
|
+
it turns a silent runtime risk into a loud install-time error. Tell us and we
|
|
59
|
+
will qualify 6 properly — widening a range later is safe by construction,
|
|
60
|
+
since it only ever adds candidates.
|
|
61
|
+
|
|
62
|
+
`>=X <2.0` remains correct for a *first-party* sibling, where we cut the
|
|
63
|
+
releases and the upper bound is a promise we keep. On third-party it is a
|
|
64
|
+
promise someone else makes.
|
|
65
|
+
|
|
66
|
+
- **xterm stays a PEER deliberately, and this is the release that writes down
|
|
67
|
+
why.** `<Terminal>` hands the live `XTerm` instance to the consumer through
|
|
68
|
+
`handle.xterm`, `handle.ready` and `onReady(xterm)` — the same reason a React
|
|
69
|
+
component cannot own its copy of React. If this package owned xterm, a
|
|
70
|
+
consumer on a different version would get two copies and addons and
|
|
71
|
+
`instanceof` would operate on the wrong class with no warning. The peer's
|
|
72
|
+
value is not "one copy", it is that a conflict fails loudly at install. The
|
|
73
|
+
reasoning now sits in the README, in `src/index.ts`, in `xterm.css` and in
|
|
74
|
+
`src/packaging.test.ts`, so the next person who proposes tidying it into
|
|
75
|
+
`dependencies` meets the argument first.
|
|
76
|
+
|
|
77
|
+
### Fixed
|
|
78
|
+
|
|
79
|
+
- **`CHANGELOG.md` is now in the published tarball.** `files` did not whitelist it, so npm never shipped it — and this package puts breaking changes in MINOR releases and tells you in the README to read the entry before taking one. The instruction existed for the author, who has the file, and not for the consumer, who is the only one being instructed. Nothing for you to do; the file simply arrives from this release on.
|
|
80
|
+
|
|
81
|
+
### Security
|
|
82
|
+
|
|
83
|
+
- `source-map-js` is pinned forward to `^1.2.2` via `overrides`. Versions up to
|
|
84
|
+
1.2.1 allow an event-loop denial of service through indexed source-map section
|
|
85
|
+
offsets, and it arrives here transitively through the build toolchain.
|
|
86
|
+
**Nothing for a consumer to do, and no runtime change**: an npm package does
|
|
87
|
+
not ship a lockfile, so this governs builds OF this repo, not anything
|
|
88
|
+
installed FROM it. Recorded rather than left silent because the override it
|
|
89
|
+
sits beside — `shell-quote` `^1.9.0`, added for an earlier advisory — was
|
|
90
|
+
carried with no note of why, and had drifted back inside the vulnerable range
|
|
91
|
+
before anyone looked.
|
|
92
|
+
|
|
93
|
+
## 0.5.1 — 2026-09-29
|
|
94
|
+
|
|
95
|
+
### Fixed
|
|
96
|
+
|
|
97
|
+
- **`docs/` is real now.** `files` already listed `docs`, and the directory did
|
|
98
|
+
not exist — so every published tarball carried a `files` entry pointing at
|
|
99
|
+
nothing and shipped no reference at all. That is the worse half of this
|
|
100
|
+
defect: a `files` array that names `docs` reads as compliant to anyone
|
|
101
|
+
checking the manifest, so nothing ever looked in the tarball.
|
|
102
|
+
|
|
103
|
+
Adds `docs/Terminal.md` and `docs/ShellSwitcher.md` — the full prop and
|
|
104
|
+
`TerminalOptions` tables, the imperative handle (including `getBuffer()`, the
|
|
105
|
+
Human+ affordance an agent reads instead of scraping the DOM), why the
|
|
106
|
+
clipboard is injectable and what silently breaks in a sandboxed Electron
|
|
107
|
+
renderer without a provider, why OSC 52 defaults to write-only, and how
|
|
108
|
+
pasted images reach the host.
|
|
109
|
+
|
|
110
|
+
No code changed.
|
|
111
|
+
|
|
112
|
+
## 0.5.0 — 2026-08-07
|
|
113
|
+
|
|
114
|
+
### Changed
|
|
115
|
+
|
|
116
|
+
- **BREAKING — Node 22 is now declared as the floor.** `engines.node` is `>=22`, where this package previously declared **nothing at all**.
|
|
117
|
+
|
|
118
|
+
Declaring nothing was not the same as supporting old Node: a consumer on 18 installed cleanly and found out at runtime.
|
|
119
|
+
|
|
120
|
+
**What you must do:** on Node 22 or newer, nothing. Note npm only *warns* on an `engines` mismatch while **pnpm fails the install**, so this surfaces differently depending on your package manager. Node 18 is end-of-life and 20 is maintenance-only.
|
|
121
|
+
|
|
122
|
+
- **BREAKING — React 18 is no longer supported.** `peerDependencies.react` / `react-dom` are now `^19.0.0`.
|
|
123
|
+
|
|
124
|
+
**What you must do:** on React 19, nothing. On React 18, stay on the previous release, or upgrade your app to 19 first.
|
|
125
|
+
|
|
126
|
+
React 18 support was a claim nothing tested — every build and test in this package ran against 19, so the 18 half of the old range was never executed. An untested compatibility claim is worse than an absent one, because it reads as support.
|
|
127
|
+
|
|
128
|
+
### Why
|
|
129
|
+
|
|
130
|
+
These are the kit 0.5 platform floors, applied across every package at once so a consumer never has to resolve a mix. **No API changed, nothing was removed, nothing was renamed** — only what the package requires.
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
## 0.4.1 — 2026-07-06
|
|
134
|
+
|
|
135
|
+
### Fixed
|
|
136
|
+
|
|
137
|
+
- context-menu Copy writes the menu-open selection snapshot, not a click-time re-read
|
|
138
|
+
|
|
139
|
+
## 0.4.0 — 2026-07-02
|
|
140
|
+
|
|
141
|
+
### Added
|
|
142
|
+
|
|
143
|
+
- Electron-safe clipboard — injectable provider, OSC 52, copy/paste modes, ready signal (#1)
|
|
144
|
+
|
|
145
|
+
## 0.3.0 — 2026-06-14
|
|
146
|
+
|
|
147
|
+
### Added
|
|
148
|
+
|
|
149
|
+
- clipboard (copy/paste + images) + customizable selection context menu
|
|
150
|
+
|
|
151
|
+
## 0.2.2 — 2026-06-11
|
|
152
|
+
|
|
153
|
+
### Fixed
|
|
154
|
+
|
|
155
|
+
- omit undefined rows/cols from xterm constructor (was console-erroring)
|
|
156
|
+
|
|
157
|
+
## 0.2.1 — 2026-06-11
|
|
158
|
+
|
|
159
|
+
### Fixed
|
|
160
|
+
|
|
161
|
+
- guard fit() via proposeDimensions — no xterm resize(undefined) on unlaid-out container
|
|
162
|
+
|
|
163
|
+
## 0.2.0 — 2026-06-11
|
|
164
|
+
|
|
165
|
+
### Changed
|
|
166
|
+
|
|
167
|
+
- **BREAKING** — shell / profile switching (UI + API + session)
|
|
168
|
+
|
|
169
|
+
## 0.1.0 — 2026-06-10
|
|
170
|
+
|
|
171
|
+
### Added
|
|
172
|
+
|
|
173
|
+
- fancy-term 0.1.0 — Human+ Terminal (xterm.js wrapper)
|
|
174
|
+
|
|
175
|
+
### Changed
|
|
176
|
+
|
|
177
|
+
- Replaced an `eslint-disable jsx-a11y/no-autofocus` in `ShellSwitcher` with a
|
|
178
|
+
plain comment explaining why the autofocus is deliberate. `jsx-a11y` has never
|
|
179
|
+
been a dependency of this package, so the directive silenced a rule that did
|
|
180
|
+
not exist — and broke linting the moment ESLint was actually turned on.
|
|
181
|
+
**No action needed**, no behaviour change.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Particle Academy
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
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
|
|
@@ -32,12 +34,29 @@ npm install @particle-academy/fancy-term @xterm/xterm @xterm/addon-fit
|
|
|
32
34
|
|
|
33
35
|
`react`, `xterm`, and the fit addon are **peer dependencies** — the wrapper itself
|
|
34
36
|
is zero-runtime-dep (the same posture as `fancy-echarts` over ECharts). Import the
|
|
35
|
-
|
|
37
|
+
stylesheet once in your app, from here rather than from xterm:
|
|
36
38
|
|
|
37
39
|
```ts
|
|
38
|
-
import "@
|
|
40
|
+
import "@particle-academy/fancy-term/xterm.css";
|
|
39
41
|
```
|
|
40
42
|
|
|
43
|
+
Without it the terminal does not render: xterm's character-measurement helper
|
|
44
|
+
has to stay out of layout, and without these rules it does not, so the cell size
|
|
45
|
+
comes out wrong.
|
|
46
|
+
|
|
47
|
+
That subpath re-exports xterm's own stylesheet with `@import`, so it resolves to
|
|
48
|
+
the single copy already in your tree. `import "@xterm/xterm/css/xterm.css"` is
|
|
49
|
+
identical in effect and still works — the subpath exists so a Fancy-only app has
|
|
50
|
+
no reason to name a third-party package in its own source.
|
|
51
|
+
|
|
52
|
+
**Why xterm is a peer and not a dependency**, since it is the obvious thing to
|
|
53
|
+
"tidy": `<Terminal>` hands you the live `XTerm` instance — `handle.xterm`,
|
|
54
|
+
`handle.ready`, `onReady(xterm)` — exactly as a React component hands out React
|
|
55
|
+
elements and therefore cannot own React. If this package owned its own xterm
|
|
56
|
+
copy, a consumer on a different version would get two, and addons and
|
|
57
|
+
`instanceof` would operate on the wrong class with no warning anywhere. The peer
|
|
58
|
+
turns that silent runtime break into a loud install-time error.
|
|
59
|
+
|
|
41
60
|
## `<Terminal>`
|
|
42
61
|
|
|
43
62
|
The parent needs a height — the terminal fits its container (like any xterm
|
|
@@ -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.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Human+ Terminal for React — a controlled, themeable <Terminal> wrapping xterm.js, with hooks and an MCP-bridgeable surface so embedded agents read the buffer, write input, and run commands without DOM-scraping.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -22,12 +22,16 @@
|
|
|
22
22
|
"types": "./dist/index.d.cts",
|
|
23
23
|
"default": "./dist/index.cjs"
|
|
24
24
|
}
|
|
25
|
-
}
|
|
25
|
+
},
|
|
26
|
+
"./xterm.css": "./xterm.css",
|
|
27
|
+
"./package.json": "./package.json"
|
|
26
28
|
},
|
|
27
29
|
"files": [
|
|
28
30
|
"dist",
|
|
31
|
+
"xterm.css",
|
|
29
32
|
"docs",
|
|
30
|
-
"README.md"
|
|
33
|
+
"README.md",
|
|
34
|
+
"CHANGELOG.md"
|
|
31
35
|
],
|
|
32
36
|
"scripts": {
|
|
33
37
|
"build": "tsup",
|
|
@@ -47,31 +51,32 @@
|
|
|
47
51
|
"fancy"
|
|
48
52
|
],
|
|
49
53
|
"peerDependencies": {
|
|
50
|
-
"@xterm/addon-fit": ">=0.10.0",
|
|
51
|
-
"@xterm/xterm": ">=5.0.0",
|
|
54
|
+
"@xterm/addon-fit": ">=0.10.0 <0.11",
|
|
55
|
+
"@xterm/xterm": ">=5.0.0 <6",
|
|
52
56
|
"react": "^19.0.0",
|
|
53
57
|
"react-dom": "^19.0.0"
|
|
54
58
|
},
|
|
55
59
|
"devDependencies": {
|
|
56
|
-
"@types/react": "^19.2.
|
|
57
|
-
"@types/react-dom": "^19.2.
|
|
60
|
+
"@types/react": "^19.2.18",
|
|
61
|
+
"@types/react-dom": "^19.2.4",
|
|
58
62
|
"@xterm/addon-fit": "^0.10.0",
|
|
59
63
|
"@xterm/xterm": "^5.5.0",
|
|
60
64
|
"eslint": "^10.8.0",
|
|
61
65
|
"eslint-plugin-react-hooks": "^7.1.1",
|
|
62
|
-
"react": "^19.2.
|
|
63
|
-
"react-dom": "^19.2.
|
|
66
|
+
"react": "^19.2.8",
|
|
67
|
+
"react-dom": "^19.2.8",
|
|
64
68
|
"tsup": "^8.5.0",
|
|
65
69
|
"typescript": "^5.8.0",
|
|
66
70
|
"typescript-eslint": "^8.65.0",
|
|
67
|
-
"vitest": "^4.1.
|
|
71
|
+
"vitest": "^4.1.11"
|
|
68
72
|
},
|
|
69
73
|
"publishConfig": {
|
|
70
74
|
"access": "public"
|
|
71
75
|
},
|
|
72
76
|
"license": "MIT",
|
|
73
77
|
"overrides": {
|
|
74
|
-
"esbuild": "^0.28.1"
|
|
78
|
+
"esbuild": "^0.28.1",
|
|
79
|
+
"source-map-js": "^1.2.2"
|
|
75
80
|
},
|
|
76
81
|
"engines": {
|
|
77
82
|
"node": ">=22"
|
package/xterm.css
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @particle-academy/fancy-term/xterm.css
|
|
3
|
+
*
|
|
4
|
+
* The stylesheet <Terminal> needs, loadable without naming xterm.
|
|
5
|
+
*
|
|
6
|
+
* import "@particle-academy/fancy-term/xterm.css";
|
|
7
|
+
*
|
|
8
|
+
* xterm.js does not render without its own stylesheet — its character-measurement
|
|
9
|
+
* helper is absolutely positioned, so without these rules it participates in
|
|
10
|
+
* layout and the terminal computes a wrong cell size. Every consumer therefore
|
|
11
|
+
* had to write `import "@xterm/xterm/css/xterm.css"` in their own source, which
|
|
12
|
+
* made xterm the one third-party package a Fancy-only app still had to name.
|
|
13
|
+
*
|
|
14
|
+
* This is an @import, deliberately, NOT a copy of xterm's stylesheet:
|
|
15
|
+
*
|
|
16
|
+
* • It resolves from the CONSUMER's tree, where the peer is installed — so
|
|
17
|
+
* there is still exactly one xterm, which is the whole reason xterm is a
|
|
18
|
+
* peer rather than a dependency (<Terminal> hands the live XTerm instance
|
|
19
|
+
* out through `handle.xterm` / `handle.ready` / `onReady`, so a second copy
|
|
20
|
+
* would silently break addons and `instanceof`).
|
|
21
|
+
* • A vendored copy would drift from the xterm version actually installed,
|
|
22
|
+
* and would make us a redistributor of a third-party file.
|
|
23
|
+
*
|
|
24
|
+
* `src/packaging.test.ts` pins it: with comments stripped, this file must be
|
|
25
|
+
* EXACTLY the one @import below and nothing else. So do not add a rule here —
|
|
26
|
+
* app-level overrides belong in the app, where they can be seen.
|
|
27
|
+
*/
|
|
28
|
+
@import "@xterm/xterm/css/xterm.css";
|