@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.
- package/README.md +44 -0
- package/dist/chunk-47IH6X5H.js +20 -0
- package/dist/chunk-HAQGHLW6.js +235 -0
- package/dist/chunk-HYWLODZT.js +1230 -0
- package/dist/chunk-R6AXTD55.js +47 -0
- package/dist/chunk-TCVDVYMF.js +40 -0
- package/dist/commands.d.ts +25 -0
- package/dist/commands.d.ts.map +1 -0
- package/dist/commands.js +10 -0
- package/dist/describe.d.ts +25 -0
- package/dist/describe.d.ts.map +1 -0
- package/dist/describe.js +14 -0
- package/dist/ink/App.d.ts +30 -0
- package/dist/ink/App.d.ts.map +1 -0
- package/dist/ink/Modal.d.ts +6 -0
- package/dist/ink/Modal.d.ts.map +1 -0
- package/dist/ink/Palette.d.ts +7 -0
- package/dist/ink/Palette.d.ts.map +1 -0
- package/dist/ink/index.d.ts +18 -0
- package/dist/ink/index.d.ts.map +1 -0
- package/dist/ink/index.js +573 -0
- package/dist/ink/items.d.ts +12 -0
- package/dist/ink/items.d.ts.map +1 -0
- package/dist/ink/scroll.d.ts +17 -0
- package/dist/ink/scroll.d.ts.map +1 -0
- package/dist/ink/status.d.ts +21 -0
- package/dist/ink/status.d.ts.map +1 -0
- package/dist/ink/theme.d.ts +22 -0
- package/dist/ink/theme.d.ts.map +1 -0
- package/dist/markdown.d.ts +33 -0
- package/dist/markdown.d.ts.map +1 -0
- package/dist/persist.d.ts +30 -0
- package/dist/persist.d.ts.map +1 -0
- package/dist/persist.js +10 -0
- package/dist/session.d.ts +153 -0
- package/dist/session.d.ts.map +1 -0
- package/dist/session.js +12 -0
- package/dist/state.d.ts +75 -0
- package/dist/state.d.ts.map +1 -0
- package/dist/state.js +6 -0
- package/package.json +63 -0
|
@@ -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"}
|