@jini-ai/chat 0.5.3 → 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 +11 -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/composer-history.d.ts +54 -0
- package/dist/core/composer-history.js +57 -0
- package/dist/core/durable-projection.d.ts +28 -0
- package/dist/core/durable-projection.js +71 -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 +9 -19
- package/dist/core/index.js +8 -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/transport.d.ts +4 -0
- 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/Composer.js +6 -0
- package/dist/react/components/MessageRow.js +24 -11
- 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 +9 -4
- 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 +7 -1
- package/dist/react/features/chat-pane/hooks/useChatPane.hooks.js +38 -23
- 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 +30 -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/composer-history-storage.d.ts +8 -0
- package/dist/react/hooks/composer-history-storage.js +40 -0
- package/dist/react/hooks/fab-position.d.ts +90 -0
- package/dist/react/hooks/fab-position.js +271 -0
- package/dist/react/hooks/useComposer.d.ts +10 -1
- package/dist/react/hooks/useComposer.js +14 -2
- package/dist/react/hooks/useComposerHistory.hooks.d.ts +15 -0
- package/dist/react/hooks/useComposerHistory.hooks.js +90 -0
- package/dist/react/hooks/useComposerHistoryKeys.hooks.d.ts +9 -0
- package/dist/react/hooks/useComposerHistoryKeys.hooks.js +90 -0
- package/dist/react/hooks/useConversation.d.ts +4 -1
- package/dist/react/hooks/useConversation.js +20 -14
- package/dist/react/hooks/useRunStream.js +12 -2
- package/dist/react/hooks/useUserTextGuard.d.ts +11 -0
- package/dist/react/hooks/useUserTextGuard.js +19 -0
- package/dist/react/index.d.ts +14 -21
- package/dist/react/index.js +8 -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,8 @@
|
|
|
1
|
+
import { type ComposerHistoryStoragePort } from '../../core/composer-history.js';
|
|
2
|
+
/** Browser default; private/embedded hosts may replace this port without changing recall. */
|
|
3
|
+
export declare function createBrowserComposerHistoryStorage(_required: {}, { storage, prefix }?: {
|
|
4
|
+
storage?: Pick<Storage, 'getItem' | 'setItem'>;
|
|
5
|
+
prefix?: string;
|
|
6
|
+
}): ComposerHistoryStoragePort;
|
|
7
|
+
export declare const browserComposerHistoryStorage: ComposerHistoryStoragePort;
|
|
8
|
+
//# sourceMappingURL=composer-history-storage.d.ts.map
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { normalizeComposerHistory } from '../../core/composer-history.js';
|
|
2
|
+
/** Browser default; private/embedded hosts may replace this port without changing recall. */
|
|
3
|
+
export function createBrowserComposerHistoryStorage(_required, { storage, prefix = 'jini:composer-history:v1:' } = {}) {
|
|
4
|
+
const memory = new Map();
|
|
5
|
+
function resolveStorage() {
|
|
6
|
+
// Accessing localStorage itself can throw (sandboxed embeds/private browsing).
|
|
7
|
+
return storage ?? globalThis.localStorage;
|
|
8
|
+
}
|
|
9
|
+
return {
|
|
10
|
+
async read({ scope }, _optional = {}) {
|
|
11
|
+
try {
|
|
12
|
+
const raw = resolveStorage()?.getItem(prefix + encodeURIComponent(scope));
|
|
13
|
+
if (raw) {
|
|
14
|
+
const parsed = JSON.parse(raw);
|
|
15
|
+
if (Array.isArray(parsed)) {
|
|
16
|
+
const entries = normalizeComposerHistory({ entries: parsed.filter((value) => typeof value === 'string') });
|
|
17
|
+
memory.set(scope, entries);
|
|
18
|
+
return entries;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
// Storage denial/corruption must not disable draft editing or in-memory recall.
|
|
24
|
+
}
|
|
25
|
+
return memory.get(scope) ?? [];
|
|
26
|
+
},
|
|
27
|
+
async write({ scope, entries }, _optional = {}) {
|
|
28
|
+
const normalized = normalizeComposerHistory({ entries });
|
|
29
|
+
memory.set(scope, normalized);
|
|
30
|
+
try {
|
|
31
|
+
resolveStorage()?.setItem(prefix + encodeURIComponent(scope), JSON.stringify(normalized));
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
// Quota/denial falls back to the same scoped in-memory list.
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
export const browserComposerHistoryStorage = createBrowserComposerHistoryStorage({});
|
|
40
|
+
//# sourceMappingURL=composer-history-storage.js.map
|
|
@@ -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,4 +1,6 @@
|
|
|
1
|
-
import type { ChatAttachment } from '../../core/index.js';
|
|
1
|
+
import type { ChatAttachment, ChatMessage } from '../../core/index.js';
|
|
2
|
+
import type { ComposerHistoryStoragePort } from '../../core/composer-history.js';
|
|
3
|
+
import { type ComposerHistoryController } from './useComposerHistory.hooks.js';
|
|
2
4
|
import type { AgentSelection, ComposerSlots, MentionResult, ProjectContextValue } from '../slots.js';
|
|
3
5
|
export interface ComposerDraftPersistence {
|
|
4
6
|
read: () => string | null;
|
|
@@ -6,6 +8,11 @@ export interface ComposerDraftPersistence {
|
|
|
6
8
|
}
|
|
7
9
|
export interface UseComposerOptions {
|
|
8
10
|
initialDraft?: string;
|
|
11
|
+
/** This principal's current transcript; only user-role text participates in recall. */
|
|
12
|
+
historyMessages?: readonly ChatMessage[];
|
|
13
|
+
/** Scopes recent prompts across conversations. Hosts with multiple users must supply an id. */
|
|
14
|
+
historyScope?: string;
|
|
15
|
+
historyStorage?: ComposerHistoryStoragePort;
|
|
9
16
|
initialAgent?: AgentSelection;
|
|
10
17
|
project?: ProjectContextValue;
|
|
11
18
|
composerSlots?: ComposerSlots;
|
|
@@ -42,6 +49,8 @@ export interface MentionPopoverState {
|
|
|
42
49
|
}
|
|
43
50
|
export interface UseComposerResult {
|
|
44
51
|
draft: string;
|
|
52
|
+
/** Optional for compatibility with host-created composer controllers. */
|
|
53
|
+
history?: ComposerHistoryController;
|
|
45
54
|
setDraft: (next: string) => void;
|
|
46
55
|
attachments: ChatAttachment[];
|
|
47
56
|
/** Uploads via `project.uploadFiles` (when supplied) and stages the results; no-ops (with a rejected promise) when no upload port is wired. */
|
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
* the wrong thread, so they are cleared instead.
|
|
29
29
|
*/
|
|
30
30
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
31
|
+
import { useComposerHistory } from './useComposerHistory.hooks.js';
|
|
31
32
|
import { readCachedAttachments, readCachedDraft, writeCachedAttachments, writeCachedDraft, } from './composer-draft-cache.js';
|
|
32
33
|
import { cacheAttachmentPreviewSource } from './attachment-preview-cache.js';
|
|
33
34
|
const EMPTY_MENTION = { open: false, query: '', results: [] };
|
|
@@ -37,11 +38,21 @@ export function useComposer(options = {}) {
|
|
|
37
38
|
const [attachments, setAttachments] = useState([]);
|
|
38
39
|
const [agent, setAgent] = useState(options.initialAgent);
|
|
39
40
|
const [mention, setMention] = useState(EMPTY_MENTION);
|
|
40
|
-
const
|
|
41
|
+
const commitDraft = useCallback((next) => {
|
|
41
42
|
setDraftState(next);
|
|
42
43
|
persistence?.write(next);
|
|
43
44
|
writeCachedDraft({ conversationId: conversationId, draft: next });
|
|
44
45
|
}, [persistence, conversationId]);
|
|
46
|
+
const history = useComposerHistory({ setRecalledDraft: setDraftState }, {
|
|
47
|
+
...(options.historyMessages === undefined ? {} : { messages: options.historyMessages }),
|
|
48
|
+
...(options.historyScope === undefined ? {} : { scope: options.historyScope }),
|
|
49
|
+
...(options.historyStorage === undefined ? {} : { storage: options.historyStorage }),
|
|
50
|
+
...(conversationId === undefined ? {} : { conversationId }),
|
|
51
|
+
});
|
|
52
|
+
const setDraft = useCallback((next) => {
|
|
53
|
+
history.edit({});
|
|
54
|
+
commitDraft(next);
|
|
55
|
+
}, [commitDraft, history.edit]);
|
|
45
56
|
// Lets the conversation-change effect below read the live draft without taking `draft` as a
|
|
46
57
|
// dependency, which would re-run it on every keystroke.
|
|
47
58
|
const draftRef = useRef(draft);
|
|
@@ -195,6 +206,7 @@ export function useComposer(options = {}) {
|
|
|
195
206
|
const canSubmit = draft.trim().length > 0 || attachments.length > 0;
|
|
196
207
|
return useMemo(() => ({
|
|
197
208
|
draft,
|
|
209
|
+
history,
|
|
198
210
|
setDraft,
|
|
199
211
|
attachments,
|
|
200
212
|
addAttachments,
|
|
@@ -209,6 +221,6 @@ export function useComposer(options = {}) {
|
|
|
209
221
|
selectMention,
|
|
210
222
|
canSubmit,
|
|
211
223
|
reset,
|
|
212
|
-
}), [draft, setDraft, attachments, addAttachments, addAttachment, removeAttachment, clearAttachments, agent, mention, openMention, closeMention, selectMention, canSubmit, reset]);
|
|
224
|
+
}), [draft, history, setDraft, attachments, addAttachments, addAttachment, removeAttachment, clearAttachments, agent, mention, openMention, closeMention, selectMention, canSubmit, reset]);
|
|
213
225
|
}
|
|
214
226
|
//# sourceMappingURL=useComposer.js.map
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { ChatMessage } from '../../core/messages.js';
|
|
2
|
+
import { type ComposerHistoryKeyInput, type ComposerHistoryStoragePort } from '../../core/composer-history.js';
|
|
3
|
+
export interface ComposerHistoryController {
|
|
4
|
+
navigate: (input: ComposerHistoryKeyInput, optional?: {}) => boolean;
|
|
5
|
+
edit: (required: {}, optional?: {}) => void;
|
|
6
|
+
}
|
|
7
|
+
export declare function useComposerHistory({ setRecalledDraft }: {
|
|
8
|
+
setRecalledDraft: (text: string) => void;
|
|
9
|
+
}, { messages, scope, storage, conversationId }?: {
|
|
10
|
+
messages?: readonly ChatMessage[];
|
|
11
|
+
scope?: string;
|
|
12
|
+
storage?: ComposerHistoryStoragePort;
|
|
13
|
+
conversationId?: string | null;
|
|
14
|
+
}): ComposerHistoryController;
|
|
15
|
+
//# sourceMappingURL=useComposerHistory.hooks.d.ts.map
|