@jini-ai/chat 0.6.15-rc.2 → 0.6.15
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/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/assistant-client/ag-ui-transport.js +1 -1
- 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/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/transcript.js +13 -6
- package/dist/core/turn-usage.js +7 -2
- package/dist/core/user-text-redaction.d.ts +2 -5
- package/dist/core/user-text-redaction.js +3 -3
- 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/lazy-loader.js +16 -12
- package/dist/embed-widget/react/dialog.hooks.js +2 -2
- package/dist/embed-widget/react/embed.js +1 -1
- 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.js +19 -12
- package/dist/embed-widget/vanilla/state.js +4 -2
- 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/Markdown.js +29 -26
- package/dist/react/components/McpUiSurfaceCard.js +2 -1
- package/dist/react/components/MessageList.hooks.js +3 -2
- package/dist/react/components/MessageRow.js +23 -7
- 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.js +8 -7
- 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 +15 -5
- package/dist/react/features/chat-pane/components/ChatPane.js +4 -2
- 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.js +4 -2
- 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 +26 -3
- package/dist/react/features/chat-pane/types.d.ts +8 -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/message-blocks.js +4 -2
- package/dist/react/styles/reference.css +9 -0
- 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 +15 -15
|
@@ -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();
|
|
@@ -49,7 +49,7 @@ export function redactUserText({ text }, { secretField = false } = {}) {
|
|
|
49
49
|
return candidate;
|
|
50
50
|
const bits = entropy(candidate);
|
|
51
51
|
const mixedAlphabet = /[A-Z]/.test(candidate) && /[a-z]/.test(candidate);
|
|
52
|
-
if (bits < 4 || (
|
|
52
|
+
if (bits < 4 || (!/\d/.test(candidate) && (!mixedAlphabet || bits < 4.5)))
|
|
53
53
|
return candidate;
|
|
54
54
|
count += 1;
|
|
55
55
|
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))
|
|
@@ -6,10 +6,11 @@
|
|
|
6
6
|
* a standalone document reply, or as a single fenced ```html block.
|
|
7
7
|
*/
|
|
8
8
|
import { validateHtmlArtifact } from './validate.js';
|
|
9
|
+
import { fencedCodeRanges } from './markdown-context.js';
|
|
9
10
|
const HTML_OPEN_RE = /<html\b/gi;
|
|
10
11
|
const HTML_CLOSE_RE = /<\/html\s*>/gi;
|
|
11
12
|
const ADJACENT_DOCTYPE_RE = /<!doctype\s+html\b[^>]*>\s*$/i;
|
|
12
|
-
const HTML_FENCE_RE = /```(?:html|HTML)
|
|
13
|
+
const HTML_FENCE_RE = /```(?:html|HTML)/g;
|
|
13
14
|
function findLastArtifactOpen(sourceText, identifier) {
|
|
14
15
|
if (!identifier)
|
|
15
16
|
return sourceText.lastIndexOf('<artifact');
|
|
@@ -99,15 +100,11 @@ export function recoverStandaloneHtmlDocument({ sourceText }) {
|
|
|
99
100
|
*/
|
|
100
101
|
export function recoverHtmlDocumentFromMarkdownFence({ sourceText }) {
|
|
101
102
|
const text = String(sourceText || '');
|
|
102
|
-
HTML_FENCE_RE.lastIndex = 0;
|
|
103
103
|
let recovered = null;
|
|
104
104
|
let count = 0;
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
if (!/<\/html\s*>$/i.test(candidate))
|
|
109
|
-
continue;
|
|
110
|
-
if (!validateHtmlArtifact({ content: candidate }).ok)
|
|
105
|
+
for (const range of fencedCodeRanges({ content: text, opener: HTML_FENCE_RE })) {
|
|
106
|
+
const candidate = range.body.replace(/^/, '').trim();
|
|
107
|
+
if (!/<\/html\s*>$/i.test(candidate) || !validateHtmlArtifact({ content: candidate }).ok)
|
|
111
108
|
continue;
|
|
112
109
|
recovered = candidate;
|
|
113
110
|
count += 1;
|
package/dist/core/util/strip.js
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
|
+
import { trimChars } from '@jini-ai/core/primitives';
|
|
1
2
|
/**
|
|
2
3
|
* @module strip
|
|
3
4
|
*
|
|
4
5
|
* Post-stream `<artifact>` removal/summarization and the streaming-display
|
|
5
6
|
* counterpart that surfaces an in-flight (not-yet-closed) artifact block.
|
|
6
7
|
*/
|
|
7
|
-
import { computeSkipRanges, isRealArtifactOpenAt, rangeContains } from './markdown-context.js';
|
|
8
|
+
import { fencedCodeRanges, computeSkipRanges, isRealArtifactOpenAt, rangeContains } from './markdown-context.js';
|
|
8
9
|
import { parseQuotedAttrs } from './markup-attributes.js';
|
|
9
10
|
import { recoverHtmlArtifactFromPrecedingDocument, recoverHtmlDocumentFromMarkdownFence, recoverStandaloneHtmlDocument, } from './recover.js';
|
|
10
11
|
const OPEN = '<artifact';
|
|
11
12
|
const CLOSE = '</artifact>';
|
|
12
|
-
const HTML_FENCE_RE = /```(?:html|HTML)
|
|
13
|
+
const HTML_FENCE_RE = /```(?:html|HTML)/g;
|
|
13
14
|
function findUnskipped(content, needle, fromIndex, ranges) {
|
|
14
15
|
let from = fromIndex;
|
|
15
16
|
// `needle` (always `CLOSE`, non-empty) makes `content.indexOf` itself
|
|
@@ -91,17 +92,14 @@ export function stripArtifact({ content }) {
|
|
|
91
92
|
return (content.slice(0, block.open) + content.slice(block.close + CLOSE.length)).trim();
|
|
92
93
|
}
|
|
93
94
|
function findSingleRecoverableHtmlFence(content) {
|
|
94
|
-
HTML_FENCE_RE.lastIndex = 0;
|
|
95
95
|
let recovered = null;
|
|
96
96
|
let count = 0;
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
recovered = { start: match.index, end: match.index + match[0].length, html };
|
|
97
|
+
for (const range of fencedCodeRanges({ content, opener: HTML_FENCE_RE })) {
|
|
98
|
+
const html = range.body.replace(/^/, '').trim();
|
|
99
|
+
if (recoverHtmlDocumentFromMarkdownFence({ sourceText: content.slice(range.start, range.end) }) === html) {
|
|
100
|
+
recovered = { start: range.start, end: range.end, html };
|
|
102
101
|
count += 1;
|
|
103
102
|
}
|
|
104
|
-
match = HTML_FENCE_RE.exec(content);
|
|
105
103
|
}
|
|
106
104
|
return count === 1 ? recovered : null;
|
|
107
105
|
}
|
|
@@ -177,10 +175,9 @@ function artifactExtensionForAttrs(attrs) {
|
|
|
177
175
|
}
|
|
178
176
|
// The slug a persist path would derive the file name from.
|
|
179
177
|
function artifactBaseNameForAttrs(attrs) {
|
|
180
|
-
return ((attrs['identifier'] || attrs['title'] || 'artifact')
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
.replace(/^-+|-+$/g, '')
|
|
178
|
+
return (trimChars({ text: (attrs['identifier'] || attrs['title'] || 'artifact')
|
|
179
|
+
.toLowerCase()
|
|
180
|
+
.replace(/[^a-z0-9_-]+/g, '-'), chars: '-' })
|
|
184
181
|
.slice(0, 60) || 'artifact');
|
|
185
182
|
}
|
|
186
183
|
/**
|
|
@@ -296,7 +293,7 @@ export function splitStreamingArtifact({ content }) {
|
|
|
296
293
|
// known yet, but an artifact is starting, so show the box (empty body)
|
|
297
294
|
// and hide the partial `<artifact …` tail from Markdown.
|
|
298
295
|
return {
|
|
299
|
-
head: content.slice(0, open).
|
|
296
|
+
head: content.slice(0, open).trimEnd(),
|
|
300
297
|
live: { artifactType: '', title: '', identifier: '', content: '' },
|
|
301
298
|
};
|
|
302
299
|
}
|
|
@@ -311,7 +308,7 @@ export function splitStreamingArtifact({ content }) {
|
|
|
311
308
|
if (artifactType && !/html|text\//i.test(artifactType))
|
|
312
309
|
return { head: content, live: null };
|
|
313
310
|
return {
|
|
314
|
-
head: content.slice(0, open).
|
|
311
|
+
head: content.slice(0, open).trimEnd(),
|
|
315
312
|
live: {
|
|
316
313
|
artifactType,
|
|
317
314
|
title: attrs['title'] ?? '',
|
|
@@ -43,8 +43,9 @@ const URL_ATTRIBUTE_RE = /\b(href|src|srcset|poster|action|formaction|data|xlink
|
|
|
43
43
|
const STYLE_ATTRIBUTE_RE = /\bstyle\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'`=<>]+))/gi;
|
|
44
44
|
const HTML_TAG_RE = /<[a-z][^>]*>/gi;
|
|
45
45
|
const STYLE_BLOCK_RE = /<style\b[^>]*>([\s\S]*?)<\/style>/gi;
|
|
46
|
-
const CSS_URL_RE = /\burl\(
|
|
47
|
-
const
|
|
46
|
+
const CSS_URL_RE = /\burl\((?:\s*"([^"]*)"\s*|\s*'([^']*)'\s*|([^)]*))\)/gi;
|
|
47
|
+
const CSS_IMPORT_START_RE = /@import\s+/gi;
|
|
48
|
+
const CSS_QUOTED_IMPORT_RE = /(?:"([^"]*)"|'([^']*)')/y;
|
|
48
49
|
/**
|
|
49
50
|
* Sniff-test whether `content` looks like a complete HTML document rather
|
|
50
51
|
* than prose that happens to mention HTML.
|
|
@@ -127,22 +128,34 @@ function hasReservedWorkspacePathInStyleBlocks(content) {
|
|
|
127
128
|
return false;
|
|
128
129
|
}
|
|
129
130
|
function cssTextReferencesReservedWorkspacePath(cssText) {
|
|
130
|
-
for (const
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
// on a successful match — `.find` always finds it; the assertion just
|
|
137
|
-
// satisfies `noUncheckedIndexedAccess`.
|
|
138
|
-
const candidate = match.slice(1).find((value) => value !== undefined);
|
|
139
|
-
if (candidateReferencesReservedWorkspacePath(candidate, false)) {
|
|
140
|
-
return true;
|
|
141
|
-
}
|
|
142
|
-
}
|
|
131
|
+
for (const match of cssReferenceMatches({ cssText })) {
|
|
132
|
+
// Quote/unquoted alternatives are mutually exclusive, so exactly one capture is defined
|
|
133
|
+
// after success; TypeScript cannot express that guarantee for RegExpExecArray.
|
|
134
|
+
const candidate = match.slice(1).find((value) => value !== undefined);
|
|
135
|
+
if (candidateReferencesReservedWorkspacePath(candidate, false))
|
|
136
|
+
return true;
|
|
143
137
|
}
|
|
144
138
|
return false;
|
|
145
139
|
}
|
|
140
|
+
/** Keep each import's whole span consumed, including URL imports already inspected by the first
|
|
141
|
+
* pass. Rescanning @import text quoted inside such a URL would change the path-refusal policy. */
|
|
142
|
+
function* cssReferenceMatches({ cssText }) {
|
|
143
|
+
yield* cssText.matchAll(CSS_URL_RE);
|
|
144
|
+
const imports = new RegExp(CSS_IMPORT_START_RE);
|
|
145
|
+
const url = new RegExp(CSS_URL_RE.source, 'iy');
|
|
146
|
+
const quoted = new RegExp(CSS_QUOTED_IMPORT_RE);
|
|
147
|
+
let header = imports.exec(cssText);
|
|
148
|
+
while (header !== null) {
|
|
149
|
+
url.lastIndex = imports.lastIndex;
|
|
150
|
+
quoted.lastIndex = imports.lastIndex;
|
|
151
|
+
const value = url.exec(cssText) ?? quoted.exec(cssText);
|
|
152
|
+
if (value) {
|
|
153
|
+
imports.lastIndex = value.index + value[0].length;
|
|
154
|
+
yield value;
|
|
155
|
+
}
|
|
156
|
+
header = imports.exec(cssText);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
146
159
|
function candidateReferencesReservedWorkspacePath(candidate, splitCandidates) {
|
|
147
160
|
const paths = splitCandidates ? srcsetCandidateUrls(candidate) : [firstUrlToken(candidate)];
|
|
148
161
|
return paths.some((path) => {
|
|
@@ -171,10 +184,9 @@ function srcsetCandidateUrls(srcset) {
|
|
|
171
184
|
continue;
|
|
172
185
|
}
|
|
173
186
|
candidate ??= { isDataUrl: /^data:/i.test(srcset.slice(index)), sawWhitespaceAfterUrl: false };
|
|
174
|
-
if (isSpace)
|
|
187
|
+
if (isSpace)
|
|
175
188
|
candidate.sawWhitespaceAfterUrl = true;
|
|
176
|
-
|
|
177
|
-
else if (endsSrcsetCandidate(candidate, char)) {
|
|
189
|
+
if (!isSpace && endsSrcsetCandidate(candidate, char)) {
|
|
178
190
|
candidates.push(srcset.slice(start, index));
|
|
179
191
|
start = index + 1;
|
|
180
192
|
candidate = null;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
const ARIA_LABEL = 'aria-label';
|
|
1
2
|
export { CHAT_BUBBLE_STYLE } from '../../contracts/chat-bubble-style.js';
|
|
2
3
|
/**
|
|
3
4
|
* Activates a host-rendered button without importing chat, React, icons or storage.
|
|
@@ -11,10 +12,10 @@ export function installLazyChatLoader(args, _options = {}) {
|
|
|
11
12
|
let shouldOpen = false;
|
|
12
13
|
let shouldFocus = false;
|
|
13
14
|
const initialContent = args.button.innerHTML;
|
|
14
|
-
const label = args.button.getAttribute(
|
|
15
|
+
const label = args.button.getAttribute(ARIA_LABEL) ?? '';
|
|
15
16
|
const activate = async ({ open = true, focus = open } = {}, _options = {}) => {
|
|
16
17
|
if (disposed)
|
|
17
|
-
return;
|
|
18
|
+
return undefined;
|
|
18
19
|
if (!pending) {
|
|
19
20
|
shouldOpen = false;
|
|
20
21
|
shouldFocus = false;
|
|
@@ -24,26 +25,29 @@ export function installLazyChatLoader(args, _options = {}) {
|
|
|
24
25
|
if (pending)
|
|
25
26
|
return pending;
|
|
26
27
|
args.button.setAttribute('aria-busy', 'true');
|
|
27
|
-
args.button.setAttribute(
|
|
28
|
+
args.button.setAttribute(ARIA_LABEL, args.loadingLabel);
|
|
28
29
|
args.button.textContent = args.loadingLabel;
|
|
29
30
|
delete args.button.dataset.loadFailed;
|
|
30
31
|
// Defer through a promise so a synchronous host load failure follows the same retry path.
|
|
31
|
-
pending = Promise.resolve().then(() => {
|
|
32
|
-
|
|
32
|
+
pending = Promise.resolve().then(() => {
|
|
33
|
+
if (!ready)
|
|
34
|
+
return args.load({});
|
|
35
|
+
return undefined;
|
|
36
|
+
}).then(() => {
|
|
33
37
|
if (disposed)
|
|
34
38
|
return;
|
|
35
39
|
// Asset readiness survives an open failure; retry must never evaluate the script twice.
|
|
36
40
|
ready = true;
|
|
37
41
|
if (shouldOpen)
|
|
38
42
|
args.open({ focus: shouldFocus });
|
|
39
|
-
args.button.setAttribute(
|
|
43
|
+
args.button.setAttribute(ARIA_LABEL, label);
|
|
40
44
|
args.button.innerHTML = initialContent;
|
|
41
45
|
args.button.hidden = true;
|
|
42
46
|
}).catch(() => {
|
|
43
47
|
if (!disposed) {
|
|
44
48
|
args.button.hidden = false;
|
|
45
49
|
args.button.dataset.loadFailed = 'true';
|
|
46
|
-
args.button.setAttribute(
|
|
50
|
+
args.button.setAttribute(ARIA_LABEL, args.retryLabel);
|
|
47
51
|
args.button.textContent = args.retryLabel;
|
|
48
52
|
}
|
|
49
53
|
}).finally(() => {
|
|
@@ -62,17 +66,17 @@ export function installLazyChatLoader(args, _options = {}) {
|
|
|
62
66
|
args.button.addEventListener('click', click);
|
|
63
67
|
args.button.addEventListener('keydown', keydown);
|
|
64
68
|
args.button.dataset.loaderReady = 'true';
|
|
65
|
-
if (args.resume || args.deepLink)
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
+
if (args.resume || args.deepLink || args.preload) {
|
|
70
|
+
const resume = args.resume || args.deepLink;
|
|
71
|
+
void activate({ open: resume, focus: resume && args.deepLink });
|
|
72
|
+
}
|
|
69
73
|
return {
|
|
70
74
|
activate,
|
|
71
75
|
dispose(_input = {}, _options = {}) {
|
|
72
76
|
disposed = true;
|
|
73
77
|
args.button.removeEventListener('click', click);
|
|
74
78
|
args.button.removeEventListener('keydown', keydown);
|
|
75
|
-
args.button.setAttribute(
|
|
79
|
+
args.button.setAttribute(ARIA_LABEL, label);
|
|
76
80
|
args.button.innerHTML = initialContent;
|
|
77
81
|
args.button.removeAttribute('aria-busy');
|
|
78
82
|
delete args.button.dataset.loadFailed;
|
|
@@ -25,11 +25,11 @@ export function useEmbedChatDialog({ open, onClose, panelRef, launcherRef }, { r
|
|
|
25
25
|
}, [open, launcherRef]);
|
|
26
26
|
useEffect(() => {
|
|
27
27
|
if (!open)
|
|
28
|
-
return;
|
|
28
|
+
return undefined;
|
|
29
29
|
const panel = panelRef.current;
|
|
30
30
|
const view = panel?.ownerDocument.defaultView;
|
|
31
31
|
if (!panel || !view)
|
|
32
|
-
return;
|
|
32
|
+
return undefined;
|
|
33
33
|
const onKeyDown = (event) => {
|
|
34
34
|
if (event.key === 'Escape' && !event.defaultPrevented) {
|
|
35
35
|
event.preventDefault();
|
|
@@ -36,7 +36,7 @@ export function EmbedChatWidget(args) {
|
|
|
36
36
|
useEffect(() => {
|
|
37
37
|
const queued = { value: session.drain({}) };
|
|
38
38
|
if (!isQueuedPageAction(queued) || queued.value.type === 'navigate')
|
|
39
|
-
return;
|
|
39
|
+
return undefined;
|
|
40
40
|
const action = queued.value;
|
|
41
41
|
return scheduleFrame({ callback: () => { pageActions.execute({ action }); } });
|
|
42
42
|
}, [session, pageActions, scheduleFrame]);
|
|
@@ -15,7 +15,7 @@ export function useEmbedChatHeader({ hasMessages, onReset, keyboard }, _options
|
|
|
15
15
|
}, [confirming]);
|
|
16
16
|
useEffect(() => {
|
|
17
17
|
if (!confirming)
|
|
18
|
-
return;
|
|
18
|
+
return undefined;
|
|
19
19
|
return keyboard.subscribe({ onEscape: () => setConfirming(false) });
|
|
20
20
|
}, [confirming, keyboard]);
|
|
21
21
|
return { confirming, cancel, headerRef, resetButtonRef,
|
|
@@ -151,7 +151,7 @@ async function deliverResponse({ run, args, response }) {
|
|
|
151
151
|
await response.body?.cancel();
|
|
152
152
|
}
|
|
153
153
|
catch { /* closed body */ }
|
|
154
|
-
return;
|
|
154
|
+
return undefined;
|
|
155
155
|
}
|
|
156
156
|
if (!response.ok)
|
|
157
157
|
return reportHttpFailure({ run, response, requestFailed: args.copy.requestFailed });
|
|
@@ -161,6 +161,7 @@ async function deliverResponse({ run, args, response }) {
|
|
|
161
161
|
if (run.isSettled() || !dispatchFrame({ run, result: args.frameMapper({ frame }) }))
|
|
162
162
|
break;
|
|
163
163
|
}
|
|
164
|
+
return undefined;
|
|
164
165
|
}
|
|
165
166
|
async function reportHttpFailure({ run, response, requestFailed }) {
|
|
166
167
|
const body = await response.json().catch(() => null);
|