pi-studio 0.9.20 → 0.9.22
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 +18 -1
- package/README.md +2 -2
- package/client/studio-annotation-helpers.js +20 -1
- package/client/studio-client.js +451 -58
- package/client/studio.css +82 -0
- package/index.ts +168 -26
- 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
|
@@ -4068,6 +4068,79 @@
|
|
|
4068
4068
|
display: none !important;
|
|
4069
4069
|
}
|
|
4070
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
|
+
|
|
4071
4144
|
.scratchpad-textarea {
|
|
4072
4145
|
width: 100%;
|
|
4073
4146
|
min-height: 280px;
|
|
@@ -5103,6 +5176,15 @@
|
|
|
5103
5176
|
font-weight: 450;
|
|
5104
5177
|
}
|
|
5105
5178
|
|
|
5179
|
+
body.studio-ui-refresh .studio-refresh-menu-item > .source-origin-summary {
|
|
5180
|
+
width: 100%;
|
|
5181
|
+
border-color: var(--border-subtle);
|
|
5182
|
+
background: var(--panel-2);
|
|
5183
|
+
color: var(--studio-info-text, var(--muted));
|
|
5184
|
+
white-space: normal;
|
|
5185
|
+
line-height: 1.35;
|
|
5186
|
+
}
|
|
5187
|
+
|
|
5106
5188
|
body.studio-ui-refresh .studio-refresh-menu #critiqueBtn {
|
|
5107
5189
|
justify-content: flex-start;
|
|
5108
5190
|
text-align: left;
|
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,
|
|
@@ -229,9 +230,15 @@ interface PersistedStudioReviewNote {
|
|
|
229
230
|
htmlPreviewTitle?: string;
|
|
230
231
|
}
|
|
231
232
|
|
|
233
|
+
interface PersistedStudioScratchpadMetadata {
|
|
234
|
+
label?: string;
|
|
235
|
+
updatedAt?: number;
|
|
236
|
+
}
|
|
237
|
+
|
|
232
238
|
interface StudioPersistentState {
|
|
233
239
|
version: 2;
|
|
234
240
|
scratchpadsByDocument: Record<string, string>;
|
|
241
|
+
scratchpadMetadataByDocument: Record<string, PersistedStudioScratchpadMetadata>;
|
|
235
242
|
reviewNotesByDocument: Record<string, PersistedStudioReviewNote[]>;
|
|
236
243
|
}
|
|
237
244
|
|
|
@@ -728,6 +735,7 @@ function createEmptyStudioPersistentState(): StudioPersistentState {
|
|
|
728
735
|
return {
|
|
729
736
|
version: 2,
|
|
730
737
|
scratchpadsByDocument: {},
|
|
738
|
+
scratchpadMetadataByDocument: {},
|
|
731
739
|
reviewNotesByDocument: {},
|
|
732
740
|
};
|
|
733
741
|
}
|
|
@@ -784,6 +792,7 @@ function normalizeStudioPersistentState(value: unknown): StudioPersistentState {
|
|
|
784
792
|
const candidate = value as Partial<StudioPersistentState> & {
|
|
785
793
|
reviewNotesByDocument?: unknown;
|
|
786
794
|
scratchpadsByDocument?: unknown;
|
|
795
|
+
scratchpadMetadataByDocument?: unknown;
|
|
787
796
|
scratchpadText?: unknown;
|
|
788
797
|
};
|
|
789
798
|
const reviewNotesByDocument: Record<string, PersistedStudioReviewNote[]> = {};
|
|
@@ -807,9 +816,21 @@ function normalizeStudioPersistentState(value: unknown): StudioPersistentState {
|
|
|
807
816
|
} else if (typeof candidate.scratchpadText === "string" && candidate.scratchpadText.length > 0) {
|
|
808
817
|
scratchpadsByDocument[STUDIO_DEFAULT_SCRATCHPAD_DOCUMENT_KEY] = candidate.scratchpadText;
|
|
809
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
|
+
}
|
|
810
830
|
return {
|
|
811
831
|
version: 2,
|
|
812
832
|
scratchpadsByDocument,
|
|
833
|
+
scratchpadMetadataByDocument,
|
|
813
834
|
reviewNotesByDocument,
|
|
814
835
|
};
|
|
815
836
|
}
|
|
@@ -852,16 +873,50 @@ async function readPersistedStudioScratchpadText(documentKey: string): Promise<s
|
|
|
852
873
|
return typeof value === "string" ? value : "";
|
|
853
874
|
}
|
|
854
875
|
|
|
855
|
-
|
|
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> {
|
|
856
905
|
const key = String(documentKey ?? "").trim();
|
|
857
906
|
if (!key) return;
|
|
858
907
|
await mutateStudioPersistentState((state) => {
|
|
859
908
|
const normalized = String(text ?? "");
|
|
860
909
|
if (normalized.length === 0) {
|
|
861
910
|
delete state.scratchpadsByDocument[key];
|
|
911
|
+
delete state.scratchpadMetadataByDocument[key];
|
|
862
912
|
return;
|
|
863
913
|
}
|
|
864
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
|
+
};
|
|
865
920
|
});
|
|
866
921
|
}
|
|
867
922
|
|
|
@@ -4582,7 +4637,8 @@ function hasStudioYamlHeaderIncludes(markdown: string): boolean {
|
|
|
4582
4637
|
function prepareStudioMarkdownForPandoc(markdown: string, options?: { preserveLiteralLatexCommands?: boolean }): string {
|
|
4583
4638
|
const shouldPreserveLiteralLatexCommands = options?.preserveLiteralLatexCommands !== false;
|
|
4584
4639
|
return mapStudioMarkdownBodyPreservingYamlFrontMatter(markdown, (body) => {
|
|
4585
|
-
const
|
|
4640
|
+
const normalizedFences = normalizeStudioMarkdownSmartFences(body);
|
|
4641
|
+
const normalizedMath = normalizeMathDelimiters(normalizedFences);
|
|
4586
4642
|
const latexReady = shouldPreserveLiteralLatexCommands
|
|
4587
4643
|
? preserveLiteralLatexCommandsInMarkdown(normalizedMath)
|
|
4588
4644
|
: normalizedMath;
|
|
@@ -5432,9 +5488,10 @@ function prepareStudioPdfMarkdown(markdown: string, isLatex?: boolean, editorLan
|
|
|
5432
5488
|
&& !isStudioSingleFencedCodeBlock(input)
|
|
5433
5489
|
? wrapStudioCodeAsMarkdown(input, effectiveEditorLanguage)
|
|
5434
5490
|
: input;
|
|
5491
|
+
const fenceNormalizedSource = effectiveEditorLanguage === "latex" ? source : normalizeStudioMarkdownSmartFences(source);
|
|
5435
5492
|
const annotationReadySource = !effectiveEditorLanguage || effectiveEditorLanguage === "markdown" || effectiveEditorLanguage === "latex"
|
|
5436
|
-
? replaceStudioAnnotationMarkersForPdf(
|
|
5437
|
-
:
|
|
5493
|
+
? replaceStudioAnnotationMarkersForPdf(fenceNormalizedSource)
|
|
5494
|
+
: fenceNormalizedSource;
|
|
5438
5495
|
const commentStrippedSource = stripStudioMarkdownHtmlCommentsPreservingYamlFrontMatter(annotationReadySource);
|
|
5439
5496
|
return prepareStudioMarkdownForPandoc(commentStrippedSource, {
|
|
5440
5497
|
preserveLiteralLatexCommands: !hasStudioYamlHeaderIncludes(annotationReadySource),
|
|
@@ -5733,7 +5790,8 @@ function decorateStudioPandocSyntaxHtml(html: string): string {
|
|
|
5733
5790
|
|
|
5734
5791
|
async function renderStudioMarkdownWithPandoc(markdown: string, isLatex?: boolean, resourcePath?: string, sourcePath?: string): Promise<string> {
|
|
5735
5792
|
const pandocCommand = process.env.PANDOC_PATH?.trim() || "pandoc";
|
|
5736
|
-
const
|
|
5793
|
+
const markdownWithNormalizedFences = isLatex ? markdown : normalizeStudioMarkdownSmartFences(markdown);
|
|
5794
|
+
const markdownWithoutHtmlComments = isLatex ? markdownWithNormalizedFences : stripStudioMarkdownHtmlCommentsPreservingYamlFrontMatter(markdownWithNormalizedFences);
|
|
5737
5795
|
const markdownWithPreviewPageBreaks = isLatex ? markdownWithoutHtmlComments : replaceStudioPreviewPageBreakCommands(markdownWithoutHtmlComments);
|
|
5738
5796
|
const latexSubfigurePreviewTransform = isLatex
|
|
5739
5797
|
? preprocessStudioLatexSubfiguresForPreview(markdownWithPreviewPageBreaks)
|
|
@@ -6933,7 +6991,7 @@ async function respondLocalPreviewLinkJson(req: IncomingMessage, res: ServerResp
|
|
|
6933
6991
|
}
|
|
6934
6992
|
const document = buildStudioLocalResourcePreviewDocument(resource);
|
|
6935
6993
|
const docId = storeTransientStudioDocument(document);
|
|
6936
|
-
const url = buildStudioUrl(serverState.port, serverState.token, "editor-only", document, docId);
|
|
6994
|
+
const url = buildStudioUrl(serverState.port, serverState.token, "editor-only", document, docId, { skipWorkspaceRestore: true });
|
|
6937
6995
|
const parsedUrl = new URL(url);
|
|
6938
6996
|
respondJson(res, 200, {
|
|
6939
6997
|
...basePayload,
|
|
@@ -6998,7 +7056,7 @@ async function respondLocalPreviewLinkJson(req: IncomingMessage, res: ServerResp
|
|
|
6998
7056
|
}
|
|
6999
7057
|
|
|
7000
7058
|
const docId = storeTransientStudioDocument(document);
|
|
7001
|
-
const url = buildStudioUrl(serverState.port, serverState.token, "editor-only", document, docId);
|
|
7059
|
+
const url = buildStudioUrl(serverState.port, serverState.token, "editor-only", document, docId, { skipWorkspaceRestore: true });
|
|
7002
7060
|
const parsedUrl = new URL(url);
|
|
7003
7061
|
respondJson(res, 200, {
|
|
7004
7062
|
...basePayload,
|
|
@@ -9467,6 +9525,7 @@ function buildStudioUrl(
|
|
|
9467
9525
|
mode: StudioUiMode = "full",
|
|
9468
9526
|
doc?: InitialStudioDocument | null,
|
|
9469
9527
|
docId?: string,
|
|
9528
|
+
options?: { skipWorkspaceRestore?: boolean },
|
|
9470
9529
|
): string {
|
|
9471
9530
|
const params = new URLSearchParams({ token });
|
|
9472
9531
|
if (mode !== "full") params.set("mode", mode);
|
|
@@ -9476,6 +9535,7 @@ function buildStudioUrl(
|
|
|
9476
9535
|
if (doc?.path) params.set("docPath", doc.path);
|
|
9477
9536
|
if (doc?.draftId) params.set("draftId", doc.draftId);
|
|
9478
9537
|
if (doc?.resourceDir) params.set("resourceDir", doc.resourceDir);
|
|
9538
|
+
if (options?.skipWorkspaceRestore) params.set("skipWorkspaceRestore", "1");
|
|
9479
9539
|
return `http://127.0.0.1:${port}/?${params.toString()}`;
|
|
9480
9540
|
}
|
|
9481
9541
|
|
|
@@ -9936,8 +9996,8 @@ ${cssVarsBlock}
|
|
|
9936
9996
|
<option value="cursor" selected>Context: editor only</option>
|
|
9937
9997
|
<option value="session">Context: editor + latest response</option>
|
|
9938
9998
|
</select>
|
|
9939
|
-
<button id="openCompanionBtn" type="button" title="Open a
|
|
9940
|
-
<button id="sendEditorBtn" type="button">Send to
|
|
9999
|
+
<button id="openCompanionBtn" type="button" title="Open a blank editor-only Studio tab.">New editor tab</button>
|
|
10000
|
+
<button id="sendEditorBtn" type="button">Send current text to Pi editor</button>
|
|
9941
10001
|
</div>
|
|
9942
10002
|
<div class="source-actions-row">
|
|
9943
10003
|
<button id="insertHeaderBtn" type="button" title="Insert annotated-reply protocol header (source metadata, [an: ...] syntax hint, precedence note, and end marker).">Annotation header</button>
|
|
@@ -10058,7 +10118,7 @@ ${cssVarsBlock}
|
|
|
10058
10118
|
<div class="scratchpad-header">
|
|
10059
10119
|
<div>
|
|
10060
10120
|
<h2 id="reviewNotesTitle">Comments</h2>
|
|
10061
|
-
<p class="scratchpad-description">Local comments for editor text and editor previews. They stay out of the text;
|
|
10121
|
+
<p class="scratchpad-description">Local comments for editor text and editor previews. They stay out of the text; can be converted into inline <span class="review-notes-inline-token">[an: ...]</span> annotations.</p>
|
|
10062
10122
|
</div>
|
|
10063
10123
|
<button id="reviewNotesCloseBtn" type="button" class="scratchpad-close-btn" aria-label="Hide comments" title="Hide comments">✕</button>
|
|
10064
10124
|
</div>
|
|
@@ -10087,7 +10147,7 @@ ${cssVarsBlock}
|
|
|
10087
10147
|
<section id="rightPane">
|
|
10088
10148
|
<div id="rightSectionHeader" class="section-header">
|
|
10089
10149
|
<div class="section-header-main">
|
|
10090
|
-
<select id="rightViewSelect" aria-label="Response view mode" title="Right pane view mode.
|
|
10150
|
+
<select id="rightViewSelect" aria-label="Response view mode" title="Right pane view mode. F7 cycles when the right pane is active; Cmd/Ctrl+Alt+P switches directly to Preview.">
|
|
10091
10151
|
<option value="markdown">Response (Raw)</option>
|
|
10092
10152
|
<option value="preview" selected>Response (Preview)</option>
|
|
10093
10153
|
<option value="editor-preview">Editor (Preview)</option>
|
|
@@ -10101,8 +10161,10 @@ ${cssVarsBlock}
|
|
|
10101
10161
|
<span id="exportPreviewControls" class="export-preview-controls">
|
|
10102
10162
|
<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>
|
|
10103
10163
|
<div id="exportPreviewMenu" class="export-preview-menu" role="menu" hidden>
|
|
10104
|
-
<button id="
|
|
10105
|
-
<button id="
|
|
10164
|
+
<button id="exportPreviewPdfStudioBtn" type="button" role="menuitem" data-export-preview-format="pdf-studio">Export PDF and Open in Studio preview tab</button>
|
|
10165
|
+
<button id="exportPreviewPdfBtn" type="button" role="menuitem" data-export-preview-format="pdf-default">Export PDF and Open in default PDF viewer</button>
|
|
10166
|
+
<button id="exportPreviewHtmlStudioBtn" type="button" role="menuitem" data-export-preview-format="html-studio">Export HTML and Open in Studio editor</button>
|
|
10167
|
+
<button id="exportPreviewHtmlBtn" type="button" role="menuitem" data-export-preview-format="html-browser">Export HTML and Open in browser</button>
|
|
10106
10168
|
</div>
|
|
10107
10169
|
</span>
|
|
10108
10170
|
</div>
|
|
@@ -10177,6 +10239,7 @@ ${cssVarsBlock}
|
|
|
10177
10239
|
<dl>
|
|
10178
10240
|
<div><dt>F6</dt><dd>Switch between editor and right pane</dd></div>
|
|
10179
10241
|
<div><dt>F7 / Shift+F7</dt><dd>Cycle the active pane's view</dd></div>
|
|
10242
|
+
<div><dt>Cmd/Ctrl+Alt+P</dt><dd>Switch the right pane directly to Preview</dd></div>
|
|
10180
10243
|
<div><dt>F8</dt><dd>Focus editor text</dd></div>
|
|
10181
10244
|
<div><dt>Shift+F8</dt><dd>Focus right-pane content</dd></div>
|
|
10182
10245
|
<div><dt>F9</dt><dd>Toggle Zen mode</dd></div>
|
|
@@ -10227,14 +10290,16 @@ ${cssVarsBlock}
|
|
|
10227
10290
|
<div class="scratchpad-header">
|
|
10228
10291
|
<div>
|
|
10229
10292
|
<h2 id="scratchpadTitle">Scratchpad</h2>
|
|
10230
|
-
<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>
|
|
10293
|
+
<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>
|
|
10231
10294
|
</div>
|
|
10232
10295
|
<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>
|
|
10233
10296
|
</div>
|
|
10297
|
+
<div id="scratchpadRecentPanel" class="scratchpad-recent-panel" hidden></div>
|
|
10234
10298
|
<textarea id="scratchpadText" class="scratchpad-textarea" placeholder="Jot quick thoughts, TODOs, or prompt ideas here..."></textarea>
|
|
10235
10299
|
<div class="scratchpad-footer">
|
|
10236
10300
|
<span id="scratchpadMeta" class="scratchpad-meta">Empty · local only</span>
|
|
10237
10301
|
<div class="scratchpad-actions">
|
|
10302
|
+
<button id="scratchpadRecentBtn" type="button" title="Show recent non-empty scratchpads saved for other files and drafts.">Recent…</button>
|
|
10238
10303
|
<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>
|
|
10239
10304
|
<button id="scratchpadCopyBtn" type="button" title="Copy scratchpad text to the clipboard.">Copy</button>
|
|
10240
10305
|
<button id="scratchpadClearBtn" type="button" title="Clear scratchpad text.">Clear</button>
|
|
@@ -11634,7 +11699,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
11634
11699
|
resourceDir,
|
|
11635
11700
|
};
|
|
11636
11701
|
const docId = storeTransientStudioDocument(document);
|
|
11637
|
-
const url = buildStudioUrl(serverState.port, serverState.token, "editor-only", document, docId);
|
|
11702
|
+
const url = buildStudioUrl(serverState.port, serverState.token, "editor-only", document, docId, { skipWorkspaceRestore: true });
|
|
11638
11703
|
const parsedUrl = new URL(url);
|
|
11639
11704
|
sendToClient(client, {
|
|
11640
11705
|
type: "editor_only_ready",
|
|
@@ -11642,8 +11707,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
11642
11707
|
url,
|
|
11643
11708
|
relativeUrl: `${parsedUrl.pathname}${parsedUrl.search}`,
|
|
11644
11709
|
message: hasContent
|
|
11645
|
-
? "
|
|
11646
|
-
: "Blank
|
|
11710
|
+
? "Editor tab is ready with a detached copy of the current editor text."
|
|
11711
|
+
: "Blank editor tab is ready.",
|
|
11647
11712
|
});
|
|
11648
11713
|
return;
|
|
11649
11714
|
}
|
|
@@ -12607,6 +12672,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
12607
12672
|
const handleScratchpadStateRequest = async (req: IncomingMessage, res: ServerResponse, requestUrl: URL) => {
|
|
12608
12673
|
const method = (req.method ?? "GET").toUpperCase();
|
|
12609
12674
|
if (method === "GET") {
|
|
12675
|
+
const action = (requestUrl.searchParams.get("action") ?? "").trim().toLowerCase();
|
|
12676
|
+
if (action === "recent") {
|
|
12677
|
+
const limit = Number.parseInt(requestUrl.searchParams.get("limit") ?? "20", 10);
|
|
12678
|
+
respondJson(res, 200, { ok: true, scratchpads: await listRecentPersistedStudioScratchpads(limit) });
|
|
12679
|
+
return;
|
|
12680
|
+
}
|
|
12610
12681
|
const documentKey = (requestUrl.searchParams.get("documentKey") ?? "").trim();
|
|
12611
12682
|
if (!documentKey) {
|
|
12612
12683
|
respondJson(res, 400, { ok: false, error: "Missing documentKey query parameter." });
|
|
@@ -12656,8 +12727,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
12656
12727
|
respondJson(res, 400, { ok: false, error: "Missing scratchpad text in request body." });
|
|
12657
12728
|
return;
|
|
12658
12729
|
}
|
|
12730
|
+
const label =
|
|
12731
|
+
parsedBody && typeof parsedBody === "object" && typeof (parsedBody as { label?: unknown }).label === "string"
|
|
12732
|
+
? (parsedBody as { label: string }).label
|
|
12733
|
+
: undefined;
|
|
12659
12734
|
|
|
12660
|
-
await writePersistedStudioScratchpadText(documentKey, text);
|
|
12735
|
+
await writePersistedStudioScratchpadText(documentKey, text, label);
|
|
12661
12736
|
respondJson(res, 200, { ok: true });
|
|
12662
12737
|
};
|
|
12663
12738
|
|
|
@@ -12966,6 +13041,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
12966
13041
|
parsedBody && typeof parsedBody === "object" && typeof (parsedBody as { editorPdfLanguage?: unknown }).editorPdfLanguage === "string"
|
|
12967
13042
|
? (parsedBody as { editorPdfLanguage: string }).editorPdfLanguage
|
|
12968
13043
|
: "";
|
|
13044
|
+
const requestedOpenTarget =
|
|
13045
|
+
parsedBody && typeof parsedBody === "object" && typeof (parsedBody as { openTarget?: unknown }).openTarget === "string"
|
|
13046
|
+
? (parsedBody as { openTarget: string }).openTarget.trim().toLowerCase()
|
|
13047
|
+
: "default";
|
|
13048
|
+
const openTarget = requestedOpenTarget === "studio" ? "studio" : "default";
|
|
12969
13049
|
const editorPdfLanguage = inferStudioPdfLanguage(markdown, requestedEditorPdfLanguage);
|
|
12970
13050
|
const isLatex = editorPdfLanguage === "latex"
|
|
12971
13051
|
|| (
|
|
@@ -12979,17 +13059,48 @@ export default function (pi: ExtensionAPI) {
|
|
|
12979
13059
|
const writeResult = writeStudioPreviewExportFile(buildStudioPreviewExportPath(sourcePath || undefined, userResourceDir || undefined, studioCwd, filename), pdf);
|
|
12980
13060
|
const exportId = storePreparedPdfExport(pdf, filename, warning, writeResult.filePath ?? undefined);
|
|
12981
13061
|
const token = serverState?.token ?? "";
|
|
13062
|
+
if (openTarget === "studio" && serverState && writeResult.filePath) {
|
|
13063
|
+
const exportedPath = writeResult.filePath;
|
|
13064
|
+
const title = sanitizeStudioPreviewBlockLine(filename || basename(exportedPath) || "PDF preview");
|
|
13065
|
+
const document: InitialStudioDocument = {
|
|
13066
|
+
text: "```studio-pdf\n"
|
|
13067
|
+
+ `path: ${sanitizeStudioPreviewBlockLine(basename(exportedPath))}\n`
|
|
13068
|
+
+ `title: ${title || "PDF preview"}\n`
|
|
13069
|
+
+ "height: 820\n"
|
|
13070
|
+
+ "```\n",
|
|
13071
|
+
label: `${filename || basename(exportedPath) || "PDF"} preview`,
|
|
13072
|
+
source: "blank",
|
|
13073
|
+
resourceDir: dirname(exportedPath),
|
|
13074
|
+
};
|
|
13075
|
+
const docId = storeTransientStudioDocument(document);
|
|
13076
|
+
const url = buildStudioUrl(serverState.port, serverState.token, "editor-only", document, docId, { skipWorkspaceRestore: true });
|
|
13077
|
+
const parsedUrl = new URL(url);
|
|
13078
|
+
respondJson(res, 200, {
|
|
13079
|
+
ok: true,
|
|
13080
|
+
filename,
|
|
13081
|
+
path: writeResult.filePath,
|
|
13082
|
+
writeError: writeResult.error,
|
|
13083
|
+
warning: warning ?? null,
|
|
13084
|
+
openedStudio: true,
|
|
13085
|
+
url,
|
|
13086
|
+
relativeUrl: `${parsedUrl.pathname}${parsedUrl.search}`,
|
|
13087
|
+
downloadUrl: `/export-pdf?token=${encodeURIComponent(token)}&id=${encodeURIComponent(exportId)}`,
|
|
13088
|
+
});
|
|
13089
|
+
return;
|
|
13090
|
+
}
|
|
12982
13091
|
let openedExternal = false;
|
|
12983
13092
|
let openError: string | null = null;
|
|
12984
|
-
|
|
12985
|
-
|
|
12986
|
-
|
|
12987
|
-
|
|
13093
|
+
if (openTarget !== "studio") {
|
|
13094
|
+
try {
|
|
13095
|
+
const prepared = await ensurePreparedPdfExportFile(exportId);
|
|
13096
|
+
if (!prepared?.filePath) {
|
|
13097
|
+
throw new Error("Prepared PDF file was not available for external open.");
|
|
13098
|
+
}
|
|
13099
|
+
await openPathInDefaultViewer(prepared.filePath);
|
|
13100
|
+
openedExternal = true;
|
|
13101
|
+
} catch (viewerError) {
|
|
13102
|
+
openError = viewerError instanceof Error ? viewerError.message : String(viewerError);
|
|
12988
13103
|
}
|
|
12989
|
-
await openPathInDefaultViewer(prepared.filePath);
|
|
12990
|
-
openedExternal = true;
|
|
12991
|
-
} catch (viewerError) {
|
|
12992
|
-
openError = viewerError instanceof Error ? viewerError.message : String(viewerError);
|
|
12993
13104
|
}
|
|
12994
13105
|
respondJson(res, 200, {
|
|
12995
13106
|
ok: true,
|
|
@@ -13068,6 +13179,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
13068
13179
|
parsedBody && typeof parsedBody === "object" && typeof (parsedBody as { editorHtmlLanguage?: unknown }).editorHtmlLanguage === "string"
|
|
13069
13180
|
? (parsedBody as { editorHtmlLanguage: string }).editorHtmlLanguage
|
|
13070
13181
|
: "";
|
|
13182
|
+
const requestedOpenTarget =
|
|
13183
|
+
parsedBody && typeof parsedBody === "object" && typeof (parsedBody as { openTarget?: unknown }).openTarget === "string"
|
|
13184
|
+
? (parsedBody as { openTarget: string }).openTarget.trim().toLowerCase()
|
|
13185
|
+
: "browser";
|
|
13186
|
+
const openTarget = requestedOpenTarget === "studio" ? "studio" : "browser";
|
|
13071
13187
|
const editorHtmlLanguage = inferStudioPdfLanguage(markdown, requestedEditorHtmlLanguage);
|
|
13072
13188
|
const isLatex = editorHtmlLanguage === "latex"
|
|
13073
13189
|
|| (
|
|
@@ -13093,6 +13209,32 @@ export default function (pi: ExtensionAPI) {
|
|
|
13093
13209
|
const writeResult = writeStudioPreviewExportFile(buildStudioPreviewExportPath(sourcePath || undefined, userResourceDir || undefined, studioCwd, filename), html);
|
|
13094
13210
|
const exportId = storePreparedHtmlExport(html, filename, warning, writeResult.filePath ?? undefined);
|
|
13095
13211
|
const token = serverState?.token ?? "";
|
|
13212
|
+
if (openTarget === "studio" && serverState) {
|
|
13213
|
+
const exportedPath = writeResult.filePath ?? "";
|
|
13214
|
+
const document: InitialStudioDocument = {
|
|
13215
|
+
text: html.toString("utf-8"),
|
|
13216
|
+
label: filename,
|
|
13217
|
+
source: exportedPath ? "file" : "blank",
|
|
13218
|
+
path: exportedPath || undefined,
|
|
13219
|
+
resourceDir: exportedPath ? dirname(exportedPath) : (userResourceDir || resourcePath || studioCwd),
|
|
13220
|
+
draftId: exportedPath ? undefined : createStudioDraftId(),
|
|
13221
|
+
};
|
|
13222
|
+
const docId = storeTransientStudioDocument(document);
|
|
13223
|
+
const url = buildStudioUrl(serverState.port, serverState.token, "editor-only", document, docId, { skipWorkspaceRestore: true });
|
|
13224
|
+
const parsedUrl = new URL(url);
|
|
13225
|
+
respondJson(res, 200, {
|
|
13226
|
+
ok: true,
|
|
13227
|
+
filename,
|
|
13228
|
+
path: writeResult.filePath,
|
|
13229
|
+
writeError: writeResult.error,
|
|
13230
|
+
warning: warning ?? null,
|
|
13231
|
+
openedStudio: true,
|
|
13232
|
+
url,
|
|
13233
|
+
relativeUrl: `${parsedUrl.pathname}${parsedUrl.search}`,
|
|
13234
|
+
downloadUrl: `/export-html?token=${encodeURIComponent(token)}&id=${encodeURIComponent(exportId)}`,
|
|
13235
|
+
});
|
|
13236
|
+
return;
|
|
13237
|
+
}
|
|
13096
13238
|
let openedExternal = false;
|
|
13097
13239
|
let openError: string | null = null;
|
|
13098
13240
|
try {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-studio",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.22",
|
|
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
|
+
}
|