koneck 2.25.6 → 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.
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +20 -3
- package/dist/engine.js.map +1 -1
- package/dist/ink-chat.d.ts +10 -0
- package/dist/ink-chat.d.ts.map +1 -1
- package/dist/ink-chat.js +232 -16
- package/dist/ink-chat.js.map +1 -1
- package/dist/markdown-ink.d.ts +24 -0
- package/dist/markdown-ink.d.ts.map +1 -1
- package/dist/markdown-ink.js +54 -8
- package/dist/markdown-ink.js.map +1 -1
- package/dist/types.d.ts +9 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/usage.d.ts +7 -0
- package/dist/usage.d.ts.map +1 -1
- package/dist/usage.js +17 -2
- package/dist/usage.js.map +1 -1
- package/dist/workspace-files.d.ts +39 -0
- package/dist/workspace-files.d.ts.map +1 -0
- package/dist/workspace-files.js +115 -0
- package/dist/workspace-files.js.map +1 -0
- package/package.json +2 -1
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
|
|
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. */
|
|
@@ -374,6 +417,24 @@ function App({ config: initialConfig }) {
|
|
|
374
417
|
const replyStartedRef = useRef(false);
|
|
375
418
|
/** Everything streamed this turn. Empty means the provider did not stream at all. */
|
|
376
419
|
const streamedRef = useRef('');
|
|
420
|
+
/** Frames received before any content — proof the provider has the request and is working. */
|
|
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);
|
|
377
438
|
/** Pre-edit contents of the files the running tool said it would touch. */
|
|
378
439
|
const pendingDiffRef = useRef([]);
|
|
379
440
|
const charsRef = useRef(0); // streamed chars, for the live token estimate
|
|
@@ -486,6 +547,7 @@ function App({ config: initialConfig }) {
|
|
|
486
547
|
onChunk: (text) => pushSay(text),
|
|
487
548
|
onToolCall: (name, args) => pushTool(name, args),
|
|
488
549
|
onToolResult: (name, ok, ms) => finishTool(name, ok, ms),
|
|
550
|
+
onStreamActivity: (frames) => setProviderFrames(frames),
|
|
489
551
|
onAgentProgress: (list) => { agentsRef.current = list; setAgents(list); },
|
|
490
552
|
onTokens: (s) => {
|
|
491
553
|
realTokRef.current = Math.max(0, s.completionTokens - tokBaseRef.current);
|
|
@@ -684,6 +746,14 @@ function App({ config: initialConfig }) {
|
|
|
684
746
|
}, 150);
|
|
685
747
|
return () => clearInterval(id);
|
|
686
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
|
+
}, []);
|
|
687
757
|
// The tree as it stood before the session touched anything. Recorded once, so "code changes"
|
|
688
758
|
// stays a comparison against the starting point however long the session runs.
|
|
689
759
|
useEffect(() => { void snapshotTree(cfg.cwd).then(t => { baseTreeRef.current = t; }); }, [cfg.cwd]);
|
|
@@ -869,6 +939,30 @@ function App({ config: initialConfig }) {
|
|
|
869
939
|
current: p.name === cfg.provider,
|
|
870
940
|
}));
|
|
871
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
|
+
}
|
|
872
966
|
function openPicker(kind, items, query = '') {
|
|
873
967
|
setPicker({ kind, items });
|
|
874
968
|
setPickQuery(query);
|
|
@@ -883,6 +977,18 @@ function App({ config: initialConfig }) {
|
|
|
883
977
|
async function choosePick(item, kindOverride) {
|
|
884
978
|
const kind = kindOverride ?? picker?.kind;
|
|
885
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
|
+
}
|
|
886
992
|
if (kind === 'command') {
|
|
887
993
|
// Leave it in the composer so arguments can still be typed before submitting.
|
|
888
994
|
setDraft(item.value + ' ');
|
|
@@ -964,7 +1070,13 @@ function App({ config: initialConfig }) {
|
|
|
964
1070
|
const cost = estimateCost(cfg.model, stats.promptTokens, stats.completionTokens);
|
|
965
1071
|
const costStr = cost.known ? formatCost(cost.total) : 'unavailable';
|
|
966
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;
|
|
967
1078
|
addSystem(`Provider : ${cfg.provider}\n` +
|
|
1079
|
+
`Endpoint : ${endpoint}\n` +
|
|
968
1080
|
`Model : ${cfg.model}\n` +
|
|
969
1081
|
`Mode : ${mode} effort: ${effort}\n` +
|
|
970
1082
|
`Approval : ${cfg.requireApproval ? 'on' : 'off'}\n` +
|
|
@@ -1421,7 +1533,53 @@ function App({ config: initialConfig }) {
|
|
|
1421
1533
|
addSystem(`Unknown command: ${cmd}. Type /help for the full list.`);
|
|
1422
1534
|
}
|
|
1423
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
|
+
}
|
|
1424
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
|
+
}
|
|
1425
1583
|
// Esc abandons the turn in flight and hands the keyboard straight back. The composer is
|
|
1426
1584
|
// deliberately left alone: cancelling should not cost whatever was typed while waiting.
|
|
1427
1585
|
if (key.escape && busy && !picker && !keyPrompt && !updatePane && !usagePane) {
|
|
@@ -1467,6 +1625,8 @@ function App({ config: initialConfig }) {
|
|
|
1467
1625
|
setDraft('');
|
|
1468
1626
|
setCaret(0);
|
|
1469
1627
|
}
|
|
1628
|
+
if (picker.kind === 'file')
|
|
1629
|
+
atStartRef.current = -1;
|
|
1470
1630
|
closePicker();
|
|
1471
1631
|
return;
|
|
1472
1632
|
}
|
|
@@ -1499,6 +1659,21 @@ function App({ config: initialConfig }) {
|
|
|
1499
1659
|
setCaret(0);
|
|
1500
1660
|
return;
|
|
1501
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
|
+
}
|
|
1502
1677
|
setPickQuery(q => q.slice(0, -1));
|
|
1503
1678
|
setPickIndex(0);
|
|
1504
1679
|
if (picker.kind === 'command') {
|
|
@@ -1511,6 +1686,13 @@ function App({ config: initialConfig }) {
|
|
|
1511
1686
|
// A space means the command name is finished and arguments follow, so the palette
|
|
1512
1687
|
// steps out of the way. Keeping it open sent "model gpt-4o" into the filter, emptied
|
|
1513
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
|
+
}
|
|
1514
1696
|
if (input === ' ' && picker.kind === 'command') {
|
|
1515
1697
|
closePicker();
|
|
1516
1698
|
setDraft(d => { const t = d.endsWith(' ') ? d : d + ' '; setCaret(t.length); return t; });
|
|
@@ -1524,6 +1706,12 @@ function App({ config: initialConfig }) {
|
|
|
1524
1706
|
setDraft(t);
|
|
1525
1707
|
setCaret(t.length);
|
|
1526
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
|
+
}
|
|
1527
1715
|
return;
|
|
1528
1716
|
}
|
|
1529
1717
|
return;
|
|
@@ -1664,8 +1852,19 @@ function App({ config: initialConfig }) {
|
|
|
1664
1852
|
const text = input.replace(/[\u0000-\u0008\u000b-\u001f\u007f]/g, '');
|
|
1665
1853
|
if (text === '')
|
|
1666
1854
|
return;
|
|
1667
|
-
|
|
1668
|
-
|
|
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
|
+
}
|
|
1669
1868
|
}
|
|
1670
1869
|
});
|
|
1671
1870
|
/** Start of the word at or before `i`, for word-wise movement and deletion. */
|
|
@@ -1689,12 +1888,20 @@ function App({ config: initialConfig }) {
|
|
|
1689
1888
|
/** Sends the composer contents: a slash command, or a task for the agent. */
|
|
1690
1889
|
function submitDraft(raw) {
|
|
1691
1890
|
{
|
|
1692
|
-
|
|
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();
|
|
1693
1898
|
if (!task)
|
|
1694
1899
|
return;
|
|
1695
1900
|
setDraft('');
|
|
1696
1901
|
setCaret(0);
|
|
1697
|
-
|
|
1902
|
+
pastesRef.current = [];
|
|
1903
|
+
setPasteCount(0);
|
|
1904
|
+
addRow({ role: 'user', text: shown });
|
|
1698
1905
|
if (task.startsWith('/')) {
|
|
1699
1906
|
const spaceIdx = task.indexOf(' ');
|
|
1700
1907
|
const cmd = spaceIdx === -1 ? task : task.slice(0, spaceIdx);
|
|
@@ -1710,6 +1917,7 @@ function App({ config: initialConfig }) {
|
|
|
1710
1917
|
sayRef.current = '';
|
|
1711
1918
|
streamedRef.current = '';
|
|
1712
1919
|
replyStartedRef.current = false;
|
|
1920
|
+
setProviderFrames(0);
|
|
1713
1921
|
liveToolRef.current = null;
|
|
1714
1922
|
charsRef.current = 0;
|
|
1715
1923
|
realTokRef.current = 0;
|
|
@@ -1860,7 +2068,7 @@ function App({ config: initialConfig }) {
|
|
|
1860
2068
|
const bg = l.kind === 'add' ? DIFF_ADD_BG : l.kind === 'del' ? DIFF_DEL_BG : undefined;
|
|
1861
2069
|
// Tabs would make the tinted block a ragged width, so they are expanded.
|
|
1862
2070
|
const body = `${sign} ${l.text}`.replace(/\t/g, ' ');
|
|
1863
|
-
const cell = body
|
|
2071
|
+
const cell = fitCells(body, width, true);
|
|
1864
2072
|
return (_jsxs(Box, { children: [_jsxs(Text, { color: DIM, children: [String(shown ?? '').padStart(gutter), " "] }), _jsx(Text, { color: fg, ...(bg ? { backgroundColor: bg } : {}), children: cell })] }, li));
|
|
1865
2073
|
})] }, hi))), d.hiddenLines > 0 && (_jsxs(Text, { color: DIM, children: [' '.repeat(gutter), " ... ", d.hiddenLines, " more changed lines"] }))] }, key));
|
|
1866
2074
|
};
|
|
@@ -1877,7 +2085,7 @@ function App({ config: initialConfig }) {
|
|
|
1877
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' ? (
|
|
1878
2086
|
// Painted as a filled bar so the user's own words are the most findable thing
|
|
1879
2087
|
// on screen when scrolling back through a long session.
|
|
1880
|
-
_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
|
|
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' ? (() => {
|
|
1881
2089
|
const lines = [row.text ?? '', '', 'Fix the above, then retry. /status shows the active provider and model.'];
|
|
1882
2090
|
return (_jsx(Box, { marginTop: 1, children: _jsx(Panel, { width: panelWidth(lines, barWidth, 40), color: CRIMSON, title: "[!] ERROR", children: lines }) }, index));
|
|
1883
2091
|
})() : row.role === 'system' ? (() => {
|
|
@@ -1904,11 +2112,16 @@ function App({ config: initialConfig }) {
|
|
|
1904
2112
|
: a.status === 'failed' ? CRIMSON : GREEN;
|
|
1905
2113
|
const took = (a.endedAt ?? Date.now()) - a.startedAt;
|
|
1906
2114
|
const task = a.task.length > taskWidth ? a.task.slice(0, taskWidth - 3) + '...' : a.task;
|
|
1907
|
-
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
|
|
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));
|
|
1908
2116
|
})] }));
|
|
1909
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
|
|
1910
2118
|
? `${fmtTokens(liveTokens)} tokens`
|
|
1911
|
-
|
|
2119
|
+
// A gateway routing to several backends sends content-free frames while it
|
|
2120
|
+
// finds one. Saying "waiting for the first token" through that reads as a
|
|
2121
|
+
// hang, when in fact the request was accepted and is being worked on.
|
|
2122
|
+
: providerFrames > 0 ? `${cfg.provider} is holding the line, no output yet`
|
|
2123
|
+
: elapsedMs > 8_000 ? `no response from ${cfg.provider} yet`
|
|
2124
|
+
: 'starting', ")"] })] }) }), _jsx(Box, { paddingLeft: 2, children: _jsx(Text, { color: DIM, children: "esc to cancel and keep your input \u00B7 ctrl+c to exit" }) })] })) }), usagePane && (() => {
|
|
1912
2125
|
const width = Math.min(barWidth, 66);
|
|
1913
2126
|
const inner = width - 4;
|
|
1914
2127
|
const BAR_W = Math.max(12, inner - 14);
|
|
@@ -2074,28 +2287,31 @@ function App({ config: initialConfig }) {
|
|
|
2074
2287
|
: picker.kind === 'model' ? `Models - ${cfg.provider}`
|
|
2075
2288
|
: picker.kind === 'effort' ? 'Reasoning effort'
|
|
2076
2289
|
: picker.kind === 'resume' ? 'Resume a saved session'
|
|
2077
|
-
: '
|
|
2290
|
+
: picker.kind === 'file' ? 'Reference a file or directory'
|
|
2291
|
+
: 'Connect provider';
|
|
2078
2292
|
// The palette is sized to the rows it is showing rather than to the terminal. On a wide
|
|
2079
2293
|
// screen a list of short model names in a full-width box is mostly empty box.
|
|
2080
|
-
|
|
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;
|
|
2081
2298
|
const rowWidth = Math.max(headWidth, ...shown.map(it => 38 + (it.current ? 10 : 0) + it.desc.length));
|
|
2082
2299
|
const boxWidth = Math.max(40, Math.min(barWidth, rowWidth + 4));
|
|
2083
2300
|
const width = boxWidth - 4;
|
|
2084
2301
|
let lastGroup;
|
|
2085
|
-
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 }),
|
|
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) => {
|
|
2086
2303
|
const absolute = Math.max(0, start) + i;
|
|
2087
2304
|
const selected = absolute === pickIndex;
|
|
2088
2305
|
const header = item.group && item.group !== lastGroup ? item.group : null;
|
|
2089
2306
|
lastGroup = item.group;
|
|
2090
2307
|
const label = item.label.length > 34 ? item.label.slice(0, 31) + '...' : item.label;
|
|
2091
|
-
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
|
|
2092
|
-
.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));
|
|
2093
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] }))] }));
|
|
2094
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
|
|
2095
2311
|
? _jsx(Text, { color: INK, children: draft.slice(caret) })
|
|
2096
2312
|
: caret < draft.length
|
|
2097
2313
|
? _jsxs(_Fragment, { children: [_jsx(Text, { backgroundColor: CYAN, color: "#10222A", children: draft[caret] }), _jsx(Text, { color: INK, children: draft.slice(caret + 1) })] })
|
|
2098
|
-
: _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"] })] }))] }) })] }));
|
|
2099
2315
|
}
|
|
2100
2316
|
export async function runInkChatMode(config) {
|
|
2101
2317
|
// resolveConfig has already layered CLI flags above the /config store, so the config
|