@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
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
/** Client-side Workspace state model shared by Remote transport and UI projection. */
|
|
2
|
+
import { notifySubscribers } from '@deepseek-ai/dsh-client-store';
|
|
3
|
+
import { isRemoteFailure } from '@deepseek-ai/dsh-api-gateway/client';
|
|
4
|
+
/**
|
|
5
|
+
* Owns the Client Workspace projection, mutation echoes, and stream/unary race resolution.
|
|
6
|
+
*/
|
|
7
|
+
export class ClientWorkspaceModel {
|
|
8
|
+
remote;
|
|
9
|
+
items = [];
|
|
10
|
+
archivedSessionIds = [];
|
|
11
|
+
state = 'loading';
|
|
12
|
+
phase = 'pending';
|
|
13
|
+
error = null;
|
|
14
|
+
/** Latest local reorder request; only its unary echo may install order. */
|
|
15
|
+
orderRequestGeneration = 0;
|
|
16
|
+
/** Increments on stream orders so a later remote commit outranks an older unary echo. */
|
|
17
|
+
orderFrameGeneration = 0;
|
|
18
|
+
/** Last complete order accepted from a baseline, increment, or current unary echo. */
|
|
19
|
+
committedOrder = [];
|
|
20
|
+
/** Host Workspace ids are never reused, so delayed data cannot resurrect a removed row. */
|
|
21
|
+
removedIds = new Set();
|
|
22
|
+
listeners = new Set();
|
|
23
|
+
snapshotCache;
|
|
24
|
+
snapshotDirty = false;
|
|
25
|
+
notificationPending = false;
|
|
26
|
+
notificationScheduled = false;
|
|
27
|
+
notificationGeneration = 0;
|
|
28
|
+
/** @param remote - generated Workspace Remote namespace. */
|
|
29
|
+
constructor(remote) {
|
|
30
|
+
this.remote = remote;
|
|
31
|
+
this.snapshotCache = this.buildSnapshot();
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Create or resolve a Workspace and merge the unary result immediately.
|
|
35
|
+
* @param input - existing absolute path to adopt.
|
|
36
|
+
* @returns generated Remote result.
|
|
37
|
+
*/
|
|
38
|
+
async create(input) {
|
|
39
|
+
const result = await this.remote.create(input);
|
|
40
|
+
if (result.ok)
|
|
41
|
+
this.upsert(result.value.workspace);
|
|
42
|
+
return result;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Rename a Workspace and merge the unary result immediately.
|
|
46
|
+
* @param workspaceId - target Workspace.
|
|
47
|
+
* @param title - new display title.
|
|
48
|
+
* @returns generated Remote result.
|
|
49
|
+
*/
|
|
50
|
+
async rename(workspaceId, title) {
|
|
51
|
+
const result = await this.remote.rename({ workspaceId, title });
|
|
52
|
+
if (result.ok)
|
|
53
|
+
this.upsert(result.value.workspace);
|
|
54
|
+
return result;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Delete a Workspace and remove it from the local projection immediately.
|
|
58
|
+
* @param workspaceId - target Workspace.
|
|
59
|
+
* @returns generated Remote result.
|
|
60
|
+
*/
|
|
61
|
+
async delete(workspaceId) {
|
|
62
|
+
const result = await this.remote.delete({ workspaceId });
|
|
63
|
+
if (result.ok)
|
|
64
|
+
this.remove(workspaceId, true);
|
|
65
|
+
return result;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Optimistically move a Workspace and reconcile the returned complete order.
|
|
69
|
+
* @param workspaceId - Workspace to move.
|
|
70
|
+
* @param beforeWorkspaceId - anchor Workspace; omitted appends.
|
|
71
|
+
* @returns generated Remote result.
|
|
72
|
+
*/
|
|
73
|
+
async insertBefore(workspaceId, beforeWorkspaceId) {
|
|
74
|
+
const requestGeneration = ++this.orderRequestGeneration;
|
|
75
|
+
const frameGeneration = this.orderFrameGeneration;
|
|
76
|
+
const localOrder = this.items.map(workspace => workspace.workspaceId);
|
|
77
|
+
this.installOrder(insertIdBefore(localOrder, workspaceId, beforeWorkspaceId));
|
|
78
|
+
const result = await this.remote.insertBefore({
|
|
79
|
+
workspaceId,
|
|
80
|
+
...beforeWorkspaceId === undefined ? {} : { beforeWorkspaceId },
|
|
81
|
+
});
|
|
82
|
+
if (requestGeneration === this.orderRequestGeneration
|
|
83
|
+
&& frameGeneration === this.orderFrameGeneration) {
|
|
84
|
+
this.installOrder(result.ok ? result.value.workspaceIds : this.committedOrder, result.ok);
|
|
85
|
+
}
|
|
86
|
+
return result;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Move a Session within its Workspace and merge the returned row.
|
|
90
|
+
* @param workspaceId - owning Workspace.
|
|
91
|
+
* @param sessionId - accounted Session to move.
|
|
92
|
+
* @param beforeSessionId - accounted anchor; omitted appends.
|
|
93
|
+
* @returns generated Remote result.
|
|
94
|
+
*/
|
|
95
|
+
async insertSessionBefore(workspaceId, sessionId, beforeSessionId) {
|
|
96
|
+
const result = await this.remote.insertSessionBefore({
|
|
97
|
+
workspaceId,
|
|
98
|
+
sessionId,
|
|
99
|
+
...beforeSessionId === undefined ? {} : { beforeSessionId },
|
|
100
|
+
});
|
|
101
|
+
if (result.ok)
|
|
102
|
+
this.upsert(result.value.workspace);
|
|
103
|
+
return result;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Archive one Session and install the returned complete archive set.
|
|
107
|
+
* @param sessionId - Session to archive.
|
|
108
|
+
* @returns generated Remote result.
|
|
109
|
+
*/
|
|
110
|
+
async archiveSession(sessionId) {
|
|
111
|
+
const result = await this.remote.archiveSession({ sessionId });
|
|
112
|
+
if (result.ok)
|
|
113
|
+
this.installArchived(result.value.archivedSessionIds);
|
|
114
|
+
return result;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Replace the projection from one complete stream-generation baseline.
|
|
118
|
+
* @param baseline - complete Workspace and archive projection.
|
|
119
|
+
*/
|
|
120
|
+
replaceBaseline(baseline) {
|
|
121
|
+
this.orderFrameGeneration++;
|
|
122
|
+
this.installViews(baseline.items);
|
|
123
|
+
this.installArchived(baseline.archivedSessionIds);
|
|
124
|
+
this.state = 'idle';
|
|
125
|
+
this.phase = 'ready';
|
|
126
|
+
this.error = null;
|
|
127
|
+
this.invalidate();
|
|
128
|
+
}
|
|
129
|
+
/** Merge one decoded Workspace upsert from the current follow generation. */
|
|
130
|
+
upsertView(workspace) {
|
|
131
|
+
this.upsert(workspace);
|
|
132
|
+
}
|
|
133
|
+
/** Apply one decoded Workspace removal from the current follow generation. */
|
|
134
|
+
removeView(workspaceId) {
|
|
135
|
+
this.remove(workspaceId);
|
|
136
|
+
}
|
|
137
|
+
/** Replace Host-confirmed order from the current follow generation. */
|
|
138
|
+
replaceOrder(workspaceIds) {
|
|
139
|
+
this.orderFrameGeneration++;
|
|
140
|
+
this.installOrder(workspaceIds, true);
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Replace the archived Session set from the current follow generation.
|
|
144
|
+
* @param archivedSessionIds - complete Host-confirmed archive set.
|
|
145
|
+
*/
|
|
146
|
+
replaceArchived(archivedSessionIds) {
|
|
147
|
+
this.installArchived(archivedSessionIds);
|
|
148
|
+
}
|
|
149
|
+
/** Keep the last complete projection visible while a lost carrier reconnects. */
|
|
150
|
+
handleCarrierFailure() {
|
|
151
|
+
this.state = 'loading';
|
|
152
|
+
this.error = null;
|
|
153
|
+
this.invalidate();
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Publish a non-retryable stream or protocol failure.
|
|
157
|
+
* @param error - terminal stream failure.
|
|
158
|
+
*/
|
|
159
|
+
handleStreamFailure(error) {
|
|
160
|
+
if (!isRemoteFailure(error))
|
|
161
|
+
throw error;
|
|
162
|
+
this.state = 'error';
|
|
163
|
+
this.error = error;
|
|
164
|
+
this.invalidate();
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Subscribe to Workspace state invalidation.
|
|
168
|
+
* @param listener - invalidation callback.
|
|
169
|
+
* @returns unsubscribe function.
|
|
170
|
+
*/
|
|
171
|
+
subscribe(listener) {
|
|
172
|
+
this.listeners.add(listener);
|
|
173
|
+
return () => { this.listeners.delete(listener); };
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Read the cached state, rebuilding it first when necessary.
|
|
177
|
+
* @returns the current stable Workspace list snapshot.
|
|
178
|
+
*/
|
|
179
|
+
getSnapshot() {
|
|
180
|
+
this.refreshSnapshot();
|
|
181
|
+
return this.snapshotCache;
|
|
182
|
+
}
|
|
183
|
+
buildSnapshot() {
|
|
184
|
+
return {
|
|
185
|
+
items: this.items,
|
|
186
|
+
archivedSessionIds: this.archivedSessionIds,
|
|
187
|
+
state: this.state,
|
|
188
|
+
phase: this.phase,
|
|
189
|
+
error: this.error,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
installArchived(archivedSessionIds) {
|
|
193
|
+
if (archivedSessionIds.length === this.archivedSessionIds.length
|
|
194
|
+
&& archivedSessionIds.every((id, index) => id === this.archivedSessionIds[index]))
|
|
195
|
+
return;
|
|
196
|
+
this.archivedSessionIds = [...archivedSessionIds];
|
|
197
|
+
this.invalidate();
|
|
198
|
+
}
|
|
199
|
+
installOrder(workspaceIds, committed = false) {
|
|
200
|
+
if (committed)
|
|
201
|
+
this.committedOrder = [...workspaceIds];
|
|
202
|
+
const rank = new Map(workspaceIds.map((id, index) => [id, index]));
|
|
203
|
+
const items = [...this.items].sort((left, right) => (rank.get(left.workspaceId) ?? Number.MAX_SAFE_INTEGER)
|
|
204
|
+
- (rank.get(right.workspaceId) ?? Number.MAX_SAFE_INTEGER));
|
|
205
|
+
if (items.every((item, index) => item === this.items[index]))
|
|
206
|
+
return;
|
|
207
|
+
this.items = items;
|
|
208
|
+
this.invalidate();
|
|
209
|
+
}
|
|
210
|
+
upsert(view) {
|
|
211
|
+
if (this.removedIds.has(view.workspaceId))
|
|
212
|
+
return;
|
|
213
|
+
const index = this.items.findIndex(item => item.workspaceId === view.workspaceId);
|
|
214
|
+
const installed = this.items[index];
|
|
215
|
+
// Unary responses and stream increments race on separate requests. Keep
|
|
216
|
+
// the newest Host projection regardless of their arrival order.
|
|
217
|
+
if (installed !== undefined && Date.parse(view.updatedAt) < Date.parse(installed.updatedAt))
|
|
218
|
+
return;
|
|
219
|
+
if (!this.committedOrder.includes(view.workspaceId)) {
|
|
220
|
+
this.committedOrder = [view.workspaceId, ...this.committedOrder];
|
|
221
|
+
}
|
|
222
|
+
this.items = index === -1
|
|
223
|
+
? [view, ...this.items]
|
|
224
|
+
: this.items.map((item, position) => position === index ? view : item);
|
|
225
|
+
this.invalidate();
|
|
226
|
+
}
|
|
227
|
+
remove(workspaceId, immediate = false) {
|
|
228
|
+
this.removedIds.add(workspaceId);
|
|
229
|
+
this.committedOrder = this.committedOrder.filter(id => id !== workspaceId);
|
|
230
|
+
const items = this.items.filter(item => item.workspaceId !== workspaceId);
|
|
231
|
+
if (items.length === this.items.length) {
|
|
232
|
+
// A successful unary echo still publishes an earlier increment's
|
|
233
|
+
// pending removal before the user operation resolves.
|
|
234
|
+
if (immediate)
|
|
235
|
+
this.invalidate(true);
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
this.items = items;
|
|
239
|
+
this.invalidate(immediate);
|
|
240
|
+
}
|
|
241
|
+
installViews(views) {
|
|
242
|
+
const installed = new Map();
|
|
243
|
+
for (const view of views) {
|
|
244
|
+
if (!this.removedIds.has(view.workspaceId))
|
|
245
|
+
installed.set(view.workspaceId, view);
|
|
246
|
+
}
|
|
247
|
+
this.items = [...installed.values()];
|
|
248
|
+
this.committedOrder = views.map(view => view.workspaceId);
|
|
249
|
+
}
|
|
250
|
+
invalidate(immediate = false) {
|
|
251
|
+
this.snapshotDirty = true;
|
|
252
|
+
this.notificationPending = true;
|
|
253
|
+
if (immediate) {
|
|
254
|
+
this.notificationGeneration++;
|
|
255
|
+
this.notificationScheduled = false;
|
|
256
|
+
this.flush();
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
if (this.notificationScheduled)
|
|
260
|
+
return;
|
|
261
|
+
this.notificationScheduled = true;
|
|
262
|
+
const generation = ++this.notificationGeneration;
|
|
263
|
+
queueMicrotask(() => {
|
|
264
|
+
if (generation !== this.notificationGeneration)
|
|
265
|
+
return;
|
|
266
|
+
this.notificationScheduled = false;
|
|
267
|
+
this.flush();
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
flush() {
|
|
271
|
+
if (!this.notificationPending || this.listeners.size === 0)
|
|
272
|
+
return;
|
|
273
|
+
this.notificationPending = false;
|
|
274
|
+
this.refreshSnapshot();
|
|
275
|
+
notifySubscribers(this.listeners, '[workspace-controller]');
|
|
276
|
+
}
|
|
277
|
+
refreshSnapshot() {
|
|
278
|
+
if (!this.snapshotDirty)
|
|
279
|
+
return;
|
|
280
|
+
this.snapshotDirty = false;
|
|
281
|
+
this.snapshotCache = this.buildSnapshot();
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
function insertIdBefore(ids, id, beforeId) {
|
|
285
|
+
if (!ids.includes(id) || (beforeId !== undefined && !ids.includes(beforeId)) || beforeId === id) {
|
|
286
|
+
return [...ids];
|
|
287
|
+
}
|
|
288
|
+
const without = ids.filter(candidate => candidate !== id);
|
|
289
|
+
const at = beforeId === undefined ? without.length : without.indexOf(beforeId);
|
|
290
|
+
return [...without.slice(0, at), id, ...without.slice(at)];
|
|
291
|
+
}
|
|
292
|
+
//# sourceMappingURL=model.js.map
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/** React-free Client Workspace service and command facade. */
|
|
2
|
+
import { Service, type Context } from '@deepseek-ai/cordis';
|
|
3
|
+
import type { SessionId } from '@deepseek-ai/dsh-session/types';
|
|
4
|
+
import type { RemoteFailure } from '@deepseek-ai/dsh-typert-protocol';
|
|
5
|
+
import type { WorkspaceId } from '@deepseek-ai/dsh-workspace/types';
|
|
6
|
+
import type { WorkspaceView } from '../types.ts';
|
|
7
|
+
import type { ClientWorkspaceModel, WorkspaceSnapshot } from './model.ts';
|
|
8
|
+
/** Structured create failure for callers that distinguish Host business errors. */
|
|
9
|
+
export declare class WorkspaceCreateError extends Error {
|
|
10
|
+
readonly rpcError: RemoteFailure;
|
|
11
|
+
readonly name = "WorkspaceCreateError";
|
|
12
|
+
/** @param rpcError - Host business or folded carrier failure. */
|
|
13
|
+
constructor(rpcError: RemoteFailure);
|
|
14
|
+
}
|
|
15
|
+
/** Bare observable source for the Workspace Controller snapshot. */
|
|
16
|
+
export interface WorkspaceSource {
|
|
17
|
+
/** Read the identity-stable current snapshot. */
|
|
18
|
+
getSnapshot(): WorkspaceSnapshot;
|
|
19
|
+
/**
|
|
20
|
+
* Subscribe to snapshot changes.
|
|
21
|
+
* @param listener - invalidation callback.
|
|
22
|
+
* @returns unsubscribe function.
|
|
23
|
+
*/
|
|
24
|
+
subscribe(listener: () => void): () => void;
|
|
25
|
+
}
|
|
26
|
+
/** Workspace Controller's Client service face. */
|
|
27
|
+
export interface IWorkspaces {
|
|
28
|
+
/** Host-authoritative Workspace rows, order, archive set, and follow lifecycle. */
|
|
29
|
+
readonly list: WorkspaceSource;
|
|
30
|
+
/**
|
|
31
|
+
* Register an existing path as a Workspace.
|
|
32
|
+
* @param input - Host create payload.
|
|
33
|
+
* @returns the created or idempotently resolved Workspace.
|
|
34
|
+
*/
|
|
35
|
+
create(input: {
|
|
36
|
+
path: string;
|
|
37
|
+
}): Promise<WorkspaceView>;
|
|
38
|
+
/**
|
|
39
|
+
* Rename a Workspace.
|
|
40
|
+
* @param workspaceId - target Workspace.
|
|
41
|
+
* @param title - new display title.
|
|
42
|
+
* @returns the renamed Workspace.
|
|
43
|
+
*/
|
|
44
|
+
rename(workspaceId: WorkspaceId, title: string): Promise<WorkspaceView>;
|
|
45
|
+
/**
|
|
46
|
+
* Delete a Workspace registration without deleting Sessions or files.
|
|
47
|
+
* @param workspaceId - target Workspace.
|
|
48
|
+
*/
|
|
49
|
+
delete(workspaceId: WorkspaceId): Promise<void>;
|
|
50
|
+
/**
|
|
51
|
+
* Move a Workspace within the Host registry order.
|
|
52
|
+
* @param workspaceId - Workspace to move.
|
|
53
|
+
* @param beforeWorkspaceId - anchor Workspace; omitted appends.
|
|
54
|
+
*/
|
|
55
|
+
insertBefore(workspaceId: WorkspaceId, beforeWorkspaceId?: WorkspaceId): Promise<void>;
|
|
56
|
+
/**
|
|
57
|
+
* Archive a Session from Workspace grouping surfaces.
|
|
58
|
+
* @param sessionId - Session to archive.
|
|
59
|
+
*/
|
|
60
|
+
archiveSession(sessionId: SessionId): Promise<void>;
|
|
61
|
+
/**
|
|
62
|
+
* Move a Session within one Workspace account.
|
|
63
|
+
* @param workspaceId - owning Workspace.
|
|
64
|
+
* @param sessionId - Session to move.
|
|
65
|
+
* @param beforeSessionId - anchor Session; omitted appends.
|
|
66
|
+
* @returns the changed Workspace.
|
|
67
|
+
*/
|
|
68
|
+
insertSessionBefore(workspaceId: WorkspaceId, sessionId: SessionId, beforeSessionId?: SessionId): Promise<WorkspaceView>;
|
|
69
|
+
}
|
|
70
|
+
/** Owns the bare Workspace snapshot and Workspace-only commands. */
|
|
71
|
+
export declare class WorkspaceController extends Service implements IWorkspaces {
|
|
72
|
+
private readonly model;
|
|
73
|
+
readonly list: WorkspaceSource;
|
|
74
|
+
/**
|
|
75
|
+
* @param ctx - Client root Context.
|
|
76
|
+
* @param model - Remote-backed Workspace state model.
|
|
77
|
+
*/
|
|
78
|
+
constructor(ctx: Context, model: ClientWorkspaceModel);
|
|
79
|
+
create(input: {
|
|
80
|
+
path: string;
|
|
81
|
+
}): Promise<WorkspaceView>;
|
|
82
|
+
rename(workspaceId: WorkspaceId, title: string): Promise<WorkspaceView>;
|
|
83
|
+
delete(workspaceId: WorkspaceId): Promise<void>;
|
|
84
|
+
insertBefore(workspaceId: WorkspaceId, beforeWorkspaceId?: WorkspaceId): Promise<void>;
|
|
85
|
+
archiveSession(sessionId: SessionId): Promise<void>;
|
|
86
|
+
insertSessionBefore(workspaceId: WorkspaceId, sessionId: SessionId, beforeSessionId?: SessionId): Promise<WorkspaceView>;
|
|
87
|
+
}
|
|
88
|
+
//# sourceMappingURL=service.d.ts.map
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/** React-free Client Workspace service and command facade. */
|
|
2
|
+
import { Service } from '@deepseek-ai/cordis';
|
|
3
|
+
/** Structured create failure for callers that distinguish Host business errors. */
|
|
4
|
+
export class WorkspaceCreateError extends Error {
|
|
5
|
+
rpcError;
|
|
6
|
+
name = 'WorkspaceCreateError';
|
|
7
|
+
/** @param rpcError - Host business or folded carrier failure. */
|
|
8
|
+
constructor(rpcError) {
|
|
9
|
+
super(`workspace create failed: ${rpcError.code}: ${rpcError.message}`);
|
|
10
|
+
this.rpcError = rpcError;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
/** Owns the bare Workspace snapshot and Workspace-only commands. */
|
|
14
|
+
export class WorkspaceController extends Service {
|
|
15
|
+
model;
|
|
16
|
+
list;
|
|
17
|
+
/**
|
|
18
|
+
* @param ctx - Client root Context.
|
|
19
|
+
* @param model - Remote-backed Workspace state model.
|
|
20
|
+
*/
|
|
21
|
+
constructor(ctx, model) {
|
|
22
|
+
super(ctx, 'workspaces');
|
|
23
|
+
this.model = model;
|
|
24
|
+
this.list = model;
|
|
25
|
+
}
|
|
26
|
+
async create(input) {
|
|
27
|
+
const result = await this.model.create(input);
|
|
28
|
+
if (!result.ok)
|
|
29
|
+
throw new WorkspaceCreateError(result.error);
|
|
30
|
+
return result.value.workspace;
|
|
31
|
+
}
|
|
32
|
+
async rename(workspaceId, title) {
|
|
33
|
+
const result = await this.model.rename(workspaceId, title);
|
|
34
|
+
if (!result.ok)
|
|
35
|
+
throw commandError('rename', result.error);
|
|
36
|
+
return result.value.workspace;
|
|
37
|
+
}
|
|
38
|
+
async delete(workspaceId) {
|
|
39
|
+
const result = await this.model.delete(workspaceId);
|
|
40
|
+
if (!result.ok)
|
|
41
|
+
throw commandError('delete', result.error);
|
|
42
|
+
}
|
|
43
|
+
async insertBefore(workspaceId, beforeWorkspaceId) {
|
|
44
|
+
const result = await this.model.insertBefore(workspaceId, beforeWorkspaceId);
|
|
45
|
+
if (!result.ok)
|
|
46
|
+
throw commandError('reorder', result.error);
|
|
47
|
+
}
|
|
48
|
+
async archiveSession(sessionId) {
|
|
49
|
+
const result = await this.model.archiveSession(sessionId);
|
|
50
|
+
if (!result.ok)
|
|
51
|
+
throw commandError('session archive', result.error);
|
|
52
|
+
}
|
|
53
|
+
async insertSessionBefore(workspaceId, sessionId, beforeSessionId) {
|
|
54
|
+
const result = await this.model.insertSessionBefore(workspaceId, sessionId, beforeSessionId);
|
|
55
|
+
if (!result.ok)
|
|
56
|
+
throw commandError('move', result.error);
|
|
57
|
+
return result.value.workspace;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
function commandError(operation, failure) {
|
|
61
|
+
return new Error(`workspace ${operation} failed: ${failure.code}: ${failure.message}`);
|
|
62
|
+
}
|
|
63
|
+
//# sourceMappingURL=service.js.map
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/** Workspace command implementation and stable Remote failure mapping. */
|
|
2
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
3
|
+
import type { WorkspaceArchiveSessionRequest, WorkspaceArchiveValue, WorkspaceCreateRequest, WorkspaceCreateValue, WorkspaceDeleteRequest, WorkspaceDeleteValue, WorkspaceInsertBeforeRequest, WorkspaceInsertSessionBeforeRequest, WorkspaceOrderValue, WorkspaceRenameRequest, WorkspaceValue } from './types.ts';
|
|
4
|
+
/** Implements Workspace mutations against the authoritative registry. */
|
|
5
|
+
export declare class WorkspaceCommands {
|
|
6
|
+
private readonly ctx;
|
|
7
|
+
private operationTail;
|
|
8
|
+
/** @param ctx - Host context containing the Workspace registry. */
|
|
9
|
+
constructor(ctx: Context);
|
|
10
|
+
/**
|
|
11
|
+
* Create or resolve one Workspace over an existing directory.
|
|
12
|
+
* @param request - directory path to register.
|
|
13
|
+
* @returns the Workspace and whether this call created it.
|
|
14
|
+
*/
|
|
15
|
+
create(request: WorkspaceCreateRequest): Promise<WorkspaceCreateValue>;
|
|
16
|
+
/**
|
|
17
|
+
* Rename one Workspace after serializing title ownership checks.
|
|
18
|
+
* @param request - Workspace identity and proposed title.
|
|
19
|
+
* @returns the updated Workspace projection.
|
|
20
|
+
*/
|
|
21
|
+
rename(request: WorkspaceRenameRequest): Promise<WorkspaceValue>;
|
|
22
|
+
/**
|
|
23
|
+
* Delete one Workspace registration without deleting its directory or Sessions.
|
|
24
|
+
* @param request - Workspace identity to remove.
|
|
25
|
+
* @returns deletion confirmation.
|
|
26
|
+
*/
|
|
27
|
+
delete(request: WorkspaceDeleteRequest): Promise<WorkspaceDeleteValue>;
|
|
28
|
+
/**
|
|
29
|
+
* Move one Workspace within the durable registry order.
|
|
30
|
+
* @param request - moved Workspace and optional anchor.
|
|
31
|
+
* @returns the complete resulting Workspace order.
|
|
32
|
+
*/
|
|
33
|
+
insertBefore(request: WorkspaceInsertBeforeRequest): Promise<WorkspaceOrderValue>;
|
|
34
|
+
/**
|
|
35
|
+
* Move one accounted Session within a Workspace's manual order.
|
|
36
|
+
* @param request - Workspace, Session, and optional anchor identities.
|
|
37
|
+
* @returns the updated Workspace projection.
|
|
38
|
+
*/
|
|
39
|
+
insertSessionBefore(request: WorkspaceInsertSessionBeforeRequest): Promise<WorkspaceValue>;
|
|
40
|
+
/**
|
|
41
|
+
* Add one known Session to the registry-global archive set.
|
|
42
|
+
* @param request - Session identity to archive.
|
|
43
|
+
* @returns the complete resulting archive set.
|
|
44
|
+
*/
|
|
45
|
+
archiveSession(request: WorkspaceArchiveSessionRequest): Promise<WorkspaceArchiveValue>;
|
|
46
|
+
private requireWorkspace;
|
|
47
|
+
private enqueue;
|
|
48
|
+
}
|
|
49
|
+
//# sourceMappingURL=commands.d.ts.map
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/** Workspace command implementation and stable Remote failure mapping. */
|
|
2
|
+
import { WorkspaceId, WorkspaceMoveInvalidError, WorkspaceOrderInvalidError, WorkspaceUnknownSessionError, } from '@deepseek-ai/dsh-workspace';
|
|
3
|
+
import { RemoteError, remoteErrorOf } from '@deepseek-ai/dsh-typert-protocol';
|
|
4
|
+
import { workspaceView } from "./feed.js";
|
|
5
|
+
/** Implements Workspace mutations against the authoritative registry. */
|
|
6
|
+
export class WorkspaceCommands {
|
|
7
|
+
ctx;
|
|
8
|
+
operationTail = Promise.resolve();
|
|
9
|
+
/** @param ctx - Host context containing the Workspace registry. */
|
|
10
|
+
constructor(ctx) {
|
|
11
|
+
this.ctx = ctx;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Create or resolve one Workspace over an existing directory.
|
|
15
|
+
* @param request - directory path to register.
|
|
16
|
+
* @returns the Workspace and whether this call created it.
|
|
17
|
+
*/
|
|
18
|
+
create(request) {
|
|
19
|
+
return this.enqueue(async () => {
|
|
20
|
+
try {
|
|
21
|
+
const existing = await this.ctx.workspaceRegistry.resolveByPath(request.path);
|
|
22
|
+
if (existing !== undefined) {
|
|
23
|
+
return { workspace: workspaceView(existing), created: false };
|
|
24
|
+
}
|
|
25
|
+
const workspace = await this.ctx.workspaceRegistry.create(request.path);
|
|
26
|
+
return { workspace: workspaceView(workspace), created: true };
|
|
27
|
+
}
|
|
28
|
+
catch (error) {
|
|
29
|
+
if (remoteErrorOf(error) !== undefined)
|
|
30
|
+
throw error;
|
|
31
|
+
throw new RemoteError('workspace/invalid-path', `cannot create a Workspace at "${request.path}": ${errorMessage(error)}`, { path: request.path }, { cause: error });
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Rename one Workspace after serializing title ownership checks.
|
|
37
|
+
* @param request - Workspace identity and proposed title.
|
|
38
|
+
* @returns the updated Workspace projection.
|
|
39
|
+
*/
|
|
40
|
+
rename(request) {
|
|
41
|
+
const title = request.title.trim();
|
|
42
|
+
if (title === '') {
|
|
43
|
+
return Promise.reject(new RemoteError('gateway/bad-request', 'Workspace rename requires a non-blank title', {}));
|
|
44
|
+
}
|
|
45
|
+
return this.enqueue(async () => {
|
|
46
|
+
const workspace = this.requireWorkspace(request.workspaceId);
|
|
47
|
+
if (title !== workspace.title) {
|
|
48
|
+
if (this.ctx.workspaceRegistry.list().some(candidate => candidate.id !== workspace.id && candidate.title === title)) {
|
|
49
|
+
throw new RemoteError('workspace/name-conflict', `Workspace name '${title}' is already in use`, { name: title });
|
|
50
|
+
}
|
|
51
|
+
await workspace.setTitle(title);
|
|
52
|
+
}
|
|
53
|
+
return { workspace: workspaceView(workspace) };
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Delete one Workspace registration without deleting its directory or Sessions.
|
|
58
|
+
* @param request - Workspace identity to remove.
|
|
59
|
+
* @returns deletion confirmation.
|
|
60
|
+
*/
|
|
61
|
+
delete(request) {
|
|
62
|
+
return this.enqueue(async () => {
|
|
63
|
+
if (!await this.ctx.workspaceRegistry.delete(WorkspaceId(request.workspaceId))) {
|
|
64
|
+
throw workspaceNotFound(request.workspaceId);
|
|
65
|
+
}
|
|
66
|
+
return { deleted: true };
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Move one Workspace within the durable registry order.
|
|
71
|
+
* @param request - moved Workspace and optional anchor.
|
|
72
|
+
* @returns the complete resulting Workspace order.
|
|
73
|
+
*/
|
|
74
|
+
async insertBefore(request) {
|
|
75
|
+
try {
|
|
76
|
+
const workspaceIds = await this.ctx.workspaceRegistry.insertBefore(WorkspaceId(request.workspaceId), request.beforeWorkspaceId === undefined
|
|
77
|
+
? undefined
|
|
78
|
+
: WorkspaceId(request.beforeWorkspaceId));
|
|
79
|
+
return { workspaceIds: [...workspaceIds] };
|
|
80
|
+
}
|
|
81
|
+
catch (error) {
|
|
82
|
+
if (!(error instanceof WorkspaceOrderInvalidError))
|
|
83
|
+
throw error;
|
|
84
|
+
throw workspaceNotFound(error.workspaceId);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Move one accounted Session within a Workspace's manual order.
|
|
89
|
+
* @param request - Workspace, Session, and optional anchor identities.
|
|
90
|
+
* @returns the updated Workspace projection.
|
|
91
|
+
*/
|
|
92
|
+
async insertSessionBefore(request) {
|
|
93
|
+
const workspace = this.requireWorkspace(request.workspaceId);
|
|
94
|
+
try {
|
|
95
|
+
await workspace.insertSessionBefore(request.sessionId, request.beforeSessionId);
|
|
96
|
+
}
|
|
97
|
+
catch (error) {
|
|
98
|
+
if (!(error instanceof WorkspaceMoveInvalidError))
|
|
99
|
+
throw error;
|
|
100
|
+
throw new RemoteError('workspace/move-invalid', error.message, {
|
|
101
|
+
workspaceId: request.workspaceId,
|
|
102
|
+
sessionId: request.sessionId,
|
|
103
|
+
...request.beforeSessionId === undefined
|
|
104
|
+
? {}
|
|
105
|
+
: { beforeSessionId: request.beforeSessionId },
|
|
106
|
+
}, { cause: error });
|
|
107
|
+
}
|
|
108
|
+
return { workspace: workspaceView(workspace) };
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Add one known Session to the registry-global archive set.
|
|
112
|
+
* @param request - Session identity to archive.
|
|
113
|
+
* @returns the complete resulting archive set.
|
|
114
|
+
*/
|
|
115
|
+
async archiveSession(request) {
|
|
116
|
+
try {
|
|
117
|
+
await this.ctx.workspaceRegistry.archiveSession(request.sessionId);
|
|
118
|
+
}
|
|
119
|
+
catch (error) {
|
|
120
|
+
if (!(error instanceof WorkspaceUnknownSessionError))
|
|
121
|
+
throw error;
|
|
122
|
+
throw new RemoteError('session/not-found', error.message, { sessionId: request.sessionId }, { cause: error });
|
|
123
|
+
}
|
|
124
|
+
return { archivedSessionIds: [...this.ctx.workspaceRegistry.archivedSessionIds] };
|
|
125
|
+
}
|
|
126
|
+
requireWorkspace(workspaceId) {
|
|
127
|
+
const workspace = this.ctx.workspaceRegistry.get(WorkspaceId(workspaceId));
|
|
128
|
+
if (workspace === undefined)
|
|
129
|
+
throw workspaceNotFound(workspaceId);
|
|
130
|
+
return workspace;
|
|
131
|
+
}
|
|
132
|
+
enqueue(operation) {
|
|
133
|
+
const result = this.operationTail.then(operation);
|
|
134
|
+
this.operationTail = result.then(() => undefined, () => undefined);
|
|
135
|
+
return result;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
function workspaceNotFound(workspaceId) {
|
|
139
|
+
return new RemoteError('workspace/not-found', `Workspace "${workspaceId}" not found`, { workspaceId });
|
|
140
|
+
}
|
|
141
|
+
function errorMessage(error) {
|
|
142
|
+
return error instanceof Error ? error.message : String(error);
|
|
143
|
+
}
|
|
144
|
+
//# sourceMappingURL=commands.js.map
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Host directory-picking Remote owner: capability gating, cancellation, and the
|
|
3
|
+
* stable wire failure vocabulary over the `ctx.directoryPicker` seam.
|
|
4
|
+
*/
|
|
5
|
+
import { Context } from '@deepseek-ai/cordis';
|
|
6
|
+
import type { DirectoryListing } from '@deepseek-ai/dsh-host-directory-picker/types';
|
|
7
|
+
import { TypertRemoteService } from '@deepseek-ai/dsh-typert-protocol';
|
|
8
|
+
declare module '@deepseek-ai/cordis' {
|
|
9
|
+
interface Context {
|
|
10
|
+
/** Host directory-picking Remote namespace owner. */
|
|
11
|
+
directoryPickerController: DirectoryPickerController;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Host service backing the generated `ctx.remote.directoryPicker` namespace. The
|
|
16
|
+
* seam it exports is abstract and therefore never a Loader entry of its own, so
|
|
17
|
+
* this controller carries the wire verbs: one composed backend serves either the
|
|
18
|
+
* native chooser or the browse primitives, and a verb the composition cannot
|
|
19
|
+
* serve is refused rather than approximated.
|
|
20
|
+
*/
|
|
21
|
+
export declare class DirectoryPickerController extends TypertRemoteService {
|
|
22
|
+
static inject: string[];
|
|
23
|
+
/** @param ctx - Host context carrying the composed directory-picking backend. */
|
|
24
|
+
constructor(ctx: Context);
|
|
25
|
+
/**
|
|
26
|
+
* Open the host's OS chooser for a Remote caller.
|
|
27
|
+
* @param signal - caller lifetime; abort terminates the chooser.
|
|
28
|
+
* @returns the chosen absolute path, or null when the operator cancels.
|
|
29
|
+
*/
|
|
30
|
+
pick(signal: AbortSignal): Promise<string | null>;
|
|
31
|
+
/**
|
|
32
|
+
* List one directory level for a Remote caller's in-app browser.
|
|
33
|
+
* @param path - absolute directory to list; absent lists the home directory.
|
|
34
|
+
* @param signal - caller lifetime; abort stops the backend's scan instead of
|
|
35
|
+
* letting it outlive a disconnected caller.
|
|
36
|
+
* @returns the level's listing with its ancestry.
|
|
37
|
+
*/
|
|
38
|
+
list(path: string | undefined, signal: AbortSignal): Promise<DirectoryListing>;
|
|
39
|
+
/**
|
|
40
|
+
* Create one child directory for a Remote caller's in-app browser.
|
|
41
|
+
* @param path - absolute existing parent directory.
|
|
42
|
+
* @param name - single non-blank path segment.
|
|
43
|
+
* @returns the created directory's absolute path.
|
|
44
|
+
*/
|
|
45
|
+
createDirectory(path: string, name: string): Promise<string>;
|
|
46
|
+
/** Resolve the capability one wire verb needs, or refuse with the kind this backend serves. */
|
|
47
|
+
private requireCapability;
|
|
48
|
+
}
|
|
49
|
+
//# sourceMappingURL=directory-picker.d.ts.map
|