@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
@@ -1,3 +1,4 @@
1
+ const ARIA_LABEL = 'aria-label';
1
2
  export { CHAT_BUBBLE_STYLE } from '../../contracts/chat-bubble-style.js';
2
3
  /**
3
4
  * Activates a host-rendered button without importing chat, React, icons or storage.
@@ -11,10 +12,10 @@ export function installLazyChatLoader(args, _options = {}) {
11
12
  let shouldOpen = false;
12
13
  let shouldFocus = false;
13
14
  const initialContent = args.button.innerHTML;
14
- const label = args.button.getAttribute('aria-label') ?? '';
15
+ const label = args.button.getAttribute(ARIA_LABEL) ?? '';
15
16
  const activate = async ({ open = true, focus = open } = {}, _options = {}) => {
16
17
  if (disposed)
17
- return;
18
+ return undefined;
18
19
  if (!pending) {
19
20
  shouldOpen = false;
20
21
  shouldFocus = false;
@@ -24,26 +25,29 @@ export function installLazyChatLoader(args, _options = {}) {
24
25
  if (pending)
25
26
  return pending;
26
27
  args.button.setAttribute('aria-busy', 'true');
27
- args.button.setAttribute('aria-label', args.loadingLabel);
28
+ args.button.setAttribute(ARIA_LABEL, args.loadingLabel);
28
29
  args.button.textContent = args.loadingLabel;
29
30
  delete args.button.dataset.loadFailed;
30
31
  // Defer through a promise so a synchronous host load failure follows the same retry path.
31
- pending = Promise.resolve().then(() => { if (!ready)
32
- return args.load({}); }).then(() => {
32
+ pending = Promise.resolve().then(() => {
33
+ if (!ready)
34
+ return args.load({});
35
+ return undefined;
36
+ }).then(() => {
33
37
  if (disposed)
34
38
  return;
35
39
  // Asset readiness survives an open failure; retry must never evaluate the script twice.
36
40
  ready = true;
37
41
  if (shouldOpen)
38
42
  args.open({ focus: shouldFocus });
39
- args.button.setAttribute('aria-label', label);
43
+ args.button.setAttribute(ARIA_LABEL, label);
40
44
  args.button.innerHTML = initialContent;
41
45
  args.button.hidden = true;
42
46
  }).catch(() => {
43
47
  if (!disposed) {
44
48
  args.button.hidden = false;
45
49
  args.button.dataset.loadFailed = 'true';
46
- args.button.setAttribute('aria-label', args.retryLabel);
50
+ args.button.setAttribute(ARIA_LABEL, args.retryLabel);
47
51
  args.button.textContent = args.retryLabel;
48
52
  }
49
53
  }).finally(() => {
@@ -62,17 +66,17 @@ export function installLazyChatLoader(args, _options = {}) {
62
66
  args.button.addEventListener('click', click);
63
67
  args.button.addEventListener('keydown', keydown);
64
68
  args.button.dataset.loaderReady = 'true';
65
- if (args.resume || args.deepLink)
66
- void activate({ focus: args.deepLink });
67
- else if (args.preload)
68
- void activate({ open: false });
69
+ if (args.resume || args.deepLink || args.preload) {
70
+ const resume = args.resume || args.deepLink;
71
+ void activate({ open: resume, focus: resume && args.deepLink });
72
+ }
69
73
  return {
70
74
  activate,
71
75
  dispose(_input = {}, _options = {}) {
72
76
  disposed = true;
73
77
  args.button.removeEventListener('click', click);
74
78
  args.button.removeEventListener('keydown', keydown);
75
- args.button.setAttribute('aria-label', label);
79
+ args.button.setAttribute(ARIA_LABEL, label);
76
80
  args.button.innerHTML = initialContent;
77
81
  args.button.removeAttribute('aria-busy');
78
82
  delete args.button.dataset.loadFailed;
@@ -15,15 +15,10 @@ export interface BrowserSessionStore {
15
15
  state: PersistedEmbedState;
16
16
  }): void;
17
17
  clear(args: Record<string, never>): void;
18
- enqueue(args: {
19
- action: unknown;
20
- }): void;
21
- drain(args: Record<string, never>): unknown | null;
22
18
  }
23
19
  export interface BrowserSessionStoreArgs {
24
20
  readonly storage: SessionStoragePort;
25
21
  readonly transcriptKey: string;
26
- readonly actionKey: string;
27
22
  readonly maxMessages: number;
28
23
  readonly maxBytes: number;
29
24
  readonly validateMessage: (args: {
@@ -31,14 +26,13 @@ export interface BrowserSessionStoreArgs {
31
26
  }) => boolean;
32
27
  }
33
28
  /**
34
- * Creates bounded, fail-soft transcript storage and a single-slot action queue.
35
- * Corrupt transcripts are cleared as a whole. Actions are deleted before delivery,
36
- * and a failed delete prevents delivery. Serialization/storage failures never escape.
29
+ * Creates bounded, fail-soft transcript storage.
30
+ * Corrupt transcripts are cleared as a whole. Serialization/storage failures never escape.
37
31
  * Reject oversized stored envelopes before parsing, validate the whole message batch, then trim
38
32
  * oldest messages first. Circular serialization is fail-soft. Keys, limits, validator and Storage
39
33
  * are injected so separate controllers never accidentally share a product-selected namespace.
40
34
  * @complexity Load O(bytes + messages); save O(messages * serialized bytes) in the
41
- * worst trimming case; queue O(serialized action bytes). State is instance-local.
35
+ * worst trimming case. State is instance-local.
42
36
  */
43
37
  export declare function createBrowserSessionStore(args: BrowserSessionStoreArgs): BrowserSessionStore;
44
38
  //# sourceMappingURL=session-store.d.ts.map
@@ -1,17 +1,16 @@
1
1
  /**
2
- * Creates bounded, fail-soft transcript storage and a single-slot action queue.
3
- * Corrupt transcripts are cleared as a whole. Actions are deleted before delivery,
4
- * and a failed delete prevents delivery. Serialization/storage failures never escape.
2
+ * Creates bounded, fail-soft transcript storage.
3
+ * Corrupt transcripts are cleared as a whole. Serialization/storage failures never escape.
5
4
  * Reject oversized stored envelopes before parsing, validate the whole message batch, then trim
6
5
  * oldest messages first. Circular serialization is fail-soft. Keys, limits, validator and Storage
7
6
  * are injected so separate controllers never accidentally share a product-selected namespace.
8
7
  * @complexity Load O(bytes + messages); save O(messages * serialized bytes) in the
9
- * worst trimming case; queue O(serialized action bytes). State is instance-local.
8
+ * worst trimming case. State is instance-local.
10
9
  */
11
10
  export function createBrowserSessionStore(args) {
12
- const { storage, transcriptKey, actionKey, maxMessages, maxBytes, validateMessage } = args;
13
- if (!transcriptKey || !actionKey || transcriptKey === actionKey) {
14
- throw new Error('Transcript and action keys must be nonempty and distinct');
11
+ const { storage, transcriptKey, maxMessages, maxBytes, validateMessage } = args;
12
+ if (!transcriptKey) {
13
+ throw new Error('Transcript key must be nonempty');
15
14
  }
16
15
  if (!Number.isSafeInteger(maxMessages) || maxMessages < 0)
17
16
  throw new Error('Invalid message limit');
@@ -57,37 +56,6 @@ export function createBrowserSessionStore(args) {
57
56
  catch { /* The live conversation continues even when serialization or storage fails. */ }
58
57
  },
59
58
  clear,
60
- enqueue({ action }) {
61
- try {
62
- const serialized = JSON.stringify(action);
63
- if (serialized !== undefined)
64
- storage.setItem(actionKey, serialized);
65
- }
66
- catch { /* The live page continues without a queued action. */ }
67
- },
68
- drain(_args) {
69
- let raw;
70
- try {
71
- raw = storage.getItem(actionKey);
72
- }
73
- catch {
74
- return null;
75
- }
76
- if (raw === null)
77
- return null;
78
- try {
79
- storage.removeItem(actionKey);
80
- }
81
- catch {
82
- return null;
83
- }
84
- try {
85
- return JSON.parse(raw);
86
- }
87
- catch {
88
- return null;
89
- }
90
- },
91
59
  };
92
60
  }
93
61
  const utf8Bytes = (value) => new TextEncoder().encode(value).length;
@@ -25,11 +25,11 @@ export function useEmbedChatDialog({ open, onClose, panelRef, launcherRef }, { r
25
25
  }, [open, launcherRef]);
26
26
  useEffect(() => {
27
27
  if (!open)
28
- return;
28
+ return undefined;
29
29
  const panel = panelRef.current;
30
30
  const view = panel?.ownerDocument.defaultView;
31
31
  if (!panel || !view)
32
- return;
32
+ return undefined;
33
33
  const onKeyDown = (event) => {
34
34
  if (event.key === 'Escape' && !event.defaultPrevented) {
35
35
  event.preventDefault();
@@ -1,7 +1,6 @@
1
1
  import type { ChatTransport } from '../../core/transport.js';
2
2
  import type { ChatPaneAgent } from '../../react/chat-pane.js';
3
3
  import type { BrowserSessionStore } from '../browser/session-store.js';
4
- import type { PageActionPort } from '../../core/browser/page-actions.js';
5
4
  export { useEmbedChatDialog } from './dialog.hooks.js';
6
5
  import type { EmbedHeaderCopy, EscapeKeyPort } from './header.js';
7
6
  export { EmbedChatHeader } from './header.js';
@@ -10,18 +9,12 @@ export interface EmbedChatCopy extends EmbedHeaderCopy {
10
9
  readonly title: string;
11
10
  readonly fabLabel: string;
12
11
  readonly placeholder: string;
13
- readonly proposalLabel: (args: {
14
- title: string;
15
- }) => string;
16
- readonly proposalGo: string;
17
12
  }
18
13
  export interface EmbedChatWidgetArgs {
19
14
  readonly agent: ChatPaneAgent;
20
15
  readonly copy: EmbedChatCopy;
21
16
  readonly transport: ChatTransport;
22
17
  readonly session: BrowserSessionStore;
23
- readonly pageActions: PageActionPort;
24
- readonly directiveEventName: string;
25
18
  readonly keyboard: EscapeKeyPort;
26
19
  /** Schedules after host layout; returns cancellation. No browser global is captured. */
27
20
  readonly scheduleFrame: (args: {
@@ -29,10 +22,10 @@ export interface EmbedChatWidgetArgs {
29
22
  }) => () => void;
30
23
  }
31
24
  /**
32
- * Wires the existing pane/fab to per-instance transcript and page-action ports.
25
+ * Wires the existing pane/fab to per-instance transcript and transport ports.
33
26
  * Keep effect ports stable for the lifetime of a mounted widget; remount for a new
34
- * session/agent. Restored replies are marked processed before the pane mounts, so
35
- * rehydration cannot replay navigation. Reset remounts the pane after confirmation.
27
+ * session/agent. Page control lives on the shared frontend bridge, so transcript rehydration
28
+ * cannot replay effects. Reset remounts the pane after confirmation.
36
29
  * @complexity O(messages + latest reply events) per transcript update; O(message IDs) retained.
37
30
  */
38
31
  export declare function EmbedChatWidget(args: EmbedChatWidgetArgs): import("react").JSX.Element;
@@ -1,11 +1,9 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useCallback, useEffect, useRef, useState } from 'react';
2
+ import { useCallback, useEffect, useState } from 'react';
3
3
  import { createRoot } from 'react-dom/client';
4
- import { isTerminalRunStatus } from '../../core/messages.js';
5
4
  import { ChatPane } from '../../react/chat-pane.js';
6
5
  import { ChatFab } from '../../react/components/ChatFab.js';
7
6
  import { JiniChatProvider } from '../../react/components/JiniChatProvider.js';
8
- import { extractPageActions, splitPageActions, isQueuedPageAction } from '../../core/browser/page-actions.js';
9
7
  import { EmbedChatHeader } from './header.js';
10
8
  import { useEmbedChatSurface } from './dialog.hooks.js';
11
9
  export { useEmbedChatDialog } from './dialog.hooks.js';
@@ -14,71 +12,29 @@ const shellStyles = `
14
12
  .jini-chat-embed__panel { position:fixed; right:var(--jini-embed-right,24px); bottom:var(--jini-embed-bottom,88px); width:min(var(--jini-embed-width,440px),calc(100vw - 32px)); height:min(var(--jini-embed-height,620px),calc(100dvh - 120px)); z-index:var(--jini-embed-z-index,1000); display:flex; flex-direction:column; }
15
13
  .jini-chat-embed__panel[hidden] { display:none; }
16
14
  .jini-chat-embed__panel > .jini-chat-pane { flex:1; min-height:0; }
17
- .jini-chat-embed__reset-confirm,.jini-chat-embed__proposal { display:flex; align-items:center; gap:8px; }
18
- .jini-chat-embed__proposal { padding:12px; background:var(--jini-embed-proposal-background,Canvas); color:var(--jini-embed-proposal-color,CanvasText); }
15
+ .jini-chat-embed__reset-confirm { display:flex; align-items:center; gap:8px; }
19
16
  `;
20
17
  /**
21
- * Wires the existing pane/fab to per-instance transcript and page-action ports.
18
+ * Wires the existing pane/fab to per-instance transcript and transport ports.
22
19
  * Keep effect ports stable for the lifetime of a mounted widget; remount for a new
23
- * session/agent. Restored replies are marked processed before the pane mounts, so
24
- * rehydration cannot replay navigation. Reset remounts the pane after confirmation.
20
+ * session/agent. Page control lives on the shared frontend bridge, so transcript rehydration
21
+ * cannot replay effects. Reset remounts the pane after confirmation.
25
22
  * @complexity O(messages + latest reply events) per transcript update; O(message IDs) retained.
26
23
  */
27
24
  export function EmbedChatWidget(args) {
28
- const { agent, copy, transport, session, pageActions, directiveEventName, keyboard, scheduleFrame } = args;
25
+ const { agent, copy, transport, session, keyboard } = args;
29
26
  const [initial] = useState(() => session.load({}));
30
27
  const [paneKey, setPaneKey] = useState(0);
31
28
  const { open, toggle, panelRef, launcherRef, panelId } = useEmbedChatSurface({ initialOpen: initial.open }, { resetKey: paneKey });
32
29
  const [messages, setMessages] = useState(initial.messages);
33
- const [pending, setPending] = useState(null);
34
- const processed = useRef(new Set(initial.messages.map(message => message.id)));
35
30
  useEffect(() => { session.save({ state: { open, messages } }); }, [session, open, messages]);
36
- useEffect(() => {
37
- const queued = { value: session.drain({}) };
38
- if (!isQueuedPageAction(queued) || queued.value.type === 'navigate')
39
- return;
40
- const action = queued.value;
41
- return scheduleFrame({ callback: () => { pageActions.execute({ action }); } });
42
- }, [session, pageActions, scheduleFrame]);
43
- useEffect(() => () => pageActions.clearHighlight({}), [pageActions]);
44
31
  const reset = useCallback(() => {
45
32
  session.clear({});
46
33
  setMessages([]);
47
34
  setPaneKey(key => key + 1);
48
- setPending(null);
49
- processed.current.clear();
50
35
  }, [session]);
51
- const go = useCallback((proposal, transcript) => {
52
- if (!pageActions.allows({ action: proposal.navigate }))
53
- return;
54
- if (proposal.bundled && pageActions.allows({ action: proposal.bundled }))
55
- session.enqueue({ action: proposal.bundled });
56
- // Both writes must finish before the host navigation can tear down this page.
57
- session.save({ state: { open, messages: transcript } });
58
- pageActions.navigate({ action: proposal.navigate });
59
- }, [session, pageActions, open]);
60
- const onMessagesChange = useCallback((next) => {
61
- setMessages(next);
62
- const latest = next.at(-1);
63
- if (!latest || latest.role !== 'assistant' || !isTerminalRunStatus({ status: latest.runStatus }) || processed.current.has(latest.id))
64
- return;
65
- processed.current.add(latest.id);
66
- setPending(null);
67
- const actions = extractPageActions({ events: latest.events, directiveEventName })
68
- .filter(action => pageActions.allows({ action }));
69
- const { navigate, other } = splitPageActions({ actions });
70
- if (!navigate) {
71
- if (other)
72
- pageActions.execute({ action: other });
73
- return;
74
- }
75
- const proposal = { navigate, bundled: other };
76
- if (navigate.auto)
77
- go(proposal, next);
78
- else
79
- setPending(proposal);
80
- }, [directiveEventName, pageActions, go]);
81
- return _jsxs("div", { className: "jini-chat-embed", children: [_jsx("style", { children: shellStyles }), _jsx("div", { ref: launcherRef, tabIndex: -1, children: _jsx(ChatFab, { open: open, onToggle: toggle, "aria-label": copy.fabLabel, label: copy.fabLabel, "aria-controls": panelId, "aria-haspopup": "dialog" }) }), _jsx(JiniChatProvider, { transport: transport, children: _jsxs("div", { id: panelId, className: "jini-chat-embed__panel", hidden: !open, ref: panelRef, role: "dialog", "aria-modal": "true", "aria-label": copy.title, children: [_jsx(ChatPane, { transport: transport, agents: [agent], initialSelection: { agentId: agent.id }, initialMessages: messages, title: copy.title, placeholder: copy.placeholder, variant: "embed", header: _jsx(EmbedChatHeader, { title: copy.title, hasMessages: messages.length > 0, onReset: reset, copy: copy, keyboard: keyboard }), onMessagesChange: onMessagesChange }, paneKey), pending ? _jsxs("div", { className: "jini-chat-embed__proposal", children: [_jsx("span", { children: copy.proposalLabel({ title: pending.navigate.target.title }) }), _jsx("button", { type: "button", onClick: () => { setPending(null); go(pending, messages); }, children: copy.proposalGo })] }) : null] }) })] });
36
+ const onMessagesChange = useCallback((next) => { setMessages(next); }, []);
37
+ return _jsxs("div", { className: "jini-chat-embed", children: [_jsx("style", { children: shellStyles }), _jsx("div", { ref: launcherRef, tabIndex: -1, children: _jsx(ChatFab, { open: open, onToggle: toggle, "aria-label": copy.fabLabel, label: copy.fabLabel, "aria-controls": panelId, "aria-haspopup": "dialog" }) }), _jsx(JiniChatProvider, { transport: transport, children: _jsx("div", { id: panelId, className: "jini-chat-embed__panel", hidden: !open, ref: panelRef, role: "dialog", "aria-modal": "true", "aria-label": copy.title, children: _jsx(ChatPane, { transport: transport, agents: [agent], initialSelection: { agentId: agent.id }, initialMessages: messages, title: copy.title, placeholder: copy.placeholder, variant: "embed", header: _jsx(EmbedChatHeader, { title: copy.title, hasMessages: messages.length > 0, onReset: reset, copy: copy, keyboard: keyboard }), onMessagesChange: onMessagesChange }, paneKey) }) })] });
82
38
  }
83
39
  /** Mounts into a host-provided node and returns lifecycle cleanup. O(1) plus React rendering. */
84
40
  export function mountEmbedChat({ element, widget }) {
@@ -15,7 +15,7 @@ export function useEmbedChatHeader({ hasMessages, onReset, keyboard }, _options
15
15
  }, [confirming]);
16
16
  useEffect(() => {
17
17
  if (!confirming)
18
- return;
18
+ return undefined;
19
19
  return keyboard.subscribe({ onEscape: () => setConfirming(false) });
20
20
  }, [confirming, keyboard]);
21
21
  return { confirming, cancel, headerRef, resetButtonRef,
@@ -151,7 +151,7 @@ async function deliverResponse({ run, args, response }) {
151
151
  await response.body?.cancel();
152
152
  }
153
153
  catch { /* closed body */ }
154
- return;
154
+ return undefined;
155
155
  }
156
156
  if (!response.ok)
157
157
  return reportHttpFailure({ run, response, requestFailed: args.copy.requestFailed });
@@ -161,6 +161,7 @@ async function deliverResponse({ run, args, response }) {
161
161
  if (run.isSettled() || !dispatchFrame({ run, result: args.frameMapper({ frame }) }))
162
162
  break;
163
163
  }
164
+ return undefined;
164
165
  }
165
166
  async function reportHttpFailure({ run, response, requestFailed }) {
166
167
  const body = await response.json().catch(() => null);
@@ -13,10 +13,6 @@ export interface VanillaChatCopy {
13
13
  readonly confirm: string;
14
14
  readonly confirmDraft?: string;
15
15
  readonly keep: string;
16
- readonly proposalGo: string;
17
- readonly proposalLabel: (args: {
18
- title: string;
19
- }) => string;
20
16
  readonly thinking: string;
21
17
  readonly greeting?: string;
22
18
  readonly suggestionsLabel?: string;
@@ -53,7 +49,6 @@ export declare function mountVanillaChat(args: MountVanillaChatArgs, options?: {
53
49
  send({ text }: {
54
50
  text: string;
55
51
  }, _opts?: {}): Promise<boolean>;
56
- acceptProposal(_input?: Record<string, never>, _opts?: {}): void;
57
52
  cancel(_input?: Record<string, never>, _opts?: {}): void;
58
53
  reset(_input?: Record<string, never>, _opts?: {}): void;
59
54
  dispose(_input?: Record<string, never>, _opts?: {}): void;
@@ -1,6 +1,6 @@
1
+ const ARIA_LABEL = 'aria-label';
1
2
  import { CHAT_BUBBLE_STYLE } from '../../contracts/chat-bubble-style.js';
2
3
  import { createVanillaChatController } from './state.js';
3
- import { isQueuedPageAction } from '../../core/browser/page-actions.js';
4
4
  export { createVanillaChatController } from './state.js';
5
5
  function svgIcon({ document, path }) {
6
6
  const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
@@ -37,7 +37,7 @@ export function mountVanillaChat(args, options = {}) {
37
37
  const shell = node('div', 'jini-chat-vanilla');
38
38
  const fab = button('');
39
39
  fab.className = 'jini-chat-bubble';
40
- fab.setAttribute('aria-label', copy.fabLabel);
40
+ fab.setAttribute(ARIA_LABEL, copy.fabLabel);
41
41
  fab.setAttribute('aria-haspopup', 'dialog');
42
42
  fab.append(svgIcon({ document, path: 'M4 4h16v12H9l-5 4V4z' }));
43
43
  const panel = node('section', 'jini-chat-vanilla__panel');
@@ -45,12 +45,12 @@ export function mountVanillaChat(args, options = {}) {
45
45
  fab.setAttribute('aria-controls', panel.id);
46
46
  panel.setAttribute('role', 'dialog');
47
47
  panel.setAttribute('aria-modal', 'false');
48
- panel.setAttribute('aria-label', copy.title);
48
+ panel.setAttribute(ARIA_LABEL, copy.title);
49
49
  const header = node('header', 'jini-chat-vanilla__header');
50
50
  const title = node('strong', 'jini-chat-vanilla__title', copy.title);
51
51
  const reset = button(copy.reset);
52
52
  const close = button('');
53
- close.setAttribute('aria-label', copy.close);
53
+ close.setAttribute(ARIA_LABEL, copy.close);
54
54
  close.append(svgIcon({ document, path: 'M6 6l12 12M18 6L6 18' }));
55
55
  header.append(title, reset, close);
56
56
  const confirmation = node('div', 'jini-chat-vanilla__confirmation');
@@ -63,7 +63,7 @@ export function mountVanillaChat(args, options = {}) {
63
63
  log.setAttribute('role', 'log');
64
64
  log.setAttribute('aria-live', 'off');
65
65
  log.setAttribute('aria-relevant', 'additions text');
66
- log.setAttribute('aria-label', copy.title);
66
+ log.setAttribute(ARIA_LABEL, copy.title);
67
67
  log.tabIndex = 0;
68
68
  const status = node('div', 'jini-chat-vanilla__status');
69
69
  status.setAttribute('role', 'status');
@@ -74,27 +74,29 @@ export function mountVanillaChat(args, options = {}) {
74
74
  announcements.setAttribute('aria-atomic', 'false');
75
75
  const error = node('div', 'jini-chat-vanilla__error');
76
76
  error.setAttribute('role', 'alert');
77
- const proposal = node('div', 'jini-chat-vanilla__proposal');
77
+ const proposal = node('div', 'jini-chat-vanilla__tool-confirmation');
78
+ proposal.hidden = true;
79
+ proposal.setAttribute('data-agent-private', '');
78
80
  const proposalText = node('span', '');
79
- const go = button(copy.proposalGo);
81
+ const go = button('Allow');
80
82
  proposal.append(proposalText, go);
81
83
  const form = node('form', 'jini-chat-vanilla__composer');
82
84
  const input = node('textarea', 'jini-chat-vanilla__input');
83
85
  input.placeholder = copy.placeholder;
84
- input.setAttribute('aria-label', copy.placeholder);
86
+ input.setAttribute(ARIA_LABEL, copy.placeholder);
85
87
  input.maxLength = 8000;
86
88
  input.rows = 2;
87
89
  const send = button(copy.send);
88
90
  send.type = 'submit';
89
91
  send.classList.add('jini-chat-vanilla__send');
90
- send.setAttribute('aria-label', copy.send);
92
+ send.setAttribute(ARIA_LABEL, copy.send);
91
93
  send.replaceChildren(svgIcon({ document, path: 'M5 4l15 8-15 8v-6l9-2-9-2V4z' }));
92
94
  const stop = button(copy.stop);
93
95
  form.append(input, send, stop);
94
96
  const greeting = node('p', 'jini-chat-vanilla__greeting', copy.greeting);
95
97
  const suggestions = node('div', 'jini-chat-vanilla__suggestions');
96
98
  if (copy.suggestionsLabel)
97
- suggestions.setAttribute('aria-label', copy.suggestionsLabel);
99
+ suggestions.setAttribute(ARIA_LABEL, copy.suggestionsLabel);
98
100
  const suggestionEntries = (args.suggestions ?? []).map(text => ({ text, button: button(text) }));
99
101
  const suggestionButtons = suggestionEntries.map(entry => entry.button);
100
102
  suggestions.append(...suggestionButtons);
@@ -121,8 +123,6 @@ export function mountVanillaChat(args, options = {}) {
121
123
  send.disabled = state.busy || !input.value.trim();
122
124
  input.disabled = state.busy;
123
125
  stop.hidden = !state.busy;
124
- proposal.hidden = state.proposal === null;
125
- proposalText.textContent = state.proposal ? copy.proposalLabel({ title: state.proposal.navigate.target.title }) : '';
126
126
  };
127
127
  const createRow = ({ message }) => {
128
128
  const row = { element: node('div', `jini-chat-vanilla__message jini-chat-vanilla__message--${message.role}`), content: '\0' };
@@ -180,11 +180,15 @@ export function mountVanillaChat(args, options = {}) {
180
180
  const moveFocus = ({ state, composerHadFocus, stopHadFocus }) => {
181
181
  if (!state.open)
182
182
  return;
183
- if (!wasOpen && initialized)
183
+ if (!wasOpen && initialized) {
184
184
  focusEntry({ busy: state.busy });
185
- else if (state.busy && composerHadFocus)
185
+ return;
186
+ }
187
+ if (state.busy && composerHadFocus) {
186
188
  stop.focus();
187
- else if (!state.busy && stopHadFocus)
189
+ return;
190
+ }
191
+ if (!state.busy && stopHadFocus)
188
192
  input.focus();
189
193
  };
190
194
  const renderFocus = ({ state, composerHadFocus, stopHadFocus, panelHadFocus }) => {
@@ -251,7 +255,6 @@ export function mountVanillaChat(args, options = {}) {
251
255
  const dismissConfirmation = () => { confirmation.hidden = true; reset.focus(); };
252
256
  listen(keep, 'click', dismissConfirmation);
253
257
  listen(confirm, 'click', () => { confirmation.hidden = true; input.value = ''; controller.reset({}); input.focus(); });
254
- listen(go, 'click', () => controller.acceptProposal({}));
255
258
  listen(stop, 'click', () => controller.cancel({}));
256
259
  listen(form, 'submit', event => {
257
260
  event.preventDefault();
@@ -295,21 +298,17 @@ export function mountVanillaChat(args, options = {}) {
295
298
  if (!controller.getState({}).open || !panel.contains(document.activeElement))
296
299
  return;
297
300
  const key = event;
298
- if (key.key === 'Escape')
301
+ if (key.key === 'Escape') {
299
302
  onEscape({ key });
300
- else if (key.key === 'Tab')
303
+ return;
304
+ }
305
+ if (key.key === 'Tab')
301
306
  trapTab({ key });
302
307
  });
303
308
  listen(element, 'jini-chat-open', () => {
304
309
  controller.setOpen({ open: true });
305
310
  focusEntry({ busy: controller.getState({}).busy });
306
311
  });
307
- const queued = { value: args.session.drain({}) };
308
- let cancelFrame = () => { };
309
- if (isQueuedPageAction(queued) && queued.value.type !== 'navigate') {
310
- const action = queued.value;
311
- cancelFrame = args.scheduleFrame({ callback: () => { args.pageActions.execute({ action }); } });
312
- }
313
312
  controller.setOpen({ open: options.initialOpen ?? controller.getState({}).open });
314
313
  initialized = true;
315
314
  element.dataset.chatMounted = 'true';
@@ -318,11 +317,9 @@ export function mountVanillaChat(args, options = {}) {
318
317
  controller,
319
318
  unmount(_input = {}, _opts = {}) {
320
319
  listeners.forEach(dispose => dispose());
321
- cancelFrame();
322
320
  cancelRenderFrame();
323
321
  delete element.dataset.chatMounted;
324
322
  controller.dispose({});
325
- args.pageActions.clearHighlight({});
326
323
  shell.remove();
327
324
  },
328
325
  };
@@ -1,22 +1,15 @@
1
1
  import type { ChatMessage } from '../../core/messages.js';
2
2
  import type { ChatTransport } from '../../core/transport.js';
3
3
  import type { BrowserSessionStore } from '../browser/session-store.js';
4
- import type { PageActionPort, NavigateAction, NonNavigateAction } from '../../core/browser/page-actions.js';
5
4
  export interface VanillaChatState {
6
5
  readonly open: boolean;
7
6
  readonly messages: ChatMessage[];
8
7
  readonly busy: boolean;
9
8
  readonly error: string | null;
10
- readonly proposal: {
11
- navigate: NavigateAction;
12
- bundled: NonNavigateAction | null;
13
- } | null;
14
9
  }
15
10
  export interface VanillaChatControllerArgs {
16
11
  readonly transport: ChatTransport;
17
12
  readonly session: BrowserSessionStore;
18
- readonly pageActions: PageActionPort;
19
- readonly directiveEventName: string;
20
13
  readonly agentId: string;
21
14
  readonly ids: (args: Record<string, never>) => string;
22
15
  readonly createAbortController: (args: Record<string, never>) => AbortController;
@@ -25,9 +18,9 @@ export interface VanillaChatControllerArgs {
25
18
  }) => void;
26
19
  }
27
20
  /**
28
- * Framework-free visitor state over the existing transport/session/page-action owners.
29
- * Restored messages never replay directives. Only a successful live settlement can act;
30
- * persistence and queued effects finish synchronously before navigation tears down the page.
21
+ * Framework-free visitor state over the existing transport/session owners.
22
+ * Restored messages never replay effects; page control belongs to the shared frontend bridge.
23
+ * Terminal navigation flushes the transport snapshot before the host tears down the page.
31
24
  * Reset invalidates callbacks before aborting, preventing late chunks from reviving a thread.
32
25
  * @complexity O(retained transcript + reply events + reply characters) per update; storage writes
33
26
  * occur only on settle/open/reset/navigation, never per streamed chunk. Session bounds persisted bytes.
@@ -40,7 +33,6 @@ export declare function createVanillaChatController(args: VanillaChatControllerA
40
33
  send({ text }: {
41
34
  text: string;
42
35
  }, _opts?: {}): Promise<boolean>;
43
- acceptProposal(_input?: Record<string, never>, _opts?: {}): void;
44
36
  cancel(_input?: Record<string, never>, _opts?: {}): void;
45
37
  reset(_input?: Record<string, never>, _opts?: {}): void;
46
38
  dispose(_input?: Record<string, never>, _opts?: {}): void;