@jini-ai/chat 0.5.7 → 0.6.0

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 (106) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/dist/core/ag-ui/entry.js +2 -0
  3. package/dist/core/assistant-client/durable-run-subscription.d.ts +4 -1
  4. package/dist/core/assistant-client/durable-run-subscription.js +13 -4
  5. package/dist/core/events.d.ts +3 -0
  6. package/dist/core/history-tool-metadata.d.ts +23 -0
  7. package/dist/core/history-tool-metadata.js +13 -0
  8. package/dist/core/index.d.ts +3 -0
  9. package/dist/core/index.js +3 -0
  10. package/dist/core/persistence/title.d.ts +24 -0
  11. package/dist/core/persistence/title.js +60 -9
  12. package/dist/core/run-activity.d.ts +15 -2
  13. package/dist/core/run-activity.js +6 -4
  14. package/dist/core/run-events/entry.js +2 -0
  15. package/dist/core/tool-call-detail.d.ts +15 -0
  16. package/dist/core/tool-call-detail.js +18 -0
  17. package/dist/core/turn-usage.d.ts +42 -3
  18. package/dist/core/turn-usage.js +101 -4
  19. package/dist/core/usage-format.d.ts +10 -0
  20. package/dist/core/usage-format.js +15 -0
  21. package/dist/embed-widget/browser/lazy-loader.d.ts +34 -0
  22. package/dist/embed-widget/browser/lazy-loader.js +116 -0
  23. package/dist/embed-widget/react/dialog.hooks.d.ts +28 -0
  24. package/dist/embed-widget/react/dialog.hooks.js +54 -0
  25. package/dist/embed-widget/react/embed.d.ts +1 -0
  26. package/dist/embed-widget/react/embed.js +5 -3
  27. package/dist/embed-widget/react/header.d.ts +3 -1
  28. package/dist/embed-widget/react/header.hooks.d.ts +13 -0
  29. package/dist/embed-widget/react/header.hooks.js +32 -0
  30. package/dist/embed-widget/react/header.js +4 -14
  31. package/dist/embed-widget/vanilla/index.d.ts +63 -0
  32. package/dist/embed-widget/vanilla/index.js +319 -0
  33. package/dist/embed-widget/vanilla/state.d.ts +48 -0
  34. package/dist/embed-widget/vanilla/state.js +120 -0
  35. package/dist/embed-widget/vanilla/style.css +29 -0
  36. package/dist/react/components/A2uiSurfaceCard.d.ts +5 -1
  37. package/dist/react/components/A2uiSurfaceCard.js +9 -9
  38. package/dist/react/components/AttachmentPreviewModal.hooks.d.ts +7 -0
  39. package/dist/react/components/AttachmentPreviewModal.hooks.js +1 -1
  40. package/dist/react/components/ChatFab.d.ts +4 -1
  41. package/dist/react/components/ChatFab.js +2 -2
  42. package/dist/react/components/ConversationList.d.ts +8 -1
  43. package/dist/react/components/ConversationList.hooks.d.ts +18 -0
  44. package/dist/react/components/ConversationList.hooks.js +25 -0
  45. package/dist/react/components/ConversationList.js +12 -5
  46. package/dist/react/components/ConversationList.rules.d.ts +25 -0
  47. package/dist/react/components/ConversationList.rules.js +19 -0
  48. package/dist/react/components/McpUiSurfaceCard.d.ts +1 -1
  49. package/dist/react/components/McpUiSurfaceCard.js +8 -4
  50. package/dist/react/components/MessageList.d.ts +1 -1
  51. package/dist/react/components/MessageList.hooks.d.ts +4 -0
  52. package/dist/react/components/MessageList.hooks.js +6 -1
  53. package/dist/react/components/MessageList.js +4 -3
  54. package/dist/react/components/MessageRow.d.ts +17 -0
  55. package/dist/react/components/MessageRow.js +54 -35
  56. package/dist/react/components/MessageRow.usage.d.ts +48 -0
  57. package/dist/react/components/MessageRow.usage.js +47 -0
  58. package/dist/react/components/MidRunUserMessage.d.ts +2 -12
  59. package/dist/react/components/MidRunUserMessage.js +4 -2
  60. package/dist/react/components/SentAttachment.d.ts +19 -0
  61. package/dist/react/components/SentAttachment.hooks.d.ts +44 -0
  62. package/dist/react/components/SentAttachment.hooks.js +56 -0
  63. package/dist/react/components/SentAttachment.js +14 -0
  64. package/dist/react/components/ToolCallGroup.d.ts +12 -0
  65. package/dist/react/components/ToolCallGroup.hooks.d.ts +20 -0
  66. package/dist/react/components/ToolCallGroup.hooks.js +23 -0
  67. package/dist/react/components/ToolCallGroup.js +13 -0
  68. package/dist/react/components/ToolCard.js +25 -17
  69. package/dist/react/components/mcp-ui-surface-state.d.ts +21 -0
  70. package/dist/react/components/mcp-ui-surface-state.js +26 -0
  71. package/dist/react/ext-event-renderer-registry.d.ts +7 -0
  72. package/dist/react/features/chat-pane/components/ChatPane.d.ts +1 -1
  73. package/dist/react/features/chat-pane/components/ChatPane.js +11 -6
  74. package/dist/react/features/chat-pane/hooks/useChatPane.hooks.d.ts +1 -0
  75. package/dist/react/features/chat-pane/hooks/useChatPane.hooks.js +16 -10
  76. package/dist/react/features/chat-pane/hooks/useChatPaneRuntimeInventory.hooks.js +4 -0
  77. package/dist/react/features/chat-pane/hooks/useChatPaneSuggestion.hooks.d.ts +9 -0
  78. package/dist/react/features/chat-pane/hooks/useChatPaneSuggestion.hooks.js +11 -0
  79. package/dist/react/features/chat-pane/hooks/useChatPaneSurface.hooks.d.ts +17 -0
  80. package/dist/react/features/chat-pane/hooks/useChatPaneSurface.hooks.js +15 -0
  81. package/dist/react/features/chat-pane/rules.d.ts +14 -0
  82. package/dist/react/features/chat-pane/rules.js +18 -0
  83. package/dist/react/features/chat-pane/styles.d.ts +1 -1
  84. package/dist/react/features/chat-pane/styles.js +85 -1
  85. package/dist/react/features/chat-pane/types.d.ts +16 -1
  86. package/dist/react/hooks/useAnsweredCards.d.ts +20 -0
  87. package/dist/react/hooks/useAnsweredCards.js +23 -0
  88. package/dist/react/hooks/useConversation.d.ts +8 -0
  89. package/dist/react/hooks/useConversation.js +34 -0
  90. package/dist/react/hooks/useRunActivity.d.ts +5 -3
  91. package/dist/react/hooks/useRunActivity.js +4 -3
  92. package/dist/react/index.d.ts +4 -0
  93. package/dist/react/index.js +3 -0
  94. package/dist/react/message-blocks.d.ts +22 -3
  95. package/dist/react/message-blocks.js +25 -3
  96. package/dist/react/tool-call-display.d.ts +90 -0
  97. package/dist/react/tool-call-display.js +97 -0
  98. package/dist/store/ports.d.ts +25 -2
  99. package/dist/store/runtime/entry.d.ts +1 -0
  100. package/dist/store/runtime/entry.js +1 -0
  101. package/dist/store/runtime/history-tools.d.ts +27 -0
  102. package/dist/store/runtime/history-tools.js +169 -0
  103. package/dist/store/sql/store.js +52 -0
  104. package/dist/testing/index.d.ts +7 -0
  105. package/dist/testing/index.js +22 -0
  106. package/package.json +48 -23
@@ -63,6 +63,8 @@ export interface ChatSearchCapability {
63
63
  * @example store.rename({ id, title }, { source: 'generated' }); store.pageConversations({}, { limit: 50 })
64
64
  */
65
65
  export interface ChatStore {
66
+ /** Bounded history discovery/read capability; always uses this store's immutable owner scope. */
67
+ readonly history?: ChatHistoryReader;
66
68
  /** Compatibility read: all owned conversations, updatedAt descending. */
67
69
  list(): Promise<ChatConversation[]>;
68
70
  /** Unknown and other-owned IDs both return null. */
@@ -97,8 +99,29 @@ export interface ChatStore {
97
99
  pageMessages(input: ChatMessagePageInput, options?: ChatPageOptions): Promise<ChatPage<ChatMessage>>;
98
100
  readonly search?: ChatSearchCapability;
99
101
  }
100
- /** Eight-method subset for hosts/fakes without paging; shares the object call contract. */
101
- export type ChatHistoryStore = Pick<ChatStore, 'list' | 'get' | 'create' | 'rename' | 'touch' | 'delete' | 'messages' | 'appendMessage'>;
102
+ /** Eight-method compatibility subset plus optional bounded history; old hosts/fakes still fit. */
103
+ export type ChatHistoryStore = Pick<ChatStore, 'list' | 'get' | 'create' | 'rename' | 'touch' | 'delete' | 'messages' | 'appendMessage' | 'history'>;
104
+ /** Internal persistence projections. Model-facing tools redact and cap these before returning. */
105
+ export interface ChatHistoryReader {
106
+ list(required: Record<string, never>, optional?: {
107
+ query?: string;
108
+ limit?: number;
109
+ }): Promise<readonly (ChatConversation & {
110
+ firstUserLine: string;
111
+ })[]>;
112
+ read(required: {
113
+ chatId: string;
114
+ }, optional?: {
115
+ fromPosition?: number;
116
+ limit?: number;
117
+ includeToolEvents?: boolean;
118
+ }): Promise<{
119
+ messages: readonly (ChatMessage & {
120
+ position: number;
121
+ })[];
122
+ hasMore: boolean;
123
+ }>;
124
+ }
102
125
  /** Authority for cross-owner retention must never reach ordinary request handlers. */
103
126
  export interface ChatHistoryMaintenance {
104
127
  /** Delete at most limit conversations; default 500, integer 1..500. */
@@ -4,4 +4,5 @@ export * from "./chat-history-store.js";
4
4
  export * from "./run-guarded-chat-store.js";
5
5
  export type { ChatKernel, ChatDatabase } from "./kernel.js";
6
6
  export * from "./chat-expiry-sweep.js";
7
+ export * from "./history-tools.js";
7
8
  //# sourceMappingURL=entry.d.ts.map
@@ -3,4 +3,5 @@ export * from "./run-ledger.js";
3
3
  export * from "./chat-history-store.js";
4
4
  export * from "./run-guarded-chat-store.js";
5
5
  export * from "./chat-expiry-sweep.js";
6
+ export * from "./history-tools.js";
6
7
  //# sourceMappingURL=entry.js.map
@@ -0,0 +1,27 @@
1
+ /** Model-facing history tools over an authenticated owner-scoped store; no database handles. */
2
+ import { type AgentToolDefinition, type DerivedRiskByToolId, type ToolRegistration } from '@jini-ai/core';
3
+ import { type RedactUserText } from '../../core/user-text-redaction.js';
4
+ import type { ChatHistoryStore } from '../ports.js';
5
+ export declare const HISTORY_MESSAGE_CHAR_CAP = 8000;
6
+ export declare const HISTORY_TOTAL_CHAR_CAP = 40000;
7
+ export declare const assistantHistoryDerivedRisk: DerivedRiskByToolId;
8
+ /** Exported so host contract tests can pin each registration to its catalog entry. */
9
+ export declare const assistantHistoryToolCatalog: ReadonlyMap<string, AgentToolDefinition>;
10
+ /** Auth is resolved by the host using execution identity, never from model input. */
11
+ export interface AssistantHistoryToolPorts {
12
+ getStore(required: {
13
+ principalId: string;
14
+ }, optional?: Record<string, never>): Promise<ChatHistoryStore>;
15
+ }
16
+ /** Build both history registrations, using the existing catalog/risk/metadata contract.
17
+ * @param ports Authenticated scoped-store port; denied/missing history must fail closed.
18
+ * @param options Optional stricter redaction policy (the secret-card policy is the default).
19
+ * @returns Read-only tool registrations. @throws ToolInputError for malformed or refused reads.
20
+ * @complexity Bounded projections; persistence owns search cost and message ordering.
21
+ */
22
+ export declare function buildAssistantHistoryRegistrations({ ports }: {
23
+ ports: AssistantHistoryToolPorts;
24
+ }, { redact }?: {
25
+ redact?: RedactUserText;
26
+ }): ToolRegistration[];
27
+ //# sourceMappingURL=history-tools.d.ts.map
@@ -0,0 +1,169 @@
1
+ /** Model-facing history tools over an authenticated owner-scoped store; no database handles. */
2
+ import { buildDomainRegistrations, indexCatalogById, requireInputRecord, ToolInputError } from '@jini-ai/core';
3
+ import { redactUserText } from '../../core/user-text-redaction.js';
4
+ import { assistantHistoryToolMetadata } from '../../core/history-tool-metadata.js';
5
+ export const HISTORY_MESSAGE_CHAR_CAP = 8_000;
6
+ export const HISTORY_TOTAL_CHAR_CAP = 40_000;
7
+ const TOOL_EVENT_CAP = 50;
8
+ export const assistantHistoryDerivedRisk = new Map([
9
+ ['assistant_chats_list', 'none'], ['assistant_chat_read', 'none'],
10
+ ]);
11
+ /** Exported so host contract tests can pin each registration to its catalog entry. */
12
+ export const assistantHistoryToolCatalog = indexCatalogById({ catalog: [
13
+ { name: 'assistant_chats_list', sideEffects: 'none', authorization: { permission: 'admin.assistant.use' },
14
+ description: "Lists the CURRENT user's own conversations on this site, newest first, optionally searching titles and message content. Returns id, title, updatedAt, messageCount and firstUserLine, with redacted, capped text. When the user refers to an earlier or interrupted chat ('continue what we were doing'), list first, then assistant_chat_read to read its tail, summarise the state and continue. No other user's chats are accessible; unavailable history is an error.",
15
+ inputSchema: { type: 'object', additionalProperties: false, properties: {
16
+ query: { type: 'string', maxLength: 200 }, limit: { type: 'integer', minimum: 1, maximum: 50, default: 20 },
17
+ } } },
18
+ { name: 'assistant_chat_read', sideEffects: 'none', authorization: { permission: 'admin.assistant.use' },
19
+ description: "Reads one of the CURRENT user's own chats in stored message order. Omit fromPosition to read the latest tail (default 50 messages); use fromPosition and nextPosition for forward pagination. List chats first when resuming earlier work, read the tail, summarise the state, then continue. Returns redacted messages, positions, continuation and truncated flags; each serialized message is capped at 8,000 characters and the result at 40,000 characters. Tool events, when requested, contain only tool name and outcome, never payloads. Attachments are omitted. Missing and other-user chats receive the same refusal.",
20
+ inputSchema: { type: 'object', additionalProperties: false, required: ['chatId'], properties: {
21
+ chatId: { type: 'string', minLength: 1, maxLength: 128 },
22
+ fromPosition: { type: 'integer', minimum: 0 }, limit: { type: 'integer', minimum: 1, maximum: 100, default: 50 },
23
+ includeToolEvents: { type: 'boolean', default: false },
24
+ } } },
25
+ ] });
26
+ /** Cap a fresh projection including JSON escapes and tool summaries, before the total-page cap.
27
+ * The projection is local to this call, so trimming it cannot mutate stored messages or events.
28
+ * @complexity Bounded content/summary serialization; no I/O or unbounded output accumulation.
29
+ */
30
+ function capMessage({ view }, _options = {}) {
31
+ while (JSON.stringify(view).length > HISTORY_MESSAGE_CHAR_CAP) {
32
+ view.truncated = true;
33
+ if (view.content.length > 0)
34
+ view.content = view.content.slice(0, Math.floor(view.content.length / 2));
35
+ else
36
+ view.toolEvents?.pop();
37
+ }
38
+ return view;
39
+ }
40
+ /** Only bounded names and status cross this boundary; malformed historical events are ignored.
41
+ * @complexity O(e) time/space in persisted events; output is capped at 50 summaries.
42
+ */
43
+ function summarizeTools({ events, redact }, _options = {}) {
44
+ if (!Array.isArray(events))
45
+ return { items: [], truncated: false };
46
+ const outcomes = new Map();
47
+ for (const raw of events) {
48
+ const event = raw;
49
+ if (event?.kind === 'tool_result' && typeof event.toolUseId === 'string') {
50
+ outcomes.set(event.toolUseId, event.isError === true ? 'failed' : event.isError === false ? 'succeeded' : 'unknown');
51
+ }
52
+ }
53
+ const items = [];
54
+ let truncated = false;
55
+ for (const raw of events) {
56
+ const event = raw;
57
+ if (event?.kind !== 'tool_use' || typeof event.name !== 'string')
58
+ continue;
59
+ if (items.length === TOOL_EVENT_CAP) {
60
+ truncated = true;
61
+ break;
62
+ }
63
+ const name = redact({ text: event.name }).text;
64
+ if (name.length > 120)
65
+ truncated = true;
66
+ items.push({ name: name.slice(0, 120), outcome: outcomes.get(event.id ?? '') ?? 'unknown' });
67
+ }
68
+ return { items, truncated };
69
+ }
70
+ /** Validate advertised integer bounds even when a transport bypasses JSON schema validation. */
71
+ function integerOption({ input, key, fallback, max, min = 1 }, _options = {}) {
72
+ const value = input[key] === undefined ? fallback : input[key];
73
+ if (typeof value !== 'number' || !Number.isSafeInteger(value) || value < min || value > max) {
74
+ throw new ToolInputError({ message: `${key} must be an integer from ${min} through ${max}.` });
75
+ }
76
+ return value;
77
+ }
78
+ /** Strict allowlist prevents model-supplied identity from ever reaching the host scope factory. */
79
+ function inputRecord({ input, keys }, _options = {}) {
80
+ const record = requireInputRecord({ input });
81
+ if (Object.keys(record).some(key => !keys.includes(key)))
82
+ throw new ToolInputError({ message: 'Unexpected chat history input field. Use the published schema.' });
83
+ return record;
84
+ }
85
+ /** Build both history registrations, using the existing catalog/risk/metadata contract.
86
+ * @param ports Authenticated scoped-store port; denied/missing history must fail closed.
87
+ * @param options Optional stricter redaction policy (the secret-card policy is the default).
88
+ * @returns Read-only tool registrations. @throws ToolInputError for malformed or refused reads.
89
+ * @complexity Bounded projections; persistence owns search cost and message ordering.
90
+ */
91
+ export function buildAssistantHistoryRegistrations({ ports }, { redact = redactUserText } = {}) {
92
+ return buildDomainRegistrations({ domain: 'assistant-history', catalogModule: 'chat/store/runtime/history-tools', catalog: assistantHistoryToolCatalog,
93
+ derivedRisk: assistantHistoryDerivedRisk, metadata: assistantHistoryToolMetadata,
94
+ handlers: {
95
+ assistant_chats_list: async (ctx) => {
96
+ const input = inputRecord({ input: ctx.input, keys: ['query', 'limit'] });
97
+ if (input.query !== undefined && (typeof input.query !== 'string' || input.query.length > 200))
98
+ throw new ToolInputError({ message: 'query must be a string of at most 200 characters.' });
99
+ const limit = integerOption({ input, key: 'limit', fallback: 20, max: 50 });
100
+ const store = await ports.getStore({ principalId: ctx.principal.id }, {});
101
+ if (!store.history)
102
+ throw new ToolInputError({ message: 'Chat history is unavailable on this site.' });
103
+ const rows = await store.history.list({}, { ...(input.query === undefined ? {} : { query: input.query }), limit });
104
+ let truncated = false;
105
+ const chats = rows.map(row => {
106
+ // Redact BEFORE slicing: a cap must not turn a credential into an undetectable fragment.
107
+ const title = redact({ text: row.title ?? '' }).text;
108
+ const first = redact({ text: row.firstUserLine }).text.split(/\r?\n/, 1)[0];
109
+ if (title.length > 300 || first.length > 500 || row.id.length > 128)
110
+ truncated = true;
111
+ return { id: redact({ text: row.id }).text.slice(0, 128), title: title.slice(0, 300), updatedAt: row.updatedAt,
112
+ messageCount: row.messageCount, firstUserLine: first.slice(0, 500) };
113
+ });
114
+ // List is bounded too; serialized escaping can make text larger than its character count.
115
+ while (JSON.stringify({ chats, truncated }).length > HISTORY_TOTAL_CHAR_CAP) {
116
+ chats.pop();
117
+ truncated = true;
118
+ }
119
+ return { chats, truncated };
120
+ },
121
+ assistant_chat_read: async (ctx) => {
122
+ const input = inputRecord({ input: ctx.input, keys: ['chatId', 'fromPosition', 'limit', 'includeToolEvents'] });
123
+ if (typeof input.chatId !== 'string' || !input.chatId.trim() || input.chatId.length > 128)
124
+ throw new ToolInputError({ message: 'chatId must be an id returned by assistant_chats_list.' });
125
+ const limit = integerOption({ input, key: 'limit', fallback: 50, max: 100 });
126
+ const fromPosition = input.fromPosition === undefined ? undefined : integerOption({ input, key: 'fromPosition', fallback: 0, max: Number.MAX_SAFE_INTEGER, min: 0 });
127
+ if (input.includeToolEvents !== undefined && typeof input.includeToolEvents !== 'boolean')
128
+ throw new ToolInputError({ message: 'includeToolEvents must be boolean.' });
129
+ const store = await ports.getStore({ principalId: ctx.principal.id }, {});
130
+ if (!store.history)
131
+ throw new ToolInputError({ message: 'Chat history is unavailable on this site.' });
132
+ if (!await store.get({ id: input.chatId }, {}))
133
+ throw new ToolInputError({ message: 'Chat is unavailable to this caller. Choose an id from assistant_chats_list.' });
134
+ const page = await store.history.read({ chatId: input.chatId }, { ...(fromPosition === undefined ? {} : { fromPosition }), limit, includeToolEvents: input.includeToolEvents === true });
135
+ const messages = [];
136
+ let truncated = page.hasMore;
137
+ // Reserve result envelope/continuation space; measure serialized messages, including escapes.
138
+ let remaining = HISTORY_TOTAL_CHAR_CAP - 512;
139
+ const candidates = fromPosition === undefined ? [...page.messages].reverse() : page.messages;
140
+ for (const row of candidates) {
141
+ const content = redact({ text: row.content }).text;
142
+ const tools = input.includeToolEvents === true ? summarizeTools({ events: row.events, redact }) : undefined;
143
+ const view = capMessage({ view: { id: redact({ text: row.id }).text.slice(0, 128), role: row.role === 'user' ? 'user' : 'assistant', position: row.position,
144
+ content: content.slice(0, HISTORY_MESSAGE_CHAR_CAP), truncated: content.length > HISTORY_MESSAGE_CHAR_CAP || row.id.length > 128 || tools?.truncated === true,
145
+ ...(tools ? { toolEvents: tools.items } : {}) } });
146
+ if (JSON.stringify(view).length > remaining && messages.length > 0) {
147
+ truncated = true;
148
+ break;
149
+ }
150
+ // Advance only over returned positions; the next page must include the omitted row.
151
+ messages.push(view);
152
+ remaining -= JSON.stringify(view).length + 1;
153
+ if (view.truncated)
154
+ truncated = true;
155
+ }
156
+ if (fromPosition === undefined)
157
+ messages.reverse();
158
+ const last = messages.at(-1), first = messages.at(0);
159
+ const omitted = messages.length < page.messages.length;
160
+ const hasMore = fromPosition !== undefined && (omitted || page.hasMore);
161
+ return { chatId: redact({ text: input.chatId }).text, messages, truncated, hasMore,
162
+ ...(last ? { nextPosition: last.position + 1 } : {}),
163
+ ...(first ? { firstPosition: first.position } : {}),
164
+ hasMoreBefore: fromPosition === undefined && (omitted || page.hasMore) };
165
+ },
166
+ },
167
+ });
168
+ }
169
+ //# sourceMappingURL=history-tools.js.map
@@ -212,6 +212,54 @@ export function createSqlChatStore({ kernel, scope: inputScope }, { clock = crea
212
212
  return row?.max === null || row?.max === undefined ? 0 : Number(row.max) + 1;
213
213
  }
214
214
  const store = {
215
+ history: {
216
+ /** Literal search uses two scoped projections, not a transcript fetch per conversation.
217
+ * @complexity The database scans owned content for search; only limit rows cross the port.
218
+ */
219
+ async list(_required, { query, limit = 20 } = {}) {
220
+ if (!Number.isInteger(limit) || limit < 1 || limit > 50 || (query !== undefined && (typeof query !== 'string' || query.length > 200))) {
221
+ throw new ChatStoreError({ code: 'invalid-input' });
222
+ }
223
+ const rows = await run(db => {
224
+ let statement = ownedConversations(db).select(eb => eb.selectFrom('ai_chat_messages as first')
225
+ .select('first.content').whereRef('first.conversation_id', '=', 'c.id').where('first.role', '=', 'user')
226
+ .orderBy('first.position').limit(1).as('first_user_line'));
227
+ if (query?.trim()) {
228
+ // Escape LIKE wildcards: user search is literal text, never an SQL/FTS expression.
229
+ const pattern = '%' + query.trim().toLowerCase().replace(/[!%_]/g, char => '!' + char) + '%';
230
+ statement = statement.where(eb => eb.or([
231
+ sql `lower(c.title) like ${pattern} escape '!'`,
232
+ eb.exists(eb.selectFrom('ai_chat_messages as found').select('found.id')
233
+ .whereRef('found.conversation_id', '=', 'c.id')
234
+ .where(sql `lower(found.content) like ${pattern} escape '!'`)),
235
+ ]));
236
+ }
237
+ return statement.orderBy('c.updated_at', 'desc').orderBy(ordinalId(kernel.dialect, 'c.id')).limit(limit).execute();
238
+ });
239
+ return rows.map(row => ({ ...toConversation(row), firstUserLine: row.first_user_line ?? '' }));
240
+ },
241
+ /** No attachment column is read. O(limit) rows; default tail is reversed into position order. */
242
+ async read({ chatId }, { fromPosition, limit = 50, includeToolEvents = false } = {}) {
243
+ if (!Number.isInteger(limit) || limit < 1 || limit > 100 || (fromPosition !== undefined && (!Number.isSafeInteger(fromPosition) || fromPosition < 0))) {
244
+ throw new ChatStoreError({ code: 'invalid-input' });
245
+ }
246
+ const rows = await run(db => {
247
+ let statement = ownedMessages(db, chatId).clearSelect().select([
248
+ 'm.id', 'm.role', 'm.content', 'm.position', 'm.created_at',
249
+ ]);
250
+ if (fromPosition !== undefined)
251
+ statement = statement.where('m.position', '>=', fromPosition);
252
+ return statement.select(includeToolEvents ? sql `m.events_json`.as('events_json') : sql `null`.as('events_json'))
253
+ .orderBy('m.position', fromPosition === undefined ? 'desc' : 'asc').limit(limit + 1).execute();
254
+ });
255
+ const page = rows.slice(0, limit);
256
+ if (fromPosition === undefined)
257
+ page.reverse();
258
+ return { messages: page.map(row => ({ id: row.id, role: row.role, content: row.content,
259
+ position: Number(row.position), createdAt: Number(row.created_at),
260
+ ...(includeToolEvents ? { events: row.events_json ? safeParse(row.events_json) : undefined } : {}), })), hasMore: rows.length > limit };
261
+ },
262
+ },
215
263
  async list() {
216
264
  const rows = await run((db) => ownedConversations(db).orderBy("c.updated_at", "desc").execute());
217
265
  return rows.map((row) => toConversation(row));
@@ -332,6 +380,10 @@ export function createSqlChatStore({ kernel, scope: inputScope }, { clock = crea
332
380
  },
333
381
  };
334
382
  return {
383
+ history: {
384
+ list: (required, optional = {}) => atStoreBoundary({ operation: () => store.history.list(required, optional) }),
385
+ read: (required, optional = {}) => atStoreBoundary({ operation: () => store.history.read(required, optional) }),
386
+ },
335
387
  list: () => atStoreBoundary({ operation: () => store.list() }),
336
388
  get: (required, optional = {}) => atStoreBoundary({ operation: () => store.get(required, optional) }),
337
389
  create: (required, optional = {}) => atStoreBoundary({ operation: () => store.create(required, optional) }),
@@ -0,0 +1,7 @@
1
+ import type { ChatMessage } from '../core/index.js';
2
+ /** `turns` user/assistant pairs; each reply carries `toolRowsPerTurn` finished Bash calls with output. */
3
+ export declare function createLongTranscriptFixture({ turns, toolRowsPerTurn }: {
4
+ turns: number;
5
+ toolRowsPerTurn: number;
6
+ }, _options?: Record<string, never>): ChatMessage[];
7
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,22 @@
1
+ /** `turns` user/assistant pairs; each reply carries `toolRowsPerTurn` finished Bash calls with output. */
2
+ export function createLongTranscriptFixture({ turns, toolRowsPerTurn }, _options = {}) {
3
+ return Array.from({ length: turns }, (_, turn) => [
4
+ { id: `u${turn}`, role: 'user', content: `Step ${turn}`, createdAt: turn * 2 },
5
+ {
6
+ id: `a${turn}`,
7
+ role: 'assistant',
8
+ content: `Done with step ${turn}.`,
9
+ runStatus: 'succeeded',
10
+ createdAt: turn * 2 + 1,
11
+ events: [
12
+ ...Array.from({ length: toolRowsPerTurn }, (_, i) => [
13
+ { kind: 'tool_use', id: `t${turn}-${i}`, name: 'Bash', input: { command: `ls ${i}` } },
14
+ { kind: 'tool_result', toolUseId: `t${turn}-${i}`, content: 'file\n'.repeat(50), isError: false },
15
+ ]).flat(),
16
+ { kind: 'text', text: `Done with step ${turn}.` },
17
+ { kind: 'ext', name: 'render-probe', data: { turn } },
18
+ ],
19
+ },
20
+ ]).flat();
21
+ }
22
+ //# sourceMappingURL=index.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jini-ai/chat",
3
- "version": "0.5.7",
3
+ "version": "0.6.0",
4
4
  "description": "Jini's chat/artifact frontend, consolidated into one package with two subpaths: framework-free chat vocabulary and pure parsers in `/core`, headless hooks + presentational components + slots in `/react`. Import only the layer you need.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -14,6 +14,7 @@
14
14
  "runtime": "universal",
15
15
  "entries": {
16
16
  ".": "universal",
17
+ "./testing": "universal",
17
18
  "./core": "universal",
18
19
  "./react": "browser",
19
20
  "./react/chat-pane": "browser",
@@ -34,7 +35,10 @@
34
35
  "./browser/session-store": "browser",
35
36
  "./browser/page-actions": "browser",
36
37
  "./server/durable-runs": "node",
37
- "./store/runtime": "node"
38
+ "./store/runtime": "node",
39
+ "./vanilla": "browser",
40
+ "./browser/lazy-loader": "browser",
41
+ "./vanilla/style.css": "browser"
38
42
  },
39
43
  "neutralEntries": [
40
44
  "src/store/contracts"
@@ -54,6 +58,11 @@
54
58
  "main": "./dist/core/index.js",
55
59
  "types": "./dist/core/index.d.ts",
56
60
  "exports": {
61
+ "./testing": {
62
+ "types": "./dist/testing/index.d.ts",
63
+ "import": "./dist/testing/index.js",
64
+ "default": "./dist/testing/index.js"
65
+ },
57
66
  ".": {
58
67
  "types": "./dist/core/index.d.ts",
59
68
  "import": "./dist/core/index.js",
@@ -155,7 +164,18 @@
155
164
  "types": "./dist/store/runtime/entry.d.ts",
156
165
  "import": "./dist/store/runtime/entry.js",
157
166
  "default": "./dist/store/runtime/entry.js"
158
- }
167
+ },
168
+ "./vanilla": {
169
+ "types": "./dist/embed-widget/vanilla/index.d.ts",
170
+ "import": "./dist/embed-widget/vanilla/index.js",
171
+ "default": "./dist/embed-widget/vanilla/index.js"
172
+ },
173
+ "./browser/lazy-loader": {
174
+ "types": "./dist/embed-widget/browser/lazy-loader.d.ts",
175
+ "import": "./dist/embed-widget/browser/lazy-loader.js",
176
+ "default": "./dist/embed-widget/browser/lazy-loader.js"
177
+ },
178
+ "./vanilla/style.css": "./dist/embed-widget/vanilla/style.css"
159
179
  },
160
180
  "files": [
161
181
  "dist",
@@ -176,12 +196,12 @@
176
196
  "registry": "https://registry.npmjs.org"
177
197
  },
178
198
  "dependencies": {
179
- "@jini-ai/core": "^0.4.2"
199
+ "@jini-ai/core": "^0.4.4"
180
200
  },
181
201
  "peerDependencies": {
182
202
  "react": "^18.3.0 || ^19.0.0",
183
203
  "react-dom": "^18.3.0 || ^19.0.0",
184
- "@jini-ai/db": "^0.2.0 || ^0.3.0",
204
+ "@jini-ai/db": "^0.3.2",
185
205
  "kysely": "^0.29.6",
186
206
  "better-sqlite3": "^13.0.0",
187
207
  "@electric-sql/pglite": "0.5.8",
@@ -193,12 +213,11 @@
193
213
  "@radix-ui/react-radio-group": "^1.4.7",
194
214
  "@radix-ui/react-select": "^2.3.7",
195
215
  "@radix-ui/react-slot": "^1.3.3",
196
- "recharts": "^3.10.1",
197
- "@jini-ai/agent-runtime": "^0.4.4",
198
- "@jini-ai/agentic": "^0.4.2",
199
- "@jini-ai/protocol": "^0.4.3",
200
- "@jini-ai/ui": "^0.4.7",
201
- "@jini-ai/diagnostics": "^0.5.0"
216
+ "@jini-ai/agent-runtime": "^0.4.5",
217
+ "@jini-ai/agentic": "^0.4.4",
218
+ "@jini-ai/protocol": "^0.4.4",
219
+ "@jini-ai/ui": "^0.5.0",
220
+ "@jini-ai/diagnostics": "^0.5.2"
202
221
  },
203
222
  "peerDependenciesMeta": {
204
223
  "react": {
@@ -243,9 +262,6 @@
243
262
  "@radix-ui/react-slot": {
244
263
  "optional": true
245
264
  },
246
- "recharts": {
247
- "optional": true
248
- },
249
265
  "@jini-ai/agent-runtime": {
250
266
  "optional": true
251
267
  },
@@ -277,25 +293,28 @@
277
293
  "@types/react": "^19.2.0",
278
294
  "@types/react-dom": "^19.2.0",
279
295
  "@vitest/coverage-v8": "^2.1.9",
296
+ "zod": "^3.25.76",
280
297
  "jsdom": "^25.0.1",
281
298
  "react": "^19.2.0",
282
299
  "react-dom": "^19.2.0",
283
- "recharts": "^3.10.1",
284
300
  "kysely": "0.29.6",
285
301
  "better-sqlite3": "13.0.3",
286
302
  "@electric-sql/pglite": "0.5.8",
287
303
  "pg": "8.23.0",
288
304
  "@ag-ui/core": "0.0.58",
289
- "@jini-ai/ui": "^0.4.7",
290
- "@jini-ai/db": "0.3.1",
291
- "@jini-ai/protocol": "^0.4.3",
292
- "@jini-ai/diagnostics": "^0.5.1",
293
- "@jini-ai/agentic": "^0.4.3",
294
- "@jini-ai/agent-runtime": "0.4.4",
295
- "@jini-ai/core": "^0.4.2"
305
+ "@jini-ai/db": "0.3.4",
306
+ "@jini-ai/agent-runtime": "0.4.8",
307
+ "@jini-ai/core": "^0.4.4",
308
+ "@jini-ai/protocol": "^0.4.4",
309
+ "@jini-ai/ui": "^0.5.0",
310
+ "@jini-ai/diagnostics": "^0.5.2",
311
+ "@jini-ai/agentic": "^0.4.4"
296
312
  },
297
313
  "typesVersions": {
298
314
  "*": {
315
+ "testing": [
316
+ "./dist/testing/index.d.ts"
317
+ ],
299
318
  "core": [
300
319
  "./dist/core/index.d.ts"
301
320
  ],
@@ -349,11 +368,17 @@
349
368
  ],
350
369
  "store/runtime": [
351
370
  "./dist/store/runtime/entry.d.ts"
371
+ ],
372
+ "vanilla": [
373
+ "./dist/embed-widget/vanilla/index.d.ts"
374
+ ],
375
+ "browser/lazy-loader": [
376
+ "./dist/embed-widget/browser/lazy-loader.d.ts"
352
377
  ]
353
378
  }
354
379
  },
355
380
  "scripts": {
356
- "build": "tsc -p tsconfig.json && mkdir -p dist/react/styles && cp src/react/styles/reference.css dist/react/styles/",
381
+ "build": "tsc -p tsconfig.json && mkdir -p dist/react/styles && cp src/react/styles/reference.css dist/react/styles/ && mkdir -p dist/embed-widget/vanilla && node --input-type=module -e \"import {writeFileSync,readFileSync} from 'node:fs'; import {CHAT_BUBBLE_STYLE} from './dist/embed-widget/browser/lazy-loader.js'; writeFileSync('dist/embed-widget/vanilla/style.css',CHAT_BUBBLE_STYLE+'\\n'+readFileSync('src/embed-widget/vanilla/style.css','utf8'));\"",
357
382
  "typecheck": "tsc -p tsconfig.json --noEmit",
358
383
  "test": "vitest run",
359
384
  "test:coverage": "vitest run --coverage",