koneck 2.25.61 → 2.25.63

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/ink-chat.js CHANGED
@@ -2,13 +2,13 @@ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-run
2
2
  import React, { useState, useEffect, useRef } from 'react';
3
3
  import { Box, Static, Text, render, useApp, useInput, useStdin } from 'ink';
4
4
  import { execa } from 'execa';
5
- import { createAgentSession, buildClient, resolveAgentConcurrency, toolsUnsupportedNotice } from './engine.js';
5
+ import { createAgentSession, buildClient, resolveAgentConcurrency, toolsUnsupportedNotice, isLocalRuntime } from './engine.js';
6
6
  import { estimateCost, formatCost } from './pricing.js';
7
7
  import { generateSessionId, saveSession, listSessions, loadSession, relativeAge, renameSession, forkSession, archiveSession, findSession, } from './session.js';
8
8
  import { loadMemory } from './memory.js';
9
9
  import { loadKoneckConfig, saveKoneckConfig, setConfigKey, configKeyDescriptions, CONFIG_FILE, CONFIG_SCHEMA, stepValue, displayValue } from './config-store.js';
10
10
  import { PROVIDERS, resolveProvider, getApiKey, addUserProvider, userProvidersPath } from './providers.js';
11
- import { resolveEndpoint, normaliseEndpoint, withEndpoint, savedEndpoints } from './endpoints.js';
11
+ import { resolveEndpoint, normaliseEndpoint, withEndpoint, savedEndpoints, shortSource } from './endpoints.js';
12
12
  import { loadProjectConfig, loadGlobalConfig } from './config.js';
13
13
  import { listCheckpoints, revertCheckpoint, snapshotTree, filesTouchedBy } from './checkpoint.js';
14
14
  import { fileDiff, diffSummary } from './diff-view.js';
@@ -501,6 +501,7 @@ const COMMANDS = [
501
501
  { cmd: '/models', desc: 'Browse and switch models for the connected provider' },
502
502
  { cmd: '/connect', desc: 'Connect a provider, or declare a new one with a URL' },
503
503
  { cmd: '/endpoint', desc: "Show or change this provider's URL, e.g. a remote Ollama" },
504
+ { cmd: '/redraw', desc: 'Repaint the terminal — same as ctrl+L' },
504
505
  { cmd: '/key', desc: 'Enter an API key for the current provider' },
505
506
  { cmd: '/model', desc: 'Show or set the model by name' },
506
507
  { cmd: '/provider', desc: 'Show or switch provider' },
@@ -733,6 +734,7 @@ Slash commands:
733
734
  /connect [provider] Connect a provider (picker when no name given)
734
735
  /connect <name> <url> Declare a provider KONECK does not ship with, e.g.
735
736
  /connect openrouter https://openrouter.ai/api/v1
737
+ /redraw Repaint the terminal (ctrl+L) if a stale frame is left on screen
736
738
  /endpoint Show the endpoint in force and where it came from
737
739
  /endpoint <url> Point this provider somewhere else, e.g.
738
740
  /endpoint http://192.168.1.50:11434/v1
@@ -1012,6 +1014,18 @@ function App({ config: initialConfig }) {
1012
1014
  toolsWarnedRef.current = model;
1013
1015
  addSystem(toolsUnsupportedNotice(model));
1014
1016
  },
1017
+ onLooping: (copies) => {
1018
+ // The loop itself is the symptom. On a local model the cause is almost always the window:
1019
+ // Ollama serves 4096 tokens unless told otherwise, whatever the model was trained for, and
1020
+ // a conversation that no longer fits is exactly what makes a model start repeating.
1021
+ const local = isLocalRuntime(cfg.provider);
1022
+ addSystem(`${cfg.model} repeated the same text ${copies} times, so the reply was cut short.` +
1023
+ (local
1024
+ ? `\n\nThis is usually the context window rather than the model. Ollama serves 4096 ` +
1025
+ `tokens by default however large the model is — set ` +
1026
+ `\`OLLAMA_CONTEXT_LENGTH=32768\` and restart it, then \`/context\` to confirm.`
1027
+ : ''));
1028
+ },
1015
1029
  onThinking: (text, totalChars) => {
1016
1030
  thinkingRef.current = { chars: totalChars, at: Date.now() };
1017
1031
  // The text was discarded before this, so the model's reasoning was never visible — only a
@@ -1075,6 +1089,8 @@ function App({ config: initialConfig }) {
1075
1089
  * reading something further up is not yanked away by the next tool call.
1076
1090
  */
1077
1091
  const [scrollBack, setScrollBack] = useState(0);
1092
+ /** Bumped to make Ink write a frame whose content has not changed. See redrawScreen. */
1093
+ const [repaint, setRepaint] = useState(0);
1078
1094
  const [ask, setAsk] = useState(null);
1079
1095
  useEffect(() => {
1080
1096
  if (!process.stdout.isTTY)
@@ -1415,6 +1431,30 @@ function App({ config: initialConfig }) {
1415
1431
  if (newCfg)
1416
1432
  setCfg(newCfg);
1417
1433
  }
1434
+ /**
1435
+ * Repaint the terminal from scratch.
1436
+ *
1437
+ * Reported as KONECK looking "split into two parts, two message input box": a previous frame
1438
+ * left on screen above the live one, with the transcript written in between. Ink erases the
1439
+ * lines it believes it wrote, and any line the terminal wrapped occupies two rows while
1440
+ * counting as one — so the erase falls short and the old frame is stranded. It cannot be
1441
+ * unstranded from inside the frame, because Ink is not aware it is there.
1442
+ *
1443
+ * Ctrl+L is what everybody presses when a terminal looks wrong, and KONECK had no binding for
1444
+ * it at all. It wipes the screen and its scrollback; the conversation is untouched, since it
1445
+ * lives in the session rather than in the terminal's memory of it.
1446
+ */
1447
+ function redrawScreen() {
1448
+ if (!process.stdout.isTTY)
1449
+ return;
1450
+ // Clear the screen, clear the scrollback with it — a stranded frame that has scrolled out of
1451
+ // view is still in the buffer, and leaving it there means it reappears on the next scroll —
1452
+ // then home the cursor so Ink's next frame starts from a known position.
1453
+ process.stdout.write('\u001b[2J\u001b[3J\u001b[H');
1454
+ // Ink writes a frame only when its content differs from the last one, and the content has
1455
+ // not changed — the screen it was drawn on has. Bumping this forces the write.
1456
+ setRepaint(n => n + 1);
1457
+ }
1418
1458
  async function doSave() {
1419
1459
  const s = await activeSession.p.catch(() => null);
1420
1460
  const firstUser = s?.messages.find((m) => m.role === 'user');
@@ -1977,6 +2017,10 @@ function App({ config: initialConfig }) {
1977
2017
  `Session reset. /endpoint <url> changes the address.`);
1978
2018
  return;
1979
2019
  }
2020
+ case '/redraw': {
2021
+ redrawScreen();
2022
+ return;
2023
+ }
1980
2024
  case '/url':
1981
2025
  case '/baseurl':
1982
2026
  case '/endpoint': {
@@ -2673,6 +2717,12 @@ function App({ config: initialConfig }) {
2673
2717
  void doSave().catch(() => { }).then(() => exit());
2674
2718
  return;
2675
2719
  }
2720
+ // Ctrl+L repaints, as it does everywhere else. The conversation is not touched — only the
2721
+ // terminal's picture of it, which is the thing that was wrong.
2722
+ if (key.ctrl && input === 'l') {
2723
+ redrawScreen();
2724
+ return;
2725
+ }
2676
2726
  // ── Credential entry owns the keyboard, and echoes nothing ───────────────
2677
2727
  // Scrolling the transcript, which on the alternate screen is the application's job: the
2678
2728
  // terminal's own buffer is not there to fall back on.
@@ -2879,6 +2929,10 @@ function App({ config: initialConfig }) {
2879
2929
  }
2880
2930
  return;
2881
2931
  }
2932
+ if (key.ctrl && input === 'u') {
2933
+ setEditing(e => (e ? { ...e, value: '' } : e));
2934
+ return;
2935
+ }
2882
2936
  if (key.backspace || key.delete) {
2883
2937
  setEditing(e => (e ? { ...e, value: e.value.slice(0, -1) } : e));
2884
2938
  return;
@@ -3295,7 +3349,16 @@ function App({ config: initialConfig }) {
3295
3349
  // window empty. A cap is still available through `maxWidth` in config for anyone who
3296
3350
  // prefers a narrow measure on a very wide screen, but nothing is capped by default.
3297
3351
  const uiWidth = computeUiWidth(termCols, configMaxWidth);
3298
- const barWidth = uiWidth - 4; // uiWidth minus the container's paddingX={2} on both sides
3352
+ // uiWidth minus the container's paddingX={2} on both sides, and then one column of slack.
3353
+ //
3354
+ // Without the slack a full-width box draws a line exactly as wide as the terminal. Ink counts
3355
+ // the lines it wrote in order to erase them next frame, and a line that the terminal wrapped
3356
+ // occupies two rows while counting as one — so the erase falls short and the previous frame is
3357
+ // stranded on screen. That is how a screen ends up showing two composers and two status bars.
3358
+ // One character of a different width than Ink assumed is enough to cause it, and East Asian
3359
+ // ambiguous glyphs (·, ↑↓, ⌕, –, all of which the UI uses) are rendered two cells wide by some
3360
+ // terminals and one by others. A spare column costs nothing and removes the whole class.
3361
+ const barWidth = uiWidth - 5;
3299
3362
  // Space a rendered reply actually has: the bar, minus the response box border (2), its
3300
3363
  // paddingX={1} (2), and the "ꓘK " prefix plus its gap (3). Getting this even one column
3301
3364
  // too wide makes Ink reflow table rows and shatter their borders.
@@ -3449,7 +3512,7 @@ function App({ config: initialConfig }) {
3449
3512
  : displayValue(value);
3450
3513
  const shown = beingTyped
3451
3514
  ? `${editing.value}_`
3452
- : isEndpoint ? `${raw} (${endpoint.source})`
3515
+ : isEndpoint ? `${raw} (${shortSource(endpoint.source)})`
3453
3516
  : raw + (here || value === undefined ? '' : ' (inherited)');
3454
3517
  // The marker is what makes the selection legible without colour, which a panel line
3455
3518
  // drawn as one string cannot carry per-segment.
@@ -3458,12 +3521,12 @@ function App({ config: initialConfig }) {
3458
3521
  : f.type.kind === 'boolean' ? ' enter toggles'
3459
3522
  : f.type.kind === 'choice' ? ` enter cycles: ${f.type.choices.join(' / ')}`
3460
3523
  : f.type.kind === 'number' ? ` type a number, enter to save${f.clearable ? ', x clears' : ''}`
3461
- : ` enter to type a value${f.clearable ? ', x clears' : ''}`;
3524
+ : ` enter to edit${f.clearable ? ', x clears' : ''}`;
3462
3525
  lines.push(`${mark} ${f.label.padEnd(labelW)} ${selected ? '**' + shown + '**' : shown}${hint}`);
3463
3526
  }
3464
3527
  lines.push('');
3465
3528
  lines.push(editing
3466
- ? 'Typing. **enter** saves, **esc** abandons.'
3529
+ ? 'Typing. **enter** saves, **ctrl+u** empties the field, **esc** abandons.'
3467
3530
  : 'Up/down choose · enter change · x clear · left/right tabs');
3468
3531
  lines.push(`Saved to ${CONFIG_FILE}`);
3469
3532
  }
@@ -3673,7 +3736,7 @@ function App({ config: initialConfig }) {
3673
3736
  ? _jsx(Text, { color: INK, children: draft.slice(caret) })
3674
3737
  : caret < draft.length
3675
3738
  ? _jsxs(_Fragment, { children: [_jsx(Text, { backgroundColor: CYAN, color: "#10222A", children: draft[caret] }), _jsx(Text, { color: INK, children: draft.slice(caret + 1) })] })
3676
- : _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() }), plan && planSummary(plan) && (_jsxs(_Fragment, { children: [_jsx(Text, { color: MUTED, children: " / PLAN: " }), _jsx(Text, { color: CYAN, children: planSummary(plan) })] })), _jsx(Text, { color: MUTED, children: " / MODE: " }), _jsx(Text, { color: mode === 'auto' ? INK : AMBER, children: modeSpec(mode).label }), _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"] })] }))] }) })] }));
3739
+ : _jsx(Text, { color: CYAN, children: "\u2588" })] }), _jsx(Box, { marginTop: 1, paddingX: 1, children: _jsxs(Text, { wrap: "truncate", backgroundColor: "#24343B", children: [repaint % 2 === 1 ? '\u200b' : '', _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() }), plan && planSummary(plan) && (_jsxs(_Fragment, { children: [_jsx(Text, { color: MUTED, children: " / PLAN: " }), _jsx(Text, { color: CYAN, children: planSummary(plan) })] })), _jsx(Text, { color: MUTED, children: " / MODE: " }), _jsx(Text, { color: mode === 'auto' ? INK : AMBER, children: modeSpec(mode).label }), _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"] })] }))] }) })] }));
3677
3740
  }
3678
3741
  export async function runInkChatMode(config) {
3679
3742
  // Nothing is written before the first frame. Filling the screen to push the prompt to the foot