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,229 +0,0 @@
1
- // Render docs/how-it-works.png — the "How it works" flowchart used in
2
- // README.md. Reproduces the ORIGINAL mermaid `flowchart LR` look (rounded
3
- // lavender nodes, gray arrows, white background, a simple left-to-right
4
- // chain) so the diagram stays visually the same as before. The ONLY change:
5
- // the model node now shows BOTH supported backends (chat.deepseek.com /
6
- // chat.qwen.ai) instead of only chat.deepseek.com.
7
- //
8
- // Layout matches mermaid LR: FORWARD edges (the request) run through the node
9
- // centerline left->right; BACK edges (the answers) are routed BELOW the boxes
10
- // with a short vertical stub on each side, so forward and back arrows are
11
- // clearly separated instead of overlapping.
12
- //
13
- // Run: npm run render-how (tsx scripts/render-how-it-works.mts)
14
- //
15
- // Isolation: chromium.launch() uses a THROWAWAY context, never
16
- // ~/.zames/profile (see AGENTS.md — never touch the live agent's browser).
17
-
18
- import { chromium } from 'playwright'
19
- import path from 'node:path'
20
- import { fileURLToPath } from 'node:url'
21
-
22
- const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
23
- const out = path.join(root, 'docs', 'how-it-works.png')
24
-
25
- // Mermaid default-theme palette (matches the previous diagram).
26
- const FILL = '#ECECFF'
27
- const STROKE = '#9370DB'
28
- const TEXT = '#333333'
29
- const FONT = 'trebuchet ms, verdana, arial, sans-serif'
30
-
31
- const W = 1120
32
- const NODE_W = 250
33
- const NODE_H = 70
34
- const PAD = 30
35
- const TOP = 34
36
- const CY = TOP + NODE_H / 2
37
- // The back edges run below the boxes; leave room for them + their labels.
38
- const BACK_Y = TOP + NODE_H + 46
39
- const H = BACK_Y + 30
40
- const GAP = 150
41
-
42
- interface Node {
43
- x: number
44
- w: number
45
- lines: string[]
46
- }
47
-
48
- const nodes: Node[] = [
49
- { x: PAD, w: NODE_W, lines: ['terminal (you)'] },
50
- {
51
- x: PAD + NODE_W + GAP,
52
- w: 260,
53
- lines: ['zames', 'parse tool-call · run tool'],
54
- },
55
- {
56
- x: PAD + NODE_W + GAP + 260 + GAP,
57
- w: 260,
58
- lines: ['chat.deepseek.com', 'chat.qwen.ai'],
59
- },
60
- ]
61
-
62
- const esc = (s: string): string =>
63
- s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
64
-
65
- const nodeSvg = (n: Node): string => {
66
- const y = TOP
67
- const rx = 12
68
- const lh = 20
69
- const startY = CY - ((n.lines.length - 1) * lh) / 2 + 5
70
- const cx = n.x + n.w / 2
71
- const texts = n.lines
72
- .map(
73
- (t, i) =>
74
- '<text x="' +
75
- cx +
76
- '" y="' +
77
- (startY + i * lh) +
78
- '" text-anchor="middle" font-family="' +
79
- FONT +
80
- '" font-size="15" fill="' +
81
- TEXT +
82
- '">' +
83
- esc(t) +
84
- '</text>',
85
- )
86
- .join('')
87
- return (
88
- '<rect x="' +
89
- n.x +
90
- '" y="' +
91
- y +
92
- '" width="' +
93
- n.w +
94
- '" height="' +
95
- NODE_H +
96
- '" rx="' +
97
- rx +
98
- '" ry="' +
99
- rx +
100
- '" fill="' +
101
- FILL +
102
- '" stroke="' +
103
- STROKE +
104
- '" stroke-width="2"/>' +
105
- texts
106
- )
107
- }
108
-
109
- const label = (x: number, y: number, text: string): string =>
110
- '<text x="' +
111
- x +
112
- '" y="' +
113
- y +
114
- '" text-anchor="middle" font-family="' +
115
- FONT +
116
- '" font-size="12" fill="' +
117
- TEXT +
118
- '">' +
119
- esc(text) +
120
- '</text>'
121
-
122
- // A FORWARD edge: a straight horizontal arrow at the node centerline, going
123
- // left->right, with the label just above the line.
124
- const forward = (x1: number, x2: number, text: string): string => {
125
- const mid = (x1 + x2) / 2
126
- return (
127
- '<path d="M' +
128
- x1 +
129
- ' ' +
130
- CY +
131
- ' L' +
132
- x2 +
133
- ' ' +
134
- CY +
135
- '" fill="none" stroke="' +
136
- TEXT +
137
- '" stroke-width="1.6" marker-end="url(#ar)"/>' +
138
- label(mid, CY - 9, text)
139
- )
140
- }
141
-
142
- // A BACK edge: from the RIGHT box's bottom edge, DOWN to BACK_Y, LEFT, then UP
143
- // into the LEFT box's bottom edge, with the arrowhead pointing up-left into it.
144
- const back = (leftX: number, rightX: number, text: string): string => {
145
- // leftX = right edge of the LEFT node; rightX = left edge of the RIGHT node.
146
- const ly = TOP + NODE_H
147
- const d =
148
- 'M' +
149
- rightX +
150
- ' ' +
151
- ly +
152
- ' L' +
153
- rightX +
154
- ' ' +
155
- BACK_Y +
156
- ' L' +
157
- leftX +
158
- ' ' +
159
- BACK_Y +
160
- ' L' +
161
- leftX +
162
- ' ' +
163
- ly
164
- const mid = (leftX + rightX) / 2
165
- return (
166
- '<path d="' +
167
- d +
168
- '" fill="none" stroke="' +
169
- TEXT +
170
- '" stroke-width="1.6" marker-end="url(#ar)"/>' +
171
- label(mid, BACK_Y - 6, text)
172
- )
173
- }
174
-
175
- const a = nodes[0]
176
- const z = nodes[1]
177
- const m = nodes[2]
178
-
179
- const svg =
180
- '<svg xmlns="http://www.w3.org/2000/svg" width="' +
181
- W +
182
- '" height="' +
183
- H +
184
- '" viewBox="0 0 ' +
185
- W +
186
- ' ' +
187
- H +
188
- '">' +
189
- '<defs>' +
190
- // ONE right-pointing arrowhead, tip at local (10,5) so refX=10/refY=5 lands
191
- // the tip exactly on the path end. `orient="auto"` rotates the marker to
192
- // follow the path tangent, so the SAME marker works for a horizontal forward
193
- // edge and for the vertical/up back edge. (Authoring the up arrow as an
194
- // up-pointing triangle was the bug: orient=auto rotated it AGAIN, giving the
195
- // crooked tip.)
196
- '<marker id="ar" markerWidth="12" markerHeight="12" refX="10" refY="5" orient="auto" markerUnits="userSpaceOnUse"><path d="M0,0 L10,5 L0,10 z" fill="' +
197
- TEXT +
198
- '"/></marker>' +
199
- '</defs>' +
200
- '<rect width="100%" height="100%" fill="#ffffff"/>' +
201
- // back edges first (drawn under the forward edges / nodes).
202
- // back(leftEdgeRightX, rightEdgeLeftX, label): the arrow starts at the RIGHT
203
- // box's bottom, runs down-left, and points up into the LEFT box's bottom.
204
- back(z.x + z.w, m.x, 'model answers (raw SSE)') +
205
- back(a.x + a.w, z.x, 'answer') +
206
- // forward edges
207
- forward(a.x + a.w, z.x, 'task') +
208
- forward(z.x + z.w, m.x, 'system prompt + task') +
209
- nodes.map(nodeSvg).join('') +
210
- '</svg>'
211
-
212
- const html =
213
- '<!doctype html><html><head><meta charset="utf-8"><style>html,body{margin:0;padding:0;background:#fff}svg{display:block}</style></head><body>' +
214
- svg +
215
- '</body></html>'
216
-
217
- const browser = await chromium.launch({ headless: true })
218
- try {
219
- const page = await browser.newPage({
220
- viewport: { width: W, height: H },
221
- deviceScaleFactor: 2,
222
- })
223
- await page.setContent(html, { waitUntil: 'load' })
224
- const el = await page.$('svg')
225
- await (el || page).screenshot({ path: out })
226
- console.log('wrote', path.relative(root, out))
227
- } finally {
228
- await browser.close()
229
- }
@@ -1,20 +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(20, 80)
6
- const child = spawn(
7
- 'script',
8
- ['-qec', 'npx tsx scripts/tui-repro.tsx', '/dev/null'],
9
- {
10
- cwd: path.resolve('.'),
11
- env: { ...process.env, COLUMNS: '80', LINES: '20', PROBE_LOCALE: 'ru' },
12
- },
13
- )
14
- child.stdout.on('data', (d) => term.write(d.toString('utf-8')))
15
- child.stderr.on('data', (d) => term.write(d.toString('utf-8')))
16
- setTimeout(() => {
17
- process.stdout.write(term.render() + NL)
18
- child.kill('SIGTERM')
19
- }, 5000)
20
- child.on('close', () => process.exit(0))
@@ -1,309 +0,0 @@
1
- // self-smoke: launch a REAL isolated zames instance and exercise the core
2
- // functionality end-to-end (login, tools, attachments), WITHOUT touching the
3
- // operator's ~/.zames profile. Used by the agent to check its own build.
4
- //
5
- // Isolation: the script points HOME at a throwaway dir BEFORE importing any
6
- // project module (they compute ~/.zames paths at load time), copies the real
7
- // config.json (so auto-login works) and symlinks the Playwright browser cache.
8
- // Nothing in the operator's profile is written.
9
- //
10
- // Usage: npm run self-smoke (headless, empty profile)
11
- // npm run self-smoke -- --headed
12
- // npm run self-smoke -- --reuse-profile (logged-in profile, see below)
13
- // Exit code 0 = all scenarios passed; 2 = login failed (could not start).
14
- //
15
- // --reuse-profile: copy the REAL profile (~/.zames/profile) read-only into the
16
- // throwaway HOME so the browser starts ALREADY SIGNED IN. Reason: DeepSeek's
17
- // anti-bot increasingly blocks a COLD login on a fresh profile (captcha / rate
18
- // limit), so a smoke that logs in from scratch fails for reasons unrelated to
19
- // the agent. Copying (never opening the live profile) keeps isolation intact.
20
-
21
- import fs from 'fs'
22
- import path from 'path'
23
- import os from 'os'
24
-
25
- // --- flags -----------------------------------------------------------------
26
- const HEADED = process.argv.includes('--headed')
27
- const REUSE_PROFILE =
28
- process.argv.includes('--reuse-profile') ||
29
- process.env.ZAMES_SMOKE_REUSE_PROFILE === '1'
30
- // Optional explicit source profile (default: the real ~/.zames/profile).
31
- const profileArgIdx = process.argv.indexOf('--profile')
32
- const REAL_HOME = os.homedir()
33
- const PROFILE_SRC =
34
- profileArgIdx >= 0 && process.argv[profileArgIdx + 1]
35
- ? path.resolve(process.argv[profileArgIdx + 1])
36
- : path.join(REAL_HOME, '.zames', 'profile')
37
-
38
- // --- isolate HOME before importing project modules -------------------------
39
- if (!process.env.ZAMES_SMOKE_HOME) {
40
- const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'zames-smoke-'))
41
- process.env.HOME = tmp
42
- process.env.ZAMES_SMOKE_HOME = tmp
43
- fs.mkdirSync(path.join(tmp, '.zames'), { recursive: true })
44
- fs.mkdirSync(path.join(tmp, '.cache'), { recursive: true })
45
- // Credentials for auto-login (copied, never written back to the real home).
46
- const realCfg = path.join(REAL_HOME, '.zames', 'config.json')
47
- if (fs.existsSync(realCfg)) {
48
- fs.copyFileSync(realCfg, path.join(tmp, '.zames', 'config.json'))
49
- }
50
- // Playwright browsers live in the real cache; link it read-only.
51
- const realPw = path.join(REAL_HOME, '.cache', 'ms-playwright')
52
- if (fs.existsSync(realPw)) {
53
- fs.symlinkSync(realPw, path.join(tmp, '.cache', 'ms-playwright'))
54
- }
55
- // Reuse a logged-in profile: COPY it (the live one is never opened) and drop
56
- // the Singleton* files — Chrome refuses to start on a profile whose lock is
57
- // present, and the live browser may hold it. The cookies survive the copy
58
- // because Playwright launches with --password-store=basic.
59
- if (REUSE_PROFILE && fs.existsSync(PROFILE_SRC)) {
60
- fs.cpSync(PROFILE_SRC, path.join(tmp, '.zames', 'profile'), {
61
- recursive: true,
62
- filter: (src) => !/^Singleton/.test(path.basename(src)),
63
- })
64
- }
65
- }
66
- const SMOKE_HOME = process.env.ZAMES_SMOKE_HOME
67
- const WORK = path.join(SMOKE_HOME, 'work')
68
- fs.mkdirSync(WORK, { recursive: true })
69
-
70
- // --- project modules (dynamic: they read HOME at import) --------------------
71
- const { DeepSeekBrowser } = await import('../src/browser.ts')
72
- const { createTools, filterToolsForReadOnly } = await import('../src/tools.ts')
73
- const { runAgentLoop } = await import('../src/agent-loop.ts')
74
- const { createSubagentRunner } = await import('../src/subagent.ts')
75
- const { loadConfig } = await import('../src/config.ts')
76
- const { Transcript } = await import('../src/transcript.ts')
77
-
78
- interface SmokeResult {
79
- name: string
80
- ok: boolean
81
- note?: string
82
- }
83
- const results: SmokeResult[] = []
84
- // Set when we could not even start (login failed): exit code 2, distinct from a
85
- // scenario failure (1).
86
- let loginFailed = false
87
- const record = (name: string, ok: boolean, note?: string): void => {
88
- results.push({ name, ok, note })
89
- console.log((ok ? 'PASS ' : 'FAIL ') + name + (note ? ' — ' + note : ''))
90
- }
91
-
92
- const cfg = loadConfig()
93
- const transcript = new Transcript({
94
- dir: path.join(SMOKE_HOME, '.zames', 'logs'),
95
- enabled: true,
96
- sessionName: 'self-smoke',
97
- })
98
-
99
- const browser = new DeepSeekBrowser({
100
- headless: !HEADED,
101
- debug: false,
102
- channel: cfg.browserChannel,
103
- auth: cfg.browser.auth,
104
- locale: 'ru',
105
- minSendIntervalMs: cfg.browser.minSendIntervalMs,
106
- })
107
-
108
- async function main() {
109
- console.log(
110
- 'self-smoke: HOME=' +
111
- SMOKE_HOME +
112
- ' headed=' +
113
- HEADED +
114
- ' reuse-profile=' +
115
- REUSE_PROFILE,
116
- )
117
- await browser.launch()
118
- // waitForLogin() can block on a manual Enter in a pipe. Bound it so a cold
119
- // login (anti-bot / captcha) fails fast with a clear hint instead of hanging
120
- // until the caller's tool timeout. On failure exit code 2 signals "could not
121
- // even start" — distinct from a scenario failure.
122
- const loggedIn = await Promise.race([
123
- browser.waitForLogin().then(() => true),
124
- new Promise<boolean>((r) => setTimeout(() => r(false), 120_000)),
125
- ])
126
- if (!loggedIn) {
127
- console.error(
128
- '\nself-smoke: LOGIN FAILED — a cold login on a fresh profile is often\n' +
129
- 'blocked by DeepSeek anti-bot/captcha. Re-run against a signed-in profile:\n' +
130
- ' npm run self-smoke -- --reuse-profile\n' +
131
- '(or sign in once with: npm run self-smoke -- --headed).',
132
- )
133
- loginFailed = true
134
- return
135
- }
136
- record('login', true, 'DeepSeek session is active')
137
-
138
- const tools = createTools(WORK, {})
139
-
140
- // Scenario A — tools (Write, Read, Bash) + respond.
141
- const taskA =
142
- 'Do exactly this, then report: (1) Write a file smoke.txt with the single line HELLO_SMOKE. ' +
143
- '(2) Read it back. (3) Run the Bash command `echo bash_ok`. ' +
144
- 'Then call respond with one short line that contains the words HELLO_SMOKE and bash_ok.'
145
- const outA = await runAgentLoop({
146
- browser,
147
- tools,
148
- task: taskA,
149
- workdir: WORK,
150
- maxIterations: 25,
151
- freshChat: true,
152
- sendSystemPrompt: true,
153
- transcript,
154
- onToolCall: (n) => console.log(' tool: ' + n),
155
- onToolResult: () => {},
156
- onAssistantMessage: (m) => console.log(' answer: ' + m),
157
- onWarning: (m) => console.log(' warn: ' + m),
158
- locale: 'ru',
159
- askDeadlineMs: cfg.browser.askDeadlineMs,
160
- maxAfterToolRetries: cfg.browser.maxAfterToolRetries,
161
- })
162
- const fileMade = fs.existsSync(path.join(WORK, 'smoke.txt'))
163
- record('tools: file created', fileMade, 'smoke.txt')
164
- record(
165
- 'tools: agent reported',
166
- /HELLO_SMOKE/.test(outA) && /bash_ok/.test(outA),
167
- JSON.stringify(outA.slice(0, 120)),
168
- )
169
-
170
- // Scenario B — attachment: the model must read a file attached to the message.
171
- const note = path.join(WORK, 'note.txt')
172
- fs.writeFileSync(note, 'ATTACH_MARKER_42')
173
- const outB = await runAgentLoop({
174
- browser,
175
- tools,
176
- task: 'Reply with the exact content of the attached file, nothing else.',
177
- workdir: WORK,
178
- maxIterations: 12,
179
- freshChat: true,
180
- sendSystemPrompt: true,
181
- transcript,
182
- attachments: [{ path: note, name: 'note.txt', mime: 'text/plain' }],
183
- onAssistantMessage: (m) => console.log(' answer: ' + m),
184
- onWarning: (m) => console.log(' warn: ' + m),
185
- locale: 'ru',
186
- askDeadlineMs: cfg.browser.askDeadlineMs,
187
- maxAfterToolRetries: cfg.browser.maxAfterToolRetries,
188
- })
189
- record(
190
- 'attachment: model read the file',
191
- /ATTACH_MARKER_42/.test(outB),
192
- JSON.stringify(outB.slice(0, 120)),
193
- )
194
-
195
- // Scenario C — follow-up in the SAME chat (context carried over).
196
- const outC = await runAgentLoop({
197
- browser,
198
- tools,
199
- task: 'What single word did I ask you to remember in the previous message? Answer with just that word.',
200
- workdir: WORK,
201
- maxIterations: 8,
202
- freshChat: false,
203
- sendSystemPrompt: false,
204
- transcript,
205
- onAssistantMessage: (m) => console.log(' answer: ' + m),
206
- onWarning: (m) => console.log(' warn: ' + m),
207
- locale: 'ru',
208
- askDeadlineMs: cfg.browser.askDeadlineMs,
209
- maxAfterToolRetries: cfg.browser.maxAfterToolRetries,
210
- })
211
- record(
212
- 'follow-up answered',
213
- outC.trim().length > 0,
214
- JSON.stringify(outC.slice(0, 80)),
215
- )
216
- const onSubagent = createSubagentRunner({
217
- browser,
218
- workdir: WORK,
219
- locale: 'ru',
220
- runAgentLoop,
221
- buildTools: (readOnly) => {
222
- const base = createTools(WORK, {})
223
- return readOnly ? filterToolsForReadOnly(base) : base
224
- },
225
- transcript,
226
- maxSubagents: 2,
227
- askDeadlineMs: cfg.browser.askDeadlineMs,
228
- maxAfterToolRetries: cfg.browser.maxAfterToolRetries,
229
- })
230
-
231
- // Scenario D — subagents (N36): the model delegates via Task; the runner
232
- // opens a separate chat, returns only the report, and restores the parent
233
- // chat. Requires browser.subagents (off by default).
234
- const subTools = createTools(WORK, { subagents: true })
235
- const outD = await runAgentLoop({
236
- browser,
237
- tools: subTools,
238
- task:
239
- 'Use the Task tool with subagent_type "explore" to find which word is written in the file smoke.txt. ' +
240
- 'prompt the subagent in a self-contained way. Then, based on its report, call respond with that word.',
241
- workdir: WORK,
242
- maxIterations: 20,
243
- freshChat: true,
244
- sendSystemPrompt: true,
245
- transcript,
246
- onSubagent,
247
- onToolCall: (n) => console.log(' tool: ' + n),
248
- onAssistantMessage: (m) => console.log(' answer: ' + m),
249
- onWarning: (m) => console.log(' warn: ' + m),
250
- locale: 'ru',
251
- askDeadlineMs: cfg.browser.askDeadlineMs,
252
- maxAfterToolRetries: cfg.browser.maxAfterToolRetries,
253
- })
254
- record(
255
- 'subagent: parent used the report',
256
- /HELLO_SMOKE/i.test(outD),
257
- JSON.stringify(outD.slice(0, 120)),
258
- )
259
-
260
- // Scenario E — background Bash + BashOutput (N33): start a process that
261
- // keeps running, poll its output, then kill it.
262
- const outE = await runAgentLoop({
263
- browser,
264
- tools,
265
- task:
266
- 'Do exactly this, then report: (1) Start the Bash command ' +
267
- '`echo BG_MARKER_7 && sleep 120` in the BACKGROUND (run_in_background=true). ' +
268
- '(2) Poll it once with BashOutput. (3) Kill it with BashOutput kill=true. ' +
269
- 'Then call respond with one short line containing BG_MARKER_7.',
270
- workdir: WORK,
271
- maxIterations: 20,
272
- freshChat: true,
273
- sendSystemPrompt: true,
274
- transcript,
275
- onToolCall: (n) => console.log(' tool: ' + n),
276
- onAssistantMessage: (m) => console.log(' answer: ' + m),
277
- onWarning: (m) => console.log(' warn: ' + m),
278
- locale: 'ru',
279
- askDeadlineMs: cfg.browser.askDeadlineMs,
280
- maxAfterToolRetries: cfg.browser.maxAfterToolRetries,
281
- })
282
- record(
283
- 'background bash: started, polled and killed',
284
- /BG_MARKER_7/.test(outE),
285
- JSON.stringify(outE.slice(0, 120)),
286
- )
287
- }
288
-
289
- let failed = false
290
- try {
291
- await main()
292
- } catch (e) {
293
- record('run', false, (e as Error).message)
294
- failed = true
295
- } finally {
296
- await browser.close().catch(() => {})
297
- transcript.close()
298
- }
299
-
300
- const passed = results.filter((r) => r.ok).length
301
- console.log('\nself-smoke: ' + passed + '/' + results.length + ' passed')
302
- const anyFail = failed || results.some((r) => !r.ok)
303
- if (!process.env.ZAMES_SMOKE_KEEP) {
304
- // Clean the throwaway HOME unless the caller asked to keep it for debugging.
305
- try {
306
- fs.rmSync(SMOKE_HOME, { recursive: true, force: true })
307
- } catch {}
308
- }
309
- process.exit(loginFailed ? 2 : anyFail ? 1 : 0)
@@ -1,63 +0,0 @@
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))
@@ -1,62 +0,0 @@
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)