pi-studio 0.9.37 → 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 +13 -0
- package/README.md +4 -1
- package/client/studio-client.js +537 -16
- package/client/studio.css +253 -0
- package/index.ts +474 -5
- package/package.json +1 -1
- package/shared/studio-mermaid.js +39 -3
- 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>
|
|
@@ -5843,14 +5901,14 @@ async function renderStudioMermaidDiagramForPdf(source: string, workDir: string,
|
|
|
5843
5901
|
const mermaidTheme = getStudioMermaidPdfTheme();
|
|
5844
5902
|
const inputPath = join(workDir, `mermaid-diagram-${blockNumber}.mmd`);
|
|
5845
5903
|
const outputPath = join(workDir, `mermaid-diagram-${blockNumber}.pdf`);
|
|
5846
|
-
const
|
|
5904
|
+
const contrastCssPath = join(workDir, `mermaid-diagram-${blockNumber}.css`);
|
|
5847
5905
|
|
|
5848
5906
|
const preparedSource = ensureStudioMermaidSourceContrast(source);
|
|
5849
|
-
const
|
|
5907
|
+
const contrastCss = buildStudioMermaidPdfIconContrastCss(preparedSource, { theme: mermaidTheme });
|
|
5850
5908
|
await writeFile(inputPath, preparedSource, "utf-8");
|
|
5851
|
-
if (
|
|
5909
|
+
if (contrastCss) await writeFile(contrastCssPath, contrastCss, "utf-8");
|
|
5852
5910
|
const args = ["-i", inputPath, "-o", outputPath, "-t", mermaidTheme, "-f"];
|
|
5853
|
-
if (
|
|
5911
|
+
if (contrastCss) args.push("-C", contrastCssPath);
|
|
5854
5912
|
args.push(...buildStudioMermaidCliIconArgs(preparedSource));
|
|
5855
5913
|
const result = await runStudioSubprocess(mermaidCommand, args, {
|
|
5856
5914
|
timeoutMs: STUDIO_MERMAID_TIMEOUT_MS,
|
|
@@ -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",
|
package/shared/studio-mermaid.js
CHANGED
|
@@ -232,12 +232,44 @@ function buildStudioMermaidIconPaintRule(selector, color) {
|
|
|
232
232
|
].join("\n");
|
|
233
233
|
}
|
|
234
234
|
|
|
235
|
-
|
|
235
|
+
function buildStudioMermaidPdfPrintColorRule() {
|
|
236
|
+
return [
|
|
237
|
+
".node, .node *, .icon-shape, .icon-shape * {",
|
|
238
|
+
" -webkit-print-color-adjust: exact !important;",
|
|
239
|
+
" print-color-adjust: exact !important;",
|
|
240
|
+
"}",
|
|
241
|
+
].join("\n");
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function buildStudioMermaidPdfIconLabelRule(selectors, theme) {
|
|
245
|
+
if (selectors.length === 0) return "";
|
|
246
|
+
const darkTheme = String(theme || "default").toLowerCase() === "dark";
|
|
247
|
+
const background = darkTheme ? "#1f2937" : "#ffffff";
|
|
248
|
+
const foreground = darkTheme ? "#ffffff" : "#000000";
|
|
249
|
+
const backgroundSelectors = selectors.map((selector) => `${selector} .labelBkg`).join(",\n");
|
|
250
|
+
const textSelectors = selectors.flatMap((selector) => [
|
|
251
|
+
`${selector} .nodeLabel`,
|
|
252
|
+
`${selector} .nodeLabel *`,
|
|
253
|
+
]).join(",\n");
|
|
254
|
+
return [
|
|
255
|
+
`${backgroundSelectors} { background-color: ${background} !important; }`,
|
|
256
|
+
`${textSelectors} {`,
|
|
257
|
+
` color: ${foreground} !important;`,
|
|
258
|
+
` fill: ${foreground} !important;`,
|
|
259
|
+
` -webkit-text-fill-color: ${foreground} !important;`,
|
|
260
|
+
" opacity: 1 !important;",
|
|
261
|
+
"}",
|
|
262
|
+
].join("\n");
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
export function buildStudioMermaidPdfIconContrastCss(source, options = {}) {
|
|
236
266
|
const preparedSource = ensureStudioMermaidSourceContrast(source);
|
|
237
267
|
const { iconNodeIds, classNamesByNode } = collectStudioMermaidIconStyleTargets(preparedSource);
|
|
238
|
-
|
|
268
|
+
const printColorRule = buildStudioMermaidPdfPrintColorRule();
|
|
269
|
+
if (iconNodeIds.size === 0) return printColorRule;
|
|
239
270
|
const classStyles = collectStudioMermaidStylesByTarget(preparedSource, "classDef");
|
|
240
271
|
const directStyles = collectStudioMermaidStylesByTarget(preparedSource, "style");
|
|
272
|
+
const iconSelectors = Array.from(iconNodeIds, (iconNodeId) => `.icon-shape[id*="flowchart-${iconNodeId}-"]`);
|
|
241
273
|
const rulesBySelector = new Map();
|
|
242
274
|
|
|
243
275
|
for (const iconNodeId of iconNodeIds) {
|
|
@@ -250,5 +282,9 @@ export function buildStudioMermaidPdfIconContrastCss(source) {
|
|
|
250
282
|
if (paint) rulesBySelector.set(`.icon-shape[id*="flowchart-${iconNodeId}-"]`, paint);
|
|
251
283
|
}
|
|
252
284
|
|
|
253
|
-
return
|
|
285
|
+
return [
|
|
286
|
+
printColorRule,
|
|
287
|
+
buildStudioMermaidPdfIconLabelRule(iconSelectors, options.theme),
|
|
288
|
+
...Array.from(rulesBySelector, ([selector, color]) => buildStudioMermaidIconPaintRule(selector, color)),
|
|
289
|
+
].filter(Boolean).join("\n");
|
|
254
290
|
}
|