@songtonyli/dsh-cli 0.1.8 → 0.1.10
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.md +1 -1
- package/bin/dsh.mjs +1 -1
- package/node_modules/@deepseek-ai/dsh-api-authorization-controller/README.i18n.yaml +2 -2
- package/node_modules/@deepseek-ai/dsh-api-authorization-controller/README.md +1 -1
- package/node_modules/@deepseek-ai/dsh-api-authorization-controller/README.zh.md +1 -1
- package/node_modules/@deepseek-ai/dsh-api-authorization-controller/lib/index.js +1 -0
- package/node_modules/@deepseek-ai/dsh-api-authorization-controller/lib/typert.host.js +2 -1
- package/node_modules/@deepseek-ai/dsh-api-authorization-controller/lib/typert.remote-client.js +1 -0
- package/node_modules/@deepseek-ai/dsh-api-authorization-controller/lib/types/index.js +1 -0
- package/node_modules/@deepseek-ai/dsh-api-remotes/lib/client.js +1 -0
- package/node_modules/@deepseek-ai/dsh-authorization/README.i18n.yaml +2 -2
- package/node_modules/@deepseek-ai/dsh-authorization/README.md +1 -1
- package/node_modules/@deepseek-ai/dsh-authorization/README.zh.md +1 -1
- package/node_modules/@deepseek-ai/dsh-client-ui-settings-signin/lib/client.js +1 -0
- package/node_modules/@deepseek-ai/dsh-client-ui-sidebar/lib/client.js +1 -1
- package/node_modules/@deepseek-ai/dsh-llm-pi-ai/README.i18n.yaml +2 -2
- package/node_modules/@deepseek-ai/dsh-llm-pi-ai/README.md +1 -1
- package/node_modules/@deepseek-ai/dsh-llm-pi-ai/README.zh.md +1 -1
- package/node_modules/@deepseek-ai/dsh-llm-pi-ai/lib/index.js +3 -1
- package/node_modules/@deepseek-ai/dsh-native-command/README.i18n.yaml +2 -2
- package/node_modules/@deepseek-ai/dsh-native-command/README.md +7 -2
- package/node_modules/@deepseek-ai/dsh-native-command/README.zh.md +7 -2
- package/node_modules/@deepseek-ai/dsh-native-command/lib/index.js +93 -2
- package/node_modules/@deepseek-ai/dsh-native-command/package.json +4 -0
- package/node_modules/@deepseek-ai/dsh-tool-cordis/lib/index.js +2 -2
- package/node_modules/@deepseek-ai/dsh-tui-app/README.i18n.yaml +2 -2
- package/node_modules/@deepseek-ai/dsh-tui-app/README.md +10 -4
- package/node_modules/@deepseek-ai/dsh-tui-app/README.zh.md +10 -4
- package/node_modules/@deepseek-ai/dsh-tui-app/cordis.patch.yml +3 -2
- package/node_modules/@deepseek-ai/dsh-tui-app/lib/index.js +229 -48
- package/node_modules/@deepseek-ai/dsh-tui-app/lib/startup.js +3 -2
- package/node_modules/@deepseek-ai/dsh-tui-app/package.json +2 -0
- package/node_modules/@deepseek-ai/dsh-web-app/lib/index.js +2 -77
- package/node_modules/@deepseek-ai/dsh-web-app/package.json +1 -2
- package/node_modules/@types/node/README.md +1 -1
- package/node_modules/@types/node/package.json +2 -2
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -26,7 +26,7 @@ workspace and no build step. Native packages that ship one build per platform
|
|
|
26
26
|
ripgrep, the PTY backend's addons, sharp, koffi, and the Landlock launcher — are
|
|
27
27
|
ordinary optional dependencies, so npm resolves the right one for your machine.
|
|
28
28
|
|
|
29
|
-
Requires Node `^22.19.0 || >=24.0.0`. Version 0.1.
|
|
29
|
+
Requires Node `^22.19.0 || >=24.0.0`. Version 0.1.10; `dsh --version`
|
|
30
30
|
also reports the harness build the package was assembled from.
|
|
31
31
|
|
|
32
32
|
## License
|
package/bin/dsh.mjs
CHANGED
|
@@ -18,7 +18,7 @@ const argv = process.argv.slice(2)
|
|
|
18
18
|
// version this package publishes. Reporting only one of the two would make an
|
|
19
19
|
// ordinary `npm install @songtonyli/dsh-cli@x` look like it installed something else.
|
|
20
20
|
if (argv.length === 1 && (argv[0] === '--version' || argv[0] === '-V')) {
|
|
21
|
-
console.log('@songtonyli/dsh-cli 0.1.
|
|
21
|
+
console.log('@songtonyli/dsh-cli 0.1.10 (deepseek-harness 0.1.6-alpha.1)')
|
|
22
22
|
process.exit(0)
|
|
23
23
|
}
|
|
24
24
|
|
|
@@ -2,5 +2,5 @@
|
|
|
2
2
|
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
|
3
3
|
# after editing either side, bring the other along and re-record with:
|
|
4
4
|
# pnpm run verify-translation-pairing --write packages/api/authorization-controller/README.md
|
|
5
|
-
README.md:
|
|
6
|
-
README.zh.md:
|
|
5
|
+
README.md: 6020b15cee27504168035a1c35876fe41be09771
|
|
6
|
+
README.zh.md: 54ef3a48613ca189e6809f83303fdc946dfd7fd1
|
|
@@ -50,7 +50,7 @@ This package takes no configuration.
|
|
|
50
50
|
|
|
51
51
|
### Running one attempt
|
|
52
52
|
|
|
53
|
-
`begin(request, signal)` is a stream. Each item is one frame: a `notice` carrying what the human must do
|
|
53
|
+
`begin(request, signal)` is a stream. Each item is one frame: a `notice` carrying what the human must do, any page or code it refers to, and whether a desktop surface should hand that page to the default browser, a `prompt` carrying a question and the id an answer names, a `prompt-withdrawn` for a question the flow retired, and a final `settled` frame carrying `authorized` or `cancelled`. The attempt starts when the carrier pulls the first frame and ends when the stream does; closing the stream withdraws it. `cancel(key)` withdraws it from a call that no longer holds the stream, which is what a Cancel button on a re-rendered page uses.
|
|
54
54
|
|
|
55
55
|
A question is answered by a second call naming the frame's `promptId`: `answer(key, promptId, value)` with the typed text or the chosen option's id, or `decline(key, promptId)` when the human says no, which settles the attempt as `cancelled`. Only the attempt's own prompts can be answered, so a stale id is refused rather than silently applied to a newer attempt.
|
|
56
56
|
|
|
@@ -50,7 +50,7 @@ kind: "package-reference"
|
|
|
50
50
|
|
|
51
51
|
### 运行一次尝试
|
|
52
52
|
|
|
53
|
-
`begin(request, signal)` 是一条流。每一项都是一个帧:`notice`
|
|
53
|
+
`begin(request, signal)` 是一条流。每一项都是一个帧:`notice` 携带人需要做的事、它涉及的页面或验证码,以及桌面界面是否应把该页面交给默认浏览器,`prompt` 携带一个提问以及回答时要指明的 id,`prompt-withdrawn` 对应 flow 撤回的提问,最后的 `settled` 帧携带 `authorized` 或 `cancelled`。尝试在载体拉取第一帧时开始,随流结束而结束;关闭该流即撤回它。`cancel(key)` 用于已不再持有该流的调用撤回它,这正是重新渲染后的页面上「取消」按钮所用的方式。
|
|
54
54
|
|
|
55
55
|
回答提问要用第二次调用并指明该帧的 `promptId`:`answer(key, promptId, value)` 传入输入的文本或所选项的 id,或在人拒绝时用 `decline(key, promptId)`,后者会让尝试以 `cancelled` 结算。只有该尝试自己的提问可以被回答,因此过期的 id 会被拒绝,而不会被悄悄用在更新的尝试上。
|
|
56
56
|
|
|
@@ -398,6 +398,7 @@ let AuthorizationController = (() => {
|
|
|
398
398
|
type: "notice",
|
|
399
399
|
message: notice.message,
|
|
400
400
|
...notice.url === void 0 ? {} : { url: notice.url },
|
|
401
|
+
...notice.openInBrowser === void 0 ? {} : { openInBrowser: notice.openInBrowser },
|
|
401
402
|
...notice.code === void 0 ? {} : { code: notice.code }
|
|
402
403
|
});
|
|
403
404
|
},
|
|
@@ -19,6 +19,7 @@ const _deepseek_ai_dsh_api_authorization_controller_authorization_begin_result$s
|
|
|
19
19
|
'type': z.literal("notice").readonly(),
|
|
20
20
|
'message': z.string().readonly(),
|
|
21
21
|
'url': z.string().readonly().optional(),
|
|
22
|
+
'openInBrowser': z.literal(true).readonly().optional(),
|
|
22
23
|
'code': z.string().readonly().optional(),
|
|
23
24
|
}), z.object({
|
|
24
25
|
'type': z.literal("prompt").readonly(),
|
|
@@ -318,7 +319,7 @@ export const TYPERT = {
|
|
|
318
319
|
},
|
|
319
320
|
{
|
|
320
321
|
"name": "AuthorizationNoticeFrame",
|
|
321
|
-
"declaration": "export interface AuthorizationNoticeFrame {\n readonly type: 'notice';\n readonly message: string;\n readonly url?: string;\n readonly code?: string;\n}"
|
|
322
|
+
"declaration": "export interface AuthorizationNoticeFrame {\n readonly type: 'notice';\n readonly message: string;\n readonly url?: string;\n readonly openInBrowser?: true;\n readonly code?: string;\n}"
|
|
322
323
|
},
|
|
323
324
|
{
|
|
324
325
|
"name": "AuthorizationPromptFrame",
|
package/node_modules/@deepseek-ai/dsh-api-authorization-controller/lib/typert.remote-client.js
CHANGED
|
@@ -19,6 +19,7 @@ const _deepseek_ai_dsh_api_authorization_controller_authorization_begin_result$s
|
|
|
19
19
|
'type': z.literal("notice").readonly(),
|
|
20
20
|
'message': z.string().readonly(),
|
|
21
21
|
'url': z.string().readonly().optional(),
|
|
22
|
+
'openInBrowser': z.literal(true).readonly().optional(),
|
|
22
23
|
'code': z.string().readonly().optional(),
|
|
23
24
|
}), z.object({
|
|
24
25
|
'type': z.literal("prompt").readonly(),
|
|
@@ -315,6 +315,7 @@ let AuthorizationController = (() => {
|
|
|
315
315
|
type: 'notice',
|
|
316
316
|
message: notice.message,
|
|
317
317
|
...notice.url === undefined ? {} : { url: notice.url },
|
|
318
|
+
...notice.openInBrowser === undefined ? {} : { openInBrowser: notice.openInBrowser },
|
|
318
319
|
...notice.code === undefined ? {} : { code: notice.code },
|
|
319
320
|
});
|
|
320
321
|
},
|
|
@@ -4540,6 +4540,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
4540
4540
|
"type": literal("notice").readonly(),
|
|
4541
4541
|
"message": string().readonly(),
|
|
4542
4542
|
"url": string().readonly().optional(),
|
|
4543
|
+
"openInBrowser": literal(true).readonly().optional(),
|
|
4543
4544
|
"code": string().readonly().optional()
|
|
4544
4545
|
}),
|
|
4545
4546
|
object({
|
|
@@ -2,5 +2,5 @@
|
|
|
2
2
|
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
|
3
3
|
# after editing either side, bring the other along and re-record with:
|
|
4
4
|
# pnpm run verify-translation-pairing --write packages/credentials/authorization/README.md
|
|
5
|
-
README.md:
|
|
6
|
-
README.zh.md:
|
|
5
|
+
README.md: 6368d6c0e8abe14b4166250bef82a06ce2ddf425
|
|
6
|
+
README.zh.md: f1b4d92c9eb6519323d95778bd2e4e53249c1e9c
|
|
@@ -107,7 +107,7 @@ One attempt per key at a time. `begin()` validates the key and method, refuses a
|
|
|
107
107
|
|
|
108
108
|
### The interaction vocabulary
|
|
109
109
|
|
|
110
|
-
A notice is one-way and never carries a secret: a message, optionally the page the human must open and the code they must enter there. A prompt is a question the flow cannot answer for itself — `text`, `secret`, or `select` — where `secret` differs from `text` only in presentation. A prompt carries its own signal so a flow that races a typed code against a browser callback can withdraw the losing question while the attempt continues; the request's signal withdraws the whole attempt instead. The vocabulary is deliberately smaller than any one provider's: it describes what a surface must render, so a surface that renders one flow renders all of them.
|
|
110
|
+
A notice is one-way and never carries a secret: a message, optionally the page the human must open and the code they must enter there, and whether a desktop surface should hand that page to the default browser (`openInBrowser`, set for an authorization destination rather than an informational link; the printed URL always remains the fallback). A prompt is a question the flow cannot answer for itself — `text`, `secret`, or `select` — where `secret` differs from `text` only in presentation. A prompt carries its own signal so a flow that races a typed code against a browser callback can withdraw the losing question while the attempt continues; the request's signal withdraws the whole attempt instead. The vocabulary is deliberately smaller than any one provider's: it describes what a surface must render, so a surface that renders one flow renders all of them.
|
|
111
111
|
|
|
112
112
|
### Commit confirmation
|
|
113
113
|
|
|
@@ -107,7 +107,7 @@ flow 声明它写入的凭据记录、面向用户的标签以及它提供的登
|
|
|
107
107
|
|
|
108
108
|
### 交互词汇
|
|
109
109
|
|
|
110
|
-
notice
|
|
110
|
+
notice 是单向的,且从不携带机密:一条消息,可选的「人需要打开的页面」与「需要在该页面输入的码」,以及桌面界面是否应把该页面交给默认浏览器(`openInBrowser`,仅为授权目标页设置而非信息链接;打印出的 URL 始终保留为后备)。prompt 是 flow 无法自行回答的问题——`text`、`secret` 或 `select`——其中 `secret` 与 `text` 的差别仅在呈现方式。prompt 自带 signal,使得让手输码与浏览器回调赛跑的 flow 可以在尝试继续的同时撤下落败的那个问题;撤销整次尝试则用请求的 signal。这套词汇刻意小于任何单个提供方的词汇:它描述的是界面必须渲染什么,因此能渲染一个 flow 的界面就能渲染全部 flow。
|
|
111
111
|
|
|
112
112
|
### 提交确认
|
|
113
113
|
|
|
@@ -228,6 +228,7 @@ window.__ModuleLoader__.load({
|
|
|
228
228
|
directions: [...attempt.directions, {
|
|
229
229
|
message: frame.message,
|
|
230
230
|
...frame.url === void 0 ? {} : { url: frame.url },
|
|
231
|
+
...frame.openInBrowser === void 0 ? {} : { openInBrowser: frame.openInBrowser },
|
|
231
232
|
...frame.code === void 0 ? {} : { code: frame.code }
|
|
232
233
|
}]
|
|
233
234
|
};
|
|
@@ -100,7 +100,7 @@ window.__ModuleLoader__.load({
|
|
|
100
100
|
const SCROLLBAR_LINGER_MS = 2e3;
|
|
101
101
|
/** Format complete-build metadata for the local brand badge. */
|
|
102
102
|
function localBuildVersion() {
|
|
103
|
-
return `0.1.6-alpha.1-
|
|
103
|
+
return `0.1.6-alpha.1-057f07b-dirty`;
|
|
104
104
|
}
|
|
105
105
|
/** Each panel row subscribes only to its own selection state. */
|
|
106
106
|
function PanelRow({ id, label, wide, usePanelInfo, selectPanel, renderSlot }) {
|
|
@@ -2,5 +2,5 @@
|
|
|
2
2
|
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
|
3
3
|
# after editing either side, bring the other along and re-record with:
|
|
4
4
|
# pnpm run verify-translation-pairing --write packages/llm/llm-pi-ai/README.md
|
|
5
|
-
README.md:
|
|
6
|
-
README.zh.md:
|
|
5
|
+
README.md: e8b9b1e7aea345a70aaeb7667572ead879d7fcb6
|
|
6
|
+
README.zh.md: 3ad4572a7a0ea57f9bffbb4fce3a5136ab7eecc8
|
|
@@ -90,7 +90,7 @@ The generated [configuration catalog](../../../docs/config-catalog.md#deepseek-a
|
|
|
90
90
|
|
|
91
91
|
### Sign in to a provider
|
|
92
92
|
|
|
93
|
-
A provider pi-ai ships a login for can be signed into through the harness authorization seam: the flow offers OAuth or an interactive key prompt (a key is typed into pi-ai's own login prompt, not into the settings form), and the resulting credential is stored in the harness credential store at `llm-pi-ai/<provider id>`. The stored sign-in authenticates its route beneath any `apiKeyEnv` override and refreshes itself under the store's cross-process lock; signing out deletes the stored record. A hand-declared route key outside the record grammar — a lowercase hyphenated identifier — cannot be signed into, because a record write for it refuses with `LlmError('UNSTORABLE_PROVIDER_ID')`; such a route authenticates through `apiKeyEnv` or ambient provider settings instead.
|
|
93
|
+
A provider pi-ai ships a login for can be signed into through the harness authorization seam: the flow offers OAuth or an interactive key prompt (a key is typed into pi-ai's own login prompt, not into the settings form), and the resulting credential is stored in the harness credential store at `llm-pi-ai/<provider id>`. Signing in never activates a route: a dormant catalog provider still needs its profile under `providers.<id>` (an empty `{}` takes the installed defaults), and each surface owns how an authorization page reaches the browser — the Web Models page renders the link, while the terminal hands marked pages to the default browser. The stored sign-in authenticates its route beneath any `apiKeyEnv` override and refreshes itself under the store's cross-process lock; signing out deletes the stored record. A hand-declared route key outside the record grammar — a lowercase hyphenated identifier — cannot be signed into, because a record write for it refuses with `LlmError('UNSTORABLE_PROVIDER_ID')`; such a route authenticates through `apiKeyEnv` or ambient provider settings instead.
|
|
94
94
|
|
|
95
95
|
### Resolve the model catalog
|
|
96
96
|
|
|
@@ -90,7 +90,7 @@ kind: "package-reference"
|
|
|
90
90
|
|
|
91
91
|
### 登录提供方
|
|
92
92
|
|
|
93
|
-
pi-ai 提供登录的提供方可以通过 harness 授权 seam 登录:流程提供 OAuth 或交互式密钥提示(密钥键入 pi-ai 自己的登录提示,而非设置表单),得到的凭据存储在 harness 凭据存储的 `llm-pi-ai/<provider id>`
|
|
93
|
+
pi-ai 提供登录的提供方可以通过 harness 授权 seam 登录:流程提供 OAuth 或交互式密钥提示(密钥键入 pi-ai 自己的登录提示,而非设置表单),得到的凭据存储在 harness 凭据存储的 `llm-pi-ai/<provider id>` 记录中。登录不会激活路由:休眠的 catalog 提供方仍需要 `providers.<id>` 下的 profile(空的 `{}` 取已安装默认值),且各界面自行决定授权页面如何到达浏览器——Web 的 Models 页面渲染链接,终端则把被标记的页面交给默认浏览器。存储的登录在其路由的 `apiKeyEnv` 覆盖之下完成认证,并在存储的跨进程锁下自行刷新;退出登录即删除存储记录。落在记录文法之外——小写连字符标识符——的手工声明路由键无法登录,因为对它的记录写入会以 `LlmError('UNSTORABLE_PROVIDER_ID')` 拒绝;这类路由改用 `apiKeyEnv` 或提供方 ambient 设置认证。
|
|
94
94
|
|
|
95
95
|
### 解析模型目录
|
|
96
96
|
|
|
@@ -2379,13 +2379,15 @@ function relay(event, session) {
|
|
|
2379
2379
|
case "auth_url":
|
|
2380
2380
|
session.notify({
|
|
2381
2381
|
message: event.instructions ?? "Open this page to continue signing in.",
|
|
2382
|
-
url: event.url
|
|
2382
|
+
url: event.url,
|
|
2383
|
+
openInBrowser: true
|
|
2383
2384
|
});
|
|
2384
2385
|
return;
|
|
2385
2386
|
case "device_code":
|
|
2386
2387
|
session.notify({
|
|
2387
2388
|
message: "Enter this code on the verification page to finish signing in.",
|
|
2388
2389
|
url: event.verificationUri,
|
|
2390
|
+
openInBrowser: true,
|
|
2389
2391
|
code: event.userCode
|
|
2390
2392
|
});
|
|
2391
2393
|
return;
|
|
@@ -2,5 +2,5 @@
|
|
|
2
2
|
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
|
3
3
|
# after editing either side, bring the other along and re-record with:
|
|
4
4
|
# pnpm run verify-translation-pairing --write packages/util/native-command/README.md
|
|
5
|
-
README.md:
|
|
6
|
-
README.zh.md:
|
|
5
|
+
README.md: 9dae51fc0c605e9c0ac6d04a0c768897ce04fc9f
|
|
6
|
+
README.zh.md: 92a1885e51dc12c51ef3a6d127bf0e7d3dc3391c
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
-
description: "Host-native command
|
|
2
|
+
description: "Host-native command, path-opening, and default-browser URL handoff utilities with shell-free execution, cancellation, desktop detection, and credential scrubbing."
|
|
3
3
|
kind: "package-library"
|
|
4
4
|
---
|
|
5
5
|
|
|
@@ -9,7 +9,7 @@ English | [中文](README.zh.md)
|
|
|
9
9
|
|
|
10
10
|
## Summary
|
|
11
11
|
|
|
12
|
-
`dsh-native-command` runs host executables without a shell
|
|
12
|
+
`dsh-native-command` runs host executables without a shell, opens Host filesystem paths through the desktop, and hands HTTP(S) URLs to the default browser. The command runner captures utf8 output, propagates cancellation, and hides transient Windows consoles. The path opener supports default-application and text-editor intents, browser-renderable documents, WSL translation, and desktop availability checks. The URL opener keeps Harness credentials out of the browser's environment. It is a library, not a plugin: no `ctx`, no state, no events.
|
|
13
13
|
|
|
14
14
|
## Table of Contents
|
|
15
15
|
|
|
@@ -49,6 +49,10 @@ The `NativeCommandRunner` type is the injectable command boundary for host integ
|
|
|
49
49
|
|
|
50
50
|
`revealNativePath(path, signal)` selects the file in Finder or Explorer, including WSL path translation, and opens its parent directory through `xdg-open` on desktop Linux. `nativeFileManager()` identifies that action for Host-derived UI labels; desktop availability remains a separate `canOpenNativePath()` check. Callers must authorize the absolute file path before invoking either operation. Platform dispatch is covered by injected-runner tests; native desktop verification belongs to the corresponding platform. Explorer receives an encoded file URI as a separate argument. Its exit code 1 is accepted as a delegated handoff; cancellation, missing executables, and other exit codes still reject. This acknowledgement does not prove that a desktop window selected the file.
|
|
51
51
|
|
|
52
|
+
### Opening a URL in the default browser
|
|
53
|
+
|
|
54
|
+
`openNativeUrl(url)` hands one HTTP(S) URL to the operating system's default browser and rejects every other protocol before spawning. The URL travels through a short-lived Node helper running the maintained `open` package with a scrubbed environment, so Harness credentials such as API keys and the Harness home never reach a newly launched browser process; on Windows the helper stays alive until PowerShell has accepted the handoff. The returned promise settles when the operating-system launcher exits, not when the browser window closes; a launch failure rejects with the helper's first stderr line, so the caller can fall back to showing the URL. This is the URL counterpart of the filesystem path opener — do not pass a URL to `openNativePath`, which treats its argument as a filesystem path.
|
|
55
|
+
|
|
52
56
|
-----
|
|
53
57
|
|
|
54
58
|
<a id="understand-the-implementation"></a>
|
|
@@ -66,6 +70,7 @@ The command runner is a thin wrapper over Node's `execFile`. The path opener sel
|
|
|
66
70
|
| [`src/index.ts`](src/index.ts) | Public command-runner and path-opener exports |
|
|
67
71
|
| [`src/runner.ts`](src/runner.ts) | Shell-free `execFile` adapter |
|
|
68
72
|
| [`src/path-opener.ts`](src/path-opener.ts) | Desktop detection, open intents, browser preference, and WSL translation |
|
|
73
|
+
| [`src/url-opener.ts`](src/url-opener.ts) | Credential-scrubbed default-browser handoff for HTTP(S) URLs |
|
|
69
74
|
| — | No runtime invariant companion is published; each run is one stateless child-process round trip with no owned event stream or mutable runtime data; behavior is enforced by unit tests. |
|
|
70
75
|
|
|
71
76
|
### What execFile gives the runner
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
-
description: "
|
|
2
|
+
description: "宿主原生命令、路径打开与默认浏览器 URL 交接工具,提供无 shell 执行、取消、桌面探测与凭据擦除。"
|
|
3
3
|
kind: "package-library"
|
|
4
4
|
---
|
|
5
5
|
|
|
@@ -9,7 +9,7 @@ kind: "package-library"
|
|
|
9
9
|
|
|
10
10
|
## 概述
|
|
11
11
|
|
|
12
|
-
`dsh-native-command` 无需 shell 即可运行 Host
|
|
12
|
+
`dsh-native-command` 无需 shell 即可运行 Host 可执行文件、通过桌面打开 Host 文件系统路径,并把 HTTP(S) URL 交给默认浏览器。命令运行器捕获 utf8 输出、传播取消,并隐藏 Windows 瞬时控制台。路径打开器支持默认应用与文本编辑器意图、浏览器可渲染文档、WSL 转换与桌面可用性检查。URL 打开器让 Harness 凭据不进入浏览器的环境。它是库而非插件:没有 `ctx`、无状态、不发事件。
|
|
13
13
|
|
|
14
14
|
## 目录
|
|
15
15
|
|
|
@@ -49,6 +49,10 @@ const { stdout, stderr } = await runNativeCommand('osascript', ['-e', script], s
|
|
|
49
49
|
|
|
50
50
|
`revealNativePath(path, signal)` 在 Finder 或文件资源管理器中选中文件,包含 WSL 路径转换;在桌面 Linux 上通过 `xdg-open` 打开上层目录。`nativeFileManager()` 标识该操作,供 UI 根据 Host 选择文案;桌面是否可用仍由独立的 `canOpenNativePath()` 检查决定。调用方必须先授权绝对文件路径,再执行操作。平台分派由注入运行器的测试覆盖;原生桌面验证由对应平台负责。 Explorer 接收独立参数中的编码文件 URI。退出码 1 按已转交请求处理;取消、找不到可执行文件和其他退出码仍然报错。该确认不能证明桌面窗口已选中文件。
|
|
51
51
|
|
|
52
|
+
### 在默认浏览器中打开 URL
|
|
53
|
+
|
|
54
|
+
`openNativeUrl(url)` 把一个 HTTP(S) URL 交给操作系统的默认浏览器,并在启动任何进程前拒绝其他协议。URL 经由一个短生命周期的 Node 辅助进程传递,该进程运行受维护的 `open` 包且使用擦除后的环境,因此 API 密钥与 Harness 主目录等 Harness 凭据不会进入新启动的浏览器进程;在 Windows 上辅助进程会存活到 PowerShell 接受交接为止。返回的 promise 在操作系统启动器退出时结算,而不是在浏览器窗口关闭时;启动失败会以辅助进程 stderr 的首行拒绝,调用方因此可以回退到展示 URL。这是与文件系统路径打开器对应的 URL 能力——不要把 URL 传给 `openNativePath`,后者把参数当作文件系统路径处理。
|
|
55
|
+
|
|
52
56
|
-----
|
|
53
57
|
|
|
54
58
|
<a id="understand-the-implementation"></a>
|
|
@@ -66,6 +70,7 @@ const { stdout, stderr } = await runNativeCommand('osascript', ['-e', script], s
|
|
|
66
70
|
| [`src/index.ts`](src/index.ts) | 命令运行器与路径打开器的公共导出 |
|
|
67
71
|
| [`src/runner.ts`](src/runner.ts) | 无 shell 的 `execFile` 适配器 |
|
|
68
72
|
| [`src/path-opener.ts`](src/path-opener.ts) | 桌面探测、打开意图、浏览器偏好与 WSL 转换 |
|
|
73
|
+
| [`src/url-opener.ts`](src/url-opener.ts) | 面向 HTTP(S) URL 的凭据擦除默认浏览器交接 |
|
|
69
74
|
| — | 不发布运行时不变式伴生入口;每次运行都是一次无状态的子进程往返,不拥有事件流或可变运行时数据;相关行为由单元测试保障。 |
|
|
70
75
|
|
|
71
76
|
### execFile 给了运行器什么
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { execFile } from "node:child_process";
|
|
1
|
+
import { execFile, spawn } from "node:child_process";
|
|
2
|
+
import { scrubbedParentEnv } from "@deepseek-ai/dsh-subprocess";
|
|
2
3
|
import { release } from "node:os";
|
|
3
4
|
import { dirname, extname } from "node:path";
|
|
4
5
|
import { pathToFileURL } from "node:url";
|
|
@@ -35,6 +36,96 @@ const runNativeCommand = (command, args, signal) => new Promise((resolve, reject
|
|
|
35
36
|
});
|
|
36
37
|
});
|
|
37
38
|
//#endregion
|
|
39
|
+
//#region lib/types/url-opener.js
|
|
40
|
+
/**
|
|
41
|
+
* Default-browser handoff for HTTP(S) URLs through a credential-scrubbed helper.
|
|
42
|
+
* @module @deepseek-ai/dsh-native-command/url-opener
|
|
43
|
+
*/
|
|
44
|
+
const BROWSER_OPENER_MODULE = import.meta.resolve("open");
|
|
45
|
+
const BROWSER_OPENER_PROGRAM = `
|
|
46
|
+
try {
|
|
47
|
+
const { default: open } = await import(${JSON.stringify(BROWSER_OPENER_MODULE)})
|
|
48
|
+
const launcher = await open(process.argv[1])
|
|
49
|
+
if (process.platform === 'win32') {
|
|
50
|
+
// open resolves at PowerShell spawn; keep it referenced until that launcher hands the URL to Windows.
|
|
51
|
+
const code = launcher.exitCode ?? await new Promise((resolve, reject) => {
|
|
52
|
+
function onError(error) {
|
|
53
|
+
launcher.off('close', onClose)
|
|
54
|
+
reject(error)
|
|
55
|
+
}
|
|
56
|
+
function onClose(code) {
|
|
57
|
+
launcher.off('error', onError)
|
|
58
|
+
resolve(code)
|
|
59
|
+
}
|
|
60
|
+
launcher.ref()
|
|
61
|
+
launcher.once('error', onError)
|
|
62
|
+
launcher.once('close', onClose)
|
|
63
|
+
})
|
|
64
|
+
if (code !== 0) throw new Error('browser operating-system launcher exited with code ' + String(code))
|
|
65
|
+
}
|
|
66
|
+
process.exitCode = 0
|
|
67
|
+
} catch (error) {
|
|
68
|
+
console.error(error)
|
|
69
|
+
process.exitCode = 1
|
|
70
|
+
}
|
|
71
|
+
`;
|
|
72
|
+
/** Start the maintained platform opener without forwarding Harness credentials. */
|
|
73
|
+
function spawnBrowserLauncher(url) {
|
|
74
|
+
return spawn(process.execPath, [
|
|
75
|
+
"--input-type=module",
|
|
76
|
+
"--eval",
|
|
77
|
+
BROWSER_OPENER_PROGRAM,
|
|
78
|
+
"--",
|
|
79
|
+
url
|
|
80
|
+
], {
|
|
81
|
+
env: scrubbedParentEnv(),
|
|
82
|
+
stdio: [
|
|
83
|
+
"ignore",
|
|
84
|
+
"inherit",
|
|
85
|
+
"pipe"
|
|
86
|
+
]
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Hand an HTTP(S) URL to the operating system's default browser.
|
|
91
|
+
*
|
|
92
|
+
* The promise settles after the short-lived operating-system launcher exits,
|
|
93
|
+
* not after the browser window closes. Browser-launch failures reject with the
|
|
94
|
+
* helper's first stderr line when available. The helper receives a scrubbed
|
|
95
|
+
* environment so Harness credentials do not reach the browser process.
|
|
96
|
+
* @param url - absolute HTTP(S) URL to open.
|
|
97
|
+
* @returns once the operating-system launcher has accepted the URL.
|
|
98
|
+
*/
|
|
99
|
+
async function openNativeUrl(url) {
|
|
100
|
+
const protocol = new URL(url).protocol;
|
|
101
|
+
if (protocol !== "http:" && protocol !== "https:") throw new Error(`native URL opener supports only HTTP(S), not ${protocol}`);
|
|
102
|
+
const launcher = spawnBrowserLauncher(url);
|
|
103
|
+
let launcherStderr = "";
|
|
104
|
+
launcher.stderr?.setEncoding("utf8");
|
|
105
|
+
launcher.stderr?.on("data", (chunk) => {
|
|
106
|
+
launcherStderr += chunk;
|
|
107
|
+
});
|
|
108
|
+
await new Promise((resolve, reject) => {
|
|
109
|
+
function onError(error) {
|
|
110
|
+
launcher.off("close", onClose);
|
|
111
|
+
reject(error);
|
|
112
|
+
}
|
|
113
|
+
function onClose(code) {
|
|
114
|
+
launcher.off("error", onError);
|
|
115
|
+
if (code !== 0) {
|
|
116
|
+
const firstLine = launcherStderr.trim().split(/\r?\n/u)[0];
|
|
117
|
+
const reason = firstLine === void 0 || firstLine === "" ? `browser launcher exited with code ${String(code)}` : firstLine.replace(/^(?:[A-Za-z]*Error):\s*/u, "");
|
|
118
|
+
reject(new Error(reason));
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
if (launcherStderr !== "") process.stderr.write(launcherStderr);
|
|
122
|
+
resolve();
|
|
123
|
+
}
|
|
124
|
+
launcher.once("error", onError);
|
|
125
|
+
launcher.once("close", onClose);
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
//#endregion
|
|
38
129
|
//#region lib/types/path-opener.js
|
|
39
130
|
/**
|
|
40
131
|
* Cross-platform native path and text-document openers for Host UI
|
|
@@ -241,4 +332,4 @@ async function revealNativePath(path, signal, internals = {}) {
|
|
|
241
332
|
throw new Error(`native file manager is unsupported on ${platform}`);
|
|
242
333
|
}
|
|
243
334
|
//#endregion
|
|
244
|
-
export { canOpenNativePath, nativeFileManager, openNativePath, openNativeTextFile, revealNativePath, runNativeCommand };
|
|
335
|
+
export { canOpenNativePath, nativeFileManager, openNativePath, openNativeTextFile, openNativeUrl, revealNativePath, runNativeCommand };
|
|
@@ -6320,11 +6320,11 @@ const TYPE_API = [
|
|
|
6320
6320
|
},
|
|
6321
6321
|
{
|
|
6322
6322
|
name: "AuthorizationNotice",
|
|
6323
|
-
declaration: "export interface AuthorizationNotice {\n message: string;\n url?: string;\n code?: string;\n}"
|
|
6323
|
+
declaration: "export interface AuthorizationNotice {\n message: string;\n url?: string;\n openInBrowser?: true;\n code?: string;\n}"
|
|
6324
6324
|
},
|
|
6325
6325
|
{
|
|
6326
6326
|
name: "AuthorizationNoticeFrame",
|
|
6327
|
-
declaration: "export interface AuthorizationNoticeFrame {\n readonly type: 'notice';\n readonly message: string;\n readonly url?: string;\n readonly code?: string;\n}"
|
|
6327
|
+
declaration: "export interface AuthorizationNoticeFrame {\n readonly type: 'notice';\n readonly message: string;\n readonly url?: string;\n readonly openInBrowser?: true;\n readonly code?: string;\n}"
|
|
6328
6328
|
},
|
|
6329
6329
|
{
|
|
6330
6330
|
name: "AuthorizationOutcome",
|
|
@@ -2,5 +2,5 @@
|
|
|
2
2
|
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
|
3
3
|
# after editing either side, bring the other along and re-record with:
|
|
4
4
|
# pnpm run verify-translation-pairing --write packages/bundle/tui-app/README.md
|
|
5
|
-
README.md:
|
|
6
|
-
README.zh.md:
|
|
5
|
+
README.md: 1176042ca87a4a439e65818c9333b9004ea3519e
|
|
6
|
+
README.zh.md: a2d0e296e2e1e5aaf7e3568727fd57e30c2e538b
|
|
@@ -9,7 +9,7 @@ English | [中文](README.zh.md)
|
|
|
9
9
|
|
|
10
10
|
## Summary
|
|
11
11
|
|
|
12
|
-
`dsh-tui-app` is the terminal surface of dsh: `dsh tui` starts a multi-turn session in the terminal you are already in, with no browser and no server. Replies stream as you watch, tool calls become foldable cards, approvals and `ask_user_question` questions appear above the input, `@` completes paths and sessions, `/attach` adds images and files, and `/`-commands share the Web registry. Sessions persist: `/sessions`, `/new`, and `/fork` switch between them, `/export` writes the browser's ZIP, and `--resume` continues one later. It runs the same model, tools, and safety defaults as `dsh web`, one session at a time.
|
|
12
|
+
`dsh-tui-app` is the terminal surface of dsh: `dsh tui` starts a multi-turn session in the terminal you are already in, with no browser-hosted application and no server. Replies stream as you watch, tool calls become foldable cards, approvals and `ask_user_question` questions appear above the input, `@` completes paths and sessions, `/attach` adds images and files, and `/`-commands share the Web registry. Sessions persist: `/sessions`, `/new`, and `/fork` switch between them, `/export` writes the browser's ZIP, and `--resume` continues one later. It runs the same model, tools, and safety defaults as `dsh web`, one session at a time.
|
|
13
13
|
|
|
14
14
|
## Table of Contents
|
|
15
15
|
|
|
@@ -33,6 +33,7 @@ Start a session, type, and read the answer in place. `dsh tui` is an alias of `d
|
|
|
33
33
|
dsh tui # new session, wait for input
|
|
34
34
|
dsh tui "explain this repository" # new session with a first prompt
|
|
35
35
|
dsh tui --resume <session-id> # continue an earlier session
|
|
36
|
+
dsh tui --no-open # print sign-in URLs without opening a browser
|
|
36
37
|
```
|
|
37
38
|
|
|
38
39
|
On quit the app prints `dsh: session <id> saved; resume with: dsh --profile tui --resume <id>` on stderr for the session bound at that moment. A resumed session redraws its persisted history before accepting input; inside the terminal, `/sessions` opens a picker over every persisted root session, `/new` starts a fresh one, and `/fork` copies the current session up to its last completed turn into a new one, the same cut the browser's fork takes. Switching releases the previous Agent and redraws the transcript of the next.
|
|
@@ -82,6 +83,10 @@ Typing `/` at the start of the editor completes the terminal's own commands and
|
|
|
82
83
|
|
|
83
84
|
Every other `/name` line goes to the shared command registry, so `/compact`, `/permission`, `/goal`, and plugin commands work as they do in the browser.
|
|
84
85
|
|
|
86
|
+
### Subscription sign-in
|
|
87
|
+
|
|
88
|
+
`/login` stores a subscription credential but does not activate a dormant model route. Configure the catalog route first, then use its full credential key; for example, run `/settings llm-pi-ai providers.openai-codex {}` and then `/login llm-pi-ai/openai-codex`. A marked authorization page opens in the local default browser while its URL remains in the transcript as a fallback. SSH launches, hosts without a desktop, `--no-open`, and opener failures leave the manual URL and device-code paths available instead.
|
|
89
|
+
|
|
85
90
|
### Prompts from the agent
|
|
86
91
|
|
|
87
92
|
An approval request draws `Allow <tool>?` with the asker's reason, the logged call the request names (the same rows as its tool card, so a shell command reads before it runs), and two rows, allow once or reject; `Esc` rejects and `Ctrl+C` cancels the request. An `ask_user_question` question renders its `detail` as Markdown above its options plus a free-text row; multi-select toggles rows with `Space` and confirms through `Done`. A plan review (the `plan-review` intent `exit_plan_mode` sets) draws the plan as Markdown with Approve, Decline, and Discuss rows, where Discuss returns the request to the composer as the browser's card does. Prompts queue and show one at a time, and an aborted request withdraws its prompt.
|
|
@@ -97,8 +102,9 @@ An approval request draws `Allow <tool>?` with the asker's reason, the logged ca
|
|
|
97
102
|
| `prompt` | none | A first prompt submitted when the terminal is up |
|
|
98
103
|
| `resume` | none | A persisted session id to continue instead of starting a new one |
|
|
99
104
|
| `toolPreviewLines` | `8` | Collapsed tool-card body rows before `Ctrl+O` expands them |
|
|
105
|
+
| `openBrowser` | `true` | Hand marked authorization pages to the local default browser |
|
|
100
106
|
|
|
101
|
-
`prompt` and `
|
|
107
|
+
`prompt`, `resume`, and `openBrowser` come from the command line through the startup provider; the generated [configuration catalog](../../../docs/config-catalog.md#deepseek-aidsh-tui-app) is the exhaustive source for every accepted field.
|
|
102
108
|
|
|
103
109
|
-----
|
|
104
110
|
|
|
@@ -112,7 +118,7 @@ The runner is a direct driver over the core API carrier, like `dsh-headless`, th
|
|
|
112
118
|
|
|
113
119
|
### Run flow
|
|
114
120
|
|
|
115
|
-
The runner awaits the complete application (`ctx.get('loader')?.await()`) and builds a session host over the core registry with three operations: `create` makes one fresh persisted Agent with the shared [`agentDefaultModel`](../../core/agent-default-model/README.md) selection, `resume` reads the persisted log in pages through a read handle of `ctx.sessionPersistence` and resumes the Agent through the registry, and `fork` observes the source through `ctx.sessionQuery`, cuts after the chosen (by default the last) `turn/end` up to the next `turn/start`, and creates a seeded Agent with `parentSession` and `isSeeded` metadata. Every operation installs a `ModelSelectionRef` in the Agent's scoped setup so `/model` changes the next request. The terminal application starts on the session `--resume` or a fresh `create` yields, subscribes to `session/event`, `agent/assistant-stream`, and `agent/status`, answers the `approval/request` and `user-questions/request` waterfalls for the bound Agent only, and switches sessions by binding the next one and disposing the previous handle; while the host opens the next session the editor refuses input, and a quit during that wait releases the session that arrives afterwards. Quitting cancels any running turn, waits for quiescence, flushes the bound Session, disposes its handle, and requests exit 0; a driver failure writes `dsh: <message>` to stderr and requests exit 1. Shift+Tab cycles the bound model's adapter-owned reasoning efforts, wrapping through the provider default, and `/login` starts `authorization.begin` with only subscription methods (every method except a key-collecting `api-key` login).
|
|
121
|
+
The runner awaits the complete application (`ctx.get('loader')?.await()`) and builds a session host over the core registry with three operations: `create` makes one fresh persisted Agent with the shared [`agentDefaultModel`](../../core/agent-default-model/README.md) selection, `resume` reads the persisted log in pages through a read handle of `ctx.sessionPersistence` and resumes the Agent through the registry, and `fork` observes the source through `ctx.sessionQuery`, cuts after the chosen (by default the last) `turn/end` up to the next `turn/start`, and creates a seeded Agent with `parentSession` and `isSeeded` metadata. Every operation installs a `ModelSelectionRef` in the Agent's scoped setup so `/model` changes the next request. The terminal application starts on the session `--resume` or a fresh `create` yields, subscribes to `session/event`, `agent/assistant-stream`, and `agent/status`, answers the `approval/request` and `user-questions/request` waterfalls for the bound Agent only, and switches sessions by binding the next one and disposing the previous handle; while the host opens the next session the editor refuses input, and a quit during that wait releases the session that arrives afterwards. Quitting cancels any running turn, waits for quiescence, flushes the bound Session, disposes its handle, and requests exit 0; a driver failure writes `dsh: <message>` to stderr and requests exit 1. Shift+Tab cycles the bound model's adapter-owned reasoning efforts, wrapping through the provider default, and `/login` starts `authorization.begin` with only subscription methods (every method except a key-collecting `api-key` login). A notice the flow marks with `openInBrowser` is handed to the default browser through `dsh-native-command`'s credential-scrubbed helper while the URL stays printed; the handoff is suppressed when `openBrowser` is false, the launch came through SSH, or the host has no desktop, and an opener failure becomes a notice beside the URL rather than a sign-in failure.
|
|
116
122
|
|
|
117
123
|
### Rendering model
|
|
118
124
|
|
|
@@ -127,7 +133,7 @@ The patch rides over `dsh-base`: it sets the coding persona prefix and cwd suffi
|
|
|
127
133
|
| File | Role |
|
|
128
134
|
|---|---|
|
|
129
135
|
| [`src/index.ts`](src/index.ts) | The `tui-app` plugin: the session host (create, resume, fork), history read, quit flow, exit mapping |
|
|
130
|
-
| [`src/startup.ts`](src/startup.ts) | The `tui-app-startup` provider: prompt positional, `--resume`, and `--help` |
|
|
136
|
+
| [`src/startup.ts`](src/startup.ts) | The `tui-app-startup` provider: prompt positional, `--resume`, `--no-open`, and `--help` |
|
|
131
137
|
| [`src/app.ts`](src/app.ts) | The terminal application: layout, keys, commands, session binding, seams, log and stream folding |
|
|
132
138
|
| [`src/sessions.ts`](src/sessions.ts) | The `/sessions` list over the query engine and its picker rows |
|
|
133
139
|
| [`src/attach.ts`](src/attach.ts) | `/attach`: local files into image or file blocks through the attachment store |
|
|
@@ -9,7 +9,7 @@ kind: "package-bundle"
|
|
|
9
9
|
|
|
10
10
|
## 概述
|
|
11
11
|
|
|
12
|
-
`dsh-tui-app` 是 dsh 的终端表层:`dsh tui`
|
|
12
|
+
`dsh-tui-app` 是 dsh 的终端表层:`dsh tui` 在你当前所在的终端里启动一个多轮会话,没有浏览器托管的应用、也没有服务器。回复实时流式显示,工具调用变成可折叠的卡片,审批与 `ask_user_question` 的问题出现在输入框上方,`@` 补全路径与会话,`/attach` 加入图片与文件,`/` 命令与 Web 共用注册表。会话持久化:`/sessions`、`/new` 与 `/fork` 在会话间切换,`/export` 写出浏览器的 ZIP,`--resume` 稍后继续。它运行与 `dsh web` 相同的模型、工具与安全默认值,同一时间一个会话。
|
|
13
13
|
|
|
14
14
|
## 目录
|
|
15
15
|
|
|
@@ -33,6 +33,7 @@ kind: "package-bundle"
|
|
|
33
33
|
dsh tui # new session, wait for input
|
|
34
34
|
dsh tui "explain this repository" # new session with a first prompt
|
|
35
35
|
dsh tui --resume <session-id> # continue an earlier session
|
|
36
|
+
dsh tui --no-open # print sign-in URLs without opening a browser
|
|
36
37
|
```
|
|
37
38
|
|
|
38
39
|
退出时应用在 stderr 为当时绑定的会话打印 `dsh: session <id> saved; resume with: dsh --profile tui --resume <id>`。恢复的会话会在接受输入前先重绘其持久化历史;在终端内,`/sessions` 打开覆盖所有持久化根会话的选择器,`/new` 开始一个新会话,`/fork` 把当前会话复制到其最后一个完成轮次并作为新会话,切割点与浏览器的 fork 相同。切换会释放先前的 Agent 并重绘下一个会话的对话记录。
|
|
@@ -82,6 +83,10 @@ dsh tui --resume <session-id> # continue an earlier session
|
|
|
82
83
|
|
|
83
84
|
其他每条 `/name` 行都交给共享命令注册表,因此 `/compact`、`/permission`、`/goal` 与插件命令的行为和浏览器中一致。
|
|
84
85
|
|
|
86
|
+
### 订阅登录
|
|
87
|
+
|
|
88
|
+
`/login` 只存储订阅凭据,不会激活休眠的模型路由。先配置 catalog 路由,再使用完整的凭据键;例如先运行 `/settings llm-pi-ai providers.openai-codex {}`,再运行 `/login llm-pi-ai/openai-codex`。被标记的授权页面会在本地默认浏览器中打开,其 URL 同时保留在对话记录中作为后备。SSH 启动、无桌面的宿主、`--no-open` 以及打开器失败时,手动 URL 与设备码路径仍然可用。
|
|
89
|
+
|
|
85
90
|
### 来自 agent 的提示
|
|
86
91
|
|
|
87
92
|
审批请求绘制 `Allow <tool>?`、请求方的理由、请求所指的已记录调用(与其工具卡片相同的行,因此 shell 命令在运行前可读)以及两行选项:允许一次或拒绝;`Esc` 拒绝,`Ctrl+C` 取消该请求。`ask_user_question` 的问题把其 `detail` 渲染为 Markdown,置于选项与一行自由文本之上;多选用 `Space` 切换各行并通过 `Done` 确认。计划评审(`exit_plan_mode` 设置的 `plan-review` 意图)把计划绘制为 Markdown,并提供 Approve、Decline 与 Discuss 行,其中 Discuss 像浏览器卡片一样把请求交回编辑器。提示排队、一次只显示一个,被中止的请求会撤回其提示。
|
|
@@ -97,8 +102,9 @@ dsh tui --resume <session-id> # continue an earlier session
|
|
|
97
102
|
| `prompt` | 无 | 终端就绪后提交的首个提示 |
|
|
98
103
|
| `resume` | 无 | 要继续的持久化会话 id,而不是新建会话 |
|
|
99
104
|
| `toolPreviewLines` | `8` | `Ctrl+O` 展开前折叠的工具卡片正文行数 |
|
|
105
|
+
| `openBrowser` | `true` | 把被标记的授权页面交给本地默认浏览器 |
|
|
100
106
|
|
|
101
|
-
`prompt` 与 `
|
|
107
|
+
`prompt`、`resume` 与 `openBrowser` 经启动提供方来自命令行;生成的[配置目录](../../../docs/config-catalog.zh.md#deepseek-aidsh-tui-app)是所有可接受字段的完整来源。
|
|
102
108
|
|
|
103
109
|
-----
|
|
104
110
|
|
|
@@ -112,7 +118,7 @@ runner 与 `dsh-headless` 一样是核心 API 载体之上的直接驱动器,
|
|
|
112
118
|
|
|
113
119
|
### 运行流程
|
|
114
120
|
|
|
115
|
-
runner 等待完整应用就绪(`ctx.get('loader')?.await()`),并在核心注册表之上构建含三个操作的会话宿主:`create` 用共享的 [`agentDefaultModel`](../../core/agent-default-model/README.zh.md) 选择创建一个全新的持久化 Agent,`resume` 通过 `ctx.sessionPersistence` 的只读句柄分页读取持久化日志并经注册表恢复 Agent,`fork` 通过 `ctx.sessionQuery` 观察源会话、在所选(默认最后一个)`turn/end` 之后直到下一个 `turn/start` 处切割,并创建带 `parentSession` 与 `isSeeded` 元数据的种子 Agent。每个操作都在 Agent 的作用域 setup 中安装 `ModelSelectionRef`,因此 `/model` 会改变下一次请求。终端应用从 `--resume` 或一次新的 `create` 产生的会话开始,订阅 `session/event`、`agent/assistant-stream` 与 `agent/status`,只为绑定的 Agent 应答 `approval/request` 与 `user-questions/request` waterfall,并通过绑定下一个会话、dispose 先前句柄来切换会话;宿主打开下一个会话期间编辑器拒绝输入,等待期间退出会释放随后到达的会话。退出时取消任何进行中的轮次、等待完全停稳、flush 绑定的会话、dispose 其句柄并请求以 0 退出;驱动器失败会向 stderr 写入 `dsh: <message>` 并请求以 1 退出。Shift+Tab 循环切换绑定模型的适配器自有推理强度,并在提供方默认值处回绕;`/login` 只带着订阅方法(除收集密钥的 `api-key` 登录外的每一种方法)启动 `authorization.begin`。
|
|
121
|
+
runner 等待完整应用就绪(`ctx.get('loader')?.await()`),并在核心注册表之上构建含三个操作的会话宿主:`create` 用共享的 [`agentDefaultModel`](../../core/agent-default-model/README.zh.md) 选择创建一个全新的持久化 Agent,`resume` 通过 `ctx.sessionPersistence` 的只读句柄分页读取持久化日志并经注册表恢复 Agent,`fork` 通过 `ctx.sessionQuery` 观察源会话、在所选(默认最后一个)`turn/end` 之后直到下一个 `turn/start` 处切割,并创建带 `parentSession` 与 `isSeeded` 元数据的种子 Agent。每个操作都在 Agent 的作用域 setup 中安装 `ModelSelectionRef`,因此 `/model` 会改变下一次请求。终端应用从 `--resume` 或一次新的 `create` 产生的会话开始,订阅 `session/event`、`agent/assistant-stream` 与 `agent/status`,只为绑定的 Agent 应答 `approval/request` 与 `user-questions/request` waterfall,并通过绑定下一个会话、dispose 先前句柄来切换会话;宿主打开下一个会话期间编辑器拒绝输入,等待期间退出会释放随后到达的会话。退出时取消任何进行中的轮次、等待完全停稳、flush 绑定的会话、dispose 其句柄并请求以 0 退出;驱动器失败会向 stderr 写入 `dsh: <message>` 并请求以 1 退出。Shift+Tab 循环切换绑定模型的适配器自有推理强度,并在提供方默认值处回绕;`/login` 只带着订阅方法(除收集密钥的 `api-key` 登录外的每一种方法)启动 `authorization.begin`。flow 用 `openInBrowser` 标记的 notice 会经 `dsh-native-command` 的凭据擦除辅助进程交给默认浏览器,URL 同时保持打印;当 `openBrowser` 为 false、启动经过 SSH 或宿主没有桌面时抑制该交接,打开器失败则成为 URL 旁的一条通知,而非登录失败。
|
|
116
122
|
|
|
117
123
|
### 渲染模型
|
|
118
124
|
|
|
@@ -127,7 +133,7 @@ runner 等待完整应用就绪(`ctx.get('loader')?.await()`),并在核心
|
|
|
127
133
|
| 文件 | 职责 |
|
|
128
134
|
|---|---|
|
|
129
135
|
| [`src/index.ts`](src/index.ts) | `tui-app` 插件:会话宿主(创建、恢复、fork)、历史读取、退出流程、退出码映射 |
|
|
130
|
-
| [`src/startup.ts`](src/startup.ts) | `tui-app-startup` 提供方:提示位置参数、`--resume` 与 `--help` |
|
|
136
|
+
| [`src/startup.ts`](src/startup.ts) | `tui-app-startup` 提供方:提示位置参数、`--resume`、`--no-open` 与 `--help` |
|
|
131
137
|
| [`src/app.ts`](src/app.ts) | 终端应用:布局、按键、命令、会话绑定、接缝、日志与流的折叠 |
|
|
132
138
|
| [`src/sessions.ts`](src/sessions.ts) | 基于查询引擎的 `/sessions` 列表及其选择器行 |
|
|
133
139
|
| [`src/attach.ts`](src/attach.ts) | `/attach`:本地文件经附件存储成为图片或文件块 |
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# The dsh-tui-app bundle patch: the interactive terminal surface directly over
|
|
2
2
|
# dsh-base. It mounts no Host, HTTP server, Web runtime, or browser plugin. An
|
|
3
3
|
# ordinary provider plugin injects `cmdlineArgs`, parses the optional prompt
|
|
4
|
-
# positional and `--
|
|
5
|
-
# --help, then the terminal runner creates or resumes one Agent through the
|
|
4
|
+
# positional, `--resume`, and `--no-open` (`dsh --profile tui [prompt...]`)
|
|
5
|
+
# plus this app's --help, then the terminal runner creates or resumes one Agent through the
|
|
6
6
|
# core registry and drives it from the terminal until the user quits.
|
|
7
7
|
|
|
8
8
|
- id: system-prompt
|
|
@@ -56,4 +56,5 @@
|
|
|
56
56
|
config:
|
|
57
57
|
prompt: !!js ctx.tuiStartup.prompt
|
|
58
58
|
resume: !!js ctx.tuiStartup.resume
|
|
59
|
+
openBrowser: !!js ctx.tuiStartup.openBrowser
|
|
59
60
|
toolPreviewLines: 8
|
|
@@ -3,9 +3,12 @@ import { Container, Editor, Input, Loader, Markdown, ProcessTerminal, SelectList
|
|
|
3
3
|
import z from "@deepseek-ai/schemastery";
|
|
4
4
|
import { brandString } from "@deepseek-ai/dsh-brand";
|
|
5
5
|
import { installModelSelection } from "@deepseek-ai/dsh-agent";
|
|
6
|
+
import { launchEnvironmentOf, launchedThroughSsh } from "@deepseek-ai/dsh-launch-environment";
|
|
7
|
+
import { canOpenNativePath, openNativeUrl } from "@deepseek-ai/dsh-native-command";
|
|
6
8
|
import { SessionLogOffset } from "@deepseek-ai/dsh-session";
|
|
9
|
+
import "@deepseek-ai/cordis";
|
|
10
|
+
import { HarnessError, ReasoningEffortId, createUserMessage } from "@deepseek-ai/dsh-llm";
|
|
7
11
|
import { activeAtToken, formatFileMention } from "@deepseek-ai/dsh-file-reference";
|
|
8
|
-
import { ReasoningEffortId, createUserMessage } from "@deepseek-ai/dsh-llm";
|
|
9
12
|
import { formatSessionReferenceMention } from "@deepseek-ai/dsh-session-reference";
|
|
10
13
|
import { assertNever } from "@deepseek-ai/dsh-util-values";
|
|
11
14
|
import { readFile } from "node:fs/promises";
|
|
@@ -16,6 +19,31 @@ import { Readable } from "node:stream";
|
|
|
16
19
|
import { pipeline } from "node:stream/promises";
|
|
17
20
|
import { DEFAULT_SESSION_LOG_COMPRESSION_LEVEL, flushLiveSessionLog, readSessionLogText, sessionLogExportDeps, sessionLogZipFilename, streamSessionLogZip } from "@deepseek-ai/dsh-session-log-export";
|
|
18
21
|
import { planReviewOptions } from "@deepseek-ai/dsh-user-questions";
|
|
22
|
+
//#region ../../credentials/authorization/src/index.ts
|
|
23
|
+
/** Stable error taxonomy for authorization failures. */
|
|
24
|
+
var AuthorizationError = class extends HarnessError {
|
|
25
|
+
constructor(message, code, options) {
|
|
26
|
+
super(message, code, options);
|
|
27
|
+
this.name = "AuthorizationError";
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* The rejection an {@link AuthorizationInteraction.prompt} uses to say the
|
|
32
|
+
* human declined — dismissed the question, chose not to answer — rather than
|
|
33
|
+
* that the surface broke. An attempt whose flow fails after a prompt was
|
|
34
|
+
* declined settles as `cancelled`, the same outcome as a withdrawn signal,
|
|
35
|
+
* because the human saying no is a refusal, not a breakage. Only a human's
|
|
36
|
+
* "no" may reject with this class: a prompt withdrawn by its own `signal` (a
|
|
37
|
+
* flow retiring the losing question of a race) must reject with something
|
|
38
|
+
* else, or a later genuine failure would be misread as a decline.
|
|
39
|
+
*/
|
|
40
|
+
var AuthorizationDeclinedError = class extends AuthorizationError {
|
|
41
|
+
constructor(message = "the authorization prompt was declined") {
|
|
42
|
+
super(message, "DECLINED");
|
|
43
|
+
this.name = "AuthorizationDeclinedError";
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
//#endregion
|
|
19
47
|
//#region lib/types/attach.js
|
|
20
48
|
/**
|
|
21
49
|
* `/attach`: read a local file and admit it into the attachment store as the
|
|
@@ -927,6 +955,59 @@ function editorCompletion(sources) {
|
|
|
927
955
|
}
|
|
928
956
|
};
|
|
929
957
|
}
|
|
958
|
+
/** What the provider-default row is called. */
|
|
959
|
+
const PROVIDER_DEFAULT_LABEL = "Provider default";
|
|
960
|
+
/** The `/effort` argument that restores the provider default. */
|
|
961
|
+
const DEFAULT_ARGUMENT = "default";
|
|
962
|
+
/**
|
|
963
|
+
* The display name of one effort.
|
|
964
|
+
* @param reasoning - what the model declares.
|
|
965
|
+
* @param effort - the selected effort, or undefined for the provider default.
|
|
966
|
+
* @returns the declared name, the raw id when the model no longer declares it, or the provider-default label.
|
|
967
|
+
*/
|
|
968
|
+
function effortName(reasoning, effort) {
|
|
969
|
+
if (effort === void 0) return PROVIDER_DEFAULT_LABEL;
|
|
970
|
+
return reasoning.efforts.find((candidate) => candidate.id === effort)?.name ?? effort;
|
|
971
|
+
}
|
|
972
|
+
/**
|
|
973
|
+
* Picker rows for one model: the provider default above the adapter's own order.
|
|
974
|
+
* @param reasoning - what the model declares.
|
|
975
|
+
* @returns the rows, provider default first.
|
|
976
|
+
*/
|
|
977
|
+
function effortItems(reasoning) {
|
|
978
|
+
const resolved = reasoning.defaultEffort === void 0 ? void 0 : `resolves to ${effortName(reasoning, reasoning.defaultEffort)}`;
|
|
979
|
+
return [{
|
|
980
|
+
value: "",
|
|
981
|
+
label: PROVIDER_DEFAULT_LABEL,
|
|
982
|
+
...resolved === void 0 ? {} : { description: resolved }
|
|
983
|
+
}, ...reasoning.efforts.map((effort) => ({
|
|
984
|
+
value: effort.id,
|
|
985
|
+
label: effort.name,
|
|
986
|
+
...effort.description === void 0 ? {} : { description: effort.description }
|
|
987
|
+
}))];
|
|
988
|
+
}
|
|
989
|
+
/**
|
|
990
|
+
* The dim row under the picker heading.
|
|
991
|
+
* @param reasoning - what the model declares.
|
|
992
|
+
* @param effort - the effort in force, or undefined for the provider default.
|
|
993
|
+
* @returns one row naming the effort in force and the keys that change it.
|
|
994
|
+
*/
|
|
995
|
+
function effortHint(reasoning, effort) {
|
|
996
|
+
return `current: ${effortName(reasoning, effort)} · Esc keeps it · Shift+Tab cycles`;
|
|
997
|
+
}
|
|
998
|
+
/**
|
|
999
|
+
* The effort a typed `/effort` argument names. A declared id wins over the
|
|
1000
|
+
* `default` keyword, so an adapter may own that id.
|
|
1001
|
+
* @param reasoning - what the model declares.
|
|
1002
|
+
* @param typed - the argument, matched against declared ids without case.
|
|
1003
|
+
* @returns the declared effort, undefined for the provider default, or null when the model declares no such effort.
|
|
1004
|
+
*/
|
|
1005
|
+
function matchEffort(reasoning, typed) {
|
|
1006
|
+
const wanted = typed.toLowerCase();
|
|
1007
|
+
const declared = reasoning.efforts.find((effort) => effort.id.toLowerCase() === wanted);
|
|
1008
|
+
if (declared !== void 0) return declared.id;
|
|
1009
|
+
return wanted === DEFAULT_ARGUMENT ? void 0 : null;
|
|
1010
|
+
}
|
|
930
1011
|
//#endregion
|
|
931
1012
|
//#region lib/types/export.js
|
|
932
1013
|
/**
|
|
@@ -1034,6 +1115,13 @@ var ListPrompt = class {
|
|
|
1034
1115
|
settle(value) {
|
|
1035
1116
|
this.settlement.settle(value);
|
|
1036
1117
|
}
|
|
1118
|
+
/**
|
|
1119
|
+
* Open the list on one row instead of the first.
|
|
1120
|
+
* @param index - the row to highlight.
|
|
1121
|
+
*/
|
|
1122
|
+
highlight(index) {
|
|
1123
|
+
this.list.setSelectedIndex(index);
|
|
1124
|
+
}
|
|
1037
1125
|
handleInput(data) {
|
|
1038
1126
|
this.list.handleInput(data);
|
|
1039
1127
|
}
|
|
@@ -1074,10 +1162,17 @@ var ApprovalPrompt = class extends ListPrompt {
|
|
|
1074
1162
|
this.settle("cancelled");
|
|
1075
1163
|
}
|
|
1076
1164
|
};
|
|
1165
|
+
/** Marks the row a picker opened on, so it stays visible after the highlight moves. */
|
|
1166
|
+
const CURRENT_MARK = " ✓";
|
|
1077
1167
|
/** A generic list picker (models, sessions); Escape settles undefined. */
|
|
1078
1168
|
var PickPrompt = class extends ListPrompt {
|
|
1079
|
-
constructor(palette, title, items) {
|
|
1080
|
-
|
|
1169
|
+
constructor(palette, title, items, options = {}) {
|
|
1170
|
+
const inForce = items.findIndex((item) => item.value === options.current);
|
|
1171
|
+
super(palette, `${palette.accent("?")} ${palette.bold(title)}`, options.body ?? [], items.map((item, index) => index === inForce ? {
|
|
1172
|
+
...item,
|
|
1173
|
+
label: `${item.label}${CURRENT_MARK}`
|
|
1174
|
+
} : { ...item }), (row) => items.find((item) => item.value === row.value), () => void 0);
|
|
1175
|
+
if (inForce > 0) this.highlight(inForce);
|
|
1081
1176
|
}
|
|
1082
1177
|
withdraw() {
|
|
1083
1178
|
this.settle(void 0);
|
|
@@ -1549,6 +1644,10 @@ const LOCAL_COMMANDS = [
|
|
|
1549
1644
|
name: "model",
|
|
1550
1645
|
description: "Pick the model and reasoning effort for the next request (/model provider/model, /model save)"
|
|
1551
1646
|
},
|
|
1647
|
+
{
|
|
1648
|
+
name: "effort",
|
|
1649
|
+
description: "Pick the current model's reasoning effort for the next request (/effort <id>, /effort default)"
|
|
1650
|
+
},
|
|
1552
1651
|
{
|
|
1553
1652
|
name: "sessions",
|
|
1554
1653
|
description: "Switch to another session"
|
|
@@ -2041,6 +2140,9 @@ var TuiApp = class {
|
|
|
2041
2140
|
case "model":
|
|
2042
2141
|
await this.chooseModel(argument);
|
|
2043
2142
|
return;
|
|
2143
|
+
case "effort":
|
|
2144
|
+
await this.chooseCurrentEffort(argument);
|
|
2145
|
+
return;
|
|
2044
2146
|
case "sessions":
|
|
2045
2147
|
await this.chooseSession();
|
|
2046
2148
|
return;
|
|
@@ -2167,7 +2269,8 @@ var TuiApp = class {
|
|
|
2167
2269
|
this.notice("no models are available from the composed providers", "error");
|
|
2168
2270
|
return;
|
|
2169
2271
|
}
|
|
2170
|
-
const
|
|
2272
|
+
const current = this.currentSelection();
|
|
2273
|
+
const picked = await this.modals.run(new PickPrompt(this.deps.palette, "Model for the next request", items, { current: `${current.provider}/${current.model}` }));
|
|
2171
2274
|
if (picked === void 0) return;
|
|
2172
2275
|
const slash = picked.value.indexOf("/");
|
|
2173
2276
|
next = {
|
|
@@ -2185,68 +2288,135 @@ var TuiApp = class {
|
|
|
2185
2288
|
this.refreshFooter();
|
|
2186
2289
|
}
|
|
2187
2290
|
/**
|
|
2188
|
-
* Offer the model's reasoning efforts when it declares more than one.
|
|
2291
|
+
* Offer the model's reasoning efforts when it declares more than one. The
|
|
2292
|
+
* picker opens on the effort already in force for that exact model.
|
|
2293
|
+
* @param model - the model the user picked.
|
|
2189
2294
|
* @returns the chosen effort, undefined for the provider default, or null when dismissed or the model is unknown.
|
|
2190
2295
|
*/
|
|
2191
2296
|
async chooseEffort(model) {
|
|
2192
|
-
const
|
|
2193
|
-
if (
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
efforts = (await llm.resolveModelInfo(model.provider, model.model)).reasoning?.efforts ?? [];
|
|
2197
|
-
} catch (error) {
|
|
2198
|
-
this.notice(`${model.provider}/${model.model}: ${describeFailure(error)}`, "error");
|
|
2297
|
+
const lookup = await this.lookupEfforts(model);
|
|
2298
|
+
if (this.stopped) return null;
|
|
2299
|
+
if (lookup.kind === "failed") {
|
|
2300
|
+
this.notice(lookup.message, "error");
|
|
2199
2301
|
return null;
|
|
2200
2302
|
}
|
|
2201
|
-
if (
|
|
2202
|
-
const
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
...effort.description === void 0 ? {} : { description: effort.description }
|
|
2209
|
-
}))];
|
|
2210
|
-
const picked = await this.modals.run(new PickPrompt(this.deps.palette, `Reasoning effort for ${model.model}`, items));
|
|
2303
|
+
if (lookup.kind !== "ready") return void 0;
|
|
2304
|
+
const current = this.currentSelection();
|
|
2305
|
+
const sameModel = current.provider === model.provider && current.model === model.model;
|
|
2306
|
+
const picked = await this.modals.run(new PickPrompt(this.deps.palette, `Reasoning effort · ${model.provider}/${model.model}`, effortItems(lookup.reasoning), {
|
|
2307
|
+
body: ["Esc cancels the model change"],
|
|
2308
|
+
current: (sameModel ? current.reasoningEffort : void 0) ?? ""
|
|
2309
|
+
}));
|
|
2211
2310
|
if (picked === void 0) return null;
|
|
2212
2311
|
return picked.value === "" ? void 0 : ReasoningEffortId(picked.value);
|
|
2213
2312
|
}
|
|
2214
2313
|
/**
|
|
2314
|
+
* Choose the bound model's reasoning effort for the next request: an empty
|
|
2315
|
+
* argument opens the picker on the effort in force, `default` restores the
|
|
2316
|
+
* provider default, and anything else names a declared effort.
|
|
2317
|
+
* @param argument - a declared effort id, `default`, or empty.
|
|
2318
|
+
*/
|
|
2319
|
+
async chooseCurrentEffort(argument) {
|
|
2320
|
+
const current = this.currentSelection();
|
|
2321
|
+
const lookup = await this.lookupEfforts(current);
|
|
2322
|
+
if (this.stopped) return;
|
|
2323
|
+
if (lookup.kind !== "ready") {
|
|
2324
|
+
this.reportEffortLookup(lookup, current);
|
|
2325
|
+
return;
|
|
2326
|
+
}
|
|
2327
|
+
const { reasoning } = lookup;
|
|
2328
|
+
if (argument !== "") {
|
|
2329
|
+
const matched = matchEffort(reasoning, argument);
|
|
2330
|
+
if (matched === null) {
|
|
2331
|
+
const declared = reasoning.efforts.map((effort) => effort.id).join(", ");
|
|
2332
|
+
this.notice(`${current.provider}/${current.model} has no effort "${argument}" (${declared}, default)`, "error");
|
|
2333
|
+
return;
|
|
2334
|
+
}
|
|
2335
|
+
this.applyEffort(current, matched);
|
|
2336
|
+
return;
|
|
2337
|
+
}
|
|
2338
|
+
const picked = await this.modals.run(new PickPrompt(this.deps.palette, `Reasoning effort · ${current.provider}/${current.model}`, effortItems(reasoning), {
|
|
2339
|
+
body: [effortHint(reasoning, current.reasoningEffort)],
|
|
2340
|
+
current: current.reasoningEffort ?? ""
|
|
2341
|
+
}));
|
|
2342
|
+
if (picked === void 0) return;
|
|
2343
|
+
this.applyEffort(current, picked.value === "" ? void 0 : ReasoningEffortId(picked.value));
|
|
2344
|
+
}
|
|
2345
|
+
/**
|
|
2215
2346
|
* Advance the bound selection to the next reasoning effort, wrapping through
|
|
2216
2347
|
* the provider default. A model with fewer than two efforts has nothing to cycle.
|
|
2217
2348
|
*/
|
|
2218
2349
|
async cycleEffort() {
|
|
2219
2350
|
if (this.stopped || this.modals.isActive()) return;
|
|
2220
2351
|
const current = this.currentSelection();
|
|
2221
|
-
const
|
|
2222
|
-
if (
|
|
2223
|
-
|
|
2352
|
+
const lookup = await this.lookupEfforts(current);
|
|
2353
|
+
if (this.stopped) return;
|
|
2354
|
+
if (lookup.kind !== "ready") {
|
|
2355
|
+
this.reportEffortLookup(lookup, current);
|
|
2224
2356
|
return;
|
|
2225
2357
|
}
|
|
2226
|
-
|
|
2358
|
+
const steps = [void 0, ...lookup.reasoning.efforts.map((effort) => effort.id)];
|
|
2359
|
+
const index = steps.findIndex((step) => step === current.reasoningEffort);
|
|
2360
|
+
this.applyEffort(current, steps[index === -1 ? 1 : (index + 1) % steps.length]);
|
|
2361
|
+
}
|
|
2362
|
+
/**
|
|
2363
|
+
* Read what one model declares about reasoning effort.
|
|
2364
|
+
* @param model - the model to resolve.
|
|
2365
|
+
* @returns the declared efforts, or why the terminal has none to offer.
|
|
2366
|
+
*/
|
|
2367
|
+
async lookupEfforts(model) {
|
|
2368
|
+
const llm = this.deps.ctx.get("llm");
|
|
2369
|
+
if (llm === void 0) return { kind: "no-catalog" };
|
|
2370
|
+
let reasoning;
|
|
2227
2371
|
try {
|
|
2228
|
-
|
|
2372
|
+
reasoning = (await llm.resolveModelInfo(model.provider, model.model)).reasoning;
|
|
2229
2373
|
} catch (error) {
|
|
2230
|
-
|
|
2231
|
-
|
|
2374
|
+
return {
|
|
2375
|
+
kind: "failed",
|
|
2376
|
+
message: `${model.provider}/${model.model}: ${describeFailure(error)}`
|
|
2377
|
+
};
|
|
2232
2378
|
}
|
|
2233
|
-
if (
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2379
|
+
if (reasoning === void 0 || reasoning.efforts.length < 2) return { kind: "no-efforts" };
|
|
2380
|
+
return {
|
|
2381
|
+
kind: "ready",
|
|
2382
|
+
reasoning
|
|
2383
|
+
};
|
|
2384
|
+
}
|
|
2385
|
+
/**
|
|
2386
|
+
* Print why a model offers no effort to choose.
|
|
2387
|
+
* @param lookup - a lookup that resolved no efforts.
|
|
2388
|
+
* @param model - the model it was read for.
|
|
2389
|
+
*/
|
|
2390
|
+
reportEffortLookup(lookup, model) {
|
|
2391
|
+
switch (lookup.kind) {
|
|
2392
|
+
case "no-catalog":
|
|
2393
|
+
this.notice("no model catalog is composed", "error");
|
|
2394
|
+
return;
|
|
2395
|
+
case "failed":
|
|
2396
|
+
this.notice(lookup.message, "error");
|
|
2397
|
+
return;
|
|
2398
|
+
case "no-efforts":
|
|
2399
|
+
this.notice(`${model.provider}/${model.model} has no selectable reasoning efforts`);
|
|
2400
|
+
return;
|
|
2401
|
+
/* v8 ignore next -- closed-union exhaustiveness guard */
|
|
2402
|
+
default: assertNever(lookup, "effort lookup");
|
|
2237
2403
|
}
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2404
|
+
}
|
|
2405
|
+
/**
|
|
2406
|
+
* Install one reasoning effort on the bound selection for the next request.
|
|
2407
|
+
* @param model - the provider and model the effort belongs to.
|
|
2408
|
+
* @param effort - the chosen effort, or undefined for the provider default.
|
|
2409
|
+
*/
|
|
2410
|
+
applyEffort(model, effort) {
|
|
2411
|
+
this.bound.selection.current = effort === void 0 ? {
|
|
2412
|
+
provider: model.provider,
|
|
2413
|
+
model: model.model
|
|
2244
2414
|
} : {
|
|
2245
|
-
provider:
|
|
2246
|
-
model:
|
|
2247
|
-
reasoningEffort:
|
|
2415
|
+
provider: model.provider,
|
|
2416
|
+
model: model.model,
|
|
2417
|
+
reasoningEffort: effort
|
|
2248
2418
|
};
|
|
2249
|
-
this.notice(
|
|
2419
|
+
this.notice(effort === void 0 ? "effort: provider default from the next request" : `effort ${effort} from the next request`, "success");
|
|
2250
2420
|
this.refreshFooter();
|
|
2251
2421
|
}
|
|
2252
2422
|
async modelItems() {
|
|
@@ -2407,6 +2577,9 @@ var TuiApp = class {
|
|
|
2407
2577
|
if (notice.url !== void 0) parts.push(notice.url);
|
|
2408
2578
|
if (notice.code !== void 0) parts.push(`code: ${notice.code}`);
|
|
2409
2579
|
this.notice(parts.join(" "));
|
|
2580
|
+
if (notice.url !== void 0 && notice.openInBrowser === true && this.deps.openUrl !== void 0) this.deps.openUrl(notice.url).catch((error) => {
|
|
2581
|
+
this.notice(`could not open sign-in page: ${describeFailure(error)}`, "error");
|
|
2582
|
+
});
|
|
2410
2583
|
},
|
|
2411
2584
|
prompt: (prompt) => this.answerAuthorizationPrompt(prompt)
|
|
2412
2585
|
}
|
|
@@ -2423,7 +2596,7 @@ var TuiApp = class {
|
|
|
2423
2596
|
label: option.label,
|
|
2424
2597
|
...option.description === void 0 ? {} : { description: option.description }
|
|
2425
2598
|
}))), prompt.signal);
|
|
2426
|
-
if (picked === void 0) throw new
|
|
2599
|
+
if (picked === void 0) throw new AuthorizationDeclinedError("the sign-in prompt was dismissed");
|
|
2427
2600
|
return picked.value;
|
|
2428
2601
|
}
|
|
2429
2602
|
const answer = await this.modals.run(new QuestionPrompt(this.deps.palette, {
|
|
@@ -2431,7 +2604,7 @@ var TuiApp = class {
|
|
|
2431
2604
|
question: prompt.message,
|
|
2432
2605
|
...prompt.placeholder === void 0 ? {} : { detail: prompt.placeholder }
|
|
2433
2606
|
}), prompt.signal);
|
|
2434
|
-
if (answer?.custom === void 0) throw new
|
|
2607
|
+
if (answer?.custom === void 0) throw new AuthorizationDeclinedError("the sign-in prompt was dismissed");
|
|
2435
2608
|
return answer.custom;
|
|
2436
2609
|
}
|
|
2437
2610
|
async exportSession(argument) {
|
|
@@ -2674,7 +2847,8 @@ const inject = [
|
|
|
2674
2847
|
const Config = z.object({
|
|
2675
2848
|
prompt: z.string(),
|
|
2676
2849
|
resume: z.string(),
|
|
2677
|
-
toolPreviewLines: z.natural().min(1).default(8)
|
|
2850
|
+
toolPreviewLines: z.natural().min(1).default(8),
|
|
2851
|
+
openBrowser: z.boolean().default(true)
|
|
2678
2852
|
});
|
|
2679
2853
|
/** The process-bound pieces of the host; tests substitute a fake terminal and captured streams. */
|
|
2680
2854
|
const internals = {
|
|
@@ -2685,7 +2859,9 @@ const internals = {
|
|
|
2685
2859
|
process.stdin.unref();
|
|
2686
2860
|
},
|
|
2687
2861
|
stderr: process.stderr,
|
|
2688
|
-
color: colorEnabled(process.env, process.stdout.isTTY)
|
|
2862
|
+
color: colorEnabled(process.env, process.stdout.isTTY),
|
|
2863
|
+
canOpenUrl: () => canOpenNativePath(),
|
|
2864
|
+
openUrl: openNativeUrl
|
|
2689
2865
|
};
|
|
2690
2866
|
/** Events read per page when a persisted session is resumed, through the storage handle before the Agent takes the log over. */
|
|
2691
2867
|
const HISTORY_PAGE = 256;
|
|
@@ -2834,14 +3010,17 @@ async function run(ctx, config, host) {
|
|
|
2834
3010
|
if (core === void 0) return;
|
|
2835
3011
|
const cwd = process.cwd();
|
|
2836
3012
|
const sessions = sessionHost(ctx, core, cwd);
|
|
3013
|
+
const initial = config.resume === void 0 ? await sessions.create() : await sessions.resume(brandString(config.resume));
|
|
3014
|
+
const openUrl = config.openBrowser && host.canOpenUrl() && !launchedThroughSsh(launchEnvironmentOf(ctx)) ? (url) => host.openUrl(url) : void 0;
|
|
2837
3015
|
new TuiApp({
|
|
2838
3016
|
ctx,
|
|
2839
3017
|
host: sessions,
|
|
2840
|
-
initial
|
|
3018
|
+
initial,
|
|
2841
3019
|
terminal: host.createTerminal(),
|
|
2842
3020
|
palette: createPalette(host.color),
|
|
2843
3021
|
toolPreviewLines: config.toolPreviewLines,
|
|
2844
3022
|
cwd,
|
|
3023
|
+
...openUrl === void 0 ? {} : { openUrl },
|
|
2845
3024
|
releaseInput: () => {
|
|
2846
3025
|
host.releaseInput();
|
|
2847
3026
|
},
|
|
@@ -2873,6 +3052,8 @@ function apply(ctx, config) {
|
|
|
2873
3052
|
releaseInput: internals.releaseInput,
|
|
2874
3053
|
stderr: internals.stderr,
|
|
2875
3054
|
color: internals.color,
|
|
3055
|
+
canOpenUrl: internals.canOpenUrl,
|
|
3056
|
+
openUrl: internals.openUrl,
|
|
2876
3057
|
exit
|
|
2877
3058
|
};
|
|
2878
3059
|
run(ctx, config, host).catch((error) => {
|
|
@@ -19,7 +19,7 @@ const TUI_STARTUP_SERVICE = "tuiStartup";
|
|
|
19
19
|
* @returns a fresh program, so one process can parse more than once (tests).
|
|
20
20
|
*/
|
|
21
21
|
function tuiCommand() {
|
|
22
|
-
return new Command().name("dsh --profile tui").description("Talk to the agent in this terminal: streamed replies, tool cards, approvals, and slash commands.").helpOption("-h, --help", "show this help").option("--resume <session-id>", "continue a persisted session instead of starting a new one").argument("[prompt...]", "an optional first prompt; multiple words are joined by spaces").addHelpText("after", `
|
|
22
|
+
return new Command().name("dsh --profile tui").description("Talk to the agent in this terminal: streamed replies, tool cards, approvals, and slash commands.").helpOption("-h, --help", "show this help").option("--resume <session-id>", "continue a persisted session instead of starting a new one").option("--no-open", "print sign-in URLs instead of opening the default browser").argument("[prompt...]", "an optional first prompt; multiple words are joined by spaces").addHelpText("after", `
|
|
23
23
|
Examples:
|
|
24
24
|
dsh --profile tui start a new session and wait for input
|
|
25
25
|
dsh --profile tui "explain this repo" start with a first prompt
|
|
@@ -43,7 +43,8 @@ function apply(ctx) {
|
|
|
43
43
|
if (options.resume !== void 0 && options.resume.trim() === "") program.error("error: --resume needs a session id");
|
|
44
44
|
ctx.provide(TUI_STARTUP_SERVICE, {
|
|
45
45
|
prompt: prompt === "" ? void 0 : prompt,
|
|
46
|
-
resume: options.resume
|
|
46
|
+
resume: options.resume,
|
|
47
|
+
openBrowser: options.open
|
|
47
48
|
});
|
|
48
49
|
});
|
|
49
50
|
parseCmdline(ctx, program);
|
|
@@ -44,6 +44,8 @@
|
|
|
44
44
|
"@deepseek-ai/dsh-file-reference": "0.1.6-alpha.1",
|
|
45
45
|
"@deepseek-ai/dsh-file-reference-local": "0.1.6-alpha.1",
|
|
46
46
|
"@deepseek-ai/dsh-host-plugin-inventory": "0.1.6-alpha.1",
|
|
47
|
+
"@deepseek-ai/dsh-launch-environment": "0.1.6-alpha.1",
|
|
48
|
+
"@deepseek-ai/dsh-native-command": "0.1.6-alpha.1",
|
|
47
49
|
"@deepseek-ai/dsh-session-log-export": "0.1.6-alpha.1",
|
|
48
50
|
"@deepseek-ai/dsh-session-reference": "0.1.6-alpha.1",
|
|
49
51
|
"@deepseek-ai/dsh-session-stats": "0.1.6-alpha.1",
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { createRequire } from "node:module";
|
|
2
|
-
import { spawn } from "node:child_process";
|
|
3
2
|
import { dirname, join } from "node:path";
|
|
4
3
|
import { networkInterfaces } from "node:os";
|
|
5
4
|
import { fileURLToPath } from "node:url";
|
|
@@ -7,7 +6,7 @@ import z from "@deepseek-ai/schemastery";
|
|
|
7
6
|
import { addHarnessSourceSection, auditStartupEntries } from "@deepseek-ai/dsh-app-boot";
|
|
8
7
|
import * as FrontendStatic from "@deepseek-ai/dsh-host-frontend-static";
|
|
9
8
|
import { launchEnvironmentOf, launchedThroughSsh } from "@deepseek-ai/dsh-launch-environment";
|
|
10
|
-
import {
|
|
9
|
+
import { openNativeUrl } from "@deepseek-ai/dsh-native-command";
|
|
11
10
|
//#region lib/types/index.js
|
|
12
11
|
/**
|
|
13
12
|
* @deepseek-ai/dsh-web-app — the browser-surface bundle's runtime glue plugin
|
|
@@ -41,35 +40,6 @@ const DSH_WEB_URL = "DSH_WEB_URL";
|
|
|
41
40
|
const LOOPBACK_HOST = "127.0.0.1";
|
|
42
41
|
/** The webserver schema's all-interfaces bind literal. */
|
|
43
42
|
const ALL_INTERFACES_HOST = "0.0.0.0";
|
|
44
|
-
const BROWSER_OPENER_MODULE = import.meta.resolve("open");
|
|
45
|
-
const BROWSER_OPENER_PROGRAM = `
|
|
46
|
-
try {
|
|
47
|
-
const { default: open } = await import(${JSON.stringify(BROWSER_OPENER_MODULE)})
|
|
48
|
-
const launcher = await open(process.argv[1])
|
|
49
|
-
if (process.platform === 'win32') {
|
|
50
|
-
// open resolves at PowerShell spawn; keep it referenced until that launcher hands the URL to Windows.
|
|
51
|
-
const code = launcher.exitCode ?? await new Promise((resolve, reject) => {
|
|
52
|
-
function onError(error) {
|
|
53
|
-
launcher.off('close', onClose)
|
|
54
|
-
reject(error)
|
|
55
|
-
}
|
|
56
|
-
function onClose(code) {
|
|
57
|
-
launcher.off('error', onError)
|
|
58
|
-
resolve(code)
|
|
59
|
-
}
|
|
60
|
-
launcher.ref()
|
|
61
|
-
launcher.once('error', onError)
|
|
62
|
-
launcher.once('close', onClose)
|
|
63
|
-
})
|
|
64
|
-
if (code !== 0) throw new Error('browser operating-system launcher exited with code ' + String(code))
|
|
65
|
-
}
|
|
66
|
-
process.exitCode = 0
|
|
67
|
-
} catch (error) {
|
|
68
|
-
// The parent turns this exit into the manual-URL warning.
|
|
69
|
-
console.error(error)
|
|
70
|
-
process.exitCode = 1
|
|
71
|
-
}
|
|
72
|
-
`;
|
|
73
43
|
/**
|
|
74
44
|
* Resolve one LAN-trust snapshot from the active server bind.
|
|
75
45
|
*
|
|
@@ -113,55 +83,10 @@ function resolveDistIndex() {
|
|
|
113
83
|
throw new Error("web-app: @deepseek-ai/dsh-web-frontend is not resolvable from this composition");
|
|
114
84
|
}
|
|
115
85
|
}
|
|
116
|
-
/** Start the maintained platform opener without forwarding Harness credentials. */
|
|
117
|
-
function spawnBrowserLauncher(url) {
|
|
118
|
-
return spawn(process.execPath, [
|
|
119
|
-
"--input-type=module",
|
|
120
|
-
"--eval",
|
|
121
|
-
BROWSER_OPENER_PROGRAM,
|
|
122
|
-
"--",
|
|
123
|
-
url
|
|
124
|
-
], {
|
|
125
|
-
env: scrubbedParentEnv(),
|
|
126
|
-
stdio: [
|
|
127
|
-
"ignore",
|
|
128
|
-
"inherit",
|
|
129
|
-
"pipe"
|
|
130
|
-
]
|
|
131
|
-
});
|
|
132
|
-
}
|
|
133
|
-
/** Hand one URL to the operating system's default browser. */
|
|
134
|
-
async function openBrowser(url) {
|
|
135
|
-
const launcher = spawnBrowserLauncher(url);
|
|
136
|
-
let launcherStderr = "";
|
|
137
|
-
launcher.stderr?.setEncoding("utf8");
|
|
138
|
-
launcher.stderr?.on("data", (chunk) => {
|
|
139
|
-
launcherStderr += chunk;
|
|
140
|
-
});
|
|
141
|
-
await new Promise((resolve, reject) => {
|
|
142
|
-
function onError(error) {
|
|
143
|
-
launcher.off("close", onClose);
|
|
144
|
-
reject(error);
|
|
145
|
-
}
|
|
146
|
-
function onClose(code) {
|
|
147
|
-
launcher.off("error", onError);
|
|
148
|
-
if (code !== 0) {
|
|
149
|
-
const firstLine = launcherStderr.trim().split(/\r?\n/u)[0];
|
|
150
|
-
const reason = firstLine === void 0 || firstLine === "" ? `browser launcher exited with code ${String(code)}` : firstLine.replace(/^(?:[A-Za-z]*Error):\s*/u, "");
|
|
151
|
-
reject(new Error(reason));
|
|
152
|
-
return;
|
|
153
|
-
}
|
|
154
|
-
if (launcherStderr !== "") process.stderr.write(launcherStderr);
|
|
155
|
-
resolve();
|
|
156
|
-
}
|
|
157
|
-
launcher.once("error", onError);
|
|
158
|
-
launcher.once("close", onClose);
|
|
159
|
-
});
|
|
160
|
-
}
|
|
161
86
|
/** Test hooks for the built dist and native browser handoff; production never mutates them. */
|
|
162
87
|
const internals = {
|
|
163
88
|
resolveDistIndex,
|
|
164
|
-
openBrowser
|
|
89
|
+
openBrowser: openNativeUrl
|
|
165
90
|
};
|
|
166
91
|
/**
|
|
167
92
|
* Mount the Web runtime: dist serving, surface prompt, the bash runtime
|
|
@@ -110,17 +110,16 @@
|
|
|
110
110
|
"@deepseek-ai/dsh-host-webserver": "0.1.6-alpha.1",
|
|
111
111
|
"@deepseek-ai/dsh-launch-environment": "0.1.6-alpha.1",
|
|
112
112
|
"@deepseek-ai/dsh-message-feedback": "0.1.6-alpha.1",
|
|
113
|
+
"@deepseek-ai/dsh-native-command": "0.1.6-alpha.1",
|
|
113
114
|
"@deepseek-ai/dsh-session-log-export": "0.1.6-alpha.1",
|
|
114
115
|
"@deepseek-ai/dsh-session-reference": "0.1.6-alpha.1",
|
|
115
116
|
"@deepseek-ai/dsh-session-stats": "0.1.6-alpha.1",
|
|
116
117
|
"@deepseek-ai/dsh-session-turn-outline": "0.1.6-alpha.1",
|
|
117
|
-
"@deepseek-ai/dsh-subprocess": "0.1.6-alpha.1",
|
|
118
118
|
"@deepseek-ai/dsh-tool-subagent": "0.1.6-alpha.1",
|
|
119
119
|
"@deepseek-ai/dsh-web-frontend": "0.1.6-alpha.1",
|
|
120
120
|
"@deepseek-ai/dsh-workspace": "0.1.6-alpha.1",
|
|
121
121
|
"@deepseek-ai/schemastery": "3.18.2",
|
|
122
122
|
"commander": "^15.0.0",
|
|
123
|
-
"open": "^11.0.0",
|
|
124
123
|
"@deepseek-ai/dsh-api-terminal-controller": "0.1.6-alpha.1",
|
|
125
124
|
"@deepseek-ai/dsh-client-ui-sidebar-terminal": "0.1.6-alpha.1"
|
|
126
125
|
},
|
|
@@ -8,7 +8,7 @@ This package contains type definitions for node (https://nodejs.org/).
|
|
|
8
8
|
Files were exported from https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/node/v22.
|
|
9
9
|
|
|
10
10
|
### Additional Details
|
|
11
|
-
* Last updated:
|
|
11
|
+
* Last updated: Tue, 15 Sep 2026 20:33:28 GMT
|
|
12
12
|
* Dependencies: [undici-types](https://npmjs.com/package/undici-types)
|
|
13
13
|
|
|
14
14
|
# Credits
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@types/node",
|
|
3
|
-
"version": "22.20.
|
|
3
|
+
"version": "22.20.3",
|
|
4
4
|
"description": "TypeScript definitions for node",
|
|
5
5
|
"homepage": "https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/node",
|
|
6
6
|
"license": "MIT",
|
|
@@ -139,6 +139,6 @@
|
|
|
139
139
|
"undici-types": "~6.21.0"
|
|
140
140
|
},
|
|
141
141
|
"peerDependencies": {},
|
|
142
|
-
"typesPublisherContentHash": "
|
|
142
|
+
"typesPublisherContentHash": "96f0a72c6119022091f6102f750c1a5d1b46ea12c877c15cd9614b20c51cdbc0",
|
|
143
143
|
"typeScriptVersion": "5.6"
|
|
144
144
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@songtonyli/dsh-cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.10",
|
|
4
4
|
"description": "The dsh terminal agent: an interactive TUI over the DeepSeek Harness, with its whole Node runtime bundled in one install.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -341,7 +341,7 @@
|
|
|
341
341
|
"@smithy/util-utf8": "2.3.0",
|
|
342
342
|
"@stablelib/base64": "1.0.1",
|
|
343
343
|
"@standard-schema/spec": "1.1.0",
|
|
344
|
-
"@types/node": "22.20.
|
|
344
|
+
"@types/node": "22.20.3",
|
|
345
345
|
"@types/retry": "0.12.0",
|
|
346
346
|
"@vscode/ripgrep": "1.18.0",
|
|
347
347
|
"@xterm/addon-serialize": "0.14.0",
|