pi-studio 0.9.55 → 0.9.57

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,227 @@
1
+ import { createHash } from "node:crypto";
2
+
3
+ export const REPL_SUBMISSION_DISPLAY_VERSION = 1;
4
+ export const DEFAULT_REPL_SUBMISSION_ECHO_MODE = "off";
5
+ export const REPL_SUBMISSION_ECHO_MODES = Object.freeze(["off", "summary", "full"]);
6
+ export const REPL_SUBMISSION_SUMMARY_MAX_CHARS = 600;
7
+ export const REPL_SUBMISSION_SUMMARY_MAX_LINES = 6;
8
+ export const REPL_SUBMISSION_FULL_MAX_CHARS = 4_000;
9
+ export const REPL_SUBMISSION_FULL_MAX_LINES = 40;
10
+
11
+ const UNSAFE_UNICODE_PATTERN = /[\u2028\u2029\u202a-\u202e\u2066-\u2069]/g;
12
+ const OTHER_CONTROL_PATTERN = /[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f-\u009f]/g;
13
+ const COMPACT_BEGIN_MARKER_PATTERN = /^── ([a-z0-9][a-z0-9-]{0,31}) · ([a-f0-9]{12}) · ([1-9]\d*) (line|lines) ──$/;
14
+ const COMPACT_OUTPUT_MARKER = "── output ──";
15
+ const COMPACT_END_MARKER_PATTERN = /^── done · ([a-f0-9]{12}) ──$/;
16
+ const LEGACY_MARKER_PATTERN = /^── ([a-z0-9][a-z0-9-]{0,31}) (submitted|output|complete)(?: · ([1-9]\d*) (line|lines))? · ([a-f0-9]{12}) ──$/;
17
+
18
+ export function normalizeReplSubmissionEchoMode(value, fallback = DEFAULT_REPL_SUBMISSION_ECHO_MODE) {
19
+ const normalizedFallback = REPL_SUBMISSION_ECHO_MODES.includes(String(fallback || "").trim().toLowerCase())
20
+ ? String(fallback).trim().toLowerCase()
21
+ : DEFAULT_REPL_SUBMISSION_ECHO_MODE;
22
+ const normalized = String(value || "").trim().toLowerCase();
23
+ return REPL_SUBMISSION_ECHO_MODES.includes(normalized) ? normalized : normalizedFallback;
24
+ }
25
+
26
+ function escapeCodePoint(char) {
27
+ const codePoint = char.codePointAt(0) ?? 0;
28
+ return codePoint <= 0xff
29
+ ? `\\x${codePoint.toString(16).padStart(2, "0")}`
30
+ : `\\u{${codePoint.toString(16)}}`;
31
+ }
32
+
33
+ export function sanitizeReplSubmissionDisplayText(value) {
34
+ return String(value || "")
35
+ .replace(/\r\n?/g, "\n")
36
+ .replace(/\t/g, " ")
37
+ .replace(OTHER_CONTROL_PATTERN, escapeCodePoint)
38
+ .replace(UNSAFE_UNICODE_PATTERN, escapeCodePoint);
39
+ }
40
+
41
+ function truncateCodePoints(value, maxChars) {
42
+ const chars = Array.from(String(value || ""));
43
+ if (chars.length <= maxChars) return { text: chars.join(""), truncated: false };
44
+ return { text: `${chars.slice(0, Math.max(0, maxChars - 1)).join("")}…`, truncated: true };
45
+ }
46
+
47
+ function normalizeDisplayCode(code) {
48
+ const sanitized = sanitizeReplSubmissionDisplayText(code)
49
+ .split("\n")
50
+ .map((line) => line.replace(/ +$/g, ""))
51
+ .join("\n")
52
+ .replace(/\n+$/, "");
53
+ return sanitized || "(empty submission)";
54
+ }
55
+
56
+ function formatPreviewLine(line) {
57
+ return line ? `│ ${line}` : "│";
58
+ }
59
+
60
+ function buildBoundedPreviewLines(codeLines, maxLines, maxChars) {
61
+ const shown = [];
62
+ let usedChars = 0;
63
+ let truncated = false;
64
+ for (let index = 0; index < codeLines.length; index += 1) {
65
+ if (shown.length >= maxLines) {
66
+ truncated = true;
67
+ break;
68
+ }
69
+ const line = codeLines[index];
70
+ const remaining = maxChars - usedChars;
71
+ if (remaining <= 0) {
72
+ truncated = true;
73
+ break;
74
+ }
75
+ const clipped = truncateCodePoints(line, remaining);
76
+ shown.push(formatPreviewLine(clipped.text));
77
+ usedChars += Array.from(clipped.text).length + 1;
78
+ if (clipped.truncated) {
79
+ truncated = true;
80
+ break;
81
+ }
82
+ }
83
+ if (shown.length < codeLines.length) truncated = true;
84
+ if (truncated) shown.push(`│ … preview truncated; ${codeLines.length} ${codeLines.length === 1 ? "line" : "lines"} total`);
85
+ return shown;
86
+ }
87
+
88
+ export function createReplSubmissionDisplay(details = {}) {
89
+ const mode = normalizeReplSubmissionEchoMode(details.mode);
90
+ const origin = String(details.origin || "pi")
91
+ .trim()
92
+ .toLowerCase()
93
+ .replace(/[^a-z0-9-]+/g, "-")
94
+ .replace(/^-+|-+$/g, "")
95
+ .slice(0, 32) || "pi";
96
+ const entryId = String(details.entryId || "");
97
+ const anchorId = createHash("sha256")
98
+ .update(`pi-repl-submission-display-v${REPL_SUBMISSION_DISPLAY_VERSION}\0${entryId}`, "utf8")
99
+ .digest("hex")
100
+ .slice(0, 12);
101
+ const displayCode = normalizeDisplayCode(details.code);
102
+ const codeLines = displayCode.split("\n");
103
+ const lineLabel = `${codeLines.length} ${codeLines.length === 1 ? "line" : "lines"}`;
104
+ const beginMarker = `── ${origin} · ${anchorId} · ${lineLabel} ──`;
105
+ const outputMarker = COMPACT_OUTPUT_MARKER;
106
+ const endMarker = `── done · ${anchorId} ──`;
107
+ const enabled = mode !== "off";
108
+ const previewLines = !enabled
109
+ ? []
110
+ : mode === "full"
111
+ ? buildBoundedPreviewLines(codeLines, REPL_SUBMISSION_FULL_MAX_LINES, REPL_SUBMISSION_FULL_MAX_CHARS)
112
+ : buildBoundedPreviewLines(codeLines, REPL_SUBMISSION_SUMMARY_MAX_LINES, REPL_SUBMISSION_SUMMARY_MAX_CHARS);
113
+ return {
114
+ version: REPL_SUBMISSION_DISPLAY_VERSION,
115
+ mode,
116
+ enabled,
117
+ origin,
118
+ entryId,
119
+ anchorId,
120
+ beginMarker,
121
+ outputMarker,
122
+ endMarker,
123
+ previewLines,
124
+ prefixLines: enabled ? [beginMarker, ...previewLines, outputMarker] : [],
125
+ };
126
+ }
127
+
128
+ function parseLineCount(countText, countLabel) {
129
+ const lineCount = Number(countText);
130
+ if (!Number.isSafeInteger(lineCount) || lineCount < 1) return null;
131
+ if ((lineCount === 1) !== (countLabel === "line")) return null;
132
+ return lineCount;
133
+ }
134
+
135
+ export function parseReplSubmissionDisplayMarker(line) {
136
+ const normalized = String(line || "").replace(/\r$/, "");
137
+ const compactBegin = normalized.match(COMPACT_BEGIN_MARKER_PATTERN);
138
+ if (compactBegin) {
139
+ const [, origin, anchorId, countText, countLabel] = compactBegin;
140
+ const lineCount = parseLineCount(countText, countLabel);
141
+ return lineCount === null
142
+ ? null
143
+ : { version: REPL_SUBMISSION_DISPLAY_VERSION, origin, phase: "submitted", anchorId, lineCount };
144
+ }
145
+ if (normalized === COMPACT_OUTPUT_MARKER) {
146
+ return { version: REPL_SUBMISSION_DISPLAY_VERSION, phase: "output" };
147
+ }
148
+ const compactEnd = normalized.match(COMPACT_END_MARKER_PATTERN);
149
+ if (compactEnd) {
150
+ return { version: REPL_SUBMISSION_DISPLAY_VERSION, phase: "complete", anchorId: compactEnd[1] };
151
+ }
152
+
153
+ // Accept the earlier three-marker development format so raw histories made
154
+ // while testing it remain interpretable; new displays do not emit this form.
155
+ const legacy = normalized.match(LEGACY_MARKER_PATTERN);
156
+ if (!legacy) return null;
157
+ const [, origin, phase, countText, countLabel, anchorId] = legacy;
158
+ if (phase === "submitted") {
159
+ if (!countText) return null;
160
+ const lineCount = parseLineCount(countText, countLabel);
161
+ return lineCount === null
162
+ ? null
163
+ : { version: REPL_SUBMISSION_DISPLAY_VERSION, origin, phase, anchorId, lineCount, legacy: true };
164
+ }
165
+ if (countText) return null;
166
+ return { version: REPL_SUBMISSION_DISPLAY_VERSION, origin, phase, anchorId, legacy: true };
167
+ }
168
+
169
+ function consumeExactDisplayLine(value, offset, line) {
170
+ if (!value.startsWith(line, offset)) return null;
171
+ let end = offset + line.length;
172
+ while (end < value.length && (value[end] === " " || value[end] === "\t")) end += 1;
173
+ if (end < value.length && value[end] !== "\n") return null;
174
+ return value[end] === "\n" ? end + 1 : end;
175
+ }
176
+
177
+ function findExactDisplayLine(value, line, useLast = false) {
178
+ let index = useLast ? value.lastIndexOf(line) : value.indexOf(line);
179
+ while (index >= 0) {
180
+ const startsLine = index === 0 || value[index - 1] === "\n";
181
+ const end = consumeExactDisplayLine(value, index, line);
182
+ if (startsLine && end !== null) return { index, end };
183
+ if (useLast) {
184
+ if (index === 0) break;
185
+ index = value.lastIndexOf(line, index - 1);
186
+ } else {
187
+ index = value.indexOf(line, index + line.length);
188
+ }
189
+ }
190
+ return null;
191
+ }
192
+
193
+ function removeMarkerLine(value, marker, useLast = false) {
194
+ const found = findExactDisplayLine(value, marker, useLast);
195
+ return found ? value.slice(0, found.index) + value.slice(found.end) : value;
196
+ }
197
+
198
+ export function stripReplSubmissionDisplay(output, display) {
199
+ let value = String(output || "").replace(/\r\n?/g, "\n");
200
+ if (!display || display.enabled !== true) return value;
201
+ const begin = findExactDisplayLine(value, display.beginMarker);
202
+ if (begin) {
203
+ const beginIndex = begin.index;
204
+ const afterBegin = value.slice(begin.end);
205
+ const outputDivider = display.outputMarker
206
+ ? findExactDisplayLine(afterBegin, display.outputMarker)
207
+ : null;
208
+ if (outputDivider) {
209
+ // The plain divider is an unambiguous boundary because it is emitted
210
+ // before user code runs. Removing through it is resilient to terminal
211
+ // wrapping or whitespace changes within the displayed source preview.
212
+ const suffixStart = begin.end + outputDivider.end;
213
+ value = value.slice(0, beginIndex) + value.slice(suffixStart);
214
+ } else {
215
+ // If the runtime disappears mid-prefix, remove only contiguous exact
216
+ // request lines and preserve any different error text after them.
217
+ let suffixStart = beginIndex;
218
+ for (const line of display.prefixLines) {
219
+ const next = consumeExactDisplayLine(value, suffixStart, line);
220
+ if (next === null) break;
221
+ suffixStart = next;
222
+ }
223
+ value = value.slice(0, beginIndex) + value.slice(suffixStart);
224
+ }
225
+ }
226
+ return removeMarkerLine(value, display.endMarker, true);
227
+ }