@lvce-editor/chat-view 1.18.0 → 1.19.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 => {
@@ -1385,6 +1390,7 @@ const createDefaultState = () => {
1385
1390
  const composerLineHeight = 20;
1386
1391
  return {
1387
1392
  assetDir: '',
1393
+ chatListScrollTop: 0,
1388
1394
  composerFontFamily: 'system-ui',
1389
1395
  composerFontSize,
1390
1396
  composerHeight: composerLineHeight + 8,
@@ -1399,6 +1405,8 @@ const createDefaultState = () => {
1399
1405
  inputSource: 'script',
1400
1406
  lastSubmittedSessionId: '',
1401
1407
  listItemHeight: 40,
1408
+ maxComposerRows: 5,
1409
+ messagesScrollTop: 0,
1402
1410
  mockApiCommandId: '',
1403
1411
  models: getDefaultModels(),
1404
1412
  nextMessageId: 1,
@@ -1420,6 +1428,7 @@ const createDefaultState = () => {
1420
1428
  messages: [],
1421
1429
  title: defaultSessionTitle()
1422
1430
  }],
1431
+ streamingEnabled: false,
1423
1432
  tokensMax: 0,
1424
1433
  tokensUsed: 0,
1425
1434
  uid: 0,
@@ -1471,12 +1480,17 @@ const isEqual = (oldState, newState) => {
1471
1480
  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
1481
  };
1473
1482
 
1483
+ const diffScrollTop = (oldState, newState) => {
1484
+ return oldState.chatListScrollTop === newState.chatListScrollTop && oldState.messagesScrollTop === newState.messagesScrollTop;
1485
+ };
1486
+
1474
1487
  const RenderItems = 4;
1475
1488
  const RenderFocus = 6;
1476
1489
  const RenderFocusContext = 7;
1477
1490
  const RenderValue = 8;
1478
1491
  const RenderCss = 10;
1479
1492
  const RenderIncremental = 11;
1493
+ const RenderScrollTop = 12;
1480
1494
 
1481
1495
  const diffValue = (oldState, newState) => {
1482
1496
  if (oldState.composerValue === newState.composerValue) {
@@ -1485,8 +1499,8 @@ const diffValue = (oldState, newState) => {
1485
1499
  return newState.inputSource !== 'script';
1486
1500
  };
1487
1501
 
1488
- const modules = [isEqual, diffValue, diffFocus, isEqual$1, diffFocus];
1489
- const numbers = [RenderIncremental, RenderValue, RenderFocus, RenderCss, RenderFocusContext];
1502
+ const modules = [isEqual, diffValue, diffFocus, isEqual$1, diffFocus, diffScrollTop];
1503
+ const numbers = [RenderIncremental, RenderValue, RenderFocus, RenderCss, RenderFocusContext, RenderScrollTop];
1490
1504
 
1491
1505
  const diff = (oldState, newState) => {
1492
1506
  const diffResult = [];
@@ -1519,6 +1533,7 @@ const FocusSelector = 'Viewlet.focusSelector';
1519
1533
  const SetCss = 'Viewlet.setCss';
1520
1534
  const SetDom2 = 'Viewlet.setDom2';
1521
1535
  const SetFocusContext = 'Viewlet.setFocusContext';
1536
+ const SetProperty = 'Viewlet.setProperty';
1522
1537
  const SetValueByName = 'Viewlet.setValueByName';
1523
1538
  const SetPatches = 'Viewlet.setPatches';
1524
1539
 
@@ -2495,7 +2510,16 @@ const executeChatTool = async (name, rawArguments, options) => {
2495
2510
  });
2496
2511
  };
2497
2512
 
2498
- const getOpenApiApiEndpoint = openApiApiBaseUrl => {
2513
+ const getClientRequestIdHeader = () => {
2514
+ return {
2515
+ 'x-client-request-id': crypto.randomUUID()
2516
+ };
2517
+ };
2518
+
2519
+ const getOpenApiApiEndpoint = (openApiApiBaseUrl, stream) => {
2520
+ if (stream) {
2521
+ return `${openApiApiBaseUrl}/chat/completions?stream=true`;
2522
+ }
2499
2523
  return `${openApiApiBaseUrl}/chat/completions`;
2500
2524
  };
2501
2525
 
@@ -2520,6 +2544,148 @@ const getTextContent = content => {
2520
2544
  return textParts.join('\n');
2521
2545
  };
2522
2546
 
2547
+ const getStreamChunkText = content => {
2548
+ if (typeof content === 'string') {
2549
+ return content;
2550
+ }
2551
+ if (!Array.isArray(content)) {
2552
+ return '';
2553
+ }
2554
+ return content.map(part => {
2555
+ if (!part || typeof part !== 'object') {
2556
+ return '';
2557
+ }
2558
+ const text = Reflect.get(part, 'text');
2559
+ return typeof text === 'string' ? text : '';
2560
+ }).join('');
2561
+ };
2562
+ const parseSseEvent = eventChunk => {
2563
+ const lines = eventChunk.split('\n');
2564
+ const dataLines = [];
2565
+ for (const line of lines) {
2566
+ if (!line.startsWith('data:')) {
2567
+ continue;
2568
+ }
2569
+ dataLines.push(line.slice(5).trimStart());
2570
+ }
2571
+ return dataLines;
2572
+ };
2573
+ const parseOpenApiStream = async (response, onTextChunk) => {
2574
+ if (!response.body) {
2575
+ return {
2576
+ details: 'request-failed',
2577
+ type: 'error'
2578
+ };
2579
+ }
2580
+ const reader = response.body.getReader();
2581
+ const decoder = new TextDecoder();
2582
+ let remainder = '';
2583
+ let text = '';
2584
+ let done = false;
2585
+ while (!done) {
2586
+ const {
2587
+ done: streamDone,
2588
+ value
2589
+ } = await reader.read();
2590
+ if (streamDone) {
2591
+ done = true;
2592
+ } else if (value) {
2593
+ remainder += decoder.decode(value, {
2594
+ stream: true
2595
+ });
2596
+ }
2597
+ while (true) {
2598
+ const separatorIndex = remainder.indexOf('\n\n');
2599
+ if (separatorIndex === -1) {
2600
+ break;
2601
+ }
2602
+ const rawEvent = remainder.slice(0, separatorIndex);
2603
+ remainder = remainder.slice(separatorIndex + 2);
2604
+ const dataLines = parseSseEvent(rawEvent);
2605
+ if (dataLines.length === 0) {
2606
+ continue;
2607
+ }
2608
+ for (const line of dataLines) {
2609
+ if (line === '[DONE]') {
2610
+ done = true;
2611
+ break;
2612
+ }
2613
+ let parsed;
2614
+ try {
2615
+ parsed = JSON.parse(line);
2616
+ } catch {
2617
+ continue;
2618
+ }
2619
+ if (!parsed || typeof parsed !== 'object') {
2620
+ continue;
2621
+ }
2622
+ const choices = Reflect.get(parsed, 'choices');
2623
+ if (!Array.isArray(choices)) {
2624
+ continue;
2625
+ }
2626
+ const firstChoice = choices[0];
2627
+ if (!firstChoice || typeof firstChoice !== 'object') {
2628
+ continue;
2629
+ }
2630
+ const delta = Reflect.get(firstChoice, 'delta');
2631
+ if (!delta || typeof delta !== 'object') {
2632
+ continue;
2633
+ }
2634
+ const content = Reflect.get(delta, 'content');
2635
+ const chunkText = getStreamChunkText(content);
2636
+ if (!chunkText) {
2637
+ continue;
2638
+ }
2639
+ text += chunkText;
2640
+ if (onTextChunk) {
2641
+ await onTextChunk(chunkText);
2642
+ }
2643
+ }
2644
+ }
2645
+ }
2646
+ if (remainder) {
2647
+ const dataLines = parseSseEvent(remainder);
2648
+ for (const line of dataLines) {
2649
+ if (line === '[DONE]') {
2650
+ continue;
2651
+ }
2652
+ let parsed;
2653
+ try {
2654
+ parsed = JSON.parse(line);
2655
+ } catch {
2656
+ continue;
2657
+ }
2658
+ if (!parsed || typeof parsed !== 'object') {
2659
+ continue;
2660
+ }
2661
+ const choices = Reflect.get(parsed, 'choices');
2662
+ if (!Array.isArray(choices)) {
2663
+ continue;
2664
+ }
2665
+ const firstChoice = choices[0];
2666
+ if (!firstChoice || typeof firstChoice !== 'object') {
2667
+ continue;
2668
+ }
2669
+ const delta = Reflect.get(firstChoice, 'delta');
2670
+ if (!delta || typeof delta !== 'object') {
2671
+ continue;
2672
+ }
2673
+ const content = Reflect.get(delta, 'content');
2674
+ const chunkText = getStreamChunkText(content);
2675
+ if (!chunkText) {
2676
+ continue;
2677
+ }
2678
+ text += chunkText;
2679
+ if (onTextChunk) {
2680
+ await onTextChunk(chunkText);
2681
+ }
2682
+ }
2683
+ }
2684
+ return {
2685
+ text,
2686
+ type: 'success'
2687
+ };
2688
+ };
2523
2689
  const getOpenApiErrorDetails = async response => {
2524
2690
  let parsed;
2525
2691
  try {
@@ -2543,7 +2709,13 @@ const getOpenApiErrorDetails = async response => {
2543
2709
  errorType: typeof errorType === 'string' ? errorType : undefined
2544
2710
  };
2545
2711
  };
2546
- const getOpenApiAssistantText = async (messages, modelId, openApiApiKey, openApiApiBaseUrl, assetDir, platform) => {
2712
+ const getOpenApiAssistantText = async (messages, modelId, openApiApiKey, openApiApiBaseUrl, assetDir, platform, options) => {
2713
+ const {
2714
+ onTextChunk,
2715
+ stream
2716
+ } = options ?? {
2717
+ stream: false
2718
+ };
2547
2719
  const completionMessages = messages.map(message => ({
2548
2720
  content: message.text,
2549
2721
  role: message.role
@@ -2553,16 +2725,20 @@ const getOpenApiAssistantText = async (messages, modelId, openApiApiKey, openApi
2553
2725
  for (let i = 0; i <= maxToolIterations; i++) {
2554
2726
  let response;
2555
2727
  try {
2556
- response = await fetch(getOpenApiApiEndpoint(openApiApiBaseUrl), {
2728
+ response = await fetch(getOpenApiApiEndpoint(openApiApiBaseUrl, stream), {
2557
2729
  body: JSON.stringify({
2558
2730
  messages: completionMessages,
2559
2731
  model: modelId,
2732
+ ...(stream ? {
2733
+ stream: true
2734
+ } : {}),
2560
2735
  tool_choice: 'auto',
2561
2736
  tools
2562
2737
  }),
2563
2738
  headers: {
2564
2739
  Authorization: `Bearer ${openApiApiKey}`,
2565
- 'Content-Type': 'application/json'
2740
+ 'Content-Type': 'application/json',
2741
+ ...getClientRequestIdHeader()
2566
2742
  },
2567
2743
  method: 'POST'
2568
2744
  });
@@ -2587,6 +2763,9 @@ const getOpenApiAssistantText = async (messages, modelId, openApiApiKey, openApi
2587
2763
  type: 'error'
2588
2764
  };
2589
2765
  }
2766
+ if (stream) {
2767
+ return parseOpenApiStream(response, onTextChunk);
2768
+ }
2590
2769
  let parsed;
2591
2770
  try {
2592
2771
  parsed = await response.json();
@@ -2758,7 +2937,8 @@ const getOpenRouterLimitInfo = async (openRouterApiKey, openRouterApiBaseUrl) =>
2758
2937
  try {
2759
2938
  response = await fetch(getOpenRouterKeyEndpoint(openRouterApiBaseUrl), {
2760
2939
  headers: {
2761
- Authorization: `Bearer ${openRouterApiKey}`
2940
+ Authorization: `Bearer ${openRouterApiKey}`,
2941
+ ...getClientRequestIdHeader()
2762
2942
  },
2763
2943
  method: 'GET'
2764
2944
  });
@@ -2816,7 +2996,8 @@ const getOpenRouterAssistantText = async (messages, modelId, openRouterApiKey, o
2816
2996
  }),
2817
2997
  headers: {
2818
2998
  Authorization: `Bearer ${openRouterApiKey}`,
2819
- 'Content-Type': 'application/json'
2999
+ 'Content-Type': 'application/json',
3000
+ ...getClientRequestIdHeader()
2820
3001
  },
2821
3002
  method: 'POST'
2822
3003
  });
@@ -3001,12 +3182,14 @@ const getAiResponse = async ({
3001
3182
  mockApiCommandId,
3002
3183
  models,
3003
3184
  nextMessageId,
3185
+ onTextChunk,
3004
3186
  openApiApiBaseUrl,
3005
3187
  openApiApiKey,
3006
3188
  openRouterApiBaseUrl,
3007
3189
  openRouterApiKey,
3008
3190
  platform,
3009
3191
  selectedModelId,
3192
+ streamingEnabled = false,
3010
3193
  useMockApi,
3011
3194
  userText
3012
3195
  }) => {
@@ -3015,7 +3198,10 @@ const getAiResponse = async ({
3015
3198
  const usesOpenRouterModel = isOpenRouterModel(selectedModelId, models);
3016
3199
  if (usesOpenApiModel) {
3017
3200
  if (openApiApiKey) {
3018
- const result = await getOpenApiAssistantText(messages, getOpenApiModelId(selectedModelId), openApiApiKey, openApiApiBaseUrl, assetDir, platform);
3201
+ const result = await getOpenApiAssistantText(messages, getOpenApiModelId(selectedModelId), openApiApiKey, openApiApiBaseUrl, assetDir, platform, {
3202
+ onTextChunk,
3203
+ stream: streamingEnabled
3204
+ });
3019
3205
  if (result.type === 'success') {
3020
3206
  const {
3021
3207
  text: assistantText
@@ -3233,6 +3419,37 @@ const focusInput = state => {
3233
3419
  };
3234
3420
  };
3235
3421
 
3422
+ const appendMessageToSelectedSession = (sessions, selectedSessionId, message) => {
3423
+ return sessions.map(session => {
3424
+ if (session.id !== selectedSessionId) {
3425
+ return session;
3426
+ }
3427
+ return {
3428
+ ...session,
3429
+ messages: [...session.messages, message]
3430
+ };
3431
+ });
3432
+ };
3433
+ const updateMessageTextInSelectedSession = (sessions, selectedSessionId, messageId, text, inProgress) => {
3434
+ return sessions.map(session => {
3435
+ if (session.id !== selectedSessionId) {
3436
+ return session;
3437
+ }
3438
+ return {
3439
+ ...session,
3440
+ messages: session.messages.map(message => {
3441
+ if (message.id !== messageId) {
3442
+ return message;
3443
+ }
3444
+ return {
3445
+ ...message,
3446
+ inProgress,
3447
+ text
3448
+ };
3449
+ })
3450
+ };
3451
+ });
3452
+ };
3236
3453
  const handleSubmit = async state => {
3237
3454
  const {
3238
3455
  assetDir,
@@ -3248,6 +3465,7 @@ const handleSubmit = async state => {
3248
3465
  selectedModelId,
3249
3466
  selectedSessionId,
3250
3467
  sessions,
3468
+ streamingEnabled,
3251
3469
  useMockApi,
3252
3470
  viewMode
3253
3471
  } = state;
@@ -3265,6 +3483,18 @@ const handleSubmit = async state => {
3265
3483
  text: userText,
3266
3484
  time: userTime
3267
3485
  };
3486
+ const assistantMessageId = `message-${nextMessageId + 1}`;
3487
+ const assistantTime = new Date().toLocaleTimeString([], {
3488
+ hour: '2-digit',
3489
+ minute: '2-digit'
3490
+ });
3491
+ const inProgressAssistantMessage = {
3492
+ id: assistantMessageId,
3493
+ inProgress: true,
3494
+ role: 'assistant',
3495
+ text: '',
3496
+ time: assistantTime
3497
+ };
3268
3498
  let workingSessions = sessions;
3269
3499
  if (viewMode === 'detail') {
3270
3500
  const loadedSession = await getChatSession(selectedSessionId);
@@ -3282,7 +3512,7 @@ const handleSubmit = async state => {
3282
3512
  const newSessionId = generateSessionId();
3283
3513
  const newSession = {
3284
3514
  id: newSessionId,
3285
- messages: [userMessage],
3515
+ messages: streamingEnabled ? [userMessage, inProgressAssistantMessage] : [userMessage],
3286
3516
  title: `Chat ${workingSessions.length + 1}`
3287
3517
  };
3288
3518
  await saveChatSession(newSession);
@@ -3298,15 +3528,8 @@ const handleSubmit = async state => {
3298
3528
  viewMode: 'detail'
3299
3529
  });
3300
3530
  } 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
- });
3531
+ const updatedWithUser = appendMessageToSelectedSession(workingSessions, selectedSessionId, userMessage);
3532
+ const updatedSessions = streamingEnabled ? appendMessageToSelectedSession(updatedWithUser, selectedSessionId, inProgressAssistantMessage) : updatedWithUser;
3310
3533
  const selectedSession = updatedSessions.find(session => session.id === selectedSessionId);
3311
3534
  if (selectedSession) {
3312
3535
  await saveChatSession(selectedSession);
@@ -3324,39 +3547,56 @@ const handleSubmit = async state => {
3324
3547
  set(state.uid, state, optimisticState);
3325
3548
  // @ts-ignore
3326
3549
  await invoke('Chat.rerender');
3327
- const selectedOptimisticSession = optimisticState.sessions.find(session => session.id === optimisticState.selectedSessionId);
3328
- const messages = selectedOptimisticSession?.messages ?? [];
3550
+ let latestState = optimisticState;
3551
+ let previousState = optimisticState;
3552
+ const selectedOptimisticSession = latestState.sessions.find(session => session.id === latestState.selectedSessionId);
3553
+ const messages = (selectedOptimisticSession?.messages ?? []).filter(message => !message.inProgress);
3554
+ const onTextChunk = streamingEnabled ? async chunk => {
3555
+ const selectedSession = latestState.sessions.find(session => session.id === latestState.selectedSessionId);
3556
+ if (!selectedSession) {
3557
+ return;
3558
+ }
3559
+ const assistantMessage = selectedSession.messages.find(message => message.id === assistantMessageId);
3560
+ if (!assistantMessage) {
3561
+ return;
3562
+ }
3563
+ const updatedText = assistantMessage.text + chunk;
3564
+ const updatedSessions = updateMessageTextInSelectedSession(latestState.sessions, latestState.selectedSessionId, assistantMessageId, updatedText, true);
3565
+ const nextState = {
3566
+ ...latestState,
3567
+ sessions: updatedSessions
3568
+ };
3569
+ set(state.uid, previousState, nextState);
3570
+ previousState = nextState;
3571
+ latestState = nextState;
3572
+ // @ts-ignore
3573
+ await invoke('Chat.rerender');
3574
+ } : undefined;
3329
3575
  const assistantMessage = await getAiResponse({
3330
3576
  assetDir,
3331
3577
  messages,
3332
3578
  mockApiCommandId,
3333
3579
  models,
3334
3580
  nextMessageId: optimisticState.nextMessageId,
3581
+ onTextChunk,
3335
3582
  openApiApiBaseUrl,
3336
3583
  openApiApiKey,
3337
3584
  openRouterApiBaseUrl,
3338
3585
  openRouterApiKey,
3339
3586
  platform,
3340
3587
  selectedModelId,
3588
+ streamingEnabled,
3341
3589
  useMockApi,
3342
3590
  userText
3343
3591
  });
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);
3592
+ const updatedSessions = streamingEnabled ? updateMessageTextInSelectedSession(latestState.sessions, latestState.selectedSessionId, assistantMessageId, assistantMessage.text, false) : appendMessageToSelectedSession(latestState.sessions, latestState.selectedSessionId, assistantMessage);
3593
+ const selectedSession = updatedSessions.find(session => session.id === latestState.selectedSessionId);
3354
3594
  if (selectedSession) {
3355
3595
  await saveChatSession(selectedSession);
3356
3596
  }
3357
3597
  return focusInput({
3358
- ...optimisticState,
3359
- nextMessageId: optimisticState.nextMessageId + 1,
3598
+ ...latestState,
3599
+ nextMessageId: latestState.nextMessageId + 1,
3360
3600
  sessions: updatedSessions
3361
3601
  });
3362
3602
  };
@@ -3658,6 +3898,25 @@ const handleNewline = async state => {
3658
3898
  return handleInput(state, Composer, `${composerValue}\n`);
3659
3899
  };
3660
3900
 
3901
+ const handleChatListScroll = async (state, chatListScrollTop) => {
3902
+ if (state.chatListScrollTop === chatListScrollTop) {
3903
+ return state;
3904
+ }
3905
+ return {
3906
+ ...state,
3907
+ chatListScrollTop
3908
+ };
3909
+ };
3910
+ const handleMessagesScroll = async (state, messagesScrollTop) => {
3911
+ if (state.messagesScrollTop === messagesScrollTop) {
3912
+ return state;
3913
+ }
3914
+ return {
3915
+ ...state,
3916
+ messagesScrollTop
3917
+ };
3918
+ };
3919
+
3661
3920
  const id = 7201;
3662
3921
  const sendMessagePortToExtensionHostWorker = async port => {
3663
3922
  await sendMessagePortToExtensionHostWorker$1(port, id);
@@ -3684,6 +3943,32 @@ const isObject = value => {
3684
3943
  return typeof value === 'object' && value !== null;
3685
3944
  };
3686
3945
 
3946
+ const getSavedChatListScrollTop = savedState => {
3947
+ if (!isObject(savedState)) {
3948
+ return undefined;
3949
+ }
3950
+ const {
3951
+ chatListScrollTop
3952
+ } = savedState;
3953
+ if (typeof chatListScrollTop !== 'number') {
3954
+ return undefined;
3955
+ }
3956
+ return chatListScrollTop;
3957
+ };
3958
+
3959
+ const getSavedMessagesScrollTop = savedState => {
3960
+ if (!isObject(savedState)) {
3961
+ return undefined;
3962
+ }
3963
+ const {
3964
+ messagesScrollTop
3965
+ } = savedState;
3966
+ if (typeof messagesScrollTop !== 'number') {
3967
+ return undefined;
3968
+ }
3969
+ return messagesScrollTop;
3970
+ };
3971
+
3687
3972
  const getSavedSelectedModelId = savedState => {
3688
3973
  if (!isObject(savedState)) {
3689
3974
  return undefined;
@@ -3736,6 +4021,39 @@ const getSavedViewMode = savedState => {
3736
4021
  return viewMode;
3737
4022
  };
3738
4023
 
4024
+ const loadOpenApiApiKey = async () => {
4025
+ try {
4026
+ const savedOpenApiKey = await get('secrets.openApiKey');
4027
+ if (typeof savedOpenApiKey === 'string' && savedOpenApiKey) {
4028
+ return savedOpenApiKey;
4029
+ }
4030
+ const legacySavedOpenApiApiKey = await get('secrets.openApiApiKey');
4031
+ if (typeof legacySavedOpenApiApiKey === 'string' && legacySavedOpenApiApiKey) {
4032
+ return legacySavedOpenApiApiKey;
4033
+ }
4034
+ const legacySavedOpenAiApiKey = await get('secrets.openAiApiKey');
4035
+ return typeof legacySavedOpenAiApiKey === 'string' ? legacySavedOpenAiApiKey : '';
4036
+ } catch {
4037
+ return '';
4038
+ }
4039
+ };
4040
+ const loadOpenRouterApiKey = async () => {
4041
+ try {
4042
+ const savedOpenRouterApiKey = await get('secrets.openRouterApiKey');
4043
+ return typeof savedOpenRouterApiKey === 'string' ? savedOpenRouterApiKey : '';
4044
+ } catch {
4045
+ return '';
4046
+ }
4047
+ };
4048
+ const loadPreferences = async () => {
4049
+ const openApiApiKey = await loadOpenApiApiKey();
4050
+ const openRouterApiKey = await loadOpenRouterApiKey();
4051
+ return {
4052
+ openApiApiKey,
4053
+ openRouterApiKey
4054
+ };
4055
+ };
4056
+
3739
4057
  const toSummarySession = session => {
3740
4058
  return {
3741
4059
  id: session.id,
@@ -3761,30 +4079,10 @@ const loadSelectedSessionMessages = async (sessions, selectedSessionId) => {
3761
4079
  const loadContent = async (state, savedState) => {
3762
4080
  const savedSelectedModelId = getSavedSelectedModelId(savedState);
3763
4081
  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
- }
4082
+ const {
4083
+ openApiApiKey,
4084
+ openRouterApiKey
4085
+ } = await loadPreferences();
3788
4086
  const legacySavedSessions = getSavedSessions(savedState);
3789
4087
  const storedSessions = await listChatSessions();
3790
4088
  let sessions = storedSessions;
@@ -3802,6 +4100,8 @@ const loadContent = async (state, savedState) => {
3802
4100
  }
3803
4101
  const preferredSessionId = getSavedSelectedSessionId(savedState) || state.selectedSessionId;
3804
4102
  const preferredModelId = savedSelectedModelId || state.selectedModelId;
4103
+ const chatListScrollTop = getSavedChatListScrollTop(savedState) ?? state.chatListScrollTop;
4104
+ const messagesScrollTop = getSavedMessagesScrollTop(savedState) ?? state.messagesScrollTop;
3805
4105
  const selectedModelId = state.models.some(model => model.id === preferredModelId) ? preferredModelId : state.models[0]?.id || '';
3806
4106
  const selectedSessionId = sessions.some(session => session.id === preferredSessionId) ? preferredSessionId : sessions[0]?.id || '';
3807
4107
  sessions = await loadSelectedSessionMessages(sessions, selectedSessionId);
@@ -3809,7 +4109,9 @@ const loadContent = async (state, savedState) => {
3809
4109
  const viewMode = sessions.length === 0 || !selectedSessionId ? 'list' : preferredViewMode === 'detail' ? 'detail' : 'list';
3810
4110
  return {
3811
4111
  ...state,
4112
+ chatListScrollTop,
3812
4113
  initial: false,
4114
+ messagesScrollTop,
3813
4115
  openApiApiKey,
3814
4116
  openApiApiKeyInput: openApiApiKey,
3815
4117
  openRouterApiKey,
@@ -3946,6 +4248,8 @@ const HandleClickList = 17;
3946
4248
  const HandleClickDelete = 18;
3947
4249
  const HandleSubmit = 19;
3948
4250
  const HandleModelChange = 20;
4251
+ const HandleChatListScroll = 21;
4252
+ const HandleMessagesScroll = 22;
3949
4253
 
3950
4254
  const getModelLabel = model => {
3951
4255
  if (model.provider === 'openRouter') {
@@ -4253,18 +4557,20 @@ const getEmptyMessagesDom = () => {
4253
4557
  }, text(startConversation())];
4254
4558
  };
4255
4559
 
4256
- const getMessagesDom = (messages, openRouterApiKeyInput, openApiApiKeyInput = '', openRouterApiKeyState = 'idle') => {
4560
+ const getMessagesDom = (messages, openRouterApiKeyInput, openApiApiKeyInput = '', openRouterApiKeyState = 'idle', messagesScrollTop = 0) => {
4257
4561
  if (messages.length === 0) {
4258
4562
  return getEmptyMessagesDom();
4259
4563
  }
4260
4564
  return [{
4261
4565
  childCount: messages.length,
4262
4566
  className: 'ChatMessages',
4567
+ onScroll: HandleMessagesScroll,
4568
+ scrollTop: messagesScrollTop,
4263
4569
  type: Div
4264
4570
  }, ...messages.flatMap(message => getChatMessageDom(message, openRouterApiKeyInput, openApiApiKeyInput, openRouterApiKeyState))];
4265
4571
  };
4266
4572
 
4267
- const getChatModeDetailVirtualDom = (sessions, selectedSessionId, composerValue, openRouterApiKeyInput, openApiApiKeyInput, models, selectedModelId, usageOverviewEnabled, tokensUsed, tokensMax, openRouterApiKeyState = 'idle', composerHeight = 28, composerFontSize = 13, composerFontFamily = 'system-ui', composerLineHeight = 20) => {
4573
+ 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
4574
  const selectedSession = sessions.find(session => session.id === selectedSessionId);
4269
4575
  const selectedSessionTitle = selectedSession?.title || chatTitle();
4270
4576
  const messages = selectedSession ? selectedSession.messages : [];
@@ -4272,7 +4578,7 @@ const getChatModeDetailVirtualDom = (sessions, selectedSessionId, composerValue,
4272
4578
  childCount: 3,
4273
4579
  className: mergeClassNames(Viewlet, Chat),
4274
4580
  type: Div
4275
- }, ...getChatHeaderDomDetailMode(selectedSessionTitle), ...getMessagesDom(messages, openRouterApiKeyInput, openApiApiKeyInput, openRouterApiKeyState), ...getChatSendAreaDom(composerValue, models, selectedModelId, usageOverviewEnabled, tokensUsed, tokensMax, composerHeight, composerFontSize, composerFontFamily, composerLineHeight)];
4581
+ }, ...getChatHeaderDomDetailMode(selectedSessionTitle), ...getMessagesDom(messages, openRouterApiKeyInput, openApiApiKeyInput, openRouterApiKeyState, messagesScrollTop), ...getChatSendAreaDom(composerValue, models, selectedModelId, usageOverviewEnabled, tokensUsed, tokensMax, composerHeight, composerFontSize, composerFontFamily, composerLineHeight)];
4276
4582
  };
4277
4583
 
4278
4584
  const getChatHeaderListModeDom = () => {
@@ -4329,7 +4635,7 @@ const getSessionDom = session => {
4329
4635
  }, text('🗑')];
4330
4636
  };
4331
4637
 
4332
- const getChatListDom = (sessions, selectedSessionId) => {
4638
+ const getChatListDom = (sessions, selectedSessionId, chatListScrollTop = 0) => {
4333
4639
  if (sessions.length === 0) {
4334
4640
  return getEmptyChatSessionsDom();
4335
4641
  }
@@ -4337,16 +4643,18 @@ const getChatListDom = (sessions, selectedSessionId) => {
4337
4643
  childCount: sessions.length,
4338
4644
  className: ChatList,
4339
4645
  onClick: HandleClickList,
4646
+ onScroll: HandleChatListScroll,
4647
+ scrollTop: chatListScrollTop,
4340
4648
  type: Div
4341
4649
  }, ...sessions.flatMap(getSessionDom)];
4342
4650
  };
4343
4651
 
4344
- const getChatModeListVirtualDom = (sessions, selectedSessionId, composerValue, models, selectedModelId, usageOverviewEnabled, tokensUsed, tokensMax, composerHeight = 28, composerFontSize = 13, composerFontFamily = 'system-ui', composerLineHeight = 20) => {
4652
+ const getChatModeListVirtualDom = (sessions, selectedSessionId, composerValue, models, selectedModelId, usageOverviewEnabled, tokensUsed, tokensMax, composerHeight = 28, composerFontSize = 13, composerFontFamily = 'system-ui', composerLineHeight = 20, chatListScrollTop = 0) => {
4345
4653
  return [{
4346
4654
  childCount: 3,
4347
4655
  className: mergeClassNames(Viewlet, Chat),
4348
4656
  type: Div
4349
- }, ...getChatHeaderListModeDom(), ...getChatListDom(sessions), ...getChatSendAreaDom(composerValue, models, selectedModelId, usageOverviewEnabled, tokensUsed, tokensMax, composerHeight, composerFontSize, composerFontFamily, composerLineHeight)];
4657
+ }, ...getChatHeaderListModeDom(), ...getChatListDom(sessions, selectedSessionId, chatListScrollTop), ...getChatSendAreaDom(composerValue, models, selectedModelId, usageOverviewEnabled, tokensUsed, tokensMax, composerHeight, composerFontSize, composerFontFamily, composerLineHeight)];
4350
4658
  };
4351
4659
 
4352
4660
  const getChatModeUnsupportedVirtualDom = () => {
@@ -4356,12 +4664,12 @@ const getChatModeUnsupportedVirtualDom = () => {
4356
4664
  }, text(unknownViewMode())];
4357
4665
  };
4358
4666
 
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) => {
4667
+ 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
4668
  switch (viewMode) {
4361
4669
  case 'detail':
4362
- return getChatModeDetailVirtualDom(sessions, selectedSessionId, composerValue, openRouterApiKeyInput, openApiApiKeyInput, models, selectedModelId, usageOverviewEnabled, tokensUsed, tokensMax, openRouterApiKeyState, composerHeight, composerFontSize, composerFontFamily, composerLineHeight);
4670
+ return getChatModeDetailVirtualDom(sessions, selectedSessionId, composerValue, openRouterApiKeyInput, openApiApiKeyInput, models, selectedModelId, usageOverviewEnabled, tokensUsed, tokensMax, openRouterApiKeyState, composerHeight, composerFontSize, composerFontFamily, composerLineHeight, messagesScrollTop);
4363
4671
  case 'list':
4364
- return getChatModeListVirtualDom(sessions, selectedSessionId, composerValue, models, selectedModelId, usageOverviewEnabled, tokensUsed, tokensMax, composerHeight, composerFontSize, composerFontFamily, composerLineHeight);
4672
+ return getChatModeListVirtualDom(sessions, selectedSessionId, composerValue, models, selectedModelId, usageOverviewEnabled, tokensUsed, tokensMax, composerHeight, composerFontSize, composerFontFamily, composerLineHeight, chatListScrollTop);
4365
4673
  default:
4366
4674
  return getChatModeUnsupportedVirtualDom();
4367
4675
  }
@@ -4369,12 +4677,14 @@ const getChatVirtualDom = (sessions, selectedSessionId, composerValue, openRoute
4369
4677
 
4370
4678
  const renderItems = (oldState, newState) => {
4371
4679
  const {
4680
+ chatListScrollTop,
4372
4681
  composerFontFamily,
4373
4682
  composerFontSize,
4374
4683
  composerHeight,
4375
4684
  composerLineHeight,
4376
4685
  composerValue,
4377
4686
  initial,
4687
+ messagesScrollTop,
4378
4688
  models,
4379
4689
  openApiApiKeyInput,
4380
4690
  openRouterApiKeyInput,
@@ -4391,7 +4701,7 @@ const renderItems = (oldState, newState) => {
4391
4701
  if (initial) {
4392
4702
  return [SetDom2, uid, []];
4393
4703
  }
4394
- const dom = getChatVirtualDom(sessions, selectedSessionId, composerValue, openRouterApiKeyInput, viewMode, models, selectedModelId, usageOverviewEnabled, tokensUsed, tokensMax, openApiApiKeyInput, openRouterApiKeyState, composerHeight, composerFontSize, composerFontFamily, composerLineHeight);
4704
+ const dom = getChatVirtualDom(sessions, selectedSessionId, composerValue, openRouterApiKeyInput, viewMode, models, selectedModelId, usageOverviewEnabled, tokensUsed, tokensMax, openApiApiKeyInput, openRouterApiKeyState, composerHeight, composerFontSize, composerFontFamily, composerLineHeight, chatListScrollTop, messagesScrollTop);
4395
4705
  return [SetDom2, uid, dom];
4396
4706
  };
4397
4707
 
@@ -4402,6 +4712,14 @@ const renderIncremental = (oldState, newState) => {
4402
4712
  return [SetPatches, newState.uid, patches];
4403
4713
  };
4404
4714
 
4715
+ const renderScrollTop = (oldState, newState) => {
4716
+ const {
4717
+ messagesScrollTop,
4718
+ uid
4719
+ } = newState;
4720
+ return [SetProperty, uid, '.ChatMessages', 'scrollTop', messagesScrollTop];
4721
+ };
4722
+
4405
4723
  const renderValue = (oldState, newState) => {
4406
4724
  const {
4407
4725
  composerValue
@@ -4421,6 +4739,8 @@ const getRenderer = diffType => {
4421
4739
  return renderIncremental;
4422
4740
  case RenderItems:
4423
4741
  return renderItems;
4742
+ case RenderScrollTop:
4743
+ return renderScrollTop;
4424
4744
  case RenderValue:
4425
4745
  return renderValue;
4426
4746
  default:
@@ -4482,6 +4802,12 @@ const renderEventListeners = () => {
4482
4802
  }, {
4483
4803
  name: HandleModelChange,
4484
4804
  params: ['handleModelChange', TargetValue]
4805
+ }, {
4806
+ name: HandleChatListScroll,
4807
+ params: ['handleChatListScroll', 'event.target.scrollTop']
4808
+ }, {
4809
+ name: HandleMessagesScroll,
4810
+ params: ['handleMessagesScroll', 'event.target.scrollTop']
4485
4811
  }, {
4486
4812
  name: HandleFocus,
4487
4813
  params: ['handleInputFocus', TargetName]
@@ -4520,8 +4846,10 @@ const resize = (state, dimensions) => {
4520
4846
 
4521
4847
  const saveState = state => {
4522
4848
  const {
4849
+ chatListScrollTop,
4523
4850
  composerValue,
4524
4851
  height,
4852
+ messagesScrollTop,
4525
4853
  nextMessageId,
4526
4854
  renamingSessionId,
4527
4855
  selectedModelId,
@@ -4532,8 +4860,10 @@ const saveState = state => {
4532
4860
  y
4533
4861
  } = state;
4534
4862
  return {
4863
+ chatListScrollTop,
4535
4864
  composerValue,
4536
4865
  height,
4866
+ messagesScrollTop,
4537
4867
  nextMessageId,
4538
4868
  renamingSessionId,
4539
4869
  selectedModelId,
@@ -4591,6 +4921,7 @@ const commandMap = {
4591
4921
  'Chat.getKeyBindings': getKeyBindings,
4592
4922
  'Chat.getSelectedSessionId': wrapGetter(getSelectedSessionId),
4593
4923
  'Chat.handleChatListContextMenu': handleChatListContextMenu,
4924
+ 'Chat.handleChatListScroll': wrapCommand(handleChatListScroll),
4594
4925
  'Chat.handleClick': wrapCommand(handleClick),
4595
4926
  'Chat.handleClickBack': wrapCommand(handleClickBack),
4596
4927
  'Chat.handleClickClose': handleClickClose,
@@ -4601,6 +4932,7 @@ const commandMap = {
4601
4932
  'Chat.handleInput': wrapCommand(handleInput),
4602
4933
  'Chat.handleInputFocus': wrapCommand(handleInputFocus),
4603
4934
  'Chat.handleKeyDown': wrapCommand(handleKeyDown),
4935
+ 'Chat.handleMessagesScroll': wrapCommand(handleMessagesScroll),
4604
4936
  'Chat.handleModelChange': wrapCommand(handleModelChange),
4605
4937
  'Chat.handleSubmit': wrapCommand(handleSubmit),
4606
4938
  'Chat.initialize': initialize,
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.19.0",
4
4
  "description": "Chat View Worker",
5
5
  "repository": {
6
6
  "type": "git",