zames_pro 1.0.3 → 1.1.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.
- package/README.md +1 -1
- package/package.json +14 -3
- package/src/agent-loop.js +5 -1
- package/src/browser.js +150 -13
- package/src/confirm.js +10 -10
- package/src/diff.js +21 -113
- package/src/index.js +159 -107
- package/src/markdown.js +49 -0
- package/src/self-review.js +30 -30
- package/src/spinner.js +15 -22
- package/src/theme.js +34 -0
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# zames_pro
|
|
2
2
|
|
|
3
|
-

|
|
3
|
+

|
|
4
4
|
|
|
5
5
|
Terminal coding agent, работающий поверх chat.deepseek.com через Playwright.
|
|
6
6
|
По духу похож на Claude Code / Codex CLI: запускается в текущей директории, читает и правит файлы, запускает команды, коммитит в git.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zames_pro",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Terminal coding agent over chat.deepseek.com via Playwright",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -28,11 +28,22 @@
|
|
|
28
28
|
"node": ">=18"
|
|
29
29
|
},
|
|
30
30
|
"dependencies": {
|
|
31
|
-
"playwright": "^1.48.0",
|
|
32
31
|
"chalk": "^5.3.0",
|
|
33
|
-
"
|
|
32
|
+
"cli-highlight": "^2.1.11",
|
|
33
|
+
"markdansi": "^0.3.4",
|
|
34
|
+
"marked": "^15.0.12",
|
|
35
|
+
"ora": "^8.0.1",
|
|
36
|
+
"playwright": "^1.48.0"
|
|
34
37
|
},
|
|
35
38
|
"publishConfig": {
|
|
36
39
|
"access": "public"
|
|
40
|
+
},
|
|
41
|
+
"repository": {
|
|
42
|
+
"type": "git",
|
|
43
|
+
"url": "git+https://github.com/Viqto0r/zames_pro.git"
|
|
44
|
+
},
|
|
45
|
+
"homepage": "https://github.com/Viqto0r/zames_pro#readme",
|
|
46
|
+
"bugs": {
|
|
47
|
+
"url": "https://github.com/Viqto0r/zames_pro/issues"
|
|
37
48
|
}
|
|
38
49
|
}
|
package/src/agent-loop.js
CHANGED
|
@@ -15,6 +15,7 @@ export async function runAgentLoop({
|
|
|
15
15
|
onToolCall = () => {},
|
|
16
16
|
onToolResult = () => {},
|
|
17
17
|
onAssistantMessage = () => {},
|
|
18
|
+
debugLog = false,
|
|
18
19
|
}) {
|
|
19
20
|
if (freshChat) {
|
|
20
21
|
await browser.newChat()
|
|
@@ -60,8 +61,11 @@ export async function runAgentLoop({
|
|
|
60
61
|
|
|
61
62
|
const parsedCalls = Array.isArray(parsed) ? parsed : parsed ? [parsed] : []
|
|
62
63
|
if (parsedCalls.some((p) => p && p._permissive)) {
|
|
64
|
+
// Пишем в транскрипт (для отладки), но не сыпем в консоль.
|
|
63
65
|
transcript?.log('permissive_parse', { response: rawResponse })
|
|
64
|
-
|
|
66
|
+
if (debugLog) {
|
|
67
|
+
console.error('внимание: tool-call распознан нестрогим парсером')
|
|
68
|
+
}
|
|
65
69
|
}
|
|
66
70
|
|
|
67
71
|
if (!parsed) {
|
package/src/browser.js
CHANGED
|
@@ -16,7 +16,6 @@ const INPUT_SELECTORS = [
|
|
|
16
16
|
const ANSWER_SELECTORS = [
|
|
17
17
|
'div[class*="ds-markdown"]',
|
|
18
18
|
'div[class*="markdown"]',
|
|
19
|
-
'[class*="message"]',
|
|
20
19
|
]
|
|
21
20
|
|
|
22
21
|
const STOP_SELECTORS = [
|
|
@@ -25,6 +24,11 @@ const STOP_SELECTORS = [
|
|
|
25
24
|
'button[aria-label*="Stop" i]',
|
|
26
25
|
]
|
|
27
26
|
|
|
27
|
+
// Служебные статусы интерфейса DeepSeek, которые НЕ являются ответом модели.
|
|
28
|
+
// Иначе агент принимает статус (Reading...) за ответ и ломает разбор.
|
|
29
|
+
const STATUS_RE = /^(reading|thinking|searching|analyzing|generating|stop|остановить|читаю|думаю|поиск|анализ)[\s.…]*$/i
|
|
30
|
+
|
|
31
|
+
|
|
28
32
|
// ---------- profile cleanup ----------
|
|
29
33
|
|
|
30
34
|
async function killStaleChrome() {
|
|
@@ -237,15 +241,91 @@ export class DeepSeekBrowser {
|
|
|
237
241
|
if (c.parentNode) c.parentNode.replaceChild(replacement, c)
|
|
238
242
|
})
|
|
239
243
|
|
|
240
|
-
|
|
244
|
+
// textContent склеивает блоки без переводов строк, из-за чего
|
|
245
|
+
// Markdown-рендер получает одну длинную строку. Обходим DOM сами и
|
|
246
|
+
// расставляем переводы строк / маркеры Markdown по блочным элементам.
|
|
247
|
+
const NL = String.fromCharCode(10)
|
|
248
|
+
const BULLET = String.fromCharCode(45) + ' ' // '- '
|
|
249
|
+
|
|
250
|
+
function domToMarkdown(node) {
|
|
251
|
+
if (node.nodeType === 3) return node.textContent || ''
|
|
252
|
+
if (node.nodeType !== 1) return ''
|
|
253
|
+
const tag = node.tagName.toLowerCase()
|
|
254
|
+
|
|
255
|
+
if (tag === 'br') return NL
|
|
256
|
+
|
|
257
|
+
const inner = Array.from(node.childNodes)
|
|
258
|
+
.map(domToMarkdown)
|
|
259
|
+
.join('')
|
|
260
|
+
|
|
261
|
+
const STAR = String.fromCharCode(42) // '*'
|
|
262
|
+
if (tag === 'strong' || tag === 'b') return STAR + STAR + inner + STAR + STAR
|
|
263
|
+
if (tag === 'em' || tag === 'i') return STAR + inner + STAR
|
|
264
|
+
if (tag === 'del' || tag === 's') return '~~' + inner + '~~'
|
|
265
|
+
if (tag === 'a') {
|
|
266
|
+
const href = node.getAttribute('href') || ''
|
|
267
|
+
return href ? '[' + inner + '](' + href + ')' : inner
|
|
268
|
+
}
|
|
269
|
+
if (/^h[1-6]$/.test(tag)) {
|
|
270
|
+
const level = Number(tag[1])
|
|
271
|
+
const hashes = '#'.repeat(level)
|
|
272
|
+
return NL + NL + hashes + ' ' + inner.trim() + NL + NL
|
|
273
|
+
}
|
|
274
|
+
if (tag === 'li') {
|
|
275
|
+
const text = inner.trim().replace(new RegExp(NL + '+', 'g'), ' ')
|
|
276
|
+
return BULLET + text + NL
|
|
277
|
+
}
|
|
278
|
+
if (tag === 'ul' || tag === 'ol' || tag === 'blockquote') {
|
|
279
|
+
return NL + inner + NL
|
|
280
|
+
}
|
|
281
|
+
if (
|
|
282
|
+
tag === 'p' ||
|
|
283
|
+
tag === 'div' ||
|
|
284
|
+
tag === 'section' ||
|
|
285
|
+
tag === 'article' ||
|
|
286
|
+
tag === 'tr' ||
|
|
287
|
+
tag === 'table'
|
|
288
|
+
) {
|
|
289
|
+
const text = inner.trim()
|
|
290
|
+
return text ? NL + NL + text : ''
|
|
291
|
+
}
|
|
292
|
+
return inner
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const out = domToMarkdown(clone)
|
|
296
|
+
// Схлопываем тройные+ переводы строк до двойных (разделитель блоков).
|
|
297
|
+
return out.replace(new RegExp(NL + '{3,}', 'g'), NL + NL).trim()
|
|
241
298
|
}, ANSWER_SELECTORS)
|
|
242
299
|
}
|
|
243
300
|
|
|
301
|
+
async _readLastAnswerTextClean() {
|
|
302
|
+
const raw = await this._readLastAnswerText().catch(() => '')
|
|
303
|
+
const t = (raw || '').trim()
|
|
304
|
+
if (!t) return ''
|
|
305
|
+
// Отсекаем служебные статусы интерфейса (Reading..., Думаю...).
|
|
306
|
+
if (STATUS_RE.test(t)) return ''
|
|
307
|
+
return raw
|
|
308
|
+
}
|
|
309
|
+
|
|
244
310
|
async _isGenerating() {
|
|
245
311
|
const stop = await this._findVisible(STOP_SELECTORS, 300)
|
|
246
312
|
return !!stop
|
|
247
313
|
}
|
|
248
314
|
|
|
315
|
+
// Прервать текущую генерацию: нажать Stop в интерфейсе.
|
|
316
|
+
// Используется при нажатии Esc пользователем.
|
|
317
|
+
async stopGeneration() {
|
|
318
|
+
this._abort = true
|
|
319
|
+
const btn = await this._findVisible(STOP_SELECTORS, 500)
|
|
320
|
+
if (btn) {
|
|
321
|
+
try {
|
|
322
|
+
await btn.click({ timeout: 1000 })
|
|
323
|
+
return true
|
|
324
|
+
} catch {}
|
|
325
|
+
}
|
|
326
|
+
return false
|
|
327
|
+
}
|
|
328
|
+
|
|
249
329
|
async ask(prompt, { timeout = this.answerTimeoutMs } = {}) {
|
|
250
330
|
let lastErr = null
|
|
251
331
|
|
|
@@ -279,6 +359,65 @@ export class DeepSeekBrowser {
|
|
|
279
359
|
)
|
|
280
360
|
}
|
|
281
361
|
|
|
362
|
+
// Вставка текста в поле ввода.
|
|
363
|
+
//
|
|
364
|
+
// fill()/insertText() ломаются на многострочном тексте в contenteditable-
|
|
365
|
+
// редакторах (ProseMirror/Lexical/...): символ перевода строки там
|
|
366
|
+
// трактуется как Enter, и в поле остаётся только первая строка. Поэтому
|
|
367
|
+
// для contenteditable и [role=textbox] эмулируем paste-событие с полным
|
|
368
|
+
// текстом — то же, что делает Shift+Insert. Для нативных textarea/input
|
|
369
|
+
// перевод строки работает и так.
|
|
370
|
+
async _setInputText(input, text) {
|
|
371
|
+
const tag = await input.evaluate((el) => el.tagName.toLowerCase())
|
|
372
|
+
const isNative = tag === 'textarea' || tag === 'input'
|
|
373
|
+
|
|
374
|
+
if (isNative) {
|
|
375
|
+
try {
|
|
376
|
+
await input.fill(text, { timeout: 5000 })
|
|
377
|
+
return
|
|
378
|
+
} catch {}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
await input.click()
|
|
382
|
+
// Выделяем всё содержимое, чтобы вставка заменила его целиком.
|
|
383
|
+
await this.page.keyboard.press('Control+A')
|
|
384
|
+
await this.page.keyboard.press('Delete')
|
|
385
|
+
|
|
386
|
+
const ok = await input.evaluate((el, value) => {
|
|
387
|
+
el.focus()
|
|
388
|
+
const dt = new DataTransfer()
|
|
389
|
+
dt.setData('text/plain', value)
|
|
390
|
+
const ev = new ClipboardEvent('paste', {
|
|
391
|
+
clipboardData: dt,
|
|
392
|
+
bubbles: true,
|
|
393
|
+
cancelable: true,
|
|
394
|
+
})
|
|
395
|
+
el.dispatchEvent(ev)
|
|
396
|
+
// Если обработчик не отменил вставку и поле осталось пустым —
|
|
397
|
+
// пробуем через insertText вручную (fallback ниже).
|
|
398
|
+
return true
|
|
399
|
+
}, text)
|
|
400
|
+
|
|
401
|
+
if (!ok) {
|
|
402
|
+
await this.page.keyboard.insertText(text)
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// Проверяем, что текст реально попал в поле. Если редактор проигнорировал
|
|
406
|
+
// paste — падаем на insertText.
|
|
407
|
+
const got = await input.evaluate((el) => {
|
|
408
|
+
if (el.tagName.toLowerCase() === 'textarea' || el.tagName.toLowerCase() === 'input') {
|
|
409
|
+
return el.value
|
|
410
|
+
}
|
|
411
|
+
return el.innerText || el.textContent || ''
|
|
412
|
+
})
|
|
413
|
+
const norm = (s) =>
|
|
414
|
+
(s || '').replace(/\r\n/g, '\n').replace(/\u00a0/g, ' ').trim()
|
|
415
|
+
if (!norm(got)) {
|
|
416
|
+
await input.click()
|
|
417
|
+
await this.page.keyboard.insertText(text)
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
282
421
|
async _askOnce(prompt, { timeout }) {
|
|
283
422
|
const input = await this._findVisible(INPUT_SELECTORS, 10_000)
|
|
284
423
|
if (!input) {
|
|
@@ -287,16 +426,9 @@ export class DeepSeekBrowser {
|
|
|
287
426
|
)
|
|
288
427
|
}
|
|
289
428
|
|
|
290
|
-
const beforeText = await this.
|
|
429
|
+
const beforeText = await this._readLastAnswerTextClean().catch(() => '')
|
|
291
430
|
|
|
292
|
-
await
|
|
293
|
-
try {
|
|
294
|
-
await input.fill(prompt, { timeout: 5000 })
|
|
295
|
-
} catch {
|
|
296
|
-
// contenteditable / [role=textbox] не поддерживает fill() — печатаем
|
|
297
|
-
// текст в уже сфокусированное поле, не задевая раскладку.
|
|
298
|
-
await this.page.keyboard.insertText(prompt)
|
|
299
|
-
}
|
|
431
|
+
await this._setInputText(input, prompt)
|
|
300
432
|
await this.page.waitForTimeout(200)
|
|
301
433
|
|
|
302
434
|
const sendBtn = this.page
|
|
@@ -313,7 +445,7 @@ export class DeepSeekBrowser {
|
|
|
313
445
|
let started = false
|
|
314
446
|
while (Date.now() < startDeadline) {
|
|
315
447
|
const gen = await this._isGenerating()
|
|
316
|
-
const cur = await this.
|
|
448
|
+
const cur = await this._readLastAnswerTextClean().catch(() => '')
|
|
317
449
|
if (gen || (cur && cur !== beforeText)) {
|
|
318
450
|
started = true
|
|
319
451
|
break
|
|
@@ -329,9 +461,14 @@ export class DeepSeekBrowser {
|
|
|
329
461
|
const deadline = Date.now() + timeout
|
|
330
462
|
let last = ''
|
|
331
463
|
let stable = 0
|
|
464
|
+
this._abort = false
|
|
332
465
|
while (Date.now() < deadline) {
|
|
466
|
+
if (this._abort) {
|
|
467
|
+
// Пользователь нажал Esc — вернём то, что успело сгенерироваться.
|
|
468
|
+
return last || '(прервано пользователем)'
|
|
469
|
+
}
|
|
333
470
|
const gen = await this._isGenerating()
|
|
334
|
-
const cur = await this.
|
|
471
|
+
const cur = await this._readLastAnswerTextClean().catch(() => '')
|
|
335
472
|
if (cur && cur === last && !gen) {
|
|
336
473
|
stable++
|
|
337
474
|
if (stable >= 2) return cur
|
package/src/confirm.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as readlinePromises from 'readline/promises'
|
|
2
|
-
import
|
|
2
|
+
import { theme } from './theme.js'
|
|
3
3
|
import { unifiedDiff, colorDiff } from './diff.js'
|
|
4
4
|
|
|
5
5
|
export class ConfirmManager {
|
|
@@ -36,14 +36,14 @@ export class ConfirmManager {
|
|
|
36
36
|
|
|
37
37
|
const label = message || `Разрешить ${kind}?`
|
|
38
38
|
const question =
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
39
|
+
theme.warn(`${label} `) +
|
|
40
|
+
theme.system('[') +
|
|
41
|
+
theme.assistant('y') +
|
|
42
|
+
theme.system(' = да, ') +
|
|
43
|
+
theme.error('n') +
|
|
44
|
+
theme.system(' = нет, ') +
|
|
45
|
+
theme.user('a') +
|
|
46
|
+
theme.system(' = всегда для этого типа] ')
|
|
47
47
|
|
|
48
48
|
const answer = await this._prompt(question)
|
|
49
49
|
|
|
@@ -81,5 +81,5 @@ export class ConfirmManager {
|
|
|
81
81
|
|
|
82
82
|
export function formatDiffPreview(oldText, newText, { label } = {}) {
|
|
83
83
|
const diff = unifiedDiff(oldText, newText, { label })
|
|
84
|
-
return colorDiff(diff
|
|
84
|
+
return colorDiff(diff)
|
|
85
85
|
}
|
package/src/diff.js
CHANGED
|
@@ -1,124 +1,32 @@
|
|
|
1
|
-
|
|
2
|
-
oldText,
|
|
3
|
-
newText,
|
|
4
|
-
{ context = 3, label = '' } = {},
|
|
5
|
-
) {
|
|
6
|
-
const a = (oldText ?? '').split('\n')
|
|
7
|
-
const b = (newText ?? '').split('\n')
|
|
1
|
+
import { theme } from './theme.js'
|
|
8
2
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
const
|
|
3
|
+
export function unifiedDiff(a, b, { label = '' } = {}) {
|
|
4
|
+
const la = a.split(String.fromCharCode(10))
|
|
5
|
+
const lb = b.split(String.fromCharCode(10))
|
|
6
|
+
const n = Math.max(la.length, lb.length)
|
|
12
7
|
const out = []
|
|
13
|
-
if (label) out.push(
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
if (
|
|
20
|
-
let j = i
|
|
21
|
-
while (j < ops.length && ops[j].type === 'same') j++
|
|
22
|
-
const blockLen = j - i
|
|
23
|
-
|
|
24
|
-
if (i === 0 && j === ops.length) {
|
|
25
|
-
i = j
|
|
26
|
-
continue
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
if (i === 0) {
|
|
30
|
-
const end = Math.min(j, i + context)
|
|
31
|
-
for (let k = i; k < end; k++) out.push(' ' + ops[k].line)
|
|
32
|
-
if (end < j) out.push('...')
|
|
33
|
-
} else if (j === ops.length) {
|
|
34
|
-
const start = Math.max(i, j - context)
|
|
35
|
-
if (start > i) out.push('...')
|
|
36
|
-
for (let k = start; k < j; k++) out.push(' ' + ops[k].line)
|
|
37
|
-
} else {
|
|
38
|
-
const start = Math.max(i, j - context)
|
|
39
|
-
const end = Math.min(j, i + context)
|
|
40
|
-
if (start > i) out.push('...')
|
|
41
|
-
for (let k = start; k < end; k++) out.push(' ' + ops[k].line)
|
|
42
|
-
if (end < j) out.push('...')
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
i = j
|
|
46
|
-
continue
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
const blockStart = i
|
|
50
|
-
let j = i
|
|
51
|
-
while (j < ops.length && ops[j].type !== 'same') j++
|
|
52
|
-
|
|
53
|
-
const preStart = Math.max(0, blockStart - context)
|
|
54
|
-
if (preStart < blockStart) {
|
|
55
|
-
if (preStart > 0) out.push('...')
|
|
56
|
-
for (let k = preStart; k < blockStart; k++) {
|
|
57
|
-
out.push(' ' + ops[k].line)
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
for (let k = blockStart; k < j; k++) {
|
|
62
|
-
const o = ops[k]
|
|
63
|
-
out.push((o.type === 'del' ? '-' : '+') + o.line)
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
const postEnd = Math.min(ops.length, j + context)
|
|
67
|
-
for (let k = j; k < postEnd; k++) {
|
|
68
|
-
out.push(' ' + ops[k].line)
|
|
69
|
-
}
|
|
70
|
-
if (postEnd < ops.length) out.push('...')
|
|
71
|
-
|
|
72
|
-
i = j
|
|
8
|
+
if (label) out.push('... ' + label)
|
|
9
|
+
for (let i = 0; i < n; i++) {
|
|
10
|
+
const x = la[i]
|
|
11
|
+
const y = lb[i]
|
|
12
|
+
if (x === y) continue
|
|
13
|
+
if (x !== undefined) out.push('-' + x)
|
|
14
|
+
if (y !== undefined) out.push('+' + y)
|
|
73
15
|
}
|
|
74
|
-
|
|
75
|
-
return out.join('\n')
|
|
16
|
+
return out.join(String.fromCharCode(10))
|
|
76
17
|
}
|
|
77
18
|
|
|
78
|
-
export function colorDiff(diffText
|
|
19
|
+
export function colorDiff(diffText) {
|
|
79
20
|
return diffText
|
|
80
|
-
.split(
|
|
21
|
+
.split(String.fromCharCode(10))
|
|
81
22
|
.map((line) => {
|
|
82
|
-
if (line.startsWith('+++') || line.startsWith('---'))
|
|
83
|
-
|
|
84
|
-
if (line.startsWith('
|
|
85
|
-
if (line.startsWith('
|
|
86
|
-
if (line.startsWith('...')) return chalk.gray(line)
|
|
23
|
+
if (line.startsWith('+++') || line.startsWith('---')) return theme.bold(line)
|
|
24
|
+
if (line.startsWith('+')) return theme.assistant(line)
|
|
25
|
+
if (line.startsWith('-')) return theme.error(line)
|
|
26
|
+
if (line.startsWith('...')) return theme.system(line)
|
|
87
27
|
return line
|
|
88
28
|
})
|
|
89
|
-
.join(
|
|
29
|
+
.join(String.fromCharCode(10))
|
|
90
30
|
}
|
|
91
31
|
|
|
92
|
-
|
|
93
|
-
const n = a.length
|
|
94
|
-
const m = b.length
|
|
95
|
-
const dp = Array.from({ length: n + 1 }, () => new Uint32Array(m + 1))
|
|
96
|
-
|
|
97
|
-
for (let i = n - 1; i >= 0; i--) {
|
|
98
|
-
for (let j = m - 1; j >= 0; j--) {
|
|
99
|
-
if (a[i] === b[j]) dp[i][j] = dp[i + 1][j + 1] + 1
|
|
100
|
-
else dp[i][j] = Math.max(dp[i + 1][j], dp[i][j + 1])
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
const ops = []
|
|
105
|
-
let i = 0
|
|
106
|
-
let j = 0
|
|
107
|
-
while (i < n && j < m) {
|
|
108
|
-
if (a[i] === b[j]) {
|
|
109
|
-
ops.push({ type: 'same', line: a[i] })
|
|
110
|
-
i++
|
|
111
|
-
j++
|
|
112
|
-
} else if (dp[i + 1][j] >= dp[i][j + 1]) {
|
|
113
|
-
ops.push({ type: 'del', line: a[i] })
|
|
114
|
-
i++
|
|
115
|
-
} else {
|
|
116
|
-
ops.push({ type: 'add', line: b[j] })
|
|
117
|
-
j++
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
while (i < n) ops.push({ type: 'del', line: a[i++] })
|
|
121
|
-
while (j < m) ops.push({ type: 'add', line: b[j++] })
|
|
122
|
-
|
|
123
|
-
return ops
|
|
124
|
-
}
|
|
32
|
+
export default { unifiedDiff, colorDiff }
|