pi-studio 0.9.52 → 0.9.53
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 -0
- package/README.md +9 -7
- package/ROADMAP.md +14 -1
- package/client/studio-client.js +598 -106
- package/client/studio-preview-resource-helpers.js +61 -12
- package/client/studio.css +130 -3
- package/index.ts +547 -70
- package/package.json +1 -1
- package/shared/studio-resource-grants.js +157 -0
- package/shared/studio-side-question-context.js +17 -4
package/client/studio-client.js
CHANGED
|
@@ -242,6 +242,7 @@
|
|
|
242
242
|
|| typeof previewResourceHelpers.buildStudioPdfVersionSignature !== "function"
|
|
243
243
|
|| typeof previewResourceHelpers.createStudioPdfVersionObservationState !== "function"
|
|
244
244
|
|| typeof previewResourceHelpers.hydrateStudioPreviewLocalImages !== "function"
|
|
245
|
+
|| typeof previewResourceHelpers.hydrateStudioPreviewLocalPdfEmbeds !== "function"
|
|
245
246
|
|| typeof previewResourceHelpers.observeStudioPdfVersion !== "function"
|
|
246
247
|
) {
|
|
247
248
|
throw new Error("Studio preview resource helpers failed to load.");
|
|
@@ -402,8 +403,8 @@
|
|
|
402
403
|
option.disabled = (isEditorOnlyMode && !EDITOR_ONLY_RIGHT_VIEW_ALLOWED.has(option.value)) || (isQuartoOption && !quartoRelevant);
|
|
403
404
|
});
|
|
404
405
|
rightViewSelect.title = isEditorOnlyMode
|
|
405
|
-
? "Editor-only views: Editor Preview, contextual Quarto Preview for .qmd/.md/.markdown files, Changes, Files, REPL, or Side questions. F7 cycles; Cmd/Ctrl+Alt+3/5/6/7/8 switch directly to numbered right-pane views, and Cmd/Ctrl+Alt+Q
|
|
406
|
-
: "Right pane view mode. F7 cycles, including contextual Quarto Preview for file-backed .qmd, .md, and .markdown documents; Cmd/Ctrl+Alt+1–8 switches directly between the numbered views. Cmd/Ctrl+Alt+P/E/W keep
|
|
406
|
+
? "Editor-only views: Editor Preview, contextual Quarto Preview for .qmd/.md/.markdown files, Changes, Files, REPL, or Side questions. F7 cycles; Cmd/Ctrl+Alt+3/5/6/7/8 switch directly to numbered right-pane views, and Cmd/Ctrl+Alt+F/Q open Files/Side questions."
|
|
407
|
+
: "Right pane view mode. F7 cycles, including contextual Quarto Preview for file-backed .qmd, .md, and .markdown documents; Cmd/Ctrl+Alt+1–8 switches directly between the numbered views. Cmd/Ctrl+Alt+P/E/W/F/Q keep mnemonic shortcuts for Preview, Editor Preview, Working, Files, and Side questions.";
|
|
407
408
|
}
|
|
408
409
|
|
|
409
410
|
function getInitialRightView(source) {
|
|
@@ -478,6 +479,7 @@
|
|
|
478
479
|
let sideQuestionAvailablePiTools = [];
|
|
479
480
|
let sideQuestionPreviewRenderNonce = 0;
|
|
480
481
|
let sideQuestionContextRefreshHandle = null;
|
|
482
|
+
let sideQuestionContextGrantPending = false;
|
|
481
483
|
let sideQuestionMarkdownExportRequest = null;
|
|
482
484
|
const sideQuestionMarkdownRenderCache = new Map();
|
|
483
485
|
let sideQuestionUi = {
|
|
@@ -2293,6 +2295,9 @@
|
|
|
2293
2295
|
relativeDir: "",
|
|
2294
2296
|
parentDir: null,
|
|
2295
2297
|
entries: [],
|
|
2298
|
+
exactFiles: [],
|
|
2299
|
+
locations: [],
|
|
2300
|
+
grantRequiredPath: "",
|
|
2296
2301
|
omitted: 0,
|
|
2297
2302
|
omittedIgnored: 0,
|
|
2298
2303
|
sort: fileBrowserSortMode,
|
|
@@ -3894,8 +3899,12 @@
|
|
|
3894
3899
|
secondaryBtn.className = "studio-decision-secondary";
|
|
3895
3900
|
secondaryBtn.hidden = true;
|
|
3896
3901
|
secondaryBtn.addEventListener("click", () => {
|
|
3897
|
-
const
|
|
3898
|
-
|
|
3902
|
+
const state = studioDecisionState;
|
|
3903
|
+
const handler = state && state.onSecondary;
|
|
3904
|
+
if (typeof handler !== "function") {
|
|
3905
|
+
if (state && state.hasSecondaryValue) finishStudioDecision(state.secondaryValue);
|
|
3906
|
+
return;
|
|
3907
|
+
}
|
|
3899
3908
|
try {
|
|
3900
3909
|
const result = handler();
|
|
3901
3910
|
if (result && typeof result.catch === "function") {
|
|
@@ -3990,6 +3999,8 @@
|
|
|
3990
3999
|
resolve,
|
|
3991
4000
|
returnFocusEl,
|
|
3992
4001
|
onSecondary: typeof settings.onSecondary === "function" ? settings.onSecondary : null,
|
|
4002
|
+
hasSecondaryValue: Object.prototype.hasOwnProperty.call(settings, "secondaryValue"),
|
|
4003
|
+
secondaryValue: settings.secondaryValue,
|
|
3993
4004
|
};
|
|
3994
4005
|
studioDecisionState = decisionState;
|
|
3995
4006
|
const schedule = typeof window.requestAnimationFrame === "function"
|
|
@@ -4821,6 +4832,16 @@
|
|
|
4821
4832
|
return;
|
|
4822
4833
|
}
|
|
4823
4834
|
|
|
4835
|
+
const isFilesShortcut = (key.toLowerCase() === "f" || code === "KeyF")
|
|
4836
|
+
&& (event.metaKey || event.ctrlKey)
|
|
4837
|
+
&& event.altKey
|
|
4838
|
+
&& !event.shiftKey;
|
|
4839
|
+
if (isFilesShortcut) {
|
|
4840
|
+
event.preventDefault();
|
|
4841
|
+
switchRightPaneToView("files");
|
|
4842
|
+
return;
|
|
4843
|
+
}
|
|
4844
|
+
|
|
4824
4845
|
const isSideQuestionsShortcut = (key.toLowerCase() === "q" || code === "KeyQ")
|
|
4825
4846
|
&& (event.metaKey || event.ctrlKey)
|
|
4826
4847
|
&& event.altKey
|
|
@@ -6041,7 +6062,8 @@
|
|
|
6041
6062
|
+ " const images = Array.prototype.slice.call(document.querySelectorAll('img[src]'));\n"
|
|
6042
6063
|
+ " images.forEach((image) => {\n"
|
|
6043
6064
|
+ " if (!image || !image.getAttribute) return;\n"
|
|
6044
|
-
+ "
|
|
6065
|
+
+ " const resourceState = image.getAttribute('data-pi-studio-html-resource-resolved') || '';\n"
|
|
6066
|
+
+ " if (resourceState === 'true' || resourceState === 'blocked' || resourceState === 'failed') return;\n"
|
|
6045
6067
|
+ " const raw = String(image.getAttribute('src') || '').trim();\n"
|
|
6046
6068
|
+ " if (!shouldResolveHtmlPreviewResourceUrl(raw)) return;\n"
|
|
6047
6069
|
+ " let resourceId = image.getAttribute('data-pi-studio-html-resource-id') || '';\n"
|
|
@@ -6073,6 +6095,7 @@
|
|
|
6073
6095
|
+ " image.setAttribute('data-pi-studio-html-resource-resolved', 'true');\n"
|
|
6074
6096
|
+ " } else if (typeof result.error === 'string' && result.error) {\n"
|
|
6075
6097
|
+ " image.setAttribute('title', result.error);\n"
|
|
6098
|
+
+ " image.setAttribute('data-pi-studio-html-resource-resolved', result.blocked === true ? 'blocked' : 'failed');\n"
|
|
6076
6099
|
+ " }\n"
|
|
6077
6100
|
+ " htmlResourcePlaceholders.delete(resourceId);\n"
|
|
6078
6101
|
+ " });\n"
|
|
@@ -6523,18 +6546,28 @@
|
|
|
6523
6546
|
const message = payload && typeof payload.error === "string"
|
|
6524
6547
|
? payload.error
|
|
6525
6548
|
: (timeoutLabel || "Local preview resource load") + " failed with HTTP " + response.status + ".";
|
|
6526
|
-
|
|
6549
|
+
const requestError = new Error(message);
|
|
6550
|
+
requestError.studioPayload = payload && typeof payload === "object" ? payload : null;
|
|
6551
|
+
requestError.studioStatus = response.status;
|
|
6552
|
+
throw requestError;
|
|
6527
6553
|
}
|
|
6528
6554
|
return payload.dataUrl;
|
|
6529
6555
|
}
|
|
6530
6556
|
|
|
6531
6557
|
function postHtmlArtifactResourceResults(record, results) {
|
|
6532
6558
|
if (!record || !record.iframe || !record.iframe.isConnected || !record.iframe.contentWindow) return;
|
|
6559
|
+
const publicResults = Array.isArray(results) ? results.map((result) => ({
|
|
6560
|
+
resourceId: result && result.resourceId ? result.resourceId : "",
|
|
6561
|
+
ok: Boolean(result && result.ok === true),
|
|
6562
|
+
dataUrl: result && typeof result.dataUrl === "string" ? result.dataUrl : undefined,
|
|
6563
|
+
error: result && typeof result.error === "string" ? result.error : undefined,
|
|
6564
|
+
blocked: Boolean(result && result.blocked === true),
|
|
6565
|
+
})) : [];
|
|
6533
6566
|
try {
|
|
6534
6567
|
record.iframe.contentWindow.postMessage({
|
|
6535
6568
|
type: "pi-studio-html-artifact-resources-resolved",
|
|
6536
6569
|
id: record.id || "",
|
|
6537
|
-
results:
|
|
6570
|
+
results: publicResults,
|
|
6538
6571
|
}, "*");
|
|
6539
6572
|
} catch {
|
|
6540
6573
|
// Ignore iframe postMessage failures.
|
|
@@ -6552,14 +6585,58 @@
|
|
|
6552
6585
|
);
|
|
6553
6586
|
return { resourceId, ok: true, dataUrl };
|
|
6554
6587
|
} catch (error) {
|
|
6555
|
-
|
|
6588
|
+
const grantRequest = getStudioResourceGrantRequest(error);
|
|
6589
|
+
return {
|
|
6590
|
+
resourceId,
|
|
6591
|
+
ok: false,
|
|
6592
|
+
error: error && error.message ? error.message : String(error || "HTML preview resource load failed."),
|
|
6593
|
+
blocked: Boolean(grantRequest),
|
|
6594
|
+
grantRequest,
|
|
6595
|
+
sourceUrl: item && item.url ? String(item.url) : "",
|
|
6596
|
+
};
|
|
6556
6597
|
}
|
|
6557
6598
|
}
|
|
6558
6599
|
|
|
6600
|
+
function renderHtmlArtifactBlockedResources(record) {
|
|
6601
|
+
if (!record || !record.shell) return;
|
|
6602
|
+
const existing = record.shell.querySelector(".studio-html-artifact-blocked-media");
|
|
6603
|
+
if (existing) existing.remove();
|
|
6604
|
+
const blocked = record.blockedResources instanceof Map ? Array.from(record.blockedResources.values()) : [];
|
|
6605
|
+
if (!blocked.length) return;
|
|
6606
|
+
const container = document.createElement("div");
|
|
6607
|
+
container.className = "studio-html-artifact-blocked-media";
|
|
6608
|
+
blocked.forEach((entry) => {
|
|
6609
|
+
const notice = createStudioBlockedMediaNoticeForRequest(entry.grantRequest, "image", entry.sourceUrl);
|
|
6610
|
+
if (notice) container.appendChild(notice);
|
|
6611
|
+
});
|
|
6612
|
+
if (!container.childNodes.length) return;
|
|
6613
|
+
const frame = record.iframe && record.iframe.parentNode === record.shell ? record.iframe : null;
|
|
6614
|
+
record.shell.insertBefore(container, frame);
|
|
6615
|
+
}
|
|
6616
|
+
|
|
6617
|
+
function syncHtmlArtifactBlockedResources(record, results) {
|
|
6618
|
+
if (!record) return;
|
|
6619
|
+
if (!(record.blockedResources instanceof Map)) record.blockedResources = new Map();
|
|
6620
|
+
(Array.isArray(results) ? results : []).forEach((result) => {
|
|
6621
|
+
const resourceId = result && result.resourceId ? String(result.resourceId) : "";
|
|
6622
|
+
if (!resourceId) return;
|
|
6623
|
+
if (result.blocked === true && result.grantRequest) {
|
|
6624
|
+
record.blockedResources.set(resourceId, {
|
|
6625
|
+
grantRequest: result.grantRequest,
|
|
6626
|
+
sourceUrl: result.sourceUrl || "local image",
|
|
6627
|
+
});
|
|
6628
|
+
} else {
|
|
6629
|
+
record.blockedResources.delete(resourceId);
|
|
6630
|
+
}
|
|
6631
|
+
});
|
|
6632
|
+
renderHtmlArtifactBlockedResources(record);
|
|
6633
|
+
}
|
|
6634
|
+
|
|
6559
6635
|
async function resolveHtmlArtifactResources(record, items) {
|
|
6560
6636
|
if (!record || !Array.isArray(items) || items.length === 0) return;
|
|
6561
6637
|
if (record.detail) record.detail.textContent = "HTML preview · loading local images";
|
|
6562
6638
|
const results = await Promise.all(items.map((item) => fetchHtmlArtifactResource(record, item)));
|
|
6639
|
+
syncHtmlArtifactBlockedResources(record, results);
|
|
6563
6640
|
postHtmlArtifactResourceResults(record, results);
|
|
6564
6641
|
setHtmlArtifactDetailText(record, "HTML preview");
|
|
6565
6642
|
}
|
|
@@ -6620,12 +6697,14 @@
|
|
|
6620
6697
|
const action = typeof data.action === "string" ? data.action : "open";
|
|
6621
6698
|
if (action === "contextmenu") {
|
|
6622
6699
|
const point = getHtmlArtifactLocalLinkClientPoint(record, data);
|
|
6623
|
-
showPreviewLinkMenu(null, point, context);
|
|
6700
|
+
void showPreviewLinkMenu(null, point, context);
|
|
6624
6701
|
return;
|
|
6625
6702
|
}
|
|
6626
6703
|
const kind = getPreviewLocalLinkKind(context.href);
|
|
6627
6704
|
if (kind === "pdf") {
|
|
6628
|
-
openPreviewPdfLink(context.href, context.title, context)
|
|
6705
|
+
void openPreviewPdfLink(context.href, context.title, context).catch((error) => {
|
|
6706
|
+
setStatus((error && error.message) ? error.message : String(error || "Could not open linked PDF."), "warning");
|
|
6707
|
+
});
|
|
6629
6708
|
return;
|
|
6630
6709
|
}
|
|
6631
6710
|
if (kind === "image") {
|
|
@@ -6635,9 +6714,8 @@
|
|
|
6635
6714
|
return;
|
|
6636
6715
|
}
|
|
6637
6716
|
if (kind === "text" || kind === "office") {
|
|
6638
|
-
|
|
6639
|
-
|
|
6640
|
-
});
|
|
6717
|
+
const point = getHtmlArtifactLocalLinkClientPoint(record, data);
|
|
6718
|
+
void showPreviewLinkMenu(null, point, context);
|
|
6641
6719
|
return;
|
|
6642
6720
|
}
|
|
6643
6721
|
setStatus("Right-click this local HTML preview link for file actions.", "warning");
|
|
@@ -7041,6 +7119,7 @@
|
|
|
7041
7119
|
mathRenderItemCount: 0,
|
|
7042
7120
|
resourceResolveBatchCount: 0,
|
|
7043
7121
|
resourceResolveItemCount: 0,
|
|
7122
|
+
blockedResources: new Map(),
|
|
7044
7123
|
});
|
|
7045
7124
|
|
|
7046
7125
|
targetEl.appendChild(shell);
|
|
@@ -7927,6 +8006,13 @@
|
|
|
7927
8006
|
const target = event && event.target;
|
|
7928
8007
|
if (!(target instanceof Element)) return;
|
|
7929
8008
|
|
|
8009
|
+
const blockedMediaButton = target.closest("button.studio-blocked-media-allow");
|
|
8010
|
+
if (blockedMediaButton) {
|
|
8011
|
+
consumeStudioPreviewMediaEvent(event);
|
|
8012
|
+
void handleStudioBlockedMediaAllowButton(blockedMediaButton);
|
|
8013
|
+
return;
|
|
8014
|
+
}
|
|
8015
|
+
|
|
7930
8016
|
const imageEl = target.closest("img.studio-image-focus-target");
|
|
7931
8017
|
if (imageEl) {
|
|
7932
8018
|
consumeStudioPreviewMediaEvent(event);
|
|
@@ -8400,7 +8486,53 @@
|
|
|
8400
8486
|
});
|
|
8401
8487
|
}
|
|
8402
8488
|
|
|
8403
|
-
function
|
|
8489
|
+
function createStudioPdfStatusCard(block, content) {
|
|
8490
|
+
const options = block && block.options ? block.options : {};
|
|
8491
|
+
const path = String(options.path || "").trim();
|
|
8492
|
+
const title = String(options.title || path || "Embedded PDF").trim();
|
|
8493
|
+
const caption = String(options.caption || "").trim();
|
|
8494
|
+
const card = document.createElement("figure");
|
|
8495
|
+
card.className = "studio-pdf-card studio-pdf-card-status";
|
|
8496
|
+
const header = document.createElement("figcaption");
|
|
8497
|
+
header.className = "studio-pdf-card-header";
|
|
8498
|
+
const titleEl = document.createElement("div");
|
|
8499
|
+
titleEl.className = "studio-pdf-card-title";
|
|
8500
|
+
titleEl.textContent = title;
|
|
8501
|
+
header.appendChild(titleEl);
|
|
8502
|
+
card.appendChild(header);
|
|
8503
|
+
if (caption) {
|
|
8504
|
+
const captionEl = document.createElement("div");
|
|
8505
|
+
captionEl.className = "studio-pdf-card-caption";
|
|
8506
|
+
captionEl.textContent = caption;
|
|
8507
|
+
card.appendChild(captionEl);
|
|
8508
|
+
}
|
|
8509
|
+
if (content instanceof Element) {
|
|
8510
|
+
card.appendChild(content);
|
|
8511
|
+
} else {
|
|
8512
|
+
const errorEl = document.createElement("div");
|
|
8513
|
+
errorEl.className = "studio-pdf-card-error";
|
|
8514
|
+
errorEl.textContent = String(content || "PDF resource unavailable.");
|
|
8515
|
+
card.appendChild(errorEl);
|
|
8516
|
+
}
|
|
8517
|
+
return card;
|
|
8518
|
+
}
|
|
8519
|
+
|
|
8520
|
+
async function createStudioPdfCard(block, useEditorResourceContext) {
|
|
8521
|
+
const options = block && block.options ? block.options : {};
|
|
8522
|
+
const path = String(options.path || "").trim();
|
|
8523
|
+
if (!path) return createAuthorizedStudioPdfCard(block, useEditorResourceContext);
|
|
8524
|
+
const resourceQuery = buildStudioPdfResourceQuery(options, useEditorResourceContext);
|
|
8525
|
+
try {
|
|
8526
|
+
await fetchPreviewLocalLink("resolve", path, resourceQuery, { skipGrantPrompt: true });
|
|
8527
|
+
return createAuthorizedStudioPdfCard(block, useEditorResourceContext);
|
|
8528
|
+
} catch (error) {
|
|
8529
|
+
const notice = createStudioBlockedMediaNotice(error, "pdf", path);
|
|
8530
|
+
if (notice) return createStudioPdfStatusCard(block, notice);
|
|
8531
|
+
return createStudioPdfStatusCard(block, error && error.message ? error.message : "PDF resource unavailable.");
|
|
8532
|
+
}
|
|
8533
|
+
}
|
|
8534
|
+
|
|
8535
|
+
function createAuthorizedStudioPdfCard(block, useEditorResourceContext) {
|
|
8404
8536
|
const options = block && block.options ? block.options : {};
|
|
8405
8537
|
const path = String(options.path || "").trim();
|
|
8406
8538
|
const title = String(options.title || path || "Embedded PDF").trim();
|
|
@@ -8563,17 +8695,17 @@
|
|
|
8563
8695
|
return card;
|
|
8564
8696
|
}
|
|
8565
8697
|
|
|
8566
|
-
function renderStudioPdfBlocksInElement(targetEl, blocks, useEditorResourceContext) {
|
|
8698
|
+
async function renderStudioPdfBlocksInElement(targetEl, blocks, useEditorResourceContext) {
|
|
8567
8699
|
if (!targetEl || !Array.isArray(blocks) || blocks.length === 0) return;
|
|
8568
8700
|
const candidates = Array.from(targetEl.querySelectorAll("p, pre, div"));
|
|
8569
|
-
|
|
8701
|
+
for (const block of blocks) {
|
|
8570
8702
|
const placeholder = block && block.placeholder ? block.placeholder : "";
|
|
8571
|
-
if (!placeholder)
|
|
8703
|
+
if (!placeholder) continue;
|
|
8572
8704
|
const match = candidates.find((el) => String(el.textContent || "").trim() === placeholder);
|
|
8573
|
-
if (match
|
|
8574
|
-
|
|
8575
|
-
|
|
8576
|
-
}
|
|
8705
|
+
if (!match || !match.parentNode) continue;
|
|
8706
|
+
const card = await createStudioPdfCard(block, useEditorResourceContext);
|
|
8707
|
+
if (match.isConnected && match.parentNode) match.replaceWith(card);
|
|
8708
|
+
}
|
|
8577
8709
|
}
|
|
8578
8710
|
|
|
8579
8711
|
function sanitizeRenderedHtml(html, markdown, options) {
|
|
@@ -9145,6 +9277,91 @@
|
|
|
9145
9277
|
targetEl.appendChild(el);
|
|
9146
9278
|
}
|
|
9147
9279
|
|
|
9280
|
+
function createStudioBlockedMediaNotice(error, mediaKind, sourceLabel) {
|
|
9281
|
+
return createStudioBlockedMediaNoticeForRequest(getStudioResourceGrantRequest(error), mediaKind, sourceLabel);
|
|
9282
|
+
}
|
|
9283
|
+
|
|
9284
|
+
function createStudioBlockedMediaNoticeForRequest(grantRequest, mediaKind, sourceLabel) {
|
|
9285
|
+
if (!grantRequest) return null;
|
|
9286
|
+
const kind = mediaKind === "pdf" ? "PDF" : "image";
|
|
9287
|
+
const notice = document.createElement("span");
|
|
9288
|
+
notice.className = "studio-blocked-media studio-blocked-media-" + (mediaKind === "pdf" ? "pdf" : "image");
|
|
9289
|
+
notice.setAttribute("role", "note");
|
|
9290
|
+
|
|
9291
|
+
const text = document.createElement("span");
|
|
9292
|
+
text.className = "studio-blocked-media-text";
|
|
9293
|
+
const title = document.createElement("strong");
|
|
9294
|
+
title.textContent = "Local " + kind + " blocked";
|
|
9295
|
+
const detail = document.createElement("span");
|
|
9296
|
+
detail.textContent = "This media is outside the locations allowed for the current Studio session.";
|
|
9297
|
+
const path = document.createElement("code");
|
|
9298
|
+
path.textContent = grantRequest.path || String(sourceLabel || "local media");
|
|
9299
|
+
path.title = "Path on the computer running Pi";
|
|
9300
|
+
text.appendChild(title);
|
|
9301
|
+
text.appendChild(detail);
|
|
9302
|
+
text.appendChild(path);
|
|
9303
|
+
notice.appendChild(text);
|
|
9304
|
+
|
|
9305
|
+
const allowButton = document.createElement("button");
|
|
9306
|
+
allowButton.type = "button";
|
|
9307
|
+
allowButton.className = "studio-blocked-media-allow";
|
|
9308
|
+
allowButton.textContent = "Allow local " + kind + "…";
|
|
9309
|
+
allowButton.title = "Choose whether to allow only this file or its containing folder for the current Studio session.";
|
|
9310
|
+
allowButton.dataset.studioBlockedMediaPath = grantRequest.path;
|
|
9311
|
+
allowButton.dataset.studioBlockedMediaDirectory = grantRequest.directoryPath;
|
|
9312
|
+
allowButton.dataset.studioBlockedMediaLabel = grantRequest.label;
|
|
9313
|
+
allowButton.dataset.studioBlockedMediaKind = mediaKind === "pdf" ? "pdf" : "image";
|
|
9314
|
+
notice.appendChild(allowButton);
|
|
9315
|
+
return notice;
|
|
9316
|
+
}
|
|
9317
|
+
|
|
9318
|
+
function replaceStudioBlockedMediaElement(element, source, error, mediaKind) {
|
|
9319
|
+
if (!element || !element.parentNode) return false;
|
|
9320
|
+
const notice = createStudioBlockedMediaNotice(error, mediaKind, source);
|
|
9321
|
+
if (!notice) return false;
|
|
9322
|
+
element.replaceWith(notice);
|
|
9323
|
+
return true;
|
|
9324
|
+
}
|
|
9325
|
+
|
|
9326
|
+
function refreshStudioPassiveMediaAfterGrant() {
|
|
9327
|
+
renderSourcePreview({ previewDelayMs: 0 });
|
|
9328
|
+
if (rightView === "preview" || rightView === "editor-preview" || rightView === "side-questions") {
|
|
9329
|
+
renderActiveResult();
|
|
9330
|
+
}
|
|
9331
|
+
if (isQuizOpen()) renderQuizOverlay({ preserveScroll: true });
|
|
9332
|
+
}
|
|
9333
|
+
|
|
9334
|
+
async function handleStudioBlockedMediaAllowButton(button) {
|
|
9335
|
+
if (!(button instanceof HTMLButtonElement) || button.dataset.studioBlockedMediaBusy === "1") return false;
|
|
9336
|
+
const request = {
|
|
9337
|
+
path: String(button.dataset.studioBlockedMediaPath || "").trim(),
|
|
9338
|
+
directoryPath: String(button.dataset.studioBlockedMediaDirectory || "").trim(),
|
|
9339
|
+
label: String(button.dataset.studioBlockedMediaLabel || "local media").trim() || "local media",
|
|
9340
|
+
};
|
|
9341
|
+
if (!request.path || !request.directoryPath) {
|
|
9342
|
+
setStatus("Could not resolve this blocked media path.", "warning");
|
|
9343
|
+
return false;
|
|
9344
|
+
}
|
|
9345
|
+
button.dataset.studioBlockedMediaBusy = "1";
|
|
9346
|
+
button.disabled = true;
|
|
9347
|
+
const baselineText = button.textContent;
|
|
9348
|
+
button.textContent = "Choosing…";
|
|
9349
|
+
try {
|
|
9350
|
+
if (!(await requestStudioResourceGrant(request))) return false;
|
|
9351
|
+
refreshStudioPassiveMediaAfterGrant();
|
|
9352
|
+
return true;
|
|
9353
|
+
} catch (error) {
|
|
9354
|
+
setStatus("Could not allow local media: " + (error && error.message ? error.message : String(error || "unknown error")), "warning");
|
|
9355
|
+
return false;
|
|
9356
|
+
} finally {
|
|
9357
|
+
if (button.isConnected) {
|
|
9358
|
+
delete button.dataset.studioBlockedMediaBusy;
|
|
9359
|
+
button.disabled = false;
|
|
9360
|
+
button.textContent = baselineText;
|
|
9361
|
+
}
|
|
9362
|
+
}
|
|
9363
|
+
}
|
|
9364
|
+
|
|
9148
9365
|
function hasMeaningfulPreviewContent(targetEl) {
|
|
9149
9366
|
if (!targetEl || typeof targetEl.querySelector !== "function") return false;
|
|
9150
9367
|
if (targetEl.querySelector(".preview-loading")) return false;
|
|
@@ -10451,6 +10668,33 @@
|
|
|
10451
10668
|
});
|
|
10452
10669
|
}
|
|
10453
10670
|
|
|
10671
|
+
async function hydrateStudioPreviewLocalMedia(targetEl, resourceContext) {
|
|
10672
|
+
const fetchResource = (resourceUrl, timeoutLabel) => fetchLocalPreviewResourceDataUrl(
|
|
10673
|
+
resourceContext,
|
|
10674
|
+
resourceUrl,
|
|
10675
|
+
RENDERED_PREVIEW_IMAGE_FETCH_TIMEOUT_MS,
|
|
10676
|
+
timeoutLabel,
|
|
10677
|
+
);
|
|
10678
|
+
await previewResourceHelpers.hydrateStudioPreviewLocalImages(
|
|
10679
|
+
targetEl,
|
|
10680
|
+
(resourceUrl) => fetchResource(resourceUrl, "Preview image load"),
|
|
10681
|
+
{
|
|
10682
|
+
onError: (imageEl, resourceUrl, error) => {
|
|
10683
|
+
replaceStudioBlockedMediaElement(imageEl, resourceUrl, error, "image");
|
|
10684
|
+
},
|
|
10685
|
+
},
|
|
10686
|
+
);
|
|
10687
|
+
await previewResourceHelpers.hydrateStudioPreviewLocalPdfEmbeds(
|
|
10688
|
+
targetEl,
|
|
10689
|
+
(resourceUrl) => fetchResource(resourceUrl, "Preview PDF load"),
|
|
10690
|
+
{
|
|
10691
|
+
onError: (embedEl, resourceUrl, error) => {
|
|
10692
|
+
replaceStudioBlockedMediaElement(embedEl, resourceUrl, error, "pdf");
|
|
10693
|
+
},
|
|
10694
|
+
},
|
|
10695
|
+
);
|
|
10696
|
+
}
|
|
10697
|
+
|
|
10454
10698
|
async function applyRenderedMarkdown(targetEl, markdown, pane, nonce) {
|
|
10455
10699
|
const previewPrepared = annotationsEnabled
|
|
10456
10700
|
? prepareMarkdownForPandocPreview(markdown)
|
|
@@ -10477,15 +10721,8 @@
|
|
|
10477
10721
|
clearPreviewJumpHighlight(targetEl);
|
|
10478
10722
|
finishPreviewRender(targetEl);
|
|
10479
10723
|
targetEl.innerHTML = sanitizeRenderedHtml(renderedHtml, markdown, previewFallbackOptions);
|
|
10480
|
-
await
|
|
10481
|
-
|
|
10482
|
-
previewResourceContext,
|
|
10483
|
-
resourceUrl,
|
|
10484
|
-
RENDERED_PREVIEW_IMAGE_FETCH_TIMEOUT_MS,
|
|
10485
|
-
"Preview image load",
|
|
10486
|
-
)
|
|
10487
|
-
));
|
|
10488
|
-
renderStudioPdfBlocksInElement(targetEl, pdfPrepared.blocks, previewingEditorText);
|
|
10724
|
+
await hydrateStudioPreviewLocalMedia(targetEl, previewResourceContext);
|
|
10725
|
+
await renderStudioPdfBlocksInElement(targetEl, pdfPrepared.blocks, previewingEditorText);
|
|
10489
10726
|
applyPreviewAnnotationPlaceholdersToElement(targetEl, previewPrepared.placeholders);
|
|
10490
10727
|
await renderAnnotationMathInElement(targetEl);
|
|
10491
10728
|
decoratePdfEmbeds(targetEl);
|
|
@@ -11355,71 +11592,92 @@
|
|
|
11355
11592
|
return "<select class='files-sort-select' data-files-sort aria-label='Files sort order' title='Sort file browser entries. Folders remain grouped first.'>" + options + "</select>";
|
|
11356
11593
|
}
|
|
11357
11594
|
|
|
11595
|
+
function buildFileBrowserEntryRowHtml(entry) {
|
|
11596
|
+
const type = entry && entry.type === "directory" ? "directory" : "file";
|
|
11597
|
+
const kind = (entry && entry.kind) || (type === "directory" ? "directory" : "other");
|
|
11598
|
+
const path = String((entry && entry.path) || "");
|
|
11599
|
+
const name = String((entry && entry.name) || basenameForStudioPath(path));
|
|
11600
|
+
const icon = type === "directory" ? "📁" : (kind === "pdf" ? "📄" : (kind === "image" ? "🖼️" : (kind === "text" || kind === "office" ? "📝" : "📦")));
|
|
11601
|
+
const metaParts = [getFileBrowserKindLabel(entry)];
|
|
11602
|
+
if (type === "file") metaParts.push(formatFileBrowserSize(entry && entry.size));
|
|
11603
|
+
const time = formatFileBrowserTime(entry && entry.mtimeMs);
|
|
11604
|
+
if (time) metaParts.push(time);
|
|
11605
|
+
const newTabAction = kind === "text" || kind === "office"
|
|
11606
|
+
? "open-new"
|
|
11607
|
+
: ((kind === "pdf" || kind === "image") ? "open-preview-new" : "");
|
|
11608
|
+
const newTabLabel = kind === "text"
|
|
11609
|
+
? "Open file-backed tab"
|
|
11610
|
+
: (kind === "office" ? "Convert tab" : ((kind === "pdf" || kind === "image") ? "Preview tab" : "New tab"));
|
|
11611
|
+
const newTabTitle = kind === "text"
|
|
11612
|
+
? "Open this file-backed document in a new refreshable editor tab. Save editor and Refresh from disk will use this file."
|
|
11613
|
+
: (kind === "office" ? "Convert this document to Markdown in a new editor tab." : ((kind === "pdf" || kind === "image") ? "Open this preview in a new Studio tab." : "Open in a new Studio tab."));
|
|
11614
|
+
const newTabButton = newTabAction
|
|
11615
|
+
? "<button type='button' data-files-action='" + escapeHtml(newTabAction) + "' data-files-path='" + escapeHtml(path) + "' data-files-kind='" + escapeHtml(kind) + "' title='" + escapeHtml(newTabTitle) + "'>" + escapeHtml(newTabLabel) + "</button>"
|
|
11616
|
+
: "";
|
|
11617
|
+
const openTitle = type === "directory"
|
|
11618
|
+
? "Open folder"
|
|
11619
|
+
: (kind === "text" ? "Open file-backed document in the current editor. Save editor and Refresh from disk will use this file." : (kind === "office" ? "Convert to Markdown in the current editor" : (kind === "pdf" ? "Open PDF preview" : (kind === "image" ? "Open image preview" : "Copy or reveal this file"))));
|
|
11620
|
+
return "<div class='files-row files-row-" + escapeHtml(type) + " files-kind-" + escapeHtml(kind) + "'>"
|
|
11621
|
+
+ "<button type='button' class='files-open-btn' data-files-action='" + (type === "directory" ? "open-dir" : "open") + "' data-files-path='" + escapeHtml(path) + "' data-files-kind='" + escapeHtml(kind) + "' title='" + escapeHtml(openTitle) + "'>"
|
|
11622
|
+
+ "<span class='files-icon' aria-hidden='true'>" + icon + "</span>"
|
|
11623
|
+
+ "<span class='files-name'>" + escapeHtml(name) + "</span>"
|
|
11624
|
+
+ "<span class='files-meta'>" + escapeHtml(metaParts.filter(Boolean).join(" · ")) + "</span>"
|
|
11625
|
+
+ "</button>"
|
|
11626
|
+
+ "<span class='files-actions'>"
|
|
11627
|
+
+ newTabButton
|
|
11628
|
+
+ "<button type='button' data-files-action='copy-path' data-files-path='" + escapeHtml(path) + "'>Copy path</button>"
|
|
11629
|
+
+ (type === "file" ? "<button type='button' data-files-action='reveal' data-files-path='" + escapeHtml(path) + "'>Reveal</button>" : "")
|
|
11630
|
+
+ "</span>"
|
|
11631
|
+
+ "</div>";
|
|
11632
|
+
}
|
|
11633
|
+
|
|
11634
|
+
function buildFileBrowserLocationSelectHtml(state) {
|
|
11635
|
+
const locations = Array.isArray(state && state.locations) ? state.locations : [];
|
|
11636
|
+
if (!locations.length) return "<span class='files-no-locations'>No allowed folders</span>";
|
|
11637
|
+
const rootDir = String((state && state.rootDir) || "");
|
|
11638
|
+
const options = locations.map((location) => {
|
|
11639
|
+
const path = String((location && location.path) || "");
|
|
11640
|
+
const label = String((location && location.label) || basenameForStudioPath(path) || path);
|
|
11641
|
+
return "<option value='" + escapeHtml(path) + "'" + (path === rootDir ? " selected" : "") + ">" + escapeHtml(label + " — " + path) + "</option>";
|
|
11642
|
+
}).join("");
|
|
11643
|
+
return "<select class='files-location-select' data-files-location aria-label='Allowed Files location' title='Choose a folder allowed for this Studio session on the computer running Pi.'>" + options + "</select>";
|
|
11644
|
+
}
|
|
11645
|
+
|
|
11358
11646
|
function buildFileBrowserPanelHtml() {
|
|
11359
11647
|
const state = fileBrowserState || {};
|
|
11360
11648
|
const entries = Array.isArray(state.entries) ? state.entries : [];
|
|
11649
|
+
const exactFiles = Array.isArray(state.exactFiles) ? state.exactFiles : [];
|
|
11361
11650
|
const currentDir = state.currentDir || "";
|
|
11362
11651
|
const rootDir = state.rootDir || "";
|
|
11363
11652
|
const relativeDir = state.relativeDir || ".";
|
|
11364
11653
|
const parentDisabled = state.parentDir ? "" : " disabled";
|
|
11365
11654
|
const rows = entries.length
|
|
11366
|
-
? entries.map((
|
|
11367
|
-
const type = entry.type === "directory" ? "directory" : "file";
|
|
11368
|
-
const kind = entry.kind || (type === "directory" ? "directory" : "other");
|
|
11369
|
-
const icon = type === "directory" ? "📁" : (kind === "pdf" ? "📄" : (kind === "image" ? "🖼️" : (kind === "text" || kind === "office" ? "📝" : "📦")));
|
|
11370
|
-
const metaParts = [];
|
|
11371
|
-
metaParts.push(getFileBrowserKindLabel(entry));
|
|
11372
|
-
if (type === "file") metaParts.push(formatFileBrowserSize(entry.size));
|
|
11373
|
-
const time = formatFileBrowserTime(entry.mtimeMs);
|
|
11374
|
-
if (time) metaParts.push(time);
|
|
11375
|
-
const newTabAction = kind === "text" || kind === "office"
|
|
11376
|
-
? "open-new"
|
|
11377
|
-
: ((kind === "pdf" || kind === "image") ? "open-preview-new" : "");
|
|
11378
|
-
const newTabLabel = kind === "text"
|
|
11379
|
-
? "Open file-backed tab"
|
|
11380
|
-
: (kind === "office" ? "Convert tab" : ((kind === "pdf" || kind === "image") ? "Preview tab" : "New tab"));
|
|
11381
|
-
const newTabTitle = kind === "text"
|
|
11382
|
-
? "Open this file-backed document in a new refreshable editor tab. Save editor and Refresh from disk will use this file."
|
|
11383
|
-
: (kind === "office" ? "Convert this document to Markdown in a new editor tab." : ((kind === "pdf" || kind === "image") ? "Open this preview in a new Studio tab." : "Open in a new Studio tab."));
|
|
11384
|
-
const textActions = newTabAction
|
|
11385
|
-
? "<button type='button' data-files-action='" + escapeHtml(newTabAction) + "' data-files-path='" + escapeHtml(entry.path) + "' title='" + escapeHtml(newTabTitle) + "'>" + escapeHtml(newTabLabel) + "</button>"
|
|
11386
|
-
: "";
|
|
11387
|
-
const openTitle = type === "directory"
|
|
11388
|
-
? "Open folder"
|
|
11389
|
-
: (kind === "text" ? "Open file-backed document in the current editor. Save editor and Refresh from disk will use this file." : (kind === "office" ? "Convert to Markdown in the current editor" : (kind === "pdf" ? "Open PDF preview" : (kind === "image" ? "Open image preview" : "Copy or reveal this file"))));
|
|
11390
|
-
return "<div class='files-row files-row-" + escapeHtml(type) + " files-kind-" + escapeHtml(kind) + "'>"
|
|
11391
|
-
+ "<button type='button' class='files-open-btn' data-files-action='" + (type === "directory" ? "open-dir" : "open") + "' data-files-path='" + escapeHtml(entry.path) + "' data-files-kind='" + escapeHtml(kind) + "' title='" + escapeHtml(openTitle) + "'>"
|
|
11392
|
-
+ "<span class='files-icon' aria-hidden='true'>" + icon + "</span>"
|
|
11393
|
-
+ "<span class='files-name'>" + escapeHtml(entry.name) + "</span>"
|
|
11394
|
-
+ "<span class='files-meta'>" + escapeHtml(metaParts.filter(Boolean).join(" · ")) + "</span>"
|
|
11395
|
-
+ "</button>"
|
|
11396
|
-
+ "<span class='files-actions'>"
|
|
11397
|
-
+ textActions
|
|
11398
|
-
+ "<button type='button' data-files-action='copy-path' data-files-path='" + escapeHtml(entry.path) + "'>Copy path</button>"
|
|
11399
|
-
+ (type === "file" ? "<button type='button' data-files-action='reveal' data-files-path='" + escapeHtml(entry.path) + "'>Reveal</button>" : "")
|
|
11400
|
-
+ "</span>"
|
|
11401
|
-
+ "</div>";
|
|
11402
|
-
}).join("")
|
|
11655
|
+
? entries.map(buildFileBrowserEntryRowHtml).join("")
|
|
11403
11656
|
: "<div class='files-empty'>" + (state.loading ? "Loading files…" : "This folder is empty.") + "</div>";
|
|
11657
|
+
const exactRows = exactFiles.map(buildFileBrowserEntryRowHtml).join("");
|
|
11404
11658
|
const notices = [];
|
|
11405
11659
|
if (state.error) notices.push("<div class='files-notice files-notice-error'>" + escapeHtml(state.error) + "</div>");
|
|
11660
|
+
if (state.grantRequiredPath) notices.push("<div class='files-notice'>Allow that folder to browse it in Files. Exact-file grants do not expose their parent folders.</div>");
|
|
11406
11661
|
if (state.omitted) notices.push("<div class='files-notice'>" + escapeHtml(String(state.omitted)) + " item" + (state.omitted === 1 ? "" : "s") + " omitted.</div>");
|
|
11407
11662
|
if (state.omittedIgnored) notices.push("<div class='files-notice'>" + escapeHtml(String(state.omittedIgnored)) + " heavy/cache folder" + (state.omittedIgnored === 1 ? "" : "s") + " hidden.</div>");
|
|
11408
11663
|
return "<div class='files-panel'>"
|
|
11409
11664
|
+ "<div class='files-toolbar'>"
|
|
11410
11665
|
+ "<div class='files-path-group'><span class='files-label'>Files</span><span class='files-path' title='" + escapeHtml(currentDir) + "'>" + escapeHtml(relativeDir || ".") + "</span></div>"
|
|
11411
11666
|
+ "<div class='files-toolbar-actions'>"
|
|
11667
|
+
+ buildFileBrowserLocationSelectHtml(state)
|
|
11412
11668
|
+ buildFileBrowserSortSelectHtml()
|
|
11669
|
+
+ "<button type='button' data-files-action='allow-folder'>Allow folder…</button>"
|
|
11413
11670
|
+ "<button type='button' data-files-action='parent'" + parentDisabled + ">Parent</button>"
|
|
11414
11671
|
+ "<button type='button' data-files-action='refresh'>Refresh</button>"
|
|
11415
11672
|
+ (currentDir ? "<button type='button' data-files-action='copy-current' data-files-path='" + escapeHtml(currentDir) + "'>Copy path</button>" : "")
|
|
11416
11673
|
+ (currentDir ? "<button type='button' data-files-action='use-working-dir' data-files-path='" + escapeHtml(currentDir) + "'>Use as working dir</button>" : "")
|
|
11417
|
-
+ (rootDir ? "<button type='button' data-files-action='open-root' data-files-path='" + escapeHtml(rootDir) + "' title='Open the Files root folder in Finder or the system file manager.'>Open root</button>" : "")
|
|
11674
|
+
+ (rootDir ? "<button type='button' data-files-action='open-root' data-files-path='" + escapeHtml(rootDir) + "' title='Open the Files root folder in Finder or the system file manager on the computer running Pi.'>Open root</button>" : "")
|
|
11418
11675
|
+ (rootDir ? "<button type='button' data-files-action='copy-root' data-files-path='" + escapeHtml(rootDir) + "'>Copy root</button>" : "")
|
|
11419
11676
|
+ "</div>"
|
|
11420
11677
|
+ "</div>"
|
|
11421
|
-
+ "<div class='files-subtitle'>
|
|
11678
|
+
+ "<div class='files-subtitle'>Allowed root on the computer running Pi: <span title='" + escapeHtml(rootDir) + "'>" + escapeHtml(rootDir || "none selected") + "</span></div>"
|
|
11422
11679
|
+ notices.join("")
|
|
11680
|
+
+ (exactRows ? "<section class='files-exact-section'><div class='files-section-title'>Allowed exact files</div><div class='files-list' role='list'>" + exactRows + "</div></section>" : "")
|
|
11423
11681
|
+ "<div class='files-list' role='list'>" + rows + "</div>"
|
|
11424
11682
|
+ "</div>";
|
|
11425
11683
|
}
|
|
@@ -11434,6 +11692,9 @@
|
|
|
11434
11692
|
relativeDir: "",
|
|
11435
11693
|
parentDir: null,
|
|
11436
11694
|
entries: [],
|
|
11695
|
+
exactFiles: [],
|
|
11696
|
+
locations: [],
|
|
11697
|
+
grantRequiredPath: "",
|
|
11437
11698
|
omitted: 0,
|
|
11438
11699
|
omittedIgnored: 0,
|
|
11439
11700
|
sort: fileBrowserSortMode,
|
|
@@ -11455,12 +11716,17 @@
|
|
|
11455
11716
|
async function loadFileBrowserDirectory(dir, options) {
|
|
11456
11717
|
const context = getHtmlPreviewResourceContextOptions();
|
|
11457
11718
|
const contextKey = getFileBrowserContextKey();
|
|
11719
|
+
const config = options && typeof options === "object" ? options : {};
|
|
11720
|
+
const requestedRoot = Object.prototype.hasOwnProperty.call(config, "root")
|
|
11721
|
+
? normalizeStudioResourceDirValue(config.root || "")
|
|
11722
|
+
: normalizeStudioResourceDirValue(fileBrowserState.rootDir || "");
|
|
11458
11723
|
const nonce = ++fileBrowserLoadNonce;
|
|
11459
11724
|
fileBrowserState = {
|
|
11460
11725
|
...fileBrowserState,
|
|
11461
11726
|
contextKey,
|
|
11462
11727
|
loading: true,
|
|
11463
11728
|
error: "",
|
|
11729
|
+
grantRequiredPath: "",
|
|
11464
11730
|
};
|
|
11465
11731
|
if (rightView === "files") {
|
|
11466
11732
|
finishPreviewRender(critiqueViewEl);
|
|
@@ -11469,6 +11735,7 @@
|
|
|
11469
11735
|
try {
|
|
11470
11736
|
const query = {};
|
|
11471
11737
|
if (dir) query.dir = String(dir);
|
|
11738
|
+
if (requestedRoot) query.root = requestedRoot;
|
|
11472
11739
|
if (context.sourcePath) query.sourcePath = context.sourcePath;
|
|
11473
11740
|
if (context.resourceDir) query.resourceDir = context.resourceDir;
|
|
11474
11741
|
query.sort = normalizeFileBrowserSortMode(fileBrowserSortMode);
|
|
@@ -11481,6 +11748,9 @@
|
|
|
11481
11748
|
relativeDir: typeof payload.relativeDir === "string" ? payload.relativeDir : ".",
|
|
11482
11749
|
parentDir: typeof payload.parentDir === "string" ? payload.parentDir : null,
|
|
11483
11750
|
entries: Array.isArray(payload.entries) ? payload.entries : [],
|
|
11751
|
+
exactFiles: Array.isArray(payload.exactFiles) ? payload.exactFiles : [],
|
|
11752
|
+
locations: Array.isArray(payload.locations) ? payload.locations : [],
|
|
11753
|
+
grantRequiredPath: "",
|
|
11484
11754
|
omitted: Number(payload.omitted) || 0,
|
|
11485
11755
|
omittedIgnored: Number(payload.omittedIgnored) || 0,
|
|
11486
11756
|
sort: normalizeFileBrowserSortMode(payload.sort || fileBrowserSortMode),
|
|
@@ -11494,11 +11764,18 @@
|
|
|
11494
11764
|
critiqueViewEl.innerHTML = buildFileBrowserPanelHtml();
|
|
11495
11765
|
scheduleResponsePaneRepaintNudge();
|
|
11496
11766
|
}
|
|
11497
|
-
if (
|
|
11767
|
+
if (config.user) setStatus("Loaded file list.", "success");
|
|
11498
11768
|
} catch (error) {
|
|
11499
11769
|
if (nonce !== fileBrowserLoadNonce) return;
|
|
11770
|
+
const errorPayload = error && error.studioPayload && typeof error.studioPayload === "object" ? error.studioPayload : null;
|
|
11771
|
+
const grantRequiredPath = errorPayload && errorPayload.code === "studio-resource-directory-grant-required" && typeof errorPayload.directoryPath === "string"
|
|
11772
|
+
? normalizeStudioResourceDirValue(errorPayload.directoryPath)
|
|
11773
|
+
: "";
|
|
11500
11774
|
fileBrowserState = {
|
|
11501
11775
|
...fileBrowserState,
|
|
11776
|
+
exactFiles: errorPayload && Array.isArray(errorPayload.exactFiles) ? errorPayload.exactFiles : fileBrowserState.exactFiles,
|
|
11777
|
+
locations: errorPayload && Array.isArray(errorPayload.locations) ? errorPayload.locations : fileBrowserState.locations,
|
|
11778
|
+
grantRequiredPath,
|
|
11502
11779
|
loading: false,
|
|
11503
11780
|
error: (error && error.message) ? error.message : String(error || "Could not load files."),
|
|
11504
11781
|
loaded: true,
|
|
@@ -11546,7 +11823,7 @@
|
|
|
11546
11823
|
return;
|
|
11547
11824
|
}
|
|
11548
11825
|
if (kind === "pdf") {
|
|
11549
|
-
openPreviewPdfLink(path, path, context);
|
|
11826
|
+
await openPreviewPdfLink(path, path, context);
|
|
11550
11827
|
return;
|
|
11551
11828
|
}
|
|
11552
11829
|
if (kind === "image") {
|
|
@@ -11556,6 +11833,34 @@
|
|
|
11556
11833
|
setStatus("No Studio preview for this file type. Use Copy path or Reveal.", "warning");
|
|
11557
11834
|
}
|
|
11558
11835
|
|
|
11836
|
+
async function allowFileBrowserFolder() {
|
|
11837
|
+
const context = getHtmlPreviewResourceContextOptions();
|
|
11838
|
+
const suggested = normalizeStudioResourceDirValue(
|
|
11839
|
+
fileBrowserState.grantRequiredPath
|
|
11840
|
+
|| fileBrowserState.currentDir
|
|
11841
|
+
|| fileBrowserState.rootDir
|
|
11842
|
+
|| context.resourceDir
|
|
11843
|
+
|| (context.sourcePath ? dirnameForDisplayPath(context.sourcePath) : "")
|
|
11844
|
+
|| "./",
|
|
11845
|
+
);
|
|
11846
|
+
const path = await requestStudioTextInput(
|
|
11847
|
+
"Allow a folder on the computer running Pi for this Studio session:",
|
|
11848
|
+
suggested,
|
|
11849
|
+
{ title: "Allow folder", confirmLabel: "Allow folder", inputLabel: "Folder path" },
|
|
11850
|
+
);
|
|
11851
|
+
if (!path) return;
|
|
11852
|
+
const payload = await fetchStudioJson("/resource-grants", {
|
|
11853
|
+
method: "POST",
|
|
11854
|
+
body: JSON.stringify({ grantKind: "directory", path }),
|
|
11855
|
+
});
|
|
11856
|
+
const grantedPath = payload && payload.grant && payload.grant.kind === "directory" && typeof payload.grant.path === "string"
|
|
11857
|
+
? payload.grant.path
|
|
11858
|
+
: "";
|
|
11859
|
+
if (!grantedPath) throw new Error("Studio did not return the allowed folder.");
|
|
11860
|
+
await loadFileBrowserDirectory("", { root: grantedPath });
|
|
11861
|
+
setStatus(typeof payload.message === "string" ? payload.message : "Allowed folder for this Studio session.", "success");
|
|
11862
|
+
}
|
|
11863
|
+
|
|
11559
11864
|
function setFileBrowserCurrentDirectoryAsWorkingDir(path) {
|
|
11560
11865
|
const nextDir = normalizeStudioResourceDirValue(path || fileBrowserState.currentDir || "");
|
|
11561
11866
|
if (!nextDir) {
|
|
@@ -11577,6 +11882,7 @@
|
|
|
11577
11882
|
}
|
|
11578
11883
|
const context = getHtmlPreviewResourceContextOptions();
|
|
11579
11884
|
const body = { dir: targetDir };
|
|
11885
|
+
if (fileBrowserState.rootDir) body.root = fileBrowserState.rootDir;
|
|
11580
11886
|
if (context.sourcePath) body.sourcePath = context.sourcePath;
|
|
11581
11887
|
if (context.resourceDir) body.resourceDir = context.resourceDir;
|
|
11582
11888
|
const payload = await fetchStudioJson("/file-browser-open", {
|
|
@@ -11589,6 +11895,14 @@
|
|
|
11589
11895
|
async function handleFilesPaneChange(event) {
|
|
11590
11896
|
if (rightView !== "files") return;
|
|
11591
11897
|
const target = event.target;
|
|
11898
|
+
const locationSelect = target instanceof Element ? target.closest("[data-files-location]") : null;
|
|
11899
|
+
if (locationSelect && "value" in locationSelect) {
|
|
11900
|
+
const nextRoot = normalizeStudioResourceDirValue(locationSelect.value || "");
|
|
11901
|
+
if (nextRoot && nextRoot !== fileBrowserState.rootDir) {
|
|
11902
|
+
await loadFileBrowserDirectory("", { root: nextRoot, user: true });
|
|
11903
|
+
}
|
|
11904
|
+
return;
|
|
11905
|
+
}
|
|
11592
11906
|
const sortSelect = target instanceof Element ? target.closest("[data-files-sort]") : null;
|
|
11593
11907
|
if (!sortSelect || !("value" in sortSelect)) return;
|
|
11594
11908
|
const nextSort = writeFileBrowserSortMode(sortSelect.value);
|
|
@@ -11607,6 +11921,10 @@
|
|
|
11607
11921
|
const path = actionEl.getAttribute("data-files-path") || "";
|
|
11608
11922
|
const kind = actionEl.getAttribute("data-files-kind") || getPreviewLocalLinkKind(path);
|
|
11609
11923
|
try {
|
|
11924
|
+
if (action === "allow-folder") {
|
|
11925
|
+
await allowFileBrowserFolder();
|
|
11926
|
+
return;
|
|
11927
|
+
}
|
|
11610
11928
|
if (action === "parent") {
|
|
11611
11929
|
if (fileBrowserState.parentDir) await loadFileBrowserDirectory(fileBrowserState.parentDir, { user: true });
|
|
11612
11930
|
return;
|
|
@@ -11624,14 +11942,6 @@
|
|
|
11624
11942
|
return;
|
|
11625
11943
|
}
|
|
11626
11944
|
if (action === "open-new") {
|
|
11627
|
-
if (kind === "text" && isLikelyAbsoluteStudioPath(path)) {
|
|
11628
|
-
openFileBackedStudioEditorTab(path, {
|
|
11629
|
-
label: basenameForStudioPath(path),
|
|
11630
|
-
resourceDir: fileBrowserState.rootDir || getCurrentResourceDirValue() || dirnameForDisplayPath(path),
|
|
11631
|
-
});
|
|
11632
|
-
setStatus("Opening file-backed document in a new editor.");
|
|
11633
|
-
return;
|
|
11634
|
-
}
|
|
11635
11945
|
await openPreviewDocumentInNewEditor(path, getFileBrowserLocalLinkContext());
|
|
11636
11946
|
return;
|
|
11637
11947
|
}
|
|
@@ -12345,6 +12655,33 @@
|
|
|
12345
12655
|
};
|
|
12346
12656
|
}
|
|
12347
12657
|
|
|
12658
|
+
async function ensureSideQuestionContextRootAuthorized(context) {
|
|
12659
|
+
if (!context || context.gatherScope === "none") return { contextRoot: "" };
|
|
12660
|
+
const body = JSON.stringify({
|
|
12661
|
+
gatherScope: context.gatherScope,
|
|
12662
|
+
sourcePath: context.sourcePath,
|
|
12663
|
+
resourceDir: context.resourceDir,
|
|
12664
|
+
contextPath: context.contextPath,
|
|
12665
|
+
});
|
|
12666
|
+
const check = () => fetchStudioJson("/side-question-context-root", { method: "POST", body });
|
|
12667
|
+
try {
|
|
12668
|
+
return await check();
|
|
12669
|
+
} catch (error) {
|
|
12670
|
+
const request = getStudioDirectoryGrantRequest(error);
|
|
12671
|
+
if (!request) throw error;
|
|
12672
|
+
const allowed = await requestStudioDirectoryGrant(request, {
|
|
12673
|
+
message: "Side questions can map, search, and read supported files beneath this folder through read-only context tools. Allow that access for this Studio session?",
|
|
12674
|
+
cancelStatus: "Side-question folder access cancelled.",
|
|
12675
|
+
});
|
|
12676
|
+
if (!allowed) {
|
|
12677
|
+
const cancelled = new Error("Side-question folder access cancelled.");
|
|
12678
|
+
cancelled.studioCancelled = true;
|
|
12679
|
+
throw cancelled;
|
|
12680
|
+
}
|
|
12681
|
+
return check();
|
|
12682
|
+
}
|
|
12683
|
+
}
|
|
12684
|
+
|
|
12348
12685
|
async function renderSideQuestionMarkdownToHtml(markdown) {
|
|
12349
12686
|
const source = String(markdown || "");
|
|
12350
12687
|
if (sideQuestionMarkdownRenderCache.has(source)) return sideQuestionMarkdownRenderCache.get(source);
|
|
@@ -12369,7 +12706,11 @@
|
|
|
12369
12706
|
const html = await renderSideQuestionMarkdownToHtml(markdown);
|
|
12370
12707
|
if (nonce !== sideQuestionPreviewRenderNonce || rightView !== "side-questions" || !critiqueViewEl.contains(target)) return;
|
|
12371
12708
|
target.innerHTML = html;
|
|
12709
|
+
await hydrateStudioPreviewLocalMedia(target, getHtmlPreviewResourceContextOptions());
|
|
12372
12710
|
await renderAnnotationMathInElement(target);
|
|
12711
|
+
decoratePdfEmbeds(target);
|
|
12712
|
+
await renderPdfPreviewsInElement(target);
|
|
12713
|
+
decoratePreviewPdfFigures(target);
|
|
12373
12714
|
await renderMermaidInElement(target);
|
|
12374
12715
|
await renderMathFallbackInElement(target);
|
|
12375
12716
|
decorateCopyablePreviewBlocks(target);
|
|
@@ -12432,6 +12773,7 @@
|
|
|
12432
12773
|
], sideQuestionUi.thinking) + "</select></label>"
|
|
12433
12774
|
+ "</div>"
|
|
12434
12775
|
+ "<details class='side-question-context-rule'><summary id='sideQuestionContextRule'>Automatic: selection → heading block at cursor → nearby text</summary><p>A heading block starts at the nearest Markdown/LaTeX heading above the cursor and ends before the next heading of the same or higher level. With no heading, Studio uses the surrounding text block or a nearby excerpt.</p></details>"
|
|
12776
|
+
+ (scope === "none" ? "" : "<p class='side-question-context-access-note'>Related-file access is limited to folders allowed for this Studio session. Studio asks before starting if this folder is not already allowed.</p>")
|
|
12435
12777
|
+ (scope === "custom" ? "<label class='side-question-path-label'>Folder path<input data-side-question-field='customPath' type='text' value='" + escapeHtml(sideQuestionUi.customPath) + "' placeholder='Folder on the computer running Pi'></label>" : "")
|
|
12436
12778
|
+ "<div class='side-question-checks'>"
|
|
12437
12779
|
+ "<label><input data-side-question-field='includeConversation' type='checkbox'" + (sideQuestionUi.includeConversation ? " checked" : "") + "> Include the current main conversation snapshot</label>"
|
|
@@ -12442,7 +12784,7 @@
|
|
|
12442
12784
|
+ "<dl class='side-question-context-summary'><div><dt>Starting text</dt><dd>" + escapeHtml(summary.attachmentText) + "</dd></div><div><dt>Related files</dt><dd>" + escapeHtml(summary.relatedFilesText) + "</dd></div>" + (summary.gitContextText ? "<div><dt>Git context</dt><dd>" + escapeHtml(summary.gitContextText) + "</dd></div>" : "") + "</dl>"
|
|
12443
12785
|
+ "<label class='side-question-composer-label'>Question<textarea data-side-question-field='draft' rows='4' title='Enter adds a new line. Cmd/Ctrl+Enter asks the side question.' placeholder='Ask about the starting text or anything you want checked…'>" + escapeHtml(sideQuestionUi.draft) + "</textarea></label>"
|
|
12444
12786
|
+ (sideQuestionState && sideQuestionState.error ? "<div class='side-question-error'>" + escapeHtml(sideQuestionState.error) + "</div>" : "")
|
|
12445
|
-
+ "<div class='side-question-actions'><button type='button' class='side-question-primary' data-side-question-action='ask' aria-keyshortcuts='Meta+Enter Control+Enter' title='Ask side question (Cmd/Ctrl+Enter)'" + (!isSideQuestionConnectionReady() || (sideQuestionState && sideQuestionState.status === "running") || !sideQuestionUi.draft.trim() || (scope === "custom" && !sideQuestionUi.customPath.trim()) ? " disabled" : "") + ">" + (sideQuestionState && sideQuestionState.status === "running" ? "Preparing side thread…" : "Ask side question") + "</button></div>"
|
|
12787
|
+
+ "<div class='side-question-actions'><button type='button' class='side-question-primary' data-side-question-action='ask' aria-keyshortcuts='Meta+Enter Control+Enter' title='Ask side question (Cmd/Ctrl+Enter)'" + (sideQuestionContextGrantPending || !isSideQuestionConnectionReady() || (sideQuestionState && sideQuestionState.status === "running") || !sideQuestionUi.draft.trim() || (scope === "custom" && !sideQuestionUi.customPath.trim()) ? " disabled" : "") + ">" + (sideQuestionContextGrantPending ? "Checking related-file access…" : (sideQuestionState && sideQuestionState.status === "running" ? "Preparing side thread…" : "Ask side question")) + "</button></div>"
|
|
12446
12788
|
+ "</div>";
|
|
12447
12789
|
}
|
|
12448
12790
|
|
|
@@ -12513,7 +12855,8 @@
|
|
|
12513
12855
|
}
|
|
12514
12856
|
}
|
|
12515
12857
|
|
|
12516
|
-
function submitSideQuestion() {
|
|
12858
|
+
async function submitSideQuestion() {
|
|
12859
|
+
if (sideQuestionContextGrantPending) return;
|
|
12517
12860
|
const question = String(sideQuestionUi.draft || "").trim();
|
|
12518
12861
|
if (!question) {
|
|
12519
12862
|
setStatus("Enter a side question first.", "warning");
|
|
@@ -12532,11 +12875,31 @@
|
|
|
12532
12875
|
if (sideQuestionState && sideQuestionState.threadId) {
|
|
12533
12876
|
message.threadId = sideQuestionState.threadId;
|
|
12534
12877
|
} else {
|
|
12535
|
-
|
|
12536
|
-
if (
|
|
12878
|
+
const context = buildSideQuestionContextPayload();
|
|
12879
|
+
if (context.gatherScope === "custom" && !String(context.contextPath || "").trim()) {
|
|
12537
12880
|
setStatus("Choose a custom context path first.", "warning");
|
|
12538
12881
|
return;
|
|
12539
12882
|
}
|
|
12883
|
+
if (context.gatherScope !== "none") {
|
|
12884
|
+
sideQuestionContextGrantPending = true;
|
|
12885
|
+
if (rightView === "side-questions") renderSideQuestionView();
|
|
12886
|
+
setStatus("Checking side-question folder access…", "warning");
|
|
12887
|
+
try {
|
|
12888
|
+
await ensureSideQuestionContextRootAuthorized(context);
|
|
12889
|
+
} catch (error) {
|
|
12890
|
+
if (!(error && error.studioCancelled)) {
|
|
12891
|
+
const messageText = error && error.message ? error.message : String(error || "Could not authorize side-question context.");
|
|
12892
|
+
if (!sideQuestionState) sideQuestionState = normalizeSideQuestionState(null);
|
|
12893
|
+
sideQuestionState.error = messageText;
|
|
12894
|
+
setStatus("Could not use related files: " + messageText, "warning");
|
|
12895
|
+
}
|
|
12896
|
+
return;
|
|
12897
|
+
} finally {
|
|
12898
|
+
sideQuestionContextGrantPending = false;
|
|
12899
|
+
if (rightView === "side-questions" && (!sideQuestionState || !sideQuestionState.threadId)) renderSideQuestionView();
|
|
12900
|
+
}
|
|
12901
|
+
}
|
|
12902
|
+
message.context = context;
|
|
12540
12903
|
}
|
|
12541
12904
|
if (!sendMessage(message)) return;
|
|
12542
12905
|
sideQuestionUi.draft = "";
|
|
@@ -12652,7 +13015,7 @@
|
|
|
12652
13015
|
event.preventDefault();
|
|
12653
13016
|
const action = target.getAttribute("data-side-question-action");
|
|
12654
13017
|
if (action === "ask") {
|
|
12655
|
-
submitSideQuestion();
|
|
13018
|
+
await submitSideQuestion();
|
|
12656
13019
|
} else if (action === "stop") {
|
|
12657
13020
|
if (sideQuestionState && sideQuestionState.threadId && sideQuestionState.requestId) {
|
|
12658
13021
|
sendMessage({ type: "side_question_cancel_request", threadId: sideQuestionState.threadId, requestId: sideQuestionState.requestId });
|
|
@@ -12705,7 +13068,7 @@
|
|
|
12705
13068
|
if (field === "draft") sideQuestionUi.draft = target.value;
|
|
12706
13069
|
if (field === "customPath") sideQuestionUi.customPath = target.value;
|
|
12707
13070
|
const askButton = critiqueViewEl.querySelector("[data-side-question-action='ask']");
|
|
12708
|
-
if (askButton) askButton.disabled = !isSideQuestionConnectionReady() || !sideQuestionUi.draft.trim() || (getSideQuestionGatherScope() === "custom" && !sideQuestionUi.customPath.trim());
|
|
13071
|
+
if (askButton) askButton.disabled = sideQuestionContextGrantPending || !isSideQuestionConnectionReady() || !sideQuestionUi.draft.trim() || (getSideQuestionGatherScope() === "custom" && !sideQuestionUi.customPath.trim());
|
|
12709
13072
|
}
|
|
12710
13073
|
|
|
12711
13074
|
function handleSideQuestionKeydown(event) {
|
|
@@ -12719,7 +13082,7 @@
|
|
|
12719
13082
|
if (!submitShortcut) return;
|
|
12720
13083
|
event.preventDefault();
|
|
12721
13084
|
event.stopPropagation();
|
|
12722
|
-
if (!sideQuestionState || sideQuestionState.status !== "running") submitSideQuestion();
|
|
13085
|
+
if (!sideQuestionState || sideQuestionState.status !== "running") void submitSideQuestion();
|
|
12723
13086
|
}
|
|
12724
13087
|
|
|
12725
13088
|
async function handleSideQuestionChange(event) {
|
|
@@ -14538,6 +14901,11 @@
|
|
|
14538
14901
|
return launch.fail(message || "Studio could not prepare this tab. Return to the originating Studio page for details.");
|
|
14539
14902
|
}
|
|
14540
14903
|
|
|
14904
|
+
function cancelPendingStudioTab(launch, message) {
|
|
14905
|
+
if (!launch || typeof launch.cancel !== "function") return false;
|
|
14906
|
+
return launch.cancel(message || "Studio tab launch was cancelled.");
|
|
14907
|
+
}
|
|
14908
|
+
|
|
14541
14909
|
function abandonAllStudioTabLaunches(message) {
|
|
14542
14910
|
const launches = Array.from(activeStudioTabLaunches);
|
|
14543
14911
|
activeStudioTabLaunches.clear();
|
|
@@ -14616,7 +14984,10 @@
|
|
|
14616
14984
|
const message = payload && typeof payload.error === "string"
|
|
14617
14985
|
? payload.error
|
|
14618
14986
|
: (response.status + " " + response.statusText).trim();
|
|
14619
|
-
|
|
14987
|
+
const requestError = new Error(message || (method + " " + pathname + " failed."));
|
|
14988
|
+
requestError.studioPayload = payload && typeof payload === "object" ? payload : null;
|
|
14989
|
+
requestError.studioStatus = response.status;
|
|
14990
|
+
throw requestError;
|
|
14620
14991
|
}
|
|
14621
14992
|
return payload;
|
|
14622
14993
|
}
|
|
@@ -14649,6 +15020,7 @@
|
|
|
14649
15020
|
]);
|
|
14650
15021
|
let previewLinkMenuEl = null;
|
|
14651
15022
|
let activePreviewLinkContext = null;
|
|
15023
|
+
let previewLinkMenuRequestId = 0;
|
|
14652
15024
|
|
|
14653
15025
|
function stripPreviewLocalLinkUrlSuffix(href) {
|
|
14654
15026
|
const raw = String(href || "").trim();
|
|
@@ -14715,7 +15087,7 @@
|
|
|
14715
15087
|
if (!raw || raw.charAt(0) === "#") return false;
|
|
14716
15088
|
if (/^\/\//.test(raw)) return false;
|
|
14717
15089
|
if (/^(?:https?|mailto|tel|data|blob|javascript|about):/i.test(raw)) return false;
|
|
14718
|
-
if (/^\/(?:pdf-resource|html-preview-resource|export-pdf|export-html|render-preview|render-math|import-file-copy|local-preview-link|reveal-local-resource|open-local-resource)(?:[?#/]|$)/i.test(raw)) return false;
|
|
15090
|
+
if (/^\/(?:pdf-resource|html-preview-resource|export-pdf|export-html|render-preview|render-math|import-file-copy|resource-grants|local-preview-link|reveal-local-resource|open-local-resource)(?:[?#/]|$)/i.test(raw)) return false;
|
|
14719
15091
|
return true;
|
|
14720
15092
|
}
|
|
14721
15093
|
|
|
@@ -14785,19 +15157,35 @@
|
|
|
14785
15157
|
menu.style.top = y + "px";
|
|
14786
15158
|
}
|
|
14787
15159
|
|
|
14788
|
-
function showPreviewLinkMenu(anchor, event, contextOverride) {
|
|
15160
|
+
async function showPreviewLinkMenu(anchor, event, contextOverride) {
|
|
14789
15161
|
const href = String(anchor && anchor.getAttribute ? anchor.getAttribute("href") || "" : (contextOverride && contextOverride.href ? contextOverride.href : "")).trim();
|
|
14790
15162
|
if (!isStudioLocalPreviewHref(href)) return false;
|
|
14791
15163
|
const kind = getPreviewLocalLinkKind(href);
|
|
14792
|
-
const menu = ensurePreviewLinkMenu();
|
|
14793
|
-
menu.innerHTML = "";
|
|
14794
15164
|
const linkContext = getEffectivePreviewLinkContext(contextOverride);
|
|
14795
|
-
|
|
15165
|
+
const nextContext = {
|
|
14796
15166
|
href,
|
|
14797
15167
|
title: String((contextOverride && contextOverride.title) || (anchor && anchor.textContent) || href || "local link").trim() || href,
|
|
14798
15168
|
sourcePath: linkContext.sourcePath,
|
|
14799
15169
|
resourceDir: linkContext.resourceDir,
|
|
14800
15170
|
};
|
|
15171
|
+
const menuPoint = {
|
|
15172
|
+
clientX: event && event.clientX,
|
|
15173
|
+
clientY: event && event.clientY,
|
|
15174
|
+
};
|
|
15175
|
+
closePreviewLinkMenu();
|
|
15176
|
+
const menuRequestId = ++previewLinkMenuRequestId;
|
|
15177
|
+
try {
|
|
15178
|
+
await fetchPreviewLocalLink("resolve", href, nextContext);
|
|
15179
|
+
} catch (error) {
|
|
15180
|
+
if (!(error && error.studioCancelled)) {
|
|
15181
|
+
setStatus((error && error.message) ? error.message : String(error || "Could not inspect this local resource."), "warning");
|
|
15182
|
+
}
|
|
15183
|
+
return false;
|
|
15184
|
+
}
|
|
15185
|
+
if (menuRequestId !== previewLinkMenuRequestId) return false;
|
|
15186
|
+
const menu = ensurePreviewLinkMenu();
|
|
15187
|
+
menu.innerHTML = "";
|
|
15188
|
+
activePreviewLinkContext = nextContext;
|
|
14801
15189
|
if (kind === "pdf") {
|
|
14802
15190
|
appendPreviewLinkMenuButton(menu, "Open PDF preview", "open-pdf");
|
|
14803
15191
|
appendPreviewLinkMenuButton(menu, "Open in new Studio tab", "open-preview-new");
|
|
@@ -14814,7 +15202,7 @@
|
|
|
14814
15202
|
}
|
|
14815
15203
|
appendPreviewLinkMenuButton(menu, "Reveal in file manager", "reveal");
|
|
14816
15204
|
appendPreviewLinkMenuButton(menu, "Copy path", "copy-path");
|
|
14817
|
-
positionPreviewLinkMenu(menu,
|
|
15205
|
+
positionPreviewLinkMenu(menu, menuPoint.clientX, menuPoint.clientY);
|
|
14818
15206
|
const firstButton = menu.querySelector("button");
|
|
14819
15207
|
if (firstButton && typeof firstButton.focus === "function") {
|
|
14820
15208
|
window.setTimeout(() => firstButton.focus({ preventScroll: true }), 0);
|
|
@@ -14822,10 +15210,102 @@
|
|
|
14822
15210
|
return true;
|
|
14823
15211
|
}
|
|
14824
15212
|
|
|
14825
|
-
|
|
14826
|
-
|
|
15213
|
+
function getStudioResourceGrantRequest(error) {
|
|
15214
|
+
const payload = error && error.studioPayload && typeof error.studioPayload === "object"
|
|
15215
|
+
? error.studioPayload
|
|
15216
|
+
: null;
|
|
15217
|
+
if (!payload || payload.code !== "studio-resource-grant-required") return null;
|
|
15218
|
+
const path = typeof payload.path === "string" ? payload.path.trim() : "";
|
|
15219
|
+
const directoryPath = typeof payload.directoryPath === "string" ? payload.directoryPath.trim() : "";
|
|
15220
|
+
if (!path || !directoryPath) return null;
|
|
15221
|
+
return {
|
|
15222
|
+
path,
|
|
15223
|
+
directoryPath,
|
|
15224
|
+
label: typeof payload.label === "string" && payload.label.trim() ? payload.label.trim() : basenameForStudioPath(path),
|
|
15225
|
+
};
|
|
15226
|
+
}
|
|
15227
|
+
|
|
15228
|
+
function getStudioDirectoryGrantRequest(error) {
|
|
15229
|
+
const payload = error && error.studioPayload && typeof error.studioPayload === "object"
|
|
15230
|
+
? error.studioPayload
|
|
15231
|
+
: null;
|
|
15232
|
+
if (!payload || payload.code !== "studio-resource-directory-grant-required") return null;
|
|
15233
|
+
const directoryPath = typeof payload.directoryPath === "string" ? payload.directoryPath.trim() : "";
|
|
15234
|
+
if (!directoryPath) return null;
|
|
15235
|
+
return {
|
|
15236
|
+
directoryPath,
|
|
15237
|
+
label: typeof payload.label === "string" && payload.label.trim() ? payload.label.trim() : basenameForStudioPath(directoryPath),
|
|
15238
|
+
};
|
|
15239
|
+
}
|
|
15240
|
+
|
|
15241
|
+
async function requestStudioDirectoryGrant(request, options) {
|
|
15242
|
+
if (!request || !request.directoryPath) return false;
|
|
15243
|
+
const config = options && typeof options === "object" ? options : {};
|
|
15244
|
+
const choice = await openStudioDecision({
|
|
15245
|
+
mode: "confirm",
|
|
15246
|
+
title: "Allow " + (request.label || "this folder") + "?",
|
|
15247
|
+
message: (config.message || "Allow read-only access beneath this folder for the current Studio session?")
|
|
15248
|
+
+ "\n\nFolder on the computer running Pi:\n" + request.directoryPath,
|
|
15249
|
+
cancelLabel: "Cancel",
|
|
15250
|
+
confirmLabel: "Allow this folder for this Studio session",
|
|
15251
|
+
});
|
|
15252
|
+
if (choice !== true) {
|
|
15253
|
+
setStatus(config.cancelStatus || "Folder access cancelled.", "warning");
|
|
15254
|
+
return false;
|
|
15255
|
+
}
|
|
15256
|
+
const payload = await fetchStudioJson("/resource-grants", {
|
|
15257
|
+
method: "POST",
|
|
15258
|
+
body: JSON.stringify({ grantKind: "directory", path: request.directoryPath }),
|
|
15259
|
+
});
|
|
15260
|
+
fileBrowserState = { ...fileBrowserState, contextKey: "", loaded: false };
|
|
15261
|
+
setStatus(typeof payload.message === "string" ? payload.message : "Allowed this folder for the current Studio session.", "success");
|
|
15262
|
+
return true;
|
|
15263
|
+
}
|
|
15264
|
+
|
|
15265
|
+
async function requestStudioResourceGrant(request) {
|
|
15266
|
+
if (!request) return false;
|
|
15267
|
+
const choice = await openStudioDecision({
|
|
15268
|
+
mode: "confirm",
|
|
15269
|
+
title: "Allow " + request.label + "?",
|
|
15270
|
+
message: "This local resource is outside the locations currently available to Studio.\n\n"
|
|
15271
|
+
+ "File on the computer running Pi:\n" + request.path + "\n\n"
|
|
15272
|
+
+ "Allow only this file, or allow its containing folder for this Studio session?",
|
|
15273
|
+
cancelLabel: "Cancel",
|
|
15274
|
+
secondaryLabel: "Allow this folder for this Studio session",
|
|
15275
|
+
secondaryValue: "directory",
|
|
15276
|
+
confirmLabel: "Allow this file",
|
|
15277
|
+
});
|
|
15278
|
+
const grantKind = choice === true ? "file" : choice === "directory" ? "directory" : "";
|
|
15279
|
+
if (!grantKind) {
|
|
15280
|
+
setStatus("Local resource access cancelled.", "warning");
|
|
15281
|
+
return false;
|
|
15282
|
+
}
|
|
15283
|
+
const grantPath = grantKind === "directory" ? request.directoryPath : request.path;
|
|
15284
|
+
const payload = await fetchStudioJson("/resource-grants", {
|
|
15285
|
+
method: "POST",
|
|
15286
|
+
body: JSON.stringify({ grantKind, path: grantPath }),
|
|
15287
|
+
});
|
|
15288
|
+
fileBrowserState = { ...fileBrowserState, contextKey: "", loaded: false };
|
|
15289
|
+
setStatus(typeof payload.message === "string" ? payload.message : "Allowed local resource for this Studio session.", "success");
|
|
15290
|
+
return true;
|
|
15291
|
+
}
|
|
15292
|
+
|
|
15293
|
+
async function fetchPreviewLocalLink(action, href, contextOverride, options) {
|
|
15294
|
+
const request = () => fetchStudioJson("/local-preview-link", {
|
|
14827
15295
|
query: { ...getPreviewLinkResourceQuery(href, contextOverride), action },
|
|
14828
15296
|
});
|
|
15297
|
+
try {
|
|
15298
|
+
return await request();
|
|
15299
|
+
} catch (error) {
|
|
15300
|
+
const grantRequest = getStudioResourceGrantRequest(error);
|
|
15301
|
+
if (!grantRequest || (options && options.skipGrantPrompt === true)) throw error;
|
|
15302
|
+
if (!(await requestStudioResourceGrant(grantRequest))) {
|
|
15303
|
+
const cancelled = new Error("Local resource access cancelled.");
|
|
15304
|
+
cancelled.studioCancelled = true;
|
|
15305
|
+
throw cancelled;
|
|
15306
|
+
}
|
|
15307
|
+
return request();
|
|
15308
|
+
}
|
|
14829
15309
|
}
|
|
14830
15310
|
|
|
14831
15311
|
function getPreviewPdfViewerUrl(href, contextOverride) {
|
|
@@ -14836,16 +15316,16 @@
|
|
|
14836
15316
|
return resourceUrl && page ? resourceUrl + "#page=" + encodeURIComponent(String(page)) : resourceUrl;
|
|
14837
15317
|
}
|
|
14838
15318
|
|
|
14839
|
-
function openPreviewPdfLink(href, title, contextOverride) {
|
|
15319
|
+
async function openPreviewPdfLink(href, title, contextOverride) {
|
|
15320
|
+
await fetchPreviewLocalLink("resolve", href, contextOverride);
|
|
14840
15321
|
const viewerUrl = getPreviewPdfViewerUrl(href, contextOverride);
|
|
14841
15322
|
if (!viewerUrl) {
|
|
14842
15323
|
setStatus("Could not resolve this PDF link. Open the source file or set a working directory first.", "warning");
|
|
14843
15324
|
return false;
|
|
14844
15325
|
}
|
|
14845
|
-
const cleanPath = stripPreviewLocalLinkUrlSuffix(href);
|
|
14846
15326
|
const context = contextOverride && typeof contextOverride === "object" ? contextOverride : {};
|
|
14847
15327
|
const resourceQuery = buildStudioPdfResourceQuery({
|
|
14848
|
-
path:
|
|
15328
|
+
path: stripPreviewLocalLinkUrlSuffix(href),
|
|
14849
15329
|
sourcePath: context.sourcePath || "",
|
|
14850
15330
|
resourceDir: context.resourceDir || "",
|
|
14851
15331
|
}, true);
|
|
@@ -14854,6 +15334,7 @@
|
|
|
14854
15334
|
}
|
|
14855
15335
|
|
|
14856
15336
|
async function openPreviewImageLink(href, title, contextOverride) {
|
|
15337
|
+
await fetchPreviewLocalLink("resolve", href, contextOverride);
|
|
14857
15338
|
const payload = await fetchStudioJson("/html-preview-resource", {
|
|
14858
15339
|
query: getPreviewLinkResourceQuery(href, contextOverride),
|
|
14859
15340
|
});
|
|
@@ -14955,7 +15436,11 @@
|
|
|
14955
15436
|
navigatePendingStudioTab(launch, relativeUrl);
|
|
14956
15437
|
setStatus(payload && payload.converted ? "Opening converted document in a new editor." : "Opening file-backed document in a new editor.");
|
|
14957
15438
|
} catch (error) {
|
|
14958
|
-
|
|
15439
|
+
if (error && error.studioCancelled) {
|
|
15440
|
+
cancelPendingStudioTab(launch, "Local resource access was cancelled.");
|
|
15441
|
+
} else {
|
|
15442
|
+
failPendingStudioTab(launch, "Studio could not prepare this document tab. Return to the originating Studio page for details.");
|
|
15443
|
+
}
|
|
14959
15444
|
throw error;
|
|
14960
15445
|
}
|
|
14961
15446
|
}
|
|
@@ -14970,7 +15455,11 @@
|
|
|
14970
15455
|
navigatePendingStudioTab(launch, relativeUrl);
|
|
14971
15456
|
setStatus("Opening preview in a new Studio tab.");
|
|
14972
15457
|
} catch (error) {
|
|
14973
|
-
|
|
15458
|
+
if (error && error.studioCancelled) {
|
|
15459
|
+
cancelPendingStudioTab(launch, "Local resource access was cancelled.");
|
|
15460
|
+
} else {
|
|
15461
|
+
failPendingStudioTab(launch, "Studio could not prepare this preview tab. Return to the originating Studio page for details.");
|
|
15462
|
+
}
|
|
14974
15463
|
throw error;
|
|
14975
15464
|
}
|
|
14976
15465
|
}
|
|
@@ -14985,6 +15474,7 @@
|
|
|
14985
15474
|
}
|
|
14986
15475
|
|
|
14987
15476
|
async function revealPreviewLocalLink(href, contextOverride) {
|
|
15477
|
+
await fetchPreviewLocalLink("resolve", href, contextOverride);
|
|
14988
15478
|
const query = getPreviewLinkResourceQuery(href, contextOverride);
|
|
14989
15479
|
const payload = await fetchStudioJson("/reveal-local-resource", {
|
|
14990
15480
|
method: "POST",
|
|
@@ -14998,10 +15488,11 @@
|
|
|
14998
15488
|
if (!href) return;
|
|
14999
15489
|
try {
|
|
15000
15490
|
if (action === "open-pdf") {
|
|
15001
|
-
openPreviewPdfLink(href, context.title || href, context);
|
|
15491
|
+
await openPreviewPdfLink(href, context.title || href, context);
|
|
15002
15492
|
return;
|
|
15003
15493
|
}
|
|
15004
15494
|
if (action === "open-system") {
|
|
15495
|
+
await fetchPreviewLocalLink("resolve", href, context);
|
|
15005
15496
|
await runStudioPdfLocalAction("system-viewer", getPreviewLinkResourceQuery(href, context));
|
|
15006
15497
|
return;
|
|
15007
15498
|
}
|
|
@@ -15043,7 +15534,9 @@
|
|
|
15043
15534
|
closePreviewLinkMenu();
|
|
15044
15535
|
const title = String(anchor.textContent || href).trim() || href;
|
|
15045
15536
|
if (kind === "pdf") {
|
|
15046
|
-
openPreviewPdfLink(href, title)
|
|
15537
|
+
void openPreviewPdfLink(href, title).catch((error) => {
|
|
15538
|
+
setStatus((error && error.message) ? error.message : String(error || "Could not open linked PDF."), "warning");
|
|
15539
|
+
});
|
|
15047
15540
|
return;
|
|
15048
15541
|
}
|
|
15049
15542
|
if (kind === "image") {
|
|
@@ -15053,9 +15546,7 @@
|
|
|
15053
15546
|
return;
|
|
15054
15547
|
}
|
|
15055
15548
|
if (kind === "text" || kind === "office") {
|
|
15056
|
-
void
|
|
15057
|
-
setStatus((error && error.message) ? error.message : String(error || "Could not open linked file."), "warning");
|
|
15058
|
-
});
|
|
15549
|
+
void showPreviewLinkMenu(anchor, event);
|
|
15059
15550
|
return;
|
|
15060
15551
|
}
|
|
15061
15552
|
setStatus("Right-click this local link for file actions.", "warning");
|
|
@@ -15066,7 +15557,7 @@
|
|
|
15066
15557
|
if (!anchor) return;
|
|
15067
15558
|
event.preventDefault();
|
|
15068
15559
|
event.stopPropagation();
|
|
15069
|
-
showPreviewLinkMenu(anchor, event);
|
|
15560
|
+
void showPreviewLinkMenu(anchor, event);
|
|
15070
15561
|
}
|
|
15071
15562
|
|
|
15072
15563
|
function makeRequestId() {
|
|
@@ -15245,6 +15736,7 @@
|
|
|
15245
15736
|
const html = await renderQuizMarkdownToHtml(markdown);
|
|
15246
15737
|
if (nonce !== quizPreviewRenderNonce || !quizDialogEl || !quizDialogEl.contains(target)) return;
|
|
15247
15738
|
target.innerHTML = html;
|
|
15739
|
+
await hydrateStudioPreviewLocalMedia(target, getHtmlPreviewResourceContextOptions());
|
|
15248
15740
|
await renderAnnotationMathInElement(target);
|
|
15249
15741
|
decoratePdfEmbeds(target);
|
|
15250
15742
|
await renderPdfPreviewsInElement(target);
|