@djangocfg/ui-tools 2.1.371 → 2.1.373

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/dist/index.cjs CHANGED
@@ -7,7 +7,7 @@ var chunkKNDLV4PI_cjs = require('./chunk-KNDLV4PI.cjs');
7
7
  var chunk5I5QNGUG_cjs = require('./chunk-5I5QNGUG.cjs');
8
8
  var chunkYW5IVWHQ_cjs = require('./chunk-YW5IVWHQ.cjs');
9
9
  var chunk76NNDZH6_cjs = require('./chunk-76NNDZH6.cjs');
10
- var chunk2SKR4U5S_cjs = require('./chunk-2SKR4U5S.cjs');
10
+ var chunkTAEHNX4W_cjs = require('./chunk-TAEHNX4W.cjs');
11
11
  var chunkYXZ6GU7H_cjs = require('./chunk-YXZ6GU7H.cjs');
12
12
  var chunkFVVF7VCD_cjs = require('./chunk-FVVF7VCD.cjs');
13
13
  var chunk7EYHNP3E_cjs = require('./chunk-7EYHNP3E.cjs');
@@ -373,7 +373,7 @@ var LazyTree = createLazyComponent(
373
373
  }
374
374
  );
375
375
  var LazyChat = createLazyComponent(
376
- () => import('./ChatRoot-HOQ37WRE.cjs').then((m) => ({ default: m.ChatRoot })),
376
+ () => import('./ChatRoot-3LA3DSNY.cjs').then((m) => ({ default: m.ChatRoot })),
377
377
  {
378
378
  displayName: "LazyChat",
379
379
  fallback: /* @__PURE__ */ jsxRuntime.jsx(LoadingFallback, { minHeight: 320, text: "Loading chat\u2026" })
@@ -410,7 +410,7 @@ async function* parseSSE(response, options = {}) {
410
410
  throw new Error("SSE response has no body");
411
411
  }
412
412
  const map = options.map ?? DEFAULT_MAP;
413
- const idleMs = options.idleTimeoutMs ?? chunk2SKR4U5S_cjs.LIMITS.sseIdleMs;
413
+ const idleMs = options.idleTimeoutMs ?? chunkTAEHNX4W_cjs.LIMITS.sseIdleMs;
414
414
  const reader = response.body.getReader();
415
415
  const decoder = new TextDecoder();
416
416
  let buffer = "";
@@ -613,7 +613,7 @@ function createMockTransport(opts = {}) {
613
613
  async createSession(_opts) {
614
614
  await sleep(latency);
615
615
  return {
616
- sessionId: chunk2SKR4U5S_cjs.createId("s"),
616
+ sessionId: chunkTAEHNX4W_cjs.createId("s"),
617
617
  messages: history.length ? [...history] : void 0,
618
618
  hasMore: false,
619
619
  cursor: null,
@@ -630,12 +630,12 @@ function createMockTransport(opts = {}) {
630
630
  throw new Error("mock transport scripted failure");
631
631
  }
632
632
  history.push({
633
- id: chunk2SKR4U5S_cjs.createId("u"),
633
+ id: chunkTAEHNX4W_cjs.createId("u"),
634
634
  role: "user",
635
635
  content,
636
636
  createdAt: Date.now()
637
637
  });
638
- const messageId = chunk2SKR4U5S_cjs.createId("a");
638
+ const messageId = chunkTAEHNX4W_cjs.createId("a");
639
639
  yield { type: "message_start", messageId, sessionId: _sid };
640
640
  const reply = replies[turn % replies.length];
641
641
  turn += 1;
@@ -664,7 +664,7 @@ function createMockTransport(opts = {}) {
664
664
  turn += 1;
665
665
  const text = typeof reply === "string" ? reply : reply.filter((e) => e.type === "chunk").map((e) => e.delta).join("");
666
666
  return {
667
- id: chunk2SKR4U5S_cjs.createId("a"),
667
+ id: chunkTAEHNX4W_cjs.createId("a"),
668
668
  role: "assistant",
669
669
  content: text || DEFAULT_REPLY,
670
670
  createdAt: Date.now()
@@ -864,9 +864,9 @@ function AudioToggle({
864
864
  alwaysShow = false,
865
865
  className
866
866
  }) {
867
- const muted = chunk2SKR4U5S_cjs.useChatAudioPrefs((s) => s.muted);
868
- const setMuted = chunk2SKR4U5S_cjs.useChatAudioPrefs((s) => s.setMuted);
869
- const ctx = chunk2SKR4U5S_cjs.useChatContextOptional();
867
+ const muted = chunkTAEHNX4W_cjs.useChatAudioPrefs((s) => s.muted);
868
+ const setMuted = chunkTAEHNX4W_cjs.useChatAudioPrefs((s) => s.setMuted);
869
+ const ctx = chunkTAEHNX4W_cjs.useChatContextOptional();
870
870
  if (ctx && !ctx.hasAudio && !alwaysShow) return null;
871
871
  const Icon = muted ? lucideReact.VolumeX : lucideReact.Volume2;
872
872
  const label = muted ? "Unmute chat sounds" : "Mute chat sounds";
@@ -2180,151 +2180,151 @@ Object.defineProperty(exports, "useCronWeekDays", {
2180
2180
  });
2181
2181
  Object.defineProperty(exports, "Attachments", {
2182
2182
  enumerable: true,
2183
- get: function () { return chunk2SKR4U5S_cjs.Attachments; }
2183
+ get: function () { return chunkTAEHNX4W_cjs.Attachments; }
2184
2184
  });
2185
2185
  Object.defineProperty(exports, "AttachmentsGrid", {
2186
2186
  enumerable: true,
2187
- get: function () { return chunk2SKR4U5S_cjs.AttachmentsGrid; }
2187
+ get: function () { return chunkTAEHNX4W_cjs.AttachmentsGrid; }
2188
2188
  });
2189
2189
  Object.defineProperty(exports, "AttachmentsList", {
2190
2190
  enumerable: true,
2191
- get: function () { return chunk2SKR4U5S_cjs.AttachmentsList; }
2191
+ get: function () { return chunkTAEHNX4W_cjs.AttachmentsList; }
2192
2192
  });
2193
2193
  Object.defineProperty(exports, "CHAT_EVENT_NAME", {
2194
2194
  enumerable: true,
2195
- get: function () { return chunk2SKR4U5S_cjs.CHAT_EVENT_NAME; }
2195
+ get: function () { return chunkTAEHNX4W_cjs.CHAT_EVENT_NAME; }
2196
2196
  });
2197
2197
  Object.defineProperty(exports, "CSS_VARS", {
2198
2198
  enumerable: true,
2199
- get: function () { return chunk2SKR4U5S_cjs.CSS_VARS; }
2199
+ get: function () { return chunkTAEHNX4W_cjs.CSS_VARS; }
2200
2200
  });
2201
2201
  Object.defineProperty(exports, "ChatProvider", {
2202
2202
  enumerable: true,
2203
- get: function () { return chunk2SKR4U5S_cjs.ChatProvider; }
2203
+ get: function () { return chunkTAEHNX4W_cjs.ChatProvider; }
2204
2204
  });
2205
2205
  Object.defineProperty(exports, "ChatRoot", {
2206
2206
  enumerable: true,
2207
- get: function () { return chunk2SKR4U5S_cjs.ChatRoot; }
2207
+ get: function () { return chunkTAEHNX4W_cjs.ChatRoot; }
2208
2208
  });
2209
2209
  Object.defineProperty(exports, "Composer", {
2210
2210
  enumerable: true,
2211
- get: function () { return chunk2SKR4U5S_cjs.Composer; }
2211
+ get: function () { return chunkTAEHNX4W_cjs.Composer; }
2212
2212
  });
2213
2213
  Object.defineProperty(exports, "DEFAULT_LABELS", {
2214
2214
  enumerable: true,
2215
- get: function () { return chunk2SKR4U5S_cjs.DEFAULT_LABELS; }
2215
+ get: function () { return chunkTAEHNX4W_cjs.DEFAULT_LABELS; }
2216
2216
  });
2217
2217
  Object.defineProperty(exports, "DEFAULT_SIDEBAR", {
2218
2218
  enumerable: true,
2219
- get: function () { return chunk2SKR4U5S_cjs.DEFAULT_SIDEBAR; }
2219
+ get: function () { return chunkTAEHNX4W_cjs.DEFAULT_SIDEBAR; }
2220
2220
  });
2221
2221
  Object.defineProperty(exports, "DEFAULT_Z_INDEX", {
2222
2222
  enumerable: true,
2223
- get: function () { return chunk2SKR4U5S_cjs.DEFAULT_Z_INDEX; }
2223
+ get: function () { return chunkTAEHNX4W_cjs.DEFAULT_Z_INDEX; }
2224
2224
  });
2225
2225
  Object.defineProperty(exports, "EmptyState", {
2226
2226
  enumerable: true,
2227
- get: function () { return chunk2SKR4U5S_cjs.EmptyState; }
2227
+ get: function () { return chunkTAEHNX4W_cjs.EmptyState; }
2228
2228
  });
2229
2229
  Object.defineProperty(exports, "ErrorBanner", {
2230
2230
  enumerable: true,
2231
- get: function () { return chunk2SKR4U5S_cjs.ErrorBanner; }
2231
+ get: function () { return chunkTAEHNX4W_cjs.ErrorBanner; }
2232
2232
  });
2233
2233
  Object.defineProperty(exports, "HOTKEYS", {
2234
2234
  enumerable: true,
2235
- get: function () { return chunk2SKR4U5S_cjs.HOTKEYS; }
2235
+ get: function () { return chunkTAEHNX4W_cjs.HOTKEYS; }
2236
2236
  });
2237
2237
  Object.defineProperty(exports, "JumpToLatest", {
2238
2238
  enumerable: true,
2239
- get: function () { return chunk2SKR4U5S_cjs.JumpToLatest; }
2239
+ get: function () { return chunkTAEHNX4W_cjs.JumpToLatest; }
2240
2240
  });
2241
2241
  Object.defineProperty(exports, "LIMITS", {
2242
2242
  enumerable: true,
2243
- get: function () { return chunk2SKR4U5S_cjs.LIMITS; }
2243
+ get: function () { return chunkTAEHNX4W_cjs.LIMITS; }
2244
2244
  });
2245
2245
  Object.defineProperty(exports, "MessageActions", {
2246
2246
  enumerable: true,
2247
- get: function () { return chunk2SKR4U5S_cjs.MessageActions; }
2247
+ get: function () { return chunkTAEHNX4W_cjs.MessageActions; }
2248
2248
  });
2249
2249
  Object.defineProperty(exports, "MessageBubble", {
2250
2250
  enumerable: true,
2251
- get: function () { return chunk2SKR4U5S_cjs.MessageBubble; }
2251
+ get: function () { return chunkTAEHNX4W_cjs.MessageBubble; }
2252
2252
  });
2253
2253
  Object.defineProperty(exports, "MessageList", {
2254
2254
  enumerable: true,
2255
- get: function () { return chunk2SKR4U5S_cjs.MessageList; }
2255
+ get: function () { return chunkTAEHNX4W_cjs.MessageList; }
2256
2256
  });
2257
2257
  Object.defineProperty(exports, "STORAGE_KEYS", {
2258
2258
  enumerable: true,
2259
- get: function () { return chunk2SKR4U5S_cjs.STORAGE_KEYS; }
2259
+ get: function () { return chunkTAEHNX4W_cjs.STORAGE_KEYS; }
2260
2260
  });
2261
2261
  Object.defineProperty(exports, "Sources", {
2262
2262
  enumerable: true,
2263
- get: function () { return chunk2SKR4U5S_cjs.Sources; }
2263
+ get: function () { return chunkTAEHNX4W_cjs.Sources; }
2264
2264
  });
2265
2265
  Object.defineProperty(exports, "StreamingIndicator", {
2266
2266
  enumerable: true,
2267
- get: function () { return chunk2SKR4U5S_cjs.StreamingIndicator; }
2267
+ get: function () { return chunkTAEHNX4W_cjs.StreamingIndicator; }
2268
2268
  });
2269
2269
  Object.defineProperty(exports, "ToolCalls", {
2270
2270
  enumerable: true,
2271
- get: function () { return chunk2SKR4U5S_cjs.ToolCalls; }
2271
+ get: function () { return chunkTAEHNX4W_cjs.ToolCalls; }
2272
2272
  });
2273
2273
  Object.defineProperty(exports, "createId", {
2274
2274
  enumerable: true,
2275
- get: function () { return chunk2SKR4U5S_cjs.createId; }
2275
+ get: function () { return chunkTAEHNX4W_cjs.createId; }
2276
2276
  });
2277
2277
  Object.defineProperty(exports, "createTokenBuffer", {
2278
2278
  enumerable: true,
2279
- get: function () { return chunk2SKR4U5S_cjs.createTokenBuffer; }
2279
+ get: function () { return chunkTAEHNX4W_cjs.createTokenBuffer; }
2280
2280
  });
2281
2281
  Object.defineProperty(exports, "deriveInitials", {
2282
2282
  enumerable: true,
2283
- get: function () { return chunk2SKR4U5S_cjs.deriveInitials; }
2283
+ get: function () { return chunkTAEHNX4W_cjs.deriveInitials; }
2284
2284
  });
2285
2285
  Object.defineProperty(exports, "getChatLogger", {
2286
2286
  enumerable: true,
2287
- get: function () { return chunk2SKR4U5S_cjs.getChatLogger; }
2287
+ get: function () { return chunkTAEHNX4W_cjs.getChatLogger; }
2288
2288
  });
2289
2289
  Object.defineProperty(exports, "initialState", {
2290
2290
  enumerable: true,
2291
- get: function () { return chunk2SKR4U5S_cjs.initialState; }
2291
+ get: function () { return chunkTAEHNX4W_cjs.initialState; }
2292
2292
  });
2293
2293
  Object.defineProperty(exports, "reducer", {
2294
2294
  enumerable: true,
2295
- get: function () { return chunk2SKR4U5S_cjs.reducer; }
2295
+ get: function () { return chunkTAEHNX4W_cjs.reducer; }
2296
2296
  });
2297
2297
  Object.defineProperty(exports, "resolvePersona", {
2298
2298
  enumerable: true,
2299
- get: function () { return chunk2SKR4U5S_cjs.resolvePersona; }
2299
+ get: function () { return chunkTAEHNX4W_cjs.resolvePersona; }
2300
2300
  });
2301
2301
  Object.defineProperty(exports, "useChat", {
2302
2302
  enumerable: true,
2303
- get: function () { return chunk2SKR4U5S_cjs.useChat; }
2303
+ get: function () { return chunkTAEHNX4W_cjs.useChat; }
2304
2304
  });
2305
2305
  Object.defineProperty(exports, "useChatAudio", {
2306
2306
  enumerable: true,
2307
- get: function () { return chunk2SKR4U5S_cjs.useChatAudio; }
2307
+ get: function () { return chunkTAEHNX4W_cjs.useChatAudio; }
2308
2308
  });
2309
2309
  Object.defineProperty(exports, "useChatAudioPrefs", {
2310
2310
  enumerable: true,
2311
- get: function () { return chunk2SKR4U5S_cjs.useChatAudioPrefs; }
2311
+ get: function () { return chunkTAEHNX4W_cjs.useChatAudioPrefs; }
2312
2312
  });
2313
2313
  Object.defineProperty(exports, "useChatComposer", {
2314
2314
  enumerable: true,
2315
- get: function () { return chunk2SKR4U5S_cjs.useChatComposer; }
2315
+ get: function () { return chunkTAEHNX4W_cjs.useChatComposer; }
2316
2316
  });
2317
2317
  Object.defineProperty(exports, "useChatContext", {
2318
2318
  enumerable: true,
2319
- get: function () { return chunk2SKR4U5S_cjs.useChatContext; }
2319
+ get: function () { return chunkTAEHNX4W_cjs.useChatContext; }
2320
2320
  });
2321
2321
  Object.defineProperty(exports, "useChatContextOptional", {
2322
2322
  enumerable: true,
2323
- get: function () { return chunk2SKR4U5S_cjs.useChatContextOptional; }
2323
+ get: function () { return chunkTAEHNX4W_cjs.useChatContextOptional; }
2324
2324
  });
2325
2325
  Object.defineProperty(exports, "useChatLayout", {
2326
2326
  enumerable: true,
2327
- get: function () { return chunk2SKR4U5S_cjs.useChatLayout; }
2327
+ get: function () { return chunkTAEHNX4W_cjs.useChatLayout; }
2328
2328
  });
2329
2329
  Object.defineProperty(exports, "TreeError", {
2330
2330
  enumerable: true,
package/dist/index.d.cts CHANGED
@@ -2413,9 +2413,12 @@ interface MessageListProps {
2413
2413
  */
2414
2414
  noVirtualize?: boolean;
2415
2415
  /**
2416
- * Initial item height estimate fed to Virtuoso's first-paint pass.
2417
- * The library re-measures every item after mount; this just tightens
2418
- * the initial scrollbar before measurements land. Default `120`.
2416
+ * @deprecated No-op as of 2.1.373. We intentionally let Virtuoso run
2417
+ * its first-item probe pass: cmdop bubbles vary from 40px (one-line
2418
+ * user message) to 800px (markdown with code blocks), so any single
2419
+ * estimate fed here forces a measure/reshape loop on every render.
2420
+ * The kept prop avoids a breaking-change bump for hosts that still
2421
+ * pass it explicitly.
2419
2422
  */
2420
2423
  defaultItemHeight?: number;
2421
2424
  /**
package/dist/index.d.ts CHANGED
@@ -2413,9 +2413,12 @@ interface MessageListProps {
2413
2413
  */
2414
2414
  noVirtualize?: boolean;
2415
2415
  /**
2416
- * Initial item height estimate fed to Virtuoso's first-paint pass.
2417
- * The library re-measures every item after mount; this just tightens
2418
- * the initial scrollbar before measurements land. Default `120`.
2416
+ * @deprecated No-op as of 2.1.373. We intentionally let Virtuoso run
2417
+ * its first-item probe pass: cmdop bubbles vary from 40px (one-line
2418
+ * user message) to 800px (markdown with code blocks), so any single
2419
+ * estimate fed here forces a measure/reshape loop on every render.
2420
+ * The kept prop avoids a breaking-change bump for hosts that still
2421
+ * pass it explicitly.
2419
2422
  */
2420
2423
  defaultItemHeight?: number;
2421
2424
  /**
package/dist/index.mjs CHANGED
@@ -5,8 +5,8 @@ export { NativeProvider, StreamProvider, VideoControls, VideoErrorFallback, Vide
5
5
  export { ImageViewer } from './chunk-OBRSGM64.mjs';
6
6
  export { generateContentKey, useAudioCache, useBlobUrlCleanup, useImageCache, useMediaCacheStore, useVideoCache, useVideoPlayerSettings } from './chunk-C6GXVH5J.mjs';
7
7
  export { CronSchedulerProvider, CustomInput, DayChips, MonthDayGrid, SchedulePreview, ScheduleTypeSelector, TimeSelector, buildCron, humanizeCron, isValidCron, parseCron, useCronCustom, useCronMonthDays, useCronPreview, useCronScheduler, useCronSchedulerContext, useCronTime, useCronType, useCronWeekDays } from './chunk-PVAX67JG.mjs';
8
- import { LIMITS, createId, useChatAudioPrefs, useChatContextOptional } from './chunk-MVAT6OPZ.mjs';
9
- export { Attachments, AttachmentsGrid, AttachmentsList, CHAT_EVENT_NAME, CSS_VARS, ChatProvider, ChatRoot, Composer, DEFAULT_LABELS, DEFAULT_SIDEBAR, DEFAULT_Z_INDEX, EmptyState, ErrorBanner, HOTKEYS, JumpToLatest, LIMITS, MessageActions, MessageBubble, MessageList, STORAGE_KEYS, Sources, StreamingIndicator, ToolCalls, createId, createTokenBuffer, deriveInitials, getChatLogger, initialState, reducer, resolvePersona, useChat, useChatAudio, useChatAudioPrefs, useChatComposer, useChatContext, useChatContextOptional, useChatLayout } from './chunk-MVAT6OPZ.mjs';
8
+ import { LIMITS, createId, useChatAudioPrefs, useChatContextOptional } from './chunk-PSM3DUTC.mjs';
9
+ export { Attachments, AttachmentsGrid, AttachmentsList, CHAT_EVENT_NAME, CSS_VARS, ChatProvider, ChatRoot, Composer, DEFAULT_LABELS, DEFAULT_SIDEBAR, DEFAULT_Z_INDEX, EmptyState, ErrorBanner, HOTKEYS, JumpToLatest, LIMITS, MessageActions, MessageBubble, MessageList, STORAGE_KEYS, Sources, StreamingIndicator, ToolCalls, createId, createTokenBuffer, deriveInitials, getChatLogger, initialState, reducer, resolvePersona, useChat, useChatAudio, useChatAudioPrefs, useChatComposer, useChatContext, useChatContextOptional, useChatLayout } from './chunk-PSM3DUTC.mjs';
10
10
  export { TreeError, TreeSkeleton, createDemoTree } from './chunk-B6IR5KSC.mjs';
11
11
  export { DEFAULT_TREE_APPEARANCE, DEFAULT_TREE_LABELS, TreeRoot as Tree, TreeChevron, TreeContent, TreeEmpty, TreeIcon, TreeIndentGuides, TreeLabel, TreeProvider, TreeRoot, TreeRow, TreeSearchInput, appearanceToStyle, clearTreeState, createChildCache, flattenTree, loadTreeState, resolveAppearance, resolveChildren, saveTreeState, useTreeActions, useTreeContext, useTreeExpansion, useTreeFocus, useTreeKeyboard, useTreeLabels, useTreeRows, useTreeSearch, useTreeSelection, useTreeTypeAhead } from './chunk-ZL7FH4NW.mjs';
12
12
  import { PlaygroundProvider } from './chunk-Y6UTOBF6.mjs';
@@ -348,7 +348,7 @@ var LazyTree = createLazyComponent(
348
348
  }
349
349
  );
350
350
  var LazyChat = createLazyComponent(
351
- () => import('./ChatRoot-DYMCNGOB.mjs').then((m) => ({ default: m.ChatRoot })),
351
+ () => import('./ChatRoot-RIETBE55.mjs').then((m) => ({ default: m.ChatRoot })),
352
352
  {
353
353
  displayName: "LazyChat",
354
354
  fallback: /* @__PURE__ */ jsx(LoadingFallback, { minHeight: 320, text: "Loading chat\u2026" })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@djangocfg/ui-tools",
3
- "version": "2.1.371",
3
+ "version": "2.1.373",
4
4
  "description": "Heavy React tools with lazy loading - for Electron, Vite, CRA, Next.js apps",
5
5
  "keywords": [
6
6
  "ui-tools",
@@ -156,8 +156,8 @@
156
156
  "check": "tsc --noEmit"
157
157
  },
158
158
  "peerDependencies": {
159
- "@djangocfg/i18n": "^2.1.371",
160
- "@djangocfg/ui-core": "^2.1.371",
159
+ "@djangocfg/i18n": "^2.1.373",
160
+ "@djangocfg/ui-core": "^2.1.373",
161
161
  "consola": "^3.4.2",
162
162
  "lodash-es": "^4.18.1",
163
163
  "lucide-react": "^0.545.0",
@@ -211,10 +211,10 @@
211
211
  "material-file-icons": "^2.4.0"
212
212
  },
213
213
  "devDependencies": {
214
- "@djangocfg/i18n": "^2.1.371",
214
+ "@djangocfg/i18n": "^2.1.373",
215
215
  "@djangocfg/playground": "workspace:*",
216
- "@djangocfg/typescript-config": "^2.1.371",
217
- "@djangocfg/ui-core": "^2.1.371",
216
+ "@djangocfg/typescript-config": "^2.1.373",
217
+ "@djangocfg/ui-core": "^2.1.373",
218
218
  "@types/lodash-es": "^4.17.12",
219
219
  "@types/mapbox__mapbox-gl-draw": "^1.4.8",
220
220
  "@types/node": "^24.7.2",
@@ -1110,3 +1110,174 @@ export const Playground = () => {
1110
1110
  </div>
1111
1111
  );
1112
1112
  };
1113
+
1114
+ // ---------------------------------------------------------------------------
1115
+ // 15) WailsLikeVirtualization — stress-test for the virtuoso integration
1116
+ //
1117
+ // Mirrors the cmdop-desktop (Wails) chat use case that drove the
1118
+ // plan64 migration:
1119
+ // - 200 pre-loaded history bubbles of varying heights (short, long
1120
+ // prose, code blocks, tool calls) — exercises Virtuoso's dynamic
1121
+ // measurement path and the bug where bubbles "disappeared" when
1122
+ // scrolling to the bottom.
1123
+ // - Streamed reply with token-by-token deltas — exercises
1124
+ // `followOutput` sticky-bottom behaviour.
1125
+ // - Tool call mid-stream — exercises bubble-resize during streaming
1126
+ // (the second virtuoso jitter source).
1127
+ //
1128
+ // Use this story to verify:
1129
+ // 1. Mount → viewport lands at the last bubble, not the top.
1130
+ // 2. Scroll up → JumpToLatest pill appears; click → smooth jump.
1131
+ // 3. Sit at the bottom → streaming reply tracks without flicker.
1132
+ // 4. Drag scrollbar to the very bottom → bubbles stay visible (no
1133
+ // empty viewport).
1134
+ // ---------------------------------------------------------------------------
1135
+
1136
+ function makeWailsLikeHistory(count: number): ChatMessage[] {
1137
+ // Mix of bubble shapes so virtuoso has to measure heterogeneous
1138
+ // heights instead of locking onto one cheap estimate.
1139
+ const SHORT_USER = 'Quick ping — can you check the latest deploy?';
1140
+ const SHORT_AI = 'Yes, all clear. CI passed, no regressions.';
1141
+ const LONG_PROSE =
1142
+ 'Sure, here is the rundown.\n\nThe service has been migrated to the new transport layer. The reducer split is in plan64 phase 4, and the audio cues now respect `prefers-reduced-motion`.\n\n' +
1143
+ 'A few things to double-check before we sign off:\n\n' +
1144
+ '- The auto-scroll behaviour on streaming chunks\n' +
1145
+ '- Tool-call panels expanding mid-stream\n' +
1146
+ '- The mention popup positioning inside the composer\n\n' +
1147
+ 'Let me know if any of those need a second pass.';
1148
+ const CODE_REPLY =
1149
+ 'Here is the snippet you asked for:\n\n```ts\n' +
1150
+ 'export function createWailsTransport(): ChatTransport {\n' +
1151
+ ' return {\n' +
1152
+ ' async *stream(sid, content, { signal }) {\n' +
1153
+ ' const queue = createAsyncQueue<ChatStreamEvent>();\n' +
1154
+ ' const off = subscribeChatStreamEvents(sid, (e) => queue.push(e));\n' +
1155
+ ' try {\n' +
1156
+ ' await ChatService.SendMessage(sid, content);\n' +
1157
+ ' for await (const evt of queue) {\n' +
1158
+ ' yield evt;\n' +
1159
+ ' if (isTerminalStreamEvent(evt)) break;\n' +
1160
+ ' }\n' +
1161
+ ' } finally {\n' +
1162
+ ' off(); queue.close();\n' +
1163
+ ' }\n' +
1164
+ ' },\n' +
1165
+ ' };\n' +
1166
+ '}\n' +
1167
+ '```\n\nDoes that match what you had in mind?';
1168
+
1169
+ const shapes: Array<(i: number) => ChatMessage> = [
1170
+ (i) => ({
1171
+ id: `u${i}`,
1172
+ role: 'user',
1173
+ content: SHORT_USER,
1174
+ createdAt: Date.now() - (count - i) * 10_000,
1175
+ }),
1176
+ (i) => ({
1177
+ id: `a${i}`,
1178
+ role: 'assistant',
1179
+ content: SHORT_AI,
1180
+ createdAt: Date.now() - (count - i) * 10_000,
1181
+ }),
1182
+ (i) => ({
1183
+ id: `a${i}`,
1184
+ role: 'assistant',
1185
+ content: LONG_PROSE,
1186
+ createdAt: Date.now() - (count - i) * 10_000,
1187
+ }),
1188
+ (i) => ({
1189
+ id: `a${i}`,
1190
+ role: 'assistant',
1191
+ content: CODE_REPLY,
1192
+ createdAt: Date.now() - (count - i) * 10_000,
1193
+ }),
1194
+ (i) => ({
1195
+ id: `a${i}`,
1196
+ role: 'assistant',
1197
+ content: 'Running diagnostics…',
1198
+ createdAt: Date.now() - (count - i) * 10_000,
1199
+ toolCalls: [
1200
+ {
1201
+ id: `t${i}`,
1202
+ name: 'read_file',
1203
+ input: { path: `/var/log/app-${i}.log`, lines: 50 },
1204
+ output: { ok: true, bytes: 4096 + (i % 7) * 512 },
1205
+ status: 'success',
1206
+ startedAt: Date.now() - (count - i) * 10_000,
1207
+ endedAt: Date.now() - (count - i) * 10_000 + 250,
1208
+ },
1209
+ ],
1210
+ }),
1211
+ ];
1212
+
1213
+ return Array.from({ length: count }, (_, i) => shapes[i % shapes.length](i));
1214
+ }
1215
+
1216
+ export const WailsLikeVirtualization = () => {
1217
+ const [count] = useSelect('history', {
1218
+ options: ['50', '200', '500', '1000'] as const,
1219
+ defaultValue: '200',
1220
+ label: 'History size',
1221
+ });
1222
+
1223
+ // Pre-seeded history simulating a long-running cmdop session.
1224
+ // `resumeSession` is the key bit: the mock transport returns these
1225
+ // as `SessionInfo.messages` so they're available at mount time
1226
+ // (mirrors `ChatService.GetHistoryPaginated` on the Wails side).
1227
+ const history = useMemo(() => makeWailsLikeHistory(Number(count)), [count]);
1228
+
1229
+ const transport = useMemo(() => {
1230
+ const sequence: ChatStreamEvent[] = [
1231
+ { type: 'chunk', delta: 'Let me pull the latest stats.\n\n' },
1232
+ {
1233
+ type: 'tool_call_start',
1234
+ toolId: 'live-1',
1235
+ name: 'fetch_metrics',
1236
+ input: { window: '15m' },
1237
+ },
1238
+ { type: 'tool_call_delta', toolId: 'live-1', delta: 'connecting…\n' },
1239
+ { type: 'tool_call_delta', toolId: 'live-1', delta: 'fetched 3214 rows\n' },
1240
+ {
1241
+ type: 'tool_call_end',
1242
+ toolId: 'live-1',
1243
+ output: { rows: 3214, p95_ms: 142, errors: 0 },
1244
+ status: 'success',
1245
+ },
1246
+ {
1247
+ type: 'chunk',
1248
+ delta:
1249
+ 'All green:\n\n- **3214 rows** in the last 15 min\n- **p95 latency**: 142 ms\n- **errors**: 0\n\n',
1250
+ },
1251
+ {
1252
+ type: 'chunk',
1253
+ delta:
1254
+ 'If you want to stress-test virtualization, scroll up and check the JumpToLatest pill — it should appear once you move out of the sticky-bottom zone.',
1255
+ },
1256
+ { type: 'message_end' },
1257
+ ];
1258
+ return createMockTransport({
1259
+ replies: [sequence, sequence, sequence],
1260
+ latencyMs: 25,
1261
+ // Mock transport returns `initialMessages` from `createSession`
1262
+ // as `SessionInfo.messages` — mimics the Wails path where the
1263
+ // default chat session resumes with its on-disk history.
1264
+ initialMessages: history,
1265
+ });
1266
+ }, [history]);
1267
+
1268
+ return (
1269
+ <Frame h={620} w={520}>
1270
+ <ChatRoot
1271
+ transport={transport}
1272
+ config={{
1273
+ greeting: `Wails-like session — ${history.length} bubbles preloaded`,
1274
+ description:
1275
+ 'Stress test for the virtuoso integration: long history, dynamic bubble heights, streaming tool calls. Scroll to the bottom; bubbles must stay visible. Streaming should follow without flicker.',
1276
+ placeholder: 'Send a message to trigger a streamed reply…',
1277
+ user: { name: 'Mark', initials: 'MO' },
1278
+ assistant: { name: 'cmdop', initials: 'CM' },
1279
+ }}
1280
+ />
1281
+ </Frame>
1282
+ );
1283
+ };
@@ -55,9 +55,12 @@ export interface MessageListProps {
55
55
  */
56
56
  noVirtualize?: boolean;
57
57
  /**
58
- * Initial item height estimate fed to Virtuoso's first-paint pass.
59
- * The library re-measures every item after mount; this just tightens
60
- * the initial scrollbar before measurements land. Default `120`.
58
+ * @deprecated No-op as of 2.1.373. We intentionally let Virtuoso run
59
+ * its first-item probe pass: cmdop bubbles vary from 40px (one-line
60
+ * user message) to 800px (markdown with code blocks), so any single
61
+ * estimate fed here forces a measure/reshape loop on every render.
62
+ * The kept prop avoids a breaking-change bump for hosts that still
63
+ * pass it explicitly.
61
64
  */
62
65
  defaultItemHeight?: number;
63
66
  /**
@@ -85,17 +88,26 @@ export const MessageList = forwardRef<MessageListHandle, MessageListProps>(funct
85
88
  className,
86
89
  itemClassName,
87
90
  noVirtualize = false,
88
- defaultItemHeight = 120,
91
+ defaultItemHeight: _deprecatedDefaultItemHeight,
89
92
  onAtBottomChange,
90
93
  },
91
94
  ref,
92
95
  ) {
96
+ void _deprecatedDefaultItemHeight;
93
97
  const ctx = useChatContextOptional();
94
98
  const messages = messagesProp ?? ctx?.messages ?? [];
95
99
  const isLoadingMore = isLoadingMoreProp ?? ctx?.isLoadingMore ?? false;
96
100
  const { copyToClipboard } = useCopy();
97
101
 
98
102
  const virtuosoRef = useRef<VirtuosoHandle | null>(null);
103
+ // Track whether we've already landed on the bottom for the initial
104
+ // history. Virtuoso's `initialTopMostItemIndex` only fires on first
105
+ // mount and uses the `messages` length at that moment. Chats almost
106
+ // always mount with `messages=[]` and then receive history one
107
+ // dispatch later — so we ALSO fire an imperative scroll when the
108
+ // first non-empty batch arrives. Subsequent updates fall through to
109
+ // `followOutput` (sticky-bottom while the user is at bottom).
110
+ const didInitialScrollRef = useRef(false);
99
111
 
100
112
  useImperativeHandle(
101
113
  ref,
@@ -132,6 +144,24 @@ export const MessageList = forwardRef<MessageListHandle, MessageListProps>(funct
132
144
  );
133
145
 
134
146
  const itemRenderer = renderItem ?? defaultRenderItem;
147
+ // Jump to bottom on the first non-empty messages batch. Wrapped in
148
+ // rAF so virtuoso has measured the new items before we ask it to
149
+ // scroll — otherwise the call happens while the list is still
150
+ // mid-layout and lands on the wrong offset.
151
+ useEffect(() => {
152
+ if (didInitialScrollRef.current) return;
153
+ if (messages.length === 0) return;
154
+ didInitialScrollRef.current = true;
155
+ const id = requestAnimationFrame(() => {
156
+ virtuosoRef.current?.scrollToIndex({
157
+ index: 'LAST',
158
+ align: 'end',
159
+ behavior: 'auto',
160
+ });
161
+ });
162
+ return () => cancelAnimationFrame(id);
163
+ }, [messages.length]);
164
+
135
165
  // Virtuoso may invoke `computeItemKey` for an index briefly out of
136
166
  // sync with `data` during fast state churn (streaming chunks +
137
167
  // Strict Mode double-mount). `m` arrives undefined in that window.
@@ -212,19 +242,31 @@ export const MessageList = forwardRef<MessageListHandle, MessageListProps>(funct
212
242
  data={messages}
213
243
  computeItemKey={computeItemKey}
214
244
  itemContent={(index, m) => (m ? itemRenderer(m, index) : null)}
215
- defaultItemHeight={defaultItemHeight}
216
- // Sticky-bottom: keep the viewport anchored to the latest message
217
- // unless the user scrolled up. `'smooth'` while streaming would
218
- // jank; Virtuoso defaults to `'auto'` which is what we want.
245
+ // No `defaultItemHeight` — Virtuoso uses its first-item probe
246
+ // pass. We previously fed it `120` which was wildly low for
247
+ // chat bubbles (markdown + code blocks reach 500px+) and forced
248
+ // virtuoso into a measure/reshape loop on every render: predicted
249
+ // height << real height → page recompute → measure → repeat,
250
+ // visible as jittery scrolling and bubbles "blinking" in and out.
251
+ // The probe pass costs one extra render on mount; that's cheaper
252
+ // than perpetual reshape.
253
+ //
254
+ // No `alignToBottom` — it only matters when the list is shorter
255
+ // than the viewport (centers items near the bottom). Combined
256
+ // with dynamic bubble heights it triggered the same measure loop:
257
+ // virtuoso recomputes top padding on every size change to keep
258
+ // the cluster bottom-aligned. `initialTopMostItemIndex` + the
259
+ // mount-time imperative scroll already land us at the bottom on
260
+ // open, which is what users actually want.
261
+ //
262
+ // No `increaseViewportBy` overscan — virtuoso's default (~0px)
263
+ // is the right call for chat: every overscanned bubble re-renders
264
+ // on every streaming token delta, so a 400px buffer means 3–4
265
+ // extra bubbles re-rendering at 60Hz during a stream. Default
266
+ // keeps the working set tight.
267
+ initialTopMostItemIndex={messages.length > 0 ? messages.length - 1 : 0}
219
268
  followOutput={(isAtBottom) => (isAtBottom ? 'auto' : false)}
220
269
  atBottomStateChange={onAtBottomChange}
221
- // Pad the list so a short conversation still hugs the bottom of
222
- // the viewport (Telegram / iMessage feel) instead of stacking at
223
- // the top.
224
- alignToBottom
225
- // Top-of-list pagination — fire when the topmost item enters the
226
- // viewport. Virtuoso pauses this until `data` grows, so loadMore
227
- // implementations don't need their own debounce.
228
270
  startReached={startReachedHandler}
229
271
  // Spinner while older history is loading. Rendering it as the
230
272
  // Header keeps it inside the virtualized scroll, so it doesn't
@@ -245,10 +287,6 @@ export const MessageList = forwardRef<MessageListHandle, MessageListProps>(funct
245
287
  }
246
288
  : EMPTY_COMPONENTS
247
289
  }
248
- // Item height is dynamic; Virtuoso re-measures on resize. We
249
- // bias the initial overscan a bit so streaming-token re-layout
250
- // doesn't leave gaps before the next measurement lands.
251
- increaseViewportBy={{ top: 200, bottom: 400 }}
252
290
  />
253
291
  );
254
292
  });
@@ -1,5 +0,0 @@
1
- export { ChatRoot } from './chunk-MVAT6OPZ.mjs';
2
- import './chunk-NWUT327A.mjs';
3
- import './chunk-N2XQF2OL.mjs';
4
- //# sourceMappingURL=ChatRoot-DYMCNGOB.mjs.map
5
- //# sourceMappingURL=ChatRoot-DYMCNGOB.mjs.map