@yiliang114/dsh-session-deck 0.0.0-stage → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 yiliang114
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,153 @@
1
- # Temporary Holding Version
1
+ # DSH Session Deck
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ [简体中文](https://github.com/yiliang114/dsh-session-deck/blob/main/README.zh-CN.md)
4
+
5
+ **Keep track of work across DSH workspaces without leaving your conversation.** Session Deck adds Activity and Favorites views to the native DeepSeek Harness sidebar, so you can spot running agents, reply to waiting sessions, and return to completed results.
6
+
7
+ A community plugin with an English interface, no extra npm dependencies, and no telemetry. Licensed under [MIT](LICENSE).
8
+
9
+ ## Three views, one sidebar
10
+
11
+ | View | What it helps you do |
12
+ | --- | --- |
13
+ | **Activity** | Follow running, waiting, unread, and recent sessions across all workspaces. |
14
+ | **Favorites** | Keep important sessions close using DSH's native pinned sessions. |
15
+ | **Workspaces** | Return to the native workspace tree, its management tools, and other plugins' menus. |
16
+
17
+ Switching views keeps your current conversation open. Session rows show the title, workspace, time, and status; their menus provide favorite, rename, fork, archive, and copy-ID actions.
18
+
19
+ *Screenshots captured from a local DSH installation in English; session titles and workspace names anonymized.*
20
+
21
+ | Activity | Favorites | Workspaces |
22
+ | --- | --- | --- |
23
+ | Follow active work and unread results. | Return to native pinned sessions. | Keep the complete workspace controls. |
24
+ | ![English Activity view](https://raw.githubusercontent.com/yiliang114/dsh-session-deck/main/docs/images/activity.png) | ![English Favorites view](https://raw.githubusercontent.com/yiliang114/dsh-session-deck/main/docs/images/favorites.png) | ![English native Workspaces view](https://raw.githubusercontent.com/yiliang114/dsh-session-deck/main/docs/images/workspaces.png) |
25
+
26
+ Screenshots show the switcher above the workspace region without host patches, below global panels. The interface and anonymized session titles are English; browser menus, tabs, and the address bar are excluded.
27
+
28
+ ## Install locally
29
+
30
+ The package name is **`@yiliang114/dsh-session-deck`**. A local checkout or tarball can be installed without npm publication; for registry installation, check version availability as described below.
31
+
32
+ Requirements:
33
+
34
+ - Node.js **22 or newer**.
35
+ - DSH Web GUI **0.2.1-rc.2**. This is the exact supported host version; other versions are unverified.
36
+ - DSH's native sidebar, session, and workspace plugins enabled.
37
+
38
+ ```sh
39
+ git clone https://github.com/yiliang114/dsh-session-deck.git
40
+ cd dsh-session-deck
41
+ npm run verify
42
+ dsh plugin --profile web add "link:$PWD" --offline --ignore-scripts
43
+ ```
44
+
45
+ There are no dependencies to install. The repository includes the client bundle; `build` checks JavaScript syntax rather than generating it. Replace `web` if you use another profile. Keep the checkout in place while using a linked installation.
46
+
47
+ You can also install through **Plugins → Add plugin → Install a third-party plugin**: enter `link:/absolute/path/to/dsh-session-deck`, choose **Install**, then **Enable now**, and refresh the page.
48
+
49
+ The plugin's own labels are English. To make the rest of DSH English too, use **More → Settings → General → Language → English**. Session titles remain in the language you wrote them.
50
+
51
+ Refresh your existing DSH GUI, then choose **Workspaces / Activity / Favorites** above the workspace region, below global panels. On the supported DSH 0.2.1-rc.2 layout, the plugin places the switcher there automatically without patching installed DSH files. The native workspace browser remains available in Workspaces. Refresh after client changes too; hot reload is not assumed.
52
+
53
+ If you installed an earlier local version of this plugin, disable and remove it in DSH's plugin manager first to avoid two instances controlling the sidebar. You can keep its browser storage.
54
+
55
+ ### Install from a tarball
56
+
57
+ To distribute the plugin without npm, run from the checkout:
58
+
59
+ ```sh
60
+ npm run verify
61
+ npm pack --ignore-scripts
62
+ ```
63
+
64
+ Copy the generated archive to the target machine, then install using its absolute path:
65
+
66
+ ```sh
67
+ dsh plugin --profile web add /absolute/path/yiliang114-dsh-session-deck-0.1.2.tgz --ignore-scripts
68
+ ```
69
+
70
+ Refresh the GUI. A tarball installation does not depend on the original checkout. Both linked and tarball installations place the switcher above the workspace region automatically on the supported rc.2 layout; neither needs a host patch.
71
+
72
+ ### npm installation and market discovery
73
+
74
+ **Confirm that version 0.1.2 exists in the official registry before installing:**
75
+
76
+ ```sh
77
+ npm view @yiliang114/dsh-session-deck@0.1.2 version --registry=https://registry.npmjs.org
78
+ dsh plugin --profile web add @yiliang114/dsh-session-deck@0.1.2 --ignore-scripts
79
+ ```
80
+
81
+ The [market index](market-index.json) lists only verified npm releases. An empty `plugins: []` means there are **no released entries** to display. DSH's custom plugin market uses user-added index sources; npm publication does not automatically collect or list a plugin in an official DSH market.
82
+
83
+ After a confirmed release is added to the index, its direct URL can be added through DSH's custom plugin market:
84
+
85
+ ```text
86
+ https://raw.githubusercontent.com/yiliang114/dsh-session-deck/main/market-index.json
87
+ ```
88
+
89
+ Maintainers: see the [release guide](https://github.com/yiliang114/dsh-session-deck/blob/main/docs/releasing.md) for npm bootstrap, OIDC configuration, and index updates.
90
+
91
+ ## How session status works
92
+
93
+ - **Activity keeps all running, waiting, and unread sessions**, plus at most **20 ordinary recent sessions**, sorted by DSH's `updatedAt` rather than last-opened time. Favorites has no 20-session limit.
94
+ - Favorites are **native DSH pins**, shared with the workspace tree. Archived, subagent, and empty sessions are omitted from the lists; ordinary forks remain visible.
95
+ - Status priority is **waiting for a reply → running → unread**. A parent remains running while any known subagent descendant is running; ordinary forks do not propagate status to their source.
96
+ - Aggregation covers only descendants known to DSH's session directory. **Independent background shell jobs are not tracked**: an idle session does not mean every process it launched has exited.
97
+ - A session becomes unread when it and its known descendants finish while you are viewing something else. Opening it clears the marker. Persisted observations can recover completions across a refresh, but cannot reconstruct work that started and finished while every page was closed or was never observed.
98
+ - Archiving an active session requires a **Stop and archive** confirmation. There is no permanent-delete action. Switch to Workspaces for the complete native menus, including actions from other plugins.
99
+
100
+ ## DSH compatibility and switcher position
101
+
102
+ This plugin declares an exact `engines.dsh` match of **0.2.1-rc.2**. Automatic top placement uses a DOM compatibility adapter for that release, not an official DSH extension API.
103
+
104
+ | DSH sidebar | Placement mechanism | Session Deck support |
105
+ | --- | --- | --- |
106
+ | Official **0.2.1-rc.2**, without patches | No official `sidebar.workspaces.before` slot; the adapter places the switcher above the workspace region by default. | Supported target, with a precise rc.2 DOM structure guard. |
107
+ | **0.2.1-rc.2** with the legacy local patch | Reuses the existing `sidebar.workspaces.before` slot below global panels. | Compatible; the patch is no longer needed. |
108
+ | Official **0.2.1-rc.3** | The published client and slot contract still lack the before-slot. | Not supported: full compatibility is unverified and the exact engine declaration excludes it. |
109
+
110
+ On the recognized rc.2 layout, the adapter renders a React portal into its own DOM host immediately before the workspace region, below global panels such as Automation tasks. It does not write installed DSH files or replace the native workspace browser. An unknown DOM layout falls back to the footer. Disabling the plugin removes its scoped observer and DOM host.
111
+
112
+ The rc.3 published files were inspected on **2026-10-23**; that inspection does not establish plugin compatibility. Supporting another host version requires validating its session, workspace, slot, and UI APIs and DOM layout before widening `engines.dsh`.
113
+
114
+ ## Legacy sidebar patch rollback
115
+
116
+ The [sidebar patch script](scripts/patch-sidebar-before.mjs) is retained for rollback and backward compatibility. New installations do not need it. If you previously installed its local before-slot patch, the plugin can reuse that slot, or you can restore the original sidebar.
117
+
118
+ Locate the `@deepseek-ai/dsh-client-ui-sidebar` package actually used by your running DSH. From this repository, replace the placeholder with that package's directory, not the DSH root or this plugin directory:
119
+
120
+ ```sh
121
+ node scripts/patch-sidebar-before.mjs /path/to/dsh-client-ui-sidebar --rollback
122
+ ```
123
+
124
+ Rollback requires the original adjacent `.dsa-before.backup` files and refuses to overwrite unrelated changes. Refresh the GUI after rollback: the new plugin still places the switcher above the workspace region on the supported unpatched rc.2 layout. If rolling back a DSH source checkout, rebuild the affected client artifacts as well.
125
+
126
+ ## Privacy and local storage
127
+
128
+ Session Deck uses DSH's session and workspace APIs. It makes **no model calls, sends no telemetry, and has no external upload endpoint**.
129
+
130
+ Browser `localStorage` retains the original keys for compatibility:
131
+
132
+ | Key | Stored data |
133
+ | --- | --- |
134
+ | `dsh-session-activity.mode.v1` | Selected view. |
135
+ | `dsh-session-activity.pending.v1` | Session IDs, observed running state, and completion/unread metadata. |
136
+
137
+ No conversation bodies are stored by the plugin. Favorites remain in DSH's native pinned data. View and unread state sync between tabs on the same origin, not across devices. If storage is unavailable, state stays in memory. Clearing browser storage resets this metadata; processed records are capped at 1,000, while running and unread records are retained.
138
+
139
+ Disable or remove **`@yiliang114/dsh-session-deck`** in DSH's plugin manager to stop using it. Disabling removes the adapter's observer and DOM host and restores the native workspace browser. Removal does not clear browser storage or undo a legacy host patch; use the rollback command separately if needed.
140
+
141
+ ## Development
142
+
143
+ ```sh
144
+ npm run verify
145
+ ```
146
+
147
+ This runs syntax checks, Node.js tests for session selection and state handling, and a dry-run package-content check. CI runs it on Node.js 22 and 24. There is no separate lint or TypeScript check, and automated checks do not replace testing the real DSH GUI.
148
+
149
+ The client lives in [lib/client.js](lib/client.js), with a minimal [host entry](src/host.mjs) and [Cordis configuration](cordis.patch.yml). Report bugs with your DSH version, installation method, and reproduction steps in [GitHub Issues](https://github.com/yiliang114/dsh-session-deck/issues).
150
+
151
+ ## License
152
+
153
+ [MIT](LICENSE). Independently maintained; not an official built-in DSH component.
@@ -0,0 +1,153 @@
1
+ # DSH Session Deck
2
+
3
+ [English](https://github.com/yiliang114/dsh-session-deck/blob/main/README.md)
4
+
5
+ **不用离开当前对话,就能掌握所有 DSH 工作区中的会话进展。** Session Deck 在 DeepSeek Harness 原生侧栏中加入 Activity 和 Favorites 视图,让你快速找到运行中的代理、等待回复的会话,以及尚未查看的完成结果。
6
+
7
+ 社区维护的第三方插件,默认英文界面,无额外 npm 依赖、无遥测,采用 [MIT 许可证](LICENSE)。
8
+
9
+ ## 三种视图,一个侧栏
10
+
11
+ | 视图 | 用途 |
12
+ | --- | --- |
13
+ | **Activity(活动)** | 跨工作区查看运行中、等待回复、未读和最近会话。 |
14
+ | **Favorites(收藏)** | 汇总 DSH 原生置顶会话,随时回到重要任务。 |
15
+ | **Workspaces(工作区)** | 使用原生工作区树、管理功能及其他插件提供的菜单。 |
16
+
17
+ 切换视图不会离开当前对话。会话行显示标题、工作区、时间和状态,菜单提供收藏、重命名、分叉、归档和复制 ID 等操作。
18
+
19
+ *截图来自本地 DSH 安装的英文界面;会话标题和工作区名称已匿名化。*
20
+
21
+ | Activity | Favorites | Workspaces |
22
+ | --- | --- | --- |
23
+ | 掌握运行状态和未读结果。 | 回到原生置顶的重要会话。 | 保留完整的工作区操作。 |
24
+ | ![英文 Activity 视图](https://raw.githubusercontent.com/yiliang114/dsh-session-deck/main/docs/images/activity.png) | ![英文 Favorites 视图](https://raw.githubusercontent.com/yiliang114/dsh-session-deck/main/docs/images/favorites.png) | ![英文原生 Workspaces 视图](https://raw.githubusercontent.com/yiliang114/dsh-session-deck/main/docs/images/workspaces.png) |
25
+
26
+ 截图展示无需宿主补丁、自动位于全局面板下方和工作区区域上方的切换条。界面及匿名化后的会话标题使用英文;不包含浏览器顶部菜单、标签栏和地址栏。
27
+
28
+ ## 本地安装
29
+
30
+ 包名为 **`@yiliang114/dsh-session-deck`**。本地仓库和 tarball 安装不依赖 npm 发布;使用注册表安装时,请先按下文确认版本是否可用。
31
+
32
+ 环境要求:
33
+
34
+ - Node.js **22 或以上**。
35
+ - DSH Web GUI **0.2.1-rc.2**,这是当前精确适配的宿主版本,其他版本未经验证。
36
+ - 已启用 DSH 原生侧栏、会话和工作区插件。
37
+
38
+ ```sh
39
+ git clone https://github.com/yiliang114/dsh-session-deck.git
40
+ cd dsh-session-deck
41
+ npm run verify
42
+ dsh plugin --profile web add "link:$PWD" --offline --ignore-scripts
43
+ ```
44
+
45
+ 项目没有需要安装的依赖,仓库已包含客户端文件;`build` 仅检查 JavaScript 语法,不生成客户端。使用其他 profile 时替换 `web`。链接安装期间请保留仓库目录。
46
+
47
+ 也可以通过 **Plugins → Add plugin → Install a third-party plugin** 安装:输入 `link:/absolute/path/to/dsh-session-deck`,点击 **Install**,然后 **Enable now**,最后刷新页面。
48
+
49
+ 插件自身文案使用英文。要让 DSH 其他界面也显示英文,在 **更多 → 设置 → 通用设置 → 语言** 中选择 **English**。会话标题仍保留用户原来使用的语言。
50
+
51
+ 刷新现有 DSH GUI,然后在全局面板下方、工作区区域上方选择 **Workspaces / Activity / Favorites**。在支持的 DSH 0.2.1-rc.2 布局中,插件会自动将切换条放到该位置,无需修改已安装的 DSH 文件。Workspaces 保留原生工作区浏览器。修改客户端后也需要刷新,不应假定热更新已启用。
52
+
53
+ 如果已安装本插件的早期本地版本,请先在 DSH 插件管理中停用并移除旧实例,避免同时接管侧栏;浏览器存储可以保留。
54
+
55
+ ### 使用 tarball 安装
56
+
57
+ 不经过 npm 注册表分发时,在仓库根目录执行:
58
+
59
+ ```sh
60
+ npm run verify
61
+ npm pack --ignore-scripts
62
+ ```
63
+
64
+ 将生成的归档文件复制到目标机器,再用绝对路径安装:
65
+
66
+ ```sh
67
+ dsh plugin --profile web add /absolute/path/yiliang114-dsh-session-deck-0.1.2.tgz --ignore-scripts
68
+ ```
69
+
70
+ 安装后刷新 GUI。tarball 安装不依赖原仓库目录持续存在。链接和 tarball 两种安装方式都会在支持的 rc.2 布局中自动将切换条放到工作区区域上方,无需宿主补丁。
71
+
72
+ ### npm 安装与市场发现
73
+
74
+ **安装前,先确认官方注册表中存在 0.1.2 版本:**
75
+
76
+ ```sh
77
+ npm view @yiliang114/dsh-session-deck@0.1.2 version --registry=https://registry.npmjs.org
78
+ dsh plugin --profile web add @yiliang114/dsh-session-deck@0.1.2 --ignore-scripts
79
+ ```
80
+
81
+ [市场索引](market-index.json)只列出经 npm 验证的发布版本。若内容为 `plugins: []`,表示**没有可显示的已发布条目**。DSH 自定义插件市场读取用户添加的索引源;发布 npm 不会触发官方 DSH 市场的自动收录。
82
+
83
+ 确认发布且更新索引后,可以在 DSH 自定义插件市场中添加以下直接 JSON 地址:
84
+
85
+ ```text
86
+ https://raw.githubusercontent.com/yiliang114/dsh-session-deck/main/market-index.json
87
+ ```
88
+
89
+ 维护者请参阅[发布指南(英文)](https://github.com/yiliang114/dsh-session-deck/blob/main/docs/releasing.md),了解首次 npm 发布、OIDC 授权和索引更新。
90
+
91
+ ## 会话状态与边界
92
+
93
+ - Activity 保留**全部运行中、等待回复和未读会话**,以及最多 **20 条普通最近会话**,按 DSH 的 `updatedAt` 排序,而非最后打开时间。Favorites 不受 20 条限制。
94
+ - 收藏直接使用 **DSH 原生置顶数据**,与工作区树同步。归档、子代理和空白会话不单独显示;普通分叉正常显示。
95
+ - 状态优先级为**等待回复 → 运行中 → 未读**。已知子代理后代仍在运行时,父会话保持运行状态;普通分叉不会向来源会话传播状态。
96
+ - 仅聚合 DSH 会话目录中已知的子代理后代。**独立后台 shell 作业不在跟踪范围内**,会话空闲不代表它启动的所有进程均已退出。
97
+ - 会话及已知后代全部停止时,如果你正在查看其他会话,它会标记为未读;打开即清除。持久化记录可恢复刷新期间发生的完成状态,但无法补全所有页面关闭期间完整开始并结束、或从未观察到的运行。
98
+ - 归档活动会话需要确认 **Stop and archive(停止并归档)**。插件不提供永久删除;其他插件贡献的完整原生菜单请切回 Workspaces 使用。
99
+
100
+ ## DSH 兼容性与切换条位置
101
+
102
+ 插件的 `engines.dsh` 精确声明支持 **0.2.1-rc.2**。自动顶部定位使用针对该版本的 DOM 兼容适配层,并非 DSH 官方扩展 API。
103
+
104
+ | DSH 侧栏 | 定位方式 | Session Deck 支持情况 |
105
+ | --- | --- | --- |
106
+ | 官方 **0.2.1-rc.2**,未打补丁 | 没有官方 `sidebar.workspaces.before` 槽位;适配层默认将切换条放到工作区区域上方。 | 当前支持目标,严格检查 rc.2 DOM 结构。 |
107
+ | **0.2.1-rc.2** 加旧版本地补丁 | 复用全局面板下方已有的 `sidebar.workspaces.before` 槽位。 | 兼容,但已不再需要补丁。 |
108
+ | 官方 **0.2.1-rc.3** | 已发布的客户端及槽位声明中仍没有 before 槽位。 | 不支持:整体兼容性未经验证,精确版本声明不包含它。 |
109
+
110
+ 识别到 rc.2 布局时,适配层在工作区区域紧前方、Automation tasks 等全局面板下方插入自己的 DOM 容器,通过 React portal 渲染切换条。它不会写入已安装的 DSH 文件,也不会替换原生工作区浏览器。遇到未知 DOM 布局时回退到底部。停用插件会移除其限定范围的观察器和 DOM 容器。
111
+
112
+ rc.3 发布文件检查于 **2026-10-23**,该检查不能证明插件兼容。支持其他宿主版本时,需要先验证会话、工作区、槽位、UI 接口及 DOM 布局,再扩大 `engines.dsh` 的版本范围。
113
+
114
+ ## 旧版侧栏补丁回滚
115
+
116
+ [侧栏补丁脚本](scripts/patch-sidebar-before.mjs)保留用于回滚和向后兼容,新安装无需执行。如果之前安装过本地 before 槽位补丁,插件可以继续复用该槽位,也可以恢复原侧栏。
117
+
118
+ 找到当前运行的 DSH 实际加载的 `@deepseek-ai/dsh-client-ui-sidebar` 包目录,在本仓库中执行以下命令。占位路径应替换为该侧栏包目录,而非 DSH 根目录或本插件目录:
119
+
120
+ ```sh
121
+ node scripts/patch-sidebar-before.mjs /path/to/dsh-client-ui-sidebar --rollback
122
+ ```
123
+
124
+ 恢复依赖原始的相邻 `.dsa-before.backup` 备份,并会拒绝覆盖无关改动。回滚后刷新 GUI:在支持的未打补丁 rc.2 布局中,新插件仍会将切换条放到工作区区域上方。如果回滚的是 DSH 源码仓库,还需重新构建相关客户端产物。
125
+
126
+ ## 隐私与本地存储
127
+
128
+ 插件使用 DSH 的会话和工作区 API,**不发起模型调用、不发送遥测、没有外部上传端点**。
129
+
130
+ 为兼容早期版本,浏览器 `localStorage` 沿用以下键:
131
+
132
+ | 键 | 内容 |
133
+ | --- | --- |
134
+ | `dsh-session-activity.mode.v1` | 当前视图。 |
135
+ | `dsh-session-activity.pending.v1` | 会话 ID、观察到的运行状态和完成未读元数据。 |
136
+
137
+ 插件不在其中保存会话正文。收藏仍使用 DSH 原生置顶数据。视图和未读状态在同源标签页间同步,不跨设备同步;存储不可用时仅保留在内存中。清除浏览器存储会重置这些元数据。已处理记录最多保留 1,000 条,运行中和未读记录不受此清理上限影响。
138
+
139
+ 停用时,在 DSH 插件管理中禁用或移除 **`@yiliang114/dsh-session-deck`**。停用会移除适配层的观察器和 DOM 容器,并恢复原生工作区浏览器。移除不会清除浏览器存储,也不会撤销旧版宿主补丁;如需撤销,请单独执行恢复命令。
140
+
141
+ ## 开发
142
+
143
+ ```sh
144
+ npm run verify
145
+ ```
146
+
147
+ 该命令依次执行语法检查、会话筛选与状态逻辑的 Node.js 测试,以及打包清单 dry-run 检查。CI 使用 Node.js 22 和 24。没有独立 lint 或 TypeScript 检查;自动检查不能替代真实 DSH GUI 验证。
148
+
149
+ 客户端位于 [lib/client.js](lib/client.js),另有最小[宿主入口](src/host.mjs)和 [Cordis 配置](cordis.patch.yml)。反馈问题时,请在 [GitHub Issues](https://github.com/yiliang114/dsh-session-deck/issues) 中提供 DSH 版本、安装方式和复现步骤。
150
+
151
+ ## 许可证
152
+
153
+ [MIT](LICENSE)。本项目独立维护,不属于 DSH 官方内置组件。
@@ -0,0 +1,3 @@
1
+ - insert:
2
+ - id: yiliang114-session-deck
3
+ name: '@yiliang114/dsh-session-deck'
package/lib/client.js ADDED
@@ -0,0 +1,372 @@
1
+ // Hand-written lazy ModuleLoader bundle: no dependency installation or build.
2
+ window.__ModuleLoader__.load({
3
+ id: '@yiliang114/dsh-session-deck',
4
+ factory: (require) => {
5
+ const { createElement: h, Fragment, useEffect, useLayoutEffect, useState, useSyncExternalStore } = require('react')
6
+ const { createPortal } = require('react-dom')
7
+ const { Menu, MenuItemButton, Modal, Button, writeClipboard, IconEllipsisOutlineRegular, IconArchiveOutlineRegular } = require('@deepseek-ai/dsh-client-ui-primitives')
8
+ const KEY = 'dsh-session-activity.mode.v1'
9
+ const PENDING_KEY = 'dsh-session-activity.pending.v1'
10
+ const MAX_RECORDS = 1000
11
+ const identity = x => x
12
+ // Client SessionSummary exposes parentId; only subagent edges represent work.
13
+ // byId also includes retained children absent from the Host-list ids.
14
+ function aggregateRunning(list, statuses) {
15
+ const running = new Map(), queue = []
16
+ for (const s of Object.values(list.byId)) {
17
+ const value = statuses.get(s.id)?.running ?? s.running
18
+ running.set(s.id, value)
19
+ if (value === true) queue.push(s.id)
20
+ }
21
+ for (let i = 0; i < queue.length; i++) {
22
+ const s = list.byId[queue[i]]
23
+ if (s.origin !== 'subagent' || !running.has(s.parentId) || running.get(s.parentId) === true) continue
24
+ running.set(s.parentId, true)
25
+ queue.push(s.parentId)
26
+ }
27
+ return running
28
+ }
29
+ // Only observed runs or native unread signals create metadata, never idle history.
30
+ function createPendingStore(storage) {
31
+ const read = () => {
32
+ try {
33
+ const value = JSON.parse(storage?.getItem(PENDING_KEY) || '{}')
34
+ return Object.fromEntries(Object.entries(value).filter(([id, r]) => id && r &&
35
+ typeof r.running === 'boolean' && typeof r.pending === 'boolean' &&
36
+ typeof r.seeded === 'boolean' && Number.isFinite(r.revision)))
37
+ } catch { return {} }
38
+ }
39
+ let records = read()
40
+ const listeners = new Set(), observed = new Map()
41
+ const trim = value => {
42
+ let handled = 0
43
+ return Object.fromEntries(Object.entries(value).sort((a, b) => b[1].revision - a[1].revision)
44
+ .filter(([, r]) => r.pending || r.running || handled++ < MAX_RECORDS))
45
+ }
46
+ records = trim(records)
47
+ const merge = () => {
48
+ const next = { ...records }
49
+ for (const [id, r] of Object.entries(read())) if (!next[id] || r.revision > next[id].revision) next[id] = r
50
+ return next
51
+ }
52
+ function publish(next, persist) {
53
+ next = trim(next)
54
+ if (JSON.stringify(next) === JSON.stringify(records)) return
55
+ records = next
56
+ if (persist) { try { storage?.setItem(PENDING_KEY, JSON.stringify(records)) } catch {} }
57
+ for (const fn of listeners) fn()
58
+ }
59
+ function update(next, id, patch) {
60
+ const old = next[id]
61
+ next[id] = { running: false, pending: false, seeded: false, ...old, ...patch,
62
+ revision: Math.max(Date.now(), (old?.revision || 0) + 1) }
63
+ }
64
+ return {
65
+ getSnapshot: () => records,
66
+ subscribe: fn => { listeners.add(fn); return () => listeners.delete(fn) },
67
+ sync: () => publish(merge(), false),
68
+ observe(list, statuses) {
69
+ // Hydration placeholders must not turn a persisted running baseline into a completion.
70
+ if (list.phase !== 'ready') return
71
+ const next = merge(), runningById = aggregateRunning(list, statuses)
72
+ for (const id of list.ids ?? []) {
73
+ const s = list.byId[id]
74
+ if (!s || s.origin === 'subagent' || s.blank) continue
75
+ const status = statuses.get(id), running = runningById.get(id)
76
+ if (typeof running !== 'boolean') continue
77
+ const old = next[id]
78
+ const previous = observed.has(id) ? observed.get(id) : old?.running
79
+ const finished = previous === true && !running && old?.running !== false
80
+ const seed = !running && !!status?.completionUnread && !old?.seeded
81
+ const read = (s.retainedBy?.mainView ?? 0) > 0
82
+ // A remote acknowledgement wins over an unchanged local snapshot.
83
+ if (finished || seed || (running && previous !== true) || (old && previous !== running)) {
84
+ update(next, id, { running, pending: !read && (!!old?.pending || finished || seed),
85
+ seeded: !!old?.seeded || seed })
86
+ } else if (read && next[id]?.pending) {
87
+ // Codex-style: viewing the session marks its finished output as read.
88
+ update(next, id, { pending: false })
89
+ }
90
+ observed.set(id, running)
91
+ }
92
+ publish(next, true)
93
+ },
94
+ acknowledge(id) {
95
+ const next = merge()
96
+ if (next[id]?.pending) update(next, id, { pending: false, seeded: true })
97
+ publish(next, true)
98
+ },
99
+ }
100
+ }
101
+ function selectActivity(list, statuses, workspaces, limit = 20, pending = {}) {
102
+ const runningById = aggregateRunning(list, statuses)
103
+ const archived = new Set(workspaces.archivedSessionIds ?? [])
104
+ const pins = new Set(workspaces.pinnedSessionIds ?? [])
105
+ const membership = new Map()
106
+ for (const w of workspaces.items ?? []) for (const id of w.sessionIds) membership.set(id, w)
107
+ const rows = (list.ids ?? []).map(id => list.byId[id]).filter(s => s && s.origin !== 'subagent' && !archived.has(s.id) && !s.blank).map(s => {
108
+ const state = statuses.get(s.id)
109
+ const kind = state?.pendingInteraction ? 'waiting' : runningById.get(s.id) ? 'running' : pending[s.id]?.pending ? 'pending' : 'recent'
110
+ const workspace = membership.get(s.id)
111
+ return { ...s, kind, pinned: pins.has(s.id), workspace: workspace?.title || s.cwd?.split(/[\\/]/).filter(Boolean).pop() || 'Ungrouped', time: Number.isFinite(s.updatedAt) ? s.updatedAt : 0 }
112
+ }).sort((a, b) => b.time - a.time || a.id.localeCompare(b.id))
113
+ const recent = rows.filter(s => s.kind === 'recent').slice(0, limit)
114
+ const visible = new Set(recent.map(s => s.id))
115
+ return { active: rows.filter(s => s.kind !== 'recent'), recent, activity: rows.filter(s => s.kind !== 'recent' || visible.has(s.id)), favorites: rows.filter(s => s.pinned) }
116
+ }
117
+ function createModeStore(storage) {
118
+ let mode = 'workspace'
119
+ try { const saved = storage?.getItem(KEY); if (saved === 'activity' || saved === 'favorites') mode = saved } catch {}
120
+ const listeners = new Set()
121
+ return {
122
+ getSnapshot: () => mode,
123
+ subscribe: fn => { listeners.add(fn); return () => listeners.delete(fn) },
124
+ set(next, persist = true) {
125
+ next = next === 'activity' || next === 'favorites' ? next : 'workspace'
126
+ if (mode === next) return
127
+ mode = next
128
+ if (persist) { try { storage?.setItem(KEY, mode) } catch {} }
129
+ for (const fn of listeners) fn()
130
+ },
131
+ }
132
+ }
133
+ function ago(time, now) {
134
+ if (!time) return ''
135
+ const minutes = Math.max(0, Math.floor((now - time) / 60000))
136
+ if (minutes < 1) return 'Just now'
137
+ if (minutes < 60) return `${minutes}m ago`
138
+ if (minutes < 1440) return `${Math.floor(minutes / 60)}h ago`
139
+ return `${Math.floor(minutes / 1440)}d ago`
140
+ }
141
+ // Compatibility adapter for DSH 0.2.1-rc.2, NOT an official public slot.
142
+ // Only our container is inserted/removed; native and React-owned nodes stay put.
143
+ // Capture the anchor's own sidebar, never a document-wide first match.
144
+ function mountNavigationHost(anchor, onHost) {
145
+ const root = anchor?.closest('.hHd-Xa_root')
146
+ const shape = () => {
147
+ if (!root || anchor.closest('.hHd-Xa_root') !== root) return null
148
+ const children = [...root.children]
149
+ const regions = children.filter(node => node.classList.contains('hHd-Xa_regionArea'))
150
+ const footers = children.filter(node => node.classList.contains('hHd-Xa_footArea'))
151
+ return regions.length === 1 && footers.length === 1 && footers[0].contains(anchor) &&
152
+ children.indexOf(regions[0]) < children.indexOf(footers[0]) ? regions[0] : null
153
+ }
154
+ const Observer = anchor?.ownerDocument.defaultView?.MutationObserver
155
+ if (!shape() || !Observer) { onHost(null); return () => {} }
156
+ const host = anchor.ownerDocument.createElement('div')
157
+ host.className = 'dsa-navigation-host'
158
+ let active = true, published
159
+ const sync = () => {
160
+ if (!active) return
161
+ const region = shape()
162
+ if (region) {
163
+ if (host.parentNode !== root || host.nextSibling !== region) root.insertBefore(host, region)
164
+ } else host.remove()
165
+ const next = region ? host : null
166
+ if (published !== next) { published = next; onHost(next) }
167
+ }
168
+ // No subtree/body observation: portal rendering cannot trigger this observer.
169
+ // The adjacency guard makes our own insertion notification a no-op.
170
+ const observer = new Observer(sync)
171
+ observer.observe(root, { childList: true })
172
+ sync()
173
+ return () => { active = false; observer.disconnect(); host.remove() }
174
+ }
175
+ const labels = { running: 'Running', waiting: 'Waiting for reply', pending: 'Unread', recent: 'Recent sessions' }
176
+ const css = `
177
+ .dsa-root{display:flex;flex-direction:column;flex:1;min-height:0;overflow:hidden;color:var(--dsw-alias-label-primary,inherit)}
178
+ .dsa-header{display:flex;align-items:baseline;justify-content:space-between;padding:18px 18px 10px;gap:8px}
179
+ .dsa-heading{margin:0;font:inherit;font-size:12px;font-weight:600;letter-spacing:.02em}
180
+ .dsa-caption{font-size:10px;color:var(--dsw-alias-label-tertiary,#888)}
181
+ .dsa-scroll{overflow-y:auto;min-height:0;padding:0 8px 16px;scrollbar-width:thin}
182
+ .dsa-item{position:relative;margin:2px 0;border-radius:8px;isolation:isolate}
183
+ .dsa-item:hover,.dsa-item:focus-within,.dsa-item[data-current=true]{background:var(--dsw-alias-interactive-bg-hover,#8882)}
184
+ .dsa-row{display:block;box-sizing:border-box;width:100%;padding:9px 10px;border:0;border-radius:8px;background:transparent;color:inherit;font:inherit;text-align:left;cursor:pointer}
185
+ .dsa-line1{display:flex;align-items:center;gap:10px;min-width:0}
186
+ .dsa-title-text{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:13px;line-height:19px;font-weight:450}
187
+ .dsa-trail{display:flex;align-items:center;gap:6px;flex:none;color:var(--dsw-alias-label-tertiary,#888);transition:opacity .12s ease}
188
+ .dsa-trail time{font-size:10px;font-variant-numeric:tabular-nums}
189
+ .dsa-pin-mark{font-size:11px;line-height:1;color:var(--dsw-alias-label-tertiary,#999)}
190
+ .dsa-dot{width:6px;height:6px;flex:none;border-radius:50%}
191
+ .dsa-dot[data-kind=waiting]{background:#d6a45c}
192
+ .dsa-dot[data-kind=pending]{background:#8fa9cf}
193
+ .dsa-spin{width:11px;height:11px;flex:none;color:var(--dsw-alias-label-secondary,#aaa);animation:dsa-rotate .9s linear infinite}
194
+ .dsa-spin-track{stroke:currentColor;opacity:.22}
195
+ .dsa-spin-arc{stroke:currentColor}
196
+ @keyframes dsa-rotate{to{transform:rotate(360deg)}}
197
+ .dsa-meta{margin:2px 0 0;font-size:11px;line-height:15px;color:var(--dsw-alias-label-tertiary,#888);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
198
+ .dsa-actions{position:absolute;right:6px;top:6px;display:flex;gap:1px;align-items:center;height:24px;opacity:0;transition:opacity .12s ease}
199
+ .dsa-item:hover .dsa-actions,.dsa-item:focus-within .dsa-actions,.dsa-item[data-menu-open=true] .dsa-actions{opacity:1}
200
+ .dsa-item:hover .dsa-trail,.dsa-item:focus-within .dsa-trail,.dsa-item[data-menu-open=true] .dsa-trail{opacity:0}
201
+ .dsa-menu-trigger{display:flex;align-items:center;justify-content:center;width:24px;height:24px;padding:0;border:0;border-radius:5px;background:transparent;color:var(--dsw-alias-label-secondary,#aaa);cursor:pointer}
202
+ .dsa-menu-trigger:hover{background:var(--dsw-alias-interactive-bg-hover,#8883);color:var(--dsw-alias-label-primary,inherit)}
203
+ .dsa-menu-trigger:disabled{cursor:wait}
204
+ .dsa-row:focus-visible,.dsa-menu-trigger:focus-visible,.dsa-switch button:focus-visible{outline:2px solid var(--dsw-alias-label-secondary,#aaa);outline-offset:-2px}
205
+ .dsa-navigation-host{flex-shrink:0;padding:8px 0}
206
+ .dsa-switch{display:flex;gap:2px;padding:3px;border-radius:8px;background:var(--dsw-alias-interactive-bg-hover,#8881);flex-shrink:0;min-width:0}
207
+ .dsa-switch button{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;border:0;border-radius:5px;padding:6px 6px;background:transparent;color:var(--dsw-alias-label-tertiary,#888);font:inherit;font-size:11px;cursor:pointer;white-space:nowrap}
208
+ .dsa-switch[data-compact=true]{flex-direction:column}
209
+ .dsa-switch[data-compact=true] button{padding:6px 0}
210
+ .dsa-switch button[aria-pressed=true]{background:var(--dsw-alias-interactive-bg-hover,#8882);color:var(--dsw-alias-label-primary,inherit);font-weight:600}
211
+ .dsa-empty{margin:22px 12px;font-size:12px;line-height:1.8;color:var(--dsw-alias-label-tertiary,#888)}
212
+ .dsa-rename{box-sizing:border-box;width:100%;font:inherit;padding:8px;border-radius:6px;border:1px solid var(--dsw-alias-label-tertiary,#777);background:transparent;color:inherit}
213
+ @media(hover:none){.dsa-actions{opacity:1}.dsa-trail{opacity:0}}
214
+ @media(prefers-reduced-motion:reduce){.dsa-actions,.dsa-trail{transition:none}.dsa-spin{animation:none}}
215
+ `
216
+
217
+ function apply(ctx) {
218
+ let storage
219
+ try { storage = window.localStorage } catch {}
220
+ const mode = createModeStore(storage)
221
+ const pending = createPendingStore(storage)
222
+ const style = document.createElement('style')
223
+ style.id = 'yiliang114-dsh-session-deck-style'
224
+ style.textContent = css
225
+ const onStorage = event => { if (event.key === PENDING_KEY) pending.sync(); if (event.key === KEY || event.key === null) { let value; try { value = storage?.getItem(KEY) } catch {} mode.set(value, false) } }
226
+ ctx.effect(() => {
227
+ document.getElementById(style.id)?.remove()
228
+ document.head.appendChild(style)
229
+ window.addEventListener('storage', onStorage)
230
+ return () => { style.remove(); window.removeEventListener('storage', onStorage) }
231
+ })
232
+ function Switch({ wide, useSessions, useSessionStatus }) {
233
+ const list = useSessions(identity), statuses = useSessionStatus(identity)
234
+ useEffect(() => pending.observe(list, statuses), [list, statuses])
235
+ const selected = useSyncExternalStore(mode.subscribe, mode.getSnapshot)
236
+ return h('div', { className: 'dsa-switch', role: 'group', 'aria-label': 'Session navigation view', 'data-compact': !wide },
237
+ ...[['workspace', 'Workspaces', 'W'], ['activity', 'Activity', 'A'], ['favorites', 'Favorites', '★']].map(([value, label, compact]) => h('button', { key: value, type: 'button', title: label, 'aria-label': label, 'aria-pressed': selected === value, onClick: () => mode.set(value) }, wide ? label : compact)))
238
+ }
239
+ function FooterNavigation(props) {
240
+ const [anchor, setAnchor] = useState(null)
241
+ const [host, setHost] = useState(null)
242
+ useLayoutEffect(() => mountNavigationHost(anchor, setHost), [anchor])
243
+ // The hidden callback ref stays in the registered footer's React tree.
244
+ // A portal retains slot/React context and all injected props. Unknown DOM
245
+ // shapes render inline in that footer instead of guessing a destination.
246
+ return h(Fragment, null, h('span', { hidden: true, ref: setAnchor }),
247
+ host ? createPortal(h(Switch, props), host) : h(Switch, props))
248
+ }
249
+ function Activity({ wide, expandSidebar, useSessions, useSessionStatus, useWorkspaces }) {
250
+ const list = useSessions(identity)
251
+ const statuses = useSessionStatus(identity)
252
+ const workspaces = useWorkspaces(identity)
253
+ const [now, setNow] = useState(Date.now)
254
+ useEffect(() => { const timer = setInterval(() => setNow(Date.now()), 60000); return () => clearInterval(timer) }, [])
255
+ const selectedMode = useSyncExternalStore(mode.subscribe, mode.getSnapshot)
256
+ const queue = useSyncExternalStore(pending.subscribe, pending.getSnapshot)
257
+ const groups = selectActivity(list, statuses, workspaces, 20, queue)
258
+ if (!wide) return h('button', { type: 'button', className: 'dsa-row', title: 'Expand session activity', 'aria-label': 'Expand session activity', onClick: expandSidebar }, '◷')
259
+ const favorites = selectedMode === 'favorites'
260
+ const rows = favorites ? groups.favorites : groups.activity
261
+ return h('section', { className: 'dsa-root', 'aria-label': favorites ? 'Favorite sessions' : 'Session activity' },
262
+ h('header', { className: 'dsa-header' }, h('h2', { className: 'dsa-heading' }, favorites ? 'Favorites' : 'Activity'), h('span', { className: 'dsa-caption' }, 'All workspaces')),
263
+ h('div', { className: 'dsa-scroll' }, ...rows.map(s => h(SessionRow, { key: s.id, s, now, showPin: !favorites })),
264
+ !rows.length && h('p', { className: 'dsa-empty' }, list.phase !== 'ready' ? 'Loading sessions…' : favorites ? 'Favorite sessions to keep them close at hand.' : 'Recent sessions will appear here.')))
265
+ }
266
+
267
+ // Native child slots (`sidebar.workspaces.session.menu.item` and
268
+ // `sidebar.workspaces.session.row.action`) are declared by the shipped
269
+ // WorkspaceBrowser entry. A replacing entry cannot declare them again, so
270
+ // the activity rows carry their own menu built from public uiWorkspace
271
+ // actions instead of rendering those slots.
272
+ function SessionRow({ s, now, showPin }) {
273
+ const [menuOpen, setMenuOpen] = useState(false)
274
+ const [dialog, setDialog] = useState(null)
275
+ const [draft, setDraft] = useState('')
276
+ const [busy, setBusy] = useState(false)
277
+ const [error, setError] = useState('')
278
+ const title = s.displayTitle || s.title || s.id
279
+ const run = async operation => {
280
+ if (busy) return
281
+ setMenuOpen(false); setBusy(true); setError('')
282
+ try { await operation(); setDialog(null) }
283
+ catch (reason) { setError(reason?.message || String(reason)); setDialog('error') }
284
+ finally { setBusy(false) }
285
+ }
286
+ const archive = async (stopActivity = false) => {
287
+ if (busy) return
288
+ setMenuOpen(false); setBusy(true); setError('')
289
+ try {
290
+ await ctx.uiWorkspace.archiveSession(s.id, stopActivity ? { stopActivity: true } : undefined)
291
+ setDialog(null)
292
+ } catch (reason) {
293
+ if (!stopActivity && reason?.name === 'WorkspaceArchiveError' && reason?.rpcError?.code === 'workspace/session-active') setDialog('archive')
294
+ else { setError(reason?.message || String(reason)); setDialog('error') }
295
+ } finally { setBusy(false) }
296
+ }
297
+ const close = () => { if (!busy) { setDialog(null); setError('') } }
298
+ const rename = () => run(async () => {
299
+ const result = await ctx.sessions.using(s.id, { source: 'workspaceOperation' }, reference => reference.binding.session.rename(draft.trim()))
300
+ if (!result.ok) throw new Error(result.error.message)
301
+ })
302
+ const item = (label, onClick) => h(MenuItemButton, { key: label, disabled: busy, onSelect: onClick }, label)
303
+ const current = (s.retainedBy?.mainView ?? 0) > 0
304
+ const mark = s.kind === 'running' ? h('svg', { className: 'dsa-spin', viewBox: '0 0 16 16', role: 'img', 'aria-label': 'Running' },
305
+ h('circle', { className: 'dsa-spin-track', cx: 8, cy: 8, r: 6.5, fill: 'none', strokeWidth: 2 }),
306
+ h('circle', { className: 'dsa-spin-arc', cx: 8, cy: 8, r: 6.5, fill: 'none', strokeWidth: 2, strokeLinecap: 'round', strokeDasharray: '70 30', pathLength: 100 }))
307
+ : s.kind === 'waiting' ? h('span', { className: 'dsa-dot', 'data-kind': 'waiting', role: 'img', 'aria-label': 'Waiting for reply' })
308
+ : s.kind === 'pending' ? h('span', { className: 'dsa-dot', 'data-kind': 'pending', role: 'img', 'aria-label': 'Unread' }) : null
309
+ return h('div', { className: 'dsa-item', 'data-session-id': s.id, 'data-current': current, 'data-menu-open': menuOpen },
310
+ h('button', { type: 'button', className: 'dsa-row', 'aria-current': current ? 'true' : undefined,
311
+ title: `${title} · ${labels[s.kind]}\n${s.cwd || s.workspace}`, onClick: () => ctx.uiWorkspace.openSession(s.id) },
312
+ h('span', { className: 'dsa-line1' },
313
+ h('span', { className: 'dsa-title-text' }, title),
314
+ h('span', { className: 'dsa-trail' },
315
+ showPin && s.pinned && h('span', { className: 'dsa-pin-mark', role: 'img', 'aria-label': 'Favorited' }, '★'),
316
+ h('time', { dateTime: s.time ? new Date(s.time).toISOString() : undefined }, ago(s.time, now)),
317
+ mark)),
318
+ h('span', { className: 'dsa-meta' }, s.workspace)),
319
+ h('span', { className: 'dsa-actions' },
320
+ h('button', { type: 'button', className: 'dsa-menu-trigger dsa-archive-btn', disabled: busy,
321
+ 'aria-label': `Archive session: ${title}`, title: 'Archive session', onClick: () => archive() },
322
+ h(IconArchiveOutlineRegular, { size: 14 })),
323
+ h(Menu, { open: menuOpen, onClose: () => setMenuOpen(false), portal: true,
324
+ anchor: h('button', { type: 'button', disabled: busy, className: 'dsa-menu-trigger', 'aria-label': `Session actions: ${title}`, 'aria-haspopup': 'menu', 'aria-expanded': menuOpen,
325
+ onClick: () => setMenuOpen(v => !v) }, h(IconEllipsisOutlineRegular, { size: 16 })) },
326
+ item(s.pinned ? 'Remove from favorites (unpin)' : 'Add to favorites (pin)', () => run(() => s.pinned ? ctx.uiWorkspace.unpinSession(s.id) : ctx.uiWorkspace.pinSession(s.id))),
327
+ item('Rename', () => { setMenuOpen(false); setDraft(title); setDialog('rename') }),
328
+ item('Fork session', () => run(() => ctx.uiWorkspace.forkSession(s.id, undefined, { allowMigration: false }))),
329
+ item('Archive session', () => archive()),
330
+ item('Copy session ID', () => run(async () => { if (!await writeClipboard(s.id)) throw new Error('Copy failed. Check clipboard permissions.') })),
331
+ item('Open in workspace for all actions', () => { setMenuOpen(false); mode.set('workspace'); ctx.uiWorkspace.openSession(s.id) }))),
332
+ dialog && h(Modal, { open: true, onClose: close, closeLabel: 'Close', title: dialog === 'rename' ? 'Rename session' : dialog === 'archive' ? 'Stop and archive session?' : 'Action failed',
333
+ footer: h('div', { style: { display: 'flex', gap: 8 } },
334
+ h(Button, { variant: 'outline', disabled: busy, onClick: close }, dialog === 'error' ? 'Close' : 'Cancel'),
335
+ dialog === 'rename' && h(Button, { variant: 'primary', disabled: busy || !draft.trim(), onClick: rename }, 'Save'),
336
+ dialog === 'archive' && h(Button, { variant: 'primary', disabled: busy, onClick: () => archive(true) }, 'Stop and archive')) },
337
+ dialog === 'rename' && h('input', { className: 'dsa-rename', value: draft, disabled: busy, 'aria-label': 'Session name', 'data-modal-autofocus': true,
338
+ onChange: e => setDraft(e.target.value), onKeyDown: e => { if (e.key === 'Enter' && !e.nativeEvent.isComposing && draft.trim()) rename() } }),
339
+ dialog === 'archive' && h('p', null, `“${title}” still has running tasks. Continuing will stop those tasks and archive the session.`),
340
+ error && h('p', { role: 'alert' }, error)))
341
+ }
342
+
343
+ ctx.slots.inject('sidebar.footer.action', () => {
344
+ let unregister, current
345
+ const sync = () => {
346
+ const name = ctx.slots.spec('sidebar.workspaces.before') ? 'sidebar.workspaces.before' : 'sidebar.footer.action'
347
+ if (name === current) return
348
+ unregister?.()
349
+ current = name
350
+ unregister = ctx.slots.register({ name, id: '@yiliang114/dsh-session-deck.switch', order: 0, label: 'Session view' },
351
+ name === 'sidebar.workspaces.before' ? function NavigationSwitch(props) {
352
+ return h('div', { style: { flexShrink: 0, padding: props.wide ? '8px 12px' : '8px 4px' } }, h(Switch, props))
353
+ } : FooterNavigation)
354
+ }
355
+ sync()
356
+ const unsubscribe = ctx.slots.subscribe('sidebar.workspaces.before', sync)
357
+ return () => { unsubscribe(); unregister?.() }
358
+ })
359
+ ctx.slots.inject('sidebar.workspaces', () => {
360
+ let unregister
361
+ const sync = () => {
362
+ if (mode.getSnapshot() !== 'workspace' && !unregister) unregister = ctx.slots.register({ name: 'sidebar.workspaces', priority: -100 }, Activity)
363
+ if (mode.getSnapshot() === 'workspace' && unregister) { unregister(); unregister = undefined }
364
+ }
365
+ sync()
366
+ const unsubscribe = mode.subscribe(sync)
367
+ return () => { unsubscribe(); unregister?.() }
368
+ })
369
+ }
370
+ return { inject: ['slots', 'uiWorkspace', 'sessions'], apply, __test__: { mountNavigationHost, selectActivity, createModeStore, createPendingStore, ago, KEY, PENDING_KEY, MAX_RECORDS } }
371
+ },
372
+ })
package/package.json CHANGED
@@ -1,6 +1,50 @@
1
1
  {
2
2
  "name": "@yiliang114/dsh-session-deck",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.2",
4
+ "license": "MIT",
5
+ "author": "yiliang114",
6
+ "publishConfig": {
7
+ "access": "public",
8
+ "registry": "https://registry.npmjs.org/"
9
+ },
10
+ "keywords": ["dsh-plugin", "deepseek-harness", "sidebar", "sessions", "session-management", "favorites", "ai-agents"],
11
+ "type": "module",
12
+ "description": "Activity and Favorites views for DeepSeek Harness: track running, waiting, and unread sessions across workspaces.",
13
+ "repository": {
14
+ "type": "git",
15
+ "url": "git+https://github.com/yiliang114/dsh-session-deck.git"
16
+ },
17
+ "homepage": "https://github.com/yiliang114/dsh-session-deck#readme",
18
+ "bugs": {
19
+ "url": "https://github.com/yiliang114/dsh-session-deck/issues"
20
+ },
21
+ "engines": {
22
+ "node": ">=22",
23
+ "dsh": "0.2.1-rc.2"
24
+ },
25
+ "exports": {
26
+ ".": "./src/host.mjs",
27
+ "./client": "./lib/client.js",
28
+ "./cordis.patch.yml": "./cordis.patch.yml",
29
+ "./package.json": "./package.json"
30
+ },
31
+ "files": ["src", "lib", "scripts/patch-sidebar-before.mjs", "cordis.patch.yml", "README.md"],
32
+ "scripts": {
33
+ "build": "node --check src/host.mjs && node --check lib/client.js && node --check scripts/patch-sidebar-before.mjs && node --check scripts/check-package.mjs && node --check scripts/update-market-index.mjs && node --check scripts/verify-published.mjs",
34
+ "market:update": "node scripts/update-market-index.mjs",
35
+ "test": "node --test test/*.test.mjs",
36
+ "check:package": "node scripts/check-package.mjs",
37
+ "verify": "npm run build && npm test && npm run check:package"
38
+ },
39
+ "dsh": {
40
+ "bundle": {"patch": "./cordis.patch.yml"},
41
+ "client": {
42
+ "platform": "web",
43
+ "inject": [
44
+ "@deepseek-ai/dsh-client-ui-sidebar",
45
+ "@deepseek-ai/dsh-client-ui-session",
46
+ "@deepseek-ai/dsh-client-ui-workspace"
47
+ ]
48
+ }
49
+ }
50
+ }
@@ -0,0 +1,47 @@
1
+ #!/usr/bin/env node
2
+ // Local, reversible compatibility extension for the published sidebar bundle.
3
+ import { readFileSync, writeFileSync, existsSync, unlinkSync } from 'node:fs'
4
+ import { resolve, join } from 'node:path'
5
+
6
+ const args = process.argv.slice(2)
7
+ const rollback = args.includes('--rollback')
8
+ const target = args.find(arg => !arg.startsWith('--'))
9
+ if (!target) throw new Error('Usage: node scripts/patch-sidebar-before.mjs <sidebar-package-directory> [--rollback]')
10
+ const root = resolve(target)
11
+ const version = JSON.parse(readFileSync(join(root, 'package.json'), 'utf8')).version
12
+ if (version !== '0.2.1-rc.2') throw new Error(`Unsupported sidebar version ${version}; review the extension against the new public contract first`)
13
+ const changes = [
14
+ ['lib/client.js', [
15
+ ['\t\t\t\t\t(0, react_jsx_runtime.jsx)("div", {\n\t\t\t\t\t\tclassName: SidebarRoot_module_css_default.regionArea,', '\t\t\t\t\trenderSlot("sidebar.workspaces.before", { wide }),\n\t\t\t\t\t(0, react_jsx_runtime.jsx)("div", {\n\t\t\t\t\t\tclassName: SidebarRoot_module_css_default.regionArea,'],
16
+ ['\t\t\t\t\t"sidebar.workspaces": {', '\t\t\t\t\t"sidebar.workspaces.before": {\n\t\t\t\t\t\tkind: "list",\n\t\t\t\t\t\tscope: "root"\n\t\t\t\t\t},\n\t\t\t\t\t"sidebar.workspaces": {'],
17
+ ]],
18
+ ['lib/types/client/contract/slots.d.ts', [
19
+ [" 'sidebar.workspaces': {", " /** Additive controls after all global panel rows, before the browsing region. */\n 'sidebar.workspaces.before': {\n kind: 'list';\n scope: 'root';\n owner: SidebarFooterActionOwnerProps;\n };\n 'sidebar.workspaces': {"],
20
+ ["'sidebar.panellist' | 'sidebar.workspaces'", "'sidebar.panellist' | 'sidebar.workspaces.before' | 'sidebar.workspaces'"],
21
+ ]],
22
+ ]
23
+ const plans = changes.map(([relative, replacements]) => {
24
+ const path = join(root, relative), backup = `${path}.dsa-before.backup`
25
+ const current = readFileSync(path, 'utf8')
26
+ const original = existsSync(backup) ? readFileSync(backup, 'utf8') : current
27
+ let patched = original
28
+ for (const [before, after] of replacements) {
29
+ if (patched.split(before).length !== 2 || patched.includes(after)) throw new Error(`Unknown or already modified anchor: ${relative}`)
30
+ patched = patched.replace(before, after)
31
+ }
32
+ if (current !== original && current !== patched) throw new Error(`Concurrent modification: ${relative}; refusing to overwrite`)
33
+ if (rollback && !existsSync(backup)) throw new Error(`No backup: ${relative}`)
34
+ return { path, backup, original, patched }
35
+ })
36
+ // Validate every target before changing either file. Backups preserve exact original bytes.
37
+ for (const { path, backup, original, patched } of plans) {
38
+ if (rollback) {
39
+ writeFileSync(path, original)
40
+ // backup is the checked absolute sibling of the target above.
41
+ unlinkSync(backup)
42
+ } else {
43
+ if (!existsSync(backup)) writeFileSync(backup, original, { flag: 'wx' })
44
+ writeFileSync(path, patched)
45
+ }
46
+ console.log(`${rollback ? 'Restored' : 'Patched'} ${path}`)
47
+ }
package/src/host.mjs ADDED
@@ -0,0 +1,3 @@
1
+ // Client-only feature; host entry keeps the plugin bundle discoverable.
2
+ export const name = '@yiliang114/dsh-session-deck'
3
+ export function apply() {}