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/CHANGELOG.md +14 -0
- package/README.md +4 -1
- package/client/studio-client.js +553 -21
- package/client/studio.css +253 -0
- package/index.ts +472 -2
- package/package.json +1 -1
- package/shared/studio-quarto-preview.js +169 -0
- package/shared/studio-repl-tmux.js +28 -0
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(
|
|
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.
|
|
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",
|