@jini-ai/chat 0.6.6 → 0.6.10
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 +129 -1
- 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 +187 -98
- 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 +31 -6
- package/dist/core/durable-runs/ledger-contracts.d.ts +2 -1
- 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.js +54 -32
- 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.d.ts +1 -1
- package/dist/react/components/MessageRow.js +75 -47
- package/dist/react/components/MessageRow.usage.js +16 -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/ToolCallGroup.d.ts +4 -3
- package/dist/react/components/ToolCallGroup.js +7 -5
- 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 +14 -0
- package/dist/react/features/chat-pane/model-options.js +45 -25
- 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 +152 -36
- package/dist/react/features/chat-pane/types.d.ts +19 -9
- 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.d.ts +12 -6
- package/dist/react/tool-call-display.js +53 -24
- package/dist/server/durable-runs/recover.js +132 -80
- 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 +18 -16
package/dist/core/util/parser.js
CHANGED
|
@@ -11,6 +11,8 @@ import { computeSkipRanges, FENCE_OPEN_RE, isRealArtifactOpenAt, rangeContains }
|
|
|
11
11
|
import { parseQuotedAttrs } from './markup-attributes.js';
|
|
12
12
|
const OPEN_PREFIX = '<artifact';
|
|
13
13
|
const CLOSE_TAG = '</artifact>';
|
|
14
|
+
const NO_OPEN = { kind: 'none' };
|
|
15
|
+
const SKIP = { kind: 'skip' };
|
|
14
16
|
// Scan the buffer for `<artifact …>` while skipping any positions that a
|
|
15
17
|
// Markdown renderer would render as a fenced code block or inline code span
|
|
16
18
|
// — see markdown-context.ts for the shared classification used by both this
|
|
@@ -23,107 +25,100 @@ const CLOSE_TAG = '</artifact>';
|
|
|
23
25
|
// * Unmatched opening backtick after the last \n → hold back from it; a
|
|
24
26
|
// future chunk may turn it into an inline code span.
|
|
25
27
|
function findOpenTag(buffer) {
|
|
26
|
-
const len = buffer.length;
|
|
27
28
|
const { ranges, unclosedFenceStart } = computeSkipRanges({ buffer: buffer });
|
|
29
|
+
const ctx = { buffer, ranges };
|
|
28
30
|
// Pass 1: scan for the earliest *complete* real `<artifact …>` open outside
|
|
29
31
|
// any skip range, before any hold-back decision — otherwise a stray
|
|
30
32
|
// backtick or fence-opener prefix on a tail line would suppress an
|
|
31
33
|
// already self-contained artifact earlier in the buffer.
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
const idx = buffer.indexOf(OPEN_PREFIX, from);
|
|
36
|
-
if (idx === -1)
|
|
37
|
-
break;
|
|
38
|
-
if (rangeContains({ ranges: ranges, p: idx })) {
|
|
39
|
-
from = idx + OPEN_PREFIX.length;
|
|
40
|
-
continue;
|
|
41
|
-
}
|
|
42
|
-
if (unclosedFenceStart !== null && idx >= unclosedFenceStart) {
|
|
43
|
-
// Anything past an unclosed fence opener is inside a code block that
|
|
44
|
-
// will close in a later chunk; treat as skip range, not a real tag.
|
|
45
|
-
break;
|
|
46
|
-
}
|
|
47
|
-
const after = idx + OPEN_PREFIX.length;
|
|
48
|
-
const next = buffer.charAt(after);
|
|
49
|
-
if (next === '') {
|
|
50
|
-
// `<artifact` at very end of buffer — could become real with the next
|
|
51
|
-
// chunk. Remember the earliest one and keep looking for a complete tag.
|
|
52
|
-
if (earliestPartialOpen === -1)
|
|
53
|
-
earliestPartialOpen = idx;
|
|
54
|
-
break;
|
|
55
|
-
}
|
|
56
|
-
if (!isRealArtifactOpenAt({ content: buffer, idx: idx })) {
|
|
57
|
-
from = after; // e.g. "<artifactual" — not a real open, keep scanning
|
|
58
|
-
continue;
|
|
59
|
-
}
|
|
60
|
-
let j = after;
|
|
61
|
-
let quote = null;
|
|
62
|
-
while (j < len) {
|
|
63
|
-
const c = buffer.charAt(j);
|
|
64
|
-
if (quote !== null) {
|
|
65
|
-
if (c === quote)
|
|
66
|
-
quote = null;
|
|
67
|
-
}
|
|
68
|
-
else if (c === '"' || c === "'") {
|
|
69
|
-
quote = c;
|
|
70
|
-
}
|
|
71
|
-
else if (c === '>') {
|
|
72
|
-
return { kind: 'complete', start: idx, end: j + 1, attrs: buffer.slice(after, j) };
|
|
73
|
-
}
|
|
74
|
-
j++;
|
|
75
|
-
}
|
|
76
|
-
// Ran out of buffer before the closing `>` arrived — an open tag mid-stream.
|
|
77
|
-
if (earliestPartialOpen === -1)
|
|
78
|
-
earliestPartialOpen = idx;
|
|
79
|
-
break;
|
|
80
|
-
}
|
|
34
|
+
const open = scanForOpen(ctx, { unclosedFenceStart });
|
|
35
|
+
if (open.kind === 'complete')
|
|
36
|
+
return open;
|
|
81
37
|
// Pass 2: no complete open found. Decide whether to hold back, and if so,
|
|
82
38
|
// from which position. Earliest hold-back wins so the text-flush boundary
|
|
83
39
|
// never crosses something that might still resolve into a tag/fence/span.
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
40
|
+
const holdbacks = [
|
|
41
|
+
open.kind === 'partial' ? open.start : -1,
|
|
42
|
+
unclosedFenceStart ?? -1,
|
|
43
|
+
tailFenceHoldback(ctx),
|
|
44
|
+
unmatchedBacktickHoldback(ctx),
|
|
45
|
+
tailPrefixHoldback(ctx),
|
|
46
|
+
].filter((pos) => pos !== -1);
|
|
47
|
+
if (holdbacks.length === 0)
|
|
48
|
+
return NO_OPEN;
|
|
49
|
+
return { kind: 'partial', start: Math.min(...holdbacks) };
|
|
50
|
+
}
|
|
51
|
+
function scanForOpen(ctx, { unclosedFenceStart }) {
|
|
52
|
+
let idx = ctx.buffer.indexOf(OPEN_PREFIX);
|
|
53
|
+
while (idx !== -1) {
|
|
54
|
+
const found = classifyOpenAt(ctx, { idx, unclosedFenceStart });
|
|
55
|
+
if (found.kind !== 'skip')
|
|
56
|
+
return found;
|
|
57
|
+
idx = ctx.buffer.indexOf(OPEN_PREFIX, idx + OPEN_PREFIX.length);
|
|
58
|
+
}
|
|
59
|
+
return NO_OPEN;
|
|
60
|
+
}
|
|
61
|
+
function classifyOpenAt({ buffer, ranges }, { idx, unclosedFenceStart }) {
|
|
62
|
+
if (rangeContains({ ranges: ranges, p: idx }))
|
|
63
|
+
return SKIP;
|
|
64
|
+
// Anything past an unclosed fence opener is inside a code block that
|
|
65
|
+
// will close in a later chunk; treat as skip range, not a real tag.
|
|
66
|
+
if (unclosedFenceStart !== null && idx >= unclosedFenceStart)
|
|
67
|
+
return NO_OPEN;
|
|
68
|
+
const after = idx + OPEN_PREFIX.length;
|
|
69
|
+
// `<artifact` at very end of buffer — could become real with the next
|
|
70
|
+
// chunk. Remember the earliest one and keep looking for a complete tag.
|
|
71
|
+
if (after === buffer.length)
|
|
72
|
+
return { kind: 'partial', start: idx };
|
|
73
|
+
if (!isRealArtifactOpenAt({ content: buffer, idx: idx }))
|
|
74
|
+
return SKIP; // e.g. "<artifactual" — not a real open, keep scanning
|
|
75
|
+
const close = findTagEnd({ buffer, from: after });
|
|
76
|
+
// Ran out of buffer before the closing `>` arrived — an open tag mid-stream.
|
|
77
|
+
if (close === -1)
|
|
78
|
+
return { kind: 'partial', start: idx };
|
|
79
|
+
return { kind: 'complete', start: idx, end: close + 1, attrs: buffer.slice(after, close) };
|
|
80
|
+
}
|
|
81
|
+
/** Index of the open tag's closing `>`, ignoring any `>` inside a quoted attribute value; -1 if not yet arrived. */
|
|
82
|
+
function findTagEnd({ buffer, from }) {
|
|
83
|
+
let quote = null;
|
|
84
|
+
for (let j = from; j < buffer.length; j++) {
|
|
85
|
+
const c = buffer.charAt(j);
|
|
86
|
+
if (quote !== null) {
|
|
87
|
+
if (c === quote)
|
|
88
|
+
quote = null;
|
|
89
|
+
}
|
|
90
|
+
else if (c === '"' || c === "'") {
|
|
91
|
+
quote = c;
|
|
92
|
+
}
|
|
93
|
+
else if (c === '>') {
|
|
94
|
+
return j;
|
|
97
95
|
}
|
|
98
96
|
}
|
|
97
|
+
return -1;
|
|
98
|
+
}
|
|
99
|
+
/** Start of an unterminated tail line that could still resolve into a fence delimiter; -1 otherwise. */
|
|
100
|
+
function tailFenceHoldback({ buffer }) {
|
|
101
|
+
const tailLineStart = buffer.lastIndexOf('\n') + 1;
|
|
102
|
+
const tail = buffer.slice(tailLineStart);
|
|
103
|
+
return FENCE_OPEN_RE.test(tail) || /^`{1,2}$/.test(tail) ? tailLineStart : -1;
|
|
104
|
+
}
|
|
105
|
+
/** Position of an unmatched opening backtick after the last `\n` (outside skip ranges); -1 otherwise. */
|
|
106
|
+
function unmatchedBacktickHoldback({ buffer, ranges }) {
|
|
99
107
|
let firstUnmatched = -1;
|
|
100
|
-
let
|
|
101
|
-
|
|
102
|
-
if (buffer.charAt(k) !== '`')
|
|
108
|
+
for (let k = buffer.lastIndexOf('\n') + 1; k < buffer.length; k++) {
|
|
109
|
+
if (buffer.charAt(k) !== '`' || rangeContains({ ranges: ranges, p: k }))
|
|
103
110
|
continue;
|
|
104
|
-
|
|
105
|
-
continue;
|
|
106
|
-
if (parity === 0) {
|
|
107
|
-
firstUnmatched = k;
|
|
108
|
-
parity = 1;
|
|
109
|
-
}
|
|
110
|
-
else {
|
|
111
|
-
firstUnmatched = -1;
|
|
112
|
-
parity = 0;
|
|
113
|
-
}
|
|
111
|
+
firstUnmatched = firstUnmatched === -1 ? k : -1;
|
|
114
112
|
}
|
|
115
|
-
|
|
116
|
-
|
|
113
|
+
return firstUnmatched;
|
|
114
|
+
}
|
|
115
|
+
// Strict prefix at the tail (e.g. "<art") — hold back.
|
|
116
|
+
function tailPrefixHoldback({ buffer, ranges }) {
|
|
117
117
|
const tailLt = buffer.lastIndexOf('<');
|
|
118
|
-
if (tailLt
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
if (holdback !== -1)
|
|
125
|
-
return { kind: 'partial', start: holdback };
|
|
126
|
-
return { kind: 'none' };
|
|
118
|
+
if (tailLt === -1 || rangeContains({ ranges: ranges, p: tailLt }))
|
|
119
|
+
return -1;
|
|
120
|
+
const slice = buffer.slice(tailLt);
|
|
121
|
+
return OPEN_PREFIX.startsWith(slice) && slice.length < OPEN_PREFIX.length ? tailLt : -1;
|
|
127
122
|
}
|
|
128
123
|
/**
|
|
129
124
|
* Create a fresh streaming artifact parser. Call `feed({ delta })` for each
|
|
@@ -146,64 +141,61 @@ export function createArtifactParser(_requiredArgs) {
|
|
|
146
141
|
};
|
|
147
142
|
function* feed({ delta }) {
|
|
148
143
|
state.buffer += delta;
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
}
|
|
192
|
-
return;
|
|
193
|
-
}
|
|
194
|
-
const finalChunk = state.buffer.slice(0, closeIdx);
|
|
195
|
-
if (finalChunk.length > 0) {
|
|
196
|
-
state.content += finalChunk;
|
|
197
|
-
yield { type: 'artifact:chunk', identifier: state.identifier, delta: finalChunk };
|
|
198
|
-
}
|
|
199
|
-
yield { type: 'artifact:end', identifier: state.identifier, fullContent: state.content };
|
|
200
|
-
state.buffer = state.buffer.slice(closeIdx + CLOSE_TAG.length);
|
|
201
|
-
state.inside = false;
|
|
202
|
-
state.identifier = '';
|
|
203
|
-
state.artifactType = '';
|
|
204
|
-
state.title = '';
|
|
205
|
-
state.content = '';
|
|
144
|
+
let progressed = true;
|
|
145
|
+
while (progressed && state.buffer.length > 0) {
|
|
146
|
+
progressed = state.inside ? yield* drainInside() : yield* drainOutside();
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
/** Emit text up to the next open tag; `true` when an artifact was opened and draining should continue. */
|
|
150
|
+
function* drainOutside() {
|
|
151
|
+
const open = findOpenTag(state.buffer);
|
|
152
|
+
const textEnd = open.kind === 'none' ? state.buffer.length : open.start;
|
|
153
|
+
if (textEnd > 0) {
|
|
154
|
+
yield { type: 'text', delta: state.buffer.slice(0, textEnd) };
|
|
155
|
+
state.buffer = state.buffer.slice(textEnd);
|
|
156
|
+
}
|
|
157
|
+
if (open.kind !== 'complete')
|
|
158
|
+
return false;
|
|
159
|
+
const attrs = parseQuotedAttrs({ raw: open.attrs });
|
|
160
|
+
Object.assign(state, {
|
|
161
|
+
inside: true,
|
|
162
|
+
identifier: attrs['identifier'] ?? '',
|
|
163
|
+
artifactType: attrs['type'] ?? '',
|
|
164
|
+
title: attrs['title'] ?? '',
|
|
165
|
+
content: '',
|
|
166
|
+
buffer: state.buffer.slice(open.end - open.start),
|
|
167
|
+
});
|
|
168
|
+
yield {
|
|
169
|
+
type: 'artifact:start',
|
|
170
|
+
identifier: state.identifier,
|
|
171
|
+
artifactType: state.artifactType,
|
|
172
|
+
title: state.title,
|
|
173
|
+
};
|
|
174
|
+
return true;
|
|
175
|
+
}
|
|
176
|
+
/** Emit body up to the close tag; `true` when the artifact closed and draining should continue. */
|
|
177
|
+
function* drainInside() {
|
|
178
|
+
const closeIdx = state.buffer.indexOf(CLOSE_TAG);
|
|
179
|
+
// Hold back enough bytes to detect a partial close tag at the tail.
|
|
180
|
+
const chunkEnd = closeIdx === -1 ? state.buffer.length - (CLOSE_TAG.length - 1) : closeIdx;
|
|
181
|
+
if (chunkEnd > 0) {
|
|
182
|
+
const chunk = state.buffer.slice(0, chunkEnd);
|
|
183
|
+
state.content += chunk;
|
|
184
|
+
state.buffer = state.buffer.slice(chunkEnd);
|
|
185
|
+
yield { type: 'artifact:chunk', identifier: state.identifier, delta: chunk };
|
|
206
186
|
}
|
|
187
|
+
if (closeIdx === -1)
|
|
188
|
+
return false;
|
|
189
|
+
yield { type: 'artifact:end', identifier: state.identifier, fullContent: state.content };
|
|
190
|
+
Object.assign(state, {
|
|
191
|
+
inside: false,
|
|
192
|
+
identifier: '',
|
|
193
|
+
artifactType: '',
|
|
194
|
+
title: '',
|
|
195
|
+
content: '',
|
|
196
|
+
buffer: state.buffer.slice(CLOSE_TAG.length),
|
|
197
|
+
});
|
|
198
|
+
return true;
|
|
207
199
|
}
|
|
208
200
|
function* flush(_requiredArgs) {
|
|
209
201
|
if (state.inside) {
|
|
@@ -13,29 +13,29 @@ const HTML_TAG_RE = /<[^>]+>/g;
|
|
|
13
13
|
* @complexity O(n + m) in `content.length` plus `projectFiles.length`.
|
|
14
14
|
*/
|
|
15
15
|
export function resolveHtmlPointerArtifactTarget(input) {
|
|
16
|
-
const
|
|
17
|
-
if (
|
|
16
|
+
const target = extractPointerTarget(input.content);
|
|
17
|
+
if (target === null || target === input.candidateFileName || !isSafeHtmlTarget(target))
|
|
18
18
|
return null;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
|
|
19
|
+
return resolveAgainstProjectFiles({ target, candidateFileName: input.candidateFileName, projectFiles: input.projectFiles });
|
|
20
|
+
}
|
|
21
|
+
/** The normalized file name a bare short pointer reply names, or `null` when `content` isn't one. */
|
|
22
|
+
function extractPointerTarget(content) {
|
|
23
|
+
const pointerText = visiblePointerText(content);
|
|
24
|
+
if (!pointerText || utf8ByteLength(pointerText) > MAX_POINTER_TEXT_BYTES)
|
|
22
25
|
return null;
|
|
26
|
+
const match = POINTER_TARGET_RE.exec(pointerText);
|
|
23
27
|
// The target group is a mandatory (non-optional) part of the pattern, not
|
|
24
28
|
// inside an alternation, so it's always defined when `match` is non-null;
|
|
25
29
|
// the assertion just satisfies `noUncheckedIndexedAccess`.
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
const projectFileNames =
|
|
30
|
+
return match ? normalizeTarget(match[1]) : null;
|
|
31
|
+
}
|
|
32
|
+
/** A full-path match wins; otherwise exactly one basename match that isn't the candidate itself. */
|
|
33
|
+
function resolveAgainstProjectFiles({ target, candidateFileName, projectFiles }) {
|
|
34
|
+
const projectFileNames = projectFiles.map((file) => file.path || file.name).filter((name) => /\.html?$/i.test(name));
|
|
31
35
|
if (projectFileNames.includes(target))
|
|
32
36
|
return target;
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
if (basenameMatches.length === 1 && basenameMatch && basenameMatch !== input.candidateFileName) {
|
|
36
|
-
return basenameMatch;
|
|
37
|
-
}
|
|
38
|
-
return null;
|
|
37
|
+
const [only, ...others] = projectFileNames.filter((name) => basename(name) === target);
|
|
38
|
+
return only !== undefined && others.length === 0 && only !== candidateFileName ? only : null;
|
|
39
39
|
}
|
|
40
40
|
function visiblePointerText(content) {
|
|
41
41
|
const stripped = content
|
package/dist/core/util/strip.js
CHANGED
|
@@ -45,6 +45,29 @@ function findRealOpen(content, fromIndex, ranges) {
|
|
|
45
45
|
return idx;
|
|
46
46
|
}
|
|
47
47
|
}
|
|
48
|
+
// For complete (non-streaming) content, an unclosed fence renders as a code
|
|
49
|
+
// block extending to end of input — the stripper mirrors that, otherwise a
|
|
50
|
+
// literal `<artifact …>` tucked into a trailing code example (no trailing
|
|
51
|
+
// newline) gets treated as a real protocol tag and eaten.
|
|
52
|
+
function completeContentSkipRanges({ content }) {
|
|
53
|
+
const { ranges, unclosedFenceStart } = computeSkipRanges({ buffer: content });
|
|
54
|
+
return unclosedFenceStart !== null ? [...ranges, [unclosedFenceStart, content.length]] : ranges;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* The first complete real `<artifact …>…</artifact>` block at or after
|
|
58
|
+
* `fromIndex`, or `null` when there is no real open, the open tag never
|
|
59
|
+
* terminates with `>`, or no real close follows it.
|
|
60
|
+
*/
|
|
61
|
+
function findCompleteArtifactBlock({ content, ranges }, { fromIndex = 0 } = {}) {
|
|
62
|
+
const open = findRealOpen(content, fromIndex, ranges);
|
|
63
|
+
if (open === -1)
|
|
64
|
+
return null;
|
|
65
|
+
const openEnd = content.indexOf('>', open);
|
|
66
|
+
if (openEnd === -1)
|
|
67
|
+
return null;
|
|
68
|
+
const close = findUnskipped(content, CLOSE, openEnd, ranges);
|
|
69
|
+
return close === -1 ? null : { open, openEnd, close };
|
|
70
|
+
}
|
|
48
71
|
/**
|
|
49
72
|
* Remove the first real `<artifact …>…</artifact>` block from `content`.
|
|
50
73
|
*
|
|
@@ -62,22 +85,10 @@ function findRealOpen(content, fromIndex, ranges) {
|
|
|
62
85
|
* pair of linear scans for the open/close tags.
|
|
63
86
|
*/
|
|
64
87
|
export function stripArtifact({ content }) {
|
|
65
|
-
const { ranges:
|
|
66
|
-
|
|
67
|
-
// block extending to end of input — the stripper mirrors that, otherwise a
|
|
68
|
-
// literal `<artifact …>` tucked into a trailing code example (no trailing
|
|
69
|
-
// newline) gets treated as a real protocol tag and eaten.
|
|
70
|
-
const ranges = unclosedFenceStart !== null ? [...baseRanges, [unclosedFenceStart, content.length]] : baseRanges;
|
|
71
|
-
const open = findRealOpen(content, 0, ranges);
|
|
72
|
-
if (open === -1)
|
|
88
|
+
const block = findCompleteArtifactBlock({ content, ranges: completeContentSkipRanges({ content }) });
|
|
89
|
+
if (!block)
|
|
73
90
|
return content;
|
|
74
|
-
|
|
75
|
-
if (closeTag === -1)
|
|
76
|
-
return content;
|
|
77
|
-
const end = findUnskipped(content, CLOSE, closeTag, ranges);
|
|
78
|
-
if (end === -1)
|
|
79
|
-
return content;
|
|
80
|
-
return (content.slice(0, open) + content.slice(end + CLOSE.length)).trim();
|
|
91
|
+
return (content.slice(0, block.open) + content.slice(block.close + CLOSE.length)).trim();
|
|
81
92
|
}
|
|
82
93
|
function findSingleRecoverableHtmlFence(content) {
|
|
83
94
|
HTML_FENCE_RE.lastIndex = 0;
|
|
@@ -95,31 +106,24 @@ function findSingleRecoverableHtmlFence(content) {
|
|
|
95
106
|
return count === 1 ? recovered : null;
|
|
96
107
|
}
|
|
97
108
|
function findRecoverablePrecedingHtmlArtifact(sourceText) {
|
|
98
|
-
const
|
|
99
|
-
const ranges = unclosedFenceStart !== null ? [...baseRanges, [unclosedFenceStart, sourceText.length]] : baseRanges;
|
|
109
|
+
const ranges = completeContentSkipRanges({ content: sourceText });
|
|
100
110
|
let from = 0;
|
|
101
111
|
// `findRealOpen` already returns -1 once `from` runs past `sourceText.length`
|
|
102
112
|
// (it inherits the same `indexOf`-based bound safety), so this loop also
|
|
103
113
|
// always exits through an inner return — no trailing fallback needed.
|
|
104
114
|
for (;;) {
|
|
105
|
-
const
|
|
106
|
-
if (
|
|
107
|
-
return null;
|
|
108
|
-
const closeTag = sourceText.indexOf('>', open);
|
|
109
|
-
if (closeTag === -1)
|
|
110
|
-
return null;
|
|
111
|
-
const end = findUnskipped(sourceText, CLOSE, closeTag, ranges);
|
|
112
|
-
if (end === -1)
|
|
115
|
+
const block = findCompleteArtifactBlock({ content: sourceText, ranges }, { fromIndex: from });
|
|
116
|
+
if (!block)
|
|
113
117
|
return null;
|
|
114
|
-
const attrs = parseQuotedAttrs({ raw: sourceText.slice(open,
|
|
118
|
+
const attrs = parseQuotedAttrs({ raw: sourceText.slice(block.open, block.openEnd) });
|
|
115
119
|
const recovered = recoverHtmlArtifactFromPrecedingDocument({
|
|
116
|
-
artifactHtml: sourceText.slice(
|
|
120
|
+
artifactHtml: sourceText.slice(block.openEnd + 1, block.close),
|
|
117
121
|
identifier: attrs['identifier'],
|
|
118
122
|
sourceText,
|
|
119
123
|
});
|
|
120
124
|
if (recovered)
|
|
121
125
|
return recovered;
|
|
122
|
-
from =
|
|
126
|
+
from = block.close + CLOSE.length;
|
|
123
127
|
}
|
|
124
128
|
}
|
|
125
129
|
function stripRecoverablePrecedingHtml(content, sourceText) {
|
|
@@ -156,21 +160,20 @@ export function stripRecoveredHtmlFallbackForDisplay({ content }, { sourceText =
|
|
|
156
160
|
return content;
|
|
157
161
|
return `${content.slice(0, fence.start)}${content.slice(fence.end)}`.trim();
|
|
158
162
|
}
|
|
163
|
+
// Checked in order; the first rule whose identifier suffix, exact type, or type substring matches wins.
|
|
164
|
+
const EXTENSION_RULES = [
|
|
165
|
+
{ ext: '.tsx', typeIncludes: ['tsx'] },
|
|
166
|
+
{ ext: '.jsx', typeIncludes: ['jsx', 'react'] },
|
|
167
|
+
{ ext: '.css', typeIncludes: ['css'] },
|
|
168
|
+
{ ext: '.svg', typeIncludes: ['svg'] },
|
|
169
|
+
{ ext: '.md', typeIncludes: ['markdown'], typeEquals: 'md' },
|
|
170
|
+
];
|
|
159
171
|
// The on-disk extension a persist path would pick from the artifact's type/identifier.
|
|
160
172
|
function artifactExtensionForAttrs(attrs) {
|
|
161
173
|
const type = (attrs['type'] || '').toLowerCase();
|
|
162
174
|
const identifier = (attrs['identifier'] || '').toLowerCase();
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
if (type.includes('jsx') || type.includes('react') || identifier.endsWith('.jsx'))
|
|
166
|
-
return '.jsx';
|
|
167
|
-
if (type.includes('css') || identifier.endsWith('.css'))
|
|
168
|
-
return '.css';
|
|
169
|
-
if (type.includes('svg') || identifier.endsWith('.svg'))
|
|
170
|
-
return '.svg';
|
|
171
|
-
if (type.includes('markdown') || type === 'md' || identifier.endsWith('.md'))
|
|
172
|
-
return '.md';
|
|
173
|
-
return '.html';
|
|
175
|
+
const rule = EXTENSION_RULES.find((r) => identifier.endsWith(r.ext) || r.typeEquals === type || r.typeIncludes.some((hint) => type.includes(hint)));
|
|
176
|
+
return rule?.ext ?? '.html';
|
|
174
177
|
}
|
|
175
178
|
// The slug a persist path would derive the file name from.
|
|
176
179
|
function artifactBaseNameForAttrs(attrs) {
|
|
@@ -234,38 +237,26 @@ export function summarizeArtifactsForTranscript({ content, persistedFiles }) {
|
|
|
234
237
|
let result = '';
|
|
235
238
|
let cursor = 0;
|
|
236
239
|
// Recompute skip ranges per iteration against the remaining tail so indices
|
|
237
|
-
// stay valid as we consume the string left to right.
|
|
238
|
-
|
|
240
|
+
// stay valid as we consume the string left to right. Each iteration either
|
|
241
|
+
// returns or consumes at least one whole block, so the loop terminates.
|
|
242
|
+
for (;;) {
|
|
239
243
|
const tail = content.slice(cursor);
|
|
240
|
-
const {
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
const
|
|
248
|
-
if (gt === -1) {
|
|
249
|
-
result += tail;
|
|
250
|
-
break;
|
|
251
|
-
}
|
|
252
|
-
const end = findUnskipped(tail, CLOSE, gt, ranges);
|
|
253
|
-
if (end === -1) {
|
|
254
|
-
// Real open but no real close — refuse to summarize (safer than eating
|
|
255
|
-
// to end-of-string on a malformed/streaming tag). Keep the rest as-is.
|
|
256
|
-
result += tail;
|
|
257
|
-
break;
|
|
258
|
-
}
|
|
259
|
-
const attrs = parseQuotedAttrs({ raw: tail.slice(open, gt) });
|
|
244
|
+
const block = findCompleteArtifactBlock({ content: tail, ranges: completeContentSkipRanges({ content: tail }) });
|
|
245
|
+
// No real open, an open tag that never terminates, or a real open but no
|
|
246
|
+
// real close — refuse to summarize (safer than eating to end-of-string on
|
|
247
|
+
// a malformed/streaming tag). Keep the rest as-is.
|
|
248
|
+
if (!block)
|
|
249
|
+
return result + tail;
|
|
250
|
+
const afterBlock = block.close + CLOSE.length;
|
|
251
|
+
const attrs = parseQuotedAttrs({ raw: tail.slice(block.open, block.openEnd) });
|
|
260
252
|
const persisted = matchPersistedArtifactFile({ attrs: attrs, persistedFiles: persistedFiles });
|
|
261
253
|
result += persisted
|
|
262
|
-
? tail.slice(0, open) + artifactTranscriptSummary(attrs, persisted)
|
|
254
|
+
? tail.slice(0, block.open) + artifactTranscriptSummary(attrs, persisted)
|
|
263
255
|
: // Unconfirmed save — the transcript copy may be the only surviving
|
|
264
256
|
// body. Keep the block verbatim and keep scanning past it.
|
|
265
|
-
tail.slice(0,
|
|
266
|
-
cursor +=
|
|
257
|
+
tail.slice(0, afterBlock);
|
|
258
|
+
cursor += afterBlock;
|
|
267
259
|
}
|
|
268
|
-
return result;
|
|
269
260
|
}
|
|
270
261
|
function artifactTranscriptSummary(attrs, persisted) {
|
|
271
262
|
const id = attrs['identifier'] ?? '';
|
|
@@ -295,8 +286,7 @@ function artifactTranscriptSummary(attrs, persisted) {
|
|
|
295
286
|
* @complexity O(n) in `content.length`.
|
|
296
287
|
*/
|
|
297
288
|
export function splitStreamingArtifact({ content }) {
|
|
298
|
-
const
|
|
299
|
-
const ranges = unclosedFenceStart !== null ? [...baseRanges, [unclosedFenceStart, content.length]] : baseRanges;
|
|
289
|
+
const ranges = completeContentSkipRanges({ content });
|
|
300
290
|
const open = findRealOpen(content, 0, ranges);
|
|
301
291
|
if (open === -1)
|
|
302
292
|
return { head: content, live: null };
|
|
@@ -162,34 +162,32 @@ function pathnameOnly(path) {
|
|
|
162
162
|
function srcsetCandidateUrls(srcset) {
|
|
163
163
|
const candidates = [];
|
|
164
164
|
let start = 0;
|
|
165
|
-
let
|
|
166
|
-
let dataUrlCandidate = false;
|
|
167
|
-
let sawWhitespaceAfterUrl = false;
|
|
165
|
+
let candidate = null;
|
|
168
166
|
for (let index = 0; index < srcset.length; index += 1) {
|
|
169
167
|
const char = srcset[index];
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
continue;
|
|
174
|
-
}
|
|
175
|
-
sawCandidate = true;
|
|
176
|
-
dataUrlCandidate = /^data:/i.test(srcset.slice(index));
|
|
177
|
-
}
|
|
178
|
-
if (/\s/.test(char)) {
|
|
179
|
-
sawWhitespaceAfterUrl = true;
|
|
168
|
+
const isSpace = /\s/.test(char);
|
|
169
|
+
if (candidate === null && (char === ',' || isSpace)) {
|
|
170
|
+
start = index + 1;
|
|
180
171
|
continue;
|
|
181
172
|
}
|
|
182
|
-
|
|
173
|
+
candidate ??= { isDataUrl: /^data:/i.test(srcset.slice(index)), sawWhitespaceAfterUrl: false };
|
|
174
|
+
if (isSpace) {
|
|
175
|
+
candidate.sawWhitespaceAfterUrl = true;
|
|
176
|
+
}
|
|
177
|
+
else if (endsSrcsetCandidate(candidate, char)) {
|
|
183
178
|
candidates.push(srcset.slice(start, index));
|
|
184
179
|
start = index + 1;
|
|
185
|
-
|
|
186
|
-
dataUrlCandidate = false;
|
|
187
|
-
sawWhitespaceAfterUrl = false;
|
|
180
|
+
candidate = null;
|
|
188
181
|
}
|
|
189
182
|
}
|
|
190
183
|
candidates.push(srcset.slice(start));
|
|
191
184
|
return candidates.map(firstUrlToken).filter(Boolean);
|
|
192
185
|
}
|
|
186
|
+
// A data: URL may itself contain commas, so for one a comma only ends the
|
|
187
|
+
// candidate once whitespace has separated the URL from its descriptor.
|
|
188
|
+
function endsSrcsetCandidate(candidate, char) {
|
|
189
|
+
return char === ',' && (!candidate.isDataUrl || candidate.sawWhitespaceAfterUrl);
|
|
190
|
+
}
|
|
193
191
|
function firstUrlToken(value) {
|
|
194
192
|
// `String.split()` on a regex separator always returns at least one
|
|
195
193
|
// element (even `''.split(/\s+/)` is `['']`), so `[0]` is never
|