pi-openui 0.0.0-stage → 0.1.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.
@@ -0,0 +1,277 @@
1
+ // Notes: the user annotates an element of a widget and sends the notes back to the agent as one message.
2
+ // Pure logic, no pi imports. The words a note carries about an element come from the widget (an agent wrote them) and end up
3
+ // in a message the user sends, so they are cleaned first: control and invisible characters go, backticks and quotes are made
4
+ // harmless, and `@file:`-style references are defused.
5
+ import { textWidth, truncateText, type Cell } from "../diagram/canvas.ts";
6
+ import type { El } from "../lang/library.ts";
7
+ import type { Block, ItemInfo, Note, Target, WState } from "./types.ts";
8
+ import { boxed, cell, fromRows, textCells, vstack, wrapText } from "./util.ts";
9
+
10
+ export const NOTE_LIMITS = { note: 1000, notes: 20, label: 40, cell: 60, excerpt: 60 };
11
+
12
+ /** Components a note can be attached to. Buttons and choices act, and layout components only hold others. */
13
+ export const NOTEABLE = new Set(["Text", "Markdown", "Callout", "CodeBlock", "Faq", "Metric", "Image", "Table", "BarChart", "LineChart", "AreaChart", "PieChart", "RadarChart", "GeoMap", "Dag", "Erd", "InfraMap", "SequenceDiagram"]);
14
+ /** Components whose whole body is one item: there is nothing finer to point at. */
15
+ export const WHOLE_ONLY = new Set(["Text", "Markdown", "Callout", "CodeBlock", "Faq", "Metric", "Image"]);
16
+
17
+ // ---------- cleaning ----------
18
+
19
+ /** Control characters, line and paragraph separators and Unicode format characters (zero width, bidi marks): none may reach a message. */
20
+ export function strip(value: unknown, keepNewline = false): string {
21
+ const s = value == null ? "" : String(value);
22
+ return s.replace(/\r\n?/g, "\n").replace(/[\p{Cc}\p{Cf}\u2028\u2029]/gu, (ch) => (keepNewline && ch === "\n" ? ch : ""));
23
+ }
24
+
25
+ /** Any run of whitespace becomes one space; the ends are trimmed. */
26
+ export function collapse(value: unknown): string {
27
+ const s = value == null ? "" : String(value);
28
+ return s.replace(/\p{Cf}/gu, "").replace(/[^\P{Cc}\t\n\r]/gu, "").replace(/\s+/g, " ").trim();
29
+ }
30
+
31
+ export function clip(s: string, max: number): string {
32
+ const chars = Array.from(s);
33
+ return chars.length <= max ? s : chars.slice(0, Math.max(0, max - 3)).join("").trimEnd() + "...";
34
+ }
35
+
36
+ /** A word that starts with @ could become a file reference in a message: write it as (at). */
37
+ export const neutralize = (s: string): string => s.replace(/(^|\s)@(?=\S)/g, "$1(at)");
38
+
39
+ /** A word of the widget that goes inside backticks or double quotes of the message: one line, no backtick or quote of its own. */
40
+ export function quiet(raw: unknown, max: number): string {
41
+ return clip(neutralize(collapse(raw).replace(/`/g, "'").replace(/"/g, "'")), max);
42
+ }
43
+
44
+ /** The user's own text: newlines stay, trailing blanks go. Not neutralized: it is theirs. */
45
+ export function cleanNote(raw: unknown): string {
46
+ return strip(raw, true)
47
+ .split("\n")
48
+ .map((l) => l.replace(/[ \t]+$/g, ""))
49
+ .join("\n")
50
+ .trim();
51
+ }
52
+
53
+ // ---------- words for an item ----------
54
+
55
+ const q = (s: unknown, max = NOTE_LIMITS.label) => `"${quiet(s, max)}"`;
56
+ const b = (s: unknown, max = NOTE_LIMITS.label) => `\`${quiet(s, max)}\``;
57
+ const firstLine = (s: unknown) => collapse(String(s ?? "").split("\n").find((l) => l.trim()) ?? "");
58
+
59
+ /** What identifies a whole component when it has no finer key: a short quote of its own words. */
60
+ function excerpt(el: El): string {
61
+ const p = el.props;
62
+ switch (el.type) {
63
+ case "Text":
64
+ case "Callout":
65
+ return quiet(p.text, NOTE_LIMITS.excerpt);
66
+ case "Markdown":
67
+ return quiet(String(p.text ?? "").replace(/^#+\s*/gm, "").split("\n").find((l) => l.trim()) ?? "", NOTE_LIMITS.excerpt);
68
+ case "CodeBlock":
69
+ return quiet(p.title ?? firstLine(p.code), NOTE_LIMITS.excerpt);
70
+ case "Faq":
71
+ return quiet(p.items?.[0]?.props?.question ?? "", NOTE_LIMITS.excerpt);
72
+ case "Metric":
73
+ return quiet(p.label, NOTE_LIMITS.label);
74
+ case "Image":
75
+ return quiet(p.alt ?? p.src, NOTE_LIMITS.excerpt);
76
+ default:
77
+ return p.title ? quiet(p.title, NOTE_LIMITS.excerpt) : "";
78
+ }
79
+ }
80
+
81
+ function tableWords(el: El, key: string): { where: string; short: string } {
82
+ const rows = (el.props.rows ?? []) as unknown[][];
83
+ const cols = ((el.props.columns ?? []) as string[]).map(String);
84
+ let m: RegExpExecArray | null;
85
+ if ((m = /^row:(\d+)$/.exec(key))) {
86
+ const first = rows[Number(m[1]) - 1]?.[0];
87
+ return { where: `row ${m[1]}${first !== undefined ? ` (${q(first, NOTE_LIMITS.cell)})` : ""}`, short: `row ${m[1]}` };
88
+ }
89
+ if ((m = /^col:(.*)$/.exec(key))) return { where: `column ${b(m[1])}`, short: `column ${quiet(m[1], 20)}` };
90
+ if ((m = /^cell:(\d+)\.(.*)$/.exec(key))) {
91
+ const v = rows[Number(m[1]) - 1]?.[cols.indexOf(m[2])];
92
+ return { where: `row ${m[1]}, column ${b(m[2])}${v !== undefined ? `, cell ${q(v, NOTE_LIMITS.cell)}` : ""}`, short: `row ${m[1]}, ${quiet(m[2], 20)}` };
93
+ }
94
+ return { where: key, short: key };
95
+ }
96
+
97
+ /** The words that say which element of a component a note is about. `key` is the tooltip vocabulary; "" is the whole component. */
98
+ export function itemWords(el: El, key: string): { where: string; short: string } {
99
+ if (!key) {
100
+ const x = excerpt(el);
101
+ return { where: x ? `"${x}"` : "", short: x ? `"${clip(x, 24)}"` : "" };
102
+ }
103
+ let m: RegExpExecArray | null;
104
+ switch (el.type) {
105
+ case "Table":
106
+ return tableWords(el, key);
107
+ case "Dag": {
108
+ const n = (el.props.nodes as El[] | undefined)?.find((x) => x.props.id === key);
109
+ const label = n && n.props.label !== key ? ` (${q(n.props.label)})` : "";
110
+ return { where: `node ${b(key)}${label}`, short: `node ${quiet(n?.props.label ?? key, 24)}` };
111
+ }
112
+ case "Erd":
113
+ return key.includes(".") ? { where: `column ${b(key)}`, short: `column ${quiet(key, 30)}` } : { where: `table ${b(key)}`, short: `table ${quiet(key, 24)}` };
114
+ case "InfraMap":
115
+ if ((m = /^zone:(.*)$/.exec(key))) return { where: `zone ${q(m[1])}`, short: `zone ${quiet(m[1], 24)}` };
116
+ if (key.includes(">")) {
117
+ const [from, to] = key.split(">");
118
+ return { where: `link ${b(from)} -> ${b(to)}`, short: `link ${quiet(key, 30)}` };
119
+ }
120
+ return { where: `node ${b(key)}`, short: `node ${quiet(key, 24)}` };
121
+ case "SequenceDiagram": {
122
+ const isPart = (el.props.participants as El[] | undefined)?.some((x) => x.props.id === key);
123
+ return { where: `${isPart ? "participant" : "message"} ${b(key)}`, short: `${isPart ? "participant" : "message"} ${quiet(key, 24)}` };
124
+ }
125
+ case "GeoMap":
126
+ if (key.includes(">")) {
127
+ const [from, to] = key.split(">");
128
+ return { where: `link ${q(from)} -> ${q(to)}`, short: `link ${quiet(key, 30)}` };
129
+ }
130
+ return { where: `pin ${q(key)}`, short: `pin ${quiet(key, 24)}` };
131
+ case "BarChart":
132
+ return { where: `bar ${q(key)}`, short: `bar ${quiet(key, 24)}` };
133
+ case "PieChart": {
134
+ const labels = (el.props.labels as string[]) ?? [];
135
+ const v = (el.props.values as number[])?.[labels.indexOf(key)];
136
+ return { where: `slice ${q(key)}${v !== undefined ? ` (value ${v})` : ""}`, short: `slice ${quiet(key, 24)}` };
137
+ }
138
+ case "LineChart":
139
+ case "AreaChart":
140
+ case "RadarChart":
141
+ if ((m = /^series:(.*)$/.exec(key))) return { where: `series ${q(m[1])}`, short: `series ${quiet(m[1], 24)}` };
142
+ return el.type === "RadarChart" ? { where: `axis ${q(key)}`, short: `axis ${quiet(key, 24)}` } : { where: `point ${q(key)}`, short: `point ${quiet(key, 24)}` };
143
+ }
144
+ return { where: key, short: key };
145
+ }
146
+
147
+ /** Words and identity for one item. `path` is the component's place in the tree; `tab` the tabs it sits under. */
148
+ export function itemInfo(el: El, key: string, path: string, tabs: string[], parentKey?: string): ItemInfo {
149
+ const w = itemWords(el, key);
150
+ const tab = tabs.length ? tabs.map((t) => quiet(t, 30)).join(" > ") : undefined;
151
+ return {
152
+ host: path,
153
+ type: el.type,
154
+ name: el.name,
155
+ key,
156
+ where: w.where,
157
+ short: [el.type, w.short].filter(Boolean).join(" > "),
158
+ line: el.line,
159
+ ...(tab ? { tab } : {}),
160
+ ...(parentKey !== undefined ? { parent: `item:${path}:${parentKey}` } : {}),
161
+ };
162
+ }
163
+
164
+ export const itemId = (path: string, key: string) => `item:${path}:${key}`;
165
+
166
+ /** The item that contains this one: a table cell's row, an ERD column's table, otherwise the whole component. */
167
+ export function parentKeyOf(type: string, key: string): string {
168
+ if (type === "Table") {
169
+ const m = /^cell:(\d+)\./.exec(key);
170
+ if (m) return `row:${m[1]}`;
171
+ }
172
+ if (type === "Erd" && key.includes(".")) return key.split(".")[0];
173
+ return "";
174
+ }
175
+
176
+ // ---------- the message ----------
177
+
178
+ /** A title to name the widget in the message: the first heading, or the first component title. */
179
+ export function widgetTitle(root: El | undefined): string {
180
+ let found = "";
181
+ const walk = (el: El | undefined) => {
182
+ if (!el || found) return;
183
+ if (el.type === "Text" && (el.props.variant === "title" || el.props.variant === "heading")) found = String(el.props.text ?? "");
184
+ else if (typeof el.props.title === "string" && el.props.title) found = el.props.title;
185
+ if (found) return;
186
+ for (const v of Object.values(el.props ?? {})) for (const x of Array.isArray(v) ? v : [v]) if (x && typeof x === "object" && "type" in (x as object) && "props" in (x as object)) walk(x as El);
187
+ };
188
+ walk(root);
189
+ return quiet(found, 60);
190
+ }
191
+
192
+ export function describeNote(n: Note): string {
193
+ const who = n.info.name ? b(n.info.name) : `the ${n.info.type}${n.info.line ? ` at line ${n.info.line}` : ""}`;
194
+ const where = n.info.where ? ` > ${n.info.where}` : "";
195
+ const tab = n.info.tab ? ` [tab "${n.info.tab}"]` : "";
196
+ return `${who} (${n.info.type})${tab}${where}`;
197
+ }
198
+
199
+ /** The text that lands in the message box. Numbered, so the agent can answer each note by its number. */
200
+ export function buildMessage(notes: Note[], title: string): string {
201
+ const lines = [`Notes on the widget${title ? ` "${title}"` : " you drew"} (${notes.length}). Please answer each note by its number. Names in backticks are the variable names in the widget source; table rows count from 1, not counting the header row.`, ""];
202
+ notes.forEach((n, i) => {
203
+ lines.push(`${i + 1}. ${describeNote(n)}`);
204
+ for (const l of n.text.split("\n")) lines.push(` ${l}`);
205
+ });
206
+ return lines.join("\n");
207
+ }
208
+
209
+ // ---------- the tray and the pins ----------
210
+
211
+ const ROLE_PIN = "note.pin";
212
+
213
+ function isBlank(c: Cell | undefined) {
214
+ return !!c && c.ch === " " && c.role === "";
215
+ }
216
+
217
+ /** Draw the number of a note on its element where it does not hide anything: the left end of a box's top border, the cell before a one-line element, or just after it. */
218
+ export function placePin(block: Block, t: Target, n: number): boolean {
219
+ const digits = String(n);
220
+ const w = digits.length;
221
+ const r = t.rect;
222
+ const put = (x: number, y: number): boolean => {
223
+ const row = block.rows[y];
224
+ if (!row || x < 0 || x + w > row.length) return false;
225
+ for (let i = 0; i < w; i++) row[x + i] = cell(digits[i], ROLE_PIN);
226
+ return true;
227
+ };
228
+ const row = block.rows[r.y];
229
+ if (!row) return false;
230
+ // the top border of a box
231
+ if (r.h >= 2) {
232
+ for (let x = r.x + 2; x + w < r.x + r.w - 1; x++) {
233
+ let ok = true;
234
+ for (let i = 0; i < w + 1; i++) {
235
+ const c = row[x + i];
236
+ if (!c || !(c.ch === "─" || c.ch === "╌")) ok = false;
237
+ }
238
+ if (ok) return put(x, r.y);
239
+ }
240
+ }
241
+ // before the element, then after the last character of its first row
242
+ if (r.x - w >= 0 && Array.from({ length: w }, (_, i) => row[r.x - w + i]).every(isBlank)) return put(r.x - w, r.y);
243
+ let xe = Math.min(block.w, r.x + r.w) - 1;
244
+ while (xe >= r.x && isBlank(row[xe])) xe--;
245
+ const after = xe + 1 + (row[xe + 1]?.ch === "◆" ? 2 : 0);
246
+ if (after + w <= block.w && Array.from({ length: w }, (_, i) => row[after + i]).every(isBlank)) return put(after, r.y);
247
+ return false;
248
+ }
249
+
250
+ /** Pins on the elements that have notes, then the tray under the widget: one entry per note and the two buttons. */
251
+ export function decorateNotes(block: Block, state: WState, width: number): Block {
252
+ if (!state.notes.length) return block;
253
+ state.notes.forEach((n, i) => {
254
+ const t = block.targets.find((x) => x.id === n.itemId);
255
+ if (t) placePin(block, t, i + 1);
256
+ });
257
+ const iw = Math.max(20, width - 4);
258
+ const rows: Cell[][] = [];
259
+ const targets: Target[] = [];
260
+ state.notes.forEach((n, i) => {
261
+ const head: Cell[] = [...textCells(`${i + 1}`, ROLE_PIN), cell(" "), ...textCells(truncateText(describeNote(n).replace(/`/g, ""), iw - 4), "notes.where")];
262
+ const body = wrapText(n.text.replace(/\n/g, " ¶ "), iw - 3, "notes.text").slice(0, 2);
263
+ const y0 = rows.length;
264
+ rows.push(head, ...body.map((l) => [cell(" "), cell(" "), cell(" "), ...l]));
265
+ targets.push({ id: `note:${n.id}`, kind: "note", rect: { x: 0, y: y0, w: iw, h: 1 + body.length }, label: `note ${i + 1}`, noteId: n.id });
266
+ });
267
+ const addLabel = `[ Add ${state.notes.length} note${state.notes.length === 1 ? "" : "s"} to your message ]`;
268
+ const clearLabel = "[ Clear ]";
269
+ const btn: Cell[] = [...textCells(addLabel, "btn"), cell(" "), cell(" "), ...textCells(clearLabel, "btn")];
270
+ targets.push({ id: "noteadd", kind: "noteadd", rect: { x: 0, y: rows.length, w: textWidth(addLabel), h: 1 }, label: addLabel });
271
+ targets.push({ id: "noteclear", kind: "noteclear", rect: { x: textWidth(addLabel) + 2, y: rows.length, w: textWidth(clearLabel), h: 1 }, label: clearLabel });
272
+ rows.push(btn);
273
+ const tray = boxed(fromRows(iw, rows, targets), { role: "notes.border", title: `Notes (${state.notes.length})`, titleRole: "notes.title" });
274
+ const out = vstack([block, tray], 0, Math.max(block.w, tray.w));
275
+ return out;
276
+ }
277
+
@@ -0,0 +1,148 @@
1
+ // Dispatcher: a compiled element tree -> a Block (rows of cells + click/focus targets).
2
+ import type { El } from "../lang/library.ts";
3
+ import { barChart, lineArea, pieChart, radarChart } from "./charts.ts";
4
+ import { codeBlock } from "./code.ts";
5
+ import { addWholeItem, button, callout, card, choice, faq, image, metric, stack, tabs, text, withTips, type Anchor } from "./core.ts";
6
+ import { dagBlock, erdBlock, infraBlock, panBlock } from "./diagrams.ts";
7
+ import { geoMap } from "./geo.ts";
8
+ import { markdownBlock } from "./markdown.ts";
9
+ import { sequence } from "./sequence.ts";
10
+ import { decorateNotes } from "./notes.ts";
11
+ import { tableBlock } from "./table.ts";
12
+ import type { Block, Ctx, Services, WState } from "./types.ts";
13
+ import { blank, textBlock, vstack } from "./util.ts";
14
+
15
+ /** Most rows one widget may have. The validation limits keep a widget far below this; it is the backstop, not the plan. */
16
+ export const MAX_ROWS = 1500;
17
+
18
+ function capRows(block: Block, warnings: string[]): Block {
19
+ if (block.rows.length <= MAX_ROWS) return block;
20
+ warnings.push(`${block.rows.length - MAX_ROWS} more rows are not drawn: a widget is limited to ${MAX_ROWS} rows. Show less`);
21
+ return { w: block.w, rows: block.rows.slice(0, MAX_ROWS), targets: block.targets.filter((t) => t.rect.y + t.rect.h <= MAX_ROWS) };
22
+ }
23
+
24
+ export function renderTree(root: El, width: number, state: WState, services: Services = {}): { block: Block; warnings: string[] } {
25
+ const warnings: string[] = [];
26
+ const ctx: Ctx = { state, warnings, services, tabs: [], render: (el, w, path) => renderEl(el, w, path, ctx) };
27
+ const base = capRows(renderEl(root, Math.max(20, width), "0", ctx), warnings);
28
+ const block = decorateNotes(base, state, Math.max(20, width));
29
+ return { block, warnings };
30
+ }
31
+
32
+ export function renderEl(el: El, width: number, path: string, ctx: Ctx): Block {
33
+ try {
34
+ const block = renderInner(el, width, path, ctx);
35
+ addWholeItem(el, block, path, ctx);
36
+ return block;
37
+ } catch (e) {
38
+ ctx.warnings.push(`${el.type} could not be drawn: ${(e as Error).message}`);
39
+ return textBlock(`[${el.type} could not be drawn]`, width, "text.warn");
40
+ }
41
+ }
42
+
43
+ function renderInner(el: El, width: number, path: string, ctx: Ctx): Block {
44
+ const p = el.props;
45
+ switch (el.type) {
46
+ case "Stack":
47
+ return stack(el, width, ctx, path);
48
+ case "Text":
49
+ return text(el, width);
50
+ case "Card":
51
+ return card(el, width, ctx, path);
52
+ case "Callout":
53
+ return callout(el, width);
54
+ case "Button":
55
+ return button(el, width, path);
56
+ case "Choice":
57
+ return choice(el, width, ctx, path);
58
+ case "Tabs":
59
+ return tabs(el, width, ctx, path);
60
+ case "Faq":
61
+ return faq(el, width, ctx, path);
62
+ case "Markdown":
63
+ return markdownBlock(String(p.text ?? ""), width, ctx, path);
64
+ case "CodeBlock": {
65
+ const id = `copy:${path}`;
66
+ return codeBlock(String(p.code ?? ""), p.language, p.title, width, id, ctx.state.copied === id);
67
+ }
68
+ case "Metric": {
69
+ const m = metric(el, width, path);
70
+ return withTips(el, m.block, m.anchors as Map<string, Anchor>, path, ctx, width);
71
+ }
72
+ case "Image": {
73
+ const m = image(el, width, path, ctx);
74
+ return withTips(el, m.block, m.anchors, path, ctx, width);
75
+ }
76
+ case "Table": {
77
+ const t = tableBlock(p.columns ?? [], p.rows ?? [], width);
78
+ const anchors = new Map<string, Anchor>();
79
+ const cols: string[] = (p.columns ?? []).map(String);
80
+ const pad = (c: { x: number; w: number }, y: number): [number, number] => [c.x + c.w, y];
81
+ const lastCol = t.anchors.cols[t.anchors.cols.length - 1];
82
+ t.anchors.rows.forEach((r, i) => anchors.set(`row:${i + 1}`, { rect: r, badge: lastCol ? pad(lastCol, r.y) : undefined }));
83
+ t.anchors.cols.forEach((r, i) => anchors.set(`col:${cols[i]}`, { rect: r, badge: pad(r, r.y) }));
84
+ t.anchors.cells.forEach((rs, ri) => rs.forEach((r, ci) => anchors.set(`cell:${ri + 1}.${cols[ci]}`, { rect: r, badge: pad(r, r.y) })));
85
+ const key = `pan:${path}`;
86
+ const pb = panBlock(t.block, width, key, ctx);
87
+ const dx = pb === t.block ? 0 : ctx.state.pan[key] ?? 0;
88
+ if (dx) {
89
+ for (const [, a] of anchors) {
90
+ a.rect = { ...a.rect, x: a.rect.x - dx };
91
+ if (a.badge) a.badge = [a.badge[0] - dx, a.badge[1]];
92
+ }
93
+ }
94
+ return withTips(el, pb, anchors, path, ctx, width);
95
+ }
96
+ case "BarChart": {
97
+ const b = barChart(el, width);
98
+ return withTips(el, b.block, b.anchors, path, ctx, width);
99
+ }
100
+ case "LineChart":
101
+ case "AreaChart": {
102
+ const c = lineArea(el, width, el.type === "AreaChart");
103
+ return withTips(el, c.block, c.anchors, path, ctx, width);
104
+ }
105
+ case "PieChart": {
106
+ const c = pieChart(el, width);
107
+ return withTips(el, c.block, c.anchors, path, ctx, width);
108
+ }
109
+ case "RadarChart": {
110
+ const c = radarChart(el, width);
111
+ return withTips(el, c.block, c.anchors, path, ctx, width);
112
+ }
113
+ case "GeoMap": {
114
+ const g = geoMap(el, width);
115
+ return withTips(el, g.block, g.anchors, path, ctx, width);
116
+ }
117
+ case "Dag": {
118
+ const d = dagBlock(el, width, path, ctx);
119
+ return withTips(el, d.block, d.anchors, path, ctx, width);
120
+ }
121
+ case "Erd": {
122
+ const d = erdBlock(el, width, path, ctx);
123
+ return withTips(el, d.block, d.anchors, path, ctx, width);
124
+ }
125
+ case "InfraMap": {
126
+ const d = infraBlock(el, width, path, ctx);
127
+ return withTips(el, d.block, d.anchors, path, ctx, width);
128
+ }
129
+ case "SequenceDiagram": {
130
+ const s = sequence(el, width, path);
131
+ const b = panBlock(s.block, width, `pan:${path}`, ctx);
132
+ if (b !== s.block) {
133
+ const pan = b.targets[0]?.pan;
134
+ const dx = pan ? pan.x - b.targets[0].rect.x : 0;
135
+ for (const [, a] of s.anchors) {
136
+ a.rect = { ...a.rect, x: a.rect.x - dx };
137
+ if (a.badge) a.badge = [a.badge[0] - dx, a.badge[1]];
138
+ }
139
+ }
140
+ return withTips(el, b, s.anchors, path, ctx, width);
141
+ }
142
+ default:
143
+ ctx.warnings.push(`${el.type} cannot be drawn on its own`);
144
+ return blank(width, 0);
145
+ }
146
+ }
147
+
148
+ void vstack;