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