@jini-ai/chat 0.6.6 → 0.6.10

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 (160) hide show
  1. package/CHANGELOG.md +129 -1
  2. package/dist/core/accepted-input.d.ts +54 -0
  3. package/dist/core/accepted-input.js +165 -0
  4. package/dist/core/agent-plugin-chat-state.d.ts +43 -0
  5. package/dist/core/agent-plugin-chat-state.js +48 -0
  6. package/dist/core/assistant-client/a2ui-action-poster.js +32 -28
  7. package/dist/core/assistant-client/messages.d.ts +1 -1
  8. package/dist/core/assistant-client/messages.js +2 -2
  9. package/dist/core/assistant-client/transport.d.ts +2 -2
  10. package/dist/core/assistant-client/transport.js +187 -98
  11. package/dist/core/assistant-content.d.ts +2 -1
  12. package/dist/core/assistant-content.js +9 -11
  13. package/dist/core/browser/page-actions.js +4 -2
  14. package/dist/core/composer-history.d.ts +16 -9
  15. package/dist/core/composer-history.js +28 -16
  16. package/dist/core/conversation-title.d.ts +3 -0
  17. package/dist/core/conversation-title.js +3 -0
  18. package/dist/core/durable-projection.js +20 -13
  19. package/dist/core/durable-runs/continuation.d.ts +4 -0
  20. package/dist/core/durable-runs/continuation.js +31 -6
  21. package/dist/core/durable-runs/ledger-contracts.d.ts +2 -1
  22. package/dist/core/durable-runs/ports.d.ts +6 -2
  23. package/dist/core/index.d.ts +2 -0
  24. package/dist/core/index.js +2 -0
  25. package/dist/core/messages.d.ts +2 -0
  26. package/dist/core/partial-json.d.ts +6 -7
  27. package/dist/core/partial-json.js +81 -47
  28. package/dist/core/persistence/title.js +4 -2
  29. package/dist/core/question-answer.d.ts +23 -0
  30. package/dist/core/question-answer.js +66 -0
  31. package/dist/core/question-form/format.d.ts +6 -6
  32. package/dist/core/question-form/format.js +16 -17
  33. package/dist/core/question-form/normalize.d.ts +18 -0
  34. package/dist/core/question-form/normalize.js +112 -122
  35. package/dist/core/question-form/scan.js +141 -203
  36. package/dist/core/run-activity.js +62 -49
  37. package/dist/core/run-events/entry.d.ts +10 -1
  38. package/dist/core/run-events/entry.js +117 -87
  39. package/dist/core/todos.d.ts +1 -1
  40. package/dist/core/todos.js +60 -85
  41. package/dist/core/tool-events.js +14 -12
  42. package/dist/core/transcript.js +62 -16
  43. package/dist/core/turn-usage.js +54 -32
  44. package/dist/core/user-text-redaction.js +3 -0
  45. package/dist/core/util/manifest.js +77 -82
  46. package/dist/core/util/markdown-context.js +51 -53
  47. package/dist/core/util/parser.js +136 -144
  48. package/dist/core/util/pointer.js +16 -16
  49. package/dist/core/util/strip.js +57 -67
  50. package/dist/core/util/validate.js +15 -17
  51. package/dist/embed-widget/browser/session-store.js +21 -12
  52. package/dist/embed-widget/transports/fetch-sse.js +108 -95
  53. package/dist/embed-widget/vanilla/index.js +81 -70
  54. package/dist/react/agent-bridge/frontend-session-bridge.js +80 -52
  55. package/dist/react/components/AttachmentPreviewModal.js +25 -1
  56. package/dist/react/components/ChatSettingsDialog.d.ts +13 -0
  57. package/dist/react/components/ChatSettingsDialog.hooks.d.ts +115 -0
  58. package/dist/react/components/ChatSettingsDialog.hooks.js +40 -0
  59. package/dist/react/components/ChatSettingsDialog.js +72 -0
  60. package/dist/react/components/ChatSettingsSegmented.d.ts +18 -0
  61. package/dist/react/components/ChatSettingsSegmented.js +11 -0
  62. package/dist/react/components/Composer.d.ts +5 -1
  63. package/dist/react/components/Composer.js +126 -82
  64. package/dist/react/components/ConversationList.js +25 -39
  65. package/dist/react/components/ConversationTitleEditor.d.ts +27 -0
  66. package/dist/react/components/ConversationTitleEditor.js +54 -0
  67. package/dist/react/components/Icon.d.ts +1 -1
  68. package/dist/react/components/Icon.js +25 -45
  69. package/dist/react/components/Markdown.js +134 -123
  70. package/dist/react/components/McpUiSurfaceCard.d.ts +2 -2
  71. package/dist/react/components/McpUiSurfaceCard.js +30 -9
  72. package/dist/react/components/MessageList.d.ts +13 -13
  73. package/dist/react/components/MessageList.hooks.d.ts +6 -1
  74. package/dist/react/components/MessageList.hooks.js +19 -3
  75. package/dist/react/components/MessageList.js +34 -10
  76. package/dist/react/components/MessageRow.d.ts +1 -1
  77. package/dist/react/components/MessageRow.js +75 -47
  78. package/dist/react/components/MessageRow.usage.js +16 -11
  79. package/dist/react/components/MidRunUserMessage.js +16 -3
  80. package/dist/react/components/QuestionForm.js +148 -76
  81. package/dist/react/components/QuestionsPanel.js +1 -1
  82. package/dist/react/components/TodoCard.js +12 -6
  83. package/dist/react/components/ToolCallGroup.d.ts +4 -3
  84. package/dist/react/components/ToolCallGroup.js +7 -5
  85. package/dist/react/components/ToolCard.js +47 -40
  86. package/dist/react/components/composer-discovery.js +6 -1
  87. package/dist/react/components/mcp-ui-surface-state.d.ts +6 -11
  88. package/dist/react/components/mcp-ui-surface-state.js +23 -8
  89. package/dist/react/ext-event-renderer-registry.d.ts +8 -0
  90. package/dist/react/ext-event-renderer-registry.js +5 -2
  91. package/dist/react/features/chat-pane/components/AgentPluginUsage.d.ts +7 -0
  92. package/dist/react/features/chat-pane/components/AgentPluginUsage.js +45 -0
  93. package/dist/react/features/chat-pane/components/AgentRuntimePicker.js +55 -19
  94. package/dist/react/features/chat-pane/components/ChatPane.d.ts +1 -1
  95. package/dist/react/features/chat-pane/components/ChatPane.js +192 -74
  96. package/dist/react/features/chat-pane/components/RuntimeDropUp.d.ts +29 -0
  97. package/dist/react/features/chat-pane/components/RuntimeDropUp.js +45 -0
  98. package/dist/react/features/chat-pane/create-daemon-attachment-uploader.js +80 -47
  99. package/dist/react/features/chat-pane/create-mcp-ui-tool-caller.js +44 -40
  100. package/dist/react/features/chat-pane/create-mid-run-message-poster.d.ts +6 -6
  101. package/dist/react/features/chat-pane/create-mid-run-message-poster.js +8 -8
  102. package/dist/react/features/chat-pane/create-typed-answer-poster.d.ts +3 -3
  103. package/dist/react/features/chat-pane/create-typed-answer-poster.js +3 -3
  104. package/dist/react/features/chat-pane/hooks/useAgentRuntimePicker.hooks.d.ts +6 -1
  105. package/dist/react/features/chat-pane/hooks/useAgentRuntimePicker.hooks.js +11 -5
  106. package/dist/react/features/chat-pane/hooks/useChatPane.hooks.d.ts +12 -7
  107. package/dist/react/features/chat-pane/hooks/useChatPane.hooks.js +340 -149
  108. package/dist/react/features/chat-pane/hooks/useChatPaneControlsHeight.hooks.js +7 -6
  109. package/dist/react/features/chat-pane/hooks/useChatPaneNewTabLinks.hooks.js +28 -10
  110. package/dist/react/features/chat-pane/hooks/useChatPaneRuntimeInventory.hooks.d.ts +3 -3
  111. package/dist/react/features/chat-pane/hooks/useChatPaneRuntimeInventory.hooks.js +60 -12
  112. package/dist/react/features/chat-pane/hooks/useRuntimeDropUp.hooks.d.ts +49 -0
  113. package/dist/react/features/chat-pane/hooks/useRuntimeDropUp.hooks.js +143 -0
  114. package/dist/react/features/chat-pane/model-options.d.ts +14 -0
  115. package/dist/react/features/chat-pane/model-options.js +45 -25
  116. package/dist/react/features/chat-pane/rules.d.ts +8 -9
  117. package/dist/react/features/chat-pane/rules.js +84 -46
  118. package/dist/react/features/chat-pane/styles.d.ts +1 -1
  119. package/dist/react/features/chat-pane/styles.js +152 -36
  120. package/dist/react/features/chat-pane/types.d.ts +19 -9
  121. package/dist/react/hooks/assistant-chats.js +12 -5
  122. package/dist/react/hooks/composer-draft-cache.js +116 -118
  123. package/dist/react/hooks/composer-history-storage.js +5 -5
  124. package/dist/react/hooks/fab-position.js +7 -4
  125. package/dist/react/hooks/useAnsweredCards.d.ts +21 -4
  126. package/dist/react/hooks/useAnsweredCards.js +38 -11
  127. package/dist/react/hooks/useArtifactStream.js +28 -22
  128. package/dist/react/hooks/useComposer.d.ts +20 -0
  129. package/dist/react/hooks/useComposer.js +22 -9
  130. package/dist/react/hooks/useComposerHistory.hooks.d.ts +6 -1
  131. package/dist/react/hooks/useComposerHistory.hooks.js +39 -19
  132. package/dist/react/hooks/useComposerHistoryKeys.hooks.js +24 -14
  133. package/dist/react/hooks/useConversation.d.ts +7 -0
  134. package/dist/react/hooks/useConversation.js +23 -10
  135. package/dist/react/hooks/useExtEventGroups.js +39 -39
  136. package/dist/react/hooks/useQuestionForms.js +21 -26
  137. package/dist/react/hooks/useRunStream.js +10 -2
  138. package/dist/react/index.d.ts +7 -1
  139. package/dist/react/index.js +9 -0
  140. package/dist/react/message-blocks.d.ts +6 -4
  141. package/dist/react/message-blocks.js +94 -72
  142. package/dist/react/slots.d.ts +2 -2
  143. package/dist/react/tool-call-display.d.ts +12 -6
  144. package/dist/react/tool-call-display.js +53 -24
  145. package/dist/server/durable-runs/recover.js +132 -80
  146. package/dist/server/run-finalizer/entry.js +25 -15
  147. package/dist/store/legacy/sqlite/conversations.d.ts +11 -11
  148. package/dist/store/legacy/sqlite/conversations.js +69 -98
  149. package/dist/store/legacy/sqlite/messages.d.ts +10 -2
  150. package/dist/store/legacy/sqlite/messages.js +113 -71
  151. package/dist/store/runtime/chat-history-store.js +9 -3
  152. package/dist/store/runtime/durable-run-store.d.ts +2 -2
  153. package/dist/store/runtime/durable-run-store.js +165 -117
  154. package/dist/store/runtime/history-tools.js +118 -82
  155. package/dist/store/runtime/run-ledger.d.ts +4 -1
  156. package/dist/store/runtime/run-ledger.js +56 -16
  157. package/dist/store/sql/cursor.js +38 -18
  158. package/dist/store/sql/store.d.ts +18 -0
  159. package/dist/store/sql/store.js +46 -15
  160. package/package.json +18 -16
package/CHANGELOG.md CHANGED
@@ -1,3 +1,131 @@
1
+ ## 0.6.10
2
+
3
+ ### Patch Changes
4
+
5
+ - f9d9894: Hide recovered tool failures when tool calls are hidden. Keep unrecovered failures in one compact, localized disclosure per reply, preserving their existing details.
6
+ - 8ff5653: Make the chat-pane transcript readable from outside the component — a must-have for an AI (or an
7
+ e2e harness) verifying that commands it gave the pane (create a form, add a user, change a
8
+ permission) actually happened.
9
+
10
+ Before this, `<ChatPane>` fully encapsulated its conversation state: `useConversation()`'s
11
+ `messages` never left the component, `ChatPaneProps` had no callback that reported the live message
12
+ list, and the rendered transcript (`MessageList`/`MessageRow`/`ToolCard`) carried zero
13
+ `data-agent-element` tags — unlike every other interactive surface in this app's reference pages.
14
+
15
+ - `ChatPaneProps.onMessagesChange?: (messages: ChatMessage[]) => void` — fires with the full message
16
+ array on every change, mirroring the existing `onActivityChange` pattern. Each `ChatMessage`
17
+ already carries its `events` array (`tool_use`/`tool_result`), so a subscriber gets complete,
18
+ structured tool-call data for free — no DOM parsing needed.
19
+ - `data-agent-element="chat-transcript"` (role `list`) on the message-list container, and
20
+ `data-agent-element="chat-message-<id>"` (role `region`) on every message row, so `page.*` DOM
21
+ tools can enumerate the transcript the same way they read everything else in this app.
22
+ - `ToolCard` now wraps whatever it renders (any of its dozen built-in card variants, or a host's own
23
+ custom-registered renderer) in one `data-agent-element="tool-call-<use.id>"` (role `region`)
24
+ element — added at the single dispatch point every branch already funnels through, so no
25
+ individual card variant's own markup changed. The collapsed-by-default accordion detail (args/
26
+ result JSON) sits inside this tagged element, not gated behind expanding it: `textContent` (what
27
+ the `page.*` DOM driver actually reads) is layout-agnostic, so the full detail is readable whether
28
+ or not anything has been clicked open.
29
+
30
+ - 3fe4d14: Add compatibility regression fixtures for legacy inline image results, media-only saved events and delegated echo notes; runtime behavior is unchanged.
31
+ - 8ff5653: Add `createDaemonAttachmentUploader`, so composer drag-and-drop is one line instead of 160.
32
+
33
+ `ChatPane` has always gated its drop target and file picker on `uploadAttachments` being supplied — but
34
+ supplying it meant writing the client half yourself: a bounded-concurrency worker pool that preserves
35
+ input order, per-turn quota accounting, abort and timeout plumbing, and cleanup of files that already
36
+ landed when a later one fails. Every host would write the same ~160 lines, and the ordering and
37
+ cleanup details are exactly the ones that get skipped.
38
+
39
+ ```tsx
40
+ <ChatPane
41
+ transport={transport}
42
+ uploadAttachments={createDaemonAttachmentUploader(daemonUrl)}
43
+ />
44
+ ```
45
+
46
+ That talks to `@jini-ai/http-kit`'s `POST`/`DELETE /api/attachments`. Pass `''` when a dev-server proxy
47
+ already forwards `/api` from the page's own origin. `maxAttachmentBytes`, `maxAttachmentCount`,
48
+ `maxBatchBytes`, `timeoutMs`, and `concurrency` are all configurable and all default to the daemon side's
49
+ own limits, so the client's early rejection matches what the daemon would have said anyway.
50
+
51
+ Client-side quotas are a courtesy, never the boundary — telling a user their 400 MB video is too large
52
+ before the browser streams it, not after. The daemon re-derives every one of them.
53
+
54
+ Two behavioral notes for anyone porting hand-rolled code onto this:
55
+
56
+ - **The request always sends `content-type: application/octet-stream`**, not `file.type`. The daemon
57
+ sniffs the real kind from the bytes and ignores the header, so forwarding the browser's guess buys
58
+ nothing — and breaks real uploads: a dropped `.json` file arrives as `application/json`, which an
59
+ app-wide `express.json()` on the daemon then claims, draining the request stream before the upload
60
+ route can read a byte.
61
+ - **Batch accounting is per uploader instance**, not module-global, so two panes pointed at two daemons
62
+ cannot consume each other's per-turn quota. Reservations are rolled back on failure, so a retry of a
63
+ failed turn is not refused for quota it never actually used.
64
+
65
+ - 72ea04d: Folded `@jini-ai/renderers-react` into `@jini-ai/ui` as an independently-importable, tree-shakeable
66
+ `./renderers` subpath, and retired the package.
67
+
68
+ Real (non-comment) imports of `@jini-ai/renderers-react` anywhere in the repo were exactly 3 files,
69
+ 2 symbols, all inside `@jini-ai/ui`'s own `features/html-viewer` — `openSandboxedPreviewInNewTab`
70
+ and `useSandboxBridge`. `@jini-ai/ui` was already the package's only real consumer, so a standalone
71
+ package bought nothing, the same reasoning that folded `@jini-ai/ui-core` into `./core` and
72
+ `@jini-ai/composio` into `@jini-ai/admin`. The whole `src/**` tree (registry, renderers, srcDoc
73
+ host, annotation-canvas, preview-modal-shell, i18n) moves to `packages/ui/src/renderers/` as one
74
+ `git mv`, unchanged in shape; `html-viewer`'s two consumers switch from the package-qualified
75
+ import to a relative one, since they're now in the same package.
76
+
77
+ **The `@jini-ai/renderers-react → @jini-ai/chat` dependency was dropped first, before the move,
78
+ not after.** `registry.ts` imported `ArtifactManifest`/`inferLegacyManifest` from `@jini-ai/chat`,
79
+ and `types.ts` re-exported five manifest types from there. `@jini-ai/chat` already depends on
80
+ `@jini-ai/ui`, so folding renderers-react into `ui` unchanged would have created a real `ui ↔ chat`
81
+ package cycle. The five manifest types (`ArtifactManifest`, `ArtifactKind`, `ArtifactRendererId`,
82
+ `ArtifactExportKind`, `ArtifactStatus`) are now defined locally in `types.ts`, copied verbatim from
83
+ `@jini-ai/chat`'s `src/core/util/types.ts` — a small duplicated-type cost, accepted because a
84
+ renderer package must not depend on a chat package.
85
+
86
+ **`resolveArtifactManifest` no longer infers a legacy manifest from a file's name/extension** — a
87
+ real, disclosed API behavior change. It now returns `file.manifest ?? null`. Manifest inference
88
+ (guessing kind/renderer from `.md`/`.html`/`.svg`/`.tsx`, etc.) is a caller concern that depends on
89
+ a product's own naming conventions, not something a generic renderer registry should do on a
90
+ caller's behalf; a caller that still wants that inference should resolve a manifest for the file
91
+ itself (see `@jini-ai/chat`'s `inferLegacyManifest`) before handing it to this registry. Verified
92
+ zero external callers of `RendererRegistry`/`resolveArtifactManifest`/`createDefaultRendererRegistry`
93
+ anywhere in Jini or Tovu before making this change — `@jini-ai/chat`'s own `RendererRegistry` is an
94
+ unrelated, locally-defined class in `artifact-types.ts`, and `@jini-ai/artifacts`' own
95
+ `resolveArtifactManifest` is an unrelated 3-argument function; both are same-name coincidences, not
96
+ real dependents. The four bundled renderers (`HtmlRenderer`, `MarkdownRenderer`, `SvgRenderer`,
97
+ `ReactComponentRenderer`) gate `canRender` on a resolvable manifest before falling back to any
98
+ `file.kind`/extension heuristic of their own, so this ripples into every test that exercised
99
+ default-registry routing via a bare file name with no explicit `manifest` — all updated to attach
100
+ one, with the extension-inference-specific assertions flipped to assert the removed behavior no
101
+ longer fires. No test was weakened or deleted: every changed assertion now tests the new,
102
+ documented contract, or restores a manifest to keep testing what it always meant to test.
103
+
104
+ **`@jini-ai/renderers-react` is retired, not versioned forward.** It was published (0.1.2), and
105
+ `packages/renderers-react` is deleted from this workspace. Deleting the source does not unpublish
106
+ anything, so whoever runs the next release should follow up with:
107
+
108
+ ```
109
+ npm deprecate @jini-ai/renderers-react "Merged into @jini-ai/ui; import from '@jini-ai/ui/renderers' instead."
110
+ ```
111
+
112
+ `@jini-ai/chat`'s dead `@jini-ai/renderers-react` dependency (declared, never imported — its own
113
+ `react/artifact-types.ts` defines a local placeholder registry instead, per its
114
+ `TODO(renderers-react)` header, left untouched here as out of scope) is removed from its
115
+ `package.json`, hence the accompanying patch bump.
116
+
117
+ - 0c247a8: Fix visitor chat streaming announcements, resumed focus, frame batching, persistence and lazy mount/retry handling. Document the additive A2uiSurfaceCard registry prop for host validation and rendering.
118
+ - e16a907: Clear floating-point noise in displayed costs, reconcile resumed counter resets and revisited sessions, and expose reported output tokens to assistive technology.
119
+ - ea8d883: Release modal semantics when focus leaves chat, announce bounded rendered reply text, and retry opening without reloading mounted assets.
120
+ - Updated dependencies [22f0619]
121
+ - Updated dependencies [82ed2d3]
122
+ - Updated dependencies [5a3de86]
123
+ - @jini-ai/core@0.6.10
124
+
125
+ ## 0.6.7 — 2026-10-22
126
+
127
+ - 2026-10-22: Memory fixes (durable run store reads its own row, ledger parses events once), shared settings segmented control, Slack token rule restored, coverage.
128
+
1
129
  ## 0.6.6 — 2026-10-16
2
130
 
3
131
  - Staged attachment rows sit 8px apart so the top row's remove target stays 44px (4884cf15).
@@ -114,7 +242,6 @@
114
242
  - Correct renderer priority and opt-in stylesheet documentation; retain the layout rationale with neutral provenance.
115
243
  - BREAKING: replace the local finalizer Clock with core Clock.nowMs(); core/React helpers and ext-event slot-key callbacks take required objects.
116
244
 
117
-
118
245
  - BREAKING: ChatStore methods use required argument objects; paging options move to the second bag. ChatHistoryMaintenance and ChatStoreFactory follow the same convention.
119
246
  - BREAKING: SQLite/Postgres/PGlite factories use core `Clock` through `{ clock }` rather than `{ now }`; system-time defaults and SQL behavior are preserved.
120
247
 
@@ -204,5 +331,6 @@
204
331
  - @jini-ai/agentic@0.1.1
205
332
 
206
333
  ### C2 storage ownership
334
+
207
335
  - Added `./store/legacy` and `./store/legacy/sqlite`; preserved sync local project chat CRUD and schema.
208
336
  - Retained the original owner-isolation suite on the concern adapter after draining the old package.
@@ -0,0 +1,54 @@
1
+ import type { AgentEvent } from './events.js';
2
+ import type { ChatAttachment, ChatMessage } from './messages.js';
3
+ export interface AcceptedChatInput {
4
+ id: string;
5
+ text: string;
6
+ attachments?: ChatAttachment[];
7
+ secretRedaction?: ChatMessage['secretRedaction'];
8
+ /** Wire ids replaced by this receipt remain seen by recall. */
9
+ aliases?: readonly string[];
10
+ }
11
+ /** Display queue constituents using their accepted identities; storage and delivery retain one turn.
12
+ * MOVE: Receipts travel in events_json, so separate bubbles survive a history reload without another store.
13
+ * @complexity O(m + e + i) time/space for m messages, e receipt events and i accepted queue inputs.
14
+ */
15
+ export declare function projectAcceptedInputMessages({ messages }: {
16
+ messages: readonly ChatMessage[];
17
+ }, _options?: {}): ChatMessage[];
18
+ type UserMessageEvent = Extract<AgentEvent, {
19
+ kind: 'ext';
20
+ }> & {
21
+ data: AcceptedChatInput & {
22
+ optimistic?: boolean;
23
+ toolUseId?: string;
24
+ afterInputId?: string;
25
+ inputs?: AcceptedChatInput[];
26
+ };
27
+ };
28
+ /** Reads the existing event-backed user bubble without depending on a host renderer. */
29
+ export declare function acceptedInputEvent({ event }: {
30
+ event: AgentEvent;
31
+ }, _options?: {}): UserMessageEvent | null;
32
+ /** Builds the same bubble for delivered answers and steering; queues use the existing pending row. */
33
+ export declare function acceptedInputUserEvent({ input, inputs, toolUseId, afterInputId }: {
34
+ input: AcceptedChatInput;
35
+ inputs?: AcceptedChatInput[];
36
+ toolUseId?: string;
37
+ afterInputId?: string;
38
+ }, { receipt }?: {
39
+ receipt?: 'optimistic' | 'accepted';
40
+ }): AgentEvent;
41
+ /** Projects saved answers and reconciles optimistic bubbles with wire events, occurrence for
42
+ * occurrence. Legacy steering receipts have no client id: only unmatched optimistic input may
43
+ * match by text; two real events with equal text always remain distinct.
44
+ * @complexity O(e + u + a) time, O(e + u) space for e events, u user inputs and total answer-label lookup cost a.
45
+ */
46
+ export declare function projectAcceptedInputEvents({ events }: {
47
+ events: readonly AgentEvent[];
48
+ }, _options?: {}): AgentEvent[];
49
+ /** The sole transcript-to-recall projection. A merged queued turn retains its original inputs. */
50
+ export declare function acceptedInputsFromMessages({ messages }: {
51
+ messages: readonly ChatMessage[];
52
+ }, _options?: {}): AcceptedChatInput[];
53
+ export {};
54
+ //# sourceMappingURL=accepted-input.d.ts.map
@@ -0,0 +1,165 @@
1
+ /** Accepted composer input has one identity across optimistic display, replay and recall. */
2
+ import { questionAnswerFromResult } from './question-answer.js';
3
+ import { MID_RUN_USER_MESSAGE_EVENT_NAME } from './turn-usage.js';
4
+ /** Display queue constituents using their accepted identities; storage and delivery retain one turn.
5
+ * MOVE: Receipts travel in events_json, so separate bubbles survive a history reload without another store.
6
+ * @complexity O(m + e + i) time/space for m messages, e receipt events and i accepted queue inputs.
7
+ */
8
+ export function projectAcceptedInputMessages({ messages }, _options = {}) {
9
+ return messages.flatMap(message => {
10
+ if (message.role !== 'user')
11
+ return [message];
12
+ const inputs = (message.events ?? []).flatMap(event => {
13
+ const input = acceptedInputEvent({ event });
14
+ return input ? [input.data] : [];
15
+ });
16
+ if (inputs.length === 0)
17
+ return [message];
18
+ const { attachments: _attachments, secretRedaction: _redaction, ...turn } = message;
19
+ // MOVE: Older receipts have no per-input files; retain those legacy files on the final bubble.
20
+ return inputs.map((input, index) => ({ ...turn, id: input.id, content: input.text,
21
+ attachments: input.attachments ?? (index === inputs.length - 1 ? message.attachments ?? [] : []), ...(input.secretRedaction ? { secretRedaction: input.secretRedaction } : {}) }));
22
+ });
23
+ }
24
+ /** Reads the existing event-backed user bubble without depending on a host renderer. */
25
+ export function acceptedInputEvent({ event }, _options = {}) {
26
+ if (event.kind !== 'ext' || event.name !== MID_RUN_USER_MESSAGE_EVENT_NAME)
27
+ return null;
28
+ const data = event.data;
29
+ return data && typeof data.id === 'string' && typeof data.text === 'string' ? event : null;
30
+ }
31
+ /** Project a persisted answer through the same receipt owner as composer input. */
32
+ function questionAnswerEvent({ event, input }, _options = {}) {
33
+ if (event.kind !== 'tool_result')
34
+ return null;
35
+ const text = questionAnswerFromResult({ result: event, input });
36
+ if (text === undefined)
37
+ return null;
38
+ return { kind: 'ext', name: MID_RUN_USER_MESSAGE_EVENT_NAME,
39
+ data: { id: `answer:${event.toolUseId}`, text, toolUseId: event.toolUseId } };
40
+ }
41
+ /** Builds the same bubble for delivered answers and steering; queues use the existing pending row. */
42
+ export function acceptedInputUserEvent({ input, inputs, toolUseId, afterInputId }, { receipt = 'optimistic' } = {}) {
43
+ return { kind: 'ext', name: MID_RUN_USER_MESSAGE_EVENT_NAME,
44
+ data: { ...input, ...(receipt === 'optimistic' ? { optimistic: true } : {}), ...(inputs === undefined ? {} : { inputs }), ...(afterInputId === undefined ? {} : { afterInputId }), ...(toolUseId === undefined ? {} : { toolUseId }) } };
45
+ }
46
+ function expandQuestionAnswers({ events }, _options = {}) {
47
+ const savedAnswers = new Set(events.flatMap(event => {
48
+ const input = acceptedInputEvent({ event });
49
+ return input && !input.data.optimistic && input.data.toolUseId ? [input.data.toolUseId] : [];
50
+ }));
51
+ // MOVE: Persisted picks carry values; the original tool input retains their human-readable labels.
52
+ const questions = new Map(events.flatMap(event => event.kind === 'tool_use' ? [[event.id, event.input]] : []));
53
+ return events.flatMap(event => {
54
+ const answer = questionAnswerEvent({ event, input: event.kind === 'tool_result' ? questions.get(event.toolUseId) : undefined });
55
+ return answer && !savedAnswers.has(answer.data.toolUseId) ? [event, answer] : [event];
56
+ });
57
+ }
58
+ function collectOptimisticInputs({ events }, _options = {}) {
59
+ const optimisticByText = new Map();
60
+ const optimisticByQuestion = new Map();
61
+ for (const event of events) {
62
+ const input = acceptedInputEvent({ event });
63
+ if (!input?.data.optimistic)
64
+ continue;
65
+ if (input.data.toolUseId)
66
+ optimisticByQuestion.set(input.data.toolUseId, input);
67
+ else {
68
+ const list = optimisticByText.get(input.data.text) ?? { events: [], index: 0 };
69
+ list.events.push(input);
70
+ optimisticByText.set(input.data.text, list);
71
+ }
72
+ }
73
+ return { byText: optimisticByText, byQuestion: optimisticByQuestion };
74
+ }
75
+ function matchOptimisticInput({ input, optimistic, seen }, _options = {}) {
76
+ const candidates = optimistic.byText.get(input.data.text);
77
+ const next = candidates?.events[candidates.index];
78
+ const follows = next?.data.afterInputId === undefined || seen.has(next.data.afterInputId);
79
+ // Keep predecessor testing before the caller adds this receipt's ids to seen.
80
+ return input.data.toolUseId ? optimistic.byQuestion.get(input.data.toolUseId) : follows ? next : undefined;
81
+ }
82
+ function mergeAcceptedReceipt({ input, pending }, _options = {}) {
83
+ return { ...input, data: { ...input.data, id: pending.data.id,
84
+ ...(pending.data.inputs ? { inputs: pending.data.inputs } : {}),
85
+ aliases: [...new Set([...(input.data.aliases ?? []), input.data.id, ...(pending.data.aliases ?? [])])] } };
86
+ }
87
+ function acceptedInputIds({ input }, _options = {}) {
88
+ return [input.data.id, ...(input.data.toolUseId ? [`answer:${input.data.toolUseId}`] : []), ...(input.data.aliases ?? [])];
89
+ }
90
+ function reconcileAcceptedInput({ event, optimistic, state }, _options = {}) {
91
+ const input = acceptedInputEvent({ event });
92
+ if (!input || input.data.optimistic)
93
+ return;
94
+ const { replacements, replaced, seen } = state;
95
+ const ids = acceptedInputIds({ input });
96
+ if (ids.some(id => seen.has(id))) {
97
+ replaced.add(event);
98
+ return;
99
+ }
100
+ const pending = matchOptimisticInput({ input, optimistic, seen });
101
+ for (const id of ids)
102
+ seen.add(id);
103
+ if (!pending)
104
+ return;
105
+ advanceOptimisticInput({ input, optimistic });
106
+ for (const id of [pending.data.id, ...(pending.data.aliases ?? [])])
107
+ seen.add(id);
108
+ optimistic.byQuestion.delete(input.data.toolUseId ?? '');
109
+ replaced.add(pending);
110
+ replacements.set(event, mergeAcceptedReceipt({ input, pending }));
111
+ }
112
+ function advanceOptimisticInput({ input, optimistic }, _options = {}) {
113
+ const candidates = optimistic.byText.get(input.data.text);
114
+ if (candidates && !input.data.toolUseId)
115
+ candidates.index += 1;
116
+ }
117
+ /** Projects saved answers and reconciles optimistic bubbles with wire events, occurrence for
118
+ * occurrence. Legacy steering receipts have no client id: only unmatched optimistic input may
119
+ * match by text; two real events with equal text always remain distinct.
120
+ * @complexity O(e + u + a) time, O(e + u) space for e events, u user inputs and total answer-label lookup cost a.
121
+ */
122
+ export function projectAcceptedInputEvents({ events }, _options = {}) {
123
+ const expanded = expandQuestionAnswers({ events });
124
+ const optimistic = collectOptimisticInputs({ events: expanded });
125
+ const state = { replacements: new Map(), replaced: new Set(), seen: new Set() };
126
+ for (const event of expanded)
127
+ reconcileAcceptedInput({ event, optimistic, state });
128
+ const { replacements, replaced } = state;
129
+ const projected = [];
130
+ const emitted = new Set();
131
+ for (const event of expanded) {
132
+ if (replaced.has(event))
133
+ continue;
134
+ const next = replacements.get(event) ?? event;
135
+ const input = acceptedInputEvent({ event: next });
136
+ if (input && emitted.has(input.data.id))
137
+ continue;
138
+ if (input)
139
+ emitted.add(input.data.id);
140
+ projected.push(next);
141
+ }
142
+ return projected;
143
+ }
144
+ /** The sole transcript-to-recall projection. A merged queued turn retains its original inputs. */
145
+ export function acceptedInputsFromMessages({ messages }, _options = {}) {
146
+ return messages.flatMap(message => {
147
+ if (message.role === 'user') {
148
+ if (message.inputPending)
149
+ return [];
150
+ // Queue constituents travel in events_json, already preserved by the existing chat store.
151
+ const receipts = (message.events ?? []).flatMap(event => {
152
+ const input = acceptedInputEvent({ event });
153
+ return input ? [input.data] : [];
154
+ });
155
+ return receipts.length > 0 ? receipts : [{ id: message.id, text: message.content }];
156
+ }
157
+ return projectAcceptedInputEvents({ events: message.events ?? [] }).flatMap(event => {
158
+ const input = acceptedInputEvent({ event });
159
+ if (!input)
160
+ return [];
161
+ return input.data.inputs?.map((receipt, index) => index === 0 ? { ...receipt, aliases: input.data.aliases ?? [] } : receipt) ?? [input.data];
162
+ });
163
+ });
164
+ }
165
+ //# sourceMappingURL=accepted-input.js.map
@@ -0,0 +1,43 @@
1
+ /** Browser-local per-chat decisions; the person's mode remains in the host's settings ledger. */
2
+ export interface AgentPluginChatState {
3
+ decision: {
4
+ selected: readonly string[];
5
+ asked?: boolean;
6
+ } | null;
7
+ excluded: readonly string[];
8
+ }
9
+ export interface AgentPluginUsage {
10
+ conversationId: string;
11
+ state: AgentPluginChatState;
12
+ plugins: readonly {
13
+ id: string;
14
+ name: string;
15
+ }[];
16
+ }
17
+ export interface ChatPluginStorage {
18
+ getItem(key: string): string | null;
19
+ setItem(key: string, value: string): void;
20
+ }
21
+ export declare const AGENT_PLUGIN_STATE_CHANGED = "jini:agent-plugin-state";
22
+ /** Corrupt/blocked storage permits a fresh question, as clearing localStorage does. */
23
+ export declare function readAgentPluginChatState({ conversationId }: {
24
+ conversationId: string;
25
+ }, { storage }?: {
26
+ storage?: ChatPluginStorage | undefined;
27
+ }): AgentPluginChatState;
28
+ /** Persist only decision/exclusions. Existing exclusions win over an in-flight server response. */
29
+ export declare function writeAgentPluginChatState({ conversationId, state }: {
30
+ conversationId: string;
31
+ state: AgentPluginChatState;
32
+ }, { storage }?: {
33
+ storage?: ChatPluginStorage | undefined;
34
+ }): void;
35
+ /** Called only for the protocol's structured plugin event, before delivering it to the transcript. */
36
+ export declare function captureAgentPluginUsage({ event }: {
37
+ event: {
38
+ kind: string;
39
+ name?: string;
40
+ data?: unknown;
41
+ };
42
+ }, _options?: {}): void;
43
+ //# sourceMappingURL=agent-plugin-chat-state.d.ts.map
@@ -0,0 +1,48 @@
1
+ export const AGENT_PLUGIN_STATE_CHANGED = 'jini:agent-plugin-state';
2
+ const key = (conversationId) => `jini:chat-agent-plugins:${conversationId}`;
3
+ function browserStorage() {
4
+ try {
5
+ return typeof window === 'undefined' ? undefined : window.localStorage;
6
+ }
7
+ catch {
8
+ return undefined;
9
+ }
10
+ }
11
+ function isStringList({ value }) {
12
+ return Array.isArray(value) && value.every(id => typeof id === 'string');
13
+ }
14
+ /** A stored row is usable only when its exclusions and (null or listed) decision are all string ids. */
15
+ function usableChatState({ value }) {
16
+ return value && isStringList({ value: value.excluded }) && (value.decision === null || isStringList({ value: value.decision?.selected })) ? value : null;
17
+ }
18
+ /** Corrupt/blocked storage permits a fresh question, as clearing localStorage does. */
19
+ export function readAgentPluginChatState({ conversationId }, { storage = browserStorage() } = {}) {
20
+ try {
21
+ const state = usableChatState({ value: JSON.parse(storage?.getItem(key(conversationId)) ?? 'null') });
22
+ if (state)
23
+ return state;
24
+ }
25
+ catch { /* Private browsing/corrupt rows behave as a new chat decision. */ }
26
+ return { decision: null, excluded: [] };
27
+ }
28
+ /** Persist only decision/exclusions. Existing exclusions win over an in-flight server response. */
29
+ export function writeAgentPluginChatState({ conversationId, state }, { storage = browserStorage() } = {}) {
30
+ const current = readAgentPluginChatState({ conversationId }, { storage });
31
+ const next = { decision: state.decision, excluded: [...new Set([...current.excluded, ...state.excluded])] };
32
+ try {
33
+ storage?.setItem(key(conversationId), JSON.stringify(next));
34
+ }
35
+ catch { /* Session can continue without persistence. */ }
36
+ if (typeof window !== 'undefined')
37
+ window.dispatchEvent(new CustomEvent(AGENT_PLUGIN_STATE_CHANGED, { detail: { conversationId, state: next } }));
38
+ }
39
+ /** Called only for the protocol's structured plugin event, before delivering it to the transcript. */
40
+ export function captureAgentPluginUsage({ event }, _options = {}) {
41
+ if (event.kind !== 'ext' || event.name !== 'agent_plugins')
42
+ return;
43
+ const usage = event.data;
44
+ if (typeof usage?.conversationId !== 'string' || !usage.state || !Array.isArray(usage.state.excluded))
45
+ return;
46
+ writeAgentPluginChatState({ conversationId: usage.conversationId, state: usage.state });
47
+ }
48
+ //# sourceMappingURL=agent-plugin-chat-state.js.map
@@ -80,34 +80,38 @@ export function createA2uiActionPoster({ baseUrl, path, fetch, reportFailure },
80
80
  }
81
81
  const controller = new AbortController();
82
82
  const timer = setTimeout(() => controller.abort(), timeoutMs);
83
- try {
84
- const response = await fetch(endpoint, {
85
- method: "POST",
86
- credentials: "same-origin",
87
- headers: { ...options.headers, "content-type": "application/json" },
88
- body: JSON.stringify({ exchangeId, message }),
89
- signal: controller.signal,
90
- });
91
- if (!response.ok) {
92
- const detail = await response.text().catch(() => "");
93
- reportFailure({ args: [`[a2ui] action delivery failed (${response.status}) for surface ${exchangeId}`, detail] }, {});
94
- return { ok: false, reason: reasonForStatus(response.status) };
95
- }
96
- return { ok: true };
97
- }
98
- catch (error) {
99
- const timedOut = error instanceof Error && error.name === "AbortError";
100
- reportFailure({ args: [`[a2ui] action delivery failed for surface ${exchangeId}`, timedOut ? `timed out after ${timeoutMs}ms` : error] }, {});
101
- return {
102
- ok: false,
103
- reason: timedOut
104
- ? "This took too long to deliver and was given up on."
105
- : "Couldn't reach the server to deliver this action.",
106
- };
107
- }
108
- finally {
109
- clearTimeout(timer);
110
- }
83
+ // Cleared on every outcome; a `.finally` rather than try/finally so no path falls through.
84
+ return postOnce({ endpoint, exchangeId, message, timeoutMs, reportFailure, fetch, signal: controller.signal, headers: options.headers })
85
+ .finally(() => clearTimeout(timer));
111
86
  };
112
87
  }
88
+ /** One POST attempt and its classification; never rejects. @complexity O(1). */
89
+ async function postOnce(attempt) {
90
+ const { exchangeId, reportFailure } = attempt;
91
+ try {
92
+ const response = await attempt.fetch(attempt.endpoint, {
93
+ method: "POST",
94
+ credentials: "same-origin",
95
+ headers: { ...attempt.headers, "content-type": "application/json" },
96
+ body: JSON.stringify({ exchangeId, message: attempt.message }),
97
+ signal: attempt.signal,
98
+ });
99
+ if (!response.ok) {
100
+ const detail = await response.text().catch(() => "");
101
+ reportFailure({ args: [`[a2ui] action delivery failed (${response.status}) for surface ${exchangeId}`, detail] }, {});
102
+ return { ok: false, reason: reasonForStatus(response.status) };
103
+ }
104
+ return { ok: true };
105
+ }
106
+ catch (error) {
107
+ const timedOut = error instanceof Error && error.name === "AbortError";
108
+ reportFailure({ args: [`[a2ui] action delivery failed for surface ${exchangeId}`, timedOut ? `timed out after ${attempt.timeoutMs}ms` : error] }, {});
109
+ return {
110
+ ok: false,
111
+ reason: timedOut
112
+ ? "This took too long to deliver and was given up on."
113
+ : "Couldn't reach the server to deliver this action.",
114
+ };
115
+ }
116
+ }
113
117
  //# sourceMappingURL=a2ui-action-poster.js.map
@@ -8,7 +8,7 @@ export interface AssistantConversation {
8
8
  updatedAt: number;
9
9
  }
10
10
  /**
11
- * The subset of a live transcript that is worth writing: user turns, and assistant turns whose run
11
+ * The subset of a live transcript that is worth writing: accepted user turns, and assistant turns whose run
12
12
  * has reached a terminal status.
13
13
  *
14
14
  * An assistant message that is still `queued` or `running` is rewritten on every delta, so
@@ -1,6 +1,6 @@
1
1
  import { isTerminalRunStatus } from "../messages.js";
2
2
  /**
3
- * The subset of a live transcript that is worth writing: user turns, and assistant turns whose run
3
+ * The subset of a live transcript that is worth writing: accepted user turns, and assistant turns whose run
4
4
  * has reached a terminal status.
5
5
  *
6
6
  * An assistant message that is still `queued` or `running` is rewritten on every delta, so
@@ -18,7 +18,7 @@ import { isTerminalRunStatus } from "../messages.js";
18
18
  * Deleting them here would trade a history bug for a forensics hole.
19
19
  */
20
20
  export function persistableMessages({ messages }, _options = {}) {
21
- return messages.filter((message) => message.role === "user" || isTerminalRunStatus({ status: message.runStatus }));
21
+ return messages.filter((message) => message.role === "user" ? !message.inputPending : isTerminalRunStatus({ status: message.runStatus }));
22
22
  }
23
23
  /**
24
24
  * The key the admin's chat writers use to remember "this message is already saved": the message id,
@@ -2,7 +2,7 @@ import type { AgentEvent } from "../events.js";
2
2
  import type { ChatMessage } from "../messages.js";
3
3
  import type { ChatTransport, RunHandlers, StartRunInput } from "../transport.js";
4
4
  import type { RunNotices } from "../run-events/entry.js";
5
- import type { createAgUiRunClient } from "./ag-ui-transport.js";
5
+ import type { AgUiRunClient } from "./ag-ui-transport.js";
6
6
  import type { DurableRunBinding } from "./durable-run-subscription.js";
7
7
  /** Per-turn dispatch input; hosts may return their fuller saved settings without UI dependencies. */
8
8
  export interface AssistantExecutionConfig {
@@ -127,7 +127,7 @@ export interface AssistantTransportPorts {
127
127
  messageId: unknown;
128
128
  conversationId?: string;
129
129
  }, optional: Record<string, never>) => Promise<Response>;
130
- readonly agUi: ReturnType<typeof createAgUiRunClient>;
130
+ readonly agUi: AgUiRunClient;
131
131
  }
132
132
  export interface ByokFrameContext {
133
133
  collected: AgentEvent[];