zames_pro 2.74.0 → 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 (56) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/README.md +24 -0
  3. package/dist/attachments.js +6 -1
  4. package/dist/config.js +7 -7
  5. package/dist/i18n/common.js +4 -0
  6. package/dist/i18n/help.js +8 -0
  7. package/dist/index.js +9 -0
  8. package/dist/input/suggest.js +19 -4
  9. package/dist/theme.js +9 -0
  10. package/dist/tui/ConfigMenu.js +44 -11
  11. package/dist/tui/FullScreenApp.js +37 -9
  12. package/dist/tui/InputLine.js +52 -19
  13. package/dist/tui/SelectList.js +6 -2
  14. package/dist/tui/StatusBar.js +66 -36
  15. package/dist/tui/TuiApp.js +3 -2
  16. package/dist/tui/edit.js +21 -1
  17. package/dist/tui/keys.js +33 -0
  18. package/dist/tui/store.js +1 -1
  19. package/dist/tui/useMouseWheel.js +80 -29
  20. package/dist/tui/widgets.js +6 -1
  21. package/package.json +2 -1
  22. package/scripts/inline-status-driver.mts +31 -0
  23. package/scripts/inline-status.tsx +33 -0
  24. package/scripts/mouse-churn.mts +34 -0
  25. package/scripts/qwen-models-smoke.mts +117 -0
  26. package/scripts/tui-comp-shot.mts +63 -0
  27. package/scripts/tui-components.tsx +62 -0
  28. package/scripts/tui-config-driver.mts +82 -0
  29. package/scripts/tui-config-probe.tsx +74 -0
  30. package/scripts/tui-edit-driver.mts +100 -0
  31. package/scripts/tui-edit-probe.tsx +38 -0
  32. package/scripts/tui-editor-probe.tsx +24 -0
  33. package/scripts/tui-input-driver.mts +54 -0
  34. package/scripts/tui-input-shot.tsx +59 -0
  35. package/scripts/tui-log-driver.mts +35 -0
  36. package/scripts/tui-log-shot.tsx +48 -0
  37. package/scripts/tui-longpaste-driver.mts +56 -0
  38. package/scripts/tui-longpaste.tsx +35 -0
  39. package/scripts/tui-manyselect-shot.mts +34 -0
  40. package/scripts/tui-manyselect.tsx +17 -0
  41. package/scripts/tui-mouse-churn-probe.tsx +28 -0
  42. package/scripts/tui-paste-driver.mts +88 -0
  43. package/scripts/tui-paste-kinds.tsx +76 -0
  44. package/scripts/tui-pastetest.mts +38 -0
  45. package/scripts/tui-repro.tsx +64 -0
  46. package/scripts/tui-resize.mts +70 -0
  47. package/scripts/tui-scroll-driver.mts +53 -0
  48. package/scripts/tui-scroll-probe.tsx +46 -0
  49. package/scripts/tui-shot.mts +106 -0
  50. package/scripts/tui-status-driver.mts +24 -0
  51. package/scripts/tui-status-shot.tsx +27 -0
  52. package/scripts/tui-submit.tsx +57 -0
  53. package/scripts/tui-wheel-driver.mts +61 -0
  54. package/scripts/tui-wheel-probe.tsx +48 -0
  55. package/scripts/tui-widgets-driver.mts +20 -0
  56. package/scripts/tui-widgets.tsx +44 -0
@@ -0,0 +1,35 @@
1
+ // Screenshot the FullScreenApp log for a given content spec.
2
+ import { spawn } from 'node:child_process'
3
+ import path from 'node:path'
4
+ import { Term } from '../test/vt100.mts'
5
+
6
+ const NL = String.fromCharCode(10)
7
+ function arg(n: string, d = ''): string {
8
+ const h = process.argv.find((a) => a.startsWith('--' + n + '='))
9
+ return h ? h.slice(n.length + 3) : d
10
+ }
11
+ const cols = Number(arg('cols', '80'))
12
+ const rows = Number(arg('rows', '24'))
13
+ const spec = arg('log', 'empty')
14
+
15
+ const term = new Term(rows, cols)
16
+ const child = spawn(
17
+ 'script',
18
+ ['-qec', 'npx tsx scripts/tui-log-shot.tsx', '/dev/null'],
19
+ {
20
+ cwd: path.resolve('.'),
21
+ env: {
22
+ ...process.env,
23
+ COLUMNS: String(cols),
24
+ LINES: String(rows),
25
+ PROBE_LOG: spec,
26
+ },
27
+ },
28
+ )
29
+ child.stdout.on('data', (d) => term.write(d.toString('utf-8')))
30
+ child.stderr.on('data', (d) => term.write(d.toString('utf-8')))
31
+ setTimeout(() => {
32
+ process.stdout.write(term.render() + NL)
33
+ child.kill('SIGTERM')
34
+ }, 5000)
35
+ child.on('close', () => process.exit(0))
@@ -0,0 +1,48 @@
1
+ // Mount the REAL FullScreenApp with a log driven by PROBE_LOG (JSON entries),
2
+ // so empty/overflowing/long-wrapped logs 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
+
9
+ const store = new TuiStore()
10
+ store.set({
11
+ title: 'zames',
12
+ version: '2.74',
13
+ workdir: '/tmp',
14
+ prompt: '\u2794 zames \u203a ',
15
+ locale: 'en',
16
+ commands: [],
17
+ edit: { ...initialEditState(), buf: '' },
18
+ })
19
+ const spec = process.env.PROBE_LOG || 'empty'
20
+ if (spec === 'empty') {
21
+ // nothing
22
+ } else if (spec === 'few') {
23
+ store.pushLog('user', 'hello')
24
+ store.pushLog('assistant', 'hi there')
25
+ } else if (spec === 'many') {
26
+ for (let i = 0; i < 100; i++) store.pushLog('system', 'log line ' + i)
27
+ } else if (spec === 'long') {
28
+ store.pushLog('assistant', 'L'.repeat(300))
29
+ store.pushLog('system', 'after long')
30
+ }
31
+ const inputProps = {
32
+ onSubmit: () => {},
33
+ onEscape: () => {},
34
+ onCtrlC: () => {},
35
+ onCopyCode: () => {},
36
+ onAttach: () => null,
37
+ onClipboard: () => Promise.resolve(null),
38
+ onHistoryChange: () => {},
39
+ onClearScreen: () => {},
40
+ attachments: [],
41
+ onPastesChange: () => {},
42
+ }
43
+ render(React.createElement(FullScreenApp, { store, inputProps }), {
44
+ exitOnCtrlC: false,
45
+ patchConsole: false,
46
+ alternateScreen: true,
47
+ })
48
+ setInterval(() => {}, 1 << 30)
@@ -0,0 +1,56 @@
1
+ // Paste a long bracketed block into the REAL TuiEditor under a pty, screenshot
2
+ // the screen (marker shown), press Enter, and print the submit log.
3
+ import { spawn } from 'node:child_process'
4
+ import fs from 'node:fs'
5
+ import path from 'node:path'
6
+ import { Term } from '../test/vt100.mts'
7
+ const ESC = String.fromCharCode(27)
8
+ const CR = String.fromCharCode(13)
9
+ const NL = String.fromCharCode(10)
10
+ const LOG = '/tmp/longpaste.log'
11
+ try {
12
+ fs.unlinkSync(LOG)
13
+ } catch {
14
+ /* ignore */
15
+ }
16
+ const lines = Number(process.argv[2] || '25')
17
+ const body = Array.from(
18
+ { length: lines },
19
+ (_, i) => 'line ' + i + ' of the pasted block',
20
+ ).join(NL)
21
+ const cols = Number(process.argv[3] || '80')
22
+ const rows = Number(process.argv[4] || '24')
23
+ const term = new Term(rows, cols)
24
+ const child = spawn(
25
+ 'script',
26
+ ['-qec', 'npx tsx scripts/tui-longpaste.tsx', '/dev/null'],
27
+ {
28
+ cwd: path.resolve('.'),
29
+ env: {
30
+ ...process.env,
31
+ COLUMNS: String(cols),
32
+ LINES: String(rows),
33
+ PROBE_PASTE_LOG: LOG,
34
+ },
35
+ },
36
+ )
37
+ child.stdout.on('data', (d) => term.write(d.toString('utf-8')))
38
+ child.stderr.on('data', (d) => term.write(d.toString('utf-8')))
39
+ setTimeout(() => {
40
+ child.stdin.write(ESC + '[200~' + body + ESC + '[201~')
41
+ setTimeout(() => {
42
+ process.stdout.write('=== SCREEN (after paste) ===' + NL)
43
+ process.stdout.write(term.render() + NL)
44
+ child.stdin.write(CR)
45
+ setTimeout(() => {
46
+ process.stdout.write('=== LOG ===' + NL)
47
+ try {
48
+ process.stdout.write(fs.readFileSync(LOG, 'utf-8'))
49
+ } catch {
50
+ /* ignore */
51
+ }
52
+ child.kill('SIGTERM')
53
+ }, 600)
54
+ }, 600)
55
+ }, 5000)
56
+ child.on('close', () => process.exit(0))
@@ -0,0 +1,35 @@
1
+ // Mount the REAL TuiEditor and log every onSubmit (the EXPANDED text), so a pty
2
+ // can paste a LONG multi-line block and prove the agent receives all of it —
3
+ // not the [Pasted lines#N] marker.
4
+ import fs from 'node:fs'
5
+ import { TuiEditor } from '../src/tui/TuiEditor.js'
6
+ const LOG = process.env.PROBE_PASTE_LOG || '/tmp/longpaste.log'
7
+ fs.writeFileSync(LOG, '')
8
+ const ed = new TuiEditor({
9
+ prompt: '> ',
10
+ commands: [],
11
+ locale: 'en',
12
+ alternateScreen: true,
13
+ })
14
+ ed.onSubmit = (text: string) => {
15
+ fs.appendFileSync(
16
+ LOG,
17
+ 'SUBMIT_LEN=' +
18
+ text.length +
19
+ ' LINES=' +
20
+ text.split(String.fromCharCode(10)).length +
21
+ String.fromCharCode(10),
22
+ )
23
+ fs.appendFileSync(
24
+ LOG,
25
+ 'HEAD=' + JSON.stringify(text.slice(0, 40)) + String.fromCharCode(10),
26
+ )
27
+ fs.appendFileSync(
28
+ LOG,
29
+ 'TAIL=' + JSON.stringify(text.slice(-40)) + String.fromCharCode(10),
30
+ )
31
+ }
32
+ ed.onAttach = () => null
33
+ ed.onClipboard = () => Promise.resolve(null)
34
+ ed.start()
35
+ setInterval(() => {}, 1 << 30)
@@ -0,0 +1,34 @@
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 ESC = String.fromCharCode(27)
6
+ const keys = (process.argv[2] || '')
7
+ .split(',')
8
+ .filter(Boolean)
9
+ .map((k) => (k === 'UP' ? ESC + '[A' : k === 'DOWN' ? ESC + '[B' : k))
10
+ const term = new Term(20, 60)
11
+ const child = spawn(
12
+ 'script',
13
+ ['-qec', 'npx tsx scripts/tui-manyselect.tsx', '/dev/null'],
14
+ {
15
+ cwd: path.resolve('.'),
16
+ env: { ...process.env, COLUMNS: '60', LINES: '20' },
17
+ },
18
+ )
19
+ child.stdout.on('data', (d) => term.write(d.toString('utf-8')))
20
+ child.stderr.on('data', (d) => term.write(d.toString('utf-8')))
21
+ let i = 0
22
+ const tick = (): void => {
23
+ if (i < keys.length) {
24
+ child.stdin.write(keys[i])
25
+ i++
26
+ setTimeout(tick, 250)
27
+ } else
28
+ setTimeout(() => {
29
+ process.stdout.write(term.render() + NL)
30
+ child.kill('SIGTERM')
31
+ }, 400)
32
+ }
33
+ setTimeout(tick, 4500)
34
+ child.on('close', () => process.exit(0))
@@ -0,0 +1,17 @@
1
+ import React from 'react'
2
+ import { render } from 'ink'
3
+ import { SelectList } from '../src/tui/SelectList.js'
4
+ const items = Array.from({ length: 30 }, (_, i) => ({
5
+ value: 'm' + i,
6
+ label: 'модель-' + i,
7
+ }))
8
+ render(
9
+ React.createElement(SelectList, {
10
+ title: 'Много',
11
+ items,
12
+ initial: 'm15',
13
+ onSelect: () => {},
14
+ }),
15
+ { exitOnCtrlC: false, patchConsole: false },
16
+ )
17
+ setInterval(() => {}, 1 << 30)
@@ -0,0 +1,28 @@
1
+ // Mount the REAL TuiEditor and tick the store every 150ms (like the 1s badge
2
+ // timer does), so the raw stdout can be scanned for how many times the
3
+ // mouse-mode sequences are written. A well-behaved hook writes them ONCE on
4
+ // mount; a hook whose effect depends on an inline `onWheel` re-runs on every
5
+ // render and writes `1007h` (alternate-scroll ON => wheel becomes history)
6
+ // again and again.
7
+
8
+ import { TuiEditor } from '../src/tui/TuiEditor.js'
9
+
10
+ const ed = new TuiEditor({
11
+ prompt: '> ',
12
+ commands: [],
13
+ locale: 'en',
14
+ alternateScreen: true,
15
+ })
16
+ ed.onSubmit = () => {}
17
+ ed.onAttach = () => null
18
+ ed.onClipboard = () => Promise.resolve(null)
19
+ ed.start()
20
+
21
+ let n = 0
22
+ const timer = setInterval(() => {
23
+ // A no-op-ish store change: bump the queue badge, like refreshBadges does.
24
+ ed.store.set({ queueLength: n++ })
25
+ if (n > 20) clearInterval(timer)
26
+ }, 150)
27
+
28
+ setInterval(() => {}, 1 << 30)
@@ -0,0 +1,88 @@
1
+ // Feed a scripted set of bracketed pastes + Enter into the REAL TuiEditor
2
+ // probe under a pty, then print /tmp/paste-kinds.log (what onSubmit received).
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 CR = String.fromCharCode(13)
9
+ const LOG = '/tmp/paste-kinds.log'
10
+ const TMP = '/tmp/paste-probe-tmp'
11
+ for (const f of [LOG]) {
12
+ try {
13
+ fs.unlinkSync(f)
14
+ } catch {
15
+ /* ignore */
16
+ }
17
+ }
18
+ fs.mkdirSync(TMP, { recursive: true })
19
+
20
+ const png =
21
+ 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+M8AAAMBAQDJ/pLvAAAAAElFTkSuQmCC'
22
+ const dataUrl = 'data:image/png;base64,' + png
23
+ const existingFile = path.resolve('README.md')
24
+
25
+ const pastes = [
26
+ 'hello plain text',
27
+ 'line1' +
28
+ String.fromCharCode(10) +
29
+ 'line2' +
30
+ String.fromCharCode(10) +
31
+ 'line3' +
32
+ String.fromCharCode(10) +
33
+ 'line4',
34
+ existingFile,
35
+ dataUrl,
36
+ ]
37
+
38
+ const child = spawn(
39
+ 'script',
40
+ ['-qec', 'npx tsx scripts/tui-paste-kinds.tsx', '/dev/null'],
41
+ {
42
+ cwd: path.resolve('.'),
43
+ env: {
44
+ ...process.env,
45
+ COLUMNS: '90',
46
+ LINES: '28',
47
+ PROBE_PASTE_LOG: LOG,
48
+ PROBE_PASTE_TMP: TMP,
49
+ },
50
+ },
51
+ )
52
+ child.stdout.on('data', () => {})
53
+ child.stderr.on('data', () => {})
54
+
55
+ const enc = (s: string): string => ESC + '[200~' + s + ESC + '[201~'
56
+ let i = 0
57
+ const step = (): void => {
58
+ if (i < pastes.length) {
59
+ child.stdin.write(enc(pastes[i]))
60
+ setTimeout(() => {
61
+ child.stdin.write(CR)
62
+ i++
63
+ setTimeout(step, 500)
64
+ }, 500)
65
+ } else {
66
+ setTimeout(() => child.kill('SIGTERM'), 600)
67
+ }
68
+ }
69
+ setTimeout(step, 5000)
70
+ child.on('close', () => {
71
+ let out = ''
72
+ try {
73
+ out = fs.readFileSync(LOG, 'utf-8')
74
+ } catch {
75
+ out = '(no log)'
76
+ }
77
+ process.stdout.write(out + String.fromCharCode(10))
78
+ try {
79
+ process.stdout.write('--- tmp files ---' + String.fromCharCode(10))
80
+ process.stdout.write(
81
+ fs.readdirSync(TMP).join(String.fromCharCode(10)) +
82
+ String.fromCharCode(10),
83
+ )
84
+ } catch {
85
+ /* ignore */
86
+ }
87
+ process.exit(0)
88
+ })
@@ -0,0 +1,76 @@
1
+ // Drive the REAL TuiEditor under a pty and log every onSubmit (text +
2
+ // attachments) so all paste kinds can be exercised: plain text, a big
3
+ // multi-line paste, a file path, and a data: URL image. The attach/clipboard
4
+ // callbacks mirror index.ts (parseImagePaste -> saveToTemp; resolveAttachPath
5
+ // -> readFile) so the probe exercises the same code path the live agent does.
6
+
7
+ import fs from 'node:fs'
8
+ import fsp from 'node:fs/promises'
9
+ import path from 'node:path'
10
+ import { TuiEditor } from '../src/tui/TuiEditor.js'
11
+ import {
12
+ contentHash,
13
+ extForMime,
14
+ guessMime,
15
+ parseImagePaste,
16
+ saveToTemp,
17
+ } from '../src/attachments.js'
18
+ import { resolveAttachPath } from '../src/attach-refs.js'
19
+
20
+ const LOG = process.env.PROBE_PASTE_LOG || '/tmp/paste-kinds.log'
21
+ const TMP = process.env.PROBE_PASTE_TMP || '/tmp/paste-probe-tmp'
22
+ const WORKDIR = process.cwd()
23
+ const line = (s: string): void => {
24
+ fs.appendFileSync(LOG, s + String.fromCharCode(10))
25
+ }
26
+
27
+ const ed = new TuiEditor({
28
+ prompt: '> ',
29
+ commands: [],
30
+ locale: 'en',
31
+ alternateScreen: true,
32
+ })
33
+
34
+ ed.onSubmit = (text, atts) => {
35
+ line(
36
+ 'SUBMIT text=' +
37
+ JSON.stringify(text) +
38
+ ' atts=' +
39
+ JSON.stringify(
40
+ atts.map((a) => ({ marker: a.marker, image: a.image, name: a.name })),
41
+ ),
42
+ )
43
+ }
44
+ ed.onAttach = async (raw: string) => {
45
+ const image = parseImagePaste(raw)
46
+ if (image) {
47
+ const name = 'paste' + extForMime(image.mime)
48
+ const p = await saveToTemp(TMP, name, image.data)
49
+ const att = ed.attachments.add({
50
+ path: p,
51
+ name,
52
+ mime: image.mime,
53
+ size: image.data.length,
54
+ hash: contentHash(image.data),
55
+ })
56
+ line('ATTACH_IMAGE ' + att.marker + ' ' + p)
57
+ return att
58
+ }
59
+ const filePath = await resolveAttachPath(WORKDIR, raw)
60
+ if (!filePath) return null
61
+ const data = await fsp.readFile(filePath).catch(() => null)
62
+ if (!data) return null
63
+ const name = path.basename(filePath)
64
+ const att = ed.attachments.add({
65
+ path: filePath,
66
+ name,
67
+ mime: guessMime(name),
68
+ size: data.length,
69
+ })
70
+ line('ATTACH_FILE ' + att.marker + ' ' + filePath)
71
+ return att
72
+ }
73
+ ed.onClipboard = () => Promise.resolve(null)
74
+
75
+ ed.start()
76
+ setInterval(() => {}, 1 << 30)
@@ -0,0 +1,38 @@
1
+ // Reproduce the [Pasted lines#N] path end to end under a pty: send ONE
2
+ // bracketed-paste chunk with several lines, press Enter, and print what the
3
+ // onSubmit callback actually received (that is what reaches the agent).
4
+ import { spawn } from 'node:child_process'
5
+ import path from 'node:path'
6
+ import fs from 'node:fs'
7
+
8
+ const ESC = String.fromCharCode(27)
9
+ const LF = String.fromCharCode(10)
10
+ const CR = String.fromCharCode(13)
11
+ const LOG = '/tmp/paste-submit.log'
12
+ fs.writeFileSync(LOG, '')
13
+
14
+ const child = spawn(
15
+ 'script',
16
+ ['-qec', 'npx tsx scripts/tui-submit.tsx', '/dev/null'],
17
+ {
18
+ cwd: path.resolve('.'),
19
+ env: { ...process.env, COLUMNS: '80', LINES: '24', PROBE_SUBMIT_LOG: LOG },
20
+ },
21
+ )
22
+ child.stdout.on('data', () => {})
23
+ child.stderr.on('data', () => {})
24
+
25
+ setTimeout(() => {
26
+ const paste =
27
+ ESC + '[200~' + ['l1', 'l2', 'l3', 'l4', 'l5'].join(LF) + ESC + '[201~'
28
+ child.stdin.write(paste)
29
+ setTimeout(() => {
30
+ child.stdin.write(CR)
31
+ setTimeout(() => child.kill('SIGTERM'), 800)
32
+ }, 800)
33
+ }, 4500)
34
+
35
+ child.on('close', () => {
36
+ process.stdout.write(fs.readFileSync(LOG, 'utf-8'))
37
+ process.exit(0)
38
+ })
@@ -0,0 +1,64 @@
1
+ // Reproduce the LIVE TUI scenario for the input-box geometry bug.
2
+ //
3
+ // The stock probe uses a bare `> ` prompt, an empty command list and no header, which is why the gated pty test stays green while the live agent still overprints the second input line. This probe mounts the same FullScreenApp with a realistic prompt, a non-empty slash command list and a long title.
4
+ //
5
+ // NOT shipped: a dev probe only.
6
+
7
+ import React from 'react'
8
+ import { render } from 'ink'
9
+ import { TuiStore } from '../src/tui/store.js'
10
+ import { FullScreenApp } from '../src/tui/FullScreenApp.js'
11
+ import { TuiApp } from '../src/tui/TuiApp.js'
12
+ import { initialEditState } from '../src/tui/edit.js'
13
+
14
+ const store = new TuiStore()
15
+ const commands = [
16
+ { name: '/help', description: 'show help' },
17
+ { name: '/history', description: 'input history' },
18
+ { name: '/new', description: 'new chat' },
19
+ { name: '/resume', description: 'open a chat' },
20
+ { name: '/config', description: 'settings' },
21
+ {
22
+ name: '/self-review',
23
+ description:
24
+ 'snapshot src/ and review the current working tree for bugs and risks',
25
+ },
26
+ ]
27
+ store.set({
28
+ title: 'zames_pro',
29
+ version: '2.34.0',
30
+ workdir: '/workspaces/zames/zames_pro',
31
+ prompt: '➔ zames_pro › ',
32
+ locale: 'en',
33
+ commands,
34
+ edit: { ...initialEditState(), buf: process.argv[2] ?? '' },
35
+ })
36
+
37
+ store.pushLog('system', 'log line one')
38
+ store.pushLog('system', 'log line two')
39
+
40
+ const INLINE = process.env.PROBE_INLINE === '1'
41
+
42
+ const inputProps = {
43
+ onSubmit: () => {},
44
+ onEscape: () => {},
45
+ onCtrlC: () => {},
46
+ onCopyCode: () => {},
47
+ onAttach: () => null,
48
+ onClipboard: () => Promise.resolve(null),
49
+ onHistoryChange: () => {},
50
+ onClearScreen: () => {},
51
+ attachments: [],
52
+ onPastesChange: () => {},
53
+ }
54
+
55
+ render(
56
+ INLINE ? (
57
+ <TuiApp store={store} inputProps={inputProps} />
58
+ ) : (
59
+ <FullScreenApp store={store} inputProps={inputProps} />
60
+ ),
61
+ { exitOnCtrlC: false, patchConsole: false, alternateScreen: !INLINE },
62
+ )
63
+
64
+ setInterval(() => {}, 1 << 30)
@@ -0,0 +1,70 @@
1
+ // Resize a REAL TuiEditor under a pty and screenshot the final grid. The pty
2
+ // WINSIZE is changed mid-run so the kernel delivers SIGWINCH, which exercises
3
+ // TuiEditor.installResizeHandler (unmount + remount), not just Ink's erase.
4
+ //
5
+ // Usage: npx tsx scripts/tui-resize.mts [sc sr nc nr text]
6
+ import { spawnSync } from 'node:child_process'
7
+ import { Term } from '../test/vt100.mts'
8
+
9
+ const NL = String.fromCharCode(10)
10
+ const sc = Number(process.argv[2] || 80)
11
+ const sr = Number(process.argv[3] || 24)
12
+ const nc = Number(process.argv[4] || 100)
13
+ const nr = Number(process.argv[5] || 30)
14
+ const text = process.argv[6] || ''
15
+
16
+ // Inline python pty harness (no external file).
17
+ const py = [
18
+ 'import os,pty,sys,time,fcntl,termios,struct,signal,select',
19
+ 'sc,sr,nc,nr=' + sc + ',' + sr + ',' + nc + ',' + nr,
20
+ 'text=' + JSON.stringify(text),
21
+ 'pid,fd=pty.fork()',
22
+ 'if pid==0:',
23
+ " os.environ['COLUMNS']=str(sc); os.environ['LINES']=str(sr)",
24
+ " os.execvp('npx',['npx','tsx','scripts/tui-editor-probe.tsx'])",
25
+ 'else:',
26
+ " def sz(c,r): fcntl.ioctl(fd,termios.TIOCSWINSZ,struct.pack('HHHH',r,c,0,0))",
27
+ ' sz(sc,sr)',
28
+ " buf=b''; start=time.time(); sent=False; resized=False",
29
+ ' while time.time()-start<14:',
30
+ ' t=time.time()-start',
31
+ ' if not sent and t>5:',
32
+ ' for ch in text:',
33
+ ' os.write(fd,ch.encode()); time.sleep(0.12)',
34
+ ' sent=True',
35
+ ' if not resized and t>9:',
36
+ ' sz(nc,nr); os.kill(pid,signal.SIGWINCH); resized=True',
37
+ ' r,_,_=select.select([fd],[],[],0.2)',
38
+ ' if r:',
39
+ ' try:',
40
+ ' d=os.read(fd,65536)',
41
+ ' if not d: break',
42
+ ' buf+=d',
43
+ ' except OSError: break',
44
+ ' time.sleep(0.5)',
45
+ ' try:',
46
+ ' while True:',
47
+ ' r,_,_=select.select([fd],[],[],0.2)',
48
+ ' if not r: break',
49
+ ' d=os.read(fd,65536)',
50
+ ' if not d: break',
51
+ ' buf+=d',
52
+ ' except OSError: pass',
53
+ ' try: os.kill(pid,signal.SIGKILL)',
54
+ ' except ProcessLookupError: pass',
55
+ ' sys.stdout.buffer.write(buf)',
56
+ ].join(NL)
57
+
58
+ const out = spawnSync('python3', ['-c', py], {
59
+ encoding: 'buffer',
60
+ maxBuffer: 64 * 1024 * 1024,
61
+ })
62
+ const term = new Term(Math.max(sr, nr), Math.max(sc, nc))
63
+ term.write(out.stdout.toString('utf-8'))
64
+ process.stdout.write(
65
+ term
66
+ .render()
67
+ .split(NL)
68
+ .map((l, n) => String(n).padStart(2) + '| ' + l)
69
+ .join(NL) + NL,
70
+ )
@@ -0,0 +1,53 @@
1
+ // Press keys on the scroll probe and print the scroll values it saw. Accepts
2
+ // several End/Home encodings so a terminal that sends a non-xterm form can be
3
+ // diagnosed.
4
+ import { spawn } from 'node:child_process'
5
+ import fs from 'node:fs'
6
+ import path from 'node:path'
7
+ const ESC = String.fromCharCode(27)
8
+ const LOG = '/tmp/scroll-probe.log'
9
+ try {
10
+ fs.unlinkSync(LOG)
11
+ } catch {
12
+ /* ignore */
13
+ }
14
+ const map: Record<string, string> = {
15
+ PGUP: ESC + '[5~',
16
+ PGDN: ESC + '[6~',
17
+ HOME: ESC + '[H',
18
+ END: ESC + '[F',
19
+ END4: ESC + '[4~',
20
+ HOME1: ESC + '[1~',
21
+ CEND: ESC + '[1;5F',
22
+ CHOME: ESC + '[1;5H',
23
+ SPGDN: ESC + '[6;2~',
24
+ SPGUP: ESC + '[5;2~',
25
+ }
26
+ const keys = (process.argv[2] || 'PGUP').split(',').map((k) => map[k] ?? k)
27
+ const child = spawn(
28
+ 'script',
29
+ ['-qec', 'npx tsx scripts/tui-scroll-probe.tsx', '/dev/null'],
30
+ {
31
+ cwd: path.resolve('.'),
32
+ env: { ...process.env, COLUMNS: '80', LINES: '24', PROBE_SCROLL_LOG: LOG },
33
+ },
34
+ )
35
+ child.stdout.on('data', () => {})
36
+ child.stderr.on('data', () => {})
37
+ let i = 0
38
+ const tick = (): void => {
39
+ if (i < keys.length) {
40
+ child.stdin.write(keys[i])
41
+ i++
42
+ setTimeout(tick, 250)
43
+ } else setTimeout(() => child.kill('SIGTERM'), 400)
44
+ }
45
+ setTimeout(tick, 5000)
46
+ child.on('close', () => {
47
+ const lines = fs
48
+ .readFileSync(LOG, 'utf-8')
49
+ .trim()
50
+ .split(String.fromCharCode(10))
51
+ console.log('SCROLL=' + JSON.stringify(lines.map(Number)))
52
+ process.exit(0)
53
+ })