@aiwayds/dsh-tui-pi 2.20.0 → 2.22.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.md +19 -8
- package/README.zh-CN.md +19 -8
- package/cordis.patch.yml +11 -2
- package/icon.svg +7 -0
- package/lib/agents.js +23 -3
- package/lib/agents.js.map +1 -1
- package/lib/btw-overlay.d.ts +18 -2
- package/lib/btw-overlay.js +55 -15
- package/lib/btw-overlay.js.map +1 -1
- package/lib/btw.d.ts +43 -5
- package/lib/btw.js +40 -6
- package/lib/btw.js.map +1 -1
- package/lib/dsh-events.d.ts +23 -9
- package/lib/dsh-events.js +20 -8
- package/lib/dsh-events.js.map +1 -1
- package/lib/history.d.ts +8 -2
- package/lib/history.js +4 -1
- package/lib/history.js.map +1 -1
- package/lib/index.d.ts +3 -1
- package/lib/index.js +119 -61
- package/lib/index.js.map +1 -1
- package/lib/live-widgets.js +6 -3
- package/lib/live-widgets.js.map +1 -1
- package/lib/login.js.map +1 -1
- package/lib/messages.d.ts +10 -0
- package/lib/messages.js +39 -0
- package/lib/messages.js.map +1 -1
- package/lib/preset.d.ts +128 -58
- package/lib/preset.js +208 -113
- package/lib/preset.js.map +1 -1
- package/lib/remote-tail.js +3 -0
- package/lib/remote-tail.js.map +1 -1
- package/lib/selectors.d.ts +4 -1
- package/lib/selectors.js +8 -3
- package/lib/selectors.js.map +1 -1
- package/lib/session.d.ts +3 -1
- package/lib/session.js +16 -11
- package/lib/session.js.map +1 -1
- package/lib/sessions.d.ts +7 -0
- package/lib/sessions.js +15 -1
- package/lib/sessions.js.map +1 -1
- package/lib/settings.d.ts +17 -11
- package/lib/settings.js +35 -26
- package/lib/settings.js.map +1 -1
- package/lib/source-kind.d.ts +26 -0
- package/lib/source-kind.js +17 -0
- package/lib/source-kind.js.map +1 -0
- package/lib/subagent-policy.js +1 -1
- package/lib/subagent-policy.js.map +1 -1
- package/lib/subagent-viewer.d.ts +3 -3
- package/lib/subagent-viewer.js +9 -6
- package/lib/subagent-viewer.js.map +1 -1
- package/lib/theme-settings.d.ts +353 -159
- package/lib/theme-settings.js +258 -336
- package/lib/theme-settings.js.map +1 -1
- package/lib/tokens.d.ts +5 -3
- package/lib/tokens.js +5 -8
- package/lib/tokens.js.map +1 -1
- package/locale/en.json +6 -0
- package/locale/ja.json +6 -0
- package/locale/ko.json +6 -0
- package/locale/zh-CN.json +6 -0
- package/locales/en.json +7 -2
- package/locales/ja.json +7 -2
- package/locales/ko.json +7 -2
- package/locales/zh-CN.json +7 -2
- package/package.json +22 -16
- package/scripts/diagnose-container-tui.sh +3 -3
- package/scripts/smoke-boot.mjs +2 -2
- package/skills/dsh-tui-pi-config/SKILL.md +11 -7
package/lib/theme-settings.js
CHANGED
|
@@ -1,22 +1,36 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Theme settings:
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* command. The theme and panel-height preferences are read once at TUI
|
|
6
|
-
* startup (`readThemePreference` / `readPanelHeightPreference`); the subagent
|
|
7
|
-
* limits are read live at every policy decision (`readSubagentLimits`). The
|
|
8
|
-
* namespace is marked `applies: 'live'`: a committed change (the /theme
|
|
9
|
-
* picker, the /settings browser, an external edit) is pushed through the
|
|
10
|
-
* watch hook, so the running TUI repaints without a restart.
|
|
2
|
+
* Theme settings: the dsh-tui entry configuration — the user's theme
|
|
3
|
+
* preference, the think/tool panel height, the UI language, the footer-hint
|
|
4
|
+
* selection, the icon-set mode and the subagent concurrency/rounds limits.
|
|
11
5
|
*
|
|
12
|
-
*
|
|
13
|
-
* namespace
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
6
|
+
* dsh 0.1.7 model (breaking change from 0.1.5): the runtime
|
|
7
|
+
* namespace-registration API is GONE. A plugin declares its
|
|
8
|
+
* user-facing configuration as a `static Config` schema; the loader projects
|
|
9
|
+
* every `.volatile()` field into the settings surface (describe / update /
|
|
10
|
+
* mutate) and hands the resolved values to `apply(ctx, config)` — volatile
|
|
11
|
+
* fields arrive as live `Volatile<T>` references. Declaration IS
|
|
12
|
+
* registration: there is nothing to register at runtime anymore.
|
|
13
|
+
*
|
|
14
|
+
* Reading: `config.field.get()` — a deep-frozen snapshot, refreshed IN PLACE
|
|
15
|
+
* by the loader on every volatile-only commit (the fiber never remounts, the
|
|
16
|
+
* references stay identical), so a value read is always current.
|
|
17
|
+
*
|
|
18
|
+
* Hot-apply: the plugin subscribes to `settings/document-updated` (the
|
|
19
|
+
* 0.1.5 per-namespace watch-hook replacement) and re-reads the references on
|
|
20
|
+
* every event. The
|
|
21
|
+
* event also fires for this TUI's own writes; every sink downstream is an
|
|
22
|
+
* idempotent no-op on an unchanged value (theme-bundle identity guard, same
|
|
23
|
+
* height, same hints), so the echo is harmless.
|
|
24
|
+
*
|
|
25
|
+
* Session-management sections (`retention`, `resume`, `askUser`) keep their
|
|
26
|
+
* USER-layer precedence seam: only a field the user explicitly wrote into the
|
|
27
|
+
* profile patch (`cordis.patch.yml` → entry `dsh-tui` → `config:`; the 0.1.5
|
|
28
|
+
* `settings.yaml` document is auto-imported there once on first 0.1.7 boot)
|
|
29
|
+
* is an override — the resolved value's baked-in defaults must not shadow the
|
|
30
|
+
* DSH_TUI_RETENTION_* / DSH_TUI_RESUME_* / DSH_TUI_ASK_USER_* environment
|
|
31
|
+
* variables (precedence: settings explicit > env > default). Those readers
|
|
32
|
+
* go through `settings.describe()[].user`, the only channel exposing the raw
|
|
33
|
+
* explicit layer.
|
|
20
34
|
*/
|
|
21
35
|
import { SettingsConflictError, } from '@deepseek-ai/dsh-settings';
|
|
22
36
|
import z from '@deepseek-ai/schemastery';
|
|
@@ -27,16 +41,14 @@ import { narrowStringList } from "./model-list.js";
|
|
|
27
41
|
import { RETENTION_MAX_AGE_DAYS, RETENTION_MAX_COUNT, RETENTION_MIN_IDLE_HOURS } from "./retention.js";
|
|
28
42
|
import { RESUME_MAX_AGE_DAYS, RESUME_MIN_BYTES } from "./sessions.js";
|
|
29
43
|
import { ASK_USER_ABSOLUTE_MINUTES_DEFAULT, ASK_USER_IDLE_MINUTES_DEFAULT } from "./ask-user.js";
|
|
30
|
-
import { emitNotice } from "./notice-bridge.js";
|
|
31
44
|
/**
|
|
32
|
-
* Settings
|
|
45
|
+
* Settings entry id carrying the persisted dsh-tui preferences.
|
|
33
46
|
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
* (
|
|
38
|
-
*
|
|
39
|
-
* branded `ns` stay exact string equality.
|
|
47
|
+
* This is BOTH the profile patch entry id (cordis.patch.yml mounts this
|
|
48
|
+
* package as `- id: dsh-tui`) and the legacy settings.yaml section name —
|
|
49
|
+
* keeping them identical is what lets the 0.1.7 one-time settings.yaml
|
|
50
|
+
* import (`settings.yaml` → renamed `.imported`, sections merged into the
|
|
51
|
+
* matching entry's config) pick up every existing user value without a shim.
|
|
40
52
|
*/
|
|
41
53
|
export const THEME_SETTINGS_NAMESPACE = 'dsh-tui';
|
|
42
54
|
/** The footer CH default: per-message, matching the pi-tui footer. */
|
|
@@ -48,13 +60,12 @@ export function narrowCacheHitMode(value) {
|
|
|
48
60
|
return value === 'session' ? 'session' : DEFAULT_CACHE_HIT_MODE;
|
|
49
61
|
}
|
|
50
62
|
/**
|
|
51
|
-
* Default subagent limits, applied whenever the
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
* limits when the plain tool is needed again).
|
|
63
|
+
* Default subagent limits, applied whenever the entry config cannot be read.
|
|
64
|
+
* 4 concurrent children and 75 rounds per child are the documented
|
|
65
|
+
* out-of-the-box behavior (75 rounds = 75 LLM round-trips, headroom for heavy
|
|
66
|
+
* delegated tasks while still capping a runaway child); the native `subagent`
|
|
67
|
+
* tool is disabled by default — the TUI's user delegates through registered
|
|
68
|
+
* agents (toggle it in /agents → l limits when the plain tool is needed again).
|
|
58
69
|
*/
|
|
59
70
|
export const DEFAULT_SUBAGENT_LIMITS = Object.freeze({
|
|
60
71
|
maxAgents: 4,
|
|
@@ -63,41 +74,66 @@ export const DEFAULT_SUBAGENT_LIMITS = Object.freeze({
|
|
|
63
74
|
disableSubagent: true,
|
|
64
75
|
registeredOnly: false,
|
|
65
76
|
});
|
|
66
|
-
/**
|
|
67
|
-
|
|
77
|
+
/**
|
|
78
|
+
* The `dsh-tui` entry Config schema — the whole settings surface. Exported
|
|
79
|
+
* from the plugin root (src/index.ts re-exports it): the loader reads
|
|
80
|
+
* `plugin.Config` off the module namespace and validates + resolves the
|
|
81
|
+
* entry's config against it; every field is `.volatile()`, so the settings
|
|
82
|
+
* browser lists all of them and a legacy settings.yaml `dsh-tui:` section
|
|
83
|
+
* imports wholesale (an import carrying any non-volatile field would be
|
|
84
|
+
* rejected as a whole).
|
|
85
|
+
*
|
|
86
|
+
* Plain z.number() (not z.natural()) inside `retention`/`resume`/`askUser` on
|
|
87
|
+
* purpose, same as the 0.1.5 schema: a range-constrained field lets one
|
|
88
|
+
* hand-edited out-of-range number get the whole volatile-only update refused
|
|
89
|
+
* (the write is rejected and the raw value still lands on disk — see the
|
|
90
|
+
* dsh 0.1.7 "not volatile / validation" semantics), so the per-field range
|
|
91
|
+
* check happens in the readers (resolveRetentionConfig / resolveResumeConfig /
|
|
92
|
+
* resolveAskUserTimeouts), which fall back to env/defaults with one stderr
|
|
93
|
+
* line instead.
|
|
94
|
+
*/
|
|
95
|
+
export const Config = z.object({
|
|
68
96
|
language: z
|
|
69
97
|
.string()
|
|
70
98
|
.default(DEFAULT_LANGUAGE)
|
|
99
|
+
.volatile()
|
|
71
100
|
.description(t('settings.language.description')),
|
|
72
101
|
theme: z
|
|
73
102
|
.string()
|
|
74
103
|
.default('auto')
|
|
104
|
+
.volatile()
|
|
75
105
|
.description(t('settings.theme.description')),
|
|
76
106
|
panelHeight: z
|
|
77
107
|
.union(['1', '5', '7', '10', 'all'])
|
|
78
108
|
.default(DEFAULT_PANEL_HEIGHT)
|
|
109
|
+
.volatile()
|
|
79
110
|
.description(t('settings.panelHeight.description')),
|
|
80
111
|
// `z.natural()` is schemastery's constraint for a non-negative integer
|
|
81
112
|
// (the `z.number().int().min(0)` intent — no `.int()` chain exists here).
|
|
82
113
|
maxAgents: z
|
|
83
114
|
.natural()
|
|
84
115
|
.default(DEFAULT_SUBAGENT_LIMITS.maxAgents)
|
|
116
|
+
.volatile()
|
|
85
117
|
.description(t('settings.maxAgents.description')),
|
|
86
118
|
maxRounds: z
|
|
87
119
|
.natural()
|
|
88
120
|
.default(DEFAULT_SUBAGENT_LIMITS.maxRounds)
|
|
121
|
+
.volatile()
|
|
89
122
|
.description(t('settings.maxRounds.description')),
|
|
90
123
|
maxRoundsGrace: z
|
|
91
124
|
.natural()
|
|
92
125
|
.default(DEFAULT_SUBAGENT_LIMITS.maxRoundsGrace)
|
|
126
|
+
.volatile()
|
|
93
127
|
.description(t('settings.maxRoundsGrace.description')),
|
|
94
128
|
disableSubagent: z
|
|
95
129
|
.boolean()
|
|
96
130
|
.default(DEFAULT_SUBAGENT_LIMITS.disableSubagent)
|
|
131
|
+
.volatile()
|
|
97
132
|
.description(t('settings.disableSubagent.description')),
|
|
98
133
|
registeredOnly: z
|
|
99
134
|
.boolean()
|
|
100
135
|
.default(DEFAULT_SUBAGENT_LIMITS.registeredOnly)
|
|
136
|
+
.volatile()
|
|
101
137
|
.description(t('settings.registeredOnly.description')),
|
|
102
138
|
footerHints: z
|
|
103
139
|
.object({
|
|
@@ -110,34 +146,33 @@ const THEME_SETTINGS_SCHEMA = z.object({
|
|
|
110
146
|
history: z.boolean().default(true).description(t('settings.footerHints.history.description')),
|
|
111
147
|
})
|
|
112
148
|
.default({ ...DEFAULT_FOOTER_HINTS })
|
|
149
|
+
.volatile()
|
|
113
150
|
.description(t('settings.footerHints.description')),
|
|
114
151
|
cacheHitMode: z
|
|
115
152
|
.union(['lastMessage', 'session'])
|
|
116
153
|
.default(DEFAULT_CACHE_HIT_MODE)
|
|
154
|
+
.volatile()
|
|
117
155
|
.description(t('settings.cacheHitMode.description')),
|
|
118
156
|
iconSet: z
|
|
119
157
|
.union(['auto', 'nerdfont', 'plain'])
|
|
120
158
|
.default('auto')
|
|
159
|
+
.volatile()
|
|
121
160
|
.description(t('settings.iconSet.description')),
|
|
122
161
|
rememberPreset: z
|
|
123
162
|
.boolean()
|
|
124
163
|
.default(true)
|
|
164
|
+
.volatile()
|
|
125
165
|
.description(t('settings.rememberPreset.description')),
|
|
126
166
|
favoriteModels: z
|
|
127
167
|
.array(z.string())
|
|
128
168
|
.default([])
|
|
169
|
+
.volatile()
|
|
129
170
|
.description(t('settings.favoriteModels.description')),
|
|
130
171
|
hiddenModels: z
|
|
131
172
|
.array(z.string())
|
|
132
173
|
.default([])
|
|
174
|
+
.volatile()
|
|
133
175
|
.description(t('settings.hiddenModels.description')),
|
|
134
|
-
// Plain z.number() (not z.natural()) on purpose: the settings service
|
|
135
|
-
// validates the stored section against this schema at registration and
|
|
136
|
-
// fails LOUD, so a range-constrained schema would let one hand-edited
|
|
137
|
-
// out-of-range number take the whole dsh-tui namespace (theme, panel
|
|
138
|
-
// height, everything) down with it. The per-field range check happens in
|
|
139
|
-
// the readers (resolveRetentionConfig / resolveResumeConfig), which fall
|
|
140
|
-
// back to env/defaults with one stderr line instead.
|
|
141
176
|
retention: z
|
|
142
177
|
.object({
|
|
143
178
|
maxCount: z
|
|
@@ -158,6 +193,7 @@ const THEME_SETTINGS_SCHEMA = z.object({
|
|
|
158
193
|
maxAgeDays: RETENTION_MAX_AGE_DAYS,
|
|
159
194
|
minIdleHours: RETENTION_MIN_IDLE_HOURS,
|
|
160
195
|
})
|
|
196
|
+
.volatile()
|
|
161
197
|
.description(t('settings.retention.description')),
|
|
162
198
|
resume: z
|
|
163
199
|
.object({
|
|
@@ -171,6 +207,7 @@ const THEME_SETTINGS_SCHEMA = z.object({
|
|
|
171
207
|
.description(t('settings.resume.minBytes.description')),
|
|
172
208
|
})
|
|
173
209
|
.default({ maxAgeDays: RESUME_MAX_AGE_DAYS, minBytes: RESUME_MIN_BYTES })
|
|
210
|
+
.volatile()
|
|
174
211
|
.description(t('settings.resume.description')),
|
|
175
212
|
askUser: z
|
|
176
213
|
.object({
|
|
@@ -184,101 +221,34 @@ const THEME_SETTINGS_SCHEMA = z.object({
|
|
|
184
221
|
.description(t('settings.askUser.absoluteMinutes.description')),
|
|
185
222
|
})
|
|
186
223
|
.default({ idleMinutes: ASK_USER_IDLE_MINUTES_DEFAULT, absoluteMinutes: ASK_USER_ABSOLUTE_MINUTES_DEFAULT })
|
|
224
|
+
.volatile()
|
|
187
225
|
.description(t('settings.askUser.description')),
|
|
188
226
|
});
|
|
189
|
-
/** Composition entry below the user layer: fall back to the defaults. */
|
|
190
|
-
const THEME_SETTINGS_ENTRY = {
|
|
191
|
-
language: DEFAULT_LANGUAGE,
|
|
192
|
-
theme: 'auto',
|
|
193
|
-
panelHeight: DEFAULT_PANEL_HEIGHT,
|
|
194
|
-
maxAgents: DEFAULT_SUBAGENT_LIMITS.maxAgents,
|
|
195
|
-
maxRounds: DEFAULT_SUBAGENT_LIMITS.maxRounds,
|
|
196
|
-
maxRoundsGrace: DEFAULT_SUBAGENT_LIMITS.maxRoundsGrace,
|
|
197
|
-
disableSubagent: DEFAULT_SUBAGENT_LIMITS.disableSubagent,
|
|
198
|
-
registeredOnly: DEFAULT_SUBAGENT_LIMITS.registeredOnly,
|
|
199
|
-
footerHints: { ...DEFAULT_FOOTER_HINTS },
|
|
200
|
-
cacheHitMode: DEFAULT_CACHE_HIT_MODE,
|
|
201
|
-
iconSet: 'auto',
|
|
202
|
-
rememberPreset: true,
|
|
203
|
-
favoriteModels: [],
|
|
204
|
-
hiddenModels: [],
|
|
205
|
-
retention: {
|
|
206
|
-
maxCount: RETENTION_MAX_COUNT,
|
|
207
|
-
maxAgeDays: RETENTION_MAX_AGE_DAYS,
|
|
208
|
-
minIdleHours: RETENTION_MIN_IDLE_HOURS,
|
|
209
|
-
},
|
|
210
|
-
resume: { maxAgeDays: RESUME_MAX_AGE_DAYS, minBytes: RESUME_MIN_BYTES },
|
|
211
|
-
askUser: { idleMinutes: ASK_USER_IDLE_MINUTES_DEFAULT, absoluteMinutes: ASK_USER_ABSOLUTE_MINUTES_DEFAULT },
|
|
212
|
-
};
|
|
213
227
|
/**
|
|
214
|
-
*
|
|
215
|
-
*
|
|
216
|
-
*
|
|
217
|
-
* (bounded) before describing. `undefined` until the first registration.
|
|
228
|
+
* The schema's own defaults, resolved ONCE as volatile references — the
|
|
229
|
+
* fallback for every reader while no entry config has been bound (a test
|
|
230
|
+
* driving `apply(ctx)` without a loader, or a read before `apply` ran).
|
|
218
231
|
*/
|
|
219
|
-
|
|
232
|
+
const DEFAULT_TUI_SETTINGS = Config({});
|
|
233
|
+
/** The live entry config handed to `apply` by the loader (undefined = not bound yet). */
|
|
234
|
+
let boundConfig;
|
|
220
235
|
/**
|
|
221
|
-
*
|
|
222
|
-
*
|
|
223
|
-
*
|
|
224
|
-
*
|
|
225
|
-
*
|
|
226
|
-
* receives every committed change (including this TUI's own writes) through
|
|
227
|
-
* the scope's watch hook; callers guard re-applies by theme-bundle identity
|
|
228
|
-
* and height change, so an echoed self-write is a no-op. No source thunk is
|
|
229
|
-
* needed — the read helpers read the resolved values on demand at TUI
|
|
230
|
-
* startup.
|
|
231
|
-
*
|
|
232
|
-
* @param ctx - plugin context; does nothing while no settings service is mounted.
|
|
233
|
-
* @param onPreferenceChange - hot-reload sink for committed `dsh-tui` theme,
|
|
234
|
-
* panel-height, footer-hints, icon-set and language changes; `undefined` when the
|
|
235
|
-
* namespace is already registered (a reloaded plugin instance, a second mount
|
|
236
|
-
* of this bundle) or registration fails.
|
|
236
|
+
* Bind the loader-resolved entry config (the `config` parameter of
|
|
237
|
+
* `apply(ctx, config)`). Called once per plugin application; a `/reload`
|
|
238
|
+
* re-runs `apply` and re-binds. Volatile-only commits later swap the values
|
|
239
|
+
* behind the SAME references in place, so the bound object stays current
|
|
240
|
+
* without rebinding.
|
|
237
241
|
*/
|
|
238
|
-
export function
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
}
|
|
249
|
-
const scope = sctx.settings.register(THEME_SETTINGS_NAMESPACE, THEME_SETTINGS_SCHEMA, {
|
|
250
|
-
base: THEME_SETTINGS_ENTRY,
|
|
251
|
-
// 'live': a committed change takes effect immediately — the TUI
|
|
252
|
-
// hot-applies the theme bundle and the panel height via the watch
|
|
253
|
-
// hook below. 'restart' was the old contract, when every component
|
|
254
|
-
// baked its theme at startup.
|
|
255
|
-
applies: 'live',
|
|
256
|
-
});
|
|
257
|
-
if (onPreferenceChange !== undefined) {
|
|
258
|
-
scope.watch((next) => {
|
|
259
|
-
// The resolved section is `{ language: ..., theme: ...,
|
|
260
|
-
// panelHeight: ..., footerHints: {...}, iconSet: ... }` — narrow
|
|
261
|
-
// the unknown to the observed fields.
|
|
262
|
-
const section = next;
|
|
263
|
-
const theme = section.theme;
|
|
264
|
-
const panelHeight = section.panelHeight;
|
|
265
|
-
onPreferenceChange(typeof theme === 'string' && theme !== '' ? theme : 'auto', isPanelHeight(panelHeight) ? panelHeight : DEFAULT_PANEL_HEIGHT, narrowFooterHints(section.footerHints), narrowIconSet(section.iconSet), narrowLanguage(section.language));
|
|
266
|
-
});
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
catch (error) {
|
|
270
|
-
// TUI startup awaits `registrationPromise` — it must settle no matter
|
|
271
|
-
// what, so a failed registration degrades to 'auto' instead of
|
|
272
|
-
// hanging. Leave a trace for the operator: this fires during
|
|
273
|
-
// apply(), before the TUI's notice sink exists, so the message goes
|
|
274
|
-
// through the shared bridge and surfaces above the footer once the
|
|
275
|
-
// first frame lands (never raw stderr — the alt-screen owns the
|
|
276
|
-
// terminal by then).
|
|
277
|
-
emitNotice(`settings namespace registration failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
278
|
-
}
|
|
279
|
-
resolve();
|
|
280
|
-
});
|
|
281
|
-
});
|
|
242
|
+
export function bindTuiConfig(config) {
|
|
243
|
+
boundConfig = config;
|
|
244
|
+
}
|
|
245
|
+
/** The config to read: the bound entry config, else the schema defaults. */
|
|
246
|
+
function tuiConfig() {
|
|
247
|
+
return boundConfig ?? DEFAULT_TUI_SETTINGS;
|
|
248
|
+
}
|
|
249
|
+
/** Resolve an export of the Config schema into a `TuiSettings`-shaped object (test helper). */
|
|
250
|
+
export function resolveTuiSettings(overrides = {}) {
|
|
251
|
+
return Config(overrides);
|
|
282
252
|
}
|
|
283
253
|
/** Validate an unknown `footerHints` value into the typed shape (defaults win). */
|
|
284
254
|
function narrowFooterHints(value) {
|
|
@@ -304,150 +274,112 @@ export function narrowLanguage(value) {
|
|
|
304
274
|
function narrowRememberPreset(value) {
|
|
305
275
|
return typeof value === 'boolean' ? value : true;
|
|
306
276
|
}
|
|
307
|
-
/**
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
*
|
|
311
|
-
* The registration is delivered through the settings injection fiber, so the
|
|
312
|
-
* value may not be visible synchronously right after `registerThemeSettings`:
|
|
313
|
-
* the settings service mounts asynchronously (its init sets up the provider,
|
|
314
|
-
* a file watcher, ...), a tick after the registration request in the dsh
|
|
315
|
-
* profile. Wait for the registration to land before describing — bounded, so
|
|
316
|
-
* a settings-less deployment degrades to the defaults instead of hanging TUI
|
|
317
|
-
* startup. Without a registration request there is nothing to wait for.
|
|
318
|
-
*
|
|
319
|
-
* @returns the descriptor, or `undefined` when no registration is in
|
|
320
|
-
* flight, the settings service is absent, or the namespace has not landed.
|
|
321
|
-
*/
|
|
322
|
-
async function registeredDescriptor(ctx) {
|
|
323
|
-
if (registrationPromise === undefined)
|
|
324
|
-
return undefined;
|
|
325
|
-
let fallback;
|
|
326
|
-
await Promise.race([
|
|
327
|
-
registrationPromise,
|
|
328
|
-
new Promise(resolve => { fallback = setTimeout(resolve, 2000); }),
|
|
329
|
-
]);
|
|
330
|
-
if (fallback !== undefined)
|
|
331
|
-
clearTimeout(fallback);
|
|
332
|
-
const settings = ctx.get('settings');
|
|
333
|
-
if (settings === undefined)
|
|
334
|
-
return undefined;
|
|
335
|
-
return settings.describe().find((descriptor) => descriptor.ns === THEME_SETTINGS_NAMESPACE);
|
|
277
|
+
/** Validate an unknown `theme` value (custom theme names pass through). */
|
|
278
|
+
function narrowTheme(value) {
|
|
279
|
+
return typeof value === 'string' && value !== '' ? value : 'auto';
|
|
336
280
|
}
|
|
337
281
|
/**
|
|
338
|
-
*
|
|
339
|
-
*
|
|
282
|
+
* Subscribe the hot-reload sink to committed `dsh-tui` changes — the 0.1.7
|
|
283
|
+
* replacement of the 0.1.5 watch hook (the runtime registration call itself
|
|
284
|
+
* is gone: the static Config schema declares the entry config, the loader
|
|
285
|
+
* owns the projection).
|
|
340
286
|
*
|
|
341
|
-
*
|
|
342
|
-
*
|
|
287
|
+
* `settings/document-updated` carries `(ns, revision)` and fires whenever the
|
|
288
|
+
* entry's raw config changed — the /theme picker, the /settings browser, an
|
|
289
|
+
* external patch edit, or this TUI's own write (the echo). The handler
|
|
290
|
+
* re-reads the live volatile references (the loader updated them in place
|
|
291
|
+
* before the event) and forwards the narrowed bundle to the sink; callers
|
|
292
|
+
* guard re-applies by theme-bundle identity and height change, so an echoed
|
|
293
|
+
* self-write is a no-op. The whole commit → references → event → `.get()`
|
|
294
|
+
* → sink chain is synchronous per event.
|
|
295
|
+
*
|
|
296
|
+
* @param ctx - plugin context; the subscription dies with the plugin fiber.
|
|
297
|
+
* @param onPreferenceChange - hot-reload sink for committed `dsh-tui` theme,
|
|
298
|
+
* panel-height, footer-hints, icon-set and language changes; `undefined`
|
|
299
|
+
* registers nothing.
|
|
343
300
|
*/
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
301
|
+
export function subscribeThemeSettings(ctx, onPreferenceChange) {
|
|
302
|
+
if (onPreferenceChange === undefined)
|
|
303
|
+
return;
|
|
304
|
+
ctx.on('settings/document-updated', (ns) => {
|
|
305
|
+
if (ns !== THEME_SETTINGS_NAMESPACE)
|
|
306
|
+
return;
|
|
307
|
+
const section = tuiConfig();
|
|
308
|
+
onPreferenceChange(narrowTheme(section.theme.get()), isPanelHeight(section.panelHeight.get()) ? section.panelHeight.get() : DEFAULT_PANEL_HEIGHT, narrowFooterHints(section.footerHints.get()), narrowIconSet(section.iconSet.get()), narrowLanguage(section.language.get()));
|
|
309
|
+
});
|
|
349
310
|
}
|
|
350
311
|
/**
|
|
351
312
|
* Read the persisted theme preference (the startup snapshot).
|
|
352
313
|
*
|
|
353
|
-
* @
|
|
354
|
-
*
|
|
355
|
-
* service is absent or the namespace/value cannot be read.
|
|
314
|
+
* @returns the live `dsh-tui` theme value, or `'auto'` when the entry config
|
|
315
|
+
* is not bound (a `Config`-less deployment always has the schema default).
|
|
356
316
|
*/
|
|
357
|
-
export
|
|
358
|
-
|
|
359
|
-
if (typeof pref === 'string' && pref !== '')
|
|
360
|
-
return pref;
|
|
361
|
-
return 'auto';
|
|
317
|
+
export function readThemePreference(_ctx) {
|
|
318
|
+
return narrowTheme(tuiConfig().theme.get());
|
|
362
319
|
}
|
|
363
320
|
/**
|
|
364
321
|
* Read the persisted think/tool panel height (the startup snapshot).
|
|
365
322
|
*
|
|
366
|
-
* @
|
|
367
|
-
*
|
|
368
|
-
* when the settings service is absent or the namespace/value cannot be read.
|
|
323
|
+
* @returns the live `dsh-tui` panelHeight value, or DEFAULT_PANEL_HEIGHT when
|
|
324
|
+
* the entry config is not bound.
|
|
369
325
|
*/
|
|
370
|
-
export
|
|
371
|
-
const height = (
|
|
372
|
-
|
|
373
|
-
return height;
|
|
374
|
-
return DEFAULT_PANEL_HEIGHT;
|
|
326
|
+
export function readPanelHeightPreference(_ctx) {
|
|
327
|
+
const height = tuiConfig().panelHeight.get();
|
|
328
|
+
return isPanelHeight(height) ? height : DEFAULT_PANEL_HEIGHT;
|
|
375
329
|
}
|
|
376
330
|
/**
|
|
377
331
|
* Read the persisted footer-hint selection (the startup snapshot).
|
|
378
332
|
*
|
|
379
|
-
* @
|
|
380
|
-
*
|
|
381
|
-
* when the settings service is absent or the namespace/value cannot be read.
|
|
333
|
+
* @returns the live `dsh-tui` footerHints object, or DEFAULT_FOOTER_HINTS
|
|
334
|
+
* when the entry config is not bound.
|
|
382
335
|
*/
|
|
383
|
-
export
|
|
384
|
-
return narrowFooterHints((
|
|
336
|
+
export function readFooterHintsPreference(_ctx) {
|
|
337
|
+
return narrowFooterHints(tuiConfig().footerHints.get());
|
|
385
338
|
}
|
|
386
339
|
/**
|
|
387
340
|
* Read the persisted icon-set mode (the startup snapshot).
|
|
388
341
|
*
|
|
389
|
-
* @
|
|
390
|
-
*
|
|
391
|
-
* service is absent or the namespace/value cannot be read.
|
|
342
|
+
* @returns the live `dsh-tui` iconSet value, or `'auto'` when the entry
|
|
343
|
+
* config is not bound.
|
|
392
344
|
*/
|
|
393
|
-
export
|
|
394
|
-
return narrowIconSet((
|
|
345
|
+
export function readIconSetPreference(_ctx) {
|
|
346
|
+
return narrowIconSet(tuiConfig().iconSet.get());
|
|
395
347
|
}
|
|
396
348
|
/**
|
|
397
349
|
* Read the persisted UI language (the startup snapshot).
|
|
398
350
|
*
|
|
399
|
-
* @
|
|
400
|
-
*
|
|
401
|
-
*
|
|
402
|
-
* Whether the id is actually installed is decided by the i18n registry
|
|
403
|
-
* (initI18n degrades unknown ids to 'en').
|
|
351
|
+
* @returns the live `dsh-tui` language value, or DEFAULT_LANGUAGE ('en') when
|
|
352
|
+
* the entry config is not bound. Whether the id is actually installed is
|
|
353
|
+
* decided by the i18n registry (initI18n degrades unknown ids to 'en').
|
|
404
354
|
*/
|
|
405
|
-
export
|
|
406
|
-
return narrowLanguage((
|
|
355
|
+
export function readLanguagePreference(_ctx) {
|
|
356
|
+
return narrowLanguage(tuiConfig().language.get());
|
|
407
357
|
}
|
|
408
358
|
/**
|
|
409
359
|
* Read the persisted preset-memory toggle (the startup snapshot). Default
|
|
410
360
|
* TRUE: remembering is the out-of-the-box behavior; only an explicit
|
|
411
|
-
* `
|
|
361
|
+
* `rememberPreset: false` turns it off.
|
|
412
362
|
*
|
|
413
|
-
* @
|
|
414
|
-
* @returns the resolved boolean, or `true` when the settings service is
|
|
415
|
-
* absent or the namespace/value cannot be read.
|
|
416
|
-
*/
|
|
417
|
-
export async function readRememberPreset(ctx) {
|
|
418
|
-
return narrowRememberPreset((await readResolvedSection(ctx))?.rememberPreset);
|
|
419
|
-
}
|
|
420
|
-
/**
|
|
421
|
-
* Read the currently persisted preset-memory toggle, synchronously. Unlike
|
|
422
|
-
* `readRememberPreset` (the startup snapshot), this describes whatever the
|
|
423
|
-
* settings service exposes right now — the /preset switch path calls it at
|
|
424
|
-
* every commit, so a `/settings` toggle applies without a restart.
|
|
425
|
-
* @returns `true` when the service, namespace, or value is absent.
|
|
363
|
+
* @returns the live boolean, or `true` when the entry config is not bound.
|
|
426
364
|
*/
|
|
427
|
-
export function
|
|
428
|
-
|
|
429
|
-
if (settings === undefined)
|
|
430
|
-
return true;
|
|
431
|
-
return narrowRememberPreset(settings
|
|
432
|
-
.describe()
|
|
433
|
-
.find((descriptor) => descriptor.ns === THEME_SETTINGS_NAMESPACE)?.value?.rememberPreset);
|
|
365
|
+
export function readRememberPreset(_ctx) {
|
|
366
|
+
return narrowRememberPreset(tuiConfig().rememberPreset.get());
|
|
434
367
|
}
|
|
435
368
|
/**
|
|
436
|
-
* Read the explicit `dsh-tui.retention` / `dsh-tui.resume` sections from
|
|
437
|
-
*
|
|
438
|
-
* Awaits the namespace registration bounded (same plumbing as the theme
|
|
439
|
-
* readers), so the startup retention pass can call it without hanging a
|
|
440
|
-
* settings-less deployment.
|
|
369
|
+
* Read the explicit `dsh-tui.retention` / `dsh-tui.resume` sections from the
|
|
370
|
+
* settings document's user layer (see `SessionManagementExplicit`).
|
|
441
371
|
*
|
|
372
|
+
* @param ctx - plugin context.
|
|
442
373
|
* @returns ALWAYS the two-key shape — a section absent from the user
|
|
443
|
-
* layer (or the whole service/
|
|
374
|
+
* layer (or the whole service/entry missing) reads as
|
|
444
375
|
* `{ retention: undefined, resume: undefined }`, never a bare
|
|
445
376
|
* `undefined`, so callers destructure one stable shape. "Nothing
|
|
446
377
|
* explicitly configured" (env/defaults govern) and "nothing to read at
|
|
447
378
|
* all" are the same outcome for every consumer.
|
|
448
379
|
*/
|
|
449
|
-
export
|
|
450
|
-
const
|
|
380
|
+
export function readSessionManagementExplicit(ctx) {
|
|
381
|
+
const settings = ctx.get('settings');
|
|
382
|
+
const user = settings?.describe().find((descriptor) => descriptor.ns === THEME_SETTINGS_NAMESPACE)?.user;
|
|
451
383
|
if (user === null || typeof user !== 'object') {
|
|
452
384
|
return { retention: undefined, resume: undefined };
|
|
453
385
|
}
|
|
@@ -465,14 +397,13 @@ export async function readSessionManagementExplicit(ctx) {
|
|
|
465
397
|
}
|
|
466
398
|
/**
|
|
467
399
|
* Read the explicit `dsh-tui.askUser` section from the settings document's
|
|
468
|
-
* user layer.
|
|
469
|
-
*
|
|
470
|
-
*
|
|
471
|
-
* section, the user layer, the namespace, or the whole service is absent —
|
|
472
|
-
* "nothing explicitly configured" for the ask-user resolver.
|
|
400
|
+
* user layer. Returns `undefined` when the section, the user layer, the
|
|
401
|
+
* entry, or the whole service is absent — "nothing explicitly configured"
|
|
402
|
+
* for the ask-user resolver.
|
|
473
403
|
*/
|
|
474
|
-
export
|
|
475
|
-
const
|
|
404
|
+
export function readAskUserExplicit(ctx) {
|
|
405
|
+
const settings = ctx.get('settings');
|
|
406
|
+
const user = settings?.describe().find((descriptor) => descriptor.ns === THEME_SETTINGS_NAMESPACE)?.user;
|
|
476
407
|
if (user === null || typeof user !== 'object')
|
|
477
408
|
return undefined;
|
|
478
409
|
const section = user.askUser;
|
|
@@ -481,74 +412,58 @@ export async function readAskUserExplicit(ctx) {
|
|
|
481
412
|
: undefined;
|
|
482
413
|
}
|
|
483
414
|
/**
|
|
484
|
-
* Read the currently persisted footer-hint selection, synchronously
|
|
485
|
-
*
|
|
486
|
-
*
|
|
487
|
-
* service exposes right now, so a caller can honor a live change immediately.
|
|
488
|
-
* @returns DEFAULT_FOOTER_HINTS when the service, namespace, or value is absent.
|
|
415
|
+
* Read the currently persisted footer-hint selection, synchronously — the
|
|
416
|
+
* live volatile reference, so a committed change applies on the next read.
|
|
417
|
+
* @returns DEFAULT_FOOTER_HINTS when the entry config is not bound.
|
|
489
418
|
*/
|
|
490
|
-
export function currentFooterHints(
|
|
491
|
-
|
|
492
|
-
if (settings === undefined)
|
|
493
|
-
return { ...DEFAULT_FOOTER_HINTS };
|
|
494
|
-
const hints = settings
|
|
495
|
-
.describe()
|
|
496
|
-
.find((descriptor) => descriptor.ns === THEME_SETTINGS_NAMESPACE)?.value?.footerHints;
|
|
497
|
-
return narrowFooterHints(hints);
|
|
419
|
+
export function currentFooterHints(_ctx) {
|
|
420
|
+
return narrowFooterHints(tuiConfig().footerHints.get());
|
|
498
421
|
}
|
|
499
422
|
/**
|
|
500
|
-
* Read the currently persisted footer CH mode, synchronously
|
|
501
|
-
*
|
|
502
|
-
*
|
|
503
|
-
* (the /settings browser, an external edit) applies on the next repaint.
|
|
504
|
-
* @returns DEFAULT_CACHE_HIT_MODE when the service, namespace, or value is absent.
|
|
423
|
+
* Read the currently persisted footer CH mode, synchronously — the footer
|
|
424
|
+
* calls it on every render, so a committed change applies on the next repaint.
|
|
425
|
+
* @returns DEFAULT_CACHE_HIT_MODE when the entry config is not bound.
|
|
505
426
|
*/
|
|
506
|
-
export function currentCacheHitMode(
|
|
507
|
-
|
|
508
|
-
if (settings === undefined)
|
|
509
|
-
return DEFAULT_CACHE_HIT_MODE;
|
|
510
|
-
const mode = settings
|
|
511
|
-
.describe()
|
|
512
|
-
.find((descriptor) => descriptor.ns === THEME_SETTINGS_NAMESPACE)?.value?.cacheHitMode;
|
|
513
|
-
return narrowCacheHitMode(mode);
|
|
427
|
+
export function currentCacheHitMode(_ctx) {
|
|
428
|
+
return narrowCacheHitMode(tuiConfig().cacheHitMode.get());
|
|
514
429
|
}
|
|
515
430
|
/**
|
|
516
|
-
* Read the currently persisted theme preference, synchronously
|
|
517
|
-
*
|
|
518
|
-
*
|
|
519
|
-
* exposes right now, so the `/theme` picker preselects the live value, which
|
|
520
|
-
* may have changed since startup (e.g. through the /settings browser).
|
|
521
|
-
* @returns 'auto' when the service, namespace, or value cannot be read.
|
|
431
|
+
* Read the currently persisted theme preference, synchronously — the /theme
|
|
432
|
+
* picker preselects the live value.
|
|
433
|
+
* @returns 'auto' when the entry config is not bound.
|
|
522
434
|
*/
|
|
523
|
-
export function currentThemePreference(
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
435
|
+
export function currentThemePreference(_ctx) {
|
|
436
|
+
return narrowTheme(tuiConfig().theme.get());
|
|
437
|
+
}
|
|
438
|
+
/**
|
|
439
|
+
* Read the currently persisted preset-memory toggle, synchronously — the
|
|
440
|
+
* /preset switch path calls it at every commit, so a `/settings` toggle
|
|
441
|
+
* applies immediately.
|
|
442
|
+
* @returns `true` when the entry config is not bound.
|
|
443
|
+
*/
|
|
444
|
+
export function currentRememberPreset(_ctx) {
|
|
445
|
+
return narrowRememberPreset(tuiConfig().rememberPreset.get());
|
|
533
446
|
}
|
|
534
447
|
/**
|
|
535
448
|
* Persist one `dsh-tui` preference (theme, panelHeight, or a subagent limit)
|
|
536
|
-
*
|
|
537
|
-
*
|
|
538
|
-
*
|
|
539
|
-
*
|
|
540
|
-
*
|
|
541
|
-
*
|
|
542
|
-
*
|
|
449
|
+
* through `settings.mutate` (the 0.1.7 spell of the same 0.1.5 write — the
|
|
450
|
+
* signature is unchanged). The write is volatile-only, so the loader commits
|
|
451
|
+
* it into the running references without remounting the plugin, and the
|
|
452
|
+
* `settings/document-updated` event hot-applies it to the TUI. Best-effort:
|
|
453
|
+
* a deployment without the settings service reports the failure; a failed
|
|
454
|
+
* write returns its error message for the caller to surface. A concurrent
|
|
455
|
+
* writer moving the entry rejects with `SettingsConflictError` — retried
|
|
456
|
+
* once against a fresh revision; a second conflict surfaces a friendly
|
|
457
|
+
* message instead of the raw error.
|
|
543
458
|
* @returns undefined on success, the failure message otherwise.
|
|
544
459
|
*/
|
|
545
460
|
async function writeDshTuiPreference(ctx, key, value) {
|
|
546
461
|
const settings = ctx.get('settings');
|
|
547
462
|
if (settings === undefined)
|
|
548
463
|
return 'Settings service is not available.';
|
|
549
|
-
// The descriptor carries the
|
|
550
|
-
// token for mutate) and proves the
|
|
551
|
-
//
|
|
464
|
+
// The descriptor carries the entry's revision (optimistic-concurrency
|
|
465
|
+
// token for mutate) and proves the entry is live; the write rejects when
|
|
466
|
+
// it is not.
|
|
552
467
|
const ops = [{ op: 'set', path: [key], value }];
|
|
553
468
|
for (let attempt = 0;; attempt++) {
|
|
554
469
|
const descriptor = settings.describe().find((d) => d.ns === THEME_SETTINGS_NAMESPACE);
|
|
@@ -566,86 +481,93 @@ async function writeDshTuiPreference(ctx, key, value) {
|
|
|
566
481
|
}
|
|
567
482
|
}
|
|
568
483
|
/**
|
|
569
|
-
* Persist the theme preference to the `dsh-tui`
|
|
570
|
-
*
|
|
571
|
-
*
|
|
484
|
+
* Persist the theme preference to the `dsh-tui` entry config. Volatile-only
|
|
485
|
+
* write: the commit hot-applies to the running TUI through the
|
|
486
|
+
* `settings/document-updated` subscription.
|
|
572
487
|
* @returns undefined on success, the failure message otherwise.
|
|
573
488
|
*/
|
|
574
489
|
export async function writeThemePreference(ctx, pref) {
|
|
575
490
|
return writeDshTuiPreference(ctx, 'theme', pref);
|
|
576
491
|
}
|
|
577
492
|
/**
|
|
578
|
-
* Persist the UI language to the `dsh-tui`
|
|
579
|
-
*
|
|
580
|
-
* watch hook) hot-applies the language to the running TUI.
|
|
493
|
+
* Persist the UI language to the `dsh-tui` entry config. Volatile-only
|
|
494
|
+
* write: the commit hot-applies to the running TUI.
|
|
581
495
|
* @returns undefined on success, the failure message otherwise.
|
|
582
496
|
*/
|
|
583
497
|
export async function writeLanguagePreference(ctx, id) {
|
|
584
498
|
return writeDshTuiPreference(ctx, 'language', id);
|
|
585
499
|
}
|
|
586
500
|
/**
|
|
587
|
-
* Read the currently resolved subagent limits, synchronously
|
|
588
|
-
*
|
|
589
|
-
*
|
|
590
|
-
*
|
|
591
|
-
*
|
|
592
|
-
*
|
|
593
|
-
* service or namespace, or a non-integer/negative field, degrades to the
|
|
594
|
-
* defaults — a settings-less deployment keeps the documented caps.
|
|
501
|
+
* Read the currently resolved subagent limits, synchronously — the live
|
|
502
|
+
* volatile references, so every policy decision (the guard at each spawn,
|
|
503
|
+
* `onRoundCount` at each child assistant message) reflects the latest
|
|
504
|
+
* committed value without a watcher. An unbound entry config or a
|
|
505
|
+
* non-integer/negative field degrades to the defaults — a config-less
|
|
506
|
+
* deployment keeps the documented caps.
|
|
595
507
|
*/
|
|
596
|
-
export function readSubagentLimits(
|
|
597
|
-
const
|
|
598
|
-
if (settings === undefined)
|
|
599
|
-
return { ...DEFAULT_SUBAGENT_LIMITS };
|
|
600
|
-
// The descriptor's `value` is the whole resolved section
|
|
601
|
-
// (`{ theme: ..., panelHeight: ..., maxAgents: ..., maxRounds: ...,
|
|
602
|
-
// disableSubagent: ... }`) — narrow the unknown to the observed fields.
|
|
603
|
-
const section = settings
|
|
604
|
-
.describe()
|
|
605
|
-
.find((descriptor) => descriptor.ns === THEME_SETTINGS_NAMESPACE)?.value;
|
|
508
|
+
export function readSubagentLimits(_ctx) {
|
|
509
|
+
const section = tuiConfig();
|
|
606
510
|
const natural = (value, fallback) => typeof value === 'number' && Number.isInteger(value) && value >= 0 ? value : fallback;
|
|
607
511
|
return {
|
|
608
|
-
maxAgents: natural(section
|
|
609
|
-
maxRounds: natural(section
|
|
610
|
-
maxRoundsGrace: natural(section
|
|
611
|
-
disableSubagent: typeof section
|
|
612
|
-
? section.disableSubagent
|
|
512
|
+
maxAgents: natural(section.maxAgents.get(), DEFAULT_SUBAGENT_LIMITS.maxAgents),
|
|
513
|
+
maxRounds: natural(section.maxRounds.get(), DEFAULT_SUBAGENT_LIMITS.maxRounds),
|
|
514
|
+
maxRoundsGrace: natural(section.maxRoundsGrace.get(), DEFAULT_SUBAGENT_LIMITS.maxRoundsGrace),
|
|
515
|
+
disableSubagent: typeof section.disableSubagent.get() === 'boolean'
|
|
516
|
+
? section.disableSubagent.get()
|
|
613
517
|
: DEFAULT_SUBAGENT_LIMITS.disableSubagent,
|
|
614
|
-
registeredOnly: typeof section
|
|
615
|
-
? section.registeredOnly
|
|
518
|
+
registeredOnly: typeof section.registeredOnly.get() === 'boolean'
|
|
519
|
+
? section.registeredOnly.get()
|
|
616
520
|
: DEFAULT_SUBAGENT_LIMITS.registeredOnly,
|
|
617
521
|
};
|
|
618
522
|
}
|
|
523
|
+
/**
|
|
524
|
+
* Read the official `subagent` entry's resolved config through the settings
|
|
525
|
+
* forms descriptor (the only public channel for ANOTHER entry's live
|
|
526
|
+
* values). Best-effort: no settings service, no such entry, or a malformed
|
|
527
|
+
* value reads as `undefined` per field — the panel then annotates the row
|
|
528
|
+
* instead of showing a number it cannot vouch for.
|
|
529
|
+
*/
|
|
530
|
+
export function readOfficialSubagentLimits(ctx) {
|
|
531
|
+
const settings = ctx.get('settings');
|
|
532
|
+
const value = settings?.describe().find((d) => d.ns === 'subagent')?.value;
|
|
533
|
+
const natural = (v) => typeof v === 'number' && Number.isInteger(v) && v >= 0 ? v : undefined;
|
|
534
|
+
if (value === null || typeof value !== 'object') {
|
|
535
|
+
return { maxActiveSubagents: undefined, maxDepth: undefined };
|
|
536
|
+
}
|
|
537
|
+
const section = value;
|
|
538
|
+
return {
|
|
539
|
+
maxActiveSubagents: natural(section.maxActiveSubagents),
|
|
540
|
+
maxDepth: natural(section.maxDepth),
|
|
541
|
+
};
|
|
542
|
+
}
|
|
619
543
|
/**
|
|
620
544
|
* Persist one subagent policy knob (maxAgents, maxRounds, or disableSubagent)
|
|
621
|
-
* to the `dsh-tui`
|
|
622
|
-
*
|
|
623
|
-
*
|
|
624
|
-
*
|
|
625
|
-
* failure message for the caller.
|
|
545
|
+
* to the `dsh-tui` entry config. Volatile-only write: the commit hot-applies
|
|
546
|
+
* without a restart — the policy reads `readSubagentLimits` at the next
|
|
547
|
+
* decision point. Never throws: a deployment without the settings service
|
|
548
|
+
* surfaces a failure message for the caller.
|
|
626
549
|
* @returns undefined on success, the failure message otherwise.
|
|
627
550
|
*/
|
|
628
551
|
export async function writeSubagentLimit(ctx, key, value) {
|
|
629
552
|
return writeDshTuiPreference(ctx, key, value);
|
|
630
553
|
}
|
|
631
554
|
/**
|
|
632
|
-
* Read the persisted model favorites/hiddens
|
|
633
|
-
*
|
|
634
|
-
* degrades to an empty list.
|
|
555
|
+
* Read the persisted model favorites/hiddens. Both lists narrow through
|
|
556
|
+
* `narrowStringList` — a malformed field degrades to an empty list.
|
|
635
557
|
*/
|
|
636
|
-
export
|
|
637
|
-
const section =
|
|
558
|
+
export function readModelPrefs(_ctx) {
|
|
559
|
+
const section = tuiConfig();
|
|
638
560
|
return {
|
|
639
|
-
favoriteModels: narrowStringList(section
|
|
640
|
-
hiddenModels: narrowStringList(section
|
|
561
|
+
favoriteModels: narrowStringList(section.favoriteModels.get()),
|
|
562
|
+
hiddenModels: narrowStringList(section.hiddenModels.get()),
|
|
641
563
|
};
|
|
642
564
|
}
|
|
643
565
|
/**
|
|
644
566
|
* Persist one model pref list (favoriteModels or hiddenModels) to the
|
|
645
|
-
* `dsh-tui`
|
|
646
|
-
*
|
|
647
|
-
*
|
|
648
|
-
* Best-effort: a deployment without the settings
|
|
567
|
+
* `dsh-tui` entry config via `settings.mutate` (optimistic concurrency, one
|
|
568
|
+
* retry on `SettingsConflictError`) — never a whole-file rewrite. The caller
|
|
569
|
+
* invokes this on every f/h toggle, so each press lands immediately.
|
|
570
|
+
* Best-effort: a deployment without the settings service surfaces the
|
|
649
571
|
* failure message; the in-panel state stays session-local either way.
|
|
650
572
|
* @returns undefined on success, the failure message otherwise.
|
|
651
573
|
*/
|