pi-studio 0.9.9 → 0.9.11
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 +15 -0
- package/client/studio-client.js +666 -10
- package/client/studio.css +128 -13
- package/index.ts +228 -4
- package/package.json +1 -1
package/client/studio.css
CHANGED
|
@@ -2456,6 +2456,39 @@
|
|
|
2456
2456
|
overflow-x: auto;
|
|
2457
2457
|
}
|
|
2458
2458
|
|
|
2459
|
+
.rendered-markdown .mermaid-source-toolbar {
|
|
2460
|
+
display: flex;
|
|
2461
|
+
justify-content: flex-end;
|
|
2462
|
+
margin: 0 0 4px;
|
|
2463
|
+
pointer-events: none;
|
|
2464
|
+
}
|
|
2465
|
+
|
|
2466
|
+
.rendered-markdown .mermaid-source-toolbar .studio-copy-mermaid-source-btn {
|
|
2467
|
+
position: static;
|
|
2468
|
+
top: auto;
|
|
2469
|
+
right: auto;
|
|
2470
|
+
z-index: auto;
|
|
2471
|
+
padding: 2px 7px;
|
|
2472
|
+
border-color: transparent;
|
|
2473
|
+
background: transparent;
|
|
2474
|
+
box-shadow: none;
|
|
2475
|
+
font-size: 11px;
|
|
2476
|
+
opacity: 0.38;
|
|
2477
|
+
pointer-events: auto;
|
|
2478
|
+
}
|
|
2479
|
+
|
|
2480
|
+
.rendered-markdown .mermaid-container:hover > .studio-copy-block-btn {
|
|
2481
|
+
opacity: 0.38;
|
|
2482
|
+
}
|
|
2483
|
+
|
|
2484
|
+
.rendered-markdown .mermaid-source-toolbar .studio-copy-mermaid-source-btn:hover,
|
|
2485
|
+
.rendered-markdown .mermaid-source-toolbar .studio-copy-mermaid-source-btn:focus-visible {
|
|
2486
|
+
opacity: 1;
|
|
2487
|
+
color: var(--text);
|
|
2488
|
+
border-color: var(--control-border);
|
|
2489
|
+
background: var(--panel);
|
|
2490
|
+
}
|
|
2491
|
+
|
|
2459
2492
|
.rendered-markdown .mermaid-container svg {
|
|
2460
2493
|
max-width: 100%;
|
|
2461
2494
|
height: auto;
|
|
@@ -3326,15 +3359,26 @@
|
|
|
3326
3359
|
grid-area: hint;
|
|
3327
3360
|
justify-self: end;
|
|
3328
3361
|
align-self: center;
|
|
3362
|
+
display: inline-flex;
|
|
3363
|
+
align-items: center;
|
|
3364
|
+
gap: 6px;
|
|
3365
|
+
padding: 4px 8px;
|
|
3366
|
+
border-radius: 999px;
|
|
3367
|
+
border: 1px solid transparent;
|
|
3368
|
+
background: transparent;
|
|
3329
3369
|
color: var(--studio-footer-text, var(--muted));
|
|
3330
3370
|
font-size: 11px;
|
|
3371
|
+
line-height: 1.2;
|
|
3331
3372
|
white-space: nowrap;
|
|
3332
|
-
|
|
3333
|
-
|
|
3334
|
-
|
|
3335
|
-
|
|
3336
|
-
|
|
3337
|
-
|
|
3373
|
+
opacity: 0.9;
|
|
3374
|
+
}
|
|
3375
|
+
|
|
3376
|
+
.shortcut-hint:not(:disabled):hover,
|
|
3377
|
+
.shortcut-hint:focus-visible {
|
|
3378
|
+
background: var(--panel-2);
|
|
3379
|
+
border-color: var(--control-border);
|
|
3380
|
+
color: var(--text);
|
|
3381
|
+
opacity: 1;
|
|
3338
3382
|
}
|
|
3339
3383
|
|
|
3340
3384
|
.footer-compact-btn {
|
|
@@ -3369,7 +3413,8 @@
|
|
|
3369
3413
|
overflow: hidden;
|
|
3370
3414
|
}
|
|
3371
3415
|
|
|
3372
|
-
.scratchpad-overlay
|
|
3416
|
+
.scratchpad-overlay,
|
|
3417
|
+
.shortcuts-overlay {
|
|
3373
3418
|
position: fixed;
|
|
3374
3419
|
inset: 0;
|
|
3375
3420
|
z-index: 50;
|
|
@@ -3381,10 +3426,28 @@
|
|
|
3381
3426
|
backdrop-filter: blur(2px);
|
|
3382
3427
|
}
|
|
3383
3428
|
|
|
3384
|
-
.scratchpad-overlay[hidden]
|
|
3429
|
+
.scratchpad-overlay[hidden],
|
|
3430
|
+
.shortcuts-overlay[hidden] {
|
|
3385
3431
|
display: none !important;
|
|
3386
3432
|
}
|
|
3387
3433
|
|
|
3434
|
+
.scratchpad-dialog,
|
|
3435
|
+
.shortcuts-dialog {
|
|
3436
|
+
width: min(860px, 100%);
|
|
3437
|
+
max-height: min(82vh, 900px);
|
|
3438
|
+
border: 1px solid var(--panel-border);
|
|
3439
|
+
border-radius: 14px;
|
|
3440
|
+
background: var(--panel);
|
|
3441
|
+
box-shadow: 0 18px 50px var(--shadow-color);
|
|
3442
|
+
display: flex;
|
|
3443
|
+
flex-direction: column;
|
|
3444
|
+
overflow: hidden;
|
|
3445
|
+
}
|
|
3446
|
+
|
|
3447
|
+
.shortcuts-dialog {
|
|
3448
|
+
width: min(720px, 100%);
|
|
3449
|
+
}
|
|
3450
|
+
|
|
3388
3451
|
.scratchpad-dialog {
|
|
3389
3452
|
width: min(860px, 100%);
|
|
3390
3453
|
max-height: min(82vh, 900px);
|
|
@@ -3397,7 +3460,8 @@
|
|
|
3397
3460
|
overflow: hidden;
|
|
3398
3461
|
}
|
|
3399
3462
|
|
|
3400
|
-
.scratchpad-header
|
|
3463
|
+
.scratchpad-header,
|
|
3464
|
+
.shortcuts-header {
|
|
3401
3465
|
display: flex;
|
|
3402
3466
|
align-items: flex-start;
|
|
3403
3467
|
justify-content: space-between;
|
|
@@ -3407,18 +3471,21 @@
|
|
|
3407
3471
|
background: var(--scratchpad-header-bg, var(--panel-2));
|
|
3408
3472
|
}
|
|
3409
3473
|
|
|
3410
|
-
.scratchpad-header > div
|
|
3474
|
+
.scratchpad-header > div,
|
|
3475
|
+
.shortcuts-header > div {
|
|
3411
3476
|
flex: 1 1 auto;
|
|
3412
3477
|
min-width: 0;
|
|
3413
3478
|
}
|
|
3414
3479
|
|
|
3415
|
-
.scratchpad-header h2
|
|
3480
|
+
.scratchpad-header h2,
|
|
3481
|
+
.shortcuts-header h2 {
|
|
3416
3482
|
margin: 0;
|
|
3417
3483
|
font-size: 17px;
|
|
3418
3484
|
font-weight: 600;
|
|
3419
3485
|
}
|
|
3420
3486
|
|
|
3421
|
-
.scratchpad-description
|
|
3487
|
+
.scratchpad-description,
|
|
3488
|
+
.shortcuts-description {
|
|
3422
3489
|
margin: 6px 0 0;
|
|
3423
3490
|
font-size: 12px;
|
|
3424
3491
|
line-height: 1.45;
|
|
@@ -3426,12 +3493,60 @@
|
|
|
3426
3493
|
max-width: none;
|
|
3427
3494
|
}
|
|
3428
3495
|
|
|
3429
|
-
.scratchpad-close-btn
|
|
3496
|
+
.scratchpad-close-btn,
|
|
3497
|
+
.shortcuts-close-btn {
|
|
3430
3498
|
padding: 6px 10px;
|
|
3431
3499
|
line-height: 1;
|
|
3432
3500
|
flex: 0 0 auto;
|
|
3433
3501
|
}
|
|
3434
3502
|
|
|
3503
|
+
.shortcuts-body {
|
|
3504
|
+
display: grid;
|
|
3505
|
+
gap: 14px;
|
|
3506
|
+
padding: 16px 18px 18px;
|
|
3507
|
+
overflow: auto;
|
|
3508
|
+
background: var(--panel);
|
|
3509
|
+
}
|
|
3510
|
+
|
|
3511
|
+
.shortcuts-group h3 {
|
|
3512
|
+
margin: 0 0 8px;
|
|
3513
|
+
font-size: 12px;
|
|
3514
|
+
font-weight: 700;
|
|
3515
|
+
color: var(--studio-info-text, var(--muted));
|
|
3516
|
+
text-transform: uppercase;
|
|
3517
|
+
letter-spacing: 0.04em;
|
|
3518
|
+
}
|
|
3519
|
+
|
|
3520
|
+
.shortcuts-group dl {
|
|
3521
|
+
display: grid;
|
|
3522
|
+
gap: 6px;
|
|
3523
|
+
margin: 0;
|
|
3524
|
+
}
|
|
3525
|
+
|
|
3526
|
+
.shortcuts-group dl > div {
|
|
3527
|
+
display: grid;
|
|
3528
|
+
grid-template-columns: minmax(130px, max-content) minmax(0, 1fr);
|
|
3529
|
+
gap: 12px;
|
|
3530
|
+
align-items: baseline;
|
|
3531
|
+
padding: 6px 0;
|
|
3532
|
+
border-top: 1px solid var(--border-subtle);
|
|
3533
|
+
}
|
|
3534
|
+
|
|
3535
|
+
.shortcuts-group dt {
|
|
3536
|
+
margin: 0;
|
|
3537
|
+
font-family: var(--font-mono);
|
|
3538
|
+
font-size: 12px;
|
|
3539
|
+
color: var(--text);
|
|
3540
|
+
white-space: nowrap;
|
|
3541
|
+
}
|
|
3542
|
+
|
|
3543
|
+
.shortcuts-group dd {
|
|
3544
|
+
margin: 0;
|
|
3545
|
+
color: var(--studio-info-text, var(--muted));
|
|
3546
|
+
font-size: 12px;
|
|
3547
|
+
line-height: 1.35;
|
|
3548
|
+
}
|
|
3549
|
+
|
|
3435
3550
|
.scratchpad-textarea {
|
|
3436
3551
|
width: 100%;
|
|
3437
3552
|
min-height: 280px;
|
package/index.ts
CHANGED
|
@@ -64,6 +64,19 @@ interface StudioPromptDescriptor {
|
|
|
64
64
|
promptTriggerText: string | null;
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
+
interface StudioHtmlPreviewMathRenderItem {
|
|
68
|
+
mathId: string;
|
|
69
|
+
tex: string;
|
|
70
|
+
display: boolean;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
interface StudioHtmlPreviewMathRenderResult {
|
|
74
|
+
mathId: string;
|
|
75
|
+
ok: boolean;
|
|
76
|
+
html?: string;
|
|
77
|
+
error?: string;
|
|
78
|
+
}
|
|
79
|
+
|
|
67
80
|
interface ActiveStudioRequest extends StudioPromptDescriptor {
|
|
68
81
|
id: string;
|
|
69
82
|
kind: StudioRequestKind;
|
|
@@ -460,6 +473,9 @@ const REQUEST_TIMEOUT_MS = 5 * 60 * 1000;
|
|
|
460
473
|
const PREVIEW_RENDER_MAX_CHARS = 400_000;
|
|
461
474
|
const PDF_EXPORT_MAX_CHARS = 400_000;
|
|
462
475
|
const HTML_EXPORT_MAX_CHARS = 400_000;
|
|
476
|
+
const HTML_PREVIEW_MATH_RENDER_MAX_ITEMS = 250;
|
|
477
|
+
const HTML_PREVIEW_MATH_RENDER_ITEM_MAX_CHARS = 8_000;
|
|
478
|
+
const HTML_PREVIEW_MATH_RENDER_TOTAL_MAX_CHARS = 120_000;
|
|
463
479
|
const STUDIO_QUIZ_SOURCE_MAX_CHARS = 80_000;
|
|
464
480
|
const STUDIO_QUIZ_CONTEXT_FILE_MAX_CHARS = 14_000;
|
|
465
481
|
const STUDIO_QUIZ_CONTEXT_MAX_FILES = 18;
|
|
@@ -4900,6 +4916,72 @@ function stripMathMlAnnotationTags(html: string): string {
|
|
|
4900
4916
|
});
|
|
4901
4917
|
}
|
|
4902
4918
|
|
|
4919
|
+
function normalizeStudioHtmlPreviewMathForPandoc(tex: string): string {
|
|
4920
|
+
return String(tex ?? "")
|
|
4921
|
+
.replace(/\r\n/g, "\n")
|
|
4922
|
+
.replace(/\\rm\s*\{([^{}]+)\}/g, "\\mathrm{$1}")
|
|
4923
|
+
.replace(/\\rm\s+([A-Za-z]+)(?=[^A-Za-z]|$)/g, "\\mathrm{$1}");
|
|
4924
|
+
}
|
|
4925
|
+
|
|
4926
|
+
function getStudioHtmlPreviewMathWrapperId(index: number): string {
|
|
4927
|
+
return `studio-html-preview-math-${Math.max(0, Math.floor(index))}`;
|
|
4928
|
+
}
|
|
4929
|
+
|
|
4930
|
+
function buildStudioHtmlPreviewMathPandocSource(items: StudioHtmlPreviewMathRenderItem[]): string {
|
|
4931
|
+
return items.map((item, index) => {
|
|
4932
|
+
const wrapperId = getStudioHtmlPreviewMathWrapperId(index);
|
|
4933
|
+
const tex = normalizeStudioHtmlPreviewMathForPandoc(item.tex);
|
|
4934
|
+
const mathSource = item.display ? `\\[\n${tex}\n\\]` : `\\(${tex}\\)`;
|
|
4935
|
+
return `:::: {#${wrapperId} .studio-html-preview-math-render-item}\n${mathSource}\n::::`;
|
|
4936
|
+
}).join("\n\n");
|
|
4937
|
+
}
|
|
4938
|
+
|
|
4939
|
+
function extractStudioHtmlPreviewMathHtml(renderedHtml: string, wrapperId: string): string {
|
|
4940
|
+
const idPattern = escapeStudioRegExpLiteral(wrapperId);
|
|
4941
|
+
const wrapperPattern = new RegExp(`<div\\b(?=[^>]*\\bid="${idPattern}")[^>]*>([\\s\\S]*?)<\\/div>`, "i");
|
|
4942
|
+
const wrapperMatch = String(renderedHtml ?? "").match(wrapperPattern);
|
|
4943
|
+
const wrapperHtml = wrapperMatch ? String(wrapperMatch[1] ?? "") : "";
|
|
4944
|
+
const mathMatch = wrapperHtml.match(/<math\b[\s\S]*?<\/math>/i);
|
|
4945
|
+
return mathMatch ? stripMathMlAnnotationTags(mathMatch[0]) : "";
|
|
4946
|
+
}
|
|
4947
|
+
|
|
4948
|
+
async function renderStudioHtmlPreviewMathWithPandoc(items: StudioHtmlPreviewMathRenderItem[]): Promise<StudioHtmlPreviewMathRenderResult[]> {
|
|
4949
|
+
if (items.length === 0) return [];
|
|
4950
|
+
const pandocCommand = process.env.PANDOC_PATH?.trim() || "pandoc";
|
|
4951
|
+
const inputFormat = "markdown+tex_math_dollars+tex_math_single_backslash+tex_math_double_backslash";
|
|
4952
|
+
const args = ["-f", inputFormat, "-t", "html5", "--mathml", "--wrap=none"];
|
|
4953
|
+
const source = buildStudioHtmlPreviewMathPandocSource(items);
|
|
4954
|
+
const pandocResult = await runStudioSubprocess(pandocCommand, args, {
|
|
4955
|
+
input: source,
|
|
4956
|
+
timeoutMs: STUDIO_PANDOC_TIMEOUT_MS,
|
|
4957
|
+
stdoutMaxBytes: Math.min(STUDIO_HTML_RENDER_OUTPUT_MAX_BYTES, 10_000_000),
|
|
4958
|
+
label: "pandoc HTML preview math render",
|
|
4959
|
+
notFoundMessage: "pandoc was not found. Install pandoc or set PANDOC_PATH to the pandoc binary.",
|
|
4960
|
+
});
|
|
4961
|
+
if (pandocResult.code !== 0) {
|
|
4962
|
+
throw new Error(`pandoc math render failed with exit code ${pandocResult.code}${pandocResult.stderr ? `: ${pandocResult.stderr}` : ""}`);
|
|
4963
|
+
}
|
|
4964
|
+
if (pandocResult.stdoutTruncated) {
|
|
4965
|
+
throw new Error("pandoc math render output exceeded Studio's size limit.");
|
|
4966
|
+
}
|
|
4967
|
+
|
|
4968
|
+
return items.map((item, index) => {
|
|
4969
|
+
const html = extractStudioHtmlPreviewMathHtml(pandocResult.stdout, getStudioHtmlPreviewMathWrapperId(index));
|
|
4970
|
+
if (!html) {
|
|
4971
|
+
return {
|
|
4972
|
+
mathId: item.mathId,
|
|
4973
|
+
ok: false,
|
|
4974
|
+
error: "Pandoc did not render this expression as MathML.",
|
|
4975
|
+
};
|
|
4976
|
+
}
|
|
4977
|
+
return {
|
|
4978
|
+
mathId: item.mathId,
|
|
4979
|
+
ok: true,
|
|
4980
|
+
html,
|
|
4981
|
+
};
|
|
4982
|
+
});
|
|
4983
|
+
}
|
|
4984
|
+
|
|
4903
4985
|
function normalizeObsidianImages(markdown: string): string {
|
|
4904
4986
|
// Use angle-bracket destinations so paths with spaces/special chars are safe for Pandoc
|
|
4905
4987
|
return markdown
|
|
@@ -8675,7 +8757,7 @@ ${cssVarsBlock}
|
|
|
8675
8757
|
<label class="file-label" title="Load a local file into editor text.">Load file content<input id="fileInput" type="file" accept=".md,.markdown,.mdx,.qmd,.js,.mjs,.cjs,.jsx,.ts,.mts,.cts,.tsx,.py,.pyw,.sh,.bash,.zsh,.json,.jsonc,.json5,.rs,.c,.h,.cpp,.cxx,.cc,.hpp,.hxx,.jl,.f90,.f95,.f03,.f,.for,.r,.R,.m,.tex,.latex,.diff,.patch,.java,.go,.rb,.swift,.html,.htm,.css,.xml,.yaml,.yml,.toml,.lua,.txt,.rst,.adoc" /></label>
|
|
8676
8758
|
<button id="loadGitDiffBtn" type="button" title="Load the current git diff from the Studio context into the editor.">Load git diff</button>
|
|
8677
8759
|
<button id="getEditorBtn" type="button" title="Load the current terminal editor draft into Studio.">Load from pi editor</button>
|
|
8678
|
-
<button id="zenModeBtn" class="zen-mode-btn" type="button" title="Hide secondary Studio controls.">Zen</button>
|
|
8760
|
+
<button id="zenModeBtn" class="zen-mode-btn" type="button" title="Hide secondary Studio controls. Shortcut: F9.">Zen</button>
|
|
8679
8761
|
</div>
|
|
8680
8762
|
</header>
|
|
8681
8763
|
|
|
@@ -8683,7 +8765,7 @@ ${cssVarsBlock}
|
|
|
8683
8765
|
<section id="leftPane">
|
|
8684
8766
|
<div id="leftSectionHeader" class="section-header">
|
|
8685
8767
|
<div class="section-header-main">
|
|
8686
|
-
<select id="editorViewSelect" aria-label="Editor view mode">
|
|
8768
|
+
<select id="editorViewSelect" aria-label="Editor view mode" title="Editor view mode. Shortcut: F7 when the editor pane is active; F6 switches panes.">
|
|
8687
8769
|
<option value="markdown" selected>Editor (Raw)</option>
|
|
8688
8770
|
<option value="preview">Editor (Preview)</option>
|
|
8689
8771
|
</select>
|
|
@@ -8858,7 +8940,7 @@ ${cssVarsBlock}
|
|
|
8858
8940
|
<section id="rightPane">
|
|
8859
8941
|
<div id="rightSectionHeader" class="section-header">
|
|
8860
8942
|
<div class="section-header-main">
|
|
8861
|
-
<select id="rightViewSelect" aria-label="Response view mode">
|
|
8943
|
+
<select id="rightViewSelect" aria-label="Response view mode" title="Right pane view mode. Shortcut: F7 when the right pane is active; F6 switches panes.">
|
|
8862
8944
|
<option value="markdown">Response (Raw)</option>
|
|
8863
8945
|
<option value="preview" selected>Response (Preview)</option>
|
|
8864
8946
|
<option value="editor-preview">Editor (Preview)</option>
|
|
@@ -8929,9 +9011,50 @@ ${cssVarsBlock}
|
|
|
8929
9011
|
<footer>
|
|
8930
9012
|
<span id="statusLine"><span id="statusSpinner" aria-hidden="true"> </span><span id="status">Booting studio…</span></span>
|
|
8931
9013
|
<span id="footerMeta" class="footer-meta"><span id="footerMetaText" class="footer-meta-text"><span id="footerMetaModel" class="footer-meta-part footer-meta-model">${initialModel}</span><span class="footer-meta-sep">·</span><span id="footerMetaTerminal" class="footer-meta-part footer-meta-terminal">${initialTerminal}</span><span class="footer-meta-sep">·</span><span id="footerMetaContext" class="footer-meta-part footer-meta-context">unknown</span></span><button id="compactBtn" class="footer-compact-btn" type="button" title="Trigger pi context compaction now.">Compact</button></span>
|
|
8932
|
-
<
|
|
9014
|
+
<button id="shortcutsBtn" class="shortcut-hint" type="button" title="Show Studio keyboard shortcuts. Press ? when not editing text.">Shortcuts (?)</button>
|
|
8933
9015
|
</footer>
|
|
8934
9016
|
|
|
9017
|
+
<div id="shortcutsOverlay" class="shortcuts-overlay" hidden>
|
|
9018
|
+
<div id="shortcutsDialog" class="shortcuts-dialog" role="dialog" aria-modal="true" aria-labelledby="shortcutsTitle">
|
|
9019
|
+
<div class="shortcuts-header">
|
|
9020
|
+
<div>
|
|
9021
|
+
<h2 id="shortcutsTitle">Keyboard shortcuts</h2>
|
|
9022
|
+
<p class="shortcuts-description">Studio navigation and high-frequency actions.</p>
|
|
9023
|
+
</div>
|
|
9024
|
+
<button id="shortcutsCloseBtn" class="shortcuts-close-btn" type="button" aria-label="Close keyboard shortcuts">Close</button>
|
|
9025
|
+
</div>
|
|
9026
|
+
<div class="shortcuts-body">
|
|
9027
|
+
<section class="shortcuts-group">
|
|
9028
|
+
<h3>Navigation</h3>
|
|
9029
|
+
<dl>
|
|
9030
|
+
<div><dt>F6</dt><dd>Switch between editor and right pane</dd></div>
|
|
9031
|
+
<div><dt>F7 / Shift+F7</dt><dd>Cycle the active pane's view</dd></div>
|
|
9032
|
+
<div><dt>F8</dt><dd>Focus editor text</dd></div>
|
|
9033
|
+
<div><dt>Shift+F8</dt><dd>Focus right-pane content</dd></div>
|
|
9034
|
+
<div><dt>F9</dt><dd>Toggle Zen mode</dd></div>
|
|
9035
|
+
<div><dt>F10</dt><dd>Focus or unfocus the active pane</dd></div>
|
|
9036
|
+
<div><dt>Esc</dt><dd>Close overlays, exit pane focus, or stop an active request</dd></div>
|
|
9037
|
+
<div><dt>?</dt><dd>Show keyboard shortcuts when not editing text</dd></div>
|
|
9038
|
+
</dl>
|
|
9039
|
+
</section>
|
|
9040
|
+
<section class="shortcuts-group">
|
|
9041
|
+
<h3>Editor</h3>
|
|
9042
|
+
<dl>
|
|
9043
|
+
<div><dt>Cmd/Ctrl+S</dt><dd>Save editor</dd></div>
|
|
9044
|
+
<div><dt>Cmd/Ctrl+Enter</dt><dd>Run editor text, or queue steering during an active run</dd></div>
|
|
9045
|
+
<div><dt>Tab / Shift+Tab</dt><dd>Indent or unindent selected editor text</dd></div>
|
|
9046
|
+
</dl>
|
|
9047
|
+
</section>
|
|
9048
|
+
<section class="shortcuts-group">
|
|
9049
|
+
<h3>REPL</h3>
|
|
9050
|
+
<dl>
|
|
9051
|
+
<div><dt>Cmd/Ctrl+Shift+Enter</dt><dd>Send selection, chunks, or editor text to the active REPL when the right pane is REPL</dd></div>
|
|
9052
|
+
</dl>
|
|
9053
|
+
</section>
|
|
9054
|
+
</div>
|
|
9055
|
+
</div>
|
|
9056
|
+
</div>
|
|
9057
|
+
|
|
8935
9058
|
<div id="scratchpadOverlay" class="scratchpad-overlay" hidden>
|
|
8936
9059
|
<div id="scratchpadDialog" class="scratchpad-dialog" role="dialog" aria-modal="true" aria-labelledby="scratchpadTitle">
|
|
8937
9060
|
<div class="scratchpad-header">
|
|
@@ -11438,6 +11561,84 @@ export default function (pi: ExtensionAPI) {
|
|
|
11438
11561
|
}
|
|
11439
11562
|
};
|
|
11440
11563
|
|
|
11564
|
+
const handleRenderMathRequest = async (req: IncomingMessage, res: ServerResponse) => {
|
|
11565
|
+
let rawBody = "";
|
|
11566
|
+
try {
|
|
11567
|
+
rawBody = await readRequestBody(req, REQUEST_BODY_MAX_BYTES);
|
|
11568
|
+
} catch (error) {
|
|
11569
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
11570
|
+
const status = message.includes("exceeds") ? 413 : 400;
|
|
11571
|
+
respondJson(res, status, { ok: false, error: message });
|
|
11572
|
+
return;
|
|
11573
|
+
}
|
|
11574
|
+
|
|
11575
|
+
let parsedBody: unknown;
|
|
11576
|
+
try {
|
|
11577
|
+
parsedBody = rawBody ? JSON.parse(rawBody) : {};
|
|
11578
|
+
} catch {
|
|
11579
|
+
respondJson(res, 400, { ok: false, error: "Invalid JSON body." });
|
|
11580
|
+
return;
|
|
11581
|
+
}
|
|
11582
|
+
|
|
11583
|
+
const rawItems =
|
|
11584
|
+
parsedBody && typeof parsedBody === "object" && Array.isArray((parsedBody as { items?: unknown }).items)
|
|
11585
|
+
? (parsedBody as { items: unknown[] }).items
|
|
11586
|
+
: null;
|
|
11587
|
+
if (!rawItems) {
|
|
11588
|
+
respondJson(res, 400, { ok: false, error: "Missing math items array in request body." });
|
|
11589
|
+
return;
|
|
11590
|
+
}
|
|
11591
|
+
if (rawItems.length > HTML_PREVIEW_MATH_RENDER_MAX_ITEMS) {
|
|
11592
|
+
respondJson(res, 413, {
|
|
11593
|
+
ok: false,
|
|
11594
|
+
error: `HTML preview math render accepts at most ${HTML_PREVIEW_MATH_RENDER_MAX_ITEMS} items per request.`,
|
|
11595
|
+
});
|
|
11596
|
+
return;
|
|
11597
|
+
}
|
|
11598
|
+
|
|
11599
|
+
const items: StudioHtmlPreviewMathRenderItem[] = [];
|
|
11600
|
+
let totalChars = 0;
|
|
11601
|
+
for (const rawItem of rawItems) {
|
|
11602
|
+
const item = rawItem && typeof rawItem === "object" ? rawItem as { mathId?: unknown; tex?: unknown; display?: unknown } : null;
|
|
11603
|
+
const mathId = typeof item?.mathId === "string" ? item.mathId.trim() : "";
|
|
11604
|
+
const tex = typeof item?.tex === "string" ? item.tex : "";
|
|
11605
|
+
if (!mathId || !tex.trim()) continue;
|
|
11606
|
+
if (mathId.length > 160) {
|
|
11607
|
+
respondJson(res, 400, { ok: false, error: "Math item id is too long." });
|
|
11608
|
+
return;
|
|
11609
|
+
}
|
|
11610
|
+
if (tex.length > HTML_PREVIEW_MATH_RENDER_ITEM_MAX_CHARS) {
|
|
11611
|
+
respondJson(res, 413, {
|
|
11612
|
+
ok: false,
|
|
11613
|
+
error: `A math expression exceeds ${HTML_PREVIEW_MATH_RENDER_ITEM_MAX_CHARS} characters.`,
|
|
11614
|
+
});
|
|
11615
|
+
return;
|
|
11616
|
+
}
|
|
11617
|
+
totalChars += tex.length;
|
|
11618
|
+
if (totalChars > HTML_PREVIEW_MATH_RENDER_TOTAL_MAX_CHARS) {
|
|
11619
|
+
respondJson(res, 413, {
|
|
11620
|
+
ok: false,
|
|
11621
|
+
error: `Math render text exceeds ${HTML_PREVIEW_MATH_RENDER_TOTAL_MAX_CHARS} characters.`,
|
|
11622
|
+
});
|
|
11623
|
+
return;
|
|
11624
|
+
}
|
|
11625
|
+
items.push({ mathId, tex, display: Boolean(item?.display) });
|
|
11626
|
+
}
|
|
11627
|
+
|
|
11628
|
+
if (items.length === 0) {
|
|
11629
|
+
respondJson(res, 400, { ok: false, error: "No valid math items to render." });
|
|
11630
|
+
return;
|
|
11631
|
+
}
|
|
11632
|
+
|
|
11633
|
+
try {
|
|
11634
|
+
const results = await renderStudioHtmlPreviewMathWithPandoc(items);
|
|
11635
|
+
respondJson(res, 200, { ok: true, renderer: "pandoc", results });
|
|
11636
|
+
} catch (error) {
|
|
11637
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
11638
|
+
respondJson(res, 500, { ok: false, error: `Math render failed: ${message}` });
|
|
11639
|
+
}
|
|
11640
|
+
};
|
|
11641
|
+
|
|
11441
11642
|
const handleExportPdfRequest = async (req: IncomingMessage, res: ServerResponse) => {
|
|
11442
11643
|
let rawBody = "";
|
|
11443
11644
|
try {
|
|
@@ -11803,6 +12004,29 @@ export default function (pi: ExtensionAPI) {
|
|
|
11803
12004
|
return;
|
|
11804
12005
|
}
|
|
11805
12006
|
|
|
12007
|
+
if (requestUrl.pathname === "/render-math") {
|
|
12008
|
+
const token = requestUrl.searchParams.get("token") ?? "";
|
|
12009
|
+
if (token !== serverState.token) {
|
|
12010
|
+
respondJson(res, 403, { ok: false, error: "Invalid or expired studio token. Re-run /studio." });
|
|
12011
|
+
return;
|
|
12012
|
+
}
|
|
12013
|
+
|
|
12014
|
+
const method = (req.method ?? "GET").toUpperCase();
|
|
12015
|
+
if (method !== "POST") {
|
|
12016
|
+
res.setHeader("Allow", "POST");
|
|
12017
|
+
respondJson(res, 405, { ok: false, error: "Method not allowed. Use POST." });
|
|
12018
|
+
return;
|
|
12019
|
+
}
|
|
12020
|
+
|
|
12021
|
+
void handleRenderMathRequest(req, res).catch((error) => {
|
|
12022
|
+
respondJson(res, 500, {
|
|
12023
|
+
ok: false,
|
|
12024
|
+
error: `Math render failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
12025
|
+
});
|
|
12026
|
+
});
|
|
12027
|
+
return;
|
|
12028
|
+
}
|
|
12029
|
+
|
|
11806
12030
|
if (requestUrl.pathname === "/export-pdf") {
|
|
11807
12031
|
const token = requestUrl.searchParams.get("token") ?? "";
|
|
11808
12032
|
if (token !== serverState.token) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-studio",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.11",
|
|
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",
|