zames_pro 2.74.0 → 2.75.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +38 -0
- package/README.md +24 -0
- package/dist/attachments.js +6 -1
- package/dist/commands.js +0 -1
- package/dist/config.js +7 -7
- package/dist/i18n/common.js +0 -8
- package/dist/i18n/config.js +0 -1
- package/dist/i18n/help.js +8 -4
- package/dist/i18n/qwen.js +0 -4
- package/dist/i18n/self-review.js +2 -18
- package/dist/i18n/spinner.js +5 -0
- package/dist/index.js +30 -43
- package/dist/input/suggest.js +19 -4
- package/dist/theme.js +9 -0
- package/dist/tui/ConfigMenu.js +44 -11
- package/dist/tui/FullScreenApp.js +44 -11
- package/dist/tui/InputLine.js +52 -19
- package/dist/tui/SelectList.js +6 -2
- package/dist/tui/StatusBar.js +66 -36
- package/dist/tui/TuiApp.js +3 -2
- package/dist/tui/TuiEditor.js +44 -4
- package/dist/tui/edit.js +21 -1
- package/dist/tui/keys.js +46 -2
- package/dist/tui/store.js +1 -1
- package/dist/tui/useMouseWheel.js +80 -29
- package/dist/tui/widgets.js +6 -1
- package/package.json +2 -1
- package/scripts/ctd-live-driver.mts +51 -0
- package/scripts/ctd-live.tsx +33 -0
- package/scripts/escd-live-driver.mts +37 -0
- package/scripts/inline-status-driver.mts +31 -0
- package/scripts/inline-status.tsx +33 -0
- package/scripts/mouse-churn.mts +34 -0
- package/scripts/qwen-models-smoke.mts +117 -0
- package/scripts/ru-shot.mts +20 -0
- package/scripts/tui-comp-shot.mts +63 -0
- package/scripts/tui-components.tsx +62 -0
- package/scripts/tui-config-driver.mts +82 -0
- package/scripts/tui-config-probe.tsx +74 -0
- package/scripts/tui-edit-driver.mts +100 -0
- package/scripts/tui-edit-probe.tsx +38 -0
- package/scripts/tui-editor-probe.tsx +24 -0
- package/scripts/tui-input-driver.mts +54 -0
- package/scripts/tui-input-shot.tsx +59 -0
- package/scripts/tui-log-driver.mts +35 -0
- package/scripts/tui-log-shot.tsx +48 -0
- package/scripts/tui-longpaste-driver.mts +56 -0
- package/scripts/tui-longpaste.tsx +35 -0
- package/scripts/tui-manyselect-shot.mts +34 -0
- package/scripts/tui-manyselect.tsx +17 -0
- package/scripts/tui-mouse-churn-probe.tsx +28 -0
- package/scripts/tui-paste-driver.mts +88 -0
- package/scripts/tui-paste-kinds.tsx +76 -0
- package/scripts/tui-pastetest.mts +38 -0
- package/scripts/tui-repro.tsx +64 -0
- package/scripts/tui-resize.mts +70 -0
- package/scripts/tui-scroll-driver.mts +53 -0
- package/scripts/tui-scroll-probe.tsx +46 -0
- package/scripts/tui-shot.mts +106 -0
- package/scripts/tui-status-driver.mts +24 -0
- package/scripts/tui-status-shot.tsx +27 -0
- package/scripts/tui-submit.tsx +57 -0
- package/scripts/tui-wheel-driver.mts +61 -0
- package/scripts/tui-wheel-probe.tsx +48 -0
- package/scripts/tui-widgets-driver.mts +20 -0
- package/scripts/tui-widgets.tsx +44 -0
|
@@ -13,18 +13,32 @@
|
|
|
13
13
|
// ALTERNATE-SCROLL: while the app is in the ALTERNATE SCREEN (full-screen
|
|
14
14
|
// mode), terminals turn the mouse wheel into ARROW KEYS by default (DECSET
|
|
15
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
|
|
16
|
+
// and the input editor at a buffer edge maps them to HISTORY -- so the wheel
|
|
17
17
|
// scrolled the INPUT HISTORY instead of the log (the operator's bug).
|
|
18
18
|
//
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
//
|
|
25
|
-
//
|
|
26
|
-
//
|
|
27
|
-
|
|
19
|
+
// Two clear, MUTUALLY EXCLUSIVE modes (a terminal cannot do both at once):
|
|
20
|
+
// - SELECT (ui.mouseScroll=false): mouse reporting stays OFF, so the
|
|
21
|
+
// terminal owns the mouse -- drag-select works natively and the wheel is
|
|
22
|
+
// inert (no arrows, no history jump).
|
|
23
|
+
// - SCROLL (ui.mouseScroll=true): mouse reporting ON, so the wheel arrives
|
|
24
|
+
// as SGR events and scrolls the log; drag-select then needs SHIFT.
|
|
25
|
+
//
|
|
26
|
+
// THE CRITICAL DETAIL (this bug kept coming back): the mode sequences must be
|
|
27
|
+
// written ONCE per mode change, NEVER per render. An earlier version listed
|
|
28
|
+
// `onWheel` in the effect deps, but callers pass an inline arrow, so the
|
|
29
|
+
// effect re-ran on EVERY render (the status spinner re-renders constantly) --
|
|
30
|
+
// each cleanup wrote `1007h` (alternate-scroll ON) before the next setup wrote
|
|
31
|
+
// `1007l`. A wheel notch that landed in that window became an ARROW and
|
|
32
|
+
// scrolled the input history: the intermittent "either scroll or selection"
|
|
33
|
+
// behaviour. The effect therefore depends ONLY on the mode + stdin, and the
|
|
34
|
+
// callback is read from a ref (set in its own effect) so a new closure never tears
|
|
35
|
+
// the terminal mode down.
|
|
36
|
+
//
|
|
37
|
+
// ORDER MATTERS: some terminals only apply `1007l` while mouse reporting is
|
|
38
|
+
// enabled, so we enable 1000/1006 FIRST, then disable 1007. In SELECT mode we
|
|
39
|
+
// actually want reporting off, so we send the on/off pair ONCE (the 1007l then
|
|
40
|
+
// "sticks") and then leave reporting off.
|
|
41
|
+
import { useEffect, useRef } from 'react';
|
|
28
42
|
import { useStdin } from 'ink';
|
|
29
43
|
/** True when a chunk contains an SGR mouse wheel event; returns the delta. */
|
|
30
44
|
export function parseWheel(chunk) {
|
|
@@ -61,30 +75,64 @@ export function incompleteTail(chunk) {
|
|
|
61
75
|
}
|
|
62
76
|
const ESC = String.fromCharCode(27);
|
|
63
77
|
/**
|
|
64
|
-
* The terminal-mode sequences the hook writes
|
|
65
|
-
* the exact bytes AND their order
|
|
66
|
-
*
|
|
67
|
-
*
|
|
78
|
+
* The terminal-mode sequences the hook writes for a given mode. Exported PURE
|
|
79
|
+
* so a test can pin the exact bytes AND their order:
|
|
80
|
+
* - SELECT (enabled=false): NO `1000h/1006h` in the STEADY state, `on`
|
|
81
|
+
* only applies `1007l` (the one-time prime), and `off` guarantees the
|
|
82
|
+
* reporting bits are clear on unmount;
|
|
83
|
+
* - SCROLL (enabled=true): `1000h 1006h 1007l` in the STEADY state.
|
|
68
84
|
*/
|
|
69
|
-
export function mouseModeSequences() {
|
|
70
|
-
|
|
71
|
-
//
|
|
72
|
-
|
|
73
|
-
const
|
|
74
|
-
|
|
85
|
+
export function mouseModeSequences(enabled) {
|
|
86
|
+
// 1000 = button-event tracking, 1006 = SGR extended coords, 1007 off always
|
|
87
|
+
// (being in alt-screen otherwise turns the wheel into arrows => history).
|
|
88
|
+
const reportingOn = ESC + '[?1000h' + ESC + '[?1006h';
|
|
89
|
+
const reportingOff = ESC + '[?1006l' + ESC + '[?1000l';
|
|
90
|
+
const alternateScrollOff = ESC + '[?1007l';
|
|
91
|
+
const alternateScrollOn = ESC + '[?1007h';
|
|
92
|
+
if (enabled) {
|
|
93
|
+
return {
|
|
94
|
+
on: reportingOn + alternateScrollOff,
|
|
95
|
+
off: reportingOff + alternateScrollOn,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
// SELECT: the on/off pair is applied ONCE at mount so the 1007l sticks even
|
|
99
|
+
// though reporting is then off (some terminals ignore 1007l while reporting
|
|
100
|
+
// is off). After the prime the steady state has reporting OFF, and the
|
|
101
|
+
// listener is not bound, so nothing consumes the native selection.
|
|
102
|
+
return {
|
|
103
|
+
on: reportingOn + alternateScrollOff + reportingOff,
|
|
104
|
+
off: reportingOff + alternateScrollOn,
|
|
105
|
+
};
|
|
75
106
|
}
|
|
76
107
|
/**
|
|
77
108
|
* Subscribe to wheel events on the given terminal and call `onWheel(delta)`
|
|
78
|
-
* where delta > 0 means "scroll up / back".
|
|
79
|
-
*
|
|
80
|
-
*
|
|
109
|
+
* where delta > 0 means "scroll up / back".
|
|
110
|
+
*
|
|
111
|
+
* Two mutually exclusive modes:
|
|
112
|
+
* - `enabled=false` (SELECT): mouse reporting stays OFF, native drag-select
|
|
113
|
+
* works, the wheel is inert. The listener is still bound so an accidental
|
|
114
|
+
* SGR byte from a previous mode never leaks into the editor as an arrow.
|
|
115
|
+
* - `enabled=true` (SCROLL): reporting is on, the wheel scrolls the log
|
|
116
|
+
* (via `onWheel`), drag-select needs Shift.
|
|
117
|
+
*
|
|
118
|
+
* The terminal mode is written ONCE per `enabled` change. The wheel listener
|
|
119
|
+
* is bound once and reads the latest `onWheel` from a ref, so an inline
|
|
120
|
+
* callback (a new identity every render) does NOT re-run the effect -- a
|
|
121
|
+
* re-run would momentarily re-enable alternate-scroll and let a wheel notch
|
|
122
|
+
* scroll the input history.
|
|
81
123
|
*/
|
|
82
|
-
export function useMouseWheel(onWheel, enabled =
|
|
124
|
+
export function useMouseWheel(onWheel, enabled = false) {
|
|
83
125
|
const { stdin, isRawModeSupported } = useStdin();
|
|
126
|
+
// The latest callback, without making the mode effect depend on its
|
|
127
|
+
// identity (callers pass an inline arrow).
|
|
128
|
+
const onWheelRef = useRef(onWheel);
|
|
129
|
+
useEffect(() => {
|
|
130
|
+
onWheelRef.current = onWheel;
|
|
131
|
+
}, [onWheel]);
|
|
84
132
|
useEffect(() => {
|
|
85
133
|
if (!isRawModeSupported)
|
|
86
134
|
return;
|
|
87
|
-
const seq = mouseModeSequences();
|
|
135
|
+
const seq = mouseModeSequences(enabled);
|
|
88
136
|
process.stdout.write(seq.on);
|
|
89
137
|
// A sequence may be split across two chunks; carry the incomplete tail.
|
|
90
138
|
let carry = '';
|
|
@@ -95,13 +143,14 @@ export function useMouseWheel(onWheel, enabled = true) {
|
|
|
95
143
|
const scan = carry ? buf.slice(0, buf.length - carry.length) : buf;
|
|
96
144
|
if (!scan.includes('\u001b[<'))
|
|
97
145
|
return;
|
|
98
|
-
//
|
|
99
|
-
// either
|
|
146
|
+
// SELECT mode ignores the wheel entirely (the event never reaches the
|
|
147
|
+
// editor either -- there is no reporting to generate it in the first
|
|
148
|
+
// place, but a stale SGR byte from a previous mode is swallowed here).
|
|
100
149
|
if (!enabled)
|
|
101
150
|
return;
|
|
102
151
|
const delta = parseWheel(scan);
|
|
103
152
|
if (delta !== 0)
|
|
104
|
-
|
|
153
|
+
onWheelRef.current(delta);
|
|
105
154
|
};
|
|
106
155
|
stdin.on('data', onData);
|
|
107
156
|
return () => {
|
|
@@ -109,5 +158,7 @@ export function useMouseWheel(onWheel, enabled = true) {
|
|
|
109
158
|
// Disable mouse reporting on unmount so normal selection works again.
|
|
110
159
|
process.stdout.write(seq.off);
|
|
111
160
|
};
|
|
112
|
-
|
|
161
|
+
// `onWheel` is deliberately NOT a dependency: it is read via the ref so an
|
|
162
|
+
// inline callback does not tear the terminal mode down on every render.
|
|
163
|
+
}, [stdin, isRawModeSupported, enabled]);
|
|
113
164
|
}
|
package/dist/tui/widgets.js
CHANGED
|
@@ -26,7 +26,12 @@ export function Select({ value, focused, }) {
|
|
|
26
26
|
}
|
|
27
27
|
/** A framed panel (rounded border) with an optional title bar and footer. */
|
|
28
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(
|
|
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(panelInnerCols(width)) })) : null, children, footer ? footer : null] }));
|
|
30
|
+
}
|
|
31
|
+
/** Inner width (minus border + paddingX) the panel divider should span. */
|
|
32
|
+
function panelInnerCols(width) {
|
|
33
|
+
const cols = width || process.stdout.columns || 80;
|
|
34
|
+
return Math.max(1, cols - 6);
|
|
30
35
|
}
|
|
31
36
|
/** A single hint chip: `↑/↓ select`. */
|
|
32
37
|
export function Hint({ keys, label, }) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zames_pro",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.75.1",
|
|
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": {
|
|
@@ -34,6 +34,7 @@
|
|
|
34
34
|
"test:watch": "tsx --test --watch test/*.test.ts",
|
|
35
35
|
"self-smoke": "tsx scripts/self-smoke.mts",
|
|
36
36
|
"qwen-smoke": "tsx scripts/qwen-smoke.mts",
|
|
37
|
+
"qwen-models-smoke": "tsx scripts/qwen-models-smoke.mts",
|
|
37
38
|
"render-demo": "tsx scripts/render-demo.mts",
|
|
38
39
|
"render-how": "tsx scripts/render-how-it-works.mts",
|
|
39
40
|
"build:readme": "tsx scripts/build-readme.mts",
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// Drive the live-like probe: type a seed, then send Ctrl+Delete in EVERY
|
|
2
|
+
// encoding a terminal might use, and print the raw bytes + the buffer.
|
|
3
|
+
import { spawn } from 'node:child_process'
|
|
4
|
+
import fs from 'node:fs'
|
|
5
|
+
import path from 'node:path'
|
|
6
|
+
|
|
7
|
+
const ESC = String.fromCharCode(27)
|
|
8
|
+
const LOG = '/tmp/ctd-live.log'
|
|
9
|
+
try {
|
|
10
|
+
fs.unlinkSync(LOG)
|
|
11
|
+
} catch {
|
|
12
|
+
/* ignore */
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const child = spawn(
|
|
16
|
+
'script',
|
|
17
|
+
['-qec', 'npx tsx scripts/ctd-live.tsx', '/dev/null'],
|
|
18
|
+
{
|
|
19
|
+
cwd: path.resolve('.'),
|
|
20
|
+
env: { ...process.env, COLUMNS: '80', LINES: '24', PROBE_CTD_LOG: LOG },
|
|
21
|
+
},
|
|
22
|
+
)
|
|
23
|
+
child.stdout.on('data', () => {})
|
|
24
|
+
child.stderr.on('data', () => {})
|
|
25
|
+
|
|
26
|
+
const seq = process.argv[2] || 'xterm'
|
|
27
|
+
const enc: Record<string, string> = {
|
|
28
|
+
xterm: ESC + '[3;5~',
|
|
29
|
+
kitty: ESC + '[3;5u',
|
|
30
|
+
vt: ESC + '[3;5:1~',
|
|
31
|
+
}
|
|
32
|
+
const bytes = enc[seq] ?? seq
|
|
33
|
+
|
|
34
|
+
setTimeout(() => {
|
|
35
|
+
child.stdin.write('foo bar baz') // seed
|
|
36
|
+
setTimeout(() => {
|
|
37
|
+
child.stdin.write(bytes) // the Ctrl+Delete encoding
|
|
38
|
+
setTimeout(() => {
|
|
39
|
+
const log = fs.readFileSync(LOG, 'utf-8')
|
|
40
|
+
process.stdout.write('=== ' + seq + ' ===' + String.fromCharCode(10))
|
|
41
|
+
process.stdout.write(
|
|
42
|
+
log
|
|
43
|
+
.split(String.fromCharCode(10))
|
|
44
|
+
.filter((l) => l.startsWith('RAW') || l.startsWith('BUF'))
|
|
45
|
+
.join(String.fromCharCode(10)) + String.fromCharCode(10),
|
|
46
|
+
)
|
|
47
|
+
child.kill('SIGTERM')
|
|
48
|
+
}, 500)
|
|
49
|
+
}, 400)
|
|
50
|
+
}, 5000)
|
|
51
|
+
child.on('close', () => process.exit(0))
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// Minimal LIVE-like probe: the REAL TuiEditor exactly as index.ts builds it
|
|
2
|
+
// (default options), plus a raw-stdin TAP that logs every byte the terminal
|
|
3
|
+
// sends, so we can see EXACTLY what Ctrl+Delete produces on this terminal.
|
|
4
|
+
import fs from 'node:fs'
|
|
5
|
+
import { TuiEditor } from '../src/tui/TuiEditor.js'
|
|
6
|
+
|
|
7
|
+
const LOG = process.env.PROBE_CTD_LOG || '/tmp/ctd-live.log'
|
|
8
|
+
fs.writeFileSync(LOG, '')
|
|
9
|
+
const w = (s: string): void => {
|
|
10
|
+
fs.appendFileSync(LOG, s + String.fromCharCode(10))
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const ed = new TuiEditor({
|
|
14
|
+
prompt: '❯ › ',
|
|
15
|
+
commands: [],
|
|
16
|
+
locale: 'ru',
|
|
17
|
+
alternateScreen: true,
|
|
18
|
+
})
|
|
19
|
+
ed.onSubmit = () => {}
|
|
20
|
+
ed.onAttach = () => null
|
|
21
|
+
ed.onClipboard = () => Promise.resolve(null)
|
|
22
|
+
ed.start()
|
|
23
|
+
|
|
24
|
+
// Tap raw stdin: hex-dump every chunk so we see the exact bytes.
|
|
25
|
+
process.stdin.on('data', (d: Buffer) => {
|
|
26
|
+
w('RAW ' + d.toString('hex') + ' ' + JSON.stringify(d.toString('utf-8')))
|
|
27
|
+
})
|
|
28
|
+
// Log the buffer after every change.
|
|
29
|
+
ed.store.subscribe(() => {
|
|
30
|
+
const s = ed.store.getSnapshot().edit
|
|
31
|
+
w('BUF ' + JSON.stringify(s.buf) + ' cur=' + s.cursor)
|
|
32
|
+
})
|
|
33
|
+
setInterval(() => {}, 1 << 30)
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// Send `ESC d` (what VSCode sends for Ctrl+Delete) to the REAL TuiEditor and
|
|
2
|
+
// print the resulting buffer.
|
|
3
|
+
import { spawn } from 'node:child_process'
|
|
4
|
+
import fs from 'node:fs'
|
|
5
|
+
import path from 'node:path'
|
|
6
|
+
const ESC = String.fromCharCode(27)
|
|
7
|
+
const LOG = '/tmp/ctd-live.log'
|
|
8
|
+
try {
|
|
9
|
+
fs.unlinkSync(LOG)
|
|
10
|
+
} catch {}
|
|
11
|
+
const child = spawn(
|
|
12
|
+
'script',
|
|
13
|
+
['-qec', 'npx tsx scripts/ctd-live.tsx', '/dev/null'],
|
|
14
|
+
{
|
|
15
|
+
cwd: path.resolve('.'),
|
|
16
|
+
env: { ...process.env, COLUMNS: '80', LINES: '24', PROBE_CTD_LOG: LOG },
|
|
17
|
+
},
|
|
18
|
+
)
|
|
19
|
+
child.stdout.on('data', () => {})
|
|
20
|
+
child.stderr.on('data', () => {})
|
|
21
|
+
setTimeout(() => {
|
|
22
|
+
child.stdin.write('foo bar baz')
|
|
23
|
+
setTimeout(() => {
|
|
24
|
+
child.stdin.write(ESC + 'd')
|
|
25
|
+
setTimeout(() => {
|
|
26
|
+
const log = fs.readFileSync(LOG, 'utf-8')
|
|
27
|
+
process.stdout.write(
|
|
28
|
+
log
|
|
29
|
+
.split(String.fromCharCode(10))
|
|
30
|
+
.filter((l) => l.startsWith('RAW') || l.startsWith('BUF'))
|
|
31
|
+
.join(String.fromCharCode(10)) + String.fromCharCode(10),
|
|
32
|
+
)
|
|
33
|
+
child.kill('SIGTERM')
|
|
34
|
+
}, 500)
|
|
35
|
+
}, 400)
|
|
36
|
+
}, 5000)
|
|
37
|
+
child.on('close', () => process.exit(0))
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import { Term } from '../test/vt100.mts'
|
|
4
|
+
const NL = String.fromCharCode(10)
|
|
5
|
+
const term = new Term(12, 60)
|
|
6
|
+
const child = spawn(
|
|
7
|
+
'script',
|
|
8
|
+
['-qec', 'npx tsx scripts/inline-status.tsx', '/dev/null'],
|
|
9
|
+
{
|
|
10
|
+
cwd: path.resolve('.'),
|
|
11
|
+
env: { ...process.env, COLUMNS: '60', LINES: '12' },
|
|
12
|
+
},
|
|
13
|
+
)
|
|
14
|
+
child.stdout.on('data', (d) => term.write(d.toString('utf-8')))
|
|
15
|
+
child.stderr.on('data', (d) => term.write(d.toString('utf-8')))
|
|
16
|
+
setTimeout(() => {
|
|
17
|
+
const g = term.grid
|
|
18
|
+
if (g[term.r]) g[term.r][term.c] = String.fromCharCode(9673)
|
|
19
|
+
process.stdout.write(
|
|
20
|
+
g
|
|
21
|
+
.map((r) => r.join('').replace(/\s+$/, ''))
|
|
22
|
+
.map((l, n) => String(n).padStart(2) + '| ' + l)
|
|
23
|
+
.join(NL) +
|
|
24
|
+
NL +
|
|
25
|
+
'CURSOR row=' +
|
|
26
|
+
term.r +
|
|
27
|
+
NL,
|
|
28
|
+
)
|
|
29
|
+
child.kill('SIGTERM')
|
|
30
|
+
}, 5000)
|
|
31
|
+
child.on('close', () => process.exit(0))
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { render } from 'ink'
|
|
3
|
+
import { TuiStore } from '../src/tui/store.js'
|
|
4
|
+
import { TuiApp } from '../src/tui/TuiApp.js'
|
|
5
|
+
import { initialEditState } from '../src/tui/edit.js'
|
|
6
|
+
const store = new TuiStore()
|
|
7
|
+
store.set({
|
|
8
|
+
title: 'z',
|
|
9
|
+
version: '1',
|
|
10
|
+
workdir: '/tmp',
|
|
11
|
+
prompt: '> ',
|
|
12
|
+
locale: 'en',
|
|
13
|
+
commands: [],
|
|
14
|
+
edit: { ...initialEditState(), buf: 'abc' },
|
|
15
|
+
statusText: 'STATUS HERE',
|
|
16
|
+
})
|
|
17
|
+
const p = {
|
|
18
|
+
onSubmit: () => {},
|
|
19
|
+
onEscape: () => {},
|
|
20
|
+
onCtrlC: () => {},
|
|
21
|
+
onCopyCode: () => {},
|
|
22
|
+
onAttach: () => null,
|
|
23
|
+
onClipboard: () => Promise.resolve(null),
|
|
24
|
+
onHistoryChange: () => {},
|
|
25
|
+
onClearScreen: () => {},
|
|
26
|
+
attachments: [],
|
|
27
|
+
onPastesChange: () => {},
|
|
28
|
+
}
|
|
29
|
+
render(React.createElement(TuiApp, { store, inputProps: p }), {
|
|
30
|
+
exitOnCtrlC: false,
|
|
31
|
+
patchConsole: false,
|
|
32
|
+
})
|
|
33
|
+
setInterval(() => {}, 1 << 30)
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// Prove the mouse-mode churn is ZERO: mount the REAL TuiEditor, force several
|
|
2
|
+
// store updates (like the 1s badge refresh does), and COUNT how many times the
|
|
3
|
+
// raw stdout receives the mouse-mode sequences. With the effect depending on
|
|
4
|
+
// an inline onWheel, the count grew per render (the wheel-vs-history bug);
|
|
5
|
+
// with the ref fix, each mode byte is written ONCE.
|
|
6
|
+
//
|
|
7
|
+
// Exit code 0 = OK, 1 = churn detected. Prints a one-line verdict so the pty
|
|
8
|
+
// test can assert on it.
|
|
9
|
+
|
|
10
|
+
import { spawn } from 'node:child_process'
|
|
11
|
+
import path from 'node:path'
|
|
12
|
+
|
|
13
|
+
const child = spawn(
|
|
14
|
+
'script',
|
|
15
|
+
['-qec', 'npx tsx scripts/tui-mouse-churn-probe.tsx', '/dev/null'],
|
|
16
|
+
{
|
|
17
|
+
cwd: path.resolve('.'),
|
|
18
|
+
env: { ...process.env, COLUMNS: '80', LIMES: '24' },
|
|
19
|
+
},
|
|
20
|
+
)
|
|
21
|
+
let out = ''
|
|
22
|
+
child.stdout.on('data', (d) => (out += d.toString('utf-8')))
|
|
23
|
+
child.stderr.on('data', (d) => (out += d.toString('utf-8')))
|
|
24
|
+
setTimeout(() => child.kill('SIGTERM'), 8000)
|
|
25
|
+
child.on('close', () => {
|
|
26
|
+
const count = (seq: string): number => out.split(seq).length - 1
|
|
27
|
+
const on = count('\u001b[?1000h')
|
|
28
|
+
const altOff = count('\u001b[?1007l')
|
|
29
|
+
const altOn = count('\u001b[?1007h')
|
|
30
|
+
console.log('MOUSE_CHURN 1000h=' + on + ' 1007l=' + altOff + ' 1007h=' + altOn)
|
|
31
|
+
const ok = on === 1 && altOff === 1 && altOn === 0
|
|
32
|
+
console.log(ok ? 'MOUSE_CHURN_OK' : 'MOUSE_CHURN_FAIL')
|
|
33
|
+
process.exit(ok ? 0 : 1)
|
|
34
|
+
})
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
// Qwen model-switching self-smoke: launch a REAL isolated QwenBrowser, list
|
|
2
|
+
// the model catalogue, then SWITCH to several models and ask a question with
|
|
3
|
+
// each — the operator explicitly asked that model switching be exercised.
|
|
4
|
+
//
|
|
5
|
+
// Isolation mirrors qwen-smoke.mts: a throwaway HOME + a read-only COPY of the
|
|
6
|
+
// real qwen-profile (the live profile is never opened).
|
|
7
|
+
//
|
|
8
|
+
// Usage: npx tsx scripts/qwen-models-smoke.mts [--headed] [--all]
|
|
9
|
+
|
|
10
|
+
import fs from 'fs'
|
|
11
|
+
import path from 'path'
|
|
12
|
+
import os from 'os'
|
|
13
|
+
|
|
14
|
+
const HEADED = process.argv.includes('--headed')
|
|
15
|
+
const ALL = process.argv.includes('--all')
|
|
16
|
+
const REAL_HOME = os.homedir()
|
|
17
|
+
const PROFILE_SRC = path.join(REAL_HOME, '.zames', 'qwen-profile')
|
|
18
|
+
|
|
19
|
+
if (!process.env.ZAMES_QWEN_MODELS_HOME) {
|
|
20
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'zames-qwen-models-'))
|
|
21
|
+
process.env.HOME = tmp
|
|
22
|
+
process.env.ZAMES_QWEN_MODELS_HOME = tmp
|
|
23
|
+
fs.mkdirSync(path.join(tmp, '.zames'), { recursive: true })
|
|
24
|
+
fs.mkdirSync(path.join(tmp, '.cache'), { recursive: true })
|
|
25
|
+
const realCfg = path.join(REAL_HOME, '.zames', 'config.json')
|
|
26
|
+
if (fs.existsSync(realCfg)) {
|
|
27
|
+
fs.copyFileSync(realCfg, path.join(tmp, '.zames', 'config.json'))
|
|
28
|
+
}
|
|
29
|
+
const realPw = path.join(REAL_HOME, '.cache', 'ms-playwright')
|
|
30
|
+
if (fs.existsSync(realPw)) {
|
|
31
|
+
fs.symlinkSync(realPw, path.join(tmp, '.cache', 'ms-playwright'))
|
|
32
|
+
}
|
|
33
|
+
if (fs.existsSync(PROFILE_SRC)) {
|
|
34
|
+
fs.cpSync(PROFILE_SRC, path.join(tmp, '.zames', 'qwen-profile'), {
|
|
35
|
+
recursive: true,
|
|
36
|
+
filter: (src) => !/^Singleton/.test(path.basename(src)),
|
|
37
|
+
})
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const { QwenBrowser } = await import('../src/qwen-browser.ts')
|
|
42
|
+
const { loadConfig } = await import('../src/config.ts')
|
|
43
|
+
const cfg = loadConfig()
|
|
44
|
+
|
|
45
|
+
const results: Array<{ name: string; ok: boolean; note?: string }> = []
|
|
46
|
+
const record = (name: string, ok: boolean, note?: string): void => {
|
|
47
|
+
results.push({ name, ok, note })
|
|
48
|
+
console.log((ok ? 'PASS ' : 'FAIL ') + name + (note ? ' — ' + note : ''))
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const browser = new QwenBrowser({
|
|
52
|
+
headless: !HEADED,
|
|
53
|
+
debug: false,
|
|
54
|
+
channel: cfg.browserChannel,
|
|
55
|
+
auth: cfg.browser.auth,
|
|
56
|
+
locale: 'ru',
|
|
57
|
+
minSendIntervalMs: 0,
|
|
58
|
+
answerTimeoutMs: 180000,
|
|
59
|
+
webSearch: false,
|
|
60
|
+
deepThinking: false,
|
|
61
|
+
})
|
|
62
|
+
browser.onNotice = (t: string) => console.log(' [notice] ' + t)
|
|
63
|
+
|
|
64
|
+
async function main(): Promise<void> {
|
|
65
|
+
await browser.launch()
|
|
66
|
+
const timedOut = await Promise.race([
|
|
67
|
+
browser.waitForLogin().then(() => false),
|
|
68
|
+
new Promise<boolean>((r) => setTimeout(() => r(true), 60000)),
|
|
69
|
+
])
|
|
70
|
+
if (timedOut) {
|
|
71
|
+
record('login', false, 'timed out')
|
|
72
|
+
await browser.close().catch(() => {})
|
|
73
|
+
process.exit(2)
|
|
74
|
+
}
|
|
75
|
+
record('login', true)
|
|
76
|
+
|
|
77
|
+
const models = await browser.listModels()
|
|
78
|
+
record(
|
|
79
|
+
'listModels',
|
|
80
|
+
models.length > 0,
|
|
81
|
+
'n=' + models.length + ' ' + models.map((m) => m.id).join(','),
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
// Switch through a few models and ask with each. Prefer distinct vendors so
|
|
85
|
+
// the dropdown selection really changes the backend.
|
|
86
|
+
const wanted = ALL ? models : models.slice(0, 3)
|
|
87
|
+
for (const m of wanted) {
|
|
88
|
+
try {
|
|
89
|
+
await browser.newChat()
|
|
90
|
+
browser.model = m.id
|
|
91
|
+
const ans = await browser.ask('Reply with exactly: OK', { agent: true })
|
|
92
|
+
const ok = /OK/i.test(ans)
|
|
93
|
+
record('model:' + m.id, ok, ok ? '' : 'got: ' + ans.slice(0, 60))
|
|
94
|
+
} catch (e) {
|
|
95
|
+
record('model:' + m.id, false, (e as Error).message)
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
await browser.close().catch(() => {})
|
|
100
|
+
const failed = results.filter((r) => !r.ok)
|
|
101
|
+
console.log(
|
|
102
|
+
'\nqwen-models-smoke: ' +
|
|
103
|
+
(results.length - failed.length) +
|
|
104
|
+
'/' +
|
|
105
|
+
results.length +
|
|
106
|
+
' passed',
|
|
107
|
+
)
|
|
108
|
+
if (!process.env.ZAMES_QWEN_MODELS_KEEP) {
|
|
109
|
+
fs.rmSync(process.env.ZAMES_QWEN_MODELS_HOME as string, {
|
|
110
|
+
recursive: true,
|
|
111
|
+
force: true,
|
|
112
|
+
})
|
|
113
|
+
}
|
|
114
|
+
process.exit(failed.length ? 1 : 0)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
void main()
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import { Term } from '../test/vt100.mts'
|
|
4
|
+
const NL = String.fromCharCode(10)
|
|
5
|
+
const term = new Term(20, 80)
|
|
6
|
+
const child = spawn(
|
|
7
|
+
'script',
|
|
8
|
+
['-qec', 'npx tsx scripts/tui-repro.tsx', '/dev/null'],
|
|
9
|
+
{
|
|
10
|
+
cwd: path.resolve('.'),
|
|
11
|
+
env: { ...process.env, COLUMNS: '80', LINES: '20', PROBE_LOCALE: 'ru' },
|
|
12
|
+
},
|
|
13
|
+
)
|
|
14
|
+
child.stdout.on('data', (d) => term.write(d.toString('utf-8')))
|
|
15
|
+
child.stderr.on('data', (d) => term.write(d.toString('utf-8')))
|
|
16
|
+
setTimeout(() => {
|
|
17
|
+
process.stdout.write(term.render() + NL)
|
|
18
|
+
child.kill('SIGTERM')
|
|
19
|
+
}, 5000)
|
|
20
|
+
child.on('close', () => process.exit(0))
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// Screenshot a standalone TUI component under a pty, optionally driving keys.
|
|
2
|
+
// Usage: npx tsx scripts/tui-comp-shot.mts --component=select --keys=j,j --cols=80 --rows=24
|
|
3
|
+
import { spawn } from 'node:child_process'
|
|
4
|
+
import path from 'node:path'
|
|
5
|
+
import { Term } from '../test/vt100.mts'
|
|
6
|
+
|
|
7
|
+
const ESC = String.fromCharCode(27)
|
|
8
|
+
const NL = String.fromCharCode(10)
|
|
9
|
+
function arg(name: string, def = ''): string {
|
|
10
|
+
const hit = process.argv.find((a) => a.startsWith('--' + name + '='))
|
|
11
|
+
return hit ? hit.slice(name.length + 3) : def
|
|
12
|
+
}
|
|
13
|
+
const component = arg('component', 'select')
|
|
14
|
+
const cols = Number(arg('cols', '80'))
|
|
15
|
+
const rows = Number(arg('rows', '24'))
|
|
16
|
+
const keys = arg('keys', '')
|
|
17
|
+
.split(',')
|
|
18
|
+
.filter((s) => s.length > 0)
|
|
19
|
+
.map((s) =>
|
|
20
|
+
s === 'UP'
|
|
21
|
+
? ESC + '[A'
|
|
22
|
+
: s === 'DOWN'
|
|
23
|
+
? ESC + '[B'
|
|
24
|
+
: s === 'TAB'
|
|
25
|
+
? String.fromCharCode(9)
|
|
26
|
+
: s === 'ENTER'
|
|
27
|
+
? String.fromCharCode(13)
|
|
28
|
+
: s === 'ESC'
|
|
29
|
+
? ESC
|
|
30
|
+
: s,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
const term = new Term(rows, cols)
|
|
34
|
+
const child = spawn(
|
|
35
|
+
'script',
|
|
36
|
+
['-qec', 'npx tsx scripts/tui-components.tsx', '/dev/null'],
|
|
37
|
+
{
|
|
38
|
+
cwd: path.resolve('.'),
|
|
39
|
+
env: {
|
|
40
|
+
...process.env,
|
|
41
|
+
COLUMNS: String(cols),
|
|
42
|
+
LINES: String(rows),
|
|
43
|
+
PROBE_COMPONENT: component,
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
)
|
|
47
|
+
child.stdout.on('data', (d) => term.write(d.toString('utf-8')))
|
|
48
|
+
child.stderr.on('data', (d) => term.write(d.toString('utf-8')))
|
|
49
|
+
let i = 0
|
|
50
|
+
const tick = (): void => {
|
|
51
|
+
if (i < keys.length) {
|
|
52
|
+
child.stdin.write(keys[i])
|
|
53
|
+
i++
|
|
54
|
+
setTimeout(tick, 250)
|
|
55
|
+
} else {
|
|
56
|
+
setTimeout(() => {
|
|
57
|
+
process.stdout.write(term.render() + NL)
|
|
58
|
+
child.kill('SIGTERM')
|
|
59
|
+
}, 400)
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
setTimeout(tick, 4500)
|
|
63
|
+
child.on('close', () => process.exit(0))
|