@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.
Files changed (37) hide show
  1. package/README.md +1 -1
  2. package/bin/dsh.mjs +1 -1
  3. package/node_modules/@deepseek-ai/dsh-api-authorization-controller/README.i18n.yaml +2 -2
  4. package/node_modules/@deepseek-ai/dsh-api-authorization-controller/README.md +1 -1
  5. package/node_modules/@deepseek-ai/dsh-api-authorization-controller/README.zh.md +1 -1
  6. package/node_modules/@deepseek-ai/dsh-api-authorization-controller/lib/index.js +1 -0
  7. package/node_modules/@deepseek-ai/dsh-api-authorization-controller/lib/typert.host.js +2 -1
  8. package/node_modules/@deepseek-ai/dsh-api-authorization-controller/lib/typert.remote-client.js +1 -0
  9. package/node_modules/@deepseek-ai/dsh-api-authorization-controller/lib/types/index.js +1 -0
  10. package/node_modules/@deepseek-ai/dsh-api-remotes/lib/client.js +1 -0
  11. package/node_modules/@deepseek-ai/dsh-authorization/README.i18n.yaml +2 -2
  12. package/node_modules/@deepseek-ai/dsh-authorization/README.md +1 -1
  13. package/node_modules/@deepseek-ai/dsh-authorization/README.zh.md +1 -1
  14. package/node_modules/@deepseek-ai/dsh-client-ui-settings-signin/lib/client.js +1 -0
  15. package/node_modules/@deepseek-ai/dsh-client-ui-sidebar/lib/client.js +1 -1
  16. package/node_modules/@deepseek-ai/dsh-llm-pi-ai/README.i18n.yaml +2 -2
  17. package/node_modules/@deepseek-ai/dsh-llm-pi-ai/README.md +1 -1
  18. package/node_modules/@deepseek-ai/dsh-llm-pi-ai/README.zh.md +1 -1
  19. package/node_modules/@deepseek-ai/dsh-llm-pi-ai/lib/index.js +3 -1
  20. package/node_modules/@deepseek-ai/dsh-native-command/README.i18n.yaml +2 -2
  21. package/node_modules/@deepseek-ai/dsh-native-command/README.md +7 -2
  22. package/node_modules/@deepseek-ai/dsh-native-command/README.zh.md +7 -2
  23. package/node_modules/@deepseek-ai/dsh-native-command/lib/index.js +93 -2
  24. package/node_modules/@deepseek-ai/dsh-native-command/package.json +4 -0
  25. package/node_modules/@deepseek-ai/dsh-tool-cordis/lib/index.js +2 -2
  26. package/node_modules/@deepseek-ai/dsh-tui-app/README.i18n.yaml +2 -2
  27. package/node_modules/@deepseek-ai/dsh-tui-app/README.md +10 -4
  28. package/node_modules/@deepseek-ai/dsh-tui-app/README.zh.md +10 -4
  29. package/node_modules/@deepseek-ai/dsh-tui-app/cordis.patch.yml +3 -2
  30. package/node_modules/@deepseek-ai/dsh-tui-app/lib/index.js +229 -48
  31. package/node_modules/@deepseek-ai/dsh-tui-app/lib/startup.js +3 -2
  32. package/node_modules/@deepseek-ai/dsh-tui-app/package.json +2 -0
  33. package/node_modules/@deepseek-ai/dsh-web-app/lib/index.js +2 -77
  34. package/node_modules/@deepseek-ai/dsh-web-app/package.json +1 -2
  35. package/node_modules/@types/node/README.md +1 -1
  36. package/node_modules/@types/node/package.json +2 -2
  37. 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.8; `dsh --version`
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.8 (deepseek-harness 0.1.6-alpha.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: bdf227b45a0f31adc8ceb7c0d2cf8e5002c487a6
6
- README.zh.md: b65b8ce1840395b89b5feb36083b96650cb122da
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 and any page or code it refers to, 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.
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` 携带人需要做的事以及它涉及的页面或验证码,`prompt` 携带一个提问以及回答时要指明的 id,`prompt-withdrawn` 对应 flow 撤回的提问,最后的 `settled` 帧携带 `authorized` 或 `cancelled`。尝试在载体拉取第一帧时开始,随流结束而结束;关闭该流即撤回它。`cancel(key)` 用于已不再持有该流的调用撤回它,这正是重新渲染后的页面上「取消」按钮所用的方式。
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",
@@ -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: 9b4f4e895bc94c0a34a1cee2d6bf28d7663decea
6
- README.zh.md: 44c354fef1cb2bd9261483cb4048858a7f70bd31
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 是单向的,且从不携带机密:一条消息,以及可选的「人需要打开的页面」与「需要在该页面输入的码」。prompt 是 flow 无法自行回答的问题——`text`、`secret` 或 `select`——其中 `secret` 与 `text` 的差别仅在呈现方式。prompt 自带 signal,使得让手输码与浏览器回调赛跑的 flow 可以在尝试继续的同时撤下落败的那个问题;撤销整次尝试则用请求的 signal。这套词汇刻意小于任何单个提供方的词汇:它描述的是界面必须渲染什么,因此能渲染一个 flow 的界面就能渲染全部 flow。
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-0fd6c65-dirty`;
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: a49281b5ddc63c1fe4ce628857ff6f267dc498cd
6
- README.zh.md: 78aed7db98e217691351f49f3f8ee41e74177e17
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>` 记录中。存储的登录在其路由的 `apiKeyEnv` 覆盖之下完成认证,并在存储的跨进程锁下自行刷新;退出登录即删除存储记录。落在记录文法之外——小写连字符标识符——的手工声明路由键无法登录,因为对它的记录写入会以 `LlmError('UNSTORABLE_PROVIDER_ID')` 拒绝;这类路由改用 `apiKeyEnv` 或提供方 ambient 设置认证。
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: 10b60266403f6a479a286cccd369ee2e52dd3aa6
6
- README.zh.md: e8f2adda57ee257983a06bb2798c852cab20bf6a
5
+ README.md: 9dae51fc0c605e9c0ac6d04a0c768897ce04fc9f
6
+ README.zh.md: 92a1885e51dc12c51ef3a6d127bf0e7d3dc3391c
@@ -1,5 +1,5 @@
1
1
  ---
2
- description: "Host-native command and path-opening utilities with shell-free execution, cancellation, desktop detection, and WSL path handoff."
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 and opens Host filesystem paths through the desktop. 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. It is a library, not a plugin: no `ctx`, no state, no events.
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: "宿主原生命令与路径打开工具,提供无 shell 执行、取消、桌面探测与 WSL 路径交接。"
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 可执行文件,并通过桌面打开 Host 文件系统路径。命令运行器捕获 utf8 输出、传播取消,并隐藏 Windows 瞬时控制台。路径打开器支持默认应用与文本编辑器意图、浏览器可渲染文档、WSL 转换与桌面可用性检查。它是库而非插件:没有 `ctx`、无状态、不发事件。
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 };
@@ -26,6 +26,10 @@
26
26
  "lib/types/**/*.d.ts"
27
27
  ],
28
28
  "license": "MIT",
29
+ "dependencies": {
30
+ "@deepseek-ai/dsh-subprocess": "0.1.6-alpha.1",
31
+ "open": "^11.0.0"
32
+ },
29
33
  "peerDependencies": {
30
34
  "@deepseek-ai/cordis": "4.0.2"
31
35
  },
@@ -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: a74c0b8642192cd2b44abd939c3d8082e935991a
6
- README.zh.md: 3b9ba69a63d5461e85fab40b80f21502aaf8558a
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 `resume` 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.
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` 在你当前所在的终端里启动一个多轮会话,没有浏览器、没有服务器。回复实时流式显示,工具调用变成可折叠的卡片,审批与 `ask_user_question` 的问题出现在输入框上方,`@` 补全路径与会话,`/attach` 加入图片与文件,`/` 命令与 Web 共用注册表。会话持久化:`/sessions`、`/new` 与 `/fork` 在会话间切换,`/export` 写出浏览器的 ZIP,`--resume` 稍后继续。它运行与 `dsh web` 相同的模型、工具与安全默认值,同一时间一个会话。
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` 与 `resume` 经启动提供方来自命令行;生成的[配置目录](../../../docs/config-catalog.zh.md#deepseek-aidsh-tui-app)是所有可接受字段的完整来源。
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 `--resume` (`dsh --profile tui [prompt...]`) plus this app's
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
- super(palette, `${palette.accent("?")} ${palette.bold(title)}`, [], [...items], (item) => item, () => void 0);
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 picked = await this.modals.run(new PickPrompt(this.deps.palette, "Model for the next request", items));
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 llm = this.deps.ctx.get("llm");
2193
- if (llm === void 0) return void 0;
2194
- let efforts;
2195
- try {
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 (efforts.length < 2) return void 0;
2202
- const items = [{
2203
- value: "",
2204
- label: "provider default"
2205
- }, ...efforts.map((effort) => ({
2206
- value: effort.id,
2207
- label: effort.name,
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 llm = this.deps.ctx.get("llm");
2222
- if (llm === void 0) {
2223
- this.notice("no model catalog is composed", "error");
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
- let efforts;
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
- efforts = (await llm.resolveModelInfo(current.provider, current.model)).reasoning?.efforts ?? [];
2372
+ reasoning = (await llm.resolveModelInfo(model.provider, model.model)).reasoning;
2229
2373
  } catch (error) {
2230
- this.notice(`${current.provider}/${current.model}: ${describeFailure(error)}`, "error");
2231
- return;
2374
+ return {
2375
+ kind: "failed",
2376
+ message: `${model.provider}/${model.model}: ${describeFailure(error)}`
2377
+ };
2232
2378
  }
2233
- if (this.stopped) return;
2234
- if (efforts.length < 2) {
2235
- this.notice(`${current.provider}/${current.model} has no selectable reasoning efforts`);
2236
- return;
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
- const steps = [void 0, ...efforts.map((effort) => effort.id)];
2239
- const index = steps.findIndex((step) => step === current.reasoningEffort);
2240
- const next = steps[index === -1 ? 1 : (index + 1) % steps.length];
2241
- this.bound.selection.current = next === void 0 ? {
2242
- provider: current.provider,
2243
- model: current.model
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: current.provider,
2246
- model: current.model,
2247
- reasoningEffort: next
2415
+ provider: model.provider,
2416
+ model: model.model,
2417
+ reasoningEffort: effort
2248
2418
  };
2249
- this.notice(next === void 0 ? "effort: provider default from the next request" : `effort ${next} from the next request`, "success");
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 Error("the sign-in prompt was dismissed");
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 Error("the sign-in prompt was dismissed");
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: config.resume === void 0 ? await sessions.create() : await sessions.resume(brandString(config.resume)),
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 { scrubbedParentEnv } from "@deepseek-ai/dsh-subprocess";
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: Wed, 09 Sep 2026 18:05:03 GMT
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.2",
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": "8e6b0ce9d1f2dc282d7964257456bcffbdd45115e795462dde19073b17676daa",
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.8",
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.2",
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",