dsh-ws-recency 0.11.4 → 0.11.5
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/client.js +60 -0
- package/index.js +51 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -130,7 +130,7 @@ dsh plugin --profile <profile> add github:chyxhtc/dsh-ws-recency
|
|
|
130
130
|
或固定版本的预构建包:
|
|
131
131
|
|
|
132
132
|
```powershell
|
|
133
|
-
dsh plugin --profile <profile> add https://github.com/chyxhtc/dsh-ws-recency/releases/download/v0.11.
|
|
133
|
+
dsh plugin --profile <profile> add https://github.com/chyxhtc/dsh-ws-recency/releases/download/v0.11.5/dsh-ws-recency-0.11.5.tgz
|
|
134
134
|
```
|
|
135
135
|
|
|
136
136
|
装完把 `dsh-ws-recency` 加进 `~/.hypers-work/profiles/<profile>/package.json` 的 `dsh.profile.bundles`,重启应用。
|
package/client.js
CHANGED
|
@@ -214,6 +214,12 @@
|
|
|
214
214
|
"move.done": "已移动到「{workspace}」。若列表没刷新,切换一下工作区或重启应用。",
|
|
215
215
|
"move.doneLive": "\n\n这个会话刚才是打开状态:建议重启应用后再继续对话,避免它沿用旧的目录。",
|
|
216
216
|
"move.repaired": "已补登记到「{workspace}」:上一次移动只搬了日志、没登记工作区,这次把它补上了。",
|
|
217
|
+
"adopt.title": "补登记游离会话",
|
|
218
|
+
"adopt.description": "有些会话的目录已经指向某个工作区,却不在任何分组里(移动中断会留下这种状态)。这里可以一次性找回。",
|
|
219
|
+
"adopt.action": "扫描",
|
|
220
|
+
"adopt.working": "扫描中…",
|
|
221
|
+
"adopt.done": "已补登记 {n} 个会话。",
|
|
222
|
+
"adopt.failed": "扫描失败:",
|
|
217
223
|
"move.failed": "移动失败:",
|
|
218
224
|
"stats.chart": "消耗分布",
|
|
219
225
|
"stats.details.show": "显示明细 ▾",
|
|
@@ -269,6 +275,12 @@
|
|
|
269
275
|
"move.done": "Moved to \"{workspace}\". If the list has not refreshed, switch workspaces or restart the app.",
|
|
270
276
|
"move.doneLive": "\n\nThe session was open: restart the app before continuing it so it does not keep using the old directory.",
|
|
271
277
|
"move.repaired": "Registered in \"{workspace}\": the previous attempt moved the log without the bookkeeping, and this repaired it.",
|
|
278
|
+
"adopt.title": "Adopt unlisted sessions",
|
|
279
|
+
"adopt.description": "A session whose directory already belongs to a workspace can end up listed nowhere (an interrupted move leaves exactly this). Scan to re-attach them.",
|
|
280
|
+
"adopt.action": "Scan",
|
|
281
|
+
"adopt.working": "Scanning\u2026",
|
|
282
|
+
"adopt.done": "Re-attached {n} session(s).",
|
|
283
|
+
"adopt.failed": "Scan failed: ",
|
|
272
284
|
"move.failed": "Move failed: ",
|
|
273
285
|
"stats.chart": "Where the tokens went",
|
|
274
286
|
"stats.details.show": "Show details",
|
|
@@ -1567,6 +1579,44 @@
|
|
|
1567
1579
|
);
|
|
1568
1580
|
}
|
|
1569
1581
|
|
|
1582
|
+
/** Settings -> General: re-attach sessions that belong to no workspace. */
|
|
1583
|
+
function AdoptRow() {
|
|
1584
|
+
var result = react.useState("");
|
|
1585
|
+
var message = result[0];
|
|
1586
|
+
var setMessage = result[1];
|
|
1587
|
+
var run = function () {
|
|
1588
|
+
setMessage(t("adopt.working"));
|
|
1589
|
+
fetch("/ws-recency/session-adopt", { method: "POST", headers: { "content-type": "application/json" }, body: "{}" })
|
|
1590
|
+
.then(function (response) {
|
|
1591
|
+
return response.text().then(function (text) {
|
|
1592
|
+
var parsed = null;
|
|
1593
|
+
try { parsed = JSON.parse(text); } catch (error) { parsed = null; }
|
|
1594
|
+
return { status: response.status, text: text, json: parsed };
|
|
1595
|
+
});
|
|
1596
|
+
})
|
|
1597
|
+
.then(function (answer) {
|
|
1598
|
+
if (answer.json !== null && answer.json.ok === true) {
|
|
1599
|
+
var count = Array.isArray(answer.json.adopted) ? answer.json.adopted.length : 0;
|
|
1600
|
+
setMessage(t("adopt.done", { n: count }));
|
|
1601
|
+
return;
|
|
1602
|
+
}
|
|
1603
|
+
setMessage(t("adopt.failed") + (answer.json && answer.json.error ? answer.json.error : answer.text.slice(0, 120)));
|
|
1604
|
+
})
|
|
1605
|
+
.catch(function (error) {
|
|
1606
|
+
setMessage(t("adopt.failed") + (error && error.message ? error.message : error));
|
|
1607
|
+
});
|
|
1608
|
+
};
|
|
1609
|
+
return h("div", { className: "wsr-setRow" },
|
|
1610
|
+
h("div", { className: "wsr-setText" },
|
|
1611
|
+
h("div", { className: "wsr-setTitle" }, t("adopt.title")),
|
|
1612
|
+
h("div", { className: "wsr-setDesc" }, message === "" ? t("adopt.description") : message),
|
|
1613
|
+
),
|
|
1614
|
+
h("div", { className: "wsr-controls" },
|
|
1615
|
+
h("button", { type: "button", className: "wsr-step", onClick: run }, t("adopt.action")),
|
|
1616
|
+
),
|
|
1617
|
+
);
|
|
1618
|
+
}
|
|
1619
|
+
|
|
1570
1620
|
/** Settings -> General row: the global pinned section on/off. */
|
|
1571
1621
|
function PinSectionRow(props) {
|
|
1572
1622
|
var settings = props.useStore(function (state) { return state; });
|
|
@@ -1705,6 +1755,16 @@
|
|
|
1705
1755
|
}, ColorsRow);
|
|
1706
1756
|
});
|
|
1707
1757
|
|
|
1758
|
+
ctx.slots.inject("settings.general.item", function () {
|
|
1759
|
+
return ctx.slots.register({
|
|
1760
|
+
name: "settings.general.item",
|
|
1761
|
+
id: "ws-recency-adopt",
|
|
1762
|
+
order: 49,
|
|
1763
|
+
store: prefs,
|
|
1764
|
+
locale: NS,
|
|
1765
|
+
}, AdoptRow);
|
|
1766
|
+
});
|
|
1767
|
+
|
|
1708
1768
|
ctx.slots.inject("settings.general.item", function () {
|
|
1709
1769
|
return ctx.slots.register({
|
|
1710
1770
|
name: "settings.general.item",
|
package/index.js
CHANGED
|
@@ -36,6 +36,7 @@ export const TRASH_SESSION_PATH = "/ws-recency/session-trash";
|
|
|
36
36
|
export const STATS_SESSION_PATH = "/ws-recency/session-stats";
|
|
37
37
|
export const HEALTH_PATH = "/ws-recency/health";
|
|
38
38
|
export const MOVE_SESSION_PATH = "/ws-recency/session-move";
|
|
39
|
+
export const ADOPT_SESSION_PATH = "/ws-recency/session-adopt";
|
|
39
40
|
|
|
40
41
|
const STOP_SETTLE_MS = 500;
|
|
41
42
|
const MAX_BODY_BYTES = 16 * 1024;
|
|
@@ -641,6 +642,51 @@ export async function moveSessionToWorkspace(ctx, sessionId, workspaceId) {
|
|
|
641
642
|
return { ok: true, workspace: target.id, path: targetPath, detached, backup, live: wasLive };
|
|
642
643
|
}
|
|
643
644
|
|
|
645
|
+
/**
|
|
646
|
+
* Re-attach sessions whose stored cwd already names a workspace but which are
|
|
647
|
+
* listed by none - the shape an aborted move leaves behind. Idempotent: it only
|
|
648
|
+
* ever adds memberships the registry's own validation accepts.
|
|
649
|
+
*/
|
|
650
|
+
export async function adoptOrphanSessions(ctx) {
|
|
651
|
+
const snapshots = await ctx.sessionPersistence.list();
|
|
652
|
+
const workspaces = ctx.workspaceRegistry.list();
|
|
653
|
+
const byPath = new Map();
|
|
654
|
+
for (const workspace of workspaces) byPath.set(workspace.path, workspace);
|
|
655
|
+
const adopted = [];
|
|
656
|
+
for (const snapshot of snapshots) {
|
|
657
|
+
const header = snapshot !== null && snapshot !== undefined ? snapshot.header : undefined;
|
|
658
|
+
if (header === undefined || header === null || typeof header.cwd !== "string" || typeof header.id !== "string") continue;
|
|
659
|
+
const target = byPath.get(header.cwd);
|
|
660
|
+
if (target === undefined) continue;
|
|
661
|
+
const listed = workspaces.some((workspace) => Array.isArray(workspace.sessionIds) && workspace.sessionIds.includes(header.id));
|
|
662
|
+
if (listed) continue;
|
|
663
|
+
try {
|
|
664
|
+
await target.attachSession(header.id);
|
|
665
|
+
adopted.push(header.id);
|
|
666
|
+
} catch {
|
|
667
|
+
/* the registry refused it; leave it alone */
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
return { ok: true, adopted };
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
/** POST {} -> attach every session that belongs nowhere. */
|
|
674
|
+
export async function handleAdoptRequest(ctx, req, res) {
|
|
675
|
+
if ((req.method ?? "POST").toUpperCase() !== "POST") {
|
|
676
|
+
sendJson(res, 405, { ok: false, error: "method not allowed" });
|
|
677
|
+
return;
|
|
678
|
+
}
|
|
679
|
+
if (!loopbackOrigin(req)) {
|
|
680
|
+
sendJson(res, 403, { ok: false, error: "origin not trusted" });
|
|
681
|
+
return;
|
|
682
|
+
}
|
|
683
|
+
try {
|
|
684
|
+
sendJson(res, 200, await adoptOrphanSessions(ctx));
|
|
685
|
+
} catch (error) {
|
|
686
|
+
sendJson(res, 500, { ok: false, error: error instanceof Error ? error.message : String(error) });
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
|
|
644
690
|
/** POST { sessionId, workspaceId } -> move that session to another workspace. */
|
|
645
691
|
export async function handleMoveRequest(ctx, req, res) {
|
|
646
692
|
if ((req.method ?? "POST").toUpperCase() !== "POST") {
|
|
@@ -731,6 +777,11 @@ export function apply(ctx) {
|
|
|
731
777
|
path: MOVE_SESSION_PATH,
|
|
732
778
|
handler: (req, res) => handleMoveRequest(moveCtx, req, res),
|
|
733
779
|
}), "ws-recency: move route");
|
|
780
|
+
webCtx.effect(() => server.register({
|
|
781
|
+
kind: "exact",
|
|
782
|
+
path: ADOPT_SESSION_PATH,
|
|
783
|
+
handler: (req, res) => handleAdoptRequest(moveCtx, req, res),
|
|
784
|
+
}), "ws-recency: adopt route");
|
|
734
785
|
});
|
|
735
786
|
|
|
736
787
|
webCtx.inject(["sessionPersistence", "workspaceRegistry", "sessions"], (trashCtx) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-ws-recency",
|
|
3
|
-
"version": "0.11.
|
|
3
|
+
"version": "0.11.5",
|
|
4
4
|
"description": "Sidebar session management for DeepSeek Harness: workspace groups sorted by latest activity, stale history folded, a global pinned section, follow-up marks, session delete into a recoverable trash, per-session token statistics split by tool, and moving a session to another workspace.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "chyxhtc",
|