@semalt-ai/code 1.6.0 → 1.8.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/.claude/settings.local.json +8 -0
- package/ARCHITECTURE.md +99 -0
- package/CLAUDE.md +349 -0
- package/README.md +16 -2
- package/index.js +79 -7
- package/lib/agent.js +508 -39
- package/lib/api.js +347 -77
- package/lib/args.js +34 -0
- package/lib/audit.js +31 -0
- package/lib/commands.js +1018 -183
- package/lib/config.js +68 -5
- package/lib/constants.js +58 -0
- package/lib/context.js +2 -6
- package/lib/metrics.js +94 -0
- package/lib/permissions.js +180 -49
- package/lib/prompts.js +89 -13
- package/lib/storage.js +96 -0
- package/lib/tools.js +896 -35
- package/lib/ui/ansi.js +64 -0
- package/lib/ui/chat-history.js +217 -0
- package/lib/ui/create-ui.js +474 -0
- package/lib/ui/diff.js +243 -0
- package/lib/ui/input-field.js +1176 -0
- package/lib/ui/layout.js +53 -0
- package/lib/ui/legacy.js +130 -0
- package/lib/ui/status-bar.js +130 -0
- package/lib/ui/stream.js +158 -0
- package/lib/ui/utils.js +45 -0
- package/lib/ui.js +42 -598
- package/package.json +1 -1
- package/path +1 -0
package/lib/ui/diff.js
ADDED
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { FG_DARK, FG_RED, FG_GREEN, FG_GRAY, FG_YELLOW, FG_TEAL, RST, THEME } = require('./ansi');
|
|
4
|
+
const { getCols, stripAnsi } = require('./utils');
|
|
5
|
+
|
|
6
|
+
function diffLines(oldLines, newLines) {
|
|
7
|
+
const m = oldLines.length, n = newLines.length;
|
|
8
|
+
const dp = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0));
|
|
9
|
+
for (let i = 1; i <= m; i++)
|
|
10
|
+
for (let j = 1; j <= n; j++)
|
|
11
|
+
dp[i][j] = oldLines[i-1] === newLines[j-1] ? dp[i-1][j-1] + 1 : Math.max(dp[i-1][j], dp[i][j-1]);
|
|
12
|
+
const result = [];
|
|
13
|
+
let i = m, j = n;
|
|
14
|
+
while (i > 0 || j > 0) {
|
|
15
|
+
if (i > 0 && j > 0 && oldLines[i-1] === newLines[j-1]) { result.unshift({ type:'same', text: oldLines[i-1] }); i--; j--; }
|
|
16
|
+
else if (j > 0 && (i === 0 || dp[i][j-1] >= dp[i-1][j])) { result.unshift({ type:'add', text: newLines[j-1] }); j--; }
|
|
17
|
+
else { result.unshift({ type:'del', text: oldLines[i-1] }); i--; }
|
|
18
|
+
}
|
|
19
|
+
return result;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function diffLinesHashed(oldLines, newLines) {
|
|
23
|
+
const hashLine = (s) => { let h = 5381; for (let k = 0; k < s.length; k++) h = ((h << 5) + h + s.charCodeAt(k)) | 0; return h; };
|
|
24
|
+
const oldH = oldLines.map(hashLine), newH = newLines.map(hashLine);
|
|
25
|
+
const m = oldH.length, n = newH.length;
|
|
26
|
+
const dp = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0));
|
|
27
|
+
for (let i = 1; i <= m; i++)
|
|
28
|
+
for (let j = 1; j <= n; j++)
|
|
29
|
+
dp[i][j] = oldH[i-1] === newH[j-1] ? dp[i-1][j-1] + 1 : Math.max(dp[i-1][j], dp[i][j-1]);
|
|
30
|
+
const result = [];
|
|
31
|
+
let i = m, j = n;
|
|
32
|
+
while (i > 0 || j > 0) {
|
|
33
|
+
if (i > 0 && j > 0 && oldH[i-1] === newH[j-1]) { result.unshift({ type:'same', text: oldLines[i-1] }); i--; j--; }
|
|
34
|
+
else if (j > 0 && (i === 0 || dp[i][j-1] >= dp[i-1][j])) { result.unshift({ type:'add', text: newLines[j-1] }); j--; }
|
|
35
|
+
else { result.unshift({ type:'del', text: oldLines[i-1] }); i--; }
|
|
36
|
+
}
|
|
37
|
+
return result;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function renderDiff(oldText, newText, filePath) {
|
|
41
|
+
if (oldText.includes('\x00') || newText.includes('\x00')) return '[binary file — diff skipped]';
|
|
42
|
+
const isTTY = process.stdout.isTTY === true;
|
|
43
|
+
const cols = getCols();
|
|
44
|
+
|
|
45
|
+
// ── Layout constants ───────────────────────────────────────────────────────
|
|
46
|
+
const INDENT = ' '; // left offset from main stream
|
|
47
|
+
const boxWidth = Math.max(40, cols - INDENT.length);
|
|
48
|
+
const innerWidth = boxWidth - 2; // between ╭ and ╮
|
|
49
|
+
|
|
50
|
+
const a = (code) => isTTY ? code : '';
|
|
51
|
+
const R = a(RST);
|
|
52
|
+
|
|
53
|
+
// Palette
|
|
54
|
+
const BORDER = a(FG_DARK);
|
|
55
|
+
const DEL_BG = a('\x1b[48;5;52m'); // dark red background
|
|
56
|
+
const ADD_BG = a('\x1b[48;5;22m'); // dark green background
|
|
57
|
+
const DEL_FG = a('\x1b[38;5;203m'); // bright red text (= FG_RED)
|
|
58
|
+
const ADD_FG = a('\x1b[38;5;114m'); // bright green text (= FG_GREEN)
|
|
59
|
+
const NUM_CLR = a('\x1b[38;5;240m'); // muted line-number gutter
|
|
60
|
+
const CTX_CLR = a('\x1b[38;5;245m'); // dim context text
|
|
61
|
+
const HUNK_CLR = a('\x1b[38;5;73m'); // teal hunk separator
|
|
62
|
+
const PATH_CLR = a('\x1b[38;5;222m'); // yellow filepath in header
|
|
63
|
+
|
|
64
|
+
const oldLines = oldText === '' ? [] : oldText.split('\n');
|
|
65
|
+
const newLines = newText.split('\n');
|
|
66
|
+
const isNewFile = oldText === '';
|
|
67
|
+
|
|
68
|
+
let diff;
|
|
69
|
+
if (!isNewFile) {
|
|
70
|
+
diff = (oldLines.length > 200 || newLines.length > 200)
|
|
71
|
+
? diffLinesHashed(oldLines, newLines) : diffLines(oldLines, newLines);
|
|
72
|
+
if (!diff.some((d) => d.type !== 'same')) return `${INDENT} No changes detected`;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Gutter width: enough digits for the largest line number
|
|
76
|
+
const maxLn = Math.max(oldLines.length, newLines.length, 1);
|
|
77
|
+
const LN_W = Math.max(2, String(maxLn).length);
|
|
78
|
+
|
|
79
|
+
// gutter template: ' NNNN │S ' — LN_W + 5 chars
|
|
80
|
+
const GUTTER_W = LN_W + 5;
|
|
81
|
+
const CONT_W = Math.max(1, innerWidth - GUTTER_W);
|
|
82
|
+
|
|
83
|
+
// ── Builders ───────────────────────────────────────────────────────────────
|
|
84
|
+
|
|
85
|
+
// One content line inside the box.
|
|
86
|
+
// lnStr – line-number string (right-aligned to LN_W, or '' for unknown)
|
|
87
|
+
// symbol – '-', '+', or ' '
|
|
88
|
+
// text – raw source text
|
|
89
|
+
// bg – background ANSI string (or '' for context)
|
|
90
|
+
// fg – foreground ANSI string
|
|
91
|
+
function makeLine(lnStr, symbol, text, bg, fg) {
|
|
92
|
+
const num = lnStr ? lnStr.padStart(LN_W) : ' '.repeat(LN_W);
|
|
93
|
+
const disp = text.replace(/\t/g, ' ');
|
|
94
|
+
const clp = disp.length > CONT_W ? disp.slice(0, CONT_W - 1) + '…' : disp;
|
|
95
|
+
const padded = clp.padEnd(CONT_W);
|
|
96
|
+
// gutter: ' NUM │S '
|
|
97
|
+
const gt = ` ${NUM_CLR}${num}${R}${bg}${BORDER} │${R}${bg}${fg}${symbol} ${padded}${R}`;
|
|
98
|
+
return `${INDENT}${BORDER}│${R}${bg}${gt}${R}${BORDER}│${R}`;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Hunk separator: ···· @@ -x,y +a,b @@ ····
|
|
102
|
+
function hunkSep(label) {
|
|
103
|
+
const body = ` ${label} `;
|
|
104
|
+
const total = innerWidth - 2; // 2 for leading/trailing ·
|
|
105
|
+
const half = Math.max(0, Math.floor((total - body.length) / 2));
|
|
106
|
+
const right = Math.max(0, total - body.length - half);
|
|
107
|
+
return `${INDENT}${BORDER}│${R}${HUNK_CLR} ${'·'.repeat(half)}${body}${'·'.repeat(right)} ${R}${BORDER}│${R}`;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const out = [];
|
|
111
|
+
|
|
112
|
+
// ── Header ─────────────────────────────────────────────────────────────────
|
|
113
|
+
const pathPart = ` ${filePath} `;
|
|
114
|
+
const fillRight = Math.max(1, innerWidth - 1 - pathPart.length); // 1 for leading ─
|
|
115
|
+
out.push(`${INDENT}${BORDER}╭─${PATH_CLR}${pathPart}${R}${BORDER}${'─'.repeat(fillRight)}╮${R}`);
|
|
116
|
+
|
|
117
|
+
// ── Body ───────────────────────────────────────────────────────────────────
|
|
118
|
+
if (isNewFile) {
|
|
119
|
+
out.push(hunkSep('new file'));
|
|
120
|
+
let ln = 1;
|
|
121
|
+
for (const line of newLines) {
|
|
122
|
+
out.push(makeLine(String(ln++), '+', line, ADD_BG, ADD_FG));
|
|
123
|
+
}
|
|
124
|
+
} else {
|
|
125
|
+
let oldLn = 1, newLn = 1;
|
|
126
|
+
const annotated = diff.map((d) => {
|
|
127
|
+
const e = { type: d.type, text: d.text, oldLine: null, newLine: null };
|
|
128
|
+
if (d.type !== 'add') e.oldLine = oldLn++;
|
|
129
|
+
if (d.type !== 'del') e.newLine = newLn++;
|
|
130
|
+
return e;
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
const changedIdx = [];
|
|
134
|
+
annotated.forEach((d, i) => { if (d.type !== 'same') changedIdx.push(i); });
|
|
135
|
+
|
|
136
|
+
const CONTEXT = 3;
|
|
137
|
+
const ranges = [];
|
|
138
|
+
let rS = Math.max(0, changedIdx[0] - CONTEXT);
|
|
139
|
+
let rE = Math.min(annotated.length - 1, changedIdx[0] + CONTEXT);
|
|
140
|
+
for (let k = 1; k < changedIdx.length; k++) {
|
|
141
|
+
const ci = changedIdx[k];
|
|
142
|
+
if (ci - CONTEXT <= rE + 1) { rE = Math.min(annotated.length - 1, ci + CONTEXT); }
|
|
143
|
+
else { ranges.push([rS, rE]); rS = Math.max(0, ci - CONTEXT); rE = Math.min(annotated.length - 1, ci + CONTEXT); }
|
|
144
|
+
}
|
|
145
|
+
ranges.push([rS, rE]);
|
|
146
|
+
|
|
147
|
+
for (const [hS, hE] of ranges) {
|
|
148
|
+
const hunk = annotated.slice(hS, hE + 1);
|
|
149
|
+
const oldStart = hunk.find((e) => e.oldLine !== null)?.oldLine ?? 0;
|
|
150
|
+
const newStart = hunk.find((e) => e.newLine !== null)?.newLine ?? 0;
|
|
151
|
+
const oldCnt = hunk.filter((e) => e.oldLine !== null).length;
|
|
152
|
+
const newCnt = hunk.filter((e) => e.newLine !== null).length;
|
|
153
|
+
out.push(hunkSep(`@@ -${oldStart},${oldCnt} +${newStart},${newCnt} @@`));
|
|
154
|
+
|
|
155
|
+
for (const e of hunk) {
|
|
156
|
+
if (e.type === 'del') {
|
|
157
|
+
out.push(makeLine(String(e.oldLine), '-', e.text, DEL_BG, DEL_FG));
|
|
158
|
+
} else if (e.type === 'add') {
|
|
159
|
+
out.push(makeLine(String(e.newLine), '+', e.text, ADD_BG, ADD_FG));
|
|
160
|
+
} else {
|
|
161
|
+
out.push(makeLine(String(e.newLine), ' ', e.text, '', CTX_CLR));
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// ── Footer ─────────────────────────────────────────────────────────────────
|
|
168
|
+
out.push(`${INDENT}${BORDER}╰${'─'.repeat(innerWidth)}╯${R}`);
|
|
169
|
+
|
|
170
|
+
const MAX = 120;
|
|
171
|
+
if (out.length > MAX) {
|
|
172
|
+
const extra = out.length - MAX;
|
|
173
|
+
const result = out.slice(0, MAX);
|
|
174
|
+
result.push(isTTY
|
|
175
|
+
? `${INDENT}${FG_DARK} … ${extra} more lines${RST}`
|
|
176
|
+
: `… ${extra} more lines`);
|
|
177
|
+
return result.join('\n');
|
|
178
|
+
}
|
|
179
|
+
return out.join('\n');
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function _mdInline(text) {
|
|
183
|
+
let out = '', i = 0;
|
|
184
|
+
while (i < text.length) {
|
|
185
|
+
const c = text[i], c1 = i + 1 < text.length ? text[i+1] : '';
|
|
186
|
+
if (c === '`') { const end = text.indexOf('`', i+1); if (end !== -1) { out += '\x1b[7m' + text.slice(i+1, end) + '\x1b[27m'; i = end+1; continue; } }
|
|
187
|
+
if (c === '*' && c1 === '*') { const end = text.indexOf('**', i+2); if (end !== -1) { out += '\x1b[1m' + text.slice(i+2, end) + '\x1b[22m'; i = end+2; continue; } }
|
|
188
|
+
if (c === '_' && c1 === '_') { const end = text.indexOf('__', i+2); if (end !== -1) { out += '\x1b[1m' + text.slice(i+2, end) + '\x1b[22m'; i = end+2; continue; } }
|
|
189
|
+
if (c === '*' && c1 !== '*') { let end = -1; for (let j = i+1; j < text.length; j++) { if (text[j] === '*' && (j+1 >= text.length || text[j+1] !== '*')) { end = j; break; } } if (end !== -1) { out += '\x1b[3m' + text.slice(i+1, end) + '\x1b[23m'; i = end+1; continue; } }
|
|
190
|
+
if (c === '_' && c1 !== '_') { let end = -1; for (let j = i+1; j < text.length; j++) { if (text[j] === '_' && (j+1 >= text.length || text[j+1] !== '_')) { end = j; break; } } if (end !== -1) { out += '\x1b[3m' + text.slice(i+1, end) + '\x1b[23m'; i = end+1; continue; } }
|
|
191
|
+
out += text[i++];
|
|
192
|
+
}
|
|
193
|
+
return out;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function renderMarkdown(text) {
|
|
197
|
+
if (!process.stdout.isTTY) { process.stdout.write(text); return; }
|
|
198
|
+
const { loadConfig } = require('../config');
|
|
199
|
+
const maxLines = (loadConfig().max_output_lines) || 50;
|
|
200
|
+
const cols = getCols();
|
|
201
|
+
const srcLines = text.split('\n');
|
|
202
|
+
const output = [];
|
|
203
|
+
let inCode = false;
|
|
204
|
+
for (let li = 0; li < srcLines.length; li++) {
|
|
205
|
+
const line = srcLines[li];
|
|
206
|
+
if (!inCode && line.length >= 3 && line[0] === '`' && line[1] === '`' && line[2] === '`') {
|
|
207
|
+
const lang = line.slice(3).trim();
|
|
208
|
+
const label = lang ? (' ' + lang + ' ') : '';
|
|
209
|
+
output.push('┌─── ' + label + '─'.repeat(Math.max(1, cols - 5 - label.length)));
|
|
210
|
+
inCode = true; continue;
|
|
211
|
+
}
|
|
212
|
+
if (inCode) {
|
|
213
|
+
const t = line.trim();
|
|
214
|
+
if (t.length === 3 && t[0] === '`' && t[1] === '`' && t[2] === '`') { output.push('└' + '─'.repeat(Math.max(1, cols - 2))); inCode = false; }
|
|
215
|
+
else { output.push('│ ' + THEME.dim + line + THEME.reset); }
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
const trimmed = line.trim();
|
|
219
|
+
if (trimmed === '---' || trimmed === '===') { output.push(THEME.dim + '─'.repeat(cols) + THEME.reset); continue; }
|
|
220
|
+
let level = 0;
|
|
221
|
+
while (level < line.length && line[level] === '#') level++;
|
|
222
|
+
if (level >= 1 && level <= 3 && level < line.length && line[level] === ' ') {
|
|
223
|
+
const htext = line.slice(level + 1);
|
|
224
|
+
if (level === 1) { output.push(THEME.agent + '\x1b[1m' + htext + THEME.reset); output.push(THEME.dim + '═'.repeat(cols) + THEME.reset); }
|
|
225
|
+
else if (level === 2) { output.push('\x1b[1m' + htext + '\x1b[0m'); output.push(THEME.dim + '─'.repeat(cols) + THEME.reset); }
|
|
226
|
+
else { output.push('\x1b[1m' + htext + '\x1b[0m'); }
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
if (line.length > 0 && line[0] === '>') { const inner = line.length > 1 && line[1] === ' ' ? line.slice(2) : line.slice(1); output.push(THEME.tool + '│' + THEME.reset + ' ' + _mdInline(inner)); continue; }
|
|
230
|
+
let indent = 0; while (indent < line.length && line[indent] === ' ') indent++;
|
|
231
|
+
const rest = line.slice(indent);
|
|
232
|
+
if (rest.length > 2 && (rest[0] === '-' || rest[0] === '*') && rest[1] === ' ') { output.push(' '.repeat(Math.floor(indent/2)) + THEME.agent + '❯' + THEME.reset + ' ' + _mdInline(rest.slice(2))); continue; }
|
|
233
|
+
let ni = 0; while (ni < line.length && line[ni] >= '0' && line[ni] <= '9') ni++;
|
|
234
|
+
if (ni > 0 && ni < line.length - 1 && line[ni] === '.' && line[ni+1] === ' ') { output.push(THEME.dim + line.slice(0, ni) + '.' + THEME.reset + ' ' + _mdInline(line.slice(ni+2))); continue; }
|
|
235
|
+
output.push(_mdInline(line));
|
|
236
|
+
}
|
|
237
|
+
let overflow = 0, printLines = output;
|
|
238
|
+
if (output.length > maxLines) { overflow = output.length - maxLines; printLines = output.slice(0, maxLines); }
|
|
239
|
+
for (const l of printLines) process.stdout.write(l + '\n');
|
|
240
|
+
if (overflow > 0) process.stdout.write(THEME.dim + '[... ' + overflow + ' more lines]' + THEME.reset + '\n');
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
module.exports = { renderDiff, renderMarkdown, _mdInline };
|