zames_pro 2.75.5 → 2.76.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.
- package/CHANGELOG.md +25 -0
- package/README.md +10 -4
- package/dist/agent-loop.js +61 -1
- package/dist/browser.js +40 -8
- package/dist/commands.js +0 -1
- package/dist/config.js +8 -3
- package/dist/context.js +99 -0
- package/dist/deepseek-ui.js +1 -3
- package/dist/i18n/common.js +13 -16
- package/dist/i18n/config.js +2 -2
- package/dist/i18n/help.js +48 -6
- package/dist/i18n/spinner.js +7 -1
- package/dist/index.js +10 -50
- package/dist/net-capture.js +43 -11
- package/dist/provider-handoff.js +0 -4
- package/dist/qwen-ui.js +0 -8
- package/dist/run-task.js +24 -1
- package/dist/shell.js +0 -6
- package/dist/system-prompt.js +52 -2
- package/dist/tools.js +33 -0
- package/dist/tui/AskQuestion.js +108 -0
- package/dist/tui/FullScreenApp.js +2 -1
- package/dist/tui/InputLine.js +0 -3
- package/dist/tui/StatusBar.js +0 -9
- package/dist/tui/TuiEditor.js +4 -43
- package/dist/tui/edit.js +13 -22
- package/dist/tui/keys.js +12 -14
- package/dist/tui/run-ask.js +22 -0
- package/dist/tui/store.js +0 -2
- package/dist/tui/widgets.js +3 -9
- package/package.json +2 -2
- package/scripts/build-readme.mts +0 -101
- package/scripts/changelog-draft.mts +0 -93
- package/scripts/clean-dist.mts +0 -17
- package/scripts/coverage-gate.mts +0 -112
- package/scripts/ctd-live-driver.mts +0 -51
- package/scripts/ctd-live.tsx +0 -33
- package/scripts/escd-live-driver.mts +0 -37
- package/scripts/eslint-ts6.mts +0 -58
- package/scripts/inline-status-driver.mts +0 -31
- package/scripts/inline-status.tsx +0 -33
- package/scripts/mouse-churn.mts +0 -34
- package/scripts/qwen-models-smoke.mts +0 -117
- package/scripts/qwen-smoke.mts +0 -221
- package/scripts/release.mts +0 -199
- package/scripts/render-demo.mts +0 -154
- package/scripts/render-how-it-works.mts +0 -229
- package/scripts/ru-shot.mts +0 -20
- package/scripts/sched-smoke.mts +0 -150
- package/scripts/self-smoke.mts +0 -309
- package/scripts/tui-comp-shot.mts +0 -63
- package/scripts/tui-components.tsx +0 -62
- package/scripts/tui-config-driver.mts +0 -82
- package/scripts/tui-config-probe.tsx +0 -74
- package/scripts/tui-edit-driver.mts +0 -100
- package/scripts/tui-edit-probe.tsx +0 -38
- package/scripts/tui-editor-probe.tsx +0 -24
- package/scripts/tui-input-driver.mts +0 -54
- package/scripts/tui-input-shot.tsx +0 -59
- package/scripts/tui-log-driver.mts +0 -35
- package/scripts/tui-log-shot.tsx +0 -48
- package/scripts/tui-longpaste-driver.mts +0 -56
- package/scripts/tui-longpaste.tsx +0 -35
- package/scripts/tui-manyselect-shot.mts +0 -34
- package/scripts/tui-manyselect.tsx +0 -17
- package/scripts/tui-mouse-churn-probe.tsx +0 -28
- package/scripts/tui-paste-driver.mts +0 -88
- package/scripts/tui-paste-kinds.tsx +0 -76
- package/scripts/tui-pastetest.mts +0 -38
- package/scripts/tui-probe.tsx +0 -62
- package/scripts/tui-repro.tsx +0 -64
- package/scripts/tui-resize.mts +0 -70
- package/scripts/tui-scroll-driver.mts +0 -53
- package/scripts/tui-scroll-probe.tsx +0 -46
- package/scripts/tui-shot.mts +0 -106
- package/scripts/tui-status-driver.mts +0 -24
- package/scripts/tui-status-shot.tsx +0 -27
- package/scripts/tui-submit.tsx +0 -57
- package/scripts/tui-wheel-driver.mts +0 -61
- package/scripts/tui-wheel-probe.tsx +0 -48
- package/scripts/tui-widgets-driver.mts +0 -20
- package/scripts/tui-widgets.tsx +0 -44
package/scripts/self-smoke.mts
DELETED
|
@@ -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)
|
|
@@ -1,82 +0,0 @@
|
|
|
1
|
-
// Drive scripts/tui-config-probe.tsx under a pty: send keys one at a time,
|
|
2
|
-
// screenshot the VT100 grid at the end, and print the probe's set/reset log.
|
|
3
|
-
// Used by test/tui-pty-render.test.ts to pin the config-menu regressions
|
|
4
|
-
// (the stale value after a toggle, and the overflowing block at a small
|
|
5
|
-
// terminal).
|
|
6
|
-
import { spawn } from 'node:child_process'
|
|
7
|
-
import fs from 'node:fs'
|
|
8
|
-
import path from 'node:path'
|
|
9
|
-
import { Term } from '../test/vt100.mts'
|
|
10
|
-
|
|
11
|
-
const ESC = String.fromCharCode(27)
|
|
12
|
-
const NL = String.fromCharCode(10)
|
|
13
|
-
|
|
14
|
-
function arg(name: string, def = ''): string {
|
|
15
|
-
const hit = process.argv.find((a) => a.startsWith('--' + name + '='))
|
|
16
|
-
return hit ? hit.slice(name.length + 3) : def
|
|
17
|
-
}
|
|
18
|
-
const cols = Number(arg('cols', '90'))
|
|
19
|
-
const rows = Number(arg('rows', '30'))
|
|
20
|
-
const keys = arg('keys', '')
|
|
21
|
-
.split(',')
|
|
22
|
-
.filter((s) => s.length > 0)
|
|
23
|
-
.map((s) =>
|
|
24
|
-
s === 'RIGHT'
|
|
25
|
-
? ESC + '[C'
|
|
26
|
-
: s === 'LEFT'
|
|
27
|
-
? ESC + '[D'
|
|
28
|
-
: s === 'DOWN'
|
|
29
|
-
? ESC + '[B'
|
|
30
|
-
: s === 'UP'
|
|
31
|
-
? ESC + '[A'
|
|
32
|
-
: s === 'ENTER'
|
|
33
|
-
? String.fromCharCode(13)
|
|
34
|
-
: s,
|
|
35
|
-
)
|
|
36
|
-
|
|
37
|
-
const LOG = '/tmp/config-probe.log'
|
|
38
|
-
try {
|
|
39
|
-
fs.unlinkSync(LOG)
|
|
40
|
-
} catch {
|
|
41
|
-
/* ignore */
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
const term = new Term(rows, cols)
|
|
45
|
-
const child = spawn(
|
|
46
|
-
'script',
|
|
47
|
-
['-qec', 'npx tsx scripts/tui-config-probe.tsx', '/dev/null'],
|
|
48
|
-
{
|
|
49
|
-
cwd: path.resolve('.'),
|
|
50
|
-
env: {
|
|
51
|
-
...process.env,
|
|
52
|
-
COLUMNS: String(cols),
|
|
53
|
-
LINES: String(rows),
|
|
54
|
-
PROBE_CONFIG_LOG: LOG,
|
|
55
|
-
},
|
|
56
|
-
},
|
|
57
|
-
)
|
|
58
|
-
child.stdout.on('data', (d) => term.write(d.toString('utf-8')))
|
|
59
|
-
child.stderr.on('data', (d) => term.write(d.toString('utf-8')))
|
|
60
|
-
|
|
61
|
-
let i = 0
|
|
62
|
-
const tick = (): void => {
|
|
63
|
-
if (i < keys.length) {
|
|
64
|
-
child.stdin.write(keys[i])
|
|
65
|
-
i++
|
|
66
|
-
setTimeout(tick, 220)
|
|
67
|
-
} else {
|
|
68
|
-
setTimeout(() => {
|
|
69
|
-
process.stdout.write('===SCREEN===' + NL)
|
|
70
|
-
process.stdout.write(term.render() + NL)
|
|
71
|
-
process.stdout.write('===LOG===' + NL)
|
|
72
|
-
try {
|
|
73
|
-
process.stdout.write(fs.readFileSync(LOG, 'utf-8'))
|
|
74
|
-
} catch {
|
|
75
|
-
/* ignore */
|
|
76
|
-
}
|
|
77
|
-
child.kill('SIGTERM')
|
|
78
|
-
}, 500)
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
setTimeout(tick, 5000)
|
|
82
|
-
child.on('close', () => process.exit(0))
|
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
// Mount the REAL Ink config menu with the REAL schema and callbacks so a pty
|
|
2
|
-
// run can screenshot it and drive it (toggle a boolean, cycle an enum, open an
|
|
3
|
-
// edit prompt) — the menu is otherwise only reachable from a live agent.
|
|
4
|
-
|
|
5
|
-
import React from 'react'
|
|
6
|
-
import fs from 'node:fs'
|
|
7
|
-
import { render } from 'ink'
|
|
8
|
-
import { ConfigMenuApp } from '../src/tui/ConfigMenu.js'
|
|
9
|
-
import {
|
|
10
|
-
CONFIG_SCHEMA,
|
|
11
|
-
DEFAULTS,
|
|
12
|
-
validateConfigValue,
|
|
13
|
-
type ConfigField,
|
|
14
|
-
} from '../src/config.js'
|
|
15
|
-
import { translate } from '../src/i18n.js'
|
|
16
|
-
|
|
17
|
-
const LOG = process.env.PROBE_CONFIG_LOG || '/tmp/config-probe.log'
|
|
18
|
-
// A tiny in-memory config so get/set/reset are real (validate + default).
|
|
19
|
-
function getByPath(obj: Record<string, unknown>, path: string): unknown {
|
|
20
|
-
return path
|
|
21
|
-
.split('.')
|
|
22
|
-
.reduce<unknown>(
|
|
23
|
-
(o, k) => (o == null ? undefined : (o as Record<string, unknown>)[k]),
|
|
24
|
-
obj,
|
|
25
|
-
)
|
|
26
|
-
}
|
|
27
|
-
function setByPath(path: string, value: unknown): void {
|
|
28
|
-
const parts = path.split('.')
|
|
29
|
-
let o: Record<string, unknown> = cfg
|
|
30
|
-
for (let i = 0; i < parts.length - 1; i++) {
|
|
31
|
-
o[parts[i]] = o[parts[i]] ?? {}
|
|
32
|
-
o = o[parts[i]] as Record<string, unknown>
|
|
33
|
-
}
|
|
34
|
-
o[parts[parts.length - 1]] = value
|
|
35
|
-
}
|
|
36
|
-
const cfg: Record<string, unknown> = {}
|
|
37
|
-
// Seed a few NESTED values so toggling is observable (the menu reads through
|
|
38
|
-
// getByPath, which walks the path — a flat key would never be found).
|
|
39
|
-
setByPath('ui.locale', 'ru')
|
|
40
|
-
setByPath('ui.tui', true)
|
|
41
|
-
setByPath('ui.alternateScreen', true)
|
|
42
|
-
setByPath('ui.mouseScroll', false)
|
|
43
|
-
setByPath('maxIterations', 0)
|
|
44
|
-
setByPath('headless', true)
|
|
45
|
-
setByPath('provider', 'deepseek')
|
|
46
|
-
|
|
47
|
-
const log = (line: string): void => {
|
|
48
|
-
fs.appendFileSync(LOG, line + String.fromCharCode(10))
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
render(
|
|
52
|
-
React.createElement(ConfigMenuApp, {
|
|
53
|
-
fields: CONFIG_SCHEMA,
|
|
54
|
-
t: translate('ru'),
|
|
55
|
-
get: (p: string) => getByPath(cfg, p),
|
|
56
|
-
getDefault: (p: string) =>
|
|
57
|
-
getByPath(DEFAULTS as unknown as Record<string, unknown>, p),
|
|
58
|
-
set: (field: ConfigField, raw: string) => {
|
|
59
|
-
const v = validateConfigValue(field, raw)
|
|
60
|
-
setByPath(field.path, v)
|
|
61
|
-
log('SET ' + field.path + '=' + JSON.stringify(v))
|
|
62
|
-
},
|
|
63
|
-
reset: (field: ConfigField) => {
|
|
64
|
-
setByPath(
|
|
65
|
-
field.path,
|
|
66
|
-
getByPath(DEFAULTS as unknown as Record<string, unknown>, field.path),
|
|
67
|
-
)
|
|
68
|
-
log('RESET ' + field.path)
|
|
69
|
-
},
|
|
70
|
-
onExit: () => log('EXIT'),
|
|
71
|
-
}),
|
|
72
|
-
{ exitOnCtrlC: false, patchConsole: false },
|
|
73
|
-
)
|
|
74
|
-
setInterval(() => {}, 1 << 30)
|
|
@@ -1,100 +0,0 @@
|
|
|
1
|
-
// Drive scripts/tui-edit-probe.tsx under a pty with a scripted key sequence,
|
|
2
|
-
// then print the LAST buffer state the probe logged.
|
|
3
|
-
import { spawn } from 'node:child_process'
|
|
4
|
-
import fs from 'node:fs'
|
|
5
|
-
import path from 'node:path'
|
|
6
|
-
|
|
7
|
-
const ESC = String.fromCharCode(27)
|
|
8
|
-
const LOG = '/tmp/edit-probe.log'
|
|
9
|
-
|
|
10
|
-
function arg(name: string, def = ''): string {
|
|
11
|
-
const hit = process.argv.find((a) => a.startsWith('--' + name + '='))
|
|
12
|
-
return hit ? hit.slice(name.length + 3) : def
|
|
13
|
-
}
|
|
14
|
-
const seed = arg('seed', '')
|
|
15
|
-
const keys = arg('keys', '')
|
|
16
|
-
.split(',')
|
|
17
|
-
.filter((s) => s.length > 0)
|
|
18
|
-
.map((s) =>
|
|
19
|
-
s === 'CTDL'
|
|
20
|
-
? ESC + '[3;5~'
|
|
21
|
-
: s === 'CTDLU'
|
|
22
|
-
? ESC + '[3;5u'
|
|
23
|
-
: s === 'CTB127U'
|
|
24
|
-
? ESC + '[127;5u'
|
|
25
|
-
: s === 'DELU'
|
|
26
|
-
? ESC + '[3u'
|
|
27
|
-
: s === 'BSU'
|
|
28
|
-
? ESC + '[127u'
|
|
29
|
-
: s === 'CTRLBS'
|
|
30
|
-
? ESC + String.fromCharCode(127)
|
|
31
|
-
: s === 'CTB08'
|
|
32
|
-
? String.fromCharCode(8)
|
|
33
|
-
: s === 'DEL'
|
|
34
|
-
? ESC + '[3~'
|
|
35
|
-
: s === 'CTRLUNDO'
|
|
36
|
-
? String.fromCharCode(31)
|
|
37
|
-
: s === 'CTRLU'
|
|
38
|
-
? String.fromCharCode(21)
|
|
39
|
-
: s === 'CTRLK'
|
|
40
|
-
? String.fromCharCode(11)
|
|
41
|
-
: s === 'CTRLR'
|
|
42
|
-
? String.fromCharCode(18)
|
|
43
|
-
: s === 'ESCAPE'
|
|
44
|
-
? ESC
|
|
45
|
-
: s === 'CTRLW'
|
|
46
|
-
? String.fromCharCode(23)
|
|
47
|
-
: s === 'LEFT'
|
|
48
|
-
? ESC + '[D'
|
|
49
|
-
: s === 'RIGHT'
|
|
50
|
-
? ESC + '[C'
|
|
51
|
-
: s === 'UP'
|
|
52
|
-
? ESC + '[A'
|
|
53
|
-
: s === 'DOWN'
|
|
54
|
-
? ESC + '[B'
|
|
55
|
-
: s === 'ENTER'
|
|
56
|
-
? String.fromCharCode(13)
|
|
57
|
-
: s,
|
|
58
|
-
)
|
|
59
|
-
|
|
60
|
-
const child = spawn(
|
|
61
|
-
'script',
|
|
62
|
-
['-qec', 'npx tsx scripts/tui-edit-probe.tsx', '/dev/null'],
|
|
63
|
-
{
|
|
64
|
-
cwd: path.resolve('.'),
|
|
65
|
-
env: {
|
|
66
|
-
...process.env,
|
|
67
|
-
COLUMNS: '80',
|
|
68
|
-
LINES: '24',
|
|
69
|
-
PROBE_EDIT_LOG: LOG,
|
|
70
|
-
PROBE_EDIT_SEED: seed,
|
|
71
|
-
},
|
|
72
|
-
},
|
|
73
|
-
)
|
|
74
|
-
child.stdout.on('data', () => {})
|
|
75
|
-
child.stderr.on('data', () => {})
|
|
76
|
-
|
|
77
|
-
let i = 0
|
|
78
|
-
const tick = (): void => {
|
|
79
|
-
if (i < keys.length) {
|
|
80
|
-
child.stdin.write(keys[i])
|
|
81
|
-
i++
|
|
82
|
-
setTimeout(tick, 200)
|
|
83
|
-
} else {
|
|
84
|
-
setTimeout(() => child.kill('SIGTERM'), 300)
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
setTimeout(tick, 5000)
|
|
88
|
-
child.on('close', () => {
|
|
89
|
-
let lines: string[] = []
|
|
90
|
-
try {
|
|
91
|
-
lines = fs.readFileSync(LOG, 'utf-8').trim().split(String.fromCharCode(10))
|
|
92
|
-
} catch {
|
|
93
|
-
/* ignore */
|
|
94
|
-
}
|
|
95
|
-
console.log(
|
|
96
|
-
'SEED=' + JSON.stringify(seed) + ' KEYS=' + JSON.stringify(arg('keys', '')),
|
|
97
|
-
)
|
|
98
|
-
console.log('FINAL=' + (lines[lines.length - 1] || '(none)'))
|
|
99
|
-
process.exit(0)
|
|
100
|
-
})
|