@lvce-editor/chat-view 1.13.0 → 1.15.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.
@@ -1036,6 +1036,7 @@ const create$2 = rpcId => {
1036
1036
  };
1037
1037
 
1038
1038
  const {
1039
+ invoke: invoke$1,
1039
1040
  set: set$2
1040
1041
  } = create$2(ExtensionHostWorker);
1041
1042
 
@@ -1048,9 +1049,15 @@ const sendMessagePortToExtensionHostWorker$1 = async (port, rpcId = 0) => {
1048
1049
  const command = 'HandleMessagePort.handleMessagePort2';
1049
1050
  await invokeAndTransfer('SendMessagePortToExtensionHostWorker.sendMessagePortToExtensionHostWorker', port, command, rpcId);
1050
1051
  };
1052
+ const activateByEvent$1 = (event, assetDir, platform) => {
1053
+ return invoke('ExtensionHostManagement.activateByEvent', event, assetDir, platform);
1054
+ };
1051
1055
  const getPreference = async key => {
1052
1056
  return await invoke('Preferences.get', key);
1053
1057
  };
1058
+ const openExternal = async uri => {
1059
+ await invoke('Open.openExternal', uri);
1060
+ };
1054
1061
 
1055
1062
  const toCommandId = key => {
1056
1063
  const dotIndex = key.indexOf('.');
@@ -1191,25 +1198,128 @@ const i18nString = (key, placeholders = emptyObject) => {
1191
1198
  return key.replaceAll(RE_PLACEHOLDER, replacer);
1192
1199
  };
1193
1200
 
1194
- const chatTitle = i18nString('Chat');
1195
- const chats = i18nString('Chats');
1196
- const newChat = i18nString('New Chat');
1197
- const backToChats = i18nString('Back to chats');
1198
- const settings = i18nString('Settings');
1199
- const closeChat = i18nString('Close Chat');
1200
- const clickToOpenNewChat = i18nString('Click the + button to open a new chat.');
1201
- const startConversation = i18nString('Start a conversation by typing below.');
1202
- i18nString('You');
1203
- i18nString('Assistant');
1204
- const composePlaceholder = i18nString('Type your message. Enter to send, Shift+Enter for newline.');
1205
- const sendMessage = i18nString('Send message');
1206
- i18nString('Send');
1207
- const deleteChatSession$1 = i18nString('Delete chat session');
1208
- const defaultSessionTitle = i18nString('Chat 1');
1209
- const dummyChatA = i18nString('Dummy Chat A');
1210
- const dummyChatB = i18nString('Dummy Chat B');
1211
- const dummyChatC = i18nString('Dummy Chat C');
1201
+ const chatTitle = () => {
1202
+ return i18nString('Chat');
1203
+ };
1204
+ const chats = () => {
1205
+ return i18nString('Chats');
1206
+ };
1207
+ const newChat = () => {
1208
+ return i18nString('New Chat');
1209
+ };
1210
+ const backToChats = () => {
1211
+ return i18nString('Back to chats');
1212
+ };
1213
+ const settings = () => {
1214
+ return i18nString('Settings');
1215
+ };
1216
+ const closeChat = () => {
1217
+ return i18nString('Close Chat');
1218
+ };
1219
+ const clickToOpenNewChat = () => {
1220
+ return i18nString('Click the + button to open a new chat.');
1221
+ };
1222
+ const startConversation = () => {
1223
+ return i18nString('Start a conversation by typing below.');
1224
+ };
1225
+ const composePlaceholder = () => {
1226
+ return i18nString('Type your message. Enter to send, Shift+Enter for newline.');
1227
+ };
1228
+ const openRouterApiKeyPlaceholder = () => {
1229
+ return i18nString('Enter OpenRouter API key');
1230
+ };
1231
+ const openApiApiKeyPlaceholder = () => {
1232
+ return i18nString('Enter OpenAI API key');
1233
+ };
1234
+ const sendMessage = () => {
1235
+ return i18nString('Send message');
1236
+ };
1237
+ const save = () => {
1238
+ return i18nString('Save');
1239
+ };
1240
+ const getOpenRouterApiKey = () => {
1241
+ return i18nString('Get API Key');
1242
+ };
1243
+ const getOpenApiApiKey = () => {
1244
+ return i18nString('Get API Key');
1245
+ };
1246
+ const deleteChatSession$1 = () => {
1247
+ return i18nString('Delete chat session');
1248
+ };
1249
+ const defaultSessionTitle = () => {
1250
+ return i18nString('Chat 1');
1251
+ };
1252
+ const dummyChatA = () => {
1253
+ return i18nString('Dummy Chat A');
1254
+ };
1255
+ const dummyChatB = () => {
1256
+ return i18nString('Dummy Chat B');
1257
+ };
1258
+ const dummyChatC = () => {
1259
+ return i18nString('Dummy Chat C');
1260
+ };
1261
+ const unknownViewMode = () => {
1262
+ return i18nString('Unknown view mode');
1263
+ };
1264
+
1265
+ /* eslint-disable @cspell/spellchecker */
1212
1266
 
1267
+ const getDefaultModels = () => {
1268
+ const defaultModelId = 'test';
1269
+ return [{
1270
+ id: defaultModelId,
1271
+ name: 'test',
1272
+ provider: 'test'
1273
+ }, {
1274
+ id: 'codex-5.3',
1275
+ name: 'Codex 5.3',
1276
+ provider: 'openRouter'
1277
+ }, {
1278
+ id: 'claude-code',
1279
+ name: 'Claude Code',
1280
+ provider: 'openRouter'
1281
+ }, {
1282
+ id: 'claude-haiku',
1283
+ name: 'Claude Haiku',
1284
+ provider: 'openRouter'
1285
+ }, {
1286
+ id: 'openRouter/openai/gpt-4o-mini',
1287
+ name: 'GPT-4o Mini',
1288
+ provider: 'openRouter'
1289
+ }, {
1290
+ id: 'openRouter/anthropic/claude-3.5-haiku',
1291
+ name: 'Claude 3.5 Haiku',
1292
+ provider: 'openRouter'
1293
+ }, {
1294
+ id: 'openRouter/google/gemini-2.0-flash-001',
1295
+ name: 'Gemini 2.0 Flash',
1296
+ provider: 'openRouter'
1297
+ }, {
1298
+ id: 'openRouter/openai/gpt-oss-20b:free',
1299
+ name: 'GPT OSS 20B (Free)',
1300
+ provider: 'openRouter'
1301
+ }, {
1302
+ id: 'openRouter/openai/gpt-oss-120b:free',
1303
+ name: 'GPT OSS 120B (Free)',
1304
+ provider: 'openRouter'
1305
+ }, {
1306
+ id: 'openRouter/meta-llama/llama-3.3-70b-instruct:free',
1307
+ name: 'Llama 3.3 70B Instruct (Free)',
1308
+ provider: 'openRouter'
1309
+ }, {
1310
+ id: 'openRouter/google/gemma-3-27b-it:free',
1311
+ name: 'Gemma 3 27B IT (Free)',
1312
+ provider: 'openRouter'
1313
+ }, {
1314
+ id: 'openRouter/qwen/qwen3-coder:free',
1315
+ name: 'Qwen3 Coder (Free)',
1316
+ provider: 'openRouter'
1317
+ }, {
1318
+ id: 'openRouter/mistralai/mistral-small-3.1-24b-instruct:free',
1319
+ name: 'Mistral Small 3.1 24B Instruct (Free)',
1320
+ provider: 'openRouter'
1321
+ }];
1322
+ };
1213
1323
  const createDefaultState = () => {
1214
1324
  const defaultSessionId = 'session-1';
1215
1325
  const defaultModelId = 'test';
@@ -1225,38 +1335,17 @@ const createDefaultState = () => {
1225
1335
  inputSource: 'script',
1226
1336
  lastSubmittedSessionId: '',
1227
1337
  listItemHeight: 40,
1228
- models: [{
1229
- id: defaultModelId,
1230
- name: 'test',
1231
- provider: 'test'
1232
- }, {
1233
- id: 'codex-5.3',
1234
- name: 'Codex 5.3',
1235
- provider: 'openRouter'
1236
- }, {
1237
- id: 'claude-code',
1238
- name: 'Claude Code',
1239
- provider: 'openRouter'
1240
- }, {
1241
- id: 'claude-haiku',
1242
- name: 'Claude Haiku',
1243
- provider: 'openRouter'
1244
- }, {
1245
- id: 'openRouter/openai/gpt-4o-mini',
1246
- name: 'GPT-4o Mini',
1247
- provider: 'openRouter'
1248
- }, {
1249
- id: 'openRouter/anthropic/claude-3.5-haiku',
1250
- name: 'Claude 3.5 Haiku',
1251
- provider: 'openRouter'
1252
- }, {
1253
- id: 'openRouter/google/gemini-2.0-flash-001',
1254
- name: 'Gemini 2.0 Flash',
1255
- provider: 'openRouter'
1256
- }],
1338
+ mockApiCommandId: '',
1339
+ models: getDefaultModels(),
1257
1340
  nextMessageId: 1,
1341
+ openApiApiBaseUrl: 'https://api.openai.com/v1',
1342
+ openApiApiKey: '',
1343
+ openApiApiKeyInput: '',
1344
+ openApiApiKeysSettingsUrl: 'https://platform.openai.com/api-keys',
1258
1345
  openRouterApiBaseUrl: 'https://openrouter.ai/api/v1',
1259
1346
  openRouterApiKey: '',
1347
+ openRouterApiKeyInput: '',
1348
+ openRouterApiKeysSettingsUrl: 'https://openrouter.ai/settings/keys',
1260
1349
  platform: 0,
1261
1350
  renamingSessionId: '',
1262
1351
  selectedModelId: defaultModelId,
@@ -1264,12 +1353,13 @@ const createDefaultState = () => {
1264
1353
  sessions: [{
1265
1354
  id: defaultSessionId,
1266
1355
  messages: [],
1267
- title: defaultSessionTitle
1356
+ title: defaultSessionTitle()
1268
1357
  }],
1269
1358
  tokensMax: 0,
1270
1359
  tokensUsed: 0,
1271
1360
  uid: 0,
1272
1361
  usageOverviewEnabled: false,
1362
+ useMockApi: false,
1273
1363
  viewMode: 'list',
1274
1364
  warningCount: 0,
1275
1365
  width: 0,
@@ -1369,12 +1459,15 @@ const SetPatches = 'Viewlet.setPatches';
1369
1459
 
1370
1460
  const FocusChatInput = 8000;
1371
1461
 
1372
- const Button$1 = 'button';
1462
+ const Button$2 = 'button';
1373
1463
 
1374
- const Button = 1;
1464
+ const Button$1 = 1;
1375
1465
  const Div = 4;
1466
+ const Input = 6;
1376
1467
  const Span = 8;
1377
1468
  const Text = 12;
1469
+ const Li = 48;
1470
+ const Ol = 49;
1378
1471
  const P = 50;
1379
1472
  const TextArea = 62;
1380
1473
  const Select$1 = 63;
@@ -1697,6 +1790,10 @@ const getKeyBindings = () => {
1697
1790
  }];
1698
1791
  };
1699
1792
 
1793
+ const getSelectedSessionId = state => {
1794
+ return state.selectedSessionId;
1795
+ };
1796
+
1700
1797
  const getListIndex = (state, eventX, eventY) => {
1701
1798
  const {
1702
1799
  headerHeight,
@@ -1734,96 +1831,143 @@ const handleChatListContextMenu = async (state, eventX, eventY) => {
1734
1831
  return state;
1735
1832
  };
1736
1833
 
1737
- const LVCE_CHAT_SESSIONS_DB_NAME = 'lvce-chat-view-sessions';
1738
- const LVCE_CHAT_SESSIONS_DB_VERSION = 1;
1739
- const LVCE_CHAT_SESSIONS_STORE = 'chat-sessions';
1740
1834
  const toError = error => {
1741
1835
  if (error instanceof Error) {
1742
1836
  return error;
1743
1837
  }
1744
1838
  return new Error('IndexedDB request failed');
1745
1839
  };
1840
+
1746
1841
  const requestToPromise = async createRequest => {
1747
1842
  const request = createRequest();
1748
- return new Promise((resolve, reject) => {
1749
- request.addEventListener('success', () => {
1750
- resolve(request.result);
1751
- });
1752
- request.addEventListener('error', () => {
1753
- reject(toError(request.error));
1754
- });
1843
+ const {
1844
+ promise,
1845
+ reject,
1846
+ resolve
1847
+ } = Promise.withResolvers();
1848
+ request.addEventListener('success', () => {
1849
+ resolve(request.result);
1755
1850
  });
1756
- };
1757
- const transactionToPromise = async createTransaction => {
1758
- const transaction = createTransaction();
1759
- return new Promise((resolve, reject) => {
1760
- transaction.addEventListener('complete', () => {
1761
- resolve();
1762
- });
1763
- transaction.addEventListener('error', () => {
1764
- reject(toError(transaction.error));
1765
- });
1766
- transaction.addEventListener('abort', () => {
1767
- reject(toError(transaction.error));
1768
- });
1851
+ request.addEventListener('error', () => {
1852
+ reject(toError(request.error));
1769
1853
  });
1854
+ return promise;
1770
1855
  };
1771
- const openSessionsDatabase = async () => {
1772
- const request = indexedDB.open(LVCE_CHAT_SESSIONS_DB_NAME, LVCE_CHAT_SESSIONS_DB_VERSION);
1856
+
1857
+ const openSessionsDatabase = async (databaseName, databaseVersion, storeName) => {
1858
+ const request = indexedDB.open(databaseName, databaseVersion);
1773
1859
  request.addEventListener('upgradeneeded', () => {
1774
1860
  const database = request.result;
1775
- if (!database.objectStoreNames.contains(LVCE_CHAT_SESSIONS_STORE)) {
1776
- database.createObjectStore(LVCE_CHAT_SESSIONS_STORE, {
1861
+ if (!database.objectStoreNames.contains(storeName)) {
1862
+ database.createObjectStore(storeName, {
1777
1863
  keyPath: 'id'
1778
1864
  });
1779
1865
  }
1780
1866
  });
1781
1867
  return requestToPromise(() => request);
1782
1868
  };
1869
+
1870
+ const getDatabase = async (getDatabasePromise, setDatabasePromise, databaseName, databaseVersion, storeName) => {
1871
+ const existingDatabasePromise = getDatabasePromise();
1872
+ if (existingDatabasePromise) {
1873
+ return existingDatabasePromise;
1874
+ }
1875
+ const nextDatabasePromise = openSessionsDatabase(databaseName, databaseVersion, storeName);
1876
+ setDatabasePromise(nextDatabasePromise);
1877
+ return nextDatabasePromise;
1878
+ };
1879
+
1880
+ const transactionToPromise = async createTransaction => {
1881
+ const transaction = createTransaction();
1882
+ const {
1883
+ promise,
1884
+ reject,
1885
+ resolve
1886
+ } = Promise.withResolvers();
1887
+ transaction.addEventListener('complete', () => {
1888
+ resolve();
1889
+ });
1890
+ transaction.addEventListener('error', () => {
1891
+ reject(toError(transaction.error));
1892
+ });
1893
+ transaction.addEventListener('abort', () => {
1894
+ reject(toError(transaction.error));
1895
+ });
1896
+ return promise;
1897
+ };
1898
+
1899
+ const clear = async (getDatabasePromise, setDatabasePromise, databaseName, databaseVersion, storeName) => {
1900
+ const database = await getDatabase(getDatabasePromise, setDatabasePromise, databaseName, databaseVersion, storeName);
1901
+ const transaction = database.transaction(storeName, 'readwrite');
1902
+ const createTransaction = () => transaction;
1903
+ const store = transaction.objectStore(storeName);
1904
+ store.clear();
1905
+ await transactionToPromise(createTransaction);
1906
+ };
1907
+
1908
+ const deleteSession$1 = async (getDatabasePromise, setDatabasePromise, databaseName, databaseVersion, storeName, id) => {
1909
+ const database = await getDatabase(getDatabasePromise, setDatabasePromise, databaseName, databaseVersion, storeName);
1910
+ const transaction = database.transaction(storeName, 'readwrite');
1911
+ const createTransaction = () => transaction;
1912
+ const store = transaction.objectStore(storeName);
1913
+ store.delete(id);
1914
+ await transactionToPromise(createTransaction);
1915
+ };
1916
+
1917
+ const getSession = async (getDatabasePromise, setDatabasePromise, databaseName, databaseVersion, storeName, id) => {
1918
+ const database = await getDatabase(getDatabasePromise, setDatabasePromise, databaseName, databaseVersion, storeName);
1919
+ const transaction = database.transaction(storeName, 'readonly');
1920
+ const store = transaction.objectStore(storeName);
1921
+ const result = await requestToPromise(() => store.get(id));
1922
+ return result;
1923
+ };
1924
+
1925
+ const listSessions = async (getDatabasePromise, setDatabasePromise, databaseName, databaseVersion, storeName) => {
1926
+ const database = await getDatabase(getDatabasePromise, setDatabasePromise, databaseName, databaseVersion, storeName);
1927
+ const transaction = database.transaction(storeName, 'readonly');
1928
+ const store = transaction.objectStore(storeName);
1929
+ const result = await requestToPromise(() => store.getAll());
1930
+ return result;
1931
+ };
1932
+
1933
+ const setSession = async (getDatabasePromise, setDatabasePromise, databaseName, databaseVersion, storeName, session) => {
1934
+ const database = await getDatabase(getDatabasePromise, setDatabasePromise, databaseName, databaseVersion, storeName);
1935
+ const transaction = database.transaction(storeName, 'readwrite');
1936
+ const createTransaction = () => transaction;
1937
+ const store = transaction.objectStore(storeName);
1938
+ store.put(session);
1939
+ await transactionToPromise(createTransaction);
1940
+ };
1941
+
1783
1942
  class IndexedDbChatSessionStorage {
1784
- async getDatabase() {
1785
- if (!this.databasePromise) {
1786
- this.databasePromise = openSessionsDatabase();
1787
- }
1788
- return this.databasePromise;
1943
+ constructor(options = {}) {
1944
+ this.state = {
1945
+ databaseName: options.databaseName || 'lvce-chat-view-sessions',
1946
+ databasePromise: undefined,
1947
+ databaseVersion: options.databaseVersion || 1,
1948
+ storeName: options.storeName || 'chat-sessions'
1949
+ };
1789
1950
  }
1951
+ getDatabasePromise = () => {
1952
+ return this.state.databasePromise;
1953
+ };
1954
+ setDatabasePromise = databasePromise => {
1955
+ this.state.databasePromise = databasePromise;
1956
+ };
1790
1957
  async clear() {
1791
- const database = await this.getDatabase();
1792
- const transaction = database.transaction(LVCE_CHAT_SESSIONS_STORE, 'readwrite');
1793
- const createTransaction = () => transaction;
1794
- const store = transaction.objectStore(LVCE_CHAT_SESSIONS_STORE);
1795
- store.clear();
1796
- await transactionToPromise(createTransaction);
1958
+ return clear(this.getDatabasePromise, this.setDatabasePromise, this.state.databaseName, this.state.databaseVersion, this.state.storeName);
1797
1959
  }
1798
1960
  async deleteSession(id) {
1799
- const database = await this.getDatabase();
1800
- const transaction = database.transaction(LVCE_CHAT_SESSIONS_STORE, 'readwrite');
1801
- const createTransaction = () => transaction;
1802
- const store = transaction.objectStore(LVCE_CHAT_SESSIONS_STORE);
1803
- store.delete(id);
1804
- await transactionToPromise(createTransaction);
1961
+ return deleteSession$1(this.getDatabasePromise, this.setDatabasePromise, this.state.databaseName, this.state.databaseVersion, this.state.storeName, id);
1805
1962
  }
1806
1963
  async getSession(id) {
1807
- const database = await this.getDatabase();
1808
- const transaction = database.transaction(LVCE_CHAT_SESSIONS_STORE, 'readonly');
1809
- const store = transaction.objectStore(LVCE_CHAT_SESSIONS_STORE);
1810
- const result = await requestToPromise(() => store.get(id));
1811
- return result;
1964
+ return getSession(this.getDatabasePromise, this.setDatabasePromise, this.state.databaseName, this.state.databaseVersion, this.state.storeName, id);
1812
1965
  }
1813
1966
  async listSessions() {
1814
- const database = await this.getDatabase();
1815
- const transaction = database.transaction(LVCE_CHAT_SESSIONS_STORE, 'readonly');
1816
- const store = transaction.objectStore(LVCE_CHAT_SESSIONS_STORE);
1817
- const result = await requestToPromise(() => store.getAll());
1818
- return result;
1967
+ return listSessions(this.getDatabasePromise, this.setDatabasePromise, this.state.databaseName, this.state.databaseVersion, this.state.storeName);
1819
1968
  }
1820
1969
  async setSession(session) {
1821
- const database = await this.getDatabase();
1822
- const transaction = database.transaction(LVCE_CHAT_SESSIONS_STORE, 'readwrite');
1823
- const createTransaction = () => transaction;
1824
- const store = transaction.objectStore(LVCE_CHAT_SESSIONS_STORE);
1825
- store.put(session);
1826
- await transactionToPromise(createTransaction);
1970
+ return setSession(this.getDatabasePromise, this.setDatabasePromise, this.state.databaseName, this.state.databaseVersion, this.state.storeName, session);
1827
1971
  }
1828
1972
  }
1829
1973
 
@@ -1956,14 +2100,45 @@ const deleteSession = async (state, id) => {
1956
2100
  };
1957
2101
  };
1958
2102
 
1959
- const focusInput = state => {
1960
- return {
1961
- ...state,
1962
- focus: 'composer',
1963
- focused: true
1964
- };
2103
+ const handleClickOpenApiApiKeySettings = async state => {
2104
+ await openExternal(state.openApiApiKeysSettingsUrl);
2105
+ return state;
1965
2106
  };
1966
2107
 
2108
+ const handleClickOpenRouterApiKeySettings = async state => {
2109
+ await openExternal(state.openRouterApiKeysSettingsUrl);
2110
+ return state;
2111
+ };
2112
+
2113
+ const openApiApiKeyRequiredMessage = 'OpenAI API key is not configured. Enter your OpenAI API key below and click Save.';
2114
+ const openApiRequestFailedMessage = 'OpenAI request failed.';
2115
+ const openRouterApiKeyRequiredMessage = 'OpenRouter API key is not configured. Enter your OpenRouter API key below and click Save.';
2116
+ const openRouterRequestFailedMessage = 'OpenRouter request failed. Possible reasons:';
2117
+ const openRouterTooManyRequestsMessage = 'OpenRouter rate limit reached (429). Please try again soon. Helpful tips:';
2118
+ const openRouterRequestFailureReasons = ['ContentSecurityPolicyViolation: Check DevTools for details.', 'OpenRouter server offline: Check DevTools for details.', 'Check your internet connection.'];
2119
+ const openRouterTooManyRequestsReasons = ['Wait a short time and retry your request.', 'Reduce request frequency to avoid rate limits.', 'Use a different model if this one is saturated.'];
2120
+
2121
+ const activateByEvent = (event, assetDir, platform) => {
2122
+ // @ts-ignore
2123
+ return activateByEvent$1(event, assetDir, platform);
2124
+ };
2125
+
2126
+ const executeProvider = async ({
2127
+ assetDir,
2128
+ event,
2129
+ method,
2130
+ noProviderFoundMessage,
2131
+ params,
2132
+ platform
2133
+ }) => {
2134
+ await activateByEvent(event, assetDir, platform);
2135
+ // @ts-ignore
2136
+ const result = invoke$1(method, ...params);
2137
+ return result;
2138
+ };
2139
+
2140
+ const CommandExecute = 'ExtensionHostCommand.executeCommand';
2141
+
1967
2142
  const delay = async ms => {
1968
2143
  await new Promise(resolve => setTimeout(resolve, ms));
1969
2144
  };
@@ -1973,6 +2148,10 @@ const getMockAiResponse = async userMessage => {
1973
2148
  return `Mock AI response: I received "${userMessage}".`;
1974
2149
  };
1975
2150
 
2151
+ const getOpenApiApiEndpoint = openApiApiBaseUrl => {
2152
+ return `${openApiApiBaseUrl}/chat/completions`;
2153
+ };
2154
+
1976
2155
  const getTextContent = content => {
1977
2156
  if (typeof content === 'string') {
1978
2157
  return content;
@@ -1994,75 +2173,482 @@ const getTextContent = content => {
1994
2173
  return textParts.join('\n');
1995
2174
  };
1996
2175
 
1997
- const defaultOpenRouterApiBaseUrl = 'https://openrouter.ai/api/v1';
2176
+ const getOpenApiAssistantText = async (messages, modelId, openApiApiKey, openApiApiBaseUrl) => {
2177
+ let response;
2178
+ try {
2179
+ response = await fetch(getOpenApiApiEndpoint(openApiApiBaseUrl), {
2180
+ body: JSON.stringify({
2181
+ messages: messages.map(message => ({
2182
+ content: message.text,
2183
+ role: message.role
2184
+ })),
2185
+ model: modelId
2186
+ }),
2187
+ headers: {
2188
+ Authorization: `Bearer ${openApiApiKey}`,
2189
+ 'Content-Type': 'application/json'
2190
+ },
2191
+ method: 'POST'
2192
+ });
2193
+ } catch {
2194
+ return {
2195
+ details: 'request-failed',
2196
+ type: 'error'
2197
+ };
2198
+ }
2199
+ if (!response.ok) {
2200
+ return {
2201
+ details: 'http-error',
2202
+ statusCode: response.status,
2203
+ type: 'error'
2204
+ };
2205
+ }
2206
+ let parsed;
2207
+ try {
2208
+ parsed = await response.json();
2209
+ } catch {
2210
+ return {
2211
+ details: 'request-failed',
2212
+ type: 'error'
2213
+ };
2214
+ }
2215
+ if (!parsed || typeof parsed !== 'object') {
2216
+ return {
2217
+ text: '',
2218
+ type: 'success'
2219
+ };
2220
+ }
2221
+ const choices = Reflect.get(parsed, 'choices');
2222
+ if (!Array.isArray(choices)) {
2223
+ return {
2224
+ text: '',
2225
+ type: 'success'
2226
+ };
2227
+ }
2228
+ const firstChoice = choices[0];
2229
+ if (!firstChoice || typeof firstChoice !== 'object') {
2230
+ return {
2231
+ text: '',
2232
+ type: 'success'
2233
+ };
2234
+ }
2235
+ const message = Reflect.get(firstChoice, 'message');
2236
+ if (!message || typeof message !== 'object') {
2237
+ return {
2238
+ text: '',
2239
+ type: 'success'
2240
+ };
2241
+ }
2242
+ const content = Reflect.get(message, 'content');
2243
+ return {
2244
+ text: getTextContent(content),
2245
+ type: 'success'
2246
+ };
2247
+ };
2248
+
2249
+ const getOpenApiModelId = selectedModelId => {
2250
+ const openApiPrefix = 'openapi/';
2251
+ const openAiPrefix = 'openai/';
2252
+ const normalizedModelId = selectedModelId.toLowerCase();
2253
+ if (normalizedModelId.startsWith(openApiPrefix)) {
2254
+ return selectedModelId.slice(openApiPrefix.length);
2255
+ }
2256
+ if (normalizedModelId.startsWith(openAiPrefix)) {
2257
+ return selectedModelId.slice(openAiPrefix.length);
2258
+ }
2259
+ return selectedModelId;
2260
+ };
2261
+
1998
2262
  const getOpenRouterApiEndpoint = openRouterApiBaseUrl => {
1999
- const trimmedBaseUrl = (openRouterApiBaseUrl || defaultOpenRouterApiBaseUrl).replace(/\/+$/, '');
2263
+ const trimmedBaseUrl = openRouterApiBaseUrl.replace(/\/+$/, '');
2000
2264
  return `${trimmedBaseUrl}/chat/completions`;
2001
2265
  };
2002
- const getOpenRouterAssistantText = async (userText, modelId, openRouterApiKey, openRouterApiBaseUrl) => {
2003
- const response = await fetch(getOpenRouterApiEndpoint(openRouterApiBaseUrl), {
2004
- body: JSON.stringify({
2005
- messages: [{
2006
- content: userText,
2007
- role: 'user'
2008
- }],
2009
- model: modelId
2010
- }),
2011
- headers: {
2012
- Authorization: `Bearer ${openRouterApiKey}`,
2013
- 'Content-Type': 'application/json'
2014
- },
2015
- method: 'POST'
2016
- });
2266
+
2267
+ const getOpenRouterKeyEndpoint = openRouterApiBaseUrl => {
2268
+ const trimmedBaseUrl = openRouterApiBaseUrl.replace(/\/+$/, '');
2269
+ return `${trimmedBaseUrl}/auth/key`;
2270
+ };
2271
+
2272
+ const getOpenRouterRaw429Message = async response => {
2273
+ let parsed;
2274
+ try {
2275
+ parsed = await response.json();
2276
+ } catch {
2277
+ return undefined;
2278
+ }
2279
+ if (!parsed || typeof parsed !== 'object') {
2280
+ return undefined;
2281
+ }
2282
+ const error = Reflect.get(parsed, 'error');
2283
+ if (!error || typeof error !== 'object') {
2284
+ return undefined;
2285
+ }
2286
+ const metadata = Reflect.get(error, 'metadata');
2287
+ if (!metadata || typeof metadata !== 'object') {
2288
+ return undefined;
2289
+ }
2290
+ const raw = Reflect.get(metadata, 'raw');
2291
+ if (typeof raw !== 'string' || !raw) {
2292
+ return undefined;
2293
+ }
2294
+ return raw;
2295
+ };
2296
+ const getOpenRouterLimitInfo = async (openRouterApiKey, openRouterApiBaseUrl) => {
2297
+ let response;
2298
+ try {
2299
+ response = await fetch(getOpenRouterKeyEndpoint(openRouterApiBaseUrl), {
2300
+ headers: {
2301
+ Authorization: `Bearer ${openRouterApiKey}`
2302
+ },
2303
+ method: 'GET'
2304
+ });
2305
+ } catch {
2306
+ return undefined;
2307
+ }
2017
2308
  if (!response.ok) {
2018
- throw new Error(`Failed to get OpenRouter response: ${response.status}`);
2309
+ return undefined;
2310
+ }
2311
+ let parsed;
2312
+ try {
2313
+ parsed = await response.json();
2314
+ } catch {
2315
+ return undefined;
2019
2316
  }
2020
- const parsed = await response.json();
2021
2317
  if (!parsed || typeof parsed !== 'object') {
2022
- return '';
2318
+ return undefined;
2319
+ }
2320
+ const data = Reflect.get(parsed, 'data');
2321
+ if (!data || typeof data !== 'object') {
2322
+ return undefined;
2323
+ }
2324
+ const limitRemaining = Reflect.get(data, 'limit_remaining');
2325
+ const limitReset = Reflect.get(data, 'limit_reset');
2326
+ const usage = Reflect.get(data, 'usage');
2327
+ const usageDaily = Reflect.get(data, 'usage_daily');
2328
+ const normalizedLimitInfo = {
2329
+ limitRemaining: typeof limitRemaining === 'number' || limitRemaining === null ? limitRemaining : undefined,
2330
+ limitReset: typeof limitReset === 'string' || limitReset === null ? limitReset : undefined,
2331
+ usage: typeof usage === 'number' ? usage : undefined,
2332
+ usageDaily: typeof usageDaily === 'number' ? usageDaily : undefined
2333
+ };
2334
+ const hasLimitInfo = normalizedLimitInfo.limitRemaining !== undefined || normalizedLimitInfo.limitReset !== undefined || normalizedLimitInfo.usage !== undefined || normalizedLimitInfo.usageDaily !== undefined;
2335
+ if (!hasLimitInfo) {
2336
+ return undefined;
2337
+ }
2338
+ return normalizedLimitInfo;
2339
+ };
2340
+ const getOpenRouterAssistantText = async (messages, modelId, openRouterApiKey, openRouterApiBaseUrl) => {
2341
+ let response;
2342
+ try {
2343
+ response = await fetch(getOpenRouterApiEndpoint(openRouterApiBaseUrl), {
2344
+ body: JSON.stringify({
2345
+ messages: messages.map(message => ({
2346
+ content: message.text,
2347
+ role: message.role
2348
+ })),
2349
+ model: modelId
2350
+ }),
2351
+ headers: {
2352
+ Authorization: `Bearer ${openRouterApiKey}`,
2353
+ 'Content-Type': 'application/json'
2354
+ },
2355
+ method: 'POST'
2356
+ });
2357
+ } catch {
2358
+ return {
2359
+ details: 'request-failed',
2360
+ type: 'error'
2361
+ };
2362
+ }
2363
+ if (!response.ok) {
2364
+ if (response.status === 429) {
2365
+ const retryAfter = response.headers?.get?.('retry-after') ?? null;
2366
+ const rawMessage = await getOpenRouterRaw429Message(response);
2367
+ const limitInfo = await getOpenRouterLimitInfo(openRouterApiKey, openRouterApiBaseUrl);
2368
+ return {
2369
+ details: 'too-many-requests',
2370
+ limitInfo: limitInfo || retryAfter ? {
2371
+ ...limitInfo,
2372
+ retryAfter
2373
+ } : undefined,
2374
+ rawMessage,
2375
+ statusCode: 429,
2376
+ type: 'error'
2377
+ };
2378
+ }
2379
+ return {
2380
+ details: 'http-error',
2381
+ statusCode: response.status,
2382
+ type: 'error'
2383
+ };
2384
+ }
2385
+ let parsed;
2386
+ try {
2387
+ parsed = await response.json();
2388
+ } catch {
2389
+ return {
2390
+ details: 'request-failed',
2391
+ type: 'error'
2392
+ };
2393
+ }
2394
+ if (!parsed || typeof parsed !== 'object') {
2395
+ return {
2396
+ text: '',
2397
+ type: 'success'
2398
+ };
2023
2399
  }
2024
2400
  const choices = Reflect.get(parsed, 'choices');
2025
2401
  if (!Array.isArray(choices)) {
2026
- return '';
2402
+ return {
2403
+ text: '',
2404
+ type: 'success'
2405
+ };
2027
2406
  }
2028
2407
  const firstChoice = choices[0];
2029
2408
  if (!firstChoice || typeof firstChoice !== 'object') {
2030
- return '';
2409
+ return {
2410
+ text: '',
2411
+ type: 'success'
2412
+ };
2031
2413
  }
2032
2414
  const message = Reflect.get(firstChoice, 'message');
2033
2415
  if (!message || typeof message !== 'object') {
2034
- return '';
2416
+ return {
2417
+ text: '',
2418
+ type: 'success'
2419
+ };
2035
2420
  }
2036
2421
  const content = Reflect.get(message, 'content');
2037
- return getTextContent(content);
2422
+ return {
2423
+ text: getTextContent(content),
2424
+ type: 'success'
2425
+ };
2038
2426
  };
2039
2427
 
2428
+ /* eslint-disable @cspell/spellchecker */
2040
2429
  const getOpenRouterModelId = selectedModelId => {
2041
- if (selectedModelId.startsWith('openRouter/')) {
2042
- return selectedModelId.slice('openRouter/'.length);
2430
+ const openRouterPrefix = 'openrouter/';
2431
+ if (selectedModelId.toLowerCase().startsWith(openRouterPrefix)) {
2432
+ return selectedModelId.slice(openRouterPrefix.length);
2043
2433
  }
2044
2434
  return selectedModelId;
2045
2435
  };
2046
2436
 
2437
+ const isOpenApiModel = (selectedModelId, models) => {
2438
+ const selectedModel = models.find(model => model.id === selectedModelId);
2439
+ const normalizedProvider = selectedModel?.provider?.toLowerCase();
2440
+ if (normalizedProvider === 'openapi' || normalizedProvider === 'openai' || normalizedProvider === 'open-ai') {
2441
+ return true;
2442
+ }
2443
+ const normalizedModelId = selectedModelId.toLowerCase();
2444
+ return normalizedModelId.startsWith('openapi/') || normalizedModelId.startsWith('openai/');
2445
+ };
2446
+
2447
+ /* eslint-disable @cspell/spellchecker */
2448
+
2047
2449
  const isOpenRouterModel = (selectedModelId, models) => {
2048
2450
  const selectedModel = models.find(model => model.id === selectedModelId);
2049
- if (selectedModel?.provider === 'openRouter') {
2451
+ const normalizedProvider = selectedModel?.provider?.toLowerCase();
2452
+ if (normalizedProvider === 'openrouter' || normalizedProvider === 'open-router') {
2050
2453
  return true;
2051
2454
  }
2052
- return selectedModelId.startsWith('openRouter/');
2455
+ return selectedModelId.toLowerCase().startsWith('openrouter/');
2053
2456
  };
2054
2457
 
2055
- const getAiResponse = async (userText, nextMessageId, selectedModelId, models, openRouterApiKey, openRouterApiBaseUrl) => {
2458
+ const getOpenRouterTooManyRequestsMessage = errorResult => {
2459
+ const details = [];
2460
+ if (errorResult.rawMessage) {
2461
+ details.push(errorResult.rawMessage);
2462
+ }
2463
+ const {
2464
+ limitInfo
2465
+ } = errorResult;
2466
+ if (limitInfo) {
2467
+ if (limitInfo.retryAfter) {
2468
+ details.push(`Retry after: ${limitInfo.retryAfter}.`);
2469
+ }
2470
+ if (limitInfo.limitReset) {
2471
+ details.push(`Limit resets: ${limitInfo.limitReset}.`);
2472
+ }
2473
+ if (limitInfo.limitRemaining === null) {
2474
+ details.push('Credits remaining: unlimited.');
2475
+ } else if (typeof limitInfo.limitRemaining === 'number') {
2476
+ details.push(`Credits remaining: ${limitInfo.limitRemaining}.`);
2477
+ }
2478
+ if (typeof limitInfo.usageDaily === 'number') {
2479
+ details.push(`Credits used today (UTC): ${limitInfo.usageDaily}.`);
2480
+ }
2481
+ if (typeof limitInfo.usage === 'number') {
2482
+ details.push(`Credits used (all time): ${limitInfo.usage}.`);
2483
+ }
2484
+ }
2485
+ if (details.length === 0) {
2486
+ return openRouterTooManyRequestsMessage;
2487
+ }
2488
+ return `${openRouterTooManyRequestsMessage} ${details.join(' ')}`;
2489
+ };
2490
+ const getOpenRouterErrorMessage = errorResult => {
2491
+ switch (errorResult.details) {
2492
+ case 'http-error':
2493
+ case 'request-failed':
2494
+ return openRouterRequestFailedMessage;
2495
+ case 'too-many-requests':
2496
+ return getOpenRouterTooManyRequestsMessage(errorResult);
2497
+ }
2498
+ };
2499
+ const getOpenApiErrorMessage = errorResult => {
2500
+ switch (errorResult.details) {
2501
+ case 'http-error':
2502
+ case 'request-failed':
2503
+ return openApiRequestFailedMessage;
2504
+ }
2505
+ };
2506
+ const normalizeLimitInfo = value => {
2507
+ if (!value || typeof value !== 'object') {
2508
+ return undefined;
2509
+ }
2510
+ const limitRemaining = Reflect.get(value, 'limitRemaining');
2511
+ const limitReset = Reflect.get(value, 'limitReset');
2512
+ const retryAfter = Reflect.get(value, 'retryAfter');
2513
+ const usage = Reflect.get(value, 'usage');
2514
+ const usageDaily = Reflect.get(value, 'usageDaily');
2515
+ const normalized = {
2516
+ limitRemaining: typeof limitRemaining === 'number' || limitRemaining === null ? limitRemaining : undefined,
2517
+ limitReset: typeof limitReset === 'string' || limitReset === null ? limitReset : undefined,
2518
+ retryAfter: typeof retryAfter === 'string' || retryAfter === null ? retryAfter : undefined,
2519
+ usage: typeof usage === 'number' ? usage : undefined,
2520
+ usageDaily: typeof usageDaily === 'number' ? usageDaily : undefined
2521
+ };
2522
+ const hasDetails = normalized.limitRemaining !== undefined || normalized.limitReset !== undefined || normalized.retryAfter !== undefined || normalized.usage !== undefined || normalized.usageDaily !== undefined;
2523
+ return hasDetails ? normalized : undefined;
2524
+ };
2525
+ const normalizeMockResult = value => {
2526
+ if (typeof value === 'string') {
2527
+ return {
2528
+ text: value,
2529
+ type: 'success'
2530
+ };
2531
+ }
2532
+ if (!value || typeof value !== 'object') {
2533
+ return {
2534
+ details: 'request-failed',
2535
+ type: 'error'
2536
+ };
2537
+ }
2538
+ const type = Reflect.get(value, 'type');
2539
+ if (type === 'success') {
2540
+ const text = Reflect.get(value, 'text');
2541
+ if (typeof text === 'string') {
2542
+ return {
2543
+ text,
2544
+ type: 'success'
2545
+ };
2546
+ }
2547
+ return {
2548
+ details: 'request-failed',
2549
+ type: 'error'
2550
+ };
2551
+ }
2552
+ if (type === 'error') {
2553
+ const details = Reflect.get(value, 'details');
2554
+ if (details === 'request-failed' || details === 'too-many-requests' || details === 'http-error') {
2555
+ const rawMessage = Reflect.get(value, 'rawMessage');
2556
+ const statusCode = Reflect.get(value, 'statusCode');
2557
+ return {
2558
+ details,
2559
+ limitInfo: normalizeLimitInfo(Reflect.get(value, 'limitInfo')),
2560
+ rawMessage: typeof rawMessage === 'string' ? rawMessage : undefined,
2561
+ statusCode: typeof statusCode === 'number' ? statusCode : undefined,
2562
+ type: 'error'
2563
+ };
2564
+ }
2565
+ }
2566
+ const text = Reflect.get(value, 'text');
2567
+ if (typeof text === 'string') {
2568
+ return {
2569
+ text,
2570
+ type: 'success'
2571
+ };
2572
+ }
2573
+ return {
2574
+ details: 'request-failed',
2575
+ type: 'error'
2576
+ };
2577
+ };
2578
+ const getMockOpenRouterAssistantText = async (messages, modelId, openRouterApiBaseUrl, openRouterApiKey, mockApiCommandId, assetDir, platform) => {
2579
+ if (!mockApiCommandId) {
2580
+ return {
2581
+ details: 'request-failed',
2582
+ type: 'error'
2583
+ };
2584
+ }
2585
+ try {
2586
+ const result = await executeProvider({
2587
+ assetDir,
2588
+ event: `onCommand:${mockApiCommandId}`,
2589
+ method: CommandExecute,
2590
+ noProviderFoundMessage: 'No mock api command found',
2591
+ params: [mockApiCommandId, {
2592
+ messages,
2593
+ modelId,
2594
+ openRouterApiBaseUrl,
2595
+ openRouterApiKey
2596
+ }],
2597
+ platform
2598
+ });
2599
+ return normalizeMockResult(result);
2600
+ } catch {
2601
+ return {
2602
+ details: 'request-failed',
2603
+ type: 'error'
2604
+ };
2605
+ }
2606
+ };
2607
+ const getAiResponse = async (userText, messages, nextMessageId, selectedModelId, models, openApiApiKey, openApiApiBaseUrl, openRouterApiKey, openRouterApiBaseUrl, useMockApi, mockApiCommandId, assetDir, platform) => {
2056
2608
  let text = '';
2057
- const shouldUseOpenRouter = isOpenRouterModel(selectedModelId, models) && openRouterApiKey;
2058
- if (shouldUseOpenRouter) {
2059
- try {
2060
- text = await getOpenRouterAssistantText(userText, getOpenRouterModelId(selectedModelId), openRouterApiKey, openRouterApiBaseUrl);
2061
- } catch {
2062
- text = '';
2609
+ const usesOpenApiModel = isOpenApiModel(selectedModelId, models);
2610
+ const usesOpenRouterModel = isOpenRouterModel(selectedModelId, models);
2611
+ if (usesOpenApiModel) {
2612
+ if (openApiApiKey) {
2613
+ const result = await getOpenApiAssistantText(messages, getOpenApiModelId(selectedModelId), openApiApiKey, openApiApiBaseUrl);
2614
+ if (result.type === 'success') {
2615
+ const {
2616
+ text: assistantText
2617
+ } = result;
2618
+ text = assistantText;
2619
+ } else {
2620
+ text = getOpenApiErrorMessage(result);
2621
+ }
2622
+ } else {
2623
+ text = openApiApiKeyRequiredMessage;
2624
+ }
2625
+ } else if (usesOpenRouterModel) {
2626
+ const modelId = getOpenRouterModelId(selectedModelId);
2627
+ if (useMockApi) {
2628
+ const result = await getMockOpenRouterAssistantText(messages, modelId, openRouterApiBaseUrl, openRouterApiKey, mockApiCommandId, assetDir, platform);
2629
+ if (result.type === 'success') {
2630
+ const {
2631
+ text: assistantText
2632
+ } = result;
2633
+ text = assistantText;
2634
+ } else {
2635
+ text = getOpenRouterErrorMessage(result);
2636
+ }
2637
+ } else if (openRouterApiKey) {
2638
+ const result = await getOpenRouterAssistantText(messages, modelId, openRouterApiKey, openRouterApiBaseUrl);
2639
+ if (result.type === 'success') {
2640
+ const {
2641
+ text: assistantText
2642
+ } = result;
2643
+ text = assistantText;
2644
+ } else {
2645
+ text = getOpenRouterErrorMessage(result);
2646
+ }
2647
+ } else {
2648
+ text = openRouterApiKeyRequiredMessage;
2063
2649
  }
2064
2650
  }
2065
- if (!text) {
2651
+ if (!text && !usesOpenApiModel && !usesOpenRouterModel) {
2066
2652
  text = await getMockAiResponse(userText);
2067
2653
  }
2068
2654
  const assistantTime = new Date().toLocaleTimeString([], {
@@ -2077,16 +2663,149 @@ const getAiResponse = async (userText, nextMessageId, selectedModelId, models, o
2077
2663
  };
2078
2664
  };
2079
2665
 
2666
+ const get = async key => {
2667
+ return getPreference(key);
2668
+ };
2669
+ const update = async settings => {
2670
+ await invoke('Preferences.update', settings);
2671
+ };
2672
+
2673
+ const setOpenApiApiKey = async (state, openApiApiKey, persist = true) => {
2674
+ if (persist) {
2675
+ await update({
2676
+ 'secrets.openApiKey': openApiApiKey
2677
+ });
2678
+ }
2679
+ return {
2680
+ ...state,
2681
+ openApiApiKey,
2682
+ openApiApiKeyInput: openApiApiKey
2683
+ };
2684
+ };
2685
+
2686
+ const handleClickSaveOpenApiApiKey = async state => {
2687
+ const {
2688
+ openApiApiKeyInput
2689
+ } = state;
2690
+ const openApiApiKey = openApiApiKeyInput.trim();
2691
+ if (!openApiApiKey) {
2692
+ return state;
2693
+ }
2694
+ const updatedState = await setOpenApiApiKey(state, openApiApiKey);
2695
+ const session = updatedState.sessions.find(item => item.id === updatedState.selectedSessionId);
2696
+ if (!session) {
2697
+ return updatedState;
2698
+ }
2699
+ const lastMessage = session.messages.at(-1);
2700
+ const shouldRetryOpenApi = lastMessage?.role === 'assistant' && lastMessage.text === openApiApiKeyRequiredMessage;
2701
+ if (!shouldRetryOpenApi) {
2702
+ return updatedState;
2703
+ }
2704
+ const previousUserMessage = session.messages.toReversed().find(item => item.role === 'user');
2705
+ if (!previousUserMessage) {
2706
+ return updatedState;
2707
+ }
2708
+ const retryMessages = session.messages.slice(0, -1);
2709
+ const assistantMessage = await getAiResponse(previousUserMessage.text, retryMessages, updatedState.nextMessageId, updatedState.selectedModelId, updatedState.models, updatedState.openApiApiKey, updatedState.openApiApiBaseUrl, updatedState.openRouterApiKey, updatedState.openRouterApiBaseUrl, updatedState.useMockApi, updatedState.mockApiCommandId, updatedState.assetDir, updatedState.platform);
2710
+ const updatedSession = {
2711
+ ...session,
2712
+ messages: [...session.messages.slice(0, -1), assistantMessage]
2713
+ };
2714
+ await saveChatSession(updatedSession);
2715
+ const updatedSessions = updatedState.sessions.map(item => {
2716
+ if (item.id !== updatedState.selectedSessionId) {
2717
+ return item;
2718
+ }
2719
+ return updatedSession;
2720
+ });
2721
+ return {
2722
+ ...updatedState,
2723
+ nextMessageId: updatedState.nextMessageId + 1,
2724
+ sessions: updatedSessions
2725
+ };
2726
+ };
2727
+
2728
+ const setOpenRouterApiKey = async (state, openRouterApiKey, persist = true) => {
2729
+ if (persist) {
2730
+ await update({
2731
+ 'secrets.openRouterApiKey': openRouterApiKey
2732
+ });
2733
+ }
2734
+ return {
2735
+ ...state,
2736
+ openRouterApiKey,
2737
+ openRouterApiKeyInput: openRouterApiKey
2738
+ };
2739
+ };
2740
+
2741
+ const handleClickSaveOpenRouterApiKey = async state => {
2742
+ const {
2743
+ openRouterApiKeyInput
2744
+ } = state;
2745
+ const openRouterApiKey = openRouterApiKeyInput.trim();
2746
+ if (!openRouterApiKey) {
2747
+ return state;
2748
+ }
2749
+ const updatedState = await setOpenRouterApiKey(state, openRouterApiKey);
2750
+ const session = updatedState.sessions.find(item => item.id === updatedState.selectedSessionId);
2751
+ if (!session) {
2752
+ return updatedState;
2753
+ }
2754
+ const lastMessage = session.messages.at(-1);
2755
+ const shouldRetryOpenRouter = lastMessage?.role === 'assistant' && lastMessage.text === openRouterApiKeyRequiredMessage;
2756
+ if (!shouldRetryOpenRouter) {
2757
+ return updatedState;
2758
+ }
2759
+ const previousUserMessage = session.messages.toReversed().find(item => item.role === 'user');
2760
+ if (!previousUserMessage) {
2761
+ return updatedState;
2762
+ }
2763
+ const retryMessages = session.messages.slice(0, -1);
2764
+
2765
+ // @ts-ignore
2766
+ const assistantMessage = await getAiResponse(previousUserMessage.text, retryMessages, updatedState.nextMessageId, updatedState.selectedModelId, updatedState.models, updatedState.openApiApiKey, updatedState.openApiApiBaseUrl, openRouterApiKey, updatedState.openRouterApiBaseUrl, updatedState.useMockApi, updatedState.mockApiCommandId, updatedState.assetDir, updatedState.platform);
2767
+ const updatedSession = {
2768
+ ...session,
2769
+ messages: [...session.messages.slice(0, -1), assistantMessage]
2770
+ };
2771
+ await saveChatSession(updatedSession);
2772
+ const updatedSessions = updatedState.sessions.map(item => {
2773
+ if (item.id !== updatedState.selectedSessionId) {
2774
+ return item;
2775
+ }
2776
+ return updatedSession;
2777
+ });
2778
+ return {
2779
+ ...updatedState,
2780
+ nextMessageId: updatedState.nextMessageId + 1,
2781
+ sessions: updatedSessions
2782
+ };
2783
+ };
2784
+
2785
+ const focusInput = state => {
2786
+ return {
2787
+ ...state,
2788
+ focus: 'composer',
2789
+ focused: true
2790
+ };
2791
+ };
2792
+
2080
2793
  const handleSubmit = async state => {
2081
2794
  const {
2795
+ assetDir,
2082
2796
  composerValue,
2797
+ mockApiCommandId,
2083
2798
  models,
2084
2799
  nextMessageId,
2800
+ openApiApiBaseUrl,
2801
+ openApiApiKey,
2085
2802
  openRouterApiBaseUrl,
2086
2803
  openRouterApiKey,
2804
+ platform,
2087
2805
  selectedModelId,
2088
2806
  selectedSessionId,
2089
2807
  sessions,
2808
+ useMockApi,
2090
2809
  viewMode
2091
2810
  } = state;
2092
2811
  const userText = composerValue.trim();
@@ -2160,7 +2879,9 @@ const handleSubmit = async state => {
2160
2879
  set(state.uid, state, optimisticState);
2161
2880
  // @ts-ignore
2162
2881
  await invoke('Chat.rerender');
2163
- const assistantMessage = await getAiResponse(userText, optimisticState.nextMessageId, selectedModelId, models, openRouterApiKey, openRouterApiBaseUrl);
2882
+ const selectedOptimisticSession = optimisticState.sessions.find(session => session.id === optimisticState.selectedSessionId);
2883
+ const messages = selectedOptimisticSession?.messages ?? [];
2884
+ const assistantMessage = await getAiResponse(userText, messages, optimisticState.nextMessageId, selectedModelId, models, openApiApiKey, openApiApiBaseUrl, openRouterApiKey, openRouterApiBaseUrl, useMockApi, mockApiCommandId, assetDir, platform);
2164
2885
  const updatedSessions = optimisticState.sessions.map(session => {
2165
2886
  if (session.id !== optimisticState.selectedSessionId) {
2166
2887
  return session;
@@ -2195,6 +2916,41 @@ const handleClickSend = async state => {
2195
2916
  return handleSubmit(submitState);
2196
2917
  };
2197
2918
 
2919
+ const Composer = 'composer';
2920
+ const Send = 'send';
2921
+ const Back = 'back';
2922
+ const Model = 'model';
2923
+ const CreateSession = 'create-session';
2924
+ const Settings = 'settings';
2925
+ const CloseChat = 'close-chat';
2926
+ const SessionDelete = 'SessionDelete';
2927
+ const SessionPrefix = 'session:';
2928
+ const RenamePrefix = 'session-rename:';
2929
+ const getSessionInputName = sessionId => {
2930
+ return `${SessionPrefix}${sessionId}`;
2931
+ };
2932
+ const isSessionInputName = name => {
2933
+ return name.startsWith(SessionPrefix);
2934
+ };
2935
+ const getSessionIdFromInputName = name => {
2936
+ return name.slice(SessionPrefix.length);
2937
+ };
2938
+ const isRenameInputName = name => {
2939
+ return name.startsWith(RenamePrefix);
2940
+ };
2941
+ const getRenameIdFromInputName = name => {
2942
+ return name.slice(RenamePrefix.length);
2943
+ };
2944
+
2945
+ const OpenApiApiKeyInput = 'open-api-api-key';
2946
+ const SaveOpenApiApiKey = 'save-openapi-api-key';
2947
+ const OpenOpenApiApiKeySettings = 'open-openapi-api-key-settings';
2948
+
2949
+ /* eslint-disable @cspell/spellchecker */
2950
+ const OpenRouterApiKeyInput = 'open-router-api-key';
2951
+ const SaveOpenRouterApiKey = 'save-openrouter-api-key';
2952
+ const OpenOpenRouterApiKeySettings = 'open-openrouter-api-key-settings';
2953
+
2198
2954
  const selectSession = async (state, id) => {
2199
2955
  const exists = state.sessions.some(session => session.id === id);
2200
2956
  if (!exists) {
@@ -2252,32 +3008,39 @@ const handleClickList = async (state, eventX, eventY) => {
2252
3008
  return selectListIndex(state, index);
2253
3009
  };
2254
3010
 
2255
- const CREATE_SESSION = 'create-session';
2256
- const SESSION_PREFIX = 'session:';
2257
- const RENAME_PREFIX = 'session-rename:';
2258
- const SESSION_DELETE = 'SessionDelete';
2259
- const SEND = 'send';
2260
3011
  const handleClick = async (state, name, id = '') => {
2261
3012
  if (!name) {
2262
3013
  return state;
2263
3014
  }
2264
- if (name === CREATE_SESSION) {
3015
+ if (name === CreateSession) {
2265
3016
  return createSession(state);
2266
3017
  }
2267
- if (name.startsWith(SESSION_PREFIX)) {
2268
- const id = name.slice(SESSION_PREFIX.length);
2269
- return selectSession(state, id);
3018
+ if (isSessionInputName(name)) {
3019
+ const sessionId = getSessionIdFromInputName(name);
3020
+ return selectSession(state, sessionId);
2270
3021
  }
2271
- if (name.startsWith(RENAME_PREFIX)) {
2272
- const id = name.slice(RENAME_PREFIX.length);
2273
- return startRename(state, id);
3022
+ if (isRenameInputName(name)) {
3023
+ const sessionId = getRenameIdFromInputName(name);
3024
+ return startRename(state, sessionId);
2274
3025
  }
2275
- if (name === SESSION_DELETE) {
3026
+ if (name === SessionDelete) {
2276
3027
  return deleteSession(state, id);
2277
3028
  }
2278
- if (name === SEND) {
3029
+ if (name === Send) {
2279
3030
  return handleClickSend(state);
2280
3031
  }
3032
+ if (name === SaveOpenRouterApiKey) {
3033
+ return handleClickSaveOpenRouterApiKey(state);
3034
+ }
3035
+ if (name === SaveOpenApiApiKey) {
3036
+ return handleClickSaveOpenApiApiKey(state);
3037
+ }
3038
+ if (name === OpenOpenRouterApiKeySettings) {
3039
+ return handleClickOpenRouterApiKeySettings(state);
3040
+ }
3041
+ if (name === OpenOpenApiApiKeySettings) {
3042
+ return handleClickOpenApiApiKeySettings(state);
3043
+ }
2281
3044
  return state;
2282
3045
  };
2283
3046
 
@@ -2307,7 +3070,22 @@ const handleClickSettings = async () => {
2307
3070
  await invoke('Main.openUri', 'app://settings.json');
2308
3071
  };
2309
3072
 
2310
- const handleInput = async (state, value, inputSource = 'user') => {
3073
+ const handleInput = async (state, name, value, inputSource = 'user') => {
3074
+ if (name === OpenApiApiKeyInput) {
3075
+ return {
3076
+ ...state,
3077
+ openApiApiKeyInput: value
3078
+ };
3079
+ }
3080
+ if (name === OpenRouterApiKeyInput) {
3081
+ return {
3082
+ ...state,
3083
+ openRouterApiKeyInput: value
3084
+ };
3085
+ }
3086
+ if (name !== Composer) {
3087
+ return state;
3088
+ }
2311
3089
  return {
2312
3090
  ...state,
2313
3091
  composerValue: value,
@@ -2316,24 +3094,24 @@ const handleInput = async (state, value, inputSource = 'user') => {
2316
3094
  };
2317
3095
 
2318
3096
  const handleInputFocus = async (state, name) => {
2319
- if (name === 'composer') {
3097
+ if (name === Composer) {
2320
3098
  return focusInput(state);
2321
3099
  }
2322
- if (name === 'send') {
3100
+ if (name === Send) {
2323
3101
  return {
2324
3102
  ...state,
2325
3103
  focus: 'send-button',
2326
3104
  focused: true
2327
3105
  };
2328
3106
  }
2329
- if (name.startsWith('session:') || name === 'SessionDelete') {
3107
+ if (isSessionInputName(name) || name === SessionDelete) {
2330
3108
  return {
2331
3109
  ...state,
2332
3110
  focus: 'list',
2333
3111
  focused: true
2334
3112
  };
2335
3113
  }
2336
- if (name === 'create-session' || name === 'settings' || name === 'close-chat' || name === 'back') {
3114
+ if (name === CreateSession || name === Settings || name === CloseChat || name === Back) {
2337
3115
  return {
2338
3116
  ...state,
2339
3117
  focus: 'header',
@@ -2412,7 +3190,7 @@ const handleModelChange = async (state, value) => {
2412
3190
  };
2413
3191
 
2414
3192
  const handleNewline = async state => {
2415
- return handleInput(state, `${state.composerValue}\n`);
3193
+ return handleInput(state, Composer, `${state.composerValue}\n`);
2416
3194
  };
2417
3195
 
2418
3196
  const id = 7201;
@@ -2441,36 +3219,6 @@ const isObject = value => {
2441
3219
  return typeof value === 'object' && value !== null;
2442
3220
  };
2443
3221
 
2444
- const getSavedBounds = savedState => {
2445
- if (!isObject(savedState)) {
2446
- return undefined;
2447
- }
2448
- const {
2449
- height,
2450
- width,
2451
- x,
2452
- y
2453
- } = savedState;
2454
- if (typeof x !== 'number') {
2455
- return undefined;
2456
- }
2457
- if (typeof y !== 'number') {
2458
- return undefined;
2459
- }
2460
- if (typeof width !== 'number') {
2461
- return undefined;
2462
- }
2463
- if (typeof height !== 'number') {
2464
- return undefined;
2465
- }
2466
- return {
2467
- height,
2468
- width,
2469
- x,
2470
- y
2471
- };
2472
- };
2473
-
2474
3222
  const getSavedSelectedModelId = savedState => {
2475
3223
  if (!isObject(savedState)) {
2476
3224
  return undefined;
@@ -2523,10 +3271,6 @@ const getSavedViewMode = savedState => {
2523
3271
  return viewMode;
2524
3272
  };
2525
3273
 
2526
- const get = async key => {
2527
- return getPreference(key);
2528
- };
2529
-
2530
3274
  const toSummarySession = session => {
2531
3275
  return {
2532
3276
  id: session.id,
@@ -2550,9 +3294,25 @@ const loadSelectedSessionMessages = async (sessions, selectedSessionId) => {
2550
3294
  });
2551
3295
  };
2552
3296
  const loadContent = async (state, savedState) => {
2553
- const savedBounds = getSavedBounds(savedState);
2554
3297
  const savedSelectedModelId = getSavedSelectedModelId(savedState);
2555
3298
  const savedViewMode = getSavedViewMode(savedState);
3299
+ let openApiApiKey = '';
3300
+ try {
3301
+ const savedOpenApiKey = await get('secrets.openApiKey');
3302
+ if (typeof savedOpenApiKey === 'string' && savedOpenApiKey) {
3303
+ openApiApiKey = savedOpenApiKey;
3304
+ } else {
3305
+ const legacySavedOpenApiApiKey = await get('secrets.openApiApiKey');
3306
+ if (typeof legacySavedOpenApiApiKey === 'string' && legacySavedOpenApiApiKey) {
3307
+ openApiApiKey = legacySavedOpenApiApiKey;
3308
+ } else {
3309
+ const legacySavedOpenAiApiKey = await get('secrets.openAiApiKey');
3310
+ openApiApiKey = typeof legacySavedOpenAiApiKey === 'string' ? legacySavedOpenAiApiKey : '';
3311
+ }
3312
+ }
3313
+ } catch {
3314
+ openApiApiKey = '';
3315
+ }
2556
3316
  let openRouterApiKey = '';
2557
3317
  try {
2558
3318
  const savedOpenRouterApiKey = await get('secrets.openRouterApiKey');
@@ -2584,9 +3344,11 @@ const loadContent = async (state, savedState) => {
2584
3344
  const viewMode = sessions.length === 0 || !selectedSessionId ? 'list' : preferredViewMode === 'detail' ? 'detail' : 'list';
2585
3345
  return {
2586
3346
  ...state,
2587
- ...savedBounds,
2588
3347
  initial: false,
3348
+ openApiApiKey,
3349
+ openApiApiKeyInput: openApiApiKey,
2589
3350
  openRouterApiKey,
3351
+ openRouterApiKeyInput: openRouterApiKey,
2590
3352
  selectedModelId,
2591
3353
  selectedSessionId,
2592
3354
  sessions,
@@ -2595,11 +3357,14 @@ const loadContent = async (state, savedState) => {
2595
3357
  };
2596
3358
 
2597
3359
  const openMockSession = async (state, mockSessionId, mockChatMessages) => {
3360
+ const {
3361
+ sessions: currentSessions
3362
+ } = state;
2598
3363
  if (!mockSessionId) {
2599
3364
  return state;
2600
3365
  }
2601
- const existingSession = state.sessions.find(session => session.id === mockSessionId);
2602
- const sessions = existingSession ? state.sessions.map(session => {
3366
+ const existingSession = currentSessions.find(session => session.id === mockSessionId);
3367
+ const sessions = existingSession ? currentSessions.map(session => {
2603
3368
  if (session.id !== mockSessionId) {
2604
3369
  return session;
2605
3370
  }
@@ -2607,7 +3372,7 @@ const openMockSession = async (state, mockSessionId, mockChatMessages) => {
2607
3372
  ...session,
2608
3373
  messages: mockChatMessages
2609
3374
  };
2610
- }) : [...state.sessions, {
3375
+ }) : [...currentSessions, {
2611
3376
  id: mockSessionId,
2612
3377
  messages: mockChatMessages,
2613
3378
  title: mockSessionId
@@ -2657,6 +3422,7 @@ const renderFocusContext = (oldState, newState) => {
2657
3422
  return [SetFocusContext, newState.uid, FocusChatInput];
2658
3423
  };
2659
3424
 
3425
+ const Actions = 'Actions';
2660
3426
  const ChatActions = 'ChatActions';
2661
3427
  const ChatName = 'ChatName';
2662
3428
  const ChatSendArea = 'ChatSendArea';
@@ -2665,7 +3431,11 @@ const ChatSendAreaBottom = 'ChatSendAreaBottom';
2665
3431
  const ChatSendAreaContent = 'ChatSendAreaContent';
2666
3432
  const Chat = 'Chat';
2667
3433
  const ChatHeader = 'ChatHeader';
3434
+ const Button = 'Button';
3435
+ const ButtonPrimary = 'ButtonPrimary';
3436
+ const ButtonSecondary = 'ButtonSecondary';
2668
3437
  const IconButton = 'IconButton';
3438
+ const InputBox = 'InputBox';
2669
3439
  const Label = 'Label';
2670
3440
  const LabelDetail = 'LabelDetail';
2671
3441
  const ChatList = 'ChatList';
@@ -2675,6 +3445,8 @@ const ChatListItemLabel = 'ChatListItemLabel';
2675
3445
  const Markdown = 'Markdown';
2676
3446
  const Message = 'Message';
2677
3447
  const ChatMessageContent = 'ChatMessageContent';
3448
+ const ChatOrderedList = 'ChatOrderedList';
3449
+ const ChatOrderedListItem = 'ChatOrderedListItem';
2678
3450
  const MessageUser = 'MessageUser';
2679
3451
  const MessageAssistant = 'MessageAssistant';
2680
3452
  const MultilineInputBox = 'MultilineInputBox';
@@ -2704,8 +3476,12 @@ const getModelLabel = model => {
2704
3476
  if (model.provider === 'openRouter') {
2705
3477
  return `${model.name} (OpenRouter)`;
2706
3478
  }
3479
+ if (model.provider === 'openApi' || model.provider === 'openAI' || model.provider === 'openai') {
3480
+ return `${model.name} (OpenAI)`;
3481
+ }
2707
3482
  return model.name;
2708
3483
  };
3484
+
2709
3485
  const getModelOptionDOm = (model, selectedModelId) => {
2710
3486
  return [{
2711
3487
  childCount: 1,
@@ -2726,11 +3502,11 @@ const getSendButtonDom = isSendDisabled => {
2726
3502
  childCount: 1,
2727
3503
  className: sendButtonClassName,
2728
3504
  disabled: isSendDisabled,
2729
- name: 'send',
3505
+ name: Send,
2730
3506
  onClick: HandleSubmit,
2731
- role: Button$1,
2732
- title: sendMessage,
2733
- type: Button
3507
+ role: Button$2,
3508
+ title: sendMessage(),
3509
+ type: Button$1
2734
3510
  }, {
2735
3511
  childCount: 0,
2736
3512
  className: 'MaskIcon MaskIconSend',
@@ -2787,10 +3563,10 @@ const getChatSendAreaDom = (composerValue, models, selectedModelId, usageOvervie
2787
3563
  }, {
2788
3564
  childCount: 0,
2789
3565
  className: MultilineInputBox,
2790
- name: 'composer',
3566
+ name: Composer,
2791
3567
  onFocus: HandleFocus,
2792
3568
  onInput: HandleInput,
2793
- placeholder: composePlaceholder,
3569
+ placeholder: composePlaceholder(),
2794
3570
  rows: 4,
2795
3571
  type: TextArea,
2796
3572
  value: composerValue
@@ -2801,7 +3577,7 @@ const getChatSendAreaDom = (composerValue, models, selectedModelId, usageOvervie
2801
3577
  }, {
2802
3578
  childCount: models.length,
2803
3579
  className: Select,
2804
- name: 'model',
3580
+ name: Model,
2805
3581
  onInput: HandleModelChange,
2806
3582
  type: Select$1,
2807
3583
  value: selectedModelId
@@ -2812,11 +3588,11 @@ const getBackButtonVirtualDom = () => {
2812
3588
  return [{
2813
3589
  childCount: 1,
2814
3590
  className: IconButton,
2815
- name: 'back',
3591
+ name: Back,
2816
3592
  onClick: HandleClickBack,
2817
- role: Button$1,
2818
- title: backToChats,
2819
- type: Button
3593
+ role: Button$2,
3594
+ title: backToChats(),
3595
+ type: Button$1
2820
3596
  }, {
2821
3597
  childCount: 0,
2822
3598
  className: 'MaskIcon MaskIconArrowLeft',
@@ -2830,9 +3606,9 @@ const getHeaderActionVirtualDom = item => {
2830
3606
  className: IconButton,
2831
3607
  name: item.name,
2832
3608
  onClick: item.onClick,
2833
- role: Button$1,
3609
+ role: Button$2,
2834
3610
  title: item.title,
2835
- type: Button
3611
+ type: Button$1
2836
3612
  }, {
2837
3613
  childCount: 0,
2838
3614
  className: item.icon,
@@ -2843,19 +3619,19 @@ const getHeaderActionVirtualDom = item => {
2843
3619
  const getChatHeaderActionsDom = () => {
2844
3620
  const items = [{
2845
3621
  icon: 'MaskIcon MaskIconAdd',
2846
- name: 'create-session',
3622
+ name: CreateSession,
2847
3623
  onClick: HandleClickNew,
2848
- title: newChat
3624
+ title: newChat()
2849
3625
  }, {
2850
3626
  icon: 'MaskIcon MaskIconSettingsGear',
2851
- name: 'settings',
3627
+ name: Settings,
2852
3628
  onClick: HandleClickSettings,
2853
- title: settings
3629
+ title: settings()
2854
3630
  }, {
2855
3631
  icon: 'MaskIcon MaskIconClose',
2856
- name: 'close-chat',
3632
+ name: CloseChat,
2857
3633
  onClick: HandleClickClose,
2858
- title: closeChat
3634
+ title: closeChat()
2859
3635
  }];
2860
3636
  return [{
2861
3637
  childCount: items.length,
@@ -2880,47 +3656,142 @@ const getChatHeaderDomDetailMode = selectedSessionTitle => {
2880
3656
  }, text(selectedSessionTitle), ...getChatHeaderActionsDom()];
2881
3657
  };
2882
3658
 
2883
- const getChatMessageDom = message => {
3659
+ const getMissingApiKeyDom = ({
3660
+ getApiKeyText,
3661
+ inputName,
3662
+ inputValue,
3663
+ openSettingsButtonName,
3664
+ placeholder,
3665
+ saveButtonName
3666
+ }) => {
3667
+ return [{
3668
+ childCount: 2,
3669
+ type: Div
3670
+ }, {
3671
+ childCount: 0,
3672
+ className: InputBox,
3673
+ name: inputName,
3674
+ onInput: HandleInput,
3675
+ placeholder,
3676
+ type: Input,
3677
+ value: inputValue
3678
+ }, {
3679
+ childCount: 2,
3680
+ className: Actions,
3681
+ type: Div
3682
+ }, {
3683
+ childCount: 1,
3684
+ className: mergeClassNames(Button, ButtonPrimary),
3685
+ name: saveButtonName,
3686
+ onClick: HandleClick,
3687
+ type: Button$1
3688
+ }, text(save()), {
3689
+ childCount: 1,
3690
+ className: mergeClassNames(Button, ButtonSecondary),
3691
+ name: openSettingsButtonName,
3692
+ onClick: HandleClick,
3693
+ type: Button$1
3694
+ }, text(getApiKeyText)];
3695
+ };
3696
+
3697
+ const getMissingOpenApiApiKeyDom = openApiApiKeyInput => {
3698
+ return getMissingApiKeyDom({
3699
+ getApiKeyText: getOpenApiApiKey(),
3700
+ inputName: OpenApiApiKeyInput,
3701
+ inputValue: openApiApiKeyInput,
3702
+ openSettingsButtonName: OpenOpenApiApiKeySettings,
3703
+ placeholder: openApiApiKeyPlaceholder(),
3704
+ saveButtonName: SaveOpenApiApiKey
3705
+ });
3706
+ };
3707
+
3708
+ const getMissingOpenRouterApiKeyDom = openRouterApiKeyInput => {
3709
+ return getMissingApiKeyDom({
3710
+ getApiKeyText: getOpenRouterApiKey(),
3711
+ inputName: OpenRouterApiKeyInput,
3712
+ inputValue: openRouterApiKeyInput,
3713
+ openSettingsButtonName: OpenOpenRouterApiKeySettings,
3714
+ placeholder: openRouterApiKeyPlaceholder(),
3715
+ saveButtonName: SaveOpenRouterApiKey
3716
+ });
3717
+ };
3718
+
3719
+ const getOpenRouterRequestFailedDom = () => {
3720
+ return [{
3721
+ childCount: openRouterRequestFailureReasons.length,
3722
+ className: ChatOrderedList,
3723
+ type: Ol
3724
+ }, ...openRouterRequestFailureReasons.flatMap(reason => {
3725
+ return [{
3726
+ childCount: 1,
3727
+ className: ChatOrderedListItem,
3728
+ type: Li
3729
+ }, text(reason)];
3730
+ })];
3731
+ };
3732
+ const getOpenRouterTooManyRequestsDom = () => {
3733
+ return [{
3734
+ childCount: openRouterTooManyRequestsReasons.length,
3735
+ className: ChatOrderedList,
3736
+ type: Ol
3737
+ }, ...openRouterTooManyRequestsReasons.flatMap(reason => {
3738
+ return [{
3739
+ childCount: 1,
3740
+ className: ChatOrderedListItem,
3741
+ type: Li
3742
+ }, text(reason)];
3743
+ })];
3744
+ };
3745
+ const getChatMessageDom = (message, openRouterApiKeyInput, openApiApiKeyInput = '') => {
2884
3746
  const roleClassName = message.role === 'user' ? MessageUser : MessageAssistant;
3747
+ const isOpenApiApiKeyMissingMessage = message.role === 'assistant' && message.text === openApiApiKeyRequiredMessage;
3748
+ const isOpenRouterApiKeyMissingMessage = message.role === 'assistant' && message.text === openRouterApiKeyRequiredMessage;
3749
+ const isOpenRouterRequestFailedMessage = message.role === 'assistant' && message.text === openRouterRequestFailedMessage;
3750
+ const isOpenRouterTooManyRequestsMessage = message.role === 'assistant' && message.text.startsWith(openRouterTooManyRequestsMessage);
3751
+ const extraChildCount = isOpenApiApiKeyMissingMessage || isOpenRouterApiKeyMissingMessage || isOpenRouterRequestFailedMessage || isOpenRouterTooManyRequestsMessage ? 2 : 1;
2885
3752
  return [{
2886
3753
  childCount: 1,
2887
3754
  className: mergeClassNames(Message, roleClassName),
2888
3755
  type: Div
2889
3756
  }, {
2890
- childCount: 1,
3757
+ childCount: extraChildCount,
2891
3758
  className: ChatMessageContent,
2892
3759
  type: Div
2893
3760
  }, {
2894
3761
  childCount: 1,
2895
3762
  className: Markdown,
2896
3763
  type: P
2897
- }, text(message.text)];
3764
+ }, text(message.text), ...(isOpenApiApiKeyMissingMessage ? getMissingOpenApiApiKeyDom(openApiApiKeyInput) : []), ...(isOpenRouterApiKeyMissingMessage ? getMissingOpenRouterApiKeyDom(openRouterApiKeyInput) : []), ...(isOpenRouterRequestFailedMessage ? getOpenRouterRequestFailedDom() : []), ...(isOpenRouterTooManyRequestsMessage ? getOpenRouterTooManyRequestsDom() : [])];
2898
3765
  };
2899
3766
 
2900
- const getMessagesDom = messages => {
3767
+ const getEmptyMessagesDom = () => {
3768
+ return [{
3769
+ childCount: 1,
3770
+ className: ChatWelcomeMessage,
3771
+ type: Div
3772
+ }, text(startConversation())];
3773
+ };
3774
+
3775
+ const getMessagesDom = (messages, openRouterApiKeyInput, openApiApiKeyInput = '') => {
2901
3776
  if (messages.length === 0) {
2902
- return [{
2903
- childCount: 1,
2904
- className: ChatWelcomeMessage,
2905
- type: Div
2906
- }, text(startConversation)];
3777
+ return getEmptyMessagesDom();
2907
3778
  }
2908
3779
  return [{
2909
3780
  childCount: messages.length,
2910
3781
  className: 'ChatMessages',
2911
3782
  type: Div
2912
- }, ...messages.flatMap(getChatMessageDom)];
3783
+ }, ...messages.flatMap(message => getChatMessageDom(message, openRouterApiKeyInput, openApiApiKeyInput))];
2913
3784
  };
2914
3785
 
2915
- const getChatModeDetailVirtualDom = (sessions, selectedSessionId, composerValue, models, selectedModelId, usageOverviewEnabled, tokensUsed, tokensMax) => {
3786
+ const getChatModeDetailVirtualDom = (sessions, selectedSessionId, composerValue, openRouterApiKeyInput, openApiApiKeyInput, models, selectedModelId, usageOverviewEnabled, tokensUsed, tokensMax) => {
2916
3787
  const selectedSession = sessions.find(session => session.id === selectedSessionId);
2917
- const selectedSessionTitle = selectedSession?.title || chatTitle;
3788
+ const selectedSessionTitle = selectedSession?.title || chatTitle();
2918
3789
  const messages = selectedSession ? selectedSession.messages : [];
2919
3790
  return [{
2920
3791
  childCount: 3,
2921
3792
  className: mergeClassNames(Viewlet, Chat),
2922
3793
  type: Div
2923
- }, ...getChatHeaderDomDetailMode(selectedSessionTitle), ...getMessagesDom(messages), ...getChatSendAreaDom(composerValue, models, selectedModelId, usageOverviewEnabled, tokensUsed, tokensMax)];
3794
+ }, ...getChatHeaderDomDetailMode(selectedSessionTitle), ...getMessagesDom(messages, openRouterApiKeyInput, openApiApiKeyInput), ...getChatSendAreaDom(composerValue, models, selectedModelId, usageOverviewEnabled, tokensUsed, tokensMax)];
2924
3795
  };
2925
3796
 
2926
3797
  const getChatHeaderListModeDom = () => {
@@ -2932,7 +3803,7 @@ const getChatHeaderListModeDom = () => {
2932
3803
  childCount: 1,
2933
3804
  className: Label,
2934
3805
  type: Span
2935
- }, text(chats), ...getChatHeaderActionsDom()];
3806
+ }, text(chats()), ...getChatHeaderActionsDom()];
2936
3807
  };
2937
3808
 
2938
3809
  const getEmptyChatSessionsDom = () => {
@@ -2944,7 +3815,7 @@ const getEmptyChatSessionsDom = () => {
2944
3815
  childCount: 1,
2945
3816
  className: Label,
2946
3817
  type: Div
2947
- }, text(clickToOpenNewChat)];
3818
+ }, text(clickToOpenNewChat())];
2948
3819
  };
2949
3820
 
2950
3821
  const getSessionDom = session => {
@@ -2956,7 +3827,7 @@ const getSessionDom = session => {
2956
3827
  }, {
2957
3828
  childCount: 1,
2958
3829
  className: ChatListItemLabel,
2959
- name: `session:${session.id}`,
3830
+ name: getSessionInputName(session.id),
2960
3831
  onContextMenu: HandleListContextMenu,
2961
3832
  tabIndex: 0,
2962
3833
  type: Div
@@ -2968,12 +3839,12 @@ const getSessionDom = session => {
2968
3839
  childCount: 1,
2969
3840
  className: IconButton,
2970
3841
  'data-id': session.id,
2971
- name: 'SessionDelete',
3842
+ name: SessionDelete,
2972
3843
  onClick: HandleClickDelete,
2973
- role: Button$1,
3844
+ role: Button$2,
2974
3845
  tabIndex: 0,
2975
- title: deleteChatSession$1,
2976
- type: Button
3846
+ title: deleteChatSession$1(),
3847
+ type: Button$1
2977
3848
  }, text('🗑')];
2978
3849
  };
2979
3850
 
@@ -3001,13 +3872,13 @@ const getChatModeUnsupportedVirtualDom = () => {
3001
3872
  return [{
3002
3873
  childCount: 1,
3003
3874
  type: Div
3004
- }, text('Unknown view mode')];
3875
+ }, text(unknownViewMode())];
3005
3876
  };
3006
3877
 
3007
- const getChatVirtualDom = (sessions, selectedSessionId, composerValue, viewMode, models, selectedModelId, usageOverviewEnabled, tokensUsed, tokensMax) => {
3878
+ const getChatVirtualDom = (sessions, selectedSessionId, composerValue, openRouterApiKeyInput, viewMode, models, selectedModelId, usageOverviewEnabled, tokensUsed, tokensMax, openApiApiKeyInput = '') => {
3008
3879
  switch (viewMode) {
3009
3880
  case 'detail':
3010
- return getChatModeDetailVirtualDom(sessions, selectedSessionId, composerValue, models, selectedModelId, usageOverviewEnabled, tokensUsed, tokensMax);
3881
+ return getChatModeDetailVirtualDom(sessions, selectedSessionId, composerValue, openRouterApiKeyInput, openApiApiKeyInput, models, selectedModelId, usageOverviewEnabled, tokensUsed, tokensMax);
3011
3882
  case 'list':
3012
3883
  return getChatModeListVirtualDom(sessions, selectedSessionId, composerValue, models, selectedModelId, usageOverviewEnabled, tokensUsed, tokensMax);
3013
3884
  default:
@@ -3020,6 +3891,8 @@ const renderItems = (oldState, newState) => {
3020
3891
  composerValue,
3021
3892
  initial,
3022
3893
  models,
3894
+ openApiApiKeyInput,
3895
+ openRouterApiKeyInput,
3023
3896
  selectedModelId,
3024
3897
  selectedSessionId,
3025
3898
  sessions,
@@ -3032,7 +3905,7 @@ const renderItems = (oldState, newState) => {
3032
3905
  if (initial) {
3033
3906
  return [SetDom2, uid, []];
3034
3907
  }
3035
- const dom = getChatVirtualDom(sessions, selectedSessionId, composerValue, viewMode, models, selectedModelId, usageOverviewEnabled, tokensUsed, tokensMax);
3908
+ const dom = getChatVirtualDom(sessions, selectedSessionId, composerValue, openRouterApiKeyInput, viewMode, models, selectedModelId, usageOverviewEnabled, tokensUsed, tokensMax, openApiApiKeyInput);
3036
3909
  return [SetDom2, uid, dom];
3037
3910
  };
3038
3911
 
@@ -3047,7 +3920,7 @@ const renderValue = (oldState, newState) => {
3047
3920
  const {
3048
3921
  composerValue
3049
3922
  } = newState;
3050
- return [SetValueByName, newState.uid, 'composer', composerValue];
3923
+ return [SetValueByName, newState.uid, Composer, composerValue];
3051
3924
  };
3052
3925
 
3053
3926
  const getRenderer = diffType => {
@@ -3119,7 +3992,7 @@ const renderEventListeners = () => {
3119
3992
  params: ['handleClickList', ClientX, ClientY]
3120
3993
  }, {
3121
3994
  name: HandleInput,
3122
- params: ['handleInput', TargetValue]
3995
+ params: ['handleInput', TargetName, TargetValue]
3123
3996
  }, {
3124
3997
  name: HandleModelChange,
3125
3998
  params: ['handleModelChange', TargetValue]
@@ -3144,6 +4017,7 @@ const reset = async state => {
3144
4017
  return {
3145
4018
  ...state,
3146
4019
  composerValue: '',
4020
+ openRouterApiKey: '',
3147
4021
  selectedSessionId: '',
3148
4022
  sessions: [],
3149
4023
  viewMode: 'list'
@@ -3187,15 +4061,15 @@ const saveState = state => {
3187
4061
  const dummySessions = [{
3188
4062
  id: 'session-a',
3189
4063
  messages: [],
3190
- title: dummyChatA
4064
+ title: dummyChatA()
3191
4065
  }, {
3192
4066
  id: 'session-b',
3193
4067
  messages: [],
3194
- title: dummyChatB
4068
+ title: dummyChatB()
3195
4069
  }, {
3196
4070
  id: 'session-c',
3197
4071
  messages: [],
3198
- title: dummyChatC
4072
+ title: dummyChatC()
3199
4073
  }];
3200
4074
  const setChatList = state => {
3201
4075
  return {
@@ -3206,6 +4080,21 @@ const setChatList = state => {
3206
4080
  };
3207
4081
  };
3208
4082
 
4083
+ const defaultMockApiCommandId = 'ChatE2e.mockApi';
4084
+ const useMockApi = (state, value, mockApiCommandId = defaultMockApiCommandId) => {
4085
+ if (!value) {
4086
+ return {
4087
+ ...state,
4088
+ useMockApi: false
4089
+ };
4090
+ }
4091
+ return {
4092
+ ...state,
4093
+ mockApiCommandId,
4094
+ useMockApi: true
4095
+ };
4096
+ };
4097
+
3209
4098
  const commandMap = {
3210
4099
  'Chat.clearInput': wrapCommand(clearInput),
3211
4100
  'Chat.create': create,
@@ -3213,6 +4102,7 @@ const commandMap = {
3213
4102
  'Chat.enterNewLine': wrapCommand(handleNewline),
3214
4103
  'Chat.getCommandIds': getCommandIds,
3215
4104
  'Chat.getKeyBindings': getKeyBindings,
4105
+ 'Chat.getSelectedSessionId': wrapGetter(getSelectedSessionId),
3216
4106
  'Chat.handleChatListContextMenu': handleChatListContextMenu,
3217
4107
  'Chat.handleClick': wrapCommand(handleClick),
3218
4108
  'Chat.handleClickBack': wrapCommand(handleClickBack),
@@ -3237,7 +4127,9 @@ const commandMap = {
3237
4127
  'Chat.resize': wrapCommand(resize),
3238
4128
  'Chat.saveState': wrapGetter(saveState),
3239
4129
  'Chat.setChatList': wrapCommand(setChatList),
3240
- 'Chat.terminate': terminate
4130
+ 'Chat.setOpenRouterApiKey': wrapCommand(setOpenRouterApiKey),
4131
+ 'Chat.terminate': terminate,
4132
+ 'Chat.useMockApi': wrapCommand(useMockApi)
3241
4133
  };
3242
4134
 
3243
4135
  const listen = async () => {