koneck 2.21.1 → 2.23.0

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
@@ -4,12 +4,13 @@ import { Box, Static, Text, render, useApp, useInput } from 'ink';
4
4
  import { execa } from 'execa';
5
5
  import { createAgentSession, buildClient } from './engine.js';
6
6
  import { estimateCost, formatCost } from './pricing.js';
7
- import { generateSessionId, saveSession, listSessions } from './session.js';
7
+ import { generateSessionId, saveSession, listSessions, loadSession } from './session.js';
8
8
  import { loadMemory } from './memory.js';
9
9
  import { loadKoneckConfig, saveKoneckConfig, setConfigKey, configKeyDescriptions, CONFIG_FILE } from './config-store.js';
10
10
  import { PROVIDERS, resolveProvider, getApiKey, addUserProvider, userProvidersPath } from './providers.js';
11
11
  import { listCheckpoints, revertCheckpoint } from './checkpoint.js';
12
12
  import { Markdown, Panel } from './markdown-ink.js';
13
+ import { startBus, listSessions as listBusSessions, resolveTarget, deliver, } from './session-bus.js';
13
14
  const CYAN = '#54D7E7';
14
15
  const GREEN = '#9BCB8F';
15
16
  const CRIMSON = '#E8878D';
@@ -209,6 +210,9 @@ const COMMANDS = [
209
210
  { cmd: '/provider', desc: 'Show or switch provider' },
210
211
  { cmd: '/status', desc: 'Provider, model, mode, tokens, cost, workspace' },
211
212
  { cmd: '/revert', desc: 'Undo file edits KONECK made' },
213
+ { cmd: '/sessions', desc: 'List running KONECK sessions and their state' },
214
+ { cmd: '/send', desc: 'Hand work to another session: /send <repo> <instruction>' },
215
+ { cmd: '/ask', desc: 'Ask another session and wait: /ask <repo> <question>' },
212
216
  { cmd: '/agents', desc: 'How parallel sub-agents are configured' },
213
217
  { cmd: '/subtask', desc: 'Run a task in an isolated sub-agent' },
214
218
  { cmd: '/clear', desc: 'Clear conversation history' },
@@ -225,7 +229,8 @@ const COMMANDS = [
225
229
  { cmd: '/rewind', desc: 'Drop the last exchange from history' },
226
230
  { cmd: '/copy', desc: 'Copy the last response to the clipboard' },
227
231
  { cmd: '/save', desc: 'Save session to disk' },
228
- { cmd: '/sessions', desc: 'List recent saved sessions' },
232
+ { cmd: '/history', desc: 'List saved sessions on disk' },
233
+ { cmd: '/resume', desc: 'Resume a saved session (picker, or /resume <id>)' },
229
234
  { cmd: '/exit', desc: 'Save and exit' },
230
235
  { cmd: '/quit', desc: 'Save and exit (alias of /exit)' },
231
236
  ];
@@ -352,12 +357,17 @@ function App({ config: initialConfig }) {
352
357
  }))[0];
353
358
  const [rows, setRows] = useState([{ role: 'header' }]);
354
359
  const [draft, setDraft] = useState('');
360
+ /** Caret offset into `draft`. Kept beside it so every edit can place the caret deliberately. */
361
+ const [caret, setCaret] = useState(0);
355
362
  const [busy, setBusy] = useState(false);
356
363
  const [agentState, setAgentState] = useState('ready');
357
364
  const [mode, setMode] = useState('auto');
358
365
  const [effort, setEffort] = useState('medium');
359
366
  const [showAnalytics, setShowAnalytics] = useState(false);
360
367
  const [sessionId] = useState(() => generateSessionId());
368
+ /** Messages that arrived while a turn was running; drained when the agent goes idle. */
369
+ const inboxRef = useRef([]);
370
+ const busRef = useRef(null);
361
371
  const [stats, setStats] = useState({ turns: 0, promptTokens: 0, completionTokens: 0, totalTokens: 0 });
362
372
  const [lastAgentText, setLastAgentText] = useState('');
363
373
  // Masked credential entry. The value is held in component state for the session and passed
@@ -368,6 +378,7 @@ function App({ config: initialConfig }) {
368
378
  const [pickIndex, setPickIndex] = useState(0);
369
379
  const [pickQuery, setPickQuery] = useState('');
370
380
  const visiblePickerItems = React.useMemo(() => (picker ? filterPickerItems(picker.items, pickQuery) : []), [picker, pickQuery]);
381
+ const [inboxTick, setInboxTick] = useState(0);
371
382
  const [spinFrame, setSpinFrame] = useState(0);
372
383
  const [shimmerFrame, setShimmerFrame] = useState(0);
373
384
  const [spinWord, setSpinWord] = useState(0);
@@ -376,6 +387,55 @@ function App({ config: initialConfig }) {
376
387
  const turnRecoveredRef = useRef(false); // a tool failed and was worked around
377
388
  const busyStart = useRef(0);
378
389
  const wordTimer = useRef(0);
390
+ // Register on the session bus so other running KONECK instances can reach this one.
391
+ useEffect(() => {
392
+ const bus = startBus({ id: sessionId, cwd: cfg.cwd, model: cfg.model, provider: cfg.provider }, {
393
+ onSend: (from, payload) => {
394
+ // Never interrupt a turn: queue it and let the idle drain pick it up.
395
+ inboxRef.current.push({ from, payload });
396
+ process.stdout.write('\u0007'); // a bell, so an unattended session is noticed
397
+ setInboxTick(t => t + 1);
398
+ },
399
+ onAsk: async (from, question) => {
400
+ // A question is answered from this session's own context, in its own sub-session, so
401
+ // the asking side gets a real answer without this session losing its place.
402
+ const active = await getSession();
403
+ const before = active.messages.length;
404
+ try {
405
+ await active.send(`[Question from the KONECK session in ${from.repoName} (${from.cwd})]\n${question}\n\n` +
406
+ 'Answer concisely from what you know about this project. Do not modify any file.');
407
+ const last = active.messages
408
+ .filter(m => m.role === 'assistant' && typeof m.content === 'string' && m.content.trim())
409
+ .at(-1);
410
+ return typeof last?.content === 'string' ? last.content.trim() : '(no answer)';
411
+ }
412
+ finally {
413
+ // The exchange is not part of this session's own conversation.
414
+ active.messages.splice(before);
415
+ }
416
+ },
417
+ });
418
+ busRef.current = bus;
419
+ return () => bus?.close();
420
+ }, []);
421
+ // The bus only knows idle from busy if it is told.
422
+ useEffect(() => { busRef.current?.setStatus(busy ? 'busy' : 'idle'); }, [busy]);
423
+ // Drain the inbox the moment the agent is free, so a handoff is never lost or interleaved.
424
+ useEffect(() => {
425
+ if (busy || inboxRef.current.length === 0)
426
+ return;
427
+ const queued = inboxRef.current.splice(0);
428
+ for (const { from, payload } of queued) {
429
+ addRow({ role: 'steps', steps: [{ kind: 'say',
430
+ text: `Handoff from ${from.repoName} (${from.cwd})\n\n${payload.summary}` +
431
+ (payload.changedFiles.length ? `\n\nChanged: ${payload.changedFiles.join(', ')}` : '') }] });
432
+ }
433
+ const combined = queued.map(q => `[Handoff from the KONECK session in ${q.from.repoName} (${q.from.cwd})]\n` +
434
+ `Context: ${q.payload.summary}\n` +
435
+ (q.payload.changedFiles.length ? `Files they changed: ${q.payload.changedFiles.join(', ')}\n` : '') +
436
+ `Your task: ${q.payload.instruction}`).join('\n\n');
437
+ submitDraft(combined);
438
+ }, [busy, inboxTick]);
379
439
  // Drive spinner + elapsed counter while busy
380
440
  useEffect(() => {
381
441
  if (!busy) {
@@ -420,6 +480,75 @@ function App({ config: initialConfig }) {
420
480
  turns: stats.turns, totalTokens: stats.totalTokens,
421
481
  }, s?.messages ?? []);
422
482
  }
483
+ /**
484
+ * Compresses this session's state into something worth sending. One small model call, capped
485
+ * hard, because the point of a handoff is to save the receiver the reading, not to move the
486
+ * transcript across the wire.
487
+ */
488
+ async function buildHandoff(instruction) {
489
+ const changed = [...new Set(rows.flatMap(r => (r.steps ?? [])
490
+ .filter((st) => st.kind === 'tool')
491
+ .filter(st => ['write_file', 'search_replace', 'apply_diff', 'move_file', 'delete_file'].includes(st.name))
492
+ .map(st => st.detail)
493
+ .filter(Boolean)))].slice(0, 12);
494
+ const transcript = rows
495
+ .filter(r => r.role === 'user' || r.role === 'agent')
496
+ .slice(-8)
497
+ .map(r => `${r.role === 'user' ? 'User' : 'Agent'}: ${(r.text ?? '').slice(0, 400)}`)
498
+ .join('\n');
499
+ let summary = transcript ? transcript.slice(0, 600) : 'No work has been done in this session yet.';
500
+ if (transcript.trim() !== '') {
501
+ try {
502
+ const client = buildClient(cfg);
503
+ const res = await client.chat.completions.create({
504
+ model: cfg.model,
505
+ max_tokens: 220,
506
+ temperature: 0,
507
+ messages: [{ role: 'user', content: 'Summarise this coding session in 2-3 sentences for a colleague picking up related ' +
508
+ 'work in another repository. State what was done and anything they must know. No preamble.\n\n' +
509
+ transcript }],
510
+ });
511
+ const text = res.choices[0]?.message?.content;
512
+ if (typeof text === 'string' && text.trim())
513
+ summary = text.trim();
514
+ }
515
+ catch { /* the raw tail is a serviceable fallback */ }
516
+ }
517
+ return { summary, changedFiles: changed, instruction };
518
+ }
519
+ /**
520
+ * Replaces the live conversation with a saved one. The agent session is rebuilt from the
521
+ * stored messages, so the model genuinely has the old context rather than a summary of it,
522
+ * and the transcript is repainted so the screen matches what the agent now believes.
523
+ */
524
+ async function resumeSession(id) {
525
+ try {
526
+ const { meta, messages } = await loadSession(cfg.cwd, id);
527
+ activeSession.p = createAgentSession({ ...cfg, silent: true, ...sessionOpts }, messages);
528
+ const restored = [{ role: 'header' }];
529
+ for (const m of messages) {
530
+ if (m.role === 'user' && typeof m.content === 'string' && !m.content.startsWith('[')) {
531
+ restored.push({ role: 'user', text: m.content });
532
+ }
533
+ else if (m.role === 'assistant' && typeof m.content === 'string' && m.content.trim()) {
534
+ restored.push({ role: 'agent', text: m.content });
535
+ }
536
+ }
537
+ setRows(restored);
538
+ setStats({
539
+ turns: meta.turns, promptTokens: 0,
540
+ completionTokens: 0, totalTokens: meta.totalTokens,
541
+ });
542
+ addSystem(`Resumed ${id}\n` +
543
+ ` ${meta.turns} turns, ${meta.totalTokens.toLocaleString()} tokens\n` +
544
+ ` originally: ${meta.provider}/${meta.model}\n` +
545
+ ` task: ${meta.task.slice(0, 90)}\n\n` +
546
+ `The agent has the full history; carry on where it left off.`);
547
+ }
548
+ catch (e) {
549
+ addSystem(`Could not resume ${id}: ${e instanceof Error ? e.message : String(e)}`);
550
+ }
551
+ }
423
552
  /** Live model list from the connected provider's OpenAI-compatible /models endpoint. */
424
553
  async function fetchModels() {
425
554
  const client = buildClient(cfg);
@@ -459,6 +588,11 @@ function App({ config: initialConfig }) {
459
588
  if (kind === 'command') {
460
589
  // Leave it in the composer so arguments can still be typed before submitting.
461
590
  setDraft(item.value + ' ');
591
+ setCaret(item.value.length + 1);
592
+ return;
593
+ }
594
+ if (kind === 'resume') {
595
+ await resumeSession(item.value);
462
596
  return;
463
597
  }
464
598
  if (kind === 'effort') {
@@ -681,7 +815,31 @@ function App({ config: initialConfig }) {
681
815
  addSystem(`Saved: ${file}`);
682
816
  return;
683
817
  }
684
- case '/sessions': {
818
+ case '/resume': {
819
+ const saved = await listSessions(cfg.cwd).catch(() => []);
820
+ if (saved.length === 0) {
821
+ addSystem('No saved sessions in .koneck/sessions/ for this workspace.');
822
+ return;
823
+ }
824
+ // With an id, restore straight away; without one, offer a picker.
825
+ const pick = arg.trim();
826
+ if (pick === '') {
827
+ openPicker('resume', saved.slice(0, 20).map(x => ({
828
+ value: x.id,
829
+ label: x.id,
830
+ desc: `${x.turns} turns ${x.totalTokens.toLocaleString()} tok ${x.task.slice(0, 44)}`,
831
+ })));
832
+ return;
833
+ }
834
+ const chosen = saved.find(x => x.id === pick) ?? saved.find(x => x.id.startsWith(pick));
835
+ if (!chosen) {
836
+ addSystem(`No saved session matching "${pick}". /history lists them.`);
837
+ return;
838
+ }
839
+ await resumeSession(chosen.id);
840
+ return;
841
+ }
842
+ case '/history': {
685
843
  const sessions = await listSessions(cfg.cwd).catch(() => []);
686
844
  if (!sessions.length) {
687
845
  addSystem('No sessions found.');
@@ -797,6 +955,73 @@ function App({ config: initialConfig }) {
797
955
  setKeyPrompt({ provider: cfg.provider, env: def?.apiKeyEnv ?? 'KONECK_API_KEY', value: '' });
798
956
  return;
799
957
  }
958
+ case '/sessions': {
959
+ const live = listBusSessions();
960
+ if (live.length === 0) {
961
+ addSystem('No KONECK sessions are registered.');
962
+ return;
963
+ }
964
+ const rows = live.map(x => {
965
+ const me = x.id === sessionId ? ' (this one)' : '';
966
+ // Kept short so the row cannot wrap and tear the column alignment.
967
+ const full = x.cwd.replace(process.env['HOME'] ?? '', '~');
968
+ const parts = full.split('/').filter(Boolean);
969
+ const where = full.length <= 30 || parts.length <= 2 ? full : '.../' + parts.slice(-2).join('/');
970
+ return `${('#' + x.id).padEnd(22)} ${x.repoName.slice(0, 16).padEnd(17)} ${x.status.toUpperCase().padEnd(5)} ${where}${me}`;
971
+ });
972
+ addSystem(`${'SESSION'.padEnd(22)} ${'REPO'.padEnd(17)} ${'STATE'.padEnd(5)} DIRECTORY\n` +
973
+ rows.join('\n') +
974
+ `\n\nAddress a session by repo or folder name, not its id:\n` +
975
+ ` /send ${live.find(x => x.id !== sessionId)?.repoName ?? '<repo>'} <instruction>\n` +
976
+ ` /ask ${live.find(x => x.id !== sessionId)?.repoName ?? '<repo>'} <question>`);
977
+ return;
978
+ }
979
+ case '/send':
980
+ case '/ask': {
981
+ const twoWay = cmd === '/ask';
982
+ const gap = arg.indexOf(' ');
983
+ if (gap === -1) {
984
+ addSystem(`Usage: ${cmd} <target> <${twoWay ? 'question' : 'instruction'}>\n` +
985
+ `Target is a repo or folder name, or #id. /sessions lists them.`);
986
+ return;
987
+ }
988
+ const target = arg.slice(0, gap);
989
+ const body = arg.slice(gap + 1).trim();
990
+ if (body === '') {
991
+ addSystem(`Nothing to ${twoWay ? 'ask' : 'send'}.`);
992
+ return;
993
+ }
994
+ const found = resolveTarget(target, sessionId);
995
+ if (!found.ok) {
996
+ addSystem(found.error);
997
+ return;
998
+ }
999
+ const bus = busRef.current;
1000
+ if (!bus) {
1001
+ addSystem('This session is not on the bus, so it cannot send.');
1002
+ return;
1003
+ }
1004
+ const from = { id: sessionId, repoName: bus.record.repoName, cwd: cfg.cwd };
1005
+ if (twoWay) {
1006
+ addSystem(`Asking ${found.session.repoName}: "${body}"\nWaiting for its answer…`);
1007
+ const reply = await deliver(found.session, { kind: 'ask', token: found.session.authToken, from, question: body });
1008
+ addSystem(reply.ok
1009
+ ? `${found.session.repoName} replied:\n\n${reply.answer ?? '(empty)'}`
1010
+ : `${found.session.repoName} could not answer: ${reply.error}`);
1011
+ return;
1012
+ }
1013
+ // A handoff carries a summary, never the transcript: the receiving session pays for
1014
+ // every token, and a raw history mostly restates what this one already worked out.
1015
+ const payload = await buildHandoff(body);
1016
+ const reply = await deliver(found.session, { kind: 'send', token: found.session.authToken, from, instruction: body, payload });
1017
+ addSystem(reply.ok
1018
+ ? `Handed off to ${found.session.repoName} (${found.session.status}).\n` +
1019
+ (found.session.status === 'busy'
1020
+ ? 'It is mid-task, so the work is queued and starts when it finishes.'
1021
+ : 'It picks the work up immediately.')
1022
+ : `Could not reach ${found.session.repoName}: ${reply.error}`);
1023
+ return;
1024
+ }
800
1025
  case '/agents': {
801
1026
  // spawn_agents is a tool the model calls itself; this reports how it is configured
802
1027
  // rather than pretending to be a separate agent registry.
@@ -923,8 +1148,10 @@ function App({ config: initialConfig }) {
923
1148
  if (key.escape) {
924
1149
  // The command palette owns the composer while open, so cancelling clears the "/…"
925
1150
  // it put there rather than leaving an orphan the user has to backspace away.
926
- if (picker.kind === 'command')
1151
+ if (picker.kind === 'command') {
927
1152
  setDraft('');
1153
+ setCaret(0);
1154
+ }
928
1155
  closePicker();
929
1156
  return;
930
1157
  }
@@ -954,12 +1181,16 @@ function App({ config: initialConfig }) {
954
1181
  if (picker.kind === 'command' && pickQuery === '') {
955
1182
  closePicker();
956
1183
  setDraft('');
1184
+ setCaret(0);
957
1185
  return;
958
1186
  }
959
1187
  setPickQuery(q => q.slice(0, -1));
960
1188
  setPickIndex(0);
961
- if (picker.kind === 'command')
962
- setDraft('/' + pickQuery.slice(0, -1));
1189
+ if (picker.kind === 'command') {
1190
+ const t = '/' + pickQuery.slice(0, -1);
1191
+ setDraft(t);
1192
+ setCaret(t.length);
1193
+ }
963
1194
  return;
964
1195
  }
965
1196
  // A space means the command name is finished and arguments follow, so the palette
@@ -967,14 +1198,17 @@ function App({ config: initialConfig }) {
967
1198
  // the list, and left Enter with nothing to select.
968
1199
  if (input === ' ' && picker.kind === 'command') {
969
1200
  closePicker();
970
- setDraft(d => (d.endsWith(' ') ? d : d + ' '));
1201
+ setDraft(d => { const t = d.endsWith(' ') ? d : d + ' '; setCaret(t.length); return t; });
971
1202
  return;
972
1203
  }
973
1204
  if (!key.ctrl && !key.meta && input) {
974
1205
  setPickQuery(q => q + input);
975
1206
  setPickIndex(0);
976
- if (picker.kind === 'command')
977
- setDraft('/' + pickQuery + input);
1207
+ if (picker.kind === 'command') {
1208
+ const t = '/' + pickQuery + input;
1209
+ setDraft(t);
1210
+ setCaret(t.length);
1211
+ }
978
1212
  return;
979
1213
  }
980
1214
  return;
@@ -998,19 +1232,87 @@ function App({ config: initialConfig }) {
998
1232
  submitDraft(draft);
999
1233
  return;
1000
1234
  }
1001
- if (key.backspace || key.delete) {
1002
- setDraft(prev => prev.slice(0, -1));
1235
+ // ── Line editing ────────────────────────────────────────────────────────
1236
+ // The composer used to be append-and-backspace only, so fixing a typo near the start of a
1237
+ // long prompt meant deleting everything after it. Everything below is the readline set a
1238
+ // shell gives you, so an edit costs one keystroke rather than a retype.
1239
+ if (key.leftArrow) {
1240
+ setCaret(c => (key.ctrl || key.meta ? wordStart(draft, c) : Math.max(0, c - 1)));
1241
+ return;
1242
+ }
1243
+ if (key.rightArrow) {
1244
+ setCaret(c => (key.ctrl || key.meta ? wordEnd(draft, c) : Math.min(draft.length, c + 1)));
1245
+ return;
1246
+ }
1247
+ // ctrl+a / ctrl+e are line start and end, as in bash. Home/End arrive as these on most
1248
+ // terminals, so both spellings land here.
1249
+ if (key.ctrl && input === 'a') {
1250
+ setCaret(0);
1251
+ return;
1252
+ }
1253
+ if (key.ctrl && input === 'e') {
1254
+ setCaret(draft.length);
1255
+ return;
1256
+ }
1257
+ // ctrl+u clears to line start, ctrl+k to line end, ctrl+w deletes the word behind.
1258
+ if (key.ctrl && input === 'u') {
1259
+ setDraft(d => d.slice(caret));
1260
+ setCaret(0);
1261
+ return;
1262
+ }
1263
+ if (key.ctrl && input === 'k') {
1264
+ setDraft(d => d.slice(0, caret));
1265
+ return;
1266
+ }
1267
+ if (key.ctrl && input === 'w') {
1268
+ const from = wordStart(draft, caret);
1269
+ setDraft(d => d.slice(0, from) + d.slice(caret));
1270
+ setCaret(from);
1271
+ return;
1272
+ }
1273
+ if (key.backspace) {
1274
+ if (caret === 0)
1275
+ return;
1276
+ setDraft(d => d.slice(0, caret - 1) + d.slice(caret));
1277
+ setCaret(c => Math.max(0, c - 1));
1278
+ return;
1279
+ }
1280
+ if (key.delete) { // forward delete leaves the caret put
1281
+ setDraft(d => d.slice(0, caret) + d.slice(caret + 1));
1003
1282
  return;
1004
1283
  }
1005
1284
  // Typing "/" on an empty composer opens the command palette.
1006
1285
  if (input === '/' && draft === '') {
1007
1286
  setDraft('/');
1287
+ setCaret(1);
1008
1288
  openPicker('command', COMMANDS.map(c => ({ value: c.cmd, label: c.cmd, desc: c.desc })));
1009
1289
  return;
1010
1290
  }
1011
- if (!key.ctrl && !key.meta && input)
1012
- setDraft(prev => prev + input);
1291
+ // Insert at the caret rather than appending, so typing mid-string works. A paste arrives as
1292
+ // one chunk and is inserted whole.
1293
+ if (!key.ctrl && !key.meta && input) {
1294
+ setDraft(d => d.slice(0, caret) + input + d.slice(caret));
1295
+ setCaret(c => c + input.length);
1296
+ }
1013
1297
  });
1298
+ /** Start of the word at or before `i`, for word-wise movement and deletion. */
1299
+ function wordStart(text, i) {
1300
+ let j = i;
1301
+ while (j > 0 && /\s/.test(text[j - 1] ?? ''))
1302
+ j--; // skip the gap
1303
+ while (j > 0 && !/\s/.test(text[j - 1] ?? ''))
1304
+ j--; // then the word
1305
+ return j;
1306
+ }
1307
+ /** Start of the word after `i`. */
1308
+ function wordEnd(text, i) {
1309
+ let j = i;
1310
+ while (j < text.length && !/\s/.test(text[j] ?? ''))
1311
+ j++;
1312
+ while (j < text.length && /\s/.test(text[j] ?? ''))
1313
+ j++;
1314
+ return j;
1315
+ }
1014
1316
  /** Sends the composer contents: a slash command, or a task for the agent. */
1015
1317
  function submitDraft(raw) {
1016
1318
  {
@@ -1018,6 +1320,7 @@ function App({ config: initialConfig }) {
1018
1320
  if (!task)
1019
1321
  return;
1020
1322
  setDraft('');
1323
+ setCaret(0);
1021
1324
  addRow({ role: 'user', text: task });
1022
1325
  if (task.startsWith('/')) {
1023
1326
  const spaceIdx = task.indexOf(' ');
@@ -1152,7 +1455,8 @@ function App({ config: initialConfig }) {
1152
1455
  const title = picker.kind === 'command' ? 'Commands'
1153
1456
  : picker.kind === 'model' ? `Models - ${cfg.provider}`
1154
1457
  : picker.kind === 'effort' ? 'Reasoning effort'
1155
- : 'Connect provider';
1458
+ : picker.kind === 'resume' ? 'Resume a saved session'
1459
+ : 'Connect provider';
1156
1460
  let lastGroup;
1157
1461
  return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: CYAN, paddingX: 1, marginTop: 1, width: barWidth, children: [_jsxs(Box, { justifyContent: "space-between", children: [_jsx(Text, { color: CYAN, bold: true, children: title }), _jsxs(Text, { color: DIM, children: [list.length, " match", list.length === 1 ? '' : 'es', " \u00B7 esc to close"] })] }), list.length === 0 && _jsxs(Text, { color: MUTED, children: ["No match for \"", pickQuery, "\""] }), shown.map((item, i) => {
1158
1462
  const absolute = Math.max(0, start) + i;
@@ -1163,7 +1467,11 @@ function App({ config: initialConfig }) {
1163
1467
  return (_jsxs(Box, { flexDirection: "column", children: [header && _jsx(Text, { color: DIM, children: header }), _jsx(Text, { backgroundColor: selected ? CYAN : undefined, color: selected ? '#10222A' : INK, bold: selected, children: (`${selected ? '❯ ' : ' '}${label.padEnd(36)}${item.current ? '(current) ' : ''}${item.desc}`)
1164
1468
  .slice(0, width).padEnd(width) })] }, item.value + absolute));
1165
1469
  }), list.length > PICKER_ROWS && (_jsxs(Text, { color: DIM, children: ["\u2191\u2193 navigate \u00B7 enter select \u00B7 showing ", Math.max(0, start) + 1, "\u2013", Math.max(0, start) + shown.length, " of ", list.length] }))] }));
1166
- })(), keyPrompt && (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: AMBER, paddingX: 1, marginTop: 1, width: barWidth, children: [_jsxs(Text, { color: AMBER, bold: true, children: ["API key for ", keyPrompt.provider] }), _jsx(Text, { color: MUTED, children: "Paste it and press enter. Held in memory for this session only; esc to cancel." }), _jsxs(Box, { marginTop: 1, children: [_jsxs(Text, { color: MUTED, children: [keyPrompt.env, ": "] }), _jsx(Text, { color: INK, children: '*'.repeat(Math.min(keyPrompt.value.length, 48)) }), _jsx(Text, { color: CYAN, children: "\u2588" })] })] })), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: CYAN, bold: true, children: "\u276F " }), _jsx(Text, { color: INK, children: draft }), _jsx(Text, { color: CYAN, children: busy ? '' : '█' })] }), _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() }), _jsx(Text, { color: MUTED, children: " / MODE: " }), _jsx(Text, { color: INK, children: mode }), _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: " / PING: " }), _jsx(Text, { color: INK, children: "50ms" })] }) })] }));
1470
+ })(), keyPrompt && (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: AMBER, paddingX: 1, marginTop: 1, width: barWidth, children: [_jsxs(Text, { color: AMBER, bold: true, children: ["API key for ", keyPrompt.provider] }), _jsx(Text, { color: MUTED, children: "Paste it and press enter. Held in memory for this session only; esc to cancel." }), _jsxs(Box, { marginTop: 1, children: [_jsxs(Text, { color: MUTED, children: [keyPrompt.env, ": "] }), _jsx(Text, { color: INK, children: '*'.repeat(Math.min(keyPrompt.value.length, 48)) }), _jsx(Text, { color: CYAN, children: "\u2588" })] })] })), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: CYAN, bold: true, children: "\u276F " }), _jsx(Text, { color: INK, children: draft.slice(0, caret) }), busy
1471
+ ? _jsx(Text, { color: INK, children: draft.slice(caret) })
1472
+ : caret < draft.length
1473
+ ? _jsxs(_Fragment, { children: [_jsx(Text, { backgroundColor: CYAN, color: "#10222A", children: draft[caret] }), _jsx(Text, { color: INK, children: draft.slice(caret + 1) })] })
1474
+ : _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() }), _jsx(Text, { color: MUTED, children: " / MODE: " }), _jsx(Text, { color: INK, children: mode }), _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: " / PING: " }), _jsx(Text, { color: INK, children: "50ms" })] }) })] }));
1167
1475
  }
1168
1476
  export async function runInkChatMode(config) {
1169
1477
  // resolveConfig has already layered CLI flags above the /config store, so the config