@aiwayds/dsh-opentui 0.3.1 → 0.4.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/dist-host/index.js +4429 -1097
- package/package.json +4 -3
- package/scripts/e2e-fork.mjs +44 -0
- package/scripts/e2e-live.mjs +137 -0
- package/scripts/install-profile.mjs +6 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aiwayds/dsh-opentui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
6
|
"dsh-opentui": "bin/dsh-opentui.mjs"
|
|
@@ -27,7 +27,8 @@
|
|
|
27
27
|
"typecheck": "tsc --noEmit",
|
|
28
28
|
"build:bin": "bun build.ts",
|
|
29
29
|
"postinstall": "node scripts/postinstall.mjs",
|
|
30
|
-
"smoke": "node scripts/smoke-boot.mjs"
|
|
30
|
+
"smoke": "node scripts/smoke-boot.mjs",
|
|
31
|
+
"test:e2e": "node scripts/e2e-live.mjs && node scripts/e2e-fork.mjs"
|
|
31
32
|
},
|
|
32
33
|
"dependencies": {
|
|
33
34
|
"@opentui/core": "0.7.0",
|
|
@@ -44,4 +45,4 @@
|
|
|
44
45
|
"engines": {
|
|
45
46
|
"node": ">=26.4.0"
|
|
46
47
|
}
|
|
47
|
-
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Fork-context E2E: boot the real profile, plant a fact ("pineapple"),
|
|
3
|
+
// /history-fork at the completed turn, then ask for the fact in English —
|
|
4
|
+
// the fork is correct only if the SEEDED session answers it. Skips like
|
|
5
|
+
// e2e-live.mjs when the environment can't run it.
|
|
6
|
+
import { spawn } from 'node:child_process'
|
|
7
|
+
import { spawnSync } from 'node:child_process'
|
|
8
|
+
import { writeFileSync } from 'node:fs'
|
|
9
|
+
const cwd = `${process.env.HOME}/.dsh/profiles/dsh-opentui`
|
|
10
|
+
|
|
11
|
+
const SKIP = (() => {
|
|
12
|
+
const which = spawnSync('which', ['dsh'], { encoding: 'utf8' })
|
|
13
|
+
if (which.status !== 0 || !which.stdout.trim()) return 'no global dsh on PATH'
|
|
14
|
+
return null
|
|
15
|
+
})()
|
|
16
|
+
if (SKIP) {
|
|
17
|
+
console.log(`e2e-fork: skipped (${SKIP})`)
|
|
18
|
+
process.exit(0)
|
|
19
|
+
}
|
|
20
|
+
const env = { ...process.env, NODE_OPTIONS: '--experimental-ffi' }
|
|
21
|
+
delete env.DSH_HOME
|
|
22
|
+
const child = spawn('dsh', ['--profile', 'dsh-opentui'], { cwd, env, stdio: ['pipe','pipe','pipe'] })
|
|
23
|
+
let out = ''
|
|
24
|
+
child.stdout.on('data', c => out += c); child.stderr.on('data', c => out += c)
|
|
25
|
+
const sleep = ms => new Promise(r => setTimeout(r, ms))
|
|
26
|
+
const send = async (line) => { for (const ch of line) { child.stdin.write(ch); await sleep(70) } child.stdin.write('\r'); await sleep(400) }
|
|
27
|
+
await sleep(15000)
|
|
28
|
+
// turn 1: distinctive fact
|
|
29
|
+
await send('My favorite fruit is pineapple. Just remember it, reply OK only.')
|
|
30
|
+
await sleep(30000)
|
|
31
|
+
// fork at turn 1 (the only completed turn)
|
|
32
|
+
await send('/history')
|
|
33
|
+
await sleep(2500)
|
|
34
|
+
child.stdin.write('\r')
|
|
35
|
+
await sleep(7000)
|
|
36
|
+
// context probe in English → ASCII-safe assertion
|
|
37
|
+
await send('In one short English sentence: what is my favorite fruit?')
|
|
38
|
+
await sleep(40000)
|
|
39
|
+
writeFileSync('/tmp/fork-probe.txt', out)
|
|
40
|
+
child.kill('SIGTERM'); await sleep(600)
|
|
41
|
+
const plain = out.replace(/\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)/g, '')
|
|
42
|
+
const squash = plain.replace(/\s+/g, '')
|
|
43
|
+
console.log('fork rebuilt (no runtime-context leak):', !squash.includes('Currentruntimecontext'))
|
|
44
|
+
console.log('reply mentions pineapple:', squash.toLowerCase().includes('pineapple'))
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Live interactive E2E for @aiwayds/dsh-opentui against the REAL installed
|
|
3
|
+
// profile (real dsh host, real model turns, slow-typed piped keys).
|
|
4
|
+
//
|
|
5
|
+
// Skips (exit 0) when the environment can't run it: no global dsh on PATH
|
|
6
|
+
// or no provider key in the environment — CI runs skip; a developer
|
|
7
|
+
// machine with the profile installed runs the full 19-check suite.
|
|
8
|
+
// Drive mechanics notes:
|
|
9
|
+
// - alt-screen repaints in place: every check is presence-over-time on the
|
|
10
|
+
// whole stream, never window offsets (they mis-slice);
|
|
11
|
+
// - piped stdin DROPS burst-written keys in OpenTUI's input parser — all
|
|
12
|
+
// typing is fed char-by-char with a ~60ms inter-key gap;
|
|
13
|
+
// - OSC sequences end in BEL or ST; a BEL-only strip regex eats across
|
|
14
|
+
// frames and fakes negatives.
|
|
15
|
+
// Boots the real `dsh --profile dsh-opentui`, drives it through piped
|
|
16
|
+
// stdin, and asserts on cumulative output (alt-screen repaints in place —
|
|
17
|
+
// presence-over-time, never window offsets).
|
|
18
|
+
import { spawn } from 'node:child_process'
|
|
19
|
+
import { writeFileSync } from 'node:fs'
|
|
20
|
+
|
|
21
|
+
import { spawnSync } from 'node:child_process'
|
|
22
|
+
|
|
23
|
+
const PROFILE = 'dsh-opentui'
|
|
24
|
+
const SKIP = (() => {
|
|
25
|
+
const which = spawnSync('which', ['dsh'], { encoding: 'utf8' })
|
|
26
|
+
if (which.status !== 0 || !which.stdout.trim()) return 'no global dsh on PATH'
|
|
27
|
+
return null
|
|
28
|
+
})()
|
|
29
|
+
if (SKIP) {
|
|
30
|
+
console.log(`e2e-live: skipped (${SKIP})`)
|
|
31
|
+
process.exit(0)
|
|
32
|
+
}
|
|
33
|
+
const cwd = `${process.env.HOME}/.dsh/profiles/${PROFILE}`
|
|
34
|
+
const RAW = '/tmp/opentui-e2e.raw.txt'
|
|
35
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms))
|
|
36
|
+
|
|
37
|
+
const env = { ...process.env, NODE_OPTIONS: '--experimental-ffi' }
|
|
38
|
+
delete env.DSH_HOME
|
|
39
|
+
env.PATH = env.PATH.split(':')
|
|
40
|
+
.filter((s) => s && !s.includes('dsh-opentui/node_modules/.bin'))
|
|
41
|
+
.join(':')
|
|
42
|
+
for (const k of Object.keys(env)) if (k.startsWith('npm_config_')) delete env[k]
|
|
43
|
+
|
|
44
|
+
const child = spawn('dsh', ['--profile', PROFILE], { cwd, env, stdio: ['pipe', 'pipe', 'pipe'] })
|
|
45
|
+
let out = ''
|
|
46
|
+
child.stdout.on('data', (c) => { out += c })
|
|
47
|
+
child.stderr.on('data', (c) => { out += c })
|
|
48
|
+
|
|
49
|
+
// OpenTUI's stdin parser drops keys from burst writes under piped stdin —
|
|
50
|
+
// feed char-by-char with a real inter-key gap (a real terminal always does).
|
|
51
|
+
const send = async (line) => {
|
|
52
|
+
for (const ch of line) { child.stdin.write(ch); await sleep(60) }
|
|
53
|
+
child.stdin.write('\r')
|
|
54
|
+
}
|
|
55
|
+
const results = []
|
|
56
|
+
const check = (name, ok, hint = '') => {
|
|
57
|
+
results.push({ name, ok })
|
|
58
|
+
console.log(`${ok ? 'PASS' : 'FAIL'} ${name}${ok ? '' : ' ← ' + hint}`)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
await sleep(9000) // boot + welcome + powerline
|
|
63
|
+
|
|
64
|
+
// --- turn 1: tool use → ToolWidget ⚙, ⚒ counter, live TPS, answer
|
|
65
|
+
await send('用 bash 工具运行 date 命令,然后告诉我今天是几月几号。只回答一句话。')
|
|
66
|
+
await sleep(6000) // mid-turn: TPS segment + thinking + tool active
|
|
67
|
+
const midTurn = out.length
|
|
68
|
+
await sleep(22000)
|
|
69
|
+
|
|
70
|
+
// --- turn 2: todos + steering
|
|
71
|
+
await send('先用 todo 工具记录两个任务:测试多行、收尾,然后从 1 数到 20,每个数字单独一行。')
|
|
72
|
+
await sleep(5000)
|
|
73
|
+
await send('改一下:只要数到 10 就行。')
|
|
74
|
+
await sleep(55000)
|
|
75
|
+
|
|
76
|
+
// --- /theme picker opens, esc closes
|
|
77
|
+
await send('/theme')
|
|
78
|
+
await sleep(2000)
|
|
79
|
+
const themeOpen = out.length
|
|
80
|
+
child.stdin.write('\x1b') // esc closes the overlay
|
|
81
|
+
await sleep(1500)
|
|
82
|
+
|
|
83
|
+
// --- multi-line submit: alt+enter (= ESC CR) inserts a newline
|
|
84
|
+
for (const ch of '第一行') { child.stdin.write(ch); await sleep(60) }
|
|
85
|
+
child.stdin.write('\x1b\r')
|
|
86
|
+
await sleep(300)
|
|
87
|
+
for (const ch of '第二行') { child.stdin.write(ch); await sleep(60) }
|
|
88
|
+
await sleep(600)
|
|
89
|
+
child.stdin.write('\r') // Enter submits the buffer
|
|
90
|
+
await sleep(8000)
|
|
91
|
+
|
|
92
|
+
// --- subagent turn: dispatch a child, expect panel rows (tree rail)
|
|
93
|
+
await send('派一个子代理去运行 ls 命令看看当前目录有什么,然后把结果告诉我。')
|
|
94
|
+
await sleep(55000)
|
|
95
|
+
|
|
96
|
+
// --- /history fork at the newest turn boundary + context probe
|
|
97
|
+
await send('/history')
|
|
98
|
+
await sleep(2000)
|
|
99
|
+
child.stdin.write('\r') // pick the highlighted (newest) turn
|
|
100
|
+
await sleep(6000)
|
|
101
|
+
await send('刚才我最后请你做的是什么?一句话回答。')
|
|
102
|
+
await sleep(45000)
|
|
103
|
+
|
|
104
|
+
child.stdin.end()
|
|
105
|
+
await Promise.race([sleep(4000), new Promise((r) => child.once('exit', r))])
|
|
106
|
+
} finally {
|
|
107
|
+
child.kill('SIGTERM')
|
|
108
|
+
writeFileSync(RAW, out)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const plain = out.replace(/\x1b\][^\x07\x1b]*(?:\x07|\x1b\\\\)/g, '').replace(/\x1b\[[0-9;?]*[a-zA-Z]/g, '')
|
|
112
|
+
const full = plain
|
|
113
|
+
const themeWin = plain.slice(0) // whole stream; markers are phase-specific
|
|
114
|
+
|
|
115
|
+
check('welcome banner (block DSH)', /D\s*S\s*H|█|▄|▀/.test(full) || full.includes('dsh ·'), 'no ascii DSH')
|
|
116
|
+
check('daily quote 「…」', full.includes('「') && full.includes('」'), 'no quote')
|
|
117
|
+
check('powerline clock ticks', /\d{1,2}:\d{2}:\d{2}/.test(full), 'no clock')
|
|
118
|
+
check('ThinkWidget appears', full.includes('💭') || full.includes('thinking ·'), 'no think dock')
|
|
119
|
+
check('ToolWidget ⚙ running state', full.includes('⚙'), 'no ⚙')
|
|
120
|
+
check('tool counter ⚒', full.includes('⚒'), 'no ⚒')
|
|
121
|
+
check('live TPS segment (tok/s)', full.includes('tok/s'), 'no tok/s anywhere')
|
|
122
|
+
check('answer delivered (Chinese date phrase)', /\d{1,2}月\d{1,2}日/.test(full), 'no rendered 中文日期 in reply')
|
|
123
|
+
check('steer ⚡ line', full.includes('⚡'), 'no ⚡')
|
|
124
|
+
check('todo panel 📋 or ▶/☐ rows', full.includes('📋') || full.includes('☐') || full.includes('▶'), 'no todo widget rows')
|
|
125
|
+
check('theme picker opened (dark/light group headers)', themeWin.includes('dark') && themeWin.includes('light'), 'no group headers')
|
|
126
|
+
check('theme picker lists themes (dracula)', themeWin.includes('dracula'), 'no dracula in stream')
|
|
127
|
+
check('multi-line submit (第一行+第二行 as user row)', full.includes('第一行') && full.includes('第二行'), 'multi-line rows missing')
|
|
128
|
+
check('subagent panel rows (tree rail)', full.includes('├─') || full.includes('└─'), 'no subagent rail rows')
|
|
129
|
+
check('history picker items (fork before this turn)', full.includes('fork before this turn'), 'no picker item rows')
|
|
130
|
+
check('fork transcript rebuild without internal leak', !out.replace(/\s+/g, '').includes('Currentruntimecontext'), 'runtime-context leaked into rebuild')
|
|
131
|
+
check('no loader failure markers', !full.includes('did not activate') && !full.includes('failed to import'), '')
|
|
132
|
+
check('no SSR leak (dist/server.js)', !out.includes('dist/server.js'), '')
|
|
133
|
+
check('alt-screen entered', out.includes('\x1b[?1049h'), '')
|
|
134
|
+
|
|
135
|
+
const fails = results.filter((r) => !r.ok)
|
|
136
|
+
console.log(`\n${results.length - fails.length}/${results.length} passed — raw: ${RAW} (${out.length} bytes)`)
|
|
137
|
+
process.exit(fails.length ? 1 : 0)
|
|
@@ -86,6 +86,12 @@ const patchBody = `# dsh-opentui profile patch (managed by @aiwayds/dsh-opentui
|
|
|
86
86
|
model: ${model}
|
|
87
87
|
reasoningEffort: ${reasoningEffort}
|
|
88
88
|
- insert:
|
|
89
|
+
- id: tool-ask-user
|
|
90
|
+
name: "@deepseek-ai/dsh-tool-ask-user"
|
|
91
|
+
- id: tool-todo
|
|
92
|
+
name: "@deepseek-ai/dsh-tool-todo"
|
|
93
|
+
config:
|
|
94
|
+
allowParallelInProgress: false
|
|
89
95
|
- id: dsh-opentui
|
|
90
96
|
name: "@aiwayds/dsh-opentui"
|
|
91
97
|
config:
|