@lvce-editor/chat-view 2.8.0 → 2.10.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.
@@ -1121,6 +1121,9 @@ const sendMessagePortToExtensionHostWorker$1 = async (port, rpcId = 0) => {
1121
1121
  const activateByEvent$1 = (event, assetDir, platform) => {
1122
1122
  return invoke('ExtensionHostManagement.activateByEvent', event, assetDir, platform);
1123
1123
  };
1124
+ const getWorkspacePath = () => {
1125
+ return invoke('Workspace.getPath');
1126
+ };
1124
1127
  const getPreference = async key => {
1125
1128
  return await invoke('Preferences.get', key);
1126
1129
  };
@@ -1543,8 +1546,56 @@ const create = (uid, x, y, width, height, platform, assetDir) => {
1543
1546
  set(uid, state, state);
1544
1547
  };
1545
1548
 
1549
+ const parseRenderHtmlArguments = rawArguments => {
1550
+ try {
1551
+ const parsed = JSON.parse(rawArguments);
1552
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
1553
+ return undefined;
1554
+ }
1555
+ const html = typeof Reflect.get(parsed, 'html') === 'string' ? String(Reflect.get(parsed, 'html')) : '';
1556
+ if (!html) {
1557
+ return undefined;
1558
+ }
1559
+ const css = typeof Reflect.get(parsed, 'css') === 'string' ? String(Reflect.get(parsed, 'css')) : '';
1560
+ const title = typeof Reflect.get(parsed, 'title') === 'string' ? String(Reflect.get(parsed, 'title')) : 'visual preview';
1561
+ return {
1562
+ css,
1563
+ html,
1564
+ title
1565
+ };
1566
+ } catch {
1567
+ return undefined;
1568
+ }
1569
+ };
1570
+
1571
+ const getRenderHtmlCss = (sessions, selectedSessionId) => {
1572
+ const selectedSession = sessions.find(session => session.id === selectedSessionId);
1573
+ if (!selectedSession) {
1574
+ return '';
1575
+ }
1576
+ const cssRules = new Set();
1577
+ for (const message of selectedSession.messages) {
1578
+ if (message.role !== 'assistant' || !message.toolCalls) {
1579
+ continue;
1580
+ }
1581
+ for (const toolCall of message.toolCalls) {
1582
+ if (toolCall.name !== 'render_html') {
1583
+ continue;
1584
+ }
1585
+ const parsed = parseRenderHtmlArguments(toolCall.arguments);
1586
+ if (!parsed || !parsed.css.trim()) {
1587
+ continue;
1588
+ }
1589
+ cssRules.add(parsed.css);
1590
+ }
1591
+ }
1592
+ return [...cssRules].join('\n\n');
1593
+ };
1594
+
1546
1595
  const isEqual$1 = (oldState, newState) => {
1547
- return oldState.initial === newState.initial && oldState.chatMessageFontFamily === newState.chatMessageFontFamily && oldState.chatMessageFontSize === newState.chatMessageFontSize && oldState.chatMessageLineHeight === newState.chatMessageLineHeight && oldState.composerHeight === newState.composerHeight && oldState.composerLineHeight === newState.composerLineHeight && oldState.composerFontFamily === newState.composerFontFamily && oldState.composerFontSize === newState.composerFontSize && oldState.listItemHeight === newState.listItemHeight;
1596
+ const oldRenderHtmlCss = getRenderHtmlCss(oldState.sessions, oldState.selectedSessionId);
1597
+ const newRenderHtmlCss = getRenderHtmlCss(newState.sessions, newState.selectedSessionId);
1598
+ return oldState.initial === newState.initial && oldState.chatMessageFontFamily === newState.chatMessageFontFamily && oldState.chatMessageFontSize === newState.chatMessageFontSize && oldState.chatMessageLineHeight === newState.chatMessageLineHeight && oldState.composerHeight === newState.composerHeight && oldState.composerLineHeight === newState.composerLineHeight && oldState.composerFontFamily === newState.composerFontFamily && oldState.composerFontSize === newState.composerFontSize && oldState.listItemHeight === newState.listItemHeight && oldRenderHtmlCss === newRenderHtmlCss;
1548
1599
  };
1549
1600
 
1550
1601
  const diffFocus = (oldState, newState) => {
@@ -1602,17 +1653,57 @@ const diff2 = uid => {
1602
1653
 
1603
1654
  const Button$2 = 'button';
1604
1655
 
1656
+ const Audio = 0;
1605
1657
  const Button$1 = 1;
1658
+ const Col = 2;
1659
+ const ColGroup = 3;
1606
1660
  const Div = 4;
1661
+ const H1 = 5;
1607
1662
  const Input = 6;
1608
1663
  const Span = 8;
1664
+ const Table = 9;
1665
+ const TBody = 10;
1666
+ const Td = 11;
1609
1667
  const Text = 12;
1668
+ const Th = 13;
1669
+ const THead = 14;
1670
+ const Tr = 15;
1671
+ const I = 16;
1672
+ const Img = 17;
1673
+ const H2 = 22;
1674
+ const H3 = 23;
1675
+ const H4 = 24;
1676
+ const H5 = 25;
1677
+ const H6 = 26;
1678
+ const Article = 27;
1679
+ const Aside = 28;
1680
+ const Footer = 29;
1681
+ const Header = 30;
1682
+ const Nav = 40;
1683
+ const Section = 41;
1684
+ const Dd = 43;
1685
+ const Dl = 44;
1686
+ const Figcaption = 45;
1687
+ const Figure = 46;
1688
+ const Hr = 47;
1610
1689
  const Li = 48;
1611
1690
  const Ol = 49;
1612
1691
  const P = 50;
1692
+ const Pre = 51;
1693
+ const A = 53;
1694
+ const Abbr = 54;
1695
+ const Br = 55;
1696
+ const Tfoot = 59;
1697
+ const Ul = 60;
1613
1698
  const TextArea = 62;
1614
1699
  const Select$1 = 63;
1615
1700
  const Option$1 = 64;
1701
+ const Code = 65;
1702
+ const Label$1 = 66;
1703
+ const Dt = 67;
1704
+ const Main = 69;
1705
+ const Strong = 70;
1706
+ const Em = 71;
1616
1707
  const Reference = 100;
1617
1708
 
1618
1709
  const Enter = 3;
@@ -2649,7 +2740,8 @@ const deleteSession = async (state, id) => {
2649
2740
  };
2650
2741
 
2651
2742
  const handleClickOpenApiApiKeySettings = async state => {
2652
- await openExternal(state.openApiApiKeysSettingsUrl);
2743
+ // Open the built-in settings editor so the user can inspect or edit their OpenAI API key.
2744
+ await invoke('Main.openUri', 'app://settings.json');
2653
2745
  return state;
2654
2746
  };
2655
2747
 
@@ -3053,7 +3145,42 @@ const getMockOpenRouterAssistantText = async (messages, modelId, openRouterApiBa
3053
3145
  }
3054
3146
  };
3055
3147
 
3056
- const OnFileSystem = 'onFileSystem';
3148
+ const executeGetWorkspaceUriTool = async (_args, _options) => {
3149
+ try {
3150
+ const workspaceUri = await getWorkspacePath();
3151
+ return JSON.stringify({
3152
+ workspaceUri
3153
+ });
3154
+ } catch (error) {
3155
+ return JSON.stringify({
3156
+ error: String(error)
3157
+ });
3158
+ }
3159
+ };
3160
+
3161
+ const isAbsoluteUri$1 = value => {
3162
+ return /^[a-zA-Z][a-zA-Z0-9+.-]*:\/\//.test(value);
3163
+ };
3164
+ const executeListFilesTool = async (args, _options) => {
3165
+ const uri = typeof args.uri === 'string' ? args.uri : '';
3166
+ if (!uri || !isAbsoluteUri$1(uri)) {
3167
+ return JSON.stringify({
3168
+ error: 'Invalid argument: uri must be an absolute URI.'
3169
+ });
3170
+ }
3171
+ try {
3172
+ const entries = await invoke('FileSystem.readDirWithFileTypes', uri);
3173
+ return JSON.stringify({
3174
+ entries,
3175
+ uri
3176
+ });
3177
+ } catch (error) {
3178
+ return JSON.stringify({
3179
+ error: String(error),
3180
+ uri
3181
+ });
3182
+ }
3183
+ };
3057
3184
 
3058
3185
  const isPathTraversalAttempt = path => {
3059
3186
  if (!path) {
@@ -3071,6 +3198,7 @@ const isPathTraversalAttempt = path => {
3071
3198
  const segments = path.split(/[\\/]/);
3072
3199
  return segments.includes('..');
3073
3200
  };
3201
+
3074
3202
  const normalizeRelativePath = path => {
3075
3203
  const segments = path.split(/[\\/]/).filter(segment => segment && segment !== '.');
3076
3204
  if (segments.length === 0) {
@@ -3078,6 +3206,111 @@ const normalizeRelativePath = path => {
3078
3206
  }
3079
3207
  return segments.join('/');
3080
3208
  };
3209
+
3210
+ const isAbsoluteUri = value => {
3211
+ return /^[a-zA-Z][a-zA-Z0-9+.-]*:\/\//.test(value);
3212
+ };
3213
+ const executeReadFileTool = async (args, _options) => {
3214
+ const uri = typeof args.uri === 'string' ? args.uri : '';
3215
+ if (uri) {
3216
+ if (!isAbsoluteUri(uri)) {
3217
+ return JSON.stringify({
3218
+ error: 'Invalid argument: uri must be an absolute URI.'
3219
+ });
3220
+ }
3221
+ try {
3222
+ const content = await readFile(uri);
3223
+ return JSON.stringify({
3224
+ content,
3225
+ uri
3226
+ });
3227
+ } catch (error) {
3228
+ return JSON.stringify({
3229
+ error: String(error),
3230
+ uri
3231
+ });
3232
+ }
3233
+ }
3234
+ const filePath = typeof args.path === 'string' ? args.path : '';
3235
+ if (!filePath || isPathTraversalAttempt(filePath)) {
3236
+ return JSON.stringify({
3237
+ error: 'Access denied: path must be relative and stay within the open workspace folder.'
3238
+ });
3239
+ }
3240
+ const normalizedPath = normalizeRelativePath(filePath);
3241
+ try {
3242
+ const content = await readFile(normalizedPath);
3243
+ return JSON.stringify({
3244
+ content,
3245
+ path: normalizedPath
3246
+ });
3247
+ } catch (error) {
3248
+ return JSON.stringify({
3249
+ error: String(error),
3250
+ path: normalizedPath
3251
+ });
3252
+ }
3253
+ };
3254
+
3255
+ const maxPayloadLength = 40_000;
3256
+ const executeRenderHtmlTool = async (args, _options) => {
3257
+ const html = typeof args.html === 'string' ? args.html : '';
3258
+ const css = typeof args.css === 'string' ? args.css : '';
3259
+ const title = typeof args.title === 'string' ? args.title : '';
3260
+ if (!html) {
3261
+ return JSON.stringify({
3262
+ error: 'Missing required argument: html'
3263
+ });
3264
+ }
3265
+ if (html.length > maxPayloadLength || css.length > maxPayloadLength) {
3266
+ return JSON.stringify({
3267
+ error: 'Payload too large: keep html/css under 40,000 characters each.'
3268
+ });
3269
+ }
3270
+ return JSON.stringify({
3271
+ css,
3272
+ html,
3273
+ ok: true,
3274
+ title
3275
+ });
3276
+ };
3277
+
3278
+ const OnFileSystem = 'onFileSystem';
3279
+
3280
+ const executeFileSystemCommand = async (method, params, options) => {
3281
+ return executeProvider({
3282
+ assetDir: options.assetDir,
3283
+ event: OnFileSystem,
3284
+ method,
3285
+ noProviderFoundMessage: 'No file system provider found',
3286
+ params,
3287
+ platform: options.platform
3288
+ });
3289
+ };
3290
+
3291
+ const executeWriteFileTool = async (args, options) => {
3292
+ const filePath = typeof args.path === 'string' ? args.path : '';
3293
+ const content = typeof args.content === 'string' ? args.content : '';
3294
+ if (!filePath || isPathTraversalAttempt(filePath)) {
3295
+ return JSON.stringify({
3296
+ error: 'Access denied: path must be relative and stay within the open workspace folder.'
3297
+ });
3298
+ }
3299
+ const normalizedPath = normalizeRelativePath(filePath);
3300
+ try {
3301
+ await executeFileSystemCommand(FileSystemWriteFile, ['file', normalizedPath, content], options);
3302
+ return JSON.stringify({
3303
+ ok: true,
3304
+ path: normalizedPath
3305
+ });
3306
+ } catch (error) {
3307
+ return JSON.stringify({
3308
+ error: String(error),
3309
+ path: normalizedPath
3310
+ });
3311
+ }
3312
+ };
3313
+
3081
3314
  const parseToolArguments = rawArguments => {
3082
3315
  if (typeof rawArguments !== 'string') {
3083
3316
  return {};
@@ -3092,35 +3325,51 @@ const parseToolArguments = rawArguments => {
3092
3325
  return {};
3093
3326
  }
3094
3327
  };
3095
- const executeFileSystemCommand = async (method, params, options) => {
3096
- return executeProvider({
3097
- assetDir: options.assetDir,
3098
- event: OnFileSystem,
3099
- method,
3100
- noProviderFoundMessage: 'No file system provider found',
3101
- params,
3102
- platform: options.platform
3328
+
3329
+ const executeChatTool = async (name, rawArguments, options) => {
3330
+ const args = parseToolArguments(rawArguments);
3331
+ if (name === 'read_file') {
3332
+ return executeReadFileTool(args);
3333
+ }
3334
+ if (name === 'write_file') {
3335
+ return executeWriteFileTool(args, options);
3336
+ }
3337
+ if (name === 'list_files') {
3338
+ return executeListFilesTool(args);
3339
+ }
3340
+ if (name === 'getWorkspaceUri') {
3341
+ return executeGetWorkspaceUriTool();
3342
+ }
3343
+ if (name === 'render_html') {
3344
+ return executeRenderHtmlTool(args);
3345
+ }
3346
+ return JSON.stringify({
3347
+ error: `Unknown tool: ${name}`
3103
3348
  });
3104
3349
  };
3105
- const getBasicChatTools = () => {
3106
- return [{
3350
+
3351
+ const getReadFileTool = () => {
3352
+ return {
3107
3353
  function: {
3108
- description: 'Read UTF-8 text content from a file inside the currently open workspace folder.',
3354
+ description: 'Read UTF-8 text content from a file inside the currently open workspace folder. Only pass an absolute URI.',
3109
3355
  name: 'read_file',
3110
3356
  parameters: {
3111
3357
  additionalProperties: false,
3112
3358
  properties: {
3113
- path: {
3114
- description: 'Relative file path within the workspace (for example: src/index.ts).',
3359
+ uri: {
3360
+ description: 'Absolute file URI within the workspace (for example: file:///workspace/src/index.ts).',
3115
3361
  type: 'string'
3116
3362
  }
3117
3363
  },
3118
- required: ['path'],
3364
+ required: ['uri'],
3119
3365
  type: 'object'
3120
3366
  }
3121
3367
  },
3122
3368
  type: 'function'
3123
- }, {
3369
+ };
3370
+ };
3371
+ const getWriteFileTool = () => {
3372
+ return {
3124
3373
  function: {
3125
3374
  description: 'Write UTF-8 text content to a file inside the currently open workspace folder.',
3126
3375
  name: 'write_file',
@@ -3141,93 +3390,72 @@ const getBasicChatTools = () => {
3141
3390
  }
3142
3391
  },
3143
3392
  type: 'function'
3144
- }, {
3393
+ };
3394
+ };
3395
+ const getListFilesTool = () => {
3396
+ return {
3145
3397
  function: {
3146
- description: 'List direct children (files and folders) for a folder inside the currently open workspace folder.',
3398
+ description: 'List direct children (files and folders) for a folder URI inside the currently open workspace folder. Only pass an absolute URI.',
3147
3399
  name: 'list_files',
3148
3400
  parameters: {
3149
3401
  additionalProperties: false,
3150
3402
  properties: {
3151
- path: {
3152
- description: 'Relative folder path within the workspace. Use "." for the workspace root.',
3403
+ uri: {
3404
+ description: 'Absolute folder URI within the workspace (for example: file:///workspace/src).',
3153
3405
  type: 'string'
3154
3406
  }
3155
3407
  },
3408
+ required: ['uri'],
3156
3409
  type: 'object'
3157
3410
  }
3158
3411
  },
3159
3412
  type: 'function'
3160
- }];
3413
+ };
3161
3414
  };
3162
- const executeChatTool = async (name, rawArguments, options) => {
3163
- const args = parseToolArguments(rawArguments);
3164
- if (name === 'read_file') {
3165
- const filePath = typeof args.path === 'string' ? args.path : '';
3166
- if (!filePath || isPathTraversalAttempt(filePath)) {
3167
- return JSON.stringify({
3168
- error: 'Access denied: path must be relative and stay within the open workspace folder.'
3169
- });
3170
- }
3171
- const normalizedPath = normalizeRelativePath(filePath);
3172
- try {
3173
- const content = await readFile(normalizedPath);
3174
- return JSON.stringify({
3175
- content,
3176
- path: normalizedPath
3177
- });
3178
- } catch (error) {
3179
- return JSON.stringify({
3180
- error: String(error),
3181
- path: normalizedPath
3182
- });
3183
- }
3184
- }
3185
- if (name === 'write_file') {
3186
- const filePath = typeof args.path === 'string' ? args.path : '';
3187
- const content = typeof args.content === 'string' ? args.content : '';
3188
- if (!filePath || isPathTraversalAttempt(filePath)) {
3189
- return JSON.stringify({
3190
- error: 'Access denied: path must be relative and stay within the open workspace folder.'
3191
- });
3192
- }
3193
- const normalizedPath = normalizeRelativePath(filePath);
3194
- try {
3195
- await executeFileSystemCommand(FileSystemWriteFile, ['file', normalizedPath, content], options);
3196
- return JSON.stringify({
3197
- ok: true,
3198
- path: normalizedPath
3199
- });
3200
- } catch (error) {
3201
- return JSON.stringify({
3202
- error: String(error),
3203
- path: normalizedPath
3204
- });
3205
- }
3206
- }
3207
- if (name === 'list_files') {
3208
- const folderPath = typeof args.path === 'string' && args.path ? args.path : '.';
3209
- if (isPathTraversalAttempt(folderPath)) {
3210
- return JSON.stringify({
3211
- error: 'Access denied: path must be relative and stay within the open workspace folder.'
3212
- });
3213
- }
3214
- const normalizedPath = normalizeRelativePath(folderPath);
3215
- try {
3216
- const entries = await invoke('FileSystem.readDirWithFileTypes', normalizedPath);
3217
- return JSON.stringify({
3218
- entries,
3219
- path: normalizedPath
3220
- });
3221
- } catch (error) {
3222
- return JSON.stringify({
3223
- error: String(error),
3224
- path: normalizedPath
3225
- });
3226
- }
3227
- }
3228
- return JSON.stringify({
3229
- error: `Unknown tool: ${name}`
3230
- });
3415
+ const getGetWorkspaceUriTool = () => {
3416
+ return {
3417
+ function: {
3418
+ description: 'Get the URI of the currently open workspace folder.',
3419
+ name: 'getWorkspaceUri',
3420
+ parameters: {
3421
+ additionalProperties: false,
3422
+ properties: {},
3423
+ type: 'object'
3424
+ }
3425
+ },
3426
+ type: 'function'
3427
+ };
3428
+ };
3429
+ const getRenderHtmlTool = () => {
3430
+ return {
3431
+ function: {
3432
+ description: 'Render custom HTML and optional CSS directly in the chat tool call list using native chat UI rendering. Use this for structured cards, tables, and small dashboards. After calling this tool, do not repeat the same HTML, data table, or long content again as plain text unless the user explicitly asks for a text-only version.',
3433
+ name: 'render_html',
3434
+ parameters: {
3435
+ additionalProperties: false,
3436
+ properties: {
3437
+ css: {
3438
+ description: 'Optional CSS string applied inside the preview document.',
3439
+ type: 'string'
3440
+ },
3441
+ html: {
3442
+ description: 'HTML string to render in the preview document.',
3443
+ type: 'string'
3444
+ },
3445
+ title: {
3446
+ description: 'Optional short title for the preview.',
3447
+ type: 'string'
3448
+ }
3449
+ },
3450
+ required: ['html'],
3451
+ type: 'object'
3452
+ }
3453
+ },
3454
+ type: 'function'
3455
+ };
3456
+ };
3457
+ const getBasicChatTools = () => {
3458
+ return [getReadFileTool(), getWriteFileTool(), getListFilesTool(), getGetWorkspaceUriTool(), getRenderHtmlTool()];
3231
3459
  };
3232
3460
 
3233
3461
  const getClientRequestIdHeader = () => {
@@ -4049,6 +4277,12 @@ const getOpenApiErrorMessage = errorResult => {
4049
4277
  const errorMessage = errorResult.errorMessage?.trim();
4050
4278
  const hasErrorCode = typeof errorResult.errorCode === 'string' && errorResult.errorCode.length > 0;
4051
4279
  const hasErrorType = typeof errorResult.errorType === 'string' && errorResult.errorType.length > 0;
4280
+
4281
+ // Provide a concise, user-friendly message when OpenAI reports an invalid API key.
4282
+ if (errorResult.errorCode === 'invalid_api_key') {
4283
+ const status = typeof errorResult.statusCode === 'number' ? errorResult.statusCode : 401;
4284
+ return `OpenAI request failed (status ${status}): Invalid API key. Please verify your OpenAI API key in Chat settings.`;
4285
+ }
4052
4286
  if (errorResult.statusCode === 429) {
4053
4287
  let prefix = 'OpenAI rate limit exceeded (429)';
4054
4288
  if (hasErrorCode) {
@@ -5641,8 +5875,8 @@ const openMockSession = async (state, mockSessionId, mockChatMessages) => {
5641
5875
  };
5642
5876
  };
5643
5877
 
5644
- const getCss = (composerHeight, listItemHeight, chatMessageFontSize, chatMessageLineHeight, chatMessageFontFamily) => {
5645
- return `:root {
5878
+ const getCss = (composerHeight, listItemHeight, chatMessageFontSize, chatMessageLineHeight, chatMessageFontFamily, renderHtmlCss) => {
5879
+ const baseCss = `:root {
5646
5880
  --ChatInputBoxHeight: ${composerHeight}px;
5647
5881
  --ChatListItemHeight: ${listItemHeight}px;
5648
5882
  --ChatMessageFontSize: ${chatMessageFontSize}px;
@@ -5676,7 +5910,42 @@ const getCss = (composerHeight, listItemHeight, chatMessageFontSize, chatMessage
5676
5910
  .ChatToolCallReadFileLink {
5677
5911
  color: var(--vscode-textLink-foreground);
5678
5912
  text-decoration: underline;
5913
+ }
5914
+
5915
+ .ChatToolCallRenderHtmlLabel {
5916
+ margin-bottom: 6px;
5917
+ color: var(--vscode-descriptionForeground);
5918
+ font-size: 12px;
5919
+ }
5920
+
5921
+ .ChatToolCallRenderHtmlContent {
5922
+ border: 1px solid var(--vscode-editorWidget-border);
5923
+ border-radius: 6px;
5924
+ background: var(--vscode-editor-background);
5925
+ overflow: hidden;
5926
+ }
5927
+
5928
+ .ChatToolCallRenderHtmlBody {
5929
+ min-height: 180px;
5930
+ padding: 12px;
5931
+ }
5932
+
5933
+ .ChatToolCallRenderHtmlBody * {
5934
+ box-sizing: border-box;
5935
+ }
5936
+
5937
+ .ChatMessageLink {
5938
+ color: #4d94ff;
5939
+ text-decoration: underline;
5940
+ cursor: pointer;
5679
5941
  }`;
5942
+ if (!renderHtmlCss.trim()) {
5943
+ return baseCss;
5944
+ }
5945
+ return `${baseCss}
5946
+
5947
+ /* render_html tool css */
5948
+ ${renderHtmlCss}`;
5680
5949
  };
5681
5950
 
5682
5951
  // TODO render things like scrollbar height,scrollbar offset, textarea height,
@@ -5689,9 +5958,12 @@ const renderCss = (oldState, newState) => {
5689
5958
  chatMessageLineHeight,
5690
5959
  composerHeight,
5691
5960
  listItemHeight,
5961
+ selectedSessionId,
5962
+ sessions,
5692
5963
  uid
5693
5964
  } = newState;
5694
- const css = getCss(composerHeight, listItemHeight, chatMessageFontSize, chatMessageLineHeight, chatMessageFontFamily);
5965
+ const renderHtmlCss = getRenderHtmlCss(sessions, selectedSessionId);
5966
+ const css = getCss(composerHeight, listItemHeight, chatMessageFontSize, chatMessageLineHeight, chatMessageFontFamily, renderHtmlCss);
5695
5967
  return [SetCss, uid, css];
5696
5968
  };
5697
5969
 
@@ -5746,6 +6018,10 @@ const ChatMessageContent = 'ChatMessageContent';
5746
6018
  const ChatToolCalls = 'ChatToolCalls';
5747
6019
  const ChatToolCallsLabel = 'ChatToolCallsLabel';
5748
6020
  const ChatToolCallReadFileLink = 'ChatToolCallReadFileLink';
6021
+ const ChatToolCallRenderHtmlLabel = 'ChatToolCallRenderHtmlLabel';
6022
+ const ChatToolCallRenderHtmlContent = 'ChatToolCallRenderHtmlContent';
6023
+ const ChatToolCallRenderHtmlBody = 'ChatToolCallRenderHtmlBody';
6024
+ const ChatMessageLink = 'ChatMessageLink';
5749
6025
  const ChatOrderedList = 'ChatOrderedList';
5750
6026
  const ChatOrderedListItem = 'ChatOrderedListItem';
5751
6027
  const MessageUser = 'MessageUser';
@@ -5970,6 +6246,47 @@ const getChatHeaderDomDetailMode = selectedSessionTitle => {
5970
6246
  }, text(selectedSessionTitle), ...getChatHeaderActionsDom()];
5971
6247
  };
5972
6248
 
6249
+ const getInlineNodeDom = inlineNode => {
6250
+ if (inlineNode.type === 'text') {
6251
+ return [text(inlineNode.text)];
6252
+ }
6253
+ return [{
6254
+ childCount: 1,
6255
+ className: ChatMessageLink,
6256
+ href: inlineNode.href,
6257
+ rel: 'noopener noreferrer',
6258
+ target: '_blank',
6259
+ title: inlineNode.href,
6260
+ type: A
6261
+ }, text(inlineNode.text)];
6262
+ };
6263
+
6264
+ const getOrderedListItemDom = item => {
6265
+ return [{
6266
+ childCount: item.children.length,
6267
+ className: ChatOrderedListItem,
6268
+ type: Li
6269
+ }, ...item.children.flatMap(getInlineNodeDom)];
6270
+ };
6271
+ const getMessageNodeDom = node => {
6272
+ if (node.type === 'text') {
6273
+ return [{
6274
+ childCount: node.children.length,
6275
+ className: Markdown,
6276
+ type: P
6277
+ }, ...node.children.flatMap(getInlineNodeDom)];
6278
+ }
6279
+ return [{
6280
+ childCount: node.items.length,
6281
+ className: ChatOrderedList,
6282
+ type: Ol
6283
+ }, ...node.items.flatMap(getOrderedListItemDom)];
6284
+ };
6285
+
6286
+ const getMessageContentDom = nodes => {
6287
+ return nodes.flatMap(getMessageNodeDom);
6288
+ };
6289
+
5973
6290
  const getMissingApiKeyDom = ({
5974
6291
  getApiKeyText,
5975
6292
  inputName,
@@ -6118,7 +6435,7 @@ const getReadFileTarget = rawArguments => {
6118
6435
  if (!title) {
6119
6436
  return undefined;
6120
6437
  }
6121
- // `read_file` tool calls usually provide a relative `path`; pass it through so UI clicks can open the file.
6438
+ // `read_file` tool calls now use absolute `uri`; keep `path` as a legacy fallback for old transcripts.
6122
6439
  const clickableUri = uriValue || pathValue;
6123
6440
  return {
6124
6441
  clickableUri,
@@ -6126,7 +6443,7 @@ const getReadFileTarget = rawArguments => {
6126
6443
  };
6127
6444
  };
6128
6445
 
6129
- const getToolCallStatusLabel$1 = toolCall => {
6446
+ const getToolCallStatusLabel = toolCall => {
6130
6447
  if (toolCall.status === 'not-found') {
6131
6448
  return ' (not-found)';
6132
6449
  }
@@ -6146,7 +6463,7 @@ const getToolCallReadFileVirtualDom = toolCall => {
6146
6463
  }
6147
6464
  const fileName = getFileNameFromUri(target.title);
6148
6465
  const toolNameLabel = `${toolCall.name} `;
6149
- const statusLabel = getToolCallStatusLabel$1(toolCall);
6466
+ const statusLabel = getToolCallStatusLabel(toolCall);
6150
6467
  const fileNameClickableProps = target.clickableUri ? {
6151
6468
  'data-uri': target.clickableUri,
6152
6469
  onClick: HandleClickReadFile
@@ -6168,18 +6485,305 @@ const getToolCallReadFileVirtualDom = toolCall => {
6168
6485
  }, text(fileName), ...(statusLabel ? [text(statusLabel)] : [])];
6169
6486
  };
6170
6487
 
6171
- const getToolCallStatusLabel = toolCall => {
6172
- if (toolCall.status === 'not-found') {
6173
- return ' (not-found)';
6488
+ const maxHtmlLength = 40_000;
6489
+ const tokenRegex = /<!--[\s\S]*?-->|<\/?[a-zA-Z][\w:-]*(?:\s[^<>]*?)?>|[^<]+/g;
6490
+ const attributeRegex = /([^\s=/>]+)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'=<>`]+)))?/g;
6491
+ const inlineTags = new Set(['a', 'abbr', 'b', 'code', 'em', 'i', 'label', 'small', 'span', 'strong', 'sub', 'sup', 'u']);
6492
+ const voidElements = new Set(['area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'param', 'source', 'track', 'wbr']);
6493
+ const sanitizeHtml = value => {
6494
+ return value.slice(0, maxHtmlLength).replaceAll(/<script\b[\s\S]*?<\/script>/gi, '').replaceAll(/<style\b[\s\S]*?<\/style>/gi, '').replaceAll(/<head\b[\s\S]*?<\/head>/gi, '').replaceAll(/<meta\b[^>]*>/gi, '').replaceAll(/<link\b[^>]*>/gi, '');
6495
+ };
6496
+ const decodeEntities = value => {
6497
+ return value.replaceAll('&nbsp;', ' ').replaceAll('&quot;', '"').replaceAll('&#39;', "'").replaceAll('&lt;', '<').replaceAll('&gt;', '>').replaceAll('&amp;', '&');
6498
+ };
6499
+ const parseAttributes = token => {
6500
+ const withoutTag = token.replace(/^<\/?\s*[a-zA-Z][\w:-]*/, '').replace(/\/?\s*>$/, '').trim();
6501
+ if (!withoutTag) {
6502
+ return Object.create(null);
6174
6503
  }
6175
- if (toolCall.status === 'error') {
6176
- if (toolCall.errorMessage) {
6177
- return ` (error: ${toolCall.errorMessage})`;
6504
+ const attributes = Object.create(null);
6505
+ const matches = withoutTag.matchAll(attributeRegex);
6506
+ for (const match of matches) {
6507
+ const name = String(match[1] || '').toLowerCase();
6508
+ if (!name || name.startsWith('on')) {
6509
+ continue;
6178
6510
  }
6179
- return ' (error)';
6511
+ const value = String(match[2] ?? match[3] ?? match[4] ?? '');
6512
+ attributes[name] = decodeEntities(value);
6180
6513
  }
6181
- return '';
6514
+ return attributes;
6182
6515
  };
6516
+ const parseHtml = value => {
6517
+ const root = {
6518
+ attributes: Object.create(null),
6519
+ children: [],
6520
+ tagName: 'root',
6521
+ type: 'element'
6522
+ };
6523
+ const stack = [root];
6524
+ const matches = sanitizeHtml(value).match(tokenRegex);
6525
+ if (!matches) {
6526
+ return [];
6527
+ }
6528
+ for (const token of matches) {
6529
+ if (token.startsWith('<!--')) {
6530
+ continue;
6531
+ }
6532
+ if (token.startsWith('</')) {
6533
+ const closingTagName = token.slice(2, -1).trim().toLowerCase();
6534
+ while (stack.length > 1) {
6535
+ const top = stack.at(-1);
6536
+ if (!top) {
6537
+ break;
6538
+ }
6539
+ stack.pop();
6540
+ if (top.tagName === closingTagName) {
6541
+ break;
6542
+ }
6543
+ }
6544
+ continue;
6545
+ }
6546
+ if (token.startsWith('<')) {
6547
+ const openTagNameMatch = /^<\s*([a-zA-Z][\w:-]*)/.exec(token);
6548
+ if (!openTagNameMatch) {
6549
+ continue;
6550
+ }
6551
+ const tagName = openTagNameMatch[1].toLowerCase();
6552
+ const elementNode = {
6553
+ attributes: parseAttributes(token),
6554
+ children: [],
6555
+ tagName,
6556
+ type: 'element'
6557
+ };
6558
+ const parent = stack.at(-1);
6559
+ if (!parent) {
6560
+ continue;
6561
+ }
6562
+ parent.children.push(elementNode);
6563
+ const selfClosing = token.endsWith('/>') || voidElements.has(tagName);
6564
+ if (!selfClosing) {
6565
+ stack.push(elementNode);
6566
+ }
6567
+ continue;
6568
+ }
6569
+ const decoded = decodeEntities(token);
6570
+ if (!decoded) {
6571
+ continue;
6572
+ }
6573
+ const parent = stack.at(-1);
6574
+ if (!parent) {
6575
+ continue;
6576
+ }
6577
+ parent.children.push({
6578
+ type: 'text',
6579
+ value: decoded
6580
+ });
6581
+ }
6582
+ return root.children;
6583
+ };
6584
+ const getElementType = tagName => {
6585
+ switch (tagName) {
6586
+ case 'a':
6587
+ return A;
6588
+ case 'abbr':
6589
+ return Abbr;
6590
+ case 'article':
6591
+ return Article;
6592
+ case 'aside':
6593
+ return Aside;
6594
+ case 'audio':
6595
+ return Audio;
6596
+ case 'br':
6597
+ return Br;
6598
+ case 'button':
6599
+ return Button$1;
6600
+ case 'code':
6601
+ return Code;
6602
+ case 'col':
6603
+ return Col;
6604
+ case 'colgroup':
6605
+ return ColGroup;
6606
+ case 'dd':
6607
+ return Dd;
6608
+ case 'dl':
6609
+ return Dl;
6610
+ case 'dt':
6611
+ return Dt;
6612
+ case 'em':
6613
+ return Em;
6614
+ case 'figcaption':
6615
+ return Figcaption;
6616
+ case 'figure':
6617
+ return Figure;
6618
+ case 'footer':
6619
+ return Footer;
6620
+ case 'h1':
6621
+ return H1;
6622
+ case 'h2':
6623
+ return H2;
6624
+ case 'h3':
6625
+ return H3;
6626
+ case 'h4':
6627
+ return H4;
6628
+ case 'h5':
6629
+ return H5;
6630
+ case 'h6':
6631
+ return H6;
6632
+ case 'header':
6633
+ return Header;
6634
+ case 'hr':
6635
+ return Hr;
6636
+ case 'i':
6637
+ return I;
6638
+ case 'img':
6639
+ return Img;
6640
+ case 'input':
6641
+ return Input;
6642
+ case 'label':
6643
+ return Label$1;
6644
+ case 'li':
6645
+ return Li;
6646
+ case 'main':
6647
+ return Main;
6648
+ case 'nav':
6649
+ return Nav;
6650
+ case 'ol':
6651
+ return Ol;
6652
+ case 'option':
6653
+ return Option$1;
6654
+ case 'p':
6655
+ return P;
6656
+ case 'pre':
6657
+ return Pre;
6658
+ case 'section':
6659
+ return Section;
6660
+ case 'select':
6661
+ return Select$1;
6662
+ case 'span':
6663
+ return Span;
6664
+ case 'strong':
6665
+ return Strong;
6666
+ case 'table':
6667
+ return Table;
6668
+ case 'tbody':
6669
+ return TBody;
6670
+ case 'td':
6671
+ return Td;
6672
+ case 'textarea':
6673
+ return TextArea;
6674
+ case 'tfoot':
6675
+ return Tfoot;
6676
+ case 'th':
6677
+ return Th;
6678
+ case 'thead':
6679
+ return THead;
6680
+ case 'tr':
6681
+ return Tr;
6682
+ case 'ul':
6683
+ return Ul;
6684
+ default:
6685
+ return inlineTags.has(tagName) ? Span : Div;
6686
+ }
6687
+ };
6688
+ const normalizeUrl = url => {
6689
+ return url.toLowerCase().startsWith('javascript:') ? '#' : url;
6690
+ };
6691
+ const getElementAttributes = node => {
6692
+ const attributes = {};
6693
+ const className = node.attributes.class || node.attributes.classname;
6694
+ if (className) {
6695
+ attributes.className = className;
6696
+ }
6697
+ if (node.attributes.style) {
6698
+ attributes.style = node.attributes.style;
6699
+ }
6700
+ if (node.attributes.id) {
6701
+ attributes.id = node.attributes.id;
6702
+ }
6703
+ if (node.attributes.name) {
6704
+ attributes.name = node.attributes.name;
6705
+ }
6706
+ if (node.attributes.placeholder) {
6707
+ attributes.placeholder = node.attributes.placeholder;
6708
+ }
6709
+ if (node.attributes.title) {
6710
+ attributes.title = node.attributes.title;
6711
+ }
6712
+ if (node.attributes.value) {
6713
+ attributes.value = node.attributes.value;
6714
+ }
6715
+ if (node.attributes.href) {
6716
+ attributes.href = normalizeUrl(node.attributes.href);
6717
+ }
6718
+ if (node.attributes.src) {
6719
+ attributes.src = normalizeUrl(node.attributes.src);
6720
+ }
6721
+ if (node.attributes.target) {
6722
+ attributes.target = node.attributes.target;
6723
+ }
6724
+ if (node.attributes.rel) {
6725
+ attributes.rel = node.attributes.rel;
6726
+ }
6727
+ if ('checked' in node.attributes) {
6728
+ attributes.checked = node.attributes.checked !== 'false';
6729
+ }
6730
+ if ('disabled' in node.attributes) {
6731
+ attributes.disabled = node.attributes.disabled !== 'false';
6732
+ }
6733
+ if ('readonly' in node.attributes) {
6734
+ attributes.readOnly = node.attributes.readonly !== 'false';
6735
+ }
6736
+ return attributes;
6737
+ };
6738
+ const toVirtualDom = node => {
6739
+ if (node.type === 'text') {
6740
+ return [text(node.value)];
6741
+ }
6742
+ const children = node.children.flatMap(toVirtualDom);
6743
+ return [{
6744
+ childCount: node.children.length,
6745
+ ...getElementAttributes(node),
6746
+ type: getElementType(node.tagName)
6747
+ }, ...children];
6748
+ };
6749
+ const parseHtmlToVirtualDomWithRootCount = value => {
6750
+ const rootNodes = parseHtml(value);
6751
+ return {
6752
+ rootChildCount: rootNodes.length,
6753
+ virtualDom: rootNodes.flatMap(toVirtualDom)
6754
+ };
6755
+ };
6756
+
6757
+ const getToolCallRenderHtmlVirtualDom = toolCall => {
6758
+ const parsed = parseRenderHtmlArguments(toolCall.arguments);
6759
+ if (!parsed) {
6760
+ return [];
6761
+ }
6762
+ const statusLabel = getToolCallStatusLabel(toolCall);
6763
+ const label = `${toolCall.name}: ${parsed.title}${statusLabel}`;
6764
+ const parsedHtml = parseHtmlToVirtualDomWithRootCount(parsed.html);
6765
+ const {
6766
+ rootChildCount
6767
+ } = parsedHtml;
6768
+ return [{
6769
+ childCount: 2,
6770
+ className: ChatOrderedListItem,
6771
+ type: Li
6772
+ }, {
6773
+ childCount: 1,
6774
+ className: ChatToolCallRenderHtmlLabel,
6775
+ type: Div
6776
+ }, text(label), {
6777
+ childCount: 1,
6778
+ className: ChatToolCallRenderHtmlContent,
6779
+ type: Div
6780
+ }, {
6781
+ childCount: rootChildCount,
6782
+ className: ChatToolCallRenderHtmlBody,
6783
+ type: Div
6784
+ }, ...parsedHtml.virtualDom];
6785
+ };
6786
+
6183
6787
  const getToolCallDom = toolCall => {
6184
6788
  if (toolCall.name === 'read_file') {
6185
6789
  const virtualDom = getToolCallReadFileVirtualDom(toolCall);
@@ -6187,6 +6791,12 @@ const getToolCallDom = toolCall => {
6187
6791
  return virtualDom;
6188
6792
  }
6189
6793
  }
6794
+ if (toolCall.name === 'render_html') {
6795
+ const virtualDom = getToolCallRenderHtmlVirtualDom(toolCall);
6796
+ if (virtualDom.length > 0) {
6797
+ return virtualDom;
6798
+ }
6799
+ }
6190
6800
  const argumentPreview = getToolCallArgumentPreview(toolCall.arguments);
6191
6801
  const label = `${toolCall.name} ${argumentPreview}${getToolCallStatusLabel(toolCall)}`;
6192
6802
  return [{
@@ -6216,10 +6826,50 @@ const getToolCallsDom = message => {
6216
6826
  };
6217
6827
 
6218
6828
  const orderedListItemRegex = /^\s*\d+\.\s+(.*)$/;
6829
+ const markdownLinkRegex = /\[([^\]]+)\]\(([^)]+)\)/g;
6830
+ const parseInlineNodes = value => {
6831
+ const matches = value.matchAll(markdownLinkRegex);
6832
+ const nodes = [];
6833
+ let lastIndex = 0;
6834
+ for (const match of matches) {
6835
+ const fullMatch = match[0];
6836
+ const linkText = match[1];
6837
+ const href = match[2];
6838
+ const index = match.index ?? 0;
6839
+ if (index > lastIndex) {
6840
+ nodes.push({
6841
+ text: value.slice(lastIndex, index),
6842
+ type: 'text'
6843
+ });
6844
+ }
6845
+ nodes.push({
6846
+ href,
6847
+ text: linkText,
6848
+ type: 'link'
6849
+ });
6850
+ lastIndex = index + fullMatch.length;
6851
+ }
6852
+ if (lastIndex < value.length) {
6853
+ nodes.push({
6854
+ text: value.slice(lastIndex),
6855
+ type: 'text'
6856
+ });
6857
+ }
6858
+ if (nodes.length === 0) {
6859
+ return [{
6860
+ text: value,
6861
+ type: 'text'
6862
+ }];
6863
+ }
6864
+ return nodes;
6865
+ };
6219
6866
  const parseMessageContent = rawMessage => {
6220
6867
  if (rawMessage === '') {
6221
6868
  return [{
6222
- text: '',
6869
+ children: [{
6870
+ text: '',
6871
+ type: 'text'
6872
+ }],
6223
6873
  type: 'text'
6224
6874
  }];
6225
6875
  }
@@ -6232,7 +6882,7 @@ const parseMessageContent = rawMessage => {
6232
6882
  return;
6233
6883
  }
6234
6884
  nodes.push({
6235
- text: paragraphLines.join('\n'),
6885
+ children: parseInlineNodes(paragraphLines.join('\n')),
6236
6886
  type: 'text'
6237
6887
  });
6238
6888
  paragraphLines = [];
@@ -6257,7 +6907,7 @@ const parseMessageContent = rawMessage => {
6257
6907
  if (match) {
6258
6908
  flushParagraph();
6259
6909
  listItems.push({
6260
- text: match[1],
6910
+ children: parseInlineNodes(match[1]),
6261
6911
  type: 'list-item'
6262
6912
  });
6263
6913
  continue;
@@ -6268,32 +6918,13 @@ const parseMessageContent = rawMessage => {
6268
6918
  flushList();
6269
6919
  flushParagraph();
6270
6920
  return nodes.length === 0 ? [{
6271
- text: '',
6921
+ children: [{
6922
+ text: '',
6923
+ type: 'text'
6924
+ }],
6272
6925
  type: 'text'
6273
6926
  }] : nodes;
6274
6927
  };
6275
- const getMessageContentDom = nodes => {
6276
- return nodes.flatMap(node => {
6277
- if (node.type === 'text') {
6278
- return [{
6279
- childCount: 1,
6280
- className: Markdown,
6281
- type: P
6282
- }, text(node.text)];
6283
- }
6284
- return [{
6285
- childCount: node.items.length,
6286
- className: ChatOrderedList,
6287
- type: Ol
6288
- }, ...node.items.flatMap(item => {
6289
- return [{
6290
- childCount: 1,
6291
- className: ChatOrderedListItem,
6292
- type: Li
6293
- }, text(item.text)];
6294
- })];
6295
- });
6296
- };
6297
6928
 
6298
6929
  const getChatMessageDom = (message, openRouterApiKeyInput, openApiApiKeyInput = '', openRouterApiKeyState = 'idle') => {
6299
6930
  const roleClassName = message.role === 'user' ? MessageUser : MessageAssistant;
@@ -6630,30 +7261,22 @@ const saveState = state => {
6630
7261
  const {
6631
7262
  chatListScrollTop,
6632
7263
  composerValue,
6633
- height,
6634
7264
  messagesScrollTop,
6635
7265
  nextMessageId,
6636
7266
  renamingSessionId,
6637
7267
  selectedModelId,
6638
7268
  selectedSessionId,
6639
- viewMode,
6640
- width,
6641
- x,
6642
- y
7269
+ viewMode
6643
7270
  } = state;
6644
7271
  return {
6645
7272
  chatListScrollTop,
6646
7273
  composerValue,
6647
- height,
6648
7274
  messagesScrollTop,
6649
7275
  nextMessageId,
6650
7276
  renamingSessionId,
6651
7277
  selectedModelId,
6652
7278
  selectedSessionId,
6653
- viewMode,
6654
- width,
6655
- x,
6656
- y
7279
+ viewMode
6657
7280
  };
6658
7281
  };
6659
7282
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lvce-editor/chat-view",
3
- "version": "2.8.0",
3
+ "version": "2.10.0",
4
4
  "description": "Chat View Worker",
5
5
  "repository": {
6
6
  "type": "git",