@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.
- package/CHANGELOG.md +4 -0
- package/dist/core/ag-ui/entry.d.ts +33 -0
- package/dist/core/ag-ui/entry.js +80 -0
- package/dist/core/agentic/chat-capabilities.d.ts +2 -4
- package/dist/core/agentic/index.d.ts +4 -17
- package/dist/core/agentic/index.js +4 -17
- package/dist/core/assistant-client/a2ui-action-poster.d.ts +46 -0
- package/dist/core/assistant-client/a2ui-action-poster.js +113 -0
- package/dist/core/assistant-client/ag-ui-transport.d.ts +68 -0
- package/dist/core/assistant-client/ag-ui-transport.js +147 -0
- package/dist/core/assistant-client/attachment-liveness.d.ts +37 -0
- package/dist/core/assistant-client/attachment-liveness.js +93 -0
- package/dist/core/assistant-client/durable-run-subscription.d.ts +39 -0
- package/dist/core/assistant-client/durable-run-subscription.js +96 -0
- package/dist/core/assistant-client/entry.d.ts +13 -0
- package/dist/core/assistant-client/entry.js +7 -0
- package/dist/core/assistant-client/messages.d.ts +73 -0
- package/dist/core/assistant-client/messages.js +68 -0
- package/dist/core/assistant-client/transport.d.ts +164 -0
- package/dist/core/assistant-client/transport.js +734 -0
- package/dist/core/browser/page-actions.d.ts +121 -0
- package/dist/core/browser/page-actions.js +113 -0
- package/dist/core/durable-runs/continuation.d.ts +18 -0
- package/dist/core/durable-runs/continuation.js +61 -0
- package/dist/core/durable-runs/ledger-contracts.d.ts +53 -0
- package/dist/core/durable-runs/ledger-contracts.js +2 -0
- package/dist/core/durable-runs/policies.d.ts +6 -0
- package/dist/core/durable-runs/policies.js +6 -0
- package/dist/core/durable-runs/ports.d.ts +123 -0
- package/dist/core/durable-runs/ports.js +2 -0
- package/dist/core/durable-runs/run-contracts.d.ts +17 -0
- package/dist/core/durable-runs/run-contracts.js +2 -0
- package/dist/core/durable-runs/run-lock.d.ts +4 -0
- package/dist/core/durable-runs/run-lock.js +4 -0
- package/dist/core/index.d.ts +6 -19
- package/dist/core/index.js +6 -19
- package/dist/core/messages.d.ts +2 -0
- package/dist/core/question-form/format.d.ts +0 -9
- package/dist/core/question-form/format.js +16 -2
- package/dist/core/question-form/normalize.js +1 -0
- package/dist/core/question-form/secret-answers.d.ts +38 -0
- package/dist/core/question-form/secret-answers.js +45 -0
- package/dist/core/question-form/types.d.ts +2 -0
- package/dist/core/question-form.d.ts +1 -0
- package/dist/core/question-form.js +1 -0
- package/dist/core/run-events/entry.d.ts +208 -16
- package/dist/core/run-events/entry.js +273 -39
- package/dist/core/transcript.d.ts +3 -2
- package/dist/core/transcript.js +19 -3
- package/dist/core/user-text-redaction.d.ts +56 -0
- package/dist/core/user-text-redaction.js +89 -0
- package/dist/embed-widget/browser/page-actions.d.ts +1 -120
- package/dist/embed-widget/browser/page-actions.js +2 -112
- package/dist/embed-widget/react/embed.d.ts +1 -1
- package/dist/embed-widget/react/embed.js +1 -1
- package/dist/react/artifact-types.d.ts +9 -15
- package/dist/react/components/QuestionForm.d.ts +2 -1
- package/dist/react/components/QuestionForm.js +18 -12
- package/dist/react/components/QuestionsPanel.d.ts +3 -2
- package/dist/react/components/QuestionsPanel.js +2 -2
- package/dist/react/features/chat-pane/agent-tools.d.ts +1 -1
- package/dist/react/features/chat-pane/agent-tools.js +0 -1
- package/dist/react/features/chat-pane/components/AgentRuntimePicker.js +5 -2
- package/dist/react/features/chat-pane/components/ChatPane.d.ts +1 -1
- package/dist/react/features/chat-pane/components/ChatPane.js +5 -2
- package/dist/react/features/chat-pane/create-typed-answer-poster.d.ts +2 -1
- package/dist/react/features/chat-pane/create-typed-answer-poster.js +33 -1
- package/dist/react/features/chat-pane/hooks/useAgentRuntimePicker.hooks.js +3 -2
- package/dist/react/features/chat-pane/hooks/useChatPane.hooks.d.ts +4 -1
- package/dist/react/features/chat-pane/hooks/useChatPane.hooks.js +29 -17
- package/dist/react/features/chat-pane/model-options.d.ts +10 -0
- package/dist/react/features/chat-pane/model-options.js +51 -0
- package/dist/react/features/chat-pane/rules.d.ts +3 -2
- package/dist/react/features/chat-pane/rules.js +12 -4
- package/dist/react/features/chat-pane/types.d.ts +25 -1
- package/dist/react/hooks/assistant-chats-contracts.d.ts +164 -0
- package/dist/react/hooks/assistant-chats-contracts.js +2 -0
- package/dist/react/hooks/assistant-chats.d.ts +7 -0
- package/dist/react/hooks/assistant-chats.js +960 -0
- package/dist/react/hooks/attachment-preview-cache.d.ts +1 -1
- package/dist/react/hooks/attachment-preview-cache.js +1 -1
- package/dist/react/hooks/composer-draft-cache.d.ts +2 -2
- package/dist/react/hooks/composer-draft-cache.js +2 -2
- package/dist/react/hooks/fab-position.d.ts +90 -0
- package/dist/react/hooks/fab-position.js +271 -0
- package/dist/react/hooks/useConversation.d.ts +4 -1
- package/dist/react/hooks/useConversation.js +14 -12
- package/dist/react/hooks/useUserTextGuard.d.ts +11 -0
- package/dist/react/hooks/useUserTextGuard.js +19 -0
- package/dist/react/index.d.ts +12 -21
- package/dist/react/index.js +7 -0
- package/dist/server/durable-runs/accept.d.ts +25 -0
- package/dist/server/durable-runs/accept.js +21 -0
- package/dist/server/durable-runs/entry.d.ts +5 -0
- package/dist/server/durable-runs/entry.js +4 -0
- package/dist/server/durable-runs/recover.d.ts +5 -0
- package/dist/server/durable-runs/recover.js +147 -0
- package/dist/server/run-finalizer/ports.d.ts +2 -16
- package/dist/store/legacy/sqlite/messages.d.ts +24 -18
- package/dist/store/legacy/sqlite/messages.js +12 -2
- package/dist/store/pglite/index.d.ts +2 -1
- package/dist/store/postgres/index.d.ts +2 -1
- package/dist/store/runtime/chat-expiry-sweep.d.ts +40 -0
- package/dist/store/runtime/chat-expiry-sweep.js +70 -0
- package/dist/store/runtime/chat-history-store.d.ts +20 -0
- package/dist/store/runtime/chat-history-store.js +50 -0
- package/dist/store/runtime/durable-run-store.d.ts +21 -0
- package/dist/store/runtime/durable-run-store.js +342 -0
- package/dist/store/runtime/entry.d.ts +7 -0
- package/dist/store/runtime/entry.js +6 -0
- package/dist/store/runtime/kernel.d.ts +29 -0
- package/dist/store/runtime/kernel.js +2 -0
- package/dist/store/runtime/run-guarded-chat-store.d.ts +10 -0
- package/dist/store/runtime/run-guarded-chat-store.js +42 -0
- package/dist/store/runtime/run-ledger.d.ts +15 -0
- package/dist/store/runtime/run-ledger.js +206 -0
- package/dist/store/sql/boundary.d.ts +6 -0
- package/dist/store/sql/boundary.js +7 -1
- package/dist/store/sql/store.d.ts +3 -2
- package/dist/store/sql/store.js +9 -2
- package/dist/store/sqlite/index.d.ts +3 -2
- 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
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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
|
package/dist/react/index.d.ts
CHANGED
|
@@ -1,25 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @jini-ai/chat
|
|
3
|
-
*
|
|
4
|
-
*
|
|
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
|
package/dist/react/index.js
CHANGED
|
@@ -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
|