@nextclaw/ui 0.11.20 → 0.11.22

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 (125) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/dist/assets/{ChannelsList-DAx7wv0_.js → ChannelsList-Zeys_w43.js} +6 -6
  3. package/dist/assets/ChatPage-DWOU_8P6.js +43 -0
  4. package/dist/assets/DocBrowser-B9OaZjmg.js +1 -0
  5. package/dist/assets/{DocBrowser-DKkE3Y4I.js → DocBrowser-BmtBLFU0.js} +1 -1
  6. package/dist/assets/{DocBrowserContext-BcZRBsCg.js → DocBrowserContext-YIKkPb76.js} +1 -1
  7. package/dist/assets/{LogoBadge-BIPDLEwK.js → LogoBadge-F7ZWdxLT.js} +1 -1
  8. package/dist/assets/MarketplacePage-BfaTTqN6.js +1 -0
  9. package/dist/assets/{MarketplacePage-Dlp5BgCh.js → MarketplacePage-Cd4faegU.js} +2 -2
  10. package/dist/assets/{McpMarketplacePage-CwKtAil8.js → McpMarketplacePage-C09Ngs7O.js} +2 -2
  11. package/dist/assets/ModelConfig-DJgdcgvQ.js +1 -0
  12. package/dist/assets/ProvidersList-w0rVFIBf.js +1 -0
  13. package/dist/assets/RemoteAccessPage-BJ_ckkOV.js +1 -0
  14. package/dist/assets/RuntimeConfig-Cmn2xPQO.js +1 -0
  15. package/dist/assets/{SearchConfig-v46R5a2U.js → SearchConfig-BT13qpR_.js} +1 -1
  16. package/dist/assets/{SecretsConfig-CXvUpbB_.js → SecretsConfig-CvqEVn0B.js} +2 -2
  17. package/dist/assets/{SessionsConfig-7vUHMtOh.js → SessionsConfig-DHHcYznk.js} +2 -2
  18. package/dist/assets/{book-open-DzSduAaw.js → book-open-CXoF5nQC.js} +1 -1
  19. package/dist/assets/chat-session-display-VW6ZMvZP.js +1 -0
  20. package/dist/assets/{chunk-JZWAC4HX-C1vpvW4r.js → chunk-JZWAC4HX-CvRWvTy5.js} +1 -1
  21. package/dist/assets/{config-Df97LeLR.js → config-DJswxxE8.js} +1 -1
  22. package/dist/assets/{createLucideIcon-CcR5wVoU.js → createLucideIcon-CjGHOWb6.js} +1 -1
  23. package/dist/assets/{dist-Dii9v3X9.js → dist-Cl2QB-2y.js} +1 -1
  24. package/dist/assets/{dist-BMlnBah3.js → dist-nqTTbVdA.js} +1 -1
  25. package/dist/assets/{external-link-CnSDrvJE.js → external-link-tIO7zING.js} +1 -1
  26. package/dist/assets/{hash-CAnX6PNt.js → hash-JWUyl1pT.js} +1 -1
  27. package/dist/assets/i18n-CDHMXlRZ.js +1 -0
  28. package/dist/assets/index-BlH4-cBw.css +1 -0
  29. package/dist/assets/{index-B0DzQqwv.js → index-C6d0xmtm.js} +3 -3
  30. package/dist/assets/{label-CtIFj7_6.js → label-BIpeNu4r.js} +1 -1
  31. package/dist/assets/loader-circle-Cs8XVFTw.js +1 -0
  32. package/dist/assets/{logos-3KFNiOej.js → logos-DThdM9lk.js} +1 -1
  33. package/dist/assets/{page-layout-BMwpn87D.js → page-layout-D3Xo605Z.js} +1 -1
  34. package/dist/assets/plus-PHf8q-Ct.js +1 -0
  35. package/dist/assets/{popover-BIzq25oH.js → popover-BJRUGA_H.js} +1 -1
  36. package/dist/assets/provider-models-bz5y28rq.js +1 -0
  37. package/dist/assets/{react-ji6GGP_j.js → react-7ZHqQtEV.js} +1 -1
  38. package/dist/assets/refresh-ccw-CC6-_QuL.js +1 -0
  39. package/dist/assets/{save-CMgYkJ-y.js → save-DJM5RRWW.js} +1 -1
  40. package/dist/assets/search-C91yH_6y.js +1 -0
  41. package/dist/assets/{security-config-Xi5DYW7j.js → security-config-T5zpg16O.js} +1 -1
  42. package/dist/assets/{select-Cz82gl01.js → select-DSkTc61S.js} +1 -1
  43. package/dist/assets/skeleton-Dzg-HOiN.js +1 -0
  44. package/dist/assets/{status-dot-C7q1HvLH.js → status-dot-LNBlDu3q.js} +1 -1
  45. package/dist/assets/{switch-DYswvkYj.js → switch-Bo-Y46HZ.js} +1 -1
  46. package/dist/assets/tabs-custom-DXv507_2.js +1 -0
  47. package/dist/assets/{trash-2-DfXI7-ap.js → trash-2-DFZmW6Gg.js} +1 -1
  48. package/dist/assets/useConfirmDialog-Bs5Ll17m.js +1 -0
  49. package/dist/assets/{useMutation-s2sn2yzh.js → useMutation-DrZrOgVL.js} +1 -1
  50. package/dist/assets/x-D7Q1yqSF.js +1 -0
  51. package/dist/index.html +18 -18
  52. package/package.json +6 -6
  53. package/src/api/ncp-session.test.ts +37 -0
  54. package/src/api/ncp-session.ts +29 -1
  55. package/src/api/server-path.ts +23 -0
  56. package/src/api/types.ts +41 -0
  57. package/src/components/chat/ChatConversationPanel.test.tsx +43 -7
  58. package/src/components/chat/ChatConversationPanel.tsx +23 -17
  59. package/src/components/chat/ChatSidebar.test.tsx +2 -2
  60. package/src/components/chat/ChatSidebar.tsx +2 -2
  61. package/src/components/chat/adapters/chat-input-bar.adapter.test.ts +1 -0
  62. package/src/components/chat/adapters/chat-input-bar.adapter.ts +7 -2
  63. package/src/components/chat/adapters/chat-message-part.adapter.ts +81 -6
  64. package/src/components/chat/adapters/chat-message.adapter.test.ts +393 -3
  65. package/src/components/chat/adapters/chat-message.partial-json.ts +89 -0
  66. package/src/components/chat/adapters/file-operation/card.ts +330 -0
  67. package/src/components/chat/adapters/file-operation/diff.ts +398 -0
  68. package/src/components/chat/adapters/file-operation/line-builder.ts +249 -0
  69. package/src/components/chat/adapters/file-operation/record-readers.ts +233 -0
  70. package/src/components/chat/chat-composer-state.ts +3 -3
  71. package/src/components/chat/chat-session-display.test.ts +21 -0
  72. package/src/components/chat/chat-session-display.ts +6 -1
  73. package/src/components/chat/containers/chat-input-bar.container.tsx +29 -32
  74. package/src/components/chat/containers/chat-message-list.container.tsx +1 -0
  75. package/src/components/chat/hooks/use-chat-session-label.ts +19 -0
  76. package/src/components/chat/hooks/use-chat-session-project.test.tsx +117 -0
  77. package/src/components/chat/hooks/use-chat-session-project.ts +40 -0
  78. package/src/components/chat/{chat-session-label.service.ts → hooks/use-chat-session-update.ts} +11 -7
  79. package/src/components/chat/managers/chat-session-list.manager.ts +5 -1
  80. package/src/components/chat/ncp/NcpChatPage.tsx +55 -17
  81. package/src/components/chat/ncp/ncp-chat-page-data.test.ts +33 -0
  82. package/src/components/chat/ncp/ncp-chat-page-data.ts +21 -15
  83. package/src/components/chat/ncp/ncp-session-adapter.test.ts +176 -0
  84. package/src/components/chat/ncp/ncp-session-adapter.ts +16 -0
  85. package/src/components/chat/session-header/chat-session-header-actions.test.tsx +63 -0
  86. package/src/components/chat/session-header/chat-session-header-actions.tsx +95 -0
  87. package/src/components/chat/session-header/chat-session-header-menu-item.tsx +35 -0
  88. package/src/components/chat/session-header/chat-session-project-badge.test.tsx +66 -0
  89. package/src/components/chat/session-header/chat-session-project-badge.tsx +102 -0
  90. package/src/components/chat/session-header/chat-session-project-dialog.tsx +34 -0
  91. package/src/components/chat/stores/chat-input.store.ts +6 -3
  92. package/src/components/chat/stores/chat-thread.store.ts +6 -2
  93. package/src/components/chat/useNcpAgentRuntime.test.tsx +90 -0
  94. package/src/components/path-picker/server-path-picker-dialog.test.tsx +92 -0
  95. package/src/components/path-picker/server-path-picker-dialog.tsx +282 -0
  96. package/src/hooks/server-path/use-server-path-browse.ts +19 -0
  97. package/src/hooks/useConfig.ts +26 -1
  98. package/src/lib/i18n/i18n-language-owner.ts +94 -0
  99. package/src/lib/i18n/i18n.path-picker.ts +12 -0
  100. package/src/lib/i18n.chat.ts +25 -1
  101. package/src/lib/i18n.ts +21 -84
  102. package/src/lib/session-project/session-project.utils.ts +30 -0
  103. package/src/remote/remote-access-feedback.service.test.ts +18 -0
  104. package/src/remote/remote-access-feedback.service.ts +10 -1
  105. package/dist/assets/ChatPage-l2PYwCeB.js +0 -38
  106. package/dist/assets/DocBrowser-CIHLqoIm.js +0 -1
  107. package/dist/assets/MarketplacePage-TVeyVOuO.js +0 -1
  108. package/dist/assets/ModelConfig-Dg6F3Ldb.js +0 -1
  109. package/dist/assets/ProvidersList-f7bQdRxA.js +0 -1
  110. package/dist/assets/RemoteAccessPage-w_dY7P4T.js +0 -1
  111. package/dist/assets/RuntimeConfig-M4OKjmgU.js +0 -1
  112. package/dist/assets/chat-session-display-CGfXhJoT.js +0 -1
  113. package/dist/assets/i18n-CXBpwAwA.js +0 -1
  114. package/dist/assets/index-BahpXJg8.css +0 -1
  115. package/dist/assets/loader-circle-qgU4zQDw.js +0 -1
  116. package/dist/assets/plus-C9cYVbL-.js +0 -1
  117. package/dist/assets/provider-models-C8JQUd1E.js +0 -1
  118. package/dist/assets/search-sl1OeJFl.js +0 -1
  119. package/dist/assets/skeleton-rgIt7a5q.js +0 -1
  120. package/dist/assets/tabs-custom-DKYQxrx1.js +0 -1
  121. package/dist/assets/useConfirmDialog-CXDAxtRL.js +0 -1
  122. package/dist/assets/x-MIimOGs6.js +0 -1
  123. /package/dist/assets/{config-hints-fGnUjDe9.js → config-hints-WtpHP_DW.js} +0 -0
  124. /package/dist/assets/{config-layout-B-7erZRN.js → config-layout-LQ10ozRC.js} +0 -0
  125. /package/dist/assets/{marketplace-localization-CXeGRf6E.js → marketplace-localization-CxSTG9wr.js} +0 -0
@@ -0,0 +1,398 @@
1
+ import type { ChatFileOperationLineViewModel } from "@nextclaw/agent-chat-ui";
2
+ import {
3
+ buildLineDiff,
4
+ buildPreviewLines,
5
+ createLine,
6
+ incrementLineNumber,
7
+ readUnifiedDiffHunkStart,
8
+ splitLines,
9
+ } from "@/components/chat/adapters/file-operation/line-builder";
10
+
11
+ export type ParsedBlock = {
12
+ path: string;
13
+ display: "preview" | "diff";
14
+ caption?: string;
15
+ lines: ChatFileOperationLineViewModel[];
16
+ rawText?: string;
17
+ truncated?: boolean;
18
+ };
19
+
20
+ const MAX_VISIBLE_DIFF_LINES = 120;
21
+
22
+ function buildCaption(params: {
23
+ operation?: string | null;
24
+ lines: ChatFileOperationLineViewModel[];
25
+ }): string | undefined {
26
+ const additions = params.lines.filter((line) => line.kind === "add").length;
27
+ const deletions = params.lines.filter(
28
+ (line) => line.kind === "remove",
29
+ ).length;
30
+ const parts: string[] = [];
31
+ const normalizedOperation = params.operation?.trim().toLowerCase() ?? "";
32
+ if (normalizedOperation && normalizedOperation !== "update") {
33
+ parts.push(normalizedOperation);
34
+ }
35
+ if (additions > 0) {
36
+ parts.push(`+${additions}`);
37
+ }
38
+ if (deletions > 0) {
39
+ parts.push(`-${deletions}`);
40
+ }
41
+ return parts.length > 0 ? parts.join(" · ") : undefined;
42
+ }
43
+
44
+ function readDefaultDiffStartLines(params: {
45
+ operation?: string | null;
46
+ beforeText?: string | null;
47
+ afterText?: string | null;
48
+ oldStartLine?: number | null;
49
+ newStartLine?: number | null;
50
+ }): {
51
+ oldStartLine?: number;
52
+ newStartLine?: number;
53
+ } {
54
+ const normalizedOperation = params.operation?.trim().toLowerCase() ?? "";
55
+ const oldStartLine =
56
+ typeof params.oldStartLine === "number"
57
+ ? params.oldStartLine
58
+ : (normalizedOperation === "delete" ||
59
+ normalizedOperation === "remove") &&
60
+ params.beforeText != null
61
+ ? 1
62
+ : undefined;
63
+ const newStartLine =
64
+ typeof params.newStartLine === "number"
65
+ ? params.newStartLine
66
+ : (normalizedOperation === "write" || normalizedOperation === "add") &&
67
+ params.afterText != null
68
+ ? 1
69
+ : undefined;
70
+ return { oldStartLine, newStartLine };
71
+ }
72
+
73
+ function limitLines(lines: ChatFileOperationLineViewModel[]): {
74
+ lines: ChatFileOperationLineViewModel[];
75
+ truncated: boolean;
76
+ } {
77
+ if (lines.length <= MAX_VISIBLE_DIFF_LINES) {
78
+ return { lines, truncated: false };
79
+ }
80
+ return {
81
+ lines: lines.slice(0, MAX_VISIBLE_DIFF_LINES),
82
+ truncated: true,
83
+ };
84
+ }
85
+
86
+ export function buildRawPreviewBlock(params: {
87
+ path: string;
88
+ text: string;
89
+ operation?: string | null;
90
+ oldStartLine?: number | null;
91
+ newStartLine?: number | null;
92
+ }): ParsedBlock | null {
93
+ const previewText = params.text.trim();
94
+ if (!previewText) {
95
+ return null;
96
+ }
97
+ const previewKind =
98
+ params.operation?.trim().toLowerCase() === "write" ? "add" : "context";
99
+ const oldStartLine =
100
+ typeof params.oldStartLine === "number" ? params.oldStartLine : 1;
101
+ const newStartLine =
102
+ typeof params.newStartLine === "number" ? params.newStartLine : 1;
103
+ const lines = buildPreviewLines({
104
+ text: previewText,
105
+ kind: previewKind,
106
+ oldStartLine,
107
+ newStartLine,
108
+ });
109
+ return {
110
+ path: params.path,
111
+ display: "preview",
112
+ caption: buildCaption({
113
+ operation: params.operation,
114
+ lines,
115
+ }),
116
+ lines,
117
+ };
118
+ }
119
+
120
+ export function buildFullReplaceBlock(params: {
121
+ path: string;
122
+ beforeText?: string | null;
123
+ afterText?: string | null;
124
+ operation?: string | null;
125
+ oldStartLine?: number | null;
126
+ newStartLine?: number | null;
127
+ }): ParsedBlock | null {
128
+ const { oldStartLine, newStartLine } = readDefaultDiffStartLines(params);
129
+ const lines = buildLineDiff({
130
+ beforeText: params.beforeText ?? "",
131
+ afterText: params.afterText ?? "",
132
+ oldStartLine,
133
+ newStartLine,
134
+ });
135
+ const limited = limitLines(lines);
136
+ if (limited.lines.length === 0) {
137
+ return null;
138
+ }
139
+ return {
140
+ path: params.path,
141
+ display: "diff",
142
+ caption: buildCaption({
143
+ operation: params.operation,
144
+ lines,
145
+ }),
146
+ lines: limited.lines,
147
+ truncated: limited.truncated,
148
+ };
149
+ }
150
+
151
+ function buildParsedPatchBlock(params: {
152
+ path: string;
153
+ operation: string | null;
154
+ lines: ChatFileOperationLineViewModel[];
155
+ }): ParsedBlock {
156
+ const limited = limitLines(params.lines);
157
+ return {
158
+ path: params.path,
159
+ display: "diff",
160
+ caption: buildCaption({
161
+ operation: params.operation,
162
+ lines: params.lines,
163
+ }),
164
+ lines: limited.lines,
165
+ truncated: limited.truncated,
166
+ };
167
+ }
168
+
169
+ function updateApplyPatchCursor(params: {
170
+ line: string;
171
+ flushCurrent: () => void;
172
+ setCurrent: (path: string, operation: string) => void;
173
+ }): boolean {
174
+ if (params.line.startsWith("*** Update File: ")) {
175
+ params.flushCurrent();
176
+ params.setCurrent(
177
+ params.line.slice("*** Update File: ".length).trim(),
178
+ "update",
179
+ );
180
+ return true;
181
+ }
182
+ if (params.line.startsWith("*** Add File: ")) {
183
+ params.flushCurrent();
184
+ params.setCurrent(params.line.slice("*** Add File: ".length).trim(), "add");
185
+ return true;
186
+ }
187
+ if (params.line.startsWith("*** Delete File: ")) {
188
+ params.flushCurrent();
189
+ params.setCurrent(
190
+ params.line.slice("*** Delete File: ".length).trim(),
191
+ "delete",
192
+ );
193
+ return true;
194
+ }
195
+ return false;
196
+ }
197
+
198
+ function appendPatchLine(params: {
199
+ currentLines: ChatFileOperationLineViewModel[];
200
+ line: string;
201
+ oldLineNumber?: number;
202
+ newLineNumber?: number;
203
+ }): {
204
+ oldLineNumber?: number;
205
+ newLineNumber?: number;
206
+ } {
207
+ const { currentLines, line } = params;
208
+ if (line.startsWith("+")) {
209
+ currentLines.push(
210
+ createLine({
211
+ kind: "add",
212
+ text: line.slice(1),
213
+ newLineNumber: params.newLineNumber,
214
+ }),
215
+ );
216
+ return {
217
+ oldLineNumber: params.oldLineNumber,
218
+ newLineNumber: incrementLineNumber(params.newLineNumber),
219
+ };
220
+ }
221
+ if (line.startsWith("-")) {
222
+ currentLines.push(
223
+ createLine({
224
+ kind: "remove",
225
+ text: line.slice(1),
226
+ oldLineNumber: params.oldLineNumber,
227
+ }),
228
+ );
229
+ return {
230
+ oldLineNumber: incrementLineNumber(params.oldLineNumber),
231
+ newLineNumber: params.newLineNumber,
232
+ };
233
+ }
234
+ if (line.startsWith(" ")) {
235
+ currentLines.push(
236
+ createLine({
237
+ kind: "context",
238
+ text: line.slice(1),
239
+ oldLineNumber: params.oldLineNumber,
240
+ newLineNumber: params.newLineNumber,
241
+ }),
242
+ );
243
+ return {
244
+ oldLineNumber: incrementLineNumber(params.oldLineNumber),
245
+ newLineNumber: incrementLineNumber(params.newLineNumber),
246
+ };
247
+ }
248
+ return {
249
+ oldLineNumber: params.oldLineNumber,
250
+ newLineNumber: params.newLineNumber,
251
+ };
252
+ }
253
+
254
+ function parseApplyPatchText(patchText: string): ParsedBlock[] {
255
+ const blocks: ParsedBlock[] = [];
256
+ let currentPath: string | null = null;
257
+ let currentOperation: string | null = null;
258
+ let currentLines: ChatFileOperationLineViewModel[] = [];
259
+ let currentOldLineNumber: number | undefined;
260
+ let currentNewLineNumber: number | undefined;
261
+
262
+ const flushCurrent = () => {
263
+ if (!currentPath) {
264
+ currentLines = [];
265
+ currentOperation = null;
266
+ currentOldLineNumber = undefined;
267
+ currentNewLineNumber = undefined;
268
+ return;
269
+ }
270
+ blocks.push(
271
+ buildParsedPatchBlock({
272
+ path: currentPath,
273
+ operation: currentOperation,
274
+ lines: currentLines,
275
+ }),
276
+ );
277
+ currentPath = null;
278
+ currentOperation = null;
279
+ currentLines = [];
280
+ currentOldLineNumber = undefined;
281
+ currentNewLineNumber = undefined;
282
+ };
283
+
284
+ for (const line of splitLines(patchText)) {
285
+ if (
286
+ updateApplyPatchCursor({
287
+ line,
288
+ flushCurrent,
289
+ setCurrent: (path, operation) => {
290
+ currentPath = path;
291
+ currentOperation = operation;
292
+ },
293
+ })
294
+ ) {
295
+ continue;
296
+ }
297
+ if (
298
+ line.startsWith("*** Move to: ") ||
299
+ line.startsWith("*** Begin Patch") ||
300
+ line.startsWith("*** End Patch")
301
+ ) {
302
+ continue;
303
+ }
304
+ if (line.startsWith("@@")) {
305
+ const hunkStart = readUnifiedDiffHunkStart(line);
306
+ currentOldLineNumber = hunkStart?.oldLineNumber;
307
+ currentNewLineNumber = hunkStart?.newLineNumber;
308
+ continue;
309
+ }
310
+ if (!currentPath) {
311
+ continue;
312
+ }
313
+ const nextCursor = appendPatchLine({
314
+ currentLines,
315
+ line,
316
+ oldLineNumber: currentOldLineNumber,
317
+ newLineNumber: currentNewLineNumber,
318
+ });
319
+ currentOldLineNumber = nextCursor.oldLineNumber;
320
+ currentNewLineNumber = nextCursor.newLineNumber;
321
+ }
322
+
323
+ flushCurrent();
324
+ return blocks;
325
+ }
326
+
327
+ function parseUnifiedDiffText(patchText: string): ParsedBlock[] {
328
+ const blocks: ParsedBlock[] = [];
329
+ let currentPath: string | null = null;
330
+ let currentLines: ChatFileOperationLineViewModel[] = [];
331
+ let currentOldLineNumber: number | undefined;
332
+ let currentNewLineNumber: number | undefined;
333
+
334
+ const flushCurrent = () => {
335
+ if (!currentPath) {
336
+ currentLines = [];
337
+ currentOldLineNumber = undefined;
338
+ currentNewLineNumber = undefined;
339
+ return;
340
+ }
341
+ blocks.push(
342
+ buildParsedPatchBlock({
343
+ path: currentPath,
344
+ operation: "update",
345
+ lines: currentLines,
346
+ }),
347
+ );
348
+ currentPath = null;
349
+ currentLines = [];
350
+ currentOldLineNumber = undefined;
351
+ currentNewLineNumber = undefined;
352
+ };
353
+
354
+ for (const line of splitLines(patchText)) {
355
+ if (line.startsWith("+++ ")) {
356
+ flushCurrent();
357
+ currentPath = line
358
+ .slice(4)
359
+ .trim()
360
+ .replace(/^b\//, "")
361
+ .replace(/^a\//, "");
362
+ continue;
363
+ }
364
+ if (line.startsWith("--- ")) {
365
+ continue;
366
+ }
367
+ if (line.startsWith("@@")) {
368
+ const hunkStart = readUnifiedDiffHunkStart(line);
369
+ currentOldLineNumber = hunkStart?.oldLineNumber;
370
+ currentNewLineNumber = hunkStart?.newLineNumber;
371
+ continue;
372
+ }
373
+ if (!currentPath) {
374
+ continue;
375
+ }
376
+ const nextCursor = appendPatchLine({
377
+ currentLines,
378
+ line,
379
+ oldLineNumber: currentOldLineNumber,
380
+ newLineNumber: currentNewLineNumber,
381
+ });
382
+ currentOldLineNumber = nextCursor.oldLineNumber;
383
+ currentNewLineNumber = nextCursor.newLineNumber;
384
+ }
385
+
386
+ flushCurrent();
387
+ return blocks;
388
+ }
389
+
390
+ export function parsePatchBlocks(patchText: string): ParsedBlock[] {
391
+ if (patchText.includes("*** Begin Patch")) {
392
+ return parseApplyPatchText(patchText);
393
+ }
394
+ if (patchText.includes("--- ") && patchText.includes("+++ ")) {
395
+ return parseUnifiedDiffText(patchText);
396
+ }
397
+ return [];
398
+ }
@@ -0,0 +1,249 @@
1
+ import type { ChatFileOperationLineViewModel } from "@nextclaw/agent-chat-ui";
2
+
3
+ const MAX_DIFF_MATRIX_CELLS = 12_000;
4
+ const UNIFIED_DIFF_HUNK_HEADER_PATTERN =
5
+ /^@@ -(\d+)(?:,\d+)? \+(\d+)(?:,\d+)? @@/;
6
+
7
+ export function splitLines(value: string): string[] {
8
+ const normalized = value.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
9
+ return normalized === "" ? [] : normalized.split("\n");
10
+ }
11
+
12
+ export function incrementLineNumber(value?: number): number | undefined {
13
+ return typeof value === "number" ? value + 1 : undefined;
14
+ }
15
+
16
+ export function createLine(params: {
17
+ kind: ChatFileOperationLineViewModel["kind"];
18
+ text: string;
19
+ oldLineNumber?: number;
20
+ newLineNumber?: number;
21
+ }): ChatFileOperationLineViewModel {
22
+ return {
23
+ kind: params.kind,
24
+ text: params.text,
25
+ ...(typeof params.oldLineNumber === "number"
26
+ ? { oldLineNumber: params.oldLineNumber }
27
+ : {}),
28
+ ...(typeof params.newLineNumber === "number"
29
+ ? { newLineNumber: params.newLineNumber }
30
+ : {}),
31
+ };
32
+ }
33
+
34
+ export function readUnifiedDiffHunkStart(line: string): {
35
+ oldLineNumber: number;
36
+ newLineNumber: number;
37
+ } | null {
38
+ const match = UNIFIED_DIFF_HUNK_HEADER_PATTERN.exec(line);
39
+ if (!match) {
40
+ return null;
41
+ }
42
+ return {
43
+ oldLineNumber: Number(match[1]),
44
+ newLineNumber: Number(match[2]),
45
+ };
46
+ }
47
+
48
+ export function buildPreviewLines(params: {
49
+ text: string;
50
+ kind: "add" | "context";
51
+ oldStartLine: number;
52
+ newStartLine: number;
53
+ }): ChatFileOperationLineViewModel[] {
54
+ return splitLines(params.text).map((line, index) =>
55
+ params.kind === "add"
56
+ ? createLine({
57
+ kind: "add",
58
+ text: line,
59
+ newLineNumber: params.newStartLine + index,
60
+ })
61
+ : createLine({
62
+ kind: "context",
63
+ text: line,
64
+ oldLineNumber: params.oldStartLine + index,
65
+ newLineNumber: params.newStartLine + index,
66
+ }),
67
+ );
68
+ }
69
+
70
+ function buildFallbackDiffLines(params: {
71
+ beforeLines: string[];
72
+ afterLines: string[];
73
+ oldStartLine?: number;
74
+ newStartLine?: number;
75
+ }): ChatFileOperationLineViewModel[] {
76
+ let oldLineNumber = params.oldStartLine;
77
+ let newLineNumber = params.newStartLine;
78
+ return [
79
+ ...params.beforeLines.map((line) => {
80
+ const nextLine = createLine({
81
+ kind: "remove",
82
+ text: line,
83
+ oldLineNumber,
84
+ });
85
+ oldLineNumber = incrementLineNumber(oldLineNumber);
86
+ return nextLine;
87
+ }),
88
+ ...params.afterLines.map((line) => {
89
+ const nextLine = createLine({
90
+ kind: "add",
91
+ text: line,
92
+ newLineNumber,
93
+ });
94
+ newLineNumber = incrementLineNumber(newLineNumber);
95
+ return nextLine;
96
+ }),
97
+ ];
98
+ }
99
+
100
+ function buildLcsMatrix(params: {
101
+ beforeLines: string[];
102
+ afterLines: string[];
103
+ }): number[][] {
104
+ const matrix: number[][] = Array.from(
105
+ { length: params.beforeLines.length + 1 },
106
+ () => Array.from({ length: params.afterLines.length + 1 }, () => 0),
107
+ );
108
+ for (
109
+ let beforeIndex = params.beforeLines.length - 1;
110
+ beforeIndex >= 0;
111
+ beforeIndex -= 1
112
+ ) {
113
+ for (
114
+ let afterIndex = params.afterLines.length - 1;
115
+ afterIndex >= 0;
116
+ afterIndex -= 1
117
+ ) {
118
+ matrix[beforeIndex]![afterIndex] =
119
+ params.beforeLines[beforeIndex] === params.afterLines[afterIndex]
120
+ ? (matrix[beforeIndex + 1]![afterIndex + 1] ?? 0) + 1
121
+ : Math.max(
122
+ matrix[beforeIndex + 1]![afterIndex] ?? 0,
123
+ matrix[beforeIndex]![afterIndex + 1] ?? 0,
124
+ );
125
+ }
126
+ }
127
+ return matrix;
128
+ }
129
+
130
+ function appendRemainingDiffLines(params: {
131
+ lines: ChatFileOperationLineViewModel[];
132
+ beforeLines: string[];
133
+ afterLines: string[];
134
+ beforeIndex: number;
135
+ afterIndex: number;
136
+ oldLineNumber?: number;
137
+ newLineNumber?: number;
138
+ }): void {
139
+ for (
140
+ let index = params.beforeIndex;
141
+ index < params.beforeLines.length;
142
+ index += 1
143
+ ) {
144
+ params.lines.push(
145
+ createLine({
146
+ kind: "remove",
147
+ text: params.beforeLines[index] ?? "",
148
+ oldLineNumber: params.oldLineNumber,
149
+ }),
150
+ );
151
+ params.oldLineNumber = incrementLineNumber(params.oldLineNumber);
152
+ }
153
+ for (
154
+ let index = params.afterIndex;
155
+ index < params.afterLines.length;
156
+ index += 1
157
+ ) {
158
+ params.lines.push(
159
+ createLine({
160
+ kind: "add",
161
+ text: params.afterLines[index] ?? "",
162
+ newLineNumber: params.newLineNumber,
163
+ }),
164
+ );
165
+ params.newLineNumber = incrementLineNumber(params.newLineNumber);
166
+ }
167
+ }
168
+
169
+ export function buildLineDiff(params: {
170
+ beforeText: string;
171
+ afterText: string;
172
+ oldStartLine?: number;
173
+ newStartLine?: number;
174
+ }): ChatFileOperationLineViewModel[] {
175
+ const beforeLines = splitLines(params.beforeText);
176
+ const afterLines = splitLines(params.afterText);
177
+ if (beforeLines.length * afterLines.length > MAX_DIFF_MATRIX_CELLS) {
178
+ return buildFallbackDiffLines({
179
+ beforeLines,
180
+ afterLines,
181
+ oldStartLine: params.oldStartLine,
182
+ newStartLine: params.newStartLine,
183
+ });
184
+ }
185
+
186
+ const lcs = buildLcsMatrix({
187
+ beforeLines,
188
+ afterLines,
189
+ });
190
+ const lines: ChatFileOperationLineViewModel[] = [];
191
+ let beforeIndex = 0;
192
+ let afterIndex = 0;
193
+ let oldLineNumber = params.oldStartLine;
194
+ let newLineNumber = params.newStartLine;
195
+ while (beforeIndex < beforeLines.length && afterIndex < afterLines.length) {
196
+ if (beforeLines[beforeIndex] === afterLines[afterIndex]) {
197
+ lines.push(
198
+ createLine({
199
+ kind: "context",
200
+ text: beforeLines[beforeIndex] ?? "",
201
+ oldLineNumber,
202
+ newLineNumber,
203
+ }),
204
+ );
205
+ beforeIndex += 1;
206
+ afterIndex += 1;
207
+ oldLineNumber = incrementLineNumber(oldLineNumber);
208
+ newLineNumber = incrementLineNumber(newLineNumber);
209
+ continue;
210
+ }
211
+
212
+ if (
213
+ (lcs[beforeIndex + 1]![afterIndex] ?? 0) >=
214
+ (lcs[beforeIndex]![afterIndex + 1] ?? 0)
215
+ ) {
216
+ lines.push(
217
+ createLine({
218
+ kind: "remove",
219
+ text: beforeLines[beforeIndex] ?? "",
220
+ oldLineNumber,
221
+ }),
222
+ );
223
+ beforeIndex += 1;
224
+ oldLineNumber = incrementLineNumber(oldLineNumber);
225
+ continue;
226
+ }
227
+
228
+ lines.push(
229
+ createLine({
230
+ kind: "add",
231
+ text: afterLines[afterIndex] ?? "",
232
+ newLineNumber,
233
+ }),
234
+ );
235
+ afterIndex += 1;
236
+ newLineNumber = incrementLineNumber(newLineNumber);
237
+ }
238
+
239
+ appendRemainingDiffLines({
240
+ lines,
241
+ beforeLines,
242
+ afterLines,
243
+ beforeIndex,
244
+ afterIndex,
245
+ oldLineNumber,
246
+ newLineNumber,
247
+ });
248
+ return lines;
249
+ }