@gonrocca/nodd 0.1.2 → 0.2.1
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/extensions/nodd-agents.test.ts +10 -0
- package/extensions/nodd-agents.ts +24 -2
- package/extensions/nodd-enforcement.test.ts +2 -1
- package/extensions/nodd-kernel.ts +8 -3
- package/extensions/nodd-models.test.ts +42 -5
- package/extensions/nodd-models.ts +145 -42
- package/extensions/nodd-tools.test.ts +2 -1
- package/package.json +1 -1
- package/src/config.test.ts +11 -0
- package/src/gates/request.ts +4 -1
- package/src/gates/track.test.ts +14 -0
- package/src/models/layout.test.ts +122 -0
- package/src/models/layout.ts +187 -0
- package/src/models/picker.test.ts +467 -77
- package/src/models/picker.ts +644 -65
- package/src/models/profiles.test.ts +86 -0
- package/src/models/profiles.ts +65 -4
- package/src/models/slots.ts +4 -3
- package/src/models/thinking.test.ts +14 -0
- package/src/models/thinking.ts +22 -0
- package/src/state.test.ts +14 -0
- package/src/state.ts +4 -1
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
// Terminal-layout helpers for the `/nodd-models` picker.
|
|
2
|
+
//
|
|
3
|
+
// Adapted from `zero-tui-layout.ts` — adapted, never imported: NODD is a
|
|
4
|
+
// standalone package.
|
|
5
|
+
//
|
|
6
|
+
// pi has no windows. `ctx.ui.custom()` hands a component `render(width):
|
|
7
|
+
// string[]` and pi prints those lines into the terminal flow; it never clips
|
|
8
|
+
// them. So a block returning more lines than the terminal has rows scrolls its
|
|
9
|
+
// own top — frame border and title first — off the screen, and a line wider than
|
|
10
|
+
// the terminal wraps and breaks the box. Fitting the viewport is the block's own
|
|
11
|
+
// job, and this module is where that job lives.
|
|
12
|
+
//
|
|
13
|
+
// Zero imports, on purpose: no `node:*`, no pi, no TUI package. The terminal
|
|
14
|
+
// size always arrives as an argument, so `node --test` exercises every helper
|
|
15
|
+
// with no terminal at all.
|
|
16
|
+
//
|
|
17
|
+
// Unlike forge's version there is no `setWidthFns` injection hook. That hook
|
|
18
|
+
// exists so a real session can swap in pi-tui's own width functions, and NODD
|
|
19
|
+
// may not import that package under any spelling — an injection point nobody can
|
|
20
|
+
// reach would be dead configuration.
|
|
21
|
+
|
|
22
|
+
/** The escape byte, spelled as a unicode escape so the source stays printable. */
|
|
23
|
+
const ESC = "\u001b";
|
|
24
|
+
|
|
25
|
+
/** ANSI CSI/SGR sequences and OSC strings — they occupy zero display cells. */
|
|
26
|
+
const ANSI_RE = new RegExp(`${ESC}\\[[0-9;?]*[ -/]*[@-~]|${ESC}\\][^]*?(?:\\u0007|${ESC}\\\\)`, "g");
|
|
27
|
+
|
|
28
|
+
/** The reset appended when a truncation cuts inside a styled run. */
|
|
29
|
+
const ANSI_RESET = `${ESC}[0m`;
|
|
30
|
+
|
|
31
|
+
/** Strip every ANSI escape, leaving only the characters that occupy cells. */
|
|
32
|
+
export function stripAnsi(text: string): string {
|
|
33
|
+
return text.replace(ANSI_RE, "");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* How many terminal cells one code point occupies: combining marks and
|
|
38
|
+
* zero-width joiners none, East-Asian Wide/Fullwidth and emoji two, the rest
|
|
39
|
+
* one. A range table rather than a dependency, because the no-TUI-import rule
|
|
40
|
+
* leaves nothing to delegate to.
|
|
41
|
+
*/
|
|
42
|
+
function codePointWidth(cp: number): number {
|
|
43
|
+
if (cp < 32 || (cp >= 0x7f && cp < 0xa0)) return 0;
|
|
44
|
+
if (
|
|
45
|
+
(cp >= 0x0300 && cp <= 0x036f) ||
|
|
46
|
+
(cp >= 0x200b && cp <= 0x200f) ||
|
|
47
|
+
(cp >= 0xfe00 && cp <= 0xfe0f) ||
|
|
48
|
+
(cp >= 0xfe20 && cp <= 0xfe2f)
|
|
49
|
+
) {
|
|
50
|
+
return 0;
|
|
51
|
+
}
|
|
52
|
+
if (
|
|
53
|
+
(cp >= 0x1100 && cp <= 0x115f) ||
|
|
54
|
+
(cp >= 0x2e80 && cp <= 0x303e) ||
|
|
55
|
+
(cp >= 0x3041 && cp <= 0x33ff) ||
|
|
56
|
+
(cp >= 0x3400 && cp <= 0x4dbf) ||
|
|
57
|
+
(cp >= 0x4e00 && cp <= 0x9fff) ||
|
|
58
|
+
(cp >= 0xa000 && cp <= 0xa4cf) ||
|
|
59
|
+
(cp >= 0xac00 && cp <= 0xd7a3) ||
|
|
60
|
+
(cp >= 0xf900 && cp <= 0xfaff) ||
|
|
61
|
+
(cp >= 0xfe30 && cp <= 0xfe6f) ||
|
|
62
|
+
(cp >= 0xff00 && cp <= 0xff60) ||
|
|
63
|
+
(cp >= 0xffe0 && cp <= 0xffe6) ||
|
|
64
|
+
(cp >= 0x1f300 && cp <= 0x1f64f) ||
|
|
65
|
+
(cp >= 0x1f680 && cp <= 0x1f6ff) ||
|
|
66
|
+
(cp >= 0x1f7e0 && cp <= 0x1f7eb) ||
|
|
67
|
+
(cp >= 0x1f900 && cp <= 0x1f9ff) ||
|
|
68
|
+
(cp >= 0x1fa70 && cp <= 0x1faff) ||
|
|
69
|
+
(cp >= 0x20000 && cp <= 0x3fffd)
|
|
70
|
+
) {
|
|
71
|
+
return 2;
|
|
72
|
+
}
|
|
73
|
+
return 1;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Printable width of `text` in terminal cells, ANSI escapes excluded. */
|
|
77
|
+
export function visibleWidth(text: string): number {
|
|
78
|
+
let width = 0;
|
|
79
|
+
for (const ch of stripAnsi(text)) width += codePointWidth(ch.codePointAt(0) ?? 0);
|
|
80
|
+
return width;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Cut `text` to at most `maxWidth` cells, marking the cut with `ellipsis`.
|
|
85
|
+
*
|
|
86
|
+
* Iterates by code point, never by UTF-16 unit, so a surrogate pair is never
|
|
87
|
+
* split in half, and accounts for wide glyphs so the result cannot overshoot the
|
|
88
|
+
* budget. ANSI escapes pass through verbatim and a reset is appended when the
|
|
89
|
+
* cut lands inside a styled run, so a truncated coloured label cannot bleed its
|
|
90
|
+
* colour into the rest of the frame.
|
|
91
|
+
*/
|
|
92
|
+
export function truncateToWidth(text: string, maxWidth: number, ellipsis = "…"): string {
|
|
93
|
+
if (maxWidth <= 0) return "";
|
|
94
|
+
if (visibleWidth(text) <= maxWidth) return text;
|
|
95
|
+
|
|
96
|
+
const mark = visibleWidth(ellipsis) <= maxWidth ? ellipsis : "";
|
|
97
|
+
const budget = maxWidth - visibleWidth(mark);
|
|
98
|
+
|
|
99
|
+
let out = "";
|
|
100
|
+
let width = 0;
|
|
101
|
+
let styled = false;
|
|
102
|
+
let index = 0;
|
|
103
|
+
|
|
104
|
+
while (index < text.length) {
|
|
105
|
+
ANSI_RE.lastIndex = index;
|
|
106
|
+
const match = ANSI_RE.exec(text);
|
|
107
|
+
if (match && match.index === index) {
|
|
108
|
+
out += match[0];
|
|
109
|
+
styled = match[0] !== ANSI_RESET;
|
|
110
|
+
index += match[0].length;
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
const ch = String.fromCodePoint(text.codePointAt(index) ?? 0);
|
|
114
|
+
const cells = codePointWidth(ch.codePointAt(0) ?? 0);
|
|
115
|
+
if (width + cells > budget) break;
|
|
116
|
+
out += ch;
|
|
117
|
+
width += cells;
|
|
118
|
+
index += ch.length;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return out + mark + (styled ? ANSI_RESET : "");
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** Truncate *and* space-pad `text` so it occupies exactly `width` cells. */
|
|
125
|
+
export function padToWidth(text: string, width: number): string {
|
|
126
|
+
if (width <= 0) return "";
|
|
127
|
+
const clipped = truncateToWidth(text, width);
|
|
128
|
+
return clipped + " ".repeat(Math.max(0, width - visibleWidth(clipped)));
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** The slice of a long list that is actually drawn, plus what it hides. */
|
|
132
|
+
export type RowWindow = {
|
|
133
|
+
/** First drawn index, inclusive. */
|
|
134
|
+
start: number;
|
|
135
|
+
/** Last drawn index, exclusive. */
|
|
136
|
+
end: number;
|
|
137
|
+
/** How many items sit above `start`. */
|
|
138
|
+
hiddenBefore: number;
|
|
139
|
+
/** How many items sit below `end`. */
|
|
140
|
+
hiddenAfter: number;
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Pick the slice of `total` items to draw so `cursor` is always visible.
|
|
145
|
+
*
|
|
146
|
+
* Centred on the cursor and clamped at both ends, so moving through a long list
|
|
147
|
+
* scrolls only once the cursor reaches the edge instead of jumping the viewport
|
|
148
|
+
* on every keystroke.
|
|
149
|
+
*/
|
|
150
|
+
export function windowRows(total: number, cursor: number, capacity: number): RowWindow {
|
|
151
|
+
if (capacity <= 0 || total <= 0) {
|
|
152
|
+
return { start: 0, end: 0, hiddenBefore: 0, hiddenAfter: Math.max(0, total) };
|
|
153
|
+
}
|
|
154
|
+
if (total <= capacity) return { start: 0, end: total, hiddenBefore: 0, hiddenAfter: 0 };
|
|
155
|
+
|
|
156
|
+
const safeCursor = Math.min(Math.max(0, cursor), total - 1);
|
|
157
|
+
const start = Math.min(Math.max(0, safeCursor - Math.floor(capacity / 2)), total - capacity);
|
|
158
|
+
const end = start + capacity;
|
|
159
|
+
return { start, end, hiddenBefore: start, hiddenAfter: total - end };
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** Rows pi's own chrome (input box, status line, spacing) takes off the top. */
|
|
163
|
+
const PI_CHROME_ROWS = 8;
|
|
164
|
+
|
|
165
|
+
/** Never render a block shorter than this, whatever the terminal claims. */
|
|
166
|
+
const MIN_BLOCK_ROWS = 8;
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* How many rows the picker may use in a terminal of `terminalRows` rows. A
|
|
170
|
+
* terminal too short to host the reserve still gets `MIN_BLOCK_ROWS`: a
|
|
171
|
+
* slightly-too-tall block beats an empty frame.
|
|
172
|
+
*/
|
|
173
|
+
export function usableRows(terminalRows: number | undefined): number {
|
|
174
|
+
if (!terminalRows || !Number.isFinite(terminalRows) || terminalRows <= 0) return MIN_BLOCK_ROWS * 2;
|
|
175
|
+
return Math.max(MIN_BLOCK_ROWS, terminalRows - PI_CHROME_ROWS);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Last-resort height clamp for an already-rendered block. Keeps the *first*
|
|
180
|
+
* `maxRows` lines: when something still overflows, losing the bottom border
|
|
181
|
+
* beats losing the frame top and the title, which is exactly what the terminal's
|
|
182
|
+
* own scroll takes away.
|
|
183
|
+
*/
|
|
184
|
+
export function fitRows(lines: readonly string[], maxRows: number): string[] {
|
|
185
|
+
if (maxRows <= 0) return [];
|
|
186
|
+
return lines.length <= maxRows ? [...lines] : lines.slice(0, maxRows);
|
|
187
|
+
}
|