@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aiwayds/dsh-opentui",
3
- "version": "0.3.1",
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: