@jini-ai/chat 0.5.4 → 0.5.5

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 (122) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/dist/core/ag-ui/entry.d.ts +33 -0
  3. package/dist/core/ag-ui/entry.js +80 -0
  4. package/dist/core/agentic/chat-capabilities.d.ts +2 -4
  5. package/dist/core/agentic/index.d.ts +4 -17
  6. package/dist/core/agentic/index.js +4 -17
  7. package/dist/core/assistant-client/a2ui-action-poster.d.ts +46 -0
  8. package/dist/core/assistant-client/a2ui-action-poster.js +113 -0
  9. package/dist/core/assistant-client/ag-ui-transport.d.ts +68 -0
  10. package/dist/core/assistant-client/ag-ui-transport.js +147 -0
  11. package/dist/core/assistant-client/attachment-liveness.d.ts +37 -0
  12. package/dist/core/assistant-client/attachment-liveness.js +93 -0
  13. package/dist/core/assistant-client/durable-run-subscription.d.ts +39 -0
  14. package/dist/core/assistant-client/durable-run-subscription.js +96 -0
  15. package/dist/core/assistant-client/entry.d.ts +13 -0
  16. package/dist/core/assistant-client/entry.js +7 -0
  17. package/dist/core/assistant-client/messages.d.ts +73 -0
  18. package/dist/core/assistant-client/messages.js +68 -0
  19. package/dist/core/assistant-client/transport.d.ts +164 -0
  20. package/dist/core/assistant-client/transport.js +734 -0
  21. package/dist/core/browser/page-actions.d.ts +121 -0
  22. package/dist/core/browser/page-actions.js +113 -0
  23. package/dist/core/durable-runs/continuation.d.ts +18 -0
  24. package/dist/core/durable-runs/continuation.js +61 -0
  25. package/dist/core/durable-runs/ledger-contracts.d.ts +53 -0
  26. package/dist/core/durable-runs/ledger-contracts.js +2 -0
  27. package/dist/core/durable-runs/policies.d.ts +6 -0
  28. package/dist/core/durable-runs/policies.js +6 -0
  29. package/dist/core/durable-runs/ports.d.ts +123 -0
  30. package/dist/core/durable-runs/ports.js +2 -0
  31. package/dist/core/durable-runs/run-contracts.d.ts +17 -0
  32. package/dist/core/durable-runs/run-contracts.js +2 -0
  33. package/dist/core/durable-runs/run-lock.d.ts +4 -0
  34. package/dist/core/durable-runs/run-lock.js +4 -0
  35. package/dist/core/index.d.ts +6 -19
  36. package/dist/core/index.js +6 -19
  37. package/dist/core/messages.d.ts +2 -0
  38. package/dist/core/question-form/format.d.ts +0 -9
  39. package/dist/core/question-form/format.js +16 -2
  40. package/dist/core/question-form/normalize.js +1 -0
  41. package/dist/core/question-form/secret-answers.d.ts +38 -0
  42. package/dist/core/question-form/secret-answers.js +45 -0
  43. package/dist/core/question-form/types.d.ts +2 -0
  44. package/dist/core/question-form.d.ts +1 -0
  45. package/dist/core/question-form.js +1 -0
  46. package/dist/core/run-events/entry.d.ts +208 -16
  47. package/dist/core/run-events/entry.js +273 -39
  48. package/dist/core/transcript.d.ts +3 -2
  49. package/dist/core/transcript.js +19 -3
  50. package/dist/core/user-text-redaction.d.ts +56 -0
  51. package/dist/core/user-text-redaction.js +89 -0
  52. package/dist/embed-widget/browser/page-actions.d.ts +1 -120
  53. package/dist/embed-widget/browser/page-actions.js +2 -112
  54. package/dist/embed-widget/react/embed.d.ts +1 -1
  55. package/dist/embed-widget/react/embed.js +1 -1
  56. package/dist/react/artifact-types.d.ts +9 -15
  57. package/dist/react/components/QuestionForm.d.ts +2 -1
  58. package/dist/react/components/QuestionForm.js +18 -12
  59. package/dist/react/components/QuestionsPanel.d.ts +3 -2
  60. package/dist/react/components/QuestionsPanel.js +2 -2
  61. package/dist/react/features/chat-pane/agent-tools.d.ts +1 -1
  62. package/dist/react/features/chat-pane/agent-tools.js +0 -1
  63. package/dist/react/features/chat-pane/components/AgentRuntimePicker.js +5 -2
  64. package/dist/react/features/chat-pane/components/ChatPane.d.ts +1 -1
  65. package/dist/react/features/chat-pane/components/ChatPane.js +5 -2
  66. package/dist/react/features/chat-pane/create-typed-answer-poster.d.ts +2 -1
  67. package/dist/react/features/chat-pane/create-typed-answer-poster.js +33 -1
  68. package/dist/react/features/chat-pane/hooks/useAgentRuntimePicker.hooks.js +3 -2
  69. package/dist/react/features/chat-pane/hooks/useChatPane.hooks.d.ts +4 -1
  70. package/dist/react/features/chat-pane/hooks/useChatPane.hooks.js +29 -17
  71. package/dist/react/features/chat-pane/model-options.d.ts +10 -0
  72. package/dist/react/features/chat-pane/model-options.js +51 -0
  73. package/dist/react/features/chat-pane/rules.d.ts +3 -2
  74. package/dist/react/features/chat-pane/rules.js +12 -4
  75. package/dist/react/features/chat-pane/types.d.ts +25 -1
  76. package/dist/react/hooks/assistant-chats-contracts.d.ts +164 -0
  77. package/dist/react/hooks/assistant-chats-contracts.js +2 -0
  78. package/dist/react/hooks/assistant-chats.d.ts +7 -0
  79. package/dist/react/hooks/assistant-chats.js +960 -0
  80. package/dist/react/hooks/attachment-preview-cache.d.ts +1 -1
  81. package/dist/react/hooks/attachment-preview-cache.js +1 -1
  82. package/dist/react/hooks/composer-draft-cache.d.ts +2 -2
  83. package/dist/react/hooks/composer-draft-cache.js +2 -2
  84. package/dist/react/hooks/fab-position.d.ts +90 -0
  85. package/dist/react/hooks/fab-position.js +271 -0
  86. package/dist/react/hooks/useConversation.d.ts +4 -1
  87. package/dist/react/hooks/useConversation.js +14 -12
  88. package/dist/react/hooks/useUserTextGuard.d.ts +11 -0
  89. package/dist/react/hooks/useUserTextGuard.js +19 -0
  90. package/dist/react/index.d.ts +12 -21
  91. package/dist/react/index.js +7 -0
  92. package/dist/server/durable-runs/accept.d.ts +25 -0
  93. package/dist/server/durable-runs/accept.js +21 -0
  94. package/dist/server/durable-runs/entry.d.ts +5 -0
  95. package/dist/server/durable-runs/entry.js +4 -0
  96. package/dist/server/durable-runs/recover.d.ts +5 -0
  97. package/dist/server/durable-runs/recover.js +147 -0
  98. package/dist/server/run-finalizer/ports.d.ts +2 -16
  99. package/dist/store/legacy/sqlite/messages.d.ts +24 -18
  100. package/dist/store/legacy/sqlite/messages.js +12 -2
  101. package/dist/store/pglite/index.d.ts +2 -1
  102. package/dist/store/postgres/index.d.ts +2 -1
  103. package/dist/store/runtime/chat-expiry-sweep.d.ts +40 -0
  104. package/dist/store/runtime/chat-expiry-sweep.js +70 -0
  105. package/dist/store/runtime/chat-history-store.d.ts +20 -0
  106. package/dist/store/runtime/chat-history-store.js +50 -0
  107. package/dist/store/runtime/durable-run-store.d.ts +21 -0
  108. package/dist/store/runtime/durable-run-store.js +342 -0
  109. package/dist/store/runtime/entry.d.ts +7 -0
  110. package/dist/store/runtime/entry.js +6 -0
  111. package/dist/store/runtime/kernel.d.ts +29 -0
  112. package/dist/store/runtime/kernel.js +2 -0
  113. package/dist/store/runtime/run-guarded-chat-store.d.ts +10 -0
  114. package/dist/store/runtime/run-guarded-chat-store.js +42 -0
  115. package/dist/store/runtime/run-ledger.d.ts +15 -0
  116. package/dist/store/runtime/run-ledger.js +206 -0
  117. package/dist/store/sql/boundary.d.ts +6 -0
  118. package/dist/store/sql/boundary.js +7 -1
  119. package/dist/store/sql/store.d.ts +3 -2
  120. package/dist/store/sql/store.js +9 -2
  121. package/dist/store/sqlite/index.d.ts +3 -2
  122. package/package.json +75 -10
@@ -37,7 +37,7 @@ export declare const MAX_CACHED_ATTACHMENT_PREVIEWS = 50;
37
37
  export declare function cacheAttachmentPreviewSource({ path, file }: {
38
38
  path: string;
39
39
  file: File;
40
- }): void;
40
+ }, _options?: Record<string, never>): void;
41
41
  /**
42
42
  * Reads back the `File` cached for `path`, or `undefined` when this browser never staged it (a
43
43
  * different session, a reload, or eviction past the cap) - the only signal a preview needs to fall
@@ -35,7 +35,7 @@ const sources = new Map();
35
35
  * if this would introduce a new entry past {@link MAX_CACHED_ATTACHMENT_PREVIEWS}.
36
36
  * @complexity Time/space: O(1) amortized.
37
37
  */
38
- export function cacheAttachmentPreviewSource({ path, file }) {
38
+ export function cacheAttachmentPreviewSource({ path, file }, _options = {}) {
39
39
  if (!sources.has(path) && sources.size >= MAX_CACHED_ATTACHMENT_PREVIEWS) {
40
40
  const oldest = sources.keys().next().value;
41
41
  if (oldest !== undefined)
@@ -131,7 +131,7 @@ export declare function readCachedAttachments({ conversationId }: {
131
131
  */
132
132
  export declare function readCachedAttachmentBatchId({ conversationId }: {
133
133
  conversationId: string | null | undefined;
134
- }): string | null;
134
+ }, _options?: Record<string, never>): string | null;
135
135
  /**
136
136
  * Records the upload batch of an attachment about to be staged in `conversationId`'s composer.
137
137
  * Memory only: the stored envelope picks it up on the {@link writeCachedAttachments} that persists
@@ -142,7 +142,7 @@ export declare function readCachedAttachmentBatchId({ conversationId }: {
142
142
  export declare function writeCachedAttachmentBatchId({ conversationId, batchId }: {
143
143
  conversationId: string | null | undefined;
144
144
  batchId: string;
145
- }): void;
145
+ }, _options?: Record<string, never>): void;
146
146
  /**
147
147
  * Stores `attachments` for `conversationId` in both tiers.
148
148
  *
@@ -370,7 +370,7 @@ export function readCachedAttachments({ conversationId }) {
370
370
  * written before batch ids were recorded) — the caller then starts a fresh batch.
371
371
  * @complexity Time/space: O(1) on an in-memory hit, O(n) in the stored count otherwise.
372
372
  */
373
- export function readCachedAttachmentBatchId({ conversationId }) {
373
+ export function readCachedAttachmentBatchId({ conversationId }, _options = {}) {
374
374
  if (!conversationId)
375
375
  return null;
376
376
  if (readCachedAttachments({ conversationId: conversationId }) === null)
@@ -384,7 +384,7 @@ export function readCachedAttachmentBatchId({ conversationId }) {
384
384
  *
385
385
  * @complexity Time/space: O(1) amortized.
386
386
  */
387
- export function writeCachedAttachmentBatchId({ conversationId, batchId }) {
387
+ export function writeCachedAttachmentBatchId({ conversationId, batchId }, _options = {}) {
388
388
  if (!conversationId)
389
389
  return;
390
390
  if (!stagedBatchIds.has(conversationId) && stagedBatchIds.size >= MAX_CACHED_CONVERSATION_DRAFTS) {
@@ -0,0 +1,90 @@
1
+ export interface FabPositionResult {
2
+ /** Inline `style` for the FAB button — `right` and `bottom`, in px, always resolved against
3
+ * the *current* viewport (never a stale stored pixel value). Always both: the FAB is
4
+ * positioned as an offset from the right/bottom edges regardless of where on screen it was
5
+ * dropped, so there is no left-vs-right branch to keep in sync with the stored fractions. */
6
+ style: {
7
+ right: number;
8
+ bottom: number;
9
+ };
10
+ /** Attach to the FAB's `onPointerDown`. */
11
+ onPointerDown: (e: React.PointerEvent<HTMLButtonElement>) => void;
12
+ /**
13
+ * Call from the FAB's `onClick`, in place of the toggle callback directly — returns `true` if
14
+ * the click should be treated as a real activation (fire the toggle) and `false` if it is the
15
+ * tail end of a drag and should be swallowed.
16
+ *
17
+ * Deliberately NOT the `isDragging` boolean below checked inline at the call site: `pointerup`
18
+ * (which ends the drag) and `click` (which this guards) are two different events, dispatched
19
+ * back-to-back by the browser, and by the time `click` fires React may not have re-rendered
20
+ * with `isDragging`'s new value yet — reading a piece of React state that was *just* set a
21
+ * moment earlier in the same synchronous sequence is exactly the kind of timing this project's
22
+ * conventions call out as unreliable (see `use-settings-slice.hooks.ts`'s "read at run time, not
23
+ * schedule time" refrain, one level down in raw event timing rather than async scheduling).
24
+ * `consumeDragFlag` reads and clears a ref instead, which has no such lag.
25
+ */
26
+ consumeDragFlag: () => boolean;
27
+ /** For UI feedback only (e.g. suppressing the hover/press transition while dragging) — not to
28
+ * be used to decide whether to swallow a click; use `consumeDragFlag` for that. */
29
+ isDragging: boolean;
30
+ }
31
+ /**
32
+ * @param options.dockOpen When the assistant dock/sheet is open, an UNTOUCHED or migrated-legacy
33
+ * resting spot moves off it so it never sits over the dock's own composer/close controls by
34
+ * accident — this mirrors what the old hard-coded `.chat-fab-dock-open` rule did for the
35
+ * desktop-docked panel, generalized to the dragged position and to the mobile sheet's own
36
+ * (viewport-fraction) height. A spot the operator explicitly dragged onto the dock is left alone
37
+ * (see `pinnedByUser`'s own doc on `StoredFabPosition`) — this option only guards against an
38
+ * unintentional overlap, not against a deliberate one.
39
+ * @param options.avoidBottomPx Extra clearance to hold above the bottom edge while `dockOpen` —
40
+ * the caller passes the mobile sheet's current height here (0 at desktop, where the dock sits
41
+ * to the *side*, not below, so no bottom clearance is needed there — see `App.tsx`).
42
+ * @param options.avoidRightPx Extra clearance to hold left of the right edge while `dockOpen` —
43
+ * the caller passes the DESKTOP dock's current width here (0 in sheet mode, where the sheet
44
+ * spans the full width and `avoidBottomPx` is the axis that matters).
45
+ *
46
+ * This axis was missing until 2026-08-05, and its absence was a live defect rather than a
47
+ * theoretical gap: the FAB is `position: fixed` and offset from the RIGHT edge, and the desktop
48
+ * dock is also pinned to the right edge, so with the dock open the 56px FAB sat directly on top
49
+ * of the dock's own composer send button and swallowed its clicks. Playwright caught it as
50
+ * `<button class="chat-fab chat-fab-dock-open"> intercepts pointer events` while trying to send
51
+ * a message — i.e. the assistant could not be used with a mouse at the default FAB position.
52
+ *
53
+ * How it got lost is worth recording, because the code confidently said otherwise: the old
54
+ * `.chat-fab-dock-open { right: calc(380px + 20px) }` rule moved the FAB SIDEWAYS. When this
55
+ * hook replaced it, only the vertical axis was carried over, while the comment on
56
+ * `effectiveBottom` below claimed `avoidBottomPx` "generalizes" what that rule did — it does
57
+ * not, it generalizes a different axis, and desktop passes `avoidBottomPx: 0`, so on desktop
58
+ * nothing displaced the FAB at all. The `.chat-fab-dock-open` class is still applied by
59
+ * `ChatFab.tsx` and has had no CSS rule behind it since.
60
+ *
61
+ * Measured by the caller rather than hard-coded back to 380px, for the same
62
+ * resolution-independence reason this whole hook exists (see the module doc).
63
+ *
64
+ * @complexity O(1) per pointer event — no work scales with anything caller-controlled.
65
+ */
66
+ export declare function useFabPosition(options: FabPositionInput, _optional?: Record<string, never>): FabPositionResult;
67
+ /** Storage and DOM handles are host ports; geometry matches the host button CSS. */
68
+ export interface FabPositionInput {
69
+ readonly dockOpen: boolean;
70
+ readonly avoidBottomPx: number;
71
+ readonly avoidRightPx?: number;
72
+ readonly storageKey: string;
73
+ readonly storage: Pick<Storage, "getItem" | "setItem">;
74
+ readonly viewport: Window;
75
+ readonly pointerEvents: Document;
76
+ /** Keeps the FAB's edge margin and the sheet-avoidance gap in one place instead of duplicating
77
+ * the literal in both this file and `styles.css`. */
78
+ readonly edgeMargin: number;
79
+ /** Matches `.chat-fab`'s own `width`/`height` in `styles/assistant.css` — used only to keep a
80
+ * dragged position from clamping the FAB half off-screen; if that CSS size ever changes, update
81
+ * both together (there is no single source of truth to read the rendered size from here without
82
+ * a layout measurement on every drag frame, which is not worth it for a constant that changes
83
+ * approximately never). */
84
+ readonly sizePx: number;
85
+ /** Below this many pixels of net pointer movement, a pointerdown/up pair is treated as a click,
86
+ * not a drag — small enough that an intentional drag is never mistaken for a tap, large enough
87
+ * that a tap's inevitable few pixels of finger movement never gets misread as a drag and eaten. */
88
+ readonly dragThresholdPx: number;
89
+ }
90
+ //# sourceMappingURL=fab-position.d.ts.map
@@ -0,0 +1,271 @@
1
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
2
+ function isLegacyShape(value) {
3
+ return (value.side === "left" || value.side === "right") && typeof value.bottomFraction === "number";
4
+ }
5
+ function clamp(value, min, max) {
6
+ return Math.min(max, Math.max(min, value));
7
+ }
8
+ function readPersisted({ storage, storageKey: STORAGE_KEY, viewport: window, edgeMargin: FAB_EDGE_MARGIN, sizePx: FAB_SIZE_PX }) {
9
+ try {
10
+ const raw = storage.getItem(STORAGE_KEY);
11
+ if (!raw)
12
+ return null;
13
+ const parsed = JSON.parse(raw);
14
+ if (typeof parsed.rightFraction === "number" && typeof parsed.bottomFraction === "number") {
15
+ return {
16
+ rightFraction: clamp(parsed.rightFraction, 0, 1),
17
+ bottomFraction: clamp(parsed.bottomFraction, 0, 1),
18
+ // Absent on any value written before the `pinnedByUser` field existed — treated as `false`
19
+ // (same protection as the default spot) rather than assumed `true`, since a pre-existing
20
+ // stored drop is exactly the case `pinnedByUser`'s own doc calls out as unearned evidence.
21
+ pinnedByUser: parsed.pinnedByUser === true,
22
+ };
23
+ }
24
+ if (isLegacyShape(parsed)) {
25
+ // Map the edge to an x-fraction near that edge — the legacy shape never recorded a
26
+ // horizontal offset (it was always pinned flush to the edge margin), so "near that edge" is
27
+ // the most faithful reading of it available, not an approximation of a value that existed.
28
+ const rightFraction = parsed.side === "right"
29
+ ? FAB_EDGE_MARGIN / window.innerWidth
30
+ : (window.innerWidth - FAB_EDGE_MARGIN - FAB_SIZE_PX) / window.innerWidth;
31
+ return {
32
+ rightFraction: clamp(rightFraction, 0, 1),
33
+ bottomFraction: clamp(parsed.bottomFraction, 0, 1),
34
+ pinnedByUser: false,
35
+ };
36
+ }
37
+ // Unrecognized shape (corrupted value, a future format, hand-edited localStorage) — fall back
38
+ // to the default rather than risk propagating `NaN`/`undefined` into pixel math downstream.
39
+ return null;
40
+ }
41
+ catch {
42
+ return null;
43
+ }
44
+ }
45
+ function writePersisted(position, { storage, storageKey }) {
46
+ try {
47
+ storage.setItem(storageKey, JSON.stringify(position));
48
+ }
49
+ catch {
50
+ // Best-effort, same reasoning as `useSidebarRail`'s `writePersisted` — a user who cannot
51
+ // persist the position still gets a working, draggable FAB for the current tab.
52
+ }
53
+ }
54
+ /** A safe default before the first drag — bottom-right corner, matching the FAB's original
55
+ * hard-coded `right: 20px; bottom: 20px`. Both fractions are `0` ("flush against the edge"),
56
+ * not "off-screen at the corner" — `style` below clamps them up to `FAB_EDGE_MARGIN` regardless
57
+ * of viewport size. */
58
+ const DEFAULT_POSITION = { rightFraction: 0, bottomFraction: 0, pinnedByUser: false };
59
+ /**
60
+ * @param options.dockOpen When the assistant dock/sheet is open, an UNTOUCHED or migrated-legacy
61
+ * resting spot moves off it so it never sits over the dock's own composer/close controls by
62
+ * accident — this mirrors what the old hard-coded `.chat-fab-dock-open` rule did for the
63
+ * desktop-docked panel, generalized to the dragged position and to the mobile sheet's own
64
+ * (viewport-fraction) height. A spot the operator explicitly dragged onto the dock is left alone
65
+ * (see `pinnedByUser`'s own doc on `StoredFabPosition`) — this option only guards against an
66
+ * unintentional overlap, not against a deliberate one.
67
+ * @param options.avoidBottomPx Extra clearance to hold above the bottom edge while `dockOpen` —
68
+ * the caller passes the mobile sheet's current height here (0 at desktop, where the dock sits
69
+ * to the *side*, not below, so no bottom clearance is needed there — see `App.tsx`).
70
+ * @param options.avoidRightPx Extra clearance to hold left of the right edge while `dockOpen` —
71
+ * the caller passes the DESKTOP dock's current width here (0 in sheet mode, where the sheet
72
+ * spans the full width and `avoidBottomPx` is the axis that matters).
73
+ *
74
+ * This axis was missing until 2026-08-05, and its absence was a live defect rather than a
75
+ * theoretical gap: the FAB is `position: fixed` and offset from the RIGHT edge, and the desktop
76
+ * dock is also pinned to the right edge, so with the dock open the 56px FAB sat directly on top
77
+ * of the dock's own composer send button and swallowed its clicks. Playwright caught it as
78
+ * `<button class="chat-fab chat-fab-dock-open"> intercepts pointer events` while trying to send
79
+ * a message — i.e. the assistant could not be used with a mouse at the default FAB position.
80
+ *
81
+ * How it got lost is worth recording, because the code confidently said otherwise: the old
82
+ * `.chat-fab-dock-open { right: calc(380px + 20px) }` rule moved the FAB SIDEWAYS. When this
83
+ * hook replaced it, only the vertical axis was carried over, while the comment on
84
+ * `effectiveBottom` below claimed `avoidBottomPx` "generalizes" what that rule did — it does
85
+ * not, it generalizes a different axis, and desktop passes `avoidBottomPx: 0`, so on desktop
86
+ * nothing displaced the FAB at all. The `.chat-fab-dock-open` class is still applied by
87
+ * `ChatFab.tsx` and has had no CSS rule behind it since.
88
+ *
89
+ * Measured by the caller rather than hard-coded back to 380px, for the same
90
+ * resolution-independence reason this whole hook exists (see the module doc).
91
+ *
92
+ * @complexity O(1) per pointer event — no work scales with anything caller-controlled.
93
+ */
94
+ export function useFabPosition(options, _optional = {}) {
95
+ // Storage/DOM handles and CSS geometry are fixed for this mounted button; dock clearances stay live.
96
+ const environment = useRef(options).current;
97
+ const { viewport: window, pointerEvents: document, edgeMargin: FAB_EDGE_MARGIN, sizePx: FAB_SIZE_PX, dragThresholdPx: DRAG_THRESHOLD_PX } = environment;
98
+ const { dockOpen, avoidBottomPx } = options;
99
+ const avoidRightPx = options.avoidRightPx ?? 0;
100
+ const [persisted, setPersisted] = useState(() => readPersisted(environment) ?? DEFAULT_POSITION);
101
+ const [isDragging, setIsDragging] = useState(false);
102
+ /**
103
+ * All the mutable drag bookkeeping lives in refs, not state, and is read/written only inside
104
+ * the `pointermove`/`pointerup` handlers below.
105
+ *
106
+ * This is deliberate, not an optimization: `document.addEventListener("pointermove", handler)`
107
+ * binds to whatever `handler` closure existed at `pointerdown` time and is never re-subscribed
108
+ * mid-drag. If this logic instead branched on the REACT STATE `isDragging` — which is exactly
109
+ * what an earlier version of this hook did — every handler invoked after the first `pointermove`
110
+ * would still see the `isDragging=false` closure captured at `pointerdown`, because the state
111
+ * update that flips it to `true` doesn't re-create (and re-attach) the listener. Concretely,
112
+ * that made `handlePointerUp`'s `if (isDragging)` check always false, so a real drag's end
113
+ * position was silently never persisted. Refs have no such staleness — they are the same
114
+ * mutable cell on every read regardless of which render's closure is doing the reading.
115
+ */
116
+ const draggingRef = useRef(false);
117
+ const didDragRef = useRef(false);
118
+ const liveRef = useRef(null);
119
+ const dragStart = useRef(null);
120
+ /** Bumped to force a re-render on every pointermove and on resize — `liveRef`/`window.innerWidth`
121
+ * are read directly in `style`'s `useMemo` below, neither of which React tracks as a reactive
122
+ * dependency on its own, so without this counter in that hook's dependency array the memo
123
+ * would never invalidate and the FAB would visually freeze mid-drag. */
124
+ const [renderTick, forceRender] = useState(0);
125
+ // Re-clamp on resize/orientationchange — `rightFraction`/`bottomFraction` are already
126
+ // resolution-independent on their own axes, but this still forces a re-render so any consumer
127
+ // computing pixels from `window.innerWidth`/`innerHeight` (this hook included, in `style`
128
+ // below) recomputes rather than holding a value measured against the previous viewport.
129
+ useEffect(() => {
130
+ function onResize() {
131
+ forceRender((n) => n + 1);
132
+ }
133
+ window.addEventListener("resize", onResize);
134
+ window.addEventListener("orientationchange", onResize);
135
+ return () => {
136
+ window.removeEventListener("resize", onResize);
137
+ window.removeEventListener("orientationchange", onResize);
138
+ };
139
+ }, []);
140
+ // Stable identities (empty dep arrays) — see the `draggingRef` doc above for why these must
141
+ // never depend on state that would force a re-subscribe mid-gesture.
142
+ const handlePointerMove = useCallback((e) => {
143
+ const start = dragStart.current;
144
+ if (!start)
145
+ return;
146
+ const dx = e.clientX - start.startX;
147
+ const dy = e.clientY - start.startY;
148
+ if (!draggingRef.current && Math.hypot(dx, dy) > DRAG_THRESHOLD_PX) {
149
+ draggingRef.current = true;
150
+ didDragRef.current = true;
151
+ setIsDragging(true);
152
+ }
153
+ if (draggingRef.current) {
154
+ // Dragging moves the FAB's bottom-right-anchored box: moving the pointer right shrinks
155
+ // `right` (px from the right edge); moving it down shrinks `bottom`.
156
+ liveRef.current = { right: start.startRight - dx, bottom: start.startBottom - dy };
157
+ forceRender((n) => n + 1);
158
+ }
159
+ }, []);
160
+ const handlePointerUp = useCallback((e) => {
161
+ const start = dragStart.current;
162
+ if (!start)
163
+ return;
164
+ e.target.releasePointerCapture?.(start.pointerId);
165
+ document.removeEventListener("pointermove", handlePointerMove);
166
+ document.removeEventListener("pointerup", handlePointerUp);
167
+ document.removeEventListener("pointercancel", handlePointerUp);
168
+ const live = liveRef.current;
169
+ const wasDragging = draggingRef.current;
170
+ dragStart.current = null;
171
+ liveRef.current = null;
172
+ draggingRef.current = false;
173
+ if (live && wasDragging) {
174
+ // Persist exactly where it was dropped — no edge-snap (see file header). Clamped to
175
+ // `[0, 1]` here only to keep the *fraction* well-formed; the margin-aware on-screen clamp
176
+ // happens once, in `style` below, the same way it already did for the vertical axis alone —
177
+ // so a drop near an edge still ends up fully on-screen after conversion back to pixels, on
178
+ // this viewport and any other.
179
+ const next = {
180
+ rightFraction: clamp(live.right / window.innerWidth, 0, 1),
181
+ bottomFraction: clamp(live.bottom / window.innerHeight, 0, 1),
182
+ // A completed drag-and-drop is exactly the deliberate placement `pinnedByUser` exists to
183
+ // recognize — see its own doc for what this unlocks in `style` below.
184
+ pinnedByUser: true,
185
+ };
186
+ setPersisted(next);
187
+ writePersisted(next, environment);
188
+ }
189
+ setIsDragging(false);
190
+ // `didDragRef` is intentionally NOT cleared here — `consumeDragFlag` (called from `onClick`,
191
+ // which fires after this handler) is what clears it, once. Clearing it here would defeat the
192
+ // whole point: `click` always follows `pointerup`, so the flag has to survive until then.
193
+ }, [handlePointerMove]);
194
+ const onPointerDown = useCallback((e) => {
195
+ // Right-button/non-primary pointers don't start a drag — a right-click should still be
196
+ // able to open a context menu, not get captured as a failed drag gesture.
197
+ if (e.button !== 0)
198
+ return;
199
+ const rect = e.currentTarget.getBoundingClientRect();
200
+ dragStart.current = {
201
+ pointerId: e.pointerId,
202
+ startX: e.clientX,
203
+ startY: e.clientY,
204
+ startRight: window.innerWidth - rect.right,
205
+ startBottom: window.innerHeight - rect.bottom,
206
+ };
207
+ e.currentTarget.setPointerCapture(e.pointerId);
208
+ document.addEventListener("pointermove", handlePointerMove);
209
+ document.addEventListener("pointerup", handlePointerUp);
210
+ document.addEventListener("pointercancel", handlePointerUp);
211
+ }, [handlePointerMove, handlePointerUp]);
212
+ const consumeDragFlag = useCallback(() => {
213
+ const dragged = didDragRef.current;
214
+ didDragRef.current = false;
215
+ return dragged;
216
+ }, []);
217
+ // `renderTick`/`isDragging` are read only to force recomputation while dragging (their values
218
+ // are not otherwise used in the body — the live position comes from `liveRef`/`draggingRef`
219
+ // directly, per the staleness note above) — expected extra deps, not a lint miss.
220
+ // biome-ignore lint/correctness/useExhaustiveDependencies: renderTick/isDragging force recompute while dragging; not otherwise used in the body.
221
+ const style = useMemo(() => {
222
+ const live = liveRef.current;
223
+ if (draggingRef.current && live) {
224
+ // Clamped live so a fast drag can't throw the FAB fully off-screen mid-gesture, only to
225
+ // have it snap back on release — the same margin used for the resting position below.
226
+ return {
227
+ right: clamp(live.right, FAB_EDGE_MARGIN, window.innerWidth - FAB_EDGE_MARGIN - FAB_SIZE_PX),
228
+ bottom: clamp(live.bottom, FAB_EDGE_MARGIN, window.innerHeight - FAB_EDGE_MARGIN - FAB_SIZE_PX),
229
+ };
230
+ }
231
+ const rightPx = clamp(persisted.rightFraction * window.innerWidth, FAB_EDGE_MARGIN, window.innerWidth - FAB_EDGE_MARGIN - FAB_SIZE_PX);
232
+ const bottomPx = clamp(persisted.bottomFraction * window.innerHeight, FAB_EDGE_MARGIN, window.innerHeight - FAB_EDGE_MARGIN - FAB_SIZE_PX);
233
+ // While the dock/sheet is open, hold clear of it — but only at the UNTOUCHED default spot or a
234
+ // migrated legacy one (`!persisted.pinnedByUser`); an operator who explicitly dragged the FAB
235
+ // somewhere, including onto the open dock, gets that placement respected instead of vetoed (see
236
+ // `pinnedByUser`'s own doc for the 2026-08-05 bug this distinction fixes: the avoidance below
237
+ // was pushing ANY drop inside the dock's width back out onto the page on every release, because
238
+ // it did not yet know a drop was deliberate).
239
+ //
240
+ // TWO axes, because the dock occupies a different edge depending on mode, and getting this
241
+ // wrong is not cosmetic — a FAB overlapping the composer's send button eats the click:
242
+ // - sheet mode (mobile): the sheet spans the full width along the BOTTOM, so `avoidBottomPx`
243
+ // (its measured height) is the axis that matters.
244
+ // - docked mode (desktop): the dock is pinned to the RIGHT edge, which is the same edge the
245
+ // FAB offsets from, so `avoidRightPx` (its measured width) is the axis that matters.
246
+ // The old `.chat-fab-dock-open { right: calc(380px + 20px) }` rule covered only the second of
247
+ // those, and only at one hard-coded width; an earlier version of this comment claimed
248
+ // `avoidBottomPx` generalized it, which was wrong — different axis. See the `avoidRightPx`
249
+ // param doc for what that cost.
250
+ //
251
+ // Re-clamped against the viewport, and that clamp is load-bearing rather than defensive.
252
+ // `avoidBottomPx` is a raw PIXEL measurement of another element (`App.tsx` measures the sheet),
253
+ // so it is the one input here that is not viewport-relative by construction — everything else
254
+ // on this path is a 0-1 fraction re-derived against the current window. A sheet taller than
255
+ // `innerHeight - FAB_EDGE_MARGIN - FAB_SIZE_PX` therefore pushes the FAB clean off the top
256
+ // edge, where it is unreachable and looks simply missing. Reported by the operator as "hidden
257
+ // and not shown at the bottom" and identified by reading this expression — the specific
258
+ // viewport it reproduces at has NOT been pinned down, so treat the clamp as the fix and this
259
+ // sentence as the limit of what was confirmed. Bounded below by the margin too, so an
260
+ // unmeasured sheet reporting 0 cannot pin the FAB flush to the edge either.
261
+ const avoidDock = dockOpen && !persisted.pinnedByUser;
262
+ const effectiveBottom = clamp(avoidDock ? Math.max(bottomPx, avoidBottomPx + FAB_EDGE_MARGIN) : bottomPx, FAB_EDGE_MARGIN, Math.max(FAB_EDGE_MARGIN, window.innerHeight - FAB_EDGE_MARGIN - FAB_SIZE_PX));
263
+ // Same shape, same clamp, and the clamp is load-bearing for the same reason: `avoidRightPx` is
264
+ // a raw pixel measurement of another element, so a dock wider than the viewport allows would
265
+ // otherwise push the FAB off the left edge where it looks simply missing.
266
+ const effectiveRight = clamp(avoidDock ? Math.max(rightPx, avoidRightPx + FAB_EDGE_MARGIN) : rightPx, FAB_EDGE_MARGIN, Math.max(FAB_EDGE_MARGIN, window.innerWidth - FAB_EDGE_MARGIN - FAB_SIZE_PX));
267
+ return { right: effectiveRight, bottom: effectiveBottom };
268
+ }, [persisted, dockOpen, avoidBottomPx, avoidRightPx, renderTick, isDragging]);
269
+ return { style, onPointerDown, consumeDragFlag, isDragging };
270
+ }
271
+ //# sourceMappingURL=fab-position.js.map
@@ -1,6 +1,7 @@
1
+ import { type UserTextRedactionOptions } from '../../core/user-text-redaction.js';
1
2
  import type { ChatAttachment, ChatMessage } from '../../core/index.js';
2
3
  import type { ChatTransport, RunContext } from '../../core/index.js';
3
- export interface UseConversationOptions {
4
+ export interface UseConversationOptions extends UserTextRedactionOptions {
4
5
  transport: ChatTransport;
5
6
  initialMessages?: ChatMessage[];
6
7
  conversationId?: string | null;
@@ -9,6 +10,8 @@ export interface UseConversationOptions {
9
10
  createMessageId?: () => string;
10
11
  }
11
12
  export interface SendMessageOptions {
13
+ /** Non-secret signal from a composer that already sanitized the content before this boundary. */
14
+ secretRedaction?: ChatMessage['secretRedaction'];
12
15
  attachments?: ChatAttachment[];
13
16
  context?: RunContext;
14
17
  agentId?: string;
@@ -1,3 +1,4 @@
1
+ import { redactUserHistory, redactUserMessage } from '../../core/user-text-redaction.js';
1
2
  import { preserveAssistantContent, mergeRunEvents } from '../../core/durable-projection.js';
2
3
  /**
3
4
  * @module useConversation
@@ -23,7 +24,7 @@ function defaultCreateMessageId() {
23
24
  }
24
25
  export function useConversation(options) {
25
26
  const { transport, conversationId = null, agentId, createMessageId = defaultCreateMessageId } = options;
26
- const [messages, setMessagesState] = useState(options.initialMessages ?? []);
27
+ const [messages, setMessagesState] = useState(() => redactUserHistory({ history: options.initialMessages ?? [] }, { ...(options.redactUserText ? { redactUserText: options.redactUserText } : {}) }));
27
28
  const [scrollIntent, setScrollIntent] = useState(false);
28
29
  const run = useRunStream({ transport });
29
30
  // The assistant message id the currently-active run is writing into.
@@ -134,13 +135,14 @@ export function useConversation(options) {
134
135
  // eslint-disable-next-line react-hooks/exhaustive-deps
135
136
  }, [run.events, run.status, run.runId]);
136
137
  const sendMessage = useCallback(async (content, sendOptions = {}) => {
137
- const userMessage = {
138
- id: createMessageId(),
139
- role: 'user',
140
- content,
141
- createdAt: Date.now(),
142
- ...(sendOptions.attachments ? { attachments: sendOptions.attachments } : {}),
143
- };
138
+ const userMessage = redactUserMessage({ message: {
139
+ id: createMessageId(),
140
+ role: 'user',
141
+ content,
142
+ createdAt: Date.now(),
143
+ ...(sendOptions.secretRedaction ? { secretRedaction: sendOptions.secretRedaction } : {}),
144
+ ...(sendOptions.attachments ? { attachments: sendOptions.attachments } : {}),
145
+ } }, options).message;
144
146
  const resolvedAgentId = sendOptions.agentId ?? agentId;
145
147
  const assistantMessage = {
146
148
  id: createMessageId(),
@@ -152,7 +154,7 @@ export function useConversation(options) {
152
154
  startedAt: Date.now(),
153
155
  };
154
156
  activeAssistantIdRef.current = assistantMessage.id;
155
- const history = [...messagesRef.current, userMessage];
157
+ const history = redactUserHistory({ history: [...messagesRef.current, userMessage] }, options);
156
158
  // Functional, never `[...history, assistantMessage]`: `messagesRef` is the last RENDER's
157
159
  // snapshot, and a host that sends from an effect (`useChatPane`'s queued-prompt flush) runs in
158
160
  // the same commit as the reconciliation effect that just marked the previous turn terminal.
@@ -169,7 +171,7 @@ export function useConversation(options) {
169
171
  ...(sendOptions.attachments !== undefined ? { attachments: sendOptions.attachments } : {}),
170
172
  ...(sendOptions.context !== undefined ? { context: sendOptions.context } : {}),
171
173
  });
172
- }, [agentId, conversationId, createMessageId, run]);
174
+ }, [agentId, conversationId, createMessageId, run, options.redactUserText, options.onSecretRedacted]);
173
175
  const retry = useCallback(async (assistantMessageId) => {
174
176
  const idx = messagesRef.current.findIndex((m) => m.id === assistantMessageId);
175
177
  if (idx <= 0)
@@ -177,7 +179,7 @@ export function useConversation(options) {
177
179
  const priorUser = messagesRef.current[idx - 1];
178
180
  if (!priorUser || priorUser.role !== 'user')
179
181
  return;
180
- const history = messagesRef.current.slice(0, idx);
182
+ const history = redactUserHistory({ history: messagesRef.current.slice(0, idx) }, options);
181
183
  const resetAssistant = { ...messagesRef.current[idx], content: '', events: [], runStatus: 'queued' };
182
184
  activeAssistantIdRef.current = resetAssistant.id;
183
185
  // Functional for the same reason as `sendMessage`'s: keep a just-queued terminal update.
@@ -200,7 +202,7 @@ export function useConversation(options) {
200
202
  conversationId,
201
203
  ...(priorUser.attachments !== undefined ? { attachments: priorUser.attachments } : {}),
202
204
  });
203
- }, [agentId, conversationId, run]);
205
+ }, [agentId, conversationId, run, options.redactUserText, options.onSecretRedacted]);
204
206
  const cancel = useCallback(() => run.cancel(), [run]);
205
207
  const acknowledgeScroll = useCallback(() => setScrollIntent(false), []);
206
208
  const suppressScroll = useCallback(() => setScrollIntent(false), []);
@@ -0,0 +1,11 @@
1
+ import { type SecretRedactionSignal, type UserTextRedactionOptions } from '../../core/user-text-redaction.js';
2
+ /** Shared notice state for composer and question answers. Effects see only a count, never text. */
3
+ export declare function useUserTextGuard(options: UserTextRedactionOptions, _optional?: Record<string, never>): {
4
+ secretRedacted: boolean;
5
+ redact: ({ text }: {
6
+ text: string;
7
+ }, _options?: {}) => import("../../core/user-text-redaction.js").UserTextRedaction;
8
+ redactText: (text: string) => string;
9
+ notifyRedaction: (signal: SecretRedactionSignal) => void;
10
+ };
11
+ //# sourceMappingURL=useUserTextGuard.d.ts.map
@@ -0,0 +1,19 @@
1
+ import { useCallback, useState } from 'react';
2
+ import { redactUserText } from '../../core/user-text-redaction.js';
3
+ /** Shared notice state for composer and question answers. Effects see only a count, never text. */
4
+ export function useUserTextGuard(options, _optional = {}) {
5
+ const [secretRedacted, setSecretRedacted] = useState(false);
6
+ const notifyRedaction = useCallback((signal) => {
7
+ setSecretRedacted(true);
8
+ options.onSecretRedacted?.(signal);
9
+ }, [options.onSecretRedacted]);
10
+ const redact = useCallback(({ text }, _options = {}) => {
11
+ const result = (options.redactUserText ?? redactUserText)({ text }, {});
12
+ if (result.secretRedacted)
13
+ notifyRedaction({ secretRedacted: true, count: result.count });
14
+ return result;
15
+ }, [options.redactUserText, notifyRedaction]);
16
+ const redactText = useCallback((text) => redact({ text }, {}).text, [redact]);
17
+ return { secretRedacted, redact, redactText, notifyRedaction };
18
+ }
19
+ //# sourceMappingURL=useUserTextGuard.js.map
@@ -1,25 +1,7 @@
1
1
  /**
2
- * @jini-ai/chat-react — headless hooks + presentational components + slots for
3
- * a chat/artifact frontend, built on `@jini-ai/chat/core`'s framework-free
4
- * vocabulary. See ADS-memory/reports/jini-port/recon/r4b-webui-design.md §1/§2/§4 for the
5
- * spec this package implements, and archived provenance ledger for provenance.
6
- *
7
- * This barrel is filled in incrementally as each layer lands (hooks first,
8
- * then presentational components, then the `<JiniChatProvider>` composition
9
- * root) — see archived provenance ledger's "Status" section for what's shipped so far.
10
- *
11
- * ## Every export here is explicit. Do not reintroduce `export *`.
12
- *
13
- * This file used 15 `export * from` statements until 2026-08-05. That made the package's public
14
- * API *implicit*: whatever any of those modules happened to export was public, so renaming an
15
- * internal helper shipped a breaking change nobody reviewed, and no one could read this file and
16
- * say what the package exports. It is also the likeliest reason consumers reach past this barrel
17
- * into `@jini-ai/chat/core` deep paths — a grab-bag invites bypassing.
18
- *
19
- * The expansion was mechanical and provably surface-preserving: 193 symbols before and after,
20
- * identical in name AND in value-vs-type kind. `features/chat-pane/index.ts` is the style model.
21
- *
22
- * When adding an export, add the name here deliberately. That edit IS the API review.
2
+ * @jini-ai/chat/react — headless hooks, presentational components and slots over framework-free
3
+ * chat/core vocabulary. Every export is explicit: adding or renaming a name here is a public API
4
+ * review, so internal module exports cannot silently become public. Do not use export *.
23
5
  */
24
6
  export type { ChatTransport, FeedbackChange, OnFeedback, ReattachRunOptions, RunContext, RunHandlers, StartRunInput, } from '../core/index.js';
25
7
  export { RendererRegistry, } from './artifact-types.js';
@@ -165,4 +147,13 @@ export type { FrontendSessionBridge, FrontendSessionBridgeArgs, FrontendSessionB
165
147
  export { createDomPageDriver, currentAgentPage, type DomPageDriverOptions } from '@jini-ai/agentic/dom';
166
148
  export { createBrowserComposerHistoryStorage } from './hooks/composer-history-storage.js';
167
149
  export type { ComposerHistoryStoragePort } from '../core/composer-history.js';
150
+ export { SECRET_REDACTED_NOTICE, redactUserText } from '../core/user-text-redaction.js';
151
+ export type { UserTextRedactionOptions } from '../core/user-text-redaction.js';
152
+ export { useUserTextGuard } from './hooks/useUserTextGuard.js';
153
+ export { cacheAttachmentPreviewSource } from './hooks/attachment-preview-cache.js';
154
+ export { readCachedAttachmentBatchId, writeCachedAttachmentBatchId } from './hooks/composer-draft-cache.js';
155
+ export { createAssistantChatsClient } from "./hooks/assistant-chats.js";
156
+ export type { AssistantChatsPort, AssistantChatsClientPorts, AssistantChatsClient, UseAssistantChats } from "./hooks/assistant-chats-contracts.js";
157
+ export { useFabPosition } from "./hooks/fab-position.js";
158
+ export type { FabPositionInput, FabPositionResult } from "./hooks/fab-position.js";
168
159
  //# sourceMappingURL=index.d.ts.map
@@ -122,4 +122,11 @@ export { JiniChatProvider, useJiniChatSlots, useOnFeedback } from './components/
122
122
  export { createFrontendSessionBridge } from './agent-bridge/frontend-session-bridge.js';
123
123
  export { createDomPageDriver, currentAgentPage } from '@jini-ai/agentic/dom';
124
124
  export { createBrowserComposerHistoryStorage } from './hooks/composer-history-storage.js';
125
+ // ChatPane consumers share the same redaction and draft/preview state owners.
126
+ export { SECRET_REDACTED_NOTICE, redactUserText } from '../core/user-text-redaction.js';
127
+ export { useUserTextGuard } from './hooks/useUserTextGuard.js';
128
+ export { cacheAttachmentPreviewSource } from './hooks/attachment-preview-cache.js';
129
+ export { readCachedAttachmentBatchId, writeCachedAttachmentBatchId } from './hooks/composer-draft-cache.js';
130
+ export { createAssistantChatsClient } from "./hooks/assistant-chats.js";
131
+ export { useFabPosition } from "./hooks/fab-position.js";
125
132
  //# sourceMappingURL=index.js.map
@@ -0,0 +1,25 @@
1
+ import type { AcceptedRunRequest, DurableRun, DurableRunStore } from "../../core/durable-runs/ports.js";
2
+ export interface RunAcceptancePorts {
3
+ readonly store: DurableRunStore;
4
+ /** Sanitize the host envelope before it is durably accepted. */
5
+ readonly sanitizeContextRef: (required: {
6
+ contextRef: string;
7
+ }, optional: {}) => string;
8
+ readonly now: () => number;
9
+ readonly mintRunId: () => string;
10
+ readonly launch: (required: {
11
+ run: DurableRun;
12
+ request: AcceptedRunRequest;
13
+ }, optional: {}) => Promise<void>;
14
+ readonly attach: (required: DurableRun, optional: {}) => void;
15
+ }
16
+ export declare function createRunAcceptance(ports: RunAcceptancePorts, _optional?: {}): {
17
+ accept(required: {
18
+ principalId: string;
19
+ workspaceId: string;
20
+ conversationId: string;
21
+ messageId: string;
22
+ request: AcceptedRunRequest;
23
+ }, _options?: {}): Promise<DurableRun | null>;
24
+ };
25
+ //# sourceMappingURL=accept.d.ts.map