zames_pro 2.73.11 → 2.75.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 +48 -0
- package/README.md +37 -7
- package/dist/attachments.js +6 -1
- 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 +35 -0
- package/dist/i18n/config.js +20 -0
- package/dist/i18n/deepseek.js +14 -0
- package/dist/i18n/help.js +30 -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 +487 -65
- package/dist/input/layout.js +19 -0
- package/dist/input/suggest.js +29 -1
- 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 +16 -1
- package/dist/tool-summary.js +83 -0
- package/dist/tui/ConfigMenu.js +337 -0
- package/dist/tui/ConfirmDialog.js +42 -0
- package/dist/tui/FullScreenApp.js +113 -0
- package/dist/tui/InputLine.js +318 -0
- package/dist/tui/LogView.js +48 -0
- package/dist/tui/LoginForm.js +83 -0
- package/dist/tui/SelectList.js +54 -0
- package/dist/tui/StatusBar.js +170 -0
- package/dist/tui/TextPrompt.js +41 -0
- package/dist/tui/TuiApp.js +29 -0
- package/dist/tui/TuiEditor.js +490 -0
- package/dist/tui/edit.js +317 -0
- package/dist/tui/keys.js +195 -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 +164 -0
- package/dist/tui/widgets.js +60 -0
- package/package.json +7 -3
- package/scripts/coverage-gate.mts +10 -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/render-demo.mts +1 -1
- 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-probe.tsx +62 -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
|
@@ -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()
|
package/scripts/render-demo.mts
CHANGED
|
@@ -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))
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// Mount EACH standalone TUI component (SelectList, ConfirmDialog, LoginForm,
|
|
2
|
+
// TextPrompt) under a pty so every menu can be screenshotted and driven.
|
|
3
|
+
// Pick the component with PROBE_COMPONENT.
|
|
4
|
+
|
|
5
|
+
import React from 'react'
|
|
6
|
+
import { render } from 'ink'
|
|
7
|
+
import { SelectList } from '../src/tui/SelectList.js'
|
|
8
|
+
import { ConfirmDialog } from '../src/tui/ConfirmDialog.js'
|
|
9
|
+
import { LoginForm } from '../src/tui/LoginForm.js'
|
|
10
|
+
import { TextPrompt } from '../src/tui/TextPrompt.js'
|
|
11
|
+
import { translate } from '../src/i18n.js'
|
|
12
|
+
|
|
13
|
+
const which = process.env.PROBE_COMPONENT || 'select'
|
|
14
|
+
const t = translate('ru')
|
|
15
|
+
|
|
16
|
+
function node(): React.ReactElement {
|
|
17
|
+
switch (which) {
|
|
18
|
+
case 'select':
|
|
19
|
+
return React.createElement(SelectList, {
|
|
20
|
+
title: 'Модель Qwen',
|
|
21
|
+
initial: 'qwen3.7-plus',
|
|
22
|
+
items: [
|
|
23
|
+
{ value: 'qwen3.7-plus', label: 'qwen3.7-plus', detail: '← текущая' },
|
|
24
|
+
{ value: 'qwen3.8-max', label: 'qwen3.8-max' },
|
|
25
|
+
{
|
|
26
|
+
value: 'qwen3.8-omni-flash',
|
|
27
|
+
label: 'qwen3.8-omni-flash',
|
|
28
|
+
description: 'быстрая',
|
|
29
|
+
},
|
|
30
|
+
{ value: 'qwen3.7-max', label: 'qwen3.7-max' },
|
|
31
|
+
{ value: 'qwen3.6-plus', label: 'qwen3.6-plus' },
|
|
32
|
+
],
|
|
33
|
+
onSelect: () => {},
|
|
34
|
+
})
|
|
35
|
+
case 'confirm':
|
|
36
|
+
return React.createElement(ConfirmDialog, {
|
|
37
|
+
title: 'Подтверждение',
|
|
38
|
+
message: 'Выполнить Bash: rm -rf build ?',
|
|
39
|
+
yesLabel: 'Да',
|
|
40
|
+
noLabel: 'Нет',
|
|
41
|
+
onAnswer: () => {},
|
|
42
|
+
})
|
|
43
|
+
case 'login':
|
|
44
|
+
return React.createElement(LoginForm, { t, onResult: () => {} })
|
|
45
|
+
case 'text':
|
|
46
|
+
return React.createElement(TextPrompt, {
|
|
47
|
+
t,
|
|
48
|
+
label: 'Одноразовый код',
|
|
49
|
+
secret: false,
|
|
50
|
+
onResult: () => {},
|
|
51
|
+
})
|
|
52
|
+
default:
|
|
53
|
+
return React.createElement(SelectList, {
|
|
54
|
+
title: 'x',
|
|
55
|
+
items: [],
|
|
56
|
+
onSelect: () => {},
|
|
57
|
+
})
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
render(node(), { exitOnCtrlC: false, patchConsole: false })
|
|
62
|
+
setInterval(() => {}, 1 << 30)
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
// Drive scripts/tui-config-probe.tsx under a pty: send keys one at a time,
|
|
2
|
+
// screenshot the VT100 grid at the end, and print the probe's set/reset log.
|
|
3
|
+
// Used by test/tui-pty-render.test.ts to pin the config-menu regressions
|
|
4
|
+
// (the stale value after a toggle, and the overflowing block at a small
|
|
5
|
+
// terminal).
|
|
6
|
+
import { spawn } from 'node:child_process'
|
|
7
|
+
import fs from 'node:fs'
|
|
8
|
+
import path from 'node:path'
|
|
9
|
+
import { Term } from '../test/vt100.mts'
|
|
10
|
+
|
|
11
|
+
const ESC = String.fromCharCode(27)
|
|
12
|
+
const NL = String.fromCharCode(10)
|
|
13
|
+
|
|
14
|
+
function arg(name: string, def = ''): string {
|
|
15
|
+
const hit = process.argv.find((a) => a.startsWith('--' + name + '='))
|
|
16
|
+
return hit ? hit.slice(name.length + 3) : def
|
|
17
|
+
}
|
|
18
|
+
const cols = Number(arg('cols', '90'))
|
|
19
|
+
const rows = Number(arg('rows', '30'))
|
|
20
|
+
const keys = arg('keys', '')
|
|
21
|
+
.split(',')
|
|
22
|
+
.filter((s) => s.length > 0)
|
|
23
|
+
.map((s) =>
|
|
24
|
+
s === 'RIGHT'
|
|
25
|
+
? ESC + '[C'
|
|
26
|
+
: s === 'LEFT'
|
|
27
|
+
? ESC + '[D'
|
|
28
|
+
: s === 'DOWN'
|
|
29
|
+
? ESC + '[B'
|
|
30
|
+
: s === 'UP'
|
|
31
|
+
? ESC + '[A'
|
|
32
|
+
: s === 'ENTER'
|
|
33
|
+
? String.fromCharCode(13)
|
|
34
|
+
: s,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
const LOG = '/tmp/config-probe.log'
|
|
38
|
+
try {
|
|
39
|
+
fs.unlinkSync(LOG)
|
|
40
|
+
} catch {
|
|
41
|
+
/* ignore */
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const term = new Term(rows, cols)
|
|
45
|
+
const child = spawn(
|
|
46
|
+
'script',
|
|
47
|
+
['-qec', 'npx tsx scripts/tui-config-probe.tsx', '/dev/null'],
|
|
48
|
+
{
|
|
49
|
+
cwd: path.resolve('.'),
|
|
50
|
+
env: {
|
|
51
|
+
...process.env,
|
|
52
|
+
COLUMNS: String(cols),
|
|
53
|
+
LINES: String(rows),
|
|
54
|
+
PROBE_CONFIG_LOG: LOG,
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
)
|
|
58
|
+
child.stdout.on('data', (d) => term.write(d.toString('utf-8')))
|
|
59
|
+
child.stderr.on('data', (d) => term.write(d.toString('utf-8')))
|
|
60
|
+
|
|
61
|
+
let i = 0
|
|
62
|
+
const tick = (): void => {
|
|
63
|
+
if (i < keys.length) {
|
|
64
|
+
child.stdin.write(keys[i])
|
|
65
|
+
i++
|
|
66
|
+
setTimeout(tick, 220)
|
|
67
|
+
} else {
|
|
68
|
+
setTimeout(() => {
|
|
69
|
+
process.stdout.write('===SCREEN===' + NL)
|
|
70
|
+
process.stdout.write(term.render() + NL)
|
|
71
|
+
process.stdout.write('===LOG===' + NL)
|
|
72
|
+
try {
|
|
73
|
+
process.stdout.write(fs.readFileSync(LOG, 'utf-8'))
|
|
74
|
+
} catch {
|
|
75
|
+
/* ignore */
|
|
76
|
+
}
|
|
77
|
+
child.kill('SIGTERM')
|
|
78
|
+
}, 500)
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
setTimeout(tick, 5000)
|
|
82
|
+
child.on('close', () => process.exit(0))
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// Mount the REAL Ink config menu with the REAL schema and callbacks so a pty
|
|
2
|
+
// run can screenshot it and drive it (toggle a boolean, cycle an enum, open an
|
|
3
|
+
// edit prompt) — the menu is otherwise only reachable from a live agent.
|
|
4
|
+
|
|
5
|
+
import React from 'react'
|
|
6
|
+
import fs from 'node:fs'
|
|
7
|
+
import { render } from 'ink'
|
|
8
|
+
import { ConfigMenuApp } from '../src/tui/ConfigMenu.js'
|
|
9
|
+
import {
|
|
10
|
+
CONFIG_SCHEMA,
|
|
11
|
+
DEFAULTS,
|
|
12
|
+
validateConfigValue,
|
|
13
|
+
type ConfigField,
|
|
14
|
+
} from '../src/config.js'
|
|
15
|
+
import { translate } from '../src/i18n.js'
|
|
16
|
+
|
|
17
|
+
const LOG = process.env.PROBE_CONFIG_LOG || '/tmp/config-probe.log'
|
|
18
|
+
// A tiny in-memory config so get/set/reset are real (validate + default).
|
|
19
|
+
function getByPath(obj: Record<string, unknown>, path: string): unknown {
|
|
20
|
+
return path
|
|
21
|
+
.split('.')
|
|
22
|
+
.reduce<unknown>(
|
|
23
|
+
(o, k) => (o == null ? undefined : (o as Record<string, unknown>)[k]),
|
|
24
|
+
obj,
|
|
25
|
+
)
|
|
26
|
+
}
|
|
27
|
+
function setByPath(path: string, value: unknown): void {
|
|
28
|
+
const parts = path.split('.')
|
|
29
|
+
let o: Record<string, unknown> = cfg
|
|
30
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
31
|
+
o[parts[i]] = o[parts[i]] ?? {}
|
|
32
|
+
o = o[parts[i]] as Record<string, unknown>
|
|
33
|
+
}
|
|
34
|
+
o[parts[parts.length - 1]] = value
|
|
35
|
+
}
|
|
36
|
+
const cfg: Record<string, unknown> = {}
|
|
37
|
+
// Seed a few NESTED values so toggling is observable (the menu reads through
|
|
38
|
+
// getByPath, which walks the path — a flat key would never be found).
|
|
39
|
+
setByPath('ui.locale', 'ru')
|
|
40
|
+
setByPath('ui.tui', true)
|
|
41
|
+
setByPath('ui.alternateScreen', true)
|
|
42
|
+
setByPath('ui.mouseScroll', false)
|
|
43
|
+
setByPath('maxIterations', 0)
|
|
44
|
+
setByPath('headless', true)
|
|
45
|
+
setByPath('provider', 'deepseek')
|
|
46
|
+
|
|
47
|
+
const log = (line: string): void => {
|
|
48
|
+
fs.appendFileSync(LOG, line + String.fromCharCode(10))
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
render(
|
|
52
|
+
React.createElement(ConfigMenuApp, {
|
|
53
|
+
fields: CONFIG_SCHEMA,
|
|
54
|
+
t: translate('ru'),
|
|
55
|
+
get: (p: string) => getByPath(cfg, p),
|
|
56
|
+
getDefault: (p: string) =>
|
|
57
|
+
getByPath(DEFAULTS as unknown as Record<string, unknown>, p),
|
|
58
|
+
set: (field: ConfigField, raw: string) => {
|
|
59
|
+
const v = validateConfigValue(field, raw)
|
|
60
|
+
setByPath(field.path, v)
|
|
61
|
+
log('SET ' + field.path + '=' + JSON.stringify(v))
|
|
62
|
+
},
|
|
63
|
+
reset: (field: ConfigField) => {
|
|
64
|
+
setByPath(
|
|
65
|
+
field.path,
|
|
66
|
+
getByPath(DEFAULTS as unknown as Record<string, unknown>, field.path),
|
|
67
|
+
)
|
|
68
|
+
log('RESET ' + field.path)
|
|
69
|
+
},
|
|
70
|
+
onExit: () => log('EXIT'),
|
|
71
|
+
}),
|
|
72
|
+
{ exitOnCtrlC: false, patchConsole: false },
|
|
73
|
+
)
|
|
74
|
+
setInterval(() => {}, 1 << 30)
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
// Drive scripts/tui-edit-probe.tsx under a pty with a scripted key sequence,
|
|
2
|
+
// then print the LAST buffer state the probe logged.
|
|
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/edit-probe.log'
|
|
9
|
+
|
|
10
|
+
function arg(name: string, def = ''): string {
|
|
11
|
+
const hit = process.argv.find((a) => a.startsWith('--' + name + '='))
|
|
12
|
+
return hit ? hit.slice(name.length + 3) : def
|
|
13
|
+
}
|
|
14
|
+
const seed = arg('seed', '')
|
|
15
|
+
const keys = arg('keys', '')
|
|
16
|
+
.split(',')
|
|
17
|
+
.filter((s) => s.length > 0)
|
|
18
|
+
.map((s) =>
|
|
19
|
+
s === 'CTDL'
|
|
20
|
+
? ESC + '[3;5~'
|
|
21
|
+
: s === 'CTDLU'
|
|
22
|
+
? ESC + '[3;5u'
|
|
23
|
+
: s === 'CTB127U'
|
|
24
|
+
? ESC + '[127;5u'
|
|
25
|
+
: s === 'DELU'
|
|
26
|
+
? ESC + '[3u'
|
|
27
|
+
: s === 'BSU'
|
|
28
|
+
? ESC + '[127u'
|
|
29
|
+
: s === 'CTRLBS'
|
|
30
|
+
? ESC + String.fromCharCode(127)
|
|
31
|
+
: s === 'CTB08'
|
|
32
|
+
? String.fromCharCode(8)
|
|
33
|
+
: s === 'DEL'
|
|
34
|
+
? ESC + '[3~'
|
|
35
|
+
: s === 'CTRLUNDO'
|
|
36
|
+
? String.fromCharCode(31)
|
|
37
|
+
: s === 'CTRLU'
|
|
38
|
+
? String.fromCharCode(21)
|
|
39
|
+
: s === 'CTRLK'
|
|
40
|
+
? String.fromCharCode(11)
|
|
41
|
+
: s === 'CTRLR'
|
|
42
|
+
? String.fromCharCode(18)
|
|
43
|
+
: s === 'ESCAPE'
|
|
44
|
+
? ESC
|
|
45
|
+
: s === 'CTRLW'
|
|
46
|
+
? String.fromCharCode(23)
|
|
47
|
+
: s === 'LEFT'
|
|
48
|
+
? ESC + '[D'
|
|
49
|
+
: s === 'RIGHT'
|
|
50
|
+
? ESC + '[C'
|
|
51
|
+
: s === 'UP'
|
|
52
|
+
? ESC + '[A'
|
|
53
|
+
: s === 'DOWN'
|
|
54
|
+
? ESC + '[B'
|
|
55
|
+
: s === 'ENTER'
|
|
56
|
+
? String.fromCharCode(13)
|
|
57
|
+
: s,
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
const child = spawn(
|
|
61
|
+
'script',
|
|
62
|
+
['-qec', 'npx tsx scripts/tui-edit-probe.tsx', '/dev/null'],
|
|
63
|
+
{
|
|
64
|
+
cwd: path.resolve('.'),
|
|
65
|
+
env: {
|
|
66
|
+
...process.env,
|
|
67
|
+
COLUMNS: '80',
|
|
68
|
+
LINES: '24',
|
|
69
|
+
PROBE_EDIT_LOG: LOG,
|
|
70
|
+
PROBE_EDIT_SEED: seed,
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
)
|
|
74
|
+
child.stdout.on('data', () => {})
|
|
75
|
+
child.stderr.on('data', () => {})
|
|
76
|
+
|
|
77
|
+
let i = 0
|
|
78
|
+
const tick = (): void => {
|
|
79
|
+
if (i < keys.length) {
|
|
80
|
+
child.stdin.write(keys[i])
|
|
81
|
+
i++
|
|
82
|
+
setTimeout(tick, 200)
|
|
83
|
+
} else {
|
|
84
|
+
setTimeout(() => child.kill('SIGTERM'), 300)
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
setTimeout(tick, 5000)
|
|
88
|
+
child.on('close', () => {
|
|
89
|
+
let lines: string[] = []
|
|
90
|
+
try {
|
|
91
|
+
lines = fs.readFileSync(LOG, 'utf-8').trim().split(String.fromCharCode(10))
|
|
92
|
+
} catch {
|
|
93
|
+
/* ignore */
|
|
94
|
+
}
|
|
95
|
+
console.log(
|
|
96
|
+
'SEED=' + JSON.stringify(seed) + ' KEYS=' + JSON.stringify(arg('keys', '')),
|
|
97
|
+
)
|
|
98
|
+
console.log('FINAL=' + (lines[lines.length - 1] || '(none)'))
|
|
99
|
+
process.exit(0)
|
|
100
|
+
})
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// Mount the REAL TuiEditor and log the editor BUFFER after every change, so a
|
|
2
|
+
// pty driver can send keys and assert what the buffer became (word deletion,
|
|
3
|
+
// newlines, history) instead of guessing from bytes. The log is one JSON line
|
|
4
|
+
// per change: {buf, cursor}.
|
|
5
|
+
|
|
6
|
+
import fs from 'node:fs'
|
|
7
|
+
import { TuiEditor } from '../src/tui/TuiEditor.js'
|
|
8
|
+
|
|
9
|
+
const LOG = process.env.PROBE_EDIT_LOG || '/tmp/edit-probe.log'
|
|
10
|
+
const SEED = process.env.PROBE_EDIT_SEED || ''
|
|
11
|
+
fs.writeFileSync(LOG, '')
|
|
12
|
+
|
|
13
|
+
const ed = new TuiEditor({
|
|
14
|
+
prompt: '> ',
|
|
15
|
+
commands: [],
|
|
16
|
+
locale: 'en',
|
|
17
|
+
alternateScreen: process.env.PROBE_INLINE !== '1',
|
|
18
|
+
})
|
|
19
|
+
ed.onSubmit = () => {}
|
|
20
|
+
ed.onAttach = () => null
|
|
21
|
+
ed.onClipboard = () => Promise.resolve(null)
|
|
22
|
+
|
|
23
|
+
ed.store.subscribe(() => {
|
|
24
|
+
const s = ed.store.getSnapshot().edit
|
|
25
|
+
fs.appendFileSync(
|
|
26
|
+
LOG,
|
|
27
|
+
JSON.stringify({ buf: s.buf, cursor: s.cursor }) + String.fromCharCode(10),
|
|
28
|
+
)
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
ed.start()
|
|
32
|
+
if (SEED) {
|
|
33
|
+
// Type the seed through the store so the buffer starts non-empty.
|
|
34
|
+
ed.store.update((snap) => ({
|
|
35
|
+
edit: { ...snap.edit, buf: SEED, cursor: Array.from(SEED).length },
|
|
36
|
+
}))
|
|
37
|
+
}
|
|
38
|
+
setInterval(() => {}, 1 << 30)
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// Mount the REAL TuiEditor (the adapter index.ts uses), so the pty resize test
|
|
2
|
+
// exercises TuiEditor.installResizeHandler (unmount+remount), NOT just Ink's
|
|
3
|
+
// built-in erase. The raw FullScreenApp probe bypasses that handler.
|
|
4
|
+
|
|
5
|
+
import { TuiEditor } from '../src/tui/TuiEditor.js'
|
|
6
|
+
|
|
7
|
+
const ed = new TuiEditor({
|
|
8
|
+
prompt: '\u2794 zames_pro \u203a ',
|
|
9
|
+
commands: [
|
|
10
|
+
{ name: '/help', description: 'show help' },
|
|
11
|
+
{ name: '/new', description: 'new chat' },
|
|
12
|
+
],
|
|
13
|
+
locale: 'en',
|
|
14
|
+
alternateScreen: true,
|
|
15
|
+
})
|
|
16
|
+
ed.setHeader?.({
|
|
17
|
+
title: 'zames_pro',
|
|
18
|
+
version: '2.74.0',
|
|
19
|
+
workdir: '/workspaces/zames/zames_pro',
|
|
20
|
+
})
|
|
21
|
+
ed.start()
|
|
22
|
+
ed.printAbove('log line one')
|
|
23
|
+
ed.printAbove('log line two')
|
|
24
|
+
setInterval(() => {}, 1 << 30)
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// Screenshot the REAL FullScreenApp input box for a given buffer/cursor, and
|
|
2
|
+
// print the VT100 grid WITH a cursor marker (the real terminal cursor cell).
|
|
3
|
+
import { spawn } from 'node:child_process'
|
|
4
|
+
import path from 'node:path'
|
|
5
|
+
import { Term } from '../test/vt100.mts'
|
|
6
|
+
|
|
7
|
+
const NL = String.fromCharCode(10)
|
|
8
|
+
function arg(name: string, def = ''): string {
|
|
9
|
+
const hit = process.argv.find((a) => a.startsWith('--' + name + '='))
|
|
10
|
+
return hit ? hit.slice(name.length + 3) : def
|
|
11
|
+
}
|
|
12
|
+
const cols = Number(arg('cols', '80'))
|
|
13
|
+
const rows = Number(arg('rows', '24'))
|
|
14
|
+
const buf = arg('buf', '')
|
|
15
|
+
.replace(/\\n/g, NL)
|
|
16
|
+
.replace(/\\t/g, String.fromCharCode(9))
|
|
17
|
+
const cursor = arg('cursor', '')
|
|
18
|
+
|
|
19
|
+
const term = new Term(rows, cols)
|
|
20
|
+
let stderr = ''
|
|
21
|
+
const child = spawn(
|
|
22
|
+
'script',
|
|
23
|
+
['-qec', 'npx tsx scripts/tui-input-shot.tsx', '/dev/null'],
|
|
24
|
+
{
|
|
25
|
+
cwd: path.resolve('.'),
|
|
26
|
+
env: {
|
|
27
|
+
...process.env,
|
|
28
|
+
COLUMNS: String(cols),
|
|
29
|
+
LINES: String(rows),
|
|
30
|
+
PROBE_BUF: buf,
|
|
31
|
+
...(cursor ? { PROBE_CURSOR: cursor } : {}),
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
)
|
|
35
|
+
child.stdout.on('data', (d) => term.write(d.toString('utf-8')))
|
|
36
|
+
child.stderr.on('data', (d) => (stderr += d.toString('utf-8')))
|
|
37
|
+
setTimeout(() => {
|
|
38
|
+
const cr = term.r
|
|
39
|
+
const cc = term.c
|
|
40
|
+
// Mark by GRID CELL, not by string index: a wide char occupies two grid
|
|
41
|
+
// cells, so slicing the rendered string would place the marker wrong.
|
|
42
|
+
const grid = term.grid
|
|
43
|
+
if (grid[cr] && grid[cr][cc] !== undefined)
|
|
44
|
+
grid[cr][cc] = String.fromCharCode(9673)
|
|
45
|
+
const lines = grid.map((row) => row.join('').replace(/\s+$/, ''))
|
|
46
|
+
process.stdout.write(
|
|
47
|
+
lines.map((l, n) => String(n).padStart(2) + '| ' + l).join(NL) + NL,
|
|
48
|
+
)
|
|
49
|
+
process.stdout.write('CURSOR row=' + cr + ' col=' + cc + NL)
|
|
50
|
+
const lay = stderr.split(NL).find((l) => l.startsWith('LAYOUT'))
|
|
51
|
+
if (lay) process.stdout.write(lay + NL)
|
|
52
|
+
child.kill('SIGTERM')
|
|
53
|
+
}, 5000)
|
|
54
|
+
child.on('close', () => process.exit(0))
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// Mount the REAL FullScreenApp with a buffer set from env, so the input box
|
|
2
|
+
// rendering (wrapping, cursor, CJK width, paste markers) can be screenshotted.
|
|
3
|
+
import React from 'react'
|
|
4
|
+
import { render } from 'ink'
|
|
5
|
+
import { TuiStore } from '../src/tui/store.js'
|
|
6
|
+
import { FullScreenApp } from '../src/tui/FullScreenApp.js'
|
|
7
|
+
import { initialEditState } from '../src/tui/edit.js'
|
|
8
|
+
import { layoutInput, inputInnerCols } from '../src/input/layout.js'
|
|
9
|
+
|
|
10
|
+
const buf = process.env.PROBE_BUF || ''
|
|
11
|
+
const cursor = process.env.PROBE_CURSOR
|
|
12
|
+
? Number(process.env.PROBE_CURSOR)
|
|
13
|
+
: buf.length
|
|
14
|
+
const store = new TuiStore()
|
|
15
|
+
store.set({
|
|
16
|
+
title: 'zames',
|
|
17
|
+
version: '2.74',
|
|
18
|
+
workdir: '/workspaces/zames/zames_pro',
|
|
19
|
+
prompt: '\u2794 zames \u203a ',
|
|
20
|
+
locale: 'en',
|
|
21
|
+
commands: [],
|
|
22
|
+
edit: { ...initialEditState(), buf, cursor },
|
|
23
|
+
})
|
|
24
|
+
store.pushLog('system', 'log line one')
|
|
25
|
+
const inputProps = {
|
|
26
|
+
onSubmit: () => {},
|
|
27
|
+
onEscape: () => {},
|
|
28
|
+
onCtrlC: () => {},
|
|
29
|
+
onCopyCode: () => {},
|
|
30
|
+
onAttach: () => null,
|
|
31
|
+
onClipboard: () => Promise.resolve(null),
|
|
32
|
+
onHistoryChange: () => {},
|
|
33
|
+
onClearScreen: () => {},
|
|
34
|
+
attachments: [],
|
|
35
|
+
onPastesChange: () => {},
|
|
36
|
+
}
|
|
37
|
+
// Print the expected layout to stderr so it can be compared with the screen.
|
|
38
|
+
const cols = Number(process.env.COLUMNS || 80)
|
|
39
|
+
const lay = layoutInput(
|
|
40
|
+
'\u2794 zames \u203a ',
|
|
41
|
+
buf,
|
|
42
|
+
cursor,
|
|
43
|
+
inputInnerCols(cols),
|
|
44
|
+
)
|
|
45
|
+
process.stderr.write(
|
|
46
|
+
'LAYOUT rows=' +
|
|
47
|
+
lay.rows.length +
|
|
48
|
+
' cursorRow=' +
|
|
49
|
+
lay.cursorRow +
|
|
50
|
+
' cursorCol=' +
|
|
51
|
+
lay.cursorCol +
|
|
52
|
+
String.fromCharCode(10),
|
|
53
|
+
)
|
|
54
|
+
render(React.createElement(FullScreenApp, { store, inputProps }), {
|
|
55
|
+
exitOnCtrlC: false,
|
|
56
|
+
patchConsole: false,
|
|
57
|
+
alternateScreen: true,
|
|
58
|
+
})
|
|
59
|
+
setInterval(() => {}, 1 << 30)
|