@lvce-editor/chat-view 1.15.0 → 1.17.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.
@@ -1045,6 +1045,9 @@ const {
1045
1045
  invokeAndTransfer,
1046
1046
  set: set$1
1047
1047
  } = create$2(RendererWorker);
1048
+ const readFile = async uri => {
1049
+ return invoke('FileSystem.readFile', uri);
1050
+ };
1048
1051
  const sendMessagePortToExtensionHostWorker$1 = async (port, rpcId = 0) => {
1049
1052
  const command = 'HandleMessagePort.handleMessagePort2';
1050
1053
  await invokeAndTransfer('SendMessagePortToExtensionHostWorker.sendMessagePortToExtensionHostWorker', port, command, rpcId);
@@ -1179,9 +1182,45 @@ const terminate = () => {
1179
1182
  globalThis.close();
1180
1183
  };
1181
1184
 
1185
+ const measureTextBlockHeight = async (text, fontFamily, fontSize, lineHeight, width) => {
1186
+ return invoke('MeasureTextBlockHeight.measureTextBlockHeight', text, fontSize, fontFamily, lineHeight, width);
1187
+ };
1188
+
1189
+ const getComposerWidth = width => {
1190
+ return Math.max(1, width - 32);
1191
+ };
1192
+ const getMinComposerHeight = lineHeight => {
1193
+ return lineHeight + 8;
1194
+ };
1195
+ const estimateComposerHeight = (value, lineHeight) => {
1196
+ const lineCount = value.split('\n').length;
1197
+ return lineCount * lineHeight + 8;
1198
+ };
1199
+ const getComposerHeight = async (state, value, width = state.width) => {
1200
+ const {
1201
+ composerFontFamily,
1202
+ composerFontSize,
1203
+ composerLineHeight
1204
+ } = state;
1205
+ const minimumHeight = getMinComposerHeight(composerLineHeight);
1206
+ const content = value || ' ';
1207
+ const composerWidth = getComposerWidth(width);
1208
+ try {
1209
+ const measuredHeight = await measureTextBlockHeight(content, composerFontFamily, composerFontSize, composerLineHeight, composerWidth);
1210
+ const height = Math.ceil(measuredHeight) + 8;
1211
+ return Math.max(minimumHeight, height);
1212
+ } catch {
1213
+ return Math.max(minimumHeight, estimateComposerHeight(value, composerLineHeight));
1214
+ }
1215
+ };
1216
+ const getMinComposerHeightForState = state => {
1217
+ return getMinComposerHeight(state.composerLineHeight);
1218
+ };
1219
+
1182
1220
  const clearInput = async state => {
1183
1221
  return {
1184
1222
  ...state,
1223
+ composerHeight: getMinComposerHeightForState(state),
1185
1224
  composerValue: ''
1186
1225
  };
1187
1226
  };
@@ -1237,6 +1276,9 @@ const sendMessage = () => {
1237
1276
  const save = () => {
1238
1277
  return i18nString('Save');
1239
1278
  };
1279
+ const saving = () => {
1280
+ return i18nString('Saving...');
1281
+ };
1240
1282
  const getOpenRouterApiKey = () => {
1241
1283
  return i18nString('Get API Key');
1242
1284
  };
@@ -1270,6 +1312,22 @@ const getDefaultModels = () => {
1270
1312
  id: defaultModelId,
1271
1313
  name: 'test',
1272
1314
  provider: 'test'
1315
+ }, {
1316
+ id: 'openapi/gpt-5-mini',
1317
+ name: 'GPT-5 Mini',
1318
+ provider: 'openApi'
1319
+ }, {
1320
+ id: 'openapi/gpt-4o-mini',
1321
+ name: 'GPT-4o Mini',
1322
+ provider: 'openApi'
1323
+ }, {
1324
+ id: 'openapi/gpt-4o',
1325
+ name: 'GPT-4o',
1326
+ provider: 'openApi'
1327
+ }, {
1328
+ id: 'openapi/gpt-4.1-mini',
1329
+ name: 'GPT-4.1 Mini',
1330
+ provider: 'openApi'
1273
1331
  }, {
1274
1332
  id: 'codex-5.3',
1275
1333
  name: 'Codex 5.3',
@@ -1323,8 +1381,14 @@ const getDefaultModels = () => {
1323
1381
  const createDefaultState = () => {
1324
1382
  const defaultSessionId = 'session-1';
1325
1383
  const defaultModelId = 'test';
1384
+ const composerFontSize = 13;
1385
+ const composerLineHeight = 20;
1326
1386
  return {
1327
1387
  assetDir: '',
1388
+ composerFontFamily: 'system-ui',
1389
+ composerFontSize,
1390
+ composerHeight: composerLineHeight + 8,
1391
+ composerLineHeight,
1328
1392
  composerValue: '',
1329
1393
  errorCount: 0,
1330
1394
  focus: 'composer',
@@ -1346,6 +1410,7 @@ const createDefaultState = () => {
1346
1410
  openRouterApiKey: '',
1347
1411
  openRouterApiKeyInput: '',
1348
1412
  openRouterApiKeysSettingsUrl: 'https://openrouter.ai/settings/keys',
1413
+ openRouterApiKeyState: 'idle',
1349
1414
  platform: 0,
1350
1415
  renamingSessionId: '',
1351
1416
  selectedModelId: defaultModelId,
@@ -2118,6 +2183,15 @@ const openRouterTooManyRequestsMessage = 'OpenRouter rate limit reached (429). P
2118
2183
  const openRouterRequestFailureReasons = ['ContentSecurityPolicyViolation: Check DevTools for details.', 'OpenRouter server offline: Check DevTools for details.', 'Check your internet connection.'];
2119
2184
  const openRouterTooManyRequestsReasons = ['Wait a short time and retry your request.', 'Reduce request frequency to avoid rate limits.', 'Use a different model if this one is saturated.'];
2120
2185
 
2186
+ const delay = async ms => {
2187
+ await new Promise(resolve => setTimeout(resolve, ms));
2188
+ };
2189
+
2190
+ const getMockAiResponse = async userMessage => {
2191
+ await delay(800);
2192
+ return `Mock AI response: I received "${userMessage}".`;
2193
+ };
2194
+
2121
2195
  const activateByEvent = (event, assetDir, platform) => {
2122
2196
  // @ts-ignore
2123
2197
  return activateByEvent$1(event, assetDir, platform);
@@ -2138,161 +2212,547 @@ const executeProvider = async ({
2138
2212
  };
2139
2213
 
2140
2214
  const CommandExecute = 'ExtensionHostCommand.executeCommand';
2215
+ const FileSystemWriteFile = 'ExtensionHostFileSystem.writeFile';
2141
2216
 
2142
- const delay = async ms => {
2143
- await new Promise(resolve => setTimeout(resolve, ms));
2144
- };
2145
-
2146
- const getMockAiResponse = async userMessage => {
2147
- await delay(800);
2148
- return `Mock AI response: I received "${userMessage}".`;
2149
- };
2150
-
2151
- const getOpenApiApiEndpoint = openApiApiBaseUrl => {
2152
- return `${openApiApiBaseUrl}/chat/completions`;
2153
- };
2154
-
2155
- const getTextContent = content => {
2156
- if (typeof content === 'string') {
2157
- return content;
2158
- }
2159
- if (!Array.isArray(content)) {
2160
- return '';
2161
- }
2162
- const textParts = [];
2163
- for (const part of content) {
2164
- if (!part || typeof part !== 'object') {
2165
- continue;
2166
- }
2167
- const maybeType = Reflect.get(part, 'type');
2168
- const maybeText = Reflect.get(part, 'text');
2169
- if (maybeType === 'text' && typeof maybeText === 'string') {
2170
- textParts.push(maybeText);
2171
- }
2217
+ const normalizeLimitInfo = value => {
2218
+ if (!value || typeof value !== 'object') {
2219
+ return undefined;
2172
2220
  }
2173
- return textParts.join('\n');
2221
+ const limitRemaining = Reflect.get(value, 'limitRemaining');
2222
+ const limitReset = Reflect.get(value, 'limitReset');
2223
+ const retryAfter = Reflect.get(value, 'retryAfter');
2224
+ const usage = Reflect.get(value, 'usage');
2225
+ const usageDaily = Reflect.get(value, 'usageDaily');
2226
+ const normalized = {
2227
+ limitRemaining: typeof limitRemaining === 'number' || limitRemaining === null ? limitRemaining : undefined,
2228
+ limitReset: typeof limitReset === 'string' || limitReset === null ? limitReset : undefined,
2229
+ retryAfter: typeof retryAfter === 'string' || retryAfter === null ? retryAfter : undefined,
2230
+ usage: typeof usage === 'number' ? usage : undefined,
2231
+ usageDaily: typeof usageDaily === 'number' ? usageDaily : undefined
2232
+ };
2233
+ const hasDetails = normalized.limitRemaining !== undefined || normalized.limitReset !== undefined || normalized.retryAfter !== undefined || normalized.usage !== undefined || normalized.usageDaily !== undefined;
2234
+ return hasDetails ? normalized : undefined;
2174
2235
  };
2175
2236
 
2176
- const getOpenApiAssistantText = async (messages, modelId, openApiApiKey, openApiApiBaseUrl) => {
2177
- let response;
2178
- try {
2179
- response = await fetch(getOpenApiApiEndpoint(openApiApiBaseUrl), {
2180
- body: JSON.stringify({
2181
- messages: messages.map(message => ({
2182
- content: message.text,
2183
- role: message.role
2184
- })),
2185
- model: modelId
2186
- }),
2187
- headers: {
2188
- Authorization: `Bearer ${openApiApiKey}`,
2189
- 'Content-Type': 'application/json'
2190
- },
2191
- method: 'POST'
2192
- });
2193
- } catch {
2237
+ const normalizeMockResult = value => {
2238
+ if (typeof value === 'string') {
2194
2239
  return {
2195
- details: 'request-failed',
2196
- type: 'error'
2240
+ text: value,
2241
+ type: 'success'
2197
2242
  };
2198
2243
  }
2199
- if (!response.ok) {
2244
+ if (!value || typeof value !== 'object') {
2200
2245
  return {
2201
- details: 'http-error',
2202
- statusCode: response.status,
2246
+ details: 'request-failed',
2203
2247
  type: 'error'
2204
2248
  };
2205
2249
  }
2206
- let parsed;
2207
- try {
2208
- parsed = await response.json();
2209
- } catch {
2250
+ const type = Reflect.get(value, 'type');
2251
+ if (type === 'success') {
2252
+ const text = Reflect.get(value, 'text');
2253
+ if (typeof text === 'string') {
2254
+ return {
2255
+ text,
2256
+ type: 'success'
2257
+ };
2258
+ }
2210
2259
  return {
2211
2260
  details: 'request-failed',
2212
2261
  type: 'error'
2213
2262
  };
2214
2263
  }
2215
- if (!parsed || typeof parsed !== 'object') {
2216
- return {
2217
- text: '',
2218
- type: 'success'
2219
- };
2264
+ if (type === 'error') {
2265
+ const details = Reflect.get(value, 'details');
2266
+ if (details === 'request-failed' || details === 'too-many-requests' || details === 'http-error') {
2267
+ const rawMessage = Reflect.get(value, 'rawMessage');
2268
+ const statusCode = Reflect.get(value, 'statusCode');
2269
+ return {
2270
+ details,
2271
+ limitInfo: normalizeLimitInfo(Reflect.get(value, 'limitInfo')),
2272
+ rawMessage: typeof rawMessage === 'string' ? rawMessage : undefined,
2273
+ statusCode: typeof statusCode === 'number' ? statusCode : undefined,
2274
+ type: 'error'
2275
+ };
2276
+ }
2220
2277
  }
2221
- const choices = Reflect.get(parsed, 'choices');
2222
- if (!Array.isArray(choices)) {
2278
+ const text = Reflect.get(value, 'text');
2279
+ if (typeof text === 'string') {
2223
2280
  return {
2224
- text: '',
2281
+ text,
2225
2282
  type: 'success'
2226
2283
  };
2227
2284
  }
2228
- const firstChoice = choices[0];
2229
- if (!firstChoice || typeof firstChoice !== 'object') {
2285
+ return {
2286
+ details: 'request-failed',
2287
+ type: 'error'
2288
+ };
2289
+ };
2290
+
2291
+ const getMockOpenRouterAssistantText = async (messages, modelId, openRouterApiBaseUrl, openRouterApiKey, mockApiCommandId, assetDir, platform) => {
2292
+ if (!mockApiCommandId) {
2230
2293
  return {
2231
- text: '',
2232
- type: 'success'
2294
+ details: 'request-failed',
2295
+ type: 'error'
2233
2296
  };
2234
2297
  }
2235
- const message = Reflect.get(firstChoice, 'message');
2236
- if (!message || typeof message !== 'object') {
2298
+ try {
2299
+ const result = await executeProvider({
2300
+ assetDir,
2301
+ event: `onCommand:${mockApiCommandId}`,
2302
+ method: CommandExecute,
2303
+ noProviderFoundMessage: 'No mock api command found',
2304
+ params: [mockApiCommandId, {
2305
+ messages,
2306
+ modelId,
2307
+ openRouterApiBaseUrl,
2308
+ openRouterApiKey
2309
+ }],
2310
+ platform
2311
+ });
2312
+ return normalizeMockResult(result);
2313
+ } catch {
2237
2314
  return {
2238
- text: '',
2239
- type: 'success'
2315
+ details: 'request-failed',
2316
+ type: 'error'
2240
2317
  };
2241
2318
  }
2242
- const content = Reflect.get(message, 'content');
2243
- return {
2244
- text: getTextContent(content),
2245
- type: 'success'
2246
- };
2247
2319
  };
2248
2320
 
2249
- const getOpenApiModelId = selectedModelId => {
2250
- const openApiPrefix = 'openapi/';
2251
- const openAiPrefix = 'openai/';
2252
- const normalizedModelId = selectedModelId.toLowerCase();
2253
- if (normalizedModelId.startsWith(openApiPrefix)) {
2254
- return selectedModelId.slice(openApiPrefix.length);
2321
+ const OnFileSystem = 'onFileSystem';
2322
+
2323
+ const isPathTraversalAttempt = path => {
2324
+ if (!path) {
2325
+ return false;
2255
2326
  }
2256
- if (normalizedModelId.startsWith(openAiPrefix)) {
2257
- return selectedModelId.slice(openAiPrefix.length);
2327
+ if (path.startsWith('/') || path.startsWith('\\')) {
2328
+ return true;
2258
2329
  }
2259
- return selectedModelId;
2330
+ if (path.startsWith('file://')) {
2331
+ return true;
2332
+ }
2333
+ if (/^[a-zA-Z]:[\\/]/.test(path)) {
2334
+ return true;
2335
+ }
2336
+ const segments = path.split(/[\\/]/);
2337
+ return segments.includes('..');
2338
+ };
2339
+ const normalizeRelativePath = path => {
2340
+ const segments = path.split(/[\\/]/).filter(segment => segment && segment !== '.');
2341
+ if (segments.length === 0) {
2342
+ return '.';
2343
+ }
2344
+ return segments.join('/');
2345
+ };
2346
+ const parseToolArguments = rawArguments => {
2347
+ if (typeof rawArguments !== 'string') {
2348
+ return {};
2349
+ }
2350
+ try {
2351
+ const parsed = JSON.parse(rawArguments);
2352
+ if (!parsed || typeof parsed !== 'object') {
2353
+ return {};
2354
+ }
2355
+ return parsed;
2356
+ } catch {
2357
+ return {};
2358
+ }
2359
+ };
2360
+ const executeFileSystemCommand = async (method, params, options) => {
2361
+ return executeProvider({
2362
+ assetDir: options.assetDir,
2363
+ event: OnFileSystem,
2364
+ method,
2365
+ noProviderFoundMessage: 'No file system provider found',
2366
+ params,
2367
+ platform: options.platform
2368
+ });
2369
+ };
2370
+ const getBasicChatTools = () => {
2371
+ return [{
2372
+ function: {
2373
+ description: 'Read UTF-8 text content from a file inside the currently open workspace folder.',
2374
+ name: 'read_file',
2375
+ parameters: {
2376
+ additionalProperties: false,
2377
+ properties: {
2378
+ path: {
2379
+ description: 'Relative file path within the workspace (for example: src/index.ts).',
2380
+ type: 'string'
2381
+ }
2382
+ },
2383
+ required: ['path'],
2384
+ type: 'object'
2385
+ }
2386
+ },
2387
+ type: 'function'
2388
+ }, {
2389
+ function: {
2390
+ description: 'Write UTF-8 text content to a file inside the currently open workspace folder.',
2391
+ name: 'write_file',
2392
+ parameters: {
2393
+ additionalProperties: false,
2394
+ properties: {
2395
+ content: {
2396
+ description: 'New UTF-8 text content to write to the file.',
2397
+ type: 'string'
2398
+ },
2399
+ path: {
2400
+ description: 'Relative file path within the workspace (for example: src/index.ts).',
2401
+ type: 'string'
2402
+ }
2403
+ },
2404
+ required: ['path', 'content'],
2405
+ type: 'object'
2406
+ }
2407
+ },
2408
+ type: 'function'
2409
+ }, {
2410
+ function: {
2411
+ description: 'List direct children (files and folders) for a folder inside the currently open workspace folder.',
2412
+ name: 'list_files',
2413
+ parameters: {
2414
+ additionalProperties: false,
2415
+ properties: {
2416
+ path: {
2417
+ description: 'Relative folder path within the workspace. Use "." for the workspace root.',
2418
+ type: 'string'
2419
+ }
2420
+ },
2421
+ type: 'object'
2422
+ }
2423
+ },
2424
+ type: 'function'
2425
+ }];
2426
+ };
2427
+ const executeChatTool = async (name, rawArguments, options) => {
2428
+ const args = parseToolArguments(rawArguments);
2429
+ if (name === 'read_file') {
2430
+ const filePath = typeof args.path === 'string' ? args.path : '';
2431
+ if (!filePath || isPathTraversalAttempt(filePath)) {
2432
+ return JSON.stringify({
2433
+ error: 'Access denied: path must be relative and stay within the open workspace folder.'
2434
+ });
2435
+ }
2436
+ const normalizedPath = normalizeRelativePath(filePath);
2437
+ try {
2438
+ const content = await readFile(normalizedPath);
2439
+ return JSON.stringify({
2440
+ content,
2441
+ path: normalizedPath
2442
+ });
2443
+ } catch (error) {
2444
+ return JSON.stringify({
2445
+ error: String(error),
2446
+ path: normalizedPath
2447
+ });
2448
+ }
2449
+ }
2450
+ if (name === 'write_file') {
2451
+ const filePath = typeof args.path === 'string' ? args.path : '';
2452
+ const content = typeof args.content === 'string' ? args.content : '';
2453
+ if (!filePath || isPathTraversalAttempt(filePath)) {
2454
+ return JSON.stringify({
2455
+ error: 'Access denied: path must be relative and stay within the open workspace folder.'
2456
+ });
2457
+ }
2458
+ const normalizedPath = normalizeRelativePath(filePath);
2459
+ try {
2460
+ await executeFileSystemCommand(FileSystemWriteFile, ['file', normalizedPath, content], options);
2461
+ return JSON.stringify({
2462
+ ok: true,
2463
+ path: normalizedPath
2464
+ });
2465
+ } catch (error) {
2466
+ return JSON.stringify({
2467
+ error: String(error),
2468
+ path: normalizedPath
2469
+ });
2470
+ }
2471
+ }
2472
+ if (name === 'list_files') {
2473
+ const folderPath = typeof args.path === 'string' && args.path ? args.path : '.';
2474
+ if (isPathTraversalAttempt(folderPath)) {
2475
+ return JSON.stringify({
2476
+ error: 'Access denied: path must be relative and stay within the open workspace folder.'
2477
+ });
2478
+ }
2479
+ const normalizedPath = normalizeRelativePath(folderPath);
2480
+ try {
2481
+ const entries = await invoke('FileSystem.readDirWithFileTypes', normalizedPath);
2482
+ return JSON.stringify({
2483
+ entries,
2484
+ path: normalizedPath
2485
+ });
2486
+ } catch (error) {
2487
+ return JSON.stringify({
2488
+ error: String(error),
2489
+ path: normalizedPath
2490
+ });
2491
+ }
2492
+ }
2493
+ return JSON.stringify({
2494
+ error: `Unknown tool: ${name}`
2495
+ });
2260
2496
  };
2261
2497
 
2262
- const getOpenRouterApiEndpoint = openRouterApiBaseUrl => {
2263
- const trimmedBaseUrl = openRouterApiBaseUrl.replace(/\/+$/, '');
2264
- return `${trimmedBaseUrl}/chat/completions`;
2498
+ const getOpenApiApiEndpoint = openApiApiBaseUrl => {
2499
+ return `${openApiApiBaseUrl}/chat/completions`;
2265
2500
  };
2266
2501
 
2267
- const getOpenRouterKeyEndpoint = openRouterApiBaseUrl => {
2268
- const trimmedBaseUrl = openRouterApiBaseUrl.replace(/\/+$/, '');
2269
- return `${trimmedBaseUrl}/auth/key`;
2502
+ const getTextContent = content => {
2503
+ if (typeof content === 'string') {
2504
+ return content;
2505
+ }
2506
+ if (!Array.isArray(content)) {
2507
+ return '';
2508
+ }
2509
+ const textParts = [];
2510
+ for (const part of content) {
2511
+ if (!part || typeof part !== 'object') {
2512
+ continue;
2513
+ }
2514
+ const maybeType = Reflect.get(part, 'type');
2515
+ const maybeText = Reflect.get(part, 'text');
2516
+ if (maybeType === 'text' && typeof maybeText === 'string') {
2517
+ textParts.push(maybeText);
2518
+ }
2519
+ }
2520
+ return textParts.join('\n');
2270
2521
  };
2271
2522
 
2272
- const getOpenRouterRaw429Message = async response => {
2523
+ const getOpenApiErrorDetails = async response => {
2273
2524
  let parsed;
2274
2525
  try {
2275
2526
  parsed = await response.json();
2276
2527
  } catch {
2277
- return undefined;
2528
+ return {};
2278
2529
  }
2279
2530
  if (!parsed || typeof parsed !== 'object') {
2280
- return undefined;
2531
+ return {};
2281
2532
  }
2282
2533
  const error = Reflect.get(parsed, 'error');
2283
2534
  if (!error || typeof error !== 'object') {
2284
- return undefined;
2285
- }
2286
- const metadata = Reflect.get(error, 'metadata');
2287
- if (!metadata || typeof metadata !== 'object') {
2288
- return undefined;
2289
- }
2290
- const raw = Reflect.get(metadata, 'raw');
2291
- if (typeof raw !== 'string' || !raw) {
2292
- return undefined;
2535
+ return {};
2293
2536
  }
2294
- return raw;
2295
- };
2537
+ const errorCode = Reflect.get(error, 'code');
2538
+ const errorMessage = Reflect.get(error, 'message');
2539
+ const errorType = Reflect.get(error, 'type');
2540
+ return {
2541
+ errorCode: typeof errorCode === 'string' ? errorCode : undefined,
2542
+ errorMessage: typeof errorMessage === 'string' ? errorMessage : undefined,
2543
+ errorType: typeof errorType === 'string' ? errorType : undefined
2544
+ };
2545
+ };
2546
+ const getOpenApiAssistantText = async (messages, modelId, openApiApiKey, openApiApiBaseUrl, assetDir, platform) => {
2547
+ const completionMessages = messages.map(message => ({
2548
+ content: message.text,
2549
+ role: message.role
2550
+ }));
2551
+ const tools = getBasicChatTools();
2552
+ const maxToolIterations = 4;
2553
+ for (let i = 0; i <= maxToolIterations; i++) {
2554
+ let response;
2555
+ try {
2556
+ response = await fetch(getOpenApiApiEndpoint(openApiApiBaseUrl), {
2557
+ body: JSON.stringify({
2558
+ messages: completionMessages,
2559
+ model: modelId,
2560
+ tool_choice: 'auto',
2561
+ tools
2562
+ }),
2563
+ headers: {
2564
+ Authorization: `Bearer ${openApiApiKey}`,
2565
+ 'Content-Type': 'application/json'
2566
+ },
2567
+ method: 'POST'
2568
+ });
2569
+ } catch {
2570
+ return {
2571
+ details: 'request-failed',
2572
+ type: 'error'
2573
+ };
2574
+ }
2575
+ if (!response.ok) {
2576
+ const {
2577
+ errorCode,
2578
+ errorMessage,
2579
+ errorType
2580
+ } = await getOpenApiErrorDetails(response);
2581
+ return {
2582
+ details: 'http-error',
2583
+ errorCode,
2584
+ errorMessage,
2585
+ errorType,
2586
+ statusCode: response.status,
2587
+ type: 'error'
2588
+ };
2589
+ }
2590
+ let parsed;
2591
+ try {
2592
+ parsed = await response.json();
2593
+ } catch {
2594
+ return {
2595
+ details: 'request-failed',
2596
+ type: 'error'
2597
+ };
2598
+ }
2599
+ if (!parsed || typeof parsed !== 'object') {
2600
+ return {
2601
+ text: '',
2602
+ type: 'success'
2603
+ };
2604
+ }
2605
+ const choices = Reflect.get(parsed, 'choices');
2606
+ if (!Array.isArray(choices)) {
2607
+ return {
2608
+ text: '',
2609
+ type: 'success'
2610
+ };
2611
+ }
2612
+ const firstChoice = choices[0];
2613
+ if (!firstChoice || typeof firstChoice !== 'object') {
2614
+ return {
2615
+ text: '',
2616
+ type: 'success'
2617
+ };
2618
+ }
2619
+ const message = Reflect.get(firstChoice, 'message');
2620
+ if (!message || typeof message !== 'object') {
2621
+ return {
2622
+ text: '',
2623
+ type: 'success'
2624
+ };
2625
+ }
2626
+ const toolCalls = Reflect.get(message, 'tool_calls');
2627
+ if (Array.isArray(toolCalls) && toolCalls.length > 0) {
2628
+ completionMessages.push(message);
2629
+ for (const toolCall of toolCalls) {
2630
+ if (!toolCall || typeof toolCall !== 'object') {
2631
+ continue;
2632
+ }
2633
+ const id = Reflect.get(toolCall, 'id');
2634
+ const toolFunction = Reflect.get(toolCall, 'function');
2635
+ if (typeof id !== 'string' || !toolFunction || typeof toolFunction !== 'object') {
2636
+ continue;
2637
+ }
2638
+ const name = Reflect.get(toolFunction, 'name');
2639
+ const rawArguments = Reflect.get(toolFunction, 'arguments');
2640
+ const content = typeof name === 'string' ? await executeChatTool(name, rawArguments, {
2641
+ assetDir,
2642
+ platform
2643
+ }) : '{}';
2644
+ completionMessages.push({
2645
+ content,
2646
+ role: 'tool',
2647
+ tool_call_id: id
2648
+ });
2649
+ }
2650
+ continue;
2651
+ }
2652
+ const content = Reflect.get(message, 'content');
2653
+ return {
2654
+ text: getTextContent(content),
2655
+ type: 'success'
2656
+ };
2657
+ }
2658
+ return {
2659
+ details: 'request-failed',
2660
+ type: 'error'
2661
+ };
2662
+ };
2663
+
2664
+ const getOpenApiErrorMessage = errorResult => {
2665
+ switch (errorResult.details) {
2666
+ case 'http-error':
2667
+ {
2668
+ const errorMessage = errorResult.errorMessage?.trim();
2669
+ const hasErrorCode = typeof errorResult.errorCode === 'string' && errorResult.errorCode.length > 0;
2670
+ const hasErrorType = typeof errorResult.errorType === 'string' && errorResult.errorType.length > 0;
2671
+ if (errorResult.statusCode === 429) {
2672
+ let prefix = 'OpenAI rate limit exceeded (429)';
2673
+ if (hasErrorCode) {
2674
+ prefix = `OpenAI rate limit exceeded (429: ${errorResult.errorCode})`;
2675
+ }
2676
+ if (hasErrorType) {
2677
+ prefix += ` [${errorResult.errorType}]`;
2678
+ }
2679
+ prefix += '.';
2680
+ if (!errorMessage) {
2681
+ return prefix;
2682
+ }
2683
+ return `${prefix} ${errorMessage}`;
2684
+ }
2685
+ if (typeof errorResult.statusCode === 'number') {
2686
+ let prefix = `OpenAI request failed (status ${errorResult.statusCode})`;
2687
+ if (hasErrorCode) {
2688
+ prefix += `: ${errorResult.errorCode}`;
2689
+ }
2690
+ if (hasErrorType) {
2691
+ prefix += ` [${errorResult.errorType}]`;
2692
+ }
2693
+ prefix += '.';
2694
+ if (!errorMessage) {
2695
+ return prefix;
2696
+ }
2697
+ return `${prefix} ${errorMessage}`;
2698
+ }
2699
+ if (errorMessage) {
2700
+ return `OpenAI request failed. ${errorMessage}`;
2701
+ }
2702
+ return openApiRequestFailedMessage;
2703
+ }
2704
+ case 'request-failed':
2705
+ return openApiRequestFailedMessage;
2706
+ }
2707
+ };
2708
+
2709
+ const getOpenApiModelId = selectedModelId => {
2710
+ const openApiPrefix = 'openapi/';
2711
+ const openAiPrefix = 'openai/';
2712
+ const normalizedModelId = selectedModelId.toLowerCase();
2713
+ if (normalizedModelId.startsWith(openApiPrefix)) {
2714
+ return selectedModelId.slice(openApiPrefix.length);
2715
+ }
2716
+ if (normalizedModelId.startsWith(openAiPrefix)) {
2717
+ return selectedModelId.slice(openAiPrefix.length);
2718
+ }
2719
+ return selectedModelId;
2720
+ };
2721
+
2722
+ const getOpenRouterApiEndpoint = openRouterApiBaseUrl => {
2723
+ const trimmedBaseUrl = openRouterApiBaseUrl.replace(/\/+$/, '');
2724
+ return `${trimmedBaseUrl}/chat/completions`;
2725
+ };
2726
+
2727
+ const getOpenRouterKeyEndpoint = openRouterApiBaseUrl => {
2728
+ const trimmedBaseUrl = openRouterApiBaseUrl.replace(/\/+$/, '');
2729
+ return `${trimmedBaseUrl}/auth/key`;
2730
+ };
2731
+
2732
+ const getOpenRouterRaw429Message = async response => {
2733
+ let parsed;
2734
+ try {
2735
+ parsed = await response.json();
2736
+ } catch {
2737
+ return undefined;
2738
+ }
2739
+ if (!parsed || typeof parsed !== 'object') {
2740
+ return undefined;
2741
+ }
2742
+ const error = Reflect.get(parsed, 'error');
2743
+ if (!error || typeof error !== 'object') {
2744
+ return undefined;
2745
+ }
2746
+ const metadata = Reflect.get(error, 'metadata');
2747
+ if (!metadata || typeof metadata !== 'object') {
2748
+ return undefined;
2749
+ }
2750
+ const raw = Reflect.get(metadata, 'raw');
2751
+ if (typeof raw !== 'string' || !raw) {
2752
+ return undefined;
2753
+ }
2754
+ return raw;
2755
+ };
2296
2756
  const getOpenRouterLimitInfo = async (openRouterApiKey, openRouterApiBaseUrl) => {
2297
2757
  let response;
2298
2758
  try {
@@ -2337,124 +2797,131 @@ const getOpenRouterLimitInfo = async (openRouterApiKey, openRouterApiBaseUrl) =>
2337
2797
  }
2338
2798
  return normalizedLimitInfo;
2339
2799
  };
2340
- const getOpenRouterAssistantText = async (messages, modelId, openRouterApiKey, openRouterApiBaseUrl) => {
2341
- let response;
2342
- try {
2343
- response = await fetch(getOpenRouterApiEndpoint(openRouterApiBaseUrl), {
2344
- body: JSON.stringify({
2345
- messages: messages.map(message => ({
2346
- content: message.text,
2347
- role: message.role
2348
- })),
2349
- model: modelId
2350
- }),
2351
- headers: {
2352
- Authorization: `Bearer ${openRouterApiKey}`,
2353
- 'Content-Type': 'application/json'
2354
- },
2355
- method: 'POST'
2356
- });
2357
- } catch {
2358
- return {
2359
- details: 'request-failed',
2360
- type: 'error'
2361
- };
2362
- }
2363
- if (!response.ok) {
2364
- if (response.status === 429) {
2365
- const retryAfter = response.headers?.get?.('retry-after') ?? null;
2366
- const rawMessage = await getOpenRouterRaw429Message(response);
2367
- const limitInfo = await getOpenRouterLimitInfo(openRouterApiKey, openRouterApiBaseUrl);
2800
+ const getOpenRouterAssistantText = async (messages, modelId, openRouterApiKey, openRouterApiBaseUrl, assetDir, platform) => {
2801
+ const completionMessages = messages.map(message => ({
2802
+ content: message.text,
2803
+ role: message.role
2804
+ }));
2805
+ const tools = getBasicChatTools();
2806
+ const maxToolIterations = 4;
2807
+ for (let i = 0; i <= maxToolIterations; i++) {
2808
+ let response;
2809
+ try {
2810
+ response = await fetch(getOpenRouterApiEndpoint(openRouterApiBaseUrl), {
2811
+ body: JSON.stringify({
2812
+ messages: completionMessages,
2813
+ model: modelId,
2814
+ tool_choice: 'auto',
2815
+ tools
2816
+ }),
2817
+ headers: {
2818
+ Authorization: `Bearer ${openRouterApiKey}`,
2819
+ 'Content-Type': 'application/json'
2820
+ },
2821
+ method: 'POST'
2822
+ });
2823
+ } catch {
2368
2824
  return {
2369
- details: 'too-many-requests',
2370
- limitInfo: limitInfo || retryAfter ? {
2371
- ...limitInfo,
2372
- retryAfter
2373
- } : undefined,
2374
- rawMessage,
2375
- statusCode: 429,
2825
+ details: 'request-failed',
2376
2826
  type: 'error'
2377
2827
  };
2378
2828
  }
2829
+ if (!response.ok) {
2830
+ if (response.status === 429) {
2831
+ const retryAfter = response.headers?.get?.('retry-after') ?? null;
2832
+ const rawMessage = await getOpenRouterRaw429Message(response);
2833
+ const limitInfo = await getOpenRouterLimitInfo(openRouterApiKey, openRouterApiBaseUrl);
2834
+ return {
2835
+ details: 'too-many-requests',
2836
+ limitInfo: limitInfo || retryAfter ? {
2837
+ ...limitInfo,
2838
+ retryAfter
2839
+ } : undefined,
2840
+ rawMessage,
2841
+ statusCode: 429,
2842
+ type: 'error'
2843
+ };
2844
+ }
2845
+ return {
2846
+ details: 'http-error',
2847
+ statusCode: response.status,
2848
+ type: 'error'
2849
+ };
2850
+ }
2851
+ let parsed;
2852
+ try {
2853
+ parsed = await response.json();
2854
+ } catch {
2855
+ return {
2856
+ details: 'request-failed',
2857
+ type: 'error'
2858
+ };
2859
+ }
2860
+ if (!parsed || typeof parsed !== 'object') {
2861
+ return {
2862
+ text: '',
2863
+ type: 'success'
2864
+ };
2865
+ }
2866
+ const choices = Reflect.get(parsed, 'choices');
2867
+ if (!Array.isArray(choices)) {
2868
+ return {
2869
+ text: '',
2870
+ type: 'success'
2871
+ };
2872
+ }
2873
+ const firstChoice = choices[0];
2874
+ if (!firstChoice || typeof firstChoice !== 'object') {
2875
+ return {
2876
+ text: '',
2877
+ type: 'success'
2878
+ };
2879
+ }
2880
+ const message = Reflect.get(firstChoice, 'message');
2881
+ if (!message || typeof message !== 'object') {
2882
+ return {
2883
+ text: '',
2884
+ type: 'success'
2885
+ };
2886
+ }
2887
+ const toolCalls = Reflect.get(message, 'tool_calls');
2888
+ if (Array.isArray(toolCalls) && toolCalls.length > 0) {
2889
+ completionMessages.push(message);
2890
+ for (const toolCall of toolCalls) {
2891
+ if (!toolCall || typeof toolCall !== 'object') {
2892
+ continue;
2893
+ }
2894
+ const id = Reflect.get(toolCall, 'id');
2895
+ const toolFunction = Reflect.get(toolCall, 'function');
2896
+ if (typeof id !== 'string' || !toolFunction || typeof toolFunction !== 'object') {
2897
+ continue;
2898
+ }
2899
+ const name = Reflect.get(toolFunction, 'name');
2900
+ const rawArguments = Reflect.get(toolFunction, 'arguments');
2901
+ const content = typeof name === 'string' ? await executeChatTool(name, rawArguments, {
2902
+ assetDir,
2903
+ platform
2904
+ }) : '{}';
2905
+ completionMessages.push({
2906
+ content,
2907
+ role: 'tool',
2908
+ tool_call_id: id
2909
+ });
2910
+ }
2911
+ continue;
2912
+ }
2913
+ const content = Reflect.get(message, 'content');
2379
2914
  return {
2380
- details: 'http-error',
2381
- statusCode: response.status,
2382
- type: 'error'
2383
- };
2384
- }
2385
- let parsed;
2386
- try {
2387
- parsed = await response.json();
2388
- } catch {
2389
- return {
2390
- details: 'request-failed',
2391
- type: 'error'
2392
- };
2393
- }
2394
- if (!parsed || typeof parsed !== 'object') {
2395
- return {
2396
- text: '',
2397
- type: 'success'
2398
- };
2399
- }
2400
- const choices = Reflect.get(parsed, 'choices');
2401
- if (!Array.isArray(choices)) {
2402
- return {
2403
- text: '',
2404
- type: 'success'
2405
- };
2406
- }
2407
- const firstChoice = choices[0];
2408
- if (!firstChoice || typeof firstChoice !== 'object') {
2409
- return {
2410
- text: '',
2411
- type: 'success'
2412
- };
2413
- }
2414
- const message = Reflect.get(firstChoice, 'message');
2415
- if (!message || typeof message !== 'object') {
2416
- return {
2417
- text: '',
2915
+ text: getTextContent(content),
2418
2916
  type: 'success'
2419
2917
  };
2420
2918
  }
2421
- const content = Reflect.get(message, 'content');
2422
2919
  return {
2423
- text: getTextContent(content),
2424
- type: 'success'
2920
+ details: 'request-failed',
2921
+ type: 'error'
2425
2922
  };
2426
2923
  };
2427
2924
 
2428
- /* eslint-disable @cspell/spellchecker */
2429
- const getOpenRouterModelId = selectedModelId => {
2430
- const openRouterPrefix = 'openrouter/';
2431
- if (selectedModelId.toLowerCase().startsWith(openRouterPrefix)) {
2432
- return selectedModelId.slice(openRouterPrefix.length);
2433
- }
2434
- return selectedModelId;
2435
- };
2436
-
2437
- const isOpenApiModel = (selectedModelId, models) => {
2438
- const selectedModel = models.find(model => model.id === selectedModelId);
2439
- const normalizedProvider = selectedModel?.provider?.toLowerCase();
2440
- if (normalizedProvider === 'openapi' || normalizedProvider === 'openai' || normalizedProvider === 'open-ai') {
2441
- return true;
2442
- }
2443
- const normalizedModelId = selectedModelId.toLowerCase();
2444
- return normalizedModelId.startsWith('openapi/') || normalizedModelId.startsWith('openai/');
2445
- };
2446
-
2447
- /* eslint-disable @cspell/spellchecker */
2448
-
2449
- const isOpenRouterModel = (selectedModelId, models) => {
2450
- const selectedModel = models.find(model => model.id === selectedModelId);
2451
- const normalizedProvider = selectedModel?.provider?.toLowerCase();
2452
- if (normalizedProvider === 'openrouter' || normalizedProvider === 'open-router') {
2453
- return true;
2454
- }
2455
- return selectedModelId.toLowerCase().startsWith('openrouter/');
2456
- };
2457
-
2458
2925
  const getOpenRouterTooManyRequestsMessage = errorResult => {
2459
2926
  const details = [];
2460
2927
  if (errorResult.rawMessage) {
@@ -2487,6 +2954,7 @@ const getOpenRouterTooManyRequestsMessage = errorResult => {
2487
2954
  }
2488
2955
  return `${openRouterTooManyRequestsMessage} ${details.join(' ')}`;
2489
2956
  };
2957
+
2490
2958
  const getOpenRouterErrorMessage = errorResult => {
2491
2959
  switch (errorResult.details) {
2492
2960
  case 'http-error':
@@ -2496,121 +2964,58 @@ const getOpenRouterErrorMessage = errorResult => {
2496
2964
  return getOpenRouterTooManyRequestsMessage(errorResult);
2497
2965
  }
2498
2966
  };
2499
- const getOpenApiErrorMessage = errorResult => {
2500
- switch (errorResult.details) {
2501
- case 'http-error':
2502
- case 'request-failed':
2503
- return openApiRequestFailedMessage;
2504
- }
2505
- };
2506
- const normalizeLimitInfo = value => {
2507
- if (!value || typeof value !== 'object') {
2508
- return undefined;
2967
+
2968
+ /* eslint-disable @cspell/spellchecker */
2969
+ const getOpenRouterModelId = selectedModelId => {
2970
+ const openRouterPrefix = 'openrouter/';
2971
+ if (selectedModelId.toLowerCase().startsWith(openRouterPrefix)) {
2972
+ return selectedModelId.slice(openRouterPrefix.length);
2509
2973
  }
2510
- const limitRemaining = Reflect.get(value, 'limitRemaining');
2511
- const limitReset = Reflect.get(value, 'limitReset');
2512
- const retryAfter = Reflect.get(value, 'retryAfter');
2513
- const usage = Reflect.get(value, 'usage');
2514
- const usageDaily = Reflect.get(value, 'usageDaily');
2515
- const normalized = {
2516
- limitRemaining: typeof limitRemaining === 'number' || limitRemaining === null ? limitRemaining : undefined,
2517
- limitReset: typeof limitReset === 'string' || limitReset === null ? limitReset : undefined,
2518
- retryAfter: typeof retryAfter === 'string' || retryAfter === null ? retryAfter : undefined,
2519
- usage: typeof usage === 'number' ? usage : undefined,
2520
- usageDaily: typeof usageDaily === 'number' ? usageDaily : undefined
2521
- };
2522
- const hasDetails = normalized.limitRemaining !== undefined || normalized.limitReset !== undefined || normalized.retryAfter !== undefined || normalized.usage !== undefined || normalized.usageDaily !== undefined;
2523
- return hasDetails ? normalized : undefined;
2974
+ return selectedModelId;
2524
2975
  };
2525
- const normalizeMockResult = value => {
2526
- if (typeof value === 'string') {
2527
- return {
2528
- text: value,
2529
- type: 'success'
2530
- };
2531
- }
2532
- if (!value || typeof value !== 'object') {
2533
- return {
2534
- details: 'request-failed',
2535
- type: 'error'
2536
- };
2537
- }
2538
- const type = Reflect.get(value, 'type');
2539
- if (type === 'success') {
2540
- const text = Reflect.get(value, 'text');
2541
- if (typeof text === 'string') {
2542
- return {
2543
- text,
2544
- type: 'success'
2545
- };
2546
- }
2547
- return {
2548
- details: 'request-failed',
2549
- type: 'error'
2550
- };
2551
- }
2552
- if (type === 'error') {
2553
- const details = Reflect.get(value, 'details');
2554
- if (details === 'request-failed' || details === 'too-many-requests' || details === 'http-error') {
2555
- const rawMessage = Reflect.get(value, 'rawMessage');
2556
- const statusCode = Reflect.get(value, 'statusCode');
2557
- return {
2558
- details,
2559
- limitInfo: normalizeLimitInfo(Reflect.get(value, 'limitInfo')),
2560
- rawMessage: typeof rawMessage === 'string' ? rawMessage : undefined,
2561
- statusCode: typeof statusCode === 'number' ? statusCode : undefined,
2562
- type: 'error'
2563
- };
2564
- }
2565
- }
2566
- const text = Reflect.get(value, 'text');
2567
- if (typeof text === 'string') {
2568
- return {
2569
- text,
2570
- type: 'success'
2571
- };
2976
+
2977
+ const isOpenApiModel = (selectedModelId, models) => {
2978
+ const selectedModel = models.find(model => model.id === selectedModelId);
2979
+ const normalizedProvider = selectedModel?.provider?.toLowerCase();
2980
+ if (normalizedProvider === 'openapi' || normalizedProvider === 'openai' || normalizedProvider === 'open-ai') {
2981
+ return true;
2572
2982
  }
2573
- return {
2574
- details: 'request-failed',
2575
- type: 'error'
2576
- };
2983
+ const normalizedModelId = selectedModelId.toLowerCase();
2984
+ return normalizedModelId.startsWith('openapi/') || normalizedModelId.startsWith('openai/');
2577
2985
  };
2578
- const getMockOpenRouterAssistantText = async (messages, modelId, openRouterApiBaseUrl, openRouterApiKey, mockApiCommandId, assetDir, platform) => {
2579
- if (!mockApiCommandId) {
2580
- return {
2581
- details: 'request-failed',
2582
- type: 'error'
2583
- };
2584
- }
2585
- try {
2586
- const result = await executeProvider({
2587
- assetDir,
2588
- event: `onCommand:${mockApiCommandId}`,
2589
- method: CommandExecute,
2590
- noProviderFoundMessage: 'No mock api command found',
2591
- params: [mockApiCommandId, {
2592
- messages,
2593
- modelId,
2594
- openRouterApiBaseUrl,
2595
- openRouterApiKey
2596
- }],
2597
- platform
2598
- });
2599
- return normalizeMockResult(result);
2600
- } catch {
2601
- return {
2602
- details: 'request-failed',
2603
- type: 'error'
2604
- };
2986
+
2987
+ /* eslint-disable @cspell/spellchecker */
2988
+
2989
+ const isOpenRouterModel = (selectedModelId, models) => {
2990
+ const selectedModel = models.find(model => model.id === selectedModelId);
2991
+ const normalizedProvider = selectedModel?.provider?.toLowerCase();
2992
+ if (normalizedProvider === 'openrouter' || normalizedProvider === 'open-router') {
2993
+ return true;
2605
2994
  }
2995
+ return selectedModelId.toLowerCase().startsWith('openrouter/');
2606
2996
  };
2607
- const getAiResponse = async (userText, messages, nextMessageId, selectedModelId, models, openApiApiKey, openApiApiBaseUrl, openRouterApiKey, openRouterApiBaseUrl, useMockApi, mockApiCommandId, assetDir, platform) => {
2997
+
2998
+ const getAiResponse = async ({
2999
+ assetDir,
3000
+ messages,
3001
+ mockApiCommandId,
3002
+ models,
3003
+ nextMessageId,
3004
+ openApiApiBaseUrl,
3005
+ openApiApiKey,
3006
+ openRouterApiBaseUrl,
3007
+ openRouterApiKey,
3008
+ platform,
3009
+ selectedModelId,
3010
+ useMockApi,
3011
+ userText
3012
+ }) => {
2608
3013
  let text = '';
2609
3014
  const usesOpenApiModel = isOpenApiModel(selectedModelId, models);
2610
3015
  const usesOpenRouterModel = isOpenRouterModel(selectedModelId, models);
2611
3016
  if (usesOpenApiModel) {
2612
3017
  if (openApiApiKey) {
2613
- const result = await getOpenApiAssistantText(messages, getOpenApiModelId(selectedModelId), openApiApiKey, openApiApiBaseUrl);
3018
+ const result = await getOpenApiAssistantText(messages, getOpenApiModelId(selectedModelId), openApiApiKey, openApiApiBaseUrl, assetDir, platform);
2614
3019
  if (result.type === 'success') {
2615
3020
  const {
2616
3021
  text: assistantText
@@ -2635,7 +3040,7 @@ const getAiResponse = async (userText, messages, nextMessageId, selectedModelId,
2635
3040
  text = getOpenRouterErrorMessage(result);
2636
3041
  }
2637
3042
  } else if (openRouterApiKey) {
2638
- const result = await getOpenRouterAssistantText(messages, modelId, openRouterApiKey, openRouterApiBaseUrl);
3043
+ const result = await getOpenRouterAssistantText(messages, modelId, openRouterApiKey, openRouterApiBaseUrl, assetDir, platform);
2639
3044
  if (result.type === 'success') {
2640
3045
  const {
2641
3046
  text: assistantText
@@ -2706,7 +3111,21 @@ const handleClickSaveOpenApiApiKey = async state => {
2706
3111
  return updatedState;
2707
3112
  }
2708
3113
  const retryMessages = session.messages.slice(0, -1);
2709
- const assistantMessage = await getAiResponse(previousUserMessage.text, retryMessages, updatedState.nextMessageId, updatedState.selectedModelId, updatedState.models, updatedState.openApiApiKey, updatedState.openApiApiBaseUrl, updatedState.openRouterApiKey, updatedState.openRouterApiBaseUrl, updatedState.useMockApi, updatedState.mockApiCommandId, updatedState.assetDir, updatedState.platform);
3114
+ const assistantMessage = await getAiResponse({
3115
+ assetDir: updatedState.assetDir,
3116
+ messages: retryMessages,
3117
+ mockApiCommandId: updatedState.mockApiCommandId,
3118
+ models: updatedState.models,
3119
+ nextMessageId: updatedState.nextMessageId,
3120
+ openApiApiBaseUrl: updatedState.openApiApiBaseUrl,
3121
+ openApiApiKey: updatedState.openApiApiKey,
3122
+ openRouterApiBaseUrl: updatedState.openRouterApiBaseUrl,
3123
+ openRouterApiKey: updatedState.openRouterApiKey,
3124
+ platform: updatedState.platform,
3125
+ selectedModelId: updatedState.selectedModelId,
3126
+ useMockApi: updatedState.useMockApi,
3127
+ userText: previousUserMessage.text
3128
+ });
2710
3129
  const updatedSession = {
2711
3130
  ...session,
2712
3131
  messages: [...session.messages.slice(0, -1), assistantMessage]
@@ -2746,7 +3165,18 @@ const handleClickSaveOpenRouterApiKey = async state => {
2746
3165
  if (!openRouterApiKey) {
2747
3166
  return state;
2748
3167
  }
2749
- const updatedState = await setOpenRouterApiKey(state, openRouterApiKey);
3168
+ const optimisticState = {
3169
+ ...state,
3170
+ openRouterApiKeyState: 'saving'
3171
+ };
3172
+ set(state.uid, state, optimisticState);
3173
+ // @ts-ignore
3174
+ await invoke('Chat.rerender');
3175
+ const persistedState = await setOpenRouterApiKey(optimisticState, openRouterApiKey);
3176
+ const updatedState = {
3177
+ ...persistedState,
3178
+ openRouterApiKeyState: 'idle'
3179
+ };
2750
3180
  const session = updatedState.sessions.find(item => item.id === updatedState.selectedSessionId);
2751
3181
  if (!session) {
2752
3182
  return updatedState;
@@ -2761,9 +3191,21 @@ const handleClickSaveOpenRouterApiKey = async state => {
2761
3191
  return updatedState;
2762
3192
  }
2763
3193
  const retryMessages = session.messages.slice(0, -1);
2764
-
2765
- // @ts-ignore
2766
- const assistantMessage = await getAiResponse(previousUserMessage.text, retryMessages, updatedState.nextMessageId, updatedState.selectedModelId, updatedState.models, updatedState.openApiApiKey, updatedState.openApiApiBaseUrl, openRouterApiKey, updatedState.openRouterApiBaseUrl, updatedState.useMockApi, updatedState.mockApiCommandId, updatedState.assetDir, updatedState.platform);
3194
+ const assistantMessage = await getAiResponse({
3195
+ assetDir: updatedState.assetDir,
3196
+ messages: retryMessages,
3197
+ mockApiCommandId: updatedState.mockApiCommandId,
3198
+ models: updatedState.models,
3199
+ nextMessageId: updatedState.nextMessageId,
3200
+ openApiApiBaseUrl: updatedState.openApiApiBaseUrl,
3201
+ openApiApiKey: updatedState.openApiApiKey,
3202
+ openRouterApiBaseUrl: updatedState.openRouterApiBaseUrl,
3203
+ openRouterApiKey,
3204
+ platform: updatedState.platform,
3205
+ selectedModelId: updatedState.selectedModelId,
3206
+ useMockApi: updatedState.useMockApi,
3207
+ userText: previousUserMessage.text
3208
+ });
2767
3209
  const updatedSession = {
2768
3210
  ...session,
2769
3211
  messages: [...session.messages.slice(0, -1), assistantMessage]
@@ -2778,6 +3220,7 @@ const handleClickSaveOpenRouterApiKey = async state => {
2778
3220
  return {
2779
3221
  ...updatedState,
2780
3222
  nextMessageId: updatedState.nextMessageId + 1,
3223
+ openRouterApiKeyState: 'idle',
2781
3224
  sessions: updatedSessions
2782
3225
  };
2783
3226
  };
@@ -2845,6 +3288,7 @@ const handleSubmit = async state => {
2845
3288
  await saveChatSession(newSession);
2846
3289
  optimisticState = focusInput({
2847
3290
  ...state,
3291
+ composerHeight: getMinComposerHeightForState(state),
2848
3292
  composerValue: '',
2849
3293
  inputSource: 'script',
2850
3294
  lastSubmittedSessionId: newSessionId,
@@ -2869,6 +3313,7 @@ const handleSubmit = async state => {
2869
3313
  }
2870
3314
  optimisticState = focusInput({
2871
3315
  ...state,
3316
+ composerHeight: getMinComposerHeightForState(state),
2872
3317
  composerValue: '',
2873
3318
  inputSource: 'script',
2874
3319
  lastSubmittedSessionId: selectedSessionId,
@@ -2881,7 +3326,21 @@ const handleSubmit = async state => {
2881
3326
  await invoke('Chat.rerender');
2882
3327
  const selectedOptimisticSession = optimisticState.sessions.find(session => session.id === optimisticState.selectedSessionId);
2883
3328
  const messages = selectedOptimisticSession?.messages ?? [];
2884
- const assistantMessage = await getAiResponse(userText, messages, optimisticState.nextMessageId, selectedModelId, models, openApiApiKey, openApiApiBaseUrl, openRouterApiKey, openRouterApiBaseUrl, useMockApi, mockApiCommandId, assetDir, platform);
3329
+ const assistantMessage = await getAiResponse({
3330
+ assetDir,
3331
+ messages,
3332
+ mockApiCommandId,
3333
+ models,
3334
+ nextMessageId: optimisticState.nextMessageId,
3335
+ openApiApiBaseUrl,
3336
+ openApiApiKey,
3337
+ openRouterApiBaseUrl,
3338
+ openRouterApiKey,
3339
+ platform,
3340
+ selectedModelId,
3341
+ useMockApi,
3342
+ userText
3343
+ });
2885
3344
  const updatedSessions = optimisticState.sessions.map(session => {
2886
3345
  if (session.id !== optimisticState.selectedSessionId) {
2887
3346
  return session;
@@ -3086,8 +3545,10 @@ const handleInput = async (state, name, value, inputSource = 'user') => {
3086
3545
  if (name !== Composer) {
3087
3546
  return state;
3088
3547
  }
3548
+ const composerHeight = await getComposerHeight(state, value);
3089
3549
  return {
3090
3550
  ...state,
3551
+ composerHeight,
3091
3552
  composerValue: value,
3092
3553
  inputSource
3093
3554
  };
@@ -3152,6 +3613,7 @@ const submitRename = async state => {
3152
3613
  }
3153
3614
  return {
3154
3615
  ...state,
3616
+ composerHeight: getMinComposerHeightForState(state),
3155
3617
  composerValue: '',
3156
3618
  inputSource: 'script',
3157
3619
  renamingSessionId: '',
@@ -3549,7 +4011,7 @@ const getUsageOverviewDom = (tokensUsed, tokensMax) => {
3549
4011
  }, text(usageLabel)];
3550
4012
  };
3551
4013
 
3552
- const getChatSendAreaDom = (composerValue, models, selectedModelId, usageOverviewEnabled, tokensUsed, tokensMax) => {
4014
+ const getChatSendAreaDom = (composerValue, models, selectedModelId, usageOverviewEnabled, tokensUsed, tokensMax, composerHeight = 28, composerFontSize = 13, composerFontFamily = 'system-ui', composerLineHeight = 20) => {
3553
4015
  const isSendDisabled = composerValue.trim() === '';
3554
4016
  const modelOptions = models.flatMap(model => getModelOptionDOm(model, selectedModelId));
3555
4017
  return [{
@@ -3567,7 +4029,7 @@ const getChatSendAreaDom = (composerValue, models, selectedModelId, usageOvervie
3567
4029
  onFocus: HandleFocus,
3568
4030
  onInput: HandleInput,
3569
4031
  placeholder: composePlaceholder(),
3570
- rows: 4,
4032
+ style: `height:${composerHeight}px;font-size:${composerFontSize}px;font-family:${composerFontFamily};line-height:${composerLineHeight}px;`,
3571
4033
  type: TextArea,
3572
4034
  value: composerValue
3573
4035
  }, {
@@ -3662,7 +4124,9 @@ const getMissingApiKeyDom = ({
3662
4124
  inputValue,
3663
4125
  openSettingsButtonName,
3664
4126
  placeholder,
3665
- saveButtonName
4127
+ saveButtonDisabled = false,
4128
+ saveButtonName,
4129
+ saveButtonText = save()
3666
4130
  }) => {
3667
4131
  return [{
3668
4132
  childCount: 2,
@@ -3682,10 +4146,11 @@ const getMissingApiKeyDom = ({
3682
4146
  }, {
3683
4147
  childCount: 1,
3684
4148
  className: mergeClassNames(Button, ButtonPrimary),
4149
+ disabled: saveButtonDisabled,
3685
4150
  name: saveButtonName,
3686
4151
  onClick: HandleClick,
3687
4152
  type: Button$1
3688
- }, text(save()), {
4153
+ }, text(saveButtonText), {
3689
4154
  childCount: 1,
3690
4155
  className: mergeClassNames(Button, ButtonSecondary),
3691
4156
  name: openSettingsButtonName,
@@ -3705,14 +4170,17 @@ const getMissingOpenApiApiKeyDom = openApiApiKeyInput => {
3705
4170
  });
3706
4171
  };
3707
4172
 
3708
- const getMissingOpenRouterApiKeyDom = openRouterApiKeyInput => {
4173
+ const getMissingOpenRouterApiKeyDom = (openRouterApiKeyInput, openRouterApiKeyState = 'idle') => {
4174
+ const isSaving = openRouterApiKeyState === 'saving';
3709
4175
  return getMissingApiKeyDom({
3710
4176
  getApiKeyText: getOpenRouterApiKey(),
3711
4177
  inputName: OpenRouterApiKeyInput,
3712
4178
  inputValue: openRouterApiKeyInput,
3713
4179
  openSettingsButtonName: OpenOpenRouterApiKeySettings,
3714
4180
  placeholder: openRouterApiKeyPlaceholder(),
3715
- saveButtonName: SaveOpenRouterApiKey
4181
+ saveButtonDisabled: isSaving,
4182
+ saveButtonName: SaveOpenRouterApiKey,
4183
+ saveButtonText: isSaving ? saving() : save()
3716
4184
  });
3717
4185
  };
3718
4186
 
@@ -3742,7 +4210,7 @@ const getOpenRouterTooManyRequestsDom = () => {
3742
4210
  }, text(reason)];
3743
4211
  })];
3744
4212
  };
3745
- const getChatMessageDom = (message, openRouterApiKeyInput, openApiApiKeyInput = '') => {
4213
+ const getChatMessageDom = (message, openRouterApiKeyInput, openApiApiKeyInput = '', openRouterApiKeyState = 'idle') => {
3746
4214
  const roleClassName = message.role === 'user' ? MessageUser : MessageAssistant;
3747
4215
  const isOpenApiApiKeyMissingMessage = message.role === 'assistant' && message.text === openApiApiKeyRequiredMessage;
3748
4216
  const isOpenRouterApiKeyMissingMessage = message.role === 'assistant' && message.text === openRouterApiKeyRequiredMessage;
@@ -3761,7 +4229,7 @@ const getChatMessageDom = (message, openRouterApiKeyInput, openApiApiKeyInput =
3761
4229
  childCount: 1,
3762
4230
  className: Markdown,
3763
4231
  type: P
3764
- }, text(message.text), ...(isOpenApiApiKeyMissingMessage ? getMissingOpenApiApiKeyDom(openApiApiKeyInput) : []), ...(isOpenRouterApiKeyMissingMessage ? getMissingOpenRouterApiKeyDom(openRouterApiKeyInput) : []), ...(isOpenRouterRequestFailedMessage ? getOpenRouterRequestFailedDom() : []), ...(isOpenRouterTooManyRequestsMessage ? getOpenRouterTooManyRequestsDom() : [])];
4232
+ }, text(message.text), ...(isOpenApiApiKeyMissingMessage ? getMissingOpenApiApiKeyDom(openApiApiKeyInput) : []), ...(isOpenRouterApiKeyMissingMessage ? getMissingOpenRouterApiKeyDom(openRouterApiKeyInput, openRouterApiKeyState) : []), ...(isOpenRouterRequestFailedMessage ? getOpenRouterRequestFailedDom() : []), ...(isOpenRouterTooManyRequestsMessage ? getOpenRouterTooManyRequestsDom() : [])];
3765
4233
  };
3766
4234
 
3767
4235
  const getEmptyMessagesDom = () => {
@@ -3772,7 +4240,7 @@ const getEmptyMessagesDom = () => {
3772
4240
  }, text(startConversation())];
3773
4241
  };
3774
4242
 
3775
- const getMessagesDom = (messages, openRouterApiKeyInput, openApiApiKeyInput = '') => {
4243
+ const getMessagesDom = (messages, openRouterApiKeyInput, openApiApiKeyInput = '', openRouterApiKeyState = 'idle') => {
3776
4244
  if (messages.length === 0) {
3777
4245
  return getEmptyMessagesDom();
3778
4246
  }
@@ -3780,10 +4248,10 @@ const getMessagesDom = (messages, openRouterApiKeyInput, openApiApiKeyInput = ''
3780
4248
  childCount: messages.length,
3781
4249
  className: 'ChatMessages',
3782
4250
  type: Div
3783
- }, ...messages.flatMap(message => getChatMessageDom(message, openRouterApiKeyInput, openApiApiKeyInput))];
4251
+ }, ...messages.flatMap(message => getChatMessageDom(message, openRouterApiKeyInput, openApiApiKeyInput, openRouterApiKeyState))];
3784
4252
  };
3785
4253
 
3786
- const getChatModeDetailVirtualDom = (sessions, selectedSessionId, composerValue, openRouterApiKeyInput, openApiApiKeyInput, models, selectedModelId, usageOverviewEnabled, tokensUsed, tokensMax) => {
4254
+ const getChatModeDetailVirtualDom = (sessions, selectedSessionId, composerValue, openRouterApiKeyInput, openApiApiKeyInput, models, selectedModelId, usageOverviewEnabled, tokensUsed, tokensMax, openRouterApiKeyState = 'idle', composerHeight = 28, composerFontSize = 13, composerFontFamily = 'system-ui', composerLineHeight = 20) => {
3787
4255
  const selectedSession = sessions.find(session => session.id === selectedSessionId);
3788
4256
  const selectedSessionTitle = selectedSession?.title || chatTitle();
3789
4257
  const messages = selectedSession ? selectedSession.messages : [];
@@ -3791,7 +4259,7 @@ const getChatModeDetailVirtualDom = (sessions, selectedSessionId, composerValue,
3791
4259
  childCount: 3,
3792
4260
  className: mergeClassNames(Viewlet, Chat),
3793
4261
  type: Div
3794
- }, ...getChatHeaderDomDetailMode(selectedSessionTitle), ...getMessagesDom(messages, openRouterApiKeyInput, openApiApiKeyInput), ...getChatSendAreaDom(composerValue, models, selectedModelId, usageOverviewEnabled, tokensUsed, tokensMax)];
4262
+ }, ...getChatHeaderDomDetailMode(selectedSessionTitle), ...getMessagesDom(messages, openRouterApiKeyInput, openApiApiKeyInput, openRouterApiKeyState), ...getChatSendAreaDom(composerValue, models, selectedModelId, usageOverviewEnabled, tokensUsed, tokensMax, composerHeight, composerFontSize, composerFontFamily, composerLineHeight)];
3795
4263
  };
3796
4264
 
3797
4265
  const getChatHeaderListModeDom = () => {
@@ -3860,12 +4328,12 @@ const getChatListDom = (sessions, selectedSessionId) => {
3860
4328
  }, ...sessions.flatMap(getSessionDom)];
3861
4329
  };
3862
4330
 
3863
- const getChatModeListVirtualDom = (sessions, selectedSessionId, composerValue, models, selectedModelId, usageOverviewEnabled, tokensUsed, tokensMax) => {
4331
+ const getChatModeListVirtualDom = (sessions, selectedSessionId, composerValue, models, selectedModelId, usageOverviewEnabled, tokensUsed, tokensMax, composerHeight = 28, composerFontSize = 13, composerFontFamily = 'system-ui', composerLineHeight = 20) => {
3864
4332
  return [{
3865
4333
  childCount: 3,
3866
4334
  className: mergeClassNames(Viewlet, Chat),
3867
4335
  type: Div
3868
- }, ...getChatHeaderListModeDom(), ...getChatListDom(sessions), ...getChatSendAreaDom(composerValue, models, selectedModelId, usageOverviewEnabled, tokensUsed, tokensMax)];
4336
+ }, ...getChatHeaderListModeDom(), ...getChatListDom(sessions), ...getChatSendAreaDom(composerValue, models, selectedModelId, usageOverviewEnabled, tokensUsed, tokensMax, composerHeight, composerFontSize, composerFontFamily, composerLineHeight)];
3869
4337
  };
3870
4338
 
3871
4339
  const getChatModeUnsupportedVirtualDom = () => {
@@ -3875,12 +4343,12 @@ const getChatModeUnsupportedVirtualDom = () => {
3875
4343
  }, text(unknownViewMode())];
3876
4344
  };
3877
4345
 
3878
- const getChatVirtualDom = (sessions, selectedSessionId, composerValue, openRouterApiKeyInput, viewMode, models, selectedModelId, usageOverviewEnabled, tokensUsed, tokensMax, openApiApiKeyInput = '') => {
4346
+ const getChatVirtualDom = (sessions, selectedSessionId, composerValue, openRouterApiKeyInput, viewMode, models, selectedModelId, usageOverviewEnabled, tokensUsed, tokensMax, openApiApiKeyInput = '', openRouterApiKeyState = 'idle', composerHeight = 28, composerFontSize = 13, composerFontFamily = 'system-ui', composerLineHeight = 20) => {
3879
4347
  switch (viewMode) {
3880
4348
  case 'detail':
3881
- return getChatModeDetailVirtualDom(sessions, selectedSessionId, composerValue, openRouterApiKeyInput, openApiApiKeyInput, models, selectedModelId, usageOverviewEnabled, tokensUsed, tokensMax);
4349
+ return getChatModeDetailVirtualDom(sessions, selectedSessionId, composerValue, openRouterApiKeyInput, openApiApiKeyInput, models, selectedModelId, usageOverviewEnabled, tokensUsed, tokensMax, openRouterApiKeyState, composerHeight, composerFontSize, composerFontFamily, composerLineHeight);
3882
4350
  case 'list':
3883
- return getChatModeListVirtualDom(sessions, selectedSessionId, composerValue, models, selectedModelId, usageOverviewEnabled, tokensUsed, tokensMax);
4351
+ return getChatModeListVirtualDom(sessions, selectedSessionId, composerValue, models, selectedModelId, usageOverviewEnabled, tokensUsed, tokensMax, composerHeight, composerFontSize, composerFontFamily, composerLineHeight);
3884
4352
  default:
3885
4353
  return getChatModeUnsupportedVirtualDom();
3886
4354
  }
@@ -3888,11 +4356,16 @@ const getChatVirtualDom = (sessions, selectedSessionId, composerValue, openRoute
3888
4356
 
3889
4357
  const renderItems = (oldState, newState) => {
3890
4358
  const {
4359
+ composerFontFamily,
4360
+ composerFontSize,
4361
+ composerHeight,
4362
+ composerLineHeight,
3891
4363
  composerValue,
3892
4364
  initial,
3893
4365
  models,
3894
4366
  openApiApiKeyInput,
3895
4367
  openRouterApiKeyInput,
4368
+ openRouterApiKeyState,
3896
4369
  selectedModelId,
3897
4370
  selectedSessionId,
3898
4371
  sessions,
@@ -3905,7 +4378,7 @@ const renderItems = (oldState, newState) => {
3905
4378
  if (initial) {
3906
4379
  return [SetDom2, uid, []];
3907
4380
  }
3908
- const dom = getChatVirtualDom(sessions, selectedSessionId, composerValue, openRouterApiKeyInput, viewMode, models, selectedModelId, usageOverviewEnabled, tokensUsed, tokensMax, openApiApiKeyInput);
4381
+ const dom = getChatVirtualDom(sessions, selectedSessionId, composerValue, openRouterApiKeyInput, viewMode, models, selectedModelId, usageOverviewEnabled, tokensUsed, tokensMax, openApiApiKeyInput, openRouterApiKeyState, composerHeight, composerFontSize, composerFontFamily, composerLineHeight);
3909
4382
  return [SetDom2, uid, dom];
3910
4383
  };
3911
4384
 
@@ -4016,6 +4489,7 @@ const reset = async state => {
4016
4489
  await clearChatSessions();
4017
4490
  return {
4018
4491
  ...state,
4492
+ composerHeight: getMinComposerHeightForState(state),
4019
4493
  composerValue: '',
4020
4494
  openRouterApiKey: '',
4021
4495
  selectedSessionId: '',