English | 简体中文
@zhangzhian/dsh-devtools-terminal
DSH 开发工具的右侧栏终端:接管平台内置的 terminal 页签,输出里的 URL 支持
Ctrl/Cmd + 点击在 DSH 右栏的浏览器页签里打开(普通点击仍然是选择/光标,
与 VSCode 终端同一套语义)。
为什么必须接管:平台终端把 xterm 实例和 UI 做进了 @deepseek-ai/dsh-client-ui-sidebar-terminal
的私有懒加载块,第三方插件拿不到实例、也换不掉那块 UI;平台自己提供的干净扩展点是
页签类型注册表——同 kind 的 extension 注册会顶掉内置类型。接管的是「怎么画」,
PTY 仍然是平台的 ctx.webTerminals(并发上限、空闲回收、崩溃恢复、关闭 handler 照旧)。
安装
不要单独装它。装清单包——它会作为依赖自动拉下来,并在插件页上给你一个可开关的组件行:
dsh plugin --profile web add @zhangzhian/dsh-devtools
清单包同时用 browser.patch.yml 打开了平台默认在 web 上关掉的右栏浏览器
(@deepseek-ai/dsh-client-ui-sidebar-browser),所以终端链接点下去就落在那里。
本仓库开发回路:
pnpm install
pnpm --filter @zhangzhian/dsh-devtools-terminal build
node scripts/profile-link.mjs # 把声明了 dsh.client 的包接进 ~/.dsh/profiles/web
node scripts/smoke-terminal-links.mjs # 真浏览器 + 真 PTY 冒烟(链接落在右栏浏览器里)
装完/改完都要让运行中的 dsh web 重新发现插件(重启 dsh web,或等 dsh-hmr 重载)。 不需要改 DSH 安装本身——换机器、换版本、远端 SSH 环境,装一次插件即可。
提供什么
- 右侧栏终端(多页签),悬停 URL 出现下划线 + 手型;
- Ctrl / Cmd + 点击在右栏浏览器页签里打开链接——
ctx.sidebarRight.openTab('browser', { params: { url } }),载体由平台决定: web / 移动端是 iframe,桌面端(dsh-electron 的 carrier)是原生<webview>; 地址栏、后退/前进/刷新和「在系统浏览器中打开」都由那个面板提供; - 右栏浏览器页签不可用时(profile 关掉了那一行、或平台版本没有这个页签类型)
自动回退
window.open(..., 'noopener,noreferrer'):浏览器里是新标签页, 桌面端由 carrier 交给系统浏览器——回退不会吞掉这次点击; - 普通点击不打开(选择/光标原样);
- 折行 URL、宽字符、中文标点、成对括号都按终端真实 buffer 处理;
- OSC 8 超链接走同一套逻辑(替掉 xterm 默认「单击 + confirm + 空白页」的兜底);
- 终端服务缺席时正文显示提示,插件其余部分不崩。
为什么优先右栏浏览器(而不是系统浏览器)
开发工具用户的日常恰好是面板能打开的那一类页面:自己起的 dev server(vite / next /
storybook、后端预览页……),本地回环端口几乎都不发反嵌头。起完服务直接在右栏看,
不用离开 DSH 再开一个浏览器标签页。公网站点仍可能因为 X-Frame-Options /
CSP frame-ancestors 白屏——那是 iframe 的物理上限,用面板工具栏右侧的
「在系统浏览器中打开」(↗)即可。
落点规则是纯函数(src/link-target.ts),单元测试钉住「面板优先 / 面板抛错回退 /
非 http(s) 一律不打开」;真浏览器里的点击语义由 scripts/smoke-terminal-links.mjs
核对(普通点击不开、Cmd/Ctrl+点击在右栏浏览器里打开且 window.open 零次)。
要求
- DSH
0.2.0-rc.2或更新(peer 区间写在package.json里); - 浏览器侧运行时:
dsh.client.platform是web; - 宿主的终端服务(
@deepseek-ai/dsh-api-terminal-controller)默认随dsh web提供; 没有它时正文显示「不可用」,不报错。
English
@zhangzhian/dsh-devtools-terminal
The right-sidebar terminal for DSH devtools: takes over the platform's built-in
terminal tab kind so URLs in terminal output open on Ctrl/Cmd + click in the
DSH right-Sidebar browser tab (a plain click still selects text — the same
semantics as the VS Code terminal).
Why it takes over instead of extending: the platform terminal keeps its xterm
instance and UI inside a private lazy chunk of
@deepseek-ai/dsh-client-ui-sidebar-terminal, so third-party plugins can neither
reach the instance nor replace that UI. The clean extension point the platform
does offer is the tab-type registry: an extension registration with the same
kind takes precedence over the built-in one. Only rendering is taken over; the
PTY still comes from the platform's ctx.webTerminals (concurrency limits, idle
reclaim, crash recovery and the close handler keep working).
Install
Do not install it directly. Install the manifest package — it pulls this in as a dependency and gives you a switchable component row on the plugin page:
dsh plugin --profile web add @zhangzhian/dsh-devtools
The manifest package also enables the right-Sidebar browser
(@deepseek-ai/dsh-client-ui-sidebar-browser) that the platform ships disabled on
Web, through its browser.patch.yml — so a terminal link lands right there.
Local development loop of this repository:
pnpm install
pnpm --filter @zhangzhian/dsh-devtools-terminal build
node scripts/profile-link.mjs # link every package declaring dsh.client into ~/.dsh/profiles/web
node scripts/smoke-terminal-links.mjs # real browser + real PTY smoke (link lands in the Sidebar browser)
After installing or rebuilding, let the running dsh web rediscover the plugin
(restart it, or wait for dsh-hmr to reload). No change to the DSH installation
itself is needed — a new machine, a new version or a remote SSH environment
only needs the plugin installed once.
What it provides
- Right-sidebar terminal (multiple tabs); hovering a URL shows an underline and a pointer cursor;
- Ctrl / Cmd + click opens the link in the right-Sidebar browser tab —
ctx.sidebarRight.openTab('browser', { params: { url } }); the platform picks the carrier: an iframe on Web and mobile, a native<webview>on Desktop (dsh-electron's carrier). Address bar, Back/Forward/Reload and "Open in system browser" all come from that panel; - When the browser tab is unavailable (the profile disabled that row, or the
platform version has no such tab kind) it falls back to
window.open(..., 'noopener,noreferrer')— a new browser tab on the Web, the system browser through the desktop carrier. The fallback never swallows the click; - Plain click does not open (selection and cursor behave as before);
- Wrapped URLs, wide characters, CJK punctuation and balanced brackets are all handled from the real terminal buffer;
- OSC 8 hyperlinks follow the same rules (replacing xterm's default "single click + confirm + blank page" fallback);
- If the terminal service is absent, the body shows a notice and the rest of the plugin keeps working.
Why the Sidebar browser comes first
Dev-server pages are exactly what the panel handles best: vite / next /
storybook and backend preview pages on a loopback port almost never refuse
framing. Start the server and watch it in the Sidebar instead of leaving DSH for
another browser tab. Public sites can still show a blank frame because of
X-Frame-Options / CSP frame-ancestors — that is the physical limit of an
iframe, and the panel's "Open in system browser" (↗) is the way out.
The destination rule is a pure function (src/link-target.ts) with unit tests
pinning "panel first / fall back when the panel throws / never open non-http(s)";
the real click semantics are checked in a real browser by
scripts/smoke-terminal-links.mjs (plain click opens nothing, Ctrl/Cmd+click
opens in the Sidebar browser with zero window.open calls).
Requirements
- DSH
0.2.0-rc.2or newer (peer range lives inpackage.json); - Browser-side runtime:
dsh.client.platformisweb; - The host's terminal service (
@deepseek-ai/dsh-api-terminal-controller) ships withdsh webby default; without it the body shows "unavailable" instead of failing.