dsh-rewind-plugin 0.6.3 → 0.7.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 +14 -10
- package/README.md +10 -7
- package/assets/screenshots/cleanup-setting.png +0 -0
- package/docs/compat/audit.md +18 -22
- package/docs/format.md +11 -0
- package/docs/snapshot-auto-cleanup.md +4 -12
- package/docs/snapshot-auto-cleanup.zh.md +4 -8
- package/lib/client.js +528 -9
- package/lib/index.js +85 -39
- package/lib/types/client/locales.d.ts +34 -0
- package/lib/types/client/portals.d.ts +29 -6
- package/lib/types/client/settings-card.d.ts +76 -0
- package/lib/types/client/styles.d.ts +1 -1
- package/lib/types/locales.d.ts +0 -2
- package/lib/types/session-events.d.ts +34 -0
- package/lib/types/snapshot-cleanup.d.ts +67 -7
- package/package.json +12 -2
package/lib/index.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
2
|
import { createAssistantMessage } from "@deepseek-ai/dsh-llm";
|
|
3
|
-
import { unlink } from "node:fs/promises";
|
|
3
|
+
import { unlink as unlink2 } from "node:fs/promises";
|
|
4
4
|
import * as dshSettings from "@deepseek-ai/dsh-settings";
|
|
5
|
+
import "@deepseek-ai/schemastery";
|
|
5
6
|
|
|
6
7
|
// src/locales.ts
|
|
7
8
|
var en = {
|
|
@@ -33,11 +34,9 @@ var en = {
|
|
|
33
34
|
"command.description": "Rewind the conversation back to an earlier user message (optionally restoring files)",
|
|
34
35
|
"cleanup.description": "Manage automatic cleanup of session snapshot backups",
|
|
35
36
|
"cleanup.inputHint": "on | off | max-age <days> | run [--apply] [--current]",
|
|
36
|
-
"cleanup.status": "Auto-cleanup: {state}. Max age: {days} day(s).
|
|
37
|
+
"cleanup.status": "Auto-cleanup: {state}. Max age: {days} day(s).",
|
|
37
38
|
"cleanup.enabled": "enabled",
|
|
38
39
|
"cleanup.disabled": "disabled",
|
|
39
|
-
"cleanup.present": "file present",
|
|
40
|
-
"cleanup.absent": "no file \u2014 defaults",
|
|
41
40
|
"cleanup.onOk": "Auto-cleanup enabled.",
|
|
42
41
|
"cleanup.offOk": "Auto-cleanup disabled \u2014 all snapshots kept.",
|
|
43
42
|
"cleanup.maxAgeOk": "Auto-cleanup max age set to {days} day(s).",
|
|
@@ -83,11 +82,9 @@ var zh = {
|
|
|
83
82
|
"command.description": "\u5728\u540C\u7A97\u53E3\u5185\u5C06\u5BF9\u8BDD\u56DE\u9000\u5230\u66F4\u65E9\u7684\u7528\u6237\u6D88\u606F\uFF08\u53EF\u540C\u65F6\u8FD8\u539F\u6587\u4EF6\uFF09",
|
|
84
83
|
"cleanup.description": "\u7BA1\u7406\u4F1A\u8BDD\u5FEB\u7167\u5907\u4EFD\u7684\u81EA\u52A8\u6E05\u7406",
|
|
85
84
|
"cleanup.inputHint": "on | off | max-age <\u5929\u6570> | run [--apply] [--current]",
|
|
86
|
-
"cleanup.status": "\u81EA\u52A8\u6E05\u7406\uFF1A{state}\u3002\u6700\u5927\u4FDD\u7559\u5929\u6570\uFF1A{days} \u5929\u3002
|
|
85
|
+
"cleanup.status": "\u81EA\u52A8\u6E05\u7406\uFF1A{state}\u3002\u6700\u5927\u4FDD\u7559\u5929\u6570\uFF1A{days} \u5929\u3002",
|
|
87
86
|
"cleanup.enabled": "\u5DF2\u5F00\u542F",
|
|
88
87
|
"cleanup.disabled": "\u5DF2\u5173\u95ED",
|
|
89
|
-
"cleanup.present": "\u5B58\u5728\u914D\u7F6E\u6587\u4EF6",
|
|
90
|
-
"cleanup.absent": "\u65E0\u6587\u4EF6\u2014\u2014\u4F7F\u7528\u9ED8\u8BA4\u503C",
|
|
91
88
|
"cleanup.onOk": "\u5DF2\u5F00\u542F\u81EA\u52A8\u6E05\u7406\u3002",
|
|
92
89
|
"cleanup.offOk": "\u5DF2\u5173\u95ED\u81EA\u52A8\u6E05\u7406\u2014\u2014\u4FDD\u7559\u5168\u90E8\u5FEB\u7167\u3002",
|
|
93
90
|
"cleanup.maxAgeOk": "\u5DF2\u5C06\u81EA\u52A8\u6E05\u7406\u7684\u6700\u5927\u4FDD\u7559\u5929\u6570\u8BBE\u4E3A {days} \u5929\u3002",
|
|
@@ -114,6 +111,14 @@ function translate(lang, key, params = {}) {
|
|
|
114
111
|
return text;
|
|
115
112
|
}
|
|
116
113
|
|
|
114
|
+
// src/session-events.ts
|
|
115
|
+
function eventsOf(session) {
|
|
116
|
+
const s = session;
|
|
117
|
+
if (s.snapshotEvents !== void 0) return s.snapshotEvents();
|
|
118
|
+
if (s.events !== void 0) return s.events;
|
|
119
|
+
return [];
|
|
120
|
+
}
|
|
121
|
+
|
|
117
122
|
// src/settings-locale.ts
|
|
118
123
|
function readSettingsSection(provider, ns, brand) {
|
|
119
124
|
const key = brand?.(ns) ?? ns;
|
|
@@ -1331,16 +1336,49 @@ async function reconcileTracked(store, sessionId, anchorSeq, tracked, probe = de
|
|
|
1331
1336
|
}
|
|
1332
1337
|
|
|
1333
1338
|
// src/snapshot-cleanup.ts
|
|
1334
|
-
import { mkdir as mkdir2, readFile as readFile2, rename as rename2, writeFile as writeFile2 } from "node:fs/promises";
|
|
1339
|
+
import { mkdir as mkdir2, readFile as readFile2, rename as rename2, unlink, writeFile as writeFile2 } from "node:fs/promises";
|
|
1335
1340
|
import { dirname as dirname2, join as join2 } from "node:path";
|
|
1336
1341
|
import { resolveDshHome as resolveDshHome2 } from "@deepseek-ai/dsh-home-paths";
|
|
1342
|
+
import z from "@deepseek-ai/schemastery";
|
|
1337
1343
|
var CLEANUP_CONFIG_FILENAME = "snapshot-cleanup.json";
|
|
1338
|
-
var CLEANUP_CONFIG_ENV = "DSH_SNAPSHOT_CLEANUP_CONFIG";
|
|
1339
1344
|
var DEFAULT_MAX_AGE_DAYS = 30;
|
|
1340
1345
|
var DEFAULT_CLEANUP_CONFIG = { enabled: false, maxAgeDays: DEFAULT_MAX_AGE_DAYS };
|
|
1346
|
+
var CLEANUP_SETTINGS_NAMESPACE = "dsh-rewind-snapshot-cleanup";
|
|
1347
|
+
var CleanupConfigSchema = z.object({
|
|
1348
|
+
enabled: z.boolean().default(DEFAULT_CLEANUP_CONFIG.enabled),
|
|
1349
|
+
maxAgeDays: z.number().step(1).min(1).default(DEFAULT_CLEANUP_CONFIG.maxAgeDays)
|
|
1350
|
+
});
|
|
1351
|
+
function settingsCleanupStore(scope) {
|
|
1352
|
+
return {
|
|
1353
|
+
load: () => scope.get(),
|
|
1354
|
+
save: async (next) => {
|
|
1355
|
+
const parsed = parseCleanupConfig({ enabled: next.enabled, maxAgeDays: next.maxAgeDays });
|
|
1356
|
+
if (!parsed.ok) throw new RangeError(parsed.error);
|
|
1357
|
+
await scope.update({ enabled: parsed.config.enabled, maxAgeDays: parsed.config.maxAgeDays });
|
|
1358
|
+
}
|
|
1359
|
+
};
|
|
1360
|
+
}
|
|
1361
|
+
async function migrateLegacyCleanupConfig(legacyPath, scope, log) {
|
|
1362
|
+
const loaded = await loadCleanupConfig(legacyPath);
|
|
1363
|
+
if (!loaded.ok) {
|
|
1364
|
+
log(`[dsh-rewind] legacy snapshot-cleanup config invalid, migrating defaults and removing: ${loaded.error}`);
|
|
1365
|
+
await scope.update({ enabled: false, maxAgeDays: DEFAULT_MAX_AGE_DAYS });
|
|
1366
|
+
await unlink(legacyPath).catch(() => void 0);
|
|
1367
|
+
return true;
|
|
1368
|
+
}
|
|
1369
|
+
if (!loaded.fromFile) return false;
|
|
1370
|
+
if (loaded.config.enabled === DEFAULT_CLEANUP_CONFIG.enabled && loaded.config.maxAgeDays === DEFAULT_CLEANUP_CONFIG.maxAgeDays) {
|
|
1371
|
+
await unlink(legacyPath).catch(() => void 0);
|
|
1372
|
+
return true;
|
|
1373
|
+
}
|
|
1374
|
+
await scope.update({ enabled: loaded.config.enabled, maxAgeDays: loaded.config.maxAgeDays });
|
|
1375
|
+
await unlink(legacyPath).catch(() => void 0);
|
|
1376
|
+
log(`[dsh-rewind] migrated legacy snapshot-cleanup config (enabled=${String(loaded.config.enabled)}, maxAgeDays=${String(loaded.config.maxAgeDays)})`);
|
|
1377
|
+
return true;
|
|
1378
|
+
}
|
|
1341
1379
|
var AUTO_SWEEP_INTERVAL_MS = 24 * 60 * 60 * 1e3;
|
|
1342
1380
|
function resolveCleanupConfigPath(dshHome) {
|
|
1343
|
-
return
|
|
1381
|
+
return join2(resolveDshHome2(dshHome), CLEANUP_CONFIG_FILENAME);
|
|
1344
1382
|
}
|
|
1345
1383
|
var STATE_FILENAME = "snapshot-cleanup-last-sweep.json";
|
|
1346
1384
|
function resolveCleanupStatePath(dshHome) {
|
|
@@ -1363,7 +1401,7 @@ async function saveLastSweepAt(path, ms) {
|
|
|
1363
1401
|
}
|
|
1364
1402
|
async function runAutoCleanupCheck(deps, sessionId) {
|
|
1365
1403
|
try {
|
|
1366
|
-
const loaded = await
|
|
1404
|
+
const loaded = await deps.readConfig();
|
|
1367
1405
|
if (!loaded.ok) {
|
|
1368
1406
|
deps.log(`[dsh-rewind] snapshot cleanup config invalid; auto-cleanup skipped: ${loaded.error}`);
|
|
1369
1407
|
return;
|
|
@@ -1416,15 +1454,6 @@ async function loadCleanupConfig(path) {
|
|
|
1416
1454
|
if (!parsed.ok) return { ok: false, error: parsed.error };
|
|
1417
1455
|
return { ok: true, config: parsed.config, fromFile: true };
|
|
1418
1456
|
}
|
|
1419
|
-
async function saveCleanupConfig(path, config) {
|
|
1420
|
-
if (typeof config.enabled !== "boolean" || !Number.isInteger(config.maxAgeDays) || config.maxAgeDays <= 0) {
|
|
1421
|
-
throw new RangeError("invalid cleanup config: enabled must be a boolean and maxAgeDays a positive integer");
|
|
1422
|
-
}
|
|
1423
|
-
const tmp = `${path}.tmp`;
|
|
1424
|
-
await mkdir2(dirname2(path), { recursive: true });
|
|
1425
|
-
await writeFile2(tmp, JSON.stringify(config, null, 2), "utf8");
|
|
1426
|
-
await rename2(tmp, path);
|
|
1427
|
-
}
|
|
1428
1457
|
function parseCleanupCommand(rawInput) {
|
|
1429
1458
|
const parts = rawInput.trim().split(/\s+/).filter(Boolean);
|
|
1430
1459
|
if (parts.length === 0) return { action: "status" };
|
|
@@ -1471,6 +1500,7 @@ var inject = ["commands", "tools"];
|
|
|
1471
1500
|
var TRACKED_TOOLS = /* @__PURE__ */ new Set(["write", "edit", "str_replace_editor"]);
|
|
1472
1501
|
var MUTATING_EDITOR_COMMANDS = /* @__PURE__ */ new Set(["create", "str_replace", "insert"]);
|
|
1473
1502
|
var activeLocale = "en";
|
|
1503
|
+
var cleanupStore;
|
|
1474
1504
|
function t(key, params) {
|
|
1475
1505
|
return translate(activeLocale, key, params);
|
|
1476
1506
|
}
|
|
@@ -1494,7 +1524,7 @@ function mutationPathOf(exec) {
|
|
|
1494
1524
|
return void 0;
|
|
1495
1525
|
}
|
|
1496
1526
|
function anchorSeqOf(session, cache) {
|
|
1497
|
-
const events = session
|
|
1527
|
+
const events = eventsOf(session);
|
|
1498
1528
|
const cached = cache.get(session);
|
|
1499
1529
|
if (cached !== void 0 && cached.eventsLength === events.length) return cached.anchor;
|
|
1500
1530
|
let anchor = cached?.anchor;
|
|
@@ -1672,15 +1702,15 @@ async function executeRewind(ctx, store, fs, invocation, rawTarget, mode, inflig
|
|
|
1672
1702
|
}
|
|
1673
1703
|
let plan;
|
|
1674
1704
|
try {
|
|
1675
|
-
plan = resolveOrError(agent.session
|
|
1705
|
+
plan = resolveOrError(eventsOf(agent.session), agent.session.surface.nodes, rawTarget);
|
|
1676
1706
|
} catch (error) {
|
|
1677
1707
|
return rewindErrorResult(error);
|
|
1678
1708
|
}
|
|
1679
1709
|
const marker = buildMarker();
|
|
1680
1710
|
let event;
|
|
1681
1711
|
try {
|
|
1682
|
-
const turn = markerTurnOf(agent.session
|
|
1683
|
-
const step = markerStepOf(agent.session
|
|
1712
|
+
const turn = markerTurnOf(eventsOf(agent.session));
|
|
1713
|
+
const step = markerStepOf(eventsOf(agent.session), turn);
|
|
1684
1714
|
agent.session.append("step/start", { turn, step });
|
|
1685
1715
|
try {
|
|
1686
1716
|
event = agent.session.append("assistant/message", { turn, step, message: marker }, {
|
|
@@ -1700,7 +1730,7 @@ async function executeRewind(ctx, store, fs, invocation, rawTarget, mode, inflig
|
|
|
1700
1730
|
}
|
|
1701
1731
|
let restore = "";
|
|
1702
1732
|
if (mode === "both") {
|
|
1703
|
-
const outcome = await store.restoreAfter(agent.session.id, plan.targetSeq, (path) =>
|
|
1733
|
+
const outcome = await store.restoreAfter(agent.session.id, plan.targetSeq, (path) => unlink2(path));
|
|
1704
1734
|
await syncRestoreObservations(ctx, fs, agent, outcome);
|
|
1705
1735
|
const parts = [];
|
|
1706
1736
|
if (outcome.restored.length > 0) parts.push(t("restore.count", { count: outcome.restored.length }));
|
|
@@ -1734,7 +1764,7 @@ async function handleRewind(ctx, store, fs, invocation, inflight) {
|
|
|
1734
1764
|
const session = invocation.agent.session;
|
|
1735
1765
|
const input = invocation.rawInput.trim();
|
|
1736
1766
|
if (input === "") {
|
|
1737
|
-
const candidates = listRewindCandidates(session
|
|
1767
|
+
const candidates = listRewindCandidates(eventsOf(session), session.surface.nodes, 1);
|
|
1738
1768
|
if (candidates.length === 0) {
|
|
1739
1769
|
return { kind: "error", text: t("noUserMessages") };
|
|
1740
1770
|
}
|
|
@@ -1746,7 +1776,7 @@ async function handleRewind(ctx, store, fs, invocation, inflight) {
|
|
|
1746
1776
|
if (target2 === void 0) return { kind: "error", text: usage() };
|
|
1747
1777
|
let plan;
|
|
1748
1778
|
try {
|
|
1749
|
-
plan = resolveOrError(session
|
|
1779
|
+
plan = resolveOrError(eventsOf(session), session.surface.nodes, target2);
|
|
1750
1780
|
} catch (error) {
|
|
1751
1781
|
return rewindErrorResult(error);
|
|
1752
1782
|
}
|
|
@@ -1754,7 +1784,7 @@ async function handleRewind(ctx, store, fs, invocation, inflight) {
|
|
|
1754
1784
|
return { kind: "success", text: formatPlan(plan, impacts) };
|
|
1755
1785
|
}
|
|
1756
1786
|
if (parts[0] === "__candidates") {
|
|
1757
|
-
const candidates = listRewindCandidates(session
|
|
1787
|
+
const candidates = listRewindCandidates(eventsOf(session), session.surface.nodes);
|
|
1758
1788
|
return { kind: "success", text: formatCandidateList(candidates) };
|
|
1759
1789
|
}
|
|
1760
1790
|
const target = parts[0];
|
|
@@ -1778,11 +1808,21 @@ async function maybeRunAutoCleanup(ctx, store, sessionId, dshHome) {
|
|
|
1778
1808
|
autoSweepChecked = true;
|
|
1779
1809
|
await runAutoCleanupCheck({
|
|
1780
1810
|
pruner: store,
|
|
1781
|
-
|
|
1811
|
+
readConfig: () => readCleanupPolicy(),
|
|
1782
1812
|
statePath: resolveCleanupStatePath(dshHome),
|
|
1783
1813
|
log: (msg) => ctx.logger.warn(msg)
|
|
1784
1814
|
}, sessionId);
|
|
1785
1815
|
}
|
|
1816
|
+
async function readCleanupPolicy() {
|
|
1817
|
+
if (cleanupStore === void 0) {
|
|
1818
|
+
return { ok: false, error: "settings service unavailable; snapshot cleanup policy cannot be read" };
|
|
1819
|
+
}
|
|
1820
|
+
return { ok: true, config: cleanupStore.load() };
|
|
1821
|
+
}
|
|
1822
|
+
async function writeCleanupPolicy(next) {
|
|
1823
|
+
if (cleanupStore === void 0) throw new Error("settings service unavailable; snapshot cleanup policy cannot be written");
|
|
1824
|
+
await cleanupStore.save(next);
|
|
1825
|
+
}
|
|
1786
1826
|
function formatCleanupReport(report) {
|
|
1787
1827
|
const key = report.dryRun ? "cleanup.runDry" : "cleanup.runApply";
|
|
1788
1828
|
const text = t(key, {
|
|
@@ -1797,37 +1837,34 @@ ${t("cleanup.skipped", { skipped: report.skippedActive })}` : text;
|
|
|
1797
1837
|
async function handleSnapshotCleanup(store, invocation, dshHome, trackedBySession) {
|
|
1798
1838
|
const parsed = parseCleanupCommand(invocation.rawInput);
|
|
1799
1839
|
if ("error" in parsed) return { kind: "error", text: t("cleanup.usage") };
|
|
1800
|
-
const configPath = resolveCleanupConfigPath(dshHome);
|
|
1801
1840
|
switch (parsed.action) {
|
|
1802
1841
|
case "status": {
|
|
1803
|
-
const loaded = await
|
|
1842
|
+
const loaded = await readCleanupPolicy();
|
|
1804
1843
|
if (!loaded.ok) return { kind: "error", text: t("cleanup.cfgInvalid", { detail: loaded.error }) };
|
|
1805
1844
|
return {
|
|
1806
1845
|
kind: "success",
|
|
1807
1846
|
text: t("cleanup.status", {
|
|
1808
1847
|
state: t(loaded.config.enabled ? "cleanup.enabled" : "cleanup.disabled"),
|
|
1809
|
-
days: loaded.config.maxAgeDays
|
|
1810
|
-
path: configPath,
|
|
1811
|
-
present: t(loaded.fromFile ? "cleanup.present" : "cleanup.absent")
|
|
1848
|
+
days: loaded.config.maxAgeDays
|
|
1812
1849
|
})
|
|
1813
1850
|
};
|
|
1814
1851
|
}
|
|
1815
1852
|
case "on":
|
|
1816
1853
|
case "off": {
|
|
1817
|
-
const loaded = await
|
|
1854
|
+
const loaded = await readCleanupPolicy();
|
|
1818
1855
|
const next = { ...loaded.ok ? loaded.config : DEFAULT_CLEANUP_CONFIG, enabled: parsed.action === "on" };
|
|
1819
1856
|
try {
|
|
1820
|
-
await
|
|
1857
|
+
await writeCleanupPolicy(next);
|
|
1821
1858
|
} catch (error) {
|
|
1822
1859
|
return { kind: "error", text: t("cleanup.saveFailed", { detail: error instanceof Error ? error.message : String(error) }) };
|
|
1823
1860
|
}
|
|
1824
1861
|
return { kind: "success", text: t(parsed.action === "on" ? "cleanup.onOk" : "cleanup.offOk") };
|
|
1825
1862
|
}
|
|
1826
1863
|
case "max-age": {
|
|
1827
|
-
const loaded = await
|
|
1864
|
+
const loaded = await readCleanupPolicy();
|
|
1828
1865
|
const next = { ...loaded.ok ? loaded.config : DEFAULT_CLEANUP_CONFIG, maxAgeDays: parsed.value };
|
|
1829
1866
|
try {
|
|
1830
|
-
await
|
|
1867
|
+
await writeCleanupPolicy(next);
|
|
1831
1868
|
} catch (error) {
|
|
1832
1869
|
return { kind: "error", text: t("cleanup.saveFailed", { detail: error instanceof Error ? error.message : String(error) }) };
|
|
1833
1870
|
}
|
|
@@ -1838,7 +1875,7 @@ async function handleSnapshotCleanup(store, invocation, dshHome, trackedBySessio
|
|
|
1838
1875
|
if (parsed.target === "current") {
|
|
1839
1876
|
return handleClearCurrent(store, invocation, apply2, trackedBySession);
|
|
1840
1877
|
}
|
|
1841
|
-
const loaded = await
|
|
1878
|
+
const loaded = await readCleanupPolicy();
|
|
1842
1879
|
if (!loaded.ok) return { kind: "error", text: t("cleanup.cfgInvalid", { detail: loaded.error }) };
|
|
1843
1880
|
try {
|
|
1844
1881
|
const report = await store.pruneStale({
|
|
@@ -1901,6 +1938,15 @@ function apply(ctx, config) {
|
|
|
1901
1938
|
if (section?.preference === "zh" || section?.preference === "en") {
|
|
1902
1939
|
activeLocale = section.preference;
|
|
1903
1940
|
}
|
|
1941
|
+
const cleanupScope = settingsCtx.settings.register(CLEANUP_SETTINGS_NAMESPACE, CleanupConfigSchema, { base: DEFAULT_CLEANUP_CONFIG });
|
|
1942
|
+
cleanupStore = settingsCleanupStore(cleanupScope);
|
|
1943
|
+
void migrateLegacyCleanupConfig(
|
|
1944
|
+
resolveCleanupConfigPath(dshHome),
|
|
1945
|
+
cleanupScope,
|
|
1946
|
+
(msg) => ctx.logger.warn(msg)
|
|
1947
|
+
).catch((error) => {
|
|
1948
|
+
ctx.logger.warn(`[dsh-rewind] snapshot cleanup migration failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
1949
|
+
});
|
|
1904
1950
|
});
|
|
1905
1951
|
ctx.effect(function* () {
|
|
1906
1952
|
const rewindHandler = (invocation) => handleRewind(ctx, store, fsService, invocation, inflight);
|
|
@@ -25,6 +25,23 @@ export declare const zh: {
|
|
|
25
25
|
'popover.impact.delete': string;
|
|
26
26
|
'popover.confirm': string;
|
|
27
27
|
'popover.back': string;
|
|
28
|
+
'cleanup.title': string;
|
|
29
|
+
'cleanup.desc': string;
|
|
30
|
+
'cleanup.expand': string;
|
|
31
|
+
'cleanup.collapse': string;
|
|
32
|
+
'cleanup.unsaved': string;
|
|
33
|
+
'cleanup.auto': string;
|
|
34
|
+
'cleanup.auto.on': string;
|
|
35
|
+
'cleanup.auto.off': string;
|
|
36
|
+
'cleanup.maxAge': string;
|
|
37
|
+
'cleanup.maxAge.hint': string;
|
|
38
|
+
'cleanup.invalid': string;
|
|
39
|
+
'cleanup.discard': string;
|
|
40
|
+
'cleanup.save': string;
|
|
41
|
+
'cleanup.saving': string;
|
|
42
|
+
'cleanup.saved': string;
|
|
43
|
+
'cleanup.saveFailed': string;
|
|
44
|
+
'cleanup.readonly': string;
|
|
28
45
|
};
|
|
29
46
|
/** The rewind namespace key union. */
|
|
30
47
|
export type RewindKey = keyof typeof zh;
|
|
@@ -60,4 +77,21 @@ export declare const en: {
|
|
|
60
77
|
'popover.impact.delete': string;
|
|
61
78
|
'popover.confirm': string;
|
|
62
79
|
'popover.back': string;
|
|
80
|
+
'cleanup.title': string;
|
|
81
|
+
'cleanup.desc': string;
|
|
82
|
+
'cleanup.expand': string;
|
|
83
|
+
'cleanup.collapse': string;
|
|
84
|
+
'cleanup.unsaved': string;
|
|
85
|
+
'cleanup.auto': string;
|
|
86
|
+
'cleanup.auto.on': string;
|
|
87
|
+
'cleanup.auto.off': string;
|
|
88
|
+
'cleanup.maxAge': string;
|
|
89
|
+
'cleanup.maxAge.hint': string;
|
|
90
|
+
'cleanup.invalid': string;
|
|
91
|
+
'cleanup.discard': string;
|
|
92
|
+
'cleanup.save': string;
|
|
93
|
+
'cleanup.saving': string;
|
|
94
|
+
'cleanup.saved': string;
|
|
95
|
+
'cleanup.saveFailed': string;
|
|
96
|
+
'cleanup.readonly': string;
|
|
63
97
|
};
|
|
@@ -72,13 +72,14 @@ export interface RewindBridgeDeps {
|
|
|
72
72
|
/** Structural face of the runtime slot service (see the module doc). */
|
|
73
73
|
export interface SlotsLike {
|
|
74
74
|
inject(key: string, install: () => () => void): () => void;
|
|
75
|
-
register(entry: {
|
|
75
|
+
register<P>(entry: {
|
|
76
76
|
readonly name: string;
|
|
77
|
-
readonly id
|
|
78
|
-
readonly order
|
|
79
|
-
|
|
80
|
-
readonly
|
|
81
|
-
|
|
77
|
+
readonly id?: string;
|
|
78
|
+
readonly order?: number;
|
|
79
|
+
readonly key?: string;
|
|
80
|
+
readonly locale?: string;
|
|
81
|
+
readonly inject?: () => P;
|
|
82
|
+
}, component: (props: P) => ReactNode): () => void;
|
|
82
83
|
}
|
|
83
84
|
/** Join the text blocks of a user message into one plain preview. */
|
|
84
85
|
/**
|
|
@@ -124,6 +125,28 @@ export declare function writeComposer(text: string, facade: ComposerDraftWriter
|
|
|
124
125
|
* after switching sessions or restarting dsh.
|
|
125
126
|
*/
|
|
126
127
|
export declare function runRewindAndFill(session: SessionFace, seq: number, mode: 'chat' | 'both', currentSessionId: () => string | undefined, chatOf: ChatOf, setComposerText: (sessionId: string, text: string) => boolean): Promise<void>;
|
|
128
|
+
/**
|
|
129
|
+
* Locate the actions container of a user/steering seat row — the element the
|
|
130
|
+
* ↶ button portals into (the copy/branch IconActions row).
|
|
131
|
+
*
|
|
132
|
+
* Dual channel:
|
|
133
|
+
* - rc.2 (0.1.1-rc.2) / alpha.2+ (0.1.2-alpha.2): the hover-reveal root is
|
|
134
|
+
* attribute-marked (`[data-time-hover-root]` ≤ rc.x, `[data-actions-reveal]`
|
|
135
|
+
* alpha.2+) and mounts the actions row as its LAST child. A pending row IS
|
|
136
|
+
* that root; a durable row CONTAINS it as a descendant.
|
|
137
|
+
* - alpha.4 (0.1.2-alpha.4): the marker was removed from user rows (it now
|
|
138
|
+
* lives only on the per-turn tail footer, `TurnTailNodeView`), and the user
|
|
139
|
+
* action row is revealed via CSS `:has()`. The container is instead located
|
|
140
|
+
* structurally: the direct holder of the copy `<button>` (the
|
|
141
|
+
* `MessageIconActions` container, which mounts that button as a direct
|
|
142
|
+
* child — `MessageIconActions.tsx:83,86`).
|
|
143
|
+
*
|
|
144
|
+
* Returns undefined when no qualifying container is found; the caller refuses
|
|
145
|
+
* to portal (never a crash, never a wrong attachment). Exported as a test seam
|
|
146
|
+
* (see `collectTargets`) so the dual-channel finder is exercised directly for
|
|
147
|
+
* both durable and pending row shapes without a full React portal render.
|
|
148
|
+
*/
|
|
149
|
+
export declare function actionsContainerOf(row: HTMLElement | undefined): HTMLElement | undefined;
|
|
127
150
|
/**
|
|
128
151
|
* Collect the portal targets of one session: user rows × snapshot nodes.
|
|
129
152
|
* Exported as a test seam — the DOM→targets pairing that drives the ↶ button
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-rewind client settings card: the "Snapshot cleanup" module under
|
|
3
|
+
* Settings > Plugins > Plugin configuration, drawn as one `settings.plugin.item`
|
|
4
|
+
* card (keyed by the host-registered settings namespace).
|
|
5
|
+
*
|
|
6
|
+
* The card edits exactly two knobs — `enabled` (auto-cleanup switch) and
|
|
7
|
+
* `maxAgeDays` (idle cutoff, a positive integer) — and stages them exactly like
|
|
8
|
+
* the host-side /snapshot-auto-cleanup command does, so the GUI and the command
|
|
9
|
+
* can never disagree. The switch collapses/expands the max-age editor; a
|
|
10
|
+
* non-positive/non-integer draft blocks save (the same single validator the
|
|
11
|
+
* host schema enforces). "Discard changes" restores the last-read baseline.
|
|
12
|
+
*
|
|
13
|
+
* It neither imports the client settings typed contract nor depends on the
|
|
14
|
+
* alpha-only `mutate` write API: it reads `getSnapshot().value` and writes via
|
|
15
|
+
* the `set(field, value)` method present on both rc.2 and alpha, and the card
|
|
16
|
+
* receives a tiny structural `CleanupCardApi` supplied by `src/client/index.ts`
|
|
17
|
+
* so the component stays harness-agnostic and unit-testable in isolation.
|
|
18
|
+
*
|
|
19
|
+
* @module dsh-rewind/client/settings-card
|
|
20
|
+
*/
|
|
21
|
+
/**
|
|
22
|
+
* The dsh-settings namespace the card binds to. Duplicated here (not imported
|
|
23
|
+
* from the host module) because the client build must stay free of host/node
|
|
24
|
+
* imports; a cross-config test pins it equal to the host's constant. The
|
|
25
|
+
* settings grammar forbids dots, so this is hyphenated.
|
|
26
|
+
*/
|
|
27
|
+
export declare const CLEANUP_SETTINGS_NAMESPACE = "dsh-rewind-snapshot-cleanup";
|
|
28
|
+
/** The defaults the host uses; shown as the field placeholder until a draft. */
|
|
29
|
+
export declare const DEFAULT_MAX_AGE_DAYS = 30;
|
|
30
|
+
/** The two editable knobs, exactly as the host policy exposes them. */
|
|
31
|
+
export interface CleanupPolicy {
|
|
32
|
+
readonly enabled: boolean;
|
|
33
|
+
readonly maxAgeDays: number;
|
|
34
|
+
}
|
|
35
|
+
/** A staged draft: the switch state and the raw (unparsed) max-age text. */
|
|
36
|
+
export interface CleanupDraft {
|
|
37
|
+
readonly enabled: boolean;
|
|
38
|
+
readonly maxAgeDays: string;
|
|
39
|
+
}
|
|
40
|
+
/** The structural api the card reads/saves through (supplied by the client). */
|
|
41
|
+
export interface CleanupCardApi {
|
|
42
|
+
/** Read the resolved policy; `undefined` while the describe mirror loads. */
|
|
43
|
+
read(): CleanupPolicy | undefined;
|
|
44
|
+
/** Whether the settings source accepts writes (false = read-only card). */
|
|
45
|
+
writable(): boolean;
|
|
46
|
+
/** Persist a validated policy; rejects on failure. */
|
|
47
|
+
save(next: CleanupPolicy): Promise<void>;
|
|
48
|
+
/** Optional change subscription (returns the disposer). */
|
|
49
|
+
subscribe(cb: () => void): () => void;
|
|
50
|
+
}
|
|
51
|
+
/** Translate one client dictionary key (the card's `t`). */
|
|
52
|
+
export type CardTranslate = (key: string, params?: Record<string, string | number>) => string;
|
|
53
|
+
/** Load a draft from a policy (defaults when the view has not loaded). */
|
|
54
|
+
export declare function draftFrom(policy: CleanupPolicy | undefined): CleanupDraft;
|
|
55
|
+
/** Parse the max-age text: a strict positive integer, else `null`. */
|
|
56
|
+
export declare function maxAgeOf(text: string): number | null;
|
|
57
|
+
/**
|
|
58
|
+
* The policy a draft resolves to, or `null` when the max-age draft is invalid
|
|
59
|
+
* (which blocks save). `enabled` is always a boolean from the switch, and
|
|
60
|
+
* `maxAgeDays` comes from the validated draft.
|
|
61
|
+
*/
|
|
62
|
+
export declare function configOf(draft: CleanupDraft): CleanupPolicy | null;
|
|
63
|
+
/** True when the draft differs from the baseline (an unsaved edit). */
|
|
64
|
+
export declare function dirtyOf(base: CleanupDraft, draft: CleanupDraft): boolean;
|
|
65
|
+
/**
|
|
66
|
+
* The card body. Draws the switch (+ collapse), the max-age editor, and the
|
|
67
|
+
* discard/save actions. Pure of host wiring: everything goes through the
|
|
68
|
+
* supplied {@link CleanupCardApi}.
|
|
69
|
+
* @param api - the read/write transport.
|
|
70
|
+
* @param t - the client dictionary translator.
|
|
71
|
+
* @returns the card element.
|
|
72
|
+
*/
|
|
73
|
+
export declare function SettingsCleanupCard({ api, t }: {
|
|
74
|
+
api: CleanupCardApi;
|
|
75
|
+
t: CardTranslate;
|
|
76
|
+
}): import("react").JSX.Element;
|
|
@@ -23,4 +23,4 @@ export declare const CLASS: {
|
|
|
23
23
|
/** The ↶ glyph, drawn inline so the bundle stays dependency-free. */
|
|
24
24
|
export declare const REWIND_ICON_SVG: string;
|
|
25
25
|
/** One injected stylesheet (scoped under `.dsh-rewind-*`). */
|
|
26
|
-
export declare const STYLE = "\n.dsh-rewind-btn {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 28px;\n height: 28px;\n padding: 6px;\n border: none;\n border-radius: 28px;\n background: transparent;\n color: var(--dsw-alias-label-tertiary);\n cursor: pointer;\n}\n.dsh-rewind-btn:hover {\n background: var(--dsw-alias-interactive-bg-hover);\n color: var(--dsw-alias-label-secondary);\n}\n\n.dsh-rewind-popover {\n position: fixed;\n z-index: 1000;\n width: 288px;\n padding: 12px;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 12px;\n background: var(--dsw-specific-menu, var(--dsw-alias-bg-layer-3));\n box-shadow: var(--dsw-shadow-lv3);\n font-size: 14px;\n line-height: 20px;\n color: var(--dsw-alias-label-primary);\n}\n.dsh-rewind-popover-title {\n font-size: 14px;\n font-weight: 600;\n line-height: 20px;\n}\n.dsh-rewind-popover-target {\n margin: 4px 0 10px;\n font-size: 12px;\n line-height: 16px;\n color: var(--dsw-alias-label-tertiary);\n word-break: break-all;\n}\n.dsh-rewind-popover-option {\n display: flex;\n flex-direction: column;\n gap: 2px;\n width: 100%;\n margin: 0 0 6px;\n padding: 8px 10px;\n border: 1px solid transparent;\n border-radius: 8px;\n background: transparent;\n color: inherit;\n font: inherit;\n text-align: left;\n cursor: pointer;\n}\n.dsh-rewind-popover-option:hover {\n background: var(--dsw-alias-interactive-bg-hover);\n}\n.dsh-rewind-popover-option:disabled {\n opacity: 0.5;\n cursor: default;\n}\n.dsh-rewind-popover-option-label {\n font-weight: 500;\n}\n.dsh-rewind-popover-option-hint {\n font-size: 12px;\n line-height: 16px;\n color: var(--dsw-alias-label-tertiary);\n}\n.dsh-rewind-popover-impact {\n margin: 4px 0 10px;\n padding: 8px 10px;\n border-radius: 8px;\n background: var(--dsw-alias-interactive-bg-hover);\n font-size: 12px;\n line-height: 16px;\n color: var(--dsw-alias-label-secondary);\n white-space: pre-wrap;\n max-height: 160px;\n overflow: auto;\n}\n.dsh-rewind-popover-actions {\n display: flex;\n justify-content: flex-end;\n gap: 8px;\n}\n.dsh-rewind-popover-primary,\n.dsh-rewind-popover-ghost {\n padding: 5px 12px;\n border: none;\n border-radius: 8px;\n font: inherit;\n font-size: 13px;\n line-height: 18px;\n cursor: pointer;\n}\n.dsh-rewind-popover-primary {\n background: var(--dsw-alias-button-primary-fill);\n color: var(--dsw-alias-label-primary-foreground);\n}\n.dsh-rewind-popover-primary:hover:not(:disabled) {\n background: var(--dsw-alias-button-primary-hover);\n}\n.dsh-rewind-popover-primary:disabled {\n opacity: 0.5;\n cursor: default;\n}\n.dsh-rewind-popover-ghost {\n background: transparent;\n color: var(--dsw-alias-label-secondary);\n}\n.dsh-rewind-popover-ghost:hover {\n background: var(--dsw-alias-interactive-bg-hover);\n}\n\n.dsh-rewind-guard-hint {\n position: fixed;\n z-index: 1000;\n max-width: min(440px, calc(100vw - 24px));\n padding: 8px 12px;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 10px;\n background: var(--dsw-specific-menu, var(--dsw-alias-bg-layer-3));\n box-shadow: var(--dsw-shadow-lv3);\n font-size: 13px;\n line-height: 18px;\n color: var(--dsw-alias-label-primary);\n pointer-events: none;\n}\n";
|
|
26
|
+
export declare const STYLE = "\n.dsh-rewind-btn {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 28px;\n height: 28px;\n padding: 6px;\n border: none;\n border-radius: 28px;\n background: transparent;\n color: var(--dsw-alias-label-tertiary);\n cursor: pointer;\n}\n.dsh-rewind-btn:hover {\n background: var(--dsw-alias-interactive-bg-hover);\n color: var(--dsw-alias-label-secondary);\n}\n\n.dsh-rewind-popover {\n position: fixed;\n z-index: 1000;\n width: 288px;\n padding: 12px;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 12px;\n background: var(--dsw-specific-menu, var(--dsw-alias-bg-layer-3));\n box-shadow: var(--dsw-shadow-lv3);\n font-size: 14px;\n line-height: 20px;\n color: var(--dsw-alias-label-primary);\n}\n.dsh-rewind-popover-title {\n font-size: 14px;\n font-weight: 600;\n line-height: 20px;\n}\n.dsh-rewind-popover-target {\n margin: 4px 0 10px;\n font-size: 12px;\n line-height: 16px;\n color: var(--dsw-alias-label-tertiary);\n word-break: break-all;\n}\n.dsh-rewind-popover-option {\n display: flex;\n flex-direction: column;\n gap: 2px;\n width: 100%;\n margin: 0 0 6px;\n padding: 8px 10px;\n border: 1px solid transparent;\n border-radius: 8px;\n background: transparent;\n color: inherit;\n font: inherit;\n text-align: left;\n cursor: pointer;\n}\n.dsh-rewind-popover-option:hover {\n background: var(--dsw-alias-interactive-bg-hover);\n}\n.dsh-rewind-popover-option:disabled {\n opacity: 0.5;\n cursor: default;\n}\n.dsh-rewind-popover-option-label {\n font-weight: 500;\n}\n.dsh-rewind-popover-option-hint {\n font-size: 12px;\n line-height: 16px;\n color: var(--dsw-alias-label-tertiary);\n}\n.dsh-rewind-popover-impact {\n margin: 4px 0 10px;\n padding: 8px 10px;\n border-radius: 8px;\n background: var(--dsw-alias-interactive-bg-hover);\n font-size: 12px;\n line-height: 16px;\n color: var(--dsw-alias-label-secondary);\n white-space: pre-wrap;\n max-height: 160px;\n overflow: auto;\n}\n.dsh-rewind-popover-actions {\n display: flex;\n justify-content: flex-end;\n gap: 8px;\n}\n.dsh-rewind-popover-primary,\n.dsh-rewind-popover-ghost {\n padding: 5px 12px;\n border: none;\n border-radius: 8px;\n font: inherit;\n font-size: 13px;\n line-height: 18px;\n cursor: pointer;\n}\n.dsh-rewind-popover-primary {\n background: var(--dsw-alias-button-primary-fill);\n color: var(--dsw-alias-label-primary-foreground);\n}\n.dsh-rewind-popover-primary:hover:not(:disabled) {\n background: var(--dsw-alias-button-primary-hover);\n}\n.dsh-rewind-popover-primary:disabled {\n opacity: 0.5;\n cursor: default;\n}\n.dsh-rewind-popover-ghost {\n background: transparent;\n color: var(--dsw-alias-label-secondary);\n}\n.dsh-rewind-popover-ghost:hover {\n background: var(--dsw-alias-interactive-bg-hover);\n}\n\n.dsh-rewind-guard-hint {\n position: fixed;\n z-index: 1000;\n max-width: min(440px, calc(100vw - 24px));\n padding: 8px 12px;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 10px;\n background: var(--dsw-specific-menu, var(--dsw-alias-bg-layer-3));\n box-shadow: var(--dsw-shadow-lv3);\n font-size: 13px;\n line-height: 18px;\n color: var(--dsw-alias-label-primary);\n pointer-events: none;\n}\n\n/* ---- Snapshot-cleanup settings card (mirrors the harness PluginCard look) ---- */\n.dsh-rewind-cleanup-card {\n list-style: none;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 12px;\n background: var(--dsw-alias-bg-layer-3);\n transition: border-color .16s, background .16s;\n}\n.dsh-rewind-cleanup-card:hover {\n border-color: var(--dsw-alias-label-dimmed);\n}\n.dsh-rewind-cleanup-card-open {\n background: var(--dsw-alias-bg-layer-2);\n border-color: var(--dsw-alias-label-dimmed);\n}\n.dsh-rewind-cleanup-header {\n width: 100%;\n appearance: none;\n border: 0;\n background: none;\n font: inherit;\n color: inherit;\n text-align: left;\n cursor: pointer;\n display: flex;\n align-items: center;\n gap: 12px;\n padding: 14px 16px;\n border-radius: 12px;\n}\n.dsh-rewind-cleanup-header:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: -2px;\n}\n.dsh-rewind-cleanup-head-text {\n flex: 1;\n min-width: 0;\n display: flex;\n flex-direction: column;\n gap: 4px;\n}\n.dsh-rewind-cleanup-name {\n font-size: 15px;\n font-weight: 600;\n line-height: 1.4;\n color: var(--dsw-alias-label-primary);\n}\n.dsh-rewind-cleanup-desc {\n font-size: 13px;\n line-height: 1.5;\n color: var(--dsw-alias-label-tertiary);\n}\n.dsh-rewind-cleanup-chevron {\n flex: none;\n color: var(--dsw-alias-label-tertiary);\n transition: transform .16s;\n}\n.dsh-rewind-cleanup-chevron-open {\n transform: rotate(180deg);\n}\n.dsh-rewind-cleanup-pending {\n flex: none;\n border-radius: 999px;\n padding: 1px 8px;\n font-size: 11px;\n line-height: 17px;\n font-weight: 500;\n white-space: nowrap;\n background: var(--dsw-alias-bg-module-platform);\n color: var(--dsw-alias-label-secondary);\n}\n.dsh-rewind-cleanup-body {\n border-top: 1px solid var(--dsw-alias-border-l2);\n margin: 0 16px;\n padding: 4px 0 8px;\n}\n.dsh-rewind-cleanup-readonly {\n margin: 12px 0 0;\n font-size: 12px;\n line-height: 1.5;\n color: var(--dsw-alias-label-tertiary);\n}\n.dsh-rewind-cleanup-permission {\n display: grid;\n gap: 6px;\n padding: 12px 0;\n}\n.dsh-rewind-cleanup-field {\n display: flex;\n flex-direction: column;\n gap: 6px;\n padding: 12px 0;\n}\n.dsh-rewind-cleanup-field + .dsh-rewind-cleanup-field {\n border-top: 1px solid var(--dsw-alias-border-l2);\n}\n.dsh-rewind-cleanup-head {\n display: flex;\n align-items: center;\n gap: 8px;\n}\n.dsh-rewind-cleanup-label {\n flex: 1;\n min-width: 0;\n font-size: 13px;\n font-weight: 500;\n line-height: 1.5;\n color: var(--dsw-alias-label-primary);\n}\n.dsh-rewind-cleanup-hint {\n margin: 0;\n font-size: 12px;\n line-height: 1.5;\n color: var(--dsw-alias-label-tertiary);\n}\n.dsh-rewind-cleanup-error {\n margin: 0;\n font-size: 12px;\n line-height: 1.5;\n color: var(--dsw-alias-label-error);\n}\n/* Switch row: label left, role=switch button right, hint below (Subagent module). */\n.dsh-rewind-cleanup-toggle-row {\n display: flex;\n align-items: flex-start;\n justify-content: space-between;\n gap: 16px;\n font-size: 13px;\n line-height: 1.5;\n color: var(--dsw-alias-label-primary);\n}\n.dsh-rewind-cleanup-toggle-label {\n flex: 1;\n min-width: 0;\n}\n.dsh-rewind-cleanup-switch {\n box-sizing: border-box;\n position: relative;\n flex: 0 0 auto;\n width: 36px;\n height: 20px;\n padding: 2px;\n border: 0;\n border-radius: 10px;\n background: var(--dsw-alias-border-l3);\n cursor: pointer;\n}\n.dsh-rewind-cleanup-switch-on {\n background: var(--dsw-alias-brand-primary);\n}\n.dsh-rewind-cleanup-switch:disabled {\n cursor: default;\n opacity: 0.5;\n}\n.dsh-rewind-cleanup-switch:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 2px;\n}\n.dsh-rewind-cleanup-thumb {\n display: block;\n width: 16px;\n height: 16px;\n border-radius: 50%;\n corner-shape: round;\n background: var(--dsw-alias-label-primary-foreground);\n transition: transform 120ms ease;\n}\n.dsh-rewind-cleanup-switch-on .dsh-rewind-cleanup-thumb {\n transform: translateX(16px);\n}\n.dsh-rewind-cleanup-input {\n box-sizing: border-box;\n height: 34px;\n padding: 0 12px;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 8px;\n background: var(--dsw-alias-bg-layer-3);\n font: inherit;\n font-size: 13px;\n line-height: 1.5;\n color: var(--dsw-alias-label-primary);\n}\n.dsh-rewind-cleanup-input:focus-visible {\n outline: none;\n border-color: var(--dsw-alias-brand-primary);\n}\n.dsh-rewind-cleanup-input:disabled {\n color: var(--dsw-alias-label-tertiary);\n cursor: default;\n}\n.dsh-rewind-cleanup-input-invalid {\n border-color: var(--dsw-alias-label-error);\n}\n.dsh-rewind-cleanup-footer {\n display: flex;\n align-items: center;\n justify-content: flex-end;\n gap: 8px;\n padding: 12px 0 4px;\n border-top: 1px solid var(--dsw-alias-border-l2);\n}\n.dsh-rewind-cleanup-failed {\n flex: 1;\n min-width: 0;\n margin: 0;\n font-size: 12px;\n line-height: 1.5;\n color: var(--dsw-alias-label-error);\n}\n.dsh-rewind-cleanup-discard,\n.dsh-rewind-cleanup-save {\n appearance: none;\n border: 1px solid transparent;\n border-radius: 8px;\n padding: 5px 14px;\n font: inherit;\n font-size: 13px;\n line-height: 1.5;\n cursor: pointer;\n}\n.dsh-rewind-cleanup-discard {\n border-color: var(--dsw-alias-border-l2);\n background: none;\n color: var(--dsw-alias-label-secondary);\n}\n.dsh-rewind-cleanup-discard:hover:not(:disabled) {\n color: var(--dsw-alias-label-primary);\n border-color: var(--dsw-alias-label-dimmed);\n}\n.dsh-rewind-cleanup-save {\n background: var(--dsw-alias-label-primary);\n color: var(--dsw-alias-bg-layer-3);\n}\n.dsh-rewind-cleanup-discard:disabled,\n.dsh-rewind-cleanup-save:disabled {\n opacity: 0.4;\n cursor: default;\n}\n.dsh-rewind-cleanup-discard:focus-visible,\n.dsh-rewind-cleanup-save:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n";
|
package/lib/types/locales.d.ts
CHANGED
|
@@ -54,8 +54,6 @@ export declare const en: {
|
|
|
54
54
|
'cleanup.status': string;
|
|
55
55
|
'cleanup.enabled': string;
|
|
56
56
|
'cleanup.disabled': string;
|
|
57
|
-
'cleanup.present': string;
|
|
58
|
-
'cleanup.absent': string;
|
|
59
57
|
'cleanup.onOk': string;
|
|
60
58
|
'cleanup.offOk': string;
|
|
61
59
|
'cleanup.maxAgeOk': string;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross-channel session event-log reader.
|
|
3
|
+
*
|
|
4
|
+
* Harness 0.1.1-rc.2 exposes the full log as `Session.events`
|
|
5
|
+
* (`get events(): readonly SessionEvent[]`), while 0.1.2-alpha.4 removed that
|
|
6
|
+
* member and replaced it with the on-demand APIs `snapshotEvents(from?, to?)`
|
|
7
|
+
* (half-open range, internally cached), `eventAt(seq)`, `ownEvents()` and
|
|
8
|
+
* `seq`. This module reads the full log through whichever channel the host
|
|
9
|
+
* exposes, so the plugin keeps one code path across both.
|
|
10
|
+
*
|
|
11
|
+
* `snapshotEvents()` is deliberately preferred over `ownEvents()`:
|
|
12
|
+
* `snapshotEvents()` defaults to the whole log (including any fork-inherited
|
|
13
|
+
* prefix), which is exactly what `Session.events` returned on rc.2 — the
|
|
14
|
+
* rewind semantics are preserved for fork-seeded sessions. `ownEvents()`
|
|
15
|
+
* would drop the inherited prefix and change behaviour.
|
|
16
|
+
*
|
|
17
|
+
* @module dsh-rewind/session-events
|
|
18
|
+
*/
|
|
19
|
+
import type { Session, SessionEvent } from '@deepseek-ai/dsh-session';
|
|
20
|
+
/**
|
|
21
|
+
* Read the full event log of a session, transparently across harness channels:
|
|
22
|
+
* - 0.1.1-rc.2 (`Session.events`): the live immutable log array.
|
|
23
|
+
* - 0.1.2-alpha.4 (`Session.snapshotEvents()`): a deep-frozen snapshot, cached
|
|
24
|
+
* by the session until the next append.
|
|
25
|
+
*
|
|
26
|
+
* Falls back to an empty log when the session exposes neither member (a future
|
|
27
|
+
* harness shape) rather than throwing — callers already treat the empty log as
|
|
28
|
+
* "no candidates".
|
|
29
|
+
*
|
|
30
|
+
* @param session - the session to read.
|
|
31
|
+
* @returns the full event log in log order (the same shape rc.2's `events`
|
|
32
|
+
* returned, so existing consumers are unchanged).
|
|
33
|
+
*/
|
|
34
|
+
export declare function eventsOf(session: Session): readonly SessionEvent[];
|
|
@@ -18,25 +18,79 @@
|
|
|
18
18
|
*
|
|
19
19
|
* @module dsh-rewind/snapshot-cleanup
|
|
20
20
|
*/
|
|
21
|
+
import z from '@deepseek-ai/schemastery';
|
|
21
22
|
/** The cleanup policy, as persisted under `~/.dsh/snapshot-cleanup.json`. */
|
|
22
23
|
export interface CleanupConfig {
|
|
23
24
|
readonly enabled: boolean;
|
|
24
25
|
readonly maxAgeDays: number;
|
|
25
26
|
}
|
|
26
27
|
export declare const CLEANUP_CONFIG_FILENAME = "snapshot-cleanup.json";
|
|
27
|
-
/** Environment variable overriding the config file path. */
|
|
28
|
-
export declare const CLEANUP_CONFIG_ENV = "DSH_SNAPSHOT_CLEANUP_CONFIG";
|
|
29
28
|
/** The default keep threshold: finished sessions idle > 30 days are pruned. */
|
|
30
29
|
export declare const DEFAULT_MAX_AGE_DAYS = 30;
|
|
31
30
|
/** The safe default policy (off) — a missing/corrupt file behaves like this. */
|
|
32
31
|
export declare const DEFAULT_CLEANUP_CONFIG: CleanupConfig;
|
|
32
|
+
/**
|
|
33
|
+
* The dsh-settings namespace that backs the cleanup policy after migration.
|
|
34
|
+
* Namespaces must match the settings provider's `^[a-z][a-z0-9-]*$` grammar (no
|
|
35
|
+
* dots), so this is hyphenated, not dotted.
|
|
36
|
+
*/
|
|
37
|
+
export declare const CLEANUP_SETTINGS_NAMESPACE = "dsh-rewind-snapshot-cleanup";
|
|
38
|
+
/**
|
|
39
|
+
* The schemastery schema that persists + validates the cleanup policy in the
|
|
40
|
+
* dsh-settings document. This is the SINGLE storage validator: the `maxAgeDays`
|
|
41
|
+
* rule is enforced by `.step(1).min(1)` (positive integer) and the defaults by
|
|
42
|
+
* `.default(...)`, so the resolved value is always a valid {@link CleanupConfig}
|
|
43
|
+
* and a bad stored/user value cannot steer the sweep into deleting everything.
|
|
44
|
+
*/
|
|
45
|
+
export declare const CleanupConfigSchema: z<CleanupConfig>;
|
|
46
|
+
/**
|
|
47
|
+
* Structural face of the settings scope the host needs for the policy: a
|
|
48
|
+
* resolved read and a validated write. Kept local (never imports the settings
|
|
49
|
+
* contract) so the host bundle links on both rc.2 and alpha — the settings
|
|
50
|
+
* API drift (alpha adds `mutate`, rd2 does not) is confined to the seam the
|
|
51
|
+
* host passes in, never to this module.
|
|
52
|
+
*/
|
|
53
|
+
export interface CleanupSettingsScope {
|
|
54
|
+
/** The resolved policy: schema defaults, then base, then the user layer. */
|
|
55
|
+
get(): CleanupConfig;
|
|
56
|
+
/** Merge a partial patch into the user layer (validated by the schema). */
|
|
57
|
+
update(patch: {
|
|
58
|
+
enabled?: boolean;
|
|
59
|
+
maxAgeDays?: number;
|
|
60
|
+
}): Promise<void>;
|
|
61
|
+
}
|
|
62
|
+
/** A validated policy read/write port the command + auto-sweep use. */
|
|
63
|
+
export interface CleanupConfigStore {
|
|
64
|
+
/** The resolved policy (always schema-valid, fail-closes when unavailable). */
|
|
65
|
+
load(): CleanupConfig;
|
|
66
|
+
/** Persist a validated policy, throwing when invalid or unavailable. */
|
|
67
|
+
save(next: CleanupConfig): Promise<void>;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Adapter that turns a {@link CleanupSettingsScope} into a
|
|
71
|
+
* {@link CleanupConfigStore}. Reads come straight from the resolved scope; a
|
|
72
|
+
* write validates via `parseCleanupConfig` before touching the scope, so a bad
|
|
73
|
+
* value can never reach the document (defense-in-depth below the schema).
|
|
74
|
+
*/
|
|
75
|
+
export declare function settingsCleanupStore(scope: CleanupSettingsScope): CleanupConfigStore;
|
|
76
|
+
/**
|
|
77
|
+
* One-time migration of the pre-GUI cleanup policy file into the settings
|
|
78
|
+
* document. Idempotent and cheap: it is called on every startup but only does
|
|
79
|
+
* work once — a present-and-parsed legacy file is written into the scope and
|
|
80
|
+
* then deleted, after which the read is an ENOENT no-op. A missing file is a
|
|
81
|
+
* no-op; an invalid file writes the safe default (deleting nothing) and logs.
|
|
82
|
+
* This is the ONLY consumption of {@link loadCleanupConfig} after migration.
|
|
83
|
+
* @returns whether a legacy file was actually migrated.
|
|
84
|
+
*/
|
|
85
|
+
export declare function migrateLegacyCleanupConfig(legacyPath: string, scope: CleanupSettingsScope, log: (msg: string) => void): Promise<boolean>;
|
|
33
86
|
/** Auto-sweep cadence (the user's hardcoded 24h rhythm — not user-set). */
|
|
34
87
|
export declare const AUTO_SWEEP_INTERVAL_MS: number;
|
|
35
88
|
/**
|
|
36
|
-
* Resolve the config file path (
|
|
37
|
-
*
|
|
38
|
-
* `dshHome` (config.dshHome > `$DSH_HOME` > `~/.dsh`) so the
|
|
39
|
-
* harness home instead of hardcoding `~/.dsh`.
|
|
89
|
+
* Resolve the LEGACY pre-migration config file path (the only remaining use of
|
|
90
|
+
* the file store): `<harness home>/snapshot-cleanup.json`, derived from
|
|
91
|
+
* `dshHome` (config.dshHome > `$DSH_HOME` > `~/.dsh`) so the migration follows
|
|
92
|
+
* the harness home instead of hardcoding `~/.dsh`. The `DSH_SNAPSHOT_CLEANUP_CONFIG`
|
|
93
|
+
* env override was removed when the policy moved into the dsh-settings document.
|
|
40
94
|
*/
|
|
41
95
|
export declare function resolveCleanupConfigPath(dshHome?: string): string;
|
|
42
96
|
/** The state file that records the last automatic-sweep wall-clock time. */
|
|
@@ -74,7 +128,13 @@ export interface AutoCleanupPruner {
|
|
|
74
128
|
*/
|
|
75
129
|
export declare function runAutoCleanupCheck(deps: {
|
|
76
130
|
pruner: AutoCleanupPruner;
|
|
77
|
-
|
|
131
|
+
readConfig: () => Promise<{
|
|
132
|
+
ok: true;
|
|
133
|
+
config: CleanupConfig;
|
|
134
|
+
} | {
|
|
135
|
+
ok: false;
|
|
136
|
+
error: string;
|
|
137
|
+
}>;
|
|
78
138
|
statePath: string;
|
|
79
139
|
log: (msg: string) => void;
|
|
80
140
|
}, sessionId: string | undefined): Promise<void>;
|