@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 +39 -5
- package/README.md +4 -2
- package/README.zh.md +4 -2
- package/lib/client.js +172 -56
- package/lib/types/client/OpenInAppAction.d.ts +4 -0
- package/lib/types/client/OpenTargetButton.d.ts +4 -1
- package/lib/types/client/applications.d.ts +5 -0
- package/lib/types/client/controller.d.ts +13 -0
- package/lib/types/client/index.d.ts +1 -1
- package/lib/types/client/locales.d.ts +3 -0
- package/package.json +21 -17
package/README.i18n.yaml
CHANGED
|
@@ -1,6 +1,40 @@
|
|
|
1
|
-
# Bilingual-pair consistency record (docs/i18n/README.md):
|
|
2
|
-
#
|
|
3
|
-
#
|
|
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
|
-
|
|
6
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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`
|
|
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
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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);
|
|
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
|
|
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
|
-
|
|
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 ===
|
|
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,
|
|
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
|
|
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,
|
|
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-
|
|
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-
|
|
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-
|
|
62
|
-
"@deepseek-ai/dsh-
|
|
63
|
-
"@deepseek-ai/dsh-client-
|
|
64
|
-
"@deepseek-ai/dsh-client-
|
|
65
|
-
"@deepseek-ai/dsh-client-
|
|
66
|
-
"@deepseek-ai/dsh-client-ui-
|
|
67
|
-
"@deepseek-ai/dsh-client-ui-
|
|
68
|
-
"@deepseek-ai/dsh-
|
|
69
|
-
"@deepseek-ai/dsh-
|
|
70
|
-
"@deepseek-ai/dsh-client-ui-
|
|
71
|
-
"@deepseek-ai/dsh-
|
|
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",
|