@jini-ai/chat 0.6.5 → 0.6.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/dist/core/accepted-input.d.ts +54 -0
- package/dist/core/accepted-input.js +165 -0
- package/dist/core/agent-plugin-chat-state.d.ts +43 -0
- package/dist/core/agent-plugin-chat-state.js +48 -0
- package/dist/core/assistant-client/a2ui-action-poster.js +32 -28
- package/dist/core/assistant-client/messages.d.ts +1 -1
- package/dist/core/assistant-client/messages.js +2 -2
- package/dist/core/assistant-client/transport.d.ts +2 -2
- package/dist/core/assistant-client/transport.js +184 -97
- package/dist/core/assistant-content.d.ts +2 -1
- package/dist/core/assistant-content.js +9 -11
- package/dist/core/browser/page-actions.js +4 -2
- package/dist/core/composer-history.d.ts +16 -9
- package/dist/core/composer-history.js +28 -16
- package/dist/core/conversation-title.d.ts +3 -0
- package/dist/core/conversation-title.js +3 -0
- package/dist/core/durable-projection.js +20 -13
- package/dist/core/durable-runs/continuation.d.ts +4 -0
- package/dist/core/durable-runs/continuation.js +35 -6
- package/dist/core/durable-runs/ledger-contracts.d.ts +2 -1
- package/dist/core/durable-runs/policies.d.ts +1 -0
- package/dist/core/durable-runs/policies.js +1 -0
- package/dist/core/durable-runs/ports.d.ts +6 -2
- package/dist/core/index.d.ts +2 -0
- package/dist/core/index.js +2 -0
- package/dist/core/messages.d.ts +2 -0
- package/dist/core/partial-json.d.ts +6 -7
- package/dist/core/partial-json.js +81 -47
- package/dist/core/persistence/title.js +4 -2
- package/dist/core/question-answer.d.ts +23 -0
- package/dist/core/question-answer.js +66 -0
- package/dist/core/question-form/format.d.ts +6 -6
- package/dist/core/question-form/format.js +16 -17
- package/dist/core/question-form/normalize.d.ts +18 -0
- package/dist/core/question-form/normalize.js +112 -122
- package/dist/core/question-form/scan.js +141 -203
- package/dist/core/run-activity.js +62 -49
- package/dist/core/run-events/entry.d.ts +10 -1
- package/dist/core/run-events/entry.js +117 -87
- package/dist/core/todos.d.ts +1 -1
- package/dist/core/todos.js +60 -85
- package/dist/core/tool-events.js +14 -12
- package/dist/core/transcript.js +62 -16
- package/dist/core/turn-usage.d.ts +2 -1
- package/dist/core/turn-usage.js +56 -27
- package/dist/core/user-text-redaction.js +3 -0
- package/dist/core/util/manifest.js +77 -82
- package/dist/core/util/markdown-context.js +51 -53
- package/dist/core/util/parser.js +136 -144
- package/dist/core/util/pointer.js +16 -16
- package/dist/core/util/strip.js +57 -67
- package/dist/core/util/validate.js +15 -17
- package/dist/embed-widget/browser/session-store.js +21 -12
- package/dist/embed-widget/transports/fetch-sse.js +108 -95
- package/dist/embed-widget/vanilla/index.js +81 -70
- package/dist/react/agent-bridge/frontend-session-bridge.js +80 -52
- package/dist/react/components/AttachmentPreviewModal.js +25 -1
- package/dist/react/components/ChatSettingsDialog.d.ts +13 -0
- package/dist/react/components/ChatSettingsDialog.hooks.d.ts +115 -0
- package/dist/react/components/ChatSettingsDialog.hooks.js +40 -0
- package/dist/react/components/ChatSettingsDialog.js +72 -0
- package/dist/react/components/ChatSettingsSegmented.d.ts +18 -0
- package/dist/react/components/ChatSettingsSegmented.js +11 -0
- package/dist/react/components/Composer.d.ts +5 -1
- package/dist/react/components/Composer.js +126 -82
- package/dist/react/components/ConversationList.js +25 -39
- package/dist/react/components/ConversationTitleEditor.d.ts +27 -0
- package/dist/react/components/ConversationTitleEditor.js +54 -0
- package/dist/react/components/Icon.d.ts +1 -1
- package/dist/react/components/Icon.js +25 -45
- package/dist/react/components/Markdown.js +134 -123
- package/dist/react/components/McpUiSurfaceCard.d.ts +2 -2
- package/dist/react/components/McpUiSurfaceCard.js +30 -9
- package/dist/react/components/MessageList.d.ts +13 -13
- package/dist/react/components/MessageList.hooks.d.ts +6 -1
- package/dist/react/components/MessageList.hooks.js +19 -3
- package/dist/react/components/MessageList.js +34 -10
- package/dist/react/components/MessageRow.js +48 -32
- package/dist/react/components/MessageRow.usage.d.ts +5 -2
- package/dist/react/components/MessageRow.usage.js +20 -11
- package/dist/react/components/MidRunUserMessage.js +16 -3
- package/dist/react/components/QuestionForm.js +148 -76
- package/dist/react/components/QuestionsPanel.js +1 -1
- package/dist/react/components/TodoCard.js +12 -6
- package/dist/react/components/ToolCard.js +47 -40
- package/dist/react/components/composer-discovery.js +6 -1
- package/dist/react/components/mcp-ui-surface-state.d.ts +6 -11
- package/dist/react/components/mcp-ui-surface-state.js +23 -8
- package/dist/react/ext-event-renderer-registry.d.ts +8 -0
- package/dist/react/ext-event-renderer-registry.js +5 -2
- package/dist/react/features/chat-pane/components/AgentPluginUsage.d.ts +7 -0
- package/dist/react/features/chat-pane/components/AgentPluginUsage.js +45 -0
- package/dist/react/features/chat-pane/components/AgentRuntimePicker.js +55 -19
- package/dist/react/features/chat-pane/components/ChatPane.d.ts +1 -1
- package/dist/react/features/chat-pane/components/ChatPane.js +192 -74
- package/dist/react/features/chat-pane/components/RuntimeDropUp.d.ts +29 -0
- package/dist/react/features/chat-pane/components/RuntimeDropUp.js +45 -0
- package/dist/react/features/chat-pane/create-daemon-attachment-uploader.js +80 -47
- package/dist/react/features/chat-pane/create-mcp-ui-tool-caller.js +44 -40
- package/dist/react/features/chat-pane/create-mid-run-message-poster.d.ts +6 -6
- package/dist/react/features/chat-pane/create-mid-run-message-poster.js +8 -8
- package/dist/react/features/chat-pane/create-typed-answer-poster.d.ts +3 -3
- package/dist/react/features/chat-pane/create-typed-answer-poster.js +3 -3
- package/dist/react/features/chat-pane/hooks/useAgentRuntimePicker.hooks.d.ts +6 -1
- package/dist/react/features/chat-pane/hooks/useAgentRuntimePicker.hooks.js +11 -5
- package/dist/react/features/chat-pane/hooks/useChatPane.hooks.d.ts +12 -7
- package/dist/react/features/chat-pane/hooks/useChatPane.hooks.js +340 -149
- package/dist/react/features/chat-pane/hooks/useChatPaneControlsHeight.hooks.js +7 -6
- package/dist/react/features/chat-pane/hooks/useChatPaneNewTabLinks.hooks.js +28 -10
- package/dist/react/features/chat-pane/hooks/useChatPaneRuntimeInventory.hooks.d.ts +3 -3
- package/dist/react/features/chat-pane/hooks/useChatPaneRuntimeInventory.hooks.js +60 -12
- package/dist/react/features/chat-pane/hooks/useRuntimeDropUp.hooks.d.ts +49 -0
- package/dist/react/features/chat-pane/hooks/useRuntimeDropUp.hooks.js +143 -0
- package/dist/react/features/chat-pane/model-options.d.ts +18 -1
- package/dist/react/features/chat-pane/model-options.js +49 -20
- package/dist/react/features/chat-pane/rules.d.ts +8 -9
- package/dist/react/features/chat-pane/rules.js +84 -46
- package/dist/react/features/chat-pane/styles.d.ts +1 -1
- package/dist/react/features/chat-pane/styles.js +156 -37
- package/dist/react/features/chat-pane/types.d.ts +17 -8
- package/dist/react/hooks/assistant-chats.js +12 -5
- package/dist/react/hooks/composer-draft-cache.js +116 -118
- package/dist/react/hooks/composer-history-storage.js +5 -5
- package/dist/react/hooks/fab-position.js +7 -4
- package/dist/react/hooks/useAnsweredCards.d.ts +21 -4
- package/dist/react/hooks/useAnsweredCards.js +38 -11
- package/dist/react/hooks/useArtifactStream.js +28 -22
- package/dist/react/hooks/useComposer.d.ts +20 -0
- package/dist/react/hooks/useComposer.js +22 -9
- package/dist/react/hooks/useComposerHistory.hooks.d.ts +6 -1
- package/dist/react/hooks/useComposerHistory.hooks.js +39 -19
- package/dist/react/hooks/useComposerHistoryKeys.hooks.js +24 -14
- package/dist/react/hooks/useConversation.d.ts +7 -0
- package/dist/react/hooks/useConversation.js +23 -10
- package/dist/react/hooks/useExtEventGroups.js +39 -39
- package/dist/react/hooks/useQuestionForms.js +21 -26
- package/dist/react/hooks/useRunStream.js +10 -2
- package/dist/react/index.d.ts +7 -1
- package/dist/react/index.js +9 -0
- package/dist/react/message-blocks.d.ts +6 -4
- package/dist/react/message-blocks.js +94 -72
- package/dist/react/slots.d.ts +2 -2
- package/dist/react/tool-call-display.js +19 -17
- package/dist/server/durable-runs/recover.d.ts +1 -1
- package/dist/server/durable-runs/recover.js +139 -76
- package/dist/server/run-finalizer/entry.js +25 -15
- package/dist/store/legacy/sqlite/conversations.d.ts +11 -11
- package/dist/store/legacy/sqlite/conversations.js +69 -98
- package/dist/store/legacy/sqlite/messages.d.ts +10 -2
- package/dist/store/legacy/sqlite/messages.js +113 -71
- package/dist/store/runtime/chat-history-store.js +9 -3
- package/dist/store/runtime/durable-run-store.d.ts +2 -2
- package/dist/store/runtime/durable-run-store.js +165 -117
- package/dist/store/runtime/history-tools.js +118 -82
- package/dist/store/runtime/run-ledger.d.ts +4 -1
- package/dist/store/runtime/run-ledger.js +56 -16
- package/dist/store/sql/cursor.js +38 -18
- package/dist/store/sql/store.d.ts +18 -0
- package/dist/store/sql/store.js +46 -15
- package/package.json +12 -10
|
@@ -19,7 +19,6 @@ export function createBrowserSessionStore(args) {
|
|
|
19
19
|
if (!Number.isSafeInteger(maxBytes) || maxBytes < 28)
|
|
20
20
|
throw new Error('Invalid byte limit');
|
|
21
21
|
const empty = () => ({ open: false, messages: [] });
|
|
22
|
-
const bytes = (value) => new TextEncoder().encode(value).length;
|
|
23
22
|
const clear = (_args) => {
|
|
24
23
|
try {
|
|
25
24
|
storage.removeItem(transcriptKey);
|
|
@@ -38,16 +37,7 @@ export function createBrowserSessionStore(args) {
|
|
|
38
37
|
if (raw === null)
|
|
39
38
|
return empty();
|
|
40
39
|
try {
|
|
41
|
-
|
|
42
|
-
throw new Error('Transcript exceeds byte limit');
|
|
43
|
-
const value = JSON.parse(raw);
|
|
44
|
-
if (typeof value !== 'object' || value === null || Array.isArray(value))
|
|
45
|
-
throw new Error('Invalid envelope');
|
|
46
|
-
const state = value;
|
|
47
|
-
if (typeof state.open !== 'boolean' || !Array.isArray(state.messages)
|
|
48
|
-
|| state.messages.length > maxMessages || !state.messages.every(value => validateMessage({ value })))
|
|
49
|
-
throw new Error('Invalid transcript');
|
|
50
|
-
return { open: state.open, messages: state.messages };
|
|
40
|
+
return parseTranscript({ raw, maxBytes, maxMessages, validateMessage });
|
|
51
41
|
}
|
|
52
42
|
catch {
|
|
53
43
|
clear({});
|
|
@@ -58,7 +48,7 @@ export function createBrowserSessionStore(args) {
|
|
|
58
48
|
try {
|
|
59
49
|
let messages = maxMessages === 0 ? [] : state.messages.slice(-maxMessages);
|
|
60
50
|
let serialized = JSON.stringify({ open: state.open, messages });
|
|
61
|
-
while (messages.length > 0 &&
|
|
51
|
+
while (messages.length > 0 && utf8Bytes(serialized) > maxBytes) {
|
|
62
52
|
messages = messages.slice(1);
|
|
63
53
|
serialized = JSON.stringify({ open: state.open, messages });
|
|
64
54
|
}
|
|
@@ -100,4 +90,23 @@ export function createBrowserSessionStore(args) {
|
|
|
100
90
|
},
|
|
101
91
|
};
|
|
102
92
|
}
|
|
93
|
+
const utf8Bytes = (value) => new TextEncoder().encode(value).length;
|
|
94
|
+
/**
|
|
95
|
+
* Parses and validates one stored transcript envelope. Throws on any defect so the caller can
|
|
96
|
+
* clear the whole entry (no partial recovery). Oversized envelopes are rejected before parsing.
|
|
97
|
+
*/
|
|
98
|
+
function parseTranscript(args) {
|
|
99
|
+
const { raw, maxBytes, maxMessages, validateMessage } = args;
|
|
100
|
+
if (utf8Bytes(raw) > maxBytes)
|
|
101
|
+
throw new Error('Transcript exceeds byte limit');
|
|
102
|
+
const value = JSON.parse(raw);
|
|
103
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value))
|
|
104
|
+
throw new Error('Invalid envelope');
|
|
105
|
+
const state = value;
|
|
106
|
+
if (typeof state.open !== 'boolean' || !Array.isArray(state.messages))
|
|
107
|
+
throw new Error('Invalid transcript');
|
|
108
|
+
if (state.messages.length > maxMessages || !state.messages.every(value => validateMessage({ value })))
|
|
109
|
+
throw new Error('Invalid transcript');
|
|
110
|
+
return { open: state.open, messages: state.messages };
|
|
111
|
+
}
|
|
103
112
|
//# sourceMappingURL=session-store.js.map
|
|
@@ -8,19 +8,17 @@ export function mapJsonChatSseFrame({ frame, directiveEventName, streamError })
|
|
|
8
8
|
return {};
|
|
9
9
|
const data = JSON.parse(frame.data);
|
|
10
10
|
const obj = typeof data === 'object' && data !== null ? data : {};
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
11
|
+
switch (frame.event) {
|
|
12
|
+
case 'text':
|
|
13
|
+
if (typeof obj.delta !== 'string')
|
|
14
|
+
throw new Error('Invalid text frame');
|
|
15
|
+
return { event: { kind: 'text', text: obj.delta } };
|
|
16
|
+
case 'error': return { error: new Error(nonEmptyString(obj.message) ?? streamError) };
|
|
17
|
+
case 'end': return { done: true };
|
|
18
|
+
default: return frame.event === directiveEventName ? { event: { kind: 'ext', name: directiveEventName, data } } : {};
|
|
15
19
|
}
|
|
16
|
-
if (frame.event === 'error')
|
|
17
|
-
return { error: new Error(typeof obj.message === 'string' && obj.message ? obj.message : streamError) };
|
|
18
|
-
if (frame.event === 'end')
|
|
19
|
-
return { done: true };
|
|
20
|
-
if (frame.event === directiveEventName)
|
|
21
|
-
return { event: { kind: 'ext', name: directiveEventName, data } };
|
|
22
|
-
return {};
|
|
23
20
|
}
|
|
21
|
+
const nonEmptyString = (value) => (typeof value === 'string' && value ? value : undefined);
|
|
24
22
|
async function* responseChunks(body, signal) {
|
|
25
23
|
const reader = body.getReader();
|
|
26
24
|
const cancel = () => { void reader.cancel().catch(() => { }); };
|
|
@@ -71,15 +69,11 @@ export function createFetchSseTransport(args, options = {}) {
|
|
|
71
69
|
};
|
|
72
70
|
return {
|
|
73
71
|
async startRun(input, handlers) {
|
|
74
|
-
|
|
75
|
-
while (latestIndex >= 0 && input.history[latestIndex]?.role !== 'user')
|
|
76
|
-
latestIndex -= 1;
|
|
72
|
+
const latestIndex = latestUserIndex(input.history);
|
|
77
73
|
const message = input.history[latestIndex]?.content;
|
|
78
74
|
if (!message)
|
|
79
75
|
throw new Error(args.copy.noUserMessage);
|
|
80
|
-
const history = (
|
|
81
|
-
.filter(m => m.content.trim().length > 0)
|
|
82
|
-
.map(m => ({ role: m.role, content: m.content.slice(0, args.maxHistoryMessageChars) }));
|
|
76
|
+
const history = priorHistory({ history: input.history, latestIndex, maxMessages: args.maxHistoryMessages, maxChars: args.maxHistoryMessageChars });
|
|
83
77
|
// Build before registering a run, so a synchronous host-builder failure leaks no state.
|
|
84
78
|
const request = args.requestBuilder({ endpoint: args.endpoint, agentId: args.agentId, message, history, input });
|
|
85
79
|
const runId = args.ids({});
|
|
@@ -87,84 +81,8 @@ export function createFetchSseTransport(args, options = {}) {
|
|
|
87
81
|
throw new Error('Run ID must be unique and nonempty');
|
|
88
82
|
const controller = options.createAbortController?.({}) ?? new AbortController();
|
|
89
83
|
activeRuns.set(runId, controller);
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
const signals = [input.signal, ...(input.cancelSignal ? [input.cancelSignal] : [])];
|
|
93
|
-
const finish = () => {
|
|
94
|
-
if (settled)
|
|
95
|
-
return;
|
|
96
|
-
settled = true;
|
|
97
|
-
activeRuns.delete(runId);
|
|
98
|
-
for (const signal of signals)
|
|
99
|
-
signal.removeEventListener('abort', abort);
|
|
100
|
-
controller.signal.removeEventListener('abort', onAborted);
|
|
101
|
-
observe(runId, 'settled');
|
|
102
|
-
handlers.onDone([...collected]);
|
|
103
|
-
};
|
|
104
|
-
const onAborted = () => { observe(runId, 'aborted'); finish(); };
|
|
105
|
-
const abort = () => controller.abort();
|
|
106
|
-
controller.signal.addEventListener('abort', onAborted, { once: true });
|
|
107
|
-
for (const signal of signals)
|
|
108
|
-
signal.addEventListener('abort', abort, { once: true });
|
|
109
|
-
observe(runId, 'started');
|
|
110
|
-
if (signals.some(signal => signal.aborted))
|
|
111
|
-
abort();
|
|
112
|
-
void (async () => {
|
|
113
|
-
try {
|
|
114
|
-
if (settled)
|
|
115
|
-
return;
|
|
116
|
-
const response = await args.fetch(args.endpoint, { ...request, signal: controller.signal });
|
|
117
|
-
if (settled) {
|
|
118
|
-
try {
|
|
119
|
-
await response.body?.cancel();
|
|
120
|
-
}
|
|
121
|
-
catch { /* closed body */ }
|
|
122
|
-
return;
|
|
123
|
-
}
|
|
124
|
-
if (!response.ok) {
|
|
125
|
-
const body = await response.json().catch(() => null);
|
|
126
|
-
if (settled)
|
|
127
|
-
return;
|
|
128
|
-
const error = typeof body === 'object' && body !== null && 'error' in body && typeof body.error === 'string' && body.error
|
|
129
|
-
? body.error : args.copy.requestFailed({ status: response.status });
|
|
130
|
-
observe(runId, 'error');
|
|
131
|
-
handlers.onError(new Error(error));
|
|
132
|
-
return;
|
|
133
|
-
}
|
|
134
|
-
if (!response.body)
|
|
135
|
-
throw new Error(args.copy.missingBody);
|
|
136
|
-
for await (const frame of args.decode({ source: responseChunks(response.body, controller.signal) })) {
|
|
137
|
-
if (settled)
|
|
138
|
-
break;
|
|
139
|
-
const result = args.frameMapper({ frame });
|
|
140
|
-
if (result.event) {
|
|
141
|
-
collected.push(result.event);
|
|
142
|
-
handlers.onEvent(result.event);
|
|
143
|
-
}
|
|
144
|
-
if (settled)
|
|
145
|
-
break;
|
|
146
|
-
if (result.error) {
|
|
147
|
-
observe(runId, 'error');
|
|
148
|
-
handlers.onError(result.error);
|
|
149
|
-
}
|
|
150
|
-
if (settled)
|
|
151
|
-
break;
|
|
152
|
-
if (result.done) {
|
|
153
|
-
finish();
|
|
154
|
-
break;
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
catch (error) {
|
|
159
|
-
if (!settled && !controller.signal.aborted) {
|
|
160
|
-
observe(runId, 'error');
|
|
161
|
-
handlers.onError(error instanceof Error ? error : new Error(String(error)));
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
finally {
|
|
165
|
-
finish();
|
|
166
|
-
}
|
|
167
|
-
})();
|
|
84
|
+
const run = openRun({ controller, input, handlers }, { observe: phase => observe(runId, phase), release: () => activeRuns.delete(runId) });
|
|
85
|
+
void pumpRun({ run, args, request });
|
|
168
86
|
return { runId };
|
|
169
87
|
},
|
|
170
88
|
async reattachRun(_runId, handlers) { handlers.onDone([]); },
|
|
@@ -172,4 +90,99 @@ export function createFetchSseTransport(args, options = {}) {
|
|
|
172
90
|
async stopRun(runId) { activeRuns.get(runId)?.abort(); },
|
|
173
91
|
};
|
|
174
92
|
}
|
|
93
|
+
function latestUserIndex(history) {
|
|
94
|
+
let index = history.length - 1;
|
|
95
|
+
while (index >= 0 && history[index]?.role !== 'user')
|
|
96
|
+
index -= 1;
|
|
97
|
+
return index;
|
|
98
|
+
}
|
|
99
|
+
/** Prior turns only (the latest user turn travels as `message`), non-empty, count- and char-bounded. */
|
|
100
|
+
function priorHistory({ history, latestIndex, maxMessages, maxChars }) {
|
|
101
|
+
return (maxMessages === 0 ? [] : history.slice(Math.max(0, latestIndex - maxMessages), latestIndex))
|
|
102
|
+
.filter(m => m.content.trim().length > 0)
|
|
103
|
+
.map(m => ({ role: m.role, content: m.content.slice(0, maxChars) }));
|
|
104
|
+
}
|
|
105
|
+
/** Wires abort listeners and returns the settle-once handle; an already-aborted signal aborts at once. */
|
|
106
|
+
function openRun({ controller, input, handlers }, { observe, release }) {
|
|
107
|
+
const collected = [];
|
|
108
|
+
let settled = false;
|
|
109
|
+
const signals = [input.signal, ...(input.cancelSignal ? [input.cancelSignal] : [])];
|
|
110
|
+
const finish = () => {
|
|
111
|
+
if (settled)
|
|
112
|
+
return;
|
|
113
|
+
settled = true;
|
|
114
|
+
release();
|
|
115
|
+
for (const signal of signals)
|
|
116
|
+
signal.removeEventListener('abort', abort);
|
|
117
|
+
controller.signal.removeEventListener('abort', onAborted);
|
|
118
|
+
observe('settled');
|
|
119
|
+
handlers.onDone([...collected]);
|
|
120
|
+
};
|
|
121
|
+
const onAborted = () => { observe('aborted'); finish(); };
|
|
122
|
+
const abort = () => controller.abort();
|
|
123
|
+
controller.signal.addEventListener('abort', onAborted, { once: true });
|
|
124
|
+
for (const signal of signals)
|
|
125
|
+
signal.addEventListener('abort', abort, { once: true });
|
|
126
|
+
observe('started');
|
|
127
|
+
if (signals.some(signal => signal.aborted))
|
|
128
|
+
abort();
|
|
129
|
+
const report = (error) => { observe('error'); handlers.onError(error); };
|
|
130
|
+
return { controller, handlers, collected, isSettled: () => settled, finish, report };
|
|
131
|
+
}
|
|
132
|
+
/** Performs the POST and streams frames; always settles the run exactly once when it returns. */
|
|
133
|
+
async function pumpRun({ run, args, request }) {
|
|
134
|
+
try {
|
|
135
|
+
if (run.isSettled())
|
|
136
|
+
return;
|
|
137
|
+
const response = await args.fetch(args.endpoint, { ...request, signal: run.controller.signal });
|
|
138
|
+
await deliverResponse({ run, args, response });
|
|
139
|
+
}
|
|
140
|
+
catch (error) {
|
|
141
|
+
if (!run.isSettled() && !run.controller.signal.aborted)
|
|
142
|
+
run.report(error instanceof Error ? error : new Error(String(error)));
|
|
143
|
+
}
|
|
144
|
+
finally {
|
|
145
|
+
run.finish();
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
async function deliverResponse({ run, args, response }) {
|
|
149
|
+
if (run.isSettled()) {
|
|
150
|
+
try {
|
|
151
|
+
await response.body?.cancel();
|
|
152
|
+
}
|
|
153
|
+
catch { /* closed body */ }
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
if (!response.ok)
|
|
157
|
+
return reportHttpFailure({ run, response, requestFailed: args.copy.requestFailed });
|
|
158
|
+
if (!response.body)
|
|
159
|
+
throw new Error(args.copy.missingBody);
|
|
160
|
+
for await (const frame of args.decode({ source: responseChunks(response.body, run.controller.signal) })) {
|
|
161
|
+
if (run.isSettled() || !dispatchFrame({ run, result: args.frameMapper({ frame }) }))
|
|
162
|
+
break;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
async function reportHttpFailure({ run, response, requestFailed }) {
|
|
166
|
+
const body = await response.json().catch(() => null);
|
|
167
|
+
if (run.isSettled())
|
|
168
|
+
return;
|
|
169
|
+
const serverError = typeof body === 'object' && body !== null && 'error' in body ? nonEmptyString(body.error) : undefined;
|
|
170
|
+
run.report(new Error(serverError ?? requestFailed({ status: response.status })));
|
|
171
|
+
}
|
|
172
|
+
/** Delivers one mapped frame; returns false once the run is settled (by a handler or an end frame). */
|
|
173
|
+
function dispatchFrame({ run, result }) {
|
|
174
|
+
if (result.event) {
|
|
175
|
+
run.collected.push(result.event);
|
|
176
|
+
run.handlers.onEvent(result.event);
|
|
177
|
+
}
|
|
178
|
+
if (run.isSettled())
|
|
179
|
+
return false;
|
|
180
|
+
if (result.error)
|
|
181
|
+
run.report(result.error);
|
|
182
|
+
if (run.isSettled() || result.done) {
|
|
183
|
+
run.finish();
|
|
184
|
+
return false;
|
|
185
|
+
}
|
|
186
|
+
return true;
|
|
187
|
+
}
|
|
175
188
|
//# sourceMappingURL=fetch-sse.js.map
|
|
@@ -95,7 +95,8 @@ export function mountVanillaChat(args, options = {}) {
|
|
|
95
95
|
const suggestions = node('div', 'jini-chat-vanilla__suggestions');
|
|
96
96
|
if (copy.suggestionsLabel)
|
|
97
97
|
suggestions.setAttribute('aria-label', copy.suggestionsLabel);
|
|
98
|
-
const
|
|
98
|
+
const suggestionEntries = (args.suggestions ?? []).map(text => ({ text, button: button(text) }));
|
|
99
|
+
const suggestionButtons = suggestionEntries.map(entry => entry.button);
|
|
99
100
|
suggestions.append(...suggestionButtons);
|
|
100
101
|
log.append(greeting, suggestions);
|
|
101
102
|
panel.append(header, confirmation, log, status, announcements, error, proposal, form);
|
|
@@ -123,6 +124,39 @@ export function mountVanillaChat(args, options = {}) {
|
|
|
123
124
|
proposal.hidden = state.proposal === null;
|
|
124
125
|
proposalText.textContent = state.proposal ? copy.proposalLabel({ title: state.proposal.navigate.target.title }) : '';
|
|
125
126
|
};
|
|
127
|
+
const createRow = ({ message }) => {
|
|
128
|
+
const row = { element: node('div', `jini-chat-vanilla__message jini-chat-vanilla__message--${message.role}`), content: '\0' };
|
|
129
|
+
rows.set(message.id, row);
|
|
130
|
+
log.append(row.element);
|
|
131
|
+
return row;
|
|
132
|
+
};
|
|
133
|
+
const renderRow = ({ message }) => {
|
|
134
|
+
const row = rows.get(message.id) ?? createRow({ message });
|
|
135
|
+
if (row.content === message.content)
|
|
136
|
+
return;
|
|
137
|
+
if (message.role === 'user')
|
|
138
|
+
row.element.textContent = message.content;
|
|
139
|
+
else
|
|
140
|
+
args.renderMarkdown({ content: message.content, element: row.element });
|
|
141
|
+
row.content = message.content;
|
|
142
|
+
};
|
|
143
|
+
const announce = ({ text }) => {
|
|
144
|
+
if (!text)
|
|
145
|
+
return;
|
|
146
|
+
announcements.append(node('span', '', text));
|
|
147
|
+
// Bound the additions-only live region without replaying earlier replies.
|
|
148
|
+
while (announcements.childElementCount > 2)
|
|
149
|
+
announcements.firstElementChild.remove();
|
|
150
|
+
};
|
|
151
|
+
const announceSettledReplies = ({ messages }) => {
|
|
152
|
+
// A dedicated additions-only region announces each LIVE settled reply once. Markdown rows
|
|
153
|
+
// may be rebuilt while streaming, but restored transcripts and old text stay silent.
|
|
154
|
+
for (const message of messages) {
|
|
155
|
+
if (message.role !== 'assistant' || message.runStatus === 'running' || !liveReplies.delete(message.id))
|
|
156
|
+
continue;
|
|
157
|
+
announce({ text: rows.get(message.id)?.element.textContent });
|
|
158
|
+
}
|
|
159
|
+
};
|
|
126
160
|
const renderRows = ({ state }) => {
|
|
127
161
|
const pinned = log.scrollHeight - log.scrollTop - log.clientHeight < 80;
|
|
128
162
|
const ids = new Set(state.messages.map(message => message.id));
|
|
@@ -131,53 +165,33 @@ export function mountVanillaChat(args, options = {}) {
|
|
|
131
165
|
row.element.remove();
|
|
132
166
|
rows.delete(id);
|
|
133
167
|
}
|
|
134
|
-
for (const message of state.messages)
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
row = { element: node('div', `jini-chat-vanilla__message jini-chat-vanilla__message--${message.role}`), content: '\0' };
|
|
138
|
-
rows.set(message.id, row);
|
|
139
|
-
log.append(row.element);
|
|
140
|
-
}
|
|
141
|
-
if (row.content !== message.content) {
|
|
142
|
-
if (message.role === 'user')
|
|
143
|
-
row.element.textContent = message.content;
|
|
144
|
-
else
|
|
145
|
-
args.renderMarkdown({ content: message.content, element: row.element });
|
|
146
|
-
row.content = message.content;
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
// A dedicated additions-only region announces each LIVE settled reply once. Markdown rows
|
|
150
|
-
// may be rebuilt while streaming, but restored transcripts and old text stay silent.
|
|
151
|
-
for (const message of state.messages) {
|
|
152
|
-
if (message.role !== 'assistant' || message.runStatus === 'running' || !liveReplies.delete(message.id))
|
|
153
|
-
continue;
|
|
154
|
-
const text = rows.get(message.id)?.element.textContent;
|
|
155
|
-
if (text) {
|
|
156
|
-
announcements.append(node('span', '', text));
|
|
157
|
-
// Bound the additions-only live region without replaying earlier replies.
|
|
158
|
-
while (announcements.childElementCount > 2)
|
|
159
|
-
announcements.firstElementChild.remove();
|
|
160
|
-
}
|
|
161
|
-
}
|
|
168
|
+
for (const message of state.messages)
|
|
169
|
+
renderRow({ message });
|
|
170
|
+
announceSettledReplies({ messages: state.messages });
|
|
162
171
|
if (state.messages.length === 0)
|
|
163
172
|
announcements.replaceChildren();
|
|
164
173
|
if (pinned)
|
|
165
174
|
log.scrollTop = log.scrollHeight;
|
|
166
175
|
};
|
|
167
|
-
const
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
176
|
+
const focusEntry = ({ busy }) => { if (busy)
|
|
177
|
+
close.focus();
|
|
178
|
+
else
|
|
179
|
+
input.focus(); };
|
|
180
|
+
const moveFocus = ({ state, composerHadFocus, stopHadFocus }) => {
|
|
181
|
+
if (!state.open)
|
|
182
|
+
return;
|
|
183
|
+
if (!wasOpen && initialized)
|
|
184
|
+
focusEntry({ busy: state.busy });
|
|
185
|
+
else if (state.busy && composerHadFocus)
|
|
175
186
|
stop.focus();
|
|
176
|
-
else if (
|
|
187
|
+
else if (!state.busy && stopHadFocus)
|
|
177
188
|
input.focus();
|
|
189
|
+
};
|
|
190
|
+
const renderFocus = ({ state, composerHadFocus, stopHadFocus, panelHadFocus }) => {
|
|
191
|
+
moveFocus({ state, composerHadFocus, stopHadFocus });
|
|
192
|
+
// renderChrome (which always runs first) already ended modal semantics for a closed panel.
|
|
178
193
|
if (!state.open && wasOpen) {
|
|
179
194
|
confirmation.hidden = true;
|
|
180
|
-
panel.setAttribute('aria-modal', 'false');
|
|
181
195
|
if (panelHadFocus)
|
|
182
196
|
fab.focus();
|
|
183
197
|
}
|
|
@@ -216,8 +230,8 @@ export function mountVanillaChat(args, options = {}) {
|
|
|
216
230
|
target.addEventListener(type, handler);
|
|
217
231
|
listeners.push(() => target.removeEventListener(type, handler));
|
|
218
232
|
};
|
|
219
|
-
|
|
220
|
-
input.value =
|
|
233
|
+
suggestionEntries.forEach(({ text, button }) => listen(button, 'click', () => {
|
|
234
|
+
input.value = text;
|
|
221
235
|
renderChrome({ state: controller.getState({}) });
|
|
222
236
|
input.focus();
|
|
223
237
|
}));
|
|
@@ -234,7 +248,8 @@ export function mountVanillaChat(args, options = {}) {
|
|
|
234
248
|
confirmation.hidden = false;
|
|
235
249
|
keep.focus();
|
|
236
250
|
});
|
|
237
|
-
|
|
251
|
+
const dismissConfirmation = () => { confirmation.hidden = true; reset.focus(); };
|
|
252
|
+
listen(keep, 'click', dismissConfirmation);
|
|
238
253
|
listen(confirm, 'click', () => { confirmation.hidden = true; input.value = ''; controller.reset({}); input.focus(); });
|
|
239
254
|
listen(go, 'click', () => controller.acceptProposal({}));
|
|
240
255
|
listen(stop, 'click', () => controller.cancel({}));
|
|
@@ -257,41 +272,37 @@ export function mountVanillaChat(args, options = {}) {
|
|
|
257
272
|
form.requestSubmit();
|
|
258
273
|
}
|
|
259
274
|
});
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
else
|
|
271
|
-
controller.setOpen({ open: false });
|
|
272
|
-
return;
|
|
273
|
-
}
|
|
274
|
-
if (key.key !== 'Tab')
|
|
275
|
-
return;
|
|
275
|
+
const onEscape = ({ key }) => {
|
|
276
|
+
key.preventDefault();
|
|
277
|
+
if (!confirmation.hidden)
|
|
278
|
+
dismissConfirmation();
|
|
279
|
+
else
|
|
280
|
+
controller.setOpen({ open: false });
|
|
281
|
+
};
|
|
282
|
+
// Only reached while focus is inside the panel (see the document keydown guard), so wrapping
|
|
283
|
+
// happens exactly at the first/last visible enabled control.
|
|
284
|
+
const trapTab = ({ key }) => {
|
|
276
285
|
const focusable = Array.from(panel.querySelectorAll('button:not(:disabled), textarea:not(:disabled), a[href], [tabindex="0"]'))
|
|
277
286
|
.filter(candidate => !candidate.closest('[hidden]'));
|
|
278
287
|
const first = focusable[0];
|
|
279
288
|
const last = focusable.at(-1);
|
|
280
|
-
if (
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
289
|
+
if (document.activeElement !== (key.shiftKey ? first : last))
|
|
290
|
+
return;
|
|
291
|
+
key.preventDefault();
|
|
292
|
+
(key.shiftKey ? last : first)?.focus();
|
|
293
|
+
};
|
|
294
|
+
listen(document, 'keydown', event => {
|
|
295
|
+
if (!controller.getState({}).open || !panel.contains(document.activeElement))
|
|
296
|
+
return;
|
|
297
|
+
const key = event;
|
|
298
|
+
if (key.key === 'Escape')
|
|
299
|
+
onEscape({ key });
|
|
300
|
+
else if (key.key === 'Tab')
|
|
301
|
+
trapTab({ key });
|
|
288
302
|
});
|
|
289
303
|
listen(element, 'jini-chat-open', () => {
|
|
290
304
|
controller.setOpen({ open: true });
|
|
291
|
-
|
|
292
|
-
close.focus();
|
|
293
|
-
else
|
|
294
|
-
input.focus();
|
|
305
|
+
focusEntry({ busy: controller.getState({}).busy });
|
|
295
306
|
});
|
|
296
307
|
const queued = { value: args.session.drain({}) };
|
|
297
308
|
let cancelFrame = () => { };
|
|
@@ -44,14 +44,65 @@ function claimedCapabilities(options) {
|
|
|
44
44
|
if (options.pageDriver !== undefined) {
|
|
45
45
|
claims.push(...PAGE_CAPABILITIES.map((capability) => capability.id));
|
|
46
46
|
}
|
|
47
|
-
|
|
48
|
-
claims.push(prefix);
|
|
49
|
-
}
|
|
47
|
+
claims.push(...Object.keys(options.executors ?? {}));
|
|
50
48
|
return claims;
|
|
51
49
|
}
|
|
52
50
|
function errorMessage(error) {
|
|
53
51
|
return error instanceof Error ? error.message : String(error);
|
|
54
52
|
}
|
|
53
|
+
/** Parses a frame's data into an object, or says why it is not one. */
|
|
54
|
+
function parseFrameObject(data) {
|
|
55
|
+
let parsed;
|
|
56
|
+
try {
|
|
57
|
+
parsed = JSON.parse(data);
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
return { error };
|
|
61
|
+
}
|
|
62
|
+
// `JSON.parse` succeeds on any valid JSON value, not just objects — a bare `null`, number,
|
|
63
|
+
// string or boolean parses cleanly and would throw *outside* this try/catch on the very next
|
|
64
|
+
// `frame['type']` read (`null` is the one of those that actually throws; the others just read
|
|
65
|
+
// back `undefined` harmlessly). Guarding here routes every one of those through the same
|
|
66
|
+
// `onError` path as an unparseable frame, rather than only some of them crashing the listener.
|
|
67
|
+
if (parsed === null || typeof parsed !== 'object') {
|
|
68
|
+
return { error: new Error(`malformed frame: expected a JSON object, got ${JSON.stringify(parsed)}`) };
|
|
69
|
+
}
|
|
70
|
+
return { frame: parsed };
|
|
71
|
+
}
|
|
72
|
+
/** Reads an `invocation` frame into the pane's action shape, refusing one with no usable id. */
|
|
73
|
+
function readInvocation(frame) {
|
|
74
|
+
const rawInvocationId = frame['invocationId'];
|
|
75
|
+
if (typeof rawInvocationId !== 'string' || rawInvocationId.length === 0) {
|
|
76
|
+
return { kind: 'malformed', error: new Error('invocation frame missing invocationId') };
|
|
77
|
+
}
|
|
78
|
+
return {
|
|
79
|
+
kind: 'invocation',
|
|
80
|
+
action: {
|
|
81
|
+
invocationId: rawInvocationId,
|
|
82
|
+
capabilityId: String(frame['capabilityId']),
|
|
83
|
+
input: (frame['input'] ?? {}),
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
/** Classifies one frame's raw data; the listener only applies the result. */
|
|
88
|
+
function readFrame(data) {
|
|
89
|
+
const parsed = parseFrameObject(data);
|
|
90
|
+
if ('error' in parsed)
|
|
91
|
+
return { kind: 'malformed', error: parsed.error };
|
|
92
|
+
const { frame } = parsed;
|
|
93
|
+
switch (frame['type']) {
|
|
94
|
+
case 'attached':
|
|
95
|
+
return { kind: 'attached', sessionId: String(frame['sessionId']), bindToken: String(frame['bindToken']) };
|
|
96
|
+
case 'error':
|
|
97
|
+
return { kind: 'refused', message: String(frame['message']) };
|
|
98
|
+
case 'invocation':
|
|
99
|
+
return readInvocation(frame);
|
|
100
|
+
default:
|
|
101
|
+
// Unknown frame types are ignored rather than treated as errors, so the daemon's event
|
|
102
|
+
// vocabulary can grow without every deployed surface needing to ship first.
|
|
103
|
+
return { kind: 'ignored' };
|
|
104
|
+
}
|
|
105
|
+
}
|
|
55
106
|
/**
|
|
56
107
|
* Opens the surface's connection and starts routing invocations.
|
|
57
108
|
*
|
|
@@ -60,11 +111,8 @@ function errorMessage(error) {
|
|
|
60
111
|
*/
|
|
61
112
|
export function createFrontendSessionBridge({ baseUrl: base, openStream, request, fetch }, options = {}) {
|
|
62
113
|
const capabilities = claimedCapabilities(options);
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
const signal = init.signal ? AbortSignal.any([init.signal, timeout]) : timeout;
|
|
66
|
-
return (fetch ?? globalThis.fetch)(url, { ...init, signal });
|
|
67
|
-
});
|
|
114
|
+
// The only caller (`respond`) never passes its own signal, so the deadline is the whole signal.
|
|
115
|
+
const send = request ?? (({ url, init, timeoutMs }) => (fetch ?? globalThis.fetch)(url, { ...init, signal: AbortSignal.timeout(timeoutMs) }));
|
|
68
116
|
let sessionId;
|
|
69
117
|
/** Replaced on every reattach; a promise resolved once could never carry this. */
|
|
70
118
|
let currentBindToken;
|
|
@@ -125,50 +173,8 @@ export function createFrontendSessionBridge({ baseUrl: base, openStream, request
|
|
|
125
173
|
executed.delete(oldest);
|
|
126
174
|
}
|
|
127
175
|
}
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
try {
|
|
131
|
-
parsed = JSON.parse(event.data);
|
|
132
|
-
}
|
|
133
|
-
catch (error) {
|
|
134
|
-
options.onError?.(error);
|
|
135
|
-
return;
|
|
136
|
-
}
|
|
137
|
-
// `JSON.parse` succeeds on any valid JSON value, not just objects — a bare `null`, number,
|
|
138
|
-
// string or boolean parses cleanly and would throw *outside* this try/catch on the very next
|
|
139
|
-
// `frame['type']` read (`null` is the one of those that actually throws; the others just read
|
|
140
|
-
// back `undefined` harmlessly). Guarding here routes every one of those through the same
|
|
141
|
-
// `onError` path as an unparseable frame, rather than only some of them crashing the listener.
|
|
142
|
-
if (parsed === null || typeof parsed !== 'object') {
|
|
143
|
-
options.onError?.(new Error(`malformed frame: expected a JSON object, got ${JSON.stringify(parsed)}`));
|
|
144
|
-
return;
|
|
145
|
-
}
|
|
146
|
-
const frame = parsed;
|
|
147
|
-
if (frame['type'] === 'attached') {
|
|
148
|
-
sessionId = String(frame['sessionId']);
|
|
149
|
-
currentBindToken = String(frame['bindToken']);
|
|
150
|
-
// A no-op after the first attach — which is exactly why `currentBindToken` exists.
|
|
151
|
-
resolveReady({ sessionId, bindToken: currentBindToken });
|
|
152
|
-
return;
|
|
153
|
-
}
|
|
154
|
-
if (frame['type'] === 'error') {
|
|
155
|
-
rejectReady(new Error(String(frame['message'])));
|
|
156
|
-
return;
|
|
157
|
-
}
|
|
158
|
-
// Unknown frame types are ignored rather than treated as errors, so the daemon's event
|
|
159
|
-
// vocabulary can grow without every deployed surface needing to ship first.
|
|
160
|
-
if (frame['type'] !== 'invocation')
|
|
161
|
-
return;
|
|
162
|
-
const rawInvocationId = frame['invocationId'];
|
|
163
|
-
if (typeof rawInvocationId !== 'string' || rawInvocationId.length === 0) {
|
|
164
|
-
options.onError?.(new Error('invocation frame missing invocationId'));
|
|
165
|
-
return;
|
|
166
|
-
}
|
|
167
|
-
const action = {
|
|
168
|
-
invocationId: rawInvocationId,
|
|
169
|
-
capabilityId: String(frame['capabilityId']),
|
|
170
|
-
input: (frame['input'] ?? {}),
|
|
171
|
-
};
|
|
176
|
+
/** Runs one well-formed invocation: replay check, announcement, then pane or local serving. */
|
|
177
|
+
function handleInvocation(action) {
|
|
172
178
|
// A redelivery after a dropped answer must not re-run the side effect.
|
|
173
179
|
if (executed.has(action.invocationId)) {
|
|
174
180
|
void respondError(action.invocationId, 'already executed on this surface').catch(() => undefined);
|
|
@@ -182,6 +188,28 @@ export function createFrontendSessionBridge({ baseUrl: base, openStream, request
|
|
|
182
188
|
return;
|
|
183
189
|
}
|
|
184
190
|
void serveLocally(action);
|
|
191
|
+
}
|
|
192
|
+
source.addEventListener('message', (event) => {
|
|
193
|
+
const frame = readFrame(event.data);
|
|
194
|
+
switch (frame.kind) {
|
|
195
|
+
case 'attached':
|
|
196
|
+
sessionId = frame.sessionId;
|
|
197
|
+
currentBindToken = frame.bindToken;
|
|
198
|
+
// A no-op after the first attach — which is exactly why `currentBindToken` exists.
|
|
199
|
+
resolveReady({ sessionId, bindToken: currentBindToken });
|
|
200
|
+
return;
|
|
201
|
+
case 'refused':
|
|
202
|
+
rejectReady(new Error(frame.message));
|
|
203
|
+
return;
|
|
204
|
+
case 'malformed':
|
|
205
|
+
options.onError?.(frame.error);
|
|
206
|
+
return;
|
|
207
|
+
case 'invocation':
|
|
208
|
+
handleInvocation(frame.action);
|
|
209
|
+
return;
|
|
210
|
+
case 'ignored':
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
185
213
|
});
|
|
186
214
|
source.addEventListener('error', (event) => {
|
|
187
215
|
// The daemon deletes a session's bind token the instant that connection drops, so by the time
|