koneck 2.128.41 → 2.128.42

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
@@ -221,7 +221,17 @@ function fitCells(text, width, ellipsis = false) {
221
221
  }
222
222
  /** A picker label uses all of its allocated column, only ellipsising at the actual boundary. */
223
223
  export function pickerLabel(text, width) {
224
- return fitCells(text, width, true);
224
+ /*
225
+ * One column of the field always stays blank, so the label cannot touch what follows it.
226
+ *
227
+ * Padding to exactly `width` left no gap for a label that filled the field, and the row ran
228
+ * together: `muse-glimmer:latestlibrary · local` in an Ollama list, where the name ends and its
229
+ * group begins is anybody's guess. Every shorter label already had a gap, so this only ever
230
+ * showed up on the longest one — which is also the one the field was sized from.
231
+ *
232
+ * A label one cell shorter beats a label welded to its description.
233
+ */
234
+ return fitCells(text, Math.max(1, width - 1), true) + ' ';
225
235
  }
226
236
  /**
227
237
  * Wraps text while keeping the shape it was written in.
@@ -1033,7 +1043,9 @@ export function pickerLabelWidth(labels, descs, width) {
1033
1043
  // Terminal cells, not JS code units: one wide glyph in a model name or session title used to
1034
1044
  // make a supposedly exact row spill past the palette edge.
1035
1045
  const longestDesc = Math.max(0, ...descs.map(displayWidth));
1036
- const longestLabel = Math.max(18, ...labels.map(displayWidth));
1046
+ // Plus the blank column pickerLabel always keeps, so the longest label is sized to fit whole
1047
+ // rather than being the one label the field truncates.
1048
+ const longestLabel = Math.max(18, ...labels.map(l => displayWidth(l) + 1));
1037
1049
  return Math.max(18, Math.min(longestLabel, width - longestDesc - 4));
1038
1050
  }
1039
1051
  /**
@@ -2205,6 +2217,37 @@ export function App({ config: initialConfig, clearFrame, synchronizedFrames = fa
2205
2217
  */
2206
2218
  const [selAnchor, setSelAnchor] = useState(null);
2207
2219
  const [busy, setBusy] = useState(false);
2220
+ /**
2221
+ * Whether a turn is running, readable from an async path.
2222
+ *
2223
+ * A command started while the agent works must not take ownership of this flag, and cannot know
2224
+ * from a captured render whether it already belonged to somebody.
2225
+ */
2226
+ const busyRef = useRef(false);
2227
+ busyRef.current = busy;
2228
+ /**
2229
+ * Run a command, holding `busy` only if nothing else already is.
2230
+ *
2231
+ * Both callers used to do `setBusy(true)` and then clear it unconditionally when the command
2232
+ * finished — including when a turn was already running, whose flag it then cleared. The live
2233
+ * region is gated on `busy` and the status bar on `agentState`, so the working line vanished
2234
+ * while the bar went on saying "processing" and the turn carried on unseen. Reported as the
2235
+ * agent looking stuck: "it is working on the background, that is bad".
2236
+ *
2237
+ * The command is not what makes the interface busy. A turn is.
2238
+ */
2239
+ async function runCommandDetached(cmd, arg) {
2240
+ const alreadyBusy = busyRef.current;
2241
+ if (!alreadyBusy)
2242
+ setBusy(true);
2243
+ try {
2244
+ await handleCommand(cmd, arg);
2245
+ }
2246
+ finally {
2247
+ if (!alreadyBusy)
2248
+ setBusy(false);
2249
+ }
2250
+ }
2208
2251
  const [agentState, setAgentState] = useState('ready');
2209
2252
  // A shell normally uses the machine name as its tab title. Keep the KONECK mark and workspace
2210
2253
  // there instead. The working marker changes in the terminal chrome only: it does not set React
@@ -3648,10 +3691,16 @@ export function App({ config: initialConfig, clearFrame, synchronizedFrames = fa
3648
3691
  }
3649
3692
  }
3650
3693
  /** The provider's own model listing, kept so the models tab and picker agree on the facts. */
3694
+ const catalogEndpoint = useRef('');
3651
3695
  async function loadModelCatalog() {
3652
3696
  const { listModels } = await import('./model-catalog.js');
3653
3697
  const catalog = await listModels(cfg);
3654
3698
  setModelCatalog(catalog.entries);
3699
+ // The host that answered, kept for the picker's header. endpointFor() resolves from the saved
3700
+ // layers and does not see the session's live baseURL, so it reported ollama's default
3701
+ // localhost:11434 for a list fetched from somewhere else entirely — the one label whose whole
3702
+ // job is to show that the request went to the wrong place.
3703
+ catalogEndpoint.current = catalog.endpoint;
3655
3704
  catalogNote.current = catalog.listed ? null : (catalog.note ?? null);
3656
3705
  return catalog.entries; // empty says so rather than showing zeroes
3657
3706
  }
@@ -3937,8 +3986,7 @@ export function App({ config: initialConfig, clearFrame, synchronizedFrames = fa
3937
3986
  }
3938
3987
  setDraft('');
3939
3988
  setCaret(0);
3940
- setBusy(true);
3941
- await handleCommand(item.value, '').finally(() => setBusy(false));
3989
+ await runCommandDetached(item.value, '');
3942
3990
  return;
3943
3991
  }
3944
3992
  if (kind === 'resume') {
@@ -4829,9 +4877,15 @@ export function App({ config: initialConfig, clearFrame, synchronizedFrames = fa
4829
4877
  ? 'File changes hidden. Edits are still listed with what they touched, just not shown.'
4830
4878
  : canPin
4831
4879
  ? 'Live diff panel open and pinned beside the conversation, until `/diff off`.'
4880
+ // Names where the setting came from. "off for this workspace" was true and unusable:
4881
+ // reported as `/diff` looking right on one machine and printing this on another, with
4882
+ // no way to tell that the difference was a saved `altScreen: false` on one of them
4883
+ // rather than the platform.
4832
4884
  : 'File changes shown inline in the conversation, until `/diff off`.\n\n'
4833
- + 'The side panel needs the alternate screen, which is off for this workspace. '
4834
- + '`/altscreen on` moves them into a pinned panel instead.');
4885
+ + 'The pinned side panel needs the alternate screen, and `altScreen: false` is set '
4886
+ + 'in this workspace\'s config — so it is off by your choice, not by platform. '
4887
+ + '`/altscreen on` turns it back on and moves the changes into a pinned panel; '
4888
+ + '`/config altScreen` shows the current value.');
4835
4889
  return;
4836
4890
  }
4837
4891
  case '/compact': {
@@ -6431,8 +6485,7 @@ export function App({ config: initialConfig, clearFrame, synchronizedFrames = fa
6431
6485
  const spaceIdx = task.indexOf(' ');
6432
6486
  const cmd = spaceIdx === -1 ? task : task.slice(0, spaceIdx);
6433
6487
  const arg = spaceIdx === -1 ? '' : task.slice(spaceIdx + 1).trim();
6434
- setBusy(true);
6435
- void handleCommand(cmd, arg).finally(() => setBusy(false));
6488
+ void runCommandDetached(cmd, arg);
6436
6489
  return;
6437
6490
  }
6438
6491
  if (!containsSecret(task))
@@ -7241,61 +7294,6 @@ export function App({ config: initialConfig, clearFrame, synchronizedFrames = fa
7241
7294
  lines.push('**Sessions** /sessions · /send <repo> <task> · /ask <repo> <q> · /resume');
7242
7295
  lines.push('**Changelog** github.com/Gubevu/konech/commits/main');
7243
7296
  return (_jsx(Box, { marginTop: 1, justifyContent: "flex-end", children: _jsx(Panel, { width: Math.min(barWidth, 62), color: AMBER, title: "KONECK \u00B7 updates & guide", children: lines }) }));
7244
- })(), picker && (() => {
7245
- const list = visiblePickerItems;
7246
- // In the alternate screen, the picker shares a fixed frame with a bordered composer and
7247
- // footer. They consume six rows and the picker itself has six non-choice rows (margin,
7248
- // border, title/search and pager), so reserve thirteen in total. Letting 16 choices into
7249
- // a 24-row frame made Ink reflow the live region and visibly blink on every keypress.
7250
- const pickerRows = pickerRowLimit(picker.kind, list.length, termRows, altScreen ? 13 : undefined);
7251
- // Keep the highlighted row inside the visible window as the selection moves.
7252
- const start = Math.max(0, Math.min(pickIndex - Math.floor(pickerRows / 2), list.length - pickerRows));
7253
- const shown = list.slice(Math.max(0, start), Math.max(0, start) + pickerRows);
7254
- const title = picker.kind === 'command' ? 'Commands'
7255
- /*
7256
- * The provider's name and the address the list actually came from.
7257
- *
7258
- * The name alone was a claim the list could not support: a header reading "Models -
7259
- * b.ai" over `muse-glimmer:latest`, `qwen2.5-coder:14b` and `qwen3-coder:30b` — an
7260
- * Ollama library, listed from an Ollama endpoint, under somebody else's name. Nothing
7261
- * on screen said where the answer had come from, so a misrouted endpoint looked like a
7262
- * provider serving the wrong models rather than a request going to the wrong host.
7263
- *
7264
- * The host is enough: it is the part that differs when this goes wrong, and a full URL
7265
- * would push the count and the hint off a narrow palette.
7266
- */
7267
- : picker.kind === 'model' ? `Models - ${cfg.provider} · ${endpointHost(endpointFor(cfg.provider).url)}`
7268
- : picker.kind === 'effort' ? 'Reasoning effort'
7269
- : picker.kind === 'resume' ? 'Resume a saved session'
7270
- : picker.kind === 'file' ? 'Reference a file or directory'
7271
- : 'Connect provider';
7272
- // The palette is sized to the rows it is showing rather than to the terminal. On a wide
7273
- // screen a list of short model names in a full-width box is mostly empty box.
7274
- // The header is a title on the left and a count on the right, on one line. Estimating the
7275
- // right-hand side at a constant made "Reference a file or directory" wrap onto two rows.
7276
- const countText = pickerCountText(pickIndex, list.length, picker.items.length);
7277
- const headWidth = displayWidth(title) + displayWidth(countText) + 3;
7278
- // Measure the filtered list, not just its current scroll window. A palette whose right
7279
- // edge breathes as the highlight crosses a long description feels broken even when the
7280
- // selection itself is correct.
7281
- const rowWidth = Math.max(headWidth, ...list.map(it => 38 + (it.current ? 10 : 0) + displayWidth(it.desc) + displayWidth(it.group ?? '') + 3));
7282
- const boxWidth = pickerBoxWidth(barWidth, rowWidth + 4);
7283
- const width = boxWidth - 4;
7284
- // Room for the longest label the list actually holds, rather than a constant. A session
7285
- // title is what identifies the row, and 36 cells cut most of them in half.
7286
- const labelWidth = pickerLabelWidth(list.map(it => it.label), list.map(it => it.desc), width);
7287
- let lastGroup;
7288
- 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 })] }), _jsxs(Box, { children: [_jsxs(Text, { color: DIM, children: [G.search, " "] }), pickQuery === ''
7289
- ? _jsx(Text, { color: DIM, children: "type to search" })
7290
- : _jsx(Text, { color: INK, children: pickQuery }), _jsx(Text, { color: CYAN, children: pickQuery === '' ? '' : '▌' })] }), list.length === 0 && _jsxs(Text, { color: MUTED, children: ["Nothing matches \"", pickQuery, "\""] }), shown.map((item, i) => {
7291
- const absolute = Math.max(0, start) + i;
7292
- const selected = absolute === pickIndex;
7293
- const header = item.group && item.group !== lastGroup ? item.group : null;
7294
- lastGroup = item.group;
7295
- const label = pickerLabel(item.label, labelWidth);
7296
- return (_jsx(Box, { flexDirection: "column", children: _jsx(Text, { backgroundColor: selected ? CYAN : undefined, color: selected ? '#10222A' : INK, bold: selected, children: fitCells(`${selected ? G.caret + ' ' : ' '}${label}` +
7297
- `${item.current ? '(current) ' : ''}${header ? `${header} · ` : ''}${item.desc}`, width) }) }, item.value + absolute));
7298
- }), list.length > pickerRows && (_jsxs(Text, { color: DIM, children: ["\u2191\u2193 navigate \u00B7 pgup/pgdn jump \u00B7 enter select \u00B7 showing ", Math.max(0, start) + 1, "\u2013", Math.max(0, start) + shown.length, " of ", list.length] }))] }));
7299
7297
  })(), addProv && (() => {
7300
7298
  const shadowed = BUILT_IN_PROVIDERS[addProv.name];
7301
7299
  const ask = addProv.step === 'name' ? { label: 'name', hint: 'short and lowercase, e.g. openrouter' }
@@ -7368,7 +7366,69 @@ export function App({ config: initialConfig, clearFrame, synchronizedFrames = fa
7368
7366
  return (_jsx(Box, { marginTop: 1, children: _jsx(Panel, { width: panelWidth(lines, barWidth, 46), color: AMBER, title: `Permission — ${modeSpec(mode).label} mode`, children: lines }) }));
7369
7367
  })()] }), sidePanelOpen && (_jsx(Box, { flexDirection: "column", width: diffPanelWidth, flexShrink: 0, children: diffPanelOpen && _jsx(DiffPanel, { files: liveDiffs, width: diffPanelWidth, scroll: panelScroll, height: Math.max(6, frameRows - 6), activityRows: activityPanelOpen ? 4 : 0, activity: activityPanelOpen ? _jsxs(_Fragment, { children: [_jsxs(Text, { color: CYAN, bold: true, children: ["Activity \u00B7 ", fmtElapsed(busyStart.current ? Date.now() - busyStart.current : 0)] }), _jsx(Text, { color: AMBER, wrap: "truncate", children: ask ? 'Waiting for your approval' : liveToolRef.current
7370
7368
  ? 'Running ' + liveToolRef.current.name : thinkingRef.current.chars > 0
7371
- ? 'Waiting for model · reasoning received' : 'Waiting for model' }), _jsx(Text, { color: MUTED, wrap: "truncate", children: liveToolRef.current?.detail || plan?.steps.find(s => s.state !== 'done')?.text || 'esc to interrupt' })] }) : undefined }) }))] }), _jsx(Box, { marginTop: 1, borderStyle: "round", borderColor: busy ? DIM : CYAN, paddingX: 1, width: uiWidth - 5, flexShrink: 0, children: _jsxs(Text, { wrap: "wrap", children: [_jsxs(Text, { color: CYAN, bold: true, children: [G.caret, " "] }), (() => {
7369
+ ? 'Waiting for model · reasoning received' : 'Waiting for model' }), _jsx(Text, { color: MUTED, wrap: "truncate", children: liveToolRef.current?.detail || plan?.steps.find(s => s.state !== 'done')?.text || 'esc to interrupt' })] }) : undefined }) }))] }), picker && (() => {
7370
+ const list = visiblePickerItems;
7371
+ // In the alternate screen, the picker shares a fixed frame with a bordered composer and
7372
+ // footer. They consume six rows and the picker itself has six non-choice rows (margin,
7373
+ // border, title/search and pager), so reserve thirteen in total. Letting 16 choices into
7374
+ // a 24-row frame made Ink reflow the live region and visibly blink on every keypress.
7375
+ const pickerRows = pickerRowLimit(picker.kind, list.length, termRows, altScreen ? 13 : undefined);
7376
+ // Keep the highlighted row inside the visible window as the selection moves.
7377
+ const start = Math.max(0, Math.min(pickIndex - Math.floor(pickerRows / 2), list.length - pickerRows));
7378
+ const shown = list.slice(Math.max(0, start), Math.max(0, start) + pickerRows);
7379
+ const title = picker.kind === 'command' ? 'Commands'
7380
+ /*
7381
+ * The provider's name and the address the list actually came from.
7382
+ *
7383
+ * The name alone was a claim the list could not support: a header reading "Models -
7384
+ * b.ai" over `muse-glimmer:latest`, `qwen2.5-coder:14b` and `qwen3-coder:30b` — an
7385
+ * Ollama library, listed from an Ollama endpoint, under somebody else's name. Nothing
7386
+ * on screen said where the answer had come from, so a misrouted endpoint looked like a
7387
+ * provider serving the wrong models rather than a request going to the wrong host.
7388
+ *
7389
+ * The host is enough: it is the part that differs when this goes wrong, and a full URL
7390
+ * would push the count and the hint off a narrow palette.
7391
+ */
7392
+ : picker.kind === 'model' ? `Models - ${cfg.provider} · ${endpointHost(catalogEndpoint.current || endpointFor(cfg.provider).url)}`
7393
+ : picker.kind === 'effort' ? 'Reasoning effort'
7394
+ : picker.kind === 'resume' ? 'Resume a saved session'
7395
+ : picker.kind === 'file' ? 'Reference a file or directory'
7396
+ : 'Connect provider';
7397
+ // The palette is sized to the rows it is showing rather than to the terminal. On a wide
7398
+ // screen a list of short model names in a full-width box is mostly empty box.
7399
+ // The header is a title on the left and a count on the right, on one line. Estimating the
7400
+ // right-hand side at a constant made "Reference a file or directory" wrap onto two rows.
7401
+ const countText = pickerCountText(pickIndex, list.length, picker.items.length);
7402
+ const headWidth = displayWidth(title) + displayWidth(countText) + 3;
7403
+ // Measure the filtered list, not just its current scroll window. A palette whose right
7404
+ // edge breathes as the highlight crosses a long description feels broken even when the
7405
+ // selection itself is correct.
7406
+ const rowWidth = Math.max(headWidth, ...list.map(it => 38 + (it.current ? 10 : 0) + displayWidth(it.desc) + displayWidth(it.group ?? '') + 3));
7407
+ /*
7408
+ * The frame's width, not the conversation column's. `barWidth` is narrowed to make room
7409
+ * for the diff panel, and the palette no longer lives in that column — measuring it
7410
+ * against barWidth is what squeezed the box to 37 cells beside an open panel and turned
7411
+ * `muse-glimmer:latest` into `muse-glimmer:la...`. The composer below uses the same
7412
+ * width for the same reason.
7413
+ */
7414
+ const boxWidth = pickerBoxWidth(uiWidth - 5, rowWidth + 4);
7415
+ const width = boxWidth - 4;
7416
+ // Room for the longest label the list actually holds, rather than a constant. A session
7417
+ // title is what identifies the row, and 36 cells cut most of them in half.
7418
+ const labelWidth = pickerLabelWidth(list.map(it => it.label), list.map(it => it.desc), width);
7419
+ let lastGroup;
7420
+ return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: CYAN, paddingX: 1, marginTop: 1, width: boxWidth, flexShrink: 0, children: [_jsxs(Box, { justifyContent: "space-between", children: [_jsx(Text, { color: CYAN, bold: true, children: title }), _jsx(Text, { color: DIM, children: countText })] }), _jsxs(Box, { children: [_jsxs(Text, { color: DIM, children: [G.search, " "] }), pickQuery === ''
7421
+ ? _jsx(Text, { color: DIM, children: "type to search" })
7422
+ : _jsx(Text, { color: INK, children: pickQuery }), _jsx(Text, { color: CYAN, children: pickQuery === '' ? '' : '▌' })] }), list.length === 0 && _jsxs(Text, { color: MUTED, children: ["Nothing matches \"", pickQuery, "\""] }), shown.map((item, i) => {
7423
+ const absolute = Math.max(0, start) + i;
7424
+ const selected = absolute === pickIndex;
7425
+ const header = item.group && item.group !== lastGroup ? item.group : null;
7426
+ lastGroup = item.group;
7427
+ const label = pickerLabel(item.label, labelWidth);
7428
+ return (_jsx(Box, { flexDirection: "column", children: _jsx(Text, { backgroundColor: selected ? CYAN : undefined, color: selected ? '#10222A' : INK, bold: selected, children: fitCells(`${selected ? G.caret + ' ' : ' '}${label}` +
7429
+ `${item.current ? '(current) ' : ''}${header ? `${header} · ` : ''}${item.desc}`, width) }) }, item.value + absolute));
7430
+ }), list.length > pickerRows && (_jsxs(Text, { color: DIM, children: ["\u2191\u2193 navigate \u00B7 pgup/pgdn jump \u00B7 enter select \u00B7 showing ", Math.max(0, start) + 1, "\u2013", Math.max(0, start) + shown.length, " of ", list.length] }))] }));
7431
+ })(), _jsx(Box, { marginTop: 1, borderStyle: "round", borderColor: busy ? DIM : CYAN, paddingX: 1, width: uiWidth - 5, flexShrink: 0, children: _jsxs(Text, { wrap: "wrap", children: [_jsxs(Text, { color: CYAN, bold: true, children: [G.caret, " "] }), (() => {
7372
7432
  const sel = selectionRange(selAnchor, caret, draft.length);
7373
7433
  if (sel) {
7374
7434
  return (_jsxs(_Fragment, { children: [_jsx(Text, { color: INK, children: draft.slice(0, sel[0]) }), _jsx(Text, { backgroundColor: CYAN, color: "#10222A", children: draft.slice(sel[0], sel[1]) }), _jsx(Text, { color: INK, children: draft.slice(sel[1]) })] }));