@corenel/term 0.2.9 → 0.4.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/index.js CHANGED
@@ -1,12 +1,16 @@
1
1
  import {
2
2
  TermSession,
3
+ ageWord,
4
+ formatContext,
5
+ mcpStatusWord,
3
6
  parseMarkdown
4
- } from "../chunk-QZJTOVZL.js";
5
- import "../chunk-L4KCEKIT.js";
7
+ } from "../chunk-HUVRQX53.js";
8
+ import "../chunk-2MGDL3I4.js";
6
9
  import {
10
+ TERM_COMMANDS,
7
11
  filterCommands,
8
12
  parseCommand
9
- } from "../chunk-VTJEWHM3.js";
13
+ } from "../chunk-5RPHYXND.js";
10
14
  import "../chunk-M44CET5L.js";
11
15
  import {
12
16
  formatEndingChange
@@ -28,7 +32,7 @@ import { STANDARD_POLICY } from "@corenel/harness/guardrail/builtins";
28
32
  // src/ink/App.tsx
29
33
  import { useEffect, useState } from "react";
30
34
  import { homedir } from "node:os";
31
- import { Box as Box5, Static, Text as Text5, useStdout } from "ink";
35
+ import { Box as Box8, Static, Text as Text8, useStdout } from "ink";
32
36
  import TextInput from "ink-text-input";
33
37
 
34
38
  // src/ink/items.tsx
@@ -169,29 +173,226 @@ function LiveToolRow({ tool }) {
169
173
  ] });
170
174
  }
171
175
 
172
- // src/ink/Modal.tsx
176
+ // src/ink/McpOverlay.tsx
173
177
  import { Box as Box2, Text as Text2 } from "ink";
174
- import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
175
- function DiffPreview({ diff }) {
176
- const contentUnchanged = diff.added === 0 && diff.removed === 0;
177
- const endingRow = diff.endingChange && /* @__PURE__ */ jsx2(Text2, { color: palette.amber, bold: true, children: formatEndingChange(diff.endingChange, contentUnchanged) });
178
- const unexplainedRow = diff.unexplainedChange && /* @__PURE__ */ jsx2(Text2, { color: palette.amber, bold: true, children: "the file changes in a way not shown above (e.g. trailing whitespace or encoding)" });
178
+
179
+ // src/ink/overlayBudget.ts
180
+ var RESERVED_CHROME_ROWS = 6;
181
+ function overlayBoxBudget(rows) {
182
+ if (rows === void 0) return Number.MAX_SAFE_INTEGER;
183
+ return Math.max(rows - RESERVED_CHROME_ROWS, 3);
184
+ }
185
+ function capList(items, cap, overflowLabel) {
186
+ const rowCap = Math.max(cap, 0);
187
+ if (items.length <= rowCap) return { shown: items, overflow: null };
188
+ if (rowCap <= 0) return { shown: [], overflow: null };
189
+ const shown = Math.max(rowCap - 1, 0);
190
+ const hidden = items.length - shown;
191
+ return { shown: items.slice(0, shown), overflow: overflowLabel(hidden) };
192
+ }
193
+
194
+ // src/ink/McpOverlay.tsx
195
+ import { Fragment, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
196
+ function statusColor(word) {
197
+ if (word === "connected") return palette.good;
198
+ if (word === "unreachable") return palette.bad;
199
+ return palette.warn;
200
+ }
201
+ var BORDER_ROWS = 2;
202
+ function McpOverlay({ modal, rows }) {
203
+ let content = Math.max(overlayBoxBudget(rows) - BORDER_ROWS, 0);
204
+ const take = (n) => {
205
+ if (content < n) return false;
206
+ content -= n;
207
+ return true;
208
+ };
209
+ if (modal.login) {
210
+ const haveCore = take(1);
211
+ const showEsc = haveCore && modal.login.url !== null && take(1);
212
+ const showMessage = haveCore && modal.login.url !== null && take(1);
213
+ const showHeader2 = take(1);
214
+ const showFooter2 = take(1);
215
+ return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", borderStyle: "round", borderColor: palette.tool, paddingX: 1, children: [
216
+ showHeader2 ? /* @__PURE__ */ jsx2(Text2, { color: palette.tool, bold: true, children: "MCP servers" }) : null,
217
+ haveCore && modal.login.url ? /* @__PURE__ */ jsxs2(Fragment, { children: [
218
+ showMessage ? /* @__PURE__ */ jsxs2(Text2, { color: palette.amber, children: [
219
+ "signing in to ",
220
+ modal.login.server,
221
+ " -- open this URL:"
222
+ ] }) : null,
223
+ /* @__PURE__ */ jsx2(Text2, { wrap: "truncate-end", children: modal.login.url }),
224
+ showEsc ? /* @__PURE__ */ jsx2(Text2, { color: palette.dim, children: "esc cancels" }) : null
225
+ ] }) : haveCore ? /* @__PURE__ */ jsxs2(Text2, { color: palette.amber, wrap: "truncate-end", children: [
226
+ "signing in to ",
227
+ modal.login.server,
228
+ " -- waiting for a login URL..."
229
+ ] }) : null,
230
+ showFooter2 ? /* @__PURE__ */ jsxs2(Text2, { color: palette.dim, children: [
231
+ glyph.active,
232
+ " select ",
233
+ glyph.sep,
234
+ " a add ",
235
+ glyph.sep,
236
+ " x remove ",
237
+ glyph.sep,
238
+ " l login ",
239
+ glyph.sep,
240
+ " r refresh ",
241
+ glyph.sep,
242
+ " esc close"
243
+ ] }) : null
244
+ ] });
245
+ }
246
+ const showHeader = take(1);
247
+ const emptyRows = modal.servers.length === 0 && take(1);
248
+ const showFooter = take(1);
249
+ const { shown, overflow } = capList(modal.servers, content, (n) => `\u2026 ${n} more server${n === 1 ? "" : "s"}`);
250
+ return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", borderStyle: "round", borderColor: palette.tool, paddingX: 1, children: [
251
+ showHeader ? /* @__PURE__ */ jsx2(Text2, { color: palette.tool, bold: true, children: "MCP servers" }) : null,
252
+ emptyRows ? /* @__PURE__ */ jsx2(Text2, { color: palette.dim, children: "none configured -- press a to add one" }) : /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", children: [
253
+ shown.map((s) => {
254
+ const i = modal.servers.indexOf(s);
255
+ const word = mcpStatusWord(s);
256
+ const isSelected = i === modal.selected;
257
+ return /* @__PURE__ */ jsxs2(Box2, { children: [
258
+ /* @__PURE__ */ jsx2(Text2, { color: palette.ember, children: isSelected ? `${glyph.active} ` : " " }),
259
+ /* @__PURE__ */ jsx2(Text2, { bold: isSelected, wrap: "truncate-end", children: s.name }),
260
+ /* @__PURE__ */ jsx2(Text2, { color: palette.dim, children: " " }),
261
+ /* @__PURE__ */ jsx2(Text2, { color: statusColor(word), children: word }),
262
+ /* @__PURE__ */ jsxs2(Text2, { color: palette.dim, wrap: "truncate-end", children: [
263
+ " ",
264
+ s.toolCount,
265
+ " tool",
266
+ s.toolCount === 1 ? "" : "s",
267
+ ", ",
268
+ ageWord(s.fetchedAt, Date.now())
269
+ ] })
270
+ ] }, s.name);
271
+ }),
272
+ overflow ? /* @__PURE__ */ jsxs2(Text2, { color: palette.dim, children: [
273
+ " ",
274
+ overflow
275
+ ] }) : null
276
+ ] }),
277
+ showFooter ? /* @__PURE__ */ jsxs2(Text2, { color: palette.dim, children: [
278
+ glyph.active,
279
+ " select ",
280
+ glyph.sep,
281
+ " a add ",
282
+ glyph.sep,
283
+ " x remove ",
284
+ glyph.sep,
285
+ " l login ",
286
+ glyph.sep,
287
+ " r refresh ",
288
+ glyph.sep,
289
+ " esc close"
290
+ ] }) : null
291
+ ] });
292
+ }
293
+
294
+ // src/ink/HelpOverlay.tsx
295
+ import { Box as Box3, Text as Text3 } from "ink";
296
+ import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
297
+ var FIXED_CHROME_ROWS = 4;
298
+ function HelpOverlay({ rows }) {
299
+ const width = Math.max(...TERM_COMMANDS.map((c) => `${c.name} ${c.args ?? ""}`.trim().length));
300
+ const bodyBudget = Math.max(overlayBoxBudget(rows) - FIXED_CHROME_ROWS, 0);
301
+ const { shown, overflow } = capList(TERM_COMMANDS, bodyBudget, (n) => `\u2026 ${n} more command${n === 1 ? "" : "s"}`);
302
+ return /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", borderStyle: "round", borderColor: palette.tool, paddingX: 1, children: [
303
+ /* @__PURE__ */ jsx3(Text3, { color: palette.tool, bold: true, children: "Commands" }),
304
+ /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", children: [
305
+ shown.map((c) => {
306
+ const usage = `${c.name} ${c.args ?? ""}`.trim();
307
+ return /* @__PURE__ */ jsxs3(Box3, { children: [
308
+ /* @__PURE__ */ jsxs3(Text3, { color: palette.ember, wrap: "truncate-end", children: [
309
+ "/",
310
+ usage.padEnd(width)
311
+ ] }),
312
+ /* @__PURE__ */ jsxs3(Text3, { color: palette.dim, wrap: "truncate-end", children: [
313
+ " ",
314
+ c.summary
315
+ ] })
316
+ ] }, c.name);
317
+ }),
318
+ overflow ? /* @__PURE__ */ jsxs3(Text3, { color: palette.dim, children: [
319
+ " ",
320
+ overflow
321
+ ] }) : null
322
+ ] }),
323
+ /* @__PURE__ */ jsx3(Text3, { color: palette.dim, children: "esc close" })
324
+ ] });
325
+ }
326
+
327
+ // src/ink/ContextOverlay.tsx
328
+ import { Box as Box4, Text as Text4 } from "ink";
329
+ import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
330
+ var FIXED_CHROME_ROWS2 = 4;
331
+ function ContextOverlay({ snapshot, rows }) {
332
+ const lines = formatContext(snapshot).split("\n");
333
+ const bodyBudget = Math.max(overlayBoxBudget(rows) - FIXED_CHROME_ROWS2, 0);
334
+ const { shown, overflow } = capList(lines, bodyBudget, (n) => `\u2026 ${n} more line${n === 1 ? "" : "s"}`);
335
+ return /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", borderStyle: "round", borderColor: palette.tool, paddingX: 1, children: [
336
+ /* @__PURE__ */ jsx4(Text4, { color: palette.tool, bold: true, children: "Context window" }),
337
+ /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", children: [
338
+ shown.map((line, i) => /* @__PURE__ */ jsx4(Text4, { wrap: "truncate-end", children: line === "" ? " " : line }, i)),
339
+ overflow ? /* @__PURE__ */ jsx4(Text4, { color: palette.dim, children: overflow }) : null
340
+ ] }),
341
+ /* @__PURE__ */ jsx4(Text4, { color: palette.dim, children: "esc close" })
342
+ ] });
343
+ }
344
+ function contextSnapshotFromState(state, contextWindow, toolCount) {
345
+ return {
346
+ sentTokens: state.sentTokens,
347
+ contextWindow,
348
+ offloaded: state.offloaded,
349
+ checkpoints: state.checkpoints,
350
+ toolCount,
351
+ loadedToolNames: state.toolNames
352
+ };
353
+ }
354
+
355
+ // src/ink/Modal.tsx
356
+ import { Box as Box5, Text as Text5 } from "ink";
357
+ import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
358
+ function capDiffRows(rows, max) {
359
+ if (rows.length <= max || max <= 0) return max <= 0 ? [] : rows;
360
+ const kept = rows.slice(0, Math.max(max - 1, 0));
361
+ const dropped = rows.slice(kept.length);
362
+ const hidden = dropped.reduce((n, r) => n + (r.kind === "skip" ? r.count : 1), 0);
363
+ return [...kept, { kind: "skip", count: hidden, reason: "context" }];
364
+ }
365
+ function DiffPreview({ diff, budget }) {
366
+ if (budget !== void 0 && budget <= 0) return null;
179
367
  if (diff.noop) {
180
- return /* @__PURE__ */ jsx2(Text2, { color: palette.dim, children: "(no change -- new content is identical to what's on disk)" });
368
+ return /* @__PURE__ */ jsx5(Text5, { color: palette.dim, children: "(no change -- new content is identical to what's on disk)" });
181
369
  }
182
- return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", children: [
370
+ const contentUnchanged = diff.added === 0 && diff.removed === 0;
371
+ let remaining = budget !== void 0 ? budget - 1 : void 0;
372
+ const fits = () => {
373
+ if (remaining === void 0) return true;
374
+ if (remaining < 1) return false;
375
+ remaining -= 1;
376
+ return true;
377
+ };
378
+ const endingRow = diff.endingChange && fits() && /* @__PURE__ */ jsx5(Text5, { color: palette.amber, bold: true, children: formatEndingChange(diff.endingChange, contentUnchanged) });
379
+ const unexplainedRow = diff.unexplainedChange && fits() && /* @__PURE__ */ jsx5(Text5, { color: palette.amber, bold: true, children: "the file changes in a way not shown above (e.g. trailing whitespace or encoding)" });
380
+ const newFileRow = diff.isNewFile && fits() && /* @__PURE__ */ jsx5(Text5, { color: palette.dim, children: "(new file)" });
381
+ const rows = remaining !== void 0 ? capDiffRows(diff.rows, remaining) : diff.rows;
382
+ return /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", children: [
183
383
  endingRow,
184
384
  unexplainedRow,
185
- diff.isNewFile && /* @__PURE__ */ jsx2(Text2, { color: palette.dim, children: "(new file)" }),
186
- diff.rows.map((r, i) => {
385
+ newFileRow,
386
+ rows.map((r, i) => {
187
387
  if (r.kind === "skip") {
188
388
  const label = r.reason === "changes" ? `\u2026 ${r.count} more changed line${r.count === 1 ? "" : "s"}` : `\u2026 ${r.count} unchanged line${r.count === 1 ? "" : "s"}`;
189
- return /* @__PURE__ */ jsx2(Text2, { color: palette.dim, children: label }, i);
389
+ return /* @__PURE__ */ jsx5(Text5, { color: palette.dim, children: label }, i);
190
390
  }
191
- return /* @__PURE__ */ jsxs2(
192
- Text2,
391
+ return /* @__PURE__ */ jsxs5(
392
+ Text5,
193
393
  {
194
394
  color: r.kind === "add" ? palette.good : r.kind === "del" ? palette.bad : palette.dim,
395
+ wrap: "truncate-end",
195
396
  children: [
196
397
  r.kind === "add" ? "+ " : r.kind === "del" ? "- " : " ",
197
398
  r.text
@@ -200,7 +401,7 @@ function DiffPreview({ diff }) {
200
401
  i
201
402
  );
202
403
  }),
203
- /* @__PURE__ */ jsxs2(Text2, { color: palette.dim, children: [
404
+ /* @__PURE__ */ jsxs5(Text5, { color: palette.dim, children: [
204
405
  "+",
205
406
  diff.added,
206
407
  " \u2212",
@@ -208,69 +409,90 @@ function DiffPreview({ diff }) {
208
409
  ] })
209
410
  ] });
210
411
  }
211
- function ModalView({ modal }) {
412
+ var PERMISSION_BORDER_ROWS = 2;
413
+ var QUESTION_FIXED_ROWS = 5;
414
+ function ModalView({ modal, rows }) {
212
415
  if (modal.kind === "permission") {
213
- return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", borderStyle: "round", borderColor: palette.warn, paddingX: 1, children: [
214
- /* @__PURE__ */ jsxs2(Text2, { color: palette.warn, children: [
416
+ let content = Math.max(overlayBoxBudget(rows) - PERMISSION_BORDER_ROWS, 0);
417
+ const take = (n) => {
418
+ if (content < n) return false;
419
+ content -= n;
420
+ return true;
421
+ };
422
+ const showButtons = take(1);
423
+ const showTool = take(1);
424
+ const showDetail = take(1);
425
+ const diffBudget = modal.diff ? content : void 0;
426
+ return /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", borderStyle: "round", borderColor: palette.warn, paddingX: 1, children: [
427
+ showTool ? /* @__PURE__ */ jsxs5(Text5, { color: palette.warn, children: [
215
428
  modal.tool,
216
429
  " wants to run"
217
- ] }),
218
- /* @__PURE__ */ jsx2(Box2, { marginTop: 1, children: /* @__PURE__ */ jsx2(Text2, { bold: true, children: modal.detail }) }),
219
- modal.diff && /* @__PURE__ */ jsx2(Box2, { marginTop: 1, children: /* @__PURE__ */ jsx2(DiffPreview, { diff: modal.diff }) }),
220
- /* @__PURE__ */ jsxs2(Box2, { marginTop: 1, children: [
221
- /* @__PURE__ */ jsx2(Text2, { color: palette.good, children: "[y]" }),
222
- /* @__PURE__ */ jsx2(Text2, { color: palette.dim, children: " run it " }),
223
- /* @__PURE__ */ jsx2(Text2, { color: palette.bad, children: "[n]" }),
224
- /* @__PURE__ */ jsx2(Text2, { color: palette.dim, children: " deny esc denies" })
225
- ] })
430
+ ] }) : null,
431
+ showDetail ? /* @__PURE__ */ jsx5(Text5, { bold: true, wrap: "truncate-end", children: modal.detail }) : null,
432
+ modal.diff && /* @__PURE__ */ jsx5(DiffPreview, { diff: modal.diff, budget: diffBudget }),
433
+ showButtons ? /* @__PURE__ */ jsxs5(Box5, { children: [
434
+ /* @__PURE__ */ jsx5(Text5, { color: palette.good, children: "[y]" }),
435
+ /* @__PURE__ */ jsx5(Text5, { color: palette.dim, children: " run it " }),
436
+ /* @__PURE__ */ jsx5(Text5, { color: palette.bad, children: "[n]" }),
437
+ /* @__PURE__ */ jsx5(Text5, { color: palette.dim, children: " deny esc denies" })
438
+ ] }) : null
226
439
  ] });
227
440
  }
228
441
  const q = modal.questions[0];
229
- return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", borderStyle: "round", borderColor: palette.ember, paddingX: 1, children: [
230
- /* @__PURE__ */ jsx2(Text2, { color: palette.dim, children: q?.header }),
231
- /* @__PURE__ */ jsx2(Text2, { bold: true, children: q?.question }),
232
- /* @__PURE__ */ jsx2(Box2, { flexDirection: "column", marginTop: 1, children: (q?.options ?? []).map((o, i) => /* @__PURE__ */ jsxs2(Box2, { children: [
233
- /* @__PURE__ */ jsxs2(Text2, { color: palette.amber, children: [
234
- i + 1,
235
- " "
236
- ] }),
237
- /* @__PURE__ */ jsx2(Text2, { children: o.label }),
238
- o.description ? /* @__PURE__ */ jsxs2(Text2, { color: palette.dim, children: [
239
- " ",
240
- o.description
241
- ] }) : null
242
- ] }, o.label)) }),
243
- /* @__PURE__ */ jsx2(Box2, { marginTop: 1, children: /* @__PURE__ */ jsxs2(Text2, { color: palette.dim, children: [
442
+ const options = q?.options ?? [];
443
+ const optionsBudget = Math.max(overlayBoxBudget(rows) - QUESTION_FIXED_ROWS, 0);
444
+ const { shown, overflow } = capList(options, optionsBudget, (n) => `\u2026 ${n} more option${n === 1 ? "" : "s"}`);
445
+ return /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", borderStyle: "round", borderColor: palette.ember, paddingX: 1, children: [
446
+ /* @__PURE__ */ jsx5(Text5, { color: palette.dim, children: q?.header }),
447
+ /* @__PURE__ */ jsx5(Text5, { bold: true, wrap: "truncate-end", children: q?.question }),
448
+ /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", children: [
449
+ shown.map((o) => {
450
+ const i = options.indexOf(o);
451
+ return /* @__PURE__ */ jsxs5(Box5, { children: [
452
+ /* @__PURE__ */ jsxs5(Text5, { color: palette.amber, children: [
453
+ i + 1,
454
+ " "
455
+ ] }),
456
+ /* @__PURE__ */ jsx5(Text5, { wrap: "truncate-end", children: o.label }),
457
+ o.description ? /* @__PURE__ */ jsxs5(Text5, { color: palette.dim, wrap: "truncate-end", children: [
458
+ " ",
459
+ o.description
460
+ ] }) : null
461
+ ] }, o.label);
462
+ }),
463
+ overflow ? /* @__PURE__ */ jsx5(Text5, { color: palette.dim, children: overflow }) : null
464
+ ] }),
465
+ /* @__PURE__ */ jsxs5(Text5, { color: palette.dim, children: [
244
466
  "1-",
245
- (q?.options ?? []).length,
467
+ options.length,
246
468
  " choose \xB7 \u21B5 confirm \xB7 or type your own answer \xB7 esc dismisses"
247
- ] }) })
469
+ ] })
248
470
  ] });
249
471
  }
250
472
 
251
473
  // src/ink/Palette.tsx
252
- import { Box as Box3, Text as Text3 } from "ink";
253
- import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
474
+ import { Box as Box6, Text as Text6 } from "ink";
475
+ import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
254
476
  function PaletteView({ items, selected }) {
255
- return /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", children: [
256
- items.map((c, i) => /* @__PURE__ */ jsxs3(Box3, { children: [
257
- /* @__PURE__ */ jsxs3(Text3, { color: i === selected ? palette.ember : palette.dim, children: [
477
+ return /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", children: [
478
+ items.map((c, i) => /* @__PURE__ */ jsxs6(Box6, { children: [
479
+ /* @__PURE__ */ jsxs6(Text6, { color: i === selected ? palette.ember : palette.dim, children: [
258
480
  i === selected ? "\u203A " : " ",
259
481
  "/",
260
482
  c.name,
261
483
  c.args ? ` ${c.args}` : ""
262
484
  ] }),
263
- /* @__PURE__ */ jsxs3(Text3, { color: palette.dim, children: [
485
+ /* @__PURE__ */ jsxs6(Text6, { color: palette.dim, children: [
264
486
  " ",
265
487
  c.summary
266
488
  ] })
267
489
  ] }, c.name)),
268
- /* @__PURE__ */ jsx3(Text3, { color: palette.dim, children: " \u2191\u2193 choose \xB7 tab completes \xB7 esc closes" })
490
+ /* @__PURE__ */ jsx6(Text6, { color: palette.dim, children: " \u2191\u2193 choose \xB7 tab completes \xB7 esc closes" })
269
491
  ] });
270
492
  }
271
493
 
272
494
  // src/ink/TodoBlock.tsx
273
- import { Box as Box4, Text as Text4 } from "ink";
495
+ import { Box as Box7, Text as Text7 } from "ink";
274
496
 
275
497
  // src/ink/todos.ts
276
498
  var DEFAULT_TODO_ROW_CAP = 4;
@@ -300,7 +522,7 @@ function toRows(items) {
300
522
  }
301
523
 
302
524
  // src/ink/TodoBlock.tsx
303
- import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
525
+ import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
304
526
  function marker(status) {
305
527
  switch (status) {
306
528
  case "in_progress":
@@ -316,21 +538,21 @@ function marker(status) {
316
538
  }
317
539
  function TodoRowView({ row }) {
318
540
  const { char, color } = marker(row.status);
319
- return /* @__PURE__ */ jsxs4(Box4, { children: [
320
- /* @__PURE__ */ jsxs4(Text4, { color, children: [
541
+ return /* @__PURE__ */ jsxs7(Box7, { children: [
542
+ /* @__PURE__ */ jsxs7(Text7, { color, children: [
321
543
  char,
322
544
  " "
323
545
  ] }),
324
- /* @__PURE__ */ jsx4(Text4, { color: row.status === "in_progress" ? palette.ink : palette.soft, children: row.title })
546
+ /* @__PURE__ */ jsx7(Text7, { color: row.status === "in_progress" ? palette.ink : palette.soft, children: row.title })
325
547
  ] });
326
548
  }
327
549
  function TodoBlock({ todos }) {
328
550
  const view = visibleTodos(todos);
329
551
  if (!view) return null;
330
- return /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", marginTop: 1, children: [
331
- /* @__PURE__ */ jsx4(Text4, { color: palette.dim, children: view.header }),
332
- view.rows.map((row) => /* @__PURE__ */ jsx4(TodoRowView, { row }, row.id)),
333
- view.overflow ? /* @__PURE__ */ jsxs4(Text4, { color: palette.dim, children: [
552
+ return /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", marginTop: 1, children: [
553
+ /* @__PURE__ */ jsx7(Text7, { color: palette.dim, children: view.header }),
554
+ view.rows.map((row) => /* @__PURE__ */ jsx7(TodoRowView, { row }, row.id)),
555
+ view.overflow ? /* @__PURE__ */ jsxs7(Text7, { color: palette.dim, children: [
334
556
  " ",
335
557
  view.overflow
336
558
  ] }) : null
@@ -380,7 +602,7 @@ function fitStatus(left, identity, columns) {
380
602
  }
381
603
 
382
604
  // src/ink/App.tsx
383
- import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
605
+ import { jsx as jsx8, jsxs as jsxs8 } from "react/jsx-runtime";
384
606
  function useColumns() {
385
607
  const { stdout } = useStdout();
386
608
  const [columns, setColumns] = useState(stdout?.columns ?? DEFAULT_COLUMNS);
@@ -395,6 +617,23 @@ function useColumns() {
395
617
  }, [stdout]);
396
618
  return columns;
397
619
  }
620
+ function usableRows(rows) {
621
+ return rows !== void 0 && Number.isFinite(rows) && rows > 0 ? rows : void 0;
622
+ }
623
+ function useRows() {
624
+ const { stdout } = useStdout();
625
+ const [rows, setRows] = useState(usableRows(stdout?.rows));
626
+ useEffect(() => {
627
+ if (!stdout) return;
628
+ const onResize = () => setRows(usableRows(stdout.rows));
629
+ onResize();
630
+ stdout.on("resize", onResize);
631
+ return () => {
632
+ stdout.off("resize", onResize);
633
+ };
634
+ }, [stdout]);
635
+ return rows;
636
+ }
398
637
  function usd(v) {
399
638
  return v < 0.01 ? `$${v.toFixed(4)}` : `$${v.toFixed(2)}`;
400
639
  }
@@ -404,6 +643,8 @@ function App(props) {
404
643
  const paletteSelected = Math.min(props.paletteSelected, Math.max(paletteItems.length - 1, 0));
405
644
  const busy = state.status !== "idle";
406
645
  const columns = useColumns();
646
+ const rows = useRows();
647
+ const modalOpen = state.modal != null;
407
648
  const statusWord = busy ? "running" : "idle";
408
649
  const roundSeg = state.live ? ` ${glyph.sep} turn ${state.live.turn}` : ` ${glyph.sep} ${props.toolCount} tools`;
409
650
  const turnTokens = tokens(state.turnUsage.promptTokens + state.turnUsage.completionTokens);
@@ -412,31 +653,31 @@ function App(props) {
412
653
  const costSeg = state.costUsd > 0 ? ` ${glyph.sep} ${usd(state.costUsd)}` : "";
413
654
  const escSeg = busy ? ` ${glyph.sep} esc stops` : "";
414
655
  const queuedSeg = state.queued.length ? ` ${glyph.sep} ${state.queued.length} queued` : "";
415
- const shellSeg = !busy ? ` ${glyph.sep} shell ${props.approveAll ? "auto-approved" : "asks first"}` : "";
656
+ const shellSeg = ` ${glyph.sep} shell ${props.approveAll ? "auto-approved" : "asks first"}`;
416
657
  const statusLeft = ` ${statusWord}${roundSeg}${tokenSeg}${costSeg}${escSeg}${queuedSeg}${shellSeg}`;
417
- const { right: statusRight } = fitStatus(statusLeft, { model: props.model, cwd: props.cwd, home: homedir() }, columns);
418
- return /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", children: [
419
- /* @__PURE__ */ jsx5(Static, { items: state.committed, children: (item) => /* @__PURE__ */ jsx5(CommittedRow, { item }, item.id) }, `transcript-${state.transcriptGen}`),
420
- state.live?.tools.map((t) => /* @__PURE__ */ jsx5(LiveToolRow, { tool: t }, t.callId)),
421
- state.live?.streaming ? /* @__PURE__ */ jsx5(Box5, { marginTop: 1, children: /* @__PURE__ */ jsx5(Text5, { color: palette.soft, children: state.live.streaming }) }) : null,
422
- /* @__PURE__ */ jsx5(TodoBlock, { todos: state.todos }),
423
- state.modal ? /* @__PURE__ */ jsx5(Box5, { marginTop: 1, children: /* @__PURE__ */ jsx5(ModalView, { modal: state.modal }) }) : null,
424
- paletteItems.length && !state.modal ? /* @__PURE__ */ jsx5(Box5, { marginTop: 1, children: /* @__PURE__ */ jsx5(PaletteView, { items: paletteItems, selected: paletteSelected }) }) : null,
425
- state.pendingSkill ? /* @__PURE__ */ jsxs5(Box5, { marginTop: 1, children: [
426
- /* @__PURE__ */ jsxs5(Text5, { color: palette.ember, children: [
658
+ const { right: statusRight } = fitStatus(statusLeft, { model: state.model, cwd: props.cwd, home: homedir() }, columns);
659
+ return /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", children: [
660
+ /* @__PURE__ */ jsx8(Static, { items: state.committed, children: (item) => /* @__PURE__ */ jsx8(CommittedRow, { item }, item.id) }, `transcript-${state.transcriptGen}`),
661
+ !modalOpen && state.live?.tools.map((t) => /* @__PURE__ */ jsx8(LiveToolRow, { tool: t }, t.callId)),
662
+ !modalOpen && state.live?.streaming ? /* @__PURE__ */ jsx8(Box8, { marginTop: 1, children: /* @__PURE__ */ jsx8(Text8, { color: palette.soft, children: state.live.streaming }) }) : null,
663
+ !modalOpen ? /* @__PURE__ */ jsx8(TodoBlock, { todos: state.todos }) : null,
664
+ state.modal ? /* @__PURE__ */ jsx8(Box8, { marginTop: 1, children: state.modal.kind === "mcp" ? /* @__PURE__ */ jsx8(McpOverlay, { modal: state.modal, rows }) : state.modal.kind === "help" ? /* @__PURE__ */ jsx8(HelpOverlay, { rows }) : state.modal.kind === "context" ? /* @__PURE__ */ jsx8(ContextOverlay, { snapshot: contextSnapshotFromState(state, props.contextWindow ?? null, props.toolCount), rows }) : state.modal.kind === "permission" || state.modal.kind === "user" ? /* @__PURE__ */ jsx8(ModalView, { modal: state.modal, rows }) : null }) : null,
665
+ paletteItems.length && !modalOpen ? /* @__PURE__ */ jsx8(Box8, { marginTop: 1, children: /* @__PURE__ */ jsx8(PaletteView, { items: paletteItems, selected: paletteSelected }) }) : null,
666
+ state.pendingSkill && !modalOpen ? /* @__PURE__ */ jsxs8(Box8, { marginTop: 1, children: [
667
+ /* @__PURE__ */ jsxs8(Text8, { color: palette.ember, children: [
427
668
  glyph.sep,
428
669
  " skill armed: ",
429
670
  state.pendingSkill,
430
671
  " "
431
672
  ] }),
432
- /* @__PURE__ */ jsx5(Text5, { color: palette.dim, children: "-- applies to your next message, then clears" })
673
+ /* @__PURE__ */ jsx8(Text8, { color: palette.dim, children: "-- applies to your next message, then clears" })
433
674
  ] }) : null,
434
- /* @__PURE__ */ jsxs5(Box5, { borderStyle: "round", borderColor: palette.dim, paddingX: 1, marginTop: 1, children: [
435
- /* @__PURE__ */ jsxs5(Text5, { color: palette.ember, children: [
675
+ /* @__PURE__ */ jsxs8(Box8, { borderStyle: "round", borderColor: palette.dim, paddingX: 1, marginTop: 1, children: [
676
+ /* @__PURE__ */ jsxs8(Text8, { color: palette.ember, children: [
436
677
  glyph.prompt,
437
678
  " "
438
679
  ] }),
439
- /* @__PURE__ */ jsx5(
680
+ /* @__PURE__ */ jsx8(
440
681
  TextInput,
441
682
  {
442
683
  value: props.value,
@@ -448,9 +689,9 @@ function App(props) {
448
689
  props.inputKey
449
690
  )
450
691
  ] }),
451
- state.committed.length === 0 ? /* @__PURE__ */ jsx5(Box5, { marginBottom: 1, children: /* @__PURE__ */ jsxs5(Text5, { color: palette.soft, children: [
692
+ state.committed.length === 0 && !modalOpen ? /* @__PURE__ */ jsx8(Box8, { marginBottom: 1, children: /* @__PURE__ */ jsxs8(Text8, { color: palette.soft, children: [
452
693
  "Type a request. ",
453
- /* @__PURE__ */ jsxs5(Text5, { color: palette.dim, children: [
694
+ /* @__PURE__ */ jsxs8(Text8, { color: palette.dim, children: [
454
695
  "/help for commands ",
455
696
  glyph.sep,
456
697
  " Ctrl-C stops a turn ",
@@ -458,22 +699,22 @@ function App(props) {
458
699
  " Ctrl-D leaves"
459
700
  ] })
460
701
  ] }) }) : null,
461
- /* @__PURE__ */ jsxs5(Box5, { justifyContent: "space-between", children: [
462
- /* @__PURE__ */ jsxs5(Box5, { children: [
463
- /* @__PURE__ */ jsxs5(Text5, { color: busy ? palette.amber : palette.dim, children: [
702
+ /* @__PURE__ */ jsxs8(Box8, { justifyContent: "space-between", children: [
703
+ /* @__PURE__ */ jsxs8(Box8, { children: [
704
+ /* @__PURE__ */ jsxs8(Text8, { color: busy ? palette.amber : palette.dim, children: [
464
705
  " ",
465
706
  statusWord
466
707
  ] }),
467
- /* @__PURE__ */ jsxs5(Text5, { color: palette.dim, children: [
708
+ /* @__PURE__ */ jsxs8(Text8, { color: palette.dim, children: [
468
709
  roundSeg,
469
710
  tokenSeg,
470
711
  costSeg,
471
712
  escSeg,
472
713
  queuedSeg
473
714
  ] }),
474
- !busy ? /* @__PURE__ */ jsx5(Text5, { color: props.approveAll ? palette.ember : palette.warn, children: shellSeg }) : null
715
+ /* @__PURE__ */ jsx8(Text8, { color: props.approveAll ? palette.ember : palette.warn, children: shellSeg })
475
716
  ] }),
476
- statusRight ? /* @__PURE__ */ jsx5(Text5, { color: palette.dim, children: statusRight }) : null
717
+ statusRight ? /* @__PURE__ */ jsx8(Text8, { color: palette.dim, children: statusRight }) : null
477
718
  ] })
478
719
  ] });
479
720
  }
@@ -561,7 +802,7 @@ function writeBanner(out, opts) {
561
802
  }
562
803
 
563
804
  // src/ink/index.tsx
564
- import { jsx as jsx6 } from "react/jsx-runtime";
805
+ import { jsx as jsx9 } from "react/jsx-runtime";
565
806
  function Root({ session, deps }) {
566
807
  const state = useSyncExternalStore(
567
808
  useCallback((fn) => session.subscribe(fn), [session]),
@@ -592,6 +833,53 @@ function Root({ session, deps }) {
592
833
  };
593
834
  useInput((input, key) => {
594
835
  if (state.modal) {
836
+ if (state.modal.kind === "mcp") {
837
+ if (key.escape) {
838
+ session.closeMcpManager();
839
+ return;
840
+ }
841
+ if (key.upArrow) {
842
+ session.moveMcpSelection(-1);
843
+ return;
844
+ }
845
+ if (key.downArrow) {
846
+ session.moveMcpSelection(1);
847
+ return;
848
+ }
849
+ const lower = input.toLowerCase();
850
+ if (lower === "x") {
851
+ void session.removeMcpSelection();
852
+ return;
853
+ }
854
+ if (lower === "l") {
855
+ void session.loginMcpSelection();
856
+ return;
857
+ }
858
+ if (lower === "r") {
859
+ void session.refreshMcpSelection();
860
+ return;
861
+ }
862
+ if (lower === "a") {
863
+ session.closeMcpManager();
864
+ completeTo("/mcp add ");
865
+ return;
866
+ }
867
+ return;
868
+ }
869
+ if (state.modal.kind === "help") {
870
+ if (key.escape) {
871
+ session.closeHelp();
872
+ return;
873
+ }
874
+ return;
875
+ }
876
+ if (state.modal.kind === "context") {
877
+ if (key.escape) {
878
+ session.closeContext();
879
+ return;
880
+ }
881
+ return;
882
+ }
595
883
  if (key.escape) {
596
884
  session.answer(state.modal.kind === "permission" ? "deny" : null);
597
885
  return;
@@ -649,7 +937,7 @@ function Root({ session, deps }) {
649
937
  }
650
938
  if (sawSigint) setSawSigint(false);
651
939
  });
652
- return /* @__PURE__ */ jsx6(
940
+ return /* @__PURE__ */ jsx9(
653
941
  App,
654
942
  {
655
943
  state,
@@ -657,10 +945,26 @@ function Root({ session, deps }) {
657
945
  cwd: deps.cwd,
658
946
  version: deps.version,
659
947
  toolCount: deps.tools.length,
948
+ contextWindow: deps.contextWindow?.() ?? null,
660
949
  approveAll: deps.approveAll === true,
661
950
  value,
662
951
  onChange: handleChange,
663
952
  onSubmit: (v) => {
953
+ if (v.trim() === "/help") {
954
+ setValue("");
955
+ session.openHelp();
956
+ return;
957
+ }
958
+ if (v.trim() === "/mcp") {
959
+ setValue("");
960
+ void session.openMcpManager();
961
+ return;
962
+ }
963
+ if (v.trim() === "/context") {
964
+ setValue("");
965
+ session.openContext();
966
+ return;
967
+ }
664
968
  const items = v.startsWith("/") ? filterCommands(v) : [];
665
969
  if (items.length > 0 && !parseCommand(v)) {
666
970
  const chosen = items[Math.min(paletteIndex, items.length - 1)];
@@ -677,6 +981,10 @@ function Root({ session, deps }) {
677
981
  return;
678
982
  }
679
983
  }
984
+ if (session.willReject(v)) {
985
+ void session.submit(v);
986
+ return;
987
+ }
680
988
  setValue("");
681
989
  void session.submit(v).then(() => {
682
990
  if (session.wantsExit) exit();
@@ -687,9 +995,9 @@ function Root({ session, deps }) {
687
995
  const modal = session.getSnapshot().modal;
688
996
  if (!modal) return;
689
997
  if (modal.kind === "permission") session.answer(/^y(es)?$/i.test(v.trim()) ? "allow" : "deny");
690
- else session.answer(v.trim() ? { [modal.questions[0].header]: [v.trim()] } : null);
998
+ else if (modal.kind === "user") session.answer(v.trim() ? { [modal.questions[0].header]: [v.trim()] } : null);
691
999
  },
692
- focus: state.modal?.kind !== "permission",
1000
+ focus: state.modal == null || state.modal.kind === "user",
693
1001
  paletteSelected: paletteIndex,
694
1002
  inputKey: fieldGen
695
1003
  }
@@ -714,7 +1022,7 @@ async function runTui(deps) {
714
1022
  reserve: bannerHeight(process.stdout, bannerOpts)
715
1023
  });
716
1024
  writeBanner(process.stdout, bannerOpts);
717
- const app = render(/* @__PURE__ */ jsx6(Root, { session, deps }), {
1025
+ const app = render(/* @__PURE__ */ jsx9(Root, { session, deps }), {
718
1026
  /* LOAD-BEARING — do not "clean up" or default this away. Ink's own App
719
1027
  * component (build/components/App.js, handleInput) intercepts the raw
720
1028
  * \x03 byte and calls handleExit() BEFORE it ever emits the 'input'