@lvce-editor/chat-view 1.12.0 → 1.14.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.
@@ -486,7 +486,7 @@ const execute = (command, ...args) => {
486
486
 
487
487
  const Two$1 = '2.0';
488
488
  const callbacks = Object.create(null);
489
- const get$2 = id => {
489
+ const get$3 = id => {
490
490
  return callbacks[id];
491
491
  };
492
492
  const remove$1 = id => {
@@ -635,7 +635,7 @@ const warn = (...args) => {
635
635
  console.warn(...args);
636
636
  };
637
637
  const resolve = (id, response) => {
638
- const fn = get$2(id);
638
+ const fn = get$3(id);
639
639
  if (!fn) {
640
640
  console.log(response);
641
641
  warn(`callback ${id} may already be disposed`);
@@ -991,7 +991,7 @@ const rpcs = Object.create(null);
991
991
  const set$3 = (id, rpc) => {
992
992
  rpcs[id] = rpc;
993
993
  };
994
- const get$1 = id => {
994
+ const get$2 = id => {
995
995
  return rpcs[id];
996
996
  };
997
997
  const remove = id => {
@@ -1002,18 +1002,18 @@ const remove = id => {
1002
1002
  const create$2 = rpcId => {
1003
1003
  return {
1004
1004
  async dispose() {
1005
- const rpc = get$1(rpcId);
1005
+ const rpc = get$2(rpcId);
1006
1006
  await rpc.dispose();
1007
1007
  },
1008
1008
  // @ts-ignore
1009
1009
  invoke(method, ...params) {
1010
- const rpc = get$1(rpcId);
1010
+ const rpc = get$2(rpcId);
1011
1011
  // @ts-ignore
1012
1012
  return rpc.invoke(method, ...params);
1013
1013
  },
1014
1014
  // @ts-ignore
1015
1015
  invokeAndTransfer(method, ...params) {
1016
- const rpc = get$1(rpcId);
1016
+ const rpc = get$2(rpcId);
1017
1017
  // @ts-ignore
1018
1018
  return rpc.invokeAndTransfer(method, ...params);
1019
1019
  },
@@ -1048,6 +1048,12 @@ const sendMessagePortToExtensionHostWorker$1 = async (port, rpcId = 0) => {
1048
1048
  const command = 'HandleMessagePort.handleMessagePort2';
1049
1049
  await invokeAndTransfer('SendMessagePortToExtensionHostWorker.sendMessagePortToExtensionHostWorker', port, command, rpcId);
1050
1050
  };
1051
+ const getPreference = async key => {
1052
+ return await invoke('Preferences.get', key);
1053
+ };
1054
+ const openExternal = async uri => {
1055
+ await invoke('Open.openExternal', uri);
1056
+ };
1051
1057
 
1052
1058
  const toCommandId = key => {
1053
1059
  const dotIndex = key.indexOf('.');
@@ -1188,25 +1194,122 @@ const i18nString = (key, placeholders = emptyObject) => {
1188
1194
  return key.replaceAll(RE_PLACEHOLDER, replacer);
1189
1195
  };
1190
1196
 
1191
- const chatTitle = i18nString('Chat');
1192
- const chats = i18nString('Chats');
1193
- const newChat = i18nString('New Chat');
1194
- const backToChats = i18nString('Back to chats');
1195
- const settings = i18nString('Settings');
1196
- const closeChat = i18nString('Close Chat');
1197
- const clickToOpenNewChat = i18nString('Click the + button to open a new chat.');
1198
- const startConversation = i18nString('Start a conversation by typing below.');
1199
- i18nString('You');
1200
- i18nString('Assistant');
1201
- const composePlaceholder = i18nString('Type your message. Enter to send, Shift+Enter for newline.');
1202
- const sendMessage = i18nString('Send message');
1203
- i18nString('Send');
1204
- const deleteChatSession$1 = i18nString('Delete chat session');
1205
- const defaultSessionTitle = i18nString('Chat 1');
1206
- const dummyChatA = i18nString('Dummy Chat A');
1207
- const dummyChatB = i18nString('Dummy Chat B');
1208
- const dummyChatC = i18nString('Dummy Chat C');
1197
+ const chatTitle = () => {
1198
+ return i18nString('Chat');
1199
+ };
1200
+ const chats = () => {
1201
+ return i18nString('Chats');
1202
+ };
1203
+ const newChat = () => {
1204
+ return i18nString('New Chat');
1205
+ };
1206
+ const backToChats = () => {
1207
+ return i18nString('Back to chats');
1208
+ };
1209
+ const settings = () => {
1210
+ return i18nString('Settings');
1211
+ };
1212
+ const closeChat = () => {
1213
+ return i18nString('Close Chat');
1214
+ };
1215
+ const clickToOpenNewChat = () => {
1216
+ return i18nString('Click the + button to open a new chat.');
1217
+ };
1218
+ const startConversation = () => {
1219
+ return i18nString('Start a conversation by typing below.');
1220
+ };
1221
+ const composePlaceholder = () => {
1222
+ return i18nString('Type your message. Enter to send, Shift+Enter for newline.');
1223
+ };
1224
+ const openRouterApiKeyPlaceholder = () => {
1225
+ return i18nString('Enter OpenRouter API key');
1226
+ };
1227
+ const sendMessage = () => {
1228
+ return i18nString('Send message');
1229
+ };
1230
+ const save = () => {
1231
+ return i18nString('Save');
1232
+ };
1233
+ const getOpenRouterApiKey = () => {
1234
+ return i18nString('Get API Key');
1235
+ };
1236
+ const deleteChatSession$1 = () => {
1237
+ return i18nString('Delete chat session');
1238
+ };
1239
+ const defaultSessionTitle = () => {
1240
+ return i18nString('Chat 1');
1241
+ };
1242
+ const dummyChatA = () => {
1243
+ return i18nString('Dummy Chat A');
1244
+ };
1245
+ const dummyChatB = () => {
1246
+ return i18nString('Dummy Chat B');
1247
+ };
1248
+ const dummyChatC = () => {
1249
+ return i18nString('Dummy Chat C');
1250
+ };
1251
+ const unknownViewMode = () => {
1252
+ return i18nString('Unknown view mode');
1253
+ };
1209
1254
 
1255
+ /* eslint-disable @cspell/spellchecker */
1256
+
1257
+ const getDefaultModels = () => {
1258
+ const defaultModelId = 'test';
1259
+ return [{
1260
+ id: defaultModelId,
1261
+ name: 'test',
1262
+ provider: 'test'
1263
+ }, {
1264
+ id: 'codex-5.3',
1265
+ name: 'Codex 5.3',
1266
+ provider: 'openRouter'
1267
+ }, {
1268
+ id: 'claude-code',
1269
+ name: 'Claude Code',
1270
+ provider: 'openRouter'
1271
+ }, {
1272
+ id: 'claude-haiku',
1273
+ name: 'Claude Haiku',
1274
+ provider: 'openRouter'
1275
+ }, {
1276
+ id: 'openRouter/openai/gpt-4o-mini',
1277
+ name: 'GPT-4o Mini',
1278
+ provider: 'openRouter'
1279
+ }, {
1280
+ id: 'openRouter/anthropic/claude-3.5-haiku',
1281
+ name: 'Claude 3.5 Haiku',
1282
+ provider: 'openRouter'
1283
+ }, {
1284
+ id: 'openRouter/google/gemini-2.0-flash-001',
1285
+ name: 'Gemini 2.0 Flash',
1286
+ provider: 'openRouter'
1287
+ }, {
1288
+ id: 'openRouter/openai/gpt-oss-20b:free',
1289
+ name: 'GPT OSS 20B (Free)',
1290
+ provider: 'openRouter'
1291
+ }, {
1292
+ id: 'openRouter/openai/gpt-oss-120b:free',
1293
+ name: 'GPT OSS 120B (Free)',
1294
+ provider: 'openRouter'
1295
+ }, {
1296
+ id: 'openRouter/meta-llama/llama-3.3-70b-instruct:free',
1297
+ name: 'Llama 3.3 70B Instruct (Free)',
1298
+ provider: 'openRouter'
1299
+ }, {
1300
+ id: 'openRouter/google/gemma-3-27b-it:free',
1301
+ name: 'Gemma 3 27B IT (Free)',
1302
+ provider: 'openRouter'
1303
+ }, {
1304
+ id: 'openRouter/qwen/qwen3-coder:free',
1305
+ name: 'Qwen3 Coder (Free)',
1306
+ provider: 'openRouter'
1307
+ }, {
1308
+ id: 'openRouter/mistralai/mistral-small-3.1-24b-instruct:free',
1309
+ name: 'Mistral Small 3.1 24B Instruct (Free)',
1310
+ provider: 'openRouter'
1311
+ }];
1312
+ };
1210
1313
  const createDefaultState = () => {
1211
1314
  const defaultSessionId = 'session-1';
1212
1315
  const defaultModelId = 'test';
@@ -1222,20 +1325,12 @@ const createDefaultState = () => {
1222
1325
  inputSource: 'script',
1223
1326
  lastSubmittedSessionId: '',
1224
1327
  listItemHeight: 40,
1225
- models: [{
1226
- id: defaultModelId,
1227
- name: 'test'
1228
- }, {
1229
- id: 'codex-5.3',
1230
- name: 'Codex 5.3'
1231
- }, {
1232
- id: 'claude-code',
1233
- name: 'Claude Code'
1234
- }, {
1235
- id: 'claude-haiku',
1236
- name: 'Claude Haiku'
1237
- }],
1328
+ models: getDefaultModels(),
1238
1329
  nextMessageId: 1,
1330
+ openRouterApiBaseUrl: 'https://openrouter.ai/api/v1',
1331
+ openRouterApiKey: '',
1332
+ openRouterApiKeyInput: '',
1333
+ openRouterApiKeysSettingsUrl: 'https://openrouter.ai/settings/keys',
1239
1334
  platform: 0,
1240
1335
  renamingSessionId: '',
1241
1336
  selectedModelId: defaultModelId,
@@ -1243,7 +1338,7 @@ const createDefaultState = () => {
1243
1338
  sessions: [{
1244
1339
  id: defaultSessionId,
1245
1340
  messages: [],
1246
- title: defaultSessionTitle
1341
+ title: defaultSessionTitle()
1247
1342
  }],
1248
1343
  tokensMax: 0,
1249
1344
  tokensUsed: 0,
@@ -1258,7 +1353,7 @@ const createDefaultState = () => {
1258
1353
  };
1259
1354
 
1260
1355
  const {
1261
- get,
1356
+ get: get$1,
1262
1357
  getCommandIds,
1263
1358
  registerCommands,
1264
1359
  set,
@@ -1266,7 +1361,7 @@ const {
1266
1361
  wrapGetter
1267
1362
  } = create$1();
1268
1363
 
1269
- const create = (uid, _uri, x, y, width, height, platform, assetDir) => {
1364
+ const create = (uid, x, y, width, height, platform, assetDir) => {
1270
1365
  const state = {
1271
1366
  ...createDefaultState(),
1272
1367
  assetDir,
@@ -1327,7 +1422,7 @@ const diff2 = uid => {
1327
1422
  const {
1328
1423
  newState,
1329
1424
  oldState
1330
- } = get(uid);
1425
+ } = get$1(uid);
1331
1426
  const result = diff(oldState, newState);
1332
1427
  return result;
1333
1428
  };
@@ -1352,8 +1447,11 @@ const Button$2 = 'button';
1352
1447
 
1353
1448
  const Button$1 = 1;
1354
1449
  const Div = 4;
1450
+ const Input = 6;
1355
1451
  const Span = 8;
1356
1452
  const Text = 12;
1453
+ const Li = 48;
1454
+ const Ol = 49;
1357
1455
  const P = 50;
1358
1456
  const TextArea = 62;
1359
1457
  const Select$1 = 63;
@@ -1676,107 +1774,184 @@ const getKeyBindings = () => {
1676
1774
  }];
1677
1775
  };
1678
1776
 
1679
- const SESSION_PREFIX$1 = 'session:';
1777
+ const getSelectedSessionId = state => {
1778
+ return state.selectedSessionId;
1779
+ };
1780
+
1781
+ const getListIndex = (state, eventX, eventY) => {
1782
+ const {
1783
+ headerHeight,
1784
+ height,
1785
+ listItemHeight,
1786
+ width,
1787
+ x,
1788
+ y
1789
+ } = state;
1790
+ if (eventX < x || eventY < y) {
1791
+ return -1;
1792
+ }
1793
+ if (eventX >= x + width || eventY >= y + height) {
1794
+ return -1;
1795
+ }
1796
+ const listY = eventY - y - headerHeight;
1797
+ if (listY < 0) {
1798
+ return -1;
1799
+ }
1800
+ const itemHeight = listItemHeight > 0 ? listItemHeight : 40;
1801
+ return Math.floor(listY / itemHeight);
1802
+ };
1803
+
1680
1804
  const CHAT_LIST_ITEM_CONTEXT_MENU = 'ChatListItemContextMenu';
1681
- const handleChatListContextMenu = async (name, x, y) => {
1682
- if (!name || !name.startsWith(SESSION_PREFIX$1)) {
1683
- return;
1805
+ const handleChatListContextMenu = async (state, eventX, eventY) => {
1806
+ const index = getListIndex(state, eventX, eventY);
1807
+ if (index === -1) {
1808
+ return state;
1684
1809
  }
1685
- const sessionId = name.slice(SESSION_PREFIX$1.length);
1686
- // @ts-ignore
1687
- await invoke('ContextMenu.show', x, y, CHAT_LIST_ITEM_CONTEXT_MENU, sessionId);
1810
+ const item = state.sessions[index];
1811
+ if (!item) {
1812
+ return state;
1813
+ }
1814
+ await invoke('ContextMenu.show', eventX, eventY, CHAT_LIST_ITEM_CONTEXT_MENU, item.id);
1815
+ return state;
1688
1816
  };
1689
1817
 
1690
- const LVCE_CHAT_SESSIONS_DB_NAME = 'lvce-chat-view-sessions';
1691
- const LVCE_CHAT_SESSIONS_DB_VERSION = 1;
1692
- const LVCE_CHAT_SESSIONS_STORE = 'chat-sessions';
1693
1818
  const toError = error => {
1694
1819
  if (error instanceof Error) {
1695
1820
  return error;
1696
1821
  }
1697
1822
  return new Error('IndexedDB request failed');
1698
1823
  };
1824
+
1699
1825
  const requestToPromise = async createRequest => {
1700
1826
  const request = createRequest();
1701
- return new Promise((resolve, reject) => {
1702
- request.addEventListener('success', () => {
1703
- resolve(request.result);
1704
- });
1705
- request.addEventListener('error', () => {
1706
- reject(toError(request.error));
1707
- });
1827
+ const {
1828
+ promise,
1829
+ reject,
1830
+ resolve
1831
+ } = Promise.withResolvers();
1832
+ request.addEventListener('success', () => {
1833
+ resolve(request.result);
1708
1834
  });
1709
- };
1710
- const transactionToPromise = async createTransaction => {
1711
- const transaction = createTransaction();
1712
- return new Promise((resolve, reject) => {
1713
- transaction.addEventListener('complete', () => {
1714
- resolve();
1715
- });
1716
- transaction.addEventListener('error', () => {
1717
- reject(toError(transaction.error));
1718
- });
1719
- transaction.addEventListener('abort', () => {
1720
- reject(toError(transaction.error));
1721
- });
1835
+ request.addEventListener('error', () => {
1836
+ reject(toError(request.error));
1722
1837
  });
1838
+ return promise;
1723
1839
  };
1724
- const openSessionsDatabase = async () => {
1725
- const request = indexedDB.open(LVCE_CHAT_SESSIONS_DB_NAME, LVCE_CHAT_SESSIONS_DB_VERSION);
1840
+
1841
+ const openSessionsDatabase = async (databaseName, databaseVersion, storeName) => {
1842
+ const request = indexedDB.open(databaseName, databaseVersion);
1726
1843
  request.addEventListener('upgradeneeded', () => {
1727
1844
  const database = request.result;
1728
- if (!database.objectStoreNames.contains(LVCE_CHAT_SESSIONS_STORE)) {
1729
- database.createObjectStore(LVCE_CHAT_SESSIONS_STORE, {
1845
+ if (!database.objectStoreNames.contains(storeName)) {
1846
+ database.createObjectStore(storeName, {
1730
1847
  keyPath: 'id'
1731
1848
  });
1732
1849
  }
1733
1850
  });
1734
1851
  return requestToPromise(() => request);
1735
1852
  };
1853
+
1854
+ const getDatabase = async (getDatabasePromise, setDatabasePromise, databaseName, databaseVersion, storeName) => {
1855
+ const existingDatabasePromise = getDatabasePromise();
1856
+ if (existingDatabasePromise) {
1857
+ return existingDatabasePromise;
1858
+ }
1859
+ const nextDatabasePromise = openSessionsDatabase(databaseName, databaseVersion, storeName);
1860
+ setDatabasePromise(nextDatabasePromise);
1861
+ return nextDatabasePromise;
1862
+ };
1863
+
1864
+ const transactionToPromise = async createTransaction => {
1865
+ const transaction = createTransaction();
1866
+ const {
1867
+ promise,
1868
+ reject,
1869
+ resolve
1870
+ } = Promise.withResolvers();
1871
+ transaction.addEventListener('complete', () => {
1872
+ resolve();
1873
+ });
1874
+ transaction.addEventListener('error', () => {
1875
+ reject(toError(transaction.error));
1876
+ });
1877
+ transaction.addEventListener('abort', () => {
1878
+ reject(toError(transaction.error));
1879
+ });
1880
+ return promise;
1881
+ };
1882
+
1883
+ const clear = async (getDatabasePromise, setDatabasePromise, databaseName, databaseVersion, storeName) => {
1884
+ const database = await getDatabase(getDatabasePromise, setDatabasePromise, databaseName, databaseVersion, storeName);
1885
+ const transaction = database.transaction(storeName, 'readwrite');
1886
+ const createTransaction = () => transaction;
1887
+ const store = transaction.objectStore(storeName);
1888
+ store.clear();
1889
+ await transactionToPromise(createTransaction);
1890
+ };
1891
+
1892
+ const deleteSession$1 = async (getDatabasePromise, setDatabasePromise, databaseName, databaseVersion, storeName, id) => {
1893
+ const database = await getDatabase(getDatabasePromise, setDatabasePromise, databaseName, databaseVersion, storeName);
1894
+ const transaction = database.transaction(storeName, 'readwrite');
1895
+ const createTransaction = () => transaction;
1896
+ const store = transaction.objectStore(storeName);
1897
+ store.delete(id);
1898
+ await transactionToPromise(createTransaction);
1899
+ };
1900
+
1901
+ const getSession = async (getDatabasePromise, setDatabasePromise, databaseName, databaseVersion, storeName, id) => {
1902
+ const database = await getDatabase(getDatabasePromise, setDatabasePromise, databaseName, databaseVersion, storeName);
1903
+ const transaction = database.transaction(storeName, 'readonly');
1904
+ const store = transaction.objectStore(storeName);
1905
+ const result = await requestToPromise(() => store.get(id));
1906
+ return result;
1907
+ };
1908
+
1909
+ const listSessions = async (getDatabasePromise, setDatabasePromise, databaseName, databaseVersion, storeName) => {
1910
+ const database = await getDatabase(getDatabasePromise, setDatabasePromise, databaseName, databaseVersion, storeName);
1911
+ const transaction = database.transaction(storeName, 'readonly');
1912
+ const store = transaction.objectStore(storeName);
1913
+ const result = await requestToPromise(() => store.getAll());
1914
+ return result;
1915
+ };
1916
+
1917
+ const setSession = async (getDatabasePromise, setDatabasePromise, databaseName, databaseVersion, storeName, session) => {
1918
+ const database = await getDatabase(getDatabasePromise, setDatabasePromise, databaseName, databaseVersion, storeName);
1919
+ const transaction = database.transaction(storeName, 'readwrite');
1920
+ const createTransaction = () => transaction;
1921
+ const store = transaction.objectStore(storeName);
1922
+ store.put(session);
1923
+ await transactionToPromise(createTransaction);
1924
+ };
1925
+
1736
1926
  class IndexedDbChatSessionStorage {
1737
- async getDatabase() {
1738
- if (!this.databasePromise) {
1739
- this.databasePromise = openSessionsDatabase();
1740
- }
1741
- return this.databasePromise;
1927
+ constructor(options = {}) {
1928
+ this.state = {
1929
+ databaseName: options.databaseName || 'lvce-chat-view-sessions',
1930
+ databasePromise: undefined,
1931
+ databaseVersion: options.databaseVersion || 1,
1932
+ storeName: options.storeName || 'chat-sessions'
1933
+ };
1742
1934
  }
1935
+ getDatabasePromise = () => {
1936
+ return this.state.databasePromise;
1937
+ };
1938
+ setDatabasePromise = databasePromise => {
1939
+ this.state.databasePromise = databasePromise;
1940
+ };
1743
1941
  async clear() {
1744
- const database = await this.getDatabase();
1745
- const transaction = database.transaction(LVCE_CHAT_SESSIONS_STORE, 'readwrite');
1746
- const createTransaction = () => transaction;
1747
- const store = transaction.objectStore(LVCE_CHAT_SESSIONS_STORE);
1748
- store.clear();
1749
- await transactionToPromise(createTransaction);
1942
+ return clear(this.getDatabasePromise, this.setDatabasePromise, this.state.databaseName, this.state.databaseVersion, this.state.storeName);
1750
1943
  }
1751
1944
  async deleteSession(id) {
1752
- const database = await this.getDatabase();
1753
- const transaction = database.transaction(LVCE_CHAT_SESSIONS_STORE, 'readwrite');
1754
- const createTransaction = () => transaction;
1755
- const store = transaction.objectStore(LVCE_CHAT_SESSIONS_STORE);
1756
- store.delete(id);
1757
- await transactionToPromise(createTransaction);
1945
+ return deleteSession$1(this.getDatabasePromise, this.setDatabasePromise, this.state.databaseName, this.state.databaseVersion, this.state.storeName, id);
1758
1946
  }
1759
1947
  async getSession(id) {
1760
- const database = await this.getDatabase();
1761
- const transaction = database.transaction(LVCE_CHAT_SESSIONS_STORE, 'readonly');
1762
- const store = transaction.objectStore(LVCE_CHAT_SESSIONS_STORE);
1763
- const result = await requestToPromise(() => store.get(id));
1764
- return result;
1948
+ return getSession(this.getDatabasePromise, this.setDatabasePromise, this.state.databaseName, this.state.databaseVersion, this.state.storeName, id);
1765
1949
  }
1766
1950
  async listSessions() {
1767
- const database = await this.getDatabase();
1768
- const transaction = database.transaction(LVCE_CHAT_SESSIONS_STORE, 'readonly');
1769
- const store = transaction.objectStore(LVCE_CHAT_SESSIONS_STORE);
1770
- const result = await requestToPromise(() => store.getAll());
1771
- return result;
1951
+ return listSessions(this.getDatabasePromise, this.setDatabasePromise, this.state.databaseName, this.state.databaseVersion, this.state.storeName);
1772
1952
  }
1773
1953
  async setSession(session) {
1774
- const database = await this.getDatabase();
1775
- const transaction = database.transaction(LVCE_CHAT_SESSIONS_STORE, 'readwrite');
1776
- const createTransaction = () => transaction;
1777
- const store = transaction.objectStore(LVCE_CHAT_SESSIONS_STORE);
1778
- store.put(session);
1779
- await transactionToPromise(createTransaction);
1954
+ return setSession(this.getDatabasePromise, this.setDatabasePromise, this.state.databaseName, this.state.databaseVersion, this.state.storeName, session);
1780
1955
  }
1781
1956
  }
1782
1957
 
@@ -1909,22 +2084,135 @@ const deleteSession = async (state, id) => {
1909
2084
  };
1910
2085
  };
1911
2086
 
1912
- const focusInput = state => {
1913
- return {
1914
- ...state,
1915
- focus: 'composer',
1916
- focused: true
1917
- };
2087
+ const handleClickOpenRouterApiKeySettings = async state => {
2088
+ await openExternal(state.openRouterApiKeysSettingsUrl);
2089
+ return state;
1918
2090
  };
1919
2091
 
2092
+ const openRouterApiKeyRequiredMessage = 'OpenRouter API key is not configured. Enter your OpenRouter API key below and click Save.';
2093
+ const openRouterRequestFailedMessage = 'OpenRouter request failed. Possible reasons:';
2094
+ const openRouterRequestFailureReasons = ['ContentSecurityPolicyViolation: Check DevTools for details.', 'OpenRouter server offline: Check DevTools for details.', 'Check your internet connection.'];
2095
+
1920
2096
  const delay = async ms => {
1921
2097
  await new Promise(resolve => setTimeout(resolve, ms));
1922
2098
  };
1923
- const getMockAiResponse = userMessage => {
2099
+
2100
+ const getMockAiResponse = async userMessage => {
2101
+ await delay(800);
1924
2102
  return `Mock AI response: I received "${userMessage}".`;
1925
2103
  };
1926
- const getAiResponse = async (userText, nextMessageId) => {
1927
- await delay(800);
2104
+
2105
+ const defaultOpenRouterApiBaseUrl = 'https://openrouter.ai/api/v1';
2106
+ const getOpenRouterApiEndpoint = openRouterApiBaseUrl => {
2107
+ const trimmedBaseUrl = (openRouterApiBaseUrl || defaultOpenRouterApiBaseUrl).replace(/\/+$/, '');
2108
+ return `${trimmedBaseUrl}/chat/completions`;
2109
+ };
2110
+
2111
+ const getTextContent = content => {
2112
+ if (typeof content === 'string') {
2113
+ return content;
2114
+ }
2115
+ if (!Array.isArray(content)) {
2116
+ return '';
2117
+ }
2118
+ const textParts = [];
2119
+ for (const part of content) {
2120
+ if (!part || typeof part !== 'object') {
2121
+ continue;
2122
+ }
2123
+ const maybeType = Reflect.get(part, 'type');
2124
+ const maybeText = Reflect.get(part, 'text');
2125
+ if (maybeType === 'text' && typeof maybeText === 'string') {
2126
+ textParts.push(maybeText);
2127
+ }
2128
+ }
2129
+ return textParts.join('\n');
2130
+ };
2131
+
2132
+ const getOpenRouterAssistantText = async (userText, modelId, openRouterApiKey, openRouterApiBaseUrl) => {
2133
+ let response;
2134
+ try {
2135
+ response = await fetch(getOpenRouterApiEndpoint(openRouterApiBaseUrl), {
2136
+ body: JSON.stringify({
2137
+ messages: [{
2138
+ content: userText,
2139
+ role: 'user'
2140
+ }],
2141
+ model: modelId
2142
+ }),
2143
+ headers: {
2144
+ Authorization: `Bearer ${openRouterApiKey}`,
2145
+ 'Content-Type': 'application/json'
2146
+ },
2147
+ method: 'POST'
2148
+ });
2149
+ } catch {
2150
+ throw new Error(openRouterRequestFailedMessage);
2151
+ }
2152
+ if (!response.ok) {
2153
+ throw new Error(`Failed to get OpenRouter response: ${response.status}`);
2154
+ }
2155
+ const parsed = await response.json();
2156
+ if (!parsed || typeof parsed !== 'object') {
2157
+ return '';
2158
+ }
2159
+ const choices = Reflect.get(parsed, 'choices');
2160
+ if (!Array.isArray(choices)) {
2161
+ return '';
2162
+ }
2163
+ const firstChoice = choices[0];
2164
+ if (!firstChoice || typeof firstChoice !== 'object') {
2165
+ return '';
2166
+ }
2167
+ const message = Reflect.get(firstChoice, 'message');
2168
+ if (!message || typeof message !== 'object') {
2169
+ return '';
2170
+ }
2171
+ const content = Reflect.get(message, 'content');
2172
+ return getTextContent(content);
2173
+ };
2174
+
2175
+ /* eslint-disable @cspell/spellchecker */
2176
+ const getOpenRouterModelId = selectedModelId => {
2177
+ const openRouterPrefix = 'openrouter/';
2178
+ if (selectedModelId.toLowerCase().startsWith(openRouterPrefix)) {
2179
+ return selectedModelId.slice(openRouterPrefix.length);
2180
+ }
2181
+ return selectedModelId;
2182
+ };
2183
+
2184
+ /* eslint-disable @cspell/spellchecker */
2185
+
2186
+ const isOpenRouterModel = (selectedModelId, models) => {
2187
+ const selectedModel = models.find(model => model.id === selectedModelId);
2188
+ const normalizedProvider = selectedModel?.provider?.toLowerCase();
2189
+ if (normalizedProvider === 'openrouter' || normalizedProvider === 'open-router') {
2190
+ return true;
2191
+ }
2192
+ return selectedModelId.toLowerCase().startsWith('openrouter/');
2193
+ };
2194
+
2195
+ const getAiResponse = async (userText, nextMessageId, selectedModelId, models, openRouterApiKey, openRouterApiBaseUrl) => {
2196
+ let text = '';
2197
+ const usesOpenRouterModel = isOpenRouterModel(selectedModelId, models);
2198
+ if (usesOpenRouterModel) {
2199
+ if (openRouterApiKey) {
2200
+ try {
2201
+ text = await getOpenRouterAssistantText(userText, getOpenRouterModelId(selectedModelId), openRouterApiKey, openRouterApiBaseUrl);
2202
+ } catch (error) {
2203
+ if (error instanceof Error && error.message) {
2204
+ text = error.message;
2205
+ } else {
2206
+ text = openRouterRequestFailedMessage;
2207
+ }
2208
+ }
2209
+ } else {
2210
+ text = openRouterApiKeyRequiredMessage;
2211
+ }
2212
+ }
2213
+ if (!text && !usesOpenRouterModel) {
2214
+ text = await getMockAiResponse(userText);
2215
+ }
1928
2216
  const assistantTime = new Date().toLocaleTimeString([], {
1929
2217
  hour: '2-digit',
1930
2218
  minute: '2-digit'
@@ -1932,15 +2220,88 @@ const getAiResponse = async (userText, nextMessageId) => {
1932
2220
  return {
1933
2221
  id: `message-${nextMessageId}`,
1934
2222
  role: 'assistant',
1935
- text: getMockAiResponse(userText),
2223
+ text,
1936
2224
  time: assistantTime
1937
2225
  };
1938
2226
  };
1939
2227
 
2228
+ const get = async key => {
2229
+ return getPreference(key);
2230
+ };
2231
+ const update = async settings => {
2232
+ await invoke('Preferences.update', settings);
2233
+ };
2234
+
2235
+ const setOpenRouterApiKey = async (state, openRouterApiKey, persist = true) => {
2236
+ if (persist) {
2237
+ await update({
2238
+ 'secrets.openRouterApiKey': openRouterApiKey
2239
+ });
2240
+ }
2241
+ return {
2242
+ ...state,
2243
+ openRouterApiKey,
2244
+ openRouterApiKeyInput: openRouterApiKey
2245
+ };
2246
+ };
2247
+
2248
+ const handleClickSaveOpenRouterApiKey = async state => {
2249
+ const {
2250
+ openRouterApiKeyInput
2251
+ } = state;
2252
+ const openRouterApiKey = openRouterApiKeyInput.trim();
2253
+ if (!openRouterApiKey) {
2254
+ return state;
2255
+ }
2256
+ const updatedState = await setOpenRouterApiKey(state, openRouterApiKey);
2257
+ const session = updatedState.sessions.find(item => item.id === updatedState.selectedSessionId);
2258
+ if (!session) {
2259
+ return updatedState;
2260
+ }
2261
+ const lastMessage = session.messages.at(-1);
2262
+ const shouldRetryOpenRouter = lastMessage?.role === 'assistant' && lastMessage.text === openRouterApiKeyRequiredMessage;
2263
+ if (!shouldRetryOpenRouter) {
2264
+ return updatedState;
2265
+ }
2266
+ const previousUserMessage = session.messages.toReversed().find(item => item.role === 'user');
2267
+ if (!previousUserMessage) {
2268
+ return updatedState;
2269
+ }
2270
+ const assistantMessage = await getAiResponse(previousUserMessage.text, updatedState.nextMessageId, updatedState.selectedModelId, updatedState.models, openRouterApiKey, updatedState.openRouterApiBaseUrl);
2271
+ const updatedSession = {
2272
+ ...session,
2273
+ messages: [...session.messages.slice(0, -1), assistantMessage]
2274
+ };
2275
+ await saveChatSession(updatedSession);
2276
+ const updatedSessions = updatedState.sessions.map(item => {
2277
+ if (item.id !== updatedState.selectedSessionId) {
2278
+ return item;
2279
+ }
2280
+ return updatedSession;
2281
+ });
2282
+ return {
2283
+ ...updatedState,
2284
+ nextMessageId: updatedState.nextMessageId + 1,
2285
+ sessions: updatedSessions
2286
+ };
2287
+ };
2288
+
2289
+ const focusInput = state => {
2290
+ return {
2291
+ ...state,
2292
+ focus: 'composer',
2293
+ focused: true
2294
+ };
2295
+ };
2296
+
1940
2297
  const handleSubmit = async state => {
1941
2298
  const {
1942
2299
  composerValue,
2300
+ models,
1943
2301
  nextMessageId,
2302
+ openRouterApiBaseUrl,
2303
+ openRouterApiKey,
2304
+ selectedModelId,
1944
2305
  selectedSessionId,
1945
2306
  sessions,
1946
2307
  viewMode
@@ -2016,7 +2377,7 @@ const handleSubmit = async state => {
2016
2377
  set(state.uid, state, optimisticState);
2017
2378
  // @ts-ignore
2018
2379
  await invoke('Chat.rerender');
2019
- const assistantMessage = await getAiResponse(userText, optimisticState.nextMessageId);
2380
+ const assistantMessage = await getAiResponse(userText, optimisticState.nextMessageId, selectedModelId, models, openRouterApiKey, openRouterApiBaseUrl);
2020
2381
  const updatedSessions = optimisticState.sessions.map(session => {
2021
2382
  if (session.id !== optimisticState.selectedSessionId) {
2022
2383
  return session;
@@ -2051,6 +2412,37 @@ const handleClickSend = async state => {
2051
2412
  return handleSubmit(submitState);
2052
2413
  };
2053
2414
 
2415
+ const Composer = 'composer';
2416
+ const Send = 'send';
2417
+ const Back = 'back';
2418
+ const Model = 'model';
2419
+ const CreateSession = 'create-session';
2420
+ const Settings = 'settings';
2421
+ const CloseChat = 'close-chat';
2422
+ const SessionDelete = 'SessionDelete';
2423
+ const SessionPrefix = 'session:';
2424
+ const RenamePrefix = 'session-rename:';
2425
+ const getSessionInputName = sessionId => {
2426
+ return `${SessionPrefix}${sessionId}`;
2427
+ };
2428
+ const isSessionInputName = name => {
2429
+ return name.startsWith(SessionPrefix);
2430
+ };
2431
+ const getSessionIdFromInputName = name => {
2432
+ return name.slice(SessionPrefix.length);
2433
+ };
2434
+ const isRenameInputName = name => {
2435
+ return name.startsWith(RenamePrefix);
2436
+ };
2437
+ const getRenameIdFromInputName = name => {
2438
+ return name.slice(RenamePrefix.length);
2439
+ };
2440
+
2441
+ /* eslint-disable @cspell/spellchecker */
2442
+ const OpenRouterApiKeyInput = 'open-router-api-key';
2443
+ const SaveOpenRouterApiKey = 'save-openrouter-api-key';
2444
+ const OpenOpenRouterApiKeySettings = 'open-openrouter-api-key-settings';
2445
+
2054
2446
  const selectSession = async (state, id) => {
2055
2447
  const exists = state.sessions.some(session => session.id === id);
2056
2448
  if (!exists) {
@@ -2092,29 +2484,6 @@ const startRename = (state, id) => {
2092
2484
  };
2093
2485
  };
2094
2486
 
2095
- const getListIndex = (state, eventX, eventY) => {
2096
- const {
2097
- headerHeight,
2098
- height,
2099
- listItemHeight,
2100
- width,
2101
- x,
2102
- y
2103
- } = state;
2104
- if (eventX < x || eventY < y) {
2105
- return -1;
2106
- }
2107
- if (eventX >= x + width || eventY >= y + height) {
2108
- return -1;
2109
- }
2110
- const listY = eventY - y - headerHeight;
2111
- if (listY < 0) {
2112
- return -1;
2113
- }
2114
- const itemHeight = listItemHeight > 0 ? listItemHeight : 40;
2115
- return Math.floor(listY / itemHeight);
2116
- };
2117
-
2118
2487
  const selectListIndex = async (state, index) => {
2119
2488
  const {
2120
2489
  sessions
@@ -2131,32 +2500,33 @@ const handleClickList = async (state, eventX, eventY) => {
2131
2500
  return selectListIndex(state, index);
2132
2501
  };
2133
2502
 
2134
- const CREATE_SESSION = 'create-session';
2135
- const SESSION_PREFIX = 'session:';
2136
- const RENAME_PREFIX = 'session-rename:';
2137
- const SESSION_DELETE = 'SessionDelete';
2138
- const SEND = 'send';
2139
2503
  const handleClick = async (state, name, id = '') => {
2140
2504
  if (!name) {
2141
2505
  return state;
2142
2506
  }
2143
- if (name === CREATE_SESSION) {
2507
+ if (name === CreateSession) {
2144
2508
  return createSession(state);
2145
2509
  }
2146
- if (name.startsWith(SESSION_PREFIX)) {
2147
- const id = name.slice(SESSION_PREFIX.length);
2148
- return selectSession(state, id);
2510
+ if (isSessionInputName(name)) {
2511
+ const sessionId = getSessionIdFromInputName(name);
2512
+ return selectSession(state, sessionId);
2149
2513
  }
2150
- if (name.startsWith(RENAME_PREFIX)) {
2151
- const id = name.slice(RENAME_PREFIX.length);
2152
- return startRename(state, id);
2514
+ if (isRenameInputName(name)) {
2515
+ const sessionId = getRenameIdFromInputName(name);
2516
+ return startRename(state, sessionId);
2153
2517
  }
2154
- if (name === SESSION_DELETE) {
2518
+ if (name === SessionDelete) {
2155
2519
  return deleteSession(state, id);
2156
2520
  }
2157
- if (name === SEND) {
2521
+ if (name === Send) {
2158
2522
  return handleClickSend(state);
2159
2523
  }
2524
+ if (name === SaveOpenRouterApiKey) {
2525
+ return handleClickSaveOpenRouterApiKey(state);
2526
+ }
2527
+ if (name === OpenOpenRouterApiKeySettings) {
2528
+ return handleClickOpenRouterApiKeySettings(state);
2529
+ }
2160
2530
  return state;
2161
2531
  };
2162
2532
 
@@ -2186,7 +2556,16 @@ const handleClickSettings = async () => {
2186
2556
  await invoke('Main.openUri', 'app://settings.json');
2187
2557
  };
2188
2558
 
2189
- const handleInput = async (state, value, inputSource = 'user') => {
2559
+ const handleInput = async (state, name, value, inputSource = 'user') => {
2560
+ if (name === OpenRouterApiKeyInput) {
2561
+ return {
2562
+ ...state,
2563
+ openRouterApiKeyInput: value
2564
+ };
2565
+ }
2566
+ if (name !== Composer) {
2567
+ return state;
2568
+ }
2190
2569
  return {
2191
2570
  ...state,
2192
2571
  composerValue: value,
@@ -2195,24 +2574,24 @@ const handleInput = async (state, value, inputSource = 'user') => {
2195
2574
  };
2196
2575
 
2197
2576
  const handleInputFocus = async (state, name) => {
2198
- if (name === 'composer') {
2577
+ if (name === Composer) {
2199
2578
  return focusInput(state);
2200
2579
  }
2201
- if (name === 'send') {
2580
+ if (name === Send) {
2202
2581
  return {
2203
2582
  ...state,
2204
2583
  focus: 'send-button',
2205
2584
  focused: true
2206
2585
  };
2207
2586
  }
2208
- if (name.startsWith('session:') || name === 'SessionDelete') {
2587
+ if (isSessionInputName(name) || name === SessionDelete) {
2209
2588
  return {
2210
2589
  ...state,
2211
2590
  focus: 'list',
2212
2591
  focused: true
2213
2592
  };
2214
2593
  }
2215
- if (name === 'create-session' || name === 'settings' || name === 'close-chat' || name === 'back') {
2594
+ if (name === CreateSession || name === Settings || name === CloseChat || name === Back) {
2216
2595
  return {
2217
2596
  ...state,
2218
2597
  focus: 'header',
@@ -2291,7 +2670,7 @@ const handleModelChange = async (state, value) => {
2291
2670
  };
2292
2671
 
2293
2672
  const handleNewline = async state => {
2294
- return handleInput(state, `${state.composerValue}\n`);
2673
+ return handleInput(state, Composer, `${state.composerValue}\n`);
2295
2674
  };
2296
2675
 
2297
2676
  const id = 7201;
@@ -2320,36 +2699,6 @@ const isObject = value => {
2320
2699
  return typeof value === 'object' && value !== null;
2321
2700
  };
2322
2701
 
2323
- const getSavedBounds = savedState => {
2324
- if (!isObject(savedState)) {
2325
- return undefined;
2326
- }
2327
- const {
2328
- height,
2329
- width,
2330
- x,
2331
- y
2332
- } = savedState;
2333
- if (typeof x !== 'number') {
2334
- return undefined;
2335
- }
2336
- if (typeof y !== 'number') {
2337
- return undefined;
2338
- }
2339
- if (typeof width !== 'number') {
2340
- return undefined;
2341
- }
2342
- if (typeof height !== 'number') {
2343
- return undefined;
2344
- }
2345
- return {
2346
- height,
2347
- width,
2348
- x,
2349
- y
2350
- };
2351
- };
2352
-
2353
2702
  const getSavedSelectedModelId = savedState => {
2354
2703
  if (!isObject(savedState)) {
2355
2704
  return undefined;
@@ -2425,9 +2774,15 @@ const loadSelectedSessionMessages = async (sessions, selectedSessionId) => {
2425
2774
  });
2426
2775
  };
2427
2776
  const loadContent = async (state, savedState) => {
2428
- const savedBounds = getSavedBounds(savedState);
2429
2777
  const savedSelectedModelId = getSavedSelectedModelId(savedState);
2430
2778
  const savedViewMode = getSavedViewMode(savedState);
2779
+ let openRouterApiKey = '';
2780
+ try {
2781
+ const savedOpenRouterApiKey = await get('secrets.openRouterApiKey');
2782
+ openRouterApiKey = typeof savedOpenRouterApiKey === 'string' ? savedOpenRouterApiKey : '';
2783
+ } catch {
2784
+ openRouterApiKey = '';
2785
+ }
2431
2786
  const legacySavedSessions = getSavedSessions(savedState);
2432
2787
  const storedSessions = await listChatSessions();
2433
2788
  let sessions = storedSessions;
@@ -2452,8 +2807,9 @@ const loadContent = async (state, savedState) => {
2452
2807
  const viewMode = sessions.length === 0 || !selectedSessionId ? 'list' : preferredViewMode === 'detail' ? 'detail' : 'list';
2453
2808
  return {
2454
2809
  ...state,
2455
- ...savedBounds,
2456
2810
  initial: false,
2811
+ openRouterApiKey,
2812
+ openRouterApiKeyInput: openRouterApiKey,
2457
2813
  selectedModelId,
2458
2814
  selectedSessionId,
2459
2815
  sessions,
@@ -2462,11 +2818,14 @@ const loadContent = async (state, savedState) => {
2462
2818
  };
2463
2819
 
2464
2820
  const openMockSession = async (state, mockSessionId, mockChatMessages) => {
2821
+ const {
2822
+ sessions: currentSessions
2823
+ } = state;
2465
2824
  if (!mockSessionId) {
2466
2825
  return state;
2467
2826
  }
2468
- const existingSession = state.sessions.find(session => session.id === mockSessionId);
2469
- const sessions = existingSession ? state.sessions.map(session => {
2827
+ const existingSession = currentSessions.find(session => session.id === mockSessionId);
2828
+ const sessions = existingSession ? currentSessions.map(session => {
2470
2829
  if (session.id !== mockSessionId) {
2471
2830
  return session;
2472
2831
  }
@@ -2474,7 +2833,7 @@ const openMockSession = async (state, mockSessionId, mockChatMessages) => {
2474
2833
  ...session,
2475
2834
  messages: mockChatMessages
2476
2835
  };
2477
- }) : [...state.sessions, {
2836
+ }) : [...currentSessions, {
2478
2837
  id: mockSessionId,
2479
2838
  messages: mockChatMessages,
2480
2839
  title: mockSessionId
@@ -2524,17 +2883,20 @@ const renderFocusContext = (oldState, newState) => {
2524
2883
  return [SetFocusContext, newState.uid, FocusChatInput];
2525
2884
  };
2526
2885
 
2886
+ const Actions = 'Actions';
2527
2887
  const ChatActions = 'ChatActions';
2528
2888
  const ChatName = 'ChatName';
2529
2889
  const ChatSendArea = 'ChatSendArea';
2890
+ const SendButtonDisabled = 'SendButtonDisabled';
2530
2891
  const ChatSendAreaBottom = 'ChatSendAreaBottom';
2531
2892
  const ChatSendAreaContent = 'ChatSendAreaContent';
2532
2893
  const Chat = 'Chat';
2533
2894
  const ChatHeader = 'ChatHeader';
2534
2895
  const Button = 'Button';
2535
- const ButtonDisabled = 'ButtonDisabled';
2536
2896
  const ButtonPrimary = 'ButtonPrimary';
2897
+ const ButtonSecondary = 'ButtonSecondary';
2537
2898
  const IconButton = 'IconButton';
2899
+ const InputBox = 'InputBox';
2538
2900
  const Label = 'Label';
2539
2901
  const LabelDetail = 'LabelDetail';
2540
2902
  const ChatList = 'ChatList';
@@ -2569,6 +2931,12 @@ const HandleClickDelete = 18;
2569
2931
  const HandleSubmit = 19;
2570
2932
  const HandleModelChange = 20;
2571
2933
 
2934
+ const getModelLabel = model => {
2935
+ if (model.provider === 'openRouter') {
2936
+ return `${model.name} (OpenRouter)`;
2937
+ }
2938
+ return model.name;
2939
+ };
2572
2940
  const getModelOptionDOm = (model, selectedModelId) => {
2573
2941
  return [{
2574
2942
  childCount: 1,
@@ -2576,11 +2944,11 @@ const getModelOptionDOm = (model, selectedModelId) => {
2576
2944
  selected: model.id === selectedModelId,
2577
2945
  type: Option$1,
2578
2946
  value: model.id
2579
- }, text(model.name)];
2947
+ }, text(getModelLabel(model))];
2580
2948
  };
2581
2949
 
2582
2950
  const getSendButtonClassName = isSendDisabled => {
2583
- return isSendDisabled ? `${Button} ${ButtonPrimary} ${ButtonDisabled}` : `${Button} ${ButtonPrimary}`;
2951
+ return isSendDisabled ? `${IconButton} ${SendButtonDisabled}` : `${IconButton}`;
2584
2952
  };
2585
2953
 
2586
2954
  const getSendButtonDom = isSendDisabled => {
@@ -2589,14 +2957,14 @@ const getSendButtonDom = isSendDisabled => {
2589
2957
  childCount: 1,
2590
2958
  className: sendButtonClassName,
2591
2959
  disabled: isSendDisabled,
2592
- name: 'send',
2960
+ name: Send,
2593
2961
  onClick: HandleSubmit,
2594
2962
  role: Button$2,
2595
- title: sendMessage,
2963
+ title: sendMessage(),
2596
2964
  type: Button$1
2597
2965
  }, {
2598
2966
  childCount: 0,
2599
- class: 'MaskIcon MaskIconSend',
2967
+ className: 'MaskIcon MaskIconSend',
2600
2968
  type: Div
2601
2969
  }];
2602
2970
  };
@@ -2650,10 +3018,10 @@ const getChatSendAreaDom = (composerValue, models, selectedModelId, usageOvervie
2650
3018
  }, {
2651
3019
  childCount: 0,
2652
3020
  className: MultilineInputBox,
2653
- name: 'composer',
3021
+ name: Composer,
2654
3022
  onFocus: HandleFocus,
2655
3023
  onInput: HandleInput,
2656
- placeholder: composePlaceholder,
3024
+ placeholder: composePlaceholder(),
2657
3025
  rows: 4,
2658
3026
  type: TextArea,
2659
3027
  value: composerValue
@@ -2664,7 +3032,7 @@ const getChatSendAreaDom = (composerValue, models, selectedModelId, usageOvervie
2664
3032
  }, {
2665
3033
  childCount: models.length,
2666
3034
  className: Select,
2667
- name: 'model',
3035
+ name: Model,
2668
3036
  onInput: HandleModelChange,
2669
3037
  type: Select$1,
2670
3038
  value: selectedModelId
@@ -2675,10 +3043,10 @@ const getBackButtonVirtualDom = () => {
2675
3043
  return [{
2676
3044
  childCount: 1,
2677
3045
  className: IconButton,
2678
- name: 'back',
3046
+ name: Back,
2679
3047
  onClick: HandleClickBack,
2680
3048
  role: Button$2,
2681
- title: backToChats,
3049
+ title: backToChats(),
2682
3050
  type: Button$1
2683
3051
  }, {
2684
3052
  childCount: 0,
@@ -2706,19 +3074,19 @@ const getHeaderActionVirtualDom = item => {
2706
3074
  const getChatHeaderActionsDom = () => {
2707
3075
  const items = [{
2708
3076
  icon: 'MaskIcon MaskIconAdd',
2709
- name: 'create-session',
3077
+ name: CreateSession,
2710
3078
  onClick: HandleClickNew,
2711
- title: newChat
3079
+ title: newChat()
2712
3080
  }, {
2713
3081
  icon: 'MaskIcon MaskIconSettingsGear',
2714
- name: 'settings',
3082
+ name: Settings,
2715
3083
  onClick: HandleClickSettings,
2716
- title: settings
3084
+ title: settings()
2717
3085
  }, {
2718
3086
  icon: 'MaskIcon MaskIconClose',
2719
- name: 'close-chat',
3087
+ name: CloseChat,
2720
3088
  onClick: HandleClickClose,
2721
- title: closeChat
3089
+ title: closeChat()
2722
3090
  }];
2723
3091
  return [{
2724
3092
  childCount: items.length,
@@ -2743,47 +3111,96 @@ const getChatHeaderDomDetailMode = selectedSessionTitle => {
2743
3111
  }, text(selectedSessionTitle), ...getChatHeaderActionsDom()];
2744
3112
  };
2745
3113
 
2746
- const getChatMessageDom = message => {
3114
+ const getMissingOpenRouterApiKeyDom = openRouterApiKeyInput => {
3115
+ return [{
3116
+ childCount: 2,
3117
+ type: Div
3118
+ }, {
3119
+ childCount: 0,
3120
+ className: InputBox,
3121
+ name: OpenRouterApiKeyInput,
3122
+ onInput: HandleInput,
3123
+ placeholder: openRouterApiKeyPlaceholder(),
3124
+ type: Input,
3125
+ value: openRouterApiKeyInput
3126
+ }, {
3127
+ childCount: 2,
3128
+ className: Actions,
3129
+ type: Div
3130
+ }, {
3131
+ childCount: 1,
3132
+ className: mergeClassNames(Button, ButtonPrimary),
3133
+ name: SaveOpenRouterApiKey,
3134
+ onClick: HandleClick,
3135
+ type: Button$1
3136
+ }, text(save()), {
3137
+ childCount: 1,
3138
+ className: mergeClassNames(Button, ButtonSecondary),
3139
+ name: OpenOpenRouterApiKeySettings,
3140
+ onClick: HandleClick,
3141
+ type: Button$1
3142
+ }, text(getOpenRouterApiKey())];
3143
+ };
3144
+
3145
+ const getOpenRouterRequestFailedDom = () => {
3146
+ return [{
3147
+ childCount: openRouterRequestFailureReasons.length,
3148
+ className: Markdown,
3149
+ type: Ol
3150
+ }, ...openRouterRequestFailureReasons.flatMap(reason => {
3151
+ return [{
3152
+ childCount: 1,
3153
+ type: Li
3154
+ }, text(reason)];
3155
+ })];
3156
+ };
3157
+ const getChatMessageDom = (message, openRouterApiKeyInput) => {
2747
3158
  const roleClassName = message.role === 'user' ? MessageUser : MessageAssistant;
3159
+ const isOpenRouterApiKeyMissingMessage = message.role === 'assistant' && message.text === openRouterApiKeyRequiredMessage;
3160
+ const isOpenRouterRequestFailedMessage = message.role === 'assistant' && message.text === openRouterRequestFailedMessage;
3161
+ const extraChildCount = isOpenRouterApiKeyMissingMessage || isOpenRouterRequestFailedMessage ? 2 : 1;
2748
3162
  return [{
2749
3163
  childCount: 1,
2750
3164
  className: mergeClassNames(Message, roleClassName),
2751
3165
  type: Div
2752
3166
  }, {
2753
- childCount: 1,
3167
+ childCount: extraChildCount,
2754
3168
  className: ChatMessageContent,
2755
3169
  type: Div
2756
3170
  }, {
2757
3171
  childCount: 1,
2758
3172
  className: Markdown,
2759
3173
  type: P
2760
- }, text(message.text)];
3174
+ }, text(message.text), ...(isOpenRouterApiKeyMissingMessage ? getMissingOpenRouterApiKeyDom(openRouterApiKeyInput) : []), ...(isOpenRouterRequestFailedMessage ? getOpenRouterRequestFailedDom() : [])];
2761
3175
  };
2762
3176
 
2763
- const getMessagesDom = messages => {
3177
+ const getEmptyMessagesDom = () => {
3178
+ return [{
3179
+ childCount: 1,
3180
+ className: ChatWelcomeMessage,
3181
+ type: Div
3182
+ }, text(startConversation())];
3183
+ };
3184
+ const getMessagesDom = (messages, openRouterApiKeyInput) => {
2764
3185
  if (messages.length === 0) {
2765
- return [{
2766
- childCount: 1,
2767
- className: ChatWelcomeMessage,
2768
- type: Div
2769
- }, text(startConversation)];
3186
+ return getEmptyMessagesDom();
2770
3187
  }
2771
3188
  return [{
2772
3189
  childCount: messages.length,
2773
3190
  className: 'ChatMessages',
2774
3191
  type: Div
2775
- }, ...messages.flatMap(getChatMessageDom)];
3192
+ }, ...messages.flatMap(message => getChatMessageDom(message, openRouterApiKeyInput))];
2776
3193
  };
2777
3194
 
2778
- const getChatModeDetailVirtualDom = (sessions, selectedSessionId, composerValue, models, selectedModelId, usageOverviewEnabled, tokensUsed, tokensMax) => {
3195
+ const getChatModeDetailVirtualDom = (sessions, selectedSessionId, composerValue, openRouterApiKeyInput, models, selectedModelId, usageOverviewEnabled, tokensUsed, tokensMax) => {
2779
3196
  const selectedSession = sessions.find(session => session.id === selectedSessionId);
2780
- const selectedSessionTitle = selectedSession?.title || chatTitle;
3197
+ const selectedSessionTitle = selectedSession?.title || chatTitle();
2781
3198
  const messages = selectedSession ? selectedSession.messages : [];
2782
3199
  return [{
2783
3200
  childCount: 3,
2784
3201
  className: mergeClassNames(Viewlet, Chat),
2785
3202
  type: Div
2786
- }, ...getChatHeaderDomDetailMode(selectedSessionTitle), ...getMessagesDom(messages), ...getChatSendAreaDom(composerValue, models, selectedModelId, usageOverviewEnabled, tokensUsed, tokensMax)];
3203
+ }, ...getChatHeaderDomDetailMode(selectedSessionTitle), ...getMessagesDom(messages, openRouterApiKeyInput), ...getChatSendAreaDom(composerValue, models, selectedModelId, usageOverviewEnabled, tokensUsed, tokensMax)];
2787
3204
  };
2788
3205
 
2789
3206
  const getChatHeaderListModeDom = () => {
@@ -2795,7 +3212,7 @@ const getChatHeaderListModeDom = () => {
2795
3212
  childCount: 1,
2796
3213
  className: Label,
2797
3214
  type: Span
2798
- }, text(chats), ...getChatHeaderActionsDom()];
3215
+ }, text(chats()), ...getChatHeaderActionsDom()];
2799
3216
  };
2800
3217
 
2801
3218
  const getEmptyChatSessionsDom = () => {
@@ -2807,7 +3224,7 @@ const getEmptyChatSessionsDom = () => {
2807
3224
  childCount: 1,
2808
3225
  className: Label,
2809
3226
  type: Div
2810
- }, text(clickToOpenNewChat)];
3227
+ }, text(clickToOpenNewChat())];
2811
3228
  };
2812
3229
 
2813
3230
  const getSessionDom = session => {
@@ -2819,7 +3236,7 @@ const getSessionDom = session => {
2819
3236
  }, {
2820
3237
  childCount: 1,
2821
3238
  className: ChatListItemLabel,
2822
- name: `session:${session.id}`,
3239
+ name: getSessionInputName(session.id),
2823
3240
  onContextMenu: HandleListContextMenu,
2824
3241
  tabIndex: 0,
2825
3242
  type: Div
@@ -2831,11 +3248,11 @@ const getSessionDom = session => {
2831
3248
  childCount: 1,
2832
3249
  className: IconButton,
2833
3250
  'data-id': session.id,
2834
- name: 'SessionDelete',
3251
+ name: SessionDelete,
2835
3252
  onClick: HandleClickDelete,
2836
3253
  role: Button$2,
2837
3254
  tabIndex: 0,
2838
- title: deleteChatSession$1,
3255
+ title: deleteChatSession$1(),
2839
3256
  type: Button$1
2840
3257
  }, text('🗑')];
2841
3258
  };
@@ -2864,13 +3281,13 @@ const getChatModeUnsupportedVirtualDom = () => {
2864
3281
  return [{
2865
3282
  childCount: 1,
2866
3283
  type: Div
2867
- }, text('Unknown view mode')];
3284
+ }, text(unknownViewMode())];
2868
3285
  };
2869
3286
 
2870
- const getChatVirtualDom = (sessions, selectedSessionId, composerValue, viewMode, models, selectedModelId, usageOverviewEnabled, tokensUsed, tokensMax) => {
3287
+ const getChatVirtualDom = (sessions, selectedSessionId, composerValue, openRouterApiKeyInput, viewMode, models, selectedModelId, usageOverviewEnabled, tokensUsed, tokensMax) => {
2871
3288
  switch (viewMode) {
2872
3289
  case 'detail':
2873
- return getChatModeDetailVirtualDom(sessions, selectedSessionId, composerValue, models, selectedModelId, usageOverviewEnabled, tokensUsed, tokensMax);
3290
+ return getChatModeDetailVirtualDom(sessions, selectedSessionId, composerValue, openRouterApiKeyInput, models, selectedModelId, usageOverviewEnabled, tokensUsed, tokensMax);
2874
3291
  case 'list':
2875
3292
  return getChatModeListVirtualDom(sessions, selectedSessionId, composerValue, models, selectedModelId, usageOverviewEnabled, tokensUsed, tokensMax);
2876
3293
  default:
@@ -2883,6 +3300,7 @@ const renderItems = (oldState, newState) => {
2883
3300
  composerValue,
2884
3301
  initial,
2885
3302
  models,
3303
+ openRouterApiKeyInput,
2886
3304
  selectedModelId,
2887
3305
  selectedSessionId,
2888
3306
  sessions,
@@ -2895,7 +3313,7 @@ const renderItems = (oldState, newState) => {
2895
3313
  if (initial) {
2896
3314
  return [SetDom2, uid, []];
2897
3315
  }
2898
- const dom = getChatVirtualDom(sessions, selectedSessionId, composerValue, viewMode, models, selectedModelId, usageOverviewEnabled, tokensUsed, tokensMax);
3316
+ const dom = getChatVirtualDom(sessions, selectedSessionId, composerValue, openRouterApiKeyInput, viewMode, models, selectedModelId, usageOverviewEnabled, tokensUsed, tokensMax);
2899
3317
  return [SetDom2, uid, dom];
2900
3318
  };
2901
3319
 
@@ -2910,7 +3328,7 @@ const renderValue = (oldState, newState) => {
2910
3328
  const {
2911
3329
  composerValue
2912
3330
  } = newState;
2913
- return [SetValueByName, newState.uid, 'composer', composerValue];
3331
+ return [SetValueByName, newState.uid, Composer, composerValue];
2914
3332
  };
2915
3333
 
2916
3334
  const getRenderer = diffType => {
@@ -2948,7 +3366,7 @@ const render2 = (uid, diffResult) => {
2948
3366
  const {
2949
3367
  newState,
2950
3368
  oldState
2951
- } = get(uid);
3369
+ } = get$1(uid);
2952
3370
  set(uid, newState, newState);
2953
3371
  const commands = applyRender(oldState, newState, diffResult);
2954
3372
  return commands;
@@ -2982,7 +3400,7 @@ const renderEventListeners = () => {
2982
3400
  params: ['handleClickList', ClientX, ClientY]
2983
3401
  }, {
2984
3402
  name: HandleInput,
2985
- params: ['handleInput', TargetValue]
3403
+ params: ['handleInput', TargetName, TargetValue]
2986
3404
  }, {
2987
3405
  name: HandleModelChange,
2988
3406
  params: ['handleModelChange', TargetValue]
@@ -3007,6 +3425,7 @@ const reset = async state => {
3007
3425
  return {
3008
3426
  ...state,
3009
3427
  composerValue: '',
3428
+ openRouterApiKey: '',
3010
3429
  selectedSessionId: '',
3011
3430
  sessions: [],
3012
3431
  viewMode: 'list'
@@ -3050,15 +3469,15 @@ const saveState = state => {
3050
3469
  const dummySessions = [{
3051
3470
  id: 'session-a',
3052
3471
  messages: [],
3053
- title: dummyChatA
3472
+ title: dummyChatA()
3054
3473
  }, {
3055
3474
  id: 'session-b',
3056
3475
  messages: [],
3057
- title: dummyChatB
3476
+ title: dummyChatB()
3058
3477
  }, {
3059
3478
  id: 'session-c',
3060
3479
  messages: [],
3061
- title: dummyChatC
3480
+ title: dummyChatC()
3062
3481
  }];
3063
3482
  const setChatList = state => {
3064
3483
  return {
@@ -3076,6 +3495,7 @@ const commandMap = {
3076
3495
  'Chat.enterNewLine': wrapCommand(handleNewline),
3077
3496
  'Chat.getCommandIds': getCommandIds,
3078
3497
  'Chat.getKeyBindings': getKeyBindings,
3498
+ 'Chat.getSelectedSessionId': wrapGetter(getSelectedSessionId),
3079
3499
  'Chat.handleChatListContextMenu': handleChatListContextMenu,
3080
3500
  'Chat.handleClick': wrapCommand(handleClick),
3081
3501
  'Chat.handleClickBack': wrapCommand(handleClickBack),
@@ -3100,6 +3520,7 @@ const commandMap = {
3100
3520
  'Chat.resize': wrapCommand(resize),
3101
3521
  'Chat.saveState': wrapGetter(saveState),
3102
3522
  'Chat.setChatList': wrapCommand(setChatList),
3523
+ 'Chat.setOpenRouterApiKey': wrapCommand(setOpenRouterApiKey),
3103
3524
  'Chat.terminate': terminate
3104
3525
  };
3105
3526