@deepseek-ai/dsh-client-ui-open-in-app 0.1.3-alpha.2

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 DeepSeek
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,6 @@
1
+ # Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
2
+ # side as of the last confirmed-consistent state. Both languages carry equal authority;
3
+ # after editing either side, bring the other along and re-record with:
4
+ # pnpm run verify-translation-pairing --write packages/client/ui-open-in-app/README.md
5
+ README.md: d242d5a84dffbf0895485f7c79eafd5e332fe446
6
+ README.zh.md: e50e08ab52ce8462af47d75f09a276674d4f4652
package/README.md ADDED
@@ -0,0 +1,83 @@
1
+ ---
2
+ description: "Web Session-header \"Open In...\" split button: launches the remembered application on the session workspace directory and lists every application the host probed as installed."
3
+ kind: "package-reference"
4
+ ---
5
+
6
+ # @deepseek-ai/dsh-client-ui-open-in-app
7
+
8
+ English | [中文](README.zh.md)
9
+
10
+ ## Summary
11
+
12
+ This package provides the browser surface of the open-in-app feature: a Session-header split button whose main button opens the current session's workspace directory (the summary's `cwd`) in the remembered application, and whose chevron lists every catalog application the host probed as installed. Availability, icons, and launches come from the host routes of [`dsh-host-open-in-app`](../../host/open-in-app/README.md); mount the two packages together. A session without a workspace directory, or a host where nothing nameable is installed, renders no button at all.
13
+
14
+ ## Table of Contents
15
+
16
+ - [Use this package](#use-this-package)
17
+ - [Understand the implementation](#understand-the-implementation)
18
+ - [Further Exploration](#further-exploration)
19
+ - [Model Experience](#model-experience)
20
+ - [Known Limitations and Deferred Work](#known-limitations-and-deferred-work)
21
+ - [Dev Note](#dev-note)
22
+
23
+ -----
24
+
25
+ <a id="use-this-package"></a>
26
+ ## Use this package
27
+
28
+ Mount this plugin in the Web composition beside [`dsh-host-open-in-app`](../../host/open-in-app/README.md); the pair composes the whole feature in two cordis.yml rows and this row takes no config. The Session header grows an "Open In..." split button whenever the host probed at least one installed catalog application and the session has a known workspace directory.
29
+
30
+ ### What to expect
31
+
32
+ The main button shows the remembered application's icon — the real application icon wherever the host extracts one (macOS bundle icons, Windows executable icons, Linux theme icons), a generic glyph where it serves none — and a design-system tooltip ("Open locally"); clicking launches immediately. The chevron opens a dense menu of the installed applications with the remembered one marked by a filled row. Availability is read once per page from the host; the last chosen application persists in the browser (`dsh.open-in-app.choice`), and a choice that is no longer installed falls back to the first available entry. A launch that finishes quickly leaves the button untouched — the dimmed busy treatment appears only after 250 ms in flight — and a failed launch shows the error tooltip and a red outline for two seconds. All copy lives in the bilingual `open-in-app` locale namespace; an application id the dictionaries cannot name is not offered.
33
+
34
+ -----
35
+
36
+ <a id="understand-the-implementation"></a>
37
+ ## Understand the implementation
38
+
39
+ <details>
40
+ <summary>Implementation internals — click to expand</summary>
41
+
42
+ The plugin registers the split button on `conversation.session.header.utilities` through the standard slot/inject currency and registers the `open-in-app` dictionaries as one effect. A page-lifetime controller ([`src/client/controller.ts`](src/client/controller.ts)) owns the once-per-page availability read, the persisted choice snapshot store, and the launch POST; the component receives both stores through the inject `hooks` compartment, so every Session header shares one truth. Route paths and wire payload types are inlined from the host package's browser-safe `@deepseek-ai/dsh-host-open-in-app/shared` subpath. In-flight launches are guarded by a ref — repeat clicks and menu picks during a launch are ignored whole (a pick would otherwise persist a choice the gesture never opened) — and the busy/error dress is timer-driven around the `launch` promise. The node half is an empty `apply` that keeps the plugin on the host roster.
43
+
44
+ </details>
45
+
46
+ -----
47
+
48
+ <a id="further-exploration"></a>
49
+ ## Further Exploration
50
+
51
+ - [dsh-host-open-in-app](../../host/open-in-app/README.md) — the host routes serving availability, icons, and launches, and the catalog behind them.
52
+ - [dsh-session-log-export](../../session-query/session-log-export/README.md) — the sibling Session-header action.
53
+ - [Web client architecture](../../../.agents/notes/implemented/architecture/2026-07-19-gui-web-client-architecture.md) — how browser plugin rows load and register slots.
54
+
55
+ -----
56
+
57
+ <a id="model-experience"></a>
58
+ ## Model Experience
59
+
60
+ None, as the split button is browser chrome; nothing here reaches a model request.
61
+
62
+ #### KV Cache effect
63
+
64
+ None; this package neither assembles nor sends a provider request.
65
+
66
+ ## Known Limitations and Deferred Work
67
+
68
+ <a id="known-limitations-and-deferred-work"></a>
69
+
70
+ - **The dictionaries gate the menu.** A host catalog extension without a matching `app.<id>` entry in both dictionaries stays invisible instead of showing a raw id; extending the catalog means extending [`dsh-host-open-in-app`](../../host/open-in-app/README.md) and this package's locales together.
71
+ - **Availability is read once per page.** An application installed while the page is open appears after a reload (and, host-side, after a host restart).
72
+
73
+ <a id="dev-note"></a>
74
+ ### Dev Note
75
+
76
+ <details>
77
+ <summary>Working context for maintainers — click to expand</summary>
78
+
79
+ The feature-level decisions, including the split into the host package and this surface, are recorded in the [promotion Agent Note](../../../.agents/notes/implemented/feature/2026-08-25-promote-open-anywhere-plugin.md).
80
+
81
+ </details>
82
+
83
+ **Runtime invariant:** No companion is published. The plugin registers one dictionary effect and one header-slot entry whose disposal the HMR-safety spec proves; availability and choice live in the controller's snapshot stores with no second copy to diverge.
package/README.zh.md ADDED
@@ -0,0 +1,83 @@
1
+ ---
2
+ description: "Web 会话头部 \"Open In...\" 分体按钮:在记住的应用中打开会话 workspace 目录,并列出主机探测到已安装的全部应用。"
3
+ kind: "package-reference"
4
+ ---
5
+
6
+ # @deepseek-ai/dsh-client-ui-open-in-app
7
+
8
+ [English](README.md) | 中文
9
+
10
+ ## 概述
11
+
12
+ 本包提供 open-in-app 功能的浏览器表面:会话头部的一个分体按钮,主按钮在记住的应用中打开当前会话的 workspace 目录(会话摘要的 `cwd`),下拉箭头列出主机探测到已安装的全部目录应用。可用性、图标与启动均来自 [`dsh-host-open-in-app`](../../host/open-in-app/README.zh.md) 的主机路由;两个包应一起挂载。没有 workspace 目录的会话、或没装任何可命名应用的主机,完全不渲染按钮。
13
+
14
+ ## 目录
15
+
16
+ - [使用本包](#use-this-package)
17
+ - [理解实现](#understand-the-implementation)
18
+ - [进一步探索](#further-exploration)
19
+ - [模型体验](#model-experience)
20
+ - [已知限制与延后工作](#known-limitations-and-deferred-work)
21
+ - [开发备注](#dev-note)
22
+
23
+ -----
24
+
25
+ <a id="use-this-package"></a>
26
+ ## 使用本包
27
+
28
+ 把本插件与 [`dsh-host-open-in-app`](../../host/open-in-app/README.zh.md) 并排挂进 Web 组合;这对包用两行 cordis.yml 组成完整功能,本行不接受任何 config。只要主机探测到至少一个已安装的目录应用且会话有已知的 workspace 目录,会话头部就会出现 "Open In..." 分体按钮。
29
+
30
+ ### 预期行为
31
+
32
+ 主按钮显示记住的应用图标——凡主机能提取的都是应用真实图标(macOS bundle 图标、Windows 可执行文件图标、Linux 主题图标),提取不到时是通用占位图形——并带设计系统 tooltip(「在本地打开」);点击立即启动。下拉箭头打开已安装应用的紧凑菜单,记住的条目以整行填充标记。可用性每页读取一次;上次选择的应用持久化在浏览器中(`dsh.open-in-app.choice`),不再安装的选择回退到第一个可用条目。快速完成的启动不改变按钮外观——变暗的等待态只在飞行超过 250 毫秒后出现——失败的启动显示错误 tooltip 与红色描边两秒。所有文案在双语 `open-in-app` locale 命名空间中;词典无法命名的应用 id 不会被提供。
33
+
34
+ -----
35
+
36
+ <a id="understand-the-implementation"></a>
37
+ ## 理解实现
38
+
39
+ <details>
40
+ <summary>实现内幕——点击展开</summary>
41
+
42
+ 插件经标准 slot/inject 通货把分体按钮注册到 `conversation.session.header.utilities`,并以一个 effect 注册 `open-in-app` 词典。一个页面生命周期的 controller([`src/client/controller.ts`](src/client/controller.ts))拥有每页一次的可用性读取、持久化选择的 snapshot store 与启动 POST;组件经 inject 的 `hooks` 隔间接收两个 store,因此所有会话头部共享同一份事实。路由路径与 wire 载荷类型从主机包的浏览器安全子路径 `@deepseek-ai/dsh-host-open-in-app/shared` 内联。飞行中的启动由 ref 守卫——启动期间的重复点击与菜单选择被整体忽略(否则会持久化一个该手势从未打开的选择)——busy/error 视觉由围绕 `launch` promise 的定时器驱动。节点半边是一个空 `apply`,让插件出现在主机侧的插件名册上。
43
+
44
+ </details>
45
+
46
+ -----
47
+
48
+ <a id="further-exploration"></a>
49
+ ## 进一步探索
50
+
51
+ - [dsh-host-open-in-app](../../host/open-in-app/README.zh.md)——提供可用性、图标与启动的主机路由,及其背后的目录。
52
+ - [dsh-session-log-export](../../session-query/session-log-export/README.zh.md)——会话头部的姊妹动作。
53
+ - [Web client 架构](../../../.agents/notes/implemented/architecture/2026-07-19-gui-web-client-architecture.zh.md)——浏览器插件行如何加载并注册 slot。
54
+
55
+ -----
56
+
57
+ <a id="model-experience"></a>
58
+ ## 模型体验
59
+
60
+ 无。分体按钮是浏览器 chrome;这里没有任何东西进入模型请求。
61
+
62
+ #### KV 缓存影响
63
+
64
+ 无;本包从不组装或发送 provider 请求。
65
+
66
+ ## 已知限制与延后工作
67
+
68
+ <a id="known-limitations-and-deferred-work"></a>
69
+
70
+ - **词典把守菜单。** 主机目录的新条目若在两份词典中没有对应的 `app.<id>` 条目,将保持不可见而不是显示裸 id;扩展目录意味着同时扩展 [`dsh-host-open-in-app`](../../host/open-in-app/README.zh.md) 与本包的 locale。
71
+ - **可用性每页只读一次。** 页面打开期间安装的应用要重新加载页面后才出现(主机侧还需主机重启)。
72
+
73
+ <a id="dev-note"></a>
74
+ ### 开发备注
75
+
76
+ <details>
77
+ <summary>维护者工作语境——点击展开</summary>
78
+
79
+ 功能层面的各项决定,包括拆分为主机包与本表面包,记录在[转正 Agent Note](../../../.agents/notes/implemented/feature/2026-08-25-promote-open-anywhere-plugin.zh.md)。
80
+
81
+ </details>
82
+
83
+ **运行时不变量:** 不发布 companion。插件注册一个词典 effect 与一个头部 slot 条目,HMR 安全测试已证明其可处置;可用性与选择存于 controller 的 snapshot store,没有可能分叉的第二份副本。
package/lib/client.js ADDED
@@ -0,0 +1,431 @@
1
+ window.__ModuleLoader__.load({
2
+ id: "@deepseek-ai/dsh-client-ui-open-in-app",
3
+ factory: (require) => {
4
+ var module = { exports: {} };
5
+ var exports = module.exports;
6
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
7
+ let _deepseek_ai_dsh_client_store = require("@deepseek-ai/dsh-client-store");
8
+ let react_jsx_runtime = require("react/jsx-runtime");
9
+ let react = require("react");
10
+ let _deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
11
+ //#region ../../host/open-in-app/src/shared.ts
12
+ /**
13
+ * Route paths and wire payloads shared verbatim by the host routes and the
14
+ * browser package (`@deepseek-ai/dsh-client-ui-open-in-app`), published as
15
+ * the `./shared` subpath. Browser-safe: constants and types only.
16
+ */
17
+ /** GET route serving the probed application ids. */
18
+ const OPEN_IN_APP_APPS_ROUTE = "/open-in-app/apps";
19
+ /** GET prefix serving one PNG bundle icon per application id. */
20
+ const OPEN_IN_APP_ICON_PREFIX = "/open-in-app/icon";
21
+ /** POST route launching one application on one workspace directory. */
22
+ const OPEN_IN_APP_OPEN_ROUTE = "/open-in-app/open";
23
+ //#endregion
24
+ //#region lib/types/client/controller.js
25
+ /** Browser availability/choice state and the launch carrier for the split button. */
26
+ /** Resolve the browser's Host base with the connection carrier's null-origin fallback. */
27
+ function hostBase() {
28
+ const origin = globalThis.location?.origin;
29
+ return origin !== void 0 && origin !== "null" ? origin : "http://dsh.internal";
30
+ }
31
+ /**
32
+ * Owns the once-per-page availability read, the persisted last choice, and
33
+ * the launch POST. Availability and choice publish through uSES-safe sources
34
+ * so every Session header shares one truth.
35
+ */
36
+ var OpenInAppController = class {
37
+ fetcher;
38
+ /** Installed app ids in host menu order; null until the host answered. */
39
+ apps = (0, _deepseek_ai_dsh_client_store.createSnapshotStore)(null);
40
+ /** Last chosen app id, or empty before the first choice, shared across sessions and browser restarts. */
41
+ choice = (0, _deepseek_ai_dsh_client_store.createSnapshotStore)("", { persist: { name: "dsh.open-in-app.choice" } });
42
+ loading;
43
+ /**
44
+ * @param fetcher - HTTP carrier for the apps read and the launch POST.
45
+ */
46
+ constructor(fetcher = (input, init) => fetch(input, init)) {
47
+ this.fetcher = fetcher;
48
+ }
49
+ /**
50
+ * Read availability once per controller life; concurrent calls share the read.
51
+ * A failed read publishes an empty list, which renders no button at all.
52
+ * @returns after availability is published.
53
+ */
54
+ load() {
55
+ this.loading ??= this.run();
56
+ return this.loading;
57
+ }
58
+ /**
59
+ * Remember one picked app id.
60
+ * @param appId - catalog id from the availability list.
61
+ */
62
+ choose(appId) {
63
+ this.choice.set(appId);
64
+ }
65
+ /**
66
+ * Launch one installed app on a workspace directory.
67
+ * @param appId - catalog id from the availability list.
68
+ * @param path - the session's absolute workspace directory.
69
+ * @returns after the host acknowledged the launch; rejects on any failure.
70
+ */
71
+ async launch(appId, path) {
72
+ const body = {
73
+ app: appId,
74
+ path
75
+ };
76
+ const response = await this.fetcher(new URL(OPEN_IN_APP_OPEN_ROUTE, hostBase()), {
77
+ method: "POST",
78
+ headers: { "content-type": "application/json" },
79
+ body: JSON.stringify(body)
80
+ });
81
+ if (!response.ok) throw new Error(`open failed: HTTP ${String(response.status)}`);
82
+ }
83
+ async run() {
84
+ let apps = [];
85
+ try {
86
+ const response = await this.fetcher(new URL(OPEN_IN_APP_APPS_ROUTE, hostBase()), { headers: { accept: "application/json" } });
87
+ if (response.ok) {
88
+ const payload = await response.json();
89
+ if (Array.isArray(payload.apps)) apps = payload.apps.filter((id) => typeof id === "string");
90
+ }
91
+ } catch {}
92
+ this.apps.set(apps);
93
+ }
94
+ };
95
+ //#endregion
96
+ //#region \0dsh-css:/home/runner/work/deepseek-harness/deepseek-harness/packages/client/ui-open-in-app/src/client/OpenInAppAction.module.css.mjs
97
+ const css = ".CAgGvG_split{box-sizing:border-box;border:.5px solid var(--dsw-alias-border-l4);height:26px;font-family:var(--dsw-font-family);border-radius:13px;align-items:stretch;display:inline-flex;overflow:hidden}.CAgGvG_main,.CAgGvG_chevron{color:var(--dsw-alias-label-primary);cursor:pointer;white-space:nowrap;background:0 0;border:0;align-items:center;gap:5px;font-size:11px;font-weight:400;line-height:16px;display:inline-flex}.CAgGvG_main{padding:5px 6px 5px 7px}.CAgGvG_main:hover:not(:disabled),.CAgGvG_main:focus-visible,.CAgGvG_chevron:hover,.CAgGvG_chevron:focus-visible{background:var(--dsw-alias-interactive-bg-hover)}.CAgGvG_main:disabled{color:var(--dsw-alias-label-dimmed);cursor:wait}.CAgGvG_main[data-state=error]{color:var(--dsw-alias-state-error-primary);box-shadow:inset 0 0 0 1px var(--dsw-alias-state-error-primary)}.CAgGvG_chevron{border-left:.5px solid var(--dsw-alias-border-l4);color:var(--dsw-alias-label-secondary);padding:5px 6px 5px 4px}.CAgGvG_icon{flex:none}img.CAgGvG_icon{object-fit:contain;user-select:none;display:block}";
98
+ const tagId = "@deepseek-ai/dsh-client-ui-open-in-app/OpenInAppAction.module.css";
99
+ if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
100
+ const tag = document.createElement("style");
101
+ tag.dataset.plugin = "@deepseek-ai/dsh-client-ui-open-in-app";
102
+ tag.dataset.pluginCss = tagId;
103
+ tag.textContent = css;
104
+ document.head.appendChild(tag);
105
+ }
106
+ var OpenInAppAction_module_css_default = {
107
+ "chevron": "CAgGvG_chevron",
108
+ "icon": "CAgGvG_icon",
109
+ "main": "CAgGvG_main",
110
+ "split": "CAgGvG_split"
111
+ };
112
+ //#endregion
113
+ //#region lib/types/client/OpenInAppAction.js
114
+ /**
115
+ * Label keys per catalog id: the browser renders only ids it can name, so a
116
+ * host catalog extension without a matching dictionary entry stays invisible
117
+ * instead of showing a raw id.
118
+ */
119
+ const APP_LABEL_KEY = {
120
+ finder: "app.finder",
121
+ explorer: "app.explorer",
122
+ filemanager: "app.filemanager",
123
+ cursor: "app.cursor",
124
+ vscode: "app.vscode",
125
+ vscodeinsiders: "app.vscodeinsiders",
126
+ windsurf: "app.windsurf",
127
+ zed: "app.zed",
128
+ sublimetext: "app.sublimetext",
129
+ xcode: "app.xcode",
130
+ androidstudio: "app.androidstudio",
131
+ intellij: "app.intellij",
132
+ pycharm: "app.pycharm",
133
+ webstorm: "app.webstorm",
134
+ phpstorm: "app.phpstorm",
135
+ goland: "app.goland",
136
+ rider: "app.rider",
137
+ rustrover: "app.rustrover",
138
+ fork: "app.fork",
139
+ sourcetree: "app.sourcetree",
140
+ github: "app.github",
141
+ tower: "app.tower",
142
+ gitkraken: "app.gitkraken",
143
+ smartgit: "app.smartgit",
144
+ sublimemerge: "app.sublimemerge",
145
+ ghostty: "app.ghostty",
146
+ warp: "app.warp",
147
+ iterm: "app.iterm",
148
+ kitty: "app.kitty",
149
+ terminal: "app.terminal",
150
+ windowsterminal: "app.windowsterminal",
151
+ gitbash: "app.gitbash",
152
+ gnometerminal: "app.gnometerminal",
153
+ konsole: "app.konsole"
154
+ };
155
+ /** App ids whose icon image already failed this page; a 404 icon is fetched once, not per menu open. */
156
+ const failedIcons = /* @__PURE__ */ new Set();
157
+ /**
158
+ * One application's real bundle icon (host-served PNG) with an inline generic
159
+ * app-square fallback while the host has none.
160
+ * @param props - catalog id, host icon URL, and rendered size.
161
+ * @returns the icon image or its fallback glyph.
162
+ */
163
+ function AppIcon({ id, url, size }) {
164
+ const [failed, setFailed] = (0, react.useState)(failedIcons.has(id));
165
+ if (failed) return (0, react_jsx_runtime.jsx)("svg", {
166
+ width: size,
167
+ height: size,
168
+ viewBox: "0 0 24 24",
169
+ fill: "none",
170
+ stroke: "currentColor",
171
+ strokeWidth: 1.8,
172
+ className: OpenInAppAction_module_css_default.icon,
173
+ "aria-hidden": true,
174
+ children: (0, react_jsx_runtime.jsx)("rect", {
175
+ x: 3,
176
+ y: 3,
177
+ width: 18,
178
+ height: 18,
179
+ rx: 5
180
+ })
181
+ });
182
+ return (0, react_jsx_runtime.jsx)("img", {
183
+ src: url,
184
+ width: size,
185
+ height: size,
186
+ className: OpenInAppAction_module_css_default.icon,
187
+ alt: "",
188
+ "aria-hidden": true,
189
+ draggable: false,
190
+ onError: () => {
191
+ failedIcons.add(id);
192
+ setFailed(true);
193
+ }
194
+ });
195
+ }
196
+ /**
197
+ * Quick launches settle well under this delay, so their busy dress never
198
+ * paints — the visible dim-and-wait treatment is reserved for launches that
199
+ * are actually taking a while, instead of flashing on every click.
200
+ */
201
+ const BUSY_DRESS_DELAY_MS = 250;
202
+ /**
203
+ * Session-header split button: the main button opens the session's workspace
204
+ * directory in the remembered application, the chevron opens the menu of
205
+ * every application the host probed as installed. It renders nothing until
206
+ * the host reported at least one nameable application and the session has a
207
+ * known workspace directory, so a host without the capability never grows
208
+ * the control.
209
+ * @param props - session runtime, injected controller face, and localized copy.
210
+ * @returns the split button and its menu, or null when there is nothing to offer.
211
+ */
212
+ function OpenInAppAction(props) {
213
+ const { sessionId, useSessions, useOpenInAppApps, useOpenInAppChoice, t } = props;
214
+ const cwd = useSessions((state) => state.byId[sessionId]?.cwd);
215
+ const available = useOpenInAppApps((apps) => apps);
216
+ const choice = useOpenInAppChoice((id) => id);
217
+ const [open, setOpen] = (0, react.useState)(false);
218
+ const [phase, setPhase] = (0, react.useState)("idle");
219
+ const inFlight = (0, react.useRef)(false);
220
+ const busyTimer = (0, react.useRef)(void 0);
221
+ const errorTimer = (0, react.useRef)(void 0);
222
+ (0, react.useEffect)(() => () => {
223
+ clearTimeout(busyTimer.current);
224
+ clearTimeout(errorTimer.current);
225
+ }, []);
226
+ const apps = (available ?? []).map((id) => ({
227
+ id,
228
+ labelKey: APP_LABEL_KEY[id]
229
+ })).filter((entry) => entry.labelKey !== void 0);
230
+ const currentEntry = apps.find((entry) => entry.id === choice) ?? apps[0];
231
+ if (currentEntry === void 0 || cwd === void 0 || cwd === "") return null;
232
+ const current = currentEntry.id;
233
+ const currentLabel = t(currentEntry.labelKey);
234
+ const title = phase === "error" ? t("open.error") : t("open.title", { app: currentLabel });
235
+ const launch = (appId) => {
236
+ if (inFlight.current) return;
237
+ inFlight.current = true;
238
+ clearTimeout(errorTimer.current);
239
+ clearTimeout(busyTimer.current);
240
+ busyTimer.current = setTimeout(() => {
241
+ setPhase("busy");
242
+ }, BUSY_DRESS_DELAY_MS);
243
+ props.launch(appId, cwd).then(() => {
244
+ inFlight.current = false;
245
+ clearTimeout(busyTimer.current);
246
+ setPhase("idle");
247
+ }, () => {
248
+ inFlight.current = false;
249
+ clearTimeout(busyTimer.current);
250
+ setPhase("error");
251
+ clearTimeout(errorTimer.current);
252
+ errorTimer.current = setTimeout(() => {
253
+ setPhase("idle");
254
+ }, 2e3);
255
+ });
256
+ };
257
+ return (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Menu, {
258
+ open,
259
+ align: "end",
260
+ dense: true,
261
+ selection: "fill",
262
+ onClose: () => {
263
+ setOpen(false);
264
+ },
265
+ items: apps.map((entry) => ({
266
+ id: entry.id,
267
+ label: t(entry.labelKey),
268
+ icon: (0, react_jsx_runtime.jsx)(AppIcon, {
269
+ id: entry.id,
270
+ url: props.iconUrl(entry.id),
271
+ size: 18
272
+ })
273
+ })),
274
+ selectedId: current,
275
+ onSelect: (id) => {
276
+ setOpen(false);
277
+ if (inFlight.current) return;
278
+ props.choose(id);
279
+ launch(id);
280
+ },
281
+ anchor: (0, react_jsx_runtime.jsxs)("div", {
282
+ className: OpenInAppAction_module_css_default.split,
283
+ children: [(0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tooltip, {
284
+ label: phase === "error" ? t("open.error") : t("open.tooltip"),
285
+ side: "bottom",
286
+ children: (0, react_jsx_runtime.jsx)("button", {
287
+ type: "button",
288
+ className: OpenInAppAction_module_css_default.main,
289
+ "data-state": phase,
290
+ disabled: phase === "busy",
291
+ "aria-label": title,
292
+ onClick: () => {
293
+ launch(current);
294
+ },
295
+ children: (0, react_jsx_runtime.jsx)(AppIcon, {
296
+ id: current,
297
+ url: props.iconUrl(current),
298
+ size: 15
299
+ })
300
+ })
301
+ }), (0, react_jsx_runtime.jsx)("button", {
302
+ type: "button",
303
+ className: OpenInAppAction_module_css_default.chevron,
304
+ "aria-expanded": open,
305
+ "aria-haspopup": "menu",
306
+ title: t("menu.toggle"),
307
+ "aria-label": t("menu.toggle"),
308
+ onClick: () => {
309
+ setOpen((value) => !value);
310
+ },
311
+ children: (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutline14, { size: 11 })
312
+ })]
313
+ })
314
+ });
315
+ }
316
+ //#endregion
317
+ //#region lib/types/client/locales.js
318
+ /** `open-in-app` namespace dictionaries. */
319
+ /** Dictionary namespace owned by this plugin. */
320
+ const NS = "open-in-app";
321
+ /** Application labels shared verbatim by both dictionaries (product names). */
322
+ const PRODUCT_NAMES = {
323
+ "app.cursor": "Cursor",
324
+ "app.vscode": "VS Code",
325
+ "app.vscodeinsiders": "VS Code Insiders",
326
+ "app.windsurf": "Windsurf",
327
+ "app.zed": "Zed",
328
+ "app.sublimetext": "Sublime Text",
329
+ "app.xcode": "Xcode",
330
+ "app.androidstudio": "Android Studio",
331
+ "app.intellij": "IntelliJ IDEA",
332
+ "app.pycharm": "PyCharm",
333
+ "app.webstorm": "WebStorm",
334
+ "app.phpstorm": "PhpStorm",
335
+ "app.goland": "GoLand",
336
+ "app.rider": "Rider",
337
+ "app.rustrover": "RustRover",
338
+ "app.fork": "Fork",
339
+ "app.sourcetree": "Sourcetree",
340
+ "app.github": "GitHub Desktop",
341
+ "app.tower": "Tower",
342
+ "app.gitkraken": "GitKraken",
343
+ "app.smartgit": "SmartGit",
344
+ "app.sublimemerge": "Sublime Merge",
345
+ "app.ghostty": "Ghostty",
346
+ "app.warp": "Warp",
347
+ "app.iterm": "iTerm2",
348
+ "app.kitty": "kitty",
349
+ "app.windowsterminal": "Windows Terminal",
350
+ "app.gitbash": "Git Bash",
351
+ "app.gnometerminal": "GNOME Terminal",
352
+ "app.konsole": "Konsole"
353
+ };
354
+ /** Simplified Chinese dictionary (the key-set source of truth). */
355
+ const zh = {
356
+ "open.title": "在 {app} 中打开工作目录",
357
+ "open.tooltip": "在本地打开",
358
+ "open.error": "打开失败",
359
+ "menu.toggle": "选择打开方式",
360
+ "menu.aria": "打开方式",
361
+ ...PRODUCT_NAMES,
362
+ "app.finder": "访达",
363
+ "app.explorer": "文件资源管理器",
364
+ "app.filemanager": "文件管理器",
365
+ "app.terminal": "终端"
366
+ };
367
+ /** English dictionary, key-identical to the Chinese source of truth. */
368
+ const en = {
369
+ "open.title": "Open workspace in {app}",
370
+ "open.tooltip": "Open locally",
371
+ "open.error": "Failed to open",
372
+ "menu.toggle": "Choose an app to open in",
373
+ "menu.aria": "Open in",
374
+ ...PRODUCT_NAMES,
375
+ "app.finder": "Finder",
376
+ "app.explorer": "File Explorer",
377
+ "app.filemanager": "Files",
378
+ "app.terminal": "Terminal"
379
+ };
380
+ //#endregion
381
+ //#region lib/types/client/index.js
382
+ /**
383
+ * Browser half of open-in-app: one Session-header split button opening the
384
+ * session's workspace directory (the summary's `cwd`) in the remembered
385
+ * installed application. Availability arrives once per page from the host
386
+ * apps route; the last choice persists in the browser through the controller's
387
+ * persisted snapshot store.
388
+ */
389
+ /** Required services for locale registration and the header-slot contribution. */
390
+ const inject = [
391
+ "sessions",
392
+ "slots",
393
+ "locale"
394
+ ];
395
+ /**
396
+ * Client plugin body: register the dictionaries and the header split button.
397
+ * @param ctx - client root context.
398
+ */
399
+ function apply(ctx) {
400
+ const controller = new OpenInAppController();
401
+ controller.load();
402
+ ctx.effect(() => ctx.locale.register(NS, {
403
+ zh,
404
+ en
405
+ }), "open-in-app: dictionaries");
406
+ ctx.slots.inject("conversation.session.header.utilities", () => ctx.slots.register({
407
+ name: "conversation.session.header.utilities",
408
+ id: "open-in-app",
409
+ order: -10,
410
+ locale: NS,
411
+ inject: () => ({
412
+ hooks: {
413
+ openInAppApps: controller.apps,
414
+ openInAppChoice: controller.choice
415
+ },
416
+ launch: (appId, path) => controller.launch(appId, path),
417
+ choose: (appId) => {
418
+ controller.choose(appId);
419
+ },
420
+ iconUrl: (appId) => `${OPEN_IN_APP_ICON_PREFIX}/${appId}`
421
+ })
422
+ }, OpenInAppAction));
423
+ }
424
+ //#endregion
425
+ exports.apply = apply;
426
+ exports.inject = inject;
427
+ return module.exports;
428
+ }
429
+ });
430
+
431
+ //# sourceMappingURL=client.js.map
package/lib/index.js ADDED
@@ -0,0 +1,12 @@
1
+ //#region lib/types/index.js
2
+ /**
3
+ * Open-in-app browsing surface, node half. Pure UI plugin: the empty apply
4
+ * exists so the plugin appears in the host cordis.yml / Loader; the browser
5
+ * half ships via exports["./client"], discovered through the package.json
6
+ * dsh.client declaration. The routes it drives live in
7
+ * `@deepseek-ai/dsh-host-open-in-app`.
8
+ */
9
+ /** Host plugin body — no host-side behavior for this surface plugin. */
10
+ function apply() {}
11
+ //#endregion
12
+ export { apply };
@@ -0,0 +1,27 @@
1
+ import type { ObservableSnapshot } from '@deepseek-ai/dsh-client-store';
2
+ import type { InjectFace, PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots';
3
+ import { NS } from './locales.ts';
4
+ /** Browser operations and state injected into the Session Header contribution. */
5
+ export interface OpenInAppActionInjected {
6
+ hooks: {
7
+ openInAppApps: ObservableSnapshot<readonly string[] | null>;
8
+ openInAppChoice: ObservableSnapshot<string>;
9
+ };
10
+ launch: (appId: string, path: string) => Promise<void>;
11
+ choose: (appId: string) => void;
12
+ iconUrl: (appId: string) => string;
13
+ }
14
+ /** Full props for the Session-header open-in-app split button. */
15
+ export type OpenInAppActionProps = PropsRuntime<'conversation.session.header.utilities'> & PropsLocale<typeof NS> & InjectFace<OpenInAppActionInjected>;
16
+ /**
17
+ * Session-header split button: the main button opens the session's workspace
18
+ * directory in the remembered application, the chevron opens the menu of
19
+ * every application the host probed as installed. It renders nothing until
20
+ * the host reported at least one nameable application and the session has a
21
+ * known workspace directory, so a host without the capability never grows
22
+ * the control.
23
+ * @param props - session runtime, injected controller face, and localized copy.
24
+ * @returns the split button and its menu, or null when there is nothing to offer.
25
+ */
26
+ export declare function OpenInAppAction(props: OpenInAppActionProps): React.JSX.Element | null;
27
+ //# sourceMappingURL=OpenInAppAction.d.ts.map
@@ -0,0 +1,41 @@
1
+ /** Browser availability/choice state and the launch carrier for the split button. */
2
+ import { type SnapshotStore } from '@deepseek-ai/dsh-client-store';
3
+ type Fetch = (input: string | URL, init?: RequestInit) => Promise<Response>;
4
+ /**
5
+ * Owns the once-per-page availability read, the persisted last choice, and
6
+ * the launch POST. Availability and choice publish through uSES-safe sources
7
+ * so every Session header shares one truth.
8
+ */
9
+ export declare class OpenInAppController {
10
+ private readonly fetcher;
11
+ /** Installed app ids in host menu order; null until the host answered. */
12
+ readonly apps: SnapshotStore<readonly string[] | null>;
13
+ /** Last chosen app id, or empty before the first choice, shared across sessions and browser restarts. */
14
+ readonly choice: SnapshotStore<string>;
15
+ private loading;
16
+ /**
17
+ * @param fetcher - HTTP carrier for the apps read and the launch POST.
18
+ */
19
+ constructor(fetcher?: Fetch);
20
+ /**
21
+ * Read availability once per controller life; concurrent calls share the read.
22
+ * A failed read publishes an empty list, which renders no button at all.
23
+ * @returns after availability is published.
24
+ */
25
+ load(): Promise<void>;
26
+ /**
27
+ * Remember one picked app id.
28
+ * @param appId - catalog id from the availability list.
29
+ */
30
+ choose(appId: string): void;
31
+ /**
32
+ * Launch one installed app on a workspace directory.
33
+ * @param appId - catalog id from the availability list.
34
+ * @param path - the session's absolute workspace directory.
35
+ * @returns after the host acknowledged the launch; rejects on any failure.
36
+ */
37
+ launch(appId: string, path: string): Promise<void>;
38
+ private run;
39
+ }
40
+ export {};
41
+ //# sourceMappingURL=controller.d.ts.map
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Browser half of open-in-app: one Session-header split button opening the
3
+ * session's workspace directory (the summary's `cwd`) in the remembered
4
+ * installed application. Availability arrives once per page from the host
5
+ * apps route; the last choice persists in the browser through the controller's
6
+ * persisted snapshot store.
7
+ */
8
+ import type { Context as ClientContext } from '@deepseek-ai/cordis';
9
+ import { type OpenInAppKey } from './locales.ts';
10
+ declare module '@deepseek-ai/dsh-client-ui-slots' {
11
+ interface LocaleNamespaceMap {
12
+ /** Session-header "open workspace in application" copy. */
13
+ 'open-in-app': OpenInAppKey;
14
+ }
15
+ }
16
+ export type { OpenInAppActionInjected, OpenInAppActionProps } from './OpenInAppAction.tsx';
17
+ /** Required services for locale registration and the header-slot contribution. */
18
+ export declare const inject: string[];
19
+ /**
20
+ * Client plugin body: register the dictionaries and the header split button.
21
+ * @param ctx - client root context.
22
+ */
23
+ export declare function apply(ctx: ClientContext): void;
24
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,50 @@
1
+ /** `open-in-app` namespace dictionaries. */
2
+ /** Dictionary namespace owned by this plugin. */
3
+ export declare const NS = "open-in-app";
4
+ /** Simplified Chinese dictionary (the key-set source of truth). */
5
+ export declare const zh: {
6
+ readonly 'app.finder': "访达";
7
+ readonly 'app.explorer': "文件资源管理器";
8
+ readonly 'app.filemanager': "文件管理器";
9
+ readonly 'app.terminal': "终端";
10
+ readonly 'app.cursor': "Cursor";
11
+ readonly 'app.vscode': "VS Code";
12
+ readonly 'app.vscodeinsiders': "VS Code Insiders";
13
+ readonly 'app.windsurf': "Windsurf";
14
+ readonly 'app.zed': "Zed";
15
+ readonly 'app.sublimetext': "Sublime Text";
16
+ readonly 'app.xcode': "Xcode";
17
+ readonly 'app.androidstudio': "Android Studio";
18
+ readonly 'app.intellij': "IntelliJ IDEA";
19
+ readonly 'app.pycharm': "PyCharm";
20
+ readonly 'app.webstorm': "WebStorm";
21
+ readonly 'app.phpstorm': "PhpStorm";
22
+ readonly 'app.goland': "GoLand";
23
+ readonly 'app.rider': "Rider";
24
+ readonly 'app.rustrover': "RustRover";
25
+ readonly 'app.fork': "Fork";
26
+ readonly 'app.sourcetree': "Sourcetree";
27
+ readonly 'app.github': "GitHub Desktop";
28
+ readonly 'app.tower': "Tower";
29
+ readonly 'app.gitkraken': "GitKraken";
30
+ readonly 'app.smartgit': "SmartGit";
31
+ readonly 'app.sublimemerge': "Sublime Merge";
32
+ readonly 'app.ghostty': "Ghostty";
33
+ readonly 'app.warp': "Warp";
34
+ readonly 'app.iterm': "iTerm2";
35
+ readonly 'app.kitty': "kitty";
36
+ readonly 'app.windowsterminal': "Windows Terminal";
37
+ readonly 'app.gitbash': "Git Bash";
38
+ readonly 'app.gnometerminal': "GNOME Terminal";
39
+ readonly 'app.konsole': "Konsole";
40
+ readonly 'open.title': "在 {app} 中打开工作目录";
41
+ readonly 'open.tooltip': "在本地打开";
42
+ readonly 'open.error': "打开失败";
43
+ readonly 'menu.toggle': "选择打开方式";
44
+ readonly 'menu.aria': "打开方式";
45
+ };
46
+ /** English dictionary, key-identical to the Chinese source of truth. */
47
+ export declare const en: Record<OpenInAppKey, string>;
48
+ /** Key domain of the `open-in-app` namespace (zh is the source of truth). */
49
+ export type OpenInAppKey = keyof typeof zh;
50
+ //# sourceMappingURL=locales.d.ts.map
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Open-in-app browsing surface, node half. Pure UI plugin: the empty apply
3
+ * exists so the plugin appears in the host cordis.yml / Loader; the browser
4
+ * half ships via exports["./client"], discovered through the package.json
5
+ * dsh.client declaration. The routes it drives live in
6
+ * `@deepseek-ai/dsh-host-open-in-app`.
7
+ */
8
+ /** Host plugin body — no host-side behavior for this surface plugin. */
9
+ export declare function apply(): void;
10
+ //# sourceMappingURL=index.d.ts.map
package/package.json ADDED
@@ -0,0 +1,68 @@
1
+ {
2
+ "name": "@deepseek-ai/dsh-client-ui-open-in-app",
3
+ "description": "Web Session-header \"Open In...\" split button opening the session workspace directory in a locally installed application",
4
+ "version": "0.1.3-alpha.2",
5
+ "publishConfig": {
6
+ "access": "public"
7
+ },
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/deepseek-ai/deepseek-harness.git",
11
+ "directory": "packages/client/ui-open-in-app"
12
+ },
13
+ "type": "module",
14
+ "main": "lib/index.js",
15
+ "types": "lib/types/index.d.ts",
16
+ "exports": {
17
+ ".": {
18
+ "types": "./lib/types/index.d.ts",
19
+ "default": "./lib/index.js"
20
+ },
21
+ "./client": {
22
+ "types": "./lib/types/client/index.d.ts",
23
+ "default": "./lib/client.js"
24
+ },
25
+ "./src/*": "./src/*",
26
+ "./package.json": "./package.json"
27
+ },
28
+ "files": [
29
+ "lib/index.js",
30
+ "lib/client.js",
31
+ "lib/types/**/*.d.ts"
32
+ ],
33
+ "license": "MIT",
34
+ "dsh": {
35
+ "client": {
36
+ "inject": [
37
+ "@deepseek-ai/dsh-client-locale",
38
+ "@deepseek-ai/dsh-client-ui-conversation",
39
+ "@deepseek-ai/dsh-client-ui-renderer",
40
+ "@deepseek-ai/dsh-client-ui-session"
41
+ ],
42
+ "platform": "web"
43
+ }
44
+ },
45
+ "peerDependencies": {
46
+ "@deepseek-ai/cordis": "^4.0.2"
47
+ },
48
+ "devDependencies": {
49
+ "@types/react": "~18.3.1",
50
+ "react": "^18.2.0",
51
+ "@deepseek-ai/cordis": "^4.0.2",
52
+ "@deepseek-ai/dsh-client-locale": "^0.1.3-alpha.2",
53
+ "@deepseek-ai/dsh-client-store": "^0.1.3-alpha.2",
54
+ "@deepseek-ai/dsh-client-test-runtime": "^0.1.3-alpha.2",
55
+ "@deepseek-ai/dsh-client-ui-conversation": "^0.1.3-alpha.2",
56
+ "@deepseek-ai/dsh-client-ui-primitives": "^0.1.3-alpha.2",
57
+ "@deepseek-ai/dsh-client-ui-renderer": "^0.1.3-alpha.2",
58
+ "@deepseek-ai/dsh-client-ui-session": "^0.1.3-alpha.2",
59
+ "@deepseek-ai/dsh-api-session-controller": "^0.1.3-alpha.2",
60
+ "@deepseek-ai/dsh-client-ui-slots": "^0.1.3-alpha.2",
61
+ "@deepseek-ai/dsh-host-open-in-app": "^0.1.3-alpha.2",
62
+ "@deepseek-ai/dsh-session": "^0.1.3-alpha.2"
63
+ },
64
+ "scripts": {
65
+ "bundle": "tsdown",
66
+ "watch": "tsdown --watch"
67
+ }
68
+ }