@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.
- package/README.md +6 -1
- package/dist/core/accepted-input.js +20 -11
- package/dist/core/ag-ui/entry.js +3 -1
- package/dist/core/agent-plugin-chat-state.js +3 -1
- package/dist/core/antigravity/tool-presentation.d.ts +13 -0
- package/dist/core/antigravity/tool-presentation.js +20 -0
- package/dist/core/assistant-client/ag-ui-transport.js +14 -2
- package/dist/core/assistant-client/attachment-liveness.js +1 -1
- package/dist/core/assistant-client/durable-run-subscription.js +2 -1
- package/dist/core/assistant-client/transport.js +13 -7
- package/dist/core/assistant-content.js +2 -1
- package/dist/core/composer-history.js +2 -1
- package/dist/core/durable-projection.js +2 -1
- package/dist/core/durable-runs/continuation.js +2 -1
- package/dist/core/events.d.ts +5 -1
- package/dist/core/messages.d.ts +5 -1
- package/dist/core/partial-json.js +18 -9
- package/dist/core/persistence/title.d.ts +1 -1
- package/dist/core/persistence/title.js +25 -7
- package/dist/core/question-form/normalize.js +1 -1
- package/dist/core/question-form/scan.js +25 -15
- package/dist/core/question-form/secret-answers.js +2 -1
- package/dist/core/run-activity.js +8 -4
- package/dist/core/run-events/entry.js +6 -2
- package/dist/core/tool-events.d.ts +18 -8
- package/dist/core/tool-events.js +72 -16
- package/dist/core/tool-presentation.d.ts +14 -0
- package/dist/core/tool-presentation.js +8 -0
- package/dist/core/transcript.js +13 -6
- package/dist/core/turn-usage.js +7 -2
- package/dist/core/user-text-redaction.d.ts +4 -6
- package/dist/core/user-text-redaction.js +13 -6
- package/dist/core/util/manifest.js +13 -11
- package/dist/core/util/markdown-context.d.ts +14 -0
- package/dist/core/util/markdown-context.js +42 -4
- package/dist/core/util/markup-attributes.js +1 -1
- package/dist/core/util/parser.js +7 -7
- package/dist/core/util/pointer.js +22 -6
- package/dist/core/util/recover.js +5 -8
- package/dist/core/util/strip.js +12 -15
- package/dist/core/util/validate.js +30 -18
- package/dist/embed-widget/browser/frontend-session-bridge.d.ts +99 -0
- package/dist/embed-widget/browser/frontend-session-bridge.js +384 -0
- package/dist/embed-widget/browser/lazy-loader.js +16 -12
- package/dist/embed-widget/browser/session-store.d.ts +3 -9
- package/dist/embed-widget/browser/session-store.js +6 -38
- package/dist/embed-widget/react/dialog.hooks.js +2 -2
- package/dist/embed-widget/react/embed.d.ts +3 -10
- package/dist/embed-widget/react/embed.js +8 -52
- package/dist/embed-widget/react/header.hooks.js +1 -1
- package/dist/embed-widget/transports/fetch-sse.js +2 -1
- package/dist/embed-widget/vanilla/index.d.ts +0 -5
- package/dist/embed-widget/vanilla/index.js +23 -26
- package/dist/embed-widget/vanilla/state.d.ts +3 -11
- package/dist/embed-widget/vanilla/state.js +10 -39
- package/dist/embed-widget/vanilla/style.css +1 -1
- package/dist/react/agent-bridge/frontend-session-bridge.d.ts +2 -56
- package/dist/react/agent-bridge/frontend-session-bridge.js +2 -248
- package/dist/react/components/AttachmentPreviewModal.hooks.js +2 -1
- package/dist/react/components/ChatSettingsDialog.js +18 -9
- package/dist/react/components/Composer.js +5 -4
- package/dist/react/components/ConversationList.js +5 -4
- package/dist/react/components/Icon.d.ts +1 -1
- package/dist/react/components/Icon.js +1 -0
- package/dist/react/components/Markdown.d.ts +1 -0
- package/dist/react/components/Markdown.js +30 -27
- package/dist/react/components/McpUiSurfaceCard.js +2 -1
- package/dist/react/components/MessageList.d.ts +1 -1
- package/dist/react/components/MessageList.hooks.js +3 -2
- package/dist/react/components/MessageRow.d.ts +3 -0
- package/dist/react/components/MessageRow.js +32 -14
- package/dist/react/components/MessageRow.usage.js +3 -1
- package/dist/react/components/QuestionForm.js +4 -5
- package/dist/react/components/SentAttachment.js +4 -1
- package/dist/react/components/TodoCard.js +9 -1
- package/dist/react/components/ToolCard.d.ts +3 -1
- package/dist/react/components/ToolCard.js +84 -64
- package/dist/react/components/ToolCard.rules.d.ts +20 -0
- package/dist/react/components/ToolCard.rules.js +92 -0
- package/dist/react/components/composer-discovery.js +8 -8
- package/dist/react/features/chat-pane/components/AgentPluginUsage.js +1 -2
- package/dist/react/features/chat-pane/components/AgentRuntimePicker.d.ts +1 -1
- package/dist/react/features/chat-pane/components/AgentRuntimePicker.js +31 -9
- package/dist/react/features/chat-pane/components/ChatPane.js +7 -5
- package/dist/react/features/chat-pane/components/RuntimeDropUp.js +1 -1
- package/dist/react/features/chat-pane/create-daemon-attachment-uploader.js +1 -1
- package/dist/react/features/chat-pane/hooks/useAgentRuntimePicker.hooks.js +2 -2
- package/dist/react/features/chat-pane/hooks/useChatPane.hooks.d.ts +3 -1
- package/dist/react/features/chat-pane/hooks/useChatPane.hooks.js +13 -5
- package/dist/react/features/chat-pane/hooks/useChatPaneAgentControl.hooks.js +2 -2
- package/dist/react/features/chat-pane/hooks/useChatPaneControlsHeight.hooks.js +1 -1
- package/dist/react/features/chat-pane/hooks/useChatPaneRuntimeInventory.hooks.js +2 -2
- package/dist/react/features/chat-pane/hooks/useChatPaneWorkingDirectory.hooks.js +2 -2
- package/dist/react/features/chat-pane/hooks/useRuntimeDropUp.hooks.js +2 -2
- package/dist/react/features/chat-pane/model-options.js +4 -2
- package/dist/react/features/chat-pane/styles.d.ts +1 -1
- package/dist/react/features/chat-pane/styles.js +95 -5
- package/dist/react/features/chat-pane/types.d.ts +11 -0
- package/dist/react/hooks/assistant-chats.js +22 -8
- package/dist/react/hooks/useArtifactStream.js +2 -1
- package/dist/react/hooks/useComposer.js +2 -2
- package/dist/react/hooks/useComposerHistoryKeys.hooks.js +2 -2
- package/dist/react/hooks/useConversation.js +9 -4
- package/dist/react/hooks/useExtEventGroups.js +4 -2
- package/dist/react/hooks/useLatestOperation.js +2 -1
- package/dist/react/hooks/useQuestionForms.js +32 -22
- package/dist/react/hooks/useRunStream.js +4 -2
- package/dist/react/hooks/useUserTextGuard.d.ts +1 -0
- package/dist/react/hooks/useUserTextGuard.js +3 -1
- package/dist/react/message-blocks.js +4 -2
- package/dist/react/styles/reference.css +9 -0
- package/dist/react/tool-call-display.d.ts +11 -0
- package/dist/react/tool-call-display.js +59 -2
- package/dist/server/durable-runs/recover.js +13 -5
- package/dist/server/run-finalizer/entry.js +1 -0
- package/dist/store/runtime/chat-history-store.js +9 -3
- package/dist/store/runtime/durable-run-store.js +34 -24
- package/dist/store/runtime/run-ledger.js +7 -4
- package/dist/store/sql/boundary.js +3 -1
- package/dist/store/sql/cursor.js +8 -3
- package/dist/store/sql/store.js +36 -28
- package/package.json +93 -88
- package/dist/core/browser/page-actions.d.ts +0 -121
- package/dist/core/browser/page-actions.js +0 -115
- package/dist/embed-widget/browser/page-actions.d.ts +0 -2
- 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(
|
|
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(
|
|
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(() => {
|
|
32
|
-
|
|
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(
|
|
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(
|
|
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
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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(
|
|
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
|
|
35
|
-
* Corrupt transcripts are cleared as a whole.
|
|
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
|
|
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
|
|
3
|
-
* Corrupt transcripts are cleared as a whole.
|
|
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
|
|
8
|
+
* worst trimming case. State is instance-local.
|
|
10
9
|
*/
|
|
11
10
|
export function createBrowserSessionStore(args) {
|
|
12
|
-
const { storage, transcriptKey,
|
|
13
|
-
if (!transcriptKey
|
|
14
|
-
throw new Error('Transcript
|
|
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
|
|
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.
|
|
35
|
-
*
|
|
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,
|
|
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
|
|
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
|
|
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.
|
|
24
|
-
*
|
|
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,
|
|
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
|
|
52
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
if (state.busy && composerHadFocus) {
|
|
186
188
|
stop.focus();
|
|
187
|
-
|
|
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
|
-
|
|
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
|
|
29
|
-
* Restored messages never replay
|
|
30
|
-
*
|
|
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;
|