@aiwayds/dsh-tui-pi 2.19.0 → 2.20.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 +12 -48
- package/README.zh-CN.md +11 -47
- package/lib/agents.js +67 -52
- package/lib/agents.js.map +1 -1
- package/lib/ask-user.js +30 -30
- package/lib/ask-user.js.map +1 -1
- package/lib/attachments.js +8 -3
- package/lib/attachments.js.map +1 -1
- package/lib/btw.d.ts +4 -2
- package/lib/btw.js +22 -16
- package/lib/btw.js.map +1 -1
- package/lib/custom-provider.js +34 -28
- package/lib/custom-provider.js.map +1 -1
- package/lib/footer.d.ts +9 -8
- package/lib/footer.js +36 -18
- package/lib/footer.js.map +1 -1
- package/lib/hard-exit.d.ts +36 -0
- package/lib/hard-exit.js +64 -0
- package/lib/hard-exit.js.map +1 -0
- package/lib/history-fork.d.ts +2 -2
- package/lib/history-fork.js +12 -9
- package/lib/history-fork.js.map +1 -1
- package/lib/history-turns.d.ts +4 -2
- package/lib/history-turns.js +7 -3
- package/lib/history-turns.js.map +1 -1
- package/lib/history.js +57 -45
- package/lib/history.js.map +1 -1
- package/lib/hotkeys.js +44 -33
- package/lib/hotkeys.js.map +1 -1
- package/lib/i18n/index.d.ts +54 -0
- package/lib/i18n/index.js +98 -0
- package/lib/i18n/index.js.map +1 -0
- package/lib/i18n/registry.d.ts +65 -0
- package/lib/i18n/registry.js +129 -0
- package/lib/i18n/registry.js.map +1 -0
- package/lib/index.js +216 -108
- package/lib/index.js.map +1 -1
- package/lib/language-ask.d.ts +98 -0
- package/lib/language-ask.js +118 -0
- package/lib/language-ask.js.map +1 -0
- package/lib/live-widgets.js +9 -8
- package/lib/live-widgets.js.map +1 -1
- package/lib/login.js +5 -4
- package/lib/login.js.map +1 -1
- package/lib/messages.d.ts +0 -4
- package/lib/messages.js +14 -15
- package/lib/messages.js.map +1 -1
- package/lib/model-profiles.js +32 -20
- package/lib/model-profiles.js.map +1 -1
- package/lib/panels.js +18 -17
- package/lib/panels.js.map +1 -1
- package/lib/preflight-projcache.js +9 -3
- package/lib/preflight-projcache.js.map +1 -1
- package/lib/preset-dialog.d.ts +3 -3
- package/lib/preset-dialog.js +20 -17
- package/lib/preset-dialog.js.map +1 -1
- package/lib/provider-catalog.d.ts +23 -7
- package/lib/provider-catalog.js +90 -68
- package/lib/provider-catalog.js.map +1 -1
- package/lib/queue-panel.d.ts +2 -2
- package/lib/queue-panel.js +13 -9
- package/lib/queue-panel.js.map +1 -1
- package/lib/repair-dialog.d.ts +4 -4
- package/lib/repair-dialog.js +23 -16
- package/lib/repair-dialog.js.map +1 -1
- package/lib/route-dialog.d.ts +3 -3
- package/lib/route-dialog.js +20 -14
- package/lib/route-dialog.js.map +1 -1
- package/lib/selectors.d.ts +15 -1
- package/lib/selectors.js +57 -23
- package/lib/selectors.js.map +1 -1
- package/lib/sessions.d.ts +47 -14
- package/lib/sessions.js +162 -53
- package/lib/sessions.js.map +1 -1
- package/lib/settings.d.ts +68 -9
- package/lib/settings.js +327 -78
- package/lib/settings.js.map +1 -1
- package/lib/skills-manager.js +27 -21
- package/lib/skills-manager.js.map +1 -1
- package/lib/skills.js +5 -4
- package/lib/skills.js.map +1 -1
- package/lib/startup-info.js +9 -3
- package/lib/startup-info.js.map +1 -1
- package/lib/steer-flow.d.ts +14 -4
- package/lib/steer-flow.js +24 -9
- package/lib/steer-flow.js.map +1 -1
- package/lib/stop-dialog.d.ts +2 -2
- package/lib/stop-dialog.js +16 -12
- package/lib/stop-dialog.js.map +1 -1
- package/lib/subagent-viewer.d.ts +3 -6
- package/lib/subagent-viewer.js +38 -36
- package/lib/subagent-viewer.js.map +1 -1
- package/lib/theme-preview.js +12 -11
- package/lib/theme-preview.js.map +1 -1
- package/lib/theme-settings.d.ts +23 -2
- package/lib/theme-settings.js +68 -61
- package/lib/theme-settings.js.map +1 -1
- package/lib/tui.js +2 -1
- package/lib/tui.js.map +1 -1
- package/locales/en.json +646 -0
- package/locales/ja.json +646 -0
- package/locales/ko.json +646 -0
- package/locales/zh-CN.json +646 -0
- package/package.json +2 -1
- package/skills/dsh-tui-pi-config/SKILL.md +9 -2
package/lib/settings.js
CHANGED
|
@@ -6,8 +6,9 @@
|
|
|
6
6
|
* schemastery schemas → resolved values) and renders it as nested FW list
|
|
7
7
|
* panels (src/panels.ts SettingsListPanel), one level per schema depth:
|
|
8
8
|
*
|
|
9
|
-
* level 0 category list (searchable)
|
|
10
|
-
*
|
|
9
|
+
* level 0 category list (searchable), alphabetical by canonical label:
|
|
10
|
+
* Agent Presets / General / Models / Other / Plugins / TUI,
|
|
11
|
+
* with `other` hidden when no namespace falls into it. Namespace→category comes
|
|
11
12
|
* from a static mapping (categorizeNamespaces) mirroring the web
|
|
12
13
|
* settings page: the web client slots namespaces into categories
|
|
13
14
|
* client-side and the data plane carries no category field, so
|
|
@@ -19,9 +20,13 @@
|
|
|
19
20
|
* model summary, API-key state) instead of the raw namespace —
|
|
20
21
|
* the original schema surface is hidden — plus dedicated
|
|
21
22
|
* llm-deepseek / agent-default-model rows and an add-provider
|
|
22
|
-
* flow (built-in directory → one API key → write)
|
|
23
|
+
* flow (built-in directory → one API key → write). The
|
|
24
|
+
* single-namespace dsh-tui category drills straight through to
|
|
25
|
+
* its field list, titled by the category label.
|
|
23
26
|
* level 2+ schema walk, dispatched on node type:
|
|
24
|
-
* - object/dict → drill in (nested list; dicts get an add-key
|
|
27
|
+
* - object/dict → drill in (nested list; dicts get an add-key
|
|
28
|
+
* row); at a grouped namespace's root (FIELD_GROUPS) member
|
|
29
|
+
* fields collapse into one virtual group drill row
|
|
25
30
|
* - boolean / all-literal union → Enter cycles the value
|
|
26
31
|
* - string/number/mixed union → inline Input editor
|
|
27
32
|
* - array / literal / const / unknown → read-only viewer
|
|
@@ -37,26 +42,125 @@ import { getPath, nodeAtPath, rehydrateSchema } from "./schema-model.js";
|
|
|
37
42
|
import { getKeybindings, Input, matchesKey, } from '@earendil-works/pi-tui';
|
|
38
43
|
import { columnWidths, MARKER_W, padCell, panelThemeFns, SettingsListPanel, TABLE_SEP, tableHeaderLine, tableRuleLine, ViewerPanel, } from "./panels.js";
|
|
39
44
|
import { ansiFg, BOLD, RESET } from "./theme/index.js";
|
|
45
|
+
import { t } from "./i18n/index.js";
|
|
40
46
|
import { clipToWidth, visibleWidth } from "./text.js";
|
|
41
47
|
import { wrapFramedOverlay } from "./frame.js";
|
|
42
|
-
import {
|
|
48
|
+
import { languagePickerPanel } from "./selectors.js";
|
|
49
|
+
import { THEME_SETTINGS_NAMESPACE } from "./theme-settings.js";
|
|
50
|
+
import { catalogEntry, deriveKeyRef, directoryProviderEntries, providerProfileFor, providerRowView, resolveCatalogHint, unconfiguredCatalogEntries, } from "./provider-catalog.js";
|
|
43
51
|
/**
|
|
44
52
|
* Static namespace→category mapping, mirroring the web settings page's
|
|
45
53
|
* client-side slot structure (see the module header): the data plane carries
|
|
46
54
|
* no category field, so the slots are maintained here by hand. Namespaces not
|
|
47
|
-
* listed anywhere fall into the trailing `other` category.
|
|
55
|
+
* listed anywhere fall into the trailing `other` category. Beyond the web
|
|
56
|
+
* page's own slots, the plugins category enumerates the settings namespaces
|
|
57
|
+
* of the dsh plugins installed in this ecosystem (mcp-adapter, topics, vault,
|
|
58
|
+
* model-sync, dsh-feishu, dsh-llm-proxy) so their rows group under Plugins
|
|
59
|
+
* instead of leaking into `other`.
|
|
48
60
|
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
61
|
+
* The `label`s here are the English source of truth: rows render through
|
|
62
|
+
* `categoryLabel()` (t() at render time, keyed by category id), so the active
|
|
63
|
+
* UI language translates them while search keeps matching whatever is
|
|
64
|
+
* displayed. The first level is ordered by this canonical label
|
|
65
|
+
* (categorizeNamespaces sorts), not by map order.
|
|
51
66
|
*/
|
|
52
67
|
export const CATEGORY_MAP = [
|
|
53
|
-
{ id: '
|
|
68
|
+
{ id: 'dsh-tui', label: 'TUI', namespaces: ['dsh-tui'] },
|
|
69
|
+
{ id: 'general', label: 'General', namespaces: ['permission'] },
|
|
54
70
|
{ id: 'models', label: 'Models', namespaces: ['llm-deepseek', 'llm-pi-ai', 'agent-default-model'] },
|
|
55
|
-
{
|
|
71
|
+
{
|
|
72
|
+
id: 'plugins',
|
|
73
|
+
label: 'Plugins',
|
|
74
|
+
namespaces: [
|
|
75
|
+
'shell',
|
|
76
|
+
'agent-loop',
|
|
77
|
+
'web-search-deepseek',
|
|
78
|
+
// Settings namespaces of installed dsh plugins — dsh-llm-proxy
|
|
79
|
+
// registers through settings.installSection, the rest through
|
|
80
|
+
// settings.register; plugins without a namespace never reach the
|
|
81
|
+
// settings browser.
|
|
82
|
+
'mcp-adapter',
|
|
83
|
+
'topics',
|
|
84
|
+
'vault',
|
|
85
|
+
'model-sync',
|
|
86
|
+
'dsh-feishu',
|
|
87
|
+
'dsh-llm-proxy',
|
|
88
|
+
],
|
|
89
|
+
},
|
|
56
90
|
{ id: 'agent', label: 'Agent Presets', namespaces: ['agent-presets'] },
|
|
57
91
|
];
|
|
92
|
+
/**
|
|
93
|
+
* Category id → i18n key resolver for the user-facing label. The static
|
|
94
|
+
* labels in CATEGORY_MAP stay English (module data must not freeze a
|
|
95
|
+
* language at import time); rows resolve through these literal t() call
|
|
96
|
+
* sites at render time and fall back to the static label for an id without
|
|
97
|
+
* an entry.
|
|
98
|
+
*/
|
|
99
|
+
const CATEGORY_LABEL_KEYS = {
|
|
100
|
+
'dsh-tui': () => t('settingsui.category.dsh-tui'),
|
|
101
|
+
general: () => t('settingsui.category.general'),
|
|
102
|
+
models: () => t('settingsui.category.models'),
|
|
103
|
+
plugins: () => t('settingsui.category.plugins'),
|
|
104
|
+
agent: () => t('settingsui.category.agent'),
|
|
105
|
+
other: () => t('settingsui.category.other'),
|
|
106
|
+
};
|
|
107
|
+
/** User-facing label of one category (t()-resolved; English fallback). */
|
|
108
|
+
function categoryLabel(cat) {
|
|
109
|
+
const make = CATEGORY_LABEL_KEYS[cat.id];
|
|
110
|
+
return make === undefined ? cat.label : make();
|
|
111
|
+
}
|
|
58
112
|
/** Cap for a category row's member-name description line. */
|
|
59
113
|
export const CATEGORY_DESC_MAX = 60;
|
|
114
|
+
export const FIELD_GROUPS = {
|
|
115
|
+
'dsh-tui': [
|
|
116
|
+
{
|
|
117
|
+
id: 'subagent',
|
|
118
|
+
labelKey: 'settingsui.group.subagent',
|
|
119
|
+
fields: ['maxAgents', 'maxRounds', 'maxRoundsGrace', 'disableSubagent', 'registeredOnly'],
|
|
120
|
+
},
|
|
121
|
+
],
|
|
122
|
+
};
|
|
123
|
+
/**
|
|
124
|
+
* Resolve one group's label to display text. The static tables carry i18n
|
|
125
|
+
* KEYS (module data must not freeze a language at import time); this switch
|
|
126
|
+
* enumerates them as literal t() call sites — which is also what the i18n
|
|
127
|
+
* coverage guard matches against. Anything else passes through `t()`
|
|
128
|
+
* unchanged, since a non-key falls back to itself.
|
|
129
|
+
*/
|
|
130
|
+
export function resolveFieldGroupLabel(labelKey) {
|
|
131
|
+
switch (labelKey) {
|
|
132
|
+
case 'settingsui.group.subagent': return t('settingsui.group.subagent');
|
|
133
|
+
default: return t(labelKey);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Order schema field keys for display: grouped fields collapse into a single
|
|
138
|
+
* group slot at the position of the group's FIRST member present (members
|
|
139
|
+
* list only the keys actually present, in group-declaration order). Keys in
|
|
140
|
+
* no group pass through in order. Unknown/hidden keys never reach this
|
|
141
|
+
* function — the caller walks already-filtered rows.
|
|
142
|
+
*/
|
|
143
|
+
export function slotFieldKeys(ns, keys) {
|
|
144
|
+
const groups = FIELD_GROUPS[ns];
|
|
145
|
+
if (groups === undefined || groups.length === 0) {
|
|
146
|
+
return keys.map(key => ({ kind: 'field', key }));
|
|
147
|
+
}
|
|
148
|
+
const keySet = new Set(keys);
|
|
149
|
+
const emitted = new Set();
|
|
150
|
+
const slots = [];
|
|
151
|
+
for (const key of keys) {
|
|
152
|
+
const group = groups.find(def => def.fields.includes(key));
|
|
153
|
+
if (group === undefined) {
|
|
154
|
+
slots.push({ kind: 'field', key });
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
if (emitted.has(group.id))
|
|
158
|
+
continue;
|
|
159
|
+
emitted.add(group.id);
|
|
160
|
+
slots.push({ kind: 'group', group, members: group.fields.filter(field => keySet.has(field)) });
|
|
161
|
+
}
|
|
162
|
+
return slots;
|
|
163
|
+
}
|
|
60
164
|
/**
|
|
61
165
|
* Namespaces the Models category reads and writes. dsh-settings
|
|
62
166
|
* 0.1.2-alpha.3 removed the runtime settingsNamespace() helper: plain
|
|
@@ -68,8 +172,11 @@ const NS_LLM_PI_AI = 'llm-pi-ai';
|
|
|
68
172
|
const NS_LLM_DEEPSEEK = 'llm-deepseek';
|
|
69
173
|
const NS_AGENT_DEFAULT_MODEL = 'agent-default-model';
|
|
70
174
|
/**
|
|
71
|
-
* Group a describe() namespace list into ordered categories
|
|
72
|
-
*
|
|
175
|
+
* Group a describe() namespace list into ordered categories. The result is
|
|
176
|
+
* sorted alphabetically by the canonical (English) label — Agent Presets,
|
|
177
|
+
* General, Models, Other, Plugins, TUI — so the first level reads the same
|
|
178
|
+
* regardless of CATEGORY_MAP order or the active UI language (sorting keys on
|
|
179
|
+
* the canonical label, never the t()-translated one). Categories with no
|
|
73
180
|
* members are dropped, including `other` when nothing falls into it.
|
|
74
181
|
*
|
|
75
182
|
* Defensive: duplicate namespaces in the input count once (a namespace
|
|
@@ -92,7 +199,11 @@ export function categorizeNamespaces(nses) {
|
|
|
92
199
|
const others = [...input].filter(ns => !mapped.has(ns));
|
|
93
200
|
if (others.length > 0)
|
|
94
201
|
categories.push({ id: 'other', label: 'Other', namespaces: others });
|
|
95
|
-
return categories
|
|
202
|
+
return categories.sort((a, b) => {
|
|
203
|
+
const la = a.label.toLowerCase();
|
|
204
|
+
const lb = b.label.toLowerCase();
|
|
205
|
+
return la < lb ? -1 : la > lb ? 1 : 0;
|
|
206
|
+
});
|
|
96
207
|
}
|
|
97
208
|
/**
|
|
98
209
|
* Description line for a category row: member names joined with ", ", capped
|
|
@@ -109,7 +220,7 @@ export function categoryDescription(namespaces, max = 60) {
|
|
|
109
220
|
/** Human display for a resolved value (row value column; never re-scans anything). */
|
|
110
221
|
export function formatValue(value) {
|
|
111
222
|
if (value === undefined)
|
|
112
|
-
return '
|
|
223
|
+
return t('settingsui.value.unset');
|
|
113
224
|
if (value === null)
|
|
114
225
|
return 'null';
|
|
115
226
|
if (typeof value === 'string')
|
|
@@ -117,10 +228,10 @@ export function formatValue(value) {
|
|
|
117
228
|
if (typeof value === 'number' || typeof value === 'boolean')
|
|
118
229
|
return String(value);
|
|
119
230
|
if (Array.isArray(value))
|
|
120
|
-
return value.length === 0 ? '[]' :
|
|
231
|
+
return value.length === 0 ? '[]' : t('settingsui.value.items', { count: value.length });
|
|
121
232
|
if (typeof value === 'object') {
|
|
122
233
|
const keys = Object.keys(value);
|
|
123
|
-
return keys.length === 0 ? '{}' : `{${keys.length
|
|
234
|
+
return keys.length === 0 ? '{}' : `{${t('settingsui.value.keys', { count: keys.length })}}`;
|
|
124
235
|
}
|
|
125
236
|
return String(value);
|
|
126
237
|
}
|
|
@@ -147,11 +258,11 @@ export function parseNumberInput(text) {
|
|
|
147
258
|
// Literal decimal subset: hex/octal/binary prefixes and trailing garbage
|
|
148
259
|
// are rejected; decimal scientific notation stays (1e3 = 1000).
|
|
149
260
|
if (!/^[-+]?(\d+\.?\d*|\.\d+)([eE][-+]?\d+)?$/.test(text.trim())) {
|
|
150
|
-
return { kind: 'error', error:
|
|
261
|
+
return { kind: 'error', error: t('settingsui.parse.number', { text }) };
|
|
151
262
|
}
|
|
152
263
|
const n = Number(text);
|
|
153
264
|
if (!Number.isFinite(n))
|
|
154
|
-
return { kind: 'error', error:
|
|
265
|
+
return { kind: 'error', error: t('settingsui.parse.number', { text }) };
|
|
155
266
|
return { kind: 'value', value: n };
|
|
156
267
|
}
|
|
157
268
|
export function parseStringInput(text) {
|
|
@@ -171,7 +282,7 @@ export function parseUnionInput(text, node) {
|
|
|
171
282
|
const hasStringBranch = (node.list ?? []).some(member => member.type === 'string' || (member.type === 'literal' && typeof member.value === 'string'));
|
|
172
283
|
if (hasStringBranch)
|
|
173
284
|
return { kind: 'value', value: text };
|
|
174
|
-
return { kind: 'error', error: '
|
|
285
|
+
return { kind: 'error', error: t('settingsui.parse.json') };
|
|
175
286
|
}
|
|
176
287
|
/** Seed value for a newly added dict key, from the inner schema. */
|
|
177
288
|
export function defaultValueFor(node) {
|
|
@@ -206,27 +317,27 @@ export function fieldDescription(node, userOverride) {
|
|
|
206
317
|
parts.push(raw['en'] ?? raw[''] ?? Object.values(raw)[0] ?? '');
|
|
207
318
|
}
|
|
208
319
|
if (meta.required === true)
|
|
209
|
-
parts.push('required');
|
|
320
|
+
parts.push(t('settingsui.meta.required'));
|
|
210
321
|
if (meta.default !== undefined)
|
|
211
|
-
parts.push(
|
|
322
|
+
parts.push(t('settingsui.meta.default', { value: formatValue(meta.default) }));
|
|
212
323
|
if (meta.min !== undefined)
|
|
213
|
-
parts.push(
|
|
324
|
+
parts.push(t('settingsui.meta.min', { value: meta.min }));
|
|
214
325
|
if (meta.max !== undefined)
|
|
215
|
-
parts.push(
|
|
326
|
+
parts.push(t('settingsui.meta.max', { value: meta.max }));
|
|
216
327
|
if (meta.step !== undefined)
|
|
217
|
-
parts.push(
|
|
328
|
+
parts.push(t('settingsui.meta.step', { value: meta.step }));
|
|
218
329
|
if (meta.pattern !== undefined)
|
|
219
|
-
parts.push(
|
|
330
|
+
parts.push(t('settingsui.meta.pattern', { value: meta.pattern.source }));
|
|
220
331
|
if (meta.role === 'secret')
|
|
221
|
-
parts.push('secret');
|
|
332
|
+
parts.push(t('settingsui.meta.secret'));
|
|
222
333
|
if (meta.disabled === true)
|
|
223
|
-
parts.push('disabled');
|
|
334
|
+
parts.push(t('settingsui.meta.disabled'));
|
|
224
335
|
for (const badge of meta.badges ?? []) {
|
|
225
336
|
if (typeof badge.text === 'string')
|
|
226
337
|
parts.push(badge.text);
|
|
227
338
|
}
|
|
228
339
|
if (userOverride)
|
|
229
|
-
parts.push('
|
|
340
|
+
parts.push(t('settingsui.meta.userSet'));
|
|
230
341
|
return parts.filter(part => part !== '').join(' · ');
|
|
231
342
|
}
|
|
232
343
|
/** Inline value editor: title, current-value line, error/notice line, Input. */
|
|
@@ -330,7 +441,7 @@ export class EditField {
|
|
|
330
441
|
lines.push(...this.input.render(width));
|
|
331
442
|
}
|
|
332
443
|
lines.push('');
|
|
333
|
-
lines.push(fns.subtle(clipToWidth('
|
|
444
|
+
lines.push(fns.subtle(clipToWidth(t('settingsui.edit.footer'), wrap)));
|
|
334
445
|
return lines;
|
|
335
446
|
}
|
|
336
447
|
/**
|
|
@@ -371,7 +482,7 @@ class ConfirmReset {
|
|
|
371
482
|
return [
|
|
372
483
|
fns.accent(BOLD + this.label + RESET),
|
|
373
484
|
'',
|
|
374
|
-
fns.subtle(this.pending ?
|
|
485
|
+
fns.subtle(this.pending ? ` ${t('settingsui.reset.working')}` : ` ${t('settingsui.reset.hints')}`),
|
|
375
486
|
];
|
|
376
487
|
}
|
|
377
488
|
handleInput(data) {
|
|
@@ -452,12 +563,12 @@ export class AddProviderFlow {
|
|
|
452
563
|
return;
|
|
453
564
|
}
|
|
454
565
|
this.list = new SettingsListPanel(theme, {
|
|
455
|
-
title: '
|
|
566
|
+
title: t('settingsui.addProvider.title'),
|
|
456
567
|
rows: options.entries.map(entry => ({
|
|
457
568
|
id: entry.id,
|
|
458
569
|
label: entry.name,
|
|
459
570
|
value: '',
|
|
460
|
-
description: entry.hint,
|
|
571
|
+
description: resolveCatalogHint(entry.hint),
|
|
461
572
|
submenu: (_current, done) => isCustom(entry)
|
|
462
573
|
? options.customFlow(done)
|
|
463
574
|
: this.keyEditor(entry, options, done),
|
|
@@ -472,8 +583,8 @@ export class AddProviderFlow {
|
|
|
472
583
|
keyEditor(entry, options, done) {
|
|
473
584
|
const ref = deriveKeyRef(entry.id);
|
|
474
585
|
return new EditField(this.tui, {
|
|
475
|
-
title:
|
|
476
|
-
subtitle:
|
|
586
|
+
title: t('settingsui.addProvider.keyTitle', { name: entry.name }),
|
|
587
|
+
subtitle: t('settingsui.addProvider.keySubtitle', { ref }),
|
|
477
588
|
initial: '',
|
|
478
589
|
// Never echo the key — masked dot row (B2).
|
|
479
590
|
secret: true,
|
|
@@ -481,7 +592,7 @@ export class AddProviderFlow {
|
|
|
481
592
|
// Unlike an edit of a stored secret, the key is required here: a
|
|
482
593
|
// route with no key address cannot serve a request.
|
|
483
594
|
if (text.trim() === '')
|
|
484
|
-
return { kind: 'error', error: '
|
|
595
|
+
return { kind: 'error', error: t('settingsui.addProvider.keyRequired') };
|
|
485
596
|
return { kind: 'value', value: text.trim() };
|
|
486
597
|
},
|
|
487
598
|
onCommit: async (outcome) => {
|
|
@@ -509,11 +620,11 @@ export class AddProviderFlow {
|
|
|
509
620
|
if (this.empty) {
|
|
510
621
|
const fns = panelThemeFns(this.theme);
|
|
511
622
|
return [
|
|
512
|
-
fns.accent(BOLD + '
|
|
623
|
+
fns.accent(BOLD + t('settingsui.addProvider.title') + RESET),
|
|
513
624
|
'',
|
|
514
|
-
fns.muted('
|
|
625
|
+
fns.muted(t('settingsui.addProvider.empty')),
|
|
515
626
|
'',
|
|
516
|
-
fns.subtle(
|
|
627
|
+
fns.subtle(` ${t('settingsui.escClose')}`),
|
|
517
628
|
];
|
|
518
629
|
}
|
|
519
630
|
return this.list.render(width);
|
|
@@ -555,7 +666,7 @@ export async function commitProvider(ctx, writeProfile, entry, key) {
|
|
|
555
666
|
// No credential store in this process — the row is configured and the
|
|
556
667
|
// key must come from the environment; this is a success with a hint,
|
|
557
668
|
// never an error. Enter re-runs the commit idempotently.
|
|
558
|
-
return { notice:
|
|
669
|
+
return { notice: t('settingsui.commit.noCredentials', { ref }) };
|
|
559
670
|
}
|
|
560
671
|
try {
|
|
561
672
|
await credentials.set(ref, key);
|
|
@@ -565,8 +676,10 @@ export async function commitProvider(ctx, writeProfile, entry, key) {
|
|
|
565
676
|
// counts as configured, so the user needs the manual path. The error
|
|
566
677
|
// stays retryable in place — Enter re-runs the whole commit (B3).
|
|
567
678
|
return {
|
|
568
|
-
error:
|
|
569
|
-
|
|
679
|
+
error: t('settingsui.commit.keyFailed', {
|
|
680
|
+
cause: cause instanceof Error ? cause.message : String(cause),
|
|
681
|
+
ref,
|
|
682
|
+
}),
|
|
570
683
|
};
|
|
571
684
|
}
|
|
572
685
|
return undefined;
|
|
@@ -671,7 +784,7 @@ class SettingsBrowser {
|
|
|
671
784
|
return categorizeNamespaces(this.descriptors.map(d => d.ns));
|
|
672
785
|
}
|
|
673
786
|
categorySummary(cat) {
|
|
674
|
-
return
|
|
787
|
+
return t('settingsui.summary.namespaces', { count: cat.namespaces.length });
|
|
675
788
|
}
|
|
676
789
|
categoryDescription(cat) {
|
|
677
790
|
return categoryDescription(cat.namespaces, CATEGORY_DESC_MAX);
|
|
@@ -679,19 +792,30 @@ class SettingsBrowser {
|
|
|
679
792
|
categoryList() {
|
|
680
793
|
const items = this.categories().map(cat => ({
|
|
681
794
|
id: cat.id,
|
|
682
|
-
label: cat
|
|
795
|
+
label: categoryLabel(cat),
|
|
683
796
|
value: this.categorySummary(cat),
|
|
684
797
|
description: this.categoryDescription(cat),
|
|
685
798
|
submenu: cat.id === 'models'
|
|
686
799
|
? (_current, done) => this.openModelsSubmenu(done)
|
|
687
|
-
:
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
800
|
+
: cat.id === 'dsh-tui'
|
|
801
|
+
// Single-namespace category: drill straight into the config
|
|
802
|
+
// fields — no intermediate namespace list repeating the namespace
|
|
803
|
+
// name under the category that names it.
|
|
804
|
+
? (_current, done) => {
|
|
805
|
+
const section = this.sectionList(THEME_SETTINGS_NAMESPACE, [], () => {
|
|
806
|
+
this.refreshCategoryList();
|
|
807
|
+
done();
|
|
808
|
+
}, categoryLabel(cat));
|
|
809
|
+
return section.list;
|
|
810
|
+
}
|
|
811
|
+
: (_current, done) => {
|
|
812
|
+
const list = this.namespaceList(categoryLabel(cat), this.descriptors.filter(d => cat.namespaces.includes(d.ns)), done);
|
|
813
|
+
this.nsList = list;
|
|
814
|
+
return list;
|
|
815
|
+
},
|
|
692
816
|
}));
|
|
693
817
|
return new SettingsListPanel(this.theme, {
|
|
694
|
-
title: '
|
|
818
|
+
title: t('settingsui.title'),
|
|
695
819
|
rows: items,
|
|
696
820
|
maxVisible: 10,
|
|
697
821
|
enableSearch: true,
|
|
@@ -710,14 +834,14 @@ class SettingsBrowser {
|
|
|
710
834
|
nsSummary(desc) {
|
|
711
835
|
const root = this.root(desc.ns);
|
|
712
836
|
if (root !== undefined && root.type === 'object') {
|
|
713
|
-
return
|
|
837
|
+
return t('settingsui.fields.count', { count: Object.keys(root.dict ?? {}).length });
|
|
714
838
|
}
|
|
715
839
|
return formatValue(desc.value);
|
|
716
840
|
}
|
|
717
841
|
nsDescription(desc) {
|
|
718
|
-
const parts = [
|
|
842
|
+
const parts = [t('settingsui.ns.applies', { applies: desc.applies })];
|
|
719
843
|
if (desc.user !== undefined)
|
|
720
|
-
parts.push('
|
|
844
|
+
parts.push(t('settingsui.meta.userSet'));
|
|
721
845
|
return parts.join(' · ');
|
|
722
846
|
}
|
|
723
847
|
/** Namespace list for one category; Esc pops back to the category level. */
|
|
@@ -935,7 +1059,7 @@ class SettingsBrowser {
|
|
|
935
1059
|
submenu: (_current, done) => new ViewerPanel(this.theme, {
|
|
936
1060
|
title: `providers.${id}`,
|
|
937
1061
|
lines: [
|
|
938
|
-
'
|
|
1062
|
+
t('settingsui.models.readonly'),
|
|
939
1063
|
'',
|
|
940
1064
|
...JSON.stringify(profile, null, 2).split('\n'),
|
|
941
1065
|
],
|
|
@@ -949,7 +1073,7 @@ class SettingsBrowser {
|
|
|
949
1073
|
if (deepseekDesc !== undefined) {
|
|
950
1074
|
items.push({
|
|
951
1075
|
id: 'llm-deepseek',
|
|
952
|
-
label: '
|
|
1076
|
+
label: t('settingsui.models.deepseek'),
|
|
953
1077
|
value: this.nsSummary(deepseekDesc),
|
|
954
1078
|
description: this.nsDescription(deepseekDesc),
|
|
955
1079
|
submenu: (_current, done) => {
|
|
@@ -965,7 +1089,7 @@ class SettingsBrowser {
|
|
|
965
1089
|
if (agentDesc !== undefined) {
|
|
966
1090
|
items.push({
|
|
967
1091
|
id: 'agent-default-model',
|
|
968
|
-
label: '
|
|
1092
|
+
label: t('settingsui.models.defaultModel'),
|
|
969
1093
|
value: this.defaultModelSummary(agentDesc),
|
|
970
1094
|
description: this.nsDescription(agentDesc),
|
|
971
1095
|
submenu: (_current, done) => {
|
|
@@ -982,9 +1106,9 @@ class SettingsBrowser {
|
|
|
982
1106
|
const configured = new Set(Object.keys(providers.providers ?? {}));
|
|
983
1107
|
items.push({
|
|
984
1108
|
id: '\u0000add-provider',
|
|
985
|
-
label: '
|
|
1109
|
+
label: t('settingsui.models.add'),
|
|
986
1110
|
value: '',
|
|
987
|
-
description: '
|
|
1111
|
+
description: t('settingsui.models.addDesc'),
|
|
988
1112
|
submenu: (_current, done) => new AddProviderFlow(this.tui, this.theme, {
|
|
989
1113
|
entries: this.addProviderEntries(configured),
|
|
990
1114
|
onCommit: (entry, key) => this.commitNewProvider(entry, key),
|
|
@@ -997,7 +1121,7 @@ class SettingsBrowser {
|
|
|
997
1121
|
});
|
|
998
1122
|
}
|
|
999
1123
|
return new SettingsListPanel(this.theme, {
|
|
1000
|
-
title: '
|
|
1124
|
+
title: t('settingsui.models.title'),
|
|
1001
1125
|
rows: items,
|
|
1002
1126
|
maxVisible: 12,
|
|
1003
1127
|
enableSearch: true,
|
|
@@ -1014,7 +1138,7 @@ class SettingsBrowser {
|
|
|
1014
1138
|
if (typeof provider === 'string' && provider !== '' && typeof model === 'string' && model !== '') {
|
|
1015
1139
|
const effort = value?.reasoningEffort;
|
|
1016
1140
|
return typeof effort === 'string' && effort !== ''
|
|
1017
|
-
?
|
|
1141
|
+
? t('settingsui.route.think', { provider, model, effort })
|
|
1018
1142
|
: `${provider}/${model}`;
|
|
1019
1143
|
}
|
|
1020
1144
|
return formatValue(desc.value);
|
|
@@ -1023,9 +1147,12 @@ class SettingsBrowser {
|
|
|
1023
1147
|
/**
|
|
1024
1148
|
* Build the FW list panel for one schema node at `path` of `ns`.
|
|
1025
1149
|
* `onExit` runs when the list is popped (Esc) — it must refresh the parent
|
|
1026
|
-
* level and call the parent's submenu `done()`.
|
|
1150
|
+
* level and call the parent's submenu `done()`. `title` overrides the
|
|
1151
|
+
* default panel title (the namespace or path spelling) — the TUI category's
|
|
1152
|
+
* direct drill uses it to title the fields with the category label instead
|
|
1153
|
+
* of the raw namespace name.
|
|
1027
1154
|
*/
|
|
1028
|
-
sectionList(ns, path, onExit) {
|
|
1155
|
+
sectionList(ns, path, onExit, title) {
|
|
1029
1156
|
const root = this.root(ns);
|
|
1030
1157
|
const desc = this.descriptor(ns);
|
|
1031
1158
|
const node = root !== undefined && path.length > 0
|
|
@@ -1033,9 +1160,82 @@ class SettingsBrowser {
|
|
|
1033
1160
|
: root;
|
|
1034
1161
|
const rows = node === undefined ? [] : this.buildRows(ns, node, path, desc?.value);
|
|
1035
1162
|
const refresh = () => { this.refreshRows(rows, list); };
|
|
1163
|
+
// Reset/add-key rows always lead; the field rows after them collapse
|
|
1164
|
+
// into virtual groups at a grouped namespace's root (FIELD_GROUPS).
|
|
1165
|
+
const structural = rows.filter(row => row.kind === 'reset' || row.kind === 'addkey');
|
|
1166
|
+
const fields = rows.filter(row => row.kind !== 'reset' && row.kind !== 'addkey');
|
|
1167
|
+
const slots = path.length === 0 && node?.type === 'object'
|
|
1168
|
+
? slotFieldKeys(ns, fields.map(row => row.path[row.path.length - 1]))
|
|
1169
|
+
: undefined;
|
|
1170
|
+
let fieldItems;
|
|
1171
|
+
if (slots === undefined) {
|
|
1172
|
+
fieldItems = fields.map(row => this.rowItem(row, refresh));
|
|
1173
|
+
}
|
|
1174
|
+
else {
|
|
1175
|
+
const byKey = new Map(fields.map(row => [row.path[row.path.length - 1], row]));
|
|
1176
|
+
fieldItems = [];
|
|
1177
|
+
for (const slot of slots) {
|
|
1178
|
+
if (slot.kind === 'field') {
|
|
1179
|
+
const row = byKey.get(slot.key);
|
|
1180
|
+
if (row !== undefined)
|
|
1181
|
+
fieldItems.push(this.rowItem(row, refresh));
|
|
1182
|
+
}
|
|
1183
|
+
else {
|
|
1184
|
+
fieldItems.push(this.fieldGroupRow(ns, slot));
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
const items = [...structural.map(row => this.rowItem(row, refresh)), ...fieldItems];
|
|
1189
|
+
const list = new SettingsListPanel(this.theme, {
|
|
1190
|
+
title: title ?? (path.length === 0 ? ns : path.join('.')),
|
|
1191
|
+
rows: items,
|
|
1192
|
+
maxVisible: 12,
|
|
1193
|
+
enableSearch: true,
|
|
1194
|
+
onChange: (id, newValue) => { void this.onCycle(rows, list, id, newValue); },
|
|
1195
|
+
onCancel: () => {
|
|
1196
|
+
refresh();
|
|
1197
|
+
onExit();
|
|
1198
|
+
},
|
|
1199
|
+
});
|
|
1200
|
+
return { list, refresh };
|
|
1201
|
+
}
|
|
1202
|
+
/**
|
|
1203
|
+
* The drill row of one virtual field group (see FIELD_GROUPS): value shows
|
|
1204
|
+
* the member count, the description names the members, and Enter opens the
|
|
1205
|
+
* group's field list.
|
|
1206
|
+
*/
|
|
1207
|
+
fieldGroupRow(ns, slot) {
|
|
1208
|
+
const label = resolveFieldGroupLabel(slot.group.labelKey);
|
|
1209
|
+
return {
|
|
1210
|
+
id: `group:${slot.group.id}`,
|
|
1211
|
+
label,
|
|
1212
|
+
value: `{${t('settingsui.fields.count', { count: slot.members.length })}}`,
|
|
1213
|
+
description: slot.members.join(' · '),
|
|
1214
|
+
submenu: (_current, done) => {
|
|
1215
|
+
const child = this.fieldGroupList(ns, label, slot.members, done);
|
|
1216
|
+
return child.list;
|
|
1217
|
+
},
|
|
1218
|
+
};
|
|
1219
|
+
}
|
|
1220
|
+
/**
|
|
1221
|
+
* Field list of one virtual group: the member schema fields exactly as
|
|
1222
|
+
* they would render flat, with the same cycle/input editing — minus the
|
|
1223
|
+
* reset/add-key rows (a virtual group has no single path to reset; the
|
|
1224
|
+
* namespace level still resets the whole subtree).
|
|
1225
|
+
*/
|
|
1226
|
+
fieldGroupList(ns, label, members, onExit) {
|
|
1227
|
+
const root = this.root(ns);
|
|
1228
|
+
const rows = [];
|
|
1229
|
+
for (const name of members) {
|
|
1230
|
+
const child = root !== undefined && root.type === 'object' ? root.dict?.[name] : undefined;
|
|
1231
|
+
if (child === undefined || fieldMeta(child).hidden === true)
|
|
1232
|
+
continue;
|
|
1233
|
+
rows.push(this.fieldRow(ns, child, [name], name));
|
|
1234
|
+
}
|
|
1235
|
+
const refresh = () => { this.refreshRows(rows, list); };
|
|
1036
1236
|
const items = rows.map(row => this.rowItem(row, refresh));
|
|
1037
1237
|
const list = new SettingsListPanel(this.theme, {
|
|
1038
|
-
title:
|
|
1238
|
+
title: label,
|
|
1039
1239
|
rows: items,
|
|
1040
1240
|
maxVisible: 12,
|
|
1041
1241
|
enableSearch: true,
|
|
@@ -1054,7 +1254,9 @@ class SettingsBrowser {
|
|
|
1054
1254
|
id: '\u0000reset',
|
|
1055
1255
|
ns,
|
|
1056
1256
|
path,
|
|
1057
|
-
label:
|
|
1257
|
+
label: path.length === 0
|
|
1258
|
+
? t('settingsui.reset.namespace')
|
|
1259
|
+
: t('settingsui.reset.path', { path: path.join('.') }),
|
|
1058
1260
|
kind: 'reset',
|
|
1059
1261
|
node,
|
|
1060
1262
|
value: undefined,
|
|
@@ -1065,7 +1267,7 @@ class SettingsBrowser {
|
|
|
1065
1267
|
id: '\u0000add',
|
|
1066
1268
|
ns,
|
|
1067
1269
|
path,
|
|
1068
|
-
label: '
|
|
1270
|
+
label: t('settingsui.addKey.label'),
|
|
1069
1271
|
kind: 'addkey',
|
|
1070
1272
|
node,
|
|
1071
1273
|
value: undefined,
|
|
@@ -1144,14 +1346,16 @@ class SettingsBrowser {
|
|
|
1144
1346
|
}
|
|
1145
1347
|
computeDisplay(kind, node, value, secret) {
|
|
1146
1348
|
if (secret)
|
|
1147
|
-
return value === undefined || value === null || value === '' ? '
|
|
1349
|
+
return value === undefined || value === null || value === '' ? t('settingsui.value.unset') : '••••••';
|
|
1148
1350
|
switch (kind) {
|
|
1149
1351
|
case 'drill': {
|
|
1150
1352
|
if (node.type === 'object')
|
|
1151
|
-
return `{${Object.keys(node.dict ?? {}).length
|
|
1353
|
+
return `{${t('settingsui.fields.count', { count: Object.keys(node.dict ?? {}).length })}}`;
|
|
1152
1354
|
if (node.type === 'dict') {
|
|
1153
1355
|
const entries = Object.keys((value ?? {}));
|
|
1154
|
-
return
|
|
1356
|
+
return entries.length === 1
|
|
1357
|
+
? t('settingsui.dict.one', { count: entries.length })
|
|
1358
|
+
: t('settingsui.dict.many', { count: entries.length });
|
|
1155
1359
|
}
|
|
1156
1360
|
return formatValue(value);
|
|
1157
1361
|
}
|
|
@@ -1188,7 +1392,7 @@ class SettingsBrowser {
|
|
|
1188
1392
|
submenu: (_current, done) => new ViewerPanel(this.theme, {
|
|
1189
1393
|
title: row.path.join('.'),
|
|
1190
1394
|
lines: [
|
|
1191
|
-
'
|
|
1395
|
+
t('settingsui.models.readonly'),
|
|
1192
1396
|
'',
|
|
1193
1397
|
...JSON.stringify(row.value, null, 2).split('\n'),
|
|
1194
1398
|
],
|
|
@@ -1305,6 +1509,12 @@ class SettingsBrowser {
|
|
|
1305
1509
|
}
|
|
1306
1510
|
/** Build the edit submenu for a leaf row; commits write to settings. */
|
|
1307
1511
|
inputSubmenu(row, refresh, done) {
|
|
1512
|
+
// dsh-tui.language: pick from the discovered locales instead of typing a
|
|
1513
|
+
// raw id — the same panel the /language command opens, writing through
|
|
1514
|
+
// the standard mutate path (the watch hook applies the language live).
|
|
1515
|
+
if (row.ns === THEME_SETTINGS_NAMESPACE && row.path.length === 1 && row.path[0] === 'language') {
|
|
1516
|
+
return this.languageSubmenu(row, refresh, done);
|
|
1517
|
+
}
|
|
1308
1518
|
const meta = fieldMeta(row.node);
|
|
1309
1519
|
const initial = row.secret === true || row.value === undefined || row.value === null
|
|
1310
1520
|
? ''
|
|
@@ -1312,8 +1522,10 @@ class SettingsBrowser {
|
|
|
1312
1522
|
return new EditField(this.tui, {
|
|
1313
1523
|
title: row.path.join('.'),
|
|
1314
1524
|
subtitle: row.secret === true
|
|
1315
|
-
? 'secret
|
|
1316
|
-
:
|
|
1525
|
+
? t('settingsui.edit.secret')
|
|
1526
|
+
: t('settingsui.edit.current', {
|
|
1527
|
+
value: row.display + (meta.required === true ? ` · ${t('settingsui.meta.required')}` : ''),
|
|
1528
|
+
}),
|
|
1317
1529
|
initial,
|
|
1318
1530
|
// role('secret') rows get the masked dot-row renderer too (B2).
|
|
1319
1531
|
secret: row.secret === true,
|
|
@@ -1323,6 +1535,43 @@ class SettingsBrowser {
|
|
|
1323
1535
|
onError: message => this.onError(message),
|
|
1324
1536
|
}, this.theme);
|
|
1325
1537
|
}
|
|
1538
|
+
/**
|
|
1539
|
+
* The language picker as the dsh-tui.language edit submenu: Enter picks
|
|
1540
|
+
* and commits through the standard mutate path, Esc pops back. A failed
|
|
1541
|
+
* write keeps the picker open with the error on the status line (the
|
|
1542
|
+
* EditField ✘ contract, transplanted onto a table).
|
|
1543
|
+
*/
|
|
1544
|
+
languageSubmenu(row, refresh, done) {
|
|
1545
|
+
let status;
|
|
1546
|
+
// A pick is single-shot: the commit is async, and a second Enter landing
|
|
1547
|
+
// before it resolves would close the submenu twice — the second close
|
|
1548
|
+
// returning to the LIST would then re-open the picker on the same row.
|
|
1549
|
+
let settled = false;
|
|
1550
|
+
// Esc may close the picker while a commit is in flight (the EditField
|
|
1551
|
+
// contract); a late failure then has no status line to land on and goes
|
|
1552
|
+
// through onError instead of vanishing.
|
|
1553
|
+
let closed = false;
|
|
1554
|
+
const cancel = () => { closed = true; done(); };
|
|
1555
|
+
return languagePickerPanel(this.theme, typeof row.value === 'string' ? row.value : '', async (picked) => {
|
|
1556
|
+
if (settled)
|
|
1557
|
+
return;
|
|
1558
|
+
settled = true;
|
|
1559
|
+
const result = await this.commitInput(row, refresh, { kind: 'value', value: picked });
|
|
1560
|
+
if (result?.error !== undefined) {
|
|
1561
|
+
if (closed) {
|
|
1562
|
+
this.onError(result.error);
|
|
1563
|
+
return;
|
|
1564
|
+
}
|
|
1565
|
+
// The write failed with the picker still open — re-arm for a retry.
|
|
1566
|
+
settled = false;
|
|
1567
|
+
status = t('sel.status.saveFailed', { message: result.error });
|
|
1568
|
+
this.tui.requestRender();
|
|
1569
|
+
return;
|
|
1570
|
+
}
|
|
1571
|
+
if (!closed)
|
|
1572
|
+
done();
|
|
1573
|
+
}, cancel, () => status);
|
|
1574
|
+
}
|
|
1326
1575
|
parseFor(row, text) {
|
|
1327
1576
|
// Whitespace-only input on a secret also means "keep" — an accidental
|
|
1328
1577
|
// space must not unset (delete) the stored secret; clearing a secret is
|
|
@@ -1335,9 +1584,9 @@ class SettingsBrowser {
|
|
|
1335
1584
|
case 'union': return parseUnionInput(text, row.node);
|
|
1336
1585
|
case 'transform':
|
|
1337
1586
|
return row.node.inner === undefined
|
|
1338
|
-
? { kind: 'error', error:
|
|
1587
|
+
? { kind: 'error', error: t('settingsui.parse.type', { type: row.node.type }) }
|
|
1339
1588
|
: this.parseFor({ ...row, node: row.node.inner }, text);
|
|
1340
|
-
default: return { kind: 'error', error:
|
|
1589
|
+
default: return { kind: 'error', error: t('settingsui.parse.type', { type: row.node.type }) };
|
|
1341
1590
|
}
|
|
1342
1591
|
}
|
|
1343
1592
|
async commitInput(row, refresh, outcome) {
|
|
@@ -1356,15 +1605,15 @@ class SettingsBrowser {
|
|
|
1356
1605
|
const inner = row.node.inner ?? row.node;
|
|
1357
1606
|
const existing = (getPath(this.descriptor(row.ns)?.value, row.path) ?? {});
|
|
1358
1607
|
return new EditField(this.tui, {
|
|
1359
|
-
title:
|
|
1360
|
-
subtitle:
|
|
1608
|
+
title: t('settingsui.addKey.title', { path: row.path.length === 0 ? '…' : row.path.join('.') }),
|
|
1609
|
+
subtitle: t('settingsui.addKey.subtitle', { default: formatValue(defaultValueFor(inner)) }),
|
|
1361
1610
|
initial: '',
|
|
1362
1611
|
parse: text => {
|
|
1363
1612
|
const key = text.trim();
|
|
1364
1613
|
if (key === '')
|
|
1365
|
-
return { kind: 'error', error: '
|
|
1614
|
+
return { kind: 'error', error: t('settingsui.addKey.empty') };
|
|
1366
1615
|
if (key in existing)
|
|
1367
|
-
return { kind: 'error', error:
|
|
1616
|
+
return { kind: 'error', error: t('settingsui.addKey.exists', { key }) };
|
|
1368
1617
|
return { kind: 'value', value: key };
|
|
1369
1618
|
},
|
|
1370
1619
|
onCommit: async (outcome) => {
|
|
@@ -1383,8 +1632,8 @@ class SettingsBrowser {
|
|
|
1383
1632
|
/** Reset-to-defaults confirmation for group/namespace rows. */
|
|
1384
1633
|
resetSubmenu(row, refresh, done) {
|
|
1385
1634
|
return new ConfirmReset(this.theme, row.path.length === 0
|
|
1386
|
-
?
|
|
1387
|
-
:
|
|
1635
|
+
? t('settingsui.reset.ns', { ns: row.ns })
|
|
1636
|
+
: t('settingsui.reset.path', { path: row.path.join('.') }), () => {
|
|
1388
1637
|
void this.write(row.ns, [{ op: 'unset', path: row.path }]).then(error => {
|
|
1389
1638
|
if (error !== undefined)
|
|
1390
1639
|
this.onError(error);
|