@jini-ai/chat 0.6.15-rc.2 → 0.6.16

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.
Files changed (126) hide show
  1. package/README.md +6 -1
  2. package/dist/core/accepted-input.js +20 -11
  3. package/dist/core/ag-ui/entry.js +3 -1
  4. package/dist/core/agent-plugin-chat-state.js +3 -1
  5. package/dist/core/antigravity/tool-presentation.d.ts +13 -0
  6. package/dist/core/antigravity/tool-presentation.js +20 -0
  7. package/dist/core/assistant-client/ag-ui-transport.js +14 -2
  8. package/dist/core/assistant-client/attachment-liveness.js +1 -1
  9. package/dist/core/assistant-client/durable-run-subscription.js +2 -1
  10. package/dist/core/assistant-client/transport.js +13 -7
  11. package/dist/core/assistant-content.js +2 -1
  12. package/dist/core/composer-history.js +2 -1
  13. package/dist/core/durable-projection.js +2 -1
  14. package/dist/core/durable-runs/continuation.js +2 -1
  15. package/dist/core/events.d.ts +5 -1
  16. package/dist/core/messages.d.ts +5 -1
  17. package/dist/core/partial-json.js +18 -9
  18. package/dist/core/persistence/title.d.ts +1 -1
  19. package/dist/core/persistence/title.js +25 -7
  20. package/dist/core/question-form/normalize.js +1 -1
  21. package/dist/core/question-form/scan.js +25 -15
  22. package/dist/core/question-form/secret-answers.js +2 -1
  23. package/dist/core/run-activity.js +8 -4
  24. package/dist/core/run-events/entry.js +6 -2
  25. package/dist/core/tool-events.d.ts +18 -8
  26. package/dist/core/tool-events.js +72 -16
  27. package/dist/core/tool-presentation.d.ts +14 -0
  28. package/dist/core/tool-presentation.js +8 -0
  29. package/dist/core/transcript.js +13 -6
  30. package/dist/core/turn-usage.js +7 -2
  31. package/dist/core/user-text-redaction.d.ts +4 -6
  32. package/dist/core/user-text-redaction.js +13 -6
  33. package/dist/core/util/manifest.js +13 -11
  34. package/dist/core/util/markdown-context.d.ts +14 -0
  35. package/dist/core/util/markdown-context.js +42 -4
  36. package/dist/core/util/markup-attributes.js +1 -1
  37. package/dist/core/util/parser.js +7 -7
  38. package/dist/core/util/pointer.js +22 -6
  39. package/dist/core/util/recover.js +5 -8
  40. package/dist/core/util/strip.js +12 -15
  41. package/dist/core/util/validate.js +30 -18
  42. package/dist/embed-widget/browser/frontend-session-bridge.d.ts +99 -0
  43. package/dist/embed-widget/browser/frontend-session-bridge.js +384 -0
  44. package/dist/embed-widget/browser/lazy-loader.js +16 -12
  45. package/dist/embed-widget/browser/session-store.d.ts +3 -9
  46. package/dist/embed-widget/browser/session-store.js +6 -38
  47. package/dist/embed-widget/react/dialog.hooks.js +2 -2
  48. package/dist/embed-widget/react/embed.d.ts +3 -10
  49. package/dist/embed-widget/react/embed.js +8 -52
  50. package/dist/embed-widget/react/header.hooks.js +1 -1
  51. package/dist/embed-widget/transports/fetch-sse.js +2 -1
  52. package/dist/embed-widget/vanilla/index.d.ts +0 -5
  53. package/dist/embed-widget/vanilla/index.js +23 -26
  54. package/dist/embed-widget/vanilla/state.d.ts +3 -11
  55. package/dist/embed-widget/vanilla/state.js +10 -39
  56. package/dist/embed-widget/vanilla/style.css +1 -1
  57. package/dist/react/agent-bridge/frontend-session-bridge.d.ts +2 -56
  58. package/dist/react/agent-bridge/frontend-session-bridge.js +2 -248
  59. package/dist/react/components/AttachmentPreviewModal.hooks.js +2 -1
  60. package/dist/react/components/ChatSettingsDialog.js +18 -9
  61. package/dist/react/components/Composer.js +5 -4
  62. package/dist/react/components/ConversationList.js +5 -4
  63. package/dist/react/components/Icon.d.ts +1 -1
  64. package/dist/react/components/Icon.js +1 -0
  65. package/dist/react/components/Markdown.d.ts +1 -0
  66. package/dist/react/components/Markdown.js +30 -27
  67. package/dist/react/components/McpUiSurfaceCard.js +2 -1
  68. package/dist/react/components/MessageList.d.ts +1 -1
  69. package/dist/react/components/MessageList.hooks.js +3 -2
  70. package/dist/react/components/MessageRow.d.ts +3 -0
  71. package/dist/react/components/MessageRow.js +32 -14
  72. package/dist/react/components/MessageRow.usage.js +3 -1
  73. package/dist/react/components/QuestionForm.js +4 -5
  74. package/dist/react/components/SentAttachment.js +4 -1
  75. package/dist/react/components/TodoCard.js +9 -1
  76. package/dist/react/components/ToolCard.d.ts +3 -1
  77. package/dist/react/components/ToolCard.js +84 -64
  78. package/dist/react/components/ToolCard.rules.d.ts +20 -0
  79. package/dist/react/components/ToolCard.rules.js +92 -0
  80. package/dist/react/components/composer-discovery.js +8 -8
  81. package/dist/react/features/chat-pane/components/AgentPluginUsage.js +1 -2
  82. package/dist/react/features/chat-pane/components/AgentRuntimePicker.d.ts +1 -1
  83. package/dist/react/features/chat-pane/components/AgentRuntimePicker.js +31 -9
  84. package/dist/react/features/chat-pane/components/ChatPane.js +7 -5
  85. package/dist/react/features/chat-pane/components/RuntimeDropUp.js +1 -1
  86. package/dist/react/features/chat-pane/create-daemon-attachment-uploader.js +1 -1
  87. package/dist/react/features/chat-pane/hooks/useAgentRuntimePicker.hooks.js +2 -2
  88. package/dist/react/features/chat-pane/hooks/useChatPane.hooks.d.ts +3 -1
  89. package/dist/react/features/chat-pane/hooks/useChatPane.hooks.js +13 -5
  90. package/dist/react/features/chat-pane/hooks/useChatPaneAgentControl.hooks.js +2 -2
  91. package/dist/react/features/chat-pane/hooks/useChatPaneControlsHeight.hooks.js +1 -1
  92. package/dist/react/features/chat-pane/hooks/useChatPaneRuntimeInventory.hooks.js +2 -2
  93. package/dist/react/features/chat-pane/hooks/useChatPaneWorkingDirectory.hooks.js +2 -2
  94. package/dist/react/features/chat-pane/hooks/useRuntimeDropUp.hooks.js +2 -2
  95. package/dist/react/features/chat-pane/model-options.js +4 -2
  96. package/dist/react/features/chat-pane/styles.d.ts +1 -1
  97. package/dist/react/features/chat-pane/styles.js +95 -5
  98. package/dist/react/features/chat-pane/types.d.ts +11 -0
  99. package/dist/react/hooks/assistant-chats.js +22 -8
  100. package/dist/react/hooks/useArtifactStream.js +2 -1
  101. package/dist/react/hooks/useComposer.js +2 -2
  102. package/dist/react/hooks/useComposerHistoryKeys.hooks.js +2 -2
  103. package/dist/react/hooks/useConversation.js +9 -4
  104. package/dist/react/hooks/useExtEventGroups.js +4 -2
  105. package/dist/react/hooks/useLatestOperation.js +2 -1
  106. package/dist/react/hooks/useQuestionForms.js +32 -22
  107. package/dist/react/hooks/useRunStream.js +4 -2
  108. package/dist/react/hooks/useUserTextGuard.d.ts +1 -0
  109. package/dist/react/hooks/useUserTextGuard.js +3 -1
  110. package/dist/react/message-blocks.js +4 -2
  111. package/dist/react/styles/reference.css +9 -0
  112. package/dist/react/tool-call-display.d.ts +11 -0
  113. package/dist/react/tool-call-display.js +59 -2
  114. package/dist/server/durable-runs/recover.js +13 -5
  115. package/dist/server/run-finalizer/entry.js +1 -0
  116. package/dist/store/runtime/chat-history-store.js +9 -3
  117. package/dist/store/runtime/durable-run-store.js +34 -24
  118. package/dist/store/runtime/run-ledger.js +7 -4
  119. package/dist/store/sql/boundary.js +3 -1
  120. package/dist/store/sql/cursor.js +8 -3
  121. package/dist/store/sql/store.js +36 -28
  122. package/package.json +93 -88
  123. package/dist/core/browser/page-actions.d.ts +0 -121
  124. package/dist/core/browser/page-actions.js +0 -115
  125. package/dist/embed-widget/browser/page-actions.d.ts +0 -2
  126. package/dist/embed-widget/browser/page-actions.js +0 -3
package/README.md CHANGED
@@ -197,12 +197,17 @@ required argument objects: e.g. `isTerminalRunStatus({ status })`,
197
197
  `registerExtEventRenderer({ name, renderer }, { slotKey })`. A slot-key callback receives `{ data }`.
198
198
  `interleaveMessageBlocks({ events, content, rows }, { slotOf })` retains its reconstruction checks.
199
199
  The isolated `core/run-events`, `core/ag-ui`, `server/run-finalizer`, `react/embed`,
200
- `transports/fetch-sse`, `browser/session-store` and `browser/page-actions` subpaths are published.
200
+ `transports/fetch-sse`, `browser/session-store` and `browser/frontend-session-bridge` subpaths are published. Page control uses the shared frontend bridge and page executor; the retired directive executor and cross-page action queue have been removed.
201
201
  `FrontendSessionBridgeArgs` is exported from `react`; hosts pass baseUrl and openStream, optionally request or native fetch.
202
202
  The SSE transport takes an injected decoder; browser hosts must supply a browser-safe module.
203
203
 
204
204
  ## React integration boundaries
205
205
 
206
+ `ChatPane` accepts an optional `sendingLabel?: string` for host-localized startup feedback,
207
+ for example `<ChatPane transport={transport} sendingLabel="Connecting to the assistant…" />`.
208
+ The active, sending activity line displays it verbatim until run acceptance. Omitting the prop
209
+ restores the translated `Sending…` label; it has no effect once sending ends.
210
+
206
211
  Model/provider selection belongs to the host-injected `ModelAgentPickerSlot`, because choosing
207
212
  an LLM or agent is a provider concern rather than a chat-domain concern. The unused internal
208
213
  picker was removed after a fresh caller check confirmed that only its own tests reached it.
@@ -15,7 +15,9 @@ export function projectAcceptedInputMessages({ messages }, _options = {}) {
15
15
  });
16
16
  if (inputs.length === 0)
17
17
  return [message];
18
- const { attachments: _attachments, secretRedaction: _redaction, ...turn } = message;
18
+ const turn = { ...message };
19
+ delete turn.attachments;
20
+ delete turn.secretRedaction;
19
21
  // MOVE: Older receipts have no per-input files; retain those legacy files on the final bubble.
20
22
  return inputs.map((input, index) => ({ ...turn, id: input.id, content: input.text,
21
23
  attachments: input.attachments ?? (index === inputs.length - 1 ? message.attachments ?? [] : []), ...(input.secretRedaction ? { secretRedaction: input.secretRedaction } : {}) }));
@@ -86,7 +88,8 @@ function expandQuestionAnswers({ events }, { answerContexts } = {}) {
86
88
  const questions = new Map(events.flatMap(event => event.kind === 'tool_use' ? [[event.id, event.input]] : []));
87
89
  const answers = new Map(events.flatMap(event => {
88
90
  const context = event.kind === 'tool_result' ? answerContexts?.get(event.toolUseId) : undefined;
89
- const input = context ? context.input : event.kind === 'tool_result' ? questions.get(event.toolUseId) : undefined;
91
+ const originalInput = event.kind === 'tool_result' ? questions.get(event.toolUseId) : undefined;
92
+ const input = context ? context.input : originalInput;
90
93
  const answer = questionAnswerEvent({ event, input }, { includeTitle: context?.includeTitle === true });
91
94
  return answer ? [[answer.data.toolUseId, answer]] : [];
92
95
  }));
@@ -121,13 +124,17 @@ function matchOptimisticInput({ input, optimistic, seen }, _options = {}) {
121
124
  const next = candidates?.events[candidates.index];
122
125
  const follows = next?.data.afterInputId === undefined || seen.has(next.data.afterInputId);
123
126
  // Keep predecessor testing before the caller adds this receipt's ids to seen.
124
- return input.data.toolUseId ? optimistic.byQuestion.get(input.data.toolUseId) : follows ? next : undefined;
127
+ if (input.data.toolUseId)
128
+ return optimistic.byQuestion.get(input.data.toolUseId);
129
+ return follows ? next : undefined;
125
130
  }
126
131
  /** Apply authoritative receipt data while retaining local identity and queued constituents. */
127
132
  function mergeAcceptedReceipt({ input, pending }, _options = {}) {
128
133
  // Replayed queue acceptance cannot undo confirmed consumption.
129
134
  const deliveryState = pending.data.deliveryState === 'consumed' ? 'consumed' : input.data.deliveryState ?? pending.data.deliveryState;
130
- const { optimistic: _optimistic, afterInputId: _afterInputId, ...pendingData } = pending.data;
135
+ const pendingData = { ...pending.data };
136
+ delete pendingData.optimistic;
137
+ delete pendingData.afterInputId;
131
138
  const aliases = [...new Set([...(input.data.aliases ?? []), input.data.id, ...(pending.data.aliases ?? [])])].filter(id => id !== pending.data.id);
132
139
  return { ...input, data: { ...pendingData, ...input.data, id: pending.data.id,
133
140
  // Legacy acceptance receipts omit state; HTTP acceptance still means queued until consumption.
@@ -199,8 +206,10 @@ export function projectAcceptedInputEvents({ events }, options) {
199
206
  return projectEvents({ events }, options);
200
207
  // Event arrays are replaced, never mutated, so a transcript update re-projects only changed messages.
201
208
  let projected = defaultProjections.get(events);
202
- if (!projected)
203
- defaultProjections.set(events, projected = projectEvents({ events }, {}));
209
+ if (!projected) {
210
+ projected = projectEvents({ events }, {});
211
+ defaultProjections.set(events, projected);
212
+ }
204
213
  return projected;
205
214
  }
206
215
  const defaultProjections = new WeakMap();
@@ -218,11 +227,11 @@ function projectEvents({ events }, options) {
218
227
  continue;
219
228
  const next = replacements.get(event) ?? event;
220
229
  const input = acceptedInputEvent({ event: next });
221
- if (input && emitted.has(input.data.id))
222
- continue;
223
- if (input)
224
- emitted.add(input.data.id);
225
- projected.push(next);
230
+ if (!input || !emitted.has(input.data.id)) {
231
+ if (input)
232
+ emitted.add(input.data.id);
233
+ projected.push(next);
234
+ }
226
235
  }
227
236
  return projected;
228
237
  }
@@ -6,7 +6,9 @@
6
6
  * The host emits RUN_STARTED and terminal RUN_FINISHED/RUN_ERROR outside this pure projection. */
7
7
  import { EventType } from '@ag-ui/core';
8
8
  function asString(v) {
9
- return typeof v === "string" ? v : v == null ? "" : JSON.stringify(v);
9
+ if (typeof v === "string")
10
+ return v;
11
+ return v == null ? "" : JSON.stringify(v);
10
12
  }
11
13
  function reduceUsagePayload(payload) {
12
14
  const usage = (payload.usage ?? {});
@@ -13,7 +13,9 @@ function isStringList({ value }) {
13
13
  }
14
14
  /** A stored row is usable only when its exclusions and (null or listed) decision are all string ids. */
15
15
  function usableChatState({ value }) {
16
- return value && isStringList({ value: value.excluded }) && (value.decision === null || isStringList({ value: value.decision?.selected })) ? value : null;
16
+ if (!value || !isStringList({ value: value.excluded }))
17
+ return null;
18
+ return value.decision === null || isStringList({ value: value.decision?.selected }) ? value : null;
17
19
  }
18
20
  /** Corrupt/blocked storage permits a fresh question, as clearing localStorage does. */
19
21
  export function readAgentPluginChatState({ conversationId }, { storage = browserStorage() } = {}) {
@@ -0,0 +1,13 @@
1
+ /** Antigravity's saved CLI receipts use capitalized argument keys. Display only: never rewrite history. */
2
+ type Call = {
3
+ name: string;
4
+ input: unknown;
5
+ };
6
+ /** Translate agy's MCP envelope and file arguments into the generic chat presentation contract.
7
+ * Returns undefined for other shapes. Pure; O(k) when copying k file argument keys, otherwise O(1).
8
+ */
9
+ export declare function antigravityToolPresentation({ name, input }: Call, _options?: Record<string, never>): (Call & {
10
+ wrapper?: Call;
11
+ }) | undefined;
12
+ export {};
13
+ //# sourceMappingURL=tool-presentation.d.ts.map
@@ -0,0 +1,20 @@
1
+ /** A transport envelope must name both server and tool before its arguments can be unwrapped. */
2
+ function nonEmptyString(value) {
3
+ return typeof value === 'string' && value.length > 0;
4
+ }
5
+ /** Translate agy's MCP envelope and file arguments into the generic chat presentation contract.
6
+ * Returns undefined for other shapes. Pure; O(k) when copying k file argument keys, otherwise O(1).
7
+ */
8
+ export function antigravityToolPresentation({ name, input }, _options = {}) {
9
+ if (input === null || typeof input !== 'object')
10
+ return undefined;
11
+ const args = input;
12
+ if (name === 'read_file' && typeof args.Path === 'string') {
13
+ return { name, input: { ...args, file_path: args.Path } };
14
+ }
15
+ if (name !== 'call_mcp_tool' || !nonEmptyString(args.ServerName) || !nonEmptyString(args.ToolName))
16
+ return undefined;
17
+ const call = { name: args.ToolName, input: args.Arguments };
18
+ return { ...call, wrapper: call };
19
+ }
20
+ //# sourceMappingURL=tool-presentation.js.map
@@ -1,5 +1,11 @@
1
1
  import { EventType } from "@ag-ui/core";
2
2
  import { createAgUiToAgentTranslationState, translateAgUiEventToAgentEvent } from "../ag-ui/entry.js";
3
+ /** Only the originating attachment can authorize page execution. Read its token for every turn;
4
+ * caller-supplied tool definitions never authorize or execute a transcript event. */
5
+ function frontendBinding({ input }, _options = {}) {
6
+ const token = input.context?.["frontendBindToken"];
7
+ return typeof token === "string" && token.length > 0 ? { frontendBindToken: token } : {};
8
+ }
3
9
  /** Bind the official client without adding its dependency to every chat consumer. */
4
10
  export function createAgUiRunClient({ ports }, _options = {}) {
5
11
  /** In-flight AG-UI turns' driving `HttpAgent`, keyed by the client-minted runId — the ONLY way
@@ -54,8 +60,11 @@ export function createAgUiRunClient({ ports }, _options = {}) {
54
60
  // none yet (e.g. the very first turn).
55
61
  const threadId = input.conversationId ?? ports.mintId({}, {});
56
62
  const agent = ports.createAgent({ url: ports.endpoint, threadId }, {});
63
+ if (input.signal.aborted)
64
+ throw new DOMException("Run cancelled", "AbortError");
57
65
  agUiAgents.set(runId, agent);
58
- input.signal?.addEventListener("abort", () => agent.abortRun());
66
+ const abort = () => agent.abortRun();
67
+ input.signal.addEventListener("abort", abort, { once: true });
59
68
  const messages = input.history.filter((message) => message.content.trim().length > 0).map(toAgUiMessage);
60
69
  const runAgentInput = {
61
70
  threadId,
@@ -65,8 +74,9 @@ export function createAgUiRunClient({ ports }, _options = {}) {
65
74
  tools: [],
66
75
  context: [],
67
76
  forwardedProps: {
77
+ ...frontendBinding({ input }),
68
78
  ...(input.agentId ? { agentId: input.agentId } : {}),
69
- ...(input.history.some(message => message.role === "user" && message.secretRedaction?.secretRedacted) ? { secretRedacted: true } : {}),
79
+ ...(input.history.findLast(message => message.role === "user")?.secretRedaction?.secretRedacted ? { secretRedacted: true } : {}),
70
80
  },
71
81
  };
72
82
  const collected = [];
@@ -76,6 +86,7 @@ export function createAgUiRunClient({ ports }, _options = {}) {
76
86
  return;
77
87
  settled = true;
78
88
  agUiAgents.delete(runId);
89
+ input.signal.removeEventListener("abort", abort);
79
90
  handlers.onDone(collected);
80
91
  };
81
92
  const state = createAgUiToAgentTranslationState({ customEventNames: ports.customEventNames }, {});
@@ -96,6 +107,7 @@ export function createAgUiRunClient({ ports }, _options = {}) {
96
107
  },
97
108
  error: (error) => {
98
109
  agUiAgents.delete(runId);
110
+ input.signal.removeEventListener("abort", abort);
99
111
  const err = error instanceof Error ? error : new Error(String(error));
100
112
  if (!startSettled) {
101
113
  startSettled = true;
@@ -47,7 +47,7 @@ async function isAttachmentStillServed(ref, options) {
47
47
  * @complexity O(n) requests, at most `concurrency` concurrent; O(n) space for the verdicts.
48
48
  */
49
49
  async function probeAll(refs, concurrency, probe) {
50
- const verdicts = new Array(refs.length).fill(false);
50
+ const verdicts = Array.from({ length: refs.length }, () => false);
51
51
  const entries = refs.entries();
52
52
  async function worker() {
53
53
  for (const [index, ref] of entries) {
@@ -29,7 +29,8 @@ export function followDurableRun({ runId: initialRunId, handlers, signal, bindin
29
29
  handlers.onDone(message.events ?? []);
30
30
  }
31
31
  function open() {
32
- const epoch = ++streamEpoch;
32
+ streamEpoch += 1;
33
+ const epoch = streamEpoch;
33
34
  dropped = false;
34
35
  stopStream?.();
35
36
  stopStream = ports.stream({ runId, cursor,
@@ -293,7 +293,9 @@ export function createAssistantTransportClient({ ports }, _options = {}) {
293
293
  const payload = JSON.parse(frame.data);
294
294
  ctx.handlers.onError(new Error(asString(payload.message) || "BYOK turn failed"));
295
295
  }
296
- else if (frame.event === "end") {
296
+ else {
297
+ if (frame.event !== "end")
298
+ return;
297
299
  const notice = terminalReasonNotice(readTerminalReason(frame.data, false));
298
300
  if (notice) {
299
301
  ctx.collected.push(notice);
@@ -391,7 +393,8 @@ export function createAssistantTransportClient({ ports }, _options = {}) {
391
393
  // A response whose body stream errors out (e.g. a dropped connection) must not turn "the server
392
394
  // rejected the request" into "the client crashed reading the rejection".
393
395
  const detail = await response.text().catch(() => "");
394
- return new Error(`${action} (${response.status})${detail ? `: ${detail.slice(0, 300)}` : ""}`);
396
+ const suffix = detail ? ": " + detail.slice(0, 300) : "";
397
+ return new Error(`${action} (${response.status})${suffix}`);
395
398
  }
396
399
  /**
397
400
  * The BYOK run path (2026-08-04) — one held-open `POST` to `assistant-byok.ts`, no separate
@@ -573,8 +576,9 @@ export function createAssistantTransportClient({ ports }, _options = {}) {
573
576
  prompt: ports.projectPrompt({ prompt, context: input.context }, {}),
574
577
  // Select one complete transcript after binding; concatenating it with the tail repeats turns.
575
578
  coldStartContext: ports.projectPrompt({ prompt: runPrompt(input.history), context: input.context }, {}),
576
- // The transcript is already sanitized; retain its signal across the daemon JSON boundary.
577
- ...(input.history.some(message => message.role === "user" && message.secretRedaction?.secretRedacted) ? { secretRedacted: true } : {}),
579
+ // The transcript is already sanitized; retain only the CURRENT user's signal across the
580
+ // daemon JSON boundary. Replaying an old signal reports another paste on every later turn.
581
+ ...(newestUserMessage({ history: input.history }, {})?.secretRedaction?.secretRedacted ? { secretRedacted: true } : {}),
578
582
  assistantMessageId: input.assistantMessageId,
579
583
  ...frontendBindTokenField(input),
580
584
  ...modelField(input),
@@ -766,7 +770,7 @@ export function createAssistantTransportClient({ ports }, _options = {}) {
766
770
  // can never arrive.
767
771
  if (runId.startsWith(BYOK_RUN_ID_PREFIX)) {
768
772
  handlers.onDone([]);
769
- return;
773
+ return undefined;
770
774
  }
771
775
  // Same "no server-side record to resume" reasoning as the BYOK branch above — see
772
776
  // `assistant-transport-ag-ui.ts`'s own doc for why this path has no reattach story yet.
@@ -774,6 +778,7 @@ export function createAssistantTransportClient({ ports }, _options = {}) {
774
778
  return ports.agUi.reattachAgUiRun({ handlers }, {});
775
779
  }
776
780
  subscribeToRun(runId, handlers, options?.signal);
781
+ return undefined;
777
782
  },
778
783
  async fetchRunStatus(runId) {
779
784
  // Same reasoning as `reattachRun` above — no server-side run record exists for a BYOK run id,
@@ -794,7 +799,7 @@ export function createAssistantTransportClient({ ports }, _options = {}) {
794
799
  // this exact id — there is no server-side run to `POST .../cancel` against.
795
800
  if (runId.startsWith(BYOK_RUN_ID_PREFIX)) {
796
801
  byokAbortControllers.get(runId)?.abort();
797
- return;
802
+ return undefined;
798
803
  }
799
804
  if (ports.agUi.isAgUiRunId({ runId }, {})) {
800
805
  return ports.agUi.stopAgUiRun({ runId }, {});
@@ -807,11 +812,12 @@ export function createAssistantTransportClient({ ports }, _options = {}) {
807
812
  });
808
813
  // 404: the daemon no longer knows this run (it restarted), so nothing is left running to stop.
809
814
  if (response.status === 404)
810
- return;
815
+ return undefined;
811
816
  // `fetch` resolves on any status: without this a refused cancel (401/403/500) resolved exactly
812
817
  // like a successful one while the run kept executing. `useRunStream` logs a rejection here.
813
818
  if (!response.ok)
814
819
  throw await refusalError({ action: "cancelling agent run failed", response }, {});
820
+ return undefined;
815
821
  },
816
822
  };
817
823
  }
@@ -32,8 +32,9 @@ export function assistantContentFromEvents({ events }, _options = {}) {
32
32
  for (const ev of dedupeToolUsesById({ events: events ? [...events] : undefined })) {
33
33
  if (isAssistantStepBoundary({ event: ev })) {
34
34
  stepBoundary = true;
35
+ continue;
35
36
  }
36
- else if (ev.kind === 'text' && ev.text.length > 0) {
37
+ if (ev.kind === 'text' && ev.text.length > 0) {
37
38
  out += (stepBoundary ? separatorBefore({ out, text: ev.text }) : '') + ev.text;
38
39
  stepBoundary = false;
39
40
  }
@@ -21,7 +21,8 @@ export function createComposerHistoryState(_required, _optional = {}) {
21
21
  return { entries: [], index: null, draft: null };
22
22
  }
23
23
  function hasModifierOrPopup({ input }, _optional = {}) {
24
- return Boolean(input.shiftKey || input.ctrlKey || input.altKey || input.metaKey || input.isComposing || input.popupOpen);
24
+ const modified = input.shiftKey || input.ctrlKey || input.altKey || input.metaKey;
25
+ return Boolean(modified || input.isComposing || input.popupOpen);
25
26
  }
26
27
  /**
27
28
  * Whether the selection sits on the first/last line. A selection spanning a newline is on
@@ -52,8 +52,9 @@ export function mergeRunEvents({ saved, incoming }, _optional = {}) {
52
52
  skip = Math.max(0, skip - event.text.length);
53
53
  if (text)
54
54
  result.push({ ...event, text });
55
+ continue;
55
56
  }
56
- else if (claimNewKey({ key: identity(event, incomingUsage), keys })) {
57
+ if (claimNewKey({ key: identity(event, incomingUsage), keys })) {
57
58
  result.push(event);
58
59
  }
59
60
  }
@@ -6,7 +6,8 @@ export function canonicalToolInput({ value }, _optional = {}) {
6
6
  return `[${value.map((item) => canonicalToolInput({ value: item }, {})).join(",")}]`;
7
7
  if (value !== null && typeof value === "object") {
8
8
  const object = value;
9
- return `{${Object.keys(object).sort().map((key) => `${JSON.stringify(key)}:${canonicalToolInput({ value: object[key] }, {})}`).join(",")}}`;
9
+ const fields = Object.keys(object).sort().map((key) => `${JSON.stringify(key)}:${canonicalToolInput({ value: object[key] }, {})}`);
10
+ return `{${fields.join(",")}}`;
10
11
  }
11
12
  return JSON.stringify(value) ?? "null";
12
13
  }
@@ -17,7 +17,7 @@
17
17
  * not depend on `@jini-ai/protocol` (or, by the same reasoning, on `@jini-ai/daemon`), the same
18
18
  * "translate the wire, don't import it" posture `assistant-ag-ui.ts` documents for its own port of
19
19
  * this package's vocabulary in the other direction. Keep in sync by hand if the daemon's shape
20
- * changes — there are exactly two fields, so drift is easy to spot in review.
20
+ * changes — the image arm has two required and two optional fields, so drift is easy to spot in review.
21
21
  */
22
22
  export type ToolResultMediaBlock = {
23
23
  readonly type: 'text';
@@ -26,6 +26,10 @@ export type ToolResultMediaBlock = {
26
26
  readonly type: 'image';
27
27
  readonly mimeType: string;
28
28
  readonly data: string;
29
+ /** Optional caption and alt text (e.g. the media item's title). */
30
+ readonly title?: string;
31
+ /** Optional link to the full file; the renderer only links a safe (http(s) or same-site) target. */
32
+ readonly href?: string;
29
33
  };
30
34
  /**
31
35
  * A single renderable unit of agent output. Covers every generic variant a
@@ -25,6 +25,10 @@ export interface ChatAttachment {
25
25
  /** User-visible attachment order for this turn; older items may omit it. */
26
26
  order?: number;
27
27
  }
28
+ export interface SecretRedactionSignal {
29
+ readonly secretRedacted: true;
30
+ readonly count: number;
31
+ }
28
32
  /**
29
33
  * A single turn in a conversation. This is the generic subset of OD's
30
34
  * `ChatMessage` (`packages/contracts/src/api/chat.ts`): product-shaped
@@ -36,7 +40,7 @@ export interface ChatMessage {
36
40
  /** A normal send stays outside recall until the transport accepts it. */
37
41
  inputPending?: boolean;
38
42
  /** Transient safe receipt; storage requires no new column. */
39
- secretRedaction?: import('./user-text-redaction.js').SecretRedactionSignal;
43
+ secretRedaction?: SecretRedactionSignal;
40
44
  id: string;
41
45
  role: ChatRole;
42
46
  content: string;
@@ -38,14 +38,22 @@ function scanJsonPrefix({ buf }, _optional = {}) {
38
38
  ({ inStr, esc } = advanceJsonString({ c, esc }));
39
39
  continue;
40
40
  }
41
- if (c === '"')
42
- inStr = true;
43
- else if (c === '{')
44
- stack.push('}');
45
- else if (c === '[')
46
- stack.push(']');
47
- else if (c === '}' || c === ']')
48
- stack.pop();
41
+ switch (c) {
42
+ case '"':
43
+ inStr = true;
44
+ break;
45
+ case '{':
46
+ stack.push('}');
47
+ break;
48
+ case '[':
49
+ stack.push(']');
50
+ break;
51
+ case '}':
52
+ case ']':
53
+ stack.pop();
54
+ break;
55
+ default: break;
56
+ }
49
57
  }
50
58
  return { stack, inStr };
51
59
  }
@@ -76,7 +84,8 @@ function trimIncompleteTail({ out }, _optional = {}) {
76
84
  let prev;
77
85
  let passes = 0;
78
86
  do {
79
- if (++passes > MAX_TAIL_CLEANUP_PASSES)
87
+ passes += 1;
88
+ if (passes > MAX_TAIL_CLEANUP_PASSES)
80
89
  return null;
81
90
  prev = out;
82
91
  out = trimTrailingSeparators({ text: out });
@@ -13,7 +13,7 @@
13
13
  * Ported from Open Design's `summarizeProjectNameFromPrompt` (`apps/web/src/utils/projectName.ts`).
14
14
  * The Latin path has since diverged: it keeps the prompt's words, names and dotted domains (the
15
15
  * original's stop-word removal and title casing turned "port example.com to jini" into "Port
16
- * Example Com Jini"), and `LEADING_LATIN_FILLER` fixes the original's article bug, documented at
16
+ * Example Com Jini"), and `stripLeadingLatinFiller` fixes the original's article bug, documented at
17
17
  * its declaration. The CJK path keeps the original's filler and punctuation rules, which is not
18
18
  * decoration: they differ enough from the Latin ones that a shared path produced worse titles in both.
19
19
  */
@@ -13,7 +13,7 @@
13
13
  * Ported from Open Design's `summarizeProjectNameFromPrompt` (`apps/web/src/utils/projectName.ts`).
14
14
  * The Latin path has since diverged: it keeps the prompt's words, names and dotted domains (the
15
15
  * original's stop-word removal and title casing turned "port example.com to jini" into "Port
16
- * Example Com Jini"), and `LEADING_LATIN_FILLER` fixes the original's article bug, documented at
16
+ * Example Com Jini"), and `stripLeadingLatinFiller` fixes the original's article bug, documented at
17
17
  * its declaration. The CJK path keeps the original's filler and punctuation rules, which is not
18
18
  * decoration: they differ enough from the Latin ones that a shared path produced worse titles in both.
19
19
  */
@@ -38,7 +38,16 @@ const CJK_PATTERN = /[㐀-鿿]/;
38
38
  * with `a` after a verb silently lost its first character. Requiring the separator as part of the
39
39
  * article makes an article an article rather than a prefix.
40
40
  */
41
- const LEADING_LATIN_FILLER = /^(please\s+)?(can\s+you\s+|could\s+you\s+|help\s+me\s+|i\s+want\s+to\s+|i\s+need\s+to\s+)?(create|build|make|design|implement|add|fix|update|improve|optimize|generate|write|search|find|show|list|turn)\s+((this|that)\s+into\s+)?((an|a|the|this|that|my|me)\s+)?/i;
41
+ function stripLeadingLatinFiller(input) {
42
+ const polite = input.replace(/^please\s+/i, '');
43
+ const request = polite.replace(/^(?:can\s+you|could\s+you|help\s+me|i\s+want\s+to|i\s+need\s+to)\s+/i, '');
44
+ const verb = /^(?:create|build|make|design|implement|add|fix|update|improve|optimize|generate|write|search|find|show|list|turn)\s+/i.exec(request);
45
+ if (!verb)
46
+ return input;
47
+ return request.slice(verb[0].length)
48
+ .replace(/^(?:this|that)\s+into\s+/i, '')
49
+ .replace(/^(?:an|a|the|this|that|my|me)\s+/i, '');
50
+ }
42
51
  const LEADING_CJK_FILLER = [
43
52
  /^先?(帮我|帮忙|麻烦|请|可以|能不能|能否|给我|我想要|我要)/,
44
53
  /^(先)?(实现|做|做一下|创建|生成|设计|开发|新增|添加|优化|修复|改|更改|调整)(一下|一个|一版|下)?/,
@@ -72,11 +81,11 @@ export function stripConversationTitleMarkdown({ text }, _options = {}) {
72
81
  .replace(/^\s{0,3}\[[^\]\n]+\]:[^\n]*(?:\n|$)/gm, ' ')
73
82
  .replace(/!?\[([^\[\]\n]+)\]\((?:[^()[\]\n]|\([^()[\]\n]*\))*(?:\)|$)/g, '$1')
74
83
  .replace(/\[([^\[\]\n]+)\]\[[^\[\]\n]*(?:\]|$)/g, '$1')
75
- .replace(/^\s{0,3}#{1,6}\s+|[ \t]+#+[ \t]*$/gm, '')
84
+ .replace(/^\s{0,3}#{1,6}\s+|(?<![ \t])[ \t]+#+[ \t]*$/gm, '')
76
85
  .replace(/(?<![\w\\*])(\*\*|\*)(?=[^\s*])([^*\n]*?[^\s*\\])\1(?![\w*])/g, '$2')
77
- .replace(/(?<![\w\\_])(__|_)(?=[^\s_])([^\n]*?[^\s\\])\1(?![\w_])/g, '$2')
86
+ .replace(/(?<![\w\\])(__|_)(?=[^\s_])([^\n]*?[^\s\\])\1(?!\w)/g, '$2')
78
87
  .replace(/`|~~/g, '')
79
- .replace(/!?\[([^\]\n]*)(?=\n|$)/g, '$1')
88
+ .split('\n').map(stripUnclosedTitleLink).join('\n')
80
89
  // Unescape after markup removal so an escaped literal marker cannot become emphasis.
81
90
  .replace(/\\([\\`*_[\]~])/g, '$1')
82
91
  .replace(/\s+/g, ' ')
@@ -97,7 +106,7 @@ function truncateTitle(title) {
97
106
  return (lastSpace > 0 ? prefix.slice(0, lastSpace) : prefix).trimEnd();
98
107
  }
99
108
  function trimLatinTitle(input) {
100
- const words = input.replace(LEADING_LATIN_FILLER, '').match(/[\p{L}\p{N}]+(?:[._-][\p{L}\p{N}]+)*/gu) ?? [];
109
+ const words = stripLeadingLatinFiller(input).match(/[\p{L}\p{N}]+(?:[._-][\p{L}\p{N}]+)*/gu) ?? [];
101
110
  const title = words.join(' ');
102
111
  // Domain and filename tokens retain their original case, even at the start of a sentence.
103
112
  return words[0]?.includes('.') ? title : title.charAt(0).toUpperCase() + title.slice(1);
@@ -107,7 +116,7 @@ function trimCjkTitle(input) {
107
116
  for (const pattern of LEADING_CJK_FILLER)
108
117
  title = title.replace(pattern, '').trim();
109
118
  // Cut at the first sentence-ending punctuation, then drop spacing — CJK titles do not use it.
110
- title = title.replace(/[,。!?;:,.!?;:].*$/, '').replace(/\s+/g, '');
119
+ title = title.split(/[,。!?;:,.!?;:]/, 1)[0].replace(/\s+/g, '');
111
120
  return title.slice(0, MAX_CJK_LENGTH);
112
121
  }
113
122
  /**
@@ -144,4 +153,13 @@ export function conversationTitleForDisplay({ title, titleSource, firstPrompt },
144
153
  }
145
154
  return truncateTitle(stripConversationTitleMarkdown({ text: title })) || null;
146
155
  }
156
+ /** The first open label after the last closing bracket is the only possible trailing cut link.
157
+ * Retain any later '[' characters inside its label, as the original tolerant pattern does. */
158
+ function stripUnclosedTitleLink(line) {
159
+ const open = line.indexOf('[', line.lastIndexOf(']') + 1);
160
+ if (open === -1)
161
+ return line;
162
+ const start = line[open - 1] === '!' ? open - 1 : open;
163
+ return line.slice(0, start) + line.slice(open + 1);
164
+ }
147
165
  //# sourceMappingURL=title.js.map
@@ -135,7 +135,7 @@ const COLOR_KEYWORD = /^[a-z]+$/i;
135
135
  * A colour function. The argument charset deliberately excludes `(`, so no nested function —
136
136
  * `var()`, `url()`, `image-set()`, `attr()` — can appear inside one of these either.
137
137
  */
138
- const COLOR_FUNCTION = /^(?:rgb|rgba|hsl|hsla|hwb|lab|lch|oklab|oklch|color)\(\s*[0-9a-z%.,/\s+-]*\)$/i;
138
+ const COLOR_FUNCTION = /^(?:rgb|rgba|hsl|hsla|hwb|lab|lch|oklab|oklch|color)\([0-9a-z%.,/\s+-]*\)$/i;
139
139
  /**
140
140
  * Whether a model-supplied palette entry may be written into a CSS value.
141
141
  *
@@ -197,7 +197,7 @@ export function parsePartialQuestionForm({ input }) {
197
197
  // body — but only when those backticks are the closing wrapper, not content
198
198
  // of a JSON string value still being typed.
199
199
  function stripTrailingFence(body) {
200
- const m = /\s*`{1,3}\s*$/.exec(body);
200
+ const m = /(?<!\s)\s*`{1,3}\s*$/.exec(body);
201
201
  if (!m)
202
202
  return body;
203
203
  const before = body.slice(0, m.index);
@@ -206,10 +206,12 @@ function stripTrailingFence(body) {
206
206
  return before;
207
207
  }
208
208
  function endsInsideJsonString(s) {
209
- for (let i = s.indexOf('"'); i !== -1; i = s.indexOf('"', i)) {
210
- i = jsonStringEnd({ s, start: i });
211
- if (i === -1)
209
+ let i = s.indexOf('"');
210
+ while (i !== -1) {
211
+ const end = jsonStringEnd({ s, start: i });
212
+ if (end === -1)
212
213
  return true;
214
+ i = s.indexOf('"', end);
213
215
  }
214
216
  return false;
215
217
  }
@@ -217,9 +219,11 @@ function endsInsideJsonString(s) {
217
219
  // quote is at `start`, or -1 when that literal hasn't finished streaming.
218
220
  function jsonStringEnd({ s, start }) {
219
221
  for (let i = start + 1; i < s.length; i++) {
220
- if (s[i] === '\\')
222
+ if (s[i] === '\\') {
221
223
  i++;
222
- else if (s[i] === '"')
224
+ continue;
225
+ }
226
+ if (s[i] === '"')
223
227
  return i + 1;
224
228
  }
225
229
  return -1;
@@ -231,10 +235,12 @@ const DEPTH_DELTA = { '{': 1, '[': 1, '}': -1, ']': -1 };
231
235
  function completeTopLevelString(body, field) {
232
236
  const marker = `"${field}"`;
233
237
  let depth = 0;
234
- for (let i = 0; i < body.length; i++) {
238
+ let i = 0;
239
+ while (i < body.length) {
235
240
  const c = body[i];
236
241
  if (c !== '"') {
237
242
  depth += DEPTH_DELTA[c] ?? 0;
243
+ i += 1;
238
244
  continue;
239
245
  }
240
246
  const valueStart = depth === 1 ? keyValueStart({ body, at: i, marker }) : -1;
@@ -244,7 +250,7 @@ function completeTopLevelString(body, field) {
244
250
  const end = jsonStringEnd({ s: body, start: i });
245
251
  if (end === -1)
246
252
  return undefined;
247
- i = end - 1;
253
+ i = end;
248
254
  }
249
255
  return undefined;
250
256
  }
@@ -322,20 +328,24 @@ function countClosedQuestionObjects(body) {
322
328
  // it never closes (string-aware so braces inside strings don't count).
323
329
  function balancedObjectEnd({ s, start }) {
324
330
  let depth = 0;
325
- for (let i = start; i < s.length; i++) {
331
+ let i = start;
332
+ while (i < s.length) {
326
333
  const c = s[i];
327
334
  if (c === '"') {
328
335
  const end = jsonStringEnd({ s, start: i });
329
336
  if (end === -1)
330
337
  return -1;
331
- i = end - 1;
332
- }
333
- else if (c === '{') {
334
- depth++;
338
+ i = end;
339
+ continue;
335
340
  }
336
- else if (c === '}' && --depth === 0) {
337
- return i + 1;
341
+ if (c === '{')
342
+ depth += 1;
343
+ if (c === '}') {
344
+ depth -= 1;
345
+ if (depth === 0)
346
+ return i + 1;
338
347
  }
348
+ i += 1;
339
349
  }
340
350
  return -1;
341
351
  }
@@ -31,7 +31,8 @@ export function withoutSecretQuestionDefaults({ form }) {
31
31
  return { ...form, questions: form.questions.map(question => {
32
32
  if (!isSecretQuestion({ question }))
33
33
  return question;
34
- const { defaultValue: _defaultValue, ...safe } = question;
34
+ const safe = { ...question };
35
+ delete safe.defaultValue;
35
36
  return safe;
36
37
  }) };
37
38
  }