bingocode 1.0.2 → 1.0.3

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 (186) hide show
  1. package/desktop/README.md +30 -0
  2. package/desktop/bunfig.toml +1 -0
  3. package/desktop/index.html +17 -0
  4. package/desktop/package.json +55 -0
  5. package/desktop/pnpm-lock.yaml +3832 -0
  6. package/desktop/public/app-icon.jpg +0 -0
  7. package/desktop/public/fonts/inter-latin-ext.woff2 +0 -0
  8. package/desktop/public/fonts/inter-latin.woff2 +0 -0
  9. package/desktop/public/fonts/jetbrains-mono-latin-ext.woff2 +0 -0
  10. package/desktop/public/fonts/jetbrains-mono-latin.woff2 +0 -0
  11. package/desktop/public/fonts/manrope-latin-ext.woff2 +0 -0
  12. package/desktop/public/fonts/manrope-latin.woff2 +0 -0
  13. package/desktop/public/fonts/material-symbols-outlined.woff2 +0 -0
  14. package/desktop/public/icons/bilibili.svg +1 -0
  15. package/desktop/public/icons/douyin.svg +1 -0
  16. package/desktop/public/icons/github.svg +3 -0
  17. package/desktop/public/icons/xiaohongshu.svg +1 -0
  18. package/desktop/scripts/build-macos-arm64.sh +270 -0
  19. package/desktop/scripts/build-sidecars.ts +183 -0
  20. package/desktop/scripts/build-windows-x64.ps1 +295 -0
  21. package/desktop/scripts/scan-missing-imports.ts +235 -0
  22. package/desktop/sidecars/claude-sidecar.ts +156 -0
  23. package/desktop/src/App.tsx +5 -0
  24. package/desktop/src/__tests__/agentsSettings.test.tsx +349 -0
  25. package/desktop/src/__tests__/pages.test.tsx +290 -0
  26. package/desktop/src/__tests__/skillsSettings.test.tsx +205 -0
  27. package/desktop/src/api/adapters.ts +12 -0
  28. package/desktop/src/api/agents.ts +36 -0
  29. package/desktop/src/api/cliTasks.ts +28 -0
  30. package/desktop/src/api/client.ts +63 -0
  31. package/desktop/src/api/computerUse.ts +76 -0
  32. package/desktop/src/api/filesystem.ts +30 -0
  33. package/desktop/src/api/hahaOAuth.ts +38 -0
  34. package/desktop/src/api/models.ts +28 -0
  35. package/desktop/src/api/providers.ts +63 -0
  36. package/desktop/src/api/search.ts +29 -0
  37. package/desktop/src/api/sessions.ts +56 -0
  38. package/desktop/src/api/settings.ts +20 -0
  39. package/desktop/src/api/skills.ts +19 -0
  40. package/desktop/src/api/tasks.ts +36 -0
  41. package/desktop/src/api/teams.ts +44 -0
  42. package/desktop/src/api/websocket.ts +164 -0
  43. package/desktop/src/components/chat/AskUserQuestion.tsx +268 -0
  44. package/desktop/src/components/chat/AssistantMessage.tsx +29 -0
  45. package/desktop/src/components/chat/AttachmentGallery.tsx +113 -0
  46. package/desktop/src/components/chat/ChatInput.tsx +622 -0
  47. package/desktop/src/components/chat/CodeViewer.tsx +161 -0
  48. package/desktop/src/components/chat/ComputerUsePermissionModal.test.tsx +174 -0
  49. package/desktop/src/components/chat/ComputerUsePermissionModal.tsx +311 -0
  50. package/desktop/src/components/chat/DiffViewer.tsx +157 -0
  51. package/desktop/src/components/chat/FileSearchMenu.tsx +198 -0
  52. package/desktop/src/components/chat/ImageGalleryModal.tsx +91 -0
  53. package/desktop/src/components/chat/InlineImageGallery.tsx +106 -0
  54. package/desktop/src/components/chat/InlineTaskSummary.tsx +60 -0
  55. package/desktop/src/components/chat/MermaidRenderer.test.tsx +98 -0
  56. package/desktop/src/components/chat/MermaidRenderer.tsx +361 -0
  57. package/desktop/src/components/chat/MessageActionBar.tsx +27 -0
  58. package/desktop/src/components/chat/MessageList.test.tsx +313 -0
  59. package/desktop/src/components/chat/MessageList.tsx +249 -0
  60. package/desktop/src/components/chat/PermissionDialog.tsx +262 -0
  61. package/desktop/src/components/chat/SessionTaskBar.test.tsx +99 -0
  62. package/desktop/src/components/chat/SessionTaskBar.tsx +159 -0
  63. package/desktop/src/components/chat/StreamingIndicator.tsx +41 -0
  64. package/desktop/src/components/chat/TerminalChrome.tsx +35 -0
  65. package/desktop/src/components/chat/ThinkingBlock.tsx +87 -0
  66. package/desktop/src/components/chat/ToolCallBlock.tsx +247 -0
  67. package/desktop/src/components/chat/ToolCallGroup.tsx +617 -0
  68. package/desktop/src/components/chat/ToolResultBlock.tsx +107 -0
  69. package/desktop/src/components/chat/UserMessage.tsx +38 -0
  70. package/desktop/src/components/chat/chatBlocks.test.tsx +136 -0
  71. package/desktop/src/components/chat/clipboard.ts +25 -0
  72. package/desktop/src/components/chat/composerUtils.test.ts +55 -0
  73. package/desktop/src/components/chat/composerUtils.ts +149 -0
  74. package/desktop/src/components/controls/ModelSelector.tsx +156 -0
  75. package/desktop/src/components/controls/PermissionModeSelector.tsx +229 -0
  76. package/desktop/src/components/layout/AppShell.tsx +107 -0
  77. package/desktop/src/components/layout/ContentRouter.tsx +27 -0
  78. package/desktop/src/components/layout/ProjectFilter.tsx +126 -0
  79. package/desktop/src/components/layout/Sidebar.test.tsx +158 -0
  80. package/desktop/src/components/layout/Sidebar.tsx +384 -0
  81. package/desktop/src/components/layout/StatusBar.tsx +31 -0
  82. package/desktop/src/components/layout/TabBar.test.tsx +136 -0
  83. package/desktop/src/components/layout/TabBar.tsx +318 -0
  84. package/desktop/src/components/layout/TitleBar.tsx +96 -0
  85. package/desktop/src/components/layout/WindowControls.test.tsx +69 -0
  86. package/desktop/src/components/layout/WindowControls.tsx +89 -0
  87. package/desktop/src/components/markdown/MarkdownRenderer.test.tsx +100 -0
  88. package/desktop/src/components/markdown/MarkdownRenderer.tsx +229 -0
  89. package/desktop/src/components/settings/ClaudeOfficialLogin.tsx +107 -0
  90. package/desktop/src/components/shared/Button.tsx +63 -0
  91. package/desktop/src/components/shared/CopyButton.tsx +58 -0
  92. package/desktop/src/components/shared/DirectoryPicker.tsx +316 -0
  93. package/desktop/src/components/shared/Dropdown.tsx +91 -0
  94. package/desktop/src/components/shared/Input.tsx +38 -0
  95. package/desktop/src/components/shared/Modal.tsx +65 -0
  96. package/desktop/src/components/shared/ProjectContextChip.tsx +30 -0
  97. package/desktop/src/components/shared/Spinner.tsx +30 -0
  98. package/desktop/src/components/shared/Textarea.tsx +38 -0
  99. package/desktop/src/components/shared/Toast.tsx +47 -0
  100. package/desktop/src/components/shared/UpdateChecker.tsx +90 -0
  101. package/desktop/src/components/skills/SkillDetail.test.tsx +89 -0
  102. package/desktop/src/components/skills/SkillDetail.tsx +403 -0
  103. package/desktop/src/components/skills/SkillList.tsx +254 -0
  104. package/desktop/src/components/tasks/DayOfWeekPicker.tsx +57 -0
  105. package/desktop/src/components/tasks/NewTaskModal.tsx +407 -0
  106. package/desktop/src/components/tasks/PromptEditor.tsx +74 -0
  107. package/desktop/src/components/tasks/TaskEmptyState.tsx +30 -0
  108. package/desktop/src/components/tasks/TaskList.tsx +46 -0
  109. package/desktop/src/components/tasks/TaskRow.tsx +253 -0
  110. package/desktop/src/components/tasks/TaskRunsPanel.tsx +195 -0
  111. package/desktop/src/components/teams/TeamStatusBar.tsx +147 -0
  112. package/desktop/src/config/providerPresets.ts +78 -0
  113. package/desktop/src/config/spinnerVerbs.ts +193 -0
  114. package/desktop/src/hooks/useKeyboardShortcuts.ts +60 -0
  115. package/desktop/src/i18n/index.ts +54 -0
  116. package/desktop/src/i18n/locales/en.ts +670 -0
  117. package/desktop/src/i18n/locales/zh.ts +670 -0
  118. package/desktop/src/lib/__tests__/cronDescribe.test.ts +93 -0
  119. package/desktop/src/lib/cronDescribe.ts +188 -0
  120. package/desktop/src/lib/desktopRuntime.ts +54 -0
  121. package/desktop/src/lib/parseRunOutput.ts +79 -0
  122. package/desktop/src/main.tsx +13 -0
  123. package/desktop/src/mocks/data.ts +202 -0
  124. package/desktop/src/pages/ActiveSession.test.tsx +181 -0
  125. package/desktop/src/pages/ActiveSession.tsx +219 -0
  126. package/desktop/src/pages/AdapterSettings.tsx +375 -0
  127. package/desktop/src/pages/AgentTeams.tsx +200 -0
  128. package/desktop/src/pages/ComputerUseSettings.tsx +420 -0
  129. package/desktop/src/pages/EmptySession.tsx +518 -0
  130. package/desktop/src/pages/NewTaskModal.tsx +346 -0
  131. package/desktop/src/pages/ScheduledTasks.tsx +66 -0
  132. package/desktop/src/pages/ScheduledTasksEmpty.tsx +152 -0
  133. package/desktop/src/pages/ScheduledTasksList.tsx +416 -0
  134. package/desktop/src/pages/SessionControls.tsx +460 -0
  135. package/desktop/src/pages/Settings.tsx +1448 -0
  136. package/desktop/src/pages/ToolInspection.tsx +235 -0
  137. package/desktop/src/stores/adapterStore.ts +106 -0
  138. package/desktop/src/stores/agentStore.ts +34 -0
  139. package/desktop/src/stores/chatStore.test.ts +505 -0
  140. package/desktop/src/stores/chatStore.ts +850 -0
  141. package/desktop/src/stores/cliTaskStore.ts +152 -0
  142. package/desktop/src/stores/hahaOAuthStore.test.ts +77 -0
  143. package/desktop/src/stores/hahaOAuthStore.ts +97 -0
  144. package/desktop/src/stores/providerStore.ts +101 -0
  145. package/desktop/src/stores/sessionStore.test.ts +63 -0
  146. package/desktop/src/stores/sessionStore.ts +102 -0
  147. package/desktop/src/stores/settingsStore.ts +120 -0
  148. package/desktop/src/stores/skillStore.ts +51 -0
  149. package/desktop/src/stores/tabStore.ts +169 -0
  150. package/desktop/src/stores/taskStore.ts +68 -0
  151. package/desktop/src/stores/teamStore.ts +344 -0
  152. package/desktop/src/stores/uiStore.ts +100 -0
  153. package/desktop/src/stores/updateStore.test.ts +71 -0
  154. package/desktop/src/stores/updateStore.ts +221 -0
  155. package/desktop/src/theme/globals.css +465 -0
  156. package/desktop/src/types/adapter.ts +33 -0
  157. package/desktop/src/types/chat.ts +152 -0
  158. package/desktop/src/types/cliTask.ts +24 -0
  159. package/desktop/src/types/provider.ts +62 -0
  160. package/desktop/src/types/session.ts +27 -0
  161. package/desktop/src/types/settings.ts +22 -0
  162. package/desktop/src/types/skill.ts +38 -0
  163. package/desktop/src/types/task.ts +56 -0
  164. package/desktop/src/types/team.ts +38 -0
  165. package/desktop/src-tauri/Cargo.lock +5549 -0
  166. package/desktop/src-tauri/Cargo.toml +20 -0
  167. package/desktop/src-tauri/app-icon.svg +13 -0
  168. package/desktop/src-tauri/build.rs +3 -0
  169. package/desktop/src-tauri/capabilities/default.json +106 -0
  170. package/desktop/src-tauri/icons/android/mipmap-anydpi-v26/ic_launcher.xml +5 -0
  171. package/desktop/src-tauri/icons/android/values/ic_launcher_background.xml +4 -0
  172. package/desktop/src-tauri/icons/icon.icns +0 -0
  173. package/desktop/src-tauri/icons/icon.ico +0 -0
  174. package/desktop/src-tauri/src/lib.rs +408 -0
  175. package/desktop/src-tauri/src/main.rs +6 -0
  176. package/desktop/src-tauri/tauri.conf.json +78 -0
  177. package/desktop/src-tauri/tauri.macos.conf.json +18 -0
  178. package/desktop/src-tauri/tauri.release-ci.json +5 -0
  179. package/desktop/src-tauri/tauri.windows.conf.json +16 -0
  180. package/desktop/src-tauri/windows-installer-hooks.nsh +17 -0
  181. package/desktop/tsconfig.json +25 -0
  182. package/desktop/vite.config.ts +26 -0
  183. package/desktop/vitest.config.ts +18 -0
  184. package/package.json +1 -1
  185. package/src/commands/desktop/desktop.tsx +9 -0
  186. package/src/commands/desktop/index.ts +26 -0
@@ -0,0 +1,247 @@
1
+ import { useMemo, useState } from 'react'
2
+ import { CodeViewer } from './CodeViewer'
3
+ import { DiffViewer } from './DiffViewer'
4
+ import { TerminalChrome } from './TerminalChrome'
5
+ import { CopyButton } from '../shared/CopyButton'
6
+ import { useTranslation } from '../../i18n'
7
+ import type { TranslationKey } from '../../i18n'
8
+ import { InlineImageGallery } from './InlineImageGallery'
9
+ import type { AgentTaskNotification } from '../../types/chat'
10
+
11
+ type Props = {
12
+ toolName: string
13
+ input: unknown
14
+ result?: { content: unknown; isError: boolean } | null
15
+ agentTaskNotification?: AgentTaskNotification
16
+ compact?: boolean
17
+ }
18
+
19
+ const TOOL_ICONS: Record<string, string> = {
20
+ Bash: 'terminal',
21
+ Read: 'description',
22
+ Write: 'edit_document',
23
+ Edit: 'edit_note',
24
+ Glob: 'search',
25
+ Grep: 'find_in_page',
26
+ Agent: 'smart_toy',
27
+ WebSearch: 'travel_explore',
28
+ WebFetch: 'cloud_download',
29
+ NotebookEdit: 'note',
30
+ Skill: 'auto_awesome',
31
+ }
32
+
33
+ export function ToolCallBlock({ toolName, input, result, compact = false }: Props) {
34
+ const [expanded, setExpanded] = useState(false)
35
+ const t = useTranslation()
36
+ const obj = input && typeof input === 'object' ? (input as Record<string, unknown>) : {}
37
+ const icon = TOOL_ICONS[toolName] || 'build'
38
+ const filePath = typeof obj.file_path === 'string' ? obj.file_path : ''
39
+ const summary = getToolSummary(toolName, obj, t)
40
+ const outputSummary = getToolResultSummary(toolName, result?.content, t)
41
+
42
+ const preview = useMemo(() => renderPreview(toolName, obj, result, t), [obj, result, toolName, t])
43
+ const details = useMemo(() => renderDetails(toolName, obj, t), [obj, toolName, t])
44
+ const hasResultDetails = Boolean(result && extractTextContent(result.content))
45
+ const expandable = toolName === 'Edit' || toolName === 'Write' || hasResultDetails
46
+
47
+ return (
48
+ <div className={`overflow-hidden rounded-lg border border-[var(--color-border)]/50 bg-[var(--color-surface-container-lowest)] ${
49
+ compact ? 'mb-0' : 'mb-2 ml-10'
50
+ }`}>
51
+ <button
52
+ type="button"
53
+ onClick={() => {
54
+ if (expandable) {
55
+ setExpanded((value) => !value)
56
+ }
57
+ }}
58
+ className="flex w-full items-center gap-2 px-3 py-2 text-left transition-colors hover:bg-[var(--color-surface-hover)]/50"
59
+ >
60
+ <span className="material-symbols-outlined text-[14px] text-[var(--color-outline)]">{icon}</span>
61
+ <span className="text-[11px] font-semibold text-[var(--color-text-secondary)]">
62
+ {toolName}
63
+ </span>
64
+ {filePath ? (
65
+ <span className="min-w-0 flex-1 truncate font-[var(--font-mono)] text-[11px] text-[var(--color-text-tertiary)]">
66
+ {filePath.split('/').pop()}
67
+ </span>
68
+ ) : summary ? (
69
+ <span className="min-w-0 flex-1 truncate font-[var(--font-mono)] text-[11px] text-[var(--color-text-tertiary)]">
70
+ {summary}
71
+ </span>
72
+ ) : (
73
+ <span className="flex-1" />
74
+ )}
75
+ {result && outputSummary && (
76
+ <span className="shrink-0 text-[10px] text-[var(--color-outline)]">
77
+ {outputSummary}
78
+ </span>
79
+ )}
80
+ {result?.isError && (
81
+ <span className="material-symbols-outlined shrink-0 text-[14px] text-[var(--color-error)]">error</span>
82
+ )}
83
+ {expandable && (
84
+ <span className="material-symbols-outlined text-[14px] text-[var(--color-outline)]">
85
+ {expanded ? 'expand_less' : 'expand_more'}
86
+ </span>
87
+ )}
88
+ </button>
89
+
90
+ {expandable && expanded && (
91
+ <div className="space-y-2.5 border-t border-[var(--color-border)]/60 px-3 py-3">
92
+ {preview}
93
+ {details}
94
+ </div>
95
+ )}
96
+ </div>
97
+ )
98
+ }
99
+
100
+ function renderPreview(
101
+ toolName: string,
102
+ obj: Record<string, unknown>,
103
+ result?: { content: unknown; isError: boolean } | null,
104
+ t?: (key: TranslationKey, params?: Record<string, string | number>) => string,
105
+ ) {
106
+ const filePath = typeof obj.file_path === 'string' ? obj.file_path : 'file'
107
+
108
+ if (toolName === 'Edit' && typeof obj.old_string === 'string' && typeof obj.new_string === 'string') {
109
+ return <DiffViewer filePath={filePath} oldString={obj.old_string} newString={obj.new_string} />
110
+ }
111
+
112
+ if (toolName === 'Write' && typeof obj.content === 'string') {
113
+ return <DiffViewer filePath={filePath} oldString="" newString={obj.content} />
114
+ }
115
+
116
+ if (toolName === 'Bash' && typeof obj.command === 'string') {
117
+ return (
118
+ <TerminalChrome title={typeof obj.description === 'string' ? obj.description : filePath}>
119
+ <div className="px-3 py-2.5 font-[var(--font-mono)] text-[11px] leading-[1.3] text-[var(--color-terminal-fg)]">
120
+ <span className="text-[var(--color-terminal-accent)]">$</span> {obj.command}
121
+ </div>
122
+ </TerminalChrome>
123
+ )
124
+ }
125
+
126
+ if (toolName === 'Read') {
127
+ return null
128
+ }
129
+
130
+ if (result) {
131
+ const text = extractTextContent(result.content)
132
+ if (text) {
133
+ return (
134
+ <>
135
+ <InlineImageGallery text={text} />
136
+ <div className={`overflow-hidden rounded-lg border ${
137
+ result.isError
138
+ ? 'border-[var(--color-error)]/20 bg-[var(--color-error-container)]/60'
139
+ : 'border-[var(--color-border)] bg-[var(--color-surface)]'
140
+ }`}>
141
+ <div className="flex items-center justify-between border-b border-[var(--color-border)]/60 px-3 py-2 text-[10px] uppercase tracking-[0.18em] text-[var(--color-outline)]">
142
+ <span>{result.isError ? t?.('tool.errorOutput') ?? 'Error Output' : t?.('tool.toolOutput') ?? 'Tool Output'}</span>
143
+ <CopyButton
144
+ text={text}
145
+ className="rounded-md border border-[var(--color-border)] px-2 py-1 text-[10px] normal-case tracking-normal text-[var(--color-text-tertiary)] transition-colors hover:text-[var(--color-text-primary)]"
146
+ />
147
+ </div>
148
+ <CodeViewer code={text} language="plaintext" maxLines={18} />
149
+ </div>
150
+ </>
151
+ )
152
+ }
153
+ }
154
+
155
+ return null
156
+ }
157
+
158
+ function renderDetails(toolName: string, obj: Record<string, unknown>, t?: (key: TranslationKey, params?: Record<string, string | number>) => string) {
159
+ if (toolName === 'Edit' || toolName === 'Write') {
160
+ return null
161
+ }
162
+
163
+ const text = JSON.stringify(obj, null, 2)
164
+ return (
165
+ <div className="overflow-hidden rounded-lg border border-[var(--color-border)] bg-[var(--color-surface)]">
166
+ <div className="flex items-center justify-between border-b border-[var(--color-border)] px-3 py-2 text-[10px] uppercase tracking-[0.18em] text-[var(--color-outline)]">
167
+ <span>{t?.('tool.toolInput') ?? 'Tool Input'}</span>
168
+ <CopyButton
169
+ text={text}
170
+ className="rounded-md border border-[var(--color-border)] px-2 py-1 text-[10px] normal-case tracking-normal text-[var(--color-text-tertiary)] transition-colors hover:text-[var(--color-text-primary)]"
171
+ />
172
+ </div>
173
+ <CodeViewer code={text} language="json" maxLines={18} />
174
+ </div>
175
+ )
176
+ }
177
+
178
+ function getToolResultSummary(toolName: string, content: unknown, t?: (key: TranslationKey, params?: Record<string, string | number>) => string): string {
179
+ if (toolName === 'Bash') return ''
180
+
181
+ const text = extractTextContent(content)
182
+ if (!text) return ''
183
+
184
+ const lineCount = text.split('\n').length
185
+ if (lineCount > 1) {
186
+ return t?.('tool.linesOutput', { count: lineCount }) ?? `${lineCount} lines output`
187
+ }
188
+
189
+ const compact = text.replace(/\s+/g, ' ').trim()
190
+ if (!compact) return ''
191
+ if (compact.length <= 36) return compact
192
+ return `${compact.slice(0, 36)}…`
193
+ }
194
+
195
+ function getToolSummary(toolName: string, obj: Record<string, unknown>, t?: (key: TranslationKey, params?: Record<string, string | number>) => string): string {
196
+ switch (toolName) {
197
+ case 'Bash':
198
+ return typeof obj.command === 'string' ? obj.command : ''
199
+ case 'Read':
200
+ return t?.('tool.readFileContents') ?? 'Read file contents'
201
+ case 'Write':
202
+ return typeof obj.content === 'string'
203
+ ? (t?.('tool.linesCreated', { count: obj.content.split('\n').length }) ?? `${obj.content.split('\n').length} lines created`)
204
+ : (t?.('tool.createFile') ?? 'Create file')
205
+ case 'Edit':
206
+ return typeof obj.old_string === 'string' && typeof obj.new_string === 'string'
207
+ ? changedLineSummary(obj.old_string, obj.new_string, t)
208
+ : (t?.('tool.updateFileContents') ?? 'Update file contents')
209
+ case 'Glob':
210
+ return typeof obj.pattern === 'string' ? obj.pattern : ''
211
+ case 'Grep':
212
+ return typeof obj.pattern === 'string' ? obj.pattern : ''
213
+ case 'Agent':
214
+ return typeof obj.description === 'string' ? obj.description : ''
215
+ default:
216
+ return ''
217
+ }
218
+ }
219
+
220
+ function extractTextContent(content: unknown): string | null {
221
+ if (typeof content === 'string') return content
222
+ if (Array.isArray(content)) {
223
+ return content
224
+ .map((chunk: any) => (typeof chunk === 'string' ? chunk : chunk?.text || ''))
225
+ .filter(Boolean)
226
+ .join('\n')
227
+ }
228
+ if (content && typeof content === 'object') {
229
+ return JSON.stringify(content, null, 2)
230
+ }
231
+ return null
232
+ }
233
+
234
+ function changedLineSummary(oldString: string, newString: string, t?: (key: TranslationKey, params?: Record<string, string | number>) => string): string {
235
+ const oldLines = oldString.split('\n')
236
+ const newLines = newString.split('\n')
237
+ let changed = 0
238
+ const max = Math.max(oldLines.length, newLines.length)
239
+
240
+ for (let index = 0; index < max; index += 1) {
241
+ if ((oldLines[index] ?? '') !== (newLines[index] ?? '')) {
242
+ changed += 1
243
+ }
244
+ }
245
+
246
+ return t?.('tool.linesChanged', { count: changed }) ?? `${changed} lines changed`
247
+ }