@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
|
@@ -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
|
/**
|
|
@@ -97,7 +103,9 @@ export function createAssistantTransportClient({ ports }, _options = {}) {
|
|
|
97
103
|
* `events_json` is the sole durable record of why a run died (exit code, signal, the CLI's own
|
|
98
104
|
* stderr) once the daemon's in-memory event log is gone, and the pane renders it on reload — so
|
|
99
105
|
* dropping the row trades a history bug for a forensics hole. The skip belongs here, on the read
|
|
100
|
-
* side.
|
|
106
|
+
* side. The transcript's hidden-tool mode likewise filters only recovered tool failures and keeps
|
|
107
|
+
* unrecovered failures behind a compact disclosure; it never deletes this durable evidence.
|
|
108
|
+
* See {@link historyForTranscript} and `assistant-chats.ts`'s `persistableMessages`.
|
|
101
109
|
*/
|
|
102
110
|
function isAnsweredAssistantTurn(message) {
|
|
103
111
|
return message.runStatus === undefined || message.runStatus === "succeeded";
|
|
@@ -205,7 +213,49 @@ export function createAssistantTransportClient({ ports }, _options = {}) {
|
|
|
205
213
|
/** The durable message owns recovery and terminal state; this subscription follows replacement
|
|
206
214
|
* attempts without writing a browser-inferred failure into history. */
|
|
207
215
|
function subscribeToRun(runId, handlers, signal) {
|
|
208
|
-
ports.followRun({ runId, handlers, ...(signal === undefined ? {} : { signal }) }, {});
|
|
216
|
+
ports.followRun({ runId, handlers: capturePluginUsage(handlers), ...(signal === undefined ? {} : { signal }) }, {});
|
|
217
|
+
}
|
|
218
|
+
/** The well-formed plugin usage an `agent_plugins` extension event carries, or `undefined` for any
|
|
219
|
+
* other event — the one validity rule both live events and checkpoint replays are captured by. */
|
|
220
|
+
function pluginUsageOf({ event }, _optional) {
|
|
221
|
+
if (event.kind !== 'ext' || event.name !== 'agent_plugins')
|
|
222
|
+
return undefined;
|
|
223
|
+
const usage = event.data;
|
|
224
|
+
return typeof usage?.conversationId === 'string' && usage.state && Array.isArray(usage.state.excluded) ? usage : undefined;
|
|
225
|
+
}
|
|
226
|
+
/** Capture before notifying the UI so answers received after reload persist for the next turn.
|
|
227
|
+
* Checkpoint panes skip live deltas to avoid duplicate text, so capture their saved events here
|
|
228
|
+
* too. Keep this wrapper as the single capture owner for both delivery contracts.
|
|
229
|
+
* @complexity O(e + s) per checkpoint; O(s) per live event, in saved events and plugin-state size.
|
|
230
|
+
*/
|
|
231
|
+
function capturePluginUsage(handlers) {
|
|
232
|
+
const captured = new Map();
|
|
233
|
+
const capture = (event) => {
|
|
234
|
+
const usage = pluginUsageOf({ event }, {});
|
|
235
|
+
if (!usage)
|
|
236
|
+
return;
|
|
237
|
+
const state = JSON.stringify(usage.state);
|
|
238
|
+
if (captured.get(usage.conversationId) === state)
|
|
239
|
+
return;
|
|
240
|
+
captured.set(usage.conversationId, state);
|
|
241
|
+
captureAgentPluginUsage({ event });
|
|
242
|
+
};
|
|
243
|
+
return {
|
|
244
|
+
...handlers,
|
|
245
|
+
onEvent: event => { capture(event); handlers.onEvent(event); },
|
|
246
|
+
...(handlers.onCheckpoint ? { onCheckpoint: (message) => {
|
|
247
|
+
// Historical answers must not roll the current state backwards on every poll.
|
|
248
|
+
const latest = new Map();
|
|
249
|
+
for (const event of message.events ?? []) {
|
|
250
|
+
const usage = pluginUsageOf({ event }, {});
|
|
251
|
+
if (usage)
|
|
252
|
+
latest.set(usage.conversationId, event);
|
|
253
|
+
}
|
|
254
|
+
for (const event of latest.values())
|
|
255
|
+
capture(event);
|
|
256
|
+
handlers.onCheckpoint(message);
|
|
257
|
+
} } : {}),
|
|
258
|
+
};
|
|
209
259
|
}
|
|
210
260
|
/** Client-minted, not server-minted — see module doc's path-2 section: a BYOK run has no server-side
|
|
211
261
|
* run record to name it. Prefixed so `stopRun`/`fetchRunStatus`/`reattachRun` below can tell a
|
|
@@ -293,6 +343,56 @@ export function createAssistantTransportClient({ ports }, _options = {}) {
|
|
|
293
343
|
ctx.finish();
|
|
294
344
|
}
|
|
295
345
|
}
|
|
346
|
+
/**
|
|
347
|
+
* The provider-bound history of one BYOK turn: only turns the agent actually answered, never an
|
|
348
|
+
* empty one, each carrying its redaction signal.
|
|
349
|
+
*/
|
|
350
|
+
function byokMessages({ history }, _optional) {
|
|
351
|
+
// `historyForTranscript` first (Defect 3, 2026-09-11): the empty-content filter below already
|
|
352
|
+
// hides a failed run that produced nothing, but NOT one that emitted a few tokens before dying —
|
|
353
|
+
// that partial output would be sent to the provider as if it were the assistant's answer. This is
|
|
354
|
+
// the same defect as on the Local CLI path, reachable by a second route.
|
|
355
|
+
return historyForTranscript(history)
|
|
356
|
+
.filter((message) => message.content.trim().length > 0)
|
|
357
|
+
.map((message) => ({ role: message.role, content: message.content,
|
|
358
|
+
...(message.secretRedaction?.secretRedacted ? { secretRedacted: true } : {}),
|
|
359
|
+
}));
|
|
360
|
+
}
|
|
361
|
+
/** The credential block of a BYOK turn; an empty base URL and an unset token cap are omitted. */
|
|
362
|
+
function byokCredentials({ byok }, _optional) {
|
|
363
|
+
return {
|
|
364
|
+
protocol: byok.protocol,
|
|
365
|
+
apiKey: byok.apiKey,
|
|
366
|
+
...(byok.baseUrl ? { baseUrl: byok.baseUrl } : {}),
|
|
367
|
+
model: byok.model,
|
|
368
|
+
...(byok.maxTokens !== undefined ? { maxTokens: byok.maxTokens } : {}),
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
/** The JSON body `POST byokTurnUrl` carries for one turn. */
|
|
372
|
+
function byokTurnBody({ input, byok }, _optional) {
|
|
373
|
+
return {
|
|
374
|
+
messages: byokMessages({ history: input.history }, {}),
|
|
375
|
+
...attachmentIdsField(input),
|
|
376
|
+
...conversationIdField(input),
|
|
377
|
+
agentPluginState: input.context?.["agentPluginState"],
|
|
378
|
+
userPrompt: input.context?.["userPrompt"],
|
|
379
|
+
...pluginRefIdsField(input),
|
|
380
|
+
// The admin screen, as on the Local CLI path (`buildLocalCliContextRef`): the server puts it
|
|
381
|
+
// in front of the newest message so a BYOK turn knows the open page and where drawings go.
|
|
382
|
+
...pageContextField(input),
|
|
383
|
+
byok: byokCredentials({ byok }, {}),
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
/**
|
|
387
|
+
* The error for a refused request: `<action> (<status>)`, plus up to 300 characters of the
|
|
388
|
+
* response body when it has one — shared by the BYOK start, Local CLI start and cancel refusals.
|
|
389
|
+
*/
|
|
390
|
+
async function refusalError({ action, response }, _optional) {
|
|
391
|
+
// A response whose body stream errors out (e.g. a dropped connection) must not turn "the server
|
|
392
|
+
// rejected the request" into "the client crashed reading the rejection".
|
|
393
|
+
const detail = await response.text().catch(() => "");
|
|
394
|
+
return new Error(`${action} (${response.status})${detail ? `: ${detail.slice(0, 300)}` : ""}`);
|
|
395
|
+
}
|
|
296
396
|
/**
|
|
297
397
|
* The BYOK run path (2026-08-04) — one held-open `POST` to `assistant-byok.ts`, no separate
|
|
298
398
|
* `EventSource`/reattach. See module doc's path-2 section for why this shape differs from
|
|
@@ -307,35 +407,13 @@ export function createAssistantTransportClient({ ports }, _options = {}) {
|
|
|
307
407
|
const controller = new AbortController();
|
|
308
408
|
byokAbortControllers.set(runId, controller);
|
|
309
409
|
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
410
|
let response;
|
|
320
411
|
try {
|
|
321
|
-
response = await ports.fetch(
|
|
412
|
+
response = await ports.fetch(ports.byokTurnUrl, {
|
|
322
413
|
method: "POST",
|
|
323
414
|
headers: { "Content-Type": "application/json" },
|
|
324
415
|
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
|
-
}),
|
|
416
|
+
body: JSON.stringify(byokTurnBody({ input, byok }, {})),
|
|
339
417
|
signal: controller.signal,
|
|
340
418
|
});
|
|
341
419
|
}
|
|
@@ -345,8 +423,7 @@ export function createAssistantTransportClient({ ports }, _options = {}) {
|
|
|
345
423
|
}
|
|
346
424
|
if (!response.ok || !response.body) {
|
|
347
425
|
byokAbortControllers.delete(runId);
|
|
348
|
-
|
|
349
|
-
throw new Error(`BYOK turn failed to start (${response.status})${detail ? `: ${detail.slice(0, 300)}` : ""}`);
|
|
426
|
+
throw await refusalError({ action: "BYOK turn failed to start", response }, {});
|
|
350
427
|
}
|
|
351
428
|
const collected = [];
|
|
352
429
|
let settled = false;
|
|
@@ -484,10 +561,14 @@ export function createAssistantTransportClient({ ports }, _options = {}) {
|
|
|
484
561
|
* adopt a conversation because `input.context` named none (the first turn of a chat). Omitted by
|
|
485
562
|
* every caller that already has an id on `input.context`, and by this function's own unit tests —
|
|
486
563
|
* in which case the `context` field is read exactly as before.
|
|
564
|
+
* The bounded `coldStartContext` is the complete prompt, including the current request. The
|
|
565
|
+
* daemon selects it instead of the resume tail when native resume is unavailable.
|
|
487
566
|
*/
|
|
488
567
|
function buildLocalCliContextRef(input, prompt, resolvedConversationId) {
|
|
489
568
|
return {
|
|
490
569
|
prompt: ports.projectPrompt({ prompt, context: input.context }, {}),
|
|
570
|
+
// Select one complete transcript after binding; concatenating it with the tail repeats turns.
|
|
571
|
+
coldStartContext: ports.projectPrompt({ prompt: runPrompt(input.history), context: input.context }, {}),
|
|
491
572
|
// The transcript is already sanitized; retain its signal across the daemon JSON boundary.
|
|
492
573
|
...(input.history.some(message => message.role === "user" && message.secretRedaction?.secretRedacted) ? { secretRedacted: true } : {}),
|
|
493
574
|
assistantMessageId: input.assistantMessageId,
|
|
@@ -496,6 +577,8 @@ export function createAssistantTransportClient({ ports }, _options = {}) {
|
|
|
496
577
|
...reasoningField(input),
|
|
497
578
|
...attachmentIdsField(input),
|
|
498
579
|
...pluginRefIdsField(input),
|
|
580
|
+
agentPluginState: input.context?.["agentPluginState"],
|
|
581
|
+
userPrompt: input.context?.["userPrompt"],
|
|
499
582
|
...conversationIdField(input, resolvedConversationId),
|
|
500
583
|
...pageContextField(input),
|
|
501
584
|
};
|
|
@@ -523,27 +606,28 @@ export function createAssistantTransportClient({ ports }, _options = {}) {
|
|
|
523
606
|
const adopted = await ensureConversationId({}, {}).catch(() => null);
|
|
524
607
|
return typeof adopted === "string" && adopted.length > 0 ? adopted : undefined;
|
|
525
608
|
}
|
|
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.
|
|
609
|
+
/** Send a resumed agent only its unseen conversation tail, retaining other agents' outcomes.
|
|
610
|
+
* Legacy rows without agent identity retain the existing unanswered-user delivery contract.
|
|
611
|
+
* @complexity O(n) to find the last answered turn; transcript rendering is capped at 40 turns.
|
|
535
612
|
*/
|
|
536
613
|
function resolveLocalCliPrompt(input, resumeCapableAgentIds) {
|
|
537
614
|
const carriesOwnMemory = input.agentId !== undefined && resumeCapableAgentIds?.has(input.agentId) === true;
|
|
615
|
+
if (!carriesOwnMemory)
|
|
616
|
+
return runPrompt(input.history);
|
|
617
|
+
let lastOwnAnswer = -1;
|
|
618
|
+
input.history.forEach((message, index) => {
|
|
619
|
+
if (message.role === 'assistant' && (!message.agentId || message.agentId === input.agentId) && isAnsweredAssistantTurn(message))
|
|
620
|
+
lastOwnAnswer = index;
|
|
621
|
+
});
|
|
622
|
+
const unseen = input.history.slice(lastOwnAnswer + 1);
|
|
623
|
+
const crossedAgents = unseen.some(message => message.role === 'assistant' && message.agentId && message.agentId !== input.agentId);
|
|
538
624
|
// BEHAVIOR CHANGE (2026-09-11, Defect 2): the resume-capable branch was `latestUserPrompt` — the
|
|
539
625
|
// single newest user turn, on the assumption that everything before it is already inside the
|
|
540
626
|
// CLI's own session. That assumption holds only for turns the agent actually answered; a turn
|
|
541
627
|
// whose run died before the CLI read its prompt is in neither place, and nothing ever sent it
|
|
542
628
|
// 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);
|
|
629
|
+
// differs only when there is genuinely something unanswered to catch up on.
|
|
630
|
+
return crossedAgents ? runPrompt(unseen) : undeliveredUserPrompt(input.history);
|
|
547
631
|
}
|
|
548
632
|
/**
|
|
549
633
|
* Writes the newest user turn to durable storage before its run is dispatched, and never fails the
|
|
@@ -562,44 +646,73 @@ export function createAssistantTransportClient({ ports }, _options = {}) {
|
|
|
562
646
|
* two, and a retry of the same content is free. `useAssistantChats`'s own `persistUserTurn` also
|
|
563
647
|
* marks the id written, so in practice `flush` skips it entirely.
|
|
564
648
|
*
|
|
565
|
-
* @param
|
|
566
|
-
*
|
|
649
|
+
* @param message - The newest `role: "user"` message in `startRun`'s history — what gets written.
|
|
650
|
+
* `startRun` has already refused a history without one, so it is always present here.
|
|
567
651
|
* @param conversationId - The already-resolved conversation (see `resolveRunConversationId`), or
|
|
568
652
|
* `undefined` when adoption failed — there is no row to write into in that case.
|
|
569
653
|
* @param persistUserTurn - `AssistantTransportOptions.persistUserTurn`, or `undefined`
|
|
570
654
|
* for a transport built without it (every test and any non-dock consumer).
|
|
571
655
|
* @returns Nothing, always. The `.catch` keeps a failed browser PUT from refusing the user's turn;
|
|
572
656
|
* durable acceptance is now responsible for committing its copy before dispatch.
|
|
573
|
-
* @complexity O(
|
|
657
|
+
* @complexity O(1) plus one PUT.
|
|
574
658
|
*/
|
|
575
|
-
async function persistUserTurnBeforeDispatch(
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
const message = [...history].reverse().find((candidate) => candidate.role === "user");
|
|
580
|
-
if (message === undefined)
|
|
659
|
+
async function persistUserTurnBeforeDispatch({ message, conversationId }, { persistUserTurn }) {
|
|
660
|
+
// Pending composer input is persisted by atomic run acceptance. An earlier PUT could save a
|
|
661
|
+
// rejected (409) draft as a user turn; confirmed/legacy callers retain their existing ordering.
|
|
662
|
+
if (message.inputPending || conversationId === undefined || persistUserTurn === undefined)
|
|
581
663
|
return;
|
|
582
664
|
await persistUserTurn({ conversationId, message }, {}).catch(() => undefined);
|
|
583
665
|
}
|
|
666
|
+
/** The newest `role: "user"` message in a history, or `undefined` when it has none.
|
|
667
|
+
* @complexity O(n) in history length. */
|
|
668
|
+
function newestUserMessage({ history }, _optional) {
|
|
669
|
+
return [...history].reverse().find((message) => message.role === "user");
|
|
670
|
+
}
|
|
584
671
|
/**
|
|
585
672
|
* Turns `startRun`'s Local CLI `POST /api/runs` response into either a thrown, detailed error or a
|
|
586
673
|
* subscribed run — the two outcomes once the request lands. Pulled out of `startRun` (2026-09-04,
|
|
587
674
|
* 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
|
|
675
|
+
* `.text().catch(() => "")` is load-bearing, not decorative — see {@link refusalError}'s inline comment.
|
|
589
676
|
*/
|
|
590
677
|
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
|
-
}
|
|
678
|
+
if (!response.ok)
|
|
679
|
+
throw await refusalError({ action: "agent run failed to start", response }, {});
|
|
597
680
|
const { run, messageId, conversationId } = (await response.json());
|
|
598
681
|
if (messageId && conversationId)
|
|
599
682
|
ports.bindRun({ runId: run.id, binding: { messageId, conversationId } }, {});
|
|
600
683
|
subscribeToRun(run.id, handlers, signal);
|
|
601
684
|
return { runId: run.id };
|
|
602
685
|
}
|
|
686
|
+
/**
|
|
687
|
+
* The Local CLI path of `startRun`: durable user turn, then acceptance, then the followed run.
|
|
688
|
+
*
|
|
689
|
+
* @param input - `startRun`'s input with its conversation identity already merged into `context`.
|
|
690
|
+
* @param userMessage - The newest user message; acceptance carries it verbatim.
|
|
691
|
+
* @param conversationId - The resolved conversation, or `undefined` when adoption failed.
|
|
692
|
+
*/
|
|
693
|
+
async function startLocalCliRun({ input, handlers, userMessage, conversationId }, { options }) {
|
|
694
|
+
// Local CLI path below. `resolveLocalCliPrompt`'s own doc has the full contract for why an
|
|
695
|
+
// unresolved/absent agentId fails open to the full transcript rather than the bare message.
|
|
696
|
+
const prompt = resolveLocalCliPrompt(input, options.getResumeCapableAgentIds?.({}, {}));
|
|
697
|
+
// Also before the POST, and for the same reason it is awaited rather than fired off: the
|
|
698
|
+
// user's turn must be durable before a run that can fail to deliver it exists at all
|
|
699
|
+
// (Defect 2, 2026-09-11). Ordered after conversation adoption because it needs the id that
|
|
700
|
+
// adoption produces. BYOK and AG-UI share the identity above; neither writes a durable
|
|
701
|
+
// user turn ahead of dispatch; those transports retain their own persistence behavior.
|
|
702
|
+
await persistUserTurnBeforeDispatch({ message: userMessage, conversationId }, { persistUserTurn: options.persistUserTurn });
|
|
703
|
+
// Acceptance commits the question and stub together even if the separate browser PUT fails
|
|
704
|
+
// or the tab disappears. Carry the exact message ID/payload, never the rendered prompt.
|
|
705
|
+
const base = buildLocalCliContextRef(input, prompt, conversationId);
|
|
706
|
+
const contextRef = { ...base, userMessage };
|
|
707
|
+
const response = await ports.acceptRun({
|
|
708
|
+
...(input.cancelSignal === undefined ? {} : { signal: input.cancelSignal }),
|
|
709
|
+
requestSignal: input.cancelSignal ?? input.signal,
|
|
710
|
+
body: { contextRef: JSON.stringify(contextRef), agentId: input.agentId, idempotencyKey: base.assistantMessageId },
|
|
711
|
+
messageId: base.assistantMessageId,
|
|
712
|
+
...(conversationId === undefined ? {} : { conversationId }),
|
|
713
|
+
}, {});
|
|
714
|
+
return finishLocalCliRun(response, handlers, input.signal);
|
|
715
|
+
}
|
|
603
716
|
function createAssistantTransport(options = {}) {
|
|
604
717
|
return {
|
|
605
718
|
async startRun(input, handlers) {
|
|
@@ -608,9 +721,13 @@ export function createAssistantTransportClient({ ports }, _options = {}) {
|
|
|
608
721
|
// prompt asks the agent to reply to itself. Shared by both paths below, and reused (rather than
|
|
609
722
|
// recomputed) by the Local CLI branch's own resume-capable check further down.
|
|
610
723
|
const latestUserPrompt = latestUserPromptFromHistory({ history: input.history });
|
|
611
|
-
|
|
724
|
+
const userMessage = newestUserMessage({ history: input.history }, {});
|
|
725
|
+
if (!latestUserPrompt || userMessage === undefined) {
|
|
612
726
|
throw new Error("no user message to send");
|
|
613
727
|
}
|
|
728
|
+
// Adopt identity before either runtime branch so first-turn decisions survive a reload.
|
|
729
|
+
const conversationId = await resolveRunConversationId(input, options.ensureConversationId);
|
|
730
|
+
input = { ...input, context: { ...input.context, ...(conversationId ? { conversationId, agentPluginState: readAgentPluginChatState({ conversationId }) } : {}), userPrompt: latestUserPrompt } };
|
|
614
731
|
const executionConfig = options.getExecutionConfig?.({}, {});
|
|
615
732
|
// Dispatches on MODE alone, not on whether a key is typed in this browser right now
|
|
616
733
|
// (2026-08-05). `executionConfig.byok.apiKey` is write-only server-side — it is empty on
|
|
@@ -623,7 +740,7 @@ export function createAssistantTransportClient({ ports }, _options = {}) {
|
|
|
623
740
|
// level down — the route's own 400 `"no usable BYOK credential..."` — which is a real,
|
|
624
741
|
// actionable answer instead of the mode picker silently refusing to try.
|
|
625
742
|
if (executionConfig?.mode === "byok") {
|
|
626
|
-
return startByokRun(ports.projectInput({ input }, {}), handlers, executionConfig.byok);
|
|
743
|
+
return startByokRun(ports.projectInput({ input }, {}), capturePluginUsage(handlers), executionConfig.byok);
|
|
627
744
|
}
|
|
628
745
|
// AG-UI canary path (ADR-059) — a host-supplied toggle, independent of `executionConfig.mode`
|
|
629
746
|
// (Jini's own closed union, never extended for this). Checked AFTER the byok branch above:
|
|
@@ -633,35 +750,9 @@ export function createAssistantTransportClient({ ports }, _options = {}) {
|
|
|
633
750
|
if (options.getAgUiEnabled?.({}, {})) {
|
|
634
751
|
// `historyForTranscript` here rather than inside `startAgUiRun`: that module importing this
|
|
635
752
|
// 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 }, {});
|
|
753
|
+
return ports.agUi.startAgUiRun({ input: ports.projectInput({ input: { ...input, history: historyForTranscript(input.history) } }, {}), handlers: capturePluginUsage(handlers) }, {});
|
|
637
754
|
}
|
|
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);
|
|
755
|
+
return startLocalCliRun({ input, handlers, userMessage, conversationId }, { options });
|
|
665
756
|
},
|
|
666
757
|
async reattachRun(runId, handlers, options) {
|
|
667
758
|
// A BYOK run has no server-side record to reattach to (module doc's path-2 section) — the
|
|
@@ -688,7 +779,7 @@ export function createAssistantTransportClient({ ports }, _options = {}) {
|
|
|
688
779
|
return null;
|
|
689
780
|
if (ports.agUi.isAgUiRunId({ runId }, {}))
|
|
690
781
|
return ports.agUi.fetchAgUiRunStatus({}, {});
|
|
691
|
-
const response = await ports.fetch(`${
|
|
782
|
+
const response = await ports.fetch(`${ports.runsUrl}/${encodeURIComponent(runId)}`, { credentials: "same-origin" });
|
|
692
783
|
if (!response.ok)
|
|
693
784
|
return null;
|
|
694
785
|
const { run } = (await response.json());
|
|
@@ -704,7 +795,7 @@ export function createAssistantTransportClient({ ports }, _options = {}) {
|
|
|
704
795
|
if (ports.agUi.isAgUiRunId({ runId }, {})) {
|
|
705
796
|
return ports.agUi.stopAgUiRun({ runId }, {});
|
|
706
797
|
}
|
|
707
|
-
const response = await ports.fetch(`${
|
|
798
|
+
const response = await ports.fetch(`${ports.runsUrl}/${encodeURIComponent(runId)}/cancel`, {
|
|
708
799
|
method: "POST",
|
|
709
800
|
headers: { "Content-Type": "application/json" },
|
|
710
801
|
credentials: "same-origin",
|
|
@@ -715,10 +806,8 @@ export function createAssistantTransportClient({ ports }, _options = {}) {
|
|
|
715
806
|
return;
|
|
716
807
|
// `fetch` resolves on any status: without this a refused cancel (401/403/500) resolved exactly
|
|
717
808
|
// 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
|
-
}
|
|
809
|
+
if (!response.ok)
|
|
810
|
+
throw await refusalError({ action: "cancelling agent run failed", response }, {});
|
|
722
811
|
},
|
|
723
812
|
};
|
|
724
813
|
}
|
|
@@ -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
|
}
|