@deepseek-ai/dsh-client-ui-sidebar-files 0.1.5-alpha.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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 DeepSeek
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,6 @@
1
+ # Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
2
+ # side as of the last confirmed-consistent state. Both languages carry equal authority;
3
+ # after editing either side, bring the other along and re-record with:
4
+ # pnpm run verify-translation-pairing --write packages/client/ui-sidebar-files/README.md
5
+ README.md: 6e9e305b15058797156f832a56a29c095b0641f3
6
+ README.zh.md: 95a15444182a988fdf512735be4e0fbfcaad6bdd
package/README.md ADDED
@@ -0,0 +1,72 @@
1
+ ---
2
+ description: "The right Sidebar's file-tree tab type for the dsh web client: the session workspace root listed one level at a time over the wire, opening files into the Sidebar by resource address."
3
+ kind: "package-reference"
4
+ ---
5
+
6
+ # @deepseek-ai/dsh-client-ui-sidebar-files
7
+
8
+ English | [中文](README.zh.md)
9
+
10
+ ## Summary
11
+
12
+ The right Sidebar's navigator tab type: the session's workspace root as a tree, listed one level at a time over the wire, opening files into the Sidebar. It is a page type reached from the guide and claims no address; it opens files by address for the `dsh-resource://file` viewers to claim — nothing in `ui-sidebar-right` knows this package.
13
+
14
+ ## Table of Contents
15
+
16
+ - [What it registers](#what-it-registers)
17
+ - [The tree](#the-tree)
18
+ - [Model Experience](#model-experience)
19
+ - [Known Limitations and Deferred Work](#known-limitations-and-deferred-work)
20
+ - [Dev Note](#dev-note)
21
+
22
+ -----
23
+
24
+ <a id="what-it-registers"></a>
25
+ ## What it registers
26
+
27
+ - **The type** — `ctx.sidebarRightTabs.register(...)` with kind `files`, id `@deepseek-ai/dsh-client-ui-sidebar-files`, band `builtin`, no patterns, and one guide entry (order 10, titled from the `sidebarFiles` namespace) that opens the type.
28
+ - **The body** — the keyed `sidebar.right.pane.tab` seat under that id: the tree, with its one control, reload, at the right of its header row.
29
+
30
+ Six source files under `src/client/`: `definition.ts` (the type), `store.ts` (what it keeps), `face.ts` (how it lists, Remote binding included), `FilesBody.tsx` (what it draws, with its ordering and failure-line helpers), `locales.ts` (what it says), and `index.ts` (the wiring).
31
+
32
+ <a id="the-tree"></a>
33
+ ## The tree
34
+
35
+ The root is the session's working directory, read from `useSessions().byId[sessionId].cwd`, and labelled by `workspaceTitleOf` from `@deepseek-ai/dsh-util-workspace-path`. Every level is keyed by absolute path; a child's path is its parent's joined with the entry name by `/`. A level is listed when it is first expanded, through `remote.workspaceFiles.list(sessionId, absolutePath)` on the `@deepseek-ai/dsh-api-workspace-files` namespace; the adapter keeps the listing's entries and truncation flag and drops its workspace-relative path. Rows are ordered directories first, then by natural, case-insensitive name; dotfiles are shown like any other entry.
36
+
37
+ | Entry type | Row |
38
+ |---|---|
39
+ | `directory` | Toggles; the level is fetched the first time it opens and kept while collapsed. |
40
+ | `file` | Opens `dsh-resource://file/session/<sessionId>/<encoded path relative to the root>`, built by `fileAddressFor` from `@deepseek-ai/dsh-util-workspace-path` from the entry's absolute path and the tree's root, through `useTabInfo().tab.actions.openResource`, landing in the tab's own pane. |
41
+ | `other` | Shown greyed and not clickable, so the directory is reported whole. |
42
+
43
+ A level cut by the endpoint's entry cap ends with a marker; an empty level says so; a level that failed shows one line per code — `workspace-file/not-found`, `outside-workspace`, `not-directory` — and the transport's own message otherwise. Reload drops every listed level and asks again for the expanded ones; collapsed levels are fetched again when they next open. A session without a working directory shows a single line instead of a tree.
44
+
45
+ State lives in the type's own store, bucketed by tab id: `root`, `levels` (loading / ready / failed per absolute path), and `expanded`. The owner's `signal` ends a bucket: on abort the tab is forgotten and a listing that settles afterwards writes nothing.
46
+
47
+ <a id="model-experience"></a>
48
+ ## Model Experience
49
+
50
+ None, as this package draws a workspace file tree in the browser and registers nothing model-facing.
51
+
52
+ #### KV Cache effect
53
+
54
+ None; directory listings travel over the Remote and assemble no model request.
55
+
56
+ ## Known Limitations and Deferred Work
57
+
58
+ <a id="known-limitations-and-deferred-work"></a>
59
+ - **Listing only.** No search, artifact filter, drag-and-drop, rename, context menu, current-file highlight, or filesystem watching; a level changes only through reload.
60
+ - **One root.** The tree is rooted at the session's working directory; there is no way to browse above it, and the Host refuses paths outside the workspace root anyway.
61
+
62
+ <a id="dev-note"></a>
63
+ ### Dev Note
64
+
65
+ <details>
66
+ <summary>Working context for maintainers — click to expand</summary>
67
+
68
+ None.
69
+
70
+ </details>
71
+
72
+ **Runtime invariant:** No companion is published. The tree's only runtime state is one Slot store per tab, written by the body that owns it and forgotten on the tab's abort signal; there is no second observation of it to compare against.
package/README.zh.md ADDED
@@ -0,0 +1,72 @@
1
+ ---
2
+ description: "dsh Web 客户端右侧 Sidebar 的文件树 tab 类型:逐层经线上列出会话工作区根目录,按资源地址把文件打开到 Sidebar。"
3
+ kind: "package-reference"
4
+ ---
5
+
6
+ # @deepseek-ai/dsh-client-ui-sidebar-files
7
+
8
+ [English](README.md) | 中文
9
+
10
+ ## 概述
11
+
12
+ 右侧 Sidebar 的导航器 tab 类型:把会话的工作区根目录画成一棵树,逐层经线上列出,并把文件打开到 Sidebar 里。它是从引导页进入的页类型,不认领任何地址;它按地址打开文件,交给 `dsh-resource://file` 的查看器认领:`ui-sidebar-right` 里没有任何东西认识本包。
13
+
14
+ ## 目录
15
+
16
+ - [注册了什么](#what-it-registers)
17
+ - [树](#the-tree)
18
+ - [模型体验](#model-experience)
19
+ - [已知限制与暂缓事项](#known-limitations-and-deferred-work)
20
+ - [开发备注](#dev-note)
21
+
22
+ -----
23
+
24
+ <a id="what-it-registers"></a>
25
+ ## 注册了什么
26
+
27
+ - **类型**:`ctx.sidebarRightTabs.register(...)`,kind 为 `files`,id 为 `@deepseek-ai/dsh-client-ui-sidebar-files`,档位 `builtin`,没有 patterns,另有一个打开该类型的引导页入口(order 10,标题取自 `sidebarFiles` 命名空间)。
28
+ - **正文**:以该 id 为键的 `sidebar.right.pane.tab` 坑位:树本身,以及它唯一的控件、位于标题行右端的重新读取。
29
+
30
+ `src/client/` 下六个源文件:`definition.ts`(类型是什么)、`store.ts`(它保存什么)、`face.ts`(它如何列目录,含 Remote 绑定)、`FilesBody.tsx`(它画什么,含排序与失败行两个辅助函数)、`locales.ts`(它说什么)、`index.ts`(接线)。
31
+
32
+ <a id="the-tree"></a>
33
+ ## 树
34
+
35
+ 根是会话的工作目录,读自 `useSessions().byId[sessionId].cwd`,标签由 `@deepseek-ai/dsh-util-workspace-path` 的 `workspaceTitleOf` 给出。每一层以绝对路径为键;子路径是父路径以 `/` 拼上条目名。一层在首次展开时经 `@deepseek-ai/dsh-api-workspace-files` 命名空间的 `remote.workspaceFiles.list(sessionId, absolutePath)` 列出;适配层保留列表的条目与截断标志,丢弃其工作区相对路径。行序为目录优先,其后按自然序、不分大小写的名称排列;dotfiles 与其他条目一样显示。
36
+
37
+ | 条目类型 | 行 |
38
+ |---|---|
39
+ | `directory` | 切换展开与折叠;该层在首次打开时拉取,折叠期间保留。 |
40
+ | `file` | 经 `useTabInfo().tab.actions.openResource` 打开 `dsh-resource://file/session/<sessionId>/<encoded path relative to the root>`,地址由 `@deepseek-ai/dsh-util-workspace-path` 的 `fileAddressFor` 从条目的绝对路径与树的根生成,落在该 tab 自己的 pane 里。 |
41
+ | `other` | 灰显且不可点击,使目录被完整报告。 |
42
+
43
+ 被端点条目上限截断的层以一条标记收尾;空层如实说明;失败的层按错误码各显示一行(`workspace-file/not-found`、`outside-workspace`、`not-directory`),其他情况显示传输层自己的消息。重新读取丢弃所有已列出的层并只对展开中的层重新请求;折叠的层在下次打开时重新拉取。没有工作目录的会话只显示一行说明,而不是树。
44
+
45
+ 状态住在类型自己的存储里,按 tab id 分桶:`root`、`levels`(每个绝对路径的 loading / ready / failed)与 `expanded`。owner 的 `signal` 终结一个桶:中止时忘掉该 tab,其后才结算的列表什么也不写。
46
+
47
+ <a id="model-experience"></a>
48
+ ## 模型体验
49
+
50
+ 无,因为本包在浏览器里绘制工作区文件树,不注册任何面向模型的内容。
51
+
52
+ #### KV Cache 影响
53
+
54
+ 无;目录列表经 Remote 传输,不会组装模型请求。
55
+
56
+ ## 已知限制与暂缓事项
57
+
58
+ <a id="known-limitations-and-deferred-work"></a>
59
+ - **只有列目录。**没有搜索、产物过滤、拖拽、重命名、右键菜单、当前文件高亮或文件系统监听;一层只会因重新读取而变化。
60
+ - **只有一个根。**树以会话工作目录为根;没有办法浏览到它之上,而 Host 本来也拒绝工作区根之外的路径。
61
+
62
+ <a id="dev-note"></a>
63
+ ### 开发备注
64
+
65
+ <details>
66
+ <summary>维护者工作上下文——点击展开</summary>
67
+
68
+ 无。
69
+
70
+ </details>
71
+
72
+ **运行时不变量:** 不发布 companion。树唯一的运行时状态是每 tab 一份的 Slot store,由持有它的正文写入、随 tab 的中止信号忘掉;没有第二个观测源可与之比对。
package/lib/client.js ADDED
@@ -0,0 +1,640 @@
1
+ window.__ModuleLoader__.load({
2
+ id: "@deepseek-ai/dsh-client-ui-sidebar-files",
3
+ factory: (require) => {
4
+ var module = { exports: {} };
5
+ var exports = module.exports;
6
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
7
+ let _deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
8
+ let react_jsx_runtime = require("react/jsx-runtime");
9
+ let react = require("react");
10
+ let _deepseek_ai_dsh_client_store = require("@deepseek-ai/dsh-client-store");
11
+ //#region lib/types/client/definition.js
12
+ /** The tab kind this package owns. */
13
+ const FILES_KIND = "files";
14
+ /** This implementation's identity in the tab system, and the key its body registers under. */
15
+ const FILES_ID = "@deepseek-ai/dsh-client-ui-sidebar-files";
16
+ /**
17
+ * The files type's registry definition.
18
+ * @param t - namespace-bound translate, read fresh on every label call.
19
+ * @returns the definition to register.
20
+ */
21
+ function filesDefinition(t) {
22
+ return {
23
+ id: FILES_ID,
24
+ kind: FILES_KIND,
25
+ priority: "builtin",
26
+ title: () => t("type.label"),
27
+ guide: [{
28
+ order: 10,
29
+ title: () => t("guide.title"),
30
+ description: () => t("guide.description"),
31
+ icon: _deepseek_ai_dsh_client_ui_primitives.IconFolderClose16
32
+ }]
33
+ };
34
+ }
35
+ //#endregion
36
+ //#region lib/types/client/face.js
37
+ /**
38
+ * Bind the listing to one Remote face, keeping only what the tree stores.
39
+ * @param remote - the Client Remote face carrying the `workspaceFiles` namespace.
40
+ * @returns the listing the tree's face performs.
41
+ */
42
+ function createList(remote) {
43
+ return async (sessionId, path, signal) => {
44
+ const result = await remote.workspaceFiles.list(sessionId, path, signal);
45
+ if (!result.ok) return result;
46
+ return {
47
+ ok: true,
48
+ value: {
49
+ entries: result.value.entries,
50
+ truncated: result.value.truncated
51
+ }
52
+ };
53
+ };
54
+ }
55
+ /**
56
+ * The absolute path of one child entry.
57
+ *
58
+ * Joined with `/` whatever the parent's separators: the Host resolves mixed
59
+ * separators, and the tree only needs a stable key.
60
+ * @param parent - absolute path of the listed directory.
61
+ * @param name - the entry's basename.
62
+ * @returns the child's absolute path.
63
+ */
64
+ function childPath(parent, name) {
65
+ return `${parent.replace(/[/\\]+$/, "")}/${name}`;
66
+ }
67
+ /**
68
+ * Bind the tree's face to one directory listing.
69
+ * @param list - the bound `workspaceFiles.list` call.
70
+ * @returns the Slot `inject` factory: session and bound actions in, face out.
71
+ */
72
+ function filesFace(list) {
73
+ return (sessionId, actions) => {
74
+ /** Per tab, per absolute path: the listing generation a settlement must match; the latest request wins. */
75
+ const generations = /* @__PURE__ */ new Map();
76
+ const nextGeneration = (tabId, path) => {
77
+ const byPath = generations.get(tabId) ?? /* @__PURE__ */ new Map();
78
+ generations.set(tabId, byPath);
79
+ const generation = (byPath.get(path) ?? 0) + 1;
80
+ byPath.set(path, generation);
81
+ return generation;
82
+ };
83
+ const load = (tabId, path, signal) => {
84
+ if (signal.aborted) return;
85
+ const generation = nextGeneration(tabId, path);
86
+ actions.loading(tabId, path);
87
+ list(sessionId, path, signal).then((result) => {
88
+ if (generations.get(tabId)?.get(path) !== generation) return;
89
+ if (result.ok) actions.loaded(tabId, path, result.value);
90
+ else actions.failed(tabId, path, result.error);
91
+ });
92
+ };
93
+ return {
94
+ start(tabId, root, signal) {
95
+ actions.start(tabId, root);
96
+ signal.addEventListener("abort", () => {
97
+ generations.delete(tabId);
98
+ actions.forget(tabId);
99
+ }, { once: true });
100
+ load(tabId, root, signal);
101
+ },
102
+ load,
103
+ toggle(tabId, path, loaded, signal) {
104
+ actions.toggled(tabId, path);
105
+ if (!loaded) load(tabId, path, signal);
106
+ }
107
+ };
108
+ };
109
+ }
110
+ //#endregion
111
+ //#region ../../../node_modules/.pnpm/clsx@2.1.1/node_modules/clsx/dist/clsx.mjs
112
+ function r(e) {
113
+ var t, f, n = "";
114
+ if ("string" == typeof e || "number" == typeof e) n += e;
115
+ else if ("object" == typeof e) if (Array.isArray(e)) {
116
+ var o = e.length;
117
+ for (t = 0; t < o; t++) e[t] && (f = r(e[t])) && (n && (n += " "), n += f);
118
+ } else for (f in e) e[f] && (n && (n += " "), n += f);
119
+ return n;
120
+ }
121
+ function clsx() {
122
+ for (var e, t, f = 0, n = "", o = arguments.length; f < o; f++) (e = arguments[f]) && (t = r(e)) && (n && (n += " "), n += t);
123
+ return n;
124
+ }
125
+ //#endregion
126
+ //#region ../../util/workspace-path/src/file-address.ts
127
+ /** The scheme and type every file address opens with. */
128
+ const FILE_ADDRESS_PREFIX = "dsh-resource://file/";
129
+ /** Component-encode one id or path segment, keeping `:` literal for drive letters. */
130
+ function encodeSegment(segment) {
131
+ return encodeURIComponent(segment).replace(/%3A/gi, ":");
132
+ }
133
+ /** Encode a `/`-separated path segment by segment. */
134
+ function encodePath(path) {
135
+ return path.split("/").map(encodeSegment).join("/");
136
+ }
137
+ /**
138
+ * Build the address of a file inside one Session's workspace.
139
+ * @param sessionId - the Session whose workspace root the path is relative to.
140
+ * @param path - workspace-relative path; backslashes are normalized to `/`, and a leading `./` or `/` is dropped.
141
+ * @returns the `dsh-resource://file/session/<sessionId>/<path>` address.
142
+ */
143
+ function sessionFileAddress(sessionId, path) {
144
+ const relative = path.replace(/\\/g, "/").replace(/^(?:\.\/)+/, "").replace(/^\/+/, "");
145
+ return `${FILE_ADDRESS_PREFIX}session/${encodeSegment(sessionId)}/${encodePath(relative)}`;
146
+ }
147
+ /**
148
+ * Build the address of a file by its absolute path.
149
+ * @param path - absolute path; backslashes are normalized to `/` and the leading `/` is dropped,
150
+ * except that a UNC path (`\\server\share`) keeps one empty first segment.
151
+ * @returns the `dsh-resource://file/absolute/<path>` address.
152
+ */
153
+ function absoluteFileAddress(path) {
154
+ const normalized = path.replace(/\\/g, "/");
155
+ const unc = normalized.startsWith("//");
156
+ const absolute = normalized.replace(/^\/+/, "");
157
+ return `${FILE_ADDRESS_PREFIX}absolute/${unc ? "/" : ""}${encodePath(absolute)}`;
158
+ }
159
+ //#endregion
160
+ //#region ../../util/workspace-path/src/index.ts
161
+ /**
162
+ * Browser-safe Workspace path and display helpers.
163
+ * @module @deepseek-ai/dsh-util-workspace-path
164
+ */
165
+ /** Whether a path uses a Windows drive or UNC prefix. */
166
+ function isWindowsStylePath(value) {
167
+ return /^[A-Za-z]:[/\\]/.test(value) || value.startsWith("\\\\");
168
+ }
169
+ /**
170
+ * Whether a path is absolute in either spelling the Host accepts: POSIX (`/a/b`) or Windows drive or UNC.
171
+ * @param path - the path to classify.
172
+ * @returns `true` for an absolute path; `false` for a Workspace-relative one.
173
+ */
174
+ function isAbsoluteWorkspacePath(path) {
175
+ return path.startsWith("/") || isWindowsStylePath(path);
176
+ }
177
+ /**
178
+ * Read the final non-empty segment of a Workspace path for display.
179
+ * Workspace-label surfaces use this helper instead of deriving another basename.
180
+ * @param path - Workspace directory path using POSIX or Windows separators.
181
+ * @returns the final segment, or an empty string for a separator-only path.
182
+ */
183
+ function workspaceTitleOf(path) {
184
+ const trimmed = path.replace(/[/\\]+$/, "");
185
+ const separator = Math.max(trimmed.lastIndexOf("/"), trimmed.lastIndexOf("\\"));
186
+ return trimmed.slice(separator + 1);
187
+ }
188
+ /**
189
+ * The address for a path as a caller holds it: a relative path, or an absolute
190
+ * path inside the Session's workspace, becomes a `session`-scoped address; an
191
+ * absolute path outside it, or one whose workspace root is unknown, becomes an
192
+ * `absolute`-scoped address.
193
+ * @param sessionId - the Session the path is read in.
194
+ * @param cwd - that Session's workspace root, when known.
195
+ * @param path - absolute or workspace-relative path, in either separator spelling.
196
+ * @returns the `dsh-resource://file/…` address.
197
+ */
198
+ function fileAddressFor(sessionId, cwd, path) {
199
+ const normalized = path.replace(/\\/g, "/");
200
+ if (!isAbsoluteWorkspacePath(normalized)) return sessionFileAddress(sessionId, normalized);
201
+ const root = cwd === void 0 ? "" : cwd.replace(/\\/g, "/").replace(/\/+$/, "");
202
+ if (root !== "" && normalized === root) return sessionFileAddress(sessionId, "");
203
+ if (root !== "" && normalized.startsWith(`${root}/`)) return sessionFileAddress(sessionId, normalized.slice(root.length + 1));
204
+ return absoluteFileAddress(normalized);
205
+ }
206
+ //#endregion
207
+ //#region \0dsh-css:/home/runner/work/deepseek-harness/deepseek-harness/packages/client/ui-sidebar-files/src/client/FilesBody.module.css.mjs
208
+ const css = ".k-1LKG_root{min-height:0;color:var(--dsw-alias-label-primary);font-size:var(--dsh-content-font-size-secondary,13px);flex-direction:column;flex:auto;padding:4px 0 8px;line-height:1.5;display:flex;overflow:auto}.k-1LKG_header{color:var(--dsw-alias-label-secondary);flex:none;align-items:center;gap:6px;padding:4px 10px;font-weight:500;display:flex}.k-1LKG_level{margin:0;padding:0;list-style:none}.k-1LKG_level .k-1LKG_level{padding-left:14px}.k-1LKG_item{margin:0;padding:0}.k-1LKG_row{width:100%;min-width:0;color:inherit;font:inherit;text-align:left;cursor:pointer;background:0 0;border:0;border-radius:6px;align-items:center;gap:6px;padding:3px 10px;display:flex}.k-1LKG_row:hover{background:var(--dsw-alias-interactive-bg-hover)}.k-1LKG_icon{color:var(--dsw-alias-label-secondary);flex:none}.k-1LKG_fileIcon{flex:none;width:14px;height:16px}.k-1LKG_name{white-space:nowrap;text-overflow:ellipsis;min-width:0;overflow:hidden}.k-1LKG_other{color:var(--dsw-alias-label-tertiary);cursor:default}.k-1LKG_other:hover{background:0 0}.k-1LKG_note{color:var(--dsw-alias-label-tertiary);margin:0;padding:3px 10px;font-size:12px}.k-1LKG_status{flex-direction:column;padding:12px 10px;display:flex}.k-1LKG_statusLine{color:var(--dsw-alias-label-secondary);font-size:var(--dsh-content-font-size-secondary,13px);margin:0;line-height:1.6}.k-1LKG_tool{width:24px;height:24px;color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border:0;border-radius:6px;flex:none;justify-content:center;align-items:center;margin-left:auto;padding:0;display:inline-flex}.k-1LKG_tool:hover{background:var(--dsw-alias-interactive-bg-hover)}";
209
+ const tagId = "@deepseek-ai/dsh-client-ui-sidebar-files/FilesBody.module.css";
210
+ if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
211
+ const tag = document.createElement("style");
212
+ tag.dataset.plugin = "@deepseek-ai/dsh-client-ui-sidebar-files";
213
+ tag.dataset.pluginCss = tagId;
214
+ tag.textContent = css;
215
+ document.head.appendChild(tag);
216
+ }
217
+ var FilesBody_module_css_default = {
218
+ "fileIcon": "k-1LKG_fileIcon",
219
+ "header": "k-1LKG_header",
220
+ "icon": "k-1LKG_icon",
221
+ "item": "k-1LKG_item",
222
+ "level": "k-1LKG_level",
223
+ "name": "k-1LKG_name",
224
+ "note": "k-1LKG_note",
225
+ "other": "k-1LKG_other",
226
+ "root": "k-1LKG_root",
227
+ "row": "k-1LKG_row",
228
+ "status": "k-1LKG_status",
229
+ "statusLine": "k-1LKG_statusLine",
230
+ "tool": "k-1LKG_tool"
231
+ };
232
+ //#endregion
233
+ //#region lib/types/client/FilesBody.js
234
+ /**
235
+ * The file tree's body: the session's workspace root, listed one level at a time.
236
+ *
237
+ * Everything the tree keeps lives in its store, keyed by tab; everything it asks
238
+ * for goes through its injected face. The component itself only decides what to
239
+ * draw for each absolute path and what a click means: a directory toggles, a
240
+ * file opens through the owner's `tabActions` for a `file:` viewer to claim, and
241
+ * anything else is shown but refuses to open. The header row carries the one
242
+ * control: reload, which drops every listed level and asks again for the
243
+ * expanded ones.
244
+ */
245
+ /** Natural, case-insensitive name order, so `file2` precedes `file10`. */
246
+ const byName = new Intl.Collator(void 0, {
247
+ numeric: true,
248
+ sensitivity: "base"
249
+ });
250
+ /**
251
+ * Order one level's entries for display: directories first, then everything
252
+ * else, each group by name. The endpoint's order is a listing fact; this is the
253
+ * reader's.
254
+ * @param entries - the listing as the endpoint returned it.
255
+ * @returns a new array, directories first, then by name within each group.
256
+ */
257
+ function orderEntries(entries) {
258
+ return [...entries].sort((left, right) => {
259
+ const group = Number(right.type === "directory") - Number(left.type === "directory");
260
+ return group !== 0 ? group : byName.compare(left.name, right.name);
261
+ });
262
+ }
263
+ /**
264
+ * Say why a directory could not be listed, in terms of the directory.
265
+ * @param t - namespace-bound translate.
266
+ * @param failure - the settled Remote failure.
267
+ * @returns the line to show under the directory.
268
+ */
269
+ function failureLine(t, failure) {
270
+ switch (failure.code) {
271
+ case "workspace-file/not-found": return t("error.notFound");
272
+ case "workspace-file/outside-workspace": return t("error.outsideWorkspace");
273
+ case "workspace-file/not-directory": return t("error.notDirectory");
274
+ default: return t("error.unavailable", { message: failure.message });
275
+ }
276
+ }
277
+ /** One entry's row, and its children when it is an expanded directory. */
278
+ function Entry({ parent, entry, tree }) {
279
+ const path = childPath(parent, entry.name);
280
+ if (entry.type === "directory") {
281
+ const expanded = tree.state.expanded.includes(path);
282
+ return (0, react_jsx_runtime.jsxs)("li", {
283
+ className: FilesBody_module_css_default.item,
284
+ "data-files-entry": "directory",
285
+ "data-files-path": path,
286
+ children: [(0, react_jsx_runtime.jsxs)("button", {
287
+ type: "button",
288
+ className: FilesBody_module_css_default.row,
289
+ "aria-expanded": expanded,
290
+ onClick: () => {
291
+ tree.onToggle(path);
292
+ },
293
+ children: [expanded ? (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconFolderOpen16, { className: FilesBody_module_css_default.icon }) : (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconFolderClose16, { className: FilesBody_module_css_default.icon }), (0, react_jsx_runtime.jsx)("span", {
294
+ className: FilesBody_module_css_default.name,
295
+ children: entry.name
296
+ })]
297
+ }), expanded && (0, react_jsx_runtime.jsx)("ul", {
298
+ className: FilesBody_module_css_default.level,
299
+ children: (0, react_jsx_runtime.jsx)(Level, {
300
+ path,
301
+ tree
302
+ })
303
+ })]
304
+ });
305
+ }
306
+ if (entry.type === "file") return (0, react_jsx_runtime.jsx)("li", {
307
+ className: FilesBody_module_css_default.item,
308
+ "data-files-entry": "file",
309
+ "data-files-path": path,
310
+ children: (0, react_jsx_runtime.jsxs)("button", {
311
+ type: "button",
312
+ className: FilesBody_module_css_default.row,
313
+ onClick: () => {
314
+ tree.onOpen(path);
315
+ },
316
+ children: [(0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.DocumentFileIcon, { className: FilesBody_module_css_default.fileIcon }), (0, react_jsx_runtime.jsx)("span", {
317
+ className: FilesBody_module_css_default.name,
318
+ children: entry.name
319
+ })]
320
+ })
321
+ });
322
+ return (0, react_jsx_runtime.jsx)("li", {
323
+ className: FilesBody_module_css_default.item,
324
+ "data-files-entry": "other",
325
+ "data-files-path": path,
326
+ children: (0, react_jsx_runtime.jsx)("span", {
327
+ className: clsx(FilesBody_module_css_default.row, FilesBody_module_css_default.other),
328
+ "aria-disabled": "true",
329
+ title: tree.t("entry.other"),
330
+ children: (0, react_jsx_runtime.jsx)("span", {
331
+ className: FilesBody_module_css_default.name,
332
+ children: entry.name
333
+ })
334
+ })
335
+ });
336
+ }
337
+ /** One directory's rows: its state while listing, its entries once listed. */
338
+ function Level({ path, tree }) {
339
+ const { state, t } = tree;
340
+ const level = state.levels[path];
341
+ if (level === void 0 || level.kind === "loading") return (0, react_jsx_runtime.jsx)("li", {
342
+ className: FilesBody_module_css_default.note,
343
+ "data-files-row": "loading",
344
+ children: t("loading")
345
+ });
346
+ if (level.kind === "failed") return (0, react_jsx_runtime.jsx)("li", {
347
+ className: FilesBody_module_css_default.note,
348
+ "data-files-row": "failed",
349
+ "data-files-code": level.failure.code,
350
+ children: failureLine(t, level.failure)
351
+ });
352
+ const entries = orderEntries(level.level.entries);
353
+ return (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
354
+ entries.length === 0 && (0, react_jsx_runtime.jsx)("li", {
355
+ className: FilesBody_module_css_default.note,
356
+ "data-files-row": "empty",
357
+ children: t("empty")
358
+ }),
359
+ entries.map((entry) => (0, react_jsx_runtime.jsx)(Entry, {
360
+ parent: path,
361
+ entry,
362
+ tree
363
+ }, entry.name)),
364
+ level.level.truncated && (0, react_jsx_runtime.jsx)("li", {
365
+ className: FilesBody_module_css_default.note,
366
+ "data-files-row": "truncated",
367
+ children: t("truncated")
368
+ })
369
+ ] });
370
+ }
371
+ /** The file tree's body: the workspace root and whatever the reader has opened under it. */
372
+ function FilesBody({ useTabInfo, sessionId, useSessions, useStore, actions, start, load, toggle, t }) {
373
+ const { tab } = useTabInfo();
374
+ const { signal, actions: tabActions } = tab;
375
+ const cwd = useSessions((sessions) => sessions.byId[sessionId]?.cwd);
376
+ const state = useStore((store) => store.byTab[tab.id]);
377
+ (0, react.useEffect)(() => {
378
+ if (state !== void 0 || cwd === void 0 || signal.aborted) return;
379
+ start(tab.id, cwd, signal);
380
+ }, [
381
+ state,
382
+ cwd,
383
+ tab.id,
384
+ signal,
385
+ start
386
+ ]);
387
+ if (cwd === void 0) return (0, react_jsx_runtime.jsx)("div", {
388
+ className: FilesBody_module_css_default.status,
389
+ "data-files-state": "no-workspace",
390
+ children: (0, react_jsx_runtime.jsx)("p", {
391
+ className: FilesBody_module_css_default.statusLine,
392
+ children: t("noWorkspace")
393
+ })
394
+ });
395
+ if (state === void 0) return null;
396
+ const tree = {
397
+ state,
398
+ onToggle: (path) => {
399
+ toggle(tab.id, path, state.levels[path] !== void 0, signal);
400
+ },
401
+ onOpen: (path) => {
402
+ tabActions.openResource(fileAddressFor(sessionId, state.root, path));
403
+ },
404
+ t
405
+ };
406
+ const reload = () => {
407
+ actions.reset(tab.id);
408
+ for (const path of state.expanded) load(tab.id, path, signal);
409
+ };
410
+ const title = workspaceTitleOf(state.root) || state.root;
411
+ return (0, react_jsx_runtime.jsxs)("div", {
412
+ className: FilesBody_module_css_default.root,
413
+ "data-files-state": "tree",
414
+ "data-files-root": state.root,
415
+ children: [(0, react_jsx_runtime.jsxs)("div", {
416
+ className: FilesBody_module_css_default.header,
417
+ children: [
418
+ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconFolderOpen16, { className: FilesBody_module_css_default.icon }),
419
+ (0, react_jsx_runtime.jsx)("span", {
420
+ className: FilesBody_module_css_default.name,
421
+ children: title
422
+ }),
423
+ (0, react_jsx_runtime.jsx)("button", {
424
+ type: "button",
425
+ className: FilesBody_module_css_default.tool,
426
+ "aria-label": t("reload"),
427
+ title: t("reload"),
428
+ "data-files-reload": true,
429
+ onClick: reload,
430
+ children: (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconRefreshOutline16, {})
431
+ })
432
+ ]
433
+ }), (0, react_jsx_runtime.jsx)("ul", {
434
+ className: FilesBody_module_css_default.level,
435
+ children: (0, react_jsx_runtime.jsx)(Level, {
436
+ path: state.root,
437
+ tree
438
+ })
439
+ })]
440
+ });
441
+ }
442
+ //#endregion
443
+ //#region lib/types/client/locales.js
444
+ /** Simplified Chinese dictionary and key-set source of truth. */
445
+ const zh = {
446
+ "type.label": "文件",
447
+ "guide.title": "文件",
448
+ "guide.description": "浏览这个会话工作区里的文件,点开就能查看。",
449
+ loading: "正在读取…",
450
+ empty: "空目录",
451
+ truncated: "条目太多,只显示了一部分。",
452
+ noWorkspace: "这个会话没有工作区目录。",
453
+ reload: "重新读取",
454
+ "entry.other": "这不是文件或目录,没法打开。",
455
+ "error.notFound": "这个目录不在了。可能已被移动或删除。",
456
+ "error.outsideWorkspace": "这个目录在工作区之外,侧栏不会读取它。",
457
+ "error.notDirectory": "这不是一个目录。",
458
+ "error.unavailable": "读取失败:{message}"
459
+ };
460
+ /** English dictionary, checked against the Chinese key set. */
461
+ const en = {
462
+ "type.label": "Files",
463
+ "guide.title": "Files",
464
+ "guide.description": "Browse the files in this session's workspace and open any of them.",
465
+ loading: "Reading…",
466
+ empty: "Empty directory",
467
+ truncated: "Too many entries; showing only some of them.",
468
+ noWorkspace: "This session has no workspace directory.",
469
+ reload: "Reload",
470
+ "entry.other": "Not a file or a directory, so it cannot be opened.",
471
+ "error.notFound": "That directory is gone. It may have been moved or deleted.",
472
+ "error.outsideWorkspace": "That directory is outside the workspace, so the sidebar will not read it.",
473
+ "error.notDirectory": "That is not a directory.",
474
+ "error.unavailable": "Read failed: {message}"
475
+ };
476
+ //#endregion
477
+ //#region lib/types/client/store.js
478
+ /**
479
+ * The file tree's view state: which directories are expanded, and what each
480
+ * loaded level contains.
481
+ *
482
+ * The tree is not one resource. A directory listing per level, expanded lazily,
483
+ * is state the type owns — so it lives in a Slot-standard exclusive store
484
+ * (one instance per session), bucketed by tab id because two tabs of this kind
485
+ * in one session expand independently.
486
+ *
487
+ * Writers run between `start` and `forget`: the owner's `signal` is what ends a
488
+ * bucket's life, and the face stops dispatching once it aborts.
489
+ */
490
+ /**
491
+ * One tab's bucket, which every writer after `start` relies on: the face only
492
+ * dispatches while the record's signal is live, and `forget` runs on its abort.
493
+ * @param state - the draft.
494
+ * @param tabId - the tab being written.
495
+ * @returns the tab's tree.
496
+ */
497
+ function bucket(state, tabId) {
498
+ const tree = state.byTab[tabId];
499
+ if (tree === void 0) throw new Error(`ui-sidebar-files: no tree for tab "${tabId}"`);
500
+ return tree;
501
+ }
502
+ /**
503
+ * Declare the file tree's store.
504
+ *
505
+ * A factory rather than a shared handle: the registration declares it as an
506
+ * exclusive store, so the framework mints one instance per session.
507
+ * @returns the store handle to declare on the registration.
508
+ */
509
+ function createFilesStore() {
510
+ return (0, _deepseek_ai_dsh_client_store.defineStore)({
511
+ init: () => ({ byTab: {} }),
512
+ actions: {
513
+ /**
514
+ * Seed one tab's tree at its workspace root, with the root expanded.
515
+ * @param d - draft state.
516
+ * @param tabId - the tab being drawn.
517
+ * @param root - absolute path of the workspace root.
518
+ */
519
+ start: (d, tabId, root) => {
520
+ d.byTab[tabId] = {
521
+ root,
522
+ levels: {},
523
+ expanded: [root]
524
+ };
525
+ },
526
+ /**
527
+ * Mark one directory as being listed.
528
+ * @param d - draft state.
529
+ * @param tabId - the tab being drawn.
530
+ * @param path - absolute directory path.
531
+ */
532
+ loading: (d, tabId, path) => {
533
+ bucket(d, tabId).levels[path] = { kind: "loading" };
534
+ },
535
+ /**
536
+ * Record one directory's contents.
537
+ * @param d - draft state.
538
+ * @param tabId - the tab being drawn.
539
+ * @param path - absolute directory path.
540
+ * @param level - the listing to show under it.
541
+ */
542
+ loaded: (d, tabId, path, level) => {
543
+ bucket(d, tabId).levels[path] = {
544
+ kind: "ready",
545
+ level
546
+ };
547
+ },
548
+ /**
549
+ * Record why one directory could not be listed.
550
+ * @param d - draft state.
551
+ * @param tabId - the tab being drawn.
552
+ * @param path - absolute directory path.
553
+ * @param failure - the settled Remote failure.
554
+ */
555
+ failed: (d, tabId, path, failure) => {
556
+ bucket(d, tabId).levels[path] = {
557
+ kind: "failed",
558
+ failure
559
+ };
560
+ },
561
+ /**
562
+ * Open a collapsed directory, or collapse an open one.
563
+ *
564
+ * A collapsed level keeps what it loaded, so reopening it draws at once.
565
+ * @param d - draft state.
566
+ * @param tabId - the tab being drawn.
567
+ * @param path - absolute directory path.
568
+ */
569
+ toggled: (d, tabId, path) => {
570
+ const state = bucket(d, tabId);
571
+ const at = state.expanded.indexOf(path);
572
+ if (at >= 0) state.expanded.splice(at, 1);
573
+ else state.expanded.push(path);
574
+ },
575
+ /**
576
+ * Drop every loaded level, keeping what is expanded.
577
+ *
578
+ * This is the reload gesture's first half: the expanded set says which
579
+ * levels to fetch again.
580
+ * @param d - draft state.
581
+ * @param tabId - the tab being drawn.
582
+ */
583
+ reset: (d, tabId) => {
584
+ bucket(d, tabId).levels = {};
585
+ },
586
+ /**
587
+ * Forget one tab's tree, for a tab record that is gone.
588
+ * @param d - draft state.
589
+ * @param tabId - the tab that went away.
590
+ */
591
+ forget: (d, tabId) => {
592
+ d.byTab = Object.fromEntries(Object.entries(d.byTab).filter(([id]) => id !== tabId));
593
+ }
594
+ }
595
+ });
596
+ }
597
+ //#endregion
598
+ //#region lib/types/client/index.js
599
+ /** This package's copy namespace. */
600
+ const NS = "sidebarFiles";
601
+ /**
602
+ * Required browser services: the tab registry, the keyed seat, the Remote
603
+ * carrier and its namespace, and copy.
604
+ */
605
+ const inject = [
606
+ "slots",
607
+ "locale",
608
+ "sidebarRightTabs",
609
+ "remote",
610
+ "remote.workspaceFiles"
611
+ ];
612
+ /**
613
+ * Client plugin body: register the type, its dictionaries, then its body.
614
+ * @param ctx - client root context carrying the registry, the slots, and the Remote face.
615
+ */
616
+ function apply(ctx) {
617
+ const t = ctx.locale.bind(NS);
618
+ ctx.effect(() => ctx.sidebarRightTabs.register(filesDefinition(t)), "ui-sidebar-files: files type");
619
+ ctx.effect(() => ctx.locale.register(NS, {
620
+ zh,
621
+ en
622
+ }), "ui-sidebar-files: dictionaries");
623
+ const store = createFilesStore();
624
+ const inject = filesFace(createList(ctx.remote));
625
+ ctx.effect(() => ctx.slots.inject("sidebar.right.pane.tab", () => ctx.slots.register({
626
+ name: "sidebar.right.pane.tab",
627
+ key: FILES_ID,
628
+ locale: NS,
629
+ store,
630
+ inject
631
+ }, FilesBody)), "ui-sidebar-files: files tab body");
632
+ }
633
+ //#endregion
634
+ exports.apply = apply;
635
+ exports.inject = inject;
636
+ return module.exports;
637
+ }
638
+ });
639
+
640
+ //# sourceMappingURL=client.js.map
package/lib/index.js ADDED
@@ -0,0 +1,6 @@
1
+ //#region lib/types/index.js
2
+ /** Pure host half; the whole tab type lives in the browser export. */
3
+ /** Host plugin body: the file tree contributes nothing to the host tree. */
4
+ function apply() {}
5
+ //#endregion
6
+ export { apply };
@@ -0,0 +1,26 @@
1
+ import type { ReactNode } from 'react';
2
+ import type { RemoteFailure } from '@deepseek-ai/dsh-api-remotes/client';
3
+ import type { PropsLocale, PropsRuntime, PropsStore, TranslateNS } from '@deepseek-ai/dsh-client-ui-slots';
4
+ import type { WorkspaceDirectoryEntry } from '@deepseek-ai/dsh-api-workspace-files/types';
5
+ import type { FilesInjected } from './face.ts';
6
+ import type { createFilesStore } from './store.ts';
7
+ /** The body's composed props: the tab it draws, its store, its face, and its copy. */
8
+ export type FilesBodyProps = PropsRuntime<'sidebar.right.pane.tab'> & PropsStore<ReturnType<typeof createFilesStore>> & FilesInjected & PropsLocale<'sidebarFiles'>;
9
+ /**
10
+ * Order one level's entries for display: directories first, then everything
11
+ * else, each group by name. The endpoint's order is a listing fact; this is the
12
+ * reader's.
13
+ * @param entries - the listing as the endpoint returned it.
14
+ * @returns a new array, directories first, then by name within each group.
15
+ */
16
+ export declare function orderEntries(entries: readonly WorkspaceDirectoryEntry[]): WorkspaceDirectoryEntry[];
17
+ /**
18
+ * Say why a directory could not be listed, in terms of the directory.
19
+ * @param t - namespace-bound translate.
20
+ * @param failure - the settled Remote failure.
21
+ * @returns the line to show under the directory.
22
+ */
23
+ export declare function failureLine(t: TranslateNS<'sidebarFiles'>, failure: RemoteFailure): string;
24
+ /** The file tree's body: the workspace root and whatever the reader has opened under it. */
25
+ export declare function FilesBody({ useTabInfo, sessionId, useSessions, useStore, actions, start, load, toggle, t, }: FilesBodyProps): ReactNode;
26
+ //# sourceMappingURL=FilesBody.d.ts.map
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Stage one of this package's registration: what the `files` tab type IS.
3
+ *
4
+ * The type is a page, not a viewer: it claims no address. The guide page offers
5
+ * it as an entry box, and the tree opens files through `tabActions.openResource`
6
+ * for the `dsh-resource://file` viewers to claim.
7
+ */
8
+ import type { SidebarRightTabDefinition } from '@deepseek-ai/dsh-client-ui-sidebar-right/client';
9
+ import type { TranslateNS } from '@deepseek-ai/dsh-client-locale/client';
10
+ /** The tab kind this package owns. */
11
+ export declare const FILES_KIND = "files";
12
+ /** This implementation's identity in the tab system, and the key its body registers under. */
13
+ export declare const FILES_ID = "@deepseek-ai/dsh-client-ui-sidebar-files";
14
+ /**
15
+ * The files type's registry definition.
16
+ * @param t - namespace-bound translate, read fresh on every label call.
17
+ * @returns the definition to register.
18
+ */
19
+ export declare function filesDefinition(t: TranslateNS<'sidebarFiles'>): SidebarRightTabDefinition;
20
+ //# sourceMappingURL=definition.d.ts.map
@@ -0,0 +1,88 @@
1
+ /**
2
+ * The tree's asynchronous half: listing directories into the store.
3
+ *
4
+ * The component never awaits anything. It calls `start` / `load` / `toggle`, and
5
+ * this face performs the listing and writes the outcome through the store's own
6
+ * actions — the Slot-standard `inject` shape, so the session id is resolved by
7
+ * the framework and the write set stays the store's.
8
+ *
9
+ * The listing itself is bound here to the Client Remote face: the tree keys
10
+ * every level by absolute path and hands the endpoint that same absolute path;
11
+ * the endpoint answers with the directory's workspace-relative path as well,
12
+ * which the tree has no use for and drops.
13
+ *
14
+ * One level has one listing in force: asking for a level again — the reload
15
+ * gesture, a directory reopened after a reset — retires the listing still in
16
+ * flight for it, whose settlement then writes nothing. Cleanup rides the owner's
17
+ * `signal`: a request is not made for a record that already ended, and when the
18
+ * record goes away the bucket and the tab's listing bookkeeping are forgotten,
19
+ * so no later settlement writes to it.
20
+ */
21
+ import type { ClientRemote, RemoteResult } from '@deepseek-ai/dsh-api-remotes/client';
22
+ import type { BoundActions } from '@deepseek-ai/dsh-client-store';
23
+ import type { TabId } from '@deepseek-ai/dsh-client-ui-dockkit';
24
+ import type { SessionId } from '@deepseek-ai/dsh-session/types';
25
+ import type { DirLevel, createFilesStore } from './store.ts';
26
+ /**
27
+ * One directory listing, bound to a Remote face.
28
+ *
29
+ * The session travels with the call because the endpoint resolves the workspace
30
+ * root from it: the same path means different directories in different sessions.
31
+ * A Remote call does not reject — the result carries the failure.
32
+ */
33
+ export type ListWorkspaceDirectory = (sessionId: SessionId, path: string, signal: AbortSignal) => Promise<RemoteResult<DirLevel>>;
34
+ /**
35
+ * The slice of the Client Remote face this package calls: the `workspaceFiles`
36
+ * namespace's `list`, exactly as the Host's generated client declares it.
37
+ */
38
+ export type WorkspaceFilesListRemote = {
39
+ readonly workspaceFiles: Pick<ClientRemote['workspaceFiles'], 'list'>;
40
+ };
41
+ /**
42
+ * Bind the listing to one Remote face, keeping only what the tree stores.
43
+ * @param remote - the Client Remote face carrying the `workspaceFiles` namespace.
44
+ * @returns the listing the tree's face performs.
45
+ */
46
+ export declare function createList(remote: WorkspaceFilesListRemote): ListWorkspaceDirectory;
47
+ /**
48
+ * The absolute path of one child entry.
49
+ *
50
+ * Joined with `/` whatever the parent's separators: the Host resolves mixed
51
+ * separators, and the tree only needs a stable key.
52
+ * @param parent - absolute path of the listed directory.
53
+ * @param name - the entry's basename.
54
+ * @returns the child's absolute path.
55
+ */
56
+ export declare function childPath(parent: string, name: string): string;
57
+ /** The tree's injected business face, as the body receives it. */
58
+ export interface FilesInjected {
59
+ /**
60
+ * Seed this tab's tree and list its root.
61
+ * @param tabId - the tab being drawn.
62
+ * @param root - absolute path of the workspace root.
63
+ * @param signal - the tab record's lifetime.
64
+ */
65
+ readonly start: (tabId: TabId, root: string, signal: AbortSignal) => void;
66
+ /**
67
+ * List one directory into the store.
68
+ * @param tabId - the tab being drawn.
69
+ * @param path - absolute directory path.
70
+ * @param signal - the tab record's lifetime.
71
+ */
72
+ readonly load: (tabId: TabId, path: string, signal: AbortSignal) => void;
73
+ /**
74
+ * Open or collapse one directory, listing it the first time it opens.
75
+ * @param tabId - the tab being drawn.
76
+ * @param path - absolute directory path.
77
+ * @param loaded - whether this level already has state.
78
+ * @param signal - the tab record's lifetime.
79
+ */
80
+ readonly toggle: (tabId: TabId, path: string, loaded: boolean, signal: AbortSignal) => void;
81
+ }
82
+ /**
83
+ * Bind the tree's face to one directory listing.
84
+ * @param list - the bound `workspaceFiles.list` call.
85
+ * @returns the Slot `inject` factory: session and bound actions in, face out.
86
+ */
87
+ export declare function filesFace(list: ListWorkspaceDirectory): (sessionId: SessionId, actions: BoundActions<ReturnType<typeof createFilesStore>>) => FilesInjected;
88
+ //# sourceMappingURL=face.d.ts.map
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Browser half: register `files` as a right-Sidebar tab type.
3
+ *
4
+ * The public two-stage path, unmodified: the type into `ctx.sidebarRightTabs`,
5
+ * the body into the keyed `sidebar.right.pane.tab` seat under the type's `id`.
6
+ *
7
+ * The file split is this package's layering: what the type IS
8
+ * (`definition.ts`), what it keeps (`store.ts`), how it lists (`face.ts`), what
9
+ * it draws (`FilesBody.tsx`), what it says (`locales.ts`), and this module,
10
+ * which only wires them together.
11
+ */
12
+ import type { Context as ClientContext } from '@deepseek-ai/cordis';
13
+ export type { SidebarFilesKey } from './locales.ts';
14
+ export type { DirLevel, FilesState, FilesTabState, LevelState } from './store.ts';
15
+ export type { FilesInjected, ListWorkspaceDirectory, WorkspaceFilesListRemote } from './face.ts';
16
+ export type { FilesBodyProps } from './FilesBody.tsx';
17
+ /**
18
+ * Required browser services: the tab registry, the keyed seat, the Remote
19
+ * carrier and its namespace, and copy.
20
+ */
21
+ export declare const inject: string[];
22
+ /**
23
+ * Client plugin body: register the type, its dictionaries, then its body.
24
+ * @param ctx - client root context carrying the registry, the slots, and the Remote face.
25
+ */
26
+ export declare function apply(ctx: ClientContext): void;
27
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,41 @@
1
+ declare module '@deepseek-ai/dsh-client-ui-slots' {
2
+ interface LocaleNamespaceMap {
3
+ /** File-tree type name, guide entry, row states, and failure lines. */
4
+ sidebarFiles: SidebarFilesKey;
5
+ }
6
+ }
7
+ /** Simplified Chinese dictionary and key-set source of truth. */
8
+ export declare const zh: {
9
+ 'type.label': string;
10
+ 'guide.title': string;
11
+ 'guide.description': string;
12
+ loading: string;
13
+ empty: string;
14
+ truncated: string;
15
+ noWorkspace: string;
16
+ reload: string;
17
+ 'entry.other': string;
18
+ 'error.notFound': string;
19
+ 'error.outsideWorkspace': string;
20
+ 'error.notDirectory': string;
21
+ 'error.unavailable': string;
22
+ };
23
+ /** Files dictionary key union. */
24
+ export type SidebarFilesKey = keyof typeof zh;
25
+ /** English dictionary, checked against the Chinese key set. */
26
+ export declare const en: {
27
+ 'type.label': string;
28
+ 'guide.title': string;
29
+ 'guide.description': string;
30
+ loading: string;
31
+ empty: string;
32
+ truncated: string;
33
+ noWorkspace: string;
34
+ reload: string;
35
+ 'entry.other': string;
36
+ 'error.notFound': string;
37
+ 'error.outsideWorkspace': string;
38
+ 'error.notDirectory': string;
39
+ 'error.unavailable': string;
40
+ };
41
+ //# sourceMappingURL=locales.d.ts.map
@@ -0,0 +1,76 @@
1
+ /**
2
+ * The file tree's view state: which directories are expanded, and what each
3
+ * loaded level contains.
4
+ *
5
+ * The tree is not one resource. A directory listing per level, expanded lazily,
6
+ * is state the type owns — so it lives in a Slot-standard exclusive store
7
+ * (one instance per session), bucketed by tab id because two tabs of this kind
8
+ * in one session expand independently.
9
+ *
10
+ * Writers run between `start` and `forget`: the owner's `signal` is what ends a
11
+ * bucket's life, and the face stops dispatching once it aborts.
12
+ */
13
+ import { type EngineStoreHandle } from '@deepseek-ai/dsh-client-store';
14
+ import type { RemoteFailure } from '@deepseek-ai/dsh-api-remotes/client';
15
+ import type { TabId } from '@deepseek-ai/dsh-client-ui-dockkit';
16
+ import type { WorkspaceDirectoryEntry } from '@deepseek-ai/dsh-api-workspace-files/types';
17
+ /**
18
+ * One directory's contents, as one expanded level of the tree.
19
+ *
20
+ * The endpoint's listing also names the directory as a workspace-relative path;
21
+ * the tree keys every level by absolute path instead, so the adapter drops it.
22
+ */
23
+ export interface DirLevel {
24
+ /** The directory's entries, in the endpoint's order. */
25
+ readonly entries: readonly WorkspaceDirectoryEntry[];
26
+ /** The listing hit the endpoint's entry cap, so entries are missing. */
27
+ readonly truncated: boolean;
28
+ }
29
+ /** What one directory level is doing right now. */
30
+ export type LevelState = {
31
+ readonly kind: 'loading';
32
+ } | {
33
+ readonly kind: 'ready';
34
+ readonly level: DirLevel;
35
+ } | {
36
+ readonly kind: 'failed';
37
+ readonly failure: RemoteFailure;
38
+ };
39
+ /**
40
+ * One tab's tree: its root, the levels it has asked for, and what is open.
41
+ *
42
+ * Every path here is absolute: the root is the session's working directory as
43
+ * the Host reports it, and a child is the parent joined with the entry name.
44
+ */
45
+ export interface FilesTabState {
46
+ /** Absolute path of the workspace root this tree is rooted at. */
47
+ root: string;
48
+ /** Level state by absolute directory path; a path absent here was never asked for. */
49
+ levels: Record<string, LevelState>;
50
+ /** Expanded absolute directory paths, root included. */
51
+ expanded: string[];
52
+ }
53
+ /** Every tab's tree, keyed by tab id. */
54
+ export interface FilesState {
55
+ byTab: Record<TabId, FilesTabState>;
56
+ }
57
+ /** The tree store's write set; every action names the tab it writes. */
58
+ type FilesActions = {
59
+ start: (draft: FilesState, tabId: TabId, root: string) => void;
60
+ loading: (draft: FilesState, tabId: TabId, path: string) => void;
61
+ loaded: (draft: FilesState, tabId: TabId, path: string, level: DirLevel) => void;
62
+ failed: (draft: FilesState, tabId: TabId, path: string, failure: RemoteFailure) => void;
63
+ toggled: (draft: FilesState, tabId: TabId, path: string) => void;
64
+ reset: (draft: FilesState, tabId: TabId) => void;
65
+ forget: (draft: FilesState, tabId: TabId) => void;
66
+ };
67
+ /**
68
+ * Declare the file tree's store.
69
+ *
70
+ * A factory rather than a shared handle: the registration declares it as an
71
+ * exclusive store, so the framework mints one instance per session.
72
+ * @returns the store handle to declare on the registration.
73
+ */
74
+ export declare function createFilesStore(): EngineStoreHandle<FilesState, FilesActions>;
75
+ export {};
76
+ //# sourceMappingURL=store.d.ts.map
@@ -0,0 +1,4 @@
1
+ /** Pure host half; the whole tab type lives in the browser export. */
2
+ /** Host plugin body: the file tree contributes nothing to the host tree. */
3
+ export declare function apply(): void;
4
+ //# sourceMappingURL=index.d.ts.map
package/package.json ADDED
@@ -0,0 +1,76 @@
1
+ {
2
+ "name": "@deepseek-ai/dsh-client-ui-sidebar-files",
3
+ "description": "Workspace file tree tab type for the right Sidebar: lazy directory listing over the workspaceFiles Remote namespace, opening files into the Sidebar",
4
+ "version": "0.1.5-alpha.1",
5
+ "publishConfig": {
6
+ "access": "public"
7
+ },
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/deepseek-ai/deepseek-harness.git",
11
+ "directory": "packages/client/ui-sidebar-files"
12
+ },
13
+ "type": "module",
14
+ "main": "lib/index.js",
15
+ "types": "lib/types/index.d.ts",
16
+ "exports": {
17
+ ".": {
18
+ "types": "./lib/types/index.d.ts",
19
+ "default": "./lib/index.js"
20
+ },
21
+ "./client": {
22
+ "types": "./lib/types/client/index.d.ts",
23
+ "default": "./lib/client.js"
24
+ },
25
+ "./src/*": "./src/*",
26
+ "./package.json": "./package.json"
27
+ },
28
+ "dsh": {
29
+ "client": {
30
+ "inject": [
31
+ "@deepseek-ai/dsh-api-workspace-files",
32
+ "@deepseek-ai/dsh-client-ui-sidebar-right",
33
+ "@deepseek-ai/dsh-client-ui-session",
34
+ "@deepseek-ai/dsh-api-remotes"
35
+ ],
36
+ "platform": "web"
37
+ }
38
+ },
39
+ "license": "MIT",
40
+ "dependencies": {
41
+ "clsx": "^2.0.0",
42
+ "react": "^18.2.0",
43
+ "react-dom": "^18.2.0"
44
+ },
45
+ "peerDependencies": {
46
+ "@deepseek-ai/cordis": "^4.0.2"
47
+ },
48
+ "devDependencies": {
49
+ "@testing-library/react": "^16.1.0",
50
+ "@types/react": "~18.3.1",
51
+ "@types/react-dom": "~18.3.0",
52
+ "@deepseek-ai/cordis": "^4.0.2",
53
+ "@deepseek-ai/dsh-api-workspace-files": "^0.1.5-alpha.1",
54
+ "@deepseek-ai/dsh-api-remotes": "^0.1.5-alpha.1",
55
+ "@deepseek-ai/dsh-client-locale": "^0.1.5-alpha.1",
56
+ "@deepseek-ai/dsh-client-store": "^0.1.5-alpha.1",
57
+ "@deepseek-ai/dsh-client-test-runtime": "^0.1.5-alpha.1",
58
+ "@deepseek-ai/dsh-client-ui-dockkit": "^0.1.5-alpha.1",
59
+ "@deepseek-ai/dsh-client-ui-primitives": "^0.1.5-alpha.1",
60
+ "@deepseek-ai/dsh-client-ui-renderer": "^0.1.5-alpha.1",
61
+ "@deepseek-ai/dsh-client-ui-session": "^0.1.5-alpha.1",
62
+ "@deepseek-ai/dsh-client-ui-sidebar-right": "^0.1.5-alpha.1",
63
+ "@deepseek-ai/dsh-client-ui-slots": "^0.1.5-alpha.1",
64
+ "@deepseek-ai/dsh-session": "^0.1.5-alpha.1",
65
+ "@deepseek-ai/dsh-util-workspace-path": "^0.1.5-alpha.1"
66
+ },
67
+ "files": [
68
+ "lib/index.js",
69
+ "lib/client.js",
70
+ "lib/types/**/*.d.ts"
71
+ ],
72
+ "scripts": {
73
+ "bundle": "tsdown",
74
+ "watch": "tsdown --watch"
75
+ }
76
+ }