@jini-ai/chat 0.6.5 → 0.6.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/dist/core/accepted-input.d.ts +54 -0
- package/dist/core/accepted-input.js +165 -0
- package/dist/core/agent-plugin-chat-state.d.ts +43 -0
- package/dist/core/agent-plugin-chat-state.js +48 -0
- package/dist/core/assistant-client/a2ui-action-poster.js +32 -28
- package/dist/core/assistant-client/messages.d.ts +1 -1
- package/dist/core/assistant-client/messages.js +2 -2
- package/dist/core/assistant-client/transport.d.ts +2 -2
- package/dist/core/assistant-client/transport.js +184 -97
- package/dist/core/assistant-content.d.ts +2 -1
- package/dist/core/assistant-content.js +9 -11
- package/dist/core/browser/page-actions.js +4 -2
- package/dist/core/composer-history.d.ts +16 -9
- package/dist/core/composer-history.js +28 -16
- package/dist/core/conversation-title.d.ts +3 -0
- package/dist/core/conversation-title.js +3 -0
- package/dist/core/durable-projection.js +20 -13
- package/dist/core/durable-runs/continuation.d.ts +4 -0
- package/dist/core/durable-runs/continuation.js +35 -6
- package/dist/core/durable-runs/ledger-contracts.d.ts +2 -1
- package/dist/core/durable-runs/policies.d.ts +1 -0
- package/dist/core/durable-runs/policies.js +1 -0
- package/dist/core/durable-runs/ports.d.ts +6 -2
- package/dist/core/index.d.ts +2 -0
- package/dist/core/index.js +2 -0
- package/dist/core/messages.d.ts +2 -0
- package/dist/core/partial-json.d.ts +6 -7
- package/dist/core/partial-json.js +81 -47
- package/dist/core/persistence/title.js +4 -2
- package/dist/core/question-answer.d.ts +23 -0
- package/dist/core/question-answer.js +66 -0
- package/dist/core/question-form/format.d.ts +6 -6
- package/dist/core/question-form/format.js +16 -17
- package/dist/core/question-form/normalize.d.ts +18 -0
- package/dist/core/question-form/normalize.js +112 -122
- package/dist/core/question-form/scan.js +141 -203
- package/dist/core/run-activity.js +62 -49
- package/dist/core/run-events/entry.d.ts +10 -1
- package/dist/core/run-events/entry.js +117 -87
- package/dist/core/todos.d.ts +1 -1
- package/dist/core/todos.js +60 -85
- package/dist/core/tool-events.js +14 -12
- package/dist/core/transcript.js +62 -16
- package/dist/core/turn-usage.d.ts +2 -1
- package/dist/core/turn-usage.js +56 -27
- package/dist/core/user-text-redaction.js +3 -0
- package/dist/core/util/manifest.js +77 -82
- package/dist/core/util/markdown-context.js +51 -53
- package/dist/core/util/parser.js +136 -144
- package/dist/core/util/pointer.js +16 -16
- package/dist/core/util/strip.js +57 -67
- package/dist/core/util/validate.js +15 -17
- package/dist/embed-widget/browser/session-store.js +21 -12
- package/dist/embed-widget/transports/fetch-sse.js +108 -95
- package/dist/embed-widget/vanilla/index.js +81 -70
- package/dist/react/agent-bridge/frontend-session-bridge.js +80 -52
- package/dist/react/components/AttachmentPreviewModal.js +25 -1
- package/dist/react/components/ChatSettingsDialog.d.ts +13 -0
- package/dist/react/components/ChatSettingsDialog.hooks.d.ts +115 -0
- package/dist/react/components/ChatSettingsDialog.hooks.js +40 -0
- package/dist/react/components/ChatSettingsDialog.js +72 -0
- package/dist/react/components/ChatSettingsSegmented.d.ts +18 -0
- package/dist/react/components/ChatSettingsSegmented.js +11 -0
- package/dist/react/components/Composer.d.ts +5 -1
- package/dist/react/components/Composer.js +126 -82
- package/dist/react/components/ConversationList.js +25 -39
- package/dist/react/components/ConversationTitleEditor.d.ts +27 -0
- package/dist/react/components/ConversationTitleEditor.js +54 -0
- package/dist/react/components/Icon.d.ts +1 -1
- package/dist/react/components/Icon.js +25 -45
- package/dist/react/components/Markdown.js +134 -123
- package/dist/react/components/McpUiSurfaceCard.d.ts +2 -2
- package/dist/react/components/McpUiSurfaceCard.js +30 -9
- package/dist/react/components/MessageList.d.ts +13 -13
- package/dist/react/components/MessageList.hooks.d.ts +6 -1
- package/dist/react/components/MessageList.hooks.js +19 -3
- package/dist/react/components/MessageList.js +34 -10
- package/dist/react/components/MessageRow.js +48 -32
- package/dist/react/components/MessageRow.usage.d.ts +5 -2
- package/dist/react/components/MessageRow.usage.js +20 -11
- package/dist/react/components/MidRunUserMessage.js +16 -3
- package/dist/react/components/QuestionForm.js +148 -76
- package/dist/react/components/QuestionsPanel.js +1 -1
- package/dist/react/components/TodoCard.js +12 -6
- package/dist/react/components/ToolCard.js +47 -40
- package/dist/react/components/composer-discovery.js +6 -1
- package/dist/react/components/mcp-ui-surface-state.d.ts +6 -11
- package/dist/react/components/mcp-ui-surface-state.js +23 -8
- package/dist/react/ext-event-renderer-registry.d.ts +8 -0
- package/dist/react/ext-event-renderer-registry.js +5 -2
- package/dist/react/features/chat-pane/components/AgentPluginUsage.d.ts +7 -0
- package/dist/react/features/chat-pane/components/AgentPluginUsage.js +45 -0
- package/dist/react/features/chat-pane/components/AgentRuntimePicker.js +55 -19
- package/dist/react/features/chat-pane/components/ChatPane.d.ts +1 -1
- package/dist/react/features/chat-pane/components/ChatPane.js +192 -74
- package/dist/react/features/chat-pane/components/RuntimeDropUp.d.ts +29 -0
- package/dist/react/features/chat-pane/components/RuntimeDropUp.js +45 -0
- package/dist/react/features/chat-pane/create-daemon-attachment-uploader.js +80 -47
- package/dist/react/features/chat-pane/create-mcp-ui-tool-caller.js +44 -40
- package/dist/react/features/chat-pane/create-mid-run-message-poster.d.ts +6 -6
- package/dist/react/features/chat-pane/create-mid-run-message-poster.js +8 -8
- package/dist/react/features/chat-pane/create-typed-answer-poster.d.ts +3 -3
- package/dist/react/features/chat-pane/create-typed-answer-poster.js +3 -3
- package/dist/react/features/chat-pane/hooks/useAgentRuntimePicker.hooks.d.ts +6 -1
- package/dist/react/features/chat-pane/hooks/useAgentRuntimePicker.hooks.js +11 -5
- package/dist/react/features/chat-pane/hooks/useChatPane.hooks.d.ts +12 -7
- package/dist/react/features/chat-pane/hooks/useChatPane.hooks.js +340 -149
- package/dist/react/features/chat-pane/hooks/useChatPaneControlsHeight.hooks.js +7 -6
- package/dist/react/features/chat-pane/hooks/useChatPaneNewTabLinks.hooks.js +28 -10
- package/dist/react/features/chat-pane/hooks/useChatPaneRuntimeInventory.hooks.d.ts +3 -3
- package/dist/react/features/chat-pane/hooks/useChatPaneRuntimeInventory.hooks.js +60 -12
- package/dist/react/features/chat-pane/hooks/useRuntimeDropUp.hooks.d.ts +49 -0
- package/dist/react/features/chat-pane/hooks/useRuntimeDropUp.hooks.js +143 -0
- package/dist/react/features/chat-pane/model-options.d.ts +18 -1
- package/dist/react/features/chat-pane/model-options.js +49 -20
- package/dist/react/features/chat-pane/rules.d.ts +8 -9
- package/dist/react/features/chat-pane/rules.js +84 -46
- package/dist/react/features/chat-pane/styles.d.ts +1 -1
- package/dist/react/features/chat-pane/styles.js +156 -37
- package/dist/react/features/chat-pane/types.d.ts +17 -8
- package/dist/react/hooks/assistant-chats.js +12 -5
- package/dist/react/hooks/composer-draft-cache.js +116 -118
- package/dist/react/hooks/composer-history-storage.js +5 -5
- package/dist/react/hooks/fab-position.js +7 -4
- package/dist/react/hooks/useAnsweredCards.d.ts +21 -4
- package/dist/react/hooks/useAnsweredCards.js +38 -11
- package/dist/react/hooks/useArtifactStream.js +28 -22
- package/dist/react/hooks/useComposer.d.ts +20 -0
- package/dist/react/hooks/useComposer.js +22 -9
- package/dist/react/hooks/useComposerHistory.hooks.d.ts +6 -1
- package/dist/react/hooks/useComposerHistory.hooks.js +39 -19
- package/dist/react/hooks/useComposerHistoryKeys.hooks.js +24 -14
- package/dist/react/hooks/useConversation.d.ts +7 -0
- package/dist/react/hooks/useConversation.js +23 -10
- package/dist/react/hooks/useExtEventGroups.js +39 -39
- package/dist/react/hooks/useQuestionForms.js +21 -26
- package/dist/react/hooks/useRunStream.js +10 -2
- package/dist/react/index.d.ts +7 -1
- package/dist/react/index.js +9 -0
- package/dist/react/message-blocks.d.ts +6 -4
- package/dist/react/message-blocks.js +94 -72
- package/dist/react/slots.d.ts +2 -2
- package/dist/react/tool-call-display.js +19 -17
- package/dist/server/durable-runs/recover.d.ts +1 -1
- package/dist/server/durable-runs/recover.js +139 -76
- package/dist/server/run-finalizer/entry.js +25 -15
- package/dist/store/legacy/sqlite/conversations.d.ts +11 -11
- package/dist/store/legacy/sqlite/conversations.js +69 -98
- package/dist/store/legacy/sqlite/messages.d.ts +10 -2
- package/dist/store/legacy/sqlite/messages.js +113 -71
- package/dist/store/runtime/chat-history-store.js +9 -3
- package/dist/store/runtime/durable-run-store.d.ts +2 -2
- package/dist/store/runtime/durable-run-store.js +165 -117
- package/dist/store/runtime/history-tools.js +118 -82
- package/dist/store/runtime/run-ledger.d.ts +4 -1
- package/dist/store/runtime/run-ledger.js +56 -16
- package/dist/store/sql/cursor.js +38 -18
- package/dist/store/sql/store.d.ts +18 -0
- package/dist/store/sql/store.js +46 -15
- package/package.json +12 -10
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { readAgentPluginChatState, captureAgentPluginUsage } from '../agent-plugin-chat-state.js';
|
|
1
2
|
import { buildTranscript, latestUserPromptFromHistory } from "../transcript.js";
|
|
2
3
|
import * as runEvents from "../run-events/entry.js";
|
|
3
4
|
/** Bind transport effects once; in-flight controllers belong to this client, never the package.
|
|
@@ -34,8 +35,6 @@ export function createAssistantTransportClient({ ports }, _options = {}) {
|
|
|
34
35
|
* accumulated here) — chat-core's own `ChatMessage.events` array is what concatenates them into one
|
|
35
36
|
* growing message, so accumulating twice would double the text.
|
|
36
37
|
*/
|
|
37
|
-
const RUNS_URL = ports.runsUrl;
|
|
38
|
-
const BYOK_TURN_URL = ports.byokTurnUrl;
|
|
39
38
|
/** Distinguishes a BYOK-run id (client-minted, no server-side run record) from a daemon-run id
|
|
40
39
|
* (server-minted, reattachable) wherever a bare `runId` string is all a `ChatTransport` method
|
|
41
40
|
* receives — see `stopRun`/`fetchRunStatus`/`reattachRun` below for why the distinction matters. */
|
|
@@ -73,11 +72,18 @@ export function createAssistantTransportClient({ ports }, _options = {}) {
|
|
|
73
72
|
* Bounded deliberately: each run is billed, and an unbounded transcript grows the input cost of
|
|
74
73
|
* every subsequent turn in a conversation that has no natural end. {@link MAX_TRANSCRIPT_TURNS}
|
|
75
74
|
* keeps a long-running chat from silently becoming the most expensive thing in the product.
|
|
75
|
+
* Failed turns are labeled outcomes so another agent can explain and recover from them.
|
|
76
76
|
*/
|
|
77
77
|
function runPrompt(history) {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
78
|
+
const recent = history.slice(-MAX_TRANSCRIPT_TURNS).map(message => {
|
|
79
|
+
if (message.role !== 'assistant' || isAnsweredAssistantTurn(message))
|
|
80
|
+
return message;
|
|
81
|
+
const detail = message.events?.filter(event => event.kind === 'status')
|
|
82
|
+
.map(event => [event.label, event.detail].filter(Boolean).join(': ')).join('\n').slice(-4000);
|
|
83
|
+
// Reserve space for the failure reason even when partial output fills the transcript cap.
|
|
84
|
+
const outcome = [message.content.slice(0, 6000), detail].filter(Boolean).join('\n\n');
|
|
85
|
+
return { ...message, content: `[${message.agentName ?? message.agentId ?? 'Assistant'} run ${message.runStatus}; not a completed answer]\n${outcome || 'No assistant reply was produced.'}` };
|
|
86
|
+
});
|
|
81
87
|
return buildTranscript({ history: recent });
|
|
82
88
|
}
|
|
83
89
|
/**
|
|
@@ -205,7 +211,49 @@ export function createAssistantTransportClient({ ports }, _options = {}) {
|
|
|
205
211
|
/** The durable message owns recovery and terminal state; this subscription follows replacement
|
|
206
212
|
* attempts without writing a browser-inferred failure into history. */
|
|
207
213
|
function subscribeToRun(runId, handlers, signal) {
|
|
208
|
-
ports.followRun({ runId, handlers, ...(signal === undefined ? {} : { signal }) }, {});
|
|
214
|
+
ports.followRun({ runId, handlers: capturePluginUsage(handlers), ...(signal === undefined ? {} : { signal }) }, {});
|
|
215
|
+
}
|
|
216
|
+
/** The well-formed plugin usage an `agent_plugins` extension event carries, or `undefined` for any
|
|
217
|
+
* other event — the one validity rule both live events and checkpoint replays are captured by. */
|
|
218
|
+
function pluginUsageOf({ event }, _optional) {
|
|
219
|
+
if (event.kind !== 'ext' || event.name !== 'agent_plugins')
|
|
220
|
+
return undefined;
|
|
221
|
+
const usage = event.data;
|
|
222
|
+
return typeof usage?.conversationId === 'string' && usage.state && Array.isArray(usage.state.excluded) ? usage : undefined;
|
|
223
|
+
}
|
|
224
|
+
/** Capture before notifying the UI so answers received after reload persist for the next turn.
|
|
225
|
+
* Checkpoint panes skip live deltas to avoid duplicate text, so capture their saved events here
|
|
226
|
+
* too. Keep this wrapper as the single capture owner for both delivery contracts.
|
|
227
|
+
* @complexity O(e + s) per checkpoint; O(s) per live event, in saved events and plugin-state size.
|
|
228
|
+
*/
|
|
229
|
+
function capturePluginUsage(handlers) {
|
|
230
|
+
const captured = new Map();
|
|
231
|
+
const capture = (event) => {
|
|
232
|
+
const usage = pluginUsageOf({ event }, {});
|
|
233
|
+
if (!usage)
|
|
234
|
+
return;
|
|
235
|
+
const state = JSON.stringify(usage.state);
|
|
236
|
+
if (captured.get(usage.conversationId) === state)
|
|
237
|
+
return;
|
|
238
|
+
captured.set(usage.conversationId, state);
|
|
239
|
+
captureAgentPluginUsage({ event });
|
|
240
|
+
};
|
|
241
|
+
return {
|
|
242
|
+
...handlers,
|
|
243
|
+
onEvent: event => { capture(event); handlers.onEvent(event); },
|
|
244
|
+
...(handlers.onCheckpoint ? { onCheckpoint: (message) => {
|
|
245
|
+
// Historical answers must not roll the current state backwards on every poll.
|
|
246
|
+
const latest = new Map();
|
|
247
|
+
for (const event of message.events ?? []) {
|
|
248
|
+
const usage = pluginUsageOf({ event }, {});
|
|
249
|
+
if (usage)
|
|
250
|
+
latest.set(usage.conversationId, event);
|
|
251
|
+
}
|
|
252
|
+
for (const event of latest.values())
|
|
253
|
+
capture(event);
|
|
254
|
+
handlers.onCheckpoint(message);
|
|
255
|
+
} } : {}),
|
|
256
|
+
};
|
|
209
257
|
}
|
|
210
258
|
/** Client-minted, not server-minted — see module doc's path-2 section: a BYOK run has no server-side
|
|
211
259
|
* run record to name it. Prefixed so `stopRun`/`fetchRunStatus`/`reattachRun` below can tell a
|
|
@@ -293,6 +341,56 @@ export function createAssistantTransportClient({ ports }, _options = {}) {
|
|
|
293
341
|
ctx.finish();
|
|
294
342
|
}
|
|
295
343
|
}
|
|
344
|
+
/**
|
|
345
|
+
* The provider-bound history of one BYOK turn: only turns the agent actually answered, never an
|
|
346
|
+
* empty one, each carrying its redaction signal.
|
|
347
|
+
*/
|
|
348
|
+
function byokMessages({ history }, _optional) {
|
|
349
|
+
// `historyForTranscript` first (Defect 3, 2026-09-11): the empty-content filter below already
|
|
350
|
+
// hides a failed run that produced nothing, but NOT one that emitted a few tokens before dying —
|
|
351
|
+
// that partial output would be sent to the provider as if it were the assistant's answer. This is
|
|
352
|
+
// the same defect as on the Local CLI path, reachable by a second route.
|
|
353
|
+
return historyForTranscript(history)
|
|
354
|
+
.filter((message) => message.content.trim().length > 0)
|
|
355
|
+
.map((message) => ({ role: message.role, content: message.content,
|
|
356
|
+
...(message.secretRedaction?.secretRedacted ? { secretRedacted: true } : {}),
|
|
357
|
+
}));
|
|
358
|
+
}
|
|
359
|
+
/** The credential block of a BYOK turn; an empty base URL and an unset token cap are omitted. */
|
|
360
|
+
function byokCredentials({ byok }, _optional) {
|
|
361
|
+
return {
|
|
362
|
+
protocol: byok.protocol,
|
|
363
|
+
apiKey: byok.apiKey,
|
|
364
|
+
...(byok.baseUrl ? { baseUrl: byok.baseUrl } : {}),
|
|
365
|
+
model: byok.model,
|
|
366
|
+
...(byok.maxTokens !== undefined ? { maxTokens: byok.maxTokens } : {}),
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
/** The JSON body `POST byokTurnUrl` carries for one turn. */
|
|
370
|
+
function byokTurnBody({ input, byok }, _optional) {
|
|
371
|
+
return {
|
|
372
|
+
messages: byokMessages({ history: input.history }, {}),
|
|
373
|
+
...attachmentIdsField(input),
|
|
374
|
+
...conversationIdField(input),
|
|
375
|
+
agentPluginState: input.context?.["agentPluginState"],
|
|
376
|
+
userPrompt: input.context?.["userPrompt"],
|
|
377
|
+
...pluginRefIdsField(input),
|
|
378
|
+
// The admin screen, as on the Local CLI path (`buildLocalCliContextRef`): the server puts it
|
|
379
|
+
// in front of the newest message so a BYOK turn knows the open page and where drawings go.
|
|
380
|
+
...pageContextField(input),
|
|
381
|
+
byok: byokCredentials({ byok }, {}),
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
/**
|
|
385
|
+
* The error for a refused request: `<action> (<status>)`, plus up to 300 characters of the
|
|
386
|
+
* response body when it has one — shared by the BYOK start, Local CLI start and cancel refusals.
|
|
387
|
+
*/
|
|
388
|
+
async function refusalError({ action, response }, _optional) {
|
|
389
|
+
// A response whose body stream errors out (e.g. a dropped connection) must not turn "the server
|
|
390
|
+
// rejected the request" into "the client crashed reading the rejection".
|
|
391
|
+
const detail = await response.text().catch(() => "");
|
|
392
|
+
return new Error(`${action} (${response.status})${detail ? `: ${detail.slice(0, 300)}` : ""}`);
|
|
393
|
+
}
|
|
296
394
|
/**
|
|
297
395
|
* The BYOK run path (2026-08-04) — one held-open `POST` to `assistant-byok.ts`, no separate
|
|
298
396
|
* `EventSource`/reattach. See module doc's path-2 section for why this shape differs from
|
|
@@ -307,35 +405,13 @@ export function createAssistantTransportClient({ ports }, _options = {}) {
|
|
|
307
405
|
const controller = new AbortController();
|
|
308
406
|
byokAbortControllers.set(runId, controller);
|
|
309
407
|
input.signal?.addEventListener("abort", () => controller.abort());
|
|
310
|
-
// `historyForTranscript` first (Defect 3, 2026-09-11): the empty-content filter below already
|
|
311
|
-
// hides a failed run that produced nothing, but NOT one that emitted a few tokens before dying —
|
|
312
|
-
// that partial output would be sent to the provider as if it were the assistant's answer. This is
|
|
313
|
-
// the same defect as on the Local CLI path, reachable by a second route.
|
|
314
|
-
const messages = historyForTranscript(input.history)
|
|
315
|
-
.filter((message) => message.content.trim().length > 0)
|
|
316
|
-
.map((message) => ({ role: message.role, content: message.content,
|
|
317
|
-
...(message.secretRedaction?.secretRedacted ? { secretRedacted: true } : {}),
|
|
318
|
-
}));
|
|
319
408
|
let response;
|
|
320
409
|
try {
|
|
321
|
-
response = await ports.fetch(
|
|
410
|
+
response = await ports.fetch(ports.byokTurnUrl, {
|
|
322
411
|
method: "POST",
|
|
323
412
|
headers: { "Content-Type": "application/json" },
|
|
324
413
|
credentials: "same-origin",
|
|
325
|
-
body: JSON.stringify({
|
|
326
|
-
messages,
|
|
327
|
-
...attachmentIdsField(input),
|
|
328
|
-
// The admin screen, as on the Local CLI path (`buildLocalCliContextRef`): the server puts it
|
|
329
|
-
// in front of the newest message so a BYOK turn knows the open page and where drawings go.
|
|
330
|
-
...pageContextField(input),
|
|
331
|
-
byok: {
|
|
332
|
-
protocol: byok.protocol,
|
|
333
|
-
apiKey: byok.apiKey,
|
|
334
|
-
...(byok.baseUrl ? { baseUrl: byok.baseUrl } : {}),
|
|
335
|
-
model: byok.model,
|
|
336
|
-
...(byok.maxTokens !== undefined ? { maxTokens: byok.maxTokens } : {}),
|
|
337
|
-
},
|
|
338
|
-
}),
|
|
414
|
+
body: JSON.stringify(byokTurnBody({ input, byok }, {})),
|
|
339
415
|
signal: controller.signal,
|
|
340
416
|
});
|
|
341
417
|
}
|
|
@@ -345,8 +421,7 @@ export function createAssistantTransportClient({ ports }, _options = {}) {
|
|
|
345
421
|
}
|
|
346
422
|
if (!response.ok || !response.body) {
|
|
347
423
|
byokAbortControllers.delete(runId);
|
|
348
|
-
|
|
349
|
-
throw new Error(`BYOK turn failed to start (${response.status})${detail ? `: ${detail.slice(0, 300)}` : ""}`);
|
|
424
|
+
throw await refusalError({ action: "BYOK turn failed to start", response }, {});
|
|
350
425
|
}
|
|
351
426
|
const collected = [];
|
|
352
427
|
let settled = false;
|
|
@@ -484,10 +559,14 @@ export function createAssistantTransportClient({ ports }, _options = {}) {
|
|
|
484
559
|
* adopt a conversation because `input.context` named none (the first turn of a chat). Omitted by
|
|
485
560
|
* every caller that already has an id on `input.context`, and by this function's own unit tests —
|
|
486
561
|
* in which case the `context` field is read exactly as before.
|
|
562
|
+
* The bounded `coldStartContext` is the complete prompt, including the current request. The
|
|
563
|
+
* daemon selects it instead of the resume tail when native resume is unavailable.
|
|
487
564
|
*/
|
|
488
565
|
function buildLocalCliContextRef(input, prompt, resolvedConversationId) {
|
|
489
566
|
return {
|
|
490
567
|
prompt: ports.projectPrompt({ prompt, context: input.context }, {}),
|
|
568
|
+
// Select one complete transcript after binding; concatenating it with the tail repeats turns.
|
|
569
|
+
coldStartContext: ports.projectPrompt({ prompt: runPrompt(input.history), context: input.context }, {}),
|
|
491
570
|
// The transcript is already sanitized; retain its signal across the daemon JSON boundary.
|
|
492
571
|
...(input.history.some(message => message.role === "user" && message.secretRedaction?.secretRedacted) ? { secretRedacted: true } : {}),
|
|
493
572
|
assistantMessageId: input.assistantMessageId,
|
|
@@ -496,6 +575,8 @@ export function createAssistantTransportClient({ ports }, _options = {}) {
|
|
|
496
575
|
...reasoningField(input),
|
|
497
576
|
...attachmentIdsField(input),
|
|
498
577
|
...pluginRefIdsField(input),
|
|
578
|
+
agentPluginState: input.context?.["agentPluginState"],
|
|
579
|
+
userPrompt: input.context?.["userPrompt"],
|
|
499
580
|
...conversationIdField(input, resolvedConversationId),
|
|
500
581
|
...pageContextField(input),
|
|
501
582
|
};
|
|
@@ -523,27 +604,28 @@ export function createAssistantTransportClient({ ports }, _options = {}) {
|
|
|
523
604
|
const adopted = await ensureConversationId({}, {}).catch(() => null);
|
|
524
605
|
return typeof adopted === "string" && adopted.length > 0 ? adopted : undefined;
|
|
525
606
|
}
|
|
526
|
-
/**
|
|
527
|
-
*
|
|
528
|
-
*
|
|
529
|
-
* ({@link undeliveredUserPrompt}) when `input.agentId` is present in `resumeCapableAgentIds` — see
|
|
530
|
-
* `getResumeCapableAgentIds`'s own doc on {@link AssistantTransportOptions} for the full
|
|
531
|
-
* contract this mirrors. Pulled out of `startRun` (2026-09-04, complexity pass) as its own pure
|
|
532
|
-
* step. `options.getResumeCapableAgentIds?.({}, {})` is still called at the same point in `startRun` as
|
|
533
|
-
* before — only the "which agentIds carries own memory" branch itself moves here, so the "read fresh
|
|
534
|
-
* on every call" convention that doc argues for is unchanged.
|
|
607
|
+
/** Send a resumed agent only its unseen conversation tail, retaining other agents' outcomes.
|
|
608
|
+
* Legacy rows without agent identity retain the existing unanswered-user delivery contract.
|
|
609
|
+
* @complexity O(n) to find the last answered turn; transcript rendering is capped at 40 turns.
|
|
535
610
|
*/
|
|
536
611
|
function resolveLocalCliPrompt(input, resumeCapableAgentIds) {
|
|
537
612
|
const carriesOwnMemory = input.agentId !== undefined && resumeCapableAgentIds?.has(input.agentId) === true;
|
|
613
|
+
if (!carriesOwnMemory)
|
|
614
|
+
return runPrompt(input.history);
|
|
615
|
+
let lastOwnAnswer = -1;
|
|
616
|
+
input.history.forEach((message, index) => {
|
|
617
|
+
if (message.role === 'assistant' && (!message.agentId || message.agentId === input.agentId) && isAnsweredAssistantTurn(message))
|
|
618
|
+
lastOwnAnswer = index;
|
|
619
|
+
});
|
|
620
|
+
const unseen = input.history.slice(lastOwnAnswer + 1);
|
|
621
|
+
const crossedAgents = unseen.some(message => message.role === 'assistant' && message.agentId && message.agentId !== input.agentId);
|
|
538
622
|
// BEHAVIOR CHANGE (2026-09-11, Defect 2): the resume-capable branch was `latestUserPrompt` — the
|
|
539
623
|
// single newest user turn, on the assumption that everything before it is already inside the
|
|
540
624
|
// CLI's own session. That assumption holds only for turns the agent actually answered; a turn
|
|
541
625
|
// whose run died before the CLI read its prompt is in neither place, and nothing ever sent it
|
|
542
626
|
// again. `undeliveredUserPrompt` returns the identical single string in the ordinary case and
|
|
543
|
-
// differs only when there is genuinely something unanswered to catch up on.
|
|
544
|
-
|
|
545
|
-
// string for its own send guard, but this branch has no use for it any more.
|
|
546
|
-
return carriesOwnMemory ? undeliveredUserPrompt(input.history) : runPrompt(input.history);
|
|
627
|
+
// differs only when there is genuinely something unanswered to catch up on.
|
|
628
|
+
return crossedAgents ? runPrompt(unseen) : undeliveredUserPrompt(input.history);
|
|
547
629
|
}
|
|
548
630
|
/**
|
|
549
631
|
* Writes the newest user turn to durable storage before its run is dispatched, and never fails the
|
|
@@ -562,44 +644,73 @@ export function createAssistantTransportClient({ ports }, _options = {}) {
|
|
|
562
644
|
* two, and a retry of the same content is free. `useAssistantChats`'s own `persistUserTurn` also
|
|
563
645
|
* marks the id written, so in practice `flush` skips it entirely.
|
|
564
646
|
*
|
|
565
|
-
* @param
|
|
566
|
-
*
|
|
647
|
+
* @param message - The newest `role: "user"` message in `startRun`'s history — what gets written.
|
|
648
|
+
* `startRun` has already refused a history without one, so it is always present here.
|
|
567
649
|
* @param conversationId - The already-resolved conversation (see `resolveRunConversationId`), or
|
|
568
650
|
* `undefined` when adoption failed — there is no row to write into in that case.
|
|
569
651
|
* @param persistUserTurn - `AssistantTransportOptions.persistUserTurn`, or `undefined`
|
|
570
652
|
* for a transport built without it (every test and any non-dock consumer).
|
|
571
653
|
* @returns Nothing, always. The `.catch` keeps a failed browser PUT from refusing the user's turn;
|
|
572
654
|
* durable acceptance is now responsible for committing its copy before dispatch.
|
|
573
|
-
* @complexity O(
|
|
655
|
+
* @complexity O(1) plus one PUT.
|
|
574
656
|
*/
|
|
575
|
-
async function persistUserTurnBeforeDispatch(
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
const message = [...history].reverse().find((candidate) => candidate.role === "user");
|
|
580
|
-
if (message === undefined)
|
|
657
|
+
async function persistUserTurnBeforeDispatch({ message, conversationId }, { persistUserTurn }) {
|
|
658
|
+
// Pending composer input is persisted by atomic run acceptance. An earlier PUT could save a
|
|
659
|
+
// rejected (409) draft as a user turn; confirmed/legacy callers retain their existing ordering.
|
|
660
|
+
if (message.inputPending || conversationId === undefined || persistUserTurn === undefined)
|
|
581
661
|
return;
|
|
582
662
|
await persistUserTurn({ conversationId, message }, {}).catch(() => undefined);
|
|
583
663
|
}
|
|
664
|
+
/** The newest `role: "user"` message in a history, or `undefined` when it has none.
|
|
665
|
+
* @complexity O(n) in history length. */
|
|
666
|
+
function newestUserMessage({ history }, _optional) {
|
|
667
|
+
return [...history].reverse().find((message) => message.role === "user");
|
|
668
|
+
}
|
|
584
669
|
/**
|
|
585
670
|
* Turns `startRun`'s Local CLI `POST /api/runs` response into either a thrown, detailed error or a
|
|
586
671
|
* subscribed run — the two outcomes once the request lands. Pulled out of `startRun` (2026-09-04,
|
|
587
672
|
* complexity pass) as its own step: `response.ok` in, `{runId}` or a thrown `Error` out. The
|
|
588
|
-
* `.text().catch(() => "")` is load-bearing, not decorative — see
|
|
673
|
+
* `.text().catch(() => "")` is load-bearing, not decorative — see {@link refusalError}'s inline comment.
|
|
589
674
|
*/
|
|
590
675
|
async function finishLocalCliRun(response, handlers, signal) {
|
|
591
|
-
if (!response.ok)
|
|
592
|
-
|
|
593
|
-
// rejected the request" into "the client crashed reading the rejection".
|
|
594
|
-
const detail = await response.text().catch(() => "");
|
|
595
|
-
throw new Error(`agent run failed to start (${response.status})${detail ? `: ${detail.slice(0, 300)}` : ""}`);
|
|
596
|
-
}
|
|
676
|
+
if (!response.ok)
|
|
677
|
+
throw await refusalError({ action: "agent run failed to start", response }, {});
|
|
597
678
|
const { run, messageId, conversationId } = (await response.json());
|
|
598
679
|
if (messageId && conversationId)
|
|
599
680
|
ports.bindRun({ runId: run.id, binding: { messageId, conversationId } }, {});
|
|
600
681
|
subscribeToRun(run.id, handlers, signal);
|
|
601
682
|
return { runId: run.id };
|
|
602
683
|
}
|
|
684
|
+
/**
|
|
685
|
+
* The Local CLI path of `startRun`: durable user turn, then acceptance, then the followed run.
|
|
686
|
+
*
|
|
687
|
+
* @param input - `startRun`'s input with its conversation identity already merged into `context`.
|
|
688
|
+
* @param userMessage - The newest user message; acceptance carries it verbatim.
|
|
689
|
+
* @param conversationId - The resolved conversation, or `undefined` when adoption failed.
|
|
690
|
+
*/
|
|
691
|
+
async function startLocalCliRun({ input, handlers, userMessage, conversationId }, { options }) {
|
|
692
|
+
// Local CLI path below. `resolveLocalCliPrompt`'s own doc has the full contract for why an
|
|
693
|
+
// unresolved/absent agentId fails open to the full transcript rather than the bare message.
|
|
694
|
+
const prompt = resolveLocalCliPrompt(input, options.getResumeCapableAgentIds?.({}, {}));
|
|
695
|
+
// Also before the POST, and for the same reason it is awaited rather than fired off: the
|
|
696
|
+
// user's turn must be durable before a run that can fail to deliver it exists at all
|
|
697
|
+
// (Defect 2, 2026-09-11). Ordered after conversation adoption because it needs the id that
|
|
698
|
+
// adoption produces. BYOK and AG-UI share the identity above; neither writes a durable
|
|
699
|
+
// user turn ahead of dispatch; those transports retain their own persistence behavior.
|
|
700
|
+
await persistUserTurnBeforeDispatch({ message: userMessage, conversationId }, { persistUserTurn: options.persistUserTurn });
|
|
701
|
+
// Acceptance commits the question and stub together even if the separate browser PUT fails
|
|
702
|
+
// or the tab disappears. Carry the exact message ID/payload, never the rendered prompt.
|
|
703
|
+
const base = buildLocalCliContextRef(input, prompt, conversationId);
|
|
704
|
+
const contextRef = { ...base, userMessage };
|
|
705
|
+
const response = await ports.acceptRun({
|
|
706
|
+
...(input.cancelSignal === undefined ? {} : { signal: input.cancelSignal }),
|
|
707
|
+
requestSignal: input.cancelSignal ?? input.signal,
|
|
708
|
+
body: { contextRef: JSON.stringify(contextRef), agentId: input.agentId, idempotencyKey: base.assistantMessageId },
|
|
709
|
+
messageId: base.assistantMessageId,
|
|
710
|
+
...(conversationId === undefined ? {} : { conversationId }),
|
|
711
|
+
}, {});
|
|
712
|
+
return finishLocalCliRun(response, handlers, input.signal);
|
|
713
|
+
}
|
|
603
714
|
function createAssistantTransport(options = {}) {
|
|
604
715
|
return {
|
|
605
716
|
async startRun(input, handlers) {
|
|
@@ -608,9 +719,13 @@ export function createAssistantTransportClient({ ports }, _options = {}) {
|
|
|
608
719
|
// prompt asks the agent to reply to itself. Shared by both paths below, and reused (rather than
|
|
609
720
|
// recomputed) by the Local CLI branch's own resume-capable check further down.
|
|
610
721
|
const latestUserPrompt = latestUserPromptFromHistory({ history: input.history });
|
|
611
|
-
|
|
722
|
+
const userMessage = newestUserMessage({ history: input.history }, {});
|
|
723
|
+
if (!latestUserPrompt || userMessage === undefined) {
|
|
612
724
|
throw new Error("no user message to send");
|
|
613
725
|
}
|
|
726
|
+
// Adopt identity before either runtime branch so first-turn decisions survive a reload.
|
|
727
|
+
const conversationId = await resolveRunConversationId(input, options.ensureConversationId);
|
|
728
|
+
input = { ...input, context: { ...input.context, ...(conversationId ? { conversationId, agentPluginState: readAgentPluginChatState({ conversationId }) } : {}), userPrompt: latestUserPrompt } };
|
|
614
729
|
const executionConfig = options.getExecutionConfig?.({}, {});
|
|
615
730
|
// Dispatches on MODE alone, not on whether a key is typed in this browser right now
|
|
616
731
|
// (2026-08-05). `executionConfig.byok.apiKey` is write-only server-side — it is empty on
|
|
@@ -623,7 +738,7 @@ export function createAssistantTransportClient({ ports }, _options = {}) {
|
|
|
623
738
|
// level down — the route's own 400 `"no usable BYOK credential..."` — which is a real,
|
|
624
739
|
// actionable answer instead of the mode picker silently refusing to try.
|
|
625
740
|
if (executionConfig?.mode === "byok") {
|
|
626
|
-
return startByokRun(ports.projectInput({ input }, {}), handlers, executionConfig.byok);
|
|
741
|
+
return startByokRun(ports.projectInput({ input }, {}), capturePluginUsage(handlers), executionConfig.byok);
|
|
627
742
|
}
|
|
628
743
|
// AG-UI canary path (ADR-059) — a host-supplied toggle, independent of `executionConfig.mode`
|
|
629
744
|
// (Jini's own closed union, never extended for this). Checked AFTER the byok branch above:
|
|
@@ -633,35 +748,9 @@ export function createAssistantTransportClient({ ports }, _options = {}) {
|
|
|
633
748
|
if (options.getAgUiEnabled?.({}, {})) {
|
|
634
749
|
// `historyForTranscript` here rather than inside `startAgUiRun`: that module importing this
|
|
635
750
|
// one would be a cycle, and this is its only caller. Same rule as the BYOK and Local CLI paths.
|
|
636
|
-
return ports.agUi.startAgUiRun({ input: ports.projectInput({ input: { ...input, history: historyForTranscript(input.history) } }, {}), handlers }, {});
|
|
751
|
+
return ports.agUi.startAgUiRun({ input: ports.projectInput({ input: { ...input, history: historyForTranscript(input.history) } }, {}), handlers: capturePluginUsage(handlers) }, {});
|
|
637
752
|
}
|
|
638
|
-
|
|
639
|
-
// unresolved/absent agentId fails open to the full transcript rather than the bare message.
|
|
640
|
-
const prompt = resolveLocalCliPrompt(input, options.getResumeCapableAgentIds?.({}, {}));
|
|
641
|
-
// Awaited BEFORE the POST, and only on this branch: session resume is a daemon-run concept, so
|
|
642
|
-
// neither the BYOK nor the AG-UI path above has anything to file a session id under. See
|
|
643
|
-
// `resolveRunConversationId` and `AssistantTransportOptions.ensureConversationId`.
|
|
644
|
-
const conversationId = await resolveRunConversationId(input, options.ensureConversationId);
|
|
645
|
-
// Also before the POST, and for the same reason it is awaited rather than fired off: the
|
|
646
|
-
// user's turn must be durable before a run that can fail to deliver it exists at all
|
|
647
|
-
// (Defect 2, 2026-09-11). Ordered after conversation adoption because it needs the id that
|
|
648
|
-
// adoption produces. Known gap, not an oversight: the BYOK and AG-UI branches above return
|
|
649
|
-
// before this point and resolve no conversation id of their own, so neither writes a durable
|
|
650
|
-
// user turn ahead of dispatch — closing that needs those paths to adopt a conversation first.
|
|
651
|
-
await persistUserTurnBeforeDispatch(input, conversationId, options.persistUserTurn);
|
|
652
|
-
const userMessage = [...input.history].reverse().find((message) => message.role === "user");
|
|
653
|
-
// Acceptance commits the question and stub together even if the separate browser PUT fails
|
|
654
|
-
// or the tab disappears. Carry the exact message ID/payload, never the rendered prompt.
|
|
655
|
-
const base = buildLocalCliContextRef(input, prompt, conversationId);
|
|
656
|
-
const contextRef = userMessage ? { ...base, userMessage } : base;
|
|
657
|
-
const response = await ports.acceptRun({
|
|
658
|
-
...(input.cancelSignal === undefined ? {} : { signal: input.cancelSignal }),
|
|
659
|
-
requestSignal: input.cancelSignal ?? input.signal,
|
|
660
|
-
body: { contextRef: JSON.stringify(contextRef), agentId: input.agentId, idempotencyKey: base.assistantMessageId },
|
|
661
|
-
messageId: base.assistantMessageId,
|
|
662
|
-
...(conversationId === undefined ? {} : { conversationId }),
|
|
663
|
-
}, {});
|
|
664
|
-
return finishLocalCliRun(response, handlers, input.signal);
|
|
753
|
+
return startLocalCliRun({ input, handlers, userMessage, conversationId }, { options });
|
|
665
754
|
},
|
|
666
755
|
async reattachRun(runId, handlers, options) {
|
|
667
756
|
// A BYOK run has no server-side record to reattach to (module doc's path-2 section) — the
|
|
@@ -688,7 +777,7 @@ export function createAssistantTransportClient({ ports }, _options = {}) {
|
|
|
688
777
|
return null;
|
|
689
778
|
if (ports.agUi.isAgUiRunId({ runId }, {}))
|
|
690
779
|
return ports.agUi.fetchAgUiRunStatus({}, {});
|
|
691
|
-
const response = await ports.fetch(`${
|
|
780
|
+
const response = await ports.fetch(`${ports.runsUrl}/${encodeURIComponent(runId)}`, { credentials: "same-origin" });
|
|
692
781
|
if (!response.ok)
|
|
693
782
|
return null;
|
|
694
783
|
const { run } = (await response.json());
|
|
@@ -704,7 +793,7 @@ export function createAssistantTransportClient({ ports }, _options = {}) {
|
|
|
704
793
|
if (ports.agUi.isAgUiRunId({ runId }, {})) {
|
|
705
794
|
return ports.agUi.stopAgUiRun({ runId }, {});
|
|
706
795
|
}
|
|
707
|
-
const response = await ports.fetch(`${
|
|
796
|
+
const response = await ports.fetch(`${ports.runsUrl}/${encodeURIComponent(runId)}/cancel`, {
|
|
708
797
|
method: "POST",
|
|
709
798
|
headers: { "Content-Type": "application/json" },
|
|
710
799
|
credentials: "same-origin",
|
|
@@ -715,10 +804,8 @@ export function createAssistantTransportClient({ ports }, _options = {}) {
|
|
|
715
804
|
return;
|
|
716
805
|
// `fetch` resolves on any status: without this a refused cancel (401/403/500) resolved exactly
|
|
717
806
|
// like a successful one while the run kept executing. `useRunStream` logs a rejection here.
|
|
718
|
-
if (!response.ok)
|
|
719
|
-
|
|
720
|
-
throw new Error(`cancelling agent run failed (${response.status})${detail ? `: ${detail.slice(0, 300)}` : ""}`);
|
|
721
|
-
}
|
|
807
|
+
if (!response.ok)
|
|
808
|
+
throw await refusalError({ action: "cancelling agent run failed", response }, {});
|
|
722
809
|
},
|
|
723
810
|
};
|
|
724
811
|
}
|
|
@@ -19,9 +19,10 @@ import type { AgentEvent } from './events.js';
|
|
|
19
19
|
export declare const ASSISTANT_STEP_SEPARATOR = "\n\n";
|
|
20
20
|
/**
|
|
21
21
|
* A turn's text, with a paragraph break between runs separated by a tool call or card.
|
|
22
|
+
* A replay of an already-seen tool ID is not a new step, matching the saved-event projection.
|
|
22
23
|
*
|
|
23
24
|
* @param events - The turn's events, in arrival order.
|
|
24
|
-
* @complexity O(total text length).
|
|
25
|
+
* @complexity O(n + total text length) time and O(n) space for n events.
|
|
25
26
|
*/
|
|
26
27
|
export declare function assistantContentFromEvents({ events }: {
|
|
27
28
|
events: readonly AgentEvent[] | undefined;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { dedupeToolUsesById } from './tool-events.js';
|
|
1
2
|
/** What separates two text runs that a tool call or card sat between. */
|
|
2
3
|
export const ASSISTANT_STEP_SEPARATOR = '\n\n';
|
|
3
4
|
function separatorBefore(out, text) {
|
|
@@ -7,25 +8,22 @@ function separatorBefore(out, text) {
|
|
|
7
8
|
}
|
|
8
9
|
/**
|
|
9
10
|
* A turn's text, with a paragraph break between runs separated by a tool call or card.
|
|
11
|
+
* A replay of an already-seen tool ID is not a new step, matching the saved-event projection.
|
|
10
12
|
*
|
|
11
13
|
* @param events - The turn's events, in arrival order.
|
|
12
|
-
* @complexity O(total text length).
|
|
14
|
+
* @complexity O(n + total text length) time and O(n) space for n events.
|
|
13
15
|
*/
|
|
14
16
|
export function assistantContentFromEvents({ events }) {
|
|
15
17
|
let out = '';
|
|
16
18
|
let stepBoundary = false;
|
|
17
|
-
for (const ev of events
|
|
18
|
-
if (ev.kind === '
|
|
19
|
-
if (ev.text.length === 0)
|
|
20
|
-
continue;
|
|
21
|
-
if (stepBoundary)
|
|
22
|
-
out += separatorBefore(out, ev.text);
|
|
23
|
-
out += ev.text;
|
|
24
|
-
stepBoundary = false;
|
|
25
|
-
}
|
|
26
|
-
else if (ev.kind === 'tool_use' || ev.kind === 'ext') {
|
|
19
|
+
for (const ev of dedupeToolUsesById({ events: events ? [...events] : undefined })) {
|
|
20
|
+
if (ev.kind === 'tool_use' || ev.kind === 'ext') {
|
|
27
21
|
stepBoundary = true;
|
|
28
22
|
}
|
|
23
|
+
else if (ev.kind === 'text' && ev.text.length > 0) {
|
|
24
|
+
out += (stepBoundary ? separatorBefore(out, ev.text) : '') + ev.text;
|
|
25
|
+
stepBoundary = false;
|
|
26
|
+
}
|
|
29
27
|
}
|
|
30
28
|
return out;
|
|
31
29
|
}
|
|
@@ -41,8 +41,10 @@ export function findTargetElement({ title, root, selection }) {
|
|
|
41
41
|
return null;
|
|
42
42
|
const candidates = Array.from(root.querySelectorAll(selection.candidateSelector))
|
|
43
43
|
.filter(el => !el.closest(selection.excludedAncestorSelector));
|
|
44
|
-
|
|
45
|
-
|
|
44
|
+
// `textContent` is null only for documents and doctypes; every Element the selector yields has a string.
|
|
45
|
+
const text = (el) => el.textContent.trim();
|
|
46
|
+
return candidates.find(el => text(el) === needle)
|
|
47
|
+
?? candidates.find(el => text(el).toLowerCase() === needle.toLowerCase()) ?? null;
|
|
46
48
|
}
|
|
47
49
|
/**
|
|
48
50
|
* Creates page effects with injected DOM, navigation, URL policy, scroll and timers.
|
|
@@ -7,11 +7,15 @@ export interface ComposerHistoryState {
|
|
|
7
7
|
export interface ComposerHistoryStoragePort {
|
|
8
8
|
read: (required: {
|
|
9
9
|
scope: string;
|
|
10
|
-
}, optional: {
|
|
10
|
+
}, optional: {
|
|
11
|
+
cap?: number;
|
|
12
|
+
}) => Promise<readonly string[]>;
|
|
11
13
|
write: (required: {
|
|
12
14
|
scope: string;
|
|
13
15
|
entries: readonly string[];
|
|
14
|
-
}, optional: {
|
|
16
|
+
}, optional: {
|
|
17
|
+
cap?: number;
|
|
18
|
+
}) => Promise<void>;
|
|
15
19
|
}
|
|
16
20
|
export interface ComposerHistoryKeyInput {
|
|
17
21
|
key: string;
|
|
@@ -40,15 +44,18 @@ export declare function mergeComposerHistory({ recent, current }: {
|
|
|
40
44
|
cap?: number;
|
|
41
45
|
}): string[];
|
|
42
46
|
export declare function createComposerHistoryState(_required: {}, _optional?: {}): ComposerHistoryState;
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
state: ComposerHistoryState;
|
|
46
|
-
action: 'previous' | 'next' | 'restore' | 'edit' | 'reset';
|
|
47
|
-
text: string;
|
|
48
|
-
entries: readonly string[];
|
|
49
|
-
}, _optional?: {}): {
|
|
47
|
+
type ComposerHistoryKeyAction = 'previous' | 'next' | 'restore';
|
|
48
|
+
type ComposerHistoryTransition = {
|
|
50
49
|
state: ComposerHistoryState;
|
|
51
50
|
text: string;
|
|
52
51
|
handled: boolean;
|
|
53
52
|
};
|
|
53
|
+
export declare function composerHistoryKeyAction(input: ComposerHistoryKeyInput, _optional?: {}): ComposerHistoryKeyAction | null;
|
|
54
|
+
export declare function transitionComposerHistory({ state, action, text, entries }: {
|
|
55
|
+
state: ComposerHistoryState;
|
|
56
|
+
action: ComposerHistoryKeyAction | 'edit' | 'reset';
|
|
57
|
+
text: string;
|
|
58
|
+
entries: readonly string[];
|
|
59
|
+
}, _optional?: {}): ComposerHistoryTransition;
|
|
60
|
+
export {};
|
|
54
61
|
//# sourceMappingURL=composer-history.d.ts.map
|
|
@@ -20,34 +20,46 @@ export function mergeComposerHistory({ recent, current }, { cap = 100 } = {}) {
|
|
|
20
20
|
export function createComposerHistoryState(_required, _optional = {}) {
|
|
21
21
|
return { entries: [], index: null, draft: null };
|
|
22
22
|
}
|
|
23
|
+
function hasModifierOrPopup({ input }, _optional = {}) {
|
|
24
|
+
return Boolean(input.shiftKey || input.ctrlKey || input.altKey || input.metaKey || input.isComposing || input.popupOpen);
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Whether the selection sits on the first/last line. A selection spanning a newline is on
|
|
28
|
+
* neither, because text[0, end) and text[start, ∞) both contain the selected newline.
|
|
29
|
+
*/
|
|
30
|
+
function caretBoundaries({ input }, _optional = {}) {
|
|
31
|
+
const start = Math.min(input.selectionStart, input.selectionEnd);
|
|
32
|
+
const end = Math.max(input.selectionStart, input.selectionEnd);
|
|
33
|
+
return {
|
|
34
|
+
firstLine: !input.text.slice(0, end).includes('\n') && input.firstLine !== false,
|
|
35
|
+
lastLine: !input.text.slice(start).includes('\n') && input.lastLine !== false,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
23
38
|
export function composerHistoryKeyAction(input, _optional = {}) {
|
|
24
|
-
if (
|
|
39
|
+
if (hasModifierOrPopup({ input }))
|
|
25
40
|
return null;
|
|
26
41
|
if (input.key === 'Escape')
|
|
27
42
|
return 'restore';
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
if (input.text.slice(start, end).includes('\n'))
|
|
31
|
-
return null;
|
|
32
|
-
const firstLine = !input.text.slice(0, end).includes('\n') && input.firstLine !== false;
|
|
33
|
-
const lastLine = !input.text.slice(start).includes('\n') && input.lastLine !== false;
|
|
34
|
-
if (input.key === 'ArrowUp' && firstLine)
|
|
43
|
+
const lines = caretBoundaries({ input });
|
|
44
|
+
if (input.key === 'ArrowUp' && lines.firstLine)
|
|
35
45
|
return 'previous';
|
|
36
|
-
if (input.key === 'ArrowDown' && lastLine)
|
|
46
|
+
if (input.key === 'ArrowDown' && lines.lastLine)
|
|
37
47
|
return 'next';
|
|
38
48
|
return null;
|
|
39
49
|
}
|
|
50
|
+
function startBrowsing({ state, action, text, entries }, _optional = {}) {
|
|
51
|
+
if (action !== 'previous' || entries.length === 0)
|
|
52
|
+
return { state, text, handled: false };
|
|
53
|
+
const snapshot = [...entries];
|
|
54
|
+
const index = snapshot.length - 1;
|
|
55
|
+
return { state: { entries: snapshot, index, draft: text }, text: snapshot[index], handled: true };
|
|
56
|
+
}
|
|
40
57
|
export function transitionComposerHistory({ state, action, text, entries }, _optional = {}) {
|
|
41
58
|
if (action === 'edit' || action === 'reset') {
|
|
42
59
|
return { state: createComposerHistoryState({}), text, handled: false };
|
|
43
60
|
}
|
|
44
|
-
if (state.index === null)
|
|
45
|
-
|
|
46
|
-
return { state, text, handled: false };
|
|
47
|
-
const snapshot = [...entries];
|
|
48
|
-
const index = snapshot.length - 1;
|
|
49
|
-
return { state: { entries: snapshot, index, draft: text }, text: snapshot[index], handled: true };
|
|
50
|
-
}
|
|
61
|
+
if (state.index === null)
|
|
62
|
+
return startBrowsing({ state, action, text, entries });
|
|
51
63
|
if (action === 'restore' || (action === 'next' && state.index === state.entries.length - 1)) {
|
|
52
64
|
return { state: createComposerHistoryState({}), text: state.draft ?? '', handled: true };
|
|
53
65
|
}
|