@jini-ai/chat 0.6.5 → 0.6.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/dist/core/accepted-input.d.ts +54 -0
- package/dist/core/accepted-input.js +165 -0
- package/dist/core/agent-plugin-chat-state.d.ts +43 -0
- package/dist/core/agent-plugin-chat-state.js +48 -0
- package/dist/core/assistant-client/a2ui-action-poster.js +32 -28
- package/dist/core/assistant-client/messages.d.ts +1 -1
- package/dist/core/assistant-client/messages.js +2 -2
- package/dist/core/assistant-client/transport.d.ts +2 -2
- package/dist/core/assistant-client/transport.js +184 -97
- package/dist/core/assistant-content.d.ts +2 -1
- package/dist/core/assistant-content.js +9 -11
- package/dist/core/browser/page-actions.js +4 -2
- package/dist/core/composer-history.d.ts +16 -9
- package/dist/core/composer-history.js +28 -16
- package/dist/core/conversation-title.d.ts +3 -0
- package/dist/core/conversation-title.js +3 -0
- package/dist/core/durable-projection.js +20 -13
- package/dist/core/durable-runs/continuation.d.ts +4 -0
- package/dist/core/durable-runs/continuation.js +35 -6
- package/dist/core/durable-runs/ledger-contracts.d.ts +2 -1
- package/dist/core/durable-runs/policies.d.ts +1 -0
- package/dist/core/durable-runs/policies.js +1 -0
- package/dist/core/durable-runs/ports.d.ts +6 -2
- package/dist/core/index.d.ts +2 -0
- package/dist/core/index.js +2 -0
- package/dist/core/messages.d.ts +2 -0
- package/dist/core/partial-json.d.ts +6 -7
- package/dist/core/partial-json.js +81 -47
- package/dist/core/persistence/title.js +4 -2
- package/dist/core/question-answer.d.ts +23 -0
- package/dist/core/question-answer.js +66 -0
- package/dist/core/question-form/format.d.ts +6 -6
- package/dist/core/question-form/format.js +16 -17
- package/dist/core/question-form/normalize.d.ts +18 -0
- package/dist/core/question-form/normalize.js +112 -122
- package/dist/core/question-form/scan.js +141 -203
- package/dist/core/run-activity.js +62 -49
- package/dist/core/run-events/entry.d.ts +10 -1
- package/dist/core/run-events/entry.js +117 -87
- package/dist/core/todos.d.ts +1 -1
- package/dist/core/todos.js +60 -85
- package/dist/core/tool-events.js +14 -12
- package/dist/core/transcript.js +62 -16
- package/dist/core/turn-usage.d.ts +2 -1
- package/dist/core/turn-usage.js +56 -27
- package/dist/core/user-text-redaction.js +3 -0
- package/dist/core/util/manifest.js +77 -82
- package/dist/core/util/markdown-context.js +51 -53
- package/dist/core/util/parser.js +136 -144
- package/dist/core/util/pointer.js +16 -16
- package/dist/core/util/strip.js +57 -67
- package/dist/core/util/validate.js +15 -17
- package/dist/embed-widget/browser/session-store.js +21 -12
- package/dist/embed-widget/transports/fetch-sse.js +108 -95
- package/dist/embed-widget/vanilla/index.js +81 -70
- package/dist/react/agent-bridge/frontend-session-bridge.js +80 -52
- package/dist/react/components/AttachmentPreviewModal.js +25 -1
- package/dist/react/components/ChatSettingsDialog.d.ts +13 -0
- package/dist/react/components/ChatSettingsDialog.hooks.d.ts +115 -0
- package/dist/react/components/ChatSettingsDialog.hooks.js +40 -0
- package/dist/react/components/ChatSettingsDialog.js +72 -0
- package/dist/react/components/ChatSettingsSegmented.d.ts +18 -0
- package/dist/react/components/ChatSettingsSegmented.js +11 -0
- package/dist/react/components/Composer.d.ts +5 -1
- package/dist/react/components/Composer.js +126 -82
- package/dist/react/components/ConversationList.js +25 -39
- package/dist/react/components/ConversationTitleEditor.d.ts +27 -0
- package/dist/react/components/ConversationTitleEditor.js +54 -0
- package/dist/react/components/Icon.d.ts +1 -1
- package/dist/react/components/Icon.js +25 -45
- package/dist/react/components/Markdown.js +134 -123
- package/dist/react/components/McpUiSurfaceCard.d.ts +2 -2
- package/dist/react/components/McpUiSurfaceCard.js +30 -9
- package/dist/react/components/MessageList.d.ts +13 -13
- package/dist/react/components/MessageList.hooks.d.ts +6 -1
- package/dist/react/components/MessageList.hooks.js +19 -3
- package/dist/react/components/MessageList.js +34 -10
- package/dist/react/components/MessageRow.js +48 -32
- package/dist/react/components/MessageRow.usage.d.ts +5 -2
- package/dist/react/components/MessageRow.usage.js +20 -11
- package/dist/react/components/MidRunUserMessage.js +16 -3
- package/dist/react/components/QuestionForm.js +148 -76
- package/dist/react/components/QuestionsPanel.js +1 -1
- package/dist/react/components/TodoCard.js +12 -6
- package/dist/react/components/ToolCard.js +47 -40
- package/dist/react/components/composer-discovery.js +6 -1
- package/dist/react/components/mcp-ui-surface-state.d.ts +6 -11
- package/dist/react/components/mcp-ui-surface-state.js +23 -8
- package/dist/react/ext-event-renderer-registry.d.ts +8 -0
- package/dist/react/ext-event-renderer-registry.js +5 -2
- package/dist/react/features/chat-pane/components/AgentPluginUsage.d.ts +7 -0
- package/dist/react/features/chat-pane/components/AgentPluginUsage.js +45 -0
- package/dist/react/features/chat-pane/components/AgentRuntimePicker.js +55 -19
- package/dist/react/features/chat-pane/components/ChatPane.d.ts +1 -1
- package/dist/react/features/chat-pane/components/ChatPane.js +192 -74
- package/dist/react/features/chat-pane/components/RuntimeDropUp.d.ts +29 -0
- package/dist/react/features/chat-pane/components/RuntimeDropUp.js +45 -0
- package/dist/react/features/chat-pane/create-daemon-attachment-uploader.js +80 -47
- package/dist/react/features/chat-pane/create-mcp-ui-tool-caller.js +44 -40
- package/dist/react/features/chat-pane/create-mid-run-message-poster.d.ts +6 -6
- package/dist/react/features/chat-pane/create-mid-run-message-poster.js +8 -8
- package/dist/react/features/chat-pane/create-typed-answer-poster.d.ts +3 -3
- package/dist/react/features/chat-pane/create-typed-answer-poster.js +3 -3
- package/dist/react/features/chat-pane/hooks/useAgentRuntimePicker.hooks.d.ts +6 -1
- package/dist/react/features/chat-pane/hooks/useAgentRuntimePicker.hooks.js +11 -5
- package/dist/react/features/chat-pane/hooks/useChatPane.hooks.d.ts +12 -7
- package/dist/react/features/chat-pane/hooks/useChatPane.hooks.js +340 -149
- package/dist/react/features/chat-pane/hooks/useChatPaneControlsHeight.hooks.js +7 -6
- package/dist/react/features/chat-pane/hooks/useChatPaneNewTabLinks.hooks.js +28 -10
- package/dist/react/features/chat-pane/hooks/useChatPaneRuntimeInventory.hooks.d.ts +3 -3
- package/dist/react/features/chat-pane/hooks/useChatPaneRuntimeInventory.hooks.js +60 -12
- package/dist/react/features/chat-pane/hooks/useRuntimeDropUp.hooks.d.ts +49 -0
- package/dist/react/features/chat-pane/hooks/useRuntimeDropUp.hooks.js +143 -0
- package/dist/react/features/chat-pane/model-options.d.ts +18 -1
- package/dist/react/features/chat-pane/model-options.js +49 -20
- package/dist/react/features/chat-pane/rules.d.ts +8 -9
- package/dist/react/features/chat-pane/rules.js +84 -46
- package/dist/react/features/chat-pane/styles.d.ts +1 -1
- package/dist/react/features/chat-pane/styles.js +156 -37
- package/dist/react/features/chat-pane/types.d.ts +17 -8
- package/dist/react/hooks/assistant-chats.js +12 -5
- package/dist/react/hooks/composer-draft-cache.js +116 -118
- package/dist/react/hooks/composer-history-storage.js +5 -5
- package/dist/react/hooks/fab-position.js +7 -4
- package/dist/react/hooks/useAnsweredCards.d.ts +21 -4
- package/dist/react/hooks/useAnsweredCards.js +38 -11
- package/dist/react/hooks/useArtifactStream.js +28 -22
- package/dist/react/hooks/useComposer.d.ts +20 -0
- package/dist/react/hooks/useComposer.js +22 -9
- package/dist/react/hooks/useComposerHistory.hooks.d.ts +6 -1
- package/dist/react/hooks/useComposerHistory.hooks.js +39 -19
- package/dist/react/hooks/useComposerHistoryKeys.hooks.js +24 -14
- package/dist/react/hooks/useConversation.d.ts +7 -0
- package/dist/react/hooks/useConversation.js +23 -10
- package/dist/react/hooks/useExtEventGroups.js +39 -39
- package/dist/react/hooks/useQuestionForms.js +21 -26
- package/dist/react/hooks/useRunStream.js +10 -2
- package/dist/react/index.d.ts +7 -1
- package/dist/react/index.js +9 -0
- package/dist/react/message-blocks.d.ts +6 -4
- package/dist/react/message-blocks.js +94 -72
- package/dist/react/slots.d.ts +2 -2
- package/dist/react/tool-call-display.js +19 -17
- package/dist/server/durable-runs/recover.d.ts +1 -1
- package/dist/server/durable-runs/recover.js +139 -76
- package/dist/server/run-finalizer/entry.js +25 -15
- package/dist/store/legacy/sqlite/conversations.d.ts +11 -11
- package/dist/store/legacy/sqlite/conversations.js +69 -98
- package/dist/store/legacy/sqlite/messages.d.ts +10 -2
- package/dist/store/legacy/sqlite/messages.js +113 -71
- package/dist/store/runtime/chat-history-store.js +9 -3
- package/dist/store/runtime/durable-run-store.d.ts +2 -2
- package/dist/store/runtime/durable-run-store.js +165 -117
- package/dist/store/runtime/history-tools.js +118 -82
- package/dist/store/runtime/run-ledger.d.ts +4 -1
- package/dist/store/runtime/run-ledger.js +56 -16
- package/dist/store/sql/cursor.js +38 -18
- package/dist/store/sql/store.d.ts +18 -0
- package/dist/store/sql/store.js +46 -15
- package/package.json +12 -10
|
@@ -21,6 +21,20 @@ function identity(event, usageOccurrences) {
|
|
|
21
21
|
function textOf(events) {
|
|
22
22
|
return events.filter((event) => event.kind === 'text').map((event) => event.text).join('');
|
|
23
23
|
}
|
|
24
|
+
/** Records `key` and reports whether it was new, so each identifiable event is merged once. */
|
|
25
|
+
function claimNewKey({ key, keys }) {
|
|
26
|
+
if (keys.has(key))
|
|
27
|
+
return false;
|
|
28
|
+
keys.add(key);
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
const NO_ROLLOUT = /no rollout found for thread id/;
|
|
32
|
+
/** The missing native resume diagnostic, in either the status or the raw-line form a runtime emits. */
|
|
33
|
+
function isNoRolloutDiagnostic({ event }) {
|
|
34
|
+
if (event.kind === 'status')
|
|
35
|
+
return NO_ROLLOUT.test(`${event.label}\n${event.detail ?? ''}`);
|
|
36
|
+
return event.kind === 'raw' && NO_ROLLOUT.test(event.line);
|
|
37
|
+
}
|
|
24
38
|
/** Saved checkpoints can coalesce deltas and a completion may replay only a prefix. Keep the
|
|
25
39
|
* saved projection, merge identifiable events, and append only text beyond its saved prefix. */
|
|
26
40
|
export function mergeRunEvents({ saved, incoming }, _optional = {}) {
|
|
@@ -39,12 +53,8 @@ export function mergeRunEvents({ saved, incoming }, _optional = {}) {
|
|
|
39
53
|
if (text)
|
|
40
54
|
result.push({ ...event, text });
|
|
41
55
|
}
|
|
42
|
-
else {
|
|
43
|
-
|
|
44
|
-
if (!keys.has(key)) {
|
|
45
|
-
keys.add(key);
|
|
46
|
-
result.push(event);
|
|
47
|
-
}
|
|
56
|
+
else if (claimNewKey({ key: identity(event, incomingUsage), keys })) {
|
|
57
|
+
result.push(event);
|
|
48
58
|
}
|
|
49
59
|
}
|
|
50
60
|
return result;
|
|
@@ -84,16 +94,13 @@ export function recoveredRunEvents({ saved }, { notice = 'visible' } = {}) {
|
|
|
84
94
|
// A missing rollout is an expected reconstruction fallback, not a failed user turn. Keep an
|
|
85
95
|
// invisible attempt marker for fencing/diagnostic offsets, and preserve all unrelated work.
|
|
86
96
|
const attemptStart = saved.findLastIndex(event => event.kind === 'status' && event.code === 'run_recovering') + 1;
|
|
87
|
-
const diagnostic = saved.findIndex((event, index) => index >= attemptStart && (
|
|
88
|
-
(event.kind === 'raw' && /no rollout found for thread id/.test(event.line))));
|
|
97
|
+
const diagnostic = saved.findIndex((event, index) => index >= attemptStart && isNoRolloutDiagnostic({ event }));
|
|
89
98
|
const events = saved.filter((event, index) => {
|
|
90
99
|
if (diagnostic < 0 || index < attemptStart)
|
|
91
100
|
return true;
|
|
92
|
-
if (event.kind === '
|
|
93
|
-
return
|
|
94
|
-
|
|
95
|
-
return event.label !== 'Run failed — the agent process exited without answering' && !/no rollout found for thread id/.test(`${event.label}\n${event.detail ?? ''}`);
|
|
96
|
-
return true;
|
|
101
|
+
if (event.kind === 'status' && event.label === 'Run failed — the agent process exited without answering')
|
|
102
|
+
return false;
|
|
103
|
+
return !isNoRolloutDiagnostic({ event });
|
|
97
104
|
});
|
|
98
105
|
return [...events, { kind: 'status', code: 'run_recovering', label: '' }];
|
|
99
106
|
}
|
|
@@ -11,6 +11,10 @@ export declare function hasUnknownToolCall({ events, toolId, input }: {
|
|
|
11
11
|
toolId: string;
|
|
12
12
|
input: unknown;
|
|
13
13
|
}, _optional?: {}): boolean;
|
|
14
|
+
/** Reconstruct one attempt from the accepted task and its saved work, including plugin consent.
|
|
15
|
+
* Returns a new request without changing the accepted input or transcript; invalid context JSON throws.
|
|
16
|
+
* @complexity O(e + t) time/space in saved events and transcript text; no external effects.
|
|
17
|
+
*/
|
|
14
18
|
export declare function continuationRequest({ run, native }: {
|
|
15
19
|
run: DurableRun;
|
|
16
20
|
native: boolean;
|
|
@@ -22,6 +22,9 @@ export function hasUnknownToolCall({ events, toolId, input }, _optional = {}) {
|
|
|
22
22
|
return events.some((event) => {
|
|
23
23
|
if (event.kind !== "tool_use" || completed.has(event.id))
|
|
24
24
|
return false;
|
|
25
|
+
// The read-only gateway either reads or refuses before execution; neither can leave a mutation.
|
|
26
|
+
if (/execute_readonly_delegated_tool$/.test(event.name))
|
|
27
|
+
return false;
|
|
25
28
|
const call = delegatedCall(event);
|
|
26
29
|
return call.toolId === toolId && canonicalToolInput({ value: call.input }, {}) === canonical;
|
|
27
30
|
});
|
|
@@ -33,23 +36,49 @@ function toolHistory(events) {
|
|
|
33
36
|
return JSON.stringify({ call: delegatedCall(event), result: result ?? "OUTCOME UNKNOWN: verify state before repeating" });
|
|
34
37
|
}).join("\n");
|
|
35
38
|
}
|
|
36
|
-
|
|
37
|
-
|
|
39
|
+
/** The accepted browser state predates this turn's Ask answer. A replacement attempt must
|
|
40
|
+
* carry the latest saved decision, including Skip, instead of asking from that frozen state.
|
|
41
|
+
* Scan backwards so recovery markers and unrelated extensions cannot hide the last answer.
|
|
42
|
+
*/
|
|
43
|
+
function latestAgentPluginState({ events, conversationId, accepted }, _optional = {}) {
|
|
44
|
+
for (let index = events.length - 1; index >= 0; index--) {
|
|
45
|
+
const event = events[index];
|
|
46
|
+
if (event.kind !== "ext" || event.name !== "agent_plugins")
|
|
47
|
+
continue;
|
|
48
|
+
const usage = event.data;
|
|
49
|
+
if (usage?.conversationId === conversationId && usage.state)
|
|
50
|
+
return usage.state;
|
|
51
|
+
}
|
|
52
|
+
return accepted;
|
|
53
|
+
}
|
|
54
|
+
function continuationPrompt({ run, native, acceptedPrompt, events }, _optional = {}) {
|
|
38
55
|
// Native sessions may have disappeared on deploy. Reconstruction includes the saved answer
|
|
39
56
|
// and tool results for every runtime; completed side effects must never be silently repeated.
|
|
40
57
|
const transcript = run.transcript.map((message) => `${message.role}: ${message.content}`).join("\n\n");
|
|
41
|
-
|
|
58
|
+
return [
|
|
42
59
|
"Continue the interrupted answer from the saved work below. Preserve completed work. Do not repeat completed mutations.",
|
|
60
|
+
"Omission markers in the saved work are system housekeeping, not user content; do not mention them to the user or infer a user disclosure from them.",
|
|
43
61
|
"Calls without a result have an unknown outcome: inspect current state before any retry. Expired plans or approvals must be re-planned and re-approved; never reuse them.",
|
|
44
|
-
"
|
|
45
|
-
`Accepted task:\n${String(
|
|
62
|
+
"Never re-ask a pending question in this turn. The interrupted segment remains visible; append the continuation.",
|
|
63
|
+
`Accepted task:\n${String(acceptedPrompt ?? "")}`,
|
|
46
64
|
native ? "Native session is confirmed; continue its saved task." : `Saved conversation:\n${transcript}`,
|
|
47
65
|
`Saved partial answer:\n${run.message.content}`,
|
|
48
|
-
`Saved tool calls and results:\n${toolHistory(
|
|
66
|
+
`Saved tool calls and results:\n${toolHistory(events)}`,
|
|
49
67
|
].join("\n\n");
|
|
68
|
+
}
|
|
69
|
+
/** Reconstruct one attempt from the accepted task and its saved work, including plugin consent.
|
|
70
|
+
* Returns a new request without changing the accepted input or transcript; invalid context JSON throws.
|
|
71
|
+
* @complexity O(e + t) time/space in saved events and transcript text; no external effects.
|
|
72
|
+
*/
|
|
73
|
+
export function continuationRequest({ run, native }, _optional = {}) {
|
|
74
|
+
const original = JSON.parse(run.request.contextRef);
|
|
75
|
+
const events = run.message.events ?? [];
|
|
76
|
+
const agentPluginState = latestAgentPluginState({ events, conversationId: run.conversationId, accepted: original.agentPluginState }, {});
|
|
77
|
+
const prompt = continuationPrompt({ run, native, acceptedPrompt: original.prompt, events }, {});
|
|
50
78
|
return {
|
|
51
79
|
...run.request,
|
|
52
80
|
contextRef: JSON.stringify({ ...original, prompt, conversationId: run.conversationId,
|
|
81
|
+
...(agentPluginState === undefined ? {} : { agentPluginState }),
|
|
53
82
|
assistantMessageId: run.messageId, principalId: run.principalId,
|
|
54
83
|
recoveryMode: native ? "native" : "reconstruction", recoverySessionId: native ? run.sessionId : undefined,
|
|
55
84
|
// Old attachment claims have been consumed. Saved transcript/tool results carry their
|
|
@@ -40,7 +40,8 @@ export interface ChatRunLedger {
|
|
|
40
40
|
/**
|
|
41
41
|
* Saves what a still-running run has produced so far (content and events; the status stays), so
|
|
42
42
|
* a process that dies mid-run leaves its partial answer for {@link reconcileInterrupted} to keep.
|
|
43
|
-
* A no-op once the row is terminal or belongs to another run. Resolves `true` when it wrote
|
|
43
|
+
* A no-op once the row is terminal or belongs to another run. Resolves `true` when it wrote, or
|
|
44
|
+
* when the row already held exactly this checkpoint (accepted without rewriting the row).
|
|
44
45
|
*/
|
|
45
46
|
checkpoint(progress: RunProgress, optional?: {}): Promise<boolean>;
|
|
46
47
|
/** Serving-only discovery of unfinished rows. The injected recovery coordinator owns adopted
|
|
@@ -2,5 +2,6 @@ export declare const RECOVERY_LIMIT = 3;
|
|
|
2
2
|
export declare const RECOVERY_WINDOW_MS: number;
|
|
3
3
|
export declare const RUN_STALENESS_MS: number;
|
|
4
4
|
export declare const SAVED_WORK_NOTICE = "The assistant stopped after making no progress. Saved work is above.";
|
|
5
|
+
export declare const AWAITING_ANSWER_NOTICE = "The assistant stopped while waiting for your answer. Saved work is above.";
|
|
5
6
|
export declare const EXHAUSTED_NOTICE = "The assistant could not continue this answer. Saved work is above.";
|
|
6
7
|
//# sourceMappingURL=policies.d.ts.map
|
|
@@ -2,5 +2,6 @@ export const RECOVERY_LIMIT = 3;
|
|
|
2
2
|
export const RECOVERY_WINDOW_MS = 5 * 60_000;
|
|
3
3
|
export const RUN_STALENESS_MS = 30 * 60_000;
|
|
4
4
|
export const SAVED_WORK_NOTICE = "The assistant stopped after making no progress. Saved work is above.";
|
|
5
|
+
export const AWAITING_ANSWER_NOTICE = "The assistant stopped while waiting for your answer. Saved work is above.";
|
|
5
6
|
export const EXHAUSTED_NOTICE = "The assistant could not continue this answer. Saved work is above.";
|
|
6
7
|
//# sourceMappingURL=policies.js.map
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { AgentEvent, ChatMessage } from "../index.js";
|
|
2
|
-
import type { RunRef, RunSettlement } from "./
|
|
2
|
+
import type { RunRef, RunSettlement } from "./run-contracts.js";
|
|
3
3
|
export interface AcceptedRunRequest {
|
|
4
4
|
readonly agentId?: string;
|
|
5
5
|
readonly contextRef: string;
|
|
@@ -24,14 +24,18 @@ export interface DurableRun extends RunRef {
|
|
|
24
24
|
} | null;
|
|
25
25
|
readonly attemptBase: readonly AgentEvent[];
|
|
26
26
|
}
|
|
27
|
+
/** A run without its conversation transcript: what a fence, lock or tool guard reads. `message`
|
|
28
|
+
* still carries the run's own saved events; only `load`/`accept`/`cancelPending` read every message. */
|
|
29
|
+
export type DurableRunHead = Omit<DurableRun, "transcript">;
|
|
27
30
|
export interface DurableRunStore {
|
|
28
31
|
load(required: {
|
|
29
32
|
messageId: string;
|
|
30
33
|
}, optional: {}): Promise<DurableRun | null>;
|
|
34
|
+
/** Reads only this run's message and attempt rows; `load` when the transcript is needed. */
|
|
31
35
|
find(required: {
|
|
32
36
|
runId: string;
|
|
33
37
|
principalId?: string;
|
|
34
|
-
}, optional: {}): Promise<
|
|
38
|
+
}, optional: {}): Promise<DurableRunHead | null>;
|
|
35
39
|
accept(required: RunRef & {
|
|
36
40
|
principalId: string;
|
|
37
41
|
workspaceId: string;
|
package/dist/core/index.d.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
export * from './events.js';
|
|
7
7
|
export * from './messages.js';
|
|
8
|
+
export * from './conversation-title.js';
|
|
8
9
|
export * from './partial-json.js';
|
|
9
10
|
export * from './tool-events.js';
|
|
10
11
|
export * from './tool-output.js';
|
|
@@ -36,4 +37,5 @@ export * from './user-text-redaction.js';
|
|
|
36
37
|
export * from './history-tool-metadata.js';
|
|
37
38
|
export { turnUsage, conversationUsage, replySegments, isReplyBoundary, MID_RUN_USER_MESSAGE_EVENT_NAME, type ChatUsageSummary } from './turn-usage.js';
|
|
38
39
|
export { formatUsageCost } from './usage-format.js';
|
|
40
|
+
export * from './accepted-input.js';
|
|
39
41
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/core/index.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
export * from './events.js';
|
|
7
7
|
export * from './messages.js';
|
|
8
|
+
export * from './conversation-title.js';
|
|
8
9
|
export * from './partial-json.js';
|
|
9
10
|
export * from './tool-events.js';
|
|
10
11
|
export * from './tool-output.js';
|
|
@@ -35,4 +36,5 @@ export * from './user-text-redaction.js';
|
|
|
35
36
|
export * from './history-tool-metadata.js';
|
|
36
37
|
export { turnUsage, conversationUsage, replySegments, isReplyBoundary, MID_RUN_USER_MESSAGE_EVENT_NAME } from './turn-usage.js';
|
|
37
38
|
export { formatUsageCost } from './usage-format.js';
|
|
39
|
+
export * from './accepted-input.js';
|
|
38
40
|
//# sourceMappingURL=index.js.map
|
package/dist/core/messages.d.ts
CHANGED
|
@@ -33,6 +33,8 @@ export interface ChatAttachment {
|
|
|
33
33
|
* host layers those on top via its own message extension, not this type.
|
|
34
34
|
*/
|
|
35
35
|
export interface ChatMessage {
|
|
36
|
+
/** A normal send stays outside recall until the transport accepts it. */
|
|
37
|
+
inputPending?: boolean;
|
|
36
38
|
/** Transient safe receipt; storage requires no new column. */
|
|
37
39
|
secretRedaction?: import('./user-text-redaction.js').SecretRedactionSignal;
|
|
38
40
|
id: string;
|
|
@@ -16,25 +16,24 @@
|
|
|
16
16
|
* open-container stack, then closing whatever is still open.
|
|
17
17
|
*
|
|
18
18
|
* @param buf - A possibly-incomplete JSON text (a streamed prefix).
|
|
19
|
-
* @returns
|
|
20
|
-
* parse (e.g. an empty buffer round-trips unchanged) — callers should still
|
|
19
|
+
* @returns Empty text above 64 Ki characters or for non-JSON tails that never settle; otherwise
|
|
20
|
+
* best-effort closed JSON. Not guaranteed to parse (e.g. an empty buffer round-trips unchanged) — callers should still
|
|
21
21
|
* wrap consumption in `JSON.parse`/try-catch (see {@link parsePartialJson}).
|
|
22
22
|
* @complexity O(n) time / O(d) space in the input length `n` and max nesting
|
|
23
|
-
* depth `d` — one linear scan plus
|
|
24
|
-
* the (already shrinking) tail.
|
|
23
|
+
* depth `d` — one linear scan plus at most 8 linear cleanup passes over the tail.
|
|
25
24
|
*/
|
|
26
25
|
export declare function repairJsonPrefix({ buf }: {
|
|
27
26
|
buf: string;
|
|
28
|
-
}): string;
|
|
27
|
+
}, _optional?: {}): string;
|
|
29
28
|
/**
|
|
30
29
|
* Best-effort parse of a (possibly truncated) JSON prefix.
|
|
31
30
|
*
|
|
32
31
|
* @param buf - A possibly-incomplete JSON text.
|
|
33
|
-
* @returns The parsed value, or `null` on empty input or an unrecoverable
|
|
32
|
+
* @returns The parsed value, or `null` on input above 64 Ki characters, empty input or an unrecoverable
|
|
34
33
|
* parse failure — callers should fall back to their last good value.
|
|
35
34
|
* @complexity O(n) — dominated by {@link repairJsonPrefix}.
|
|
36
35
|
*/
|
|
37
36
|
export declare function parsePartialJson({ buf }: {
|
|
38
37
|
buf: string;
|
|
39
|
-
}): unknown;
|
|
38
|
+
}, _optional?: {}): unknown;
|
|
40
39
|
//# sourceMappingURL=partial-json.d.ts.map
|
|
@@ -10,32 +10,32 @@
|
|
|
10
10
|
* structure that simply hasn't finished — not arbitrary corruption. Callers
|
|
11
11
|
* should still keep their last good parsed result as a fallback.
|
|
12
12
|
*/
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
*/
|
|
26
|
-
|
|
13
|
+
// Bound repair and JSON.parse work for untrusted model-generated structured output.
|
|
14
|
+
const MAX_STRUCTURED_INPUT_CHARS = 64 * 1024;
|
|
15
|
+
// A valid streamed prefix settles in two cleanup passes (cut value, then its key/comma). Input
|
|
16
|
+
// needing more is not JSON (`[t,t,t,…`), and each extra pass would rescan the whole buffer.
|
|
17
|
+
const MAX_TAIL_CLEANUP_PASSES = 8;
|
|
18
|
+
/** Linear form of `/[,\s]+$/`, which V8 retries from every separator in a long run (quadratic). */
|
|
19
|
+
function trimTrailingSeparators({ text }, _optional = {}) {
|
|
20
|
+
let end = text.length;
|
|
21
|
+
while (end > 0 && /[,\s]/.test(text[end - 1]))
|
|
22
|
+
end -= 1;
|
|
23
|
+
return text.slice(0, end);
|
|
24
|
+
}
|
|
25
|
+
/** Consume one quoted character; an escaped quote does not end the string. */
|
|
26
|
+
function advanceJsonString({ c, esc }, _optional = {}) {
|
|
27
|
+
if (esc)
|
|
28
|
+
return { inStr: true, esc: false };
|
|
29
|
+
return { inStr: c !== '"', esc: c === '\\' };
|
|
30
|
+
}
|
|
31
|
+
/** Track strings/escapes and owed container closers in one pass over a bounded JSON prefix. */
|
|
32
|
+
function scanJsonPrefix({ buf }, _optional = {}) {
|
|
27
33
|
const stack = []; // closers owed, e.g. ['}', ']']
|
|
28
34
|
let inStr = false;
|
|
29
35
|
let esc = false;
|
|
30
|
-
for (
|
|
31
|
-
const c = buf[i];
|
|
36
|
+
for (const c of buf) {
|
|
32
37
|
if (inStr) {
|
|
33
|
-
|
|
34
|
-
esc = false;
|
|
35
|
-
else if (c === '\\')
|
|
36
|
-
esc = true;
|
|
37
|
-
else if (c === '"')
|
|
38
|
-
inStr = false;
|
|
38
|
+
({ inStr, esc } = advanceJsonString({ c, esc }));
|
|
39
39
|
continue;
|
|
40
40
|
}
|
|
41
41
|
if (c === '"')
|
|
@@ -47,19 +47,23 @@ export function repairJsonPrefix({ buf }) {
|
|
|
47
47
|
else if (c === '}' || c === ']')
|
|
48
48
|
stack.pop();
|
|
49
49
|
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
50
|
+
return { stack, inStr };
|
|
51
|
+
}
|
|
52
|
+
/** Close a string cut off mid-value (or mid-key). First neutralize a
|
|
53
|
+
* dangling escape at the cut point, otherwise the closing quote would be
|
|
54
|
+
* swallowed (`...x\` + `"` → escaped quote) or a partial `\uXXXX` would
|
|
55
|
+
* be an invalid escape — either makes JSON.parse fail and collapses the
|
|
56
|
+
* live preview. Common in prompts that mention Windows paths, regexes,
|
|
57
|
+
* or escaped quotes.
|
|
58
|
+
*/
|
|
59
|
+
function closeJsonString({ out }, _optional = {}) {
|
|
60
|
+
out = out.replace(/\\u[0-9a-fA-F]{0,3}$/, ''); // incomplete \uXXXX
|
|
61
|
+
if (/(?:^|[^\\])(?:\\\\)*\\$/.test(out))
|
|
62
|
+
out = out.slice(0, -1); // lone trailing backslash
|
|
63
|
+
return out + '"';
|
|
64
|
+
}
|
|
65
|
+
/** Returns null when the tail never settles within the pass bound. */
|
|
66
|
+
function trimIncompleteTail({ out }, _optional = {}) {
|
|
63
67
|
// 2. Trim trailing structural noise that can't be completed into a value:
|
|
64
68
|
// a dangling comma, a `"key":` with no value yet, and an unfinished
|
|
65
69
|
// *scalar* value cut mid-token (a partial `true`/`false`/`null`, or a
|
|
@@ -70,42 +74,72 @@ export function repairJsonPrefix({ buf }) {
|
|
|
70
74
|
// `1.5`, `1e3`) are not prefixes of these patterns, so they survive.
|
|
71
75
|
const valueStart = '([:[,]\\s*)';
|
|
72
76
|
let prev;
|
|
77
|
+
let passes = 0;
|
|
73
78
|
do {
|
|
79
|
+
if (++passes > MAX_TAIL_CLEANUP_PASSES)
|
|
80
|
+
return null;
|
|
74
81
|
prev = out;
|
|
75
|
-
out =
|
|
82
|
+
out = trimTrailingSeparators({ text: out });
|
|
76
83
|
out = out.replace(/"(?:[^"\\]|\\.)*"\s*:\s*$/, ''); // key + colon, no value
|
|
77
84
|
out = out.replace(new RegExp(`${valueStart}(?:tru|tr|t|fals|fal|fa|f|nul|nu|n)$`), '$1'); // partial bool/null
|
|
78
|
-
out = out.replace(new RegExp(`${valueStart}-?(?:\\d
|
|
85
|
+
out = out.replace(new RegExp(`${valueStart}-?(?:\\d+(?:\\.\\d*)?|\\.\\d+)?[eE][+-]?$`), '$1'); // dangling exponent
|
|
79
86
|
out = out.replace(new RegExp(`${valueStart}-?\\d*\\.$`), '$1'); // number ending in '.'
|
|
80
87
|
out = out.replace(new RegExp(`${valueStart}-$`), '$1'); // lone minus
|
|
81
88
|
out = out.replace(/"(?:[^"\\]|\\.)*"\s*:\s*$/, ''); // key + colon exposed by the trims above
|
|
82
|
-
out =
|
|
89
|
+
out = trimTrailingSeparators({ text: out });
|
|
83
90
|
} while (out !== prev);
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
91
|
+
return out;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Repair a truncated JSON prefix into the largest valid JSON text that can be
|
|
95
|
+
* parsed, by walking the buffer once to track string/escape state and the
|
|
96
|
+
* open-container stack, then closing whatever is still open.
|
|
97
|
+
*
|
|
98
|
+
* @param buf - A possibly-incomplete JSON text (a streamed prefix).
|
|
99
|
+
* @returns Empty text above 64 Ki characters or for non-JSON tails that never settle; otherwise
|
|
100
|
+
* best-effort closed JSON. Not guaranteed to parse (e.g. an empty buffer round-trips unchanged) — callers should still
|
|
101
|
+
* wrap consumption in `JSON.parse`/try-catch (see {@link parsePartialJson}).
|
|
102
|
+
* @complexity O(n) time / O(d) space in the input length `n` and max nesting
|
|
103
|
+
* depth `d` — one linear scan plus at most 8 linear cleanup passes over the tail.
|
|
104
|
+
*/
|
|
105
|
+
export function repairJsonPrefix({ buf }, _optional = {}) {
|
|
106
|
+
if (buf.length > MAX_STRUCTURED_INPUT_CHARS)
|
|
107
|
+
return '';
|
|
108
|
+
const { stack, inStr } = scanJsonPrefix({ buf });
|
|
109
|
+
const settled = trimIncompleteTail({ out: inStr ? closeJsonString({ out: buf }) : buf });
|
|
110
|
+
if (settled === null)
|
|
111
|
+
return '';
|
|
112
|
+
let out = dropPendingKey({ out: settled, stack });
|
|
113
|
+
// 3. Close every still-open container, innermost first.
|
|
114
|
+
for (let i = stack.length - 1; i >= 0; i--)
|
|
115
|
+
out += stack[i];
|
|
116
|
+
return out;
|
|
117
|
+
}
|
|
118
|
+
// A bare trailing key (string with no following colon) only happens when
|
|
119
|
+
// the *innermost* open container is an object. Drop it so we don't emit
|
|
120
|
+
// `{"hea"}` which is invalid (key without value).
|
|
121
|
+
function dropPendingKey({ out, stack }, _optional = {}) {
|
|
87
122
|
if (stack[stack.length - 1] === '}' && /"(?:[^"\\]|\\.)*"\s*$/.test(out)) {
|
|
88
123
|
const trimmed = out.replace(/"(?:[^"\\]|\\.)*"\s*$/, '');
|
|
89
124
|
// Only drop it if what precedes is a container/comma boundary (i.e. the
|
|
90
125
|
// string really is a pending key, not a completed value like `:"x"`).
|
|
91
126
|
if (/[{,]\s*$/.test(trimmed)) {
|
|
92
|
-
out =
|
|
127
|
+
out = trimTrailingSeparators({ text: trimmed });
|
|
93
128
|
}
|
|
94
129
|
}
|
|
95
|
-
// 3. Close every still-open container, innermost first.
|
|
96
|
-
for (let i = stack.length - 1; i >= 0; i--)
|
|
97
|
-
out += stack[i];
|
|
98
130
|
return out;
|
|
99
131
|
}
|
|
100
132
|
/**
|
|
101
133
|
* Best-effort parse of a (possibly truncated) JSON prefix.
|
|
102
134
|
*
|
|
103
135
|
* @param buf - A possibly-incomplete JSON text.
|
|
104
|
-
* @returns The parsed value, or `null` on empty input or an unrecoverable
|
|
136
|
+
* @returns The parsed value, or `null` on input above 64 Ki characters, empty input or an unrecoverable
|
|
105
137
|
* parse failure — callers should fall back to their last good value.
|
|
106
138
|
* @complexity O(n) — dominated by {@link repairJsonPrefix}.
|
|
107
139
|
*/
|
|
108
|
-
export function parsePartialJson({ buf }) {
|
|
140
|
+
export function parsePartialJson({ buf }, _optional = {}) {
|
|
141
|
+
if (buf.length > MAX_STRUCTURED_INPUT_CHARS)
|
|
142
|
+
return null;
|
|
109
143
|
const trimmed = buf.trim();
|
|
110
144
|
if (!trimmed)
|
|
111
145
|
return null;
|
|
@@ -126,8 +126,9 @@ function truncateTitle(title) {
|
|
|
126
126
|
const lastSpace = prefix.lastIndexOf(' ');
|
|
127
127
|
return (lastSpace > 0 ? prefix.slice(0, lastSpace) : prefix).trimEnd();
|
|
128
128
|
}
|
|
129
|
+
/** Callers pass only non-empty words (`trimLatinTitle` filters blanks before mapping). */
|
|
129
130
|
function toTitleCase(word) {
|
|
130
|
-
return word
|
|
131
|
+
return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase();
|
|
131
132
|
}
|
|
132
133
|
function trimLatinTitle(input, { casing }) {
|
|
133
134
|
const words = input
|
|
@@ -164,7 +165,8 @@ export function deriveConversationTitle({ prompt }, { casing = 'title' } = {}) {
|
|
|
164
165
|
const cleaned = cleanPrompt(prompt);
|
|
165
166
|
if (!cleaned)
|
|
166
167
|
return '';
|
|
167
|
-
|
|
168
|
+
// `split` always yields a first element, even for a string that starts with a separator.
|
|
169
|
+
const firstClause = cleaned.split(/[\n\r。!?!?]/)[0].trim();
|
|
168
170
|
if (!firstClause)
|
|
169
171
|
return '';
|
|
170
172
|
const title = CJK_PATTERN.test(firstClause) ? trimCjkTitle(firstClause) : trimLatinTitle(firstClause, { casing });
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/** Read a classified result or delivered callback as plain user text. Decline wins over other fields;
|
|
2
|
+
* expiry, invalid answers and unrecognized results produce no user message.
|
|
3
|
+
* @returns Answer text, or undefined when no supported human answer is present; never throws.
|
|
4
|
+
* @example questionAnswerText({ answer: { submitted: true, freeText: 'Custom theme' } })
|
|
5
|
+
* @complexity O(p + s*m) time, O(s) space for p single options, s selections, m multi options; no I/O.
|
|
6
|
+
*/
|
|
7
|
+
export declare function questionAnswerText({ answer, input }: {
|
|
8
|
+
answer: unknown;
|
|
9
|
+
input?: unknown;
|
|
10
|
+
}, _options?: {}): string | undefined;
|
|
11
|
+
/** Decode the persisted JSON result; failed tools and non-answer payloads never become user text.
|
|
12
|
+
* @returns Plain answer text or undefined for missing, failed or malformed results; never throws.
|
|
13
|
+
* @example questionAnswerFromResult({ result: { content: '{"submitted":true,"choice":"Blue"}', isError: false } })
|
|
14
|
+
* @complexity O(r + p + s*m) time, O(r + s) space including JSON decoding of r result characters.
|
|
15
|
+
*/
|
|
16
|
+
export declare function questionAnswerFromResult({ result, input }: {
|
|
17
|
+
result: {
|
|
18
|
+
content: string;
|
|
19
|
+
isError: boolean;
|
|
20
|
+
} | undefined;
|
|
21
|
+
input?: unknown;
|
|
22
|
+
}, _options?: {}): string | undefined;
|
|
23
|
+
//# sourceMappingURL=question-answer.d.ts.map
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/** @module question-answer — plain transcript text from the ask-choice result contract. */
|
|
2
|
+
function record(value) {
|
|
3
|
+
return typeof value === 'object' && value !== null ? value : {};
|
|
4
|
+
}
|
|
5
|
+
/** Resolve a stored option value through the question that offered it. Unknown values stay readable. */
|
|
6
|
+
function optionLabel(group, value) {
|
|
7
|
+
const options = record(group).options;
|
|
8
|
+
if (!Array.isArray(options))
|
|
9
|
+
return value;
|
|
10
|
+
const option = options.find(candidate => record(candidate).value === value);
|
|
11
|
+
const label = record(option).label;
|
|
12
|
+
return typeof label === 'string' ? label : value;
|
|
13
|
+
}
|
|
14
|
+
function selectedAnswerText({ result, input }, _options = {}) {
|
|
15
|
+
const question = record(input);
|
|
16
|
+
const labels = [];
|
|
17
|
+
if (typeof result.choice === 'string')
|
|
18
|
+
labels.push(optionLabel(question.singleSelect, result.choice));
|
|
19
|
+
if (Array.isArray(result.selections)) {
|
|
20
|
+
for (const selection of result.selections) {
|
|
21
|
+
if (typeof selection === 'string')
|
|
22
|
+
labels.push(optionLabel(question.multiSelect, selection));
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
if (labels.length > 0)
|
|
26
|
+
return labels.join(', ');
|
|
27
|
+
return Array.isArray(result.selections) && result.selections.length === 0 ? 'None selected' : undefined;
|
|
28
|
+
}
|
|
29
|
+
/** Read a classified result or delivered callback as plain user text. Decline wins over other fields;
|
|
30
|
+
* expiry, invalid answers and unrecognized results produce no user message.
|
|
31
|
+
* @returns Answer text, or undefined when no supported human answer is present; never throws.
|
|
32
|
+
* @example questionAnswerText({ answer: { submitted: true, freeText: 'Custom theme' } })
|
|
33
|
+
* @complexity O(p + s*m) time, O(s) space for p single options, s selections, m multi options; no I/O.
|
|
34
|
+
*/
|
|
35
|
+
export function questionAnswerText({ answer, input }, _options = {}) {
|
|
36
|
+
const result = record(answer);
|
|
37
|
+
if ((result.submitted === false && result.reason === 'cancelled') || result.__dismissed === true)
|
|
38
|
+
return 'Declined';
|
|
39
|
+
if (result.submitted === false)
|
|
40
|
+
return undefined;
|
|
41
|
+
const freeText = result.freeText ?? result.__typedAnswer;
|
|
42
|
+
if (typeof freeText === 'string' && freeText.trim())
|
|
43
|
+
return freeText.trim();
|
|
44
|
+
return selectedAnswerText({ result, input });
|
|
45
|
+
}
|
|
46
|
+
/** Decode the persisted JSON result; failed tools and non-answer payloads never become user text.
|
|
47
|
+
* @returns Plain answer text or undefined for missing, failed or malformed results; never throws.
|
|
48
|
+
* @example questionAnswerFromResult({ result: { content: '{"submitted":true,"choice":"Blue"}', isError: false } })
|
|
49
|
+
* @complexity O(r + p + s*m) time, O(r + s) space including JSON decoding of r result characters.
|
|
50
|
+
*/
|
|
51
|
+
export function questionAnswerFromResult({ result, input }, _options = {}) {
|
|
52
|
+
if (!result || result.isError)
|
|
53
|
+
return undefined;
|
|
54
|
+
let answer;
|
|
55
|
+
try {
|
|
56
|
+
answer = JSON.parse(result.content);
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
return undefined;
|
|
60
|
+
}
|
|
61
|
+
const fields = record(answer);
|
|
62
|
+
if (fields.submitted !== true && !(fields.submitted === false && fields.reason === 'cancelled'))
|
|
63
|
+
return undefined;
|
|
64
|
+
return questionAnswerText({ answer, input });
|
|
65
|
+
}
|
|
66
|
+
//# sourceMappingURL=question-answer.js.map
|
|
@@ -10,18 +10,18 @@ export declare function formatFormAnswers({ form, answers }: {
|
|
|
10
10
|
form: QuestionForm;
|
|
11
11
|
answers: Record<string, string | string[]>;
|
|
12
12
|
}): string;
|
|
13
|
+
type OptionHolder = {
|
|
14
|
+
options?: FormOption[] | undefined;
|
|
15
|
+
};
|
|
13
16
|
/** Resolve a submitted answer value back to its display label, or the value itself when unmatched. */
|
|
14
17
|
export declare function formOptionLabelForValue({ question, value }: {
|
|
15
|
-
question:
|
|
16
|
-
options?: FormOption[] | undefined;
|
|
17
|
-
};
|
|
18
|
+
question: OptionHolder;
|
|
18
19
|
value: string;
|
|
19
20
|
}): string;
|
|
20
21
|
/** Resolve a label or value to the canonical option `value`, or the input itself when unmatched. */
|
|
21
22
|
export declare function formOptionValueForLabel({ question, labelOrValue }: {
|
|
22
|
-
question:
|
|
23
|
-
options?: FormOption[] | undefined;
|
|
24
|
-
};
|
|
23
|
+
question: OptionHolder;
|
|
25
24
|
labelOrValue: string;
|
|
26
25
|
}): string;
|
|
26
|
+
export {};
|
|
27
27
|
//# sourceMappingURL=format.d.ts.map
|
|
@@ -21,25 +21,26 @@ export function formatFormAnswers({ form, answers }) {
|
|
|
21
21
|
const lines = [];
|
|
22
22
|
lines.push(`[form answers — ${form.id}]`);
|
|
23
23
|
for (const q of form.questions) {
|
|
24
|
-
|
|
25
|
-
let display;
|
|
26
|
-
if (Array.isArray(v)) {
|
|
27
|
-
display = v.length > 0 ? v.map((value) => formOptionDisplayForValue(q, value)).join(', ') : '(skipped)';
|
|
28
|
-
}
|
|
29
|
-
else if (typeof v === 'string') {
|
|
30
|
-
display = v.trim().length > 0 ? formOptionDisplayForValue(q, v.trim()) : '(skipped)';
|
|
31
|
-
}
|
|
32
|
-
else {
|
|
33
|
-
display = '(skipped)';
|
|
34
|
-
}
|
|
35
|
-
lines.push(`- ${q.label}: ${display}`);
|
|
24
|
+
lines.push(`- ${q.label}: ${answerDisplay(q, safe.answers[q.id])}`);
|
|
36
25
|
}
|
|
37
26
|
if (safe.secretRedacted || form.questions.some(question => isSecretQuestion({ question })))
|
|
38
27
|
lines.push(CREDENTIAL_CARD_GUIDANCE);
|
|
39
28
|
return redactUserText({ text: lines.join('\n') }).text;
|
|
40
29
|
}
|
|
30
|
+
// Array entries are shown as given; a string answer is trimmed first. An
|
|
31
|
+
// empty answer of either shape reads as skipped.
|
|
32
|
+
function answerDisplay(question, answer) {
|
|
33
|
+
if (Array.isArray(answer)) {
|
|
34
|
+
return answer.length > 0 ? answer.map((value) => formOptionDisplayForValue(question, value)).join(', ') : '(skipped)';
|
|
35
|
+
}
|
|
36
|
+
const value = typeof answer === 'string' ? answer.trim() : '';
|
|
37
|
+
return value ? formOptionDisplayForValue(question, value) : '(skipped)';
|
|
38
|
+
}
|
|
39
|
+
function findOption(question, labelOrValue) {
|
|
40
|
+
return question.options?.find((option) => option.value === labelOrValue || option.label === labelOrValue);
|
|
41
|
+
}
|
|
41
42
|
function formOptionDisplayForValue(question, value) {
|
|
42
|
-
const match = question
|
|
43
|
+
const match = findOption(question, value);
|
|
43
44
|
if (!match)
|
|
44
45
|
return value;
|
|
45
46
|
if (match.value === match.label)
|
|
@@ -48,12 +49,10 @@ function formOptionDisplayForValue(question, value) {
|
|
|
48
49
|
}
|
|
49
50
|
/** Resolve a submitted answer value back to its display label, or the value itself when unmatched. */
|
|
50
51
|
export function formOptionLabelForValue({ question, value }) {
|
|
51
|
-
|
|
52
|
-
return match?.label ?? value;
|
|
52
|
+
return findOption(question, value)?.label ?? value;
|
|
53
53
|
}
|
|
54
54
|
/** Resolve a label or value to the canonical option `value`, or the input itself when unmatched. */
|
|
55
55
|
export function formOptionValueForLabel({ question, labelOrValue }) {
|
|
56
|
-
|
|
57
|
-
return match?.value ?? labelOrValue;
|
|
56
|
+
return findOption(question, labelOrValue)?.value ?? labelOrValue;
|
|
58
57
|
}
|
|
59
58
|
//# sourceMappingURL=format.js.map
|