koneck 2.25.7 → 2.25.9
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 +1 -1
- 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 +330 -28
- 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/tools.d.ts +19 -2
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +64 -10
- package/dist/tools.js.map +1 -1
- package/dist/types.d.ts +8 -0
- package/dist/types.d.ts.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 {
|
|
@@ -265,6 +284,7 @@ const COMMANDS = [
|
|
|
265
284
|
{ cmd: '/ask', desc: 'Ask another session and wait: /ask <repo> <question>' },
|
|
266
285
|
{ cmd: '/agents', desc: 'How parallel sub-agents are configured' },
|
|
267
286
|
{ cmd: '/subtask', desc: 'Run a task in an isolated sub-agent' },
|
|
287
|
+
{ cmd: '/btw', desc: 'Ask a side question without interrupting the running task' },
|
|
268
288
|
{ cmd: '/clear', desc: 'Clear conversation history' },
|
|
269
289
|
{ cmd: '/new', desc: 'New session (alias of /clear)' },
|
|
270
290
|
{ cmd: '/reset', desc: 'Reset session (alias of /clear)' },
|
|
@@ -292,6 +312,30 @@ const EFFORT_BLURB = {
|
|
|
292
312
|
high: 'Thorough — covers edge cases, costs more tokens',
|
|
293
313
|
max: 'Most rigorous analysis available; slowest and priciest',
|
|
294
314
|
};
|
|
315
|
+
// Bracketed paste. A terminal that has it enabled wraps pasted text in these markers, which is
|
|
316
|
+
// the only reliable way to tell a paste from very fast typing. Ink strips the leading escape of
|
|
317
|
+
// the opening marker before handing input to useInput, so the literal it sees is "[200~".
|
|
318
|
+
const PASTE_ON = '\u001b[?2004h';
|
|
319
|
+
const PASTE_OFF = '\u001b[?2004l';
|
|
320
|
+
const PASTE_BEGIN = '[200~';
|
|
321
|
+
const PASTE_END = '\u001b[201~';
|
|
322
|
+
/** How a held-back paste appears in the composer. */
|
|
323
|
+
export function pasteToken(index, lines) {
|
|
324
|
+
return `[Pasted text #${index} +${lines} lines]`;
|
|
325
|
+
}
|
|
326
|
+
/**
|
|
327
|
+
* Puts held-back pastes back into the text before it is sent.
|
|
328
|
+
*
|
|
329
|
+
* The token is what the user sees; the model must receive what they actually copied. Anything
|
|
330
|
+
* typed around a token is preserved in place, so a paste can be introduced and followed by a
|
|
331
|
+
* question in one prompt.
|
|
332
|
+
*/
|
|
333
|
+
export function expandPastes(draft, pastes) {
|
|
334
|
+
return draft.replace(/\[Pasted text #(\d+) \+\d+ lines\]/g, (whole, n) => {
|
|
335
|
+
const text = pastes[Number(n) - 1];
|
|
336
|
+
return text === undefined ? whole : text;
|
|
337
|
+
});
|
|
338
|
+
}
|
|
295
339
|
/** Past this, a file is not read for diffing — the render would be useless anyway. */
|
|
296
340
|
const MAX_DIFF_BYTES = 512 * 1024;
|
|
297
341
|
/** One tool rarely touches more; the cap stops a bulk move from flooding scrollback. */
|
|
@@ -344,6 +388,7 @@ Slash commands:
|
|
|
344
388
|
/mode [auto|plan|code] Show or set mode
|
|
345
389
|
/effort [low|med|high|max] Set reasoning effort
|
|
346
390
|
/config [key] [value] View or set persistent config
|
|
391
|
+
/btw <question> Ask a side question without interrupting the running task
|
|
347
392
|
/usage Usage pane: session, context, models, limits
|
|
348
393
|
/tokens Show token usage breakdown
|
|
349
394
|
/cost Show session cost
|
|
@@ -374,8 +419,33 @@ function App({ config: initialConfig }) {
|
|
|
374
419
|
const replyStartedRef = useRef(false);
|
|
375
420
|
/** Everything streamed this turn. Empty means the provider did not stream at all. */
|
|
376
421
|
const streamedRef = useRef('');
|
|
422
|
+
/** When the live region last had something new to show, used to pick the repaint rate. */
|
|
423
|
+
const lastActivityRef = useRef(Date.now());
|
|
377
424
|
/** Frames received before any content — proof the provider has the request and is working. */
|
|
378
425
|
const [providerFrames, setProviderFrames] = useState(0);
|
|
426
|
+
/**
|
|
427
|
+
* Text pasted into the composer, held aside so the prompt stays readable.
|
|
428
|
+
*
|
|
429
|
+
* A thirty-line paste in the input box buries whatever the user meant to type around it, and the
|
|
430
|
+
* paragraph structure is lost the moment it is folded into one wrapped line. Each paste is kept
|
|
431
|
+
* verbatim here and stands in the draft as a short token, which is expanded again on submit — so
|
|
432
|
+
* the model receives exactly what was copied, blank lines and all.
|
|
433
|
+
*/
|
|
434
|
+
const pastesRef = useRef([]);
|
|
435
|
+
const [pasteCount, setPasteCount] = useState(0);
|
|
436
|
+
/** Accumulates a paste that spans more than one stdin chunk. */
|
|
437
|
+
const pasteBufRef = useRef(null);
|
|
438
|
+
/** Workspace paths for the `@` picker, read once and reused. */
|
|
439
|
+
const entriesRef = useRef(null);
|
|
440
|
+
/** Where the `@` being completed starts in the draft. */
|
|
441
|
+
const atStartRef = useRef(-1);
|
|
442
|
+
/**
|
|
443
|
+
* Prompts typed while a turn was running.
|
|
444
|
+
*
|
|
445
|
+
* Enter used to be swallowed outright, so a thought had to be held until the agent finished or
|
|
446
|
+
* typed again from memory. Queued prompts run in order as soon as the current turn ends.
|
|
447
|
+
*/
|
|
448
|
+
const [queued, setQueued] = useState([]);
|
|
379
449
|
/** Pre-edit contents of the files the running tool said it would touch. */
|
|
380
450
|
const pendingDiffRef = useRef([]);
|
|
381
451
|
const charsRef = useRef(0); // streamed chars, for the live token estimate
|
|
@@ -397,6 +467,7 @@ function App({ config: initialConfig }) {
|
|
|
397
467
|
* whatever is left in the buffer.
|
|
398
468
|
*/
|
|
399
469
|
function pushSay(text) {
|
|
470
|
+
lastActivityRef.current = Date.now();
|
|
400
471
|
sayRef.current += text;
|
|
401
472
|
streamedRef.current += text;
|
|
402
473
|
charsRef.current += text.length;
|
|
@@ -441,6 +512,7 @@ function App({ config: initialConfig }) {
|
|
|
441
512
|
function pushTool(name, args) {
|
|
442
513
|
commitSay();
|
|
443
514
|
// Photograph every file this tool has announced it will touch, before it touches it.
|
|
515
|
+
lastActivityRef.current = Date.now();
|
|
444
516
|
pendingDiffRef.current = filesTouchedBy(name, args).slice(0, MAX_DIFF_FILES)
|
|
445
517
|
.map(rel => ({ rel, before: readForDiff(rel) }));
|
|
446
518
|
liveToolRef.current = { kind: 'tool', name, detail: toolDetail(name, args), startedAt: Date.now() };
|
|
@@ -488,6 +560,11 @@ function App({ config: initialConfig }) {
|
|
|
488
560
|
onChunk: (text) => pushSay(text),
|
|
489
561
|
onToolCall: (name, args) => pushTool(name, args),
|
|
490
562
|
onToolResult: (name, ok, ms) => finishTool(name, ok, ms),
|
|
563
|
+
onToolOutput: (_name, line) => {
|
|
564
|
+
lastActivityRef.current = Date.now();
|
|
565
|
+
if (liveToolRef.current)
|
|
566
|
+
liveToolRef.current.output = line;
|
|
567
|
+
},
|
|
491
568
|
onStreamActivity: (frames) => setProviderFrames(frames),
|
|
492
569
|
onAgentProgress: (list) => { agentsRef.current = list; setAgents(list); },
|
|
493
570
|
onTokens: (s) => {
|
|
@@ -673,20 +750,54 @@ function App({ config: initialConfig }) {
|
|
|
673
750
|
}
|
|
674
751
|
busyStart.current = Date.now();
|
|
675
752
|
wordTimer.current = 0;
|
|
676
|
-
//
|
|
677
|
-
//
|
|
678
|
-
|
|
753
|
+
// The frame rate is the flicker rate. Every tick makes Ink erase and rewrite the whole live
|
|
754
|
+
// region — measured at 188 line-erases a second, which on a modest terminal reads as the
|
|
755
|
+
// bottom of the screen flashing. So the rate follows how much is actually happening: quick
|
|
756
|
+
// while output is arriving, and much slower once a single long command is all that is left,
|
|
757
|
+
// where a spinner turning four times a second says as much as one turning seven.
|
|
758
|
+
let timer;
|
|
759
|
+
const tick = () => {
|
|
760
|
+
const elapsed = Date.now() - busyStart.current;
|
|
679
761
|
setSpinFrame(f => (f + 1) % SPINNER.length);
|
|
680
762
|
setShimmerFrame(f => f + 1);
|
|
681
|
-
setElapsedMs(
|
|
763
|
+
setElapsedMs(elapsed);
|
|
682
764
|
wordTimer.current += 1;
|
|
683
|
-
//
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
765
|
+
// Roughly every 15 seconds, whatever the current rate.
|
|
766
|
+
if (wordTimer.current % Math.max(20, Math.round(15_000 / currentDelay())) === 0) {
|
|
767
|
+
setSpinWord(w => w + 1);
|
|
768
|
+
}
|
|
769
|
+
timer = setTimeout(tick, currentDelay());
|
|
770
|
+
};
|
|
771
|
+
const currentDelay = () => {
|
|
772
|
+
const idleFor = Date.now() - lastActivityRef.current;
|
|
773
|
+
// Something is still arriving, so keep it lively.
|
|
774
|
+
if (idleFor < 2_000)
|
|
775
|
+
return 150;
|
|
776
|
+
// A long wait with nothing to show: slow right down.
|
|
777
|
+
return idleFor > 15_000 ? 500 : 250;
|
|
778
|
+
};
|
|
779
|
+
timer = setTimeout(tick, 150);
|
|
780
|
+
return () => clearTimeout(timer);
|
|
689
781
|
}, [busy]);
|
|
782
|
+
// A queued prompt runs the moment the agent is free again. Keyed on `busy` rather than done
|
|
783
|
+
// inside the turn's own exit paths, so every way a turn can end — success, failure, cancel —
|
|
784
|
+
// drains the queue the same way.
|
|
785
|
+
useEffect(() => {
|
|
786
|
+
if (busy || queued.length === 0)
|
|
787
|
+
return;
|
|
788
|
+
const [next, ...rest] = queued;
|
|
789
|
+
setQueued(rest);
|
|
790
|
+
if (next)
|
|
791
|
+
submitDraft(next);
|
|
792
|
+
}, [busy, queued]);
|
|
793
|
+
// Ask the terminal to bracket pasted text. Without this a paste is indistinguishable from
|
|
794
|
+
// typing, and there is no way to keep it out of the composer or preserve its line breaks.
|
|
795
|
+
useEffect(() => {
|
|
796
|
+
process.stdout.write(PASTE_ON);
|
|
797
|
+
const off = () => { process.stdout.write(PASTE_OFF); };
|
|
798
|
+
process.on('exit', off);
|
|
799
|
+
return () => { off(); process.off('exit', off); };
|
|
800
|
+
}, []);
|
|
690
801
|
// The tree as it stood before the session touched anything. Recorded once, so "code changes"
|
|
691
802
|
// stays a comparison against the starting point however long the session runs.
|
|
692
803
|
useEffect(() => { void snapshotTree(cfg.cwd).then(t => { baseTreeRef.current = t; }); }, [cfg.cwd]);
|
|
@@ -811,6 +922,35 @@ function App({ config: initialConfig }) {
|
|
|
811
922
|
return []; // the pane says so rather than showing zeroes
|
|
812
923
|
}
|
|
813
924
|
}
|
|
925
|
+
/**
|
|
926
|
+
* Answers a question without disturbing the turn in flight.
|
|
927
|
+
*
|
|
928
|
+
* The agent's own conversation is left untouched: the question runs in a separate, isolated
|
|
929
|
+
* session, so asking "which model am I on?" mid-build neither interrupts the build nor pollutes
|
|
930
|
+
* the history the agent is reasoning over. It is the one thing that may jump the queue.
|
|
931
|
+
*/
|
|
932
|
+
async function askAside(question) {
|
|
933
|
+
if (!question.trim()) {
|
|
934
|
+
addSystem('Usage: /btw <question> — answers alongside the running task, without interrupting it.');
|
|
935
|
+
return;
|
|
936
|
+
}
|
|
937
|
+
addRow({ role: 'user', text: `/btw ${question}` });
|
|
938
|
+
try {
|
|
939
|
+
const { runAgent } = await import('./engine.js');
|
|
940
|
+
const result = await runAgent(question, {
|
|
941
|
+
...cfg, ci: true, silent: true,
|
|
942
|
+
// A side question answers from what it is told; it must not edit files behind the
|
|
943
|
+
// running task's back, so it is given no reason to and kept to a couple of turns.
|
|
944
|
+
maxTurns: Math.min(cfg.maxTurns, 3),
|
|
945
|
+
});
|
|
946
|
+
const last = [...result.messages].reverse()
|
|
947
|
+
.find(m => m.role === 'assistant' && typeof m.content === 'string' && m.content.trim());
|
|
948
|
+
addRow({ role: 'agent', text: typeof last?.content === 'string' ? last.content : '(no answer)' });
|
|
949
|
+
}
|
|
950
|
+
catch (e) {
|
|
951
|
+
addSystem(`/btw failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
952
|
+
}
|
|
953
|
+
}
|
|
814
954
|
/** Opens the update panel, checking the registry each time it is opened deliberately. */
|
|
815
955
|
async function openUpdatePane() {
|
|
816
956
|
setUpdatePane({ current: VERSION, latest: null, behind: false, manager: 'npm' });
|
|
@@ -872,6 +1012,30 @@ function App({ config: initialConfig }) {
|
|
|
872
1012
|
current: p.name === cfg.provider,
|
|
873
1013
|
}));
|
|
874
1014
|
}
|
|
1015
|
+
/**
|
|
1016
|
+
* Offers workspace paths for an `@` reference. The listing is read on first use rather than at
|
|
1017
|
+
* startup, so a session in a large repository is not delayed by a scan it may never need.
|
|
1018
|
+
*/
|
|
1019
|
+
async function openFilePicker(query) {
|
|
1020
|
+
if (entriesRef.current === null)
|
|
1021
|
+
entriesRef.current = await listWorkspaceEntries(cfg.cwd);
|
|
1022
|
+
const all = entriesRef.current;
|
|
1023
|
+
if (all.length === 0) {
|
|
1024
|
+
addSystem('No files found to reference here.');
|
|
1025
|
+
return;
|
|
1026
|
+
}
|
|
1027
|
+
setPicker({ kind: 'file', items: fileItems(all, query) });
|
|
1028
|
+
setPickQuery(query);
|
|
1029
|
+
setPickIndex(0);
|
|
1030
|
+
}
|
|
1031
|
+
/** The picker rows for a query, ranked by how well each path matches. */
|
|
1032
|
+
function fileItems(all, query) {
|
|
1033
|
+
return filterEntries(all, query, 200).map(entry => ({
|
|
1034
|
+
value: entry,
|
|
1035
|
+
label: entry,
|
|
1036
|
+
desc: entry.endsWith('/') ? 'directory' : '',
|
|
1037
|
+
}));
|
|
1038
|
+
}
|
|
875
1039
|
function openPicker(kind, items, query = '') {
|
|
876
1040
|
setPicker({ kind, items });
|
|
877
1041
|
setPickQuery(query);
|
|
@@ -886,6 +1050,18 @@ function App({ config: initialConfig }) {
|
|
|
886
1050
|
async function choosePick(item, kindOverride) {
|
|
887
1051
|
const kind = kindOverride ?? picker?.kind;
|
|
888
1052
|
closePicker();
|
|
1053
|
+
if (kind === 'file') {
|
|
1054
|
+
// Replace the partial "@query" with the full path, leaving a trailing space so the next
|
|
1055
|
+
// word can be typed straight away. A directory keeps its slash so more can be typed after.
|
|
1056
|
+
const start = atStartRef.current;
|
|
1057
|
+
atStartRef.current = -1;
|
|
1058
|
+
if (start >= 0) {
|
|
1059
|
+
const suffix = item.value.endsWith('/') ? '' : ' ';
|
|
1060
|
+
setDraft(d => d.slice(0, start) + '@' + item.value + suffix + d.slice(caret));
|
|
1061
|
+
setCaret(start + 1 + item.value.length + suffix.length);
|
|
1062
|
+
}
|
|
1063
|
+
return;
|
|
1064
|
+
}
|
|
889
1065
|
if (kind === 'command') {
|
|
890
1066
|
// Leave it in the composer so arguments can still be typed before submitting.
|
|
891
1067
|
setDraft(item.value + ' ');
|
|
@@ -967,7 +1143,13 @@ function App({ config: initialConfig }) {
|
|
|
967
1143
|
const cost = estimateCost(cfg.model, stats.promptTokens, stats.completionTokens);
|
|
968
1144
|
const costStr = cost.known ? formatCost(cost.total) : 'unavailable';
|
|
969
1145
|
const ws = cfg.cwd.replace(process.env['HOME'] ?? '', '~');
|
|
1146
|
+
// The endpoint is resolved from four places — an explicit override, an env var, the
|
|
1147
|
+
// config file, then the built-in default — so showing the name alone leaves the one
|
|
1148
|
+
// question that actually matters when a provider misbehaves unanswered: where did the
|
|
1149
|
+
// request go? Working that out took a proxy and half an hour.
|
|
1150
|
+
const endpoint = resolveProvider(cfg.provider, cfg.baseURL).baseURL;
|
|
970
1151
|
addSystem(`Provider : ${cfg.provider}\n` +
|
|
1152
|
+
`Endpoint : ${endpoint}\n` +
|
|
971
1153
|
`Model : ${cfg.model}\n` +
|
|
972
1154
|
`Mode : ${mode} effort: ${effort}\n` +
|
|
973
1155
|
`Approval : ${cfg.requireApproval ? 'on' : 'off'}\n` +
|
|
@@ -1042,6 +1224,11 @@ function App({ config: initialConfig }) {
|
|
|
1042
1224
|
addSystem(`Effort → ${arg}`);
|
|
1043
1225
|
return;
|
|
1044
1226
|
}
|
|
1227
|
+
case '/btw': {
|
|
1228
|
+
// Also available when nothing is running, where it simply answers in a clean context.
|
|
1229
|
+
await askAside(arg);
|
|
1230
|
+
return;
|
|
1231
|
+
}
|
|
1045
1232
|
case '/usage': {
|
|
1046
1233
|
const wanted = USAGE_TABS.find(t => t.startsWith(arg.trim().toLowerCase()));
|
|
1047
1234
|
await openUsagePane(wanted ?? 'session');
|
|
@@ -1424,7 +1611,53 @@ function App({ config: initialConfig }) {
|
|
|
1424
1611
|
addSystem(`Unknown command: ${cmd}. Type /help for the full list.`);
|
|
1425
1612
|
}
|
|
1426
1613
|
}
|
|
1614
|
+
/**
|
|
1615
|
+
* Files a completed paste and puts a token in the composer.
|
|
1616
|
+
*
|
|
1617
|
+
* A short single-line paste is inserted literally — turning "npm test" into a token would be
|
|
1618
|
+
* obstructive. Anything with a line break is held back, because that is the case where the text
|
|
1619
|
+
* would both swamp the composer and lose its structure.
|
|
1620
|
+
*/
|
|
1621
|
+
function filePaste(text) {
|
|
1622
|
+
const clean = text.replace(/\r\n?/g, '\n');
|
|
1623
|
+
const lines = clean.split('\n').length;
|
|
1624
|
+
if (!clean.includes('\n') && clean.length <= 200) {
|
|
1625
|
+
setDraft(d => d.slice(0, caret) + clean + d.slice(caret));
|
|
1626
|
+
setCaret(c => c + clean.length);
|
|
1627
|
+
return;
|
|
1628
|
+
}
|
|
1629
|
+
pastesRef.current = [...pastesRef.current, clean];
|
|
1630
|
+
const token = pasteToken(pastesRef.current.length, lines);
|
|
1631
|
+
setPasteCount(pastesRef.current.length);
|
|
1632
|
+
setDraft(d => d.slice(0, caret) + token + d.slice(caret));
|
|
1633
|
+
setCaret(c => c + token.length);
|
|
1634
|
+
}
|
|
1427
1635
|
useInput((input, key) => {
|
|
1636
|
+
// ── Bracketed paste ─────────────────────────────────────────────────────
|
|
1637
|
+
// This runs before every other branch. A paste carries newlines, and any handler that treats
|
|
1638
|
+
// a newline as "submit" would fire partway through one.
|
|
1639
|
+
if (pasteBufRef.current !== null) {
|
|
1640
|
+
const end = input.indexOf(PASTE_END);
|
|
1641
|
+
if (end === -1) {
|
|
1642
|
+
pasteBufRef.current += input;
|
|
1643
|
+
return;
|
|
1644
|
+
}
|
|
1645
|
+
const whole = pasteBufRef.current + input.slice(0, end);
|
|
1646
|
+
pasteBufRef.current = null;
|
|
1647
|
+
filePaste(whole);
|
|
1648
|
+
return;
|
|
1649
|
+
}
|
|
1650
|
+
const begin = input.indexOf(PASTE_BEGIN);
|
|
1651
|
+
if (begin !== -1) {
|
|
1652
|
+
const after = input.slice(begin + PASTE_BEGIN.length);
|
|
1653
|
+
const end = after.indexOf(PASTE_END);
|
|
1654
|
+
if (end === -1) {
|
|
1655
|
+
pasteBufRef.current = after;
|
|
1656
|
+
return;
|
|
1657
|
+
} // more chunks to come
|
|
1658
|
+
filePaste(after.slice(0, end));
|
|
1659
|
+
return;
|
|
1660
|
+
}
|
|
1428
1661
|
// Esc abandons the turn in flight and hands the keyboard straight back. The composer is
|
|
1429
1662
|
// deliberately left alone: cancelling should not cost whatever was typed while waiting.
|
|
1430
1663
|
if (key.escape && busy && !picker && !keyPrompt && !updatePane && !usagePane) {
|
|
@@ -1470,6 +1703,8 @@ function App({ config: initialConfig }) {
|
|
|
1470
1703
|
setDraft('');
|
|
1471
1704
|
setCaret(0);
|
|
1472
1705
|
}
|
|
1706
|
+
if (picker.kind === 'file')
|
|
1707
|
+
atStartRef.current = -1;
|
|
1473
1708
|
closePicker();
|
|
1474
1709
|
return;
|
|
1475
1710
|
}
|
|
@@ -1502,6 +1737,21 @@ function App({ config: initialConfig }) {
|
|
|
1502
1737
|
setCaret(0);
|
|
1503
1738
|
return;
|
|
1504
1739
|
}
|
|
1740
|
+
if (picker.kind === 'file') {
|
|
1741
|
+
// Deleting the "@" itself is how the picker is dismissed without choosing anything.
|
|
1742
|
+
setDraft(d => d.slice(0, Math.max(0, caret - 1)) + d.slice(caret));
|
|
1743
|
+
setCaret(c => Math.max(0, c - 1));
|
|
1744
|
+
if (pickQuery === '') {
|
|
1745
|
+
closePicker();
|
|
1746
|
+
atStartRef.current = -1;
|
|
1747
|
+
return;
|
|
1748
|
+
}
|
|
1749
|
+
const q = pickQuery.slice(0, -1);
|
|
1750
|
+
setPickQuery(q);
|
|
1751
|
+
setPickIndex(0);
|
|
1752
|
+
setPicker(p => (p ? { ...p, items: fileItems(entriesRef.current ?? [], q) } : p));
|
|
1753
|
+
return;
|
|
1754
|
+
}
|
|
1505
1755
|
setPickQuery(q => q.slice(0, -1));
|
|
1506
1756
|
setPickIndex(0);
|
|
1507
1757
|
if (picker.kind === 'command') {
|
|
@@ -1514,6 +1764,13 @@ function App({ config: initialConfig }) {
|
|
|
1514
1764
|
// A space means the command name is finished and arguments follow, so the palette
|
|
1515
1765
|
// steps out of the way. Keeping it open sent "model gpt-4o" into the filter, emptied
|
|
1516
1766
|
// the list, and left Enter with nothing to select.
|
|
1767
|
+
if (input === ' ' && picker.kind === 'file') {
|
|
1768
|
+
closePicker();
|
|
1769
|
+
atStartRef.current = -1;
|
|
1770
|
+
setDraft(d => d.slice(0, caret) + ' ' + d.slice(caret));
|
|
1771
|
+
setCaret(c => c + 1);
|
|
1772
|
+
return;
|
|
1773
|
+
}
|
|
1517
1774
|
if (input === ' ' && picker.kind === 'command') {
|
|
1518
1775
|
closePicker();
|
|
1519
1776
|
setDraft(d => { const t = d.endsWith(' ') ? d : d + ' '; setCaret(t.length); return t; });
|
|
@@ -1527,6 +1784,12 @@ function App({ config: initialConfig }) {
|
|
|
1527
1784
|
setDraft(t);
|
|
1528
1785
|
setCaret(t.length);
|
|
1529
1786
|
}
|
|
1787
|
+
if (picker.kind === 'file') {
|
|
1788
|
+
const q = pickQuery + input;
|
|
1789
|
+
setPicker(p => (p ? { ...p, items: fileItems(entriesRef.current ?? [], q) } : p));
|
|
1790
|
+
setDraft(d => d.slice(0, caret) + input + d.slice(caret));
|
|
1791
|
+
setCaret(c => c + input.length);
|
|
1792
|
+
}
|
|
1530
1793
|
return;
|
|
1531
1794
|
}
|
|
1532
1795
|
return;
|
|
@@ -1584,8 +1847,24 @@ function App({ config: initialConfig }) {
|
|
|
1584
1847
|
// while it works can be typed as it arrives rather than held until the turn ends — and
|
|
1585
1848
|
// cancelling with esc then hands the keyboard back to a composer that still has it.
|
|
1586
1849
|
if (key.return) {
|
|
1587
|
-
if (busy)
|
|
1850
|
+
if (busy) {
|
|
1851
|
+
const text = draft.trim();
|
|
1852
|
+
if (!text)
|
|
1853
|
+
return;
|
|
1854
|
+
// A side question is answered straight away and separately; everything else waits its turn.
|
|
1855
|
+
if (/^\/btw(\s|$)/.test(text)) {
|
|
1856
|
+
setDraft('');
|
|
1857
|
+
setCaret(0);
|
|
1858
|
+
void askAside(text.replace(/^\/btw\s*/, ''));
|
|
1859
|
+
return;
|
|
1860
|
+
}
|
|
1861
|
+
setQueued(q => [...q, expandPastes(text, pastesRef.current)]);
|
|
1862
|
+
setDraft('');
|
|
1863
|
+
setCaret(0);
|
|
1864
|
+
pastesRef.current = [];
|
|
1865
|
+
setPasteCount(0);
|
|
1588
1866
|
return;
|
|
1867
|
+
}
|
|
1589
1868
|
submitDraft(draft);
|
|
1590
1869
|
return;
|
|
1591
1870
|
}
|
|
@@ -1667,8 +1946,19 @@ function App({ config: initialConfig }) {
|
|
|
1667
1946
|
const text = input.replace(/[\u0000-\u0008\u000b-\u001f\u007f]/g, '');
|
|
1668
1947
|
if (text === '')
|
|
1669
1948
|
return;
|
|
1670
|
-
|
|
1671
|
-
|
|
1949
|
+
const next = draft.slice(0, caret) + text + draft.slice(caret);
|
|
1950
|
+
const nextCaret = caret + text.length;
|
|
1951
|
+
setDraft(next);
|
|
1952
|
+
setCaret(nextCaret);
|
|
1953
|
+
// A lone "@" at a word boundary offers workspace paths. Checking the draft as it will be,
|
|
1954
|
+
// rather than the character typed, means it also fires when "@" arrives mid-chunk.
|
|
1955
|
+
if (text.endsWith('@')) {
|
|
1956
|
+
const ref = activeReference(next, nextCaret);
|
|
1957
|
+
if (ref) {
|
|
1958
|
+
atStartRef.current = ref.start;
|
|
1959
|
+
void openFilePicker(ref.query);
|
|
1960
|
+
}
|
|
1961
|
+
}
|
|
1672
1962
|
}
|
|
1673
1963
|
});
|
|
1674
1964
|
/** Start of the word at or before `i`, for word-wise movement and deletion. */
|
|
@@ -1692,12 +1982,20 @@ function App({ config: initialConfig }) {
|
|
|
1692
1982
|
/** Sends the composer contents: a slash command, or a task for the agent. */
|
|
1693
1983
|
function submitDraft(raw) {
|
|
1694
1984
|
{
|
|
1695
|
-
|
|
1985
|
+
// Tokens are expanded here, at the last moment. The model gets the text as it was copied —
|
|
1986
|
+
// blank lines, indentation and all — while the transcript shows the compact form so a
|
|
1987
|
+
// thirty-line paste does not bury the question asked about it.
|
|
1988
|
+
const shown = raw.trim();
|
|
1989
|
+
if (!shown)
|
|
1990
|
+
return;
|
|
1991
|
+
const task = expandPastes(shown, pastesRef.current).trim();
|
|
1696
1992
|
if (!task)
|
|
1697
1993
|
return;
|
|
1698
1994
|
setDraft('');
|
|
1699
1995
|
setCaret(0);
|
|
1700
|
-
|
|
1996
|
+
pastesRef.current = [];
|
|
1997
|
+
setPasteCount(0);
|
|
1998
|
+
addRow({ role: 'user', text: shown });
|
|
1701
1999
|
if (task.startsWith('/')) {
|
|
1702
2000
|
const spaceIdx = task.indexOf(' ');
|
|
1703
2001
|
const cmd = spaceIdx === -1 ? task : task.slice(0, spaceIdx);
|
|
@@ -1713,6 +2011,7 @@ function App({ config: initialConfig }) {
|
|
|
1713
2011
|
sayRef.current = '';
|
|
1714
2012
|
streamedRef.current = '';
|
|
1715
2013
|
replyStartedRef.current = false;
|
|
2014
|
+
lastActivityRef.current = Date.now();
|
|
1716
2015
|
setProviderFrames(0);
|
|
1717
2016
|
liveToolRef.current = null;
|
|
1718
2017
|
charsRef.current = 0;
|
|
@@ -1864,7 +2163,7 @@ function App({ config: initialConfig }) {
|
|
|
1864
2163
|
const bg = l.kind === 'add' ? DIFF_ADD_BG : l.kind === 'del' ? DIFF_DEL_BG : undefined;
|
|
1865
2164
|
// Tabs would make the tinted block a ragged width, so they are expanded.
|
|
1866
2165
|
const body = `${sign} ${l.text}`.replace(/\t/g, ' ');
|
|
1867
|
-
const cell = body
|
|
2166
|
+
const cell = fitCells(body, width, true);
|
|
1868
2167
|
return (_jsxs(Box, { children: [_jsxs(Text, { color: DIM, children: [String(shown ?? '').padStart(gutter), " "] }), _jsx(Text, { color: fg, ...(bg ? { backgroundColor: bg } : {}), children: cell })] }, li));
|
|
1869
2168
|
})] }, hi))), d.hiddenLines > 0 && (_jsxs(Text, { color: DIM, children: [' '.repeat(gutter), " ... ", d.hiddenLines, " more changed lines"] }))] }, key));
|
|
1870
2169
|
};
|
|
@@ -1876,12 +2175,12 @@ function App({ config: initialConfig }) {
|
|
|
1876
2175
|
const ms = (step.endedAt ?? Date.now()) - step.startedAt;
|
|
1877
2176
|
const glyph = running ? SPINNER[spinFrame] : step.ok === false ? '✖' : '✓';
|
|
1878
2177
|
const color = running ? AMBER : step.ok === false ? CRIMSON : GREEN;
|
|
1879
|
-
return (_jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsxs(Box, { gap: 1, children: [_jsx(Text, { color: color, children: glyph }), _jsx(Text, { color: AMBER, bold: true, children: step.name }), _jsx(Text, { color: DIM, children: "\u00B7" }), _jsx(Text, { color: MUTED, children: fmtElapsed(ms) })] }), step.detail !== '' && (step.diffs ?? []).length === 0 && (_jsxs(Box, { paddingLeft: 2, gap: 1, children: [_jsx(Text, { color: DIM, children: "\u2514" }), _jsx(Text, { color: MUTED, children: step.detail })] })), (step.diffs ?? []).map((d, i) => renderDiff(d, i))] }, key));
|
|
2178
|
+
return (_jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsxs(Box, { gap: 1, children: [_jsx(Text, { color: color, children: glyph }), _jsx(Text, { color: AMBER, bold: true, children: step.name }), _jsx(Text, { color: DIM, children: "\u00B7" }), _jsx(Text, { color: MUTED, children: fmtElapsed(ms) })] }), step.detail !== '' && (step.diffs ?? []).length === 0 && (_jsxs(Box, { paddingLeft: 2, gap: 1, children: [_jsx(Text, { color: DIM, children: "\u2514" }), _jsx(Text, { color: MUTED, children: step.detail })] })), running && step.output && (_jsx(Box, { paddingLeft: 4, children: _jsx(Text, { color: DIM, children: fitCells(step.output, Math.max(20, barWidth - 6)) }) })), (step.diffs ?? []).map((d, i) => renderDiff(d, i))] }, key));
|
|
1880
2179
|
};
|
|
1881
2180
|
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
2181
|
// Painted as a filled bar so the user's own words are the most findable thing
|
|
1883
2182
|
// 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
|
|
2183
|
+
_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
2184
|
const lines = [row.text ?? '', '', 'Fix the above, then retry. /status shows the active provider and model.'];
|
|
1886
2185
|
return (_jsx(Box, { marginTop: 1, children: _jsx(Panel, { width: panelWidth(lines, barWidth, 40), color: CRIMSON, title: "[!] ERROR", children: lines }) }, index));
|
|
1887
2186
|
})() : row.role === 'system' ? (() => {
|
|
@@ -1893,7 +2192,7 @@ function App({ config: initialConfig }) {
|
|
|
1893
2192
|
// markdown tables needlessly narrow. A marker plus the content reads better and
|
|
1894
2193
|
// cannot misalign, which is how Claude Code presents its own answers.
|
|
1895
2194
|
_jsxs(Box, { flexDirection: "column", marginTop: row.continued ? 0 : 1, children: [_jsxs(Box, { gap: 1, children: [_jsx(Text, { color: CYAN, bold: true, children: row.continued ? ' ' : 'ꞰK' }), _jsx(Box, { flexDirection: "column", children: _jsx(Markdown, { text: row.text ?? '', width: barWidth - 3 }) })] }), row.tokens != null && (_jsxs(Text, { color: MUTED, children: [_jsx(Text, { color: GREEN, children: "* " }), row.done ?? 'Done', " in ", fmtElapsed(row.elapsed ?? 0), ' | ', row.tokens, " tokens", ' | ', cfg.provider, " ", modelShort] }))] }, index)) }), _jsx(Box, { flexDirection: "column", children: busy && (agentState === 'processing' || agentState === 'syncing') && (_jsxs(Box, { flexDirection: "column", children: [sayRef.current.trim() !== '' && (_jsxs(Box, { gap: 1, children: [_jsx(Text, { color: CYAN, bold: true, children: replyStartedRef.current ? ' ' : 'ꞰK' }), _jsx(Box, { flexDirection: "column", children: tailLines(sayRef.current, liveReplyLines(termRows), replyWidth - 3)
|
|
1896
|
-
.split('\n').map((line, i) => _jsx(Text, { color: INK, children: line }, i)) })] })), liveToolRef.current && renderStep(liveToolRef.current, 1), agents.length > 0 && (() => {
|
|
2195
|
+
.split('\n').map((line, i) => _jsx(Text, { color: INK, children: line }, i)) })] })), liveToolRef.current && renderStep(liveToolRef.current, 1), queued.length > 0 && (_jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsxs(Box, { gap: 1, children: [_jsx(Text, { color: AMBER, children: "\u25AA" }), _jsxs(Text, { color: MUTED, children: [queued.length, " queued, will run when this finishes \u00B7 /btw to ask without waiting"] })] }), queued.map((q, i) => (_jsx(Box, { paddingLeft: 2, children: _jsx(Text, { color: DIM, children: fitCells(q.split('\n')[0] ?? '', Math.max(20, barWidth - 6), true) }) }, i)))] })), agents.length > 0 && (() => {
|
|
1897
2196
|
const done = agents.filter(a => a.status !== 'running').length;
|
|
1898
2197
|
const failed = agents.filter(a => a.status === 'failed').length;
|
|
1899
2198
|
const tokens = agents.reduce((n, a) => n + a.tokens, 0);
|
|
@@ -1908,7 +2207,7 @@ function App({ config: initialConfig }) {
|
|
|
1908
2207
|
: a.status === 'failed' ? CRIMSON : GREEN;
|
|
1909
2208
|
const took = (a.endedAt ?? Date.now()) - a.startedAt;
|
|
1910
2209
|
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
|
|
2210
|
+
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
2211
|
})] }));
|
|
1913
2212
|
})(), _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
2213
|
? `${fmtTokens(liveTokens)} tokens`
|
|
@@ -2083,28 +2382,31 @@ function App({ config: initialConfig }) {
|
|
|
2083
2382
|
: picker.kind === 'model' ? `Models - ${cfg.provider}`
|
|
2084
2383
|
: picker.kind === 'effort' ? 'Reasoning effort'
|
|
2085
2384
|
: picker.kind === 'resume' ? 'Resume a saved session'
|
|
2086
|
-
: '
|
|
2385
|
+
: picker.kind === 'file' ? 'Reference a file or directory'
|
|
2386
|
+
: 'Connect provider';
|
|
2087
2387
|
// The palette is sized to the rows it is showing rather than to the terminal. On a wide
|
|
2088
2388
|
// screen a list of short model names in a full-width box is mostly empty box.
|
|
2089
|
-
|
|
2389
|
+
// The header is a title on the left and a count on the right, on one line. Estimating the
|
|
2390
|
+
// right-hand side at a constant made "Reference a file or directory" wrap onto two rows.
|
|
2391
|
+
const countText = `${list.length} match${list.length === 1 ? '' : 'es'} · esc to close`;
|
|
2392
|
+
const headWidth = title.length + countText.length + 3;
|
|
2090
2393
|
const rowWidth = Math.max(headWidth, ...shown.map(it => 38 + (it.current ? 10 : 0) + it.desc.length));
|
|
2091
2394
|
const boxWidth = Math.max(40, Math.min(barWidth, rowWidth + 4));
|
|
2092
2395
|
const width = boxWidth - 4;
|
|
2093
2396
|
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 }),
|
|
2397
|
+
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
2398
|
const absolute = Math.max(0, start) + i;
|
|
2096
2399
|
const selected = absolute === pickIndex;
|
|
2097
2400
|
const header = item.group && item.group !== lastGroup ? item.group : null;
|
|
2098
2401
|
lastGroup = item.group;
|
|
2099
2402
|
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));
|
|
2403
|
+
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
2404
|
}), 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
2405
|
})(), 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
2406
|
? _jsx(Text, { color: INK, children: draft.slice(caret) })
|
|
2105
2407
|
: caret < draft.length
|
|
2106
2408
|
? _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" })] }) })] }));
|
|
2409
|
+
: _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
2410
|
}
|
|
2109
2411
|
export async function runInkChatMode(config) {
|
|
2110
2412
|
// resolveConfig has already layered CLI flags above the /config store, so the config
|