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.
Files changed (88) hide show
  1. package/CHANGELOG.md +48 -0
  2. package/README.md +37 -7
  3. package/dist/attachments.js +6 -1
  4. package/dist/banner.js +71 -0
  5. package/dist/browser.js +84 -27
  6. package/dist/code-blocks.js +48 -0
  7. package/dist/commands.js +1 -0
  8. package/dist/config-live.js +53 -0
  9. package/dist/config.js +38 -0
  10. package/dist/goal.js +81 -0
  11. package/dist/i18n/common.js +35 -0
  12. package/dist/i18n/config.js +20 -0
  13. package/dist/i18n/deepseek.js +14 -0
  14. package/dist/i18n/help.js +30 -2
  15. package/dist/i18n/qwen.js +4 -0
  16. package/dist/i18n/self-review.js +8 -0
  17. package/dist/i18n/spinner.js +6 -0
  18. package/dist/index.js +487 -65
  19. package/dist/input/layout.js +19 -0
  20. package/dist/input/suggest.js +29 -1
  21. package/dist/input.js +14 -3
  22. package/dist/net-capture.js +18 -0
  23. package/dist/qwen-browser.js +43 -20
  24. package/dist/run-task.js +97 -4
  25. package/dist/theme.js +16 -1
  26. package/dist/tool-summary.js +83 -0
  27. package/dist/tui/ConfigMenu.js +337 -0
  28. package/dist/tui/ConfirmDialog.js +42 -0
  29. package/dist/tui/FullScreenApp.js +113 -0
  30. package/dist/tui/InputLine.js +318 -0
  31. package/dist/tui/LogView.js +48 -0
  32. package/dist/tui/LoginForm.js +83 -0
  33. package/dist/tui/SelectList.js +54 -0
  34. package/dist/tui/StatusBar.js +170 -0
  35. package/dist/tui/TextPrompt.js +41 -0
  36. package/dist/tui/TuiApp.js +29 -0
  37. package/dist/tui/TuiEditor.js +490 -0
  38. package/dist/tui/edit.js +317 -0
  39. package/dist/tui/keys.js +195 -0
  40. package/dist/tui/log-window.js +93 -0
  41. package/dist/tui/login-pure.js +20 -0
  42. package/dist/tui/run-config-menu.js +19 -0
  43. package/dist/tui/run-confirm.js +22 -0
  44. package/dist/tui/run-login.js +43 -0
  45. package/dist/tui/run-select.js +18 -0
  46. package/dist/tui/store.js +104 -0
  47. package/dist/tui/types.js +10 -0
  48. package/dist/tui/useMouseWheel.js +164 -0
  49. package/dist/tui/widgets.js +60 -0
  50. package/package.json +7 -3
  51. package/scripts/coverage-gate.mts +10 -0
  52. package/scripts/inline-status-driver.mts +31 -0
  53. package/scripts/inline-status.tsx +33 -0
  54. package/scripts/mouse-churn.mts +34 -0
  55. package/scripts/qwen-models-smoke.mts +117 -0
  56. package/scripts/render-demo.mts +1 -1
  57. package/scripts/tui-comp-shot.mts +63 -0
  58. package/scripts/tui-components.tsx +62 -0
  59. package/scripts/tui-config-driver.mts +82 -0
  60. package/scripts/tui-config-probe.tsx +74 -0
  61. package/scripts/tui-edit-driver.mts +100 -0
  62. package/scripts/tui-edit-probe.tsx +38 -0
  63. package/scripts/tui-editor-probe.tsx +24 -0
  64. package/scripts/tui-input-driver.mts +54 -0
  65. package/scripts/tui-input-shot.tsx +59 -0
  66. package/scripts/tui-log-driver.mts +35 -0
  67. package/scripts/tui-log-shot.tsx +48 -0
  68. package/scripts/tui-longpaste-driver.mts +56 -0
  69. package/scripts/tui-longpaste.tsx +35 -0
  70. package/scripts/tui-manyselect-shot.mts +34 -0
  71. package/scripts/tui-manyselect.tsx +17 -0
  72. package/scripts/tui-mouse-churn-probe.tsx +28 -0
  73. package/scripts/tui-paste-driver.mts +88 -0
  74. package/scripts/tui-paste-kinds.tsx +76 -0
  75. package/scripts/tui-pastetest.mts +38 -0
  76. package/scripts/tui-probe.tsx +62 -0
  77. package/scripts/tui-repro.tsx +64 -0
  78. package/scripts/tui-resize.mts +70 -0
  79. package/scripts/tui-scroll-driver.mts +53 -0
  80. package/scripts/tui-scroll-probe.tsx +46 -0
  81. package/scripts/tui-shot.mts +106 -0
  82. package/scripts/tui-status-driver.mts +24 -0
  83. package/scripts/tui-status-shot.tsx +27 -0
  84. package/scripts/tui-submit.tsx +57 -0
  85. package/scripts/tui-wheel-driver.mts +61 -0
  86. package/scripts/tui-wheel-probe.tsx +48 -0
  87. package/scripts/tui-widgets-driver.mts +20 -0
  88. 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()
@@ -21,7 +21,7 @@ const out = path.join(root, 'docs', 'demo.png')
21
21
 
22
22
  const C = {
23
23
  bg: '#23272e',
24
- user: '#b4b8d0',
24
+ user: '#8fd4f0',
25
25
  prompt: '#c8b06a',
26
26
  dir: '#7fc4f0',
27
27
  assistant: '#cfc9b0',
@@ -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)