division-ds 0.3.1 → 0.4.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/AGENTS.md +2 -2
- package/package.json +1 -1
- package/public/docs/components/chart-panel.md +57 -42
- package/public/docs/components/code-block.md +106 -0
- package/public/docs/components/dropdown-menu.md +1 -0
- package/public/docs/components/kbd.md +24 -3
- package/public/docs/components/navbar.md +54 -3
- package/public/docs/components/panel.md +139 -0
- package/public/docs/components/progress.md +1 -1
- package/public/docs/components/terminal.md +405 -0
- package/public/manifest/components.json +581 -12
- package/public/manifest/tokens.json +178 -7
- package/scripts/lib/component-props.mjs +10 -1
- package/skills/division-ds/SKILL.md +1 -1
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
# Terminal
|
|
2
|
+
|
|
3
|
+
> A live terminal session in the page, built on xterm.js. It renders the output you write to it and reports what the person types. It knows nothing about the connection: your app owns that.
|
|
4
|
+
|
|
5
|
+
- **Best for:** A live, interactive process that needs real terminal behaviour: colour, cursor movement, full-screen programs.
|
|
6
|
+
- **Not for:** Static code or a finished log: use CodeBlock. A conversation shown as messages: use ChatThread.
|
|
7
|
+
- Organism. Source: `src/components/organisms/terminal/Terminal.tsx`
|
|
8
|
+
- Rendered docs page: `/components/terminal`
|
|
9
|
+
|
|
10
|
+
## Import
|
|
11
|
+
|
|
12
|
+
```tsx
|
|
13
|
+
import { Terminal } from 'src/components/organisms/terminal/Terminal'
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Paths are from the Division DS repository root. Installed with shadcn they start `src/division-ds/` instead of `src/` (`src/division-ds/components/...`); import them relative to your file. An install has no index barrels: import each component from its own file, never from `components/molecules`.
|
|
17
|
+
|
|
18
|
+
## Requires
|
|
19
|
+
|
|
20
|
+
- Import it from its own file and load the screen that uses it with React.lazy. It adds about 100 kB of JavaScript after gzip (about 380 kB before) and 1 kB of CSS, so it is not in the organisms barrel.
|
|
21
|
+
- A height. It is 24rem by default; set another with className (h-[32rem]), or flex-1 inside a column.
|
|
22
|
+
|
|
23
|
+
## Props
|
|
24
|
+
|
|
25
|
+
| Prop | Type | Default | Description |
|
|
26
|
+
|---|---|---|---|
|
|
27
|
+
| `label` | `string` | | Required. What this session is ("Build agent"). Names the terminal, its input and its status announcements. |
|
|
28
|
+
| `status` | `'connecting' \| 'live' \| 'reconnecting' \| 'exited' \| 'error'` | | Required. connecting and reconnecting are info, live is ok, exited is neutral for code 0 and hostile otherwise, error is hostile. Buffered output stays visible in all of them. |
|
|
29
|
+
| `exitCode` | `number` | | With status exited: shown beside the word, and a non-zero code turns it hostile. |
|
|
30
|
+
| `statusMessage` | `string` | | Detail beside the status, and announced with it: the reason for an error, the attempt while reconnecting. |
|
|
31
|
+
| `readOnly` | `boolean` | `false` | Blocks input and never fires onData. Selecting and copying keep working, and Tab and Escape leave the terminal as they do anywhere else. |
|
|
32
|
+
| `announceOutput` | `boolean` | `false` | Reads output aloud through xterm’s live region, up to 20 lines at a time. For a single session a screen reader user is working in. Leave it off when several terminals share a page. |
|
|
33
|
+
| `onData` | `(data: string) => void` | | What the person typed or pasted, plus the replies a program asks the terminal for. Send it to the process. |
|
|
34
|
+
| `onResize` | `(size: { cols: number; rows: number }) => void` | | The new grid size, debounced by 100ms and only when cols or rows changed. It fires once after mounting. |
|
|
35
|
+
| `scrollback` | `number` | `5000` | Lines kept above the screen. xterm’s own default is 1000, which a busy agent passes in seconds. |
|
|
36
|
+
| `className` | `string` | | Merged onto the frame. Set the height here. |
|
|
37
|
+
| `ref` | `Ref<TerminalHandle>` | | The handle with the four methods below. |
|
|
38
|
+
| `write (TerminalHandle)` | `(data: string \| Uint8Array, callback?: () => void) => void` | | Sends output to the terminal, without a React render. The callback runs when it has been parsed: use it for flow control, because xterm throws data away past 50 MB waiting. |
|
|
39
|
+
| `clear (TerminalHandle)` | `() => void` | | Clears the screen and the scrollback, keeping the line the cursor is on. |
|
|
40
|
+
| `focus (TerminalHandle)` | `() => void` | | Moves keyboard focus into the terminal. |
|
|
41
|
+
| `fit (TerminalHandle)` | `() => void` | | Fits the grid to the frame now. Resizing already does this. |
|
|
42
|
+
|
|
43
|
+
## Types and helpers
|
|
44
|
+
|
|
45
|
+
Exported beside the component, from the same file. Import a type with `import type { ... }`.
|
|
46
|
+
|
|
47
|
+
```ts
|
|
48
|
+
export type TerminalStatus = 'connecting' | 'live' | 'reconnecting' | 'exited' | 'error'
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
```ts
|
|
52
|
+
export type TerminalHandle = {
|
|
53
|
+
/** Sends output to the terminal, without a React render. The callback runs once it is parsed: use it for flow control. */
|
|
54
|
+
write(data: string | Uint8Array, callback?: () => void): void
|
|
55
|
+
/** Clears the screen and the scrollback, keeping the cursor's line. */
|
|
56
|
+
clear(): void
|
|
57
|
+
/** Moves keyboard focus into the terminal. */
|
|
58
|
+
focus(): void
|
|
59
|
+
/** Fits the grid to the container now. Resizing already does this; use it after a change no resize reports. */
|
|
60
|
+
fit(): void
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Examples
|
|
65
|
+
|
|
66
|
+
Each is compiled and rendered on the docs page. Imports are omitted; see above.
|
|
67
|
+
|
|
68
|
+
### Connecting
|
|
69
|
+
|
|
70
|
+
```tsx
|
|
71
|
+
<Terminal label="Build agent" status="connecting" className="h-40 max-w-3xl" />
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### Live, with a simulated stream
|
|
75
|
+
|
|
76
|
+
```tsx
|
|
77
|
+
const ESC = '\u001b'
|
|
78
|
+
|
|
79
|
+
const paint = (code: number, text: string) => `${ESC}[${code}m${text}${ESC}[0m`
|
|
80
|
+
|
|
81
|
+
const COLOURS = [31, 32, 33, 34, 35, 36, 37, 90, 91, 92, 93, 94, 95, 96, 97]
|
|
82
|
+
.map(code => paint(code, `${code} `.padEnd(4)))
|
|
83
|
+
.join('')
|
|
84
|
+
|
|
85
|
+
const TRANSCRIPT = [
|
|
86
|
+
paint(1, 'Sample session, written by this page'),
|
|
87
|
+
'┌──────────────────────────────┐',
|
|
88
|
+
`│ ${paint(32, '✔')} tests passed 宽字符 🚀 │`,
|
|
89
|
+
'└──────────────────────────────┘',
|
|
90
|
+
COLOURS,
|
|
91
|
+
'Docs at https://example.com/terminal',
|
|
92
|
+
'',
|
|
93
|
+
].join('\r\n')
|
|
94
|
+
|
|
95
|
+
const STREAM = ['compiling', 'linking', 'checking types', 'running tests', 'writing bundle']
|
|
96
|
+
|
|
97
|
+
function LiveDemo() {
|
|
98
|
+
const term = useRef<TerminalHandle>(null)
|
|
99
|
+
const [streaming, setStreaming] = useState(false)
|
|
100
|
+
|
|
101
|
+
useEffect(() => {
|
|
102
|
+
term.current?.write(`${TRANSCRIPT}Type here, or start the stream.\r\n$ `)
|
|
103
|
+
}, [])
|
|
104
|
+
|
|
105
|
+
useEffect(() => {
|
|
106
|
+
if (!streaming) return
|
|
107
|
+
let line = 0
|
|
108
|
+
const timer = window.setInterval(() => {
|
|
109
|
+
const step = STREAM[line % STREAM.length]
|
|
110
|
+
const code = 31 + (line % 6)
|
|
111
|
+
term.current?.write(`${paint(code, `[${String(line + 1).padStart(3, '0')}]`)} ${step}\r\n`)
|
|
112
|
+
line += 1
|
|
113
|
+
if (line === 40) setStreaming(false)
|
|
114
|
+
}, 120)
|
|
115
|
+
return () => window.clearInterval(timer)
|
|
116
|
+
}, [streaming])
|
|
117
|
+
|
|
118
|
+
return (
|
|
119
|
+
<div className="flex max-w-3xl flex-col gap-3">
|
|
120
|
+
<Terminal
|
|
121
|
+
ref={term}
|
|
122
|
+
label="Sample build"
|
|
123
|
+
status="live"
|
|
124
|
+
className="h-80"
|
|
125
|
+
onData={data => term.current?.write(data === '\r' ? '\r\n$ ' : data)}
|
|
126
|
+
/>
|
|
127
|
+
<div className="flex flex-wrap gap-3">
|
|
128
|
+
<Button variant="secondary" onClick={() => setStreaming(on => !on)}>{streaming ? 'Stop the stream' : 'Start the stream'}</Button>
|
|
129
|
+
<Button variant="secondary" onClick={() => term.current?.clear()}>Clear</Button>
|
|
130
|
+
</div>
|
|
131
|
+
</div>
|
|
132
|
+
)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
<LiveDemo />
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### Read only: select and copy, but no typing
|
|
139
|
+
|
|
140
|
+
```tsx
|
|
141
|
+
const ESC = '\u001b'
|
|
142
|
+
|
|
143
|
+
const paint = (code: number, text: string) => `${ESC}[${code}m${text}${ESC}[0m`
|
|
144
|
+
|
|
145
|
+
const COLOURS = [31, 32, 33, 34, 35, 36, 37, 90, 91, 92, 93, 94, 95, 96, 97]
|
|
146
|
+
.map(code => paint(code, `${code} `.padEnd(4)))
|
|
147
|
+
.join('')
|
|
148
|
+
|
|
149
|
+
const TRANSCRIPT = [
|
|
150
|
+
paint(1, 'Sample session, written by this page'),
|
|
151
|
+
'┌──────────────────────────────┐',
|
|
152
|
+
`│ ${paint(32, '✔')} tests passed 宽字符 🚀 │`,
|
|
153
|
+
'└──────────────────────────────┘',
|
|
154
|
+
COLOURS,
|
|
155
|
+
'Docs at https://example.com/terminal',
|
|
156
|
+
'',
|
|
157
|
+
].join('\r\n')
|
|
158
|
+
|
|
159
|
+
function SampleTerminal(props: Omit<TerminalProps, 'ref'>) {
|
|
160
|
+
const term = useRef<TerminalHandle>(null)
|
|
161
|
+
useEffect(() => term.current?.write(TRANSCRIPT), [])
|
|
162
|
+
return <Terminal ref={term} className="h-64 max-w-3xl" {...props} />
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
<SampleTerminal label="Deploy log" status="live" readOnly />
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
### Reconnecting, with the output kept
|
|
169
|
+
|
|
170
|
+
```tsx
|
|
171
|
+
const ESC = '\u001b'
|
|
172
|
+
|
|
173
|
+
const paint = (code: number, text: string) => `${ESC}[${code}m${text}${ESC}[0m`
|
|
174
|
+
|
|
175
|
+
const COLOURS = [31, 32, 33, 34, 35, 36, 37, 90, 91, 92, 93, 94, 95, 96, 97]
|
|
176
|
+
.map(code => paint(code, `${code} `.padEnd(4)))
|
|
177
|
+
.join('')
|
|
178
|
+
|
|
179
|
+
const TRANSCRIPT = [
|
|
180
|
+
paint(1, 'Sample session, written by this page'),
|
|
181
|
+
'┌──────────────────────────────┐',
|
|
182
|
+
`│ ${paint(32, '✔')} tests passed 宽字符 🚀 │`,
|
|
183
|
+
'└──────────────────────────────┘',
|
|
184
|
+
COLOURS,
|
|
185
|
+
'Docs at https://example.com/terminal',
|
|
186
|
+
'',
|
|
187
|
+
].join('\r\n')
|
|
188
|
+
|
|
189
|
+
function SampleTerminal(props: Omit<TerminalProps, 'ref'>) {
|
|
190
|
+
const term = useRef<TerminalHandle>(null)
|
|
191
|
+
useEffect(() => term.current?.write(TRANSCRIPT), [])
|
|
192
|
+
return <Terminal ref={term} className="h-64 max-w-3xl" {...props} />
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
<SampleTerminal label="Test agent" status="reconnecting" statusMessage="Attempt 2 of 5" />
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
### Exited with code 0
|
|
199
|
+
|
|
200
|
+
```tsx
|
|
201
|
+
const ESC = '\u001b'
|
|
202
|
+
|
|
203
|
+
const paint = (code: number, text: string) => `${ESC}[${code}m${text}${ESC}[0m`
|
|
204
|
+
|
|
205
|
+
const COLOURS = [31, 32, 33, 34, 35, 36, 37, 90, 91, 92, 93, 94, 95, 96, 97]
|
|
206
|
+
.map(code => paint(code, `${code} `.padEnd(4)))
|
|
207
|
+
.join('')
|
|
208
|
+
|
|
209
|
+
const TRANSCRIPT = [
|
|
210
|
+
paint(1, 'Sample session, written by this page'),
|
|
211
|
+
'┌──────────────────────────────┐',
|
|
212
|
+
`│ ${paint(32, '✔')} tests passed 宽字符 🚀 │`,
|
|
213
|
+
'└──────────────────────────────┘',
|
|
214
|
+
COLOURS,
|
|
215
|
+
'Docs at https://example.com/terminal',
|
|
216
|
+
'',
|
|
217
|
+
].join('\r\n')
|
|
218
|
+
|
|
219
|
+
function SampleTerminal(props: Omit<TerminalProps, 'ref'>) {
|
|
220
|
+
const term = useRef<TerminalHandle>(null)
|
|
221
|
+
useEffect(() => term.current?.write(TRANSCRIPT), [])
|
|
222
|
+
return <Terminal ref={term} className="h-64 max-w-3xl" {...props} />
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
<SampleTerminal label="Migration" status="exited" exitCode={0} readOnly />
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
### Exited with an error code
|
|
229
|
+
|
|
230
|
+
```tsx
|
|
231
|
+
const ESC = '\u001b'
|
|
232
|
+
|
|
233
|
+
const paint = (code: number, text: string) => `${ESC}[${code}m${text}${ESC}[0m`
|
|
234
|
+
|
|
235
|
+
const COLOURS = [31, 32, 33, 34, 35, 36, 37, 90, 91, 92, 93, 94, 95, 96, 97]
|
|
236
|
+
.map(code => paint(code, `${code} `.padEnd(4)))
|
|
237
|
+
.join('')
|
|
238
|
+
|
|
239
|
+
const TRANSCRIPT = [
|
|
240
|
+
paint(1, 'Sample session, written by this page'),
|
|
241
|
+
'┌──────────────────────────────┐',
|
|
242
|
+
`│ ${paint(32, '✔')} tests passed 宽字符 🚀 │`,
|
|
243
|
+
'└──────────────────────────────┘',
|
|
244
|
+
COLOURS,
|
|
245
|
+
'Docs at https://example.com/terminal',
|
|
246
|
+
'',
|
|
247
|
+
].join('\r\n')
|
|
248
|
+
|
|
249
|
+
function SampleTerminal(props: Omit<TerminalProps, 'ref'>) {
|
|
250
|
+
const term = useRef<TerminalHandle>(null)
|
|
251
|
+
useEffect(() => term.current?.write(TRANSCRIPT), [])
|
|
252
|
+
return <Terminal ref={term} className="h-64 max-w-3xl" {...props} />
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
<SampleTerminal label="Type check" status="exited" exitCode={2} readOnly />
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
### Connection error
|
|
259
|
+
|
|
260
|
+
```tsx
|
|
261
|
+
const ESC = '\u001b'
|
|
262
|
+
|
|
263
|
+
const paint = (code: number, text: string) => `${ESC}[${code}m${text}${ESC}[0m`
|
|
264
|
+
|
|
265
|
+
const COLOURS = [31, 32, 33, 34, 35, 36, 37, 90, 91, 92, 93, 94, 95, 96, 97]
|
|
266
|
+
.map(code => paint(code, `${code} `.padEnd(4)))
|
|
267
|
+
.join('')
|
|
268
|
+
|
|
269
|
+
const TRANSCRIPT = [
|
|
270
|
+
paint(1, 'Sample session, written by this page'),
|
|
271
|
+
'┌──────────────────────────────┐',
|
|
272
|
+
`│ ${paint(32, '✔')} tests passed 宽字符 🚀 │`,
|
|
273
|
+
'└──────────────────────────────┘',
|
|
274
|
+
COLOURS,
|
|
275
|
+
'Docs at https://example.com/terminal',
|
|
276
|
+
'',
|
|
277
|
+
].join('\r\n')
|
|
278
|
+
|
|
279
|
+
function SampleTerminal(props: Omit<TerminalProps, 'ref'>) {
|
|
280
|
+
const term = useRef<TerminalHandle>(null)
|
|
281
|
+
useEffect(() => term.current?.write(TRANSCRIPT), [])
|
|
282
|
+
return <Terminal ref={term} className="h-64 max-w-3xl" {...props} />
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
<SampleTerminal label="Review agent" status="error" statusMessage="The connection was refused." readOnly />
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
### Switching theme updates the colours and keeps the buffer
|
|
289
|
+
|
|
290
|
+
```tsx
|
|
291
|
+
const ESC = '\u001b'
|
|
292
|
+
|
|
293
|
+
const paint = (code: number, text: string) => `${ESC}[${code}m${text}${ESC}[0m`
|
|
294
|
+
|
|
295
|
+
const COLOURS = [31, 32, 33, 34, 35, 36, 37, 90, 91, 92, 93, 94, 95, 96, 97]
|
|
296
|
+
.map(code => paint(code, `${code} `.padEnd(4)))
|
|
297
|
+
.join('')
|
|
298
|
+
|
|
299
|
+
const TRANSCRIPT = [
|
|
300
|
+
paint(1, 'Sample session, written by this page'),
|
|
301
|
+
'┌──────────────────────────────┐',
|
|
302
|
+
`│ ${paint(32, '✔')} tests passed 宽字符 🚀 │`,
|
|
303
|
+
'└──────────────────────────────┘',
|
|
304
|
+
COLOURS,
|
|
305
|
+
'Docs at https://example.com/terminal',
|
|
306
|
+
'',
|
|
307
|
+
].join('\r\n')
|
|
308
|
+
|
|
309
|
+
function SampleTerminal(props: Omit<TerminalProps, 'ref'>) {
|
|
310
|
+
const term = useRef<TerminalHandle>(null)
|
|
311
|
+
useEffect(() => term.current?.write(TRANSCRIPT), [])
|
|
312
|
+
return <Terminal ref={term} className="h-64 max-w-3xl" {...props} />
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
<div className="flex max-w-3xl flex-col gap-3">
|
|
316
|
+
<SampleTerminal label="Theme check" status="live" readOnly />
|
|
317
|
+
<div className="flex flex-wrap gap-3">
|
|
318
|
+
<Button variant="secondary" onClick={() => setTheme('light')}>Light theme</Button>
|
|
319
|
+
<Button variant="secondary" onClick={() => setTheme('dark')}>Dark theme</Button>
|
|
320
|
+
</div>
|
|
321
|
+
</div>
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
## Keyboard
|
|
325
|
+
|
|
326
|
+
| Keys | Result |
|
|
327
|
+
|---|---|
|
|
328
|
+
| `Tab` | From the page, moves into the terminal. Inside it, goes to the program, unless readOnly is set, when it moves on. |
|
|
329
|
+
| `Ctrl` `Shift` `L` | Moves focus out of the terminal to the next tab stop. Tab and Escape belong to the program, so this is the way out. |
|
|
330
|
+
| `Ctrl` `C` | With text selected, copies it and clears the selection. With nothing selected, goes to the program as an interrupt. |
|
|
331
|
+
| `Ctrl` `Shift` `C` | Copies the selection and keeps it. |
|
|
332
|
+
| `Ctrl` `V` | Pastes. Ctrl+Shift+V does the same. A program that wants a literal Ctrl+V does not get it. |
|
|
333
|
+
|
|
334
|
+
## Do
|
|
335
|
+
|
|
336
|
+
- Give each terminal a label that says which session it is, so a page of them can be told apart.
|
|
337
|
+
- Set readOnly when nobody can usefully type: a finished process, a session someone else owns.
|
|
338
|
+
- Say why in statusMessage when status is error.
|
|
339
|
+
- Use write’s callback to pause the source when output arrives faster than the page can draw it.
|
|
340
|
+
|
|
341
|
+
## Don't
|
|
342
|
+
|
|
343
|
+
- Don’t import it from the organisms barrel, and don’t load it on a screen that does not show it.
|
|
344
|
+
- Don’t put a terminal inside another bordered panel; it brings its own frame.
|
|
345
|
+
- Don’t keep output in React state to pass down. Write it through the ref.
|
|
346
|
+
- Don’t expect the status to block input by itself: set readOnly when a process has exited.
|
|
347
|
+
|
|
348
|
+
## Common mistakes
|
|
349
|
+
|
|
350
|
+
Wrong:
|
|
351
|
+
|
|
352
|
+
```tsx
|
|
353
|
+
<Terminal label="Agent" status="live" output={text} />
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
Right:
|
|
357
|
+
|
|
358
|
+
```tsx
|
|
359
|
+
const term = useRef<TerminalHandle>(null)
|
|
360
|
+
const socket = useRef<WebSocket>(null)
|
|
361
|
+
const [status, setStatus] = useState<TerminalStatus>('connecting')
|
|
362
|
+
|
|
363
|
+
useEffect(() => {
|
|
364
|
+
const ws = new WebSocket(url)
|
|
365
|
+
ws.binaryType = 'arraybuffer'
|
|
366
|
+
socket.current = ws
|
|
367
|
+
ws.onopen = () => setStatus('live')
|
|
368
|
+
ws.onmessage = event =>
|
|
369
|
+
term.current?.write(typeof event.data === 'string' ? event.data : new Uint8Array(event.data))
|
|
370
|
+
ws.onclose = () => setStatus('reconnecting')
|
|
371
|
+
return () => ws.close()
|
|
372
|
+
}, [url])
|
|
373
|
+
|
|
374
|
+
<Terminal
|
|
375
|
+
ref={term}
|
|
376
|
+
label="Build agent"
|
|
377
|
+
status={status}
|
|
378
|
+
onData={data => socket.current?.send(data)}
|
|
379
|
+
onResize={size => socket.current?.send(JSON.stringify({ type: 'resize', ...size }))}
|
|
380
|
+
/>
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
Output goes through the ref, so a fast stream never re-renders React. The connection lives in your app, not in the component.
|
|
384
|
+
|
|
385
|
+
Wrong:
|
|
386
|
+
|
|
387
|
+
```tsx
|
|
388
|
+
<Suspense fallback={null}><LazyTerminal ref={term} ... /></Suspense> // with the socket in this component's effect
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
Right:
|
|
392
|
+
|
|
393
|
+
```tsx
|
|
394
|
+
const Screen = lazy(() => import('./AgentScreen')) // AgentScreen imports Terminal directly and owns the socket
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
If only the Terminal is lazy, your effect runs while the ref is still empty and the first output is lost. Lazy-load the screen that holds both.
|
|
398
|
+
|
|
399
|
+
## Anatomy
|
|
400
|
+
|
|
401
|
+
- **Frame**: A Panel on surface.base, which is the surface all sixteen ANSI colours are measured on. It shows the focus ring while the terminal has focus.
|
|
402
|
+
- **Title bar**: The label, the status Badge with a word and a tone, a Read only Badge when set, statusMessage, and the shortcut that leaves the terminal.
|
|
403
|
+
- **Status announcement**: A visually hidden status region. It says only the label and the status, so a screen reader hears changes and not output.
|
|
404
|
+
- **Grid**: xterm.js in the mono font, fitted to the frame. Colours are read from tokens at runtime and updated when the theme changes, keeping the buffer.
|
|
405
|
+
- **Screen reader rows**: xterm’s accessibility tree: the visible rows as a list a screen reader can browse. Scrolling the terminal changes which rows it holds.
|