@jini-ai/chat 0.6.15 → 0.6.17

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 (75) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/README.md +6 -1
  3. package/dist/core/accepted-input.d.ts +12 -0
  4. package/dist/core/accepted-input.js +29 -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 +13 -1
  8. package/dist/core/assistant-client/transport.js +2 -0
  9. package/dist/core/events.d.ts +5 -1
  10. package/dist/core/messages.d.ts +2 -0
  11. package/dist/core/tool-events.d.ts +18 -8
  12. package/dist/core/tool-events.js +72 -16
  13. package/dist/core/tool-presentation.d.ts +14 -0
  14. package/dist/core/tool-presentation.js +8 -0
  15. package/dist/core/transport.d.ts +2 -0
  16. package/dist/core/user-text-redaction.d.ts +2 -1
  17. package/dist/core/user-text-redaction.js +37 -4
  18. package/dist/embed-widget/browser/frontend-session-bridge.d.ts +99 -0
  19. package/dist/embed-widget/browser/frontend-session-bridge.js +384 -0
  20. package/dist/embed-widget/browser/session-store.d.ts +3 -9
  21. package/dist/embed-widget/browser/session-store.js +6 -38
  22. package/dist/embed-widget/react/embed.d.ts +3 -10
  23. package/dist/embed-widget/react/embed.js +8 -52
  24. package/dist/embed-widget/vanilla/index.d.ts +0 -5
  25. package/dist/embed-widget/vanilla/index.js +4 -14
  26. package/dist/embed-widget/vanilla/state.d.ts +3 -11
  27. package/dist/embed-widget/vanilla/state.js +6 -37
  28. package/dist/embed-widget/vanilla/style.css +1 -1
  29. package/dist/react/agent-bridge/frontend-session-bridge.d.ts +2 -56
  30. package/dist/react/agent-bridge/frontend-session-bridge.js +2 -248
  31. package/dist/react/components/Composer.js +2 -1
  32. package/dist/react/components/CopyMessageButton.d.ts +4 -1
  33. package/dist/react/components/CopyMessageButton.js +3 -2
  34. package/dist/react/components/FolderChips.d.ts +13 -0
  35. package/dist/react/components/FolderChips.js +32 -0
  36. package/dist/react/components/Icon.d.ts +1 -1
  37. package/dist/react/components/Icon.js +2 -0
  38. package/dist/react/components/Markdown.d.ts +1 -0
  39. package/dist/react/components/Markdown.js +1 -1
  40. package/dist/react/components/MessageList.d.ts +2 -1
  41. package/dist/react/components/MessageList.js +2 -1
  42. package/dist/react/components/MessageRow.d.ts +3 -0
  43. package/dist/react/components/MessageRow.js +12 -9
  44. package/dist/react/components/MidRunUserMessage.js +3 -2
  45. package/dist/react/components/ToolCard.d.ts +3 -1
  46. package/dist/react/components/ToolCard.js +77 -58
  47. package/dist/react/components/ToolCard.rules.d.ts +20 -0
  48. package/dist/react/components/ToolCard.rules.js +92 -0
  49. package/dist/react/features/chat-pane/components/AgentRuntimePicker.js +18 -6
  50. package/dist/react/features/chat-pane/components/ChatPane.js +4 -3
  51. package/dist/react/features/chat-pane/hooks/useChatPane.hooks.d.ts +3 -1
  52. package/dist/react/features/chat-pane/hooks/useChatPane.hooks.js +42 -19
  53. package/dist/react/features/chat-pane/styles.d.ts +1 -1
  54. package/dist/react/features/chat-pane/styles.js +96 -9
  55. package/dist/react/features/chat-pane/types.d.ts +7 -0
  56. package/dist/react/hooks/composer-draft-cache.d.ts +9 -6
  57. package/dist/react/hooks/composer-draft-cache.js +24 -0
  58. package/dist/react/hooks/useComposer.d.ts +7 -0
  59. package/dist/react/hooks/useComposer.js +31 -4
  60. package/dist/react/hooks/useConversation.d.ts +2 -0
  61. package/dist/react/hooks/useConversation.js +4 -1
  62. package/dist/react/hooks/useUserTextGuard.d.ts +1 -0
  63. package/dist/react/hooks/useUserTextGuard.js +3 -1
  64. package/dist/react/styles/reference.css +6 -0
  65. package/dist/react/tool-call-display.d.ts +11 -0
  66. package/dist/react/tool-call-display.js +59 -2
  67. package/dist/store/legacy/sqlite/messages.d.ts +12 -10
  68. package/dist/store/legacy/sqlite/messages.js +4 -1
  69. package/dist/store/runtime/durable-run-store.js +35 -3
  70. package/dist/store/sql/store.js +5 -1
  71. package/package.json +91 -86
  72. package/dist/core/browser/page-actions.d.ts +0 -121
  73. package/dist/core/browser/page-actions.js +0 -115
  74. package/dist/embed-widget/browser/page-actions.d.ts +0 -2
  75. package/dist/embed-widget/browser/page-actions.js +0 -3
package/CHANGELOG.md CHANGED
@@ -1,3 +1,12 @@
1
+ ## 0.6.17
2
+
3
+ ### Patch Changes
4
+
5
+ - Carry picked folders as per-message folder references, with draft and sent-message chips, persistence and replay.
6
+ - Preserve ordinary typed paths while redacting glued-on secrets and slash-containing keys.
7
+ - Handle npm 12 pack JSON through the shared parser in isolated-consumer tests.
8
+ - Treat Node builtin dependencies as builtins when copying isolated runtime fixtures.
9
+
1
10
  ## 0.6.13
2
11
 
3
12
  ### Patch Changes
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.
@@ -9,6 +9,8 @@ export interface AcceptedChatInput {
9
9
  answerText?: string;
10
10
  /** Absent for legacy/ordinary inputs; consumed only after a CLI UUID receipt. */
11
11
  deliveryState?: 'queued' | 'consumed';
12
+ /** Picked directory paths, kept byte-exact and separate from upload references. */
13
+ folders?: string[];
12
14
  attachments?: ChatAttachment[];
13
15
  secretRedaction?: ChatMessage['secretRedaction'];
14
16
  /** Wire ids replaced by this receipt remain seen by recall. */
@@ -71,5 +73,15 @@ export declare function projectAcceptedInputEvents({ events }: {
71
73
  export declare function acceptedInputsFromMessages({ messages }: {
72
74
  messages: readonly ChatMessage[];
73
75
  }, _options?: {}): AcceptedChatInput[];
76
+ /** Preserve folder metadata through existing event JSON storage; no attachment lifecycle or schema.
77
+ * Existing per-input receipts already own queued variants and must retain their identities.
78
+ */
79
+ export declare function withFolderReceipt({ message }: {
80
+ message: ChatMessage;
81
+ }, _options?: {}): ChatMessage;
82
+ /** Hydrate the message's folder list from its accepted receipts after a database read. */
83
+ export declare function foldersFromReceipts({ events }: {
84
+ events: unknown;
85
+ }, _options?: {}): string[];
74
86
  export {};
75
87
  //# sourceMappingURL=accepted-input.d.ts.map
@@ -17,9 +17,11 @@ export function projectAcceptedInputMessages({ messages }, _options = {}) {
17
17
  return [message];
18
18
  const turn = { ...message };
19
19
  delete turn.attachments;
20
+ delete turn.folders;
20
21
  delete turn.secretRedaction;
21
22
  // MOVE: Older receipts have no per-input files; retain those legacy files on the final bubble.
22
23
  return inputs.map((input, index) => ({ ...turn, id: input.id, content: input.text,
24
+ folders: input.folders ?? (index === inputs.length - 1 ? message.folders ?? [] : []),
23
25
  attachments: input.attachments ?? (index === inputs.length - 1 ? message.attachments ?? [] : []), ...(input.secretRedaction ? { secretRedaction: input.secretRedaction } : {}) }));
24
26
  });
25
27
  }
@@ -246,7 +248,7 @@ export function acceptedInputsFromMessages({ messages }, _options = {}) {
246
248
  const input = acceptedInputEvent({ event });
247
249
  return input ? [input.data] : [];
248
250
  });
249
- return receipts.length > 0 ? receipts : [{ id: message.id, text: message.content }];
251
+ return receipts.length > 0 ? receipts : [{ id: message.id, text: message.content, ...(message.folders ? { folders: message.folders } : {}) }];
250
252
  }
251
253
  return projectAcceptedInputEvents({ events: message.events ?? [] }).flatMap(event => {
252
254
  const input = acceptedInputEvent({ event });
@@ -256,4 +258,30 @@ export function acceptedInputsFromMessages({ messages }, _options = {}) {
256
258
  });
257
259
  });
258
260
  }
261
+ /** Preserve folder metadata through existing event JSON storage; no attachment lifecycle or schema.
262
+ * Existing per-input receipts already own queued variants and must retain their identities.
263
+ */
264
+ export function withFolderReceipt({ message }, _options = {}) {
265
+ if (message.role !== 'user' || !message.folders?.length)
266
+ return message;
267
+ const events = message.events ?? [];
268
+ if (events.some(event => acceptedInputEvent({ event })))
269
+ return message;
270
+ return { ...message, events: [...events, acceptedInputUserEvent({ input: {
271
+ id: message.id, text: message.content, folders: message.folders,
272
+ ...(message.secretRedaction ? { secretRedaction: message.secretRedaction } : {}),
273
+ ...(message.attachments ? { attachments: message.attachments } : {}),
274
+ } }, { receipt: 'accepted' })] };
275
+ }
276
+ /** Hydrate the message's folder list from its accepted receipts after a database read. */
277
+ export function foldersFromReceipts({ events }, _options = {}) {
278
+ if (!Array.isArray(events))
279
+ return [];
280
+ return events.flatMap(event => {
281
+ if (event === null || typeof event !== 'object')
282
+ return [];
283
+ const folders = acceptedInputEvent({ event })?.data.folders;
284
+ return Array.isArray(folders) ? folders.filter((path) => typeof path === 'string') : [];
285
+ });
286
+ }
259
287
  //# sourceMappingURL=accepted-input.js.map
@@ -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,6 +74,7 @@ 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
79
  ...(input.history.findLast(message => message.role === "user")?.secretRedaction?.secretRedacted ? { secretRedacted: true } : {}),
70
80
  },
@@ -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;
@@ -375,6 +375,7 @@ export function createAssistantTransportClient({ ports }, _options = {}) {
375
375
  return {
376
376
  messages: byokMessages({ history: input.history }, {}),
377
377
  ...attachmentIdsField(input),
378
+ ...(input.folders?.length ? { folders: input.folders } : {}),
378
379
  ...conversationIdField(input),
379
380
  agentPluginState: input.context?.["agentPluginState"],
380
381
  userPrompt: input.context?.["userPrompt"],
@@ -584,6 +585,7 @@ export function createAssistantTransportClient({ ports }, _options = {}) {
584
585
  ...modelField(input),
585
586
  ...reasoningField(input),
586
587
  ...attachmentIdsField(input),
588
+ ...(input.folders?.length ? { folders: input.folders } : {}),
587
589
  ...pluginRefIdsField(input),
588
590
  agentPluginState: input.context?.["agentPluginState"],
589
591
  userPrompt: input.context?.["userPrompt"],
@@ -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
@@ -58,6 +58,8 @@ export interface ChatMessage {
58
58
  lastRunEventId?: string;
59
59
  startedAt?: number;
60
60
  endedAt?: number;
61
+ /** Picked directory paths, kept byte-exact and separate from upload references. */
62
+ folders?: string[];
61
63
  attachments?: ChatAttachment[];
62
64
  }
63
65
  //# sourceMappingURL=messages.d.ts.map
@@ -29,7 +29,7 @@ export declare function isDelegatedWrapperToolName({ name }: {
29
29
  name: string;
30
30
  }): boolean;
31
31
  /**
32
- * Drop each delegated-tool wrapper call that the daemon also reported as its own canonical row,
32
+ * Drop each delegated or direct MCP wrapper call that the daemon also reported as its own canonical row,
33
33
  * so one call shows as one row.
34
34
  *
35
35
  * WHY. A delegated call arrives twice: the vendor CLI's wrapper `tool_use`
@@ -39,16 +39,26 @@ export declare function isDelegatedWrapperToolName({ name }: {
39
39
  * failed call showed two red rows. The canonical row is the one to keep: it carries the tool's own
40
40
  * name, input and result.
41
41
  *
42
- * A wrapper folds onto the first LATER canonical `tool_use` whose name equals its `input.toolId`
43
- * and that no earlier wrapper has claimed; its `tool_result` events go with it. A wrapper with no
44
- * such canonical row (refused before the bridge, e.g. a read-only 403) stays, so its error is not
45
- * hidden.
42
+ * A direct call (`mcp__jini__theme_read_file`) arrives twice the same way: the CLI's `mcp__<server>__X`
43
+ * row and the bridge's `X` row with the same input, so every direct Jini MCP call drew two rows.
46
44
  *
47
- * @param events - The event list, normally already passed through {@link dedupeToolUsesById}.
45
+ * Antigravity buffers CLI receipts: its adapter-described wrappers match equal inner inputs
46
+ * anywhere in this assistant turn, in appearance order among equal keys. Pass only one turn's events;
47
+ * no state is retained across calls. Wrapper and bridge rows are each claimed at most once.
48
+ * A legacy gateway folds onto the first LATER canonical `tool_use` whose name equals its `input.toolId`.
49
+ * A direct call folds onto the first LATER `X` row with structurally equal JSON input that arrives
50
+ * before the CLI's own result for it. No canonical row is claimed twice, and a folded wrapper's
51
+ * `tool_result` events go with it. A wrapper with no such canonical row (refused before the
52
+ * bridge, e.g. a read-only 403) stays, so its error is not hidden.
53
+ *
54
+ * @param events - One assistant turn's events, normally already passed through {@link dedupeToolUsesById}.
48
55
  * @returns A new array without the folded wrappers, or the original reference when none fold.
49
- * @complexity O(n·w): n events, w wrapper calls (a handful per message).
56
+ * @throws CanonicalJsonError if a direct call's input contains non-JSON values.
57
+ * @complexity O(n·w·s): n events, w wrappers, s canonical input serialization cost (including key sorting).
58
+ * O(n + s) extra space; inputs and events are not mutated.
59
+ * @example foldDelegatedWrapperCalls({ events }); // canonical rows retain their own results
50
60
  */
51
61
  export declare function foldDelegatedWrapperCalls({ events }: {
52
62
  events: AgentEvent[] | undefined;
53
- }): AgentEvent[];
63
+ }, _options?: Record<string, never>): AgentEvent[];
54
64
  //# sourceMappingURL=tool-events.d.ts.map
@@ -1,3 +1,5 @@
1
+ import { canonicalJson } from '@jini-ai/core/records';
2
+ import { toolPresentation } from './tool-presentation.js';
1
3
  /**
2
4
  * Drop repeated `tool_use` events that share an `id`. A retried/duplicated
3
5
  * stream can replay the same tool-call id (e.g. reconnect-and-replay landing
@@ -46,15 +48,31 @@ const DELEGATED_WRAPPER_PATTERN = /^(?:mcp__.+__)?execute_(?:readonly_)?delegate
46
48
  export function isDelegatedWrapperToolName({ name }) {
47
49
  return DELEGATED_WRAPPER_PATTERN.test(name);
48
50
  }
49
- function wrapperToolId({ event }) {
50
- if (!isDelegatedWrapperToolName({ name: event.name }))
51
+ /** The tool `mcp__<server>__X` names (`X`), or undefined for any other name. O(n) in the name length. */
52
+ function directMcpToolName({ name }) {
53
+ if (!name.startsWith('mcp__'))
51
54
  return undefined;
52
- const input = event.input;
53
- const toolId = typeof input === 'object' && input !== null ? input.toolId : undefined;
54
- return typeof toolId === 'string' && toolId ? toolId : undefined;
55
+ // The server segment is at least one character, so its closing `__` starts at index 6 or later.
56
+ const separator = name.indexOf('__', 6);
57
+ return separator === -1 ? undefined : name.slice(separator + 2);
58
+ }
59
+ /** Resolve the generic wrapper contract or an MCP gateway/direct name. Pure; O(n) in the name.
60
+ * Legacy gateways retain name-only matching for malformed-input bridge errors;
61
+ * Antigravity's adapter-described wrappers require equal inner inputs but may arrive buffered.
62
+ */
63
+ function wrapperCall({ event }, _options = {}) {
64
+ const wrapper = toolPresentation(event).wrapper;
65
+ const call = wrapper ?? event;
66
+ if (!isDelegatedWrapperToolName({ name: call.name })) {
67
+ const name = wrapper ? wrapper.name : directMcpToolName({ name: call.name });
68
+ return name ? { name, input: call.input, matchInput: true, buffered: wrapper !== undefined } : undefined;
69
+ }
70
+ const args = call.input !== null && typeof call.input === 'object' ? call.input : {};
71
+ const name = args.toolId;
72
+ return typeof name === 'string' && name ? { name, input: args.input, matchInput: wrapper !== undefined, buffered: wrapper !== undefined } : undefined;
55
73
  }
56
74
  /**
57
- * Drop each delegated-tool wrapper call that the daemon also reported as its own canonical row,
75
+ * Drop each delegated or direct MCP wrapper call that the daemon also reported as its own canonical row,
58
76
  * so one call shows as one row.
59
77
  *
60
78
  * WHY. A delegated call arrives twice: the vendor CLI's wrapper `tool_use`
@@ -64,16 +82,26 @@ function wrapperToolId({ event }) {
64
82
  * failed call showed two red rows. The canonical row is the one to keep: it carries the tool's own
65
83
  * name, input and result.
66
84
  *
67
- * A wrapper folds onto the first LATER canonical `tool_use` whose name equals its `input.toolId`
68
- * and that no earlier wrapper has claimed; its `tool_result` events go with it. A wrapper with no
69
- * such canonical row (refused before the bridge, e.g. a read-only 403) stays, so its error is not
70
- * hidden.
85
+ * A direct call (`mcp__jini__theme_read_file`) arrives twice the same way: the CLI's `mcp__<server>__X`
86
+ * row and the bridge's `X` row with the same input, so every direct Jini MCP call drew two rows.
71
87
  *
72
- * @param events - The event list, normally already passed through {@link dedupeToolUsesById}.
88
+ * Antigravity buffers CLI receipts: its adapter-described wrappers match equal inner inputs
89
+ * anywhere in this assistant turn, in appearance order among equal keys. Pass only one turn's events;
90
+ * no state is retained across calls. Wrapper and bridge rows are each claimed at most once.
91
+ * A legacy gateway folds onto the first LATER canonical `tool_use` whose name equals its `input.toolId`.
92
+ * A direct call folds onto the first LATER `X` row with structurally equal JSON input that arrives
93
+ * before the CLI's own result for it. No canonical row is claimed twice, and a folded wrapper's
94
+ * `tool_result` events go with it. A wrapper with no such canonical row (refused before the
95
+ * bridge, e.g. a read-only 403) stays, so its error is not hidden.
96
+ *
97
+ * @param events - One assistant turn's events, normally already passed through {@link dedupeToolUsesById}.
73
98
  * @returns A new array without the folded wrappers, or the original reference when none fold.
74
- * @complexity O(n·w): n events, w wrapper calls (a handful per message).
99
+ * @throws CanonicalJsonError if a direct call's input contains non-JSON values.
100
+ * @complexity O(n·w·s): n events, w wrappers, s canonical input serialization cost (including key sorting).
101
+ * O(n + s) extra space; inputs and events are not mutated.
102
+ * @example foldDelegatedWrapperCalls({ events }); // canonical rows retain their own results
75
103
  */
76
- export function foldDelegatedWrapperCalls({ events }) {
104
+ export function foldDelegatedWrapperCalls({ events }, _options = {}) {
77
105
  if (!events || events.length === 0)
78
106
  return [];
79
107
  const claimed = new Set();
@@ -81,11 +109,11 @@ export function foldDelegatedWrapperCalls({ events }) {
81
109
  events.forEach((event, i) => {
82
110
  if (event.kind !== 'tool_use')
83
111
  return;
84
- const toolId = wrapperToolId({ event });
112
+ const call = wrapperCall({ event });
85
113
  // A replayed wrapper (same id) must not claim a second canonical row.
86
- if (toolId === undefined || folded.has(event.id))
114
+ if (call === undefined || folded.has(event.id))
87
115
  return;
88
- const match = events.findIndex((other, j) => j > i && !claimed.has(j) && other.kind === 'tool_use' && other.name === toolId);
116
+ const match = findCanonicalRow({ events, wrapper: event, wrapperIndex: i, call, claimed });
89
117
  if (match === -1)
90
118
  return;
91
119
  claimed.add(match);
@@ -96,4 +124,32 @@ export function foldDelegatedWrapperCalls({ events }) {
96
124
  return events.filter((event) => !((event.kind === 'tool_use' && folded.has(event.id)) ||
97
125
  (event.kind === 'tool_result' && folded.has(event.toolUseId))));
98
126
  }
127
+ /**
128
+ * Index of the first unclaimed canonical row in the wrapper's matching window, or -1. Legacy gateways match
129
+ * on name alone; direct calls and adapter-described wrappers also require equal inner input.
130
+ * @throws CanonicalJsonError if a direct call's input contains non-JSON values.
131
+ * @complexity O(n·s): n events, s canonical input serialization cost.
132
+ */
133
+ function findCanonicalRow({ events, wrapper, wrapperIndex, call, claimed }) {
134
+ const inputKey = call.matchInput ? canonicalJson({ value: { input: call.input } }) : undefined;
135
+ const [start, end] = canonicalSearchWindow({ events, wrapper, wrapperIndex, call });
136
+ for (let j = start; j < end; j += 1) {
137
+ const other = events[j];
138
+ if (claimed.has(j) || other.kind !== 'tool_use' || other.name !== call.name)
139
+ continue;
140
+ if (inputKey === undefined || canonicalJson({ value: { input: other.input } }) === inputKey)
141
+ return j;
142
+ }
143
+ return -1;
144
+ }
145
+ /** Pure matching bounds within one turn; O(n) time, O(1) space for n events.
146
+ * Antigravity's buffered receipts cannot establish arrival/result boundaries. Direct MCP calls
147
+ * retain their later-only window so a completed/refused call cannot consume a separate retry.
148
+ */
149
+ function canonicalSearchWindow({ events, wrapper, wrapperIndex, call }, _options = {}) {
150
+ if (call.buffered)
151
+ return [0, events.length];
152
+ const resultIndex = !call.matchInput ? -1 : events.findIndex(other => other.kind === 'tool_result' && other.toolUseId === wrapper.id);
153
+ return [wrapperIndex + 1, resultIndex === -1 ? events.length : resultIndex];
154
+ }
99
155
  //# sourceMappingURL=tool-events.js.map
@@ -0,0 +1,14 @@
1
+ type Call = {
2
+ name: string;
3
+ input: unknown;
4
+ };
5
+ /** Display-only call and, when present, the transport's inner call to correlate with the bridge. */
6
+ export type ToolPresentation = Call & {
7
+ wrapper?: Call;
8
+ };
9
+ /** Resolve CLI argument dialects at one boundary. Raw/persisted events remain untouched.
10
+ * Pure; O(k) only when an adapter copies k argument keys, otherwise O(1).
11
+ */
12
+ export declare function toolPresentation(call: Call, _options?: Record<string, never>): ToolPresentation;
13
+ export {};
14
+ //# sourceMappingURL=tool-presentation.d.ts.map
@@ -0,0 +1,8 @@
1
+ import { antigravityToolPresentation } from './antigravity/tool-presentation.js';
2
+ /** Resolve CLI argument dialects at one boundary. Raw/persisted events remain untouched.
3
+ * Pure; O(k) only when an adapter copies k argument keys, otherwise O(1).
4
+ */
5
+ export function toolPresentation(call, _options = {}) {
6
+ return antigravityToolPresentation(call) ?? call;
7
+ }
8
+ //# sourceMappingURL=tool-presentation.js.map
@@ -57,6 +57,8 @@ export interface StartRunInput {
57
57
  history: ChatMessage[];
58
58
  agentId?: string;
59
59
  conversationId?: string | null;
60
+ /** Picked directory paths, kept byte-exact and separate from upload references. */
61
+ folders?: string[];
60
62
  attachments?: ChatAttachment[];
61
63
  context?: RunContext;
62
64
  /** Stops the browser-side subscription; the run continues host-side. */
@@ -19,7 +19,8 @@ export interface UserTextRedactionOptions {
19
19
  export declare const SECRET_REDACTED_NOTICE = "I removed what looked like a key from your message. If it was real, rotate it. Use the secure form to save it.";
20
20
  export declare const CREDENTIAL_CARD_GUIDANCE = "Use the secure form to save keys, tokens, passwords, and secrets. Chat answers are sent to the assistant.";
21
21
  /** Stricter than error redaction: also detects unlabelled long tokens, while retaining ordinary URLs,
22
- * hexadecimal hashes, identifiers and prose. Strong credential syntax inside a URL is still caught. */
22
+ * filesystem paths, hexadecimal hashes, identifiers and prose. Strong credential syntax inside
23
+ * URLs or paths is still caught. */
23
24
  export declare function redactUserText({ text }: {
24
25
  readonly text: string;
25
26
  }, { secretField }?: {
@@ -25,8 +25,27 @@ function entropy(text) {
25
25
  }
26
26
  return bits;
27
27
  }
28
+ /** Recognize identifier paths, including the fragment after a space in a typed path.
29
+ * Require multiple segments and readable words: arbitrary base64 and long digit-mixed runs
30
+ * must still reach the entropy guard. Specific credential rules have already run. */
31
+ function isIdentifierPath({ text }, _options = {}) {
32
+ const segments = text.replace(/^(?:[a-z]:|~)?[\\/]*/i, '').split(/[\\/]/).filter(Boolean);
33
+ return segments.length >= 2 && segments.every(segment => /^[a-z][a-z0-9]*(?:[-_.][a-z0-9]+)*$/.test(segment)
34
+ || segment.split(/[-_.]/).every(word => /^[A-Z]$/.test(word)
35
+ || /^[A-Z]?[a-z]{2,}(?:[A-Z][a-z]{2,}|[0-9]{1,4}(?=[A-Z][a-z]{2,}|$))*$/.test(word)));
36
+ }
37
+ /** A piece of a random key: long, both letter cases, and a character-class change at most positions.
38
+ * Readable names (CamelCase, SCREAMING_CASE, dated file names) change class far less often, and
39
+ * every piece of a 40-character base64 key split by '/' is 12+ characters in most keys. */
40
+ function isOpaqueSegment(segment) {
41
+ if (segment.length < 12 || !/[A-Z]/.test(segment) || !/[a-z]/.test(segment))
42
+ return false;
43
+ const changes = segment.match(/[A-Z](?=[^A-Z])|[a-z](?=[^a-z])|\d(?=\D)/g)?.length ?? 0;
44
+ return changes / (segment.length - 1) >= 0.4;
45
+ }
28
46
  /** Stricter than error redaction: also detects unlabelled long tokens, while retaining ordinary URLs,
29
- * hexadecimal hashes, identifiers and prose. Strong credential syntax inside a URL is still caught. */
47
+ * filesystem paths, hexadecimal hashes, identifiers and prose. Strong credential syntax inside
48
+ * URLs or paths is still caught. */
30
49
  export function redactUserText({ text }, { secretField = false } = {}) {
31
50
  // Existing removal markers must survive a second pass through assignment rules (api_key=…).
32
51
  const literalMarkers = new Map();
@@ -40,10 +59,24 @@ export function redactUserText({ text }, { secretField = false } = {}) {
40
59
  }).replaceAll(REMOVED, INTERNAL_REMOVED);
41
60
  const base = redactSecretShapes({ text: protectedText, policy: { additionalRules: CHAT_RULES } });
42
61
  let count = base.redactions;
43
- const urls = [...base.text.matchAll(/\b[a-z][a-z0-9+.-]*:\/\/[^\s<>"']+/gi)]
44
- .map(match => ({ start: match.index, end: match.index + match[0].length }));
62
+ // Exempt locations only AFTER specific secret rules. Only quoted paths may contain whitespace;
63
+ // unquoted paths and URLs end there, and at , | ; and invisible separators, so an adjacent
64
+ // secret glued after them cannot inherit the exemption.
65
+ // A quoted path ends at its first matching quote, which must sit on a word boundary, so an
66
+ // apostrophe later in the line ("don't") cannot close an unterminated quote and exempt the gap.
67
+ // Require a root boundary so a slash inside an opaque token cannot exempt that token, and a
68
+ // folder after the root so a bare "/" + token is not mistaken for a path.
69
+ const locations = [
70
+ ...[...base.text.matchAll(/\b[a-z][a-z0-9+.-]*:\/\/[^\s<>"'`,|;\u200B-\u200D\uFEFF]+|(?<!\w)(["'`])(?:~\/|[a-z]:[\\/]|\\\\|\/)[^\r\n"'`\\/]+[\\/](?:(?!\1)[^\r\n])*\1(?!\w)|(?<![\w+\/=\\~.-])(?:~\/|[a-z]:[\\/]|\\\\|\/)[^\s<>"'`,|;\u200B-\u200D\uFEFF\\/]+[\\/][^\s<>"'`,|;\u200B-\u200D\uFEFF]*/gi)]
71
+ .map(match => ({ match, url: /^[a-z][a-z0-9+.-]*:\/\//i.test(match[0]) })),
72
+ // The part of a hand-typed unquoted path after a space (Work/CustomerPortal…) has no root.
73
+ ...[...base.text.matchAll(/[^\s<>"'`,|;\u200B-\u200D\uFEFF]+/g)]
74
+ .filter(match => isIdentifierPath({ text: match[0] })).map(match => ({ match, url: false })),
75
+ ].map(({ match, url }) => ({ start: match.index, end: match.index + match[0].length, url }));
45
76
  let cleaned = base.text.replace(/(?<![A-Za-z0-9_+\/=-])[A-Za-z0-9_+\/=-]{40,}(?![A-Za-z0-9_+\/=-])/g, (candidate, offset) => {
46
- if (urls.some(url => offset >= url.start && offset < url.end))
77
+ const location = locations.find(span => offset >= span.start && offset + candidate.length <= span.end);
78
+ // A key glued straight onto a path (/proj/KEY) is still checked; URLs keep their opaque ids.
79
+ if (location && (location.url || !candidate.split(/[\\/]/).some(isOpaqueSegment)))
47
80
  return candidate;
48
81
  if (/^[a-f0-9]+$/i.test(candidate) || !/[A-Za-z]/.test(candidate))
49
82
  return candidate;
@@ -0,0 +1,99 @@
1
+ import { type PageDriver } from '@jini-ai/agentic';
2
+ import type { ApprovalAnswer } from '@jini-ai/core';
3
+ import type { ChatPaneAgentToolAction, ChatPaneAgentBridgeAccess } from '../../react/features/chat-pane/types.js';
4
+ export interface FrontendSessionBridgeOptions {
5
+ /** Host validates its server-bound envelope before the shared page executor sees plain args. */
6
+ readonly preparePageInput?: (args: {
7
+ action: ChatPaneAgentToolAction;
8
+ }) => Record<string, unknown>;
9
+ readonly capabilityIds?: readonly string[];
10
+ /** Bounds the HTTP envelope before it reaches a host JSON-body limit. Oversized reads refuse. */
11
+ readonly maxResponseBytes?: number;
12
+ /** Flush host state; a defined return replaces the machine response before acknowledgement. */
13
+ readonly onBeforeResponse?: (args: {
14
+ action: ChatPaneAgentToolAction;
15
+ output: unknown;
16
+ }) => unknown | Promise<unknown>;
17
+ readonly onAfterResponse?: (args: {
18
+ action: ChatPaneAgentToolAction;
19
+ output: unknown;
20
+ }) => void | Promise<void>;
21
+ /** Host policy and the existing app dialog. It never receives an assistant decision. */
22
+ readonly pageApproval?: {
23
+ isDirect(required: {
24
+ capabilityId: string;
25
+ input: Record<string, unknown>;
26
+ }): boolean;
27
+ request(required: {
28
+ capabilityId: string;
29
+ input: Record<string, unknown>;
30
+ signal: AbortSignal;
31
+ }): Promise<ApprovalAnswer>;
32
+ };
33
+ /** Supplying one claims the six `page.*` verbs. Omit for a chat-only surface. */
34
+ readonly pageDriver?: PageDriver;
35
+ /**
36
+ * Product capabilities, keyed by id prefix (`'cms.'`). Each claims every capability under it.
37
+ * This is how a consumer exposes verbs the engine has never heard of.
38
+ */
39
+ readonly executors?: Readonly<Record<string, (capabilityId: string, input: Record<string, unknown>) => Promise<unknown>>>;
40
+ /** Called for every invocation before it runs — the hook a product uses to show an activity trail. */
41
+ readonly onInvocation?: (action: ChatPaneAgentToolAction) => void;
42
+ /** Reported for failures that are nobody's tool call: stream errors, malformed frames. */
43
+ readonly onError?: (error: unknown) => void;
44
+ }
45
+ /** The host supplies its origin and stream transport; native fetch defaults may be overridden. */
46
+ export interface FrontendSessionBridgeArgs {
47
+ /** Pass an empty string for the same-origin proxy used by a browser host. */
48
+ readonly baseUrl: string;
49
+ readonly endpointPrefix?: string;
50
+ readonly headers?: () => Record<string, string>;
51
+ readonly openStream: (args: {
52
+ url: string;
53
+ headers?: Record<string, string>;
54
+ }) => Pick<EventSource, 'addEventListener' | 'close'>;
55
+ readonly fetch?: typeof globalThis.fetch;
56
+ readonly request?: (args: {
57
+ url: string;
58
+ init: RequestInit;
59
+ timeoutMs: number;
60
+ }) => Promise<Response>;
61
+ }
62
+ export interface FrontendSessionBridge {
63
+ /** Hand this to `ChatPane`'s `agentControl.bridgeAccess`. */
64
+ readonly bridgeAccess: ChatPaneAgentBridgeAccess;
65
+ /**
66
+ * Resolves the *first* time the daemon attaches this surface — an "is it live yet" signal.
67
+ *
68
+ * Its `bindToken` is only correct until the next reconnect, so do not stash it. Call
69
+ * {@link FrontendSessionBridge.bindToken} when building a run request instead.
70
+ */
71
+ readonly ready: Promise<{
72
+ sessionId: string;
73
+ bindToken: string;
74
+ }>;
75
+ /**
76
+ * The token for the attachment that is live *now*, or `undefined` before the first attach and
77
+ * after {@link FrontendSessionBridge.close}. Read it at run-start; see the module doc.
78
+ */
79
+ bindToken(): string | undefined;
80
+ close(): void;
81
+ }
82
+ /**
83
+ * Opens the surface's connection and starts routing invocations.
84
+ *
85
+ * @param options - What this page can serve and where the daemon is.
86
+ * @returns The pane's bridge, the attach result, and a teardown.
87
+ */
88
+ export declare function createFrontendSessionBridge({ baseUrl: base, openStream, request, fetch, endpointPrefix, headers }: FrontendSessionBridgeArgs, options?: FrontendSessionBridgeOptions): FrontendSessionBridge;
89
+ /** Authenticated streaming replacement for EventSource when credentials belong in headers.
90
+ * Uses the shared SSE decoder; reconnect creates a new attachment and clears the old token.
91
+ * O(stream bytes), bounded frame buffer; close aborts fetch/readers and the reconnect timer. */
92
+ export declare function openFetchFrontendStream({ url, headers }: {
93
+ url: string;
94
+ headers: () => Record<string, string>;
95
+ }, { fetch: send, reconnectMs }?: {
96
+ fetch?: typeof globalThis.fetch;
97
+ reconnectMs?: number;
98
+ }): Pick<EventSource, 'addEventListener' | 'close'>;
99
+ //# sourceMappingURL=frontend-session-bridge.d.ts.map