koneck 2.128.55 → 2.128.57

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/ink-chat.js CHANGED
@@ -46,6 +46,7 @@ import { SessionChanges, readForDiff as readForDiffAt, totalsOf } from './sessio
46
46
  import os from 'node:os';
47
47
  import { modeLine, nextMode, statusFacts, barCells, PLACEHOLDER, contextSplit, contextSplitLines } from './status-line.js';
48
48
  import { KONECK_VERSION } from './version.js';
49
+ import { fileListView, listLength, diffRows, PANEL_CONTEXT, PANEL_DIFF_LINES } from './diff-layout.js';
49
50
  import { savePlan, plansDir, PLAN_CHOICES, PROCEED_PROMPT } from './plan-file.js';
50
51
  import { logoFor } from './logo.js';
51
52
  import { describeActivity } from './activity.js';
@@ -793,7 +794,7 @@ export function expandPastes(draft, pastes) {
793
794
  * capped: a 4,000-line rewrite scrolled past is no more readable in a panel than in a transcript,
794
795
  * and the count of what was left out is shown instead.
795
796
  */
796
- const PANEL_DIFF_LINES = 400;
797
+ // PANEL_DIFF_LINES lives in diff-layout.ts, so the browser's Changes view caps at the same point.
797
798
  /** The tabs of the usage pane, in the order they are shown. */
798
799
  const USAGE_TABS = ['settings', 'session', 'context', 'models', 'limits'];
799
800
  const MIN_REPLY_WIDTH = 24; // below this a table cannot render legibly anyway
@@ -1263,6 +1264,34 @@ export function inkApprovalDecision(requireApproval, mode, tool, argsJson, rules
1263
1264
  return rules.some(rule => ruleMatches(rule, tool, argsJson)) ? 'allow' : 'ask';
1264
1265
  }
1265
1266
  /** A context report names the evidence for its denominator instead of pretending every model fits. */
1267
+ /**
1268
+ * What the turn is waiting on, for the bracket after the elapsed time.
1269
+ *
1270
+ * Reasoning comes first once any has arrived. A reasoning chunk carries no reply text, so it
1271
+ * counted as a frame "before content" and the line said "ollama is holding the line, no output
1272
+ * yet" — with the thinking count tacked on after it — for the whole time the model was visibly
1273
+ * thinking. Reported as: "koneck write 'ollama is holding the line, no output' yet the model is
1274
+ * already thinking". Holding the line is for frames that carry nothing at all: a gateway's
1275
+ * keep-alives. The browser already said "thinking" here.
1276
+ */
1277
+ export function waitDetail(w) {
1278
+ const thinking = `thinking, ${fmtTokens(Math.round(w.thinkingChars / 4))} reasoning tokens`;
1279
+ if (w.liveTokens > 0) {
1280
+ // Reply text has come and the model has gone back to reasoning: both are true, both are said.
1281
+ return `${fmtTokens(w.liveTokens)} tokens` + (w.thinkingChars > 0 && w.thinkingRecent ? ` | ${thinking}` : '');
1282
+ }
1283
+ if (w.tool)
1284
+ return 'running ' + w.tool;
1285
+ if (w.thinkingChars > 0)
1286
+ return thinking;
1287
+ // A gateway routing to several backends sends content-free frames while it finds one. Saying
1288
+ // "waiting for the first token" through that reads as a hang, when the request was accepted.
1289
+ if (w.frames > 0)
1290
+ return `${w.provider} is holding the line, no output yet`;
1291
+ if (w.elapsedMs > 8_000)
1292
+ return `no response from ${w.provider} yet`;
1293
+ return 'starting';
1294
+ }
1266
1295
  export function contextStatusText(model, sessionPromptTokens, configuredLimit, detected) {
1267
1296
  const known = detected?.limit ?? configuredLimit ?? contextWindowFor(model);
1268
1297
  const source = detected?.source === 'provider' ? 'reported by the connected provider'
@@ -1534,96 +1563,71 @@ function renderCodeLine(text, kind, width, lang) {
1534
1563
  /**
1535
1564
  * The live diff panel — the work as it happens, beside the conversation.
1536
1565
  *
1537
- * Asked for: the same running view the web has, in the terminal. It shows every file the session
1538
- * has changed, newest first, with the counts in the header, so a person watching can see the edit
1539
- * land rather than reading a description of it afterwards. Bounded on both axes: the newest files
1540
- * fill the height available and the rest are counted, since a panel that scrolls past what you
1541
- * wanted is no better than no panel.
1566
+ * Laid out the way Claude Code's is, which was the picture it was asked for with: the changed files
1567
+ * listed at the top with their own counts, tests and generated files folded into one line, and
1568
+ * below a rule the one file being read. Its changed lines sit on a band the whole width of the
1569
+ * panel, and a line too long for it wraps, carrying its + or - onto each piece.
1570
+ *
1571
+ * It used to stack every file's diff in one column, so the fifteenth file was a scroll through
1572
+ * fourteen others, and a long line was cut at the panel's edge. What is listed, in what order,
1573
+ * what is folded and how a line wraps are decided in diff-layout.ts, which the browser's Changes
1574
+ * view uses too.
1575
+ *
1576
+ * Alt+↑/↓ (or Alt+P/N, for Windows Terminal, which keeps Alt+arrows for its own panes) moves
1577
+ * through the list, Alt+PgUp/PgDn and the wheel scroll the file, Alt+T opens the fold.
1542
1578
  */
1543
- export function DiffPanel({ files, width, height, activity, activityRows = 0, scroll = 0 }) {
1544
- const added = files.reduce((n, d) => n + d.added, 0);
1545
- const removed = files.reduce((n, d) => n + d.removed, 0);
1546
- // The persistent panel needs a visible boundary even before an edit arrives. Without one, its
1547
- // empty state was a single easy-to-miss line and looked as if `/diff` had done nothing.
1548
- const inner = Math.max(20, width - 4);
1549
- const bodyHeight = Math.max(1, height - 4 - activityRows);
1550
- const rows = [];
1551
- let shownFiles = 0;
1552
- for (const d of [...files].reverse()) {
1553
- /*
1554
- * A rule between files, so the panel reads as sections rather than one column of lines.
1555
- *
1556
- * Asked for, against Claude Code's panel as the reference: "it is clean and well structured".
1557
- * The difference was structural, not chromatic — the colours were already right, but with a
1558
- * blank line as the only separation two files' diffs ran together and the filename headers had
1559
- * nothing to sit under. Drawn only between files, never above the first.
1560
- */
1561
- if (shownFiles > 0)
1562
- rows.push({ t: 'rule', key: `r-${d.path}` });
1563
- const numbers = d.hunks.flatMap(h => h.lines.map(l => l.after ?? l.before ?? 0));
1564
- const gutter = Math.max(3, String(Math.max(0, ...numbers)).length);
1565
- const codeWidth = Math.max(12, inner - gutter - 2);
1566
- const lang = spanLangFor(d.path);
1567
- const tag = d.created ? 'new' : d.deleted ? 'deleted' : `+${d.added}${d.removed ? ` -${d.removed}` : ''}`;
1568
- rows.push({ t: 'head', key: `h-${d.path}`, path: d.path, tag, deleted: d.deleted });
1569
- if (d.binary) {
1570
- rows.push({ t: 'binary', key: `b-${d.path}` });
1571
- shownFiles++;
1572
- continue;
1573
- }
1574
- for (const [hi, hunk] of d.hunks.entries()) {
1575
- if (hi > 0)
1576
- rows.push({ t: 'gap', key: `g-${d.path}-${hi}`, gutter });
1577
- for (const [li, l] of hunk.lines.entries()) {
1578
- rows.push({
1579
- t: 'line', key: `l-${d.path}-${hi}-${li}`, gutter, codeWidth, lang,
1580
- shown: (l.kind === 'del' ? l.before : l.after) ?? undefined,
1581
- sign: l.kind === 'add' ? '+' : l.kind === 'del' ? '-' : ' ',
1582
- kind: l.kind, text: l.text,
1583
- });
1584
- }
1585
- }
1586
- if (d.hiddenLines > 0) {
1587
- rows.push({ t: 'more', key: `m-${d.path}`, gutter, count: d.hiddenLines });
1588
- }
1589
- shownFiles++;
1590
- rows.push({ t: 'spacer', key: `s-${d.path}` });
1579
+ export function DiffPanel({ files, width, height, activity, activityRows = 0, scroll = 0, selected = 0, showGenerated = false }) {
1580
+ // A rule down the left instead of a box round it: two rows more for the diff, and the panel
1581
+ // reads as a pane beside the conversation, as in the panel it was modelled on.
1582
+ const inner = Math.max(20, width - 3);
1583
+ const room = Math.max(4, height - activityRows);
1584
+ const listLen = listLength(files, showGenerated);
1585
+ const listRows = Math.max(1, Math.min(listLen, 8, Math.max(2, Math.floor(room * 0.3))));
1586
+ const view = fileListView(files, { selected, rows: listRows, showGenerated });
1587
+ const d = view.selected >= 0 ? files[view.selected] : undefined;
1588
+ const counts = (added, removed, tag) => (_jsxs(_Fragment, { children: [tag !== '' && added === 0 && removed === 0 && _jsx(Text, { color: DIM, children: tag }), added > 0 && _jsxs(Text, { color: DIFF_ADD, children: ["+", added] }), removed > 0 && _jsxs(Text, { color: DIFF_DEL, children: [added > 0 ? ' ' : '', "-", removed] })] }));
1589
+ const list = view.rows.map(f => (_jsxs(Box, { justifyContent: "space-between", width: inner, children: [_jsxs(Text, { color: f.selected ? INK : MUTED, bold: f.selected, wrap: "truncate-start", children: [f.selected ? '› ' : ' ', f.path] }), _jsxs(Text, { children: [" ", counts(f.added, f.removed, f.tag)] })] }, `f-${f.path}`)));
1590
+ const hints = [];
1591
+ if (view.above > 0 || view.below > 0) {
1592
+ hints.push(_jsxs(Text, { color: DIM, wrap: "truncate", children: [view.above > 0 ? ` ↑ ${view.above} more above` : '', view.above > 0 && view.below > 0 ? ' ·' : '', view.below > 0 ? ` ↓ ${view.below} more below` : '', " (alt+\u2193 to move)"] }, "more"));
1593
+ }
1594
+ if (view.folded.count > 0) {
1595
+ hints.push(_jsx(Text, { color: DIM, wrap: "truncate", children: ` ${view.folded.count} tests/generated (alt+t to ${view.folded.open ? 'hide' : 'show'})` }, "fold"));
1591
1596
  }
1597
+ /*
1598
+ * Only the rows on screen are built into elements.
1599
+ *
1600
+ * Measured on a Raspberry Pi 5 with two 200-line diffs: building every row's elements on every
1601
+ * frame cost 281ms a repaint, against 1.09ms with the panel shut. A row object is cheap; a React
1602
+ * element per row is not. So the rows are described in full — the scroll needs the count — and
1603
+ * drawn only for the window.
1604
+ */
1605
+ const fixedRows = 2 + list.length + hints.length + (d ? 4 : 0) + (activity ? activityRows : 0);
1606
+ const bodyHeight = Math.max(1, height - fixedRows);
1607
+ const numbers = d ? d.hunks.flatMap(h => h.lines.map(l => l.after ?? l.before ?? 0)) : [];
1608
+ const gutter = Math.max(3, String(Math.max(0, ...numbers)).length);
1609
+ const codeWidth = Math.max(8, inner - gutter - 3);
1610
+ const rows = d ? diffRows(d, codeWidth) : [];
1592
1611
  const maxScroll = Math.max(0, rows.length - bodyHeight);
1593
1612
  const from = Math.max(0, Math.min(scroll, maxScroll));
1594
- const body = rows.slice(from, from + bodyHeight).map(r => {
1595
- switch (r.t) {
1596
- case 'rule':
1597
- return _jsx(Text, { color: DIM, children: '─'.repeat(inner) }, r.key);
1598
- case 'head':
1599
- 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));
1600
- case 'binary':
1601
- return _jsx(Text, { color: DIM, children: " binary \u2014 not shown" }, r.key);
1602
- case 'gap':
1603
- return _jsx(Text, { color: DIM, children: `${' '.repeat(r.gutter)} ⋯` }, r.key);
1604
- case 'more':
1605
- return _jsx(Text, { color: DIM, children: `${' '.repeat(r.gutter)} ⋯ ${r.count} more` }, r.key);
1606
- case 'spacer':
1607
- return _jsx(Text, { children: " " }, r.key);
1608
- default:
1609
- /*
1610
- * One Text per line, with the pieces nested inside it, rather than a Box of siblings.
1611
- *
1612
- * A Box is a layout node: the engine measures and places it and every child. A Text with
1613
- * Texts inside it is one node holding spans. With forty lines on screen and half a dozen
1614
- * pieces each that is the difference between laying out two hundred and fifty nodes a
1615
- * frame and laying out forty — and it is the same mistake that stranded the composer's
1616
- * cursor on the first row, in a place where it costs time rather than correctness.
1617
- */
1618
- 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'
1619
- ? { backgroundColor: r.kind === 'add' ? DIFF_ADD_BG : DIFF_DEL_BG } : {}), children: r.sign }), renderCodeLine(r.text, r.kind, r.codeWidth, r.lang)] }, r.key));
1620
- }
1613
+ const lang = d ? spanLangFor(d.path) : undefined;
1614
+ const body = rows.slice(from, from + bodyHeight).map((r, i) => {
1615
+ const key = `d-${from + i}`;
1616
+ if (r.t === 'binary')
1617
+ return _jsx(Text, { color: DIM, children: " binary \u2014 not shown" }, key);
1618
+ if (r.t === 'more')
1619
+ return _jsx(Text, { color: DIM, children: `${' '.repeat(gutter)} ⋯ ${r.count} more lines not shown` }, key);
1620
+ const bg = r.kind === 'add' ? DIFF_ADD_BG : r.kind === 'del' ? DIFF_DEL_BG : undefined;
1621
+ const fg = r.kind === 'add' ? DIFF_ADD : r.kind === 'del' ? DIFF_DEL : DIM;
1622
+ const tint = bg ? { backgroundColor: bg } : {};
1623
+ // One Text with the pieces nested: a Box of siblings is a layout node per piece, forty lines
1624
+ // times half a dozen pieces a frame.
1625
+ return (_jsxs(Text, { wrap: "truncate", children: [_jsxs(Text, { color: r.kind === 'ctx' ? DIM : fg, ...tint, children: [(r.first && r.number !== null ? String(r.number) : '').padStart(gutter), ' ', r.sign, ' '] }), renderCodeLine(r.text, r.kind, codeWidth, lang)] }, key));
1621
1626
  });
1622
- const summary = files.length === 0
1623
- ? 'no changes yet'
1624
- : `${files.length} file${files.length === 1 ? '' : 's'} changed`
1625
- + (maxScroll > 0 ? ` ${from > 0 ? `↑${from}` : ''}${from < maxScroll ? ` ↓${maxScroll - from}` : ''}`.trimEnd() : '');
1626
- 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` }))] }));
1627
+ const scrolled = maxScroll > 0
1628
+ ? `${from > 0 ? `↑${from}` : ''}${from < maxScroll ? ` ↓${maxScroll - from}` : ''}`.trim()
1629
+ : '';
1630
+ return (_jsxs(Box, { flexDirection: "column", width: width, height: height, borderStyle: "single", borderColor: DIM, borderTop: false, borderRight: false, borderBottom: false, paddingLeft: 1, flexShrink: 0, overflowY: "hidden", children: [_jsxs(Box, { justifyContent: "space-between", width: inner, children: [_jsxs(Box, { children: [_jsx(Text, { color: INK, bold: true, children: files.length === 0 ? 'no changes yet' : `${files.length} file${files.length === 1 ? '' : 's'} changed` }), view.added > 0 && _jsxs(Text, { color: DIFF_ADD, children: [" +", view.added] }), view.removed > 0 && _jsxs(Text, { color: DIFF_DEL, children: [" -", view.removed] })] }), _jsx(Text, { color: DIM, children: "/diff off" })] }), 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(Text, { children: " " }), list, hints, d && _jsxs(_Fragment, { children: [_jsx(Text, { children: " " }), _jsx(Text, { color: DIM, children: '─'.repeat(inner) }), _jsxs(Box, { justifyContent: "space-between", width: inner, children: [_jsx(Text, { color: INK, bold: true, wrap: "truncate-start", children: d.path }), _jsxs(Text, { color: DIM, children: [d.created && d.removed === 0 ? ' new' : d.deleted ? ' deleted' : '', scrolled ? ` ${scrolled}` : ''] })] }), _jsx(Text, { color: DIM, children: '─'.repeat(inner) })] }), _jsx(Box, { flexDirection: "column", flexGrow: 1, flexShrink: 1, overflowY: "hidden", children: body })] }));
1627
1631
  }
1628
1632
  /**
1629
1633
  * Exported for one reason: so a test can mount it.
@@ -1950,7 +1954,7 @@ export function App({ config: initialConfig, clearFrame, synchronizedFrames = fa
1950
1954
  * the truth about what the change currently is.
1951
1955
  */
1952
1956
  if (changesRef.current.size) {
1953
- const all = changesRef.current.diffs(PANEL_DIFF_LINES);
1957
+ const all = changesRef.current.diffs(PANEL_DIFF_LINES, PANEL_CONTEXT);
1954
1958
  setLiveDiffs(all);
1955
1959
  /*
1956
1960
  * Open itself on the session's first change, once, unless the person has already decided.
@@ -2372,6 +2376,9 @@ export function App({ config: initialConfig, clearFrame, synchronizedFrames = fa
2372
2376
  * studying a diff should not cost your place in the conversation.
2373
2377
  */
2374
2378
  const [panelScroll, setPanelScroll] = useState(0);
2379
+ /** Which file of the panel's list is being read, and whether tests and generated files are listed. */
2380
+ const [panelFile, setPanelFile] = useState(0);
2381
+ const [panelGenerated, setPanelGenerated] = useState(false);
2375
2382
  /**
2376
2383
  * The layout the wheel listener needs, in refs.
2377
2384
  *
@@ -5859,6 +5866,24 @@ export function App({ config: initialConfig, clearFrame, synchronizedFrames = fa
5859
5866
  * needs `/mouse on`. This is the keyboard equivalent, and it is deliberately a modifier
5860
5867
  * nothing else in the composer uses, so it cannot be reached by accident mid-sentence.
5861
5868
  */
5869
+ /*
5870
+ * Alt+↑/↓ moves through the panel's list of files, as in the panel it was modelled on; the
5871
+ * file itself scrolls with Alt+PgUp/PgDn and the wheel. Alt+P/N do the same for Windows
5872
+ * Terminal, which keeps Alt+arrows for moving between its own panes. Alt+T opens the fold
5873
+ * of tests and generated files.
5874
+ */
5875
+ const prevFile = key.meta && (key.upArrow || input === 'p');
5876
+ const nextFile = key.meta && (key.downArrow || input === 'n');
5877
+ if (diffPanelOpenRef.current && (prevFile || nextFile)) {
5878
+ const last = Math.max(0, listLength(liveDiffs, panelGenerated) - 1);
5879
+ setPanelFile(v => Math.max(0, Math.min(last, v + (prevFile ? -1 : 1))));
5880
+ setPanelScroll(0);
5881
+ return;
5882
+ }
5883
+ if (diffPanelOpenRef.current && key.meta && input === 't') {
5884
+ setPanelGenerated(v => !v);
5885
+ return;
5886
+ }
5862
5887
  if (key.meta && (key.upArrow || key.downArrow)) {
5863
5888
  setPanelScroll(v => Math.max(0, Math.min(2_000, v + (key.upArrow ? -3 : 3))));
5864
5889
  return;
@@ -6856,34 +6881,25 @@ export function App({ config: initialConfig, clearFrame, synchronizedFrames = fa
6856
6881
  const gutter = Math.max(2, String(Math.max(0, ...numbers)).length);
6857
6882
  const width = Math.max(24, barWidth - gutter - 8);
6858
6883
  const header = d.created ? 'new file' : d.deleted ? 'deleted' : diffSummary(d);
6859
- const all = [];
6860
- for (const [hi, hunk] of d.hunks.entries()) {
6861
- if (hi > 0)
6862
- all.push({ key: `g${hi}`, gap: true });
6863
- for (const [li, l] of hunk.lines.entries()) {
6864
- all.push({ key: `${hi}-${li}`, gap: false, kind: l.kind,
6865
- shown: (l.kind === 'del' ? l.before : l.after) ?? undefined,
6866
- sign: l.kind === 'add' ? '+' : l.kind === 'del' ? '-' : ' ',
6867
- text: l.text });
6868
- }
6869
- }
6884
+ const lang = spanLangFor(d.path);
6885
+ /*
6886
+ * Rows from diff-layout.ts, the same the panel draws: a long line wraps with its sign on each
6887
+ * piece rather than being cut at the edge, and the band behind a changed line covers its
6888
+ * number too. The ceiling counts rows as drawn, so a wrapped line costs what it takes.
6889
+ */
6890
+ const all = diffRows({ ...d, hiddenLines: 0 }, width);
6870
6891
  // A budget of zero is meaningful: show the file and what happened to it, and none of the
6871
6892
  // body. On a short terminal those lines are the difference between a calm screen and one
6872
6893
  // that wipes itself on every keystroke.
6873
6894
  const shownLines = all.slice(0, Math.max(0, maxLines));
6874
6895
  const clipped = all.length - shownLines.length;
6875
- return (_jsxs(Box, { flexDirection: "column", paddingLeft: 2, children: [_jsxs(Box, { gap: 1, children: [_jsx(Text, { color: DIM, children: "\u2514" }), _jsx(Text, { color: CYAN, children: d.path }), _jsx(Text, { color: d.created ? DIFF_ADD : d.deleted ? DIFF_DEL : MUTED, children: header })] }), d.binary
6876
- ? _jsx(Text, { color: DIM, children: ' binary file - not shown' })
6877
- : shownLines.map(l => l.gap
6878
- // A gap between hunks stands for the untouched lines that were skipped.
6879
- ? _jsxs(Text, { color: DIM, children: [' '.repeat(gutter), " ..."] }, l.key)
6880
- : (() => {
6881
- const fg = l.kind === 'add' ? DIFF_ADD : l.kind === 'del' ? DIFF_DEL : MUTED;
6882
- const bg = l.kind === 'add' ? DIFF_ADD_BG : l.kind === 'del' ? DIFF_DEL_BG : undefined;
6883
- // Tabs would make the tinted block a ragged width, so they are expanded.
6884
- const body = `${l.sign} ${l.text}`.replace(/\t/g, ' ');
6885
- return (_jsxs(Box, { children: [_jsxs(Text, { color: DIM, children: [String(l.shown ?? '').padStart(gutter), " "] }), _jsx(Text, { color: fg, ...(bg ? { backgroundColor: bg } : {}), children: fitCells(body, width, true) })] }, l.key));
6886
- })()), clipped > 0 && (_jsxs(Text, { color: DIM, children: [' '.repeat(gutter), " ", shownLines.length === 0 ? '' : '... ', clipped, " ", shownLines.length === 0 ? 'changed lines' : 'more lines', ", shown in full when this finishes"] })), d.hiddenLines > 0 && (_jsxs(Text, { color: DIM, children: [' '.repeat(gutter), " ... ", d.hiddenLines, " more changed lines"] }))] }, key));
6896
+ return (_jsxs(Box, { flexDirection: "column", paddingLeft: 2, children: [_jsxs(Box, { gap: 1, children: [_jsx(Text, { color: DIM, children: "\u2514" }), _jsx(Text, { color: CYAN, children: d.path }), _jsx(Text, { color: d.created ? DIFF_ADD : d.deleted ? DIFF_DEL : MUTED, children: header })] }), shownLines.map((r, i) => {
6897
+ if (r.t !== 'line')
6898
+ return _jsx(Text, { color: DIM, children: ' binary file - not shown' }, i);
6899
+ const fg = r.kind === 'add' ? DIFF_ADD : r.kind === 'del' ? DIFF_DEL : DIM;
6900
+ const bg = r.kind === 'add' ? DIFF_ADD_BG : r.kind === 'del' ? DIFF_DEL_BG : undefined;
6901
+ return (_jsxs(Text, { wrap: "truncate", children: [_jsxs(Text, { color: fg, ...(bg ? { backgroundColor: bg } : {}), children: [(r.first && r.number !== null ? String(r.number) : '').padStart(gutter), ' ', r.sign, ' '] }), renderCodeLine(r.text, r.kind, width, lang)] }, i));
6902
+ }), clipped > 0 && (_jsxs(Text, { color: DIM, children: [' '.repeat(gutter), " ", shownLines.length === 0 ? '' : '... ', clipped, " ", shownLines.length === 0 ? 'changed lines' : 'more lines', ", shown in full when this finishes"] })), d.hiddenLines > 0 && (_jsxs(Text, { color: DIM, children: [' '.repeat(gutter), " ... ", d.hiddenLines, " more changed lines"] }))] }, key));
6887
6903
  };
6888
6904
  const renderStep = (step, key) => {
6889
6905
  if (step.kind === 'say') {
@@ -7076,17 +7092,12 @@ export function App({ config: initialConfig, clearFrame, synchronizedFrames = fa
7076
7092
  })(), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { children: [_jsxs(Text, { color: GREEN, children: [animate ? SPINNER[spinFrame] : '●', " "] }), _jsx(Shimmer, { text: workWord({
7077
7093
  elapsedMs, tools: turnToolsRef.current,
7078
7094
  tokens: liveTokens, recovered: turnRecoveredRef.current,
7079
- }, spinWord)[0], frame: shimmerFrame, base: MUTED }), _jsx(Text, { color: MUTED, children: "\u2026 " }), _jsxs(Text, { color: DIM, children: ["(", fmtElapsed(elapsedMs), " | ", liveTokens > 0
7080
- ? `${fmtTokens(liveTokens)} tokens`
7081
- // A gateway routing to several backends sends content-free frames while it
7082
- // finds one. Saying "waiting for the first token" through that reads as a
7083
- // hang, when in fact the request was accepted and is being worked on.
7084
- : liveToolRef.current ? 'running ' + liveToolRef.current.name
7085
- : providerFramesRef.current > 0 ? `${cfg.provider} is holding the line, no output yet`
7086
- : elapsedMs > 8_000 ? `no response from ${cfg.provider} yet`
7087
- : 'starting', thinkingRef.current.chars > 0 && Date.now() - thinkingRef.current.at < 2_000
7088
- ? ` | thinking, ${fmtTokens(Math.round(thinkingRef.current.chars / 4))} reasoning tokens`
7089
- : '', ")"] })] }) }), showThinking && !ask && thinkingTextRef.current.trim() !== '' && (_jsx(Box, { flexDirection: "column", paddingLeft: 2, children: thinkingTail(thinkingTextRef.current, THINKING_LINES, barWidth - 6).map((line, i) => (_jsx(Text, { color: DIM, italic: true, children: line }, i))) })), _jsx(Box, { paddingLeft: 2, children: _jsx(Text, { color: DIM, children: "esc to cancel and keep your input \u00B7 ctrl+c to exit" }) })] })) }), usagePane && (() => {
7095
+ }, spinWord)[0], frame: shimmerFrame, base: MUTED }), _jsx(Text, { color: MUTED, children: "\u2026 " }), _jsxs(Text, { color: DIM, children: ["(", fmtElapsed(elapsedMs), " | ", waitDetail({
7096
+ liveTokens, tool: liveToolRef.current?.name ?? null, provider: cfg.provider,
7097
+ frames: providerFramesRef.current, elapsedMs,
7098
+ thinkingChars: thinkingRef.current.chars,
7099
+ thinkingRecent: Date.now() - thinkingRef.current.at < 2_000,
7100
+ }), ")"] })] }) }), showThinking && !ask && thinkingTextRef.current.trim() !== '' && (_jsx(Box, { flexDirection: "column", paddingLeft: 2, children: thinkingTail(thinkingTextRef.current, THINKING_LINES, barWidth - 6).map((line, i) => (_jsx(Text, { color: DIM, italic: true, children: line }, i))) })), _jsx(Box, { paddingLeft: 2, children: _jsx(Text, { color: DIM, children: "esc to cancel and keep your input \u00B7 ctrl+c to exit" }) })] })) }), usagePane && (() => {
7090
7101
  const width = Math.min(barWidth, usagePane === 'settings' ? 96 : 66);
7091
7102
  const inner = width - 4;
7092
7103
  const BAR_W = Math.max(12, inner - 14);
@@ -7510,7 +7521,7 @@ export function App({ config: initialConfig, clearFrame, synchronizedFrames = fa
7510
7521
  : '*type the number, or arrows and enter · esc refuses*',
7511
7522
  ];
7512
7523
  return (_jsx(Box, { marginTop: 1, children: _jsx(Panel, { width: panelWidth(lines, barWidth, 46), color: AMBER, title: `Permission — ${modeSpec(mode).label} mode`, children: lines }) }));
7513
- })()] }), sidePanelOpen && (_jsx(Box, { flexDirection: "column", width: diffPanelWidth, flexShrink: 0, children: diffPanelOpen && _jsx(DiffPanel, { files: liveDiffs, width: diffPanelWidth, scroll: panelScroll, height: Math.max(6, frameRows - 6), activityRows: activityPanelOpen ? 4 : 0, activity: activityPanelOpen ? _jsxs(_Fragment, { children: [_jsxs(Text, { color: CYAN, bold: true, children: ["Activity \u00B7 ", fmtElapsed(busyStart.current ? Date.now() - busyStart.current : 0)] }), _jsx(Text, { color: AMBER, wrap: "truncate", children: ask ? 'Waiting for your approval' : liveToolRef.current
7524
+ })()] }), sidePanelOpen && (_jsx(Box, { flexDirection: "column", width: diffPanelWidth, flexShrink: 0, children: diffPanelOpen && _jsx(DiffPanel, { files: liveDiffs, width: diffPanelWidth, scroll: panelScroll, selected: panelFile, showGenerated: panelGenerated, height: Math.max(6, frameRows - 6), activityRows: activityPanelOpen ? 4 : 0, activity: activityPanelOpen ? _jsxs(_Fragment, { children: [_jsxs(Text, { color: CYAN, bold: true, children: ["Activity \u00B7 ", fmtElapsed(busyStart.current ? Date.now() - busyStart.current : 0)] }), _jsx(Text, { color: AMBER, wrap: "truncate", children: ask ? 'Waiting for your approval' : liveToolRef.current
7514
7525
  ? 'Running ' + liveToolRef.current.name : thinkingRef.current.chars > 0
7515
7526
  ? 'Waiting for model · reasoning received' : 'Waiting for model' }), _jsx(Text, { color: MUTED, wrap: "truncate", children: liveToolRef.current?.detail || plan?.steps.find(s => s.state !== 'done')?.text || 'esc to interrupt' })] }) : undefined }) }))] }), picker && (() => {
7516
7527
  const list = visiblePickerItems;