@jini-ai/ui 0.4.3 → 0.4.5

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 (169) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/README.md +20 -0
  3. package/dist/core/theme/default.d.ts +4 -0
  4. package/dist/core/theme/default.js +11 -0
  5. package/dist/core/theme/stylesheet.d.ts +8 -0
  6. package/dist/core/theme/stylesheet.js +14 -0
  7. package/dist/core/theme/types.d.ts +39 -0
  8. package/dist/core/theme/types.js +2 -0
  9. package/dist/core/theme/validation.d.ts +15 -0
  10. package/dist/core/theme/validation.js +55 -0
  11. package/dist/features/admin-widgets/components/Select/Select.d.ts +11 -3
  12. package/dist/features/admin-widgets/components/Select/Select.hooks.d.ts +8 -13
  13. package/dist/features/admin-widgets/components/Select/Select.hooks.js +37 -215
  14. package/dist/features/admin-widgets/components/Select/Select.js +4 -3
  15. package/dist/features/appearance/constants.js +1 -1
  16. package/dist/features/connectors/rules.d.ts +1 -6
  17. package/dist/features/connectors/types.d.ts +1 -6
  18. package/dist/features/connectors/types.js +1 -6
  19. package/dist/features/execution/http-adapter.d.ts +111 -0
  20. package/dist/features/execution/http-adapter.js +160 -0
  21. package/dist/features/execution/index.d.ts +4 -0
  22. package/dist/features/execution/index.js +2 -0
  23. package/dist/features/execution/react/hooks/use-execution-credential.hooks.d.ts +232 -0
  24. package/dist/features/execution/react/hooks/use-execution-credential.hooks.js +217 -0
  25. package/dist/features/html-editor/canvas-style.d.ts +1 -1
  26. package/dist/features/html-editor/canvas-style.js +2 -2
  27. package/dist/features/i18n/dictionaries/settings-dialog.ar.js +2 -0
  28. package/dist/features/i18n/dictionaries/settings-dialog.bn.js +2 -0
  29. package/dist/features/i18n/dictionaries/settings-dialog.de.js +2 -0
  30. package/dist/features/i18n/dictionaries/settings-dialog.en.js +2 -0
  31. package/dist/features/i18n/dictionaries/settings-dialog.es.js +2 -0
  32. package/dist/features/i18n/dictionaries/settings-dialog.fa.js +2 -0
  33. package/dist/features/i18n/dictionaries/settings-dialog.fr.js +2 -0
  34. package/dist/features/i18n/dictionaries/settings-dialog.hi.js +2 -0
  35. package/dist/features/i18n/dictionaries/settings-dialog.hu.js +2 -0
  36. package/dist/features/i18n/dictionaries/settings-dialog.id.js +2 -0
  37. package/dist/features/i18n/dictionaries/settings-dialog.it.js +2 -0
  38. package/dist/features/i18n/dictionaries/settings-dialog.ja.js +2 -0
  39. package/dist/features/i18n/dictionaries/settings-dialog.ko.js +2 -0
  40. package/dist/features/i18n/dictionaries/settings-dialog.pl.js +2 -0
  41. package/dist/features/i18n/dictionaries/settings-dialog.pt-BR.js +2 -0
  42. package/dist/features/i18n/dictionaries/settings-dialog.ru.js +2 -0
  43. package/dist/features/i18n/dictionaries/settings-dialog.th.js +2 -0
  44. package/dist/features/i18n/dictionaries/settings-dialog.tr.js +2 -0
  45. package/dist/features/i18n/dictionaries/settings-dialog.uk.js +2 -0
  46. package/dist/features/i18n/dictionaries/settings-dialog.ur.js +2 -0
  47. package/dist/features/i18n/dictionaries/settings-dialog.zh-CN.js +2 -0
  48. package/dist/features/i18n/dictionaries/settings-dialog.zh-TW.js +2 -0
  49. package/dist/features/i18n/dictionary-translator.d.ts +5 -2
  50. package/dist/features/i18n/dictionary-translator.js +1 -1
  51. package/dist/features/i18n/index.d.ts +1 -1
  52. package/dist/features/i18n/index.js +1 -1
  53. package/dist/features/i18n/template-i18n.d.ts +11 -0
  54. package/dist/features/i18n/template-i18n.js +12 -0
  55. package/dist/features/interactive-ui/interactive-ui.css +1 -1
  56. package/dist/features/interactive-ui/providers/recharts/bar-chart.js +3 -2
  57. package/dist/features/interactive-ui/providers/recharts/chart-tokens.d.ts +43 -0
  58. package/dist/features/interactive-ui/providers/recharts/chart-tokens.js +44 -0
  59. package/dist/features/interactive-ui/providers/recharts/line-chart.js +3 -2
  60. package/dist/features/interactive-ui/providers/recharts/pie-chart.js +2 -2
  61. package/dist/features/lexical-rich-text-editor/deserialize.d.ts +0 -5
  62. package/dist/features/lexical-rich-text-editor/deserialize.js +0 -5
  63. package/dist/features/lexical-rich-text-editor/mention-node.d.ts +5 -17
  64. package/dist/features/lexical-rich-text-editor/mention-node.js +5 -17
  65. package/dist/features/lexical-rich-text-editor/mention-parser.d.ts +2 -4
  66. package/dist/features/lexical-rich-text-editor/serialize.d.ts +0 -4
  67. package/dist/features/lexical-rich-text-editor/serialize.js +0 -4
  68. package/dist/features/lexical-rich-text-editor/types.d.ts +3 -13
  69. package/dist/features/lexical-rich-text-editor/types.js +3 -13
  70. package/dist/features/mcp-ui/early-message-buffer.d.ts +1 -2
  71. package/dist/features/mcp-ui/early-message-buffer.js +1 -2
  72. package/dist/features/mcp-ui/escape.d.ts +4 -0
  73. package/dist/features/mcp-ui/escape.js +2 -9
  74. package/dist/features/mcp-ui/index.d.ts +1 -0
  75. package/dist/features/mcp-ui/index.js +1 -0
  76. package/dist/features/mcp-ui/protocol.d.ts +2 -5
  77. package/dist/features/mcp-ui/protocol.js +2 -5
  78. package/dist/features/mcp-ui/resource.d.ts +47 -7
  79. package/dist/features/mcp-ui/resource.js +5 -8
  80. package/dist/features/mcp-ui/secret-card.d.ts +115 -0
  81. package/dist/features/mcp-ui/secret-card.js +137 -0
  82. package/dist/features/mcp-ui/surfaces/confirmation.js +5 -0
  83. package/dist/features/mcp-ui/surfaces/fields.d.ts +5 -2
  84. package/dist/features/mcp-ui/surfaces/fields.js +3 -1
  85. package/dist/features/mcp-ui/surfaces/form.d.ts +4 -1
  86. package/dist/features/mcp-ui/surfaces/form.js +65 -3
  87. package/dist/features/mcp-ui/surfaces/outcome.d.ts +2 -0
  88. package/dist/features/mcp-ui/surfaces/text-input.d.ts +14 -6
  89. package/dist/features/mcp-ui/surfaces/text-input.js +16 -2
  90. package/dist/features/mcp-ui/surfaces/tokens.js +1 -1
  91. package/dist/features/memory/react/components/MemoryConnectedPanel.d.ts +1 -1
  92. package/dist/features/memory/react/components/MemoryConnectedPanel.js +0 -1
  93. package/dist/features/panel-kit/helpers/format-timestamp.d.ts +5 -3
  94. package/dist/features/panel-kit/helpers/format-timestamp.js +13 -4
  95. package/dist/features/panel-kit/helpers/permissions.js +2 -0
  96. package/dist/features/panel-kit/helpers/resolve-active-tab-id.js +2 -0
  97. package/dist/features/panel-kit/helpers/standing-draft-local-backup.d.ts +51 -0
  98. package/dist/features/panel-kit/helpers/standing-draft-local-backup.js +77 -0
  99. package/dist/features/panel-kit/helpers/standing-draft-recovery-message.d.ts +8 -0
  100. package/dist/features/panel-kit/helpers/standing-draft-recovery-message.js +10 -0
  101. package/dist/features/panel-kit/hooks/use-async-action.hooks.d.ts +1 -0
  102. package/dist/features/panel-kit/hooks/use-dirty-guard.hooks.d.ts +1 -0
  103. package/dist/features/panel-kit/hooks/use-dirty-guard.hooks.js +1 -0
  104. package/dist/features/panel-kit/hooks/use-serial-writes.hooks.d.ts +8 -0
  105. package/dist/features/panel-kit/hooks/use-serial-writes.hooks.js +3 -0
  106. package/dist/features/panel-kit/hooks/use-settings-slice.hooks.d.ts +211 -0
  107. package/dist/features/panel-kit/hooks/use-settings-slice.hooks.js +382 -0
  108. package/dist/features/panel-kit/hooks/use-standing-draft-autosave.hooks.d.ts +179 -0
  109. package/dist/features/panel-kit/hooks/use-standing-draft-autosave.hooks.js +300 -0
  110. package/dist/features/panel-kit/index.d.ts +7 -1
  111. package/dist/features/panel-kit/index.js +5 -1
  112. package/dist/features/resource-dashboard/types.d.ts +8 -41
  113. package/dist/features/resource-dashboard/types.js +3 -26
  114. package/dist/features/tab-strip/constants.d.ts +3 -4
  115. package/dist/features/tab-strip/constants.js +3 -4
  116. package/dist/features/tab-strip/dependencies.d.ts +1 -6
  117. package/dist/features/tab-strip/dependencies.js +1 -6
  118. package/dist/features/tab-strip/react/components/TabBar.d.ts +33 -1
  119. package/dist/features/tab-strip/react/components/TabBar.hooks.d.ts +57 -5
  120. package/dist/features/tab-strip/react/components/TabBar.hooks.js +63 -5
  121. package/dist/features/tab-strip/react/components/TabBar.js +12 -1
  122. package/dist/features/tab-strip/rules.d.ts +4 -17
  123. package/dist/features/tab-strip/rules.js +4 -17
  124. package/dist/features/tab-strip/types.d.ts +2 -13
  125. package/dist/icon-name.d.ts +1 -1
  126. package/dist/icon-name.js +1 -0
  127. package/dist/index.d.ts +42 -417
  128. package/dist/index.js +42 -430
  129. package/dist/react/components/CustomSelect.d.ts +6 -2
  130. package/dist/react/components/CustomSelect.js +20 -91
  131. package/dist/react/components/Icon.d.ts +6 -0
  132. package/dist/react/components/Icon.js +12 -0
  133. package/dist/react/components/select-popover.d.ts +106 -0
  134. package/dist/react/components/select-popover.js +210 -0
  135. package/dist/renderers/sandbox-scripts.d.ts +29 -0
  136. package/dist/renderers/sandbox-scripts.js +127 -0
  137. package/dist/renderers/sandboxed-document.js +3 -86
  138. package/dist/renderers/srcdoc/build.d.ts +10 -17
  139. package/dist/renderers/srcdoc/build.js +31 -234
  140. package/dist/styles/variables.css +30 -0
  141. package/dist/theme/apply.d.ts +1 -1
  142. package/dist/theme/apply.js +2 -2
  143. package/dist/theme/color-scheme.d.ts +1 -1
  144. package/dist/theme/default.d.ts +1 -3
  145. package/dist/theme/default.js +2 -10
  146. package/dist/theme/index.d.ts +3 -3
  147. package/dist/theme/index.js +2 -2
  148. package/dist/theme/stylesheet.d.ts +1 -7
  149. package/dist/theme/stylesheet.js +2 -13
  150. package/dist/theme/types.d.ts +1 -38
  151. package/dist/theme/types.js +2 -1
  152. package/dist/theme/validation.d.ts +1 -14
  153. package/dist/theme/validation.js +2 -54
  154. package/dist/utils/endpoint-policy.d.ts +21 -39
  155. package/dist/utils/endpoint-policy.js +19 -107
  156. package/dist/utils/file-system-errors.d.ts +1 -2
  157. package/dist/utils/file-system-errors.js +1 -2
  158. package/dist/utils/ime-composing.d.ts +0 -3
  159. package/dist/utils/ime-composing.js +0 -3
  160. package/dist/utils/platform.d.ts +1 -4
  161. package/dist/utils/platform.js +1 -4
  162. package/dist/utils/smooth-scroll-to-top.d.ts +1 -3
  163. package/dist/utils/smooth-scroll-to-top.js +3 -13
  164. package/dist/utils/timezone.js +1 -5
  165. package/dist/utils/uuid.d.ts +0 -3
  166. package/dist/utils/uuid.js +0 -3
  167. package/dist/utils/visual-stability.d.ts +1 -3
  168. package/dist/utils/visual-stability.js +1 -3
  169. package/package.json +74 -15
@@ -1,3 +1,5 @@
1
+ // ADR-063: URL-owned screens retain their own tab lists and defaults. Pages mirrors local state
2
+ // to the URL to avoid navigation over eagerly fetched data; tab-manifest enumerability is a separate decision.
1
3
  /** Keep an allowed selection or choose the allowed default/first id; an empty list keeps defaultId. */
2
4
  /**
3
5
  * URL query values can come from stale bookmarks or typos; validating against allowed ids prevents
@@ -0,0 +1,51 @@
1
+ import type { StandingDraftAutosaveInput, StandingDraftAutosaveSnapshot } from "../hooks/use-standing-draft-autosave.hooks.js";
2
+ /**
3
+ * @file Last-resort per-tab mirror for a standing draft the SERVER REFUSED (2026-09-06 stale-basis
4
+ * fix). Deliberately NOT a general autosave backend — browser storage was explicitly rejected for
5
+ * that (see `use-standing-draft-autosave.hooks.ts`'s header); this holds exactly one thing: the
6
+ * text the server answered `applied: false` or HTTP 401 to, which it will keep refusing until the
7
+ * editor reloads the row or the operator signs in again. Server-side parking is impossible for that text, so without this the
8
+ * operator's only copy lives in a tab they may close at any moment.
9
+ *
10
+ * Written only on a refusal, read only as the mount-time fallback when the server has nothing
11
+ * parked, and dropped the moment the server accepts a write again, the operator discards, or a real
12
+ * Save lands. Every access is wrapped: `localStorage` throws outright in some privacy modes and on
13
+ * quota exhaustion, and a failed backup must never break the editor it is protecting.
14
+ *
15
+ * Type-only import from the hook module — no runtime edge, and the hook keeps owning the shapes
16
+ * both editors already import from it.
17
+ */
18
+ /** Namespaced so one entry's backup can never collide with another's, or with unrelated app keys. */
19
+ /** Legacy marker for injected ports with no identity metadata — a recovered backup is identifiably
20
+ * local rather than pretending the server parked it. Production now uses the actual principal id
21
+ * captured by the session adapter. The legacy marker still keeps old fakes honest because
22
+ * {@link StandingDraftAutosaveSnapshot} requires the field; inventing a server principal would be
23
+ * the same class of defect this whole fix is about. */
24
+ export declare const LOCAL_BACKUP_PRINCIPAL_ID = "local-tab-backup";
25
+ /** Bind a profile-local refusal mirror without choosing the host's storage namespace.
26
+ * Storage is obtained lazily inside guarded operations, so privacy-mode getters cannot break mount.
27
+ * @complexity O(draft size) for reads/writes; O(1) for clears.
28
+ */
29
+ export declare function createStandingDraftLocalBackup({ keyPrefix: KEY_PREFIX, getStorage }: {
30
+ keyPrefix: string;
31
+ getStorage: (required: Record<string, never>, optional?: Record<string, never>) => Pick<Storage, "getItem" | "setItem" | "removeItem">;
32
+ }, _options?: Record<string, never>): {
33
+ write: ({ entryId, draft, savedAt }: {
34
+ entryId: string;
35
+ draft: StandingDraftAutosaveInput;
36
+ savedAt: string;
37
+ }, options?: {
38
+ principalId?: string | null | undefined;
39
+ }) => void;
40
+ read: ({ entryId }: {
41
+ entryId: string;
42
+ }, options?: {
43
+ principalId?: string | null | undefined;
44
+ }) => StandingDraftAutosaveSnapshot | null;
45
+ clear: ({ entryId }: {
46
+ entryId: string;
47
+ }, options?: {
48
+ principalId?: string | null | undefined;
49
+ }) => void;
50
+ };
51
+ //# sourceMappingURL=standing-draft-local-backup.d.ts.map
@@ -0,0 +1,77 @@
1
+ /**
2
+ * @file Last-resort per-tab mirror for a standing draft the SERVER REFUSED (2026-09-06 stale-basis
3
+ * fix). Deliberately NOT a general autosave backend — browser storage was explicitly rejected for
4
+ * that (see `use-standing-draft-autosave.hooks.ts`'s header); this holds exactly one thing: the
5
+ * text the server answered `applied: false` or HTTP 401 to, which it will keep refusing until the
6
+ * editor reloads the row or the operator signs in again. Server-side parking is impossible for that text, so without this the
7
+ * operator's only copy lives in a tab they may close at any moment.
8
+ *
9
+ * Written only on a refusal, read only as the mount-time fallback when the server has nothing
10
+ * parked, and dropped the moment the server accepts a write again, the operator discards, or a real
11
+ * Save lands. Every access is wrapped: `localStorage` throws outright in some privacy modes and on
12
+ * quota exhaustion, and a failed backup must never break the editor it is protecting.
13
+ *
14
+ * Type-only import from the hook module — no runtime edge, and the hook keeps owning the shapes
15
+ * both editors already import from it.
16
+ */
17
+ /** Namespaced so one entry's backup can never collide with another's, or with unrelated app keys. */
18
+ /** Legacy marker for injected ports with no identity metadata — a recovered backup is identifiably
19
+ * local rather than pretending the server parked it. Production now uses the actual principal id
20
+ * captured by the session adapter. The legacy marker still keeps old fakes honest because
21
+ * {@link StandingDraftAutosaveSnapshot} requires the field; inventing a server principal would be
22
+ * the same class of defect this whole fix is about. */
23
+ export const LOCAL_BACKUP_PRINCIPAL_ID = "local-tab-backup";
24
+ /** Bind a profile-local refusal mirror without choosing the host's storage namespace.
25
+ * Storage is obtained lazily inside guarded operations, so privacy-mode getters cannot break mount.
26
+ * @complexity O(draft size) for reads/writes; O(1) for clears.
27
+ */
28
+ export function createStandingDraftLocalBackup({ keyPrefix: KEY_PREFIX, getStorage }, _options = {}) {
29
+ function keyFor(entryId, principalId) {
30
+ return principalId === undefined ? `${KEY_PREFIX}${entryId}` : `${KEY_PREFIX}${encodeURIComponent(principalId ?? "")}.${encodeURIComponent(entryId)}`;
31
+ }
32
+ /** Mirrors a refused draft for this browser profile. Silent no-op on any storage failure.
33
+ * @complexity Time/space: O(size of the draft) for the one `JSON.stringify`. */
34
+ function writeStandingDraftLocalBackup({ entryId, draft, savedAt }, options = {}) {
35
+ if (options.principalId === null)
36
+ return;
37
+ const snapshot = { ...draft, savedAt, savedByPrincipalId: options.principalId ?? LOCAL_BACKUP_PRINCIPAL_ID };
38
+ try {
39
+ const storage = getStorage({}, {});
40
+ storage.setItem(keyFor(entryId, options.principalId), JSON.stringify(snapshot));
41
+ }
42
+ catch {
43
+ // eslint-disable-next-line no-console -- best-effort; the operator's text is still in the tab.
44
+ console.warn("standing-draft local backup: could not write");
45
+ }
46
+ }
47
+ /** The mirrored draft, or `null` when there is none (or storage is unreadable/corrupt).
48
+ * @complexity Time/space: O(size of the stored draft). */
49
+ function readStandingDraftLocalBackup({ entryId }, options = {}) {
50
+ if (options.principalId === null)
51
+ return null;
52
+ try {
53
+ const storage = getStorage({}, {});
54
+ const raw = storage.getItem(keyFor(entryId, options.principalId));
55
+ return raw ? JSON.parse(raw) : null;
56
+ }
57
+ catch {
58
+ return null;
59
+ }
60
+ }
61
+ /** Drops the mirror. Called on an accepted write, an explicit discard, and a real Save.
62
+ * @complexity Time/space: O(1). */
63
+ function clearStandingDraftLocalBackup({ entryId }, options = {}) {
64
+ if (options.principalId === null)
65
+ return;
66
+ try {
67
+ const storage = getStorage({}, {});
68
+ storage.removeItem(keyFor(entryId, options.principalId));
69
+ }
70
+ catch {
71
+ // Nothing to do and nothing to tell the operator — a backup that cannot be removed is
72
+ // superseded by the next write anyway, and is never auto-applied.
73
+ }
74
+ }
75
+ return { write: writeStandingDraftLocalBackup, read: readStandingDraftLocalBackup, clear: clearStandingDraftLocalBackup };
76
+ }
77
+ //# sourceMappingURL=standing-draft-local-backup.js.map
@@ -0,0 +1,8 @@
1
+ import type { StandingDraftAutosaveSnapshot } from "../hooks/use-standing-draft-autosave.hooks.js";
2
+ /** Compare timestamps explicitly; restoration only changes the working copy, never the server. */
3
+ export declare function standingDraftRecoveryMessage({ draft, t, serverUpdatedAt }: {
4
+ draft: StandingDraftAutosaveSnapshot;
5
+ t: (key: string) => string;
6
+ serverUpdatedAt?: string;
7
+ }, _options?: {}): string;
8
+ //# sourceMappingURL=standing-draft-recovery-message.d.ts.map
@@ -0,0 +1,10 @@
1
+ /** Compare timestamps explicitly; restoration only changes the working copy, never the server. */
2
+ export function standingDraftRecoveryMessage({ draft, t, serverUpdatedAt }, _options = {}) {
3
+ const captured = Date.parse(draft.savedAt);
4
+ const server = Date.parse(serverUpdatedAt ?? draft.serverUpdatedAt ?? "");
5
+ const comparison = server > captured ? "Server content is newer."
6
+ : server < captured ? "Local changes are newer."
7
+ : server === captured ? "Both copies have the same timestamp." : "";
8
+ return `${t("Restore unsaved changes from")} ${draft.savedAt}${comparison ? ` · ${t(comparison)}` : ""}`;
9
+ }
10
+ //# sourceMappingURL=standing-draft-recovery-message.js.map
@@ -1,5 +1,6 @@
1
1
  import { type Dispatch, type SetStateAction } from "react";
2
2
  export interface AsyncActionState {
3
+ /** True for the most recent run; error resets before each attempt and stays null on success. */
3
4
  saving: boolean;
4
5
  error: string | null;
5
6
  setError: Dispatch<SetStateAction<string | null>>;
@@ -6,6 +6,7 @@ export interface DirtyGuard {
6
6
  unsavedBeyondTracked?: boolean;
7
7
  }) => boolean;
8
8
  }
9
+ /** @complexity O(n) per render in the serialized size of current/original; intended for small forms. */
9
10
  /** Compare small JSON-serializable form states (including key order). Null original is clean.
10
11
  * Warn on unload and offer a translated native confirmation for caller-owned navigation. */
11
12
  export declare function useDirtyGuard<T>({ current, original }: {
@@ -4,6 +4,7 @@ import { useEffect } from "react";
4
4
  function shallowJsonEqual(a, b) {
5
5
  return JSON.stringify(a) === JSON.stringify(b);
6
6
  }
7
+ /** @complexity O(n) per render in the serialized size of current/original; intended for small forms. */
7
8
  /** Compare small JSON-serializable form states (including key order). Null original is clean.
8
9
  * Warn on unload and offer a translated native confirmation for caller-owned navigation. */
9
10
  export function useDirtyGuard({ current, original }, { translate = (key) => key, host = window } = {}) {
@@ -12,6 +12,14 @@
12
12
  * Re-entrant run calls queue normally, but awaiting an inner call from a task on the same
13
13
  * lane deadlocks: the inner task is waiting for the outer task to release that lane.
14
14
  */
15
+ /**
16
+ * @example
17
+ * const generation = settlement.next();
18
+ * await writes.run({ task: async () => {
19
+ * const result = await port.write();
20
+ * if (settlement.isCurrent({ generation })) setResult(result);
21
+ * } });
22
+ */
15
23
  export interface SerialWrites {
16
24
  /** Each caller observes its own task result or rejection, never the internal lane tail. */
17
25
  run<T>({ task }: {
@@ -38,4 +38,7 @@ export function useSerialWrites() {
38
38
  ref.current = createSerialWrites();
39
39
  return ref.current;
40
40
  }
41
+ // Credentials, activation and autosave share this queue so writes cannot race or be dropped;
42
+ // some callers enqueue their last persistence write from unmount cleanup to prevent data loss.
43
+ // Controllers use this handle in useCallback/useMemo dependencies; keep the adapter stable too.
41
44
  //# sourceMappingURL=use-serial-writes.hooks.js.map
@@ -0,0 +1,211 @@
1
+ export type SettingsRefreshScope = readonly string[] | null;
2
+ /**
3
+ * @file One settings-dialog tab's load/edit/debounced-save lifecycle,
4
+ * generic over the tab's own config type.
5
+ *
6
+ * This was `SettingsUi.tsx`'s inline machinery when exactly one tab (Execution
7
+ * mode) was mounted. It is extracted rather than copied because the logic is
8
+ * subtle out of proportion to its size — successive audits have found FOUR
9
+ * distinct data-loss bugs in it (see `saveTicket`, `hasUnsavedEdits`, `runSave`,
10
+ * and `commits` below), and every one of those hazards is identical for every
11
+ * tab. Duplicating it per tab would mean re-introducing all four bugs three
12
+ * more times, and fixing them in three more places.
13
+ *
14
+ * The last two share one root cause worth stating plainly, because it is the
15
+ * shape every future bug here will take: **a value read at schedule time is a
16
+ * claim about the store that may already be false by the time the code using it
17
+ * runs.** `runSave` fixes it for writes by reading the diff base at run time;
18
+ * `commits` fixes it for reads by discarding a reload whose response predates a
19
+ * write that landed while it was in flight. Anything added here that captures
20
+ * `persisted` or `latest` across an `await` needs the same treatment.
21
+ *
22
+ * Each mounted tab owns one instance. They are independent by construction:
23
+ * separate debounce timers, separate save chains, separate diff bases. The
24
+ * only shared surface is the merged status the page chrome renders, which
25
+ * `SettingsUi.tsx` derives.
26
+ */
27
+ export type SaveState = {
28
+ status: "idle";
29
+ } | {
30
+ status: "saving";
31
+ } | {
32
+ status: "saved";
33
+ } | {
34
+ status: "error";
35
+ message: string;
36
+ };
37
+ /** Debounce for text fields: tabs are controlled and fire on every keystroke,
38
+ * and each ledger write is an append-only revision. Without this, typing into
39
+ * a textarea would write one revision per character. */
40
+ export declare const SAVE_DEBOUNCE_MS = 600;
41
+ /** Namespace-only notifications preserve the authorized re-read boundary; never carry values. */
42
+ export interface SettingsSliceRefreshPort {
43
+ publish(required: {
44
+ namespaces: readonly string[];
45
+ }, optional?: Record<string, never>): void;
46
+ subscribe(required: {
47
+ listener: (scope: SettingsRefreshScope) => void;
48
+ }, optional?: Record<string, never>): () => void;
49
+ }
50
+ export interface SettingsSliceOptions<T> {
51
+ /** Reads the persisted value. A rejection surfaces as `loadError` and the
52
+ * slice falls back to `defaultValue` — an unreadable setting shows defaults
53
+ * with a visible warning rather than blocking the whole dialog. */
54
+ load: (required: Record<string, never>, optional?: Record<string, never>) => Promise<T>;
55
+ /** Writes whatever changed between `next` and `previous`, returning the keys
56
+ * actually written (an empty array means "nothing to do", which the status
57
+ * indicator renders as idle rather than a misleading "Saved"). */
58
+ save: (required: {
59
+ next: T;
60
+ previous: T;
61
+ }, optional?: Record<string, never>) => Promise<readonly string[]>;
62
+ defaultValue: T;
63
+ /**
64
+ * Ledger namespaces this slice reads, so a refresh naming other namespaces
65
+ * is ignored instead of costing a pointless reload.
66
+ *
67
+ * Omit to reload on EVERY refresh. That is the safe default in both
68
+ * directions: a slice that forgets to declare its namespaces stays correct
69
+ * (it just refetches more than it needs), whereas defaulting to "ignore
70
+ * everything" would make a forgotten declaration silently stop updating —
71
+ * the exact bug this feature exists to remove.
72
+ */
73
+ namespaces?: readonly string[];
74
+ /** Use one port for publication and subscription, so successful writes refresh their own surface.
75
+ * The host supplies a namespace-only bus; ports own their subscription cleanup. */
76
+ refreshPort: SettingsSliceRefreshPort;
77
+ /**
78
+ * Reconciles a freshly loaded value against the operator's current in-memory value before
79
+ * {@link SettingsSlice.refresh} lets it replace `value`/`persisted`/`latest`. Defaults to "the
80
+ * loaded value wins outright" — correct for every field this slice actually round-trips through
81
+ * `save`/`load`.
82
+ *
83
+ * Provide this when `T` carries a field `save`/`load` deliberately never touch (see
84
+ * `execution-settings.ts`'s `reconcileExecutionConfigRefresh` for `byok.apiKey`, which `save`
85
+ * never writes and `load` always returns empty). Without it, an external refresh — a same-tab
86
+ * echo of this slice's OWN write arriving over the settings-changed SSE feed, another tab,
87
+ * another operator — carries that field back at its "nothing here" default and silently
88
+ * overwrites an operator's typed-but-unsaved edit to it. `runSave` cannot leak this on its own:
89
+ * it never calls `setValue`. This path can, because replacing `value` with server truth is its
90
+ * entire job.
91
+ *
92
+ * @param current - `latest.current` at the moment the reload settled — the newest value the
93
+ * operator has produced, saved or not.
94
+ * @param loaded - What `load()` just returned — true server state for every field it manages.
95
+ */
96
+ reconcileRefresh?: (required: {
97
+ current: T;
98
+ loaded: T;
99
+ }, optional?: Record<string, never>) => T;
100
+ }
101
+ /**
102
+ * Runs one save-chain link: calls `runSave`, then paints the ticket-owning outcome onto
103
+ * `saveState` — unless a newer save has since taken the ticket (`currentTicket()` no longer matches
104
+ * `ticket`), or the operator has queued another edit behind this one (`hasUnsavedEdits()`). Pulled
105
+ * out of `onChange`'s debounce-timer closure (2026-08-06, complexity pass) — it was the deepest
106
+ * nesting in this file, three closures in (`onChange` > the `setTimeout` callback > the
107
+ * `saveChain.current.then(async () => …)` link) — so the ticket/staleness handling is directly
108
+ * assertable against a fake `runSave`, without a real timer or save chain.
109
+ *
110
+ * The ref reads are passed as thunks rather than plain booleans specifically so this keeps the
111
+ * original "read at run time, not schedule time" contract this whole file's header calls out:
112
+ * `deps.currentTicket()`/`deps.hasUnsavedEdits()` are called from inside this function, at the same
113
+ * points the inline version read `saveTicket.current`/`hasUnsavedEdits.current` directly, so a
114
+ * ticket or edit that changes while `runSave` is in flight is still seen correctly.
115
+ *
116
+ * @param ticket - This link's own ticket, taken by the caller when the debounce timer fired.
117
+ */
118
+ export declare function commitQueuedSave({ ticket, deps }: {
119
+ ticket: number;
120
+ deps: {
121
+ runSave: () => Promise<readonly string[]>;
122
+ currentTicket: () => number;
123
+ hasUnsavedEdits: () => boolean;
124
+ setSaveState: (state: SaveState) => void;
125
+ };
126
+ }, _options?: Record<string, never>): Promise<void>;
127
+ /**
128
+ * Whether a background `refresh()` reload may still land, given what is true about the slice's
129
+ * uncommitted-work state at the moment of the check.
130
+ *
131
+ * Pulled out of `refresh` (2026-08-06, complexity pass, third pass): the same four-flag shape is
132
+ * checked TWICE inside `refresh` — once before the `load()` await (where `mounted`/`commitsUnchanged`
133
+ * are trivially true, since nothing could have unmounted or committed a save before the function has
134
+ * even started awaiting), once after (where all four are live) — and collapsing the check into one
135
+ * named function removes the duplicated branch count rather than moving it. This does not change
136
+ * WHEN any ref is read: every call site below still reads `timer.current`/`hasUnsavedEdits.current`/
137
+ * `mounted.current`/`commits.current` at exactly the point in `refresh`'s own body the inline version
138
+ * did, in the same tick — this function only names the boolean combination those reads produce. See
139
+ * `refresh`'s own doc for why a re-check after the await is necessary at all.
140
+ */
141
+ export declare function canAcceptRefresh(state: {
142
+ timerPending: boolean;
143
+ hasUnsavedEdits: boolean;
144
+ mounted: boolean;
145
+ commitsUnchanged: boolean;
146
+ }, _options?: Record<string, never>): boolean;
147
+ export interface SettingsSlice<T> {
148
+ /** `null` until the initial load settles — the caller renders a loading state. */
149
+ value: T | null;
150
+ loadError: string | null;
151
+ saveState: SaveState;
152
+ onChange: (next: T) => void;
153
+ /**
154
+ * Re-reads the persisted value, discarding nothing.
155
+ *
156
+ * Wired to {@link subscribeToSettingsRefresh} automatically; exposed mainly so
157
+ * a caller can force one. Declines while the operator has uncommitted work —
158
+ * see the implementation for why that is a refusal rather than a merge.
159
+ */
160
+ refresh: () => Promise<void>;
161
+ }
162
+ /**
163
+ * @complexity O(1) per edit — one debounce timer reset plus one link appended
164
+ * to the save chain.
165
+ *
166
+ * @complexityExemption (2026-08-06, complexity pass, third pass; bar raised to ≤9/≤9 same day,
167
+ * exemption reconfirmed against the new bar) **Score: this hook's own lexical scope is 1
168
+ * cyclomatic / ~0 cognitive under ESLint — every closure inside it is independently ≤9/≤9 too
169
+ * (`onChange`'s debounce callback is 2/1, the unmount-flush cleanup is 3/2, `refresh` is 5/4 as of
170
+ * this pass — see below). What is exempted here is a DIFFERENT, unmeasurable-by-me number: the
171
+ * dispatch brief's owner-tool score of 17/23, which rolls every nested closure's branches into the
172
+ * hook's total. Bar: ≤9/≤9 on whichever view is scored — ESLint's view already clears it; the
173
+ * owner-tool aggregate does not, and I have no local tool that reproduces that aggregate to
174
+ * re-measure against the new bar.**
175
+ *
176
+ * Same shape as `use-assistant-chats.hooks.ts`'s `useAssistantChats` exemption — see that one for
177
+ * the full reasoning. `commitQueuedSave` (first pass) and `canAcceptRefresh` (this pass, pulled out
178
+ * of `refresh`'s two duplicated guard checks — see its own doc) are the two pieces of this hook's
179
+ * logic that genuinely fit a top-level pure-function shape (0–4 plain params, no ref reads);
180
+ * `refresh` itself dropped from 9/5 to 5/4 as a result, its own per-function score real progress,
181
+ * not exempted. What remains (`runSave`/`onChange`/the unmount cleanup/the rest of `refresh`) each
182
+ * reads or writes 3–6 of this hook's own refs (`io`, `persisted`, `latest`, `timer`,
183
+ * `saveTicket`, `hasUnsavedEdits`, `commits`, `mounted`) plus the shared `writes` lane, and several of
184
+ * them call each other
185
+ * (`onChange` schedules a link that calls `runSave`; the unmount cleanup calls `runSave` directly),
186
+ * so none of them can become a top-level function without threading that whole ref set through as a
187
+ * parameter object. This file's own header states the reason that is dangerous here specifically:
188
+ * "a value read at schedule time is a claim about the store that may already be false by the time
189
+ * the code using it runs," and every ref above exists because an earlier version of exactly this
190
+ * logic got that ordering wrong (four distinct data-loss bugs, per the header). A previous pass
191
+ * already tried this kind of extraction on this file and the total was unchanged, 25 → 25 — the
192
+ * honest reading of that result is that the remaining closures do not have a top-level-shaped cut
193
+ * available at the risk level a time-boxed pass allows, not that the attempt was done wrong.
194
+ * Declined for the remainder; flagged to the coordinator. See this session's report for the full
195
+ * reasoning.
196
+ */
197
+ export declare function useSettingsSlice<T>(options: SettingsSliceOptions<T>, _optional?: Record<string, never>): SettingsSlice<T>;
198
+ /**
199
+ * Collapses several slices' save states into the single one the page chrome
200
+ * renders.
201
+ *
202
+ * Precedence is error > saving > saved > idle, and it is deliberate: an error
203
+ * is the only state the operator must act on, so it must not be hidden by
204
+ * another tab's cheerful "Saved". "Saving" outranks "saved" for the same
205
+ * reason in miniature — while anything is still in flight, claiming everything
206
+ * is saved is false.
207
+ */
208
+ export declare function mergeSaveStates({ states }: {
209
+ states: readonly SaveState[];
210
+ }, _options?: Record<string, never>): SaveState;
211
+ //# sourceMappingURL=use-settings-slice.hooks.d.ts.map