@lvce-editor/chat-view 1.14.0 → 1.16.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,6 +1049,9 @@ 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
  };
@@ -1175,9 +1179,45 @@ const terminate = () => {
1175
1179
  globalThis.close();
1176
1180
  };
1177
1181
 
1182
+ const measureTextBlockHeight = async (text, fontFamily, fontSize, lineHeight, width) => {
1183
+ return invoke('MeasureTextBlockHeight.measureTextBlockHeight', text, fontSize, fontFamily, lineHeight, width);
1184
+ };
1185
+
1186
+ const getComposerWidth = width => {
1187
+ return Math.max(1, width - 32);
1188
+ };
1189
+ const getMinComposerHeight = lineHeight => {
1190
+ return lineHeight + 8;
1191
+ };
1192
+ const estimateComposerHeight = (value, lineHeight) => {
1193
+ const lineCount = value.split('\n').length;
1194
+ return lineCount * lineHeight + 8;
1195
+ };
1196
+ const getComposerHeight = async (state, value, width = state.width) => {
1197
+ const {
1198
+ composerFontFamily,
1199
+ composerFontSize,
1200
+ composerLineHeight
1201
+ } = state;
1202
+ const minimumHeight = getMinComposerHeight(composerLineHeight);
1203
+ const content = value || ' ';
1204
+ const composerWidth = getComposerWidth(width);
1205
+ try {
1206
+ const measuredHeight = await measureTextBlockHeight(content, composerFontFamily, composerFontSize, composerLineHeight, composerWidth);
1207
+ const height = Math.ceil(measuredHeight) + 8;
1208
+ return Math.max(minimumHeight, height);
1209
+ } catch {
1210
+ return Math.max(minimumHeight, estimateComposerHeight(value, composerLineHeight));
1211
+ }
1212
+ };
1213
+ const getMinComposerHeightForState = state => {
1214
+ return getMinComposerHeight(state.composerLineHeight);
1215
+ };
1216
+
1178
1217
  const clearInput = async state => {
1179
1218
  return {
1180
1219
  ...state,
1220
+ composerHeight: getMinComposerHeightForState(state),
1181
1221
  composerValue: ''
1182
1222
  };
1183
1223
  };
@@ -1224,15 +1264,24 @@ const composePlaceholder = () => {
1224
1264
  const openRouterApiKeyPlaceholder = () => {
1225
1265
  return i18nString('Enter OpenRouter API key');
1226
1266
  };
1267
+ const openApiApiKeyPlaceholder = () => {
1268
+ return i18nString('Enter OpenAI API key');
1269
+ };
1227
1270
  const sendMessage = () => {
1228
1271
  return i18nString('Send message');
1229
1272
  };
1230
1273
  const save = () => {
1231
1274
  return i18nString('Save');
1232
1275
  };
1276
+ const saving = () => {
1277
+ return i18nString('Saving...');
1278
+ };
1233
1279
  const getOpenRouterApiKey = () => {
1234
1280
  return i18nString('Get API Key');
1235
1281
  };
1282
+ const getOpenApiApiKey = () => {
1283
+ return i18nString('Get API Key');
1284
+ };
1236
1285
  const deleteChatSession$1 = () => {
1237
1286
  return i18nString('Delete chat session');
1238
1287
  };
@@ -1260,6 +1309,18 @@ const getDefaultModels = () => {
1260
1309
  id: defaultModelId,
1261
1310
  name: 'test',
1262
1311
  provider: 'test'
1312
+ }, {
1313
+ id: 'openapi/gpt-4o-mini',
1314
+ name: 'GPT-4o Mini',
1315
+ provider: 'openApi'
1316
+ }, {
1317
+ id: 'openapi/gpt-4o',
1318
+ name: 'GPT-4o',
1319
+ provider: 'openApi'
1320
+ }, {
1321
+ id: 'openapi/gpt-4.1-mini',
1322
+ name: 'GPT-4.1 Mini',
1323
+ provider: 'openApi'
1263
1324
  }, {
1264
1325
  id: 'codex-5.3',
1265
1326
  name: 'Codex 5.3',
@@ -1313,8 +1374,14 @@ const getDefaultModels = () => {
1313
1374
  const createDefaultState = () => {
1314
1375
  const defaultSessionId = 'session-1';
1315
1376
  const defaultModelId = 'test';
1377
+ const composerFontSize = 13;
1378
+ const composerLineHeight = 20;
1316
1379
  return {
1317
1380
  assetDir: '',
1381
+ composerFontFamily: 'system-ui',
1382
+ composerFontSize,
1383
+ composerHeight: composerLineHeight + 8,
1384
+ composerLineHeight,
1318
1385
  composerValue: '',
1319
1386
  errorCount: 0,
1320
1387
  focus: 'composer',
@@ -1325,12 +1392,18 @@ const createDefaultState = () => {
1325
1392
  inputSource: 'script',
1326
1393
  lastSubmittedSessionId: '',
1327
1394
  listItemHeight: 40,
1395
+ mockApiCommandId: '',
1328
1396
  models: getDefaultModels(),
1329
1397
  nextMessageId: 1,
1398
+ openApiApiBaseUrl: 'https://api.openai.com/v1',
1399
+ openApiApiKey: '',
1400
+ openApiApiKeyInput: '',
1401
+ openApiApiKeysSettingsUrl: 'https://platform.openai.com/api-keys',
1330
1402
  openRouterApiBaseUrl: 'https://openrouter.ai/api/v1',
1331
1403
  openRouterApiKey: '',
1332
1404
  openRouterApiKeyInput: '',
1333
1405
  openRouterApiKeysSettingsUrl: 'https://openrouter.ai/settings/keys',
1406
+ openRouterApiKeyState: 'idle',
1334
1407
  platform: 0,
1335
1408
  renamingSessionId: '',
1336
1409
  selectedModelId: defaultModelId,
@@ -1344,6 +1417,7 @@ const createDefaultState = () => {
1344
1417
  tokensUsed: 0,
1345
1418
  uid: 0,
1346
1419
  usageOverviewEnabled: false,
1420
+ useMockApi: false,
1347
1421
  viewMode: 'list',
1348
1422
  warningCount: 0,
1349
1423
  width: 0,
@@ -2084,14 +2158,23 @@ const deleteSession = async (state, id) => {
2084
2158
  };
2085
2159
  };
2086
2160
 
2161
+ const handleClickOpenApiApiKeySettings = async state => {
2162
+ await openExternal(state.openApiApiKeysSettingsUrl);
2163
+ return state;
2164
+ };
2165
+
2087
2166
  const handleClickOpenRouterApiKeySettings = async state => {
2088
2167
  await openExternal(state.openRouterApiKeysSettingsUrl);
2089
2168
  return state;
2090
2169
  };
2091
2170
 
2171
+ const openApiApiKeyRequiredMessage = 'OpenAI API key is not configured. Enter your OpenAI API key below and click Save.';
2172
+ const openApiRequestFailedMessage = 'OpenAI request failed.';
2092
2173
  const openRouterApiKeyRequiredMessage = 'OpenRouter API key is not configured. Enter your OpenRouter API key below and click Save.';
2093
2174
  const openRouterRequestFailedMessage = 'OpenRouter request failed. Possible reasons:';
2175
+ const openRouterTooManyRequestsMessage = 'OpenRouter rate limit reached (429). Please try again soon. Helpful tips:';
2094
2176
  const openRouterRequestFailureReasons = ['ContentSecurityPolicyViolation: Check DevTools for details.', 'OpenRouter server offline: Check DevTools for details.', 'Check your internet connection.'];
2177
+ 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.'];
2095
2178
 
2096
2179
  const delay = async ms => {
2097
2180
  await new Promise(resolve => setTimeout(resolve, ms));
@@ -2102,10 +2185,133 @@ const getMockAiResponse = async userMessage => {
2102
2185
  return `Mock AI response: I received "${userMessage}".`;
2103
2186
  };
2104
2187
 
2105
- const defaultOpenRouterApiBaseUrl = 'https://openrouter.ai/api/v1';
2106
- const getOpenRouterApiEndpoint = openRouterApiBaseUrl => {
2107
- const trimmedBaseUrl = (openRouterApiBaseUrl || defaultOpenRouterApiBaseUrl).replace(/\/+$/, '');
2108
- return `${trimmedBaseUrl}/chat/completions`;
2188
+ const activateByEvent = (event, assetDir, platform) => {
2189
+ // @ts-ignore
2190
+ return activateByEvent$1(event, assetDir, platform);
2191
+ };
2192
+
2193
+ const executeProvider = async ({
2194
+ assetDir,
2195
+ event,
2196
+ method,
2197
+ noProviderFoundMessage,
2198
+ params,
2199
+ platform
2200
+ }) => {
2201
+ await activateByEvent(event, assetDir, platform);
2202
+ // @ts-ignore
2203
+ const result = invoke$1(method, ...params);
2204
+ return result;
2205
+ };
2206
+
2207
+ const CommandExecute = 'ExtensionHostCommand.executeCommand';
2208
+
2209
+ const normalizeLimitInfo = value => {
2210
+ if (!value || typeof value !== 'object') {
2211
+ return undefined;
2212
+ }
2213
+ const limitRemaining = Reflect.get(value, 'limitRemaining');
2214
+ const limitReset = Reflect.get(value, 'limitReset');
2215
+ const retryAfter = Reflect.get(value, 'retryAfter');
2216
+ const usage = Reflect.get(value, 'usage');
2217
+ const usageDaily = Reflect.get(value, 'usageDaily');
2218
+ const normalized = {
2219
+ limitRemaining: typeof limitRemaining === 'number' || limitRemaining === null ? limitRemaining : undefined,
2220
+ limitReset: typeof limitReset === 'string' || limitReset === null ? limitReset : undefined,
2221
+ retryAfter: typeof retryAfter === 'string' || retryAfter === null ? retryAfter : undefined,
2222
+ usage: typeof usage === 'number' ? usage : undefined,
2223
+ usageDaily: typeof usageDaily === 'number' ? usageDaily : undefined
2224
+ };
2225
+ const hasDetails = normalized.limitRemaining !== undefined || normalized.limitReset !== undefined || normalized.retryAfter !== undefined || normalized.usage !== undefined || normalized.usageDaily !== undefined;
2226
+ return hasDetails ? normalized : undefined;
2227
+ };
2228
+
2229
+ const normalizeMockResult = value => {
2230
+ if (typeof value === 'string') {
2231
+ return {
2232
+ text: value,
2233
+ type: 'success'
2234
+ };
2235
+ }
2236
+ if (!value || typeof value !== 'object') {
2237
+ return {
2238
+ details: 'request-failed',
2239
+ type: 'error'
2240
+ };
2241
+ }
2242
+ const type = Reflect.get(value, 'type');
2243
+ if (type === 'success') {
2244
+ const text = Reflect.get(value, 'text');
2245
+ if (typeof text === 'string') {
2246
+ return {
2247
+ text,
2248
+ type: 'success'
2249
+ };
2250
+ }
2251
+ return {
2252
+ details: 'request-failed',
2253
+ type: 'error'
2254
+ };
2255
+ }
2256
+ if (type === 'error') {
2257
+ const details = Reflect.get(value, 'details');
2258
+ if (details === 'request-failed' || details === 'too-many-requests' || details === 'http-error') {
2259
+ const rawMessage = Reflect.get(value, 'rawMessage');
2260
+ const statusCode = Reflect.get(value, 'statusCode');
2261
+ return {
2262
+ details,
2263
+ limitInfo: normalizeLimitInfo(Reflect.get(value, 'limitInfo')),
2264
+ rawMessage: typeof rawMessage === 'string' ? rawMessage : undefined,
2265
+ statusCode: typeof statusCode === 'number' ? statusCode : undefined,
2266
+ type: 'error'
2267
+ };
2268
+ }
2269
+ }
2270
+ const text = Reflect.get(value, 'text');
2271
+ if (typeof text === 'string') {
2272
+ return {
2273
+ text,
2274
+ type: 'success'
2275
+ };
2276
+ }
2277
+ return {
2278
+ details: 'request-failed',
2279
+ type: 'error'
2280
+ };
2281
+ };
2282
+
2283
+ const getMockOpenRouterAssistantText = async (messages, modelId, openRouterApiBaseUrl, openRouterApiKey, mockApiCommandId, assetDir, platform) => {
2284
+ if (!mockApiCommandId) {
2285
+ return {
2286
+ details: 'request-failed',
2287
+ type: 'error'
2288
+ };
2289
+ }
2290
+ try {
2291
+ const result = await executeProvider({
2292
+ assetDir,
2293
+ event: `onCommand:${mockApiCommandId}`,
2294
+ method: CommandExecute,
2295
+ noProviderFoundMessage: 'No mock api command found',
2296
+ params: [mockApiCommandId, {
2297
+ messages,
2298
+ modelId,
2299
+ openRouterApiBaseUrl,
2300
+ openRouterApiKey
2301
+ }],
2302
+ platform
2303
+ });
2304
+ return normalizeMockResult(result);
2305
+ } catch {
2306
+ return {
2307
+ details: 'request-failed',
2308
+ type: 'error'
2309
+ };
2310
+ }
2311
+ };
2312
+
2313
+ const getOpenApiApiEndpoint = openApiApiBaseUrl => {
2314
+ return `${openApiApiBaseUrl}/chat/completions`;
2109
2315
  };
2110
2316
 
2111
2317
  const getTextContent = content => {
@@ -2129,15 +2335,255 @@ const getTextContent = content => {
2129
2335
  return textParts.join('\n');
2130
2336
  };
2131
2337
 
2132
- const getOpenRouterAssistantText = async (userText, modelId, openRouterApiKey, openRouterApiBaseUrl) => {
2338
+ const getOpenApiErrorDetails = async response => {
2339
+ let parsed;
2340
+ try {
2341
+ parsed = await response.json();
2342
+ } catch {
2343
+ return {};
2344
+ }
2345
+ if (!parsed || typeof parsed !== 'object') {
2346
+ return {};
2347
+ }
2348
+ const error = Reflect.get(parsed, 'error');
2349
+ if (!error || typeof error !== 'object') {
2350
+ return {};
2351
+ }
2352
+ const errorCode = Reflect.get(error, 'code');
2353
+ const errorMessage = Reflect.get(error, 'message');
2354
+ const errorType = Reflect.get(error, 'type');
2355
+ return {
2356
+ errorCode: typeof errorCode === 'string' ? errorCode : undefined,
2357
+ errorMessage: typeof errorMessage === 'string' ? errorMessage : undefined,
2358
+ errorType: typeof errorType === 'string' ? errorType : undefined
2359
+ };
2360
+ };
2361
+ const getOpenApiAssistantText = async (messages, modelId, openApiApiKey, openApiApiBaseUrl) => {
2362
+ let response;
2363
+ try {
2364
+ response = await fetch(getOpenApiApiEndpoint(openApiApiBaseUrl), {
2365
+ body: JSON.stringify({
2366
+ messages: messages.map(message => ({
2367
+ content: message.text,
2368
+ role: message.role
2369
+ })),
2370
+ model: modelId
2371
+ }),
2372
+ headers: {
2373
+ Authorization: `Bearer ${openApiApiKey}`,
2374
+ 'Content-Type': 'application/json'
2375
+ },
2376
+ method: 'POST'
2377
+ });
2378
+ } catch {
2379
+ return {
2380
+ details: 'request-failed',
2381
+ type: 'error'
2382
+ };
2383
+ }
2384
+ if (!response.ok) {
2385
+ const {
2386
+ errorCode,
2387
+ errorMessage,
2388
+ errorType
2389
+ } = await getOpenApiErrorDetails(response);
2390
+ return {
2391
+ details: 'http-error',
2392
+ errorCode,
2393
+ errorMessage,
2394
+ errorType,
2395
+ statusCode: response.status,
2396
+ type: 'error'
2397
+ };
2398
+ }
2399
+ let parsed;
2400
+ try {
2401
+ parsed = await response.json();
2402
+ } catch {
2403
+ return {
2404
+ details: 'request-failed',
2405
+ type: 'error'
2406
+ };
2407
+ }
2408
+ if (!parsed || typeof parsed !== 'object') {
2409
+ return {
2410
+ text: '',
2411
+ type: 'success'
2412
+ };
2413
+ }
2414
+ const choices = Reflect.get(parsed, 'choices');
2415
+ if (!Array.isArray(choices)) {
2416
+ return {
2417
+ text: '',
2418
+ type: 'success'
2419
+ };
2420
+ }
2421
+ const firstChoice = choices[0];
2422
+ if (!firstChoice || typeof firstChoice !== 'object') {
2423
+ return {
2424
+ text: '',
2425
+ type: 'success'
2426
+ };
2427
+ }
2428
+ const message = Reflect.get(firstChoice, 'message');
2429
+ if (!message || typeof message !== 'object') {
2430
+ return {
2431
+ text: '',
2432
+ type: 'success'
2433
+ };
2434
+ }
2435
+ const content = Reflect.get(message, 'content');
2436
+ return {
2437
+ text: getTextContent(content),
2438
+ type: 'success'
2439
+ };
2440
+ };
2441
+
2442
+ const getOpenApiErrorMessage = errorResult => {
2443
+ switch (errorResult.details) {
2444
+ case 'http-error':
2445
+ {
2446
+ const errorMessage = errorResult.errorMessage?.trim();
2447
+ const hasErrorCode = typeof errorResult.errorCode === 'string' && errorResult.errorCode.length > 0;
2448
+ const hasErrorType = typeof errorResult.errorType === 'string' && errorResult.errorType.length > 0;
2449
+ if (errorResult.statusCode === 429) {
2450
+ let prefix = 'OpenAI rate limit exceeded (429)';
2451
+ if (hasErrorCode) {
2452
+ prefix = `OpenAI rate limit exceeded (429: ${errorResult.errorCode})`;
2453
+ }
2454
+ if (hasErrorType) {
2455
+ prefix += ` [${errorResult.errorType}]`;
2456
+ }
2457
+ prefix += '.';
2458
+ if (!errorMessage) {
2459
+ return prefix;
2460
+ }
2461
+ return `${prefix} ${errorMessage}`;
2462
+ }
2463
+ if (typeof errorResult.statusCode === 'number') {
2464
+ let prefix = `OpenAI request failed (status ${errorResult.statusCode})`;
2465
+ if (hasErrorCode) {
2466
+ prefix += `: ${errorResult.errorCode}`;
2467
+ }
2468
+ if (hasErrorType) {
2469
+ prefix += ` [${errorResult.errorType}]`;
2470
+ }
2471
+ prefix += '.';
2472
+ if (!errorMessage) {
2473
+ return prefix;
2474
+ }
2475
+ return `${prefix} ${errorMessage}`;
2476
+ }
2477
+ if (errorMessage) {
2478
+ return `OpenAI request failed. ${errorMessage}`;
2479
+ }
2480
+ return openApiRequestFailedMessage;
2481
+ }
2482
+ case 'request-failed':
2483
+ return openApiRequestFailedMessage;
2484
+ }
2485
+ };
2486
+
2487
+ const getOpenApiModelId = selectedModelId => {
2488
+ const openApiPrefix = 'openapi/';
2489
+ const openAiPrefix = 'openai/';
2490
+ const normalizedModelId = selectedModelId.toLowerCase();
2491
+ if (normalizedModelId.startsWith(openApiPrefix)) {
2492
+ return selectedModelId.slice(openApiPrefix.length);
2493
+ }
2494
+ if (normalizedModelId.startsWith(openAiPrefix)) {
2495
+ return selectedModelId.slice(openAiPrefix.length);
2496
+ }
2497
+ return selectedModelId;
2498
+ };
2499
+
2500
+ const getOpenRouterApiEndpoint = openRouterApiBaseUrl => {
2501
+ const trimmedBaseUrl = openRouterApiBaseUrl.replace(/\/+$/, '');
2502
+ return `${trimmedBaseUrl}/chat/completions`;
2503
+ };
2504
+
2505
+ const getOpenRouterKeyEndpoint = openRouterApiBaseUrl => {
2506
+ const trimmedBaseUrl = openRouterApiBaseUrl.replace(/\/+$/, '');
2507
+ return `${trimmedBaseUrl}/auth/key`;
2508
+ };
2509
+
2510
+ const getOpenRouterRaw429Message = async response => {
2511
+ let parsed;
2512
+ try {
2513
+ parsed = await response.json();
2514
+ } catch {
2515
+ return undefined;
2516
+ }
2517
+ if (!parsed || typeof parsed !== 'object') {
2518
+ return undefined;
2519
+ }
2520
+ const error = Reflect.get(parsed, 'error');
2521
+ if (!error || typeof error !== 'object') {
2522
+ return undefined;
2523
+ }
2524
+ const metadata = Reflect.get(error, 'metadata');
2525
+ if (!metadata || typeof metadata !== 'object') {
2526
+ return undefined;
2527
+ }
2528
+ const raw = Reflect.get(metadata, 'raw');
2529
+ if (typeof raw !== 'string' || !raw) {
2530
+ return undefined;
2531
+ }
2532
+ return raw;
2533
+ };
2534
+ const getOpenRouterLimitInfo = async (openRouterApiKey, openRouterApiBaseUrl) => {
2535
+ let response;
2536
+ try {
2537
+ response = await fetch(getOpenRouterKeyEndpoint(openRouterApiBaseUrl), {
2538
+ headers: {
2539
+ Authorization: `Bearer ${openRouterApiKey}`
2540
+ },
2541
+ method: 'GET'
2542
+ });
2543
+ } catch {
2544
+ return undefined;
2545
+ }
2546
+ if (!response.ok) {
2547
+ return undefined;
2548
+ }
2549
+ let parsed;
2550
+ try {
2551
+ parsed = await response.json();
2552
+ } catch {
2553
+ return undefined;
2554
+ }
2555
+ if (!parsed || typeof parsed !== 'object') {
2556
+ return undefined;
2557
+ }
2558
+ const data = Reflect.get(parsed, 'data');
2559
+ if (!data || typeof data !== 'object') {
2560
+ return undefined;
2561
+ }
2562
+ const limitRemaining = Reflect.get(data, 'limit_remaining');
2563
+ const limitReset = Reflect.get(data, 'limit_reset');
2564
+ const usage = Reflect.get(data, 'usage');
2565
+ const usageDaily = Reflect.get(data, 'usage_daily');
2566
+ const normalizedLimitInfo = {
2567
+ limitRemaining: typeof limitRemaining === 'number' || limitRemaining === null ? limitRemaining : undefined,
2568
+ limitReset: typeof limitReset === 'string' || limitReset === null ? limitReset : undefined,
2569
+ usage: typeof usage === 'number' ? usage : undefined,
2570
+ usageDaily: typeof usageDaily === 'number' ? usageDaily : undefined
2571
+ };
2572
+ const hasLimitInfo = normalizedLimitInfo.limitRemaining !== undefined || normalizedLimitInfo.limitReset !== undefined || normalizedLimitInfo.usage !== undefined || normalizedLimitInfo.usageDaily !== undefined;
2573
+ if (!hasLimitInfo) {
2574
+ return undefined;
2575
+ }
2576
+ return normalizedLimitInfo;
2577
+ };
2578
+ const getOpenRouterAssistantText = async (messages, modelId, openRouterApiKey, openRouterApiBaseUrl) => {
2133
2579
  let response;
2134
2580
  try {
2135
2581
  response = await fetch(getOpenRouterApiEndpoint(openRouterApiBaseUrl), {
2136
2582
  body: JSON.stringify({
2137
- messages: [{
2138
- content: userText,
2139
- role: 'user'
2140
- }],
2583
+ messages: messages.map(message => ({
2584
+ content: message.text,
2585
+ role: message.role
2586
+ })),
2141
2587
  model: modelId
2142
2588
  }),
2143
2589
  headers: {
@@ -2147,29 +2593,117 @@ const getOpenRouterAssistantText = async (userText, modelId, openRouterApiKey, o
2147
2593
  method: 'POST'
2148
2594
  });
2149
2595
  } catch {
2150
- throw new Error(openRouterRequestFailedMessage);
2596
+ return {
2597
+ details: 'request-failed',
2598
+ type: 'error'
2599
+ };
2151
2600
  }
2152
2601
  if (!response.ok) {
2153
- throw new Error(`Failed to get OpenRouter response: ${response.status}`);
2602
+ if (response.status === 429) {
2603
+ const retryAfter = response.headers?.get?.('retry-after') ?? null;
2604
+ const rawMessage = await getOpenRouterRaw429Message(response);
2605
+ const limitInfo = await getOpenRouterLimitInfo(openRouterApiKey, openRouterApiBaseUrl);
2606
+ return {
2607
+ details: 'too-many-requests',
2608
+ limitInfo: limitInfo || retryAfter ? {
2609
+ ...limitInfo,
2610
+ retryAfter
2611
+ } : undefined,
2612
+ rawMessage,
2613
+ statusCode: 429,
2614
+ type: 'error'
2615
+ };
2616
+ }
2617
+ return {
2618
+ details: 'http-error',
2619
+ statusCode: response.status,
2620
+ type: 'error'
2621
+ };
2622
+ }
2623
+ let parsed;
2624
+ try {
2625
+ parsed = await response.json();
2626
+ } catch {
2627
+ return {
2628
+ details: 'request-failed',
2629
+ type: 'error'
2630
+ };
2154
2631
  }
2155
- const parsed = await response.json();
2156
2632
  if (!parsed || typeof parsed !== 'object') {
2157
- return '';
2633
+ return {
2634
+ text: '',
2635
+ type: 'success'
2636
+ };
2158
2637
  }
2159
2638
  const choices = Reflect.get(parsed, 'choices');
2160
2639
  if (!Array.isArray(choices)) {
2161
- return '';
2640
+ return {
2641
+ text: '',
2642
+ type: 'success'
2643
+ };
2162
2644
  }
2163
2645
  const firstChoice = choices[0];
2164
2646
  if (!firstChoice || typeof firstChoice !== 'object') {
2165
- return '';
2647
+ return {
2648
+ text: '',
2649
+ type: 'success'
2650
+ };
2166
2651
  }
2167
2652
  const message = Reflect.get(firstChoice, 'message');
2168
2653
  if (!message || typeof message !== 'object') {
2169
- return '';
2654
+ return {
2655
+ text: '',
2656
+ type: 'success'
2657
+ };
2170
2658
  }
2171
2659
  const content = Reflect.get(message, 'content');
2172
- return getTextContent(content);
2660
+ return {
2661
+ text: getTextContent(content),
2662
+ type: 'success'
2663
+ };
2664
+ };
2665
+
2666
+ const getOpenRouterTooManyRequestsMessage = errorResult => {
2667
+ const details = [];
2668
+ if (errorResult.rawMessage) {
2669
+ details.push(errorResult.rawMessage);
2670
+ }
2671
+ const {
2672
+ limitInfo
2673
+ } = errorResult;
2674
+ if (limitInfo) {
2675
+ if (limitInfo.retryAfter) {
2676
+ details.push(`Retry after: ${limitInfo.retryAfter}.`);
2677
+ }
2678
+ if (limitInfo.limitReset) {
2679
+ details.push(`Limit resets: ${limitInfo.limitReset}.`);
2680
+ }
2681
+ if (limitInfo.limitRemaining === null) {
2682
+ details.push('Credits remaining: unlimited.');
2683
+ } else if (typeof limitInfo.limitRemaining === 'number') {
2684
+ details.push(`Credits remaining: ${limitInfo.limitRemaining}.`);
2685
+ }
2686
+ if (typeof limitInfo.usageDaily === 'number') {
2687
+ details.push(`Credits used today (UTC): ${limitInfo.usageDaily}.`);
2688
+ }
2689
+ if (typeof limitInfo.usage === 'number') {
2690
+ details.push(`Credits used (all time): ${limitInfo.usage}.`);
2691
+ }
2692
+ }
2693
+ if (details.length === 0) {
2694
+ return openRouterTooManyRequestsMessage;
2695
+ }
2696
+ return `${openRouterTooManyRequestsMessage} ${details.join(' ')}`;
2697
+ };
2698
+
2699
+ const getOpenRouterErrorMessage = errorResult => {
2700
+ switch (errorResult.details) {
2701
+ case 'http-error':
2702
+ case 'request-failed':
2703
+ return openRouterRequestFailedMessage;
2704
+ case 'too-many-requests':
2705
+ return getOpenRouterTooManyRequestsMessage(errorResult);
2706
+ }
2173
2707
  };
2174
2708
 
2175
2709
  /* eslint-disable @cspell/spellchecker */
@@ -2181,6 +2715,16 @@ const getOpenRouterModelId = selectedModelId => {
2181
2715
  return selectedModelId;
2182
2716
  };
2183
2717
 
2718
+ const isOpenApiModel = (selectedModelId, models) => {
2719
+ const selectedModel = models.find(model => model.id === selectedModelId);
2720
+ const normalizedProvider = selectedModel?.provider?.toLowerCase();
2721
+ if (normalizedProvider === 'openapi' || normalizedProvider === 'openai' || normalizedProvider === 'open-ai') {
2722
+ return true;
2723
+ }
2724
+ const normalizedModelId = selectedModelId.toLowerCase();
2725
+ return normalizedModelId.startsWith('openapi/') || normalizedModelId.startsWith('openai/');
2726
+ };
2727
+
2184
2728
  /* eslint-disable @cspell/spellchecker */
2185
2729
 
2186
2730
  const isOpenRouterModel = (selectedModelId, models) => {
@@ -2192,25 +2736,65 @@ const isOpenRouterModel = (selectedModelId, models) => {
2192
2736
  return selectedModelId.toLowerCase().startsWith('openrouter/');
2193
2737
  };
2194
2738
 
2195
- const getAiResponse = async (userText, nextMessageId, selectedModelId, models, openRouterApiKey, openRouterApiBaseUrl) => {
2739
+ const getAiResponse = async ({
2740
+ assetDir,
2741
+ messages,
2742
+ mockApiCommandId,
2743
+ models,
2744
+ nextMessageId,
2745
+ openApiApiBaseUrl,
2746
+ openApiApiKey,
2747
+ openRouterApiBaseUrl,
2748
+ openRouterApiKey,
2749
+ platform,
2750
+ selectedModelId,
2751
+ useMockApi,
2752
+ userText
2753
+ }) => {
2196
2754
  let text = '';
2755
+ const usesOpenApiModel = isOpenApiModel(selectedModelId, models);
2197
2756
  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
- }
2757
+ if (usesOpenApiModel) {
2758
+ if (openApiApiKey) {
2759
+ const result = await getOpenApiAssistantText(messages, getOpenApiModelId(selectedModelId), openApiApiKey, openApiApiBaseUrl);
2760
+ if (result.type === 'success') {
2761
+ const {
2762
+ text: assistantText
2763
+ } = result;
2764
+ text = assistantText;
2765
+ } else {
2766
+ text = getOpenApiErrorMessage(result);
2767
+ }
2768
+ } else {
2769
+ text = openApiApiKeyRequiredMessage;
2770
+ }
2771
+ } else if (usesOpenRouterModel) {
2772
+ const modelId = getOpenRouterModelId(selectedModelId);
2773
+ if (useMockApi) {
2774
+ const result = await getMockOpenRouterAssistantText(messages, modelId, openRouterApiBaseUrl, openRouterApiKey, mockApiCommandId, assetDir, platform);
2775
+ if (result.type === 'success') {
2776
+ const {
2777
+ text: assistantText
2778
+ } = result;
2779
+ text = assistantText;
2780
+ } else {
2781
+ text = getOpenRouterErrorMessage(result);
2782
+ }
2783
+ } else if (openRouterApiKey) {
2784
+ const result = await getOpenRouterAssistantText(messages, modelId, openRouterApiKey, openRouterApiBaseUrl);
2785
+ if (result.type === 'success') {
2786
+ const {
2787
+ text: assistantText
2788
+ } = result;
2789
+ text = assistantText;
2790
+ } else {
2791
+ text = getOpenRouterErrorMessage(result);
2208
2792
  }
2209
2793
  } else {
2210
2794
  text = openRouterApiKeyRequiredMessage;
2211
2795
  }
2212
2796
  }
2213
- if (!text && !usesOpenRouterModel) {
2797
+ if (!text && !usesOpenApiModel && !usesOpenRouterModel) {
2214
2798
  text = await getMockAiResponse(userText);
2215
2799
  }
2216
2800
  const assistantTime = new Date().toLocaleTimeString([], {
@@ -2232,6 +2816,75 @@ const update = async settings => {
2232
2816
  await invoke('Preferences.update', settings);
2233
2817
  };
2234
2818
 
2819
+ const setOpenApiApiKey = async (state, openApiApiKey, persist = true) => {
2820
+ if (persist) {
2821
+ await update({
2822
+ 'secrets.openApiKey': openApiApiKey
2823
+ });
2824
+ }
2825
+ return {
2826
+ ...state,
2827
+ openApiApiKey,
2828
+ openApiApiKeyInput: openApiApiKey
2829
+ };
2830
+ };
2831
+
2832
+ const handleClickSaveOpenApiApiKey = async state => {
2833
+ const {
2834
+ openApiApiKeyInput
2835
+ } = state;
2836
+ const openApiApiKey = openApiApiKeyInput.trim();
2837
+ if (!openApiApiKey) {
2838
+ return state;
2839
+ }
2840
+ const updatedState = await setOpenApiApiKey(state, openApiApiKey);
2841
+ const session = updatedState.sessions.find(item => item.id === updatedState.selectedSessionId);
2842
+ if (!session) {
2843
+ return updatedState;
2844
+ }
2845
+ const lastMessage = session.messages.at(-1);
2846
+ const shouldRetryOpenApi = lastMessage?.role === 'assistant' && lastMessage.text === openApiApiKeyRequiredMessage;
2847
+ if (!shouldRetryOpenApi) {
2848
+ return updatedState;
2849
+ }
2850
+ const previousUserMessage = session.messages.toReversed().find(item => item.role === 'user');
2851
+ if (!previousUserMessage) {
2852
+ return updatedState;
2853
+ }
2854
+ const retryMessages = session.messages.slice(0, -1);
2855
+ const assistantMessage = await getAiResponse({
2856
+ assetDir: updatedState.assetDir,
2857
+ messages: retryMessages,
2858
+ mockApiCommandId: updatedState.mockApiCommandId,
2859
+ models: updatedState.models,
2860
+ nextMessageId: updatedState.nextMessageId,
2861
+ openApiApiBaseUrl: updatedState.openApiApiBaseUrl,
2862
+ openApiApiKey: updatedState.openApiApiKey,
2863
+ openRouterApiBaseUrl: updatedState.openRouterApiBaseUrl,
2864
+ openRouterApiKey: updatedState.openRouterApiKey,
2865
+ platform: updatedState.platform,
2866
+ selectedModelId: updatedState.selectedModelId,
2867
+ useMockApi: updatedState.useMockApi,
2868
+ userText: previousUserMessage.text
2869
+ });
2870
+ const updatedSession = {
2871
+ ...session,
2872
+ messages: [...session.messages.slice(0, -1), assistantMessage]
2873
+ };
2874
+ await saveChatSession(updatedSession);
2875
+ const updatedSessions = updatedState.sessions.map(item => {
2876
+ if (item.id !== updatedState.selectedSessionId) {
2877
+ return item;
2878
+ }
2879
+ return updatedSession;
2880
+ });
2881
+ return {
2882
+ ...updatedState,
2883
+ nextMessageId: updatedState.nextMessageId + 1,
2884
+ sessions: updatedSessions
2885
+ };
2886
+ };
2887
+
2235
2888
  const setOpenRouterApiKey = async (state, openRouterApiKey, persist = true) => {
2236
2889
  if (persist) {
2237
2890
  await update({
@@ -2253,7 +2906,18 @@ const handleClickSaveOpenRouterApiKey = async state => {
2253
2906
  if (!openRouterApiKey) {
2254
2907
  return state;
2255
2908
  }
2256
- const updatedState = await setOpenRouterApiKey(state, openRouterApiKey);
2909
+ const optimisticState = {
2910
+ ...state,
2911
+ openRouterApiKeyState: 'saving'
2912
+ };
2913
+ set(state.uid, state, optimisticState);
2914
+ // @ts-ignore
2915
+ await invoke('Chat.rerender');
2916
+ const persistedState = await setOpenRouterApiKey(optimisticState, openRouterApiKey);
2917
+ const updatedState = {
2918
+ ...persistedState,
2919
+ openRouterApiKeyState: 'idle'
2920
+ };
2257
2921
  const session = updatedState.sessions.find(item => item.id === updatedState.selectedSessionId);
2258
2922
  if (!session) {
2259
2923
  return updatedState;
@@ -2267,7 +2931,22 @@ const handleClickSaveOpenRouterApiKey = async state => {
2267
2931
  if (!previousUserMessage) {
2268
2932
  return updatedState;
2269
2933
  }
2270
- const assistantMessage = await getAiResponse(previousUserMessage.text, updatedState.nextMessageId, updatedState.selectedModelId, updatedState.models, openRouterApiKey, updatedState.openRouterApiBaseUrl);
2934
+ const retryMessages = session.messages.slice(0, -1);
2935
+ const assistantMessage = await getAiResponse({
2936
+ assetDir: updatedState.assetDir,
2937
+ messages: retryMessages,
2938
+ mockApiCommandId: updatedState.mockApiCommandId,
2939
+ models: updatedState.models,
2940
+ nextMessageId: updatedState.nextMessageId,
2941
+ openApiApiBaseUrl: updatedState.openApiApiBaseUrl,
2942
+ openApiApiKey: updatedState.openApiApiKey,
2943
+ openRouterApiBaseUrl: updatedState.openRouterApiBaseUrl,
2944
+ openRouterApiKey,
2945
+ platform: updatedState.platform,
2946
+ selectedModelId: updatedState.selectedModelId,
2947
+ useMockApi: updatedState.useMockApi,
2948
+ userText: previousUserMessage.text
2949
+ });
2271
2950
  const updatedSession = {
2272
2951
  ...session,
2273
2952
  messages: [...session.messages.slice(0, -1), assistantMessage]
@@ -2282,6 +2961,7 @@ const handleClickSaveOpenRouterApiKey = async state => {
2282
2961
  return {
2283
2962
  ...updatedState,
2284
2963
  nextMessageId: updatedState.nextMessageId + 1,
2964
+ openRouterApiKeyState: 'idle',
2285
2965
  sessions: updatedSessions
2286
2966
  };
2287
2967
  };
@@ -2296,14 +2976,20 @@ const focusInput = state => {
2296
2976
 
2297
2977
  const handleSubmit = async state => {
2298
2978
  const {
2979
+ assetDir,
2299
2980
  composerValue,
2981
+ mockApiCommandId,
2300
2982
  models,
2301
2983
  nextMessageId,
2984
+ openApiApiBaseUrl,
2985
+ openApiApiKey,
2302
2986
  openRouterApiBaseUrl,
2303
2987
  openRouterApiKey,
2988
+ platform,
2304
2989
  selectedModelId,
2305
2990
  selectedSessionId,
2306
2991
  sessions,
2992
+ useMockApi,
2307
2993
  viewMode
2308
2994
  } = state;
2309
2995
  const userText = composerValue.trim();
@@ -2343,6 +3029,7 @@ const handleSubmit = async state => {
2343
3029
  await saveChatSession(newSession);
2344
3030
  optimisticState = focusInput({
2345
3031
  ...state,
3032
+ composerHeight: getMinComposerHeightForState(state),
2346
3033
  composerValue: '',
2347
3034
  inputSource: 'script',
2348
3035
  lastSubmittedSessionId: newSessionId,
@@ -2367,6 +3054,7 @@ const handleSubmit = async state => {
2367
3054
  }
2368
3055
  optimisticState = focusInput({
2369
3056
  ...state,
3057
+ composerHeight: getMinComposerHeightForState(state),
2370
3058
  composerValue: '',
2371
3059
  inputSource: 'script',
2372
3060
  lastSubmittedSessionId: selectedSessionId,
@@ -2377,7 +3065,23 @@ const handleSubmit = async state => {
2377
3065
  set(state.uid, state, optimisticState);
2378
3066
  // @ts-ignore
2379
3067
  await invoke('Chat.rerender');
2380
- const assistantMessage = await getAiResponse(userText, optimisticState.nextMessageId, selectedModelId, models, openRouterApiKey, openRouterApiBaseUrl);
3068
+ const selectedOptimisticSession = optimisticState.sessions.find(session => session.id === optimisticState.selectedSessionId);
3069
+ const messages = selectedOptimisticSession?.messages ?? [];
3070
+ const assistantMessage = await getAiResponse({
3071
+ assetDir,
3072
+ messages,
3073
+ mockApiCommandId,
3074
+ models,
3075
+ nextMessageId: optimisticState.nextMessageId,
3076
+ openApiApiBaseUrl,
3077
+ openApiApiKey,
3078
+ openRouterApiBaseUrl,
3079
+ openRouterApiKey,
3080
+ platform,
3081
+ selectedModelId,
3082
+ useMockApi,
3083
+ userText
3084
+ });
2381
3085
  const updatedSessions = optimisticState.sessions.map(session => {
2382
3086
  if (session.id !== optimisticState.selectedSessionId) {
2383
3087
  return session;
@@ -2438,6 +3142,10 @@ const getRenameIdFromInputName = name => {
2438
3142
  return name.slice(RenamePrefix.length);
2439
3143
  };
2440
3144
 
3145
+ const OpenApiApiKeyInput = 'open-api-api-key';
3146
+ const SaveOpenApiApiKey = 'save-openapi-api-key';
3147
+ const OpenOpenApiApiKeySettings = 'open-openapi-api-key-settings';
3148
+
2441
3149
  /* eslint-disable @cspell/spellchecker */
2442
3150
  const OpenRouterApiKeyInput = 'open-router-api-key';
2443
3151
  const SaveOpenRouterApiKey = 'save-openrouter-api-key';
@@ -2524,9 +3232,15 @@ const handleClick = async (state, name, id = '') => {
2524
3232
  if (name === SaveOpenRouterApiKey) {
2525
3233
  return handleClickSaveOpenRouterApiKey(state);
2526
3234
  }
3235
+ if (name === SaveOpenApiApiKey) {
3236
+ return handleClickSaveOpenApiApiKey(state);
3237
+ }
2527
3238
  if (name === OpenOpenRouterApiKeySettings) {
2528
3239
  return handleClickOpenRouterApiKeySettings(state);
2529
3240
  }
3241
+ if (name === OpenOpenApiApiKeySettings) {
3242
+ return handleClickOpenApiApiKeySettings(state);
3243
+ }
2530
3244
  return state;
2531
3245
  };
2532
3246
 
@@ -2557,6 +3271,12 @@ const handleClickSettings = async () => {
2557
3271
  };
2558
3272
 
2559
3273
  const handleInput = async (state, name, value, inputSource = 'user') => {
3274
+ if (name === OpenApiApiKeyInput) {
3275
+ return {
3276
+ ...state,
3277
+ openApiApiKeyInput: value
3278
+ };
3279
+ }
2560
3280
  if (name === OpenRouterApiKeyInput) {
2561
3281
  return {
2562
3282
  ...state,
@@ -2566,8 +3286,10 @@ const handleInput = async (state, name, value, inputSource = 'user') => {
2566
3286
  if (name !== Composer) {
2567
3287
  return state;
2568
3288
  }
3289
+ const composerHeight = await getComposerHeight(state, value);
2569
3290
  return {
2570
3291
  ...state,
3292
+ composerHeight,
2571
3293
  composerValue: value,
2572
3294
  inputSource
2573
3295
  };
@@ -2632,6 +3354,7 @@ const submitRename = async state => {
2632
3354
  }
2633
3355
  return {
2634
3356
  ...state,
3357
+ composerHeight: getMinComposerHeightForState(state),
2635
3358
  composerValue: '',
2636
3359
  inputSource: 'script',
2637
3360
  renamingSessionId: '',
@@ -2776,6 +3499,23 @@ const loadSelectedSessionMessages = async (sessions, selectedSessionId) => {
2776
3499
  const loadContent = async (state, savedState) => {
2777
3500
  const savedSelectedModelId = getSavedSelectedModelId(savedState);
2778
3501
  const savedViewMode = getSavedViewMode(savedState);
3502
+ let openApiApiKey = '';
3503
+ try {
3504
+ const savedOpenApiKey = await get('secrets.openApiKey');
3505
+ if (typeof savedOpenApiKey === 'string' && savedOpenApiKey) {
3506
+ openApiApiKey = savedOpenApiKey;
3507
+ } else {
3508
+ const legacySavedOpenApiApiKey = await get('secrets.openApiApiKey');
3509
+ if (typeof legacySavedOpenApiApiKey === 'string' && legacySavedOpenApiApiKey) {
3510
+ openApiApiKey = legacySavedOpenApiApiKey;
3511
+ } else {
3512
+ const legacySavedOpenAiApiKey = await get('secrets.openAiApiKey');
3513
+ openApiApiKey = typeof legacySavedOpenAiApiKey === 'string' ? legacySavedOpenAiApiKey : '';
3514
+ }
3515
+ }
3516
+ } catch {
3517
+ openApiApiKey = '';
3518
+ }
2779
3519
  let openRouterApiKey = '';
2780
3520
  try {
2781
3521
  const savedOpenRouterApiKey = await get('secrets.openRouterApiKey');
@@ -2808,6 +3548,8 @@ const loadContent = async (state, savedState) => {
2808
3548
  return {
2809
3549
  ...state,
2810
3550
  initial: false,
3551
+ openApiApiKey,
3552
+ openApiApiKeyInput: openApiApiKey,
2811
3553
  openRouterApiKey,
2812
3554
  openRouterApiKeyInput: openRouterApiKey,
2813
3555
  selectedModelId,
@@ -2906,6 +3648,8 @@ const ChatListItemLabel = 'ChatListItemLabel';
2906
3648
  const Markdown = 'Markdown';
2907
3649
  const Message = 'Message';
2908
3650
  const ChatMessageContent = 'ChatMessageContent';
3651
+ const ChatOrderedList = 'ChatOrderedList';
3652
+ const ChatOrderedListItem = 'ChatOrderedListItem';
2909
3653
  const MessageUser = 'MessageUser';
2910
3654
  const MessageAssistant = 'MessageAssistant';
2911
3655
  const MultilineInputBox = 'MultilineInputBox';
@@ -2935,8 +3679,12 @@ const getModelLabel = model => {
2935
3679
  if (model.provider === 'openRouter') {
2936
3680
  return `${model.name} (OpenRouter)`;
2937
3681
  }
3682
+ if (model.provider === 'openApi' || model.provider === 'openAI' || model.provider === 'openai') {
3683
+ return `${model.name} (OpenAI)`;
3684
+ }
2938
3685
  return model.name;
2939
3686
  };
3687
+
2940
3688
  const getModelOptionDOm = (model, selectedModelId) => {
2941
3689
  return [{
2942
3690
  childCount: 1,
@@ -3004,7 +3752,7 @@ const getUsageOverviewDom = (tokensUsed, tokensMax) => {
3004
3752
  }, text(usageLabel)];
3005
3753
  };
3006
3754
 
3007
- const getChatSendAreaDom = (composerValue, models, selectedModelId, usageOverviewEnabled, tokensUsed, tokensMax) => {
3755
+ const getChatSendAreaDom = (composerValue, models, selectedModelId, usageOverviewEnabled, tokensUsed, tokensMax, composerHeight = 28, composerFontSize = 13, composerFontFamily = 'system-ui', composerLineHeight = 20) => {
3008
3756
  const isSendDisabled = composerValue.trim() === '';
3009
3757
  const modelOptions = models.flatMap(model => getModelOptionDOm(model, selectedModelId));
3010
3758
  return [{
@@ -3022,7 +3770,7 @@ const getChatSendAreaDom = (composerValue, models, selectedModelId, usageOvervie
3022
3770
  onFocus: HandleFocus,
3023
3771
  onInput: HandleInput,
3024
3772
  placeholder: composePlaceholder(),
3025
- rows: 4,
3773
+ style: `height:${composerHeight}px;font-size:${composerFontSize}px;font-family:${composerFontFamily};line-height:${composerLineHeight}px;`,
3026
3774
  type: TextArea,
3027
3775
  value: composerValue
3028
3776
  }, {
@@ -3111,18 +3859,27 @@ const getChatHeaderDomDetailMode = selectedSessionTitle => {
3111
3859
  }, text(selectedSessionTitle), ...getChatHeaderActionsDom()];
3112
3860
  };
3113
3861
 
3114
- const getMissingOpenRouterApiKeyDom = openRouterApiKeyInput => {
3862
+ const getMissingApiKeyDom = ({
3863
+ getApiKeyText,
3864
+ inputName,
3865
+ inputValue,
3866
+ openSettingsButtonName,
3867
+ placeholder,
3868
+ saveButtonDisabled = false,
3869
+ saveButtonName,
3870
+ saveButtonText = save()
3871
+ }) => {
3115
3872
  return [{
3116
3873
  childCount: 2,
3117
3874
  type: Div
3118
3875
  }, {
3119
3876
  childCount: 0,
3120
3877
  className: InputBox,
3121
- name: OpenRouterApiKeyInput,
3878
+ name: inputName,
3122
3879
  onInput: HandleInput,
3123
- placeholder: openRouterApiKeyPlaceholder(),
3880
+ placeholder,
3124
3881
  type: Input,
3125
- value: openRouterApiKeyInput
3882
+ value: inputValue
3126
3883
  }, {
3127
3884
  childCount: 2,
3128
3885
  className: Actions,
@@ -3130,35 +3887,77 @@ const getMissingOpenRouterApiKeyDom = openRouterApiKeyInput => {
3130
3887
  }, {
3131
3888
  childCount: 1,
3132
3889
  className: mergeClassNames(Button, ButtonPrimary),
3133
- name: SaveOpenRouterApiKey,
3890
+ disabled: saveButtonDisabled,
3891
+ name: saveButtonName,
3134
3892
  onClick: HandleClick,
3135
3893
  type: Button$1
3136
- }, text(save()), {
3894
+ }, text(saveButtonText), {
3137
3895
  childCount: 1,
3138
3896
  className: mergeClassNames(Button, ButtonSecondary),
3139
- name: OpenOpenRouterApiKeySettings,
3897
+ name: openSettingsButtonName,
3140
3898
  onClick: HandleClick,
3141
3899
  type: Button$1
3142
- }, text(getOpenRouterApiKey())];
3900
+ }, text(getApiKeyText)];
3901
+ };
3902
+
3903
+ const getMissingOpenApiApiKeyDom = openApiApiKeyInput => {
3904
+ return getMissingApiKeyDom({
3905
+ getApiKeyText: getOpenApiApiKey(),
3906
+ inputName: OpenApiApiKeyInput,
3907
+ inputValue: openApiApiKeyInput,
3908
+ openSettingsButtonName: OpenOpenApiApiKeySettings,
3909
+ placeholder: openApiApiKeyPlaceholder(),
3910
+ saveButtonName: SaveOpenApiApiKey
3911
+ });
3912
+ };
3913
+
3914
+ const getMissingOpenRouterApiKeyDom = (openRouterApiKeyInput, openRouterApiKeyState = 'idle') => {
3915
+ const isSaving = openRouterApiKeyState === 'saving';
3916
+ return getMissingApiKeyDom({
3917
+ getApiKeyText: getOpenRouterApiKey(),
3918
+ inputName: OpenRouterApiKeyInput,
3919
+ inputValue: openRouterApiKeyInput,
3920
+ openSettingsButtonName: OpenOpenRouterApiKeySettings,
3921
+ placeholder: openRouterApiKeyPlaceholder(),
3922
+ saveButtonDisabled: isSaving,
3923
+ saveButtonName: SaveOpenRouterApiKey,
3924
+ saveButtonText: isSaving ? saving() : save()
3925
+ });
3143
3926
  };
3144
3927
 
3145
3928
  const getOpenRouterRequestFailedDom = () => {
3146
3929
  return [{
3147
3930
  childCount: openRouterRequestFailureReasons.length,
3148
- className: Markdown,
3931
+ className: ChatOrderedList,
3149
3932
  type: Ol
3150
3933
  }, ...openRouterRequestFailureReasons.flatMap(reason => {
3151
3934
  return [{
3152
3935
  childCount: 1,
3936
+ className: ChatOrderedListItem,
3937
+ type: Li
3938
+ }, text(reason)];
3939
+ })];
3940
+ };
3941
+ const getOpenRouterTooManyRequestsDom = () => {
3942
+ return [{
3943
+ childCount: openRouterTooManyRequestsReasons.length,
3944
+ className: ChatOrderedList,
3945
+ type: Ol
3946
+ }, ...openRouterTooManyRequestsReasons.flatMap(reason => {
3947
+ return [{
3948
+ childCount: 1,
3949
+ className: ChatOrderedListItem,
3153
3950
  type: Li
3154
3951
  }, text(reason)];
3155
3952
  })];
3156
3953
  };
3157
- const getChatMessageDom = (message, openRouterApiKeyInput) => {
3954
+ const getChatMessageDom = (message, openRouterApiKeyInput, openApiApiKeyInput = '', openRouterApiKeyState = 'idle') => {
3158
3955
  const roleClassName = message.role === 'user' ? MessageUser : MessageAssistant;
3956
+ const isOpenApiApiKeyMissingMessage = message.role === 'assistant' && message.text === openApiApiKeyRequiredMessage;
3159
3957
  const isOpenRouterApiKeyMissingMessage = message.role === 'assistant' && message.text === openRouterApiKeyRequiredMessage;
3160
3958
  const isOpenRouterRequestFailedMessage = message.role === 'assistant' && message.text === openRouterRequestFailedMessage;
3161
- const extraChildCount = isOpenRouterApiKeyMissingMessage || isOpenRouterRequestFailedMessage ? 2 : 1;
3959
+ const isOpenRouterTooManyRequestsMessage = message.role === 'assistant' && message.text.startsWith(openRouterTooManyRequestsMessage);
3960
+ const extraChildCount = isOpenApiApiKeyMissingMessage || isOpenRouterApiKeyMissingMessage || isOpenRouterRequestFailedMessage || isOpenRouterTooManyRequestsMessage ? 2 : 1;
3162
3961
  return [{
3163
3962
  childCount: 1,
3164
3963
  className: mergeClassNames(Message, roleClassName),
@@ -3171,7 +3970,7 @@ const getChatMessageDom = (message, openRouterApiKeyInput) => {
3171
3970
  childCount: 1,
3172
3971
  className: Markdown,
3173
3972
  type: P
3174
- }, text(message.text), ...(isOpenRouterApiKeyMissingMessage ? getMissingOpenRouterApiKeyDom(openRouterApiKeyInput) : []), ...(isOpenRouterRequestFailedMessage ? getOpenRouterRequestFailedDom() : [])];
3973
+ }, text(message.text), ...(isOpenApiApiKeyMissingMessage ? getMissingOpenApiApiKeyDom(openApiApiKeyInput) : []), ...(isOpenRouterApiKeyMissingMessage ? getMissingOpenRouterApiKeyDom(openRouterApiKeyInput, openRouterApiKeyState) : []), ...(isOpenRouterRequestFailedMessage ? getOpenRouterRequestFailedDom() : []), ...(isOpenRouterTooManyRequestsMessage ? getOpenRouterTooManyRequestsDom() : [])];
3175
3974
  };
3176
3975
 
3177
3976
  const getEmptyMessagesDom = () => {
@@ -3181,7 +3980,8 @@ const getEmptyMessagesDom = () => {
3181
3980
  type: Div
3182
3981
  }, text(startConversation())];
3183
3982
  };
3184
- const getMessagesDom = (messages, openRouterApiKeyInput) => {
3983
+
3984
+ const getMessagesDom = (messages, openRouterApiKeyInput, openApiApiKeyInput = '', openRouterApiKeyState = 'idle') => {
3185
3985
  if (messages.length === 0) {
3186
3986
  return getEmptyMessagesDom();
3187
3987
  }
@@ -3189,10 +3989,10 @@ const getMessagesDom = (messages, openRouterApiKeyInput) => {
3189
3989
  childCount: messages.length,
3190
3990
  className: 'ChatMessages',
3191
3991
  type: Div
3192
- }, ...messages.flatMap(message => getChatMessageDom(message, openRouterApiKeyInput))];
3992
+ }, ...messages.flatMap(message => getChatMessageDom(message, openRouterApiKeyInput, openApiApiKeyInput, openRouterApiKeyState))];
3193
3993
  };
3194
3994
 
3195
- const getChatModeDetailVirtualDom = (sessions, selectedSessionId, composerValue, openRouterApiKeyInput, models, selectedModelId, usageOverviewEnabled, tokensUsed, tokensMax) => {
3995
+ const getChatModeDetailVirtualDom = (sessions, selectedSessionId, composerValue, openRouterApiKeyInput, openApiApiKeyInput, models, selectedModelId, usageOverviewEnabled, tokensUsed, tokensMax, openRouterApiKeyState = 'idle', composerHeight = 28, composerFontSize = 13, composerFontFamily = 'system-ui', composerLineHeight = 20) => {
3196
3996
  const selectedSession = sessions.find(session => session.id === selectedSessionId);
3197
3997
  const selectedSessionTitle = selectedSession?.title || chatTitle();
3198
3998
  const messages = selectedSession ? selectedSession.messages : [];
@@ -3200,7 +4000,7 @@ const getChatModeDetailVirtualDom = (sessions, selectedSessionId, composerValue,
3200
4000
  childCount: 3,
3201
4001
  className: mergeClassNames(Viewlet, Chat),
3202
4002
  type: Div
3203
- }, ...getChatHeaderDomDetailMode(selectedSessionTitle), ...getMessagesDom(messages, openRouterApiKeyInput), ...getChatSendAreaDom(composerValue, models, selectedModelId, usageOverviewEnabled, tokensUsed, tokensMax)];
4003
+ }, ...getChatHeaderDomDetailMode(selectedSessionTitle), ...getMessagesDom(messages, openRouterApiKeyInput, openApiApiKeyInput, openRouterApiKeyState), ...getChatSendAreaDom(composerValue, models, selectedModelId, usageOverviewEnabled, tokensUsed, tokensMax, composerHeight, composerFontSize, composerFontFamily, composerLineHeight)];
3204
4004
  };
3205
4005
 
3206
4006
  const getChatHeaderListModeDom = () => {
@@ -3269,12 +4069,12 @@ const getChatListDom = (sessions, selectedSessionId) => {
3269
4069
  }, ...sessions.flatMap(getSessionDom)];
3270
4070
  };
3271
4071
 
3272
- const getChatModeListVirtualDom = (sessions, selectedSessionId, composerValue, models, selectedModelId, usageOverviewEnabled, tokensUsed, tokensMax) => {
4072
+ const getChatModeListVirtualDom = (sessions, selectedSessionId, composerValue, models, selectedModelId, usageOverviewEnabled, tokensUsed, tokensMax, composerHeight = 28, composerFontSize = 13, composerFontFamily = 'system-ui', composerLineHeight = 20) => {
3273
4073
  return [{
3274
4074
  childCount: 3,
3275
4075
  className: mergeClassNames(Viewlet, Chat),
3276
4076
  type: Div
3277
- }, ...getChatHeaderListModeDom(), ...getChatListDom(sessions), ...getChatSendAreaDom(composerValue, models, selectedModelId, usageOverviewEnabled, tokensUsed, tokensMax)];
4077
+ }, ...getChatHeaderListModeDom(), ...getChatListDom(sessions), ...getChatSendAreaDom(composerValue, models, selectedModelId, usageOverviewEnabled, tokensUsed, tokensMax, composerHeight, composerFontSize, composerFontFamily, composerLineHeight)];
3278
4078
  };
3279
4079
 
3280
4080
  const getChatModeUnsupportedVirtualDom = () => {
@@ -3284,12 +4084,12 @@ const getChatModeUnsupportedVirtualDom = () => {
3284
4084
  }, text(unknownViewMode())];
3285
4085
  };
3286
4086
 
3287
- const getChatVirtualDom = (sessions, selectedSessionId, composerValue, openRouterApiKeyInput, viewMode, models, selectedModelId, usageOverviewEnabled, tokensUsed, tokensMax) => {
4087
+ const getChatVirtualDom = (sessions, selectedSessionId, composerValue, openRouterApiKeyInput, viewMode, models, selectedModelId, usageOverviewEnabled, tokensUsed, tokensMax, openApiApiKeyInput = '', openRouterApiKeyState = 'idle', composerHeight = 28, composerFontSize = 13, composerFontFamily = 'system-ui', composerLineHeight = 20) => {
3288
4088
  switch (viewMode) {
3289
4089
  case 'detail':
3290
- return getChatModeDetailVirtualDom(sessions, selectedSessionId, composerValue, openRouterApiKeyInput, models, selectedModelId, usageOverviewEnabled, tokensUsed, tokensMax);
4090
+ return getChatModeDetailVirtualDom(sessions, selectedSessionId, composerValue, openRouterApiKeyInput, openApiApiKeyInput, models, selectedModelId, usageOverviewEnabled, tokensUsed, tokensMax, openRouterApiKeyState, composerHeight, composerFontSize, composerFontFamily, composerLineHeight);
3291
4091
  case 'list':
3292
- return getChatModeListVirtualDom(sessions, selectedSessionId, composerValue, models, selectedModelId, usageOverviewEnabled, tokensUsed, tokensMax);
4092
+ return getChatModeListVirtualDom(sessions, selectedSessionId, composerValue, models, selectedModelId, usageOverviewEnabled, tokensUsed, tokensMax, composerHeight, composerFontSize, composerFontFamily, composerLineHeight);
3293
4093
  default:
3294
4094
  return getChatModeUnsupportedVirtualDom();
3295
4095
  }
@@ -3297,10 +4097,16 @@ const getChatVirtualDom = (sessions, selectedSessionId, composerValue, openRoute
3297
4097
 
3298
4098
  const renderItems = (oldState, newState) => {
3299
4099
  const {
4100
+ composerFontFamily,
4101
+ composerFontSize,
4102
+ composerHeight,
4103
+ composerLineHeight,
3300
4104
  composerValue,
3301
4105
  initial,
3302
4106
  models,
4107
+ openApiApiKeyInput,
3303
4108
  openRouterApiKeyInput,
4109
+ openRouterApiKeyState,
3304
4110
  selectedModelId,
3305
4111
  selectedSessionId,
3306
4112
  sessions,
@@ -3313,7 +4119,7 @@ const renderItems = (oldState, newState) => {
3313
4119
  if (initial) {
3314
4120
  return [SetDom2, uid, []];
3315
4121
  }
3316
- const dom = getChatVirtualDom(sessions, selectedSessionId, composerValue, openRouterApiKeyInput, viewMode, models, selectedModelId, usageOverviewEnabled, tokensUsed, tokensMax);
4122
+ const dom = getChatVirtualDom(sessions, selectedSessionId, composerValue, openRouterApiKeyInput, viewMode, models, selectedModelId, usageOverviewEnabled, tokensUsed, tokensMax, openApiApiKeyInput, openRouterApiKeyState, composerHeight, composerFontSize, composerFontFamily, composerLineHeight);
3317
4123
  return [SetDom2, uid, dom];
3318
4124
  };
3319
4125
 
@@ -3424,6 +4230,7 @@ const reset = async state => {
3424
4230
  await clearChatSessions();
3425
4231
  return {
3426
4232
  ...state,
4233
+ composerHeight: getMinComposerHeightForState(state),
3427
4234
  composerValue: '',
3428
4235
  openRouterApiKey: '',
3429
4236
  selectedSessionId: '',
@@ -3488,6 +4295,21 @@ const setChatList = state => {
3488
4295
  };
3489
4296
  };
3490
4297
 
4298
+ const defaultMockApiCommandId = 'ChatE2e.mockApi';
4299
+ const useMockApi = (state, value, mockApiCommandId = defaultMockApiCommandId) => {
4300
+ if (!value) {
4301
+ return {
4302
+ ...state,
4303
+ useMockApi: false
4304
+ };
4305
+ }
4306
+ return {
4307
+ ...state,
4308
+ mockApiCommandId,
4309
+ useMockApi: true
4310
+ };
4311
+ };
4312
+
3491
4313
  const commandMap = {
3492
4314
  'Chat.clearInput': wrapCommand(clearInput),
3493
4315
  'Chat.create': create,
@@ -3521,7 +4343,8 @@ const commandMap = {
3521
4343
  'Chat.saveState': wrapGetter(saveState),
3522
4344
  'Chat.setChatList': wrapCommand(setChatList),
3523
4345
  'Chat.setOpenRouterApiKey': wrapCommand(setOpenRouterApiKey),
3524
- 'Chat.terminate': terminate
4346
+ 'Chat.terminate': terminate,
4347
+ 'Chat.useMockApi': wrapCommand(useMockApi)
3525
4348
  };
3526
4349
 
3527
4350
  const listen = async () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lvce-editor/chat-view",
3
- "version": "1.14.0",
3
+ "version": "1.16.0",
4
4
  "description": "Chat View Worker",
5
5
  "repository": {
6
6
  "type": "git",