@djangocfg/ui-tools 2.1.444 → 2.1.446
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@djangocfg/ui-tools",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.446",
|
|
4
4
|
"description": "Heavy React tools with lazy loading - for Electron, Vite, CRA, Next.js apps",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ui-tools",
|
|
@@ -334,8 +334,8 @@
|
|
|
334
334
|
"test:watch": "vitest"
|
|
335
335
|
},
|
|
336
336
|
"peerDependencies": {
|
|
337
|
-
"@djangocfg/i18n": "^2.1.
|
|
338
|
-
"@djangocfg/ui-core": "^2.1.
|
|
337
|
+
"@djangocfg/i18n": "^2.1.446",
|
|
338
|
+
"@djangocfg/ui-core": "^2.1.446",
|
|
339
339
|
"consola": "^3.4.2",
|
|
340
340
|
"lodash-es": "^4.18.1",
|
|
341
341
|
"lucide-react": "^0.545.0",
|
|
@@ -418,9 +418,9 @@
|
|
|
418
418
|
"@maplibre/maplibre-gl-geocoder": "^1.7.0"
|
|
419
419
|
},
|
|
420
420
|
"devDependencies": {
|
|
421
|
-
"@djangocfg/i18n": "^2.1.
|
|
422
|
-
"@djangocfg/typescript-config": "^2.1.
|
|
423
|
-
"@djangocfg/ui-core": "^2.1.
|
|
421
|
+
"@djangocfg/i18n": "^2.1.446",
|
|
422
|
+
"@djangocfg/typescript-config": "^2.1.446",
|
|
423
|
+
"@djangocfg/ui-core": "^2.1.446",
|
|
424
424
|
"@types/lodash-es": "^4.17.12",
|
|
425
425
|
"@types/mapbox__mapbox-gl-draw": "^1.4.8",
|
|
426
426
|
"@types/node": "^25.2.3",
|
|
@@ -622,7 +622,10 @@ export const Composer = forwardRef<HTMLDivElement, ComposerProps>(function Compo
|
|
|
622
622
|
{...dragProps}
|
|
623
623
|
onKeyDownCapture={handleSlashKeyDownCapture}
|
|
624
624
|
className={cn(
|
|
625
|
-
|
|
625
|
+
// No border-t: the composer's own rounded field separates it from
|
|
626
|
+
// the transcript — a hard rule above the input reads as chrome
|
|
627
|
+
// (ChatGPT/Claude float the composer the same way).
|
|
628
|
+
'relative bg-background/95',
|
|
626
629
|
sz.containerPadding,
|
|
627
630
|
ap.containerPadding,
|
|
628
631
|
className,
|
|
@@ -744,7 +747,9 @@ export const Composer = forwardRef<HTMLDivElement, ComposerProps>(function Compo
|
|
|
744
747
|
{...dragProps}
|
|
745
748
|
onKeyDownCapture={handleSlashKeyDownCapture}
|
|
746
749
|
className={cn(
|
|
747
|
-
|
|
750
|
+
// No border-t: the composer's own rounded field separates it from
|
|
751
|
+
// the transcript (same rationale as the inline layout above).
|
|
752
|
+
'relative bg-background/95',
|
|
748
753
|
sz.containerPadding,
|
|
749
754
|
ap.containerPadding,
|
|
750
755
|
className,
|
|
@@ -272,6 +272,16 @@ export function useChat(config: UseChatConfig): UseChatReturn {
|
|
|
272
272
|
let serverMessageId: string | null = null;
|
|
273
273
|
let chunkCount = 0;
|
|
274
274
|
let charsReceived = 0;
|
|
275
|
+
// Tracks whether the stream already delivered a TERMINAL event
|
|
276
|
+
// (`message_end` or `error`) via handleEvent. Guards the manual-finalize
|
|
277
|
+
// below. Without it, the finalize reads `stateRef.current.isStreaming` —
|
|
278
|
+
// a snapshot NOT updated synchronously after a `dispatch` — so right
|
|
279
|
+
// after a STREAM_ERROR it still sees `isStreaming: true` and fires a
|
|
280
|
+
// spurious STREAM_DONE, whose reducer clears `state.error`. Net effect:
|
|
281
|
+
// the error is set then wiped in the same tick and the ErrorBanner never
|
|
282
|
+
// paints. `sawTerminal` is a closure flag, so it reflects the truth the
|
|
283
|
+
// ref can't. See @dev/web2/chat/reports/COMPARE-new-vs-old-chat-error-handling.md (layer 3).
|
|
284
|
+
let sawTerminal = false;
|
|
275
285
|
const t0 = performance.now();
|
|
276
286
|
|
|
277
287
|
try {
|
|
@@ -311,8 +321,12 @@ export function useChat(config: UseChatConfig): UseChatReturn {
|
|
|
311
321
|
}
|
|
312
322
|
tokenBuffer.flush();
|
|
313
323
|
|
|
314
|
-
// If transport never emitted message_end
|
|
315
|
-
|
|
324
|
+
// If transport never emitted a terminal event (message_end / error),
|
|
325
|
+
// finalize manually. Gated on `sawTerminal` — NOT on
|
|
326
|
+
// `stateRef.current.isStreaming`, which is a stale snapshot right after
|
|
327
|
+
// a STREAM_ERROR dispatch and would otherwise fire a STREAM_DONE that
|
|
328
|
+
// wipes `state.error`.
|
|
329
|
+
if (!sawTerminal && stateRef.current.isStreaming) {
|
|
316
330
|
dispatch({ type: 'STREAM_DONE', id: targetId });
|
|
317
331
|
}
|
|
318
332
|
|
|
@@ -397,6 +411,7 @@ export function useChat(config: UseChatConfig): UseChatReturn {
|
|
|
397
411
|
log.tools.info('call_end', { toolId: ev.toolId, status: ev.status });
|
|
398
412
|
return;
|
|
399
413
|
case 'message_end':
|
|
414
|
+
sawTerminal = true;
|
|
400
415
|
tokenBuffer.flush();
|
|
401
416
|
dispatch({
|
|
402
417
|
type: 'STREAM_DONE',
|
|
@@ -440,6 +455,7 @@ export function useChat(config: UseChatConfig): UseChatReturn {
|
|
|
440
455
|
});
|
|
441
456
|
return;
|
|
442
457
|
case 'error':
|
|
458
|
+
sawTerminal = true;
|
|
443
459
|
tokenBuffer.flush();
|
|
444
460
|
dispatch({
|
|
445
461
|
type: 'STREAM_ERROR',
|
|
@@ -191,23 +191,84 @@ export const MessageList = forwardRef<MessageListHandle, MessageListProps>(funct
|
|
|
191
191
|
);
|
|
192
192
|
|
|
193
193
|
const itemRenderer = renderItem ?? defaultRenderItem;
|
|
194
|
-
// Jump to bottom on the first non-empty messages batch.
|
|
195
|
-
//
|
|
196
|
-
//
|
|
197
|
-
//
|
|
194
|
+
// Jump to bottom on the first non-empty messages batch. A SINGLE rAF +
|
|
195
|
+
// scrollToIndex is not enough when the list mounts with a long transcript
|
|
196
|
+
// (opening / switching a contact): virtuoso measures the dynamic bubble
|
|
197
|
+
// heights ASYNCHRONOUSLY, seconds after the data lands — until then the
|
|
198
|
+
// scroller reports scrollHeight === clientHeight (gap 0), so any "stop when
|
|
199
|
+
// the bottom sticks" exit fires on that transient zero and the late height
|
|
200
|
+
// growth is never corrected (live incident 2026-07-03: contact switch left
|
|
201
|
+
// a 112-message room at scrollTop 0 / blank until measured). The landing is
|
|
202
|
+
// therefore a PHASE, not a one-shot: ONE scrollToIndex seeds the tail
|
|
203
|
+
// render, then per-frame DIRECT scrollTop writes chase every height change
|
|
204
|
+
// for the whole landing window (~2.5s) — ending early ONLY on real user
|
|
205
|
+
// scroll intent, never on a momentarily-zero gap. Direct writes are
|
|
206
|
+
// ordinary scrolls to virtuoso — repeating scrollToIndex instead re-enters
|
|
207
|
+
// its initial-location seek and blanks the list (zero rows,
|
|
208
|
+
// visibility:hidden).
|
|
198
209
|
useEffect(() => {
|
|
199
210
|
if (didInitialScrollRef.current) return;
|
|
200
211
|
if (messages.length === 0) return;
|
|
201
212
|
didInitialScrollRef.current = true;
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
213
|
+
let raf = 0;
|
|
214
|
+
let framesLeft = 150;
|
|
215
|
+
let seeded = false;
|
|
216
|
+
let userTookOver = false;
|
|
217
|
+
const cancelOnUserScroll = () => {
|
|
218
|
+
userTookOver = true;
|
|
219
|
+
};
|
|
220
|
+
// Real scroll intent (wheel / touch / keys) ends the landing phase — the
|
|
221
|
+
// user reading history must never be yanked back down. Programmatic
|
|
222
|
+
// scrollTop writes fire none of these, so our own corrections don't
|
|
223
|
+
// self-cancel.
|
|
224
|
+
const scroller = scrollerRef.current;
|
|
225
|
+
const scrollerEl =
|
|
226
|
+
scroller != null && scroller !== window && scroller instanceof HTMLElement
|
|
227
|
+
? scroller
|
|
228
|
+
: null;
|
|
229
|
+
scrollerEl?.addEventListener('wheel', cancelOnUserScroll, { passive: true });
|
|
230
|
+
scrollerEl?.addEventListener('touchstart', cancelOnUserScroll, { passive: true });
|
|
231
|
+
scrollerEl?.addEventListener('keydown', cancelOnUserScroll);
|
|
232
|
+
const land = () => {
|
|
233
|
+
framesLeft -= 1;
|
|
234
|
+
if (userTookOver || framesLeft <= 0) return;
|
|
235
|
+
if (!seeded) {
|
|
236
|
+
// ONE virtuoso-level jump renders the tail items into the window.
|
|
237
|
+
// Never re-issue it per frame: repeated scrollToIndex on a list whose
|
|
238
|
+
// bubble heights are still being measured keeps virtuoso in its
|
|
239
|
+
// initial-location seek (zero rows, visibility:hidden) — the exact
|
|
240
|
+
// blank-transcript deadlock initialTopMostItemIndex causes.
|
|
241
|
+
seeded = true;
|
|
242
|
+
virtuosoRef.current?.scrollToIndex({
|
|
243
|
+
index: 'LAST',
|
|
244
|
+
align: 'end',
|
|
245
|
+
offset: BOTTOM_GAP_PX,
|
|
246
|
+
behavior: 'auto',
|
|
247
|
+
});
|
|
248
|
+
raf = requestAnimationFrame(land);
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
// Correction frames: as measurements land and the total height grows,
|
|
252
|
+
// keep pinning the bottom with DIRECT scrollTop writes (an ordinary
|
|
253
|
+
// scroll to virtuoso, no seek mode). A zero gap does NOT end the phase —
|
|
254
|
+
// it is usually the not-yet-measured state, and the growth we exist to
|
|
255
|
+
// chase arrives after it.
|
|
256
|
+
const el = scrollerRef.current;
|
|
257
|
+
if (el != null && el !== window && el instanceof HTMLElement) {
|
|
258
|
+
const gap = el.scrollHeight - el.clientHeight - el.scrollTop;
|
|
259
|
+
if (gap > 4) {
|
|
260
|
+
el.scrollTop = el.scrollHeight - el.clientHeight;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
raf = requestAnimationFrame(land);
|
|
264
|
+
};
|
|
265
|
+
raf = requestAnimationFrame(land);
|
|
266
|
+
return () => {
|
|
267
|
+
cancelAnimationFrame(raf);
|
|
268
|
+
scrollerEl?.removeEventListener('wheel', cancelOnUserScroll);
|
|
269
|
+
scrollerEl?.removeEventListener('touchstart', cancelOnUserScroll);
|
|
270
|
+
scrollerEl?.removeEventListener('keydown', cancelOnUserScroll);
|
|
271
|
+
};
|
|
211
272
|
}, [messages.length]);
|
|
212
273
|
|
|
213
274
|
// Force-scroll to bottom whenever the consumer bumps `scrollAnchorId`
|
|
@@ -400,16 +461,26 @@ export const MessageList = forwardRef<MessageListHandle, MessageListProps>(funct
|
|
|
400
461
|
// than the viewport (centers items near the bottom). Combined
|
|
401
462
|
// with dynamic bubble heights it triggered the same measure loop:
|
|
402
463
|
// virtuoso recomputes top padding on every size change to keep
|
|
403
|
-
// the cluster bottom-aligned.
|
|
404
|
-
//
|
|
405
|
-
//
|
|
464
|
+
// the cluster bottom-aligned. The mount-time imperative scroll
|
|
465
|
+
// already lands us at the bottom on open, which is what users
|
|
466
|
+
// actually want.
|
|
467
|
+
//
|
|
468
|
+
// No `initialTopMostItemIndex` — when the list mounts WITH
|
|
469
|
+
// history already present (a resumed engine / remounted chat),
|
|
470
|
+
// virtuoso enters its initial-location seek: it renders ZERO rows
|
|
471
|
+
// and keeps the item list `visibility: hidden` until the seek
|
|
472
|
+
// settles — and with dynamic unmeasured bubble heights that seek
|
|
473
|
+
// can never settle, leaving the whole transcript permanently
|
|
474
|
+
// blank (0 items, no error). The mount-time imperative
|
|
475
|
+
// `scrollToIndex('LAST')` effect below covers the land-at-bottom
|
|
476
|
+
// job for both mount paths (empty-then-fill AND mount-with-data),
|
|
477
|
+
// at worst costing one frame of top-anchored paint.
|
|
406
478
|
//
|
|
407
479
|
// No `increaseViewportBy` overscan — virtuoso's default (~0px)
|
|
408
480
|
// is the right call for chat: every overscanned bubble re-renders
|
|
409
481
|
// on every streaming token delta, so a 400px buffer means 3–4
|
|
410
482
|
// extra bubbles re-rendering at 60Hz during a stream. Default
|
|
411
483
|
// keeps the working set tight.
|
|
412
|
-
initialTopMostItemIndex={messages.length > 0 ? messages.length - 1 : 0}
|
|
413
484
|
atBottomThreshold={atBottomThreshold}
|
|
414
485
|
followOutput={(isAtBottom) => (isAtBottom ? 'auto' : false)}
|
|
415
486
|
scrollerRef={(el) => {
|