@lvce-editor/chat-view 3.9.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
  };
@@ -1393,13 +1403,16 @@ const unknownViewMode = () => {
1393
1403
 
1394
1404
  /* eslint-disable @cspell/spellchecker */
1395
1405
 
1396
- const getDefaultModels = () => {
1406
+ const getDefaultModelsTest = () => {
1397
1407
  const defaultModelId = 'test';
1398
1408
  return [{
1399
1409
  id: defaultModelId,
1400
1410
  name: 'test',
1401
1411
  provider: 'test'
1402
- }, {
1412
+ }];
1413
+ };
1414
+ const getDefaultModelsOpenAi = () => {
1415
+ return [{
1403
1416
  id: 'openapi/gpt-5-mini',
1404
1417
  name: 'GPT-5 Mini',
1405
1418
  provider: 'openApi'
@@ -1415,7 +1428,10 @@ const getDefaultModels = () => {
1415
1428
  id: 'openapi/gpt-4.1-mini',
1416
1429
  name: 'GPT-4.1 Mini',
1417
1430
  provider: 'openApi'
1418
- }, {
1431
+ }];
1432
+ };
1433
+ const getDefaultModelsOpenRouter = () => {
1434
+ return [{
1419
1435
  id: 'codex-5.3',
1420
1436
  name: 'Codex 5.3',
1421
1437
  provider: 'openRouter'
@@ -1465,7 +1481,12 @@ const getDefaultModels = () => {
1465
1481
  provider: 'openRouter'
1466
1482
  }];
1467
1483
  };
1484
+ const getDefaultModels = () => {
1485
+ return [...getDefaultModelsTest(), ...getDefaultModelsOpenAi(), ...getDefaultModelsOpenRouter()];
1486
+ };
1487
+
1468
1488
  const createDefaultState = () => {
1489
+ const defaultProjectId = 'project-1';
1469
1490
  const defaultSessionId = 'session-1';
1470
1491
  const defaultModelId = 'test';
1471
1492
  const chatMessageFontSize = 13;
@@ -1494,6 +1515,7 @@ const createDefaultState = () => {
1494
1515
  height: 0,
1495
1516
  initial: true,
1496
1517
  inputSource: 'script',
1518
+ lastNormalViewMode: 'list',
1497
1519
  lastSubmittedSessionId: '',
1498
1520
  listItemHeight: 40,
1499
1521
  maxComposerRows: 5,
@@ -1513,12 +1535,21 @@ const createDefaultState = () => {
1513
1535
  openRouterApiKeyState: 'idle',
1514
1536
  passIncludeObfuscation: false,
1515
1537
  platform: 0,
1538
+ projectExpandedIds: [defaultProjectId],
1539
+ projectListScrollTop: 0,
1540
+ projects: [{
1541
+ id: defaultProjectId,
1542
+ name: '_blank',
1543
+ uri: ''
1544
+ }],
1516
1545
  renamingSessionId: '',
1517
1546
  selectedModelId: defaultModelId,
1547
+ selectedProjectId: defaultProjectId,
1518
1548
  selectedSessionId: defaultSessionId,
1519
1549
  sessions: [{
1520
1550
  id: defaultSessionId,
1521
1551
  messages: [],
1552
+ projectId: defaultProjectId,
1522
1553
  title: defaultSessionTitle()
1523
1554
  }],
1524
1555
  streamingEnabled: true,
@@ -2138,29 +2169,49 @@ const createDefaultStorage = () => {
2138
2169
  let chatSessionStorage = createDefaultStorage();
2139
2170
  const listChatSessions = async () => {
2140
2171
  const sessions = await chatSessionStorage.listSessions();
2141
- return sessions.map(session => ({
2142
- id: session.id,
2143
- messages: [],
2144
- title: session.title
2145
- }));
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
+ });
2146
2186
  };
2147
2187
  const getChatSession = async id => {
2148
2188
  const session = await chatSessionStorage.getSession(id);
2149
2189
  if (!session) {
2150
2190
  return undefined;
2151
2191
  }
2152
- return {
2192
+ const result = {
2153
2193
  id: session.id,
2154
2194
  messages: [...session.messages],
2155
2195
  title: session.title
2156
2196
  };
2197
+ if (!session.projectId) {
2198
+ return result;
2199
+ }
2200
+ return {
2201
+ ...result,
2202
+ projectId: session.projectId
2203
+ };
2157
2204
  };
2158
2205
  const saveChatSession = async session => {
2159
- await chatSessionStorage.setSession({
2206
+ const value = {
2160
2207
  id: session.id,
2161
2208
  messages: [...session.messages],
2162
2209
  title: session.title
2163
- });
2210
+ };
2211
+ await chatSessionStorage.setSession(session.projectId ? {
2212
+ ...value,
2213
+ projectId: session.projectId
2214
+ } : value);
2164
2215
  };
2165
2216
  const deleteChatSession = async id => {
2166
2217
  await chatSessionStorage.deleteSession(id);
@@ -2184,6 +2235,17 @@ const getNextSelectedSessionId = (sessions, deletedId) => {
2184
2235
  return sessions[nextIndex].id;
2185
2236
  };
2186
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
+
2187
2249
  const deleteSession = async (state, id) => {
2188
2250
  const {
2189
2251
  renamingSessionId,
@@ -2222,10 +2284,8 @@ const deleteSession = async (state, id) => {
2222
2284
  };
2223
2285
  };
2224
2286
  const deleteSessionAtIndex = async (state, index) => {
2225
- const {
2226
- sessions
2227
- } = state;
2228
- const session = sessions[index];
2287
+ const visibleSessions = getVisibleSessions(state.sessions, state.selectedProjectId);
2288
+ const session = visibleSessions[index];
2229
2289
  if (!session) {
2230
2290
  return state;
2231
2291
  }
@@ -2291,12 +2351,23 @@ const diffFocus = (oldState, newState) => {
2291
2351
  return oldState.focus === newState.focus && oldState.focused === newState.focused;
2292
2352
  };
2293
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
+ };
2294
2365
  const isEqual = (oldState, newState) => {
2295
- 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;
2296
2367
  };
2297
2368
 
2298
2369
  const diffScrollTop = (oldState, newState) => {
2299
- return oldState.chatListScrollTop === newState.chatListScrollTop && oldState.messagesScrollTop === newState.messagesScrollTop;
2370
+ return oldState.chatListScrollTop === newState.chatListScrollTop && oldState.messagesScrollTop === newState.messagesScrollTop && oldState.projectListScrollTop === newState.projectListScrollTop;
2300
2371
  };
2301
2372
 
2302
2373
  const RenderItems = 4;
@@ -2735,7 +2806,8 @@ const handleChatListContextMenu = async (state, eventX, eventY) => {
2735
2806
  if (index === -1) {
2736
2807
  return state;
2737
2808
  }
2738
- const item = state.sessions[index];
2809
+ const visibleSessions = getVisibleSessions(state.sessions, state.selectedProjectId);
2810
+ const item = visibleSessions[index];
2739
2811
  if (!item) {
2740
2812
  return state;
2741
2813
  }
@@ -2747,22 +2819,106 @@ const generateSessionId = () => {
2747
2819
  return crypto.randomUUID();
2748
2820
  };
2749
2821
 
2750
- const createSession = async state => {
2822
+ const createSession = async (state, projectIdOverride = '') => {
2751
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;
2752
2826
  const session = {
2753
2827
  id,
2754
2828
  messages: [],
2829
+ projectId,
2755
2830
  title: `Chat ${state.sessions.length + 1}`
2756
2831
  };
2757
2832
  await saveChatSession(session);
2758
2833
  return {
2759
2834
  ...state,
2835
+ projectExpandedIds: state.projectExpandedIds.includes(projectId) ? state.projectExpandedIds : [...state.projectExpandedIds, projectId],
2760
2836
  renamingSessionId: '',
2837
+ selectedProjectId: projectId,
2761
2838
  selectedSessionId: id,
2762
2839
  sessions: [...state.sessions, session]
2763
2840
  };
2764
2841
  };
2765
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
+
2766
2922
  const handleClickOpenApiApiKeySettings = async state => {
2767
2923
  await invoke('Main.openUri', 'app://settings.json');
2768
2924
  return state;
@@ -5812,13 +5968,35 @@ const ComposerDropTarget = 'composer-drop-target';
5812
5968
  const Send = 'send';
5813
5969
  const Back = 'back';
5814
5970
  const Model = 'model';
5971
+ const ToggleChatFocus = 'toggle-chat-focus';
5972
+ const CreateProject = 'create-project';
5815
5973
  const CreateSession = 'create-session';
5974
+ const CreateSessionInProjectPrefix = 'create-session-in-project:';
5816
5975
  const SessionDebug = 'session-debug';
5817
5976
  const Settings = 'settings';
5818
5977
  const CloseChat = 'close-chat';
5819
5978
  const SessionDelete = 'SessionDelete';
5979
+ const ProjectPrefix = 'project:';
5820
5980
  const SessionPrefix = 'session:';
5821
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
+ };
5822
6000
  const getSessionInputName = sessionId => {
5823
6001
  return `${SessionPrefix}${sessionId}`;
5824
6002
  };
@@ -5863,10 +6041,11 @@ const selectSession = async (state, id) => {
5863
6041
  });
5864
6042
  return {
5865
6043
  ...state,
6044
+ lastNormalViewMode: state.viewMode === 'chat-focus' ? state.lastNormalViewMode : 'detail',
5866
6045
  renamingSessionId: '',
5867
6046
  selectedSessionId: id,
5868
6047
  sessions,
5869
- viewMode: 'detail'
6048
+ viewMode: state.viewMode === 'chat-focus' ? 'chat-focus' : 'detail'
5870
6049
  };
5871
6050
  };
5872
6051
 
@@ -5887,14 +6066,61 @@ const startRename = (state, id) => {
5887
6066
  };
5888
6067
  };
5889
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
+
5890
6118
  const selectListIndex = async (state, index) => {
5891
- const {
5892
- sessions
5893
- } = state;
5894
- if (index < 0 || index >= sessions.length) {
6119
+ const visibleSessions = getVisibleSessions(state.sessions, state.selectedProjectId);
6120
+ if (index < 0 || index >= visibleSessions.length) {
5895
6121
  return state;
5896
6122
  }
5897
- const session = sessions[index];
6123
+ const session = visibleSessions[index];
5898
6124
  return selectSession(state, session.id);
5899
6125
  };
5900
6126
 
@@ -5910,6 +6136,23 @@ const handleClick = async (state, name, id = '') => {
5910
6136
  if (name === CreateSession) {
5911
6137
  return createSession(state);
5912
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
+ }
5913
6156
  if (isSessionInputName(name)) {
5914
6157
  const sessionId = getSessionIdFromInputName(name);
5915
6158
  return selectSession(state, sessionId);
@@ -5948,6 +6191,7 @@ const handleClick = async (state, name, id = '') => {
5948
6191
  const handleClickBack = async state => {
5949
6192
  return {
5950
6193
  ...state,
6194
+ lastNormalViewMode: 'list',
5951
6195
  renamingSessionId: '',
5952
6196
  viewMode: 'list'
5953
6197
  };
@@ -6229,6 +6473,15 @@ const handleMessagesScroll = async (state, messagesScrollTop) => {
6229
6473
  messagesScrollTop
6230
6474
  };
6231
6475
  };
6476
+ const handleProjectListScroll = async (state, projectListScrollTop) => {
6477
+ if (state.projectListScrollTop === projectListScrollTop) {
6478
+ return state;
6479
+ }
6480
+ return {
6481
+ ...state,
6482
+ projectListScrollTop
6483
+ };
6484
+ };
6232
6485
 
6233
6486
  const id = 7201;
6234
6487
  const sendMessagePortToExtensionHostWorker = async port => {
@@ -6328,7 +6581,7 @@ const getSavedViewMode = savedState => {
6328
6581
  const {
6329
6582
  viewMode
6330
6583
  } = savedState;
6331
- if (viewMode !== 'list' && viewMode !== 'detail') {
6584
+ if (viewMode !== 'list' && viewMode !== 'detail' && viewMode !== 'chat-focus') {
6332
6585
  return undefined;
6333
6586
  }
6334
6587
  return viewMode;
@@ -6445,11 +6698,97 @@ const loadSelectedSessionMessages = async (sessions, selectedSessionId) => {
6445
6698
  };
6446
6699
 
6447
6700
  const toSummarySession = session => {
6448
- return {
6701
+ const summary = {
6449
6702
  id: session.id,
6450
6703
  messages: [],
6451
6704
  title: session.title
6452
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;
6453
6792
  };
6454
6793
  const loadContent = async (state, savedState) => {
6455
6794
  const savedSelectedModelId = getSavedSelectedModelId(savedState);
@@ -6480,14 +6819,30 @@ const loadContent = async (state, savedState) => {
6480
6819
  sessions = state.sessions.map(toSummarySession);
6481
6820
  }
6482
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 || '';
6483
6832
  const preferredModelId = savedSelectedModelId || state.selectedModelId;
6484
6833
  const chatListScrollTop = getSavedChatListScrollTop(savedState) ?? state.chatListScrollTop;
6485
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));
6486
6838
  const selectedModelId = state.models.some(model => model.id === preferredModelId) ? preferredModelId : state.models[0]?.id || '';
6487
- 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 || '';
6488
6841
  sessions = await loadSelectedSessionMessages(sessions, selectedSessionId);
6489
6842
  const preferredViewMode = savedViewMode || state.viewMode;
6490
- 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;
6491
6846
  return {
6492
6847
  ...state,
6493
6848
  aiSessionTitleGenerationEnabled,
@@ -6496,13 +6851,18 @@ const loadContent = async (state, savedState) => {
6496
6851
  composerDropEnabled,
6497
6852
  emitStreamingFunctionCallEvents,
6498
6853
  initial: false,
6854
+ lastNormalViewMode,
6499
6855
  messagesScrollTop,
6500
6856
  openApiApiKey,
6501
6857
  openApiApiKeyInput: openApiApiKey,
6502
6858
  openRouterApiKey,
6503
6859
  openRouterApiKeyInput: openRouterApiKey,
6504
6860
  passIncludeObfuscation,
6861
+ projectExpandedIds,
6862
+ projectListScrollTop,
6863
+ projects,
6505
6864
  selectedModelId,
6865
+ selectedProjectId,
6506
6866
  selectedSessionId,
6507
6867
  sessions,
6508
6868
  streamingEnabled,
@@ -6589,6 +6949,219 @@ const getCss = (composerHeight, listItemHeight, chatMessageFontSize, chatMessage
6589
6949
  --ChatMessageLineHeight: ${chatMessageLineHeight}px;
6590
6950
  --ChatMessageFontFamily: ${chatMessageFontFamily};
6591
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
+ }
6592
7165
  `;
6593
7166
  if (!renderHtmlCss.trim()) {
6594
7167
  return baseCss;
@@ -6666,6 +7239,19 @@ const ChatList = 'ChatList';
6666
7239
  const ChatListEmpty = 'ChatListEmpty';
6667
7240
  const ChatListItem = 'ChatListItem';
6668
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';
6669
7255
  const Markdown = 'Markdown';
6670
7256
  const MarkdownTable = 'MarkdownTable';
6671
7257
  const Message = 'Message';
@@ -6716,6 +7302,7 @@ const HandleDragLeave = 28;
6716
7302
  const HandleDrop = 29;
6717
7303
  const HandleDragEnterChatView = 30;
6718
7304
  const HandleDragOverChatView = 31;
7305
+ const HandleProjectListScroll = 32;
6719
7306
 
6720
7307
  const getModelLabel = model => {
6721
7308
  if (model.provider === 'openRouter') {
@@ -6832,82 +7419,6 @@ const getChatSendAreaDom = (composerValue, models, selectedModelId, usageOvervie
6832
7419
  }, ...getChatSelectVirtualDom(models, selectedModelId), ...(usageOverviewEnabled ? getUsageOverviewDom(tokensUsed, tokensMax) : []), ...getSendButtonDom(isSendDisabled)];
6833
7420
  };
6834
7421
 
6835
- const getBackButtonVirtualDom = () => {
6836
- return [{
6837
- childCount: 1,
6838
- className: IconButton,
6839
- name: Back,
6840
- onClick: HandleClickBack,
6841
- role: Button$2,
6842
- title: backToChats(),
6843
- type: Button$1
6844
- }, {
6845
- childCount: 0,
6846
- className: 'MaskIcon MaskIconArrowLeft',
6847
- type: Div
6848
- }];
6849
- };
6850
-
6851
- const getHeaderActionVirtualDom = item => {
6852
- return [{
6853
- childCount: 1,
6854
- className: IconButton,
6855
- name: item.name,
6856
- onClick: item.onClick,
6857
- title: item.title,
6858
- type: Button$1
6859
- }, {
6860
- childCount: 0,
6861
- className: item.icon,
6862
- type: Div
6863
- }];
6864
- };
6865
-
6866
- const getChatHeaderActionsDom = () => {
6867
- const items = [{
6868
- icon: 'MaskIcon MaskIconDebugPause',
6869
- name: SessionDebug,
6870
- onClick: HandleClickSessionDebug,
6871
- title: debug()
6872
- }, {
6873
- icon: 'MaskIcon MaskIconAdd',
6874
- name: CreateSession,
6875
- onClick: HandleClickNew,
6876
- title: newChat()
6877
- }, {
6878
- icon: 'MaskIcon MaskIconSettingsGear',
6879
- name: Settings,
6880
- onClick: HandleClickSettings,
6881
- title: settings()
6882
- }, {
6883
- icon: 'MaskIcon MaskIconClose',
6884
- name: CloseChat,
6885
- onClick: HandleClickClose,
6886
- title: closeChat()
6887
- }];
6888
- return [{
6889
- childCount: items.length,
6890
- className: ChatActions,
6891
- type: Div
6892
- }, ...items.flatMap(getHeaderActionVirtualDom)];
6893
- };
6894
-
6895
- const getChatHeaderDomDetailMode = selectedSessionTitle => {
6896
- return [{
6897
- childCount: 2,
6898
- className: ChatHeader,
6899
- type: Div
6900
- }, {
6901
- childCount: 2,
6902
- className: ChatName,
6903
- type: Div
6904
- }, ...getBackButtonVirtualDom(), {
6905
- childCount: 1,
6906
- className: Label,
6907
- type: Span
6908
- }, text(selectedSessionTitle), ...getChatHeaderActionsDom()];
6909
- };
6910
-
6911
7422
  const getInlineNodeDom = inlineNode => {
6912
7423
  if (inlineNode.type === 'text') {
6913
7424
  return [text(inlineNode.text)];
@@ -7447,8 +7958,12 @@ const getElementType = tagName => {
7447
7958
  return inlineTags.has(tagName) ? Span : Div;
7448
7959
  }
7449
7960
  };
7961
+ const isHttpUrl$1 = url => {
7962
+ const normalized = url.trim().toLowerCase();
7963
+ return normalized.startsWith('http://') || normalized.startsWith('https://');
7964
+ };
7450
7965
  const normalizeUrl = url => {
7451
- return url.toLowerCase().startsWith('javascript:') ? '#' : url;
7966
+ return isHttpUrl$1(url) ? url : '#';
7452
7967
  };
7453
7968
  const getElementAttributes = node => {
7454
7969
  const attributes = {};
@@ -7593,6 +8108,13 @@ const markdownInlineRegex = /\[([^\]]+)\]\(([^)]+)\)|\*\*([^*]+)\*\*|\*([^*]+)\*
7593
8108
  const markdownTableSeparatorCellRegex = /^:?-{3,}:?$/;
7594
8109
  const fencedCodeBlockRegex = /^```/;
7595
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
+ };
7596
8118
  const normalizeEscapedNewlines = value => {
7597
8119
  if (value.includes('\\n')) {
7598
8120
  return value.replaceAll(/\\r\\n|\\n/g, '\n');
@@ -7662,7 +8184,7 @@ const parseInlineNodes = value => {
7662
8184
  }
7663
8185
  if (linkText && href) {
7664
8186
  nodes.push({
7665
- href,
8187
+ href: sanitizeUrl(href),
7666
8188
  text: linkText,
7667
8189
  type: 'link'
7668
8190
  });
@@ -7870,6 +8392,204 @@ const getMessagesDom = (messages, openRouterApiKeyInput, openApiApiKeyInput = ''
7870
8392
  }, ...messages.flatMap(message => getChatMessageDom(message, openRouterApiKeyInput, openApiApiKeyInput, openRouterApiKeyState))];
7871
8393
  };
7872
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
+
7873
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) => {
7874
8594
  const selectedSession = sessions.find(session => session.id === selectedSessionId);
7875
8595
  const selectedSessionTitle = selectedSession?.title || chatTitle();
@@ -7904,7 +8624,7 @@ const getChatHeaderListModeDom = () => {
7904
8624
  childCount: 1,
7905
8625
  className: Label,
7906
8626
  type: Span
7907
- }, text(chats()), ...getChatHeaderActionsDom()];
8627
+ }, text(chats()), ...getChatHeaderActionsDom('list')];
7908
8628
  };
7909
8629
 
7910
8630
  const getEmptyChatSessionsDom = () => {
@@ -7991,8 +8711,10 @@ const getChatModeUnsupportedVirtualDom = () => {
7991
8711
  }, text(unknownViewMode())];
7992
8712
  };
7993
8713
 
7994
- 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) => {
7995
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);
7996
8718
  case 'detail':
7997
8719
  return getChatModeDetailVirtualDom(sessions, selectedSessionId, composerValue, openRouterApiKeyInput, openApiApiKeyInput, models, selectedModelId, usageOverviewEnabled, tokensUsed, tokensMax, openRouterApiKeyState, composerHeight, composerFontSize, composerFontFamily, composerLineHeight, messagesScrollTop, composerDropActive, composerDropEnabled);
7998
8720
  case 'list':
@@ -8018,7 +8740,11 @@ const renderItems = (oldState, newState) => {
8018
8740
  openApiApiKeyInput,
8019
8741
  openRouterApiKeyInput,
8020
8742
  openRouterApiKeyState,
8743
+ projectExpandedIds,
8744
+ projectListScrollTop,
8745
+ projects,
8021
8746
  selectedModelId,
8747
+ selectedProjectId,
8022
8748
  selectedSessionId,
8023
8749
  sessions,
8024
8750
  tokensMax,
@@ -8030,7 +8756,7 @@ const renderItems = (oldState, newState) => {
8030
8756
  if (initial) {
8031
8757
  return [SetDom2, uid, []];
8032
8758
  }
8033
- 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);
8034
8760
  return [SetDom2, uid, dom];
8035
8761
  };
8036
8762
 
@@ -8167,6 +8893,9 @@ const renderEventListeners = () => {
8167
8893
  }, {
8168
8894
  name: HandleMessagesScroll,
8169
8895
  params: ['handleMessagesScroll', 'event.target.scrollTop']
8896
+ }, {
8897
+ name: HandleProjectListScroll,
8898
+ params: ['handleProjectListScroll', 'event.target.scrollTop']
8170
8899
  }, {
8171
8900
  name: HandleMessagesContextMenu,
8172
8901
  params: ['handleMessagesContextMenu'],
@@ -8214,20 +8943,30 @@ const saveState = state => {
8214
8943
  const {
8215
8944
  chatListScrollTop,
8216
8945
  composerValue,
8946
+ lastNormalViewMode,
8217
8947
  messagesScrollTop,
8218
8948
  nextMessageId,
8949
+ projectExpandedIds,
8950
+ projectListScrollTop,
8951
+ projects,
8219
8952
  renamingSessionId,
8220
8953
  selectedModelId,
8954
+ selectedProjectId,
8221
8955
  selectedSessionId,
8222
8956
  viewMode
8223
8957
  } = state;
8224
8958
  return {
8225
8959
  chatListScrollTop,
8226
8960
  composerValue,
8961
+ lastNormalViewMode,
8227
8962
  messagesScrollTop,
8228
8963
  nextMessageId,
8964
+ projectExpandedIds,
8965
+ projectListScrollTop,
8966
+ projects,
8229
8967
  renamingSessionId,
8230
8968
  selectedModelId,
8969
+ selectedProjectId,
8231
8970
  selectedSessionId,
8232
8971
  viewMode
8233
8972
  };
@@ -8326,6 +9065,7 @@ const commandMap = {
8326
9065
  'Chat.handleMessagesContextMenu': wrapCommand(handleMessagesContextMenu),
8327
9066
  'Chat.handleMessagesScroll': wrapCommand(handleMessagesScroll),
8328
9067
  'Chat.handleModelChange': wrapCommand(handleModelChange),
9068
+ 'Chat.handleProjectListScroll': wrapCommand(handleProjectListScroll),
8329
9069
  'Chat.handleSubmit': wrapCommand(handleSubmit),
8330
9070
  'Chat.initialize': initialize,
8331
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.9.0",
3
+ "version": "4.0.0",
4
4
  "description": "Chat View Worker",
5
5
  "repository": {
6
6
  "type": "git",