@lvce-editor/chat-view 2.9.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;
@@ -3054,6 +3145,43 @@ const getMockOpenRouterAssistantText = async (messages, modelId, openRouterApiBa
3054
3145
  }
3055
3146
  };
3056
3147
 
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
+ };
3184
+
3057
3185
  const isPathTraversalAttempt = path => {
3058
3186
  if (!path) {
3059
3187
  return false;
@@ -3079,18 +3207,41 @@ const normalizeRelativePath = path => {
3079
3207
  return segments.join('/');
3080
3208
  };
3081
3209
 
3082
- const executeListFilesTool = async (args, _options) => {
3083
- const folderPath = typeof args.path === 'string' && args.path ? args.path : '.';
3084
- if (isPathTraversalAttempt(folderPath)) {
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)) {
3085
3236
  return JSON.stringify({
3086
3237
  error: 'Access denied: path must be relative and stay within the open workspace folder.'
3087
3238
  });
3088
3239
  }
3089
- const normalizedPath = normalizeRelativePath(folderPath);
3240
+ const normalizedPath = normalizeRelativePath(filePath);
3090
3241
  try {
3091
- const entries = await invoke('FileSystem.readDirWithFileTypes', normalizedPath);
3242
+ const content = await readFile(normalizedPath);
3092
3243
  return JSON.stringify({
3093
- entries,
3244
+ content,
3094
3245
  path: normalizedPath
3095
3246
  });
3096
3247
  } catch (error) {
@@ -3101,26 +3252,27 @@ const executeListFilesTool = async (args, _options) => {
3101
3252
  }
3102
3253
  };
3103
3254
 
3104
- const executeReadFileTool = async (args, _options) => {
3105
- const filePath = typeof args.path === 'string' ? args.path : '';
3106
- if (!filePath || isPathTraversalAttempt(filePath)) {
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) {
3107
3261
  return JSON.stringify({
3108
- error: 'Access denied: path must be relative and stay within the open workspace folder.'
3262
+ error: 'Missing required argument: html'
3109
3263
  });
3110
3264
  }
3111
- const normalizedPath = normalizeRelativePath(filePath);
3112
- try {
3113
- const content = await readFile(normalizedPath);
3114
- return JSON.stringify({
3115
- content,
3116
- path: normalizedPath
3117
- });
3118
- } catch (error) {
3265
+ if (html.length > maxPayloadLength || css.length > maxPayloadLength) {
3119
3266
  return JSON.stringify({
3120
- error: String(error),
3121
- path: normalizedPath
3267
+ error: 'Payload too large: keep html/css under 40,000 characters each.'
3122
3268
  });
3123
3269
  }
3270
+ return JSON.stringify({
3271
+ css,
3272
+ html,
3273
+ ok: true,
3274
+ title
3275
+ });
3124
3276
  };
3125
3277
 
3126
3278
  const OnFileSystem = 'onFileSystem';
@@ -3185,30 +3337,39 @@ const executeChatTool = async (name, rawArguments, options) => {
3185
3337
  if (name === 'list_files') {
3186
3338
  return executeListFilesTool(args);
3187
3339
  }
3340
+ if (name === 'getWorkspaceUri') {
3341
+ return executeGetWorkspaceUriTool();
3342
+ }
3343
+ if (name === 'render_html') {
3344
+ return executeRenderHtmlTool(args);
3345
+ }
3188
3346
  return JSON.stringify({
3189
3347
  error: `Unknown tool: ${name}`
3190
3348
  });
3191
3349
  };
3192
3350
 
3193
- const getBasicChatTools = () => {
3194
- return [{
3351
+ const getReadFileTool = () => {
3352
+ return {
3195
3353
  function: {
3196
- 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.',
3197
3355
  name: 'read_file',
3198
3356
  parameters: {
3199
3357
  additionalProperties: false,
3200
3358
  properties: {
3201
- path: {
3202
- 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).',
3203
3361
  type: 'string'
3204
3362
  }
3205
3363
  },
3206
- required: ['path'],
3364
+ required: ['uri'],
3207
3365
  type: 'object'
3208
3366
  }
3209
3367
  },
3210
3368
  type: 'function'
3211
- }, {
3369
+ };
3370
+ };
3371
+ const getWriteFileTool = () => {
3372
+ return {
3212
3373
  function: {
3213
3374
  description: 'Write UTF-8 text content to a file inside the currently open workspace folder.',
3214
3375
  name: 'write_file',
@@ -3229,23 +3390,72 @@ const getBasicChatTools = () => {
3229
3390
  }
3230
3391
  },
3231
3392
  type: 'function'
3232
- }, {
3393
+ };
3394
+ };
3395
+ const getListFilesTool = () => {
3396
+ return {
3233
3397
  function: {
3234
- 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.',
3235
3399
  name: 'list_files',
3236
3400
  parameters: {
3237
3401
  additionalProperties: false,
3238
3402
  properties: {
3239
- path: {
3240
- 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).',
3241
3405
  type: 'string'
3242
3406
  }
3243
3407
  },
3408
+ required: ['uri'],
3244
3409
  type: 'object'
3245
3410
  }
3246
3411
  },
3247
3412
  type: 'function'
3248
- }];
3413
+ };
3414
+ };
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()];
3249
3459
  };
3250
3460
 
3251
3461
  const getClientRequestIdHeader = () => {
@@ -5665,8 +5875,8 @@ const openMockSession = async (state, mockSessionId, mockChatMessages) => {
5665
5875
  };
5666
5876
  };
5667
5877
 
5668
- const getCss = (composerHeight, listItemHeight, chatMessageFontSize, chatMessageLineHeight, chatMessageFontFamily) => {
5669
- return `:root {
5878
+ const getCss = (composerHeight, listItemHeight, chatMessageFontSize, chatMessageLineHeight, chatMessageFontFamily, renderHtmlCss) => {
5879
+ const baseCss = `:root {
5670
5880
  --ChatInputBoxHeight: ${composerHeight}px;
5671
5881
  --ChatListItemHeight: ${listItemHeight}px;
5672
5882
  --ChatMessageFontSize: ${chatMessageFontSize}px;
@@ -5700,7 +5910,42 @@ const getCss = (composerHeight, listItemHeight, chatMessageFontSize, chatMessage
5700
5910
  .ChatToolCallReadFileLink {
5701
5911
  color: var(--vscode-textLink-foreground);
5702
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;
5703
5941
  }`;
5942
+ if (!renderHtmlCss.trim()) {
5943
+ return baseCss;
5944
+ }
5945
+ return `${baseCss}
5946
+
5947
+ /* render_html tool css */
5948
+ ${renderHtmlCss}`;
5704
5949
  };
5705
5950
 
5706
5951
  // TODO render things like scrollbar height,scrollbar offset, textarea height,
@@ -5713,9 +5958,12 @@ const renderCss = (oldState, newState) => {
5713
5958
  chatMessageLineHeight,
5714
5959
  composerHeight,
5715
5960
  listItemHeight,
5961
+ selectedSessionId,
5962
+ sessions,
5716
5963
  uid
5717
5964
  } = newState;
5718
- const css = getCss(composerHeight, listItemHeight, chatMessageFontSize, chatMessageLineHeight, chatMessageFontFamily);
5965
+ const renderHtmlCss = getRenderHtmlCss(sessions, selectedSessionId);
5966
+ const css = getCss(composerHeight, listItemHeight, chatMessageFontSize, chatMessageLineHeight, chatMessageFontFamily, renderHtmlCss);
5719
5967
  return [SetCss, uid, css];
5720
5968
  };
5721
5969
 
@@ -5770,6 +6018,10 @@ const ChatMessageContent = 'ChatMessageContent';
5770
6018
  const ChatToolCalls = 'ChatToolCalls';
5771
6019
  const ChatToolCallsLabel = 'ChatToolCallsLabel';
5772
6020
  const ChatToolCallReadFileLink = 'ChatToolCallReadFileLink';
6021
+ const ChatToolCallRenderHtmlLabel = 'ChatToolCallRenderHtmlLabel';
6022
+ const ChatToolCallRenderHtmlContent = 'ChatToolCallRenderHtmlContent';
6023
+ const ChatToolCallRenderHtmlBody = 'ChatToolCallRenderHtmlBody';
6024
+ const ChatMessageLink = 'ChatMessageLink';
5773
6025
  const ChatOrderedList = 'ChatOrderedList';
5774
6026
  const ChatOrderedListItem = 'ChatOrderedListItem';
5775
6027
  const MessageUser = 'MessageUser';
@@ -5994,25 +6246,41 @@ const getChatHeaderDomDetailMode = selectedSessionTitle => {
5994
6246
  }, text(selectedSessionTitle), ...getChatHeaderActionsDom()];
5995
6247
  };
5996
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
+ };
5997
6271
  const getMessageNodeDom = node => {
5998
6272
  if (node.type === 'text') {
5999
6273
  return [{
6000
- childCount: 1,
6274
+ childCount: node.children.length,
6001
6275
  className: Markdown,
6002
6276
  type: P
6003
- }, text(node.text)];
6277
+ }, ...node.children.flatMap(getInlineNodeDom)];
6004
6278
  }
6005
6279
  return [{
6006
6280
  childCount: node.items.length,
6007
6281
  className: ChatOrderedList,
6008
6282
  type: Ol
6009
- }, ...node.items.flatMap(item => {
6010
- return [{
6011
- childCount: 1,
6012
- className: ChatOrderedListItem,
6013
- type: Li
6014
- }, text(item.text)];
6015
- })];
6283
+ }, ...node.items.flatMap(getOrderedListItemDom)];
6016
6284
  };
6017
6285
 
6018
6286
  const getMessageContentDom = nodes => {
@@ -6167,7 +6435,7 @@ const getReadFileTarget = rawArguments => {
6167
6435
  if (!title) {
6168
6436
  return undefined;
6169
6437
  }
6170
- // `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.
6171
6439
  const clickableUri = uriValue || pathValue;
6172
6440
  return {
6173
6441
  clickableUri,
@@ -6175,7 +6443,7 @@ const getReadFileTarget = rawArguments => {
6175
6443
  };
6176
6444
  };
6177
6445
 
6178
- const getToolCallStatusLabel$1 = toolCall => {
6446
+ const getToolCallStatusLabel = toolCall => {
6179
6447
  if (toolCall.status === 'not-found') {
6180
6448
  return ' (not-found)';
6181
6449
  }
@@ -6195,7 +6463,7 @@ const getToolCallReadFileVirtualDom = toolCall => {
6195
6463
  }
6196
6464
  const fileName = getFileNameFromUri(target.title);
6197
6465
  const toolNameLabel = `${toolCall.name} `;
6198
- const statusLabel = getToolCallStatusLabel$1(toolCall);
6466
+ const statusLabel = getToolCallStatusLabel(toolCall);
6199
6467
  const fileNameClickableProps = target.clickableUri ? {
6200
6468
  'data-uri': target.clickableUri,
6201
6469
  onClick: HandleClickReadFile
@@ -6217,18 +6485,305 @@ const getToolCallReadFileVirtualDom = toolCall => {
6217
6485
  }, text(fileName), ...(statusLabel ? [text(statusLabel)] : [])];
6218
6486
  };
6219
6487
 
6220
- const getToolCallStatusLabel = toolCall => {
6221
- if (toolCall.status === 'not-found') {
6222
- 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);
6223
6503
  }
6224
- if (toolCall.status === 'error') {
6225
- if (toolCall.errorMessage) {
6226
- 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;
6227
6510
  }
6228
- return ' (error)';
6511
+ const value = String(match[2] ?? match[3] ?? match[4] ?? '');
6512
+ attributes[name] = decodeEntities(value);
6229
6513
  }
6230
- return '';
6514
+ return attributes;
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;
6231
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
+
6232
6787
  const getToolCallDom = toolCall => {
6233
6788
  if (toolCall.name === 'read_file') {
6234
6789
  const virtualDom = getToolCallReadFileVirtualDom(toolCall);
@@ -6236,6 +6791,12 @@ const getToolCallDom = toolCall => {
6236
6791
  return virtualDom;
6237
6792
  }
6238
6793
  }
6794
+ if (toolCall.name === 'render_html') {
6795
+ const virtualDom = getToolCallRenderHtmlVirtualDom(toolCall);
6796
+ if (virtualDom.length > 0) {
6797
+ return virtualDom;
6798
+ }
6799
+ }
6239
6800
  const argumentPreview = getToolCallArgumentPreview(toolCall.arguments);
6240
6801
  const label = `${toolCall.name} ${argumentPreview}${getToolCallStatusLabel(toolCall)}`;
6241
6802
  return [{
@@ -6265,10 +6826,50 @@ const getToolCallsDom = message => {
6265
6826
  };
6266
6827
 
6267
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
+ };
6268
6866
  const parseMessageContent = rawMessage => {
6269
6867
  if (rawMessage === '') {
6270
6868
  return [{
6271
- text: '',
6869
+ children: [{
6870
+ text: '',
6871
+ type: 'text'
6872
+ }],
6272
6873
  type: 'text'
6273
6874
  }];
6274
6875
  }
@@ -6281,7 +6882,7 @@ const parseMessageContent = rawMessage => {
6281
6882
  return;
6282
6883
  }
6283
6884
  nodes.push({
6284
- text: paragraphLines.join('\n'),
6885
+ children: parseInlineNodes(paragraphLines.join('\n')),
6285
6886
  type: 'text'
6286
6887
  });
6287
6888
  paragraphLines = [];
@@ -6306,7 +6907,7 @@ const parseMessageContent = rawMessage => {
6306
6907
  if (match) {
6307
6908
  flushParagraph();
6308
6909
  listItems.push({
6309
- text: match[1],
6910
+ children: parseInlineNodes(match[1]),
6310
6911
  type: 'list-item'
6311
6912
  });
6312
6913
  continue;
@@ -6317,7 +6918,10 @@ const parseMessageContent = rawMessage => {
6317
6918
  flushList();
6318
6919
  flushParagraph();
6319
6920
  return nodes.length === 0 ? [{
6320
- text: '',
6921
+ children: [{
6922
+ text: '',
6923
+ type: 'text'
6924
+ }],
6321
6925
  type: 'text'
6322
6926
  }] : nodes;
6323
6927
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lvce-editor/chat-view",
3
- "version": "2.9.0",
3
+ "version": "2.10.0",
4
4
  "description": "Chat View Worker",
5
5
  "repository": {
6
6
  "type": "git",