pi-studio 0.9.46 → 0.9.48

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/client/studio.css CHANGED
@@ -148,7 +148,7 @@
148
148
  background: var(--panel-2);
149
149
  }
150
150
 
151
- button, select, .file-label {
151
+ button, select {
152
152
  border: 1px solid var(--control-border);
153
153
  background: var(--panel);
154
154
  color: var(--text);
@@ -163,14 +163,12 @@
163
163
  }
164
164
 
165
165
  button:not(:disabled):hover,
166
- select:hover,
167
- .file-label:hover {
166
+ select:hover {
168
167
  background: var(--panel-2);
169
168
  }
170
169
 
171
170
  button:focus-visible,
172
- select:focus-visible,
173
- .file-label:focus-within {
171
+ select:focus-visible {
174
172
  outline: 2px solid var(--accent-soft-strong);
175
173
  outline-offset: 1px;
176
174
  }
@@ -229,15 +227,8 @@
229
227
  filter: none;
230
228
  }
231
229
 
232
- .file-label {
233
- cursor: pointer;
234
- display: inline-flex;
235
- align-items: center;
236
- gap: 6px;
237
- }
238
-
239
- .file-label input {
240
- display: none;
230
+ .file-input-hidden {
231
+ display: none !important;
241
232
  }
242
233
 
243
234
  main {
@@ -1749,6 +1740,7 @@
1749
1740
 
1750
1741
  .rendered-markdown .studio-pdf-card-header {
1751
1742
  display: flex;
1743
+ flex-wrap: wrap;
1752
1744
  align-items: center;
1753
1745
  justify-content: space-between;
1754
1746
  gap: 10px;
@@ -1777,10 +1769,13 @@
1777
1769
  }
1778
1770
 
1779
1771
  .rendered-markdown .studio-pdf-card-actions {
1780
- flex: 0 0 auto;
1772
+ flex: 0 1 auto;
1781
1773
  display: inline-flex;
1774
+ flex-wrap: wrap;
1775
+ justify-content: flex-end;
1782
1776
  align-items: center;
1783
1777
  gap: 8px;
1778
+ margin-left: auto;
1784
1779
  }
1785
1780
 
1786
1781
  .rendered-markdown .studio-pdf-card-link,
@@ -1907,6 +1902,7 @@
1907
1902
  .studio-pdf-focus-header {
1908
1903
  flex: 0 0 auto;
1909
1904
  display: flex;
1905
+ flex-wrap: wrap;
1910
1906
  align-items: center;
1911
1907
  justify-content: space-between;
1912
1908
  gap: 12px;
@@ -1934,10 +1930,13 @@
1934
1930
  }
1935
1931
 
1936
1932
  .studio-pdf-focus-actions {
1937
- flex: 0 0 auto;
1933
+ flex: 0 1 auto;
1938
1934
  display: inline-flex;
1935
+ flex-wrap: wrap;
1936
+ justify-content: flex-end;
1939
1937
  align-items: center;
1940
1938
  gap: 8px;
1939
+ margin-left: auto;
1941
1940
  }
1942
1941
 
1943
1942
  .studio-pdf-focus-link,
@@ -2909,6 +2908,10 @@
2909
2908
  font-style: italic;
2910
2909
  }
2911
2910
 
2911
+ .studio-latex-compatibility-warning {
2912
+ margin: 0 0 0.85em;
2913
+ }
2914
+
2912
2915
  .marker {
2913
2916
  display: inline-block;
2914
2917
  padding: 0 4px;
@@ -5362,13 +5365,13 @@
5362
5365
  }
5363
5366
 
5364
5367
  body.studio-ui-refresh > header button,
5365
- body.studio-ui-refresh > header .file-label,
5366
5368
  body.studio-ui-refresh #responseActions button,
5367
5369
  body.studio-ui-refresh #responseActions select {
5368
5370
  padding: 5px 7px;
5369
5371
  font-size: 12px;
5370
5372
  }
5371
5373
 
5374
+
5372
5375
  body.studio-ui-refresh main {
5373
5376
  gap: 0;
5374
5377
  padding: 9px;
@@ -6101,6 +6104,102 @@
6101
6104
  font-weight: 700;
6102
6105
  }
6103
6106
 
6107
+ .studio-decision-overlay {
6108
+ position: fixed;
6109
+ inset: 0;
6110
+ z-index: 13000;
6111
+ display: grid;
6112
+ place-items: center;
6113
+ padding: 20px;
6114
+ background: rgba(0, 0, 0, 0.58);
6115
+ backdrop-filter: blur(2px);
6116
+ }
6117
+
6118
+ .studio-decision-overlay[hidden] {
6119
+ display: none !important;
6120
+ }
6121
+
6122
+ .studio-decision-dialog {
6123
+ width: min(560px, calc(100vw - 32px));
6124
+ max-height: min(80vh, 680px);
6125
+ overflow: auto;
6126
+ padding: 20px;
6127
+ border: 1px solid var(--panel-border);
6128
+ border-radius: 14px;
6129
+ background: var(--panel);
6130
+ color: var(--text);
6131
+ box-shadow: 0 24px 70px rgba(0, 0, 0, 0.48);
6132
+ }
6133
+
6134
+ .studio-decision-title {
6135
+ margin: 0;
6136
+ color: var(--text);
6137
+ font-size: 17px;
6138
+ line-height: 1.3;
6139
+ }
6140
+
6141
+ .studio-decision-message {
6142
+ margin-top: 10px;
6143
+ color: var(--studio-info-text, var(--muted));
6144
+ font-size: 13px;
6145
+ line-height: 1.5;
6146
+ white-space: pre-wrap;
6147
+ overflow-wrap: anywhere;
6148
+ }
6149
+
6150
+ .studio-decision-input {
6151
+ width: 100%;
6152
+ margin-top: 16px;
6153
+ padding: 9px 10px;
6154
+ border: 1px solid var(--control-border);
6155
+ border-radius: 8px;
6156
+ background: var(--panel-2);
6157
+ color: var(--text);
6158
+ font-family: var(--font-mono);
6159
+ font-size: 13px;
6160
+ }
6161
+
6162
+ .studio-decision-input[hidden] {
6163
+ display: none !important;
6164
+ }
6165
+
6166
+ .studio-decision-input:focus {
6167
+ border-color: var(--accent);
6168
+ outline: 2px solid var(--accent-soft);
6169
+ outline-offset: 1px;
6170
+ }
6171
+
6172
+ .studio-decision-actions {
6173
+ display: flex;
6174
+ flex-wrap: wrap;
6175
+ justify-content: flex-end;
6176
+ gap: 8px;
6177
+ margin-top: 18px;
6178
+ }
6179
+
6180
+ .studio-decision-confirm:not(.is-destructive) {
6181
+ border-color: var(--accent);
6182
+ background: var(--accent);
6183
+ color: var(--accent-contrast);
6184
+ font-weight: 650;
6185
+ }
6186
+
6187
+ .studio-decision-confirm.is-destructive {
6188
+ border-color: var(--warn-border);
6189
+ background: color-mix(in srgb, var(--warn) 15%, var(--panel));
6190
+ color: var(--warn);
6191
+ font-weight: 650;
6192
+ }
6193
+
6194
+ .studio-decision-confirm.is-destructive:hover,
6195
+ .studio-decision-confirm.is-destructive:focus-visible {
6196
+ background: color-mix(in srgb, var(--warn) 24%, var(--panel));
6197
+ }
6198
+
6199
+ body.studio-decision-open {
6200
+ overflow: hidden;
6201
+ }
6202
+
6104
6203
  .studio-preview-link-menu {
6105
6204
  position: fixed;
6106
6205
  z-index: 12000;
package/index.ts CHANGED
@@ -29,6 +29,8 @@ import {
29
29
  } from "./shared/studio-markdown-latex-literals.js";
30
30
  import { escapeStudioPdfLatexTextFragment } from "./shared/studio-pdf-escape.js";
31
31
  import { resolveStudioPdfResourceFile } from "./shared/studio-pdf-resource.js";
32
+ import { createStudioPandocHtmlResourceFlagResolver } from "./shared/studio-pandoc-resource-flag.js";
33
+ import { prepareStudioLatexForPandoc } from "./shared/studio-latex-pandoc-compat.js";
32
34
  import { isStudioCmuxSession, openStudioUrlInBrowser } from "./shared/studio-browser-launcher.js";
33
35
  import { buildStudioReplTmuxStartArgs } from "./shared/studio-repl-tmux.js";
34
36
  import { buildStudioForwardingHint, buildStudioSshTunnelHint, isStudioSshSession as isSshSession } from "./shared/studio-ssh-hint.js";
@@ -647,6 +649,7 @@ const STUDIO_QUIZ_CONTEXT_MAX_FILES = 18;
647
649
  const STUDIO_QUIZ_SNIPPET_MAX_CHARS = 8_000;
648
650
  const STUDIO_QUIZ_DISCUSSION_MAX_CHARS = 6_000;
649
651
  const REQUEST_BODY_MAX_BYTES = 1_000_000;
652
+ const STUDIO_IMPORT_FILE_MAX_BYTES = 10_000_000;
650
653
  const STUDIO_WORKSPACE_STATE_REQUEST_MAX_BYTES = 4_000_000;
651
654
  const RESPONSE_HISTORY_LIMIT = 30;
652
655
  const CMUX_NOTIFY_TIMEOUT_MS = 1200;
@@ -6126,28 +6129,45 @@ function decorateStudioPandocSyntaxHtml(html: string): string {
6126
6129
  );
6127
6130
  }
6128
6131
 
6129
- const studioPandocHtmlResourceFlagCache = new Map<string, Promise<"--embed-resources" | "--self-contained">>();
6132
+ function buildStudioLatexPandocCompatibilityWarning(omittedPackages: Array<{ name: string }>): string | undefined {
6133
+ const packageNames = Array.from(new Set(
6134
+ omittedPackages
6135
+ .map((entry) => String(entry?.name ?? "").trim())
6136
+ .filter(Boolean),
6137
+ ));
6138
+ if (packageNames.length === 0) return undefined;
6139
+ const packageLabel = packageNames
6140
+ .map((name) => name.toLowerCase().endsWith(".sty") ? name : `${name}.sty`)
6141
+ .join(", ");
6142
+ const noun = packageNames.length === 1 ? "package" : "packages";
6143
+ const subject = packageNames.length === 1 ? "it redefines" : "they redefine";
6144
+ return `Studio omitted local LaTeX ${noun} ${packageLabel} from this Pandoc rendering because ${subject} document startup. Package-specific layout may be absent; compile the source directly with LaTeX for authoritative output.`;
6145
+ }
6130
6146
 
6131
- async function getStudioPandocHtmlResourceFlag(pandocCommand: string): Promise<"--embed-resources" | "--self-contained"> {
6132
- let cached = studioPandocHtmlResourceFlagCache.get(pandocCommand);
6133
- if (!cached) {
6134
- cached = runStudioSubprocess(pandocCommand, ["--help"], {
6135
- timeoutMs: 5_000,
6136
- stdoutMaxBytes: 250_000,
6137
- stderrMaxBytes: 20_000,
6138
- label: "pandoc capability probe",
6139
- notFoundMessage: "pandoc was not found. Install pandoc or set PANDOC_PATH to the pandoc binary.",
6140
- }).then((result) => {
6141
- if (result.code !== 0) {
6142
- throw new Error(`pandoc capability probe failed with exit code ${result.code}${result.stderr ? `: ${result.stderr}` : ""}`);
6143
- }
6144
- return result.stdout.includes("--embed-resources") ? "--embed-resources" : "--self-contained";
6145
- });
6146
- studioPandocHtmlResourceFlagCache.set(pandocCommand, cached);
6147
- }
6148
- return cached;
6147
+ function combineStudioWarnings(...warnings: Array<string | undefined>): string | undefined {
6148
+ const messages = Array.from(new Set(warnings.map((warning) => String(warning ?? "").trim()).filter(Boolean)));
6149
+ return messages.length > 0 ? messages.join(" ") : undefined;
6150
+ }
6151
+
6152
+ function renderStudioLatexPandocCompatibilityWarningHtml(warning: string | undefined): string {
6153
+ if (!warning) return "";
6154
+ return `<div class="preview-warning studio-latex-compatibility-warning" role="note">${escapeStudioHtmlText(warning)}</div>`;
6149
6155
  }
6150
6156
 
6157
+ const resolveStudioPandocHtmlResourceFlag = createStudioPandocHtmlResourceFlagResolver(async (pandocCommand: string) => {
6158
+ const result = await runStudioSubprocess(pandocCommand, ["--help"], {
6159
+ timeoutMs: 5_000,
6160
+ stdoutMaxBytes: 250_000,
6161
+ stderrMaxBytes: 20_000,
6162
+ label: "pandoc capability probe",
6163
+ notFoundMessage: "pandoc was not found. Install pandoc or set PANDOC_PATH to the pandoc binary.",
6164
+ });
6165
+ if (result.code !== 0) {
6166
+ throw new Error(`pandoc capability probe failed with exit code ${result.code}${result.stderr ? `: ${result.stderr}` : ""}`);
6167
+ }
6168
+ return result.stdout;
6169
+ });
6170
+
6151
6171
  function preprocessStudioLatexFootnotemarksForPreview(latex: string): string {
6152
6172
  return String(latex ?? "").replace(/\\footnotemark\s*\[\s*([^\]\r\n]+?)\s*\]/g, (_match, marker: string) => {
6153
6173
  const value = String(marker || "").trim();
@@ -6157,8 +6177,15 @@ function preprocessStudioLatexFootnotemarksForPreview(latex: string): string {
6157
6177
 
6158
6178
  async function renderStudioMarkdownWithPandoc(markdown: string, isLatex?: boolean, resourcePath?: string, sourcePath?: string): Promise<string> {
6159
6179
  const pandocCommand = process.env.PANDOC_PATH?.trim() || "pandoc";
6160
- const latexPreviewSource = isLatex ? preprocessStudioLatexFootnotemarksForPreview(markdown) : markdown;
6161
- const markdownWithNormalizedFences = isLatex ? latexPreviewSource : normalizeStudioMarkdownSmartFences(markdown);
6180
+ const pandocWorkingDir = resolveStudioPandocWorkingDir(resourcePath)
6181
+ ?? resolveStudioPandocWorkingDir(sourcePath ? dirname(sourcePath) : undefined);
6182
+ const latexPandocCompatibility = isLatex
6183
+ ? prepareStudioLatexForPandoc(markdown, pandocWorkingDir)
6184
+ : { source: markdown, omittedPackages: [] as Array<{ name: string; path: string }> };
6185
+ const latexPandocCompatibilityWarning = buildStudioLatexPandocCompatibilityWarning(latexPandocCompatibility.omittedPackages);
6186
+ const pandocInputSource = latexPandocCompatibility.source;
6187
+ const latexPreviewSource = isLatex ? preprocessStudioLatexFootnotemarksForPreview(pandocInputSource) : pandocInputSource;
6188
+ const markdownWithNormalizedFences = isLatex ? latexPreviewSource : normalizeStudioMarkdownSmartFences(pandocInputSource);
6162
6189
  const markdownWithoutHtmlComments = isLatex ? markdownWithNormalizedFences : stripStudioMarkdownHtmlCommentsPreservingYamlFrontMatter(markdownWithNormalizedFences);
6163
6190
  const markdownWithPreviewPageBreaks = isLatex ? markdownWithoutHtmlComments : replaceStudioPreviewPageBreakCommands(markdownWithoutHtmlComments);
6164
6191
  const latexSubfigurePreviewTransform = isLatex
@@ -6171,7 +6198,7 @@ async function renderStudioMarkdownWithPandoc(markdown: string, isLatex?: boolea
6171
6198
  ? preprocessStudioLatexReferences(latexAlgorithmPreviewTransform.markdown, sourcePath, resourcePath)
6172
6199
  : markdownWithPreviewPageBreaks;
6173
6200
  const inputFormat = isLatex ? "latex" : "markdown+lists_without_preceding_blankline-blank_before_blockquote-blank_before_header+tex_math_dollars+tex_math_single_backslash+tex_math_double_backslash+autolink_bare_uris-raw_html";
6174
- const bibliographyArgs = buildStudioPandocBibliographyArgs(markdown, isLatex, resourcePath);
6201
+ const bibliographyArgs = buildStudioPandocBibliographyArgs(pandocInputSource, isLatex, resourcePath);
6175
6202
  const args = ["-f", inputFormat, "-t", "html5", "--mathml", "--wrap=none", ...bibliographyArgs];
6176
6203
  let htmlTemplateDir: string | null = null;
6177
6204
  const useStudioHtmlTemplate = Boolean(resourcePath || isLatex);
@@ -6186,13 +6213,12 @@ async function renderStudioMarkdownWithPandoc(markdown: string, isLatex?: boolea
6186
6213
  await mkdir(htmlTemplateDir, { recursive: true });
6187
6214
  const htmlTemplatePath = join(htmlTemplateDir, "template.html");
6188
6215
  await writeFile(htmlTemplatePath, STUDIO_PANDOC_HTML_FRAGMENT_TEMPLATE, "utf-8");
6189
- if (resourcePath) args.push(await getStudioPandocHtmlResourceFlag(pandocCommand));
6216
+ if (resourcePath) args.push(await resolveStudioPandocHtmlResourceFlag(pandocCommand));
6190
6217
  args.push("--standalone", `--template=${htmlTemplatePath}`);
6191
6218
  }
6192
6219
  const normalizedMarkdown = isLatex
6193
6220
  ? sourceWithResolvedRefs
6194
6221
  : normalizeStudioMarkdownFencedBlocks(prepareStudioMarkdownForPandoc(sourceWithResolvedRefs));
6195
- const pandocWorkingDir = resolveStudioPandocWorkingDir(resourcePath);
6196
6222
 
6197
6223
  let pandocResult: StudioSubprocessResult;
6198
6224
  try {
@@ -6237,6 +6263,7 @@ async function renderStudioMarkdownWithPandoc(markdown: string, isLatex?: boolea
6237
6263
  renderedHtml = decorateStudioPreviewPageBreakHtml(renderedHtml);
6238
6264
  }
6239
6265
  renderedHtml = decorateStudioPandocSyntaxHtml(renderedHtml);
6266
+ renderedHtml = renderStudioLatexPandocCompatibilityWarningHtml(latexPandocCompatibilityWarning) + renderedHtml;
6240
6267
  return stripMathMlAnnotationTags(renderedHtml);
6241
6268
  }
6242
6269
 
@@ -6623,8 +6650,15 @@ async function renderStudioStandaloneHtmlWithPandoc(
6623
6650
  sourcePath?: string,
6624
6651
  options?: StudioHtmlRenderOptions,
6625
6652
  ): Promise<{ html: Buffer; warning?: string }> {
6626
- const delimitedMarkdown = isLatex ? null : formatStudioDelimitedTextAsMarkdown(markdown, editorLanguage);
6627
- const input = delimitedMarkdown ?? markdown;
6653
+ const pandocWorkingDir = resolveStudioPandocWorkingDir(resourcePath)
6654
+ ?? resolveStudioPandocWorkingDir(sourcePath ? dirname(sourcePath) : undefined);
6655
+ const latexPandocCompatibility = isLatex
6656
+ ? prepareStudioLatexForPandoc(markdown, pandocWorkingDir)
6657
+ : { source: markdown, omittedPackages: [] as Array<{ name: string; path: string }> };
6658
+ const latexPandocCompatibilityWarning = buildStudioLatexPandocCompatibilityWarning(latexPandocCompatibility.omittedPackages);
6659
+ const pandocCompatibleMarkdown = latexPandocCompatibility.source;
6660
+ const delimitedMarkdown = isLatex ? null : formatStudioDelimitedTextAsMarkdown(pandocCompatibleMarkdown, editorLanguage);
6661
+ const input = delimitedMarkdown ?? pandocCompatibleMarkdown;
6628
6662
  const effectiveEditorLanguage = delimitedMarkdown ? "markdown" : inferStudioPdfLanguage(input, editorLanguage);
6629
6663
  if (!isLatex && isLikelyStandaloneStudioHtml(input, effectiveEditorLanguage)) {
6630
6664
  return { html: Buffer.from(String(input ?? ""), "utf-8") };
@@ -6641,8 +6675,12 @@ async function renderStudioStandaloneHtmlWithPandoc(
6641
6675
  let renderedHtml = await renderStudioMarkdownWithPandoc(pdfPrepared.markdown, isLatex, resourcePath, sourcePath);
6642
6676
  renderedHtml = renderStudioPdfBlocksInHtml(renderedHtml, pdfPrepared.blocks, sourcePath, resourcePath);
6643
6677
  renderedHtml = applyStudioAnnotationPlaceholdersToHtml(renderedHtml, annotationPrepared.placeholders);
6678
+ renderedHtml = renderStudioLatexPandocCompatibilityWarningHtml(latexPandocCompatibilityWarning) + renderedHtml;
6644
6679
  const standaloneHtml = buildStudioStandaloneHtmlDocument(renderedHtml, resourcePath, options);
6645
- return { html: Buffer.from(standaloneHtml, "utf-8") };
6680
+ return {
6681
+ html: Buffer.from(standaloneHtml, "utf-8"),
6682
+ warning: latexPandocCompatibilityWarning,
6683
+ };
6646
6684
  }
6647
6685
 
6648
6686
  async function renderStudioLiteralTextPdf(text: string, title = "Studio export", options?: StudioPdfRenderOptions): Promise<Buffer> {
@@ -7031,19 +7069,26 @@ async function renderStudioPdfWithPandoc(
7031
7069
  ): Promise<{ pdf: Buffer; warning?: string }> {
7032
7070
  const pandocCommand = process.env.PANDOC_PATH?.trim() || "pandoc";
7033
7071
  const pdfEngine = process.env.PANDOC_PDF_ENGINE?.trim() || "xelatex";
7072
+ const pandocWorkingDir = resolveStudioPandocWorkingDir(resourcePath)
7073
+ ?? resolveStudioPandocWorkingDir(sourcePath ? dirname(sourcePath) : undefined);
7074
+ const latexPandocCompatibility = isLatex
7075
+ ? prepareStudioLatexForPandoc(markdown, pandocWorkingDir)
7076
+ : { source: markdown, omittedPackages: [] as Array<{ name: string; path: string }> };
7077
+ const latexPandocCompatibilityWarning = buildStudioLatexPandocCompatibilityWarning(latexPandocCompatibility.omittedPackages);
7078
+ const pandocInputSource = latexPandocCompatibility.source;
7034
7079
  const latexSubfigurePdfTransform = isLatex
7035
- ? preprocessStudioLatexSubfiguresForPdf(markdown)
7036
- : { markdown, groups: [] };
7080
+ ? preprocessStudioLatexSubfiguresForPdf(pandocInputSource)
7081
+ : { markdown: pandocInputSource, groups: [] };
7037
7082
  const latexPdfSource = isLatex
7038
7083
  ? preprocessStudioLatexAlgorithmsForPdf(
7039
7084
  latexSubfigurePdfTransform.markdown,
7040
7085
  sourcePath,
7041
7086
  resourcePath,
7042
7087
  )
7043
- : markdown;
7088
+ : pandocInputSource;
7044
7089
  const sourceWithResolvedRefs = isLatex
7045
7090
  ? injectStudioLatexEquationTags(preprocessStudioLatexReferences(latexPdfSource, sourcePath, resourcePath), sourcePath, resourcePath)
7046
- : markdown;
7091
+ : pandocInputSource;
7047
7092
  const effectiveEditorLanguage = inferStudioPdfLanguage(sourceWithResolvedRefs, editorPdfLanguage);
7048
7093
  const pdfCalloutTransform = !isLatex && (!effectiveEditorLanguage || effectiveEditorLanguage === "markdown")
7049
7094
  ? preprocessStudioMarkdownCalloutsForPdf(sourceWithResolvedRefs)
@@ -7051,8 +7096,7 @@ async function renderStudioPdfWithPandoc(
7051
7096
  const pdfAlignedImageTransform = !isLatex && (!effectiveEditorLanguage || effectiveEditorLanguage === "markdown")
7052
7097
  ? preprocessStudioMarkdownImageAlignmentForPdf(pdfCalloutTransform.markdown)
7053
7098
  : { markdown: pdfCalloutTransform.markdown, blocks: [] as StudioPdfAlignedImageBlock[] };
7054
- const pandocWorkingDir = resolveStudioPandocWorkingDir(resourcePath);
7055
- const bibliographyArgs = buildStudioPandocBibliographyArgs(markdown, isLatex, resourcePath);
7099
+ const bibliographyArgs = buildStudioPandocBibliographyArgs(pandocInputSource, isLatex, resourcePath);
7056
7100
 
7057
7101
  const runPandocPdfExport = async (
7058
7102
  inputFormat: string,
@@ -7105,7 +7149,7 @@ async function renderStudioPdfWithPandoc(
7105
7149
  };
7106
7150
 
7107
7151
  if (isLatex && (latexSubfigurePdfTransform.groups.length > 0 || collectStudioInlineAnnotationMarkers(sourceWithResolvedRefs).length > 0)) {
7108
- return await renderStudioPdfFromGeneratedLatex(
7152
+ const rendered = await renderStudioPdfFromGeneratedLatex(
7109
7153
  sourceWithResolvedRefs,
7110
7154
  pandocCommand,
7111
7155
  pdfEngine,
@@ -7121,6 +7165,10 @@ async function renderStudioPdfWithPandoc(
7121
7165
  "",
7122
7166
  themeStyle,
7123
7167
  );
7168
+ return {
7169
+ pdf: rendered.pdf,
7170
+ warning: combineStudioWarnings(latexPandocCompatibilityWarning, rendered.warning),
7171
+ };
7124
7172
  }
7125
7173
 
7126
7174
  if (!isLatex && effectiveEditorLanguage === "diff") {
@@ -7194,7 +7242,10 @@ async function renderStudioPdfWithPandoc(
7194
7242
  themeStyle,
7195
7243
  );
7196
7244
  await rm(tempDir, { recursive: true, force: true }).catch(() => undefined);
7197
- return { pdf: rendered.pdf, warning: mermaidPrepared.warning ?? rendered.warning };
7245
+ return {
7246
+ pdf: rendered.pdf,
7247
+ warning: combineStudioWarnings(latexPandocCompatibilityWarning, mermaidPrepared.warning, rendered.warning),
7248
+ };
7198
7249
  }
7199
7250
 
7200
7251
  const hasYamlHeaderIncludesForPdf = !isLatex && hasStudioYamlHeaderIncludes(markdownForPdf);
@@ -7229,7 +7280,10 @@ async function renderStudioPdfWithPandoc(
7229
7280
  throw new Error(`pandoc PDF export failed with exit code ${pandocResult.code}${stderr ? `: ${stderr}` : ""}${hint}`);
7230
7281
  }
7231
7282
 
7232
- return { pdf: await readFile(outputPath), warning: mermaidPrepared.warning };
7283
+ return {
7284
+ pdf: await readFile(outputPath),
7285
+ warning: combineStudioWarnings(latexPandocCompatibilityWarning, mermaidPrepared.warning),
7286
+ };
7233
7287
  } finally {
7234
7288
  await rm(tempDir, { recursive: true, force: true }).catch(() => undefined);
7235
7289
  }
@@ -7539,6 +7593,64 @@ function revealStudioLocalFile(filePath: string): { ok: true; message: string }
7539
7593
  return { ok: true, message: process.platform === "linux" ? "Opened containing folder." : "Revealed resource in file manager." };
7540
7594
  }
7541
7595
 
7596
+ async function handleImportStudioFileCopyRequest(req: IncomingMessage, res: ServerResponse, studioCwd: string): Promise<void> {
7597
+ const method = (req.method ?? "GET").toUpperCase();
7598
+ if (method !== "POST") {
7599
+ res.setHeader("Allow", "POST");
7600
+ respondJson(res, 405, { ok: false, error: "Method not allowed. Use POST." });
7601
+ return;
7602
+ }
7603
+
7604
+ const rawBody = await readRequestBody(req, REQUEST_BODY_MAX_BYTES);
7605
+ let payload: Record<string, unknown> = {};
7606
+ try {
7607
+ payload = rawBody ? JSON.parse(rawBody) : {};
7608
+ } catch {
7609
+ respondJson(res, 400, { ok: false, error: "Invalid JSON body." });
7610
+ return;
7611
+ }
7612
+
7613
+ const requestedPath = typeof payload.path === "string" ? payload.path : "";
7614
+ const resolved = resolveStudioPath(requestedPath, studioCwd);
7615
+ if (resolved.ok === false) {
7616
+ respondJson(res, 400, { ok: false, error: resolved.message });
7617
+ return;
7618
+ }
7619
+
7620
+ try {
7621
+ const stats = statSync(resolved.resolved);
7622
+ if (!stats.isFile()) {
7623
+ respondJson(res, 400, { ok: false, error: `Path is not a file: ${resolved.label}` });
7624
+ return;
7625
+ }
7626
+ if (stats.size > STUDIO_IMPORT_FILE_MAX_BYTES) {
7627
+ respondJson(res, 413, {
7628
+ ok: false,
7629
+ error: `File is too large to import into Studio (${stats.size} bytes; limit ${STUDIO_IMPORT_FILE_MAX_BYTES} bytes).`,
7630
+ });
7631
+ return;
7632
+ }
7633
+ } catch (error) {
7634
+ respondJson(res, 404, {
7635
+ ok: false,
7636
+ error: `Could not access file: ${resolved.label} (${error instanceof Error ? error.message : String(error)})`,
7637
+ });
7638
+ return;
7639
+ }
7640
+
7641
+ const file = readStudioFile(resolved.resolved, studioCwd);
7642
+ if (file.ok === false) {
7643
+ respondJson(res, 400, { ok: false, error: file.message });
7644
+ return;
7645
+ }
7646
+ respondJson(res, 200, {
7647
+ ok: true,
7648
+ text: file.text,
7649
+ filename: basename(file.resolvedPath),
7650
+ resolvedPath: file.resolvedPath,
7651
+ });
7652
+ }
7653
+
7542
7654
  async function handleRevealLocalPreviewResourceRequest(req: IncomingMessage, res: ServerResponse, studioCwd: string): Promise<void> {
7543
7655
  const method = (req.method ?? "GET").toUpperCase();
7544
7656
  if (method !== "POST") {
@@ -7574,6 +7686,50 @@ async function handleRevealLocalPreviewResourceRequest(req: IncomingMessage, res
7574
7686
  }
7575
7687
  }
7576
7688
 
7689
+ async function handleOpenLocalPreviewResourceRequest(req: IncomingMessage, res: ServerResponse, studioCwd: string): Promise<void> {
7690
+ const method = (req.method ?? "GET").toUpperCase();
7691
+ if (method !== "POST") {
7692
+ res.setHeader("Allow", "POST");
7693
+ respondJson(res, 405, { ok: false, error: "Method not allowed. Use POST." });
7694
+ return;
7695
+ }
7696
+ if (isSshSession()) {
7697
+ respondJson(res, 409, { ok: false, error: "Cannot open a system PDF viewer from an SSH/headless Studio session. Copy the path instead." });
7698
+ return;
7699
+ }
7700
+
7701
+ const rawBody = await readRequestBody(req, REQUEST_BODY_MAX_BYTES);
7702
+ let payload: Record<string, unknown> = {};
7703
+ try {
7704
+ payload = rawBody ? JSON.parse(rawBody) : {};
7705
+ } catch {
7706
+ respondJson(res, 400, { ok: false, error: "Invalid JSON body." });
7707
+ return;
7708
+ }
7709
+
7710
+ try {
7711
+ const resource = resolveStudioLocalPreviewResourcePath(
7712
+ typeof payload.path === "string" ? payload.path : "",
7713
+ typeof payload.sourcePath === "string" ? payload.sourcePath : undefined,
7714
+ typeof payload.resourceDir === "string" ? payload.resourceDir : undefined,
7715
+ studioCwd,
7716
+ );
7717
+ if (resource.kind !== "pdf") {
7718
+ respondJson(res, 400, { ok: false, error: "Only local PDF previews can be opened in the system PDF viewer." });
7719
+ return;
7720
+ }
7721
+ await openPathInDefaultViewer(resource.filePath);
7722
+ respondJson(res, 200, {
7723
+ ok: true,
7724
+ message: "Opened PDF in the system viewer.",
7725
+ path: resource.filePath,
7726
+ label: resource.label,
7727
+ });
7728
+ } catch (error) {
7729
+ respondJson(res, 404, { ok: false, error: `Local PDF unavailable: ${error instanceof Error ? error.message : String(error)}` });
7730
+ }
7731
+ }
7732
+
7577
7733
  function openPathInDefaultViewer(path: string): Promise<void> {
7578
7734
  const openCommand =
7579
7735
  process.platform === "darwin"
@@ -10596,7 +10752,8 @@ ${cssVarsBlock}
10596
10752
  <button id="saveOverBtn" type="button" title="Overwrite current file with editor content. Shortcut: Cmd/Ctrl+S.">Save editor</button>
10597
10753
  <button id="refreshFromDiskBtn" type="button" title="Reload the current file-backed document from disk.">Refresh from disk</button>
10598
10754
  <button id="clearWorkspaceBtn" type="button" title="Clear editor text and reset this tab to a fresh blank draft. Saved files and responses are not changed.">Reset editor</button>
10599
- <label class="file-label" title="Browser import: load a selected text file as a detached copy. Use Save editor as… to attach this copy to a file path and make it file-backed, or use the Files view to open a refreshable file-backed document directly.">Import file copy…<input id="fileInput" type="file" accept=".md,.markdown,.mdx,.qmd,.js,.mjs,.cjs,.jsx,.ts,.mts,.cts,.tsx,.py,.pyw,.sh,.bash,.zsh,.json,.jsonc,.json5,.rs,.c,.h,.cpp,.cxx,.cc,.hpp,.hxx,.jl,.f90,.f95,.f03,.f,.for,.r,.R,.m,.tex,.latex,.diff,.patch,.java,.go,.rb,.swift,.html,.htm,.css,.xml,.yaml,.yml,.toml,.lua,.txt,.rst,.adoc" /></label>
10755
+ <button id="importFileBtn" type="button" title="Import a file as an editable copy.">Import file copy…</button>
10756
+ <input id="fileInput" class="file-input-hidden" type="file" tabindex="-1" aria-hidden="true" accept=".md,.markdown,.mdx,.qmd,.js,.mjs,.cjs,.jsx,.ts,.mts,.cts,.tsx,.py,.pyw,.sh,.bash,.zsh,.json,.jsonc,.json5,.rs,.c,.h,.cpp,.cxx,.cc,.hpp,.hxx,.jl,.f90,.f95,.f03,.f,.for,.r,.R,.m,.tex,.latex,.diff,.patch,.java,.go,.rb,.swift,.html,.htm,.css,.xml,.yaml,.yml,.toml,.lua,.txt,.rst,.adoc" />
10600
10757
  <button id="getEditorBtn" type="button" title="Load the current terminal editor draft into Studio.">Load from pi editor</button>
10601
10758
  <button id="zenModeBtn" class="zen-mode-btn" type="button" title="Hide secondary Studio controls. Shortcut: F9.">Zen</button>
10602
10759
  </div>
@@ -14916,6 +15073,19 @@ export default function (pi: ExtensionAPI) {
14916
15073
  return;
14917
15074
  }
14918
15075
 
15076
+ if (requestUrl.pathname === "/import-file-copy") {
15077
+ const token = requestUrl.searchParams.get("token") ?? "";
15078
+ if (token !== serverState.token) {
15079
+ respondJson(res, 403, { ok: false, error: "Invalid or expired studio token. Re-run /studio." });
15080
+ return;
15081
+ }
15082
+
15083
+ void handleImportStudioFileCopyRequest(req, res, studioCwd).catch((error) => {
15084
+ respondJson(res, 500, { ok: false, error: `File import failed: ${error instanceof Error ? error.message : String(error)}` });
15085
+ });
15086
+ return;
15087
+ }
15088
+
14919
15089
  if (requestUrl.pathname === "/local-preview-link") {
14920
15090
  const token = requestUrl.searchParams.get("token") ?? "";
14921
15091
  if (token !== serverState.token) {
@@ -14952,6 +15122,19 @@ export default function (pi: ExtensionAPI) {
14952
15122
  return;
14953
15123
  }
14954
15124
 
15125
+ if (requestUrl.pathname === "/open-local-resource") {
15126
+ const token = requestUrl.searchParams.get("token") ?? "";
15127
+ if (token !== serverState.token) {
15128
+ respondJson(res, 403, { ok: false, error: "Invalid or expired studio token. Re-run /studio." });
15129
+ return;
15130
+ }
15131
+
15132
+ void handleOpenLocalPreviewResourceRequest(req, res, studioCwd).catch((error) => {
15133
+ respondJson(res, 500, { ok: false, error: `Open in system viewer failed: ${error instanceof Error ? error.message : String(error)}` });
15134
+ });
15135
+ return;
15136
+ }
15137
+
14955
15138
  if (requestUrl.pathname === "/pdf-resource") {
14956
15139
  const token = requestUrl.searchParams.get("token") ?? "";
14957
15140
  if (token !== serverState.token) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-studio",
3
- "version": "0.9.46",
3
+ "version": "0.9.48",
4
4
  "description": "Two-pane browser workspace for pi with prompt/response editing, annotations, critiques, active quiz, prompt/response history, live previews, and tmux-backed REPL/literate REPL workflows",
5
5
  "type": "module",
6
6
  "license": "MIT",