dsh-tui-theme 0.6.0 → 0.6.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/lib/types/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAGH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAMlD,OAAO,EAAwB,KAAK,eAAe,EAAE,MAAM,sBAAsB,CAAA;AAIjF,eAAO,MAAM,IAAI,kBAAY,CAAA;AAE7B,wDAAwD;AACxD,MAAM,MAAM,MAAM,GAAG,eAAe,CAAA;AAUpC,eAAO,MAAM,MAAM,EAAE,WAAW,CAAC,MAAM,CAQrC,CAAA;AAkBF;;;;GAIG;AACH,wBAAgB,KAAK,CAAC,GAAG,EAAE,OAAO,EAAE,MAAM,GAAE,MAAW,GAAG,IAAI,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAGH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAMlD,OAAO,EAAwB,KAAK,eAAe,EAAE,MAAM,sBAAsB,CAAA;AAIjF,eAAO,MAAM,IAAI,kBAAY,CAAA;AAE7B,wDAAwD;AACxD,MAAM,MAAM,MAAM,GAAG,eAAe,CAAA;AAUpC,eAAO,MAAM,MAAM,EAAE,WAAW,CAAC,MAAM,CAQrC,CAAA;AAkBF;;;;GAIG;AACH,wBAAgB,KAAK,CAAC,GAAG,EAAE,OAAO,EAAE,MAAM,GAAE,MAAW,GAAG,IAAI,CAwJ7D"}
|
package/lib/types/index.js
CHANGED
|
@@ -101,8 +101,15 @@ export function apply(ctx, config = {}) {
|
|
|
101
101
|
const firstConfirmation = !runtimeConfirmed;
|
|
102
102
|
runtimeConfirmed = true;
|
|
103
103
|
if (ownedStaticFiles.size > 0) {
|
|
104
|
-
|
|
104
|
+
// The write-then-remove cycle: this boot installed static fallbacks
|
|
105
|
+
// because the runtime seam was absent at apply time, then the seam
|
|
106
|
+
// arrived. Logged so the cycle's real-world frequency (and any future
|
|
107
|
+
// deferred-install optimization) can be measured from plugin logs.
|
|
108
|
+
const removed = removeBundledThemes([...ownedStaticFiles]);
|
|
105
109
|
ownedStaticFiles.clear();
|
|
110
|
+
if (removed.length > 0) {
|
|
111
|
+
ctx.logger.info(`${PLUGIN_ID}: runtime themes confirmed; removed ${removed.length} static fallback file(s) installed earlier this boot`);
|
|
112
|
+
}
|
|
106
113
|
}
|
|
107
114
|
// Same-named static files win over runtime registrations. User-edited
|
|
108
115
|
// files stay unmentioned; files byte-identical to the bundled copy add
|
|
@@ -8,8 +8,16 @@
|
|
|
8
8
|
*
|
|
9
9
|
* The line belongs to the pink palettes: by default it only renders while a
|
|
10
10
|
* pink theme is active (checked per render with the host's own theme
|
|
11
|
-
* precedence, so a mid-session /theme switch takes effect
|
|
12
|
-
* `statusScope: 'all-themes'` opts it
|
|
11
|
+
* precedence, so a mid-session /theme switch takes effect within the pref
|
|
12
|
+
* cache TTL — at most one clock tick); `statusScope: 'all-themes'` opts it
|
|
13
|
+
* into every other theme too.
|
|
14
|
+
*
|
|
15
|
+
* Cost discipline: `session/event` is a token-level firehose (assistant/chunk
|
|
16
|
+
* et al.), but the rendered text only changes at turn boundaries and on the
|
|
17
|
+
* clock, so renders run on turn/start, turn/end, session/disposed, and the
|
|
18
|
+
* 15s tick — never per streamed chunk. The persisted-pref read behind the
|
|
19
|
+
* theme check is cached for the same tick length so a render is pure string
|
|
20
|
+
* building.
|
|
13
21
|
* @module dsh-tui-theme/statusLine
|
|
14
22
|
*/
|
|
15
23
|
import type { Context } from '@deepseek-ai/cordis';
|
|
@@ -29,6 +37,11 @@ export interface StatusOptions {
|
|
|
29
37
|
}
|
|
30
38
|
/** Fully-resolved status knobs. */
|
|
31
39
|
export type EffectiveStatus = Required<StatusOptions>;
|
|
40
|
+
/**
|
|
41
|
+
* @internal Drop the persisted-pref cache (verify.mjs only; not part of the
|
|
42
|
+
* plugin's behavioral contract). Production invalidation is the TTL.
|
|
43
|
+
*/
|
|
44
|
+
export declare function invalidateThemePrefCacheForTests(): void;
|
|
32
45
|
/**
|
|
33
46
|
* Start the status line inside the `tuiStatus` inject.
|
|
34
47
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"statusLine.d.ts","sourceRoot":"","sources":["../../src/statusLine.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"statusLine.d.ts","sourceRoot":"","sources":["../../src/statusLine.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAGH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAQlD,mDAAmD;AACnD,MAAM,MAAM,WAAW,GAAG,WAAW,GAAG,YAAY,CAAA;AAEpD,MAAM,WAAW,aAAa;IAC5B,2EAA2E;IAC3E,aAAa,CAAC,EAAE,OAAO,CAAA;IACvB,sCAAsC;IACtC,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,+BAA+B;IAC/B,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,0DAA0D;IAC1D,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,iEAAiE;IACjE,WAAW,CAAC,EAAE,WAAW,CAAA;CAC1B;AAED,mCAAmC;AACnC,MAAM,MAAM,eAAe,GAAG,QAAQ,CAAC,aAAa,CAAC,CAAA;AAsBrD;;;GAGG;AACH,wBAAgB,gCAAgC,IAAI,IAAI,CAEvD;AA0CD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,OAAO,EAAE,YAAY,EAAE,MAAM,eAAe,GAAG,IAAI,CAqEvF"}
|
package/lib/types/statusLine.js
CHANGED
|
@@ -8,8 +8,16 @@
|
|
|
8
8
|
*
|
|
9
9
|
* The line belongs to the pink palettes: by default it only renders while a
|
|
10
10
|
* pink theme is active (checked per render with the host's own theme
|
|
11
|
-
* precedence, so a mid-session /theme switch takes effect
|
|
12
|
-
* `statusScope: 'all-themes'` opts it
|
|
11
|
+
* precedence, so a mid-session /theme switch takes effect within the pref
|
|
12
|
+
* cache TTL — at most one clock tick); `statusScope: 'all-themes'` opts it
|
|
13
|
+
* into every other theme too.
|
|
14
|
+
*
|
|
15
|
+
* Cost discipline: `session/event` is a token-level firehose (assistant/chunk
|
|
16
|
+
* et al.), but the rendered text only changes at turn boundaries and on the
|
|
17
|
+
* clock, so renders run on turn/start, turn/end, session/disposed, and the
|
|
18
|
+
* 15s tick — never per streamed chunk. The persisted-pref read behind the
|
|
19
|
+
* theme check is cached for the same tick length so a render is pure string
|
|
20
|
+
* building.
|
|
13
21
|
* @module dsh-tui-theme/statusLine
|
|
14
22
|
*/
|
|
15
23
|
import { join } from 'node:path';
|
|
@@ -21,13 +29,30 @@ const GLYPH = '✿';
|
|
|
21
29
|
// the cordis plugin name — one literal would be three drift risks).
|
|
22
30
|
const STATUS_KEY = PLUGIN_ID;
|
|
23
31
|
const CLOCK_TICK_MS = 15_000;
|
|
32
|
+
// The persisted-pref cache lives for one clock tick: a mid-session /theme
|
|
33
|
+
// switch (the host rewrites theme.json alongside its own in-memory switch)
|
|
34
|
+
// lands on the next turn boundary or tick, which is exactly the "next tick"
|
|
35
|
+
// semantic the render path always promised. Without the cache every render
|
|
36
|
+
// would pay a synchronous stat+read+JSON.parse on the host's UI thread.
|
|
37
|
+
const THEME_PREF_TTL_MS = CLOCK_TICK_MS;
|
|
24
38
|
/** The bundled themes this garnish belongs to. */
|
|
25
39
|
const PINK_THEMES = new Set(['pink-night', 'pink-day', 'pink-ansi']);
|
|
40
|
+
// `value: undefined` is a cached "no pref file / unparsable" answer, so the
|
|
41
|
+
// common non-pink host (no theme.json at all) is one memory read per TTL
|
|
42
|
+
// window instead of a failed syscall per render.
|
|
43
|
+
let prefCache;
|
|
44
|
+
/**
|
|
45
|
+
* @internal Drop the persisted-pref cache (verify.mjs only; not part of the
|
|
46
|
+
* plugin's behavioral contract). Production invalidation is the TTL.
|
|
47
|
+
*/
|
|
48
|
+
export function invalidateThemePrefCacheForTests() {
|
|
49
|
+
prefCache = undefined;
|
|
50
|
+
}
|
|
26
51
|
/**
|
|
27
52
|
* The active theme name by the host's own precedence: DSH_TUI_THEME first,
|
|
28
|
-
* then the persisted ~/.dsh-tui/theme.json pref
|
|
29
|
-
* auto-detection) only ever resolves to a
|
|
30
|
-
* so "no pref" means non-pink.
|
|
53
|
+
* then the persisted ~/.dsh-tui/theme.json pref (read through a one-tick
|
|
54
|
+
* cache). The unforced path (OSC 11 auto-detection) only ever resolves to a
|
|
55
|
+
* builtin palette, never a pink one, so "no pref" means non-pink.
|
|
31
56
|
*
|
|
32
57
|
* This deliberately mirrors the host's ThemeProvider resolution chain
|
|
33
58
|
* (`components/design-system/ThemeProvider.tsx`, baseline dsh-TUI 0.9.3);
|
|
@@ -38,7 +63,11 @@ function activeThemeName(dataDir) {
|
|
|
38
63
|
const env = process.env.DSH_TUI_THEME;
|
|
39
64
|
if (env !== undefined && env !== '')
|
|
40
65
|
return env;
|
|
41
|
-
|
|
66
|
+
const now = Date.now();
|
|
67
|
+
if (prefCache === undefined || now - prefCache.at >= THEME_PREF_TTL_MS) {
|
|
68
|
+
prefCache = { at: now, value: readThemePref(dataDir) };
|
|
69
|
+
}
|
|
70
|
+
return prefCache.value;
|
|
42
71
|
}
|
|
43
72
|
function isPinkThemeActive(dataDir) {
|
|
44
73
|
const name = activeThemeName(dataDir);
|
|
@@ -76,7 +105,8 @@ export function startStatusLine(ctx, getEffective) {
|
|
|
76
105
|
const parts = [];
|
|
77
106
|
// The line is pink garnish: off on other themes unless opted in.
|
|
78
107
|
// The theme check runs here (not once at startup) so a mid-session
|
|
79
|
-
// /theme switch lands
|
|
108
|
+
// /theme switch lands within one pref-cache TTL — the next tick or
|
|
109
|
+
// turn boundary.
|
|
80
110
|
const themeAllows = eff.statusScope === 'all-themes' || isPinkThemeActive(dataDir);
|
|
81
111
|
if (eff.statusEnabled && themeAllows) {
|
|
82
112
|
if (eff.showGlyph)
|
|
@@ -102,10 +132,17 @@ export function startStatusLine(ctx, getEffective) {
|
|
|
102
132
|
};
|
|
103
133
|
statusCtx.on('session/event', (session, event) => {
|
|
104
134
|
current = session;
|
|
105
|
-
|
|
135
|
+
const type = event?.type;
|
|
136
|
+
if (type === 'turn/end') {
|
|
106
137
|
turns.set(session, (turns.get(session) ?? 0) + 1);
|
|
107
138
|
}
|
|
108
|
-
|
|
139
|
+
// The firehose filter: session/event carries every token-level chunk,
|
|
140
|
+
// tool call, and step bracket, but the rendered text only changes at
|
|
141
|
+
// turn boundaries (the count) or on the clock timer. turn/start is a
|
|
142
|
+
// session's first live event, so a session switch repaints immediately
|
|
143
|
+
// (the new session's count is 0 until its first turn ends).
|
|
144
|
+
if (type === 'turn/start' || type === 'turn/end')
|
|
145
|
+
render();
|
|
109
146
|
});
|
|
110
147
|
statusCtx.on('session/disposed', session => {
|
|
111
148
|
turns.delete(session);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-tui-theme",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.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",
|
package/scripts/verify.mjs
CHANGED
|
@@ -45,7 +45,7 @@ const sandboxThemes = join(sandboxHome, '.dsh-tui', 'themes')
|
|
|
45
45
|
|
|
46
46
|
const { name, apply } = await import('../lib/types/index.js')
|
|
47
47
|
const { installBundledThemes, readBundledThemes, findShadowedBundledThemes } = await import('../lib/types/themeAssets.js')
|
|
48
|
-
const { startStatusLine } = await import('../lib/types/statusLine.js')
|
|
48
|
+
const { startStatusLine, invalidateThemePrefCacheForTests } = await import('../lib/types/statusLine.js')
|
|
49
49
|
const { setToastRetryDelaysForTests } = await import('../lib/types/toast.js')
|
|
50
50
|
const {
|
|
51
51
|
themeForBackground,
|
|
@@ -399,8 +399,10 @@ const emit = (record, event, ...args) => {
|
|
|
399
399
|
emit(record, 'session/event', session, { type: 'turn/end' })
|
|
400
400
|
assert.match(statusCalls.at(-1)[1], /^✿ · \d{2}:\d{2} · 1✦$/)
|
|
401
401
|
|
|
402
|
-
// Non-pink theme active → hidden by default.
|
|
402
|
+
// Non-pink theme active → hidden by default. The pref cache is dropped so
|
|
403
|
+
// the rewrite is visible immediately (production invalidation is the TTL).
|
|
403
404
|
writeFileSync(themePrefPath, JSON.stringify({ theme: 'dark' }, null, 2))
|
|
405
|
+
invalidateThemePrefCacheForTests()
|
|
404
406
|
emit(record, 'session/event', session, { type: 'turn/end' })
|
|
405
407
|
assert.equal(statusCalls.at(-1)[1], undefined)
|
|
406
408
|
|
|
@@ -837,5 +839,83 @@ const emit = (record, event, ...args) => {
|
|
|
837
839
|
console.log('✓ toast feedback: follow/self-heal/shadow hints delivered, dropped and seam-less sends retried, legacy hosts silent and bounded')
|
|
838
840
|
}
|
|
839
841
|
|
|
842
|
+
// ── 14. hot path: the token firehose must not reach render or the disk ──────
|
|
843
|
+
{
|
|
844
|
+
const themePrefPath = join(sandboxHome, '.dsh-tui', 'theme.json')
|
|
845
|
+
writeFileSync(themePrefPath, JSON.stringify({ theme: 'pink-night' }, null, 2))
|
|
846
|
+
// Earlier scenarios ran status activations against a different pref; drop
|
|
847
|
+
// their cached answer so this scenario starts from the fresh sandbox state.
|
|
848
|
+
invalidateThemePrefCacheForTests()
|
|
849
|
+
const statusCalls = []
|
|
850
|
+
const settingsRecord = { registerCalls: [], watchers: [] }
|
|
851
|
+
const { ctx, record } = makeStubCtx({
|
|
852
|
+
status: fakeStatus(statusCalls),
|
|
853
|
+
settingsService: fakeSettingsService(settingsRecord, {}),
|
|
854
|
+
})
|
|
855
|
+
await applyAndSettle(ctx)
|
|
856
|
+
const session = { id: 'h1' }
|
|
857
|
+
emit(record, 'session/event', session, { type: 'turn/end' })
|
|
858
|
+
const baselineCalls = statusCalls.length
|
|
859
|
+
assert.match(statusCalls.at(-1)[1], /1✦$/)
|
|
860
|
+
|
|
861
|
+
// A firehose burst (streaming chunks, tool traffic, step brackets) updates
|
|
862
|
+
// the tracked session but must render nothing and never touch the disk.
|
|
863
|
+
const originalRead = builtinFs.readFileSync
|
|
864
|
+
let prefReads = 0
|
|
865
|
+
try {
|
|
866
|
+
builtinFs.readFileSync = (path, ...rest) => {
|
|
867
|
+
if (String(path) === themePrefPath) prefReads += 1
|
|
868
|
+
return originalRead(path, ...rest)
|
|
869
|
+
}
|
|
870
|
+
syncBuiltinESMExports()
|
|
871
|
+
for (let index = 0; index < 50; index += 1) {
|
|
872
|
+
emit(record, 'session/event', session, {
|
|
873
|
+
type: 'assistant/chunk',
|
|
874
|
+
turn: 1,
|
|
875
|
+
step: 1,
|
|
876
|
+
chunk: { type: 'text', text: 'x' },
|
|
877
|
+
})
|
|
878
|
+
}
|
|
879
|
+
emit(record, 'session/event', session, {
|
|
880
|
+
type: 'tool/call', turn: 1, step: 1, callId: 'c1', name: 'tool', arguments: '{}',
|
|
881
|
+
})
|
|
882
|
+
emit(record, 'session/event', session, { type: 'step/end', turn: 1, step: 1 })
|
|
883
|
+
emit(record, 'session/event', session, { type: 'todo/write', todos: [] })
|
|
884
|
+
assert.equal(statusCalls.length, baselineCalls, 'firehose events render nothing')
|
|
885
|
+
assert.equal(prefReads, 0, 'firehose events never read the theme pref')
|
|
886
|
+
|
|
887
|
+
// A turn boundary renders — served by the warm pref cache (no disk hit
|
|
888
|
+
// within the TTL), so even boundary renders stay off the filesystem.
|
|
889
|
+
emit(record, 'session/event', session, { type: 'turn/end' })
|
|
890
|
+
assert.equal(statusCalls.length, baselineCalls + 1, 'a turn boundary renders')
|
|
891
|
+
assert.equal(prefReads, 0, 'the warm cache serves the boundary render')
|
|
892
|
+
|
|
893
|
+
// A session switch repaints at its first turn/start with a fresh count.
|
|
894
|
+
const nextSession = { id: 'h2' }
|
|
895
|
+
emit(record, 'session/event', nextSession, { type: 'turn/start' })
|
|
896
|
+
assert.equal(statusCalls.length, baselineCalls + 2)
|
|
897
|
+
assert.match(statusCalls.at(-1)[1], /0✦$/)
|
|
898
|
+
assert.equal(prefReads, 0)
|
|
899
|
+
|
|
900
|
+
// Inside the TTL a pref rewrite is not yet visible and costs no read…
|
|
901
|
+
writeFileSync(themePrefPath, JSON.stringify({ theme: 'dark' }, null, 2))
|
|
902
|
+
emit(record, 'session/event', nextSession, { type: 'turn/end' })
|
|
903
|
+
assert.equal(statusCalls.length, baselineCalls + 3)
|
|
904
|
+
assert.match(statusCalls.at(-1)[1], /1✦$/, 'the cached pink pref keeps the line visible')
|
|
905
|
+
assert.equal(prefReads, 0, 'the TTL serves the stale-but-pink answer without I/O')
|
|
906
|
+
|
|
907
|
+
// …after invalidation (TTL expiry in production) the next render re-reads.
|
|
908
|
+
invalidateThemePrefCacheForTests()
|
|
909
|
+
emit(record, 'session/event', nextSession, { type: 'turn/end' })
|
|
910
|
+
assert.equal(prefReads, 1, 'invalidation re-reads the pref exactly once')
|
|
911
|
+
assert.equal(statusCalls.at(-1)[1], undefined, 'the non-pink pref hides the line')
|
|
912
|
+
} finally {
|
|
913
|
+
builtinFs.readFileSync = originalRead
|
|
914
|
+
syncBuiltinESMExports()
|
|
915
|
+
invalidateThemePrefCacheForTests()
|
|
916
|
+
}
|
|
917
|
+
console.log('✓ hot path: firehose events render nothing and never read the pref; boundaries use the cache')
|
|
918
|
+
}
|
|
919
|
+
|
|
840
920
|
console.log('\nAll plugin verifications passed.')
|
|
841
921
|
console.log(`(sandbox used: ${sandboxHome} — the real home was never touched)`)
|