@knightcodeai/cli-linux-arm64 0.9.1 → 0.9.3
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/bin/CHANGELOG.md +60 -0
- package/bin/README.md +52 -19
- package/bin/docs/cli-integration.md +106 -0
- package/bin/docs/cli.md +270 -0
- package/bin/docs/compaction.md +56 -37
- package/bin/docs/configuration.md +46 -0
- package/bin/docs/containerization.md +86 -54
- package/bin/docs/custom-provider.md +132 -785
- package/bin/docs/docs.json +143 -103
- package/bin/docs/environment-variables.md +5 -4
- package/bin/docs/extensions.md +134 -2956
- package/bin/docs/how-knightcode-works.md +49 -0
- package/bin/docs/index.md +24 -69
- package/bin/docs/json.md +193 -65
- package/bin/docs/keybindings.md +56 -101
- package/bin/docs/llama-cpp.md +3 -3
- package/bin/docs/message-types.md +261 -0
- package/bin/docs/models.md +64 -547
- package/bin/docs/packages.md +66 -167
- package/bin/docs/prompt-templates.md +31 -68
- package/bin/docs/providers.md +103 -241
- package/bin/docs/quickstart.md +61 -106
- package/bin/docs/rpc-commands.md +854 -0
- package/bin/docs/rpc-extension-ui.md +200 -0
- package/bin/docs/rpc.md +129 -1556
- package/bin/docs/sdk.md +76 -1160
- package/bin/docs/security.md +70 -32
- package/bin/docs/session-format.md +25 -216
- package/bin/docs/sessions.md +38 -143
- package/bin/docs/settings.md +111 -389
- package/bin/docs/shell-aliases.md +85 -5
- package/bin/docs/skills.md +51 -189
- package/bin/docs/slash-commands.md +63 -0
- package/bin/docs/terminal-setup.md +107 -79
- package/bin/docs/termux.md +74 -83
- package/bin/docs/themes.md +68 -280
- package/bin/docs/tmux.md +31 -39
- package/bin/docs/tui.md +69 -923
- package/bin/docs/usage.md +79 -286
- package/bin/docs/windows.md +43 -17
- package/bin/export-html/template.js +6 -1
- package/bin/knightcode +2 -2
- package/bin/package.json +6 -6
- package/package.json +1 -1
- package/bin/docs/development.md +0 -71
package/bin/docs/tui.md
CHANGED
|
@@ -1,961 +1,107 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
# TUI Components
|
|
4
|
-
|
|
5
|
-
Extensions and custom tools can render custom TUI components for interactive user interfaces. This page covers the component system and available building blocks.
|
|
6
|
-
|
|
7
|
-
**Source:** [`@knightcode/tui`](https://github.com/KnightCodeAI/knightcode/tree/main/packages/tui)
|
|
8
|
-
|
|
9
|
-
## Component Interface
|
|
10
|
-
|
|
11
|
-
All components implement:
|
|
12
|
-
|
|
13
|
-
```typescript
|
|
14
|
-
interface Component {
|
|
15
|
-
render(width: number): string[];
|
|
16
|
-
handleInput?(data: string): void;
|
|
17
|
-
handleMouse?(event: TuiMouseEvent): TuiMouseEventResult | undefined;
|
|
18
|
-
wantsKeyRelease?: boolean;
|
|
19
|
-
invalidate(): void;
|
|
20
|
-
}
|
|
21
|
-
```
|
|
22
|
-
|
|
23
|
-
| Method | Description |
|
|
24
|
-
|--------|-------------|
|
|
25
|
-
| `render(width)` | Return array of strings (one per line). Each line **must not exceed `width`**. |
|
|
26
|
-
| `handleInput?(data)` | Receive keyboard input when component has focus. |
|
|
27
|
-
| `handleMouse?(event)` | Receive normalized pointer input in fullscreen mode. |
|
|
28
|
-
| `wantsKeyRelease?` | If true, component receives key release events (Kitty protocol). Default: false. |
|
|
29
|
-
| `invalidate()` | Clear cached render state. Called on theme changes. |
|
|
30
|
-
|
|
31
|
-
The TUI appends a full SGR reset and OSC 8 reset at the end of each rendered line. Styles do not carry across lines. If you emit multi-line text with styling, reapply styles per line or use `wrapTextWithAnsi()` so styles are preserved for each wrapped line.
|
|
32
|
-
|
|
33
|
-
## Focusable Interface (IME Support)
|
|
34
|
-
|
|
35
|
-
Components that display a text cursor and need IME (Input Method Editor) support should implement the `Focusable` interface:
|
|
36
|
-
|
|
37
|
-
```typescript
|
|
38
|
-
import { CURSOR_MARKER, type Component, type Focusable } from "@knightcode/tui";
|
|
39
|
-
|
|
40
|
-
class MyInput implements Component, Focusable {
|
|
41
|
-
focused: boolean = false; // Set by TUI when focus changes
|
|
42
|
-
|
|
43
|
-
render(width: number): string[] {
|
|
44
|
-
const marker = this.focused ? CURSOR_MARKER : "";
|
|
45
|
-
// Emit marker right before the fake cursor
|
|
46
|
-
return [`> ${beforeCursor}${marker}\x1b[7m${atCursor}\x1b[27m${afterCursor}`];
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
```
|
|
50
|
-
|
|
51
|
-
When a `Focusable` component has focus, TUI:
|
|
52
|
-
1. Sets `focused = true` on the component
|
|
53
|
-
2. Scans rendered output for `CURSOR_MARKER` (a zero-width APC escape sequence)
|
|
54
|
-
3. Positions the hardware terminal cursor at that location
|
|
55
|
-
4. Shows the hardware cursor only when `showHardwareCursor` is enabled
|
|
56
|
-
|
|
57
|
-
The cursor remains hidden by default. This keeps the fake cursor rendering, while still positioning the hardware cursor for terminals that track IME candidate windows with hidden cursors. Some terminals require a visible hardware cursor for IME positioning; enable it with the renderer's `showHardwareCursor` constructor argument or `setShowHardwareCursor(true)`. KnightCode also maps `KNIGHTCODE_HARDWARE_CURSOR=1` to this setting before it creates its renderer. The `Editor` and `Input` built-in components already implement this interface.
|
|
58
|
-
|
|
59
|
-
### Container Components with Embedded Inputs
|
|
60
|
-
|
|
61
|
-
When a container component (dialog, selector, etc.) contains an `Input` or `Editor` child, the container must implement `Focusable` and propagate the focus state to the child. Otherwise, the hardware cursor won't be positioned correctly for IME input.
|
|
62
|
-
|
|
63
|
-
```typescript
|
|
64
|
-
import { Container, type Focusable, Input } from "@knightcode/tui";
|
|
65
|
-
|
|
66
|
-
class SearchDialog extends Container implements Focusable {
|
|
67
|
-
private searchInput: Input;
|
|
68
|
-
|
|
69
|
-
// Focusable implementation - propagate to child input for IME cursor positioning
|
|
70
|
-
private _focused = false;
|
|
71
|
-
get focused(): boolean {
|
|
72
|
-
return this._focused;
|
|
73
|
-
}
|
|
74
|
-
set focused(value: boolean) {
|
|
75
|
-
this._focused = value;
|
|
76
|
-
this.searchInput.focused = value;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
constructor() {
|
|
80
|
-
super();
|
|
81
|
-
this.searchInput = new Input();
|
|
82
|
-
this.addChild(this.searchInput);
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
```
|
|
86
|
-
|
|
87
|
-
Without this propagation, typing with an IME (Chinese, Japanese, Korean, etc.) will show the candidate window in the wrong position on screen.
|
|
88
|
-
|
|
89
|
-
## Using Components
|
|
90
|
-
|
|
91
|
-
**In extensions** via `ctx.ui.custom()`:
|
|
92
|
-
|
|
93
|
-
```typescript
|
|
94
|
-
knightcode.on("session_start", async (_event, ctx) => {
|
|
95
|
-
const result = await ctx.ui.custom<string | null>((tui, theme, keybindings, done) =>
|
|
96
|
-
new MyComponent({
|
|
97
|
-
theme,
|
|
98
|
-
keybindings,
|
|
99
|
-
onChange: () => tui.requestRender(),
|
|
100
|
-
onSelect: (value) => done(value),
|
|
101
|
-
onCancel: () => done(null),
|
|
102
|
-
})
|
|
103
|
-
);
|
|
104
|
-
});
|
|
105
|
-
```
|
|
106
|
-
|
|
107
|
-
**In custom tools** via `ctx.ui.custom()`:
|
|
108
|
-
|
|
109
|
-
```typescript
|
|
110
|
-
async execute(toolCallId, params, signal, onUpdate, ctx) {
|
|
111
|
-
const result = await ctx.ui.custom<string | null>((tui, theme, keybindings, done) =>
|
|
112
|
-
new MyComponent({
|
|
113
|
-
theme,
|
|
114
|
-
keybindings,
|
|
115
|
-
onChange: () => tui.requestRender(),
|
|
116
|
-
onSelect: (value) => done(value),
|
|
117
|
-
onCancel: () => done(null),
|
|
118
|
-
})
|
|
119
|
-
);
|
|
120
|
-
// Use result...
|
|
121
|
-
}
|
|
122
|
-
```
|
|
123
|
-
|
|
124
|
-
## Overlays
|
|
125
|
-
|
|
126
|
-
Overlays render components on top of existing content without clearing the screen. Pass `{ overlay: true }` to `ctx.ui.custom()`:
|
|
127
|
-
|
|
128
|
-
```typescript
|
|
129
|
-
const result = await ctx.ui.custom<string | null>(
|
|
130
|
-
(tui, theme, keybindings, done) => new MyDialog({ onClose: done }),
|
|
131
|
-
{ overlay: true }
|
|
132
|
-
);
|
|
133
|
-
```
|
|
134
|
-
|
|
135
|
-
For positioning and sizing, use `overlayOptions`:
|
|
136
|
-
|
|
137
|
-
```typescript
|
|
138
|
-
const result = await ctx.ui.custom<string | null>(
|
|
139
|
-
(tui, theme, keybindings, done) => new SidePanel({ onClose: done }),
|
|
140
|
-
{
|
|
141
|
-
overlay: true,
|
|
142
|
-
overlayOptions: {
|
|
143
|
-
// Size: number or percentage string
|
|
144
|
-
width: "50%", // 50% of terminal width
|
|
145
|
-
minWidth: 40, // minimum 40 columns
|
|
146
|
-
maxHeight: "80%", // max 80% of terminal height
|
|
147
|
-
|
|
148
|
-
// Position: anchor-based (default: "center")
|
|
149
|
-
anchor: "right-center", // 9 positions: center, top-left, top-center, etc.
|
|
150
|
-
offsetX: -2, // offset from anchor
|
|
151
|
-
offsetY: 0,
|
|
152
|
-
|
|
153
|
-
// Or percentage/absolute positioning
|
|
154
|
-
row: "25%", // 25% from top
|
|
155
|
-
col: 10, // column 10
|
|
156
|
-
|
|
157
|
-
// Margins
|
|
158
|
-
margin: 2, // all sides, or { top, right, bottom, left }
|
|
159
|
-
|
|
160
|
-
// Responsive: hide on narrow terminals
|
|
161
|
-
visible: (termWidth, termHeight) => termWidth >= 80,
|
|
162
|
-
},
|
|
163
|
-
// Get handle for programmatic focus and visibility control
|
|
164
|
-
onHandle: (handle) => {
|
|
165
|
-
// handle.focus() - focus this overlay and bring it to the visual front
|
|
166
|
-
// handle.unfocus() - release input to normal fallback
|
|
167
|
-
// handle.unfocus({ target }) - release input to a specific component or null
|
|
168
|
-
// handle.setHidden(true/false) - toggle visibility
|
|
169
|
-
// handle.hide() - permanently remove
|
|
170
|
-
},
|
|
171
|
-
}
|
|
172
|
-
);
|
|
173
|
-
```
|
|
174
|
-
|
|
175
|
-
### Overlay Focus
|
|
176
|
-
|
|
177
|
-
A focused visible overlay keeps input ownership across temporary non-overlay UI. If an overlay opens another `ctx.ui.custom()` component without `{ overlay: true }`, that replacement UI receives input while it is active; when it closes, the focused overlay can reclaim input.
|
|
178
|
-
|
|
179
|
-
Use `handle.unfocus()` when a visible overlay should stop owning input and let TUI fall back to another visible capturing overlay or the previous focus target. Use `handle.unfocus({ target })` when a specific component should receive input while the overlay stays visible. Passing `{ target: null }` intentionally leaves no focused component until focus is set again.
|
|
180
|
-
|
|
181
|
-
### Overlay Lifecycle
|
|
182
|
-
|
|
183
|
-
Overlay components are disposed when closed. Don't reuse references - create fresh instances:
|
|
184
|
-
|
|
185
|
-
```typescript
|
|
186
|
-
// Wrong - stale reference
|
|
187
|
-
let menu: MenuComponent;
|
|
188
|
-
await ctx.ui.custom((_, __, ___, done) => {
|
|
189
|
-
menu = new MenuComponent(done);
|
|
190
|
-
return menu;
|
|
191
|
-
}, { overlay: true });
|
|
192
|
-
setActiveComponent(menu); // Disposed
|
|
193
|
-
|
|
194
|
-
// Correct - re-call to re-show
|
|
195
|
-
const showMenu = () => ctx.ui.custom((_, __, ___, done) =>
|
|
196
|
-
new MenuComponent(done), { overlay: true });
|
|
197
|
-
|
|
198
|
-
await showMenu(); // First show
|
|
199
|
-
await showMenu(); // "Back" = just call again
|
|
200
|
-
```
|
|
201
|
-
|
|
202
|
-
See [overlay-qa-tests.ts](../examples/extensions/overlay-qa-tests.ts) for comprehensive examples covering anchors, margins, stacking, responsive visibility, and animation.
|
|
203
|
-
|
|
204
|
-
## Built-in Components
|
|
205
|
-
|
|
206
|
-
Import from `@knightcode/tui`:
|
|
207
|
-
|
|
208
|
-
```typescript
|
|
209
|
-
import { Text, Box, Container, Spacer, Markdown } from "@knightcode/tui";
|
|
210
|
-
```
|
|
211
|
-
|
|
212
|
-
### Text
|
|
213
|
-
|
|
214
|
-
Multi-line text with word wrapping.
|
|
215
|
-
|
|
216
|
-
```typescript
|
|
217
|
-
const text = new Text(
|
|
218
|
-
"Hello World", // content
|
|
219
|
-
1, // paddingX (default: 1)
|
|
220
|
-
1, // paddingY (default: 1)
|
|
221
|
-
(s) => bgGray(s) // optional background function
|
|
222
|
-
);
|
|
223
|
-
text.setText("Updated");
|
|
224
|
-
```
|
|
225
|
-
|
|
226
|
-
### Box
|
|
227
|
-
|
|
228
|
-
Container with padding and background color.
|
|
229
|
-
|
|
230
|
-
```typescript
|
|
231
|
-
const box = new Box(
|
|
232
|
-
1, // paddingX
|
|
233
|
-
1, // paddingY
|
|
234
|
-
(s) => bgGray(s) // background function
|
|
235
|
-
);
|
|
236
|
-
box.addChild(new Text("Content", 0, 0));
|
|
237
|
-
box.setBgFn((s) => bgBlue(s));
|
|
238
|
-
```
|
|
239
|
-
|
|
240
|
-
### Container
|
|
241
|
-
|
|
242
|
-
Groups child components vertically.
|
|
243
|
-
|
|
244
|
-
```typescript
|
|
245
|
-
const container = new Container();
|
|
246
|
-
container.addChild(component1);
|
|
247
|
-
container.addChild(component2);
|
|
248
|
-
container.removeChild(component1);
|
|
249
|
-
```
|
|
1
|
+
# Terminal UI
|
|
250
2
|
|
|
251
|
-
|
|
3
|
+
`@knightcode/tui` provides the terminal component system used by KnightCode. Extensions use it when built-in dialogs, notifications, status text, and widgets are not enough for the interaction they need.
|
|
252
4
|
|
|
253
|
-
|
|
5
|
+
Start with `ctx.ui` methods from an [extension](extensions.md#interact-with-the-user). Build a custom component only when the UI needs its own rendering, keyboard or mouse input, focus, layout, or lifecycle.
|
|
254
6
|
|
|
255
|
-
|
|
256
|
-
const spacer = new Spacer(2); // 2 empty lines
|
|
257
|
-
```
|
|
7
|
+
## Choose an integration point
|
|
258
8
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
1, // paddingY
|
|
268
|
-
theme // MarkdownTheme (see below)
|
|
269
|
-
);
|
|
270
|
-
md.setText("Updated markdown");
|
|
271
|
-
```
|
|
9
|
+
| Need | Use |
|
|
10
|
+
|---|---|
|
|
11
|
+
| Select, confirm, input, or multi-line editor | `ctx.ui.select()`, `confirm()`, `input()`, or `editor()` |
|
|
12
|
+
| Non-blocking feedback | `ctx.ui.notify()` or `setStatus()` |
|
|
13
|
+
| Persistent content near the editor | `ctx.ui.setWidget()` |
|
|
14
|
+
| Replace the header, footer, or editor | The corresponding `ctx.ui` component factory |
|
|
15
|
+
| Temporary interactive screen or overlay | `ctx.ui.custom()` |
|
|
16
|
+
| Custom rendering for a tool or session entry | An extension renderer |
|
|
272
17
|
|
|
273
|
-
|
|
18
|
+
These APIs receive KnightCode’s active theme and keybindings where needed. Do not create a second terminal renderer inside an extension.
|
|
274
19
|
|
|
275
|
-
|
|
20
|
+
## Understand the component model
|
|
276
21
|
|
|
277
|
-
|
|
278
|
-
const image = new Image(
|
|
279
|
-
base64Data, // base64-encoded image
|
|
280
|
-
"image/png", // MIME type
|
|
281
|
-
theme, // ImageTheme
|
|
282
|
-
{ maxWidthCells: 80, maxHeightCells: 24 }
|
|
283
|
-
);
|
|
284
|
-
```
|
|
285
|
-
|
|
286
|
-
## Keyboard Input
|
|
287
|
-
|
|
288
|
-
Use `matchesKey()` for key detection:
|
|
289
|
-
|
|
290
|
-
```typescript
|
|
291
|
-
import { matchesKey, Key } from "@knightcode/tui";
|
|
292
|
-
|
|
293
|
-
handleInput(data: string) {
|
|
294
|
-
if (matchesKey(data, Key.up)) {
|
|
295
|
-
this.selectedIndex--;
|
|
296
|
-
} else if (matchesKey(data, Key.enter)) {
|
|
297
|
-
this.onSelect?.(this.selectedIndex);
|
|
298
|
-
} else if (matchesKey(data, Key.escape)) {
|
|
299
|
-
this.onCancel?.();
|
|
300
|
-
} else if (matchesKey(data, Key.ctrl("c"))) {
|
|
301
|
-
// Ctrl+C
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
|
-
```
|
|
305
|
-
|
|
306
|
-
**Key identifiers** (use `Key.*` for autocomplete, or string literals):
|
|
307
|
-
- Basic keys: `Key.enter`, `Key.escape`, `Key.tab`, `Key.space`, `Key.backspace`, `Key.delete`, `Key.home`, `Key.end`
|
|
308
|
-
- Arrow keys: `Key.up`, `Key.down`, `Key.left`, `Key.right`
|
|
309
|
-
- With modifiers: `Key.ctrl("c")`, `Key.shift("tab")`, `Key.alt("left")`, `Key.ctrlShift("p")`
|
|
310
|
-
- String format also works: `"enter"`, `"ctrl+c"`, `"shift+tab"`, `"ctrl+shift+p"`
|
|
311
|
-
|
|
312
|
-
## Mouse Input
|
|
313
|
-
|
|
314
|
-
Fullscreen mode routes normalized press, release, click, move, drag, and wheel events to components and overlays. Return `{ handled: true }` to suppress default behavior, `capture: true` to retain drag/release ownership, `focus: true` to request keyboard focus, and `render: true` when a hover or release visibly changes the component. Press, click, drag, and wheel render by default; no-op move/release events do not.
|
|
315
|
-
|
|
316
|
-
```typescript
|
|
317
|
-
import { MouseRegion } from "@knightcode/tui";
|
|
318
|
-
|
|
319
|
-
const clickable = new MouseRegion(content, (event) => {
|
|
320
|
-
if (event.type !== "click" || event.button !== "left") return undefined;
|
|
321
|
-
expanded = !expanded;
|
|
322
|
-
return { handled: true };
|
|
323
|
-
});
|
|
324
|
-
```
|
|
325
|
-
|
|
326
|
-
Unhandled wheel input scrolls the nearest `ScrollView`; unhandled primary-button drags retain transcript selection. OSC 8 links take precedence over parent click regions. `Input`, `Editor`, `SelectList`, and `SettingsList` include fullscreen mouse behavior. Regular mode does not capture mouse input because the terminal owns its scrollback.
|
|
327
|
-
|
|
328
|
-
## Line Width
|
|
329
|
-
|
|
330
|
-
**Critical:** Each line from `render()` must not exceed the `width` parameter.
|
|
331
|
-
|
|
332
|
-
```typescript
|
|
333
|
-
import { visibleWidth, truncateToWidth } from "@knightcode/tui";
|
|
334
|
-
|
|
335
|
-
render(width: number): string[] {
|
|
336
|
-
// Truncate long lines
|
|
337
|
-
return [truncateToWidth(this.text, width)];
|
|
338
|
-
}
|
|
339
|
-
```
|
|
340
|
-
|
|
341
|
-
Utilities:
|
|
342
|
-
- `visibleWidth(str)` - Get display width (ignores ANSI codes)
|
|
343
|
-
- `truncateToWidth(str, width, ellipsis?)` - Truncate with optional ellipsis
|
|
344
|
-
- `wrapTextWithAnsi(str, width)` - Word wrap preserving ANSI codes
|
|
345
|
-
|
|
346
|
-
## Creating Custom Components
|
|
347
|
-
|
|
348
|
-
Example: Interactive selector
|
|
349
|
-
|
|
350
|
-
```typescript
|
|
351
|
-
import {
|
|
352
|
-
matchesKey, Key,
|
|
353
|
-
truncateToWidth, visibleWidth
|
|
354
|
-
} from "@knightcode/tui";
|
|
355
|
-
|
|
356
|
-
class MySelector {
|
|
357
|
-
private items: string[];
|
|
358
|
-
private selected = 0;
|
|
359
|
-
private cachedWidth?: number;
|
|
360
|
-
private cachedLines?: string[];
|
|
361
|
-
|
|
362
|
-
public onSelect?: (item: string) => void;
|
|
363
|
-
public onCancel?: () => void;
|
|
364
|
-
|
|
365
|
-
constructor(items: string[]) {
|
|
366
|
-
this.items = items;
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
handleInput(data: string): void {
|
|
370
|
-
if (matchesKey(data, Key.up) && this.selected > 0) {
|
|
371
|
-
this.selected--;
|
|
372
|
-
this.invalidate();
|
|
373
|
-
} else if (matchesKey(data, Key.down) && this.selected < this.items.length - 1) {
|
|
374
|
-
this.selected++;
|
|
375
|
-
this.invalidate();
|
|
376
|
-
} else if (matchesKey(data, Key.enter)) {
|
|
377
|
-
this.onSelect?.(this.items[this.selected]);
|
|
378
|
-
} else if (matchesKey(data, Key.escape)) {
|
|
379
|
-
this.onCancel?.();
|
|
380
|
-
}
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
render(width: number): string[] {
|
|
384
|
-
if (this.cachedLines && this.cachedWidth === width) {
|
|
385
|
-
return this.cachedLines;
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
this.cachedLines = this.items.map((item, i) => {
|
|
389
|
-
const prefix = i === this.selected ? "> " : " ";
|
|
390
|
-
return truncateToWidth(prefix + item, width);
|
|
391
|
-
});
|
|
392
|
-
this.cachedWidth = width;
|
|
393
|
-
return this.cachedLines;
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
invalidate(): void {
|
|
397
|
-
this.cachedWidth = undefined;
|
|
398
|
-
this.cachedLines = undefined;
|
|
399
|
-
}
|
|
400
|
-
}
|
|
401
|
-
```
|
|
402
|
-
|
|
403
|
-
Usage in an extension:
|
|
404
|
-
|
|
405
|
-
```typescript
|
|
406
|
-
knightcode.registerCommand("pick", {
|
|
407
|
-
description: "Pick an item",
|
|
408
|
-
handler: async (_args, ctx) => {
|
|
409
|
-
const items = ["Option A", "Option B", "Option C"];
|
|
410
|
-
const selected = await ctx.ui.custom<string | null>((tui, _theme, _keybindings, done) => {
|
|
411
|
-
const selector = new MySelector(items);
|
|
412
|
-
selector.onSelect = done;
|
|
413
|
-
selector.onCancel = () => done(null);
|
|
414
|
-
|
|
415
|
-
return {
|
|
416
|
-
render: (width) => selector.render(width),
|
|
417
|
-
handleInput: (data) => {
|
|
418
|
-
selector.handleInput(data);
|
|
419
|
-
tui.requestRender();
|
|
420
|
-
},
|
|
421
|
-
invalidate: () => selector.invalidate(),
|
|
422
|
-
};
|
|
423
|
-
});
|
|
424
|
-
|
|
425
|
-
if (selected !== null) {
|
|
426
|
-
ctx.ui.notify(`Selected: ${selected}`, "info");
|
|
427
|
-
}
|
|
428
|
-
}
|
|
429
|
-
});
|
|
430
|
-
```
|
|
431
|
-
|
|
432
|
-
## Theming
|
|
433
|
-
|
|
434
|
-
Components accept theme objects for styling.
|
|
435
|
-
|
|
436
|
-
**In `renderCall`/`renderResult`**, use the `theme` parameter:
|
|
437
|
-
|
|
438
|
-
```typescript
|
|
439
|
-
renderResult(result, options, theme, context) {
|
|
440
|
-
// Use theme.fg() for foreground colors
|
|
441
|
-
return new Text(theme.fg("success", "Done!"), 0, 0);
|
|
442
|
-
|
|
443
|
-
// Use theme.bg() for background colors
|
|
444
|
-
const styled = theme.bg("toolPendingBg", theme.fg("accent", "text"));
|
|
445
|
-
}
|
|
446
|
-
```
|
|
447
|
-
|
|
448
|
-
**Foreground colors** (`theme.fg(color, text)`):
|
|
449
|
-
|
|
450
|
-
| Category | Colors |
|
|
451
|
-
|----------|--------|
|
|
452
|
-
| General | `text`, `accent`, `muted`, `dim`, `searchMatchText` |
|
|
453
|
-
| Status | `success`, `error`, `warning` |
|
|
454
|
-
| Borders | `border`, `borderAccent`, `borderMuted` |
|
|
455
|
-
| Messages | `userMessageText`, `customMessageText`, `customMessageLabel` |
|
|
456
|
-
| Tools | `toolTitle`, `toolOutput` |
|
|
457
|
-
| Diffs | `toolDiffAdded`, `toolDiffRemoved`, `toolDiffContext` |
|
|
458
|
-
| Markdown | `mdHeading`, `mdLink`, `mdLinkUrl`, `mdCode`, `mdCodeBlock`, `mdCodeBlockBorder`, `mdQuote`, `mdQuoteBorder`, `mdHr`, `mdListBullet` |
|
|
459
|
-
| Syntax | `syntaxComment`, `syntaxKeyword`, `syntaxFunction`, `syntaxVariable`, `syntaxString`, `syntaxNumber`, `syntaxType`, `syntaxOperator`, `syntaxPunctuation` |
|
|
460
|
-
| Thinking | `thinkingOff`, `thinkingMinimal`, `thinkingLow`, `thinkingMedium`, `thinkingHigh`, `thinkingXhigh`, `thinkingMax` |
|
|
461
|
-
| Modes | `bashMode` |
|
|
462
|
-
|
|
463
|
-
**Background colors** (`theme.bg(color, text)`):
|
|
464
|
-
|
|
465
|
-
`selectedBg`, `searchMatchBg`, `userMessageBg`, `customMessageBg`, `toolPendingBg`, `toolSuccessBg`, `toolErrorBg`
|
|
466
|
-
|
|
467
|
-
**For Markdown**, use `getMarkdownTheme()`:
|
|
468
|
-
|
|
469
|
-
```typescript
|
|
470
|
-
import { getMarkdownTheme } from "@knightcodeai/cli";
|
|
471
|
-
import { Markdown } from "@knightcode/tui";
|
|
472
|
-
|
|
473
|
-
renderResult(result, options, theme, context) {
|
|
474
|
-
const mdTheme = getMarkdownTheme();
|
|
475
|
-
return new Markdown(result.details.markdown, 0, 0, mdTheme);
|
|
476
|
-
}
|
|
477
|
-
```
|
|
478
|
-
|
|
479
|
-
**For custom components**, define your own theme interface:
|
|
480
|
-
|
|
481
|
-
```typescript
|
|
482
|
-
interface MyTheme {
|
|
483
|
-
selected: (s: string) => string;
|
|
484
|
-
normal: (s: string) => string;
|
|
485
|
-
}
|
|
486
|
-
```
|
|
487
|
-
|
|
488
|
-
## Debug logging
|
|
489
|
-
|
|
490
|
-
Set `KNIGHTCODE_TUI_WRITE_LOG` to capture the raw ANSI stream written to stdout.
|
|
491
|
-
|
|
492
|
-
```bash
|
|
493
|
-
KNIGHTCODE_TUI_WRITE_LOG=/tmp/tui-ansi.log npx tsx packages/tui/test/chat-simple.ts
|
|
494
|
-
```
|
|
495
|
-
|
|
496
|
-
## Performance
|
|
497
|
-
|
|
498
|
-
Cache rendered output when possible:
|
|
499
|
-
|
|
500
|
-
```typescript
|
|
501
|
-
class CachedComponent {
|
|
502
|
-
private cachedWidth?: number;
|
|
503
|
-
private cachedLines?: string[];
|
|
504
|
-
|
|
505
|
-
render(width: number): string[] {
|
|
506
|
-
if (this.cachedLines && this.cachedWidth === width) {
|
|
507
|
-
return this.cachedLines;
|
|
508
|
-
}
|
|
509
|
-
// ... compute lines ...
|
|
510
|
-
this.cachedWidth = width;
|
|
511
|
-
this.cachedLines = lines;
|
|
512
|
-
return lines;
|
|
513
|
-
}
|
|
514
|
-
|
|
515
|
-
invalidate(): void {
|
|
516
|
-
this.cachedWidth = undefined;
|
|
517
|
-
this.cachedLines = undefined;
|
|
518
|
-
}
|
|
519
|
-
}
|
|
520
|
-
```
|
|
521
|
-
|
|
522
|
-
Call `invalidate()` when state changes, then use the injected `tui.requestRender()` to trigger re-render.
|
|
523
|
-
|
|
524
|
-
## Invalidation and Theme Changes
|
|
525
|
-
|
|
526
|
-
When the theme changes, the TUI calls `invalidate()` on all components to clear their caches. Components must properly implement `invalidate()` to ensure theme changes take effect.
|
|
22
|
+
A component renders an array of terminal lines for an available width. It can optionally handle keyboard and mouse input, and it must invalidate cached output when its state or theme-dependent content changes.
|
|
527
23
|
|
|
528
|
-
|
|
24
|
+
Every rendered line must fit within the supplied width. Measure visible terminal columns rather than string length because ANSI escapes, wide characters, emoji, and combining characters change display width.
|
|
529
25
|
|
|
530
|
-
|
|
26
|
+
Use `visibleWidth()`, `truncateToWidth()`, `sliceByColumn()`, and `wrapTextWithAnsi()` instead of implementing terminal-width handling yourself. KnightCode resets styling and hyperlinks after every line, so reapply styles on each rendered line.
|
|
531
27
|
|
|
532
|
-
|
|
28
|
+
After changing component state, invalidate the affected component and call the injected `tui.requestRender()`. The TUI coalesces render requests and updates the terminal.
|
|
533
29
|
|
|
534
|
-
|
|
535
|
-
class BadComponent extends Container {
|
|
536
|
-
private content: Text;
|
|
30
|
+
## Compose built-in components
|
|
537
31
|
|
|
538
|
-
|
|
539
|
-
super();
|
|
540
|
-
// Pre-baked theme colors stored in Text component
|
|
541
|
-
this.content = new Text(theme.fg("accent", message), 1, 0);
|
|
542
|
-
this.addChild(this.content);
|
|
543
|
-
}
|
|
544
|
-
// No invalidate override - parent's invalidate only clears
|
|
545
|
-
// child render caches, not the pre-baked content
|
|
546
|
-
}
|
|
547
|
-
```
|
|
32
|
+
The package includes components for common layouts and controls:
|
|
548
33
|
|
|
549
|
-
|
|
34
|
+
- `Text`, `Markdown`, `Image`, and `TruncatedText` render content.
|
|
35
|
+
- `Container`, `VStack`, `HStack`, `Box`, and `Spacer` compose layouts.
|
|
36
|
+
- `Input` and `Editor` accept text.
|
|
37
|
+
- `SelectList` and `SettingsList` implement searchable selection and settings flows.
|
|
38
|
+
- `ScrollView` provides a bounded scrollable viewport.
|
|
39
|
+
- `Loader` and `CancellableLoader` report ongoing work.
|
|
40
|
+
- `MouseRegion` adds pointer behavior around another component.
|
|
550
41
|
|
|
551
|
-
|
|
42
|
+
Prefer these components over rebuilding selection, scrolling, text editing, or width handling. The extension examples show how to combine them with KnightCode’s borders and themes.
|
|
552
43
|
|
|
553
|
-
|
|
554
|
-
class GoodComponent extends Container {
|
|
555
|
-
private message: string;
|
|
556
|
-
private content: Text;
|
|
44
|
+
## Handle keyboard input and focus
|
|
557
45
|
|
|
558
|
-
|
|
559
|
-
super();
|
|
560
|
-
this.message = message;
|
|
561
|
-
this.content = new Text("", 1, 0);
|
|
562
|
-
this.addChild(this.content);
|
|
563
|
-
this.updateDisplay();
|
|
564
|
-
}
|
|
46
|
+
Use `matchesKey()` and `Key` for terminal keyboard input. The parser accounts for supported terminal protocols and key modifiers. Extension components should use the injected `KeybindingsManager` for configurable application actions.
|
|
565
47
|
|
|
566
|
-
|
|
567
|
-
// Rebuild content with current theme
|
|
568
|
-
this.content.setText(theme.fg("accent", this.message));
|
|
569
|
-
}
|
|
48
|
+
A component that displays a text cursor should implement `Focusable` and place `CURSOR_MARKER` immediately before its visual cursor. The TUI uses that marker to position the hardware cursor for input method editors.
|
|
570
49
|
|
|
571
|
-
|
|
572
|
-
super.invalidate(); // Clear child caches
|
|
573
|
-
this.updateDisplay(); // Rebuild with new theme
|
|
574
|
-
}
|
|
575
|
-
}
|
|
576
|
-
```
|
|
50
|
+
Containers that wrap an `Input` or `Editor` must propagate their `focused` state to that child. Without propagation, Chinese, Japanese, Korean, and other IME candidate windows can appear at the wrong screen position.
|
|
577
51
|
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
For components with complex content:
|
|
581
|
-
|
|
582
|
-
```typescript
|
|
583
|
-
class ComplexComponent extends Container {
|
|
584
|
-
private data: SomeData;
|
|
585
|
-
|
|
586
|
-
constructor(data: SomeData) {
|
|
587
|
-
super();
|
|
588
|
-
this.data = data;
|
|
589
|
-
this.rebuild();
|
|
590
|
-
}
|
|
52
|
+
Extend KnightCode’s `CustomEditor` when replacing the main editor. It preserves application shortcuts and agent controls.
|
|
591
53
|
|
|
592
|
-
|
|
593
|
-
this.clear(); // Remove all children
|
|
54
|
+
Forward keys your editor does not own to the base implementation, and restore the default by clearing the custom editor factory.
|
|
594
55
|
|
|
595
|
-
|
|
596
|
-
this.addChild(new Text(theme.fg("accent", theme.bold("Title")), 1, 0));
|
|
597
|
-
this.addChild(new Spacer(1));
|
|
598
|
-
|
|
599
|
-
for (const item of this.data.items) {
|
|
600
|
-
const color = item.active ? "success" : "muted";
|
|
601
|
-
this.addChild(new Text(theme.fg(color, item.label), 1, 0));
|
|
602
|
-
}
|
|
603
|
-
}
|
|
604
|
-
|
|
605
|
-
override invalidate(): void {
|
|
606
|
-
super.invalidate();
|
|
607
|
-
this.rebuild();
|
|
608
|
-
}
|
|
609
|
-
}
|
|
610
|
-
```
|
|
56
|
+
## Handle mouse input
|
|
611
57
|
|
|
612
|
-
|
|
58
|
+
Fullscreen mode routes normalized mouse events to components. A handler can mark an event handled, capture a drag sequence, request focus, or request a render.
|
|
613
59
|
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
1. **Pre-baking theme colors** - Using `theme.fg()` or `theme.bg()` to create styled strings stored in child components
|
|
617
|
-
2. **Syntax highlighting** - Using `highlightCode()` which applies theme-based syntax colors
|
|
618
|
-
3. **Complex layouts** - Building child component trees that embed theme colors
|
|
60
|
+
Unhandled wheel events scroll the nearest `ScrollView`. Unhandled primary-button drags remain available for transcript selection. OSC 8 links take precedence over enclosing click regions.
|
|
619
61
|
|
|
620
|
-
|
|
62
|
+
Regular mode leaves mouse input to the terminal because the terminal owns scrollback. Design every interaction with a keyboard path even when fullscreen mouse input is available.
|
|
621
63
|
|
|
622
|
-
|
|
623
|
-
2. **Simple containers** - Just grouping other components without adding themed content
|
|
624
|
-
3. **Stateless render** - Computing themed output fresh in every `render()` call (no caching)
|
|
64
|
+
## Use custom screens and overlays
|
|
625
65
|
|
|
626
|
-
|
|
66
|
+
`ctx.ui.custom()` temporarily gives one component control of the interactive area and resolves when that component calls the supplied completion callback.
|
|
627
67
|
|
|
628
|
-
|
|
68
|
+
Pass `overlay: true` to draw above existing content. Overlay options control size, anchors, offsets, margins, and responsive visibility. An overlay handle can change focus or temporarily hide and show the overlay with `setHidden()` while the interaction remains active.
|
|
629
69
|
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
For letting users pick from a list of options. Use `SelectList` from `@knightcode/tui` with `DynamicBorder` for framing.
|
|
633
|
-
|
|
634
|
-
```typescript
|
|
635
|
-
import type { ExtensionAPI } from "@knightcodeai/cli";
|
|
636
|
-
import { DynamicBorder } from "@knightcodeai/cli";
|
|
637
|
-
import { Container, type SelectItem, SelectList, Text } from "@knightcode/tui";
|
|
638
|
-
|
|
639
|
-
knightcode.registerCommand("pick", {
|
|
640
|
-
handler: async (_args, ctx) => {
|
|
641
|
-
const items: SelectItem[] = [
|
|
642
|
-
{ value: "opt1", label: "Option 1", description: "First option" },
|
|
643
|
-
{ value: "opt2", label: "Option 2", description: "Second option" },
|
|
644
|
-
{ value: "opt3", label: "Option 3" }, // description is optional
|
|
645
|
-
];
|
|
646
|
-
|
|
647
|
-
const result = await ctx.ui.custom<string | null>((tui, theme, _kb, done) => {
|
|
648
|
-
const container = new Container();
|
|
649
|
-
|
|
650
|
-
// Top border
|
|
651
|
-
container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)));
|
|
652
|
-
|
|
653
|
-
// Title
|
|
654
|
-
container.addChild(new Text(theme.fg("accent", theme.bold("Pick an Option")), 1, 0));
|
|
655
|
-
|
|
656
|
-
// SelectList with theme
|
|
657
|
-
const selectList = new SelectList(items, Math.min(items.length, 10), {
|
|
658
|
-
selectedPrefix: (t) => theme.fg("accent", t),
|
|
659
|
-
selectedText: (t) => theme.fg("accent", t),
|
|
660
|
-
description: (t) => theme.fg("muted", t),
|
|
661
|
-
scrollInfo: (t) => theme.fg("dim", t),
|
|
662
|
-
noMatch: (t) => theme.fg("warning", t),
|
|
663
|
-
});
|
|
664
|
-
selectList.onSelect = (item) => done(item.value);
|
|
665
|
-
selectList.onCancel = () => done(null);
|
|
666
|
-
container.addChild(selectList);
|
|
667
|
-
|
|
668
|
-
// Help text
|
|
669
|
-
container.addChild(new Text(theme.fg("dim", "↑↓ navigate • enter select • esc cancel"), 1, 0));
|
|
670
|
-
|
|
671
|
-
// Bottom border
|
|
672
|
-
container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)));
|
|
673
|
-
|
|
674
|
-
return {
|
|
675
|
-
render: (w) => container.render(w),
|
|
676
|
-
invalidate: () => container.invalidate(),
|
|
677
|
-
handleInput: (data) => { selectList.handleInput(data); tui.requestRender(); },
|
|
678
|
-
};
|
|
679
|
-
});
|
|
680
|
-
|
|
681
|
-
if (result) {
|
|
682
|
-
ctx.ui.notify(`Selected: ${result}`, "info");
|
|
683
|
-
}
|
|
684
|
-
},
|
|
685
|
-
});
|
|
686
|
-
```
|
|
687
|
-
|
|
688
|
-
**Examples:** [preset.ts](../examples/extensions/preset.ts), [tools.ts](../examples/extensions/tools.ts)
|
|
689
|
-
|
|
690
|
-
### Pattern 2: Async Operation with Cancel (BorderedLoader)
|
|
691
|
-
|
|
692
|
-
For operations that take time and should be cancellable. `BorderedLoader` shows a spinner and handles escape to cancel.
|
|
693
|
-
|
|
694
|
-
```typescript
|
|
695
|
-
import { BorderedLoader } from "@knightcodeai/cli";
|
|
696
|
-
|
|
697
|
-
knightcode.registerCommand("fetch", {
|
|
698
|
-
handler: async (_args, ctx) => {
|
|
699
|
-
const result = await ctx.ui.custom<string | null>((tui, theme, _kb, done) => {
|
|
700
|
-
const loader = new BorderedLoader(tui, theme, "Fetching data...");
|
|
701
|
-
loader.onAbort = () => done(null);
|
|
702
|
-
|
|
703
|
-
// Do async work
|
|
704
|
-
fetchData(loader.signal)
|
|
705
|
-
.then((data) => done(data))
|
|
706
|
-
.catch(() => done(null));
|
|
707
|
-
|
|
708
|
-
return loader;
|
|
709
|
-
});
|
|
70
|
+
Focused overlays retain input ownership across ordinary renders. If another component should receive input while an overlay remains visible, explicitly release or redirect focus through the handle.
|
|
710
71
|
|
|
711
|
-
|
|
712
|
-
ctx.ui.notify("Cancelled", "info");
|
|
713
|
-
} else {
|
|
714
|
-
ctx.ui.setEditorText(result);
|
|
715
|
-
}
|
|
716
|
-
},
|
|
717
|
-
});
|
|
718
|
-
```
|
|
72
|
+
Treat each custom component instance as belonging to one interaction. Create a new instance when starting that interaction again.
|
|
719
73
|
|
|
720
|
-
|
|
74
|
+
Finish the interaction with the completion callback supplied to the component factory. It resolves the `ctx.ui.custom()` promise and disposes the component. Do not call `OverlayHandle.hide()` on an overlay created by `ctx.ui.custom()`.
|
|
721
75
|
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
For toggling multiple settings. Use `SettingsList` from `@knightcode/tui` with `getSettingsListTheme()`.
|
|
725
|
-
|
|
726
|
-
```typescript
|
|
727
|
-
import { getSettingsListTheme } from "@knightcodeai/cli";
|
|
728
|
-
import { Container, type SettingItem, SettingsList, Text } from "@knightcode/tui";
|
|
729
|
-
|
|
730
|
-
knightcode.registerCommand("settings", {
|
|
731
|
-
handler: async (_args, ctx) => {
|
|
732
|
-
const items: SettingItem[] = [
|
|
733
|
-
{ id: "verbose", label: "Verbose mode", currentValue: "off", values: ["on", "off"] },
|
|
734
|
-
{ id: "color", label: "Color output", currentValue: "on", values: ["on", "off"] },
|
|
735
|
-
];
|
|
736
|
-
|
|
737
|
-
await ctx.ui.custom((_tui, theme, _kb, done) => {
|
|
738
|
-
const container = new Container();
|
|
739
|
-
container.addChild(new Text(theme.fg("accent", theme.bold("Settings")), 1, 1));
|
|
740
|
-
|
|
741
|
-
const settingsList = new SettingsList(
|
|
742
|
-
items,
|
|
743
|
-
Math.min(items.length + 2, 15),
|
|
744
|
-
getSettingsListTheme(),
|
|
745
|
-
(id, newValue) => {
|
|
746
|
-
// Handle value change
|
|
747
|
-
ctx.ui.notify(`${id} = ${newValue}`, "info");
|
|
748
|
-
},
|
|
749
|
-
() => done(undefined), // On close
|
|
750
|
-
{ enableSearch: true }, // Optional: enable fuzzy search by label
|
|
751
|
-
);
|
|
752
|
-
container.addChild(settingsList);
|
|
753
|
-
|
|
754
|
-
return {
|
|
755
|
-
render: (w) => container.render(w),
|
|
756
|
-
invalidate: () => container.invalidate(),
|
|
757
|
-
handleInput: (data) => settingsList.handleInput?.(data),
|
|
758
|
-
};
|
|
759
|
-
});
|
|
760
|
-
},
|
|
761
|
-
});
|
|
762
|
-
```
|
|
763
|
-
|
|
764
|
-
**Examples:** [tools.ts](../examples/extensions/tools.ts)
|
|
76
|
+
See [`overlay-qa-tests.ts`](../examples/extensions/overlay-qa-tests.ts) for positioning, stacking, focus, responsive visibility, and animation behavior.
|
|
765
77
|
|
|
766
|
-
|
|
78
|
+
## Apply themes correctly
|
|
767
79
|
|
|
768
|
-
|
|
80
|
+
Use the theme passed to the extension or component callback. Theme helpers produce ANSI-styled strings for semantic colors such as accent, muted text, success, warnings, errors, tool output, and Markdown.
|
|
769
81
|
|
|
770
|
-
|
|
771
|
-
// Set status (shown in footer)
|
|
772
|
-
ctx.ui.setStatus("my-ext", ctx.ui.theme.fg("accent", "● active"));
|
|
82
|
+
Do not permanently store strings with theme colors unless `invalidate()` rebuilds them. A theme change clears render caches, but it cannot remove old ANSI colors embedded in application state.
|
|
773
83
|
|
|
774
|
-
|
|
775
|
-
ctx.ui.setStatus("my-ext", undefined);
|
|
776
|
-
```
|
|
84
|
+
Theme callbacks evaluated during rendering do not need special rebuilding. Stateless components can also calculate themed output on every render.
|
|
777
85
|
|
|
778
|
-
|
|
86
|
+
Use [Themes](themes.md) to create terminal palettes. Use KnightCode’s `getMarkdownTheme()` when rendering Markdown that should match the active application theme.
|
|
779
87
|
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
Customize the inline working indicator shown while knightcode is streaming a response.
|
|
783
|
-
|
|
784
|
-
```typescript
|
|
785
|
-
// Static indicator
|
|
786
|
-
ctx.ui.setWorkingIndicator({ frames: [ctx.ui.theme.fg("accent", "●")] });
|
|
88
|
+
## Keep rendering responsive
|
|
787
89
|
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
This only affects the normal streaming working indicator. Compaction and retry loaders keep their built-in styling. Custom frames are rendered verbatim, so extensions must add their own colors when needed.
|
|
807
|
-
|
|
808
|
-
**Examples:** [working-indicator.ts](../examples/extensions/working-indicator.ts)
|
|
809
|
-
|
|
810
|
-
### Pattern 5: Widgets Above/Below Editor
|
|
811
|
-
|
|
812
|
-
Show persistent content above or below the input editor. Good for todo lists, progress.
|
|
813
|
-
|
|
814
|
-
```typescript
|
|
815
|
-
// Simple string array (above editor by default)
|
|
816
|
-
ctx.ui.setWidget("my-widget", ["Line 1", "Line 2"]);
|
|
817
|
-
|
|
818
|
-
// Render below the editor
|
|
819
|
-
ctx.ui.setWidget("my-widget", ["Line 1", "Line 2"], { placement: "belowEditor" });
|
|
820
|
-
|
|
821
|
-
// Or with theme
|
|
822
|
-
ctx.ui.setWidget("my-widget", (_tui, theme) => {
|
|
823
|
-
const lines = items.map((item, i) =>
|
|
824
|
-
item.done
|
|
825
|
-
? theme.fg("success", "✓ ") + theme.fg("muted", item.text)
|
|
826
|
-
: theme.fg("dim", "○ ") + item.text
|
|
827
|
-
);
|
|
828
|
-
return {
|
|
829
|
-
render: () => lines,
|
|
830
|
-
invalidate: () => {},
|
|
831
|
-
};
|
|
832
|
-
});
|
|
833
|
-
|
|
834
|
-
// Clear
|
|
835
|
-
ctx.ui.setWidget("my-widget", undefined);
|
|
836
|
-
```
|
|
837
|
-
|
|
838
|
-
**Examples:** [plan-mode/index.ts](../examples/extensions/plan-mode/index.ts)
|
|
839
|
-
|
|
840
|
-
### Pattern 6: Custom Footer
|
|
841
|
-
|
|
842
|
-
Replace the footer. `footerData` exposes data not otherwise accessible to extensions.
|
|
843
|
-
|
|
844
|
-
```typescript
|
|
845
|
-
ctx.ui.setFooter((tui, theme, footerData) => ({
|
|
846
|
-
invalidate() {},
|
|
847
|
-
render(width: number): string[] {
|
|
848
|
-
// footerData.getGitBranch(): string | null
|
|
849
|
-
// footerData.getExtensionStatuses(): ReadonlyMap<string, string>
|
|
850
|
-
return [`${ctx.model?.id} (${footerData.getGitBranch() || "no git"})`];
|
|
851
|
-
},
|
|
852
|
-
dispose: footerData.onBranchChange(() => tui.requestRender()), // reactive
|
|
853
|
-
}));
|
|
854
|
-
|
|
855
|
-
ctx.ui.setFooter(undefined); // restore default
|
|
856
|
-
```
|
|
857
|
-
|
|
858
|
-
Token stats available via `ctx.sessionManager.getBranch()` and `ctx.model`.
|
|
859
|
-
|
|
860
|
-
**Examples:** [custom-footer.ts](../examples/extensions/custom-footer.ts)
|
|
861
|
-
|
|
862
|
-
### Pattern 7: Custom Editor (vim mode, etc.)
|
|
863
|
-
|
|
864
|
-
Replace the main input editor with a custom implementation. Useful for modal editing (vim), different keybindings (emacs), or specialized input handling.
|
|
865
|
-
|
|
866
|
-
```typescript
|
|
867
|
-
import { CustomEditor, type ExtensionAPI } from "@knightcodeai/cli";
|
|
868
|
-
import { matchesKey, truncateToWidth } from "@knightcode/tui";
|
|
869
|
-
|
|
870
|
-
type Mode = "normal" | "insert";
|
|
871
|
-
|
|
872
|
-
class VimEditor extends CustomEditor {
|
|
873
|
-
private mode: Mode = "insert";
|
|
874
|
-
|
|
875
|
-
handleInput(data: string): void {
|
|
876
|
-
// Escape: switch to normal mode, or pass through for app handling
|
|
877
|
-
if (matchesKey(data, "escape")) {
|
|
878
|
-
if (this.mode === "insert") {
|
|
879
|
-
this.mode = "normal";
|
|
880
|
-
return;
|
|
881
|
-
}
|
|
882
|
-
// In normal mode, escape aborts agent (handled by CustomEditor)
|
|
883
|
-
super.handleInput(data);
|
|
884
|
-
return;
|
|
885
|
-
}
|
|
886
|
-
|
|
887
|
-
// Insert mode: pass everything to CustomEditor
|
|
888
|
-
if (this.mode === "insert") {
|
|
889
|
-
super.handleInput(data);
|
|
890
|
-
return;
|
|
891
|
-
}
|
|
892
|
-
|
|
893
|
-
// Normal mode: vim-style navigation
|
|
894
|
-
switch (data) {
|
|
895
|
-
case "i": this.mode = "insert"; return;
|
|
896
|
-
case "h": super.handleInput("\x1b[D"); return; // Left
|
|
897
|
-
case "j": super.handleInput("\x1b[B"); return; // Down
|
|
898
|
-
case "k": super.handleInput("\x1b[A"); return; // Up
|
|
899
|
-
case "l": super.handleInput("\x1b[C"); return; // Right
|
|
900
|
-
}
|
|
901
|
-
// Pass unhandled keys to super (ctrl+c, etc.), but filter printable chars
|
|
902
|
-
if (data.length === 1 && data.charCodeAt(0) >= 32) return;
|
|
903
|
-
super.handleInput(data);
|
|
904
|
-
}
|
|
905
|
-
|
|
906
|
-
render(width: number): string[] {
|
|
907
|
-
const lines = super.render(width);
|
|
908
|
-
// Add mode indicator to bottom border (use truncateToWidth for ANSI-safe truncation)
|
|
909
|
-
if (lines.length > 0) {
|
|
910
|
-
const label = this.mode === "normal" ? " NORMAL " : " INSERT ";
|
|
911
|
-
const lastLine = lines[lines.length - 1]!;
|
|
912
|
-
// Pass "" as ellipsis to avoid adding "..." when truncating
|
|
913
|
-
lines[lines.length - 1] = truncateToWidth(lastLine, width - label.length, "") + label;
|
|
914
|
-
}
|
|
915
|
-
return lines;
|
|
916
|
-
}
|
|
917
|
-
}
|
|
918
|
-
|
|
919
|
-
export default function (knightcode: ExtensionAPI) {
|
|
920
|
-
knightcode.on("session_start", (_event, ctx) => {
|
|
921
|
-
// Factory receives the TUI, theme, and keybindings from the app
|
|
922
|
-
ctx.ui.setEditorComponent((tui, theme, keybindings) =>
|
|
923
|
-
new VimEditor(tui, theme, keybindings)
|
|
924
|
-
);
|
|
925
|
-
});
|
|
926
|
-
}
|
|
927
|
-
```
|
|
928
|
-
|
|
929
|
-
**Key points:**
|
|
930
|
-
|
|
931
|
-
- **Extend `CustomEditor`** (not base `Editor`) to get app keybindings (escape to abort, ctrl+d to exit, model switching, etc.)
|
|
932
|
-
- **Call `super.handleInput(data)`** for keys you don't handle
|
|
933
|
-
- **Status spinners**: custom editors keep standalone status rows by default. Pass `{ embedWorkingStatus: true }` as the fourth `CustomEditor` constructor argument to embed working, compaction, branch summarization, and retry spinners in the editor border instead.
|
|
934
|
-
- **Factory pattern**: `setEditorComponent` receives a factory function that gets `tui`, `theme`, and `keybindings`
|
|
935
|
-
- **Pass `undefined`** to restore the default editor: `ctx.ui.setEditorComponent(undefined)`
|
|
936
|
-
|
|
937
|
-
**Examples:** [modal-editor.ts](../examples/extensions/modal-editor.ts)
|
|
938
|
-
|
|
939
|
-
## Key Rules
|
|
940
|
-
|
|
941
|
-
1. **Always use theme from callback** - Don't import theme directly. Use `theme` from the `ctx.ui.custom((tui, theme, keybindings, done) => ...)` callback.
|
|
942
|
-
|
|
943
|
-
2. **Always type DynamicBorder color param** - Write `(s: string) => theme.fg("accent", s)`, not `(s) => theme.fg("accent", s)`.
|
|
944
|
-
|
|
945
|
-
3. **Call tui.requestRender() after state changes** - In `handleInput`, call `tui.requestRender()` after updating state.
|
|
946
|
-
|
|
947
|
-
4. **Return the three-method object** - Custom components need `{ render, invalidate, handleInput }`.
|
|
948
|
-
|
|
949
|
-
5. **Use existing components** - `SelectList`, `SettingsList`, `BorderedLoader` cover 90% of cases. Don't rebuild them.
|
|
950
|
-
|
|
951
|
-
## Examples
|
|
952
|
-
|
|
953
|
-
- **Selection UI**: [examples/extensions/preset.ts](../examples/extensions/preset.ts) - SelectList with DynamicBorder framing
|
|
954
|
-
- **Async with cancel**: [examples/extensions/qna.ts](../examples/extensions/qna.ts) - BorderedLoader for LLM calls
|
|
955
|
-
- **Settings toggles**: [examples/extensions/tools.ts](../examples/extensions/tools.ts) - SettingsList for tool enable/disable
|
|
956
|
-
- **Status indicators**: [examples/extensions/plan-mode/index.ts](../examples/extensions/plan-mode/index.ts) - setStatus and setWidget
|
|
957
|
-
- **Working indicator**: [examples/extensions/working-indicator.ts](../examples/extensions/working-indicator.ts) - setWorkingIndicator
|
|
958
|
-
- **Custom footer**: [examples/extensions/custom-footer.ts](../examples/extensions/custom-footer.ts) - setFooter with stats
|
|
959
|
-
- **Custom editor**: [examples/extensions/modal-editor.ts](../examples/extensions/modal-editor.ts) - Vim-like modal editing
|
|
960
|
-
- **Snake game**: [examples/extensions/snake.ts](../examples/extensions/snake.ts) - Full game with keyboard input, game loop
|
|
961
|
-
- **Custom tool rendering**: [examples/extensions/todo.ts](../examples/extensions/todo.ts) - renderCall and renderResult
|
|
90
|
+
Rendering runs on the interactive path. Cache expensive layout and highlighting work by width and content, then clear that cache from `invalidate()`.
|
|
91
|
+
|
|
92
|
+
Keep the default view compact and reveal detail through expansion or a dedicated screen. For custom tool rendering, handle partial results and reuse the previous component when it can be updated safely.
|
|
93
|
+
|
|
94
|
+
Use `KNIGHTCODE_TUI_WRITE_LOG` to capture the raw ANSI stream when diagnosing rendering problems. Test narrow widths, wide characters, resize events, theme changes, focus transitions, and both regular and fullscreen modes.
|
|
95
|
+
|
|
96
|
+
## Examples and source
|
|
97
|
+
|
|
98
|
+
The checked extension examples cover the main patterns:
|
|
99
|
+
|
|
100
|
+
- [`preset.ts`](../examples/extensions/preset.ts) and [`tools.ts`](../examples/extensions/tools.ts) use selection and settings lists.
|
|
101
|
+
- [`qna.ts`](../examples/extensions/qna.ts) uses cancellable asynchronous UI.
|
|
102
|
+
- [`modal-editor.ts`](../examples/extensions/modal-editor.ts) replaces the editor.
|
|
103
|
+
- [`custom-footer.ts`](../examples/extensions/custom-footer.ts) replaces the footer.
|
|
104
|
+
- [`widget-placement.ts`](../examples/extensions/widget-placement.ts) places persistent content around the editor.
|
|
105
|
+
- [`doom-overlay/`](../examples/extensions/doom-overlay/) demonstrates a continuously rendered overlay.
|
|
106
|
+
|
|
107
|
+
The public exports are defined in [`packages/tui/src/index.ts`](https://github.com/KnightCodeAI/knightcode/blob/main/packages/tui/src/index.ts). See [Extensions](extensions.md) for extension lifecycle, state, tools, events, and mode behavior.
|