@corenel/term 0.1.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.
@@ -0,0 +1,573 @@
1
+ import {
2
+ STANDARD_POLICY,
3
+ TermSession,
4
+ matchesAnyGlob,
5
+ parseMarkdown
6
+ } from "../chunk-HYWLODZT.js";
7
+ import "../chunk-47IH6X5H.js";
8
+ import {
9
+ filterCommands
10
+ } from "../chunk-R6AXTD55.js";
11
+ import "../chunk-TCVDVYMF.js";
12
+ import "../chunk-HAQGHLW6.js";
13
+
14
+ // src/ink/index.tsx
15
+ import { useCallback, useState as useState2, useSyncExternalStore } from "react";
16
+ import { render, useApp, useInput } from "ink";
17
+
18
+ // ../harness/guardrail/permission-service.ts
19
+ var LEVEL_RANK = { allow: 0, ask: 1, deny: 2 };
20
+ function subjectMatches(tool, glob) {
21
+ if (tool.address && matchesAnyGlob(tool.address, [glob])) return true;
22
+ return matchesAnyGlob(tool.name, [glob]);
23
+ }
24
+ function specificity(glob) {
25
+ const literal = glob.replace(/\*/g, "").length;
26
+ return (glob.includes("*") ? 0 : 1e4) + literal;
27
+ }
28
+ function matchLevel(tool, rules) {
29
+ let best;
30
+ let bestScore = -1;
31
+ for (const [glob, level] of Object.entries(rules)) {
32
+ if (!subjectMatches(tool, glob)) continue;
33
+ const score = specificity(glob);
34
+ if (score > bestScore) {
35
+ bestScore = score;
36
+ best = level;
37
+ }
38
+ }
39
+ return best;
40
+ }
41
+ function mostRestrictive(a, b) {
42
+ return LEVEL_RANK[a] >= LEVEL_RANK[b] ? a : b;
43
+ }
44
+ var PermissionService = class {
45
+ policy;
46
+ prompt;
47
+ defaultForReadOnly;
48
+ defaultForMutating;
49
+ ceiling;
50
+ sessionAllow = /* @__PURE__ */ new Set();
51
+ constructor(opts) {
52
+ this.policy = opts.policy;
53
+ if (opts.prompt) this.prompt = opts.prompt;
54
+ this.defaultForReadOnly = opts.defaultForReadOnly ?? "allow";
55
+ this.defaultForMutating = opts.defaultForMutating ?? "ask";
56
+ if (opts.ceiling) this.ceiling = opts.ceiling;
57
+ }
58
+ /** Pre-approve a tool-name glob for the rest of the session (an "ask" becomes "allow"). */
59
+ preApprove(toolGlob) {
60
+ this.sessionAllow.add(toolGlob);
61
+ }
62
+ reset() {
63
+ this.sessionAllow.clear();
64
+ }
65
+ async check(tool, input) {
66
+ const level = this.resolveLevel(tool);
67
+ return this.decide(level, tool.name, input);
68
+ }
69
+ /**
70
+ * Resolve the effective level. Base precedence (highest first):
71
+ * policy.permissions glob > tool.permission > mutates heuristic > policy.defaultPermission.
72
+ * Then the ceiling clamps to the more restrictive of base and ceiling.
73
+ */
74
+ resolveLevel(tool) {
75
+ return mostRestrictive(this.resolveBaseLevel(tool), this.resolveCeilingLevel(tool));
76
+ }
77
+ resolveBaseLevel(tool) {
78
+ const matched = this.policy.permissions && matchLevel(tool, this.policy.permissions);
79
+ if (matched) return matched;
80
+ if (tool.mutates && this.policy.mutatingDefault) return this.policy.mutatingDefault;
81
+ if (tool.permission) return tool.permission;
82
+ if (this.policy.defaultPermission) return this.policy.defaultPermission;
83
+ if (tool.mutates) return this.defaultForMutating;
84
+ return this.defaultForReadOnly;
85
+ }
86
+ resolveCeilingLevel(tool) {
87
+ const c = this.ceiling;
88
+ if (!c) return "allow";
89
+ const matched = c.overrides && matchLevel(tool, c.overrides);
90
+ if (matched) return matched;
91
+ return c.defaultLevel ?? "allow";
92
+ }
93
+ async decide(level, name, input) {
94
+ if (level === "allow") return "allow";
95
+ if (level === "deny") return "deny";
96
+ if (this.sessionAllow.size && matchesAnyGlob(name, [...this.sessionAllow])) return "allow";
97
+ if (!this.prompt) return "deny";
98
+ return this.prompt.ask({ tool: name, input });
99
+ }
100
+ };
101
+
102
+ // src/ink/App.tsx
103
+ import { useEffect, useState } from "react";
104
+ import { homedir } from "node:os";
105
+ import { Box as Box4, Static, Text as Text4, useStdout } from "ink";
106
+ import TextInput from "ink-text-input";
107
+
108
+ // src/ink/items.tsx
109
+ import { Box, Text } from "ink";
110
+
111
+ // src/ink/theme.ts
112
+ var palette = {
113
+ ember: "#e2622a",
114
+ amber: "#f0b429",
115
+ ink: "#f6ece3",
116
+ dim: "#8d7d6f",
117
+ soft: "#c2b1a2",
118
+ good: "#63a375",
119
+ warn: "#d9a227",
120
+ bad: "#d9483b",
121
+ tool: "#c4b5fd"
122
+ };
123
+ var glyph = {
124
+ prompt: "\u203A",
125
+ bullet: "\xB7",
126
+ ok: "\u2713",
127
+ fail: "\u2717",
128
+ bar: "\u2502",
129
+ sep: "\xB7"
130
+ };
131
+ function tokens(n) {
132
+ return n >= 1e3 ? `${(n / 1e3).toFixed(1)}k` : `${n}`;
133
+ }
134
+
135
+ // src/ink/items.tsx
136
+ import { jsx, jsxs } from "react/jsx-runtime";
137
+ function CommittedRow({ item }) {
138
+ switch (item.kind) {
139
+ case "user":
140
+ return /* @__PURE__ */ jsxs(Box, { marginTop: 1, children: [
141
+ /* @__PURE__ */ jsxs(Text, { color: palette.ember, children: [
142
+ glyph.prompt,
143
+ " "
144
+ ] }),
145
+ /* @__PURE__ */ jsx(Text, { bold: true, children: item.text })
146
+ ] });
147
+ case "assistant":
148
+ return /* @__PURE__ */ jsx(Box, { flexDirection: "column", marginTop: 1, children: parseMarkdown(item.text).map((b, i) => /* @__PURE__ */ jsx(MdLine, { block: b }, i)) });
149
+ case "tool":
150
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
151
+ /* @__PURE__ */ jsxs(Box, { children: [
152
+ /* @__PURE__ */ jsxs(Text, { color: palette.dim, children: [
153
+ glyph.bullet,
154
+ " "
155
+ ] }),
156
+ /* @__PURE__ */ jsx(Text, { color: palette.tool, children: item.name.padEnd(11) }),
157
+ /* @__PURE__ */ jsx(Text, { color: palette.ink, children: item.subject }),
158
+ /* @__PURE__ */ jsxs(Text, { color: item.isError ? palette.bad : palette.good, children: [
159
+ " ",
160
+ item.isError ? glyph.fail : glyph.ok
161
+ ] }),
162
+ /* @__PURE__ */ jsxs(Text, { color: palette.dim, children: [
163
+ item.durationMs !== void 0 ? ` ${(item.durationMs / 1e3).toFixed(1)}s` : "",
164
+ item.offloadedBytes !== void 0 ? ` offloaded ${Math.round(item.offloadedBytes / 1024)} KB` : "",
165
+ " ",
166
+ "#",
167
+ item.ord
168
+ ] })
169
+ ] }),
170
+ item.preview.map((line, i) => /* @__PURE__ */ jsxs(Box, { children: [
171
+ /* @__PURE__ */ jsxs(Text, { color: palette.dim, children: [
172
+ " ",
173
+ glyph.bar,
174
+ " "
175
+ ] }),
176
+ /* @__PURE__ */ jsx(Text, { color: item.isError ? palette.bad : palette.soft, children: line })
177
+ ] }, i))
178
+ ] });
179
+ case "note":
180
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginTop: item.level === "info" ? 1 : 0, children: [
181
+ /* @__PURE__ */ jsx(Box, { children: /* @__PURE__ */ jsxs(Text, { color: item.level === "error" ? palette.bad : item.level === "warn" ? palette.warn : palette.dim, children: [
182
+ item.level === "info" ? " " : " ! ",
183
+ item.text
184
+ ] }) }),
185
+ item.detail ? /* @__PURE__ */ jsxs(Text, { color: palette.dim, children: [
186
+ " ",
187
+ item.detail
188
+ ] }) : null
189
+ ] });
190
+ }
191
+ }
192
+ function MdLine({ block }) {
193
+ switch (block.kind) {
194
+ case "h":
195
+ return /* @__PURE__ */ jsx(Text, { color: palette.ember, bold: true, children: block.text });
196
+ case "li":
197
+ return /* @__PURE__ */ jsxs(Box, { children: [
198
+ /* @__PURE__ */ jsx(Text, { color: palette.tool, children: " \u2022 " }),
199
+ /* @__PURE__ */ jsx(Text, { color: palette.soft, children: block.text })
200
+ ] });
201
+ case "code":
202
+ return /* @__PURE__ */ jsx(Box, { flexDirection: "column", marginY: 1, children: block.text.split("\n").map((l, i) => /* @__PURE__ */ jsxs(Box, { children: [
203
+ /* @__PURE__ */ jsxs(Text, { color: palette.dim, children: [
204
+ " ",
205
+ glyph.bar,
206
+ " "
207
+ ] }),
208
+ /* @__PURE__ */ jsx(Text, { color: palette.amber, children: l })
209
+ ] }, i)) });
210
+ case "blank":
211
+ return /* @__PURE__ */ jsx(Text, { children: " " });
212
+ default:
213
+ return /* @__PURE__ */ jsx(Text, { color: palette.soft, children: block.text });
214
+ }
215
+ }
216
+ function LiveToolRow({ tool }) {
217
+ return /* @__PURE__ */ jsxs(Box, { children: [
218
+ /* @__PURE__ */ jsxs(Text, { color: palette.amber, children: [
219
+ glyph.bullet,
220
+ " "
221
+ ] }),
222
+ /* @__PURE__ */ jsx(Text, { color: palette.tool, children: tool.name.padEnd(11) }),
223
+ /* @__PURE__ */ jsx(Text, { color: palette.ink, children: tool.subject }),
224
+ /* @__PURE__ */ jsxs(Text, { color: palette.dim, children: [
225
+ " ",
226
+ "running\u2026 #",
227
+ tool.ord
228
+ ] })
229
+ ] });
230
+ }
231
+
232
+ // src/ink/Modal.tsx
233
+ import { Box as Box2, Text as Text2 } from "ink";
234
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
235
+ function ModalView({ modal }) {
236
+ if (modal.kind === "permission") {
237
+ return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", borderStyle: "round", borderColor: palette.warn, paddingX: 1, children: [
238
+ /* @__PURE__ */ jsxs2(Text2, { color: palette.warn, children: [
239
+ modal.tool,
240
+ " wants to run"
241
+ ] }),
242
+ /* @__PURE__ */ jsx2(Box2, { marginTop: 1, children: /* @__PURE__ */ jsx2(Text2, { bold: true, children: modal.detail }) }),
243
+ /* @__PURE__ */ jsxs2(Box2, { marginTop: 1, children: [
244
+ /* @__PURE__ */ jsx2(Text2, { color: palette.good, children: "[y]" }),
245
+ /* @__PURE__ */ jsx2(Text2, { color: palette.dim, children: " run it " }),
246
+ /* @__PURE__ */ jsx2(Text2, { color: palette.bad, children: "[n]" }),
247
+ /* @__PURE__ */ jsx2(Text2, { color: palette.dim, children: " deny esc denies" })
248
+ ] })
249
+ ] });
250
+ }
251
+ const q = modal.questions[0];
252
+ return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", borderStyle: "round", borderColor: palette.ember, paddingX: 1, children: [
253
+ /* @__PURE__ */ jsx2(Text2, { color: palette.dim, children: q?.header }),
254
+ /* @__PURE__ */ jsx2(Text2, { bold: true, children: q?.question }),
255
+ /* @__PURE__ */ jsx2(Box2, { flexDirection: "column", marginTop: 1, children: (q?.options ?? []).map((o, i) => /* @__PURE__ */ jsxs2(Box2, { children: [
256
+ /* @__PURE__ */ jsxs2(Text2, { color: palette.amber, children: [
257
+ i + 1,
258
+ " "
259
+ ] }),
260
+ /* @__PURE__ */ jsx2(Text2, { children: o.label }),
261
+ o.description ? /* @__PURE__ */ jsxs2(Text2, { color: palette.dim, children: [
262
+ " ",
263
+ o.description
264
+ ] }) : null
265
+ ] }, o.label)) }),
266
+ /* @__PURE__ */ jsx2(Box2, { marginTop: 1, children: /* @__PURE__ */ jsxs2(Text2, { color: palette.dim, children: [
267
+ "1-",
268
+ (q?.options ?? []).length,
269
+ " choose \xB7 \u21B5 confirm \xB7 or type your own answer \xB7 esc dismisses"
270
+ ] }) })
271
+ ] });
272
+ }
273
+
274
+ // src/ink/Palette.tsx
275
+ import { Box as Box3, Text as Text3 } from "ink";
276
+ import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
277
+ function PaletteView({ items, selected }) {
278
+ return /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", children: [
279
+ items.map((c, i) => /* @__PURE__ */ jsxs3(Box3, { children: [
280
+ /* @__PURE__ */ jsxs3(Text3, { color: i === selected ? palette.ember : palette.dim, children: [
281
+ i === selected ? "\u203A " : " ",
282
+ "/",
283
+ c.name,
284
+ c.args ? ` ${c.args}` : ""
285
+ ] }),
286
+ /* @__PURE__ */ jsxs3(Text3, { color: palette.dim, children: [
287
+ " ",
288
+ c.summary
289
+ ] })
290
+ ] }, c.name)),
291
+ /* @__PURE__ */ jsx3(Text3, { color: palette.dim, children: " \u2191\u2193 choose \xB7 tab completes \xB7 esc closes" })
292
+ ] });
293
+ }
294
+
295
+ // src/ink/status.ts
296
+ var DEFAULT_COLUMNS = 80;
297
+ var MIN_GAP = 2;
298
+ var SEP = " \xB7 ";
299
+ var ELLIPSIS = "\u2026";
300
+ function shortenHome(cwd, home) {
301
+ if (!home) return cwd;
302
+ if (cwd === home) return "~";
303
+ for (const slash of ["\\", "/"]) {
304
+ const prefix = home.endsWith(slash) ? home : `${home}${slash}`;
305
+ if (cwd.startsWith(prefix)) return `~${slash}${cwd.slice(prefix.length)}`;
306
+ }
307
+ return cwd;
308
+ }
309
+ function shortenTrailing(path, budget) {
310
+ if (path.length <= budget) return path;
311
+ if (budget <= ELLIPSIS.length) return "";
312
+ const parts = path.split(/[\\/]/).filter((p) => p.length > 0);
313
+ let kept = "";
314
+ for (let i = parts.length - 1; i >= 0; i--) {
315
+ const candidate = `${ELLIPSIS}${parts.slice(i).join("/")}`;
316
+ if (candidate.length > budget) break;
317
+ kept = candidate;
318
+ }
319
+ return kept;
320
+ }
321
+ function fitStatus(left, identity, columns) {
322
+ const cols = columns !== void 0 && columns > 0 ? columns : DEFAULT_COLUMNS;
323
+ const budget = cols - left.length - MIN_GAP;
324
+ if (budget <= 0) return { left, right: "" };
325
+ const cwd = shortenHome(identity.cwd, identity.home);
326
+ const full = `${identity.model}${SEP}${cwd}`;
327
+ if (full.length <= budget) return { left, right: full };
328
+ const cwdBudget = budget - identity.model.length - SEP.length;
329
+ if (cwdBudget > 0) {
330
+ const shrunkCwd = shortenTrailing(cwd, cwdBudget);
331
+ if (shrunkCwd) return { left, right: `${identity.model}${SEP}${shrunkCwd}` };
332
+ }
333
+ if (identity.model.length <= budget) return { left, right: identity.model };
334
+ return { left, right: "" };
335
+ }
336
+
337
+ // src/ink/App.tsx
338
+ import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
339
+ function useColumns() {
340
+ const { stdout } = useStdout();
341
+ const [columns, setColumns] = useState(stdout?.columns ?? DEFAULT_COLUMNS);
342
+ useEffect(() => {
343
+ if (!stdout) return;
344
+ const onResize = () => setColumns(stdout.columns ?? DEFAULT_COLUMNS);
345
+ onResize();
346
+ stdout.on("resize", onResize);
347
+ return () => {
348
+ stdout.off("resize", onResize);
349
+ };
350
+ }, [stdout]);
351
+ return columns;
352
+ }
353
+ function App(props) {
354
+ const { state } = props;
355
+ const paletteItems = props.value.startsWith("/") ? filterCommands(props.value) : [];
356
+ const paletteSelected = Math.min(props.paletteSelected, Math.max(paletteItems.length - 1, 0));
357
+ const busy = state.status !== "idle";
358
+ const columns = useColumns();
359
+ const statusWord = busy ? "running" : "idle";
360
+ const roundSeg = state.live ? ` ${glyph.sep} turn ${state.live.turn}` : ` ${glyph.sep} ${props.toolCount} tools`;
361
+ const turnTokens = tokens(state.turnUsage.promptTokens + state.turnUsage.completionTokens);
362
+ const totalTokens = tokens(state.usage.promptTokens + state.usage.completionTokens);
363
+ const tokenSeg = ` ${glyph.sep} ${turnTokens} turn ${glyph.sep} ${totalTokens} total`;
364
+ const escSeg = busy ? ` ${glyph.sep} esc stops` : "";
365
+ const queuedSeg = state.queued.length ? ` ${glyph.sep} ${state.queued.length} queued` : "";
366
+ const shellSeg = !busy ? ` ${glyph.sep} shell ${props.approveAll ? "auto-approved" : "asks first"}` : "";
367
+ const statusLeft = ` ${statusWord}${roundSeg}${tokenSeg}${escSeg}${queuedSeg}${shellSeg}`;
368
+ const { right: statusRight } = fitStatus(statusLeft, { model: props.model, cwd: props.cwd, home: homedir() }, columns);
369
+ return /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", children: [
370
+ /* @__PURE__ */ jsx4(Static, { items: state.committed, children: (item) => /* @__PURE__ */ jsx4(CommittedRow, { item }, item.id) }, `transcript-${state.transcriptGen}`),
371
+ state.committed.length === 0 ? /* @__PURE__ */ jsx4(Box4, { marginBottom: 1, children: /* @__PURE__ */ jsxs4(Text4, { color: palette.soft, children: [
372
+ "Type a request. ",
373
+ /* @__PURE__ */ jsxs4(Text4, { color: palette.dim, children: [
374
+ "/help for commands ",
375
+ glyph.sep,
376
+ " Ctrl-C stops a turn ",
377
+ glyph.sep,
378
+ " Ctrl-D leaves"
379
+ ] })
380
+ ] }) }) : null,
381
+ state.live?.tools.map((t) => /* @__PURE__ */ jsx4(LiveToolRow, { tool: t }, t.callId)),
382
+ state.live?.streaming ? /* @__PURE__ */ jsx4(Box4, { marginTop: 1, children: /* @__PURE__ */ jsx4(Text4, { color: palette.soft, children: state.live.streaming }) }) : null,
383
+ state.modal ? /* @__PURE__ */ jsx4(Box4, { marginTop: 1, children: /* @__PURE__ */ jsx4(ModalView, { modal: state.modal }) }) : null,
384
+ paletteItems.length && !state.modal ? /* @__PURE__ */ jsx4(Box4, { marginTop: 1, children: /* @__PURE__ */ jsx4(PaletteView, { items: paletteItems, selected: paletteSelected }) }) : null,
385
+ /* @__PURE__ */ jsxs4(Box4, { borderStyle: "round", borderColor: palette.dim, paddingX: 1, marginTop: 1, children: [
386
+ /* @__PURE__ */ jsxs4(Text4, { color: palette.ember, children: [
387
+ glyph.prompt,
388
+ " "
389
+ ] }),
390
+ /* @__PURE__ */ jsx4(
391
+ TextInput,
392
+ {
393
+ value: props.value,
394
+ onChange: props.onChange,
395
+ onSubmit: state.modal ? props.onAnswer : props.onSubmit,
396
+ placeholder: "",
397
+ focus: props.focus
398
+ }
399
+ )
400
+ ] }),
401
+ /* @__PURE__ */ jsxs4(Box4, { justifyContent: "space-between", children: [
402
+ /* @__PURE__ */ jsxs4(Box4, { children: [
403
+ /* @__PURE__ */ jsxs4(Text4, { color: busy ? palette.amber : palette.dim, children: [
404
+ " ",
405
+ statusWord
406
+ ] }),
407
+ /* @__PURE__ */ jsxs4(Text4, { color: palette.dim, children: [
408
+ roundSeg,
409
+ tokenSeg,
410
+ escSeg,
411
+ queuedSeg
412
+ ] }),
413
+ !busy ? /* @__PURE__ */ jsx4(Text4, { color: props.approveAll ? palette.ember : palette.warn, children: shellSeg }) : null
414
+ ] }),
415
+ statusRight ? /* @__PURE__ */ jsx4(Text4, { color: palette.dim, children: statusRight }) : null
416
+ ] })
417
+ ] });
418
+ }
419
+
420
+ // src/ink/scroll.ts
421
+ function primeScroll(out, opts) {
422
+ if (out.isTTY !== true) return 0;
423
+ if (opts.resuming) return 0;
424
+ const rows = out.rows;
425
+ if (typeof rows !== "number" || !Number.isFinite(rows) || rows <= 0) return 0;
426
+ out.write("\n".repeat(rows));
427
+ return rows;
428
+ }
429
+
430
+ // src/ink/index.tsx
431
+ import { jsx as jsx5 } from "react/jsx-runtime";
432
+ function Root({ session, deps }) {
433
+ const state = useSyncExternalStore(
434
+ useCallback((fn) => session.subscribe(fn), [session]),
435
+ useCallback(() => session.getSnapshot(), [session])
436
+ );
437
+ const [value, setValue] = useState2("");
438
+ const { exit } = useApp();
439
+ const [sawSigint, setSawSigint] = useState2(false);
440
+ const [paletteIndex, setPaletteIndex] = useState2(0);
441
+ const handleChange = (v) => {
442
+ const cleaned = v.replace(/\x1b/g, "");
443
+ if (state.modal?.kind === "user" && value === "" && cleaned.length === 1) {
444
+ const q = state.modal.questions[0];
445
+ const n = Number(cleaned);
446
+ if (q && Number.isInteger(n) && n >= 1 && n <= q.options.length) {
447
+ const answers = { [q.header]: [q.options[n - 1].label] };
448
+ session.answer(answers);
449
+ return;
450
+ }
451
+ }
452
+ setValue(cleaned);
453
+ setPaletteIndex(0);
454
+ };
455
+ useInput((input, key) => {
456
+ if (state.modal) {
457
+ if (key.escape) {
458
+ session.answer(state.modal.kind === "permission" ? "deny" : null);
459
+ return;
460
+ }
461
+ if (state.modal.kind === "permission") {
462
+ if (input.toLowerCase() === "y") {
463
+ session.answer("allow");
464
+ return;
465
+ }
466
+ if (input.toLowerCase() === "n") {
467
+ session.answer("deny");
468
+ return;
469
+ }
470
+ }
471
+ return;
472
+ }
473
+ const paletteItems = value.startsWith("/") ? filterCommands(value) : [];
474
+ if (paletteItems.length > 0) {
475
+ if (key.escape) {
476
+ setValue("");
477
+ setPaletteIndex(0);
478
+ return;
479
+ }
480
+ if (key.upArrow) {
481
+ setPaletteIndex((i) => (i - 1 + paletteItems.length) % paletteItems.length);
482
+ return;
483
+ }
484
+ if (key.downArrow) {
485
+ setPaletteIndex((i) => (i + 1) % paletteItems.length);
486
+ return;
487
+ }
488
+ if (key.tab) {
489
+ const chosen = paletteItems[Math.min(paletteIndex, paletteItems.length - 1)];
490
+ if (chosen) setValue(`/${chosen.name} `);
491
+ setPaletteIndex(0);
492
+ return;
493
+ }
494
+ }
495
+ if (key.escape && state.status !== "idle") {
496
+ session.abort();
497
+ return;
498
+ }
499
+ if (key.ctrl && input === "c") {
500
+ if (state.status !== "idle" && !sawSigint) {
501
+ session.abort();
502
+ setSawSigint(true);
503
+ return;
504
+ }
505
+ exit();
506
+ return;
507
+ }
508
+ if (key.ctrl && input === "d") {
509
+ if (state.status === "idle" && value === "") exit();
510
+ return;
511
+ }
512
+ if (sawSigint) setSawSigint(false);
513
+ });
514
+ return /* @__PURE__ */ jsx5(
515
+ App,
516
+ {
517
+ state,
518
+ model: deps.model,
519
+ cwd: deps.cwd,
520
+ version: deps.version,
521
+ toolCount: deps.tools.length,
522
+ approveAll: deps.approveAll === true,
523
+ value,
524
+ onChange: handleChange,
525
+ onSubmit: (v) => {
526
+ setValue("");
527
+ void session.submit(v).then(() => {
528
+ if (session.wantsExit) exit();
529
+ });
530
+ },
531
+ onAnswer: (v) => {
532
+ setValue("");
533
+ const modal = session.getSnapshot().modal;
534
+ if (!modal) return;
535
+ if (modal.kind === "permission") session.answer(/^y(es)?$/i.test(v.trim()) ? "allow" : "deny");
536
+ else session.answer(v.trim() ? { [modal.questions[0].header]: [v.trim()] } : null);
537
+ },
538
+ focus: state.modal?.kind !== "permission",
539
+ paletteSelected: paletteIndex
540
+ }
541
+ );
542
+ }
543
+ async function runTui(deps) {
544
+ const session = new TermSession(deps);
545
+ if (deps.resume) await session.resume(deps.resume.id);
546
+ const permission = new PermissionService({
547
+ policy: STANDARD_POLICY,
548
+ prompt: { ask: (req) => session.askPermission(req) }
549
+ });
550
+ session.attachPermission(permission);
551
+ primeScroll(process.stdout, { resuming: deps.resume != null });
552
+ const app = render(/* @__PURE__ */ jsx5(Root, { session, deps }), {
553
+ /* LOAD-BEARING — do not "clean up" or default this away. Ink's own App
554
+ * component (build/components/App.js, handleInput) intercepts the raw
555
+ * \x03 byte and calls handleExit() BEFORE it ever emits the 'input'
556
+ * event that `useInput` listens on, whenever this flag is left at its
557
+ * default of `true`. With the default, our press-once-to-abort /
558
+ * press-twice-to-exit handler in `Root` above could never run at all —
559
+ * Ink would kill the process on the very first Ctrl-C, mid-turn,
560
+ * silently. This exact regression cannot be caught by
561
+ * `interaction.test.tsx`: `ink-testing-library`'s own `render()`
562
+ * hardcodes `exitOnCtrlC: false` for every tree it mounts, so no test
563
+ * run through that harness can tell this line apart from its absence.
564
+ * This comment is the only guard that's left. */
565
+ exitOnCtrlC: false
566
+ });
567
+ await app.waitUntilExit();
568
+ return 0;
569
+ }
570
+ export {
571
+ Root,
572
+ runTui
573
+ };
@@ -0,0 +1,12 @@
1
+ import React from 'react';
2
+ import type { CommittedItem, LiveTool } from '../state';
3
+ /** One finished item. Rendered ONCE inside <Static> — never re-rendered, or the
4
+ * block duplicates in the user's scrollback. */
5
+ export declare function CommittedRow({ item }: {
6
+ item: CommittedItem;
7
+ }): React.ReactElement;
8
+ /** A call that has been announced and has not come back yet. */
9
+ export declare function LiveToolRow({ tool }: {
10
+ tool: LiveTool;
11
+ }): React.ReactElement;
12
+ //# sourceMappingURL=items.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"items.d.ts","sourceRoot":"","sources":["../../src/ink/items.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;AAE1B,OAAO,KAAK,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,UAAU,CAAC;AAIxD;gDACgD;AAChD,wBAAgB,YAAY,CAAC,EAAE,IAAI,EAAE,EAAE;IAAE,IAAI,EAAE,aAAa,CAAA;CAAE,GAAG,KAAK,CAAC,YAAY,CAmDlF;AAiCD,gEAAgE;AAChE,wBAAgB,WAAW,CAAC,EAAE,IAAI,EAAE,EAAE;IAAE,IAAI,EAAE,QAAQ,CAAA;CAAE,GAAG,KAAK,CAAC,YAAY,CAS5E"}
@@ -0,0 +1,17 @@
1
+ export interface ScrollableOut {
2
+ readonly isTTY?: boolean;
3
+ readonly rows?: number;
4
+ write(chunk: string): void;
5
+ }
6
+ export interface PrimeScrollOptions {
7
+ /** True when `runTui` is reopening a saved conversation. Restored content
8
+ * usually already fills the screen; priming would push it into scrollback
9
+ * before the user ever sees it. */
10
+ resuming: boolean;
11
+ }
12
+ /** Writes `out.rows` newlines to `out` and returns how many were written (0
13
+ * when priming is skipped). Skips whenever `out.isTTY` is not `true`,
14
+ * `out.rows` is not a positive number, or `opts.resuming` is true. Must be
15
+ * called before the first Ink `render()`. */
16
+ export declare function primeScroll(out: ScrollableOut, opts: PrimeScrollOptions): number;
17
+ //# sourceMappingURL=scroll.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scroll.d.ts","sourceRoot":"","sources":["../../src/ink/scroll.ts"],"names":[],"mappings":"AAmCA,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,CAAC;IACzB,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,KAAK,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;CAC5B;AAED,MAAM,WAAW,kBAAkB;IACjC;;uCAEmC;IACnC,QAAQ,EAAE,OAAO,CAAC;CACnB;AAED;;;6CAG6C;AAC7C,wBAAgB,WAAW,CAAC,GAAG,EAAE,aAAa,EAAE,IAAI,EAAE,kBAAkB,GAAG,MAAM,CAOhF"}
@@ -0,0 +1,21 @@
1
+ export interface StatusIdentity {
2
+ model: string;
3
+ /** Absolute working directory, as-is (not yet home-shortened). */
4
+ cwd: string;
5
+ /** The user's home directory, if known, for the `~` substitution. */
6
+ home?: string;
7
+ }
8
+ export interface StatusLine {
9
+ /** Always exactly what was passed in -- the left half is never truncated. */
10
+ left: string;
11
+ /** The identity segment, or '' when there is no room for any of it. */
12
+ right: string;
13
+ }
14
+ /** Terminals that don't report `columns` (some pipes, some Windows hosts) get
15
+ * this instead of a crash or a blank render. */
16
+ export declare const DEFAULT_COLUMNS = 80;
17
+ /** Decide what the status row's right (identity) half should render, given
18
+ * the already-finalized left half and the terminal's current width. Never
19
+ * touches `left`; never returns a total wider than `columns`. */
20
+ export declare function fitStatus(left: string, identity: StatusIdentity, columns: number | undefined): StatusLine;
21
+ //# sourceMappingURL=status.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"status.d.ts","sourceRoot":"","sources":["../../src/ink/status.ts"],"names":[],"mappings":"AAcA,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,MAAM,CAAC;IACd,kEAAkE;IAClE,GAAG,EAAE,MAAM,CAAC;IACZ,qEAAqE;IACrE,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,UAAU;IACzB,6EAA6E;IAC7E,IAAI,EAAE,MAAM,CAAC;IACb,uEAAuE;IACvE,KAAK,EAAE,MAAM,CAAC;CACf;AAED;gDACgD;AAChD,eAAO,MAAM,eAAe,KAAK,CAAC;AAiClC;;iEAEiE;AACjE,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,cAAc,EAAE,OAAO,EAAE,MAAM,GAAG,SAAS,GAAG,UAAU,CAkBzG"}
@@ -0,0 +1,22 @@
1
+ export declare const palette: {
2
+ readonly ember: "#e2622a";
3
+ readonly amber: "#f0b429";
4
+ readonly ink: "#f6ece3";
5
+ readonly dim: "#8d7d6f";
6
+ readonly soft: "#c2b1a2";
7
+ readonly good: "#63a375";
8
+ readonly warn: "#d9a227";
9
+ readonly bad: "#d9483b";
10
+ readonly tool: "#c4b5fd";
11
+ };
12
+ export declare const glyph: {
13
+ readonly prompt: "›";
14
+ readonly bullet: "·";
15
+ readonly ok: "✓";
16
+ readonly fail: "✗";
17
+ readonly bar: "│";
18
+ readonly sep: "·";
19
+ };
20
+ /** Tokens, abbreviated the way a status line has room for. */
21
+ export declare function tokens(n: number): string;
22
+ //# sourceMappingURL=theme.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"theme.d.ts","sourceRoot":"","sources":["../../src/ink/theme.ts"],"names":[],"mappings":"AAKA,eAAO,MAAM,OAAO;;;;;;;;;;CAUV,CAAC;AAEX,eAAO,MAAM,KAAK;;;;;;;CAOR,CAAC;AAEX,8DAA8D;AAC9D,wBAAgB,MAAM,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAExC"}