@deepseek-ai/dsh-client-ui-sidebar-files 0.1.6-alpha.2 → 0.1.7-alpha.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.i18n.yaml +2 -2
- package/README.md +11 -9
- package/README.zh.md +11 -9
- package/lib/client.js +316 -106
- package/lib/types/client/FilesBody.d.ts +1 -1
- package/lib/types/client/directory-node.d.ts +63 -0
- package/lib/types/client/face.d.ts +25 -5
- package/lib/types/client/locales.d.ts +6 -0
- package/lib/types/client/store.d.ts +3 -0
- package/package.json +17 -16
package/README.i18n.yaml
CHANGED
|
@@ -2,5 +2,5 @@
|
|
|
2
2
|
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
|
3
3
|
# after editing either side, bring the other along and re-record with:
|
|
4
4
|
# pnpm run verify-translation-pairing --write packages/client/ui-sidebar-files/README.md
|
|
5
|
-
README.md:
|
|
6
|
-
README.zh.md:
|
|
5
|
+
README.md: 0340991f16a61b80cd616982c6a6bea6ff60beac
|
|
6
|
+
README.zh.md: 016c74d801a3582785cb600f493eaeb92f7567b9
|
package/README.md
CHANGED
|
@@ -9,7 +9,7 @@ English | [中文](README.zh.md)
|
|
|
9
9
|
|
|
10
10
|
## Summary
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
Browse a Session's workspace tree and open files in Sidebar previews. The root and expanded directories refresh automatically from direct-entry watches; manual reload remains available. The tab is reached from the guide and claims no resource address.
|
|
13
13
|
|
|
14
14
|
## Table of Contents
|
|
15
15
|
|
|
@@ -25,25 +25,27 @@ The right Sidebar's navigator tab type: the session's workspace root as a tree,
|
|
|
25
25
|
## What it registers
|
|
26
26
|
|
|
27
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, its title and description from the `sidebarFiles` namespace, its glyph the shared folder icon) that opens the type.
|
|
28
|
-
- **The body** — the keyed `sidebar.right.pane.tab` seat under that id: a header row under the strip, then the tree. The
|
|
28
|
+
- **The body** — the keyed `sidebar.right.pane.tab` seat under that id: a header row under the strip, then the tree. The shared [`PathLabel`](../ui-primitives/README.md#component-catalog) displays the root path with subdued directories and a primary final segment. A clipped path retains its trailing characters with a left-edge fade; hovering reveals the full path. The reload control stays at its right.
|
|
29
29
|
- **The chip title** — the keyed `sidebar.right.pane.tab.title` seat under that id: a shared `FileTypeIcon` folder glyph at 16px before the type's label. The tree's own rows never draw this sheet.
|
|
30
30
|
|
|
31
|
-
|
|
31
|
+
Source files under `src/client/`: `definition.tsx` (the type), `store.ts` (what it keeps), `face.ts` (Remote reads and watches), `directory-node.ts` (open directories and their lifetimes), `FilesBody.tsx` (what it draws, with its ordering and failure-line helpers), `FilesTitle.tsx` (the chip title), `locales.ts` (what it says), and `index.ts` (the wiring).
|
|
32
32
|
|
|
33
33
|
<a id="the-tree"></a>
|
|
34
34
|
## The tree
|
|
35
35
|
|
|
36
|
-
The root is the session's working directory, read from `useSessions().byId[sessionId].cwd
|
|
36
|
+
The root is the session's working directory, read from `useSessions().byId[sessionId].cwd`. Filesystem roots such as `/` and Windows drive roots are valid tree roots. 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.
|
|
37
37
|
|
|
38
38
|
| Entry type | Row |
|
|
39
39
|
|---|---|
|
|
40
|
-
| `directory` | Toggles;
|
|
40
|
+
| `directory` | Toggles; reopening lists again and restores still-present expanded descendants. Displayed entries remain cached while collapsed. |
|
|
41
41
|
| `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. |
|
|
42
42
|
| `other` | Shown greyed and not clickable, so the directory is reported whole. |
|
|
43
43
|
|
|
44
|
-
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
|
|
44
|
+
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 refreshes the root and expanded levels in place, retaining displayed entries during reads instead of resetting the whole tree; collapsed levels are fetched again when they next open. A session without a working directory shows a single line instead of a tree.
|
|
45
45
|
|
|
46
|
-
State lives in the type's own store, bucketed by tab id: `root`, `levels` (loading / ready / failed per absolute path), `expanded`, and `scrollTop`, which the body tracks locally while scrolling and commits once when it unmounts. Because the store outlives the body, switching to another sidebar tab and back remounts the tree with its levels intact and its scroll offset restored. The owner's `signal` ends a bucket: on abort the tab is forgotten, and neither a listing that settles afterwards nor the unmount's offset commit writes anything.
|
|
46
|
+
State lives in the type's own store, bucketed by tab id: `root`, `levels` (loading / ready / failed per absolute path), `expanded`, `autoRefresh`, and `scrollTop`, which the body tracks locally while scrolling and commits once when it unmounts. Because the store outlives the body, switching to another sidebar tab and back remounts the tree with its levels intact and its scroll offset restored. The owner's `signal` ends a bucket: on abort the tab is forgotten, and neither a listing that settles afterwards nor the unmount's offset commit writes anything.
|
|
47
|
+
|
|
48
|
+
Each open `DirectoryNode` owns its target watch for the Tab lifetime; collapse closes that node and its hidden descendants. Expansion changes during ancestor restoration update the store and pending nodes, so restored descendants follow the latest expansion preferences. Automatic refresh defaults to enabled; its separate toggle is hidden while state, labels, styles, and toggle logic remain. Changes received during a directory read or its completion remain pending for another refresh.
|
|
47
49
|
|
|
48
50
|
<a id="model-experience"></a>
|
|
49
51
|
## Model Experience
|
|
@@ -57,7 +59,7 @@ None; directory listings travel over the Remote and assemble no model request.
|
|
|
57
59
|
## Known Limitations and Deferred Work
|
|
58
60
|
|
|
59
61
|
<a id="known-limitations-and-deferred-work"></a>
|
|
60
|
-
- **Listing only.** No search, artifact filter, drag-and-drop, rename, context menu, current-file highlight
|
|
62
|
+
- **Listing only.** No search, artifact filter, drag-and-drop, rename, context menu, or current-file highlight.
|
|
61
63
|
- **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.
|
|
62
64
|
|
|
63
65
|
<a id="dev-note"></a>
|
|
@@ -70,4 +72,4 @@ None.
|
|
|
70
72
|
|
|
71
73
|
</details>
|
|
72
74
|
|
|
73
|
-
**Runtime invariant:** No companion is published. The
|
|
75
|
+
**Runtime invariant:** No companion is published. The tab-owned face keeps directory reads and watches private, and writes displayed state through its Slot store; tab cancellation releases both. The package exposes no independent observation for a runtime comparison.
|
package/README.zh.md
CHANGED
|
@@ -9,7 +9,7 @@ kind: "package-reference"
|
|
|
9
9
|
|
|
10
10
|
## 概述
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
浏览 Session 的工作区目录树,并在 Sidebar 预览中打开文件。根目录与已展开目录通过直接子项监听自动刷新,手动重新读取仍可用。该 tab 从引导页进入,不认领资源地址。
|
|
13
13
|
|
|
14
14
|
## 目录
|
|
15
15
|
|
|
@@ -25,25 +25,27 @@ kind: "package-reference"
|
|
|
25
25
|
## 注册了什么
|
|
26
26
|
|
|
27
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` slot:strip
|
|
28
|
+
- **正文**:以该 id 为键的 `sidebar.right.pane.tab` slot:strip 下的一行标题行,然后是树。共享的 [`PathLabel`](../ui-primitives/README.zh.md#component-catalog) 显示根路径,目录使用弱化颜色,最后一段使用主色。路径过长时保留尾部字符并在左侧渐隐;悬停显示完整路径。重新读取控件位于右端。
|
|
29
29
|
- **标签页标题**:以该 id 为键的 `sidebar.right.pane.tab.title` slot:类型标签前的一枚 16px 共享 `FileTypeIcon` 文件夹图标。树本身的行不画这枚图标。
|
|
30
30
|
|
|
31
|
-
`src/client/`
|
|
31
|
+
`src/client/` 下的源文件:`definition.tsx`(类型是什么)、`store.ts`(它保存什么)、`face.ts`(Remote 读取与监听)、`directory-node.ts`(打开的目录及其生命周期)、`FilesBody.tsx`(它画什么,含排序与失败行两个辅助函数)、`FilesTitle.tsx`(标签页标题)、`locales.ts`(它说什么)、`index.ts`(接线)。
|
|
32
32
|
|
|
33
33
|
<a id="the-tree"></a>
|
|
34
34
|
## 树
|
|
35
35
|
|
|
36
|
-
根是会话的工作目录,读自 `useSessions().byId[sessionId].cwd
|
|
36
|
+
根是会话的工作目录,读自 `useSessions().byId[sessionId].cwd`。`/` 和 Windows 盘根等文件系统根路径均可作为树的根。每一层以绝对路径为键;子路径是父路径以 `/` 拼上条目名。一层在首次展开时经 `@deepseek-ai/dsh-api-workspace-files` 命名空间的 `remote.workspaceFiles.list(sessionId, absolutePath)` 列出;适配器保留列表的条目与截断标志,丢弃其工作区相对路径。行序为目录优先,其后按自然序、不分大小写的名称排列;dotfiles 与其他条目一样显示。
|
|
37
37
|
|
|
38
38
|
| 条目类型 | 行 |
|
|
39
39
|
|---|---|
|
|
40
|
-
| `directory` |
|
|
40
|
+
| `directory` | 切换展开与折叠;再次打开时重新列举,并恢复仍存在的已展开后代。折叠期间保留已显示条目的缓存。 |
|
|
41
41
|
| `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 里。 |
|
|
42
42
|
| `other` | 灰显且不可点击,从而完整呈现目录内容。 |
|
|
43
43
|
|
|
44
|
-
被端点条目上限截断的层以一条标记收尾;空层如实说明;失败的层按错误码各显示一行(`workspace-file/not-found`、`outside-workspace`、`not-directory
|
|
44
|
+
被端点条目上限截断的层以一条标记收尾;空层如实说明;失败的层按错误码各显示一行(`workspace-file/not-found`、`outside-workspace`、`not-directory`),其他情况显示传输层自己的消息。重新读取就地刷新根与展开中的层,读取期间保留显示条目,不重置整棵树;折叠的层在下次打开时重新拉取。没有工作目录的会话只显示一行说明,而不是树。
|
|
45
45
|
|
|
46
|
-
状态保存在类型自己的存储里,按 tab id 分桶:`root`、`levels`(每个绝对路径的 loading / ready / failed)、`expanded` 与 `scrollTop`——滚动期间偏移由正文自己记录,卸载时一次性写入。存储比 body 活得久,切到其他侧栏 tab 再切回来时树带着已加载的层重新挂载,滚动位置也随之恢复。owner 的 `signal` 终结一个桶:中止时忘掉该 tab,其后才结算的列表与卸载时的偏移提交都什么也不写。
|
|
46
|
+
状态保存在类型自己的存储里,按 tab id 分桶:`root`、`levels`(每个绝对路径的 loading / ready / failed)、`expanded`、`autoRefresh` 与 `scrollTop`——滚动期间偏移由正文自己记录,卸载时一次性写入。存储比 body 活得久,切到其他侧栏 tab 再切回来时树带着已加载的层重新挂载,滚动位置也随之恢复。owner 的 `signal` 终结一个桶:中止时忘掉该 tab,其后才结算的列表与卸载时的偏移提交都什么也不写。
|
|
47
|
+
|
|
48
|
+
每个打开的 `DirectoryNode` 在 Tab 生命周期内持有自己的目标监听;折叠关闭该节点及其不可见后代。祖先恢复期间的展开状态变化会更新存储和待恢复节点,后代恢复时遵循最新展开偏好。自动刷新默认开启;独立开关暂时隐藏,状态、文案、样式与切换逻辑保留。目录读取期间或收尾时收到的变化仍会留待下一次刷新。
|
|
47
49
|
|
|
48
50
|
<a id="model-experience"></a>
|
|
49
51
|
## 模型体验
|
|
@@ -57,7 +59,7 @@ kind: "package-reference"
|
|
|
57
59
|
## 已知限制与暂缓事项
|
|
58
60
|
|
|
59
61
|
<a id="known-limitations-and-deferred-work"></a>
|
|
60
|
-
-
|
|
62
|
+
- **只有列目录。**没有搜索、产物过滤、拖拽、重命名、右键菜单或当前文件高亮。
|
|
61
63
|
- **只有一个根。**树以会话工作目录为根;没有办法浏览到它之上,而 Host 本来也拒绝工作区根之外的路径。
|
|
62
64
|
|
|
63
65
|
<a id="dev-note"></a>
|
|
@@ -70,4 +72,4 @@ kind: "package-reference"
|
|
|
70
72
|
|
|
71
73
|
</details>
|
|
72
74
|
|
|
73
|
-
**运行时不变量:** 不发布 companion
|
|
75
|
+
**运行时不变量:** 不发布 companion。tab 持有的 face 内部管理目录读取与监听,并通过 Slot store 写入显示状态;tab 取消时释放二者。本包未暴露可用于运行时比对的独立观测源。
|
package/lib/client.js
CHANGED
|
@@ -6,6 +6,7 @@ window.__ModuleLoader__.load({
|
|
|
6
6
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
7
7
|
let react_jsx_runtime = require("react/jsx-runtime");
|
|
8
8
|
let _deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
|
|
9
|
+
require("@deepseek-ai/cordis");
|
|
9
10
|
let react = require("react");
|
|
10
11
|
let _deepseek_ai_dsh_client_store = require("@deepseek-ai/dsh-client-store");
|
|
11
12
|
//#region lib/types/client/definition.js
|
|
@@ -42,8 +43,215 @@ window.__ModuleLoader__.load({
|
|
|
42
43
|
};
|
|
43
44
|
}
|
|
44
45
|
//#endregion
|
|
46
|
+
//#region ../../typert/protocol/src/remote-error.ts
|
|
47
|
+
/**
|
|
48
|
+
* One Remote call failure: a real Error carrying its stable code and typed
|
|
49
|
+
* details. Owners throw it at the failure point; the Host Gateway encodes it
|
|
50
|
+
* onto the wire unchanged; the Client face rebuilds an instance for the
|
|
51
|
+
* `RemoteResult` error branch, so `throw result.error` keeps throw semantics.
|
|
52
|
+
* Discrimination is always by `code`, never by instanceof.
|
|
53
|
+
*/
|
|
54
|
+
var RemoteError = class extends Error {
|
|
55
|
+
code;
|
|
56
|
+
details;
|
|
57
|
+
/** Structural marker: cross-realm/bundle identification never uses instanceof. */
|
|
58
|
+
isDSHRemoteError = true;
|
|
59
|
+
/**
|
|
60
|
+
* @param code - stable failure code declared in {@link RemoteErrorDetailsMap}.
|
|
61
|
+
* @param message - human diagnostic carried across the wire.
|
|
62
|
+
* @param details - structured payload typed by the code.
|
|
63
|
+
* @param options - standard Error options (`cause` survives in-process only).
|
|
64
|
+
*/
|
|
65
|
+
constructor(code, message, details, options) {
|
|
66
|
+
super(message, options);
|
|
67
|
+
this.code = code;
|
|
68
|
+
this.details = details;
|
|
69
|
+
this.name = "RemoteError";
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
//#endregion
|
|
73
|
+
//#region ../../typert/protocol/src/index.ts
|
|
74
|
+
/**
|
|
75
|
+
* Remote decorators and explicit Gateway bindings backed by versioned
|
|
76
|
+
* descriptors carried on decorated class prototypes. Strict reflection
|
|
77
|
+
* remains a Typert compiler responsibility.
|
|
78
|
+
* @module @deepseek-ai/dsh-typert-protocol
|
|
79
|
+
*/
|
|
80
|
+
//#endregion
|
|
81
|
+
//#region lib/types/client/directory-node.js
|
|
82
|
+
/** One open directory and its active child nodes; cached view preferences live in the store. */
|
|
83
|
+
var DirectoryNode = class DirectoryNode {
|
|
84
|
+
path;
|
|
85
|
+
load;
|
|
86
|
+
watch;
|
|
87
|
+
failed;
|
|
88
|
+
restore;
|
|
89
|
+
/** Open direct-child directories, keyed by absolute path. */
|
|
90
|
+
children = /* @__PURE__ */ new Map();
|
|
91
|
+
controller = new AbortController();
|
|
92
|
+
signal;
|
|
93
|
+
task;
|
|
94
|
+
reading;
|
|
95
|
+
dirty = false;
|
|
96
|
+
initialized = false;
|
|
97
|
+
automatic = true;
|
|
98
|
+
constructor(path, load, watch, failed, lifetime, restore = []) {
|
|
99
|
+
this.path = path;
|
|
100
|
+
this.load = load;
|
|
101
|
+
this.watch = watch;
|
|
102
|
+
this.failed = failed;
|
|
103
|
+
this.restore = restore;
|
|
104
|
+
this.signal = AbortSignal.any([lifetime, this.controller.signal]);
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Start observation once; readiness triggers the initial listing.
|
|
108
|
+
* @returns this node.
|
|
109
|
+
*/
|
|
110
|
+
open() {
|
|
111
|
+
this.task ??= this.follow();
|
|
112
|
+
return this;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Find an active node in this subtree.
|
|
116
|
+
* @param path - absolute directory path.
|
|
117
|
+
* @returns the matching node, or undefined when that directory is closed.
|
|
118
|
+
*/
|
|
119
|
+
find(path) {
|
|
120
|
+
if (path === this.path) return this;
|
|
121
|
+
for (const child of this.children.values()) {
|
|
122
|
+
const found = child.find(path);
|
|
123
|
+
if (found !== void 0) return found;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Update the expansion preferences used by pending directory listings.
|
|
128
|
+
* @param expanded - latest expansion preferences from the store.
|
|
129
|
+
*/
|
|
130
|
+
setExpanded(expanded) {
|
|
131
|
+
this.restore = expanded;
|
|
132
|
+
for (const child of this.children.values()) child.setExpanded(expanded);
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Open a direct child using this node's lifetime and automatic-refresh setting.
|
|
136
|
+
* @param path - absolute direct-child directory path.
|
|
137
|
+
* @param restore - descendant expansion preferences to restore after listing.
|
|
138
|
+
* @returns the active child, or undefined after cancellation.
|
|
139
|
+
*/
|
|
140
|
+
expand(path, restore = []) {
|
|
141
|
+
if (this.signal.aborted) return void 0;
|
|
142
|
+
let child = this.children.get(path);
|
|
143
|
+
if (child === void 0) {
|
|
144
|
+
child = new DirectoryNode(path, this.load, this.watch, this.failed, this.signal, restore);
|
|
145
|
+
child.automatic = this.automatic;
|
|
146
|
+
this.children.set(path, child);
|
|
147
|
+
}
|
|
148
|
+
return child.open();
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Remove a child and its pending restoration preferences.
|
|
152
|
+
* @param path - absolute direct-child directory path.
|
|
153
|
+
* @returns once the child subtree's reads and watches have ended.
|
|
154
|
+
*/
|
|
155
|
+
async collapse(path) {
|
|
156
|
+
this.restore = this.restore.filter((value) => value !== path && !value.startsWith(`${path}/`));
|
|
157
|
+
const child = this.children.get(path);
|
|
158
|
+
this.children.delete(path);
|
|
159
|
+
await child?.close();
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Control subtree rereads without ending subscriptions.
|
|
163
|
+
* @param enabled - refresh dirty nodes automatically.
|
|
164
|
+
*/
|
|
165
|
+
setAutomatic(enabled) {
|
|
166
|
+
this.automatic = enabled;
|
|
167
|
+
if (enabled && this.dirty) this.refresh();
|
|
168
|
+
for (const child of this.children.values()) child.setAutomatic(enabled);
|
|
169
|
+
}
|
|
170
|
+
/** Queue a reread, coalescing with active work. @returns completion of the active read and its coalesced rereads. */
|
|
171
|
+
refresh() {
|
|
172
|
+
this.dirty = true;
|
|
173
|
+
this.reading ??= this.read();
|
|
174
|
+
return this.reading;
|
|
175
|
+
}
|
|
176
|
+
/** Refresh this node and its open descendants. @returns once their listings settle. */
|
|
177
|
+
async refreshTree() {
|
|
178
|
+
await this.refresh();
|
|
179
|
+
await Promise.all([...this.children.values()].map((child) => child.refreshTree()));
|
|
180
|
+
}
|
|
181
|
+
/** Cancel the active subtree. @returns once all owned reads and watches have ended. */
|
|
182
|
+
async close() {
|
|
183
|
+
this.controller.abort();
|
|
184
|
+
await Promise.all([
|
|
185
|
+
this.task,
|
|
186
|
+
this.reading,
|
|
187
|
+
...[...this.children.values()].map((child) => child.close())
|
|
188
|
+
]);
|
|
189
|
+
this.children.clear();
|
|
190
|
+
}
|
|
191
|
+
async follow() {
|
|
192
|
+
try {
|
|
193
|
+
for await (const _event of this.watch(this.path, this.signal)) {
|
|
194
|
+
this.dirty = true;
|
|
195
|
+
if (!this.initialized || this.automatic) this.refresh();
|
|
196
|
+
}
|
|
197
|
+
} catch (error) {
|
|
198
|
+
if (!this.signal.aborted) {
|
|
199
|
+
if (!this.initialized) await this.refresh();
|
|
200
|
+
if (!(typeof error === "object" && error !== null && "code" in error && error.code === "workspace-file/watch-unsupported")) this.failed(this.path, error);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
async read() {
|
|
205
|
+
const readAgain = () => this.dirty && this.automatic && !this.signal.aborted;
|
|
206
|
+
try {
|
|
207
|
+
do {
|
|
208
|
+
this.dirty = false;
|
|
209
|
+
const level = await this.load(this.path, this.signal);
|
|
210
|
+
if (this.signal.aborted || level === void 0) return;
|
|
211
|
+
this.initialized = true;
|
|
212
|
+
const directories = new Set(level.entries.filter((entry) => entry.type === "directory").map((entry) => `${this.path.replace(/[/\\]+$/, "")}/${entry.name}`));
|
|
213
|
+
for (const path of this.children.keys()) if (!directories.has(path)) await this.collapse(path);
|
|
214
|
+
for (const path of directories) if (this.restore.includes(path)) this.expand(path, this.restore);
|
|
215
|
+
this.restore = [];
|
|
216
|
+
} while (readAgain());
|
|
217
|
+
} finally {
|
|
218
|
+
this.reading = void 0;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
};
|
|
222
|
+
//#endregion
|
|
45
223
|
//#region lib/types/client/face.js
|
|
46
224
|
/**
|
|
225
|
+
* Bind directory observation to the Remote stream supervisor.
|
|
226
|
+
* @param remote - Client Remote with workspace file streams.
|
|
227
|
+
* @returns a watcher that awaits stream disposal when its node ends.
|
|
228
|
+
*/
|
|
229
|
+
function createWatch(remote) {
|
|
230
|
+
return async function* (sessionId, path, signal) {
|
|
231
|
+
const aborted = () => signal.aborted;
|
|
232
|
+
if (aborted()) return;
|
|
233
|
+
const stream = remote.$stream({
|
|
234
|
+
name: `directory ${path}`,
|
|
235
|
+
open: (lifetime) => remote.workspaceFiles.changes(sessionId, path, lifetime),
|
|
236
|
+
ended: () => /* @__PURE__ */ new Error(`Directory watch ended: ${path}`)
|
|
237
|
+
});
|
|
238
|
+
const abort = () => {
|
|
239
|
+
stream.dispose();
|
|
240
|
+
};
|
|
241
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
242
|
+
try {
|
|
243
|
+
for await (const item of stream) {
|
|
244
|
+
if (aborted()) return;
|
|
245
|
+
if (item.value.kind === "ready") item.accept();
|
|
246
|
+
yield item.value.kind;
|
|
247
|
+
}
|
|
248
|
+
} finally {
|
|
249
|
+
signal.removeEventListener("abort", abort);
|
|
250
|
+
await stream.dispose();
|
|
251
|
+
}
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
47
255
|
* Bind the listing to one Remote face, keeping only what the tree stores.
|
|
48
256
|
* @param remote - the Client Remote face carrying the `workspaceFiles` namespace.
|
|
49
257
|
* @returns the listing the tree's face performs.
|
|
@@ -76,12 +284,14 @@ window.__ModuleLoader__.load({
|
|
|
76
284
|
/**
|
|
77
285
|
* Bind the tree's face to one directory listing.
|
|
78
286
|
* @param list - the bound `workspaceFiles.list` call.
|
|
287
|
+
* @param watch - target-scoped directory observation.
|
|
79
288
|
* @returns the Slot `inject` factory: session and bound actions in, face out.
|
|
80
289
|
*/
|
|
81
|
-
function filesFace(list) {
|
|
290
|
+
function filesFace(list, watch) {
|
|
82
291
|
return (sessionId, actions) => {
|
|
83
292
|
/** Per tab, per absolute path: the listing generation a settlement must match; the latest request wins. */
|
|
84
293
|
const generations = /* @__PURE__ */ new Map();
|
|
294
|
+
const roots = /* @__PURE__ */ new Map();
|
|
85
295
|
const nextGeneration = (tabId, path) => {
|
|
86
296
|
const byPath = generations.get(tabId) ?? /* @__PURE__ */ new Map();
|
|
87
297
|
generations.set(tabId, byPath);
|
|
@@ -89,29 +299,52 @@ window.__ModuleLoader__.load({
|
|
|
89
299
|
byPath.set(path, generation);
|
|
90
300
|
return generation;
|
|
91
301
|
};
|
|
92
|
-
const load = (tabId, path, signal) => {
|
|
302
|
+
const load = async (tabId, path, signal) => {
|
|
93
303
|
if (signal.aborted) return;
|
|
94
304
|
const generation = nextGeneration(tabId, path);
|
|
95
305
|
actions.loading(tabId, path);
|
|
96
|
-
list(sessionId, path, signal).then((result) => {
|
|
97
|
-
if (generations.get(tabId)?.get(path) !== generation) return;
|
|
306
|
+
return list(sessionId, path, signal).then((result) => {
|
|
307
|
+
if (signal.aborted || generations.get(tabId)?.get(path) !== generation) return;
|
|
98
308
|
if (result.ok) actions.loaded(tabId, path, result.value);
|
|
99
309
|
else actions.failed(tabId, path, result.error);
|
|
310
|
+
return result.ok ? result.value : void 0;
|
|
100
311
|
});
|
|
101
312
|
};
|
|
102
313
|
return {
|
|
314
|
+
refresh: (tabId) => {
|
|
315
|
+
roots.get(tabId)?.refreshTree();
|
|
316
|
+
},
|
|
317
|
+
setAutoRefresh: (tabId, enabled) => {
|
|
318
|
+
actions.autoRefresh(tabId, enabled);
|
|
319
|
+
roots.get(tabId)?.setAutomatic(enabled);
|
|
320
|
+
},
|
|
103
321
|
start(tabId, root, signal) {
|
|
104
322
|
actions.start(tabId, root);
|
|
105
323
|
signal.addEventListener("abort", () => {
|
|
324
|
+
roots.get(tabId)?.close();
|
|
325
|
+
roots.delete(tabId);
|
|
106
326
|
generations.delete(tabId);
|
|
107
327
|
actions.forget(tabId);
|
|
108
328
|
}, { once: true });
|
|
109
|
-
|
|
329
|
+
roots.set(tabId, new DirectoryNode(root, (path, lifetime) => load(tabId, path, lifetime), (path, lifetime) => watch(sessionId, path, lifetime), (path, error) => {
|
|
330
|
+
if (!signal.aborted) actions.failed(tabId, path, new RemoteError("gateway/internal", error instanceof Error ? error.message : String(error), {}));
|
|
331
|
+
}, signal).open());
|
|
332
|
+
},
|
|
333
|
+
load: (tabId, path, signal) => {
|
|
334
|
+
load(tabId, path, signal);
|
|
110
335
|
},
|
|
111
|
-
|
|
112
|
-
|
|
336
|
+
toggle(tabId, parentPath, path, expanded, signal) {
|
|
337
|
+
if (signal.aborted) return;
|
|
338
|
+
const root = roots.get(tabId);
|
|
339
|
+
if (root === void 0) return;
|
|
340
|
+
const parent = root.find(parentPath);
|
|
341
|
+
if (parent === void 0 && !expanded.includes(parentPath)) return;
|
|
342
|
+
const collapsing = expanded.includes(path);
|
|
343
|
+
const next = collapsing ? expanded.filter((value) => value !== path) : [...expanded, path];
|
|
344
|
+
root.setExpanded(next);
|
|
345
|
+
if (collapsing) parent?.collapse(path);
|
|
346
|
+
else parent?.expand(path, next);
|
|
113
347
|
actions.toggled(tabId, path);
|
|
114
|
-
if (!loaded) load(tabId, path, signal);
|
|
115
348
|
}
|
|
116
349
|
};
|
|
117
350
|
};
|
|
@@ -172,27 +405,6 @@ window.__ModuleLoader__.load({
|
|
|
172
405
|
return path.startsWith("/") || isWindowsStylePath(path);
|
|
173
406
|
}
|
|
174
407
|
/**
|
|
175
|
-
* Split a path for display: the directories through their last separator, and
|
|
176
|
-
* the final segment after it. Both `/` and `\` separate, so a Windows path
|
|
177
|
-
* splits where its own segments end; trailing separators are dropped first, so
|
|
178
|
-
* a directory path names its own last segment. A path with no separator, or a
|
|
179
|
-
* separator-only path, is all name.
|
|
180
|
-
* @param path - file or directory path using POSIX or Windows separators.
|
|
181
|
-
* @returns the directory prefix (possibly empty) and the final segment.
|
|
182
|
-
*/
|
|
183
|
-
function pathPartsOf(path) {
|
|
184
|
-
const trimmed = path.replace(/[/\\]+$/, "");
|
|
185
|
-
if (trimmed === "") return {
|
|
186
|
-
directory: "",
|
|
187
|
-
name: path
|
|
188
|
-
};
|
|
189
|
-
const cut = Math.max(trimmed.lastIndexOf("/"), trimmed.lastIndexOf("\\")) + 1;
|
|
190
|
-
return {
|
|
191
|
-
directory: trimmed.slice(0, cut),
|
|
192
|
-
name: trimmed.slice(cut)
|
|
193
|
-
};
|
|
194
|
-
}
|
|
195
|
-
/**
|
|
196
408
|
* The address for a path as a caller holds it: a relative path, or an absolute
|
|
197
409
|
* path inside the Session's workspace, becomes a `session`-scoped address; an
|
|
198
410
|
* absolute path outside it, or one whose workspace root is unknown, keeps its
|
|
@@ -212,7 +424,7 @@ window.__ModuleLoader__.load({
|
|
|
212
424
|
}
|
|
213
425
|
//#endregion
|
|
214
426
|
//#region \0dsh-css:/home/runner/work/deepseek-harness/deepseek-harness/packages/client/ui-sidebar-files/src/client/FilesBody.module.css.mjs
|
|
215
|
-
const css = ".k-1LKG_root{height:100%;min-height:0;color:var(--dsw-alias-label-primary);font-size:var(--dsh-content-font-size-secondary,13px);flex-direction:column;flex:auto;line-height:1.5;display:flex}.k-1LKG_header{box-sizing:border-box;border-bottom:.5px solid var(--dsw-alias-border-l3);flex:none;align-items:center;gap:4px;height:38px;padding:0 6px 0 16px;display:flex}.k-1LKG_path{
|
|
427
|
+
const css = ".k-1LKG_root{height:100%;min-height:0;color:var(--dsw-alias-label-primary);font-size:var(--dsh-content-font-size-secondary,13px);flex-direction:column;flex:auto;line-height:1.5;display:flex}.k-1LKG_header{box-sizing:border-box;border-bottom:.5px solid var(--dsw-alias-border-l3);flex:none;align-items:center;gap:4px;height:38px;padding:0 6px 0 16px;display:flex}.k-1LKG_path{margin-right:12px}.k-1LKG_body{scrollbar-gutter:stable;flex:auto;min-height:0;margin-right:2px;padding:8px 0 8px 8px;overflow:auto}.k-1LKG_body::-webkit-scrollbar-track{margin:2px}.k-1LKG_level{margin:0;padding:0;list-style:none}.k-1LKG_level .k-1LKG_level{padding-left:18px}.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:10px;align-items:center;gap:6px;padding:5px 10px;display:flex}.k-1LKG_row:hover{background:var(--dsw-alias-interactive-bg-hover)}.k-1LKG_icon{color:var(--dsw-alias-label-tertiary);flex:none}.k-1LKG_fileIcon{flex:none}.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:28px;height:28px;color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border:none;border-radius:28px;flex:none;justify-content:center;align-items:center;padding:6px;line-height:1;display:inline-flex}.k-1LKG_tool svg{width:15px;height:15px}.k-1LKG_tool:hover,.k-1LKG_tool[aria-pressed=true]{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-interactive-bg-hover)}.k-1LKG_titleIcon{flex:none}";
|
|
216
428
|
const tagId = "@deepseek-ai/dsh-client-ui-sidebar-files/FilesBody.module.css";
|
|
217
429
|
if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
|
|
218
430
|
const tag = document.createElement("style");
|
|
@@ -232,9 +444,6 @@ window.__ModuleLoader__.load({
|
|
|
232
444
|
"note": "k-1LKG_note",
|
|
233
445
|
"other": "k-1LKG_other",
|
|
234
446
|
"path": "k-1LKG_path",
|
|
235
|
-
"pathDirectory": "k-1LKG_pathDirectory",
|
|
236
|
-
"pathName": "k-1LKG_pathName",
|
|
237
|
-
"pathText": "k-1LKG_pathText",
|
|
238
447
|
"root": "k-1LKG_root",
|
|
239
448
|
"row": "k-1LKG_row",
|
|
240
449
|
"status": "k-1LKG_status",
|
|
@@ -251,10 +460,8 @@ window.__ModuleLoader__.load({
|
|
|
251
460
|
* for goes through its injected face. The component itself only decides what to
|
|
252
461
|
* draw for each absolute path and what a click means: a directory toggles, a
|
|
253
462
|
* file opens through the owner's `tabActions` for a `file:` viewer to claim, and
|
|
254
|
-
* anything else is shown but refuses to open. The header
|
|
255
|
-
*
|
|
256
|
-
* ink, then the one control at its end, reload, which drops every listed level
|
|
257
|
-
* and asks again for the expanded ones.
|
|
463
|
+
* anything else is shown but refuses to open. The header uses the shared
|
|
464
|
+
* PathLabel for the root, followed by reload for the expanded directories.
|
|
258
465
|
*/
|
|
259
466
|
/** Natural, case-insensitive name order, so `file2` precedes `file10`. */
|
|
260
467
|
const byName = new Intl.Collator(void 0, {
|
|
@@ -288,35 +495,6 @@ window.__ModuleLoader__.load({
|
|
|
288
495
|
default: return t("error.unavailable", { message: failure.message });
|
|
289
496
|
}
|
|
290
497
|
}
|
|
291
|
-
/**
|
|
292
|
-
* Keep the path row's `data-files-path-clipped` current: set while the path's
|
|
293
|
-
* text is wider than its box, so the stylesheet fades the clipped start. Read
|
|
294
|
-
* after each commit that can change the path or mount the header, and whenever
|
|
295
|
-
* either box resizes; written to the DOM directly because it changes only how
|
|
296
|
-
* the stylesheet fades what is already rendered.
|
|
297
|
-
*/
|
|
298
|
-
function usePathClipped(box, text, path) {
|
|
299
|
-
(0, react.useLayoutEffect)(() => {
|
|
300
|
-
const outer = box.current;
|
|
301
|
-
const inner = text.current;
|
|
302
|
-
if (outer === null || inner === null) return void 0;
|
|
303
|
-
const apply = () => {
|
|
304
|
-
if (inner.offsetWidth > outer.clientWidth) outer.dataset.filesPathClipped = "";
|
|
305
|
-
else delete outer.dataset.filesPathClipped;
|
|
306
|
-
};
|
|
307
|
-
apply();
|
|
308
|
-
const observer = typeof ResizeObserver === "undefined" ? void 0 : new ResizeObserver(apply);
|
|
309
|
-
observer?.observe(outer);
|
|
310
|
-
observer?.observe(inner);
|
|
311
|
-
return () => {
|
|
312
|
-
observer?.disconnect();
|
|
313
|
-
};
|
|
314
|
-
}, [
|
|
315
|
-
box,
|
|
316
|
-
text,
|
|
317
|
-
path
|
|
318
|
-
]);
|
|
319
|
-
}
|
|
320
498
|
/** One entry's row, and its children when it is an expanded directory. */
|
|
321
499
|
function Entry({ parent, entry, tree }) {
|
|
322
500
|
const path = childPath(parent, entry.name);
|
|
@@ -331,9 +509,9 @@ window.__ModuleLoader__.load({
|
|
|
331
509
|
className: FilesBody_module_css_default.row,
|
|
332
510
|
"aria-expanded": expanded,
|
|
333
511
|
onClick: () => {
|
|
334
|
-
tree.onToggle(path);
|
|
512
|
+
tree.onToggle(parent, path);
|
|
335
513
|
},
|
|
336
|
-
children: [expanded ? (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.
|
|
514
|
+
children: [expanded ? (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconFolderOpenRegular, { className: FilesBody_module_css_default.icon }) : (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconFolderCloseRegular, { className: FilesBody_module_css_default.icon }), (0, react_jsx_runtime.jsx)("span", {
|
|
337
515
|
className: FilesBody_module_css_default.name,
|
|
338
516
|
children: entry.name
|
|
339
517
|
})]
|
|
@@ -398,6 +576,11 @@ window.__ModuleLoader__.load({
|
|
|
398
576
|
});
|
|
399
577
|
const entries = orderEntries(level.level.entries);
|
|
400
578
|
return (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
579
|
+
level.failure !== void 0 && (0, react_jsx_runtime.jsx)("li", {
|
|
580
|
+
className: FilesBody_module_css_default.note,
|
|
581
|
+
"data-files-row": "failed",
|
|
582
|
+
children: failureLine(t, level.failure)
|
|
583
|
+
}),
|
|
401
584
|
entries.length === 0 && (0, react_jsx_runtime.jsx)("li", {
|
|
402
585
|
className: FilesBody_module_css_default.note,
|
|
403
586
|
"data-files-row": "empty",
|
|
@@ -416,16 +599,13 @@ window.__ModuleLoader__.load({
|
|
|
416
599
|
] });
|
|
417
600
|
}
|
|
418
601
|
/** The file tree's body: the workspace root and whatever the reader has opened under it. */
|
|
419
|
-
function FilesBody({ useTabInfo, sessionId, useSessions, useStore, actions, start,
|
|
602
|
+
function FilesBody({ useTabInfo, sessionId, useSessions, useStore, actions, start, refresh, setAutoRefresh, toggle, t }) {
|
|
420
603
|
const { tab } = useTabInfo();
|
|
421
604
|
const { signal, actions: tabActions } = tab;
|
|
422
605
|
const cwd = useSessions((sessions) => sessions.byId[sessionId]?.cwd);
|
|
423
606
|
const state = useStore((store) => store.byTab[tab.id]);
|
|
424
|
-
const pathRef = (0, react.useRef)(null);
|
|
425
|
-
const pathTextRef = (0, react.useRef)(null);
|
|
426
607
|
const bodyRef = (0, react.useRef)(null);
|
|
427
608
|
const scrollTopRef = (0, react.useRef)(0);
|
|
428
|
-
usePathClipped(pathRef, pathTextRef, state?.root);
|
|
429
609
|
const seeded = state !== void 0;
|
|
430
610
|
(0, react.useLayoutEffect)(() => {
|
|
431
611
|
const body = bodyRef.current;
|
|
@@ -463,8 +643,8 @@ window.__ModuleLoader__.load({
|
|
|
463
643
|
if (state === void 0) return null;
|
|
464
644
|
const tree = {
|
|
465
645
|
state,
|
|
466
|
-
onToggle: (path) => {
|
|
467
|
-
toggle(tab.id, path, state.
|
|
646
|
+
onToggle: (parent, path) => {
|
|
647
|
+
toggle(tab.id, parent, path, state.expanded, signal);
|
|
468
648
|
},
|
|
469
649
|
onOpen: (path) => {
|
|
470
650
|
tabActions.openResource(fileAddressFor(sessionId, state.root, path));
|
|
@@ -472,41 +652,45 @@ window.__ModuleLoader__.load({
|
|
|
472
652
|
t
|
|
473
653
|
};
|
|
474
654
|
const reload = () => {
|
|
475
|
-
|
|
476
|
-
for (const path of state.expanded) load(tab.id, path, signal);
|
|
655
|
+
refresh(tab.id);
|
|
477
656
|
};
|
|
478
|
-
const { directory, name } = pathPartsOf(state.root);
|
|
479
657
|
return (0, react_jsx_runtime.jsxs)("div", {
|
|
480
658
|
className: FilesBody_module_css_default.root,
|
|
481
659
|
"data-files-state": "tree",
|
|
482
660
|
"data-files-root": state.root,
|
|
483
661
|
children: [(0, react_jsx_runtime.jsxs)("div", {
|
|
484
662
|
className: FilesBody_module_css_default.header,
|
|
485
|
-
children: [
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
children:
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
663
|
+
children: [
|
|
664
|
+
(0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.PathLabel, {
|
|
665
|
+
path: state.root,
|
|
666
|
+
className: FilesBody_module_css_default.path,
|
|
667
|
+
"data-files-path": true
|
|
668
|
+
}),
|
|
669
|
+
(0, react_jsx_runtime.jsx)("span", {
|
|
670
|
+
hidden: true,
|
|
671
|
+
children: (0, react_jsx_runtime.jsx)("button", {
|
|
672
|
+
type: "button",
|
|
673
|
+
className: FilesBody_module_css_default.tool,
|
|
674
|
+
"aria-label": t("autoRefresh"),
|
|
675
|
+
"aria-pressed": state.autoRefresh,
|
|
676
|
+
"data-files-auto-refresh": true,
|
|
677
|
+
title: t(state.autoRefresh ? "autoRefresh.disable" : "autoRefresh.enable"),
|
|
678
|
+
onClick: () => {
|
|
679
|
+
setAutoRefresh(tab.id, !state.autoRefresh);
|
|
680
|
+
},
|
|
681
|
+
children: state.autoRefresh ? (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconPauseOutlineRegular, {}) : (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconPlayOutlineRegular, {})
|
|
682
|
+
})
|
|
683
|
+
}),
|
|
684
|
+
(0, react_jsx_runtime.jsx)("button", {
|
|
685
|
+
type: "button",
|
|
686
|
+
className: FilesBody_module_css_default.tool,
|
|
687
|
+
"aria-label": t("reload"),
|
|
688
|
+
title: t("reload"),
|
|
689
|
+
"data-files-reload": true,
|
|
690
|
+
onClick: reload,
|
|
691
|
+
children: (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconRefreshOutlineRegular, {})
|
|
500
692
|
})
|
|
501
|
-
|
|
502
|
-
type: "button",
|
|
503
|
-
className: FilesBody_module_css_default.tool,
|
|
504
|
-
"aria-label": t("reload"),
|
|
505
|
-
title: t("reload"),
|
|
506
|
-
"data-files-reload": true,
|
|
507
|
-
onClick: reload,
|
|
508
|
-
children: (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconRefreshOutline16, {})
|
|
509
|
-
})]
|
|
693
|
+
]
|
|
510
694
|
}), (0, react_jsx_runtime.jsx)("div", {
|
|
511
695
|
ref: bodyRef,
|
|
512
696
|
className: FilesBody_module_css_default.body,
|
|
@@ -551,6 +735,9 @@ window.__ModuleLoader__.load({
|
|
|
551
735
|
truncated: "条目太多,只显示了一部分。",
|
|
552
736
|
noWorkspace: "这个会话没有工作区目录。",
|
|
553
737
|
reload: "重新读取",
|
|
738
|
+
autoRefresh: "自动刷新",
|
|
739
|
+
"autoRefresh.enable": "开启自动刷新",
|
|
740
|
+
"autoRefresh.disable": "关闭自动刷新",
|
|
554
741
|
"entry.other": "这不是文件或目录,没法打开。",
|
|
555
742
|
"error.notFound": "这个目录不在了。可能已被移动或删除。",
|
|
556
743
|
"error.outsideWorkspace": "这个目录在工作区之外,侧栏不会读取它。",
|
|
@@ -567,6 +754,9 @@ window.__ModuleLoader__.load({
|
|
|
567
754
|
truncated: "Too many entries, showing only some of them.",
|
|
568
755
|
noWorkspace: "This session has no workspace directory.",
|
|
569
756
|
reload: "Reload",
|
|
757
|
+
autoRefresh: "Auto refresh",
|
|
758
|
+
"autoRefresh.enable": "Enable auto refresh",
|
|
759
|
+
"autoRefresh.disable": "Disable auto refresh",
|
|
570
760
|
"entry.other": "Not a file or a directory, so it cannot be opened.",
|
|
571
761
|
"error.notFound": "That directory is gone. It may have been moved or deleted.",
|
|
572
762
|
"error.outsideWorkspace": "That directory is outside the workspace, so the sidebar will not read it.",
|
|
@@ -610,6 +800,9 @@ window.__ModuleLoader__.load({
|
|
|
610
800
|
return (0, _deepseek_ai_dsh_client_store.defineStore)({
|
|
611
801
|
init: () => ({ byTab: {} }),
|
|
612
802
|
actions: {
|
|
803
|
+
autoRefresh: (d, tabId, enabled) => {
|
|
804
|
+
bucket(d, tabId).autoRefresh = enabled;
|
|
805
|
+
},
|
|
613
806
|
/**
|
|
614
807
|
* Seed one tab's tree at its workspace root, with the root expanded.
|
|
615
808
|
* @param d - draft state.
|
|
@@ -621,7 +814,8 @@ window.__ModuleLoader__.load({
|
|
|
621
814
|
root,
|
|
622
815
|
levels: {},
|
|
623
816
|
expanded: [root],
|
|
624
|
-
scrollTop: 0
|
|
817
|
+
scrollTop: 0,
|
|
818
|
+
autoRefresh: true
|
|
625
819
|
};
|
|
626
820
|
},
|
|
627
821
|
/**
|
|
@@ -631,7 +825,8 @@ window.__ModuleLoader__.load({
|
|
|
631
825
|
* @param path - absolute directory path.
|
|
632
826
|
*/
|
|
633
827
|
loading: (d, tabId, path) => {
|
|
634
|
-
bucket(d, tabId)
|
|
828
|
+
const state = bucket(d, tabId);
|
|
829
|
+
if (state.levels[path]?.kind !== "ready") state.levels[path] = { kind: "loading" };
|
|
635
830
|
},
|
|
636
831
|
/**
|
|
637
832
|
* Record one directory's contents.
|
|
@@ -641,7 +836,18 @@ window.__ModuleLoader__.load({
|
|
|
641
836
|
* @param level - the listing to show under it.
|
|
642
837
|
*/
|
|
643
838
|
loaded: (d, tabId, path, level) => {
|
|
644
|
-
bucket(d, tabId)
|
|
839
|
+
const state = bucket(d, tabId);
|
|
840
|
+
const previous = state.levels[path];
|
|
841
|
+
if (previous?.kind === "ready") {
|
|
842
|
+
const directories = new Set(level.entries.filter((entry) => entry.type === "directory").map((entry) => entry.name));
|
|
843
|
+
for (const entry of previous.level.entries) {
|
|
844
|
+
if (entry.type !== "directory" || directories.has(entry.name)) continue;
|
|
845
|
+
const removed = `${path.replace(/[/\\]+$/, "")}/${entry.name}`;
|
|
846
|
+
state.expanded = state.expanded.filter((value) => value !== removed && !value.startsWith(`${removed}/`));
|
|
847
|
+
state.levels = Object.fromEntries(Object.entries(state.levels).filter(([key]) => key !== removed && !key.startsWith(`${removed}/`)));
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
state.levels[path] = {
|
|
645
851
|
kind: "ready",
|
|
646
852
|
level
|
|
647
853
|
};
|
|
@@ -654,7 +860,11 @@ window.__ModuleLoader__.load({
|
|
|
654
860
|
* @param failure - the settled Remote failure.
|
|
655
861
|
*/
|
|
656
862
|
failed: (d, tabId, path, failure) => {
|
|
657
|
-
bucket(d, tabId).levels[path]
|
|
863
|
+
const level = bucket(d, tabId).levels[path];
|
|
864
|
+
bucket(d, tabId).levels[path] = level?.kind === "ready" ? {
|
|
865
|
+
...level,
|
|
866
|
+
failure
|
|
867
|
+
} : {
|
|
658
868
|
kind: "failed",
|
|
659
869
|
failure
|
|
660
870
|
};
|
|
@@ -731,7 +941,7 @@ window.__ModuleLoader__.load({
|
|
|
731
941
|
en
|
|
732
942
|
}), "ui-sidebar-files: dictionaries");
|
|
733
943
|
const store = createFilesStore();
|
|
734
|
-
const inject = filesFace(createList(ctx.remote));
|
|
944
|
+
const inject = filesFace(createList(ctx.remote), createWatch(ctx.remote));
|
|
735
945
|
ctx.effect(() => ctx.slots.inject("sidebar.right.pane.tab", () => ctx.slots.register({
|
|
736
946
|
name: "sidebar.right.pane.tab",
|
|
737
947
|
key: FILES_ID,
|
|
@@ -22,5 +22,5 @@ export declare function orderEntries(entries: readonly WorkspaceDirectoryEntry[]
|
|
|
22
22
|
*/
|
|
23
23
|
export declare function failureLine(t: TranslateNS<'sidebarFiles'>, failure: RemoteFailure): string;
|
|
24
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,
|
|
25
|
+
export declare function FilesBody({ useTabInfo, sessionId, useSessions, useStore, actions, start, refresh, setAutoRefresh, toggle, t, }: FilesBodyProps): ReactNode;
|
|
26
26
|
//# sourceMappingURL=FilesBody.d.ts.map
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/** Watch and read ownership for the Files panel's expanded directory tree. */
|
|
2
|
+
import type { DirLevel } from './store.ts';
|
|
3
|
+
/** One open directory and its active child nodes; cached view preferences live in the store. */
|
|
4
|
+
export declare class DirectoryNode {
|
|
5
|
+
readonly path: string;
|
|
6
|
+
private readonly load;
|
|
7
|
+
private readonly watch;
|
|
8
|
+
private readonly failed;
|
|
9
|
+
private restore;
|
|
10
|
+
/** Open direct-child directories, keyed by absolute path. */
|
|
11
|
+
readonly children: Map<string, DirectoryNode>;
|
|
12
|
+
private readonly controller;
|
|
13
|
+
private readonly signal;
|
|
14
|
+
private task;
|
|
15
|
+
private reading;
|
|
16
|
+
private dirty;
|
|
17
|
+
private initialized;
|
|
18
|
+
private automatic;
|
|
19
|
+
constructor(path: string, load: (path: string, signal: AbortSignal) => Promise<DirLevel | undefined>, watch: (path: string, signal: AbortSignal) => AsyncIterable<'ready' | 'change'>, failed: (path: string, error: unknown) => void, lifetime: AbortSignal, restore?: readonly string[]);
|
|
20
|
+
/**
|
|
21
|
+
* Start observation once; readiness triggers the initial listing.
|
|
22
|
+
* @returns this node.
|
|
23
|
+
*/
|
|
24
|
+
open(): this;
|
|
25
|
+
/**
|
|
26
|
+
* Find an active node in this subtree.
|
|
27
|
+
* @param path - absolute directory path.
|
|
28
|
+
* @returns the matching node, or undefined when that directory is closed.
|
|
29
|
+
*/
|
|
30
|
+
find(path: string): DirectoryNode | undefined;
|
|
31
|
+
/**
|
|
32
|
+
* Update the expansion preferences used by pending directory listings.
|
|
33
|
+
* @param expanded - latest expansion preferences from the store.
|
|
34
|
+
*/
|
|
35
|
+
setExpanded(expanded: readonly string[]): void;
|
|
36
|
+
/**
|
|
37
|
+
* Open a direct child using this node's lifetime and automatic-refresh setting.
|
|
38
|
+
* @param path - absolute direct-child directory path.
|
|
39
|
+
* @param restore - descendant expansion preferences to restore after listing.
|
|
40
|
+
* @returns the active child, or undefined after cancellation.
|
|
41
|
+
*/
|
|
42
|
+
expand(path: string, restore?: readonly string[]): DirectoryNode | undefined;
|
|
43
|
+
/**
|
|
44
|
+
* Remove a child and its pending restoration preferences.
|
|
45
|
+
* @param path - absolute direct-child directory path.
|
|
46
|
+
* @returns once the child subtree's reads and watches have ended.
|
|
47
|
+
*/
|
|
48
|
+
collapse(path: string): Promise<void>;
|
|
49
|
+
/**
|
|
50
|
+
* Control subtree rereads without ending subscriptions.
|
|
51
|
+
* @param enabled - refresh dirty nodes automatically.
|
|
52
|
+
*/
|
|
53
|
+
setAutomatic(enabled: boolean): void;
|
|
54
|
+
/** Queue a reread, coalescing with active work. @returns completion of the active read and its coalesced rereads. */
|
|
55
|
+
refresh(): Promise<void>;
|
|
56
|
+
/** Refresh this node and its open descendants. @returns once their listings settle. */
|
|
57
|
+
refreshTree(): Promise<void>;
|
|
58
|
+
/** Cancel the active subtree. @returns once all owned reads and watches have ended. */
|
|
59
|
+
close(): Promise<void>;
|
|
60
|
+
private follow;
|
|
61
|
+
private read;
|
|
62
|
+
}
|
|
63
|
+
//# sourceMappingURL=directory-node.d.ts.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The tree's asynchronous half: listing directories into the store.
|
|
3
3
|
*
|
|
4
|
-
* The component never awaits anything. It calls `start` / `
|
|
4
|
+
* The component never awaits anything. It calls `start` / `refresh` / `toggle`, and
|
|
5
5
|
* this face performs the listing and writes the outcome through the store's own
|
|
6
6
|
* actions — the Slot-standard `inject` shape, so the session id is resolved by
|
|
7
7
|
* the framework and the write set stays the store's.
|
|
@@ -23,6 +23,20 @@ import type { BoundActions } from '@deepseek-ai/dsh-client-store';
|
|
|
23
23
|
import type { TabId } from '@deepseek-ai/dsh-client-ui-dockkit';
|
|
24
24
|
import type { SessionId } from '@deepseek-ai/dsh-session/types';
|
|
25
25
|
import type { DirLevel, createFilesStore } from './store.ts';
|
|
26
|
+
/**
|
|
27
|
+
* Observe one directory without recursively watching its descendants.
|
|
28
|
+
* @param sessionId - Session owning the directory tree.
|
|
29
|
+
* @param path - absolute directory path.
|
|
30
|
+
* @param signal - node lifetime.
|
|
31
|
+
* @returns readiness and invalidation notifications.
|
|
32
|
+
*/
|
|
33
|
+
export type WatchWorkspaceDirectory = (sessionId: SessionId, path: string, signal: AbortSignal) => AsyncIterable<'ready' | 'change'>;
|
|
34
|
+
/**
|
|
35
|
+
* Bind directory observation to the Remote stream supervisor.
|
|
36
|
+
* @param remote - Client Remote with workspace file streams.
|
|
37
|
+
* @returns a watcher that awaits stream disposal when its node ends.
|
|
38
|
+
*/
|
|
39
|
+
export declare function createWatch(remote: ClientRemote): WatchWorkspaceDirectory;
|
|
26
40
|
/**
|
|
27
41
|
* One directory listing, bound to a Remote face.
|
|
28
42
|
*
|
|
@@ -56,6 +70,10 @@ export declare function createList(remote: WorkspaceFilesListRemote): ListWorksp
|
|
|
56
70
|
export declare function childPath(parent: string, name: string): string;
|
|
57
71
|
/** The tree's injected business face, as the body receives it. */
|
|
58
72
|
export interface FilesInjected {
|
|
73
|
+
/** Refresh the open directory tree. @param tabId - owning tab. */
|
|
74
|
+
readonly refresh: (tabId: TabId) => void;
|
|
75
|
+
/** Control automatic rereads without closing watches. @param tabId - owning tab. @param enabled - automatic-refresh setting. */
|
|
76
|
+
readonly setAutoRefresh: (tabId: TabId, enabled: boolean) => void;
|
|
59
77
|
/**
|
|
60
78
|
* Seed this tab's tree and list its root.
|
|
61
79
|
* @param tabId - the tab being drawn.
|
|
@@ -71,18 +89,20 @@ export interface FilesInjected {
|
|
|
71
89
|
*/
|
|
72
90
|
readonly load: (tabId: TabId, path: string, signal: AbortSignal) => void;
|
|
73
91
|
/**
|
|
74
|
-
* Open or collapse one directory,
|
|
92
|
+
* Open or collapse one directory, retaining intent during ancestor restoration.
|
|
75
93
|
* @param tabId - the tab being drawn.
|
|
94
|
+
* @param parentPath - the listed parent directory's exact tree key.
|
|
76
95
|
* @param path - absolute directory path.
|
|
77
|
-
* @param
|
|
96
|
+
* @param expanded - current expansion preferences, including descendants to restore.
|
|
78
97
|
* @param signal - the tab record's lifetime.
|
|
79
98
|
*/
|
|
80
|
-
readonly toggle: (tabId: TabId, path: string,
|
|
99
|
+
readonly toggle: (tabId: TabId, parentPath: string, path: string, expanded: readonly string[], signal: AbortSignal) => void;
|
|
81
100
|
}
|
|
82
101
|
/**
|
|
83
102
|
* Bind the tree's face to one directory listing.
|
|
84
103
|
* @param list - the bound `workspaceFiles.list` call.
|
|
104
|
+
* @param watch - target-scoped directory observation.
|
|
85
105
|
* @returns the Slot `inject` factory: session and bound actions in, face out.
|
|
86
106
|
*/
|
|
87
|
-
export declare function filesFace(list: ListWorkspaceDirectory): (sessionId: SessionId, actions: BoundActions<ReturnType<typeof createFilesStore>>) => FilesInjected;
|
|
107
|
+
export declare function filesFace(list: ListWorkspaceDirectory, watch: WatchWorkspaceDirectory): (sessionId: SessionId, actions: BoundActions<ReturnType<typeof createFilesStore>>) => FilesInjected;
|
|
88
108
|
//# sourceMappingURL=face.d.ts.map
|
|
@@ -14,6 +14,9 @@ export declare const zh: {
|
|
|
14
14
|
truncated: string;
|
|
15
15
|
noWorkspace: string;
|
|
16
16
|
reload: string;
|
|
17
|
+
autoRefresh: string;
|
|
18
|
+
'autoRefresh.enable': string;
|
|
19
|
+
'autoRefresh.disable': string;
|
|
17
20
|
'entry.other': string;
|
|
18
21
|
'error.notFound': string;
|
|
19
22
|
'error.outsideWorkspace': string;
|
|
@@ -32,6 +35,9 @@ export declare const en: {
|
|
|
32
35
|
truncated: string;
|
|
33
36
|
noWorkspace: string;
|
|
34
37
|
reload: string;
|
|
38
|
+
autoRefresh: string;
|
|
39
|
+
'autoRefresh.enable': string;
|
|
40
|
+
'autoRefresh.disable': string;
|
|
35
41
|
'entry.other': string;
|
|
36
42
|
'error.notFound': string;
|
|
37
43
|
'error.outsideWorkspace': string;
|
|
@@ -32,6 +32,7 @@ export type LevelState = {
|
|
|
32
32
|
} | {
|
|
33
33
|
readonly kind: 'ready';
|
|
34
34
|
readonly level: DirLevel;
|
|
35
|
+
readonly failure?: RemoteFailure;
|
|
35
36
|
} | {
|
|
36
37
|
readonly kind: 'failed';
|
|
37
38
|
readonly failure: RemoteFailure;
|
|
@@ -43,6 +44,7 @@ export type LevelState = {
|
|
|
43
44
|
* the Host reports it, and a child is the parent joined with the entry name.
|
|
44
45
|
*/
|
|
45
46
|
export interface FilesTabState {
|
|
47
|
+
autoRefresh: boolean;
|
|
46
48
|
/** Absolute path of the workspace root this tree is rooted at. */
|
|
47
49
|
root: string;
|
|
48
50
|
/** Level state by absolute directory path; a path absent here was never asked for. */
|
|
@@ -58,6 +60,7 @@ export interface FilesState {
|
|
|
58
60
|
}
|
|
59
61
|
/** The tree store's write set; every action names the tab it writes. */
|
|
60
62
|
type FilesActions = {
|
|
63
|
+
autoRefresh: (draft: FilesState, tabId: TabId, enabled: boolean) => void;
|
|
61
64
|
start: (draft: FilesState, tabId: TabId, root: string) => void;
|
|
62
65
|
loading: (draft: FilesState, tabId: TabId, path: string) => void;
|
|
63
66
|
loaded: (draft: FilesState, tabId: TabId, path: string, level: DirLevel) => void;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@deepseek-ai/dsh-client-ui-sidebar-files",
|
|
3
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.
|
|
4
|
+
"version": "0.1.7-alpha.2",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
7
7
|
},
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
},
|
|
39
39
|
"license": "MIT",
|
|
40
40
|
"peerDependencies": {
|
|
41
|
-
"@deepseek-ai/cordis": "
|
|
41
|
+
"@deepseek-ai/cordis": "~4.0.4"
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
44
|
"@testing-library/react": "^16.1.0",
|
|
@@ -47,20 +47,21 @@
|
|
|
47
47
|
"react": "^18.2.0",
|
|
48
48
|
"clsx": "^2.0.0",
|
|
49
49
|
"react-dom": "^18.2.0",
|
|
50
|
-
"@deepseek-ai/cordis": "
|
|
51
|
-
"@deepseek-ai/dsh-api-
|
|
52
|
-
"@deepseek-ai/dsh-client-
|
|
53
|
-
"@deepseek-ai/dsh-client-
|
|
54
|
-
"@deepseek-ai/dsh-
|
|
55
|
-
"@deepseek-ai/dsh-client-ui-dockkit": "
|
|
56
|
-
"@deepseek-ai/dsh-
|
|
57
|
-
"@deepseek-ai/dsh-client-
|
|
58
|
-
"@deepseek-ai/dsh-client-ui-
|
|
59
|
-
"@deepseek-ai/dsh-client-ui-
|
|
60
|
-
"@deepseek-ai/dsh-client-ui-
|
|
61
|
-
"@deepseek-ai/dsh-
|
|
62
|
-
"@deepseek-ai/dsh-
|
|
63
|
-
"@deepseek-ai/dsh-
|
|
50
|
+
"@deepseek-ai/cordis": "~4.0.4",
|
|
51
|
+
"@deepseek-ai/dsh-api-workspace-files": "0.1.7-alpha.2",
|
|
52
|
+
"@deepseek-ai/dsh-client-locale": "0.1.7-alpha.2",
|
|
53
|
+
"@deepseek-ai/dsh-client-store": "0.1.7-alpha.2",
|
|
54
|
+
"@deepseek-ai/dsh-client-test-runtime": "0.1.7-alpha.2",
|
|
55
|
+
"@deepseek-ai/dsh-client-ui-dockkit": "0.1.7-alpha.2",
|
|
56
|
+
"@deepseek-ai/dsh-api-remotes": "0.1.7-alpha.2",
|
|
57
|
+
"@deepseek-ai/dsh-client-ui-renderer": "0.1.7-alpha.2",
|
|
58
|
+
"@deepseek-ai/dsh-client-ui-session": "0.1.7-alpha.2",
|
|
59
|
+
"@deepseek-ai/dsh-client-ui-sidebar-right": "0.1.7-alpha.2",
|
|
60
|
+
"@deepseek-ai/dsh-client-ui-slots": "0.1.7-alpha.2",
|
|
61
|
+
"@deepseek-ai/dsh-session": "0.1.7-alpha.2",
|
|
62
|
+
"@deepseek-ai/dsh-typert-protocol": "0.1.7-alpha.2",
|
|
63
|
+
"@deepseek-ai/dsh-util-workspace-path": "0.1.7-alpha.2",
|
|
64
|
+
"@deepseek-ai/dsh-client-ui-primitives": "0.1.7-alpha.2"
|
|
64
65
|
},
|
|
65
66
|
"files": [
|
|
66
67
|
"lib/index.js",
|