@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 CHANGED
@@ -1,5 +1,7 @@
1
1
  # @particle-academy/fancy-term
2
2
 
3
+ [![Fancified](art/fancified.svg)](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:** 0.2.0. `<Terminal>` + `useTerminal` / `useTerminalFit` /
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)
@@ -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.0",
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.0",
57
- "@types/react-dom": "^19.2.0",
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.0",
63
- "react-dom": "^19.2.0",
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.8"
67
+ "vitest": "^4.1.11"
68
68
  },
69
69
  "publishConfig": {
70
70
  "access": "public"