pi-studio 0.2.6 → 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/CHANGELOG.md +14 -0
- package/README.md +14 -7
- package/WORKFLOW.md +1 -1
- package/index.ts +583 -138
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to `pi-studio` are documented here.
|
|
4
4
|
|
|
5
|
+
## [0.3.0] — 2026-03-02
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- **Editor Preview in response pane**: new `Right: Editor (Preview)` view mode renders editor text in the right pane with debounced live updates — enables Overleaf-style side-by-side source/rendered editing without a model round-trip.
|
|
9
|
+
- Code-language aware: Editor Preview renders syntax-highlighted code when a non-markdown language is selected.
|
|
10
|
+
- Response badge shows "Previewing: editor text" in editor-preview mode, with "· response updated HH:MM:SS" when a model response arrives in the background.
|
|
11
|
+
- Right pane section header updates to "Editor Preview" when in editor-preview mode.
|
|
12
|
+
|
|
13
|
+
### Changed
|
|
14
|
+
- View toggle labels now use `Left: Source (Mode)` / `Right: Source (Mode)` format for unambiguous pane identification (e.g., `Left: Editor (Raw)`, `Right: Response (Preview)`, `Right: Editor (Preview)`).
|
|
15
|
+
- Sync badge wording: `Edited since response` → `Out of sync with response` (direction-neutral, accurate regardless of which side changed).
|
|
16
|
+
- Critique load buttons now include destination: `Load critique notes into editor` / `Load full critique into editor` (consistent with `Load response into editor`).
|
|
17
|
+
- Critique loaded-state labels updated: `Critique (full) already in editor` → `Full critique already in editor`.
|
|
18
|
+
|
|
5
19
|
## [0.2.4] — 2026-03-02
|
|
6
20
|
|
|
7
21
|
### Changed
|
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)
|
|
@@ -50,19 +48,27 @@ Status: experimental alpha.
|
|
|
50
48
|
- **Critique editor text** requests structured critique (auto/writing/code focus)
|
|
51
49
|
- Response load helpers:
|
|
52
50
|
- non-critique: **Load response into editor**
|
|
53
|
-
- critique: **Load critique
|
|
54
|
-
- File actions: **Save
|
|
55
|
-
- View toggles: `Editor
|
|
51
|
+
- critique: **Load critique notes into editor** / **Load full critique into editor**
|
|
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)`
|
|
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
|
|
56
55
|
- Preview mode supports MathML equations and Mermaid fenced diagrams
|
|
57
56
|
- **Language-aware syntax highlighting** with selectable language mode:
|
|
58
57
|
- Markdown (default): headings, links, code fences, lists, quotes, inline code
|
|
59
|
-
- 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
|
|
60
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
|
|
61
61
|
- Language auto-detected from file extension on file load; manually selectable via `Lang:` dropdown
|
|
62
62
|
- Applies to both editor Raw view (highlight overlay) and fenced code blocks in markdown
|
|
63
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)
|
|
64
69
|
- Separate syntax highlight toggles for editor and response Raw views, with local preference persistence
|
|
65
70
|
- Theme-aware browser UI derived from current pi theme
|
|
71
|
+
- View mode selectors integrated into panel headers for a cleaner layout
|
|
66
72
|
|
|
67
73
|
## Commands
|
|
68
74
|
|
|
@@ -102,7 +108,8 @@ pi -e https://github.com/omaclaren/pi-studio
|
|
|
102
108
|
- One studio request at a time.
|
|
103
109
|
- Pi Studio supports both markdown workflows (model responses, plans, and notes) and code file editing with language-aware syntax highlighting.
|
|
104
110
|
- Studio URLs include a token query parameter; avoid sharing full Studio URLs.
|
|
105
|
-
- 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.
|
|
106
113
|
- Preview markdown/code colors are mapped from active theme markdown (`md*`) and syntax (`syntax*`) tokens for closer terminal-vs-browser parity.
|
|
107
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.
|
|
108
115
|
- If Mermaid cannot load or a diagram fails to render, preview shows an inline warning and keeps source text visible.
|
package/WORKFLOW.md
CHANGED
|
@@ -75,7 +75,7 @@ Rules:
|
|
|
75
75
|
## Required UI elements
|
|
76
76
|
|
|
77
77
|
- Header actions: **Save As…**, **Save file** (file-backed), **Load file in editor**
|
|
78
|
-
- Header view toggles: `
|
|
78
|
+
- Header view toggles: `Left: Editor (Raw|Preview)`, `Right: Response (Raw|Preview) | Editor (Preview)`
|
|
79
79
|
- Preview mode uses server-side `pandoc` rendering (math-aware) with plain-markdown fallback when renderer is unavailable.
|
|
80
80
|
- Editor actions: **Insert annotation header**, **Run editor text**, **Critique editor text** (+ critique focus), **Send to pi editor**, **Copy editor text**
|
|
81
81
|
- Response actions include `Auto-update response: On|Off` + **Get latest response**
|
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,29 +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">Response: Raw</option>
|
|
1916
|
-
<option value="preview" selected>Response: Preview</option>
|
|
1917
|
-
</select>
|
|
1918
|
-
<button id="saveAsBtn" type="button" title="Save editor text to a new file path.">Save As…</button>
|
|
1919
|
-
<button id="saveOverBtn" type="button" title="Overwrite current file with editor text." disabled>Save file</button>
|
|
1920
|
-
<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>
|
|
1921
2068
|
</div>
|
|
1922
2069
|
</header>
|
|
1923
2070
|
|
|
1924
2071
|
<main>
|
|
1925
2072
|
<section id="leftPane">
|
|
1926
|
-
<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>
|
|
1927
2079
|
<div class="source-wrap">
|
|
1928
2080
|
<div class="source-meta">
|
|
1929
2081
|
<div class="badge-row">
|
|
1930
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>
|
|
1931
2089
|
<span id="syncBadge" class="source-badge sync-badge">No response loaded</span>
|
|
1932
2090
|
</div>
|
|
1933
2091
|
<div class="source-actions">
|
|
@@ -1959,6 +2117,19 @@ function buildStudioHtml(initialDocument: InitialStudioDocument | null, theme?:
|
|
|
1959
2117
|
<option value="fortran">Lang: Fortran</option>
|
|
1960
2118
|
<option value="r">Lang: R</option>
|
|
1961
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>
|
|
1962
2133
|
</select>
|
|
1963
2134
|
</div>
|
|
1964
2135
|
</div>
|
|
@@ -1971,7 +2142,13 @@ function buildStudioHtml(initialDocument: InitialStudioDocument | null, theme?:
|
|
|
1971
2142
|
</section>
|
|
1972
2143
|
|
|
1973
2144
|
<section id="rightPane">
|
|
1974
|
-
<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>
|
|
1975
2152
|
<div class="reference-meta">
|
|
1976
2153
|
<span id="referenceBadge" class="source-badge">Latest response: none</span>
|
|
1977
2154
|
</div>
|
|
@@ -1988,8 +2165,8 @@ function buildStudioHtml(initialDocument: InitialStudioDocument | null, theme?:
|
|
|
1988
2165
|
</select>
|
|
1989
2166
|
<button id="pullLatestBtn" type="button" title="Fetch the latest assistant response when auto-update is off.">Get latest response</button>
|
|
1990
2167
|
<button id="loadResponseBtn" type="button">Load response into editor</button>
|
|
1991
|
-
<button id="loadCritiqueNotesBtn" type="button" hidden>Load critique
|
|
1992
|
-
<button id="loadCritiqueFullBtn" type="button" hidden>Load critique
|
|
2168
|
+
<button id="loadCritiqueNotesBtn" type="button" hidden>Load critique notes into editor</button>
|
|
2169
|
+
<button id="loadCritiqueFullBtn" type="button" hidden>Load full critique into editor</button>
|
|
1993
2170
|
<button id="copyResponseBtn" type="button">Copy response text</button>
|
|
1994
2171
|
</div>
|
|
1995
2172
|
</div>
|
|
@@ -2033,11 +2210,9 @@ function buildStudioHtml(initialDocument: InitialStudioDocument | null, theme?:
|
|
|
2033
2210
|
const sourcePreviewEl = document.getElementById("sourcePreview");
|
|
2034
2211
|
const leftPaneEl = document.getElementById("leftPane");
|
|
2035
2212
|
const rightPaneEl = document.getElementById("rightPane");
|
|
2036
|
-
const leftSectionHeaderEl = document.getElementById("leftSectionHeader");
|
|
2037
2213
|
const sourceBadgeEl = document.getElementById("sourceBadge");
|
|
2038
2214
|
const syncBadgeEl = document.getElementById("syncBadge");
|
|
2039
2215
|
const critiqueViewEl = document.getElementById("critiqueView");
|
|
2040
|
-
const rightSectionHeaderEl = document.getElementById("rightSectionHeader");
|
|
2041
2216
|
const referenceBadgeEl = document.getElementById("referenceBadge");
|
|
2042
2217
|
const editorViewSelect = document.getElementById("editorViewSelect");
|
|
2043
2218
|
const rightViewSelect = document.getElementById("rightViewSelect");
|
|
@@ -2048,6 +2223,11 @@ function buildStudioHtml(initialDocument: InitialStudioDocument | null, theme?:
|
|
|
2048
2223
|
const critiqueBtn = document.getElementById("critiqueBtn");
|
|
2049
2224
|
const lensSelect = document.getElementById("lensSelect");
|
|
2050
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");
|
|
2051
2231
|
const loadResponseBtn = document.getElementById("loadResponseBtn");
|
|
2052
2232
|
const loadCritiqueNotesBtn = document.getElementById("loadCritiqueNotesBtn");
|
|
2053
2233
|
const loadCritiqueFullBtn = document.getElementById("loadCritiqueFullBtn");
|
|
@@ -2092,12 +2272,50 @@ function buildStudioHtml(initialDocument: InitialStudioDocument | null, theme?:
|
|
|
2092
2272
|
const EDITOR_HIGHLIGHT_MAX_CHARS = 80_000;
|
|
2093
2273
|
const EDITOR_HIGHLIGHT_STORAGE_KEY = "piStudio.editorHighlightEnabled";
|
|
2094
2274
|
const EDITOR_LANGUAGE_STORAGE_KEY = "piStudio.editorLanguage";
|
|
2095
|
-
|
|
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);
|
|
2096
2313
|
const RESPONSE_HIGHLIGHT_MAX_CHARS = 120_000;
|
|
2097
2314
|
const RESPONSE_HIGHLIGHT_STORAGE_KEY = "piStudio.responseHighlightEnabled";
|
|
2098
2315
|
let sourcePreviewRenderTimer = null;
|
|
2099
2316
|
let sourcePreviewRenderNonce = 0;
|
|
2100
2317
|
let responsePreviewRenderNonce = 0;
|
|
2318
|
+
let responseEditorPreviewTimer = null;
|
|
2101
2319
|
let editorHighlightEnabled = false;
|
|
2102
2320
|
let editorLanguage = "markdown";
|
|
2103
2321
|
let responseHighlightEnabled = false;
|
|
@@ -2135,6 +2353,27 @@ function buildStudioHtml(initialDocument: InitialStudioDocument | null, theme?:
|
|
|
2135
2353
|
function updateSourceBadge() {
|
|
2136
2354
|
const label = sourceState && sourceState.label ? sourceState.label : "blank";
|
|
2137
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
|
+
}
|
|
2138
2377
|
}
|
|
2139
2378
|
|
|
2140
2379
|
function applyPaneFocusClasses() {
|
|
@@ -2242,6 +2481,18 @@ function buildStudioHtml(initialDocument: InitialStudioDocument | null, theme?:
|
|
|
2242
2481
|
function updateReferenceBadge() {
|
|
2243
2482
|
if (!referenceBadgeEl) return;
|
|
2244
2483
|
|
|
2484
|
+
if (rightView === "editor-preview") {
|
|
2485
|
+
const hasResponse = Boolean(latestResponseMarkdown && latestResponseMarkdown.trim());
|
|
2486
|
+
if (hasResponse) {
|
|
2487
|
+
const time = formatReferenceTime(latestResponseTimestamp);
|
|
2488
|
+
const suffix = time ? " · response updated " + time : " · response available";
|
|
2489
|
+
referenceBadgeEl.textContent = "Previewing: editor text" + suffix;
|
|
2490
|
+
} else {
|
|
2491
|
+
referenceBadgeEl.textContent = "Previewing: editor text";
|
|
2492
|
+
}
|
|
2493
|
+
return;
|
|
2494
|
+
}
|
|
2495
|
+
|
|
2245
2496
|
const hasResponse = Boolean(latestResponseMarkdown && latestResponseMarkdown.trim());
|
|
2246
2497
|
if (!hasResponse) {
|
|
2247
2498
|
referenceBadgeEl.textContent = "Latest response: none";
|
|
@@ -2285,7 +2536,7 @@ function buildStudioHtml(initialDocument: InitialStudioDocument | null, theme?:
|
|
|
2285
2536
|
syncBadgeEl.classList.add("sync");
|
|
2286
2537
|
syncBadgeEl.classList.remove("edited");
|
|
2287
2538
|
} else {
|
|
2288
|
-
syncBadgeEl.textContent = "
|
|
2539
|
+
syncBadgeEl.textContent = "Out of sync with response";
|
|
2289
2540
|
syncBadgeEl.classList.add("edited");
|
|
2290
2541
|
syncBadgeEl.classList.remove("sync");
|
|
2291
2542
|
}
|
|
@@ -2332,6 +2583,15 @@ function buildStudioHtml(initialDocument: InitialStudioDocument | null, theme?:
|
|
|
2332
2583
|
targetEl.appendChild(warningEl);
|
|
2333
2584
|
}
|
|
2334
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
|
+
|
|
2335
2595
|
async function getMermaidApi() {
|
|
2336
2596
|
if (mermaidModulePromise) {
|
|
2337
2597
|
return mermaidModulePromise;
|
|
@@ -2424,7 +2684,11 @@ function buildStudioHtml(initialDocument: InitialStudioDocument | null, theme?:
|
|
|
2424
2684
|
headers: {
|
|
2425
2685
|
"Content-Type": "application/json",
|
|
2426
2686
|
},
|
|
2427
|
-
body: JSON.stringify({
|
|
2687
|
+
body: JSON.stringify({
|
|
2688
|
+
markdown: String(markdown || ""),
|
|
2689
|
+
sourcePath: sourceState.path || "",
|
|
2690
|
+
resourceDir: (!sourceState.path && resourceDirInput) ? resourceDirInput.value.trim() : "",
|
|
2691
|
+
}),
|
|
2428
2692
|
signal: controller ? controller.signal : undefined,
|
|
2429
2693
|
});
|
|
2430
2694
|
} catch (error) {
|
|
@@ -2470,16 +2734,25 @@ function buildStudioHtml(initialDocument: InitialStudioDocument | null, theme?:
|
|
|
2470
2734
|
if (pane === "source") {
|
|
2471
2735
|
if (nonce !== sourcePreviewRenderNonce || editorView !== "preview") return;
|
|
2472
2736
|
} else {
|
|
2473
|
-
if (nonce !== responsePreviewRenderNonce || rightView !== "preview") return;
|
|
2737
|
+
if (nonce !== responsePreviewRenderNonce || (rightView !== "preview" && rightView !== "editor-preview")) return;
|
|
2474
2738
|
}
|
|
2475
2739
|
|
|
2476
2740
|
targetEl.innerHTML = sanitizeRenderedHtml(renderedHtml, markdown);
|
|
2477
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
|
+
}
|
|
2478
2751
|
} catch (error) {
|
|
2479
2752
|
if (pane === "source") {
|
|
2480
2753
|
if (nonce !== sourcePreviewRenderNonce || editorView !== "preview") return;
|
|
2481
2754
|
} else {
|
|
2482
|
-
if (nonce !== responsePreviewRenderNonce || rightView !== "preview") return;
|
|
2755
|
+
if (nonce !== responsePreviewRenderNonce || (rightView !== "preview" && rightView !== "editor-preview")) return;
|
|
2483
2756
|
}
|
|
2484
2757
|
|
|
2485
2758
|
const detail = error && error.message ? error.message : String(error || "unknown error");
|
|
@@ -2490,7 +2763,7 @@ function buildStudioHtml(initialDocument: InitialStudioDocument | null, theme?:
|
|
|
2490
2763
|
function renderSourcePreviewNow() {
|
|
2491
2764
|
if (editorView !== "preview") return;
|
|
2492
2765
|
const text = sourceTextEl.value || "";
|
|
2493
|
-
if (editorLanguage && editorLanguage !== "markdown") {
|
|
2766
|
+
if (editorLanguage && editorLanguage !== "markdown" && editorLanguage !== "latex") {
|
|
2494
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>";
|
|
2495
2768
|
return;
|
|
2496
2769
|
}
|
|
@@ -2521,9 +2794,43 @@ function buildStudioHtml(initialDocument: InitialStudioDocument | null, theme?:
|
|
|
2521
2794
|
if (editorHighlightEnabled && editorView === "markdown") {
|
|
2522
2795
|
scheduleEditorHighlightRender();
|
|
2523
2796
|
}
|
|
2797
|
+
if (rightView === "editor-preview") {
|
|
2798
|
+
scheduleResponseEditorPreviewRender(0);
|
|
2799
|
+
}
|
|
2800
|
+
}
|
|
2801
|
+
|
|
2802
|
+
function scheduleResponseEditorPreviewRender(delayMs) {
|
|
2803
|
+
if (responseEditorPreviewTimer) {
|
|
2804
|
+
window.clearTimeout(responseEditorPreviewTimer);
|
|
2805
|
+
responseEditorPreviewTimer = null;
|
|
2806
|
+
}
|
|
2807
|
+
|
|
2808
|
+
if (rightView !== "editor-preview") return;
|
|
2809
|
+
|
|
2810
|
+
const delay = typeof delayMs === "number" ? Math.max(0, delayMs) : 180;
|
|
2811
|
+
responseEditorPreviewTimer = window.setTimeout(() => {
|
|
2812
|
+
responseEditorPreviewTimer = null;
|
|
2813
|
+
renderActiveResult();
|
|
2814
|
+
}, delay);
|
|
2524
2815
|
}
|
|
2525
2816
|
|
|
2526
2817
|
function renderActiveResult() {
|
|
2818
|
+
if (rightView === "editor-preview") {
|
|
2819
|
+
const editorText = sourceTextEl.value || "";
|
|
2820
|
+
if (!editorText.trim()) {
|
|
2821
|
+
critiqueViewEl.innerHTML = "<pre class='plain-markdown'>Editor is empty.</pre>";
|
|
2822
|
+
return;
|
|
2823
|
+
}
|
|
2824
|
+
if (editorLanguage && editorLanguage !== "markdown" && editorLanguage !== "latex") {
|
|
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>";
|
|
2826
|
+
return;
|
|
2827
|
+
}
|
|
2828
|
+
const nonce = ++responsePreviewRenderNonce;
|
|
2829
|
+
critiqueViewEl.innerHTML = "<div class='preview-loading'>Rendering preview…</div>";
|
|
2830
|
+
void applyRenderedMarkdown(critiqueViewEl, editorText, "response", nonce);
|
|
2831
|
+
return;
|
|
2832
|
+
}
|
|
2833
|
+
|
|
2527
2834
|
const markdown = latestResponseMarkdown;
|
|
2528
2835
|
if (!markdown || !markdown.trim()) {
|
|
2529
2836
|
critiqueViewEl.innerHTML = "<pre class='plain-markdown'>No response yet. Run editor text or critique editor text.</pre>";
|
|
@@ -2570,10 +2877,10 @@ function buildStudioHtml(initialDocument: InitialStudioDocument | null, theme?:
|
|
|
2570
2877
|
loadResponseBtn.textContent = responseLoaded ? "Response already in editor" : "Load response into editor";
|
|
2571
2878
|
|
|
2572
2879
|
loadCritiqueNotesBtn.disabled = uiBusy || !isCritiqueResponse || !critiqueNotes || critiqueNotesLoaded;
|
|
2573
|
-
loadCritiqueNotesBtn.textContent = critiqueNotesLoaded ? "Critique notes already in editor" : "Load critique
|
|
2880
|
+
loadCritiqueNotesBtn.textContent = critiqueNotesLoaded ? "Critique notes already in editor" : "Load critique notes into editor";
|
|
2574
2881
|
|
|
2575
2882
|
loadCritiqueFullBtn.disabled = uiBusy || !isCritiqueResponse || responseLoaded;
|
|
2576
|
-
loadCritiqueFullBtn.textContent = responseLoaded ? "
|
|
2883
|
+
loadCritiqueFullBtn.textContent = responseLoaded ? "Full critique already in editor" : "Load full critique into editor";
|
|
2577
2884
|
|
|
2578
2885
|
copyResponseBtn.disabled = uiBusy || !hasResponse;
|
|
2579
2886
|
|
|
@@ -2584,35 +2891,37 @@ function buildStudioHtml(initialDocument: InitialStudioDocument | null, theme?:
|
|
|
2584
2891
|
}
|
|
2585
2892
|
|
|
2586
2893
|
function refreshResponseUi() {
|
|
2587
|
-
if (leftSectionHeaderEl) {
|
|
2588
|
-
leftSectionHeaderEl.textContent = "Editor";
|
|
2589
|
-
}
|
|
2590
|
-
|
|
2591
|
-
if (rightSectionHeaderEl) {
|
|
2592
|
-
rightSectionHeaderEl.textContent = "Response";
|
|
2593
|
-
}
|
|
2594
|
-
|
|
2595
2894
|
updateSourceBadge();
|
|
2596
2895
|
updateReferenceBadge();
|
|
2597
2896
|
renderActiveResult();
|
|
2598
2897
|
updateResultActionButtons();
|
|
2599
2898
|
}
|
|
2600
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
|
+
|
|
2601
2911
|
function updateSaveFileTooltip() {
|
|
2602
2912
|
if (!saveOverBtn) return;
|
|
2603
2913
|
|
|
2604
|
-
|
|
2605
|
-
if (
|
|
2606
|
-
|
|
2607
|
-
saveOverBtn.title = "Overwrite current file: " + target;
|
|
2914
|
+
var effectivePath = getEffectiveSavePath();
|
|
2915
|
+
if (effectivePath) {
|
|
2916
|
+
saveOverBtn.title = "Overwrite file: " + effectivePath;
|
|
2608
2917
|
return;
|
|
2609
2918
|
}
|
|
2610
2919
|
|
|
2611
|
-
saveOverBtn.title = "Save
|
|
2920
|
+
saveOverBtn.title = "Save editor is available after opening a file, setting a working dir, or using Save editor as…";
|
|
2612
2921
|
}
|
|
2613
2922
|
|
|
2614
2923
|
function syncActionButtons() {
|
|
2615
|
-
const canSaveOver =
|
|
2924
|
+
const canSaveOver = Boolean(getEffectiveSavePath());
|
|
2616
2925
|
|
|
2617
2926
|
fileInput.disabled = uiBusy;
|
|
2618
2927
|
saveAsBtn.disabled = uiBusy;
|
|
@@ -2672,9 +2981,15 @@ function buildStudioHtml(initialDocument: InitialStudioDocument | null, theme?:
|
|
|
2672
2981
|
}
|
|
2673
2982
|
|
|
2674
2983
|
function setRightView(nextView) {
|
|
2675
|
-
rightView = nextView === "preview" ? "preview" : "markdown";
|
|
2984
|
+
rightView = nextView === "preview" ? "preview" : (nextView === "editor-preview" ? "editor-preview" : "markdown");
|
|
2676
2985
|
rightViewSelect.value = rightView;
|
|
2677
|
-
|
|
2986
|
+
|
|
2987
|
+
if (rightView !== "editor-preview" && responseEditorPreviewTimer) {
|
|
2988
|
+
window.clearTimeout(responseEditorPreviewTimer);
|
|
2989
|
+
responseEditorPreviewTimer = null;
|
|
2990
|
+
}
|
|
2991
|
+
|
|
2992
|
+
refreshResponseUi();
|
|
2678
2993
|
syncActionButtons();
|
|
2679
2994
|
}
|
|
2680
2995
|
|
|
@@ -2749,6 +3064,7 @@ function buildStudioHtml(initialDocument: InitialStudioDocument | null, theme?:
|
|
|
2749
3064
|
|
|
2750
3065
|
const first = raw.split(/\\s+/)[0].replace(/^\\./, "").toLowerCase();
|
|
2751
3066
|
|
|
3067
|
+
// Explicit aliases that don't match extension names
|
|
2752
3068
|
if (first === "js" || first === "javascript" || first === "jsx" || first === "node") return "javascript";
|
|
2753
3069
|
if (first === "ts" || first === "typescript" || first === "tsx") return "typescript";
|
|
2754
3070
|
if (first === "py" || first === "python") return "python";
|
|
@@ -2761,8 +3077,11 @@ function buildStudioHtml(initialDocument: InitialStudioDocument | null, theme?:
|
|
|
2761
3077
|
if (first === "fortran" || first === "f90" || first === "f95" || first === "f03" || first === "f" || first === "for") return "fortran";
|
|
2762
3078
|
if (first === "r") return "r";
|
|
2763
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";
|
|
2764
3082
|
|
|
2765
|
-
|
|
3083
|
+
// Fall back to the unified extension->language map
|
|
3084
|
+
return EXT_TO_LANG[first] || "";
|
|
2766
3085
|
}
|
|
2767
3086
|
|
|
2768
3087
|
function highlightCodeTokens(line, pattern, classifyMatch) {
|
|
@@ -2927,6 +3246,31 @@ function buildStudioHtml(initialDocument: InitialStudioDocument | null, theme?:
|
|
|
2927
3246
|
return "<span class='hl-code'>" + highlighted + "</span>";
|
|
2928
3247
|
}
|
|
2929
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
|
+
|
|
2930
3274
|
return wrapHighlight("hl-code", source);
|
|
2931
3275
|
}
|
|
2932
3276
|
|
|
@@ -3013,23 +3357,10 @@ function buildStudioHtml(initialDocument: InitialStudioDocument | null, theme?:
|
|
|
3013
3357
|
|
|
3014
3358
|
function detectLanguageFromName(name) {
|
|
3015
3359
|
if (!name) return "";
|
|
3016
|
-
|
|
3360
|
+
var dot = name.lastIndexOf(".");
|
|
3017
3361
|
if (dot < 0) return "";
|
|
3018
|
-
|
|
3019
|
-
|
|
3020
|
-
if (ext === "ts" || ext === "mts" || ext === "cts" || ext === "tsx") return "typescript";
|
|
3021
|
-
if (ext === "py" || ext === "pyw") return "python";
|
|
3022
|
-
if (ext === "sh" || ext === "bash" || ext === "zsh") return "bash";
|
|
3023
|
-
if (ext === "json" || ext === "jsonc" || ext === "json5") return "json";
|
|
3024
|
-
if (ext === "rs") return "rust";
|
|
3025
|
-
if (ext === "c" || ext === "h") return "c";
|
|
3026
|
-
if (ext === "cpp" || ext === "cxx" || ext === "cc" || ext === "hpp" || ext === "hxx") return "cpp";
|
|
3027
|
-
if (ext === "jl") return "julia";
|
|
3028
|
-
if (ext === "f90" || ext === "f95" || ext === "f03" || ext === "f" || ext === "for") return "fortran";
|
|
3029
|
-
if (ext === "r" || ext === "R") return "r";
|
|
3030
|
-
if (ext === "m") return "matlab";
|
|
3031
|
-
if (ext === "md" || ext === "markdown" || ext === "mdx") return "markdown";
|
|
3032
|
-
return "";
|
|
3362
|
+
var ext = name.slice(dot + 1).toLowerCase();
|
|
3363
|
+
return EXT_TO_LANG[ext] || "";
|
|
3033
3364
|
}
|
|
3034
3365
|
|
|
3035
3366
|
function renderEditorHighlightNow() {
|
|
@@ -3473,6 +3804,17 @@ function buildStudioHtml(initialDocument: InitialStudioDocument | null, theme?:
|
|
|
3473
3804
|
setStatus(message.message);
|
|
3474
3805
|
}
|
|
3475
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
|
+
}
|
|
3476
3818
|
}
|
|
3477
3819
|
|
|
3478
3820
|
function connect() {
|
|
@@ -3748,8 +4090,8 @@ function buildStudioHtml(initialDocument: InitialStudioDocument | null, theme?:
|
|
|
3748
4090
|
|
|
3749
4091
|
sourceTextEl.value = latestResponseMarkdown;
|
|
3750
4092
|
renderSourcePreview();
|
|
3751
|
-
setSourceState({ source: "blank", label: "critique
|
|
3752
|
-
setStatus("Loaded critique
|
|
4093
|
+
setSourceState({ source: "blank", label: "full critique", path: null });
|
|
4094
|
+
setStatus("Loaded full critique into editor.", "success");
|
|
3753
4095
|
});
|
|
3754
4096
|
|
|
3755
4097
|
copyResponseBtn.addEventListener("click", async () => {
|
|
@@ -3773,8 +4115,10 @@ function buildStudioHtml(initialDocument: InitialStudioDocument | null, theme?:
|
|
|
3773
4115
|
return;
|
|
3774
4116
|
}
|
|
3775
4117
|
|
|
3776
|
-
|
|
3777
|
-
|
|
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);
|
|
3778
4122
|
if (!path) return;
|
|
3779
4123
|
|
|
3780
4124
|
const requestId = beginUiAction("save_as");
|
|
@@ -3795,21 +4139,24 @@ function buildStudioHtml(initialDocument: InitialStudioDocument | null, theme?:
|
|
|
3795
4139
|
});
|
|
3796
4140
|
|
|
3797
4141
|
saveOverBtn.addEventListener("click", () => {
|
|
3798
|
-
|
|
3799
|
-
|
|
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");
|
|
3800
4145
|
return;
|
|
3801
4146
|
}
|
|
3802
4147
|
|
|
3803
|
-
if (!window.confirm("Overwrite " +
|
|
4148
|
+
if (!window.confirm("Overwrite " + effectivePath + "?")) {
|
|
3804
4149
|
return;
|
|
3805
4150
|
}
|
|
3806
4151
|
|
|
3807
4152
|
const requestId = beginUiAction("save_over");
|
|
3808
4153
|
if (!requestId) return;
|
|
3809
4154
|
|
|
4155
|
+
// Use save_as with the effective path for both file-backed and derived paths
|
|
3810
4156
|
const sent = sendMessage({
|
|
3811
|
-
type: "
|
|
4157
|
+
type: "save_as_request",
|
|
3812
4158
|
requestId,
|
|
4159
|
+
path: effectivePath,
|
|
3813
4160
|
content: sourceTextEl.value,
|
|
3814
4161
|
});
|
|
3815
4162
|
|
|
@@ -3881,6 +4228,67 @@ function buildStudioHtml(initialDocument: InitialStudioDocument | null, theme?:
|
|
|
3881
4228
|
}
|
|
3882
4229
|
});
|
|
3883
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
|
+
|
|
3884
4292
|
fileInput.addEventListener("change", () => {
|
|
3885
4293
|
const file = fileInput.files && fileInput.files[0];
|
|
3886
4294
|
if (!file) return;
|
|
@@ -3944,6 +4352,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
3944
4352
|
let initialStudioDocument: InitialStudioDocument | null = null;
|
|
3945
4353
|
let studioCwd = process.cwd();
|
|
3946
4354
|
let lastCommandCtx: ExtensionCommandContext | null = null;
|
|
4355
|
+
let lastThemeVarsJson = "";
|
|
3947
4356
|
let agentBusy = false;
|
|
3948
4357
|
|
|
3949
4358
|
const isStudioBusy = () => agentBusy || activeRequest !== null;
|
|
@@ -4315,7 +4724,17 @@ export default function (pi: ExtensionAPI) {
|
|
|
4315
4724
|
}
|
|
4316
4725
|
|
|
4317
4726
|
try {
|
|
4318
|
-
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);
|
|
4319
4738
|
respondJson(res, 200, { ok: true, html, renderer: "pandoc" });
|
|
4320
4739
|
} catch (error) {
|
|
4321
4740
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -4482,6 +4901,27 @@ export default function (pi: ExtensionAPI) {
|
|
|
4482
4901
|
state.port = address.port;
|
|
4483
4902
|
|
|
4484
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
|
+
|
|
4485
4925
|
return state;
|
|
4486
4926
|
};
|
|
4487
4927
|
|
|
@@ -4630,6 +5070,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
4630
5070
|
await ctx.waitForIdle();
|
|
4631
5071
|
lastCommandCtx = ctx;
|
|
4632
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 */ }
|
|
4633
5078
|
|
|
4634
5079
|
const latestAssistant =
|
|
4635
5080
|
extractLatestAssistantFromEntries(ctx.sessionManager.getBranch())
|