@jini-ai/chat 0.6.6 → 0.6.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +4 -0
- package/dist/core/accepted-input.d.ts +54 -0
- package/dist/core/accepted-input.js +165 -0
- package/dist/core/agent-plugin-chat-state.d.ts +43 -0
- package/dist/core/agent-plugin-chat-state.js +48 -0
- package/dist/core/assistant-client/a2ui-action-poster.js +32 -28
- package/dist/core/assistant-client/messages.d.ts +1 -1
- package/dist/core/assistant-client/messages.js +2 -2
- package/dist/core/assistant-client/transport.d.ts +2 -2
- package/dist/core/assistant-client/transport.js +184 -97
- package/dist/core/assistant-content.d.ts +2 -1
- package/dist/core/assistant-content.js +9 -11
- package/dist/core/browser/page-actions.js +4 -2
- package/dist/core/composer-history.d.ts +16 -9
- package/dist/core/composer-history.js +28 -16
- package/dist/core/conversation-title.d.ts +3 -0
- package/dist/core/conversation-title.js +3 -0
- package/dist/core/durable-projection.js +20 -13
- package/dist/core/durable-runs/continuation.d.ts +4 -0
- package/dist/core/durable-runs/continuation.js +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.js +47 -31
- 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/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 +17 -8
- package/dist/react/hooks/assistant-chats.js +12 -5
- package/dist/react/hooks/composer-draft-cache.js +116 -118
- package/dist/react/hooks/composer-history-storage.js +5 -5
- package/dist/react/hooks/fab-position.js +7 -4
- package/dist/react/hooks/useAnsweredCards.d.ts +21 -4
- package/dist/react/hooks/useAnsweredCards.js +38 -11
- package/dist/react/hooks/useArtifactStream.js +28 -22
- package/dist/react/hooks/useComposer.d.ts +20 -0
- package/dist/react/hooks/useComposer.js +22 -9
- package/dist/react/hooks/useComposerHistory.hooks.d.ts +6 -1
- package/dist/react/hooks/useComposerHistory.hooks.js +39 -19
- package/dist/react/hooks/useComposerHistoryKeys.hooks.js +24 -14
- package/dist/react/hooks/useConversation.d.ts +7 -0
- package/dist/react/hooks/useConversation.js +23 -10
- package/dist/react/hooks/useExtEventGroups.js +39 -39
- package/dist/react/hooks/useQuestionForms.js +21 -26
- package/dist/react/hooks/useRunStream.js +10 -2
- package/dist/react/index.d.ts +7 -1
- package/dist/react/index.js +9 -0
- package/dist/react/message-blocks.d.ts +6 -4
- package/dist/react/message-blocks.js +94 -72
- package/dist/react/slots.d.ts +2 -2
- package/dist/react/tool-call-display.js +19 -17
- package/dist/server/durable-runs/recover.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 +12 -10
|
@@ -66,6 +66,18 @@ const stagedAttachments = new Map();
|
|
|
66
66
|
// attachments-only signature: `useComposer` persists the list and never sees a batch id, while
|
|
67
67
|
// `useChatPane` (the one caller that uploads in batches) records the batch here.
|
|
68
68
|
const stagedBatchIds = new Map();
|
|
69
|
+
/**
|
|
70
|
+
* Sets `key` in one of the in-memory tiers, evicting the oldest-inserted entry first if this would
|
|
71
|
+
* introduce a new key past {@link MAX_CACHED_CONVERSATION_DRAFTS}.
|
|
72
|
+
* @complexity Time/space: O(1) amortized.
|
|
73
|
+
*/
|
|
74
|
+
function setBounded(map, { key, value }) {
|
|
75
|
+
if (!map.has(key) && map.size >= MAX_CACHED_CONVERSATION_DRAFTS) {
|
|
76
|
+
// `size >= MAX_CACHED_CONVERSATION_DRAFTS > 0`, so there is always a first key to evict.
|
|
77
|
+
map.delete(map.keys().next().value);
|
|
78
|
+
}
|
|
79
|
+
map.set(key, value);
|
|
80
|
+
}
|
|
69
81
|
/**
|
|
70
82
|
* The origin's `localStorage`, or `null` when it cannot be used.
|
|
71
83
|
*
|
|
@@ -83,32 +95,29 @@ function storage() {
|
|
|
83
95
|
}
|
|
84
96
|
}
|
|
85
97
|
/**
|
|
86
|
-
* Reads and
|
|
98
|
+
* Reads the entry at `prefix + conversationId` and hands its parsed JSON to `decode`.
|
|
87
99
|
*
|
|
88
|
-
* Anything
|
|
89
|
-
*
|
|
90
|
-
*
|
|
100
|
+
* Anything `decode` rejects (returns `null` for) is treated as foreign or corrupt and deleted rather
|
|
101
|
+
* than returned — a stale format, another tool writing under a colliding key, or a truncated write
|
|
102
|
+
* must not surface as composer state.
|
|
91
103
|
*
|
|
92
|
-
* @returns The
|
|
104
|
+
* @returns The decoded value, or `null` when there is no entry, storage is unavailable, or the entry
|
|
93
105
|
* failed validation.
|
|
94
|
-
* @complexity Time/space: O(n) in the stored
|
|
106
|
+
* @complexity Time/space: O(n) in the stored entry's length (the JSON parse) plus `decode`.
|
|
95
107
|
*/
|
|
96
|
-
function
|
|
108
|
+
function readStoredEnvelope(prefix, { conversationId, decode }) {
|
|
97
109
|
const store = storage();
|
|
98
110
|
if (!store)
|
|
99
111
|
return null;
|
|
100
|
-
const key = `${
|
|
112
|
+
const key = `${prefix}${conversationId}`;
|
|
101
113
|
try {
|
|
102
114
|
const raw = store.getItem(key);
|
|
103
115
|
if (raw === null)
|
|
104
116
|
return null;
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
if (typeof draft !== 'string' || draft === '') {
|
|
117
|
+
const decoded = decode(JSON.parse(raw));
|
|
118
|
+
if (decoded === null)
|
|
108
119
|
store.removeItem(key);
|
|
109
|
-
|
|
110
|
-
}
|
|
111
|
-
return draft;
|
|
120
|
+
return decoded;
|
|
112
121
|
}
|
|
113
122
|
catch {
|
|
114
123
|
// A parse failure leaves the bad entry behind on purpose when `removeItem` is what threw —
|
|
@@ -122,6 +131,22 @@ function readStoredDraft(conversationId) {
|
|
|
122
131
|
return null;
|
|
123
132
|
}
|
|
124
133
|
}
|
|
134
|
+
/**
|
|
135
|
+
* Validates a parsed draft envelope.
|
|
136
|
+
* @returns Its draft, or `null` when it is not this module's envelope or holds a blank draft.
|
|
137
|
+
* @complexity Time/space: O(1).
|
|
138
|
+
*/
|
|
139
|
+
function decodeStoredDraft(parsed) {
|
|
140
|
+
const draft = parsed?.d;
|
|
141
|
+
return typeof draft === 'string' && draft !== '' ? draft : null;
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Reads and validates the stored draft for `conversationId`; see {@link readStoredEnvelope}.
|
|
145
|
+
* @complexity Time/space: O(n) in the stored draft's length (the JSON parse).
|
|
146
|
+
*/
|
|
147
|
+
function readStoredDraft(conversationId) {
|
|
148
|
+
return readStoredEnvelope(COMPOSER_DRAFT_STORAGE_PREFIX, { conversationId, decode: decodeStoredDraft });
|
|
149
|
+
}
|
|
125
150
|
/**
|
|
126
151
|
* Deletes every stored draft past the cap, oldest write first, so an operator who abandons drafts
|
|
127
152
|
* in many conversations cannot fill the origin's quota and silently break persistence for the
|
|
@@ -138,21 +163,7 @@ function pruneStored(prefix, keep) {
|
|
|
138
163
|
if (!store)
|
|
139
164
|
return;
|
|
140
165
|
try {
|
|
141
|
-
const entries =
|
|
142
|
-
for (let i = 0; i < store.length; i += 1) {
|
|
143
|
-
const key = store.key(i);
|
|
144
|
-
if (key === null || !key.startsWith(prefix))
|
|
145
|
-
continue;
|
|
146
|
-
let at = 0;
|
|
147
|
-
try {
|
|
148
|
-
const parsed = JSON.parse(store.getItem(key) ?? '');
|
|
149
|
-
at = typeof parsed?.t === 'number' ? parsed.t : 0;
|
|
150
|
-
}
|
|
151
|
-
catch {
|
|
152
|
-
at = 0;
|
|
153
|
-
}
|
|
154
|
-
entries.push({ key, at });
|
|
155
|
-
}
|
|
166
|
+
const entries = storedEntries(store, { prefix });
|
|
156
167
|
if (entries.length <= keep)
|
|
157
168
|
return;
|
|
158
169
|
entries.sort((a, b) => a.at - b.at);
|
|
@@ -164,44 +175,78 @@ function pruneStored(prefix, keep) {
|
|
|
164
175
|
}
|
|
165
176
|
}
|
|
166
177
|
/**
|
|
167
|
-
*
|
|
178
|
+
* Every stored key under `prefix`, with its envelope's write timestamp (`0` when the envelope does
|
|
179
|
+
* not parse or carries none).
|
|
180
|
+
* @complexity Time/space: O(n) in the number of stored keys.
|
|
181
|
+
*/
|
|
182
|
+
function storedEntries(store, { prefix }) {
|
|
183
|
+
const entries = [];
|
|
184
|
+
for (let i = 0; i < store.length; i += 1) {
|
|
185
|
+
const key = store.key(i);
|
|
186
|
+
if (key?.startsWith(prefix))
|
|
187
|
+
entries.push({ key, at: storedTimestamp(store, { key }) });
|
|
188
|
+
}
|
|
189
|
+
return entries;
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* The write timestamp of the envelope at `key`, or `0` when it cannot be read or parsed.
|
|
193
|
+
* @complexity Time/space: O(n) in the stored entry's length.
|
|
194
|
+
*/
|
|
195
|
+
function storedTimestamp(store, { key }) {
|
|
196
|
+
try {
|
|
197
|
+
// `String(null)` is `'null'`, which parses to `null` and so reads as timestamp `0` — the same
|
|
198
|
+
// answer a missing entry got before, without a branch for a key `store.key` just returned.
|
|
199
|
+
const parsed = JSON.parse(String(store.getItem(key)));
|
|
200
|
+
return typeof parsed?.t === 'number' ? parsed.t : 0;
|
|
201
|
+
}
|
|
202
|
+
catch {
|
|
203
|
+
return 0;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Writes or removes the stored entry at `prefix + conversationId`.
|
|
168
208
|
*
|
|
169
|
-
* @param
|
|
170
|
-
* has already accepted the
|
|
171
|
-
* @complexity Time O(n) in the
|
|
172
|
-
* when this introduces a new key at the cap. Space O(n) in the
|
|
209
|
+
* @param envelope `null` removes the entry. A quota rejection is swallowed: the in-memory tier
|
|
210
|
+
* has already accepted the value, so the composer keeps working and only reload-survival is lost.
|
|
211
|
+
* @complexity Time O(n) in the envelope's size, plus a prune (see {@link pruneStored}) only
|
|
212
|
+
* when this introduces a new key at the cap. Space O(n) in the envelope's size.
|
|
173
213
|
*/
|
|
174
|
-
function
|
|
214
|
+
function writeStoredEnvelope(prefix, { conversationId, envelope }) {
|
|
175
215
|
const store = storage();
|
|
176
216
|
if (!store)
|
|
177
217
|
return;
|
|
178
|
-
const key = `${
|
|
218
|
+
const key = `${prefix}${conversationId}`;
|
|
179
219
|
try {
|
|
180
|
-
if (
|
|
220
|
+
if (envelope === null) {
|
|
181
221
|
store.removeItem(key);
|
|
182
222
|
return;
|
|
183
223
|
}
|
|
184
224
|
if (store.getItem(key) === null)
|
|
185
|
-
pruneStored(
|
|
186
|
-
const envelope = { v: 1, t: Date.now(), d: draft };
|
|
225
|
+
pruneStored(prefix, MAX_CACHED_CONVERSATION_DRAFTS - 1);
|
|
187
226
|
store.setItem(key, JSON.stringify(envelope));
|
|
188
227
|
}
|
|
189
228
|
catch {
|
|
190
229
|
/* blocked, full, or unusable storage — memory-only is the documented degraded mode */
|
|
230
|
+
/* (on a removal: blocked storage; the in-memory tier is already correct) */
|
|
191
231
|
}
|
|
192
232
|
}
|
|
233
|
+
/**
|
|
234
|
+
* Writes or removes the stored copy for `conversationId`.
|
|
235
|
+
*
|
|
236
|
+
* @param draft A blank draft removes the entry; see {@link writeStoredEnvelope}.
|
|
237
|
+
* @complexity Time O(n) in the draft's length; see {@link writeStoredEnvelope}.
|
|
238
|
+
*/
|
|
239
|
+
function writeStoredDraft(conversationId, draft) {
|
|
240
|
+
const envelope = draft === '' ? null : { v: 1, t: Date.now(), d: draft };
|
|
241
|
+
writeStoredEnvelope(COMPOSER_DRAFT_STORAGE_PREFIX, { conversationId, envelope });
|
|
242
|
+
}
|
|
193
243
|
/**
|
|
194
244
|
* Records `draft` in the in-memory tier, evicting the oldest-inserted conversation first if this
|
|
195
245
|
* would introduce a new entry past {@link MAX_CACHED_CONVERSATION_DRAFTS}.
|
|
196
246
|
* @complexity Time/space: O(1) amortized.
|
|
197
247
|
*/
|
|
198
248
|
function rememberInMemory(conversationId, draft) {
|
|
199
|
-
|
|
200
|
-
const oldest = drafts.keys().next().value;
|
|
201
|
-
if (oldest !== undefined)
|
|
202
|
-
drafts.delete(oldest);
|
|
203
|
-
}
|
|
204
|
-
drafts.set(conversationId, draft);
|
|
249
|
+
setBounded(drafts, { key: conversationId, value: draft });
|
|
205
250
|
}
|
|
206
251
|
/**
|
|
207
252
|
* Reads the cached draft for `conversationId`, in memory first and storage second.
|
|
@@ -239,13 +284,8 @@ export function readCachedDraft({ conversationId }) {
|
|
|
239
284
|
export function writeCachedDraft({ conversationId, draft }) {
|
|
240
285
|
if (!conversationId)
|
|
241
286
|
return;
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
writeStoredDraft(conversationId, '');
|
|
245
|
-
return;
|
|
246
|
-
}
|
|
247
|
-
rememberInMemory(conversationId, draft);
|
|
248
|
-
writeStoredDraft(conversationId, draft);
|
|
287
|
+
rememberCachedDraft({ conversationId: conversationId, draft: draft });
|
|
288
|
+
writeStoredDraft(conversationId, draft.trim() === '' ? '' : draft);
|
|
249
289
|
}
|
|
250
290
|
/**
|
|
251
291
|
* Stores `draft` for `conversationId` in the in-memory tier only, leaving `localStorage` alone.
|
|
@@ -299,11 +339,11 @@ function isChatAttachment(candidate) {
|
|
|
299
339
|
return false;
|
|
300
340
|
if (kind !== 'image' && kind !== 'file')
|
|
301
341
|
return false;
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
return
|
|
342
|
+
return isOptionalNumber(size) && isOptionalNumber(order);
|
|
343
|
+
}
|
|
344
|
+
/** @complexity Time/space: O(1). */
|
|
345
|
+
function isOptionalNumber(value) {
|
|
346
|
+
return value === undefined || typeof value === 'number';
|
|
307
347
|
}
|
|
308
348
|
/**
|
|
309
349
|
* Reads the stored attachment references (and their upload batch) for `conversationId`.
|
|
@@ -317,37 +357,21 @@ function isChatAttachment(candidate) {
|
|
|
317
357
|
* @complexity Time/space: O(n) in the number of stored references.
|
|
318
358
|
*/
|
|
319
359
|
function readStoredAttachments(conversationId) {
|
|
320
|
-
|
|
321
|
-
|
|
360
|
+
return readStoredEnvelope(COMPOSER_ATTACHMENTS_STORAGE_PREFIX, { conversationId, decode: decodeStoredAttachments });
|
|
361
|
+
}
|
|
362
|
+
/**
|
|
363
|
+
* Validates a parsed attachments envelope; see {@link readStoredAttachments} for the rules.
|
|
364
|
+
* @complexity Time/space: O(n) in the number of stored references.
|
|
365
|
+
*/
|
|
366
|
+
function decodeStoredAttachments(parsed) {
|
|
367
|
+
const list = parsed?.a;
|
|
368
|
+
const batchId = parsed?.b;
|
|
369
|
+
if (!Array.isArray(list))
|
|
322
370
|
return null;
|
|
323
|
-
const
|
|
324
|
-
|
|
325
|
-
const raw = store.getItem(key);
|
|
326
|
-
if (raw === null)
|
|
327
|
-
return null;
|
|
328
|
-
const parsed = JSON.parse(raw);
|
|
329
|
-
const list = parsed?.a;
|
|
330
|
-
const batchId = parsed?.b;
|
|
331
|
-
if (!Array.isArray(list)) {
|
|
332
|
-
store.removeItem(key);
|
|
333
|
-
return null;
|
|
334
|
-
}
|
|
335
|
-
const valid = list.filter(isChatAttachment);
|
|
336
|
-
if (valid.length === 0) {
|
|
337
|
-
store.removeItem(key);
|
|
338
|
-
return null;
|
|
339
|
-
}
|
|
340
|
-
return { attachments: valid, batchId: typeof batchId === 'string' && batchId !== '' ? batchId : null };
|
|
341
|
-
}
|
|
342
|
-
catch {
|
|
343
|
-
try {
|
|
344
|
-
store.removeItem(key);
|
|
345
|
-
}
|
|
346
|
-
catch {
|
|
347
|
-
/* storage is unusable; the in-memory tier still serves this conversation */
|
|
348
|
-
}
|
|
371
|
+
const valid = list.filter(isChatAttachment);
|
|
372
|
+
if (valid.length === 0)
|
|
349
373
|
return null;
|
|
350
|
-
}
|
|
374
|
+
return { attachments: valid, batchId: typeof batchId === 'string' && batchId !== '' ? batchId : null };
|
|
351
375
|
}
|
|
352
376
|
/**
|
|
353
377
|
* Reads the cached attachment references for `conversationId`, in memory first, storage second.
|
|
@@ -407,12 +431,7 @@ export function readCachedAttachmentBatchId({ conversationId }, _options = {}) {
|
|
|
407
431
|
export function writeCachedAttachmentBatchId({ conversationId, batchId }, _options = {}) {
|
|
408
432
|
if (!conversationId)
|
|
409
433
|
return;
|
|
410
|
-
|
|
411
|
-
const oldest = stagedBatchIds.keys().next().value;
|
|
412
|
-
if (oldest !== undefined)
|
|
413
|
-
stagedBatchIds.delete(oldest);
|
|
414
|
-
}
|
|
415
|
-
stagedBatchIds.set(conversationId, batchId);
|
|
434
|
+
setBounded(stagedBatchIds, { key: conversationId, value: batchId });
|
|
416
435
|
}
|
|
417
436
|
/**
|
|
418
437
|
* Stores `attachments` for `conversationId` in both tiers.
|
|
@@ -424,38 +443,17 @@ export function writeCachedAttachmentBatchId({ conversationId, batchId }, _optio
|
|
|
424
443
|
export function writeCachedAttachments({ conversationId, attachments }) {
|
|
425
444
|
if (!conversationId)
|
|
426
445
|
return;
|
|
427
|
-
const key = `${COMPOSER_ATTACHMENTS_STORAGE_PREFIX}${conversationId}`;
|
|
428
|
-
const store = storage();
|
|
429
446
|
if (attachments.length === 0) {
|
|
430
447
|
stagedAttachments.delete(conversationId);
|
|
431
448
|
// A sent or cleared turn's batch is spent; the next upload starts a fresh one.
|
|
432
449
|
stagedBatchIds.delete(conversationId);
|
|
433
|
-
|
|
434
|
-
store?.removeItem(key);
|
|
435
|
-
}
|
|
436
|
-
catch {
|
|
437
|
-
/* blocked storage; the in-memory tier is already correct */
|
|
438
|
-
}
|
|
439
|
-
return;
|
|
440
|
-
}
|
|
441
|
-
if (!stagedAttachments.has(conversationId) && stagedAttachments.size >= MAX_CACHED_CONVERSATION_DRAFTS) {
|
|
442
|
-
const oldest = stagedAttachments.keys().next().value;
|
|
443
|
-
if (oldest !== undefined)
|
|
444
|
-
stagedAttachments.delete(oldest);
|
|
445
|
-
}
|
|
446
|
-
stagedAttachments.set(conversationId, attachments);
|
|
447
|
-
if (!store)
|
|
450
|
+
writeStoredEnvelope(COMPOSER_ATTACHMENTS_STORAGE_PREFIX, { conversationId, envelope: null });
|
|
448
451
|
return;
|
|
449
|
-
try {
|
|
450
|
-
if (store.getItem(key) === null)
|
|
451
|
-
pruneStored(COMPOSER_ATTACHMENTS_STORAGE_PREFIX, MAX_CACHED_CONVERSATION_DRAFTS - 1);
|
|
452
|
-
const batchId = stagedBatchIds.get(conversationId);
|
|
453
|
-
const envelope = { v: 1, t: Date.now(), a: attachments, ...(batchId === undefined ? {} : { b: batchId }) };
|
|
454
|
-
store.setItem(key, JSON.stringify(envelope));
|
|
455
|
-
}
|
|
456
|
-
catch {
|
|
457
|
-
/* blocked, full, or unusable storage — memory-only is the documented degraded mode */
|
|
458
452
|
}
|
|
453
|
+
setBounded(stagedAttachments, { key: conversationId, value: attachments });
|
|
454
|
+
const batchId = stagedBatchIds.get(conversationId);
|
|
455
|
+
const envelope = { v: 1, t: Date.now(), a: attachments, ...(batchId === undefined ? {} : { b: batchId }) };
|
|
456
|
+
writeStoredEnvelope(COMPOSER_ATTACHMENTS_STORAGE_PREFIX, { conversationId, envelope });
|
|
459
457
|
}
|
|
460
458
|
/**
|
|
461
459
|
* Test-only: empties the cache so test cases cannot leak drafts into each other.
|
|
@@ -7,13 +7,13 @@ export function createBrowserComposerHistoryStorage(_required, { storage, prefix
|
|
|
7
7
|
return storage ?? globalThis.localStorage;
|
|
8
8
|
}
|
|
9
9
|
return {
|
|
10
|
-
async read({ scope },
|
|
10
|
+
async read({ scope }, { cap = 100 } = {}) {
|
|
11
11
|
try {
|
|
12
12
|
const raw = resolveStorage()?.getItem(prefix + encodeURIComponent(scope));
|
|
13
13
|
if (raw) {
|
|
14
14
|
const parsed = JSON.parse(raw);
|
|
15
15
|
if (Array.isArray(parsed)) {
|
|
16
|
-
const entries = normalizeComposerHistory({ entries: parsed.filter((value) => typeof value === 'string') });
|
|
16
|
+
const entries = normalizeComposerHistory({ entries: parsed.filter((value) => typeof value === 'string') }, { cap });
|
|
17
17
|
memory.set(scope, entries);
|
|
18
18
|
return entries;
|
|
19
19
|
}
|
|
@@ -22,10 +22,10 @@ export function createBrowserComposerHistoryStorage(_required, { storage, prefix
|
|
|
22
22
|
catch {
|
|
23
23
|
// Storage denial/corruption must not disable draft editing or in-memory recall.
|
|
24
24
|
}
|
|
25
|
-
return memory.get(scope) ?? [];
|
|
25
|
+
return normalizeComposerHistory({ entries: memory.get(scope) ?? [] }, { cap });
|
|
26
26
|
},
|
|
27
|
-
async write({ scope, entries },
|
|
28
|
-
const normalized = normalizeComposerHistory({ entries });
|
|
27
|
+
async write({ scope, entries }, { cap = 100 } = {}) {
|
|
28
|
+
const normalized = normalizeComposerHistory({ entries }, { cap });
|
|
29
29
|
memory.set(scope, normalized);
|
|
30
30
|
try {
|
|
31
31
|
resolveStorage()?.setItem(prefix + encodeURIComponent(scope), JSON.stringify(normalized));
|
|
@@ -116,6 +116,13 @@ export function useFabPosition(options, _optional = {}) {
|
|
|
116
116
|
const draggingRef = useRef(false);
|
|
117
117
|
const didDragRef = useRef(false);
|
|
118
118
|
const liveRef = useRef(null);
|
|
119
|
+
/**
|
|
120
|
+
* Set by `onPointerDown` immediately BEFORE it attaches the document `pointermove`/`pointerup`/
|
|
121
|
+
* `pointercancel` listeners, and cleared by `handlePointerUp` in the same synchronous step that
|
|
122
|
+
* detaches them — so whenever either handler below can run, a drag start is recorded. That
|
|
123
|
+
* invariant is why the handlers read it with a cast rather than an `if (!start) return` guard: the
|
|
124
|
+
* guard that used to sit there could never be taken.
|
|
125
|
+
*/
|
|
119
126
|
const dragStart = useRef(null);
|
|
120
127
|
/** Bumped to force a re-render on every pointermove and on resize — `liveRef`/`window.innerWidth`
|
|
121
128
|
* are read directly in `style`'s `useMemo` below, neither of which React tracks as a reactive
|
|
@@ -141,8 +148,6 @@ export function useFabPosition(options, _optional = {}) {
|
|
|
141
148
|
// never depend on state that would force a re-subscribe mid-gesture.
|
|
142
149
|
const handlePointerMove = useCallback((e) => {
|
|
143
150
|
const start = dragStart.current;
|
|
144
|
-
if (!start)
|
|
145
|
-
return;
|
|
146
151
|
const dx = e.clientX - start.startX;
|
|
147
152
|
const dy = e.clientY - start.startY;
|
|
148
153
|
if (!draggingRef.current && Math.hypot(dx, dy) > DRAG_THRESHOLD_PX) {
|
|
@@ -159,8 +164,6 @@ export function useFabPosition(options, _optional = {}) {
|
|
|
159
164
|
}, []);
|
|
160
165
|
const handlePointerUp = useCallback((e) => {
|
|
161
166
|
const start = dragStart.current;
|
|
162
|
-
if (!start)
|
|
163
|
-
return;
|
|
164
167
|
e.target.releasePointerCapture?.(start.pointerId);
|
|
165
168
|
document.removeEventListener("pointermove", handlePointerMove);
|
|
166
169
|
document.removeEventListener("pointerup", handlePointerUp);
|
|
@@ -1,20 +1,37 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type ChatRunStatus } from '../../core/messages.js';
|
|
2
|
+
import type { ExtEventGroup } from './useExtEventGroups.js';
|
|
3
|
+
import type { AgentEvent, IsAnsweredCard } from '../../core/index.js';
|
|
2
4
|
export interface AnsweredCards {
|
|
5
|
+
/** Live rendering state; a terminal run overrides a stale streaming prop. */
|
|
6
|
+
runStreaming: boolean;
|
|
3
7
|
/** Whether the slot's card was answered. */
|
|
4
8
|
isAnswered(slot: string): boolean;
|
|
5
9
|
/** Records the slot's card as answered. Idempotent. */
|
|
6
|
-
markAnswered(slot: string): void;
|
|
10
|
+
markAnswered(slot: string, answerText?: string): void;
|
|
11
|
+
/** The receipt text takes precedence over the transient delivery text. */
|
|
12
|
+
answerText(slot: string): string | undefined;
|
|
13
|
+
/** Whether a user_message event already renders this answer. */
|
|
14
|
+
isRecorded(slot: string): boolean;
|
|
7
15
|
/** The same answer as `isAnswered`, keyed by an event — for `deriveRunActivity`. Stable while nothing new is answered. */
|
|
8
16
|
isAnsweredCard: IsAnsweredCard;
|
|
9
17
|
}
|
|
10
18
|
/**
|
|
11
19
|
* @param slotOf - Maps an ext event to its render slot (`extEventSlot`).
|
|
12
|
-
* @
|
|
20
|
+
* @param events - Accepted-input-projected events, including persisted or optimistic answer receipts.
|
|
21
|
+
* @param runStreaming - Host streaming state, constrained by the message's runStatus.
|
|
22
|
+
* @param runStatus - Persisted run lifecycle, including terminal status on reload.
|
|
23
|
+
* @param groups - Groups carrying their original tool id; equal answer text is never identity.
|
|
24
|
+
* @returns Slot lookups and an idempotent delivery recorder; only that recorder changes React state.
|
|
25
|
+
* @complexity O(events + groups) projection, O(1) lookups; O(events + answered cards) space.
|
|
13
26
|
*/
|
|
14
|
-
export declare function useAnsweredCards({ slotOf }: {
|
|
27
|
+
export declare function useAnsweredCards({ slotOf, events, groups, runStreaming, runStatus }: {
|
|
28
|
+
runStreaming: boolean;
|
|
29
|
+
runStatus: ChatRunStatus | undefined;
|
|
15
30
|
slotOf: (card: {
|
|
16
31
|
name: string;
|
|
17
32
|
data: unknown;
|
|
18
33
|
}) => string;
|
|
34
|
+
events?: readonly AgentEvent[];
|
|
35
|
+
groups?: readonly ExtEventGroup[];
|
|
19
36
|
}): AnsweredCards;
|
|
20
37
|
//# sourceMappingURL=useAnsweredCards.d.ts.map
|
|
@@ -3,21 +3,48 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Which of a message's cards the person has already answered, by ext-event slot. A held-open
|
|
5
5
|
* question's tool call stays open after the click while the confirmed work runs, so the event
|
|
6
|
-
* stream alone still
|
|
7
|
-
*
|
|
6
|
+
* stream alone can still read "waiting for an answer"; persisted receipts or the card delivery report
|
|
7
|
+
* clear the activity line and countdown. The receipt owns the bubble once it arrives.
|
|
8
8
|
*/
|
|
9
|
-
import { useCallback, useState } from 'react';
|
|
9
|
+
import { useCallback, useMemo, useState } from 'react';
|
|
10
|
+
import { isTerminalRunStatus } from '../../core/messages.js';
|
|
11
|
+
import { acceptedInputEvent } from '../../core/accepted-input.js';
|
|
10
12
|
/**
|
|
11
13
|
* @param slotOf - Maps an ext event to its render slot (`extEventSlot`).
|
|
12
|
-
* @
|
|
14
|
+
* @param events - Accepted-input-projected events, including persisted or optimistic answer receipts.
|
|
15
|
+
* @param runStreaming - Host streaming state, constrained by the message's runStatus.
|
|
16
|
+
* @param runStatus - Persisted run lifecycle, including terminal status on reload.
|
|
17
|
+
* @param groups - Groups carrying their original tool id; equal answer text is never identity.
|
|
18
|
+
* @returns Slot lookups and an idempotent delivery recorder; only that recorder changes React state.
|
|
19
|
+
* @complexity O(events + groups) projection, O(1) lookups; O(events + answered cards) space.
|
|
13
20
|
*/
|
|
14
|
-
export function useAnsweredCards({ slotOf }) {
|
|
15
|
-
const [answered, setAnswered] = useState(() => new
|
|
16
|
-
|
|
17
|
-
|
|
21
|
+
export function useAnsweredCards({ slotOf, events = [], groups = [], runStreaming, runStatus }) {
|
|
22
|
+
const [answered, setAnswered] = useState(() => new Map());
|
|
23
|
+
// MOVE: Match by tool identity so receipt replay hides only its own card, even for equal answers.
|
|
24
|
+
const recorded = useMemo(() => {
|
|
25
|
+
const receipts = new Map();
|
|
26
|
+
for (const event of events) {
|
|
27
|
+
const receipt = acceptedInputEvent({ event });
|
|
28
|
+
if (receipt?.data.toolUseId)
|
|
29
|
+
receipts.set(receipt.data.toolUseId, receipt.data.text);
|
|
30
|
+
}
|
|
31
|
+
const slots = new Map();
|
|
32
|
+
for (const group of groups) {
|
|
33
|
+
const text = group.call?.id === undefined ? undefined : receipts.get(group.call.id);
|
|
34
|
+
if (text !== undefined)
|
|
35
|
+
slots.set(group.slot, text);
|
|
36
|
+
}
|
|
37
|
+
return slots;
|
|
38
|
+
}, [events, groups]);
|
|
39
|
+
const markAnswered = useCallback((slot, answerText) => {
|
|
40
|
+
setAnswered((current) => (current.has(slot) ? current : new Map(current).set(slot, answerText)));
|
|
18
41
|
}, []);
|
|
19
|
-
const isAnswered = useCallback((slot) => answered.has(slot), [answered]);
|
|
20
|
-
const
|
|
21
|
-
|
|
42
|
+
const isAnswered = useCallback((slot) => recorded.has(slot) || answered.has(slot), [answered, recorded]);
|
|
43
|
+
const answerText = useCallback((slot) => recorded.get(slot) ?? answered.get(slot), [answered, recorded]);
|
|
44
|
+
const isRecorded = useCallback((slot) => recorded.has(slot), [recorded]);
|
|
45
|
+
const isAnsweredCard = useCallback((card) => isAnswered(slotOf(card)), [isAnswered, slotOf]);
|
|
46
|
+
// MOVE: Finished runs must keep orphaned questions closed even when the host still reports streaming.
|
|
47
|
+
return { isAnswered, markAnswered, isAnsweredCard, answerText, isRecorded,
|
|
48
|
+
runStreaming: runStreaming && runStatus !== undefined && !isTerminalRunStatus({ status: runStatus }) };
|
|
22
49
|
}
|
|
23
50
|
//# sourceMappingURL=useAnsweredCards.js.map
|
|
@@ -14,12 +14,33 @@
|
|
|
14
14
|
*/
|
|
15
15
|
import { useMemo } from 'react';
|
|
16
16
|
import { parseArtifacts, splitStreamingArtifact } from '../../core/index.js';
|
|
17
|
-
function
|
|
18
|
-
|
|
17
|
+
function toItem(base, { registry }) {
|
|
18
|
+
const file = { name: base.identifier || base.title || 'artifact', kind: base.artifactType, content: base.content };
|
|
19
|
+
return { ...base, file, match: registry?.resolve({ file }) ?? null };
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Completed artifacts in `head`, oldest first. Only `artifact:start` (for the
|
|
23
|
+
* metadata) and `artifact:end` (whose `fullContent` is the whole body) are
|
|
24
|
+
* needed — `artifact:chunk` deltas are already folded into `fullContent`.
|
|
25
|
+
* `createArtifactParser` never nests and always emits an `artifact:end` with
|
|
26
|
+
* the same identifier right after its `artifact:start`'s chunks, so `meta` is
|
|
27
|
+
* always set when an end arrives.
|
|
28
|
+
*/
|
|
29
|
+
function collectCompleted(head, { registry }) {
|
|
30
|
+
const items = [];
|
|
31
|
+
let meta = null;
|
|
32
|
+
for (const ev of parseArtifacts({ content: head })) {
|
|
33
|
+
if (ev.type === 'artifact:start') {
|
|
34
|
+
meta = { identifier: ev.identifier, artifactType: ev.artifactType, title: ev.title };
|
|
35
|
+
}
|
|
36
|
+
else if (ev.type === 'artifact:end') {
|
|
37
|
+
items.push(toItem({ ...meta, content: ev.fullContent, status: 'complete' }, { registry }));
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return items;
|
|
19
41
|
}
|
|
20
42
|
export function useArtifactStream({ content }, { registry } = {}) {
|
|
21
43
|
return useMemo(() => {
|
|
22
|
-
const items = [];
|
|
23
44
|
// `splitStreamingArtifact` is the authority on "is there a trailing,
|
|
24
45
|
// still-open artifact": split first so `parseArtifacts` (a one-shot
|
|
25
46
|
// feed+flush that synthesizes an `artifact:end` for whatever is still
|
|
@@ -29,27 +50,12 @@ export function useArtifactStream({ content }, { registry } = {}) {
|
|
|
29
50
|
// once (wrongly) as "complete" via the synthetic flush, and again (correctly)
|
|
30
51
|
// as "streaming" below.
|
|
31
52
|
const { head, live } = splitStreamingArtifact({ content: content });
|
|
32
|
-
|
|
33
|
-
for (const ev of parseArtifacts({ content: head })) {
|
|
34
|
-
if (ev.type === 'artifact:start') {
|
|
35
|
-
current = { identifier: ev.identifier, artifactType: ev.artifactType, title: ev.title, content: '' };
|
|
36
|
-
}
|
|
37
|
-
else if (ev.type === 'artifact:chunk' && current && current.identifier === ev.identifier) {
|
|
38
|
-
current.content += ev.delta;
|
|
39
|
-
}
|
|
40
|
-
else if (ev.type === 'artifact:end' && current && current.identifier === ev.identifier) {
|
|
41
|
-
const finished = { ...current, content: ev.fullContent, status: 'complete' };
|
|
42
|
-
items.push({ ...finished, file: toFile(finished), match: registry?.resolve({ file: toFile(finished) }) ?? null });
|
|
43
|
-
current = null;
|
|
44
|
-
}
|
|
45
|
-
}
|
|
53
|
+
const items = collectCompleted(head, { registry });
|
|
46
54
|
// A trailing artifact whose close tag hasn't streamed in yet — surfaced
|
|
47
55
|
// separately so a live preview can render mid-generation.
|
|
48
|
-
if (live)
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
}
|
|
52
|
-
return { items, current: items.length > 0 ? items[items.length - 1] : null };
|
|
56
|
+
if (live)
|
|
57
|
+
items.push(toItem({ ...live, status: 'streaming' }, { registry }));
|
|
58
|
+
return { items, current: items.at(-1) ?? null };
|
|
53
59
|
}, [content, registry]);
|
|
54
60
|
}
|
|
55
61
|
//# sourceMappingURL=useArtifactStream.js.map
|
|
@@ -13,6 +13,8 @@ export interface UseComposerOptions {
|
|
|
13
13
|
/** Scopes recent prompts across conversations. Hosts with multiple users must supply an id. */
|
|
14
14
|
historyScope?: string;
|
|
15
15
|
historyStorage?: ComposerHistoryStoragePort;
|
|
16
|
+
/** Recall retention; defaults to 100, with existing duplicate collapse. */
|
|
17
|
+
historyCap?: number;
|
|
16
18
|
initialAgent?: AgentSelection;
|
|
17
19
|
project?: ProjectContextValue;
|
|
18
20
|
composerSlots?: ComposerSlots;
|
|
@@ -41,7 +43,25 @@ export interface UseComposerOptions {
|
|
|
41
43
|
* Captured in a ref, so an inline arrow is safe and will not re-run the restore on every render.
|
|
42
44
|
*/
|
|
43
45
|
validateAttachments?: (attachments: readonly ChatAttachment[]) => Promise<readonly ChatAttachment[]>;
|
|
46
|
+
/**
|
|
47
|
+
* Where the page-exit signals that flush a pending draft write come from (`pagehide` on the
|
|
48
|
+
* window, `visibilitychange` to hidden on the document). Defaults to the browser's own
|
|
49
|
+
* {@link browserPageLifecycle}; `null` means there is no page to leave (a non-DOM renderer runs
|
|
50
|
+
* effects without `window`), so only unmount, send and switch flush. Captured once at mount.
|
|
51
|
+
*/
|
|
52
|
+
pageLifecycle?: ComposerPageLifecycle | null;
|
|
53
|
+
}
|
|
54
|
+
/** The page-exit event sources {@link useComposer} listens to, injectable so a host or test can supply its own. */
|
|
55
|
+
export interface ComposerPageLifecycle {
|
|
56
|
+
readonly window: Pick<Window, 'addEventListener' | 'removeEventListener'>;
|
|
57
|
+
readonly document: Pick<Document, 'addEventListener' | 'removeEventListener' | 'visibilityState'>;
|
|
44
58
|
}
|
|
59
|
+
/**
|
|
60
|
+
* The real window/document pair, or `null` when this runtime has no `window` — a non-DOM React
|
|
61
|
+
* renderer still runs effects, and subscribing there would throw.
|
|
62
|
+
* @complexity Time/space: O(1).
|
|
63
|
+
*/
|
|
64
|
+
export declare function browserPageLifecycle(): ComposerPageLifecycle | null;
|
|
45
65
|
export interface MentionPopoverState {
|
|
46
66
|
open: boolean;
|
|
47
67
|
query: string;
|