@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.
Files changed (67) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +1 -1
  3. package/package.json +12 -8
  4. package/spec/src/styling-and-themes.md +9 -9
  5. package/src/shared/ai/chat.css +2 -1
  6. package/src/shared/ai/composer-input/ComposerInput.svelte +3 -2
  7. package/src/shared/ai/experimental/reaction-bar/ReactionBar.svelte +1 -1
  8. package/src/shared/ai/tool-calls/ToolCalls.svelte +1 -1
  9. package/src/shared/ai/trigger-menu.ts +2 -3
  10. package/src/shared/diff/file-diff/FileDiff.svelte +5 -8
  11. package/src/shared/diff/file-listing/FileListingViewModeToggle.css +42 -5
  12. package/src/shared/diff/merge-editor/MergeEditor.svelte +2 -7
  13. package/src/shared/filter/filter-query/filters.ts +2 -2
  14. package/src/shared/filter/filter-query/parser.ts +9 -8
  15. package/src/shared/filter/power-search/PowerSearch.svelte +1 -1
  16. package/src/shared/filter/search-filter-bar/SearchFilterBar.svelte +2 -2
  17. package/src/shared/filter/search-filter-bar/SearchFilterInput.svelte +1 -1
  18. package/src/shared/filter/search-filter-bar/SearchFilterPredicateEditor.svelte +1 -1
  19. package/src/shared/filter/search-filter-bar/search-filter-syntax.ts +1 -1
  20. package/src/shared/forms/autocomplete-input/AutocompleteInput.svelte +19 -7
  21. package/src/shared/shadcn/command/command-dialog.svelte +1 -1
  22. package/src/shared/shadcn/command/command-group.svelte +3 -1
  23. package/src/shared/shadcn/command-view/command-view.svelte +1 -6
  24. package/src/shared/shadcn/dropdown-menu/dropdown-menu-checkbox-item.svelte +0 -691
  25. package/src/shared/shadcn/dropdown-menu/dropdown-menu-content.svelte +691 -0
  26. package/src/shared/shadcn/dropdown-menu/dropdown-menu-radio-group.svelte +1 -1
  27. package/src/shared/shadcn/scroll-area/scroll-area.svelte +2 -0
  28. package/src/shared/shadcn/spinner/spinner.svelte +3 -3
  29. package/src/shared/shell/app-shell/AppShell.demo.css +37 -0
  30. package/src/shared/shell/app-shell/AppShellBodyContent.svelte +5 -1
  31. package/src/shared/shell/app-shell/AppShellBodySidebar.svelte +5 -1
  32. package/src/shared/shell/app-shell/AppShellMobilePanelSelector.svelte +1 -1
  33. package/src/shared/shell/app-shell/app-shell-controller.svelte.ts +23 -9
  34. package/src/shared/shell/app-shell/examples/AppShellBodyDemo.svelte +4 -0
  35. package/src/shared/shell/app-shell/examples/AppShellCompleteDemo.svelte +1 -0
  36. package/src/shared/shell/app-shell/examples/AppShellConversationDemo.svelte +1 -0
  37. package/src/shared/shell/app-shell/examples/AppShellExamples.css +521 -0
  38. package/src/shared/shell/app-shell/examples/AppShellFilesSidebarDemo.svelte +1 -0
  39. package/src/shared/shell/app-shell/examples/AppShellMarkdownDocumentDemo.svelte +6 -1
  40. package/src/shared/shell/app-shell/examples/AppShellMarkdownFilesDemo.svelte +1 -0
  41. package/src/shared/shell/app-shell/examples/AppShellModeSwitchDemo.svelte +1 -0
  42. package/src/shared/shell/app-shell/examples/AppShellProjectSidebarDemo.svelte +1 -0
  43. package/src/shared/shell/app-shell/examples/AppShellSidebarDemo.svelte +1 -0
  44. package/src/shared/shell/app-shell/examples/AppShellToolbarDemo.svelte +1 -0
  45. package/src/shared/workspace/menu/WorkspaceContextMenuItems.svelte +2 -1
  46. package/src/shared/workspace/menu/WorkspaceMenuItems.svelte +2 -1
  47. package/src/shared/workspace/problems/ProblemsQuickFix.svelte +1 -4
  48. package/src/shared/workspace/settings/AppSettingsContent.svelte +6 -0
  49. package/src/shared/workspace/settings/AppSettingsNavigation.svelte +7 -0
  50. package/src/shared/workspace/settings/CopyableValue.svelte +81 -0
  51. package/src/shared/workspace/settings/DeclarativeSettingsSection.svelte +20 -0
  52. package/src/shared/workspace/settings/WorkspaceSettingField.svelte +110 -41
  53. package/src/shared/workspace/settings/WorkspaceSettingMultiSelect.svelte +7 -6
  54. package/src/shared/workspace/settings/WorkspaceSettingObjectTable.svelte +187 -23
  55. package/src/shared/workspace/settings/WorkspaceSettingSelect.svelte +29 -29
  56. package/src/shared/workspace/settings/WorkspaceSettings.css +190 -4
  57. package/src/shared/workspace/settings/WorkspaceSettingsStoryCustomField.svelte +2 -2
  58. package/src/shared/workspace/settings/WorkspaceSettingsStoryCustomPage.svelte +13 -0
  59. package/src/shared/workspace/settings/WorkspaceSettingsSurface.svelte +50 -0
  60. package/src/shared/workspace/settings/configuration.ts +5 -0
  61. package/src/shared/workspace/settings/copyable-value.ts +10 -0
  62. package/src/shared/workspace/settings/index.ts +5 -0
  63. package/src/shared/workspace/settings/settings-controller.svelte.ts +349 -15
  64. package/src/shared/workspace/settings/types.ts +90 -8
  65. package/src/storybook/app-shell-reference/AppShellTwoExpandedSidebarsReference.css +25 -0
  66. package/src/storybook/app-shell-reference/AppShellTwoExpandedSidebarsReference.svelte +48 -0
  67. 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 (field.type !== "action" && field.type !== "unsupported") {
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") return null;
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
- this.revealFieldId = null;
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 (!indexed || indexed.field.type === "action" || indexed.field.disabled) {
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 (!indexed || indexed.field.type === "action") return false;
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 (!indexed || indexed.field.type !== "action" || indexed.field.disabled) {
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
- await (indexed.field as WorkspaceActionSetting).run();
456
- return true;
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
- return { version: 1, values: cloneValue(this.values) };
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
- this.values = { ...defaultValues(this.sections) };
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
- this.values = { ...defaultValues(this.sections) };
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 (field.type === "action") continue;
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
- this.values[field.id] = cloneValue(field.default);
955
+ if ("default" in field && field.default !== undefined) {
956
+ this.values[field.id] = cloneValue(field.default);
957
+ }
624
958
  }
625
959
  }
626
960
  }