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.
- package/dist/ink-chat.d.ts +10 -0
- package/dist/ink-chat.d.ts.map +1 -1
- package/dist/ink-chat.js +222 -15
- 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/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.d.ts
CHANGED
|
@@ -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
|
package/dist/ink-chat.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ink-chat.d.ts","sourceRoot":"","sources":["../src/ink-chat.tsx"],"names":[],"mappings":"
|
|
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
|
|
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
|
-
|
|
1671
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
: '
|
|
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
|
-
|
|
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 }),
|
|
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
|
|
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
|