koneck 2.25.7 → 2.25.8

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.
@@ -39,6 +39,16 @@ interface PickerItem {
39
39
  group?: string;
40
40
  current?: boolean;
41
41
  }
42
+ /** How a held-back paste appears in the composer. */
43
+ export declare function pasteToken(index: number, lines: number): string;
44
+ /**
45
+ * Puts held-back pastes back into the text before it is sent.
46
+ *
47
+ * The token is what the user sees; the model must receive what they actually copied. Anything
48
+ * typed around a token is preserved in place, so a paste can be introduced and followed by a
49
+ * question in one prompt.
50
+ */
51
+ export declare function expandPastes(draft: string, pastes: readonly string[]): string;
42
52
  /**
43
53
  * Filters picker entries. Prefix matches rank above substring matches so typing "mo" offers
44
54
  * /model and /models before /memory, and a leading slash in either the query or the entry is
@@ -1 +1 @@
1
- {"version":3,"file":"ink-chat.d.ts","sourceRoot":"","sources":["../src/ink-chat.tsx"],"names":[],"mappings":"AAmCA,OAAO,KAAK,EAAE,WAAW,EAAiB,MAAM,YAAY,CAAC;AAgE7D,6FAA6F;AAC7F,wBAAgB,UAAU,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,CAM7C;AAqDD;;;;;;;;;;;GAWG;AACH,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,CAGnE;AAuBD;;;;;;;;;;;GAWG;AACH,MAAM,MAAM,QAAQ,GAAG,OAAO,GAAG,QAAQ,GAAG,OAAO,GAAG,UAAU,CAAC;AAEjE,eAAO,MAAM,UAAU,EAAE,MAAM,CAAC,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CA4DzE,CAAC;AAEF,0CAA0C;AAC1C,wBAAgB,OAAO,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,UAAQ,GAAG,QAAQ,CAKtE;AAED,0FAA0F;AAC1F,wBAAgB,QAAQ,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,UAAQ,GAAG,SAAS,CAAC,MAAM,EAAE,MAAM,CAAC,CAGtG;AAiDD,UAAU,UAAU;IAAG,KAAK,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,OAAO,CAAA;CAAE;AAetG;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,UAAU,EAAE,EAAE,KAAK,EAAE,MAAM,GAAG,UAAU,EAAE,CAWlF;AAogED,wBAAsB,cAAc,CAAC,MAAM,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAKvE"}
1
+ {"version":3,"file":"ink-chat.d.ts","sourceRoot":"","sources":["../src/ink-chat.tsx"],"names":[],"mappings":"AAoCA,OAAO,KAAK,EAAE,WAAW,EAAiB,MAAM,YAAY,CAAC;AAgE7D,6FAA6F;AAC7F,wBAAgB,UAAU,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,CAM7C;AAqED;;;;;;;;;;;GAWG;AACH,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,CAGnE;AAuBD;;;;;;;;;;;GAWG;AACH,MAAM,MAAM,QAAQ,GAAG,OAAO,GAAG,QAAQ,GAAG,OAAO,GAAG,UAAU,CAAC;AAEjE,eAAO,MAAM,UAAU,EAAE,MAAM,CAAC,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CA4DzE,CAAC;AAEF,0CAA0C;AAC1C,wBAAgB,OAAO,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,UAAQ,GAAG,QAAQ,CAKtE;AAED,0FAA0F;AAC1F,wBAAgB,QAAQ,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,UAAQ,GAAG,SAAS,CAAC,MAAM,EAAE,MAAM,CAAC,CAGtG;AAiDD,UAAU,UAAU;IAAG,KAAK,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,OAAO,CAAA;CAAE;AAUtG,qDAAqD;AACrD,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAE/D;AAED;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,MAAM,EAAE,GAAG,MAAM,CAK7E;AAeD;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,UAAU,EAAE,EAAE,KAAK,EAAE,MAAM,GAAG,UAAU,EAAE,CAWlF;AA0qED,wBAAsB,cAAc,CAAC,MAAM,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAKvE"}
package/dist/ink-chat.js CHANGED
@@ -10,8 +10,9 @@ import { loadKoneckConfig, saveKoneckConfig, setConfigKey, configKeyDescriptions
10
10
  import { PROVIDERS, resolveProvider, getApiKey, addUserProvider, userProvidersPath } from './providers.js';
11
11
  import { listCheckpoints, revertCheckpoint, snapshotTree, filesTouchedBy } from './checkpoint.js';
12
12
  import { fileDiff, diffSummary } from './diff-view.js';
13
+ import { listWorkspaceEntries, filterEntries, activeReference } from './workspace-files.js';
13
14
  import { bar, humanTokens, humanDuration, diffStatSince, modelInfoFrom, contextWindowFor, recordStatus, classifyResponse, stateLabel, readModelStatus, computeUiWidth, } from './usage.js';
14
- import { Markdown, Panel, panelWidth, safeCommitPoint } from './markdown-ink.js';
15
+ import { Markdown, Panel, panelWidth, safeCommitPoint, displayWidth, sliceToWidth } from './markdown-ink.js';
15
16
  import { fetchUpdateStatus, updateCommand } from './update-check.js';
16
17
  import { readFileSync, statSync } from 'fs';
17
18
  import { fileURLToPath } from 'url';
@@ -94,6 +95,24 @@ function fmtTokens(n) {
94
95
  * Greedy word wrap. The user's message is painted as a filled bar, so each line has to be
95
96
  * padded to the same width — a background colour only covers the characters actually drawn.
96
97
  */
98
+ /**
99
+ * Pads a string to an exact number of terminal cells, clipping it if it overflows.
100
+ *
101
+ * `padEnd` counts code units, so one emoji in a line made the padding a cell too long and pushed
102
+ * the right edge of a highlighted bar past where it was drawn. Measuring cells keeps the block
103
+ * rectangular whatever it contains.
104
+ */
105
+ function fitCells(text, width, ellipsis = false) {
106
+ const w = displayWidth(text);
107
+ if (w === width)
108
+ return text;
109
+ if (w < width)
110
+ return text + ' '.repeat(width - w);
111
+ if (!ellipsis)
112
+ return sliceToWidth(text, width).text;
113
+ const head = sliceToWidth(text, Math.max(0, width - 3));
114
+ return head.text + '.'.repeat(Math.min(3, width)) + ' '.repeat(Math.max(0, width - head.width - 3));
115
+ }
97
116
  function wrapToWidth(text, width) {
98
117
  const out = [];
99
118
  for (const paragraph of text.split('\n')) {
@@ -106,7 +125,7 @@ function wrapToWidth(text, width) {
106
125
  if (line === '') {
107
126
  line = word;
108
127
  }
109
- else if (line.length + 1 + word.length <= width) {
128
+ else if (displayWidth(line) + 1 + displayWidth(word) <= width) {
110
129
  line += ' ' + word;
111
130
  }
112
131
  else {
@@ -292,6 +311,30 @@ const EFFORT_BLURB = {
292
311
  high: 'Thorough — covers edge cases, costs more tokens',
293
312
  max: 'Most rigorous analysis available; slowest and priciest',
294
313
  };
314
+ // Bracketed paste. A terminal that has it enabled wraps pasted text in these markers, which is
315
+ // the only reliable way to tell a paste from very fast typing. Ink strips the leading escape of
316
+ // the opening marker before handing input to useInput, so the literal it sees is "[200~".
317
+ const PASTE_ON = '\u001b[?2004h';
318
+ const PASTE_OFF = '\u001b[?2004l';
319
+ const PASTE_BEGIN = '[200~';
320
+ const PASTE_END = '\u001b[201~';
321
+ /** How a held-back paste appears in the composer. */
322
+ export function pasteToken(index, lines) {
323
+ return `[Pasted text #${index} +${lines} lines]`;
324
+ }
325
+ /**
326
+ * Puts held-back pastes back into the text before it is sent.
327
+ *
328
+ * The token is what the user sees; the model must receive what they actually copied. Anything
329
+ * typed around a token is preserved in place, so a paste can be introduced and followed by a
330
+ * question in one prompt.
331
+ */
332
+ export function expandPastes(draft, pastes) {
333
+ return draft.replace(/\[Pasted text #(\d+) \+\d+ lines\]/g, (whole, n) => {
334
+ const text = pastes[Number(n) - 1];
335
+ return text === undefined ? whole : text;
336
+ });
337
+ }
295
338
  /** Past this, a file is not read for diffing — the render would be useless anyway. */
296
339
  const MAX_DIFF_BYTES = 512 * 1024;
297
340
  /** One tool rarely touches more; the cap stops a bulk move from flooding scrollback. */
@@ -376,6 +419,22 @@ function App({ config: initialConfig }) {
376
419
  const streamedRef = useRef('');
377
420
  /** Frames received before any content — proof the provider has the request and is working. */
378
421
  const [providerFrames, setProviderFrames] = useState(0);
422
+ /**
423
+ * Text pasted into the composer, held aside so the prompt stays readable.
424
+ *
425
+ * A thirty-line paste in the input box buries whatever the user meant to type around it, and the
426
+ * paragraph structure is lost the moment it is folded into one wrapped line. Each paste is kept
427
+ * verbatim here and stands in the draft as a short token, which is expanded again on submit — so
428
+ * the model receives exactly what was copied, blank lines and all.
429
+ */
430
+ const pastesRef = useRef([]);
431
+ const [pasteCount, setPasteCount] = useState(0);
432
+ /** Accumulates a paste that spans more than one stdin chunk. */
433
+ const pasteBufRef = useRef(null);
434
+ /** Workspace paths for the `@` picker, read once and reused. */
435
+ const entriesRef = useRef(null);
436
+ /** Where the `@` being completed starts in the draft. */
437
+ const atStartRef = useRef(-1);
379
438
  /** Pre-edit contents of the files the running tool said it would touch. */
380
439
  const pendingDiffRef = useRef([]);
381
440
  const charsRef = useRef(0); // streamed chars, for the live token estimate
@@ -687,6 +746,14 @@ function App({ config: initialConfig }) {
687
746
  }, 150);
688
747
  return () => clearInterval(id);
689
748
  }, [busy]);
749
+ // Ask the terminal to bracket pasted text. Without this a paste is indistinguishable from
750
+ // typing, and there is no way to keep it out of the composer or preserve its line breaks.
751
+ useEffect(() => {
752
+ process.stdout.write(PASTE_ON);
753
+ const off = () => { process.stdout.write(PASTE_OFF); };
754
+ process.on('exit', off);
755
+ return () => { off(); process.off('exit', off); };
756
+ }, []);
690
757
  // The tree as it stood before the session touched anything. Recorded once, so "code changes"
691
758
  // stays a comparison against the starting point however long the session runs.
692
759
  useEffect(() => { void snapshotTree(cfg.cwd).then(t => { baseTreeRef.current = t; }); }, [cfg.cwd]);
@@ -872,6 +939,30 @@ function App({ config: initialConfig }) {
872
939
  current: p.name === cfg.provider,
873
940
  }));
874
941
  }
942
+ /**
943
+ * Offers workspace paths for an `@` reference. The listing is read on first use rather than at
944
+ * startup, so a session in a large repository is not delayed by a scan it may never need.
945
+ */
946
+ async function openFilePicker(query) {
947
+ if (entriesRef.current === null)
948
+ entriesRef.current = await listWorkspaceEntries(cfg.cwd);
949
+ const all = entriesRef.current;
950
+ if (all.length === 0) {
951
+ addSystem('No files found to reference here.');
952
+ return;
953
+ }
954
+ setPicker({ kind: 'file', items: fileItems(all, query) });
955
+ setPickQuery(query);
956
+ setPickIndex(0);
957
+ }
958
+ /** The picker rows for a query, ranked by how well each path matches. */
959
+ function fileItems(all, query) {
960
+ return filterEntries(all, query, 200).map(entry => ({
961
+ value: entry,
962
+ label: entry,
963
+ desc: entry.endsWith('/') ? 'directory' : '',
964
+ }));
965
+ }
875
966
  function openPicker(kind, items, query = '') {
876
967
  setPicker({ kind, items });
877
968
  setPickQuery(query);
@@ -886,6 +977,18 @@ function App({ config: initialConfig }) {
886
977
  async function choosePick(item, kindOverride) {
887
978
  const kind = kindOverride ?? picker?.kind;
888
979
  closePicker();
980
+ if (kind === 'file') {
981
+ // Replace the partial "@query" with the full path, leaving a trailing space so the next
982
+ // word can be typed straight away. A directory keeps its slash so more can be typed after.
983
+ const start = atStartRef.current;
984
+ atStartRef.current = -1;
985
+ if (start >= 0) {
986
+ const suffix = item.value.endsWith('/') ? '' : ' ';
987
+ setDraft(d => d.slice(0, start) + '@' + item.value + suffix + d.slice(caret));
988
+ setCaret(start + 1 + item.value.length + suffix.length);
989
+ }
990
+ return;
991
+ }
889
992
  if (kind === 'command') {
890
993
  // Leave it in the composer so arguments can still be typed before submitting.
891
994
  setDraft(item.value + ' ');
@@ -967,7 +1070,13 @@ function App({ config: initialConfig }) {
967
1070
  const cost = estimateCost(cfg.model, stats.promptTokens, stats.completionTokens);
968
1071
  const costStr = cost.known ? formatCost(cost.total) : 'unavailable';
969
1072
  const ws = cfg.cwd.replace(process.env['HOME'] ?? '', '~');
1073
+ // The endpoint is resolved from four places — an explicit override, an env var, the
1074
+ // config file, then the built-in default — so showing the name alone leaves the one
1075
+ // question that actually matters when a provider misbehaves unanswered: where did the
1076
+ // request go? Working that out took a proxy and half an hour.
1077
+ const endpoint = resolveProvider(cfg.provider, cfg.baseURL).baseURL;
970
1078
  addSystem(`Provider : ${cfg.provider}\n` +
1079
+ `Endpoint : ${endpoint}\n` +
971
1080
  `Model : ${cfg.model}\n` +
972
1081
  `Mode : ${mode} effort: ${effort}\n` +
973
1082
  `Approval : ${cfg.requireApproval ? 'on' : 'off'}\n` +
@@ -1424,7 +1533,53 @@ function App({ config: initialConfig }) {
1424
1533
  addSystem(`Unknown command: ${cmd}. Type /help for the full list.`);
1425
1534
  }
1426
1535
  }
1536
+ /**
1537
+ * Files a completed paste and puts a token in the composer.
1538
+ *
1539
+ * A short single-line paste is inserted literally — turning "npm test" into a token would be
1540
+ * obstructive. Anything with a line break is held back, because that is the case where the text
1541
+ * would both swamp the composer and lose its structure.
1542
+ */
1543
+ function filePaste(text) {
1544
+ const clean = text.replace(/\r\n?/g, '\n');
1545
+ const lines = clean.split('\n').length;
1546
+ if (!clean.includes('\n') && clean.length <= 200) {
1547
+ setDraft(d => d.slice(0, caret) + clean + d.slice(caret));
1548
+ setCaret(c => c + clean.length);
1549
+ return;
1550
+ }
1551
+ pastesRef.current = [...pastesRef.current, clean];
1552
+ const token = pasteToken(pastesRef.current.length, lines);
1553
+ setPasteCount(pastesRef.current.length);
1554
+ setDraft(d => d.slice(0, caret) + token + d.slice(caret));
1555
+ setCaret(c => c + token.length);
1556
+ }
1427
1557
  useInput((input, key) => {
1558
+ // ── Bracketed paste ─────────────────────────────────────────────────────
1559
+ // This runs before every other branch. A paste carries newlines, and any handler that treats
1560
+ // a newline as "submit" would fire partway through one.
1561
+ if (pasteBufRef.current !== null) {
1562
+ const end = input.indexOf(PASTE_END);
1563
+ if (end === -1) {
1564
+ pasteBufRef.current += input;
1565
+ return;
1566
+ }
1567
+ const whole = pasteBufRef.current + input.slice(0, end);
1568
+ pasteBufRef.current = null;
1569
+ filePaste(whole);
1570
+ return;
1571
+ }
1572
+ const begin = input.indexOf(PASTE_BEGIN);
1573
+ if (begin !== -1) {
1574
+ const after = input.slice(begin + PASTE_BEGIN.length);
1575
+ const end = after.indexOf(PASTE_END);
1576
+ if (end === -1) {
1577
+ pasteBufRef.current = after;
1578
+ return;
1579
+ } // more chunks to come
1580
+ filePaste(after.slice(0, end));
1581
+ return;
1582
+ }
1428
1583
  // Esc abandons the turn in flight and hands the keyboard straight back. The composer is
1429
1584
  // deliberately left alone: cancelling should not cost whatever was typed while waiting.
1430
1585
  if (key.escape && busy && !picker && !keyPrompt && !updatePane && !usagePane) {
@@ -1470,6 +1625,8 @@ function App({ config: initialConfig }) {
1470
1625
  setDraft('');
1471
1626
  setCaret(0);
1472
1627
  }
1628
+ if (picker.kind === 'file')
1629
+ atStartRef.current = -1;
1473
1630
  closePicker();
1474
1631
  return;
1475
1632
  }
@@ -1502,6 +1659,21 @@ function App({ config: initialConfig }) {
1502
1659
  setCaret(0);
1503
1660
  return;
1504
1661
  }
1662
+ if (picker.kind === 'file') {
1663
+ // Deleting the "@" itself is how the picker is dismissed without choosing anything.
1664
+ setDraft(d => d.slice(0, Math.max(0, caret - 1)) + d.slice(caret));
1665
+ setCaret(c => Math.max(0, c - 1));
1666
+ if (pickQuery === '') {
1667
+ closePicker();
1668
+ atStartRef.current = -1;
1669
+ return;
1670
+ }
1671
+ const q = pickQuery.slice(0, -1);
1672
+ setPickQuery(q);
1673
+ setPickIndex(0);
1674
+ setPicker(p => (p ? { ...p, items: fileItems(entriesRef.current ?? [], q) } : p));
1675
+ return;
1676
+ }
1505
1677
  setPickQuery(q => q.slice(0, -1));
1506
1678
  setPickIndex(0);
1507
1679
  if (picker.kind === 'command') {
@@ -1514,6 +1686,13 @@ function App({ config: initialConfig }) {
1514
1686
  // A space means the command name is finished and arguments follow, so the palette
1515
1687
  // steps out of the way. Keeping it open sent "model gpt-4o" into the filter, emptied
1516
1688
  // the list, and left Enter with nothing to select.
1689
+ if (input === ' ' && picker.kind === 'file') {
1690
+ closePicker();
1691
+ atStartRef.current = -1;
1692
+ setDraft(d => d.slice(0, caret) + ' ' + d.slice(caret));
1693
+ setCaret(c => c + 1);
1694
+ return;
1695
+ }
1517
1696
  if (input === ' ' && picker.kind === 'command') {
1518
1697
  closePicker();
1519
1698
  setDraft(d => { const t = d.endsWith(' ') ? d : d + ' '; setCaret(t.length); return t; });
@@ -1527,6 +1706,12 @@ function App({ config: initialConfig }) {
1527
1706
  setDraft(t);
1528
1707
  setCaret(t.length);
1529
1708
  }
1709
+ if (picker.kind === 'file') {
1710
+ const q = pickQuery + input;
1711
+ setPicker(p => (p ? { ...p, items: fileItems(entriesRef.current ?? [], q) } : p));
1712
+ setDraft(d => d.slice(0, caret) + input + d.slice(caret));
1713
+ setCaret(c => c + input.length);
1714
+ }
1530
1715
  return;
1531
1716
  }
1532
1717
  return;
@@ -1667,8 +1852,19 @@ function App({ config: initialConfig }) {
1667
1852
  const text = input.replace(/[\u0000-\u0008\u000b-\u001f\u007f]/g, '');
1668
1853
  if (text === '')
1669
1854
  return;
1670
- setDraft(d => d.slice(0, caret) + text + d.slice(caret));
1671
- setCaret(c => c + text.length);
1855
+ const next = draft.slice(0, caret) + text + draft.slice(caret);
1856
+ const nextCaret = caret + text.length;
1857
+ setDraft(next);
1858
+ setCaret(nextCaret);
1859
+ // A lone "@" at a word boundary offers workspace paths. Checking the draft as it will be,
1860
+ // rather than the character typed, means it also fires when "@" arrives mid-chunk.
1861
+ if (text.endsWith('@')) {
1862
+ const ref = activeReference(next, nextCaret);
1863
+ if (ref) {
1864
+ atStartRef.current = ref.start;
1865
+ void openFilePicker(ref.query);
1866
+ }
1867
+ }
1672
1868
  }
1673
1869
  });
1674
1870
  /** Start of the word at or before `i`, for word-wise movement and deletion. */
@@ -1692,12 +1888,20 @@ function App({ config: initialConfig }) {
1692
1888
  /** Sends the composer contents: a slash command, or a task for the agent. */
1693
1889
  function submitDraft(raw) {
1694
1890
  {
1695
- const task = raw.trim();
1891
+ // Tokens are expanded here, at the last moment. The model gets the text as it was copied —
1892
+ // blank lines, indentation and all — while the transcript shows the compact form so a
1893
+ // thirty-line paste does not bury the question asked about it.
1894
+ const shown = raw.trim();
1895
+ if (!shown)
1896
+ return;
1897
+ const task = expandPastes(shown, pastesRef.current).trim();
1696
1898
  if (!task)
1697
1899
  return;
1698
1900
  setDraft('');
1699
1901
  setCaret(0);
1700
- addRow({ role: 'user', text: task });
1902
+ pastesRef.current = [];
1903
+ setPasteCount(0);
1904
+ addRow({ role: 'user', text: shown });
1701
1905
  if (task.startsWith('/')) {
1702
1906
  const spaceIdx = task.indexOf(' ');
1703
1907
  const cmd = spaceIdx === -1 ? task : task.slice(0, spaceIdx);
@@ -1864,7 +2068,7 @@ function App({ config: initialConfig }) {
1864
2068
  const bg = l.kind === 'add' ? DIFF_ADD_BG : l.kind === 'del' ? DIFF_DEL_BG : undefined;
1865
2069
  // Tabs would make the tinted block a ragged width, so they are expanded.
1866
2070
  const body = `${sign} ${l.text}`.replace(/\t/g, ' ');
1867
- const cell = body.length > width ? body.slice(0, width - 3) + '...' : body.padEnd(width);
2071
+ const cell = fitCells(body, width, true);
1868
2072
  return (_jsxs(Box, { children: [_jsxs(Text, { color: DIM, children: [String(shown ?? '').padStart(gutter), " "] }), _jsx(Text, { color: fg, ...(bg ? { backgroundColor: bg } : {}), children: cell })] }, li));
1869
2073
  })] }, hi))), d.hiddenLines > 0 && (_jsxs(Text, { color: DIM, children: [' '.repeat(gutter), " ... ", d.hiddenLines, " more changed lines"] }))] }, key));
1870
2074
  };
@@ -1881,7 +2085,7 @@ function App({ config: initialConfig }) {
1881
2085
  return (_jsxs(Box, { flexDirection: "column", width: uiWidth, paddingX: 2, children: [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: rows, children: (row, index) => row.role === 'header' ? (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { flexDirection: "column", alignItems: "center", marginTop: 1, marginBottom: 1, children: [_jsx(Text, { color: CYAN, bold: true, children: "KONECK / SOFTWARE DELIVERY SYSTEM" }), _jsx(Text, { color: MUTED, children: "Deliberate engineering, with control at every change." })] }), _jsx(Panel, { width: barWidth, color: CYAN, title: "KONECK", children: [`MODEL: \`${modelShort}\` PROVIDER: \`${cfg.provider}\` WORKSPACE: \`${workspace}\``] })] }, index)) : row.role === 'user' ? (
1882
2086
  // Painted as a filled bar so the user's own words are the most findable thing
1883
2087
  // on screen when scrolling back through a long session.
1884
- _jsx(Box, { marginTop: 1, flexDirection: "column", children: wrapToWidth(row.text ?? '', barWidth - 2).map((line, li) => (_jsx(Text, { backgroundColor: USER_BG, color: USER_FG, bold: li === 0, children: (li === 0 ? '❯ ' : ' ') + line.padEnd(barWidth - 2) }, li))) }, index)) : row.role === 'error' ? (() => {
2088
+ _jsx(Box, { marginTop: 1, flexDirection: "column", children: wrapToWidth(row.text ?? '', barWidth - 2).map((line, li) => (_jsx(Text, { backgroundColor: USER_BG, color: USER_FG, bold: li === 0, children: (li === 0 ? '❯ ' : ' ') + fitCells(line, barWidth - 2) }, li))) }, index)) : row.role === 'error' ? (() => {
1885
2089
  const lines = [row.text ?? '', '', 'Fix the above, then retry. /status shows the active provider and model.'];
1886
2090
  return (_jsx(Box, { marginTop: 1, children: _jsx(Panel, { width: panelWidth(lines, barWidth, 40), color: CRIMSON, title: "[!] ERROR", children: lines }) }, index));
1887
2091
  })() : row.role === 'system' ? (() => {
@@ -1908,7 +2112,7 @@ function App({ config: initialConfig }) {
1908
2112
  : a.status === 'failed' ? CRIMSON : GREEN;
1909
2113
  const took = (a.endedAt ?? Date.now()) - a.startedAt;
1910
2114
  const task = a.task.length > taskWidth ? a.task.slice(0, taskWidth - 3) + '...' : a.task;
1911
- return (_jsxs(Box, { paddingLeft: 2, gap: 1, children: [_jsx(Text, { color: color, children: glyph }), _jsx(Text, { color: DIM, children: String(a.index + 1).padStart(2) }), _jsx(Text, { color: a.status === 'running' ? INK : MUTED, children: task.padEnd(taskWidth) }), _jsx(Text, { color: DIM, children: humanTokens(a.tokens).padStart(6) }), _jsx(Text, { color: DIM, children: fmtElapsed(took).padStart(6) })] }, a.index));
2115
+ return (_jsxs(Box, { paddingLeft: 2, gap: 1, children: [_jsx(Text, { color: color, children: glyph }), _jsx(Text, { color: DIM, children: String(a.index + 1).padStart(2) }), _jsx(Text, { color: a.status === 'running' ? INK : MUTED, children: fitCells(task, taskWidth) }), _jsx(Text, { color: DIM, children: humanTokens(a.tokens).padStart(6) }), _jsx(Text, { color: DIM, children: fmtElapsed(took).padStart(6) })] }, a.index));
1912
2116
  })] }));
1913
2117
  })(), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { children: [_jsxs(Text, { color: GREEN, children: [SPINNER[spinFrame], " "] }), _jsx(Shimmer, { text: workWord(elapsedMs, spinWord, turnRecoveredRef.current)[0], frame: shimmerFrame, base: MUTED }), _jsx(Text, { color: MUTED, children: "\u2026 " }), _jsxs(Text, { color: DIM, children: ["(", fmtElapsed(elapsedMs), " | ", liveTokens > 0
1914
2118
  ? `${fmtTokens(liveTokens)} tokens`
@@ -2083,28 +2287,31 @@ function App({ config: initialConfig }) {
2083
2287
  : picker.kind === 'model' ? `Models - ${cfg.provider}`
2084
2288
  : picker.kind === 'effort' ? 'Reasoning effort'
2085
2289
  : picker.kind === 'resume' ? 'Resume a saved session'
2086
- : 'Connect provider';
2290
+ : picker.kind === 'file' ? 'Reference a file or directory'
2291
+ : 'Connect provider';
2087
2292
  // The palette is sized to the rows it is showing rather than to the terminal. On a wide
2088
2293
  // screen a list of short model names in a full-width box is mostly empty box.
2089
- const headWidth = title.length + 24;
2294
+ // The header is a title on the left and a count on the right, on one line. Estimating the
2295
+ // right-hand side at a constant made "Reference a file or directory" wrap onto two rows.
2296
+ const countText = `${list.length} match${list.length === 1 ? '' : 'es'} · esc to close`;
2297
+ const headWidth = title.length + countText.length + 3;
2090
2298
  const rowWidth = Math.max(headWidth, ...shown.map(it => 38 + (it.current ? 10 : 0) + it.desc.length));
2091
2299
  const boxWidth = Math.max(40, Math.min(barWidth, rowWidth + 4));
2092
2300
  const width = boxWidth - 4;
2093
2301
  let lastGroup;
2094
- return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: CYAN, paddingX: 1, marginTop: 1, width: boxWidth, children: [_jsxs(Box, { justifyContent: "space-between", children: [_jsx(Text, { color: CYAN, bold: true, children: title }), _jsxs(Text, { color: DIM, children: [list.length, " match", list.length === 1 ? '' : 'es', " \u00B7 esc to close"] })] }), list.length === 0 && _jsxs(Text, { color: MUTED, children: ["No match for \"", pickQuery, "\""] }), shown.map((item, i) => {
2302
+ return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: CYAN, paddingX: 1, marginTop: 1, width: boxWidth, children: [_jsxs(Box, { justifyContent: "space-between", children: [_jsx(Text, { color: CYAN, bold: true, children: title }), _jsx(Text, { color: DIM, children: countText })] }), list.length === 0 && _jsxs(Text, { color: MUTED, children: ["No match for \"", pickQuery, "\""] }), shown.map((item, i) => {
2095
2303
  const absolute = Math.max(0, start) + i;
2096
2304
  const selected = absolute === pickIndex;
2097
2305
  const header = item.group && item.group !== lastGroup ? item.group : null;
2098
2306
  lastGroup = item.group;
2099
2307
  const label = item.label.length > 34 ? item.label.slice(0, 31) + '...' : item.label;
2100
- return (_jsxs(Box, { flexDirection: "column", children: [header && _jsx(Text, { color: DIM, children: header }), _jsx(Text, { backgroundColor: selected ? CYAN : undefined, color: selected ? '#10222A' : INK, bold: selected, children: (`${selected ? '❯ ' : ' '}${label.padEnd(36)}${item.current ? '(current) ' : ''}${item.desc}`)
2101
- .slice(0, width).padEnd(width) })] }, item.value + absolute));
2308
+ return (_jsxs(Box, { flexDirection: "column", children: [header && _jsx(Text, { color: DIM, children: header }), _jsx(Text, { backgroundColor: selected ? CYAN : undefined, color: selected ? '#10222A' : INK, bold: selected, children: fitCells(`${selected ? '❯ ' : ' '}${fitCells(label, 36)}${item.current ? '(current) ' : ''}${item.desc}`, width) })] }, item.value + absolute));
2102
2309
  }), list.length > PICKER_ROWS && (_jsxs(Text, { color: DIM, children: ["\u2191\u2193 navigate \u00B7 enter select \u00B7 showing ", Math.max(0, start) + 1, "\u2013", Math.max(0, start) + shown.length, " of ", list.length] }))] }));
2103
2310
  })(), 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. Held in memory for this session only; esc to cancel." }), _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(Text, { color: CYAN, children: "\u2588" })] })] })), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: CYAN, bold: true, children: "\u276F " }), _jsx(Text, { color: INK, children: draft.slice(0, caret) }), busy
2104
2311
  ? _jsx(Text, { color: INK, children: draft.slice(caret) })
2105
2312
  : caret < draft.length
2106
2313
  ? _jsxs(_Fragment, { children: [_jsx(Text, { backgroundColor: CYAN, color: "#10222A", children: draft[caret] }), _jsx(Text, { color: INK, children: draft.slice(caret + 1) })] })
2107
- : _jsx(Text, { color: CYAN, children: "\u2588" })] }), _jsx(Box, { marginTop: 1, paddingX: 1, children: _jsxs(Text, { wrap: "truncate", backgroundColor: "#24343B", children: [_jsx(Text, { color: MUTED, children: "TAB " }), _jsx(Text, { color: INK, children: "Analytics" }), _jsx(Text, { color: MUTED, children: " / " }), _jsx(Text, { color: CYAN, children: "/HELP" }), _jsx(Text, { color: INK, children: " Commands" }), _jsx(Text, { color: MUTED, children: " / SHIFT+TAB " }), _jsx(Text, { color: INK, children: "Mode" }), _jsx(Text, { color: MUTED, children: " / " }), _jsx(Text, { color: statusColor, children: statusLabel.toLowerCase() }), _jsx(Text, { color: MUTED, children: " / MODE: " }), _jsx(Text, { color: INK, children: mode }), _jsx(Text, { color: MUTED, children: " / EFFORT: " }), _jsx(Text, { color: effort === 'medium' ? INK : AMBER, children: effort }), _jsx(Text, { color: MUTED, children: " / MODEL: " }), _jsx(Text, { color: INK, children: modelShort }), _jsx(Text, { color: MUTED, children: " / v" }), _jsx(Text, { color: updatePane?.behind ? AMBER : INK, children: VERSION }), updatePane?.behind && _jsx(Text, { color: AMBER, children: " \u2191" })] }) })] }));
2314
+ : _jsx(Text, { color: CYAN, children: "\u2588" })] }), _jsx(Box, { marginTop: 1, paddingX: 1, children: _jsxs(Text, { wrap: "truncate", backgroundColor: "#24343B", children: [_jsx(Text, { color: MUTED, children: "TAB " }), _jsx(Text, { color: INK, children: "Analytics" }), _jsx(Text, { color: MUTED, children: " / " }), _jsx(Text, { color: CYAN, children: "/HELP" }), _jsx(Text, { color: INK, children: " Commands" }), _jsx(Text, { color: MUTED, children: " / SHIFT+TAB " }), _jsx(Text, { color: INK, children: "Mode" }), _jsx(Text, { color: MUTED, children: " / " }), _jsx(Text, { color: statusColor, children: statusLabel.toLowerCase() }), _jsx(Text, { color: MUTED, children: " / MODE: " }), _jsx(Text, { color: INK, children: mode }), _jsx(Text, { color: MUTED, children: " / EFFORT: " }), _jsx(Text, { color: effort === 'medium' ? INK : AMBER, children: effort }), _jsx(Text, { color: MUTED, children: " / MODEL: " }), _jsx(Text, { color: INK, children: modelShort }), _jsx(Text, { color: MUTED, children: " / v" }), _jsx(Text, { color: updatePane?.behind ? AMBER : INK, children: VERSION }), updatePane?.behind && _jsx(Text, { color: AMBER, children: " \u2191" }), pasteCount > 0 && (_jsxs(_Fragment, { children: [_jsx(Text, { color: MUTED, children: " / " }), _jsxs(Text, { color: AMBER, children: [pasteCount, " paste", pasteCount === 1 ? '' : 's', " held, sent in full"] })] }))] }) })] }));
2108
2315
  }
2109
2316
  export async function runInkChatMode(config) {
2110
2317
  // resolveConfig has already layered CLI flags above the /config store, so the config