pi-studio 0.9.38 → 0.9.40

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/index.ts CHANGED
@@ -29,6 +29,7 @@ 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 { buildStudioReplTmuxStartArgs } from "./shared/studio-repl-tmux.js";
32
33
  import { buildStudioForwardingHint, buildStudioSshTunnelHint, isStudioSshSession as isSshSession } from "./shared/studio-ssh-hint.js";
33
34
  import { renderStudioAnnotationInlineHtml } from "./shared/studio-annotation-render.js";
34
35
  import {
@@ -36,6 +37,13 @@ import {
36
37
  buildStudioMermaidPdfIconContrastCss,
37
38
  ensureStudioMermaidSourceContrast,
38
39
  } from "./shared/studio-mermaid.js";
40
+ import {
41
+ appendStudioQuartoLog,
42
+ buildStudioQuartoPreviewArgs,
43
+ isStudioQuartoDocumentPath,
44
+ parseStudioQuartoInspect,
45
+ parseStudioQuartoPreviewUrl,
46
+ } from "./shared/studio-quarto-preview.js";
39
47
 
40
48
  type Lens = "writing" | "code";
41
49
  type RequestedLens = Lens | "auto";
@@ -50,6 +58,34 @@ type StudioQuizAngle = "general" | "scientist" | "mathematician" | "statistician
50
58
  type StudioQuizScope = "selection" | "editor" | "file" | "folder" | "repo";
51
59
  type StudioQuizThinking = "off" | "minimal" | "low" | "medium" | "high";
52
60
  type StudioPiThinkingLevel = ModelThinkingLevel | "max";
61
+ type StudioQuartoPreviewStatus = "idle" | "starting" | "running" | "stopping" | "stopped" | "error";
62
+
63
+ interface StudioQuartoPreviewContext {
64
+ sourcePath: string;
65
+ requestedSourcePath: string;
66
+ available: boolean;
67
+ reason: "ready" | "not-found" | "invalid-source" | "inspect-error";
68
+ version: string;
69
+ projectRoot: string;
70
+ projectType: string;
71
+ projectLabel: string;
72
+ outputFile: string;
73
+ isProject: boolean;
74
+ error: string | null;
75
+ inspectLog: string;
76
+ }
77
+
78
+ interface StudioQuartoPreviewState {
79
+ status: StudioQuartoPreviewStatus;
80
+ sourcePath: string;
81
+ url: string;
82
+ log: string;
83
+ error: string | null;
84
+ context: StudioQuartoPreviewContext | null;
85
+ startedAt: number | null;
86
+ updatedAt: number;
87
+ actionRequestId: string | null;
88
+ }
53
89
 
54
90
  const STUDIO_CSS_URL = new URL("./client/studio.css", import.meta.url);
55
91
  const STUDIO_ANNOTATION_HELPERS_URL = new URL("./client/studio-annotation-helpers.js", import.meta.url);
@@ -485,6 +521,23 @@ interface GetFromEditorRequestMessage {
485
521
  requestId: string;
486
522
  }
487
523
 
524
+ interface QuartoPreviewCheckRequestMessage {
525
+ type: "quarto_preview_check_request";
526
+ requestId: string;
527
+ sourcePath: string;
528
+ }
529
+
530
+ interface QuartoPreviewStartRequestMessage {
531
+ type: "quarto_preview_start_request";
532
+ requestId: string;
533
+ sourcePath: string;
534
+ }
535
+
536
+ interface QuartoPreviewStopRequestMessage {
537
+ type: "quarto_preview_stop_request";
538
+ requestId: string;
539
+ }
540
+
488
541
  interface GitChangesRequestMessage {
489
542
  type: "git_changes_request";
490
543
  requestId: string;
@@ -534,6 +587,9 @@ type IncomingStudioMessage =
534
587
  | RefreshFromDiskRequestMessage
535
588
  | SendToEditorRequestMessage
536
589
  | GetFromEditorRequestMessage
590
+ | QuartoPreviewCheckRequestMessage
591
+ | QuartoPreviewStartRequestMessage
592
+ | QuartoPreviewStopRequestMessage
537
593
  | GitChangesRequestMessage
538
594
  | OpenEditorOnlyRequestMessage
539
595
  | CancelRequestMessage;
@@ -564,6 +620,9 @@ const MAX_PREPARED_HTML_EXPORTS = 8;
564
620
  const STUDIO_TRACE_SNAPSHOT_MAX_ENTRIES = 80;
565
621
  const STUDIO_TRACE_SNAPSHOT_MAX_FIELD_CHARS = 20_000;
566
622
  const STUDIO_TRACE_TOOL_ARGS_MAX_CHARS = 20_000;
623
+ const STUDIO_QUARTO_CHECK_TIMEOUT_MS = 30_000;
624
+ const STUDIO_QUARTO_START_TIMEOUT_MS = 3 * 60_000;
625
+ const STUDIO_QUARTO_LOG_MAX_CHARS = 80_000;
567
626
  const STUDIO_TRACE_IMAGE_MAX_COUNT = 8;
568
627
  const STUDIO_TRACE_IMAGE_MAX_BASE64_CHARS = 2_500_000;
569
628
  const STUDIO_TRACE_SNAPSHOT_MAX_IMAGES = 12;
@@ -816,7 +875,7 @@ $if(subtitle)$
816
875
  <p class="subtitle">$subtitle$</p>
817
876
  $endif$
818
877
  $for(author)$
819
- <p class="author">$author$</p>
878
+ <p class="author">$if(author.name)$$author.name$$else$$author$$endif$</p>
820
879
  $endfor$
821
880
  $if(date)$
822
881
  <p class="date">$date$</p>
@@ -8713,6 +8772,37 @@ function parseIncomingMessage(data: RawData): IncomingStudioMessage | null {
8713
8772
  };
8714
8773
  }
8715
8774
 
8775
+ if (
8776
+ msg.type === "quarto_preview_check_request"
8777
+ && typeof msg.requestId === "string"
8778
+ && typeof msg.sourcePath === "string"
8779
+ ) {
8780
+ return {
8781
+ type: "quarto_preview_check_request",
8782
+ requestId: msg.requestId,
8783
+ sourcePath: msg.sourcePath,
8784
+ };
8785
+ }
8786
+
8787
+ if (
8788
+ msg.type === "quarto_preview_start_request"
8789
+ && typeof msg.requestId === "string"
8790
+ && typeof msg.sourcePath === "string"
8791
+ ) {
8792
+ return {
8793
+ type: "quarto_preview_start_request",
8794
+ requestId: msg.requestId,
8795
+ sourcePath: msg.sourcePath,
8796
+ };
8797
+ }
8798
+
8799
+ if (msg.type === "quarto_preview_stop_request" && typeof msg.requestId === "string") {
8800
+ return {
8801
+ type: "quarto_preview_stop_request",
8802
+ requestId: msg.requestId,
8803
+ };
8804
+ }
8805
+
8716
8806
  if (
8717
8807
  msg.type === "git_changes_request"
8718
8808
  && typeof msg.requestId === "string"
@@ -9537,7 +9627,7 @@ function startStudioReplSession(runtime: StudioReplRuntime, cwd: string, options
9537
9627
  };
9538
9628
  }
9539
9629
  const command = getStudioReplRuntimeCommand(runtime, commandOverride);
9540
- const result = runStudioTmux(["new-session", "-d", "-s", sessionName, "-c", cwd || process.cwd(), command], { timeout: 5_000 });
9630
+ const result = runStudioTmux(buildStudioReplTmuxStartArgs(sessionName, cwd || process.cwd(), command), { timeout: 5_000 });
9541
9631
  if (!result.ok) return { ok: false, message: result.message || `Failed to start ${STUDIO_REPL_RUNTIME_LABELS[runtime]} REPL.` };
9542
9632
  return {
9543
9633
  ok: true,
@@ -10662,6 +10752,7 @@ ${cssVarsBlock}
10662
10752
  <option value="markdown">Response (Raw)</option>
10663
10753
  <option value="preview" selected>Response (Preview)</option>
10664
10754
  <option value="editor-preview">Editor (Preview)</option>
10755
+ <option value="editor-quarto-preview" hidden>Editor (Quarto Preview)</option>
10665
10756
  <option value="trace">Working</option>
10666
10757
  <option value="changes">Changes</option>
10667
10758
  <option value="files">Files</option>
@@ -10882,6 +10973,22 @@ export default function (pi: ExtensionAPI) {
10882
10973
  let compactInProgress = false;
10883
10974
  let compactRequestId: string | null = null;
10884
10975
  const activeCompletionSuggestions = new Map<string, AbortController>();
10976
+ let studioQuartoPreviewProcess: ReturnType<typeof spawn> | null = null;
10977
+ let studioQuartoPreviewGeneration = 0;
10978
+ let studioQuartoPreviewStartupTimer: NodeJS.Timeout | null = null;
10979
+ let studioQuartoPreviewBroadcastTimer: NodeJS.Timeout | null = null;
10980
+ let studioQuartoPreviewOperation: Promise<void> = Promise.resolve();
10981
+ let studioQuartoPreviewState: StudioQuartoPreviewState = {
10982
+ status: "idle",
10983
+ sourcePath: "",
10984
+ url: "",
10985
+ log: "",
10986
+ error: null,
10987
+ context: null,
10988
+ startedAt: null,
10989
+ updatedAt: Date.now(),
10990
+ actionRequestId: null,
10991
+ };
10885
10992
 
10886
10993
  const selectStudioReplSessionForTool = (params: { sessionName?: string; target?: string }): { session: StudioReplSessionInfo | null; error?: string; sessions: StudioReplSessionInfo[] } => {
10887
10994
  const state = listStudioReplSessions();
@@ -11537,6 +11644,305 @@ export default function (pi: ExtensionAPI) {
11537
11644
  }
11538
11645
  };
11539
11646
 
11647
+ const getStudioQuartoPreviewSnapshot = (): StudioQuartoPreviewState => ({
11648
+ ...studioQuartoPreviewState,
11649
+ context: studioQuartoPreviewState.context ? { ...studioQuartoPreviewState.context } : null,
11650
+ });
11651
+
11652
+ const broadcastStudioQuartoPreviewState = () => {
11653
+ if (studioQuartoPreviewBroadcastTimer) {
11654
+ clearTimeout(studioQuartoPreviewBroadcastTimer);
11655
+ studioQuartoPreviewBroadcastTimer = null;
11656
+ }
11657
+ broadcast({ type: "quarto_preview_state", preview: getStudioQuartoPreviewSnapshot() });
11658
+ };
11659
+
11660
+ const scheduleStudioQuartoPreviewBroadcast = () => {
11661
+ if (studioQuartoPreviewBroadcastTimer) return;
11662
+ studioQuartoPreviewBroadcastTimer = setTimeout(() => {
11663
+ studioQuartoPreviewBroadcastTimer = null;
11664
+ broadcast({ type: "quarto_preview_state", preview: getStudioQuartoPreviewSnapshot() });
11665
+ }, 120);
11666
+ };
11667
+
11668
+ const updateStudioQuartoPreviewState = (
11669
+ patch: Partial<StudioQuartoPreviewState>,
11670
+ options?: { immediate?: boolean },
11671
+ ) => {
11672
+ studioQuartoPreviewState = {
11673
+ ...studioQuartoPreviewState,
11674
+ ...patch,
11675
+ updatedAt: Date.now(),
11676
+ };
11677
+ if (options?.immediate) {
11678
+ broadcastStudioQuartoPreviewState();
11679
+ } else {
11680
+ scheduleStudioQuartoPreviewBroadcast();
11681
+ }
11682
+ };
11683
+
11684
+ const makeUnavailableStudioQuartoContext = (
11685
+ sourcePath: string,
11686
+ reason: StudioQuartoPreviewContext["reason"],
11687
+ error: string,
11688
+ inspectLog = "",
11689
+ ): StudioQuartoPreviewContext => ({
11690
+ sourcePath,
11691
+ requestedSourcePath: sourcePath,
11692
+ available: false,
11693
+ reason,
11694
+ version: "",
11695
+ projectRoot: sourcePath ? dirname(sourcePath) : "",
11696
+ projectType: "",
11697
+ projectLabel: sourcePath ? basename(sourcePath) : "Quarto document",
11698
+ outputFile: "",
11699
+ isProject: false,
11700
+ error,
11701
+ inspectLog,
11702
+ });
11703
+
11704
+ const resolveStudioQuartoSourcePath = (requestedPath: string): { ok: true; path: string } | { ok: false; context: StudioQuartoPreviewContext } => {
11705
+ const rawPath = String(requestedPath || "").trim();
11706
+ if (!rawPath || rawPath.length > 32_000) {
11707
+ return {
11708
+ ok: false,
11709
+ context: makeUnavailableStudioQuartoContext(rawPath, "invalid-source", "Quarto preview requires a file-backed .qmd, .md, or .markdown document."),
11710
+ };
11711
+ }
11712
+ const candidate = resolve(studioCwd, rawPath);
11713
+ if (!isStudioQuartoDocumentPath(candidate)) {
11714
+ return {
11715
+ ok: false,
11716
+ context: makeUnavailableStudioQuartoContext(candidate, "invalid-source", "Quarto preview is currently available only for file-backed .qmd, .md, and .markdown documents."),
11717
+ };
11718
+ }
11719
+ try {
11720
+ const resolvedPath = realpathSync(candidate);
11721
+ if (!statSync(resolvedPath).isFile()) {
11722
+ throw new Error("Path is not a regular file.");
11723
+ }
11724
+ return { ok: true, path: resolvedPath };
11725
+ } catch (error) {
11726
+ return {
11727
+ ok: false,
11728
+ context: makeUnavailableStudioQuartoContext(
11729
+ candidate,
11730
+ "invalid-source",
11731
+ `Quarto source is unavailable on disk: ${error instanceof Error ? error.message : String(error)}`,
11732
+ ),
11733
+ };
11734
+ }
11735
+ };
11736
+
11737
+ const inspectStudioQuartoPreviewContext = async (requestedPath: string): Promise<StudioQuartoPreviewContext> => {
11738
+ const requestedSourcePath = resolve(studioCwd, String(requestedPath || "").trim());
11739
+ const resolved = resolveStudioQuartoSourcePath(requestedPath);
11740
+ if (resolved.ok === false) return resolved.context;
11741
+ const sourcePath = resolved.path;
11742
+ const makeInspectedUnavailableContext = (
11743
+ reason: StudioQuartoPreviewContext["reason"],
11744
+ error: string,
11745
+ inspectLog = "",
11746
+ ): StudioQuartoPreviewContext => ({
11747
+ ...makeUnavailableStudioQuartoContext(sourcePath, reason, error, inspectLog),
11748
+ requestedSourcePath,
11749
+ });
11750
+ let versionResult: StudioSubprocessResult;
11751
+ try {
11752
+ versionResult = await runStudioSubprocess("quarto", ["--version"], {
11753
+ cwd: dirname(sourcePath),
11754
+ timeoutMs: STUDIO_QUARTO_CHECK_TIMEOUT_MS,
11755
+ stdoutMaxBytes: 32_000,
11756
+ stderrMaxBytes: 32_000,
11757
+ label: "Quarto version check",
11758
+ notFoundMessage: "Quarto is not installed or is not available on Studio's PATH.",
11759
+ });
11760
+ } catch (error) {
11761
+ const message = error instanceof Error ? error.message : String(error);
11762
+ return makeInspectedUnavailableContext(
11763
+ /was not found|not installed|ENOENT/i.test(message) ? "not-found" : "inspect-error",
11764
+ message,
11765
+ );
11766
+ }
11767
+ const version = (versionResult.stdout || versionResult.stderr).split(/\r?\n/)[0]?.trim() ?? "";
11768
+ if (versionResult.code !== 0) {
11769
+ const detail = versionResult.stderr || versionResult.stdout || `Quarto exited with code ${versionResult.code}.`;
11770
+ return makeInspectedUnavailableContext("inspect-error", `Quarto version check failed: ${detail}`, detail);
11771
+ }
11772
+
11773
+ let inspectResult: StudioSubprocessResult;
11774
+ try {
11775
+ inspectResult = await runStudioSubprocess("quarto", ["inspect", sourcePath], {
11776
+ cwd: dirname(sourcePath),
11777
+ timeoutMs: STUDIO_QUARTO_CHECK_TIMEOUT_MS,
11778
+ stdoutMaxBytes: 1_000_000,
11779
+ stderrMaxBytes: 200_000,
11780
+ label: "Quarto inspect",
11781
+ notFoundMessage: "Quarto is not installed or is not available on Studio's PATH.",
11782
+ });
11783
+ } catch (error) {
11784
+ const message = error instanceof Error ? error.message : String(error);
11785
+ return makeInspectedUnavailableContext("inspect-error", message);
11786
+ }
11787
+ const inspectLog = inspectResult.stderr.trim();
11788
+ if (inspectResult.code !== 0) {
11789
+ const detail = inspectLog || inspectResult.stdout || `Quarto exited with code ${inspectResult.code}.`;
11790
+ return makeInspectedUnavailableContext("inspect-error", `Quarto could not inspect this document: ${detail}`, detail);
11791
+ }
11792
+ try {
11793
+ const inspected = parseStudioQuartoInspect(inspectResult.stdout, sourcePath, version);
11794
+ return {
11795
+ ...inspected,
11796
+ requestedSourcePath,
11797
+ available: true,
11798
+ reason: "ready",
11799
+ error: null,
11800
+ inspectLog,
11801
+ };
11802
+ } catch (error) {
11803
+ const message = error instanceof Error ? error.message : String(error);
11804
+ return makeInspectedUnavailableContext("inspect-error", message, inspectLog);
11805
+ }
11806
+ };
11807
+
11808
+ const clearStudioQuartoPreviewStartupTimer = () => {
11809
+ if (!studioQuartoPreviewStartupTimer) return;
11810
+ clearTimeout(studioQuartoPreviewStartupTimer);
11811
+ studioQuartoPreviewStartupTimer = null;
11812
+ };
11813
+
11814
+ const terminateStudioQuartoChild = async (child: ReturnType<typeof spawn>): Promise<void> => {
11815
+ if (child.exitCode !== null || child.signalCode !== null) return;
11816
+ await new Promise<void>((resolvePromise) => {
11817
+ let settled = false;
11818
+ let forceTimer: NodeJS.Timeout | null = null;
11819
+ const finish = () => {
11820
+ if (settled) return;
11821
+ settled = true;
11822
+ if (forceTimer) clearTimeout(forceTimer);
11823
+ resolvePromise();
11824
+ };
11825
+ const signalProcess = (signal: NodeJS.Signals) => {
11826
+ try {
11827
+ if (process.platform === "win32" && child.pid && signal === "SIGKILL") {
11828
+ spawnSync("taskkill", ["/pid", String(child.pid), "/T", "/F"], { stdio: "ignore", timeout: 2_000 });
11829
+ } else if (process.platform !== "win32" && child.pid) {
11830
+ process.kill(-child.pid, signal);
11831
+ } else {
11832
+ child.kill(signal);
11833
+ }
11834
+ } catch {
11835
+ try { child.kill(signal); } catch {}
11836
+ }
11837
+ };
11838
+ child.once("close", finish);
11839
+ signalProcess("SIGTERM");
11840
+ forceTimer = setTimeout(() => {
11841
+ signalProcess("SIGKILL");
11842
+ setTimeout(finish, 400);
11843
+ }, 2_000);
11844
+ });
11845
+ };
11846
+
11847
+ const stopStudioQuartoPreview = async (
11848
+ actionRequestId: string | null = null,
11849
+ options?: { preserveError?: boolean; quiet?: boolean },
11850
+ ) => {
11851
+ studioQuartoPreviewGeneration += 1;
11852
+ clearStudioQuartoPreviewStartupTimer();
11853
+ const child = studioQuartoPreviewProcess;
11854
+ studioQuartoPreviewProcess = null;
11855
+ if (child && !options?.quiet) {
11856
+ updateStudioQuartoPreviewState({ status: "stopping", actionRequestId }, { immediate: true });
11857
+ }
11858
+ if (child) await terminateStudioQuartoChild(child);
11859
+ updateStudioQuartoPreviewState({
11860
+ status: studioQuartoPreviewState.sourcePath ? "stopped" : "idle",
11861
+ url: "",
11862
+ error: options?.preserveError ? studioQuartoPreviewState.error : null,
11863
+ actionRequestId,
11864
+ }, { immediate: !options?.quiet });
11865
+ };
11866
+
11867
+ const failStudioQuartoPreview = (generation: number, message: string) => {
11868
+ if (generation !== studioQuartoPreviewGeneration) return;
11869
+ clearStudioQuartoPreviewStartupTimer();
11870
+ const child = studioQuartoPreviewProcess;
11871
+ studioQuartoPreviewProcess = null;
11872
+ studioQuartoPreviewGeneration += 1;
11873
+ updateStudioQuartoPreviewState({
11874
+ status: "error",
11875
+ url: "",
11876
+ error: message,
11877
+ log: appendStudioQuartoLog(studioQuartoPreviewState.log, `\n[Studio] ${message}\n`, STUDIO_QUARTO_LOG_MAX_CHARS),
11878
+ }, { immediate: true });
11879
+ if (child) void terminateStudioQuartoChild(child);
11880
+ };
11881
+
11882
+ const startStudioQuartoPreview = async (context: StudioQuartoPreviewContext, actionRequestId: string) => {
11883
+ await stopStudioQuartoPreview(null, { quiet: true });
11884
+ const generation = studioQuartoPreviewGeneration + 1;
11885
+ studioQuartoPreviewGeneration = generation;
11886
+ const args = buildStudioQuartoPreviewArgs(context.sourcePath);
11887
+ updateStudioQuartoPreviewState({
11888
+ status: "starting",
11889
+ sourcePath: context.sourcePath,
11890
+ url: "",
11891
+ log: `[Studio] Starting Quarto ${context.version || "preview"} with computational cell execution disabled (--no-execute).\n`,
11892
+ error: null,
11893
+ context,
11894
+ startedAt: Date.now(),
11895
+ actionRequestId,
11896
+ }, { immediate: true });
11897
+
11898
+ let child: ReturnType<typeof spawn>;
11899
+ try {
11900
+ child = spawn("quarto", args, {
11901
+ cwd: context.projectRoot || dirname(context.sourcePath),
11902
+ stdio: ["ignore", "pipe", "pipe"],
11903
+ detached: process.platform !== "win32",
11904
+ env: { ...process.env, NO_COLOR: "1" },
11905
+ });
11906
+ } catch (error) {
11907
+ failStudioQuartoPreview(generation, `Could not start Quarto: ${error instanceof Error ? error.message : String(error)}`);
11908
+ return;
11909
+ }
11910
+ studioQuartoPreviewProcess = child;
11911
+
11912
+ const handleOutput = (chunk: Buffer | string) => {
11913
+ if (generation !== studioQuartoPreviewGeneration) return;
11914
+ const log = appendStudioQuartoLog(studioQuartoPreviewState.log, chunk, STUDIO_QUARTO_LOG_MAX_CHARS);
11915
+ const previewUrl = studioQuartoPreviewState.url || parseStudioQuartoPreviewUrl(log) || "";
11916
+ if (previewUrl && studioQuartoPreviewState.status === "starting") {
11917
+ clearStudioQuartoPreviewStartupTimer();
11918
+ updateStudioQuartoPreviewState({ status: "running", url: previewUrl, log, error: null }, { immediate: true });
11919
+ return;
11920
+ }
11921
+ updateStudioQuartoPreviewState({ log, url: previewUrl });
11922
+ };
11923
+ child.stdout?.on("data", handleOutput);
11924
+ child.stderr?.on("data", handleOutput);
11925
+ child.once("error", (error) => {
11926
+ failStudioQuartoPreview(generation, `Quarto preview failed to start: ${error.message}`);
11927
+ });
11928
+ child.once("close", (code, signal) => {
11929
+ if (generation !== studioQuartoPreviewGeneration) return;
11930
+ studioQuartoPreviewProcess = null;
11931
+ clearStudioQuartoPreviewStartupTimer();
11932
+ const detail = signal ? `signal ${signal}` : `code ${code ?? "unknown"}`;
11933
+ failStudioQuartoPreview(generation, `Quarto preview exited unexpectedly (${detail}).`);
11934
+ });
11935
+ studioQuartoPreviewStartupTimer = setTimeout(() => {
11936
+ failStudioQuartoPreview(generation, `Quarto did not report a loopback preview URL within ${Math.round(STUDIO_QUARTO_START_TIMEOUT_MS / 1000)} seconds.`);
11937
+ }, STUDIO_QUARTO_START_TIMEOUT_MS);
11938
+ };
11939
+
11940
+ const enqueueStudioQuartoPreviewOperation = (operation: () => Promise<void>): Promise<void> => {
11941
+ const next = studioQuartoPreviewOperation.then(operation, operation);
11942
+ studioQuartoPreviewOperation = next.catch(() => {});
11943
+ return next;
11944
+ };
11945
+
11540
11946
  const sendReplStateToClient = (client: WebSocket, extra?: Record<string, unknown>) => {
11541
11947
  const state = listStudioReplSessions();
11542
11948
  if (studioReplActiveSessionName && !state.sessions.some((session) => session.sessionName === studioReplActiveSessionName)) {
@@ -12227,6 +12633,7 @@ export default function (pi: ExtensionAPI) {
12227
12633
  responseHistory: studioResponseHistory,
12228
12634
  traceState: studioTraceState,
12229
12635
  initialDocument: initialStudioDocument,
12636
+ quartoPreview: getStudioQuartoPreviewSnapshot(),
12230
12637
  });
12231
12638
  return;
12232
12639
  }
@@ -12330,6 +12737,62 @@ export default function (pi: ExtensionAPI) {
12330
12737
  return;
12331
12738
  }
12332
12739
 
12740
+ if (msg.type === "quarto_preview_check_request") {
12741
+ if (!isValidRequestId(msg.requestId)) {
12742
+ sendToClient(client, { type: "quarto_preview_action_error", requestId: msg.requestId, message: "Invalid request ID." });
12743
+ return;
12744
+ }
12745
+ void inspectStudioQuartoPreviewContext(msg.sourcePath)
12746
+ .then((context) => {
12747
+ sendToClient(client, { type: "quarto_preview_context", requestId: msg.requestId, context });
12748
+ })
12749
+ .catch((error) => {
12750
+ sendToClient(client, {
12751
+ type: "quarto_preview_action_error",
12752
+ requestId: msg.requestId,
12753
+ message: `Quarto check failed: ${error instanceof Error ? error.message : String(error)}`,
12754
+ });
12755
+ });
12756
+ return;
12757
+ }
12758
+
12759
+ if (msg.type === "quarto_preview_start_request") {
12760
+ if (!isValidRequestId(msg.requestId)) {
12761
+ sendToClient(client, { type: "quarto_preview_action_error", requestId: msg.requestId, message: "Invalid request ID." });
12762
+ return;
12763
+ }
12764
+ void enqueueStudioQuartoPreviewOperation(async () => {
12765
+ const context = await inspectStudioQuartoPreviewContext(msg.sourcePath);
12766
+ sendToClient(client, { type: "quarto_preview_context", requestId: msg.requestId, context });
12767
+ if (!context.available) return;
12768
+ await startStudioQuartoPreview(context, msg.requestId);
12769
+ }).catch((error) => {
12770
+ sendToClient(client, {
12771
+ type: "quarto_preview_action_error",
12772
+ requestId: msg.requestId,
12773
+ message: `Quarto preview failed: ${error instanceof Error ? error.message : String(error)}`,
12774
+ });
12775
+ });
12776
+ return;
12777
+ }
12778
+
12779
+ if (msg.type === "quarto_preview_stop_request") {
12780
+ if (!isValidRequestId(msg.requestId)) {
12781
+ sendToClient(client, { type: "quarto_preview_action_error", requestId: msg.requestId, message: "Invalid request ID." });
12782
+ return;
12783
+ }
12784
+ void enqueueStudioQuartoPreviewOperation(async () => {
12785
+ await stopStudioQuartoPreview(msg.requestId);
12786
+ }).catch((error) => {
12787
+ sendToClient(client, {
12788
+ type: "quarto_preview_action_error",
12789
+ requestId: msg.requestId,
12790
+ message: `Could not stop Quarto preview: ${error instanceof Error ? error.message : String(error)}`,
12791
+ });
12792
+ });
12793
+ return;
12794
+ }
12795
+
12333
12796
  if (msg.type === "git_changes_request") {
12334
12797
  if (!isValidRequestId(msg.requestId)) {
12335
12798
  sendToClient(client, { type: "error", requestId: msg.requestId, message: "Invalid request ID." });
@@ -14517,6 +14980,13 @@ export default function (pi: ExtensionAPI) {
14517
14980
  clearPreparedPdfExports();
14518
14981
  clearPreparedHtmlExports();
14519
14982
  clearCompactionState();
14983
+ await enqueueStudioQuartoPreviewOperation(async () => {
14984
+ await stopStudioQuartoPreview(null, { quiet: true });
14985
+ }).catch(() => {});
14986
+ if (studioQuartoPreviewBroadcastTimer) {
14987
+ clearTimeout(studioQuartoPreviewBroadcastTimer);
14988
+ studioQuartoPreviewBroadcastTimer = null;
14989
+ }
14520
14990
  closeAllClients(1001, "Server shutting down");
14521
14991
 
14522
14992
  const state = serverState;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-studio",
3
- "version": "0.9.38",
3
+ "version": "0.9.40",
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",