dsh-tui-theme 0.6.1 → 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.md +23 -13
- package/cordis.patch.yml +2 -0
- package/docs/decisions/2026-09-12-pink-day-claude-family-lightening.md +49 -0
- package/lib/types/autoTheme.d.ts +3 -0
- package/lib/types/autoTheme.d.ts.map +1 -1
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/index.js +19 -2
- package/lib/types/ornament.d.ts +38 -0
- package/lib/types/ornament.d.ts.map +1 -0
- package/lib/types/ornament.js +88 -0
- package/lib/types/settingsSection.d.ts +9 -4
- package/lib/types/settingsSection.d.ts.map +1 -1
- package/lib/types/settingsSection.js +88 -7
- package/lib/types/shadowCleanup.d.ts +39 -0
- package/lib/types/shadowCleanup.d.ts.map +1 -0
- package/lib/types/shadowCleanup.js +122 -0
- package/lib/types/statusLine.d.ts +27 -11
- package/lib/types/statusLine.d.ts.map +1 -1
- package/lib/types/statusLine.js +181 -43
- package/lib/types/statusView.d.ts +82 -0
- package/lib/types/statusView.d.ts.map +1 -0
- package/lib/types/statusView.js +126 -0
- package/lib/types/themeAssets.d.ts +4 -0
- package/lib/types/themeAssets.d.ts.map +1 -1
- package/lib/types/themeAssets.js +8 -3
- package/package.json +42 -42
- package/scripts/expected-settings-contract.mjs +21 -1
- package/scripts/headless-order-test.mjs +33 -10
- package/scripts/runtime-themes-headless.mjs +160 -23
- package/scripts/validate-themes-against-host.mjs +33 -6
- package/scripts/verify-package.mjs +16 -7
- package/scripts/verify.mjs +377 -9
- package/themes/pink-day.json +4 -4
- package/themes/pink-night.json +4 -4
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rich status view (dsh-TUI >= 0.10.1 seam: `tuiStatus.registerView`).
|
|
3
|
+
*
|
|
4
|
+
* A compact one-row host React component that renders the blossom line with
|
|
5
|
+
* colors taken from the active pink palette — the one thing the scalar
|
|
6
|
+
* `tuiStatus.set` path can never do (the host renders set() text uncolored +
|
|
7
|
+
* terminal dim).
|
|
8
|
+
*
|
|
9
|
+
* Data channel: the component subscribes to a tiny plugin-side external
|
|
10
|
+
* store via the host React's useSyncExternalStore, exactly the "owns its
|
|
11
|
+
* live data via an external store" pattern the host seam documents. The
|
|
12
|
+
* plugin pushes scalar-only snapshots (turn boundaries, the clock tick,
|
|
13
|
+
* settings edits); the component maps one snapshot to themed Text cells and
|
|
14
|
+
* never touches the filesystem, timers, or events itself — it runs on the
|
|
15
|
+
* host's render thread and must stay a pure function of its snapshot.
|
|
16
|
+
*
|
|
17
|
+
* Host kit rules honored here: the component is created with the host React
|
|
18
|
+
* instance handed in via props (single-React rule — the plugin never imports
|
|
19
|
+
* react), and `Box`/`Text` receive only layout/color props (no focus,
|
|
20
|
+
* keyboard, wheel, or ref props; no pointer handlers either — the line is
|
|
21
|
+
* pure display).
|
|
22
|
+
* @module dsh-tui-theme/statusView
|
|
23
|
+
*/
|
|
24
|
+
export const NO_COLORS = Object.freeze({
|
|
25
|
+
glyph: undefined,
|
|
26
|
+
text: undefined,
|
|
27
|
+
separator: undefined,
|
|
28
|
+
});
|
|
29
|
+
function sameSnapshot(a, b) {
|
|
30
|
+
return (a.visible === b.visible &&
|
|
31
|
+
a.glyph === b.glyph &&
|
|
32
|
+
a.clock === b.clock &&
|
|
33
|
+
a.turns === b.turns &&
|
|
34
|
+
a.separator === b.separator &&
|
|
35
|
+
a.colors.glyph === b.colors.glyph &&
|
|
36
|
+
a.colors.text === b.colors.text &&
|
|
37
|
+
a.colors.separator === b.colors.separator);
|
|
38
|
+
}
|
|
39
|
+
export function createStatusStore() {
|
|
40
|
+
const listeners = new Set();
|
|
41
|
+
let current = Object.freeze({
|
|
42
|
+
visible: false,
|
|
43
|
+
glyph: undefined,
|
|
44
|
+
clock: undefined,
|
|
45
|
+
turns: undefined,
|
|
46
|
+
separator: '·',
|
|
47
|
+
colors: NO_COLORS,
|
|
48
|
+
});
|
|
49
|
+
return {
|
|
50
|
+
getSnapshot: () => current,
|
|
51
|
+
subscribe(listener) {
|
|
52
|
+
listeners.add(listener);
|
|
53
|
+
return () => {
|
|
54
|
+
listeners.delete(listener);
|
|
55
|
+
};
|
|
56
|
+
},
|
|
57
|
+
push(next) {
|
|
58
|
+
if (sameSnapshot(current, next))
|
|
59
|
+
return;
|
|
60
|
+
current = Object.freeze({
|
|
61
|
+
visible: next.visible,
|
|
62
|
+
glyph: next.glyph,
|
|
63
|
+
clock: next.clock,
|
|
64
|
+
turns: next.turns,
|
|
65
|
+
separator: next.separator,
|
|
66
|
+
colors: Object.freeze({
|
|
67
|
+
glyph: next.colors.glyph,
|
|
68
|
+
text: next.colors.text,
|
|
69
|
+
separator: next.colors.separator,
|
|
70
|
+
}),
|
|
71
|
+
});
|
|
72
|
+
for (const listener of [...listeners]) {
|
|
73
|
+
try {
|
|
74
|
+
listener();
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
// One broken listener must not starve the others.
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
clear() {
|
|
82
|
+
listeners.clear();
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Build the rich view component bound to one store. The returned function is
|
|
88
|
+
* a host React component: it receives `{ React, ui }` from the host on every
|
|
89
|
+
* render and maps the current snapshot to a one-row themed Box.
|
|
90
|
+
*/
|
|
91
|
+
export function createStatusViewComponent(store) {
|
|
92
|
+
return props => {
|
|
93
|
+
const { React, ui } = props;
|
|
94
|
+
const snapshot = React.useSyncExternalStore(store.subscribe, store.getSnapshot);
|
|
95
|
+
if (!snapshot.visible)
|
|
96
|
+
return null;
|
|
97
|
+
const cells = [];
|
|
98
|
+
if (snapshot.glyph !== undefined) {
|
|
99
|
+
cells.push({ text: snapshot.glyph, color: snapshot.colors.glyph });
|
|
100
|
+
}
|
|
101
|
+
if (snapshot.clock !== undefined) {
|
|
102
|
+
cells.push({ text: snapshot.clock, color: snapshot.colors.text });
|
|
103
|
+
}
|
|
104
|
+
if (snapshot.turns !== undefined) {
|
|
105
|
+
cells.push({ text: snapshot.turns, color: snapshot.colors.text });
|
|
106
|
+
}
|
|
107
|
+
if (cells.length === 0)
|
|
108
|
+
return null;
|
|
109
|
+
const children = [];
|
|
110
|
+
for (const [index, cell] of cells.entries()) {
|
|
111
|
+
if (index > 0) {
|
|
112
|
+
children.push(React.createElement(ui.Text, { key: `sep-${index}`, color: snapshot.colors.separator }, ` ${snapshot.separator} `));
|
|
113
|
+
}
|
|
114
|
+
children.push(React.createElement(ui.Text, { key: `cell-${index}`, color: cell.color }, cell.text));
|
|
115
|
+
}
|
|
116
|
+
return React.createElement(ui.Box, { flexDirection: 'row' }, children);
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* The registration descriptor for the rich view. The caller passes the same
|
|
121
|
+
* contribution key the scalar path uses, so the effect-ledger resource id
|
|
122
|
+
* and the headless order-test pin remain stable across both paths.
|
|
123
|
+
*/
|
|
124
|
+
export function statusViewDescriptor(key, component) {
|
|
125
|
+
return { key, maxRows: 1, component };
|
|
126
|
+
}
|
|
@@ -19,6 +19,10 @@ export interface ThemeInstallResult {
|
|
|
19
19
|
readonly repaired: readonly string[];
|
|
20
20
|
/** Files that could not be installed (per-file failures). */
|
|
21
21
|
readonly failed: readonly string[];
|
|
22
|
+
/** Set only when the bundled directory itself was unreadable, so nothing
|
|
23
|
+
* was attempted and no per-file entry exists. Carries the directory and
|
|
24
|
+
* the underlying error for the caller's log. */
|
|
25
|
+
readonly sourceError?: string;
|
|
22
26
|
}
|
|
23
27
|
export interface BundledTheme {
|
|
24
28
|
readonly file: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"themeAssets.d.ts","sourceRoot":"","sources":["../../src/themeAssets.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAOH,MAAM,WAAW,kBAAkB;IACjC,qDAAqD;IACrD,QAAQ,CAAC,SAAS,EAAE,SAAS,MAAM,EAAE,CAAA;IACrC,sEAAsE;IACtE,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAA;IACnC,6DAA6D;IAC7D,QAAQ,CAAC,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAA;IACpC,6DAA6D;IAC7D,QAAQ,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAA;
|
|
1
|
+
{"version":3,"file":"themeAssets.d.ts","sourceRoot":"","sources":["../../src/themeAssets.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAOH,MAAM,WAAW,kBAAkB;IACjC,qDAAqD;IACrD,QAAQ,CAAC,SAAS,EAAE,SAAS,MAAM,EAAE,CAAA;IACrC,sEAAsE;IACtE,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAA;IACnC,6DAA6D;IAC7D,QAAQ,CAAC,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAA;IACpC,6DAA6D;IAC7D,QAAQ,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAA;IAClC;;qDAEiD;IACjD,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAA;CAC9B;AAED,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAA;IAC7B,QAAQ,CAAC,IAAI,EAAE,OAAO,GAAG,MAAM,GAAG,WAAW,CAAA;IAC7C,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CACxC;AAOD,wBAAgB,OAAO,IAAI,MAAM,CAEhC;AAED,2EAA2E;AAC3E,wBAAgB,gBAAgB,IAAI,MAAM,CAEzC;AAED,2DAA2D;AAC3D,wBAAgB,eAAe,IAAI,MAAM,CAExC;AAED,kFAAkF;AAClF,wBAAgB,iBAAiB,CAAC,SAAS,GAAE,MAA2B,GAAG,YAAY,EAAE,CA0CxF;AAED;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAClC,SAAS,GAAE,MAA0B,EACrC,SAAS,GAAE,MAA2B,GACrC,kBAAkB,CAqCpB;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CACjC,KAAK,EAAE,SAAS,MAAM,EAAE,EACxB,SAAS,GAAE,MAA0B,EACrC,SAAS,GAAE,MAA2B,GACrC,MAAM,EAAE,CAuBV;AAED;;;;;;GAMG;AACH,wBAAgB,yBAAyB,CACvC,SAAS,GAAE,MAA0B,EACrC,SAAS,GAAE,MAA2B,GACrC,MAAM,EAAE,CAkBV"}
|
package/lib/types/themeAssets.js
CHANGED
|
@@ -15,7 +15,10 @@ import { homedir } from 'node:os';
|
|
|
15
15
|
import { dirname, join } from 'node:path';
|
|
16
16
|
import { fileURLToPath } from 'node:url';
|
|
17
17
|
// Same resolution order as the host's utils/paths.ts homeDir(): os.homedir()
|
|
18
|
-
// first, USERPROFILE/HOME spellings as stripped-down fallbacks.
|
|
18
|
+
// first, USERPROFILE/HOME spellings as stripped-down fallbacks. When all three
|
|
19
|
+
// come up empty the returned '' makes every derived path resolve against the
|
|
20
|
+
// process cwd — the host's own semantics, kept deliberately identical rather
|
|
21
|
+
// than second-guessed here.
|
|
19
22
|
export function homeDir() {
|
|
20
23
|
return homedir() || process.env.USERPROFILE || process.env.HOME || '';
|
|
21
24
|
}
|
|
@@ -87,8 +90,10 @@ export function installBundledThemes(targetDir = themesTargetDir(), sourceDir =
|
|
|
87
90
|
try {
|
|
88
91
|
files = readdirSync(sourceDir).filter(entry => entry.toLowerCase().endsWith('.json'));
|
|
89
92
|
}
|
|
90
|
-
catch {
|
|
91
|
-
|
|
93
|
+
catch (error) {
|
|
94
|
+
// No file was attempted, so `failed` stays empty; the directory-level
|
|
95
|
+
// reason travels separately so the caller can log a precise line.
|
|
96
|
+
return { installed, skipped, repaired, failed, sourceError: `could not read bundled themes from ${sourceDir}: ${String(error)}` };
|
|
92
97
|
}
|
|
93
98
|
for (const file of files) {
|
|
94
99
|
const target = join(targetDir, file);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-tui-theme",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.1",
|
|
4
4
|
"description": "Sakura-pink themes for dsh-TUI with optional cached background follow (pink-day/pink-night), a blossom status line, and a /settings section. No shortcuts, no commands.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/types/index.js",
|
|
@@ -58,57 +58,57 @@
|
|
|
58
58
|
},
|
|
59
59
|
"peerDependencies": {
|
|
60
60
|
"@deepseek-ai/cordis": "^4.0.1",
|
|
61
|
-
"@deepseek-ai/dsh-session": "^0.1.0-rc.6 || ^0.1.1-rc.1 || ^0.1.2-alpha.2",
|
|
62
|
-
"@deepseek-ai/dsh-settings": "^0.1.0-rc.6 || ^0.1.1-rc.1 || ^0.1.2-alpha.2",
|
|
61
|
+
"@deepseek-ai/dsh-session": "^0.1.0-rc.6 || ^0.1.1-rc.1 || ^0.1.2-alpha.2 || 0.1.2-alpha.3 || 0.1.3-alpha.2 || 0.1.5-alpha.1 || 0.1.5-alpha.2 || 0.1.5-rc.1",
|
|
62
|
+
"@deepseek-ai/dsh-settings": "^0.1.0-rc.6 || ^0.1.1-rc.1 || ^0.1.2-alpha.2 || 0.1.2-alpha.3 || 0.1.3-alpha.2 || 0.1.5-alpha.1 || 0.1.5-alpha.2 || 0.1.5-rc.1",
|
|
63
63
|
"@deepseek-ai/schemastery": "^3.18.1"
|
|
64
64
|
},
|
|
65
65
|
"devDependencies": {
|
|
66
66
|
"@deepseek-ai/cordis": "^4.0.1",
|
|
67
|
-
"@deepseek-ai/dsh-session": "0.1.2-alpha.
|
|
68
|
-
"@deepseek-ai/dsh-settings": "0.1.2-alpha.
|
|
67
|
+
"@deepseek-ai/dsh-session": "0.1.2-alpha.3",
|
|
68
|
+
"@deepseek-ai/dsh-settings": "0.1.2-alpha.3",
|
|
69
69
|
"@deepseek-ai/schemastery": "^3.18.1",
|
|
70
|
-
"@deepseek-harness-tui/dsh-tui": "0.10.
|
|
70
|
+
"@deepseek-harness-tui/dsh-tui": "0.10.1",
|
|
71
71
|
"@types/node": "^22.0.0",
|
|
72
72
|
"tsx": "^4.23.12",
|
|
73
73
|
"typescript": "^6.0.3",
|
|
74
|
-
"@deepseek-ai/dsh-llm": "0.1.2-alpha.
|
|
75
|
-
"@deepseek-ai/dsh-agent": "0.1.2-alpha.
|
|
76
|
-
"@deepseek-ai/dsh-user-questions": "0.1.2-alpha.
|
|
77
|
-
"@deepseek-ai/dsh-user-approval": "0.1.2-alpha.
|
|
78
|
-
"@deepseek-ai/dsh-commands": "0.1.2-alpha.
|
|
79
|
-
"@deepseek-ai/dsh-atomic-write": "0.1.2-alpha.
|
|
80
|
-
"@deepseek-ai/dsh-tool-ask-user": "0.1.2-alpha.
|
|
81
|
-
"@deepseek-ai/dsh-skill": "0.1.2-alpha.
|
|
82
|
-
"@deepseek-ai/dsh-agent-instructions": "0.1.2-alpha.
|
|
83
|
-
"@deepseek-ai/dsh-tools": "0.1.2-alpha.
|
|
84
|
-
"@deepseek-ai/dsh-system-prompt": "0.1.2-alpha.
|
|
85
|
-
"@deepseek-ai/dsh-invariants": "0.1.2-alpha.
|
|
74
|
+
"@deepseek-ai/dsh-llm": "0.1.2-alpha.3",
|
|
75
|
+
"@deepseek-ai/dsh-agent": "0.1.2-alpha.3",
|
|
76
|
+
"@deepseek-ai/dsh-user-questions": "0.1.2-alpha.3",
|
|
77
|
+
"@deepseek-ai/dsh-user-approval": "0.1.2-alpha.3",
|
|
78
|
+
"@deepseek-ai/dsh-commands": "0.1.2-alpha.3",
|
|
79
|
+
"@deepseek-ai/dsh-atomic-write": "0.1.2-alpha.3",
|
|
80
|
+
"@deepseek-ai/dsh-tool-ask-user": "0.1.2-alpha.3",
|
|
81
|
+
"@deepseek-ai/dsh-skill": "0.1.2-alpha.3",
|
|
82
|
+
"@deepseek-ai/dsh-agent-instructions": "0.1.2-alpha.3",
|
|
83
|
+
"@deepseek-ai/dsh-tools": "0.1.2-alpha.3",
|
|
84
|
+
"@deepseek-ai/dsh-system-prompt": "0.1.2-alpha.3",
|
|
85
|
+
"@deepseek-ai/dsh-invariants": "0.1.2-alpha.3"
|
|
86
86
|
},
|
|
87
87
|
"overrides": {
|
|
88
|
-
"@deepseek-ai/dsh-session": "0.1.2-alpha.
|
|
89
|
-
"@deepseek-ai/dsh-settings": "0.1.2-alpha.
|
|
90
|
-
"@deepseek-ai/dsh-llm": "0.1.2-alpha.
|
|
91
|
-
"@deepseek-ai/dsh-agent": "0.1.2-alpha.
|
|
92
|
-
"@deepseek-ai/dsh-user-questions": "0.1.2-alpha.
|
|
93
|
-
"@deepseek-ai/dsh-user-approval": "0.1.2-alpha.
|
|
94
|
-
"@deepseek-ai/dsh-commands": "0.1.2-alpha.
|
|
95
|
-
"@deepseek-ai/dsh-atomic-write": "0.1.2-alpha.
|
|
96
|
-
"@deepseek-ai/dsh-tool-ask-user": "0.1.2-alpha.
|
|
97
|
-
"@deepseek-ai/dsh-skill": "0.1.2-alpha.
|
|
98
|
-
"@deepseek-ai/dsh-agent-instructions": "0.1.2-alpha.
|
|
99
|
-
"@deepseek-ai/dsh-tools": "0.1.2-alpha.
|
|
100
|
-
"@deepseek-ai/dsh-system-prompt": "0.1.2-alpha.
|
|
101
|
-
"@deepseek-ai/dsh-invariants": "0.1.2-alpha.
|
|
102
|
-
"@deepseek-ai/dsh-scope": "0.1.2-alpha.
|
|
103
|
-
"@deepseek-ai/dsh-attachment": "0.1.2-alpha.
|
|
104
|
-
"@deepseek-ai/dsh-brand": "0.1.2-alpha.
|
|
105
|
-
"@deepseek-ai/dsh-typert-protocol": "0.1.2-alpha.
|
|
106
|
-
"@deepseek-ai/dsh-timeout": "0.1.2-alpha.
|
|
107
|
-
"@deepseek-ai/dsh-session-projection": "0.1.2-alpha.
|
|
108
|
-
"@deepseek-ai/dsh-code-runtime": "0.1.2-alpha.
|
|
109
|
-
"@deepseek-ai/dsh-fs": "0.1.2-alpha.
|
|
110
|
-
"@deepseek-ai/dsh-home-paths": "0.1.2-alpha.
|
|
111
|
-
"@deepseek-ai/dsh-sandbox": "0.1.2-alpha.
|
|
88
|
+
"@deepseek-ai/dsh-session": "0.1.2-alpha.3",
|
|
89
|
+
"@deepseek-ai/dsh-settings": "0.1.2-alpha.3",
|
|
90
|
+
"@deepseek-ai/dsh-llm": "0.1.2-alpha.3",
|
|
91
|
+
"@deepseek-ai/dsh-agent": "0.1.2-alpha.3",
|
|
92
|
+
"@deepseek-ai/dsh-user-questions": "0.1.2-alpha.3",
|
|
93
|
+
"@deepseek-ai/dsh-user-approval": "0.1.2-alpha.3",
|
|
94
|
+
"@deepseek-ai/dsh-commands": "0.1.2-alpha.3",
|
|
95
|
+
"@deepseek-ai/dsh-atomic-write": "0.1.2-alpha.3",
|
|
96
|
+
"@deepseek-ai/dsh-tool-ask-user": "0.1.2-alpha.3",
|
|
97
|
+
"@deepseek-ai/dsh-skill": "0.1.2-alpha.3",
|
|
98
|
+
"@deepseek-ai/dsh-agent-instructions": "0.1.2-alpha.3",
|
|
99
|
+
"@deepseek-ai/dsh-tools": "0.1.2-alpha.3",
|
|
100
|
+
"@deepseek-ai/dsh-system-prompt": "0.1.2-alpha.3",
|
|
101
|
+
"@deepseek-ai/dsh-invariants": "0.1.2-alpha.3",
|
|
102
|
+
"@deepseek-ai/dsh-scope": "0.1.2-alpha.3",
|
|
103
|
+
"@deepseek-ai/dsh-attachment": "0.1.2-alpha.3",
|
|
104
|
+
"@deepseek-ai/dsh-brand": "0.1.2-alpha.3",
|
|
105
|
+
"@deepseek-ai/dsh-typert-protocol": "0.1.2-alpha.3",
|
|
106
|
+
"@deepseek-ai/dsh-timeout": "0.1.2-alpha.3",
|
|
107
|
+
"@deepseek-ai/dsh-session-projection": "0.1.2-alpha.3",
|
|
108
|
+
"@deepseek-ai/dsh-code-runtime": "0.1.2-alpha.3",
|
|
109
|
+
"@deepseek-ai/dsh-fs": "0.1.2-alpha.3",
|
|
110
|
+
"@deepseek-ai/dsh-home-paths": "0.1.2-alpha.3",
|
|
111
|
+
"@deepseek-ai/dsh-sandbox": "0.1.2-alpha.3"
|
|
112
112
|
},
|
|
113
113
|
"allowScripts": {
|
|
114
114
|
"esbuild@0.28.2": true
|
|
@@ -25,13 +25,18 @@ export const SETTINGS_FIELDS = [
|
|
|
25
25
|
[['showClock'], 'boolean'],
|
|
26
26
|
[['showTurns'], 'boolean'],
|
|
27
27
|
[['statusScope'], 'select'],
|
|
28
|
+
[['statusGlyph'], 'text'],
|
|
29
|
+
[['statusSeparator'], 'text'],
|
|
28
30
|
]
|
|
29
31
|
|
|
30
32
|
/** The values the statusScope select must expose, looked up by field path. */
|
|
31
33
|
export const STATUS_SCOPE_FIELD_PATH = ['statusScope']
|
|
32
34
|
export const STATUS_SCOPE_OPTIONS = ['pink-only', 'all-themes']
|
|
33
35
|
|
|
34
|
-
/**
|
|
36
|
+
/** Navigation group ids; every field must name one of them. */
|
|
37
|
+
export const SETTINGS_GROUPS = ['follow', 'status-line']
|
|
38
|
+
|
|
39
|
+
/** Assert a section object (ns + groups + fields) matches the contract. */
|
|
35
40
|
export function assertSettingsContract(assert, section) {
|
|
36
41
|
assert.equal(section.ns, SETTINGS_NAMESPACE, 'settings namespace must match')
|
|
37
42
|
const actual = section.fields.map(field => [field.path, field.kind])
|
|
@@ -50,4 +55,19 @@ export function assertSettingsContract(assert, section) {
|
|
|
50
55
|
STATUS_SCOPE_OPTIONS,
|
|
51
56
|
'status scope must expose both supported select values',
|
|
52
57
|
)
|
|
58
|
+
assert.deepEqual(
|
|
59
|
+
(section.groups ?? []).map(group => group.id).sort(),
|
|
60
|
+
[...SETTINGS_GROUPS].sort(),
|
|
61
|
+
'settings groups must remain compatible with the declared navigation',
|
|
62
|
+
)
|
|
63
|
+
for (const field of section.fields) {
|
|
64
|
+
assert.equal(
|
|
65
|
+
typeof field.group, 'string',
|
|
66
|
+
`field ${JSON.stringify(field.path)} must declare its navigation group`,
|
|
67
|
+
)
|
|
68
|
+
assert.equal(
|
|
69
|
+
SETTINGS_GROUPS.includes(field.group), true,
|
|
70
|
+
`field ${JSON.stringify(field.path)} names an unknown group`,
|
|
71
|
+
)
|
|
72
|
+
}
|
|
53
73
|
}
|
|
@@ -102,30 +102,35 @@ const collectBinds = () => {
|
|
|
102
102
|
.filter(entry => entry.resource?.id === STATUS_CONTRIBUTION_KEY)
|
|
103
103
|
: []
|
|
104
104
|
}
|
|
105
|
+
// The plugin renders its contribution through the rich view path on hosts
|
|
106
|
+
// with tuiStatus.registerView (0.10.1+) and through the scalar set() path on
|
|
107
|
+
// older ones — the key is the same, only the store half differs.
|
|
105
108
|
const readState = () => {
|
|
106
109
|
const runtime = app.get('tuiStatus')
|
|
107
110
|
const settingsRuntime = app.get('tuiSettingsSections')
|
|
108
111
|
const settingsHost = settingsSectionsModule.getHostSettingsSections(settingsRuntime)
|
|
112
|
+
const store = statusModule.getHostStatusStore(runtime)
|
|
109
113
|
return {
|
|
110
114
|
pinkBinds: collectBinds(),
|
|
111
|
-
snapshot:
|
|
115
|
+
snapshot: store?.getSnapshot(),
|
|
116
|
+
viewSnapshot: typeof store?.getViewSnapshot === 'function' ? store.getViewSnapshot() : [],
|
|
112
117
|
settingsSection: settingsHost?.list().find(section => section.ns === SETTINGS_NAMESPACE),
|
|
113
118
|
}
|
|
114
119
|
}
|
|
115
120
|
|
|
116
121
|
let state = readState()
|
|
117
122
|
const deadline = Date.now() + READY_TIMEOUT_MS
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
) {
|
|
123
|
+
const contributionVisible = s =>
|
|
124
|
+
s.pinkBinds.length > 0 &&
|
|
125
|
+
(s.snapshot?.some?.(entry => entry.key === STATUS_CONTRIBUTION_KEY) === true ||
|
|
126
|
+
s.viewSnapshot?.some?.(entry => entry.key === STATUS_CONTRIBUTION_KEY) === true) &&
|
|
127
|
+
s.settingsSection !== undefined
|
|
128
|
+
while (!contributionVisible(state) && Date.now() < deadline) {
|
|
124
129
|
await new Promise(resolve => setTimeout(resolve, POLL_INTERVAL_MS))
|
|
125
130
|
state = readState()
|
|
126
131
|
}
|
|
127
132
|
|
|
128
|
-
const { pinkBinds, snapshot, settingsSection } = state
|
|
133
|
+
const { pinkBinds, snapshot, viewSnapshot, settingsSection } = state
|
|
129
134
|
assert.equal(
|
|
130
135
|
STATUS_CONTRIBUTION_KEY,
|
|
131
136
|
SETTINGS_NAMESPACE,
|
|
@@ -133,7 +138,8 @@ assert.equal(
|
|
|
133
138
|
)
|
|
134
139
|
assert.ok(pinkBinds.length > 0, `plugin must bind through the late status service within ${READY_TIMEOUT_MS}ms`)
|
|
135
140
|
assert.ok(
|
|
136
|
-
snapshot?.some?.(entry => entry.key === STATUS_CONTRIBUTION_KEY)
|
|
141
|
+
snapshot?.some?.(entry => entry.key === STATUS_CONTRIBUTION_KEY) ||
|
|
142
|
+
viewSnapshot?.some?.(entry => entry.key === STATUS_CONTRIBUTION_KEY),
|
|
137
143
|
`status store must contain the plugin contribution within ${READY_TIMEOUT_MS}ms`,
|
|
138
144
|
)
|
|
139
145
|
assert.ok(
|
|
@@ -141,4 +147,21 @@ assert.ok(
|
|
|
141
147
|
`plugin must register its /settings section through the late settings service within ${READY_TIMEOUT_MS}ms`,
|
|
142
148
|
)
|
|
143
149
|
assertSettingsContract(assert, settingsSection)
|
|
144
|
-
|
|
150
|
+
if (typeof statusModule.getHostStatusStore(app.get('tuiStatus'))?.getViewSnapshot === 'function') {
|
|
151
|
+
const view = viewSnapshot.find(entry => entry.key === STATUS_CONTRIBUTION_KEY)
|
|
152
|
+
assert.ok(view, 'a registerView-capable host must carry the contribution as a rich view')
|
|
153
|
+
assert.equal(view.maxRows, 1, 'the rich status view requests exactly one row')
|
|
154
|
+
assert.equal(typeof view.component, 'function', 'the rich status view carries a component')
|
|
155
|
+
assert.equal(
|
|
156
|
+
snapshot.some(entry => entry.key === STATUS_CONTRIBUTION_KEY),
|
|
157
|
+
false,
|
|
158
|
+
'the two render paths are mutually exclusive: the rich path contributes no scalar text',
|
|
159
|
+
)
|
|
160
|
+
console.log('OK headless order: rich view registered (maxRows 1), ledger bind, settings section')
|
|
161
|
+
} else {
|
|
162
|
+
assert.ok(
|
|
163
|
+
snapshot?.some?.(entry => entry.key === STATUS_CONTRIBUTION_KEY),
|
|
164
|
+
'a set()-only host must carry the contribution as scalar text',
|
|
165
|
+
)
|
|
166
|
+
console.log(`OK headless order: ${pinkBinds.length} ledger bind(s), status contribution, settings section`)
|
|
167
|
+
}
|
|
@@ -37,36 +37,69 @@ const pluginHost = await import(pathToFileURL(join(adapter, 'plugin-host.js')).h
|
|
|
37
37
|
const extensions = await import(pathToFileURL(join(adapter, 'extensions.js')).href)
|
|
38
38
|
const themesModule = await import(pathToFileURL(join(adapter, 'themes.js')).href)
|
|
39
39
|
const themeModule = await import(pathToFileURL(join(adapter, '..', 'theme.js')).href)
|
|
40
|
-
|
|
40
|
+
// dsh-TUI >= 0.10.1 dropped test-utils.js from the npm package; fall back to a
|
|
41
|
+
// manual plugin mount (same shape as headless-order-test.mjs). The admitted
|
|
42
|
+
// path stays first so release-ledger bookkeeping stays asserted where the
|
|
43
|
+
// admission helpers exist.
|
|
44
|
+
const testUtils = existsSync(join(adapter, '..', 'test-utils.js'))
|
|
45
|
+
? await import(pathToFileURL(join(adapter, '..', 'test-utils.js')).href)
|
|
46
|
+
: undefined
|
|
47
|
+
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms))
|
|
48
|
+
const mountPlugin = async app => {
|
|
49
|
+
if (testUtils === undefined) {
|
|
50
|
+
return { context: app, fiber: app.fiber, manual: true }
|
|
51
|
+
}
|
|
52
|
+
const manifest = testUtils.testManifest({ id: SETTINGS_NAMESPACE })
|
|
53
|
+
const admitted = await testUtils.mountAdmitted(app, SETTINGS_NAMESPACE, manifest)
|
|
54
|
+
return { context: admitted.context, fiber: admitted.fiber, manual: false }
|
|
55
|
+
}
|
|
41
56
|
const pink = await import(pathToFileURL(join(pluginRoot, 'lib', 'types', 'index.js')).href)
|
|
42
57
|
const { SETTINGS_NAMESPACE } = await import(pathToFileURL(join(pluginRoot, 'scripts', 'expected-settings-contract.mjs')).href)
|
|
43
58
|
|
|
44
59
|
const app = new Context()
|
|
45
60
|
await app.plugin(pluginHost.default ?? pluginHost)
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
await
|
|
61
|
+
const mount = await mountPlugin(app)
|
|
62
|
+
if (mount.manual) console.log('* mountAdmitted unavailable on this host; using manual plugin mount')
|
|
63
|
+
await mount.context.plugin(pink)
|
|
49
64
|
await app.plugin(extensions.default ?? extensions)
|
|
50
65
|
|
|
51
66
|
const host = themesModule.getHostThemes(app.get('tuiThemes'))
|
|
52
67
|
assert.ok(host, 'runtime theme host must be mounted')
|
|
53
68
|
const deadline = Date.now() + 5_000
|
|
54
69
|
while (host.getSnapshot().length !== 3 && Date.now() < deadline) {
|
|
55
|
-
await
|
|
70
|
+
await sleep(25)
|
|
56
71
|
}
|
|
57
72
|
const snapshot = host.getSnapshot()
|
|
58
73
|
assert.deepEqual(snapshot.map(entry => entry.name).sort(), ['pink-ansi', 'pink-day', 'pink-night'])
|
|
59
74
|
|
|
75
|
+
// dsh-TUI >= 0.10.1 semantic-rename sentinel: the host normalizes legacy keys
|
|
76
|
+
// at admission (aliases map to canonical names, retired keys are dropped),
|
|
77
|
+
// while pre-0.10.1 hosts snapshot the descriptor colors verbatim.
|
|
78
|
+
const semanticKeys = 'accent' in themeModule.getTheme('dark')
|
|
60
79
|
for (const entry of snapshot) {
|
|
61
80
|
const expected = JSON.parse(readFileSync(join(pluginRoot, 'themes', `${entry.name}.json`), 'utf8'))
|
|
62
81
|
assert.equal(entry.displayName, expected.displayName)
|
|
63
82
|
assert.equal(entry.base, expected.base)
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
83
|
+
if (semanticKeys) {
|
|
84
|
+
assert.equal(entry.colors.accent, expected.colors.claude)
|
|
85
|
+
assert.equal(entry.colors.accentShimmer, expected.colors.claudeShimmer)
|
|
86
|
+
assert.equal(entry.colors.activity, expected.colors.claudeBlue_FOR_SYSTEM_SPINNER)
|
|
87
|
+
assert.equal(entry.colors.activityShimmer, expected.colors.claudeBlueShimmer_FOR_SYSTEM_SPINNER)
|
|
88
|
+
assert.equal(entry.colors.mascotBody, expected.colors.clawd_body)
|
|
89
|
+
assert.equal(entry.colors.inputBackground, expected.colors.clawd_background)
|
|
90
|
+
assert.equal(entry.colors.userPromptLabel, expected.colors.briefLabelYou)
|
|
91
|
+
assert.equal(entry.colors.text, expected.colors.text)
|
|
92
|
+
assert.equal('rainbow_red' in entry.colors, false, 'retired keys are dropped at admission')
|
|
93
|
+
assert.equal(host.resolve(entry.name).accent, expected.colors.claude)
|
|
94
|
+
assert.equal(themeModule.getTheme(entry.name).accent, expected.colors.claude)
|
|
95
|
+
} else {
|
|
96
|
+
assert.deepEqual(entry.colors, expected.colors)
|
|
97
|
+
assert.deepEqual(host.resolve(entry.name), { ...themeModule.getTheme(entry.base), ...expected.colors })
|
|
98
|
+
assert.equal(themeModule.getTheme(entry.name).claude, expected.colors.claude)
|
|
99
|
+
}
|
|
67
100
|
}
|
|
68
101
|
assert.equal(existsSync(staticThemes), false, 'runtime registration must not create static theme files')
|
|
69
|
-
await
|
|
102
|
+
await sleep(1_700)
|
|
70
103
|
assert.equal(existsSync(staticThemes), false, 'runtime confirmation must leave no static fallback files')
|
|
71
104
|
|
|
72
105
|
const ledgerPath = join(dataDir, 'effect-ledger.jsonl')
|
|
@@ -74,11 +107,13 @@ const readLedger = () => (existsSync(ledgerPath) ? readFileSync(ledgerPath, 'utf
|
|
|
74
107
|
const themeCreates = readLedger().filter(entry => entry.resource?.kind === 'theme' && entry.operation === 'create')
|
|
75
108
|
assert.deepEqual(themeCreates.map(entry => entry.resource.id).sort(), ['pink-ansi', 'pink-day', 'pink-night'])
|
|
76
109
|
|
|
77
|
-
await
|
|
78
|
-
await
|
|
110
|
+
await mount.fiber.dispose()
|
|
111
|
+
await sleep(50)
|
|
79
112
|
assert.deepEqual(host.getSnapshot(), [], 'disposing the admitted plugin must release runtime themes')
|
|
80
|
-
|
|
81
|
-
|
|
113
|
+
if (!mount.manual) {
|
|
114
|
+
const themeReleases = readLedger().filter(entry => entry.resource?.kind === 'theme' && entry.operation === 'release')
|
|
115
|
+
assert.deepEqual(themeReleases.map(entry => entry.resource.id).sort(), ['pink-ansi', 'pink-day', 'pink-night'])
|
|
116
|
+
}
|
|
82
117
|
assert.deepEqual(themeModule.getTheme('pink-night'), themeModule.getTheme('dark'), 'disposed runtime theme no longer resolves')
|
|
83
118
|
console.log('OK runtime themes: 3 registered, no static files, ledger create/release, disposal clean')
|
|
84
119
|
|
|
@@ -106,23 +141,50 @@ for (const theme of ['pink-night', 'pink-day', 'pink-ansi']) {
|
|
|
106
141
|
const deliveries = []
|
|
107
142
|
const app2 = new Context()
|
|
108
143
|
await app2.plugin(pluginHost.default ?? pluginHost)
|
|
109
|
-
const
|
|
110
|
-
await
|
|
144
|
+
const mount2 = await mountPlugin(app2)
|
|
145
|
+
await mount2.context.plugin(pink)
|
|
111
146
|
await app2.plugin(extensions.default ?? extensions)
|
|
112
147
|
toastModule.getHostToastStore(app2.get('tuiToast'))?.setSink(delivery => deliveries.push(delivery))
|
|
113
148
|
|
|
114
149
|
const toastDeadline = Date.now() + 8_000
|
|
115
150
|
while (deliveries.length === 0 && Date.now() < toastDeadline) {
|
|
116
|
-
await
|
|
151
|
+
await sleep(25)
|
|
117
152
|
}
|
|
118
153
|
assert.ok(deliveries.length >= 1, 'the shadow-hint toast must reach the host sink (retry included)')
|
|
119
154
|
assert.equal(deliveries[0].color, undefined, 'the shadow hint is neutral')
|
|
120
155
|
for (const file of ['pink-night.json', 'pink-day.json', 'pink-ansi.json']) {
|
|
121
156
|
assert.ok(deliveries[0].text.includes(file), `hint must name ${file}`)
|
|
122
157
|
}
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
158
|
+
|
|
159
|
+
// The same shadow situation additionally offers the one-shot guided cleanup
|
|
160
|
+
// dialog through the real tuiDialogs seam (host-managed confirm panel).
|
|
161
|
+
// Headless has no chat screen: the request parks in the dialog store, which
|
|
162
|
+
// is exactly the early-boot timing the plugin relies on — the dialog becomes
|
|
163
|
+
// visible as soon as the chat screen drains the store.
|
|
164
|
+
const dialogsModule = await import(pathToFileURL(join(adapter, 'dialogs.js')).href)
|
|
165
|
+
const dialogStore = dialogsModule.getHostDialogStore(app2.get('tuiDialogs'))
|
|
166
|
+
assert.ok(dialogStore, 'the real host must expose the dialog store')
|
|
167
|
+
const dialogDeadline = Date.now() + 5_000
|
|
168
|
+
while (dialogStore.getSnapshot() === null && Date.now() < dialogDeadline) {
|
|
169
|
+
await sleep(25)
|
|
170
|
+
}
|
|
171
|
+
const dialogSnapshot = dialogStore.getSnapshot()
|
|
172
|
+
assert.ok(dialogSnapshot, 'the shadow situation must offer the cleanup dialog')
|
|
173
|
+
assert.equal(dialogSnapshot.kind, 'confirm')
|
|
174
|
+
assert.ok(dialogSnapshot.title.includes('旧主题文件'), 'the dialog title names the cleanup')
|
|
175
|
+
for (const file of ['pink-night.json', 'pink-day.json', 'pink-ansi.json']) {
|
|
176
|
+
assert.ok((dialogSnapshot.message ?? '').includes(file), `the dialog message must name ${file}`)
|
|
177
|
+
}
|
|
178
|
+
assert.equal(existsSync(join(dataDir2, 'themes', 'pink-night.json')), true, 'an unanswered dialog deletes nothing')
|
|
179
|
+
await mount2.fiber.dispose()
|
|
180
|
+
await sleep(50)
|
|
181
|
+
assert.equal(dialogStore.getSnapshot(), null, 'disposing the activation settles the parked dialog')
|
|
182
|
+
assert.equal(
|
|
183
|
+
existsSync(join(dataDir2, 'themes', 'pink-night.json')),
|
|
184
|
+
true,
|
|
185
|
+
'a cancelled dialog must not delete the shadow files',
|
|
186
|
+
)
|
|
187
|
+
console.log('OK toast: shadow hint delivered through the real tuiToast seam; cleanup dialog offered and settled')
|
|
126
188
|
|
|
127
189
|
// ── Phase 3: apply-time toasts reach the sink through the seam-late retry ───
|
|
128
190
|
// On a real 0.10 host the tuiToast service arrives with the extensions row,
|
|
@@ -157,15 +219,15 @@ const fakeSettings = {
|
|
|
157
219
|
const deliveries3 = []
|
|
158
220
|
const app3 = new Context()
|
|
159
221
|
await app3.plugin(pluginHost.default ?? pluginHost)
|
|
160
|
-
const
|
|
222
|
+
const mount3 = await mountPlugin(app3)
|
|
161
223
|
app3.provide('settings', fakeSettings)
|
|
162
|
-
await
|
|
224
|
+
await mount3.context.plugin(pink)
|
|
163
225
|
await app3.plugin(extensions.default ?? extensions)
|
|
164
226
|
toastModule.getHostToastStore(app3.get('tuiToast'))?.setSink(delivery => deliveries3.push(delivery))
|
|
165
227
|
|
|
166
228
|
const applyToastDeadline = Date.now() + 8_000
|
|
167
229
|
while (deliveries3.length < 2 && Date.now() < applyToastDeadline) {
|
|
168
|
-
await
|
|
230
|
+
await sleep(25)
|
|
169
231
|
}
|
|
170
232
|
const healToast = deliveries3.find(delivery => delivery.text.includes('已修复'))
|
|
171
233
|
assert.ok(healToast, 'the self-heal warning fired during apply must reach the host sink')
|
|
@@ -176,5 +238,80 @@ assert.ok(followToast, 'the boot-follow result fired at settings time must reach
|
|
|
176
238
|
assert.equal(followToast.color, 'success', 'the boot-follow toast is a success')
|
|
177
239
|
assert.ok(followToast.text.includes('pink-day') && followToast.text.includes('reload'), 'the follow toast names the new theme and the reload hint')
|
|
178
240
|
assert.equal(JSON.parse(readFileSync(join(dataDir3, 'theme.json'), 'utf8')).theme, 'pink-day', 'the follow pref write still happened')
|
|
179
|
-
await
|
|
241
|
+
await mount3.fiber.dispose()
|
|
180
242
|
console.log('OK toast phase 3: apply-time self-heal and boot-follow toasts delivered on the real host')
|
|
243
|
+
|
|
244
|
+
// ── Phase 4: settings namespace registers against the REAL service, late ────
|
|
245
|
+
// The stub suites cover the namespace registration with fakes, and phase 3
|
|
246
|
+
// stands in a minimal fake at apply time; this phase mounts a real
|
|
247
|
+
// @deepseek-ai/dsh-settings SettingsProvider AFTER the plugin (and after the
|
|
248
|
+
// extensions row), so the plugin's parked ['settings'] inject fires against
|
|
249
|
+
// the genuine register()/scope/watch machinery. The end-to-end signal: a
|
|
250
|
+
// user-layer followSystem write through the real service flips theme.json.
|
|
251
|
+
const sandbox4 = mkdtempSync(join(tmpdir(), 'pink-settings-late-'))
|
|
252
|
+
process.env.USERPROFILE = sandbox4
|
|
253
|
+
process.env.HOME = sandbox4
|
|
254
|
+
const dataDir4 = join(sandbox4, '.dsh-tui')
|
|
255
|
+
mkdirSync(dataDir4, { recursive: true })
|
|
256
|
+
writeFileSync(join(dataDir4, 'theme-follow.json'), JSON.stringify({ light: true, at: 1 }, null, 2))
|
|
257
|
+
|
|
258
|
+
const { SettingsProvider } = await import(pathToFileURL(hostRequire.resolve('@deepseek-ai/dsh-settings')).href)
|
|
259
|
+
|
|
260
|
+
/** Minimal concrete provider: in-memory storage, no file, no background IO. */
|
|
261
|
+
class MemorySettingsProvider extends SettingsProvider {
|
|
262
|
+
static provide = 'settings'
|
|
263
|
+
writable = true
|
|
264
|
+
stored = {}
|
|
265
|
+
async load() {
|
|
266
|
+
return { ...this.stored }
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
async persist(ns, section) {
|
|
270
|
+
this.stored[ns] = { ...section }
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const app4 = new Context()
|
|
275
|
+
await app4.plugin(pluginHost.default ?? pluginHost)
|
|
276
|
+
const mount4 = await mountPlugin(app4)
|
|
277
|
+
await mount4.context.plugin(pink)
|
|
278
|
+
await app4.plugin(extensions.default ?? extensions)
|
|
279
|
+
// Deliberately last: the namespace registration can only be "late" when the
|
|
280
|
+
// service arrives after everything the plugin injects at apply time.
|
|
281
|
+
await app4.plugin(MemorySettingsProvider)
|
|
282
|
+
|
|
283
|
+
const provider = app4.get('settings')
|
|
284
|
+
assert.ok(provider, 'the real dsh-settings provider must be mounted')
|
|
285
|
+
const settingsDeadline = Date.now() + 5_000
|
|
286
|
+
while (provider.get(SETTINGS_NAMESPACE) === undefined && Date.now() < settingsDeadline) {
|
|
287
|
+
await sleep(25)
|
|
288
|
+
}
|
|
289
|
+
assert.ok(
|
|
290
|
+
provider.get(SETTINGS_NAMESPACE),
|
|
291
|
+
`the plugin namespace must register with the real service within ${5_000}ms of its late arrival`,
|
|
292
|
+
)
|
|
293
|
+
await provider.update(SETTINGS_NAMESPACE, { followSystem: true })
|
|
294
|
+
assert.equal(
|
|
295
|
+
provider.get(SETTINGS_NAMESPACE)?.followSystem,
|
|
296
|
+
true,
|
|
297
|
+
'the real service must resolve the committed user layer for the namespace',
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
const prefPath4 = join(dataDir4, 'theme.json')
|
|
301
|
+
const followDeadline = Date.now() + 5_000
|
|
302
|
+
let followed = false
|
|
303
|
+
while (!followed && Date.now() < followDeadline) {
|
|
304
|
+
try {
|
|
305
|
+
followed = JSON.parse(readFileSync(prefPath4, 'utf8')).theme === 'pink-day'
|
|
306
|
+
} catch {
|
|
307
|
+
// Pref not written yet.
|
|
308
|
+
}
|
|
309
|
+
if (!followed) await sleep(25)
|
|
310
|
+
}
|
|
311
|
+
assert.equal(
|
|
312
|
+
followed,
|
|
313
|
+
true,
|
|
314
|
+
'the user-layer follow toggle must apply the cached light background through the real service',
|
|
315
|
+
)
|
|
316
|
+
await mount4.fiber.dispose()
|
|
317
|
+
console.log('OK settings: namespace registers late on the real dsh-settings service and drives the follow pref')
|