@deepseek-ai/dsh-api-workspace-controller 0.1.2-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/LICENSE +21 -0
- package/README.i18n.yaml +6 -0
- package/README.md +56 -0
- package/README.zh.md +56 -0
- package/lib/client.js +415 -0
- package/lib/index.js +726 -0
- package/lib/invariant.js +13 -0
- package/lib/typert.host.d.ts +3 -0
- package/lib/typert.host.js +552 -0
- package/lib/typert.remote-client.d.ts +44 -0
- package/lib/typert.remote-client.js +371 -0
- package/lib/types/client/index.d.ts +45 -0
- package/lib/types/client/index.js +70 -0
- package/lib/types/client/model.d.ts +140 -0
- package/lib/types/client/model.js +292 -0
- package/lib/types/client/service.d.ts +88 -0
- package/lib/types/client/service.js +63 -0
- package/lib/types/commands.d.ts +49 -0
- package/lib/types/commands.js +144 -0
- package/lib/types/directory-picker.d.ts +49 -0
- package/lib/types/directory-picker.js +179 -0
- package/lib/types/feed.d.ts +34 -0
- package/lib/types/feed.js +175 -0
- package/lib/types/index.d.ts +64 -0
- package/lib/types/index.js +145 -0
- package/lib/types/invariant.d.ts +9 -0
- package/lib/types/invariant.js +12 -0
- package/lib/types/types.d.ts +132 -0
- package/lib/types/types.js +8 -0
- package/package.json +99 -0
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.
|
package/README.i18n.yaml
ADDED
|
@@ -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/api/workspace-controller/README.md
|
|
5
|
+
README.md: d1ce90d09662f1e39d2da5f7fe1b8276f6ad6150
|
|
6
|
+
README.zh.md: e0b7109275464ddb4c9430e15d52dcb9632314a2
|
package/README.md
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: "Host and Client workspace control: mutate workspace navigation and follow its complete projection."
|
|
3
|
+
kind: "package-reference"
|
|
4
|
+
---
|
|
5
|
+
# Workspace Controller
|
|
6
|
+
|
|
7
|
+
English | [中文](README.zh.md)
|
|
8
|
+
|
|
9
|
+
## Summary
|
|
10
|
+
|
|
11
|
+
`@deepseek-ai/dsh-api-workspace-controller` owns the Host `ctx.workspaceController` service and the generated Client `ctx.remote.workspace` namespace. Its Remote methods create, rename, remove, and reorder Workspaces, reorder Sessions within a Workspace, archive Sessions from Workspace navigation, and follow the complete Workspace projection. Use it through API Gateway when a Client must change or follow Workspace navigation. The package also owns `ctx.directoryPickerController` and the generated `ctx.remote.directoryPicker` namespace, because the directory-picking seam it carries is abstract and never a Loader entry of its own.
|
|
12
|
+
|
|
13
|
+
## Table of Contents
|
|
14
|
+
|
|
15
|
+
- [Use this package](#use-this-package)
|
|
16
|
+
- [Model Experience](#model-experience)
|
|
17
|
+
- [Known Limitations and Deferred Work](#known-limitations-and-deferred-work)
|
|
18
|
+
- [Dev Note](#dev-note)
|
|
19
|
+
|
|
20
|
+
-----
|
|
21
|
+
|
|
22
|
+
<a id="use-this-package"></a>
|
|
23
|
+
## Use this package
|
|
24
|
+
|
|
25
|
+
The Host controller serializes mutations whose correctness depends on current registry state and throws `RemoteError` with a stable `workspace/*` or `directory-picker/*` code for expected failures. Its `follow()` stream synchronously attaches to durable Workspace changes, emits one complete baseline first, then emits ordered `upsert`, `remove`, `order`, and `archived` increments. A reconnect starts another generation with a replacement baseline, so consumers do not depend on receiving every increment while disconnected.
|
|
26
|
+
|
|
27
|
+
The Client entry provides `ClientWorkspaceModel` and `createWorkspaceStateStream()`. The model owns Workspace rows, registry order, archived Session ids, unary mutation echoes, and stream/unary race resolution. A newer Host row wins by `updatedAt`; a committed stream order outranks an older unary response; a removed Workspace id cannot be resurrected by delayed data. The package exposes framework-neutral snapshots and subscriptions, leaving navigation policy and React hooks to the UI owner.
|
|
28
|
+
|
|
29
|
+
-----
|
|
30
|
+
|
|
31
|
+
<a id="model-experience"></a>
|
|
32
|
+
## Model Experience
|
|
33
|
+
|
|
34
|
+
None, as Workspace organization is browser and Host control state and registers no prompt, tool, or session event.
|
|
35
|
+
|
|
36
|
+
#### KV Cache effect
|
|
37
|
+
|
|
38
|
+
No direct effect; Workspace mutations do not alter model requests.
|
|
39
|
+
|
|
40
|
+
## Known Limitations and Deferred Work
|
|
41
|
+
|
|
42
|
+
<a id="known-limitations-and-deferred-work"></a>
|
|
43
|
+
|
|
44
|
+
- `follow()` replaces the whole projection after reconnect and has no durable cursor or incremental catch-up protocol.
|
|
45
|
+
- Process-local deletion markers prevent delayed data from reviving a removed Workspace only for the lifetime of the Client model.
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
<a id="dev-note"></a>
|
|
49
|
+
### Dev Note
|
|
50
|
+
|
|
51
|
+
<details>
|
|
52
|
+
<summary>Working context for maintainers — click to expand</summary>
|
|
53
|
+
|
|
54
|
+
None.
|
|
55
|
+
|
|
56
|
+
</details>
|
package/README.zh.md
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: "Host 与 Client 工作区控制:修改工作区导航并跟随其完整投影。"
|
|
3
|
+
kind: "package-reference"
|
|
4
|
+
---
|
|
5
|
+
# Workspace Controller
|
|
6
|
+
|
|
7
|
+
[English](README.md) | 中文
|
|
8
|
+
|
|
9
|
+
## 概述
|
|
10
|
+
|
|
11
|
+
`@deepseek-ai/dsh-api-workspace-controller` 拥有 Host 的 `ctx.workspaceController` 服务和生成的 Client `ctx.remote.workspace` namespace。它的 Remote 方法负责创建、重命名、移除和重排 Workspace,在 Workspace 内重排 Session,从 Workspace 导航中归档 Session,以及跟随完整的 Workspace 投影。当 Client 必须修改或跟随 Workspace 导航时,请通过 API Gateway 使用它。本包同时拥有 `ctx.directoryPickerController` 与生成的 `ctx.remote.directoryPicker` namespace,因为它承载的选目录 seam 是抽象的,自身从不作为 Loader entry。
|
|
12
|
+
|
|
13
|
+
## 目录
|
|
14
|
+
|
|
15
|
+
- [使用本包](#use-this-package)
|
|
16
|
+
- [模型体验](#model-experience)
|
|
17
|
+
- [已知限制与延期工作](#known-limitations-and-deferred-work)
|
|
18
|
+
- [开发备注](#dev-note)
|
|
19
|
+
|
|
20
|
+
-----
|
|
21
|
+
|
|
22
|
+
<a id="use-this-package"></a>
|
|
23
|
+
## 使用本包
|
|
24
|
+
|
|
25
|
+
Host 控制器会串行执行正确性取决于当前 registry 状态的变更,并为预期失败抛出带稳定 `workspace/*` 或 `directory-picker/*` 码的 `RemoteError`。它的 `follow()` 流会同步订阅持久 Workspace 变更,先发出一份完整 baseline,再按顺序发出 `upsert`、`remove`、`order` 和 `archived` 增量。重连会以替换 baseline 开始新一代,因此消费方不依赖收到断线期间的每个增量。
|
|
26
|
+
|
|
27
|
+
Client 入口提供 `ClientWorkspaceModel` 和 `createWorkspaceStateStream()`。该模型拥有 Workspace 行、registry 顺序、已归档 Session id、一元变更回声,以及流与一元调用的竞态处理。较新的 Host 行按 `updatedAt` 获胜;已提交的流顺序优先于较旧的一元响应;已经移除的 Workspace id 不会被延迟数据复活。该包公开与框架无关的快照和订阅,把导航策略与 React hook 留给 UI owner。
|
|
28
|
+
|
|
29
|
+
-----
|
|
30
|
+
|
|
31
|
+
<a id="model-experience"></a>
|
|
32
|
+
## 模型体验
|
|
33
|
+
|
|
34
|
+
无,因为 Workspace 组织属于浏览器与 Host 控制状态,并且不注册提示词、工具或会话事件。
|
|
35
|
+
|
|
36
|
+
#### KV Cache 影响
|
|
37
|
+
|
|
38
|
+
无直接影响;Workspace 变更不会改变模型请求。
|
|
39
|
+
|
|
40
|
+
## 已知限制与延期工作
|
|
41
|
+
|
|
42
|
+
<a id="known-limitations-and-deferred-work"></a>
|
|
43
|
+
|
|
44
|
+
- `follow()` 在重连后替换完整投影,不提供持久 cursor 或增量追赶协议。
|
|
45
|
+
- 进程本地删除标记只会在 Client 模型生命周期内阻止延迟数据复活已移除的 Workspace。
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
<a id="dev-note"></a>
|
|
49
|
+
### 开发备注
|
|
50
|
+
|
|
51
|
+
<details>
|
|
52
|
+
<summary>维护者工作上下文——点击展开</summary>
|
|
53
|
+
|
|
54
|
+
无。
|
|
55
|
+
|
|
56
|
+
</details>
|
package/lib/client.js
ADDED
|
@@ -0,0 +1,415 @@
|
|
|
1
|
+
window.__ModuleLoader__.load({
|
|
2
|
+
id: "@deepseek-ai/dsh-api-workspace-controller",
|
|
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_api_gateway_client = require("@deepseek-ai/dsh-api-gateway/client");
|
|
8
|
+
let _deepseek_ai_dsh_client_store = require("@deepseek-ai/dsh-client-store");
|
|
9
|
+
let _deepseek_ai_cordis = require("@deepseek-ai/cordis");
|
|
10
|
+
//#region lib/types/client/model.js
|
|
11
|
+
/** Client-side Workspace state model shared by Remote transport and UI projection. */
|
|
12
|
+
/**
|
|
13
|
+
* Owns the Client Workspace projection, mutation echoes, and stream/unary race resolution.
|
|
14
|
+
*/
|
|
15
|
+
var ClientWorkspaceModel = class {
|
|
16
|
+
remote;
|
|
17
|
+
items = [];
|
|
18
|
+
archivedSessionIds = [];
|
|
19
|
+
state = "loading";
|
|
20
|
+
phase = "pending";
|
|
21
|
+
error = null;
|
|
22
|
+
/** Latest local reorder request; only its unary echo may install order. */
|
|
23
|
+
orderRequestGeneration = 0;
|
|
24
|
+
/** Increments on stream orders so a later remote commit outranks an older unary echo. */
|
|
25
|
+
orderFrameGeneration = 0;
|
|
26
|
+
/** Last complete order accepted from a baseline, increment, or current unary echo. */
|
|
27
|
+
committedOrder = [];
|
|
28
|
+
/** Host Workspace ids are never reused, so delayed data cannot resurrect a removed row. */
|
|
29
|
+
removedIds = /* @__PURE__ */ new Set();
|
|
30
|
+
listeners = /* @__PURE__ */ new Set();
|
|
31
|
+
snapshotCache;
|
|
32
|
+
snapshotDirty = false;
|
|
33
|
+
notificationPending = false;
|
|
34
|
+
notificationScheduled = false;
|
|
35
|
+
notificationGeneration = 0;
|
|
36
|
+
/** @param remote - generated Workspace Remote namespace. */
|
|
37
|
+
constructor(remote) {
|
|
38
|
+
this.remote = remote;
|
|
39
|
+
this.snapshotCache = this.buildSnapshot();
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Create or resolve a Workspace and merge the unary result immediately.
|
|
43
|
+
* @param input - existing absolute path to adopt.
|
|
44
|
+
* @returns generated Remote result.
|
|
45
|
+
*/
|
|
46
|
+
async create(input) {
|
|
47
|
+
const result = await this.remote.create(input);
|
|
48
|
+
if (result.ok) this.upsert(result.value.workspace);
|
|
49
|
+
return result;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Rename a Workspace and merge the unary result immediately.
|
|
53
|
+
* @param workspaceId - target Workspace.
|
|
54
|
+
* @param title - new display title.
|
|
55
|
+
* @returns generated Remote result.
|
|
56
|
+
*/
|
|
57
|
+
async rename(workspaceId, title) {
|
|
58
|
+
const result = await this.remote.rename({
|
|
59
|
+
workspaceId,
|
|
60
|
+
title
|
|
61
|
+
});
|
|
62
|
+
if (result.ok) this.upsert(result.value.workspace);
|
|
63
|
+
return result;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Delete a Workspace and remove it from the local projection immediately.
|
|
67
|
+
* @param workspaceId - target Workspace.
|
|
68
|
+
* @returns generated Remote result.
|
|
69
|
+
*/
|
|
70
|
+
async delete(workspaceId) {
|
|
71
|
+
const result = await this.remote.delete({ workspaceId });
|
|
72
|
+
if (result.ok) this.remove(workspaceId, true);
|
|
73
|
+
return result;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Optimistically move a Workspace and reconcile the returned complete order.
|
|
77
|
+
* @param workspaceId - Workspace to move.
|
|
78
|
+
* @param beforeWorkspaceId - anchor Workspace; omitted appends.
|
|
79
|
+
* @returns generated Remote result.
|
|
80
|
+
*/
|
|
81
|
+
async insertBefore(workspaceId, beforeWorkspaceId) {
|
|
82
|
+
const requestGeneration = ++this.orderRequestGeneration;
|
|
83
|
+
const frameGeneration = this.orderFrameGeneration;
|
|
84
|
+
const localOrder = this.items.map((workspace) => workspace.workspaceId);
|
|
85
|
+
this.installOrder(insertIdBefore(localOrder, workspaceId, beforeWorkspaceId));
|
|
86
|
+
const result = await this.remote.insertBefore({
|
|
87
|
+
workspaceId,
|
|
88
|
+
...beforeWorkspaceId === void 0 ? {} : { beforeWorkspaceId }
|
|
89
|
+
});
|
|
90
|
+
if (requestGeneration === this.orderRequestGeneration && frameGeneration === this.orderFrameGeneration) this.installOrder(result.ok ? result.value.workspaceIds : this.committedOrder, result.ok);
|
|
91
|
+
return result;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Move a Session within its Workspace and merge the returned row.
|
|
95
|
+
* @param workspaceId - owning Workspace.
|
|
96
|
+
* @param sessionId - accounted Session to move.
|
|
97
|
+
* @param beforeSessionId - accounted anchor; omitted appends.
|
|
98
|
+
* @returns generated Remote result.
|
|
99
|
+
*/
|
|
100
|
+
async insertSessionBefore(workspaceId, sessionId, beforeSessionId) {
|
|
101
|
+
const result = await this.remote.insertSessionBefore({
|
|
102
|
+
workspaceId,
|
|
103
|
+
sessionId,
|
|
104
|
+
...beforeSessionId === void 0 ? {} : { beforeSessionId }
|
|
105
|
+
});
|
|
106
|
+
if (result.ok) this.upsert(result.value.workspace);
|
|
107
|
+
return result;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Archive one Session and install the returned complete archive set.
|
|
111
|
+
* @param sessionId - Session to archive.
|
|
112
|
+
* @returns generated Remote result.
|
|
113
|
+
*/
|
|
114
|
+
async archiveSession(sessionId) {
|
|
115
|
+
const result = await this.remote.archiveSession({ sessionId });
|
|
116
|
+
if (result.ok) this.installArchived(result.value.archivedSessionIds);
|
|
117
|
+
return result;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Replace the projection from one complete stream-generation baseline.
|
|
121
|
+
* @param baseline - complete Workspace and archive projection.
|
|
122
|
+
*/
|
|
123
|
+
replaceBaseline(baseline) {
|
|
124
|
+
this.orderFrameGeneration++;
|
|
125
|
+
this.installViews(baseline.items);
|
|
126
|
+
this.installArchived(baseline.archivedSessionIds);
|
|
127
|
+
this.state = "idle";
|
|
128
|
+
this.phase = "ready";
|
|
129
|
+
this.error = null;
|
|
130
|
+
this.invalidate();
|
|
131
|
+
}
|
|
132
|
+
/** Merge one decoded Workspace upsert from the current follow generation. */
|
|
133
|
+
upsertView(workspace) {
|
|
134
|
+
this.upsert(workspace);
|
|
135
|
+
}
|
|
136
|
+
/** Apply one decoded Workspace removal from the current follow generation. */
|
|
137
|
+
removeView(workspaceId) {
|
|
138
|
+
this.remove(workspaceId);
|
|
139
|
+
}
|
|
140
|
+
/** Replace Host-confirmed order from the current follow generation. */
|
|
141
|
+
replaceOrder(workspaceIds) {
|
|
142
|
+
this.orderFrameGeneration++;
|
|
143
|
+
this.installOrder(workspaceIds, true);
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Replace the archived Session set from the current follow generation.
|
|
147
|
+
* @param archivedSessionIds - complete Host-confirmed archive set.
|
|
148
|
+
*/
|
|
149
|
+
replaceArchived(archivedSessionIds) {
|
|
150
|
+
this.installArchived(archivedSessionIds);
|
|
151
|
+
}
|
|
152
|
+
/** Keep the last complete projection visible while a lost carrier reconnects. */
|
|
153
|
+
handleCarrierFailure() {
|
|
154
|
+
this.state = "loading";
|
|
155
|
+
this.error = null;
|
|
156
|
+
this.invalidate();
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Publish a non-retryable stream or protocol failure.
|
|
160
|
+
* @param error - terminal stream failure.
|
|
161
|
+
*/
|
|
162
|
+
handleStreamFailure(error) {
|
|
163
|
+
if (!(0, _deepseek_ai_dsh_api_gateway_client.isRemoteFailure)(error)) throw error;
|
|
164
|
+
this.state = "error";
|
|
165
|
+
this.error = error;
|
|
166
|
+
this.invalidate();
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Subscribe to Workspace state invalidation.
|
|
170
|
+
* @param listener - invalidation callback.
|
|
171
|
+
* @returns unsubscribe function.
|
|
172
|
+
*/
|
|
173
|
+
subscribe(listener) {
|
|
174
|
+
this.listeners.add(listener);
|
|
175
|
+
return () => {
|
|
176
|
+
this.listeners.delete(listener);
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Read the cached state, rebuilding it first when necessary.
|
|
181
|
+
* @returns the current stable Workspace list snapshot.
|
|
182
|
+
*/
|
|
183
|
+
getSnapshot() {
|
|
184
|
+
this.refreshSnapshot();
|
|
185
|
+
return this.snapshotCache;
|
|
186
|
+
}
|
|
187
|
+
buildSnapshot() {
|
|
188
|
+
return {
|
|
189
|
+
items: this.items,
|
|
190
|
+
archivedSessionIds: this.archivedSessionIds,
|
|
191
|
+
state: this.state,
|
|
192
|
+
phase: this.phase,
|
|
193
|
+
error: this.error
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
installArchived(archivedSessionIds) {
|
|
197
|
+
if (archivedSessionIds.length === this.archivedSessionIds.length && archivedSessionIds.every((id, index) => id === this.archivedSessionIds[index])) return;
|
|
198
|
+
this.archivedSessionIds = [...archivedSessionIds];
|
|
199
|
+
this.invalidate();
|
|
200
|
+
}
|
|
201
|
+
installOrder(workspaceIds, committed = false) {
|
|
202
|
+
if (committed) this.committedOrder = [...workspaceIds];
|
|
203
|
+
const rank = new Map(workspaceIds.map((id, index) => [id, index]));
|
|
204
|
+
const items = [...this.items].sort((left, right) => (rank.get(left.workspaceId) ?? Number.MAX_SAFE_INTEGER) - (rank.get(right.workspaceId) ?? Number.MAX_SAFE_INTEGER));
|
|
205
|
+
if (items.every((item, index) => item === this.items[index])) return;
|
|
206
|
+
this.items = items;
|
|
207
|
+
this.invalidate();
|
|
208
|
+
}
|
|
209
|
+
upsert(view) {
|
|
210
|
+
if (this.removedIds.has(view.workspaceId)) return;
|
|
211
|
+
const index = this.items.findIndex((item) => item.workspaceId === view.workspaceId);
|
|
212
|
+
const installed = this.items[index];
|
|
213
|
+
if (installed !== void 0 && Date.parse(view.updatedAt) < Date.parse(installed.updatedAt)) return;
|
|
214
|
+
if (!this.committedOrder.includes(view.workspaceId)) this.committedOrder = [view.workspaceId, ...this.committedOrder];
|
|
215
|
+
this.items = index === -1 ? [view, ...this.items] : this.items.map((item, position) => position === index ? view : item);
|
|
216
|
+
this.invalidate();
|
|
217
|
+
}
|
|
218
|
+
remove(workspaceId, immediate = false) {
|
|
219
|
+
this.removedIds.add(workspaceId);
|
|
220
|
+
this.committedOrder = this.committedOrder.filter((id) => id !== workspaceId);
|
|
221
|
+
const items = this.items.filter((item) => item.workspaceId !== workspaceId);
|
|
222
|
+
if (items.length === this.items.length) {
|
|
223
|
+
if (immediate) this.invalidate(true);
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
this.items = items;
|
|
227
|
+
this.invalidate(immediate);
|
|
228
|
+
}
|
|
229
|
+
installViews(views) {
|
|
230
|
+
const installed = /* @__PURE__ */ new Map();
|
|
231
|
+
for (const view of views) if (!this.removedIds.has(view.workspaceId)) installed.set(view.workspaceId, view);
|
|
232
|
+
this.items = [...installed.values()];
|
|
233
|
+
this.committedOrder = views.map((view) => view.workspaceId);
|
|
234
|
+
}
|
|
235
|
+
invalidate(immediate = false) {
|
|
236
|
+
this.snapshotDirty = true;
|
|
237
|
+
this.notificationPending = true;
|
|
238
|
+
if (immediate) {
|
|
239
|
+
this.notificationGeneration++;
|
|
240
|
+
this.notificationScheduled = false;
|
|
241
|
+
this.flush();
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
if (this.notificationScheduled) return;
|
|
245
|
+
this.notificationScheduled = true;
|
|
246
|
+
const generation = ++this.notificationGeneration;
|
|
247
|
+
queueMicrotask(() => {
|
|
248
|
+
if (generation !== this.notificationGeneration) return;
|
|
249
|
+
this.notificationScheduled = false;
|
|
250
|
+
this.flush();
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
flush() {
|
|
254
|
+
if (!this.notificationPending || this.listeners.size === 0) return;
|
|
255
|
+
this.notificationPending = false;
|
|
256
|
+
this.refreshSnapshot();
|
|
257
|
+
(0, _deepseek_ai_dsh_client_store.notifySubscribers)(this.listeners, "[workspace-controller]");
|
|
258
|
+
}
|
|
259
|
+
refreshSnapshot() {
|
|
260
|
+
if (!this.snapshotDirty) return;
|
|
261
|
+
this.snapshotDirty = false;
|
|
262
|
+
this.snapshotCache = this.buildSnapshot();
|
|
263
|
+
}
|
|
264
|
+
};
|
|
265
|
+
function insertIdBefore(ids, id, beforeId) {
|
|
266
|
+
if (!ids.includes(id) || beforeId !== void 0 && !ids.includes(beforeId) || beforeId === id) return [...ids];
|
|
267
|
+
const without = ids.filter((candidate) => candidate !== id);
|
|
268
|
+
const at = beforeId === void 0 ? without.length : without.indexOf(beforeId);
|
|
269
|
+
return [
|
|
270
|
+
...without.slice(0, at),
|
|
271
|
+
id,
|
|
272
|
+
...without.slice(at)
|
|
273
|
+
];
|
|
274
|
+
}
|
|
275
|
+
//#endregion
|
|
276
|
+
//#region lib/types/client/service.js
|
|
277
|
+
/** React-free Client Workspace service and command facade. */
|
|
278
|
+
/** Structured create failure for callers that distinguish Host business errors. */
|
|
279
|
+
var WorkspaceCreateError = class extends Error {
|
|
280
|
+
rpcError;
|
|
281
|
+
name = "WorkspaceCreateError";
|
|
282
|
+
/** @param rpcError - Host business or folded carrier failure. */
|
|
283
|
+
constructor(rpcError) {
|
|
284
|
+
super(`workspace create failed: ${rpcError.code}: ${rpcError.message}`);
|
|
285
|
+
this.rpcError = rpcError;
|
|
286
|
+
}
|
|
287
|
+
};
|
|
288
|
+
/** Owns the bare Workspace snapshot and Workspace-only commands. */
|
|
289
|
+
var WorkspaceController = class extends _deepseek_ai_cordis.Service {
|
|
290
|
+
model;
|
|
291
|
+
list;
|
|
292
|
+
/**
|
|
293
|
+
* @param ctx - Client root Context.
|
|
294
|
+
* @param model - Remote-backed Workspace state model.
|
|
295
|
+
*/
|
|
296
|
+
constructor(ctx, model) {
|
|
297
|
+
super(ctx, "workspaces");
|
|
298
|
+
this.model = model;
|
|
299
|
+
this.list = model;
|
|
300
|
+
}
|
|
301
|
+
async create(input) {
|
|
302
|
+
const result = await this.model.create(input);
|
|
303
|
+
if (!result.ok) throw new WorkspaceCreateError(result.error);
|
|
304
|
+
return result.value.workspace;
|
|
305
|
+
}
|
|
306
|
+
async rename(workspaceId, title) {
|
|
307
|
+
const result = await this.model.rename(workspaceId, title);
|
|
308
|
+
if (!result.ok) throw commandError("rename", result.error);
|
|
309
|
+
return result.value.workspace;
|
|
310
|
+
}
|
|
311
|
+
async delete(workspaceId) {
|
|
312
|
+
const result = await this.model.delete(workspaceId);
|
|
313
|
+
if (!result.ok) throw commandError("delete", result.error);
|
|
314
|
+
}
|
|
315
|
+
async insertBefore(workspaceId, beforeWorkspaceId) {
|
|
316
|
+
const result = await this.model.insertBefore(workspaceId, beforeWorkspaceId);
|
|
317
|
+
if (!result.ok) throw commandError("reorder", result.error);
|
|
318
|
+
}
|
|
319
|
+
async archiveSession(sessionId) {
|
|
320
|
+
const result = await this.model.archiveSession(sessionId);
|
|
321
|
+
if (!result.ok) throw commandError("session archive", result.error);
|
|
322
|
+
}
|
|
323
|
+
async insertSessionBefore(workspaceId, sessionId, beforeSessionId) {
|
|
324
|
+
const result = await this.model.insertSessionBefore(workspaceId, sessionId, beforeSessionId);
|
|
325
|
+
if (!result.ok) throw commandError("move", result.error);
|
|
326
|
+
return result.value.workspace;
|
|
327
|
+
}
|
|
328
|
+
};
|
|
329
|
+
function commandError(operation, failure) {
|
|
330
|
+
return /* @__PURE__ */ new Error(`workspace ${operation} failed: ${failure.code}: ${failure.message}`);
|
|
331
|
+
}
|
|
332
|
+
//#endregion
|
|
333
|
+
//#region lib/types/client/index.js
|
|
334
|
+
/** Workspace-specific adapter for the Gateway-owned snapshot stream lifecycle. */
|
|
335
|
+
/** Required Client Remote services. */
|
|
336
|
+
const inject = ["remote", "remote.workspace"];
|
|
337
|
+
/**
|
|
338
|
+
* Install Client Workspace state, commands, and reconnecting follow control.
|
|
339
|
+
* @param ctx - Client root Context.
|
|
340
|
+
*/
|
|
341
|
+
function apply(ctx) {
|
|
342
|
+
const model = new ClientWorkspaceModel(ctx.remote.workspace);
|
|
343
|
+
new WorkspaceController(ctx, model);
|
|
344
|
+
const control = createWorkspaceStateStream(ctx.remote, {
|
|
345
|
+
accept: model,
|
|
346
|
+
carrierFailed: () => {
|
|
347
|
+
model.handleCarrierFailure();
|
|
348
|
+
},
|
|
349
|
+
failed: (error) => {
|
|
350
|
+
model.handleStreamFailure(error);
|
|
351
|
+
}
|
|
352
|
+
});
|
|
353
|
+
control.start();
|
|
354
|
+
ctx.effect(() => async () => {
|
|
355
|
+
await control.dispose();
|
|
356
|
+
}, "workspace-controller.client.control");
|
|
357
|
+
}
|
|
358
|
+
/**
|
|
359
|
+
* Create the reconnecting Workspace state stream.
|
|
360
|
+
* @param remote - Client Remote face carrying the Workspace namespace and the stream factory.
|
|
361
|
+
* @param options - Workspace state destinations.
|
|
362
|
+
* @returns an unstarted stream owned by the Client Workspace runtime.
|
|
363
|
+
*/
|
|
364
|
+
function createWorkspaceStateStream(remote, options) {
|
|
365
|
+
return new _deepseek_ai_dsh_api_gateway_client.RemoteSnapshotStream(remote.$stream({
|
|
366
|
+
name: "Workspace state stream",
|
|
367
|
+
open: (signal) => remote.workspace.follow(signal),
|
|
368
|
+
ended: (accepted) => accepted ? new _deepseek_ai_dsh_api_gateway_client.RemoteStreamCarrierError("Workspace state stream ended without a terminal result") : /* @__PURE__ */ new Error("Workspace state stream ended before its opening snapshot"),
|
|
369
|
+
...options.carrierFailed === void 0 ? {} : { carrierFailed: options.carrierFailed }
|
|
370
|
+
}), {
|
|
371
|
+
name: "Workspace state stream",
|
|
372
|
+
isSnapshot: (frame) => frame.type === "baseline",
|
|
373
|
+
replace: (frame) => {
|
|
374
|
+
options.accept.replaceBaseline(frame.value);
|
|
375
|
+
},
|
|
376
|
+
update: (frame) => {
|
|
377
|
+
acceptIncrement(options.accept, frame);
|
|
378
|
+
},
|
|
379
|
+
failed: options.failed
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
function acceptIncrement(accept, frame) {
|
|
383
|
+
switch (frame.type) {
|
|
384
|
+
case "upsert":
|
|
385
|
+
accept.upsertView(frame.workspace);
|
|
386
|
+
return;
|
|
387
|
+
case "remove":
|
|
388
|
+
accept.removeView(frame.workspaceId);
|
|
389
|
+
return;
|
|
390
|
+
case "order":
|
|
391
|
+
accept.replaceOrder(frame.workspaceIds);
|
|
392
|
+
return;
|
|
393
|
+
case "archived":
|
|
394
|
+
accept.replaceArchived(frame.archivedSessionIds);
|
|
395
|
+
return;
|
|
396
|
+
/* v8 ignore next -- the generated Remote codec validates this closed union */
|
|
397
|
+
default: return assertNever(frame);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
/* v8 ignore next 3 -- closed-union backstop after generated Remote validation */
|
|
401
|
+
function assertNever(value) {
|
|
402
|
+
throw new Error(`unreachable Workspace increment: ${JSON.stringify(value)}`);
|
|
403
|
+
}
|
|
404
|
+
//#endregion
|
|
405
|
+
exports.ClientWorkspaceModel = ClientWorkspaceModel;
|
|
406
|
+
exports.WorkspaceController = WorkspaceController;
|
|
407
|
+
exports.WorkspaceCreateError = WorkspaceCreateError;
|
|
408
|
+
exports.apply = apply;
|
|
409
|
+
exports.createWorkspaceStateStream = createWorkspaceStateStream;
|
|
410
|
+
exports.inject = inject;
|
|
411
|
+
return module.exports;
|
|
412
|
+
}
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
//# sourceMappingURL=client.js.map
|