pi-studio 0.9.49 → 0.9.51

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,284 @@
1
+ (() => {
2
+ const FOCUS_MAX_CHARS = 60_000;
3
+
4
+ function clampOffset(value, length) {
5
+ return Math.max(0, Math.min(length, Math.floor(Number(value) || 0)));
6
+ }
7
+
8
+ function truncateFocus(value, maxChars) {
9
+ const source = String(value || "").trim();
10
+ const limit = Math.max(1_000, Math.floor(Number(maxChars) || FOCUS_MAX_CHARS));
11
+ if (source.length <= limit) return { text: source, truncated: false };
12
+ const marker = "\n\n[Pi Studio omitted the middle of this focus snapshot.]\n\n";
13
+ const budget = Math.max(2, limit - marker.length);
14
+ const head = Math.ceil(budget * 0.65);
15
+ const tail = Math.max(1, budget - head);
16
+ return { text: (source.slice(0, head).trimEnd() + marker + source.slice(-tail).trimStart()).slice(0, limit), truncated: true };
17
+ }
18
+
19
+ function lineRecords(text) {
20
+ const records = [];
21
+ let start = 0;
22
+ const source = String(text || "");
23
+ const lines = source.split("\n");
24
+ for (let index = 0; index < lines.length; index += 1) {
25
+ const raw = lines[index];
26
+ const end = start + raw.length;
27
+ records.push({ index: index, text: raw.replace(/\r$/, ""), start: start, end: end });
28
+ start = end + 1;
29
+ }
30
+ return records;
31
+ }
32
+
33
+ function findLineIndex(records, offset) {
34
+ if (!records.length) return 0;
35
+ for (let index = 0; index < records.length; index += 1) {
36
+ if (offset <= records[index].end) return index;
37
+ }
38
+ return records.length - 1;
39
+ }
40
+
41
+ function markdownHeading(line) {
42
+ const match = String(line || "").match(/^\s{0,3}(#{1,6})\s+(.+?)\s*#*\s*$/);
43
+ return match ? { level: match[1].length, label: match[2].trim() } : null;
44
+ }
45
+
46
+ const LATEX_LEVELS = { part: 0, chapter: 1, section: 2, subsection: 3, subsubsection: 4, paragraph: 5, subparagraph: 6 };
47
+ function latexHeading(line) {
48
+ const match = String(line || "").match(/^\s*\\(part|chapter|section|subsection|subsubsection|paragraph|subparagraph)\*?(?:\[[^\]]*\])?\{([^}]*)\}/);
49
+ if (!match) return null;
50
+ return { level: LATEX_LEVELS[match[1]], label: match[2].trim(), command: match[1] };
51
+ }
52
+
53
+ function sectionFromHeadings(text, cursorOffset, headingParser, kind) {
54
+ const records = lineRecords(text);
55
+ const cursorLine = findLineIndex(records, cursorOffset);
56
+ let headingIndex = -1;
57
+ let heading = null;
58
+ for (let index = cursorLine; index >= 0; index -= 1) {
59
+ const candidate = headingParser(records[index].text);
60
+ if (!candidate) continue;
61
+ headingIndex = index;
62
+ heading = candidate;
63
+ break;
64
+ }
65
+ if (!heading) return null;
66
+ let endIndex = records.length;
67
+ for (let index = headingIndex + 1; index < records.length; index += 1) {
68
+ const candidate = headingParser(records[index].text);
69
+ if (candidate && candidate.level <= heading.level) {
70
+ endIndex = index;
71
+ break;
72
+ }
73
+ }
74
+ const start = records[headingIndex].start;
75
+ const end = endIndex < records.length ? records[endIndex].start : String(text || "").length;
76
+ return { kind: "section", sectionKind: kind, label: heading.label || "untitled", text: String(text || "").slice(start, end).trim(), start: start, end: end };
77
+ }
78
+
79
+ function paragraphAroundCursor(text, cursorOffset) {
80
+ const source = String(text || "");
81
+ const records = lineRecords(source);
82
+ const cursorLine = findLineIndex(records, cursorOffset);
83
+ let startLine = cursorLine;
84
+ let endLine = cursorLine + 1;
85
+ while (startLine > 0 && records[startLine - 1].text.trim()) startLine -= 1;
86
+ while (endLine < records.length && records[endLine].text.trim()) endLine += 1;
87
+ let start = records[startLine] ? records[startLine].start : 0;
88
+ let end = endLine < records.length ? records[endLine].start : source.length;
89
+ let excerpt = source.slice(start, end).trim();
90
+ if (excerpt.length < 120 && source.length > excerpt.length) {
91
+ start = Math.max(0, cursorOffset - 4_000);
92
+ end = Math.min(source.length, cursorOffset + 4_000);
93
+ excerpt = source.slice(start, end).trim();
94
+ }
95
+ return { kind: "section", sectionKind: "passage", label: "Text around cursor", text: excerpt, start: start, end: end };
96
+ }
97
+
98
+ function findStudioSideQuestionSection(text, cursorOffset, language) {
99
+ const source = String(text || "");
100
+ const cursor = clampOffset(cursorOffset, source.length);
101
+ const lang = String(language || "").trim().toLowerCase();
102
+ if (lang === "latex" || lang === "tex") {
103
+ return sectionFromHeadings(source, cursor, latexHeading, "latex") || paragraphAroundCursor(source, cursor);
104
+ }
105
+ if (!lang || lang === "markdown" || lang === "md" || lang === "qmd" || lang === "mdx") {
106
+ return sectionFromHeadings(source, cursor, markdownHeading, "markdown") || paragraphAroundCursor(source, cursor);
107
+ }
108
+ return paragraphAroundCursor(source, cursor);
109
+ }
110
+
111
+ function chooseStudioSideQuestionFocus(options) {
112
+ const input = options && typeof options === "object" ? options : {};
113
+ const editorText = String(input.editorText || "");
114
+ const responseText = String(input.responseText || "");
115
+ const start = clampOffset(input.selectionStart, editorText.length);
116
+ const end = Math.max(start, clampOffset(input.selectionEnd, editorText.length));
117
+ const selection = editorText.slice(start, end).trim();
118
+ const requestedMode = String(input.mode || "auto").trim().toLowerCase();
119
+ const mode = requestedMode === "auto" ? (selection ? "selection" : "section") : requestedMode;
120
+
121
+ if (mode === "none") return { focusKind: "none", focusLabel: "No starting text", focusText: "", truncated: false };
122
+ if (mode === "response") {
123
+ if (!responseText.trim()) return { focusKind: "none", focusLabel: "No displayed response", focusText: "", truncated: false };
124
+ const bounded = truncateFocus(responseText);
125
+ return { focusKind: "response", focusLabel: "Displayed response", focusText: bounded.text, truncated: bounded.truncated };
126
+ }
127
+ if (mode === "selection") {
128
+ if (!selection) return { focusKind: "none", focusLabel: "No editor text selected", focusText: "", truncated: false };
129
+ const bounded = truncateFocus(selection);
130
+ return { focusKind: "selection", focusLabel: "Editor selection", focusText: bounded.text, truncated: bounded.truncated, start: start, end: end };
131
+ }
132
+ if (mode === "section") {
133
+ const section = findStudioSideQuestionSection(editorText, end || start, input.language);
134
+ if (section && section.text.trim()) {
135
+ const bounded = truncateFocus(section.text);
136
+ const label = section.sectionKind === "passage"
137
+ ? "Text around cursor"
138
+ : "Text under “" + String(section.label || "untitled").slice(0, 160) + "”";
139
+ return { focusKind: "section", focusLabel: label, focusText: bounded.text, truncated: bounded.truncated, start: section.start, end: section.end };
140
+ }
141
+ return { focusKind: "none", focusLabel: "No editor text at cursor", focusText: "", truncated: false };
142
+ }
143
+ if (!editorText.trim()) return { focusKind: "none", focusLabel: "Editor is empty", focusText: "", truncated: false };
144
+ const bounded = truncateFocus(editorText);
145
+ return { focusKind: "editor", focusLabel: "Whole editor document", focusText: bounded.text, truncated: bounded.truncated, start: 0, end: editorText.length };
146
+ }
147
+
148
+ function getDefaultStudioSideQuestionGatherScope(options) {
149
+ const input = options && typeof options === "object" ? options : {};
150
+ if (String(input.sourcePath || "").trim() || String(input.resourceDir || "").trim()) return "folder";
151
+ return "none";
152
+ }
153
+
154
+ function normalizeTranscriptDate(value, fallback) {
155
+ const date = value instanceof Date ? value : new Date(value == null ? fallback : value);
156
+ return Number.isFinite(date.getTime()) ? date : new Date(fallback);
157
+ }
158
+
159
+ function formatTranscriptTimestamp(value) {
160
+ const date = normalizeTranscriptDate(value, Date.now());
161
+ try { return date.toISOString(); } catch { return "unknown time"; }
162
+ }
163
+
164
+ function formatTranscriptFilename(value) {
165
+ const date = normalizeTranscriptDate(value, Date.now());
166
+ const pad = (part) => String(part).padStart(2, "0");
167
+ return "side-questions-"
168
+ + date.getFullYear()
169
+ + pad(date.getMonth() + 1)
170
+ + pad(date.getDate())
171
+ + "-"
172
+ + pad(date.getHours())
173
+ + pad(date.getMinutes())
174
+ + pad(date.getSeconds())
175
+ + ".md";
176
+ }
177
+
178
+ function escapeTranscriptInline(value) {
179
+ return String(value == null ? "" : value)
180
+ .replace(/[\r\n]+/g, " ")
181
+ .replace(/\\/g, "\\\\")
182
+ .replace(/([`*_\[\]<>|])/g, "\\$1")
183
+ .trim();
184
+ }
185
+
186
+ function formatTranscriptContextScope(context) {
187
+ if (!context || context.gatherScope === "none") return "No related files";
188
+ return context.contextRoot ? String(context.contextRoot) : String(context.gatherScope || "Local context");
189
+ }
190
+
191
+ function buildStudioSideQuestionTranscriptMarkdown(stateInput, options) {
192
+ const state = stateInput && typeof stateInput === "object" ? stateInput : {};
193
+ const context = state.context && typeof state.context === "object" ? state.context : {};
194
+ const messages = Array.isArray(state.messages) ? state.messages : [];
195
+ const activity = Array.isArray(state.activity) ? state.activity : [];
196
+ const settings = options && typeof options === "object" ? options : {};
197
+ const exportedAt = normalizeTranscriptDate(settings.exportedAt, Date.now());
198
+ const lines = [
199
+ "# Side questions",
200
+ "",
201
+ "_Exported from Pi Studio on " + formatTranscriptTimestamp(exportedAt) + "._",
202
+ "",
203
+ "> This export contains the visible side-thread transcript and context/activity labels. It does not include hidden starting-text contents, inherited main-conversation contents, or raw tool output.",
204
+ "",
205
+ "## Context",
206
+ "",
207
+ ];
208
+
209
+ if (Number.isFinite(state.createdAt)) lines.push("- Thread started: " + formatTranscriptTimestamp(state.createdAt));
210
+ if (state.modelLabel) lines.push("- Model: " + escapeTranscriptInline(state.modelLabel));
211
+ if (state.thinking) lines.push("- Thinking: " + escapeTranscriptInline(state.thinking));
212
+ lines.push("- Starting text: " + escapeTranscriptInline(context.focusLabel || "No starting text"));
213
+ lines.push("- Related files: " + escapeTranscriptInline(formatTranscriptContextScope(context)));
214
+ lines.push("- Main conversation snapshot: " + (context.includeConversation === true ? "included" : "not included"));
215
+
216
+ const gitSnapshot = context.gitSnapshot && typeof context.gitSnapshot === "object" ? context.gitSnapshot : null;
217
+ if (gitSnapshot) {
218
+ const gitParts = [gitSnapshot.branch || "repository"];
219
+ if (gitSnapshot.head) gitParts.push("HEAD " + gitSnapshot.head);
220
+ if (Number.isFinite(gitSnapshot.changeCount)) gitParts.push(String(Math.max(0, Math.floor(gitSnapshot.changeCount))) + " changes");
221
+ if (Number.isFinite(gitSnapshot.recentCommitCount)) gitParts.push(String(Math.max(0, Math.floor(gitSnapshot.recentCommitCount))) + " recent commits");
222
+ if (gitSnapshot.capturedAt) gitParts.push("captured " + formatTranscriptTimestamp(gitSnapshot.capturedAt));
223
+ if (gitSnapshot.truncated === true) gitParts.push("bounded output truncated");
224
+ lines.push("- Git snapshot: " + escapeTranscriptInline(gitParts.join(" · ")));
225
+ } else {
226
+ lines.push("- Git snapshot: not included");
227
+ }
228
+
229
+ const webLabel = context.webSearchRequested === true
230
+ ? (context.webSearchAvailable === true ? "allowed" : "requested but unavailable")
231
+ : "not allowed";
232
+ lines.push("- Web search: " + webLabel);
233
+ const tools = Array.isArray(context.tools) ? context.tools : [];
234
+ lines.push("- Additional Pi tools: " + (tools.length
235
+ ? tools.map((tool) => {
236
+ const name = tool && tool.name ? String(tool.name) : "unnamed tool";
237
+ const source = tool && tool.source ? String(tool.source) : "";
238
+ return escapeTranscriptInline(source ? name + " (" + source + ")" : name);
239
+ }).join(", ")
240
+ : "none"));
241
+
242
+ lines.push("", "## Discussion", "");
243
+ let questionCount = 0;
244
+ let answerCount = 0;
245
+ for (const message of messages) {
246
+ if (!message || typeof message !== "object") continue;
247
+ const role = message.role === "assistant" ? "assistant" : "user";
248
+ if (role === "assistant") answerCount += 1;
249
+ else questionCount += 1;
250
+ const number = role === "assistant" ? answerCount : questionCount;
251
+ const status = message.status === "streaming" || message.status === "error" ? message.status : "complete";
252
+ const heading = role === "assistant" ? "Side answer " + number : "Question " + number;
253
+ lines.push("### " + heading + (status === "complete" ? "" : " (" + status + ")"), "");
254
+ if (Number.isFinite(message.createdAt)) lines.push("_" + formatTranscriptTimestamp(message.createdAt) + "_", "");
255
+ const text = typeof message.text === "string" ? message.text.trim() : "";
256
+ lines.push(text || "_[No text captured.]_", "");
257
+ }
258
+ if (!messages.length) lines.push("_[No side-thread messages captured.]_", "");
259
+
260
+ if (activity.length) {
261
+ lines.push("## Context activity", "");
262
+ for (const entry of activity) {
263
+ if (!entry || typeof entry !== "object") continue;
264
+ const status = entry.status === "running" || entry.status === "error" ? entry.status : "complete";
265
+ const label = escapeTranscriptInline(entry.label || "Context action");
266
+ const toolName = entry.toolName ? " (`" + String(entry.toolName).replace(/`/g, "\\`") + "`)" : "";
267
+ lines.push("- " + status.charAt(0).toUpperCase() + status.slice(1) + ": " + label + toolName);
268
+ }
269
+ lines.push("");
270
+ }
271
+
272
+ return lines.join("\n").replace(/\n{3,}$/g, "\n\n").trimEnd() + "\n";
273
+ }
274
+
275
+ globalThis.PiStudioSideQuestionHelpers = Object.freeze({
276
+ FOCUS_MAX_CHARS: FOCUS_MAX_CHARS,
277
+ buildStudioSideQuestionTranscriptMarkdown: buildStudioSideQuestionTranscriptMarkdown,
278
+ chooseStudioSideQuestionFocus: chooseStudioSideQuestionFocus,
279
+ findStudioSideQuestionSection: findStudioSideQuestionSection,
280
+ formatStudioSideQuestionTranscriptFilename: formatTranscriptFilename,
281
+ getDefaultStudioSideQuestionGatherScope: getDefaultStudioSideQuestionGatherScope,
282
+ truncateFocus: truncateFocus,
283
+ });
284
+ })();