wave-code 0.0.5 → 0.0.8

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.
Files changed (93) hide show
  1. package/README.md +3 -3
  2. package/dist/cli.d.ts +1 -0
  3. package/dist/cli.d.ts.map +1 -1
  4. package/dist/cli.js +2 -2
  5. package/dist/components/App.d.ts +1 -0
  6. package/dist/components/App.d.ts.map +1 -1
  7. package/dist/components/App.js +4 -4
  8. package/dist/components/BashHistorySelector.d.ts.map +1 -1
  9. package/dist/components/BashHistorySelector.js +17 -3
  10. package/dist/components/ChatInterface.d.ts.map +1 -1
  11. package/dist/components/ChatInterface.js +6 -24
  12. package/dist/components/CommandSelector.js +4 -4
  13. package/dist/components/Confirmation.d.ts +11 -0
  14. package/dist/components/Confirmation.d.ts.map +1 -0
  15. package/dist/components/Confirmation.js +148 -0
  16. package/dist/components/DiffDisplay.d.ts +8 -0
  17. package/dist/components/DiffDisplay.d.ts.map +1 -0
  18. package/dist/components/DiffDisplay.js +168 -0
  19. package/dist/components/FileSelector.d.ts +2 -4
  20. package/dist/components/FileSelector.d.ts.map +1 -1
  21. package/dist/components/FileSelector.js +2 -2
  22. package/dist/components/InputBox.d.ts.map +1 -1
  23. package/dist/components/InputBox.js +30 -50
  24. package/dist/components/Markdown.d.ts +6 -0
  25. package/dist/components/Markdown.d.ts.map +1 -0
  26. package/dist/components/Markdown.js +22 -0
  27. package/dist/components/MemoryDisplay.js +1 -1
  28. package/dist/components/MessageItem.d.ts +8 -0
  29. package/dist/components/MessageItem.d.ts.map +1 -0
  30. package/dist/components/MessageItem.js +15 -0
  31. package/dist/components/MessageList.d.ts +1 -1
  32. package/dist/components/MessageList.d.ts.map +1 -1
  33. package/dist/components/MessageList.js +33 -33
  34. package/dist/components/ReasoningDisplay.d.ts +8 -0
  35. package/dist/components/ReasoningDisplay.d.ts.map +1 -0
  36. package/dist/components/ReasoningDisplay.js +10 -0
  37. package/dist/components/SubagentBlock.d.ts +0 -1
  38. package/dist/components/SubagentBlock.d.ts.map +1 -1
  39. package/dist/components/SubagentBlock.js +29 -30
  40. package/dist/components/ToolResultDisplay.d.ts.map +1 -1
  41. package/dist/components/ToolResultDisplay.js +6 -5
  42. package/dist/contexts/useChat.d.ts +14 -2
  43. package/dist/contexts/useChat.d.ts.map +1 -1
  44. package/dist/contexts/useChat.js +128 -17
  45. package/dist/hooks/useInputManager.d.ts +6 -1
  46. package/dist/hooks/useInputManager.d.ts.map +1 -1
  47. package/dist/hooks/useInputManager.js +32 -2
  48. package/dist/index.d.ts.map +1 -1
  49. package/dist/index.js +30 -5
  50. package/dist/managers/InputManager.d.ts +11 -1
  51. package/dist/managers/InputManager.d.ts.map +1 -1
  52. package/dist/managers/InputManager.js +77 -26
  53. package/dist/print-cli.d.ts +2 -0
  54. package/dist/print-cli.d.ts.map +1 -1
  55. package/dist/print-cli.js +121 -23
  56. package/dist/utils/toolParameterTransforms.d.ts +23 -0
  57. package/dist/utils/toolParameterTransforms.d.ts.map +1 -0
  58. package/dist/utils/toolParameterTransforms.js +77 -0
  59. package/dist/utils/usageSummary.d.ts +6 -0
  60. package/dist/utils/usageSummary.d.ts.map +1 -1
  61. package/dist/utils/usageSummary.js +72 -0
  62. package/package.json +13 -8
  63. package/src/cli.tsx +3 -1
  64. package/src/components/App.tsx +7 -3
  65. package/src/components/BashHistorySelector.tsx +26 -3
  66. package/src/components/ChatInterface.tsx +38 -54
  67. package/src/components/CommandSelector.tsx +5 -5
  68. package/src/components/Confirmation.tsx +253 -0
  69. package/src/components/DiffDisplay.tsx +300 -0
  70. package/src/components/FileSelector.tsx +4 -6
  71. package/src/components/InputBox.tsx +58 -87
  72. package/src/components/Markdown.tsx +29 -0
  73. package/src/components/MemoryDisplay.tsx +1 -1
  74. package/src/components/MessageItem.tsx +96 -0
  75. package/src/components/MessageList.tsx +140 -202
  76. package/src/components/ReasoningDisplay.tsx +33 -0
  77. package/src/components/SubagentBlock.tsx +56 -84
  78. package/src/components/ToolResultDisplay.tsx +9 -5
  79. package/src/contexts/useChat.tsx +194 -21
  80. package/src/hooks/useInputManager.ts +40 -3
  81. package/src/index.ts +45 -5
  82. package/src/managers/InputManager.ts +101 -27
  83. package/src/print-cli.ts +143 -21
  84. package/src/utils/toolParameterTransforms.ts +104 -0
  85. package/src/utils/usageSummary.ts +109 -0
  86. package/dist/components/DiffViewer.d.ts +0 -9
  87. package/dist/components/DiffViewer.d.ts.map +0 -1
  88. package/dist/components/DiffViewer.js +0 -221
  89. package/dist/utils/fileSearch.d.ts +0 -20
  90. package/dist/utils/fileSearch.d.ts.map +0 -1
  91. package/dist/utils/fileSearch.js +0 -102
  92. package/src/components/DiffViewer.tsx +0 -321
  93. package/src/utils/fileSearch.ts +0 -133
@@ -1,221 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useMemo } from "react";
3
- import { Text, Box } from "ink";
4
- import { diffWords } from "diff";
5
- // Render word-level diff
6
- const renderWordLevelDiff = (removedLine, addedLine) => {
7
- const changes = diffWords(removedLine, addedLine);
8
- const removedParts = [];
9
- const addedParts = [];
10
- changes.forEach((part, index) => {
11
- if (part.removed) {
12
- removedParts.push(_jsx(Text, { color: "black", backgroundColor: "red", children: part.value }, `removed-${index}`));
13
- }
14
- else if (part.added) {
15
- addedParts.push(_jsx(Text, { color: "black", backgroundColor: "green", children: part.value }, `added-${index}`));
16
- }
17
- else {
18
- // Unchanged parts, need to display on both sides
19
- removedParts.push(_jsx(Text, { color: "red", children: part.value }, `removed-unchanged-${index}`));
20
- addedParts.push(_jsx(Text, { color: "green", children: part.value }, `added-unchanged-${index}`));
21
- }
22
- });
23
- return { removedParts, addedParts };
24
- };
25
- export const DiffViewer = ({ block, isExpanded = false, }) => {
26
- const { diffResult } = block;
27
- const diffLines = useMemo(() => {
28
- if (!diffResult)
29
- return [];
30
- const lines = [];
31
- let originalLineNum = 1;
32
- let modifiedLineNum = 1;
33
- const maxContext = 3; // Show at most 3 lines of context
34
- // Buffer for storing context
35
- let contextBuffer = [];
36
- let hasAnyChanges = false;
37
- let afterChangeContext = 0;
38
- // Temporarily store adjacent deleted and added lines for word-level comparison
39
- let pendingRemovedLines = [];
40
- const flushPendingLines = () => {
41
- pendingRemovedLines.forEach((line) => {
42
- lines.push({
43
- content: line.content,
44
- type: "removed",
45
- lineNumber: line.lineNumber,
46
- rawContent: line.rawContent,
47
- });
48
- });
49
- pendingRemovedLines = [];
50
- };
51
- diffResult.forEach((part) => {
52
- const partLines = part.value.split("\n");
53
- // Remove the last empty line (produced by split)
54
- if (partLines[partLines.length - 1] === "") {
55
- partLines.pop();
56
- }
57
- if (part.removed) {
58
- // If this is the first change encountered, add preceding context
59
- if (!hasAnyChanges) {
60
- // Take the last few lines from the buffer as preceding context
61
- const preContext = contextBuffer.slice(-maxContext);
62
- if (contextBuffer.length > maxContext) {
63
- lines.push({
64
- content: "...",
65
- type: "separator",
66
- });
67
- }
68
- lines.push(...preContext);
69
- }
70
- else if (afterChangeContext > maxContext) {
71
- // If there's too much context after the previous change, add a separator
72
- lines.push({
73
- content: "...",
74
- type: "separator",
75
- });
76
- }
77
- // Temporarily store deleted lines, waiting for possible added lines for word-level comparison
78
- partLines.forEach((line) => {
79
- pendingRemovedLines.push({
80
- content: `- ${line}`,
81
- rawContent: line,
82
- lineNumber: originalLineNum++,
83
- });
84
- });
85
- hasAnyChanges = true;
86
- afterChangeContext = 0;
87
- contextBuffer = []; // Clear buffer
88
- }
89
- else if (part.added) {
90
- // If this is the first change encountered, add preceding context
91
- if (!hasAnyChanges) {
92
- const preContext = contextBuffer.slice(-maxContext);
93
- if (contextBuffer.length > maxContext) {
94
- lines.push({
95
- content: "...",
96
- type: "separator",
97
- });
98
- }
99
- lines.push(...preContext);
100
- }
101
- else if (afterChangeContext > maxContext) {
102
- lines.push({
103
- content: "...",
104
- type: "separator",
105
- });
106
- }
107
- // Process added lines, try to do word-level comparison with pending deleted lines
108
- partLines.forEach((line, index) => {
109
- if (index < pendingRemovedLines.length) {
110
- // Has corresponding deleted line, perform word-level comparison
111
- const removedLine = pendingRemovedLines[index];
112
- const wordDiff = renderWordLevelDiff(removedLine.rawContent, line);
113
- // Add deleted line (with word-level highlighting)
114
- lines.push({
115
- content: `- ${removedLine.rawContent}`,
116
- type: "removed",
117
- lineNumber: removedLine.lineNumber,
118
- rawContent: removedLine.rawContent,
119
- wordDiff: {
120
- removedParts: wordDiff.removedParts,
121
- addedParts: [],
122
- },
123
- });
124
- // Add added line (with word-level highlighting)
125
- lines.push({
126
- content: `+ ${line}`,
127
- type: "added",
128
- lineNumber: modifiedLineNum++,
129
- rawContent: line,
130
- wordDiff: { removedParts: [], addedParts: wordDiff.addedParts },
131
- });
132
- }
133
- else {
134
- // No corresponding deleted line, directly add the added line
135
- lines.push({
136
- content: `+ ${line}`,
137
- type: "added",
138
- lineNumber: modifiedLineNum++,
139
- rawContent: line,
140
- });
141
- }
142
- });
143
- // If there are more deleted lines than added lines, add remaining deleted lines
144
- for (let i = partLines.length; i < pendingRemovedLines.length; i++) {
145
- const removedLine = pendingRemovedLines[i];
146
- lines.push({
147
- content: removedLine.content,
148
- type: "removed",
149
- lineNumber: removedLine.lineNumber,
150
- rawContent: removedLine.rawContent,
151
- });
152
- }
153
- pendingRemovedLines = []; // Clear pending deleted lines
154
- hasAnyChanges = true;
155
- afterChangeContext = 0;
156
- contextBuffer = [];
157
- }
158
- else {
159
- // Before processing unchanged lines, first clear pending deleted lines
160
- flushPendingLines();
161
- // Process unchanged lines
162
- partLines.forEach((line) => {
163
- const contextLine = {
164
- content: ` ${line}`,
165
- type: "unchanged",
166
- lineNumber: originalLineNum,
167
- };
168
- if (hasAnyChanges) {
169
- // If there are already changes, these are post-change context
170
- if (afterChangeContext < maxContext) {
171
- lines.push(contextLine);
172
- afterChangeContext++;
173
- }
174
- }
175
- else {
176
- // If no changes yet, add to buffer
177
- contextBuffer.push(contextLine);
178
- }
179
- originalLineNum++;
180
- modifiedLineNum++;
181
- });
182
- }
183
- });
184
- // Handle remaining deleted lines at the end
185
- flushPendingLines();
186
- // Only limit displayed lines in collapsed state
187
- if (!isExpanded) {
188
- const MAX_DISPLAY_LINES = 50;
189
- if (lines.length > MAX_DISPLAY_LINES) {
190
- const truncatedLines = lines.slice(0, MAX_DISPLAY_LINES);
191
- truncatedLines.push({
192
- content: `... (${lines.length - MAX_DISPLAY_LINES} more lines truncated, press Ctrl+O to expand)`,
193
- type: "separator",
194
- });
195
- return truncatedLines;
196
- }
197
- }
198
- return lines;
199
- }, [diffResult, isExpanded]);
200
- if (!diffResult || diffResult.length === 0) {
201
- return (_jsx(Box, { flexDirection: "column", children: _jsx(Text, { color: "gray", children: "No changes detected" }) }));
202
- }
203
- return (_jsx(Box, { flexDirection: "column", children: _jsx(Box, { flexDirection: "column", children: _jsx(Box, { flexDirection: "column", children: diffLines.map((line, index) => {
204
- // If has word-level diff, render special effects
205
- if (line.wordDiff) {
206
- const prefix = line.type === "removed" ? "- " : "+ ";
207
- const parts = line.type === "removed"
208
- ? line.wordDiff.removedParts
209
- : line.wordDiff.addedParts;
210
- return (_jsxs(Box, { flexDirection: "row", children: [_jsx(Text, { color: line.type === "removed" ? "red" : "green", children: prefix }), _jsx(Box, { flexDirection: "row", flexWrap: "wrap", children: parts })] }, index));
211
- }
212
- // Normal rendering
213
- return (_jsx(Text, { color: line.type === "added"
214
- ? "green"
215
- : line.type === "removed"
216
- ? "red"
217
- : line.type === "separator"
218
- ? "gray"
219
- : "white", dimColor: line.type === "separator", children: line.content }, index));
220
- }) }) }) }));
221
- };
@@ -1,20 +0,0 @@
1
- export interface FileItem {
2
- path: string;
3
- type: "file" | "directory";
4
- }
5
- /**
6
- * Check if path is a directory
7
- */
8
- export declare const isDirectory: (filePath: string) => boolean;
9
- /**
10
- * Convert string paths to FileItem objects
11
- */
12
- export declare const convertToFileItems: (paths: string[]) => FileItem[];
13
- /**
14
- * Search files and directories using glob patterns
15
- */
16
- export declare const searchFiles: (query: string, options?: {
17
- maxResults?: number;
18
- workingDirectory?: string;
19
- }) => Promise<FileItem[]>;
20
- //# sourceMappingURL=fileSearch.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"fileSearch.d.ts","sourceRoot":"","sources":["../../src/utils/fileSearch.ts"],"names":[],"mappings":"AAKA,MAAM,WAAW,QAAQ;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,GAAG,WAAW,CAAC;CAC5B;AAED;;GAEG;AACH,eAAO,MAAM,WAAW,GAAI,UAAU,MAAM,KAAG,OAS9C,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,kBAAkB,GAAI,OAAO,MAAM,EAAE,KAAG,QAAQ,EAK5D,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,WAAW,GACtB,OAAO,MAAM,EACb,UAAU;IACR,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B,KACA,OAAO,CAAC,QAAQ,EAAE,CAyFpB,CAAC"}
@@ -1,102 +0,0 @@
1
- import { glob } from "glob";
2
- import { getGlobIgnorePatterns } from "wave-agent-sdk";
3
- import * as fs from "fs";
4
- import * as path from "path";
5
- /**
6
- * Check if path is a directory
7
- */
8
- export const isDirectory = (filePath) => {
9
- try {
10
- const fullPath = path.isAbsolute(filePath)
11
- ? filePath
12
- : path.join(process.cwd(), filePath);
13
- return fs.statSync(fullPath).isDirectory();
14
- }
15
- catch {
16
- return false;
17
- }
18
- };
19
- /**
20
- * Convert string paths to FileItem objects
21
- */
22
- export const convertToFileItems = (paths) => {
23
- return paths.map((filePath) => ({
24
- path: filePath,
25
- type: isDirectory(filePath) ? "directory" : "file",
26
- }));
27
- };
28
- /**
29
- * Search files and directories using glob patterns
30
- */
31
- export const searchFiles = async (query, options) => {
32
- const { maxResults = 10, workingDirectory = process.cwd() } = options || {};
33
- try {
34
- let files = [];
35
- let directories = [];
36
- const globOptions = {
37
- ignore: getGlobIgnorePatterns(workingDirectory),
38
- maxDepth: 10,
39
- nocase: true, // Case insensitive
40
- dot: true, // Include hidden files and directories
41
- cwd: workingDirectory, // Specify search root directory
42
- };
43
- if (!query.trim()) {
44
- // When query is empty, show some common file types and directories
45
- const commonPatterns = [
46
- "**/*.ts",
47
- "**/*.tsx",
48
- "**/*.js",
49
- "**/*.jsx",
50
- "**/*.json",
51
- ];
52
- // Search files
53
- const filePromises = commonPatterns.map((pattern) => glob(pattern, { ...globOptions, nodir: true }));
54
- // Search directories (only search first level to avoid too many results)
55
- const dirPromises = [glob("*/", { ...globOptions, maxDepth: 1 })];
56
- const fileResults = await Promise.all(filePromises);
57
- const dirResults = await Promise.all(dirPromises);
58
- files = fileResults.flat();
59
- directories = dirResults.flat().map((dir) => {
60
- // glob returns string type paths, remove trailing slash
61
- return String(dir).replace(/\/$/, "");
62
- });
63
- }
64
- else {
65
- // Build multiple glob patterns to support more flexible search
66
- const filePatterns = [
67
- // Match files with filenames containing query
68
- `**/*${query}*`,
69
- // Match files with query in path (match directory names)
70
- `**/${query}*/**/*`,
71
- ];
72
- const dirPatterns = [
73
- // Match directory names containing query
74
- `**/*${query}*/`,
75
- // Match directories containing query in path
76
- `**/${query}*/`,
77
- ];
78
- // Search files
79
- const filePromises = filePatterns.map((pattern) => glob(pattern, { ...globOptions, nodir: true }));
80
- // Search directories
81
- const dirPromises = dirPatterns.map((pattern) => glob(pattern, { ...globOptions, nodir: false }));
82
- const fileResults = await Promise.all(filePromises);
83
- const dirResults = await Promise.all(dirPromises);
84
- files = fileResults.flat();
85
- directories = dirResults.flat().map((dir) => {
86
- // glob returns string type paths, remove trailing slash
87
- return String(dir).replace(/\/$/, "");
88
- });
89
- }
90
- // Deduplicate and merge files and directories
91
- const uniqueFiles = Array.from(new Set(files));
92
- const uniqueDirectories = Array.from(new Set(directories));
93
- const allPaths = [...uniqueDirectories, ...uniqueFiles]; // Directories first
94
- // Limit to maximum results and convert to FileItem
95
- const fileItems = convertToFileItems(allPaths.slice(0, maxResults));
96
- return fileItems;
97
- }
98
- catch (error) {
99
- console.error("Glob search error:", error);
100
- return [];
101
- }
102
- };
@@ -1,321 +0,0 @@
1
- import React, { useMemo } from "react";
2
- import { Text, Box } from "ink";
3
- import { diffWords } from "diff";
4
- import type { DiffBlock } from "wave-agent-sdk";
5
-
6
- interface DiffViewerProps {
7
- block: DiffBlock;
8
- isExpanded?: boolean;
9
- }
10
-
11
- // Render word-level diff
12
- const renderWordLevelDiff = (removedLine: string, addedLine: string) => {
13
- const changes = diffWords(removedLine, addedLine);
14
-
15
- const removedParts: React.ReactNode[] = [];
16
- const addedParts: React.ReactNode[] = [];
17
-
18
- changes.forEach((part, index) => {
19
- if (part.removed) {
20
- removedParts.push(
21
- <Text key={`removed-${index}`} color="black" backgroundColor="red">
22
- {part.value}
23
- </Text>,
24
- );
25
- } else if (part.added) {
26
- addedParts.push(
27
- <Text key={`added-${index}`} color="black" backgroundColor="green">
28
- {part.value}
29
- </Text>,
30
- );
31
- } else {
32
- // Unchanged parts, need to display on both sides
33
- removedParts.push(
34
- <Text key={`removed-unchanged-${index}`} color="red">
35
- {part.value}
36
- </Text>,
37
- );
38
- addedParts.push(
39
- <Text key={`added-unchanged-${index}`} color="green">
40
- {part.value}
41
- </Text>,
42
- );
43
- }
44
- });
45
-
46
- return { removedParts, addedParts };
47
- };
48
-
49
- export const DiffViewer: React.FC<DiffViewerProps> = ({
50
- block,
51
- isExpanded = false,
52
- }) => {
53
- const { diffResult } = block;
54
-
55
- const diffLines = useMemo(() => {
56
- if (!diffResult) return [];
57
-
58
- const lines: Array<{
59
- content: string;
60
- type: "added" | "removed" | "unchanged" | "separator";
61
- lineNumber?: number;
62
- rawContent?: string; // Store original content for word-level comparison
63
- wordDiff?: {
64
- removedParts: React.ReactNode[];
65
- addedParts: React.ReactNode[];
66
- };
67
- }> = [];
68
-
69
- let originalLineNum = 1;
70
- let modifiedLineNum = 1;
71
- const maxContext = 3; // Show at most 3 lines of context
72
-
73
- // Buffer for storing context
74
- let contextBuffer: Array<{
75
- content: string;
76
- type: "unchanged";
77
- lineNumber: number;
78
- }> = [];
79
-
80
- let hasAnyChanges = false;
81
- let afterChangeContext = 0;
82
-
83
- // Temporarily store adjacent deleted and added lines for word-level comparison
84
- let pendingRemovedLines: Array<{
85
- content: string;
86
- rawContent: string;
87
- lineNumber: number;
88
- }> = [];
89
-
90
- const flushPendingLines = () => {
91
- pendingRemovedLines.forEach((line) => {
92
- lines.push({
93
- content: line.content,
94
- type: "removed",
95
- lineNumber: line.lineNumber,
96
- rawContent: line.rawContent,
97
- });
98
- });
99
- pendingRemovedLines = [];
100
- };
101
-
102
- diffResult.forEach(
103
- (part: { value: string; added?: boolean; removed?: boolean }) => {
104
- const partLines = part.value.split("\n");
105
- // Remove the last empty line (produced by split)
106
- if (partLines[partLines.length - 1] === "") {
107
- partLines.pop();
108
- }
109
-
110
- if (part.removed) {
111
- // If this is the first change encountered, add preceding context
112
- if (!hasAnyChanges) {
113
- // Take the last few lines from the buffer as preceding context
114
- const preContext = contextBuffer.slice(-maxContext);
115
- if (contextBuffer.length > maxContext) {
116
- lines.push({
117
- content: "...",
118
- type: "separator",
119
- });
120
- }
121
- lines.push(...preContext);
122
- } else if (afterChangeContext > maxContext) {
123
- // If there's too much context after the previous change, add a separator
124
- lines.push({
125
- content: "...",
126
- type: "separator",
127
- });
128
- }
129
-
130
- // Temporarily store deleted lines, waiting for possible added lines for word-level comparison
131
- partLines.forEach((line: string) => {
132
- pendingRemovedLines.push({
133
- content: `- ${line}`,
134
- rawContent: line,
135
- lineNumber: originalLineNum++,
136
- });
137
- });
138
-
139
- hasAnyChanges = true;
140
- afterChangeContext = 0;
141
- contextBuffer = []; // Clear buffer
142
- } else if (part.added) {
143
- // If this is the first change encountered, add preceding context
144
- if (!hasAnyChanges) {
145
- const preContext = contextBuffer.slice(-maxContext);
146
- if (contextBuffer.length > maxContext) {
147
- lines.push({
148
- content: "...",
149
- type: "separator",
150
- });
151
- }
152
- lines.push(...preContext);
153
- } else if (afterChangeContext > maxContext) {
154
- lines.push({
155
- content: "...",
156
- type: "separator",
157
- });
158
- }
159
-
160
- // Process added lines, try to do word-level comparison with pending deleted lines
161
- partLines.forEach((line: string, index: number) => {
162
- if (index < pendingRemovedLines.length) {
163
- // Has corresponding deleted line, perform word-level comparison
164
- const removedLine = pendingRemovedLines[index];
165
- const wordDiff = renderWordLevelDiff(
166
- removedLine.rawContent,
167
- line,
168
- );
169
-
170
- // Add deleted line (with word-level highlighting)
171
- lines.push({
172
- content: `- ${removedLine.rawContent}`,
173
- type: "removed",
174
- lineNumber: removedLine.lineNumber,
175
- rawContent: removedLine.rawContent,
176
- wordDiff: {
177
- removedParts: wordDiff.removedParts,
178
- addedParts: [],
179
- },
180
- });
181
-
182
- // Add added line (with word-level highlighting)
183
- lines.push({
184
- content: `+ ${line}`,
185
- type: "added",
186
- lineNumber: modifiedLineNum++,
187
- rawContent: line,
188
- wordDiff: { removedParts: [], addedParts: wordDiff.addedParts },
189
- });
190
- } else {
191
- // No corresponding deleted line, directly add the added line
192
- lines.push({
193
- content: `+ ${line}`,
194
- type: "added",
195
- lineNumber: modifiedLineNum++,
196
- rawContent: line,
197
- });
198
- }
199
- });
200
-
201
- // If there are more deleted lines than added lines, add remaining deleted lines
202
- for (let i = partLines.length; i < pendingRemovedLines.length; i++) {
203
- const removedLine = pendingRemovedLines[i];
204
- lines.push({
205
- content: removedLine.content,
206
- type: "removed",
207
- lineNumber: removedLine.lineNumber,
208
- rawContent: removedLine.rawContent,
209
- });
210
- }
211
-
212
- pendingRemovedLines = []; // Clear pending deleted lines
213
- hasAnyChanges = true;
214
- afterChangeContext = 0;
215
- contextBuffer = [];
216
- } else {
217
- // Before processing unchanged lines, first clear pending deleted lines
218
- flushPendingLines();
219
-
220
- // Process unchanged lines
221
- partLines.forEach((line: string) => {
222
- const contextLine = {
223
- content: ` ${line}`,
224
- type: "unchanged" as const,
225
- lineNumber: originalLineNum,
226
- };
227
-
228
- if (hasAnyChanges) {
229
- // If there are already changes, these are post-change context
230
- if (afterChangeContext < maxContext) {
231
- lines.push(contextLine);
232
- afterChangeContext++;
233
- }
234
- } else {
235
- // If no changes yet, add to buffer
236
- contextBuffer.push(contextLine);
237
- }
238
-
239
- originalLineNum++;
240
- modifiedLineNum++;
241
- });
242
- }
243
- },
244
- );
245
-
246
- // Handle remaining deleted lines at the end
247
- flushPendingLines();
248
-
249
- // Only limit displayed lines in collapsed state
250
- if (!isExpanded) {
251
- const MAX_DISPLAY_LINES = 50;
252
- if (lines.length > MAX_DISPLAY_LINES) {
253
- const truncatedLines = lines.slice(0, MAX_DISPLAY_LINES);
254
- truncatedLines.push({
255
- content: `... (${lines.length - MAX_DISPLAY_LINES} more lines truncated, press Ctrl+O to expand)`,
256
- type: "separator",
257
- });
258
- return truncatedLines;
259
- }
260
- }
261
-
262
- return lines;
263
- }, [diffResult, isExpanded]);
264
-
265
- if (!diffResult || diffResult.length === 0) {
266
- return (
267
- <Box flexDirection="column">
268
- <Text color="gray">No changes detected</Text>
269
- </Box>
270
- );
271
- }
272
-
273
- return (
274
- <Box flexDirection="column">
275
- <Box flexDirection="column">
276
- <Box flexDirection="column">
277
- {diffLines.map((line, index) => {
278
- // If has word-level diff, render special effects
279
- if (line.wordDiff) {
280
- const prefix = line.type === "removed" ? "- " : "+ ";
281
- const parts =
282
- line.type === "removed"
283
- ? line.wordDiff.removedParts
284
- : line.wordDiff.addedParts;
285
-
286
- return (
287
- <Box key={index} flexDirection="row">
288
- <Text color={line.type === "removed" ? "red" : "green"}>
289
- {prefix}
290
- </Text>
291
- <Box flexDirection="row" flexWrap="wrap">
292
- {parts}
293
- </Box>
294
- </Box>
295
- );
296
- }
297
-
298
- // Normal rendering
299
- return (
300
- <Text
301
- key={index}
302
- color={
303
- line.type === "added"
304
- ? "green"
305
- : line.type === "removed"
306
- ? "red"
307
- : line.type === "separator"
308
- ? "gray"
309
- : "white"
310
- }
311
- dimColor={line.type === "separator"}
312
- >
313
- {line.content}
314
- </Text>
315
- );
316
- })}
317
- </Box>
318
- </Box>
319
- </Box>
320
- );
321
- };