koneck 2.25.8 → 2.25.10
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 +35 -4
- package/dist/ink-chat.d.ts.map +1 -1
- package/dist/ink-chat.js +210 -26
- package/dist/ink-chat.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/package.json +1 -1
package/dist/ink-chat.js
CHANGED
|
@@ -194,6 +194,15 @@ function Shimmer({ text, frame, base }) {
|
|
|
194
194
|
}) }));
|
|
195
195
|
}
|
|
196
196
|
export const WORK_WORDS = {
|
|
197
|
+
// No tool ran: it thought and answered. A cooking metaphor is wrong here — nothing was made.
|
|
198
|
+
reply: [
|
|
199
|
+
['Thinking', 'Thought it through'], ['Weighing it up', 'Weighed it up'],
|
|
200
|
+
['Talking it through', 'Talked it through'], ['Laying it out', 'Laid it out'],
|
|
201
|
+
['Reading you', 'Read you'], ['Working it out', 'Worked it out'],
|
|
202
|
+
['Turning it over', 'Turned it over'], ['Sizing it up', 'Sized it up'],
|
|
203
|
+
['Consulting', 'Consulted'], ['Advising', 'Advised'],
|
|
204
|
+
['Counselling', 'Counselled'], ['Explaining', 'Explained'],
|
|
205
|
+
],
|
|
197
206
|
// Under ~3s. Snappy, almost dismissive — it barely counted as work.
|
|
198
207
|
quick: [
|
|
199
208
|
['Sharp-sharp', 'Sharp-sharp'], ['Doing it now-now', 'Done now-now'],
|
|
@@ -203,6 +212,13 @@ export const WORK_WORDS = {
|
|
|
203
212
|
['Zapping', 'Zapped'], ['Sorting', 'Sorted'],
|
|
204
213
|
['Hot-reloading', 'Hot-reloaded'], ['JIT-compiling', 'JIT-compiled'],
|
|
205
214
|
['Instant-noodling', 'Instant-noodled'], ['Wazza-ing', "Wazza'd"],
|
|
215
|
+
// Counter work: over before you have put your wallet away.
|
|
216
|
+
['Ringing it up', 'Rung up'], ['Swiping the card', 'Card swiped'],
|
|
217
|
+
['Stamping it', 'Stamped'], ['Checking you in', 'Checked in'],
|
|
218
|
+
['Taking the register', 'Register taken'], ['Triaging', 'Triaged'],
|
|
219
|
+
['Waving it through', 'Waved through'], ['Blowing the whistle', 'Whistle blown'],
|
|
220
|
+
['Ticking the box', 'Box ticked'], ['Handing over the key', 'Key handed over'],
|
|
221
|
+
['Weighing it', 'Weighed'], ['Bagging it', 'Bagged'],
|
|
206
222
|
],
|
|
207
223
|
// The everyday middle. General flex and completion.
|
|
208
224
|
normal: [
|
|
@@ -220,6 +236,25 @@ export const WORK_WORDS = {
|
|
|
220
236
|
['Noodling', 'Noodled'], ['Rubber-ducking', 'Rubber-ducked'],
|
|
221
237
|
['Tree-shaking', 'Tree-shook'], ['Reticulating', 'Reticulated'],
|
|
222
238
|
['Letting it cook', 'Let it cook'], ['Getting it oga-certified', 'Oga-certified'],
|
|
239
|
+
// A shift's honest work: clinic, classroom, shop floor, counting house.
|
|
240
|
+
['Doing the rounds', 'Rounds done'], ['Dispensing', 'Dispensed'],
|
|
241
|
+
['Dressing the wound', 'Wound dressed'], ['Taking the pulse', 'Pulse taken'],
|
|
242
|
+
['Marking the papers', 'Papers marked'], ['Setting the homework', 'Homework set'],
|
|
243
|
+
['Taking the lesson', 'Lesson taken'], ['Turning down the beds', 'Beds turned down'],
|
|
244
|
+
['Working the front desk', 'Front desk worked'], ['Laying the tables', 'Tables laid'],
|
|
245
|
+
['Balancing the books', 'Books balanced'], ['Reconciling', 'Reconciled'],
|
|
246
|
+
['Counting the till', 'Till counted'], ['Clearing the cheque', 'Cheque cleared'],
|
|
247
|
+
['Running the line', 'Line ran'], ['Tooling up', 'Tooled up'],
|
|
248
|
+
['Cutting the pattern', 'Pattern cut'], ['Stitching', 'Stitched'],
|
|
249
|
+
['Pressing and finishing', 'Pressed and finished'], ['Grading the sizes', 'Sizes graded'],
|
|
250
|
+
['Filing the papers', 'Papers filed'], ['Taking statements', 'Statements taken'],
|
|
251
|
+
['Serving the summons', 'Summons served'], ['Standing guard', 'Guard stood'],
|
|
252
|
+
['Walking the beat', 'Beat walked'], ['Canvassing', 'Canvassed'],
|
|
253
|
+
['Working the room', 'Room worked'], ['Tabling the motion', 'Motion tabled'],
|
|
254
|
+
['Haggling', 'Haggled'], ['Setting out the stall', 'Stall set out'],
|
|
255
|
+
['Restocking the shelves', 'Shelves restocked'], ['Cashing up', 'Cashed up'],
|
|
256
|
+
['Pounding the mealies', 'Mealies pounded'], ['Beating the drum', 'Drum beaten'],
|
|
257
|
+
['Sitting with the elders', 'Elders consulted'], ['Weaving the mat', 'Mat woven'],
|
|
223
258
|
],
|
|
224
259
|
// Over ~10s. Slow heat and visible effort — it earned the wait.
|
|
225
260
|
heavy: [
|
|
@@ -234,6 +269,18 @@ export const WORK_WORDS = {
|
|
|
234
269
|
['Locking all the way in', 'Locked all the way in'],
|
|
235
270
|
['Negotiating with malloc', 'Negotiated with malloc'],
|
|
236
271
|
['Arguing with the type checker', 'Out-argued the type checker'],
|
|
272
|
+
// The long jobs: a night shift, a whole ward, a case that ran all day.
|
|
273
|
+
['Doing the ward round', 'Ward round done'], ['Scrubbing in', 'Scrubbed out'],
|
|
274
|
+
['Sitting the long clinic', 'Clinic finished'], ['Marking the whole class', 'Whole class marked'],
|
|
275
|
+
['Sitting the exam', 'Exam sat'], ['Running the night shift', 'Night shift done'],
|
|
276
|
+
['Working the full run', 'Full run done'], ['Cutting the whole marker', 'Marker cut'],
|
|
277
|
+
['Closing the books', 'Books closed'], ['Auditing', 'Audited'],
|
|
278
|
+
['Doing the stocktake', 'Stock taken'], ['Arguing the case', 'Case argued'],
|
|
279
|
+
['Sitting in chambers', 'Judgment handed down'], ['Building the docket', 'Docket built'],
|
|
280
|
+
['Doing the night patrol', 'Patrol done'], ['Standing the long watch', 'Watch stood'],
|
|
281
|
+
['Holding the indaba', 'Indaba held'], ['Filibustering', 'Filibustered'],
|
|
282
|
+
['Brewing umqombothi', 'Umqombothi brewed'], ['Keeping the fire going', 'Fire kept going'],
|
|
283
|
+
['Walking it to market', 'Walked it to market'],
|
|
237
284
|
['Herding promises', 'Herded the promises'],
|
|
238
285
|
['Bribing the scheduler', 'Bribed the scheduler'],
|
|
239
286
|
['Smashing wazobia', 'Smashed wazobia'],
|
|
@@ -254,20 +301,49 @@ export const WORK_WORDS = {
|
|
|
254
301
|
['Duct-taping', 'Duct-taped'],
|
|
255
302
|
],
|
|
256
303
|
};
|
|
257
|
-
/**
|
|
258
|
-
|
|
259
|
-
|
|
304
|
+
/**
|
|
305
|
+
* Whether the turn earned a verb at all.
|
|
306
|
+
*
|
|
307
|
+
* "Cooked lekker in 5.0s" for answering "hi" is a boast about nothing: no tool ran, ten tokens
|
|
308
|
+
* came back, and the only reason it took five seconds is that the provider is slow. Elapsed time
|
|
309
|
+
* alone cannot tell effort from latency, so a turn that ran no tools and said almost nothing gets
|
|
310
|
+
* its timing reported plainly and no verb.
|
|
311
|
+
*/
|
|
312
|
+
export function shouldName(effort) {
|
|
313
|
+
if (effort.recovered)
|
|
314
|
+
return true; // recovering from a failure is real work
|
|
315
|
+
if (effort.tools > 0)
|
|
316
|
+
return true; // it did something to the world
|
|
317
|
+
return effort.tokens >= 60; // a substantial answer is work; "hi" is not
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* Which family of words fits.
|
|
321
|
+
*
|
|
322
|
+
* Time still decides between quick, normal and heavy, but only once the turn has done something.
|
|
323
|
+
* A turn with no tools belongs to `reply` however long it took, because nothing was built and a
|
|
324
|
+
* cooking verb would be describing work that never happened.
|
|
325
|
+
*/
|
|
326
|
+
export function tierFor(effort) {
|
|
327
|
+
if (effort.recovered)
|
|
260
328
|
return 'recovery';
|
|
261
|
-
if (
|
|
329
|
+
if (effort.tools === 0)
|
|
330
|
+
return 'reply';
|
|
331
|
+
if (effort.elapsedMs < 3_000)
|
|
262
332
|
return 'quick';
|
|
263
|
-
if (elapsedMs > 10_000)
|
|
333
|
+
if (effort.elapsedMs > 10_000)
|
|
264
334
|
return 'heavy';
|
|
265
335
|
return 'normal';
|
|
266
336
|
}
|
|
267
|
-
/**
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
337
|
+
/**
|
|
338
|
+
* The gerund to show while working, and the past tense to report when the turn lands.
|
|
339
|
+
*
|
|
340
|
+
* The past tense is empty when the turn did not earn one, which the caller renders as timing
|
|
341
|
+
* alone.
|
|
342
|
+
*/
|
|
343
|
+
export function workWord(effort, seed) {
|
|
344
|
+
const bucket = WORK_WORDS[tierFor(effort)];
|
|
345
|
+
const [gerund, past] = bucket[Math.abs(seed) % bucket.length];
|
|
346
|
+
return [gerund, shouldName(effort) ? past : ''];
|
|
271
347
|
}
|
|
272
348
|
/** Every slash command, shown in the `/` palette and filtered as you type. */
|
|
273
349
|
const COMMANDS = [
|
|
@@ -284,6 +360,7 @@ const COMMANDS = [
|
|
|
284
360
|
{ cmd: '/ask', desc: 'Ask another session and wait: /ask <repo> <question>' },
|
|
285
361
|
{ cmd: '/agents', desc: 'How parallel sub-agents are configured' },
|
|
286
362
|
{ cmd: '/subtask', desc: 'Run a task in an isolated sub-agent' },
|
|
363
|
+
{ cmd: '/btw', desc: 'Ask a side question without interrupting the running task' },
|
|
287
364
|
{ cmd: '/clear', desc: 'Clear conversation history' },
|
|
288
365
|
{ cmd: '/new', desc: 'New session (alias of /clear)' },
|
|
289
366
|
{ cmd: '/reset', desc: 'Reset session (alias of /clear)' },
|
|
@@ -387,6 +464,7 @@ Slash commands:
|
|
|
387
464
|
/mode [auto|plan|code] Show or set mode
|
|
388
465
|
/effort [low|med|high|max] Set reasoning effort
|
|
389
466
|
/config [key] [value] View or set persistent config
|
|
467
|
+
/btw <question> Ask a side question without interrupting the running task
|
|
390
468
|
/usage Usage pane: session, context, models, limits
|
|
391
469
|
/tokens Show token usage breakdown
|
|
392
470
|
/cost Show session cost
|
|
@@ -417,6 +495,8 @@ function App({ config: initialConfig }) {
|
|
|
417
495
|
const replyStartedRef = useRef(false);
|
|
418
496
|
/** Everything streamed this turn. Empty means the provider did not stream at all. */
|
|
419
497
|
const streamedRef = useRef('');
|
|
498
|
+
/** When the live region last had something new to show, used to pick the repaint rate. */
|
|
499
|
+
const lastActivityRef = useRef(Date.now());
|
|
420
500
|
/** Frames received before any content — proof the provider has the request and is working. */
|
|
421
501
|
const [providerFrames, setProviderFrames] = useState(0);
|
|
422
502
|
/**
|
|
@@ -435,6 +515,13 @@ function App({ config: initialConfig }) {
|
|
|
435
515
|
const entriesRef = useRef(null);
|
|
436
516
|
/** Where the `@` being completed starts in the draft. */
|
|
437
517
|
const atStartRef = useRef(-1);
|
|
518
|
+
/**
|
|
519
|
+
* Prompts typed while a turn was running.
|
|
520
|
+
*
|
|
521
|
+
* Enter used to be swallowed outright, so a thought had to be held until the agent finished or
|
|
522
|
+
* typed again from memory. Queued prompts run in order as soon as the current turn ends.
|
|
523
|
+
*/
|
|
524
|
+
const [queued, setQueued] = useState([]);
|
|
438
525
|
/** Pre-edit contents of the files the running tool said it would touch. */
|
|
439
526
|
const pendingDiffRef = useRef([]);
|
|
440
527
|
const charsRef = useRef(0); // streamed chars, for the live token estimate
|
|
@@ -456,6 +543,7 @@ function App({ config: initialConfig }) {
|
|
|
456
543
|
* whatever is left in the buffer.
|
|
457
544
|
*/
|
|
458
545
|
function pushSay(text) {
|
|
546
|
+
lastActivityRef.current = Date.now();
|
|
459
547
|
sayRef.current += text;
|
|
460
548
|
streamedRef.current += text;
|
|
461
549
|
charsRef.current += text.length;
|
|
@@ -500,6 +588,8 @@ function App({ config: initialConfig }) {
|
|
|
500
588
|
function pushTool(name, args) {
|
|
501
589
|
commitSay();
|
|
502
590
|
// Photograph every file this tool has announced it will touch, before it touches it.
|
|
591
|
+
lastActivityRef.current = Date.now();
|
|
592
|
+
turnToolsRef.current += 1;
|
|
503
593
|
pendingDiffRef.current = filesTouchedBy(name, args).slice(0, MAX_DIFF_FILES)
|
|
504
594
|
.map(rel => ({ rel, before: readForDiff(rel) }));
|
|
505
595
|
liveToolRef.current = { kind: 'tool', name, detail: toolDetail(name, args), startedAt: Date.now() };
|
|
@@ -547,6 +637,11 @@ function App({ config: initialConfig }) {
|
|
|
547
637
|
onChunk: (text) => pushSay(text),
|
|
548
638
|
onToolCall: (name, args) => pushTool(name, args),
|
|
549
639
|
onToolResult: (name, ok, ms) => finishTool(name, ok, ms),
|
|
640
|
+
onToolOutput: (_name, line) => {
|
|
641
|
+
lastActivityRef.current = Date.now();
|
|
642
|
+
if (liveToolRef.current)
|
|
643
|
+
liveToolRef.current.output = line;
|
|
644
|
+
},
|
|
550
645
|
onStreamActivity: (frames) => setProviderFrames(frames),
|
|
551
646
|
onAgentProgress: (list) => { agentsRef.current = list; setAgents(list); },
|
|
552
647
|
onTokens: (s) => {
|
|
@@ -672,7 +767,9 @@ function App({ config: initialConfig }) {
|
|
|
672
767
|
* killing the session or losing what is already in the composer. */
|
|
673
768
|
const abortRef = useRef(null);
|
|
674
769
|
const turnWordRef = useRef(0); // rotation seed, so consecutive turns differ
|
|
675
|
-
const turnRecoveredRef = useRef(false);
|
|
770
|
+
const turnRecoveredRef = useRef(false);
|
|
771
|
+
/** Tools this turn has run, which is what separates work from a plain answer. */
|
|
772
|
+
const turnToolsRef = useRef(0); // a tool failed and was worked around
|
|
676
773
|
const busyStart = useRef(0);
|
|
677
774
|
const wordTimer = useRef(0);
|
|
678
775
|
// Register on the session bus so other running KONECK instances can reach this one.
|
|
@@ -732,20 +829,47 @@ function App({ config: initialConfig }) {
|
|
|
732
829
|
}
|
|
733
830
|
busyStart.current = Date.now();
|
|
734
831
|
wordTimer.current = 0;
|
|
735
|
-
//
|
|
736
|
-
//
|
|
737
|
-
|
|
832
|
+
// The frame rate is the flicker rate. Every tick makes Ink erase and rewrite the whole live
|
|
833
|
+
// region — measured at 188 line-erases a second, which on a modest terminal reads as the
|
|
834
|
+
// bottom of the screen flashing. So the rate follows how much is actually happening: quick
|
|
835
|
+
// while output is arriving, and much slower once a single long command is all that is left,
|
|
836
|
+
// where a spinner turning four times a second says as much as one turning seven.
|
|
837
|
+
let timer;
|
|
838
|
+
const tick = () => {
|
|
839
|
+
const elapsed = Date.now() - busyStart.current;
|
|
738
840
|
setSpinFrame(f => (f + 1) % SPINNER.length);
|
|
739
841
|
setShimmerFrame(f => f + 1);
|
|
740
|
-
setElapsedMs(
|
|
842
|
+
setElapsedMs(elapsed);
|
|
741
843
|
wordTimer.current += 1;
|
|
742
|
-
//
|
|
743
|
-
// still
|
|
744
|
-
if (wordTimer.current %
|
|
745
|
-
setSpinWord(w => w + 1);
|
|
746
|
-
|
|
747
|
-
|
|
844
|
+
// Roughly every 25 seconds, whatever the current rate. Long enough to read the word,
|
|
845
|
+
// notice it, and still see it change before the wait starts feeling stuck.
|
|
846
|
+
if (wordTimer.current % Math.max(20, Math.round(25_000 / currentDelay())) === 0) {
|
|
847
|
+
setSpinWord(w => w + 1);
|
|
848
|
+
}
|
|
849
|
+
timer = setTimeout(tick, currentDelay());
|
|
850
|
+
};
|
|
851
|
+
const currentDelay = () => {
|
|
852
|
+
const idleFor = Date.now() - lastActivityRef.current;
|
|
853
|
+
// Something is still arriving, so keep it lively.
|
|
854
|
+
if (idleFor < 2_000)
|
|
855
|
+
return 150;
|
|
856
|
+
// A long wait with nothing to show: slow right down.
|
|
857
|
+
return idleFor > 15_000 ? 500 : 250;
|
|
858
|
+
};
|
|
859
|
+
timer = setTimeout(tick, 150);
|
|
860
|
+
return () => clearTimeout(timer);
|
|
748
861
|
}, [busy]);
|
|
862
|
+
// A queued prompt runs the moment the agent is free again. Keyed on `busy` rather than done
|
|
863
|
+
// inside the turn's own exit paths, so every way a turn can end — success, failure, cancel —
|
|
864
|
+
// drains the queue the same way.
|
|
865
|
+
useEffect(() => {
|
|
866
|
+
if (busy || queued.length === 0)
|
|
867
|
+
return;
|
|
868
|
+
const [next, ...rest] = queued;
|
|
869
|
+
setQueued(rest);
|
|
870
|
+
if (next)
|
|
871
|
+
submitDraft(next);
|
|
872
|
+
}, [busy, queued]);
|
|
749
873
|
// Ask the terminal to bracket pasted text. Without this a paste is indistinguishable from
|
|
750
874
|
// typing, and there is no way to keep it out of the composer or preserve its line breaks.
|
|
751
875
|
useEffect(() => {
|
|
@@ -878,6 +1002,35 @@ function App({ config: initialConfig }) {
|
|
|
878
1002
|
return []; // the pane says so rather than showing zeroes
|
|
879
1003
|
}
|
|
880
1004
|
}
|
|
1005
|
+
/**
|
|
1006
|
+
* Answers a question without disturbing the turn in flight.
|
|
1007
|
+
*
|
|
1008
|
+
* The agent's own conversation is left untouched: the question runs in a separate, isolated
|
|
1009
|
+
* session, so asking "which model am I on?" mid-build neither interrupts the build nor pollutes
|
|
1010
|
+
* the history the agent is reasoning over. It is the one thing that may jump the queue.
|
|
1011
|
+
*/
|
|
1012
|
+
async function askAside(question) {
|
|
1013
|
+
if (!question.trim()) {
|
|
1014
|
+
addSystem('Usage: /btw <question> — answers alongside the running task, without interrupting it.');
|
|
1015
|
+
return;
|
|
1016
|
+
}
|
|
1017
|
+
addRow({ role: 'user', text: `/btw ${question}` });
|
|
1018
|
+
try {
|
|
1019
|
+
const { runAgent } = await import('./engine.js');
|
|
1020
|
+
const result = await runAgent(question, {
|
|
1021
|
+
...cfg, ci: true, silent: true,
|
|
1022
|
+
// A side question answers from what it is told; it must not edit files behind the
|
|
1023
|
+
// running task's back, so it is given no reason to and kept to a couple of turns.
|
|
1024
|
+
maxTurns: Math.min(cfg.maxTurns, 3),
|
|
1025
|
+
});
|
|
1026
|
+
const last = [...result.messages].reverse()
|
|
1027
|
+
.find(m => m.role === 'assistant' && typeof m.content === 'string' && m.content.trim());
|
|
1028
|
+
addRow({ role: 'agent', text: typeof last?.content === 'string' ? last.content : '(no answer)' });
|
|
1029
|
+
}
|
|
1030
|
+
catch (e) {
|
|
1031
|
+
addSystem(`/btw failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
881
1034
|
/** Opens the update panel, checking the registry each time it is opened deliberately. */
|
|
882
1035
|
async function openUpdatePane() {
|
|
883
1036
|
setUpdatePane({ current: VERSION, latest: null, behind: false, manager: 'npm' });
|
|
@@ -1151,6 +1304,11 @@ function App({ config: initialConfig }) {
|
|
|
1151
1304
|
addSystem(`Effort → ${arg}`);
|
|
1152
1305
|
return;
|
|
1153
1306
|
}
|
|
1307
|
+
case '/btw': {
|
|
1308
|
+
// Also available when nothing is running, where it simply answers in a clean context.
|
|
1309
|
+
await askAside(arg);
|
|
1310
|
+
return;
|
|
1311
|
+
}
|
|
1154
1312
|
case '/usage': {
|
|
1155
1313
|
const wanted = USAGE_TABS.find(t => t.startsWith(arg.trim().toLowerCase()));
|
|
1156
1314
|
await openUsagePane(wanted ?? 'session');
|
|
@@ -1769,8 +1927,24 @@ function App({ config: initialConfig }) {
|
|
|
1769
1927
|
// while it works can be typed as it arrives rather than held until the turn ends — and
|
|
1770
1928
|
// cancelling with esc then hands the keyboard back to a composer that still has it.
|
|
1771
1929
|
if (key.return) {
|
|
1772
|
-
if (busy)
|
|
1930
|
+
if (busy) {
|
|
1931
|
+
const text = draft.trim();
|
|
1932
|
+
if (!text)
|
|
1933
|
+
return;
|
|
1934
|
+
// A side question is answered straight away and separately; everything else waits its turn.
|
|
1935
|
+
if (/^\/btw(\s|$)/.test(text)) {
|
|
1936
|
+
setDraft('');
|
|
1937
|
+
setCaret(0);
|
|
1938
|
+
void askAside(text.replace(/^\/btw\s*/, ''));
|
|
1939
|
+
return;
|
|
1940
|
+
}
|
|
1941
|
+
setQueued(q => [...q, expandPastes(text, pastesRef.current)]);
|
|
1942
|
+
setDraft('');
|
|
1943
|
+
setCaret(0);
|
|
1944
|
+
pastesRef.current = [];
|
|
1945
|
+
setPasteCount(0);
|
|
1773
1946
|
return;
|
|
1947
|
+
}
|
|
1774
1948
|
submitDraft(draft);
|
|
1775
1949
|
return;
|
|
1776
1950
|
}
|
|
@@ -1917,6 +2091,7 @@ function App({ config: initialConfig }) {
|
|
|
1917
2091
|
sayRef.current = '';
|
|
1918
2092
|
streamedRef.current = '';
|
|
1919
2093
|
replyStartedRef.current = false;
|
|
2094
|
+
lastActivityRef.current = Date.now();
|
|
1920
2095
|
setProviderFrames(0);
|
|
1921
2096
|
liveToolRef.current = null;
|
|
1922
2097
|
charsRef.current = 0;
|
|
@@ -1924,6 +2099,7 @@ function App({ config: initialConfig }) {
|
|
|
1924
2099
|
tokBaseRef.current = stats.completionTokens;
|
|
1925
2100
|
turnWordRef.current = spinWord;
|
|
1926
2101
|
turnRecoveredRef.current = false;
|
|
2102
|
+
turnToolsRef.current = 0;
|
|
1927
2103
|
void (async () => {
|
|
1928
2104
|
const outTokBefore = stats.completionTokens;
|
|
1929
2105
|
try {
|
|
@@ -1978,7 +2154,12 @@ function App({ config: initialConfig }) {
|
|
|
1978
2154
|
{ role: 'agent', text: body,
|
|
1979
2155
|
...(streamed !== '' && replyStartedRef.current ? { continued: true } : {}),
|
|
1980
2156
|
tokens: Math.max(0, active.stats.completionTokens - outTokBefore), elapsed,
|
|
1981
|
-
done: workWord(
|
|
2157
|
+
done: workWord({
|
|
2158
|
+
elapsedMs: elapsed,
|
|
2159
|
+
tools: turnToolsRef.current,
|
|
2160
|
+
tokens: Math.max(0, active.stats.completionTokens - outTokBefore),
|
|
2161
|
+
recovered: turnRecoveredRef.current,
|
|
2162
|
+
}, turnWordRef.current)[1] },
|
|
1982
2163
|
]);
|
|
1983
2164
|
setAgentState('completed');
|
|
1984
2165
|
setTimeout(() => setAgentState('ready'), 700);
|
|
@@ -2080,7 +2261,7 @@ function App({ config: initialConfig }) {
|
|
|
2080
2261
|
const ms = (step.endedAt ?? Date.now()) - step.startedAt;
|
|
2081
2262
|
const glyph = running ? SPINNER[spinFrame] : step.ok === false ? '✖' : '✓';
|
|
2082
2263
|
const color = running ? AMBER : step.ok === false ? CRIMSON : GREEN;
|
|
2083
|
-
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));
|
|
2264
|
+
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));
|
|
2084
2265
|
};
|
|
2085
2266
|
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' ? (
|
|
2086
2267
|
// Painted as a filled bar so the user's own words are the most findable thing
|
|
@@ -2096,8 +2277,8 @@ function App({ config: initialConfig }) {
|
|
|
2096
2277
|
// content, so the right edge stepped in and out; and a box inside a box left
|
|
2097
2278
|
// markdown tables needlessly narrow. A marker plus the content reads better and
|
|
2098
2279
|
// cannot misalign, which is how Claude Code presents its own answers.
|
|
2099
|
-
_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
|
|
2100
|
-
.split('\n').map((line, i) => _jsx(Text, { color: INK, children: line }, i)) })] })), liveToolRef.current && renderStep(liveToolRef.current, 1), agents.length > 0 && (() => {
|
|
2280
|
+
_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 ? `${row.done} in ${fmtElapsed(row.elapsed ?? 0)}` : 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)
|
|
2281
|
+
.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 && (() => {
|
|
2101
2282
|
const done = agents.filter(a => a.status !== 'running').length;
|
|
2102
2283
|
const failed = agents.filter(a => a.status === 'failed').length;
|
|
2103
2284
|
const tokens = agents.reduce((n, a) => n + a.tokens, 0);
|
|
@@ -2114,7 +2295,10 @@ function App({ config: initialConfig }) {
|
|
|
2114
2295
|
const task = a.task.length > taskWidth ? a.task.slice(0, taskWidth - 3) + '...' : a.task;
|
|
2115
2296
|
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));
|
|
2116
2297
|
})] }));
|
|
2117
|
-
})(), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { children: [_jsxs(Text, { color: GREEN, children: [SPINNER[spinFrame], " "] }), _jsx(Shimmer, { text: workWord(
|
|
2298
|
+
})(), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { children: [_jsxs(Text, { color: GREEN, children: [SPINNER[spinFrame], " "] }), _jsx(Shimmer, { text: workWord({
|
|
2299
|
+
elapsedMs, tools: turnToolsRef.current,
|
|
2300
|
+
tokens: liveTokens, recovered: turnRecoveredRef.current,
|
|
2301
|
+
}, spinWord)[0], frame: shimmerFrame, base: MUTED }), _jsx(Text, { color: MUTED, children: "\u2026 " }), _jsxs(Text, { color: DIM, children: ["(", fmtElapsed(elapsedMs), " | ", liveTokens > 0
|
|
2118
2302
|
? `${fmtTokens(liveTokens)} tokens`
|
|
2119
2303
|
// A gateway routing to several backends sends content-free frames while it
|
|
2120
2304
|
// finds one. Saying "waiting for the first token" through that reads as a
|