@xl0/pi-lovely-agents 0.1.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/CHANGELOG.md +17 -0
- package/LICENSE +21 -0
- package/README.md +184 -0
- package/extensions/lovely-agents/agent.ts +1374 -0
- package/extensions/lovely-agents/bash.ts +599 -0
- package/extensions/lovely-agents/child-session.ts +296 -0
- package/extensions/lovely-agents/config.ts +221 -0
- package/extensions/lovely-agents/coordinator.ts +506 -0
- package/extensions/lovely-agents/definitions.ts +380 -0
- package/extensions/lovely-agents/index.ts +400 -0
- package/extensions/lovely-agents/lifecycle.ts +251 -0
- package/extensions/lovely-agents/management.ts +638 -0
- package/extensions/lovely-agents/notifications.ts +220 -0
- package/extensions/lovely-agents/provider-limits.ts +13 -0
- package/extensions/lovely-agents/rendering.ts +90 -0
- package/extensions/lovely-agents/state.ts +1179 -0
- package/extensions/lovely-agents/task-panel.ts +192 -0
- package/extensions/lovely-agents/tools.ts +635 -0
- package/extensions/lovely-agents/updates.ts +45 -0
- package/node_modules/@xl0/pi-lovely-config/CHANGELOG.md +79 -0
- package/node_modules/@xl0/pi-lovely-config/LICENSE +21 -0
- package/node_modules/@xl0/pi-lovely-config/README.md +200 -0
- package/node_modules/@xl0/pi-lovely-config/package.json +59 -0
- package/node_modules/@xl0/pi-lovely-config/src/config.ts +399 -0
- package/node_modules/@xl0/pi-lovely-config/src/index.ts +3 -0
- package/node_modules/@xl0/pi-lovely-config/src/ui.ts +786 -0
- package/package.json +68 -0
- package/skills/agent/SKILL.md +21 -0
- package/skills/agent-creator/SKILL.md +35 -0
|
@@ -0,0 +1,786 @@
|
|
|
1
|
+
import type { Theme } from "@earendil-works/pi-coding-agent"
|
|
2
|
+
import {
|
|
3
|
+
CURSOR_MARKER,
|
|
4
|
+
Editor,
|
|
5
|
+
type EditorTheme,
|
|
6
|
+
fuzzyFilter,
|
|
7
|
+
getKeybindings,
|
|
8
|
+
Input,
|
|
9
|
+
Key,
|
|
10
|
+
matchesKey,
|
|
11
|
+
type TUI,
|
|
12
|
+
truncateToWidth,
|
|
13
|
+
visibleWidth,
|
|
14
|
+
wrapTextWithAnsi
|
|
15
|
+
} from "@earendil-works/pi-tui"
|
|
16
|
+
import type { ConfigPatch, ConfigScope, ScopedConfig, ScopedConfigField, ScopedConfigPatch } from "./config"
|
|
17
|
+
import { getConfigWarnings } from "./config"
|
|
18
|
+
|
|
19
|
+
type Keybindings = ReturnType<typeof getKeybindings>
|
|
20
|
+
|
|
21
|
+
type ScopedConfigChangeHandler<Config extends object> = (config: ScopedConfig<Config>) => void
|
|
22
|
+
type FocusPart = "include" | "value"
|
|
23
|
+
type NumberInputParseResult = { ok: true; value: number } | { ok: false; message: string }
|
|
24
|
+
type EnumField = Extract<ScopedConfigField, { kind: "enum" | "multiEnum" }>
|
|
25
|
+
// pending holds the uncommitted multiEnum selection; absent for single enums
|
|
26
|
+
type EnumSearchState = { input: Input; fieldKey: string; selectedIndex: number; pending: Set<string> | undefined }
|
|
27
|
+
type TextEditorState = { editor: Editor; fieldKey: string }
|
|
28
|
+
|
|
29
|
+
export class ScopedConfigEditor<Config extends object> {
|
|
30
|
+
private readonly tui: TUI
|
|
31
|
+
private readonly theme: Theme
|
|
32
|
+
private readonly config: ScopedConfig<Config>
|
|
33
|
+
private readonly onChange: ScopedConfigChangeHandler<Config>
|
|
34
|
+
private readonly fields: readonly ScopedConfigField[]
|
|
35
|
+
private readonly scopes: readonly ConfigScope[]
|
|
36
|
+
private readonly done: (result: undefined) => void
|
|
37
|
+
private activeInput: Input | undefined
|
|
38
|
+
private activeEnumSearch: EnumSearchState | undefined
|
|
39
|
+
private activeTextEditor: TextEditorState | undefined
|
|
40
|
+
private activeInputError: string | undefined
|
|
41
|
+
private focusPart: FocusPart = "value"
|
|
42
|
+
private currentTab = 0
|
|
43
|
+
private currentRow = 0
|
|
44
|
+
|
|
45
|
+
constructor(options: {
|
|
46
|
+
tui: TUI
|
|
47
|
+
theme: Theme
|
|
48
|
+
config: ScopedConfig<Config>
|
|
49
|
+
onChange: ScopedConfigChangeHandler<Config>
|
|
50
|
+
done: (result: undefined) => void
|
|
51
|
+
}) {
|
|
52
|
+
this.tui = options.tui
|
|
53
|
+
this.theme = options.theme
|
|
54
|
+
this.config = options.config
|
|
55
|
+
this.onChange = options.onChange
|
|
56
|
+
this.fields = options.config.fields
|
|
57
|
+
this.scopes = options.config.scopes
|
|
58
|
+
this.done = options.done
|
|
59
|
+
this.updateFocus()
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
private get scoped(): ScopedConfigPatch {
|
|
63
|
+
return this.config.scoped
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
render(width: number): string[] {
|
|
67
|
+
const lines: string[] = []
|
|
68
|
+
const renderWidth = Math.max(1, width)
|
|
69
|
+
const scope = this.currentScope()
|
|
70
|
+
const fields = this.visibleFields(scope)
|
|
71
|
+
|
|
72
|
+
lines.push(this.theme.fg("accent", "─".repeat(renderWidth)))
|
|
73
|
+
this.renderTabs(lines, renderWidth)
|
|
74
|
+
lines.push("")
|
|
75
|
+
this.renderScopeHeader(lines, renderWidth)
|
|
76
|
+
lines.push("")
|
|
77
|
+
this.renderActiveFieldDescription(lines, renderWidth, fields)
|
|
78
|
+
this.renderRows(lines, renderWidth, scope, fields)
|
|
79
|
+
this.renderActiveValueDescription(lines, renderWidth, scope, fields)
|
|
80
|
+
lines.push("")
|
|
81
|
+
addWrappedWithPrefix(
|
|
82
|
+
lines,
|
|
83
|
+
renderWidth,
|
|
84
|
+
" ",
|
|
85
|
+
this.theme.fg("dim", "Tab switch scope • ↑↓ select • ←→ include/value • Enter edit/cycle • Space toggle/step • Esc close")
|
|
86
|
+
)
|
|
87
|
+
lines.push(this.theme.fg("accent", "─".repeat(renderWidth)))
|
|
88
|
+
|
|
89
|
+
return lines
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
invalidate() {}
|
|
93
|
+
|
|
94
|
+
handleInput(data: string): void {
|
|
95
|
+
const kb = getKeybindings()
|
|
96
|
+
const isSpace = data === " " || matchesKey(data, Key.space) || matchesKey(data, Key.shift("space"))
|
|
97
|
+
if (this.handleCloseKey(data, kb)) return
|
|
98
|
+
if (this.handleActiveTextEditorInput(data, kb)) return
|
|
99
|
+
if (this.handleNavigationKey(data, kb)) return
|
|
100
|
+
if (this.handleActiveInput(data, kb, isSpace)) return
|
|
101
|
+
if (this.handleActivationKey(data, kb, isSpace)) return
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
private handleCloseKey(data: string, kb: Keybindings): boolean {
|
|
105
|
+
if (matchesKey(data, Key.ctrl("c"))) {
|
|
106
|
+
this.done(undefined)
|
|
107
|
+
return true
|
|
108
|
+
}
|
|
109
|
+
if ((this.activeInput || this.activeEnumSearch || this.activeTextEditor) && kb.matches(data, "tui.select.cancel")) {
|
|
110
|
+
this.activeInput = undefined
|
|
111
|
+
this.activeEnumSearch = undefined
|
|
112
|
+
this.activeTextEditor = undefined
|
|
113
|
+
this.activeInputError = undefined
|
|
114
|
+
this.tui.requestRender()
|
|
115
|
+
return true
|
|
116
|
+
}
|
|
117
|
+
if (kb.matches(data, "tui.select.cancel")) {
|
|
118
|
+
this.done(undefined)
|
|
119
|
+
return true
|
|
120
|
+
}
|
|
121
|
+
return false
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
private handleNavigationKey(data: string, kb: Keybindings): boolean {
|
|
125
|
+
if (this.activeEnumSearch) {
|
|
126
|
+
if (kb.matches(data, "tui.select.up")) {
|
|
127
|
+
this.moveEnumSearchSelection(-1)
|
|
128
|
+
return true
|
|
129
|
+
}
|
|
130
|
+
if (kb.matches(data, "tui.select.down")) {
|
|
131
|
+
this.moveEnumSearchSelection(1)
|
|
132
|
+
return true
|
|
133
|
+
}
|
|
134
|
+
return false
|
|
135
|
+
}
|
|
136
|
+
if (kb.matches(data, "tui.select.up")) {
|
|
137
|
+
if (!this.commitActiveInput()) return true
|
|
138
|
+
this.moveRow(-1)
|
|
139
|
+
return true
|
|
140
|
+
}
|
|
141
|
+
if (kb.matches(data, "tui.select.down")) {
|
|
142
|
+
if (!this.commitActiveInput()) return true
|
|
143
|
+
this.moveRow(1)
|
|
144
|
+
return true
|
|
145
|
+
}
|
|
146
|
+
if (matchesKey(data, Key.tab)) {
|
|
147
|
+
if (!this.commitActiveInput()) return true
|
|
148
|
+
this.switchTab(1)
|
|
149
|
+
return true
|
|
150
|
+
}
|
|
151
|
+
if (this.activeInput && (kb.matches(data, "tui.editor.cursorLeft") || kb.matches(data, "tui.editor.cursorRight"))) return false
|
|
152
|
+
if (kb.matches(data, "tui.editor.cursorRight")) {
|
|
153
|
+
const field = this.selectedField()
|
|
154
|
+
if (!field || getConfigValue(this.scoped[this.currentScope()], field.key) === undefined) return true
|
|
155
|
+
this.focusPart = "value"
|
|
156
|
+
this.tui.requestRender()
|
|
157
|
+
return true
|
|
158
|
+
}
|
|
159
|
+
if (!kb.matches(data, "tui.editor.cursorLeft")) return false
|
|
160
|
+
if (!this.commitActiveInput()) return true
|
|
161
|
+
this.focusPart = "include"
|
|
162
|
+
this.activeInput = undefined
|
|
163
|
+
this.tui.requestRender()
|
|
164
|
+
return true
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
private handleActivationKey(data: string, kb: Keybindings, isSpace: boolean): boolean {
|
|
168
|
+
if (kb.matches(data, "tui.input.submit")) {
|
|
169
|
+
this.activateRow()
|
|
170
|
+
return true
|
|
171
|
+
}
|
|
172
|
+
if (isSpace) {
|
|
173
|
+
const field = this.selectedField()
|
|
174
|
+
if (field && this.focusPart === "include") this.toggleField(this.currentScope(), field)
|
|
175
|
+
else if (field?.kind === "number" && !field.values)
|
|
176
|
+
this.saveValue(this.currentScope(), field, nextNumberValue(field, getConfigValue(this.scoped[this.currentScope()], field.key)))
|
|
177
|
+
else if (field?.kind === "enum" && field.search)
|
|
178
|
+
this.saveValue(this.currentScope(), field, nextFieldValue(this.scoped[this.currentScope()], field))
|
|
179
|
+
else if (field?.kind !== "string" && field?.kind !== "text") this.activateRow()
|
|
180
|
+
else if (this.isResetSelected()) this.reset(this.currentScope())
|
|
181
|
+
return true
|
|
182
|
+
}
|
|
183
|
+
return false
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
private renderTabs(lines: string[], width: number): void {
|
|
187
|
+
const tabs = []
|
|
188
|
+
for (const [index, scope] of this.scopes.entries()) {
|
|
189
|
+
const isUnset = this.scopeIsUnset(scope)
|
|
190
|
+
const text = ` ${isUnset ? "□" : "■"} ${scopeLabel(scope)} `
|
|
191
|
+
const styled =
|
|
192
|
+
index === this.currentTab
|
|
193
|
+
? this.theme.bg("selectedBg", this.theme.fg("text", text))
|
|
194
|
+
: this.theme.fg(isUnset ? "muted" : "success", text)
|
|
195
|
+
tabs.push(`${styled} `)
|
|
196
|
+
}
|
|
197
|
+
addWrappedWithPrefix(lines, width, " ", tabs.join(""))
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
private renderScopeHeader(lines: string[], width: number): void {
|
|
201
|
+
const scope = this.scopes[this.currentTab]
|
|
202
|
+
if (!scope) return
|
|
203
|
+
|
|
204
|
+
const path = this.config.path(scope)
|
|
205
|
+
addWrappedWithPrefix(
|
|
206
|
+
lines,
|
|
207
|
+
width,
|
|
208
|
+
" ",
|
|
209
|
+
`${this.theme.fg("accent", this.theme.bold(`${scopeLabel(scope)} config`))} ${this.theme.fg("dim", path)}`
|
|
210
|
+
)
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
private renderRows(lines: string[], width: number, scope: ConfigScope, fields: readonly ScopedConfigField[]): void {
|
|
214
|
+
for (const [index, field] of fields.entries()) {
|
|
215
|
+
const selected = index === this.currentRow
|
|
216
|
+
const prefix = this.theme.fg(selected ? "accent" : "muted", `${selected ? "> " : " "}${" ".repeat(field.depth ?? 0)}`)
|
|
217
|
+
const rawValue = getConfigValue(this.scoped[scope], field.key)
|
|
218
|
+
const isSet = rawValue !== undefined
|
|
219
|
+
const include = `${isSet ? "[x]" : "[ ]"} `
|
|
220
|
+
const renderedInclude = this.theme.fg(selected && this.focusPart === "include" ? "accent" : "muted", include)
|
|
221
|
+
let value = formatScopedValue(this.scoped[scope], field)
|
|
222
|
+
if (selected && this.focusPart === "value" && this.activeInput) {
|
|
223
|
+
if (field.kind === "string") value = renderStringInput(this.activeInput, width)
|
|
224
|
+
else if (field.kind === "number" && !field.values) value = renderInput(this.activeInput, width)
|
|
225
|
+
}
|
|
226
|
+
if (selected && this.focusPart === "value" && this.activeEnumSearch?.fieldKey === field.key) {
|
|
227
|
+
value = renderInput(this.activeEnumSearch.input, width)
|
|
228
|
+
}
|
|
229
|
+
const note = getScopeNote(scope, this.scopes, this.scoped, field)
|
|
230
|
+
const renderedNote = note ? ` ${this.theme.fg("muted", `(${note})`)}` : ""
|
|
231
|
+
const valueStyle = getFieldWarning(this.scoped[scope], field)
|
|
232
|
+
? "warning"
|
|
233
|
+
: !isSet
|
|
234
|
+
? "muted"
|
|
235
|
+
: selected && this.focusPart === "value"
|
|
236
|
+
? "accent"
|
|
237
|
+
: "text"
|
|
238
|
+
const renderedValue = !isSet && note ? "" : ` ${this.theme.fg(valueStyle, value)}`
|
|
239
|
+
addWrappedWithPrefix(lines, width, prefix, `${renderedInclude}${this.theme.fg("text", field.label)}${renderedValue}${renderedNote}`)
|
|
240
|
+
if (selected) {
|
|
241
|
+
this.renderEnumSearch(lines, width, field, " ".repeat(visibleWidth(prefix) + visibleWidth(include) + visibleWidth(field.label) + 1))
|
|
242
|
+
this.renderTextEditor(lines, width, field, " ".repeat(visibleWidth(prefix) + visibleWidth(include)))
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
this.renderResetRow(lines, width, this.isResetSelected(fields))
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
private renderEnumSearch(lines: string[], width: number, field: ScopedConfigField, indent: string): void {
|
|
249
|
+
const state = this.activeEnumSearch
|
|
250
|
+
if (!state || !isEnumField(field) || field.key !== state.fieldKey) return
|
|
251
|
+
|
|
252
|
+
const values = enumSearchValues(field, state.input.getValue(), state.pending)
|
|
253
|
+
if (values.length === 0) {
|
|
254
|
+
addWrappedWithPrefix(lines, width, indent, this.theme.fg("muted", "No matching values"))
|
|
255
|
+
return
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const maxVisible = 8
|
|
259
|
+
const startIndex = Math.max(0, Math.min(state.selectedIndex - Math.floor(maxVisible / 2), values.length - maxVisible))
|
|
260
|
+
const endIndex = Math.min(startIndex + maxVisible, values.length)
|
|
261
|
+
for (let index = startIndex; index < endIndex; index++) {
|
|
262
|
+
const value = values[index] ?? ""
|
|
263
|
+
const selected = index === state.selectedIndex
|
|
264
|
+
const marker = state.pending ? (state.pending.has(value) ? this.theme.fg("accent", "✓ ") : " ") : ""
|
|
265
|
+
const prefix = `${indent}${this.theme.fg(selected ? "accent" : "muted", selected ? "> " : " ")}${marker}`
|
|
266
|
+
const description = field.valueDescriptions?.[value]
|
|
267
|
+
const renderedValue = this.theme.fg(selected ? "accent" : "text", value)
|
|
268
|
+
const renderedDescription = description ? ` ${this.theme.fg("muted", description)}` : ""
|
|
269
|
+
addWrappedWithPrefix(lines, width, prefix, `${renderedValue}${renderedDescription}`)
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if (startIndex > 0 || endIndex < values.length) {
|
|
273
|
+
addWrappedWithPrefix(lines, width, indent, this.theme.fg("muted", `(${state.selectedIndex + 1}/${values.length})`))
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
private renderTextEditor(lines: string[], width: number, field: ScopedConfigField, indent: string): void {
|
|
278
|
+
const state = this.activeTextEditor
|
|
279
|
+
if (!state || field.kind !== "text" || field.key !== state.fieldKey) return
|
|
280
|
+
|
|
281
|
+
const editorWidth = Math.max(1, width - visibleWidth(indent))
|
|
282
|
+
for (const line of state.editor.render(editorWidth)) lines.push(`${indent}${line}`)
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
private renderActiveValueDescription(lines: string[], width: number, scope: ConfigScope, fields: readonly ScopedConfigField[]): void {
|
|
286
|
+
const field = this.selectedField(fields)
|
|
287
|
+
const error = this.activeInput ? this.activeInputError : undefined
|
|
288
|
+
const warning = field ? getFieldWarning(this.scoped[scope], field) : undefined
|
|
289
|
+
const valueDescription =
|
|
290
|
+
field && !this.activeEnumSearch && !this.activeTextEditor ? getValueDescription(this.scoped[scope], field) : undefined
|
|
291
|
+
lines.push("")
|
|
292
|
+
if (error) addWrappedWithPrefix(lines, width, " ", this.theme.fg("error", error))
|
|
293
|
+
else if (this.activeTextEditor)
|
|
294
|
+
addWrappedWithPrefix(lines, width, " ", this.theme.fg("muted", "Enter save • Shift+Enter newline • Esc discard"))
|
|
295
|
+
else if (this.activeEnumSearch?.pending)
|
|
296
|
+
addWrappedWithPrefix(lines, width, " ", this.theme.fg("muted", "Space toggle • Enter save • Esc discard"))
|
|
297
|
+
else if (warning) addWrappedWithPrefix(lines, width, " ", this.theme.fg("warning", warning))
|
|
298
|
+
else if (valueDescription) addWrappedWithPrefix(lines, width, " ", this.theme.fg("muted", valueDescription))
|
|
299
|
+
else lines.push("")
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
private renderActiveFieldDescription(lines: string[], width: number, fields: readonly ScopedConfigField[]): void {
|
|
303
|
+
const description = this.selectedField(fields)?.description
|
|
304
|
+
const wrapped = description ? wrapTextWithAnsi(this.theme.fg("muted", description), Math.max(1, width - 1)).slice(0, 2) : []
|
|
305
|
+
for (let index = 0; index < 2; index++) lines.push(wrapped[index] ? ` ${wrapped[index]}` : "")
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
private renderResetRow(lines: string[], width: number, selected: boolean): void {
|
|
309
|
+
lines.push(this.theme.fg("dim", ` ${"─".repeat(Math.max(1, width - 2))}`))
|
|
310
|
+
const prefix = this.theme.fg(selected ? "accent" : "muted", selected ? "> " : " ")
|
|
311
|
+
addWrappedWithPrefix(
|
|
312
|
+
lines,
|
|
313
|
+
width,
|
|
314
|
+
prefix,
|
|
315
|
+
`${this.theme.fg("text", `Reset ${scopeLabel(this.currentScope())} to default`)} ${this.theme.fg("muted", "delete this scope config file")}`
|
|
316
|
+
)
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
private currentScope(): ConfigScope {
|
|
320
|
+
return this.scopes[this.currentTab] ?? "user"
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
private resolvedConfig(scope: ConfigScope): Record<string, unknown> {
|
|
324
|
+
const scoped = { user: {}, workspace: {} } as ScopedConfigPatch
|
|
325
|
+
for (const configScope of this.scopes) {
|
|
326
|
+
scoped[configScope] = this.scoped[configScope]
|
|
327
|
+
if (configScope === scope) break
|
|
328
|
+
}
|
|
329
|
+
return this.config.resolve(scoped)
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
private visibleFields(scope: ConfigScope = this.currentScope()): ScopedConfigField[] {
|
|
333
|
+
const resolved = this.resolvedConfig(scope)
|
|
334
|
+
return this.fields.filter(field => isFieldVisible(field, scope, this.scoped, resolved))
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
private selectedField(fields: readonly ScopedConfigField[] = this.visibleFields()): ScopedConfigField | undefined {
|
|
338
|
+
return fields[this.currentRow]
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
private isResetSelected(fields: readonly ScopedConfigField[] = this.visibleFields()): boolean {
|
|
342
|
+
return this.currentRow === fields.length
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
private rowCount(): number {
|
|
346
|
+
return this.visibleFields().length + 1
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
private scopeIsUnset(scope: ConfigScope): boolean {
|
|
350
|
+
return this.fields.every(field => getConfigValue(this.scoped[scope], field.key) === undefined)
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
private refresh(): void {
|
|
354
|
+
this.currentRow = Math.min(this.currentRow, this.rowCount() - 1)
|
|
355
|
+
if (this.activeEnumSearch && this.selectedField()?.key !== this.activeEnumSearch.fieldKey) this.activeEnumSearch = undefined
|
|
356
|
+
if (this.activeTextEditor && this.selectedField()?.key !== this.activeTextEditor.fieldKey) this.activeTextEditor = undefined
|
|
357
|
+
this.updateFocus()
|
|
358
|
+
this.tui.requestRender()
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
private updateFocus(): void {
|
|
362
|
+
if (this.activeEnumSearch || this.activeTextEditor) {
|
|
363
|
+
this.focusPart = "value"
|
|
364
|
+
return
|
|
365
|
+
}
|
|
366
|
+
const field = this.selectedField()
|
|
367
|
+
this.focusPart = field && getConfigValue(this.scoped[this.currentScope()], field.key) === undefined ? "include" : "value"
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
private switchTab(delta: number): void {
|
|
371
|
+
this.currentTab = (this.currentTab + delta + this.scopes.length) % this.scopes.length
|
|
372
|
+
this.refresh()
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
private moveRow(delta: number): void {
|
|
376
|
+
const rowCount = this.rowCount()
|
|
377
|
+
this.currentRow = (this.currentRow + delta + rowCount) % rowCount
|
|
378
|
+
this.refresh()
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
private activateRow(): void {
|
|
382
|
+
const scope = this.currentScope()
|
|
383
|
+
const fields = this.visibleFields(scope)
|
|
384
|
+
const field = this.selectedField(fields)
|
|
385
|
+
if (!field) {
|
|
386
|
+
if (this.isResetSelected(fields)) this.reset(scope)
|
|
387
|
+
return
|
|
388
|
+
}
|
|
389
|
+
if (this.focusPart === "include") this.toggleField(scope, field)
|
|
390
|
+
else if (getConfigValue(this.scoped[scope], field.key) === undefined) this.saveValue(scope, field, field.default)
|
|
391
|
+
else if (field.kind === "number" && field.values)
|
|
392
|
+
this.saveValue(scope, field, nextNumberValue(field, getConfigValue(this.scoped[scope], field.key)))
|
|
393
|
+
else if (field.kind === "multiEnum" || (field.kind === "enum" && field.search)) this.startEnumSearch(field)
|
|
394
|
+
else if (field.kind === "text") this.startTextEditor(field)
|
|
395
|
+
else if (fieldUsesInput(field)) this.startInput(field)
|
|
396
|
+
else this.saveValue(scope, field, nextFieldValue(this.scoped[scope], field))
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
private toggleField(scope: ConfigScope, field: ScopedConfigField): void {
|
|
400
|
+
const current = getConfigValue(this.scoped[scope], field.key)
|
|
401
|
+
const nextIsSet = current === undefined
|
|
402
|
+
this.activeInput = undefined
|
|
403
|
+
this.activeTextEditor = undefined
|
|
404
|
+
this.saveValue(scope, field, nextIsSet ? field.default : undefined)
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
private startInput(field: ScopedConfigField, initial?: string): void {
|
|
408
|
+
if (!fieldUsesInput(field)) return
|
|
409
|
+
this.activeEnumSearch = undefined
|
|
410
|
+
const value = initial ?? getConfigValue(this.scoped[this.currentScope()], field.key)
|
|
411
|
+
const input = new Input()
|
|
412
|
+
input.setValue(typeof value === "string" || typeof value === "number" ? String(value) : "")
|
|
413
|
+
input.focused = true
|
|
414
|
+
this.activeInput = input
|
|
415
|
+
this.updateActiveInputError(field)
|
|
416
|
+
this.tui.requestRender()
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
private startTextEditor(field: Extract<ScopedConfigField, { kind: "text" }>): void {
|
|
420
|
+
const editor = new Editor(this.tui, getTextEditorTheme(this.theme), { paddingX: 0 })
|
|
421
|
+
const value = getConfigValue(this.scoped[this.currentScope()], field.key)
|
|
422
|
+
editor.setText(typeof value === "string" ? value : "")
|
|
423
|
+
editor.focused = true
|
|
424
|
+
editor.disableSubmit = true
|
|
425
|
+
editor.onChange = () => this.tui.requestRender()
|
|
426
|
+
this.activeInput = undefined
|
|
427
|
+
this.activeEnumSearch = undefined
|
|
428
|
+
this.activeInputError = undefined
|
|
429
|
+
this.activeTextEditor = { editor, fieldKey: field.key }
|
|
430
|
+
this.tui.requestRender()
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
private startEnumSearch(field: EnumField): void {
|
|
434
|
+
const input = new Input()
|
|
435
|
+
input.focused = true
|
|
436
|
+
const value = getConfigValue(this.scoped[this.currentScope()], field.key)
|
|
437
|
+
const selectedIndex = typeof value === "string" ? Math.max(0, field.values.indexOf(value)) : 0
|
|
438
|
+
const pending = field.kind === "multiEnum" ? new Set(Array.isArray(value) ? (value as string[]) : []) : undefined
|
|
439
|
+
this.activeInput = undefined
|
|
440
|
+
this.activeInputError = undefined
|
|
441
|
+
this.activeEnumSearch = { input, fieldKey: field.key, selectedIndex, pending }
|
|
442
|
+
this.tui.requestRender()
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
private handleActiveInput(data: string, kb: Keybindings, isSpace: boolean): boolean {
|
|
446
|
+
if (this.handleEnumSearchInput(data, kb, isSpace)) return true
|
|
447
|
+
|
|
448
|
+
const field = this.selectedField()
|
|
449
|
+
if (this.focusPart !== "value" || !field || !fieldUsesInput(field)) return false
|
|
450
|
+
|
|
451
|
+
if (this.activeInput && kb.matches(data, "tui.input.submit")) {
|
|
452
|
+
this.commitActiveInput()
|
|
453
|
+
return true
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
if (this.activeInput) {
|
|
457
|
+
if (field.kind === "number" && !isNumberInputAllowed(data, this.activeInput, kb)) return true
|
|
458
|
+
this.activeInput.handleInput(data)
|
|
459
|
+
this.updateActiveInputError(field)
|
|
460
|
+
this.tui.requestRender()
|
|
461
|
+
return true
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
return false
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
private handleActiveTextEditorInput(data: string, kb: Keybindings): boolean {
|
|
468
|
+
const state = this.activeTextEditor
|
|
469
|
+
const field = this.selectedField()
|
|
470
|
+
if (!state || field?.kind !== "text" || field.key !== state.fieldKey) return false
|
|
471
|
+
|
|
472
|
+
if (kb.matches(data, "tui.input.submit")) {
|
|
473
|
+
this.activeTextEditor = undefined
|
|
474
|
+
this.saveValue(this.currentScope(), field, state.editor.getExpandedText())
|
|
475
|
+
return true
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
state.editor.handleInput(data)
|
|
479
|
+
this.tui.requestRender()
|
|
480
|
+
return true
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
private handleEnumSearchInput(data: string, kb: Keybindings, isSpace: boolean): boolean {
|
|
484
|
+
const state = this.activeEnumSearch
|
|
485
|
+
const field = this.selectedField()
|
|
486
|
+
if (!state || !isEnumField(field) || field.key !== state.fieldKey) return false
|
|
487
|
+
|
|
488
|
+
const pending = state.pending
|
|
489
|
+
const query = state.input.getValue()
|
|
490
|
+
const highlighted = enumSearchValues(field, query, pending)[state.selectedIndex]
|
|
491
|
+
if (kb.matches(data, "tui.input.submit")) {
|
|
492
|
+
if (pending) {
|
|
493
|
+
this.saveValue(
|
|
494
|
+
this.currentScope(),
|
|
495
|
+
field,
|
|
496
|
+
field.values.filter(value => pending.has(value))
|
|
497
|
+
)
|
|
498
|
+
} else if (highlighted !== undefined) {
|
|
499
|
+
this.activeEnumSearch = undefined
|
|
500
|
+
this.saveValue(this.currentScope(), field, highlighted)
|
|
501
|
+
}
|
|
502
|
+
return true
|
|
503
|
+
}
|
|
504
|
+
if (pending && isSpace) {
|
|
505
|
+
if (highlighted !== undefined) {
|
|
506
|
+
if (pending.has(highlighted)) pending.delete(highlighted)
|
|
507
|
+
else pending.add(highlighted)
|
|
508
|
+
// list reorders on toggle; keep the cursor on the same item
|
|
509
|
+
state.selectedIndex = enumSearchValues(field, query, pending).indexOf(highlighted)
|
|
510
|
+
}
|
|
511
|
+
this.tui.requestRender()
|
|
512
|
+
return true
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
const previousQuery = state.input.getValue()
|
|
516
|
+
state.input.handleInput(data)
|
|
517
|
+
if (state.input.getValue() !== previousQuery) state.selectedIndex = 0
|
|
518
|
+
this.tui.requestRender()
|
|
519
|
+
return true
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
private moveEnumSearchSelection(delta: number): void {
|
|
523
|
+
const state = this.activeEnumSearch
|
|
524
|
+
const field = this.selectedField()
|
|
525
|
+
if (!state || !isEnumField(field) || field.key !== state.fieldKey) return
|
|
526
|
+
const values = enumSearchValues(field, state.input.getValue(), state.pending)
|
|
527
|
+
if (values.length === 0) return
|
|
528
|
+
state.selectedIndex = (state.selectedIndex + delta + values.length) % values.length
|
|
529
|
+
this.tui.requestRender()
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
private commitActiveInput(): boolean {
|
|
533
|
+
const field = this.selectedField()
|
|
534
|
+
if (!field || !fieldUsesInput(field) || !this.activeInput) return true
|
|
535
|
+
const value = this.activeInput.getValue()
|
|
536
|
+
if (field.kind === "number") {
|
|
537
|
+
const parsed = parseNumberInput(field, value)
|
|
538
|
+
if (!parsed.ok) {
|
|
539
|
+
this.activeInputError = parsed.message
|
|
540
|
+
this.tui.requestRender()
|
|
541
|
+
return false
|
|
542
|
+
}
|
|
543
|
+
this.activeInput = undefined
|
|
544
|
+
this.activeInputError = undefined
|
|
545
|
+
this.saveValue(this.currentScope(), field, parsed.value)
|
|
546
|
+
return true
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
this.activeInput = undefined
|
|
550
|
+
this.activeInputError = undefined
|
|
551
|
+
this.saveValue(this.currentScope(), field, value)
|
|
552
|
+
return true
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
private updateActiveInputError(field: ScopedConfigField): void {
|
|
556
|
+
if (field.kind !== "number" || field.values || !this.activeInput) {
|
|
557
|
+
this.activeInputError = undefined
|
|
558
|
+
return
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
const parsed = parseNumberInput(field, this.activeInput.getValue())
|
|
562
|
+
this.activeInputError = parsed.ok ? undefined : parsed.message
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
private saveValue(scope: ConfigScope, field: ScopedConfigField, value: unknown): void {
|
|
566
|
+
this.activeEnumSearch = undefined
|
|
567
|
+
this.activeTextEditor = undefined
|
|
568
|
+
this.activeInputError = undefined
|
|
569
|
+
this.config.update(scope, field.key as keyof Config & string, value as Config[keyof Config & string] | undefined)
|
|
570
|
+
this.onChange(this.config)
|
|
571
|
+
this.refresh()
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
private reset(scope: ConfigScope): void {
|
|
575
|
+
this.config.resetScope(scope)
|
|
576
|
+
this.onChange(this.config)
|
|
577
|
+
this.refresh()
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
function addWrappedWithPrefix(lines: string[], width: number, prefix: string, text: string): void {
|
|
582
|
+
const prefixWidth = visibleWidth(prefix)
|
|
583
|
+
if (prefixWidth >= width) {
|
|
584
|
+
lines.push(...wrapTextWithAnsi(prefix + text, width))
|
|
585
|
+
return
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
const wrapped = wrapTextWithAnsi(text, width - prefixWidth)
|
|
589
|
+
const continuationPrefix = " ".repeat(prefixWidth)
|
|
590
|
+
for (let i = 0; i < wrapped.length; i++) lines.push(`${i === 0 ? prefix : continuationPrefix}${wrapped[i]}`)
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
function isFieldVisible(
|
|
594
|
+
field: ScopedConfigField,
|
|
595
|
+
scope: ConfigScope,
|
|
596
|
+
configs: ScopedConfigPatch,
|
|
597
|
+
resolved: Record<string, unknown>
|
|
598
|
+
): boolean {
|
|
599
|
+
if (!field.visibleWhen) return true
|
|
600
|
+
return field.visibleWhen({
|
|
601
|
+
scope,
|
|
602
|
+
get: key => resolved[key],
|
|
603
|
+
getScoped: (key, targetScope = scope) => getConfigValue(configs[targetScope], key)
|
|
604
|
+
})
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
function getConfigValue(config: object, key: string): unknown {
|
|
608
|
+
return (config as Record<string, unknown>)[key]
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
function isEnumField(field: ScopedConfigField | undefined): field is EnumField {
|
|
612
|
+
return field?.kind === "enum" || field?.kind === "multiEnum"
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
function fieldUsesInput(field: ScopedConfigField): boolean {
|
|
616
|
+
return field.kind === "string" || (field.kind === "number" && !field.values)
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
function getTextEditorTheme(theme: Theme): EditorTheme {
|
|
620
|
+
return {
|
|
621
|
+
borderColor: text => theme.fg("border", text),
|
|
622
|
+
selectList: {
|
|
623
|
+
selectedPrefix: text => theme.fg("accent", text),
|
|
624
|
+
selectedText: text => theme.bg("selectedBg", theme.fg("text", text)),
|
|
625
|
+
description: text => theme.fg("muted", text),
|
|
626
|
+
scrollInfo: text => theme.fg("muted", text),
|
|
627
|
+
noMatch: text => theme.fg("muted", text)
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
function renderInput(input: Input, width: number): string {
|
|
633
|
+
const rendered = input.render(width + 2)[0]?.slice(2) ?? ""
|
|
634
|
+
return rendered.trimEnd()
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
function inputCursor(input: Input): number {
|
|
638
|
+
return (input as unknown as { cursor: number }).cursor
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
function renderStringInput(input: Input, width: number): string {
|
|
642
|
+
const text = `"${input.getValue()}"`
|
|
643
|
+
const cursor = Math.min(inputCursor(input) + 1, text.length - 1)
|
|
644
|
+
const beforeCursor = text.slice(0, cursor)
|
|
645
|
+
const atCursor = text[cursor] ?? '"'
|
|
646
|
+
const afterCursor = text.slice(cursor + atCursor.length)
|
|
647
|
+
return truncateToWidth(`${beforeCursor}${CURSOR_MARKER}\x1b[7m${atCursor}\x1b[27m${afterCursor}`, width, "")
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
function isNumberInputAllowed(data: string, input: Input | undefined, kb: Keybindings): boolean {
|
|
651
|
+
if (kb.matches(data, "tui.editor.deleteCharBackward") || kb.matches(data, "tui.editor.deleteCharForward")) return true
|
|
652
|
+
if (/^[0-9]$/.test(data)) return true
|
|
653
|
+
if (data === ".") return !input?.getValue().includes(".")
|
|
654
|
+
if (data === "+" || data === "-") {
|
|
655
|
+
if (!input) return true
|
|
656
|
+
return inputCursor(input) === 0 && !/[+-]/.test(input.getValue())
|
|
657
|
+
}
|
|
658
|
+
return data.length !== 1
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
function parseNumberInput(field: Extract<ScopedConfigField, { kind: "number" }>, value: string): NumberInputParseResult {
|
|
662
|
+
const trimmed = value.trim()
|
|
663
|
+
if (trimmed === "") return { ok: false, message: `${field.label} must be a number` }
|
|
664
|
+
|
|
665
|
+
const parsed = Number(trimmed)
|
|
666
|
+
if (!Number.isFinite(parsed)) return { ok: false, message: `${field.label} must be a number` }
|
|
667
|
+
if (field.values !== undefined && !field.values.includes(parsed)) {
|
|
668
|
+
return { ok: false, message: `${field.label} must be one of: ${field.values.join(", ")}` }
|
|
669
|
+
}
|
|
670
|
+
if (field.min !== undefined && parsed < field.min) return { ok: false, message: `${field.label} must be at least ${field.min}` }
|
|
671
|
+
if (field.max !== undefined && parsed > field.max) return { ok: false, message: `${field.label} must be at most ${field.max}` }
|
|
672
|
+
return { ok: true, value: parsed }
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
function nextFieldValue(config: ConfigPatch, field: ScopedConfigField): unknown {
|
|
676
|
+
const current = formatScopedValue(config, field)
|
|
677
|
+
const options = field.kind === "enum" ? field.values : ["on", "off"]
|
|
678
|
+
const next = nextOption(options, current)
|
|
679
|
+
return field.kind === "boolean" ? next === "on" : next
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
function nextNumberValue(field: Extract<ScopedConfigField, { kind: "number" }>, value: unknown): number {
|
|
683
|
+
if (field.values) return nextOption(field.values, typeof value === "number" ? value : field.default)
|
|
684
|
+
const step = field.step ?? 1
|
|
685
|
+
const current = typeof value === "number" ? value : field.default
|
|
686
|
+
const next = roundToStepPrecision(current + step, step)
|
|
687
|
+
if (field.max !== undefined && next > field.max) return current < field.max ? field.max : (field.min ?? field.default)
|
|
688
|
+
return next
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
function roundToStepPrecision(value: number, step: number): number {
|
|
692
|
+
const decimalPlaces = step.toString().split(".")[1]?.length ?? 0
|
|
693
|
+
return Number(value.toFixed(decimalPlaces))
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
function nextOption<T>(options: readonly T[], value: T): T {
|
|
697
|
+
const index = options.indexOf(value)
|
|
698
|
+
return options[(index + 1) % options.length] ?? options[0] ?? value
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
// Checked items (pending) float to the top, keeping relative order.
|
|
702
|
+
function enumSearchValues(field: EnumField, query: string, pending?: Set<string>): string[] {
|
|
703
|
+
const values = field.values.map(value => ({ value }))
|
|
704
|
+
const matched = query
|
|
705
|
+
? fuzzyFilter(values, query, item => `${item.value} ${field.valueDescriptions?.[item.value] ?? ""}`).map(item => item.value)
|
|
706
|
+
: values.map(item => item.value)
|
|
707
|
+
if (!pending) return matched
|
|
708
|
+
return [...matched.filter(value => pending.has(value)), ...matched.filter(value => !pending.has(value))]
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
function formatScopedValue(config: object, field: ScopedConfigField): string {
|
|
712
|
+
const value = getConfigValue(config, field.key)
|
|
713
|
+
return formatFieldValue(field, value)
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
function getFieldWarning(config: object, field: ScopedConfigField): string | undefined {
|
|
717
|
+
return getConfigWarnings([field], config)[0]?.message
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
function getFieldValueWarning(field: ScopedConfigField, value: unknown): string | undefined {
|
|
721
|
+
return getConfigWarnings([field], { [field.key]: value })[0]?.message
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
function getValueDescription(config: object, field: ScopedConfigField): string | undefined {
|
|
725
|
+
const rawValue = getConfigValue(config, field.key)
|
|
726
|
+
if (rawValue === undefined) return undefined
|
|
727
|
+
const value = formatFieldValue(field, rawValue)
|
|
728
|
+
|
|
729
|
+
switch (field.kind) {
|
|
730
|
+
case "enum":
|
|
731
|
+
case "number":
|
|
732
|
+
return field.valueDescriptions?.[value]
|
|
733
|
+
case "boolean":
|
|
734
|
+
return field.valueDescriptions?.[value as "on" | "off"]
|
|
735
|
+
case "multiEnum":
|
|
736
|
+
case "string":
|
|
737
|
+
case "text":
|
|
738
|
+
return undefined
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
function formatFieldValue(field: ScopedConfigField, value: unknown): string {
|
|
743
|
+
if (value === undefined) return "unset"
|
|
744
|
+
if (field.kind === "boolean") return value ? "on" : "off"
|
|
745
|
+
if (field.kind === "string") return JSON.stringify(String(value))
|
|
746
|
+
if (field.kind === "text") return formatTextValue(String(value))
|
|
747
|
+
if (field.kind === "number") return String(value)
|
|
748
|
+
if (field.kind === "multiEnum" && Array.isArray(value)) return value.length ? value.join(", ") : "none"
|
|
749
|
+
return String(value)
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
function formatTextValue(value: string): string {
|
|
753
|
+
const lineCount = value.split("\n").length
|
|
754
|
+
const preview = value.replace(/\s+/g, " ").trim()
|
|
755
|
+
const quoted = preview ? JSON.stringify(preview) : '""'
|
|
756
|
+
const suffix = lineCount > 1 ? ` (${lineCount} lines)` : ""
|
|
757
|
+
return `${truncateToWidth(quoted, 48, "…")}${suffix}`
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
function getScopeNote(
|
|
761
|
+
scope: ConfigScope,
|
|
762
|
+
scopes: readonly ConfigScope[],
|
|
763
|
+
configs: ScopedConfigPatch,
|
|
764
|
+
field: ScopedConfigField
|
|
765
|
+
): string | undefined {
|
|
766
|
+
const value = getConfigValue(configs[scope], field.key)
|
|
767
|
+
const userValue = getConfigValue(configs.user, field.key)
|
|
768
|
+
const workspaceValue = getConfigValue(configs.workspace, field.key)
|
|
769
|
+
const user = userValue === undefined || getFieldValueWarning(field, userValue) ? undefined : formatFieldValue(field, userValue)
|
|
770
|
+
const workspace =
|
|
771
|
+
workspaceValue === undefined || getFieldValueWarning(field, workspaceValue) ? undefined : formatFieldValue(field, workspaceValue)
|
|
772
|
+
const defaultValue = formatFieldValue(field, field.default)
|
|
773
|
+
|
|
774
|
+
if (value !== undefined) {
|
|
775
|
+
if (getFieldValueWarning(field, value)) return undefined
|
|
776
|
+
return scope === "user" && scopes.includes("workspace") && workspace !== undefined ? `Workspace: ${workspace}` : undefined
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
if (scope === "user" && scopes.includes("workspace") && workspace !== undefined) return `Workspace: ${workspace}`
|
|
780
|
+
if (scope === "workspace" && scopes.includes("user") && user !== undefined) return `User: ${user}`
|
|
781
|
+
return `default: ${defaultValue}`
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
function scopeLabel(scope: ConfigScope): string {
|
|
785
|
+
return `${scope[0]?.toUpperCase() ?? ""}${scope.slice(1)}`
|
|
786
|
+
}
|