koneck 2.25.2 → 2.25.4
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/config-store.d.ts +1 -0
- package/dist/config-store.d.ts.map +1 -1
- package/dist/config-store.js +2 -0
- package/dist/config-store.js.map +1 -1
- package/dist/diff-view.d.ts +52 -0
- package/dist/diff-view.d.ts.map +1 -0
- package/dist/diff-view.js +208 -0
- package/dist/diff-view.js.map +1 -0
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +31 -6
- package/dist/engine.js.map +1 -1
- package/dist/ink-chat.d.ts.map +1 -1
- package/dist/ink-chat.js +144 -11
- package/dist/ink-chat.js.map +1 -1
- package/dist/types.d.ts +18 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/usage.d.ts +10 -0
- package/dist/usage.d.ts.map +1 -1
- package/dist/usage.js +14 -0
- package/dist/usage.js.map +1 -1
- package/package.json +1 -1
package/dist/ink-chat.js
CHANGED
|
@@ -8,11 +8,12 @@ import { generateSessionId, saveSession, listSessions, loadSession } from './ses
|
|
|
8
8
|
import { loadMemory } from './memory.js';
|
|
9
9
|
import { loadKoneckConfig, saveKoneckConfig, setConfigKey, configKeyDescriptions, CONFIG_FILE } from './config-store.js';
|
|
10
10
|
import { PROVIDERS, resolveProvider, getApiKey, addUserProvider, userProvidersPath } from './providers.js';
|
|
11
|
-
import { listCheckpoints, revertCheckpoint, snapshotTree } from './checkpoint.js';
|
|
12
|
-
import {
|
|
11
|
+
import { listCheckpoints, revertCheckpoint, snapshotTree, filesTouchedBy } from './checkpoint.js';
|
|
12
|
+
import { fileDiff, diffSummary } from './diff-view.js';
|
|
13
|
+
import { bar, humanTokens, humanDuration, diffStatSince, modelInfoFrom, contextWindowFor, recordStatus, classifyResponse, stateLabel, readModelStatus, computeUiWidth, } from './usage.js';
|
|
13
14
|
import { Markdown, Panel } from './markdown-ink.js';
|
|
14
15
|
import { fetchUpdateStatus, updateCommand } from './update-check.js';
|
|
15
|
-
import { readFileSync } from 'fs';
|
|
16
|
+
import { readFileSync, statSync } from 'fs';
|
|
16
17
|
import { fileURLToPath } from 'url';
|
|
17
18
|
import path from 'path';
|
|
18
19
|
/** Read once at load so the running version can always be shown without a lookup per render. */
|
|
@@ -33,6 +34,13 @@ const AMBER = '#F6C453';
|
|
|
33
34
|
const MUTED = '#9AAAB2';
|
|
34
35
|
const INK = '#F4F1EA';
|
|
35
36
|
const DIM = '#5C717A';
|
|
37
|
+
// Diff colours. Deliberately not terminal green/red: those clash with the warm palette and read
|
|
38
|
+
// as errors. These are the existing accents lifted a little, over bands dark enough to sit on a
|
|
39
|
+
// black terminal without glowing.
|
|
40
|
+
const DIFF_ADD = '#7FE3B0';
|
|
41
|
+
const DIFF_ADD_BG = '#10291F';
|
|
42
|
+
const DIFF_DEL = '#F2909B';
|
|
43
|
+
const DIFF_DEL_BG = '#2B1419';
|
|
36
44
|
// The user's message bar: light-on-dark, high contrast against the terminal ground so an
|
|
37
45
|
// input is easy to find when scrolling back. Deliberately not the cyan used for KONECK.
|
|
38
46
|
const USER_BG = '#3A434A';
|
|
@@ -257,6 +265,10 @@ const EFFORT_BLURB = {
|
|
|
257
265
|
high: 'Thorough — covers edge cases, costs more tokens',
|
|
258
266
|
max: 'Most rigorous analysis available; slowest and priciest',
|
|
259
267
|
};
|
|
268
|
+
/** Past this, a file is not read for diffing — the render would be useless anyway. */
|
|
269
|
+
const MAX_DIFF_BYTES = 512 * 1024;
|
|
270
|
+
/** One tool rarely touches more; the cap stops a bulk move from flooding scrollback. */
|
|
271
|
+
const MAX_DIFF_FILES = 4;
|
|
260
272
|
/** The tabs of the usage pane, in the order they are shown. */
|
|
261
273
|
const USAGE_TABS = ['session', 'context', 'models', 'limits'];
|
|
262
274
|
const MIN_REPLY_WIDTH = 24; // below this a table cannot render legibly anyway
|
|
@@ -331,6 +343,8 @@ function App({ config: initialConfig }) {
|
|
|
331
343
|
// instead of calling setState per token keeps a Raspberry Pi from re-rendering on every chunk.
|
|
332
344
|
const sayRef = useRef(''); // narration not yet committed
|
|
333
345
|
const liveToolRef = useRef(null); // the one tool still running
|
|
346
|
+
/** Pre-edit contents of the files the running tool said it would touch. */
|
|
347
|
+
const pendingDiffRef = useRef([]);
|
|
334
348
|
const charsRef = useRef(0); // streamed chars, for the live token estimate
|
|
335
349
|
const realTokRef = useRef(0); // real output tokens this turn, once the provider reports usage
|
|
336
350
|
const tokBaseRef = useRef(0); // session output-token count at the start of this turn
|
|
@@ -349,25 +363,73 @@ function App({ config: initialConfig }) {
|
|
|
349
363
|
if (text !== '')
|
|
350
364
|
setRows(prev => [...prev, { role: 'steps', steps: [{ kind: 'say', text }] }]);
|
|
351
365
|
}
|
|
366
|
+
/**
|
|
367
|
+
* Reads a file for diffing, or null if it is absent or too big to be worth showing. Reading is
|
|
368
|
+
* synchronous on purpose: the tool runs the instant this callback returns, so an await here
|
|
369
|
+
* would race the very write it is trying to photograph.
|
|
370
|
+
*/
|
|
371
|
+
function readForDiff(rel) {
|
|
372
|
+
try {
|
|
373
|
+
const abs = path.resolve(cfg.cwd, rel);
|
|
374
|
+
if (statSync(abs).size > MAX_DIFF_BYTES)
|
|
375
|
+
return null;
|
|
376
|
+
return readFileSync(abs, 'utf-8');
|
|
377
|
+
}
|
|
378
|
+
catch {
|
|
379
|
+
return null; // absent: a creation, which fileDiff renders as such
|
|
380
|
+
}
|
|
381
|
+
}
|
|
352
382
|
function pushTool(name, args) {
|
|
353
383
|
commitSay();
|
|
384
|
+
// Photograph every file this tool has announced it will touch, before it touches it.
|
|
385
|
+
pendingDiffRef.current = filesTouchedBy(name, args).slice(0, MAX_DIFF_FILES)
|
|
386
|
+
.map(rel => ({ rel, before: readForDiff(rel) }));
|
|
354
387
|
liveToolRef.current = { kind: 'tool', name, detail: toolDetail(name, args), startedAt: Date.now() };
|
|
355
388
|
}
|
|
356
389
|
function finishTool(name, ok, ms) {
|
|
357
390
|
if (!ok)
|
|
358
391
|
turnRecoveredRef.current = true;
|
|
359
392
|
const live = liveToolRef.current;
|
|
393
|
+
const pending = pendingDiffRef.current;
|
|
360
394
|
liveToolRef.current = null;
|
|
395
|
+
pendingDiffRef.current = [];
|
|
361
396
|
if (!live)
|
|
362
397
|
return;
|
|
363
|
-
|
|
398
|
+
// A failed tool leaves the file as it was, so there is nothing to show.
|
|
399
|
+
const diffs = ok
|
|
400
|
+
? pending
|
|
401
|
+
.map(({ rel, before }) => fileDiff(rel, before, readForDiff(rel)))
|
|
402
|
+
.filter(d => !d.unchanged)
|
|
403
|
+
: [];
|
|
404
|
+
const done = { ...live, endedAt: live.startedAt + ms, ok, ...(diffs.length ? { diffs } : {}) };
|
|
364
405
|
setRows(prev => [...prev, { role: 'steps', steps: [done] }]);
|
|
365
406
|
}
|
|
407
|
+
/**
|
|
408
|
+
* Moves the agent panel into scrollback once the turn is over. The live panel is transient by
|
|
409
|
+
* design — it repaints — so without this the record of what the agents did would scroll away
|
|
410
|
+
* with the last repaint and leave nothing behind.
|
|
411
|
+
*/
|
|
412
|
+
function flushAgents() {
|
|
413
|
+
const list = agentsRef.current;
|
|
414
|
+
if (list.length === 0)
|
|
415
|
+
return;
|
|
416
|
+
agentsRef.current = [];
|
|
417
|
+
setAgents([]);
|
|
418
|
+
const done = list.filter(a => a.status === 'done').length;
|
|
419
|
+
const failed = list.filter(a => a.status === 'failed').length;
|
|
420
|
+
const tokens = list.reduce((n, a) => n + a.tokens, 0);
|
|
421
|
+
// The elapsed span is the longest agent, not the sum: they ran at the same time.
|
|
422
|
+
const span = Math.max(...list.map(a => (a.endedAt ?? Date.now()) - a.startedAt));
|
|
423
|
+
addRow({ role: 'steps', steps: [{ kind: 'say', text: `${list.length} parallel agents finished — ${done} succeeded` +
|
|
424
|
+
(failed ? `, ${failed} failed` : '') +
|
|
425
|
+
` · ${humanTokens(tokens)} tokens · ${fmtElapsed(span)}` }] });
|
|
426
|
+
}
|
|
366
427
|
// Session Promise is created eagerly at mount so it resolves before the user's first message.
|
|
367
428
|
const sessionOpts = {
|
|
368
429
|
onChunk: (text) => pushSay(text),
|
|
369
430
|
onToolCall: (name, args) => pushTool(name, args),
|
|
370
431
|
onToolResult: (name, ok, ms) => finishTool(name, ok, ms),
|
|
432
|
+
onAgentProgress: (list) => { agentsRef.current = list; setAgents(list); },
|
|
371
433
|
onTokens: (s) => {
|
|
372
434
|
realTokRef.current = Math.max(0, s.completionTokens - tokBaseRef.current);
|
|
373
435
|
},
|
|
@@ -404,6 +466,29 @@ function App({ config: initialConfig }) {
|
|
|
404
466
|
// compare against. Collapsing the two would flash "not a git repo" at every open.
|
|
405
467
|
const [usageDiff, setUsageDiff] = useState(undefined);
|
|
406
468
|
const [modelCatalog, setModelCatalog] = useState([]);
|
|
469
|
+
// Terminal width, tracked as state rather than read at render. Ink re-renders its own layout on
|
|
470
|
+
// a resize but has no idea a component consulted process.stdout.columns, so without this the UI
|
|
471
|
+
// kept the width it booted with until restarted.
|
|
472
|
+
// `|| 80` not `?? 80`: a pty with no negotiated size reports 0 columns, and 0 would collapse
|
|
473
|
+
// the layout to a single character.
|
|
474
|
+
const [termCols, setTermCols] = useState(() => process.stdout.columns || 80);
|
|
475
|
+
/** Optional user cap on the UI width. Undefined means fill the terminal. */
|
|
476
|
+
const [configMaxWidth, setConfigMaxWidth] = useState(undefined);
|
|
477
|
+
useEffect(() => {
|
|
478
|
+
void loadKoneckConfig().then(c => {
|
|
479
|
+
setConfigMaxWidth(typeof c.maxWidth === 'number' && c.maxWidth >= 40 ? c.maxWidth : undefined);
|
|
480
|
+
});
|
|
481
|
+
}, []);
|
|
482
|
+
useEffect(() => {
|
|
483
|
+
const onResize = () => setTermCols(process.stdout.columns || 80);
|
|
484
|
+
process.stdout.on('resize', onResize);
|
|
485
|
+
return () => { process.stdout.off('resize', onResize); };
|
|
486
|
+
}, []);
|
|
487
|
+
/** Live state of parallel sub-agents. Empty except while spawn_agents is running. */
|
|
488
|
+
const [agents, setAgents] = useState([]);
|
|
489
|
+
// Mirrored in a ref because the turn's exit paths run inside a closure that would otherwise
|
|
490
|
+
// see the state as it was when the turn began.
|
|
491
|
+
const agentsRef = useRef([]);
|
|
407
492
|
// Masked credential entry. The value is held in component state for the session and passed
|
|
408
493
|
// to the client as a runtime override — KONECK never writes it to disk, and it is never put
|
|
409
494
|
// into a transcript row, so it cannot end up in a saved session file.
|
|
@@ -1015,6 +1100,7 @@ function App({ config: initialConfig }) {
|
|
|
1015
1100
|
const val = parts.slice(1).join(' ');
|
|
1016
1101
|
if (key === 'reset') {
|
|
1017
1102
|
await saveKoneckConfig({});
|
|
1103
|
+
setConfigMaxWidth(undefined);
|
|
1018
1104
|
addSystem('Config reset to defaults.');
|
|
1019
1105
|
return;
|
|
1020
1106
|
}
|
|
@@ -1031,6 +1117,10 @@ function App({ config: initialConfig }) {
|
|
|
1031
1117
|
}
|
|
1032
1118
|
if (key === 'effort' && ['low', 'medium', 'high', 'max'].includes(val))
|
|
1033
1119
|
setEffort(val);
|
|
1120
|
+
if (key === 'maxWidth') {
|
|
1121
|
+
const n = Number(val);
|
|
1122
|
+
setConfigMaxWidth(Number.isFinite(n) && n >= 40 ? n : undefined);
|
|
1123
|
+
}
|
|
1034
1124
|
return;
|
|
1035
1125
|
}
|
|
1036
1126
|
if (key) {
|
|
@@ -1044,6 +1134,7 @@ function App({ config: initialConfig }) {
|
|
|
1044
1134
|
const live = {
|
|
1045
1135
|
provider: cfg.provider, model: cfg.model, effort, requireApproval: cfg.requireApproval,
|
|
1046
1136
|
maxTurns: cfg.maxTurns, budget: cfg.budget, baseURL: cfg.baseURL,
|
|
1137
|
+
maxWidth: configMaxWidth ?? `(not set - filling ${termCols} columns)`,
|
|
1047
1138
|
};
|
|
1048
1139
|
const lines2 = Object.entries(descs).map(([k, desc]) => {
|
|
1049
1140
|
const v = live[k] ?? stored[k];
|
|
@@ -1576,6 +1667,7 @@ function App({ config: initialConfig }) {
|
|
|
1576
1667
|
if (controller.signal.aborted) {
|
|
1577
1668
|
sayRef.current = '';
|
|
1578
1669
|
liveToolRef.current = null;
|
|
1670
|
+
flushAgents();
|
|
1579
1671
|
addRow({ role: 'system', text: `Cancelled after ${fmtElapsed(Date.now() - started)}. Anything already done is above; ` +
|
|
1580
1672
|
'your composer is untouched.' });
|
|
1581
1673
|
setAgentState('ready');
|
|
@@ -1588,6 +1680,7 @@ function App({ config: initialConfig }) {
|
|
|
1588
1680
|
const content = last && typeof last.content === 'string'
|
|
1589
1681
|
? last.content : 'Task finished with no written response.';
|
|
1590
1682
|
setLastAgentText(content);
|
|
1683
|
+
flushAgents();
|
|
1591
1684
|
const elapsed = Date.now() - started;
|
|
1592
1685
|
apiMsRef.current += elapsed;
|
|
1593
1686
|
// A turn that completed is the only proof a model actually works that costs nothing
|
|
@@ -1627,6 +1720,7 @@ function App({ config: initialConfig }) {
|
|
|
1627
1720
|
const trailing = sayRef.current.trim();
|
|
1628
1721
|
sayRef.current = '';
|
|
1629
1722
|
liveToolRef.current = null;
|
|
1723
|
+
flushAgents();
|
|
1630
1724
|
setRows(prev => [
|
|
1631
1725
|
...prev,
|
|
1632
1726
|
...(trailing ? [{ role: 'steps', steps: [{ kind: 'say', text: trailing }] }] : []),
|
|
@@ -1654,11 +1748,10 @@ function App({ config: initialConfig }) {
|
|
|
1654
1748
|
return parts.length > 2 ? '.../' + parts.slice(-2).join('/') : full;
|
|
1655
1749
|
})();
|
|
1656
1750
|
const modelShort = cfg.model.split('/').at(-1) ?? cfg.model;
|
|
1657
|
-
//
|
|
1658
|
-
//
|
|
1659
|
-
// narrow
|
|
1660
|
-
const
|
|
1661
|
-
const uiWidth = Math.max(40, Math.min(termCols, 120));
|
|
1751
|
+
// The UI fills the terminal. It used to stop at 120 columns, which left most of a wide
|
|
1752
|
+
// window empty. A cap is still available through `maxWidth` in config for anyone who
|
|
1753
|
+
// prefers a narrow measure on a very wide screen, but nothing is capped by default.
|
|
1754
|
+
const uiWidth = computeUiWidth(termCols, configMaxWidth);
|
|
1662
1755
|
const barWidth = uiWidth - 4; // uiWidth minus the container's paddingX={2} on both sides
|
|
1663
1756
|
// Space a rendered reply actually has: the bar, minus the response box border (2), its
|
|
1664
1757
|
// paddingX={1} (2), and the "ꓘK " prefix plus its gap (3). Getting this even one column
|
|
@@ -1678,6 +1771,29 @@ function App({ config: initialConfig }) {
|
|
|
1678
1771
|
// Live output token count: real usage when the provider reports it, else a char estimate
|
|
1679
1772
|
const liveTokens = Math.max(realTokRef.current, Math.round(charsRef.current / 4));
|
|
1680
1773
|
/** Renders one activity step. Shared by the live view and committed scrollback so they match. */
|
|
1774
|
+
/**
|
|
1775
|
+
* A file change, drawn the way a diff is read: a gutter of line numbers, a sign, and the line
|
|
1776
|
+
* itself on a tinted band. Every rendered line is padded to the same width so the tint forms a
|
|
1777
|
+
* solid block rather than a ragged edge, which is what makes an added run legible at a glance.
|
|
1778
|
+
*/
|
|
1779
|
+
const renderDiff = (d, key) => {
|
|
1780
|
+
const numbers = d.hunks.flatMap(h => h.lines.map(l => l.after ?? l.before ?? 0));
|
|
1781
|
+
const gutter = Math.max(2, String(Math.max(0, ...numbers)).length);
|
|
1782
|
+
const width = Math.max(24, barWidth - gutter - 8);
|
|
1783
|
+
const header = d.created ? 'new file' : d.deleted ? 'deleted' : diffSummary(d);
|
|
1784
|
+
return (_jsxs(Box, { flexDirection: "column", paddingLeft: 2, children: [_jsxs(Box, { gap: 1, children: [_jsx(Text, { color: DIM, children: "\u2514" }), _jsx(Text, { color: CYAN, children: d.path }), _jsx(Text, { color: d.created ? DIFF_ADD : d.deleted ? DIFF_DEL : MUTED, children: header })] }), d.binary
|
|
1785
|
+
? _jsx(Text, { color: DIM, children: ' binary file - not shown' })
|
|
1786
|
+
: d.hunks.map((hunk, hi) => (_jsxs(Box, { flexDirection: "column", children: [hi > 0 && _jsxs(Text, { color: DIM, children: [' '.repeat(gutter), " ..."] }), hunk.lines.map((l, li) => {
|
|
1787
|
+
const shown = l.kind === 'del' ? l.before : l.after;
|
|
1788
|
+
const sign = l.kind === 'add' ? '+' : l.kind === 'del' ? '-' : ' ';
|
|
1789
|
+
const fg = l.kind === 'add' ? DIFF_ADD : l.kind === 'del' ? DIFF_DEL : MUTED;
|
|
1790
|
+
const bg = l.kind === 'add' ? DIFF_ADD_BG : l.kind === 'del' ? DIFF_DEL_BG : undefined;
|
|
1791
|
+
// Tabs would make the tinted block a ragged width, so they are expanded.
|
|
1792
|
+
const body = `${sign} ${l.text}`.replace(/\t/g, ' ');
|
|
1793
|
+
const cell = body.length > width ? body.slice(0, width - 3) + '...' : body.padEnd(width);
|
|
1794
|
+
return (_jsxs(Box, { children: [_jsxs(Text, { color: DIM, children: [String(shown ?? '').padStart(gutter), " "] }), _jsx(Text, { color: fg, ...(bg ? { backgroundColor: bg } : {}), children: cell })] }, li));
|
|
1795
|
+
})] }, hi))), d.hiddenLines > 0 && (_jsxs(Text, { color: DIM, children: [' '.repeat(gutter), " ... ", d.hiddenLines, " more changed lines"] }))] }, key));
|
|
1796
|
+
};
|
|
1681
1797
|
const renderStep = (step, key) => {
|
|
1682
1798
|
if (step.kind === 'say') {
|
|
1683
1799
|
return (_jsxs(Box, { marginTop: 1, gap: 1, children: [_jsx(Text, { color: CYAN, children: "\u25CF" }), _jsx(Text, { color: INK, children: step.text.trim() })] }, key));
|
|
@@ -1686,7 +1802,7 @@ function App({ config: initialConfig }) {
|
|
|
1686
1802
|
const ms = (step.endedAt ?? Date.now()) - step.startedAt;
|
|
1687
1803
|
const glyph = running ? SPINNER[spinFrame] : step.ok === false ? '✖' : '✓';
|
|
1688
1804
|
const color = running ? AMBER : step.ok === false ? CRIMSON : GREEN;
|
|
1689
|
-
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 !== '' && (_jsxs(Box, { paddingLeft: 2, gap: 1, children: [_jsx(Text, { color: DIM, children: "\u2514" }), _jsx(Text, { color: MUTED, children: step.detail })] }))] }, key));
|
|
1805
|
+
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));
|
|
1690
1806
|
};
|
|
1691
1807
|
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' ? (
|
|
1692
1808
|
// Painted as a filled bar so the user's own words are the most findable thing
|
|
@@ -1696,7 +1812,24 @@ function App({ config: initialConfig }) {
|
|
|
1696
1812
|
// content, so the right edge stepped in and out; and a box inside a box left
|
|
1697
1813
|
// markdown tables needlessly narrow. A marker plus the content reads better and
|
|
1698
1814
|
// cannot misalign, which is how Claude Code presents its own answers.
|
|
1699
|
-
_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Box, { gap: 1, children: [_jsx(Text, { color: CYAN, bold: true, children: "\uA7B0K" }), _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() !== '' && renderStep({ kind: 'say', text: tailLines(sayRef.current, 2, replyWidth) }, 0), liveToolRef.current && renderStep(liveToolRef.current, 1),
|
|
1815
|
+
_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Box, { gap: 1, children: [_jsx(Text, { color: CYAN, bold: true, children: "\uA7B0K" }), _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() !== '' && renderStep({ kind: 'say', text: tailLines(sayRef.current, 2, replyWidth) }, 0), liveToolRef.current && renderStep(liveToolRef.current, 1), agents.length > 0 && (() => {
|
|
1816
|
+
const done = agents.filter(a => a.status !== 'running').length;
|
|
1817
|
+
const failed = agents.filter(a => a.status === 'failed').length;
|
|
1818
|
+
const tokens = agents.reduce((n, a) => n + a.tokens, 0);
|
|
1819
|
+
const started = Math.min(...agents.map(a => a.startedAt));
|
|
1820
|
+
// The task text is clipped to whatever is left after the fixed columns, so a long
|
|
1821
|
+
// task never wraps and breaks the alignment of the rows under it.
|
|
1822
|
+
const taskWidth = Math.max(16, barWidth - 34);
|
|
1823
|
+
return (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Box, { gap: 1, children: [_jsx(Text, { color: CYAN, children: "\u25C6" }), _jsxs(Text, { color: INK, bold: true, children: [agents.length, " agents"] }), _jsx(Text, { color: DIM, children: "\u00B7" }), _jsxs(Text, { color: done === agents.length ? GREEN : AMBER, children: [done, "/", agents.length, " complete"] }), _jsx(Text, { color: DIM, children: "\u00B7" }), _jsxs(Text, { color: MUTED, children: [humanTokens(tokens), " tok"] }), _jsx(Text, { color: DIM, children: "\u00B7" }), _jsx(Text, { color: MUTED, children: fmtElapsed(Date.now() - started) }), failed > 0 && _jsxs(Text, { color: CRIMSON, children: ["\u00B7 ", failed, " failed"] })] }), agents.map(a => {
|
|
1824
|
+
const glyph = a.status === 'running' ? SPINNER[spinFrame]
|
|
1825
|
+
: a.status === 'failed' ? '✖' : '✓';
|
|
1826
|
+
const color = a.status === 'running' ? AMBER
|
|
1827
|
+
: a.status === 'failed' ? CRIMSON : GREEN;
|
|
1828
|
+
const took = (a.endedAt ?? Date.now()) - a.startedAt;
|
|
1829
|
+
const task = a.task.length > taskWidth ? a.task.slice(0, taskWidth - 3) + '...' : a.task;
|
|
1830
|
+
return (_jsxs(Box, { paddingLeft: 2, gap: 1, children: [_jsx(Text, { color: color, children: glyph }), _jsx(Text, { color: DIM, children: String(a.index + 1).padStart(2) }), _jsx(Text, { color: a.status === 'running' ? INK : MUTED, children: task.padEnd(taskWidth) }), _jsx(Text, { color: DIM, children: humanTokens(a.tokens).padStart(6) }), _jsx(Text, { color: DIM, children: fmtElapsed(took).padStart(6) })] }, a.index));
|
|
1831
|
+
})] }));
|
|
1832
|
+
})(), _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
|
|
1700
1833
|
? `${fmtTokens(liveTokens)} tokens`
|
|
1701
1834
|
: elapsedMs > 20_000 ? 'waiting for the first token' : 'starting', ")"] })] }) }), _jsx(Box, { paddingLeft: 2, children: _jsx(Text, { color: DIM, children: "esc to cancel and keep your input \u00B7 ctrl+c to exit" }) })] })) }), usagePane && (() => {
|
|
1702
1835
|
const width = Math.min(barWidth, 66);
|