@lvce-editor/chat-view 1.18.0 → 1.20.0

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.
@@ -1192,6 +1192,9 @@ const getComposerWidth = width => {
1192
1192
  const getMinComposerHeight = lineHeight => {
1193
1193
  return lineHeight + 8;
1194
1194
  };
1195
+ const getMaxComposerHeight = (lineHeight, maxComposerRows) => {
1196
+ return lineHeight * Math.max(1, maxComposerRows) + 8;
1197
+ };
1195
1198
  const estimateComposerHeight = (value, lineHeight) => {
1196
1199
  const lineCount = value.split('\n').length;
1197
1200
  return lineCount * lineHeight + 8;
@@ -1200,17 +1203,19 @@ const getComposerHeight = async (state, value, width = state.width) => {
1200
1203
  const {
1201
1204
  composerFontFamily,
1202
1205
  composerFontSize,
1203
- composerLineHeight
1206
+ composerLineHeight,
1207
+ maxComposerRows
1204
1208
  } = state;
1205
1209
  const minimumHeight = getMinComposerHeight(composerLineHeight);
1210
+ const maximumHeight = getMaxComposerHeight(composerLineHeight, maxComposerRows);
1206
1211
  const content = value || ' ';
1207
1212
  const composerWidth = getComposerWidth(width);
1208
1213
  try {
1209
1214
  const measuredHeight = await measureTextBlockHeight(content, composerFontFamily, composerFontSize, composerLineHeight, composerWidth);
1210
1215
  const height = Math.ceil(measuredHeight) + 8;
1211
- return Math.max(minimumHeight, height);
1216
+ return Math.max(minimumHeight, Math.min(maximumHeight, height));
1212
1217
  } catch {
1213
- return Math.max(minimumHeight, estimateComposerHeight(value, composerLineHeight));
1218
+ return Math.max(minimumHeight, Math.min(maximumHeight, estimateComposerHeight(value, composerLineHeight)));
1214
1219
  }
1215
1220
  };
1216
1221
  const getMinComposerHeightForState = state => {
@@ -1381,10 +1386,16 @@ const getDefaultModels = () => {
1381
1386
  const createDefaultState = () => {
1382
1387
  const defaultSessionId = 'session-1';
1383
1388
  const defaultModelId = 'test';
1389
+ const chatMessageFontSize = 13;
1390
+ const chatMessageLineHeight = 20;
1384
1391
  const composerFontSize = 13;
1385
1392
  const composerLineHeight = 20;
1386
1393
  return {
1387
1394
  assetDir: '',
1395
+ chatListScrollTop: 0,
1396
+ chatMessageFontFamily: 'system-ui',
1397
+ chatMessageFontSize,
1398
+ chatMessageLineHeight,
1388
1399
  composerFontFamily: 'system-ui',
1389
1400
  composerFontSize,
1390
1401
  composerHeight: composerLineHeight + 8,
@@ -1399,6 +1410,8 @@ const createDefaultState = () => {
1399
1410
  inputSource: 'script',
1400
1411
  lastSubmittedSessionId: '',
1401
1412
  listItemHeight: 40,
1413
+ maxComposerRows: 5,
1414
+ messagesScrollTop: 0,
1402
1415
  mockApiCommandId: '',
1403
1416
  models: getDefaultModels(),
1404
1417
  nextMessageId: 1,
@@ -1411,6 +1424,7 @@ const createDefaultState = () => {
1411
1424
  openRouterApiKeyInput: '',
1412
1425
  openRouterApiKeysSettingsUrl: 'https://openrouter.ai/settings/keys',
1413
1426
  openRouterApiKeyState: 'idle',
1427
+ passIncludeObfuscation: false,
1414
1428
  platform: 0,
1415
1429
  renamingSessionId: '',
1416
1430
  selectedModelId: defaultModelId,
@@ -1420,6 +1434,7 @@ const createDefaultState = () => {
1420
1434
  messages: [],
1421
1435
  title: defaultSessionTitle()
1422
1436
  }],
1437
+ streamingEnabled: false,
1423
1438
  tokensMax: 0,
1424
1439
  tokensUsed: 0,
1425
1440
  uid: 0,
@@ -1457,7 +1472,7 @@ const create = (uid, x, y, width, height, platform, assetDir) => {
1457
1472
  };
1458
1473
 
1459
1474
  const isEqual$1 = (oldState, newState) => {
1460
- return oldState.initial === newState.initial && oldState.composerHeight === newState.composerHeight && oldState.composerLineHeight === newState.composerLineHeight && oldState.composerFontFamily === newState.composerFontFamily && oldState.composerFontSize === newState.composerFontSize;
1475
+ return oldState.initial === newState.initial && oldState.chatMessageFontFamily === newState.chatMessageFontFamily && oldState.chatMessageFontSize === newState.chatMessageFontSize && oldState.chatMessageLineHeight === newState.chatMessageLineHeight && oldState.composerHeight === newState.composerHeight && oldState.composerLineHeight === newState.composerLineHeight && oldState.composerFontFamily === newState.composerFontFamily && oldState.composerFontSize === newState.composerFontSize && oldState.listItemHeight === newState.listItemHeight;
1461
1476
  };
1462
1477
 
1463
1478
  const diffFocus = (oldState, newState) => {
@@ -1471,12 +1486,17 @@ const isEqual = (oldState, newState) => {
1471
1486
  return oldState.composerValue === newState.composerValue && oldState.initial === newState.initial && oldState.renamingSessionId === newState.renamingSessionId && oldState.selectedModelId === newState.selectedModelId && oldState.selectedSessionId === newState.selectedSessionId && oldState.sessions === newState.sessions && oldState.tokensMax === newState.tokensMax && oldState.tokensUsed === newState.tokensUsed && oldState.usageOverviewEnabled === newState.usageOverviewEnabled && oldState.viewMode === newState.viewMode;
1472
1487
  };
1473
1488
 
1489
+ const diffScrollTop = (oldState, newState) => {
1490
+ return oldState.chatListScrollTop === newState.chatListScrollTop && oldState.messagesScrollTop === newState.messagesScrollTop;
1491
+ };
1492
+
1474
1493
  const RenderItems = 4;
1475
1494
  const RenderFocus = 6;
1476
1495
  const RenderFocusContext = 7;
1477
1496
  const RenderValue = 8;
1478
1497
  const RenderCss = 10;
1479
1498
  const RenderIncremental = 11;
1499
+ const RenderScrollTop = 12;
1480
1500
 
1481
1501
  const diffValue = (oldState, newState) => {
1482
1502
  if (oldState.composerValue === newState.composerValue) {
@@ -1485,8 +1505,8 @@ const diffValue = (oldState, newState) => {
1485
1505
  return newState.inputSource !== 'script';
1486
1506
  };
1487
1507
 
1488
- const modules = [isEqual, diffValue, diffFocus, isEqual$1, diffFocus];
1489
- const numbers = [RenderIncremental, RenderValue, RenderFocus, RenderCss, RenderFocusContext];
1508
+ const modules = [isEqual, diffValue, diffFocus, isEqual$1, diffFocus, diffScrollTop];
1509
+ const numbers = [RenderIncremental, RenderValue, RenderFocus, RenderCss, RenderFocusContext, RenderScrollTop];
1490
1510
 
1491
1511
  const diff = (oldState, newState) => {
1492
1512
  const diffResult = [];
@@ -1519,6 +1539,7 @@ const FocusSelector = 'Viewlet.focusSelector';
1519
1539
  const SetCss = 'Viewlet.setCss';
1520
1540
  const SetDom2 = 'Viewlet.setDom2';
1521
1541
  const SetFocusContext = 'Viewlet.setFocusContext';
1542
+ const SetProperty = 'Viewlet.setProperty';
1522
1543
  const SetValueByName = 'Viewlet.setValueByName';
1523
1544
  const SetPatches = 'Viewlet.setPatches';
1524
1545
 
@@ -1868,18 +1889,12 @@ const getListIndex = (state, eventX, eventY) => {
1868
1889
  x,
1869
1890
  y
1870
1891
  } = state;
1871
- if (eventX < x || eventY < y) {
1892
+ const relativeX = eventX - x;
1893
+ const relativeY = eventY - y - headerHeight;
1894
+ if (relativeX < 0 || relativeY < 0 || relativeX >= width || relativeY >= height - headerHeight) {
1872
1895
  return -1;
1873
1896
  }
1874
- if (eventX >= x + width || eventY >= y + height) {
1875
- return -1;
1876
- }
1877
- const listY = eventY - y - headerHeight;
1878
- if (listY < 0) {
1879
- return -1;
1880
- }
1881
- const itemHeight = listItemHeight > 0 ? listItemHeight : 40;
1882
- return Math.floor(listY / itemHeight);
1897
+ return Math.floor(relativeY / listItemHeight);
1883
1898
  };
1884
1899
 
1885
1900
  const CHAT_LIST_ITEM_CONTEXT_MENU = 'ChatListItemContextMenu';
@@ -2192,6 +2207,63 @@ const getMockAiResponse = async userMessage => {
2192
2207
  return `Mock AI response: I received "${userMessage}".`;
2193
2208
  };
2194
2209
 
2210
+ let queue = [];
2211
+ let waiters = [];
2212
+ let finished = false;
2213
+ const reset$1 = () => {
2214
+ queue = [];
2215
+ waiters = [];
2216
+ finished = false;
2217
+ };
2218
+ const pushChunk = chunk => {
2219
+ if (waiters.length > 0) {
2220
+ const resolve = waiters.shift();
2221
+ resolve?.(chunk);
2222
+ return;
2223
+ }
2224
+ queue.push(chunk);
2225
+ };
2226
+ const finish = () => {
2227
+ finished = true;
2228
+ if (waiters.length === 0) {
2229
+ return;
2230
+ }
2231
+ const activeWaiters = waiters;
2232
+ waiters = [];
2233
+ for (const resolve of activeWaiters) {
2234
+ resolve(undefined);
2235
+ }
2236
+ };
2237
+ const readNextChunk = async () => {
2238
+ if (queue.length > 0) {
2239
+ return queue.shift();
2240
+ }
2241
+ if (finished) {
2242
+ return undefined;
2243
+ }
2244
+ return new Promise(resolve => {
2245
+ waiters.push(resolve);
2246
+ });
2247
+ };
2248
+
2249
+ const getMockOpenApiAssistantText = async (stream, onTextChunk) => {
2250
+ let text = '';
2251
+ while (true) {
2252
+ const chunk = await readNextChunk();
2253
+ if (typeof chunk !== 'string') {
2254
+ break;
2255
+ }
2256
+ text += chunk;
2257
+ if (stream && onTextChunk) {
2258
+ await onTextChunk(chunk);
2259
+ }
2260
+ }
2261
+ return {
2262
+ text,
2263
+ type: 'success'
2264
+ };
2265
+ };
2266
+
2195
2267
  const activateByEvent = (event, assetDir, platform) => {
2196
2268
  // @ts-ignore
2197
2269
  return activateByEvent$1(event, assetDir, platform);
@@ -2495,7 +2567,16 @@ const executeChatTool = async (name, rawArguments, options) => {
2495
2567
  });
2496
2568
  };
2497
2569
 
2498
- const getOpenApiApiEndpoint = openApiApiBaseUrl => {
2570
+ const getClientRequestIdHeader = () => {
2571
+ return {
2572
+ 'x-client-request-id': crypto.randomUUID()
2573
+ };
2574
+ };
2575
+
2576
+ const getOpenApiApiEndpoint = (openApiApiBaseUrl, stream) => {
2577
+ if (stream) {
2578
+ return `${openApiApiBaseUrl}/chat/completions?stream=true`;
2579
+ }
2499
2580
  return `${openApiApiBaseUrl}/chat/completions`;
2500
2581
  };
2501
2582
 
@@ -2520,6 +2601,148 @@ const getTextContent = content => {
2520
2601
  return textParts.join('\n');
2521
2602
  };
2522
2603
 
2604
+ const getStreamChunkText = content => {
2605
+ if (typeof content === 'string') {
2606
+ return content;
2607
+ }
2608
+ if (!Array.isArray(content)) {
2609
+ return '';
2610
+ }
2611
+ return content.map(part => {
2612
+ if (!part || typeof part !== 'object') {
2613
+ return '';
2614
+ }
2615
+ const text = Reflect.get(part, 'text');
2616
+ return typeof text === 'string' ? text : '';
2617
+ }).join('');
2618
+ };
2619
+ const parseSseEvent = eventChunk => {
2620
+ const lines = eventChunk.split('\n');
2621
+ const dataLines = [];
2622
+ for (const line of lines) {
2623
+ if (!line.startsWith('data:')) {
2624
+ continue;
2625
+ }
2626
+ dataLines.push(line.slice(5).trimStart());
2627
+ }
2628
+ return dataLines;
2629
+ };
2630
+ const parseOpenApiStream = async (response, onTextChunk) => {
2631
+ if (!response.body) {
2632
+ return {
2633
+ details: 'request-failed',
2634
+ type: 'error'
2635
+ };
2636
+ }
2637
+ const reader = response.body.getReader();
2638
+ const decoder = new TextDecoder();
2639
+ let remainder = '';
2640
+ let text = '';
2641
+ let done = false;
2642
+ while (!done) {
2643
+ const {
2644
+ done: streamDone,
2645
+ value
2646
+ } = await reader.read();
2647
+ if (streamDone) {
2648
+ done = true;
2649
+ } else if (value) {
2650
+ remainder += decoder.decode(value, {
2651
+ stream: true
2652
+ });
2653
+ }
2654
+ while (true) {
2655
+ const separatorIndex = remainder.indexOf('\n\n');
2656
+ if (separatorIndex === -1) {
2657
+ break;
2658
+ }
2659
+ const rawEvent = remainder.slice(0, separatorIndex);
2660
+ remainder = remainder.slice(separatorIndex + 2);
2661
+ const dataLines = parseSseEvent(rawEvent);
2662
+ if (dataLines.length === 0) {
2663
+ continue;
2664
+ }
2665
+ for (const line of dataLines) {
2666
+ if (line === '[DONE]') {
2667
+ done = true;
2668
+ break;
2669
+ }
2670
+ let parsed;
2671
+ try {
2672
+ parsed = JSON.parse(line);
2673
+ } catch {
2674
+ continue;
2675
+ }
2676
+ if (!parsed || typeof parsed !== 'object') {
2677
+ continue;
2678
+ }
2679
+ const choices = Reflect.get(parsed, 'choices');
2680
+ if (!Array.isArray(choices)) {
2681
+ continue;
2682
+ }
2683
+ const firstChoice = choices[0];
2684
+ if (!firstChoice || typeof firstChoice !== 'object') {
2685
+ continue;
2686
+ }
2687
+ const delta = Reflect.get(firstChoice, 'delta');
2688
+ if (!delta || typeof delta !== 'object') {
2689
+ continue;
2690
+ }
2691
+ const content = Reflect.get(delta, 'content');
2692
+ const chunkText = getStreamChunkText(content);
2693
+ if (!chunkText) {
2694
+ continue;
2695
+ }
2696
+ text += chunkText;
2697
+ if (onTextChunk) {
2698
+ await onTextChunk(chunkText);
2699
+ }
2700
+ }
2701
+ }
2702
+ }
2703
+ if (remainder) {
2704
+ const dataLines = parseSseEvent(remainder);
2705
+ for (const line of dataLines) {
2706
+ if (line === '[DONE]') {
2707
+ continue;
2708
+ }
2709
+ let parsed;
2710
+ try {
2711
+ parsed = JSON.parse(line);
2712
+ } catch {
2713
+ continue;
2714
+ }
2715
+ if (!parsed || typeof parsed !== 'object') {
2716
+ continue;
2717
+ }
2718
+ const choices = Reflect.get(parsed, 'choices');
2719
+ if (!Array.isArray(choices)) {
2720
+ continue;
2721
+ }
2722
+ const firstChoice = choices[0];
2723
+ if (!firstChoice || typeof firstChoice !== 'object') {
2724
+ continue;
2725
+ }
2726
+ const delta = Reflect.get(firstChoice, 'delta');
2727
+ if (!delta || typeof delta !== 'object') {
2728
+ continue;
2729
+ }
2730
+ const content = Reflect.get(delta, 'content');
2731
+ const chunkText = getStreamChunkText(content);
2732
+ if (!chunkText) {
2733
+ continue;
2734
+ }
2735
+ text += chunkText;
2736
+ if (onTextChunk) {
2737
+ await onTextChunk(chunkText);
2738
+ }
2739
+ }
2740
+ }
2741
+ return {
2742
+ text,
2743
+ type: 'success'
2744
+ };
2745
+ };
2523
2746
  const getOpenApiErrorDetails = async response => {
2524
2747
  let parsed;
2525
2748
  try {
@@ -2543,7 +2766,14 @@ const getOpenApiErrorDetails = async response => {
2543
2766
  errorType: typeof errorType === 'string' ? errorType : undefined
2544
2767
  };
2545
2768
  };
2546
- const getOpenApiAssistantText = async (messages, modelId, openApiApiKey, openApiApiBaseUrl, assetDir, platform) => {
2769
+ const getOpenApiAssistantText = async (messages, modelId, openApiApiKey, openApiApiBaseUrl, assetDir, platform, options) => {
2770
+ const {
2771
+ includeObfuscation = false,
2772
+ onTextChunk,
2773
+ stream
2774
+ } = options ?? {
2775
+ stream: false
2776
+ };
2547
2777
  const completionMessages = messages.map(message => ({
2548
2778
  content: message.text,
2549
2779
  role: message.role
@@ -2553,16 +2783,23 @@ const getOpenApiAssistantText = async (messages, modelId, openApiApiKey, openApi
2553
2783
  for (let i = 0; i <= maxToolIterations; i++) {
2554
2784
  let response;
2555
2785
  try {
2556
- response = await fetch(getOpenApiApiEndpoint(openApiApiBaseUrl), {
2786
+ response = await fetch(getOpenApiApiEndpoint(openApiApiBaseUrl, stream), {
2557
2787
  body: JSON.stringify({
2558
2788
  messages: completionMessages,
2559
2789
  model: modelId,
2790
+ ...(stream ? {
2791
+ stream: true
2792
+ } : {}),
2793
+ ...(includeObfuscation ? {
2794
+ include_obfuscation: true
2795
+ } : {}),
2560
2796
  tool_choice: 'auto',
2561
2797
  tools
2562
2798
  }),
2563
2799
  headers: {
2564
2800
  Authorization: `Bearer ${openApiApiKey}`,
2565
- 'Content-Type': 'application/json'
2801
+ 'Content-Type': 'application/json',
2802
+ ...getClientRequestIdHeader()
2566
2803
  },
2567
2804
  method: 'POST'
2568
2805
  });
@@ -2587,6 +2824,9 @@ const getOpenApiAssistantText = async (messages, modelId, openApiApiKey, openApi
2587
2824
  type: 'error'
2588
2825
  };
2589
2826
  }
2827
+ if (stream) {
2828
+ return parseOpenApiStream(response, onTextChunk);
2829
+ }
2590
2830
  let parsed;
2591
2831
  try {
2592
2832
  parsed = await response.json();
@@ -2758,7 +2998,8 @@ const getOpenRouterLimitInfo = async (openRouterApiKey, openRouterApiBaseUrl) =>
2758
2998
  try {
2759
2999
  response = await fetch(getOpenRouterKeyEndpoint(openRouterApiBaseUrl), {
2760
3000
  headers: {
2761
- Authorization: `Bearer ${openRouterApiKey}`
3001
+ Authorization: `Bearer ${openRouterApiKey}`,
3002
+ ...getClientRequestIdHeader()
2762
3003
  },
2763
3004
  method: 'GET'
2764
3005
  });
@@ -2816,7 +3057,8 @@ const getOpenRouterAssistantText = async (messages, modelId, openRouterApiKey, o
2816
3057
  }),
2817
3058
  headers: {
2818
3059
  Authorization: `Bearer ${openRouterApiKey}`,
2819
- 'Content-Type': 'application/json'
3060
+ 'Content-Type': 'application/json',
3061
+ ...getClientRequestIdHeader()
2820
3062
  },
2821
3063
  method: 'POST'
2822
3064
  });
@@ -3001,12 +3243,15 @@ const getAiResponse = async ({
3001
3243
  mockApiCommandId,
3002
3244
  models,
3003
3245
  nextMessageId,
3246
+ onTextChunk,
3004
3247
  openApiApiBaseUrl,
3005
3248
  openApiApiKey,
3006
3249
  openRouterApiBaseUrl,
3007
3250
  openRouterApiKey,
3251
+ passIncludeObfuscation = false,
3008
3252
  platform,
3009
3253
  selectedModelId,
3254
+ streamingEnabled = false,
3010
3255
  useMockApi,
3011
3256
  userText
3012
3257
  }) => {
@@ -3014,8 +3259,22 @@ const getAiResponse = async ({
3014
3259
  const usesOpenApiModel = isOpenApiModel(selectedModelId, models);
3015
3260
  const usesOpenRouterModel = isOpenRouterModel(selectedModelId, models);
3016
3261
  if (usesOpenApiModel) {
3017
- if (openApiApiKey) {
3018
- const result = await getOpenApiAssistantText(messages, getOpenApiModelId(selectedModelId), openApiApiKey, openApiApiBaseUrl, assetDir, platform);
3262
+ if (useMockApi) {
3263
+ const result = await getMockOpenApiAssistantText(streamingEnabled, onTextChunk);
3264
+ if (result.type === 'success') {
3265
+ const {
3266
+ text: assistantText
3267
+ } = result;
3268
+ text = assistantText;
3269
+ } else {
3270
+ text = getOpenApiErrorMessage(result);
3271
+ }
3272
+ } else if (openApiApiKey) {
3273
+ const result = await getOpenApiAssistantText(messages, getOpenApiModelId(selectedModelId), openApiApiKey, openApiApiBaseUrl, assetDir, platform, {
3274
+ includeObfuscation: passIncludeObfuscation,
3275
+ onTextChunk,
3276
+ stream: streamingEnabled
3277
+ });
3019
3278
  if (result.type === 'success') {
3020
3279
  const {
3021
3280
  text: assistantText
@@ -3233,6 +3492,67 @@ const focusInput = state => {
3233
3492
  };
3234
3493
  };
3235
3494
 
3495
+ const updateMessageTextInSelectedSession = (sessions, selectedSessionId, messageId, text, inProgress) => {
3496
+ return sessions.map(session => {
3497
+ if (session.id !== selectedSessionId) {
3498
+ return session;
3499
+ }
3500
+ return {
3501
+ ...session,
3502
+ messages: session.messages.map(message => {
3503
+ if (message.id !== messageId) {
3504
+ return message;
3505
+ }
3506
+ return {
3507
+ ...message,
3508
+ inProgress,
3509
+ text
3510
+ };
3511
+ })
3512
+ };
3513
+ });
3514
+ };
3515
+ const handleTextChunkFunction = async (uid, assistantMessageId, chunk, handleTextChunkState) => {
3516
+ const selectedSession = handleTextChunkState.latestState.sessions.find(session => session.id === handleTextChunkState.latestState.selectedSessionId);
3517
+ if (!selectedSession) {
3518
+ return {
3519
+ latestState: handleTextChunkState.latestState,
3520
+ previousState: handleTextChunkState.previousState
3521
+ };
3522
+ }
3523
+ const assistantMessage = selectedSession.messages.find(message => message.id === assistantMessageId);
3524
+ if (!assistantMessage) {
3525
+ return {
3526
+ latestState: handleTextChunkState.latestState,
3527
+ previousState: handleTextChunkState.previousState
3528
+ };
3529
+ }
3530
+ const updatedText = assistantMessage.text + chunk;
3531
+ const updatedSessions = updateMessageTextInSelectedSession(handleTextChunkState.latestState.sessions, handleTextChunkState.latestState.selectedSessionId, assistantMessageId, updatedText, true);
3532
+ const nextState = {
3533
+ ...handleTextChunkState.latestState,
3534
+ sessions: updatedSessions
3535
+ };
3536
+ set(uid, handleTextChunkState.previousState, nextState);
3537
+ // @ts-ignore
3538
+ await invoke('Chat.rerender');
3539
+ return {
3540
+ latestState: nextState,
3541
+ previousState: nextState
3542
+ };
3543
+ };
3544
+
3545
+ const appendMessageToSelectedSession = (sessions, selectedSessionId, message) => {
3546
+ return sessions.map(session => {
3547
+ if (session.id !== selectedSessionId) {
3548
+ return session;
3549
+ }
3550
+ return {
3551
+ ...session,
3552
+ messages: [...session.messages, message]
3553
+ };
3554
+ });
3555
+ };
3236
3556
  const handleSubmit = async state => {
3237
3557
  const {
3238
3558
  assetDir,
@@ -3244,10 +3564,12 @@ const handleSubmit = async state => {
3244
3564
  openApiApiKey,
3245
3565
  openRouterApiBaseUrl,
3246
3566
  openRouterApiKey,
3567
+ passIncludeObfuscation,
3247
3568
  platform,
3248
3569
  selectedModelId,
3249
3570
  selectedSessionId,
3250
3571
  sessions,
3572
+ streamingEnabled,
3251
3573
  useMockApi,
3252
3574
  viewMode
3253
3575
  } = state;
@@ -3265,6 +3587,18 @@ const handleSubmit = async state => {
3265
3587
  text: userText,
3266
3588
  time: userTime
3267
3589
  };
3590
+ const assistantMessageId = `message-${nextMessageId + 1}`;
3591
+ const assistantTime = new Date().toLocaleTimeString([], {
3592
+ hour: '2-digit',
3593
+ minute: '2-digit'
3594
+ });
3595
+ const inProgressAssistantMessage = {
3596
+ id: assistantMessageId,
3597
+ inProgress: true,
3598
+ role: 'assistant',
3599
+ text: '',
3600
+ time: assistantTime
3601
+ };
3268
3602
  let workingSessions = sessions;
3269
3603
  if (viewMode === 'detail') {
3270
3604
  const loadedSession = await getChatSession(selectedSessionId);
@@ -3282,7 +3616,7 @@ const handleSubmit = async state => {
3282
3616
  const newSessionId = generateSessionId();
3283
3617
  const newSession = {
3284
3618
  id: newSessionId,
3285
- messages: [userMessage],
3619
+ messages: streamingEnabled ? [userMessage, inProgressAssistantMessage] : [userMessage],
3286
3620
  title: `Chat ${workingSessions.length + 1}`
3287
3621
  };
3288
3622
  await saveChatSession(newSession);
@@ -3298,15 +3632,8 @@ const handleSubmit = async state => {
3298
3632
  viewMode: 'detail'
3299
3633
  });
3300
3634
  } else {
3301
- const updatedSessions = workingSessions.map(session => {
3302
- if (session.id !== selectedSessionId) {
3303
- return session;
3304
- }
3305
- return {
3306
- ...session,
3307
- messages: [...session.messages, userMessage]
3308
- };
3309
- });
3635
+ const updatedWithUser = appendMessageToSelectedSession(workingSessions, selectedSessionId, userMessage);
3636
+ const updatedSessions = streamingEnabled ? appendMessageToSelectedSession(updatedWithUser, selectedSessionId, inProgressAssistantMessage) : updatedWithUser;
3310
3637
  const selectedSession = updatedSessions.find(session => session.id === selectedSessionId);
3311
3638
  if (selectedSession) {
3312
3639
  await saveChatSession(selectedSession);
@@ -3324,39 +3651,44 @@ const handleSubmit = async state => {
3324
3651
  set(state.uid, state, optimisticState);
3325
3652
  // @ts-ignore
3326
3653
  await invoke('Chat.rerender');
3654
+ let handleTextChunkState = {
3655
+ latestState: optimisticState,
3656
+ previousState: optimisticState
3657
+ };
3327
3658
  const selectedOptimisticSession = optimisticState.sessions.find(session => session.id === optimisticState.selectedSessionId);
3328
- const messages = selectedOptimisticSession?.messages ?? [];
3659
+ const messages = (selectedOptimisticSession?.messages ?? []).filter(message => !message.inProgress);
3660
+ const handleTextChunkFunctionRef = streamingEnabled ? async chunk => {
3661
+ handleTextChunkState = await handleTextChunkFunction(state.uid, assistantMessageId, chunk, handleTextChunkState);
3662
+ } : undefined;
3329
3663
  const assistantMessage = await getAiResponse({
3330
3664
  assetDir,
3331
3665
  messages,
3332
3666
  mockApiCommandId,
3333
3667
  models,
3334
3668
  nextMessageId: optimisticState.nextMessageId,
3669
+ onTextChunk: handleTextChunkFunctionRef,
3335
3670
  openApiApiBaseUrl,
3336
3671
  openApiApiKey,
3337
3672
  openRouterApiBaseUrl,
3338
3673
  openRouterApiKey,
3674
+ passIncludeObfuscation,
3339
3675
  platform,
3340
3676
  selectedModelId,
3677
+ streamingEnabled,
3341
3678
  useMockApi,
3342
3679
  userText
3343
3680
  });
3344
- const updatedSessions = optimisticState.sessions.map(session => {
3345
- if (session.id !== optimisticState.selectedSessionId) {
3346
- return session;
3347
- }
3348
- return {
3349
- ...session,
3350
- messages: [...session.messages, assistantMessage]
3351
- };
3352
- });
3353
- const selectedSession = updatedSessions.find(session => session.id === optimisticState.selectedSessionId);
3681
+ const {
3682
+ latestState
3683
+ } = handleTextChunkState;
3684
+ const updatedSessions = streamingEnabled ? updateMessageTextInSelectedSession(latestState.sessions, latestState.selectedSessionId, assistantMessageId, assistantMessage.text, false) : appendMessageToSelectedSession(latestState.sessions, latestState.selectedSessionId, assistantMessage);
3685
+ const selectedSession = updatedSessions.find(session => session.id === latestState.selectedSessionId);
3354
3686
  if (selectedSession) {
3355
3687
  await saveChatSession(selectedSession);
3356
3688
  }
3357
3689
  return focusInput({
3358
- ...optimisticState,
3359
- nextMessageId: optimisticState.nextMessageId + 1,
3690
+ ...latestState,
3691
+ nextMessageId: latestState.nextMessageId + 1,
3360
3692
  sessions: updatedSessions
3361
3693
  });
3362
3694
  };
@@ -3658,6 +3990,25 @@ const handleNewline = async state => {
3658
3990
  return handleInput(state, Composer, `${composerValue}\n`);
3659
3991
  };
3660
3992
 
3993
+ const handleChatListScroll = async (state, chatListScrollTop) => {
3994
+ if (state.chatListScrollTop === chatListScrollTop) {
3995
+ return state;
3996
+ }
3997
+ return {
3998
+ ...state,
3999
+ chatListScrollTop
4000
+ };
4001
+ };
4002
+ const handleMessagesScroll = async (state, messagesScrollTop) => {
4003
+ if (state.messagesScrollTop === messagesScrollTop) {
4004
+ return state;
4005
+ }
4006
+ return {
4007
+ ...state,
4008
+ messagesScrollTop
4009
+ };
4010
+ };
4011
+
3661
4012
  const id = 7201;
3662
4013
  const sendMessagePortToExtensionHostWorker = async port => {
3663
4014
  await sendMessagePortToExtensionHostWorker$1(port, id);
@@ -3684,6 +4035,32 @@ const isObject = value => {
3684
4035
  return typeof value === 'object' && value !== null;
3685
4036
  };
3686
4037
 
4038
+ const getSavedChatListScrollTop = savedState => {
4039
+ if (!isObject(savedState)) {
4040
+ return undefined;
4041
+ }
4042
+ const {
4043
+ chatListScrollTop
4044
+ } = savedState;
4045
+ if (typeof chatListScrollTop !== 'number') {
4046
+ return undefined;
4047
+ }
4048
+ return chatListScrollTop;
4049
+ };
4050
+
4051
+ const getSavedMessagesScrollTop = savedState => {
4052
+ if (!isObject(savedState)) {
4053
+ return undefined;
4054
+ }
4055
+ const {
4056
+ messagesScrollTop
4057
+ } = savedState;
4058
+ if (typeof messagesScrollTop !== 'number') {
4059
+ return undefined;
4060
+ }
4061
+ return messagesScrollTop;
4062
+ };
4063
+
3687
4064
  const getSavedSelectedModelId = savedState => {
3688
4065
  if (!isObject(savedState)) {
3689
4066
  return undefined;
@@ -3736,6 +4113,63 @@ const getSavedViewMode = savedState => {
3736
4113
  return viewMode;
3737
4114
  };
3738
4115
 
4116
+ const loadOpenApiApiKey = async () => {
4117
+ try {
4118
+ const savedOpenApiKey = await get('secrets.openApiKey');
4119
+ if (typeof savedOpenApiKey === 'string' && savedOpenApiKey) {
4120
+ return savedOpenApiKey;
4121
+ }
4122
+ const legacySavedOpenApiApiKey = await get('secrets.openApiApiKey');
4123
+ if (typeof legacySavedOpenApiApiKey === 'string' && legacySavedOpenApiApiKey) {
4124
+ return legacySavedOpenApiApiKey;
4125
+ }
4126
+ const legacySavedOpenAiApiKey = await get('secrets.openAiApiKey');
4127
+ return typeof legacySavedOpenAiApiKey === 'string' ? legacySavedOpenAiApiKey : '';
4128
+ } catch {
4129
+ return '';
4130
+ }
4131
+ };
4132
+
4133
+ const loadOpenRouterApiKey = async () => {
4134
+ try {
4135
+ const savedOpenRouterApiKey = await get('secrets.openRouterApiKey');
4136
+ return typeof savedOpenRouterApiKey === 'string' ? savedOpenRouterApiKey : '';
4137
+ } catch {
4138
+ return '';
4139
+ }
4140
+ };
4141
+
4142
+ const loadPassIncludeObfuscation = async () => {
4143
+ try {
4144
+ const savedPassIncludeObfuscation = await get('chatView.passIncludeObfuscation');
4145
+ return typeof savedPassIncludeObfuscation === 'boolean' ? savedPassIncludeObfuscation : false;
4146
+ } catch {
4147
+ return false;
4148
+ }
4149
+ };
4150
+
4151
+ const loadStreamingEnabled = async () => {
4152
+ try {
4153
+ const savedStreamingEnabled = await get('chatView.streamingEnabled');
4154
+ return typeof savedStreamingEnabled === 'boolean' ? savedStreamingEnabled : false;
4155
+ } catch {
4156
+ return false;
4157
+ }
4158
+ };
4159
+
4160
+ const loadPreferences = async () => {
4161
+ const openApiApiKey = await loadOpenApiApiKey();
4162
+ const openRouterApiKey = await loadOpenRouterApiKey();
4163
+ const streamingEnabled = await loadStreamingEnabled();
4164
+ const passIncludeObfuscation = await loadPassIncludeObfuscation();
4165
+ return {
4166
+ openApiApiKey,
4167
+ openRouterApiKey,
4168
+ passIncludeObfuscation,
4169
+ streamingEnabled
4170
+ };
4171
+ };
4172
+
3739
4173
  const toSummarySession = session => {
3740
4174
  return {
3741
4175
  id: session.id,
@@ -3761,30 +4195,12 @@ const loadSelectedSessionMessages = async (sessions, selectedSessionId) => {
3761
4195
  const loadContent = async (state, savedState) => {
3762
4196
  const savedSelectedModelId = getSavedSelectedModelId(savedState);
3763
4197
  const savedViewMode = getSavedViewMode(savedState);
3764
- let openApiApiKey = '';
3765
- try {
3766
- const savedOpenApiKey = await get('secrets.openApiKey');
3767
- if (typeof savedOpenApiKey === 'string' && savedOpenApiKey) {
3768
- openApiApiKey = savedOpenApiKey;
3769
- } else {
3770
- const legacySavedOpenApiApiKey = await get('secrets.openApiApiKey');
3771
- if (typeof legacySavedOpenApiApiKey === 'string' && legacySavedOpenApiApiKey) {
3772
- openApiApiKey = legacySavedOpenApiApiKey;
3773
- } else {
3774
- const legacySavedOpenAiApiKey = await get('secrets.openAiApiKey');
3775
- openApiApiKey = typeof legacySavedOpenAiApiKey === 'string' ? legacySavedOpenAiApiKey : '';
3776
- }
3777
- }
3778
- } catch {
3779
- openApiApiKey = '';
3780
- }
3781
- let openRouterApiKey = '';
3782
- try {
3783
- const savedOpenRouterApiKey = await get('secrets.openRouterApiKey');
3784
- openRouterApiKey = typeof savedOpenRouterApiKey === 'string' ? savedOpenRouterApiKey : '';
3785
- } catch {
3786
- openRouterApiKey = '';
3787
- }
4198
+ const {
4199
+ openApiApiKey,
4200
+ openRouterApiKey,
4201
+ passIncludeObfuscation,
4202
+ streamingEnabled
4203
+ } = await loadPreferences();
3788
4204
  const legacySavedSessions = getSavedSessions(savedState);
3789
4205
  const storedSessions = await listChatSessions();
3790
4206
  let sessions = storedSessions;
@@ -3802,6 +4218,8 @@ const loadContent = async (state, savedState) => {
3802
4218
  }
3803
4219
  const preferredSessionId = getSavedSelectedSessionId(savedState) || state.selectedSessionId;
3804
4220
  const preferredModelId = savedSelectedModelId || state.selectedModelId;
4221
+ const chatListScrollTop = getSavedChatListScrollTop(savedState) ?? state.chatListScrollTop;
4222
+ const messagesScrollTop = getSavedMessagesScrollTop(savedState) ?? state.messagesScrollTop;
3805
4223
  const selectedModelId = state.models.some(model => model.id === preferredModelId) ? preferredModelId : state.models[0]?.id || '';
3806
4224
  const selectedSessionId = sessions.some(session => session.id === preferredSessionId) ? preferredSessionId : sessions[0]?.id || '';
3807
4225
  sessions = await loadSelectedSessionMessages(sessions, selectedSessionId);
@@ -3809,18 +4227,37 @@ const loadContent = async (state, savedState) => {
3809
4227
  const viewMode = sessions.length === 0 || !selectedSessionId ? 'list' : preferredViewMode === 'detail' ? 'detail' : 'list';
3810
4228
  return {
3811
4229
  ...state,
4230
+ chatListScrollTop,
3812
4231
  initial: false,
4232
+ messagesScrollTop,
3813
4233
  openApiApiKey,
3814
4234
  openApiApiKeyInput: openApiApiKey,
3815
4235
  openRouterApiKey,
3816
4236
  openRouterApiKeyInput: openRouterApiKey,
4237
+ passIncludeObfuscation,
3817
4238
  selectedModelId,
3818
4239
  selectedSessionId,
3819
4240
  sessions,
4241
+ streamingEnabled,
3820
4242
  viewMode
3821
4243
  };
3822
4244
  };
3823
4245
 
4246
+ const mockOpenApiStreamFinish = state => {
4247
+ finish();
4248
+ return state;
4249
+ };
4250
+
4251
+ const mockOpenApiStreamPushChunk = (state, chunk) => {
4252
+ pushChunk(chunk);
4253
+ return state;
4254
+ };
4255
+
4256
+ const mockOpenApiStreamReset = state => {
4257
+ reset$1();
4258
+ return state;
4259
+ };
4260
+
3824
4261
  const openMockSession = async (state, mockSessionId, mockChatMessages) => {
3825
4262
  const {
3826
4263
  sessions: currentSessions
@@ -3855,9 +4292,13 @@ const openMockSession = async (state, mockSessionId, mockChatMessages) => {
3855
4292
  };
3856
4293
  };
3857
4294
 
3858
- const getCss = composerHeight => {
4295
+ const getCss = (composerHeight, listItemHeight, chatMessageFontSize, chatMessageLineHeight, chatMessageFontFamily) => {
3859
4296
  return `:root {
3860
4297
  --ChatInputBoxHeight: ${composerHeight}px;
4298
+ --ChatListItemHeight: ${listItemHeight}px;
4299
+ --ChatMessageFontSize: ${chatMessageFontSize}px;
4300
+ --ChatMessageLineHeight: ${chatMessageLineHeight}px;
4301
+ --ChatMessageFontFamily: ${chatMessageFontFamily};
3861
4302
  }`;
3862
4303
  };
3863
4304
 
@@ -3866,10 +4307,14 @@ const getCss = composerHeight => {
3866
4307
 
3867
4308
  const renderCss = (oldState, newState) => {
3868
4309
  const {
4310
+ chatMessageFontFamily,
4311
+ chatMessageFontSize,
4312
+ chatMessageLineHeight,
3869
4313
  composerHeight,
4314
+ listItemHeight,
3870
4315
  uid
3871
4316
  } = newState;
3872
- const css = getCss(composerHeight);
4317
+ const css = getCss(composerHeight, listItemHeight, chatMessageFontSize, chatMessageLineHeight, chatMessageFontFamily);
3873
4318
  return [SetCss, uid, css];
3874
4319
  };
3875
4320
 
@@ -3946,6 +4391,8 @@ const HandleClickList = 17;
3946
4391
  const HandleClickDelete = 18;
3947
4392
  const HandleSubmit = 19;
3948
4393
  const HandleModelChange = 20;
4394
+ const HandleChatListScroll = 21;
4395
+ const HandleMessagesScroll = 22;
3949
4396
 
3950
4397
  const getModelLabel = model => {
3951
4398
  if (model.provider === 'openRouter') {
@@ -4253,18 +4700,20 @@ const getEmptyMessagesDom = () => {
4253
4700
  }, text(startConversation())];
4254
4701
  };
4255
4702
 
4256
- const getMessagesDom = (messages, openRouterApiKeyInput, openApiApiKeyInput = '', openRouterApiKeyState = 'idle') => {
4703
+ const getMessagesDom = (messages, openRouterApiKeyInput, openApiApiKeyInput = '', openRouterApiKeyState = 'idle', messagesScrollTop = 0) => {
4257
4704
  if (messages.length === 0) {
4258
4705
  return getEmptyMessagesDom();
4259
4706
  }
4260
4707
  return [{
4261
4708
  childCount: messages.length,
4262
4709
  className: 'ChatMessages',
4710
+ onScroll: HandleMessagesScroll,
4711
+ scrollTop: messagesScrollTop,
4263
4712
  type: Div
4264
4713
  }, ...messages.flatMap(message => getChatMessageDom(message, openRouterApiKeyInput, openApiApiKeyInput, openRouterApiKeyState))];
4265
4714
  };
4266
4715
 
4267
- const getChatModeDetailVirtualDom = (sessions, selectedSessionId, composerValue, openRouterApiKeyInput, openApiApiKeyInput, models, selectedModelId, usageOverviewEnabled, tokensUsed, tokensMax, openRouterApiKeyState = 'idle', composerHeight = 28, composerFontSize = 13, composerFontFamily = 'system-ui', composerLineHeight = 20) => {
4716
+ const getChatModeDetailVirtualDom = (sessions, selectedSessionId, composerValue, openRouterApiKeyInput, openApiApiKeyInput, models, selectedModelId, usageOverviewEnabled, tokensUsed, tokensMax, openRouterApiKeyState = 'idle', composerHeight = 28, composerFontSize = 13, composerFontFamily = 'system-ui', composerLineHeight = 20, messagesScrollTop = 0) => {
4268
4717
  const selectedSession = sessions.find(session => session.id === selectedSessionId);
4269
4718
  const selectedSessionTitle = selectedSession?.title || chatTitle();
4270
4719
  const messages = selectedSession ? selectedSession.messages : [];
@@ -4272,7 +4721,7 @@ const getChatModeDetailVirtualDom = (sessions, selectedSessionId, composerValue,
4272
4721
  childCount: 3,
4273
4722
  className: mergeClassNames(Viewlet, Chat),
4274
4723
  type: Div
4275
- }, ...getChatHeaderDomDetailMode(selectedSessionTitle), ...getMessagesDom(messages, openRouterApiKeyInput, openApiApiKeyInput, openRouterApiKeyState), ...getChatSendAreaDom(composerValue, models, selectedModelId, usageOverviewEnabled, tokensUsed, tokensMax, composerHeight, composerFontSize, composerFontFamily, composerLineHeight)];
4724
+ }, ...getChatHeaderDomDetailMode(selectedSessionTitle), ...getMessagesDom(messages, openRouterApiKeyInput, openApiApiKeyInput, openRouterApiKeyState, messagesScrollTop), ...getChatSendAreaDom(composerValue, models, selectedModelId, usageOverviewEnabled, tokensUsed, tokensMax, composerHeight, composerFontSize, composerFontFamily, composerLineHeight)];
4276
4725
  };
4277
4726
 
4278
4727
  const getChatHeaderListModeDom = () => {
@@ -4329,7 +4778,7 @@ const getSessionDom = session => {
4329
4778
  }, text('🗑')];
4330
4779
  };
4331
4780
 
4332
- const getChatListDom = (sessions, selectedSessionId) => {
4781
+ const getChatListDom = (sessions, selectedSessionId, chatListScrollTop = 0) => {
4333
4782
  if (sessions.length === 0) {
4334
4783
  return getEmptyChatSessionsDom();
4335
4784
  }
@@ -4337,16 +4786,18 @@ const getChatListDom = (sessions, selectedSessionId) => {
4337
4786
  childCount: sessions.length,
4338
4787
  className: ChatList,
4339
4788
  onClick: HandleClickList,
4789
+ onScroll: HandleChatListScroll,
4790
+ scrollTop: chatListScrollTop,
4340
4791
  type: Div
4341
4792
  }, ...sessions.flatMap(getSessionDom)];
4342
4793
  };
4343
4794
 
4344
- const getChatModeListVirtualDom = (sessions, selectedSessionId, composerValue, models, selectedModelId, usageOverviewEnabled, tokensUsed, tokensMax, composerHeight = 28, composerFontSize = 13, composerFontFamily = 'system-ui', composerLineHeight = 20) => {
4795
+ const getChatModeListVirtualDom = (sessions, selectedSessionId, composerValue, models, selectedModelId, usageOverviewEnabled, tokensUsed, tokensMax, composerHeight = 28, composerFontSize = 13, composerFontFamily = 'system-ui', composerLineHeight = 20, chatListScrollTop = 0) => {
4345
4796
  return [{
4346
4797
  childCount: 3,
4347
4798
  className: mergeClassNames(Viewlet, Chat),
4348
4799
  type: Div
4349
- }, ...getChatHeaderListModeDom(), ...getChatListDom(sessions), ...getChatSendAreaDom(composerValue, models, selectedModelId, usageOverviewEnabled, tokensUsed, tokensMax, composerHeight, composerFontSize, composerFontFamily, composerLineHeight)];
4800
+ }, ...getChatHeaderListModeDom(), ...getChatListDom(sessions, selectedSessionId, chatListScrollTop), ...getChatSendAreaDom(composerValue, models, selectedModelId, usageOverviewEnabled, tokensUsed, tokensMax, composerHeight, composerFontSize, composerFontFamily, composerLineHeight)];
4350
4801
  };
4351
4802
 
4352
4803
  const getChatModeUnsupportedVirtualDom = () => {
@@ -4356,12 +4807,12 @@ const getChatModeUnsupportedVirtualDom = () => {
4356
4807
  }, text(unknownViewMode())];
4357
4808
  };
4358
4809
 
4359
- const getChatVirtualDom = (sessions, selectedSessionId, composerValue, openRouterApiKeyInput, viewMode, models, selectedModelId, usageOverviewEnabled, tokensUsed, tokensMax, openApiApiKeyInput = '', openRouterApiKeyState = 'idle', composerHeight = 28, composerFontSize = 13, composerFontFamily = 'system-ui', composerLineHeight = 20) => {
4810
+ const getChatVirtualDom = (sessions, selectedSessionId, composerValue, openRouterApiKeyInput, viewMode, models, selectedModelId, usageOverviewEnabled, tokensUsed, tokensMax, openApiApiKeyInput = '', openRouterApiKeyState = 'idle', composerHeight = 28, composerFontSize = 13, composerFontFamily = 'system-ui', composerLineHeight = 20, chatListScrollTop = 0, messagesScrollTop = 0) => {
4360
4811
  switch (viewMode) {
4361
4812
  case 'detail':
4362
- return getChatModeDetailVirtualDom(sessions, selectedSessionId, composerValue, openRouterApiKeyInput, openApiApiKeyInput, models, selectedModelId, usageOverviewEnabled, tokensUsed, tokensMax, openRouterApiKeyState, composerHeight, composerFontSize, composerFontFamily, composerLineHeight);
4813
+ return getChatModeDetailVirtualDom(sessions, selectedSessionId, composerValue, openRouterApiKeyInput, openApiApiKeyInput, models, selectedModelId, usageOverviewEnabled, tokensUsed, tokensMax, openRouterApiKeyState, composerHeight, composerFontSize, composerFontFamily, composerLineHeight, messagesScrollTop);
4363
4814
  case 'list':
4364
- return getChatModeListVirtualDom(sessions, selectedSessionId, composerValue, models, selectedModelId, usageOverviewEnabled, tokensUsed, tokensMax, composerHeight, composerFontSize, composerFontFamily, composerLineHeight);
4815
+ return getChatModeListVirtualDom(sessions, selectedSessionId, composerValue, models, selectedModelId, usageOverviewEnabled, tokensUsed, tokensMax, composerHeight, composerFontSize, composerFontFamily, composerLineHeight, chatListScrollTop);
4365
4816
  default:
4366
4817
  return getChatModeUnsupportedVirtualDom();
4367
4818
  }
@@ -4369,12 +4820,14 @@ const getChatVirtualDom = (sessions, selectedSessionId, composerValue, openRoute
4369
4820
 
4370
4821
  const renderItems = (oldState, newState) => {
4371
4822
  const {
4823
+ chatListScrollTop,
4372
4824
  composerFontFamily,
4373
4825
  composerFontSize,
4374
4826
  composerHeight,
4375
4827
  composerLineHeight,
4376
4828
  composerValue,
4377
4829
  initial,
4830
+ messagesScrollTop,
4378
4831
  models,
4379
4832
  openApiApiKeyInput,
4380
4833
  openRouterApiKeyInput,
@@ -4391,7 +4844,7 @@ const renderItems = (oldState, newState) => {
4391
4844
  if (initial) {
4392
4845
  return [SetDom2, uid, []];
4393
4846
  }
4394
- const dom = getChatVirtualDom(sessions, selectedSessionId, composerValue, openRouterApiKeyInput, viewMode, models, selectedModelId, usageOverviewEnabled, tokensUsed, tokensMax, openApiApiKeyInput, openRouterApiKeyState, composerHeight, composerFontSize, composerFontFamily, composerLineHeight);
4847
+ const dom = getChatVirtualDom(sessions, selectedSessionId, composerValue, openRouterApiKeyInput, viewMode, models, selectedModelId, usageOverviewEnabled, tokensUsed, tokensMax, openApiApiKeyInput, openRouterApiKeyState, composerHeight, composerFontSize, composerFontFamily, composerLineHeight, chatListScrollTop, messagesScrollTop);
4395
4848
  return [SetDom2, uid, dom];
4396
4849
  };
4397
4850
 
@@ -4402,6 +4855,14 @@ const renderIncremental = (oldState, newState) => {
4402
4855
  return [SetPatches, newState.uid, patches];
4403
4856
  };
4404
4857
 
4858
+ const renderScrollTop = (oldState, newState) => {
4859
+ const {
4860
+ messagesScrollTop,
4861
+ uid
4862
+ } = newState;
4863
+ return [SetProperty, uid, '.ChatMessages', 'scrollTop', messagesScrollTop];
4864
+ };
4865
+
4405
4866
  const renderValue = (oldState, newState) => {
4406
4867
  const {
4407
4868
  composerValue
@@ -4421,6 +4882,8 @@ const getRenderer = diffType => {
4421
4882
  return renderIncremental;
4422
4883
  case RenderItems:
4423
4884
  return renderItems;
4885
+ case RenderScrollTop:
4886
+ return renderScrollTop;
4424
4887
  case RenderValue:
4425
4888
  return renderValue;
4426
4889
  default:
@@ -4482,6 +4945,12 @@ const renderEventListeners = () => {
4482
4945
  }, {
4483
4946
  name: HandleModelChange,
4484
4947
  params: ['handleModelChange', TargetValue]
4948
+ }, {
4949
+ name: HandleChatListScroll,
4950
+ params: ['handleChatListScroll', 'event.target.scrollTop']
4951
+ }, {
4952
+ name: HandleMessagesScroll,
4953
+ params: ['handleMessagesScroll', 'event.target.scrollTop']
4485
4954
  }, {
4486
4955
  name: HandleFocus,
4487
4956
  params: ['handleInputFocus', TargetName]
@@ -4505,8 +4974,10 @@ const reset = async state => {
4505
4974
  composerHeight: getMinComposerHeightForState(state),
4506
4975
  composerValue: '',
4507
4976
  openRouterApiKey: '',
4977
+ selectedModelId: 'test',
4508
4978
  selectedSessionId: '',
4509
4979
  sessions: [],
4980
+ streamingEnabled: false,
4510
4981
  viewMode: 'list'
4511
4982
  };
4512
4983
  };
@@ -4520,8 +4991,10 @@ const resize = (state, dimensions) => {
4520
4991
 
4521
4992
  const saveState = state => {
4522
4993
  const {
4994
+ chatListScrollTop,
4523
4995
  composerValue,
4524
4996
  height,
4997
+ messagesScrollTop,
4525
4998
  nextMessageId,
4526
4999
  renamingSessionId,
4527
5000
  selectedModelId,
@@ -4532,8 +5005,10 @@ const saveState = state => {
4532
5005
  y
4533
5006
  } = state;
4534
5007
  return {
5008
+ chatListScrollTop,
4535
5009
  composerValue,
4536
5010
  height,
5011
+ messagesScrollTop,
4537
5012
  nextMessageId,
4538
5013
  renamingSessionId,
4539
5014
  selectedModelId,
@@ -4567,6 +5042,13 @@ const setChatList = state => {
4567
5042
  };
4568
5043
  };
4569
5044
 
5045
+ const setStreamingEnabled = (state, streamingEnabled) => {
5046
+ return {
5047
+ ...state,
5048
+ streamingEnabled
5049
+ };
5050
+ };
5051
+
4570
5052
  const defaultMockApiCommandId = 'ChatE2e.mockApi';
4571
5053
  const useMockApi = (state, value, mockApiCommandId = defaultMockApiCommandId) => {
4572
5054
  if (!value) {
@@ -4591,6 +5073,7 @@ const commandMap = {
4591
5073
  'Chat.getKeyBindings': getKeyBindings,
4592
5074
  'Chat.getSelectedSessionId': wrapGetter(getSelectedSessionId),
4593
5075
  'Chat.handleChatListContextMenu': handleChatListContextMenu,
5076
+ 'Chat.handleChatListScroll': wrapCommand(handleChatListScroll),
4594
5077
  'Chat.handleClick': wrapCommand(handleClick),
4595
5078
  'Chat.handleClickBack': wrapCommand(handleClickBack),
4596
5079
  'Chat.handleClickClose': handleClickClose,
@@ -4601,11 +5084,15 @@ const commandMap = {
4601
5084
  'Chat.handleInput': wrapCommand(handleInput),
4602
5085
  'Chat.handleInputFocus': wrapCommand(handleInputFocus),
4603
5086
  'Chat.handleKeyDown': wrapCommand(handleKeyDown),
5087
+ 'Chat.handleMessagesScroll': wrapCommand(handleMessagesScroll),
4604
5088
  'Chat.handleModelChange': wrapCommand(handleModelChange),
4605
5089
  'Chat.handleSubmit': wrapCommand(handleSubmit),
4606
5090
  'Chat.initialize': initialize,
4607
5091
  'Chat.loadContent': wrapCommand(loadContent),
4608
5092
  'Chat.loadContent2': wrapCommand(loadContent),
5093
+ 'Chat.mockOpenApiStreamFinish': wrapCommand(mockOpenApiStreamFinish),
5094
+ 'Chat.mockOpenApiStreamPushChunk': wrapCommand(mockOpenApiStreamPushChunk),
5095
+ 'Chat.mockOpenApiStreamReset': wrapCommand(mockOpenApiStreamReset),
4609
5096
  'Chat.openMockSession': wrapCommand(openMockSession),
4610
5097
  'Chat.render2': render2,
4611
5098
  'Chat.renderEventListeners': renderEventListeners,
@@ -4615,6 +5102,7 @@ const commandMap = {
4615
5102
  'Chat.saveState': wrapGetter(saveState),
4616
5103
  'Chat.setChatList': wrapCommand(setChatList),
4617
5104
  'Chat.setOpenRouterApiKey': wrapCommand(setOpenRouterApiKey),
5105
+ 'Chat.setStreamingEnabled': wrapCommand(setStreamingEnabled),
4618
5106
  'Chat.terminate': terminate,
4619
5107
  'Chat.useMockApi': wrapCommand(useMockApi)
4620
5108
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lvce-editor/chat-view",
3
- "version": "1.18.0",
3
+ "version": "1.20.0",
4
4
  "description": "Chat View Worker",
5
5
  "repository": {
6
6
  "type": "git",