dsh-rewind-plugin 0.6.2 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +16 -12
- package/README.md +12 -9
- package/SECURITY.md +14 -7
- package/assets/screenshots/cleanup-setting.png +0 -0
- package/docs/architecture.md +1 -1
- package/docs/compat/audit.md +31 -20
- package/docs/format.md +11 -0
- package/docs/release/release.md +6 -6
- package/docs/release/release.zh.md +5 -4
- package/docs/snapshot-auto-cleanup.md +4 -12
- package/docs/snapshot-auto-cleanup.zh.md +4 -8
- package/lib/client.js +528 -17
- package/lib/index.js +82 -34
- package/lib/types/client/hidden.d.ts +10 -0
- package/lib/types/client/locales.d.ts +34 -0
- package/lib/types/client/portals.d.ts +8 -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/settings-locale.d.ts +32 -0
- package/lib/types/snapshot-cleanup.d.ts +67 -7
- package/package.json +24 -13
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";
|
|
4
|
-
import
|
|
3
|
+
import { unlink as unlink2 } from "node:fs/promises";
|
|
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,12 @@ function translate(lang, key, params = {}) {
|
|
|
114
111
|
return text;
|
|
115
112
|
}
|
|
116
113
|
|
|
114
|
+
// src/settings-locale.ts
|
|
115
|
+
function readSettingsSection(provider, ns, brand) {
|
|
116
|
+
const key = brand?.(ns) ?? ns;
|
|
117
|
+
return provider.get(key);
|
|
118
|
+
}
|
|
119
|
+
|
|
117
120
|
// src/rewind.ts
|
|
118
121
|
var RewindError = class extends Error {
|
|
119
122
|
constructor(code, message) {
|
|
@@ -1325,16 +1328,49 @@ async function reconcileTracked(store, sessionId, anchorSeq, tracked, probe = de
|
|
|
1325
1328
|
}
|
|
1326
1329
|
|
|
1327
1330
|
// src/snapshot-cleanup.ts
|
|
1328
|
-
import { mkdir as mkdir2, readFile as readFile2, rename as rename2, writeFile as writeFile2 } from "node:fs/promises";
|
|
1331
|
+
import { mkdir as mkdir2, readFile as readFile2, rename as rename2, unlink, writeFile as writeFile2 } from "node:fs/promises";
|
|
1329
1332
|
import { dirname as dirname2, join as join2 } from "node:path";
|
|
1330
1333
|
import { resolveDshHome as resolveDshHome2 } from "@deepseek-ai/dsh-home-paths";
|
|
1334
|
+
import z from "@deepseek-ai/schemastery";
|
|
1331
1335
|
var CLEANUP_CONFIG_FILENAME = "snapshot-cleanup.json";
|
|
1332
|
-
var CLEANUP_CONFIG_ENV = "DSH_SNAPSHOT_CLEANUP_CONFIG";
|
|
1333
1336
|
var DEFAULT_MAX_AGE_DAYS = 30;
|
|
1334
1337
|
var DEFAULT_CLEANUP_CONFIG = { enabled: false, maxAgeDays: DEFAULT_MAX_AGE_DAYS };
|
|
1338
|
+
var CLEANUP_SETTINGS_NAMESPACE = "dsh-rewind-snapshot-cleanup";
|
|
1339
|
+
var CleanupConfigSchema = z.object({
|
|
1340
|
+
enabled: z.boolean().default(DEFAULT_CLEANUP_CONFIG.enabled),
|
|
1341
|
+
maxAgeDays: z.number().step(1).min(1).default(DEFAULT_CLEANUP_CONFIG.maxAgeDays)
|
|
1342
|
+
});
|
|
1343
|
+
function settingsCleanupStore(scope) {
|
|
1344
|
+
return {
|
|
1345
|
+
load: () => scope.get(),
|
|
1346
|
+
save: async (next) => {
|
|
1347
|
+
const parsed = parseCleanupConfig({ enabled: next.enabled, maxAgeDays: next.maxAgeDays });
|
|
1348
|
+
if (!parsed.ok) throw new RangeError(parsed.error);
|
|
1349
|
+
await scope.update({ enabled: parsed.config.enabled, maxAgeDays: parsed.config.maxAgeDays });
|
|
1350
|
+
}
|
|
1351
|
+
};
|
|
1352
|
+
}
|
|
1353
|
+
async function migrateLegacyCleanupConfig(legacyPath, scope, log) {
|
|
1354
|
+
const loaded = await loadCleanupConfig(legacyPath);
|
|
1355
|
+
if (!loaded.ok) {
|
|
1356
|
+
log(`[dsh-rewind] legacy snapshot-cleanup config invalid, migrating defaults and removing: ${loaded.error}`);
|
|
1357
|
+
await scope.update({ enabled: false, maxAgeDays: DEFAULT_MAX_AGE_DAYS });
|
|
1358
|
+
await unlink(legacyPath).catch(() => void 0);
|
|
1359
|
+
return true;
|
|
1360
|
+
}
|
|
1361
|
+
if (!loaded.fromFile) return false;
|
|
1362
|
+
if (loaded.config.enabled === DEFAULT_CLEANUP_CONFIG.enabled && loaded.config.maxAgeDays === DEFAULT_CLEANUP_CONFIG.maxAgeDays) {
|
|
1363
|
+
await unlink(legacyPath).catch(() => void 0);
|
|
1364
|
+
return true;
|
|
1365
|
+
}
|
|
1366
|
+
await scope.update({ enabled: loaded.config.enabled, maxAgeDays: loaded.config.maxAgeDays });
|
|
1367
|
+
await unlink(legacyPath).catch(() => void 0);
|
|
1368
|
+
log(`[dsh-rewind] migrated legacy snapshot-cleanup config (enabled=${String(loaded.config.enabled)}, maxAgeDays=${String(loaded.config.maxAgeDays)})`);
|
|
1369
|
+
return true;
|
|
1370
|
+
}
|
|
1335
1371
|
var AUTO_SWEEP_INTERVAL_MS = 24 * 60 * 60 * 1e3;
|
|
1336
1372
|
function resolveCleanupConfigPath(dshHome) {
|
|
1337
|
-
return
|
|
1373
|
+
return join2(resolveDshHome2(dshHome), CLEANUP_CONFIG_FILENAME);
|
|
1338
1374
|
}
|
|
1339
1375
|
var STATE_FILENAME = "snapshot-cleanup-last-sweep.json";
|
|
1340
1376
|
function resolveCleanupStatePath(dshHome) {
|
|
@@ -1357,7 +1393,7 @@ async function saveLastSweepAt(path, ms) {
|
|
|
1357
1393
|
}
|
|
1358
1394
|
async function runAutoCleanupCheck(deps, sessionId) {
|
|
1359
1395
|
try {
|
|
1360
|
-
const loaded = await
|
|
1396
|
+
const loaded = await deps.readConfig();
|
|
1361
1397
|
if (!loaded.ok) {
|
|
1362
1398
|
deps.log(`[dsh-rewind] snapshot cleanup config invalid; auto-cleanup skipped: ${loaded.error}`);
|
|
1363
1399
|
return;
|
|
@@ -1410,15 +1446,6 @@ async function loadCleanupConfig(path) {
|
|
|
1410
1446
|
if (!parsed.ok) return { ok: false, error: parsed.error };
|
|
1411
1447
|
return { ok: true, config: parsed.config, fromFile: true };
|
|
1412
1448
|
}
|
|
1413
|
-
async function saveCleanupConfig(path, config) {
|
|
1414
|
-
if (typeof config.enabled !== "boolean" || !Number.isInteger(config.maxAgeDays) || config.maxAgeDays <= 0) {
|
|
1415
|
-
throw new RangeError("invalid cleanup config: enabled must be a boolean and maxAgeDays a positive integer");
|
|
1416
|
-
}
|
|
1417
|
-
const tmp = `${path}.tmp`;
|
|
1418
|
-
await mkdir2(dirname2(path), { recursive: true });
|
|
1419
|
-
await writeFile2(tmp, JSON.stringify(config, null, 2), "utf8");
|
|
1420
|
-
await rename2(tmp, path);
|
|
1421
|
-
}
|
|
1422
1449
|
function parseCleanupCommand(rawInput) {
|
|
1423
1450
|
const parts = rawInput.trim().split(/\s+/).filter(Boolean);
|
|
1424
1451
|
if (parts.length === 0) return { action: "status" };
|
|
@@ -1465,6 +1492,7 @@ var inject = ["commands", "tools"];
|
|
|
1465
1492
|
var TRACKED_TOOLS = /* @__PURE__ */ new Set(["write", "edit", "str_replace_editor"]);
|
|
1466
1493
|
var MUTATING_EDITOR_COMMANDS = /* @__PURE__ */ new Set(["create", "str_replace", "insert"]);
|
|
1467
1494
|
var activeLocale = "en";
|
|
1495
|
+
var cleanupStore;
|
|
1468
1496
|
function t(key, params) {
|
|
1469
1497
|
return translate(activeLocale, key, params);
|
|
1470
1498
|
}
|
|
@@ -1694,7 +1722,7 @@ async function executeRewind(ctx, store, fs, invocation, rawTarget, mode, inflig
|
|
|
1694
1722
|
}
|
|
1695
1723
|
let restore = "";
|
|
1696
1724
|
if (mode === "both") {
|
|
1697
|
-
const outcome = await store.restoreAfter(agent.session.id, plan.targetSeq, (path) =>
|
|
1725
|
+
const outcome = await store.restoreAfter(agent.session.id, plan.targetSeq, (path) => unlink2(path));
|
|
1698
1726
|
await syncRestoreObservations(ctx, fs, agent, outcome);
|
|
1699
1727
|
const parts = [];
|
|
1700
1728
|
if (outcome.restored.length > 0) parts.push(t("restore.count", { count: outcome.restored.length }));
|
|
@@ -1772,11 +1800,21 @@ async function maybeRunAutoCleanup(ctx, store, sessionId, dshHome) {
|
|
|
1772
1800
|
autoSweepChecked = true;
|
|
1773
1801
|
await runAutoCleanupCheck({
|
|
1774
1802
|
pruner: store,
|
|
1775
|
-
|
|
1803
|
+
readConfig: () => readCleanupPolicy(),
|
|
1776
1804
|
statePath: resolveCleanupStatePath(dshHome),
|
|
1777
1805
|
log: (msg) => ctx.logger.warn(msg)
|
|
1778
1806
|
}, sessionId);
|
|
1779
1807
|
}
|
|
1808
|
+
async function readCleanupPolicy() {
|
|
1809
|
+
if (cleanupStore === void 0) {
|
|
1810
|
+
return { ok: false, error: "settings service unavailable; snapshot cleanup policy cannot be read" };
|
|
1811
|
+
}
|
|
1812
|
+
return { ok: true, config: cleanupStore.load() };
|
|
1813
|
+
}
|
|
1814
|
+
async function writeCleanupPolicy(next) {
|
|
1815
|
+
if (cleanupStore === void 0) throw new Error("settings service unavailable; snapshot cleanup policy cannot be written");
|
|
1816
|
+
await cleanupStore.save(next);
|
|
1817
|
+
}
|
|
1780
1818
|
function formatCleanupReport(report) {
|
|
1781
1819
|
const key = report.dryRun ? "cleanup.runDry" : "cleanup.runApply";
|
|
1782
1820
|
const text = t(key, {
|
|
@@ -1791,37 +1829,34 @@ ${t("cleanup.skipped", { skipped: report.skippedActive })}` : text;
|
|
|
1791
1829
|
async function handleSnapshotCleanup(store, invocation, dshHome, trackedBySession) {
|
|
1792
1830
|
const parsed = parseCleanupCommand(invocation.rawInput);
|
|
1793
1831
|
if ("error" in parsed) return { kind: "error", text: t("cleanup.usage") };
|
|
1794
|
-
const configPath = resolveCleanupConfigPath(dshHome);
|
|
1795
1832
|
switch (parsed.action) {
|
|
1796
1833
|
case "status": {
|
|
1797
|
-
const loaded = await
|
|
1834
|
+
const loaded = await readCleanupPolicy();
|
|
1798
1835
|
if (!loaded.ok) return { kind: "error", text: t("cleanup.cfgInvalid", { detail: loaded.error }) };
|
|
1799
1836
|
return {
|
|
1800
1837
|
kind: "success",
|
|
1801
1838
|
text: t("cleanup.status", {
|
|
1802
1839
|
state: t(loaded.config.enabled ? "cleanup.enabled" : "cleanup.disabled"),
|
|
1803
|
-
days: loaded.config.maxAgeDays
|
|
1804
|
-
path: configPath,
|
|
1805
|
-
present: t(loaded.fromFile ? "cleanup.present" : "cleanup.absent")
|
|
1840
|
+
days: loaded.config.maxAgeDays
|
|
1806
1841
|
})
|
|
1807
1842
|
};
|
|
1808
1843
|
}
|
|
1809
1844
|
case "on":
|
|
1810
1845
|
case "off": {
|
|
1811
|
-
const loaded = await
|
|
1846
|
+
const loaded = await readCleanupPolicy();
|
|
1812
1847
|
const next = { ...loaded.ok ? loaded.config : DEFAULT_CLEANUP_CONFIG, enabled: parsed.action === "on" };
|
|
1813
1848
|
try {
|
|
1814
|
-
await
|
|
1849
|
+
await writeCleanupPolicy(next);
|
|
1815
1850
|
} catch (error) {
|
|
1816
1851
|
return { kind: "error", text: t("cleanup.saveFailed", { detail: error instanceof Error ? error.message : String(error) }) };
|
|
1817
1852
|
}
|
|
1818
1853
|
return { kind: "success", text: t(parsed.action === "on" ? "cleanup.onOk" : "cleanup.offOk") };
|
|
1819
1854
|
}
|
|
1820
1855
|
case "max-age": {
|
|
1821
|
-
const loaded = await
|
|
1856
|
+
const loaded = await readCleanupPolicy();
|
|
1822
1857
|
const next = { ...loaded.ok ? loaded.config : DEFAULT_CLEANUP_CONFIG, maxAgeDays: parsed.value };
|
|
1823
1858
|
try {
|
|
1824
|
-
await
|
|
1859
|
+
await writeCleanupPolicy(next);
|
|
1825
1860
|
} catch (error) {
|
|
1826
1861
|
return { kind: "error", text: t("cleanup.saveFailed", { detail: error instanceof Error ? error.message : String(error) }) };
|
|
1827
1862
|
}
|
|
@@ -1832,7 +1867,7 @@ async function handleSnapshotCleanup(store, invocation, dshHome, trackedBySessio
|
|
|
1832
1867
|
if (parsed.target === "current") {
|
|
1833
1868
|
return handleClearCurrent(store, invocation, apply2, trackedBySession);
|
|
1834
1869
|
}
|
|
1835
|
-
const loaded = await
|
|
1870
|
+
const loaded = await readCleanupPolicy();
|
|
1836
1871
|
if (!loaded.ok) return { kind: "error", text: t("cleanup.cfgInvalid", { detail: loaded.error }) };
|
|
1837
1872
|
try {
|
|
1838
1873
|
const report = await store.pruneStale({
|
|
@@ -1887,10 +1922,23 @@ function apply(ctx, config) {
|
|
|
1887
1922
|
const trackedBySession = /* @__PURE__ */ new Map();
|
|
1888
1923
|
let fsService;
|
|
1889
1924
|
ctx.inject(["settings"], (settingsCtx) => {
|
|
1890
|
-
const section =
|
|
1925
|
+
const section = readSettingsSection(
|
|
1926
|
+
settingsCtx.settings,
|
|
1927
|
+
"locale",
|
|
1928
|
+
dshSettings.settingsNamespace
|
|
1929
|
+
);
|
|
1891
1930
|
if (section?.preference === "zh" || section?.preference === "en") {
|
|
1892
1931
|
activeLocale = section.preference;
|
|
1893
1932
|
}
|
|
1933
|
+
const cleanupScope = settingsCtx.settings.register(CLEANUP_SETTINGS_NAMESPACE, CleanupConfigSchema, { base: DEFAULT_CLEANUP_CONFIG });
|
|
1934
|
+
cleanupStore = settingsCleanupStore(cleanupScope);
|
|
1935
|
+
void migrateLegacyCleanupConfig(
|
|
1936
|
+
resolveCleanupConfigPath(dshHome),
|
|
1937
|
+
cleanupScope,
|
|
1938
|
+
(msg) => ctx.logger.warn(msg)
|
|
1939
|
+
).catch((error) => {
|
|
1940
|
+
ctx.logger.warn(`[dsh-rewind] snapshot cleanup migration failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
1941
|
+
});
|
|
1894
1942
|
});
|
|
1895
1943
|
ctx.effect(function* () {
|
|
1896
1944
|
const rewindHandler = (invocation) => handleRewind(ctx, store, fsService, invocation, inflight);
|
|
@@ -41,6 +41,16 @@ export declare function chatSnapshotOf(face: {
|
|
|
41
41
|
} | undefined, chatView: {
|
|
42
42
|
getSnapshot(): unknown;
|
|
43
43
|
} | undefined): HiddenChat | undefined;
|
|
44
|
+
/**
|
|
45
|
+
* The plain text of the human message at `seq` in the chat snapshot, for
|
|
46
|
+
* filling the composer after a withdraw. Accepts BOTH `user` and `steering`
|
|
47
|
+
* nodes: a plan-mode (`/plan <text>`) input is delivered through the agent
|
|
48
|
+
* inbox next-step and claimed, so it renders as `steering`, and its text must
|
|
49
|
+
* still return to the composer (`portals.tsx` `runRewindAndFill`) — the old
|
|
50
|
+
* `user`-only read silently left it empty. State absent → undefined; a message
|
|
51
|
+
* with no text blocks → ''. Same text-blocks join the candidate side uses.
|
|
52
|
+
*/
|
|
53
|
+
export declare function messageTextAt(chat: HiddenChat | undefined, seq: number): string | undefined;
|
|
44
54
|
/**
|
|
45
55
|
* Extract the rewind target seq from a `/rewind` command's structured `args`
|
|
46
56
|
* (e.g. `@5 chat`, `preview @5 both`). Locale-independent — never parses the
|
|
@@ -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,14 +72,16 @@ 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
|
}
|
|
84
|
+
/** Join the text blocks of a user message into one plain preview. */
|
|
83
85
|
/**
|
|
84
86
|
* The alpha.1+ session input facade's write face (structural, so the plugin
|
|
85
87
|
* never imports the conversation UI package). `setDraft` replaces the whole
|
|
@@ -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 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,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Version-neutral settings-namespace reading for the host half.
|
|
3
|
+
*
|
|
4
|
+
* DSH rc.2 exposes a `settingsNamespace(value)` brand helper, and `settings.get`
|
|
5
|
+
* is typed to require a branded `SettingsNamespace`. DSH 0.1.2-alpha.2 removed
|
|
6
|
+
* that helper (the `SettingsNamespace` *type* remains) and `settings.get`
|
|
7
|
+
* accepts the raw namespace string. This module collapses both into one runtime
|
|
8
|
+
* call: on rc.2 the brand is a compile-time marker erased at runtime (so
|
|
9
|
+
* `settingsNamespace(ns)` returns `ns`), on 0.1.2-alpha.2 the brand helper is
|
|
10
|
+
* absent and the raw `ns` string is used directly. That is what lets a single
|
|
11
|
+
* compiled host bundle link and run on both harness generations.
|
|
12
|
+
*
|
|
13
|
+
* @module dsh-rewind/settings-locale
|
|
14
|
+
*/
|
|
15
|
+
/** Minimal structural face of the settings provider the host reads from. */
|
|
16
|
+
export interface SettingsProviderLike {
|
|
17
|
+
/**
|
|
18
|
+
* Read one registered settings section by namespace. Accepts whatever the
|
|
19
|
+
* running DSH generation passes: a branded `SettingsNamespace` (rc.2, brand
|
|
20
|
+
* erased) or the raw namespace string (0.1.2-alpha.2).
|
|
21
|
+
*/
|
|
22
|
+
get(ns: string): unknown;
|
|
23
|
+
}
|
|
24
|
+
/** The `settingsNamespace(ns)` brand helper, or `undefined` when removed (0.1.2-alpha.2). */
|
|
25
|
+
export type SettingsNamespaceBrand = ((value: string) => string) | undefined;
|
|
26
|
+
/**
|
|
27
|
+
* Read one settings section keyed by `ns`, tolerant of the settings-namespace
|
|
28
|
+
* brand across DSH generations. Pass the brand helper when available (the rc.2
|
|
29
|
+
* path); it is `undefined` on 0.1.2-alpha.2, where the raw `ns` string is used
|
|
30
|
+
* directly. Never throws: an absent section simply returns `undefined`.
|
|
31
|
+
*/
|
|
32
|
+
export declare function readSettingsSection(provider: SettingsProviderLike, ns: string, brand: SettingsNamespaceBrand): unknown;
|
|
@@ -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>;
|