@visual-z/xeno 0.2.1 → 0.4.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 +18 -0
- package/README.md +126 -34
- package/docs/app-server.md +84 -0
- package/docs/install.md +51 -13
- package/docs/providers.md +24 -3
- package/package.json +7 -5
- package/src/apps/app-server.ts +28 -0
- package/src/apps/approve.ts +3 -3
- package/src/apps/cli.ts +25 -10
- package/src/apps/commands.ts +45 -8
- package/src/apps/completions.ts +78 -22
- package/src/apps/control-transport.ts +89 -0
- package/src/apps/image-draft.ts +46 -0
- package/src/apps/model-catalog.ts +2 -1
- package/src/apps/project-trust.ts +2 -42
- package/src/apps/provider-connect.ts +346 -0
- package/src/apps/render.ts +22 -47
- package/src/apps/resources.ts +5 -2
- package/src/apps/shell.ts +333 -1542
- package/src/apps/startup-input.ts +45 -9
- package/src/apps/startup.ts +149 -94
- package/src/apps/ui/app.ts +1553 -0
- package/src/apps/ui/control-model.ts +26 -0
- package/src/apps/ui/extension-host.ts +194 -0
- package/src/apps/ui/image-cache.ts +78 -0
- package/src/apps/ui/palette.ts +88 -0
- package/src/apps/ui/project.ts +127 -0
- package/src/apps/ui/standalone.ts +146 -0
- package/src/apps/ui/status-model.ts +68 -0
- package/src/apps/ui/surface.ts +86 -0
- package/src/apps/ui/theme.ts +86 -0
- package/src/core/acceptance.ts +107 -0
- package/src/core/action-defaults.ts +47 -295
- package/src/core/benchmark.ts +2 -2
- package/src/core/branch-receipt.ts +24 -0
- package/src/core/checkpoints.ts +64 -0
- package/src/core/churn.ts +2 -0
- package/src/core/command-artifacts.ts +94 -0
- package/src/core/command-contract.ts +35 -0
- package/src/core/completion-action.ts +20 -0
- package/src/core/completion-review.ts +37 -21
- package/src/core/context-detail.ts +37 -0
- package/src/core/context-policy.ts +74 -1
- package/src/core/continuation.ts +17 -2
- package/src/core/control-protocol.ts +62 -0
- package/src/core/control-view.ts +42 -0
- package/src/core/derive.ts +29 -3
- package/src/core/draft-images.ts +56 -0
- package/src/core/environment.ts +145 -0
- package/src/core/evaluation-coverage.ts +74 -0
- package/src/core/event-validation.ts +81 -6
- package/src/core/events.ts +99 -3
- package/src/core/evidence-links.ts +7 -0
- package/src/core/evidence.ts +13 -3
- package/src/core/execution.ts +8 -2
- package/src/core/experiment.ts +133 -0
- package/src/core/export.ts +9 -0
- package/src/core/failure-recovery.ts +173 -0
- package/src/core/file-precondition.ts +72 -0
- package/src/core/handoff.ts +65 -4
- package/src/core/harness-diagnostics.ts +206 -0
- package/src/core/history-query.ts +38 -2
- package/src/core/history.ts +38 -1
- package/src/core/integration-health.ts +34 -0
- package/src/core/keymap.ts +8 -20
- package/src/core/model-picker.ts +64 -0
- package/src/core/native-eval.ts +25 -0
- package/src/core/note-guidance.ts +34 -0
- package/src/core/oauth-fields.ts +44 -0
- package/src/core/observation-view.ts +49 -8
- package/src/core/permissions.ts +24 -2
- package/src/core/process-receipt.ts +105 -0
- package/src/core/process-state.ts +37 -0
- package/src/core/program-status.ts +31 -0
- package/src/core/project.ts +70 -19
- package/src/core/prompt-policy.ts +37 -0
- package/src/core/query-eval-contract.ts +14 -0
- package/src/core/read-observation.ts +82 -0
- package/src/core/recovery-corpus.ts +17 -0
- package/src/core/recovery-evaluation.ts +60 -0
- package/src/core/recovery-sources.ts +74 -0
- package/src/core/request-progress.ts +87 -0
- package/src/core/request-state.ts +114 -0
- package/src/core/runtime-target.ts +94 -0
- package/src/core/search-observation.ts +97 -0
- package/src/core/session-tree.ts +147 -0
- package/src/core/settings.ts +33 -10
- package/src/core/skills.ts +44 -0
- package/src/core/sql-query.ts +211 -0
- package/src/core/theme.ts +36 -58
- package/src/core/tool-contract.ts +51 -2
- package/src/core/tool-discovery.ts +87 -0
- package/src/core/tool-failure.ts +60 -0
- package/src/core/tool-input.ts +6 -0
- package/src/core/tool-mode-protocol.ts +25 -0
- package/src/core/transcript.ts +19 -4
- package/src/core/trial-lifecycle.ts +14 -0
- package/src/core/tui-mode.ts +29 -0
- package/src/core/update-check.ts +97 -0
- package/src/core/version.ts +55 -0
- package/src/core/work-state.ts +49 -3
- package/src/core/working-set.ts +91 -0
- package/src/runtime/auth.ts +11 -4
- package/src/runtime/branch-jobs.ts +36 -0
- package/src/runtime/catalog.ts +13 -3
- package/src/runtime/clipboard.ts +15 -4
- package/src/runtime/command-artifacts.ts +64 -0
- package/src/runtime/control-connection.ts +104 -0
- package/src/runtime/environment-tools.ts +60 -0
- package/src/runtime/evidence-notes.ts +4 -2
- package/src/runtime/exec-sessions.ts +206 -61
- package/src/runtime/execution-control.ts +1 -0
- package/src/runtime/extension-ui.ts +22 -30
- package/src/runtime/extensions.ts +95 -16
- package/src/runtime/harness-comparison.ts +27 -7
- package/src/runtime/harness-eval.ts +462 -0
- package/src/runtime/history-cache.ts +57 -0
- package/src/runtime/history-tools.ts +62 -12
- package/src/runtime/integrations.ts +210 -0
- package/src/runtime/loop.ts +1088 -282
- package/src/runtime/model-discovery.ts +208 -0
- package/src/runtime/model.ts +9 -0
- package/src/runtime/native-eval-fixtures.ts +33 -0
- package/src/runtime/native-eval.ts +146 -0
- package/src/runtime/oauth.ts +80 -3
- package/src/runtime/pi-provider.ts +169 -12
- package/src/runtime/program-status.ts +38 -0
- package/src/runtime/provider-connect.ts +109 -0
- package/src/runtime/provider-presets.ts +87 -0
- package/src/runtime/provider-request.ts +13 -2
- package/src/runtime/provider.ts +23 -9
- package/src/runtime/query-eval-verifier.ts +28 -0
- package/src/runtime/recovery-coverage.ts +71 -0
- package/src/runtime/recovery-evidence.ts +76 -0
- package/src/runtime/recovery-fixtures.ts +116 -0
- package/src/runtime/runtime-target.ts +149 -0
- package/src/runtime/session-control.ts +198 -0
- package/src/runtime/tool-broker.ts +137 -0
- package/src/runtime/tool-discovery.ts +57 -0
- package/src/runtime/tool-mode-eval.ts +146 -0
- package/src/runtime/tool-queue.ts +77 -0
- package/src/runtime/tools.ts +628 -183
- package/src/runtime/trial-control.ts +45 -0
- package/src/runtime/update-check.ts +47 -0
- package/src/runtime/window-trial.ts +2 -1
- package/src/runtime/working-notes.ts +33 -3
- package/src/runtime/workspace-lifecycle.ts +97 -0
- package/src/store/acceptance.ts +15 -0
- package/src/store/checkpoint-history.ts +43 -0
- package/src/store/command-artifacts.ts +96 -0
- package/src/store/command.ts +65 -37
- package/src/store/file-checkpoints.ts +104 -0
- package/src/store/file-index.ts +13 -1
- package/src/store/git-checkpoints.ts +67 -0
- package/src/store/observations.ts +61 -20
- package/src/store/process-receipts.ts +34 -0
- package/src/store/query-source.ts +13 -8
- package/src/store/recovery.ts +2 -3
- package/src/store/replay.ts +16 -4
- package/src/store/runtime-target.ts +64 -0
- package/src/store/search.ts +171 -132
- package/src/store/session-archive.ts +4 -0
- package/src/store/source-cancellation.ts +6 -0
- package/src/store/update-cache.ts +36 -0
- package/src/store/workspace-observation.ts +113 -0
- package/src/tui/ansi-spans.ts +126 -0
- package/src/tui/style.ts +19 -16
- package/src/tui/ui/atomic-draft.ts +121 -0
- package/src/tui/ui/canvas.ts +96 -0
- package/src/tui/ui/colors.ts +70 -0
- package/src/tui/ui/composer.ts +200 -0
- package/src/tui/ui/control-panel.ts +23 -0
- package/src/tui/ui/dialog.ts +379 -0
- package/src/tui/ui/draft.ts +23 -0
- package/src/tui/ui/edge.ts +186 -0
- package/src/tui/ui/image-size.ts +40 -0
- package/src/tui/ui/ink.ts +210 -0
- package/src/tui/ui/items.ts +40 -0
- package/src/tui/ui/keys.ts +40 -0
- package/src/tui/ui/layout.ts +74 -0
- package/src/tui/ui/list-model.ts +86 -0
- package/src/tui/ui/list-view.ts +172 -0
- package/src/tui/ui/mascot.ts +67 -0
- package/src/tui/ui/messages.ts +518 -0
- package/src/tui/ui/motion.ts +28 -0
- package/src/tui/ui/paths.ts +10 -0
- package/src/tui/ui/pictures.ts +78 -0
- package/src/tui/ui/screen.ts +421 -0
- package/src/tui/ui/status.ts +279 -0
- package/src/tui/ui/styled.ts +85 -0
- package/src/tui/ui/testing.ts +5 -0
- package/src/tui/ui/tool-card.ts +150 -0
- package/src/apps/extension-ui.ts +0 -330
- package/src/apps/resource-browser.ts +0 -75
- package/src/apps/session-browser.ts +0 -113
- package/src/apps/session-picker.ts +0 -55
- package/src/tui/ansi.ts +0 -67
- package/src/tui/autocomplete.ts +0 -29
- package/src/tui/chat-viewport.ts +0 -47
- package/src/tui/complete.ts +0 -87
- package/src/tui/editor.ts +0 -940
- package/src/tui/host.ts +0 -264
- package/src/tui/input.ts +0 -75
- package/src/tui/keys.ts +0 -378
- package/src/tui/markdown.ts +0 -17
- package/src/tui/render.ts +0 -153
- package/src/tui/select.ts +0 -258
- package/src/tui/settings.ts +0 -44
- package/src/tui/statusline.ts +0 -162
- package/src/tui/system-theme.ts +0 -700
- package/src/tui/terminal.ts +0 -30
- package/src/tui/text.ts +0 -52
- package/src/tui/toolview.ts +0 -386
- package/src/tui/transcript.ts +0 -123
- package/src/tui/tree.ts +0 -471
- package/src/vendor/pi-tui/LICENSE.upstream +0 -21
- package/src/vendor/pi-tui/UPSTREAM.json +0 -50
- package/src/vendor/pi-tui/VENDOR.md +0 -25
- package/src/vendor/pi-tui/alt-screen-search.ts +0 -327
- package/src/vendor/pi-tui/autocomplete.ts +0 -860
- package/src/vendor/pi-tui/colors.ts +0 -367
- package/src/vendor/pi-tui/components/alt-screen-flash.ts +0 -51
- package/src/vendor/pi-tui/components/box.ts +0 -166
- package/src/vendor/pi-tui/components/cancellable-loader.ts +0 -40
- package/src/vendor/pi-tui/components/editor.ts +0 -2472
- package/src/vendor/pi-tui/components/h-stack.ts +0 -44
- package/src/vendor/pi-tui/components/image.ts +0 -127
- package/src/vendor/pi-tui/components/input.ts +0 -494
- package/src/vendor/pi-tui/components/loader.ts +0 -101
- package/src/vendor/pi-tui/components/markdown.ts +0 -1021
- package/src/vendor/pi-tui/components/mouse-region.ts +0 -33
- package/src/vendor/pi-tui/components/scroll-view.ts +0 -224
- package/src/vendor/pi-tui/components/select-list.ts +0 -273
- package/src/vendor/pi-tui/components/settings-list.ts +0 -328
- package/src/vendor/pi-tui/components/spacer.ts +0 -28
- package/src/vendor/pi-tui/components/stack.ts +0 -154
- package/src/vendor/pi-tui/components/text.ts +0 -107
- package/src/vendor/pi-tui/components/truncated-text.ts +0 -65
- package/src/vendor/pi-tui/components/v-stack.ts +0 -33
- package/src/vendor/pi-tui/editor-component.ts +0 -74
- package/src/vendor/pi-tui/fuzzy.ts +0 -138
- package/src/vendor/pi-tui/index.ts +0 -186
- package/src/vendor/pi-tui/keybindings.ts +0 -320
- package/src/vendor/pi-tui/keys.ts +0 -1401
- package/src/vendor/pi-tui/kill-ring.ts +0 -46
- package/src/vendor/pi-tui/latex.ts +0 -1506
- package/src/vendor/pi-tui/layout-node.ts +0 -51
- package/src/vendor/pi-tui/layout.ts +0 -449
- package/src/vendor/pi-tui/native/UPSTREAM.json +0 -23
- package/src/vendor/pi-tui/native/clipboard.h +0 -208
- package/src/vendor/pi-tui/native/darwin/README.md +0 -22
- package/src/vendor/pi-tui/native/darwin/build.sh +0 -69
- package/src/vendor/pi-tui/native/darwin/prebuilds/darwin-arm64/darwin-platform.node +0 -0
- package/src/vendor/pi-tui/native/darwin/prebuilds/darwin-x64/darwin-platform.node +0 -0
- package/src/vendor/pi-tui/native/darwin/src/darwin-platform.m +0 -132
- package/src/vendor/pi-tui/native/linux/README.md +0 -23
- package/src/vendor/pi-tui/native/linux/build.sh +0 -65
- package/src/vendor/pi-tui/native/linux/prebuilds/linux-arm64/linux-platform-x11.node +0 -0
- package/src/vendor/pi-tui/native/linux/prebuilds/linux-x64/linux-platform-x11.node +0 -0
- package/src/vendor/pi-tui/native/linux/src/clipboard-worker.h +0 -70
- package/src/vendor/pi-tui/native/linux/src/linux-platform-x11.c +0 -274
- package/src/vendor/pi-tui/native/napi.h +0 -91
- package/src/vendor/pi-tui/native/win32/README.md +0 -31
- package/src/vendor/pi-tui/native/win32/build.mjs +0 -232
- package/src/vendor/pi-tui/native/win32/prebuilds/win32-arm64/win32-platform.node +0 -0
- package/src/vendor/pi-tui/native/win32/prebuilds/win32-x64/win32-platform.node +0 -0
- package/src/vendor/pi-tui/native/win32/src/win32-platform.c +0 -241
- package/src/vendor/pi-tui/native-modifiers.ts +0 -13
- package/src/vendor/pi-tui/native-module-path.ts +0 -31
- package/src/vendor/pi-tui/native-platform.ts +0 -65
- package/src/vendor/pi-tui/oklab.ts +0 -233
- package/src/vendor/pi-tui/stdin-buffer.ts +0 -444
- package/src/vendor/pi-tui/terminal-colors.ts +0 -91
- package/src/vendor/pi-tui/terminal-image.ts +0 -731
- package/src/vendor/pi-tui/terminal.ts +0 -554
- package/src/vendor/pi-tui/tui-alt-screen.ts +0 -1750
- package/src/vendor/pi-tui/tui-main-screen.ts +0 -655
- package/src/vendor/pi-tui/tui.ts +0 -1493
- package/src/vendor/pi-tui/undo-stack.ts +0 -28
- package/src/vendor/pi-tui/utils.ts +0 -1385
- package/src/vendor/pi-tui/wheel-scroll.ts +0 -82
- package/src/vendor/pi-tui/word-navigation.ts +0 -117
package/src/tui/keys.ts
DELETED
|
@@ -1,378 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Key decoding as a pure function: bytes in, events plus an unconsumed
|
|
3
|
-
* prefix out.
|
|
4
|
-
*
|
|
5
|
-
* Escape sequences are split by pty and TCP buffering at arbitrary
|
|
6
|
-
* boundaries -- `\x1b[A` (up-arrow) can arrive as `\x1b[` then `A`. A
|
|
7
|
-
* callback state machine can only be shown to survive that by luck; a
|
|
8
|
-
* function that returns the leftover prefix can be shown by a test.
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import { assertNever } from "./exhaustive.ts";
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* xterm ctlseqs modifiers, attached only when a recognised bit is set.
|
|
15
|
-
* Absent means unmodified -- `{ type: "left" }` stays that shape so
|
|
16
|
-
* existing matches keep working. Meta (bit 3) is not modelled.
|
|
17
|
-
*/
|
|
18
|
-
export interface KeyMods {
|
|
19
|
-
readonly shift?: boolean;
|
|
20
|
-
readonly alt?: boolean;
|
|
21
|
-
readonly ctrl?: boolean;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export type Key =
|
|
25
|
-
| { type: "char"; grapheme: string }
|
|
26
|
-
| { type: "enter"; shift: boolean }
|
|
27
|
-
| { type: "backspace" }
|
|
28
|
-
| { type: "delete"; mods?: KeyMods }
|
|
29
|
-
| { type: "tab"; shift?: boolean }
|
|
30
|
-
| { type: "escape" }
|
|
31
|
-
| { type: "up"; mods?: KeyMods }
|
|
32
|
-
| { type: "down"; mods?: KeyMods }
|
|
33
|
-
| { type: "left"; mods?: KeyMods }
|
|
34
|
-
| { type: "right"; mods?: KeyMods }
|
|
35
|
-
| { type: "home"; mods?: KeyMods }
|
|
36
|
-
| { type: "end"; mods?: KeyMods }
|
|
37
|
-
| { type: "pageup"; mods?: KeyMods }
|
|
38
|
-
| { type: "pagedown"; mods?: KeyMods }
|
|
39
|
-
| { type: "ctrl"; key: string; shift?: boolean }
|
|
40
|
-
| { type: "alt"; key: string };
|
|
41
|
-
|
|
42
|
-
export interface DecodeResult {
|
|
43
|
-
events: Key[];
|
|
44
|
-
rest: Uint8Array;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* 50ms. Measured convention, not a standard: xterm, libuv and readline all
|
|
49
|
-
* pick a few tens of milliseconds to decide that a lone ESC is the Escape
|
|
50
|
-
* key rather than the start of a CSI. Too short and a slow CSI is split into
|
|
51
|
-
* Escape plus junk; too long and Escape-to-interrupt feels sticky. The
|
|
52
|
-
* decoder itself is still a pure function -- the caller passes `escTimedOut`
|
|
53
|
-
* after it has waited, because a timeout is a clock and a clock is not pure.
|
|
54
|
-
*/
|
|
55
|
-
export const ESCAPE_TIMEOUT_MS = 50;
|
|
56
|
-
|
|
57
|
-
type Seq =
|
|
58
|
-
| { kind: "csi"; params: number[]; final: string }
|
|
59
|
-
| { kind: "ss3"; final: string }
|
|
60
|
-
| { kind: "alt"; grapheme: string };
|
|
61
|
-
|
|
62
|
-
type Taken = { key: Key | null; next: number };
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* Stable chord spelling for the keymap (`ctrl+shift+x`). Alt+Enter arrives
|
|
66
|
-
* as `{ type: "alt", key: "\\r" }` on some terminals; that must still be
|
|
67
|
-
* `alt+enter` or a rebound follow-up never matches.
|
|
68
|
-
*/
|
|
69
|
-
export function formatTuiKey(key: Key): string {
|
|
70
|
-
switch (key.type) {
|
|
71
|
-
case "char":
|
|
72
|
-
return key.grapheme.length === 1 ? key.grapheme.toLowerCase() : key.grapheme;
|
|
73
|
-
case "enter":
|
|
74
|
-
return key.shift ? "shift+enter" : "enter";
|
|
75
|
-
case "backspace":
|
|
76
|
-
return "backspace";
|
|
77
|
-
case "delete":
|
|
78
|
-
return withMods(key.mods, "delete");
|
|
79
|
-
case "tab":
|
|
80
|
-
return key.shift === true ? "shift+tab" : "tab";
|
|
81
|
-
case "escape":
|
|
82
|
-
return "escape";
|
|
83
|
-
case "up":
|
|
84
|
-
case "down":
|
|
85
|
-
case "left":
|
|
86
|
-
case "right":
|
|
87
|
-
case "home":
|
|
88
|
-
case "end":
|
|
89
|
-
return withMods(key.mods, key.type);
|
|
90
|
-
case "pageup":
|
|
91
|
-
return withMods(key.mods, "pageup");
|
|
92
|
-
case "pagedown":
|
|
93
|
-
return withMods(key.mods, "pagedown");
|
|
94
|
-
case "ctrl":
|
|
95
|
-
return key.shift === true
|
|
96
|
-
? `ctrl+shift+${key.key.toLowerCase()}`
|
|
97
|
-
: `ctrl+${key.key.toLowerCase()}`;
|
|
98
|
-
case "alt": {
|
|
99
|
-
const k = key.key === "\r" || key.key === "\n" ? "enter" : key.key.toLowerCase();
|
|
100
|
-
return `alt+${k}`;
|
|
101
|
-
}
|
|
102
|
-
default:
|
|
103
|
-
return assertNever(key, "formatTuiKey");
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
function withMods(mods: KeyMods | undefined, key: string): string {
|
|
108
|
-
const parts: string[] = [];
|
|
109
|
-
if (mods?.ctrl === true) parts.push("ctrl");
|
|
110
|
-
if (mods?.alt === true) parts.push("alt");
|
|
111
|
-
if (mods?.shift === true) parts.push("shift");
|
|
112
|
-
parts.push(key);
|
|
113
|
-
return parts.join("+");
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
export function decodeKeys(bytes: Uint8Array, opts: { escTimedOut?: boolean } = {}): DecodeResult {
|
|
117
|
-
const events: Key[] = [];
|
|
118
|
-
let i = 0;
|
|
119
|
-
const timedOut = opts.escTimedOut === true;
|
|
120
|
-
while (i < bytes.length) {
|
|
121
|
-
const taken = take(bytes, i, timedOut);
|
|
122
|
-
if (taken === null) break;
|
|
123
|
-
if (taken.key !== null) events.push(taken.key);
|
|
124
|
-
i = taken.next;
|
|
125
|
-
}
|
|
126
|
-
return { events, rest: bytes.slice(i) };
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
function take(bytes: Uint8Array, i: number, timedOut: boolean): Taken | null {
|
|
130
|
-
const b = bytes[i]!;
|
|
131
|
-
|
|
132
|
-
if (b === 0x1b) {
|
|
133
|
-
if (i + 1 >= bytes.length) {
|
|
134
|
-
return timedOut ? { key: { type: "escape" }, next: i + 1 } : null;
|
|
135
|
-
}
|
|
136
|
-
const next = bytes[i + 1]!;
|
|
137
|
-
if (next === 0x5b) return takeCsi(bytes, i);
|
|
138
|
-
if (next === 0x4f) return takeSs3(bytes, i);
|
|
139
|
-
const utf = takeUtf8(bytes, i + 1);
|
|
140
|
-
if (utf === null) return null;
|
|
141
|
-
return seqToTaken({ kind: "alt", grapheme: utf.grapheme }, utf.next);
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
if (b === 0x0d || b === 0x0a) return { key: { type: "enter", shift: false }, next: i + 1 };
|
|
145
|
-
if (b === 0x09) return { key: { type: "tab" }, next: i + 1 };
|
|
146
|
-
if (b === 0x7f) return { key: { type: "backspace" }, next: i + 1 };
|
|
147
|
-
if (b < 0x20) {
|
|
148
|
-
// C0 minus ESC/CR/LF/TAB, already handled. 0x01..0x1A are Ctrl+a..z
|
|
149
|
-
// (Ctrl+H is 0x08, distinct from DEL-as-Backspace).
|
|
150
|
-
if (b >= 0x01 && b <= 0x1a) {
|
|
151
|
-
return { key: { type: "ctrl", key: String.fromCharCode(b + 96) }, next: i + 1 };
|
|
152
|
-
}
|
|
153
|
-
// Ctrl+_ is US (0x1f), not in a-z. Without this the advertised undo
|
|
154
|
-
// chord never arrives from a real tty — applyKey would only see it
|
|
155
|
-
// from tests constructing the event by hand.
|
|
156
|
-
if (b === 0x1f) return { key: { type: "ctrl", key: "_" }, next: i + 1 };
|
|
157
|
-
return { key: null, next: i + 1 };
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
const utf = takeUtf8(bytes, i);
|
|
161
|
-
if (utf === null) return null;
|
|
162
|
-
return { key: { type: "char", grapheme: utf.grapheme }, next: utf.next };
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
/**
|
|
166
|
-
* CSI: ESC [ ... final. ECMA-48 §5.4. Parameter bytes 0x30-0x3F,
|
|
167
|
-
* intermediate 0x20-0x2F, final 0x40-0x7E. Running out of bytes before a
|
|
168
|
-
* final is the split-read case and must leave the prefix in `rest`.
|
|
169
|
-
*/
|
|
170
|
-
function takeCsi(bytes: Uint8Array, i: number): Taken | null {
|
|
171
|
-
let j = i + 2;
|
|
172
|
-
while (j < bytes.length) {
|
|
173
|
-
const c = bytes[j]!;
|
|
174
|
-
if (c >= 0x40 && c <= 0x7e) {
|
|
175
|
-
const params = decodeParams(bytes.subarray(i + 2, j));
|
|
176
|
-
const final = String.fromCharCode(c);
|
|
177
|
-
return seqToTaken({ kind: "csi", params, final }, j + 1);
|
|
178
|
-
}
|
|
179
|
-
if (c >= 0x20 && c <= 0x3f) {
|
|
180
|
-
j++;
|
|
181
|
-
continue;
|
|
182
|
-
}
|
|
183
|
-
// Not a CSI parameter. Consume the ESC alone as Escape so we do not
|
|
184
|
-
// stall on garbage; the rest of the buffer is decoded on the next loop.
|
|
185
|
-
return { key: { type: "escape" }, next: i + 1 };
|
|
186
|
-
}
|
|
187
|
-
return null;
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
/**
|
|
191
|
-
* SS3: ESC O X. ECMA-48 SS3 / xterm DECCKM application-cursor mode.
|
|
192
|
-
* Without this, arrow keys in a terminal that has sent SM ?1 become
|
|
193
|
-
* unrecognised (ESC O A rather than ESC [ A).
|
|
194
|
-
*/
|
|
195
|
-
function takeSs3(bytes: Uint8Array, i: number): Taken | null {
|
|
196
|
-
if (i + 2 >= bytes.length) return null;
|
|
197
|
-
const final = String.fromCharCode(bytes[i + 2]!);
|
|
198
|
-
return seqToTaken({ kind: "ss3", final }, i + 3);
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
function seqToTaken(seq: Seq, next: number): Taken {
|
|
202
|
-
return { key: seqToKey(seq), next };
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
function seqToKey(seq: Seq): Key | null {
|
|
206
|
-
switch (seq.kind) {
|
|
207
|
-
case "csi":
|
|
208
|
-
return csiToKey(seq.params, seq.final);
|
|
209
|
-
case "ss3":
|
|
210
|
-
return ss3ToKey(seq.final);
|
|
211
|
-
case "alt":
|
|
212
|
-
return { type: "alt", key: seq.grapheme };
|
|
213
|
-
default:
|
|
214
|
-
return assertNever(seq, "seqToKey");
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
function ss3ToKey(final: string): Key | null {
|
|
219
|
-
switch (final) {
|
|
220
|
-
case "A": return { type: "up" };
|
|
221
|
-
case "B": return { type: "down" };
|
|
222
|
-
case "C": return { type: "right" };
|
|
223
|
-
case "D": return { type: "left" };
|
|
224
|
-
case "H": return { type: "home" };
|
|
225
|
-
case "F": return { type: "end" };
|
|
226
|
-
default: return null;
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
function csiToKey(params: number[], final: string): Key | null {
|
|
231
|
-
const mods = xtermMods(params[1]);
|
|
232
|
-
switch (final) {
|
|
233
|
-
case "A": return navKey("up", mods);
|
|
234
|
-
case "B": return navKey("down", mods);
|
|
235
|
-
case "C": return navKey("right", mods);
|
|
236
|
-
case "D": return navKey("left", mods);
|
|
237
|
-
case "H": return navKey("home", mods);
|
|
238
|
-
case "F": return navKey("end", mods);
|
|
239
|
-
case "Z": return { type: "tab", shift: true };
|
|
240
|
-
case "u": return kittyKey(params);
|
|
241
|
-
case "~": return tildeKey(params);
|
|
242
|
-
default: return null;
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
/**
|
|
247
|
-
* Kitty keyboard protocol CSI unicode ; modifier u.
|
|
248
|
-
* https://sw.kovidgoyal.net/kitty/keyboard-protocol/
|
|
249
|
-
* Shift+Enter is not in ECMA-48; terminals that can report it use this
|
|
250
|
-
* (or xterm modifyOtherKeys below). Without it, Shift+Enter is just Enter
|
|
251
|
-
* and a multi-line editor cannot exist.
|
|
252
|
-
*/
|
|
253
|
-
function kittyKey(params: number[]): Key | null {
|
|
254
|
-
const cp = params[0] ?? 0;
|
|
255
|
-
const shift = hasShift(params[1]);
|
|
256
|
-
if (cp === 13) return { type: "enter", shift };
|
|
257
|
-
if (cp === 27) return { type: "escape" };
|
|
258
|
-
if (cp === 127) return { type: "backspace" };
|
|
259
|
-
if (cp === 9) return hasShift(params[1]) ? { type: "tab", shift: true } : { type: "tab" };
|
|
260
|
-
if (cp >= 32) return { type: "char", grapheme: String.fromCodePoint(cp) };
|
|
261
|
-
return null;
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
/**
|
|
265
|
-
* xterm CSI n~ (vt sequences) and CSI 27 ; mod ; key ~ (modifyOtherKeys).
|
|
266
|
-
* https://invisible-island.net/xterm/ctlseqs/ctlseqs.html
|
|
267
|
-
*/
|
|
268
|
-
function tildeKey(params: number[]): Key | null {
|
|
269
|
-
const a = params[0] ?? 0;
|
|
270
|
-
if (a === 27) {
|
|
271
|
-
const key = params[2] ?? 0;
|
|
272
|
-
if (key === 13) return { type: "enter", shift: hasShift(params[1]) };
|
|
273
|
-
return null;
|
|
274
|
-
}
|
|
275
|
-
const mods = xtermMods(params[1]);
|
|
276
|
-
switch (a) {
|
|
277
|
-
case 1:
|
|
278
|
-
case 7: return navKey("home", mods);
|
|
279
|
-
case 3: return navKey("delete", mods);
|
|
280
|
-
case 4:
|
|
281
|
-
case 8: return navKey("end", mods);
|
|
282
|
-
case 5: return navKey("pageup", mods);
|
|
283
|
-
case 6: return navKey("pagedown", mods);
|
|
284
|
-
case 13: return { type: "enter", shift: hasShift(params[1]) };
|
|
285
|
-
default: return null;
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
type NavType =
|
|
290
|
-
| "up"
|
|
291
|
-
| "down"
|
|
292
|
-
| "left"
|
|
293
|
-
| "right"
|
|
294
|
-
| "home"
|
|
295
|
-
| "end"
|
|
296
|
-
| "pageup"
|
|
297
|
-
| "pagedown"
|
|
298
|
-
| "delete";
|
|
299
|
-
|
|
300
|
-
function navKey(type: NavType, mods: KeyMods | undefined): Key {
|
|
301
|
-
return mods === undefined ? { type } : { type, mods };
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
/**
|
|
305
|
-
* xterm ctlseqs (modified cursor keys / modifyOtherKeys): the CSI
|
|
306
|
-
* modifier parameter is `1 + bitmask`. Bit 0 is Shift, bit 1 Alt, bit
|
|
307
|
-
* 2 Ctrl; they combine (CSI 1;7D is Ctrl+Alt+Left).
|
|
308
|
-
* https://invisible-island.net/xterm/ctlseqs/ctlseqs.html
|
|
309
|
-
* Without these, CSI 1;3D (Alt+Left) and CSI 1;5D (Ctrl+Left) collapse
|
|
310
|
-
* onto CSI D, and the editor's word-motion bindings can never fire.
|
|
311
|
-
*/
|
|
312
|
-
const XTERM_MOD_SHIFT = 1;
|
|
313
|
-
const XTERM_MOD_ALT = 2;
|
|
314
|
-
const XTERM_MOD_CTRL = 4;
|
|
315
|
-
|
|
316
|
-
/**
|
|
317
|
-
* Decode the xterm modifier parameter. Omitted or 1 means unmodified.
|
|
318
|
-
* Values below 1 are not in the encoding -- treating them as a bitmask
|
|
319
|
-
* would sign-extend (`0 - 1` is -1, every bit set). Unrecognised bits
|
|
320
|
-
* (Meta = 8, CSI 1;9D) are ignored rather than dropping the key: a
|
|
321
|
-
* dead key looks like a stuck terminal, an unbound shortcut does not.
|
|
322
|
-
*/
|
|
323
|
-
function xtermMods(mod: number | undefined): KeyMods | undefined {
|
|
324
|
-
if (mod === undefined || mod < 2) return undefined;
|
|
325
|
-
const bits = mod - 1;
|
|
326
|
-
const shift = (bits & XTERM_MOD_SHIFT) !== 0;
|
|
327
|
-
const alt = (bits & XTERM_MOD_ALT) !== 0;
|
|
328
|
-
const ctrl = (bits & XTERM_MOD_CTRL) !== 0;
|
|
329
|
-
if (!shift && !alt && !ctrl) return undefined;
|
|
330
|
-
return {
|
|
331
|
-
...(shift ? { shift: true } : {}),
|
|
332
|
-
...(alt ? { alt: true } : {}),
|
|
333
|
-
...(ctrl ? { ctrl: true } : {}),
|
|
334
|
-
};
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
/** xterm/Kitty modifier encoding: 1 + bitfield, bit 0 is Shift. */
|
|
338
|
-
function hasShift(mod: number | undefined): boolean {
|
|
339
|
-
return xtermMods(mod)?.shift === true;
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
function decodeParams(buf: Uint8Array): number[] {
|
|
343
|
-
if (buf.length === 0) return [];
|
|
344
|
-
let s = new TextDecoder().decode(buf);
|
|
345
|
-
if (s.startsWith("?") || s.startsWith(">")) s = s.slice(1);
|
|
346
|
-
if (s.length === 0) return [];
|
|
347
|
-
const out: number[] = [];
|
|
348
|
-
for (const part of s.split(";")) {
|
|
349
|
-
const n = part === "" ? 0 : Number.parseInt(part, 10);
|
|
350
|
-
out.push(Number.isFinite(n) ? n : 0);
|
|
351
|
-
}
|
|
352
|
-
return out;
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
function utf8Need(lead: number): number {
|
|
356
|
-
if (lead < 0x80) return 1;
|
|
357
|
-
if ((lead & 0xe0) === 0xc0) return 2;
|
|
358
|
-
if ((lead & 0xf0) === 0xe0) return 3;
|
|
359
|
-
if ((lead & 0xf8) === 0xf0) return 4;
|
|
360
|
-
return 1;
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
function takeUtf8(bytes: Uint8Array, i: number): { grapheme: string; next: number } | null {
|
|
364
|
-
const lead = bytes[i]!;
|
|
365
|
-
const need = utf8Need(lead);
|
|
366
|
-
if (i + need > bytes.length) return null;
|
|
367
|
-
if (need > 1) {
|
|
368
|
-
for (let k = 1; k < need; k++) {
|
|
369
|
-
if ((bytes[i + k]! & 0xc0) !== 0x80) {
|
|
370
|
-
return { grapheme: "\uFFFD", next: i + 1 };
|
|
371
|
-
}
|
|
372
|
-
}
|
|
373
|
-
} else if (lead >= 0x80) {
|
|
374
|
-
return { grapheme: "\uFFFD", next: i + 1 };
|
|
375
|
-
}
|
|
376
|
-
const grapheme = new TextDecoder().decode(bytes.subarray(i, i + need));
|
|
377
|
-
return { grapheme, next: i + need };
|
|
378
|
-
}
|
package/src/tui/markdown.ts
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Markdown → terminal lines, via the vendored pi Markdown component.
|
|
3
|
-
*
|
|
4
|
-
* D19: the live UI is pi's renderer, which is built on `marked`. Our
|
|
5
|
-
* previous subset parser lived here; the function signature stayed so
|
|
6
|
-
* apps/render.ts and theming tests still call renderMarkdown(text, width,
|
|
7
|
-
* style). Style is mapped onto pi's MarkdownTheme in host.ts.
|
|
8
|
-
*/
|
|
9
|
-
import { renderMarkdownLines } from "./host.ts";
|
|
10
|
-
import { IDENTITY, type Style } from "./style.ts";
|
|
11
|
-
|
|
12
|
-
export { ANSI, IDENTITY } from "./style.ts";
|
|
13
|
-
export type { Style } from "./style.ts";
|
|
14
|
-
|
|
15
|
-
export function renderMarkdown(text: string, width: number, style: Style = IDENTITY): string[] {
|
|
16
|
-
return renderMarkdownLines(text, width, style);
|
|
17
|
-
}
|
package/src/tui/render.ts
DELETED
|
@@ -1,153 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Line-level differential renderer.
|
|
3
|
-
*
|
|
4
|
-
* Cell-level diffing would need a full screen buffer of character-plus-attrs
|
|
5
|
-
* per cell. Line-level diffing compares strings, and a terminal already
|
|
6
|
-
* paints in rows. Unchanged neighbours are skipped even when they sit
|
|
7
|
-
* between two changed rows -- the test that pins this would accept a
|
|
8
|
-
* contiguous first-changed..last-changed rewrite, and that rewrite is how
|
|
9
|
-
* a one-line spinner in a tall transcript becomes a full-region repaint.
|
|
10
|
-
*
|
|
11
|
-
* Main screen only. The live region sits at the bottom of the viewport;
|
|
12
|
-
* history stays in the terminal's own scrollback. No alt-screen, so we
|
|
13
|
-
* never own (and never have to reimplement) native scroll or search.
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
import { usableColumns, usableRows, type Terminal } from "./terminal.ts";
|
|
17
|
-
import type { Component } from "./text.ts";
|
|
18
|
-
import {
|
|
19
|
-
CR, LF, SYNC_BEGIN, SYNC_END, ERASE_DOWN, ERASE_LINE_TAIL,
|
|
20
|
-
cursorUp, cursorDown,
|
|
21
|
-
} from "./ansi.ts";
|
|
22
|
-
|
|
23
|
-
export class Renderer {
|
|
24
|
-
private readonly terminal: Terminal;
|
|
25
|
-
private previous: string[] = [];
|
|
26
|
-
/** Row within the live region where the hardware cursor sits, 0-based. */
|
|
27
|
-
private cursorRow = 0;
|
|
28
|
-
/**
|
|
29
|
-
* Overflow lines already written as scrollback. A count, not an identity:
|
|
30
|
-
* this matches paintStream, which only commits growth. Reset when the
|
|
31
|
-
* frame fits again so a later tall frame is not silently dropped.
|
|
32
|
-
*/
|
|
33
|
-
private flushed = 0;
|
|
34
|
-
|
|
35
|
-
constructor(terminal: Terminal) {
|
|
36
|
-
this.terminal = terminal;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/** Rows currently occupying the live region, after overflow was committed. */
|
|
40
|
-
get liveRowCount(): number {
|
|
41
|
-
return this.previous.length;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
render(root: Component): void {
|
|
45
|
-
const fitted = fitLive(
|
|
46
|
-
root.render(usableColumns(this.terminal.columns)),
|
|
47
|
-
usableRows(this.terminal.rows),
|
|
48
|
-
);
|
|
49
|
-
if (fitted.overflow.length > this.flushed) {
|
|
50
|
-
const fresh = fitted.overflow.slice(this.flushed);
|
|
51
|
-
this.eraseLive();
|
|
52
|
-
// Outside the sync pair, matching paintStream: these lines become
|
|
53
|
-
// real scrollback. Dropping them here is how L0–L2 vanished from a
|
|
54
|
-
// 5-row viewport even though fitLive had already split them off.
|
|
55
|
-
this.terminal.write(fresh.join("\n") + "\n");
|
|
56
|
-
this.flushed = fitted.overflow.length;
|
|
57
|
-
} else if (fitted.overflow.length === 0) {
|
|
58
|
-
this.flushed = 0;
|
|
59
|
-
}
|
|
60
|
-
const next = fitted.live;
|
|
61
|
-
const body = this.diff(next);
|
|
62
|
-
if (body === null) return;
|
|
63
|
-
// One write per frame. Two writes would let tmux flush the CSI 2026
|
|
64
|
-
// begin without the matching end, and the user would see the tear
|
|
65
|
-
// synchronized output exists to hide.
|
|
66
|
-
this.terminal.write(SYNC_BEGIN + body + SYNC_END);
|
|
67
|
-
this.previous = next;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
* Clear the live region so committed overflow is not painted on top of
|
|
72
|
-
* it. Same motion as Tui.dropLive; kept here so the generic render path
|
|
73
|
-
* (editor, select list) cannot forget it the way the first streaming
|
|
74
|
-
* pass did.
|
|
75
|
-
*/
|
|
76
|
-
private eraseLive(): void {
|
|
77
|
-
if (this.previous.length === 0) return;
|
|
78
|
-
let out = CR;
|
|
79
|
-
if (this.cursorRow > 0) out += cursorUp(this.cursorRow);
|
|
80
|
-
out += ERASE_DOWN;
|
|
81
|
-
this.terminal.write(out);
|
|
82
|
-
this.previous = [];
|
|
83
|
-
this.cursorRow = 0;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
private diff(next: string[]): string | null {
|
|
87
|
-
const prev = this.previous;
|
|
88
|
-
if (prev.length === next.length && prev.every((line, i) => line === next[i])) {
|
|
89
|
-
return null;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
if (prev.length === 0) {
|
|
93
|
-
this.cursorRow = Math.max(0, next.length - 1);
|
|
94
|
-
return next.join(LF);
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
let out = "";
|
|
98
|
-
let row = this.cursorRow;
|
|
99
|
-
|
|
100
|
-
const go = (target: number): void => {
|
|
101
|
-
out += CR;
|
|
102
|
-
if (target < row) out += cursorUp(row - target);
|
|
103
|
-
else if (target > row) out += cursorDown(target - row);
|
|
104
|
-
row = target;
|
|
105
|
-
};
|
|
106
|
-
|
|
107
|
-
const shared = Math.min(prev.length, next.length);
|
|
108
|
-
for (let i = 0; i < shared; i++) {
|
|
109
|
-
if (prev[i] === next[i]) continue;
|
|
110
|
-
go(i);
|
|
111
|
-
out += next[i]!;
|
|
112
|
-
out += ERASE_LINE_TAIL;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
if (next.length > prev.length) {
|
|
116
|
-
go(prev.length - 1);
|
|
117
|
-
for (let i = prev.length; i < next.length; i++) {
|
|
118
|
-
out += LF;
|
|
119
|
-
out += next[i]!;
|
|
120
|
-
row = i;
|
|
121
|
-
}
|
|
122
|
-
} else if (next.length < prev.length) {
|
|
123
|
-
go(next.length === 0 ? 0 : next.length);
|
|
124
|
-
out += ERASE_DOWN;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
if (next.length === 0) {
|
|
128
|
-
this.cursorRow = 0;
|
|
129
|
-
} else {
|
|
130
|
-
if (row !== next.length - 1) go(next.length - 1);
|
|
131
|
-
this.cursorRow = next.length - 1;
|
|
132
|
-
}
|
|
133
|
-
return out;
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
/**
|
|
138
|
-
* Split a frame so the live tail never exceeds the viewport.
|
|
139
|
-
*
|
|
140
|
-
* `diff` locates rows with cursorUp(row - target). A previous frame taller
|
|
141
|
-
* than the screen has its cursor clamped at row 0 by the terminal, and every
|
|
142
|
-
* later move is then off by the overflow. The head goes to the caller to
|
|
143
|
-
* commit as scrollback; we only keep the tail.
|
|
144
|
-
*/
|
|
145
|
-
export function fitLive(
|
|
146
|
-
lines: readonly string[],
|
|
147
|
-
maxRows: number,
|
|
148
|
-
): { overflow: string[]; live: string[] } {
|
|
149
|
-
if (maxRows <= 0) return { overflow: [...lines], live: [] };
|
|
150
|
-
if (lines.length <= maxRows) return { overflow: [], live: [...lines] };
|
|
151
|
-
const cut = lines.length - maxRows;
|
|
152
|
-
return { overflow: lines.slice(0, cut), live: lines.slice(cut) };
|
|
153
|
-
}
|