@lvce-editor/chat-view 6.8.0 → 6.9.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.
@@ -3499,6 +3499,38 @@ const executeRenderHtmlTool = async (args, _options) => {
3499
3499
  });
3500
3500
  };
3501
3501
 
3502
+ const toLines = value => {
3503
+ if (!value) {
3504
+ return [];
3505
+ }
3506
+ const split = value.split('\n').map(line => line.endsWith('\r') ? line.slice(0, -1) : line);
3507
+ if (split.length > 0 && split.at(-1) === '') {
3508
+ split.pop();
3509
+ }
3510
+ return split;
3511
+ };
3512
+ const getLineCounts = (before, after) => {
3513
+ const beforeLines = toLines(before);
3514
+ const afterLines = toLines(after);
3515
+ let start = 0;
3516
+ while (start < beforeLines.length && start < afterLines.length && beforeLines[start] === afterLines[start]) {
3517
+ start++;
3518
+ }
3519
+ let beforeEnd = beforeLines.length - 1;
3520
+ let afterEnd = afterLines.length - 1;
3521
+ while (beforeEnd >= start && afterEnd >= start && beforeLines[beforeEnd] === afterLines[afterEnd]) {
3522
+ beforeEnd--;
3523
+ afterEnd--;
3524
+ }
3525
+ return {
3526
+ linesAdded: Math.max(0, afterEnd - start + 1),
3527
+ linesDeleted: Math.max(0, beforeEnd - start + 1)
3528
+ };
3529
+ };
3530
+ const isFileNotFoundError = error => {
3531
+ const message = String(error).toLowerCase();
3532
+ return message.includes('enoent') || message.includes('not found');
3533
+ };
3502
3534
  const executeWriteFileTool = async (args, _options) => {
3503
3535
  const filePath = typeof args.path === 'string' ? args.path : '';
3504
3536
  const content = typeof args.content === 'string' ? args.content : '';
@@ -3509,8 +3541,22 @@ const executeWriteFileTool = async (args, _options) => {
3509
3541
  }
3510
3542
  const normalizedPath = normalizeRelativePath(filePath);
3511
3543
  try {
3544
+ let previousContent = '';
3545
+ try {
3546
+ previousContent = await readFile(normalizedPath);
3547
+ } catch (error) {
3548
+ if (!isFileNotFoundError(error)) {
3549
+ throw error;
3550
+ }
3551
+ }
3512
3552
  await writeFile(normalizedPath, content);
3553
+ const {
3554
+ linesAdded,
3555
+ linesDeleted
3556
+ } = getLineCounts(previousContent, content);
3513
3557
  return JSON.stringify({
3558
+ linesAdded,
3559
+ linesDeleted,
3514
3560
  ok: true,
3515
3561
  path: normalizedPath
3516
3562
  });
@@ -9288,12 +9334,14 @@ const ChatHeader = 'ChatHeader';
9288
9334
  const Button = 'Button';
9289
9335
  const ButtonPrimary = 'ButtonPrimary';
9290
9336
  const ButtonSecondary = 'ButtonSecondary';
9337
+ const Deletion = 'Deletion';
9291
9338
  const Empty = '';
9292
9339
  const FileIcon = 'FileIcon';
9293
9340
  const IconButton = 'IconButton';
9294
9341
  const IconButtonDisabled = 'IconButtonDisabled';
9295
9342
  const ImageElement = 'ImageElement';
9296
9343
  const InputBox = 'InputBox';
9344
+ const Insertion = 'Insertion';
9297
9345
  const Label = 'Label';
9298
9346
  const LabelDetail = 'LabelDetail';
9299
9347
  const ChatList = 'ChatList';
@@ -10014,6 +10062,76 @@ const getFileNameFromUri = uri => {
10014
10062
  return fileName || uri;
10015
10063
  };
10016
10064
 
10065
+ const isCompleteJson = value => {
10066
+ const trimmed = value.trim();
10067
+ if (!trimmed) {
10068
+ return false;
10069
+ }
10070
+ let depth = 0;
10071
+ let inString = false;
10072
+ let escaped = false;
10073
+ for (const char of trimmed) {
10074
+ if (inString) {
10075
+ if (escaped) {
10076
+ escaped = false;
10077
+ continue;
10078
+ }
10079
+ if (char === '\\') {
10080
+ escaped = true;
10081
+ continue;
10082
+ }
10083
+ if (char === '"') {
10084
+ inString = false;
10085
+ }
10086
+ continue;
10087
+ }
10088
+ if (char === '"') {
10089
+ inString = true;
10090
+ continue;
10091
+ }
10092
+ if (char === '{' || char === '[') {
10093
+ depth += 1;
10094
+ continue;
10095
+ }
10096
+ if (char === '}' || char === ']') {
10097
+ depth -= 1;
10098
+ if (depth < 0) {
10099
+ return false;
10100
+ }
10101
+ }
10102
+ }
10103
+ return depth === 0 && !inString && !escaped;
10104
+ };
10105
+ const getReadFileTarget = rawArguments => {
10106
+ // Tool arguments stream in chunks, so skip parsing until a full JSON payload is available.
10107
+ if (!isCompleteJson(rawArguments)) {
10108
+ return undefined;
10109
+ }
10110
+ let parsed;
10111
+ try {
10112
+ parsed = JSON.parse(rawArguments);
10113
+ } catch {
10114
+ return undefined;
10115
+ }
10116
+ if (!parsed || typeof parsed !== 'object') {
10117
+ return undefined;
10118
+ }
10119
+ const uri = Reflect.get(parsed, 'uri');
10120
+ const path = Reflect.get(parsed, 'path');
10121
+ const uriValue = typeof uri === 'string' ? uri : '';
10122
+ const pathValue = typeof path === 'string' ? path : '';
10123
+ const title = uriValue || pathValue;
10124
+ if (!title) {
10125
+ return undefined;
10126
+ }
10127
+ // `read_file` tool calls now use absolute `uri`; keep `path` as a legacy fallback for old transcripts.
10128
+ const clickableUri = uriValue || pathValue;
10129
+ return {
10130
+ clickableUri,
10131
+ title
10132
+ };
10133
+ };
10134
+
10017
10135
  const getToolCallArgumentPreview = rawArguments => {
10018
10136
  if (!rawArguments.trim()) {
10019
10137
  return '""';
@@ -10106,76 +10224,6 @@ const getToolCallAskQuestionVirtualDom = toolCall => {
10106
10224
  }, text(answer.trim() ? answer : '(empty answer)')])];
10107
10225
  };
10108
10226
 
10109
- const isCompleteJson = value => {
10110
- const trimmed = value.trim();
10111
- if (!trimmed) {
10112
- return false;
10113
- }
10114
- let depth = 0;
10115
- let inString = false;
10116
- let escaped = false;
10117
- for (const char of trimmed) {
10118
- if (inString) {
10119
- if (escaped) {
10120
- escaped = false;
10121
- continue;
10122
- }
10123
- if (char === '\\') {
10124
- escaped = true;
10125
- continue;
10126
- }
10127
- if (char === '"') {
10128
- inString = false;
10129
- }
10130
- continue;
10131
- }
10132
- if (char === '"') {
10133
- inString = true;
10134
- continue;
10135
- }
10136
- if (char === '{' || char === '[') {
10137
- depth += 1;
10138
- continue;
10139
- }
10140
- if (char === '}' || char === ']') {
10141
- depth -= 1;
10142
- if (depth < 0) {
10143
- return false;
10144
- }
10145
- }
10146
- }
10147
- return depth === 0 && !inString && !escaped;
10148
- };
10149
- const getReadFileTarget = rawArguments => {
10150
- // Tool arguments stream in chunks, so skip parsing until a full JSON payload is available.
10151
- if (!isCompleteJson(rawArguments)) {
10152
- return undefined;
10153
- }
10154
- let parsed;
10155
- try {
10156
- parsed = JSON.parse(rawArguments);
10157
- } catch {
10158
- return undefined;
10159
- }
10160
- if (!parsed || typeof parsed !== 'object') {
10161
- return undefined;
10162
- }
10163
- const uri = Reflect.get(parsed, 'uri');
10164
- const path = Reflect.get(parsed, 'path');
10165
- const uriValue = typeof uri === 'string' ? uri : '';
10166
- const pathValue = typeof path === 'string' ? path : '';
10167
- const title = uriValue || pathValue;
10168
- if (!title) {
10169
- return undefined;
10170
- }
10171
- // `read_file` tool calls now use absolute `uri`; keep `path` as a legacy fallback for old transcripts.
10172
- const clickableUri = uriValue || pathValue;
10173
- return {
10174
- clickableUri,
10175
- title
10176
- };
10177
- };
10178
-
10179
10227
  const getToolCallReadFileVirtualDom = toolCall => {
10180
10228
  const target = getReadFileTarget(toolCall.arguments);
10181
10229
  if (!target) {
@@ -10514,8 +10562,19 @@ const getToolCallDisplayName = name => {
10514
10562
  }
10515
10563
  return name;
10516
10564
  };
10565
+ const hasIncompleteJsonArguments = rawArguments => {
10566
+ try {
10567
+ JSON.parse(rawArguments);
10568
+ return false;
10569
+ } catch {
10570
+ return true;
10571
+ }
10572
+ };
10517
10573
  const getToolCallLabel = toolCall => {
10518
10574
  const displayName = getToolCallDisplayName(toolCall.name);
10575
+ if (toolCall.name === 'write_file' && !toolCall.status && hasIncompleteJsonArguments(toolCall.arguments)) {
10576
+ return `${displayName} (in progress)`;
10577
+ }
10519
10578
  const argumentPreview = getToolCallArgumentPreview(toolCall.arguments);
10520
10579
  const statusLabel = getToolCallStatusLabel(toolCall);
10521
10580
  if (argumentPreview === '{}') {
@@ -10546,6 +10605,74 @@ const getToolCallGetWorkspaceUriVirtualDom = toolCall => {
10546
10605
  type: Span
10547
10606
  }, text(fileName), ...(statusLabel ? [text(statusLabel)] : [])];
10548
10607
  };
10608
+ const parseWriteFileLineCounts = rawResult => {
10609
+ if (!rawResult) {
10610
+ return {
10611
+ linesAdded: 0,
10612
+ linesDeleted: 0
10613
+ };
10614
+ }
10615
+ let parsed;
10616
+ try {
10617
+ parsed = JSON.parse(rawResult);
10618
+ } catch {
10619
+ return {
10620
+ linesAdded: 0,
10621
+ linesDeleted: 0
10622
+ };
10623
+ }
10624
+ if (!parsed || typeof parsed !== 'object') {
10625
+ return {
10626
+ linesAdded: 0,
10627
+ linesDeleted: 0
10628
+ };
10629
+ }
10630
+ const linesAdded = Reflect.get(parsed, 'linesAdded');
10631
+ const linesDeleted = Reflect.get(parsed, 'linesDeleted');
10632
+ return {
10633
+ linesAdded: typeof linesAdded === 'number' ? Math.max(0, linesAdded) : 0,
10634
+ linesDeleted: typeof linesDeleted === 'number' ? Math.max(0, linesDeleted) : 0
10635
+ };
10636
+ };
10637
+ const getToolCallWriteFileVirtualDom = toolCall => {
10638
+ const target = getReadFileTarget(toolCall.arguments);
10639
+ if (!target) {
10640
+ return [];
10641
+ }
10642
+ const fileName = getFileNameFromUri(target.title);
10643
+ const statusLabel = getToolCallStatusLabel(toolCall);
10644
+ const {
10645
+ linesAdded,
10646
+ linesDeleted
10647
+ } = parseWriteFileLineCounts(toolCall.result);
10648
+ const fileNameClickableProps = target.clickableUri ? {
10649
+ 'data-uri': target.clickableUri,
10650
+ onClick: HandleClickReadFile
10651
+ } : {};
10652
+ return [{
10653
+ childCount: statusLabel ? 6 : 5,
10654
+ className: ChatOrderedListItem,
10655
+ title: target.title,
10656
+ type: Li
10657
+ }, {
10658
+ childCount: 0,
10659
+ className: FileIcon,
10660
+ type: Div
10661
+ }, text('write_file '), {
10662
+ childCount: 1,
10663
+ className: ChatToolCallReadFileLink,
10664
+ ...fileNameClickableProps,
10665
+ type: Span
10666
+ }, text(fileName), {
10667
+ childCount: 1,
10668
+ className: Insertion,
10669
+ type: Span
10670
+ }, text(` +${linesAdded}`), {
10671
+ childCount: 1,
10672
+ className: Deletion,
10673
+ type: Span
10674
+ }, text(` -${linesDeleted}`), ...(statusLabel ? [text(statusLabel)] : [])];
10675
+ };
10549
10676
  const getToolCallDom = toolCall => {
10550
10677
  if (toolCall.name === 'getWorkspaceUri') {
10551
10678
  const virtualDom = getToolCallGetWorkspaceUriVirtualDom(toolCall);
@@ -10559,6 +10686,12 @@ const getToolCallDom = toolCall => {
10559
10686
  return virtualDom;
10560
10687
  }
10561
10688
  }
10689
+ if (toolCall.name === 'write_file') {
10690
+ const virtualDom = getToolCallWriteFileVirtualDom(toolCall);
10691
+ if (virtualDom.length > 0) {
10692
+ return virtualDom;
10693
+ }
10694
+ }
10562
10695
  if (toolCall.name === 'render_html') {
10563
10696
  const virtualDom = getToolCallRenderHtmlVirtualDom(toolCall);
10564
10697
  if (virtualDom.length > 0) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lvce-editor/chat-view",
3
- "version": "6.8.0",
3
+ "version": "6.9.0",
4
4
  "description": "Chat View Worker",
5
5
  "repository": {
6
6
  "type": "git",