@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.
- package/CHANGELOG.md +11 -0
- package/README.md +20 -0
- package/dist/core/theme/default.d.ts +4 -0
- package/dist/core/theme/default.js +11 -0
- package/dist/core/theme/stylesheet.d.ts +8 -0
- package/dist/core/theme/stylesheet.js +14 -0
- package/dist/core/theme/types.d.ts +39 -0
- package/dist/core/theme/types.js +2 -0
- package/dist/core/theme/validation.d.ts +15 -0
- package/dist/core/theme/validation.js +55 -0
- package/dist/features/admin-widgets/components/Select/Select.d.ts +11 -3
- package/dist/features/admin-widgets/components/Select/Select.hooks.d.ts +8 -13
- package/dist/features/admin-widgets/components/Select/Select.hooks.js +37 -215
- package/dist/features/admin-widgets/components/Select/Select.js +4 -3
- package/dist/features/appearance/constants.js +1 -1
- package/dist/features/connectors/rules.d.ts +1 -6
- package/dist/features/connectors/types.d.ts +1 -6
- package/dist/features/connectors/types.js +1 -6
- package/dist/features/execution/http-adapter.d.ts +111 -0
- package/dist/features/execution/http-adapter.js +160 -0
- package/dist/features/execution/index.d.ts +4 -0
- package/dist/features/execution/index.js +2 -0
- package/dist/features/execution/react/hooks/use-execution-credential.hooks.d.ts +232 -0
- package/dist/features/execution/react/hooks/use-execution-credential.hooks.js +217 -0
- package/dist/features/html-editor/canvas-style.d.ts +1 -1
- package/dist/features/html-editor/canvas-style.js +2 -2
- package/dist/features/i18n/dictionaries/settings-dialog.ar.js +2 -0
- package/dist/features/i18n/dictionaries/settings-dialog.bn.js +2 -0
- package/dist/features/i18n/dictionaries/settings-dialog.de.js +2 -0
- package/dist/features/i18n/dictionaries/settings-dialog.en.js +2 -0
- package/dist/features/i18n/dictionaries/settings-dialog.es.js +2 -0
- package/dist/features/i18n/dictionaries/settings-dialog.fa.js +2 -0
- package/dist/features/i18n/dictionaries/settings-dialog.fr.js +2 -0
- package/dist/features/i18n/dictionaries/settings-dialog.hi.js +2 -0
- package/dist/features/i18n/dictionaries/settings-dialog.hu.js +2 -0
- package/dist/features/i18n/dictionaries/settings-dialog.id.js +2 -0
- package/dist/features/i18n/dictionaries/settings-dialog.it.js +2 -0
- package/dist/features/i18n/dictionaries/settings-dialog.ja.js +2 -0
- package/dist/features/i18n/dictionaries/settings-dialog.ko.js +2 -0
- package/dist/features/i18n/dictionaries/settings-dialog.pl.js +2 -0
- package/dist/features/i18n/dictionaries/settings-dialog.pt-BR.js +2 -0
- package/dist/features/i18n/dictionaries/settings-dialog.ru.js +2 -0
- package/dist/features/i18n/dictionaries/settings-dialog.th.js +2 -0
- package/dist/features/i18n/dictionaries/settings-dialog.tr.js +2 -0
- package/dist/features/i18n/dictionaries/settings-dialog.uk.js +2 -0
- package/dist/features/i18n/dictionaries/settings-dialog.ur.js +2 -0
- package/dist/features/i18n/dictionaries/settings-dialog.zh-CN.js +2 -0
- package/dist/features/i18n/dictionaries/settings-dialog.zh-TW.js +2 -0
- package/dist/features/i18n/dictionary-translator.d.ts +5 -2
- package/dist/features/i18n/dictionary-translator.js +1 -1
- package/dist/features/i18n/index.d.ts +1 -1
- package/dist/features/i18n/index.js +1 -1
- package/dist/features/i18n/template-i18n.d.ts +11 -0
- package/dist/features/i18n/template-i18n.js +12 -0
- package/dist/features/interactive-ui/interactive-ui.css +1 -1
- package/dist/features/interactive-ui/providers/recharts/bar-chart.js +3 -2
- package/dist/features/interactive-ui/providers/recharts/chart-tokens.d.ts +43 -0
- package/dist/features/interactive-ui/providers/recharts/chart-tokens.js +44 -0
- package/dist/features/interactive-ui/providers/recharts/line-chart.js +3 -2
- package/dist/features/interactive-ui/providers/recharts/pie-chart.js +2 -2
- package/dist/features/lexical-rich-text-editor/deserialize.d.ts +0 -5
- package/dist/features/lexical-rich-text-editor/deserialize.js +0 -5
- package/dist/features/lexical-rich-text-editor/mention-node.d.ts +5 -17
- package/dist/features/lexical-rich-text-editor/mention-node.js +5 -17
- package/dist/features/lexical-rich-text-editor/mention-parser.d.ts +2 -4
- package/dist/features/lexical-rich-text-editor/serialize.d.ts +0 -4
- package/dist/features/lexical-rich-text-editor/serialize.js +0 -4
- package/dist/features/lexical-rich-text-editor/types.d.ts +3 -13
- package/dist/features/lexical-rich-text-editor/types.js +3 -13
- package/dist/features/mcp-ui/early-message-buffer.d.ts +1 -2
- package/dist/features/mcp-ui/early-message-buffer.js +1 -2
- package/dist/features/mcp-ui/escape.d.ts +4 -0
- package/dist/features/mcp-ui/escape.js +2 -9
- package/dist/features/mcp-ui/index.d.ts +1 -0
- package/dist/features/mcp-ui/index.js +1 -0
- package/dist/features/mcp-ui/protocol.d.ts +2 -5
- package/dist/features/mcp-ui/protocol.js +2 -5
- package/dist/features/mcp-ui/resource.d.ts +47 -7
- package/dist/features/mcp-ui/resource.js +5 -8
- package/dist/features/mcp-ui/secret-card.d.ts +115 -0
- package/dist/features/mcp-ui/secret-card.js +137 -0
- package/dist/features/mcp-ui/surfaces/confirmation.js +5 -0
- package/dist/features/mcp-ui/surfaces/fields.d.ts +5 -2
- package/dist/features/mcp-ui/surfaces/fields.js +3 -1
- package/dist/features/mcp-ui/surfaces/form.d.ts +4 -1
- package/dist/features/mcp-ui/surfaces/form.js +65 -3
- package/dist/features/mcp-ui/surfaces/outcome.d.ts +2 -0
- package/dist/features/mcp-ui/surfaces/text-input.d.ts +14 -6
- package/dist/features/mcp-ui/surfaces/text-input.js +16 -2
- package/dist/features/mcp-ui/surfaces/tokens.js +1 -1
- package/dist/features/memory/react/components/MemoryConnectedPanel.d.ts +1 -1
- package/dist/features/memory/react/components/MemoryConnectedPanel.js +0 -1
- package/dist/features/panel-kit/helpers/format-timestamp.d.ts +5 -3
- package/dist/features/panel-kit/helpers/format-timestamp.js +13 -4
- package/dist/features/panel-kit/helpers/permissions.js +2 -0
- package/dist/features/panel-kit/helpers/resolve-active-tab-id.js +2 -0
- package/dist/features/panel-kit/helpers/standing-draft-local-backup.d.ts +51 -0
- package/dist/features/panel-kit/helpers/standing-draft-local-backup.js +77 -0
- package/dist/features/panel-kit/helpers/standing-draft-recovery-message.d.ts +8 -0
- package/dist/features/panel-kit/helpers/standing-draft-recovery-message.js +10 -0
- package/dist/features/panel-kit/hooks/use-async-action.hooks.d.ts +1 -0
- package/dist/features/panel-kit/hooks/use-dirty-guard.hooks.d.ts +1 -0
- package/dist/features/panel-kit/hooks/use-dirty-guard.hooks.js +1 -0
- package/dist/features/panel-kit/hooks/use-serial-writes.hooks.d.ts +8 -0
- package/dist/features/panel-kit/hooks/use-serial-writes.hooks.js +3 -0
- package/dist/features/panel-kit/hooks/use-settings-slice.hooks.d.ts +211 -0
- package/dist/features/panel-kit/hooks/use-settings-slice.hooks.js +382 -0
- package/dist/features/panel-kit/hooks/use-standing-draft-autosave.hooks.d.ts +179 -0
- package/dist/features/panel-kit/hooks/use-standing-draft-autosave.hooks.js +300 -0
- package/dist/features/panel-kit/index.d.ts +7 -1
- package/dist/features/panel-kit/index.js +5 -1
- package/dist/features/resource-dashboard/types.d.ts +8 -41
- package/dist/features/resource-dashboard/types.js +3 -26
- package/dist/features/tab-strip/constants.d.ts +3 -4
- package/dist/features/tab-strip/constants.js +3 -4
- package/dist/features/tab-strip/dependencies.d.ts +1 -6
- package/dist/features/tab-strip/dependencies.js +1 -6
- package/dist/features/tab-strip/react/components/TabBar.d.ts +33 -1
- package/dist/features/tab-strip/react/components/TabBar.hooks.d.ts +57 -5
- package/dist/features/tab-strip/react/components/TabBar.hooks.js +63 -5
- package/dist/features/tab-strip/react/components/TabBar.js +12 -1
- package/dist/features/tab-strip/rules.d.ts +4 -17
- package/dist/features/tab-strip/rules.js +4 -17
- package/dist/features/tab-strip/types.d.ts +2 -13
- package/dist/icon-name.d.ts +1 -1
- package/dist/icon-name.js +1 -0
- package/dist/index.d.ts +42 -417
- package/dist/index.js +42 -430
- package/dist/react/components/CustomSelect.d.ts +6 -2
- package/dist/react/components/CustomSelect.js +20 -91
- package/dist/react/components/Icon.d.ts +6 -0
- package/dist/react/components/Icon.js +12 -0
- package/dist/react/components/select-popover.d.ts +106 -0
- package/dist/react/components/select-popover.js +210 -0
- package/dist/renderers/sandbox-scripts.d.ts +29 -0
- package/dist/renderers/sandbox-scripts.js +127 -0
- package/dist/renderers/sandboxed-document.js +3 -86
- package/dist/renderers/srcdoc/build.d.ts +10 -17
- package/dist/renderers/srcdoc/build.js +31 -234
- package/dist/styles/variables.css +30 -0
- package/dist/theme/apply.d.ts +1 -1
- package/dist/theme/apply.js +2 -2
- package/dist/theme/color-scheme.d.ts +1 -1
- package/dist/theme/default.d.ts +1 -3
- package/dist/theme/default.js +2 -10
- package/dist/theme/index.d.ts +3 -3
- package/dist/theme/index.js +2 -2
- package/dist/theme/stylesheet.d.ts +1 -7
- package/dist/theme/stylesheet.js +2 -13
- package/dist/theme/types.d.ts +1 -38
- package/dist/theme/types.js +2 -1
- package/dist/theme/validation.d.ts +1 -14
- package/dist/theme/validation.js +2 -54
- package/dist/utils/endpoint-policy.d.ts +21 -39
- package/dist/utils/endpoint-policy.js +19 -107
- package/dist/utils/file-system-errors.d.ts +1 -2
- package/dist/utils/file-system-errors.js +1 -2
- package/dist/utils/ime-composing.d.ts +0 -3
- package/dist/utils/ime-composing.js +0 -3
- package/dist/utils/platform.d.ts +1 -4
- package/dist/utils/platform.js +1 -4
- package/dist/utils/smooth-scroll-to-top.d.ts +1 -3
- package/dist/utils/smooth-scroll-to-top.js +3 -13
- package/dist/utils/timezone.js +1 -5
- package/dist/utils/uuid.d.ts +0 -3
- package/dist/utils/uuid.js +0 -3
- package/dist/utils/visual-stability.d.ts +1 -3
- package/dist/utils/visual-stability.js +1 -3
- 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
|