@jini-ai/chat 0.5.3 → 0.5.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 (138) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/dist/core/ag-ui/entry.d.ts +33 -0
  3. package/dist/core/ag-ui/entry.js +80 -0
  4. package/dist/core/agentic/chat-capabilities.d.ts +2 -4
  5. package/dist/core/agentic/index.d.ts +4 -17
  6. package/dist/core/agentic/index.js +4 -17
  7. package/dist/core/assistant-client/a2ui-action-poster.d.ts +46 -0
  8. package/dist/core/assistant-client/a2ui-action-poster.js +113 -0
  9. package/dist/core/assistant-client/ag-ui-transport.d.ts +68 -0
  10. package/dist/core/assistant-client/ag-ui-transport.js +147 -0
  11. package/dist/core/assistant-client/attachment-liveness.d.ts +37 -0
  12. package/dist/core/assistant-client/attachment-liveness.js +93 -0
  13. package/dist/core/assistant-client/durable-run-subscription.d.ts +39 -0
  14. package/dist/core/assistant-client/durable-run-subscription.js +96 -0
  15. package/dist/core/assistant-client/entry.d.ts +13 -0
  16. package/dist/core/assistant-client/entry.js +7 -0
  17. package/dist/core/assistant-client/messages.d.ts +73 -0
  18. package/dist/core/assistant-client/messages.js +68 -0
  19. package/dist/core/assistant-client/transport.d.ts +164 -0
  20. package/dist/core/assistant-client/transport.js +734 -0
  21. package/dist/core/browser/page-actions.d.ts +121 -0
  22. package/dist/core/browser/page-actions.js +113 -0
  23. package/dist/core/composer-history.d.ts +54 -0
  24. package/dist/core/composer-history.js +57 -0
  25. package/dist/core/durable-projection.d.ts +28 -0
  26. package/dist/core/durable-projection.js +71 -0
  27. package/dist/core/durable-runs/continuation.d.ts +18 -0
  28. package/dist/core/durable-runs/continuation.js +61 -0
  29. package/dist/core/durable-runs/ledger-contracts.d.ts +53 -0
  30. package/dist/core/durable-runs/ledger-contracts.js +2 -0
  31. package/dist/core/durable-runs/policies.d.ts +6 -0
  32. package/dist/core/durable-runs/policies.js +6 -0
  33. package/dist/core/durable-runs/ports.d.ts +123 -0
  34. package/dist/core/durable-runs/ports.js +2 -0
  35. package/dist/core/durable-runs/run-contracts.d.ts +17 -0
  36. package/dist/core/durable-runs/run-contracts.js +2 -0
  37. package/dist/core/durable-runs/run-lock.d.ts +4 -0
  38. package/dist/core/durable-runs/run-lock.js +4 -0
  39. package/dist/core/index.d.ts +9 -19
  40. package/dist/core/index.js +8 -19
  41. package/dist/core/messages.d.ts +2 -0
  42. package/dist/core/question-form/format.d.ts +0 -9
  43. package/dist/core/question-form/format.js +16 -2
  44. package/dist/core/question-form/normalize.js +1 -0
  45. package/dist/core/question-form/secret-answers.d.ts +38 -0
  46. package/dist/core/question-form/secret-answers.js +45 -0
  47. package/dist/core/question-form/types.d.ts +2 -0
  48. package/dist/core/question-form.d.ts +1 -0
  49. package/dist/core/question-form.js +1 -0
  50. package/dist/core/run-events/entry.d.ts +208 -16
  51. package/dist/core/run-events/entry.js +273 -39
  52. package/dist/core/transcript.d.ts +3 -2
  53. package/dist/core/transcript.js +19 -3
  54. package/dist/core/transport.d.ts +4 -0
  55. package/dist/core/user-text-redaction.d.ts +56 -0
  56. package/dist/core/user-text-redaction.js +89 -0
  57. package/dist/embed-widget/browser/page-actions.d.ts +1 -120
  58. package/dist/embed-widget/browser/page-actions.js +2 -112
  59. package/dist/embed-widget/react/embed.d.ts +1 -1
  60. package/dist/embed-widget/react/embed.js +1 -1
  61. package/dist/react/artifact-types.d.ts +9 -15
  62. package/dist/react/components/Composer.js +6 -0
  63. package/dist/react/components/MessageRow.js +24 -11
  64. package/dist/react/components/QuestionForm.d.ts +2 -1
  65. package/dist/react/components/QuestionForm.js +18 -12
  66. package/dist/react/components/QuestionsPanel.d.ts +3 -2
  67. package/dist/react/components/QuestionsPanel.js +2 -2
  68. package/dist/react/features/chat-pane/agent-tools.d.ts +1 -1
  69. package/dist/react/features/chat-pane/agent-tools.js +0 -1
  70. package/dist/react/features/chat-pane/components/AgentRuntimePicker.js +5 -2
  71. package/dist/react/features/chat-pane/components/ChatPane.d.ts +1 -1
  72. package/dist/react/features/chat-pane/components/ChatPane.js +9 -4
  73. package/dist/react/features/chat-pane/create-typed-answer-poster.d.ts +2 -1
  74. package/dist/react/features/chat-pane/create-typed-answer-poster.js +33 -1
  75. package/dist/react/features/chat-pane/hooks/useAgentRuntimePicker.hooks.js +3 -2
  76. package/dist/react/features/chat-pane/hooks/useChatPane.hooks.d.ts +7 -1
  77. package/dist/react/features/chat-pane/hooks/useChatPane.hooks.js +38 -23
  78. package/dist/react/features/chat-pane/model-options.d.ts +10 -0
  79. package/dist/react/features/chat-pane/model-options.js +51 -0
  80. package/dist/react/features/chat-pane/rules.d.ts +3 -2
  81. package/dist/react/features/chat-pane/rules.js +12 -4
  82. package/dist/react/features/chat-pane/types.d.ts +30 -1
  83. package/dist/react/hooks/assistant-chats-contracts.d.ts +164 -0
  84. package/dist/react/hooks/assistant-chats-contracts.js +2 -0
  85. package/dist/react/hooks/assistant-chats.d.ts +7 -0
  86. package/dist/react/hooks/assistant-chats.js +960 -0
  87. package/dist/react/hooks/attachment-preview-cache.d.ts +1 -1
  88. package/dist/react/hooks/attachment-preview-cache.js +1 -1
  89. package/dist/react/hooks/composer-draft-cache.d.ts +2 -2
  90. package/dist/react/hooks/composer-draft-cache.js +2 -2
  91. package/dist/react/hooks/composer-history-storage.d.ts +8 -0
  92. package/dist/react/hooks/composer-history-storage.js +40 -0
  93. package/dist/react/hooks/fab-position.d.ts +90 -0
  94. package/dist/react/hooks/fab-position.js +271 -0
  95. package/dist/react/hooks/useComposer.d.ts +10 -1
  96. package/dist/react/hooks/useComposer.js +14 -2
  97. package/dist/react/hooks/useComposerHistory.hooks.d.ts +15 -0
  98. package/dist/react/hooks/useComposerHistory.hooks.js +90 -0
  99. package/dist/react/hooks/useComposerHistoryKeys.hooks.d.ts +9 -0
  100. package/dist/react/hooks/useComposerHistoryKeys.hooks.js +90 -0
  101. package/dist/react/hooks/useConversation.d.ts +4 -1
  102. package/dist/react/hooks/useConversation.js +20 -14
  103. package/dist/react/hooks/useRunStream.js +12 -2
  104. package/dist/react/hooks/useUserTextGuard.d.ts +11 -0
  105. package/dist/react/hooks/useUserTextGuard.js +19 -0
  106. package/dist/react/index.d.ts +14 -21
  107. package/dist/react/index.js +8 -0
  108. package/dist/server/durable-runs/accept.d.ts +25 -0
  109. package/dist/server/durable-runs/accept.js +21 -0
  110. package/dist/server/durable-runs/entry.d.ts +5 -0
  111. package/dist/server/durable-runs/entry.js +4 -0
  112. package/dist/server/durable-runs/recover.d.ts +5 -0
  113. package/dist/server/durable-runs/recover.js +147 -0
  114. package/dist/server/run-finalizer/ports.d.ts +2 -16
  115. package/dist/store/legacy/sqlite/messages.d.ts +24 -18
  116. package/dist/store/legacy/sqlite/messages.js +12 -2
  117. package/dist/store/pglite/index.d.ts +2 -1
  118. package/dist/store/postgres/index.d.ts +2 -1
  119. package/dist/store/runtime/chat-expiry-sweep.d.ts +40 -0
  120. package/dist/store/runtime/chat-expiry-sweep.js +70 -0
  121. package/dist/store/runtime/chat-history-store.d.ts +20 -0
  122. package/dist/store/runtime/chat-history-store.js +50 -0
  123. package/dist/store/runtime/durable-run-store.d.ts +21 -0
  124. package/dist/store/runtime/durable-run-store.js +342 -0
  125. package/dist/store/runtime/entry.d.ts +7 -0
  126. package/dist/store/runtime/entry.js +6 -0
  127. package/dist/store/runtime/kernel.d.ts +29 -0
  128. package/dist/store/runtime/kernel.js +2 -0
  129. package/dist/store/runtime/run-guarded-chat-store.d.ts +10 -0
  130. package/dist/store/runtime/run-guarded-chat-store.js +42 -0
  131. package/dist/store/runtime/run-ledger.d.ts +15 -0
  132. package/dist/store/runtime/run-ledger.js +206 -0
  133. package/dist/store/sql/boundary.d.ts +6 -0
  134. package/dist/store/sql/boundary.js +7 -1
  135. package/dist/store/sql/store.d.ts +3 -2
  136. package/dist/store/sql/store.js +9 -2
  137. package/dist/store/sqlite/index.d.ts +3 -2
  138. package/package.json +75 -10
@@ -0,0 +1,89 @@
1
+ /** One credential policy for chat text; secure card values must bypass this text-only boundary. */
2
+ import { redactSecretShapes } from '@jini-ai/diagnostics/redaction/secrets-only';
3
+ export const SECRET_REDACTED_NOTICE = 'I removed what looked like a key from your message. If it was real, rotate it. Use the secure form to save it.';
4
+ export const CREDENTIAL_CARD_GUIDANCE = 'Use the secure form to save keys, tokens, passwords, and secrets. Chat answers are sent to the assistant.';
5
+ const REMOVED = '[token removed]';
6
+ const INTERNAL_REMOVED = '[REDACTED:chat_secret]';
7
+ const REDACTION_MARKER = /\[REDACTED:[^\]\r\n]+\]/g;
8
+ const CHAT_RULES = [
9
+ { kind: 'chat_key', pattern: /(?<![A-Za-z0-9_-])sk-[A-Za-z0-9_-]{16,}/g, replacement: () => INTERNAL_REMOVED },
10
+ { kind: 'chat_key', pattern: /(?<![A-Za-z0-9_])xox[baprs]-[A-Za-z0-9-]{16,}/g, replacement: () => INTERNAL_REMOVED },
11
+ // Explicit authorization syntax catches even short values. Exempt scheme descriptions in prose.
12
+ { kind: 'chat_bearer', pattern: /\bBearer[ \t]+(?!\[REDACTED)(?!(?:token|tokens|authentication|authorization|auth|scheme|credentials?)\b)[A-Za-z0-9._~+\/=-]+/gi, replacement: () => 'Bearer ' + INTERNAL_REMOVED },
13
+ ];
14
+ function entropy(text) {
15
+ const counts = new Map();
16
+ for (const char of text)
17
+ counts.set(char, (counts.get(char) ?? 0) + 1);
18
+ let bits = 0;
19
+ for (const count of counts.values()) {
20
+ const probability = count / text.length;
21
+ bits -= probability * Math.log2(probability);
22
+ }
23
+ return bits;
24
+ }
25
+ /** Stricter than error redaction: also detects unlabelled long tokens, while retaining ordinary URLs,
26
+ * hexadecimal hashes, identifiers and prose. Strong credential syntax inside a URL is still caught. */
27
+ export function redactUserText({ text }, { secretField = false } = {}) {
28
+ // Existing removal markers must survive a second pass through assignment rules (api_key=…).
29
+ const literalMarkers = new Map();
30
+ let literalPrefix = 'chat_literal_';
31
+ while (text.includes(literalPrefix))
32
+ literalPrefix += '_';
33
+ const protectedText = text.replace(REDACTION_MARKER, marker => {
34
+ const key = '[REDACTED:' + literalPrefix + literalMarkers.size + ']';
35
+ literalMarkers.set(key, marker);
36
+ return key;
37
+ }).replaceAll(REMOVED, INTERNAL_REMOVED);
38
+ const base = redactSecretShapes({ text: protectedText, policy: { additionalRules: CHAT_RULES } });
39
+ let count = base.redactions;
40
+ const urls = [...base.text.matchAll(/\b[a-z][a-z0-9+.-]*:\/\/[^\s<>"']+/gi)]
41
+ .map(match => ({ start: match.index, end: match.index + match[0].length }));
42
+ let cleaned = base.text.replace(/(?<![A-Za-z0-9_+\/=-])[A-Za-z0-9_+\/=-]{40,}(?![A-Za-z0-9_+\/=-])/g, (candidate, offset) => {
43
+ if (urls.some(url => offset >= url.start && offset < url.end))
44
+ return candidate;
45
+ if (/^[a-f0-9]+$/i.test(candidate) || !/[A-Za-z]/.test(candidate))
46
+ return candidate;
47
+ const bits = entropy(candidate);
48
+ const mixedAlphabet = /[A-Z]/.test(candidate) && /[a-z]/.test(candidate);
49
+ if (bits < 4 || (!/[0-9]/.test(candidate) && (!mixedAlphabet || bits < 4.5)))
50
+ return candidate;
51
+ count += 1;
52
+ return INTERNAL_REMOVED;
53
+ });
54
+ // A labelled credential answer can be a short password with no recognisable shape at all.
55
+ if (secretField && cleaned.trim() !== '' && cleaned !== INTERNAL_REMOVED) {
56
+ cleaned = INTERNAL_REMOVED;
57
+ count = Math.max(1, count);
58
+ }
59
+ return { text: cleaned.replace(REDACTION_MARKER, marker => literalMarkers.get(marker) ?? REMOVED), secretRedacted: count > 0, count };
60
+ }
61
+ /** Prepare before storage OR model dispatch. Return only sanitized content and non-secret metadata. */
62
+ export function redactUserMessage({ message }, options = {}) {
63
+ if (message.role !== 'user')
64
+ return { message, secretRedacted: false, count: 0 };
65
+ const result = (options.redactUserText ?? redactUserText)({ text: message.content });
66
+ const signal = result.secretRedacted ? { secretRedacted: true, count: result.count } : undefined;
67
+ if (signal)
68
+ options.onSecretRedacted?.(signal);
69
+ return {
70
+ message: { ...message, content: result.text, ...(signal ? { secretRedaction: signal } : {}) },
71
+ secretRedacted: result.secretRedacted, count: result.count,
72
+ };
73
+ }
74
+ /** Also protects replay of historical text accepted before the guard was installed. */
75
+ export function redactUserHistory({ history }, options = {}) {
76
+ return history.map(message => redactUserMessage({ message }, options).message);
77
+ }
78
+ /** Redact model input and append the host's non-secret card guidance once, using the redaction
79
+ * signal rather than display text. `secretRedacted` carries an earlier ingress/composer signal;
80
+ * persistence must continue to use redactUserText/redactUserMessage without the model note. */
81
+ export function guardUserText({ text }, options = {}) {
82
+ const safe = (options.redactUserText ?? redactUserText)({ text }, {});
83
+ if (!safe.secretRedacted && options.secretRedacted !== true)
84
+ return safe.text;
85
+ if (!options.modelNote || safe.text.includes(options.modelNote))
86
+ return safe.text;
87
+ return `${safe.text}\n\n${options.modelNote}`;
88
+ }
89
+ //# sourceMappingURL=user-text-redaction.js.map
@@ -1,121 +1,2 @@
1
- /** Resolved target supplied by the host; the client never derives a path from a title. */
2
- export interface ResolvedPageTarget {
3
- readonly slug: string;
4
- readonly title: string;
5
- readonly path: string;
6
- }
7
- export type PageAction = {
8
- readonly type: 'navigate';
9
- readonly target: ResolvedPageTarget;
10
- readonly auto: boolean;
11
- } | {
12
- readonly type: 'scroll_to' | 'highlight';
13
- readonly target: ResolvedPageTarget;
14
- };
15
- export type NavigateAction = Extract<PageAction, {
16
- type: 'navigate';
17
- }>;
18
- export type NonNavigateAction = Exclude<PageAction, {
19
- type: 'navigate';
20
- }>;
21
- export interface DirectiveCarryingEvent {
22
- readonly kind: string;
23
- readonly name?: string;
24
- readonly data?: unknown;
25
- }
26
- export interface SplitPageActions {
27
- readonly navigate: NavigateAction | null;
28
- readonly other: NonNavigateAction | null;
29
- }
30
- /** Narrows an untrusted, bare queued action. URL authorization remains a separate host policy. O(1). */
31
- export declare function isQueuedPageAction(args: {
32
- value: unknown;
33
- }): args is {
34
- value: PageAction;
35
- };
36
- /** Narrows an untrusted directive envelope, dropping unknown kinds. O(1). */
37
- export declare function isPageActionDirective(args: {
38
- value: unknown;
39
- }): args is {
40
- value: {
41
- kind: 'page_action';
42
- action: PageAction;
43
- };
44
- };
45
- /** Preserves valid action order while ignoring unrelated events. O(events) time and output space. */
46
- export declare function extractPageActions({ events, directiveEventName }: {
47
- events: readonly DirectiveCarryingEvent[] | undefined;
48
- directiveEventName: string;
49
- }): PageAction[];
50
- /** Returns only the first navigation and first page treatment. O(actions), O(1) extra space. */
51
- export declare function splitPageActions({ actions }: {
52
- actions: readonly PageAction[];
53
- }): SplitPageActions;
54
- export interface TargetSelectionPolicy {
55
- readonly candidateSelector: string;
56
- readonly excludedAncestorSelector: string;
57
- }
58
- /** Exact title match first, case-insensitive fallback; never searches excluded UI. O(candidates). */
59
- export declare function findTargetElement({ title, root, selection }: {
60
- title: string;
61
- root: Pick<ParentNode, 'querySelectorAll'>;
62
- selection: TargetSelectionPolicy;
63
- }): Element | null;
64
- export interface PageActionTimerPort {
65
- schedule(args: {
66
- callback: () => void;
67
- delayMs: number;
68
- }): unknown;
69
- cancel(args: {
70
- handle: unknown;
71
- }): void;
72
- }
73
- export interface PageActionPort {
74
- allows(args: {
75
- action: PageAction;
76
- }): boolean;
77
- navigate(args: {
78
- action: NavigateAction;
79
- }): boolean;
80
- execute(args: {
81
- action: NonNavigateAction;
82
- }): boolean;
83
- clearHighlight(args: Record<string, never>): void;
84
- }
85
- export interface BrowserPageActionController extends PageActionPort {
86
- applyHighlight(args: {
87
- element: Element;
88
- }): void;
89
- scrollToElement(args: {
90
- element: Element;
91
- }): void;
92
- }
93
- export interface BrowserPageActionsArgs {
94
- readonly root: Pick<ParentNode, 'querySelectorAll'>;
95
- readonly selection: TargetSelectionPolicy;
96
- readonly allowPath: (args: {
97
- path: string;
98
- }) => boolean;
99
- readonly navigate: (args: {
100
- path: string;
101
- }) => void;
102
- readonly reducedMotion: (args: Record<string, never>) => boolean;
103
- readonly scroll: (args: {
104
- element: Element;
105
- options: ScrollIntoViewOptions;
106
- }) => void;
107
- readonly timers: PageActionTimerPort;
108
- readonly highlightClass: string;
109
- readonly fadeAnimationName: string;
110
- readonly cleanupFallbackMs: number;
111
- }
112
- /**
113
- * Creates page effects with injected DOM, navigation, URL policy, scroll and timers.
114
- * At most one highlight belongs to this instance; cleanup and navigation remove it.
115
- * URL policy is checked before queueing, navigation or page effects. Stale timers and descendant
116
- * animation events cannot remove the current highlight. Navigation paths are resolved by the host,
117
- * never guessed from a title; each controller owns its mutable highlight state.
118
- * @complexity Execute O(candidates); other operations O(1), with one timer/listener retained.
119
- */
120
- export declare function createBrowserPageActions(args: BrowserPageActionsArgs): BrowserPageActionController;
1
+ export * from '../../core/browser/page-actions.js';
121
2
  //# sourceMappingURL=page-actions.d.ts.map
@@ -1,113 +1,3 @@
1
- function record(value) {
2
- return typeof value === 'object' && value !== null && !Array.isArray(value);
3
- }
4
- function target(value) {
5
- return record(value) && typeof value.slug === 'string' && typeof value.title === 'string' && typeof value.path === 'string';
6
- }
7
- /** Narrows an untrusted, bare queued action. URL authorization remains a separate host policy. O(1). */
8
- export function isQueuedPageAction(args) {
9
- const { value } = args;
10
- if (!record(value) || !target(value.target))
11
- return false;
12
- return value.type === 'navigate' ? typeof value.auto === 'boolean' : value.type === 'scroll_to' || value.type === 'highlight';
13
- }
14
- /** Narrows an untrusted directive envelope, dropping unknown kinds. O(1). */
15
- export function isPageActionDirective(args) {
16
- const { value } = args;
17
- return record(value) && value.kind === 'page_action' && isQueuedPageAction({ value: value.action });
18
- }
19
- /** Preserves valid action order while ignoring unrelated events. O(events) time and output space. */
20
- export function extractPageActions({ events, directiveEventName }) {
21
- const actions = [];
22
- for (const event of events ?? []) {
23
- const envelope = { value: event.data };
24
- if (event.kind === 'ext' && event.name === directiveEventName && isPageActionDirective(envelope)) {
25
- actions.push(envelope.value.action);
26
- }
27
- }
28
- return actions;
29
- }
30
- /** Returns only the first navigation and first page treatment. O(actions), O(1) extra space. */
31
- export function splitPageActions({ actions }) {
32
- return {
33
- navigate: actions.find((a) => a.type === 'navigate') ?? null,
34
- other: actions.find((a) => a.type !== 'navigate') ?? null,
35
- };
36
- }
37
- /** Exact title match first, case-insensitive fallback; never searches excluded UI. O(candidates). */
38
- export function findTargetElement({ title, root, selection }) {
39
- const needle = title.trim();
40
- if (!needle)
41
- return null;
42
- const candidates = Array.from(root.querySelectorAll(selection.candidateSelector))
43
- .filter(el => !el.closest(selection.excludedAncestorSelector));
44
- return candidates.find(el => (el.textContent ?? '').trim() === needle)
45
- ?? candidates.find(el => (el.textContent ?? '').trim().toLowerCase() === needle.toLowerCase()) ?? null;
46
- }
47
- /**
48
- * Creates page effects with injected DOM, navigation, URL policy, scroll and timers.
49
- * At most one highlight belongs to this instance; cleanup and navigation remove it.
50
- * URL policy is checked before queueing, navigation or page effects. Stale timers and descendant
51
- * animation events cannot remove the current highlight. Navigation paths are resolved by the host,
52
- * never guessed from a title; each controller owns its mutable highlight state.
53
- * @complexity Execute O(candidates); other operations O(1), with one timer/listener retained.
54
- */
55
- export function createBrowserPageActions(args) {
56
- if (!Number.isFinite(args.cleanupFallbackMs) || args.cleanupFallbackMs < 0)
57
- throw new Error('Invalid highlight deadline');
58
- let current = null;
59
- const clearHighlight = (_args) => {
60
- if (!current)
61
- return;
62
- const old = current;
63
- current = null;
64
- args.timers.cancel({ handle: old.handle });
65
- old.element.removeEventListener('animationend', old.onEnd);
66
- old.element.classList.remove(args.highlightClass);
67
- };
68
- function highlight(element) {
69
- if (current?.element === element)
70
- return;
71
- clearHighlight({});
72
- const onEnd = event => {
73
- if (event.target === element && event.animationName === args.fadeAnimationName)
74
- clearHighlight({});
75
- };
76
- element.classList.add(args.highlightClass);
77
- element.addEventListener('animationend', onEnd);
78
- // Identity check makes even a late callback from a canceled timer harmless.
79
- const handle = args.timers.schedule({ delayMs: args.cleanupFallbackMs, callback: () => {
80
- if (current?.element === element && current.onEnd === onEnd)
81
- clearHighlight({});
82
- } });
83
- current = { element, handle, onEnd };
84
- }
85
- const scrollToElement = ({ element }) => {
86
- args.scroll({ element, options: { behavior: args.reducedMotion({}) ? 'auto' : 'smooth', block: 'center' } });
87
- };
88
- return {
89
- applyHighlight({ element }) { highlight(element); },
90
- scrollToElement,
91
- allows({ action }) { return args.allowPath({ path: action.target.path }); },
92
- navigate({ action }) {
93
- if (!args.allowPath({ path: action.target.path }))
94
- return false;
95
- clearHighlight({});
96
- args.navigate({ path: action.target.path });
97
- return true;
98
- },
99
- execute({ action }) {
100
- if (!args.allowPath({ path: action.target.path }))
101
- return false;
102
- const element = findTargetElement({ title: action.target.title, root: args.root, selection: args.selection });
103
- if (!element)
104
- return false;
105
- scrollToElement({ element });
106
- if (action.type === 'highlight')
107
- highlight(element);
108
- return true;
109
- },
110
- clearHighlight,
111
- };
112
- }
1
+ // Compatibility path; shared page-action decisions belong to chat core.
2
+ export * from '../../core/browser/page-actions.js';
113
3
  //# sourceMappingURL=page-actions.js.map
@@ -1,7 +1,7 @@
1
1
  import type { ChatTransport } from '../../core/transport.js';
2
2
  import type { ChatPaneAgent } from '../../react/chat-pane.js';
3
3
  import type { BrowserSessionStore } from '../browser/session-store.js';
4
- import type { PageActionPort } from '../browser/page-actions.js';
4
+ import type { PageActionPort } from '../../core/browser/page-actions.js';
5
5
  import type { EmbedHeaderCopy, EscapeKeyPort } from './header.js';
6
6
  export { EmbedChatHeader } from './header.js';
7
7
  export type { EmbedChatHeaderArgs, EmbedHeaderCopy, EscapeKeyPort } from './header.js';
@@ -5,7 +5,7 @@ import { isTerminalRunStatus } from '../../core/messages.js';
5
5
  import { ChatPane } from '../../react/chat-pane.js';
6
6
  import { ChatFab } from '../../react/components/ChatFab.js';
7
7
  import { JiniChatProvider } from '../../react/components/JiniChatProvider.js';
8
- import { extractPageActions, splitPageActions, isQueuedPageAction } from '../browser/page-actions.js';
8
+ import { extractPageActions, splitPageActions, isQueuedPageAction } from '../../core/browser/page-actions.js';
9
9
  import { EmbedChatHeader } from './header.js';
10
10
  export { EmbedChatHeader } from './header.js';
11
11
  const shellStyles = `
@@ -1,22 +1,16 @@
1
1
  /**
2
2
  * @module artifact-types
3
3
  *
4
- * Minimal `ArtifactFile`/`ArtifactRenderer`/`RendererRegistry` shapes,
5
- * defined locally per this task's SCOPE NOTE: r4b calls the artifact-renderer
6
- * package `@jini-ai/artifacts-react`; the actual locked name
7
- * (`ADS-memory/reports/jini-port/extraction-plan.md` §3) is `@jini-ai/renderers-react`, which
8
- * is still a placeholder stub being built in a separate session. `chat-react`
9
- * needs these shapes now (for `useArtifactStream` and the `ArtifactFile`
10
- * field on `ProjectContextValue`), so they are defined here to the exact
11
- * shape r4b §2 specifies, with the intent that a future pass re-points every
12
- * import below at `@jini-ai/renderers-react`'s real exports once that package
13
- * lands, deletes this file, and removes the re-export from `index.ts`.
4
+ * Chat's public streaming artifact contracts, used by useArtifactStream and
5
+ * ProjectContextValue. The renderer implementation is available in @jini-ai/ui,
6
+ * but direct delegation would change this compatibility surface: chat registers
7
+ * mutably, resolves newest-first, returns unregister handles, accepts files
8
+ * without manifests and returns the matched file. UI's registry is immutable,
9
+ * replaces registrations by ID, requires a manifest and returns that manifest;
10
+ * its predicate and resolve signatures also pass hints as a separate option.
14
11
  *
15
- * TODO(renderers-react): replace this file's contents with
16
- * `export * from '@jini-ai/renderers-react'` (or the equivalent named
17
- * re-exports) once `@jini-ai/renderers-react` ships real `ArtifactFile`/
18
- * `ArtifactRenderer`/`RendererRegistry` implementations. Do not block this
19
- * package's other work on that package landing first.
12
+ * TODO(renderer-delegation): Coordinator must resolve these contracts before
13
+ * delegating to UI. A wildcard re-export is not a behavior-preserving migration.
20
14
  */
21
15
  import type { ArtifactManifest } from '../core/index.js';
22
16
  /** Generic artifact-file shape a host's project/workspace file maps onto. */
@@ -15,6 +15,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
15
15
  import { useEffect, useLayoutEffect, useRef, useState, } from 'react';
16
16
  import { RemixIcon } from '@jini-ai/ui';
17
17
  import { useT } from '../hooks/context.js';
18
+ import { useComposerHistoryKeys } from '../hooks/useComposerHistoryKeys.hooks.js';
18
19
  import { AttachmentTray } from './AttachmentTray.js';
19
20
  import { ComposerDiscoveryMenu, ComposerSlashMenu } from './ComposerDiscovery.js';
20
21
  import { appendComposerDiscovery, composerDiscoveryMenuPosition, composerSlashMenuPosition, filterComposerDiscovery, parseComposerSlashQuery, replaceComposerSlashTrigger, resolveComposerSlashInvocation, resolveComposerSlashKeyAction, } from './composer-discovery.js';
@@ -103,6 +104,9 @@ export function Composer({ composer, onSend, disabled = false, sendDisabled = fa
103
104
  * point the shrinking list is its own feedback.
104
105
  */
105
106
  const showSlashFilterHint = slashQuery !== null && slashQuery.command === '' && slashQuery.argument === null;
107
+ const handleHistoryKey = useComposerHistoryKeys({ composer, textareaRef }, {
108
+ popupOpen: slashOpen || discoveryMenuOpen || workdirOpen || composer.mention.open,
109
+ });
106
110
  function restoreComposerFocus() {
107
111
  textareaRef.current?.focus();
108
112
  }
@@ -286,6 +290,8 @@ export function Composer({ composer, onSend, disabled = false, sendDisabled = fa
286
290
  return;
287
291
  }
288
292
  }
293
+ if (handleHistoryKey(event))
294
+ return;
289
295
  if (event.key !== 'Enter' || event.shiftKey)
290
296
  return;
291
297
  event.preventDefault();
@@ -1,4 +1,5 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { terminalMessageNotice, continuingRunNotice } from '../../core/durable-projection.js';
2
3
  /**
3
4
  * @module MessageRow
4
5
  *
@@ -120,6 +121,23 @@ function UsageSummary({ usage }) {
120
121
  return null;
121
122
  return (_jsxs("div", { className: "jini-message-usage", children: [_jsx("span", { className: "jini-message-usage-dot", "aria-hidden": true, children: "\u25CF" }), t('Done'), " \u00B7 ", parts.join(' · ')] }));
122
123
  }
124
+ function messageBlocks({ visibleContent, message, rows }, _optional = {}) {
125
+ // Interleaving is skipped outright when an artifact was stripped. `stripArtifact` operates on the
126
+ // whole string and trims, so the surviving text no longer lines up with the per-run offsets the
127
+ // event walk produces, and an artifact block could legitimately span a tool call. Rather than
128
+ // reason about partial overlaps, an artifact-bearing message keeps the flat layout — those
129
+ // messages are dominated by the artifact panel anyway, so the ordering matters least there.
130
+ if (visibleContent !== message.content)
131
+ return null;
132
+ return interleaveMessageBlocks({ events: message.events, content: message.content, rows }, { slotOf: (ev) => extEventSlot({ name: ev.name, data: ev.data }) });
133
+ }
134
+ function messageAwaitingAnswer({ runStreaming, runInProgress, events }, _optional = {}) {
135
+ return runStreaming === true && runInProgress && isAwaitingAnswer({ events });
136
+ }
137
+ function AssistantMessageFooter({ message, usageEvent, terminalNotice, pending, runInProgress, runStreaming, visibleContent }, _optional = {}) {
138
+ const t = useT();
139
+ return _jsxs(_Fragment, { children: [usageEvent ? _jsx(UsageSummary, { usage: usageEvent }) : null, terminalNotice ? _jsx("div", { className: "jini-message-error", role: "status", children: t(terminalNotice) }) : null, !pending && !runInProgress ? (_jsx("div", { className: "jini-message-actions jini-message-actions--assistant", children: _jsx(CopyMessageButton, { text: visibleContent, label: t('Copy message') }) })) : null, runInProgress ? _jsx(RunActivityLine, { events: message.events, active: pending || (runStreaming === true && runInProgress) }) : null] });
140
+ }
123
141
  /**
124
142
  * Quiet, icon-only copy affordance shared by the user- and assistant-message
125
143
  * action rows below. Delegates the actual clipboard write (and its
@@ -147,20 +165,14 @@ export function MessageRow({ message, runStreaming = false, runSucceeded = false
147
165
  if (message.role === 'user') {
148
166
  return (_jsxs("div", { className: "jini-message jini-message-user", "data-message-id": message.id, "data-agent-element": `chat-message-${message.id}`, "data-agent-role": "region", "data-agent-label": "A message from the user", children: [message.attachments && message.attachments.length > 0 ? (_jsx("div", { className: "jini-message-attachments", children: message.attachments.map((a) => (_jsx("button", { type: "button", className: "jini-message-attachment-chip", onClick: () => setOpenAttachment(a), title: t('Open {name}', { name: a.name }), "aria-label": t('Open {name}', { name: a.name }), children: renderAttachment ? renderAttachment(a) : a.name }, a.path))) })) : null, _jsx("div", { className: "jini-message-content", children: message.content }), _jsx("div", { className: "jini-message-actions jini-message-actions--user", children: _jsx(CopyMessageButton, { text: message.content, label: t('Copy message') }) }), openAttachment ? (_jsx(AttachmentPreviewModal, { attachment: openAttachment, onClose: () => setOpenAttachment(null) })) : null] }));
149
167
  }
168
+ const terminalNotice = terminalMessageNotice({ message }, {});
150
169
  const visibleContent = stripArtifact({ content: message.content });
151
170
  const segments = splitOnQuestionForms({ input: visibleContent });
152
171
  const usageEvent = message.events?.filter((ev) => ev.kind === 'usage').pop();
153
- // Interleaving is skipped outright when an artifact was stripped. `stripArtifact` operates on the
154
- // whole string and trims, so the surviving text no longer lines up with the per-run offsets the
155
- // event walk produces, and an artifact block could legitimately span a tool call. Rather than
156
- // reason about partial overlaps, an artifact-bearing message keeps the flat layout — those
157
- // messages are dominated by the artifact panel anyway, so the ordering matters least there.
158
- const blocks = visibleContent === message.content
159
- ? interleaveMessageBlocks({ events: message.events, content: message.content, rows: timeline.rows }, { slotOf: (ev) => extEventSlot({ name: ev.name, data: ev.data }) })
160
- : null;
172
+ const blocks = messageBlocks({ visibleContent, message, rows: timeline.rows }, {});
161
173
  const pending = isPendingWithNoContent(message, visibleContent, timeline.rows.length);
162
174
  const runInProgress = isRunInProgress(message.runStatus);
163
- const awaitingAnswer = runStreaming && runInProgress && isAwaitingAnswer({ events: message.events });
175
+ const awaitingAnswer = messageAwaitingAnswer({ runStreaming, runInProgress, events: message.events }, {});
164
176
  const renderToolCard = (row) => (_jsx(ToolCard, { use: row.use, result: row.result, runStreaming: runStreaming, runSucceeded: runSucceeded, ...(projectFileNames !== undefined ? { projectFileNames } : {}), ...(onRequestOpenFile !== undefined ? { onRequestOpenFile } : {}) }, row.id));
165
177
  const renderExtGroup = (group) => {
166
178
  const renderer = getExtEventRenderer({ name: group.name });
@@ -197,7 +209,7 @@ export function MessageRow({ message, runStreaming = false, runSucceeded = false
197
209
  return null;
198
210
  return (_jsx("div", { className: "jini-message-ext-events", children: renderExtGroup(group) }, block.key));
199
211
  })
200
- : (_jsxs(_Fragment, { children: [segments.map((segment, i) => segment.kind === 'text' ? (segment.text.trim() ? (_jsx("div", { className: "jini-message-content", children: _jsx(Markdown, { children: segment.text }) }, i)) : null) : (_jsx(QuestionForm, { form: segment.form, interactive: questionFormInteractive, ...(questionFormSubmittedAnswers !== undefined ? { submittedAnswers: questionFormSubmittedAnswers } : {}), ...(onQuestionFormSubmit !== undefined ? { onSubmit: onQuestionFormSubmit } : {}) }, i))), timeline.rows.length > 0 ? (_jsx("div", { className: "jini-message-tools", children: timeline.rows.map(renderToolCard) })) : null, extGroups.length > 0 ? _jsx("div", { className: "jini-message-ext-events", children: extGroups.map(renderExtGroup) }) : null] })), usageEvent ? _jsx(UsageSummary, { usage: usageEvent }) : null, message.runStatus === 'failed' ? _jsx("div", { className: "jini-message-error", children: t('This turn failed.') }) : null, !pending && !runInProgress ? (_jsx("div", { className: "jini-message-actions jini-message-actions--assistant", children: _jsx(CopyMessageButton, { text: visibleContent, label: t('Copy message') }) })) : null, _jsx(RunActivityLine, { events: message.events, active: pending || (runStreaming === true && runInProgress) })] }));
212
+ : (_jsxs(_Fragment, { children: [segments.map((segment, i) => segment.kind === 'text' ? (segment.text.trim() ? (_jsx("div", { className: "jini-message-content", children: _jsx(Markdown, { children: segment.text }) }, i)) : null) : (_jsx(QuestionForm, { form: segment.form, interactive: questionFormInteractive, ...(questionFormSubmittedAnswers !== undefined ? { submittedAnswers: questionFormSubmittedAnswers } : {}), ...(onQuestionFormSubmit !== undefined ? { onSubmit: onQuestionFormSubmit } : {}) }, i))), timeline.rows.length > 0 ? (_jsx("div", { className: "jini-message-tools", children: timeline.rows.map(renderToolCard) })) : null, extGroups.length > 0 ? _jsx("div", { className: "jini-message-ext-events", children: extGroups.map(renderExtGroup) }) : null] })), _jsx(AssistantMessageFooter, { message, usageEvent, terminalNotice, pending, runInProgress, runStreaming, visibleContent })] }));
201
213
  }
202
214
  /**
203
215
  * The live "what is it doing" line under a running reply (see `core/run-activity.ts`). Replaces the
@@ -205,7 +217,8 @@ export function MessageRow({ message, runStreaming = false, runSucceeded = false
205
217
  * Kept a separate component so its one-second tick re-renders only this line, not the whole row.
206
218
  */
207
219
  function RunActivityLine({ events, active }) {
208
- const label = useRunActivity({ events: events, active: active });
220
+ const activityLabel = useRunActivity({ events: events, active: active });
221
+ const label = continuingRunNotice({ events, active }, {}) ?? activityLabel;
209
222
  if (label === null)
210
223
  return null;
211
224
  return (_jsx("div", { className: "jini-message-pending jini-run-activity", "aria-live": "off", children: label }));
@@ -1,3 +1,4 @@
1
+ import { type UserTextRedactionOptions } from '../../core/user-text-redaction.js';
1
2
  import type { QuestionForm as QuestionFormType } from '../../core/index.js';
2
3
  export interface QuestionFormFileSubmission {
3
4
  questionId: string;
@@ -9,7 +10,7 @@ export interface QuestionFormHandle {
9
10
  /** Submit with no answers — every question is optional, so this records each as "(skipped)" and moves on. */
10
11
  skipAll: () => void;
11
12
  }
12
- export interface QuestionFormProps {
13
+ export interface QuestionFormProps extends UserTextRedactionOptions {
13
14
  form: QuestionFormType;
14
15
  /** Whether the user can still submit answers — the host disables this once the turn is no longer the most recent one. */
15
16
  interactive: boolean;
@@ -1,4 +1,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { CREDENTIAL_CARD_GUIDANCE, SECRET_REDACTED_NOTICE } from '../../core/user-text-redaction.js';
3
+ import { useUserTextGuard } from '../hooks/useUserTextGuard.js';
2
4
  /**
3
5
  * @module QuestionForm
4
6
  *
@@ -13,15 +15,17 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
13
15
  * string wrapped in `useT()`.
14
16
  */
15
17
  import { forwardRef, useEffect, useImperativeHandle, useMemo, useState } from 'react';
16
- import { formatFormAnswers, formOptionValueForLabel } from '../../core/index.js';
18
+ import { formatFormAnswers, formOptionValueForLabel, redactQuestionAnswers, isSecretQuestion, withoutSecretQuestionDefaults, questionTextInputProps, questionTextareaProps } from '../../core/index.js';
17
19
  import { useT } from '../hooks/context.js';
18
- export const QuestionForm = forwardRef(function QuestionForm({ form, interactive, submittedAnswers, hideInternalSubmit = false, draftAnswers, onReadyChange, onDraftChange, onAnswerChange, onSubmit }, ref) {
20
+ export const QuestionForm = forwardRef(function QuestionForm({ form: incomingForm, interactive, submittedAnswers, hideInternalSubmit = false, draftAnswers, onReadyChange, onDraftChange, onAnswerChange, onSubmit, redactUserText, onSecretRedacted }, ref) {
19
21
  const t = useT();
22
+ const form = useMemo(() => withoutSecretQuestionDefaults({ form: incomingForm }), [incomingForm]);
23
+ const guard = useUserTextGuard({ ...(redactUserText ? { redactUserText } : {}), ...(onSecretRedacted ? { onSecretRedacted } : {}) });
20
24
  const initial = useMemo(() => buildInitialState(form, submittedAnswers, draftAnswers), [form, submittedAnswers, draftAnswers]);
21
25
  const [answers, setAnswers] = useState(initial);
22
26
  const [fileAnswers, setFileAnswers] = useState({});
23
27
  const locked = !interactive || !onSubmit || submittedAnswers !== undefined;
24
- const currentAnswers = submittedAnswers ?? answers;
28
+ const currentAnswers = submittedAnswers ? redactQuestionAnswers({ form, answers: submittedAnswers }).answers : answers;
25
29
  useEffect(() => {
26
30
  setFileAnswers({});
27
31
  }, [form.id]);
@@ -51,8 +55,8 @@ export const QuestionForm = forwardRef(function QuestionForm({ form, interactive
51
55
  return;
52
56
  const next = { ...answers, [id]: value };
53
57
  setAnswers(next);
54
- onDraftChange?.(draftSafeAnswers(form, next));
55
- onAnswerChange?.(id, value);
58
+ onDraftChange?.(redactQuestionAnswers({ form, answers: draftSafeAnswers(form, next) }).answers);
59
+ onAnswerChange?.(id, redactQuestionAnswers({ form, answers: { [id]: value } }).answers[id]);
56
60
  }
57
61
  function toggleCheckbox(id, option, maxSelections) {
58
62
  if (locked)
@@ -64,7 +68,7 @@ export const QuestionForm = forwardRef(function QuestionForm({ form, interactive
64
68
  const next = has ? current.filter((v) => v !== option) : [...current, option];
65
69
  const nextAnswers = { ...answers, [id]: next };
66
70
  setAnswers(nextAnswers);
67
- onDraftChange?.(draftSafeAnswers(form, nextAnswers));
71
+ onDraftChange?.(redactQuestionAnswers({ form, answers: draftSafeAnswers(form, nextAnswers) }).answers);
68
72
  onAnswerChange?.(id, next);
69
73
  }
70
74
  function updateCheckboxCustom(q, raw) {
@@ -79,11 +83,13 @@ export const QuestionForm = forwardRef(function QuestionForm({ form, interactive
79
83
  return;
80
84
  if (!ready)
81
85
  return;
86
+ const safe = redactQuestionAnswers({ form, answers }, { ...(redactUserText ? { redactUserText } : {}), onSecretRedacted: guard.notifyRedaction });
87
+ setAnswers(safe.answers);
82
88
  const files = collectFileSubmissions(form, fileAnswers);
83
89
  if (files.length > 0)
84
- onSubmit(formatFormAnswers({ form: form, answers: answers }), answers, files);
90
+ onSubmit(formatFormAnswers({ form: form, answers: safe.answers }), safe.answers, files);
85
91
  else
86
- onSubmit(formatFormAnswers({ form: form, answers: answers }), answers);
92
+ onSubmit(formatFormAnswers({ form: form, answers: safe.answers }), safe.answers);
87
93
  }
88
94
  function handleSkipAll() {
89
95
  if (locked || !onSubmit)
@@ -113,18 +119,18 @@ export const QuestionForm = forwardRef(function QuestionForm({ form, interactive
113
119
  }, [onReadyChange, locked, ready]);
114
120
  return (_jsxs("div", { className: `question-form${locked ? ' question-form-locked' : ''}`, "data-form-id": form.id, children: [_jsxs("div", { className: "question-form-head", children: [_jsx("span", { className: "question-form-icon", "aria-hidden": true, children: "?" }), _jsxs("div", { className: "question-form-titles", children: [_jsx("div", { className: "question-form-title", children: form.title }), form.description ? _jsx("div", { className: "question-form-desc", children: form.description }) : null] }), locked ? _jsx("span", { className: "question-form-pill", children: t('Answered') }) : null] }), _jsx("div", { className: "question-form-body", children: form.questions.map((q) => {
115
121
  const value = currentAnswers[q.id];
116
- return (_jsxs("div", { className: "qf-field", children: [_jsxs("label", { className: "qf-label", children: [_jsx("span", { children: q.label }), q.required ? (_jsx("span", { className: "qf-required", "aria-label": t('required'), children: "*" })) : null] }), q.help ? _jsx("div", { className: "qf-help", children: q.help }) : null, q.type === 'radio' && q.options ? (_jsx("div", { className: "qf-options", children: q.options.map((opt) => (_jsxs("label", { className: `qf-chip${value === opt.value ? ' qf-chip-on' : ''}`, title: opt.description, children: [_jsx("input", { type: "radio", name: `${form.id}-${q.id}`, value: opt.value, checked: value === opt.value, disabled: locked, "aria-label": opt.label, onChange: () => update(q.id, opt.value) }), _jsx(OptionCopy, { option: opt })] }, opt.value))) })) : null, q.type === 'radio' && q.options && shouldRenderCustomChoice(q) ? (_jsx(CustomChoiceInput, { label: q.customLabel ?? t('Something else'), value: customSingleValue(q, value), placeholder: q.customPlaceholder ?? t('Type your own answer'), disabled: locked, onChange: (next) => update(q.id, next) })) : null, q.type === 'checkbox' && q.options ? (_jsx("div", { className: "qf-options", children: q.options.map((opt) => {
122
+ return (_jsxs("div", { className: "qf-field", children: [_jsxs("label", { className: "qf-label", children: [_jsx("span", { children: q.label }), q.required ? (_jsx("span", { className: "qf-required", "aria-label": t('required'), children: "*" })) : null] }), q.help ? _jsx("div", { className: "qf-help", children: q.help }) : null, isSecretQuestion({ question: q }) ? _jsx("div", { className: "qf-help", children: t(CREDENTIAL_CARD_GUIDANCE) }) : null, q.type === 'radio' && q.options ? (_jsx("div", { className: "qf-options", children: q.options.map((opt) => (_jsxs("label", { className: `qf-chip${value === opt.value ? ' qf-chip-on' : ''}`, title: opt.description, children: [_jsx("input", { type: "radio", name: `${form.id}-${q.id}`, value: opt.value, checked: value === opt.value, disabled: locked, "aria-label": opt.label, onChange: () => update(q.id, opt.value) }), _jsx(OptionCopy, { option: opt })] }, opt.value))) })) : null, q.type === 'radio' && q.options && shouldRenderCustomChoice(q) ? (_jsx(CustomChoiceInput, { label: q.customLabel ?? t('Something else'), value: customSingleValue(q, value), placeholder: q.customPlaceholder ?? t('Type your own answer'), disabled: locked, onChange: (next) => update(q.id, next) })) : null, q.type === 'checkbox' && q.options ? (_jsx("div", { className: "qf-options", children: q.options.map((opt) => {
117
123
  const arr = Array.isArray(value) ? value : [];
118
124
  const on = arr.includes(opt.value);
119
125
  const maxed = q.maxSelections !== undefined && !on && arr.length >= q.maxSelections;
120
126
  return (_jsxs("label", { title: opt.description, className: `qf-chip${on ? ' qf-chip-on' : ''}${maxed ? ' qf-chip-disabled' : ''}`, children: [_jsx("input", { type: "checkbox", value: opt.value, checked: on, disabled: locked || maxed, "aria-label": opt.label, onChange: () => toggleCheckbox(q.id, opt.value, q.maxSelections) }), _jsx(OptionCopy, { option: opt })] }, opt.value));
121
- }) })) : null, q.type === 'checkbox' && q.options && shouldRenderCustomChoice(q) ? (_jsx(CustomChoiceInput, { label: q.customLabel ?? t('Something else'), value: customCheckboxValue(q, value), placeholder: q.customPlaceholder ?? t('Type your own answer'), disabled: locked, onChange: (next) => updateCheckboxCustom(q, next) })) : null, q.type === 'select' && q.options ? (_jsxs("select", { className: "qf-select", value: typeof value === 'string' && questionValueIsKnown(q, value) ? value : '', disabled: locked, onChange: (e) => update(q.id, e.target.value), children: [_jsx("option", { value: "", disabled: true, children: q.placeholder ?? t('Choose one') }), q.options.map((opt) => (_jsx("option", { value: opt.value, title: opt.description, children: opt.label }, opt.value)))] })) : null, q.type === 'select' && q.options && shouldRenderCustomChoice(q) ? (_jsx(CustomChoiceInput, { label: q.customLabel ?? t('Something else'), value: customSingleValue(q, value), placeholder: q.customPlaceholder ?? t('Type your own answer'), disabled: locked, onChange: (next) => update(q.id, next) })) : null, q.type === 'text' ? _jsx("input", { type: "text", className: "qf-input", value: typeof value === 'string' ? value : '', placeholder: q.placeholder, disabled: locked, onChange: (e) => update(q.id, e.target.value) }) : null, q.type === 'number' ? _jsx("input", { type: "number", className: "qf-input", value: typeof value === 'string' ? value : '', placeholder: q.placeholder, min: q.min, max: q.max, step: q.step, disabled: locked, onChange: (e) => update(q.id, e.target.value) }) : null, q.type === 'range' ? (_jsxs("div", { className: "qf-range-wrap", children: [_jsx("input", { type: "range", className: "qf-range", value: typeof value === 'string' && value.trim() ? value : String(q.min ?? 0), min: q.min, max: q.max, step: q.step, disabled: locked, onChange: (e) => update(q.id, e.target.value) }), _jsx("output", { className: "qf-range-value", children: typeof value === 'string' && value.trim() ? value : String(q.min ?? 0) })] })) : null, q.type === 'date' || q.type === 'time' || q.type === 'datetime-local' ? _jsx("input", { type: q.type, className: "qf-input", value: typeof value === 'string' ? value : '', placeholder: q.placeholder, disabled: locked, onChange: (e) => update(q.id, e.target.value) }) : null, q.type === 'color' ? _jsx("input", { type: "color", className: "qf-color", value: normalizeColorInputValue(value), disabled: locked, onChange: (e) => update(q.id, e.target.value) }) : null, q.type === 'url' || q.type === 'email' || q.type === 'tel' ? _jsx("input", { type: q.type, className: "qf-input", value: typeof value === 'string' ? value : '', placeholder: q.placeholder, disabled: locked, onChange: (e) => update(q.id, e.target.value) }) : null, q.type === 'file' ? (_jsxs("div", { className: "qf-file-wrap", children: [_jsx("input", { type: "file", className: "qf-file", multiple: q.multiple, accept: q.accept, disabled: locked, onChange: (e) => {
127
+ }) })) : null, q.type === 'checkbox' && q.options && shouldRenderCustomChoice(q) ? (_jsx(CustomChoiceInput, { label: q.customLabel ?? t('Something else'), value: customCheckboxValue(q, value), placeholder: q.customPlaceholder ?? t('Type your own answer'), disabled: locked, onChange: (next) => updateCheckboxCustom(q, next) })) : null, q.type === 'select' && q.options ? (_jsxs("select", { className: "qf-select", value: typeof value === 'string' && questionValueIsKnown(q, value) ? value : '', disabled: locked, onChange: (e) => update(q.id, e.target.value), children: [_jsx("option", { value: "", disabled: true, children: q.placeholder ?? t('Choose one') }), q.options.map((opt) => (_jsx("option", { value: opt.value, title: opt.description, children: opt.label }, opt.value)))] })) : null, q.type === 'select' && q.options && shouldRenderCustomChoice(q) ? (_jsx(CustomChoiceInput, { label: q.customLabel ?? t('Something else'), value: customSingleValue(q, value), placeholder: q.customPlaceholder ?? t('Type your own answer'), disabled: locked, onChange: (next) => update(q.id, next) })) : null, q.type === 'text' ? _jsx("input", { ...questionTextInputProps({ question: q }), className: "qf-input", value: typeof value === 'string' ? value : '', placeholder: q.placeholder, disabled: locked, onChange: (e) => update(q.id, e.target.value) }) : null, q.type === 'number' ? _jsx("input", { type: "number", className: "qf-input", value: typeof value === 'string' ? value : '', placeholder: q.placeholder, min: q.min, max: q.max, step: q.step, disabled: locked, onChange: (e) => update(q.id, e.target.value) }) : null, q.type === 'range' ? (_jsxs("div", { className: "qf-range-wrap", children: [_jsx("input", { type: "range", className: "qf-range", value: typeof value === 'string' && value.trim() ? value : String(q.min ?? 0), min: q.min, max: q.max, step: q.step, disabled: locked, onChange: (e) => update(q.id, e.target.value) }), _jsx("output", { className: "qf-range-value", children: typeof value === 'string' && value.trim() ? value : String(q.min ?? 0) })] })) : null, q.type === 'date' || q.type === 'time' || q.type === 'datetime-local' ? _jsx("input", { type: q.type, className: "qf-input", value: typeof value === 'string' ? value : '', placeholder: q.placeholder, disabled: locked, onChange: (e) => update(q.id, e.target.value) }) : null, q.type === 'color' ? _jsx("input", { type: "color", className: "qf-color", value: normalizeColorInputValue(value), disabled: locked, onChange: (e) => update(q.id, e.target.value) }) : null, q.type === 'url' || q.type === 'email' || q.type === 'tel' ? _jsx("input", { type: q.type, className: "qf-input", value: typeof value === 'string' ? value : '', placeholder: q.placeholder, disabled: locked, onChange: (e) => update(q.id, e.target.value) }) : null, q.type === 'file' ? (_jsxs("div", { className: "qf-file-wrap", children: [_jsx("input", { type: "file", className: "qf-file", multiple: q.multiple, accept: q.accept, disabled: locked, onChange: (e) => {
122
128
  const files = Array.from(e.target.files ?? []);
123
129
  const names = files.map((file) => file.name);
124
130
  setFileAnswers((current) => ({ ...current, [q.id]: files }));
125
131
  update(q.id, q.multiple ? names : (names[0] ?? ''));
126
- } }), fileValueLabel(value) ? _jsx("div", { className: "qf-file-summary", children: fileValueLabel(value) }) : null] })) : null, q.type === 'switch' ? (_jsxs("label", { className: "qf-switch", children: [_jsx("input", { type: "checkbox", role: "switch", checked: value === 'true', disabled: locked, onChange: (e) => update(q.id, e.target.checked ? 'true' : 'false') }), _jsx("span", { "aria-hidden": true })] })) : null, q.type === 'textarea' ? _jsx("textarea", { className: "qf-textarea", value: typeof value === 'string' ? value : '', placeholder: q.placeholder, disabled: locked, rows: 3, onChange: (e) => update(q.id, e.target.value) }) : null, q.type === 'direction-cards' && q.cards && q.cards.length > 0 ? (_jsx("div", { className: "qf-direction-cards", children: q.cards.map((card) => (_jsx(DirectionCardView, { card: card, formId: form.id, questionId: q.id, selected: value === card.id || value === card.label, disabled: locked, onSelect: () => update(q.id, card.id) }, card.id))) })) : null, q.type === 'direction-cards' && q.cards && q.cards.length > 0 && shouldRenderCustomChoice(q) ? (_jsx(CustomChoiceInput, { label: q.customLabel ?? t('Something else'), value: customSingleValue(q, value), placeholder: q.customPlaceholder ?? t('Type your own answer'), disabled: locked, onChange: (next) => update(q.id, next) })) : null] }, q.id));
127
- }) }), hideInternalSubmit ? null : (_jsxs("div", { className: "question-form-foot", children: [locked ? _jsx("span", { className: "qf-locked-note", children: submittedAnswers ? t('You answered this') : t('A newer message answered this') }) : _jsx("span", { className: "qf-hint", children: t('Answer above, then continue') }), !locked ? (_jsx("button", { type: "button", className: "primary", onClick: handleSubmit, disabled: !ready, title: ready ? t('Submit your answers') : t('Answer the required questions first'), children: form.submitLabel ?? t('Continue') })) : null] }))] }));
132
+ } }), fileValueLabel(value) ? _jsx("div", { className: "qf-file-summary", children: fileValueLabel(value) }) : null] })) : null, q.type === 'switch' ? (_jsxs("label", { className: "qf-switch", children: [_jsx("input", { type: "checkbox", role: "switch", checked: value === 'true', disabled: locked, onChange: (e) => update(q.id, e.target.checked ? 'true' : 'false') }), _jsx("span", { "aria-hidden": true })] })) : null, q.type === 'textarea' ? _jsx("textarea", { ...questionTextareaProps({ question: q }), className: "qf-textarea", value: typeof value === 'string' ? value : '', placeholder: q.placeholder, disabled: locked, rows: 3, onChange: (e) => update(q.id, e.target.value) }) : null, q.type === 'direction-cards' && q.cards && q.cards.length > 0 ? (_jsx("div", { className: "qf-direction-cards", children: q.cards.map((card) => (_jsx(DirectionCardView, { card: card, formId: form.id, questionId: q.id, selected: value === card.id || value === card.label, disabled: locked, onSelect: () => update(q.id, card.id) }, card.id))) })) : null, q.type === 'direction-cards' && q.cards && q.cards.length > 0 && shouldRenderCustomChoice(q) ? (_jsx(CustomChoiceInput, { label: q.customLabel ?? t('Something else'), value: customSingleValue(q, value), placeholder: q.customPlaceholder ?? t('Type your own answer'), disabled: locked, onChange: (next) => update(q.id, next) })) : null] }, q.id));
133
+ }) }), guard.secretRedacted ? _jsx("div", { role: "status", children: t(SECRET_REDACTED_NOTICE) }) : null, hideInternalSubmit ? null : (_jsxs("div", { className: "question-form-foot", children: [locked ? _jsx("span", { className: "qf-locked-note", children: submittedAnswers ? t('You answered this') : t('A newer message answered this') }) : _jsx("span", { className: "qf-hint", children: t('Answer above, then continue') }), !locked ? (_jsx("button", { type: "button", className: "primary", onClick: handleSubmit, disabled: !ready, title: ready ? t('Submit your answers') : t('Answer the required questions first'), children: form.submitLabel ?? t('Continue') })) : null] }))] }));
128
134
  });
129
135
  function OptionCopy({ option }) {
130
136
  return (_jsxs("span", { className: "qf-chip-copy", children: [_jsx("span", { children: option.label }), option.description ? _jsx("span", { className: "qf-chip-desc", children: option.description }) : null] }));