@tobycaimf/dsh-archived-sessions 1.0.0 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +12 -1
- package/README.md +12 -1
- package/assets/screenshot-archived-sessions.png +0 -0
- package/assets/screenshot-batch-mode.png +0 -0
- package/lib/client.js +256 -0
- package/lib/client.js.map +2 -2
- package/lib/index.js +188 -1
- package/lib/index.js.map +2 -2
- package/package.json +2 -2
- package/src/client/index.jsx +280 -0
- package/src/index.js +188 -6
package/lib/index.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
// src/index.js
|
|
2
|
-
import { unlink } from "node:fs/promises";
|
|
2
|
+
import { mkdir, realpath, rename, unlink } from "node:fs/promises";
|
|
3
|
+
import { basename, isAbsolute, join } from "node:path";
|
|
4
|
+
import { homedir } from "node:os";
|
|
3
5
|
var name = "dsh-archived-sessions";
|
|
4
6
|
var inject = ["webServer", "workspaceRegistry", "sessionPersistence", "sessionQuery", "storageDomain"];
|
|
5
7
|
var MAX_TITLE = 80;
|
|
@@ -160,6 +162,153 @@ function apply(ctx) {
|
|
|
160
162
|
}
|
|
161
163
|
return { ok: true, deleted: true, removedPath };
|
|
162
164
|
}
|
|
165
|
+
async function moveTargetWorkspace(rawPath) {
|
|
166
|
+
if (typeof rawPath !== "string" || !rawPath.trim()) throw new Error("\u7F3A\u5C11\u76EE\u6807\u5DE5\u4F5C\u533A\u8DEF\u5F84");
|
|
167
|
+
let p = String(rawPath).trim();
|
|
168
|
+
if (p.startsWith("~/")) p = join(homedir(), p.slice(2));
|
|
169
|
+
if (!isAbsolute(p)) p = join(homedir(), p);
|
|
170
|
+
let canonical = null;
|
|
171
|
+
try {
|
|
172
|
+
canonical = await realpath(p);
|
|
173
|
+
} catch (e) {
|
|
174
|
+
canonical = null;
|
|
175
|
+
}
|
|
176
|
+
if (canonical === null) {
|
|
177
|
+
await mkdir(p, { recursive: true });
|
|
178
|
+
canonical = await realpath(p);
|
|
179
|
+
}
|
|
180
|
+
return { canonical, entity: await w.create(canonical, basename(canonical) || "workspace") };
|
|
181
|
+
}
|
|
182
|
+
async function moveOne(sid, targetPath) {
|
|
183
|
+
const sessions = ctx.get("sessions");
|
|
184
|
+
if (sessions && sessions.get(sid)) {
|
|
185
|
+
throw new Error("\u8BE5\u4F1A\u8BDD\u5F53\u524D\u5904\u4E8E\u6253\u5F00\u72B6\u6001\uFF0C\u8BF7\u5148\u5207\u6362\u5230\u522B\u7684\u4F1A\u8BDD\u518D\u79FB\u52A8\u3002");
|
|
186
|
+
}
|
|
187
|
+
const r = await sp.readFrom(sid, 0);
|
|
188
|
+
if (!r || !r.meta) throw new Error("\u65E0\u6CD5\u8BFB\u53D6\u8BE5\u4F1A\u8BDD\u7684\u65E5\u5FD7");
|
|
189
|
+
const meta = r.meta;
|
|
190
|
+
const events = r.events;
|
|
191
|
+
const oldCwd = meta.cwd || null;
|
|
192
|
+
const { canonical, entity: target } = await moveTargetWorkspace(targetPath);
|
|
193
|
+
if (oldCwd) {
|
|
194
|
+
let oldCanon = null;
|
|
195
|
+
try {
|
|
196
|
+
oldCanon = await realpath(oldCwd);
|
|
197
|
+
} catch (e) {
|
|
198
|
+
oldCanon = null;
|
|
199
|
+
}
|
|
200
|
+
if (oldCanon === canonical) {
|
|
201
|
+
return { ok: true, already: true, workspaceId: target.id, workspaceTitle: target.title };
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
const newHeader = Object.assign({}, meta, { cwd: canonical });
|
|
205
|
+
let oldPath = null;
|
|
206
|
+
let backupPath = null;
|
|
207
|
+
try {
|
|
208
|
+
const loc = sp.locate(meta);
|
|
209
|
+
if (loc && typeof loc.path === "string" && loc.path) oldPath = loc.path;
|
|
210
|
+
} catch (e) {
|
|
211
|
+
oldPath = null;
|
|
212
|
+
}
|
|
213
|
+
if (oldPath) {
|
|
214
|
+
backupPath = `${oldPath}.move-backup-${Date.now()}`;
|
|
215
|
+
try {
|
|
216
|
+
await rename(oldPath, backupPath);
|
|
217
|
+
} catch (e) {
|
|
218
|
+
if (e && e.code !== "ENOENT") throw new Error("\u79FB\u52A8\u5931\u8D25\uFF1A\u65E0\u6CD5\u5907\u4EFD\u65E7\u7684\u4F1A\u8BDD\u65E5\u5FD7");
|
|
219
|
+
backupPath = null;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
const restore = async () => {
|
|
223
|
+
if (backupPath && oldPath) {
|
|
224
|
+
try {
|
|
225
|
+
await rename(backupPath, oldPath);
|
|
226
|
+
} catch (e) {
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
if (typeof sp.create !== "function" || typeof sp.append !== "function") {
|
|
231
|
+
await restore();
|
|
232
|
+
throw new Error("\u5F53\u524D\u4F1A\u8BDD\u5B58\u50A8\u540E\u7AEF\u4E0D\u652F\u6301\u5B89\u5168\u79FB\u52A8\uFF0C\u5DF2\u4E2D\u6B62\u3002");
|
|
233
|
+
}
|
|
234
|
+
try {
|
|
235
|
+
await sp.create(newHeader);
|
|
236
|
+
await sp.append(sid, events);
|
|
237
|
+
const check = await sp.readFrom(sid, 0);
|
|
238
|
+
if (!check || !check.meta || check.meta.cwd !== canonical) {
|
|
239
|
+
throw new Error("\u79FB\u52A8\u540E\u6821\u9A8C\u5931\u8D25\uFF1A\u4F1A\u8BDD\u5DE5\u4F5C\u76EE\u5F55\u672A\u6B63\u786E\u66F4\u65B0");
|
|
240
|
+
}
|
|
241
|
+
} catch (e) {
|
|
242
|
+
await restore();
|
|
243
|
+
throw new Error("\u79FB\u52A8\u4F1A\u8BDD\u65E5\u5FD7\u5931\u8D25\uFF1A" + String(e && e.message || e));
|
|
244
|
+
}
|
|
245
|
+
if (backupPath) {
|
|
246
|
+
try {
|
|
247
|
+
await unlink(backupPath);
|
|
248
|
+
} catch (e) {
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
for (const ent of w.list()) {
|
|
252
|
+
try {
|
|
253
|
+
await ent.detachSession(sid);
|
|
254
|
+
} catch (e) {
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
if (w.headers && typeof w.headers.set === "function") w.headers.set(sid, newHeader);
|
|
258
|
+
if (w.sessionPaths && typeof w.sessionPaths.set === "function") w.sessionPaths.set(sid, canonical);
|
|
259
|
+
await target.attachSession(sid);
|
|
260
|
+
return {
|
|
261
|
+
ok: true,
|
|
262
|
+
moved: true,
|
|
263
|
+
workspaceId: target.id,
|
|
264
|
+
workspaceTitle: target.title,
|
|
265
|
+
workspacePath: canonical
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
async function listWorkspaces() {
|
|
269
|
+
const out = [];
|
|
270
|
+
try {
|
|
271
|
+
for (const ent of w.list()) out.push({ workspaceId: ent.id, title: ent.title, path: ent.path });
|
|
272
|
+
} catch (e) {
|
|
273
|
+
}
|
|
274
|
+
return out;
|
|
275
|
+
}
|
|
276
|
+
async function allSessionItems() {
|
|
277
|
+
let materialized = /* @__PURE__ */ new Set();
|
|
278
|
+
let live = ctx.get("sessions");
|
|
279
|
+
try {
|
|
280
|
+
const headers = await sp.list();
|
|
281
|
+
materialized = new Set(headers.map((h) => String(h.id)));
|
|
282
|
+
} catch (e) {
|
|
283
|
+
}
|
|
284
|
+
const ids = [];
|
|
285
|
+
try {
|
|
286
|
+
for (const header of await sp.list()) ids.push(String(header.id));
|
|
287
|
+
} catch (e) {
|
|
288
|
+
}
|
|
289
|
+
if (live) {
|
|
290
|
+
try {
|
|
291
|
+
live.list().forEach((s) => {
|
|
292
|
+
if (!ids.includes(String(s.id))) ids.push(String(s.id));
|
|
293
|
+
});
|
|
294
|
+
} catch (e) {
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
wsByPath = {};
|
|
298
|
+
try {
|
|
299
|
+
for (const ent of w.list()) wsByPath[ent.path] = ent;
|
|
300
|
+
} catch (e) {
|
|
301
|
+
wsByPath = {};
|
|
302
|
+
}
|
|
303
|
+
const currentArchived = new Set((await archivedState().catch(() => ({ archivedSessionIds: [] }))).archivedSessionIds || []);
|
|
304
|
+
const items = [];
|
|
305
|
+
const CHUNK = 6;
|
|
306
|
+
for (let i = 0; i < ids.length; i += CHUNK) {
|
|
307
|
+
const res2 = await Promise.all(ids.slice(i, i + CHUNK).map(resolveOne));
|
|
308
|
+
for (const it of res2) items.push({ ...it, archived: currentArchived.has(it.sessionId) });
|
|
309
|
+
}
|
|
310
|
+
return items;
|
|
311
|
+
}
|
|
163
312
|
ctx.effect(() => {
|
|
164
313
|
const disposers = [];
|
|
165
314
|
disposers.push(ctx.webServer.register({
|
|
@@ -267,6 +416,44 @@ function apply(ctx) {
|
|
|
267
416
|
}
|
|
268
417
|
}
|
|
269
418
|
}));
|
|
419
|
+
disposers.push(ctx.webServer.register({
|
|
420
|
+
kind: "exact",
|
|
421
|
+
path: "/archived-sessions/sessions",
|
|
422
|
+
handler: async (req, res) => {
|
|
423
|
+
try {
|
|
424
|
+
json(res, { items: await allSessionItems() });
|
|
425
|
+
} catch (e) {
|
|
426
|
+
json(res, { error: String(e && e.message || e) }, 500);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
}));
|
|
430
|
+
disposers.push(ctx.webServer.register({
|
|
431
|
+
kind: "exact",
|
|
432
|
+
path: "/archived-sessions/workspaces",
|
|
433
|
+
handler: async (req, res) => {
|
|
434
|
+
try {
|
|
435
|
+
json(res, { items: await listWorkspaces() });
|
|
436
|
+
} catch (e) {
|
|
437
|
+
json(res, { error: String(e && e.message || e) }, 500);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
}));
|
|
441
|
+
disposers.push(ctx.webServer.register({
|
|
442
|
+
kind: "exact",
|
|
443
|
+
path: "/archived-sessions/move",
|
|
444
|
+
handler: async (req, res) => {
|
|
445
|
+
try {
|
|
446
|
+
const body = await readJsonBody(req);
|
|
447
|
+
const sid = body && typeof body.sessionId === "string" ? body.sessionId : null;
|
|
448
|
+
const target = body && typeof body.targetPath === "string" ? body.targetPath : null;
|
|
449
|
+
if (!sid) return json(res, { ok: false, error: "missing sessionId" }, 400);
|
|
450
|
+
if (!target) return json(res, { ok: false, error: "missing targetPath" }, 400);
|
|
451
|
+
json(res, { sessionId: sid, ...await moveOne(sid, target) });
|
|
452
|
+
} catch (e) {
|
|
453
|
+
json(res, { ok: false, error: String(e && e.message || e) }, 500);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
}));
|
|
270
457
|
return () => {
|
|
271
458
|
for (const d of disposers) d();
|
|
272
459
|
};
|
package/lib/index.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/index.js"],
|
|
4
|
-
"sourcesContent": ["// dsh-archived-sessions \u2014 host half.\n//\n// Serves /archived-sessions/* JSON routes (list / restore / restore-many /\n// delete / delete-many) over the host `webServer`. The browser Settings\n// section (\"\u5F52\u6863\u4F1A\u8BDD\") talks to these. Reads/writes the durable workspace\n// archive set (workspaceRegistry + storageDomain), folds titles/dates/workspace\n// tags from session persistence, and physically removes a session's log file\n// on delete.\nimport { unlink } from 'node:fs/promises'\n\nexport const name = 'dsh-archived-sessions'\nexport const inject = ['webServer', 'workspaceRegistry', 'sessionPersistence', 'sessionQuery', 'storageDomain']\n\nconst MAX_TITLE = 80\n\nfunction json(res, value, status = 200) {\n res.writeHead(status, { 'content-type': 'application/json; charset=utf-8', 'cache-control': 'no-store' })\n res.end(JSON.stringify(value))\n}\n\nasync function readJsonBody(req) {\n const chunks = []\n let total = 0\n for await (const chunk of req) {\n chunks.push(chunk)\n total += chunk.length\n if (total > 1 << 20) return null\n }\n try {\n return JSON.parse(Buffer.concat(chunks).toString('utf8'))\n } catch {\n return null\n }\n}\n\nfunction parseIds(body) {\n const raw = body && body.sessionIds\n if (!Array.isArray(raw)) return null\n const ids = []\n for (const v of raw) if (typeof v === 'string' && v) ids.push(v)\n return ids\n}\n\nfunction foldTitle(events) {\n let found = null\n let firstUser = null\n for (const ev of events) {\n if (ev.type === 'session/title' && ev.data && typeof ev.data.title === 'string' && ev.data.title.length) {\n found = ev.data.title\n }\n if (firstUser === null && ev.type === 'user/message' && ev.data && Array.isArray(ev.data.content)) {\n const txt = ev.data.content.filter((b) => b && b.type === 'text').map((b) => b.text).filter(Boolean).join(' ').trim()\n if (txt) firstUser = txt\n }\n }\n return found || firstUser || null\n}\n\nexport function apply(ctx) {\n const w = ctx.workspaceRegistry\n const sp = ctx.sessionPersistence\n const sq = ctx.sessionQuery\n const dom = () => ctx.storageDomain.get('workspace')\n\n async function archivedState() {\n const d = dom()\n if (!d) throw new Error('workspace domain is not open')\n return d.global.get()\n }\n\n async function writeArchived(nextIds) {\n const d = dom()\n if (!d) throw new Error('workspace domain is not open')\n const cur = d.global.get()\n const next = Object.assign({}, cur, { archivedSessionIds: nextIds })\n await d.global.set(next)\n // Keep the registry's in-memory cache in sync so the live sidebar refreshes.\n if (w && 'state' in w) { try { w.state = next } catch (e) { /* best-effort */ } }\n return next\n }\n\n let wsByPath = {}\n\n async function resolveOne(id) {\n let title = null, createdAt = null, cwd = null\n try {\n const o = await sq.readTitleSnapshot(id)\n if (o) {\n if (o.title && o.title.title) title = String(o.title.title)\n if (o.session) { cwd = o.session.cwd || null; createdAt = o.session.createdAt || null }\n }\n } catch (e) { /* fall back to raw log */ }\n if (!title || !cwd) {\n try {\n const r = await sp.readFrom(id, 0)\n if (r.meta) {\n if (!cwd) cwd = r.meta.cwd || null\n if (!createdAt) createdAt = r.meta.createdAt || null\n }\n if (!title && Array.isArray(r.events)) title = foldTitle(r.events)\n } catch (e2) { /* keep what we have */ }\n }\n const ws = cwd ? wsByPath[cwd] : undefined\n const workspaceGone = !!(cwd && !ws)\n const display = title ? (String(title).length > MAX_TITLE ? String(title).slice(0, MAX_TITLE) + '\u2026' : String(title)) : null\n return {\n sessionId: id,\n title: display,\n createdAt: createdAt || null,\n workspacePath: cwd || null,\n workspaceTitle: (ws && ws.title) ? ws.title : null,\n workspaceGone: workspaceGone ? true : false,\n hasWorkspace: !!cwd,\n }\n }\n\n // Restore (unarchive) one session; throws on failure.\n async function restoreOne(sid) {\n const state = await archivedState()\n const list = state.archivedSessionIds.map(String)\n if (!list.includes(sid)) return { ok: true, restored: false }\n await writeArchived(list.filter((x) => x !== sid))\n return { ok: true, restored: true }\n }\n\n // Physically delete one session log + clear archive/workspace accounting;\n // throws on failure (live sessions are rejected).\n async function deleteOne(sid) {\n const sessions = ctx.get('sessions')\n if (sessions && sessions.get(sid)) {\n throw new Error('\u8BE5\u4F1A\u8BDD\u5F53\u524D\u5904\u4E8E\u6253\u5F00\u72B6\u6001\uFF0C\u8BF7\u5148\u5207\u6362\u5230\u522B\u7684\u4F1A\u8BDD\u518D\u5220\u9664\u3002')\n }\n let removedPath = null\n try {\n const headers = await sp.list()\n const header = headers.find((h) => String(h.id) === sid)\n if (header) {\n const loc = sp.locate(header)\n if (loc && typeof loc.path === 'string') removedPath = loc.path\n } else {\n // Session not in the materialized list \u2014 use its header cwd to locate the log anyway.\n const r = await sp.readFrom(sid, 0)\n if (r && r.meta && r.meta.cwd) {\n const loc = sp.locate({ id: sid, cwd: r.meta.cwd })\n if (loc && typeof loc.path === 'string') removedPath = loc.path\n }\n }\n } catch (e) { /* best-effort */ }\n if (removedPath) {\n try {\n await unlink(removedPath)\n } catch (e) {\n if (e && e.code !== 'ENOENT') throw new Error('\u5220\u9664\u65E5\u5FD7\u6587\u4EF6\u5931\u8D25\uFF1A' + String((e && e.message) || e))\n }\n }\n try {\n for (const ent of w.list()) {\n if (ent.sessionIds.includes(sid)) { try { await ent.detachSession(sid) } catch (e) { /* ignore */ } }\n }\n } catch (e) { /* ignore */ }\n try { if (w.sessionPaths && w.sessionPaths.delete) w.sessionPaths.delete(sid) } catch (e) {}\n try { if (w.headers && w.headers.delete) w.headers.delete(sid) } catch (e) {}\n // NOTE: intentionally do NOT unarchive (keep the id in the archive set).\n // Removing it from the archive set would make DSH re-show the session in the\n // sidebar; with its workspace gone it would land in \u672A\u5206\u7EC4 until the index\n // rebuilds. Keeping it hidden keeps the deleted session out of every list\n // immediately. The stale hidden id is inert (no log -> no session resolves).\n return { ok: true, deleted: true, removedPath }\n }\n\n ctx.effect(() => {\n const disposers = []\n\n disposers.push(ctx.webServer.register({\n kind: 'exact',\n path: '/archived-sessions/list',\n handler: async (req, res) => {\n try {\n const state = await archivedState()\n const ids = state.archivedSessionIds || []\n // Only surface archived ids that still exist (materialized log or live).\n // Deleted sessions keep a hidden archive id but no log, so they drop out here.\n let materialized = new Set()\n let live = ctx.get('sessions')\n try {\n const headers = await sp.list()\n materialized = new Set(headers.map((h) => String(h.id)))\n } catch (e) { /* best-effort */ }\n const idStrs = ids.map(String).filter((id) => materialized.has(id) || (live && live.get(id)))\n wsByPath = {}\n try { for (const ent of w.list()) wsByPath[ent.path] = ent } catch (e) { wsByPath = {} }\n const items = []\n const CHUNK = 6\n for (let i = 0; i < idStrs.length; i += CHUNK) {\n const res2 = await Promise.all(idStrs.slice(i, i + CHUNK).map(resolveOne))\n items.push.apply(items, res2)\n }\n json(res, { items })\n } catch (e) {\n json(res, { error: String((e && e.message) || e) }, 500)\n }\n },\n }))\n\n disposers.push(ctx.webServer.register({\n kind: 'exact',\n path: '/archived-sessions/restore',\n handler: async (req, res) => {\n try {\n const body = await readJsonBody(req)\n const sid = body && typeof body.sessionId === 'string' ? body.sessionId : null\n if (!sid) return json(res, { ok: false, error: 'missing sessionId' }, 400)\n json(res, await restoreOne(sid))\n } catch (e) {\n json(res, { ok: false, error: String((e && e.message) || e) }, 500)\n }\n },\n }))\n\n disposers.push(ctx.webServer.register({\n kind: 'exact',\n path: '/archived-sessions/restore-many',\n handler: async (req, res) => {\n try {\n const body = await readJsonBody(req)\n const ids = parseIds(body)\n if (!ids || ids.length === 0) return json(res, { ok: false, error: 'missing sessionIds' }, 400)\n const results = []\n for (const sid of ids) {\n try { results.push({ sessionId: sid, ok: true, ...(await restoreOne(sid)) }) }\n catch (e) { results.push({ sessionId: sid, ok: false, error: String((e && e.message) || e) }) }\n }\n json(res, { ok: true, restored: results.filter((r) => r.ok).length, results })\n } catch (e) {\n json(res, { ok: false, error: String((e && e.message) || e) }, 500)\n }\n },\n }))\n\n disposers.push(ctx.webServer.register({\n kind: 'exact',\n path: '/archived-sessions/delete',\n handler: async (req, res) => {\n try {\n const body = await readJsonBody(req)\n const sid = body && typeof body.sessionId === 'string' ? body.sessionId : null\n if (!sid) return json(res, { ok: false, error: 'missing sessionId' }, 400)\n json(res, await deleteOne(sid))\n } catch (e) {\n json(res, { ok: false, error: String((e && e.message) || e) }, 500)\n }\n },\n }))\n\n disposers.push(ctx.webServer.register({\n kind: 'exact',\n path: '/archived-sessions/delete-many',\n handler: async (req, res) => {\n try {\n const body = await readJsonBody(req)\n const ids = parseIds(body)\n if (!ids || ids.length === 0) return json(res, { ok: false, error: 'missing sessionIds' }, 400)\n const results = []\n for (const sid of ids) {\n try { results.push({ sessionId: sid, ok: true, ...(await deleteOne(sid)) }) }\n catch (e) { results.push({ sessionId: sid, ok: false, error: String((e && e.message) || e) }) }\n }\n json(res, { ok: true, deleted: results.filter((r) => r.ok).length, results })\n } catch (e) {\n json(res, { ok: false, error: String((e && e.message) || e) }, 500)\n }\n },\n }))\n\n return () => { for (const d of disposers) d() }\n }, 'dsh-archived-sessions: routes')\n}\n"],
|
|
5
|
-
"mappings": "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",
|
|
4
|
+
"sourcesContent": ["// dsh-archived-sessions \u2014 host half.\n//\n// Serves /archived-sessions/* JSON routes (list / restore / restore-many /\n// delete / delete-many / sessions / workspaces / move) over the host\n// `webServer`. The browser Settings sections (\"\u5F52\u6863\u4F1A\u8BDD\" & \"\u79FB\u52A8\u4F1A\u8BDD\") talk to\n// these. Reads/writes the durable workspace archive set\n// (workspaceRegistry + storageDomain), folds titles/dates/workspace tags from\n// session persistence, physically removes a session's log file on delete, and\n// relocates a conversation (session) between workspaces on move.\nimport { mkdir, realpath, rename, unlink } from 'node:fs/promises'\nimport { basename, isAbsolute, join } from 'node:path'\nimport { homedir } from 'node:os'\n\nexport const name = 'dsh-archived-sessions'\nexport const inject = ['webServer', 'workspaceRegistry', 'sessionPersistence', 'sessionQuery', 'storageDomain']\n\nconst MAX_TITLE = 80\n\nfunction json(res, value, status = 200) {\n res.writeHead(status, { 'content-type': 'application/json; charset=utf-8', 'cache-control': 'no-store' })\n res.end(JSON.stringify(value))\n}\n\nasync function readJsonBody(req) {\n const chunks = []\n let total = 0\n for await (const chunk of req) {\n chunks.push(chunk)\n total += chunk.length\n if (total > 1 << 20) return null\n }\n try {\n return JSON.parse(Buffer.concat(chunks).toString('utf8'))\n } catch {\n return null\n }\n}\n\nfunction parseIds(body) {\n const raw = body && body.sessionIds\n if (!Array.isArray(raw)) return null\n const ids = []\n for (const v of raw) if (typeof v === 'string' && v) ids.push(v)\n return ids\n}\n\nfunction foldTitle(events) {\n let found = null\n let firstUser = null\n for (const ev of events) {\n if (ev.type === 'session/title' && ev.data && typeof ev.data.title === 'string' && ev.data.title.length) {\n found = ev.data.title\n }\n if (firstUser === null && ev.type === 'user/message' && ev.data && Array.isArray(ev.data.content)) {\n const txt = ev.data.content.filter((b) => b && b.type === 'text').map((b) => b.text).filter(Boolean).join(' ').trim()\n if (txt) firstUser = txt\n }\n }\n return found || firstUser || null\n}\n\nexport function apply(ctx) {\n const w = ctx.workspaceRegistry\n const sp = ctx.sessionPersistence\n const sq = ctx.sessionQuery\n const dom = () => ctx.storageDomain.get('workspace')\n\n async function archivedState() {\n const d = dom()\n if (!d) throw new Error('workspace domain is not open')\n return d.global.get()\n }\n\n async function writeArchived(nextIds) {\n const d = dom()\n if (!d) throw new Error('workspace domain is not open')\n const cur = d.global.get()\n const next = Object.assign({}, cur, { archivedSessionIds: nextIds })\n await d.global.set(next)\n // Keep the registry's in-memory cache in sync so the live sidebar refreshes.\n if (w && 'state' in w) { try { w.state = next } catch (e) { /* best-effort */ } }\n return next\n }\n\n let wsByPath = {}\n\n async function resolveOne(id) {\n let title = null, createdAt = null, cwd = null\n try {\n const o = await sq.readTitleSnapshot(id)\n if (o) {\n if (o.title && o.title.title) title = String(o.title.title)\n if (o.session) { cwd = o.session.cwd || null; createdAt = o.session.createdAt || null }\n }\n } catch (e) { /* fall back to raw log */ }\n if (!title || !cwd) {\n try {\n const r = await sp.readFrom(id, 0)\n if (r.meta) {\n if (!cwd) cwd = r.meta.cwd || null\n if (!createdAt) createdAt = r.meta.createdAt || null\n }\n if (!title && Array.isArray(r.events)) title = foldTitle(r.events)\n } catch (e2) { /* keep what we have */ }\n }\n const ws = cwd ? wsByPath[cwd] : undefined\n const workspaceGone = !!(cwd && !ws)\n const display = title ? (String(title).length > MAX_TITLE ? String(title).slice(0, MAX_TITLE) + '\u2026' : String(title)) : null\n return {\n sessionId: id,\n title: display,\n createdAt: createdAt || null,\n workspacePath: cwd || null,\n workspaceTitle: (ws && ws.title) ? ws.title : null,\n workspaceGone: workspaceGone ? true : false,\n hasWorkspace: !!cwd,\n }\n }\n\n // Restore (unarchive) one session; throws on failure.\n async function restoreOne(sid) {\n const state = await archivedState()\n const list = state.archivedSessionIds.map(String)\n if (!list.includes(sid)) return { ok: true, restored: false }\n await writeArchived(list.filter((x) => x !== sid))\n return { ok: true, restored: true }\n }\n\n // Physically delete one session log + clear archive/workspace accounting;\n // throws on failure (live sessions are rejected).\n async function deleteOne(sid) {\n const sessions = ctx.get('sessions')\n if (sessions && sessions.get(sid)) {\n throw new Error('\u8BE5\u4F1A\u8BDD\u5F53\u524D\u5904\u4E8E\u6253\u5F00\u72B6\u6001\uFF0C\u8BF7\u5148\u5207\u6362\u5230\u522B\u7684\u4F1A\u8BDD\u518D\u5220\u9664\u3002')\n }\n let removedPath = null\n try {\n const headers = await sp.list()\n const header = headers.find((h) => String(h.id) === sid)\n if (header) {\n const loc = sp.locate(header)\n if (loc && typeof loc.path === 'string') removedPath = loc.path\n } else {\n // Session not in the materialized list \u2014 use its header cwd to locate the log anyway.\n const r = await sp.readFrom(sid, 0)\n if (r && r.meta && r.meta.cwd) {\n const loc = sp.locate({ id: sid, cwd: r.meta.cwd })\n if (loc && typeof loc.path === 'string') removedPath = loc.path\n }\n }\n } catch (e) { /* best-effort */ }\n if (removedPath) {\n try {\n await unlink(removedPath)\n } catch (e) {\n if (e && e.code !== 'ENOENT') throw new Error('\u5220\u9664\u65E5\u5FD7\u6587\u4EF6\u5931\u8D25\uFF1A' + String((e && e.message) || e))\n }\n }\n try {\n for (const ent of w.list()) {\n if (ent.sessionIds.includes(sid)) { try { await ent.detachSession(sid) } catch (e) { /* ignore */ } }\n }\n } catch (e) { /* ignore */ }\n try { if (w.sessionPaths && w.sessionPaths.delete) w.sessionPaths.delete(sid) } catch (e) {}\n try { if (w.headers && w.headers.delete) w.headers.delete(sid) } catch (e) {}\n // NOTE: intentionally do NOT unarchive (keep the id in the archive set).\n // Removing it from the archive set would make DSH re-show the session in the\n // sidebar; with its workspace gone it would land in \u672A\u5206\u7EC4 until the index\n // rebuilds. Keeping it hidden keeps the deleted session out of every list\n // immediately. The stale hidden id is inert (no log -> no session resolves).\n return { ok: true, deleted: true, removedPath }\n }\n\n // ---- \"move conversation between workspaces\" helper -----------------------\n // DSH binds a conversation to the workspace whose canonical directory path\n // equals the session's stored cwd. Moving it therefore means: (1) adopt the\n // target path as a workspace (create if needed), (2) durably relocate the\n // session's log so its header carries the new cwd, and (3) reassign the\n // workspace membership (detach everywhere, attach to target). The log\n // relocation goes through the persistence service's own encoder (handles the\n // zstd artifact encoding) with a backup + rollback so a failure never leaves\n // the session half-moved.\n\n async function moveTargetWorkspace(rawPath) {\n if (typeof rawPath !== 'string' || !rawPath.trim()) throw new Error('\u7F3A\u5C11\u76EE\u6807\u5DE5\u4F5C\u533A\u8DEF\u5F84')\n let p = String(rawPath).trim()\n if (p.startsWith('~/')) p = join(homedir(), p.slice(2))\n if (!isAbsolute(p)) p = join(homedir(), p)\n let canonical = null\n try { canonical = await realpath(p) } catch (e) { canonical = null }\n if (canonical === null) {\n await mkdir(p, { recursive: true })\n canonical = await realpath(p)\n }\n return { canonical, entity: await w.create(canonical, basename(canonical) || 'workspace') }\n }\n\n async function moveOne(sid, targetPath) {\n const sessions = ctx.get('sessions')\n if (sessions && sessions.get(sid)) {\n throw new Error('\u8BE5\u4F1A\u8BDD\u5F53\u524D\u5904\u4E8E\u6253\u5F00\u72B6\u6001\uFF0C\u8BF7\u5148\u5207\u6362\u5230\u522B\u7684\u4F1A\u8BDD\u518D\u79FB\u52A8\u3002')\n }\n const r = await sp.readFrom(sid, 0)\n if (!r || !r.meta) throw new Error('\u65E0\u6CD5\u8BFB\u53D6\u8BE5\u4F1A\u8BDD\u7684\u65E5\u5FD7')\n const meta = r.meta\n const events = r.events\n const oldCwd = meta.cwd || null\n\n const { canonical, entity: target } = await moveTargetWorkspace(targetPath)\n\n if (oldCwd) {\n let oldCanon = null\n try { oldCanon = await realpath(oldCwd) } catch (e) { oldCanon = null }\n if (oldCanon === canonical) {\n return { ok: true, already: true, workspaceId: target.id, workspaceTitle: target.title }\n }\n }\n\n const newHeader = Object.assign({}, meta, { cwd: canonical })\n\n // 1) Back up the current log artifact before touching anything.\n let oldPath = null\n let backupPath = null\n try {\n const loc = sp.locate(meta)\n if (loc && typeof loc.path === 'string' && loc.path) oldPath = loc.path\n } catch (e) { oldPath = null }\n if (oldPath) {\n backupPath = `${oldPath}.move-backup-${Date.now()}`\n try {\n await rename(oldPath, backupPath)\n } catch (e) {\n if (e && e.code !== 'ENOENT') throw new Error('\u79FB\u52A8\u5931\u8D25\uFF1A\u65E0\u6CD5\u5907\u4EFD\u65E7\u7684\u4F1A\u8BDD\u65E5\u5FD7')\n backupPath = null\n }\n }\n\n const restore = async () => {\n if (backupPath && oldPath) {\n try { await rename(backupPath, oldPath) } catch (e) { /* best-effort */ }\n }\n }\n\n // 2) Relocate the log through the persistence service (re-encodes header).\n if (typeof sp.create !== 'function' || typeof sp.append !== 'function') {\n await restore()\n throw new Error('\u5F53\u524D\u4F1A\u8BDD\u5B58\u50A8\u540E\u7AEF\u4E0D\u652F\u6301\u5B89\u5168\u79FB\u52A8\uFF0C\u5DF2\u4E2D\u6B62\u3002')\n }\n try {\n await sp.create(newHeader)\n await sp.append(sid, events)\n const check = await sp.readFrom(sid, 0)\n if (!check || !check.meta || check.meta.cwd !== canonical) {\n throw new Error('\u79FB\u52A8\u540E\u6821\u9A8C\u5931\u8D25\uFF1A\u4F1A\u8BDD\u5DE5\u4F5C\u76EE\u5F55\u672A\u6B63\u786E\u66F4\u65B0')\n }\n } catch (e) {\n await restore()\n throw new Error('\u79FB\u52A8\u4F1A\u8BDD\u65E5\u5FD7\u5931\u8D25\uFF1A' + String((e && e.message) || e))\n }\n if (backupPath) { try { await unlink(backupPath) } catch (e) { /* best-effort */ } }\n\n // 3) Reassign workspace membership (durable records + in-memory index).\n for (const ent of w.list()) {\n try { await ent.detachSession(sid) } catch (e) { /* ignore */ }\n }\n if (w.headers && typeof w.headers.set === 'function') w.headers.set(sid, newHeader)\n if (w.sessionPaths && typeof w.sessionPaths.set === 'function') w.sessionPaths.set(sid, canonical)\n await target.attachSession(sid)\n\n return {\n ok: true,\n moved: true,\n workspaceId: target.id,\n workspaceTitle: target.title,\n workspacePath: canonical,\n }\n }\n\n async function listWorkspaces() {\n const out = []\n try {\n for (const ent of w.list()) out.push({ workspaceId: ent.id, title: ent.title, path: ent.path })\n } catch (e) { /* ignore */ }\n return out\n }\n\n async function allSessionItems() {\n let materialized = new Set()\n let live = ctx.get('sessions')\n try {\n const headers = await sp.list()\n materialized = new Set(headers.map((h) => String(h.id)))\n } catch (e) { /* best-effort */ }\n const ids = []\n try { for (const header of await sp.list()) ids.push(String(header.id)) } catch (e) { /* ignore */ }\n if (live) { try { live.list().forEach((s) => { if (!ids.includes(String(s.id))) ids.push(String(s.id)) }) } catch (e) { /* ignore */ } }\n wsByPath = {}\n try { for (const ent of w.list()) wsByPath[ent.path] = ent } catch (e) { wsByPath = {} }\n const currentArchived = new Set((await archivedState().catch(() => ({ archivedSessionIds: [] }))).archivedSessionIds || [])\n const items = []\n const CHUNK = 6\n for (let i = 0; i < ids.length; i += CHUNK) {\n const res2 = await Promise.all(ids.slice(i, i + CHUNK).map(resolveOne))\n for (const it of res2) items.push({ ...it, archived: currentArchived.has(it.sessionId) })\n }\n return items\n }\n\n ctx.effect(() => {\n const disposers = []\n\n disposers.push(ctx.webServer.register({\n kind: 'exact',\n path: '/archived-sessions/list',\n handler: async (req, res) => {\n try {\n const state = await archivedState()\n const ids = state.archivedSessionIds || []\n // Only surface archived ids that still exist (materialized log or live).\n // Deleted sessions keep a hidden archive id but no log, so they drop out here.\n let materialized = new Set()\n let live = ctx.get('sessions')\n try {\n const headers = await sp.list()\n materialized = new Set(headers.map((h) => String(h.id)))\n } catch (e) { /* best-effort */ }\n const idStrs = ids.map(String).filter((id) => materialized.has(id) || (live && live.get(id)))\n wsByPath = {}\n try { for (const ent of w.list()) wsByPath[ent.path] = ent } catch (e) { wsByPath = {} }\n const items = []\n const CHUNK = 6\n for (let i = 0; i < idStrs.length; i += CHUNK) {\n const res2 = await Promise.all(idStrs.slice(i, i + CHUNK).map(resolveOne))\n items.push.apply(items, res2)\n }\n json(res, { items })\n } catch (e) {\n json(res, { error: String((e && e.message) || e) }, 500)\n }\n },\n }))\n\n disposers.push(ctx.webServer.register({\n kind: 'exact',\n path: '/archived-sessions/restore',\n handler: async (req, res) => {\n try {\n const body = await readJsonBody(req)\n const sid = body && typeof body.sessionId === 'string' ? body.sessionId : null\n if (!sid) return json(res, { ok: false, error: 'missing sessionId' }, 400)\n json(res, await restoreOne(sid))\n } catch (e) {\n json(res, { ok: false, error: String((e && e.message) || e) }, 500)\n }\n },\n }))\n\n disposers.push(ctx.webServer.register({\n kind: 'exact',\n path: '/archived-sessions/restore-many',\n handler: async (req, res) => {\n try {\n const body = await readJsonBody(req)\n const ids = parseIds(body)\n if (!ids || ids.length === 0) return json(res, { ok: false, error: 'missing sessionIds' }, 400)\n const results = []\n for (const sid of ids) {\n try { results.push({ sessionId: sid, ok: true, ...(await restoreOne(sid)) }) }\n catch (e) { results.push({ sessionId: sid, ok: false, error: String((e && e.message) || e) }) }\n }\n json(res, { ok: true, restored: results.filter((r) => r.ok).length, results })\n } catch (e) {\n json(res, { ok: false, error: String((e && e.message) || e) }, 500)\n }\n },\n }))\n\n disposers.push(ctx.webServer.register({\n kind: 'exact',\n path: '/archived-sessions/delete',\n handler: async (req, res) => {\n try {\n const body = await readJsonBody(req)\n const sid = body && typeof body.sessionId === 'string' ? body.sessionId : null\n if (!sid) return json(res, { ok: false, error: 'missing sessionId' }, 400)\n json(res, await deleteOne(sid))\n } catch (e) {\n json(res, { ok: false, error: String((e && e.message) || e) }, 500)\n }\n },\n }))\n\n disposers.push(ctx.webServer.register({\n kind: 'exact',\n path: '/archived-sessions/delete-many',\n handler: async (req, res) => {\n try {\n const body = await readJsonBody(req)\n const ids = parseIds(body)\n if (!ids || ids.length === 0) return json(res, { ok: false, error: 'missing sessionIds' }, 400)\n const results = []\n for (const sid of ids) {\n try { results.push({ sessionId: sid, ok: true, ...(await deleteOne(sid)) }) }\n catch (e) { results.push({ sessionId: sid, ok: false, error: String((e && e.message) || e) }) }\n }\n json(res, { ok: true, deleted: results.filter((r) => r.ok).length, results })\n } catch (e) {\n json(res, { ok: false, error: String((e && e.message) || e) }, 500)\n }\n },\n }))\n\n // All conversations (for the \"\u79FB\u52A8\u4F1A\u8BDD\" panel).\n disposers.push(ctx.webServer.register({\n kind: 'exact',\n path: '/archived-sessions/sessions',\n handler: async (req, res) => {\n try {\n json(res, { items: await allSessionItems() })\n } catch (e) {\n json(res, { error: String((e && e.message) || e) }, 500)\n }\n },\n }))\n\n // Available target workspaces (for the move picker).\n disposers.push(ctx.webServer.register({\n kind: 'exact',\n path: '/archived-sessions/workspaces',\n handler: async (req, res) => {\n try {\n json(res, { items: await listWorkspaces() })\n } catch (e) {\n json(res, { error: String((e && e.message) || e) }, 500)\n }\n },\n }))\n\n // Move one conversation to a target workspace (existing path or a new one).\n disposers.push(ctx.webServer.register({\n kind: 'exact',\n path: '/archived-sessions/move',\n handler: async (req, res) => {\n try {\n const body = await readJsonBody(req)\n const sid = body && typeof body.sessionId === 'string' ? body.sessionId : null\n const target = body && typeof body.targetPath === 'string' ? body.targetPath : null\n if (!sid) return json(res, { ok: false, error: 'missing sessionId' }, 400)\n if (!target) return json(res, { ok: false, error: 'missing targetPath' }, 400)\n json(res, { sessionId: sid, ...(await moveOne(sid, target)) })\n } catch (e) {\n json(res, { ok: false, error: String((e && e.message) || e) }, 500)\n }\n },\n }))\n\n return () => { for (const d of disposers) d() }\n }, 'dsh-archived-sessions: routes')\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tobycaimf/dsh-archived-sessions",
|
|
3
|
-
"description": "DSH 设置面板「归档会话」管理器:查看、恢复、彻底删除已归档的 session
|
|
4
|
-
"version": "1.0.
|
|
3
|
+
"description": "DSH 设置面板「归档会话」管理器:查看、恢复、彻底删除已归档的 session,带所属工作区标签与会话日期;并支持把会话移动到不同工作区。",
|
|
4
|
+
"version": "1.0.1",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"exports": {
|
package/src/client/index.jsx
CHANGED
|
@@ -194,11 +194,291 @@ function ArchivedPanel() {
|
|
|
194
194
|
)
|
|
195
195
|
}
|
|
196
196
|
|
|
197
|
+
const MOVE_CSS = `
|
|
198
|
+
.mv{display:flex;flex-direction:column;gap:4px;max-width:800px;padding:8px 2px 28px}
|
|
199
|
+
.mv-head{display:flex;align-items:center;gap:10px;margin:0 0 2px}
|
|
200
|
+
.mv-title{font-size:16px;font-weight:650;color:var(--dsw-alias-label-primary);letter-spacing:-0.01em;margin:0}
|
|
201
|
+
.mv-count{font-size:11px;font-weight:600;color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-fill-subtle);border:1px solid var(--dsw-alias-border-l2);border-radius:999px;padding:1px 8px;flex:none}
|
|
202
|
+
.mv-sub{font-size:12px;line-height:1.55;color:var(--dsw-alias-label-tertiary);margin:0 0 12px;max-width:64ch}
|
|
203
|
+
.mv-err{display:flex;align-items:center;justify-content:space-between;gap:10px;padding:9px 12px;border:1px solid color-mix(in srgb,var(--dsw-alias-state-error-primary) 40%,transparent);background:color-mix(in srgb,var(--dsw-alias-state-error-primary) 8%,transparent);border-radius:10px;color:var(--dsw-alias-state-error-primary);font-size:12px;margin-bottom:10px}
|
|
204
|
+
.mv-errretry{appearance:none;border:1px solid color-mix(in srgb,var(--dsw-alias-state-error-primary) 45%,transparent);background:transparent;color:inherit;border-radius:8px;padding:4px 10px;font-size:11px;cursor:pointer;flex:none}
|
|
205
|
+
.mv-list{display:flex;flex-direction:column;gap:8px}
|
|
206
|
+
.mv-card{display:flex;align-items:center;justify-content:space-between;gap:14px;padding:12px 14px;border:1px solid var(--dsw-alias-border-l2);border-radius:12px;background:var(--dsw-alias-fill-elevated);transition:border-color .15s ease}
|
|
207
|
+
.mv-card:hover{border-color:var(--dsw-alias-border-l4)}
|
|
208
|
+
.mv-main{min-width:0;display:flex;flex-direction:column;gap:4px}
|
|
209
|
+
.mv-name{font-size:13px;font-weight:550;color:var(--dsw-alias-label-primary);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
210
|
+
.mv-meta{display:flex;align-items:center;gap:7px;flex-wrap:wrap;min-width:0}
|
|
211
|
+
.mv-wtag{display:inline-flex;align-items:center;gap:4px;max-width:100%;font-size:11px;font-weight:500;line-height:1;color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-fill-subtle);border:1px solid var(--dsw-alias-border-l2);border-radius:999px;padding:3px 8px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
212
|
+
.mv-archived{color:var(--dsw-alias-state-warning-primary);border-color:color-mix(in srgb,var(--dsw-alias-state-warning-primary) 50%,transparent);background:color-mix(in srgb,var(--dsw-alias-state-warning-primary) 8%,transparent)}
|
|
213
|
+
.mv-date{font-size:11px;color:var(--dsw-alias-label-tertiary);white-space:nowrap;flex:none}
|
|
214
|
+
.mv-id{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:10.5px;color:var(--dsw-alias-label-tertiary);flex:none}
|
|
215
|
+
.mv-dot{color:var(--dsw-alias-border-l3);flex:none}
|
|
216
|
+
.mv-actions{display:flex;gap:8px;flex:none}
|
|
217
|
+
.mv-btn{appearance:none;min-height:32px;padding:0 12px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-fill-subtle);color:var(--dsw-alias-label-secondary);border-radius:9px;font-size:12px;font-weight:500;cursor:pointer;display:inline-flex;align-items:center;gap:6px;transition:background-color .15s ease,border-color .15s ease,color .15s ease}
|
|
218
|
+
.mv-btn:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}
|
|
219
|
+
.mv-btn:disabled{opacity:.5;cursor:default}
|
|
220
|
+
.mv-go{color:var(--dsw-alias-state-business-primary);border-color:color-mix(in srgb,var(--dsw-alias-state-business-primary) 45%,transparent)}
|
|
221
|
+
.mv-go:hover:not(:disabled){background:color-mix(in srgb,var(--dsw-alias-state-business-primary) 10%,transparent);color:var(--dsw-alias-state-business-primary)}
|
|
222
|
+
.mv-sheet{display:flex;flex-direction:column;gap:10px;margin-top:10px;padding:12px 14px;border:1px solid var(--dsw-alias-border-l2);border-radius:12px;background:var(--dsw-alias-fill-subtle)}
|
|
223
|
+
.mv-sheet-row{display:flex;align-items:center;gap:8px;flex-wrap:wrap;font-size:12px;color:var(--dsw-alias-label-secondary)}
|
|
224
|
+
.mv-sheet-row input[type=radio]{accent-color:var(--dsw-alias-state-business-primary)}
|
|
225
|
+
.mv-sheet select,.mv-sheet input[type=text]{appearance:none;min-height:32px;padding:0 10px;border:1px solid var(--dsw-alias-border-l2);border-radius:9px;background:var(--dsw-alias-fill-elevated);color:var(--dsw-alias-label-primary);font-size:12px;font-family:inherit}
|
|
226
|
+
.mv-sheet input[type=text]{flex:1;min-width:220px}
|
|
227
|
+
.mv-sheet select:focus-visible,.mv-sheet input[type=text]:focus-visible,.mv-go:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary);outline-offset:2px}
|
|
228
|
+
.mv-empty{display:flex;align-items:center;gap:10px;padding:20px 14px;border:1px dashed var(--dsw-alias-border-l3);border-radius:12px;color:var(--dsw-alias-label-tertiary);font-size:13px;line-height:1.5}
|
|
229
|
+
.mv-skel{display:flex;flex-direction:column;gap:8px}
|
|
230
|
+
.mv-skel-card{height:58px;border-radius:12px;background:var(--dsw-alias-fill-subtle);position:relative;overflow:hidden}
|
|
231
|
+
.mv-skel-card::after{content:'';position:absolute;inset:0;transform:translateX(-100%);background:linear-gradient(90deg,transparent,color-mix(in srgb,var(--dsw-alias-fill-elevated) 75%,transparent),transparent);animation:mv-shimmer 1.4s infinite}
|
|
232
|
+
@keyframes mv-shimmer{100%{transform:translateX(100%)}}
|
|
233
|
+
@keyframes mv-pop{from{opacity:0;transform:translateX(-50%) translateY(10px)}}
|
|
234
|
+
.mv-status{position:fixed;bottom:20px;left:50%;transform:translateX(-50%);z-index:60;background:var(--dsw-alias-fill-elevated);border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-primary);padding:9px 16px;border-radius:999px;font-size:12px;box-shadow:0 8px 24px rgb(0 0 0/.18);display:flex;align-items:center;gap:8px;animation:mv-pop .18s ease-out;max-width:min(90vw,420px)}
|
|
235
|
+
.mv-spin{width:12px;height:12px;border:2px solid color-mix(in srgb,var(--dsw-alias-label-secondary) 35%,transparent);border-top-color:var(--dsw-alias-label-secondary);border-radius:50%;animation:archv-rot .8s linear infinite;flex:none}
|
|
236
|
+
@media (prefers-reduced-motion:reduce){.mv-skel-card::after{animation:none}.mv-card,.mv-btn{transition:none}.mv-status,.mv-spin{animation:none}}
|
|
237
|
+
@media (max-width:560px){.mv-card{flex-direction:column;align-items:stretch;gap:10px}.mv-actions{justify-content:flex-end}.mv-sheet input[type=text]{min-width:0}}
|
|
238
|
+
`
|
|
239
|
+
|
|
240
|
+
function MovePanel() {
|
|
241
|
+
const [sessions, setSessions] = useState(null)
|
|
242
|
+
const [workspaces, setWorkspaces] = useState([])
|
|
243
|
+
const [busy, setBusy] = useState(null)
|
|
244
|
+
const [open, setOpen] = useState(null)
|
|
245
|
+
const [mode, setMode] = useState('existing')
|
|
246
|
+
const [targetWs, setTargetWs] = useState('')
|
|
247
|
+
const [newPath, setNewPath] = useState('')
|
|
248
|
+
const [error, setError] = useState(null)
|
|
249
|
+
const [toast, setToast] = useState(null)
|
|
250
|
+
const timer = useRef(null)
|
|
251
|
+
|
|
252
|
+
const showToast = (msg) => {
|
|
253
|
+
if (timer.current) clearTimeout(timer.current)
|
|
254
|
+
setToast(msg)
|
|
255
|
+
timer.current = setTimeout(() => setToast(null), 2400)
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const refresh = () => {
|
|
259
|
+
setError(null)
|
|
260
|
+
Promise.all([
|
|
261
|
+
postJSON('/archived-sessions/sessions', {}),
|
|
262
|
+
postJSON('/archived-sessions/workspaces', {}),
|
|
263
|
+
])
|
|
264
|
+
.then(([s, works]) => {
|
|
265
|
+
setSessions(s.items || [])
|
|
266
|
+
setWorkspaces(works.items || [])
|
|
267
|
+
if (!targetWs && works.items && works.items.length) setTargetWs(works.items[0].workspaceId)
|
|
268
|
+
})
|
|
269
|
+
.catch((e) => setError(String((e && e.message) || e)))
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
useEffect(() => {
|
|
273
|
+
refresh()
|
|
274
|
+
return () => { if (timer.current) clearTimeout(timer.current) }
|
|
275
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
276
|
+
}, [])
|
|
277
|
+
|
|
278
|
+
const wsPath = (id) => {
|
|
279
|
+
const w = workspaces.find((x) => x.workspaceId === id)
|
|
280
|
+
return w ? w.path : ''
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const doMove = (it) => {
|
|
284
|
+
const targetPath = mode === 'new' ? newPath.trim() : wsPath(targetWs)
|
|
285
|
+
if (!targetPath) { setError('请选择已有工作区或输入新的目标目录路径'); return }
|
|
286
|
+
setBusy(it.sessionId)
|
|
287
|
+
setError(null)
|
|
288
|
+
postJSON('/archived-sessions/move', { sessionId: it.sessionId, targetPath })
|
|
289
|
+
.then((r) => {
|
|
290
|
+
setBusy(null)
|
|
291
|
+
setOpen(null)
|
|
292
|
+
setMode('existing')
|
|
293
|
+
setNewPath('')
|
|
294
|
+
showToast(`已把「${it.title || it.sessionId}」移到 ${r.workspaceTitle || targetPath}`)
|
|
295
|
+
refresh()
|
|
296
|
+
})
|
|
297
|
+
.catch((e) => { setBusy(null); setError(String((e && e.message) || e)) })
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const wtag = (it) => {
|
|
301
|
+
if (it.workspacePath) {
|
|
302
|
+
return <span className="mv-wtag" title={it.workspacePath}>{it.workspaceTitle || pathName(it.workspacePath)}</span>
|
|
303
|
+
}
|
|
304
|
+
return <span className="mv-wtag">未分组</span>
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
return (
|
|
308
|
+
<div className="mv" role="region" aria-label="移动会话">
|
|
309
|
+
<style>{MOVE_CSS}</style>
|
|
310
|
+
<div className="mv-head">
|
|
311
|
+
<h2 className="mv-title">移动会话到工作区</h2>
|
|
312
|
+
{sessions !== null && <span className="mv-count" aria-label={`${sessions.length} 个会话`}>{sessions.length}</span>}
|
|
313
|
+
</div>
|
|
314
|
+
<p className="mv-sub">
|
|
315
|
+
把任意会话移动到某个工作区目录(可选用现有工作区或输入新目录,自动创建)。移动会改写会话的工作目录并把日志迁移过去;
|
|
316
|
+
会话当前处于打开状态时需先在侧栏切走才能移动。
|
|
317
|
+
</p>
|
|
318
|
+
{error && (
|
|
319
|
+
<div className="mv-err" role="alert">
|
|
320
|
+
<span>{error}</span>
|
|
321
|
+
<button type="button" className="mv-errretry" onClick={refresh}>重试</button>
|
|
322
|
+
</div>
|
|
323
|
+
)}
|
|
324
|
+
{sessions === null ? (
|
|
325
|
+
<div className="mv-skel" aria-label="加载中">
|
|
326
|
+
{[0, 1, 2].map((i) => <div key={i} className="mv-skel-card" />)}
|
|
327
|
+
</div>
|
|
328
|
+
) : sessions.length === 0 ? (
|
|
329
|
+
<div className="mv-empty">暂无可移动的会话。</div>
|
|
330
|
+
) : (
|
|
331
|
+
<div className="mv-list" role="list">
|
|
332
|
+
{sessions.map((it) => {
|
|
333
|
+
const date = fmtDate(it.createdAt)
|
|
334
|
+
const expanded = open === it.sessionId
|
|
335
|
+
return (
|
|
336
|
+
<div key={it.sessionId} className="mv-card" role="listitem">
|
|
337
|
+
<div className="mv-main">
|
|
338
|
+
<div className="mv-name" title={it.title || ''}>{it.title || '(无标题)'}</div>
|
|
339
|
+
<div className="mv-meta">
|
|
340
|
+
{it.archived && <span className="mv-wtag mv-archived">已归档</span>}
|
|
341
|
+
{wtag(it)}
|
|
342
|
+
{date && <><span className="mv-dot">·</span><span className="mv-date">{date}</span></>}
|
|
343
|
+
<span className="mv-id">{it.sessionId}</span>
|
|
344
|
+
</div>
|
|
345
|
+
{expanded && (
|
|
346
|
+
<div className="mv-sheet">
|
|
347
|
+
<label className="mv-sheet-row">
|
|
348
|
+
<input type="radio" name="mv-target" checked={mode === 'existing'} onChange={() => setMode('existing')} />
|
|
349
|
+
已有工作区
|
|
350
|
+
</label>
|
|
351
|
+
{mode === 'existing' && (
|
|
352
|
+
<div className="mv-sheet-row">
|
|
353
|
+
<select value={targetWs} onChange={(e) => setTargetWs(e.target.value)} aria-label="目标工作区">
|
|
354
|
+
{workspaces.length === 0 && <option value="">(暂无工作区)</option>}
|
|
355
|
+
{workspaces.map((w) => (
|
|
356
|
+
<option key={w.workspaceId} value={w.workspaceId}>{w.title} · {w.path}</option>
|
|
357
|
+
))}
|
|
358
|
+
</select>
|
|
359
|
+
</div>
|
|
360
|
+
)}
|
|
361
|
+
<label className="mv-sheet-row">
|
|
362
|
+
<input type="radio" name="mv-target" checked={mode === 'new'} onChange={() => setMode('new')} />
|
|
363
|
+
新建工作区目录
|
|
364
|
+
</label>
|
|
365
|
+
{mode === 'new' && (
|
|
366
|
+
<div className="mv-sheet-row">
|
|
367
|
+
<input
|
|
368
|
+
type="text"
|
|
369
|
+
value={newPath}
|
|
370
|
+
onChange={(e) => setNewPath(e.target.value)}
|
|
371
|
+
placeholder="例如 /Users/you/Projects/demo 或 ~/demo"
|
|
372
|
+
aria-label="新工作区目录路径"
|
|
373
|
+
/>
|
|
374
|
+
</div>
|
|
375
|
+
)}
|
|
376
|
+
<div className="mv-sheet-row">
|
|
377
|
+
<button type="button" className="mv-btn mv-go" disabled={busy !== null} onClick={() => doMove(it)}>
|
|
378
|
+
{busy === it.sessionId && <span className="mv-spin" aria-hidden="true" />}确认移动
|
|
379
|
+
</button>
|
|
380
|
+
<button type="button" className="mv-btn" disabled={busy !== null} onClick={() => { setOpen(null); setMode('existing'); setNewPath(''); }}>取消</button>
|
|
381
|
+
</div>
|
|
382
|
+
</div>
|
|
383
|
+
)}
|
|
384
|
+
</div>
|
|
385
|
+
<div className="mv-actions">
|
|
386
|
+
<button
|
|
387
|
+
type="button"
|
|
388
|
+
className="mv-btn mv-go"
|
|
389
|
+
disabled={busy !== null}
|
|
390
|
+
aria-label={'移动 ' + (it.title || it.sessionId)}
|
|
391
|
+
onClick={() => { if (expanded) { setOpen(null) } else { setTargetWs(workspaces.length ? (targetWs || workspaces[0].workspaceId) : ''); setMode('existing'); setNewPath(''); setOpen(it.sessionId) } }}
|
|
392
|
+
>
|
|
393
|
+
{expanded ? '收起' : '移动到…'}
|
|
394
|
+
</button>
|
|
395
|
+
</div>
|
|
396
|
+
</div>
|
|
397
|
+
)
|
|
398
|
+
})}
|
|
399
|
+
</div>
|
|
400
|
+
)}
|
|
401
|
+
{toast && <div className="mv-status" role="status">{toast}</div>}
|
|
402
|
+
</div>
|
|
403
|
+
)
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// Left-nav icon swap for the two plugin settings sections.
|
|
407
|
+
//
|
|
408
|
+
// DSH's settings shell renders a *shared* fallback gear for every custom
|
|
409
|
+
// section (its internal navIcon() only differentiates 模型/插件/Agent预设), and
|
|
410
|
+
// settings.section has no per-section icon field. So — exactly like
|
|
411
|
+
// dsh-better-sidebar — we mark our own rows by matching their visible label
|
|
412
|
+
// text against the shell DOM, then let CSS swap the gear for a distinct glyph.
|
|
413
|
+
// The marker owns no shell structure and is removed on fiber disposal, so the
|
|
414
|
+
// adaptation stays HMR-safe and ships with the plugin.
|
|
415
|
+
const NAV_CSS = `
|
|
416
|
+
[data-dsh-nav-archived] > svg:first-child,
|
|
417
|
+
[data-dsh-nav-move] > svg:first-child { display: none; }
|
|
418
|
+
[data-dsh-nav-archived]::before,
|
|
419
|
+
[data-dsh-nav-move]::before {
|
|
420
|
+
content: ''; flex: none; width: 16px; height: 16px; background: currentColor;
|
|
421
|
+
-webkit-mask: center / contain no-repeat; mask: center / contain no-repeat;
|
|
422
|
+
}
|
|
423
|
+
[data-dsh-nav-archived]::before {
|
|
424
|
+
-webkit-mask-image: url("data:image/svg+xml,%3Csvg%20xmlns%3D'http://www.w3.org/2000/svg'%20width%3D'24'%20height%3D'24'%20viewBox%3D'0%200%2024%2024'%20fill%3D'none'%20stroke%3D'black'%20stroke-width%3D'2'%20stroke-linecap%3D'round'%20stroke-linejoin%3D'round'%3E%3Cpath%20d%3D'M21%208v13H3V8'/%3E%3Cpath%20d%3D'M1%203h22v5H1z'/%3E%3Cpath%20d%3D'M10%2012h4'/%3E%3C/svg%3E");
|
|
425
|
+
mask-image: url("data:image/svg+xml,%3Csvg%20xmlns%3D'http://www.w3.org/2000/svg'%20width%3D'24'%20height%3D'24'%20viewBox%3D'0%200%2024%2024'%20fill%3D'none'%20stroke%3D'black'%20stroke-width%3D'2'%20stroke-linecap%3D'round'%20stroke-linejoin%3D'round'%3E%3Cpath%20d%3D'M21%208v13H3V8'/%3E%3Cpath%20d%3D'M1%203h22v5H1z'/%3E%3Cpath%20d%3D'M10%2012h4'/%3E%3C/svg%3E");
|
|
426
|
+
}
|
|
427
|
+
[data-dsh-nav-move]::before {
|
|
428
|
+
-webkit-mask-image: url("data:image/svg+xml,%3Csvg%20xmlns%3D'http://www.w3.org/2000/svg'%20width%3D'24'%20height%3D'24'%20viewBox%3D'0%200%2024%2024'%20fill%3D'none'%20stroke%3D'black'%20stroke-width%3D'2'%20stroke-linecap%3D'round'%20stroke-linejoin%3D'round'%3E%3Cpath%20d%3D'M22%2019a2%202%200%200%201-2%202H4a2%202%200%200%201-2-2V5a2%202%200%200%201%202-2h5l2%203h9a2%202%200%200%201%202%202z'/%3E%3Cpath%20d%3D'M12%2010v6'/%3E%3Cpath%20d%3D'm15%2013-3%203-3-3'/%3E%3C/svg%3E");
|
|
429
|
+
mask-image: url("data:image/svg+xml,%3Csvg%20xmlns%3D'http://www.w3.org/2000/svg'%20width%3D'24'%20height%3D'24'%20viewBox%3D'0%200%2024%2024'%20fill%3D'none'%20stroke%3D'black'%20stroke-width%3D'2'%20stroke-linecap%3D'round'%20stroke-linejoin%3D'round'%3E%3Cpath%20d%3D'M22%2019a2%202%200%200%201-2%202H4a2%202%200%200%201-2-2V5a2%202%200%200%201%202-2h5l2%203h9a2%202%200%200%201%202%202z'/%3E%3Cpath%20d%3D'M12%2010v6'/%3E%3Cpath%20d%3D'm15%2013-3%203-3-3'/%3E%3C/svg%3E");
|
|
430
|
+
}
|
|
431
|
+
`
|
|
432
|
+
|
|
433
|
+
function markSettingsNav() {
|
|
434
|
+
const ARCHIVED_LABEL = '归档会话'
|
|
435
|
+
const MOVE_LABEL = '移动会话'
|
|
436
|
+
let disposed = false
|
|
437
|
+
const sync = () => {
|
|
438
|
+
if (disposed) return
|
|
439
|
+
const buttons = document.querySelectorAll('[role="dialog"] nav button')
|
|
440
|
+
for (const b of buttons) {
|
|
441
|
+
const t = (b.textContent || '').trim()
|
|
442
|
+
if (t === ARCHIVED_LABEL) b.setAttribute('data-dsh-nav-archived', '')
|
|
443
|
+
else b.removeAttribute('data-dsh-nav-archived')
|
|
444
|
+
if (t === MOVE_LABEL) b.setAttribute('data-dsh-nav-move', '')
|
|
445
|
+
else b.removeAttribute('data-dsh-nav-move')
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
sync()
|
|
449
|
+
const obs = new MutationObserver(sync)
|
|
450
|
+
obs.observe(document.body, { childList: true, subtree: true, characterData: true })
|
|
451
|
+
return () => {
|
|
452
|
+
disposed = true
|
|
453
|
+
obs.disconnect()
|
|
454
|
+
document.querySelectorAll('[data-dsh-nav-archived]').forEach((e) => e.removeAttribute('data-dsh-nav-archived'))
|
|
455
|
+
document.querySelectorAll('[data-dsh-nav-move]').forEach((e) => e.removeAttribute('data-dsh-nav-move'))
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
function installSettingsNavIcons(ctx) {
|
|
460
|
+
const styleEl = document.createElement('style')
|
|
461
|
+
styleEl.textContent = NAV_CSS
|
|
462
|
+
document.head.appendChild(styleEl)
|
|
463
|
+
const dispose = markSettingsNav()
|
|
464
|
+
ctx.effect(() => () => {
|
|
465
|
+
dispose()
|
|
466
|
+
if (styleEl.parentNode) styleEl.parentNode.removeChild(styleEl)
|
|
467
|
+
})
|
|
468
|
+
}
|
|
469
|
+
|
|
197
470
|
export function apply(ctx) {
|
|
471
|
+
installSettingsNavIcons(ctx)
|
|
198
472
|
ctx.slots.inject('settings.section', () =>
|
|
199
473
|
ctx.slots.register(
|
|
200
474
|
{ name: 'settings.section', id: 'archived-sessions', order: 90, label: '归档会话' },
|
|
201
475
|
(props) => <ArchivedPanel {...props} />,
|
|
202
476
|
),
|
|
203
477
|
)
|
|
478
|
+
ctx.slots.inject('settings.section', () =>
|
|
479
|
+
ctx.slots.register(
|
|
480
|
+
{ name: 'settings.section', id: 'move-sessions', order: 91, label: '移动会话' },
|
|
481
|
+
(props) => <MovePanel {...props} />,
|
|
482
|
+
),
|
|
483
|
+
)
|
|
204
484
|
}
|