@huanlin/dsh-plugin-ya-workspace-sidebar 0.6.1 → 0.7.0
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.md +1 -1
- package/lib/client.js +3202 -2851
- package/lib/client.js.map +1 -1
- package/lib/index.js +313 -1
- package/lib/types/client/VirtualRecentList.d.ts +20 -20
- package/lib/types/client/WorkspacePicker.d.ts +26 -26
- package/lib/types/client/contract.d.ts +88 -76
- package/lib/types/client/dictionaries.d.ts +7 -2
- package/lib/types/client/index.d.ts +9 -9
- package/lib/types/client/locales.d.ts +133 -113
- package/lib/types/client/model.d.ts +96 -87
- package/lib/types/client/navigation.d.ts +45 -45
- package/lib/types/client/session-flags.d.ts +29 -0
- package/lib/types/client/settings.d.ts +37 -37
- package/lib/types/client/styles.d.ts +4 -4
- package/lib/types/index.d.ts +3 -3
- package/lib/types/reveal.d.ts +20 -0
- package/lib/types/session-artifacts.d.ts +42 -0
- package/lib/types/trust-fence.d.ts +22 -0
- package/package.json +2 -1
package/lib/index.js
CHANGED
|
@@ -1,6 +1,222 @@
|
|
|
1
|
+
import { readdir, stat } from "node:fs/promises";
|
|
1
2
|
import { readFileSync, writeFileSync } from "node:fs";
|
|
2
3
|
import { homedir } from "node:os";
|
|
3
|
-
import { join } from "node:path";
|
|
4
|
+
import { dirname, isAbsolute, join, posix, resolve, win32 } from "node:path";
|
|
5
|
+
import { spawn } from "node:child_process";
|
|
6
|
+
//#region ../dsh/packages/util/home-paths/lib/index.js
|
|
7
|
+
/**
|
|
8
|
+
* Shared filesystem path helpers for DeepSeek Harness user data.
|
|
9
|
+
*
|
|
10
|
+
* @module @deepseek-ai/dsh-home-paths
|
|
11
|
+
*/
|
|
12
|
+
/** Directory name for the default DeepSeek Harness home under the OS home. */
|
|
13
|
+
const DSH_HOME_DIR_NAME = ".dsh";
|
|
14
|
+
/** Environment variable that overrides the default DeepSeek Harness home. */
|
|
15
|
+
const DSH_HOME_ENV = "DSH_HOME";
|
|
16
|
+
/**
|
|
17
|
+
* Resolve the default DeepSeek Harness home using Node's platform path rules.
|
|
18
|
+
* @returns the absolute default harness home path.
|
|
19
|
+
*/
|
|
20
|
+
function defaultDshHome() {
|
|
21
|
+
return join(homedir(), DSH_HOME_DIR_NAME);
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Expand supported tilde prefixes against the operating-system home.
|
|
25
|
+
* @param path - configured path that may begin with `~`, `~/`, or `~\`.
|
|
26
|
+
* @returns the expanded path, or the original value when no supported prefix is present.
|
|
27
|
+
*/
|
|
28
|
+
function expandHomePath(path) {
|
|
29
|
+
if (path === "~") return homedir();
|
|
30
|
+
if (path.startsWith("~/") || path.startsWith("~\\")) return join(homedir(), path.slice(2));
|
|
31
|
+
return path;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Resolve the single-root DeepSeek Harness home.
|
|
35
|
+
*
|
|
36
|
+
* Precedence, highest first: an explicit configured path, `$DSH_HOME`, then
|
|
37
|
+
* `~/.dsh`. The harness keeps all user data under one root. An empty or
|
|
38
|
+
* whitespace-only `$DSH_HOME` is treated as unset, so a blank override never
|
|
39
|
+
* resolves the home to the current working directory.
|
|
40
|
+
* @param configured - explicit harness-home override, which has highest precedence.
|
|
41
|
+
* @param env - environment mapping used to read `DSH_HOME`.
|
|
42
|
+
* @returns the normalized absolute harness home path.
|
|
43
|
+
*/
|
|
44
|
+
function resolveDshHome(configured, env = process.env) {
|
|
45
|
+
const fromEnv = env[DSH_HOME_ENV];
|
|
46
|
+
return resolve(expandHomePath(configured ?? (fromEnv !== void 0 && fromEnv.trim().length > 0 ? fromEnv : defaultDshHome())));
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Join path segments onto the resolved DeepSeek Harness home.
|
|
50
|
+
* @param segments - path segments appended to the Harness home; an empty list returns the home itself.
|
|
51
|
+
* @returns the normalized absolute joined path.
|
|
52
|
+
*/
|
|
53
|
+
function dshHomePath(...segments) {
|
|
54
|
+
return join(resolveDshHome(), ...segments);
|
|
55
|
+
}
|
|
56
|
+
//#endregion
|
|
57
|
+
//#region src/session-artifacts.ts
|
|
58
|
+
/**
|
|
59
|
+
* Encode an arbitrary string as one safe path segment, injectively over all
|
|
60
|
+
* JS strings (parity with the official jsonl backend's segment encoder: safe
|
|
61
|
+
* units literal, every other unit `~XXXX`, dot segments traversal-proofed).
|
|
62
|
+
* @param raw - the string to encode; must be non-empty.
|
|
63
|
+
* @returns the escaped single path segment.
|
|
64
|
+
*/
|
|
65
|
+
function encodeSegment(raw) {
|
|
66
|
+
if (raw.length === 0) throw new Error("cannot encode an empty path segment");
|
|
67
|
+
if (raw === ".") return "~002E";
|
|
68
|
+
if (raw === "..") return "~002E~002E";
|
|
69
|
+
let out = "";
|
|
70
|
+
for (let i = 0; i < raw.length; i++) {
|
|
71
|
+
const code = raw.charCodeAt(i);
|
|
72
|
+
const ch = String.fromCharCode(code);
|
|
73
|
+
if (ch !== "~" && /^[A-Za-z0-9._-]$/.test(ch)) out += ch;
|
|
74
|
+
else out += "~" + code.toString(16).toUpperCase().padStart(4, "0");
|
|
75
|
+
}
|
|
76
|
+
return out;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Parse one committed generation filename into its format version.
|
|
80
|
+
* @param filename - candidate entry from a session directory.
|
|
81
|
+
* @returns the generation version (`vN.jsonl` / `vN.jsonl.zstd`; version-zero
|
|
82
|
+
* is the suffix-only `.jsonl`), or undefined for non-generation names.
|
|
83
|
+
*/
|
|
84
|
+
function logGeneration(filename) {
|
|
85
|
+
const match = /^v(\d+)\.jsonl(\.zstd)?$/.exec(filename);
|
|
86
|
+
if (match !== null) return Number(match[1]);
|
|
87
|
+
return filename === ".jsonl" ? 0 : void 0;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Find the session's storage dir and newest log file under a sessions root.
|
|
91
|
+
* @param root - sessions root directory (`$DSH_HOME/sessions`).
|
|
92
|
+
* @param sessionId - raw session id (encoded here before any filesystem use).
|
|
93
|
+
* @param io - fs surface; a stat miss under one project directory keeps the scan going.
|
|
94
|
+
* @returns the newest owning directory and its highest-generation log file,
|
|
95
|
+
* either null when absent (sqlite deployments, unflushed sessions).
|
|
96
|
+
*/
|
|
97
|
+
async function findSessionArtifacts(root, sessionId, io) {
|
|
98
|
+
const segment = encodeSegment(sessionId);
|
|
99
|
+
let best;
|
|
100
|
+
for (const entry of await io.readdir(root, { withFileTypes: true })) {
|
|
101
|
+
if (!entry.isDirectory() || !entry.name.startsWith("--") || !entry.name.endsWith("--")) continue;
|
|
102
|
+
const candidate = join(root, entry.name, segment);
|
|
103
|
+
try {
|
|
104
|
+
const stats = await io.stat(candidate);
|
|
105
|
+
if (best === void 0 || stats.mtimeMs > best.mtimeMs) best = {
|
|
106
|
+
dir: candidate,
|
|
107
|
+
mtimeMs: stats.mtimeMs
|
|
108
|
+
};
|
|
109
|
+
} catch {}
|
|
110
|
+
}
|
|
111
|
+
if (best === void 0) return {
|
|
112
|
+
dir: null,
|
|
113
|
+
log: null
|
|
114
|
+
};
|
|
115
|
+
let log;
|
|
116
|
+
let logVersion = -1;
|
|
117
|
+
for (const entry of await io.readdir(best.dir, { withFileTypes: true })) {
|
|
118
|
+
if (!entry.isFile()) continue;
|
|
119
|
+
const version = logGeneration(entry.name);
|
|
120
|
+
if (version !== void 0 && version >= logVersion) {
|
|
121
|
+
logVersion = version;
|
|
122
|
+
log = join(best.dir, entry.name);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return {
|
|
126
|
+
dir: best.dir,
|
|
127
|
+
log: log ?? null
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
//#endregion
|
|
131
|
+
//#region src/reveal.ts
|
|
132
|
+
/** Platform opener commands for revealing a path in the OS file manager. */
|
|
133
|
+
/**
|
|
134
|
+
* Build the opener invocation that reveals/selects a path (pure, platform injectable).
|
|
135
|
+
* @param path - absolute path to reveal.
|
|
136
|
+
* @param platform - Node platform selector; win32 selects the entry, darwin
|
|
137
|
+
* reveals with `open -R`, everything else opens the parent directory.
|
|
138
|
+
* @returns the argv invocation (never a shell string).
|
|
139
|
+
*/
|
|
140
|
+
function revealCommand(path, platform) {
|
|
141
|
+
switch (platform) {
|
|
142
|
+
case "darwin": return {
|
|
143
|
+
command: "open",
|
|
144
|
+
args: ["-R", path]
|
|
145
|
+
};
|
|
146
|
+
case "win32": return {
|
|
147
|
+
command: "explorer.exe",
|
|
148
|
+
args: [`/select,${path}`]
|
|
149
|
+
};
|
|
150
|
+
default: return {
|
|
151
|
+
command: "xdg-open",
|
|
152
|
+
args: [dirname(path)]
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Launch the reveal opener detached and return immediately.
|
|
158
|
+
* @param path - absolute path to reveal in the OS file manager.
|
|
159
|
+
* @returns started acknowledgement; opener failures after the spawn surface
|
|
160
|
+
* through the OS (the child's error event is swallowed on purpose).
|
|
161
|
+
*/
|
|
162
|
+
function launchReveal(path) {
|
|
163
|
+
const spec = revealCommand(path, process.platform);
|
|
164
|
+
const child = spawn(spec.command, spec.args, {
|
|
165
|
+
detached: true,
|
|
166
|
+
stdio: "ignore"
|
|
167
|
+
});
|
|
168
|
+
child.on("error", () => {});
|
|
169
|
+
child.unref();
|
|
170
|
+
return { started: true };
|
|
171
|
+
}
|
|
172
|
+
//#endregion
|
|
173
|
+
//#region src/trust-fence.ts
|
|
174
|
+
function header(headers, name) {
|
|
175
|
+
const value = headers[name];
|
|
176
|
+
return typeof value === "string" ? value : void 0;
|
|
177
|
+
}
|
|
178
|
+
/** Normalized URL of a Host-header authority, or undefined when unparsable. */
|
|
179
|
+
function parseAuthority(authority) {
|
|
180
|
+
try {
|
|
181
|
+
return new URL(`http://${authority}`);
|
|
182
|
+
} catch {
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
/** Whether a normalized URL hostname names the local loopback authority. */
|
|
187
|
+
function isLoopbackHostname(hostname) {
|
|
188
|
+
if (hostname === "localhost" || hostname === "[::1]") return true;
|
|
189
|
+
const parts = hostname.split(".");
|
|
190
|
+
return parts.length === 4 && parts[0] === "127" && parts.every((part) => /^\d{1,3}$/.test(part) && Number(part) <= 255);
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Decide whether one plugin route request may proceed.
|
|
194
|
+
* @param request - node HTTP request facts (headers).
|
|
195
|
+
* @param trustedHosts - non-loopback authorities this deployment serves.
|
|
196
|
+
* @returns true when the Host is ours (loopback or trusted) and browser
|
|
197
|
+
* markers are same-origin; absent Origin passes on the bound Host alone,
|
|
198
|
+
* the opaque "null" origin is refused.
|
|
199
|
+
*/
|
|
200
|
+
function isTrustedRouteRequest(request, trustedHosts = []) {
|
|
201
|
+
const host = header(request.headers, "host");
|
|
202
|
+
if (host === void 0) return false;
|
|
203
|
+
const hostUrl = parseAuthority(host);
|
|
204
|
+
if (hostUrl === void 0) return false;
|
|
205
|
+
if (!(isLoopbackHostname(hostUrl.hostname) || trustedHosts.some((entry) => {
|
|
206
|
+
const entryUrl = parseAuthority(entry);
|
|
207
|
+
return entryUrl !== void 0 && entryUrl.hostname === hostUrl.hostname;
|
|
208
|
+
}))) return false;
|
|
209
|
+
if (header(request.headers, "sec-fetch-site") === "cross-site") return false;
|
|
210
|
+
const origin = header(request.headers, "origin");
|
|
211
|
+
if (origin === void 0) return true;
|
|
212
|
+
if (origin === "null") return false;
|
|
213
|
+
try {
|
|
214
|
+
return new URL(origin).hostname === hostUrl.hostname;
|
|
215
|
+
} catch {
|
|
216
|
+
return false;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
//#endregion
|
|
4
220
|
//#region src/index.ts
|
|
5
221
|
/**
|
|
6
222
|
* Host half for the browser-only ya-workspace-sidebar plugin.
|
|
@@ -71,6 +287,102 @@ function apply(ctx) {
|
|
|
71
287
|
});
|
|
72
288
|
if (timer === void 0) timer = setTimeout(flush, 2e3);
|
|
73
289
|
});
|
|
290
|
+
ctx.inject(["webServer"], (wctx) => {
|
|
291
|
+
const webServer = wctx.webServer;
|
|
292
|
+
const disposeRoutes = [webServer.register({
|
|
293
|
+
kind: "exact",
|
|
294
|
+
path: "/ya-workspace-sidebar/paths",
|
|
295
|
+
handler: pathsRoute
|
|
296
|
+
}), webServer.register({
|
|
297
|
+
kind: "exact",
|
|
298
|
+
path: "/ya-workspace-sidebar/reveal",
|
|
299
|
+
handler: revealRoute
|
|
300
|
+
})];
|
|
301
|
+
return () => {
|
|
302
|
+
for (const dispose of disposeRoutes) dispose();
|
|
303
|
+
};
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
/** Body size bound of one JSON request (defense against unbounded reads). */
|
|
307
|
+
const MAX_BODY_BYTES = 1 << 20;
|
|
308
|
+
/** Read and parse one bounded JSON request body; malformed input throws. */
|
|
309
|
+
async function readJsonBody(req) {
|
|
310
|
+
const chunks = [];
|
|
311
|
+
let total = 0;
|
|
312
|
+
for await (const chunk of req) {
|
|
313
|
+
const buffer = Buffer.from(chunk);
|
|
314
|
+
total += buffer.length;
|
|
315
|
+
if (total > MAX_BODY_BYTES) throw new Error("request body too large");
|
|
316
|
+
chunks.push(buffer);
|
|
317
|
+
}
|
|
318
|
+
const text = Buffer.concat(chunks).toString("utf8");
|
|
319
|
+
if (text.trim() === "") return {};
|
|
320
|
+
return JSON.parse(text);
|
|
321
|
+
}
|
|
322
|
+
/** Write one JSON response. */
|
|
323
|
+
function writeJson(res, status, body) {
|
|
324
|
+
res.writeHead(status, { "content-type": "application/json; charset=utf-8" });
|
|
325
|
+
res.end(JSON.stringify(body));
|
|
326
|
+
}
|
|
327
|
+
/** Reject untrusted/fenced requests with 403. */
|
|
328
|
+
function refused(res) {
|
|
329
|
+
writeJson(res, 403, { error: "untrusted" });
|
|
330
|
+
}
|
|
331
|
+
/** POST /ya-workspace-sidebar/paths — probe the session's jsonl storage layout. */
|
|
332
|
+
async function pathsRoute(req, res) {
|
|
333
|
+
if (!isTrustedRouteRequest(req)) {
|
|
334
|
+
refused(res);
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
let payload;
|
|
338
|
+
try {
|
|
339
|
+
payload = await readJsonBody(req);
|
|
340
|
+
} catch (error) {
|
|
341
|
+
writeJson(res, 400, { error: error instanceof Error ? error.message : "bad json" });
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
const sessionId = payload?.sessionId;
|
|
345
|
+
if (typeof sessionId !== "string" || sessionId.length === 0) {
|
|
346
|
+
writeJson(res, 400, { error: "sessionId required" });
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
try {
|
|
350
|
+
writeJson(res, 200, await findSessionArtifacts(dshHomePath("sessions"), sessionId, {
|
|
351
|
+
readdir: (path, options) => readdir(path, options),
|
|
352
|
+
stat: (path) => stat(path)
|
|
353
|
+
}));
|
|
354
|
+
} catch (error) {
|
|
355
|
+
writeJson(res, 500, { error: error instanceof Error ? error.message : String(error) });
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
/** POST /ya-workspace-sidebar/reveal — open one existing directory in the OS file manager. */
|
|
359
|
+
async function revealRoute(req, res) {
|
|
360
|
+
if (!isTrustedRouteRequest(req)) {
|
|
361
|
+
refused(res);
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
let payload;
|
|
365
|
+
try {
|
|
366
|
+
payload = await readJsonBody(req);
|
|
367
|
+
} catch (error) {
|
|
368
|
+
writeJson(res, 400, { error: error instanceof Error ? error.message : "bad json" });
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
const path = payload?.path;
|
|
372
|
+
if (typeof path !== "string" || !(isAbsolute(path) || win32.isAbsolute(path) || posix.isAbsolute(path))) {
|
|
373
|
+
writeJson(res, 400, { error: "absolute path required" });
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
try {
|
|
377
|
+
if (!(await stat(path)).isDirectory()) {
|
|
378
|
+
writeJson(res, 400, { error: "not a directory" });
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
} catch {
|
|
382
|
+
writeJson(res, 400, { error: "path not found" });
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
writeJson(res, 200, launchReveal(path));
|
|
74
386
|
}
|
|
75
387
|
//#endregion
|
|
76
388
|
export { apply, name };
|
|
@@ -1,20 +1,20 @@
|
|
|
1
|
-
/** Fixed-stride virtual scroller for the global recent-sessions block. */
|
|
2
|
-
import { type ReactNode } from 'react';
|
|
3
|
-
import { type SessionRow } from './model.ts';
|
|
4
|
-
interface VirtualRecentListProps {
|
|
5
|
-
rows: readonly SessionRow[];
|
|
6
|
-
renderItem: (row: SessionRow) => ReactNode;
|
|
7
|
-
ariaLabel: string;
|
|
8
|
-
/** Receives the viewport element for external geometry (the drag resizer). */
|
|
9
|
-
viewportRef?: (node: HTMLDivElement | null) => void;
|
|
10
|
-
/** Overrides the stylesheet's default viewport cap, in px. */
|
|
11
|
-
maxHeight?: number;
|
|
12
|
-
}
|
|
13
|
-
/**
|
|
14
|
-
* Renders `rows` inside a bounded, scrollable viewport while mounting only
|
|
15
|
-
* the visible slice (plus overscan). The row stride is fixed
|
|
16
|
-
* ({@link RECENT_ROW_STRIDE}), so windowing is pure arithmetic — no per-row
|
|
17
|
-
* measurement — and the scroll position survives session-list updates.
|
|
18
|
-
*/
|
|
19
|
-
export declare function VirtualRecentList({ rows, renderItem, ariaLabel, viewportRef, maxHeight }: VirtualRecentListProps): import("react").JSX.Element;
|
|
20
|
-
export {};
|
|
1
|
+
/** Fixed-stride virtual scroller for the global recent-sessions block. */
|
|
2
|
+
import { type ReactNode } from 'react';
|
|
3
|
+
import { type SessionRow } from './model.ts';
|
|
4
|
+
interface VirtualRecentListProps {
|
|
5
|
+
rows: readonly SessionRow[];
|
|
6
|
+
renderItem: (row: SessionRow) => ReactNode;
|
|
7
|
+
ariaLabel: string;
|
|
8
|
+
/** Receives the viewport element for external geometry (the drag resizer). */
|
|
9
|
+
viewportRef?: (node: HTMLDivElement | null) => void;
|
|
10
|
+
/** Overrides the stylesheet's default viewport cap, in px. */
|
|
11
|
+
maxHeight?: number;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Renders `rows` inside a bounded, scrollable viewport while mounting only
|
|
15
|
+
* the visible slice (plus overscan). The row stride is fixed
|
|
16
|
+
* ({@link RECENT_ROW_STRIDE}), so windowing is pure arithmetic — no per-row
|
|
17
|
+
* measurement — and the scroll position survives session-list updates.
|
|
18
|
+
*/
|
|
19
|
+
export declare function VirtualRecentList({ rows, renderItem, ariaLabel, viewportRef, maxHeight }: VirtualRecentListProps): import("react").JSX.Element;
|
|
20
|
+
export {};
|
|
@@ -1,26 +1,26 @@
|
|
|
1
|
-
/** Existing-workspace menu plus composed directory-adoption flow. */
|
|
2
|
-
import type { ReactNode, RefObject } from 'react';
|
|
3
|
-
import type { WorkspaceId, WorkspaceSnapshot, WorkspaceView } from '@deepseek-ai/dsh-api-workspace-controller/client';
|
|
4
|
-
import type { SnapshotSelectorHook } from '@deepseek-ai/dsh-client-ui-slots';
|
|
5
|
-
import type { DirectoryFlowOwnerProps, PickerProps } from './contract.ts';
|
|
6
|
-
interface FlowProps {
|
|
7
|
-
t: PickerProps['t'];
|
|
8
|
-
open: boolean;
|
|
9
|
-
anchorRef?: RefObject<HTMLElement | null>;
|
|
10
|
-
useWorkspaces: SnapshotSelectorHook<WorkspaceSnapshot>;
|
|
11
|
-
createWorkspace: (input: {
|
|
12
|
-
path: string;
|
|
13
|
-
}) => Promise<WorkspaceView>;
|
|
14
|
-
useDirectoryFlow: SnapshotSelectorHook<boolean>;
|
|
15
|
-
renderDirectoryFlow: (owner: DirectoryFlowOwnerProps) => ReactNode;
|
|
16
|
-
onPick: (workspaceId: WorkspaceId) => void;
|
|
17
|
-
onClose: () => void;
|
|
18
|
-
addOnly?: boolean;
|
|
19
|
-
side?: 'bottom' | 'top' | 'right';
|
|
20
|
-
selectedId?: WorkspaceId;
|
|
21
|
-
}
|
|
22
|
-
/** Render the workspace target menu and directory picking conversation. */
|
|
23
|
-
export declare function WorkspacePickFlow({ t, open, anchorRef, useWorkspaces, createWorkspace, useDirectoryFlow, renderDirectoryFlow, onPick, onClose, addOnly, side, selectedId, }: FlowProps): import("react").JSX.Element;
|
|
24
|
-
/** Fill the conversation hero's workspace picker seat. */
|
|
25
|
-
export declare function WorkspacePicker({ open, anchorRef, useWorkspaces, selectedId, onPick, onClose, createWorkspace, useDirectoryFlow, renderSlot, t, }: PickerProps): import("react").JSX.Element;
|
|
26
|
-
export {};
|
|
1
|
+
/** Existing-workspace menu plus composed directory-adoption flow. */
|
|
2
|
+
import type { ReactNode, RefObject } from 'react';
|
|
3
|
+
import type { WorkspaceId, WorkspaceSnapshot, WorkspaceView } from '@deepseek-ai/dsh-api-workspace-controller/client';
|
|
4
|
+
import type { SnapshotSelectorHook } from '@deepseek-ai/dsh-client-ui-slots';
|
|
5
|
+
import type { DirectoryFlowOwnerProps, PickerProps } from './contract.ts';
|
|
6
|
+
interface FlowProps {
|
|
7
|
+
t: PickerProps['t'];
|
|
8
|
+
open: boolean;
|
|
9
|
+
anchorRef?: RefObject<HTMLElement | null>;
|
|
10
|
+
useWorkspaces: SnapshotSelectorHook<WorkspaceSnapshot>;
|
|
11
|
+
createWorkspace: (input: {
|
|
12
|
+
path: string;
|
|
13
|
+
}) => Promise<WorkspaceView>;
|
|
14
|
+
useDirectoryFlow: SnapshotSelectorHook<boolean>;
|
|
15
|
+
renderDirectoryFlow: (owner: DirectoryFlowOwnerProps) => ReactNode;
|
|
16
|
+
onPick: (workspaceId: WorkspaceId) => void;
|
|
17
|
+
onClose: () => void;
|
|
18
|
+
addOnly?: boolean;
|
|
19
|
+
side?: 'bottom' | 'top' | 'right';
|
|
20
|
+
selectedId?: WorkspaceId;
|
|
21
|
+
}
|
|
22
|
+
/** Render the workspace target menu and directory picking conversation. */
|
|
23
|
+
export declare function WorkspacePickFlow({ t, open, anchorRef, useWorkspaces, createWorkspace, useDirectoryFlow, renderDirectoryFlow, onPick, onClose, addOnly, side, selectedId, }: FlowProps): import("react").JSX.Element;
|
|
24
|
+
/** Fill the conversation hero's workspace picker seat. */
|
|
25
|
+
export declare function WorkspacePicker({ open, anchorRef, useWorkspaces, selectedId, onPick, onClose, createWorkspace, useDirectoryFlow, renderSlot, t, }: PickerProps): import("react").JSX.Element;
|
|
26
|
+
export {};
|
|
@@ -1,76 +1,88 @@
|
|
|
1
|
-
/** Slot contracts and injected Host actions for ya-workspace-sidebar. */
|
|
2
|
-
import type { HostObservable, PropsLocale, PropsRenderSlots, PropsRuntime, SnapshotSelectorHook } from '@deepseek-ai/dsh-client-ui-slots';
|
|
3
|
-
import type { SessionSearchResultItem } from '@deepseek-ai/dsh-api-session-controller/client';
|
|
4
|
-
import type { WorkspaceId, WorkspaceSnapshot, WorkspaceView } from '@deepseek-ai/dsh-api-workspace-controller/client';
|
|
5
|
-
import type { SessionId } from '@deepseek-ai/dsh-session/types';
|
|
6
|
-
import type { YaWorkspaceKey } from './locales.ts';
|
|
7
|
-
/** Directory-picker conversation owned by each trigger surface. */
|
|
8
|
-
export interface DirectoryFlowOwnerProps {
|
|
9
|
-
open: boolean;
|
|
10
|
-
busy: boolean;
|
|
11
|
-
onPicked: (path: string) => void;
|
|
12
|
-
onCancel: () => void;
|
|
13
|
-
onError: (message: string) => void;
|
|
14
|
-
}
|
|
15
|
-
declare module '@deepseek-ai/dsh-client-ui-slots' {
|
|
16
|
-
/** The official ui-workspace provider is disabled by this bundle's patch,
|
|
17
|
-
* so this plugin owns the global workspaces selector hook (bound from the
|
|
18
|
-
* Workspace Controller snapshot this apply installs via slots.provideRoot). */
|
|
19
|
-
interface GlobalStandardProps {
|
|
20
|
-
useWorkspaces: SnapshotSelectorHook<WorkspaceSnapshot>;
|
|
21
|
-
}
|
|
22
|
-
interface SlotMap {
|
|
23
|
-
'sidebar.workspaces.directoryFlow': {
|
|
24
|
-
kind: 'single';
|
|
25
|
-
scope: 'root';
|
|
26
|
-
owner: DirectoryFlowOwnerProps;
|
|
27
|
-
};
|
|
28
|
-
'conversation.hero.workspace.directoryFlow': {
|
|
29
|
-
kind: 'single';
|
|
30
|
-
scope: 'root';
|
|
31
|
-
owner: DirectoryFlowOwnerProps;
|
|
32
|
-
};
|
|
33
|
-
}
|
|
34
|
-
interface LocaleNamespaceMap {
|
|
35
|
-
'ya-workspace-sidebar': YaWorkspaceKey;
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
/** Shared directory-flow occupancy source. */
|
|
39
|
-
export interface DirectoryInjected {
|
|
40
|
-
hooks: {
|
|
41
|
-
directoryFlow: HostObservable<boolean>;
|
|
42
|
-
};
|
|
43
|
-
}
|
|
44
|
-
/**
|
|
45
|
-
export
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
1
|
+
/** Slot contracts and injected Host actions for ya-workspace-sidebar. */
|
|
2
|
+
import type { HostObservable, PropsLocale, PropsRenderSlots, PropsRuntime, SnapshotSelectorHook } from '@deepseek-ai/dsh-client-ui-slots';
|
|
3
|
+
import type { SessionSearchResultItem } from '@deepseek-ai/dsh-api-session-controller/client';
|
|
4
|
+
import type { WorkspaceId, WorkspaceSnapshot, WorkspaceView } from '@deepseek-ai/dsh-api-workspace-controller/client';
|
|
5
|
+
import type { SessionId } from '@deepseek-ai/dsh-session/types';
|
|
6
|
+
import type { YaWorkspaceKey } from './locales.ts';
|
|
7
|
+
/** Directory-picker conversation owned by each trigger surface. */
|
|
8
|
+
export interface DirectoryFlowOwnerProps {
|
|
9
|
+
open: boolean;
|
|
10
|
+
busy: boolean;
|
|
11
|
+
onPicked: (path: string) => void;
|
|
12
|
+
onCancel: () => void;
|
|
13
|
+
onError: (message: string) => void;
|
|
14
|
+
}
|
|
15
|
+
declare module '@deepseek-ai/dsh-client-ui-slots' {
|
|
16
|
+
/** The official ui-workspace provider is disabled by this bundle's patch,
|
|
17
|
+
* so this plugin owns the global workspaces selector hook (bound from the
|
|
18
|
+
* Workspace Controller snapshot this apply installs via slots.provideRoot). */
|
|
19
|
+
interface GlobalStandardProps {
|
|
20
|
+
useWorkspaces: SnapshotSelectorHook<WorkspaceSnapshot>;
|
|
21
|
+
}
|
|
22
|
+
interface SlotMap {
|
|
23
|
+
'sidebar.workspaces.directoryFlow': {
|
|
24
|
+
kind: 'single';
|
|
25
|
+
scope: 'root';
|
|
26
|
+
owner: DirectoryFlowOwnerProps;
|
|
27
|
+
};
|
|
28
|
+
'conversation.hero.workspace.directoryFlow': {
|
|
29
|
+
kind: 'single';
|
|
30
|
+
scope: 'root';
|
|
31
|
+
owner: DirectoryFlowOwnerProps;
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
interface LocaleNamespaceMap {
|
|
35
|
+
'ya-workspace-sidebar': YaWorkspaceKey;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/** Shared directory-flow occupancy source. */
|
|
39
|
+
export interface DirectoryInjected {
|
|
40
|
+
hooks: {
|
|
41
|
+
directoryFlow: HostObservable<boolean>;
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
/** Host-probed jsonl storage layout of one session (null = absent/unavailable). */
|
|
45
|
+
export interface SessionPaths {
|
|
46
|
+
/** Session storage directory, or null on sqlite-backed/unflushed deployments. */
|
|
47
|
+
dir: string | null;
|
|
48
|
+
/** Highest-generation log file inside the directory, or null when none exists. */
|
|
49
|
+
log: string | null;
|
|
50
|
+
}
|
|
51
|
+
/** Browser-private Host operations. */
|
|
52
|
+
export type SidebarInjected = DirectoryInjected & {
|
|
53
|
+
startSession: (workspaceId?: WorkspaceId) => void;
|
|
54
|
+
open: (sessionId: SessionId) => void;
|
|
55
|
+
searchSessions: (query: string, signal: AbortSignal) => Promise<{
|
|
56
|
+
items: readonly SessionSearchResultItem[];
|
|
57
|
+
hasMore: boolean;
|
|
58
|
+
}>;
|
|
59
|
+
searchResultLimit: number;
|
|
60
|
+
renameSession: (sessionId: SessionId, title: string) => Promise<void>;
|
|
61
|
+
forkSession: (sessionId: SessionId) => void;
|
|
62
|
+
renameWorkspace: (workspaceId: WorkspaceId, title: string) => Promise<void>;
|
|
63
|
+
deleteWorkspace: (workspaceId: WorkspaceId) => Promise<void>;
|
|
64
|
+
archiveSession: (sessionId: SessionId) => Promise<void>;
|
|
65
|
+
insertSessionBefore: (workspaceId: WorkspaceId, sessionId: SessionId, beforeSessionId?: SessionId) => Promise<void>;
|
|
66
|
+
createWorkspace: (input: {
|
|
67
|
+
path: string;
|
|
68
|
+
}) => Promise<WorkspaceView>;
|
|
69
|
+
resolveSessionPaths: (input: {
|
|
70
|
+
sessionId: SessionId;
|
|
71
|
+
cwd?: string;
|
|
72
|
+
}) => Promise<SessionPaths>;
|
|
73
|
+
revealInExplorer: (cwd: string) => Promise<void>;
|
|
74
|
+
};
|
|
75
|
+
/** Full sidebar component props. */
|
|
76
|
+
export type SidebarProps = PropsRuntime<'sidebar.workspaces'> & PropsRenderSlots<'sidebar.workspaces.directoryFlow'> & Omit<SidebarInjected, 'hooks'> & {
|
|
77
|
+
useDirectoryFlow: SnapshotSelectorHook<boolean>;
|
|
78
|
+
} & PropsLocale<'ya-workspace-sidebar'>;
|
|
79
|
+
/** Conversation hero picker operations. */
|
|
80
|
+
export type PickerInjected = DirectoryInjected & {
|
|
81
|
+
createWorkspace: (input: {
|
|
82
|
+
path: string;
|
|
83
|
+
}) => Promise<WorkspaceView>;
|
|
84
|
+
};
|
|
85
|
+
/** Full conversation hero picker props. */
|
|
86
|
+
export type PickerProps = PropsRuntime<'conversation.hero.workspace'> & PropsRenderSlots<'conversation.hero.workspace.directoryFlow'> & Omit<PickerInjected, 'hooks'> & {
|
|
87
|
+
useDirectoryFlow: SnapshotSelectorHook<boolean>;
|
|
88
|
+
} & PropsLocale<'ya-workspace-sidebar'>;
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
-
/** 19-language override dictionaries for the better-locale store.
|
|
1
|
+
/** 19-language override dictionaries for the better-locale store.
|
|
2
|
+
*
|
|
3
|
+
* Partial on purpose: an override language covers the keys it translated and
|
|
4
|
+
* falls back to the primary dictionary (en) for the rest, so new keys ship
|
|
5
|
+
* without forcing translations of every language up front.
|
|
6
|
+
*/
|
|
2
7
|
import type { YaWorkspaceKey } from './locales.ts';
|
|
3
|
-
export declare const dicts: Record<string, Record<YaWorkspaceKey, string
|
|
8
|
+
export declare const dicts: Record<string, Partial<Record<YaWorkspaceKey, string>>>;
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
/** Client assembly for the replacement workspace sidebar and hero picker. */
|
|
2
|
-
import type { Context } from '@deepseek-ai/cordis';
|
|
3
|
-
/**
|
|
4
|
-
* Services required by the replacement client entries. `remote` and
|
|
5
|
-
* `remote.directoryPicker` feed the uiWorkspace stand-in's directory picking.
|
|
6
|
-
*/
|
|
7
|
-
export declare const inject: string[];
|
|
8
|
-
/** Register the sidebar browser and conversation hero picker. */
|
|
9
|
-
export declare function apply(ctx: Context): void;
|
|
1
|
+
/** Client assembly for the replacement workspace sidebar and hero picker. */
|
|
2
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
3
|
+
/**
|
|
4
|
+
* Services required by the replacement client entries. `remote` and
|
|
5
|
+
* `remote.directoryPicker` feed the uiWorkspace stand-in's directory picking.
|
|
6
|
+
*/
|
|
7
|
+
export declare const inject: string[];
|
|
8
|
+
/** Register the sidebar browser and conversation hero picker. */
|
|
9
|
+
export declare function apply(ctx: Context): void;
|