@crazyhappyone/dsh-tui 0.1.0-alpha.2 → 0.1.0-alpha.22
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/README.i18n.yaml +2 -2
- package/README.md +49 -23
- package/README.zh.md +49 -23
- package/assets/screenshots/dsh-tui-hello.png +0 -0
- package/assets/screenshots/dsh-tui-home.png +0 -0
- package/bin/dsh-tui.js +6 -2
- package/cordis.patch.yml +91 -0
- package/dist/index.js +65523 -0
- package/dist/startup.js +52 -0
- package/package.json +6 -2
- package/src/launcher.js +185 -13
- package/tui/README.md +126 -0
- package/tui/README.zh.md +126 -0
- package/tui/cordis.patch.yml +91 -0
package/dist/startup.js
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// ../deepseek-harness/packages/tui/tui/src/startup.ts
|
|
2
|
+
import { Command } from "commander";
|
|
3
|
+
import { parseCmdline } from "@deepseek-ai/dsh-cmdline";
|
|
4
|
+
var name = "tui-startup";
|
|
5
|
+
var inject = ["cmdlineArgs"];
|
|
6
|
+
var TUI_STARTUP_SERVICE = "tuiStartup";
|
|
7
|
+
function tuiCommand() {
|
|
8
|
+
return new Command().name("dsh --profile deepseek-tui").description(
|
|
9
|
+
"Boot the interactive deepseek-tui terminal loop; an optional task positional seeds the first message."
|
|
10
|
+
).helpOption("-h, --help", "show this help").argument("[task...]", "an optional first-message seed; multiple words are joined by spaces").option("--resume <id>", "resume the session with this id").option("--cwd <dir>", "working directory override").option(
|
|
11
|
+
"--frame-stats <path>",
|
|
12
|
+
"write per-commit render-cost JSON to this path on orderly exit"
|
|
13
|
+
).addHelpText(
|
|
14
|
+
"after",
|
|
15
|
+
`
|
|
16
|
+
Examples:
|
|
17
|
+
dsh --profile deepseek-tui open the interactive loop idle
|
|
18
|
+
dsh --profile deepseek-tui "\u8BF4 hi" seed the first message, then keep the loop open
|
|
19
|
+
dsh --profile deepseek-tui --resume <id> resume an earlier session (loads history idle)
|
|
20
|
+
`
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
function parseTuiArgs(argv) {
|
|
24
|
+
const program = tuiCommand();
|
|
25
|
+
program.exitOverride();
|
|
26
|
+
program.allowExcessArguments();
|
|
27
|
+
const opts = program.parse(argv, { from: "user" }).opts();
|
|
28
|
+
const values = { task: program.args.join(" ").trim() };
|
|
29
|
+
if (opts.resume !== void 0) values.resume = opts.resume;
|
|
30
|
+
if (opts.cwd !== void 0) values.cwd = opts.cwd;
|
|
31
|
+
if (opts.frameStats !== void 0) values.frameStats = opts.frameStats;
|
|
32
|
+
return values;
|
|
33
|
+
}
|
|
34
|
+
function apply(ctx) {
|
|
35
|
+
const program = tuiCommand();
|
|
36
|
+
program.action(() => {
|
|
37
|
+
const options = program.opts();
|
|
38
|
+
const values = { task: program.args.join(" ").trim() };
|
|
39
|
+
if (options.resume !== void 0) values.resume = options.resume;
|
|
40
|
+
if (options.cwd !== void 0) values.cwd = options.cwd;
|
|
41
|
+
if (options.frameStats !== void 0) values.frameStats = options.frameStats;
|
|
42
|
+
ctx.provide(TUI_STARTUP_SERVICE, values);
|
|
43
|
+
});
|
|
44
|
+
parseCmdline(ctx, program);
|
|
45
|
+
}
|
|
46
|
+
export {
|
|
47
|
+
TUI_STARTUP_SERVICE,
|
|
48
|
+
apply,
|
|
49
|
+
inject,
|
|
50
|
+
name,
|
|
51
|
+
parseTuiArgs
|
|
52
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@crazyhappyone/dsh-tui",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.22",
|
|
4
4
|
"description": "Source-runtime launcher for the DeepSeek Harness terminal interface",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -8,8 +8,12 @@
|
|
|
8
8
|
"dsh-tui": "bin/dsh-tui.js"
|
|
9
9
|
},
|
|
10
10
|
"files": [
|
|
11
|
+
"assets/screenshots/*.png",
|
|
11
12
|
"bin/dsh-tui.js",
|
|
12
13
|
"src/**/*.js",
|
|
14
|
+
"dist/**/*.js",
|
|
15
|
+
"cordis.patch.yml",
|
|
16
|
+
"tui/cordis.patch.yml",
|
|
13
17
|
"README.md",
|
|
14
18
|
"README.zh.md",
|
|
15
19
|
"LICENSE"
|
|
@@ -23,7 +27,7 @@
|
|
|
23
27
|
},
|
|
24
28
|
"publishConfig": {
|
|
25
29
|
"access": "public",
|
|
26
|
-
"tag": "
|
|
30
|
+
"tag": "latest"
|
|
27
31
|
},
|
|
28
32
|
"repository": {
|
|
29
33
|
"type": "git",
|
package/src/launcher.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Hybrid runtime launcher for the DeepSeek Harness TUI.
|
|
3
|
+
* Supports zero-clone lightweight launch via installed dsh, and source-runtime fallback.
|
|
3
4
|
* @module @crazyhappyone/dsh-tui/launcher
|
|
4
5
|
*/
|
|
5
6
|
|
|
@@ -16,6 +17,10 @@ const DEFAULT_SOURCE_REF = 'feat/deepseek-tui'
|
|
|
16
17
|
* @property {string} sourceUrl
|
|
17
18
|
* @property {string} sourceRef
|
|
18
19
|
* @property {string} packageManager
|
|
20
|
+
* @property {string} [dshBin]
|
|
21
|
+
* @property {'auto' | 'lightweight' | 'source'} [mode]
|
|
22
|
+
* @property {string} [packageRoot]
|
|
23
|
+
* @property {string} [homeDirectory]
|
|
19
24
|
*/
|
|
20
25
|
|
|
21
26
|
/**
|
|
@@ -31,62 +36,162 @@ const DEFAULT_SOURCE_REF = 'feat/deepseek-tui'
|
|
|
31
36
|
* @property {(path: string) => PathKind} inspectPath
|
|
32
37
|
* @property {(path: string) => void} makeDirectory
|
|
33
38
|
* @property {(path: string) => string} readText
|
|
39
|
+
* @property {(path: string, content: string) => void} [writeText]
|
|
34
40
|
* @property {(command: string, args: string[], options?: {cwd?: string, stdio?: 'inherit', capture?: boolean}) => CommandResult} run
|
|
35
41
|
* @property {(text: string) => void} writeOut
|
|
36
42
|
* @property {(text: string) => void} writeError
|
|
37
43
|
*/
|
|
38
44
|
|
|
39
45
|
/**
|
|
40
|
-
* Parse the launcher's reserved management commands.
|
|
46
|
+
* Parse the launcher's reserved management commands and flags.
|
|
41
47
|
* @param {readonly string[]} argv - arguments after the executable.
|
|
42
|
-
* @returns {{kind: 'version'} | {kind: 'update'} | {kind: 'launch', args: string[]}}
|
|
48
|
+
* @returns {{kind: 'version'} | {kind: 'update'} | {kind: 'probe'} | {kind: 'launch', args: string[], mode?: 'auto' | 'lightweight' | 'source'}}
|
|
43
49
|
*/
|
|
44
50
|
export function parseLauncherInvocation(argv) {
|
|
45
51
|
if (argv[0] === '--') return { kind: 'launch', args: [...argv.slice(1)] }
|
|
46
|
-
if (argv[0] === 'version') {
|
|
47
|
-
if (argv.length !== 1) throw new Error(
|
|
52
|
+
if (argv[0] === 'version' || argv[0] === '--version' || argv[0] === '-v' || argv[0] === '-V') {
|
|
53
|
+
if (argv.length !== 1) throw new Error(`${argv[0]} takes no arguments; use \`dsh-tui -- ${argv[0]} ...\` to send it as a task`)
|
|
48
54
|
return { kind: 'version' }
|
|
49
55
|
}
|
|
50
56
|
if (argv[0] === 'update') {
|
|
51
57
|
if (argv.length !== 1) throw new Error('update takes no arguments; use `dsh-tui -- update ...` to send it as a task')
|
|
52
58
|
return { kind: 'update' }
|
|
53
59
|
}
|
|
54
|
-
|
|
60
|
+
if (argv[0] === 'probe') {
|
|
61
|
+
if (argv.length !== 1) throw new Error('probe takes no arguments; use `dsh-tui -- probe ...` to send it as a task')
|
|
62
|
+
return { kind: 'probe' }
|
|
63
|
+
}
|
|
64
|
+
let mode = undefined
|
|
65
|
+
const args = []
|
|
66
|
+
for (let i = 0; i < argv.length; i++) {
|
|
67
|
+
const item = argv[i]
|
|
68
|
+
if (item === '--source') {
|
|
69
|
+
mode = 'source'
|
|
70
|
+
} else if (item === '--lightweight') {
|
|
71
|
+
mode = 'lightweight'
|
|
72
|
+
} else {
|
|
73
|
+
args.push(item)
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return mode !== undefined ? { kind: 'launch', args, mode } : { kind: 'launch', args }
|
|
55
77
|
}
|
|
56
78
|
|
|
57
79
|
/**
|
|
58
80
|
* Resolve deployment settings without touching the filesystem.
|
|
59
|
-
* @param {{env: Readonly<Record<string, string | undefined>>, homeDirectory: string}} input
|
|
81
|
+
* @param {{env: Readonly<Record<string, string | undefined>>, homeDirectory: string, packageRoot?: string}} input
|
|
60
82
|
* @returns {LauncherSettings}
|
|
61
83
|
*/
|
|
62
|
-
export function resolveLauncherSettings({ env, homeDirectory }) {
|
|
84
|
+
export function resolveLauncherSettings({ env, homeDirectory, packageRoot }) {
|
|
63
85
|
const runtimeDirectory = environmentValue(
|
|
64
86
|
env,
|
|
65
87
|
'DSH_TUI_RUNTIME_DIR',
|
|
66
88
|
join(homeDirectory, '.local', 'share', 'dsh-tui', 'runtime'),
|
|
67
89
|
)
|
|
68
90
|
if (!isAbsolute(runtimeDirectory)) throw new Error('DSH_TUI_RUNTIME_DIR must be an absolute path')
|
|
69
|
-
|
|
91
|
+
const envMode = env['DSH_TUI_MODE']
|
|
92
|
+
if (envMode !== undefined && envMode !== 'auto' && envMode !== 'lightweight' && envMode !== 'source') {
|
|
93
|
+
throw new Error(`DSH_TUI_MODE must be 'auto', 'lightweight', or 'source' (got '${envMode}')`)
|
|
94
|
+
}
|
|
95
|
+
const result = {
|
|
70
96
|
runtimeDirectory,
|
|
71
97
|
sourceUrl: environmentValue(env, 'DSH_TUI_SOURCE_URL', DEFAULT_SOURCE_URL),
|
|
72
98
|
sourceRef: environmentValue(env, 'DSH_TUI_SOURCE_REF', DEFAULT_SOURCE_REF),
|
|
73
99
|
packageManager: environmentValue(env, 'DSH_TUI_PNPM', 'pnpm'),
|
|
100
|
+
homeDirectory,
|
|
101
|
+
}
|
|
102
|
+
if (env['DSH_BIN']?.trim()) result.dshBin = env['DSH_BIN'].trim()
|
|
103
|
+
if (envMode) result.mode = envMode
|
|
104
|
+
if (packageRoot) result.packageRoot = packageRoot
|
|
105
|
+
return result
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Probe whether a runnable `dsh` command is available on the system.
|
|
110
|
+
* @param {{env?: Readonly<Record<string, string | undefined>>, settings?: LauncherSettings, adapters: LauncherAdapters}} input
|
|
111
|
+
* @returns {{ok: true, binPath: string, version: string} | {ok: false, reason: string}}
|
|
112
|
+
*/
|
|
113
|
+
export function probeInstalledDsh({ env = {}, settings, adapters }) {
|
|
114
|
+
const explicit = settings?.dshBin ?? env['DSH_BIN']
|
|
115
|
+
if (explicit && explicit.trim() !== '') {
|
|
116
|
+
const trimmed = explicit.trim()
|
|
117
|
+
const probe = adapters.run(trimmed, ['--version'], { capture: true })
|
|
118
|
+
if (probe.status === 0) {
|
|
119
|
+
return { ok: true, binPath: trimmed, version: probe.stdout.trim() || 'unknown' }
|
|
120
|
+
}
|
|
121
|
+
const err = probe.stderr.trim()
|
|
122
|
+
return { ok: false, reason: `DSH_BIN is set to "${trimmed}" but failed to run${err ? `: ${err}` : ` (exit code ${probe.status})`}` }
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const pathProbe = adapters.run('dsh', ['--version'], { capture: true })
|
|
126
|
+
if (pathProbe.status === 0) {
|
|
127
|
+
return { ok: true, binPath: 'dsh', version: pathProbe.stdout.trim() || 'unknown' }
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return {
|
|
131
|
+
ok: false,
|
|
132
|
+
reason: 'dsh executable was not found in PATH or DSH_BIN',
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Locate the bundled cordis.patch.yml within the package root.
|
|
138
|
+
* @param {string | undefined} packageRoot
|
|
139
|
+
* @param {LauncherAdapters} adapters
|
|
140
|
+
* @returns {string | undefined}
|
|
141
|
+
*/
|
|
142
|
+
export function findBundledPatch(packageRoot, adapters) {
|
|
143
|
+
if (!packageRoot || typeof packageRoot !== 'string') return undefined
|
|
144
|
+
const candidates = [
|
|
145
|
+
join(packageRoot, 'cordis.patch.yml'),
|
|
146
|
+
join(packageRoot, 'tui', 'cordis.patch.yml'),
|
|
147
|
+
]
|
|
148
|
+
for (const candidate of candidates) {
|
|
149
|
+
if (adapters.inspectPath(candidate) === 'file') return candidate
|
|
150
|
+
}
|
|
151
|
+
return undefined
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Ensure the tui profile manifest exists in DSH_HOME.
|
|
156
|
+
* @param {{homeDirectory?: string, env?: Readonly<Record<string, string | undefined>>, adapters: LauncherAdapters}} input
|
|
157
|
+
*/
|
|
158
|
+
export function ensureTuiProfile({ homeDirectory, env = {}, adapters }) {
|
|
159
|
+
const dshHome = env['DSH_HOME'] || (homeDirectory ? join(homeDirectory, '.dsh') : undefined)
|
|
160
|
+
if (!dshHome) return
|
|
161
|
+
const profileDir = join(dshHome, 'profiles', 'tui')
|
|
162
|
+
const manifestPath = join(profileDir, 'package.json')
|
|
163
|
+
if (adapters.inspectPath(manifestPath) === 'file') return
|
|
164
|
+
adapters.makeDirectory(profileDir)
|
|
165
|
+
const manifest = {
|
|
166
|
+
name: 'dsh-profile-tui',
|
|
167
|
+
private: true,
|
|
168
|
+
dsh: {
|
|
169
|
+
profile: {
|
|
170
|
+
bundles: ['@deepseek-ai/dsh-base'],
|
|
171
|
+
patchReload: 'startup',
|
|
172
|
+
},
|
|
173
|
+
},
|
|
174
|
+
}
|
|
175
|
+
if (typeof adapters.writeText === 'function') {
|
|
176
|
+
adapters.writeText(manifestPath, JSON.stringify(manifest, undefined, 2) + '\n')
|
|
74
177
|
}
|
|
75
178
|
}
|
|
76
179
|
|
|
77
180
|
/**
|
|
78
181
|
* Run one parsed launcher invocation.
|
|
79
|
-
* @param {{invocation: ReturnType<typeof parseLauncherInvocation>, settings: LauncherSettings, packageVersion: string, adapters: LauncherAdapters}} input
|
|
182
|
+
* @param {{invocation: ReturnType<typeof parseLauncherInvocation>, settings: LauncherSettings, packageVersion: string, adapters: LauncherAdapters, env?: Readonly<Record<string, string | undefined>>}} input
|
|
80
183
|
* @returns {number} process exit status.
|
|
81
184
|
*/
|
|
82
|
-
export function runLauncher({ invocation, settings, packageVersion, adapters }) {
|
|
185
|
+
export function runLauncher({ invocation, settings, packageVersion, adapters, env = {} }) {
|
|
83
186
|
switch (invocation.kind) {
|
|
84
187
|
case 'version':
|
|
85
188
|
return showVersion({ settings, packageVersion, adapters })
|
|
86
189
|
case 'update':
|
|
87
190
|
return updateRuntime({ settings, adapters })
|
|
191
|
+
case 'probe':
|
|
192
|
+
return showProbe({ settings, packageVersion, adapters, env })
|
|
88
193
|
case 'launch':
|
|
89
|
-
return launchTui({ args: invocation.args, settings, adapters })
|
|
194
|
+
return launchTui({ args: invocation.args, mode: invocation.mode, settings, adapters, env })
|
|
90
195
|
default:
|
|
91
196
|
return assertNever(invocation)
|
|
92
197
|
}
|
|
@@ -111,7 +216,57 @@ function runtimeState(settings, adapters) {
|
|
|
111
216
|
return { ok: true, initialized: true }
|
|
112
217
|
}
|
|
113
218
|
|
|
114
|
-
function launchTui({ args, settings, adapters }) {
|
|
219
|
+
function launchTui({ args, mode, settings, adapters, env = {} }) {
|
|
220
|
+
const effectiveMode = mode ?? settings.mode ?? 'auto'
|
|
221
|
+
|
|
222
|
+
if (effectiveMode === 'source') {
|
|
223
|
+
return launchSourceTui({ args, settings, adapters })
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const patchPath = findBundledPatch(settings.packageRoot, adapters)
|
|
227
|
+
|
|
228
|
+
if (patchPath !== undefined) {
|
|
229
|
+
const probe = probeInstalledDsh({ env, settings, adapters })
|
|
230
|
+
if (probe.ok) {
|
|
231
|
+
return launchLightweightTui({ args, binPath: probe.binPath, patchPath, adapters, env, settings })
|
|
232
|
+
}
|
|
233
|
+
if (effectiveMode === 'lightweight') {
|
|
234
|
+
return reportError(adapters, `lightweight mode requires installed dsh (${probe.reason}); run \`npm i -g @deepseek-ai/dsh\` or use \`dsh-tui --source\``)
|
|
235
|
+
}
|
|
236
|
+
} else if (effectiveMode === 'lightweight') {
|
|
237
|
+
return reportError(adapters, `bundled TUI patch not found under ${settings.packageRoot}; run \`dsh-tui update\` to initialize source runtime`)
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Fallback to source runtime if initialized
|
|
241
|
+
const state = runtimeState(settings, adapters)
|
|
242
|
+
if (state.ok && state.initialized) {
|
|
243
|
+
return launchSourceTui({ args, settings, adapters })
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (state.ok && !state.initialized) {
|
|
247
|
+
adapters.writeError('dsh-tui: 未检测到系统已安装的 dsh 核心引擎。\n')
|
|
248
|
+
adapters.writeError('dsh-tui: 推荐通过 npm 极速安装官方运行时(无需克隆完整源码仓库,仅需数十 MB):\n')
|
|
249
|
+
adapters.writeError('dsh-tui: npm install --global @deepseek-ai/dsh\n\n')
|
|
250
|
+
adapters.writeError('dsh-tui: 若您需要进行底层源码开发与测试,可运行:\n')
|
|
251
|
+
adapters.writeError('dsh-tui: dsh-tui update\n')
|
|
252
|
+
adapters.writeError('dsh-tui: dsh-tui --source\n')
|
|
253
|
+
return reportError(adapters, `runtime is not initialized at ${settings.runtimeDirectory}; run \`dsh-tui update\` first`)
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return launchSourceTui({ args, settings, adapters })
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function launchLightweightTui({ args, binPath, patchPath, adapters, env = {}, settings }) {
|
|
260
|
+
ensureTuiProfile({ homeDirectory: settings?.homeDirectory, env, adapters })
|
|
261
|
+
const result = adapters.run(
|
|
262
|
+
binPath,
|
|
263
|
+
['--profile', 'tui', '--patch', patchPath, ...args],
|
|
264
|
+
{ stdio: 'inherit' },
|
|
265
|
+
)
|
|
266
|
+
return result.status
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function launchSourceTui({ args, settings, adapters }) {
|
|
115
270
|
const state = runtimeState(settings, adapters)
|
|
116
271
|
if (!state.ok) return reportError(adapters, state.message)
|
|
117
272
|
if (!state.initialized) {
|
|
@@ -125,6 +280,23 @@ function launchTui({ args, settings, adapters }) {
|
|
|
125
280
|
return result.status
|
|
126
281
|
}
|
|
127
282
|
|
|
283
|
+
function showProbe({ settings, packageVersion, adapters, env = {} }) {
|
|
284
|
+
if (packageVersion) {
|
|
285
|
+
adapters.writeOut(`dsh-tui: ${packageVersion}\n`)
|
|
286
|
+
}
|
|
287
|
+
const probe = probeInstalledDsh({ env, settings, adapters })
|
|
288
|
+
if (probe.ok) {
|
|
289
|
+
adapters.writeOut(`dsh: installed\n`)
|
|
290
|
+
adapters.writeOut(`executable: ${probe.binPath}\n`)
|
|
291
|
+
adapters.writeOut(`dsh engine version: ${probe.version}\n`)
|
|
292
|
+
const patchPath = findBundledPatch(settings.packageRoot, adapters)
|
|
293
|
+
adapters.writeOut(`bundled patch: ${patchPath ?? 'missing'}\n`)
|
|
294
|
+
return 0
|
|
295
|
+
}
|
|
296
|
+
adapters.writeOut(`dsh: not installed (${probe.reason})\n`)
|
|
297
|
+
return 1
|
|
298
|
+
}
|
|
299
|
+
|
|
128
300
|
function showVersion({ settings, packageVersion, adapters }) {
|
|
129
301
|
adapters.writeOut(`dsh-tui ${packageVersion}\n`)
|
|
130
302
|
adapters.writeOut(`runtime directory: ${settings.runtimeDirectory}\n`)
|
package/tui/README.md
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: "The interactive terminal profile layer for users composing deepseek-tui over dsh-base and operating sessions, tools, approvals, and notifications."
|
|
3
|
+
kind: "package-bundle"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# @deepseek-ai/dsh-tui
|
|
7
|
+
|
|
8
|
+
English | [中文](README.zh.md)
|
|
9
|
+
|
|
10
|
+
## Summary
|
|
11
|
+
|
|
12
|
+
The deepseek-tui profile bundle and runtime plugin lets a user run the interactive terminal over `dsh-base`. `dsh --profile deepseek-tui` waits for application readiness, validates the terminal, creates or resumes an Agent, and flushes the owned session before exit. The layer owns terminal lifecycle and user actions while capability packages continue to own agents, persistence, tools, and model behavior.
|
|
13
|
+
|
|
14
|
+
## Table of Contents
|
|
15
|
+
|
|
16
|
+
- [Use this package](#use-this-package)
|
|
17
|
+
- [Understand the implementation](#understand-the-implementation)
|
|
18
|
+
- [Further Exploration](#further-exploration)
|
|
19
|
+
- [Model Experience](#model-experience)
|
|
20
|
+
- [Known Limitations and Deferred Work](#known-limitations-and-deferred-work)
|
|
21
|
+
- [Dev Note](#dev-note)
|
|
22
|
+
|
|
23
|
+
-----
|
|
24
|
+
|
|
25
|
+
<a id="use-this-package"></a>
|
|
26
|
+
## Use this package
|
|
27
|
+
|
|
28
|
+
### Configuration
|
|
29
|
+
|
|
30
|
+
The bundle patch reads the optional `task` seed, optional `resume`, and optional `cwd` from `tuiStartup`. The startup provider accepts an optional task positional, `--resume <id>`, and `--cwd <dir>`; with no task (or with `--resume <id>` alone) the profile boots the full-screen loop idle with an empty focused composer (placeholder `输入消息` and a one-line operation/status footer), and `Enter` on the empty composer is a no-op.
|
|
31
|
+
|
|
32
|
+
The shipped profile is `{ bundles: ['@deepseek-ai/dsh-base', '@deepseek-ai/dsh-tui'], patchReload: 'startup' }`. Profile and home patch edits take effect on the next process start; they do not recompose a running terminal, Agent, or session. Settings-file reload remains owned by the settings service.
|
|
33
|
+
|
|
34
|
+
The optional `renderPolicy` config is resolved once at startup and passed explicitly to the renderer. `transcriptOverscan` and `cache.maxRows`/`cache.maxBytes` bound physical-row projection and reconstruction caches; `stream` owns smooth/catch-up queue thresholds and per-frame work; `scroll` owns latest-target cadence, catch-up steps, and the mouse-wheel row count. `tools.previewRows`, `detailPageRows`, `cacheEntries`, and `cacheRows` bound tool previews, detail pages, and derived storage; their defaults are 6, 40, 128, and 2048. The Config schema requires positive frame and cache limits, bounded overscan/cache budgets, and exit thresholds strictly below their matching entry thresholds, so invalid combinations fail during plugin load. The shipped values live in [`cordis.patch.yml`](cordis.patch.yml), and `pnpm run test:tui:perf` runs the strict real-PTY performance lane.
|
|
35
|
+
|
|
36
|
+
Reasoning is hidden by default. Ctrl+O or `/reasoning` shows the complete dim reasoning body in the shared transcript; streaming and settlement do not collapse it. `/scrollbar` hides or shows the right-hand rail without changing reading width or navigation. `/status` toggles detailed input/output metrics; the default single-row footer retains the context meter, percentage and cache-hit rate when authoritative values and width permit. The model name appears only in the input chip. These preferences also appear in `/settings` and persist through restart. `tui.locale` selects zh-CN (default) or en-US for the new settings, status and tool-detail labels; these controls do not change model reasoning or session events.
|
|
37
|
+
|
|
38
|
+
`--frame-stats <path>` opts into per-commit render-cost measurement. The target resolves to an absolute path at startup and must be writable — an unwritable target fails the launch (never silently skipped). On orderly exit the runtime writes one JSON file to that path and nothing else:
|
|
39
|
+
|
|
40
|
+
`renderMs` and `brandRenderMs` contain recent count/mean/p95/p99/max/samples plus `run`, a fixed-storage distribution of every measured commit. Recent samples are capped at 120; full-run quantiles use microsecond histogram buckets with three significant digits. Startup/history hydration is excluded once when the workload begins. These React Profiler costs exclude Ink diff/commit and terminal display time. `pacing` records commit count and elapsed time; `brandRevealTimers` records owned idle-home timeouts at orderly exit. `frameMetrics` separately includes completed-write intervals, input/coalescing counters, drain and scroll latencies, parse work, queue depth/age and cache statistics. The file contains statistics, platform/Node/architecture and its path, not session text, and never reaches stdout.
|
|
41
|
+
|
|
42
|
+
`/tools` opens every recorded call, including generic fallbacks and process failures. Enter opens bounded source pages with the selected action and status kept in the header; n/p move forward/back, d enables raw metadata, y copies the complete original, and e exports a private, non-overwriting `tool-*.txt` file in the launch working directory. Oversized clipboard payloads are explicitly refused with an export hint rather than truncated. Esc returns to the list and then the unchanged conversation. Approval and ask-user dialogs keep input priority.
|
|
43
|
+
|
|
44
|
+
### Contract
|
|
45
|
+
|
|
46
|
+
- The runtime owns the live Agent handle, session projection and session-directory state; the render package receives only the controller interface. The top-bar title is a folded `session/title` except a `fallback` title that coexists with a human user message; before a provider or rename title the loop shows the compact mount name `DeepSeek · deepseek-tui`. Directory rows still use the first-human-message fallback. An empty idle window paints the generated official FishLogo, `DeepSeek`, and `有什么可以帮忙的` in the conversation column. The environment selects half-block, full-block, ASCII, or plain output independently of color; `brandAnimation` stores `auto | on | off`, and every input, history, overlay, resize, completion, or unmount stop clears the one-shot reveal timer. The runtime answers `approval/request` for that live agent and calls `next()` for every other agent: `y`/`a` resolve `'allowed-once'`, `n`/`d`/Esc resolve `'rejected'`, and abort (including Ctrl+C cancelling generation) resolves `'cancelled'` via the request signal. Policy `'never'` is decided by the host before this listener runs, so no ApprovalPane appears. Ctrl+E flips the current window's tool-card fold (`toolCardsExpanded`; no session event). Empty `/permission` opens the preset overlay; parameterized `/permission <name>` runs `ctx.commands.execute`. Empty `/settings` opens the settings overlay on every top-level `describe()` field (including `llm-deepseek · models` and `llm-pi-ai · providers` as JSON); Enter apply writes the selected field through `ctx.settings.update`. Browse `e` reports `prepareDocument()` and `r` rereads rows (`✓ 已重载设置`). Empty `/resume` opens the session list. `/reload` is a local command: it flushes the live session, unmounts, disposes that session, then spawns a replacement Node with the same launcher flags plus `--resume <id>`, dropping the original task positional so the first message is not sent again. Overlay `r` only rereads settings rows. Idle boot with no configured `DEEPSEEK_API_KEY` opens `首次设置`; Enter saves through `ctx.credentials.set` and then opens the model pane. Esc skips without opening it. The `tui` section owns `colorTier`, `submitOnEnter` (Enter inserts a newline when false), `notify` (`off | attention | every-turn`, default `attention`), `notifyQuietInputSeconds` (non-negative integer, default 10), `brandAnimation` (shown as `自动 / 开启 / 关闭`), `scrollbar` (boolean, default `true`), `reasoning` and `statusDetails` (booleans, default `false`), `locale` (`zh-CN | en-US`, default `zh-CN`), and the validated `renderPolicy` described above. A `user-questions/request` waterfall listener is registered in the current runtime-root Agent scope; it delegates foreign and child requests with `next()`, while accepted requests paint `AskUserPane`, bind abort/replacement/disposal to one cleanup, and notify exactly once at admission. ↑↓/jk move the highlight, and digit then Enter returns the original option label. The slash directory includes each command's `description`.
|
|
47
|
+
- `g s` toggles the persisted session directory when the composer buffer is exactly `g`; `/resume` opens the same list and does not close it. Rows with equal folded titles append a stable shortest-unique session-id hint inline; rows with unique titles remain unchanged. Left/right move the composer caret one grapheme. ↑/k shift the conversation toward older rows and ↓/j return toward live; arrows scroll even with composer text, while j/k insert when the buffer is not empty. A detached conversation always shows `↓ 底部 · End/G`, adding the unseen-row count when new output arrives. The mouse wheel uses the same direction as ↑/k on the conversation and as j/k on list panes, moves `scroll.wheelRows` physical rows per report, and combines reports from one stdin chunk into one net request. Ctrl+N starts a new session, and directory actions switch, rename or delete sessions through the owned services. Delete calls the persistence service, which rejects live, reserved, or borrowed identities and removes derived projection-cache state after durable deletion.
|
|
48
|
+
- The terminal, fallback titles, and `/export` Markdown include only direct human `user/message` events (`source.kind === 'user'`) plus assistant replies. Durable model context from agent instructions, plugins, and skill catalogs is intentionally absent from this human transcript.
|
|
49
|
+
- Ctrl+K search requests `literal-substring` matching from `ctx.sessionQuery` and filters to `user`/`assistant` transcript roles, so a query such as `回复` finds visible `只回复` text without surfacing injected model context; other service callers retain the default token-phrase mode and complete semantic corpus.
|
|
50
|
+
- SIGINT follows the interaction state machine; SIGTERM and SIGHUP request exit. Exit flushes the owned session before the process exit request; `--frame-stats` writes its JSON in the same orderly-exit path.
|
|
51
|
+
- A non-TTY output stream or unsupported terminal is rejected before the render tree mounts. On a TTY pair the render layer enables SGR mouse and OSC 8; `/help` lists wheel, click-to-open, and drag-copy.
|
|
52
|
+
|
|
53
|
+
### Advanced entries and evidence
|
|
54
|
+
|
|
55
|
+
- [`RuntimeController`](src/index.ts) supplies running subagents to the renderer's [`Mention`](../tui-render/src/mention.tsx) and all owned children to [`AgentHubPane`](../tui-render/src/agent-hub-pane.tsx). In mention selection, Up/Down or j/k change the highlighted target, Enter inserts it with exactly one trailing space without submitting, and Escape dismisses the menu. `g a` opens Agent Hub. The session directory marks the controller-owned current parent inline, and Agent Hub renders exact children as `子会话 ID · {childId}` through [`SessionPane`](../tui-render/src/session-pane.tsx) and [`AgentHubPane`](../tui-render/src/agent-hub-pane.tsx); the assembled PTY compares those rows with ownership-derived model-admission markers, and no fixture configuration supplies either id. After this identity conversion, the controller enriches render-only Hub rows from `sessionProjections.snapshot()` for live children. Cold children first use `sessionProjectionCache.cachedSnapshot(header, keys)`; a miss borrows one exact persistence observation, folds its immutable metadata and complete ordered events through `coldSnapshot(meta, events)`, and always disposes the borrow. It sums the four durable token buckets, derives context occupancy only from measured pressure plus capacity, and uses the durable subagent timing projection; a model appears only when a registered child projection provides one, so the current projection set omits it rather than copying the parent's model. Missing services, failed cold reads, and absent values omit their segments without failing the identity table or inventing zeroes. The `Σ 子代理` row aggregates known token/duration fields and reports coverage. `SubagentListEntry` remains unchanged, and base composes the persisted projection cache exactly once.
|
|
56
|
+
- Empty `/plan` opens the plan directory; plan review remains the unique user-questions dialog, where `y` submits the host label `Approve` and `n` submits `Keep planning`. The goal footer, Todo HUD, jobs HUD, and workflow HUD/`g w` overlay are controller projections and leave the composer active. `g t` opens the workspace tree, and `g f` reads and writes message feedback through its sidecar service. Presenter-tagged tool results select generic, terminal, diff, search, read, or web cards; [`stream-view.tsx`](../tui-render/src/stream-view.tsx) displays complete reasoning only when enabled, bounds a closed tool stack to one summary plus three representative cards, displays nonzero terminal outcomes as failures, adds the completion boundary, and derives TurnTail products plus exact turn-local token/cache statistics.
|
|
57
|
+
- [`deepseek-tui-advanced-entry.expected.e2e.ts`](../../../apps/cli/tests/deepseek-tui-advanced-entry.expected.e2e.ts) drives the runnable profile. Its [`session.expected.jsonl`](../../../apps/cli/tests/snapshots/deepseek-tui-advanced-entry/session.expected.jsonl) contains session-owned durable events, [`fixture-audit.expected.jsonl`](../../../apps/cli/tests/snapshots/deepseek-tui-advanced-entry/fixture-audit.expected.jsonl) records message-feedback sidecar reads and transient workflow events, and [`terminal.expected.txt`](../../../apps/cli/tests/snapshots/deepseek-tui-advanced-entry/terminal.expected.txt) records settled 80x24/200x50 cells plus normalized cold-child Hub row and aggregate evidence. The TUI files required by `vitest.expected.config.ts` own terminal transcripts under `test:expected`; top-level recorded-session snapshots separately own model replay and durable session output. The [advanced-capability Agent Note](../../../.agents/notes/implemented/feature/2026-08-18-tui-advanced-capability-entries.md) owns the rationale and verification distinctions.
|
|
58
|
+
|
|
59
|
+
### Terminal-native interactions
|
|
60
|
+
|
|
61
|
+
- Ctrl+Y copies the latest non-empty assistant message through OSC52 and an available host clipboard helper; the final closed backtick-fenced body wins over the whole message. Ctrl+G resolves `$VISUAL` before `$EDITOR`, prepares a private draft, leaves the alternate screen, runs the editor directly on `/dev/tty`, and remounts the same controller. Missing configuration and failed editor/readback paths keep the original composer text.
|
|
62
|
+
- A bracketed paste containing one local image path, or Ctrl+P with an image clipboard, adds a memory-only `[图片 #N]` segment to the structured draft. Send-time admission commits all unsaved segments through one ordered `saveImages()` batch and appends the interleaved text and durable `ImageBlock` references through the existing `user/message` event. Provider serializers resolve those references to transient request bytes; [`projection.ts`](../tui-render/src/projection.ts) renders only the message-local ordinal and optional path-stripped name, persistence loads the same blocks unchanged, and compaction preserves image-bearing prefixes while rejecting image summary output. The dedicated [`deepseek-tui-image.expected.e2e.ts`](../../../apps/cli/tests/deepseek-tui-image.expected.e2e.ts) proves composer intake, terminal replay, durable reference-only JSONL, and text/image/text provider order through the assembled profile. This path changes neither agent-loop nor `SessionEventMap`, so it adds no TypeScript or Python SDK upload operation or expected-output change.
|
|
63
|
+
- During a running turn, the first submitted immutable structured draft enters the existing `agent.steer()` path; later drafts remain in an in-memory FIFO. The HUD shows only not-yet-handed-off FIFO items as `待发 {n} · ↑ 取出`; Up restores the oldest item only while the composer is empty and no modal owner has captured the key. Inbox claim/discard, matching durable `user/message`, `turn/end`, and Agent idle events own cleanup and one-at-a-time FIFO promotion. Compaction events add a non-accent `✂ 已压缩` divider without deleting projected rows; Ctrl+K toggles the latest divider when present and otherwise keeps its session-search behavior. The [terminal-native interaction Agent Note](../../../.agents/notes/implemented/feature/2026-08-21-tui-terminal-native-polish.md) owns the rationale and trade-offs.
|
|
64
|
+
- Notifications fire on attention events, not every turn end: an approval ask or ask-user question pops the moment it enqueues, and a run settling to `agent/status` `'idle'` pops one summary line picked by the last `turn/end` reason (`✅ 任务完成`, `❌ 回合失败:{error}`, `⚠ 输出达到上限`, `⛔ 回合被策略阻塞`, `⚠ 异常中断收尾`; a user-initiated abort stays silent). Intermediate turn ends never notify, so an orchestration burst collapses into one popup. Input within `tui.notifyQuietInputSeconds` (non-negative integer, default 10s) downgrades the popup to BEL. The body carries `· {session title}` so parallel instances are distinguishable; `notify-send` receives `-u critical|normal`. iTerm2, VTE and Konsole use OSC 99; Windows Terminal uses OSC 9; other terminals receive BEL plus one best-effort literal `notify-send` attempt. Helper absence never changes turn settlement. `tui.notify` selects `off | attention | every-turn` (default `attention`; `every-turn` keeps the legacy per-turn popup).
|
|
65
|
+
|
|
66
|
+
-----
|
|
67
|
+
|
|
68
|
+
<a id="understand-the-implementation"></a>
|
|
69
|
+
## Understand the implementation
|
|
70
|
+
|
|
71
|
+
<details>
|
|
72
|
+
<summary>Implementation internals — click to expand</summary>
|
|
73
|
+
|
|
74
|
+
[`cordis.patch.yml`](cordis.patch.yml) adds terminal-owned providers and runtime glue over the base bundle without duplicating base storage services. [`src/index.ts`](src/index.ts) owns controller and lifecycle integration, while [`../tui-render/`](../tui-render/README.md) owns terminal projection and painting.
|
|
75
|
+
|
|
76
|
+
</details>
|
|
77
|
+
|
|
78
|
+
-----
|
|
79
|
+
|
|
80
|
+
<a id="further-exploration"></a>
|
|
81
|
+
## Further Exploration
|
|
82
|
+
|
|
83
|
+
- [TUI package map](../README.md) — terminal runtime and renderer ownership.
|
|
84
|
+
- [TUI renderer](../tui-render/README.md) — bounded layout, input rendering, and terminal capabilities.
|
|
85
|
+
- [Terminal subsystem](../../../docs/subsystems/terminal.md) — terminal lifecycle and generated Cordis declarations.
|
|
86
|
+
- [Alpha.1 TUI compatibility decision](../../../.agents/notes/implemented/architecture/2026-08-28-alpha1-tui-compatibility.md) — composition and lifecycle rationale.
|
|
87
|
+
|
|
88
|
+
-----
|
|
89
|
+
|
|
90
|
+
<a id="model-experience"></a>
|
|
91
|
+
## Model Experience
|
|
92
|
+
|
|
93
|
+
### Terminal application request
|
|
94
|
+
|
|
95
|
+
#### What the model sees
|
|
96
|
+
|
|
97
|
+
The profile's system prompt identifies the terminal interface, while the runtime sends user input through the ordinary Agent APIs and reconstructs visible history from the session log. Source filtering affects only terminal presentation and export: non-human context messages remain in the durable log and in model request construction.
|
|
98
|
+
|
|
99
|
+
#### Token effect
|
|
100
|
+
|
|
101
|
+
The runtime adds no model-visible text beyond the profile's `system-prompt` configuration and ordinary user turns.
|
|
102
|
+
|
|
103
|
+
#### KV Cache effect
|
|
104
|
+
|
|
105
|
+
New turns append through the Agent's normal request construction; resuming reconstructs the session history before the next request, so cache reuse remains provider-dependent.
|
|
106
|
+
|
|
107
|
+
## Known Limitations and Deferred Work
|
|
108
|
+
|
|
109
|
+
<a id="known-limitations-and-deferred-work"></a>
|
|
110
|
+
|
|
111
|
+
- **Interactive terminal required** — non-TTY launches fail before mounting because the alternate-screen interface needs terminal control.
|
|
112
|
+
- **Cross-process session ownership** — one runtime owns one live Agent handle; switching waits for the previous owned handle to settle before another session is resumed.
|
|
113
|
+
- **Provider cache behavior** — the bundle preserves session history but cannot guarantee a provider's KV-cache retention across a resumed process.
|
|
114
|
+
- **`/reload` stacks a waiting Node** — the parent cannot `execve` in-place, so it waits until the child exits. Repeated `/reload` nests processes until you quit the innermost TUI.
|
|
115
|
+
- **Memory evidence is workload-specific** — keyless UI stress records cold activation and stable-stage RSS separately. Its cache and React timing checks do not attribute unrelated provider, subprocess, or fatal out-of-memory failures.
|
|
116
|
+
- **Draft FIFO is process-local** — later-turn drafts are not crash-recovered; only messages handed to the Agent inbox or appended to the session log have durable ownership.
|
|
117
|
+
|
|
118
|
+
<a id="dev-note"></a>
|
|
119
|
+
### Dev Note
|
|
120
|
+
|
|
121
|
+
<details>
|
|
122
|
+
<summary>Working context for maintainers — click to expand</summary>
|
|
123
|
+
|
|
124
|
+
None.
|
|
125
|
+
|
|
126
|
+
</details>
|