zames_pro 2.73.11 → 2.74.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 +24 -0
- package/README.md +13 -7
- package/dist/banner.js +71 -0
- package/dist/browser.js +84 -27
- package/dist/code-blocks.js +48 -0
- package/dist/commands.js +1 -0
- package/dist/config-live.js +53 -0
- package/dist/config.js +38 -0
- package/dist/goal.js +81 -0
- package/dist/i18n/common.js +31 -0
- package/dist/i18n/config.js +20 -0
- package/dist/i18n/deepseek.js +14 -0
- package/dist/i18n/help.js +22 -2
- package/dist/i18n/qwen.js +4 -0
- package/dist/i18n/self-review.js +8 -0
- package/dist/i18n/spinner.js +6 -0
- package/dist/index.js +478 -65
- package/dist/input/layout.js +19 -0
- package/dist/input/suggest.js +13 -0
- package/dist/input.js +14 -3
- package/dist/net-capture.js +18 -0
- package/dist/qwen-browser.js +43 -20
- package/dist/run-task.js +97 -4
- package/dist/theme.js +7 -1
- package/dist/tool-summary.js +83 -0
- package/dist/tui/ConfigMenu.js +304 -0
- package/dist/tui/ConfirmDialog.js +42 -0
- package/dist/tui/FullScreenApp.js +85 -0
- package/dist/tui/InputLine.js +285 -0
- package/dist/tui/LogView.js +48 -0
- package/dist/tui/LoginForm.js +83 -0
- package/dist/tui/SelectList.js +50 -0
- package/dist/tui/StatusBar.js +140 -0
- package/dist/tui/TextPrompt.js +41 -0
- package/dist/tui/TuiApp.js +28 -0
- package/dist/tui/TuiEditor.js +490 -0
- package/dist/tui/edit.js +297 -0
- package/dist/tui/keys.js +162 -0
- package/dist/tui/log-window.js +93 -0
- package/dist/tui/login-pure.js +20 -0
- package/dist/tui/run-config-menu.js +19 -0
- package/dist/tui/run-confirm.js +22 -0
- package/dist/tui/run-login.js +43 -0
- package/dist/tui/run-select.js +18 -0
- package/dist/tui/store.js +104 -0
- package/dist/tui/types.js +10 -0
- package/dist/tui/useMouseWheel.js +113 -0
- package/dist/tui/widgets.js +55 -0
- package/package.json +6 -3
- package/scripts/coverage-gate.mts +10 -0
- package/scripts/render-demo.mts +1 -1
- package/scripts/tui-probe.tsx +62 -0
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
// The TUI STATE STORE — the bridge between the imperative agent code and the
|
|
2
|
+
// React/Ink render tree.
|
|
3
|
+
//
|
|
4
|
+
// Why a store instead of React state:
|
|
5
|
+
// * the agent loop calls the UI from OUTSIDE React (`printAbove`, `toolCall`,
|
|
6
|
+
// `thinking`, ...), i.e. imperatively;
|
|
7
|
+
// * Ink re-renders from React state, which can only be set from inside a
|
|
8
|
+
// component or via a setter we own.
|
|
9
|
+
// So the imperative API writes into this store and the Ink tree subscribes
|
|
10
|
+
// via `useSyncExternalStore`. One direction (calls -> store -> render) means no
|
|
11
|
+
// conflict between the two worlds.
|
|
12
|
+
//
|
|
13
|
+
// The store keeps the WHOLE app state in one immutable snapshot object so
|
|
14
|
+
// `useSyncExternalStore` gets a stable reference between changes (a new object
|
|
15
|
+
// is produced only on an actual mutation).
|
|
16
|
+
import { initialEditState } from './edit.js';
|
|
17
|
+
function initialState() {
|
|
18
|
+
return {
|
|
19
|
+
log: [],
|
|
20
|
+
title: 'zames',
|
|
21
|
+
version: '',
|
|
22
|
+
workdir: '',
|
|
23
|
+
scroll: 0,
|
|
24
|
+
busy: false,
|
|
25
|
+
locked: false,
|
|
26
|
+
statusText: '',
|
|
27
|
+
sendState: '',
|
|
28
|
+
animBase: '',
|
|
29
|
+
animStart: 0,
|
|
30
|
+
pendingText: null,
|
|
31
|
+
edit: initialEditState(),
|
|
32
|
+
pastes: [],
|
|
33
|
+
attachments: [],
|
|
34
|
+
history: [],
|
|
35
|
+
prompt: '> ',
|
|
36
|
+
commands: [],
|
|
37
|
+
locale: 'ru',
|
|
38
|
+
contextText: null,
|
|
39
|
+
contextLevel: null,
|
|
40
|
+
queueLength: 0,
|
|
41
|
+
tasks: '',
|
|
42
|
+
provider: '',
|
|
43
|
+
mouseScroll: false,
|
|
44
|
+
thinkingEnabled: false,
|
|
45
|
+
searchEnabled: false,
|
|
46
|
+
statusLine: '',
|
|
47
|
+
codeBlocks: [],
|
|
48
|
+
codeFocus: -1,
|
|
49
|
+
columns: process.stdout.columns || 80,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* A minimal external store for the Ink tree.
|
|
54
|
+
*
|
|
55
|
+
* Mutations go through `set(partial)` which shallow-merges into a NEW snapshot
|
|
56
|
+
* object, so `useSyncExternalStore` sees a changed reference and re-renders.
|
|
57
|
+
* A render that changes nothing produces no new object and no repaint.
|
|
58
|
+
*/
|
|
59
|
+
export class TuiStore {
|
|
60
|
+
state = initialState();
|
|
61
|
+
listeners = new Set();
|
|
62
|
+
nextLogId = 1;
|
|
63
|
+
subscribe = (cb) => {
|
|
64
|
+
this.listeners.add(cb);
|
|
65
|
+
return () => this.listeners.delete(cb);
|
|
66
|
+
};
|
|
67
|
+
getSnapshot = () => this.state;
|
|
68
|
+
/** Shallow-merge a partial update and notify subscribers. */
|
|
69
|
+
set(partial) {
|
|
70
|
+
let changed = false;
|
|
71
|
+
for (const k of Object.keys(partial)) {
|
|
72
|
+
if (partial[k] !== this.state[k]) {
|
|
73
|
+
changed = true;
|
|
74
|
+
break;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
if (!changed)
|
|
78
|
+
return;
|
|
79
|
+
this.state = { ...this.state, ...partial };
|
|
80
|
+
this.emit();
|
|
81
|
+
}
|
|
82
|
+
/** Functional update over the whole snapshot. */
|
|
83
|
+
update(fn) {
|
|
84
|
+
this.set(fn(this.state));
|
|
85
|
+
}
|
|
86
|
+
/** Append one permanent log entry (an Ink `<Static>` item). */
|
|
87
|
+
pushLog(kind, text, raw = false) {
|
|
88
|
+
const entry = { id: this.nextLogId++, kind, text, raw };
|
|
89
|
+
this.state = { ...this.state, log: [...this.state.log, entry] };
|
|
90
|
+
this.emit();
|
|
91
|
+
}
|
|
92
|
+
/** Append several entries in one notification (cheaper than N pushes). */
|
|
93
|
+
pushLogs(entries) {
|
|
94
|
+
if (!entries.length)
|
|
95
|
+
return;
|
|
96
|
+
const withIds = entries.map((e) => ({ ...e, id: this.nextLogId++ }));
|
|
97
|
+
this.state = { ...this.state, log: [...this.state.log, ...withIds] };
|
|
98
|
+
this.emit();
|
|
99
|
+
}
|
|
100
|
+
emit() {
|
|
101
|
+
for (const cb of this.listeners)
|
|
102
|
+
cb();
|
|
103
|
+
}
|
|
104
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// The UI EDITOR interface — the shared surface of the legacy `LineEditor`
|
|
2
|
+
// (src/input.ts) and the new Ink-based `TuiEditor` (src/tui/TuiEditor.tsx).
|
|
3
|
+
//
|
|
4
|
+
// index.ts and run-task.ts talk to the UI ONLY through this interface, so the
|
|
5
|
+
// rendering engine (hand-rolled ANSI vs React/Ink) can be swapped without
|
|
6
|
+
// touching the agent code. Both classes implement every member below.
|
|
7
|
+
//
|
|
8
|
+
// Only the members the agent actually uses are listed; the concrete classes may
|
|
9
|
+
// expose more (internal render helpers, etc.).
|
|
10
|
+
export {};
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
// A hook that reads the terminal's MOUSE-WHEEL events and scrolls the log.
|
|
2
|
+
//
|
|
3
|
+
// Ink 8 does NOT parse mouse events (its `useInput` only handles keys), and it
|
|
4
|
+
// STRIPS unknown CSI sequences, so the wheel never reaches a component. To
|
|
5
|
+
// scroll the chat with the wheel we therefore:
|
|
6
|
+
// 1. enable SGR mouse reporting on the terminal (ESC[?1000h ESC[?1006h);
|
|
7
|
+
// 2. listen to the RAW stdin bytes ourselves (Ink's own listener ignores
|
|
8
|
+
// them, so there is no conflict).
|
|
9
|
+
//
|
|
10
|
+
// The SGR encoding is `ESC [ < Cb ; Cx ; Cy M` where Cb 64 = wheel up and
|
|
11
|
+
// 65 = wheel down (plus modifier bits). We only need Cb.
|
|
12
|
+
//
|
|
13
|
+
// ALTERNATE-SCROLL: while the app is in the ALTERNATE SCREEN (full-screen
|
|
14
|
+
// mode), terminals turn the mouse wheel into ARROW KEYS by default (DECSET
|
|
15
|
+
// 1007, "alternate scroll mode"). Ink parses those arrows as normal Up/Down,
|
|
16
|
+
// and the input editor at a buffer edge maps them to HISTORY — so the wheel
|
|
17
|
+
// scrolled the INPUT HISTORY instead of the log (the operator's bug).
|
|
18
|
+
//
|
|
19
|
+
// ORDER MATTERS and reporting is ALWAYS ON: some terminals only apply `1007l`
|
|
20
|
+
// while mouse reporting is enabled, so sending `1007l` alone (the previous
|
|
21
|
+
// behavior when ui.mouseScroll was off) was a no-op there and the wheel kept
|
|
22
|
+
// arriving as arrows. We now enable 1000/1006 unconditionally and disable 1007
|
|
23
|
+
// right after. Consequence: drag-select needs SHIFT (the standard TUI
|
|
24
|
+
// convention — Shift+drag bypasses the app), which is the trade-off for a
|
|
25
|
+
// working wheel. `enabled` only decides whether the wheel SCROLLS the log;
|
|
26
|
+
// the events are consumed either way so they never reach the editor.
|
|
27
|
+
import { useEffect } from 'react';
|
|
28
|
+
import { useStdin } from 'ink';
|
|
29
|
+
/** True when a chunk contains an SGR mouse wheel event; returns the delta. */
|
|
30
|
+
export function parseWheel(chunk) {
|
|
31
|
+
// Count wheel-ups (down) and wheel-downs (up) in the chunk.
|
|
32
|
+
let delta = 0;
|
|
33
|
+
const re = /\x1b\[<(\d+);\d+;\d+[Mm]/g;
|
|
34
|
+
let m;
|
|
35
|
+
while ((m = re.exec(chunk)) !== null) {
|
|
36
|
+
const cb = Number(m[1]);
|
|
37
|
+
// Button code 64 = wheel up (scroll back), 65 = wheel down (scroll fwd).
|
|
38
|
+
const button = cb & 0b11000011;
|
|
39
|
+
if (button === 64)
|
|
40
|
+
delta += 1;
|
|
41
|
+
else if (button === 65)
|
|
42
|
+
delta -= 1;
|
|
43
|
+
}
|
|
44
|
+
return delta;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* The part of an SGR mouse sequence at the END of `chunk` that is still
|
|
48
|
+
* INCOMPLETE (no `M`/`m` terminator yet), or '' when the chunk ends cleanly.
|
|
49
|
+
* A terminal may split one sequence across two `data` events; scanning each
|
|
50
|
+
* chunk alone dropped the wheel notch (the hook keeps this tail and prepends
|
|
51
|
+
* it to the next chunk).
|
|
52
|
+
*/
|
|
53
|
+
export function incompleteTail(chunk) {
|
|
54
|
+
const start = chunk.lastIndexOf('\u001b[<');
|
|
55
|
+
if (start === -1)
|
|
56
|
+
return '';
|
|
57
|
+
// Complete if an `M`/`m` follows the introducer before the chunk ends.
|
|
58
|
+
if (/[Mm]/.test(chunk.slice(start + 3)))
|
|
59
|
+
return '';
|
|
60
|
+
return chunk.slice(start);
|
|
61
|
+
}
|
|
62
|
+
const ESC = String.fromCharCode(27);
|
|
63
|
+
/**
|
|
64
|
+
* The terminal-mode sequences the hook writes. Exported PURE so a test can pin
|
|
65
|
+
* the exact bytes AND their order (reporting FIRST, then 1007 off — some
|
|
66
|
+
* terminals ignore `1007l` when mouse reporting is off, which is what made the
|
|
67
|
+
* wheel keep scrolling the input history).
|
|
68
|
+
*/
|
|
69
|
+
export function mouseModeSequences() {
|
|
70
|
+
const on =
|
|
71
|
+
// 1000 = button-event tracking, 1006 = SGR extended coords, 1007 = off.
|
|
72
|
+
ESC + '[?1000h' + ESC + '[?1006h' + ESC + '[?1007l';
|
|
73
|
+
const off = ESC + '[?1006l' + ESC + '[?1000l' + ESC + '[?1007h';
|
|
74
|
+
return { on, off };
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Subscribe to wheel events on the given terminal and call `onWheel(delta)`
|
|
78
|
+
* where delta > 0 means "scroll up / back". Enables SGR mouse reporting while
|
|
79
|
+
* mounted and disables alternate-scroll so the wheel is never turned into
|
|
80
|
+
* arrow keys.
|
|
81
|
+
*/
|
|
82
|
+
export function useMouseWheel(onWheel, enabled = true) {
|
|
83
|
+
const { stdin, isRawModeSupported } = useStdin();
|
|
84
|
+
useEffect(() => {
|
|
85
|
+
if (!isRawModeSupported)
|
|
86
|
+
return;
|
|
87
|
+
const seq = mouseModeSequences();
|
|
88
|
+
process.stdout.write(seq.on);
|
|
89
|
+
// A sequence may be split across two chunks; carry the incomplete tail.
|
|
90
|
+
let carry = '';
|
|
91
|
+
const onData = (data) => {
|
|
92
|
+
const s = typeof data === 'string' ? data : data.toString('utf-8');
|
|
93
|
+
const buf = carry + s;
|
|
94
|
+
carry = incompleteTail(buf);
|
|
95
|
+
const scan = carry ? buf.slice(0, buf.length - carry.length) : buf;
|
|
96
|
+
if (!scan.includes('\u001b[<'))
|
|
97
|
+
return;
|
|
98
|
+
// `enabled` decides whether the wheel SCROLLS; the event is consumed
|
|
99
|
+
// either way, so it never falls through to the editor as an arrow.
|
|
100
|
+
if (!enabled)
|
|
101
|
+
return;
|
|
102
|
+
const delta = parseWheel(scan);
|
|
103
|
+
if (delta !== 0)
|
|
104
|
+
onWheel(delta);
|
|
105
|
+
};
|
|
106
|
+
stdin.on('data', onData);
|
|
107
|
+
return () => {
|
|
108
|
+
stdin.removeListener('data', onData);
|
|
109
|
+
// Disable mouse reporting on unmount so normal selection works again.
|
|
110
|
+
process.stdout.write(seq.off);
|
|
111
|
+
};
|
|
112
|
+
}, [stdin, isRawModeSupported, onWheel, enabled]);
|
|
113
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
// ---- palette (kept in one place so every menu is consistent) ----
|
|
4
|
+
export const UI = {
|
|
5
|
+
accent: '#c8b06a',
|
|
6
|
+
accentDim: '#7a6b45',
|
|
7
|
+
text: '#cfc9b0',
|
|
8
|
+
dim: '#5b616e',
|
|
9
|
+
border: '#4a5058',
|
|
10
|
+
on: '#6fd0b0',
|
|
11
|
+
off: '#5b616e',
|
|
12
|
+
ok: '#a9c08c',
|
|
13
|
+
warn: '#c9a86a',
|
|
14
|
+
err: '#c98a80',
|
|
15
|
+
system: '#808896',
|
|
16
|
+
title: '#9fb8d8',
|
|
17
|
+
};
|
|
18
|
+
/** `[x]` / `[ ]` — a checkbox for a boolean value. */
|
|
19
|
+
export function Checkbox({ checked, focused, }) {
|
|
20
|
+
return (_jsx(Text, { color: checked ? UI.on : UI.off, bold: focused, children: checked ? '[x]' : '[ ]' }));
|
|
21
|
+
}
|
|
22
|
+
/** `‹ value ›` — an inline SELECT for an enum value. The arrows hint that
|
|
23
|
+
* ←/→ (or Space) cycle the option. */
|
|
24
|
+
export function Select({ value, focused, }) {
|
|
25
|
+
return (_jsxs(Text, { children: [_jsx(Text, { color: UI.off, children: '‹ ' }), _jsx(Text, { color: focused ? UI.accent : UI.text, bold: focused, children: value }), _jsx(Text, { color: UI.off, children: ' ›' })] }));
|
|
26
|
+
}
|
|
27
|
+
/** A framed panel (rounded border) with an optional title bar and footer. */
|
|
28
|
+
export function Panel({ title, subtitle, footer, width, children, }) {
|
|
29
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: UI.border, paddingX: 1, width: width, children: [title ? (_jsxs(Box, { justifyContent: "space-between", children: [_jsx(Text, { color: UI.title, bold: true, children: title }), subtitle ? _jsx(Text, { color: UI.dim, children: subtitle }) : null] })) : null, title ? _jsx(Text, { color: "#3a3f4b", children: '─'.repeat(40) }) : null, children, footer ? footer : null] }));
|
|
30
|
+
}
|
|
31
|
+
/** A single hint chip: `↑/↓ select`. */
|
|
32
|
+
export function Hint({ keys, label, }) {
|
|
33
|
+
return (_jsxs(Text, { children: [_jsx(Text, { color: UI.accent, children: keys }), _jsx(Text, { color: UI.dim, children: ' ' + label })] }));
|
|
34
|
+
}
|
|
35
|
+
/** A row of hint chips separated by ` · `. */
|
|
36
|
+
export function HintBar({ hints, }) {
|
|
37
|
+
return (_jsx(Text, { children: hints.map(([k, l], i) => (_jsxs(Text, { children: [i > 0 ? _jsx(Text, { color: UI.dim, children: ' · ' }) : null, _jsx(Text, { color: UI.accent, children: k }), _jsx(Text, { color: UI.dim, children: ' ' + l })] }, i))) }));
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* A CODE BLOCK with a syntax-highlighted body and a copy affordance.
|
|
41
|
+
*
|
|
42
|
+
* The agent's answers are printed as pre-rendered ANSI (markdown.ts already
|
|
43
|
+
* highlights with cli-highlight), so the live log does NOT use this component —
|
|
44
|
+
* it exists for menus/panels that show a snippet inline (and for a future
|
|
45
|
+
* structured log). Copying is a KEY, not a button: a terminal has no pointer.
|
|
46
|
+
* Pass `onCopy` and show `hint` so the operator learns the binding.
|
|
47
|
+
*/
|
|
48
|
+
export function CodeBlock({ code, lang, highlighted, width, hint, focused, onCopy, }) {
|
|
49
|
+
// React cannot call `onCopy` from a keydown here (Ink has no per-element
|
|
50
|
+
// focus ring by default); the caller wires the key and passes `focused` to
|
|
51
|
+
// show which block it will copy. `onCopy` is kept so a future focus manager
|
|
52
|
+
// can invoke it without changing the call sites.
|
|
53
|
+
void onCopy;
|
|
54
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: focused ? UI.accent : UI.border, paddingX: 1, width: width, children: [_jsxs(Box, { justifyContent: "space-between", children: [_jsx(Text, { color: UI.dim, children: lang || 'code' }), hint ? _jsx(Text, { color: UI.accent, children: hint }) : null] }), (highlighted || code).split(String.fromCharCode(10)).map((line, i) => (_jsx(Text, { children: line || ' ' }, i)))] }));
|
|
55
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zames_pro",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.74.0",
|
|
4
4
|
"description": "Terminal coding agent that drives a real browser to chat with DeepSeek, Qwen and other web LLMs — no API key required. Reads and edits files, runs commands, commits to git, like Claude Code / Codex CLI.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -66,16 +66,18 @@
|
|
|
66
66
|
],
|
|
67
67
|
"license": "MIT",
|
|
68
68
|
"engines": {
|
|
69
|
-
"node": ">=
|
|
69
|
+
"node": ">=22"
|
|
70
70
|
},
|
|
71
71
|
"packageManager": "npm@11.19.0",
|
|
72
72
|
"dependencies": {
|
|
73
73
|
"@modelcontextprotocol/sdk": "^1.30.1",
|
|
74
74
|
"chalk": "^5.3.0",
|
|
75
75
|
"cli-highlight": "^2.1.11",
|
|
76
|
+
"ink": "^8.0.0",
|
|
76
77
|
"markdansi": "^0.3.4",
|
|
77
78
|
"ora": "^8.0.1",
|
|
78
|
-
"playwright": "^1.48.0"
|
|
79
|
+
"playwright": "^1.48.0",
|
|
80
|
+
"react": "^19.3.0"
|
|
79
81
|
},
|
|
80
82
|
"publishConfig": {
|
|
81
83
|
"access": "public"
|
|
@@ -91,6 +93,7 @@
|
|
|
91
93
|
"devDependencies": {
|
|
92
94
|
"@eslint/js": "^9.39.5",
|
|
93
95
|
"@types/node": "^26.6.2",
|
|
96
|
+
"@types/react": "^19.3.0",
|
|
94
97
|
"eslint": "^9.39.5",
|
|
95
98
|
"eslint-config-prettier": "^10.1.8",
|
|
96
99
|
"globals": "^17.12.0",
|
|
@@ -39,6 +39,16 @@ const FLOORS = {
|
|
|
39
39
|
// C3: the pure /config subcommand dispatcher — silent routing regressions
|
|
40
40
|
// (a wrong subcommand branch) are invisible without tests.
|
|
41
41
|
'config-command.ts': 85,
|
|
42
|
+
// The Ink TUI's PURE halves: the editor state transitions (tui/edit.ts) and
|
|
43
|
+
// the key dispatcher (tui/keys.ts). A silent regression there breaks the
|
|
44
|
+
// operator's editing without any UI test catching it. NOTE: the gate keys on
|
|
45
|
+
// the BASENAME (its regex has no slash), so these are 'edit.ts' / 'keys.ts' /
|
|
46
|
+
// 'store.ts' — unique in the tree today.
|
|
47
|
+
'edit.ts': 85,
|
|
48
|
+
'keys.ts': 85,
|
|
49
|
+
'store.ts': 85,
|
|
50
|
+
'banner.ts': 85,
|
|
51
|
+
'tool-summary.ts': 85,
|
|
42
52
|
// N12: web.ts holds the SSRF guard and the DuckDuckGo/HTML parsers — a
|
|
43
53
|
// regression there is silent. Floors sit below the current values.
|
|
44
54
|
'web.ts': 40,
|
package/scripts/render-demo.mts
CHANGED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// A tiny Ink harness that mounts the REAL FullScreenApp with a scripted store,
|
|
2
|
+
// so a pty run can assert the on-screen geometry (input box height, cursor
|
|
3
|
+
// row) instead of grepping escape bytes. Run under `script -qec` and feed keys
|
|
4
|
+
// on stdin; the terminal output is replayed by the VT100 emulator in the test.
|
|
5
|
+
//
|
|
6
|
+
// NOT shipped: a dev probe only (lives in scripts/, not in `files`).
|
|
7
|
+
|
|
8
|
+
import React from 'react'
|
|
9
|
+
import fs from 'node:fs'
|
|
10
|
+
import { render } from 'ink'
|
|
11
|
+
import { TuiStore } from '../src/tui/store.js'
|
|
12
|
+
import { FullScreenApp } from '../src/tui/FullScreenApp.js'
|
|
13
|
+
import { TuiApp } from '../src/tui/TuiApp.js'
|
|
14
|
+
import { initialEditState } from '../src/tui/edit.js'
|
|
15
|
+
|
|
16
|
+
const store = new TuiStore()
|
|
17
|
+
store.set({
|
|
18
|
+
title: 'probe',
|
|
19
|
+
version: '0.0.0',
|
|
20
|
+
workdir: '/tmp',
|
|
21
|
+
prompt: '> ',
|
|
22
|
+
locale: 'en',
|
|
23
|
+
commands: [],
|
|
24
|
+
edit: { ...initialEditState(), buf: process.argv[2] ?? '' },
|
|
25
|
+
// A copyable code block, so a pty run can press Ctrl+Y and assert the copy
|
|
26
|
+
// callback fired (the raw block, not the highlighted render).
|
|
27
|
+
codeBlocks: [{ lang: 'ts', text: 'const a = 1', index: 1 }],
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
// A few log rows so the viewport is not empty.
|
|
31
|
+
store.pushLog('system', 'log line one')
|
|
32
|
+
store.pushLog('system', 'log line two')
|
|
33
|
+
|
|
34
|
+
const COPY_LOG = process.env.PROBE_COPY_LOG || ''
|
|
35
|
+
const INLINE = process.env.PROBE_INLINE === '1'
|
|
36
|
+
|
|
37
|
+
const inputProps = {
|
|
38
|
+
onSubmit: () => {},
|
|
39
|
+
onEscape: () => {},
|
|
40
|
+
onCtrlC: () => {},
|
|
41
|
+
onCopyCode: () => {
|
|
42
|
+
if (COPY_LOG) fs.appendFileSync(COPY_LOG, 'copy' + String.fromCharCode(10))
|
|
43
|
+
},
|
|
44
|
+
onAttach: () => null,
|
|
45
|
+
onClipboard: () => Promise.resolve(null),
|
|
46
|
+
onHistoryChange: () => {},
|
|
47
|
+
onClearScreen: () => {},
|
|
48
|
+
attachments: [],
|
|
49
|
+
onPastesChange: () => {},
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
render(
|
|
53
|
+
INLINE ? (
|
|
54
|
+
<TuiApp store={store} inputProps={inputProps} />
|
|
55
|
+
) : (
|
|
56
|
+
<FullScreenApp store={store} inputProps={inputProps} />
|
|
57
|
+
),
|
|
58
|
+
{ exitOnCtrlC: false, patchConsole: false, alternateScreen: !INLINE },
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
// Keep the app alive until killed.
|
|
62
|
+
setInterval(() => {}, 1 << 30)
|