@lvce-editor/chat-view 3.10.0 → 4.0.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.
@@ -1122,6 +1122,10 @@ const sendMessagePortToExtensionHostWorker$1 = async (port, rpcId = 0) => {
1122
1122
  const sendMessagePortToChatNetworkWorker = async port => {
1123
1123
  await invokeAndTransfer('SendMessagePortToExtensionHostWorker.sendMessagePortToChatNetworkWorker', port, 'HandleMessagePort.handleMessagePort');
1124
1124
  };
1125
+ const confirm = async (message, options) => {
1126
+ const result = await invoke('ConfirmPrompt.prompt', message, options);
1127
+ return result;
1128
+ };
1125
1129
  const activateByEvent$1 = (event, assetDir, platform) => {
1126
1130
  return invoke('ExtensionHostManagement.activateByEvent', event, assetDir, platform);
1127
1131
  };
@@ -1336,6 +1340,12 @@ const backToChats = () => {
1336
1340
  const settings = () => {
1337
1341
  return i18nString('Settings');
1338
1342
  };
1343
+ const chatFocusMode = () => {
1344
+ return i18nString('Switch to chat focus mode');
1345
+ };
1346
+ const normalChatMode = () => {
1347
+ return i18nString('Switch to normal chat mode');
1348
+ };
1339
1349
  const closeChat = () => {
1340
1350
  return i18nString('Close Chat');
1341
1351
  };
@@ -1476,6 +1486,7 @@ const getDefaultModels = () => {
1476
1486
  };
1477
1487
 
1478
1488
  const createDefaultState = () => {
1489
+ const defaultProjectId = 'project-1';
1479
1490
  const defaultSessionId = 'session-1';
1480
1491
  const defaultModelId = 'test';
1481
1492
  const chatMessageFontSize = 13;
@@ -1504,6 +1515,7 @@ const createDefaultState = () => {
1504
1515
  height: 0,
1505
1516
  initial: true,
1506
1517
  inputSource: 'script',
1518
+ lastNormalViewMode: 'list',
1507
1519
  lastSubmittedSessionId: '',
1508
1520
  listItemHeight: 40,
1509
1521
  maxComposerRows: 5,
@@ -1523,12 +1535,21 @@ const createDefaultState = () => {
1523
1535
  openRouterApiKeyState: 'idle',
1524
1536
  passIncludeObfuscation: false,
1525
1537
  platform: 0,
1538
+ projectExpandedIds: [defaultProjectId],
1539
+ projectListScrollTop: 0,
1540
+ projects: [{
1541
+ id: defaultProjectId,
1542
+ name: '_blank',
1543
+ uri: ''
1544
+ }],
1526
1545
  renamingSessionId: '',
1527
1546
  selectedModelId: defaultModelId,
1547
+ selectedProjectId: defaultProjectId,
1528
1548
  selectedSessionId: defaultSessionId,
1529
1549
  sessions: [{
1530
1550
  id: defaultSessionId,
1531
1551
  messages: [],
1552
+ projectId: defaultProjectId,
1532
1553
  title: defaultSessionTitle()
1533
1554
  }],
1534
1555
  streamingEnabled: true,
@@ -2148,29 +2169,49 @@ const createDefaultStorage = () => {
2148
2169
  let chatSessionStorage = createDefaultStorage();
2149
2170
  const listChatSessions = async () => {
2150
2171
  const sessions = await chatSessionStorage.listSessions();
2151
- return sessions.map(session => ({
2152
- id: session.id,
2153
- messages: [],
2154
- title: session.title
2155
- }));
2172
+ return sessions.map(session => {
2173
+ const summary = {
2174
+ id: session.id,
2175
+ messages: [],
2176
+ title: session.title
2177
+ };
2178
+ if (!session.projectId) {
2179
+ return summary;
2180
+ }
2181
+ return {
2182
+ ...summary,
2183
+ projectId: session.projectId
2184
+ };
2185
+ });
2156
2186
  };
2157
2187
  const getChatSession = async id => {
2158
2188
  const session = await chatSessionStorage.getSession(id);
2159
2189
  if (!session) {
2160
2190
  return undefined;
2161
2191
  }
2162
- return {
2192
+ const result = {
2163
2193
  id: session.id,
2164
2194
  messages: [...session.messages],
2165
2195
  title: session.title
2166
2196
  };
2197
+ if (!session.projectId) {
2198
+ return result;
2199
+ }
2200
+ return {
2201
+ ...result,
2202
+ projectId: session.projectId
2203
+ };
2167
2204
  };
2168
2205
  const saveChatSession = async session => {
2169
- await chatSessionStorage.setSession({
2206
+ const value = {
2170
2207
  id: session.id,
2171
2208
  messages: [...session.messages],
2172
2209
  title: session.title
2173
- });
2210
+ };
2211
+ await chatSessionStorage.setSession(session.projectId ? {
2212
+ ...value,
2213
+ projectId: session.projectId
2214
+ } : value);
2174
2215
  };
2175
2216
  const deleteChatSession = async id => {
2176
2217
  await chatSessionStorage.deleteSession(id);
@@ -2194,6 +2235,17 @@ const getNextSelectedSessionId = (sessions, deletedId) => {
2194
2235
  return sessions[nextIndex].id;
2195
2236
  };
2196
2237
 
2238
+ const getVisibleSessions = (sessions, selectedProjectId) => {
2239
+ if (!selectedProjectId) {
2240
+ return sessions;
2241
+ }
2242
+ const hasAssignedProjects = sessions.some(session => !!session.projectId);
2243
+ if (!hasAssignedProjects) {
2244
+ return sessions;
2245
+ }
2246
+ return sessions.filter(session => session.projectId === selectedProjectId);
2247
+ };
2248
+
2197
2249
  const deleteSession = async (state, id) => {
2198
2250
  const {
2199
2251
  renamingSessionId,
@@ -2232,10 +2284,8 @@ const deleteSession = async (state, id) => {
2232
2284
  };
2233
2285
  };
2234
2286
  const deleteSessionAtIndex = async (state, index) => {
2235
- const {
2236
- sessions
2237
- } = state;
2238
- const session = sessions[index];
2287
+ const visibleSessions = getVisibleSessions(state.sessions, state.selectedProjectId);
2288
+ const session = visibleSessions[index];
2239
2289
  if (!session) {
2240
2290
  return state;
2241
2291
  }
@@ -2301,12 +2351,23 @@ const diffFocus = (oldState, newState) => {
2301
2351
  return oldState.focus === newState.focus && oldState.focused === newState.focused;
2302
2352
  };
2303
2353
 
2354
+ const isEqualProjectExpandedIds = (a, b) => {
2355
+ if (a.length !== b.length) {
2356
+ return false;
2357
+ }
2358
+ for (let i = 0; i < a.length; i++) {
2359
+ if (a[i] !== b[i]) {
2360
+ return false;
2361
+ }
2362
+ }
2363
+ return true;
2364
+ };
2304
2365
  const isEqual = (oldState, newState) => {
2305
- return oldState.composerDropActive === newState.composerDropActive && oldState.composerDropEnabled === newState.composerDropEnabled && 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;
2366
+ return oldState.composerDropActive === newState.composerDropActive && oldState.composerDropEnabled === newState.composerDropEnabled && oldState.composerValue === newState.composerValue && oldState.initial === newState.initial && isEqualProjectExpandedIds(oldState.projectExpandedIds, newState.projectExpandedIds) && oldState.projectListScrollTop === newState.projectListScrollTop && oldState.renamingSessionId === newState.renamingSessionId && oldState.selectedModelId === newState.selectedModelId && oldState.selectedProjectId === newState.selectedProjectId && oldState.selectedSessionId === newState.selectedSessionId && oldState.sessions === newState.sessions && oldState.tokensMax === newState.tokensMax && oldState.tokensUsed === newState.tokensUsed && oldState.usageOverviewEnabled === newState.usageOverviewEnabled && oldState.viewMode === newState.viewMode;
2306
2367
  };
2307
2368
 
2308
2369
  const diffScrollTop = (oldState, newState) => {
2309
- return oldState.chatListScrollTop === newState.chatListScrollTop && oldState.messagesScrollTop === newState.messagesScrollTop;
2370
+ return oldState.chatListScrollTop === newState.chatListScrollTop && oldState.messagesScrollTop === newState.messagesScrollTop && oldState.projectListScrollTop === newState.projectListScrollTop;
2310
2371
  };
2311
2372
 
2312
2373
  const RenderItems = 4;
@@ -2745,7 +2806,8 @@ const handleChatListContextMenu = async (state, eventX, eventY) => {
2745
2806
  if (index === -1) {
2746
2807
  return state;
2747
2808
  }
2748
- const item = state.sessions[index];
2809
+ const visibleSessions = getVisibleSessions(state.sessions, state.selectedProjectId);
2810
+ const item = visibleSessions[index];
2749
2811
  if (!item) {
2750
2812
  return state;
2751
2813
  }
@@ -2757,22 +2819,106 @@ const generateSessionId = () => {
2757
2819
  return crypto.randomUUID();
2758
2820
  };
2759
2821
 
2760
- const createSession = async state => {
2822
+ const createSession = async (state, projectIdOverride = '') => {
2761
2823
  const id = generateSessionId();
2824
+ const fallbackProjectId = state.projects.find(project => project.name === '_blank')?.id || state.projects[0]?.id || 'project-1';
2825
+ const projectId = projectIdOverride || state.selectedProjectId || fallbackProjectId;
2762
2826
  const session = {
2763
2827
  id,
2764
2828
  messages: [],
2829
+ projectId,
2765
2830
  title: `Chat ${state.sessions.length + 1}`
2766
2831
  };
2767
2832
  await saveChatSession(session);
2768
2833
  return {
2769
2834
  ...state,
2835
+ projectExpandedIds: state.projectExpandedIds.includes(projectId) ? state.projectExpandedIds : [...state.projectExpandedIds, projectId],
2770
2836
  renamingSessionId: '',
2837
+ selectedProjectId: projectId,
2771
2838
  selectedSessionId: id,
2772
2839
  sessions: [...state.sessions, session]
2773
2840
  };
2774
2841
  };
2775
2842
 
2843
+ const selectProject = async (state, projectId) => {
2844
+ if (!projectId || state.selectedProjectId === projectId) {
2845
+ return state;
2846
+ }
2847
+ const visibleSessions = getVisibleSessions(state.sessions, projectId);
2848
+ if (visibleSessions.length === 0) {
2849
+ return {
2850
+ ...state,
2851
+ selectedProjectId: projectId,
2852
+ selectedSessionId: '',
2853
+ viewMode: state.viewMode === 'chat-focus' ? 'chat-focus' : 'list'
2854
+ };
2855
+ }
2856
+ const currentSessionVisible = visibleSessions.some(session => session.id === state.selectedSessionId);
2857
+ const nextSelectedSessionId = currentSessionVisible ? state.selectedSessionId : visibleSessions[0].id;
2858
+ const loadedSession = await getChatSession(nextSelectedSessionId);
2859
+ const sessions = state.sessions.map(session => {
2860
+ if (session.id !== nextSelectedSessionId || !loadedSession) {
2861
+ return session;
2862
+ }
2863
+ return loadedSession;
2864
+ });
2865
+ return {
2866
+ ...state,
2867
+ selectedProjectId: projectId,
2868
+ selectedSessionId: nextSelectedSessionId,
2869
+ sessions,
2870
+ viewMode: state.viewMode === 'chat-focus' ? 'chat-focus' : 'detail'
2871
+ };
2872
+ };
2873
+
2874
+ const getProjectName = (uri, fallbackIndex) => {
2875
+ if (!uri) {
2876
+ return `Project ${fallbackIndex}`;
2877
+ }
2878
+ const withoutScheme = uri.replace(/^file:\/\//, '');
2879
+ const normalized = withoutScheme.replace(/\/+$/, '');
2880
+ const lastSegment = normalized.split('/').pop();
2881
+ if (!lastSegment) {
2882
+ return `Project ${fallbackIndex}`;
2883
+ }
2884
+ return decodeURIComponent(lastSegment);
2885
+ };
2886
+ const getNextProjectId = projects => {
2887
+ return `project-${projects.length + 1}`;
2888
+ };
2889
+ const pickProjectUri = async () => {
2890
+ try {
2891
+ const workspaceUri = await getWorkspacePath();
2892
+ return workspaceUri;
2893
+ } catch {
2894
+ return '';
2895
+ }
2896
+ };
2897
+ const handleClickCreateProject = async state => {
2898
+ await confirm('project added');
2899
+ const uri = await pickProjectUri();
2900
+ if (!uri) {
2901
+ return state;
2902
+ }
2903
+ const existingProject = state.projects.find(project => project.uri === uri);
2904
+ if (existingProject) {
2905
+ return selectProject(state, existingProject.id);
2906
+ }
2907
+ const id = getNextProjectId(state.projects);
2908
+ const project = {
2909
+ id,
2910
+ name: getProjectName(uri, state.projects.length + 1),
2911
+ uri
2912
+ };
2913
+ return {
2914
+ ...state,
2915
+ projects: [...state.projects, project],
2916
+ selectedProjectId: project.id,
2917
+ selectedSessionId: '',
2918
+ viewMode: 'list'
2919
+ };
2920
+ };
2921
+
2776
2922
  const handleClickOpenApiApiKeySettings = async state => {
2777
2923
  await invoke('Main.openUri', 'app://settings.json');
2778
2924
  return state;
@@ -5822,13 +5968,35 @@ const ComposerDropTarget = 'composer-drop-target';
5822
5968
  const Send = 'send';
5823
5969
  const Back = 'back';
5824
5970
  const Model = 'model';
5971
+ const ToggleChatFocus = 'toggle-chat-focus';
5972
+ const CreateProject = 'create-project';
5825
5973
  const CreateSession = 'create-session';
5974
+ const CreateSessionInProjectPrefix = 'create-session-in-project:';
5826
5975
  const SessionDebug = 'session-debug';
5827
5976
  const Settings = 'settings';
5828
5977
  const CloseChat = 'close-chat';
5829
5978
  const SessionDelete = 'SessionDelete';
5979
+ const ProjectPrefix = 'project:';
5830
5980
  const SessionPrefix = 'session:';
5831
5981
  const RenamePrefix = 'session-rename:';
5982
+ const getProjectInputName = projectId => {
5983
+ return `${ProjectPrefix}${projectId}`;
5984
+ };
5985
+ const getCreateSessionInProjectInputName = projectId => {
5986
+ return `${CreateSessionInProjectPrefix}${projectId}`;
5987
+ };
5988
+ const isCreateSessionInProjectInputName = name => {
5989
+ return name.startsWith(CreateSessionInProjectPrefix);
5990
+ };
5991
+ const getProjectIdFromCreateSessionInProjectInputName = name => {
5992
+ return name.slice(CreateSessionInProjectPrefix.length);
5993
+ };
5994
+ const isProjectInputName = name => {
5995
+ return name.startsWith(ProjectPrefix);
5996
+ };
5997
+ const getProjectIdFromInputName = name => {
5998
+ return name.slice(ProjectPrefix.length);
5999
+ };
5832
6000
  const getSessionInputName = sessionId => {
5833
6001
  return `${SessionPrefix}${sessionId}`;
5834
6002
  };
@@ -5873,10 +6041,11 @@ const selectSession = async (state, id) => {
5873
6041
  });
5874
6042
  return {
5875
6043
  ...state,
6044
+ lastNormalViewMode: state.viewMode === 'chat-focus' ? state.lastNormalViewMode : 'detail',
5876
6045
  renamingSessionId: '',
5877
6046
  selectedSessionId: id,
5878
6047
  sessions,
5879
- viewMode: 'detail'
6048
+ viewMode: state.viewMode === 'chat-focus' ? 'chat-focus' : 'detail'
5880
6049
  };
5881
6050
  };
5882
6051
 
@@ -5897,14 +6066,61 @@ const startRename = (state, id) => {
5897
6066
  };
5898
6067
  };
5899
6068
 
6069
+ const toggleChatFocusMode = async state => {
6070
+ if (state.viewMode === 'chat-focus') {
6071
+ return {
6072
+ ...state,
6073
+ viewMode: state.lastNormalViewMode
6074
+ };
6075
+ }
6076
+ if (state.viewMode === 'list' || state.viewMode === 'detail') {
6077
+ return {
6078
+ ...state,
6079
+ lastNormalViewMode: state.viewMode,
6080
+ viewMode: 'chat-focus'
6081
+ };
6082
+ }
6083
+ return state;
6084
+ };
6085
+
6086
+ const toggleProjectExpanded = async (state, projectId) => {
6087
+ const isExpanded = state.projectExpandedIds.includes(projectId);
6088
+ const projectExpandedIds = isExpanded ? state.projectExpandedIds.filter(id => id !== projectId) : [...state.projectExpandedIds, projectId];
6089
+ const visibleSessions = getVisibleSessions(state.sessions, projectId);
6090
+ if (visibleSessions.length === 0) {
6091
+ return {
6092
+ ...state,
6093
+ projectExpandedIds,
6094
+ selectedProjectId: projectId,
6095
+ selectedSessionId: '',
6096
+ viewMode: 'chat-focus'
6097
+ };
6098
+ }
6099
+ const selectedSessionVisible = visibleSessions.some(session => session.id === state.selectedSessionId);
6100
+ const selectedSessionId = selectedSessionVisible ? state.selectedSessionId : visibleSessions[0].id;
6101
+ const loadedSession = await getChatSession(selectedSessionId);
6102
+ const sessions = state.sessions.map(session => {
6103
+ if (session.id !== selectedSessionId || !loadedSession) {
6104
+ return session;
6105
+ }
6106
+ return loadedSession;
6107
+ });
6108
+ return {
6109
+ ...state,
6110
+ projectExpandedIds,
6111
+ selectedProjectId: projectId,
6112
+ selectedSessionId,
6113
+ sessions,
6114
+ viewMode: 'chat-focus'
6115
+ };
6116
+ };
6117
+
5900
6118
  const selectListIndex = async (state, index) => {
5901
- const {
5902
- sessions
5903
- } = state;
5904
- if (index < 0 || index >= sessions.length) {
6119
+ const visibleSessions = getVisibleSessions(state.sessions, state.selectedProjectId);
6120
+ if (index < 0 || index >= visibleSessions.length) {
5905
6121
  return state;
5906
6122
  }
5907
- const session = sessions[index];
6123
+ const session = visibleSessions[index];
5908
6124
  return selectSession(state, session.id);
5909
6125
  };
5910
6126
 
@@ -5920,6 +6136,23 @@ const handleClick = async (state, name, id = '') => {
5920
6136
  if (name === CreateSession) {
5921
6137
  return createSession(state);
5922
6138
  }
6139
+ if (name === CreateProject) {
6140
+ return handleClickCreateProject(state);
6141
+ }
6142
+ if (isCreateSessionInProjectInputName(name)) {
6143
+ const projectId = getProjectIdFromCreateSessionInProjectInputName(name);
6144
+ return createSession(state, projectId);
6145
+ }
6146
+ if (name === ToggleChatFocus) {
6147
+ return toggleChatFocusMode(state);
6148
+ }
6149
+ if (isProjectInputName(name)) {
6150
+ const projectId = getProjectIdFromInputName(name);
6151
+ if (state.viewMode === 'chat-focus') {
6152
+ return toggleProjectExpanded(state, projectId);
6153
+ }
6154
+ return selectProject(state, projectId);
6155
+ }
5923
6156
  if (isSessionInputName(name)) {
5924
6157
  const sessionId = getSessionIdFromInputName(name);
5925
6158
  return selectSession(state, sessionId);
@@ -5958,6 +6191,7 @@ const handleClick = async (state, name, id = '') => {
5958
6191
  const handleClickBack = async state => {
5959
6192
  return {
5960
6193
  ...state,
6194
+ lastNormalViewMode: 'list',
5961
6195
  renamingSessionId: '',
5962
6196
  viewMode: 'list'
5963
6197
  };
@@ -6239,6 +6473,15 @@ const handleMessagesScroll = async (state, messagesScrollTop) => {
6239
6473
  messagesScrollTop
6240
6474
  };
6241
6475
  };
6476
+ const handleProjectListScroll = async (state, projectListScrollTop) => {
6477
+ if (state.projectListScrollTop === projectListScrollTop) {
6478
+ return state;
6479
+ }
6480
+ return {
6481
+ ...state,
6482
+ projectListScrollTop
6483
+ };
6484
+ };
6242
6485
 
6243
6486
  const id = 7201;
6244
6487
  const sendMessagePortToExtensionHostWorker = async port => {
@@ -6338,7 +6581,7 @@ const getSavedViewMode = savedState => {
6338
6581
  const {
6339
6582
  viewMode
6340
6583
  } = savedState;
6341
- if (viewMode !== 'list' && viewMode !== 'detail') {
6584
+ if (viewMode !== 'list' && viewMode !== 'detail' && viewMode !== 'chat-focus') {
6342
6585
  return undefined;
6343
6586
  }
6344
6587
  return viewMode;
@@ -6455,11 +6698,97 @@ const loadSelectedSessionMessages = async (sessions, selectedSessionId) => {
6455
6698
  };
6456
6699
 
6457
6700
  const toSummarySession = session => {
6458
- return {
6701
+ const summary = {
6459
6702
  id: session.id,
6460
6703
  messages: [],
6461
6704
  title: session.title
6462
6705
  };
6706
+ if (!session.projectId) {
6707
+ return summary;
6708
+ }
6709
+ return {
6710
+ ...summary,
6711
+ projectId: session.projectId
6712
+ };
6713
+ };
6714
+ const getSavedSelectedProjectId = savedState => {
6715
+ if (!isObject(savedState)) {
6716
+ return undefined;
6717
+ }
6718
+ const {
6719
+ selectedProjectId
6720
+ } = savedState;
6721
+ if (typeof selectedProjectId !== 'string') {
6722
+ return undefined;
6723
+ }
6724
+ return selectedProjectId;
6725
+ };
6726
+ const getSavedProjects = savedState => {
6727
+ if (!isObject(savedState)) {
6728
+ return undefined;
6729
+ }
6730
+ const {
6731
+ projects
6732
+ } = savedState;
6733
+ if (!Array.isArray(projects)) {
6734
+ return undefined;
6735
+ }
6736
+ const validProjects = projects.filter(project => {
6737
+ if (!isObject(project)) {
6738
+ return false;
6739
+ }
6740
+ return typeof project.id === 'string' && typeof project.name === 'string' && typeof project.uri === 'string';
6741
+ });
6742
+ if (validProjects.length === 0) {
6743
+ return undefined;
6744
+ }
6745
+ return validProjects;
6746
+ };
6747
+ const ensureBlankProject = (projects, fallbackBlankProject) => {
6748
+ if (projects.some(project => project.name === '_blank')) {
6749
+ return projects;
6750
+ }
6751
+ return [fallbackBlankProject, ...projects];
6752
+ };
6753
+ const getSavedProjectListScrollTop = savedState => {
6754
+ if (!isObject(savedState)) {
6755
+ return undefined;
6756
+ }
6757
+ const {
6758
+ projectListScrollTop
6759
+ } = savedState;
6760
+ if (typeof projectListScrollTop !== 'number') {
6761
+ return undefined;
6762
+ }
6763
+ return projectListScrollTop;
6764
+ };
6765
+ const getSavedProjectExpandedIds = savedState => {
6766
+ if (!isObject(savedState)) {
6767
+ return undefined;
6768
+ }
6769
+ const {
6770
+ projectExpandedIds
6771
+ } = savedState;
6772
+ if (!Array.isArray(projectExpandedIds)) {
6773
+ return undefined;
6774
+ }
6775
+ const ids = projectExpandedIds.filter(id => typeof id === 'string');
6776
+ if (ids.length === 0) {
6777
+ return undefined;
6778
+ }
6779
+ return ids;
6780
+ };
6781
+ const getSavedLastNormalViewMode = savedState => {
6782
+ if (!isObject(savedState)) {
6783
+ return undefined;
6784
+ }
6785
+ const {
6786
+ lastNormalViewMode
6787
+ } = savedState;
6788
+ if (lastNormalViewMode !== 'list' && lastNormalViewMode !== 'detail') {
6789
+ return undefined;
6790
+ }
6791
+ return lastNormalViewMode;
6463
6792
  };
6464
6793
  const loadContent = async (state, savedState) => {
6465
6794
  const savedSelectedModelId = getSavedSelectedModelId(savedState);
@@ -6490,14 +6819,30 @@ const loadContent = async (state, savedState) => {
6490
6819
  sessions = state.sessions.map(toSummarySession);
6491
6820
  }
6492
6821
  const preferredSessionId = getSavedSelectedSessionId(savedState) || state.selectedSessionId;
6822
+ const savedProjects = getSavedProjects(savedState);
6823
+ const baseProjects = savedProjects && savedProjects.length > 0 ? savedProjects : state.projects;
6824
+ const blankProject = state.projects.find(project => project.name === '_blank') || {
6825
+ id: 'project-blank',
6826
+ name: '_blank',
6827
+ uri: ''
6828
+ };
6829
+ const projects = ensureBlankProject(baseProjects, blankProject);
6830
+ const preferredProjectId = getSavedSelectedProjectId(savedState) || state.selectedProjectId;
6831
+ const selectedProjectId = projects.some(project => project.id === preferredProjectId) ? preferredProjectId : projects[0]?.id || '';
6493
6832
  const preferredModelId = savedSelectedModelId || state.selectedModelId;
6494
6833
  const chatListScrollTop = getSavedChatListScrollTop(savedState) ?? state.chatListScrollTop;
6495
6834
  const messagesScrollTop = getSavedMessagesScrollTop(savedState) ?? state.messagesScrollTop;
6835
+ const projectListScrollTop = getSavedProjectListScrollTop(savedState) ?? state.projectListScrollTop;
6836
+ const savedProjectExpandedIds = getSavedProjectExpandedIds(savedState);
6837
+ const projectExpandedIds = (savedProjectExpandedIds || state.projectExpandedIds).filter(id => projects.some(project => project.id === id));
6496
6838
  const selectedModelId = state.models.some(model => model.id === preferredModelId) ? preferredModelId : state.models[0]?.id || '';
6497
- const selectedSessionId = sessions.some(session => session.id === preferredSessionId) ? preferredSessionId : sessions[0]?.id || '';
6839
+ const visibleSessions = getVisibleSessions(sessions, selectedProjectId);
6840
+ const selectedSessionId = visibleSessions.some(session => session.id === preferredSessionId) ? preferredSessionId : visibleSessions[0]?.id || '';
6498
6841
  sessions = await loadSelectedSessionMessages(sessions, selectedSessionId);
6499
6842
  const preferredViewMode = savedViewMode || state.viewMode;
6500
- const viewMode = sessions.length === 0 || !selectedSessionId ? 'list' : preferredViewMode === 'detail' ? 'detail' : 'list';
6843
+ const savedLastNormalViewMode = getSavedLastNormalViewMode(savedState);
6844
+ const lastNormalViewMode = savedLastNormalViewMode || (preferredViewMode === 'detail' ? 'detail' : state.lastNormalViewMode);
6845
+ const viewMode = sessions.length === 0 || !selectedSessionId ? 'list' : preferredViewMode;
6501
6846
  return {
6502
6847
  ...state,
6503
6848
  aiSessionTitleGenerationEnabled,
@@ -6506,13 +6851,18 @@ const loadContent = async (state, savedState) => {
6506
6851
  composerDropEnabled,
6507
6852
  emitStreamingFunctionCallEvents,
6508
6853
  initial: false,
6854
+ lastNormalViewMode,
6509
6855
  messagesScrollTop,
6510
6856
  openApiApiKey,
6511
6857
  openApiApiKeyInput: openApiApiKey,
6512
6858
  openRouterApiKey,
6513
6859
  openRouterApiKeyInput: openRouterApiKey,
6514
6860
  passIncludeObfuscation,
6861
+ projectExpandedIds,
6862
+ projectListScrollTop,
6863
+ projects,
6515
6864
  selectedModelId,
6865
+ selectedProjectId,
6516
6866
  selectedSessionId,
6517
6867
  sessions,
6518
6868
  streamingEnabled,
@@ -6599,6 +6949,219 @@ const getCss = (composerHeight, listItemHeight, chatMessageFontSize, chatMessage
6599
6949
  --ChatMessageLineHeight: ${chatMessageLineHeight}px;
6600
6950
  --ChatMessageFontFamily: ${chatMessageFontFamily};
6601
6951
  }
6952
+
6953
+ .Viewlet.Chat.ChatFocus {
6954
+ background: linear-gradient(180deg, var(--ColorViewBackground, #1d2229) 0%, #1f252d 100%);
6955
+ display: grid;
6956
+ grid-template-columns: 320px 1fr;
6957
+ grid-template-rows: auto 1fr auto;
6958
+ }
6959
+
6960
+ .Chat.ChatFocus .ChatHeader {
6961
+ grid-column: 1 / 3;
6962
+ }
6963
+
6964
+ .Chat.ChatFocus .ProjectSidebar {
6965
+ background: color-mix(in srgb, var(--ColorSideBarBackground, #232b35) 88%, #1f2b38 12%);
6966
+ border-right: 1px solid var(--ColorBorder, #3a3d41);
6967
+ box-shadow: inset -1px 0 0 rgba(0, 0, 0, 0.2);
6968
+ display: flex;
6969
+ flex-direction: column;
6970
+ grid-column: 1;
6971
+ grid-row: 2 / 4;
6972
+ min-height: 0;
6973
+ }
6974
+
6975
+ .Chat.ChatFocus .ProjectList {
6976
+ min-height: 0;
6977
+ overflow: auto;
6978
+ padding: 10px 8px 12px;
6979
+ scrollbar-color: color-mix(in srgb, var(--ColorScrollBarSliderBackground, #4b5563) 78%, transparent)
6980
+ color-mix(in srgb, var(--ColorSideBarBackground, #232b35) 92%, transparent);
6981
+ scrollbar-width: thin;
6982
+ }
6983
+
6984
+ .Chat.ChatFocus .ProjectList::-webkit-scrollbar {
6985
+ height: 10px;
6986
+ width: 10px;
6987
+ }
6988
+
6989
+ .Chat.ChatFocus .ProjectList::-webkit-scrollbar-track {
6990
+ background: color-mix(in srgb, var(--ColorSideBarBackground, #232b35) 94%, transparent);
6991
+ border-radius: 999px;
6992
+ }
6993
+
6994
+ .Chat.ChatFocus .ProjectList::-webkit-scrollbar-thumb {
6995
+ background: color-mix(in srgb, var(--ColorScrollBarSliderBackground, #4b5563) 70%, transparent);
6996
+ border: 2px solid color-mix(in srgb, var(--ColorSideBarBackground, #232b35) 94%, transparent);
6997
+ border-radius: 999px;
6998
+ }
6999
+
7000
+ .Chat.ChatFocus .ProjectList::-webkit-scrollbar-thumb:hover {
7001
+ background: color-mix(in srgb, var(--ColorScrollBarSliderHoverBackground, #667284) 80%, transparent);
7002
+ }
7003
+
7004
+ .ProjectListGroup {
7005
+ border: 1px solid transparent;
7006
+ border-radius: 8px;
7007
+ margin-bottom: 6px;
7008
+ overflow: hidden;
7009
+ }
7010
+
7011
+ .ProjectListItem {
7012
+ align-items: center;
7013
+ display: flex;
7014
+ gap: 6px;
7015
+ min-height: calc(var(--ChatListItemHeight) - 2px);
7016
+ }
7017
+
7018
+ .ProjectListItemLabel {
7019
+ align-items: center;
7020
+ border-radius: 6px;
7021
+ color: var(--ColorForeground, #d5dbe3);
7022
+ cursor: pointer;
7023
+ display: flex;
7024
+ flex: 1;
7025
+ font-weight: 500;
7026
+ gap: 2px;
7027
+ overflow: hidden;
7028
+ padding: 0 10px;
7029
+ text-overflow: ellipsis;
7030
+ transition: background-color 80ms ease, color 80ms ease;
7031
+ white-space: nowrap;
7032
+ }
7033
+
7034
+ .ProjectListChevron {
7035
+ color: color-mix(in srgb, var(--ColorForeground, #c8d0da) 70%, transparent);
7036
+ display: inline-block;
7037
+ flex: 0 0 12px;
7038
+ font-size: 11px;
7039
+ margin-right: 4px;
7040
+ text-align: center;
7041
+ width: 12px;
7042
+ }
7043
+
7044
+ .ProjectListItemSelected {
7045
+ background: color-mix(in srgb, var(--ColorListInactiveSelectionBackground, #39424d) 84%, #2f3741 16%);
7046
+ }
7047
+
7048
+ .ProjectListItem:not(.ProjectListItemSelected) .ProjectListItemLabel:hover,
7049
+ .ProjectListItem:not(.ProjectListItemSelected) .ProjectListItemLabel:focus-visible {
7050
+ background: color-mix(in srgb, var(--ColorListHoverBackground, #38414b) 50%, transparent);
7051
+ }
7052
+
7053
+ .ProjectListItemSelected .ProjectListItemLabel {
7054
+ color: var(--ColorListInactiveSelectionForeground, #e5ebf2);
7055
+ }
7056
+
7057
+ .ProjectListItemActions {
7058
+ display: flex;
7059
+ padding-right: 6px;
7060
+ }
7061
+
7062
+ .ProjectListItemAddChatButton {
7063
+ align-items: center;
7064
+ background: color-mix(in srgb, var(--ColorButtonSecondaryBackground, #3a434f) 76%, transparent);
7065
+ border: 0;
7066
+ border-radius: 5px;
7067
+ color: var(--ColorForeground, #d0d8e2);
7068
+ cursor: pointer;
7069
+ display: inline-flex;
7070
+ font-size: 13px;
7071
+ font-weight: 500;
7072
+ height: 18px;
7073
+ justify-content: center;
7074
+ opacity: 0;
7075
+ padding: 0;
7076
+ transition: opacity 90ms ease, background-color 90ms ease;
7077
+ visibility: hidden;
7078
+ width: 18px;
7079
+ }
7080
+
7081
+ .ProjectListItem:hover .ProjectListItemAddChatButton,
7082
+ .ProjectListItem:focus-within .ProjectListItemAddChatButton {
7083
+ opacity: 1;
7084
+ visibility: visible;
7085
+ }
7086
+
7087
+ .ProjectListItemAddChatButton:hover,
7088
+ .ProjectListItemAddChatButton:focus-visible {
7089
+ background: color-mix(in srgb, var(--ColorButtonSecondaryHoverBackground, #4a5460) 82%, transparent);
7090
+ }
7091
+
7092
+ .ProjectSessionItem {
7093
+ align-items: center;
7094
+ display: flex;
7095
+ min-height: calc(var(--ChatListItemHeight) - 5px);
7096
+ }
7097
+
7098
+ .ProjectSessionItemLabel {
7099
+ border-radius: 6px;
7100
+ color: color-mix(in srgb, var(--ColorForeground, #cfd7df) 92%, transparent);
7101
+ cursor: pointer;
7102
+ display: block;
7103
+ flex: 1;
7104
+ font-size: 12.5px;
7105
+ overflow: hidden;
7106
+ padding: 0 10px 0 28px;
7107
+ text-overflow: ellipsis;
7108
+ transition: background-color 80ms ease, color 80ms ease;
7109
+ white-space: nowrap;
7110
+ }
7111
+
7112
+ .ProjectSessionItemSelected {
7113
+ background: color-mix(in srgb, var(--ColorListInactiveSelectionBackground, #353f4a) 86%, #2c3540 14%);
7114
+ }
7115
+
7116
+ .ProjectSessionItem:not(.ProjectSessionItemSelected) .ProjectSessionItemLabel:hover,
7117
+ .ProjectSessionItem:not(.ProjectSessionItemSelected) .ProjectSessionItemLabel:focus-visible {
7118
+ background: color-mix(in srgb, var(--ColorListHoverBackground, #38414c) 46%, transparent);
7119
+ }
7120
+
7121
+ .ProjectSessionItemSelected .ProjectSessionItemLabel {
7122
+ color: var(--ColorListInactiveSelectionForeground, #f2f6fc);
7123
+ }
7124
+
7125
+ .Chat.ChatFocus .ProjectAddButton {
7126
+ background: color-mix(in srgb, var(--ColorButtonSecondaryBackground, #21252c) 72%, transparent);
7127
+ border: 0;
7128
+ border-top: 1px solid color-mix(in srgb, var(--ColorBorder, #3a3d41) 70%, transparent);
7129
+ color: var(--ColorForeground, #d2d9e2);
7130
+ cursor: pointer;
7131
+ font-size: 12.5px;
7132
+ letter-spacing: 0.01em;
7133
+ margin-top: auto;
7134
+ min-height: var(--ChatListItemHeight);
7135
+ padding: 0 12px;
7136
+ text-align: left;
7137
+ transition: background-color 80ms ease;
7138
+ }
7139
+
7140
+ .Chat.ChatFocus .ProjectAddButton:hover,
7141
+ .Chat.ChatFocus .ProjectAddButton:focus-visible {
7142
+ background: color-mix(in srgb, var(--ColorButtonSecondaryHoverBackground, #2a3039) 78%, transparent);
7143
+ }
7144
+
7145
+ .ChatList,
7146
+ .ChatListEmpty,
7147
+ .ChatMessages {
7148
+ min-height: 0;
7149
+ }
7150
+
7151
+ .Chat.ChatFocus .ChatList,
7152
+ .Chat.ChatFocus .ChatListEmpty {
7153
+ display: none;
7154
+ }
7155
+
7156
+ .Chat.ChatFocus .ChatMessages {
7157
+ grid-column: 2;
7158
+ grid-row: 2;
7159
+ }
7160
+
7161
+ .Chat.ChatFocus .ChatSendArea {
7162
+ grid-column: 2;
7163
+ grid-row: 3;
7164
+ }
6602
7165
  `;
6603
7166
  if (!renderHtmlCss.trim()) {
6604
7167
  return baseCss;
@@ -6676,6 +7239,19 @@ const ChatList = 'ChatList';
6676
7239
  const ChatListEmpty = 'ChatListEmpty';
6677
7240
  const ChatListItem = 'ChatListItem';
6678
7241
  const ChatListItemLabel = 'ChatListItemLabel';
7242
+ const ProjectAddButton = 'ProjectAddButton';
7243
+ const ProjectList = 'ProjectList';
7244
+ const ProjectListChevron = 'ProjectListChevron';
7245
+ const ProjectListGroup = 'ProjectListGroup';
7246
+ const ProjectListItem = 'ProjectListItem';
7247
+ const ProjectListItemActions = 'ProjectListItemActions';
7248
+ const ProjectListItemAddChatButton = 'ProjectListItemAddChatButton';
7249
+ const ProjectListItemLabel = 'ProjectListItemLabel';
7250
+ const ProjectListItemSelected = 'ProjectListItemSelected';
7251
+ const ProjectSessionItem = 'ProjectSessionItem';
7252
+ const ProjectSessionItemLabel = 'ProjectSessionItemLabel';
7253
+ const ProjectSessionItemSelected = 'ProjectSessionItemSelected';
7254
+ const ProjectSidebar = 'ProjectSidebar';
6679
7255
  const Markdown = 'Markdown';
6680
7256
  const MarkdownTable = 'MarkdownTable';
6681
7257
  const Message = 'Message';
@@ -6726,6 +7302,7 @@ const HandleDragLeave = 28;
6726
7302
  const HandleDrop = 29;
6727
7303
  const HandleDragEnterChatView = 30;
6728
7304
  const HandleDragOverChatView = 31;
7305
+ const HandleProjectListScroll = 32;
6729
7306
 
6730
7307
  const getModelLabel = model => {
6731
7308
  if (model.provider === 'openRouter') {
@@ -6842,82 +7419,6 @@ const getChatSendAreaDom = (composerValue, models, selectedModelId, usageOvervie
6842
7419
  }, ...getChatSelectVirtualDom(models, selectedModelId), ...(usageOverviewEnabled ? getUsageOverviewDom(tokensUsed, tokensMax) : []), ...getSendButtonDom(isSendDisabled)];
6843
7420
  };
6844
7421
 
6845
- const getBackButtonVirtualDom = () => {
6846
- return [{
6847
- childCount: 1,
6848
- className: IconButton,
6849
- name: Back,
6850
- onClick: HandleClickBack,
6851
- role: Button$2,
6852
- title: backToChats(),
6853
- type: Button$1
6854
- }, {
6855
- childCount: 0,
6856
- className: 'MaskIcon MaskIconArrowLeft',
6857
- type: Div
6858
- }];
6859
- };
6860
-
6861
- const getHeaderActionVirtualDom = item => {
6862
- return [{
6863
- childCount: 1,
6864
- className: IconButton,
6865
- name: item.name,
6866
- onClick: item.onClick,
6867
- title: item.title,
6868
- type: Button$1
6869
- }, {
6870
- childCount: 0,
6871
- className: item.icon,
6872
- type: Div
6873
- }];
6874
- };
6875
-
6876
- const getChatHeaderActionsDom = () => {
6877
- const items = [{
6878
- icon: 'MaskIcon MaskIconDebugPause',
6879
- name: SessionDebug,
6880
- onClick: HandleClickSessionDebug,
6881
- title: debug()
6882
- }, {
6883
- icon: 'MaskIcon MaskIconAdd',
6884
- name: CreateSession,
6885
- onClick: HandleClickNew,
6886
- title: newChat()
6887
- }, {
6888
- icon: 'MaskIcon MaskIconSettingsGear',
6889
- name: Settings,
6890
- onClick: HandleClickSettings,
6891
- title: settings()
6892
- }, {
6893
- icon: 'MaskIcon MaskIconClose',
6894
- name: CloseChat,
6895
- onClick: HandleClickClose,
6896
- title: closeChat()
6897
- }];
6898
- return [{
6899
- childCount: items.length,
6900
- className: ChatActions,
6901
- type: Div
6902
- }, ...items.flatMap(getHeaderActionVirtualDom)];
6903
- };
6904
-
6905
- const getChatHeaderDomDetailMode = selectedSessionTitle => {
6906
- return [{
6907
- childCount: 2,
6908
- className: ChatHeader,
6909
- type: Div
6910
- }, {
6911
- childCount: 2,
6912
- className: ChatName,
6913
- type: Div
6914
- }, ...getBackButtonVirtualDom(), {
6915
- childCount: 1,
6916
- className: Label,
6917
- type: Span
6918
- }, text(selectedSessionTitle), ...getChatHeaderActionsDom()];
6919
- };
6920
-
6921
7422
  const getInlineNodeDom = inlineNode => {
6922
7423
  if (inlineNode.type === 'text') {
6923
7424
  return [text(inlineNode.text)];
@@ -7457,8 +7958,12 @@ const getElementType = tagName => {
7457
7958
  return inlineTags.has(tagName) ? Span : Div;
7458
7959
  }
7459
7960
  };
7961
+ const isHttpUrl$1 = url => {
7962
+ const normalized = url.trim().toLowerCase();
7963
+ return normalized.startsWith('http://') || normalized.startsWith('https://');
7964
+ };
7460
7965
  const normalizeUrl = url => {
7461
- return url.toLowerCase().startsWith('javascript:') ? '#' : url;
7966
+ return isHttpUrl$1(url) ? url : '#';
7462
7967
  };
7463
7968
  const getElementAttributes = node => {
7464
7969
  const attributes = {};
@@ -7603,6 +8108,13 @@ const markdownInlineRegex = /\[([^\]]+)\]\(([^)]+)\)|\*\*([^*]+)\*\*|\*([^*]+)\*
7603
8108
  const markdownTableSeparatorCellRegex = /^:?-{3,}:?$/;
7604
8109
  const fencedCodeBlockRegex = /^```/;
7605
8110
  const markdownHeadingRegex = /^\s*(#{1,6})\s+(.*)$/;
8111
+ const isHttpUrl = url => {
8112
+ const normalized = url.trim().toLowerCase();
8113
+ return normalized.startsWith('http://') || normalized.startsWith('https://');
8114
+ };
8115
+ const sanitizeUrl = url => {
8116
+ return isHttpUrl(url) ? url : '#';
8117
+ };
7606
8118
  const normalizeEscapedNewlines = value => {
7607
8119
  if (value.includes('\\n')) {
7608
8120
  return value.replaceAll(/\\r\\n|\\n/g, '\n');
@@ -7672,7 +8184,7 @@ const parseInlineNodes = value => {
7672
8184
  }
7673
8185
  if (linkText && href) {
7674
8186
  nodes.push({
7675
- href,
8187
+ href: sanitizeUrl(href),
7676
8188
  text: linkText,
7677
8189
  type: 'link'
7678
8190
  });
@@ -7880,6 +8392,204 @@ const getMessagesDom = (messages, openRouterApiKeyInput, openApiApiKeyInput = ''
7880
8392
  }, ...messages.flatMap(message => getChatMessageDom(message, openRouterApiKeyInput, openApiApiKeyInput, openRouterApiKeyState))];
7881
8393
  };
7882
8394
 
8395
+ const getProjectSessionDom = (session, selectedSessionId) => {
8396
+ const className = mergeClassNames(ProjectSessionItem, session.id === selectedSessionId ? ProjectSessionItemSelected : Empty);
8397
+ return [{
8398
+ childCount: 1,
8399
+ className,
8400
+ type: Div
8401
+ }, {
8402
+ childCount: 1,
8403
+ className: ProjectSessionItemLabel,
8404
+ name: getSessionInputName(session.id),
8405
+ onClick: HandleClick,
8406
+ tabIndex: 0,
8407
+ type: Div
8408
+ }, text(session.title)];
8409
+ };
8410
+ const getProjectGroupDom = (project, sessions, projectExpandedIds, selectedProjectId, selectedSessionId) => {
8411
+ const expanded = projectExpandedIds.includes(project.id);
8412
+ const projectClassName = mergeClassNames(ProjectListItem, project.id === selectedProjectId ? ProjectListItemSelected : Empty);
8413
+ return [{
8414
+ childCount: 1 + (expanded ? sessions.length : 0),
8415
+ className: ProjectListGroup,
8416
+ type: Div
8417
+ }, {
8418
+ childCount: 2,
8419
+ className: projectClassName,
8420
+ type: Div
8421
+ }, {
8422
+ childCount: 2,
8423
+ className: ProjectListItemLabel,
8424
+ name: getProjectInputName(project.id),
8425
+ onClick: HandleClick,
8426
+ tabIndex: 0,
8427
+ type: Div
8428
+ }, {
8429
+ childCount: 1,
8430
+ className: ProjectListChevron,
8431
+ type: Span
8432
+ }, text(expanded ? '▾' : '▸'), text(project.name), {
8433
+ childCount: 1,
8434
+ className: ProjectListItemActions,
8435
+ type: Div
8436
+ }, {
8437
+ childCount: 1,
8438
+ className: ProjectListItemAddChatButton,
8439
+ name: getCreateSessionInProjectInputName(project.id),
8440
+ onClick: HandleClick,
8441
+ tabIndex: 0,
8442
+ title: 'New chat in this project',
8443
+ type: Button$1
8444
+ }, text('+'), ...(expanded ? sessions.flatMap(session => getProjectSessionDom(session, selectedSessionId)) : [])];
8445
+ };
8446
+ const getProjectListDom = (projects, sessions, projectExpandedIds, selectedProjectId, selectedSessionId, projectListScrollTop) => {
8447
+ const blankProjectId = projects.find(project => project.name === '_blank')?.id || projects[0]?.id || '';
8448
+ const projectGroups = projects.map(project => {
8449
+ const projectSessions = sessions.filter(session => {
8450
+ const sessionProjectId = session.projectId || blankProjectId;
8451
+ return sessionProjectId === project.id;
8452
+ });
8453
+ return getProjectGroupDom(project, projectSessions, projectExpandedIds, selectedProjectId, selectedSessionId);
8454
+ });
8455
+ return [{
8456
+ childCount: 2,
8457
+ className: ProjectSidebar,
8458
+ type: Div
8459
+ }, {
8460
+ childCount: projects.length,
8461
+ className: ProjectList,
8462
+ onScroll: HandleProjectListScroll,
8463
+ scrollTop: projectListScrollTop,
8464
+ type: Div
8465
+ }, ...projectGroups.flat(), {
8466
+ childCount: 1,
8467
+ className: ProjectAddButton,
8468
+ name: CreateProject,
8469
+ onClick: HandleClick,
8470
+ tabIndex: 0,
8471
+ type: Button$1
8472
+ }, text('+ Add Project')];
8473
+ };
8474
+
8475
+ const getChatModeChatFocusVirtualDom = (sessions, selectedSessionId, composerValue, openRouterApiKeyInput, openApiApiKeyInput, models, selectedModelId, usageOverviewEnabled, tokensUsed, tokensMax, openRouterApiKeyState = 'idle', composerHeight = 28, composerFontSize = 13, composerFontFamily = 'system-ui', composerLineHeight = 20, messagesScrollTop = 0, composerDropActive = false, composerDropEnabled = true, projects = [], projectExpandedIds = [], selectedProjectId = '', projectListScrollTop = 0) => {
8476
+ const selectedSession = sessions.find(session => session.id === selectedSessionId);
8477
+ const selectedSessionTitle = selectedSession?.title || chatTitle();
8478
+ const messages = selectedSession ? selectedSession.messages : [];
8479
+ const isDropOverlayVisible = composerDropEnabled && composerDropActive;
8480
+ return [{
8481
+ childCount: 5,
8482
+ className: mergeClassNames(Viewlet, Chat, 'ChatFocus'),
8483
+ onDragEnter: HandleDragEnterChatView,
8484
+ onDragOver: HandleDragOverChatView,
8485
+ type: Div
8486
+ }, {
8487
+ childCount: 1,
8488
+ className: ChatHeader,
8489
+ type: Div
8490
+ }, {
8491
+ childCount: 1,
8492
+ className: Label,
8493
+ type: Span
8494
+ }, {
8495
+ text: selectedSessionTitle,
8496
+ type: Text
8497
+ }, ...getProjectListDom(projects, sessions, projectExpandedIds, selectedProjectId, selectedSessionId, projectListScrollTop), ...getMessagesDom(messages, openRouterApiKeyInput, openApiApiKeyInput, openRouterApiKeyState, messagesScrollTop), ...getChatSendAreaDom(composerValue, models, selectedModelId, usageOverviewEnabled, tokensUsed, tokensMax, composerHeight, composerFontSize, composerFontFamily, composerLineHeight), {
8498
+ childCount: 1,
8499
+ className: mergeClassNames(ChatViewDropOverlay, isDropOverlayVisible ? ChatViewDropOverlayActive : Empty),
8500
+ name: ComposerDropTarget,
8501
+ onDragLeave: HandleDragLeave,
8502
+ onDragOver: HandleDragOver,
8503
+ onDrop: HandleDrop,
8504
+ type: Div
8505
+ }, {
8506
+ text: attachImageAsContext(),
8507
+ type: Text
8508
+ }];
8509
+ };
8510
+
8511
+ const getBackButtonVirtualDom = () => {
8512
+ return [{
8513
+ childCount: 1,
8514
+ className: IconButton,
8515
+ name: Back,
8516
+ onClick: HandleClickBack,
8517
+ role: Button$2,
8518
+ title: backToChats(),
8519
+ type: Button$1
8520
+ }, {
8521
+ childCount: 0,
8522
+ className: 'MaskIcon MaskIconArrowLeft',
8523
+ type: Div
8524
+ }];
8525
+ };
8526
+
8527
+ const getHeaderActionVirtualDom = item => {
8528
+ return [{
8529
+ childCount: 1,
8530
+ className: IconButton,
8531
+ name: item.name,
8532
+ onClick: item.onClick,
8533
+ title: item.title,
8534
+ type: Button$1
8535
+ }, {
8536
+ childCount: 0,
8537
+ className: item.icon,
8538
+ type: Div
8539
+ }];
8540
+ };
8541
+
8542
+ const getChatHeaderActionsDom = viewMode => {
8543
+ const toggleTitle = viewMode === 'chat-focus' ? normalChatMode() : chatFocusMode();
8544
+ const items = [{
8545
+ icon: 'MaskIcon MaskIconLayoutPanelLeft',
8546
+ name: ToggleChatFocus,
8547
+ onClick: HandleClick,
8548
+ title: toggleTitle
8549
+ }, {
8550
+ icon: 'MaskIcon MaskIconDebugPause',
8551
+ name: SessionDebug,
8552
+ onClick: HandleClickSessionDebug,
8553
+ title: debug()
8554
+ }, {
8555
+ icon: 'MaskIcon MaskIconAdd',
8556
+ name: CreateSession,
8557
+ onClick: HandleClickNew,
8558
+ title: newChat()
8559
+ }, {
8560
+ icon: 'MaskIcon MaskIconSettingsGear',
8561
+ name: Settings,
8562
+ onClick: HandleClickSettings,
8563
+ title: settings()
8564
+ }, {
8565
+ icon: 'MaskIcon MaskIconClose',
8566
+ name: CloseChat,
8567
+ onClick: HandleClickClose,
8568
+ title: closeChat()
8569
+ }];
8570
+ return [{
8571
+ childCount: items.length,
8572
+ className: ChatActions,
8573
+ type: Div
8574
+ }, ...items.flatMap(getHeaderActionVirtualDom)];
8575
+ };
8576
+
8577
+ const getChatHeaderDomDetailMode = selectedSessionTitle => {
8578
+ return [{
8579
+ childCount: 2,
8580
+ className: ChatHeader,
8581
+ type: Div
8582
+ }, {
8583
+ childCount: 2,
8584
+ className: ChatName,
8585
+ type: Div
8586
+ }, ...getBackButtonVirtualDom(), {
8587
+ childCount: 1,
8588
+ className: Label,
8589
+ type: Span
8590
+ }, text(selectedSessionTitle), ...getChatHeaderActionsDom('detail')];
8591
+ };
8592
+
7883
8593
  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, composerDropActive = false, composerDropEnabled = true) => {
7884
8594
  const selectedSession = sessions.find(session => session.id === selectedSessionId);
7885
8595
  const selectedSessionTitle = selectedSession?.title || chatTitle();
@@ -7914,7 +8624,7 @@ const getChatHeaderListModeDom = () => {
7914
8624
  childCount: 1,
7915
8625
  className: Label,
7916
8626
  type: Span
7917
- }, text(chats()), ...getChatHeaderActionsDom()];
8627
+ }, text(chats()), ...getChatHeaderActionsDom('list')];
7918
8628
  };
7919
8629
 
7920
8630
  const getEmptyChatSessionsDom = () => {
@@ -8001,8 +8711,10 @@ const getChatModeUnsupportedVirtualDom = () => {
8001
8711
  }, text(unknownViewMode())];
8002
8712
  };
8003
8713
 
8004
- const getChatVirtualDom = (sessions, selectedSessionId, composerValue, openRouterApiKeyInput, viewMode, models, selectedModelId, usageOverviewEnabled, tokensUsed, tokensMax, openApiApiKeyInput, openRouterApiKeyState, composerHeight, composerFontSize, composerFontFamily, composerLineHeight, chatListScrollTop, messagesScrollTop, composerDropActive = false, composerDropEnabled = true) => {
8714
+ const getChatVirtualDom = (sessions, selectedSessionId, composerValue, openRouterApiKeyInput, viewMode, models, selectedModelId, usageOverviewEnabled, tokensUsed, tokensMax, openApiApiKeyInput, openRouterApiKeyState, composerHeight, composerFontSize, composerFontFamily, composerLineHeight, chatListScrollTop, messagesScrollTop, composerDropActive = false, composerDropEnabled = true, projects = [], projectExpandedIds = [], selectedProjectId = '', projectListScrollTop = 0) => {
8005
8715
  switch (viewMode) {
8716
+ case 'chat-focus':
8717
+ return getChatModeChatFocusVirtualDom(sessions, selectedSessionId, composerValue, openRouterApiKeyInput, openApiApiKeyInput, models, selectedModelId, usageOverviewEnabled, tokensUsed, tokensMax, openRouterApiKeyState, composerHeight, composerFontSize, composerFontFamily, composerLineHeight, messagesScrollTop, composerDropActive, composerDropEnabled, projects, projectExpandedIds, selectedProjectId, projectListScrollTop);
8006
8718
  case 'detail':
8007
8719
  return getChatModeDetailVirtualDom(sessions, selectedSessionId, composerValue, openRouterApiKeyInput, openApiApiKeyInput, models, selectedModelId, usageOverviewEnabled, tokensUsed, tokensMax, openRouterApiKeyState, composerHeight, composerFontSize, composerFontFamily, composerLineHeight, messagesScrollTop, composerDropActive, composerDropEnabled);
8008
8720
  case 'list':
@@ -8028,7 +8740,11 @@ const renderItems = (oldState, newState) => {
8028
8740
  openApiApiKeyInput,
8029
8741
  openRouterApiKeyInput,
8030
8742
  openRouterApiKeyState,
8743
+ projectExpandedIds,
8744
+ projectListScrollTop,
8745
+ projects,
8031
8746
  selectedModelId,
8747
+ selectedProjectId,
8032
8748
  selectedSessionId,
8033
8749
  sessions,
8034
8750
  tokensMax,
@@ -8040,7 +8756,7 @@ const renderItems = (oldState, newState) => {
8040
8756
  if (initial) {
8041
8757
  return [SetDom2, uid, []];
8042
8758
  }
8043
- const dom = getChatVirtualDom(sessions, selectedSessionId, composerValue, openRouterApiKeyInput, viewMode, models, selectedModelId, usageOverviewEnabled, tokensUsed, tokensMax, openApiApiKeyInput, openRouterApiKeyState, composerHeight, composerFontSize, composerFontFamily, composerLineHeight, chatListScrollTop, messagesScrollTop, composerDropActive, composerDropEnabled);
8759
+ const dom = getChatVirtualDom(sessions, selectedSessionId, composerValue, openRouterApiKeyInput, viewMode, models, selectedModelId, usageOverviewEnabled, tokensUsed, tokensMax, openApiApiKeyInput, openRouterApiKeyState, composerHeight, composerFontSize, composerFontFamily, composerLineHeight, chatListScrollTop, messagesScrollTop, composerDropActive, composerDropEnabled, projects, projectExpandedIds, selectedProjectId, projectListScrollTop);
8044
8760
  return [SetDom2, uid, dom];
8045
8761
  };
8046
8762
 
@@ -8177,6 +8893,9 @@ const renderEventListeners = () => {
8177
8893
  }, {
8178
8894
  name: HandleMessagesScroll,
8179
8895
  params: ['handleMessagesScroll', 'event.target.scrollTop']
8896
+ }, {
8897
+ name: HandleProjectListScroll,
8898
+ params: ['handleProjectListScroll', 'event.target.scrollTop']
8180
8899
  }, {
8181
8900
  name: HandleMessagesContextMenu,
8182
8901
  params: ['handleMessagesContextMenu'],
@@ -8224,20 +8943,30 @@ const saveState = state => {
8224
8943
  const {
8225
8944
  chatListScrollTop,
8226
8945
  composerValue,
8946
+ lastNormalViewMode,
8227
8947
  messagesScrollTop,
8228
8948
  nextMessageId,
8949
+ projectExpandedIds,
8950
+ projectListScrollTop,
8951
+ projects,
8229
8952
  renamingSessionId,
8230
8953
  selectedModelId,
8954
+ selectedProjectId,
8231
8955
  selectedSessionId,
8232
8956
  viewMode
8233
8957
  } = state;
8234
8958
  return {
8235
8959
  chatListScrollTop,
8236
8960
  composerValue,
8961
+ lastNormalViewMode,
8237
8962
  messagesScrollTop,
8238
8963
  nextMessageId,
8964
+ projectExpandedIds,
8965
+ projectListScrollTop,
8966
+ projects,
8239
8967
  renamingSessionId,
8240
8968
  selectedModelId,
8969
+ selectedProjectId,
8241
8970
  selectedSessionId,
8242
8971
  viewMode
8243
8972
  };
@@ -8336,6 +9065,7 @@ const commandMap = {
8336
9065
  'Chat.handleMessagesContextMenu': wrapCommand(handleMessagesContextMenu),
8337
9066
  'Chat.handleMessagesScroll': wrapCommand(handleMessagesScroll),
8338
9067
  'Chat.handleModelChange': wrapCommand(handleModelChange),
9068
+ 'Chat.handleProjectListScroll': wrapCommand(handleProjectListScroll),
8339
9069
  'Chat.handleSubmit': wrapCommand(handleSubmit),
8340
9070
  'Chat.initialize': initialize,
8341
9071
  'Chat.loadContent': wrapCommand(loadContent),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lvce-editor/chat-view",
3
- "version": "3.10.0",
3
+ "version": "4.0.0",
4
4
  "description": "Chat View Worker",
5
5
  "repository": {
6
6
  "type": "git",