@deepseek-ai/dsh-client-ui-open-in-app 0.1.7-alpha.2 → 0.1.7-rc.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/README.i18n.yaml CHANGED
@@ -1,6 +1,40 @@
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:
1
+ # Bilingual-pair consistency record for README.md (docs/i18n/README.md): per heading
2
+ # section, a hash of its English and Chinese blocks outside code blocks and generated regions.
3
+ # After editing either side, bring the other along and re-record with:
4
4
  # pnpm run verify-translation-pairing --write packages/client/ui-open-in-app/README.md
5
- README.md: 138be128c291fc4a0611295c54768a95f33f3ae1
6
- README.zh.md: b81f62f9dbf8ccfbc1822b8a72df95e7169228c0
5
+ /:
6
+ en: be11314755d8f4f9
7
+ zh: dd75b03074eea543
8
+ /deepseek-ai-dsh-client-ui-open-in-app:
9
+ en: 004b805ac638e884
10
+ zh: 6ac98df58affefcf
11
+ /deepseek-ai-dsh-client-ui-open-in-app/summary:
12
+ en: e69e4c6df4ddffa1
13
+ zh: d1ef0a722f48e9bd
14
+ /deepseek-ai-dsh-client-ui-open-in-app/table-of-contents:
15
+ en: d152484eb41ac6b4
16
+ zh: 725b47765ee8f30b
17
+ /deepseek-ai-dsh-client-ui-open-in-app/use-this-package:
18
+ en: 056e2bd9e3c78dd8
19
+ zh: be35d2ba53be0442
20
+ /deepseek-ai-dsh-client-ui-open-in-app/use-this-package/what-to-expect:
21
+ en: c93c175a3583820a
22
+ zh: fa812b70176d3d60
23
+ /deepseek-ai-dsh-client-ui-open-in-app/understand-the-implementation:
24
+ en: c8ac8873c45eb568
25
+ zh: fd623c411f79d90f
26
+ /deepseek-ai-dsh-client-ui-open-in-app/further-exploration:
27
+ en: 9deb509f65b6edfd
28
+ zh: d6cd63b2abc3731b
29
+ /deepseek-ai-dsh-client-ui-open-in-app/model-experience:
30
+ en: 5f9e6e0fb0438628
31
+ zh: fc9a81185ea6cb14
32
+ /deepseek-ai-dsh-client-ui-open-in-app/model-experience/kv-cache-effect:
33
+ en: ca75c51c89c2c9b0
34
+ zh: c2241315dc0c89c9
35
+ /deepseek-ai-dsh-client-ui-open-in-app/known-limitations-and-deferred-work:
36
+ en: 3d77fca62d3c8e86
37
+ zh: 3125d32a158e690a
38
+ /deepseek-ai-dsh-client-ui-open-in-app/known-limitations-and-deferred-work/dev-note:
39
+ en: 739fd95270d14d0a
40
+ zh: d5a6b59133ac6b3f
package/README.md CHANGED
@@ -31,7 +31,9 @@ Mount this plugin in the Web composition beside [`dsh-host-open-in-app`](../../h
31
31
 
32
32
  The Session header and document header use the same 24px-high split button with 9px corners. Both headers show only the icon; their tooltip names the default application or the reveal action. Both show the default action’s icon, mark its application with “(default)” in the menu, disable while their own gesture runs, and report failures through a transient toast. The directory adapter uses the existing cross-platform application catalog and remembers the last successful choice in `dsh.open-in-app.choice`; a missing choice falls back to the first available application.
33
33
 
34
- File menus list the discovered associated applications without a separate “Open in default app” row. “Show file location” stays in a fixed footer separated from the scrolling applications. A successful query with only one available action renders a single button without a dropdown. Initial file-association loading uses a gray skeleton icon. When no default application is identified, that row reads “Show file location (default)” and the main button reveals the file. Directory menus have no reveal row. Choosing a file application does not change the system default. An unpreviewable file uses the same menu in a 40px-high split button with an icon and action text; its label is “Open” or “Show file location” according to the default action.
34
+ The **Open locally** shortcut captures the main Session's directory and the same remembered application as the header button. The effective binding appears in the button tooltip and `aria-keyshortcuts`; Web follows the [shortcut service’s platform defaults](../shortcuts/README.md). The command runs only while the Conversation is selected and the header button has a directory and installed application to open. An in-flight launch also blocks the command. Pointer and keyboard gestures share the controller's launch state, so repeated gestures cannot launch twice or change the remembered choice mid-launch.
35
+
36
+ File menus list the discovered associated applications without a separate “Open in default app” row. “Show file location” stays in a fixed footer separated from the scrolling applications. A successful query with only one available action renders a single button without a dropdown. Initial file-association loading uses a gray skeleton icon. When the Host identifies a default application the menu marks it “(default)” and the main button opens it; otherwise the first associated application takes that place, so the main button reveals the file only when no application is registered. Directory menus have no reveal row. Choosing a file application does not change the system default. An unpreviewable file uses the same menu in a 40px-high split button with an icon and action text; its label is “Open” or “Show file location” according to the default action.
35
37
 
36
38
  Mounted controls with the same association reader and file share one query and its results; opening either menu refreshes all of them. Releasing the last control cancels the query and discards its state. Cancelled queries cannot replace another file’s results. A failed query displays a menu message and leaves reveal available as the default action. File-association discovery follows [native-command](../../util/native-command/README.md); an empty result, including a platform without a discovery adapter, uses the same reveal fallback without platform branches in the control.
37
39
 
@@ -43,7 +45,7 @@ Mounted controls with the same association reader and file share one query and i
43
45
  <details>
44
46
  <summary>Implementation internals — click to expand</summary>
45
47
 
46
- 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. Document-relative route forms and wire payload types come 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.
48
+ 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 the shared sources through the inject `hooks` compartment, so every Session header shares one truth. Document-relative route forms and wire payload types come from the host package's browser-safe `@deepseek-ai/dsh-host-open-in-app/shared` subpath. The controller guards in-flight launches and publishes their captured directory and status; header controls derive delayed busy and transient error visuals from that source.
47
49
 
48
50
  The directory and file adapters supply application metadata and operations to [`OpenTargetButton`](src/client/OpenTargetButton.tsx), which owns menu ordering, default markers, icons, sizing, and gesture feedback. The file header and empty state share `FileOpenTarget`, while `OpenPathInjected.applications` queries `session.workspacePathApplications` through [`open-path.ts`](src/client/open-path.ts). Opening uses `session.openWorkspacePath`; the Host revalidates an explicitly selected handler before launch. The directory adapter keeps the existing catalog routes. `FileRouteAction` supplies the same control to delivery cards and change review through `deliverables.file.actions` and `deliverables.review.file.actions`; their authenticated routes retain Session file authorization. A failed or unavailable file query therefore needs no platform-specific UI implementation.
49
51
 
package/README.zh.md CHANGED
@@ -31,7 +31,9 @@ kind: "package-reference"
31
31
 
32
32
  会话标题栏和文档标题栏共用同一个高 24px、圆角 9px 的分体按钮。两个标题栏都只显示图标,悬停提示显示默认应用名称或文件定位动作。两者都显示默认动作的图标,在菜单的默认应用后标注“(默认)”,仅在自己的操作执行期间禁用,并通过短暂提示报告失败。目录适配器使用已有的跨平台应用列表,将最后一次成功选择保存在 `dsh.open-in-app.choice` 中;原选择不可用时回退到第一个可用应用。
33
33
 
34
- 文件菜单列出已发现的关联应用,不单列“用默认应用打开”。“显示文件位置”固定在菜单底部,通过分隔线与滚动的应用列表分开。查询成功且只有一个可用操作时显示单按钮,不再显示下拉箭头。文件关联首次加载时使用灰色骨架图标。未识别到默认应用时,该项显示为“显示文件位置(默认)”,主按钮也执行文件定位。目录菜单不包含定位项。选择文件应用不修改系统默认应用。无法预览文件时,空态使用同一菜单,按钮增大到 40px 高并显示图标和动作文字;根据默认动作显示“打开”或“显示文件位置”。
34
+ **在本地打开**快捷键捕获主会话的目录,并使用与头部按钮相同的已记住应用。按钮 tooltip 和 `aria-keyshortcuts` 显示当前有效绑定;Web 遵循[快捷键服务的平台默认值](../shortcuts/README.zh.md)。只有选中会话界面、且头部按钮有可打开的目录和已安装应用时,命令才执行。启动尚未结束时也会阻止命令。鼠标与键盘操作共享 controller 的启动状态,重复操作不会启动两次,也不会在启动期间更改已记住的选择。
35
+
36
+ 文件菜单列出已发现的关联应用,不单列“用默认应用打开”。“显示文件位置”固定在菜单底部,通过分隔线与滚动的应用列表分开。查询成功且只有一个可用操作时显示单按钮,不再显示下拉箭头。文件关联首次加载时使用灰色骨架图标。Host 识别出默认应用时,菜单将该项标注为“(默认)”,主按钮打开该应用;否则第一个关联应用占据该位置,只有在一个关联应用都没有时主按钮才执行文件定位。目录菜单不包含定位项。选择文件应用不修改系统默认应用。无法预览文件时,空态使用同一菜单,按钮增大到 40px 高并显示图标和动作文字;根据默认动作显示“打开”或“显示文件位置”。
35
37
 
36
38
  使用同一查询函数和文件的已挂载控件共用查询与结果,打开任一菜单会刷新所有相关控件。最后一个控件释放后取消查询并清除状态。已取消的查询不会覆盖其他文件的结果。查询失败时菜单显示提示,并以文件定位作为默认动作。文件关联查询的平台支持范围见 [native-command](../../util/native-command/README.zh.md);查询结果为空时,包括平台尚无查询适配器的情况,控件统一使用定位动作,不按操作系统分支处理。
37
39
 
@@ -43,7 +45,7 @@ kind: "package-reference"
43
45
  <details>
44
46
  <summary>实现内幕——点击展开</summary>
45
47
 
46
- 插件通过标准 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 的定时器驱动。
48
+ 插件通过标准 slot/inject 机制把分体按钮注册到 `conversation.session.header.utilities`,并以一个 effect 注册 `open-in-app` 词典。一个页面生命周期的 controller([`src/client/controller.ts`](src/client/controller.ts))拥有每页一次的可用性读取、持久化选择的 snapshot store 与启动 POST;组件经 inject 的 `hooks` 隔间接收共享源,因此所有会话头部共享同一份事实。文档相对的路由形式与 wire 载荷类型来自主机包的浏览器安全子路径 `@deepseek-ai/dsh-host-open-in-app/shared`。controller 守卫执行中的启动,并发布所捕获的目录与状态;头部控件从该源派生延迟出现的等待态和短暂错误态。
47
49
 
48
50
  目录和文件适配器把应用信息与操作交给 [`OpenTargetButton`](src/client/OpenTargetButton.tsx),由它统一管理菜单顺序、默认标记、图标、尺寸和操作反馈。文件标题栏和空态共用 `FileOpenTarget`,`OpenPathInjected.applications` 通过 [`open-path.ts`](src/client/open-path.ts) 查询 `session.workspacePathApplications`。打开操作使用 `session.openWorkspacePath`,Host 在启动前重新验证指定的关联应用。`FileRouteAction` 通过 `deliverables.file.actions` 和 `deliverables.review.file.actions` 为交付卡片和变更对比页提供同一控件,其认证路由保留会话文件校验。目录适配器继续使用已有的应用列表路由,文件查询失败或不可用时无需增加平台专用的界面实现。
49
51
 
package/lib/client.js CHANGED
@@ -15,6 +15,45 @@ window.__ModuleLoader__.load({
15
15
  /** Browser-relative form of {@link OPEN_IN_APP_OPEN_PATH}. */
16
16
  const OPEN_IN_APP_OPEN_ROUTE = "/open-in-app/open".slice(1);
17
17
  //#endregion
18
+ //#region lib/types/client/applications.js
19
+ /** Label keys for applications supported by this client. */
20
+ const APP_LABEL_KEY = {
21
+ finder: "app.finder",
22
+ explorer: "app.explorer",
23
+ filemanager: "app.filemanager",
24
+ cursor: "app.cursor",
25
+ vscode: "app.vscode",
26
+ vscodeinsiders: "app.vscodeinsiders",
27
+ windsurf: "app.windsurf",
28
+ zed: "app.zed",
29
+ sublimetext: "app.sublimetext",
30
+ xcode: "app.xcode",
31
+ androidstudio: "app.androidstudio",
32
+ intellij: "app.intellij",
33
+ pycharm: "app.pycharm",
34
+ webstorm: "app.webstorm",
35
+ phpstorm: "app.phpstorm",
36
+ goland: "app.goland",
37
+ rider: "app.rider",
38
+ rustrover: "app.rustrover",
39
+ fork: "app.fork",
40
+ sourcetree: "app.sourcetree",
41
+ github: "app.github",
42
+ tower: "app.tower",
43
+ gitkraken: "app.gitkraken",
44
+ smartgit: "app.smartgit",
45
+ sublimemerge: "app.sublimemerge",
46
+ ghostty: "app.ghostty",
47
+ warp: "app.warp",
48
+ iterm: "app.iterm",
49
+ kitty: "app.kitty",
50
+ terminal: "app.terminal",
51
+ windowsterminal: "app.windowsterminal",
52
+ gitbash: "app.gitbash",
53
+ gnometerminal: "app.gnometerminal",
54
+ konsole: "app.konsole"
55
+ };
56
+ //#endregion
18
57
  //#region lib/types/client/controller.js
19
58
  /** Browser availability/choice state and the launch carrier for the split button. */
20
59
  /**
@@ -28,6 +67,20 @@ window.__ModuleLoader__.load({
28
67
  apps = (0, _deepseek_ai_dsh_client_store.createSnapshotStore)(null);
29
68
  /** Last chosen app id, or empty before the first choice, shared across sessions and browser restarts. */
30
69
  choice = (0, _deepseek_ai_dsh_client_store.createSnapshotStore)("", { persist: { name: "dsh.open-in-app.choice" } });
70
+ /** Current launch, shared by pointer and keyboard gestures. */
71
+ operation = (0, _deepseek_ai_dsh_client_store.createSnapshotStore)({
72
+ phase: "idle",
73
+ path: null
74
+ });
75
+ /**
76
+ * Resolve the remembered nameable installed application, with the button's first-app fallback.
77
+ * @returns the installed app id, or undefined while unavailable.
78
+ */
79
+ currentApp() {
80
+ const apps = (this.apps.getSnapshot() ?? []).filter((id) => APP_LABEL_KEY[id] !== void 0);
81
+ const choice = this.choice.getSnapshot();
82
+ return apps.includes(choice) ? choice : apps[0];
83
+ }
31
84
  loading;
32
85
  /**
33
86
  * @param fetcher - HTTP carrier for the apps read and the launch POST.
@@ -49,25 +102,43 @@ window.__ModuleLoader__.load({
49
102
  * @param appId - catalog id from the availability list.
50
103
  */
51
104
  choose(appId) {
52
- this.choice.set(appId);
105
+ if (this.operation.getSnapshot().phase !== "busy") this.choice.set(appId);
53
106
  }
54
107
  /**
55
108
  * Launch one installed app on a workspace directory.
56
109
  * @param appId - catalog id from the availability list.
57
110
  * @param path - the session's absolute workspace directory.
111
+ * Concurrent gestures are ignored until the current Host request settles.
58
112
  * @returns after the host acknowledged the launch; rejects on any failure.
59
113
  */
60
114
  async launch(appId, path) {
115
+ if (this.operation.getSnapshot().phase === "busy") return;
116
+ this.operation.set({
117
+ phase: "busy",
118
+ path
119
+ });
61
120
  const body = {
62
121
  app: appId,
63
122
  path
64
123
  };
65
- const response = await this.fetcher(OPEN_IN_APP_OPEN_ROUTE, {
66
- method: "POST",
67
- headers: { "content-type": "application/json" },
68
- body: JSON.stringify(body)
69
- });
70
- if (!response.ok) throw new Error(`open failed: HTTP ${String(response.status)}`);
124
+ try {
125
+ const response = await this.fetcher(OPEN_IN_APP_OPEN_ROUTE, {
126
+ method: "POST",
127
+ headers: { "content-type": "application/json" },
128
+ body: JSON.stringify(body)
129
+ });
130
+ if (!response.ok) throw new Error(`open failed: HTTP ${String(response.status)}`);
131
+ this.operation.set({
132
+ phase: "idle",
133
+ path
134
+ });
135
+ } catch (error) {
136
+ this.operation.set({
137
+ phase: "error",
138
+ path
139
+ });
140
+ throw error;
141
+ }
71
142
  }
72
143
  async run() {
73
144
  let apps = [];
@@ -113,7 +184,7 @@ window.__ModuleLoader__.load({
113
184
  }
114
185
  //#endregion
115
186
  //#region \0dsh-css:/home/runner/work/deepseek-harness/deepseek-harness/packages/client/ui-open-in-app/src/client/OpenTargetButton.module.css.mjs
116
- const css = ".OMoRSG_menuAnchor{flex:none;align-self:center;display:inline-flex}.OMoRSG_split{box-sizing:border-box;border:.5px solid var(--dsw-alias-border-l4);height:24px;font-family:var(--dsw-font-family);border-radius:9px;align-items:stretch;display:inline-flex;overflow:hidden}.OMoRSG_main,.OMoRSG_chevron{color:var(--dsw-alias-label-primary);white-space:nowrap;cursor:pointer;background:0 0;border:0;justify-content:center;align-items:center;font-size:11px;line-height:16px;display:inline-flex}.OMoRSG_main{gap:4px;padding:3px 5px}.OMoRSG_chevron{border-left:.5px solid var(--dsw-alias-border-l4);color:var(--dsw-alias-label-secondary);padding:3px 4px 3px 3px}.OMoRSG_main:hover:not(:disabled),.OMoRSG_main:focus-visible,.OMoRSG_chevron:hover:not(:disabled),.OMoRSG_chevron:focus-visible{background:var(--dsw-alias-interactive-bg-hover)}.OMoRSG_main:disabled,.OMoRSG_chevron:disabled{cursor:default}.OMoRSG_appIcon{object-fit:contain}.OMoRSG_split[data-size=large]{border-color:var(--dsw-alias-border-l2);border-radius:14px;height:36px}.OMoRSG_split[data-size=large] .OMoRSG_main{gap:6px;padding:6px 14px;font-size:14px}.OMoRSG_split[data-size=large] .OMoRSG_chevron{padding:6px 10px}.OMoRSG_skeleton{background:var(--dsw-alias-interactive-bg-hover);border-radius:3px;flex:none;display:inline-block}";
187
+ const css = ".OMoRSG_menuAnchor{flex:none;align-self:center;display:inline-flex}.OMoRSG_split{box-sizing:border-box;border:.5px solid var(--dsw-alias-border-l4);border-radius:var(--dsw-radius-sm);height:24px;font-family:var(--dsw-font-family);align-items:stretch;display:inline-flex;overflow:hidden}.OMoRSG_main,.OMoRSG_chevron{color:var(--dsw-alias-label-primary);white-space:nowrap;cursor:pointer;background:0 0;border:0;justify-content:center;align-items:center;font-size:11px;line-height:16px;display:inline-flex}.OMoRSG_main{gap:4px;padding:3px 5px}.OMoRSG_chevron{border-left:.5px solid var(--dsw-alias-border-l4);color:var(--dsw-alias-label-secondary);padding:3px 4px 3px 3px}.OMoRSG_main:hover:not(:disabled),.OMoRSG_main:focus-visible,.OMoRSG_chevron:hover:not(:disabled),.OMoRSG_chevron:focus-visible{background:var(--dsw-alias-interactive-bg-hover)}.OMoRSG_main:disabled,.OMoRSG_chevron:disabled{cursor:default}.OMoRSG_appIcon{object-fit:contain}.OMoRSG_split[data-size=large]{border-radius:var(--dsw-radius-md);border-color:var(--dsw-alias-border-l2);height:36px}.OMoRSG_split[data-size=large] .OMoRSG_main{gap:6px;padding:6px 14px;font-size:14px}.OMoRSG_split[data-size=large] .OMoRSG_chevron{padding:6px 10px}.OMoRSG_skeleton{border-radius:var(--dsw-radius-xs);background:var(--dsw-alias-interactive-bg-hover);flex:none;display:inline-block}";
117
188
  const tagId = "@deepseek-ai/dsh-client-ui-open-in-app/OpenTargetButton.module.css";
118
189
  if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
119
190
  const tag = document.createElement("style");
@@ -175,7 +246,8 @@ window.__ModuleLoader__.load({
175
246
  });
176
247
  }
177
248
  /**
178
- * Render identical split buttons for files and directories. File reveal always stays last and becomes the default when no app is selected.
249
+ * Render identical split buttons for files and directories. File reveal always
250
+ * stays last; it is the default only when no application is registered.
179
251
  * @param props - target applications, default selection, and operations.
180
252
  * @returns the control and its transient failure feedback.
181
253
  */
@@ -183,7 +255,7 @@ window.__ModuleLoader__.load({
183
255
  const { applications, defaultId, kind, t } = props;
184
256
  const [menuOpen, setMenuOpen] = (0, react.useState)(false);
185
257
  const { pending, toast, act } = useOpenTargetGesture(props.execute, t);
186
- const preferred = applications.find((app) => app.id === defaultId);
258
+ const preferred = kind === "file" ? applications.find((app) => app.id === defaultId) ?? applications[0] : applications.find((app) => app.id === defaultId);
187
259
  const disabled = pending || props.busy === true || props.loading === true;
188
260
  const hasMenu = props.loading === true || props.failed || applications.length + (kind === "file" ? 1 : 0) > 1;
189
261
  const revealDefault = kind === "file" && preferred === void 0 && props.loading !== true;
@@ -193,7 +265,18 @@ window.__ModuleLoader__.load({
193
265
  act(operation);
194
266
  };
195
267
  const primary = () => {
196
- run({ kind: revealDefault ? "reveal" : "default" });
268
+ if (revealDefault) {
269
+ run({ kind: "reveal" });
270
+ return;
271
+ }
272
+ if (preferred !== void 0 && preferred.id !== defaultId) {
273
+ run({
274
+ kind: "application",
275
+ id: preferred.id
276
+ });
277
+ return;
278
+ }
279
+ run({ kind: "default" });
197
280
  };
198
281
  const icon = props.loading === true && preferred === void 0 ? (0, react_jsx_runtime.jsx)("span", {
199
282
  className: OpenTargetButton_module_css_default.skeleton,
@@ -220,7 +303,7 @@ window.__ModuleLoader__.load({
220
303
  items: [...applications.map((app) => ({
221
304
  id: `app:${app.id}`,
222
305
  icon: (0, react_jsx_runtime.jsx)(ApplicationIcon, { source: app.icon }, app.icon),
223
- label: app.id === defaultId ? t("path.appDefault", { app: app.name }) : app.name
306
+ label: app.id === preferred?.id ? t("path.appDefault", { app: app.name }) : app.name
224
307
  })), ...props.failed ? [{
225
308
  id: "unavailable",
226
309
  label: t("path.appsError"),
@@ -246,6 +329,7 @@ window.__ModuleLoader__.load({
246
329
  children: [(0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tooltip, {
247
330
  portal: true,
248
331
  label: primaryLabel,
332
+ shortcutKeys: props.shortcut?.keys,
249
333
  side: "bottom",
250
334
  delayMs: 500,
251
335
  children: (0, react_jsx_runtime.jsxs)("button", {
@@ -253,6 +337,7 @@ window.__ModuleLoader__.load({
253
337
  className: OpenTargetButton_module_css_default.main,
254
338
  disabled,
255
339
  "aria-label": props.prominent ? void 0 : primaryLabel,
340
+ "aria-keyshortcuts": props.shortcut?.aria,
256
341
  "data-open-path-open": kind === "file" && !props.prominent ? "" : void 0,
257
342
  "data-open-path-unpreviewable": props.prominent ? "" : void 0,
258
343
  onClick: primary,
@@ -278,47 +363,6 @@ window.__ModuleLoader__.load({
278
363
  //#endregion
279
364
  //#region lib/types/client/OpenInAppAction.js
280
365
  /**
281
- * Label keys per catalog id: the browser renders only ids it can name, so a
282
- * host catalog extension without a matching dictionary entry stays invisible
283
- * instead of showing a raw id.
284
- */
285
- const APP_LABEL_KEY = {
286
- finder: "app.finder",
287
- explorer: "app.explorer",
288
- filemanager: "app.filemanager",
289
- cursor: "app.cursor",
290
- vscode: "app.vscode",
291
- vscodeinsiders: "app.vscodeinsiders",
292
- windsurf: "app.windsurf",
293
- zed: "app.zed",
294
- sublimetext: "app.sublimetext",
295
- xcode: "app.xcode",
296
- androidstudio: "app.androidstudio",
297
- intellij: "app.intellij",
298
- pycharm: "app.pycharm",
299
- webstorm: "app.webstorm",
300
- phpstorm: "app.phpstorm",
301
- goland: "app.goland",
302
- rider: "app.rider",
303
- rustrover: "app.rustrover",
304
- fork: "app.fork",
305
- sourcetree: "app.sourcetree",
306
- github: "app.github",
307
- tower: "app.tower",
308
- gitkraken: "app.gitkraken",
309
- smartgit: "app.smartgit",
310
- sublimemerge: "app.sublimemerge",
311
- ghostty: "app.ghostty",
312
- warp: "app.warp",
313
- iterm: "app.iterm",
314
- kitty: "app.kitty",
315
- terminal: "app.terminal",
316
- windowsterminal: "app.windowsterminal",
317
- gitbash: "app.gitbash",
318
- gnometerminal: "app.gnometerminal",
319
- konsole: "app.konsole"
320
- };
321
- /**
322
366
  * Adapt the installed directory catalog to the shared opening control.
323
367
  * @param props - workspace state, installed catalog, and launch operations.
324
368
  * @returns the shared control, or null without an eligible application and directory.
@@ -328,6 +372,8 @@ window.__ModuleLoader__.load({
328
372
  const cwd = useSessions((state) => state.byId[sessionId]?.cwd);
329
373
  const available = useOpenInAppApps((apps) => apps);
330
374
  const choice = useOpenInAppChoice((id) => id);
375
+ const operation = props.useOpenInAppLaunch((value) => value);
376
+ const shortcut = props.useShortcuts((rows) => rows.find((row) => row.id === "workspace.openLocal"));
331
377
  const apps = (available ?? []).flatMap((id) => {
332
378
  const key = APP_LABEL_KEY[id];
333
379
  return key === void 0 ? [] : [{
@@ -344,6 +390,8 @@ window.__ModuleLoader__.load({
344
390
  defaultId: preferred.id,
345
391
  failed: false,
346
392
  t,
393
+ busy: operation.phase === "busy",
394
+ shortcut,
347
395
  execute: async (operation) => {
348
396
  const id = operation.kind === "application" ? operation.id : preferred.id;
349
397
  try {
@@ -654,6 +702,9 @@ window.__ModuleLoader__.load({
654
702
  "open.title": "用 {app} 打开",
655
703
  "path.appDefault": "{app}(默认)",
656
704
  "path.appsError": "无法获取应用列表",
705
+ "shortcut.busy": "正在打开工作区",
706
+ "shortcut.unavailable": "当前工作区或本地应用不可用",
707
+ "open.tooltip": "在本地打开",
657
708
  "path.open": "打开",
658
709
  "path.more": "更多打开方式",
659
710
  "path.reveal": "显示文件位置",
@@ -670,6 +721,9 @@ window.__ModuleLoader__.load({
670
721
  "open.title": "Open in {app}",
671
722
  "path.appDefault": "{app} (default)",
672
723
  "path.appsError": "Could not load applications",
724
+ "shortcut.busy": "Opening workspace",
725
+ "shortcut.unavailable": "Current workspace or local application unavailable",
726
+ "open.tooltip": "Open locally",
673
727
  "path.open": "Open",
674
728
  "path.more": "More ways to open",
675
729
  "path.reveal": "Show file location",
@@ -688,13 +742,15 @@ window.__ModuleLoader__.load({
688
742
  * delivery cards, and changed-file review. Directory choices persist in the browser;
689
743
  * file defaults and application lists come from the serving Host desktop.
690
744
  */
691
- /** Required services: sessions, the slot registry, copy, and the Remote carrier with its `session` namespace. */
745
+ /** Required services: sessions, layout selection, the slot registry, copy, Remote calls, and shortcuts. */
692
746
  const inject = [
693
747
  "sessions",
694
748
  "slots",
695
749
  "locale",
696
750
  "remote",
697
- "remote.session"
751
+ "remote.session",
752
+ "shortcuts",
753
+ "layout"
698
754
  ];
699
755
  /**
700
756
  * Client plugin body: register the dictionaries, the header split button, and
@@ -709,6 +765,64 @@ window.__ModuleLoader__.load({
709
765
  zh,
710
766
  en
711
767
  }), "open-in-app: dictionaries");
768
+ const t = ctx.locale.bind(NS);
769
+ const target = () => {
770
+ if (ctx.layout.panelInfo.getSnapshot().activePanelId !== null) return void 0;
771
+ const session = Object.values(ctx.sessions.list.getSnapshot().byId).find((row) => (row.retainedBy.mainView ?? 0) > 0);
772
+ const appId = controller.currentApp();
773
+ return session?.cwd && appId !== void 0 ? {
774
+ appId,
775
+ path: session.cwd
776
+ } : void 0;
777
+ };
778
+ ctx.effect(() => ctx.shortcuts.register({
779
+ id: "workspace.openLocal",
780
+ label: () => t("open.tooltip"),
781
+ aliases: ["open workspace locally", "open in app"],
782
+ defaults: {
783
+ "desktop:macos": {
784
+ code: "KeyO",
785
+ modifiers: ["primary", "alt"]
786
+ },
787
+ "desktop:windows": {
788
+ code: "KeyO",
789
+ modifiers: ["primary", "alt"]
790
+ },
791
+ "desktop:linux": {
792
+ code: "KeyO",
793
+ modifiers: ["primary", "alt"]
794
+ },
795
+ "web:macos": {
796
+ code: "KeyO",
797
+ modifiers: ["primary", "shift"]
798
+ },
799
+ "web:windows": {
800
+ code: "KeyO",
801
+ modifiers: ["primary", "shift"]
802
+ }
803
+ },
804
+ regions: ["page", "editable"],
805
+ modals: [],
806
+ resolve: () => {
807
+ if (controller.operation.getSnapshot().phase === "busy") return {
808
+ status: "blocked",
809
+ reason: t("shortcut.busy")
810
+ };
811
+ const selected = target();
812
+ if (selected === void 0) return {
813
+ status: "blocked",
814
+ reason: t("shortcut.unavailable")
815
+ };
816
+ return {
817
+ status: "handled",
818
+ run: () => {
819
+ controller.launch(selected.appId, selected.path).catch((error) => {
820
+ console.warn("workspace open rejected:", error);
821
+ });
822
+ }
823
+ };
824
+ }
825
+ }), "open-in-app: workspace command");
712
826
  ctx.slots.inject("conversation.session.header.utilities", () => ctx.slots.register({
713
827
  name: "conversation.session.header.utilities",
714
828
  id: "open-in-app",
@@ -717,7 +831,9 @@ window.__ModuleLoader__.load({
717
831
  inject: () => ({
718
832
  hooks: {
719
833
  openInAppApps: controller.apps,
720
- openInAppChoice: controller.choice
834
+ openInAppChoice: controller.choice,
835
+ openInAppLaunch: controller.operation,
836
+ shortcuts: ctx.shortcuts.catalog
721
837
  },
722
838
  launch: (appId, path) => controller.launch(appId, path),
723
839
  choose: (appId) => {
@@ -1,11 +1,15 @@
1
1
  import type { ObservableSnapshot } from '@deepseek-ai/dsh-client-store';
2
2
  import type { InjectFace, PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots';
3
+ import type { OpenInAppLaunchState } from './controller.ts';
4
+ import type { ShortcutCatalogEntry } from '@deepseek-ai/dsh-client-shortcuts/client';
3
5
  import { NS } from './locales.ts';
4
6
  /** Browser operations and state injected into the Session Header contribution. */
5
7
  export interface OpenInAppActionInjected {
6
8
  hooks: {
7
9
  openInAppApps: ObservableSnapshot<readonly string[] | null>;
8
10
  openInAppChoice: ObservableSnapshot<string>;
11
+ openInAppLaunch: ObservableSnapshot<OpenInAppLaunchState>;
12
+ shortcuts: ObservableSnapshot<readonly ShortcutCatalogEntry[]>;
9
13
  };
10
14
  launch: (appId: string, path: string) => Promise<void>;
11
15
  choose: (appId: string) => void;
@@ -1,4 +1,5 @@
1
1
  import type { ReactNode } from 'react';
2
+ import type { ShortcutCatalogEntry } from '@deepseek-ai/dsh-client-shortcuts/client';
2
3
  import type { TranslateNS } from '@deepseek-ai/dsh-client-ui-slots';
3
4
  import type { OpenInAppPathFailure } from './open-path.ts';
4
5
  import type { NS } from './locales.ts';
@@ -19,6 +20,7 @@ export type OpenTargetOperation = {
19
20
  };
20
21
  /** Inputs shared by both target adapters and the file empty-state action. */
21
22
  export interface OpenTargetButtonProps {
23
+ readonly shortcut?: Pick<ShortcutCatalogEntry, 'keys' | 'aria'> | undefined;
22
24
  readonly kind: 'file' | 'directory';
23
25
  readonly applications: readonly OpenTargetApplication[];
24
26
  readonly defaultId: string | undefined;
@@ -42,7 +44,8 @@ export declare function useOpenTargetGesture(execute: OpenTargetButtonProps['exe
42
44
  act: (operation: OpenTargetOperation) => void;
43
45
  };
44
46
  /**
45
- * Render identical split buttons for files and directories. File reveal always stays last and becomes the default when no app is selected.
47
+ * Render identical split buttons for files and directories. File reveal always
48
+ * stays last; it is the default only when no application is registered.
46
49
  * @param props - target applications, default selection, and operations.
47
50
  * @returns the control and its transient failure feedback.
48
51
  */
@@ -0,0 +1,5 @@
1
+ /** Nameable application ids shared by the button and its keyboard command. */
2
+ import type { OpenInAppKey } from './locales.ts';
3
+ /** Label keys for applications supported by this client. */
4
+ export declare const APP_LABEL_KEY: Record<string, OpenInAppKey | undefined>;
5
+ //# sourceMappingURL=applications.d.ts.map
@@ -1,5 +1,10 @@
1
1
  /** Browser availability/choice state and the launch carrier for the split button. */
2
2
  import { type SnapshotStore } from '@deepseek-ai/dsh-client-store';
3
+ /** Shared launch status for controls targeting the captured workspace path. */
4
+ export interface OpenInAppLaunchState {
5
+ readonly phase: 'idle' | 'busy' | 'error';
6
+ readonly path: string | null;
7
+ }
3
8
  type Fetch = (input: string | URL, init?: RequestInit) => Promise<Response>;
4
9
  /**
5
10
  * Owns the once-per-page availability read, the persisted last choice, and
@@ -12,6 +17,13 @@ export declare class OpenInAppController {
12
17
  readonly apps: SnapshotStore<readonly string[] | null>;
13
18
  /** Last chosen app id, or empty before the first choice, shared across sessions and browser restarts. */
14
19
  readonly choice: SnapshotStore<string>;
20
+ /** Current launch, shared by pointer and keyboard gestures. */
21
+ readonly operation: SnapshotStore<OpenInAppLaunchState>;
22
+ /**
23
+ * Resolve the remembered nameable installed application, with the button's first-app fallback.
24
+ * @returns the installed app id, or undefined while unavailable.
25
+ */
26
+ currentApp(): string | undefined;
15
27
  private loading;
16
28
  /**
17
29
  * @param fetcher - HTTP carrier for the apps read and the launch POST.
@@ -32,6 +44,7 @@ export declare class OpenInAppController {
32
44
  * Launch one installed app on a workspace directory.
33
45
  * @param appId - catalog id from the availability list.
34
46
  * @param path - the session's absolute workspace directory.
47
+ * Concurrent gestures are ignored until the current Host request settles.
35
48
  * @returns after the host acknowledged the launch; rejects on any failure.
36
49
  */
37
50
  launch(appId: string, path: string): Promise<void>;
@@ -15,7 +15,7 @@ export type { OpenInAppActionInjected, OpenInAppActionProps } from './OpenInAppA
15
15
  export type { OpenInAppPathAction, OpenInAppPathFailure, OpenInAppPathRemote } from './open-path.ts';
16
16
  export type { OpenPathActionProps, OpenPathInjected } from './OpenPathAction.tsx';
17
17
  export type { OpenPathEmptyActionProps } from './OpenPathEmptyAction.tsx';
18
- /** Required services: sessions, the slot registry, copy, and the Remote carrier with its `session` namespace. */
18
+ /** Required services: sessions, layout selection, the slot registry, copy, Remote calls, and shortcuts. */
19
19
  export declare const inject: string[];
20
20
  /**
21
21
  * Client plugin body: register the dictionaries, the header split button, and
@@ -40,6 +40,9 @@ export declare const zh: {
40
40
  readonly 'open.title': "用 {app} 打开";
41
41
  readonly 'path.appDefault': "{app}(默认)";
42
42
  readonly 'path.appsError': "无法获取应用列表";
43
+ readonly 'shortcut.busy': "正在打开工作区";
44
+ readonly 'shortcut.unavailable': "当前工作区或本地应用不可用";
45
+ readonly 'open.tooltip': "在本地打开";
43
46
  readonly 'path.open': "打开";
44
47
  readonly 'path.more': "更多打开方式";
45
48
  readonly 'path.reveal': "显示文件位置";
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@deepseek-ai/dsh-client-ui-open-in-app",
3
3
  "description": "Web \"Open In...\" controls: the Session-header split button opening the workspace directory in an installed application, and the document preview's default-application controls for one file",
4
- "version": "0.1.7-alpha.2",
4
+ "version": "0.1.7-rc.2",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
@@ -38,7 +38,9 @@
38
38
  "@deepseek-ai/dsh-api-remotes",
39
39
  "@deepseek-ai/dsh-api-session-controller",
40
40
  "@deepseek-ai/dsh-client-locale",
41
+ "@deepseek-ai/dsh-client-shortcuts",
41
42
  "@deepseek-ai/dsh-client-ui-conversation",
43
+ "@deepseek-ai/dsh-client-ui-layout",
42
44
  "@deepseek-ai/dsh-client-ui-renderer",
43
45
  "@deepseek-ai/dsh-client-ui-session",
44
46
  "@deepseek-ai/dsh-client-ui-sidebar-documentpreview"
@@ -52,23 +54,25 @@
52
54
  "devDependencies": {
53
55
  "@types/react": "~18.3.1",
54
56
  "react": "^18.2.0",
55
- "@deepseek-ai/dsh-api-gateway": "0.1.7-alpha.2",
56
- "@deepseek-ai/dsh-api-remotes": "0.1.7-alpha.2",
57
- "@deepseek-ai/dsh-api-session-controller": "0.1.7-alpha.2",
58
- "@deepseek-ai/dsh-client-locale": "0.1.7-alpha.2",
59
- "@deepseek-ai/dsh-client-store": "0.1.7-alpha.2",
57
+ "@deepseek-ai/dsh-api-gateway": "0.1.7-rc.2",
60
58
  "@deepseek-ai/cordis": "~4.0.4",
61
- "@deepseek-ai/dsh-client-test-runtime": "0.1.7-alpha.2",
62
- "@deepseek-ai/dsh-client-ui-conversation": "0.1.7-alpha.2",
63
- "@deepseek-ai/dsh-client-ui-renderer": "0.1.7-alpha.2",
64
- "@deepseek-ai/dsh-client-ui-primitives": "0.1.7-alpha.2",
65
- "@deepseek-ai/dsh-client-ui-session": "0.1.7-alpha.2",
66
- "@deepseek-ai/dsh-client-ui-sidebar-documentpreview": "0.1.7-alpha.2",
67
- "@deepseek-ai/dsh-client-ui-slots": "0.1.7-alpha.2",
68
- "@deepseek-ai/dsh-session": "0.1.7-alpha.2",
69
- "@deepseek-ai/dsh-native-command": "0.1.7-alpha.2",
70
- "@deepseek-ai/dsh-client-ui-deliverables": "0.1.7-alpha.2",
71
- "@deepseek-ai/dsh-host-open-in-app": "0.1.7-alpha.2"
59
+ "@deepseek-ai/dsh-api-remotes": "0.1.7-rc.2",
60
+ "@deepseek-ai/dsh-api-session-controller": "0.1.7-rc.2",
61
+ "@deepseek-ai/dsh-client-locale": "0.1.7-rc.2",
62
+ "@deepseek-ai/dsh-client-store": "0.1.7-rc.2",
63
+ "@deepseek-ai/dsh-client-test-runtime": "0.1.7-rc.2",
64
+ "@deepseek-ai/dsh-client-ui-conversation": "0.1.7-rc.2",
65
+ "@deepseek-ai/dsh-client-ui-layout": "0.1.7-rc.2",
66
+ "@deepseek-ai/dsh-client-ui-primitives": "0.1.7-rc.2",
67
+ "@deepseek-ai/dsh-client-ui-renderer": "0.1.7-rc.2",
68
+ "@deepseek-ai/dsh-client-ui-session": "0.1.7-rc.2",
69
+ "@deepseek-ai/dsh-client-ui-sidebar-documentpreview": "0.1.7-rc.2",
70
+ "@deepseek-ai/dsh-client-ui-slots": "0.1.7-rc.2",
71
+ "@deepseek-ai/dsh-session": "0.1.7-rc.2",
72
+ "@deepseek-ai/dsh-native-command": "0.1.7-rc.2",
73
+ "@deepseek-ai/dsh-host-open-in-app": "0.1.7-rc.2",
74
+ "@deepseek-ai/dsh-client-shortcuts": "0.1.7-rc.2",
75
+ "@deepseek-ai/dsh-client-ui-deliverables": "0.1.7-rc.2"
72
76
  },
73
77
  "scripts": {
74
78
  "bundle": "tsdown",