koneck 2.128.36 → 2.128.38
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 +4 -0
- package/dist/config-store.d.ts.map +1 -1
- package/dist/config-store.js +12 -2
- package/dist/config-store.js.map +1 -1
- package/dist/config.d.ts +4 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js.map +1 -1
- package/dist/context-limit.d.ts +27 -1
- package/dist/context-limit.d.ts.map +1 -1
- package/dist/context-limit.js +42 -6
- package/dist/context-limit.js.map +1 -1
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +4 -2
- package/dist/engine.js.map +1 -1
- package/dist/failover.d.ts.map +1 -1
- package/dist/failover.js +21 -2
- package/dist/failover.js.map +1 -1
- package/dist/frame-sync.d.ts +22 -0
- package/dist/frame-sync.d.ts.map +1 -1
- package/dist/frame-sync.js +33 -0
- package/dist/frame-sync.js.map +1 -1
- package/dist/index.js +5 -1
- package/dist/index.js.map +1 -1
- package/dist/ink-chat.d.ts.map +1 -1
- package/dist/ink-chat.js +167 -11
- package/dist/ink-chat.js.map +1 -1
- package/dist/types.d.ts +4 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/ink-chat.js
CHANGED
|
@@ -4,7 +4,7 @@ import { Box, Static, Text, render, useApp, useInput, useStdin } from 'ink';
|
|
|
4
4
|
import { execa } from 'execa';
|
|
5
5
|
import chalk from 'chalk';
|
|
6
6
|
import { resolveAgentConcurrency, toolsUnsupportedNotice } from './engine-facts.js';
|
|
7
|
-
import { withSynchronizedFrames, probeSynchronizedOutput } from './frame-sync.js';
|
|
7
|
+
import { withSynchronizedFrames, withUsableSize, probeSynchronizedOutput } from './frame-sync.js';
|
|
8
8
|
import { pacedInterval, startingInterval, floorFor, shouldDrawFrame } from './frame-pace.js';
|
|
9
9
|
import { estimateCost, formatCost, rateText } from './pricing.js';
|
|
10
10
|
import { generateSessionId, saveSession, listSessions, loadSession, relativeAge, renameSession, forkSession, archiveSession, findSession, } from './session.js';
|
|
@@ -48,6 +48,7 @@ const VERSION = (() => {
|
|
|
48
48
|
import { startBus, listSessions as listBusSessions, resolveTarget, deliver, } from './session-bus.js';
|
|
49
49
|
import { MODES as MODE_SPECS, modeSpec, decide, loadRules, saveRules, addRule, commandPrefixSuggestion, describeRule, ruleMatches, toolRisk, isReadOnlyCommand, } from './permissions.js';
|
|
50
50
|
import { AskQueue } from './ask-queue.js';
|
|
51
|
+
import { contextLimitWithSource } from './context-limit.js';
|
|
51
52
|
import { planSummary } from './plan.js';
|
|
52
53
|
import { savePlan, plansDir, PLAN_CHOICES, PROCEED_PROMPT } from './plan-file.js';
|
|
53
54
|
import { logoFor } from './logo.js';
|
|
@@ -2052,8 +2053,25 @@ export function App({ config: initialConfig, clearFrame, synchronizedFrames = fa
|
|
|
2052
2053
|
liveToolRef.current.output = line;
|
|
2053
2054
|
},
|
|
2054
2055
|
onStreamActivity: (frames) => {
|
|
2056
|
+
/*
|
|
2057
|
+
* A keep-alive is not news, and only the first one changes anything on screen.
|
|
2058
|
+
*
|
|
2059
|
+
* A gateway looking for a backend sends content-free frames, several a second, for as long
|
|
2060
|
+
* as it takes. Each one asked for a repaint, so a turn waiting on a slow provider redrew
|
|
2061
|
+
* the interface two or three times a second to show a clock — measured against the live
|
|
2062
|
+
* session: 13KB a second written to the terminal and 13% of a core, two hours into a turn
|
|
2063
|
+
* that was still waiting. On a terminal that cannot composite a redraw, every one of those
|
|
2064
|
+
* is a visible flash.
|
|
2065
|
+
*
|
|
2066
|
+
* The first frame is worth painting: it is what turns "starting" into "the provider is
|
|
2067
|
+
* holding the line, no output yet". After that nothing it carries is new, and the heartbeat
|
|
2068
|
+
* — once a second, and only while nothing else is painting — is what says the session is
|
|
2069
|
+
* still alive.
|
|
2070
|
+
*/
|
|
2071
|
+
const first = providerFramesRef.current === 0 && frames > 0;
|
|
2055
2072
|
providerFramesRef.current = frames;
|
|
2056
|
-
|
|
2073
|
+
if (first)
|
|
2074
|
+
requestIncomingPaint(true);
|
|
2057
2075
|
},
|
|
2058
2076
|
onIncomplete: (reason) => { incompleteRef.current = reason; },
|
|
2059
2077
|
onToolsUnsupported: (model) => {
|
|
@@ -2751,7 +2769,7 @@ export function App({ config: initialConfig, clearFrame, synchronizedFrames = fa
|
|
|
2751
2769
|
* a keystroke.
|
|
2752
2770
|
*/
|
|
2753
2771
|
if (prev !== cols)
|
|
2754
|
-
redrawScreenRef.current?.();
|
|
2772
|
+
redrawScreenRef.current?.({ keepScrollback: true });
|
|
2755
2773
|
return cols;
|
|
2756
2774
|
});
|
|
2757
2775
|
};
|
|
@@ -3209,7 +3227,17 @@ export function App({ config: initialConfig, clearFrame, synchronizedFrames = fa
|
|
|
3209
3227
|
* lives in the session rather than in the terminal's memory of it.
|
|
3210
3228
|
*/
|
|
3211
3229
|
redrawScreenRef.current = redrawScreen;
|
|
3212
|
-
|
|
3230
|
+
/**
|
|
3231
|
+
* Repaint the terminal from scratch.
|
|
3232
|
+
*
|
|
3233
|
+
* `keepScrollback` is the difference between the two callers. Ctrl+L is somebody asking for a
|
|
3234
|
+
* clean slate after something has gone wrong on screen, and a stranded frame that has scrolled
|
|
3235
|
+
* out of view is still in the buffer waiting to come back — so that one takes the scrollback
|
|
3236
|
+
* with it. A resize is not a request for anything: it re-flows the conversation because the
|
|
3237
|
+
* width changed, and throwing away everything the person had scrolled past would be a far
|
|
3238
|
+
* larger thing than they asked for.
|
|
3239
|
+
*/
|
|
3240
|
+
function redrawScreen(opts = {}) {
|
|
3213
3241
|
if (!process.stdout.isTTY)
|
|
3214
3242
|
return;
|
|
3215
3243
|
// Ink's own clear first. It erases the frame using its own count of it and then resets that
|
|
@@ -3219,7 +3247,7 @@ export function App({ config: initialConfig, clearFrame, synchronizedFrames = fa
|
|
|
3219
3247
|
// Then the screen, and the scrollback with it — a stranded frame that has scrolled out of view
|
|
3220
3248
|
// is still in the buffer, and leaving it there means it comes back on the next scroll — and
|
|
3221
3249
|
// home the cursor, which is where Ink will now draw.
|
|
3222
|
-
process.stdout.write('\u001b[2J\u001b[3J\u001b[H');
|
|
3250
|
+
process.stdout.write(opts.keepScrollback ? '\u001b[2J\u001b[H' : '\u001b[2J\u001b[3J\u001b[H');
|
|
3223
3251
|
// The transcript has just been erased along with everything else, so it has to be written
|
|
3224
3252
|
// again — and writing it again is also what re-flows it to the current width.
|
|
3225
3253
|
setStaticEpoch(n => n + 1);
|
|
@@ -3329,7 +3357,19 @@ export function App({ config: initialConfig, clearFrame, synchronizedFrames = fa
|
|
|
3329
3357
|
...cfg,
|
|
3330
3358
|
provider,
|
|
3331
3359
|
model: meta.model || cfg.model,
|
|
3332
|
-
|
|
3360
|
+
/*
|
|
3361
|
+
* An endpoint the session was started with survives the resume.
|
|
3362
|
+
*
|
|
3363
|
+
* This re-resolved from the provider's saved endpoint unconditionally, which is right
|
|
3364
|
+
* when the resume changes provider and wrong when it does not: a session started against
|
|
3365
|
+
* an explicit `--base-url`, a company gateway, or a local runtime on another machine was
|
|
3366
|
+
* silently moved back to whatever the config file named. The request then went somewhere
|
|
3367
|
+
* the person had deliberately pointed away from, and the first sign of it was a rejected
|
|
3368
|
+
* credential from an endpoint they were not using.
|
|
3369
|
+
*/
|
|
3370
|
+
baseURL: provider !== cfg.provider
|
|
3371
|
+
? endpointFor(provider).url
|
|
3372
|
+
: (cfg.baseURL ?? endpointFor(provider).url),
|
|
3333
3373
|
...(typeof meta.requireApproval === 'boolean' ? { requireApproval: meta.requireApproval } : {}),
|
|
3334
3374
|
};
|
|
3335
3375
|
cfgRef.current = restoredCfg;
|
|
@@ -3359,14 +3399,58 @@ export function App({ config: initialConfig, clearFrame, synchronizedFrames = fa
|
|
|
3359
3399
|
setSessionId(id); // continue this session rather than forking a new one
|
|
3360
3400
|
resumedIdRef.current = id;
|
|
3361
3401
|
startedAtRef.current = meta.startedAt ?? startedAtRef.current;
|
|
3402
|
+
/*
|
|
3403
|
+
* The conversation as it was, tool calls included.
|
|
3404
|
+
*
|
|
3405
|
+
* Only prose was restored before, so a resumed session came back as a handful of stray
|
|
3406
|
+
* sentences with nothing between them — every file read, command and edit dropped on the
|
|
3407
|
+
* floor. Measured on a real saved session: eighteen messages, of which five were the
|
|
3408
|
+
* assistant calling tools and nine were the results, and seven survived the restore.
|
|
3409
|
+
* Reported as resuming "differently, as a summary".
|
|
3410
|
+
*
|
|
3411
|
+
* A tool result says whether it worked, so the marks are the ones the session showed at the
|
|
3412
|
+
* time rather than a row of hopeful ticks. There are no timings in the file — durations are
|
|
3413
|
+
* not saved — so each step is written as having taken no measurable time, which is honest:
|
|
3414
|
+
* this is a record of what happened, not a replay of how long it took.
|
|
3415
|
+
*/
|
|
3416
|
+
const resultFor = new Map();
|
|
3417
|
+
for (const m of messages) {
|
|
3418
|
+
if (m.role !== 'tool')
|
|
3419
|
+
continue;
|
|
3420
|
+
const body = typeof m.content === 'string' ? m.content : '';
|
|
3421
|
+
resultFor.set(String(m.tool_call_id ?? ''), {
|
|
3422
|
+
// The engine writes failures into the result body; nothing else records the outcome.
|
|
3423
|
+
ok: !/^\s*(error|failed|could not|cannot)\b/i.test(body),
|
|
3424
|
+
text: body,
|
|
3425
|
+
});
|
|
3426
|
+
}
|
|
3362
3427
|
const restored = [{ role: 'header' }];
|
|
3363
3428
|
for (const m of messages) {
|
|
3364
3429
|
if (m.role === 'user' && typeof m.content === 'string' && !m.content.startsWith('[')) {
|
|
3365
3430
|
restored.push({ role: 'user', text: m.content });
|
|
3431
|
+
continue;
|
|
3366
3432
|
}
|
|
3367
|
-
|
|
3433
|
+
if (m.role !== 'assistant')
|
|
3434
|
+
continue;
|
|
3435
|
+
if (typeof m.content === 'string' && m.content.trim()) {
|
|
3368
3436
|
restored.push({ role: 'agent', text: m.content });
|
|
3369
3437
|
}
|
|
3438
|
+
const calls = m.tool_calls;
|
|
3439
|
+
if (!calls?.length)
|
|
3440
|
+
continue;
|
|
3441
|
+
const steps = calls.map(c => {
|
|
3442
|
+
const name = c.function?.name ?? 'tool';
|
|
3443
|
+
const result = resultFor.get(String(c.id ?? ''));
|
|
3444
|
+
return {
|
|
3445
|
+
kind: 'tool',
|
|
3446
|
+
name,
|
|
3447
|
+
detail: toolDetail(name, c.function?.arguments ?? '{}'),
|
|
3448
|
+
startedAt: 0,
|
|
3449
|
+
endedAt: 0,
|
|
3450
|
+
...(result ? { ok: result.ok } : {}),
|
|
3451
|
+
};
|
|
3452
|
+
});
|
|
3453
|
+
restored.push({ role: 'steps', steps });
|
|
3370
3454
|
}
|
|
3371
3455
|
setRows(restored);
|
|
3372
3456
|
setStats({
|
|
@@ -3377,8 +3461,33 @@ export function App({ config: initialConfig, clearFrame, synchronizedFrames = fa
|
|
|
3377
3461
|
` ${meta.turns} turns, ${meta.totalTokens.toLocaleString()} tokens\n` +
|
|
3378
3462
|
` originally: ${meta.provider}/${meta.model}\n` +
|
|
3379
3463
|
` posture: ${meta.mode ?? 'auto'} mode · ${meta.effort ?? 'medium'} effort · approval ${typeof meta.requireApproval === 'boolean' ? (meta.requireApproval ? 'on' : 'off') : 'current'}\n` +
|
|
3380
|
-
` task: ${meta.task.slice(0, 90)}
|
|
3381
|
-
|
|
3464
|
+
` task: ${meta.task.slice(0, 90)}`);
|
|
3465
|
+
/*
|
|
3466
|
+
* Pick up where it stopped, when there is somewhere to pick up from.
|
|
3467
|
+
*
|
|
3468
|
+
* Resuming used to restore everything and then wait, so the first thing anybody typed was
|
|
3469
|
+
* some version of "carry on" — retyping an instruction the session already held. Reported
|
|
3470
|
+
* against Claude Code, which resumes and continues.
|
|
3471
|
+
*
|
|
3472
|
+
* Only when the plan says there is work left. A session whose plan is finished has nothing
|
|
3473
|
+
* to continue and resuming it is almost always somebody looking at what happened, so
|
|
3474
|
+
* spending tokens on their behalf would be presumptuous. With unfinished steps it is the
|
|
3475
|
+
* opposite: the session exists precisely because that work is not done.
|
|
3476
|
+
*
|
|
3477
|
+
* `resumeContinues: false` turns it off, and it never fires for a session with no plan —
|
|
3478
|
+
* there would be nothing to say about what remained.
|
|
3479
|
+
*/
|
|
3480
|
+
const unfinished = restoredPlan?.steps.filter(st => st.state !== 'done') ?? [];
|
|
3481
|
+
if (cfg.resumeContinues !== false && unfinished.length > 0) {
|
|
3482
|
+
const next = unfinished[0].text;
|
|
3483
|
+
addSystem(`Carrying on — ${unfinished.length} step${unfinished.length === 1 ? '' : 's'} `
|
|
3484
|
+
+ `left, starting with "${next.slice(0, 70)}". Esc stops it.`);
|
|
3485
|
+
setTimeout(() => submitDraft('Continue the plan above from where it stopped. Do not restate it or start again — '
|
|
3486
|
+
+ 'pick up the first step that is not done and carry on.'), 0);
|
|
3487
|
+
}
|
|
3488
|
+
else {
|
|
3489
|
+
addSystem('The agent has the full history; carry on where it left off.');
|
|
3490
|
+
}
|
|
3382
3491
|
}
|
|
3383
3492
|
catch (e) {
|
|
3384
3493
|
addSystem(`Could not resume ${id}: ${e instanceof Error ? e.message : String(e)}`);
|
|
@@ -6949,7 +7058,22 @@ export function App({ config: initialConfig, clearFrame, synchronizedFrames = fa
|
|
|
6949
7058
|
}
|
|
6950
7059
|
}
|
|
6951
7060
|
if (usagePane === 'context') {
|
|
6952
|
-
|
|
7061
|
+
/*
|
|
7062
|
+
* Where the number came from, said out loud.
|
|
7063
|
+
*
|
|
7064
|
+
* The window drives compaction, and compaction is the difference between a session that
|
|
7065
|
+
* remembers what it found and one that keeps going back to re-read it. When the figure
|
|
7066
|
+
* is a guess from the model's name — which it is for most hosted models, because almost
|
|
7067
|
+
* none of them publish a context length in `/models` — the person needs to know that,
|
|
7068
|
+
* because they are the only one who can correct it.
|
|
7069
|
+
*/
|
|
7070
|
+
const published = modelCatalog.find(m => m.id === cfg.model)?.contextLength;
|
|
7071
|
+
const guess = contextLimitWithSource(cfg.model, cfg.contextTokens, servesOwnWeights(cfg.provider));
|
|
7072
|
+
const window = published ?? guess.limit;
|
|
7073
|
+
const source = published !== undefined ? 'published by the provider'
|
|
7074
|
+
: guess.source === 'configured' ? 'set by you'
|
|
7075
|
+
: guess.source === 'known' ? 'known for this model'
|
|
7076
|
+
: 'assumed — this model publishes no window';
|
|
6953
7077
|
lines.push('**Context window**', '');
|
|
6954
7078
|
if (window) {
|
|
6955
7079
|
// This is cumulative across the session, not a made-up claim about the next request.
|
|
@@ -6957,6 +7081,11 @@ export function App({ config: initialConfig, clearFrame, synchronizedFrames = fa
|
|
|
6957
7081
|
// history, but only the provider can count the exact request after compaction.
|
|
6958
7082
|
const used = stats.promptTokens;
|
|
6959
7083
|
lines.push(...meter(Math.min(1, used / window), `${humanTokens(used)} session prompt total · ${humanTokens(window)} window · ${cfg.model}`));
|
|
7084
|
+
lines.push(`Window: ${window.toLocaleString()} tokens — ${source}.`);
|
|
7085
|
+
if (guess.source === 'assumed' && published === undefined) {
|
|
7086
|
+
lines.push('If the real one is larger, say so: `/config contextTokens <number>`. '
|
|
7087
|
+
+ 'Compacting earlier than necessary makes a session forget what it just read.');
|
|
7088
|
+
}
|
|
6960
7089
|
if (used / window > 0.7) {
|
|
6961
7090
|
lines.push('Close to the window. `/clear` starts fresh,');
|
|
6962
7091
|
lines.push('`/rewind` drops just the last exchange.');
|
|
@@ -7253,7 +7382,34 @@ export async function runInkChatMode(config) {
|
|
|
7253
7382
|
* frame is a visible blink of the whole screen. 150ms at worst, once, on startup.
|
|
7254
7383
|
*/
|
|
7255
7384
|
const synchronizedFrames = await probeSynchronizedOutput().catch(() => false);
|
|
7256
|
-
|
|
7385
|
+
/*
|
|
7386
|
+
* A clean screen to start on.
|
|
7387
|
+
*
|
|
7388
|
+
* KONECK opened wherever the shell had got to, so a session begun after an `npm install` sat
|
|
7389
|
+
* under a wall of its output with the banner halfway down the screen — it read as another
|
|
7390
|
+
* command that had printed something, rather than as a program that had taken over. Reported
|
|
7391
|
+
* exactly that way: "it fails to take over the terminal and not appear as if it's a command".
|
|
7392
|
+
*
|
|
7393
|
+
* `ESC[2J ESC[H` and nothing more. Deliberately not `ESC[3J`, which empties the scrollback too:
|
|
7394
|
+
* whatever was on screen a moment ago is still there to scroll back to, which is the difference
|
|
7395
|
+
* between clearing the screen and throwing away somebody's last command. Same sequence `clear`
|
|
7396
|
+
* uses, for the same reason.
|
|
7397
|
+
*
|
|
7398
|
+
* Written before Ink starts, so its idea of what it has drawn begins at zero and matches the
|
|
7399
|
+
* screen. Doing this after the first frame is what once left a dead copy of the interface
|
|
7400
|
+
* pinned below a working one.
|
|
7401
|
+
*
|
|
7402
|
+
* Not on the alternate screen, which arrives blank by definition, and not when the output is
|
|
7403
|
+
* not a terminal at all.
|
|
7404
|
+
*/
|
|
7405
|
+
if (config.clearOnStart !== false && process.stdout.isTTY
|
|
7406
|
+
&& initialAltScreen(config, synchronizedFrames) === false) {
|
|
7407
|
+
process.stdout.write('\u001b[2J\u001b[H');
|
|
7408
|
+
}
|
|
7409
|
+
const app = render(_jsx(App, { config: config, synchronizedFrames: synchronizedFrames, clearFrame: () => inkClear?.() }),
|
|
7410
|
+
// Sized first, then bracketed: Ink must never be handed a stream that
|
|
7411
|
+
// says it is zero rows tall. See withUsableSize.
|
|
7412
|
+
{ stdout: withSynchronizedFrames(withUsableSize(process.stdout)),
|
|
7257
7413
|
exitOnCtrlC: false, patchConsole: false });
|
|
7258
7414
|
inkClear = app.clear;
|
|
7259
7415
|
// The engine is imported on demand so the banner and the prompt are not waiting on the OpenAI
|