pi-studio 0.1.3 → 0.1.5
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 +4 -1
- package/README.md +3 -1
- package/WORKFLOW.md +2 -2
- package/index.ts +105 -16
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -12,7 +12,7 @@ All notable changes to `pi-studio` are documented here.
|
|
|
12
12
|
- **Load response into editor** (for non-critique responses)
|
|
13
13
|
- **Load critique (notes)**
|
|
14
14
|
- **Load critique (full)**
|
|
15
|
-
- **Copy response**
|
|
15
|
+
- **Copy response text**
|
|
16
16
|
- Independent Markdown/Preview toggles for Editor and right pane.
|
|
17
17
|
- `Auto-update response: On|Off` + `Get latest response` controls for terminal/editor-composability.
|
|
18
18
|
- Source action: **Run editor text** to submit current editor text directly to the model.
|
|
@@ -22,6 +22,8 @@ All notable changes to `pi-studio` are documented here.
|
|
|
22
22
|
- Math delimiter normalization before preview rendering for `\(...\)` and `\[...\]` syntax (fence-aware).
|
|
23
23
|
- **Load file in editor** action in top controls (browser file picker into editor).
|
|
24
24
|
- README screenshot gallery for dark/light workspace and critique/annotation views.
|
|
25
|
+
- Response-side markdown highlighting toggle (`Highlight markdown: Off|On`) in `Response: Markdown` view, with local preference persistence.
|
|
26
|
+
- Obsidian wiki-image syntax normalization (`![[path]]`, `![[path|alt]]`) before pandoc preview rendering.
|
|
25
27
|
|
|
26
28
|
### Changed
|
|
27
29
|
- Removed Annotate/Critique tabs and related mode state.
|
|
@@ -30,6 +32,7 @@ All notable changes to `pi-studio` are documented here.
|
|
|
30
32
|
- Editor sync badge now tracks relation to latest response (`No response loaded`, `In sync with response`, `Edited since response`).
|
|
31
33
|
- Footer continues to show explicit WS phase (`Connecting`, `Ready`, `Submitting`, `Disconnected`) alongside status text.
|
|
32
34
|
- Running text and preparing annotated scaffolds are now separate explicit actions (no hidden header wrapping on send).
|
|
35
|
+
- Renamed file-backed header action from **Save Over** to **Save file**, with tooltip showing the current overwrite target.
|
|
33
36
|
- Critique-specific load actions now focus on notes/full views and are only shown for structured critique responses.
|
|
34
37
|
- Studio still live-updates latest response when assistant output arrives outside studio requests (e.g., manual send from pi editor).
|
|
35
38
|
- Preview pane typography/style now follows the higher-fidelity `/preview-browser` rendering style more closely.
|
package/README.md
CHANGED
|
@@ -31,8 +31,9 @@ Status: experimental alpha.
|
|
|
31
31
|
- Response load helpers:
|
|
32
32
|
- non-critique: **Load response into editor**
|
|
33
33
|
- critique: **Load critique (notes)** / **Load critique (full)**
|
|
34
|
-
- File actions: **Save As…**, **Save
|
|
34
|
+
- File actions: **Save As…**, **Save file**, **Load file in editor**
|
|
35
35
|
- View toggles: `Editor: Markdown|Preview`, `Response: Markdown|Preview`
|
|
36
|
+
- Optional markdown highlighting toggles for editor and response markdown views
|
|
36
37
|
- Theme-aware browser UI based on current pi theme
|
|
37
38
|
|
|
38
39
|
## Commands
|
|
@@ -73,6 +74,7 @@ pi -e https://github.com/omaclaren/pi-studio
|
|
|
73
74
|
- One studio request at a time.
|
|
74
75
|
- Studio URLs include a token query parameter; avoid sharing full Studio URLs.
|
|
75
76
|
- Preview panes render markdown via `pandoc` (`gfm+tex_math_dollars` → HTML5 + MathML), sanitized in-browser with `dompurify`.
|
|
77
|
+
- Preview rendering normalizes Obsidian wiki-image syntax (`![[path]]`, `![[path|alt]]`) into standard markdown images.
|
|
76
78
|
- Install pandoc for full preview rendering (`brew install pandoc` on macOS).
|
|
77
79
|
- If `pandoc` is unavailable, preview falls back to plain markdown text with an inline warning.
|
|
78
80
|
- Some screenshots may show the Grammarly browser widget. Grammarly is a separate browser extension and is not part of pi-studio, but it works alongside it.
|
package/WORKFLOW.md
CHANGED
|
@@ -74,10 +74,10 @@ Rules:
|
|
|
74
74
|
|
|
75
75
|
## Required UI elements
|
|
76
76
|
|
|
77
|
-
- Header actions: **Save As…**, **Save
|
|
77
|
+
- Header actions: **Save As…**, **Save file** (file-backed), **Load file in editor**
|
|
78
78
|
- Header view toggles: `Editor: Markdown|Preview`, `Response: Markdown|Preview`
|
|
79
79
|
- Preview mode uses server-side `pandoc` rendering (math-aware) with plain-markdown fallback when renderer is unavailable.
|
|
80
|
-
- Editor actions: **Insert annotation header**, **Run editor text**, **Critique editor text** (+ critique focus), **Send to pi editor**, **Copy editor**
|
|
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**
|
|
82
82
|
- Source badge: `blank | last model response | file <path> | upload`
|
|
83
83
|
- Response badge: `none | assistant response | assistant critique` (+ timestamp)
|
package/index.ts
CHANGED
|
@@ -491,10 +491,16 @@ function stripMathMlAnnotationTags(html: string): string {
|
|
|
491
491
|
.replace(/<annotation\b[\s\S]*?<\/annotation>/gi, "");
|
|
492
492
|
}
|
|
493
493
|
|
|
494
|
+
function normalizeObsidianImages(markdown: string): string {
|
|
495
|
+
return markdown
|
|
496
|
+
.replace(/!\[\[([^|\]]+)\|([^\]]+)\]\]/g, "")
|
|
497
|
+
.replace(/!\[\[([^\]]+)\]\]/g, "");
|
|
498
|
+
}
|
|
499
|
+
|
|
494
500
|
async function renderStudioMarkdownWithPandoc(markdown: string): Promise<string> {
|
|
495
501
|
const pandocCommand = process.env.PANDOC_PATH?.trim() || "pandoc";
|
|
496
502
|
const args = ["-f", "gfm+tex_math_dollars-raw_html", "-t", "html5", "--mathml", "--no-highlight"];
|
|
497
|
-
const normalizedMarkdown = normalizeMathDelimiters(markdown);
|
|
503
|
+
const normalizedMarkdown = normalizeObsidianImages(normalizeMathDelimiters(markdown));
|
|
498
504
|
|
|
499
505
|
return await new Promise<string>((resolve, reject) => {
|
|
500
506
|
const child = spawn(pandocCommand, args, { stdio: ["pipe", "pipe", "pipe"] });
|
|
@@ -1362,6 +1368,15 @@ function buildStudioHtml(initialDocument: InitialStudioDocument | null, theme?:
|
|
|
1362
1368
|
line-height: 1.5;
|
|
1363
1369
|
}
|
|
1364
1370
|
|
|
1371
|
+
.response-markdown-highlight {
|
|
1372
|
+
margin: 0;
|
|
1373
|
+
white-space: pre-wrap;
|
|
1374
|
+
word-break: break-word;
|
|
1375
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
|
1376
|
+
font-size: 13px;
|
|
1377
|
+
line-height: 1.5;
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1365
1380
|
.preview-loading {
|
|
1366
1381
|
color: var(--muted);
|
|
1367
1382
|
font-style: italic;
|
|
@@ -1462,9 +1477,9 @@ function buildStudioHtml(initialDocument: InitialStudioDocument | null, theme?:
|
|
|
1462
1477
|
<option value="markdown">Response: Markdown</option>
|
|
1463
1478
|
<option value="preview" selected>Response: Preview</option>
|
|
1464
1479
|
</select>
|
|
1465
|
-
<button id="saveAsBtn" type="button">Save As…</button>
|
|
1466
|
-
<button id="saveOverBtn" type="button" disabled>Save
|
|
1467
|
-
<label class="file-label">Load file in editor<input id="fileInput" type="file" accept=".txt,.md,.markdown,.rst,.adoc,.tex,.json,.js,.ts,.py,.java,.c,.cpp,.go,.rs,.rb,.swift,.sh,.html,.css,.xml,.yaml,.yml,.toml" /></label>
|
|
1480
|
+
<button id="saveAsBtn" type="button" title="Save editor text to a new file path.">Save As…</button>
|
|
1481
|
+
<button id="saveOverBtn" type="button" title="Overwrite current file with editor text." disabled>Save file</button>
|
|
1482
|
+
<label class="file-label" title="Load a local file into editor text.">Load file in editor<input id="fileInput" type="file" accept=".txt,.md,.markdown,.rst,.adoc,.tex,.json,.js,.ts,.py,.java,.c,.cpp,.go,.rs,.rb,.swift,.sh,.html,.css,.xml,.yaml,.yml,.toml" /></label>
|
|
1468
1483
|
</div>
|
|
1469
1484
|
</header>
|
|
1470
1485
|
|
|
@@ -1487,10 +1502,10 @@ function buildStudioHtml(initialDocument: InitialStudioDocument | null, theme?:
|
|
|
1487
1502
|
</select>
|
|
1488
1503
|
<button id="critiqueBtn" type="button">Critique editor text</button>
|
|
1489
1504
|
<button id="sendEditorBtn" type="button">Send to pi editor</button>
|
|
1490
|
-
<button id="copyDraftBtn" type="button">Copy editor</button>
|
|
1505
|
+
<button id="copyDraftBtn" type="button">Copy editor text</button>
|
|
1491
1506
|
<select id="highlightSelect" aria-label="Editor syntax highlighting">
|
|
1492
|
-
<option value="off" selected>Highlight
|
|
1493
|
-
<option value="on">Highlight
|
|
1507
|
+
<option value="off" selected>Highlight markdown: Off</option>
|
|
1508
|
+
<option value="on">Highlight markdown: On</option>
|
|
1494
1509
|
</select>
|
|
1495
1510
|
</div>
|
|
1496
1511
|
</div>
|
|
@@ -1514,11 +1529,15 @@ function buildStudioHtml(initialDocument: InitialStudioDocument | null, theme?:
|
|
|
1514
1529
|
<option value="on" selected>Auto-update response: On</option>
|
|
1515
1530
|
<option value="off">Auto-update response: Off</option>
|
|
1516
1531
|
</select>
|
|
1532
|
+
<select id="responseHighlightSelect" aria-label="Response markdown highlighting">
|
|
1533
|
+
<option value="off" selected>Highlight markdown: Off</option>
|
|
1534
|
+
<option value="on">Highlight markdown: On</option>
|
|
1535
|
+
</select>
|
|
1517
1536
|
<button id="pullLatestBtn" type="button" title="Fetch the latest assistant response when auto-update is off.">Get latest response</button>
|
|
1518
1537
|
<button id="loadResponseBtn" type="button">Load response into editor</button>
|
|
1519
1538
|
<button id="loadCritiqueNotesBtn" type="button" hidden>Load critique (notes)</button>
|
|
1520
1539
|
<button id="loadCritiqueFullBtn" type="button" hidden>Load critique (full)</button>
|
|
1521
|
-
<button id="copyResponseBtn" type="button">Copy response</button>
|
|
1540
|
+
<button id="copyResponseBtn" type="button">Copy response text</button>
|
|
1522
1541
|
</div>
|
|
1523
1542
|
</div>
|
|
1524
1543
|
</section>
|
|
@@ -1570,6 +1589,7 @@ function buildStudioHtml(initialDocument: InitialStudioDocument | null, theme?:
|
|
|
1570
1589
|
const editorViewSelect = document.getElementById("editorViewSelect");
|
|
1571
1590
|
const rightViewSelect = document.getElementById("rightViewSelect");
|
|
1572
1591
|
const followSelect = document.getElementById("followSelect");
|
|
1592
|
+
const responseHighlightSelect = document.getElementById("responseHighlightSelect");
|
|
1573
1593
|
const pullLatestBtn = document.getElementById("pullLatestBtn");
|
|
1574
1594
|
const insertHeaderBtn = document.getElementById("insertHeaderBtn");
|
|
1575
1595
|
const critiqueBtn = document.getElementById("critiqueBtn");
|
|
@@ -1617,10 +1637,13 @@ function buildStudioHtml(initialDocument: InitialStudioDocument | null, theme?:
|
|
|
1617
1637
|
let paneFocusTarget = "off";
|
|
1618
1638
|
const EDITOR_HIGHLIGHT_MAX_CHARS = 80_000;
|
|
1619
1639
|
const EDITOR_HIGHLIGHT_STORAGE_KEY = "piStudio.editorHighlightEnabled";
|
|
1640
|
+
const RESPONSE_HIGHLIGHT_MAX_CHARS = 120_000;
|
|
1641
|
+
const RESPONSE_HIGHLIGHT_STORAGE_KEY = "piStudio.responseHighlightEnabled";
|
|
1620
1642
|
let sourcePreviewRenderTimer = null;
|
|
1621
1643
|
let sourcePreviewRenderNonce = 0;
|
|
1622
1644
|
let responsePreviewRenderNonce = 0;
|
|
1623
1645
|
let editorHighlightEnabled = false;
|
|
1646
|
+
let responseHighlightEnabled = false;
|
|
1624
1647
|
let editorHighlightRenderRaf = null;
|
|
1625
1648
|
|
|
1626
1649
|
function getIdleStatus() {
|
|
@@ -1946,6 +1969,19 @@ function buildStudioHtml(initialDocument: InitialStudioDocument | null, theme?:
|
|
|
1946
1969
|
return;
|
|
1947
1970
|
}
|
|
1948
1971
|
|
|
1972
|
+
if (responseHighlightEnabled) {
|
|
1973
|
+
if (markdown.length > RESPONSE_HIGHLIGHT_MAX_CHARS) {
|
|
1974
|
+
critiqueViewEl.innerHTML = buildPreviewErrorHtml(
|
|
1975
|
+
"Response is too large for markdown highlighting. Showing plain markdown.",
|
|
1976
|
+
markdown,
|
|
1977
|
+
);
|
|
1978
|
+
return;
|
|
1979
|
+
}
|
|
1980
|
+
|
|
1981
|
+
critiqueViewEl.innerHTML = "<div class='response-markdown-highlight'>" + highlightMarkdown(markdown) + "</div>";
|
|
1982
|
+
return;
|
|
1983
|
+
}
|
|
1984
|
+
|
|
1949
1985
|
critiqueViewEl.innerHTML = buildPlainMarkdownHtml(markdown);
|
|
1950
1986
|
}
|
|
1951
1987
|
|
|
@@ -1994,10 +2030,25 @@ function buildStudioHtml(initialDocument: InitialStudioDocument | null, theme?:
|
|
|
1994
2030
|
updateResultActionButtons();
|
|
1995
2031
|
}
|
|
1996
2032
|
|
|
2033
|
+
function updateSaveFileTooltip() {
|
|
2034
|
+
if (!saveOverBtn) return;
|
|
2035
|
+
|
|
2036
|
+
const isFileBacked = sourceState.source === "file" && Boolean(sourceState.path);
|
|
2037
|
+
if (isFileBacked) {
|
|
2038
|
+
const target = sourceState.label || sourceState.path;
|
|
2039
|
+
saveOverBtn.title = "Overwrite current file: " + target;
|
|
2040
|
+
return;
|
|
2041
|
+
}
|
|
2042
|
+
|
|
2043
|
+
saveOverBtn.title = "Save file is available after opening a file or using Save As…";
|
|
2044
|
+
}
|
|
2045
|
+
|
|
1997
2046
|
function syncActionButtons() {
|
|
2047
|
+
const canSaveOver = sourceState.source === "file" && Boolean(sourceState.path);
|
|
2048
|
+
|
|
1998
2049
|
fileInput.disabled = uiBusy;
|
|
1999
2050
|
saveAsBtn.disabled = uiBusy;
|
|
2000
|
-
saveOverBtn.disabled = uiBusy || !
|
|
2051
|
+
saveOverBtn.disabled = uiBusy || !canSaveOver;
|
|
2001
2052
|
sendEditorBtn.disabled = uiBusy;
|
|
2002
2053
|
sendRunBtn.disabled = uiBusy;
|
|
2003
2054
|
copyDraftBtn.disabled = uiBusy;
|
|
@@ -2005,9 +2056,11 @@ function buildStudioHtml(initialDocument: InitialStudioDocument | null, theme?:
|
|
|
2005
2056
|
editorViewSelect.disabled = uiBusy;
|
|
2006
2057
|
rightViewSelect.disabled = uiBusy;
|
|
2007
2058
|
followSelect.disabled = uiBusy;
|
|
2059
|
+
if (responseHighlightSelect) responseHighlightSelect.disabled = uiBusy || rightView !== "markdown";
|
|
2008
2060
|
insertHeaderBtn.disabled = uiBusy;
|
|
2009
2061
|
critiqueBtn.disabled = uiBusy;
|
|
2010
2062
|
lensSelect.disabled = uiBusy;
|
|
2063
|
+
updateSaveFileTooltip();
|
|
2011
2064
|
updateResultActionButtons();
|
|
2012
2065
|
}
|
|
2013
2066
|
|
|
@@ -2052,6 +2105,7 @@ function buildStudioHtml(initialDocument: InitialStudioDocument | null, theme?:
|
|
|
2052
2105
|
rightView = nextView === "preview" ? "preview" : "markdown";
|
|
2053
2106
|
rightViewSelect.value = rightView;
|
|
2054
2107
|
renderActiveResult();
|
|
2108
|
+
syncActionButtons();
|
|
2055
2109
|
}
|
|
2056
2110
|
|
|
2057
2111
|
function getToken() {
|
|
@@ -2225,24 +2279,40 @@ function buildStudioHtml(initialDocument: InitialStudioDocument | null, theme?:
|
|
|
2225
2279
|
sourceHighlightEl.scrollLeft = sourceTextEl.scrollLeft;
|
|
2226
2280
|
}
|
|
2227
2281
|
|
|
2228
|
-
function
|
|
2282
|
+
function readStoredToggle(storageKey) {
|
|
2229
2283
|
if (!window.localStorage) return false;
|
|
2230
2284
|
try {
|
|
2231
|
-
return window.localStorage.getItem(
|
|
2285
|
+
return window.localStorage.getItem(storageKey) === "on";
|
|
2232
2286
|
} catch {
|
|
2233
2287
|
return false;
|
|
2234
2288
|
}
|
|
2235
2289
|
}
|
|
2236
2290
|
|
|
2237
|
-
function
|
|
2291
|
+
function persistStoredToggle(storageKey, enabled) {
|
|
2238
2292
|
if (!window.localStorage) return;
|
|
2239
2293
|
try {
|
|
2240
|
-
window.localStorage.setItem(
|
|
2294
|
+
window.localStorage.setItem(storageKey, enabled ? "on" : "off");
|
|
2241
2295
|
} catch {
|
|
2242
2296
|
// ignore storage failures
|
|
2243
2297
|
}
|
|
2244
2298
|
}
|
|
2245
2299
|
|
|
2300
|
+
function readStoredEditorHighlightEnabled() {
|
|
2301
|
+
return readStoredToggle(EDITOR_HIGHLIGHT_STORAGE_KEY);
|
|
2302
|
+
}
|
|
2303
|
+
|
|
2304
|
+
function readStoredResponseHighlightEnabled() {
|
|
2305
|
+
return readStoredToggle(RESPONSE_HIGHLIGHT_STORAGE_KEY);
|
|
2306
|
+
}
|
|
2307
|
+
|
|
2308
|
+
function persistEditorHighlightEnabled(enabled) {
|
|
2309
|
+
persistStoredToggle(EDITOR_HIGHLIGHT_STORAGE_KEY, enabled);
|
|
2310
|
+
}
|
|
2311
|
+
|
|
2312
|
+
function persistResponseHighlightEnabled(enabled) {
|
|
2313
|
+
persistStoredToggle(RESPONSE_HIGHLIGHT_STORAGE_KEY, enabled);
|
|
2314
|
+
}
|
|
2315
|
+
|
|
2246
2316
|
function updateEditorHighlightState() {
|
|
2247
2317
|
const enabled = editorHighlightEnabled && editorView === "markdown";
|
|
2248
2318
|
|
|
@@ -2283,6 +2353,15 @@ function buildStudioHtml(initialDocument: InitialStudioDocument | null, theme?:
|
|
|
2283
2353
|
updateEditorHighlightState();
|
|
2284
2354
|
}
|
|
2285
2355
|
|
|
2356
|
+
function setResponseHighlightEnabled(enabled) {
|
|
2357
|
+
responseHighlightEnabled = Boolean(enabled);
|
|
2358
|
+
persistResponseHighlightEnabled(responseHighlightEnabled);
|
|
2359
|
+
if (responseHighlightSelect) {
|
|
2360
|
+
responseHighlightSelect.value = responseHighlightEnabled ? "on" : "off";
|
|
2361
|
+
}
|
|
2362
|
+
renderActiveResult();
|
|
2363
|
+
}
|
|
2364
|
+
|
|
2286
2365
|
function extractSection(markdown, title) {
|
|
2287
2366
|
if (!markdown || !title) return "";
|
|
2288
2367
|
|
|
@@ -2726,6 +2805,12 @@ function buildStudioHtml(initialDocument: InitialStudioDocument | null, theme?:
|
|
|
2726
2805
|
});
|
|
2727
2806
|
}
|
|
2728
2807
|
|
|
2808
|
+
if (responseHighlightSelect) {
|
|
2809
|
+
responseHighlightSelect.addEventListener("change", () => {
|
|
2810
|
+
setResponseHighlightEnabled(responseHighlightSelect.value === "on");
|
|
2811
|
+
});
|
|
2812
|
+
}
|
|
2813
|
+
|
|
2729
2814
|
pullLatestBtn.addEventListener("click", () => {
|
|
2730
2815
|
if (queuedLatestResponse) {
|
|
2731
2816
|
if (applyLatestPayload(queuedLatestResponse)) {
|
|
@@ -2825,7 +2910,7 @@ function buildStudioHtml(initialDocument: InitialStudioDocument | null, theme?:
|
|
|
2825
2910
|
|
|
2826
2911
|
try {
|
|
2827
2912
|
await navigator.clipboard.writeText(latestResponseMarkdown);
|
|
2828
|
-
setStatus("Copied response.", "success");
|
|
2913
|
+
setStatus("Copied response text.", "success");
|
|
2829
2914
|
} catch (error) {
|
|
2830
2915
|
setStatus("Clipboard write failed.", "warning");
|
|
2831
2916
|
}
|
|
@@ -2861,7 +2946,7 @@ function buildStudioHtml(initialDocument: InitialStudioDocument | null, theme?:
|
|
|
2861
2946
|
|
|
2862
2947
|
saveOverBtn.addEventListener("click", () => {
|
|
2863
2948
|
if (!(sourceState.source === "file" && sourceState.path)) {
|
|
2864
|
-
setStatus("Save
|
|
2949
|
+
setStatus("Save file is only available when source is a file path.", "warning");
|
|
2865
2950
|
return;
|
|
2866
2951
|
}
|
|
2867
2952
|
|
|
@@ -2977,6 +3062,10 @@ function buildStudioHtml(initialDocument: InitialStudioDocument | null, theme?:
|
|
|
2977
3062
|
|| Boolean(highlightSelect && highlightSelect.value === "on");
|
|
2978
3063
|
setEditorHighlightEnabled(initialHighlightEnabled);
|
|
2979
3064
|
|
|
3065
|
+
const initialResponseHighlightEnabled = readStoredResponseHighlightEnabled()
|
|
3066
|
+
|| Boolean(responseHighlightSelect && responseHighlightSelect.value === "on");
|
|
3067
|
+
setResponseHighlightEnabled(initialResponseHighlightEnabled);
|
|
3068
|
+
|
|
2980
3069
|
setEditorView(editorView);
|
|
2981
3070
|
setRightView(rightView);
|
|
2982
3071
|
renderSourcePreview();
|
|
@@ -3260,7 +3349,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
3260
3349
|
sendToClient(client, {
|
|
3261
3350
|
type: "error",
|
|
3262
3351
|
requestId: msg.requestId,
|
|
3263
|
-
message: "Save
|
|
3352
|
+
message: "Save file is only available for file-backed documents.",
|
|
3264
3353
|
});
|
|
3265
3354
|
return;
|
|
3266
3355
|
}
|