@jini-ai/ui 0.4.4 → 0.4.6

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 (165) hide show
  1. package/CHANGELOG.md +10 -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/providers/recharts/chart-tokens.d.ts +1 -1
  56. package/dist/features/interactive-ui/providers/recharts/chart-tokens.js +1 -1
  57. package/dist/features/lexical-rich-text-editor/deserialize.d.ts +0 -5
  58. package/dist/features/lexical-rich-text-editor/deserialize.js +0 -5
  59. package/dist/features/lexical-rich-text-editor/mention-node.d.ts +5 -17
  60. package/dist/features/lexical-rich-text-editor/mention-node.js +5 -17
  61. package/dist/features/lexical-rich-text-editor/mention-parser.d.ts +2 -4
  62. package/dist/features/lexical-rich-text-editor/serialize.d.ts +0 -4
  63. package/dist/features/lexical-rich-text-editor/serialize.js +0 -4
  64. package/dist/features/lexical-rich-text-editor/types.d.ts +3 -13
  65. package/dist/features/lexical-rich-text-editor/types.js +3 -13
  66. package/dist/features/mcp-ui/early-message-buffer.d.ts +1 -2
  67. package/dist/features/mcp-ui/early-message-buffer.js +1 -2
  68. package/dist/features/mcp-ui/escape.d.ts +4 -0
  69. package/dist/features/mcp-ui/escape.js +2 -9
  70. package/dist/features/mcp-ui/index.d.ts +1 -0
  71. package/dist/features/mcp-ui/index.js +1 -0
  72. package/dist/features/mcp-ui/protocol.d.ts +2 -5
  73. package/dist/features/mcp-ui/protocol.js +2 -5
  74. package/dist/features/mcp-ui/resource.d.ts +47 -7
  75. package/dist/features/mcp-ui/resource.js +5 -8
  76. package/dist/features/mcp-ui/secret-card.d.ts +115 -0
  77. package/dist/features/mcp-ui/secret-card.js +137 -0
  78. package/dist/features/mcp-ui/surfaces/confirmation.js +5 -0
  79. package/dist/features/mcp-ui/surfaces/fields.d.ts +5 -2
  80. package/dist/features/mcp-ui/surfaces/fields.js +3 -1
  81. package/dist/features/mcp-ui/surfaces/form.d.ts +4 -1
  82. package/dist/features/mcp-ui/surfaces/form.js +65 -3
  83. package/dist/features/mcp-ui/surfaces/outcome.d.ts +2 -0
  84. package/dist/features/mcp-ui/surfaces/text-input.d.ts +14 -6
  85. package/dist/features/mcp-ui/surfaces/text-input.js +16 -2
  86. package/dist/features/mcp-ui/surfaces/tokens.js +1 -1
  87. package/dist/features/memory/react/components/MemoryConnectedPanel.d.ts +1 -1
  88. package/dist/features/memory/react/components/MemoryConnectedPanel.js +0 -1
  89. package/dist/features/panel-kit/helpers/format-timestamp.d.ts +5 -3
  90. package/dist/features/panel-kit/helpers/format-timestamp.js +13 -4
  91. package/dist/features/panel-kit/helpers/permissions.js +2 -0
  92. package/dist/features/panel-kit/helpers/resolve-active-tab-id.js +2 -0
  93. package/dist/features/panel-kit/helpers/standing-draft-local-backup.d.ts +51 -0
  94. package/dist/features/panel-kit/helpers/standing-draft-local-backup.js +77 -0
  95. package/dist/features/panel-kit/helpers/standing-draft-recovery-message.d.ts +8 -0
  96. package/dist/features/panel-kit/helpers/standing-draft-recovery-message.js +10 -0
  97. package/dist/features/panel-kit/hooks/use-async-action.hooks.d.ts +1 -0
  98. package/dist/features/panel-kit/hooks/use-dirty-guard.hooks.d.ts +1 -0
  99. package/dist/features/panel-kit/hooks/use-dirty-guard.hooks.js +1 -0
  100. package/dist/features/panel-kit/hooks/use-serial-writes.hooks.d.ts +8 -0
  101. package/dist/features/panel-kit/hooks/use-serial-writes.hooks.js +3 -0
  102. package/dist/features/panel-kit/hooks/use-settings-slice.hooks.d.ts +211 -0
  103. package/dist/features/panel-kit/hooks/use-settings-slice.hooks.js +382 -0
  104. package/dist/features/panel-kit/hooks/use-standing-draft-autosave.hooks.d.ts +179 -0
  105. package/dist/features/panel-kit/hooks/use-standing-draft-autosave.hooks.js +300 -0
  106. package/dist/features/panel-kit/index.d.ts +7 -1
  107. package/dist/features/panel-kit/index.js +5 -1
  108. package/dist/features/resource-dashboard/types.d.ts +8 -41
  109. package/dist/features/resource-dashboard/types.js +3 -26
  110. package/dist/features/tab-strip/constants.d.ts +3 -4
  111. package/dist/features/tab-strip/constants.js +3 -4
  112. package/dist/features/tab-strip/dependencies.d.ts +1 -6
  113. package/dist/features/tab-strip/dependencies.js +1 -6
  114. package/dist/features/tab-strip/react/components/TabBar.d.ts +33 -1
  115. package/dist/features/tab-strip/react/components/TabBar.hooks.d.ts +57 -5
  116. package/dist/features/tab-strip/react/components/TabBar.hooks.js +63 -5
  117. package/dist/features/tab-strip/react/components/TabBar.js +12 -1
  118. package/dist/features/tab-strip/rules.d.ts +4 -17
  119. package/dist/features/tab-strip/rules.js +4 -17
  120. package/dist/features/tab-strip/types.d.ts +2 -13
  121. package/dist/icon-name.d.ts +1 -1
  122. package/dist/icon-name.js +1 -0
  123. package/dist/index.d.ts +42 -417
  124. package/dist/index.js +42 -430
  125. package/dist/react/components/CustomSelect.d.ts +6 -2
  126. package/dist/react/components/CustomSelect.js +20 -91
  127. package/dist/react/components/Icon.d.ts +6 -0
  128. package/dist/react/components/Icon.js +12 -0
  129. package/dist/react/components/select-popover.d.ts +106 -0
  130. package/dist/react/components/select-popover.js +210 -0
  131. package/dist/renderers/sandbox-scripts.d.ts +29 -0
  132. package/dist/renderers/sandbox-scripts.js +127 -0
  133. package/dist/renderers/sandboxed-document.js +3 -86
  134. package/dist/renderers/srcdoc/build.d.ts +10 -17
  135. package/dist/renderers/srcdoc/build.js +31 -234
  136. package/dist/styles/variables.css +2 -2
  137. package/dist/theme/apply.d.ts +1 -1
  138. package/dist/theme/apply.js +2 -2
  139. package/dist/theme/color-scheme.d.ts +1 -1
  140. package/dist/theme/default.d.ts +1 -3
  141. package/dist/theme/default.js +2 -10
  142. package/dist/theme/index.d.ts +3 -3
  143. package/dist/theme/index.js +2 -2
  144. package/dist/theme/stylesheet.d.ts +1 -7
  145. package/dist/theme/stylesheet.js +2 -13
  146. package/dist/theme/types.d.ts +1 -38
  147. package/dist/theme/types.js +2 -1
  148. package/dist/theme/validation.d.ts +1 -14
  149. package/dist/theme/validation.js +2 -54
  150. package/dist/utils/endpoint-policy.d.ts +21 -39
  151. package/dist/utils/endpoint-policy.js +19 -107
  152. package/dist/utils/file-system-errors.d.ts +1 -2
  153. package/dist/utils/file-system-errors.js +1 -2
  154. package/dist/utils/ime-composing.d.ts +0 -3
  155. package/dist/utils/ime-composing.js +0 -3
  156. package/dist/utils/platform.d.ts +1 -4
  157. package/dist/utils/platform.js +1 -4
  158. package/dist/utils/smooth-scroll-to-top.d.ts +1 -3
  159. package/dist/utils/smooth-scroll-to-top.js +3 -13
  160. package/dist/utils/timezone.js +1 -5
  161. package/dist/utils/uuid.d.ts +0 -3
  162. package/dist/utils/uuid.js +0 -3
  163. package/dist/utils/visual-stability.d.ts +1 -3
  164. package/dist/utils/visual-stability.js +1 -3
  165. package/package.json +74 -15
@@ -28,8 +28,11 @@ import { type SurfaceDetail, type SurfaceStatusText } from './document.js';
28
28
  import { type SurfaceField } from './fields.js';
29
29
  import type { BridgeScriptSpec } from './bridge.js';
30
30
  import type { SurfaceTokenName } from './tokens.js';
31
+ import { type SecretInputText } from './text-input.js';
31
32
  export interface FormSurfaceSpec {
32
33
  readonly title: string;
34
+ /** Resource framing carried through host form projections; ignored by HTML rendering. */
35
+ readonly preferredFrameSize?: readonly [string, string];
33
36
  readonly description?: string;
34
37
  /** Read-only context shown above the fields — what the input is *about*. */
35
38
  readonly details?: readonly SurfaceDetail[];
@@ -52,7 +55,7 @@ export interface FormSurfaceSpec {
52
55
  readonly toolName?: string;
53
56
  readonly params?: Readonly<Record<string, unknown>>;
54
57
  };
55
- readonly text?: Partial<SurfaceStatusText>;
58
+ readonly text?: Partial<SurfaceStatusText & SecretInputText>;
56
59
  readonly app?: BridgeScriptSpec;
57
60
  readonly lang?: string;
58
61
  readonly tokens?: Partial<Record<SurfaceTokenName, string>>;
@@ -27,6 +27,7 @@ import { createUIResource } from '../resource.js';
27
27
  import { escapeHtml, escapeJsValue } from '../escape.js';
28
28
  import { DEFAULT_SURFACE_STATUS_TEXT, SURFACE_SCRIPT_PRELUDE, renderActions, renderDetailList, renderStatusRegion, renderSurfaceDocument, renderSurfaceHeader, } from './document.js';
29
29
  import { renderFieldControl, toFieldReadSpecs } from './fields.js';
30
+ import { DEFAULT_SECRET_INPUT_TEXT } from './text-input.js';
30
31
  const DEFAULT_APP = { appName: 'jini-mcp-ui-form', appVersion: '1' };
31
32
  /** The form element's id, fixed so the emitted script can find it without a fragile selector. */
32
33
  const FORM_ELEMENT_ID = 'mcpui-form';
@@ -38,7 +39,14 @@ const FORM_ELEMENT_ID = 'mcpui-form';
38
39
  * @complexity O(n) in the number of fields plus the rendered length.
39
40
  */
40
41
  export function renderFormDocument(spec) {
41
- const text = { ...DEFAULT_SURFACE_STATUS_TEXT, ...spec.text };
42
+ // A base param with a secret field's name would leak the same value through the emitted script,
43
+ // even though the collected value overrides it at submit time. Reject that hidden prefill too.
44
+ for (const field of spec.fields) {
45
+ if (field.kind === 'string' && field.secret === true && spec.baseParams?.[field.name] !== undefined) {
46
+ throw new Error('Secret fields cannot have a pre-filled value. Omit value.');
47
+ }
48
+ }
49
+ const text = { ...DEFAULT_SURFACE_STATUS_TEXT, ...DEFAULT_SECRET_INPUT_TEXT, ...spec.text };
42
50
  // Not `type: 'submit'` — see the runtime script below for why a submit-typed button inside this
43
51
  // particular `<form>` cannot work at all in production.
44
52
  const actions = [
@@ -46,7 +54,9 @@ export function renderFormDocument(spec) {
46
54
  ];
47
55
  if (spec.cancel !== undefined)
48
56
  actions.push({ id: 'cancel', label: spec.cancel.label, variant: 'neutral' });
49
- const controls = spec.fields.map(renderFieldControl).join('\n');
57
+ const controls = spec.fields
58
+ .map((field) => renderFieldControl(field, { showSecret: text.showSecret, hideSecret: text.hideSecret }))
59
+ .join('\n');
50
60
  const warning = spec.warning === undefined ? '' : `<p class="mcpui-warning">${escapeHtml(spec.warning)}</p>`;
51
61
  const bodyHtml = [
52
62
  renderSurfaceHeader(spec.description === undefined ? { title: spec.title } : { title: spec.title, description: spec.description }),
@@ -69,6 +79,57 @@ ${SURFACE_SCRIPT_PRELUDE}
69
79
  var TEXT = ${escapeJsValue(text)};
70
80
  var form = document.getElementById(${escapeJsValue(FORM_ELEMENT_ID)});
71
81
 
82
+ // A textarea normalizes CRLF/CR to LF even on .value assignment. Keep the original trusted paste
83
+ // privately, outside DOM/HTML, so multiline credentials reach the tool with their original bytes.
84
+ // An edit that changes the DOM value makes the snapshot ineligible; typed text then follows the
85
+ // native textarea value. Offsets count CRLF as one visible newline when a second paste replaces
86
+ // a selection, otherwise the original raw string would be spliced at the wrong position.
87
+ var secretPastes = new WeakMap();
88
+ function rawOffset(raw, visibleOffset) {
89
+ var rawIndex = 0;
90
+ var visibleIndex = 0;
91
+ while (rawIndex < raw.length && visibleIndex < visibleOffset) {
92
+ if (raw.charCodeAt(rawIndex) === 13 && raw.charCodeAt(rawIndex + 1) === 10) rawIndex++;
93
+ rawIndex++;
94
+ visibleIndex++;
95
+ }
96
+ return rawIndex;
97
+ }
98
+ form.addEventListener("paste", function (event) {
99
+ var control = event.target;
100
+ if (event.isTrusted !== true || !control || control.tagName !== "TEXTAREA" || !control.hasAttribute("data-mcpui-secret") || !event.clipboardData) return;
101
+ var pasted = event.clipboardData.getData("text/plain");
102
+ if (!pasted) return;
103
+ var snapshot = secretPastes.get(control);
104
+ var raw = snapshot && snapshot.normalized === control.value ? snapshot.raw : control.value;
105
+ var start = control.selectionStart;
106
+ var end = control.selectionEnd;
107
+ var next = raw.slice(0, rawOffset(raw, start)) + pasted + raw.slice(rawOffset(raw, end));
108
+ event.preventDefault();
109
+ control.value = next;
110
+ // Caret offsets use the displayed newline representation, independent of the saved raw text.
111
+ var caret = start + pasted.replace(/\\r\\n?/g, "\\n").length;
112
+ control.setSelectionRange(caret, caret);
113
+ secretPastes.set(control, { raw: next, normalized: control.value });
114
+ control.dispatchEvent(new Event("input", { bubbles: true }));
115
+ });
116
+
117
+ // A reveal button only changes presentation: replacing a textarea with a password input would
118
+ // discard line breaks. Listen on the form so clicks on the button's contents work too.
119
+ form.addEventListener("click", function (event) {
120
+ if (event.isTrusted !== true) return;
121
+ var target = event.target;
122
+ if (!target || typeof target.closest !== "function") return;
123
+ var button = target.closest("button[data-mcpui-secret-toggle]");
124
+ if (!button || button.disabled) return;
125
+ var control = document.getElementById(button.getAttribute("data-mcpui-secret-toggle"));
126
+ if (!control) return;
127
+ var reveal = button.getAttribute("aria-pressed") !== "true";
128
+ control.setAttribute("style", "-webkit-text-security: " + (reveal ? "none" : "disc") + ";");
129
+ button.setAttribute("aria-pressed", reveal ? "true" : "false");
130
+ button.textContent = button.getAttribute(reveal ? "data-mcpui-hide-secret" : "data-mcpui-show-secret");
131
+ });
132
+
72
133
  // form.elements[name] yields a RadioNodeList when several controls share a name and the BARE
73
134
  // element when only one does. A one-option checklist would otherwise read as "no options" —
74
135
  // silently, since a bare input has no length to iterate. Presence of "checked" is what tells the
@@ -88,7 +149,8 @@ ${SURFACE_SCRIPT_PRELUDE}
88
149
  if (field.kind === "multi-enum") return checkedValues(control);
89
150
  // Covers a radio-presented enum too: a RadioNodeList's own .value is the checked radio's
90
151
  // value, or "" when none is checked — the same shape a <select> reports.
91
- return control.value;
152
+ var snapshot = secretPastes.get(control);
153
+ return snapshot && snapshot.normalized === control.value ? snapshot.raw : control.value;
92
154
  }
93
155
 
94
156
  function isBlank(field, value) {
@@ -48,6 +48,8 @@ import type { BridgeScriptSpec } from './bridge.js';
48
48
  import type { SurfaceTokenName } from './tokens.js';
49
49
  export interface SurfaceOutcomeSpec {
50
50
  readonly title: string;
51
+ /** Resource framing carried through host outcome projections; ignored by HTML rendering. */
52
+ readonly preferredFrameSize?: readonly [string, string];
51
53
  readonly description?: string;
52
54
  /** The facts of what actually happened — same shape as a confirmation's own `details`, but
53
55
  * reporting the RESULT (e.g. the real URL, the real status) rather than what was proposed. */
@@ -1,8 +1,15 @@
1
- export interface TextInputProps {
1
+ /** Labels the host can translate for a multiline secret's reveal toggle. */
2
+ export interface SecretInputText {
3
+ readonly showSecret: string;
4
+ readonly hideSecret: string;
5
+ }
6
+ export declare const DEFAULT_SECRET_INPUT_TEXT: SecretInputText;
7
+ export interface TextInputProps extends Partial<SecretInputText> {
2
8
  /** HTML `name`, DOM `id` suffix, and the params key the value is posted under. */
3
9
  readonly name: string;
4
10
  readonly label: string;
5
- /** Pre-filled value. Numbers are stringified; `undefined` leaves the control empty. */
11
+ /** Pre-filled value. Numbers are stringified; `undefined` leaves the control empty.
12
+ * Forbidden when `secret` is true, including an empty string. */
6
13
  readonly value?: string | number;
7
14
  readonly placeholder?: string;
8
15
  /** Help text, wired up with `aria-describedby`. */
@@ -11,10 +18,11 @@ export interface TextInputProps {
11
18
  readonly disabled?: boolean;
12
19
  /** `'number'` also constrains the on-screen keyboard on touch devices, which `'text'` would not. */
13
20
  readonly inputType?: 'text' | 'number';
14
- /** Renders as `<input type="password">` — the value is never visible on screen, and browser
15
- * password managers may offer to remember it. Ignored when `inputType` is `'number'` (no such
16
- * thing as a masked number) and forces `multiline` off (no `<textarea type="password">` exists) —
17
- * same precedence `multiline`'s own doc comment already gives `inputType: 'number'`. */
21
+ /** Renders as `<input type="password">`, or a masked textarea with a show/hide toggle when
22
+ * multiline. No `<textarea type="password">` exists, so multiline uses CSS text security.
23
+ * Ignored for presentation when `inputType` is `'number'` (no such thing as a masked number) —
24
+ * same precedence `multiline`'s own doc comment already gives `inputType: 'number'`.
25
+ * A supplied value is always rejected: masking cannot protect a secret serialized into HTML. */
18
26
  readonly secret?: boolean;
19
27
  /** Renders a `<textarea>`. Ignored when `inputType` is `'number'` — there is no multiline number. */
20
28
  readonly multiline?: boolean;
@@ -7,6 +7,7 @@
7
7
  */
8
8
  import { escapeHtml } from '../escape.js';
9
9
  import { fieldDescribedBy, fieldElementId, renderFieldLabel } from './document.js';
10
+ export const DEFAULT_SECRET_INPUT_TEXT = { showSecret: 'Show', hideSecret: 'Hide' };
10
11
  function optionalAttribute(name, value) {
11
12
  if (value === undefined)
12
13
  return '';
@@ -36,6 +37,11 @@ function resolveInputType(props) {
36
37
  * @complexity O(n) in the rendered length.
37
38
  */
38
39
  export function renderTextInput(props) {
40
+ // Even an empty prefill is forbidden: callers must omit value so a stored credential can never
41
+ // enter the resource's HTML. Keep this error fixed, without the field name or rejected value.
42
+ if (props.secret === true && props.value !== undefined) {
43
+ throw new Error('Secret fields cannot have a pre-filled value. Omit value.');
44
+ }
39
45
  const id = fieldElementId(props.name);
40
46
  const common = ` id="${escapeHtml(id)}" name="${escapeHtml(props.name)}"` +
41
47
  optionalAttribute('placeholder', props.placeholder) +
@@ -44,8 +50,16 @@ export function renderTextInput(props) {
44
50
  fieldDescribedBy(props);
45
51
  const type = resolveInputType(props);
46
52
  const isNumber = type === 'number';
47
- const control = props.multiline === true && type === 'text'
48
- ? `<textarea class="mcpui-textarea"${common}${optionalAttribute('rows', props.rows)}>${props.value === undefined ? '' : escapeHtml(String(props.value))}</textarea>`
53
+ const secretMultiline = props.secret === true && props.multiline === true && !isNumber;
54
+ const control = props.multiline === true && !isNumber
55
+ ? `<textarea class="mcpui-textarea"${common}${optionalAttribute('rows', props.rows)}` +
56
+ // Password inputs strip line breaks. CSS masking keeps the textarea's actual value intact,
57
+ // and disables browser text assistance that could retain or alter a pasted credential.
58
+ (secretMultiline ? ' data-mcpui-secret style="-webkit-text-security: disc;" autocomplete="off" spellcheck="false"' : '') +
59
+ `>${props.value === undefined ? '' : escapeHtml(String(props.value))}</textarea>` +
60
+ (secretMultiline
61
+ ? `<button type="button" class="mcpui-button" data-mcpui-secret-toggle="${escapeHtml(id)}" data-mcpui-show-secret="${escapeHtml(props.showSecret ?? DEFAULT_SECRET_INPUT_TEXT.showSecret)}" data-mcpui-hide-secret="${escapeHtml(props.hideSecret ?? DEFAULT_SECRET_INPUT_TEXT.hideSecret)}" aria-controls="${escapeHtml(id)}" aria-pressed="false"${booleanAttribute('disabled', props.disabled)}>${escapeHtml(props.showSecret ?? DEFAULT_SECRET_INPUT_TEXT.showSecret)}</button>`
62
+ : '')
49
63
  : `<input class="mcpui-input" type="${type}"${common}` +
50
64
  optionalAttribute('value', props.value) +
51
65
  // A stored secret (e.g. a cloud provider's access key or a database password) must never be
@@ -1,4 +1,4 @@
1
- import { defaultAdminTheme } from '../../../theme/default.js';
1
+ import { defaultAdminTheme } from '../../../core/theme/default.js';
2
2
  /**
3
3
  * @module features/mcp-ui/surfaces/tokens
4
4
  *
@@ -1,4 +1,4 @@
1
- import { type Connector, type ConnectorStatusMap } from '../../../connectors/types.js';
1
+ import type { Connector, ConnectorStatusMap } from '../../../connectors/types.js';
2
2
  import type { ConnectorMemoryAttempt, MemoryExtractionRecord, MemorySuggestion } from '../../types.js';
3
3
  export declare function MemoryConnectedPanel({ enabled, onOpenConnectors, connectorStatuses, connectorsLoading, connectedCount, selectedConnectorIds, selectedConnectedConnectorIds, connectingConnectorIds, pendingConnectorAuthIds, connectorConnectErrors, connectorIdsWithDetails, connectorExtracting, connectorSaving, connectorScanLabel, connectorSuggestions, selectedSuggestionIds, selectedConnectorSuggestions, connectorStatus, connectorError, connectorLoadError, connectorAttempts, connectorContextBytes, connectorExtractions, memoryConnectors, toggleConnectorSelection, onConnectMemoryConnector, toggleConnectorSuggestion, onSuggestConnectorMemory, onSaveConnectorSuggestions, onDiscardConnectorSuggestions, nowClock, onOpenPreview, onDeleteExtraction, }: {
4
4
  enabled: boolean;
@@ -9,7 +9,6 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
9
9
  // concern was already dropped when the connectors canary shipped; a host
10
10
  // resolving theme-swapped logo URLs does so before handing `logoUrl` in).
11
11
  import { useMemo } from 'react';
12
- import {} from '../../../connectors/types.js';
13
12
  import { connectorAttemptDetail, connectorAttemptTitle, formatConnectorContextBytes, memoryTypeLabels, } from '../../formatters.js';
14
13
  import { ConnectorLogo } from '../../../connectors/index.js';
15
14
  import { Icon } from '../../../../react/components/Icon.js';
@@ -1,11 +1,13 @@
1
1
  /**
2
- * Centralize the historical fixed-width display so formatting changes have one owner. Preserve
3
- * the stored timestamp's prefix byte for byte: no parsing, locale formatting or timezone conversion.
2
+ * Centralize the fixed-width display so formatting changes have one owner. The default
3
+ * preserves the stored timestamp's prefix byte for byte, without parsing or timezone conversion.
4
4
  * Trailing seconds, milliseconds and zone suffix are intentionally dropped; callers supply ISO text.
5
5
  */
6
- /** Render the ISO prefix as YYYY-MM-DD HH:MM without timezone conversion. */
6
+ /** Render YYYY-MM-DD HH:MM; hosts can select the viewer's local timezone explicitly. */
7
7
  export declare function formatTimestamp({ iso }: {
8
8
  iso: string;
9
+ }, { timeZone }?: {
10
+ timeZone?: "stored" | "local";
9
11
  }): string;
10
12
  /** Inject 'now' to keep relative-time display deterministic. The fallback wording uses simple
11
13
  * English singular/plural templates; hosts can translate them, but this is not full locale pluralization. */
@@ -1,10 +1,19 @@
1
1
  /**
2
- * Centralize the historical fixed-width display so formatting changes have one owner. Preserve
3
- * the stored timestamp's prefix byte for byte: no parsing, locale formatting or timezone conversion.
2
+ * Centralize the fixed-width display so formatting changes have one owner. The default
3
+ * preserves the stored timestamp's prefix byte for byte, without parsing or timezone conversion.
4
4
  * Trailing seconds, milliseconds and zone suffix are intentionally dropped; callers supply ISO text.
5
5
  */
6
- /** Render the ISO prefix as YYYY-MM-DD HH:MM without timezone conversion. */
7
- export function formatTimestamp({ iso }) {
6
+ /** Render YYYY-MM-DD HH:MM; hosts can select the viewer's local timezone explicitly. */
7
+ export function formatTimestamp({ iso }, { timeZone = "stored" } = {}) {
8
+ if (timeZone === "local") {
9
+ // A stored UTC prefix can label server time as viewer time (Tovu D-06). Local getters
10
+ // follow the browser's timezone and DST; zone-less ISO is already local wall time.
11
+ const date = new Date(iso);
12
+ if (Number.isNaN(date.getTime()))
13
+ return iso; // Keep malformed source data visible.
14
+ const pad = (value) => String(value).padStart(2, "0");
15
+ return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}`;
16
+ }
8
17
  return iso.slice(0, 16).replace("T", " ");
9
18
  }
10
19
  /** Inject 'now' to keep relative-time display deterministic. The fallback wording uses simple
@@ -1,3 +1,5 @@
1
+ // ADR-021: GET /auth/me flattens effectivePermissions, losing resourceType/constraintJson.
2
+ // ADR-006 keeps one server evaluator; every mutation and tool call must be rechecked there.
1
3
  /** UI affordance check for exact grants or the literal * wildcard; server authorization remains host-owned. */
2
4
  /**
3
5
  * A flattened permission list has lost resource scopes and constraints. Treating '*' as an ordinary
@@ -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