koneck 2.128.26 → 2.128.28

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
@@ -14,7 +14,7 @@ import { normaliseEndpoint, withEndpoint, savedEndpoints, shortSource, resolveMo
14
14
  import { loadProjectConfig, loadGlobalConfig, saveGlobalConfig, withoutStoredApiKey } from './config.js';
15
15
  import { rememberKey, forgetAllKeys } from './credentials.js';
16
16
  import { ImageDraft, pastedImagePath } from './image-draft.js';
17
- import { readClipboard, readClipboardImage, startSelectionCopy, cleanPastedSecret } from './clipboard.js';
17
+ import { readClipboard, readClipboardImage, startSelectionCopy, cleanPastedSecret, copyViaTerminal } from './clipboard.js';
18
18
  import { paceFrom } from './pace.js';
19
19
  import { policyFrom, FAILOVER_OFF } from './failover.js';
20
20
  import { listCheckpoints, revertCheckpoint, previewCheckpoint, snapshotTree, filesTouchedBy, createWorkspaceCheckpoint, checkpointScope } from './checkpoint.js';
@@ -510,17 +510,19 @@ export const HEARTBEAT_MS = 1_000;
510
510
  * scrollback that trade is a bad one — the terminal's own buffer already scrolls with the wheel,
511
511
  * so turning it on loses selection and gains nothing. Hence opt-in.
512
512
  *
513
- * On the alternate screen there is nothing on the other side of the trade. The terminal has no
514
- * scrollback there to scroll, and a selection does not survive the next repaint both of which
515
- * are already the stated reasons for avoiding the alternate screen where it is not needed. So the
516
- * wheel does nothing at all unless KONECK asks for it, and "koneck does not scroll" is the whole
517
- * of the experience: the transcript runs off the top and the obvious gesture is inert.
513
+ * It was briefly defaulted on for the alternate screen, on the reasoning that a screen with no
514
+ * scrollback of its own has nothing to lose. That was answered directly: "select must happen on
515
+ * mouse like selecting in a document" and "mouse move should be by default, not to be turned
516
+ * on/off". Shift+drag is a workaround for a cost that should not be imposed, and the real fix was
517
+ * not to take the mouse but to stop taking the screen see `defaultAltScreen`. In ordinary
518
+ * scrollback the terminal scrolls with the wheel and selects with a drag, both without being
519
+ * asked, which is the behaviour being described.
518
520
  *
519
- * So the default follows the screen. An explicit `mouse` in the config still wins, in both
520
- * directions, and `/mouse off` is still there for anyone who wants the trade back.
521
+ * So this stays off unless somebody asks for it. `/mouse on` is there for anyone who wants the
522
+ * wheel to drive the transcript inside the alternate screen and will take Shift+drag for it.
521
523
  */
522
- export function defaultMouseTracking(saved, altScreen) {
523
- return saved ?? altScreen;
524
+ export function defaultMouseTracking(saved, _altScreen) {
525
+ return saved ?? false;
524
526
  }
525
527
  /**
526
528
  * Where a tool's file changes are drawn, if anywhere.
@@ -1353,13 +1355,28 @@ compositesRedraws = false) {
1353
1355
  * a frame when nobody has expressed a view.
1354
1356
  */
1355
1357
  export function initialAltScreen(cfg, compositesRedraws, stdout = process.stdout, env = process.env) {
1356
- if (cfg.altScreen === false)
1357
- return false;
1358
- // A panel explicitly enabled in saved settings needs a frame from the first render. Previously
1359
- // it was marked on but silently had nowhere to draw until `/diff off` then `/diff on` happened.
1360
- if (cfg.diffPanel === true || cfg.altScreen === true)
1358
+ /*
1359
+ * Opt-in, on every terminal, however good it is.
1360
+ *
1361
+ * The alternate screen was the default wherever a redraw composites, because it pins the prompt
1362
+ * to the last row. Everything else about it is a loss, and the losses are the ones people
1363
+ * actually notice: the terminal's own scrollback is gone, so the wheel does nothing; a
1364
+ * selection does not survive the next repaint, so text cannot be copied out; and the whole
1365
+ * transcript is re-rendered on every frame instead of being written once into scrollback.
1366
+ *
1367
+ * Each of those was reported separately — "koneck does not scroll", "i can't select anything or
1368
+ * copy anything", and the repainting. They are one cause. Asked for plainly in the end: "select
1369
+ * must happen on mouse like selecting in a document ... mouse move should be by default, not to
1370
+ * be turned on/off". In ordinary scrollback all of that is simply true, because the terminal is
1371
+ * doing it rather than KONECK imitating it.
1372
+ *
1373
+ * So the pinned prompt is the thing you opt into now, with `/altscreen on` or `altScreen: true`.
1374
+ * `diffPanel` no longer implies it either: the panel is a reason to want a frame, not a reason
1375
+ * to take one from somebody who never asked, and with no frame the diffs render inline.
1376
+ */
1377
+ if (cfg.altScreen === true)
1361
1378
  return true;
1362
- return defaultAltScreen(stdout, env, compositesRedraws);
1379
+ return false;
1363
1380
  }
1364
1381
  /**
1365
1382
  * One diff line, syntax-coloured, on its tinted band.
@@ -1470,6 +1487,39 @@ export function DiffPanel({ files, width, height, activity, activityRows = 0 })
1470
1487
  * Typechecking passes on that code and no test could reach it, because nothing in the suite mounted
1471
1488
  * an Ink app. Something does now.
1472
1489
  */
1490
+ /**
1491
+ * The selected span of the composer, or null when nothing is selected.
1492
+ *
1493
+ * Held as an anchor plus the caret rather than as a pair, because that is what makes Shift+arrow
1494
+ * behave the way it does everywhere else: the anchor is where selecting began and stays put, the
1495
+ * caret is the end being dragged, and which of the two is on the left depends on which way it
1496
+ * was dragged. Clamped to the text, so a draft replaced underneath a stale anchor cannot produce
1497
+ * a range that is not there.
1498
+ */
1499
+ export function selectionRange(anchor, caret, length) {
1500
+ if (anchor === null)
1501
+ return null;
1502
+ const a = Math.max(0, Math.min(anchor, length));
1503
+ const c = Math.max(0, Math.min(caret, length));
1504
+ if (a === c)
1505
+ return null;
1506
+ return a < c ? [a, c] : [c, a];
1507
+ }
1508
+ /**
1509
+ * The draft after typing over, or deleting, whatever is selected.
1510
+ *
1511
+ * One function for both, because they are the same operation: a selection is replaced by what
1512
+ * was typed, and deleting is replacing it with nothing. With no selection it is an ordinary
1513
+ * insert at the caret, so every editing path can call this without first asking which case it is
1514
+ * in.
1515
+ */
1516
+ export function replaceSelection(text, range, caret, insert) {
1517
+ if (!range) {
1518
+ return { text: text.slice(0, caret) + insert + text.slice(caret), caret: caret + insert.length };
1519
+ }
1520
+ const [from, to] = range;
1521
+ return { text: text.slice(0, from) + insert + text.slice(to), caret: from + insert.length };
1522
+ }
1473
1523
  export function App({ config: initialConfig, clearFrame, synchronizedFrames = false }) {
1474
1524
  const { exit } = useApp();
1475
1525
  const [cfg, setCfg] = useState(initialConfig);
@@ -1933,6 +1983,13 @@ export function App({ config: initialConfig, clearFrame, synchronizedFrames = fa
1933
1983
  const readOnlyReviewRef = useRef(false);
1934
1984
  /** Caret offset into `draft`. Kept beside it so every edit can place the caret deliberately. */
1935
1985
  const [caret, setCaret] = useState(0);
1986
+ /**
1987
+ * Where a keyboard selection began, or null when nothing is selected.
1988
+ *
1989
+ * Shift with an arrow drops the anchor and drags the caret away from it; anything that moves
1990
+ * the caret without Shift lifts it again. See `selectionRange`.
1991
+ */
1992
+ const [selAnchor, setSelAnchor] = useState(null);
1936
1993
  const [busy, setBusy] = useState(false);
1937
1994
  const [agentState, setAgentState] = useState('ready');
1938
1995
  // A shell normally uses the machine name as its tab title. Keep the KONECK mark and workspace
@@ -2332,6 +2389,14 @@ export function App({ config: initialConfig, clearFrame, synchronizedFrames = fa
2332
2389
  }, [effort]);
2333
2390
  const [showAnalytics, setShowAnalytics] = useState(false);
2334
2391
  const [updatePane, setUpdatePane] = useState(null);
2392
+ /**
2393
+ * The version installed in the background, if one was; see auto-update.ts.
2394
+ *
2395
+ * Only ever says "installed", never "updated": the code this session is running was read into
2396
+ * memory when it started, and replacing the files on disk does not change it. The restart is
2397
+ * the part that matters and so it is the part the line ends on.
2398
+ */
2399
+ const [autoUpdated, setAutoUpdated] = useState(null);
2335
2400
  const [updating, setUpdating] = useState('idle');
2336
2401
  const [updateLog, setUpdateLog] = useState('');
2337
2402
  // Settable, because resuming has to adopt the restored session's id. Without that, every save
@@ -2747,6 +2812,35 @@ export function App({ config: initialConfig, clearFrame, synchronizedFrames = fa
2747
2812
  timer = setTimeout(tick, interval);
2748
2813
  return () => clearTimeout(timer);
2749
2814
  }, [busy, animate, ask]);
2815
+ /*
2816
+ * Check for a new version and install it, quietly, in the background.
2817
+ *
2818
+ * Deliberately late and deliberately silent. It is network and a subprocess, neither of which
2819
+ * should compete with the first turn, and there is nothing to report unless it succeeds — a
2820
+ * registry that cannot be reached or an npm that cannot write to its global prefix is not the
2821
+ * user's problem to hear about mid-task. See auto-update.ts for what it refuses to do.
2822
+ */
2823
+ useEffect(() => {
2824
+ if (cfg.autoUpdate === false)
2825
+ return;
2826
+ let live = true;
2827
+ const timer = setTimeout(() => {
2828
+ void (async () => {
2829
+ try {
2830
+ const { autoUpdate } = await import('./auto-update.js');
2831
+ const outcome = await autoUpdate({
2832
+ currentVersion: VERSION,
2833
+ modulePath: fileURLToPath(import.meta.url),
2834
+ enabled: cfg.autoUpdate !== false,
2835
+ });
2836
+ if (live && outcome.state === 'installed')
2837
+ setAutoUpdated(outcome.version);
2838
+ }
2839
+ catch { /* an update that cannot happen is not worth interrupting anyone over */ }
2840
+ })();
2841
+ }, 4_000);
2842
+ return () => { live = false; clearTimeout(timer); };
2843
+ }, [cfg.autoUpdate]);
2750
2844
  // A queued prompt runs the moment the agent is free again. Keyed on `busy` rather than done
2751
2845
  // inside the turn's own exit paths, so every way a turn can end — success, failure, cancel —
2752
2846
  // drains the queue the same way.
@@ -5085,6 +5179,8 @@ export function App({ config: initialConfig, clearFrame, synchronizedFrames = fa
5085
5179
  setCaret(c => c + token.length);
5086
5180
  }
5087
5181
  useInput((input, key) => {
5182
+ /** What is selected in the composer right now, if anything. See `selectionRange`. */
5183
+ const composerSel = selectionRange(selAnchor, caret, draft.length);
5088
5184
  /*
5089
5185
  * When a key was last pressed, so the animation can get out of the way.
5090
5186
  *
@@ -5141,6 +5237,25 @@ export function App({ config: initialConfig, clearFrame, synchronizedFrames = fa
5141
5237
  closePicker();
5142
5238
  return;
5143
5239
  }
5240
+ /*
5241
+ * With something selected this copies it, as it would in any other text box, and only
5242
+ * leaves when there is nothing selected.
5243
+ *
5244
+ * Overloading the key that quits is not done lightly. It is what everyone's hands already
5245
+ * do, the selection is visible on screen so the branch taken is never a surprise, and the
5246
+ * second press — with the selection now lifted — quits as it always did.
5247
+ *
5248
+ * The copy goes through the terminal rather than the operating system, because the
5249
+ * terminal is where the person is: over SSH, a clipboard on this machine is one nobody can
5250
+ * paste from.
5251
+ */
5252
+ if (composerSel) {
5253
+ const picked = draft.slice(composerSel[0], composerSel[1]);
5254
+ copyViaTerminal(picked, process.stdout);
5255
+ setSelAnchor(null);
5256
+ setCopyNotice(`Copied ${picked.length} chars · ctrl+c again to exit`);
5257
+ return;
5258
+ }
5144
5259
  void doSave().catch(() => { }).then(() => exit());
5145
5260
  return;
5146
5261
  }
@@ -5598,12 +5713,38 @@ export function App({ config: initialConfig, clearFrame, synchronizedFrames = fa
5598
5713
  recallDraft(historyDirection);
5599
5714
  return;
5600
5715
  }
5716
+ /*
5717
+ * Shift with an arrow selects, the way it does in every other text box.
5718
+ *
5719
+ * The anchor is dropped where selecting began and left alone; the caret is what moves. Ctrl
5720
+ * with it extends by a word, matching what Ctrl alone already does to the caret.
5721
+ */
5722
+ if ((key.leftArrow || key.rightArrow) && key.shift) {
5723
+ if (selAnchor === null)
5724
+ setSelAnchor(caret);
5725
+ setCaret(key.leftArrow
5726
+ ? (key.ctrl || key.meta ? wordStart(draft, caret) : Math.max(0, caret - 1))
5727
+ : (key.ctrl || key.meta ? wordEnd(draft, caret) : Math.min(draft.length, caret + 1)));
5728
+ return;
5729
+ }
5601
5730
  if (key.leftArrow) {
5731
+ // An arrow without Shift lifts the selection and puts the caret on the edge it moved
5732
+ // towards, rather than jumping from wherever the caret happened to be dragged to.
5733
+ if (composerSel) {
5734
+ setSelAnchor(null);
5735
+ setCaret(composerSel[0]);
5736
+ return;
5737
+ }
5602
5738
  const step = 1 + takeBurst('left');
5603
5739
  setCaret(c => (key.ctrl || key.meta ? wordStart(draft, c) : Math.max(0, c - step)));
5604
5740
  return;
5605
5741
  }
5606
5742
  if (key.rightArrow) {
5743
+ if (composerSel) {
5744
+ setSelAnchor(null);
5745
+ setCaret(composerSel[1]);
5746
+ return;
5747
+ }
5607
5748
  const step = 1 + takeBurst('right');
5608
5749
  setCaret(c => (key.ctrl || key.meta ? wordEnd(draft, c) : Math.min(draft.length, c + step)));
5609
5750
  return;
@@ -5611,10 +5752,12 @@ export function App({ config: initialConfig, clearFrame, synchronizedFrames = fa
5611
5752
  // ctrl+a / ctrl+e are line start and end, as in bash. Home/End arrive as these on most
5612
5753
  // terminals, so both spellings land here.
5613
5754
  if (key.ctrl && input === 'a') {
5755
+ setSelAnchor(null);
5614
5756
  setCaret(0);
5615
5757
  return;
5616
5758
  }
5617
5759
  if (key.ctrl && input === 'e') {
5760
+ setSelAnchor(null);
5618
5761
  setCaret(draft.length);
5619
5762
  return;
5620
5763
  }
@@ -5635,6 +5778,13 @@ export function App({ config: initialConfig, clearFrame, synchronizedFrames = fa
5635
5778
  return;
5636
5779
  }
5637
5780
  if (key.backspace || key.delete) {
5781
+ if (composerSel) {
5782
+ const next = replaceSelection(draft, composerSel, caret, '');
5783
+ setDraft(next.text);
5784
+ setCaret(next.caret);
5785
+ setSelAnchor(null);
5786
+ return;
5787
+ }
5638
5788
  if (delKindRef.current === 'forward') {
5639
5789
  setDraft(d => d.slice(0, caret) + d.slice(caret + 1)); // caret stays put
5640
5790
  return;
@@ -5676,8 +5826,12 @@ export function App({ config: initialConfig, clearFrame, synchronizedFrames = fa
5676
5826
  filePaste(text);
5677
5827
  return;
5678
5828
  }
5679
- const next = draft.slice(0, caret) + text + draft.slice(caret);
5680
- const nextCaret = caret + text.length;
5829
+ // Typing over a selection replaces it, which is the other half of selecting being useful.
5830
+ const edited = replaceSelection(draft, composerSel, caret, text);
5831
+ if (composerSel)
5832
+ setSelAnchor(null);
5833
+ const next = edited.text;
5834
+ const nextCaret = edited.caret;
5681
5835
  // Once the recalled line is edited it is a new draft, not a history cursor position.
5682
5836
  historyIndexRef.current = null;
5683
5837
  setDraft(next);
@@ -5713,6 +5867,8 @@ export function App({ config: initialConfig, clearFrame, synchronizedFrames = fa
5713
5867
  }
5714
5868
  /** Sends the composer contents: a slash command, or a task for the agent. */
5715
5869
  function submitDraft(raw) {
5870
+ // The draft is about to be replaced, so an anchor into it no longer points at anything.
5871
+ setSelAnchor(null);
5716
5872
  {
5717
5873
  // Tokens are expanded here, at the last moment. The model gets the text as it was copied —
5718
5874
  // blank lines, indentation and all — while the transcript shows the compact form so a
@@ -6552,11 +6708,17 @@ export function App({ config: initialConfig, clearFrame, synchronizedFrames = fa
6552
6708
  return (_jsx(Box, { marginTop: 1, children: _jsx(Panel, { width: panelWidth(lines, barWidth, 46), color: AMBER, title: `Permission — ${modeSpec(mode).label} mode`, children: lines }) }));
6553
6709
  })()] }), sidePanelOpen && (_jsx(Box, { flexDirection: "column", width: diffPanelWidth, flexShrink: 0, children: diffPanelOpen && _jsx(DiffPanel, { files: liveDiffs, width: diffPanelWidth, 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
6554
6710
  ? 'Running ' + liveToolRef.current.name : thinkingRef.current.chars > 0
6555
- ? '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 }) }))] }), _jsxs(Box, { marginTop: 1, borderStyle: "round", borderColor: busy ? DIM : CYAN, paddingX: 1, width: uiWidth - 5, flexShrink: 0, children: [_jsxs(Text, { color: CYAN, bold: true, children: [G.caret, " "] }), _jsx(Text, { color: INK, children: draft.slice(0, caret) }), busy
6556
- ? _jsx(Text, { color: INK, children: draft.slice(caret) })
6557
- : caret < draft.length
6558
- ? _jsxs(_Fragment, { children: [_jsx(Text, { backgroundColor: CYAN, color: "#10222A", children: draft[caret] }), _jsx(Text, { color: INK, children: draft.slice(caret + 1) })] })
6559
- : _jsx(Text, { color: CYAN, children: "\u2588" })] }), copyNotice && _jsx(Text, { color: CYAN, children: copyNotice }), _jsx(Box, { marginTop: 1, paddingX: 1, width: uiWidth - 4, flexShrink: 0, 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 }), mouse && (_jsxs(_Fragment, { children: [_jsx(Text, { color: MUTED, children: " / " }), _jsx(Text, { color: AMBER, children: "WHEEL: on \u2014 shift to select \u00B7 /mouse off" })] })), _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"] })] })), ' '.repeat(uiWidth)] }) })] }));
6711
+ ? '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 }) }))] }), _jsxs(Box, { marginTop: 1, borderStyle: "round", borderColor: busy ? DIM : CYAN, paddingX: 1, width: uiWidth - 5, flexShrink: 0, children: [_jsxs(Text, { color: CYAN, bold: true, children: [G.caret, " "] }), (() => {
6712
+ const sel = selectionRange(selAnchor, caret, draft.length);
6713
+ if (sel) {
6714
+ 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]) })] }));
6715
+ }
6716
+ return (_jsxs(_Fragment, { children: [_jsx(Text, { color: INK, children: draft.slice(0, caret) }), busy
6717
+ ? _jsx(Text, { color: INK, children: draft.slice(caret) })
6718
+ : caret < draft.length
6719
+ ? _jsxs(_Fragment, { children: [_jsx(Text, { backgroundColor: CYAN, color: "#10222A", children: draft[caret] }), _jsx(Text, { color: INK, children: draft.slice(caret + 1) })] })
6720
+ : _jsx(Text, { color: CYAN, children: "\u2588" })] }));
6721
+ })()] }), copyNotice && _jsx(Text, { color: CYAN, children: copyNotice }), _jsx(Box, { marginTop: 1, paddingX: 1, width: uiWidth - 4, flexShrink: 0, 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 }), autoUpdated && (_jsxs(_Fragment, { children: [_jsx(Text, { color: MUTED, children: " / " }), _jsxs(Text, { color: GREEN, children: [G.ok, " Update installed \u00B7 restart to use it"] })] })), mouse && (_jsxs(_Fragment, { children: [_jsx(Text, { color: MUTED, children: " / " }), _jsx(Text, { color: AMBER, children: "WHEEL: on \u2014 shift to select \u00B7 /mouse off" })] })), _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"] })] })), ' '.repeat(uiWidth)] }) })] }));
6560
6722
  }
6561
6723
  export async function runInkChatMode(config) {
6562
6724
  // Nothing is written before the first frame. Filling the screen to push the prompt to the foot