nexrall-code 0.5.115 → 0.5.117
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/dist/index.js +309 -103
- package/package.json +11 -16
- package/dist/audit.js +0 -102
- package/dist/commands/auth.js +0 -134
- package/dist/commands/chat.js +0 -2715
- package/dist/commands/plugin.js +0 -393
- package/dist/commands/sessions.js +0 -62
- package/dist/commands/update.js +0 -174
- package/dist/permissions/handler.js +0 -332
- package/dist/terminal/terminalSetup.js +0 -395
- package/dist/trust.js +0 -137
- package/dist/ui/diff.js +0 -51
- package/dist/ui/inkReadlineAdapter.js +0 -116
- package/dist/ui/inkTerminal.js +0 -1097
- package/dist/ui/resizeRepaint.js +0 -36
- package/dist/ui/screen.js +0 -482
- package/dist/ui/theme.js +0 -385
- package/dist/ui/trustPrompt.js +0 -126
package/dist/ui/theme.js
DELETED
|
@@ -1,385 +0,0 @@
|
|
|
1
|
-
import chalk from 'chalk';
|
|
2
|
-
import stringWidth from 'string-width';
|
|
3
|
-
// ─── Color Palette ────────────────────────────────────────────────────────────
|
|
4
|
-
export const colors = {
|
|
5
|
-
primary: chalk.blue,
|
|
6
|
-
success: chalk.green,
|
|
7
|
-
error: chalk.red,
|
|
8
|
-
warning: chalk.yellow,
|
|
9
|
-
dim: chalk.dim,
|
|
10
|
-
bold: chalk.bold,
|
|
11
|
-
cyan: chalk.cyan,
|
|
12
|
-
gray: chalk.gray,
|
|
13
|
-
};
|
|
14
|
-
// ─── Icons ────────────────────────────────────────────────────────────────────
|
|
15
|
-
export const icons = {
|
|
16
|
-
tool: '⚡',
|
|
17
|
-
file: '📄',
|
|
18
|
-
dir: '📁',
|
|
19
|
-
check: '✓',
|
|
20
|
-
cross: '✗',
|
|
21
|
-
arrow: '→',
|
|
22
|
-
thinking: '◆',
|
|
23
|
-
code: '{}',
|
|
24
|
-
};
|
|
25
|
-
// ─── Banner ───────────────────────────────────────────────────────────────────
|
|
26
|
-
export function banner() {
|
|
27
|
-
const version = '0.1.1';
|
|
28
|
-
console.log();
|
|
29
|
-
console.log(chalk.bold.blue(' ███╗ ██╗███████╗██╗ ██╗'));
|
|
30
|
-
console.log(chalk.bold.blue(' ████╗ ██║██╔════╝╚██╗██╔╝'));
|
|
31
|
-
console.log(chalk.bold.blue(' ██╔██╗ ██║█████╗ ╚███╔╝ '));
|
|
32
|
-
console.log(chalk.bold.blue(' ██║╚██╗██║██╔══╝ ██╔██╗ '));
|
|
33
|
-
console.log(chalk.bold.blue(' ██║ ╚████║███████╗██╔╝ ██╗'));
|
|
34
|
-
console.log(chalk.bold.blue(' ╚═╝ ╚═══╝╚══════╝╚═╝ ╚═╝'));
|
|
35
|
-
console.log();
|
|
36
|
-
console.log(chalk.bold(' Nexrall Code') +
|
|
37
|
-
chalk.dim(` v${version}`) +
|
|
38
|
-
chalk.dim(' · ') +
|
|
39
|
-
chalk.cyan('AI coding assistant'));
|
|
40
|
-
console.log(chalk.dim(' ─────────────────────────────────────'));
|
|
41
|
-
console.log();
|
|
42
|
-
}
|
|
43
|
-
// ─── Streaming Markdown Renderer ──────────────────────────────────────────────
|
|
44
|
-
//
|
|
45
|
-
// Buffers incomplete lines, renders complete lines with terminal formatting.
|
|
46
|
-
// Tables are fully buffered until a non-table line arrives so column widths
|
|
47
|
-
// can be computed dynamically — fixing alignment and ANSI padding bugs.
|
|
48
|
-
/**
|
|
49
|
-
* Width of `s` in terminal CELLS, ignoring ANSI styling.
|
|
50
|
-
*
|
|
51
|
-
* This is deliberately NOT `String#length`. A terminal grid is measured in
|
|
52
|
-
* cells, and the mapping from characters to cells is not 1:1:
|
|
53
|
-
* • CJK ideographs, kana, Hangul and fullwidth forms occupy TWO cells each
|
|
54
|
-
* ("你好" is 2 characters but 4 cells wide).
|
|
55
|
-
* • Most emoji are two cells, and a ZWJ sequence like "👨👩👧" is many code
|
|
56
|
-
* points but still renders as a single two-cell glyph.
|
|
57
|
-
* • Combining marks ("e" + U+0301) and zero-width joiners add code points
|
|
58
|
-
* but no width at all.
|
|
59
|
-
*
|
|
60
|
-
* Measuring with `.length` therefore under- or over-counts for every language
|
|
61
|
-
* that isn't plain Latin, which silently breaks anything doing column
|
|
62
|
-
* arithmetic on top of it — the welcome banner's box drew its border 6 cells
|
|
63
|
-
* too narrow the moment the working-directory path contained CJK, leaving the
|
|
64
|
-
* frame visibly torn.
|
|
65
|
-
*
|
|
66
|
-
* `string-width` is the same battle-tested implementation Ink itself uses to
|
|
67
|
-
* lay out text (it is already in the dependency tree for that reason), so
|
|
68
|
-
* this keeps our own measurements consistent with how Ink measures the rest
|
|
69
|
-
* of the UI, instead of maintaining a second, hand-rolled Unicode width table.
|
|
70
|
-
*/
|
|
71
|
-
export function visibleLen(s) {
|
|
72
|
-
return stringWidth(s);
|
|
73
|
-
}
|
|
74
|
-
/** Pad s to n visible cells (safe with ANSI codes and wide/zero-width chars). */
|
|
75
|
-
export function padVis(s, n) {
|
|
76
|
-
const pad = n - visibleLen(s);
|
|
77
|
-
return pad > 0 ? s + ' '.repeat(pad) : s;
|
|
78
|
-
}
|
|
79
|
-
/**
|
|
80
|
-
* Width (in cells) for a decorative horizontal rule — code-block borders,
|
|
81
|
-
* the `---` rule, etc. — drawn `' '` (2-cell indent) + this many `'─'`.
|
|
82
|
-
*
|
|
83
|
-
* These used to be hardcoded (`'─'.repeat(48)`, `'─'.repeat(50)`): fine at
|
|
84
|
-
* the ~80-100 column width most terminals start at, but on anything
|
|
85
|
-
* narrower the fixed-width string is itself wider than the terminal, so
|
|
86
|
-
* the terminal's own autowrap breaks it into two lines — visually
|
|
87
|
-
* identical to the "torn separator" bug already fixed for the input box's
|
|
88
|
-
* chrome (see resizeRepaint.ts), just for transcript output instead of
|
|
89
|
-
* live UI. Deriving the width from `process.stdout.columns` at print time
|
|
90
|
-
* (same convention `rowsOccupied`/`bottomChromeRows` use in screen.ts)
|
|
91
|
-
* means it can never be wider than the terminal actually is.
|
|
92
|
-
*
|
|
93
|
-
* - `MIN` keeps the rule from collapsing to nothing on extremely narrow
|
|
94
|
-
* terminals (a 0-width or 1-`─` rule reads as a rendering glitch, not
|
|
95
|
-
* a rule).
|
|
96
|
-
* - `MAX` keeps it from stretching edge-to-edge on very wide terminals,
|
|
97
|
-
* matching the previous fixed-width look rather than a rule that's
|
|
98
|
-
* visually disproportionate to a short paragraph next to it.
|
|
99
|
-
*/
|
|
100
|
-
export function ruleWidth(max = 50, columns = process.stdout.columns || 80) {
|
|
101
|
-
const MIN = 20;
|
|
102
|
-
const indent = 2; // the ' ' prefix every call site already adds
|
|
103
|
-
const available = columns - indent;
|
|
104
|
-
return Math.max(MIN, Math.min(max, available));
|
|
105
|
-
}
|
|
106
|
-
export class MarkdownStreamRenderer {
|
|
107
|
-
_buf = ''; // incomplete trailing chunk
|
|
108
|
-
_inCode = false;
|
|
109
|
-
_codeLang = '';
|
|
110
|
-
// Table rows accumulated until a non-table line flushes them.
|
|
111
|
-
// Each element is a string[] of styled cells, OR ['__SEP__'] for separator row.
|
|
112
|
-
_tbl = [];
|
|
113
|
-
// ── Public API ──────────────────────────────��──────────────────────────────
|
|
114
|
-
feed(chunk) {
|
|
115
|
-
this._buf += chunk;
|
|
116
|
-
const parts = this._buf.split('\n');
|
|
117
|
-
this._buf = parts.pop() ?? '';
|
|
118
|
-
for (const line of parts)
|
|
119
|
-
this._line(line);
|
|
120
|
-
}
|
|
121
|
-
/**
|
|
122
|
-
* Drop all buffered/partial state without emitting it.
|
|
123
|
-
*
|
|
124
|
-
* Used when a stream died mid-render and the same turn is being restarted: the
|
|
125
|
-
* replacement attempt re-sends the response from the beginning, so any half-parsed
|
|
126
|
-
* line, unterminated code fence or partially accumulated table from the dead attempt
|
|
127
|
-
* must be discarded. Flushing instead would print a fragment and then leave the
|
|
128
|
-
* renderer in `_inCode`/`_tbl` state that corrupts everything the retry prints.
|
|
129
|
-
*
|
|
130
|
-
* Note this only resets the PARSER — characters already written to stdout have
|
|
131
|
-
* scrolled away and cannot be unprinted; the caller prints a visible restart marker
|
|
132
|
-
* so the duplicated prefix is explained rather than silently confusing.
|
|
133
|
-
*/
|
|
134
|
-
reset() {
|
|
135
|
-
this._buf = '';
|
|
136
|
-
this._inCode = false;
|
|
137
|
-
this._codeLang = '';
|
|
138
|
-
this._tbl = [];
|
|
139
|
-
}
|
|
140
|
-
flush() {
|
|
141
|
-
if (this._buf) {
|
|
142
|
-
this._line(this._buf);
|
|
143
|
-
this._buf = '';
|
|
144
|
-
}
|
|
145
|
-
if (this._inCode) {
|
|
146
|
-
console.log(chalk.dim(' └' + '─'.repeat(ruleWidth(48))));
|
|
147
|
-
this._inCode = false;
|
|
148
|
-
}
|
|
149
|
-
this._flushTable();
|
|
150
|
-
console.log();
|
|
151
|
-
}
|
|
152
|
-
// ── Table flush ────────────────────────────────────────────────────────────
|
|
153
|
-
_flushTable() {
|
|
154
|
-
if (!this._tbl.length)
|
|
155
|
-
return;
|
|
156
|
-
const rows = this._tbl;
|
|
157
|
-
this._tbl = [];
|
|
158
|
-
// Find separator row index (marks end of header)
|
|
159
|
-
const sepIdx = rows.findIndex(r => r[0] === '__SEP__');
|
|
160
|
-
const dataRows = rows.filter(r => r[0] !== '__SEP__');
|
|
161
|
-
if (!dataRows.length)
|
|
162
|
-
return;
|
|
163
|
-
// Compute max visible width per column (capped at 36 to fit most terminals)
|
|
164
|
-
const colCount = Math.max(...dataRows.map(r => r.length));
|
|
165
|
-
const colWidths = Array.from({ length: colCount }, (_, ci) => Math.min(Math.max(...dataRows.map(r => visibleLen(r[ci] ?? '')), 3), 36));
|
|
166
|
-
const B = chalk.dim('│'); // border char
|
|
167
|
-
// separator: ──────┼──────┼────── (full width, closed on both ends)
|
|
168
|
-
const sep = ' ├' + colWidths.map(w => '─'.repeat(w + 2)).join('┼') + '┤';
|
|
169
|
-
console.log();
|
|
170
|
-
let dataIdx = 0;
|
|
171
|
-
rows.forEach(row => {
|
|
172
|
-
if (row[0] === '__SEP__') {
|
|
173
|
-
// separator line between header and body
|
|
174
|
-
console.log(chalk.dim(sep));
|
|
175
|
-
return;
|
|
176
|
-
}
|
|
177
|
-
const isHeader = sepIdx > 0 ? dataIdx === 0 : false;
|
|
178
|
-
dataIdx++;
|
|
179
|
-
const cells = Array.from({ length: colCount }, (_, ci) => {
|
|
180
|
-
const raw = row[ci] ?? '';
|
|
181
|
-
const styled = isHeader ? chalk.bold(raw) : raw;
|
|
182
|
-
return padVis(styled, colWidths[ci]);
|
|
183
|
-
});
|
|
184
|
-
console.log(' ' + B + ' ' + cells.join(' ' + B + ' ') + ' ' + B);
|
|
185
|
-
});
|
|
186
|
-
console.log();
|
|
187
|
-
}
|
|
188
|
-
// ── Inline markup ──────────────────────────────────────────────────────────
|
|
189
|
-
_inline(s) {
|
|
190
|
-
return s
|
|
191
|
-
.replace(/\*\*\*(.+?)\*\*\*/g, (_, t) => chalk.bold.italic(t))
|
|
192
|
-
.replace(/\*\*(.+?)\*\*/g, (_, t) => chalk.bold(t))
|
|
193
|
-
.replace(/\*(.+?)\*/g, (_, t) => chalk.italic(t))
|
|
194
|
-
.replace(/__(.+?)__/g, (_, t) => chalk.bold(t))
|
|
195
|
-
.replace(/_(.+?)_/g, (_, t) => chalk.italic(t))
|
|
196
|
-
.replace(/~~(.+?)~~/g, (_, t) => chalk.strikethrough(t))
|
|
197
|
-
.replace(/`([^`]+)`/g, (_, t) => chalk.bgBlack.whiteBright(` ${t} `));
|
|
198
|
-
}
|
|
199
|
-
// ── Per-line renderer ──────────────────────────────────────────────────────
|
|
200
|
-
_line(raw) {
|
|
201
|
-
// ── Fenced code block ──────────────────────────────────────────────
|
|
202
|
-
if (raw.startsWith('```')) {
|
|
203
|
-
this._flushTable();
|
|
204
|
-
if (!this._inCode) {
|
|
205
|
-
this._inCode = true;
|
|
206
|
-
this._codeLang = raw.slice(3).trim();
|
|
207
|
-
const label = this._codeLang ? chalk.dim(` ${this._codeLang}`) : '';
|
|
208
|
-
console.log('\n' + chalk.dim(' ┌' + '─'.repeat(ruleWidth(48))) + label);
|
|
209
|
-
}
|
|
210
|
-
else {
|
|
211
|
-
this._inCode = false;
|
|
212
|
-
console.log(chalk.dim(' └' + '─'.repeat(ruleWidth(48))) + '\n');
|
|
213
|
-
}
|
|
214
|
-
return;
|
|
215
|
-
}
|
|
216
|
-
if (this._inCode) {
|
|
217
|
-
console.log(chalk.dim(' │ ') + chalk.cyan(raw));
|
|
218
|
-
return;
|
|
219
|
-
}
|
|
220
|
-
// ── Table row — buffer, render once complete ───────────────────────
|
|
221
|
-
if (raw.includes('|') && raw.trim().startsWith('|')) {
|
|
222
|
-
const t = raw.trim();
|
|
223
|
-
if (/^\|[-| :]+\|$/.test(t)) {
|
|
224
|
-
this._tbl.push(['__SEP__']);
|
|
225
|
-
}
|
|
226
|
-
else {
|
|
227
|
-
this._tbl.push(t.replace(/^\||\|$/g, '').split('|').map(c => this._inline(c.trim())));
|
|
228
|
-
}
|
|
229
|
-
return;
|
|
230
|
-
}
|
|
231
|
-
// Non-table line → flush any buffered table rows first
|
|
232
|
-
this._flushTable();
|
|
233
|
-
// ── Horizontal rule ────────────────────────────────────────────────
|
|
234
|
-
if (/^[-*_]{3,}\s*$/.test(raw)) {
|
|
235
|
-
console.log('\n' + chalk.dim(' ' + '─'.repeat(ruleWidth(50))) + '\n');
|
|
236
|
-
return;
|
|
237
|
-
}
|
|
238
|
-
// ── Headers ───────────────────────────────────────────────────────
|
|
239
|
-
if (raw.startsWith('#### ')) {
|
|
240
|
-
console.log('\n' + chalk.bold(raw.slice(5)));
|
|
241
|
-
return;
|
|
242
|
-
}
|
|
243
|
-
if (raw.startsWith('### ')) {
|
|
244
|
-
console.log('\n' + chalk.bold.underline(raw.slice(4)));
|
|
245
|
-
return;
|
|
246
|
-
}
|
|
247
|
-
if (raw.startsWith('## ')) {
|
|
248
|
-
console.log('\n' + chalk.bold.cyan(raw.slice(3)));
|
|
249
|
-
return;
|
|
250
|
-
}
|
|
251
|
-
if (raw.startsWith('# ')) {
|
|
252
|
-
console.log('\n' + chalk.bold.blue(raw.slice(2)));
|
|
253
|
-
return;
|
|
254
|
-
}
|
|
255
|
-
// ── Blockquote ─────────────────────────────────────────────────────
|
|
256
|
-
if (raw.startsWith('> ')) {
|
|
257
|
-
console.log(chalk.dim(' ▎ ') + chalk.italic.dim(this._inline(raw.slice(2))));
|
|
258
|
-
return;
|
|
259
|
-
}
|
|
260
|
-
// ── Task list (before ul — more specific regex) ────────────────────
|
|
261
|
-
const taskM = /^(\s*)[-*+] \[([ xX])\] (.*)$/.exec(raw);
|
|
262
|
-
if (taskM) {
|
|
263
|
-
const done = taskM[2].trim() !== '';
|
|
264
|
-
console.log(` ${taskM[1]}${done ? chalk.green('✓') : chalk.dim('○')} ` +
|
|
265
|
-
`${done ? chalk.dim(taskM[3]) : this._inline(taskM[3])}`);
|
|
266
|
-
return;
|
|
267
|
-
}
|
|
268
|
-
// ── Unordered list ─────────────────────────────────────────────────
|
|
269
|
-
const ulM = /^(\s*)[-*+] (.*)$/.exec(raw);
|
|
270
|
-
if (ulM) {
|
|
271
|
-
const bullet = ulM[1].length > 0 ? chalk.dim('◦') : chalk.cyan('•');
|
|
272
|
-
console.log(` ${ulM[1]}${bullet} ${this._inline(ulM[2])}`);
|
|
273
|
-
return;
|
|
274
|
-
}
|
|
275
|
-
// ── Ordered list ───────────────────────────────────────────────────
|
|
276
|
-
const olM = /^(\s*)(\d+)\. (.*)$/.exec(raw);
|
|
277
|
-
if (olM) {
|
|
278
|
-
console.log(` ${olM[1]}${chalk.cyan(olM[2] + '.')} ${this._inline(olM[3])}`);
|
|
279
|
-
return;
|
|
280
|
-
}
|
|
281
|
-
// ── Empty line ─────────────────────────────────────────────────────
|
|
282
|
-
if (!raw.trim()) {
|
|
283
|
-
console.log();
|
|
284
|
-
return;
|
|
285
|
-
}
|
|
286
|
-
// ── Normal paragraph ───────────────────────────────────────────────
|
|
287
|
-
console.log(this._inline(raw));
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
// ─── Tool Formatting ──────────────────────────────────────────────────────────
|
|
291
|
-
export function formatToolUse(name, input) {
|
|
292
|
-
const primaryArg = input.path ??
|
|
293
|
-
input.file_path ??
|
|
294
|
-
input.command ??
|
|
295
|
-
input.pattern ??
|
|
296
|
-
input.url ??
|
|
297
|
-
input.query ??
|
|
298
|
-
Object.values(input).find(v => typeof v === 'string') ??
|
|
299
|
-
'';
|
|
300
|
-
const argDisplay = primaryArg
|
|
301
|
-
? chalk.dim(' ') + chalk.white(String(primaryArg).replace(/\n/g, '↵').slice(0, 80))
|
|
302
|
-
: '';
|
|
303
|
-
return chalk.yellow(icons.tool) + ' ' + chalk.bold(name) + argDisplay;
|
|
304
|
-
}
|
|
305
|
-
// ─── Live Tool Stream (bash progress) ─────────────────────────────────────────
|
|
306
|
-
//
|
|
307
|
-
// Bash commands used to go silent (spinner only) from onToolUse until the whole
|
|
308
|
-
// command finished — a `npm install` or long test run gave the user no signal
|
|
309
|
-
// beyond "running bash…" for however long it took. This buffers raw stdout/
|
|
310
|
-
// stderr chunks and prints each COMPLETE line as it arrives, dimmed and
|
|
311
|
-
// indented under a guide bar, so the terminal shows real progress. Partial
|
|
312
|
-
// (no-newline-yet) lines are held back so a slow-to-flush line isn't printed
|
|
313
|
-
// half-formed and then reprinted; call `flush()` at tool-result time to emit
|
|
314
|
-
// whatever partial tail remains.
|
|
315
|
-
export class ToolStreamPrinter {
|
|
316
|
-
carry = '';
|
|
317
|
-
linesShown = 0;
|
|
318
|
-
// Cap visible lines per command so a runaway/very chatty process (e.g. a
|
|
319
|
-
// build tool re-printing a progress bar thousands of times) doesn't flood
|
|
320
|
-
// the terminal — the full output is still available afterward via
|
|
321
|
-
// formatToolResult's summary and, for large output, the spill file.
|
|
322
|
-
static MAX_LINES = 200;
|
|
323
|
-
capped = false;
|
|
324
|
-
feed(chunk) {
|
|
325
|
-
if (this.capped)
|
|
326
|
-
return;
|
|
327
|
-
this.carry += chunk;
|
|
328
|
-
const lines = this.carry.split('\n');
|
|
329
|
-
this.carry = lines.pop() ?? '';
|
|
330
|
-
for (const line of lines) {
|
|
331
|
-
if (!this.printLine(line))
|
|
332
|
-
return;
|
|
333
|
-
}
|
|
334
|
-
}
|
|
335
|
-
printLine(line) {
|
|
336
|
-
if (this.linesShown >= ToolStreamPrinter.MAX_LINES) {
|
|
337
|
-
if (!this.capped) {
|
|
338
|
-
this.capped = true;
|
|
339
|
-
console.log(chalk.dim(' │ … output continues (full log in the result above once finished)'));
|
|
340
|
-
}
|
|
341
|
-
return false;
|
|
342
|
-
}
|
|
343
|
-
this.linesShown++;
|
|
344
|
-
console.log(chalk.dim(' │ ') + chalk.gray(line.slice(0, 300)));
|
|
345
|
-
return true;
|
|
346
|
-
}
|
|
347
|
-
flush() {
|
|
348
|
-
if (!this.capped && this.carry)
|
|
349
|
-
this.printLine(this.carry);
|
|
350
|
-
this.carry = '';
|
|
351
|
-
}
|
|
352
|
-
reset() {
|
|
353
|
-
this.carry = '';
|
|
354
|
-
this.linesShown = 0;
|
|
355
|
-
this.capped = false;
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
|
-
export function formatToolResult(name, result, durationMs) {
|
|
359
|
-
if (result.error) {
|
|
360
|
-
return chalk.red(icons.cross) + ' ' + chalk.dim(name) +
|
|
361
|
-
chalk.dim(' · ') + chalk.red('error') + chalk.dim(` · ${durationMs}ms`);
|
|
362
|
-
}
|
|
363
|
-
const out = result.output ?? '';
|
|
364
|
-
const lines = out.split('\n').length;
|
|
365
|
-
const sizeHint = lines > 1 ? `${lines} lines` : `${out.length} chars`;
|
|
366
|
-
return chalk.green(icons.check) + ' ' + chalk.dim(name) +
|
|
367
|
-
chalk.dim(` · ${sizeHint}`) + chalk.dim(` · ${durationMs}ms`);
|
|
368
|
-
}
|
|
369
|
-
// ─── Thinking Block ───────────────────────────────────────────────────────────
|
|
370
|
-
export function formatThinking(text, tokens) {
|
|
371
|
-
const tokStr = tokens >= 1000
|
|
372
|
-
? `~${(tokens / 1000).toFixed(1)}k tokens`
|
|
373
|
-
: tokens > 0 ? `~${tokens} tokens` : '';
|
|
374
|
-
const header = chalk.dim(`${icons.thinking} Thought`) + (tokStr ? chalk.dim(` for ${tokStr}`) : '');
|
|
375
|
-
const preview = text.trim().split('\n').filter(Boolean).slice(0, 3)
|
|
376
|
-
.map(l => chalk.dim(' ' + l.slice(0, 100))).join('\n');
|
|
377
|
-
return '\n' + header + (preview ? '\n' + preview : '') + '\n';
|
|
378
|
-
}
|
|
379
|
-
// ─── Usage — output tokens only ───────────────────────────────────────────────
|
|
380
|
-
export function formatUsage(outputTokens) {
|
|
381
|
-
const tok = outputTokens >= 1000
|
|
382
|
-
? `${(outputTokens / 1000).toFixed(1)}k`
|
|
383
|
-
: String(outputTokens);
|
|
384
|
-
return chalk.dim(`↓ ${tok} tokens`);
|
|
385
|
-
}
|
package/dist/ui/trustPrompt.js
DELETED
|
@@ -1,126 +0,0 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
import { render, Box, Text, useInput, useApp } from 'ink';
|
|
3
|
-
const CHOICES = ['Yes, I trust this folder', 'No, exit'];
|
|
4
|
-
function TrustPrompt({ workDir, signals, onDecide }) {
|
|
5
|
-
const { exit } = useApp();
|
|
6
|
-
const [index, setIndex] = React.useState(0);
|
|
7
|
-
// The selected index is read inside the input handler, which closes over
|
|
8
|
-
// state from the render it was created in. Enter pressed within the same
|
|
9
|
-
// frame as an arrow key would otherwise act on the PREVIOUS selection — the
|
|
10
|
-
// same stale-closure class of bug that made fast Vietnamese input submit an
|
|
11
|
-
// empty line (see inkTerminal.tsx's lineRef). Keep a ref in sync and read
|
|
12
|
-
// that instead.
|
|
13
|
-
const indexRef = React.useRef(0);
|
|
14
|
-
const setSelection = (next) => {
|
|
15
|
-
indexRef.current = next;
|
|
16
|
-
setIndex(next);
|
|
17
|
-
};
|
|
18
|
-
const decide = (trusted) => {
|
|
19
|
-
onDecide(trusted);
|
|
20
|
-
exit();
|
|
21
|
-
};
|
|
22
|
-
useInput((input, key) => {
|
|
23
|
-
if (key.upArrow) {
|
|
24
|
-
setSelection((indexRef.current + CHOICES.length - 1) % CHOICES.length);
|
|
25
|
-
return;
|
|
26
|
-
}
|
|
27
|
-
if (key.downArrow) {
|
|
28
|
-
setSelection((indexRef.current + 1) % CHOICES.length);
|
|
29
|
-
return;
|
|
30
|
-
}
|
|
31
|
-
// Number keys select directly, matching the visible "1." / "2." labels.
|
|
32
|
-
if (input === '1') {
|
|
33
|
-
setSelection(0);
|
|
34
|
-
decide(true);
|
|
35
|
-
return;
|
|
36
|
-
}
|
|
37
|
-
if (input === '2') {
|
|
38
|
-
setSelection(1);
|
|
39
|
-
decide(false);
|
|
40
|
-
return;
|
|
41
|
-
}
|
|
42
|
-
// Escape and Ctrl+C are treated as "do not trust". Declining is the safe
|
|
43
|
-
// default, so any way of backing out must land there rather than
|
|
44
|
-
// proceeding.
|
|
45
|
-
if (key.escape || (key.ctrl && input === 'c')) {
|
|
46
|
-
decide(false);
|
|
47
|
-
return;
|
|
48
|
-
}
|
|
49
|
-
// Enter is delivered as `return` on some terminals and as `\n` on those
|
|
50
|
-
// with ICRNL translating CR at the tty layer — Ink surfaces the latter as
|
|
51
|
-
// a distinct key with no `key.return` set, so checking `key.return` alone
|
|
52
|
-
// silently misses the most common delivery (see inkTerminal.tsx).
|
|
53
|
-
//
|
|
54
|
-
// Checking `input === '\r' || input === '\n'` (an EXACT match, as this
|
|
55
|
-
// used to) misses a real, reproducible case: Ink's `useInput` docs note
|
|
56
|
-
// that "if the user pastes text and it's more than one character, the
|
|
57
|
-
// callback will be called only once, and the whole string will be
|
|
58
|
-
// passed as `input`" — and the same applies to ordinary fast typing, not
|
|
59
|
-
// just clipboard pastes. Confirmed directly with a pty test: pressing
|
|
60
|
-
// Enter and immediately typing text right after (well within human
|
|
61
|
-
// typing speed — e.g. confirming trust, then typing the first message
|
|
62
|
-
// before the next screen has even drawn) can arrive as ONE input chunk
|
|
63
|
-
// like `"\nverifyfix"`. The exact match above never fires on that chunk,
|
|
64
|
-
// `decide()` is never called, and the prompt silently sits there having
|
|
65
|
-
// ignored the Enter that was actually pressed — until something else
|
|
66
|
-
// (Ctrl+C, which decides NOT to trust) ends it. That is a real
|
|
67
|
-
// security-relevant regression: a user who clearly chose "trust" can
|
|
68
|
-
// end up on the "do not trust" path instead, or the session can appear
|
|
69
|
-
// to hang.
|
|
70
|
-
//
|
|
71
|
-
// Scanning for an embedded \r/\n anywhere in `input` (mirroring
|
|
72
|
-
// inkTerminal.tsx's own newline-detection for exactly this class of
|
|
73
|
-
// fast-typing chunk) fixes it: the Enter is honoured as soon as it's
|
|
74
|
-
// found, regardless of what was typed immediately after it in the same
|
|
75
|
-
// chunk. Anything after the newline is simply ignored here — the trust
|
|
76
|
-
// prompt exits right after deciding, so there is no "rest of the line"
|
|
77
|
-
// to carry forward the way inkTerminal.tsx's main input box has to.
|
|
78
|
-
if (key.return || /[\r\n]/.test(input)) {
|
|
79
|
-
decide(indexRef.current === 0);
|
|
80
|
-
}
|
|
81
|
-
});
|
|
82
|
-
return (React.createElement(Box, { flexDirection: "column" },
|
|
83
|
-
React.createElement(Box, null,
|
|
84
|
-
React.createElement(Text, { bold: true, color: "yellow" }, "Accessing workspace:")),
|
|
85
|
-
React.createElement(Box, { marginTop: 1 },
|
|
86
|
-
React.createElement(Text, { bold: true }, workDir)),
|
|
87
|
-
React.createElement(Box, { marginTop: 1, flexDirection: "column" },
|
|
88
|
-
React.createElement(Text, null, "Quick safety check: is this a project you created, or one you trust?"),
|
|
89
|
-
React.createElement(Text, null, "Nexrall Code will be able to read, edit and execute files here.")),
|
|
90
|
-
signals.length > 0 && (React.createElement(Box, { marginTop: 1, flexDirection: "column" },
|
|
91
|
-
React.createElement(Text, { dimColor: true }, "This folder also configures the agent:"),
|
|
92
|
-
signals.map((s) => (React.createElement(Text, { key: s, dimColor: true }, ` • ${s}`))))),
|
|
93
|
-
React.createElement(Box, { marginTop: 1, flexDirection: "column" }, CHOICES.map((choice, i) => {
|
|
94
|
-
const selected = i === index;
|
|
95
|
-
return (React.createElement(Text, { key: choice, color: selected ? 'blue' : undefined },
|
|
96
|
-
selected ? '❯ ' : ' ',
|
|
97
|
-
`${i + 1}. ${choice}`));
|
|
98
|
-
})),
|
|
99
|
-
React.createElement(Box, { marginTop: 1 },
|
|
100
|
-
React.createElement(Text, { dimColor: true }, "Enter to confirm \u00B7 Esc to cancel"))));
|
|
101
|
-
}
|
|
102
|
-
/**
|
|
103
|
-
* Renders the trust prompt and resolves with the user's decision.
|
|
104
|
-
*
|
|
105
|
-
* This mounts its own short-lived Ink app and unmounts it before returning:
|
|
106
|
-
* the prompt has to run BEFORE the main session UI starts (that's the whole
|
|
107
|
-
* point — nothing from the folder may be loaded until the user decides), and
|
|
108
|
-
* Ink does not support two concurrent renders sharing one stdout.
|
|
109
|
-
*/
|
|
110
|
-
export function askForTrust(workDir, signals) {
|
|
111
|
-
return new Promise((resolve) => {
|
|
112
|
-
let decided = false;
|
|
113
|
-
const settle = (trusted) => {
|
|
114
|
-
if (decided)
|
|
115
|
-
return;
|
|
116
|
-
decided = true;
|
|
117
|
-
resolve(trusted);
|
|
118
|
-
};
|
|
119
|
-
const app = render(React.createElement(TrustPrompt, { workDir: workDir, signals: signals, onDecide: settle }), { exitOnCtrlC: false });
|
|
120
|
-
void app.waitUntilExit().then(() => {
|
|
121
|
-
// If the app exited without an explicit choice (terminal closed, stdin
|
|
122
|
-
// ended), fall back to NOT trusting.
|
|
123
|
-
settle(false);
|
|
124
|
-
});
|
|
125
|
-
});
|
|
126
|
-
}
|