pi-studio 0.5.51 → 0.5.53

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/index.ts CHANGED
@@ -20,6 +20,7 @@ import {
20
20
  transformStudioMarkdownOutsideFences,
21
21
  } from "./shared/studio-annotation-scanner.js";
22
22
  import { stripStudioMarkdownHtmlComments } from "./shared/studio-markdown-html-comments.js";
23
+ import { preserveLiteralLatexCommandsInMarkdown } from "./shared/studio-markdown-latex-literals.js";
23
24
  import { escapeStudioPdfLatexTextFragment } from "./shared/studio-pdf-escape.js";
24
25
 
25
26
  type Lens = "writing" | "code";
@@ -132,6 +133,46 @@ interface StudioPersistentState {
132
133
  reviewNotesByDocument: Record<string, PersistedStudioReviewNote[]>;
133
134
  }
134
135
 
136
+ type StudioTraceRunStatus = "idle" | "running" | "complete";
137
+ type StudioTraceEntryStatus = "streaming" | "pending" | "complete" | "error";
138
+
139
+ interface StudioTraceAssistantEntry {
140
+ id: string;
141
+ type: "assistant";
142
+ startedAt: number;
143
+ updatedAt: number;
144
+ thinking: string;
145
+ text: string;
146
+ status: StudioTraceEntryStatus;
147
+ stopReason: string | null;
148
+ }
149
+
150
+ interface StudioTraceToolEntry {
151
+ id: string;
152
+ type: "tool";
153
+ toolCallId: string;
154
+ toolName: string;
155
+ label: string | null;
156
+ argsSummary: string | null;
157
+ output: string;
158
+ startedAt: number;
159
+ updatedAt: number;
160
+ status: StudioTraceEntryStatus;
161
+ isError: boolean;
162
+ }
163
+
164
+ type StudioTraceEntry = StudioTraceAssistantEntry | StudioTraceToolEntry;
165
+
166
+ interface StudioTraceState {
167
+ runId: string | null;
168
+ requestId: string | null;
169
+ requestKind: string | null;
170
+ status: StudioTraceRunStatus;
171
+ startedAt: number | null;
172
+ updatedAt: number | null;
173
+ entries: StudioTraceEntry[];
174
+ }
175
+
135
176
  interface HelloMessage {
136
177
  type: "hello";
137
178
  }
@@ -3953,7 +3994,7 @@ function prepareStudioPdfMarkdown(markdown: string, isLatex?: boolean, editorLan
3953
3994
  ? replaceStudioAnnotationMarkersForPdf(source)
3954
3995
  : source;
3955
3996
  const commentStrippedSource = stripStudioMarkdownHtmlComments(annotationReadySource);
3956
- return normalizeObsidianImages(normalizeMathDelimiters(commentStrippedSource));
3997
+ return normalizeObsidianImages(preserveLiteralLatexCommandsInMarkdown(normalizeMathDelimiters(commentStrippedSource)));
3957
3998
  }
3958
3999
 
3959
4000
  function stripMathMlAnnotationTags(html: string): string {
@@ -4145,7 +4186,7 @@ async function renderStudioMarkdownWithPandoc(markdown: string, isLatex?: boolea
4145
4186
  }
4146
4187
  const normalizedMarkdown = isLatex
4147
4188
  ? sourceWithResolvedRefs
4148
- : normalizeStudioMarkdownFencedBlocks(normalizeObsidianImages(normalizeMathDelimiters(sourceWithResolvedRefs)));
4189
+ : normalizeStudioMarkdownFencedBlocks(normalizeObsidianImages(preserveLiteralLatexCommandsInMarkdown(normalizeMathDelimiters(sourceWithResolvedRefs))));
4149
4190
  const pandocWorkingDir = resolveStudioPandocWorkingDir(resourcePath);
4150
4191
 
4151
4192
  let renderedHtml = await new Promise<string>((resolve, reject) => {
@@ -5709,6 +5750,72 @@ function deriveToolActivityLabel(toolName: string, args: unknown): string | null
5709
5750
  return normalizeActivityLabel(`Running ${normalizedTool || "tool"}`);
5710
5751
  }
5711
5752
 
5753
+ function createEmptyStudioTraceState(): StudioTraceState {
5754
+ return {
5755
+ runId: null,
5756
+ requestId: null,
5757
+ requestKind: null,
5758
+ status: "idle",
5759
+ startedAt: null,
5760
+ updatedAt: null,
5761
+ entries: [],
5762
+ };
5763
+ }
5764
+
5765
+ function formatStudioTraceOutput(result: unknown): string {
5766
+ if (result == null) return "";
5767
+ if (typeof result === "string") return result;
5768
+ if (Array.isArray(result)) {
5769
+ return result.map((item) => formatStudioTraceOutput(item)).filter(Boolean).join("\n");
5770
+ }
5771
+ if (typeof result === "object") {
5772
+ const payload = result as { content?: Array<{ type?: string; text?: string }> };
5773
+ if (Array.isArray(payload.content)) {
5774
+ return payload.content
5775
+ .map((block) => {
5776
+ if (block && block.type === "text" && typeof block.text === "string") return block.text;
5777
+ try {
5778
+ return JSON.stringify(block, null, 2);
5779
+ } catch {
5780
+ return String(block);
5781
+ }
5782
+ })
5783
+ .filter(Boolean)
5784
+ .join("\n");
5785
+ }
5786
+ try {
5787
+ return JSON.stringify(result, null, 2);
5788
+ } catch {
5789
+ return String(result);
5790
+ }
5791
+ }
5792
+ return String(result);
5793
+ }
5794
+
5795
+ function summarizeStudioTraceToolArgs(toolName: string, args: unknown): string | null {
5796
+ const normalizedTool = String(toolName || "").trim().toLowerCase();
5797
+ const payload = (args && typeof args === "object") ? (args as Record<string, unknown>) : {};
5798
+ const trimSummary = (value: string | null | undefined): string | null => {
5799
+ const compact = normalizeActivityLabel(String(value || "").replace(/\s+/g, " ").trim());
5800
+ return compact && compact.length <= 220 ? compact : (compact ? `${compact.slice(0, 217).trimEnd()}…` : null);
5801
+ };
5802
+
5803
+ if (normalizedTool === "bash") {
5804
+ return trimSummary(typeof payload.command === "string" ? payload.command : "");
5805
+ }
5806
+ if (normalizedTool === "read" || normalizedTool === "write" || normalizedTool === "edit") {
5807
+ return trimSummary(typeof payload.path === "string" ? payload.path : "");
5808
+ }
5809
+ if (normalizedTool === "repl_send") {
5810
+ return trimSummary(typeof payload.code === "string" ? payload.code : "");
5811
+ }
5812
+ try {
5813
+ return trimSummary(JSON.stringify(args, null, 2));
5814
+ } catch {
5815
+ return trimSummary(String(args ?? ""));
5816
+ }
5817
+ }
5818
+
5712
5819
  function isAllowedOrigin(_origin: string | undefined, _port: number): boolean {
5713
5820
  // For local-only studio, token auth is the primary guard. In practice,
5714
5821
  // browser origin headers can vary (or be omitted) across wrappers/browsers,
@@ -6010,6 +6117,7 @@ ${cssVarsBlock}
6010
6117
  <div class="section-header-actions">
6011
6118
  <button id="leftFocusBtn" class="pane-focus-btn" type="button" title="Show only the editor pane. Shortcut: F10 or Cmd/Ctrl+Esc.">Focus pane</button>
6012
6119
  <button id="reviewNotesBtn" type="button" title="Toggle local comments beside the current editor document or draft. Comments stay outside the document text and can later be converted into [an: ...] annotations.">Comments</button>
6120
+ <button id="outlineBtn" type="button" title="Toggle document outline for the current editor text. Outline entries can jump between raw editor and preview.">Outline</button>
6013
6121
  <button id="scratchpadBtn" type="button" title="Open a local persistent scratchpad for the current editor document or draft. Scratchpad text is never run, critiqued, or exported unless you explicitly insert it into the editor.">Scratchpad</button>
6014
6122
  </div>
6015
6123
  </div>
@@ -6102,6 +6210,27 @@ ${cssVarsBlock}
6102
6210
  </div>
6103
6211
  <div id="sourcePreview" class="panel-scroll rendered-markdown" hidden><pre class="plain-markdown"></pre></div>
6104
6212
  </div>
6213
+ <aside id="outlineOverlay" class="outline-dock-wrap" hidden>
6214
+ <div id="outlineDialog" class="outline-dock" role="complementary" aria-labelledby="outlineTitle">
6215
+ <div class="scratchpad-header">
6216
+ <div>
6217
+ <h2 id="outlineTitle">Outline</h2>
6218
+ <p class="scratchpad-description">Document structure for the current editor text. Click an entry to jump in the raw editor and, when available, reveal the matching preview location.</p>
6219
+ </div>
6220
+ <button id="outlineCloseBtn" type="button" class="scratchpad-close-btn" aria-label="Hide outline" title="Hide outline">✕</button>
6221
+ </div>
6222
+ <div class="review-notes-toolbar">
6223
+ <span id="outlineMeta" class="scratchpad-meta">No outline entries</span>
6224
+ </div>
6225
+ <div id="outlineEmptyState" class="review-notes-empty">No outline available yet for this document or syntax mode.</div>
6226
+ <div id="outlineList" class="outline-list" aria-live="polite"></div>
6227
+ <div class="review-notes-dock-footer">
6228
+ <div class="scratchpad-actions">
6229
+ <button id="outlineDoneBtn" type="button" title="Hide the outline rail.">Hide</button>
6230
+ </div>
6231
+ </div>
6232
+ </div>
6233
+ </aside>
6105
6234
  <aside id="reviewNotesOverlay" class="review-notes-dock-wrap" hidden>
6106
6235
  <div id="reviewNotesDialog" class="review-notes-dock" role="complementary" aria-labelledby="reviewNotesTitle">
6107
6236
  <div class="scratchpad-header">
@@ -6136,7 +6265,7 @@ ${cssVarsBlock}
6136
6265
  <option value="markdown">Response (Raw)</option>
6137
6266
  <option value="preview" selected>Response (Preview)</option>
6138
6267
  <option value="editor-preview">Editor (Preview)</option>
6139
- <option value="thinking">Thinking (Raw)</option>
6268
+ <option value="trace">Working</option>
6140
6269
  </select>
6141
6270
  </div>
6142
6271
  <div class="section-header-actions">
@@ -6243,6 +6372,9 @@ export default function (pi: ExtensionAPI) {
6243
6372
  let studioResponseHistory: StudioResponseHistoryItem[] = [];
6244
6373
  let latestSessionUserPrompt: string | null = null;
6245
6374
  let pendingTurnPrompt: string | null = null;
6375
+ let studioTraceState: StudioTraceState = createEmptyStudioTraceState();
6376
+ let activeStudioTraceAssistantEntryId: string | null = null;
6377
+ const studioTraceToolEntryIds = new Map<string, string>();
6246
6378
  let contextUsageSnapshot: StudioContextUsageSnapshot = {
6247
6379
  tokens: null,
6248
6380
  contextWindow: null,
@@ -6687,6 +6819,184 @@ export default function (pi: ExtensionAPI) {
6687
6819
  });
6688
6820
  };
6689
6821
 
6822
+ const broadcastStudioTraceReset = () => {
6823
+ broadcast({
6824
+ type: "trace_reset",
6825
+ trace: studioTraceState,
6826
+ });
6827
+ };
6828
+
6829
+ const broadcastStudioTraceStatus = () => {
6830
+ broadcast({
6831
+ type: "trace_status",
6832
+ runId: studioTraceState.runId,
6833
+ requestId: studioTraceState.requestId,
6834
+ requestKind: studioTraceState.requestKind,
6835
+ status: studioTraceState.status,
6836
+ startedAt: studioTraceState.startedAt,
6837
+ updatedAt: studioTraceState.updatedAt,
6838
+ });
6839
+ };
6840
+
6841
+ const upsertStudioTraceEntry = (entry: StudioTraceEntry) => {
6842
+ const entryIndex = studioTraceState.entries.findIndex((candidate) => candidate.id === entry.id);
6843
+ if (entryIndex >= 0) {
6844
+ studioTraceState.entries[entryIndex] = entry;
6845
+ } else {
6846
+ studioTraceState.entries.push(entry);
6847
+ }
6848
+ studioTraceState.updatedAt = entry.updatedAt;
6849
+ broadcast({
6850
+ type: "trace_entry_upsert",
6851
+ entry,
6852
+ runId: studioTraceState.runId,
6853
+ });
6854
+ };
6855
+
6856
+ const resetStudioTraceForRun = () => {
6857
+ const now = Date.now();
6858
+ studioTraceState = {
6859
+ runId: randomUUID(),
6860
+ requestId: activeRequest?.id ?? null,
6861
+ requestKind: activeRequest?.kind ?? null,
6862
+ status: "running",
6863
+ startedAt: now,
6864
+ updatedAt: now,
6865
+ entries: [],
6866
+ };
6867
+ activeStudioTraceAssistantEntryId = null;
6868
+ studioTraceToolEntryIds.clear();
6869
+ broadcastStudioTraceReset();
6870
+ };
6871
+
6872
+ const setStudioTraceRunStatus = (status: StudioTraceRunStatus) => {
6873
+ if (studioTraceState.runId == null && status !== "idle") {
6874
+ resetStudioTraceForRun();
6875
+ }
6876
+ studioTraceState.status = status;
6877
+ studioTraceState.requestId = activeRequest?.id ?? studioTraceState.requestId ?? null;
6878
+ studioTraceState.requestKind = activeRequest?.kind ?? studioTraceState.requestKind ?? null;
6879
+ studioTraceState.updatedAt = Date.now();
6880
+ broadcastStudioTraceStatus();
6881
+ };
6882
+
6883
+ const ensureStudioTraceAssistantEntry = (): StudioTraceAssistantEntry => {
6884
+ if (activeStudioTraceAssistantEntryId) {
6885
+ const existing = studioTraceState.entries.find((entry) => entry.id === activeStudioTraceAssistantEntryId);
6886
+ if (existing && existing.type === "assistant") return existing;
6887
+ }
6888
+ if (studioTraceState.runId == null || studioTraceState.status === "idle") {
6889
+ resetStudioTraceForRun();
6890
+ }
6891
+ const now = Date.now();
6892
+ const entry: StudioTraceAssistantEntry = {
6893
+ id: randomUUID(),
6894
+ type: "assistant",
6895
+ startedAt: now,
6896
+ updatedAt: now,
6897
+ thinking: "",
6898
+ text: "",
6899
+ status: "streaming",
6900
+ stopReason: null,
6901
+ };
6902
+ activeStudioTraceAssistantEntryId = entry.id;
6903
+ upsertStudioTraceEntry(entry);
6904
+ return entry;
6905
+ };
6906
+
6907
+ const appendStudioTraceAssistantDelta = (deltaKind: "thinking" | "text", delta: string) => {
6908
+ if (!delta) return;
6909
+ const entry = ensureStudioTraceAssistantEntry();
6910
+ if (deltaKind === "thinking") {
6911
+ entry.thinking += delta;
6912
+ } else {
6913
+ entry.text += delta;
6914
+ }
6915
+ entry.status = "streaming";
6916
+ entry.updatedAt = Date.now();
6917
+ studioTraceState.updatedAt = entry.updatedAt;
6918
+ broadcast({
6919
+ type: "trace_assistant_delta",
6920
+ entryId: entry.id,
6921
+ deltaKind,
6922
+ delta,
6923
+ updatedAt: entry.updatedAt,
6924
+ runId: studioTraceState.runId,
6925
+ });
6926
+ };
6927
+
6928
+ const finalizeStudioTraceAssistantEntry = (text: string | null, thinking: string | null, stopReason?: string | null) => {
6929
+ const now = Date.now();
6930
+ let entry = activeStudioTraceAssistantEntryId
6931
+ ? studioTraceState.entries.find((candidate) => candidate.id === activeStudioTraceAssistantEntryId)
6932
+ : null;
6933
+ if (!entry || entry.type !== "assistant") {
6934
+ if (!(text && text.trim()) && !(thinking && thinking.trim())) {
6935
+ activeStudioTraceAssistantEntryId = null;
6936
+ return;
6937
+ }
6938
+ entry = ensureStudioTraceAssistantEntry();
6939
+ }
6940
+ entry.text = typeof text === "string" ? text : entry.text;
6941
+ entry.thinking = typeof thinking === "string" ? thinking : entry.thinking;
6942
+ entry.stopReason = typeof stopReason === "string" && stopReason.trim() ? stopReason : null;
6943
+ entry.status = "complete";
6944
+ entry.updatedAt = now;
6945
+ upsertStudioTraceEntry(entry);
6946
+ activeStudioTraceAssistantEntryId = null;
6947
+ };
6948
+
6949
+ const ensureStudioTraceToolEntry = (toolCallId: string, toolName: string, args: unknown): StudioTraceToolEntry => {
6950
+ const existingId = studioTraceToolEntryIds.get(toolCallId);
6951
+ if (existingId) {
6952
+ const existing = studioTraceState.entries.find((entry) => entry.id === existingId);
6953
+ if (existing && existing.type === "tool") return existing;
6954
+ }
6955
+ if (studioTraceState.runId == null || studioTraceState.status === "idle") {
6956
+ resetStudioTraceForRun();
6957
+ }
6958
+ const now = Date.now();
6959
+ const entry: StudioTraceToolEntry = {
6960
+ id: randomUUID(),
6961
+ type: "tool",
6962
+ toolCallId,
6963
+ toolName,
6964
+ label: deriveToolActivityLabel(toolName, args),
6965
+ argsSummary: summarizeStudioTraceToolArgs(toolName, args),
6966
+ output: "",
6967
+ startedAt: now,
6968
+ updatedAt: now,
6969
+ status: "pending",
6970
+ isError: false,
6971
+ };
6972
+ studioTraceToolEntryIds.set(toolCallId, entry.id);
6973
+ upsertStudioTraceEntry(entry);
6974
+ return entry;
6975
+ };
6976
+
6977
+ const updateStudioTraceToolEntry = (
6978
+ toolCallId: string,
6979
+ toolName: string,
6980
+ args: unknown,
6981
+ output: string,
6982
+ status: StudioTraceEntryStatus,
6983
+ isError: boolean,
6984
+ ) => {
6985
+ const entry = ensureStudioTraceToolEntry(toolCallId, toolName, args);
6986
+ entry.output = output;
6987
+ entry.status = status;
6988
+ entry.isError = isError;
6989
+ entry.updatedAt = Date.now();
6990
+ upsertStudioTraceEntry(entry);
6991
+ };
6992
+
6993
+ const clearStudioTrace = () => {
6994
+ studioTraceState = createEmptyStudioTraceState();
6995
+ activeStudioTraceAssistantEntryId = null;
6996
+ studioTraceToolEntryIds.clear();
6997
+ broadcastStudioTraceReset();
6998
+ };
6999
+
6690
7000
  const setTerminalActivity = (phase: TerminalActivityPhase, toolName?: string | null, label?: string | null) => {
6691
7001
  const nextPhase: TerminalActivityPhase =
6692
7002
  phase === "running" || phase === "tool" || phase === "responding"
@@ -7048,6 +7358,7 @@ export default function (pi: ExtensionAPI) {
7048
7358
  queuedSteeringCount: getQueuedStudioSteeringCount(),
7049
7359
  lastResponse: lastStudioResponse,
7050
7360
  responseHistory: studioResponseHistory,
7361
+ traceState: studioTraceState,
7051
7362
  initialDocument: initialStudioDocument,
7052
7363
  });
7053
7364
  return;
@@ -8340,6 +8651,7 @@ export default function (pi: ExtensionAPI) {
8340
8651
 
8341
8652
  pi.on("agent_start", async () => {
8342
8653
  agentBusy = true;
8654
+ resetStudioTraceForRun();
8343
8655
  emitDebugEvent("agent_start", { activeRequestId: activeRequest?.id ?? null, activeRequestKind: activeRequest?.kind ?? null });
8344
8656
  setTerminalActivity("running");
8345
8657
  });
@@ -8356,12 +8668,33 @@ export default function (pi: ExtensionAPI) {
8356
8668
  pi.on("tool_execution_start", async (event) => {
8357
8669
  if (!agentBusy) return;
8358
8670
  const label = deriveToolActivityLabel(event.toolName, event.args);
8671
+ ensureStudioTraceToolEntry(event.toolCallId, event.toolName, event.args);
8359
8672
  emitDebugEvent("tool_execution_start", { toolName: event.toolName, label, activeRequestId: activeRequest?.id ?? null, activeRequestKind: activeRequest?.kind ?? null });
8360
8673
  setTerminalActivity("tool", event.toolName, label);
8361
8674
  });
8362
8675
 
8676
+ pi.on("tool_execution_update", async (event) => {
8677
+ if (!agentBusy) return;
8678
+ updateStudioTraceToolEntry(
8679
+ event.toolCallId,
8680
+ event.toolName,
8681
+ event.args,
8682
+ formatStudioTraceOutput(event.partialResult),
8683
+ "streaming",
8684
+ false,
8685
+ );
8686
+ });
8687
+
8363
8688
  pi.on("tool_execution_end", async (event) => {
8364
8689
  if (!agentBusy) return;
8690
+ updateStudioTraceToolEntry(
8691
+ event.toolCallId,
8692
+ event.toolName,
8693
+ undefined,
8694
+ formatStudioTraceOutput(event.result),
8695
+ event.isError ? "error" : "complete",
8696
+ Boolean(event.isError),
8697
+ );
8365
8698
  emitDebugEvent("tool_execution_end", { toolName: event.toolName, activeRequestId: activeRequest?.id ?? null, activeRequestKind: activeRequest?.kind ?? null });
8366
8699
  // Keep tool phase visible until the next tool call, assistant response phase,
8367
8700
  // or agent_end. This avoids tool labels flashing too quickly to read.
@@ -8372,12 +8705,26 @@ export default function (pi: ExtensionAPI) {
8372
8705
  emitDebugEvent("message_start", { role: role ?? "", activeRequestId: activeRequest?.id ?? null, activeRequestKind: activeRequest?.kind ?? null });
8373
8706
  if (role === "assistant") {
8374
8707
  persistPendingStudioPromptMetadata();
8708
+ ensureStudioTraceAssistantEntry();
8375
8709
  }
8376
8710
  if (agentBusy && role === "assistant") {
8377
8711
  setTerminalActivity("responding");
8378
8712
  }
8379
8713
  });
8380
8714
 
8715
+ pi.on("message_update", async (event) => {
8716
+ if (!agentBusy) return;
8717
+ const deltaEvent = event.assistantMessageEvent as { type?: string; delta?: string } | undefined;
8718
+ if (!deltaEvent || typeof deltaEvent.delta !== "string" || !deltaEvent.delta) return;
8719
+ if (deltaEvent.type === "thinking_delta") {
8720
+ appendStudioTraceAssistantDelta("thinking", deltaEvent.delta);
8721
+ return;
8722
+ }
8723
+ if (deltaEvent.type === "text_delta") {
8724
+ appendStudioTraceAssistantDelta("text", deltaEvent.delta);
8725
+ }
8726
+ });
8727
+
8381
8728
  pi.on("message_end", async (event, ctx) => {
8382
8729
  const message = event.message as { stopReason?: string; role?: string };
8383
8730
  const stopReason = typeof message.stopReason === "string" ? message.stopReason : "";
@@ -8416,6 +8763,7 @@ export default function (pi: ExtensionAPI) {
8416
8763
 
8417
8764
  // Assistant is handing off to tool calls; request is still in progress.
8418
8765
  if (stopReason === "toolUse") {
8766
+ finalizeStudioTraceAssistantEntry(markdown, thinking, stopReason);
8419
8767
  emitDebugEvent("message_end_tool_use", {
8420
8768
  role,
8421
8769
  activeRequestId: activeRequest?.id ?? null,
@@ -8424,6 +8772,7 @@ export default function (pi: ExtensionAPI) {
8424
8772
  return;
8425
8773
  }
8426
8774
 
8775
+ finalizeStudioTraceAssistantEntry(markdown, thinking, stopReason);
8427
8776
  if (!markdown) return;
8428
8777
 
8429
8778
  if (suppressedStudioResponse) {
@@ -8538,6 +8887,7 @@ export default function (pi: ExtensionAPI) {
8538
8887
  });
8539
8888
  clearStudioDirectRunState();
8540
8889
  setTerminalActivity("idle");
8890
+ setStudioTraceRunStatus("complete");
8541
8891
  if (activeRequest) {
8542
8892
  const requestId = activeRequest.id;
8543
8893
  broadcast({
@@ -8561,6 +8911,7 @@ export default function (pi: ExtensionAPI) {
8561
8911
  clearPendingStudioCompletion();
8562
8912
  clearPreparedPdfExports();
8563
8913
  clearCompactionState();
8914
+ clearStudioTrace();
8564
8915
  setTerminalActivity("idle");
8565
8916
  await stopServer();
8566
8917
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-studio",
3
- "version": "0.5.51",
3
+ "version": "0.5.53",
4
4
  "description": "Two-pane browser workspace for pi with prompt/response editing, annotations, critiques, prompt/response history, and live Markdown/LaTeX/code preview",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -0,0 +1,203 @@
1
+ const STUDIO_LITERAL_MARKDOWN_LATEX_COMMANDS = new Set([
2
+ "documentclass",
3
+ "usepackage",
4
+ "newtheorem",
5
+ "begin",
6
+ "end",
7
+ "section",
8
+ "subsection",
9
+ "subsubsection",
10
+ "chapter",
11
+ "part",
12
+ "paragraph",
13
+ "subparagraph",
14
+ "title",
15
+ "author",
16
+ "date",
17
+ "maketitle",
18
+ "tableofcontents",
19
+ "includegraphics",
20
+ "caption",
21
+ "label",
22
+ "ref",
23
+ "eqref",
24
+ "autoref",
25
+ "pageref",
26
+ "cite",
27
+ "citet",
28
+ "citep",
29
+ "citealt",
30
+ "citeauthor",
31
+ "nocite",
32
+ "textbf",
33
+ "textit",
34
+ "texttt",
35
+ "emph",
36
+ "underline",
37
+ "footnote",
38
+ "centering",
39
+ "newcommand",
40
+ "renewcommand",
41
+ "providecommand",
42
+ "bibliography",
43
+ "bibliographystyle",
44
+ "printbibliography",
45
+ "addbibresource",
46
+ "bibitem",
47
+ "item",
48
+ "input",
49
+ "include",
50
+ "latex",
51
+ "tex",
52
+ ]);
53
+
54
+ function isEscapedAt(text, index) {
55
+ let slashCount = 0;
56
+ for (let i = index - 1; i >= 0 && text[i] === "\\"; i -= 1) {
57
+ slashCount += 1;
58
+ }
59
+ return slashCount % 2 === 1;
60
+ }
61
+
62
+ function findClosingUnescapedDelimiter(text, startIndex, delimiter) {
63
+ let searchIndex = Math.max(0, startIndex);
64
+ while (searchIndex <= text.length) {
65
+ const matchIndex = text.indexOf(delimiter, searchIndex);
66
+ if (matchIndex < 0) return -1;
67
+ if (!isEscapedAt(text, matchIndex)) return matchIndex;
68
+ searchIndex = matchIndex + delimiter.length;
69
+ }
70
+ return -1;
71
+ }
72
+
73
+ function preserveLiteralLatexCommandsInMarkdownSegment(markdown) {
74
+ const source = String(markdown || "");
75
+ let out = "";
76
+ let index = 0;
77
+
78
+ while (index < source.length) {
79
+ if (source[index] === "`") {
80
+ let tickCount = 1;
81
+ while (source[index + tickCount] === "`") tickCount += 1;
82
+ const fence = "`".repeat(tickCount);
83
+ const closeIndex = source.indexOf(fence, index + tickCount);
84
+ if (closeIndex >= 0) {
85
+ out += source.slice(index, closeIndex + tickCount);
86
+ index = closeIndex + tickCount;
87
+ continue;
88
+ }
89
+ }
90
+
91
+ if (source.startsWith("$$", index) && !isEscapedAt(source, index)) {
92
+ const closeIndex = findClosingUnescapedDelimiter(source, index + 2, "$$");
93
+ if (closeIndex >= 0) {
94
+ out += source.slice(index, closeIndex + 2);
95
+ index = closeIndex + 2;
96
+ continue;
97
+ }
98
+ }
99
+
100
+ if (source[index] === "$" && !isEscapedAt(source, index)) {
101
+ const closeIndex = findClosingUnescapedDelimiter(source, index + 1, "$");
102
+ if (closeIndex >= 0) {
103
+ out += source.slice(index, closeIndex + 1);
104
+ index = closeIndex + 1;
105
+ continue;
106
+ }
107
+ }
108
+
109
+ if (source.startsWith("\\(", index)) {
110
+ const closeIndex = source.indexOf("\\)", index + 2);
111
+ if (closeIndex >= 0) {
112
+ out += source.slice(index, closeIndex + 2);
113
+ index = closeIndex + 2;
114
+ continue;
115
+ }
116
+ }
117
+
118
+ if (source.startsWith("\\[", index)) {
119
+ const closeIndex = source.indexOf("\\]", index + 2);
120
+ if (closeIndex >= 0) {
121
+ out += source.slice(index, closeIndex + 2);
122
+ index = closeIndex + 2;
123
+ continue;
124
+ }
125
+ }
126
+
127
+ if (source[index] === "\\" && source[index + 1] === "\\") {
128
+ out += "\\\\";
129
+ index += 2;
130
+ continue;
131
+ }
132
+
133
+ if (source[index] === "\\" && /[A-Za-z@]/.test(source[index + 1] || "")) {
134
+ let endIndex = index + 1;
135
+ while (/[A-Za-z@]/.test(source[endIndex] || "")) endIndex += 1;
136
+ if (source[endIndex] === "*") endIndex += 1;
137
+ const commandName = source.slice(index + 1, endIndex).replace(/\*$/, "").toLowerCase();
138
+ if (STUDIO_LITERAL_MARKDOWN_LATEX_COMMANDS.has(commandName)) {
139
+ out += "\\" + source.slice(index, endIndex);
140
+ index = endIndex;
141
+ continue;
142
+ }
143
+ }
144
+
145
+ out += source[index];
146
+ index += 1;
147
+ }
148
+
149
+ return out;
150
+ }
151
+
152
+ export function preserveLiteralLatexCommandsInMarkdown(markdown) {
153
+ const lines = String(markdown || "").split("\n");
154
+ const out = [];
155
+ let plainBuffer = [];
156
+ let inFence = false;
157
+ let fenceChar;
158
+ let fenceLength = 0;
159
+
160
+ const flushPlain = () => {
161
+ if (plainBuffer.length === 0) return;
162
+ out.push(preserveLiteralLatexCommandsInMarkdownSegment(plainBuffer.join("\n")));
163
+ plainBuffer = [];
164
+ };
165
+
166
+ for (const line of lines) {
167
+ const trimmed = line.trimStart();
168
+ const fenceMatch = trimmed.match(/^(`{3,}|~{3,})/);
169
+
170
+ if (fenceMatch) {
171
+ const marker = fenceMatch[1];
172
+ const markerChar = marker[0];
173
+ const markerLength = marker.length;
174
+
175
+ if (!inFence) {
176
+ flushPlain();
177
+ inFence = true;
178
+ fenceChar = markerChar;
179
+ fenceLength = markerLength;
180
+ out.push(line);
181
+ continue;
182
+ }
183
+
184
+ if (fenceChar === markerChar && markerLength >= fenceLength) {
185
+ inFence = false;
186
+ fenceChar = undefined;
187
+ fenceLength = 0;
188
+ }
189
+
190
+ out.push(line);
191
+ continue;
192
+ }
193
+
194
+ if (inFence) {
195
+ out.push(line);
196
+ } else {
197
+ plainBuffer.push(line);
198
+ }
199
+ }
200
+
201
+ flushPlain();
202
+ return out.join("\n");
203
+ }