koneck 2.128.33 → 2.128.35
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/engine.d.ts +19 -0
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +30 -3
- package/dist/engine.js.map +1 -1
- package/dist/highlight-spans.d.ts +0 -6
- package/dist/highlight-spans.d.ts.map +1 -1
- package/dist/highlight-spans.js +30 -0
- package/dist/highlight-spans.js.map +1 -1
- package/dist/ink-chat.d.ts +19 -0
- package/dist/ink-chat.d.ts.map +1 -1
- package/dist/ink-chat.js +299 -46
- package/dist/ink-chat.js.map +1 -1
- package/dist/plan-file.d.ts +67 -0
- package/dist/plan-file.d.ts.map +1 -0
- package/dist/plan-file.js +95 -0
- package/dist/plan-file.js.map +1 -0
- package/dist/types.d.ts +2 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/ink-chat.js
CHANGED
|
@@ -2,6 +2,7 @@ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-run
|
|
|
2
2
|
import React, { useState, useEffect, useRef } from 'react';
|
|
3
3
|
import { Box, Static, Text, render, useApp, useInput, useStdin } from 'ink';
|
|
4
4
|
import { execa } from 'execa';
|
|
5
|
+
import chalk from 'chalk';
|
|
5
6
|
import { resolveAgentConcurrency, toolsUnsupportedNotice } from './engine-facts.js';
|
|
6
7
|
import { withSynchronizedFrames, probeSynchronizedOutput } from './frame-sync.js';
|
|
7
8
|
import { pacedInterval, startingInterval, floorFor, shouldDrawFrame } from './frame-pace.js';
|
|
@@ -48,6 +49,7 @@ import { startBus, listSessions as listBusSessions, resolveTarget, deliver, } fr
|
|
|
48
49
|
import { MODES as MODE_SPECS, modeSpec, decide, loadRules, saveRules, addRule, commandPrefixSuggestion, describeRule, ruleMatches, toolRisk, isReadOnlyCommand, } from './permissions.js';
|
|
49
50
|
import { AskQueue } from './ask-queue.js';
|
|
50
51
|
import { planSummary } from './plan.js';
|
|
52
|
+
import { savePlan, plansDir, PLAN_CHOICES, PROCEED_PROMPT } from './plan-file.js';
|
|
51
53
|
import { logoFor } from './logo.js';
|
|
52
54
|
import { describeActivity } from './activity.js';
|
|
53
55
|
import { redactSecrets, containsSecret } from './redact.js';
|
|
@@ -502,6 +504,41 @@ export const WORD_HOLD_MS = 60_000;
|
|
|
502
504
|
* appears frozen for seven seconds is worse than either.
|
|
503
505
|
*/
|
|
504
506
|
export const HEARTBEAT_MS = 1_000;
|
|
507
|
+
/**
|
|
508
|
+
* One row of the fork tree: the task on the left, what it has cost on the right.
|
|
509
|
+
*
|
|
510
|
+
* Right-aligning the cost is the whole point of doing this by hand. Laid out as flex siblings the
|
|
511
|
+
* numbers sat wherever the task text left them, so a column of forks had its timings in a ragged
|
|
512
|
+
* diagonal and could not be read down. Composed into one string they line up at the right edge —
|
|
513
|
+
* and it is one node per row rather than five, which is what a live list of eight agents can
|
|
514
|
+
* afford to repaint.
|
|
515
|
+
*
|
|
516
|
+
* The task gives way first: it is the part a reader can still recognise from its opening words,
|
|
517
|
+
* while a truncated token count is worth nothing at all.
|
|
518
|
+
*/
|
|
519
|
+
/** The width of what precedes the task on a fork row: a status glyph, a space, and "fork ". */
|
|
520
|
+
export const FORK_LEAD = 8;
|
|
521
|
+
export function forkRow(task, meta, width, indent = FORK_LEAD) {
|
|
522
|
+
/*
|
|
523
|
+
* Nothing here may add up to more than the width it was given.
|
|
524
|
+
*
|
|
525
|
+
* The first version floored the task at eight columns and the gap at one, which on a narrow
|
|
526
|
+
* terminal produced a row a character longer than the space it had. An overflowing row is not a
|
|
527
|
+
* cosmetic problem in this interface — it is how the live region grows past the window, and a
|
|
528
|
+
* live region past the window is what makes Ink wipe the whole terminal on every render.
|
|
529
|
+
*
|
|
530
|
+
* So the budget is spent in order of what is worth keeping: the cost first, because a truncated
|
|
531
|
+
* token count says nothing, then whatever is left goes to the task, which stays recognisable
|
|
532
|
+
* from its opening words. Where there is not even room for the cost, it is cut too — a row that
|
|
533
|
+
* fits and says less beats a row that says more and breaks the screen.
|
|
534
|
+
*/
|
|
535
|
+
const avail = Math.max(0, width - indent);
|
|
536
|
+
const shownMeta = meta.length <= avail ? meta : meta.slice(0, avail);
|
|
537
|
+
const room = Math.max(0, avail - shownMeta.length - 1);
|
|
538
|
+
const shownTask = room > 0 ? fitCells(task, room, true) : '';
|
|
539
|
+
const gap = ' '.repeat(Math.max(0, avail - displayWidth(shownTask) - shownMeta.length));
|
|
540
|
+
return { task: shownTask, gap, meta: shownMeta };
|
|
541
|
+
}
|
|
505
542
|
/**
|
|
506
543
|
* The cursor, drawn as a painted cell rather than as a character.
|
|
507
544
|
*
|
|
@@ -635,6 +672,7 @@ const COMMANDS = [
|
|
|
635
672
|
{ cmd: '/thinking', desc: 'Show or hide the model\'s reasoning as it arrives' },
|
|
636
673
|
{ cmd: '/activity', desc: 'What the model has done this session, and what it is on now' },
|
|
637
674
|
{ cmd: '/trajectory', desc: 'Where the time went: per-request timings, tool durations, waits' },
|
|
675
|
+
{ cmd: '/compact', desc: 'Summarise the conversation so far to free up context' },
|
|
638
676
|
{ cmd: '/update', desc: 'Check for a new version and update' },
|
|
639
677
|
{ cmd: '/name', desc: 'Name this session, so /resume can find it by name' },
|
|
640
678
|
{ cmd: '/fork', desc: 'Copy this session and continue on the copy' },
|
|
@@ -1225,6 +1263,7 @@ Type / on an empty line to browse every command; keep typing to filter.
|
|
|
1225
1263
|
|
|
1226
1264
|
Slash commands:
|
|
1227
1265
|
/help Show this help
|
|
1266
|
+
/compact Summarise the conversation so far to free up context
|
|
1228
1267
|
/update Updates and a key guide, in a side panel
|
|
1229
1268
|
/models [filter] Browse models from the connected provider and switch
|
|
1230
1269
|
/connect [provider] Connect a provider (picker when no name given)
|
|
@@ -1422,16 +1461,42 @@ export function initialAltScreen(cfg, compositesRedraws, stdout = process.stdout
|
|
|
1422
1461
|
* to the full width first so the tint is a solid block, and the padding is emitted as its own
|
|
1423
1462
|
* span so it takes the background without taking a foreground colour.
|
|
1424
1463
|
*/
|
|
1464
|
+
/**
|
|
1465
|
+
* One diff line, coloured, as a single string.
|
|
1466
|
+
*
|
|
1467
|
+
* It used to return one element per token, so a forty-row panel of ten-token lines handed Ink
|
|
1468
|
+
* several hundred nodes to render and lay out on every frame. Measured on a Raspberry Pi 5 with
|
|
1469
|
+
* two 200-line diffs: an open panel cost 281ms a repaint when the whole diff was built, 41ms once
|
|
1470
|
+
* only the visible rows were, and 32ms once each line was one node instead of a box of them —
|
|
1471
|
+
* against 1ms with the panel closed.
|
|
1472
|
+
*
|
|
1473
|
+
* A string is the end of that road: the colours are written into it with the same escapes Ink
|
|
1474
|
+
* would have emitted, and the line becomes a single Text. It is also then cacheable, which is
|
|
1475
|
+
* what makes a still panel free rather than merely cheap — the text of a finished edit does not
|
|
1476
|
+
* change, so it is coloured once and read back on every frame after.
|
|
1477
|
+
*/
|
|
1478
|
+
const CODE_LINE_CACHE = new Map();
|
|
1479
|
+
const CODE_LINE_CACHE_MAX = 4000;
|
|
1425
1480
|
function renderCodeLine(text, kind, width, lang) {
|
|
1481
|
+
const key = `${kind}|${width}|${lang}|${text}`;
|
|
1482
|
+
const hit = CODE_LINE_CACHE.get(key);
|
|
1483
|
+
if (hit !== undefined)
|
|
1484
|
+
return hit;
|
|
1426
1485
|
const bg = kind === 'add' ? DIFF_ADD_BG : kind === 'del' ? DIFF_DEL_BG : undefined;
|
|
1486
|
+
const paint = bg ? chalk.bgHex(bg) : chalk;
|
|
1427
1487
|
const body = text.replace(/\t/g, ' ');
|
|
1428
1488
|
const cell = fitCells(body, width, true);
|
|
1429
1489
|
const spans = tokenizeLine(cell.replace(/\s+$/, ''), lang);
|
|
1430
1490
|
const drawn = spans.map(sp => sp.text).join('');
|
|
1431
1491
|
const pad = cell.length > drawn.length ? cell.slice(drawn.length) : '';
|
|
1432
|
-
const out = spans.map(
|
|
1433
|
-
|
|
1434
|
-
|
|
1492
|
+
const out = spans.map(sp => paint.hex(SYNTAX[sp.kind])(sp.text)).join('')
|
|
1493
|
+
+ (pad ? paint(pad) : '');
|
|
1494
|
+
if (CODE_LINE_CACHE.size >= CODE_LINE_CACHE_MAX) {
|
|
1495
|
+
const oldest = CODE_LINE_CACHE.keys().next().value;
|
|
1496
|
+
if (oldest !== undefined)
|
|
1497
|
+
CODE_LINE_CACHE.delete(oldest);
|
|
1498
|
+
}
|
|
1499
|
+
CODE_LINE_CACHE.set(key, out);
|
|
1435
1500
|
return out;
|
|
1436
1501
|
}
|
|
1437
1502
|
/**
|
|
@@ -1450,10 +1515,7 @@ export function DiffPanel({ files, width, height, activity, activityRows = 0, sc
|
|
|
1450
1515
|
// empty state was a single easy-to-miss line and looked as if `/diff` had done nothing.
|
|
1451
1516
|
const inner = Math.max(20, width - 4);
|
|
1452
1517
|
const bodyHeight = Math.max(1, height - 4 - activityRows);
|
|
1453
|
-
|
|
1454
|
-
// written is the thing on screen.
|
|
1455
|
-
const body = [];
|
|
1456
|
-
let used = 0;
|
|
1518
|
+
const rows = [];
|
|
1457
1519
|
let shownFiles = 0;
|
|
1458
1520
|
for (const d of [...files].reverse()) {
|
|
1459
1521
|
/*
|
|
@@ -1464,57 +1526,72 @@ export function DiffPanel({ files, width, height, activity, activityRows = 0, sc
|
|
|
1464
1526
|
* blank line as the only separation two files' diffs ran together and the filename headers had
|
|
1465
1527
|
* nothing to sit under. Drawn only between files, never above the first.
|
|
1466
1528
|
*/
|
|
1467
|
-
if (shownFiles > 0)
|
|
1468
|
-
|
|
1469
|
-
used += 1;
|
|
1470
|
-
}
|
|
1529
|
+
if (shownFiles > 0)
|
|
1530
|
+
rows.push({ t: 'rule', key: `r-${d.path}` });
|
|
1471
1531
|
const numbers = d.hunks.flatMap(h => h.lines.map(l => l.after ?? l.before ?? 0));
|
|
1472
1532
|
const gutter = Math.max(3, String(Math.max(0, ...numbers)).length);
|
|
1473
1533
|
const codeWidth = Math.max(12, inner - gutter - 2);
|
|
1474
1534
|
const lang = spanLangFor(d.path);
|
|
1475
1535
|
const tag = d.created ? 'new' : d.deleted ? 'deleted' : `+${d.added}${d.removed ? ` -${d.removed}` : ''}`;
|
|
1476
|
-
|
|
1477
|
-
used += 1;
|
|
1536
|
+
rows.push({ t: 'head', key: `h-${d.path}`, path: d.path, tag, deleted: d.deleted });
|
|
1478
1537
|
if (d.binary) {
|
|
1479
|
-
|
|
1480
|
-
used += 1;
|
|
1538
|
+
rows.push({ t: 'binary', key: `b-${d.path}` });
|
|
1481
1539
|
shownFiles++;
|
|
1482
1540
|
continue;
|
|
1483
1541
|
}
|
|
1484
1542
|
for (const [hi, hunk] of d.hunks.entries()) {
|
|
1485
|
-
if (hi > 0)
|
|
1486
|
-
|
|
1487
|
-
used += 1;
|
|
1488
|
-
}
|
|
1543
|
+
if (hi > 0)
|
|
1544
|
+
rows.push({ t: 'gap', key: `g-${d.path}-${hi}`, gutter });
|
|
1489
1545
|
for (const [li, l] of hunk.lines.entries()) {
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1546
|
+
rows.push({
|
|
1547
|
+
t: 'line', key: `l-${d.path}-${hi}-${li}`, gutter, codeWidth, lang,
|
|
1548
|
+
shown: (l.kind === 'del' ? l.before : l.after) ?? undefined,
|
|
1549
|
+
sign: l.kind === 'add' ? '+' : l.kind === 'del' ? '-' : ' ',
|
|
1550
|
+
kind: l.kind, text: l.text,
|
|
1551
|
+
});
|
|
1495
1552
|
}
|
|
1496
1553
|
}
|
|
1497
1554
|
if (d.hiddenLines > 0) {
|
|
1498
|
-
|
|
1499
|
-
used += 1;
|
|
1555
|
+
rows.push({ t: 'more', key: `m-${d.path}`, gutter, count: d.hiddenLines });
|
|
1500
1556
|
}
|
|
1501
1557
|
shownFiles++;
|
|
1502
|
-
|
|
1503
|
-
body.push(_jsx(Text, { children: " " }, `s-${d.path}`));
|
|
1558
|
+
rows.push({ t: 'spacer', key: `s-${d.path}` });
|
|
1504
1559
|
}
|
|
1505
|
-
|
|
1506
|
-
* Built in full, then windowed — rather than stopping at the height, which is what it used to
|
|
1507
|
-
* do. A panel that only ever builds what fits cannot be scrolled, because the rows below the
|
|
1508
|
-
* fold were never made.
|
|
1509
|
-
*/
|
|
1510
|
-
const maxScroll = Math.max(0, body.length - bodyHeight);
|
|
1560
|
+
const maxScroll = Math.max(0, rows.length - bodyHeight);
|
|
1511
1561
|
const from = Math.max(0, Math.min(scroll, maxScroll));
|
|
1512
|
-
const
|
|
1562
|
+
const body = rows.slice(from, from + bodyHeight).map(r => {
|
|
1563
|
+
switch (r.t) {
|
|
1564
|
+
case 'rule':
|
|
1565
|
+
return _jsx(Text, { color: DIM, children: '─'.repeat(inner) }, r.key);
|
|
1566
|
+
case 'head':
|
|
1567
|
+
return (_jsxs(Box, { justifyContent: "space-between", width: inner, children: [_jsx(Text, { color: CYAN, wrap: "truncate-start", children: r.path }), _jsxs(Text, { color: r.deleted ? DIFF_DEL : DIFF_ADD, children: [" ", r.tag] })] }, r.key));
|
|
1568
|
+
case 'binary':
|
|
1569
|
+
return _jsx(Text, { color: DIM, children: " binary \u2014 not shown" }, r.key);
|
|
1570
|
+
case 'gap':
|
|
1571
|
+
return _jsx(Text, { color: DIM, children: `${' '.repeat(r.gutter)} ⋯` }, r.key);
|
|
1572
|
+
case 'more':
|
|
1573
|
+
return _jsx(Text, { color: DIM, children: `${' '.repeat(r.gutter)} ⋯ ${r.count} more` }, r.key);
|
|
1574
|
+
case 'spacer':
|
|
1575
|
+
return _jsx(Text, { children: " " }, r.key);
|
|
1576
|
+
default:
|
|
1577
|
+
/*
|
|
1578
|
+
* One Text per line, with the pieces nested inside it, rather than a Box of siblings.
|
|
1579
|
+
*
|
|
1580
|
+
* A Box is a layout node: the engine measures and places it and every child. A Text with
|
|
1581
|
+
* Texts inside it is one node holding spans. With forty lines on screen and half a dozen
|
|
1582
|
+
* pieces each that is the difference between laying out two hundred and fifty nodes a
|
|
1583
|
+
* frame and laying out forty — and it is the same mistake that stranded the composer's
|
|
1584
|
+
* cursor on the first row, in a place where it costs time rather than correctness.
|
|
1585
|
+
*/
|
|
1586
|
+
return (_jsxs(Text, { wrap: "truncate", children: [_jsx(Text, { color: DIM, children: String(r.shown ?? '').padStart(r.gutter) }), _jsx(Text, { color: r.kind === 'add' ? DIFF_ADD : r.kind === 'del' ? DIFF_DEL : DIM, ...(r.kind !== 'ctx'
|
|
1587
|
+
? { backgroundColor: r.kind === 'add' ? DIFF_ADD_BG : DIFF_DEL_BG } : {}), children: r.sign }), renderCodeLine(r.text, r.kind, r.codeWidth, r.lang)] }, r.key));
|
|
1588
|
+
}
|
|
1589
|
+
});
|
|
1513
1590
|
const summary = files.length === 0
|
|
1514
1591
|
? 'no changes yet'
|
|
1515
1592
|
: `${files.length} file${files.length === 1 ? '' : 's'} changed`
|
|
1516
1593
|
+ (maxScroll > 0 ? ` ${from > 0 ? `↑${from}` : ''}${from < maxScroll ? ` ↓${maxScroll - from}` : ''}`.trimEnd() : '');
|
|
1517
|
-
return (_jsxs(Box, { flexDirection: "column", width: width, height: height, borderStyle: "round", borderColor: CYAN, paddingX: 1, flexShrink: 0, overflowY: "hidden", children: [_jsxs(Box, { justifyContent: "space-between", width: inner, children: [_jsxs(Box, { children: [_jsx(Text, { color: INK, bold: true, children: summary }), added > 0 && _jsxs(Text, { color: DIFF_ADD, children: [" +", added] }), removed > 0 && _jsxs(Text, { color: DIFF_DEL, children: [" -", removed] })] }), _jsx(Text, { color: DIM, children: "/diff" })] }), activity && _jsxs(_Fragment, { children: [_jsx(Text, { color: DIM, children: '─'.repeat(inner) }), _jsx(Box, { flexDirection: "column", height: Math.max(1, activityRows - 1), overflowY: "hidden", children: activity })] }), _jsx(Box, { flexDirection: "column", flexGrow: 1, flexShrink: 1, overflowY: "hidden", children:
|
|
1594
|
+
return (_jsxs(Box, { flexDirection: "column", width: width, height: height, borderStyle: "round", borderColor: CYAN, paddingX: 1, flexShrink: 0, overflowY: "hidden", children: [_jsxs(Box, { justifyContent: "space-between", width: inner, children: [_jsxs(Box, { children: [_jsx(Text, { color: INK, bold: true, children: summary }), added > 0 && _jsxs(Text, { color: DIFF_ADD, children: [" +", added] }), removed > 0 && _jsxs(Text, { color: DIFF_DEL, children: [" -", removed] })] }), _jsx(Text, { color: DIM, children: "/diff" })] }), activity && _jsxs(_Fragment, { children: [_jsx(Text, { color: DIM, children: '─'.repeat(inner) }), _jsx(Box, { flexDirection: "column", height: Math.max(1, activityRows - 1), overflowY: "hidden", children: activity })] }), _jsx(Box, { flexDirection: "column", flexGrow: 1, flexShrink: 1, overflowY: "hidden", children: body }), shownFiles < files.length && (_jsx(Text, { color: DIM, children: `… ${files.length - shownFiles} more file(s) changed` }))] }));
|
|
1518
1595
|
}
|
|
1519
1596
|
/**
|
|
1520
1597
|
* Exported for one reason: so a test can mount it.
|
|
@@ -1923,6 +2000,22 @@ export function App({ config: initialConfig, clearFrame, synchronizedFrames = fa
|
|
|
1923
2000
|
// run is the record of what the agent thought it was doing.
|
|
1924
2001
|
addRow({ role: 'plan', plan: next });
|
|
1925
2002
|
},
|
|
2003
|
+
/*
|
|
2004
|
+
* Compaction is announced, because it is not a detail.
|
|
2005
|
+
*
|
|
2006
|
+
* It already ran on its own when the window filled — `onCompress` has been in the config all
|
|
2007
|
+
* along and this interface never wired it, so the conversation was summarised and replaced
|
|
2008
|
+
* underneath somebody mid-task with nothing on screen to say so. The next answer would then
|
|
2009
|
+
* be missing something said an hour earlier and look like the model forgetting.
|
|
2010
|
+
*/
|
|
2011
|
+
onCompress: (before, after, limit) => {
|
|
2012
|
+
addSystem(`Context compacted — ${before} messages summarised into ${after}, to stay inside `
|
|
2013
|
+
+ `${limit.toLocaleString()} tokens. Earlier detail is now a summary; `
|
|
2014
|
+
+ '`/compact` forces this, and a new session starts clean.');
|
|
2015
|
+
},
|
|
2016
|
+
onCompactNoop: (reason) => {
|
|
2017
|
+
addSystem(`Nothing was compacted — ${reason}.`);
|
|
2018
|
+
},
|
|
1926
2019
|
onChunk: (text) => pushSay(text),
|
|
1927
2020
|
onToolCall: (name, args) => pushTool(name, args),
|
|
1928
2021
|
/*
|
|
@@ -2517,6 +2610,14 @@ export function App({ config: initialConfig, clearFrame, synchronizedFrames = fa
|
|
|
2517
2610
|
})();
|
|
2518
2611
|
}, [effort]);
|
|
2519
2612
|
const [showAnalytics, setShowAnalytics] = useState(false);
|
|
2613
|
+
/**
|
|
2614
|
+
* A finished plan, waiting to be answered.
|
|
2615
|
+
*
|
|
2616
|
+
* Plan mode used to end with the plan in the transcript and nothing else — you changed mode
|
|
2617
|
+
* yourself and asked again, from memory, for the thing you had just read. This is the handover
|
|
2618
|
+
* that was missing: the plan, where it was saved, and the obvious question.
|
|
2619
|
+
*/
|
|
2620
|
+
const [planAsk, setPlanAsk] = useState(null);
|
|
2520
2621
|
const [updatePane, setUpdatePane] = useState(null);
|
|
2521
2622
|
/**
|
|
2522
2623
|
* The version installed in the background, if one was; see auto-update.ts.
|
|
@@ -2561,6 +2662,26 @@ export function App({ config: initialConfig, clearFrame, synchronizedFrames = fa
|
|
|
2561
2662
|
// kept the width it booted with until restarted.
|
|
2562
2663
|
// `|| 80` not `?? 80`: a pty with no negotiated size reports 0 columns, and 0 would collapse
|
|
2563
2664
|
// the layout to a single character.
|
|
2665
|
+
/**
|
|
2666
|
+
* Bumped to make <Static> write the whole transcript out again.
|
|
2667
|
+
*
|
|
2668
|
+
* Ink renders a Static item once and never touches it after, which is exactly what makes the
|
|
2669
|
+
* transcript cheap — and also what leaves it wrapped to whatever the terminal was when it was
|
|
2670
|
+
* written. Open KONECK in a small window, maximise it, and every finished row keeps the narrow
|
|
2671
|
+
* shape it was born with while the composer and status bar spread to the new width. Reported
|
|
2672
|
+
* exactly that way.
|
|
2673
|
+
*
|
|
2674
|
+
* A new key is a new Static as far as Ink is concerned, so it renders all of its items afresh.
|
|
2675
|
+
* Paired with a clear, that re-flows the conversation to the width the window has now.
|
|
2676
|
+
*/
|
|
2677
|
+
const [staticEpoch, setStaticEpoch] = useState(0);
|
|
2678
|
+
/**
|
|
2679
|
+
* The redraw, in a ref.
|
|
2680
|
+
*
|
|
2681
|
+
* The resize listener is attached once and outlives every render, so it cannot close over a
|
|
2682
|
+
* function defined further down the component without holding the first one forever.
|
|
2683
|
+
*/
|
|
2684
|
+
const redrawScreenRef = useRef(null);
|
|
2564
2685
|
const [termCols, setTermCols] = useState(() => process.stdout.columns || 80);
|
|
2565
2686
|
const [termRows, setTermRows] = useState(() => process.stdout.rows || 24);
|
|
2566
2687
|
/** Optional user cap on the UI width. Undefined means fill the terminal. */
|
|
@@ -2618,8 +2739,21 @@ export function App({ config: initialConfig, clearFrame, synchronizedFrames = fa
|
|
|
2618
2739
|
}
|
|
2619
2740
|
useEffect(() => {
|
|
2620
2741
|
const onResize = () => {
|
|
2621
|
-
|
|
2742
|
+
const cols = process.stdout.columns || 80;
|
|
2622
2743
|
setTermRows(process.stdout.rows || 24);
|
|
2744
|
+
setTermCols(prev => {
|
|
2745
|
+
/*
|
|
2746
|
+
* A width change re-flows the conversation; a height change does not need to.
|
|
2747
|
+
*
|
|
2748
|
+
* Rows already committed keep the wrapping they were written with, so widening the window
|
|
2749
|
+
* leaves the whole transcript in a narrow column beside a full-width composer. Redrawing
|
|
2750
|
+
* costs one clear and one re-emission, and a resize is not something that happens during
|
|
2751
|
+
* a keystroke.
|
|
2752
|
+
*/
|
|
2753
|
+
if (prev !== cols)
|
|
2754
|
+
redrawScreenRef.current?.();
|
|
2755
|
+
return cols;
|
|
2756
|
+
});
|
|
2623
2757
|
};
|
|
2624
2758
|
process.stdout.on('resize', onResize);
|
|
2625
2759
|
return () => { process.stdout.off('resize', onResize); };
|
|
@@ -3074,6 +3208,7 @@ export function App({ config: initialConfig, clearFrame, synchronizedFrames = fa
|
|
|
3074
3208
|
* it at all. It wipes the screen and its scrollback; the conversation is untouched, since it
|
|
3075
3209
|
* lives in the session rather than in the terminal's memory of it.
|
|
3076
3210
|
*/
|
|
3211
|
+
redrawScreenRef.current = redrawScreen;
|
|
3077
3212
|
function redrawScreen() {
|
|
3078
3213
|
if (!process.stdout.isTTY)
|
|
3079
3214
|
return;
|
|
@@ -3085,6 +3220,9 @@ export function App({ config: initialConfig, clearFrame, synchronizedFrames = fa
|
|
|
3085
3220
|
// is still in the buffer, and leaving it there means it comes back on the next scroll — and
|
|
3086
3221
|
// home the cursor, which is where Ink will now draw.
|
|
3087
3222
|
process.stdout.write('\u001b[2J\u001b[3J\u001b[H');
|
|
3223
|
+
// The transcript has just been erased along with everything else, so it has to be written
|
|
3224
|
+
// again — and writing it again is also what re-flows it to the current width.
|
|
3225
|
+
setStaticEpoch(n => n + 1);
|
|
3088
3226
|
// Ink writes a frame only when its content differs from the last one, and the content has not
|
|
3089
3227
|
// changed — the screen it was drawn on has. Bumping this forces the write.
|
|
3090
3228
|
setRepaint(n => n + 1);
|
|
@@ -4578,6 +4716,26 @@ export function App({ config: initialConfig, clearFrame, synchronizedFrames = fa
|
|
|
4578
4716
|
+ '`/altscreen on` moves them into a pinned panel instead.');
|
|
4579
4717
|
return;
|
|
4580
4718
|
}
|
|
4719
|
+
case '/compact': {
|
|
4720
|
+
/*
|
|
4721
|
+
* Compacting on purpose, rather than waiting for the window to force it.
|
|
4722
|
+
*
|
|
4723
|
+
* The automatic kind exists to stop a request being rejected. This is the other reason:
|
|
4724
|
+
* a session about to change subject carries the whole of the last one into every turn to
|
|
4725
|
+
* come, and that is paid for on each request. Deferred to the next send because the
|
|
4726
|
+
* summary is itself a model call.
|
|
4727
|
+
*/
|
|
4728
|
+
const active = await getSession().catch(() => null);
|
|
4729
|
+
if (!active) {
|
|
4730
|
+
addSystem('Nothing to compact yet — no turn has run in this session.');
|
|
4731
|
+
return;
|
|
4732
|
+
}
|
|
4733
|
+
active.compactNow();
|
|
4734
|
+
addSystem('The conversation will be compacted before the next message, if there is '
|
|
4735
|
+
+ 'anything to summarise — it will say either way. What was said stays as a '
|
|
4736
|
+
+ 'summary; nothing is lost from the transcript above.');
|
|
4737
|
+
return;
|
|
4738
|
+
}
|
|
4581
4739
|
case '/changes': {
|
|
4582
4740
|
try {
|
|
4583
4741
|
const [status, unstaged, staged] = await Promise.all([
|
|
@@ -5510,6 +5668,48 @@ export function App({ config: initialConfig, clearFrame, synchronizedFrames = fa
|
|
|
5510
5668
|
answer(typed);
|
|
5511
5669
|
return;
|
|
5512
5670
|
}
|
|
5671
|
+
/*
|
|
5672
|
+
* A plan waiting for an answer owns the keyboard.
|
|
5673
|
+
*
|
|
5674
|
+
* Placed before the composer for the same reason the permission question is: an answer to a
|
|
5675
|
+
* question on screen should not also be typed into a draft behind it. Escape puts it away
|
|
5676
|
+
* without choosing, which leaves the plan in the transcript and the mode alone.
|
|
5677
|
+
*/
|
|
5678
|
+
if (planAsk) {
|
|
5679
|
+
if (key.escape) {
|
|
5680
|
+
setPlanAsk(null);
|
|
5681
|
+
addSystem('Left in plan mode. The plan is above'
|
|
5682
|
+
+ (planAsk.file ? ` and saved to \`${planAsk.file}\`` : '')
|
|
5683
|
+
+ ' — say what to change, or shift+tab to a mode that can act on it.');
|
|
5684
|
+
return;
|
|
5685
|
+
}
|
|
5686
|
+
if (key.upArrow) {
|
|
5687
|
+
setPlanAsk(p => (p ? { ...p, choice: Math.max(0, p.choice - 1) } : p));
|
|
5688
|
+
return;
|
|
5689
|
+
}
|
|
5690
|
+
if (key.downArrow) {
|
|
5691
|
+
setPlanAsk(p => (p ? { ...p, choice: Math.min(PLAN_CHOICES.length - 1, p.choice + 1) } : p));
|
|
5692
|
+
return;
|
|
5693
|
+
}
|
|
5694
|
+
const typed = PLAN_CHOICES.findIndex(c => c.key === input);
|
|
5695
|
+
const index = typed >= 0 ? typed : key.return ? planAsk.choice : -1;
|
|
5696
|
+
if (index < 0)
|
|
5697
|
+
return;
|
|
5698
|
+
const chosen = PLAN_CHOICES[index];
|
|
5699
|
+
setPlanAsk(null);
|
|
5700
|
+
if (chosen.mode === null) {
|
|
5701
|
+
addSystem('Tell it what to change, and it will plan again.');
|
|
5702
|
+
return;
|
|
5703
|
+
}
|
|
5704
|
+
setMode(chosen.mode);
|
|
5705
|
+
modeRef.current = chosen.mode;
|
|
5706
|
+
addSystem(`Carrying out the plan in **${modeSpec(chosen.mode).label}** mode.`
|
|
5707
|
+
+ (planAsk.file ? `\nSaved to \`${planAsk.file}\`` : ''));
|
|
5708
|
+
// The mode has to be in force before the turn starts, which is why it is set on the ref as
|
|
5709
|
+
// well as in state — the turn reads the ref, and state has not settled by the time it does.
|
|
5710
|
+
setTimeout(() => submitDraft(PROCEED_PROMPT), 0);
|
|
5711
|
+
return;
|
|
5712
|
+
}
|
|
5513
5713
|
if (addProv) {
|
|
5514
5714
|
if (key.escape) {
|
|
5515
5715
|
setAddProv(null);
|
|
@@ -6144,7 +6344,16 @@ export function App({ config: initialConfig, clearFrame, synchronizedFrames = fa
|
|
|
6144
6344
|
const active = await getSession();
|
|
6145
6345
|
const persistentGoal = goalRef.current;
|
|
6146
6346
|
const prefix = (persistentGoal ? `[Persistent session goal: ${persistentGoal}]\n` : '') +
|
|
6147
|
-
|
|
6347
|
+
/*
|
|
6348
|
+
* The ref, not the state.
|
|
6349
|
+
*
|
|
6350
|
+
* This closure is made when the turn is submitted, and a mode chosen a moment earlier
|
|
6351
|
+
* is not in it — React has not re-rendered yet. Approving a plan set the mode to auto
|
|
6352
|
+
* and then sent the request with plan mode's prefix still attached: "you cannot write
|
|
6353
|
+
* files or run commands in this mode", to a model that had just been told to carry
|
|
6354
|
+
* the plan out. The permission checks already read the ref for exactly this reason.
|
|
6355
|
+
*/
|
|
6356
|
+
modeSpec(modeRef.current).prefix +
|
|
6148
6357
|
(effort !== 'medium' ? `[Effort: ${effort}]\n` : '');
|
|
6149
6358
|
const controller = new AbortController();
|
|
6150
6359
|
abortRef.current = controller;
|
|
@@ -6227,6 +6436,22 @@ export function App({ config: initialConfig, clearFrame, synchronizedFrames = fa
|
|
|
6227
6436
|
addSystem(incompleteRef.current);
|
|
6228
6437
|
incompleteRef.current = null;
|
|
6229
6438
|
}
|
|
6439
|
+
/*
|
|
6440
|
+
* A plan-mode turn ends with a question, not with silence.
|
|
6441
|
+
*
|
|
6442
|
+
* Only in plan mode, and only when something was actually written: the mode denies
|
|
6443
|
+
* writes and commands, so whatever came back is a proposal by construction. Saved
|
|
6444
|
+
* first, so the offer can name the file it is offering.
|
|
6445
|
+
*/
|
|
6446
|
+
if (modeRef.current === 'plan' && content.trim() !== '') {
|
|
6447
|
+
// Under the session's state home, not always the real one: a run pointed at a
|
|
6448
|
+
// different directory should not write into the home directory behind it.
|
|
6449
|
+
const saved = await savePlan(content, {
|
|
6450
|
+
goal: goalRef.current, at: Date.now(),
|
|
6451
|
+
...(cfg.stateHome ? { dir: plansDir(cfg.stateHome) } : {}),
|
|
6452
|
+
});
|
|
6453
|
+
setPlanAsk({ text: content, file: 'path' in saved ? saved.path : null, choice: 0 });
|
|
6454
|
+
}
|
|
6230
6455
|
setAgentState('completed');
|
|
6231
6456
|
setTimeout(() => setAgentState('ready'), 700);
|
|
6232
6457
|
}
|
|
@@ -6568,23 +6793,34 @@ export function App({ config: initialConfig, clearFrame, synchronizedFrames = fa
|
|
|
6568
6793
|
//
|
|
6569
6794
|
// Both halves matter. Clipping the bottom would hide what just happened; anchoring
|
|
6570
6795
|
// always to the bottom is what put the banner there.
|
|
6571
|
-
_jsx(Box, { flexGrow: 0, flexShrink: 1, flexDirection: "column", justifyContent: "flex-end", overflowY: "hidden", children: visibleRows.map((row, i) => (_jsx(Box, { flexDirection: "column", flexShrink: 0, children: renderRowCached(row, i) }, i))) })), altScreen && !ask && scrollBack > 0 && (_jsx(Text, { color: AMBER, children: `${G.caret} scrolled back ${scrollBack} rows — Ctrl+End follows live · Ctrl+Up/Down moves by section` })), showAnalytics && (_jsxs(Box, { borderStyle: "round", borderColor: AMBER, paddingX: 1, marginBottom: 1, flexDirection: "column", width: barWidth, children: [_jsx(Text, { color: AMBER, bold: true, children: "\u25C6 Session Analytics" }), _jsxs(Text, { color: MUTED, children: ["Turns : ", _jsx(Text, { color: INK, children: stats.turns })] }), _jsxs(Text, { color: MUTED, children: ["Prompt tok : ", _jsx(Text, { color: INK, children: stats.promptTokens.toLocaleString() })] }), _jsxs(Text, { color: MUTED, children: ["Output tok : ", _jsx(Text, { color: INK, children: stats.completionTokens.toLocaleString() })] }), _jsxs(Text, { color: MUTED, children: ["Total tok : ", _jsx(Text, { color: INK, children: stats.totalTokens.toLocaleString() })] }), _jsxs(Text, { color: MUTED, children: ["Model : ", _jsx(Text, { color: INK, children: modelShort }), " Provider: ", _jsx(Text, { color: INK, children: cfg.provider })] }), _jsx(Text, { color: DIM, children: "Tab to close" })] })), _jsx(Static, { items: altScreen ? [] : rows, children: (row, index) => renderRow(row, index) }), _jsx(Box, { flexDirection: "column", flexShrink: 0, children: !ask && busy && (agentState === 'processing' || agentState === 'syncing') && (_jsxs(Box, { flexDirection: "column", children: [!ask && sayRef.current.trim() !== '' && (_jsxs(Box, { gap: 1, children: [_jsx(Text, { color: CYAN, bold: true, children: replyStartedRef.current ? ' ' : G.brand }), _jsx(Box, { flexDirection: "column", children: tailLines(sayRef.current, liveReplyLines(termRows), replyWidth - 3)
|
|
6796
|
+
_jsx(Box, { flexGrow: 0, flexShrink: 1, flexDirection: "column", justifyContent: "flex-end", overflowY: "hidden", children: visibleRows.map((row, i) => (_jsx(Box, { flexDirection: "column", flexShrink: 0, children: renderRowCached(row, i) }, i))) })), altScreen && !ask && scrollBack > 0 && (_jsx(Text, { color: AMBER, children: `${G.caret} scrolled back ${scrollBack} rows — Ctrl+End follows live · Ctrl+Up/Down moves by section` })), showAnalytics && (_jsxs(Box, { borderStyle: "round", borderColor: AMBER, paddingX: 1, marginBottom: 1, flexDirection: "column", width: barWidth, children: [_jsx(Text, { color: AMBER, bold: true, children: "\u25C6 Session Analytics" }), _jsxs(Text, { color: MUTED, children: ["Turns : ", _jsx(Text, { color: INK, children: stats.turns })] }), _jsxs(Text, { color: MUTED, children: ["Prompt tok : ", _jsx(Text, { color: INK, children: stats.promptTokens.toLocaleString() })] }), _jsxs(Text, { color: MUTED, children: ["Output tok : ", _jsx(Text, { color: INK, children: stats.completionTokens.toLocaleString() })] }), _jsxs(Text, { color: MUTED, children: ["Total tok : ", _jsx(Text, { color: INK, children: stats.totalTokens.toLocaleString() })] }), _jsxs(Text, { color: MUTED, children: ["Model : ", _jsx(Text, { color: INK, children: modelShort }), " Provider: ", _jsx(Text, { color: INK, children: cfg.provider })] }), _jsx(Text, { color: DIM, children: "Tab to close" })] })), _jsx(Static, { items: altScreen ? [] : rows, children: (row, index) => renderRow(row, index) }, staticEpoch), _jsx(Box, { flexDirection: "column", flexShrink: 0, children: !ask && busy && (agentState === 'processing' || agentState === 'syncing') && (_jsxs(Box, { flexDirection: "column", children: [!ask && sayRef.current.trim() !== '' && (_jsxs(Box, { gap: 1, children: [_jsx(Text, { color: CYAN, bold: true, children: replyStartedRef.current ? ' ' : G.brand }), _jsx(Box, { flexDirection: "column", children: tailLines(sayRef.current, liveReplyLines(termRows), replyWidth - 3)
|
|
6572
6797
|
.split('\n').map((line, i) => _jsx(Text, { color: INK, children: line }, i)) })] })), liveToolRef.current && renderStep(liveToolRef.current, 1), queued.length > 0 && (_jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsxs(Box, { gap: 1, children: [_jsx(Text, { color: AMBER, children: "\u25AA" }), _jsxs(Text, { color: MUTED, children: [queued.length, " queued, will run when this finishes \u00B7 /aside to ask without waiting"] })] }), queued.map((q, i) => (_jsx(Box, { paddingLeft: 2, children: _jsx(Text, { color: DIM, children: fitCells(q.split('\n')[0] ?? '', Math.max(20, barWidth - 6), true) }) }, i)))] })), !activityPanelOpen && agents.length > 0 && (() => {
|
|
6573
6798
|
const done = agents.filter(a => a.status !== 'running').length;
|
|
6574
6799
|
const failed = agents.filter(a => a.status === 'failed').length;
|
|
6575
6800
|
const tokens = agents.reduce((n, a) => n + a.tokens, 0);
|
|
6576
6801
|
const started = Math.min(...agents.map(a => a.startedAt));
|
|
6577
|
-
|
|
6578
|
-
|
|
6579
|
-
|
|
6580
|
-
|
|
6802
|
+
/*
|
|
6803
|
+
* A tree, because that is what it is.
|
|
6804
|
+
*
|
|
6805
|
+
* This was a flat list with its columns laid out as flex siblings, so the timings
|
|
6806
|
+
* and token counts landed wherever each task's text happened to end — a ragged
|
|
6807
|
+
* diagonal down the right-hand side that could not be read as a column. Naming the
|
|
6808
|
+
* session as the root and the sub-agents as forks of it also says the thing the
|
|
6809
|
+
* list left implicit: these are branches of the work you asked for, not separate
|
|
6810
|
+
* conversations.
|
|
6811
|
+
*/
|
|
6812
|
+
const width = Math.max(30, barWidth - 2);
|
|
6813
|
+
return (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Box, { gap: 1, children: [_jsx(Text, { color: CYAN, children: "\u25C6" }), _jsxs(Text, { color: INK, bold: true, children: [agents.length, " ", agents.length === 1 ? 'fork' : 'forks'] }), _jsx(Text, { color: DIM, children: "\u00B7" }), _jsxs(Text, { color: done === agents.length ? GREEN : AMBER, children: [done, "/", agents.length, " done"] }), _jsx(Text, { color: DIM, children: "\u00B7" }), _jsxs(Text, { color: MUTED, children: [humanTokens(tokens), " tok"] }), _jsx(Text, { color: DIM, children: "\u00B7" }), _jsx(Text, { color: MUTED, children: fmtElapsed(Date.now() - started) }), failed > 0 && _jsxs(Text, { color: CRIMSON, children: ["\u00B7 ", failed, " failed"] })] }), _jsxs(Text, { children: [_jsxs(Text, { color: CYAN, children: [G.ok === '✓' ? '●' : '*', " "] }), _jsx(Text, { color: INK, children: "main" })] }), agents.map(a => {
|
|
6581
6814
|
const glyph = a.status === 'running' ? SPINNER[spinFrame]
|
|
6582
6815
|
: a.status === 'failed' ? G.fail : G.ok;
|
|
6583
6816
|
const color = a.status === 'running' ? AMBER
|
|
6584
6817
|
: a.status === 'failed' ? CRIMSON : GREEN;
|
|
6585
6818
|
const took = (a.endedAt ?? Date.now()) - a.startedAt;
|
|
6586
|
-
const
|
|
6587
|
-
|
|
6819
|
+
const meta = `${fmtElapsed(took)} · ${G.ok === '✓' ? '↓' : 'v'} ${humanTokens(a.tokens)} tokens`;
|
|
6820
|
+
const row = forkRow(a.task, meta, width, FORK_LEAD);
|
|
6821
|
+
// One Text, not a Box of them: five layout nodes per agent times eight agents
|
|
6822
|
+
// is a repaint cost for a list that changes every second anyway.
|
|
6823
|
+
return (_jsxs(Text, { children: [_jsxs(Text, { color: color, children: [glyph, " "] }), _jsx(Text, { color: DIM, children: "fork " }), _jsx(Text, { color: a.status === 'running' ? INK : MUTED, children: row.task }), _jsx(Text, { children: row.gap }), _jsx(Text, { color: DIM, children: row.meta })] }, a.index));
|
|
6588
6824
|
})] }));
|
|
6589
6825
|
})(), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { children: [_jsxs(Text, { color: GREEN, children: [animate ? SPINNER[spinFrame] : '●', " "] }), _jsx(Shimmer, { text: workWord({
|
|
6590
6826
|
elapsedMs, tools: turnToolsRef.current,
|
|
@@ -6918,7 +7154,24 @@ export function App({ config: initialConfig, clearFrame, synchronizedFrames = fa
|
|
|
6918
7154
|
: { label: 'repoint ' + addProv.name + '? (y/n)',
|
|
6919
7155
|
hint: `it ships with KONECK, pointing at ${shadowed?.baseURL}` };
|
|
6920
7156
|
return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: CYAN, paddingX: 1, marginTop: 1, width: barWidth, children: [_jsx(Text, { color: CYAN, bold: true, children: "Declare a provider" }), _jsxs(Text, { color: MUTED, children: [ask.hint, " \u00B7 esc to cancel"] }), addProv.name !== '' && (_jsxs(Text, { color: DIM, children: [addProv.name, addProv.url ? ' ' + addProv.url : ''] })), addProv.error && _jsx(Text, { color: CRIMSON, children: addProv.error }), _jsxs(Box, { marginTop: 1, children: [_jsxs(Text, { color: MUTED, children: [ask.label, ": "] }), _jsx(Text, { color: INK, children: addProv.value }), _jsx(Cursor, {})] })] }));
|
|
6921
|
-
})(), keyPrompt && (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: AMBER, paddingX: 1, marginTop: 1, width: barWidth, children: [_jsxs(Text, { color: AMBER, bold: true, children: ["API key for ", keyPrompt.provider] }), _jsx(Text, { color: MUTED, children: "Paste it and press enter, or ctrl+v to read the clipboard. Held in memory for this session only; esc to cancel." }), keyPrompt.note && _jsx(Text, { color: CYAN, children: keyPrompt.note }), _jsxs(Box, { marginTop: 1, children: [_jsxs(Text, { color: MUTED, children: [keyPrompt.env, ": "] }), _jsx(Text, { color: INK, children: '*'.repeat(Math.min(keyPrompt.value.length, 48)) }), _jsx(Cursor, {}), keyPrompt.value.length > 0 && (_jsxs(Text, { color: DIM, children: [" ", keyPrompt.value.length, " characters"] }))] })] })),
|
|
7157
|
+
})(), keyPrompt && (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: AMBER, paddingX: 1, marginTop: 1, width: barWidth, children: [_jsxs(Text, { color: AMBER, bold: true, children: ["API key for ", keyPrompt.provider] }), _jsx(Text, { color: MUTED, children: "Paste it and press enter, or ctrl+v to read the clipboard. Held in memory for this session only; esc to cancel." }), keyPrompt.note && _jsx(Text, { color: CYAN, children: keyPrompt.note }), _jsxs(Box, { marginTop: 1, children: [_jsxs(Text, { color: MUTED, children: [keyPrompt.env, ": "] }), _jsx(Text, { color: INK, children: '*'.repeat(Math.min(keyPrompt.value.length, 48)) }), _jsx(Cursor, {}), keyPrompt.value.length > 0 && (_jsxs(Text, { color: DIM, children: [" ", keyPrompt.value.length, " characters"] }))] })] })), planAsk && (() => {
|
|
7158
|
+
/*
|
|
7159
|
+
* The question, straight after the plan rather than instead of it.
|
|
7160
|
+
*
|
|
7161
|
+
* The plan itself is already in the transcript above — repeating it here would push the
|
|
7162
|
+
* choices off a short screen, which is the one thing this panel must not do. So it says
|
|
7163
|
+
* where the plan was saved and asks.
|
|
7164
|
+
*/
|
|
7165
|
+
const lines = [
|
|
7166
|
+
'A plan is ready. Carry it out?',
|
|
7167
|
+
'',
|
|
7168
|
+
...PLAN_CHOICES.map((c, i) => i === planAsk.choice ? `**${G.caret} ${c.key}. ${c.label}**` : ` ${c.key}. ${c.label}`),
|
|
7169
|
+
'',
|
|
7170
|
+
'Up/down and enter, or press the number. Esc leaves it in plan mode.',
|
|
7171
|
+
...(planAsk.file ? [`Saved to \`${planAsk.file}\``] : ['This plan could not be written to disk.']),
|
|
7172
|
+
];
|
|
7173
|
+
return (_jsx(Box, { marginTop: 1, children: _jsx(Panel, { width: Math.min(barWidth, 84), color: CYAN, title: "Plan ready", children: lines }) }));
|
|
7174
|
+
})(), ask && (() => {
|
|
6922
7175
|
let detail = '';
|
|
6923
7176
|
let isCommand = false;
|
|
6924
7177
|
try {
|