pi-studio 0.3.0 → 0.4.0
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/README.md +12 -6
- package/index.ts +520 -129
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,8 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
Experimental extension for [pi](https://github.com/badlogic/pi-mono) that opens a local browser workspace for annotating model responses/files, running edited prompts, and requesting critiques.
|
|
4
4
|
|
|
5
|
-
Status: experimental alpha.
|
|
6
|
-
|
|
7
5
|
## Screenshots
|
|
8
6
|
|
|
9
7
|
**Markdown workspace — dark** (syntax-highlighted editor + rendered preview with Julia code block, inline math, blockquotes)
|
|
@@ -51,19 +49,26 @@ Status: experimental alpha.
|
|
|
51
49
|
- Response load helpers:
|
|
52
50
|
- non-critique: **Load response into editor**
|
|
53
51
|
- critique: **Load critique notes into editor** / **Load full critique into editor**
|
|
54
|
-
- File actions: **Save
|
|
55
|
-
- View toggles: `
|
|
52
|
+
- File actions: **Save editor as…**, **Save editor**, **Load file content**
|
|
53
|
+
- View toggles: panel header dropdowns for `Editor (Raw|Preview)` and `Response (Raw|Preview) | Editor (Preview)`
|
|
56
54
|
- **Editor Preview in response pane**: side-by-side source/rendered view (Overleaf-style) — select `Right: Editor (Preview)` to render editor text in the right pane with live debounced updates
|
|
57
55
|
- Preview mode supports MathML equations and Mermaid fenced diagrams
|
|
58
56
|
- **Language-aware syntax highlighting** with selectable language mode:
|
|
59
57
|
- Markdown (default): headings, links, code fences, lists, quotes, inline code
|
|
60
|
-
- Code languages: JavaScript, TypeScript, Python, Bash, JSON, Rust, C, C++, Julia, Fortran, R, MATLAB
|
|
58
|
+
- Code languages: JavaScript, TypeScript, Python, Bash, JSON, Rust, C, C++, Julia, Fortran, R, MATLAB, LaTeX, Diff
|
|
61
59
|
- Keywords, strings, comments, numbers, and variables highlighted using theme syntax color tokens
|
|
60
|
+
- **Diff highlighting**: added/removed lines shown with green/red backgrounds in both raw and preview modes
|
|
62
61
|
- Language auto-detected from file extension on file load; manually selectable via `Lang:` dropdown
|
|
63
62
|
- Applies to both editor Raw view (highlight overlay) and fenced code blocks in markdown
|
|
64
63
|
- Preview mode renders syntax-highlighted code when a non-markdown language is selected
|
|
64
|
+
- **LaTeX file support**: `.tex`/`.latex` files detected by content, rendered via pandoc with proper title block (title, author, date, abstract) styling
|
|
65
|
+
- **Diff file support**: `.diff`/`.patch` files rendered with coloured add/remove line backgrounds
|
|
66
|
+
- **Image embedding**: images in markdown and LaTeX files embedded as base64 data URIs via pandoc `--embed-resources`, with no external file serving required
|
|
67
|
+
- **Working directory**: "Set working dir" button for uploaded files — resolves relative image paths and enables "Save editor" for uploaded content
|
|
68
|
+
- **Live theme sync**: changing the pi theme in the terminal updates the studio browser UI automatically (polled every 2 seconds)
|
|
65
69
|
- Separate syntax highlight toggles for editor and response Raw views, with local preference persistence
|
|
66
70
|
- Theme-aware browser UI derived from current pi theme
|
|
71
|
+
- View mode selectors integrated into panel headers for a cleaner layout
|
|
67
72
|
|
|
68
73
|
## Commands
|
|
69
74
|
|
|
@@ -103,7 +108,8 @@ pi -e https://github.com/omaclaren/pi-studio
|
|
|
103
108
|
- One studio request at a time.
|
|
104
109
|
- Pi Studio supports both markdown workflows (model responses, plans, and notes) and code file editing with language-aware syntax highlighting.
|
|
105
110
|
- Studio URLs include a token query parameter; avoid sharing full Studio URLs.
|
|
106
|
-
- Preview panes render markdown via `pandoc` (`gfm+tex_math_dollars` → HTML5 + MathML), including pandoc code syntax highlighting, sanitized in-browser with `dompurify`.
|
|
111
|
+
- Preview panes render markdown via `pandoc` (`gfm+tex_math_dollars` → HTML5 + MathML), including pandoc code syntax highlighting, sanitized in-browser with `dompurify`. LaTeX files are rendered from `latex` input format with title block styling.
|
|
112
|
+
- Images referenced in markdown or via `\includegraphics` in LaTeX are embedded as base64 data URIs when a file path or working directory is available. For uploaded files without a working directory set, a notice suggests setting one.
|
|
107
113
|
- Preview markdown/code colors are mapped from active theme markdown (`md*`) and syntax (`syntax*`) tokens for closer terminal-vs-browser parity.
|
|
108
114
|
- Mermaid fenced `mermaid` code blocks are rendered client-side in preview mode (Mermaid v11 loaded from jsDelivr), with palette-driven defaults for better theme fit.
|
|
109
115
|
- If Mermaid cannot load or a diagram fails to render, preview shows an inline warning and keeps source text visible.
|
package/index.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { spawn } from "node:child_process";
|
|
|
3
3
|
import { randomUUID } from "node:crypto";
|
|
4
4
|
import { readFileSync, statSync, writeFileSync } from "node:fs";
|
|
5
5
|
import { createServer, type IncomingMessage, type Server, type ServerResponse } from "node:http";
|
|
6
|
-
import { isAbsolute, join, resolve } from "node:path";
|
|
6
|
+
import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
7
7
|
import { URL } from "node:url";
|
|
8
8
|
import { WebSocketServer, WebSocket, type RawData } from "ws";
|
|
9
9
|
|
|
@@ -610,10 +610,22 @@ function readStudioFile(pathArg: string, cwd: string):
|
|
|
610
610
|
}
|
|
611
611
|
|
|
612
612
|
try {
|
|
613
|
-
|
|
614
|
-
|
|
613
|
+
// Read raw bytes first to detect binary content before UTF-8 decode
|
|
614
|
+
const buf = readFileSync(resolved.resolved);
|
|
615
|
+
// Heuristic: check the first 8KB for binary indicators
|
|
616
|
+
const sample = buf.subarray(0, 8192);
|
|
617
|
+
let nulCount = 0;
|
|
618
|
+
let controlCount = 0;
|
|
619
|
+
for (let i = 0; i < sample.length; i++) {
|
|
620
|
+
const b = sample[i];
|
|
621
|
+
if (b === 0x00) nulCount++;
|
|
622
|
+
// Control chars excluding tab (0x09), newline (0x0A), carriage return (0x0D)
|
|
623
|
+
else if (b < 0x08 || (b > 0x0D && b < 0x20 && b !== 0x1B)) controlCount++;
|
|
624
|
+
}
|
|
625
|
+
if (nulCount > 0 || (sample.length > 0 && controlCount / sample.length > 0.1)) {
|
|
615
626
|
return { ok: false, message: `File appears to be binary: ${resolved.label}` };
|
|
616
627
|
}
|
|
628
|
+
const text = buf.toString("utf-8");
|
|
617
629
|
return { ok: true, text, label: resolved.label, resolvedPath: resolved.resolved };
|
|
618
630
|
} catch (error) {
|
|
619
631
|
return {
|
|
@@ -710,15 +722,22 @@ function stripMathMlAnnotationTags(html: string): string {
|
|
|
710
722
|
}
|
|
711
723
|
|
|
712
724
|
function normalizeObsidianImages(markdown: string): string {
|
|
725
|
+
// Use angle-bracket destinations so paths with spaces/special chars are safe for Pandoc
|
|
713
726
|
return markdown
|
|
714
|
-
.replace(/!\[\[([^|\]]+)\|([^\]]+)\]\]/g,
|
|
715
|
-
.replace(/!\[\[([^\]]+)\]\]/g,
|
|
727
|
+
.replace(/!\[\[([^|\]]+)\|([^\]]+)\]\]/g, (_m, path, alt) => ``)
|
|
728
|
+
.replace(/!\[\[([^\]]+)\]\]/g, (_m, path) => ``);
|
|
716
729
|
}
|
|
717
730
|
|
|
718
|
-
async function renderStudioMarkdownWithPandoc(markdown: string): Promise<string> {
|
|
731
|
+
async function renderStudioMarkdownWithPandoc(markdown: string, isLatex?: boolean, resourcePath?: string): Promise<string> {
|
|
719
732
|
const pandocCommand = process.env.PANDOC_PATH?.trim() || "pandoc";
|
|
720
|
-
const
|
|
721
|
-
const
|
|
733
|
+
const inputFormat = isLatex ? "latex" : "gfm+tex_math_dollars-raw_html";
|
|
734
|
+
const args = ["-f", inputFormat, "-t", "html5", "--mathml"];
|
|
735
|
+
if (resourcePath) {
|
|
736
|
+
args.push(`--resource-path=${resourcePath}`);
|
|
737
|
+
// Embed images as data URIs so they render in the browser preview
|
|
738
|
+
args.push("--embed-resources", "--standalone");
|
|
739
|
+
}
|
|
740
|
+
const normalizedMarkdown = isLatex ? markdown : normalizeObsidianImages(normalizeMathDelimiters(markdown));
|
|
722
741
|
|
|
723
742
|
return await new Promise<string>((resolve, reject) => {
|
|
724
743
|
const child = spawn(pandocCommand, args, { stdio: ["pipe", "pipe", "pipe"] });
|
|
@@ -757,7 +776,12 @@ async function renderStudioMarkdownWithPandoc(markdown: string): Promise<string>
|
|
|
757
776
|
child.once("close", (code) => {
|
|
758
777
|
if (settled) return;
|
|
759
778
|
if (code === 0) {
|
|
760
|
-
|
|
779
|
+
let renderedHtml = Buffer.concat(stdoutChunks).toString("utf-8");
|
|
780
|
+
// When --standalone was used, extract only the <body> content
|
|
781
|
+
if (resourcePath) {
|
|
782
|
+
const bodyMatch = renderedHtml.match(/<body[^>]*>([\s\S]*)<\/body>/i);
|
|
783
|
+
if (bodyMatch) renderedHtml = bodyMatch[1];
|
|
784
|
+
}
|
|
761
785
|
succeed(stripMathMlAnnotationTags(renderedHtml));
|
|
762
786
|
return;
|
|
763
787
|
}
|
|
@@ -1124,12 +1148,79 @@ function buildStudioUrl(port: number, token: string): string {
|
|
|
1124
1148
|
return `http://127.0.0.1:${port}/?token=${encoded}`;
|
|
1125
1149
|
}
|
|
1126
1150
|
|
|
1151
|
+
function buildThemeCssVars(style: StudioThemeStyle): Record<string, string> {
|
|
1152
|
+
const panelShadow =
|
|
1153
|
+
style.mode === "light"
|
|
1154
|
+
? "0 1px 2px rgba(15, 23, 42, 0.03), 0 4px 14px rgba(15, 23, 42, 0.04)"
|
|
1155
|
+
: "0 1px 2px rgba(0, 0, 0, 0.36), 0 6px 18px rgba(0, 0, 0, 0.22)";
|
|
1156
|
+
const accentContrast = style.mode === "light" ? "#ffffff" : "#0e1616";
|
|
1157
|
+
const blockquoteBg = withAlpha(
|
|
1158
|
+
style.palette.mdQuoteBorder,
|
|
1159
|
+
style.mode === "light" ? 0.10 : 0.16,
|
|
1160
|
+
style.mode === "light" ? "rgba(15, 23, 42, 0.04)" : "rgba(255, 255, 255, 0.05)",
|
|
1161
|
+
);
|
|
1162
|
+
const tableAltBg = withAlpha(
|
|
1163
|
+
style.palette.mdCodeBlockBorder,
|
|
1164
|
+
style.mode === "light" ? 0.10 : 0.14,
|
|
1165
|
+
style.mode === "light" ? "rgba(15, 23, 42, 0.03)" : "rgba(255, 255, 255, 0.04)",
|
|
1166
|
+
);
|
|
1167
|
+
const editorBg = style.mode === "light"
|
|
1168
|
+
? blendColors(style.palette.panel, "#ffffff", 0.5)
|
|
1169
|
+
: style.palette.panel;
|
|
1170
|
+
|
|
1171
|
+
return {
|
|
1172
|
+
"color-scheme": style.mode,
|
|
1173
|
+
"--bg": style.palette.bg,
|
|
1174
|
+
"--panel": style.palette.panel,
|
|
1175
|
+
"--panel-2": style.palette.panel2,
|
|
1176
|
+
"--border": style.palette.border,
|
|
1177
|
+
"--border-muted": style.palette.borderMuted,
|
|
1178
|
+
"--text": style.palette.text,
|
|
1179
|
+
"--muted": style.palette.muted,
|
|
1180
|
+
"--accent": style.palette.accent,
|
|
1181
|
+
"--warn": style.palette.warn,
|
|
1182
|
+
"--error": style.palette.error,
|
|
1183
|
+
"--ok": style.palette.ok,
|
|
1184
|
+
"--marker-bg": style.palette.markerBg,
|
|
1185
|
+
"--marker-border": style.palette.markerBorder,
|
|
1186
|
+
"--accent-soft": style.palette.accentSoft,
|
|
1187
|
+
"--accent-soft-strong": style.palette.accentSoftStrong,
|
|
1188
|
+
"--ok-border": style.palette.okBorder,
|
|
1189
|
+
"--warn-border": style.palette.warnBorder,
|
|
1190
|
+
"--md-heading": style.palette.mdHeading,
|
|
1191
|
+
"--md-link": style.palette.mdLink,
|
|
1192
|
+
"--md-link-url": style.palette.mdLinkUrl,
|
|
1193
|
+
"--md-code": style.palette.mdCode,
|
|
1194
|
+
"--md-codeblock": style.palette.mdCodeBlock,
|
|
1195
|
+
"--md-codeblock-border": style.palette.mdCodeBlockBorder,
|
|
1196
|
+
"--md-quote": style.palette.mdQuote,
|
|
1197
|
+
"--md-quote-border": style.palette.mdQuoteBorder,
|
|
1198
|
+
"--md-hr": style.palette.mdHr,
|
|
1199
|
+
"--md-list-bullet": style.palette.mdListBullet,
|
|
1200
|
+
"--syntax-comment": style.palette.syntaxComment,
|
|
1201
|
+
"--syntax-keyword": style.palette.syntaxKeyword,
|
|
1202
|
+
"--syntax-function": style.palette.syntaxFunction,
|
|
1203
|
+
"--syntax-variable": style.palette.syntaxVariable,
|
|
1204
|
+
"--syntax-string": style.palette.syntaxString,
|
|
1205
|
+
"--syntax-number": style.palette.syntaxNumber,
|
|
1206
|
+
"--syntax-type": style.palette.syntaxType,
|
|
1207
|
+
"--syntax-operator": style.palette.syntaxOperator,
|
|
1208
|
+
"--syntax-punctuation": style.palette.syntaxPunctuation,
|
|
1209
|
+
"--panel-shadow": panelShadow,
|
|
1210
|
+
"--accent-contrast": accentContrast,
|
|
1211
|
+
"--blockquote-bg": blockquoteBg,
|
|
1212
|
+
"--table-alt-bg": tableAltBg,
|
|
1213
|
+
"--editor-bg": editorBg,
|
|
1214
|
+
};
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1127
1217
|
function buildStudioHtml(initialDocument: InitialStudioDocument | null, theme?: Theme): string {
|
|
1128
1218
|
const initialText = escapeHtmlForInline(initialDocument?.text ?? "");
|
|
1129
1219
|
const initialSource = initialDocument?.source ?? "blank";
|
|
1130
1220
|
const initialLabel = escapeHtmlForInline(initialDocument?.label ?? "blank");
|
|
1131
1221
|
const initialPath = escapeHtmlForInline(initialDocument?.path ?? "");
|
|
1132
1222
|
const style = getStudioThemeStyle(theme);
|
|
1223
|
+
const vars = buildThemeCssVars(style);
|
|
1133
1224
|
const mermaidConfig = {
|
|
1134
1225
|
startOnLoad: false,
|
|
1135
1226
|
theme: "base",
|
|
@@ -1157,24 +1248,7 @@ function buildStudioHtml(initialDocument: InitialStudioDocument | null, theme?:
|
|
|
1157
1248
|
titleColor: style.palette.mdHeading,
|
|
1158
1249
|
},
|
|
1159
1250
|
};
|
|
1160
|
-
const
|
|
1161
|
-
style.mode === "light"
|
|
1162
|
-
? "0 1px 2px rgba(15, 23, 42, 0.03), 0 4px 14px rgba(15, 23, 42, 0.04)"
|
|
1163
|
-
: "0 1px 2px rgba(0, 0, 0, 0.36), 0 6px 18px rgba(0, 0, 0, 0.22)";
|
|
1164
|
-
const accentContrast = style.mode === "light" ? "#ffffff" : "#0e1616";
|
|
1165
|
-
const blockquoteBg = withAlpha(
|
|
1166
|
-
style.palette.mdQuoteBorder,
|
|
1167
|
-
style.mode === "light" ? 0.10 : 0.16,
|
|
1168
|
-
style.mode === "light" ? "rgba(15, 23, 42, 0.04)" : "rgba(255, 255, 255, 0.05)",
|
|
1169
|
-
);
|
|
1170
|
-
const tableAltBg = withAlpha(
|
|
1171
|
-
style.palette.mdCodeBlockBorder,
|
|
1172
|
-
style.mode === "light" ? 0.10 : 0.14,
|
|
1173
|
-
style.mode === "light" ? "rgba(15, 23, 42, 0.03)" : "rgba(255, 255, 255, 0.04)",
|
|
1174
|
-
);
|
|
1175
|
-
const editorBg = style.mode === "light"
|
|
1176
|
-
? blendColors(style.palette.panel, "#ffffff", 0.5)
|
|
1177
|
-
: style.palette.panel;
|
|
1251
|
+
const cssVarsBlock = Object.entries(vars).map(([k, v]) => ` ${k}: ${v};`).join("\n");
|
|
1178
1252
|
|
|
1179
1253
|
return `<!doctype html>
|
|
1180
1254
|
<html>
|
|
@@ -1184,48 +1258,7 @@ function buildStudioHtml(initialDocument: InitialStudioDocument | null, theme?:
|
|
|
1184
1258
|
<title>Pi Studio: Feedback Workspace</title>
|
|
1185
1259
|
<style>
|
|
1186
1260
|
:root {
|
|
1187
|
-
|
|
1188
|
-
--bg: ${style.palette.bg};
|
|
1189
|
-
--panel: ${style.palette.panel};
|
|
1190
|
-
--panel-2: ${style.palette.panel2};
|
|
1191
|
-
--border: ${style.palette.border};
|
|
1192
|
-
--border-muted: ${style.palette.borderMuted};
|
|
1193
|
-
--text: ${style.palette.text};
|
|
1194
|
-
--muted: ${style.palette.muted};
|
|
1195
|
-
--accent: ${style.palette.accent};
|
|
1196
|
-
--warn: ${style.palette.warn};
|
|
1197
|
-
--error: ${style.palette.error};
|
|
1198
|
-
--ok: ${style.palette.ok};
|
|
1199
|
-
--marker-bg: ${style.palette.markerBg};
|
|
1200
|
-
--marker-border: ${style.palette.markerBorder};
|
|
1201
|
-
--accent-soft: ${style.palette.accentSoft};
|
|
1202
|
-
--accent-soft-strong: ${style.palette.accentSoftStrong};
|
|
1203
|
-
--ok-border: ${style.palette.okBorder};
|
|
1204
|
-
--warn-border: ${style.palette.warnBorder};
|
|
1205
|
-
--md-heading: ${style.palette.mdHeading};
|
|
1206
|
-
--md-link: ${style.palette.mdLink};
|
|
1207
|
-
--md-link-url: ${style.palette.mdLinkUrl};
|
|
1208
|
-
--md-code: ${style.palette.mdCode};
|
|
1209
|
-
--md-codeblock: ${style.palette.mdCodeBlock};
|
|
1210
|
-
--md-codeblock-border: ${style.palette.mdCodeBlockBorder};
|
|
1211
|
-
--md-quote: ${style.palette.mdQuote};
|
|
1212
|
-
--md-quote-border: ${style.palette.mdQuoteBorder};
|
|
1213
|
-
--md-hr: ${style.palette.mdHr};
|
|
1214
|
-
--md-list-bullet: ${style.palette.mdListBullet};
|
|
1215
|
-
--syntax-comment: ${style.palette.syntaxComment};
|
|
1216
|
-
--syntax-keyword: ${style.palette.syntaxKeyword};
|
|
1217
|
-
--syntax-function: ${style.palette.syntaxFunction};
|
|
1218
|
-
--syntax-variable: ${style.palette.syntaxVariable};
|
|
1219
|
-
--syntax-string: ${style.palette.syntaxString};
|
|
1220
|
-
--syntax-number: ${style.palette.syntaxNumber};
|
|
1221
|
-
--syntax-type: ${style.palette.syntaxType};
|
|
1222
|
-
--syntax-operator: ${style.palette.syntaxOperator};
|
|
1223
|
-
--syntax-punctuation: ${style.palette.syntaxPunctuation};
|
|
1224
|
-
--panel-shadow: ${panelShadow};
|
|
1225
|
-
--accent-contrast: ${accentContrast};
|
|
1226
|
-
--blockquote-bg: ${blockquoteBg};
|
|
1227
|
-
--table-alt-bg: ${tableAltBg};
|
|
1228
|
-
--editor-bg: ${editorBg};
|
|
1261
|
+
${cssVarsBlock}
|
|
1229
1262
|
}
|
|
1230
1263
|
|
|
1231
1264
|
* { box-sizing: border-box; }
|
|
@@ -1243,7 +1276,7 @@ function buildStudioHtml(initialDocument: InitialStudioDocument | null, theme?:
|
|
|
1243
1276
|
min-height: 100%;
|
|
1244
1277
|
}
|
|
1245
1278
|
|
|
1246
|
-
header {
|
|
1279
|
+
body > header {
|
|
1247
1280
|
border-bottom: 1px solid var(--border-muted);
|
|
1248
1281
|
padding: 12px 16px;
|
|
1249
1282
|
background: var(--panel);
|
|
@@ -1387,6 +1420,20 @@ function buildStudioHtml(initialDocument: InitialStudioDocument | null, theme?:
|
|
|
1387
1420
|
font-size: 14px;
|
|
1388
1421
|
}
|
|
1389
1422
|
|
|
1423
|
+
.section-header select {
|
|
1424
|
+
font-weight: 600;
|
|
1425
|
+
font-size: 14px;
|
|
1426
|
+
border: none;
|
|
1427
|
+
border-radius: 4px;
|
|
1428
|
+
background: inherit;
|
|
1429
|
+
color: inherit;
|
|
1430
|
+
cursor: pointer;
|
|
1431
|
+
padding: 2px 4px;
|
|
1432
|
+
margin: 0;
|
|
1433
|
+
-webkit-appearance: menulist;
|
|
1434
|
+
appearance: menulist;
|
|
1435
|
+
}
|
|
1436
|
+
|
|
1390
1437
|
.reference-meta {
|
|
1391
1438
|
padding: 8px 10px;
|
|
1392
1439
|
border-bottom: 1px solid var(--border-muted);
|
|
@@ -1464,6 +1511,56 @@ function buildStudioHtml(initialDocument: InitialStudioDocument | null, theme?:
|
|
|
1464
1511
|
font-size: 12px;
|
|
1465
1512
|
}
|
|
1466
1513
|
|
|
1514
|
+
.resource-dir-btn {
|
|
1515
|
+
padding: 4px 10px;
|
|
1516
|
+
font-size: 12px;
|
|
1517
|
+
border: 1px solid var(--border-muted);
|
|
1518
|
+
border-radius: 999px;
|
|
1519
|
+
background: var(--card);
|
|
1520
|
+
color: var(--fg-muted);
|
|
1521
|
+
cursor: pointer;
|
|
1522
|
+
white-space: nowrap;
|
|
1523
|
+
font-family: inherit;
|
|
1524
|
+
}
|
|
1525
|
+
.resource-dir-btn:hover {
|
|
1526
|
+
color: var(--fg);
|
|
1527
|
+
border-color: var(--fg-muted);
|
|
1528
|
+
}
|
|
1529
|
+
.resource-dir-label {
|
|
1530
|
+
cursor: pointer;
|
|
1531
|
+
max-width: 300px;
|
|
1532
|
+
overflow: hidden;
|
|
1533
|
+
text-overflow: ellipsis;
|
|
1534
|
+
white-space: nowrap;
|
|
1535
|
+
}
|
|
1536
|
+
.resource-dir-label:hover {
|
|
1537
|
+
color: var(--fg);
|
|
1538
|
+
border-color: var(--fg-muted);
|
|
1539
|
+
}
|
|
1540
|
+
.resource-dir-input-wrap {
|
|
1541
|
+
display: none;
|
|
1542
|
+
gap: 3px;
|
|
1543
|
+
align-items: center;
|
|
1544
|
+
}
|
|
1545
|
+
.resource-dir-input-wrap.visible {
|
|
1546
|
+
display: inline-flex;
|
|
1547
|
+
}
|
|
1548
|
+
.resource-dir-input-wrap input[type="text"] {
|
|
1549
|
+
width: 260px;
|
|
1550
|
+
padding: 2px 6px;
|
|
1551
|
+
font-size: 11px;
|
|
1552
|
+
border: 1px solid var(--border-muted);
|
|
1553
|
+
border-radius: 4px;
|
|
1554
|
+
background: var(--editor-bg);
|
|
1555
|
+
color: var(--fg);
|
|
1556
|
+
font-family: var(--font-mono);
|
|
1557
|
+
}
|
|
1558
|
+
.resource-dir-input-wrap button {
|
|
1559
|
+
padding: 2px 6px;
|
|
1560
|
+
font-size: 11px;
|
|
1561
|
+
cursor: pointer;
|
|
1562
|
+
}
|
|
1563
|
+
|
|
1467
1564
|
.editor-highlight-wrap {
|
|
1468
1565
|
position: relative;
|
|
1469
1566
|
display: flex;
|
|
@@ -1556,6 +1653,20 @@ function buildStudioHtml(initialDocument: InitialStudioDocument | null, theme?:
|
|
|
1556
1653
|
color: var(--syntax-variable);
|
|
1557
1654
|
}
|
|
1558
1655
|
|
|
1656
|
+
.hl-diff-add {
|
|
1657
|
+
color: var(--ok);
|
|
1658
|
+
background: rgba(46, 160, 67, 0.12);
|
|
1659
|
+
display: inline-block;
|
|
1660
|
+
width: 100%;
|
|
1661
|
+
}
|
|
1662
|
+
|
|
1663
|
+
.hl-diff-del {
|
|
1664
|
+
color: var(--error);
|
|
1665
|
+
background: rgba(248, 81, 73, 0.12);
|
|
1666
|
+
display: inline-block;
|
|
1667
|
+
width: 100%;
|
|
1668
|
+
}
|
|
1669
|
+
|
|
1559
1670
|
.hl-list {
|
|
1560
1671
|
color: var(--md-list-bullet);
|
|
1561
1672
|
font-weight: 600;
|
|
@@ -1623,6 +1734,27 @@ function buildStudioHtml(initialDocument: InitialStudioDocument | null, theme?:
|
|
|
1623
1734
|
padding-bottom: 0;
|
|
1624
1735
|
}
|
|
1625
1736
|
|
|
1737
|
+
.rendered-markdown #title-block-header {
|
|
1738
|
+
text-align: center;
|
|
1739
|
+
margin-bottom: 2em;
|
|
1740
|
+
}
|
|
1741
|
+
.rendered-markdown #title-block-header .title {
|
|
1742
|
+
margin-bottom: 0.25em;
|
|
1743
|
+
}
|
|
1744
|
+
.rendered-markdown #title-block-header .author,
|
|
1745
|
+
.rendered-markdown #title-block-header .date {
|
|
1746
|
+
margin-bottom: 0.15em;
|
|
1747
|
+
color: var(--fg-muted);
|
|
1748
|
+
}
|
|
1749
|
+
.rendered-markdown #title-block-header .abstract {
|
|
1750
|
+
text-align: left;
|
|
1751
|
+
margin-top: 1em;
|
|
1752
|
+
}
|
|
1753
|
+
.rendered-markdown #title-block-header .abstract-title {
|
|
1754
|
+
font-weight: 600;
|
|
1755
|
+
margin-bottom: 0.25em;
|
|
1756
|
+
}
|
|
1757
|
+
|
|
1626
1758
|
.rendered-markdown p,
|
|
1627
1759
|
.rendered-markdown ul,
|
|
1628
1760
|
.rendered-markdown ol,
|
|
@@ -1741,6 +1873,29 @@ function buildStudioHtml(initialDocument: InitialStudioDocument | null, theme?:
|
|
|
1741
1873
|
font-weight: 600;
|
|
1742
1874
|
}
|
|
1743
1875
|
|
|
1876
|
+
/* Diff-specific overrides for pandoc code blocks */
|
|
1877
|
+
.rendered-markdown pre.sourceCode.diff code > span:has(> .va) {
|
|
1878
|
+
color: var(--ok);
|
|
1879
|
+
background: rgba(46, 160, 67, 0.12);
|
|
1880
|
+
}
|
|
1881
|
+
.rendered-markdown pre.sourceCode.diff code > span:has(> .st) {
|
|
1882
|
+
color: var(--error);
|
|
1883
|
+
background: rgba(248, 81, 73, 0.12);
|
|
1884
|
+
}
|
|
1885
|
+
.rendered-markdown pre.sourceCode.diff code > span:has(> .dt) {
|
|
1886
|
+
color: var(--syntax-function);
|
|
1887
|
+
}
|
|
1888
|
+
.rendered-markdown pre.sourceCode.diff code > span:has(> .kw) {
|
|
1889
|
+
color: var(--syntax-keyword);
|
|
1890
|
+
}
|
|
1891
|
+
.rendered-markdown pre.sourceCode.diff .va,
|
|
1892
|
+
.rendered-markdown pre.sourceCode.diff .st,
|
|
1893
|
+
.rendered-markdown pre.sourceCode.diff .dt,
|
|
1894
|
+
.rendered-markdown pre.sourceCode.diff .kw {
|
|
1895
|
+
color: inherit;
|
|
1896
|
+
font-weight: inherit;
|
|
1897
|
+
}
|
|
1898
|
+
|
|
1744
1899
|
.rendered-markdown table {
|
|
1745
1900
|
border-collapse: collapse;
|
|
1746
1901
|
display: block;
|
|
@@ -1905,30 +2060,32 @@ function buildStudioHtml(initialDocument: InitialStudioDocument | null, theme?:
|
|
|
1905
2060
|
</head>
|
|
1906
2061
|
<body data-initial-source="${initialSource}" data-initial-label="${initialLabel}" data-initial-path="${initialPath}">
|
|
1907
2062
|
<header>
|
|
1908
|
-
<h1><span class="app-logo" aria-hidden="true">π</span> Pi Studio <span class="app-subtitle">
|
|
2063
|
+
<h1><span class="app-logo" aria-hidden="true">π</span> Pi Studio <span class="app-subtitle">Editor & Response Workspace</span></h1>
|
|
1909
2064
|
<div class="controls">
|
|
1910
|
-
<
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
</select>
|
|
1914
|
-
<select id="rightViewSelect" aria-label="Response view mode">
|
|
1915
|
-
<option value="markdown">Right: Response (Raw)</option>
|
|
1916
|
-
<option value="preview" selected>Right: Response (Preview)</option>
|
|
1917
|
-
<option value="editor-preview">Right: Editor (Preview)</option>
|
|
1918
|
-
</select>
|
|
1919
|
-
<button id="saveAsBtn" type="button" title="Save editor text to a new file path.">Save As…</button>
|
|
1920
|
-
<button id="saveOverBtn" type="button" title="Overwrite current file with editor text." disabled>Save file</button>
|
|
1921
|
-
<label class="file-label" title="Load a local file into editor text.">Load file content<input id="fileInput" type="file" accept=".txt,.md,.markdown,.rst,.adoc,.tex,.json,.js,.ts,.py,.java,.c,.cpp,.h,.hpp,.go,.rs,.rb,.swift,.sh,.html,.css,.xml,.yaml,.yml,.toml,.jl,.f90,.f95,.f03,.f,.for,.r,.R,.m,.lua" /></label>
|
|
2065
|
+
<button id="saveAsBtn" type="button" title="Save editor content to a new file path.">Save editor as…</button>
|
|
2066
|
+
<button id="saveOverBtn" type="button" title="Overwrite current file with editor content." disabled>Save editor</button>
|
|
2067
|
+
<label class="file-label" title="Load a local file into editor text.">Load file content<input id="fileInput" type="file" accept=".md,.markdown,.mdx,.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>
|
|
1922
2068
|
</div>
|
|
1923
2069
|
</header>
|
|
1924
2070
|
|
|
1925
2071
|
<main>
|
|
1926
2072
|
<section id="leftPane">
|
|
1927
|
-
<div id="leftSectionHeader" class="section-header">
|
|
2073
|
+
<div id="leftSectionHeader" class="section-header">
|
|
2074
|
+
<select id="editorViewSelect" aria-label="Editor view mode">
|
|
2075
|
+
<option value="markdown" selected>Editor (Raw)</option>
|
|
2076
|
+
<option value="preview">Editor (Preview)</option>
|
|
2077
|
+
</select>
|
|
2078
|
+
</div>
|
|
1928
2079
|
<div class="source-wrap">
|
|
1929
2080
|
<div class="source-meta">
|
|
1930
2081
|
<div class="badge-row">
|
|
1931
2082
|
<span id="sourceBadge" class="source-badge">Editor origin: ${initialLabel}</span>
|
|
2083
|
+
<button id="resourceDirBtn" type="button" class="resource-dir-btn" hidden title="Set working directory for resolving relative paths in preview">Set working dir</button>
|
|
2084
|
+
<span id="resourceDirLabel" class="source-badge resource-dir-label" hidden title="Click to change working directory"></span>
|
|
2085
|
+
<span id="resourceDirInputWrap" class="resource-dir-input-wrap">
|
|
2086
|
+
<input id="resourceDirInput" type="text" placeholder="/path/to/working/directory" title="Absolute path to working directory" />
|
|
2087
|
+
<button id="resourceDirClearBtn" type="button" title="Clear working directory">✕</button>
|
|
2088
|
+
</span>
|
|
1932
2089
|
<span id="syncBadge" class="source-badge sync-badge">No response loaded</span>
|
|
1933
2090
|
</div>
|
|
1934
2091
|
<div class="source-actions">
|
|
@@ -1960,6 +2117,19 @@ function buildStudioHtml(initialDocument: InitialStudioDocument | null, theme?:
|
|
|
1960
2117
|
<option value="fortran">Lang: Fortran</option>
|
|
1961
2118
|
<option value="r">Lang: R</option>
|
|
1962
2119
|
<option value="matlab">Lang: MATLAB</option>
|
|
2120
|
+
<option value="latex">Lang: LaTeX</option>
|
|
2121
|
+
<option value="diff">Lang: Diff</option>
|
|
2122
|
+
<option value="java">Lang: Java</option>
|
|
2123
|
+
<option value="go">Lang: Go</option>
|
|
2124
|
+
<option value="ruby">Lang: Ruby</option>
|
|
2125
|
+
<option value="swift">Lang: Swift</option>
|
|
2126
|
+
<option value="html">Lang: HTML</option>
|
|
2127
|
+
<option value="css">Lang: CSS</option>
|
|
2128
|
+
<option value="xml">Lang: XML</option>
|
|
2129
|
+
<option value="yaml">Lang: YAML</option>
|
|
2130
|
+
<option value="toml">Lang: TOML</option>
|
|
2131
|
+
<option value="lua">Lang: Lua</option>
|
|
2132
|
+
<option value="text">Lang: Plain Text</option>
|
|
1963
2133
|
</select>
|
|
1964
2134
|
</div>
|
|
1965
2135
|
</div>
|
|
@@ -1972,7 +2142,13 @@ function buildStudioHtml(initialDocument: InitialStudioDocument | null, theme?:
|
|
|
1972
2142
|
</section>
|
|
1973
2143
|
|
|
1974
2144
|
<section id="rightPane">
|
|
1975
|
-
<div id="rightSectionHeader" class="section-header">
|
|
2145
|
+
<div id="rightSectionHeader" class="section-header">
|
|
2146
|
+
<select id="rightViewSelect" aria-label="Response view mode">
|
|
2147
|
+
<option value="markdown">Response (Raw)</option>
|
|
2148
|
+
<option value="preview" selected>Response (Preview)</option>
|
|
2149
|
+
<option value="editor-preview">Editor (Preview)</option>
|
|
2150
|
+
</select>
|
|
2151
|
+
</div>
|
|
1976
2152
|
<div class="reference-meta">
|
|
1977
2153
|
<span id="referenceBadge" class="source-badge">Latest response: none</span>
|
|
1978
2154
|
</div>
|
|
@@ -2034,11 +2210,9 @@ function buildStudioHtml(initialDocument: InitialStudioDocument | null, theme?:
|
|
|
2034
2210
|
const sourcePreviewEl = document.getElementById("sourcePreview");
|
|
2035
2211
|
const leftPaneEl = document.getElementById("leftPane");
|
|
2036
2212
|
const rightPaneEl = document.getElementById("rightPane");
|
|
2037
|
-
const leftSectionHeaderEl = document.getElementById("leftSectionHeader");
|
|
2038
2213
|
const sourceBadgeEl = document.getElementById("sourceBadge");
|
|
2039
2214
|
const syncBadgeEl = document.getElementById("syncBadge");
|
|
2040
2215
|
const critiqueViewEl = document.getElementById("critiqueView");
|
|
2041
|
-
const rightSectionHeaderEl = document.getElementById("rightSectionHeader");
|
|
2042
2216
|
const referenceBadgeEl = document.getElementById("referenceBadge");
|
|
2043
2217
|
const editorViewSelect = document.getElementById("editorViewSelect");
|
|
2044
2218
|
const rightViewSelect = document.getElementById("rightViewSelect");
|
|
@@ -2049,6 +2223,11 @@ function buildStudioHtml(initialDocument: InitialStudioDocument | null, theme?:
|
|
|
2049
2223
|
const critiqueBtn = document.getElementById("critiqueBtn");
|
|
2050
2224
|
const lensSelect = document.getElementById("lensSelect");
|
|
2051
2225
|
const fileInput = document.getElementById("fileInput");
|
|
2226
|
+
const resourceDirBtn = document.getElementById("resourceDirBtn");
|
|
2227
|
+
const resourceDirLabel = document.getElementById("resourceDirLabel");
|
|
2228
|
+
const resourceDirInputWrap = document.getElementById("resourceDirInputWrap");
|
|
2229
|
+
const resourceDirInput = document.getElementById("resourceDirInput");
|
|
2230
|
+
const resourceDirClearBtn = document.getElementById("resourceDirClearBtn");
|
|
2052
2231
|
const loadResponseBtn = document.getElementById("loadResponseBtn");
|
|
2053
2232
|
const loadCritiqueNotesBtn = document.getElementById("loadCritiqueNotesBtn");
|
|
2054
2233
|
const loadCritiqueFullBtn = document.getElementById("loadCritiqueFullBtn");
|
|
@@ -2093,7 +2272,44 @@ function buildStudioHtml(initialDocument: InitialStudioDocument | null, theme?:
|
|
|
2093
2272
|
const EDITOR_HIGHLIGHT_MAX_CHARS = 80_000;
|
|
2094
2273
|
const EDITOR_HIGHLIGHT_STORAGE_KEY = "piStudio.editorHighlightEnabled";
|
|
2095
2274
|
const EDITOR_LANGUAGE_STORAGE_KEY = "piStudio.editorLanguage";
|
|
2096
|
-
|
|
2275
|
+
// Single source of truth: language -> file extensions (and display label)
|
|
2276
|
+
var LANG_EXT_MAP = {
|
|
2277
|
+
markdown: { label: "Markdown", exts: ["md", "markdown", "mdx"] },
|
|
2278
|
+
javascript: { label: "JavaScript", exts: ["js", "mjs", "cjs", "jsx"] },
|
|
2279
|
+
typescript: { label: "TypeScript", exts: ["ts", "mts", "cts", "tsx"] },
|
|
2280
|
+
python: { label: "Python", exts: ["py", "pyw"] },
|
|
2281
|
+
bash: { label: "Bash", exts: ["sh", "bash", "zsh"] },
|
|
2282
|
+
json: { label: "JSON", exts: ["json", "jsonc", "json5"] },
|
|
2283
|
+
rust: { label: "Rust", exts: ["rs"] },
|
|
2284
|
+
c: { label: "C", exts: ["c", "h"] },
|
|
2285
|
+
cpp: { label: "C++", exts: ["cpp", "cxx", "cc", "hpp", "hxx"] },
|
|
2286
|
+
julia: { label: "Julia", exts: ["jl"] },
|
|
2287
|
+
fortran: { label: "Fortran", exts: ["f90", "f95", "f03", "f", "for"] },
|
|
2288
|
+
r: { label: "R", exts: ["r", "R"] },
|
|
2289
|
+
matlab: { label: "MATLAB", exts: ["m"] },
|
|
2290
|
+
latex: { label: "LaTeX", exts: ["tex", "latex"] },
|
|
2291
|
+
diff: { label: "Diff", exts: ["diff", "patch"] },
|
|
2292
|
+
// Languages accepted for upload/detect but without syntax highlighting
|
|
2293
|
+
java: { label: "Java", exts: ["java"] },
|
|
2294
|
+
go: { label: "Go", exts: ["go"] },
|
|
2295
|
+
ruby: { label: "Ruby", exts: ["rb"] },
|
|
2296
|
+
swift: { label: "Swift", exts: ["swift"] },
|
|
2297
|
+
html: { label: "HTML", exts: ["html", "htm"] },
|
|
2298
|
+
css: { label: "CSS", exts: ["css"] },
|
|
2299
|
+
xml: { label: "XML", exts: ["xml"] },
|
|
2300
|
+
yaml: { label: "YAML", exts: ["yaml", "yml"] },
|
|
2301
|
+
toml: { label: "TOML", exts: ["toml"] },
|
|
2302
|
+
lua: { label: "Lua", exts: ["lua"] },
|
|
2303
|
+
text: { label: "Plain Text", exts: ["txt", "rst", "adoc"] },
|
|
2304
|
+
};
|
|
2305
|
+
// Build reverse map: extension -> language
|
|
2306
|
+
var EXT_TO_LANG = {};
|
|
2307
|
+
Object.keys(LANG_EXT_MAP).forEach(function(lang) {
|
|
2308
|
+
LANG_EXT_MAP[lang].exts.forEach(function(ext) { EXT_TO_LANG[ext.toLowerCase()] = lang; });
|
|
2309
|
+
});
|
|
2310
|
+
// Languages that have syntax highlighting support
|
|
2311
|
+
var HIGHLIGHTED_LANGUAGES = ["markdown", "javascript", "typescript", "python", "bash", "json", "rust", "c", "cpp", "julia", "fortran", "r", "matlab", "latex"];
|
|
2312
|
+
var SUPPORTED_LANGUAGES = Object.keys(LANG_EXT_MAP);
|
|
2097
2313
|
const RESPONSE_HIGHLIGHT_MAX_CHARS = 120_000;
|
|
2098
2314
|
const RESPONSE_HIGHLIGHT_STORAGE_KEY = "piStudio.responseHighlightEnabled";
|
|
2099
2315
|
let sourcePreviewRenderTimer = null;
|
|
@@ -2137,6 +2353,27 @@ function buildStudioHtml(initialDocument: InitialStudioDocument | null, theme?:
|
|
|
2137
2353
|
function updateSourceBadge() {
|
|
2138
2354
|
const label = sourceState && sourceState.label ? sourceState.label : "blank";
|
|
2139
2355
|
sourceBadgeEl.textContent = "Editor origin: " + label;
|
|
2356
|
+
// Show "Set working dir" button when not file-backed
|
|
2357
|
+
var isFileBacked = sourceState.source === "file" && Boolean(sourceState.path);
|
|
2358
|
+
if (isFileBacked) {
|
|
2359
|
+
if (resourceDirInput) resourceDirInput.value = "";
|
|
2360
|
+
if (resourceDirLabel) resourceDirLabel.textContent = "";
|
|
2361
|
+
if (resourceDirBtn) resourceDirBtn.hidden = true;
|
|
2362
|
+
if (resourceDirLabel) resourceDirLabel.hidden = true;
|
|
2363
|
+
if (resourceDirInputWrap) resourceDirInputWrap.classList.remove("visible");
|
|
2364
|
+
} else {
|
|
2365
|
+
// Restore to label if dir is set, otherwise show button
|
|
2366
|
+
var dir = resourceDirInput ? resourceDirInput.value.trim() : "";
|
|
2367
|
+
if (dir) {
|
|
2368
|
+
if (resourceDirBtn) resourceDirBtn.hidden = true;
|
|
2369
|
+
if (resourceDirLabel) { resourceDirLabel.textContent = "Working dir: " + dir; resourceDirLabel.hidden = false; }
|
|
2370
|
+
if (resourceDirInputWrap) resourceDirInputWrap.classList.remove("visible");
|
|
2371
|
+
} else {
|
|
2372
|
+
if (resourceDirBtn) resourceDirBtn.hidden = false;
|
|
2373
|
+
if (resourceDirLabel) resourceDirLabel.hidden = true;
|
|
2374
|
+
if (resourceDirInputWrap) resourceDirInputWrap.classList.remove("visible");
|
|
2375
|
+
}
|
|
2376
|
+
}
|
|
2140
2377
|
}
|
|
2141
2378
|
|
|
2142
2379
|
function applyPaneFocusClasses() {
|
|
@@ -2346,6 +2583,15 @@ function buildStudioHtml(initialDocument: InitialStudioDocument | null, theme?:
|
|
|
2346
2583
|
targetEl.appendChild(warningEl);
|
|
2347
2584
|
}
|
|
2348
2585
|
|
|
2586
|
+
function appendPreviewNotice(targetEl, message) {
|
|
2587
|
+
if (!targetEl || typeof targetEl.querySelector !== "function" || typeof targetEl.appendChild !== "function") return;
|
|
2588
|
+
if (targetEl.querySelector(".preview-image-warning")) return;
|
|
2589
|
+
const el = document.createElement("div");
|
|
2590
|
+
el.className = "preview-warning preview-image-warning";
|
|
2591
|
+
el.textContent = String(message || "");
|
|
2592
|
+
targetEl.appendChild(el);
|
|
2593
|
+
}
|
|
2594
|
+
|
|
2349
2595
|
async function getMermaidApi() {
|
|
2350
2596
|
if (mermaidModulePromise) {
|
|
2351
2597
|
return mermaidModulePromise;
|
|
@@ -2438,7 +2684,11 @@ function buildStudioHtml(initialDocument: InitialStudioDocument | null, theme?:
|
|
|
2438
2684
|
headers: {
|
|
2439
2685
|
"Content-Type": "application/json",
|
|
2440
2686
|
},
|
|
2441
|
-
body: JSON.stringify({
|
|
2687
|
+
body: JSON.stringify({
|
|
2688
|
+
markdown: String(markdown || ""),
|
|
2689
|
+
sourcePath: sourceState.path || "",
|
|
2690
|
+
resourceDir: (!sourceState.path && resourceDirInput) ? resourceDirInput.value.trim() : "",
|
|
2691
|
+
}),
|
|
2442
2692
|
signal: controller ? controller.signal : undefined,
|
|
2443
2693
|
});
|
|
2444
2694
|
} catch (error) {
|
|
@@ -2489,6 +2739,15 @@ function buildStudioHtml(initialDocument: InitialStudioDocument | null, theme?:
|
|
|
2489
2739
|
|
|
2490
2740
|
targetEl.innerHTML = sanitizeRenderedHtml(renderedHtml, markdown);
|
|
2491
2741
|
await renderMermaidInElement(targetEl);
|
|
2742
|
+
|
|
2743
|
+
// Warn if relative images are present but unlikely to resolve (non-file-backed content)
|
|
2744
|
+
if (!sourceState.path && !(resourceDirInput && resourceDirInput.value.trim())) {
|
|
2745
|
+
var hasRelativeImages = /!\\[.*?\\]\\((?!https?:\\/\\/|data:)[^)]+\\)/.test(markdown || "");
|
|
2746
|
+
var hasLatexImages = /\\\\includegraphics/.test(markdown || "");
|
|
2747
|
+
if (hasRelativeImages || hasLatexImages) {
|
|
2748
|
+
appendPreviewNotice(targetEl, "Images not displaying? Set working dir in the editor pane or open via /studio <path>.");
|
|
2749
|
+
}
|
|
2750
|
+
}
|
|
2492
2751
|
} catch (error) {
|
|
2493
2752
|
if (pane === "source") {
|
|
2494
2753
|
if (nonce !== sourcePreviewRenderNonce || editorView !== "preview") return;
|
|
@@ -2504,7 +2763,7 @@ function buildStudioHtml(initialDocument: InitialStudioDocument | null, theme?:
|
|
|
2504
2763
|
function renderSourcePreviewNow() {
|
|
2505
2764
|
if (editorView !== "preview") return;
|
|
2506
2765
|
const text = sourceTextEl.value || "";
|
|
2507
|
-
if (editorLanguage && editorLanguage !== "markdown") {
|
|
2766
|
+
if (editorLanguage && editorLanguage !== "markdown" && editorLanguage !== "latex") {
|
|
2508
2767
|
sourcePreviewEl.innerHTML = "<div class='response-markdown-highlight' style='white-space:pre;font-family:var(--font-mono);font-size:13px;line-height:1.5;padding:16px;overflow:auto;'>" + highlightCode(text, editorLanguage) + "</div>";
|
|
2509
2768
|
return;
|
|
2510
2769
|
}
|
|
@@ -2562,7 +2821,7 @@ function buildStudioHtml(initialDocument: InitialStudioDocument | null, theme?:
|
|
|
2562
2821
|
critiqueViewEl.innerHTML = "<pre class='plain-markdown'>Editor is empty.</pre>";
|
|
2563
2822
|
return;
|
|
2564
2823
|
}
|
|
2565
|
-
if (editorLanguage && editorLanguage !== "markdown") {
|
|
2824
|
+
if (editorLanguage && editorLanguage !== "markdown" && editorLanguage !== "latex") {
|
|
2566
2825
|
critiqueViewEl.innerHTML = "<div class='response-markdown-highlight' style='white-space:pre;font-family:var(--font-mono);font-size:13px;line-height:1.5;padding:16px;overflow:auto;'>" + highlightCode(editorText, editorLanguage) + "</div>";
|
|
2567
2826
|
return;
|
|
2568
2827
|
}
|
|
@@ -2632,35 +2891,37 @@ function buildStudioHtml(initialDocument: InitialStudioDocument | null, theme?:
|
|
|
2632
2891
|
}
|
|
2633
2892
|
|
|
2634
2893
|
function refreshResponseUi() {
|
|
2635
|
-
if (leftSectionHeaderEl) {
|
|
2636
|
-
leftSectionHeaderEl.textContent = "Editor";
|
|
2637
|
-
}
|
|
2638
|
-
|
|
2639
|
-
if (rightSectionHeaderEl) {
|
|
2640
|
-
rightSectionHeaderEl.textContent = rightView === "editor-preview" ? "Editor Preview" : "Response";
|
|
2641
|
-
}
|
|
2642
|
-
|
|
2643
2894
|
updateSourceBadge();
|
|
2644
2895
|
updateReferenceBadge();
|
|
2645
2896
|
renderActiveResult();
|
|
2646
2897
|
updateResultActionButtons();
|
|
2647
2898
|
}
|
|
2648
2899
|
|
|
2900
|
+
function getEffectiveSavePath() {
|
|
2901
|
+
// File-backed: use the original path
|
|
2902
|
+
if (sourceState.source === "file" && sourceState.path) return sourceState.path;
|
|
2903
|
+
// Upload with working dir + filename: derive path
|
|
2904
|
+
if (sourceState.source === "upload" && sourceState.label && resourceDirInput && resourceDirInput.value.trim()) {
|
|
2905
|
+
var name = sourceState.label.replace(/^upload:\\s*/i, "");
|
|
2906
|
+
if (name) return resourceDirInput.value.trim().replace(/\\/$/, "") + "/" + name;
|
|
2907
|
+
}
|
|
2908
|
+
return null;
|
|
2909
|
+
}
|
|
2910
|
+
|
|
2649
2911
|
function updateSaveFileTooltip() {
|
|
2650
2912
|
if (!saveOverBtn) return;
|
|
2651
2913
|
|
|
2652
|
-
|
|
2653
|
-
if (
|
|
2654
|
-
|
|
2655
|
-
saveOverBtn.title = "Overwrite current file: " + target;
|
|
2914
|
+
var effectivePath = getEffectiveSavePath();
|
|
2915
|
+
if (effectivePath) {
|
|
2916
|
+
saveOverBtn.title = "Overwrite file: " + effectivePath;
|
|
2656
2917
|
return;
|
|
2657
2918
|
}
|
|
2658
2919
|
|
|
2659
|
-
saveOverBtn.title = "Save
|
|
2920
|
+
saveOverBtn.title = "Save editor is available after opening a file, setting a working dir, or using Save editor as…";
|
|
2660
2921
|
}
|
|
2661
2922
|
|
|
2662
2923
|
function syncActionButtons() {
|
|
2663
|
-
const canSaveOver =
|
|
2924
|
+
const canSaveOver = Boolean(getEffectiveSavePath());
|
|
2664
2925
|
|
|
2665
2926
|
fileInput.disabled = uiBusy;
|
|
2666
2927
|
saveAsBtn.disabled = uiBusy;
|
|
@@ -2803,6 +3064,7 @@ function buildStudioHtml(initialDocument: InitialStudioDocument | null, theme?:
|
|
|
2803
3064
|
|
|
2804
3065
|
const first = raw.split(/\\s+/)[0].replace(/^\\./, "").toLowerCase();
|
|
2805
3066
|
|
|
3067
|
+
// Explicit aliases that don't match extension names
|
|
2806
3068
|
if (first === "js" || first === "javascript" || first === "jsx" || first === "node") return "javascript";
|
|
2807
3069
|
if (first === "ts" || first === "typescript" || first === "tsx") return "typescript";
|
|
2808
3070
|
if (first === "py" || first === "python") return "python";
|
|
@@ -2815,8 +3077,11 @@ function buildStudioHtml(initialDocument: InitialStudioDocument | null, theme?:
|
|
|
2815
3077
|
if (first === "fortran" || first === "f90" || first === "f95" || first === "f03" || first === "f" || first === "for") return "fortran";
|
|
2816
3078
|
if (first === "r") return "r";
|
|
2817
3079
|
if (first === "matlab" || first === "m") return "matlab";
|
|
3080
|
+
if (first === "latex" || first === "tex") return "latex";
|
|
3081
|
+
if (first === "diff" || first === "patch" || first === "udiff") return "diff";
|
|
2818
3082
|
|
|
2819
|
-
|
|
3083
|
+
// Fall back to the unified extension->language map
|
|
3084
|
+
return EXT_TO_LANG[first] || "";
|
|
2820
3085
|
}
|
|
2821
3086
|
|
|
2822
3087
|
function highlightCodeTokens(line, pattern, classifyMatch) {
|
|
@@ -2981,6 +3246,31 @@ function buildStudioHtml(initialDocument: InitialStudioDocument | null, theme?:
|
|
|
2981
3246
|
return "<span class='hl-code'>" + highlighted + "</span>";
|
|
2982
3247
|
}
|
|
2983
3248
|
|
|
3249
|
+
if (lang === "latex") {
|
|
3250
|
+
const texPattern = /(%.*$)|(\\\\(?:documentclass|usepackage|newtheorem|begin|end|section|subsection|subsubsection|chapter|part|title|author|date|maketitle|tableofcontents|includegraphics|caption|label|ref|eqref|cite|textbf|textit|texttt|emph|footnote|centering|newcommand|renewcommand|providecommand|bibliography|bibliographystyle|bibitem|item|input|include)\\b)|(\\\\[A-Za-z]+)|(\\{|\\})|(\\$\\$?(?:[^$\\\\]|\\\\.)+\\$\\$?)|(\\[(?:.*?)\\])/g;
|
|
3251
|
+
const highlighted = highlightCodeTokens(source, texPattern, (match) => {
|
|
3252
|
+
if (match[1]) return "hl-code-com";
|
|
3253
|
+
if (match[2]) return "hl-code-kw";
|
|
3254
|
+
if (match[3]) return "hl-code-fn";
|
|
3255
|
+
if (match[4]) return "hl-code-op";
|
|
3256
|
+
if (match[5]) return "hl-code-str";
|
|
3257
|
+
if (match[6]) return "hl-code-num";
|
|
3258
|
+
return "hl-code";
|
|
3259
|
+
});
|
|
3260
|
+
return highlighted;
|
|
3261
|
+
}
|
|
3262
|
+
|
|
3263
|
+
if (lang === "diff") {
|
|
3264
|
+
var escaped = escapeHtml(source);
|
|
3265
|
+
if (/^@@/.test(source)) return "<span class=\\"hl-code-fn\\">" + escaped + "</span>";
|
|
3266
|
+
if (/^\\+\\+\\+|^---/.test(source)) return "<span class=\\"hl-code-kw\\">" + escaped + "</span>";
|
|
3267
|
+
if (/^\\+/.test(source)) return "<span class=\\"hl-diff-add\\">" + escaped + "</span>";
|
|
3268
|
+
if (/^-/.test(source)) return "<span class=\\"hl-diff-del\\">" + escaped + "</span>";
|
|
3269
|
+
if (/^diff /.test(source)) return "<span class=\\"hl-code-kw\\">" + escaped + "</span>";
|
|
3270
|
+
if (/^index /.test(source)) return "<span class=\\"hl-code-com\\">" + escaped + "</span>";
|
|
3271
|
+
return escaped;
|
|
3272
|
+
}
|
|
3273
|
+
|
|
2984
3274
|
return wrapHighlight("hl-code", source);
|
|
2985
3275
|
}
|
|
2986
3276
|
|
|
@@ -3067,23 +3357,10 @@ function buildStudioHtml(initialDocument: InitialStudioDocument | null, theme?:
|
|
|
3067
3357
|
|
|
3068
3358
|
function detectLanguageFromName(name) {
|
|
3069
3359
|
if (!name) return "";
|
|
3070
|
-
|
|
3360
|
+
var dot = name.lastIndexOf(".");
|
|
3071
3361
|
if (dot < 0) return "";
|
|
3072
|
-
|
|
3073
|
-
|
|
3074
|
-
if (ext === "ts" || ext === "mts" || ext === "cts" || ext === "tsx") return "typescript";
|
|
3075
|
-
if (ext === "py" || ext === "pyw") return "python";
|
|
3076
|
-
if (ext === "sh" || ext === "bash" || ext === "zsh") return "bash";
|
|
3077
|
-
if (ext === "json" || ext === "jsonc" || ext === "json5") return "json";
|
|
3078
|
-
if (ext === "rs") return "rust";
|
|
3079
|
-
if (ext === "c" || ext === "h") return "c";
|
|
3080
|
-
if (ext === "cpp" || ext === "cxx" || ext === "cc" || ext === "hpp" || ext === "hxx") return "cpp";
|
|
3081
|
-
if (ext === "jl") return "julia";
|
|
3082
|
-
if (ext === "f90" || ext === "f95" || ext === "f03" || ext === "f" || ext === "for") return "fortran";
|
|
3083
|
-
if (ext === "r" || ext === "R") return "r";
|
|
3084
|
-
if (ext === "m") return "matlab";
|
|
3085
|
-
if (ext === "md" || ext === "markdown" || ext === "mdx") return "markdown";
|
|
3086
|
-
return "";
|
|
3362
|
+
var ext = name.slice(dot + 1).toLowerCase();
|
|
3363
|
+
return EXT_TO_LANG[ext] || "";
|
|
3087
3364
|
}
|
|
3088
3365
|
|
|
3089
3366
|
function renderEditorHighlightNow() {
|
|
@@ -3527,6 +3804,17 @@ function buildStudioHtml(initialDocument: InitialStudioDocument | null, theme?:
|
|
|
3527
3804
|
setStatus(message.message);
|
|
3528
3805
|
}
|
|
3529
3806
|
}
|
|
3807
|
+
|
|
3808
|
+
if (message.type === "theme_update" && message.vars && typeof message.vars === "object") {
|
|
3809
|
+
var root = document.documentElement;
|
|
3810
|
+
Object.keys(message.vars).forEach(function(key) {
|
|
3811
|
+
if (key === "color-scheme") {
|
|
3812
|
+
root.style.colorScheme = message.vars[key];
|
|
3813
|
+
} else {
|
|
3814
|
+
root.style.setProperty(key, message.vars[key]);
|
|
3815
|
+
}
|
|
3816
|
+
});
|
|
3817
|
+
}
|
|
3530
3818
|
}
|
|
3531
3819
|
|
|
3532
3820
|
function connect() {
|
|
@@ -3827,8 +4115,10 @@ function buildStudioHtml(initialDocument: InitialStudioDocument | null, theme?:
|
|
|
3827
4115
|
return;
|
|
3828
4116
|
}
|
|
3829
4117
|
|
|
3830
|
-
|
|
3831
|
-
|
|
4118
|
+
var suggestedName = sourceState.label ? sourceState.label.replace(/^upload:\\s*/i, "") : "draft.md";
|
|
4119
|
+
var suggestedDir = resourceDirInput && resourceDirInput.value.trim() ? resourceDirInput.value.trim().replace(/\\/$/, "") + "/" : "./";
|
|
4120
|
+
const suggested = sourceState.path || (suggestedDir + suggestedName);
|
|
4121
|
+
const path = window.prompt("Save editor content as:", suggested);
|
|
3832
4122
|
if (!path) return;
|
|
3833
4123
|
|
|
3834
4124
|
const requestId = beginUiAction("save_as");
|
|
@@ -3849,21 +4139,24 @@ function buildStudioHtml(initialDocument: InitialStudioDocument | null, theme?:
|
|
|
3849
4139
|
});
|
|
3850
4140
|
|
|
3851
4141
|
saveOverBtn.addEventListener("click", () => {
|
|
3852
|
-
|
|
3853
|
-
|
|
4142
|
+
var effectivePath = getEffectiveSavePath();
|
|
4143
|
+
if (!effectivePath) {
|
|
4144
|
+
setStatus("Save editor requires a file path. Open via /studio <path>, set a working dir, or use Save editor as…", "warning");
|
|
3854
4145
|
return;
|
|
3855
4146
|
}
|
|
3856
4147
|
|
|
3857
|
-
if (!window.confirm("Overwrite " +
|
|
4148
|
+
if (!window.confirm("Overwrite " + effectivePath + "?")) {
|
|
3858
4149
|
return;
|
|
3859
4150
|
}
|
|
3860
4151
|
|
|
3861
4152
|
const requestId = beginUiAction("save_over");
|
|
3862
4153
|
if (!requestId) return;
|
|
3863
4154
|
|
|
4155
|
+
// Use save_as with the effective path for both file-backed and derived paths
|
|
3864
4156
|
const sent = sendMessage({
|
|
3865
|
-
type: "
|
|
4157
|
+
type: "save_as_request",
|
|
3866
4158
|
requestId,
|
|
4159
|
+
path: effectivePath,
|
|
3867
4160
|
content: sourceTextEl.value,
|
|
3868
4161
|
});
|
|
3869
4162
|
|
|
@@ -3935,6 +4228,67 @@ function buildStudioHtml(initialDocument: InitialStudioDocument | null, theme?:
|
|
|
3935
4228
|
}
|
|
3936
4229
|
});
|
|
3937
4230
|
|
|
4231
|
+
// Working directory controls — three states: button | input | label
|
|
4232
|
+
function showResourceDirState(state) {
|
|
4233
|
+
// state: "button" | "input" | "label"
|
|
4234
|
+
if (resourceDirBtn) resourceDirBtn.hidden = state !== "button";
|
|
4235
|
+
if (resourceDirInputWrap) {
|
|
4236
|
+
if (state === "input") resourceDirInputWrap.classList.add("visible");
|
|
4237
|
+
else resourceDirInputWrap.classList.remove("visible");
|
|
4238
|
+
}
|
|
4239
|
+
if (resourceDirLabel) resourceDirLabel.hidden = state !== "label";
|
|
4240
|
+
}
|
|
4241
|
+
function applyResourceDir() {
|
|
4242
|
+
var dir = resourceDirInput ? resourceDirInput.value.trim() : "";
|
|
4243
|
+
if (dir) {
|
|
4244
|
+
if (resourceDirLabel) resourceDirLabel.textContent = "Working dir: " + dir;
|
|
4245
|
+
showResourceDirState("label");
|
|
4246
|
+
} else {
|
|
4247
|
+
showResourceDirState("button");
|
|
4248
|
+
}
|
|
4249
|
+
updateSaveFileTooltip();
|
|
4250
|
+
syncActionButtons();
|
|
4251
|
+
renderSourcePreview();
|
|
4252
|
+
}
|
|
4253
|
+
if (resourceDirBtn) {
|
|
4254
|
+
resourceDirBtn.addEventListener("click", () => {
|
|
4255
|
+
showResourceDirState("input");
|
|
4256
|
+
if (resourceDirInput) resourceDirInput.focus();
|
|
4257
|
+
});
|
|
4258
|
+
}
|
|
4259
|
+
if (resourceDirLabel) {
|
|
4260
|
+
resourceDirLabel.addEventListener("click", () => {
|
|
4261
|
+
showResourceDirState("input");
|
|
4262
|
+
if (resourceDirInput) resourceDirInput.focus();
|
|
4263
|
+
});
|
|
4264
|
+
}
|
|
4265
|
+
if (resourceDirInput) {
|
|
4266
|
+
resourceDirInput.addEventListener("keydown", (e) => {
|
|
4267
|
+
if (e.key === "Enter") {
|
|
4268
|
+
e.preventDefault();
|
|
4269
|
+
applyResourceDir();
|
|
4270
|
+
} else if (e.key === "Escape") {
|
|
4271
|
+
e.preventDefault();
|
|
4272
|
+
var dir = resourceDirInput.value.trim();
|
|
4273
|
+
if (dir) {
|
|
4274
|
+
showResourceDirState("label");
|
|
4275
|
+
} else {
|
|
4276
|
+
showResourceDirState("button");
|
|
4277
|
+
}
|
|
4278
|
+
}
|
|
4279
|
+
});
|
|
4280
|
+
}
|
|
4281
|
+
if (resourceDirClearBtn) {
|
|
4282
|
+
resourceDirClearBtn.addEventListener("click", () => {
|
|
4283
|
+
if (resourceDirInput) resourceDirInput.value = "";
|
|
4284
|
+
if (resourceDirLabel) resourceDirLabel.textContent = "";
|
|
4285
|
+
showResourceDirState("button");
|
|
4286
|
+
updateSaveFileTooltip();
|
|
4287
|
+
syncActionButtons();
|
|
4288
|
+
renderSourcePreview();
|
|
4289
|
+
});
|
|
4290
|
+
}
|
|
4291
|
+
|
|
3938
4292
|
fileInput.addEventListener("change", () => {
|
|
3939
4293
|
const file = fileInput.files && fileInput.files[0];
|
|
3940
4294
|
if (!file) return;
|
|
@@ -3998,6 +4352,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
3998
4352
|
let initialStudioDocument: InitialStudioDocument | null = null;
|
|
3999
4353
|
let studioCwd = process.cwd();
|
|
4000
4354
|
let lastCommandCtx: ExtensionCommandContext | null = null;
|
|
4355
|
+
let lastThemeVarsJson = "";
|
|
4001
4356
|
let agentBusy = false;
|
|
4002
4357
|
|
|
4003
4358
|
const isStudioBusy = () => agentBusy || activeRequest !== null;
|
|
@@ -4369,7 +4724,17 @@ export default function (pi: ExtensionAPI) {
|
|
|
4369
4724
|
}
|
|
4370
4725
|
|
|
4371
4726
|
try {
|
|
4372
|
-
const
|
|
4727
|
+
const sourcePath =
|
|
4728
|
+
parsedBody && typeof parsedBody === "object" && typeof (parsedBody as { sourcePath?: unknown }).sourcePath === "string"
|
|
4729
|
+
? (parsedBody as { sourcePath: string }).sourcePath
|
|
4730
|
+
: "";
|
|
4731
|
+
const userResourceDir =
|
|
4732
|
+
parsedBody && typeof parsedBody === "object" && typeof (parsedBody as { resourceDir?: unknown }).resourceDir === "string"
|
|
4733
|
+
? (parsedBody as { resourceDir: string }).resourceDir
|
|
4734
|
+
: "";
|
|
4735
|
+
const resourcePath = sourcePath ? dirname(sourcePath) : (userResourceDir || studioCwd || undefined);
|
|
4736
|
+
const isLatex = /\\documentclass\b|\\begin\{document\}/.test(markdown);
|
|
4737
|
+
const html = await renderStudioMarkdownWithPandoc(markdown, isLatex, resourcePath);
|
|
4373
4738
|
respondJson(res, 200, { ok: true, html, renderer: "pandoc" });
|
|
4374
4739
|
} catch (error) {
|
|
4375
4740
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -4536,6 +4901,27 @@ export default function (pi: ExtensionAPI) {
|
|
|
4536
4901
|
state.port = address.port;
|
|
4537
4902
|
|
|
4538
4903
|
serverState = state;
|
|
4904
|
+
|
|
4905
|
+
// Periodically check for theme changes and push to all clients
|
|
4906
|
+
const themeCheckInterval = setInterval(() => {
|
|
4907
|
+
if (!lastCommandCtx?.ui?.theme || !serverState || serverState.clients.size === 0) return;
|
|
4908
|
+
try {
|
|
4909
|
+
const style = getStudioThemeStyle(lastCommandCtx.ui.theme);
|
|
4910
|
+
const vars = buildThemeCssVars(style);
|
|
4911
|
+
const json = JSON.stringify(vars);
|
|
4912
|
+
if (json !== lastThemeVarsJson) {
|
|
4913
|
+
lastThemeVarsJson = json;
|
|
4914
|
+
for (const client of serverState.clients) {
|
|
4915
|
+
sendToClient(client, { type: "theme_update", vars });
|
|
4916
|
+
}
|
|
4917
|
+
}
|
|
4918
|
+
} catch {
|
|
4919
|
+
// Ignore theme read errors
|
|
4920
|
+
}
|
|
4921
|
+
}, 2000);
|
|
4922
|
+
// Clean up interval if server closes
|
|
4923
|
+
server.once("close", () => clearInterval(themeCheckInterval));
|
|
4924
|
+
|
|
4539
4925
|
return state;
|
|
4540
4926
|
};
|
|
4541
4927
|
|
|
@@ -4684,6 +5070,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
4684
5070
|
await ctx.waitForIdle();
|
|
4685
5071
|
lastCommandCtx = ctx;
|
|
4686
5072
|
studioCwd = ctx.cwd;
|
|
5073
|
+
// Seed theme vars so first ping doesn't trigger a false update
|
|
5074
|
+
try {
|
|
5075
|
+
const currentStyle = getStudioThemeStyle(ctx.ui.theme);
|
|
5076
|
+
lastThemeVarsJson = JSON.stringify(buildThemeCssVars(currentStyle));
|
|
5077
|
+
} catch { /* ignore */ }
|
|
4687
5078
|
|
|
4688
5079
|
const latestAssistant =
|
|
4689
5080
|
extractLatestAssistantFromEntries(ctx.sessionManager.getBranch())
|