fix(web): list each shell once in the new-terminal menu
Shell discovery keyed entries by resolved path. On merged-/usr hosts the login shell /bin/bash from $SHELL and the PATH hit /usr/bin/bash for the `bash` candidate are one file with two spellings, so the menu showed "bash" twice. Key discovered shells by executable name, compared without letter case or `.exe`, and keep the earliest entry so the default shell stays first.
This commit is contained in:
1 parent
5a9b56a8be
commit
41021fc803
5 files changed
+29
-11
No files matched your search
@@ -15,8 +15,8 @@
|
||||
en: d152484eb41ac6b4
|
||||
zh: 1fa9a72d1ee045cc
|
||||
/deepseek-ai-dsh-api-terminal-controller/use-this-package:
|
||||
en: 234403af4f95ea67
|
||||
zh: d711c2e62d7d37f8
|
||||
en: 0148cabb3a856226
|
||||
zh: 059a60413474a173
|
||||
/deepseek-ai-dsh-api-terminal-controller/understand-the-implementation:
|
||||
en: 94dcd1835c55acc2
|
||||
zh: cbfeb2da8cb60395
|
||||
|
||||
@@ -27,7 +27,7 @@ Open the execution environment's default shell in a Session workspace from the W
|
||||
|
||||
The Web bundle mounts this package with the subprocess provider, sandbox policy and Typert Gateway. Sandbox policy supplies only the fallback working directory for Sessions without a cwd. `remote.terminal` exposes `environment`, `shells`, `list`, `create`, `retain`, `follow`, `write`, `resize`, `rename` and `close`; each operation is scoped by Session identity. Listing reads retained Host terminals directly, so viewing an offline Session neither activates an Agent nor produces a recovery error.
|
||||
|
||||
Shell discovery lists the execution environment's declared default shell first. Only when the provider omits that default does resolution use `/bin/sh` on POSIX or `cmd.exe` on Windows. An optional `shell` profile overrides that choice with executable `path`, display `name` and `args` (default `[]`). The selector also probes `shellCandidates` through the execution provider and omits only confirmed lookup misses. Creation accepts a discovered `shellPath` and verifies it again; resolution or transport failure is reported without launching a different shell. Environment lookup returns the working directory and limits without resolving a shell, so an unavailable default does not prevent reattaching to an existing process. Automatic POSIX profiles start interactively, and PowerShell uses `-NoLogo`, so completion and startup configuration remain shell-owned. The Session workspace supplies the initial directory. User terminals run with the execution environment’s system-user permissions, independently of the Agent’s sandbox mode and approval policy. Operating-system and container restrictions still apply; DSH does not elevate the user. The subprocess provider retains its credential-environment scrubbing.
|
||||
Shell discovery lists the execution environment's declared default shell first. Only when the provider omits that default does resolution use `/bin/sh` on POSIX or `cmd.exe` on Windows. An optional `shell` profile overrides that choice with executable `path`, display `name` and `args` (default `[]`). The selector also probes `shellCandidates` through the execution provider and omits confirmed lookup misses. It lists each executable name once, ignoring letter case and `.exe`, and keeps the earliest entry, because PATH lookup can reach the default through another directory, such as `/usr/bin/bash` for `/bin/bash` on merged-`/usr` systems. Creation accepts a discovered `shellPath` and verifies it again; resolution or transport failure is reported without launching a different shell. Environment lookup returns the working directory and limits without resolving a shell, so an unavailable default does not prevent reattaching to an existing process. Automatic POSIX profiles start interactively, and PowerShell uses `-NoLogo`, so completion and startup configuration remain shell-owned. The Session workspace supplies the initial directory. User terminals run with the execution environment’s system-user permissions, independently of the Agent’s sandbox mode and approval policy. Operating-system and container restrictions still apply; DSH does not elevate the user. The subprocess provider retains its credential-environment scrubbing.
|
||||
|
||||
| Configuration | Default | Meaning |
|
||||
|---|---|---|
|
||||
|
||||
@@ -27,7 +27,7 @@ kind: "package-reference"
|
||||
|
||||
Web bundle 将此包与 subprocess provider、sandbox policy 和 Typert Gateway 一起挂载。Sandbox policy 仅为没有 cwd 的 Session 提供默认工作目录。`remote.terminal` 提供 `environment`、`shells`、`list`、`create`、`retain`、`follow`、`write`、`resize`、`rename` 和 `close`;每个操作均按 Session 标识限定范围。列表直接读取 Host 保留的终端,因此查看离线 Session 不会激活 Agent,也不会产生恢复错误。
|
||||
|
||||
Shell 探测结果首先列出执行环境声明的默认 shell。仅当 provider 未声明默认值时,才在 POSIX 使用 `/bin/sh`,在 Windows 使用 `cmd.exe`。可选的 `shell` profile 通过可执行路径 `path`、显示名称 `name` 和参数 `args`(默认 `[]`)覆盖这一选择。选择器还会通过执行 provider 探测 `shellCandidates`,仅省略确定未找到的候选。创建请求接受探测返回的 `shellPath` 并再次验证;解析或传输失败会直接报告,不启动其他 shell。环境查询只返回工作目录和限制,不解析 shell,因此默认 shell 不可用时仍可重新连接已有进程。POSIX 自动 profile 以交互模式启动,PowerShell 使用 `-NoLogo`,补全和启动配置仍由 shell 提供。初始目录来自 Session 工作区。用户终端使用执行环境中系统用户的权限,独立于 Agent 的沙箱模式和审批策略。操作系统和容器的限制仍然生效;DSH 不提升用户权限。Subprocess provider 继续清除环境中的凭据变量。
|
||||
Shell 探测结果首先列出执行环境声明的默认 shell。仅当 provider 未声明默认值时,才在 POSIX 使用 `/bin/sh`,在 Windows 使用 `cmd.exe`。可选的 `shell` profile 通过可执行路径 `path`、显示名称 `name` 和参数 `args`(默认 `[]`)覆盖这一选择。选择器还会通过执行 provider 探测 `shellCandidates`,并省略确定未找到的候选。同一可执行文件名(不区分大小写,忽略 `.exe`)只列出最早的一项,因为 PATH 查找可能经由其他目录找到默认 shell,例如 merged-`/usr` 系统上 `/bin/bash` 对应的 `/usr/bin/bash`。创建请求接受探测返回的 `shellPath` 并再次验证;解析或传输失败会直接报告,不启动其他 shell。环境查询只返回工作目录和限制,不解析 shell,因此默认 shell 不可用时仍可重新连接已有进程。POSIX 自动 profile 以交互模式启动,PowerShell 使用 `-NoLogo`,补全和启动配置仍由 shell 提供。初始目录来自 Session 工作区。用户终端使用执行环境中系统用户的权限,独立于 Agent 的沙箱模式和审批策略。操作系统和容器的限制仍然生效;DSH 不提升用户权限。Subprocess provider 继续清除环境中的凭据变量。
|
||||
|
||||
| 配置 | 默认值 | 含义 |
|
||||
|---|---|---|
|
||||
|
||||
@@ -22,18 +22,26 @@ export async function resolveShell(
|
||||
}
|
||||
|
||||
function profile(path: string): TerminalShell {
|
||||
const name = path.slice(Math.max(path.lastIndexOf('/'), path.lastIndexOf('\\')) + 1)
|
||||
const kind = name.toLowerCase().replace(/\.exe$/u, '')
|
||||
return { path, name, args: kind === 'cmd' ? [] : kind === 'pwsh' || kind === 'powershell' ? ['-NoLogo'] : ['-i'] }
|
||||
const kind = shellKind(path)
|
||||
return { path, name: executableName(path), args: kind === 'cmd' ? [] : kind === 'pwsh' || kind === 'powershell' ? ['-NoLogo'] : ['-i'] }
|
||||
}
|
||||
|
||||
function executableName(path: string): string {
|
||||
return path.slice(Math.max(path.lastIndexOf('/'), path.lastIndexOf('\\')) + 1)
|
||||
}
|
||||
|
||||
function shellKind(path: string): string {
|
||||
return executableName(path).toLowerCase().replace(/\.exe$/u, '')
|
||||
}
|
||||
|
||||
/**
|
||||
* List verified candidates after the configured or environment-default shell.
|
||||
* List verified candidates after the configured or environment-default shell, one per executable name ignoring case and `.exe`.
|
||||
* PATH lookup can reach the default through another directory, such as `/usr/bin/bash` for `/bin/bash` on merged-`/usr` systems.
|
||||
* @param subprocess - target execution provider.
|
||||
* @param configured - optional default profile.
|
||||
* @param candidates - executable names or paths permitted for shell selection.
|
||||
* @param signal - discovery cancellation.
|
||||
* @returns unique installed shells, with the default first; transport failures reject.
|
||||
* @returns installed shells with the default first, keeping the earliest entry per name; transport failures reject.
|
||||
*/
|
||||
export async function discoverShells(
|
||||
subprocess: SubprocessRuntime, configured: TerminalShell | undefined,
|
||||
@@ -50,8 +58,8 @@ export async function discoverShells(
|
||||
const shells = new Map<string, TerminalShell>()
|
||||
for (const shell of [preferred, ...found]) {
|
||||
if (shell === undefined) continue
|
||||
const key = shell.path.includes('\\') ? shell.path.toLowerCase() : shell.path
|
||||
if (!shells.has(key)) shells.set(key, shell)
|
||||
const kind = shellKind(shell.path)
|
||||
if (!shells.has(kind)) shells.set(kind, shell)
|
||||
}
|
||||
return [...shells.values()]
|
||||
}
|
||||
@@ -546,6 +546,16 @@ it('deduplicates Windows executable paths regardless of letter case', async () =
|
||||
expect(await h.controller.shells(h.agent, signal())).toEqual([{ path: 'C:\\Windows\\cmd.exe', name: 'Command Prompt', args: [] }])
|
||||
})
|
||||
|
||||
it('lists each executable name once when PATH reaches the default shell through another directory', async () => {
|
||||
const h = fixture({ shell: undefined, shellCandidates: ['zsh', 'bash'] })
|
||||
// Merged /usr: the login shell is /bin/bash, while PATH lists /usr/bin before the /bin link.
|
||||
h.subprocess.resolveExecutable.mockImplementation(async path => path.startsWith('/') ? path : `/usr/bin/${path}`)
|
||||
expect(await h.controller.shells(h.agent, signal())).toEqual([
|
||||
{ path: '/bin/bash', name: 'bash', args: ['-i'] },
|
||||
{ path: '/usr/bin/zsh', name: 'zsh', args: ['-i'] },
|
||||
])
|
||||
})
|
||||
|
||||
it('retains a known terminal without Agent resolution and fences stream admission after explicit close', async () => {
|
||||
const h = fixture()
|
||||
await h.controller.create(h.agent, request, signal())
|
||||
|
||||
Reference in new issue
Block a user