pi-studio 0.3.0 → 0.4.1
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 +7 -0
- package/README.md +12 -6
- package/index.ts +586 -152
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to `pi-studio` are documented here.
|
|
4
4
|
|
|
5
|
+
## [0.4.1] — 2026-03-03
|
|
6
|
+
|
|
7
|
+
### Changed
|
|
8
|
+
- Editor input keeps preview refreshes immediate (no added typing debounce) while keeping editor syntax highlighting immediate in Raw view.
|
|
9
|
+
- Response/sync state checks now reuse cached normalized response data and critique-note extracts instead of recomputing on each keystroke.
|
|
10
|
+
- Editor action/sync UI updates are now coalesced with `requestAnimationFrame` during typing.
|
|
11
|
+
|
|
5
12
|
## [0.3.0] — 2026-03-02
|
|
6
13
|
|
|
7
14
|
### Added
|
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");
|
|
@@ -2082,6 +2261,10 @@ function buildStudioHtml(initialDocument: InitialStudioDocument | null, theme?:
|
|
|
2082
2261
|
let latestResponseTimestamp = 0;
|
|
2083
2262
|
let latestResponseKind = "annotation";
|
|
2084
2263
|
let latestResponseIsStructuredCritique = false;
|
|
2264
|
+
let latestResponseHasContent = false;
|
|
2265
|
+
let latestResponseNormalized = "";
|
|
2266
|
+
let latestCritiqueNotes = "";
|
|
2267
|
+
let latestCritiqueNotesNormalized = "";
|
|
2085
2268
|
let uiBusy = false;
|
|
2086
2269
|
let sourceState = {
|
|
2087
2270
|
source: initialSourceState.source,
|
|
@@ -2093,13 +2276,52 @@ function buildStudioHtml(initialDocument: InitialStudioDocument | null, theme?:
|
|
|
2093
2276
|
const EDITOR_HIGHLIGHT_MAX_CHARS = 80_000;
|
|
2094
2277
|
const EDITOR_HIGHLIGHT_STORAGE_KEY = "piStudio.editorHighlightEnabled";
|
|
2095
2278
|
const EDITOR_LANGUAGE_STORAGE_KEY = "piStudio.editorLanguage";
|
|
2096
|
-
|
|
2279
|
+
// Single source of truth: language -> file extensions (and display label)
|
|
2280
|
+
var LANG_EXT_MAP = {
|
|
2281
|
+
markdown: { label: "Markdown", exts: ["md", "markdown", "mdx"] },
|
|
2282
|
+
javascript: { label: "JavaScript", exts: ["js", "mjs", "cjs", "jsx"] },
|
|
2283
|
+
typescript: { label: "TypeScript", exts: ["ts", "mts", "cts", "tsx"] },
|
|
2284
|
+
python: { label: "Python", exts: ["py", "pyw"] },
|
|
2285
|
+
bash: { label: "Bash", exts: ["sh", "bash", "zsh"] },
|
|
2286
|
+
json: { label: "JSON", exts: ["json", "jsonc", "json5"] },
|
|
2287
|
+
rust: { label: "Rust", exts: ["rs"] },
|
|
2288
|
+
c: { label: "C", exts: ["c", "h"] },
|
|
2289
|
+
cpp: { label: "C++", exts: ["cpp", "cxx", "cc", "hpp", "hxx"] },
|
|
2290
|
+
julia: { label: "Julia", exts: ["jl"] },
|
|
2291
|
+
fortran: { label: "Fortran", exts: ["f90", "f95", "f03", "f", "for"] },
|
|
2292
|
+
r: { label: "R", exts: ["r", "R"] },
|
|
2293
|
+
matlab: { label: "MATLAB", exts: ["m"] },
|
|
2294
|
+
latex: { label: "LaTeX", exts: ["tex", "latex"] },
|
|
2295
|
+
diff: { label: "Diff", exts: ["diff", "patch"] },
|
|
2296
|
+
// Languages accepted for upload/detect but without syntax highlighting
|
|
2297
|
+
java: { label: "Java", exts: ["java"] },
|
|
2298
|
+
go: { label: "Go", exts: ["go"] },
|
|
2299
|
+
ruby: { label: "Ruby", exts: ["rb"] },
|
|
2300
|
+
swift: { label: "Swift", exts: ["swift"] },
|
|
2301
|
+
html: { label: "HTML", exts: ["html", "htm"] },
|
|
2302
|
+
css: { label: "CSS", exts: ["css"] },
|
|
2303
|
+
xml: { label: "XML", exts: ["xml"] },
|
|
2304
|
+
yaml: { label: "YAML", exts: ["yaml", "yml"] },
|
|
2305
|
+
toml: { label: "TOML", exts: ["toml"] },
|
|
2306
|
+
lua: { label: "Lua", exts: ["lua"] },
|
|
2307
|
+
text: { label: "Plain Text", exts: ["txt", "rst", "adoc"] },
|
|
2308
|
+
};
|
|
2309
|
+
// Build reverse map: extension -> language
|
|
2310
|
+
var EXT_TO_LANG = {};
|
|
2311
|
+
Object.keys(LANG_EXT_MAP).forEach(function(lang) {
|
|
2312
|
+
LANG_EXT_MAP[lang].exts.forEach(function(ext) { EXT_TO_LANG[ext.toLowerCase()] = lang; });
|
|
2313
|
+
});
|
|
2314
|
+
// Languages that have syntax highlighting support
|
|
2315
|
+
var HIGHLIGHTED_LANGUAGES = ["markdown", "javascript", "typescript", "python", "bash", "json", "rust", "c", "cpp", "julia", "fortran", "r", "matlab", "latex"];
|
|
2316
|
+
var SUPPORTED_LANGUAGES = Object.keys(LANG_EXT_MAP);
|
|
2097
2317
|
const RESPONSE_HIGHLIGHT_MAX_CHARS = 120_000;
|
|
2098
2318
|
const RESPONSE_HIGHLIGHT_STORAGE_KEY = "piStudio.responseHighlightEnabled";
|
|
2319
|
+
const PREVIEW_INPUT_DEBOUNCE_MS = 0;
|
|
2099
2320
|
let sourcePreviewRenderTimer = null;
|
|
2100
2321
|
let sourcePreviewRenderNonce = 0;
|
|
2101
2322
|
let responsePreviewRenderNonce = 0;
|
|
2102
2323
|
let responseEditorPreviewTimer = null;
|
|
2324
|
+
let editorMetaUpdateRaf = null;
|
|
2103
2325
|
let editorHighlightEnabled = false;
|
|
2104
2326
|
let editorLanguage = "markdown";
|
|
2105
2327
|
let responseHighlightEnabled = false;
|
|
@@ -2137,6 +2359,27 @@ function buildStudioHtml(initialDocument: InitialStudioDocument | null, theme?:
|
|
|
2137
2359
|
function updateSourceBadge() {
|
|
2138
2360
|
const label = sourceState && sourceState.label ? sourceState.label : "blank";
|
|
2139
2361
|
sourceBadgeEl.textContent = "Editor origin: " + label;
|
|
2362
|
+
// Show "Set working dir" button when not file-backed
|
|
2363
|
+
var isFileBacked = sourceState.source === "file" && Boolean(sourceState.path);
|
|
2364
|
+
if (isFileBacked) {
|
|
2365
|
+
if (resourceDirInput) resourceDirInput.value = "";
|
|
2366
|
+
if (resourceDirLabel) resourceDirLabel.textContent = "";
|
|
2367
|
+
if (resourceDirBtn) resourceDirBtn.hidden = true;
|
|
2368
|
+
if (resourceDirLabel) resourceDirLabel.hidden = true;
|
|
2369
|
+
if (resourceDirInputWrap) resourceDirInputWrap.classList.remove("visible");
|
|
2370
|
+
} else {
|
|
2371
|
+
// Restore to label if dir is set, otherwise show button
|
|
2372
|
+
var dir = resourceDirInput ? resourceDirInput.value.trim() : "";
|
|
2373
|
+
if (dir) {
|
|
2374
|
+
if (resourceDirBtn) resourceDirBtn.hidden = true;
|
|
2375
|
+
if (resourceDirLabel) { resourceDirLabel.textContent = "Working dir: " + dir; resourceDirLabel.hidden = false; }
|
|
2376
|
+
if (resourceDirInputWrap) resourceDirInputWrap.classList.remove("visible");
|
|
2377
|
+
} else {
|
|
2378
|
+
if (resourceDirBtn) resourceDirBtn.hidden = false;
|
|
2379
|
+
if (resourceDirLabel) resourceDirLabel.hidden = true;
|
|
2380
|
+
if (resourceDirInputWrap) resourceDirInputWrap.classList.remove("visible");
|
|
2381
|
+
}
|
|
2382
|
+
}
|
|
2140
2383
|
}
|
|
2141
2384
|
|
|
2142
2385
|
function applyPaneFocusClasses() {
|
|
@@ -2277,23 +2520,19 @@ function buildStudioHtml(initialDocument: InitialStudioDocument | null, theme?:
|
|
|
2277
2520
|
return normalizeForCompare(a) === normalizeForCompare(b);
|
|
2278
2521
|
}
|
|
2279
2522
|
|
|
2280
|
-
function
|
|
2281
|
-
return latestResponseMarkdown;
|
|
2282
|
-
}
|
|
2283
|
-
|
|
2284
|
-
function updateSyncBadge() {
|
|
2523
|
+
function updateSyncBadge(normalizedEditorText) {
|
|
2285
2524
|
if (!syncBadgeEl) return;
|
|
2286
2525
|
|
|
2287
|
-
|
|
2288
|
-
const hasResponse = Boolean(response && response.trim());
|
|
2289
|
-
|
|
2290
|
-
if (!hasResponse) {
|
|
2526
|
+
if (!latestResponseHasContent) {
|
|
2291
2527
|
syncBadgeEl.textContent = "No response loaded";
|
|
2292
2528
|
syncBadgeEl.classList.remove("sync", "edited");
|
|
2293
2529
|
return;
|
|
2294
2530
|
}
|
|
2295
2531
|
|
|
2296
|
-
const
|
|
2532
|
+
const normalizedEditor = typeof normalizedEditorText === "string"
|
|
2533
|
+
? normalizedEditorText
|
|
2534
|
+
: normalizeForCompare(sourceTextEl.value);
|
|
2535
|
+
const inSync = normalizedEditor === latestResponseNormalized;
|
|
2297
2536
|
if (inSync) {
|
|
2298
2537
|
syncBadgeEl.textContent = "In sync with response";
|
|
2299
2538
|
syncBadgeEl.classList.add("sync");
|
|
@@ -2346,6 +2585,15 @@ function buildStudioHtml(initialDocument: InitialStudioDocument | null, theme?:
|
|
|
2346
2585
|
targetEl.appendChild(warningEl);
|
|
2347
2586
|
}
|
|
2348
2587
|
|
|
2588
|
+
function appendPreviewNotice(targetEl, message) {
|
|
2589
|
+
if (!targetEl || typeof targetEl.querySelector !== "function" || typeof targetEl.appendChild !== "function") return;
|
|
2590
|
+
if (targetEl.querySelector(".preview-image-warning")) return;
|
|
2591
|
+
const el = document.createElement("div");
|
|
2592
|
+
el.className = "preview-warning preview-image-warning";
|
|
2593
|
+
el.textContent = String(message || "");
|
|
2594
|
+
targetEl.appendChild(el);
|
|
2595
|
+
}
|
|
2596
|
+
|
|
2349
2597
|
async function getMermaidApi() {
|
|
2350
2598
|
if (mermaidModulePromise) {
|
|
2351
2599
|
return mermaidModulePromise;
|
|
@@ -2438,7 +2686,11 @@ function buildStudioHtml(initialDocument: InitialStudioDocument | null, theme?:
|
|
|
2438
2686
|
headers: {
|
|
2439
2687
|
"Content-Type": "application/json",
|
|
2440
2688
|
},
|
|
2441
|
-
body: JSON.stringify({
|
|
2689
|
+
body: JSON.stringify({
|
|
2690
|
+
markdown: String(markdown || ""),
|
|
2691
|
+
sourcePath: sourceState.path || "",
|
|
2692
|
+
resourceDir: (!sourceState.path && resourceDirInput) ? resourceDirInput.value.trim() : "",
|
|
2693
|
+
}),
|
|
2442
2694
|
signal: controller ? controller.signal : undefined,
|
|
2443
2695
|
});
|
|
2444
2696
|
} catch (error) {
|
|
@@ -2489,6 +2741,15 @@ function buildStudioHtml(initialDocument: InitialStudioDocument | null, theme?:
|
|
|
2489
2741
|
|
|
2490
2742
|
targetEl.innerHTML = sanitizeRenderedHtml(renderedHtml, markdown);
|
|
2491
2743
|
await renderMermaidInElement(targetEl);
|
|
2744
|
+
|
|
2745
|
+
// Warn if relative images are present but unlikely to resolve (non-file-backed content)
|
|
2746
|
+
if (!sourceState.path && !(resourceDirInput && resourceDirInput.value.trim())) {
|
|
2747
|
+
var hasRelativeImages = /!\\[.*?\\]\\((?!https?:\\/\\/|data:)[^)]+\\)/.test(markdown || "");
|
|
2748
|
+
var hasLatexImages = /\\\\includegraphics/.test(markdown || "");
|
|
2749
|
+
if (hasRelativeImages || hasLatexImages) {
|
|
2750
|
+
appendPreviewNotice(targetEl, "Images not displaying? Set working dir in the editor pane or open via /studio <path>.");
|
|
2751
|
+
}
|
|
2752
|
+
}
|
|
2492
2753
|
} catch (error) {
|
|
2493
2754
|
if (pane === "source") {
|
|
2494
2755
|
if (nonce !== sourcePreviewRenderNonce || editorView !== "preview") return;
|
|
@@ -2504,7 +2765,7 @@ function buildStudioHtml(initialDocument: InitialStudioDocument | null, theme?:
|
|
|
2504
2765
|
function renderSourcePreviewNow() {
|
|
2505
2766
|
if (editorView !== "preview") return;
|
|
2506
2767
|
const text = sourceTextEl.value || "";
|
|
2507
|
-
if (editorLanguage && editorLanguage !== "markdown") {
|
|
2768
|
+
if (editorLanguage && editorLanguage !== "markdown" && editorLanguage !== "latex") {
|
|
2508
2769
|
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
2770
|
return;
|
|
2510
2771
|
}
|
|
@@ -2528,15 +2789,20 @@ function buildStudioHtml(initialDocument: InitialStudioDocument | null, theme?:
|
|
|
2528
2789
|
}, delay);
|
|
2529
2790
|
}
|
|
2530
2791
|
|
|
2531
|
-
function renderSourcePreview() {
|
|
2792
|
+
function renderSourcePreview(options) {
|
|
2793
|
+
const previewDelayMs =
|
|
2794
|
+
options && typeof options.previewDelayMs === "number"
|
|
2795
|
+
? Math.max(0, options.previewDelayMs)
|
|
2796
|
+
: 0;
|
|
2797
|
+
|
|
2532
2798
|
if (editorView === "preview") {
|
|
2533
|
-
scheduleSourcePreviewRender(
|
|
2799
|
+
scheduleSourcePreviewRender(previewDelayMs);
|
|
2534
2800
|
}
|
|
2535
2801
|
if (editorHighlightEnabled && editorView === "markdown") {
|
|
2536
2802
|
scheduleEditorHighlightRender();
|
|
2537
2803
|
}
|
|
2538
2804
|
if (rightView === "editor-preview") {
|
|
2539
|
-
scheduleResponseEditorPreviewRender(
|
|
2805
|
+
scheduleResponseEditorPreviewRender(previewDelayMs);
|
|
2540
2806
|
}
|
|
2541
2807
|
}
|
|
2542
2808
|
|
|
@@ -2562,7 +2828,7 @@ function buildStudioHtml(initialDocument: InitialStudioDocument | null, theme?:
|
|
|
2562
2828
|
critiqueViewEl.innerHTML = "<pre class='plain-markdown'>Editor is empty.</pre>";
|
|
2563
2829
|
return;
|
|
2564
2830
|
}
|
|
2565
|
-
if (editorLanguage && editorLanguage !== "markdown") {
|
|
2831
|
+
if (editorLanguage && editorLanguage !== "markdown" && editorLanguage !== "latex") {
|
|
2566
2832
|
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
2833
|
return;
|
|
2568
2834
|
}
|
|
@@ -2601,14 +2867,16 @@ function buildStudioHtml(initialDocument: InitialStudioDocument | null, theme?:
|
|
|
2601
2867
|
critiqueViewEl.innerHTML = buildPlainMarkdownHtml(markdown);
|
|
2602
2868
|
}
|
|
2603
2869
|
|
|
2604
|
-
function updateResultActionButtons() {
|
|
2605
|
-
const
|
|
2606
|
-
const
|
|
2607
|
-
|
|
2870
|
+
function updateResultActionButtons(normalizedEditorText) {
|
|
2871
|
+
const hasResponse = latestResponseHasContent;
|
|
2872
|
+
const normalizedEditor = typeof normalizedEditorText === "string"
|
|
2873
|
+
? normalizedEditorText
|
|
2874
|
+
: normalizeForCompare(sourceTextEl.value);
|
|
2875
|
+
const responseLoaded = hasResponse && normalizedEditor === latestResponseNormalized;
|
|
2608
2876
|
const isCritiqueResponse = hasResponse && latestResponseIsStructuredCritique;
|
|
2609
2877
|
|
|
2610
|
-
const critiqueNotes = isCritiqueResponse ?
|
|
2611
|
-
const critiqueNotesLoaded = Boolean(critiqueNotes) &&
|
|
2878
|
+
const critiqueNotes = isCritiqueResponse ? latestCritiqueNotes : "";
|
|
2879
|
+
const critiqueNotesLoaded = Boolean(critiqueNotes) && normalizedEditor === latestCritiqueNotesNormalized;
|
|
2612
2880
|
|
|
2613
2881
|
loadResponseBtn.hidden = isCritiqueResponse;
|
|
2614
2882
|
loadCritiqueNotesBtn.hidden = !isCritiqueResponse;
|
|
@@ -2628,39 +2896,41 @@ function buildStudioHtml(initialDocument: InitialStudioDocument | null, theme?:
|
|
|
2628
2896
|
pullLatestBtn.disabled = uiBusy || followLatest;
|
|
2629
2897
|
pullLatestBtn.textContent = queuedLatestResponse ? "Get latest response *" : "Get latest response";
|
|
2630
2898
|
|
|
2631
|
-
updateSyncBadge();
|
|
2899
|
+
updateSyncBadge(normalizedEditor);
|
|
2632
2900
|
}
|
|
2633
2901
|
|
|
2634
2902
|
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
2903
|
updateSourceBadge();
|
|
2644
2904
|
updateReferenceBadge();
|
|
2645
2905
|
renderActiveResult();
|
|
2646
2906
|
updateResultActionButtons();
|
|
2647
2907
|
}
|
|
2648
2908
|
|
|
2909
|
+
function getEffectiveSavePath() {
|
|
2910
|
+
// File-backed: use the original path
|
|
2911
|
+
if (sourceState.source === "file" && sourceState.path) return sourceState.path;
|
|
2912
|
+
// Upload with working dir + filename: derive path
|
|
2913
|
+
if (sourceState.source === "upload" && sourceState.label && resourceDirInput && resourceDirInput.value.trim()) {
|
|
2914
|
+
var name = sourceState.label.replace(/^upload:\\s*/i, "");
|
|
2915
|
+
if (name) return resourceDirInput.value.trim().replace(/\\/$/, "") + "/" + name;
|
|
2916
|
+
}
|
|
2917
|
+
return null;
|
|
2918
|
+
}
|
|
2919
|
+
|
|
2649
2920
|
function updateSaveFileTooltip() {
|
|
2650
2921
|
if (!saveOverBtn) return;
|
|
2651
2922
|
|
|
2652
|
-
|
|
2653
|
-
if (
|
|
2654
|
-
|
|
2655
|
-
saveOverBtn.title = "Overwrite current file: " + target;
|
|
2923
|
+
var effectivePath = getEffectiveSavePath();
|
|
2924
|
+
if (effectivePath) {
|
|
2925
|
+
saveOverBtn.title = "Overwrite file: " + effectivePath;
|
|
2656
2926
|
return;
|
|
2657
2927
|
}
|
|
2658
2928
|
|
|
2659
|
-
saveOverBtn.title = "Save
|
|
2929
|
+
saveOverBtn.title = "Save editor is available after opening a file, setting a working dir, or using Save editor as…";
|
|
2660
2930
|
}
|
|
2661
2931
|
|
|
2662
2932
|
function syncActionButtons() {
|
|
2663
|
-
const canSaveOver =
|
|
2933
|
+
const canSaveOver = Boolean(getEffectiveSavePath());
|
|
2664
2934
|
|
|
2665
2935
|
fileInput.disabled = uiBusy;
|
|
2666
2936
|
saveAsBtn.disabled = uiBusy;
|
|
@@ -2803,6 +3073,7 @@ function buildStudioHtml(initialDocument: InitialStudioDocument | null, theme?:
|
|
|
2803
3073
|
|
|
2804
3074
|
const first = raw.split(/\\s+/)[0].replace(/^\\./, "").toLowerCase();
|
|
2805
3075
|
|
|
3076
|
+
// Explicit aliases that don't match extension names
|
|
2806
3077
|
if (first === "js" || first === "javascript" || first === "jsx" || first === "node") return "javascript";
|
|
2807
3078
|
if (first === "ts" || first === "typescript" || first === "tsx") return "typescript";
|
|
2808
3079
|
if (first === "py" || first === "python") return "python";
|
|
@@ -2815,8 +3086,11 @@ function buildStudioHtml(initialDocument: InitialStudioDocument | null, theme?:
|
|
|
2815
3086
|
if (first === "fortran" || first === "f90" || first === "f95" || first === "f03" || first === "f" || first === "for") return "fortran";
|
|
2816
3087
|
if (first === "r") return "r";
|
|
2817
3088
|
if (first === "matlab" || first === "m") return "matlab";
|
|
3089
|
+
if (first === "latex" || first === "tex") return "latex";
|
|
3090
|
+
if (first === "diff" || first === "patch" || first === "udiff") return "diff";
|
|
2818
3091
|
|
|
2819
|
-
|
|
3092
|
+
// Fall back to the unified extension->language map
|
|
3093
|
+
return EXT_TO_LANG[first] || "";
|
|
2820
3094
|
}
|
|
2821
3095
|
|
|
2822
3096
|
function highlightCodeTokens(line, pattern, classifyMatch) {
|
|
@@ -2981,6 +3255,31 @@ function buildStudioHtml(initialDocument: InitialStudioDocument | null, theme?:
|
|
|
2981
3255
|
return "<span class='hl-code'>" + highlighted + "</span>";
|
|
2982
3256
|
}
|
|
2983
3257
|
|
|
3258
|
+
if (lang === "latex") {
|
|
3259
|
+
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;
|
|
3260
|
+
const highlighted = highlightCodeTokens(source, texPattern, (match) => {
|
|
3261
|
+
if (match[1]) return "hl-code-com";
|
|
3262
|
+
if (match[2]) return "hl-code-kw";
|
|
3263
|
+
if (match[3]) return "hl-code-fn";
|
|
3264
|
+
if (match[4]) return "hl-code-op";
|
|
3265
|
+
if (match[5]) return "hl-code-str";
|
|
3266
|
+
if (match[6]) return "hl-code-num";
|
|
3267
|
+
return "hl-code";
|
|
3268
|
+
});
|
|
3269
|
+
return highlighted;
|
|
3270
|
+
}
|
|
3271
|
+
|
|
3272
|
+
if (lang === "diff") {
|
|
3273
|
+
var escaped = escapeHtml(source);
|
|
3274
|
+
if (/^@@/.test(source)) return "<span class=\\"hl-code-fn\\">" + escaped + "</span>";
|
|
3275
|
+
if (/^\\+\\+\\+|^---/.test(source)) return "<span class=\\"hl-code-kw\\">" + escaped + "</span>";
|
|
3276
|
+
if (/^\\+/.test(source)) return "<span class=\\"hl-diff-add\\">" + escaped + "</span>";
|
|
3277
|
+
if (/^-/.test(source)) return "<span class=\\"hl-diff-del\\">" + escaped + "</span>";
|
|
3278
|
+
if (/^diff /.test(source)) return "<span class=\\"hl-code-kw\\">" + escaped + "</span>";
|
|
3279
|
+
if (/^index /.test(source)) return "<span class=\\"hl-code-com\\">" + escaped + "</span>";
|
|
3280
|
+
return escaped;
|
|
3281
|
+
}
|
|
3282
|
+
|
|
2984
3283
|
return wrapHighlight("hl-code", source);
|
|
2985
3284
|
}
|
|
2986
3285
|
|
|
@@ -3067,23 +3366,10 @@ function buildStudioHtml(initialDocument: InitialStudioDocument | null, theme?:
|
|
|
3067
3366
|
|
|
3068
3367
|
function detectLanguageFromName(name) {
|
|
3069
3368
|
if (!name) return "";
|
|
3070
|
-
|
|
3369
|
+
var dot = name.lastIndexOf(".");
|
|
3071
3370
|
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 "";
|
|
3371
|
+
var ext = name.slice(dot + 1).toLowerCase();
|
|
3372
|
+
return EXT_TO_LANG[ext] || "";
|
|
3087
3373
|
}
|
|
3088
3374
|
|
|
3089
3375
|
function renderEditorHighlightNow() {
|
|
@@ -3134,6 +3420,31 @@ function buildStudioHtml(initialDocument: InitialStudioDocument | null, theme?:
|
|
|
3134
3420
|
sourceHighlightEl.scrollLeft = sourceTextEl.scrollLeft;
|
|
3135
3421
|
}
|
|
3136
3422
|
|
|
3423
|
+
function runEditorMetaUpdateNow() {
|
|
3424
|
+
const normalizedEditor = normalizeForCompare(sourceTextEl.value);
|
|
3425
|
+
updateResultActionButtons(normalizedEditor);
|
|
3426
|
+
}
|
|
3427
|
+
|
|
3428
|
+
function scheduleEditorMetaUpdate() {
|
|
3429
|
+
if (editorMetaUpdateRaf !== null) {
|
|
3430
|
+
if (typeof window.cancelAnimationFrame === "function") {
|
|
3431
|
+
window.cancelAnimationFrame(editorMetaUpdateRaf);
|
|
3432
|
+
} else {
|
|
3433
|
+
window.clearTimeout(editorMetaUpdateRaf);
|
|
3434
|
+
}
|
|
3435
|
+
editorMetaUpdateRaf = null;
|
|
3436
|
+
}
|
|
3437
|
+
|
|
3438
|
+
const schedule = typeof window.requestAnimationFrame === "function"
|
|
3439
|
+
? window.requestAnimationFrame.bind(window)
|
|
3440
|
+
: (cb) => window.setTimeout(cb, 16);
|
|
3441
|
+
|
|
3442
|
+
editorMetaUpdateRaf = schedule(() => {
|
|
3443
|
+
editorMetaUpdateRaf = null;
|
|
3444
|
+
runEditorMetaUpdateNow();
|
|
3445
|
+
});
|
|
3446
|
+
}
|
|
3447
|
+
|
|
3137
3448
|
function readStoredToggle(storageKey) {
|
|
3138
3449
|
if (!window.localStorage) return null;
|
|
3139
3450
|
try {
|
|
@@ -3320,9 +3631,18 @@ function buildStudioHtml(initialDocument: InitialStudioDocument | null, theme?:
|
|
|
3320
3631
|
latestResponseKind = kind === "critique" ? "critique" : "annotation";
|
|
3321
3632
|
latestResponseTimestamp = responseTimestamp;
|
|
3322
3633
|
latestResponseIsStructuredCritique = isStructuredCritique(markdown);
|
|
3634
|
+
latestResponseHasContent = Boolean(markdown && markdown.trim());
|
|
3635
|
+
latestResponseNormalized = normalizeForCompare(markdown);
|
|
3636
|
+
|
|
3637
|
+
if (latestResponseIsStructuredCritique) {
|
|
3638
|
+
latestCritiqueNotes = buildCritiqueNotesMarkdown(markdown);
|
|
3639
|
+
latestCritiqueNotesNormalized = normalizeForCompare(latestCritiqueNotes);
|
|
3640
|
+
} else {
|
|
3641
|
+
latestCritiqueNotes = "";
|
|
3642
|
+
latestCritiqueNotesNormalized = "";
|
|
3643
|
+
}
|
|
3323
3644
|
|
|
3324
3645
|
refreshResponseUi();
|
|
3325
|
-
syncActionButtons();
|
|
3326
3646
|
}
|
|
3327
3647
|
|
|
3328
3648
|
function applyLatestPayload(payload) {
|
|
@@ -3527,6 +3847,17 @@ function buildStudioHtml(initialDocument: InitialStudioDocument | null, theme?:
|
|
|
3527
3847
|
setStatus(message.message);
|
|
3528
3848
|
}
|
|
3529
3849
|
}
|
|
3850
|
+
|
|
3851
|
+
if (message.type === "theme_update" && message.vars && typeof message.vars === "object") {
|
|
3852
|
+
var root = document.documentElement;
|
|
3853
|
+
Object.keys(message.vars).forEach(function(key) {
|
|
3854
|
+
if (key === "color-scheme") {
|
|
3855
|
+
root.style.colorScheme = message.vars[key];
|
|
3856
|
+
} else {
|
|
3857
|
+
root.style.setProperty(key, message.vars[key]);
|
|
3858
|
+
}
|
|
3859
|
+
});
|
|
3860
|
+
}
|
|
3530
3861
|
}
|
|
3531
3862
|
|
|
3532
3863
|
function connect() {
|
|
@@ -3728,8 +4059,8 @@ function buildStudioHtml(initialDocument: InitialStudioDocument | null, theme?:
|
|
|
3728
4059
|
});
|
|
3729
4060
|
|
|
3730
4061
|
sourceTextEl.addEventListener("input", () => {
|
|
3731
|
-
renderSourcePreview();
|
|
3732
|
-
|
|
4062
|
+
renderSourcePreview({ previewDelayMs: PREVIEW_INPUT_DEBOUNCE_MS });
|
|
4063
|
+
scheduleEditorMetaUpdate();
|
|
3733
4064
|
});
|
|
3734
4065
|
|
|
3735
4066
|
sourceTextEl.addEventListener("scroll", () => {
|
|
@@ -3827,8 +4158,10 @@ function buildStudioHtml(initialDocument: InitialStudioDocument | null, theme?:
|
|
|
3827
4158
|
return;
|
|
3828
4159
|
}
|
|
3829
4160
|
|
|
3830
|
-
|
|
3831
|
-
|
|
4161
|
+
var suggestedName = sourceState.label ? sourceState.label.replace(/^upload:\\s*/i, "") : "draft.md";
|
|
4162
|
+
var suggestedDir = resourceDirInput && resourceDirInput.value.trim() ? resourceDirInput.value.trim().replace(/\\/$/, "") + "/" : "./";
|
|
4163
|
+
const suggested = sourceState.path || (suggestedDir + suggestedName);
|
|
4164
|
+
const path = window.prompt("Save editor content as:", suggested);
|
|
3832
4165
|
if (!path) return;
|
|
3833
4166
|
|
|
3834
4167
|
const requestId = beginUiAction("save_as");
|
|
@@ -3849,21 +4182,24 @@ function buildStudioHtml(initialDocument: InitialStudioDocument | null, theme?:
|
|
|
3849
4182
|
});
|
|
3850
4183
|
|
|
3851
4184
|
saveOverBtn.addEventListener("click", () => {
|
|
3852
|
-
|
|
3853
|
-
|
|
4185
|
+
var effectivePath = getEffectiveSavePath();
|
|
4186
|
+
if (!effectivePath) {
|
|
4187
|
+
setStatus("Save editor requires a file path. Open via /studio <path>, set a working dir, or use Save editor as…", "warning");
|
|
3854
4188
|
return;
|
|
3855
4189
|
}
|
|
3856
4190
|
|
|
3857
|
-
if (!window.confirm("Overwrite " +
|
|
4191
|
+
if (!window.confirm("Overwrite " + effectivePath + "?")) {
|
|
3858
4192
|
return;
|
|
3859
4193
|
}
|
|
3860
4194
|
|
|
3861
4195
|
const requestId = beginUiAction("save_over");
|
|
3862
4196
|
if (!requestId) return;
|
|
3863
4197
|
|
|
4198
|
+
// Use save_as with the effective path for both file-backed and derived paths
|
|
3864
4199
|
const sent = sendMessage({
|
|
3865
|
-
type: "
|
|
4200
|
+
type: "save_as_request",
|
|
3866
4201
|
requestId,
|
|
4202
|
+
path: effectivePath,
|
|
3867
4203
|
content: sourceTextEl.value,
|
|
3868
4204
|
});
|
|
3869
4205
|
|
|
@@ -3935,6 +4271,67 @@ function buildStudioHtml(initialDocument: InitialStudioDocument | null, theme?:
|
|
|
3935
4271
|
}
|
|
3936
4272
|
});
|
|
3937
4273
|
|
|
4274
|
+
// Working directory controls — three states: button | input | label
|
|
4275
|
+
function showResourceDirState(state) {
|
|
4276
|
+
// state: "button" | "input" | "label"
|
|
4277
|
+
if (resourceDirBtn) resourceDirBtn.hidden = state !== "button";
|
|
4278
|
+
if (resourceDirInputWrap) {
|
|
4279
|
+
if (state === "input") resourceDirInputWrap.classList.add("visible");
|
|
4280
|
+
else resourceDirInputWrap.classList.remove("visible");
|
|
4281
|
+
}
|
|
4282
|
+
if (resourceDirLabel) resourceDirLabel.hidden = state !== "label";
|
|
4283
|
+
}
|
|
4284
|
+
function applyResourceDir() {
|
|
4285
|
+
var dir = resourceDirInput ? resourceDirInput.value.trim() : "";
|
|
4286
|
+
if (dir) {
|
|
4287
|
+
if (resourceDirLabel) resourceDirLabel.textContent = "Working dir: " + dir;
|
|
4288
|
+
showResourceDirState("label");
|
|
4289
|
+
} else {
|
|
4290
|
+
showResourceDirState("button");
|
|
4291
|
+
}
|
|
4292
|
+
updateSaveFileTooltip();
|
|
4293
|
+
syncActionButtons();
|
|
4294
|
+
renderSourcePreview();
|
|
4295
|
+
}
|
|
4296
|
+
if (resourceDirBtn) {
|
|
4297
|
+
resourceDirBtn.addEventListener("click", () => {
|
|
4298
|
+
showResourceDirState("input");
|
|
4299
|
+
if (resourceDirInput) resourceDirInput.focus();
|
|
4300
|
+
});
|
|
4301
|
+
}
|
|
4302
|
+
if (resourceDirLabel) {
|
|
4303
|
+
resourceDirLabel.addEventListener("click", () => {
|
|
4304
|
+
showResourceDirState("input");
|
|
4305
|
+
if (resourceDirInput) resourceDirInput.focus();
|
|
4306
|
+
});
|
|
4307
|
+
}
|
|
4308
|
+
if (resourceDirInput) {
|
|
4309
|
+
resourceDirInput.addEventListener("keydown", (e) => {
|
|
4310
|
+
if (e.key === "Enter") {
|
|
4311
|
+
e.preventDefault();
|
|
4312
|
+
applyResourceDir();
|
|
4313
|
+
} else if (e.key === "Escape") {
|
|
4314
|
+
e.preventDefault();
|
|
4315
|
+
var dir = resourceDirInput.value.trim();
|
|
4316
|
+
if (dir) {
|
|
4317
|
+
showResourceDirState("label");
|
|
4318
|
+
} else {
|
|
4319
|
+
showResourceDirState("button");
|
|
4320
|
+
}
|
|
4321
|
+
}
|
|
4322
|
+
});
|
|
4323
|
+
}
|
|
4324
|
+
if (resourceDirClearBtn) {
|
|
4325
|
+
resourceDirClearBtn.addEventListener("click", () => {
|
|
4326
|
+
if (resourceDirInput) resourceDirInput.value = "";
|
|
4327
|
+
if (resourceDirLabel) resourceDirLabel.textContent = "";
|
|
4328
|
+
showResourceDirState("button");
|
|
4329
|
+
updateSaveFileTooltip();
|
|
4330
|
+
syncActionButtons();
|
|
4331
|
+
renderSourcePreview();
|
|
4332
|
+
});
|
|
4333
|
+
}
|
|
4334
|
+
|
|
3938
4335
|
fileInput.addEventListener("change", () => {
|
|
3939
4336
|
const file = fileInput.files && fileInput.files[0];
|
|
3940
4337
|
if (!file) return;
|
|
@@ -3998,6 +4395,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
3998
4395
|
let initialStudioDocument: InitialStudioDocument | null = null;
|
|
3999
4396
|
let studioCwd = process.cwd();
|
|
4000
4397
|
let lastCommandCtx: ExtensionCommandContext | null = null;
|
|
4398
|
+
let lastThemeVarsJson = "";
|
|
4001
4399
|
let agentBusy = false;
|
|
4002
4400
|
|
|
4003
4401
|
const isStudioBusy = () => agentBusy || activeRequest !== null;
|
|
@@ -4369,7 +4767,17 @@ export default function (pi: ExtensionAPI) {
|
|
|
4369
4767
|
}
|
|
4370
4768
|
|
|
4371
4769
|
try {
|
|
4372
|
-
const
|
|
4770
|
+
const sourcePath =
|
|
4771
|
+
parsedBody && typeof parsedBody === "object" && typeof (parsedBody as { sourcePath?: unknown }).sourcePath === "string"
|
|
4772
|
+
? (parsedBody as { sourcePath: string }).sourcePath
|
|
4773
|
+
: "";
|
|
4774
|
+
const userResourceDir =
|
|
4775
|
+
parsedBody && typeof parsedBody === "object" && typeof (parsedBody as { resourceDir?: unknown }).resourceDir === "string"
|
|
4776
|
+
? (parsedBody as { resourceDir: string }).resourceDir
|
|
4777
|
+
: "";
|
|
4778
|
+
const resourcePath = sourcePath ? dirname(sourcePath) : (userResourceDir || studioCwd || undefined);
|
|
4779
|
+
const isLatex = /\\documentclass\b|\\begin\{document\}/.test(markdown);
|
|
4780
|
+
const html = await renderStudioMarkdownWithPandoc(markdown, isLatex, resourcePath);
|
|
4373
4781
|
respondJson(res, 200, { ok: true, html, renderer: "pandoc" });
|
|
4374
4782
|
} catch (error) {
|
|
4375
4783
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -4536,6 +4944,27 @@ export default function (pi: ExtensionAPI) {
|
|
|
4536
4944
|
state.port = address.port;
|
|
4537
4945
|
|
|
4538
4946
|
serverState = state;
|
|
4947
|
+
|
|
4948
|
+
// Periodically check for theme changes and push to all clients
|
|
4949
|
+
const themeCheckInterval = setInterval(() => {
|
|
4950
|
+
if (!lastCommandCtx?.ui?.theme || !serverState || serverState.clients.size === 0) return;
|
|
4951
|
+
try {
|
|
4952
|
+
const style = getStudioThemeStyle(lastCommandCtx.ui.theme);
|
|
4953
|
+
const vars = buildThemeCssVars(style);
|
|
4954
|
+
const json = JSON.stringify(vars);
|
|
4955
|
+
if (json !== lastThemeVarsJson) {
|
|
4956
|
+
lastThemeVarsJson = json;
|
|
4957
|
+
for (const client of serverState.clients) {
|
|
4958
|
+
sendToClient(client, { type: "theme_update", vars });
|
|
4959
|
+
}
|
|
4960
|
+
}
|
|
4961
|
+
} catch {
|
|
4962
|
+
// Ignore theme read errors
|
|
4963
|
+
}
|
|
4964
|
+
}, 2000);
|
|
4965
|
+
// Clean up interval if server closes
|
|
4966
|
+
server.once("close", () => clearInterval(themeCheckInterval));
|
|
4967
|
+
|
|
4539
4968
|
return state;
|
|
4540
4969
|
};
|
|
4541
4970
|
|
|
@@ -4684,6 +5113,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
4684
5113
|
await ctx.waitForIdle();
|
|
4685
5114
|
lastCommandCtx = ctx;
|
|
4686
5115
|
studioCwd = ctx.cwd;
|
|
5116
|
+
// Seed theme vars so first ping doesn't trigger a false update
|
|
5117
|
+
try {
|
|
5118
|
+
const currentStyle = getStudioThemeStyle(ctx.ui.theme);
|
|
5119
|
+
lastThemeVarsJson = JSON.stringify(buildThemeCssVars(currentStyle));
|
|
5120
|
+
} catch { /* ignore */ }
|
|
4687
5121
|
|
|
4688
5122
|
const latestAssistant =
|
|
4689
5123
|
extractLatestAssistantFromEntries(ctx.sessionManager.getBranch())
|