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