zames_pro 2.75.4 → 2.75.6

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 (79) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/README.md +12 -6
  3. package/dist/agent-loop.js +61 -1
  4. package/dist/browser.js +40 -8
  5. package/dist/commands.js +0 -1
  6. package/dist/config.js +8 -3
  7. package/dist/deepseek-ui.js +1 -3
  8. package/dist/i18n/common.js +13 -16
  9. package/dist/i18n/config.js +2 -2
  10. package/dist/i18n/help.js +6 -10
  11. package/dist/i18n/spinner.js +16 -5
  12. package/dist/index.js +0 -49
  13. package/dist/net-capture.js +43 -11
  14. package/dist/provider-handoff.js +0 -4
  15. package/dist/qwen-ui.js +0 -8
  16. package/dist/run-task.js +24 -1
  17. package/dist/shell.js +0 -6
  18. package/dist/system-prompt.js +52 -2
  19. package/dist/tools.js +33 -0
  20. package/dist/tui/AskQuestion.js +108 -0
  21. package/dist/tui/FullScreenApp.js +2 -1
  22. package/dist/tui/InputLine.js +0 -3
  23. package/dist/tui/StatusBar.js +5 -10
  24. package/dist/tui/TuiEditor.js +0 -43
  25. package/dist/tui/keys.js +0 -2
  26. package/dist/tui/run-ask.js +22 -0
  27. package/dist/tui/store.js +0 -2
  28. package/dist/tui/widgets.js +3 -9
  29. package/package.json +3 -2
  30. package/scripts/build-readme.mts +0 -101
  31. package/scripts/changelog-draft.mts +0 -93
  32. package/scripts/clean-dist.mts +0 -17
  33. package/scripts/coverage-gate.mts +0 -112
  34. package/scripts/ctd-live-driver.mts +0 -51
  35. package/scripts/ctd-live.tsx +0 -33
  36. package/scripts/escd-live-driver.mts +0 -37
  37. package/scripts/eslint-ts6.mts +0 -58
  38. package/scripts/inline-status-driver.mts +0 -31
  39. package/scripts/inline-status.tsx +0 -33
  40. package/scripts/mouse-churn.mts +0 -34
  41. package/scripts/qwen-models-smoke.mts +0 -117
  42. package/scripts/qwen-smoke.mts +0 -221
  43. package/scripts/release.mts +0 -199
  44. package/scripts/render-demo.mts +0 -154
  45. package/scripts/render-how-it-works.mts +0 -229
  46. package/scripts/ru-shot.mts +0 -20
  47. package/scripts/self-smoke.mts +0 -309
  48. package/scripts/tui-comp-shot.mts +0 -63
  49. package/scripts/tui-components.tsx +0 -62
  50. package/scripts/tui-config-driver.mts +0 -82
  51. package/scripts/tui-config-probe.tsx +0 -74
  52. package/scripts/tui-edit-driver.mts +0 -100
  53. package/scripts/tui-edit-probe.tsx +0 -38
  54. package/scripts/tui-editor-probe.tsx +0 -24
  55. package/scripts/tui-input-driver.mts +0 -54
  56. package/scripts/tui-input-shot.tsx +0 -59
  57. package/scripts/tui-log-driver.mts +0 -35
  58. package/scripts/tui-log-shot.tsx +0 -48
  59. package/scripts/tui-longpaste-driver.mts +0 -56
  60. package/scripts/tui-longpaste.tsx +0 -35
  61. package/scripts/tui-manyselect-shot.mts +0 -34
  62. package/scripts/tui-manyselect.tsx +0 -17
  63. package/scripts/tui-mouse-churn-probe.tsx +0 -28
  64. package/scripts/tui-paste-driver.mts +0 -88
  65. package/scripts/tui-paste-kinds.tsx +0 -76
  66. package/scripts/tui-pastetest.mts +0 -38
  67. package/scripts/tui-probe.tsx +0 -62
  68. package/scripts/tui-repro.tsx +0 -64
  69. package/scripts/tui-resize.mts +0 -70
  70. package/scripts/tui-scroll-driver.mts +0 -53
  71. package/scripts/tui-scroll-probe.tsx +0 -46
  72. package/scripts/tui-shot.mts +0 -106
  73. package/scripts/tui-status-driver.mts +0 -24
  74. package/scripts/tui-status-shot.tsx +0 -27
  75. package/scripts/tui-submit.tsx +0 -57
  76. package/scripts/tui-wheel-driver.mts +0 -61
  77. package/scripts/tui-wheel-probe.tsx +0 -48
  78. package/scripts/tui-widgets-driver.mts +0 -20
  79. package/scripts/tui-widgets.tsx +0 -44
@@ -1,93 +0,0 @@
1
- import { spawnSync } from 'node:child_process'
2
- import fs from 'node:fs'
3
- import path from 'node:path'
4
- import { fileURLToPath } from 'node:url'
5
- import {
6
- parseConventionalCommit,
7
- groupCommits,
8
- renderChangelogDraft,
9
- syncUnreleased,
10
- } from '../src/changelog.js'
11
-
12
- // Draft a CHANGELOG section from conventional commits since a ref (BACKLOG
13
- // D2). PRINTS to stdout and never touches CHANGELOG.md -- the operator (or
14
- // the agent) pastes the result in by hand, so a bad commit subject can never
15
- // corrupt the changelog.
16
- //
17
- // Usage:
18
- // npm run changelog:draft # since the latest tag, or an explicit ref
19
- // npm run changelog:draft -- v2.63.0
20
- // npm run changelog:draft -- --write # update the Unreleased section in place
21
- // npm run changelog:draft -- --check # exit 1 if Unreleased is out of date
22
-
23
- function git(args: string[]): string {
24
- const r = spawnSync('git', args, { encoding: 'utf-8' })
25
- if (r.status !== 0) return ''
26
- return (r.stdout || '').toString()
27
- }
28
-
29
- const argv = process.argv.slice(2)
30
- const write = argv.includes('--write')
31
- const check = argv.includes('--check')
32
- const argRef = argv.find((a) => !a.startsWith('-'))
33
-
34
- // Resolve the start ref: an explicit arg, otherwise the latest tag,
35
- // otherwise the root commit.
36
- const latestTag = git(['describe', '--tags', '--abbrev=0']).trim()
37
- const ref =
38
- argRef ||
39
- latestTag ||
40
- git(['revlist', 'HEAD']).trim().split('\n').pop() ||
41
- 'HEAD'
42
-
43
- const subjects = git([
44
- 'log',
45
- '--no-merges',
46
- '--pretty=format:%s',
47
- `${ref}..HEAD`,
48
- ])
49
- .split('\n')
50
- .map((s) => s.trim())
51
- .filter(Boolean)
52
-
53
- const parsed = subjects
54
- .map(parseConventionalCommit)
55
- .filter((c): c is NonNullable<typeof c> => c !== null)
56
- const body = renderChangelogDraft(groupCommits(parsed))
57
-
58
- const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
59
- const changelogPath = path.join(root, 'CHANGELOG.md')
60
-
61
- // D2: sync the `## [Unreleased]` section in CHANGELOG.md from the conventional
62
- // commits since the last tag. `--write` edits the file; `--check` fails when
63
- // the committed section is out of date (for CI); neither just prints the body.
64
- if (write || check) {
65
- const onDisk = fs.readFileSync(changelogPath, 'utf-8')
66
- const nextText = syncUnreleased(onDisk, body)
67
- if (write) {
68
- if (nextText !== onDisk) {
69
- fs.writeFileSync(changelogPath, nextText)
70
- console.log('changelog: updated the Unreleased section')
71
- } else {
72
- console.log('changelog: Unreleased already up to date')
73
- }
74
- process.exit(0)
75
- }
76
- if (nextText !== onDisk) {
77
- console.error(
78
- 'changelog: the Unreleased section is out of date — run `npm run changelog:sync`',
79
- )
80
- process.exit(1)
81
- }
82
- console.log('changelog: Unreleased is up to date')
83
- process.exit(0)
84
- }
85
-
86
- if (!body) {
87
- console.error('no conventional commits since ' + ref)
88
- process.exit(0)
89
- }
90
-
91
- console.log('## [Unreleased]')
92
- console.log('')
93
- console.log(body)
@@ -1,17 +0,0 @@
1
- import { rmSync, existsSync } from 'node:fs'
2
- import path from 'node:path'
3
- import { fileURLToPath } from 'node:url'
4
-
5
- // tsc emits into dist/ without clearing it, so a source file DELETED from
6
- // src/ leaves an orphan .js in the published tree (this really happened:
7
- // dist/confirm.js survived after src/confirm.ts was removed). Wipe dist/ first
8
- // so the build output mirrors src/ exactly.
9
- //
10
- // Cross-platform: use node's fs.rm instead of a shell `rm -rf` (Windows has no
11
- // rm) and never touch anything outside this repo.
12
- const here = path.dirname(fileURLToPath(import.meta.url))
13
- const dist = path.resolve(here, '..', 'dist')
14
- if (existsSync(dist)) {
15
- rmSync(dist, { recursive: true, force: true })
16
- console.log('clean-dist: removed', dist)
17
- }
@@ -1,112 +0,0 @@
1
- import { spawnSync } from 'node:child_process'
2
-
3
- // Soft coverage FLOOR for the PURE logic modules — the ones a regression would
4
- // silently break without any UI symptom (command formatting, schedule parsing,
5
- // the raw SSE/model-answer parsing). The floors are deliberately a few points
6
- // BELOW the current values so ordinary churn does not fail the build; they
7
- // only catch a real drop (a module losing its tests).
8
- //
9
- // Node's test runner has no built-in coverage threshold, so we parse the
10
- // per-file line % from the coverage report it prints and compare here. This is
11
- // informational in the sense that the FLOORS are conservative, but a drop
12
- // below one is a hard failure.
13
- const FLOORS = {
14
- 'commands.ts': 85,
15
- 'scheduler.ts': 90,
16
- 'net-capture.ts': 90,
17
- 'xml-toolcall.ts': 90,
18
- 'diff.ts': 90,
19
- 'theme.ts': 95,
20
- // Added later: core logic or already well-tested, so a silent regression
21
- // that deletes their tests should be caught too. Floors sit a few points
22
- // below the current values.
23
- 'extraTools.ts': 85,
24
- 'fsutil.ts': 90,
25
- 'sessions.ts': 90,
26
- 'context.ts': 85,
27
- 'markdown.ts': 90,
28
- 'tools.ts': 90,
29
- 'system-prompt.ts': 90,
30
- 'undo.ts': 85,
31
- 'checkpoint.ts': 80,
32
- 'permissions.ts': 85,
33
- // changelog.ts is mostly comments + type/interface declarations (lines
34
- // 1-30) that carry no runtime statements, so its LINE % caps well below the
35
- // other modules even with every executable branch tested. The floor reflects
36
- // that ceiling (a real drop below it still fails).
37
- 'changelog.ts': 78,
38
- 'attach-refs.ts': 85,
39
- // C3: the pure /config subcommand dispatcher — silent routing regressions
40
- // (a wrong subcommand branch) are invisible without tests.
41
- 'config-command.ts': 85,
42
- // The Ink TUI's PURE halves: the editor state transitions (tui/edit.ts) and
43
- // the key dispatcher (tui/keys.ts). A silent regression there breaks the
44
- // operator's editing without any UI test catching it. NOTE: the gate keys on
45
- // the BASENAME (its regex has no slash), so these are 'edit.ts' / 'keys.ts' /
46
- // 'store.ts' — unique in the tree today.
47
- 'edit.ts': 85,
48
- 'keys.ts': 85,
49
- 'store.ts': 85,
50
- 'banner.ts': 85,
51
- 'tool-summary.ts': 85,
52
- // N12: web.ts holds the SSRF guard and the DuckDuckGo/HTML parsers — a
53
- // regression there is silent. Floors sit below the current values.
54
- 'web.ts': 40,
55
- 'spinner.ts': 55,
56
- }
57
-
58
- const run = spawnSync(
59
- 'npx',
60
- ['tsx', '--test', '--experimental-test-coverage', 'test/*.test.ts'],
61
- {
62
- encoding: 'utf-8',
63
- shell: process.platform === 'win32',
64
- maxBuffer: 1024 * 1024 * 64,
65
- },
66
- )
67
-
68
- const out = (run.stdout || '') + (run.stderr || '')
69
-
70
- if (run.status !== 0) {
71
- console.error('coverage-gate: test run failed (exit ' + run.status + ')')
72
- process.exit(1)
73
- }
74
-
75
- const linePct = new Map()
76
- for (const line of out.split('\n')) {
77
- // Per-file rows look like:
78
- // ℹ commands.ts | 94.14 | 78.70 | 93.88 | 40-56 ...
79
- const m = line.match(/^\s*\S?\s*([A-Za-z0-9._-]+\.ts)\s+\|\s+([\d.]+)\s+\|/)
80
- if (m) linePct.set(m[1], Number(m[2]))
81
- }
82
-
83
- const failures = []
84
- for (const [file, floor] of Object.entries(FLOORS)) {
85
- const pct = linePct.get(file)
86
- if (pct === undefined) {
87
- failures.push(file + ': no coverage row found')
88
- continue
89
- }
90
- if (pct < floor) {
91
- failures.push(file + ': ' + pct + '% < floor ' + floor + '%')
92
- } else {
93
- console.log('coverage-gate: ' + file + ' ' + pct + '% >= ' + floor + '%')
94
- }
95
- }
96
-
97
- // Print the lowest-covered modules, so a dead or untested module is visible
98
- // in the CI log even when it has no floor yet. Informational only.
99
- const sorted = [...linePct.entries()].sort((a, b) => a[1] - b[1]).slice(0, 8)
100
- if (sorted.length) {
101
- console.log('coverage-gate: lowest covered modules (informational):')
102
- for (const [name, pct] of sorted) {
103
- console.log(' ' + name + ' ' + pct + '%')
104
- }
105
- }
106
-
107
- if (failures.length) {
108
- console.error('coverage-gate: FAILED')
109
- for (const f of failures) console.error(' ' + f)
110
- process.exit(1)
111
- }
112
- console.log('coverage-gate: OK')
@@ -1,51 +0,0 @@
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))
@@ -1,33 +0,0 @@
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)
@@ -1,37 +0,0 @@
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))
@@ -1,58 +0,0 @@
1
- // Run ESLint so that typescript-eslint uses the TypeScript 6 API.
2
- //
3
- // The project is built with TypeScript 7, but typescript-eslint (8.x) does
4
- // not support the TS 7 API yet and refuses to start. Its own recommendation is
5
- // to run "side by side with TypeScript 6": load the TS 6 compiler just for the
6
- // linter. We do that by aliasing the `typescript` module to the `typescript-6`
7
- // package for the duration of the ESLint process, without touching the
8
- // compiler the project actually builds with.
9
- import { createRequire } from 'module'
10
- import Module from 'module'
11
- import path from 'path'
12
- import { fileURLToPath } from 'url'
13
-
14
- // Module._resolveFilename is a private Node field (not in @types/node), so it
15
- // is reached through a small structural type instead of `any`.
16
- type ModuleWithResolve = typeof Module & {
17
- _resolveFilename: (request: string, ...rest: unknown[]) => string
18
- }
19
-
20
- const require = createRequire(import.meta.url)
21
- const here = path.dirname(fileURLToPath(import.meta.url))
22
- const ts6 = path.join(here, '..', 'node_modules', 'typescript-6')
23
-
24
- try {
25
- require.resolve(path.join(ts6, 'lib', 'typescript.js'))
26
- } catch {
27
- console.error(
28
- 'eslint-ts6: the typescript-6 package is missing. Run: npm i -D typescript-6@npm:typescript@6',
29
- )
30
- process.exit(1)
31
- }
32
-
33
- // Patch Module._resolveFilename so any require('typescript') resolves to the
34
- // TS 6 package instead of the TS 7 the project depends on.
35
- const M = Module as ModuleWithResolve
36
- const origResolve = M._resolveFilename
37
- M._resolveFilename = (request: string, ...rest: unknown[]) => {
38
- if (request === 'typescript') {
39
- try {
40
- return require.resolve(path.join(ts6, 'lib', 'typescript.js'))
41
- } catch {
42
- return origResolve(request, ...rest)
43
- }
44
- }
45
- return origResolve(request, ...rest)
46
- }
47
-
48
- const { ESLint } = await import('eslint')
49
- const eslint = new ESLint({ fix: process.argv.includes('--fix') })
50
- const results = await eslint.lintFiles(['.'])
51
- if (process.argv.includes('--fix')) {
52
- await ESLint.outputFixes(results)
53
- }
54
- const formatter = await eslint.loadFormatter('stylish')
55
- const out = await formatter.format(results)
56
- if (out) process.stdout.write(out)
57
- const errors = results.reduce((n, r) => n + r.errorCount, 0)
58
- process.exit(errors > 0 ? 1 : 0)
@@ -1,31 +0,0 @@
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))
@@ -1,33 +0,0 @@
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)
@@ -1,34 +0,0 @@
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
- })
@@ -1,117 +0,0 @@
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()