@corenel/term 0.3.0 → 0.5.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
@@ -3,14 +3,19 @@ import {
3
3
  ageWord,
4
4
  formatContext,
5
5
  mcpStatusWord,
6
- parseMarkdown
7
- } from "../chunk-HUVRQX53.js";
8
- import "../chunk-2MGDL3I4.js";
6
+ parseMarkdown,
7
+ parseSuggestionChoice,
8
+ suggestionKeys,
9
+ suggestionLine
10
+ } from "../chunk-IF6DCCK2.js";
11
+ import {
12
+ SUGGESTION_ARM_DELAY_MS
13
+ } from "../chunk-MAZLORMD.js";
9
14
  import {
10
15
  TERM_COMMANDS,
11
16
  filterCommands,
12
17
  parseCommand
13
- } from "../chunk-5RPHYXND.js";
18
+ } from "../chunk-TWFWLEIP.js";
14
19
  import "../chunk-M44CET5L.js";
15
20
  import {
16
21
  formatEndingChange
@@ -28,6 +33,7 @@ import { useCallback, useState as useState2, useSyncExternalStore } from "react"
28
33
  import { render, useApp, useInput } from "ink";
29
34
  import { PermissionService as PermissionService2 } from "@corenel/harness/guardrail/permission-service";
30
35
  import { STANDARD_POLICY } from "@corenel/harness/guardrail/builtins";
36
+ import { approvalsHooks } from "@corenel/harness/approvals";
31
37
 
32
38
  // src/ink/App.tsx
33
39
  import { useEffect, useState } from "react";
@@ -175,47 +181,106 @@ function LiveToolRow({ tool }) {
175
181
 
176
182
  // src/ink/McpOverlay.tsx
177
183
  import { Box as Box2, Text as Text2 } from "ink";
184
+
185
+ // src/ink/overlayBudget.ts
186
+ var RESERVED_CHROME_ROWS = 6;
187
+ function overlayBoxBudget(rows) {
188
+ if (rows === void 0) return Number.MAX_SAFE_INTEGER;
189
+ return Math.max(rows - RESERVED_CHROME_ROWS, 3);
190
+ }
191
+ function capList(items, cap, overflowLabel) {
192
+ const rowCap = Math.max(cap, 0);
193
+ if (items.length <= rowCap) return { shown: items, overflow: null };
194
+ if (rowCap <= 0) return { shown: [], overflow: null };
195
+ const shown = Math.max(rowCap - 1, 0);
196
+ const hidden = items.length - shown;
197
+ return { shown: items.slice(0, shown), overflow: overflowLabel(hidden) };
198
+ }
199
+
200
+ // src/ink/McpOverlay.tsx
178
201
  import { Fragment, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
179
202
  function statusColor(word) {
180
203
  if (word === "connected") return palette.good;
181
204
  if (word === "unreachable") return palette.bad;
182
205
  return palette.warn;
183
206
  }
184
- function McpOverlay({ modal }) {
185
- return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", borderStyle: "round", borderColor: palette.tool, paddingX: 1, children: [
186
- /* @__PURE__ */ jsx2(Text2, { color: palette.tool, bold: true, children: "MCP servers" }),
187
- modal.servers.length === 0 ? /* @__PURE__ */ jsx2(Box2, { marginTop: 1, children: /* @__PURE__ */ jsx2(Text2, { color: palette.dim, children: "none configured -- press a to add one" }) }) : /* @__PURE__ */ jsx2(Box2, { flexDirection: "column", marginTop: 1, children: modal.servers.map((s, i) => {
188
- const word = mcpStatusWord(s);
189
- const isSelected = i === modal.selected;
190
- return /* @__PURE__ */ jsxs2(Box2, { children: [
191
- /* @__PURE__ */ jsx2(Text2, { color: palette.ember, children: isSelected ? `${glyph.active} ` : " " }),
192
- /* @__PURE__ */ jsx2(Text2, { bold: isSelected, children: s.name }),
193
- /* @__PURE__ */ jsx2(Text2, { color: palette.dim, children: " " }),
194
- /* @__PURE__ */ jsx2(Text2, { color: statusColor(word), children: word }),
195
- /* @__PURE__ */ jsxs2(Text2, { color: palette.dim, children: [
196
- " ",
197
- s.toolCount,
198
- " tool",
199
- s.toolCount === 1 ? "" : "s",
200
- ", ",
201
- ageWord(s.fetchedAt, Date.now())
202
- ] })
203
- ] }, s.name);
204
- }) }),
205
- modal.login ? /* @__PURE__ */ jsx2(Box2, { flexDirection: "column", marginTop: 1, children: modal.login.url ? /* @__PURE__ */ jsxs2(Fragment, { children: [
206
- /* @__PURE__ */ jsxs2(Text2, { color: palette.amber, children: [
207
+ var BORDER_ROWS = 2;
208
+ function McpOverlay({ modal, rows }) {
209
+ let content = Math.max(overlayBoxBudget(rows) - BORDER_ROWS, 0);
210
+ const take = (n) => {
211
+ if (content < n) return false;
212
+ content -= n;
213
+ return true;
214
+ };
215
+ if (modal.login) {
216
+ const haveCore = take(1);
217
+ const showEsc = haveCore && modal.login.url !== null && take(1);
218
+ const showMessage = haveCore && modal.login.url !== null && take(1);
219
+ const showHeader2 = take(1);
220
+ const showFooter2 = take(1);
221
+ return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", borderStyle: "round", borderColor: palette.tool, paddingX: 1, children: [
222
+ showHeader2 ? /* @__PURE__ */ jsx2(Text2, { color: palette.tool, bold: true, children: "MCP servers" }) : null,
223
+ haveCore && modal.login.url ? /* @__PURE__ */ jsxs2(Fragment, { children: [
224
+ showMessage ? /* @__PURE__ */ jsxs2(Text2, { color: palette.amber, children: [
225
+ "signing in to ",
226
+ modal.login.server,
227
+ " -- open this URL:"
228
+ ] }) : null,
229
+ /* @__PURE__ */ jsx2(Text2, { wrap: "truncate-end", children: modal.login.url }),
230
+ showEsc ? /* @__PURE__ */ jsx2(Text2, { color: palette.dim, children: "esc cancels" }) : null
231
+ ] }) : haveCore ? /* @__PURE__ */ jsxs2(Text2, { color: palette.amber, wrap: "truncate-end", children: [
207
232
  "signing in to ",
208
233
  modal.login.server,
209
- " -- open this URL:"
210
- ] }),
211
- /* @__PURE__ */ jsx2(Text2, { children: modal.login.url }),
212
- /* @__PURE__ */ jsx2(Text2, { color: palette.dim, children: "esc cancels" })
213
- ] }) : /* @__PURE__ */ jsxs2(Text2, { color: palette.amber, children: [
214
- "signing in to ",
215
- modal.login.server,
216
- " -- waiting for a login URL..."
217
- ] }) }) : null,
218
- /* @__PURE__ */ jsx2(Box2, { marginTop: 1, children: /* @__PURE__ */ jsxs2(Text2, { color: palette.dim, children: [
234
+ " -- waiting for a login URL..."
235
+ ] }) : null,
236
+ showFooter2 ? /* @__PURE__ */ jsxs2(Text2, { color: palette.dim, children: [
237
+ glyph.active,
238
+ " select ",
239
+ glyph.sep,
240
+ " a add ",
241
+ glyph.sep,
242
+ " x remove ",
243
+ glyph.sep,
244
+ " l login ",
245
+ glyph.sep,
246
+ " r refresh ",
247
+ glyph.sep,
248
+ " esc close"
249
+ ] }) : null
250
+ ] });
251
+ }
252
+ const showHeader = take(1);
253
+ const emptyRows = modal.servers.length === 0 && take(1);
254
+ const showFooter = take(1);
255
+ const { shown, overflow } = capList(modal.servers, content, (n) => `\u2026 ${n} more server${n === 1 ? "" : "s"}`);
256
+ return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", borderStyle: "round", borderColor: palette.tool, paddingX: 1, children: [
257
+ showHeader ? /* @__PURE__ */ jsx2(Text2, { color: palette.tool, bold: true, children: "MCP servers" }) : null,
258
+ emptyRows ? /* @__PURE__ */ jsx2(Text2, { color: palette.dim, children: "none configured -- press a to add one" }) : /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", children: [
259
+ shown.map((s) => {
260
+ const i = modal.servers.indexOf(s);
261
+ const word = mcpStatusWord(s);
262
+ const isSelected = i === modal.selected;
263
+ return /* @__PURE__ */ jsxs2(Box2, { children: [
264
+ /* @__PURE__ */ jsx2(Text2, { color: palette.ember, children: isSelected ? `${glyph.active} ` : " " }),
265
+ /* @__PURE__ */ jsx2(Text2, { bold: isSelected, wrap: "truncate-end", children: s.name }),
266
+ /* @__PURE__ */ jsx2(Text2, { color: palette.dim, children: " " }),
267
+ /* @__PURE__ */ jsx2(Text2, { color: statusColor(word), children: word }),
268
+ /* @__PURE__ */ jsxs2(Text2, { color: palette.dim, wrap: "truncate-end", children: [
269
+ " ",
270
+ s.toolCount,
271
+ " tool",
272
+ s.toolCount === 1 ? "" : "s",
273
+ ", ",
274
+ ageWord(s.fetchedAt, Date.now())
275
+ ] })
276
+ ] }, s.name);
277
+ }),
278
+ overflow ? /* @__PURE__ */ jsxs2(Text2, { color: palette.dim, children: [
279
+ " ",
280
+ overflow
281
+ ] }) : null
282
+ ] }),
283
+ showFooter ? /* @__PURE__ */ jsxs2(Text2, { color: palette.dim, children: [
219
284
  glyph.active,
220
285
  " select ",
221
286
  glyph.sep,
@@ -228,43 +293,58 @@ function McpOverlay({ modal }) {
228
293
  " r refresh ",
229
294
  glyph.sep,
230
295
  " esc close"
231
- ] }) })
296
+ ] }) : null
232
297
  ] });
233
298
  }
234
299
 
235
300
  // src/ink/HelpOverlay.tsx
236
301
  import { Box as Box3, Text as Text3 } from "ink";
237
302
  import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
238
- function HelpOverlay() {
303
+ var FIXED_CHROME_ROWS = 4;
304
+ function HelpOverlay({ rows }) {
239
305
  const width = Math.max(...TERM_COMMANDS.map((c) => `${c.name} ${c.args ?? ""}`.trim().length));
306
+ const bodyBudget = Math.max(overlayBoxBudget(rows) - FIXED_CHROME_ROWS, 0);
307
+ const { shown, overflow } = capList(TERM_COMMANDS, bodyBudget, (n) => `\u2026 ${n} more command${n === 1 ? "" : "s"}`);
240
308
  return /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", borderStyle: "round", borderColor: palette.tool, paddingX: 1, children: [
241
309
  /* @__PURE__ */ jsx3(Text3, { color: palette.tool, bold: true, children: "Commands" }),
242
- /* @__PURE__ */ jsx3(Box3, { flexDirection: "column", marginTop: 1, children: TERM_COMMANDS.map((c) => {
243
- const usage = `${c.name} ${c.args ?? ""}`.trim();
244
- return /* @__PURE__ */ jsxs3(Box3, { children: [
245
- /* @__PURE__ */ jsxs3(Text3, { color: palette.ember, children: [
246
- "/",
247
- usage.padEnd(width)
248
- ] }),
249
- /* @__PURE__ */ jsxs3(Text3, { color: palette.dim, children: [
250
- " ",
251
- c.summary
252
- ] })
253
- ] }, c.name);
254
- }) }),
255
- /* @__PURE__ */ jsx3(Box3, { marginTop: 1, children: /* @__PURE__ */ jsx3(Text3, { color: palette.dim, children: "esc close" }) })
310
+ /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", children: [
311
+ shown.map((c) => {
312
+ const usage = `${c.name} ${c.args ?? ""}`.trim();
313
+ return /* @__PURE__ */ jsxs3(Box3, { children: [
314
+ /* @__PURE__ */ jsxs3(Text3, { color: palette.ember, wrap: "truncate-end", children: [
315
+ "/",
316
+ usage.padEnd(width)
317
+ ] }),
318
+ /* @__PURE__ */ jsxs3(Text3, { color: palette.dim, wrap: "truncate-end", children: [
319
+ " ",
320
+ c.summary
321
+ ] })
322
+ ] }, c.name);
323
+ }),
324
+ overflow ? /* @__PURE__ */ jsxs3(Text3, { color: palette.dim, children: [
325
+ " ",
326
+ overflow
327
+ ] }) : null
328
+ ] }),
329
+ /* @__PURE__ */ jsx3(Text3, { color: palette.dim, children: "esc close" })
256
330
  ] });
257
331
  }
258
332
 
259
333
  // src/ink/ContextOverlay.tsx
260
334
  import { Box as Box4, Text as Text4 } from "ink";
261
335
  import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
262
- function ContextOverlay({ snapshot }) {
263
- const text = formatContext(snapshot);
336
+ var FIXED_CHROME_ROWS2 = 4;
337
+ function ContextOverlay({ snapshot, rows }) {
338
+ const lines = formatContext(snapshot).split("\n");
339
+ const bodyBudget = Math.max(overlayBoxBudget(rows) - FIXED_CHROME_ROWS2, 0);
340
+ const { shown, overflow } = capList(lines, bodyBudget, (n) => `\u2026 ${n} more line${n === 1 ? "" : "s"}`);
264
341
  return /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", borderStyle: "round", borderColor: palette.tool, paddingX: 1, children: [
265
342
  /* @__PURE__ */ jsx4(Text4, { color: palette.tool, bold: true, children: "Context window" }),
266
- /* @__PURE__ */ jsx4(Box4, { flexDirection: "column", marginTop: 1, children: /* @__PURE__ */ jsx4(Text4, { children: text }) }),
267
- /* @__PURE__ */ jsx4(Box4, { marginTop: 1, children: /* @__PURE__ */ jsx4(Text4, { color: palette.dim, children: "esc close" }) })
343
+ /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", children: [
344
+ shown.map((line, i) => /* @__PURE__ */ jsx4(Text4, { wrap: "truncate-end", children: line === "" ? " " : line }, i)),
345
+ overflow ? /* @__PURE__ */ jsx4(Text4, { color: palette.dim, children: overflow }) : null
346
+ ] }),
347
+ /* @__PURE__ */ jsx4(Text4, { color: palette.dim, children: "esc close" })
268
348
  ] });
269
349
  }
270
350
  function contextSnapshotFromState(state, contextWindow, toolCount) {
@@ -281,18 +361,44 @@ function contextSnapshotFromState(state, contextWindow, toolCount) {
281
361
  // src/ink/Modal.tsx
282
362
  import { Box as Box5, Text as Text5 } from "ink";
283
363
  import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
284
- function DiffPreview({ diff }) {
285
- const contentUnchanged = diff.added === 0 && diff.removed === 0;
286
- const endingRow = diff.endingChange && /* @__PURE__ */ jsx5(Text5, { color: palette.amber, bold: true, children: formatEndingChange(diff.endingChange, contentUnchanged) });
287
- const unexplainedRow = diff.unexplainedChange && /* @__PURE__ */ jsx5(Text5, { color: palette.amber, bold: true, children: "the file changes in a way not shown above (e.g. trailing whitespace or encoding)" });
364
+ function SuggestionView({ suggestion }) {
365
+ return /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", borderStyle: "round", borderColor: palette.ember, paddingX: 1, children: [
366
+ /* @__PURE__ */ jsx5(Text5, { wrap: "truncate-end", children: suggestionLine(suggestion) }),
367
+ /* @__PURE__ */ jsxs5(Text5, { color: palette.dim, wrap: "truncate-end", children: [
368
+ suggestionKeys(suggestion),
369
+ " esc ignores"
370
+ ] })
371
+ ] });
372
+ }
373
+ function capDiffRows(rows, max) {
374
+ if (rows.length <= max || max <= 0) return max <= 0 ? [] : rows;
375
+ const kept = rows.slice(0, Math.max(max - 1, 0));
376
+ const dropped = rows.slice(kept.length);
377
+ const hidden = dropped.reduce((n, r) => n + (r.kind === "skip" ? r.count : 1), 0);
378
+ return [...kept, { kind: "skip", count: hidden, reason: "context" }];
379
+ }
380
+ function DiffPreview({ diff, budget }) {
381
+ if (budget !== void 0 && budget <= 0) return null;
288
382
  if (diff.noop) {
289
383
  return /* @__PURE__ */ jsx5(Text5, { color: palette.dim, children: "(no change -- new content is identical to what's on disk)" });
290
384
  }
385
+ const contentUnchanged = diff.added === 0 && diff.removed === 0;
386
+ let remaining = budget !== void 0 ? budget - 1 : void 0;
387
+ const fits = () => {
388
+ if (remaining === void 0) return true;
389
+ if (remaining < 1) return false;
390
+ remaining -= 1;
391
+ return true;
392
+ };
393
+ const endingRow = diff.endingChange && fits() && /* @__PURE__ */ jsx5(Text5, { color: palette.amber, bold: true, children: formatEndingChange(diff.endingChange, contentUnchanged) });
394
+ 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)" });
395
+ const newFileRow = diff.isNewFile && fits() && /* @__PURE__ */ jsx5(Text5, { color: palette.dim, children: "(new file)" });
396
+ const rows = remaining !== void 0 ? capDiffRows(diff.rows, remaining) : diff.rows;
291
397
  return /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", children: [
292
398
  endingRow,
293
399
  unexplainedRow,
294
- diff.isNewFile && /* @__PURE__ */ jsx5(Text5, { color: palette.dim, children: "(new file)" }),
295
- diff.rows.map((r, i) => {
400
+ newFileRow,
401
+ rows.map((r, i) => {
296
402
  if (r.kind === "skip") {
297
403
  const label = r.reason === "changes" ? `\u2026 ${r.count} more changed line${r.count === 1 ? "" : "s"}` : `\u2026 ${r.count} unchanged line${r.count === 1 ? "" : "s"}`;
298
404
  return /* @__PURE__ */ jsx5(Text5, { color: palette.dim, children: label }, i);
@@ -301,6 +407,7 @@ function DiffPreview({ diff }) {
301
407
  Text5,
302
408
  {
303
409
  color: r.kind === "add" ? palette.good : r.kind === "del" ? palette.bad : palette.dim,
410
+ wrap: "truncate-end",
304
411
  children: [
305
412
  r.kind === "add" ? "+ " : r.kind === "del" ? "- " : " ",
306
413
  r.text
@@ -317,43 +424,64 @@ function DiffPreview({ diff }) {
317
424
  ] })
318
425
  ] });
319
426
  }
320
- function ModalView({ modal }) {
427
+ var PERMISSION_BORDER_ROWS = 2;
428
+ var QUESTION_FIXED_ROWS = 5;
429
+ function ModalView({ modal, rows }) {
321
430
  if (modal.kind === "permission") {
431
+ let content = Math.max(overlayBoxBudget(rows) - PERMISSION_BORDER_ROWS, 0);
432
+ const take = (n) => {
433
+ if (content < n) return false;
434
+ content -= n;
435
+ return true;
436
+ };
437
+ const showButtons = take(1);
438
+ const showTool = take(1);
439
+ const showDetail = take(1);
440
+ const diffBudget = modal.diff ? content : void 0;
322
441
  return /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", borderStyle: "round", borderColor: palette.warn, paddingX: 1, children: [
323
- /* @__PURE__ */ jsxs5(Text5, { color: palette.warn, children: [
442
+ showTool ? /* @__PURE__ */ jsxs5(Text5, { color: palette.warn, children: [
324
443
  modal.tool,
325
444
  " wants to run"
326
- ] }),
327
- /* @__PURE__ */ jsx5(Box5, { marginTop: 1, children: /* @__PURE__ */ jsx5(Text5, { bold: true, children: modal.detail }) }),
328
- modal.diff && /* @__PURE__ */ jsx5(Box5, { marginTop: 1, children: /* @__PURE__ */ jsx5(DiffPreview, { diff: modal.diff }) }),
329
- /* @__PURE__ */ jsxs5(Box5, { marginTop: 1, children: [
445
+ ] }) : null,
446
+ showDetail ? /* @__PURE__ */ jsx5(Text5, { bold: true, wrap: "truncate-end", children: modal.detail }) : null,
447
+ modal.diff && /* @__PURE__ */ jsx5(DiffPreview, { diff: modal.diff, budget: diffBudget }),
448
+ showButtons ? /* @__PURE__ */ jsxs5(Box5, { children: [
330
449
  /* @__PURE__ */ jsx5(Text5, { color: palette.good, children: "[y]" }),
331
450
  /* @__PURE__ */ jsx5(Text5, { color: palette.dim, children: " run it " }),
332
451
  /* @__PURE__ */ jsx5(Text5, { color: palette.bad, children: "[n]" }),
333
452
  /* @__PURE__ */ jsx5(Text5, { color: palette.dim, children: " deny esc denies" })
334
- ] })
453
+ ] }) : null
335
454
  ] });
336
455
  }
337
456
  const q = modal.questions[0];
457
+ const options = q?.options ?? [];
458
+ const optionsBudget = Math.max(overlayBoxBudget(rows) - QUESTION_FIXED_ROWS, 0);
459
+ const { shown, overflow } = capList(options, optionsBudget, (n) => `\u2026 ${n} more option${n === 1 ? "" : "s"}`);
338
460
  return /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", borderStyle: "round", borderColor: palette.ember, paddingX: 1, children: [
339
461
  /* @__PURE__ */ jsx5(Text5, { color: palette.dim, children: q?.header }),
340
- /* @__PURE__ */ jsx5(Text5, { bold: true, children: q?.question }),
341
- /* @__PURE__ */ jsx5(Box5, { flexDirection: "column", marginTop: 1, children: (q?.options ?? []).map((o, i) => /* @__PURE__ */ jsxs5(Box5, { children: [
342
- /* @__PURE__ */ jsxs5(Text5, { color: palette.amber, children: [
343
- i + 1,
344
- " "
345
- ] }),
346
- /* @__PURE__ */ jsx5(Text5, { children: o.label }),
347
- o.description ? /* @__PURE__ */ jsxs5(Text5, { color: palette.dim, children: [
348
- " ",
349
- o.description
350
- ] }) : null
351
- ] }, o.label)) }),
352
- /* @__PURE__ */ jsx5(Box5, { marginTop: 1, children: /* @__PURE__ */ jsxs5(Text5, { color: palette.dim, children: [
462
+ /* @__PURE__ */ jsx5(Text5, { bold: true, wrap: "truncate-end", children: q?.question }),
463
+ /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", children: [
464
+ shown.map((o) => {
465
+ const i = options.indexOf(o);
466
+ return /* @__PURE__ */ jsxs5(Box5, { children: [
467
+ /* @__PURE__ */ jsxs5(Text5, { color: palette.amber, children: [
468
+ i + 1,
469
+ " "
470
+ ] }),
471
+ /* @__PURE__ */ jsx5(Text5, { wrap: "truncate-end", children: o.label }),
472
+ o.description ? /* @__PURE__ */ jsxs5(Text5, { color: palette.dim, wrap: "truncate-end", children: [
473
+ " ",
474
+ o.description
475
+ ] }) : null
476
+ ] }, o.label);
477
+ }),
478
+ overflow ? /* @__PURE__ */ jsx5(Text5, { color: palette.dim, children: overflow }) : null
479
+ ] }),
480
+ /* @__PURE__ */ jsxs5(Text5, { color: palette.dim, children: [
353
481
  "1-",
354
- (q?.options ?? []).length,
482
+ options.length,
355
483
  " choose \xB7 \u21B5 confirm \xB7 or type your own answer \xB7 esc dismisses"
356
- ] }) })
484
+ ] })
357
485
  ] });
358
486
  }
359
487
 
@@ -504,6 +632,23 @@ function useColumns() {
504
632
  }, [stdout]);
505
633
  return columns;
506
634
  }
635
+ function usableRows(rows) {
636
+ return rows !== void 0 && Number.isFinite(rows) && rows > 0 ? rows : void 0;
637
+ }
638
+ function useRows() {
639
+ const { stdout } = useStdout();
640
+ const [rows, setRows] = useState(usableRows(stdout?.rows));
641
+ useEffect(() => {
642
+ if (!stdout) return;
643
+ const onResize = () => setRows(usableRows(stdout.rows));
644
+ onResize();
645
+ stdout.on("resize", onResize);
646
+ return () => {
647
+ stdout.off("resize", onResize);
648
+ };
649
+ }, [stdout]);
650
+ return rows;
651
+ }
507
652
  function usd(v) {
508
653
  return v < 0.01 ? `$${v.toFixed(4)}` : `$${v.toFixed(2)}`;
509
654
  }
@@ -513,11 +658,14 @@ function App(props) {
513
658
  const paletteSelected = Math.min(props.paletteSelected, Math.max(paletteItems.length - 1, 0));
514
659
  const busy = state.status !== "idle";
515
660
  const columns = useColumns();
661
+ const rows = useRows();
662
+ const modalOpen = state.modal != null;
516
663
  const statusWord = busy ? "running" : "idle";
517
664
  const roundSeg = state.live ? ` ${glyph.sep} turn ${state.live.turn}` : ` ${glyph.sep} ${props.toolCount} tools`;
518
665
  const turnTokens = tokens(state.turnUsage.promptTokens + state.turnUsage.completionTokens);
519
666
  const totalTokens = tokens(state.usage.promptTokens + state.usage.completionTokens);
520
- const tokenSeg = ` ${glyph.sep} ${turnTokens} turn ${glyph.sep} ${totalTokens} total`;
667
+ const ctxSeg = state.sentTokens ? `${tokens(state.sentTokens)} ctx ${glyph.sep} ` : "";
668
+ const tokenSeg = ` ${glyph.sep} ${ctxSeg}${turnTokens} turn ${glyph.sep} ${totalTokens} total`;
521
669
  const costSeg = state.costUsd > 0 ? ` ${glyph.sep} ${usd(state.costUsd)}` : "";
522
670
  const escSeg = busy ? ` ${glyph.sep} esc stops` : "";
523
671
  const queuedSeg = state.queued.length ? ` ${glyph.sep} ${state.queued.length} queued` : "";
@@ -526,12 +674,12 @@ function App(props) {
526
674
  const { right: statusRight } = fitStatus(statusLeft, { model: state.model, cwd: props.cwd, home: homedir() }, columns);
527
675
  return /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", children: [
528
676
  /* @__PURE__ */ jsx8(Static, { items: state.committed, children: (item) => /* @__PURE__ */ jsx8(CommittedRow, { item }, item.id) }, `transcript-${state.transcriptGen}`),
529
- state.live?.tools.map((t) => /* @__PURE__ */ jsx8(LiveToolRow, { tool: t }, t.callId)),
530
- state.live?.streaming ? /* @__PURE__ */ jsx8(Box8, { marginTop: 1, children: /* @__PURE__ */ jsx8(Text8, { color: palette.soft, children: state.live.streaming }) }) : null,
531
- /* @__PURE__ */ jsx8(TodoBlock, { todos: state.todos }),
532
- state.modal ? /* @__PURE__ */ jsx8(Box8, { marginTop: 1, children: state.modal.kind === "mcp" ? /* @__PURE__ */ jsx8(McpOverlay, { modal: state.modal }) : state.modal.kind === "help" ? /* @__PURE__ */ jsx8(HelpOverlay, {}) : state.modal.kind === "context" ? /* @__PURE__ */ jsx8(ContextOverlay, { snapshot: contextSnapshotFromState(state, props.contextWindow ?? null, props.toolCount) }) : state.modal.kind === "permission" || state.modal.kind === "user" ? /* @__PURE__ */ jsx8(ModalView, { modal: state.modal }) : null }) : null,
533
- paletteItems.length && !state.modal ? /* @__PURE__ */ jsx8(Box8, { marginTop: 1, children: /* @__PURE__ */ jsx8(PaletteView, { items: paletteItems, selected: paletteSelected }) }) : null,
534
- state.pendingSkill ? /* @__PURE__ */ jsxs8(Box8, { marginTop: 1, children: [
677
+ !modalOpen && state.live?.tools.map((t) => /* @__PURE__ */ jsx8(LiveToolRow, { tool: t }, t.callId)),
678
+ !modalOpen && state.live?.streaming ? /* @__PURE__ */ jsx8(Box8, { marginTop: 1, children: /* @__PURE__ */ jsx8(Text8, { color: palette.soft, children: state.live.streaming }) }) : null,
679
+ !modalOpen ? /* @__PURE__ */ jsx8(TodoBlock, { todos: state.todos }) : null,
680
+ 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 === "suggestion" ? /* @__PURE__ */ jsx8(SuggestionView, { suggestion: state.modal.suggestion }) : state.modal.kind === "permission" || state.modal.kind === "user" ? /* @__PURE__ */ jsx8(ModalView, { modal: state.modal, rows }) : null }) : null,
681
+ paletteItems.length && !modalOpen ? /* @__PURE__ */ jsx8(Box8, { marginTop: 1, children: /* @__PURE__ */ jsx8(PaletteView, { items: paletteItems, selected: paletteSelected }) }) : null,
682
+ state.pendingSkill && !modalOpen ? /* @__PURE__ */ jsxs8(Box8, { marginTop: 1, children: [
535
683
  /* @__PURE__ */ jsxs8(Text8, { color: palette.ember, children: [
536
684
  glyph.sep,
537
685
  " skill armed: ",
@@ -557,7 +705,7 @@ function App(props) {
557
705
  props.inputKey
558
706
  )
559
707
  ] }),
560
- state.committed.length === 0 ? /* @__PURE__ */ jsx8(Box8, { marginBottom: 1, children: /* @__PURE__ */ jsxs8(Text8, { color: palette.soft, children: [
708
+ state.committed.length === 0 && !modalOpen ? /* @__PURE__ */ jsx8(Box8, { marginBottom: 1, children: /* @__PURE__ */ jsxs8(Text8, { color: palette.soft, children: [
561
709
  "Type a request. ",
562
710
  /* @__PURE__ */ jsxs8(Text8, { color: palette.dim, children: [
563
711
  "/help for commands ",
@@ -748,6 +896,34 @@ function Root({ session, deps }) {
748
896
  }
749
897
  return;
750
898
  }
899
+ if (state.modal.kind === "suggestion") {
900
+ if (key.ctrl && input === "c") {
901
+ if (state.status !== "idle" && !sawSigint) {
902
+ session.abort();
903
+ setSawSigint(true);
904
+ return;
905
+ }
906
+ exit();
907
+ return;
908
+ }
909
+ if (key.escape) {
910
+ session.dismissApprovalSuggestion();
911
+ if (state.status !== "idle") session.abort();
912
+ return;
913
+ }
914
+ if (Date.now() - state.modal.openedAt < SUGGESTION_ARM_DELAY_MS) return;
915
+ if (key.return) {
916
+ if (state.modal.armedChoice) void session.respondApprovalSuggestion(state.modal.armedChoice);
917
+ return;
918
+ }
919
+ const choice = parseSuggestionChoice(state.modal.suggestion, input);
920
+ if (choice === "workspace" || choice === "global") {
921
+ session.armApprovalSuggestion(choice);
922
+ return;
923
+ }
924
+ if (choice) void session.respondApprovalSuggestion(choice);
925
+ return;
926
+ }
751
927
  if (key.escape) {
752
928
  session.answer(state.modal.kind === "permission" ? "deny" : null);
753
929
  return;
@@ -871,13 +1047,29 @@ function Root({ session, deps }) {
871
1047
  }
872
1048
  );
873
1049
  }
874
- async function runTui(deps) {
875
- const session = new TermSession(deps);
876
- if (deps.resume) await session.resume(deps.resume.id);
1050
+ function tuiPermission(session, deps) {
1051
+ const approvals = deps.approvals;
1052
+ const hooks = approvals ? approvalsHooks(approvals, {
1053
+ standing: true,
1054
+ context: () => {
1055
+ const id = session.getSnapshot().session?.id;
1056
+ return { host: "tui", surface: "chat", ...deps.approvalsContext ?? {}, ...id ? { sessionId: id } : {} };
1057
+ },
1058
+ onSuggestion: (s) => session.offerApprovalSuggestion(s)
1059
+ }) : null;
877
1060
  const permission = new PermissionService2({
878
1061
  policy: deps.policy ?? STANDARD_POLICY,
879
- prompt: { ask: (req) => session.askPermission(req) }
1062
+ prompt: { ask: (req) => session.askPermission(req) },
1063
+ ...hooks?.standing ? { standing: hooks.standing } : {},
1064
+ ...hooks ? { onDecision: hooks.onDecision } : {}
880
1065
  });
1066
+ return { permission, settled: hooks ? hooks.settled : async () => void 0 };
1067
+ }
1068
+ async function runTui(deps) {
1069
+ const approvals = deps.approvals;
1070
+ const session = new TermSession(approvals ? { ...deps, saveApprovalChoice: (s, c) => approvals.respond(s, c) } : deps);
1071
+ if (deps.resume) await session.resume(deps.resume.id);
1072
+ const { permission, settled } = tuiPermission(session, deps);
881
1073
  session.attachPermission(permission);
882
1074
  const bannerOpts = {
883
1075
  policy: deps.policy ?? STANDARD_POLICY,
@@ -906,9 +1098,12 @@ async function runTui(deps) {
906
1098
  exitOnCtrlC: false
907
1099
  });
908
1100
  await app.waitUntilExit();
1101
+ await settled();
1102
+ await approvals?.flush().catch(() => void 0);
909
1103
  return 0;
910
1104
  }
911
1105
  export {
912
1106
  Root,
913
- runTui
1107
+ runTui,
1108
+ tuiPermission
914
1109
  };
@@ -0,0 +1,27 @@
1
+ /** Rows the live region spends OUTSIDE the overlay box itself, every time an
2
+ * overlay is open: the modal Box's own `marginTop` (1), the input box's
3
+ * `marginTop` (1) plus its three border/content rows (3), and the one-line
4
+ * status row (1) -- 6 total. Live tool rows, streaming text, TodoBlock, the
5
+ * palette, the pending-skill indicator, and the empty-conversation hint are
6
+ * all suppressed while a modal is open (App.tsx), so none of them are part
7
+ * of this reservation; if a future change stops suppressing one of them,
8
+ * this constant must grow to match or the invariant this exists to keep
9
+ * (packages/term/src/ink/liveRegionHeight.test.tsx) goes red. */
10
+ export declare const RESERVED_CHROME_ROWS = 6;
11
+ /** No usable row count -- see App.tsx's `usableRows`: a stream that cannot
12
+ * report its height (not a TTY, piped output, a render test double) gets
13
+ * this, meaning "don't cap" rather than a guessed height. Callers pass it
14
+ * straight through to `capList`, whose `Number.MAX_SAFE_INTEGER` cap is a
15
+ * no-op (every real list is shorter than that). */
16
+ export declare function overlayBoxBudget(rows: number | undefined): number;
17
+ export interface CappedList<T> {
18
+ shown: T[];
19
+ /** Set only when `items` didn't fit -- the caller renders this as its own
20
+ * line, the same way TodoBlock renders `view.overflow`. */
21
+ overflow: string | null;
22
+ }
23
+ /** Cap `items` to `cap` rows TOTAL, overflow line included -- the exact rule
24
+ * `visibleTodos` (todos.ts) already applies to the todo list, generalized to
25
+ * any list an overlay renders one item per row. */
26
+ export declare function capList<T>(items: T[], cap: number, overflowLabel: (hiddenCount: number) => string): CappedList<T>;
27
+ //# sourceMappingURL=overlayBudget.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"overlayBudget.d.ts","sourceRoot":"","sources":["../../src/ink/overlayBudget.ts"],"names":[],"mappings":"AAgBA;;;;;;;;iEAQiE;AACjE,eAAO,MAAM,oBAAoB,IAAI,CAAC;AAEtC;;;;mDAImD;AACnD,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,CAGjE;AAED,MAAM,WAAW,UAAU,CAAC,CAAC;IAC3B,KAAK,EAAE,CAAC,EAAE,CAAC;IACX;+DAC2D;IAC3D,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;CACzB;AAED;;mDAEmD;AACnD,wBAAgB,OAAO,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,EAAE,GAAG,EAAE,MAAM,EAAE,aAAa,EAAE,CAAC,WAAW,EAAE,MAAM,KAAK,MAAM,GAAG,UAAU,CAAC,CAAC,CAAC,CAYjH"}