codeep 2.14.0 → 2.16.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 +47 -27
- package/dist/acp/commands.js +22 -1
- package/dist/acp/server.js +13 -2
- package/dist/acp/session.js +22 -1
- package/dist/config/index.d.ts +10 -0
- package/dist/config/index.js +2 -2
- package/dist/config/providers.js +35 -24
- package/dist/renderer/App.d.ts +77 -30
- package/dist/renderer/App.js +429 -659
- package/dist/renderer/agentExecution.d.ts +1 -0
- package/dist/renderer/agentExecution.js +3 -2
- package/dist/renderer/commands/helpers.d.ts +251 -0
- package/dist/renderer/commands/helpers.js +450 -0
- package/dist/renderer/commands/registry.js +7 -1
- package/dist/renderer/commands.d.ts +4 -0
- package/dist/renderer/commands.js +363 -318
- package/dist/renderer/components/ActionFormatting.d.ts +17 -0
- package/dist/renderer/components/ActionFormatting.js +67 -0
- package/dist/renderer/components/Autocomplete.d.ts +58 -0
- package/dist/renderer/components/Autocomplete.js +75 -0
- package/dist/renderer/components/Intro.d.ts +9 -0
- package/dist/renderer/components/Intro.js +5 -15
- package/dist/renderer/components/MessageFormatter.d.ts +96 -0
- package/dist/renderer/components/MessageFormatter.js +375 -0
- package/dist/renderer/components/Permission.d.ts +4 -0
- package/dist/renderer/components/Permission.js +1 -1
- package/dist/renderer/components/Status.d.ts +4 -0
- package/dist/renderer/components/Status.js +2 -3
- package/dist/renderer/components/WelcomeFormatter.d.ts +19 -0
- package/dist/renderer/components/WelcomeFormatter.js +79 -0
- package/dist/renderer/components/uiConstants.d.ts +8 -0
- package/dist/renderer/components/uiConstants.js +24 -0
- package/dist/renderer/inputParsing.d.ts +22 -0
- package/dist/renderer/inputParsing.js +28 -0
- package/dist/renderer/layout.d.ts +219 -0
- package/dist/renderer/layout.js +338 -0
- package/dist/renderer/main.d.ts +2 -1
- package/dist/renderer/main.js +79 -11
- package/dist/renderer/ollamaHint.d.ts +12 -0
- package/dist/renderer/ollamaHint.js +29 -0
- package/dist/utils/agentChat.js +23 -1
- package/dist/utils/codeepCloud.d.ts +54 -0
- package/dist/utils/codeepCloud.js +95 -0
- package/dist/utils/diffPreview.d.ts +31 -0
- package/dist/utils/diffPreview.js +102 -0
- package/dist/utils/export.d.ts +12 -0
- package/dist/utils/export.js +3 -3
- package/dist/utils/git.d.ts +28 -0
- package/dist/utils/git.js +111 -1
- package/dist/utils/hooks.d.ts +26 -0
- package/dist/utils/hooks.js +69 -1
- package/dist/utils/keychain.js +45 -29
- package/dist/utils/logger.d.ts +12 -0
- package/dist/utils/logger.js +1 -1
- package/dist/utils/mcpConfig.d.ts +26 -0
- package/dist/utils/mcpConfig.js +109 -4
- package/dist/utils/mentions.d.ts +195 -0
- package/dist/utils/mentions.js +672 -0
- package/dist/utils/skillBundles.d.ts +14 -0
- package/dist/utils/skillBundles.js +3 -3
- package/dist/utils/skillBundlesCloud.d.ts +7 -0
- package/dist/utils/skillBundlesCloud.js +1 -1
- package/dist/utils/tokenTracker.js +21 -5
- package/dist/utils/toolParsing.d.ts +11 -0
- package/dist/utils/toolParsing.js +6 -0
- package/dist/utils/webFetch.d.ts +101 -0
- package/dist/utils/webFetch.js +375 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Message formatting utilities — extracted from `App.ts`.
|
|
3
|
+
*
|
|
4
|
+
* These are pure (or near-pure) functions that turn a chat message
|
|
5
|
+
* (`role` + raw `content`) into the array of styled lines the custom
|
|
6
|
+
* screen renderer paints. Extracting them:
|
|
7
|
+
*
|
|
8
|
+
* - shrinks `App.ts` (the 3.2k-LoC render-loop host) by ~600 LoC,
|
|
9
|
+
* - makes the formatter unit-testable in isolation (no App state),
|
|
10
|
+
* - lets the quick-chat / ACP surfaces reuse the same rendering if
|
|
11
|
+
* they ever need to paint formatted assistant output.
|
|
12
|
+
*
|
|
13
|
+
* Convention: a `RenderedLine` carries the already-ANSI-styled `text`
|
|
14
|
+
* plus a `raw` flag — when true, the screen writer must NOT apply the
|
|
15
|
+
* per-message default style (the line has its own colours baked in).
|
|
16
|
+
* This is how code blocks and headings keep their syntax-highlight
|
|
17
|
+
* palette instead of being washed by the body colour.
|
|
18
|
+
*/
|
|
19
|
+
import { fg, style, stringWidth } from '../ansi.js';
|
|
20
|
+
import { SYNTAX, highlightCode } from '../highlight.js';
|
|
21
|
+
// Shared primary colour — same value as the one defined in App.ts. Kept
|
|
22
|
+
// here so the formatter is self-contained; the App re-exports it from
|
|
23
|
+
// the same literal to avoid drift.
|
|
24
|
+
const PRIMARY_COLOR = fg.rgb(240, 42, 48);
|
|
25
|
+
// ─── Inline markdown ──────────────────────────────────────────────────────────
|
|
26
|
+
/**
|
|
27
|
+
* Apply inline markdown formatting (bold, italic, inline code, strikethrough)
|
|
28
|
+
* to a single line. Returns the styled string plus a flag telling the caller
|
|
29
|
+
* whether any formatting was applied — the caller uses that to decide whether
|
|
30
|
+
* to mark the line `raw` (so the renderer doesn't double-apply the body colour).
|
|
31
|
+
*/
|
|
32
|
+
export function applyInlineMarkdown(text) {
|
|
33
|
+
let result = '';
|
|
34
|
+
let hasFormatting = false;
|
|
35
|
+
let i = 0;
|
|
36
|
+
while (i < text.length) {
|
|
37
|
+
// Inline code: `code`
|
|
38
|
+
if (text[i] === '`' && text[i + 1] !== '`') {
|
|
39
|
+
const end = text.indexOf('`', i + 1);
|
|
40
|
+
if (end !== -1) {
|
|
41
|
+
const code = text.slice(i + 1, end);
|
|
42
|
+
result += fg.rgb(209, 154, 102) + code + '\x1b[0m';
|
|
43
|
+
hasFormatting = true;
|
|
44
|
+
i = end + 1;
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
// Bold + italic: ***text***
|
|
49
|
+
if (text.slice(i, i + 3) === '***') {
|
|
50
|
+
const end = text.indexOf('***', i + 3);
|
|
51
|
+
if (end !== -1) {
|
|
52
|
+
const inner = text.slice(i + 3, end);
|
|
53
|
+
result += style.bold + style.italic + PRIMARY_COLOR + inner + '\x1b[0m';
|
|
54
|
+
hasFormatting = true;
|
|
55
|
+
i = end + 3;
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
// Bold: **text**
|
|
60
|
+
if (text.slice(i, i + 2) === '**') {
|
|
61
|
+
const end = text.indexOf('**', i + 2);
|
|
62
|
+
if (end !== -1) {
|
|
63
|
+
const inner = text.slice(i + 2, end);
|
|
64
|
+
result += style.bold + PRIMARY_COLOR + inner + '\x1b[0m';
|
|
65
|
+
hasFormatting = true;
|
|
66
|
+
i = end + 2;
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
// Italic: *text*
|
|
71
|
+
if (text[i] === '*' && text[i + 1] !== '*') {
|
|
72
|
+
const end = text.indexOf('*', i + 1);
|
|
73
|
+
if (end !== -1 && end > i + 1) {
|
|
74
|
+
const inner = text.slice(i + 1, end);
|
|
75
|
+
result += style.italic + inner + '\x1b[0m';
|
|
76
|
+
hasFormatting = true;
|
|
77
|
+
i = end + 1;
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
// Strikethrough: ~~text~~ — using the SGR strikethrough escape (\x1b[9m).
|
|
82
|
+
// Widely supported in modern terminals (iTerm2, Kitty, WezTerm, Alacritty,
|
|
83
|
+
// gnome-terminal, Windows Terminal). Falls back gracefully to the dim
|
|
84
|
+
// text colour on terminals that don't render the SGR.
|
|
85
|
+
if (text.slice(i, i + 2) === '~~') {
|
|
86
|
+
const end = text.indexOf('~~', i + 2);
|
|
87
|
+
if (end !== -1) {
|
|
88
|
+
const inner = text.slice(i + 2, end);
|
|
89
|
+
result += '\x1b[9m' + fg.rgb(140, 140, 140) + inner + '\x1b[0m';
|
|
90
|
+
hasFormatting = true;
|
|
91
|
+
i = end + 2;
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
result += text[i];
|
|
96
|
+
i++;
|
|
97
|
+
}
|
|
98
|
+
return { formatted: result, hasFormatting };
|
|
99
|
+
}
|
|
100
|
+
// ─── Word wrap ────────────────────────────────────────────────────────────────
|
|
101
|
+
/**
|
|
102
|
+
* Word-wrap `text` to `maxWidth` columns. Words wider than `maxWidth`
|
|
103
|
+
* (typically long file paths with no spaces) are hard-broken across lines.
|
|
104
|
+
*
|
|
105
|
+
* Note: this is NOT the `wordWrap` exported from `ansi.ts` — that one
|
|
106
|
+
* lets over-width words overflow. This chat-flavoured variant slices
|
|
107
|
+
* them so a 200-char path doesn't blow out the right margin.
|
|
108
|
+
*/
|
|
109
|
+
export function wordWrap(text, maxWidth) {
|
|
110
|
+
const words = text.split(' ');
|
|
111
|
+
const lines = [];
|
|
112
|
+
let currentLine = '';
|
|
113
|
+
for (const word of words) {
|
|
114
|
+
const wordW = stringWidth(word);
|
|
115
|
+
// Hard-break words wider than maxWidth (e.g. long file paths with no spaces)
|
|
116
|
+
if (wordW > maxWidth) {
|
|
117
|
+
if (currentLine) {
|
|
118
|
+
lines.push(currentLine);
|
|
119
|
+
currentLine = '';
|
|
120
|
+
}
|
|
121
|
+
// Slice the word into maxWidth chunks
|
|
122
|
+
let remaining = word;
|
|
123
|
+
while (stringWidth(remaining) > maxWidth) {
|
|
124
|
+
lines.push(remaining.slice(0, maxWidth));
|
|
125
|
+
remaining = remaining.slice(maxWidth);
|
|
126
|
+
}
|
|
127
|
+
currentLine = remaining;
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
if (stringWidth(currentLine) + wordW + 1 > maxWidth && currentLine) {
|
|
131
|
+
lines.push(currentLine);
|
|
132
|
+
currentLine = word;
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
currentLine += (currentLine ? ' ' : '') + word;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
if (currentLine) {
|
|
139
|
+
lines.push(currentLine);
|
|
140
|
+
}
|
|
141
|
+
return lines.length > 0 ? lines : [''];
|
|
142
|
+
}
|
|
143
|
+
// ─── Text lines ───────────────────────────────────────────────────────────────
|
|
144
|
+
/**
|
|
145
|
+
* Format a chunk of prose (text between code fences) into styled lines.
|
|
146
|
+
*
|
|
147
|
+
* Recognises, per line:
|
|
148
|
+
* - ATX headings (`#` … `######`)
|
|
149
|
+
* - horizontal rules (`---`, `***`, `___`)
|
|
150
|
+
* - blockquotes (`> …`, nestable)
|
|
151
|
+
* - bullet/numbered list items (`-`, `*`, `1.`)
|
|
152
|
+
* - plain text, with inline markdown + word-wrap
|
|
153
|
+
*
|
|
154
|
+
* `firstPrefix` / `firstStyle` apply only to the first output line — the
|
|
155
|
+
* caller uses that to attach the role indicator (▌ for user, ▸ for system);
|
|
156
|
+
* continuation lines get a blank/generic prefix so wrapped paragraphs stay
|
|
157
|
+
* visually grouped under the same marker.
|
|
158
|
+
*/
|
|
159
|
+
export function formatTextLines(text, maxWidth, firstPrefix, firstStyle, rawPrefix = false) {
|
|
160
|
+
const lines = [];
|
|
161
|
+
const contentLines = text.split('\n');
|
|
162
|
+
for (let i = 0; i < contentLines.length; i++) {
|
|
163
|
+
const line = contentLines[i];
|
|
164
|
+
const prefix = i === 0 ? firstPrefix : ' ';
|
|
165
|
+
const prefixStyle = i === 0 ? firstStyle : '';
|
|
166
|
+
const isRaw = i === 0 ? rawPrefix : false;
|
|
167
|
+
// Heading: ## or ### etc.
|
|
168
|
+
const headingMatch = line.match(/^(#{1,6})\s+(.+)$/);
|
|
169
|
+
if (headingMatch) {
|
|
170
|
+
const level = headingMatch[1].length;
|
|
171
|
+
const headingText = headingMatch[2];
|
|
172
|
+
const headingColor = level <= 2 ? fg.rgb(97, 175, 239) : fg.rgb(198, 120, 221);
|
|
173
|
+
lines.push({
|
|
174
|
+
text: prefix + headingColor + style.bold + headingText + '\x1b[0m',
|
|
175
|
+
style: prefixStyle,
|
|
176
|
+
raw: true,
|
|
177
|
+
});
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
// Horizontal rule: --- or *** or ___
|
|
181
|
+
if (/^[-*_]{3,}\s*$/.test(line)) {
|
|
182
|
+
const ruleWidth = Math.min(maxWidth - 4, 40);
|
|
183
|
+
lines.push({
|
|
184
|
+
text: prefix + fg.gray + '─'.repeat(ruleWidth) + '\x1b[0m',
|
|
185
|
+
style: prefixStyle,
|
|
186
|
+
raw: true,
|
|
187
|
+
});
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
// Blockquote: `> text` — render with a left accent bar (PRIMARY_COLOR)
|
|
191
|
+
// and the body in a dimmer grey so it visually sits behind regular text.
|
|
192
|
+
// Strips one level of `> ` so nested quotes render with their own bar.
|
|
193
|
+
const quoteMatch = line.match(/^(\s*)>\s?(.*)$/);
|
|
194
|
+
if (quoteMatch) {
|
|
195
|
+
const indent = quoteMatch[1];
|
|
196
|
+
const quoteText = quoteMatch[2];
|
|
197
|
+
const { formatted, hasFormatting } = applyInlineMarkdown(quoteText);
|
|
198
|
+
const body = hasFormatting ? formatted : quoteText;
|
|
199
|
+
// Vertical bar + space, then dim grey body. Skip if the body is
|
|
200
|
+
// empty so an isolated `>` renders cleanly.
|
|
201
|
+
const barred = PRIMARY_COLOR + '│' + '\x1b[0m' + (body ? ' ' + fg.rgb(160, 160, 160) + body + '\x1b[0m' : '');
|
|
202
|
+
lines.push({
|
|
203
|
+
text: prefix + indent + barred,
|
|
204
|
+
style: prefixStyle,
|
|
205
|
+
raw: true,
|
|
206
|
+
});
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
// List items: - item or * item or numbered 1. item
|
|
210
|
+
const listMatch = line.match(/^(\s*)([-*]|\d+\.)\s+(.+)$/);
|
|
211
|
+
if (listMatch) {
|
|
212
|
+
const indent = listMatch[1];
|
|
213
|
+
const bullet = listMatch[2];
|
|
214
|
+
const content = listMatch[3];
|
|
215
|
+
const { formatted, hasFormatting } = applyInlineMarkdown(content);
|
|
216
|
+
const bulletChar = bullet === '-' || bullet === '*' ? '\u25b8' : bullet;
|
|
217
|
+
if (hasFormatting) {
|
|
218
|
+
lines.push({
|
|
219
|
+
text: prefix + indent + fg.gray + bulletChar + '\x1b[0m' + ' ' + formatted,
|
|
220
|
+
style: prefixStyle,
|
|
221
|
+
raw: true,
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
else {
|
|
225
|
+
lines.push({
|
|
226
|
+
text: prefix + indent + bulletChar + ' ' + content,
|
|
227
|
+
style: prefixStyle,
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
// Regular text with possible inline markdown
|
|
233
|
+
const { formatted, hasFormatting } = applyInlineMarkdown(line);
|
|
234
|
+
if (hasFormatting) {
|
|
235
|
+
// Use original (no-ANSI) line to measure and wrap, then apply markdown per segment
|
|
236
|
+
if (stringWidth(line) > maxWidth - prefix.length) {
|
|
237
|
+
const wrapped = wordWrap(line, maxWidth - prefix.length);
|
|
238
|
+
for (let j = 0; j < wrapped.length; j++) {
|
|
239
|
+
const { formatted: segFormatted } = applyInlineMarkdown(wrapped[j]);
|
|
240
|
+
lines.push({
|
|
241
|
+
text: (j === 0 ? prefix : ' ') + segFormatted,
|
|
242
|
+
style: j === 0 ? prefixStyle : '',
|
|
243
|
+
raw: true,
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
else {
|
|
248
|
+
lines.push({
|
|
249
|
+
text: prefix + formatted,
|
|
250
|
+
style: prefixStyle,
|
|
251
|
+
raw: true,
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
else {
|
|
256
|
+
// Plain text - word wrap as before
|
|
257
|
+
if (stringWidth(line) > maxWidth - prefix.length) {
|
|
258
|
+
const wrapped = wordWrap(line, maxWidth - prefix.length);
|
|
259
|
+
for (let j = 0; j < wrapped.length; j++) {
|
|
260
|
+
const lineIsRaw = j === 0 ? isRaw : false;
|
|
261
|
+
lines.push({
|
|
262
|
+
text: (j === 0 ? prefix : ' ') + wrapped[j],
|
|
263
|
+
style: j === 0 ? prefixStyle : '',
|
|
264
|
+
...(lineIsRaw ? { raw: true } : {}),
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
else {
|
|
269
|
+
lines.push({
|
|
270
|
+
text: prefix + line,
|
|
271
|
+
style: prefixStyle,
|
|
272
|
+
...(isRaw ? { raw: true } : {}),
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
return lines;
|
|
278
|
+
}
|
|
279
|
+
// ─── Code block ───────────────────────────────────────────────────────────────
|
|
280
|
+
/**
|
|
281
|
+
* Format a fenced code block: language label + block number on the first
|
|
282
|
+
* line, then each source line indented and syntax-highlighted via
|
|
283
|
+
* `highlightCode`. A trailing blank line separates the block from the
|
|
284
|
+
* next paragraph.
|
|
285
|
+
*
|
|
286
|
+
* `blockNum` is the 1-based index used by `/copy [n]`; omitted when the
|
|
287
|
+
* caller is rendering a non-chat context (e.g. a paste preview).
|
|
288
|
+
*/
|
|
289
|
+
export function formatCodeBlock(code, lang, maxWidth, blockNum) {
|
|
290
|
+
const lines = [];
|
|
291
|
+
const codeLines = code.split('\n');
|
|
292
|
+
// Remove trailing empty line if exists
|
|
293
|
+
if (codeLines.length > 0 && codeLines[codeLines.length - 1] === '') {
|
|
294
|
+
codeLines.pop();
|
|
295
|
+
}
|
|
296
|
+
// Language label with block number for /copy
|
|
297
|
+
const label = blockNum ? (lang ? ` ${lang} [${blockNum}]` : ` [${blockNum}]`) : (lang ? ' ' + lang : '');
|
|
298
|
+
if (label) {
|
|
299
|
+
lines.push({ text: label, style: SYNTAX.codeLang, raw: false });
|
|
300
|
+
}
|
|
301
|
+
// Code lines with highlighting and indent
|
|
302
|
+
for (const codeLine of codeLines) {
|
|
303
|
+
const highlighted = highlightCode(codeLine, lang);
|
|
304
|
+
lines.push({
|
|
305
|
+
text: ' ' + highlighted,
|
|
306
|
+
style: '',
|
|
307
|
+
raw: true, // Don't apply additional styling, code is pre-highlighted
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
// Empty line after code block
|
|
311
|
+
lines.push({ text: '', style: '', raw: false });
|
|
312
|
+
return lines;
|
|
313
|
+
}
|
|
314
|
+
// ─── Message ──────────────────────────────────────────────────────────────────
|
|
315
|
+
/**
|
|
316
|
+
* Format a full chat message (role + content) into styled lines.
|
|
317
|
+
*
|
|
318
|
+
* Walks the content looking for `` ``` ``-fenced code blocks; anything
|
|
319
|
+
* outside a fence goes through `formatTextLines` (headings, lists, inline
|
|
320
|
+
* markdown), fenced regions go through `formatCodeBlock`. The fenced-block
|
|
321
|
+
* regex is non-greedy and stops at the first closing fence, so streaming
|
|
322
|
+
* input with an as-yet-unclosed fence still renders correctly (the open
|
|
323
|
+
* tail is treated as plain text until the closer arrives).
|
|
324
|
+
*
|
|
325
|
+
* `counter` is incremented once per fenced block encountered — callers
|
|
326
|
+
* thread the same `BlockCounter` across consecutive `formatMessage`
|
|
327
|
+
* calls so block numbers are stable across a whole render pass.
|
|
328
|
+
*/
|
|
329
|
+
export function formatMessage(role, content, maxWidth, counter) {
|
|
330
|
+
const lines = [];
|
|
331
|
+
// Role-specific prefix — user gets primary color bar, assistant gets dim header, system gets diamond
|
|
332
|
+
const contIndent = ' ';
|
|
333
|
+
let firstPrefix;
|
|
334
|
+
const firstStyle = '';
|
|
335
|
+
if (role === 'user') {
|
|
336
|
+
firstPrefix = PRIMARY_COLOR + '\u258c ' + style.reset;
|
|
337
|
+
}
|
|
338
|
+
else if (role === 'assistant') {
|
|
339
|
+
lines.push({ text: PRIMARY_COLOR + '\u254c\u254c' + style.reset + fg.rgb(120, 120, 120) + ' codeep' + style.reset, style: '', raw: true });
|
|
340
|
+
firstPrefix = ' ';
|
|
341
|
+
}
|
|
342
|
+
else {
|
|
343
|
+
firstPrefix = PRIMARY_COLOR + '\u25b8 ' + style.reset;
|
|
344
|
+
}
|
|
345
|
+
const codeBlockRegex = /```([^\n]*)\n([\s\S]*?)```/g;
|
|
346
|
+
let lastIndex = 0;
|
|
347
|
+
let match;
|
|
348
|
+
let isFirstLine = true;
|
|
349
|
+
while ((match = codeBlockRegex.exec(content)) !== null) {
|
|
350
|
+
const textBefore = content.slice(lastIndex, match.index);
|
|
351
|
+
if (textBefore) {
|
|
352
|
+
const prefix = isFirstLine ? firstPrefix : (role === 'user' ? contIndent : ' ');
|
|
353
|
+
const textLines = formatTextLines(textBefore, maxWidth, prefix, firstStyle, role === 'user' && isFirstLine);
|
|
354
|
+
lines.push(...textLines);
|
|
355
|
+
isFirstLine = false;
|
|
356
|
+
}
|
|
357
|
+
counter.current++;
|
|
358
|
+
const rawLang = (match[1] || 'text').trim();
|
|
359
|
+
let lang = rawLang;
|
|
360
|
+
if (rawLang.includes(':') || rawLang.includes('.')) {
|
|
361
|
+
lang = rawLang.split('.').pop() || rawLang;
|
|
362
|
+
}
|
|
363
|
+
lines.push(...formatCodeBlock(match[2], lang, maxWidth, counter.current));
|
|
364
|
+
lastIndex = match.index + match[0].length;
|
|
365
|
+
isFirstLine = false;
|
|
366
|
+
}
|
|
367
|
+
const textAfter = content.slice(lastIndex);
|
|
368
|
+
if (textAfter) {
|
|
369
|
+
const prefix = isFirstLine ? firstPrefix : (role === 'user' ? contIndent : ' ');
|
|
370
|
+
const textLines = formatTextLines(textAfter, maxWidth, prefix, firstStyle, role === 'user' && isFirstLine);
|
|
371
|
+
lines.push(...textLines);
|
|
372
|
+
}
|
|
373
|
+
lines.push({ text: '', style: '' });
|
|
374
|
+
return lines;
|
|
375
|
+
}
|
|
@@ -18,3 +18,7 @@ export declare function renderPermissionScreen(screen: Screen, options: Permissi
|
|
|
18
18
|
* Get permission options array for easy indexing
|
|
19
19
|
*/
|
|
20
20
|
export declare function getPermissionOptions(): PermissionLevel[];
|
|
21
|
+
/**
|
|
22
|
+
* Truncate path for display
|
|
23
|
+
*/
|
|
24
|
+
export declare function truncatePath(path: string, maxLen: number): string;
|
|
@@ -97,7 +97,7 @@ export function getPermissionOptions() {
|
|
|
97
97
|
/**
|
|
98
98
|
* Truncate path for display
|
|
99
99
|
*/
|
|
100
|
-
function truncatePath(path, maxLen) {
|
|
100
|
+
export function truncatePath(path, maxLen) {
|
|
101
101
|
if (path.length <= maxLen)
|
|
102
102
|
return path;
|
|
103
103
|
const parts = path.split('/');
|
|
@@ -25,3 +25,7 @@ export interface StatusInfo {
|
|
|
25
25
|
* Render status screen
|
|
26
26
|
*/
|
|
27
27
|
export declare function renderStatusScreen(screen: Screen, status: StatusInfo): void;
|
|
28
|
+
/**
|
|
29
|
+
* Truncate path for display
|
|
30
|
+
*/
|
|
31
|
+
export declare function truncatePath(path: string, maxLen: number): string;
|
|
@@ -2,8 +2,7 @@
|
|
|
2
2
|
* Status screen component
|
|
3
3
|
*/
|
|
4
4
|
import { fg, style } from '../ansi.js';
|
|
5
|
-
|
|
6
|
-
const PRIMARY_COLOR = fg.rgb(240, 42, 48);
|
|
5
|
+
import { PRIMARY_COLOR } from './uiConstants.js';
|
|
7
6
|
/**
|
|
8
7
|
* Render status screen
|
|
9
8
|
*/
|
|
@@ -80,7 +79,7 @@ export function renderStatusScreen(screen, status) {
|
|
|
80
79
|
/**
|
|
81
80
|
* Truncate path for display
|
|
82
81
|
*/
|
|
83
|
-
function truncatePath(path, maxLen) {
|
|
82
|
+
export function truncatePath(path, maxLen) {
|
|
84
83
|
if (path.length <= maxLen)
|
|
85
84
|
return path;
|
|
86
85
|
// Try to keep the end of the path
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export interface FormattedLine {
|
|
2
|
+
text: string;
|
|
3
|
+
style: string;
|
|
4
|
+
raw?: boolean;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Format the welcome message body into coloured terminal lines.
|
|
8
|
+
*
|
|
9
|
+
* The body is a small DSL of line shapes:
|
|
10
|
+
* - `Codeep vX.X.X · Provider · Model` — version header
|
|
11
|
+
* - ` Project <path>` — project label
|
|
12
|
+
* - ` Access <read · write>` — access label
|
|
13
|
+
* - ` Mode <mode>` — mode label
|
|
14
|
+
* - lines containing `⚠` — amber warning
|
|
15
|
+
* - lines containing `/help` — shortcuts hint
|
|
16
|
+
*
|
|
17
|
+
* Anything else is pushed verbatim.
|
|
18
|
+
*/
|
|
19
|
+
export declare function formatWelcomeMessage(content: string): FormattedLine[];
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Welcome-screen formatter.
|
|
3
|
+
*
|
|
4
|
+
* Pure function extracted from `App.ts` so the welcome banner's colour
|
|
5
|
+
* rules can be unit-tested without instantiating the full renderer.
|
|
6
|
+
* Called once per render for messages whose `role` is `'welcome'`.
|
|
7
|
+
*/
|
|
8
|
+
import { fg, style } from '../ansi.js';
|
|
9
|
+
import { PRIMARY_COLOR } from './uiConstants.js';
|
|
10
|
+
/**
|
|
11
|
+
* Format the welcome message body into coloured terminal lines.
|
|
12
|
+
*
|
|
13
|
+
* The body is a small DSL of line shapes:
|
|
14
|
+
* - `Codeep vX.X.X · Provider · Model` — version header
|
|
15
|
+
* - ` Project <path>` — project label
|
|
16
|
+
* - ` Access <read · write>` — access label
|
|
17
|
+
* - ` Mode <mode>` — mode label
|
|
18
|
+
* - lines containing `⚠` — amber warning
|
|
19
|
+
* - lines containing `/help` — shortcuts hint
|
|
20
|
+
*
|
|
21
|
+
* Anything else is pushed verbatim.
|
|
22
|
+
*/
|
|
23
|
+
export function formatWelcomeMessage(content) {
|
|
24
|
+
const lines = [];
|
|
25
|
+
const DIM = fg.rgb(80, 80, 80);
|
|
26
|
+
const LABEL = fg.rgb(100, 100, 100);
|
|
27
|
+
const SEP = DIM + ' · ' + style.reset;
|
|
28
|
+
for (const line of content.split('\n')) {
|
|
29
|
+
if (line.trim() === '') {
|
|
30
|
+
lines.push({ text: '', style: '' });
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
// Version line: "Codeep vX.X.X · Provider · Model"
|
|
34
|
+
if (line.startsWith('Codeep ')) {
|
|
35
|
+
const parts = line.split(' · ');
|
|
36
|
+
const colored = PRIMARY_COLOR + style.bold + (parts[0] || '') + style.reset
|
|
37
|
+
+ SEP + fg.rgb(180, 180, 180) + (parts[1] || '') + style.reset
|
|
38
|
+
+ SEP + fg.rgb(130, 130, 130) + (parts[2] || '') + style.reset;
|
|
39
|
+
lines.push({ text: colored, style: '', raw: true });
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
// Project line
|
|
43
|
+
if (/^\s+Project\s/.test(line)) {
|
|
44
|
+
const value = line.replace(/^\s+Project\s+/, '');
|
|
45
|
+
lines.push({ text: LABEL + ' Project ' + style.reset + fg.rgb(100, 180, 220) + value + style.reset, style: '', raw: true });
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
// Access line
|
|
49
|
+
if (/^\s+Access\s/.test(line)) {
|
|
50
|
+
const value = line.replace(/^\s+Access\s+/, '');
|
|
51
|
+
const parts = value.split(' · ');
|
|
52
|
+
const accessColored = fg.rgb(100, 200, 120) + style.bold + (parts[0] || '') + style.reset;
|
|
53
|
+
const rest = parts.slice(1).map(p => fg.rgb(80, 160, 100) + p + style.reset).join(SEP);
|
|
54
|
+
lines.push({ text: LABEL + ' Access ' + style.reset + accessColored + (rest ? SEP + rest : ''), style: '', raw: true });
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
// Mode line
|
|
58
|
+
if (/^\s+Mode\s/.test(line)) {
|
|
59
|
+
const value = line.replace(/^\s+Mode\s+/, '');
|
|
60
|
+
lines.push({ text: LABEL + ' Mode ' + style.reset + fg.rgb(160, 160, 160) + value + style.reset, style: '', raw: true });
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
// Agent Mode warning
|
|
64
|
+
if (line.includes('⚠')) {
|
|
65
|
+
lines.push({ text: ' ' + fg.rgb(220, 160, 40) + line.trim() + style.reset, style: '', raw: true });
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
// Shortcuts line
|
|
69
|
+
if (line.includes('/help')) {
|
|
70
|
+
const parts = line.trim().split(' · ');
|
|
71
|
+
const colored = parts.map(p => fg.rgb(150, 150, 150) + p.trim() + style.reset).join(DIM + ' · ' + style.reset);
|
|
72
|
+
lines.push({ text: ' ' + colored, style: '', raw: true });
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
lines.push({ text: line, style: '' });
|
|
76
|
+
}
|
|
77
|
+
lines.push({ text: '', style: '' });
|
|
78
|
+
return lines;
|
|
79
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** Brand red — used for the logo, the agent-panel title, and accents. */
|
|
2
|
+
export declare const PRIMARY_COLOR: string;
|
|
3
|
+
/** Spinner frames for the agent progress panel (8-step rotation). */
|
|
4
|
+
export declare const SPINNER_FRAMES: string[];
|
|
5
|
+
/** ASCII art logo, one string per terminal line. */
|
|
6
|
+
export declare const LOGO_LINES: string[];
|
|
7
|
+
/** Logo height in terminal lines (LOGO_LINES.length). */
|
|
8
|
+
export declare const LOGO_HEIGHT: number;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared UI constants for the renderer.
|
|
3
|
+
*
|
|
4
|
+
* Centralised so the colour palette, spinner animation, and ASCII logo
|
|
5
|
+
* have a single home — both `App.ts` and any extracted component can
|
|
6
|
+
* import them without re-declaring (which would let the palette drift
|
|
7
|
+
* between files).
|
|
8
|
+
*/
|
|
9
|
+
import { fg } from '../ansi.js';
|
|
10
|
+
/** Brand red — used for the logo, the agent-panel title, and accents. */
|
|
11
|
+
export const PRIMARY_COLOR = fg.rgb(240, 42, 48);
|
|
12
|
+
/** Spinner frames for the agent progress panel (8-step rotation). */
|
|
13
|
+
export const SPINNER_FRAMES = ['▖', '▘', '▝', '▗', '▌', '▀', '▐', '▄'];
|
|
14
|
+
/** ASCII art logo, one string per terminal line. */
|
|
15
|
+
export const LOGO_LINES = [
|
|
16
|
+
' ██████╗ ██████╗ ██████╗ ███████╗███████╗██████╗ ',
|
|
17
|
+
'██╔════╝██╔═══██╗██╔══██╗██╔════╝██╔════╝██╔══██╗',
|
|
18
|
+
'██║ ██║ ██║██║ ██║█████╗ █████╗ ██████╔╝',
|
|
19
|
+
'██║ ██║ ██║██║ ██║██╔══╝ ██╔══╝ ██╔═══╝ ',
|
|
20
|
+
'╚██████╗╚██████╔╝██████╔╝███████╗███████╗██║ ',
|
|
21
|
+
' ╚═════╝ ╚═════╝ ╚═════╝ ╚══════╝╚══════╝╚═╝ ',
|
|
22
|
+
];
|
|
23
|
+
/** Logo height in terminal lines (LOGO_LINES.length). */
|
|
24
|
+
export const LOGO_HEIGHT = LOGO_LINES.length;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure input-parsing helpers extracted from App.ts.
|
|
3
|
+
*
|
|
4
|
+
* The old `handleCommand` method inlined `input.slice(1).split(' ')` and a
|
|
5
|
+
* `.toLowerCase()` on every call, with no test coverage. Pulling the parse
|
|
6
|
+
* into a pure function lets us unit-test the edge cases (extra whitespace,
|
|
7
|
+
* empty args, uppercase, leading slash) directly.
|
|
8
|
+
*/
|
|
9
|
+
export interface ParsedCommand {
|
|
10
|
+
/** Lower-cased command name, without the leading slash. */
|
|
11
|
+
command: string;
|
|
12
|
+
/** Remaining args, already split on spaces (empty strings removed). */
|
|
13
|
+
args: string[];
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Parse a raw user input line that begins with `/` into a command name
|
|
17
|
+
* and arguments. Trims and collapses runs of whitespace so `/scan src`
|
|
18
|
+
* behaves the same as `/scan src`.
|
|
19
|
+
*
|
|
20
|
+
* Returns `null` when the input doesn’t start with `/` or is blank.
|
|
21
|
+
*/
|
|
22
|
+
export declare function parseCommandInput(input: string): ParsedCommand | null;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure input-parsing helpers extracted from App.ts.
|
|
3
|
+
*
|
|
4
|
+
* The old `handleCommand` method inlined `input.slice(1).split(' ')` and a
|
|
5
|
+
* `.toLowerCase()` on every call, with no test coverage. Pulling the parse
|
|
6
|
+
* into a pure function lets us unit-test the edge cases (extra whitespace,
|
|
7
|
+
* empty args, uppercase, leading slash) directly.
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* Parse a raw user input line that begins with `/` into a command name
|
|
11
|
+
* and arguments. Trims and collapses runs of whitespace so `/scan src`
|
|
12
|
+
* behaves the same as `/scan src`.
|
|
13
|
+
*
|
|
14
|
+
* Returns `null` when the input doesn’t start with `/` or is blank.
|
|
15
|
+
*/
|
|
16
|
+
export function parseCommandInput(input) {
|
|
17
|
+
if (!input.startsWith('/'))
|
|
18
|
+
return null;
|
|
19
|
+
// Collapse runs of whitespace so " " between args doesn’t yield
|
|
20
|
+
// empty-string args, and trim the leading "/" plus surrounding space.
|
|
21
|
+
const parts = input.slice(1).trim().split(/\s+/);
|
|
22
|
+
if (parts.length === 0 || parts[0] === '')
|
|
23
|
+
return null;
|
|
24
|
+
return {
|
|
25
|
+
command: parts[0].toLowerCase(),
|
|
26
|
+
args: parts.slice(1),
|
|
27
|
+
};
|
|
28
|
+
}
|