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,221 +0,0 @@
1
- // Qwen (chat.qwen.ai) self-smoke: a REAL isolated QwenBrowser instance against
2
- // the live site, to catch integration regressions (login, send, answer) that
3
- // unit tests cannot.
4
- //
5
- // Isolation: HOME is pointed at a throwaway dir BEFORE importing any project
6
- // module (they compute ~/.zames paths at load). The real Qwen profile
7
- // (~/.zames/qwen-profile) is COPIED read-only into the temp HOME so the
8
- // browser starts already signed in; the live profile is NEVER opened. The
9
- // Playwright browser cache is symlinked read-only. Nothing in the operator's
10
- // ~/.zames is written.
11
- //
12
- // Usage: npx tsx scripts/qwen-smoke.mts [--headed] [--no-reuse-profile]
13
- // Exit code 0 = all scenarios passed; 2 = login failed (could not start).
14
-
15
- import fs from 'fs'
16
- import path from 'path'
17
- import os from 'os'
18
-
19
- const HEADED = process.argv.includes('--headed')
20
- const REUSE_PROFILE = !process.argv.includes('--no-reuse-profile')
21
- const REAL_HOME = os.homedir()
22
- const PROFILE_SRC = path.join(REAL_HOME, '.zames', 'qwen-profile')
23
-
24
- if (!process.env.ZAMES_QWEN_SMOKE_HOME) {
25
- const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'zames-qwen-smoke-'))
26
- process.env.HOME = tmp
27
- process.env.ZAMES_QWEN_SMOKE_HOME = tmp
28
- fs.mkdirSync(path.join(tmp, '.zames'), { recursive: true })
29
- fs.mkdirSync(path.join(tmp, '.cache'), { recursive: true })
30
- const realCfg = path.join(REAL_HOME, '.zames', 'config.json')
31
- if (fs.existsSync(realCfg)) {
32
- fs.copyFileSync(realCfg, path.join(tmp, '.zames', 'config.json'))
33
- }
34
- const realPw = path.join(REAL_HOME, '.cache', 'ms-playwright')
35
- if (fs.existsSync(realPw)) {
36
- fs.symlinkSync(realPw, path.join(tmp, '.cache', 'ms-playwright'))
37
- }
38
- if (REUSE_PROFILE && fs.existsSync(PROFILE_SRC)) {
39
- fs.cpSync(PROFILE_SRC, path.join(tmp, '.zames', 'qwen-profile'), {
40
- recursive: true,
41
- filter: (src) => !/^Singleton/.test(path.basename(src)),
42
- })
43
- }
44
- }
45
- const SMOKE_HOME = process.env.ZAMES_QWEN_SMOKE_HOME as string
46
-
47
- // --- project modules (dynamic: they read HOME at import) --------------------
48
- const { QwenBrowser } = await import('../src/qwen-browser.ts')
49
- const { loadConfig } = await import('../src/config.ts')
50
-
51
- const cfg = loadConfig()
52
-
53
- interface SmokeResult {
54
- name: string
55
- ok: boolean
56
- note?: string
57
- }
58
- const results: SmokeResult[] = []
59
- let loginFailed = false
60
- const record = (name: string, ok: boolean, note?: string): void => {
61
- results.push({ name, ok, note })
62
- console.log((ok ? 'PASS ' : 'FAIL ') + name + (note ? ' — ' + note : ''))
63
- }
64
-
65
- const browser = new QwenBrowser({
66
- headless: !HEADED,
67
- debug: process.env.ZAMES_QWEN_SMOKE_DEBUG === '1',
68
- channel: cfg.browserChannel,
69
- auth: cfg.browser.auth,
70
- locale: 'ru',
71
- minSendIntervalMs: 0,
72
- answerTimeoutMs: 180000,
73
- webSearch: false,
74
- deepThinking: false,
75
- })
76
- browser.onNotice = (t: string) => console.log(' [notice] ' + t)
77
-
78
- async function main() {
79
- console.log(
80
- 'qwen-smoke: HOME=' +
81
- SMOKE_HOME +
82
- ' headed=' +
83
- HEADED +
84
- ' reuse-profile=' +
85
- REUSE_PROFILE,
86
- )
87
- await browser.launch()
88
-
89
- // Bound the login wait so a cold/expired session fails fast with a clear
90
- // hint instead of hanging until the caller's tool timeout.
91
- const loginPromise = browser.waitForLogin()
92
- const timedOut = await Promise.race([
93
- loginPromise.then(() => false),
94
- new Promise<boolean>((r) => setTimeout(() => r(true), 60000)),
95
- ])
96
- if (timedOut) {
97
- loginFailed = true
98
- record('login', false, 'timed out after 60s')
99
- } else {
100
- const logged = await browser.isLoggedIn().catch(() => false)
101
- record('login', logged, logged ? '' : 'not signed in (guest)')
102
- if (!logged) loginFailed = true
103
- }
104
-
105
- if (!loginFailed) {
106
- // 1) new chat + a SIMPLE ask that needs no tools: the model must answer.
107
- try {
108
- await browser.newChat()
109
- const id = await browser.getCurrentChatId()
110
- record('newChat', true, 'id=' + (id || '(none)'))
111
- } catch (e) {
112
- record('newChat', false, (e as Error).message)
113
- }
114
-
115
- try {
116
- const ans = await browser.ask('Reply with exactly: PONG', {
117
- agent: true,
118
- })
119
- const ok = /PONG/i.test(ans)
120
- record('ask:simple', ok, ok ? '' : 'got: ' + ans.slice(0, 80))
121
- } catch (e) {
122
- record('ask:simple', false, (e as Error).message)
123
- }
124
-
125
- // 2) a second send right after the first: guards the send-verify path (the
126
- // "text entered but send button not pressed" regression).
127
- try {
128
- const ans = await browser.ask('Reply with exactly: PONG2', {
129
- agent: true,
130
- })
131
- const ok = /PONG2/i.test(ans)
132
- record('ask:second-send', ok, ok ? '' : 'got: ' + ans.slice(0, 80))
133
- } catch (e) {
134
- record('ask:second-send', false, (e as Error).message)
135
- }
136
-
137
- // 3) model catalogue + token usage (best-effort, non-fatal).
138
- try {
139
- const models = await browser.listModels()
140
- record('listModels', models.length > 0, 'n=' + models.length)
141
- } catch (e) {
142
- record('listModels', false, (e as Error).message)
143
- }
144
-
145
- const usage = browser.getLastTokenUsage()
146
- record('tokenUsage', usage !== null, 'usage=' + String(usage))
147
-
148
- // 4) tool-call emission: the agent-loop depends on the model returning a
149
- // parseable JSON tool call. Ask for one explicitly and check the PARSER
150
- // (not the model's prose).
151
- try {
152
- const { parseToolCall } = await import('../src/answer-parse.ts')
153
- const ans = await browser.ask(
154
- 'Call the Read tool for the file package.json. Reply with EXACTLY one ' +
155
- 'JSON tool-call object and nothing else: ' +
156
- '{"tool":"Read","args":{"path":"..."}}',
157
- { agent: true },
158
- )
159
- const parsed = parseToolCall(ans)
160
- const call = Array.isArray(parsed) ? parsed[0] : parsed
161
- const ok = !!call && call.tool === 'Read'
162
- record('toolCall', ok, ok ? '' : 'got: ' + ans.slice(0, 100))
163
- } catch (e) {
164
- record('toolCall', false, (e as Error).message)
165
- }
166
-
167
- // History restore: after the sends above the chat must have messages.
168
- try {
169
- const id = await browser.getCurrentChatId()
170
- const msgs = id ? await browser.fetchChatMessages(id) : []
171
- record(
172
- 'history',
173
- msgs.length > 0,
174
- 'id=' + (id || '-') + ' msgs=' + msgs.length,
175
- )
176
- } catch (e) {
177
- record('history', false, (e as Error).message)
178
- }
179
-
180
- // Attachments: a SIGNED-IN session uploads a file; a GUEST cannot, so a
181
- // guest failure is reported as a SKIP (not a failure) — the guest limit is
182
- // a site restriction, not a regression.
183
- try {
184
- const tmpFile = path.join(SMOKE_HOME, 'zames-smoke-note.txt')
185
- fs.writeFileSync(tmpFile, 'The secret word is BANANA')
186
- const failed = await browser._attachFiles([
187
- { path: tmpFile, name: 'zames-smoke-note.txt', mime: 'text/plain' },
188
- ])
189
- const attached = failed.length === 0
190
- const isGuest = !(await browser.isLoggedIn().catch(() => false))
191
- if (attached) record('attachments', true, '')
192
- else if (isGuest)
193
- record('attachments', true, 'skipped (guest cannot upload)')
194
- else record('attachments', false, JSON.stringify(failed))
195
- } catch (e) {
196
- record('attachments', false, (e as Error).message)
197
- }
198
- }
199
-
200
- await browser.close().catch(() => {})
201
-
202
- const failed = results.filter((r) => !r.ok)
203
- console.log(
204
- '\nqwen-smoke: ' +
205
- (results.length - failed.length) +
206
- '/' +
207
- results.length +
208
- ' passed',
209
- )
210
- if (!process.env.ZAMES_QWEN_SMOKE_KEEP) {
211
- fs.rmSync(SMOKE_HOME, { recursive: true, force: true })
212
- } else {
213
- console.log('kept HOME: ' + SMOKE_HOME)
214
- }
215
- process.exit(loginFailed ? 2 : failed.length ? 1 : 0)
216
- }
217
-
218
- main().catch((e) => {
219
- console.error('qwen-smoke crashed:', (e as Error).message)
220
- process.exit(1)
221
- })
@@ -1,199 +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
- } from '../src/changelog.js'
10
-
11
- // One-command release prep (BACKLOG G4). The manual sequence — bump
12
- // package.json, write a dated CHANGELOG section, commit `chore: release X.Y.Z`,
13
- // tag — is easy to get wrong, so this script does the deterministic parts and
14
- // STOPS before the push. Pushing the branch and the tag is deliberately a
15
- // separate, explicit step: the tag triggers the npm publish workflow, and that
16
- // must never happen by accident.
17
- //
18
- // Usage:
19
- // npm run release patch # bug fixes
20
- // npm run release minor # new features
21
- // npm run release major # breaking changes
22
- // npm run release patch --no-tests # skip the local test run (CI still runs)
23
- // npm run release patch --dry-run # print the plan, change nothing
24
- //
25
- // Run via tsx so it can reuse src/changelog.ts (the conventional-commit
26
- // parser) — a plain .mjs cannot import the TypeScript sources. The other
27
- // helper scripts are .mts for the same reason; postinstall stays .mjs because
28
- // it runs before tsx is guaranteed to be installed.
29
-
30
- const NL = String.fromCharCode(10)
31
- const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
32
-
33
- function git(args: string[], allowFail = false): string {
34
- const r = spawnSync('git', args, { cwd: root, encoding: 'utf-8' })
35
- if (r.status !== 0 && !allowFail) {
36
- console.error('git ' + args.join(' ') + ' failed:' + NL + (r.stderr || ''))
37
- process.exit(1)
38
- }
39
- return (r.stdout || '').toString()
40
- }
41
-
42
- function fail(msg: string): never {
43
- console.error('release: ' + msg)
44
- process.exit(1)
45
- }
46
-
47
- function run(cmd: string, args: string[]): void {
48
- const r = spawnSync(cmd, args, {
49
- cwd: root,
50
- stdio: 'inherit',
51
- shell: process.platform === 'win32',
52
- })
53
- if (r.status !== 0) fail(cmd + ' ' + args.join(' ') + ' exited ' + r.status)
54
- }
55
-
56
- const argv = process.argv.slice(2)
57
- const flags = new Set(argv.filter((a) => a.startsWith('--')))
58
- const bump = argv.find((a) => !a.startsWith('--'))
59
- const dryRun = flags.has('--dry-run')
60
- const skipTests = flags.has('--no-tests')
61
-
62
- if (bump !== 'patch' && bump !== 'minor' && bump !== 'major') {
63
- fail('usage: npm run release patch|minor|major [--no-tests] [--dry-run]')
64
- }
65
-
66
- // 1. Clean tree: a release commit must contain ONLY the version bump and the
67
- // changelog section, never half-finished work.
68
- const dirty = git(['status', '--porcelain']).trim()
69
- if (dirty) {
70
- fail('working tree is not clean:' + NL + dirty)
71
- }
72
-
73
- // 2. Gates. The git hooks run these on commit/push too, but a release should
74
- // fail EARLY, before package.json/CHANGELOG are touched.
75
- if (!skipTests) {
76
- run('npm', ['run', 'typecheck'])
77
- run('npm', ['test'])
78
- }
79
-
80
- // 3. Compute the next version from package.json.
81
- const pkgPath = path.join(root, 'package.json')
82
- const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8')) as {
83
- version: string
84
- }
85
- const m = /^(\d+)\.(\d+)\.(\d+)$/.exec(pkg.version)
86
- if (!m) fail('package.json version is not a plain x.y.z: ' + pkg.version)
87
- let [major, minor, patch] = [Number(m[1]), Number(m[2]), Number(m[3])]
88
- if (bump === 'major') {
89
- major += 1
90
- minor = 0
91
- patch = 0
92
- } else if (bump === 'minor') {
93
- minor += 1
94
- patch = 0
95
- } else {
96
- patch += 1
97
- }
98
- const next = major + '.' + minor + '.' + patch
99
- const tag = 'v' + next
100
-
101
- if (git(['tag', '--list', tag]).trim()) {
102
- fail('tag ' + tag + ' already exists')
103
- }
104
-
105
- // 4. Build the dated CHANGELOG section from conventional commits since the
106
- // last tag. English commit subjects become the section body; the operator can
107
- // still reword the section afterwards.
108
- const latestTag = git(['describe', '--tags', '--abbrev=0'], true).trim()
109
- const ref =
110
- latestTag ||
111
- git(['rev-list', '--max-parents=0', 'HEAD']).trim().split(NL).pop() ||
112
- 'HEAD'
113
- const subjects = git([
114
- 'log',
115
- '--no-merges',
116
- '--pretty=format:%s',
117
- ref + '..HEAD',
118
- ])
119
- .split(NL)
120
- .map((s) => s.trim())
121
- .filter(Boolean)
122
- const parsed = subjects
123
- .map(parseConventionalCommit)
124
- .filter((c): c is NonNullable<typeof c> => c !== null)
125
- let body = renderChangelogDraft(groupCommits(parsed))
126
-
127
- // The conventional draft skips chore/docs/test/etc, so a release whose only
128
- // change is a docs fix (a README tweak users WILL see on the npm page) would
129
- // otherwise collapse to a useless "Internal improvements." placeholder. Fall
130
- // back to the raw commit subjects under Changed instead of that stub.
131
- if (!body) {
132
- const fallback = subjects.filter((s) => !/^chore(:|\(release\))/i.test(s))
133
- if (fallback.length) {
134
- body = ['## Changed', ''].concat(fallback.map((s) => '- ' + s)).join(NL)
135
- }
136
- }
137
-
138
- // CHANGELOG groups are `### Added` (Keep a Changelog), the draft renders
139
- // `## Added` — normalize here.
140
- const sectionBody = body
141
- .split(NL)
142
- .map((line) => (line.startsWith('## ') ? '#' + line : line))
143
- .join(NL)
144
- const date = new Date().toISOString().slice(0, 10)
145
- const section =
146
- '## [' +
147
- next +
148
- '] - ' +
149
- date +
150
- NL +
151
- NL +
152
- (sectionBody || '### Changed' + NL + NL + '- Internal improvements.') +
153
- NL
154
-
155
- if (dryRun) {
156
- console.log('release(dry-run): ' + pkg.version + ' -> ' + next)
157
- console.log('tag: ' + tag)
158
- console.log('since: ' + ref)
159
- console.log('CHANGELOG section:' + NL + section)
160
- process.exit(0)
161
- }
162
-
163
- // 5. Insert the version section in place of the Unreleased BODY. The
164
- // Unreleased body is auto-synced from commits (see changelog:sync), so it is
165
- // the same content as the new section — leaving it in place duplicated every
166
- // entry (the real "### Fixed" block twice). Replace the whole body up to the
167
- // next version heading with the new section.
168
- const changelogPath = path.join(root, 'CHANGELOG.md')
169
- const changelog = fs.readFileSync(changelogPath, 'utf-8')
170
- const anchor = '## [Unreleased]'
171
- const idx = changelog.indexOf(anchor)
172
- if (idx < 0) fail('CHANGELOG.md has no "' + anchor + '" heading')
173
- const afterAnchor = idx + anchor.length
174
- const rest = changelog.slice(afterAnchor)
175
- const nextHeading = rest.search(/\n## \[/)
176
- const tail = nextHeading < 0 ? NL : rest.slice(nextHeading)
177
- const updated = changelog.slice(0, afterAnchor) + NL + NL + section + tail
178
- fs.writeFileSync(changelogPath, updated)
179
-
180
- // 6. Bump package.json, preserving its exact formatting (single-line replace
181
- // of the version value only).
182
- const pkgText = fs.readFileSync(pkgPath, 'utf-8')
183
- fs.writeFileSync(
184
- pkgPath,
185
- pkgText.replace(
186
- '"version": "' + pkg.version + '"',
187
- '"version": "' + next + '"',
188
- ),
189
- )
190
-
191
- // 7. Commit + tag. The push is intentionally NOT done here.
192
- git(['add', 'package.json', 'CHANGELOG.md'])
193
- git(['commit', '-m', 'chore: release ' + next])
194
- git(['tag', tag])
195
-
196
- console.log(NL + 'release: committed and tagged ' + tag)
197
- console.log('Next (explicit) step — publish via CI:')
198
- console.log(' git push')
199
- console.log(' git push origin ' + tag)
@@ -1,154 +0,0 @@
1
- // Render docs/demo.png — the terminal screenshot used in README.md.
2
- //
3
- // Why a generator instead of a live screenshot: the demo must be
4
- // REPRODUCIBLE and ENGLISH (the README is English). A live capture depends on
5
- // the operator's locale/theme/font and would drift. This renders a fixed HTML
6
- // page that mirrors the REAL editor layout (src/input.ts _writeBlock) and the
7
- // theme palette (src/theme.ts) with the DejaVu Sans Mono font that the
8
- // container actually ships, then screenshots it with headless Chromium.
9
- //
10
- // Run: npm run render-demo (tsx scripts/render-demo.mts)
11
- //
12
- // Isolation: chromium.launch() uses a THROWAWAY profile, never ~/.zames/profile
13
- // (see AGENTS.md — never touch the live agent's browser/profile).
14
-
15
- import { chromium } from 'playwright'
16
- import path from 'node:path'
17
- import { fileURLToPath } from 'node:url'
18
-
19
- const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
20
- const out = path.join(root, 'docs', 'demo.png')
21
-
22
- const C = {
23
- bg: '#23272e',
24
- user: '#8fd4f0',
25
- prompt: '#c8b06a',
26
- dir: '#7fc4f0',
27
- assistant: '#cfc9b0',
28
- tool: '#c6a97e',
29
- toolResult: '#8a9bb5',
30
- system: '#808896',
31
- dim: '#5b616e',
32
- taskSummary: '#8fa3c8',
33
- toggleOn: '#6fd0b0',
34
- }
35
-
36
- const esc = (s: unknown): string =>
37
- String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
38
- const span = (color: string, text: string, bold = false): string =>
39
- '<span style="color:' +
40
- color +
41
- (bold ? ';font-weight:700' : '') +
42
- '">' +
43
- esc(text) +
44
- '</span>'
45
-
46
- // One rendered session, line by line (mirrors the real banner + answer flow).
47
- const lines: string[] = []
48
- const push = (html: string): void => {
49
- lines.push('<div class="l">' + html + '</div>')
50
- }
51
- const blank = () => push('&nbsp;')
52
-
53
- push(span(C.system, 'Working directory: ') + span(C.dir, '/home/dev/zames_pro'))
54
- push(span(C.system, 'Version: ') + span(C.user, '2.64.1'))
55
- push(
56
- span(C.system, 'Transcript: ') +
57
- span(C.dim, '~/.zames/logs/zames_pro-2026-10-07.log'),
58
- )
59
- blank()
60
- push(
61
- span(
62
- C.system,
63
- 'Interactive mode. Enter a task. Commands — /help. Exit — /exit.',
64
- ),
65
- )
66
- push(
67
- span(
68
- C.system,
69
- 'While the agent works you can type the next message — it goes to the queue (Enter — send, Esc — abort).',
70
- ),
71
- )
72
- push(
73
- span(
74
- C.dim,
75
- 'Hint: "/" lists commands, Ctrl+R searches history, paste an image with Ctrl+Shift+V.',
76
- ),
77
- )
78
- blank()
79
- push(
80
- span(C.prompt, '❯ ', true) +
81
- span(C.dir, 'zames_pro') +
82
- span(C.dim, ' › ') +
83
- span(C.user, 'Show the version from package.json.'),
84
- )
85
- blank()
86
- push(span(C.tool, '🔧 Read') + ' ' + span(C.dim, '{"path":"package.json"}'))
87
- push(
88
- span(C.toolResult, ' → ') +
89
- span(
90
- C.toolResult,
91
- '{\n "name": "zames_pro",\n "version": "2.64.1", …\n}',
92
- ),
93
- )
94
- blank()
95
- push(span(C.assistant, '● Answer'))
96
- push(
97
- span(C.assistant, 'The version in ') +
98
- '<span style="color:' +
99
- C.assistant +
100
- ';background:#2c313a;padding:0 3px;border-radius:3px">package.json</span>' +
101
- span(C.assistant, ' is ') +
102
- span(C.assistant, '2.64.1', true) +
103
- span(C.assistant, '.'),
104
- )
105
- blank()
106
- push(span(C.taskSummary, '· duration: 6s · tools: 1 · tokens: 1.2k'))
107
- push(span(C.dim, '─'.repeat(78)))
108
-
109
- // Status line (right-aligned context + toggles) and the permanent input line.
110
- const status =
111
- '<div class="l status"><span class="grow"></span>' +
112
- span(C.toggleOn, '🧠 ') +
113
- span(C.toggleOn, '🌐 ') +
114
- span(C.system, 'ctx: 12.4k · 1.2%') +
115
- '</div>'
116
- const input =
117
- '<div class="l">' +
118
- span(C.prompt, '❯ ', true) +
119
- span(C.dir, 'zames_pro') +
120
- span(C.dim, ' › ')
121
-
122
- const html =
123
- '<!doctype html><html><head><meta charset="utf-8"><style>' +
124
- '@font-face{font-family:ZM;src:local("DejaVu Sans Mono")}' +
125
- 'html,body{margin:0;padding:0;background:' +
126
- C.bg +
127
- '}' +
128
- 'body{font-family:"DejaVu Sans Mono","Noto Color Emoji",monospace;font-size:15px;line-height:1.45;' +
129
- 'color:' +
130
- C.system +
131
- ';-webkit-font-smoothing:antialiased}' +
132
- '.screen{box-sizing:border-box;width:1197px;height:562px;padding:14px 18px;overflow:hidden;' +
133
- 'display:flex;flex-direction:column}' +
134
- '.l{white-space:pre-wrap;word-break:break-word}' +
135
- '.status{display:flex;align-items:center;margin-top:auto}' +
136
- '.grow{flex:1}' +
137
- '</style></head><body><div class="screen">' +
138
- lines.join('') +
139
- status +
140
- input +
141
- '</div></body></html>'
142
-
143
- const browser = await chromium.launch({ headless: true })
144
- try {
145
- const page = await browser.newPage({
146
- viewport: { width: 1197, height: 562 },
147
- deviceScaleFactor: 2,
148
- })
149
- await page.setContent(html, { waitUntil: 'load' })
150
- await page.screenshot({ path: out })
151
- console.log('wrote', path.relative(root, out))
152
- } finally {
153
- await browser.close()
154
- }