@corenel/term 0.1.0 → 0.2.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.
Files changed (55) hide show
  1. package/dist/chunk-2D7G4YW2.js +97 -0
  2. package/dist/chunk-CB5RFNB4.js +33 -0
  3. package/dist/chunk-F4ENQXQQ.js +909 -0
  4. package/dist/chunk-I44FC7PE.js +142 -0
  5. package/dist/chunk-IK4ZANNW.js +95 -0
  6. package/dist/chunk-M44CET5L.js +40 -0
  7. package/dist/chunk-OSJFMQCW.js +44 -0
  8. package/dist/{chunk-R6AXTD55.js → chunk-VTJEWHM3.js} +19 -1
  9. package/dist/commands.d.ts +14 -0
  10. package/dist/commands.d.ts.map +1 -1
  11. package/dist/commands.js +1 -1
  12. package/dist/describe.d.ts +23 -0
  13. package/dist/describe.d.ts.map +1 -1
  14. package/dist/describe.js +6 -1
  15. package/dist/diff.d.ts +100 -0
  16. package/dist/diff.d.ts.map +1 -0
  17. package/dist/diff.js +18 -0
  18. package/dist/ink/App.d.ts +12 -0
  19. package/dist/ink/App.d.ts.map +1 -1
  20. package/dist/ink/Modal.d.ts.map +1 -1
  21. package/dist/ink/TodoBlock.d.ts +10 -0
  22. package/dist/ink/TodoBlock.d.ts.map +1 -0
  23. package/dist/ink/banner.d.ts +36 -0
  24. package/dist/ink/banner.d.ts.map +1 -0
  25. package/dist/ink/index.d.ts.map +1 -1
  26. package/dist/ink/index.js +300 -140
  27. package/dist/ink/items.d.ts.map +1 -1
  28. package/dist/ink/scroll.d.ts +14 -2
  29. package/dist/ink/scroll.d.ts.map +1 -1
  30. package/dist/ink/status.d.ts.map +1 -1
  31. package/dist/ink/theme.d.ts +9 -0
  32. package/dist/ink/theme.d.ts.map +1 -1
  33. package/dist/ink/todos.d.ts +20 -0
  34. package/dist/ink/todos.d.ts.map +1 -0
  35. package/dist/markdown.d.ts.map +1 -1
  36. package/dist/persist.d.ts +27 -4
  37. package/dist/persist.d.ts.map +1 -1
  38. package/dist/persist.js +3 -1
  39. package/dist/policySource.d.ts +26 -0
  40. package/dist/policySource.d.ts.map +1 -0
  41. package/dist/policySource.js +8 -0
  42. package/dist/session.d.ts +199 -13
  43. package/dist/session.d.ts.map +1 -1
  44. package/dist/session.js +10 -5
  45. package/dist/state.d.ts +84 -0
  46. package/dist/state.d.ts.map +1 -1
  47. package/dist/state.js +5 -3
  48. package/dist/writePreview.d.ts +58 -0
  49. package/dist/writePreview.d.ts.map +1 -0
  50. package/dist/writePreview.js +9 -0
  51. package/package.json +19 -6
  52. package/dist/chunk-47IH6X5H.js +0 -20
  53. package/dist/chunk-HAQGHLW6.js +0 -235
  54. package/dist/chunk-HYWLODZT.js +0 -1230
  55. package/dist/chunk-TCVDVYMF.js +0 -40
@@ -0,0 +1,36 @@
1
+ import { type NamespacedTool } from '@corenel/harness/tools/namespaces';
2
+ import type { Policy } from '@corenel/protocol';
3
+ import { type PolicySource } from '../policySource';
4
+ /** The posture phrase, derived from the same `resolveLevel()` the loop's own
5
+ * `PermissionService` calls before every tool check. Covers the four shapes
6
+ * the brief names, plus one more (shell allowed, writes denied outright) it
7
+ * asked to have added rather than left wrong. */
8
+ export declare function posture(policy: Policy): string;
9
+ export interface BannerOptions {
10
+ policy: Policy;
11
+ source: PolicySource;
12
+ tools: readonly NamespacedTool[];
13
+ cwd: string;
14
+ /** Terminal width. Missing/non-positive falls back to `DEFAULT_COLUMNS`. */
15
+ columns?: number;
16
+ /** Suppression gate (brief rule 5): nothing prints when this isn't `true`. */
17
+ isTTY: boolean;
18
+ }
19
+ /** Pure builder. `''` means "print nothing" -- the caller (`writeBanner`)
20
+ * writes only when this is non-empty. */
21
+ export declare function buildBanner(opts: BannerOptions): string;
22
+ export interface BannerOut {
23
+ readonly isTTY?: boolean;
24
+ readonly columns?: number;
25
+ write(chunk: string): void;
26
+ }
27
+ /** How many terminal lines `writeBanner` would emit for these options -- 0 when
28
+ * it would print nothing. `runTui` needs this BEFORE it primes the scroll, so
29
+ * the prime can reserve room for the banner instead of scrolling it away; see
30
+ * `primeScroll`'s `reserve` option. */
31
+ export declare function bannerHeight(out: BannerOut, opts: Omit<BannerOptions, 'isTTY' | 'columns'>): number;
32
+ /** The only thing here that touches a real stream -- called once, straight to
33
+ * stdout, AFTER `primeScroll` has reserved room for it (see `runTui` in
34
+ * `ink/index.tsx`, and `bannerHeight` above). */
35
+ export declare function writeBanner(out: BannerOut, opts: Omit<BannerOptions, 'isTTY' | 'columns'>): void;
36
+ //# sourceMappingURL=banner.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"banner.d.ts","sourceRoot":"","sources":["../../src/ink/banner.ts"],"names":[],"mappings":"AAcA,OAAO,EAAsC,KAAK,cAAc,EAAE,MAAM,mCAAmC,CAAC;AAC5G,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,EAAe,KAAK,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAmCjE;;;iDAGiD;AACjD,wBAAgB,OAAO,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAU9C;AAwBD,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,YAAY,CAAC;IACrB,KAAK,EAAE,SAAS,cAAc,EAAE,CAAC;IACjC,GAAG,EAAE,MAAM,CAAC;IACZ,4EAA4E;IAC5E,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,8EAA8E;IAC9E,KAAK,EAAE,OAAO,CAAC;CAChB;AAED;yCACyC;AACzC,wBAAgB,WAAW,CAAC,IAAI,EAAE,aAAa,GAAG,MAAM,CAuCvD;AAED,MAAM,WAAW,SAAS;IACxB,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,CAAC;IACzB,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B,KAAK,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;CAC5B;AAED;;;uCAGuC;AACvC,wBAAgB,YAAY,CAAC,GAAG,EAAE,SAAS,EAAE,IAAI,EAAE,IAAI,CAAC,aAAa,EAAE,OAAO,GAAG,SAAS,CAAC,GAAG,MAAM,CAInG;AAED;;iDAEiD;AACjD,wBAAgB,WAAW,CAAC,GAAG,EAAE,SAAS,EAAE,IAAI,EAAE,IAAI,CAAC,aAAa,EAAE,OAAO,GAAG,SAAS,CAAC,GAAG,IAAI,CAGhG"}
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/ink/index.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAsD,MAAM,OAAO,CAAC;AAK3E,OAAO,EAAE,WAAW,EAAE,KAAK,eAAe,EAAE,MAAM,YAAY,CAAC;AAK/D,MAAM,WAAW,OAAQ,SAAQ,eAAe;IAC9C,OAAO,EAAE,MAAM,CAAC;IAChB,qDAAqD;IACrD,MAAM,CAAC,EAAE;QAAE,EAAE,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;CAC1B;AAED;;wEAEwE;AACxE,wBAAgB,IAAI,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,EAAE;IAAE,OAAO,EAAE,WAAW,CAAC;IAAC,IAAI,EAAE,OAAO,CAAA;CAAE,GAAG,KAAK,CAAC,YAAY,CAuHnG;AAED,wBAAsB,MAAM,CAAC,IAAI,EAAE,OAAO,GAAG,OAAO,CAAC,MAAM,CAAC,CAkC3D"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/ink/index.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAsD,MAAM,OAAO,CAAC;AAK3E,OAAO,EAAE,WAAW,EAAE,KAAK,eAAe,EAAE,MAAM,YAAY,CAAC;AAM/D,MAAM,WAAW,OAAQ,SAAQ,eAAe;IAC9C,OAAO,EAAE,MAAM,CAAC;IAChB,qDAAqD;IACrD,MAAM,CAAC,EAAE;QAAE,EAAE,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;CAS1B;AAED;;wEAEwE;AACxE,wBAAgB,IAAI,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,EAAE;IAAE,OAAO,EAAE,WAAW,CAAC;IAAC,IAAI,EAAE,OAAO,CAAA;CAAE,GAAG,KAAK,CAAC,YAAY,CA8JnG;AAED,wBAAsB,MAAM,CAAC,IAAI,EAAE,OAAO,GAAG,OAAO,CAAC,MAAM,CAAC,CAyD3D"}
package/dist/ink/index.js CHANGED
@@ -1,108 +1,34 @@
1
1
  import {
2
- STANDARD_POLICY,
3
2
  TermSession,
4
- matchesAnyGlob,
5
3
  parseMarkdown
6
- } from "../chunk-HYWLODZT.js";
7
- import "../chunk-47IH6X5H.js";
4
+ } from "../chunk-F4ENQXQQ.js";
5
+ import "../chunk-OSJFMQCW.js";
8
6
  import {
9
- filterCommands
10
- } from "../chunk-R6AXTD55.js";
11
- import "../chunk-TCVDVYMF.js";
12
- import "../chunk-HAQGHLW6.js";
7
+ filterCommands,
8
+ parseCommand
9
+ } from "../chunk-VTJEWHM3.js";
10
+ import "../chunk-M44CET5L.js";
11
+ import {
12
+ formatEndingChange
13
+ } from "../chunk-I44FC7PE.js";
14
+ import "../chunk-IK4ZANNW.js";
15
+ import {
16
+ formatOrd
17
+ } from "../chunk-2D7G4YW2.js";
18
+ import {
19
+ sourceLabel
20
+ } from "../chunk-CB5RFNB4.js";
13
21
 
14
22
  // src/ink/index.tsx
15
23
  import { useCallback, useState as useState2, useSyncExternalStore } from "react";
16
24
  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
- };
25
+ import { PermissionService as PermissionService2 } from "@corenel/harness/guardrail/permission-service";
26
+ import { STANDARD_POLICY } from "@corenel/harness/guardrail/builtins";
101
27
 
102
28
  // src/ink/App.tsx
103
29
  import { useEffect, useState } from "react";
104
30
  import { homedir } from "node:os";
105
- import { Box as Box4, Static, Text as Text4, useStdout } from "ink";
31
+ import { Box as Box5, Static, Text as Text5, useStdout } from "ink";
106
32
  import TextInput from "ink-text-input";
107
33
 
108
34
  // src/ink/items.tsx
@@ -126,7 +52,16 @@ var glyph = {
126
52
  ok: "\u2713",
127
53
  fail: "\u2717",
128
54
  bar: "\u2502",
129
- sep: "\xB7"
55
+ sep: "\xB7",
56
+ /** The in-progress marker on a todo row -- the only status among
57
+ * pending/in_progress/completed/blocked without an existing glyph above. */
58
+ active: "\u25B8",
59
+ /** A restored tool row's outcome marker: a resumed conversation has no
60
+ * recorded success/failure for it (see persist.ts), so it must read as
61
+ * NEITHER `ok` nor `fail` -- a plain dash, not `sep`, which is this same
62
+ * row's own leading bullet a few columns to the left and would read as a
63
+ * second bullet rather than a status. */
64
+ unknown: "\u2013"
130
65
  };
131
66
  function tokens(n) {
132
67
  return n >= 1e3 ? `${(n / 1e3).toFixed(1)}k` : `${n}`;
@@ -145,7 +80,7 @@ function CommittedRow({ item }) {
145
80
  /* @__PURE__ */ jsx(Text, { bold: true, children: item.text })
146
81
  ] });
147
82
  case "assistant":
148
- return /* @__PURE__ */ jsx(Box, { flexDirection: "column", marginTop: 1, children: parseMarkdown(item.text).map((b, i) => /* @__PURE__ */ jsx(MdLine, { block: b }, i)) });
83
+ return /* @__PURE__ */ jsx(Box, { flexDirection: "column", marginTop: item.leadingBlank ? 1 : 0, children: parseMarkdown(item.text).map((b, i) => /* @__PURE__ */ jsx(MdLine, { block: b }, i)) });
149
84
  case "tool":
150
85
  return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
151
86
  /* @__PURE__ */ jsxs(Box, { children: [
@@ -153,18 +88,16 @@ function CommittedRow({ item }) {
153
88
  glyph.bullet,
154
89
  " "
155
90
  ] }),
91
+ /* @__PURE__ */ jsx(Text, { color: palette.dim, children: formatOrd(item.ord) }),
156
92
  /* @__PURE__ */ jsx(Text, { color: palette.tool, children: item.name.padEnd(11) }),
157
93
  /* @__PURE__ */ jsx(Text, { color: palette.ink, children: item.subject }),
158
- /* @__PURE__ */ jsxs(Text, { color: item.isError ? palette.bad : palette.good, children: [
94
+ /* @__PURE__ */ jsxs(Text, { color: item.restored ? palette.dim : item.isError ? palette.bad : palette.good, children: [
159
95
  " ",
160
- item.isError ? glyph.fail : glyph.ok
96
+ item.restored ? glyph.unknown : item.isError ? glyph.fail : glyph.ok
161
97
  ] }),
162
98
  /* @__PURE__ */ jsxs(Text, { color: palette.dim, children: [
163
99
  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
100
+ item.offloadedBytes !== void 0 ? ` offloaded ${Math.round(item.offloadedBytes / 1024)} KB` : ""
168
101
  ] })
169
102
  ] }),
170
103
  item.preview.map((line, i) => /* @__PURE__ */ jsxs(Box, { children: [
@@ -173,7 +106,7 @@ function CommittedRow({ item }) {
173
106
  glyph.bar,
174
107
  " "
175
108
  ] }),
176
- /* @__PURE__ */ jsx(Text, { color: item.isError ? palette.bad : palette.soft, children: line })
109
+ /* @__PURE__ */ jsx(Text, { color: !item.restored && item.isError ? palette.bad : palette.soft, children: line })
177
110
  ] }, i))
178
111
  ] });
179
112
  case "note":
@@ -213,18 +146,25 @@ function MdLine({ block }) {
213
146
  return /* @__PURE__ */ jsx(Text, { color: palette.soft, children: block.text });
214
147
  }
215
148
  }
149
+ function deltaTail(delta, max = 80) {
150
+ const lines = delta.split("\n").map((l) => l.trim()).filter(Boolean);
151
+ const last = lines.at(-1) ?? "";
152
+ return last.length > max ? `\u2026${last.slice(-max)}` : last;
153
+ }
216
154
  function LiveToolRow({ tool }) {
155
+ const tail = tool.delta ? deltaTail(tool.delta) : "";
217
156
  return /* @__PURE__ */ jsxs(Box, { children: [
218
157
  /* @__PURE__ */ jsxs(Text, { color: palette.amber, children: [
219
158
  glyph.bullet,
220
159
  " "
221
160
  ] }),
161
+ /* @__PURE__ */ jsx(Text, { color: palette.dim, children: formatOrd(tool.ord) }),
222
162
  /* @__PURE__ */ jsx(Text, { color: palette.tool, children: tool.name.padEnd(11) }),
223
163
  /* @__PURE__ */ jsx(Text, { color: palette.ink, children: tool.subject }),
224
164
  /* @__PURE__ */ jsxs(Text, { color: palette.dim, children: [
225
165
  " ",
226
- "running\u2026 #",
227
- tool.ord
166
+ "running\u2026",
167
+ tail ? ` ${tail}` : ""
228
168
  ] })
229
169
  ] });
230
170
  }
@@ -232,6 +172,42 @@ function LiveToolRow({ tool }) {
232
172
  // src/ink/Modal.tsx
233
173
  import { Box as Box2, Text as Text2 } from "ink";
234
174
  import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
175
+ function DiffPreview({ diff }) {
176
+ const contentUnchanged = diff.added === 0 && diff.removed === 0;
177
+ const endingRow = diff.endingChange && /* @__PURE__ */ jsx2(Text2, { color: palette.amber, bold: true, children: formatEndingChange(diff.endingChange, contentUnchanged) });
178
+ const unexplainedRow = diff.unexplainedChange && /* @__PURE__ */ jsx2(Text2, { color: palette.amber, bold: true, children: "the file changes in a way not shown above (e.g. trailing whitespace or encoding)" });
179
+ if (diff.noop) {
180
+ return /* @__PURE__ */ jsx2(Text2, { color: palette.dim, children: "(no change -- new content is identical to what's on disk)" });
181
+ }
182
+ return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", children: [
183
+ endingRow,
184
+ unexplainedRow,
185
+ diff.isNewFile && /* @__PURE__ */ jsx2(Text2, { color: palette.dim, children: "(new file)" }),
186
+ diff.rows.map((r, i) => {
187
+ if (r.kind === "skip") {
188
+ const label = r.reason === "changes" ? `\u2026 ${r.count} more changed line${r.count === 1 ? "" : "s"}` : `\u2026 ${r.count} unchanged line${r.count === 1 ? "" : "s"}`;
189
+ return /* @__PURE__ */ jsx2(Text2, { color: palette.dim, children: label }, i);
190
+ }
191
+ return /* @__PURE__ */ jsxs2(
192
+ Text2,
193
+ {
194
+ color: r.kind === "add" ? palette.good : r.kind === "del" ? palette.bad : palette.dim,
195
+ children: [
196
+ r.kind === "add" ? "+ " : r.kind === "del" ? "- " : " ",
197
+ r.text
198
+ ]
199
+ },
200
+ i
201
+ );
202
+ }),
203
+ /* @__PURE__ */ jsxs2(Text2, { color: palette.dim, children: [
204
+ "+",
205
+ diff.added,
206
+ " \u2212",
207
+ diff.removed
208
+ ] })
209
+ ] });
210
+ }
235
211
  function ModalView({ modal }) {
236
212
  if (modal.kind === "permission") {
237
213
  return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", borderStyle: "round", borderColor: palette.warn, paddingX: 1, children: [
@@ -240,6 +216,7 @@ function ModalView({ modal }) {
240
216
  " wants to run"
241
217
  ] }),
242
218
  /* @__PURE__ */ jsx2(Box2, { marginTop: 1, children: /* @__PURE__ */ jsx2(Text2, { bold: true, children: modal.detail }) }),
219
+ modal.diff && /* @__PURE__ */ jsx2(Box2, { marginTop: 1, children: /* @__PURE__ */ jsx2(DiffPreview, { diff: modal.diff }) }),
243
220
  /* @__PURE__ */ jsxs2(Box2, { marginTop: 1, children: [
244
221
  /* @__PURE__ */ jsx2(Text2, { color: palette.good, children: "[y]" }),
245
222
  /* @__PURE__ */ jsx2(Text2, { color: palette.dim, children: " run it " }),
@@ -292,6 +269,74 @@ function PaletteView({ items, selected }) {
292
269
  ] });
293
270
  }
294
271
 
272
+ // src/ink/TodoBlock.tsx
273
+ import { Box as Box4, Text as Text4 } from "ink";
274
+
275
+ // src/ink/todos.ts
276
+ var DEFAULT_TODO_ROW_CAP = 4;
277
+ function visibleTodos(todos, cap = DEFAULT_TODO_ROW_CAP) {
278
+ const total = todos.length;
279
+ const done = todos.filter((t) => t.status === "completed").length;
280
+ const outstanding = todos.filter((t) => t.status !== "completed");
281
+ if (outstanding.length === 0) return null;
282
+ const inProgress = outstanding.filter((t) => t.status === "in_progress");
283
+ const rest = outstanding.filter((t) => t.status !== "in_progress");
284
+ const ordered = [...inProgress, ...rest];
285
+ const header = `todos ${done}/${total} done`;
286
+ const rowCap = Math.max(cap, 0);
287
+ if (ordered.length <= rowCap) {
288
+ return { header, rows: toRows(ordered), overflow: null };
289
+ }
290
+ const shown = Math.max(rowCap - 1, 0);
291
+ const remaining = ordered.length - shown;
292
+ return {
293
+ header,
294
+ rows: toRows(ordered.slice(0, shown)),
295
+ overflow: `\u2026 ${remaining} more`
296
+ };
297
+ }
298
+ function toRows(items) {
299
+ return items.map((t) => ({ id: t.id, title: t.title, status: t.status }));
300
+ }
301
+
302
+ // src/ink/TodoBlock.tsx
303
+ import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
304
+ function marker(status) {
305
+ switch (status) {
306
+ case "in_progress":
307
+ return { char: glyph.active, color: palette.amber };
308
+ case "completed":
309
+ return { char: glyph.ok, color: palette.good };
310
+ case "blocked":
311
+ return { char: glyph.fail, color: palette.bad };
312
+ case "pending":
313
+ default:
314
+ return { char: glyph.bullet, color: palette.dim };
315
+ }
316
+ }
317
+ function TodoRowView({ row }) {
318
+ const { char, color } = marker(row.status);
319
+ return /* @__PURE__ */ jsxs4(Box4, { children: [
320
+ /* @__PURE__ */ jsxs4(Text4, { color, children: [
321
+ char,
322
+ " "
323
+ ] }),
324
+ /* @__PURE__ */ jsx4(Text4, { color: row.status === "in_progress" ? palette.ink : palette.soft, children: row.title })
325
+ ] });
326
+ }
327
+ function TodoBlock({ todos }) {
328
+ const view = visibleTodos(todos);
329
+ if (!view) return null;
330
+ return /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", marginTop: 1, children: [
331
+ /* @__PURE__ */ jsx4(Text4, { color: palette.dim, children: view.header }),
332
+ view.rows.map((row) => /* @__PURE__ */ jsx4(TodoRowView, { row }, row.id)),
333
+ view.overflow ? /* @__PURE__ */ jsxs4(Text4, { color: palette.dim, children: [
334
+ " ",
335
+ view.overflow
336
+ ] }) : null
337
+ ] });
338
+ }
339
+
295
340
  // src/ink/status.ts
296
341
  var DEFAULT_COLUMNS = 80;
297
342
  var MIN_GAP = 2;
@@ -335,7 +380,7 @@ function fitStatus(left, identity, columns) {
335
380
  }
336
381
 
337
382
  // src/ink/App.tsx
338
- import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
383
+ import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
339
384
  function useColumns() {
340
385
  const { stdout } = useStdout();
341
386
  const [columns, setColumns] = useState(stdout?.columns ?? DEFAULT_COLUMNS);
@@ -366,28 +411,28 @@ function App(props) {
366
411
  const shellSeg = !busy ? ` ${glyph.sep} shell ${props.approveAll ? "auto-approved" : "asks first"}` : "";
367
412
  const statusLeft = ` ${statusWord}${roundSeg}${tokenSeg}${escSeg}${queuedSeg}${shellSeg}`;
368
413
  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 ",
414
+ return /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", children: [
415
+ /* @__PURE__ */ jsx5(Static, { items: state.committed, children: (item) => /* @__PURE__ */ jsx5(CommittedRow, { item }, item.id) }, `transcript-${state.transcriptGen}`),
416
+ state.live?.tools.map((t) => /* @__PURE__ */ jsx5(LiveToolRow, { tool: t }, t.callId)),
417
+ state.live?.streaming ? /* @__PURE__ */ jsx5(Box5, { marginTop: 1, children: /* @__PURE__ */ jsx5(Text5, { color: palette.soft, children: state.live.streaming }) }) : null,
418
+ /* @__PURE__ */ jsx5(TodoBlock, { todos: state.todos }),
419
+ state.modal ? /* @__PURE__ */ jsx5(Box5, { marginTop: 1, children: /* @__PURE__ */ jsx5(ModalView, { modal: state.modal }) }) : null,
420
+ paletteItems.length && !state.modal ? /* @__PURE__ */ jsx5(Box5, { marginTop: 1, children: /* @__PURE__ */ jsx5(PaletteView, { items: paletteItems, selected: paletteSelected }) }) : null,
421
+ state.pendingSkill ? /* @__PURE__ */ jsxs5(Box5, { marginTop: 1, children: [
422
+ /* @__PURE__ */ jsxs5(Text5, { color: palette.ember, children: [
375
423
  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: [
424
+ " skill armed: ",
425
+ state.pendingSkill,
426
+ " "
427
+ ] }),
428
+ /* @__PURE__ */ jsx5(Text5, { color: palette.dim, children: "-- applies to your next message, then clears" })
429
+ ] }) : null,
430
+ /* @__PURE__ */ jsxs5(Box5, { borderStyle: "round", borderColor: palette.dim, paddingX: 1, marginTop: 1, children: [
431
+ /* @__PURE__ */ jsxs5(Text5, { color: palette.ember, children: [
387
432
  glyph.prompt,
388
433
  " "
389
434
  ] }),
390
- /* @__PURE__ */ jsx4(
435
+ /* @__PURE__ */ jsx5(
391
436
  TextInput,
392
437
  {
393
438
  value: props.value,
@@ -395,24 +440,35 @@ function App(props) {
395
440
  onSubmit: state.modal ? props.onAnswer : props.onSubmit,
396
441
  placeholder: "",
397
442
  focus: props.focus
398
- }
443
+ },
444
+ props.inputKey
399
445
  )
400
446
  ] }),
401
- /* @__PURE__ */ jsxs4(Box4, { justifyContent: "space-between", children: [
402
- /* @__PURE__ */ jsxs4(Box4, { children: [
403
- /* @__PURE__ */ jsxs4(Text4, { color: busy ? palette.amber : palette.dim, children: [
447
+ state.committed.length === 0 ? /* @__PURE__ */ jsx5(Box5, { marginBottom: 1, children: /* @__PURE__ */ jsxs5(Text5, { color: palette.soft, children: [
448
+ "Type a request. ",
449
+ /* @__PURE__ */ jsxs5(Text5, { color: palette.dim, children: [
450
+ "/help for commands ",
451
+ glyph.sep,
452
+ " Ctrl-C stops a turn ",
453
+ glyph.sep,
454
+ " Ctrl-D leaves"
455
+ ] })
456
+ ] }) }) : null,
457
+ /* @__PURE__ */ jsxs5(Box5, { justifyContent: "space-between", children: [
458
+ /* @__PURE__ */ jsxs5(Box5, { children: [
459
+ /* @__PURE__ */ jsxs5(Text5, { color: busy ? palette.amber : palette.dim, children: [
404
460
  " ",
405
461
  statusWord
406
462
  ] }),
407
- /* @__PURE__ */ jsxs4(Text4, { color: palette.dim, children: [
463
+ /* @__PURE__ */ jsxs5(Text5, { color: palette.dim, children: [
408
464
  roundSeg,
409
465
  tokenSeg,
410
466
  escSeg,
411
467
  queuedSeg
412
468
  ] }),
413
- !busy ? /* @__PURE__ */ jsx4(Text4, { color: props.approveAll ? palette.ember : palette.warn, children: shellSeg }) : null
469
+ !busy ? /* @__PURE__ */ jsx5(Text5, { color: props.approveAll ? palette.ember : palette.warn, children: shellSeg }) : null
414
470
  ] }),
415
- statusRight ? /* @__PURE__ */ jsx4(Text4, { color: palette.dim, children: statusRight }) : null
471
+ statusRight ? /* @__PURE__ */ jsx5(Text5, { color: palette.dim, children: statusRight }) : null
416
472
  ] })
417
473
  ] });
418
474
  }
@@ -423,12 +479,84 @@ function primeScroll(out, opts) {
423
479
  if (opts.resuming) return 0;
424
480
  const rows = out.rows;
425
481
  if (typeof rows !== "number" || !Number.isFinite(rows) || rows <= 0) return 0;
426
- out.write("\n".repeat(rows));
427
- return rows;
482
+ const reserve = typeof opts.reserve === "number" && Number.isFinite(opts.reserve) && opts.reserve > 0 ? Math.floor(opts.reserve) : 0;
483
+ const lines = Math.max(0, rows - reserve);
484
+ if (lines > 0) out.write("\n".repeat(lines));
485
+ return lines;
486
+ }
487
+
488
+ // src/ink/banner.ts
489
+ import { PermissionService } from "@corenel/harness/guardrail/permission-service";
490
+ import { NAMESPACE_LABELS, resolveNamespace } from "@corenel/harness/tools/namespaces";
491
+ var ART = [
492
+ " \u2588\u2588\u2588\u2588\u2588\u2588 \u2588\u2588\u2588\u2588\u2588\u2588 \u2588\u2588\u2588\u2588\u2588\u2588 \u2588\u2588\u2588\u2588\u2588\u2588\u2588 \u2588\u2588\u2588 \u2588\u2588 \u2588\u2588\u2588\u2588\u2588\u2588\u2588 \u2588\u2588",
493
+ "\u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588\u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588",
494
+ "\u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588\u2588\u2588\u2588\u2588 \u2588\u2588\u2588\u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588\u2588\u2588\u2588 \u2588\u2588",
495
+ "\u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588",
496
+ " \u2588\u2588\u2588\u2588\u2588\u2588 \u2588\u2588\u2588\u2588\u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588\u2588\u2588\u2588\u2588\u2588 \u2588\u2588 \u2588\u2588\u2588\u2588 \u2588\u2588\u2588\u2588\u2588\u2588\u2588 \u2588\u2588\u2588\u2588\u2588\u2588\u2588"
497
+ ];
498
+ var ART_MIN_COLUMNS = Math.max(...ART.map((r) => r.length));
499
+ var WORDMARK = "corenel";
500
+ var DEFAULT_COLUMNS2 = 80;
501
+ var SHELL_SUBJECT = { name: "shell", address: "shell:shell", mutates: true };
502
+ var WRITE_SUBJECT = { name: "write_file", address: "fs:write_file", mutates: true };
503
+ function posture(policy) {
504
+ const permission = new PermissionService({ policy });
505
+ const shellLevel = permission.resolveLevel(SHELL_SUBJECT);
506
+ if (shellLevel === "ask") return "shell asks first";
507
+ if (shellLevel === "deny") return "shell off";
508
+ const writeLevel = permission.resolveLevel(WRITE_SUBJECT);
509
+ if (writeLevel === "allow") return "nothing asks -- unattended";
510
+ if (writeLevel === "ask") return "shell runs, writes ask";
511
+ return "shell runs, writes off";
512
+ }
513
+ function toolBreakdown(tools) {
514
+ const counts = /* @__PURE__ */ new Map();
515
+ for (const t of tools) {
516
+ const ns = resolveNamespace(t);
517
+ counts.set(ns, (counts.get(ns) ?? 0) + 1);
518
+ }
519
+ return NAMESPACE_LABELS.filter(({ ns }) => counts.has(ns)).map(({ ns, label }) => `${label} ${counts.get(ns)}`).join(", ");
520
+ }
521
+ function truncateLeft(path, max) {
522
+ if (path.length <= max) return path;
523
+ if (max <= 1) return path.slice(-max);
524
+ return `\u2026${path.slice(path.length - (max - 1))}`;
525
+ }
526
+ function buildBanner(opts) {
527
+ if (opts.isTTY !== true) return "";
528
+ const columns = opts.columns !== void 0 && opts.columns > 0 ? opts.columns : DEFAULT_COLUMNS2;
529
+ const total = opts.tools.length;
530
+ const breakdown = toolBreakdown(opts.tools);
531
+ const toolsFull = breakdown ? `${total} tools (${breakdown})` : `${total} tools`;
532
+ const toolsShort = `${total} tools`;
533
+ const label = sourceLabel(opts.source);
534
+ const policySuffix = label ? ` \xB7 policy ${label}` : "";
535
+ const p = posture(opts.policy);
536
+ const build = (toolsStr, cwd) => ` ${p} \xB7 ${toolsStr} \xB7 ${cwd}${policySuffix}`;
537
+ let statusLine = build(toolsFull, opts.cwd);
538
+ if (statusLine.length > columns) statusLine = build(toolsShort, opts.cwd);
539
+ if (statusLine.length > columns) {
540
+ const overshoot = statusLine.length - columns;
541
+ const budget = Math.max(1, opts.cwd.length - overshoot);
542
+ statusLine = build(toolsShort, truncateLeft(opts.cwd, budget));
543
+ }
544
+ if (statusLine.length > columns) statusLine = statusLine.slice(0, columns);
545
+ if (columns < ART_MIN_COLUMNS) return [WORDMARK, "", statusLine].join("\n");
546
+ return [...ART, "", statusLine].join("\n");
547
+ }
548
+ function bannerHeight(out, opts) {
549
+ const text = buildBanner({ ...opts, isTTY: out.isTTY === true, ...out.columns !== void 0 ? { columns: out.columns } : {} });
550
+ return text ? text.split("\n").length + 1 : 0;
551
+ }
552
+ function writeBanner(out, opts) {
553
+ const text = buildBanner({ ...opts, isTTY: out.isTTY === true, ...out.columns !== void 0 ? { columns: out.columns } : {} });
554
+ if (text) out.write(`${text}
555
+ `);
428
556
  }
429
557
 
430
558
  // src/ink/index.tsx
431
- import { jsx as jsx5 } from "react/jsx-runtime";
559
+ import { jsx as jsx6 } from "react/jsx-runtime";
432
560
  function Root({ session, deps }) {
433
561
  const state = useSyncExternalStore(
434
562
  useCallback((fn) => session.subscribe(fn), [session]),
@@ -438,6 +566,11 @@ function Root({ session, deps }) {
438
566
  const { exit } = useApp();
439
567
  const [sawSigint, setSawSigint] = useState2(false);
440
568
  const [paletteIndex, setPaletteIndex] = useState2(0);
569
+ const [fieldGen, setFieldGen] = useState2(0);
570
+ const completeTo = (v) => {
571
+ setValue(v);
572
+ setFieldGen((g) => g + 1);
573
+ };
441
574
  const handleChange = (v) => {
442
575
  const cleaned = v.replace(/\x1b/g, "");
443
576
  if (state.modal?.kind === "user" && value === "" && cleaned.length === 1) {
@@ -487,7 +620,7 @@ function Root({ session, deps }) {
487
620
  }
488
621
  if (key.tab) {
489
622
  const chosen = paletteItems[Math.min(paletteIndex, paletteItems.length - 1)];
490
- if (chosen) setValue(`/${chosen.name} `);
623
+ if (chosen) completeTo(`/${chosen.name} `);
491
624
  setPaletteIndex(0);
492
625
  return;
493
626
  }
@@ -511,7 +644,7 @@ function Root({ session, deps }) {
511
644
  }
512
645
  if (sawSigint) setSawSigint(false);
513
646
  });
514
- return /* @__PURE__ */ jsx5(
647
+ return /* @__PURE__ */ jsx6(
515
648
  App,
516
649
  {
517
650
  state,
@@ -523,6 +656,22 @@ function Root({ session, deps }) {
523
656
  value,
524
657
  onChange: handleChange,
525
658
  onSubmit: (v) => {
659
+ const items = v.startsWith("/") ? filterCommands(v) : [];
660
+ if (items.length > 0 && !parseCommand(v)) {
661
+ const chosen = items[Math.min(paletteIndex, items.length - 1)];
662
+ if (chosen) {
663
+ if (chosen.args) {
664
+ completeTo(`/${chosen.name} `);
665
+ setPaletteIndex(0);
666
+ return;
667
+ }
668
+ setValue("");
669
+ void session.submit(`/${chosen.name}`).then(() => {
670
+ if (session.wantsExit) exit();
671
+ });
672
+ return;
673
+ }
674
+ }
526
675
  setValue("");
527
676
  void session.submit(v).then(() => {
528
677
  if (session.wantsExit) exit();
@@ -536,20 +685,31 @@ function Root({ session, deps }) {
536
685
  else session.answer(v.trim() ? { [modal.questions[0].header]: [v.trim()] } : null);
537
686
  },
538
687
  focus: state.modal?.kind !== "permission",
539
- paletteSelected: paletteIndex
688
+ paletteSelected: paletteIndex,
689
+ inputKey: fieldGen
540
690
  }
541
691
  );
542
692
  }
543
693
  async function runTui(deps) {
544
694
  const session = new TermSession(deps);
545
695
  if (deps.resume) await session.resume(deps.resume.id);
546
- const permission = new PermissionService({
547
- policy: STANDARD_POLICY,
696
+ const permission = new PermissionService2({
697
+ policy: deps.policy ?? STANDARD_POLICY,
548
698
  prompt: { ask: (req) => session.askPermission(req) }
549
699
  });
550
700
  session.attachPermission(permission);
551
- primeScroll(process.stdout, { resuming: deps.resume != null });
552
- const app = render(/* @__PURE__ */ jsx5(Root, { session, deps }), {
701
+ const bannerOpts = {
702
+ policy: deps.policy ?? STANDARD_POLICY,
703
+ source: deps.policySource ?? { kind: "default" },
704
+ tools: deps.tools,
705
+ cwd: deps.cwd
706
+ };
707
+ primeScroll(process.stdout, {
708
+ resuming: deps.resume != null,
709
+ reserve: bannerHeight(process.stdout, bannerOpts)
710
+ });
711
+ writeBanner(process.stdout, bannerOpts);
712
+ const app = render(/* @__PURE__ */ jsx6(Root, { session, deps }), {
553
713
  /* LOAD-BEARING — do not "clean up" or default this away. Ink's own App
554
714
  * component (build/components/App.js, handleInput) intercepts the raw
555
715
  * \x03 byte and calls handleExit() BEFORE it ever emits the 'input'
@@ -1 +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"}
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;AAKxD;gDACgD;AAChD,wBAAgB,YAAY,CAAC,EAAE,IAAI,EAAE,EAAE;IAAE,IAAI,EAAE,aAAa,CAAA;CAAE,GAAG,KAAK,CAAC,YAAY,CAwDlF;AA4CD,gEAAgE;AAChE,wBAAgB,WAAW,CAAC,EAAE,IAAI,EAAE,EAAE;IAAE,IAAI,EAAE,QAAQ,CAAA;CAAE,GAAG,KAAK,CAAC,YAAY,CAW5E"}