nexrall-code 0.5.107 → 0.5.109

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.
@@ -0,0 +1,385 @@
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
+ }
@@ -0,0 +1,126 @@
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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nexrall-code",
3
- "version": "0.5.107",
3
+ "version": "0.5.109",
4
4
  "description": "Nexrall Code — AI coding assistant for your terminal (headless agent for scripts, CI and automation)",
5
5
  "keywords": [
6
6
  "ai",