@lihengxi20/dsh-tabs 0.0.0-stage → 0.1.1
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 +21 -0
- package/README.md +64 -2
- package/assets/screenshot-menu.png +0 -0
- package/assets/screenshot-rename.png +0 -0
- package/assets/screenshot-tabs.png +0 -0
- package/cordis.patch.yml +8 -0
- package/lib/client.js +1145 -0
- package/lib/index.js +16 -0
- package/package.json +63 -3
- package/screenshots.json +5 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 lihengxi20
|
|
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.
|
package/README.md
CHANGED
|
@@ -1,3 +1,65 @@
|
|
|
1
|
-
#
|
|
1
|
+
# dsh-tabs
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
DSH(DeepSeek Harness)Web GUI 的**顶部会话标签页**插件:在会话头部下方加一行会话标签,像浏览器标签一样管理打开过的会话。
|
|
4
|
+
|
|
5
|
+
> GitHub 仓库:`lihengxi20/dsh-session-tabs` · npm 包名:`@lihengxi20/dsh-tabs`(同一份代码)。
|
|
6
|
+
|
|
7
|
+
> **English summary** — A session-tab bar for the DSH web GUI: open sessions as tabs, switch / close / rename them, with `workspace / title` labels and a persisted tab list. Published as `@lihengxi20/dsh-tabs`; plugin-only, no DSH core changes.
|
|
8
|
+
|
|
9
|
+
## 截图
|
|
10
|
+
|
|
11
|
+

|
|
12
|
+
|
|
13
|
+

|
|
14
|
+
|
|
15
|
+

|
|
16
|
+
|
|
17
|
+
## 功能
|
|
18
|
+
|
|
19
|
+
- **单/多会话**:只有一个会话时也显示一个标签;多会话时按打开顺序排列,从侧边栏、搜索或新建会话打开时自动加入。
|
|
20
|
+
- **标签文案**:`工作区 / 会话标题`;工作区取侧边栏的工作区标题(缺省回退目录名),空白会话显示 `工作区 / 新会话`。
|
|
21
|
+
- **切换与关闭**:点击切换;悬停出现 `×`(单个标签也可关闭);中键点击关闭。关闭只移除标签,不归档、不停止任务;关闭最后一个标签会回到「未选中会话」的空白页。
|
|
22
|
+
- **右键菜单**:重命名 / 关闭 / 关闭其他 (n) / 全部关闭 (n);重命名走 Host 的 `session/rename`,空白会话禁用。
|
|
23
|
+
- **状态圆点**:运行中(蓝/脉冲)、等待审批或回答(橙)、完成未读(绿)。
|
|
24
|
+
- **持久化**:标签顺序、活动标签、标题与工作区缓存存在浏览器 `localStorage`(键 `dsh-session-tabs.v1`),刷新/重开客户端后恢复。
|
|
25
|
+
- **跟随主题**:全部使用 DSH 主题变量,深浅色、字体大小自动适配。
|
|
26
|
+
|
|
27
|
+
标签是导航状态:DSH 同一时刻仍只渲染一个会话,后台任务照常运行。
|
|
28
|
+
|
|
29
|
+
## 兼容性
|
|
30
|
+
|
|
31
|
+
- 面向 **DSH 0.2.2-rc.1**(desktop profile)开发并实机验证。
|
|
32
|
+
- 依赖会话头部的 `conversation.header.leading`、`conversation.session.header.actions` 槽位,以及 `sessions` / `uiWorkspace` / `uiSession` 客户端服务;DSH 升级后请按 README 的验收清单复验,槽位不可用时插件会降级为不显示标签,不影响会话页面。
|
|
33
|
+
|
|
34
|
+
## 安装
|
|
35
|
+
|
|
36
|
+
通过 DSH 自带插件管理器添加:
|
|
37
|
+
|
|
38
|
+
```sh
|
|
39
|
+
# npm
|
|
40
|
+
dsh plugin --profile desktop add @lihengxi20/dsh-tabs
|
|
41
|
+
|
|
42
|
+
# 或直接从 GitHub 仓库安装(始终跟最新代码)
|
|
43
|
+
dsh plugin --profile desktop add github:lihengxi20/dsh-session-tabs
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
`--profile` 换成目标 profile(如 `web`)。装好后在 **设置 → 插件** 中启用/禁用它;插件市场(dshmarket)收录后也可以直接在市场里搜索安装。
|
|
47
|
+
|
|
48
|
+
## 开发
|
|
49
|
+
|
|
50
|
+
- 无需构建:`lib/client.js` 是真实的浏览器 CJS bundle,`lib/index.js` 是让 Loader 拥有条目的 host 半侧占位。
|
|
51
|
+
- 本地开发可直接以目录方式安装:
|
|
52
|
+
|
|
53
|
+
```sh
|
|
54
|
+
dsh plugin --profile desktop add link:C:/path/to/dsh-session-tabs
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
- 修改 `lib/client.js` 后,在插件管理器里禁用再启用该条目即可热重载浏览器端,无需重启 DSH。
|
|
58
|
+
|
|
59
|
+
## 数据与隐私
|
|
60
|
+
|
|
61
|
+
插件不发起网络请求、不上报遥测;只在浏览器 `localStorage['dsh-session-tabs.v1']` 保存标签 id、标题缓存与活动标签。
|
|
62
|
+
|
|
63
|
+
## License
|
|
64
|
+
|
|
65
|
+
[MIT](./LICENSE)
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/cordis.patch.yml
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# npm package: @lihengxi20/dsh-tabs (repo: dsh-session-tabs) profile patch layer.
|
|
2
|
+
#
|
|
3
|
+
# One loader entry is all this bundle needs: the host half (lib/index.js) is an
|
|
4
|
+
# inert plugin, and the browser half is picked up by dsh-client-modules through
|
|
5
|
+
# the package's `dsh.client` declaration, exactly like the shipped ui-* rows.
|
|
6
|
+
- insert:
|
|
7
|
+
- id: session-tabs
|
|
8
|
+
name: '@lihengxi20/dsh-tabs'
|
package/lib/client.js
ADDED
|
@@ -0,0 +1,1145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-session-tabs — browser half (client plugin bundle).
|
|
3
|
+
*
|
|
4
|
+
* Loaded by dsh-client-modules as the package client bundle (@lihengxi20/dsh-tabs) and run
|
|
5
|
+
* through the vendored Loader's lazy-CJS module table
|
|
6
|
+
* (window.__ModuleLoader__.load). Plain CJS, exactly the shape the shipped
|
|
7
|
+
* ui-* bundles emit: `require("react")` / `require("react-dom")` resolve
|
|
8
|
+
* against the shell's platform module table.
|
|
9
|
+
*
|
|
10
|
+
* What it does:
|
|
11
|
+
* - fills the conversation header's empty `conversation.header.leading` slot
|
|
12
|
+
* with the current session's workspace folder + name (the active-session
|
|
13
|
+
* info row);
|
|
14
|
+
* - appends one grid row to the conversation <header> and portals a session
|
|
15
|
+
* tab strip into it (one tab per opened session, active one highlighted);
|
|
16
|
+
* - tracks "opened sessions" across navigations, persists the tab list and
|
|
17
|
+
* the active tab in localStorage, and switches sessions through
|
|
18
|
+
* ctx.uiWorkspace.openSession(id).
|
|
19
|
+
*
|
|
20
|
+
* The tab strip is a navigation affordance, not a second renderer: DSH still
|
|
21
|
+
* renders exactly one session at a time, and closing a tab never archives the
|
|
22
|
+
* session, cancels its turn, or stops background work.
|
|
23
|
+
*/
|
|
24
|
+
(function (root) {
|
|
25
|
+
"use strict";
|
|
26
|
+
if (!root || !root.__ModuleLoader__ || typeof root.__ModuleLoader__.load !== "function") return;
|
|
27
|
+
|
|
28
|
+
root.__ModuleLoader__.load({
|
|
29
|
+
id: "@lihengxi20/dsh-tabs",
|
|
30
|
+
factory: function (require) {
|
|
31
|
+
var module = { exports: {} };
|
|
32
|
+
var exports = module.exports;
|
|
33
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
34
|
+
|
|
35
|
+
var React = null;
|
|
36
|
+
try { React = require("react") || null; } catch (e) { React = null; }
|
|
37
|
+
var ReactDOM = null;
|
|
38
|
+
try { ReactDOM = require("react-dom") || null; } catch (e) { ReactDOM = null; }
|
|
39
|
+
var h = React.createElement;
|
|
40
|
+
|
|
41
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
42
|
+
// constants
|
|
43
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
44
|
+
var NS = "dsh-session-tabs";
|
|
45
|
+
var LS_KEY = "dsh-session-tabs.v1";
|
|
46
|
+
var STYLE_ID = "dsh-session-tabs-style";
|
|
47
|
+
var MAX_TABS = 50;
|
|
48
|
+
|
|
49
|
+
var inject = ["sessions", "slots", "locale"];
|
|
50
|
+
|
|
51
|
+
var UI = {
|
|
52
|
+
zh: {
|
|
53
|
+
"tabs.aria": "会话标签",
|
|
54
|
+
"newSession": "新会话",
|
|
55
|
+
"untitled": "未命名",
|
|
56
|
+
"close": "关闭标签(不停止任务)",
|
|
57
|
+
"workspace": "工作区",
|
|
58
|
+
"menu.rename": "重命名",
|
|
59
|
+
"menu.close": "关闭",
|
|
60
|
+
"menu.closeOthers": "关闭其他",
|
|
61
|
+
"menu.closeAll": "全部关闭",
|
|
62
|
+
"rename.title": "重命名会话",
|
|
63
|
+
"rename.placeholder": "输入会话标题",
|
|
64
|
+
"rename.cancel": "取消",
|
|
65
|
+
"rename.save": "保存",
|
|
66
|
+
"rename.empty": "标题不能为空",
|
|
67
|
+
"rename.failed": "重命名失败"
|
|
68
|
+
},
|
|
69
|
+
en: {
|
|
70
|
+
"tabs.aria": "Session tabs",
|
|
71
|
+
"newSession": "New Session",
|
|
72
|
+
"untitled": "Untitled",
|
|
73
|
+
"close": "Close tab (does not stop the task)",
|
|
74
|
+
"workspace": "Workspace",
|
|
75
|
+
"menu.rename": "Rename",
|
|
76
|
+
"menu.close": "Close",
|
|
77
|
+
"menu.closeOthers": "Close Others",
|
|
78
|
+
"menu.closeAll": "Close All",
|
|
79
|
+
"rename.title": "Rename session",
|
|
80
|
+
"rename.placeholder": "Session title",
|
|
81
|
+
"rename.cancel": "Cancel",
|
|
82
|
+
"rename.save": "Save",
|
|
83
|
+
"rename.empty": "Title cannot be empty",
|
|
84
|
+
"rename.failed": "Rename failed"
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
89
|
+
// tiny store (React useSyncExternalStore)
|
|
90
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
91
|
+
var state = {
|
|
92
|
+
tabs: [], // session ids, opening order
|
|
93
|
+
titles: {}, // sessionId -> last known durable title
|
|
94
|
+
wsTitles: {}, // sessionId -> workspace display title
|
|
95
|
+
currentId: null, // the session the header is actually showing
|
|
96
|
+
active: null, // last current session (survives list churn)
|
|
97
|
+
probeId: null, // strict-header probe's session id
|
|
98
|
+
selectionId: null, // uiWorkspace.selection session id
|
|
99
|
+
list: null, // latest sessions list snapshot
|
|
100
|
+
statuses: null, // latest sessionStatus Map snapshot
|
|
101
|
+
ready: false,
|
|
102
|
+
uiBound: false, // sessionStatus source bound
|
|
103
|
+
closed: null, // {id, at}: just-closed tab that must not resurrect
|
|
104
|
+
clearGuardUntil: 0 // suppress list-derived current after "close all"
|
|
105
|
+
};
|
|
106
|
+
var listeners = new Set();
|
|
107
|
+
|
|
108
|
+
function getState() { return state; }
|
|
109
|
+
function subscribe(fn) {
|
|
110
|
+
listeners.add(fn);
|
|
111
|
+
return function () { listeners.delete(fn); };
|
|
112
|
+
}
|
|
113
|
+
function emit() {
|
|
114
|
+
Array.from(listeners).forEach(function (fn) {
|
|
115
|
+
try { fn(); } catch (e) { /* one consumer must not break the rest */ }
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
var lastPersisted = null;
|
|
120
|
+
function persist() {
|
|
121
|
+
try {
|
|
122
|
+
var payload = JSON.stringify({ tabs: state.tabs, titles: state.titles, wsTitles: state.wsTitles, active: state.active });
|
|
123
|
+
if (payload === lastPersisted) return;
|
|
124
|
+
root.localStorage.setItem(LS_KEY, payload);
|
|
125
|
+
lastPersisted = payload;
|
|
126
|
+
} catch (e) { /* private mode / quota — tabs still work for this page */ }
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function loadPersisted() {
|
|
130
|
+
try {
|
|
131
|
+
var raw = root.localStorage.getItem(LS_KEY);
|
|
132
|
+
if (raw === null) return;
|
|
133
|
+
var parsed = JSON.parse(raw);
|
|
134
|
+
if (!parsed || typeof parsed !== "object") return;
|
|
135
|
+
var tabs = Array.isArray(parsed.tabs)
|
|
136
|
+
? parsed.tabs.filter(function (id) { return typeof id === "string" && id.length > 0; }).slice(0, MAX_TABS)
|
|
137
|
+
: [];
|
|
138
|
+
var titles = {};
|
|
139
|
+
if (parsed.titles && typeof parsed.titles === "object") {
|
|
140
|
+
Object.keys(parsed.titles).forEach(function (id) {
|
|
141
|
+
var value = parsed.titles[id];
|
|
142
|
+
if (typeof value === "string") titles[id] = value;
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
var wsTitles = {};
|
|
146
|
+
if (parsed.wsTitles && typeof parsed.wsTitles === "object") {
|
|
147
|
+
Object.keys(parsed.wsTitles).forEach(function (id) {
|
|
148
|
+
var value = parsed.wsTitles[id];
|
|
149
|
+
if (typeof value === "string") wsTitles[id] = value;
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
state = Object.assign({}, state, {
|
|
153
|
+
tabs: tabs,
|
|
154
|
+
titles: titles,
|
|
155
|
+
wsTitles: wsTitles,
|
|
156
|
+
active: typeof parsed.active === "string" ? parsed.active : null
|
|
157
|
+
});
|
|
158
|
+
} catch (e) { /* ignore malformed storage */ }
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
162
|
+
// session list helpers
|
|
163
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
164
|
+
function currentSessionId(list) {
|
|
165
|
+
if (!list || !Array.isArray(list.ids)) return null;
|
|
166
|
+
for (var i = 0; i < list.ids.length; i++) {
|
|
167
|
+
var id = list.ids[i];
|
|
168
|
+
var row = list.byId ? list.byId[id] : undefined;
|
|
169
|
+
if (row && row.retainedBy && (row.retainedBy.mainView || 0) > 0) return id;
|
|
170
|
+
}
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function rowTitle(row) {
|
|
175
|
+
if (!row) return "";
|
|
176
|
+
if (typeof row.title === "string" && row.title.trim() !== "") return row.title.trim();
|
|
177
|
+
if (typeof row.displayTitle === "string" && row.displayTitle.trim() !== "") return row.displayTitle.trim();
|
|
178
|
+
return "";
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function syncFromList(list) {
|
|
182
|
+
if (!list) return;
|
|
183
|
+
var tabs = state.tabs.slice();
|
|
184
|
+
var titles = Object.assign({}, state.titles);
|
|
185
|
+
var changed = false;
|
|
186
|
+
// The header's own current session comes from uiSession's main binding;
|
|
187
|
+
// the list's retainedBy is only a fallback for the first paint. After
|
|
188
|
+
// "close all" it stays suppressed until the cleared main view settles.
|
|
189
|
+
var guardingClear = state.clearGuardUntil > Date.now();
|
|
190
|
+
var current = state.currentId || (guardingClear ? null : currentSessionId(list));
|
|
191
|
+
var byId = list.byId || {};
|
|
192
|
+
var ids = Array.isArray(list.ids) ? list.ids : [];
|
|
193
|
+
var ready = list.phase !== "pending" && ids.length > 0;
|
|
194
|
+
|
|
195
|
+
// Refresh cached titles (tab labels survive a reload before the list
|
|
196
|
+
// finishes loading, because they are persisted).
|
|
197
|
+
for (var t = 0; t < tabs.length; t++) {
|
|
198
|
+
var row = byId[tabs[t]];
|
|
199
|
+
if (row) {
|
|
200
|
+
var title = rowTitle(row);
|
|
201
|
+
if (title && titles[tabs[t]] !== title) {
|
|
202
|
+
titles[tabs[t]] = title;
|
|
203
|
+
changed = true;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Prune tabs whose session vanished (deleted / archived away / no
|
|
209
|
+
// longer catalogued). Never prune the currently open session.
|
|
210
|
+
if (ready) {
|
|
211
|
+
var kept = tabs.filter(function (id) {
|
|
212
|
+
return id === current || Object.prototype.hasOwnProperty.call(byId, id);
|
|
213
|
+
});
|
|
214
|
+
if (kept.length !== tabs.length) { tabs = kept; changed = true; }
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Opening a session (sidebar / search / new session / tab click) adds
|
|
218
|
+
// it. The current session is always present, so even the first session
|
|
219
|
+
// of a fresh window gets exactly one tab.
|
|
220
|
+
if (current && tabs.indexOf(current) < 0 && !isSuppressed(current)) {
|
|
221
|
+
tabs.push(current);
|
|
222
|
+
if (tabs.length > MAX_TABS) tabs = tabs.slice(tabs.length - MAX_TABS);
|
|
223
|
+
changed = true;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
var active = current || state.active || (tabs.length > 0 ? tabs[tabs.length - 1] : null);
|
|
227
|
+
if (active !== state.active) changed = true;
|
|
228
|
+
|
|
229
|
+
state = Object.assign({}, state, {
|
|
230
|
+
list: list,
|
|
231
|
+
tabs: tabs,
|
|
232
|
+
titles: titles,
|
|
233
|
+
active: active,
|
|
234
|
+
ready: state.ready || ready
|
|
235
|
+
});
|
|
236
|
+
if (changed) persist();
|
|
237
|
+
emit();
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function syncFromStatuses(statuses) {
|
|
241
|
+
state = Object.assign({}, state, { statuses: statuses });
|
|
242
|
+
emit();
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/** Workspace display title for a session: live snapshot, then cached, then cwd basename. */
|
|
246
|
+
function workspaceTitleFor(store, id, row) {
|
|
247
|
+
if (id && store.wsTitles && typeof store.wsTitles[id] === "string" && store.wsTitles[id] !== "") {
|
|
248
|
+
return store.wsTitles[id];
|
|
249
|
+
}
|
|
250
|
+
var cwd = row && typeof row.cwd === "string" ? row.cwd : "";
|
|
251
|
+
return basename(cwd);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function syncFromWorkspaces(snapshot) {
|
|
255
|
+
if (!snapshot) return;
|
|
256
|
+
var items = Array.isArray(snapshot.items) ? snapshot.items : [];
|
|
257
|
+
var changed = false;
|
|
258
|
+
var wsTitles = state.wsTitles;
|
|
259
|
+
// Ignore the empty pre-ready snapshot so cached labels do not flash away.
|
|
260
|
+
if (items.length > 0 || snapshot.phase === "ready") {
|
|
261
|
+
var next = {};
|
|
262
|
+
for (var i = 0; i < items.length; i++) {
|
|
263
|
+
var item = items[i];
|
|
264
|
+
if (!item || !Array.isArray(item.sessionIds)) continue;
|
|
265
|
+
var title = typeof item.title === "string" ? item.title.trim() : "";
|
|
266
|
+
if (title === "") continue;
|
|
267
|
+
for (var j = 0; j < item.sessionIds.length; j++) next[item.sessionIds[j]] = title;
|
|
268
|
+
}
|
|
269
|
+
if (JSON.stringify(next) !== JSON.stringify(wsTitles)) {
|
|
270
|
+
wsTitles = next;
|
|
271
|
+
changed = true;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
state = Object.assign({}, state, { wsTitles: wsTitles });
|
|
275
|
+
if (changed) persist();
|
|
276
|
+
emit();
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Two authoritative navigation signals, in order:
|
|
281
|
+
* - `uiWorkspace.selection` (set synchronously by replaceMain, also
|
|
282
|
+
* covers blank Sessions whose strict header hides the actions slot);
|
|
283
|
+
* - the strict Session header probe (exact session id the shell renders).
|
|
284
|
+
* The catalog's retainedBy projection is only the last fallback: it lags
|
|
285
|
+
* behind navigation while old main-view references drain.
|
|
286
|
+
*/
|
|
287
|
+
function isSuppressed(id) {
|
|
288
|
+
return state.closed !== null && state.closed.id === id && Date.now() - state.closed.at < 2000;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function applyCurrentId(id) {
|
|
292
|
+
var tabs = state.tabs.slice();
|
|
293
|
+
var titles = Object.assign({}, state.titles);
|
|
294
|
+
if (id && tabs.indexOf(id) < 0 && !isSuppressed(id)) {
|
|
295
|
+
tabs.push(id);
|
|
296
|
+
if (tabs.length > MAX_TABS) tabs = tabs.slice(tabs.length - MAX_TABS);
|
|
297
|
+
}
|
|
298
|
+
state = Object.assign({}, state, {
|
|
299
|
+
currentId: id,
|
|
300
|
+
active: id || state.active,
|
|
301
|
+
tabs: tabs,
|
|
302
|
+
titles: titles
|
|
303
|
+
});
|
|
304
|
+
persist();
|
|
305
|
+
emit();
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function syncSelection(selection) {
|
|
309
|
+
var id = selection && typeof selection.sessionId === "string" ? selection.sessionId : null;
|
|
310
|
+
state = Object.assign({}, state, { selectionId: id });
|
|
311
|
+
if (id !== null) applyCurrentId(id);
|
|
312
|
+
else emit();
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function setProbeId(id) {
|
|
316
|
+
state = Object.assign({}, state, { probeId: id });
|
|
317
|
+
applyCurrentId(state.selectionId || id);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function clearProbeId(id) {
|
|
321
|
+
if (state.probeId !== id) return;
|
|
322
|
+
state = Object.assign({}, state, { probeId: null });
|
|
323
|
+
// Never fall back to the catalog's retainedBy here: it lags behind
|
|
324
|
+
// navigation and would re-create a tab the user just closed.
|
|
325
|
+
if (state.selectionId !== null) applyCurrentId(state.selectionId);
|
|
326
|
+
else emit();
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
330
|
+
// actions
|
|
331
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
332
|
+
function openSession(ctx, id) {
|
|
333
|
+
if (!id) return;
|
|
334
|
+
try {
|
|
335
|
+
var uiWorkspace = ctx.get("uiWorkspace");
|
|
336
|
+
if (uiWorkspace && typeof uiWorkspace.openSession === "function") {
|
|
337
|
+
uiWorkspace.openSession(id);
|
|
338
|
+
}
|
|
339
|
+
} catch (e) {
|
|
340
|
+
try { console.warn("[dsh-session-tabs] openSession failed", e); } catch (e2) { /* ignore */ }
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function closeTab(ctx, id) {
|
|
345
|
+
var idx = state.tabs.indexOf(id);
|
|
346
|
+
if (idx < 0) return;
|
|
347
|
+
// Closing the last remaining tab behaves like "close all": the tab row
|
|
348
|
+
// disappears and the main view returns to the unselected-session page.
|
|
349
|
+
if (state.tabs.length <= 1) {
|
|
350
|
+
closeAllTabs(ctx);
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
var tabs = state.tabs.slice();
|
|
354
|
+
tabs.splice(idx, 1);
|
|
355
|
+
var titles = Object.assign({}, state.titles);
|
|
356
|
+
delete titles[id];
|
|
357
|
+
var wasCurrent = (state.currentId || currentSessionId(state.list)) === id;
|
|
358
|
+
var nextActive = tabs[Math.min(idx, tabs.length - 1)] || null;
|
|
359
|
+
state = Object.assign({}, state, {
|
|
360
|
+
tabs: tabs,
|
|
361
|
+
titles: titles,
|
|
362
|
+
active: nextActive,
|
|
363
|
+
closed: { id: id, at: Date.now() }
|
|
364
|
+
});
|
|
365
|
+
persist();
|
|
366
|
+
emit();
|
|
367
|
+
if (wasCurrent && nextActive) openSession(ctx, nextActive);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function closeOtherTabs(ctx, keepId) {
|
|
371
|
+
if (state.tabs.length <= 1 || state.tabs.indexOf(keepId) < 0) return;
|
|
372
|
+
openSession(ctx, keepId);
|
|
373
|
+
var titles = {};
|
|
374
|
+
if (state.titles[keepId]) titles[keepId] = state.titles[keepId];
|
|
375
|
+
state = Object.assign({}, state, {
|
|
376
|
+
tabs: [keepId],
|
|
377
|
+
titles: titles,
|
|
378
|
+
active: keepId,
|
|
379
|
+
currentId: keepId,
|
|
380
|
+
closed: null
|
|
381
|
+
});
|
|
382
|
+
persist();
|
|
383
|
+
emit();
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function closeAllTabs(ctx) {
|
|
387
|
+
if (state.tabs.length === 0) return;
|
|
388
|
+
state = Object.assign({}, state, {
|
|
389
|
+
tabs: [],
|
|
390
|
+
titles: {},
|
|
391
|
+
active: null,
|
|
392
|
+
currentId: null,
|
|
393
|
+
probeId: null,
|
|
394
|
+
selectionId: null,
|
|
395
|
+
clearGuardUntil: Date.now() + 1500,
|
|
396
|
+
closed: null
|
|
397
|
+
});
|
|
398
|
+
persist();
|
|
399
|
+
emit();
|
|
400
|
+
try {
|
|
401
|
+
var uiWorkspace = ctx.get("uiWorkspace");
|
|
402
|
+
if (uiWorkspace && typeof uiWorkspace.clearMain === "function") uiWorkspace.clearMain();
|
|
403
|
+
} catch (e) {
|
|
404
|
+
try { console.warn("[dsh-session-tabs] clearMain failed", e); } catch (e2) { /* ignore */ }
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function renameSession(ctx, id, title) {
|
|
409
|
+
var sessions = null;
|
|
410
|
+
try { sessions = ctx.get("sessions"); } catch (e) { sessions = null; }
|
|
411
|
+
if (!sessions || !sessions.manager || typeof sessions.manager.rename !== "function") {
|
|
412
|
+
return Promise.reject(new Error("rename unavailable"));
|
|
413
|
+
}
|
|
414
|
+
return sessions.manager.rename(id, title).then(function (result) {
|
|
415
|
+
if (result && result.ok === true) return true;
|
|
416
|
+
var message = result && result.error && result.error.message ? result.error.message : "rename failed";
|
|
417
|
+
throw new Error(message);
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
422
|
+
// icons
|
|
423
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
424
|
+
function FolderIcon() {
|
|
425
|
+
return h("svg", {
|
|
426
|
+
width: 16, height: 16, viewBox: "0 0 16 16", fill: "none",
|
|
427
|
+
"aria-hidden": "true", className: "dshst-icon"
|
|
428
|
+
}, h("path", {
|
|
429
|
+
d: "M1.75 4.25c0-.69.56-1.25 1.25-1.25h3.1c.33 0 .65.13.88.37l.9.9c.24.24.56.37.89.37h4.23c.69 0 1.25.56 1.25 1.25v6.36c0 .69-.56 1.25-1.25 1.25H3c-.69 0-1.25-.56-1.25-1.25V4.25Z",
|
|
430
|
+
stroke: "currentColor", strokeWidth: 1.2, strokeLinejoin: "round"
|
|
431
|
+
}));
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
function CloseIcon() {
|
|
435
|
+
return h("svg", {
|
|
436
|
+
width: 10, height: 10, viewBox: "0 0 10 10", fill: "none",
|
|
437
|
+
"aria-hidden": "true"
|
|
438
|
+
}, h("path", {
|
|
439
|
+
d: "M1.5 1.5l7 7M8.5 1.5l-7 7",
|
|
440
|
+
stroke: "currentColor", strokeWidth: 1.4, strokeLinecap: "round"
|
|
441
|
+
}));
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
445
|
+
// components
|
|
446
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
447
|
+
function basename(p) {
|
|
448
|
+
if (typeof p !== "string" || p === "") return "";
|
|
449
|
+
var s = p.replace(/[\\/]+$/, "");
|
|
450
|
+
if (s === "") return "";
|
|
451
|
+
var i = Math.max(s.lastIndexOf("/"), s.lastIndexOf("\\"));
|
|
452
|
+
return i < 0 ? s : s.slice(i + 1);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
function useStore() {
|
|
456
|
+
return React.useSyncExternalStore(subscribe, getState, getState);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
function statusOf(store, row) {
|
|
460
|
+
var statuses = store.statuses;
|
|
461
|
+
if (statuses && typeof statuses.get === "function") {
|
|
462
|
+
var status = statuses.get(row.id);
|
|
463
|
+
if (status) {
|
|
464
|
+
if (status.pendingInteraction) return "pending";
|
|
465
|
+
if (status.running) return "running";
|
|
466
|
+
if (status.completionUnread) return "done";
|
|
467
|
+
return "idle";
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
return row.running ? "running" : "idle";
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
function CloseButton(props) {
|
|
474
|
+
return h("button", {
|
|
475
|
+
type: "button",
|
|
476
|
+
className: "dshst-close",
|
|
477
|
+
title: props.t("close"),
|
|
478
|
+
"aria-label": props.t("close"),
|
|
479
|
+
onClick: function (event) {
|
|
480
|
+
event.stopPropagation();
|
|
481
|
+
closeTab(props.ctx, props.id);
|
|
482
|
+
},
|
|
483
|
+
onContextMenu: function (event) {
|
|
484
|
+
event.preventDefault();
|
|
485
|
+
event.stopPropagation();
|
|
486
|
+
}
|
|
487
|
+
}, CloseIcon());
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
function useMenuPosition(x, y, width, height) {
|
|
491
|
+
var ref = React.useRef(null);
|
|
492
|
+
var posState = React.useState({ x: x, y: y });
|
|
493
|
+
var pos = posState[0];
|
|
494
|
+
var setPos = posState[1];
|
|
495
|
+
React.useLayoutEffect(function () {
|
|
496
|
+
var viewportW = root.innerWidth || 1024;
|
|
497
|
+
var viewportH = root.innerHeight || 768;
|
|
498
|
+
var w = width;
|
|
499
|
+
var hgt = height;
|
|
500
|
+
var el = ref.current;
|
|
501
|
+
if (el && typeof el.getBoundingClientRect === "function") {
|
|
502
|
+
var rect = el.getBoundingClientRect();
|
|
503
|
+
if (rect.width > 0) w = rect.width;
|
|
504
|
+
if (rect.height > 0) hgt = rect.height;
|
|
505
|
+
}
|
|
506
|
+
var nextX = Math.max(8, Math.min(x, viewportW - w - 8));
|
|
507
|
+
var nextY = Math.max(8, Math.min(y, viewportH - hgt - 8));
|
|
508
|
+
if (nextX !== pos.x || nextY !== pos.y) setPos({ x: nextX, y: nextY });
|
|
509
|
+
}, [x, y, width, height]);
|
|
510
|
+
return { ref: ref, pos: pos };
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
function TabContextMenu(props) {
|
|
514
|
+
var ctx = props.ctx;
|
|
515
|
+
var t = props.t;
|
|
516
|
+
var targetId = props.targetId;
|
|
517
|
+
var total = props.total;
|
|
518
|
+
var row = props.row;
|
|
519
|
+
var canRename = true;
|
|
520
|
+
if (row && row.blank) canRename = false;
|
|
521
|
+
else if (typeof props.useSessionAccess === "function") {
|
|
522
|
+
canRename = props.useSessionAccess(targetId, "session/rename") !== false;
|
|
523
|
+
}
|
|
524
|
+
var items = [
|
|
525
|
+
{ key: "rename", label: t("menu.rename"), disabled: !canRename, onSelect: props.onRename },
|
|
526
|
+
{ key: "close", label: t("menu.close"), disabled: total === 0, onSelect: props.onCloseTab },
|
|
527
|
+
{ key: "closeOthers", label: t("menu.closeOthers"), count: Math.max(0, total - 1), disabled: total <= 1, onSelect: props.onCloseOthers },
|
|
528
|
+
{ key: "closeAll", label: t("menu.closeAll"), count: total, disabled: total === 0, onSelect: props.onCloseAll }
|
|
529
|
+
];
|
|
530
|
+
var itemsRef = React.useRef(items);
|
|
531
|
+
itemsRef.current = items;
|
|
532
|
+
var indexState = React.useState(0);
|
|
533
|
+
var activeIndex = indexState[0];
|
|
534
|
+
var setActiveIndex = indexState[1];
|
|
535
|
+
var position = useMenuPosition(props.x, props.y, 180, 4 * 32 + 8);
|
|
536
|
+
|
|
537
|
+
React.useEffect(function () {
|
|
538
|
+
var firstEnabled = 0;
|
|
539
|
+
for (var i = 0; i < itemsRef.current.length; i++) {
|
|
540
|
+
if (!itemsRef.current[i].disabled) { firstEnabled = i; break; }
|
|
541
|
+
}
|
|
542
|
+
setActiveIndex(firstEnabled);
|
|
543
|
+
}, []);
|
|
544
|
+
|
|
545
|
+
React.useEffect(function () {
|
|
546
|
+
function onPointerDown(event) {
|
|
547
|
+
var el = position.ref.current;
|
|
548
|
+
if (el && event.target instanceof Node && el.contains(event.target)) return;
|
|
549
|
+
props.onClose();
|
|
550
|
+
}
|
|
551
|
+
function onKey(event) {
|
|
552
|
+
if (event.key === "Escape") {
|
|
553
|
+
event.preventDefault();
|
|
554
|
+
event.stopPropagation();
|
|
555
|
+
props.onClose();
|
|
556
|
+
return;
|
|
557
|
+
}
|
|
558
|
+
if (event.key === "ArrowDown" || event.key === "ArrowUp") {
|
|
559
|
+
event.preventDefault();
|
|
560
|
+
var list = itemsRef.current;
|
|
561
|
+
var step = event.key === "ArrowDown" ? 1 : -1;
|
|
562
|
+
var idx = activeIndex;
|
|
563
|
+
for (var guard = 0; guard < list.length; guard++) {
|
|
564
|
+
idx = (idx + step + list.length) % list.length;
|
|
565
|
+
if (!list[idx].disabled) break;
|
|
566
|
+
}
|
|
567
|
+
setActiveIndex(idx);
|
|
568
|
+
return;
|
|
569
|
+
}
|
|
570
|
+
if (event.key === "Enter") {
|
|
571
|
+
event.preventDefault();
|
|
572
|
+
var item = itemsRef.current[activeIndex];
|
|
573
|
+
if (item && !item.disabled) item.onSelect();
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
document.addEventListener("mousedown", onPointerDown, true);
|
|
577
|
+
document.addEventListener("keydown", onKey, true);
|
|
578
|
+
document.addEventListener("scroll", props.onClose, true);
|
|
579
|
+
root.addEventListener("blur", props.onClose);
|
|
580
|
+
return function () {
|
|
581
|
+
document.removeEventListener("mousedown", onPointerDown, true);
|
|
582
|
+
document.removeEventListener("keydown", onKey, true);
|
|
583
|
+
document.removeEventListener("scroll", props.onClose, true);
|
|
584
|
+
root.removeEventListener("blur", props.onClose);
|
|
585
|
+
};
|
|
586
|
+
}, [activeIndex, props]);
|
|
587
|
+
|
|
588
|
+
var menu = h("div", {
|
|
589
|
+
ref: position.ref,
|
|
590
|
+
className: "dshst-menu",
|
|
591
|
+
role: "menu",
|
|
592
|
+
tabIndex: -1,
|
|
593
|
+
style: { left: position.pos.x + "px", top: position.pos.y + "px" }
|
|
594
|
+
}, items.map(function (item, index) {
|
|
595
|
+
return h("div", {
|
|
596
|
+
key: item.key,
|
|
597
|
+
role: "menuitem",
|
|
598
|
+
className: "dshst-menu-item",
|
|
599
|
+
"data-hovered": index === activeIndex && !item.disabled ? "true" : "false",
|
|
600
|
+
"data-disabled": item.disabled ? "true" : "false",
|
|
601
|
+
onMouseEnter: function () { if (!item.disabled) setActiveIndex(index); },
|
|
602
|
+
onClick: function () { if (!item.disabled) item.onSelect(); }
|
|
603
|
+
},
|
|
604
|
+
h("span", { className: "dshst-menu-label" }, item.label),
|
|
605
|
+
typeof item.count === "number"
|
|
606
|
+
? h("span", { className: "dshst-menu-count" }, "(" + item.count + ")")
|
|
607
|
+
: null
|
|
608
|
+
);
|
|
609
|
+
}));
|
|
610
|
+
return ReactDOM.createPortal(menu, document.body);
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
function RenamePopup(props) {
|
|
614
|
+
var ctx = props.ctx;
|
|
615
|
+
var t = props.t;
|
|
616
|
+
var valueState = React.useState(props.initial || "");
|
|
617
|
+
var value = valueState[0];
|
|
618
|
+
var setValue = valueState[1];
|
|
619
|
+
var busyState = React.useState(false);
|
|
620
|
+
var busy = busyState[0];
|
|
621
|
+
var setBusy = busyState[1];
|
|
622
|
+
var errorState = React.useState("");
|
|
623
|
+
var error = errorState[0];
|
|
624
|
+
var setError = errorState[1];
|
|
625
|
+
var inputRef = React.useRef(null);
|
|
626
|
+
var position = useMenuPosition(props.x, props.y, 280, 120);
|
|
627
|
+
|
|
628
|
+
React.useEffect(function () {
|
|
629
|
+
var input = inputRef.current;
|
|
630
|
+
if (input === null) return;
|
|
631
|
+
try {
|
|
632
|
+
input.focus();
|
|
633
|
+
if (typeof input.select === "function") input.select();
|
|
634
|
+
} catch (e) { /* ignore */ }
|
|
635
|
+
}, []);
|
|
636
|
+
|
|
637
|
+
React.useEffect(function () {
|
|
638
|
+
function onPointerDown(event) {
|
|
639
|
+
var el = position.ref.current;
|
|
640
|
+
if (el && event.target instanceof Node && el.contains(event.target)) return;
|
|
641
|
+
if (!busy) props.onClose();
|
|
642
|
+
}
|
|
643
|
+
function onKey(event) {
|
|
644
|
+
if (event.key === "Escape" && !busy) {
|
|
645
|
+
event.preventDefault();
|
|
646
|
+
event.stopPropagation();
|
|
647
|
+
props.onClose();
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
document.addEventListener("mousedown", onPointerDown, true);
|
|
651
|
+
document.addEventListener("keydown", onKey, true);
|
|
652
|
+
return function () {
|
|
653
|
+
document.removeEventListener("mousedown", onPointerDown, true);
|
|
654
|
+
document.removeEventListener("keydown", onKey, true);
|
|
655
|
+
};
|
|
656
|
+
}, [busy, props]);
|
|
657
|
+
|
|
658
|
+
function submit() {
|
|
659
|
+
if (busy) return;
|
|
660
|
+
var next = value.trim();
|
|
661
|
+
if (next === "") {
|
|
662
|
+
setError(t("rename.empty"));
|
|
663
|
+
return;
|
|
664
|
+
}
|
|
665
|
+
setBusy(true);
|
|
666
|
+
setError("");
|
|
667
|
+
renameSession(ctx, props.id, next)
|
|
668
|
+
.then(function () {
|
|
669
|
+
try { syncFromList(ctx.get("sessions").list.getSnapshot()); } catch (e) { /* ignore */ }
|
|
670
|
+
props.onClose();
|
|
671
|
+
})
|
|
672
|
+
.catch(function (reason) {
|
|
673
|
+
setBusy(false);
|
|
674
|
+
setError((reason && reason.message ? String(reason.message) : t("rename.failed")));
|
|
675
|
+
});
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
var popup = h("div", {
|
|
679
|
+
ref: position.ref,
|
|
680
|
+
className: "dshst-rename",
|
|
681
|
+
role: "dialog",
|
|
682
|
+
"aria-label": t("rename.title"),
|
|
683
|
+
style: { left: position.pos.x + "px", top: position.pos.y + "px" }
|
|
684
|
+
},
|
|
685
|
+
h("div", { className: "dshst-rename-title" }, t("rename.title")),
|
|
686
|
+
h("input", {
|
|
687
|
+
ref: inputRef,
|
|
688
|
+
className: "dshst-rename-input",
|
|
689
|
+
type: "text",
|
|
690
|
+
value: value,
|
|
691
|
+
placeholder: t("rename.placeholder"),
|
|
692
|
+
disabled: busy,
|
|
693
|
+
"data-invalid": error !== "" ? "true" : "false",
|
|
694
|
+
onChange: function (event) { setValue(event.target.value); if (error !== "") setError(""); },
|
|
695
|
+
onKeyDown: function (event) {
|
|
696
|
+
if (event.key === "Enter") { event.preventDefault(); submit(); }
|
|
697
|
+
else if (event.key === "Escape") { event.preventDefault(); event.stopPropagation(); props.onClose(); }
|
|
698
|
+
}
|
|
699
|
+
}),
|
|
700
|
+
error !== "" ? h("div", { className: "dshst-rename-error" }, error) : null,
|
|
701
|
+
h("div", { className: "dshst-rename-actions" },
|
|
702
|
+
h("button", {
|
|
703
|
+
type: "button",
|
|
704
|
+
className: "dshst-btn",
|
|
705
|
+
disabled: busy,
|
|
706
|
+
onClick: function () { props.onClose(); }
|
|
707
|
+
}, t("rename.cancel")),
|
|
708
|
+
h("button", {
|
|
709
|
+
type: "button",
|
|
710
|
+
className: "dshst-btn",
|
|
711
|
+
"data-primary": "true",
|
|
712
|
+
disabled: busy,
|
|
713
|
+
onClick: submit
|
|
714
|
+
}, t("rename.save"))
|
|
715
|
+
)
|
|
716
|
+
);
|
|
717
|
+
return ReactDOM.createPortal(popup, document.body);
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
function TabStrip(props) {
|
|
721
|
+
var store = props.store;
|
|
722
|
+
var list = store.list || { byId: {} };
|
|
723
|
+
var tabs = store.tabs || [];
|
|
724
|
+
var ctx = props.ctx;
|
|
725
|
+
var t = props.t;
|
|
726
|
+
var current = store.currentId || currentSessionId(list) || store.active;
|
|
727
|
+
var menuState = React.useState(null);
|
|
728
|
+
var menu = menuState[0];
|
|
729
|
+
var setMenu = menuState[1];
|
|
730
|
+
var renameState = React.useState(null);
|
|
731
|
+
var rename = renameState[0];
|
|
732
|
+
var setRename = renameState[1];
|
|
733
|
+
|
|
734
|
+
if (tabs.length === 0) return null;
|
|
735
|
+
|
|
736
|
+
function openTabMenu(event, id) {
|
|
737
|
+
event.preventDefault();
|
|
738
|
+
event.stopPropagation();
|
|
739
|
+
setMenu({ id: id, x: event.clientX, y: event.clientY });
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
var strip = h("div", {
|
|
743
|
+
className: "dshst-tabs",
|
|
744
|
+
role: "tablist",
|
|
745
|
+
"aria-label": t("tabs.aria")
|
|
746
|
+
}, tabs.map(function (id) {
|
|
747
|
+
var row = list.byId ? list.byId[id] : undefined;
|
|
748
|
+
var label;
|
|
749
|
+
if (row && row.blank) label = t("newSession");
|
|
750
|
+
else if (row && row.title && row.title.trim() !== "") label = row.title.trim();
|
|
751
|
+
else if (store.titles[id]) label = store.titles[id];
|
|
752
|
+
else label = t("untitled");
|
|
753
|
+
var workspace = workspaceTitleFor(store, id, row);
|
|
754
|
+
var active = id === current;
|
|
755
|
+
var status = row ? statusOf(store, row) : "idle";
|
|
756
|
+
var title = (workspace ? workspace + " / " : "") + label + (row && row.cwd ? "\n" + row.cwd : "");
|
|
757
|
+
return h("div", {
|
|
758
|
+
key: id,
|
|
759
|
+
className: "dshst-tab",
|
|
760
|
+
role: "tab",
|
|
761
|
+
tabIndex: active ? 0 : -1,
|
|
762
|
+
"aria-selected": active ? "true" : "false",
|
|
763
|
+
"data-active": active ? "true" : "false",
|
|
764
|
+
"data-status": status,
|
|
765
|
+
"data-session-id": id,
|
|
766
|
+
"data-menu-target": menu !== null && menu.id === id ? "true" : "false",
|
|
767
|
+
title: title,
|
|
768
|
+
onClick: function () { openSession(ctx, id); },
|
|
769
|
+
onAuxClick: function (event) {
|
|
770
|
+
if (event.button === 1) closeTab(ctx, id);
|
|
771
|
+
},
|
|
772
|
+
onContextMenu: function (event) { openTabMenu(event, id); },
|
|
773
|
+
onKeyDown: function (event) {
|
|
774
|
+
if (event.key === "Enter" || event.key === " ") {
|
|
775
|
+
event.preventDefault();
|
|
776
|
+
openSession(ctx, id);
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
},
|
|
780
|
+
h("span", { className: "dshst-dot", "aria-hidden": "true" }),
|
|
781
|
+
workspace
|
|
782
|
+
? h("span", { className: "dshst-ws", title: workspace }, workspace)
|
|
783
|
+
: null,
|
|
784
|
+
workspace
|
|
785
|
+
? h("span", { className: "dshst-sep", "aria-hidden": "true" }, "/")
|
|
786
|
+
: null,
|
|
787
|
+
h("span", { className: "dshst-label" }, label),
|
|
788
|
+
h(CloseButton, { ctx: ctx, id: id, t: t })
|
|
789
|
+
);
|
|
790
|
+
}));
|
|
791
|
+
|
|
792
|
+
var overlays = [];
|
|
793
|
+
if (menu !== null) {
|
|
794
|
+
var menuTarget = menu.id;
|
|
795
|
+
var menuRow = list.byId ? list.byId[menuTarget] : undefined;
|
|
796
|
+
overlays.push(h(TabContextMenu, {
|
|
797
|
+
key: "menu",
|
|
798
|
+
ctx: ctx,
|
|
799
|
+
t: t,
|
|
800
|
+
x: menu.x,
|
|
801
|
+
y: menu.y,
|
|
802
|
+
targetId: menuTarget,
|
|
803
|
+
row: menuRow,
|
|
804
|
+
total: tabs.length,
|
|
805
|
+
useSessionAccess: props.useSessionAccess,
|
|
806
|
+
onClose: function () { setMenu(null); },
|
|
807
|
+
onRename: function () {
|
|
808
|
+
setMenu(null);
|
|
809
|
+
setRename({ id: menuTarget, x: menu.x, y: menu.y });
|
|
810
|
+
},
|
|
811
|
+
onCloseTab: function () { setMenu(null); closeTab(ctx, menuTarget); },
|
|
812
|
+
onCloseOthers: function () { setMenu(null); closeOtherTabs(ctx, menuTarget); },
|
|
813
|
+
onCloseAll: function () { setMenu(null); closeAllTabs(ctx); }
|
|
814
|
+
}));
|
|
815
|
+
}
|
|
816
|
+
if (rename !== null) {
|
|
817
|
+
var renameRow = list.byId ? list.byId[rename.id] : undefined;
|
|
818
|
+
var initialTitle = renameRow && typeof renameRow.title === "string" ? renameRow.title : "";
|
|
819
|
+
overlays.push(h(RenamePopup, {
|
|
820
|
+
key: "rename",
|
|
821
|
+
ctx: ctx,
|
|
822
|
+
t: t,
|
|
823
|
+
id: rename.id,
|
|
824
|
+
initial: initialTitle,
|
|
825
|
+
x: rename.x,
|
|
826
|
+
y: rename.y,
|
|
827
|
+
onClose: function () { setRename(null); }
|
|
828
|
+
}));
|
|
829
|
+
}
|
|
830
|
+
var overlay = overlays.length > 0
|
|
831
|
+
? ReactDOM.createPortal(h(React.Fragment, null, overlays), document.body)
|
|
832
|
+
: null;
|
|
833
|
+
return h(React.Fragment, null, strip, overlay);
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
/**
|
|
837
|
+
* Headless current-session probe: lives in the strict Session header's
|
|
838
|
+
* actions list, so it is mounted with the exact `sessionId` being shown.
|
|
839
|
+
*/
|
|
840
|
+
function CurrentProbe(props) {
|
|
841
|
+
var id = typeof props.sessionId === "string" ? props.sessionId : null;
|
|
842
|
+
React.useEffect(function () {
|
|
843
|
+
if (id !== null) setProbeId(id);
|
|
844
|
+
return function () { clearProbeId(id); };
|
|
845
|
+
}, [id]);
|
|
846
|
+
return null;
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
/**
|
|
850
|
+
* Header-leading occupant: renders the workspace info inline, and portals
|
|
851
|
+
* the tab strip into one extra header row (the conversation root header is
|
|
852
|
+
* a grid; an appended child becomes the row below the View tabs).
|
|
853
|
+
*/
|
|
854
|
+
function Leading(props) {
|
|
855
|
+
var ctx = props.ctx;
|
|
856
|
+
var t = props.t;
|
|
857
|
+
var store = useStore();
|
|
858
|
+
var anchorRef = React.useRef(null);
|
|
859
|
+
var rowState = React.useState(null);
|
|
860
|
+
var row = rowState[0];
|
|
861
|
+
var setRow = rowState[1];
|
|
862
|
+
|
|
863
|
+
React.useLayoutEffect(function () {
|
|
864
|
+
var anchor = anchorRef.current;
|
|
865
|
+
if (!anchor || typeof anchor.closest !== "function") return undefined;
|
|
866
|
+
var header = anchor.closest("header");
|
|
867
|
+
if (!header || header.querySelector("[data-dsh-session-tabs]")) return undefined;
|
|
868
|
+
var el = document.createElement("div");
|
|
869
|
+
el.className = "dshst-row";
|
|
870
|
+
el.setAttribute("data-dsh-session-tabs", "");
|
|
871
|
+
header.appendChild(el);
|
|
872
|
+
setRow(el);
|
|
873
|
+
// The header is React-managed: when React mounts/reorders its own grid
|
|
874
|
+
// children it can insert them after our appended row, which would flip
|
|
875
|
+
// the auto-placement order. Keep our row last on every child mutation.
|
|
876
|
+
var observer = null;
|
|
877
|
+
if (typeof MutationObserver === "function") {
|
|
878
|
+
observer = new MutationObserver(function () {
|
|
879
|
+
if (el.parentNode === header && header.lastElementChild !== el) header.appendChild(el);
|
|
880
|
+
});
|
|
881
|
+
observer.observe(header, { childList: true });
|
|
882
|
+
}
|
|
883
|
+
return function () {
|
|
884
|
+
if (observer !== null) observer.disconnect();
|
|
885
|
+
if (el.parentNode) el.parentNode.removeChild(el);
|
|
886
|
+
setRow(null);
|
|
887
|
+
};
|
|
888
|
+
}, []);
|
|
889
|
+
|
|
890
|
+
var list = store.list;
|
|
891
|
+
var current = store.currentId || currentSessionId(list);
|
|
892
|
+
var rowData = current && list && list.byId ? list.byId[current] : undefined;
|
|
893
|
+
var cwd = rowData && typeof rowData.cwd === "string" ? rowData.cwd : "";
|
|
894
|
+
var workspace = workspaceTitleFor(store, current, rowData);
|
|
895
|
+
|
|
896
|
+
var leading = h("span", { className: "dshst-leading", ref: anchorRef },
|
|
897
|
+
workspace
|
|
898
|
+
? h(React.Fragment, null,
|
|
899
|
+
h(FolderIcon),
|
|
900
|
+
h("span", { className: "dshst-ws", title: cwd }, workspace),
|
|
901
|
+
h("span", { className: "dshst-sep", "aria-hidden": "true" }, "/"))
|
|
902
|
+
: null
|
|
903
|
+
);
|
|
904
|
+
|
|
905
|
+
var strip = null;
|
|
906
|
+
if (row && typeof ReactDOM.createPortal === "function") {
|
|
907
|
+
strip = ReactDOM.createPortal(
|
|
908
|
+
h(TabStrip, { store: store, ctx: ctx, t: t, useSessionAccess: props.useSessionAccess }),
|
|
909
|
+
row
|
|
910
|
+
);
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
return h(React.Fragment, null, leading, strip);
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
917
|
+
// styles
|
|
918
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
919
|
+
var CSS = [
|
|
920
|
+
".dshst-leading{display:inline-flex;align-items:center;gap:6px;min-width:0;color:var(--dsw-alias-label-tertiary);font-size:14px;line-height:20px;-webkit-app-region:no-drag}",
|
|
921
|
+
".dshst-icon{flex:none;opacity:.9}",
|
|
922
|
+
".dshst-ws{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}",
|
|
923
|
+
".dshst-leading .dshst-ws{max-width:180px}",
|
|
924
|
+
".dshst-sep{color:var(--dsw-alias-label-caption);flex:none}",
|
|
925
|
+
".dshst-row{--dshst-tab-accent:#e6cca0;grid-column:1/-1;min-width:0;padding-left:8px;margin-top:0;display:flex;align-items:flex-start;-webkit-app-region:no-drag}",
|
|
926
|
+
"header:has(> [data-dsh-session-tabs]){padding-bottom:0}",
|
|
927
|
+
".dshst-tabs{display:flex;align-items:center;gap:0;min-width:0;width:100%;overflow-x:auto;scrollbar-width:none;padding:0}",
|
|
928
|
+
".dshst-tabs::-webkit-scrollbar{display:none}",
|
|
929
|
+
".dshst-tab{display:inline-flex;align-items:center;gap:6px;flex:none;max-width:300px;min-width:0;min-height:24px;padding:4px 10px 7px;border:0;border-radius:6px;background:transparent;color:var(--dsw-alias-label-tertiary);font:inherit;font-size:13px;font-weight:500;line-height:16px;cursor:pointer;position:relative;-webkit-app-region:no-drag}",
|
|
930
|
+
".dshst-tab:before{content:\"\";position:absolute;inset:0;border:1px solid transparent;border-bottom:0;border-radius:6px 6px 0 0;pointer-events:none}",
|
|
931
|
+
".dshst-tab .dshst-ws{max-width:110px;flex:none;color:var(--dsw-alias-label-caption);font-size:12px;font-weight:500;line-height:16px}",
|
|
932
|
+
".dshst-tab .dshst-sep{font-size:12px;opacity:.9}",
|
|
933
|
+
"@media (hover:hover){.dshst-tab:hover{color:var(--dsw-alias-label-secondary)}.dshst-tab:hover:before{border-color:var(--dsw-alias-border-l3)}}",
|
|
934
|
+
".dshst-tab:focus-visible:before{border-color:var(--dsw-alias-border-l3)}",
|
|
935
|
+
".dshst-tab[data-active=\"true\"]{color:var(--dsw-alias-state-business-primary)}",
|
|
936
|
+
".dshst-tab[data-active=\"true\"]:before{border-color:var(--dsw-alias-border-l4)}",
|
|
937
|
+
".dshst-tab[data-active=\"true\"] .dshst-ws,.dshst-tab[data-active=\"true\"] .dshst-sep{color:var(--dsw-alias-state-business-primary);opacity:.65}",
|
|
938
|
+
".dshst-tab[data-active=\"true\"]:after{content:\"\";position:absolute;left:0;right:0;bottom:0;height:2px;border-radius:2px 2px 0 0;background:var(--dshst-tab-accent)}",
|
|
939
|
+
".dshst-dot{width:6px;height:6px;border-radius:50%;flex:none;background:var(--dsw-alias-state-idle-primary)}",
|
|
940
|
+
".dshst-tab[data-status=\"running\"] .dshst-dot{background:var(--dsw-alias-state-business-primary);animation:dshst-pulse 1.6s ease-in-out infinite}",
|
|
941
|
+
".dshst-tab[data-status=\"pending\"] .dshst-dot{background:var(--dsw-alias-state-warn-primary)}",
|
|
942
|
+
".dshst-tab[data-status=\"done\"] .dshst-dot{background:var(--dsw-alias-state-success-primary)}",
|
|
943
|
+
"@keyframes dshst-pulse{0%,100%{opacity:1}50%{opacity:.35}}",
|
|
944
|
+
".dshst-label{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}",
|
|
945
|
+
".dshst-close{display:inline-flex;align-items:center;justify-content:center;flex:none;width:14px;height:14px;margin-left:2px;padding:0;border:0;border-radius:4px;background:transparent;color:var(--dsw-alias-label-tertiary);cursor:pointer;opacity:0}",
|
|
946
|
+
".dshst-tab:hover .dshst-close,.dshst-tab:focus-within .dshst-close{opacity:1}",
|
|
947
|
+
"@media (hover:hover){.dshst-close:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}}",
|
|
948
|
+
"@media (prefers-reduced-motion:reduce){.dshst-tab[data-status=\"running\"] .dshst-dot{animation:none}}",
|
|
949
|
+
".dshst-tab[data-menu-target=\"true\"]{background:var(--dsw-alias-interactive-bg-hover)}",
|
|
950
|
+
".dshst-menu{position:fixed;z-index:1200;min-width:180px;box-sizing:border-box;padding:4px;border-radius:8px;background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l3);box-shadow:0 10px 30px rgba(0,0,0,.35);outline:none;-webkit-app-region:no-drag}",
|
|
951
|
+
".dshst-menu-item{display:flex;align-items:center;justify-content:space-between;gap:18px;height:32px;padding:0 10px;border-radius:5px;color:var(--dsw-alias-label-primary);font-size:13px;line-height:16px;cursor:pointer;white-space:nowrap;user-select:none}",
|
|
952
|
+
".dshst-menu-item[data-hovered=\"true\"]{background:var(--dsw-alias-interactive-bg-hover)}",
|
|
953
|
+
".dshst-menu-item[data-disabled=\"true\"]{color:var(--dsw-alias-label-dimmed);cursor:default;background:transparent}",
|
|
954
|
+
".dshst-menu-count{color:var(--dsw-alias-label-caption);font-size:12px;font-variant-numeric:tabular-nums}",
|
|
955
|
+
".dshst-menu-item[data-disabled=\"true\"] .dshst-menu-count{color:var(--dsw-alias-label-dimmed)}",
|
|
956
|
+
".dshst-rename{position:fixed;z-index:1201;width:280px;box-sizing:border-box;padding:12px;border-radius:10px;background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l3);box-shadow:0 12px 32px rgba(0,0,0,.4);-webkit-app-region:no-drag}",
|
|
957
|
+
".dshst-rename-title{margin-bottom:8px;color:var(--dsw-alias-label-caption);font-size:12px;line-height:16px}",
|
|
958
|
+
".dshst-rename-input{width:100%;box-sizing:border-box;height:32px;padding:0 9px;border:1px solid var(--dsw-alias-border-l3);border-radius:6px;background:var(--dsw-alias-bg-base);color:var(--dsw-alias-label-primary);font:inherit;font-size:13px;outline:none}",
|
|
959
|
+
".dshst-rename-input:focus{border-color:var(--dsw-alias-state-business-primary)}",
|
|
960
|
+
".dshst-rename-input[data-invalid=\"true\"]{border-color:var(--dsw-alias-label-error)}",
|
|
961
|
+
".dshst-rename-error{margin-top:6px;color:var(--dsw-alias-label-error);font-size:12px;line-height:16px}",
|
|
962
|
+
".dshst-rename-actions{display:flex;justify-content:flex-end;gap:8px;margin-top:10px}",
|
|
963
|
+
".dshst-btn{height:28px;padding:0 12px;border:1px solid var(--dsw-alias-border-l3);border-radius:6px;background:transparent;color:var(--dsw-alias-label-secondary);font:inherit;font-size:12px;font-weight:500;cursor:pointer}",
|
|
964
|
+
".dshst-btn:hover{background:var(--dsw-alias-interactive-bg-hover)}",
|
|
965
|
+
".dshst-btn[data-primary=\"true\"]{border-color:transparent;background:var(--dsw-alias-state-business-primary);color:var(--dsw-alias-label-primary-foreground)}",
|
|
966
|
+
".dshst-btn:disabled{opacity:.6;cursor:default}"
|
|
967
|
+
].join("\n");
|
|
968
|
+
|
|
969
|
+
function installStyles() {
|
|
970
|
+
if (document.getElementById(STYLE_ID) !== null) return;
|
|
971
|
+
var style = document.createElement("style");
|
|
972
|
+
style.id = STYLE_ID;
|
|
973
|
+
style.textContent = CSS;
|
|
974
|
+
(document.head || document.documentElement).appendChild(style);
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
function removeStyles() {
|
|
978
|
+
var style = document.getElementById(STYLE_ID);
|
|
979
|
+
if (style !== null && style.parentNode !== null) style.parentNode.removeChild(style);
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
983
|
+
// plugin body
|
|
984
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
985
|
+
function apply(ctx) {
|
|
986
|
+
if (!React || !ReactDOM) return;
|
|
987
|
+
loadPersisted();
|
|
988
|
+
|
|
989
|
+
try {
|
|
990
|
+
ctx.effect(function () {
|
|
991
|
+
return ctx.locale.register(NS, UI);
|
|
992
|
+
}, "dsh-session-tabs: locale dictionaries");
|
|
993
|
+
} catch (e) { /* locale optional: fall back to zh strings below */ }
|
|
994
|
+
|
|
995
|
+
var t;
|
|
996
|
+
try {
|
|
997
|
+
t = ctx.locale.bind(NS);
|
|
998
|
+
} catch (e) {
|
|
999
|
+
t = function (key) {
|
|
1000
|
+
var dict = UI.zh;
|
|
1001
|
+
return dict[key] !== undefined ? dict[key] : key;
|
|
1002
|
+
};
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
installStyles();
|
|
1006
|
+
|
|
1007
|
+
ctx.effect(function () {
|
|
1008
|
+
return function () { removeStyles(); };
|
|
1009
|
+
}, "dsh-session-tabs: styles");
|
|
1010
|
+
|
|
1011
|
+
ctx.effect(function () {
|
|
1012
|
+
var unsubs = [];
|
|
1013
|
+
var uiSession = null;
|
|
1014
|
+
var statusStore = null;
|
|
1015
|
+
var workspaceService = null;
|
|
1016
|
+
var selectionStore = null;
|
|
1017
|
+
var workspacesService = null;
|
|
1018
|
+
var workspacesStore = null;
|
|
1019
|
+
var retryTimer = null;
|
|
1020
|
+
|
|
1021
|
+
if (ctx.sessions && ctx.sessions.list) {
|
|
1022
|
+
var listStore = ctx.sessions.list;
|
|
1023
|
+
if (typeof listStore.getSnapshot === "function") syncFromList(listStore.getSnapshot());
|
|
1024
|
+
if (typeof listStore.subscribe === "function") {
|
|
1025
|
+
unsubs.push(listStore.subscribe(function () { syncFromList(listStore.getSnapshot()); }));
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
function bindUiSession() {
|
|
1030
|
+
if (statusStore !== null) return true;
|
|
1031
|
+
if (uiSession === null) {
|
|
1032
|
+
try { uiSession = ctx.get("uiSession"); } catch (e) { uiSession = null; }
|
|
1033
|
+
}
|
|
1034
|
+
if (!uiSession || !uiSession.sessionStatus) return false;
|
|
1035
|
+
statusStore = uiSession.sessionStatus;
|
|
1036
|
+
if (typeof statusStore.getSnapshot === "function") syncFromStatuses(statusStore.getSnapshot());
|
|
1037
|
+
if (typeof statusStore.subscribe === "function") {
|
|
1038
|
+
unsubs.push(statusStore.subscribe(function () { syncFromStatuses(statusStore.getSnapshot()); }));
|
|
1039
|
+
}
|
|
1040
|
+
if (!state.uiBound) {
|
|
1041
|
+
state = Object.assign({}, state, { uiBound: true });
|
|
1042
|
+
emit();
|
|
1043
|
+
}
|
|
1044
|
+
return true;
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
function bindWorkspaceSelection() {
|
|
1048
|
+
if (selectionStore !== null) return true;
|
|
1049
|
+
if (workspaceService === null) {
|
|
1050
|
+
try { workspaceService = ctx.get("uiWorkspace"); } catch (e) { workspaceService = null; }
|
|
1051
|
+
}
|
|
1052
|
+
var selection = workspaceService && workspaceService.selection;
|
|
1053
|
+
if (!selection || typeof selection.subscribe !== "function") return false;
|
|
1054
|
+
selectionStore = selection;
|
|
1055
|
+
if (typeof selectionStore.getSnapshot === "function") syncSelection(selectionStore.getSnapshot());
|
|
1056
|
+
unsubs.push(selectionStore.subscribe(function () { syncSelection(selectionStore.getSnapshot()); }));
|
|
1057
|
+
return true;
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
function bindWorkspaces() {
|
|
1061
|
+
if (workspacesStore !== null) return true;
|
|
1062
|
+
if (workspacesService === null) {
|
|
1063
|
+
try { workspacesService = ctx.get("workspaces"); } catch (e) { workspacesService = null; }
|
|
1064
|
+
}
|
|
1065
|
+
var store = workspacesService && workspacesService.list;
|
|
1066
|
+
if (!store || typeof store.subscribe !== "function") return false;
|
|
1067
|
+
workspacesStore = store;
|
|
1068
|
+
if (typeof store.getSnapshot === "function") syncFromWorkspaces(store.getSnapshot());
|
|
1069
|
+
unsubs.push(store.subscribe(function () { syncFromWorkspaces(store.getSnapshot()); }));
|
|
1070
|
+
return true;
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
function bindAll() {
|
|
1074
|
+
return bindUiSession() && bindWorkspaceSelection() && bindWorkspaces();
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
if (!bindAll()) {
|
|
1078
|
+
var attempts = 0;
|
|
1079
|
+
retryTimer = root.setInterval(function () {
|
|
1080
|
+
attempts += 1;
|
|
1081
|
+
if (bindAll() || attempts >= 60) {
|
|
1082
|
+
root.clearInterval(retryTimer);
|
|
1083
|
+
retryTimer = null;
|
|
1084
|
+
}
|
|
1085
|
+
}, 1000);
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
return function () {
|
|
1089
|
+
if (retryTimer !== null) root.clearInterval(retryTimer);
|
|
1090
|
+
for (var i = 0; i < unsubs.length; i++) {
|
|
1091
|
+
try { unsubs[i](); } catch (e) { /* ignore */ }
|
|
1092
|
+
}
|
|
1093
|
+
};
|
|
1094
|
+
}, "dsh-session-tabs: session subscriptions");
|
|
1095
|
+
|
|
1096
|
+
// The strict session header's actions list is a per-session list slot;
|
|
1097
|
+
// the probe entry gives us the exact session id the shell renders.
|
|
1098
|
+
try {
|
|
1099
|
+
var disposeProbe = ctx.slots.inject("conversation.session.header.actions", function () {
|
|
1100
|
+
return ctx.slots.register({
|
|
1101
|
+
name: "conversation.session.header.actions",
|
|
1102
|
+
id: "dsh-session-tabs-current",
|
|
1103
|
+
order: -100,
|
|
1104
|
+
inject: function () { return { ctx: ctx }; }
|
|
1105
|
+
}, CurrentProbe);
|
|
1106
|
+
});
|
|
1107
|
+
ctx.effect(function () {
|
|
1108
|
+
return function () {
|
|
1109
|
+
if (typeof disposeProbe === "function") disposeProbe();
|
|
1110
|
+
};
|
|
1111
|
+
}, "dsh-session-tabs: current-session probe");
|
|
1112
|
+
} catch (e) {
|
|
1113
|
+
try { console.warn("[dsh-session-tabs] session header actions slot unavailable", e); } catch (e2) { /* ignore */ }
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
// The header-leading slot is declared by ui-conversation; inject() waits
|
|
1117
|
+
// for that declaration (and re-registers after plugin reloads).
|
|
1118
|
+
try {
|
|
1119
|
+
var disposeRegistration = ctx.slots.inject("conversation.header.leading", function () {
|
|
1120
|
+
return ctx.slots.register({
|
|
1121
|
+
name: "conversation.header.leading",
|
|
1122
|
+
locale: NS,
|
|
1123
|
+
inject: function () { return { ctx: ctx }; }
|
|
1124
|
+
}, Leading);
|
|
1125
|
+
});
|
|
1126
|
+
ctx.effect(function () {
|
|
1127
|
+
return function () {
|
|
1128
|
+
if (typeof disposeRegistration === "function") disposeRegistration();
|
|
1129
|
+
};
|
|
1130
|
+
}, "dsh-session-tabs: header registration");
|
|
1131
|
+
} catch (e) {
|
|
1132
|
+
try { console.warn("[dsh-session-tabs] header-leading slot unavailable", e); } catch (e2) { /* ignore */ }
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
try {
|
|
1136
|
+
console.log("[dsh-session-tabs] active (tabs: " + state.tabs.length + ")");
|
|
1137
|
+
} catch (e) { /* ignore */ }
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
exports.inject = inject;
|
|
1141
|
+
exports.apply = apply;
|
|
1142
|
+
return module.exports;
|
|
1143
|
+
}
|
|
1144
|
+
});
|
|
1145
|
+
})(typeof window !== "undefined" ? window : typeof self !== "undefined" ? self : null);
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @lihengxi20/dsh-tabs (GitHub repo: dsh-session-tabs) — host half.
|
|
3
|
+
*
|
|
4
|
+
* This plugin is browser-only: the top session tabs live entirely in the web
|
|
5
|
+
* client (lib/client.js), which dsh-client-modules serves as the package's
|
|
6
|
+
* `/client` bundle. The host entry exists so the Loader tree owns a row for the
|
|
7
|
+
* package; without it dsh-client-modules would never scan the `dsh.client`
|
|
8
|
+
* declaration. Keep this module inert: DSH also loads it on non-web surfaces
|
|
9
|
+
* (CLI, headless) where the tab UI must not exist.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/** Plugin identity for cordis.yml rows. */
|
|
13
|
+
export const name = "@lihengxi20/dsh-tabs";
|
|
14
|
+
|
|
15
|
+
/** Inert host plugin: the browser half owns every behavior. */
|
|
16
|
+
export function apply() {}
|
package/package.json
CHANGED
|
@@ -1,6 +1,66 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lihengxi20/dsh-tabs",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Session tabs in the DSH conversation header: switch, close, rename and persist open sessions, with workspace/title labels and live status.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "lib/index.js",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": "./lib/index.js",
|
|
10
|
+
"./client": "./lib/client.js",
|
|
11
|
+
"./package.json": "./package.json"
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"lib/index.js",
|
|
15
|
+
"lib/client.js",
|
|
16
|
+
"cordis.patch.yml",
|
|
17
|
+
"assets",
|
|
18
|
+
"screenshots.json",
|
|
19
|
+
"README.md",
|
|
20
|
+
"LICENSE"
|
|
21
|
+
],
|
|
22
|
+
"engines": {
|
|
23
|
+
"node": ">=18"
|
|
24
|
+
},
|
|
25
|
+
"keywords": [
|
|
26
|
+
"deepseek-harness",
|
|
27
|
+
"dsh",
|
|
28
|
+
"dsh-plugin",
|
|
29
|
+
"dsh-web",
|
|
30
|
+
"session",
|
|
31
|
+
"tabs",
|
|
32
|
+
"会话标签",
|
|
33
|
+
"顶部标签页"
|
|
34
|
+
],
|
|
35
|
+
"repository": {
|
|
36
|
+
"type": "git",
|
|
37
|
+
"url": "git+https://github.com/lihengxi20/dsh-session-tabs.git"
|
|
38
|
+
},
|
|
39
|
+
"homepage": "https://github.com/lihengxi20/dsh-session-tabs#readme",
|
|
40
|
+
"bugs": {
|
|
41
|
+
"url": "https://github.com/lihengxi20/dsh-session-tabs/issues"
|
|
42
|
+
},
|
|
43
|
+
"author": "lihengxi20",
|
|
44
|
+
"publishConfig": {
|
|
45
|
+
"access": "public",
|
|
46
|
+
"registry": "https://registry.npmjs.org/"
|
|
47
|
+
},
|
|
48
|
+
"dsh": {
|
|
49
|
+
"bundle": {
|
|
50
|
+
"patch": "./cordis.patch.yml"
|
|
51
|
+
},
|
|
52
|
+
"client": {
|
|
53
|
+
"inject": [
|
|
54
|
+
"@deepseek-ai/dsh-client-ui-session",
|
|
55
|
+
"@deepseek-ai/dsh-client-ui-conversation",
|
|
56
|
+
"@deepseek-ai/dsh-client-locale"
|
|
57
|
+
],
|
|
58
|
+
"platform": "web"
|
|
59
|
+
},
|
|
60
|
+
"compatibility": {
|
|
61
|
+
"dshReleases": {
|
|
62
|
+
"0.2.2-rc.1": "compatible"
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
6
66
|
}
|