pi-studio 0.9.19 → 0.9.21
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 -1
- package/README.md +4 -4
- package/client/studio-annotation-helpers.js +20 -1
- package/client/studio-client.js +847 -82
- package/client/studio.css +107 -2
- package/index.ts +176 -21
- package/package.json +1 -1
- package/shared/studio-markdown-fences.js +22 -0
- package/shared/studio-markdown-fences.ts +22 -0
package/client/studio.css
CHANGED
|
@@ -392,7 +392,6 @@
|
|
|
392
392
|
}
|
|
393
393
|
|
|
394
394
|
body[data-studio-mode="editor-only"] #editorViewSelect,
|
|
395
|
-
body[data-studio-mode="editor-only"] #rightViewSelect,
|
|
396
395
|
body[data-studio-mode="editor-only"] #sendRunBtn,
|
|
397
396
|
body[data-studio-mode="editor-only"] #queueSteerBtn,
|
|
398
397
|
body[data-studio-mode="editor-only"] #sendEditorBtn,
|
|
@@ -424,7 +423,7 @@
|
|
|
424
423
|
}
|
|
425
424
|
|
|
426
425
|
body[data-studio-mode="editor-only"] #rightSectionHeader .section-header-main::before {
|
|
427
|
-
content: "
|
|
426
|
+
content: "View";
|
|
428
427
|
font-weight: 600;
|
|
429
428
|
font-size: 14px;
|
|
430
429
|
}
|
|
@@ -2506,6 +2505,35 @@
|
|
|
2506
2505
|
white-space: nowrap;
|
|
2507
2506
|
}
|
|
2508
2507
|
|
|
2508
|
+
.rendered-markdown .studio-html-artifact-comment-btn {
|
|
2509
|
+
flex: 0 0 auto;
|
|
2510
|
+
min-height: 24px;
|
|
2511
|
+
padding: 0 9px;
|
|
2512
|
+
border: 1px solid var(--control-border);
|
|
2513
|
+
border-radius: 999px;
|
|
2514
|
+
background: var(--panel);
|
|
2515
|
+
color: var(--text);
|
|
2516
|
+
font: inherit;
|
|
2517
|
+
font-size: 11px;
|
|
2518
|
+
line-height: 1;
|
|
2519
|
+
cursor: pointer;
|
|
2520
|
+
white-space: nowrap;
|
|
2521
|
+
}
|
|
2522
|
+
|
|
2523
|
+
.rendered-markdown .studio-html-artifact-comment-btn:not(:disabled):hover,
|
|
2524
|
+
.rendered-markdown .studio-html-artifact-comment-btn:focus-visible {
|
|
2525
|
+
background: var(--control-hover-bg, var(--inline-code-bg));
|
|
2526
|
+
border-color: var(--control-border-hover, var(--accent));
|
|
2527
|
+
outline: none;
|
|
2528
|
+
}
|
|
2529
|
+
|
|
2530
|
+
.rendered-markdown .studio-html-artifact-comment-btn.is-active,
|
|
2531
|
+
.rendered-markdown .studio-html-artifact-shell.is-comment-mode .studio-html-artifact-comment-btn.is-active {
|
|
2532
|
+
background: var(--accent-soft);
|
|
2533
|
+
border-color: var(--accent);
|
|
2534
|
+
color: var(--accent);
|
|
2535
|
+
}
|
|
2536
|
+
|
|
2509
2537
|
.rendered-markdown .studio-html-artifact-zoom-controls {
|
|
2510
2538
|
flex: 0 0 auto;
|
|
2511
2539
|
display: inline-flex;
|
|
@@ -4036,6 +4064,83 @@
|
|
|
4036
4064
|
line-height: 1.35;
|
|
4037
4065
|
}
|
|
4038
4066
|
|
|
4067
|
+
body[data-studio-mode="editor-only"] .shortcuts-full-only {
|
|
4068
|
+
display: none !important;
|
|
4069
|
+
}
|
|
4070
|
+
|
|
4071
|
+
.scratchpad-recent-panel {
|
|
4072
|
+
flex: 0 0 auto;
|
|
4073
|
+
max-height: 260px;
|
|
4074
|
+
overflow: auto;
|
|
4075
|
+
border-bottom: 1px solid var(--panel-border);
|
|
4076
|
+
background: var(--scratchpad-body-bg, var(--panel));
|
|
4077
|
+
padding: 10px 12px;
|
|
4078
|
+
}
|
|
4079
|
+
|
|
4080
|
+
.scratchpad-recent-panel[hidden] {
|
|
4081
|
+
display: none !important;
|
|
4082
|
+
}
|
|
4083
|
+
|
|
4084
|
+
.scratchpad-recent-empty,
|
|
4085
|
+
.scratchpad-recent-loading {
|
|
4086
|
+
color: var(--studio-info-text, var(--muted));
|
|
4087
|
+
font-size: 12px;
|
|
4088
|
+
padding: 8px 6px;
|
|
4089
|
+
}
|
|
4090
|
+
|
|
4091
|
+
.scratchpad-recent-list {
|
|
4092
|
+
display: grid;
|
|
4093
|
+
gap: 8px;
|
|
4094
|
+
}
|
|
4095
|
+
|
|
4096
|
+
.scratchpad-recent-item {
|
|
4097
|
+
display: grid;
|
|
4098
|
+
grid-template-columns: minmax(0, 1fr) auto;
|
|
4099
|
+
gap: 8px 12px;
|
|
4100
|
+
align-items: start;
|
|
4101
|
+
padding: 9px 10px;
|
|
4102
|
+
border: 1px solid var(--border-subtle);
|
|
4103
|
+
border-radius: 10px;
|
|
4104
|
+
background: var(--panel-2);
|
|
4105
|
+
}
|
|
4106
|
+
|
|
4107
|
+
.scratchpad-recent-main {
|
|
4108
|
+
min-width: 0;
|
|
4109
|
+
}
|
|
4110
|
+
|
|
4111
|
+
.scratchpad-recent-title {
|
|
4112
|
+
font-size: 12px;
|
|
4113
|
+
font-weight: 650;
|
|
4114
|
+
color: var(--text);
|
|
4115
|
+
overflow: hidden;
|
|
4116
|
+
text-overflow: ellipsis;
|
|
4117
|
+
white-space: nowrap;
|
|
4118
|
+
}
|
|
4119
|
+
|
|
4120
|
+
.scratchpad-recent-meta,
|
|
4121
|
+
.scratchpad-recent-preview {
|
|
4122
|
+
font-size: 11px;
|
|
4123
|
+
color: var(--studio-info-text, var(--muted));
|
|
4124
|
+
line-height: 1.35;
|
|
4125
|
+
}
|
|
4126
|
+
|
|
4127
|
+
.scratchpad-recent-preview {
|
|
4128
|
+
margin-top: 3px;
|
|
4129
|
+
}
|
|
4130
|
+
|
|
4131
|
+
.scratchpad-recent-actions {
|
|
4132
|
+
display: inline-flex;
|
|
4133
|
+
gap: 6px;
|
|
4134
|
+
flex-wrap: wrap;
|
|
4135
|
+
justify-content: flex-end;
|
|
4136
|
+
}
|
|
4137
|
+
|
|
4138
|
+
.scratchpad-recent-actions button {
|
|
4139
|
+
padding: 4px 7px;
|
|
4140
|
+
font-size: 11px;
|
|
4141
|
+
line-height: 1.1;
|
|
4142
|
+
}
|
|
4143
|
+
|
|
4039
4144
|
.scratchpad-textarea {
|
|
4040
4145
|
width: 100%;
|
|
4041
4146
|
min-height: 280px;
|
package/index.ts
CHANGED
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
transformStudioMarkdownOutsideFences,
|
|
23
23
|
} from "./shared/studio-annotation-scanner.js";
|
|
24
24
|
import { stripStudioMarkdownHtmlComments } from "./shared/studio-markdown-html-comments.js";
|
|
25
|
+
import { normalizeStudioMarkdownSmartFences } from "./shared/studio-markdown-fences.js";
|
|
25
26
|
import {
|
|
26
27
|
extractStandaloneLatexDefinitionsFromMarkdown,
|
|
27
28
|
preserveLiteralLatexCommandsInMarkdown,
|
|
@@ -209,6 +210,8 @@ interface InitialStudioDocument {
|
|
|
209
210
|
resourceDir?: string;
|
|
210
211
|
}
|
|
211
212
|
|
|
213
|
+
type PersistedStudioReviewNoteAnchorKind = "source" | "html-selection" | "html-element" | "html-page";
|
|
214
|
+
|
|
212
215
|
interface PersistedStudioReviewNote {
|
|
213
216
|
id: string;
|
|
214
217
|
text: string;
|
|
@@ -220,11 +223,22 @@ interface PersistedStudioReviewNote {
|
|
|
220
223
|
lineEnd: number;
|
|
221
224
|
selectedText: string;
|
|
222
225
|
selectedDisplayText?: string;
|
|
226
|
+
anchorKind?: PersistedStudioReviewNoteAnchorKind;
|
|
227
|
+
htmlSelector?: string;
|
|
228
|
+
htmlTag?: string;
|
|
229
|
+
htmlLabel?: string;
|
|
230
|
+
htmlPreviewTitle?: string;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
interface PersistedStudioScratchpadMetadata {
|
|
234
|
+
label?: string;
|
|
235
|
+
updatedAt?: number;
|
|
223
236
|
}
|
|
224
237
|
|
|
225
238
|
interface StudioPersistentState {
|
|
226
239
|
version: 2;
|
|
227
240
|
scratchpadsByDocument: Record<string, string>;
|
|
241
|
+
scratchpadMetadataByDocument: Record<string, PersistedStudioScratchpadMetadata>;
|
|
228
242
|
reviewNotesByDocument: Record<string, PersistedStudioReviewNote[]>;
|
|
229
243
|
}
|
|
230
244
|
|
|
@@ -721,10 +735,15 @@ function createEmptyStudioPersistentState(): StudioPersistentState {
|
|
|
721
735
|
return {
|
|
722
736
|
version: 2,
|
|
723
737
|
scratchpadsByDocument: {},
|
|
738
|
+
scratchpadMetadataByDocument: {},
|
|
724
739
|
reviewNotesByDocument: {},
|
|
725
740
|
};
|
|
726
741
|
}
|
|
727
742
|
|
|
743
|
+
function normalizePersistedStudioReviewNoteAnchorKind(value: unknown): PersistedStudioReviewNoteAnchorKind {
|
|
744
|
+
return value === "html-selection" || value === "html-element" || value === "html-page" ? value : "source";
|
|
745
|
+
}
|
|
746
|
+
|
|
728
747
|
function normalizePersistedStudioReviewNote(value: unknown): PersistedStudioReviewNote | null {
|
|
729
748
|
if (!value || typeof value !== "object") return null;
|
|
730
749
|
const candidate = value as Partial<PersistedStudioReviewNote>;
|
|
@@ -759,6 +778,11 @@ function normalizePersistedStudioReviewNote(value: unknown): PersistedStudioRevi
|
|
|
759
778
|
lineEnd,
|
|
760
779
|
selectedText: typeof candidate.selectedText === "string" ? candidate.selectedText : "",
|
|
761
780
|
selectedDisplayText: typeof candidate.selectedDisplayText === "string" ? candidate.selectedDisplayText : "",
|
|
781
|
+
anchorKind: normalizePersistedStudioReviewNoteAnchorKind(candidate.anchorKind),
|
|
782
|
+
htmlSelector: typeof candidate.htmlSelector === "string" ? candidate.htmlSelector : "",
|
|
783
|
+
htmlTag: typeof candidate.htmlTag === "string" ? candidate.htmlTag : "",
|
|
784
|
+
htmlLabel: typeof candidate.htmlLabel === "string" ? candidate.htmlLabel : "",
|
|
785
|
+
htmlPreviewTitle: typeof candidate.htmlPreviewTitle === "string" ? candidate.htmlPreviewTitle : "",
|
|
762
786
|
};
|
|
763
787
|
}
|
|
764
788
|
|
|
@@ -768,6 +792,7 @@ function normalizeStudioPersistentState(value: unknown): StudioPersistentState {
|
|
|
768
792
|
const candidate = value as Partial<StudioPersistentState> & {
|
|
769
793
|
reviewNotesByDocument?: unknown;
|
|
770
794
|
scratchpadsByDocument?: unknown;
|
|
795
|
+
scratchpadMetadataByDocument?: unknown;
|
|
771
796
|
scratchpadText?: unknown;
|
|
772
797
|
};
|
|
773
798
|
const reviewNotesByDocument: Record<string, PersistedStudioReviewNote[]> = {};
|
|
@@ -791,9 +816,21 @@ function normalizeStudioPersistentState(value: unknown): StudioPersistentState {
|
|
|
791
816
|
} else if (typeof candidate.scratchpadText === "string" && candidate.scratchpadText.length > 0) {
|
|
792
817
|
scratchpadsByDocument[STUDIO_DEFAULT_SCRATCHPAD_DOCUMENT_KEY] = candidate.scratchpadText;
|
|
793
818
|
}
|
|
819
|
+
const scratchpadMetadataByDocument: Record<string, PersistedStudioScratchpadMetadata> = {};
|
|
820
|
+
if (candidate.scratchpadMetadataByDocument && typeof candidate.scratchpadMetadataByDocument === "object") {
|
|
821
|
+
for (const [documentKey, rawMeta] of Object.entries(candidate.scratchpadMetadataByDocument as Record<string, unknown>)) {
|
|
822
|
+
if (typeof documentKey !== "string" || !documentKey.trim() || !rawMeta || typeof rawMeta !== "object") continue;
|
|
823
|
+
const meta = rawMeta as { label?: unknown; updatedAt?: unknown };
|
|
824
|
+
scratchpadMetadataByDocument[documentKey] = {
|
|
825
|
+
label: typeof meta.label === "string" ? meta.label : undefined,
|
|
826
|
+
updatedAt: typeof meta.updatedAt === "number" && Number.isFinite(meta.updatedAt) ? meta.updatedAt : undefined,
|
|
827
|
+
};
|
|
828
|
+
}
|
|
829
|
+
}
|
|
794
830
|
return {
|
|
795
831
|
version: 2,
|
|
796
832
|
scratchpadsByDocument,
|
|
833
|
+
scratchpadMetadataByDocument,
|
|
797
834
|
reviewNotesByDocument,
|
|
798
835
|
};
|
|
799
836
|
}
|
|
@@ -836,16 +873,50 @@ async function readPersistedStudioScratchpadText(documentKey: string): Promise<s
|
|
|
836
873
|
return typeof value === "string" ? value : "";
|
|
837
874
|
}
|
|
838
875
|
|
|
839
|
-
|
|
876
|
+
function describePersistedScratchpadKey(documentKey: string): { label: string; kind: string } {
|
|
877
|
+
const key = String(documentKey || "").trim();
|
|
878
|
+
if (key.startsWith("file:")) return { label: key.slice(5) || "file", kind: "File" };
|
|
879
|
+
if (key.startsWith("draft:")) return { label: key.slice(6) || "draft", kind: "Draft" };
|
|
880
|
+
if (key.startsWith("doc:")) return { label: key.slice(4).replace(/^blank:/, "") || "document", kind: "Document" };
|
|
881
|
+
return { label: key || "scratchpad", kind: "Scratchpad" };
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
function summarizeScratchpadText(text: string): string {
|
|
885
|
+
const normalized = String(text || "").replace(/\s+/g, " ").trim();
|
|
886
|
+
return normalized.length > 160 ? `${normalized.slice(0, 157)}…` : normalized;
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
async function listRecentPersistedStudioScratchpads(limit = 20): Promise<Array<{ documentKey: string; label: string; kind: string; updatedAt: number; textPreview: string; textLength: number }>> {
|
|
890
|
+
const state = await loadStudioPersistentState();
|
|
891
|
+
return Object.entries(state.scratchpadsByDocument)
|
|
892
|
+
.filter((entry): entry is [string, string] => typeof entry[0] === "string" && typeof entry[1] === "string" && entry[1].trim().length > 0)
|
|
893
|
+
.map(([documentKey, text]) => {
|
|
894
|
+
const fallback = describePersistedScratchpadKey(documentKey);
|
|
895
|
+
const meta = state.scratchpadMetadataByDocument[documentKey] ?? {};
|
|
896
|
+
const label = typeof meta.label === "string" && meta.label.trim() ? meta.label.trim() : fallback.label;
|
|
897
|
+
const updatedAt = typeof meta.updatedAt === "number" && Number.isFinite(meta.updatedAt) ? meta.updatedAt : 0;
|
|
898
|
+
return { documentKey, label, kind: fallback.kind, updatedAt, textPreview: summarizeScratchpadText(text), textLength: text.length };
|
|
899
|
+
})
|
|
900
|
+
.sort((left, right) => (right.updatedAt - left.updatedAt) || left.label.localeCompare(right.label) || left.documentKey.localeCompare(right.documentKey))
|
|
901
|
+
.slice(0, Math.max(1, Math.min(100, Math.floor(limit) || 20)));
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
async function writePersistedStudioScratchpadText(documentKey: string, text: string, label?: string): Promise<void> {
|
|
840
905
|
const key = String(documentKey ?? "").trim();
|
|
841
906
|
if (!key) return;
|
|
842
907
|
await mutateStudioPersistentState((state) => {
|
|
843
908
|
const normalized = String(text ?? "");
|
|
844
909
|
if (normalized.length === 0) {
|
|
845
910
|
delete state.scratchpadsByDocument[key];
|
|
911
|
+
delete state.scratchpadMetadataByDocument[key];
|
|
846
912
|
return;
|
|
847
913
|
}
|
|
848
914
|
state.scratchpadsByDocument[key] = normalized;
|
|
915
|
+
state.scratchpadMetadataByDocument[key] = {
|
|
916
|
+
...(state.scratchpadMetadataByDocument[key] ?? {}),
|
|
917
|
+
label: typeof label === "string" && label.trim() ? label.trim() : state.scratchpadMetadataByDocument[key]?.label,
|
|
918
|
+
updatedAt: Date.now(),
|
|
919
|
+
};
|
|
849
920
|
});
|
|
850
921
|
}
|
|
851
922
|
|
|
@@ -4566,7 +4637,8 @@ function hasStudioYamlHeaderIncludes(markdown: string): boolean {
|
|
|
4566
4637
|
function prepareStudioMarkdownForPandoc(markdown: string, options?: { preserveLiteralLatexCommands?: boolean }): string {
|
|
4567
4638
|
const shouldPreserveLiteralLatexCommands = options?.preserveLiteralLatexCommands !== false;
|
|
4568
4639
|
return mapStudioMarkdownBodyPreservingYamlFrontMatter(markdown, (body) => {
|
|
4569
|
-
const
|
|
4640
|
+
const normalizedFences = normalizeStudioMarkdownSmartFences(body);
|
|
4641
|
+
const normalizedMath = normalizeMathDelimiters(normalizedFences);
|
|
4570
4642
|
const latexReady = shouldPreserveLiteralLatexCommands
|
|
4571
4643
|
? preserveLiteralLatexCommandsInMarkdown(normalizedMath)
|
|
4572
4644
|
: normalizedMath;
|
|
@@ -5416,9 +5488,10 @@ function prepareStudioPdfMarkdown(markdown: string, isLatex?: boolean, editorLan
|
|
|
5416
5488
|
&& !isStudioSingleFencedCodeBlock(input)
|
|
5417
5489
|
? wrapStudioCodeAsMarkdown(input, effectiveEditorLanguage)
|
|
5418
5490
|
: input;
|
|
5491
|
+
const fenceNormalizedSource = effectiveEditorLanguage === "latex" ? source : normalizeStudioMarkdownSmartFences(source);
|
|
5419
5492
|
const annotationReadySource = !effectiveEditorLanguage || effectiveEditorLanguage === "markdown" || effectiveEditorLanguage === "latex"
|
|
5420
|
-
? replaceStudioAnnotationMarkersForPdf(
|
|
5421
|
-
:
|
|
5493
|
+
? replaceStudioAnnotationMarkersForPdf(fenceNormalizedSource)
|
|
5494
|
+
: fenceNormalizedSource;
|
|
5422
5495
|
const commentStrippedSource = stripStudioMarkdownHtmlCommentsPreservingYamlFrontMatter(annotationReadySource);
|
|
5423
5496
|
return prepareStudioMarkdownForPandoc(commentStrippedSource, {
|
|
5424
5497
|
preserveLiteralLatexCommands: !hasStudioYamlHeaderIncludes(annotationReadySource),
|
|
@@ -5717,7 +5790,8 @@ function decorateStudioPandocSyntaxHtml(html: string): string {
|
|
|
5717
5790
|
|
|
5718
5791
|
async function renderStudioMarkdownWithPandoc(markdown: string, isLatex?: boolean, resourcePath?: string, sourcePath?: string): Promise<string> {
|
|
5719
5792
|
const pandocCommand = process.env.PANDOC_PATH?.trim() || "pandoc";
|
|
5720
|
-
const
|
|
5793
|
+
const markdownWithNormalizedFences = isLatex ? markdown : normalizeStudioMarkdownSmartFences(markdown);
|
|
5794
|
+
const markdownWithoutHtmlComments = isLatex ? markdownWithNormalizedFences : stripStudioMarkdownHtmlCommentsPreservingYamlFrontMatter(markdownWithNormalizedFences);
|
|
5721
5795
|
const markdownWithPreviewPageBreaks = isLatex ? markdownWithoutHtmlComments : replaceStudioPreviewPageBreakCommands(markdownWithoutHtmlComments);
|
|
5722
5796
|
const latexSubfigurePreviewTransform = isLatex
|
|
5723
5797
|
? preprocessStudioLatexSubfiguresForPreview(markdownWithPreviewPageBreaks)
|
|
@@ -10042,14 +10116,14 @@ ${cssVarsBlock}
|
|
|
10042
10116
|
<div class="scratchpad-header">
|
|
10043
10117
|
<div>
|
|
10044
10118
|
<h2 id="reviewNotesTitle">Comments</h2>
|
|
10045
|
-
<p class="scratchpad-description">Local comments for editor text.
|
|
10119
|
+
<p class="scratchpad-description">Local comments for editor text and editor previews. They stay out of the text; source-anchored comments can be converted into inline <span class="review-notes-inline-token">[an: ...]</span> annotations.</p>
|
|
10046
10120
|
</div>
|
|
10047
10121
|
<button id="reviewNotesCloseBtn" type="button" class="scratchpad-close-btn" aria-label="Hide comments" title="Hide comments">✕</button>
|
|
10048
10122
|
</div>
|
|
10049
10123
|
<div class="review-notes-toolbar">
|
|
10050
10124
|
<span id="reviewNotesMeta" class="scratchpad-meta">No comments</span>
|
|
10051
10125
|
</div>
|
|
10052
|
-
<div id="reviewNotesEmptyState" class="review-notes-empty">No comments yet for this document. Select text in <strong>Editor (Raw)</strong> or <strong>Editor (Preview)</strong> and use <em>Comment</em>,
|
|
10126
|
+
<div id="reviewNotesEmptyState" class="review-notes-empty">No comments yet for this document. Select text in <strong>Editor (Raw)</strong> or <strong>Editor (Preview)</strong> and use <em>Comment</em>, use <em>Line comment</em> in <strong>Editor (Raw)</strong>, or use <em>Comment mode</em> in an editor HTML preview.</div>
|
|
10053
10127
|
<div id="reviewNotesList" class="review-notes-list" aria-live="polite"></div>
|
|
10054
10128
|
<div class="review-notes-dock-footer">
|
|
10055
10129
|
<div class="scratchpad-actions">
|
|
@@ -10085,8 +10159,10 @@ ${cssVarsBlock}
|
|
|
10085
10159
|
<span id="exportPreviewControls" class="export-preview-controls">
|
|
10086
10160
|
<button id="exportPdfBtn" class="export-preview-trigger" type="button" aria-haspopup="menu" aria-expanded="false" title="Choose a format and export the current right-pane preview.">Export right preview</button>
|
|
10087
10161
|
<div id="exportPreviewMenu" class="export-preview-menu" role="menu" hidden>
|
|
10088
|
-
<button id="
|
|
10089
|
-
<button id="
|
|
10162
|
+
<button id="exportPreviewPdfStudioBtn" type="button" role="menuitem" data-export-preview-format="pdf-studio">Export PDF and Open in Studio preview tab</button>
|
|
10163
|
+
<button id="exportPreviewPdfBtn" type="button" role="menuitem" data-export-preview-format="pdf-default">Export PDF and Open in default PDF viewer</button>
|
|
10164
|
+
<button id="exportPreviewHtmlStudioBtn" type="button" role="menuitem" data-export-preview-format="html-studio">Export HTML and Open in Studio editor</button>
|
|
10165
|
+
<button id="exportPreviewHtmlBtn" type="button" role="menuitem" data-export-preview-format="html-browser">Export HTML and Open in browser</button>
|
|
10090
10166
|
</div>
|
|
10091
10167
|
</span>
|
|
10092
10168
|
</div>
|
|
@@ -10181,14 +10257,14 @@ ${cssVarsBlock}
|
|
|
10181
10257
|
<h3>Editor</h3>
|
|
10182
10258
|
<dl>
|
|
10183
10259
|
<div><dt>Cmd/Ctrl+S</dt><dd>Save editor</dd></div>
|
|
10184
|
-
<div><dt>Cmd/Ctrl+Enter</dt><dd>Run editor text, or queue steering during an active run</dd></div>
|
|
10260
|
+
<div class="shortcuts-full-only"><dt>Cmd/Ctrl+Enter</dt><dd>Run editor text, or queue steering during an active run</dd></div>
|
|
10185
10261
|
<div><dt>Option/Alt+Tab or Cmd/Ctrl+Shift+Space</dt><dd>Suggest a completion at the editor cursor</dd></div>
|
|
10186
10262
|
<div><dt>Tab</dt><dd>Insert a visible completion suggestion; otherwise indent selected editor text</dd></div>
|
|
10187
10263
|
<div><dt>Esc</dt><dd>Dismiss a visible completion suggestion, close overlays, exit pane focus, or stop an active request</dd></div>
|
|
10188
10264
|
<div><dt>Shift+Tab</dt><dd>Unindent selected editor text</dd></div>
|
|
10189
10265
|
</dl>
|
|
10190
10266
|
</section>
|
|
10191
|
-
<section class="shortcuts-group">
|
|
10267
|
+
<section class="shortcuts-group shortcuts-full-only">
|
|
10192
10268
|
<h3>Response</h3>
|
|
10193
10269
|
<dl>
|
|
10194
10270
|
<div><dt>Alt/Option+←</dt><dd>Previous response when not editing text</dd></div>
|
|
@@ -10211,14 +10287,16 @@ ${cssVarsBlock}
|
|
|
10211
10287
|
<div class="scratchpad-header">
|
|
10212
10288
|
<div>
|
|
10213
10289
|
<h2 id="scratchpadTitle">Scratchpad</h2>
|
|
10214
|
-
<p class="scratchpad-description">Local persistent notes for thoughts you want to park while working on the current Studio document or draft. Closing the scratchpad does not clear it: notes persist locally for this document identity until you edit or clear them. File-backed documents reliably come back across Pi restarts; unsaved drafts stay with their own draft instance until you save them or discard them. Scratchpad text is not run, critiqued, sent, or exported unless you explicitly insert it into the editor.</p>
|
|
10290
|
+
<p class="scratchpad-description">Local persistent notes for thoughts you want to park while working on the current Studio document or draft. Closing the scratchpad does not clear it: notes persist locally for this document identity until you edit or clear them. File-backed documents reliably come back across Pi restarts; unsaved drafts stay with their own draft instance until you save them or discard them. Use Recent… to recover scratchpads from other draft identities after a Studio/Pi restart. Scratchpad text is not run, critiqued, sent, or exported unless you explicitly insert it into the editor.</p>
|
|
10215
10291
|
</div>
|
|
10216
10292
|
<button id="scratchpadCloseBtn" type="button" class="scratchpad-close-btn" aria-label="Keep current scratchpad text and close scratchpad" title="Keep current scratchpad text and close scratchpad">✕</button>
|
|
10217
10293
|
</div>
|
|
10294
|
+
<div id="scratchpadRecentPanel" class="scratchpad-recent-panel" hidden></div>
|
|
10218
10295
|
<textarea id="scratchpadText" class="scratchpad-textarea" placeholder="Jot quick thoughts, TODOs, or prompt ideas here..."></textarea>
|
|
10219
10296
|
<div class="scratchpad-footer">
|
|
10220
10297
|
<span id="scratchpadMeta" class="scratchpad-meta">Empty · local only</span>
|
|
10221
10298
|
<div class="scratchpad-actions">
|
|
10299
|
+
<button id="scratchpadRecentBtn" type="button" title="Show recent non-empty scratchpads saved for other files and drafts.">Recent…</button>
|
|
10222
10300
|
<button id="scratchpadInsertBtn" type="button" title="Insert the scratchpad text into the editor at the current selection, or append it if no editor selection is available.">Insert into editor</button>
|
|
10223
10301
|
<button id="scratchpadCopyBtn" type="button" title="Copy scratchpad text to the clipboard.">Copy</button>
|
|
10224
10302
|
<button id="scratchpadClearBtn" type="button" title="Clear scratchpad text.">Clear</button>
|
|
@@ -12591,6 +12669,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
12591
12669
|
const handleScratchpadStateRequest = async (req: IncomingMessage, res: ServerResponse, requestUrl: URL) => {
|
|
12592
12670
|
const method = (req.method ?? "GET").toUpperCase();
|
|
12593
12671
|
if (method === "GET") {
|
|
12672
|
+
const action = (requestUrl.searchParams.get("action") ?? "").trim().toLowerCase();
|
|
12673
|
+
if (action === "recent") {
|
|
12674
|
+
const limit = Number.parseInt(requestUrl.searchParams.get("limit") ?? "20", 10);
|
|
12675
|
+
respondJson(res, 200, { ok: true, scratchpads: await listRecentPersistedStudioScratchpads(limit) });
|
|
12676
|
+
return;
|
|
12677
|
+
}
|
|
12594
12678
|
const documentKey = (requestUrl.searchParams.get("documentKey") ?? "").trim();
|
|
12595
12679
|
if (!documentKey) {
|
|
12596
12680
|
respondJson(res, 400, { ok: false, error: "Missing documentKey query parameter." });
|
|
@@ -12640,8 +12724,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
12640
12724
|
respondJson(res, 400, { ok: false, error: "Missing scratchpad text in request body." });
|
|
12641
12725
|
return;
|
|
12642
12726
|
}
|
|
12727
|
+
const label =
|
|
12728
|
+
parsedBody && typeof parsedBody === "object" && typeof (parsedBody as { label?: unknown }).label === "string"
|
|
12729
|
+
? (parsedBody as { label: string }).label
|
|
12730
|
+
: undefined;
|
|
12643
12731
|
|
|
12644
|
-
await writePersistedStudioScratchpadText(documentKey, text);
|
|
12732
|
+
await writePersistedStudioScratchpadText(documentKey, text, label);
|
|
12645
12733
|
respondJson(res, 200, { ok: true });
|
|
12646
12734
|
};
|
|
12647
12735
|
|
|
@@ -12950,6 +13038,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
12950
13038
|
parsedBody && typeof parsedBody === "object" && typeof (parsedBody as { editorPdfLanguage?: unknown }).editorPdfLanguage === "string"
|
|
12951
13039
|
? (parsedBody as { editorPdfLanguage: string }).editorPdfLanguage
|
|
12952
13040
|
: "";
|
|
13041
|
+
const requestedOpenTarget =
|
|
13042
|
+
parsedBody && typeof parsedBody === "object" && typeof (parsedBody as { openTarget?: unknown }).openTarget === "string"
|
|
13043
|
+
? (parsedBody as { openTarget: string }).openTarget.trim().toLowerCase()
|
|
13044
|
+
: "default";
|
|
13045
|
+
const openTarget = requestedOpenTarget === "studio" ? "studio" : "default";
|
|
12953
13046
|
const editorPdfLanguage = inferStudioPdfLanguage(markdown, requestedEditorPdfLanguage);
|
|
12954
13047
|
const isLatex = editorPdfLanguage === "latex"
|
|
12955
13048
|
|| (
|
|
@@ -12963,17 +13056,48 @@ export default function (pi: ExtensionAPI) {
|
|
|
12963
13056
|
const writeResult = writeStudioPreviewExportFile(buildStudioPreviewExportPath(sourcePath || undefined, userResourceDir || undefined, studioCwd, filename), pdf);
|
|
12964
13057
|
const exportId = storePreparedPdfExport(pdf, filename, warning, writeResult.filePath ?? undefined);
|
|
12965
13058
|
const token = serverState?.token ?? "";
|
|
13059
|
+
if (openTarget === "studio" && serverState && writeResult.filePath) {
|
|
13060
|
+
const exportedPath = writeResult.filePath;
|
|
13061
|
+
const title = sanitizeStudioPreviewBlockLine(filename || basename(exportedPath) || "PDF preview");
|
|
13062
|
+
const document: InitialStudioDocument = {
|
|
13063
|
+
text: "```studio-pdf\n"
|
|
13064
|
+
+ `path: ${sanitizeStudioPreviewBlockLine(basename(exportedPath))}\n`
|
|
13065
|
+
+ `title: ${title || "PDF preview"}\n`
|
|
13066
|
+
+ "height: 820\n"
|
|
13067
|
+
+ "```\n",
|
|
13068
|
+
label: `${filename || basename(exportedPath) || "PDF"} preview`,
|
|
13069
|
+
source: "blank",
|
|
13070
|
+
resourceDir: dirname(exportedPath),
|
|
13071
|
+
};
|
|
13072
|
+
const docId = storeTransientStudioDocument(document);
|
|
13073
|
+
const url = buildStudioUrl(serverState.port, serverState.token, "editor-only", document, docId);
|
|
13074
|
+
const parsedUrl = new URL(url);
|
|
13075
|
+
respondJson(res, 200, {
|
|
13076
|
+
ok: true,
|
|
13077
|
+
filename,
|
|
13078
|
+
path: writeResult.filePath,
|
|
13079
|
+
writeError: writeResult.error,
|
|
13080
|
+
warning: warning ?? null,
|
|
13081
|
+
openedStudio: true,
|
|
13082
|
+
url,
|
|
13083
|
+
relativeUrl: `${parsedUrl.pathname}${parsedUrl.search}`,
|
|
13084
|
+
downloadUrl: `/export-pdf?token=${encodeURIComponent(token)}&id=${encodeURIComponent(exportId)}`,
|
|
13085
|
+
});
|
|
13086
|
+
return;
|
|
13087
|
+
}
|
|
12966
13088
|
let openedExternal = false;
|
|
12967
13089
|
let openError: string | null = null;
|
|
12968
|
-
|
|
12969
|
-
|
|
12970
|
-
|
|
12971
|
-
|
|
13090
|
+
if (openTarget !== "studio") {
|
|
13091
|
+
try {
|
|
13092
|
+
const prepared = await ensurePreparedPdfExportFile(exportId);
|
|
13093
|
+
if (!prepared?.filePath) {
|
|
13094
|
+
throw new Error("Prepared PDF file was not available for external open.");
|
|
13095
|
+
}
|
|
13096
|
+
await openPathInDefaultViewer(prepared.filePath);
|
|
13097
|
+
openedExternal = true;
|
|
13098
|
+
} catch (viewerError) {
|
|
13099
|
+
openError = viewerError instanceof Error ? viewerError.message : String(viewerError);
|
|
12972
13100
|
}
|
|
12973
|
-
await openPathInDefaultViewer(prepared.filePath);
|
|
12974
|
-
openedExternal = true;
|
|
12975
|
-
} catch (viewerError) {
|
|
12976
|
-
openError = viewerError instanceof Error ? viewerError.message : String(viewerError);
|
|
12977
13101
|
}
|
|
12978
13102
|
respondJson(res, 200, {
|
|
12979
13103
|
ok: true,
|
|
@@ -13052,6 +13176,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
13052
13176
|
parsedBody && typeof parsedBody === "object" && typeof (parsedBody as { editorHtmlLanguage?: unknown }).editorHtmlLanguage === "string"
|
|
13053
13177
|
? (parsedBody as { editorHtmlLanguage: string }).editorHtmlLanguage
|
|
13054
13178
|
: "";
|
|
13179
|
+
const requestedOpenTarget =
|
|
13180
|
+
parsedBody && typeof parsedBody === "object" && typeof (parsedBody as { openTarget?: unknown }).openTarget === "string"
|
|
13181
|
+
? (parsedBody as { openTarget: string }).openTarget.trim().toLowerCase()
|
|
13182
|
+
: "browser";
|
|
13183
|
+
const openTarget = requestedOpenTarget === "studio" ? "studio" : "browser";
|
|
13055
13184
|
const editorHtmlLanguage = inferStudioPdfLanguage(markdown, requestedEditorHtmlLanguage);
|
|
13056
13185
|
const isLatex = editorHtmlLanguage === "latex"
|
|
13057
13186
|
|| (
|
|
@@ -13077,6 +13206,32 @@ export default function (pi: ExtensionAPI) {
|
|
|
13077
13206
|
const writeResult = writeStudioPreviewExportFile(buildStudioPreviewExportPath(sourcePath || undefined, userResourceDir || undefined, studioCwd, filename), html);
|
|
13078
13207
|
const exportId = storePreparedHtmlExport(html, filename, warning, writeResult.filePath ?? undefined);
|
|
13079
13208
|
const token = serverState?.token ?? "";
|
|
13209
|
+
if (openTarget === "studio" && serverState) {
|
|
13210
|
+
const exportedPath = writeResult.filePath ?? "";
|
|
13211
|
+
const document: InitialStudioDocument = {
|
|
13212
|
+
text: html.toString("utf-8"),
|
|
13213
|
+
label: filename,
|
|
13214
|
+
source: exportedPath ? "file" : "blank",
|
|
13215
|
+
path: exportedPath || undefined,
|
|
13216
|
+
resourceDir: exportedPath ? dirname(exportedPath) : (userResourceDir || resourcePath || studioCwd),
|
|
13217
|
+
draftId: exportedPath ? undefined : createStudioDraftId(),
|
|
13218
|
+
};
|
|
13219
|
+
const docId = storeTransientStudioDocument(document);
|
|
13220
|
+
const url = buildStudioUrl(serverState.port, serverState.token, "editor-only", document, docId);
|
|
13221
|
+
const parsedUrl = new URL(url);
|
|
13222
|
+
respondJson(res, 200, {
|
|
13223
|
+
ok: true,
|
|
13224
|
+
filename,
|
|
13225
|
+
path: writeResult.filePath,
|
|
13226
|
+
writeError: writeResult.error,
|
|
13227
|
+
warning: warning ?? null,
|
|
13228
|
+
openedStudio: true,
|
|
13229
|
+
url,
|
|
13230
|
+
relativeUrl: `${parsedUrl.pathname}${parsedUrl.search}`,
|
|
13231
|
+
downloadUrl: `/export-html?token=${encodeURIComponent(token)}&id=${encodeURIComponent(exportId)}`,
|
|
13232
|
+
});
|
|
13233
|
+
return;
|
|
13234
|
+
}
|
|
13080
13235
|
let openedExternal = false;
|
|
13081
13236
|
let openError: string | null = null;
|
|
13082
13237
|
try {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-studio",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.21",
|
|
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",
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
const SMART_MARKDOWN_FENCE_CHARS = "`´‘’‚‛“”„‟′‵";
|
|
2
|
+
const SMART_MARKDOWN_FENCE_RE = new RegExp(`^([ \\t]{0,3})([${SMART_MARKDOWN_FENCE_CHARS.replace(/[\\\]^-]/g, "\\$&")}]{3,})([^${SMART_MARKDOWN_FENCE_CHARS.replace(/[\\\]^-]/g, "\\$&")}\\r\\n]*)$`);
|
|
3
|
+
|
|
4
|
+
function normalizeSmartFenceRun(run) {
|
|
5
|
+
return "`".repeat(Math.max(3, Array.from(String(run || "")).length));
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function normalizeStudioMarkdownSmartFences(markdown) {
|
|
9
|
+
return String(markdown ?? "")
|
|
10
|
+
.replace(/\r\n/g, "\n")
|
|
11
|
+
.split("\n")
|
|
12
|
+
.map((line) => {
|
|
13
|
+
const match = line.match(SMART_MARKDOWN_FENCE_RE);
|
|
14
|
+
if (!match) return line;
|
|
15
|
+
const indent = match[1] ?? "";
|
|
16
|
+
const run = match[2] ?? "";
|
|
17
|
+
const suffix = match[3] ?? "";
|
|
18
|
+
if (!/[´‘’‚‛“”„‟′‵]/u.test(run)) return line;
|
|
19
|
+
return `${indent}${normalizeSmartFenceRun(run)}${suffix}`;
|
|
20
|
+
})
|
|
21
|
+
.join("\n");
|
|
22
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
const SMART_MARKDOWN_FENCE_CHARS = "`´‘’‚‛“”„‟′‵";
|
|
2
|
+
const SMART_MARKDOWN_FENCE_RE = new RegExp(`^([ \\t]{0,3})([${SMART_MARKDOWN_FENCE_CHARS.replace(/[\\\]^-]/g, "\\$&")}]{3,})([^${SMART_MARKDOWN_FENCE_CHARS.replace(/[\\\]^-]/g, "\\$&")}\\r\\n]*)$`);
|
|
3
|
+
|
|
4
|
+
function normalizeSmartFenceRun(run: string): string {
|
|
5
|
+
return "`".repeat(Math.max(3, Array.from(String(run || "")).length));
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function normalizeStudioMarkdownSmartFences(markdown: string): string {
|
|
9
|
+
return String(markdown ?? "")
|
|
10
|
+
.replace(/\r\n/g, "\n")
|
|
11
|
+
.split("\n")
|
|
12
|
+
.map((line) => {
|
|
13
|
+
const match = line.match(SMART_MARKDOWN_FENCE_RE);
|
|
14
|
+
if (!match) return line;
|
|
15
|
+
const indent = match[1] ?? "";
|
|
16
|
+
const run = match[2] ?? "";
|
|
17
|
+
const suffix = match[3] ?? "";
|
|
18
|
+
if (!/[´‘’‚‛“”„‟′‵]/u.test(run)) return line;
|
|
19
|
+
return `${indent}${normalizeSmartFenceRun(run)}${suffix}`;
|
|
20
|
+
})
|
|
21
|
+
.join("\n");
|
|
22
|
+
}
|