@lapismd/design-core 0.1.0 → 0.1.2
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 +22 -0
- package/README.md +1 -1
- package/package.json +12 -8
- package/spec/src/styling-and-themes.md +9 -9
- package/src/shared/ai/chat.css +2 -1
- package/src/shared/ai/composer-input/ComposerInput.svelte +3 -2
- package/src/shared/ai/experimental/reaction-bar/ReactionBar.svelte +1 -1
- package/src/shared/ai/tool-calls/ToolCalls.svelte +1 -1
- package/src/shared/ai/trigger-menu.ts +2 -3
- package/src/shared/diff/file-diff/FileDiff.svelte +5 -8
- package/src/shared/diff/file-listing/FileListingViewModeToggle.css +42 -5
- package/src/shared/diff/merge-editor/MergeEditor.svelte +2 -7
- package/src/shared/filter/filter-query/filters.ts +2 -2
- package/src/shared/filter/filter-query/parser.ts +9 -8
- package/src/shared/filter/power-search/PowerSearch.svelte +1 -1
- package/src/shared/filter/search-filter-bar/SearchFilterBar.svelte +2 -2
- package/src/shared/filter/search-filter-bar/SearchFilterInput.svelte +1 -1
- package/src/shared/filter/search-filter-bar/SearchFilterPredicateEditor.svelte +1 -1
- package/src/shared/filter/search-filter-bar/search-filter-syntax.ts +1 -1
- package/src/shared/forms/autocomplete-input/AutocompleteInput.svelte +19 -7
- package/src/shared/shadcn/command/command-dialog.svelte +1 -1
- package/src/shared/shadcn/command/command-group.svelte +3 -1
- package/src/shared/shadcn/command-view/command-view.svelte +1 -6
- package/src/shared/shadcn/dropdown-menu/dropdown-menu-checkbox-item.svelte +0 -691
- package/src/shared/shadcn/dropdown-menu/dropdown-menu-content.svelte +691 -0
- package/src/shared/shadcn/dropdown-menu/dropdown-menu-radio-group.svelte +1 -1
- package/src/shared/shadcn/scroll-area/scroll-area.svelte +2 -0
- package/src/shared/shadcn/spinner/spinner.svelte +3 -3
- package/src/shared/shell/app-shell/AppShell.demo.css +37 -0
- package/src/shared/shell/app-shell/AppShellBodyContent.svelte +5 -1
- package/src/shared/shell/app-shell/AppShellBodySidebar.svelte +5 -1
- package/src/shared/shell/app-shell/AppShellMobilePanelSelector.svelte +1 -1
- package/src/shared/shell/app-shell/app-shell-controller.svelte.ts +23 -9
- package/src/shared/shell/app-shell/examples/AppShellBodyDemo.svelte +4 -0
- package/src/shared/shell/app-shell/examples/AppShellCompleteDemo.svelte +1 -0
- package/src/shared/shell/app-shell/examples/AppShellConversationDemo.svelte +1 -0
- package/src/shared/shell/app-shell/examples/AppShellExamples.css +521 -0
- package/src/shared/shell/app-shell/examples/AppShellFilesSidebarDemo.svelte +1 -0
- package/src/shared/shell/app-shell/examples/AppShellMarkdownDocumentDemo.svelte +6 -1
- package/src/shared/shell/app-shell/examples/AppShellMarkdownFilesDemo.svelte +1 -0
- package/src/shared/shell/app-shell/examples/AppShellModeSwitchDemo.svelte +1 -0
- package/src/shared/shell/app-shell/examples/AppShellProjectSidebarDemo.svelte +1 -0
- package/src/shared/shell/app-shell/examples/AppShellSidebarDemo.svelte +1 -0
- package/src/shared/shell/app-shell/examples/AppShellToolbarDemo.svelte +1 -0
- package/src/shared/workspace/menu/WorkspaceContextMenuItems.svelte +2 -1
- package/src/shared/workspace/menu/WorkspaceMenuItems.svelte +2 -1
- package/src/shared/workspace/problems/ProblemsQuickFix.svelte +1 -4
- package/src/shared/workspace/settings/AppSettingsContent.svelte +6 -0
- package/src/shared/workspace/settings/AppSettingsNavigation.svelte +7 -0
- package/src/shared/workspace/settings/CopyableValue.svelte +81 -0
- package/src/shared/workspace/settings/DeclarativeSettingsSection.svelte +20 -0
- package/src/shared/workspace/settings/WorkspaceSettingField.svelte +110 -41
- package/src/shared/workspace/settings/WorkspaceSettingMultiSelect.svelte +7 -6
- package/src/shared/workspace/settings/WorkspaceSettingObjectTable.svelte +187 -23
- package/src/shared/workspace/settings/WorkspaceSettingSelect.svelte +29 -29
- package/src/shared/workspace/settings/WorkspaceSettings.css +190 -4
- package/src/shared/workspace/settings/WorkspaceSettingsStoryCustomField.svelte +2 -2
- package/src/shared/workspace/settings/WorkspaceSettingsStoryCustomPage.svelte +13 -0
- package/src/shared/workspace/settings/WorkspaceSettingsSurface.svelte +50 -0
- package/src/shared/workspace/settings/configuration.ts +5 -0
- package/src/shared/workspace/settings/copyable-value.ts +10 -0
- package/src/shared/workspace/settings/index.ts +5 -0
- package/src/shared/workspace/settings/settings-controller.svelte.ts +349 -15
- package/src/shared/workspace/settings/types.ts +90 -8
- package/src/storybook/app-shell-reference/AppShellTwoExpandedSidebarsReference.css +25 -0
- package/src/storybook/app-shell-reference/AppShellTwoExpandedSidebarsReference.svelte +48 -0
- package/src/storybook/app-shell-reference/index.ts +1 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { WorkspaceSettingsController } from "./settings-controller.svelte.js";
|
|
2
2
|
import type {
|
|
3
3
|
WorkspaceSettingOption,
|
|
4
|
+
WorkspaceSettingsDefinition,
|
|
4
5
|
WorkspaceSettingsNavigationGroup,
|
|
5
6
|
WorkspaceSettingsSection,
|
|
6
7
|
} from "./types.js";
|
|
@@ -72,6 +73,10 @@ export class ConfigurationSchema {
|
|
|
72
73
|
return this.register(section);
|
|
73
74
|
}
|
|
74
75
|
|
|
76
|
+
registerDefinition(definition: WorkspaceSettingsDefinition): () => void {
|
|
77
|
+
return this.settings.registerDefinition(definition);
|
|
78
|
+
}
|
|
79
|
+
|
|
75
80
|
registerNavigationGroup(group: WorkspaceSettingsNavigationGroup): () => void {
|
|
76
81
|
return this.settings.registerNavigationGroup(group);
|
|
77
82
|
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export function shortenCopyableValue(
|
|
2
|
+
value: string,
|
|
3
|
+
leadingCharacters = 8,
|
|
4
|
+
trailingCharacters = 4,
|
|
5
|
+
): string {
|
|
6
|
+
const leading = Math.max(1, Math.floor(leadingCharacters));
|
|
7
|
+
const trailing = Math.max(0, Math.floor(trailingCharacters));
|
|
8
|
+
if (value.length <= leading + trailing + 1) return value;
|
|
9
|
+
return `${value.slice(0, leading)}…${trailing ? value.slice(-trailing) : ""}`;
|
|
10
|
+
}
|
|
@@ -5,11 +5,14 @@ import AppSettingsSearch from "./AppSettingsSearch.svelte";
|
|
|
5
5
|
import AppSettingsSection from "./AppSettingsSection.svelte";
|
|
6
6
|
import AppShellHotkeySettings from "./AppShellHotkeySettings.svelte";
|
|
7
7
|
import AppShellPluginsSettings from "./AppShellPluginsSettings.svelte";
|
|
8
|
+
import CopyableValue from "./CopyableValue.svelte";
|
|
9
|
+
import DeclarativeSettingsSection from "./DeclarativeSettingsSection.svelte";
|
|
8
10
|
import WorkspaceSettingField from "./WorkspaceSettingField.svelte";
|
|
9
11
|
import WorkspaceSettingList from "./WorkspaceSettingList.svelte";
|
|
10
12
|
import WorkspaceSettingsSurface from "./WorkspaceSettingsSurface.svelte";
|
|
11
13
|
|
|
12
14
|
export * from "./configuration.js";
|
|
15
|
+
export * from "./copyable-value.js";
|
|
13
16
|
export * from "./persistence.js";
|
|
14
17
|
export * from "./settings-controller.svelte.js";
|
|
15
18
|
export * from "./types.js";
|
|
@@ -22,6 +25,8 @@ export {
|
|
|
22
25
|
AppSettingsSection,
|
|
23
26
|
AppShellHotkeySettings,
|
|
24
27
|
AppShellPluginsSettings,
|
|
28
|
+
CopyableValue,
|
|
29
|
+
DeclarativeSettingsSection,
|
|
25
30
|
WorkspaceSettingField,
|
|
26
31
|
WorkspaceSettingList,
|
|
27
32
|
WorkspaceSettingsSurface,
|
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
} from "../core/event-dispatcher.js";
|
|
5
5
|
import type {
|
|
6
6
|
WorkspaceActionSetting,
|
|
7
|
+
WorkspaceCustomFieldAdapter,
|
|
7
8
|
WorkspaceSettingField,
|
|
8
9
|
WorkspaceSettingGroup,
|
|
9
10
|
WorkspaceSettingOption,
|
|
@@ -12,8 +13,10 @@ import type {
|
|
|
12
13
|
WorkspaceSettingsEventMap,
|
|
13
14
|
WorkspaceSettingsNavigationGroup,
|
|
14
15
|
WorkspaceSettingsSearchResult,
|
|
16
|
+
WorkspaceSettingsSearchEntry,
|
|
15
17
|
WorkspaceSettingsSection,
|
|
16
18
|
WorkspaceSettingsSnapshotV1,
|
|
19
|
+
WorkspaceSettingsDefinition,
|
|
17
20
|
} from "./types.js";
|
|
18
21
|
|
|
19
22
|
interface IndexedSetting {
|
|
@@ -75,13 +78,27 @@ function collectSettingGroups(
|
|
|
75
78
|
}
|
|
76
79
|
}
|
|
77
80
|
|
|
81
|
+
function sectionSearchEntries(
|
|
82
|
+
section: WorkspaceSettingsSection,
|
|
83
|
+
): readonly WorkspaceSettingsSearchEntry[] {
|
|
84
|
+
return typeof section.searchEntries === "function"
|
|
85
|
+
? section.searchEntries()
|
|
86
|
+
: (section.searchEntries ?? []);
|
|
87
|
+
}
|
|
88
|
+
|
|
78
89
|
function defaultValues(
|
|
79
90
|
sections: WorkspaceSettingsSection[],
|
|
80
91
|
): Record<string, unknown> {
|
|
81
92
|
const values: Record<string, unknown> = {};
|
|
82
93
|
for (const section of sections) {
|
|
83
94
|
for (const { field } of flattenFields(section)) {
|
|
84
|
-
if (
|
|
95
|
+
if (
|
|
96
|
+
field.type !== "action" &&
|
|
97
|
+
field.type !== "output" &&
|
|
98
|
+
field.type !== "unsupported" &&
|
|
99
|
+
"default" in field &&
|
|
100
|
+
field.default !== undefined
|
|
101
|
+
) {
|
|
85
102
|
values[field.id] = cloneValue(field.default);
|
|
86
103
|
}
|
|
87
104
|
}
|
|
@@ -94,6 +111,7 @@ function validateValue(
|
|
|
94
111
|
value: unknown,
|
|
95
112
|
): string | null {
|
|
96
113
|
if (field.type === "action") return "Actions do not store values";
|
|
114
|
+
if (field.type === "output") return null;
|
|
97
115
|
if (field.type === "boolean") {
|
|
98
116
|
return typeof value === "boolean" ? null : "Expected a boolean value";
|
|
99
117
|
}
|
|
@@ -199,7 +217,9 @@ function validateValue(
|
|
|
199
217
|
? null
|
|
200
218
|
: "Every key and value must be a string";
|
|
201
219
|
}
|
|
202
|
-
if (field.type === "custom")
|
|
220
|
+
if (field.type === "custom") {
|
|
221
|
+
return field.validate?.(value) ?? null;
|
|
222
|
+
}
|
|
203
223
|
if (field.type === "unsupported") return null;
|
|
204
224
|
if (field.type !== "list") return "Unsupported setting value";
|
|
205
225
|
if (!Array.isArray(value)) return "Expected a list";
|
|
@@ -229,6 +249,12 @@ export class WorkspaceSettingsController {
|
|
|
229
249
|
dirty = $state(false);
|
|
230
250
|
saving = $state(false);
|
|
231
251
|
validationErrors = $state<Record<string, string>>({});
|
|
252
|
+
sourceErrors = $state<Record<string, string>>({});
|
|
253
|
+
sourceBusy = $state<Record<string, boolean>>({});
|
|
254
|
+
sourceRevision = $state(0);
|
|
255
|
+
actionResults = $state<
|
|
256
|
+
Record<string, { tone: "success" | "warning" | "error"; message: string }>
|
|
257
|
+
>({});
|
|
232
258
|
selectedSectionId = $state("");
|
|
233
259
|
dialogOpen = $state(false);
|
|
234
260
|
revealFieldId = $state<string | null>(null);
|
|
@@ -240,6 +266,15 @@ export class WorkspaceSettingsController {
|
|
|
240
266
|
#pendingSaveEvent: WorkspaceSettingsChangeEvent | null = null;
|
|
241
267
|
#saveChain: Promise<void> = Promise.resolve();
|
|
242
268
|
#hydrating = false;
|
|
269
|
+
readonly #definitions = new Map<
|
|
270
|
+
string,
|
|
271
|
+
{
|
|
272
|
+
definition: WorkspaceSettingsDefinition;
|
|
273
|
+
disposeSource: () => void;
|
|
274
|
+
}
|
|
275
|
+
>();
|
|
276
|
+
readonly #sourceWriteChains = new Map<string, Promise<boolean>>();
|
|
277
|
+
readonly #sourceWriteVersions = new Map<string, number>();
|
|
243
278
|
#optionSourceLoader?: (
|
|
244
279
|
sourceId: string,
|
|
245
280
|
context: {
|
|
@@ -284,6 +319,7 @@ export class WorkspaceSettingsController {
|
|
|
284
319
|
}
|
|
285
320
|
|
|
286
321
|
registerSection(section: WorkspaceSettingsSection): () => void {
|
|
322
|
+
this.#disposeDefinition(section.id);
|
|
287
323
|
this.sections = [
|
|
288
324
|
...this.sections.filter((candidate) => candidate.id !== section.id),
|
|
289
325
|
section,
|
|
@@ -298,6 +334,36 @@ export class WorkspaceSettingsController {
|
|
|
298
334
|
return () => this.unregisterSection(section.id);
|
|
299
335
|
}
|
|
300
336
|
|
|
337
|
+
registerDefinition(definition: WorkspaceSettingsDefinition): () => void {
|
|
338
|
+
const { section, source } = definition;
|
|
339
|
+
this.unregisterSection(section.id);
|
|
340
|
+
this.sections = [...this.sections, section].sort(
|
|
341
|
+
(a, b) => (a.order ?? 0) - (b.order ?? 0),
|
|
342
|
+
);
|
|
343
|
+
const defaults = defaultValues([section]);
|
|
344
|
+
for (const [id, value] of Object.entries(defaults)) {
|
|
345
|
+
if (!(id in this.values)) this.values[id] = value;
|
|
346
|
+
}
|
|
347
|
+
this.#definitions.set(section.id, {
|
|
348
|
+
definition,
|
|
349
|
+
disposeSource: () => undefined,
|
|
350
|
+
});
|
|
351
|
+
this.#syncDefinition(definition);
|
|
352
|
+
const disposeSource = source.subscribe((fieldId) => {
|
|
353
|
+
this.sourceRevision += 1;
|
|
354
|
+
this.#syncDefinition(definition, fieldId);
|
|
355
|
+
});
|
|
356
|
+
this.#definitions.set(section.id, { definition, disposeSource });
|
|
357
|
+
this.#validateAll();
|
|
358
|
+
if (!this.selectedSectionId) this.selectedSectionId = section.id;
|
|
359
|
+
this.#events.trigger("schema-change");
|
|
360
|
+
return () => {
|
|
361
|
+
if (this.#definitions.get(section.id)?.definition === definition) {
|
|
362
|
+
this.unregisterSection(section.id);
|
|
363
|
+
}
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
|
|
301
367
|
registerNavigationGroup(group: WorkspaceSettingsNavigationGroup): () => void {
|
|
302
368
|
if (this.navigationGroups.some((candidate) => candidate.id === group.id)) {
|
|
303
369
|
throw new Error(
|
|
@@ -343,6 +409,7 @@ export class WorkspaceSettingsController {
|
|
|
343
409
|
}
|
|
344
410
|
|
|
345
411
|
unregisterSection(sectionId: string): void {
|
|
412
|
+
this.#disposeDefinition(sectionId);
|
|
346
413
|
const next = this.sections.filter((section) => section.id !== sectionId);
|
|
347
414
|
if (next.length === this.sections.length) return;
|
|
348
415
|
this.sections = next;
|
|
@@ -357,6 +424,44 @@ export class WorkspaceSettingsController {
|
|
|
357
424
|
return this.values[id] as T | undefined;
|
|
358
425
|
}
|
|
359
426
|
|
|
427
|
+
isBusy(id: string): boolean {
|
|
428
|
+
this.sourceRevision;
|
|
429
|
+
const indexed = this.#field(id);
|
|
430
|
+
return (
|
|
431
|
+
this.sourceBusy[id] === true ||
|
|
432
|
+
(indexed?.field.type === "action" && indexed.field.isBusy?.() === true)
|
|
433
|
+
);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
isDisabled(id: string): boolean {
|
|
437
|
+
this.sourceRevision;
|
|
438
|
+
const indexed = this.#field(id);
|
|
439
|
+
return Boolean(
|
|
440
|
+
indexed?.field.disabled ||
|
|
441
|
+
indexed?.field.readOnly ||
|
|
442
|
+
(indexed?.field.type === "action" && indexed.field.isDisabled?.()),
|
|
443
|
+
);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
getActionResult(id: string) {
|
|
447
|
+
return this.actionResults[id];
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
getError(id: string): string | undefined {
|
|
451
|
+
return this.validationErrors[id] ?? this.sourceErrors[id];
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
resolveCustomAdapter(
|
|
455
|
+
fieldId: string,
|
|
456
|
+
adapterId: string,
|
|
457
|
+
): WorkspaceCustomFieldAdapter | undefined {
|
|
458
|
+
const indexed = this.#field(fieldId);
|
|
459
|
+
if (!indexed) return undefined;
|
|
460
|
+
return this.#definitions
|
|
461
|
+
.get(indexed.section.id)
|
|
462
|
+
?.definition.adapters?.find((adapter) => adapter.id === adapterId);
|
|
463
|
+
}
|
|
464
|
+
|
|
360
465
|
selectSection(sectionId: string): boolean {
|
|
361
466
|
if (!this.sections.some((section) => section.id === sectionId)) {
|
|
362
467
|
return false;
|
|
@@ -371,10 +476,26 @@ export class WorkspaceSettingsController {
|
|
|
371
476
|
if (indexed) {
|
|
372
477
|
this.selectedSectionId = indexed.section.id;
|
|
373
478
|
this.revealFieldId = options.fieldId;
|
|
374
|
-
} else if (options.sectionId && !this.selectSection(options.sectionId)) {
|
|
375
|
-
return false;
|
|
376
479
|
} else {
|
|
377
|
-
|
|
480
|
+
const section = options.sectionId
|
|
481
|
+
? this.sections.find(
|
|
482
|
+
(candidate) => candidate.id === options.sectionId,
|
|
483
|
+
)
|
|
484
|
+
: this.sections.find((candidate) =>
|
|
485
|
+
sectionSearchEntries(candidate).some(
|
|
486
|
+
(entry) => entry.id === options.fieldId,
|
|
487
|
+
),
|
|
488
|
+
);
|
|
489
|
+
if (
|
|
490
|
+
!section ||
|
|
491
|
+
!sectionSearchEntries(section).some(
|
|
492
|
+
(entry) => entry.id === options.fieldId,
|
|
493
|
+
)
|
|
494
|
+
) {
|
|
495
|
+
return false;
|
|
496
|
+
}
|
|
497
|
+
this.selectedSectionId = section.id;
|
|
498
|
+
this.revealFieldId = options.fieldId;
|
|
378
499
|
}
|
|
379
500
|
} else if (options.sectionId && !this.selectSection(options.sectionId)) {
|
|
380
501
|
return false;
|
|
@@ -423,7 +544,13 @@ export class WorkspaceSettingsController {
|
|
|
423
544
|
|
|
424
545
|
update(id: string, value: unknown): boolean {
|
|
425
546
|
const indexed = this.#field(id);
|
|
426
|
-
if (
|
|
547
|
+
if (
|
|
548
|
+
!indexed ||
|
|
549
|
+
indexed.field.type === "action" ||
|
|
550
|
+
indexed.field.type === "output" ||
|
|
551
|
+
indexed.field.disabled ||
|
|
552
|
+
indexed.field.readOnly
|
|
553
|
+
) {
|
|
427
554
|
return false;
|
|
428
555
|
}
|
|
429
556
|
const error = validateValue(indexed.field, value);
|
|
@@ -432,15 +559,98 @@ export class WorkspaceSettingsController {
|
|
|
432
559
|
this.#events.trigger("validation-error", { id, value, message: error });
|
|
433
560
|
return false;
|
|
434
561
|
}
|
|
562
|
+
if (this.#definitions.has(indexed.section.id)) {
|
|
563
|
+
void this.set(id, value);
|
|
564
|
+
return true;
|
|
565
|
+
}
|
|
435
566
|
delete this.validationErrors[id];
|
|
567
|
+
delete this.sourceErrors[id];
|
|
436
568
|
this.values[id] = cloneValue(value);
|
|
437
569
|
this.#changed({ source: "update", id });
|
|
438
570
|
return true;
|
|
439
571
|
}
|
|
440
572
|
|
|
573
|
+
async set(id: string, value: unknown): Promise<boolean> {
|
|
574
|
+
const indexed = this.#field(id);
|
|
575
|
+
if (
|
|
576
|
+
!indexed ||
|
|
577
|
+
indexed.field.type === "action" ||
|
|
578
|
+
indexed.field.type === "output" ||
|
|
579
|
+
indexed.field.disabled ||
|
|
580
|
+
indexed.field.readOnly
|
|
581
|
+
) {
|
|
582
|
+
return false;
|
|
583
|
+
}
|
|
584
|
+
const error = validateValue(indexed.field, value);
|
|
585
|
+
if (error) {
|
|
586
|
+
this.validationErrors[id] = error;
|
|
587
|
+
this.#events.trigger("validation-error", { id, value, message: error });
|
|
588
|
+
return false;
|
|
589
|
+
}
|
|
590
|
+
const registration = this.#definitions.get(indexed.section.id);
|
|
591
|
+
if (!registration) return this.update(id, value);
|
|
592
|
+
|
|
593
|
+
delete this.validationErrors[id];
|
|
594
|
+
delete this.sourceErrors[id];
|
|
595
|
+
const previous = this.values[id];
|
|
596
|
+
const candidate = cloneValue(value);
|
|
597
|
+
const version = (this.#sourceWriteVersions.get(id) ?? 0) + 1;
|
|
598
|
+
this.#sourceWriteVersions.set(id, version);
|
|
599
|
+
this.values[id] = candidate;
|
|
600
|
+
this.sourceBusy[id] = true;
|
|
601
|
+
this.#events.trigger("change", { source: "source", id });
|
|
602
|
+
|
|
603
|
+
const prior = this.#sourceWriteChains.get(id) ?? Promise.resolve(true);
|
|
604
|
+
const task = prior
|
|
605
|
+
.catch(() => false)
|
|
606
|
+
.then(async () => {
|
|
607
|
+
try {
|
|
608
|
+
const canonical = await registration.definition.source.set(
|
|
609
|
+
id,
|
|
610
|
+
candidate,
|
|
611
|
+
);
|
|
612
|
+
if (this.#sourceWriteVersions.get(id) === version) {
|
|
613
|
+
const resolved =
|
|
614
|
+
canonical === undefined
|
|
615
|
+
? registration.definition.source.get(id)
|
|
616
|
+
: canonical;
|
|
617
|
+
if (resolved !== undefined) this.values[id] = cloneValue(resolved);
|
|
618
|
+
}
|
|
619
|
+
return true;
|
|
620
|
+
} catch (caught) {
|
|
621
|
+
if (this.#sourceWriteVersions.get(id) === version) {
|
|
622
|
+
this.values[id] = previous;
|
|
623
|
+
this.sourceErrors[id] =
|
|
624
|
+
caught instanceof Error
|
|
625
|
+
? caught.message
|
|
626
|
+
: "Unable to update this setting";
|
|
627
|
+
}
|
|
628
|
+
this.#events.trigger("source-error", { id, error: caught });
|
|
629
|
+
return false;
|
|
630
|
+
} finally {
|
|
631
|
+
if (this.#sourceWriteVersions.get(id) === version) {
|
|
632
|
+
this.sourceBusy[id] = false;
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
});
|
|
636
|
+
this.#sourceWriteChains.set(id, task);
|
|
637
|
+
return task;
|
|
638
|
+
}
|
|
639
|
+
|
|
441
640
|
restoreDefault(id: string): boolean {
|
|
442
641
|
const indexed = this.#field(id);
|
|
443
|
-
if (
|
|
642
|
+
if (
|
|
643
|
+
!indexed ||
|
|
644
|
+
indexed.field.type === "action" ||
|
|
645
|
+
indexed.field.type === "output" ||
|
|
646
|
+
!("default" in indexed.field) ||
|
|
647
|
+
indexed.field.default === undefined
|
|
648
|
+
) {
|
|
649
|
+
return false;
|
|
650
|
+
}
|
|
651
|
+
if (this.#definitions.has(indexed.section.id)) {
|
|
652
|
+
return this.update(id, cloneValue(indexed.field.default));
|
|
653
|
+
}
|
|
444
654
|
delete this.validationErrors[id];
|
|
445
655
|
this.values[id] = cloneValue(indexed.field.default);
|
|
446
656
|
this.#changed({ source: "restore-default", id });
|
|
@@ -449,15 +659,54 @@ export class WorkspaceSettingsController {
|
|
|
449
659
|
|
|
450
660
|
async runAction(id: string): Promise<boolean> {
|
|
451
661
|
const indexed = this.#field(id);
|
|
452
|
-
if (
|
|
662
|
+
if (
|
|
663
|
+
!indexed ||
|
|
664
|
+
indexed.field.type !== "action" ||
|
|
665
|
+
this.isDisabled(id) ||
|
|
666
|
+
this.sourceBusy[id]
|
|
667
|
+
) {
|
|
453
668
|
return false;
|
|
454
669
|
}
|
|
455
|
-
|
|
456
|
-
return
|
|
670
|
+
const action = indexed.field as WorkspaceActionSetting;
|
|
671
|
+
if (action.confirm && !(await action.confirm())) return false;
|
|
672
|
+
delete this.actionResults[id];
|
|
673
|
+
this.sourceBusy[id] = true;
|
|
674
|
+
try {
|
|
675
|
+
const result = await action.run();
|
|
676
|
+
if (result) this.actionResults[id] = result;
|
|
677
|
+
return true;
|
|
678
|
+
} catch (caught) {
|
|
679
|
+
this.actionResults[id] = {
|
|
680
|
+
tone: "error",
|
|
681
|
+
message:
|
|
682
|
+
caught instanceof Error
|
|
683
|
+
? caught.message
|
|
684
|
+
: "Unable to run this action",
|
|
685
|
+
};
|
|
686
|
+
return false;
|
|
687
|
+
} finally {
|
|
688
|
+
this.sourceBusy[id] = false;
|
|
689
|
+
}
|
|
457
690
|
}
|
|
458
691
|
|
|
459
692
|
getSnapshot(): WorkspaceSettingsSnapshotV1 {
|
|
460
|
-
|
|
693
|
+
const values: Record<string, unknown> = {};
|
|
694
|
+
for (const section of this.sections) {
|
|
695
|
+
if (this.#definitions.has(section.id)) continue;
|
|
696
|
+
for (const { field } of flattenFields(section)) {
|
|
697
|
+
if (
|
|
698
|
+
field.type === "action" ||
|
|
699
|
+
field.type === "output" ||
|
|
700
|
+
field.type === "unsupported"
|
|
701
|
+
) {
|
|
702
|
+
continue;
|
|
703
|
+
}
|
|
704
|
+
if (field.id in this.values) {
|
|
705
|
+
values[field.id] = cloneValue(this.values[field.id]);
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
return { version: 1, values };
|
|
461
710
|
}
|
|
462
711
|
|
|
463
712
|
changeSnapshot(value: unknown): void {
|
|
@@ -471,17 +720,21 @@ export class WorkspaceSettingsController {
|
|
|
471
720
|
record.values !== null
|
|
472
721
|
? record.values
|
|
473
722
|
: {};
|
|
474
|
-
|
|
723
|
+
const sourceValues = this.#sourceValues();
|
|
724
|
+
this.values = { ...defaultValues(this.#controllerSections()) };
|
|
475
725
|
for (const [id, candidate] of Object.entries(incoming)) {
|
|
476
726
|
const indexed = this.#field(id);
|
|
477
727
|
if (
|
|
478
728
|
indexed &&
|
|
729
|
+
!this.#definitions.has(indexed.section.id) &&
|
|
479
730
|
indexed.field.type !== "action" &&
|
|
731
|
+
indexed.field.type !== "output" &&
|
|
480
732
|
validateValue(indexed.field, candidate) === null
|
|
481
733
|
) {
|
|
482
734
|
this.values[id] = cloneValue(candidate);
|
|
483
735
|
}
|
|
484
736
|
}
|
|
737
|
+
Object.assign(this.values, sourceValues);
|
|
485
738
|
this.#validateAll();
|
|
486
739
|
this.#changed({ source: "replace" });
|
|
487
740
|
}
|
|
@@ -494,17 +747,21 @@ export class WorkspaceSettingsController {
|
|
|
494
747
|
if (value !== null && value !== undefined) {
|
|
495
748
|
const record = value as Partial<WorkspaceSettingsSnapshotV1>;
|
|
496
749
|
if (record.version === 1 && record.values) {
|
|
497
|
-
|
|
750
|
+
const sourceValues = this.#sourceValues();
|
|
751
|
+
this.values = { ...defaultValues(this.#controllerSections()) };
|
|
498
752
|
for (const [id, candidate] of Object.entries(record.values)) {
|
|
499
753
|
const indexed = this.#field(id);
|
|
500
754
|
if (
|
|
501
755
|
indexed &&
|
|
756
|
+
!this.#definitions.has(indexed.section.id) &&
|
|
502
757
|
indexed.field.type !== "action" &&
|
|
758
|
+
indexed.field.type !== "output" &&
|
|
503
759
|
validateValue(indexed.field, candidate) === null
|
|
504
760
|
) {
|
|
505
761
|
this.values[id] = cloneValue(candidate);
|
|
506
762
|
}
|
|
507
763
|
}
|
|
764
|
+
Object.assign(this.values, sourceValues);
|
|
508
765
|
}
|
|
509
766
|
}
|
|
510
767
|
if (this.#persistence) {
|
|
@@ -585,6 +842,20 @@ export class WorkspaceSettingsController {
|
|
|
585
842
|
score: text.startsWith(normalized) ? 0 : 1,
|
|
586
843
|
});
|
|
587
844
|
}
|
|
845
|
+
for (const entry of sectionSearchEntries(section)) {
|
|
846
|
+
const path = [section.title, ...(entry.path ?? [entry.title])];
|
|
847
|
+
const text =
|
|
848
|
+
`${entry.title} ${entry.description ?? ""} ${entry.keywords?.join(" ") ?? ""} ${path.join(" ")}`.toLocaleLowerCase();
|
|
849
|
+
if (!text.includes(normalized)) continue;
|
|
850
|
+
results.push({
|
|
851
|
+
sectionId: section.id,
|
|
852
|
+
fieldId: entry.id,
|
|
853
|
+
title: entry.title,
|
|
854
|
+
description: entry.description,
|
|
855
|
+
path,
|
|
856
|
+
score: text.startsWith(normalized) ? 0 : 1,
|
|
857
|
+
});
|
|
858
|
+
}
|
|
588
859
|
}
|
|
589
860
|
return results.sort(
|
|
590
861
|
(a, b) => a.score - b.score || a.title.localeCompare(b.title),
|
|
@@ -594,6 +865,9 @@ export class WorkspaceSettingsController {
|
|
|
594
865
|
destroy(): void {
|
|
595
866
|
if (this.#saveTimer) clearTimeout(this.#saveTimer);
|
|
596
867
|
this.close();
|
|
868
|
+
for (const sectionId of [...this.#definitions.keys()]) {
|
|
869
|
+
this.#disposeDefinition(sectionId);
|
|
870
|
+
}
|
|
597
871
|
this.#events.clear();
|
|
598
872
|
}
|
|
599
873
|
|
|
@@ -613,14 +887,74 @@ export class WorkspaceSettingsController {
|
|
|
613
887
|
return null;
|
|
614
888
|
}
|
|
615
889
|
|
|
890
|
+
#controllerSections(): WorkspaceSettingsSection[] {
|
|
891
|
+
return this.sections.filter(
|
|
892
|
+
(section) => !this.#definitions.has(section.id),
|
|
893
|
+
);
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
#sourceValues(): Record<string, unknown> {
|
|
897
|
+
const result: Record<string, unknown> = {};
|
|
898
|
+
for (const { definition } of this.#definitions.values()) {
|
|
899
|
+
for (const { field } of flattenFields(definition.section)) {
|
|
900
|
+
if (field.type === "action" || field.type === "unsupported") continue;
|
|
901
|
+
const value = definition.source.get(field.id);
|
|
902
|
+
if (value !== undefined) result[field.id] = cloneValue(value);
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
return result;
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
#syncDefinition(
|
|
909
|
+
definition: WorkspaceSettingsDefinition,
|
|
910
|
+
fieldId?: string,
|
|
911
|
+
): void {
|
|
912
|
+
for (const { field } of flattenFields(definition.section)) {
|
|
913
|
+
if (
|
|
914
|
+
(fieldId && field.id !== fieldId) ||
|
|
915
|
+
field.type === "action" ||
|
|
916
|
+
field.type === "unsupported" ||
|
|
917
|
+
this.sourceBusy[field.id]
|
|
918
|
+
) {
|
|
919
|
+
continue;
|
|
920
|
+
}
|
|
921
|
+
const value = definition.source.get(field.id);
|
|
922
|
+
if (value === undefined) continue;
|
|
923
|
+
const error = validateValue(field, value);
|
|
924
|
+
if (error) {
|
|
925
|
+
this.validationErrors[field.id] = error;
|
|
926
|
+
continue;
|
|
927
|
+
}
|
|
928
|
+
delete this.validationErrors[field.id];
|
|
929
|
+
delete this.sourceErrors[field.id];
|
|
930
|
+
this.values[field.id] = cloneValue(value);
|
|
931
|
+
this.#events.trigger("change", { source: "source", id: field.id });
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
#disposeDefinition(sectionId: string): void {
|
|
936
|
+
const registration = this.#definitions.get(sectionId);
|
|
937
|
+
if (!registration) return;
|
|
938
|
+
registration.disposeSource();
|
|
939
|
+
this.#definitions.delete(sectionId);
|
|
940
|
+
}
|
|
941
|
+
|
|
616
942
|
#validateAll(): void {
|
|
617
943
|
const next: Record<string, string> = {};
|
|
618
944
|
for (const section of this.sections) {
|
|
619
945
|
for (const { field } of flattenFields(section)) {
|
|
620
|
-
if (
|
|
946
|
+
if (
|
|
947
|
+
field.type === "action" ||
|
|
948
|
+
field.type === "output" ||
|
|
949
|
+
this.#definitions.has(section.id)
|
|
950
|
+
) {
|
|
951
|
+
continue;
|
|
952
|
+
}
|
|
621
953
|
const error = validateValue(field, this.values[field.id]);
|
|
622
954
|
if (error) {
|
|
623
|
-
|
|
955
|
+
if ("default" in field && field.default !== undefined) {
|
|
956
|
+
this.values[field.id] = cloneValue(field.default);
|
|
957
|
+
}
|
|
624
958
|
}
|
|
625
959
|
}
|
|
626
960
|
}
|