@jini-ai/chat 0.6.15-rc.2 → 0.6.16
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/README.md +6 -1
- package/dist/core/accepted-input.js +20 -11
- package/dist/core/ag-ui/entry.js +3 -1
- package/dist/core/agent-plugin-chat-state.js +3 -1
- package/dist/core/antigravity/tool-presentation.d.ts +13 -0
- package/dist/core/antigravity/tool-presentation.js +20 -0
- package/dist/core/assistant-client/ag-ui-transport.js +14 -2
- package/dist/core/assistant-client/attachment-liveness.js +1 -1
- package/dist/core/assistant-client/durable-run-subscription.js +2 -1
- package/dist/core/assistant-client/transport.js +13 -7
- package/dist/core/assistant-content.js +2 -1
- package/dist/core/composer-history.js +2 -1
- package/dist/core/durable-projection.js +2 -1
- package/dist/core/durable-runs/continuation.js +2 -1
- package/dist/core/events.d.ts +5 -1
- package/dist/core/messages.d.ts +5 -1
- package/dist/core/partial-json.js +18 -9
- package/dist/core/persistence/title.d.ts +1 -1
- package/dist/core/persistence/title.js +25 -7
- package/dist/core/question-form/normalize.js +1 -1
- package/dist/core/question-form/scan.js +25 -15
- package/dist/core/question-form/secret-answers.js +2 -1
- package/dist/core/run-activity.js +8 -4
- package/dist/core/run-events/entry.js +6 -2
- package/dist/core/tool-events.d.ts +18 -8
- package/dist/core/tool-events.js +72 -16
- package/dist/core/tool-presentation.d.ts +14 -0
- package/dist/core/tool-presentation.js +8 -0
- package/dist/core/transcript.js +13 -6
- package/dist/core/turn-usage.js +7 -2
- package/dist/core/user-text-redaction.d.ts +4 -6
- package/dist/core/user-text-redaction.js +13 -6
- package/dist/core/util/manifest.js +13 -11
- package/dist/core/util/markdown-context.d.ts +14 -0
- package/dist/core/util/markdown-context.js +42 -4
- package/dist/core/util/markup-attributes.js +1 -1
- package/dist/core/util/parser.js +7 -7
- package/dist/core/util/pointer.js +22 -6
- package/dist/core/util/recover.js +5 -8
- package/dist/core/util/strip.js +12 -15
- package/dist/core/util/validate.js +30 -18
- package/dist/embed-widget/browser/frontend-session-bridge.d.ts +99 -0
- package/dist/embed-widget/browser/frontend-session-bridge.js +384 -0
- package/dist/embed-widget/browser/lazy-loader.js +16 -12
- package/dist/embed-widget/browser/session-store.d.ts +3 -9
- package/dist/embed-widget/browser/session-store.js +6 -38
- package/dist/embed-widget/react/dialog.hooks.js +2 -2
- package/dist/embed-widget/react/embed.d.ts +3 -10
- package/dist/embed-widget/react/embed.js +8 -52
- package/dist/embed-widget/react/header.hooks.js +1 -1
- package/dist/embed-widget/transports/fetch-sse.js +2 -1
- package/dist/embed-widget/vanilla/index.d.ts +0 -5
- package/dist/embed-widget/vanilla/index.js +23 -26
- package/dist/embed-widget/vanilla/state.d.ts +3 -11
- package/dist/embed-widget/vanilla/state.js +10 -39
- package/dist/embed-widget/vanilla/style.css +1 -1
- package/dist/react/agent-bridge/frontend-session-bridge.d.ts +2 -56
- package/dist/react/agent-bridge/frontend-session-bridge.js +2 -248
- package/dist/react/components/AttachmentPreviewModal.hooks.js +2 -1
- package/dist/react/components/ChatSettingsDialog.js +18 -9
- package/dist/react/components/Composer.js +5 -4
- package/dist/react/components/ConversationList.js +5 -4
- package/dist/react/components/Icon.d.ts +1 -1
- package/dist/react/components/Icon.js +1 -0
- package/dist/react/components/Markdown.d.ts +1 -0
- package/dist/react/components/Markdown.js +30 -27
- package/dist/react/components/McpUiSurfaceCard.js +2 -1
- package/dist/react/components/MessageList.d.ts +1 -1
- package/dist/react/components/MessageList.hooks.js +3 -2
- package/dist/react/components/MessageRow.d.ts +3 -0
- package/dist/react/components/MessageRow.js +32 -14
- package/dist/react/components/MessageRow.usage.js +3 -1
- package/dist/react/components/QuestionForm.js +4 -5
- package/dist/react/components/SentAttachment.js +4 -1
- package/dist/react/components/TodoCard.js +9 -1
- package/dist/react/components/ToolCard.d.ts +3 -1
- package/dist/react/components/ToolCard.js +84 -64
- package/dist/react/components/ToolCard.rules.d.ts +20 -0
- package/dist/react/components/ToolCard.rules.js +92 -0
- package/dist/react/components/composer-discovery.js +8 -8
- package/dist/react/features/chat-pane/components/AgentPluginUsage.js +1 -2
- package/dist/react/features/chat-pane/components/AgentRuntimePicker.d.ts +1 -1
- package/dist/react/features/chat-pane/components/AgentRuntimePicker.js +31 -9
- package/dist/react/features/chat-pane/components/ChatPane.js +7 -5
- package/dist/react/features/chat-pane/components/RuntimeDropUp.js +1 -1
- package/dist/react/features/chat-pane/create-daemon-attachment-uploader.js +1 -1
- package/dist/react/features/chat-pane/hooks/useAgentRuntimePicker.hooks.js +2 -2
- package/dist/react/features/chat-pane/hooks/useChatPane.hooks.d.ts +3 -1
- package/dist/react/features/chat-pane/hooks/useChatPane.hooks.js +13 -5
- package/dist/react/features/chat-pane/hooks/useChatPaneAgentControl.hooks.js +2 -2
- package/dist/react/features/chat-pane/hooks/useChatPaneControlsHeight.hooks.js +1 -1
- package/dist/react/features/chat-pane/hooks/useChatPaneRuntimeInventory.hooks.js +2 -2
- package/dist/react/features/chat-pane/hooks/useChatPaneWorkingDirectory.hooks.js +2 -2
- package/dist/react/features/chat-pane/hooks/useRuntimeDropUp.hooks.js +2 -2
- package/dist/react/features/chat-pane/model-options.js +4 -2
- package/dist/react/features/chat-pane/styles.d.ts +1 -1
- package/dist/react/features/chat-pane/styles.js +95 -5
- package/dist/react/features/chat-pane/types.d.ts +11 -0
- package/dist/react/hooks/assistant-chats.js +22 -8
- package/dist/react/hooks/useArtifactStream.js +2 -1
- package/dist/react/hooks/useComposer.js +2 -2
- package/dist/react/hooks/useComposerHistoryKeys.hooks.js +2 -2
- package/dist/react/hooks/useConversation.js +9 -4
- package/dist/react/hooks/useExtEventGroups.js +4 -2
- package/dist/react/hooks/useLatestOperation.js +2 -1
- package/dist/react/hooks/useQuestionForms.js +32 -22
- package/dist/react/hooks/useRunStream.js +4 -2
- package/dist/react/hooks/useUserTextGuard.d.ts +1 -0
- package/dist/react/hooks/useUserTextGuard.js +3 -1
- package/dist/react/message-blocks.js +4 -2
- package/dist/react/styles/reference.css +9 -0
- package/dist/react/tool-call-display.d.ts +11 -0
- package/dist/react/tool-call-display.js +59 -2
- package/dist/server/durable-runs/recover.js +13 -5
- package/dist/server/run-finalizer/entry.js +1 -0
- package/dist/store/runtime/chat-history-store.js +9 -3
- package/dist/store/runtime/durable-run-store.js +34 -24
- package/dist/store/runtime/run-ledger.js +7 -4
- package/dist/store/sql/boundary.js +3 -1
- package/dist/store/sql/cursor.js +8 -3
- package/dist/store/sql/store.js +36 -28
- package/package.json +93 -88
- package/dist/core/browser/page-actions.d.ts +0 -121
- package/dist/core/browser/page-actions.js +0 -115
- package/dist/embed-widget/browser/page-actions.d.ts +0 -2
- package/dist/embed-widget/browser/page-actions.js +0 -3
|
@@ -85,8 +85,9 @@ function isAnswerCard(scan, name, data) {
|
|
|
85
85
|
function scanStatus({ scan, event, index }, _optional = {}) {
|
|
86
86
|
if (event.code === 'api_retry') {
|
|
87
87
|
scan.retry = { index, data: event.data };
|
|
88
|
+
return;
|
|
88
89
|
}
|
|
89
|
-
|
|
90
|
+
if (event.code === 'tool_progress') {
|
|
90
91
|
const seconds = numberField(event.data, 'elapsedSeconds');
|
|
91
92
|
if (seconds !== undefined)
|
|
92
93
|
scan.progress = { index, seconds };
|
|
@@ -108,15 +109,18 @@ function scanEvent(scan, event, index) {
|
|
|
108
109
|
scan.pendingQuestionIndex = -1;
|
|
109
110
|
scan.open = scan.open.filter((tool) => tool.id !== event.id);
|
|
110
111
|
scan.open.push({ id: event.id, name: event.name, input: event.input, index });
|
|
112
|
+
return;
|
|
111
113
|
}
|
|
112
|
-
|
|
114
|
+
if (event.kind === 'tool_result') {
|
|
113
115
|
// Completion may be unrelated background work or the ask tool returning before human input.
|
|
114
116
|
scan.open = scan.open.filter((tool) => tool.id !== event.toolUseId);
|
|
117
|
+
return;
|
|
115
118
|
}
|
|
116
|
-
|
|
119
|
+
if (event.kind === 'ext') {
|
|
117
120
|
scanCard({ scan, event, index });
|
|
121
|
+
return;
|
|
118
122
|
}
|
|
119
|
-
|
|
123
|
+
if (event.kind === 'status') {
|
|
120
124
|
scanStatus({ scan, event, index });
|
|
121
125
|
}
|
|
122
126
|
}
|
|
@@ -8,7 +8,9 @@ import { assistantContentFromEvents } from "../assistant-content.js";
|
|
|
8
8
|
import { mergeAdjacentTextEvents } from "../compact-events.js";
|
|
9
9
|
import { dedupeToolUsesById } from "../tool-events.js";
|
|
10
10
|
function wireString(v) {
|
|
11
|
-
|
|
11
|
+
if (typeof v === "string")
|
|
12
|
+
return v;
|
|
13
|
+
return v == null ? "" : JSON.stringify(v);
|
|
12
14
|
}
|
|
13
15
|
/** Display a wire scalar or JSON value using the existing empty-null convention. */
|
|
14
16
|
export function asString({ value }, _options = {}) {
|
|
@@ -470,7 +472,9 @@ function chunkEvent(raw, channel, carry) {
|
|
|
470
472
|
if (!frame)
|
|
471
473
|
return [];
|
|
472
474
|
const chunk = wireString(frame.payload?.chunk);
|
|
473
|
-
|
|
475
|
+
if (carry)
|
|
476
|
+
carry.openEchoChannels ??= new Set();
|
|
477
|
+
const open = carry?.openEchoChannels;
|
|
474
478
|
const { kept, statuses, droppedAny } = splitChunk({ chunk, channel }, { open });
|
|
475
479
|
const line = droppedAny ? kept : chunk;
|
|
476
480
|
return line.length > 0 ? [{ kind: "raw", line }, ...statuses] : statuses;
|
|
@@ -29,7 +29,7 @@ export declare function isDelegatedWrapperToolName({ name }: {
|
|
|
29
29
|
name: string;
|
|
30
30
|
}): boolean;
|
|
31
31
|
/**
|
|
32
|
-
* Drop each delegated
|
|
32
|
+
* Drop each delegated or direct MCP wrapper call that the daemon also reported as its own canonical row,
|
|
33
33
|
* so one call shows as one row.
|
|
34
34
|
*
|
|
35
35
|
* WHY. A delegated call arrives twice: the vendor CLI's wrapper `tool_use`
|
|
@@ -39,16 +39,26 @@ export declare function isDelegatedWrapperToolName({ name }: {
|
|
|
39
39
|
* failed call showed two red rows. The canonical row is the one to keep: it carries the tool's own
|
|
40
40
|
* name, input and result.
|
|
41
41
|
*
|
|
42
|
-
* A
|
|
43
|
-
* and
|
|
44
|
-
* such canonical row (refused before the bridge, e.g. a read-only 403) stays, so its error is not
|
|
45
|
-
* hidden.
|
|
42
|
+
* A direct call (`mcp__jini__theme_read_file`) arrives twice the same way: the CLI's `mcp__<server>__X`
|
|
43
|
+
* row and the bridge's `X` row with the same input, so every direct Jini MCP call drew two rows.
|
|
46
44
|
*
|
|
47
|
-
*
|
|
45
|
+
* Antigravity buffers CLI receipts: its adapter-described wrappers match equal inner inputs
|
|
46
|
+
* anywhere in this assistant turn, in appearance order among equal keys. Pass only one turn's events;
|
|
47
|
+
* no state is retained across calls. Wrapper and bridge rows are each claimed at most once.
|
|
48
|
+
* A legacy gateway folds onto the first LATER canonical `tool_use` whose name equals its `input.toolId`.
|
|
49
|
+
* A direct call folds onto the first LATER `X` row with structurally equal JSON input that arrives
|
|
50
|
+
* before the CLI's own result for it. No canonical row is claimed twice, and a folded wrapper's
|
|
51
|
+
* `tool_result` events go with it. A wrapper with no such canonical row (refused before the
|
|
52
|
+
* bridge, e.g. a read-only 403) stays, so its error is not hidden.
|
|
53
|
+
*
|
|
54
|
+
* @param events - One assistant turn's events, normally already passed through {@link dedupeToolUsesById}.
|
|
48
55
|
* @returns A new array without the folded wrappers, or the original reference when none fold.
|
|
49
|
-
* @
|
|
56
|
+
* @throws CanonicalJsonError if a direct call's input contains non-JSON values.
|
|
57
|
+
* @complexity O(n·w·s): n events, w wrappers, s canonical input serialization cost (including key sorting).
|
|
58
|
+
* O(n + s) extra space; inputs and events are not mutated.
|
|
59
|
+
* @example foldDelegatedWrapperCalls({ events }); // canonical rows retain their own results
|
|
50
60
|
*/
|
|
51
61
|
export declare function foldDelegatedWrapperCalls({ events }: {
|
|
52
62
|
events: AgentEvent[] | undefined;
|
|
53
|
-
}): AgentEvent[];
|
|
63
|
+
}, _options?: Record<string, never>): AgentEvent[];
|
|
54
64
|
//# sourceMappingURL=tool-events.d.ts.map
|
package/dist/core/tool-events.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { canonicalJson } from '@jini-ai/core/records';
|
|
2
|
+
import { toolPresentation } from './tool-presentation.js';
|
|
1
3
|
/**
|
|
2
4
|
* Drop repeated `tool_use` events that share an `id`. A retried/duplicated
|
|
3
5
|
* stream can replay the same tool-call id (e.g. reconnect-and-replay landing
|
|
@@ -46,15 +48,31 @@ const DELEGATED_WRAPPER_PATTERN = /^(?:mcp__.+__)?execute_(?:readonly_)?delegate
|
|
|
46
48
|
export function isDelegatedWrapperToolName({ name }) {
|
|
47
49
|
return DELEGATED_WRAPPER_PATTERN.test(name);
|
|
48
50
|
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
+
/** The tool `mcp__<server>__X` names (`X`), or undefined for any other name. O(n) in the name length. */
|
|
52
|
+
function directMcpToolName({ name }) {
|
|
53
|
+
if (!name.startsWith('mcp__'))
|
|
51
54
|
return undefined;
|
|
52
|
-
|
|
53
|
-
const
|
|
54
|
-
return
|
|
55
|
+
// The server segment is at least one character, so its closing `__` starts at index 6 or later.
|
|
56
|
+
const separator = name.indexOf('__', 6);
|
|
57
|
+
return separator === -1 ? undefined : name.slice(separator + 2);
|
|
58
|
+
}
|
|
59
|
+
/** Resolve the generic wrapper contract or an MCP gateway/direct name. Pure; O(n) in the name.
|
|
60
|
+
* Legacy gateways retain name-only matching for malformed-input bridge errors;
|
|
61
|
+
* Antigravity's adapter-described wrappers require equal inner inputs but may arrive buffered.
|
|
62
|
+
*/
|
|
63
|
+
function wrapperCall({ event }, _options = {}) {
|
|
64
|
+
const wrapper = toolPresentation(event).wrapper;
|
|
65
|
+
const call = wrapper ?? event;
|
|
66
|
+
if (!isDelegatedWrapperToolName({ name: call.name })) {
|
|
67
|
+
const name = wrapper ? wrapper.name : directMcpToolName({ name: call.name });
|
|
68
|
+
return name ? { name, input: call.input, matchInput: true, buffered: wrapper !== undefined } : undefined;
|
|
69
|
+
}
|
|
70
|
+
const args = call.input !== null && typeof call.input === 'object' ? call.input : {};
|
|
71
|
+
const name = args.toolId;
|
|
72
|
+
return typeof name === 'string' && name ? { name, input: args.input, matchInput: wrapper !== undefined, buffered: wrapper !== undefined } : undefined;
|
|
55
73
|
}
|
|
56
74
|
/**
|
|
57
|
-
* Drop each delegated
|
|
75
|
+
* Drop each delegated or direct MCP wrapper call that the daemon also reported as its own canonical row,
|
|
58
76
|
* so one call shows as one row.
|
|
59
77
|
*
|
|
60
78
|
* WHY. A delegated call arrives twice: the vendor CLI's wrapper `tool_use`
|
|
@@ -64,16 +82,26 @@ function wrapperToolId({ event }) {
|
|
|
64
82
|
* failed call showed two red rows. The canonical row is the one to keep: it carries the tool's own
|
|
65
83
|
* name, input and result.
|
|
66
84
|
*
|
|
67
|
-
* A
|
|
68
|
-
* and
|
|
69
|
-
* such canonical row (refused before the bridge, e.g. a read-only 403) stays, so its error is not
|
|
70
|
-
* hidden.
|
|
85
|
+
* A direct call (`mcp__jini__theme_read_file`) arrives twice the same way: the CLI's `mcp__<server>__X`
|
|
86
|
+
* row and the bridge's `X` row with the same input, so every direct Jini MCP call drew two rows.
|
|
71
87
|
*
|
|
72
|
-
*
|
|
88
|
+
* Antigravity buffers CLI receipts: its adapter-described wrappers match equal inner inputs
|
|
89
|
+
* anywhere in this assistant turn, in appearance order among equal keys. Pass only one turn's events;
|
|
90
|
+
* no state is retained across calls. Wrapper and bridge rows are each claimed at most once.
|
|
91
|
+
* A legacy gateway folds onto the first LATER canonical `tool_use` whose name equals its `input.toolId`.
|
|
92
|
+
* A direct call folds onto the first LATER `X` row with structurally equal JSON input that arrives
|
|
93
|
+
* before the CLI's own result for it. No canonical row is claimed twice, and a folded wrapper's
|
|
94
|
+
* `tool_result` events go with it. A wrapper with no such canonical row (refused before the
|
|
95
|
+
* bridge, e.g. a read-only 403) stays, so its error is not hidden.
|
|
96
|
+
*
|
|
97
|
+
* @param events - One assistant turn's events, normally already passed through {@link dedupeToolUsesById}.
|
|
73
98
|
* @returns A new array without the folded wrappers, or the original reference when none fold.
|
|
74
|
-
* @
|
|
99
|
+
* @throws CanonicalJsonError if a direct call's input contains non-JSON values.
|
|
100
|
+
* @complexity O(n·w·s): n events, w wrappers, s canonical input serialization cost (including key sorting).
|
|
101
|
+
* O(n + s) extra space; inputs and events are not mutated.
|
|
102
|
+
* @example foldDelegatedWrapperCalls({ events }); // canonical rows retain their own results
|
|
75
103
|
*/
|
|
76
|
-
export function foldDelegatedWrapperCalls({ events }) {
|
|
104
|
+
export function foldDelegatedWrapperCalls({ events }, _options = {}) {
|
|
77
105
|
if (!events || events.length === 0)
|
|
78
106
|
return [];
|
|
79
107
|
const claimed = new Set();
|
|
@@ -81,11 +109,11 @@ export function foldDelegatedWrapperCalls({ events }) {
|
|
|
81
109
|
events.forEach((event, i) => {
|
|
82
110
|
if (event.kind !== 'tool_use')
|
|
83
111
|
return;
|
|
84
|
-
const
|
|
112
|
+
const call = wrapperCall({ event });
|
|
85
113
|
// A replayed wrapper (same id) must not claim a second canonical row.
|
|
86
|
-
if (
|
|
114
|
+
if (call === undefined || folded.has(event.id))
|
|
87
115
|
return;
|
|
88
|
-
const match = events
|
|
116
|
+
const match = findCanonicalRow({ events, wrapper: event, wrapperIndex: i, call, claimed });
|
|
89
117
|
if (match === -1)
|
|
90
118
|
return;
|
|
91
119
|
claimed.add(match);
|
|
@@ -96,4 +124,32 @@ export function foldDelegatedWrapperCalls({ events }) {
|
|
|
96
124
|
return events.filter((event) => !((event.kind === 'tool_use' && folded.has(event.id)) ||
|
|
97
125
|
(event.kind === 'tool_result' && folded.has(event.toolUseId))));
|
|
98
126
|
}
|
|
127
|
+
/**
|
|
128
|
+
* Index of the first unclaimed canonical row in the wrapper's matching window, or -1. Legacy gateways match
|
|
129
|
+
* on name alone; direct calls and adapter-described wrappers also require equal inner input.
|
|
130
|
+
* @throws CanonicalJsonError if a direct call's input contains non-JSON values.
|
|
131
|
+
* @complexity O(n·s): n events, s canonical input serialization cost.
|
|
132
|
+
*/
|
|
133
|
+
function findCanonicalRow({ events, wrapper, wrapperIndex, call, claimed }) {
|
|
134
|
+
const inputKey = call.matchInput ? canonicalJson({ value: { input: call.input } }) : undefined;
|
|
135
|
+
const [start, end] = canonicalSearchWindow({ events, wrapper, wrapperIndex, call });
|
|
136
|
+
for (let j = start; j < end; j += 1) {
|
|
137
|
+
const other = events[j];
|
|
138
|
+
if (claimed.has(j) || other.kind !== 'tool_use' || other.name !== call.name)
|
|
139
|
+
continue;
|
|
140
|
+
if (inputKey === undefined || canonicalJson({ value: { input: other.input } }) === inputKey)
|
|
141
|
+
return j;
|
|
142
|
+
}
|
|
143
|
+
return -1;
|
|
144
|
+
}
|
|
145
|
+
/** Pure matching bounds within one turn; O(n) time, O(1) space for n events.
|
|
146
|
+
* Antigravity's buffered receipts cannot establish arrival/result boundaries. Direct MCP calls
|
|
147
|
+
* retain their later-only window so a completed/refused call cannot consume a separate retry.
|
|
148
|
+
*/
|
|
149
|
+
function canonicalSearchWindow({ events, wrapper, wrapperIndex, call }, _options = {}) {
|
|
150
|
+
if (call.buffered)
|
|
151
|
+
return [0, events.length];
|
|
152
|
+
const resultIndex = !call.matchInput ? -1 : events.findIndex(other => other.kind === 'tool_result' && other.toolUseId === wrapper.id);
|
|
153
|
+
return [wrapperIndex + 1, resultIndex === -1 ? events.length : resultIndex];
|
|
154
|
+
}
|
|
99
155
|
//# sourceMappingURL=tool-events.js.map
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
type Call = {
|
|
2
|
+
name: string;
|
|
3
|
+
input: unknown;
|
|
4
|
+
};
|
|
5
|
+
/** Display-only call and, when present, the transport's inner call to correlate with the bridge. */
|
|
6
|
+
export type ToolPresentation = Call & {
|
|
7
|
+
wrapper?: Call;
|
|
8
|
+
};
|
|
9
|
+
/** Resolve CLI argument dialects at one boundary. Raw/persisted events remain untouched.
|
|
10
|
+
* Pure; O(k) only when an adapter copies k argument keys, otherwise O(1).
|
|
11
|
+
*/
|
|
12
|
+
export declare function toolPresentation(call: Call, _options?: Record<string, never>): ToolPresentation;
|
|
13
|
+
export {};
|
|
14
|
+
//# sourceMappingURL=tool-presentation.d.ts.map
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { antigravityToolPresentation } from './antigravity/tool-presentation.js';
|
|
2
|
+
/** Resolve CLI argument dialects at one boundary. Raw/persisted events remain untouched.
|
|
3
|
+
* Pure; O(k) only when an adapter copies k argument keys, otherwise O(1).
|
|
4
|
+
*/
|
|
5
|
+
export function toolPresentation(call, _options = {}) {
|
|
6
|
+
return antigravityToolPresentation(call) ?? call;
|
|
7
|
+
}
|
|
8
|
+
//# sourceMappingURL=tool-presentation.js.map
|
package/dist/core/transcript.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { fencedCodeRanges } from './util/markdown-context.js';
|
|
1
2
|
/**
|
|
2
3
|
* @module transcript
|
|
3
4
|
*
|
|
@@ -152,12 +153,7 @@ export function sanitizePriorAssistantTurn({ content }, { persistedArtifactFiles
|
|
|
152
153
|
// Strip ```json (or plain ```) fenced blocks whose body matches the form
|
|
153
154
|
// schema shape — `"questions": [` is the strongest tell. A generic JSON
|
|
154
155
|
// snippet without that key is left intact.
|
|
155
|
-
sanitized =
|
|
156
|
-
if (/"questions"\s*:\s*\[/.test(body)) {
|
|
157
|
-
return '[form schema was echoed here on a prior turn; stripped to avoid a loop.]';
|
|
158
|
-
}
|
|
159
|
-
return match;
|
|
160
|
-
});
|
|
156
|
+
sanitized = stripEchoedFormFences({ content: sanitized });
|
|
161
157
|
// Replace prior-turn `<artifact>` HTML with a one-line summary for
|
|
162
158
|
// artifacts CONFIRMED persisted (see summarizeArtifactsForTranscript's own
|
|
163
159
|
// doc for why an unconfirmed save is left verbatim).
|
|
@@ -192,4 +188,15 @@ export function buildTranscript({ history }, options = {}) {
|
|
|
192
188
|
const warning = buildPriorRunContextWarning(scopedHistory, largeToolResultChars, highInputTokenWarningThreshold);
|
|
193
189
|
return warning ? `${warning}\n\n${transcript}` : transcript;
|
|
194
190
|
}
|
|
191
|
+
function stripEchoedFormFences({ content }) {
|
|
192
|
+
const chunks = [];
|
|
193
|
+
let copied = 0;
|
|
194
|
+
for (const range of fencedCodeRanges({ content, opener: /```(?:json)?/g })) {
|
|
195
|
+
if (!/"questions"\s*:\s*\[/.test(range.body))
|
|
196
|
+
continue;
|
|
197
|
+
chunks.push(content.slice(copied, range.start), '[form schema was echoed here on a prior turn; stripped to avoid a loop.]');
|
|
198
|
+
copied = range.end;
|
|
199
|
+
}
|
|
200
|
+
return chunks.join('') + content.slice(copied);
|
|
201
|
+
}
|
|
195
202
|
//# sourceMappingURL=transcript.js.map
|
package/dist/core/turn-usage.js
CHANGED
|
@@ -32,6 +32,9 @@ export function replySegments({ events }, _optional = {}) {
|
|
|
32
32
|
started = false;
|
|
33
33
|
continue;
|
|
34
34
|
}
|
|
35
|
+
// Repeated delivery receipts are bookkeeping, never content of the reply they already split.
|
|
36
|
+
if (isReplyBoundary({ event }))
|
|
37
|
+
continue;
|
|
35
38
|
const lateResult = event.kind === 'usage' && !started && segments.length > 1;
|
|
36
39
|
started ||= producesReplyOutput({ event });
|
|
37
40
|
segments[segments.length - (lateResult ? 2 : 1)].push(event);
|
|
@@ -52,8 +55,10 @@ function usageCost({ event, agentId, sessions }, _optional = {}) {
|
|
|
52
55
|
const key = JSON.stringify([agentId, sessionId]);
|
|
53
56
|
const chargedUsd = sessions.get(key)?.chargedUsd ?? 0;
|
|
54
57
|
const cumulative = validCost(event.cumulativeCostUsd);
|
|
55
|
-
|
|
56
|
-
|
|
58
|
+
let snapshotCost;
|
|
59
|
+
if (cumulative !== undefined)
|
|
60
|
+
snapshotCost = cumulative < chargedUsd ? cumulative : cumulative - chargedUsd;
|
|
61
|
+
const cost = cumulative === undefined ? validCost(event.costUsd) : snapshotCost;
|
|
57
62
|
// Interim interrupted-segment charges are already paid; later snapshots subtract them.
|
|
58
63
|
// After a counter reset the new snapshot, rather than the old epoch's total, is the baseline.
|
|
59
64
|
sessions.set(key, { sessionId, chargedUsd: cumulative ?? chargedUsd + (cost ?? 0) });
|
|
@@ -1,8 +1,5 @@
|
|
|
1
|
-
import type { ChatMessage } from './messages.js';
|
|
2
|
-
export
|
|
3
|
-
readonly secretRedacted: true;
|
|
4
|
-
readonly count: number;
|
|
5
|
-
}
|
|
1
|
+
import type { ChatMessage, SecretRedactionSignal } from './messages.js';
|
|
2
|
+
export type { SecretRedactionSignal } from './messages.js';
|
|
6
3
|
export interface UserTextRedaction {
|
|
7
4
|
readonly text: string;
|
|
8
5
|
readonly secretRedacted: boolean;
|
|
@@ -22,7 +19,8 @@ export interface UserTextRedactionOptions {
|
|
|
22
19
|
export declare 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.";
|
|
23
20
|
export declare const CREDENTIAL_CARD_GUIDANCE = "Use the secure form to save keys, tokens, passwords, and secrets. Chat answers are sent to the assistant.";
|
|
24
21
|
/** Stricter than error redaction: also detects unlabelled long tokens, while retaining ordinary URLs,
|
|
25
|
-
* hexadecimal hashes, identifiers and prose. Strong credential syntax inside
|
|
22
|
+
* filesystem paths, hexadecimal hashes, identifiers and prose. Strong credential syntax inside
|
|
23
|
+
* URLs or paths is still caught. */
|
|
26
24
|
export declare function redactUserText({ text }: {
|
|
27
25
|
readonly text: string;
|
|
28
26
|
}, { secretField }?: {
|
|
@@ -10,9 +10,9 @@ const CHAT_RULES = [
|
|
|
10
10
|
// Kept on top of the shared Slack rule: that rule refuses a token preceded by `-` (e.g. a URL path
|
|
11
11
|
// segment `/-xoxb-…`) and the entropy fallback below exempts URLs, so without this rule such a token
|
|
12
12
|
// would reach storage and model history. Only drop it once the shared rule covers that case.
|
|
13
|
-
{ kind: 'chat_key', pattern: /(
|
|
13
|
+
{ kind: 'chat_key', pattern: /(?<!\w)xox[baprs]-[A-Za-z0-9-]{16,}/g, replacement: () => INTERNAL_REMOVED },
|
|
14
14
|
// Explicit authorization syntax catches even short values. Exempt scheme descriptions in prose.
|
|
15
|
-
{ kind: 'chat_bearer', pattern: /\bBearer[ \t]+(?!\[REDACTED)(?!(?:token|tokens|authentication|authorization|auth|scheme|credentials?)\b)[
|
|
15
|
+
{ kind: 'chat_bearer', pattern: /\bBearer[ \t]+(?!\[REDACTED)(?!(?:token|tokens|authentication|authorization|auth|scheme|credentials?)\b)[\w.~+\/=-]+/gi, replacement: () => 'Bearer ' + INTERNAL_REMOVED },
|
|
16
16
|
];
|
|
17
17
|
function entropy(text) {
|
|
18
18
|
const counts = new Map();
|
|
@@ -26,7 +26,8 @@ function entropy(text) {
|
|
|
26
26
|
return bits;
|
|
27
27
|
}
|
|
28
28
|
/** Stricter than error redaction: also detects unlabelled long tokens, while retaining ordinary URLs,
|
|
29
|
-
* hexadecimal hashes, identifiers and prose. Strong credential syntax inside
|
|
29
|
+
* filesystem paths, hexadecimal hashes, identifiers and prose. Strong credential syntax inside
|
|
30
|
+
* URLs or paths is still caught. */
|
|
30
31
|
export function redactUserText({ text }, { secretField = false } = {}) {
|
|
31
32
|
// Existing removal markers must survive a second pass through assignment rules (api_key=…).
|
|
32
33
|
const literalMarkers = new Map();
|
|
@@ -40,16 +41,22 @@ export function redactUserText({ text }, { secretField = false } = {}) {
|
|
|
40
41
|
}).replaceAll(REMOVED, INTERNAL_REMOVED);
|
|
41
42
|
const base = redactSecretShapes({ text: protectedText, policy: { additionalRules: CHAT_RULES } });
|
|
42
43
|
let count = base.redactions;
|
|
43
|
-
|
|
44
|
+
// Exempt locations only AFTER specific secret rules. Only quoted paths may contain whitespace;
|
|
45
|
+
// unquoted paths end there so an adjacent slash-bearing secret cannot inherit the exemption.
|
|
46
|
+
// A quoted path ends at its first matching quote, which must sit on a word boundary, so an
|
|
47
|
+
// apostrophe later in the line ("don't") cannot close an unterminated quote and exempt the gap.
|
|
48
|
+
// Require a root boundary so a slash inside an opaque token cannot exempt that token, and a
|
|
49
|
+
// folder after the root so a bare "/" + token is not mistaken for a path.
|
|
50
|
+
const locations = [...base.text.matchAll(/\b[a-z][a-z0-9+.-]*:\/\/[^\s<>"']+|(?<!\w)(["'`])(?:~\/|[a-z]:[\\/]|\\\\|\/)[^\r\n"'`\\/]+[\\/](?:(?!\1)[^\r\n])*\1(?!\w)|(?<![\w+\/=\\~.-])(?:~\/|[a-z]:[\\/]|\\\\|\/)[^\s<>"'`\\/]+[\\/][^\s<>"'`]*/gi)]
|
|
44
51
|
.map(match => ({ start: match.index, end: match.index + match[0].length }));
|
|
45
52
|
let cleaned = base.text.replace(/(?<![A-Za-z0-9_+\/=-])[A-Za-z0-9_+\/=-]{40,}(?![A-Za-z0-9_+\/=-])/g, (candidate, offset) => {
|
|
46
|
-
if (
|
|
53
|
+
if (locations.some(location => offset >= location.start && offset < location.end))
|
|
47
54
|
return candidate;
|
|
48
55
|
if (/^[a-f0-9]+$/i.test(candidate) || !/[A-Za-z]/.test(candidate))
|
|
49
56
|
return candidate;
|
|
50
57
|
const bits = entropy(candidate);
|
|
51
58
|
const mixedAlphabet = /[A-Z]/.test(candidate) && /[a-z]/.test(candidate);
|
|
52
|
-
if (bits < 4 || (
|
|
59
|
+
if (bits < 4 || (!/\d/.test(candidate) && (!mixedAlphabet || bits < 4.5)))
|
|
53
60
|
return candidate;
|
|
54
61
|
count += 1;
|
|
55
62
|
return INTERNAL_REMOVED;
|
|
@@ -1,19 +1,21 @@
|
|
|
1
|
+
const CODE_SNIPPET = 'code-snippet';
|
|
2
|
+
const REACT_COMPONENT = 'react-component';
|
|
1
3
|
const MANIFEST_VERSION = 1;
|
|
2
4
|
const ALLOWED_KINDS = new Set([
|
|
3
5
|
'html',
|
|
4
6
|
'deck',
|
|
5
|
-
|
|
7
|
+
REACT_COMPONENT,
|
|
6
8
|
'markdown-document',
|
|
7
9
|
'svg',
|
|
8
10
|
'diagram',
|
|
9
|
-
|
|
11
|
+
CODE_SNIPPET,
|
|
10
12
|
'mini-app',
|
|
11
13
|
'design-system',
|
|
12
14
|
]);
|
|
13
15
|
const ALLOWED_RENDERERS = new Set([
|
|
14
16
|
'html',
|
|
15
17
|
'deck-html',
|
|
16
|
-
|
|
18
|
+
REACT_COMPONENT,
|
|
17
19
|
'markdown',
|
|
18
20
|
'svg',
|
|
19
21
|
'diagram',
|
|
@@ -32,20 +34,20 @@ const LEGACY_KIND_BY_EXT = new Map([
|
|
|
32
34
|
['.htm', 'html'],
|
|
33
35
|
['.svg', 'svg'],
|
|
34
36
|
['.md', 'markdown-document'],
|
|
35
|
-
['.jsx',
|
|
36
|
-
['.tsx',
|
|
37
|
-
['.js',
|
|
38
|
-
['.ts',
|
|
39
|
-
['.json',
|
|
40
|
-
['.css',
|
|
37
|
+
['.jsx', REACT_COMPONENT],
|
|
38
|
+
['.tsx', REACT_COMPONENT],
|
|
39
|
+
['.js', CODE_SNIPPET],
|
|
40
|
+
['.ts', CODE_SNIPPET],
|
|
41
|
+
['.json', CODE_SNIPPET],
|
|
42
|
+
['.css', CODE_SNIPPET],
|
|
41
43
|
]);
|
|
42
44
|
/** Renderer and export set for every kind `inferLegacyManifest` can resolve to (the inferred kinds plus the deck-name heuristic's `deck`). */
|
|
43
45
|
const LEGACY_KIND_INFO = {
|
|
44
46
|
html: { renderer: 'html', exports: ['html', 'pdf', 'zip'] },
|
|
45
47
|
deck: { renderer: 'deck-html', exports: ['html', 'pdf', 'zip'] },
|
|
46
48
|
'markdown-document': { renderer: 'markdown', exports: ['md', 'html', 'pdf', 'zip'] },
|
|
47
|
-
|
|
48
|
-
|
|
49
|
+
[REACT_COMPONENT]: { renderer: REACT_COMPONENT, exports: ['jsx', 'html', 'zip'] },
|
|
50
|
+
[CODE_SNIPPET]: { renderer: 'code', exports: ['txt', 'zip'] },
|
|
49
51
|
svg: { renderer: 'svg', exports: ['svg', 'zip'] }, // 'svg' is a valid ArtifactRendererId as-is
|
|
50
52
|
};
|
|
51
53
|
/** The manifest sidecar file name convention for a given artifact entry file name. */
|
|
@@ -52,4 +52,18 @@ export declare function rangeContains({ ranges, p }: {
|
|
|
52
52
|
ranges: ReadonlyArray<Range>;
|
|
53
53
|
p: number;
|
|
54
54
|
}): boolean;
|
|
55
|
+
export interface FencedCodeRange {
|
|
56
|
+
start: number;
|
|
57
|
+
end: number;
|
|
58
|
+
body: string;
|
|
59
|
+
}
|
|
60
|
+
/** Permissive model-output fences used for recovery/transcript cleanup, independent of the
|
|
61
|
+
* renderer's line-anchored fence grammar above. Opening whitespace is greedy through its last
|
|
62
|
+
* newline; an empty block may require giving that last newline back to the closing delimiter.
|
|
63
|
+
* Each opener and closing delimiter is scanned directly instead of backtracking across the body.
|
|
64
|
+
*/
|
|
65
|
+
export declare function fencedCodeRanges({ content, opener }: {
|
|
66
|
+
content: string;
|
|
67
|
+
opener: RegExp;
|
|
68
|
+
}, _options?: {}): Generator<FencedCodeRange>;
|
|
55
69
|
//# sourceMappingURL=markdown-context.d.ts.map
|
|
@@ -98,19 +98,21 @@ function visitLineOutsideFence(scan, line) {
|
|
|
98
98
|
if (line.hasNewline && FENCE_OPEN_RE.test(line.text)) {
|
|
99
99
|
closeBlockBefore(scan, line.start);
|
|
100
100
|
scan.fenceStart = line.start;
|
|
101
|
+
return;
|
|
101
102
|
}
|
|
102
|
-
|
|
103
|
+
if (line.text.trim() === '') {
|
|
103
104
|
closeBlockBefore(scan, line.start);
|
|
105
|
+
return;
|
|
104
106
|
}
|
|
105
|
-
|
|
107
|
+
if (HEADING_RE.test(line.text) || UL_ITEM_RE.test(line.text) || OL_ITEM_RE.test(line.text)) {
|
|
106
108
|
// Each heading/list-item line is its own inline-scan region rather
|
|
107
109
|
// than joining adjacent paragraphs.
|
|
108
110
|
closeBlockBefore(scan, line.start);
|
|
109
111
|
scan.blockRegions.push([line.start, line.end]);
|
|
112
|
+
return;
|
|
110
113
|
}
|
|
111
|
-
|
|
114
|
+
if (scan.blockStart === -1)
|
|
112
115
|
scan.blockStart = line.start;
|
|
113
|
-
}
|
|
114
116
|
}
|
|
115
117
|
/** `true` when position `p` falls inside one of `ranges` (half-open intervals). */
|
|
116
118
|
export function rangeContains({ ranges, p }) {
|
|
@@ -120,4 +122,40 @@ export function rangeContains({ ranges, p }) {
|
|
|
120
122
|
}
|
|
121
123
|
return false;
|
|
122
124
|
}
|
|
125
|
+
/** Permissive model-output fences used for recovery/transcript cleanup, independent of the
|
|
126
|
+
* renderer's line-anchored fence grammar above. Opening whitespace is greedy through its last
|
|
127
|
+
* newline; an empty block may require giving that last newline back to the closing delimiter.
|
|
128
|
+
* Each opener and closing delimiter is scanned directly instead of backtracking across the body.
|
|
129
|
+
*/
|
|
130
|
+
export function* fencedCodeRanges({ content, opener }, _options = {}) {
|
|
131
|
+
const openings = new RegExp(opener.source, 'g');
|
|
132
|
+
let opening = openings.exec(content);
|
|
133
|
+
while (opening !== null) {
|
|
134
|
+
const body = fencedBodyRange({ content, after: opening.index + opening[0].length });
|
|
135
|
+
if (body === null)
|
|
136
|
+
return; // No later closing fence exists for any subsequent opener either.
|
|
137
|
+
if (body !== undefined) {
|
|
138
|
+
yield { start: opening.index, end: body.end, body: content.slice(body.start, body.end - 4) };
|
|
139
|
+
openings.lastIndex = body.end;
|
|
140
|
+
}
|
|
141
|
+
opening = openings.exec(content);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
/** Undefined is an invalid opener; null is a valid opener with no possible closing fence. */
|
|
145
|
+
function fencedBodyRange({ content, after }) {
|
|
146
|
+
let whitespaceEnd = after;
|
|
147
|
+
while (whitespaceEnd < content.length && /\s/.test(content.charAt(whitespaceEnd)))
|
|
148
|
+
whitespaceEnd += 1;
|
|
149
|
+
const newlineOffset = content.slice(after, whitespaceEnd).indexOf('\n');
|
|
150
|
+
if (newlineOffset === -1)
|
|
151
|
+
return undefined;
|
|
152
|
+
const firstNewline = after + newlineOffset;
|
|
153
|
+
const lastNewline = content.lastIndexOf('\n', whitespaceEnd - 1);
|
|
154
|
+
const close = content.indexOf('\n```', lastNewline + 1);
|
|
155
|
+
if (close !== -1)
|
|
156
|
+
return { start: lastNewline + 1, end: close + 4 };
|
|
157
|
+
if (lastNewline === firstNewline || !content.startsWith('\n```', lastNewline))
|
|
158
|
+
return null;
|
|
159
|
+
return { start: content.lastIndexOf('\n', lastNewline - 1) + 1, end: lastNewline + 4 };
|
|
160
|
+
}
|
|
123
161
|
//# sourceMappingURL=markdown-context.js.map
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
* string (the content between the tag name and the closing `>`).
|
|
18
18
|
*/
|
|
19
19
|
export function parseQuotedAttrs({ raw }) {
|
|
20
|
-
const re = /(\w+)\s*=\s*(?:"([^"]*)"|'([^']*)')/g;
|
|
20
|
+
const re = /(?<!\w)(\w+)\s*=\s*(?:"([^"]*)"|'([^']*)')/g;
|
|
21
21
|
const out = {};
|
|
22
22
|
let m = re.exec(raw);
|
|
23
23
|
while (m !== null) {
|
package/dist/core/util/parser.js
CHANGED
|
@@ -86,13 +86,12 @@ function findTagEnd({ buffer, from }) {
|
|
|
86
86
|
if (quote !== null) {
|
|
87
87
|
if (c === quote)
|
|
88
88
|
quote = null;
|
|
89
|
+
continue;
|
|
89
90
|
}
|
|
90
|
-
|
|
91
|
-
quote = c;
|
|
92
|
-
}
|
|
93
|
-
else if (c === '>') {
|
|
91
|
+
if (c === '>')
|
|
94
92
|
return j;
|
|
95
|
-
|
|
93
|
+
if (c === '"' || c === "'")
|
|
94
|
+
quote = c;
|
|
96
95
|
}
|
|
97
96
|
return -1;
|
|
98
97
|
}
|
|
@@ -198,7 +197,8 @@ export function createArtifactParser(_requiredArgs) {
|
|
|
198
197
|
return true;
|
|
199
198
|
}
|
|
200
199
|
function* flush(_requiredArgs) {
|
|
201
|
-
|
|
200
|
+
const wasInside = state.inside;
|
|
201
|
+
if (wasInside) {
|
|
202
202
|
if (state.buffer.length > 0) {
|
|
203
203
|
state.content += state.buffer;
|
|
204
204
|
yield { type: 'artifact:chunk', identifier: state.identifier, delta: state.buffer };
|
|
@@ -206,7 +206,7 @@ export function createArtifactParser(_requiredArgs) {
|
|
|
206
206
|
}
|
|
207
207
|
yield { type: 'artifact:end', identifier: state.identifier, fullContent: state.content };
|
|
208
208
|
}
|
|
209
|
-
|
|
209
|
+
if (!wasInside && state.buffer.length > 0) {
|
|
210
210
|
yield { type: 'text', delta: state.buffer };
|
|
211
211
|
}
|
|
212
212
|
state.buffer = '';
|
|
@@ -1,7 +1,24 @@
|
|
|
1
|
+
import { trimChars } from '@jini-ai/core/primitives';
|
|
1
2
|
const MAX_POINTER_TEXT_BYTES = 100;
|
|
2
|
-
|
|
3
|
+
// Matching only runs after the 100-byte visible-text guard; bound each run to that same ceiling.
|
|
4
|
+
const POINTER_TARGET_RE = /^(?:见|see)(?:\s{0,100}[::]\s{0,100}|\s{1,100})[`"'“”‘’]?(.{1,100}?\.html?)[`"'“”‘’]?[.。]?$/iu;
|
|
3
5
|
const SCRIPT_OR_STYLE_RE = /<(script|style)\b[^>]*>[\s\S]*?<\/\1>/gi;
|
|
4
|
-
|
|
6
|
+
function stripPointerTags(content) {
|
|
7
|
+
const chunks = [];
|
|
8
|
+
let copied = 0;
|
|
9
|
+
let open = content.indexOf('<');
|
|
10
|
+
while (open !== -1) {
|
|
11
|
+
const close = content.indexOf('>', open + 1);
|
|
12
|
+
if (close === -1)
|
|
13
|
+
break;
|
|
14
|
+
if (close > open + 1) {
|
|
15
|
+
chunks.push(content.slice(copied, open), ' ');
|
|
16
|
+
copied = close + 1;
|
|
17
|
+
}
|
|
18
|
+
open = content.indexOf('<', close + 1);
|
|
19
|
+
}
|
|
20
|
+
return chunks.join('') + content.slice(copied);
|
|
21
|
+
}
|
|
5
22
|
/**
|
|
6
23
|
* Resolve a short "see foo.html"-style reply to the referenced file name, if
|
|
7
24
|
* `content` is (after stripping tags) nothing but such a pointer and the
|
|
@@ -40,12 +57,11 @@ function resolveAgainstProjectFiles({ target, candidateFileName, projectFiles })
|
|
|
40
57
|
function visiblePointerText(content) {
|
|
41
58
|
const stripped = content
|
|
42
59
|
.replace(/^/, '')
|
|
43
|
-
.replace(SCRIPT_OR_STYLE_RE, ' ')
|
|
44
|
-
|
|
45
|
-
return decodeBasicHtmlEntities(stripped).replace(/\s+/g, ' ').trim();
|
|
60
|
+
.replace(SCRIPT_OR_STYLE_RE, ' ');
|
|
61
|
+
return decodeBasicHtmlEntities(stripPointerTags(stripped)).replace(/\s+/g, ' ').trim();
|
|
46
62
|
}
|
|
47
63
|
function normalizeTarget(value) {
|
|
48
|
-
return value.trim()
|
|
64
|
+
return trimChars({ text: value.trim(), chars: "`\"'“”‘’" });
|
|
49
65
|
}
|
|
50
66
|
function isSafeHtmlTarget(value) {
|
|
51
67
|
if (!/\.(?:html?|HTML?)$/.test(value))
|