pi-studio 0.5.52 → 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/client/studio.css CHANGED
@@ -1602,6 +1602,182 @@
1602
1602
  flex: 0 0 auto;
1603
1603
  }
1604
1604
 
1605
+ .trace-panel {
1606
+ display: flex;
1607
+ flex-direction: column;
1608
+ gap: 10px;
1609
+ }
1610
+
1611
+ .trace-toolbar {
1612
+ display: flex;
1613
+ flex-wrap: wrap;
1614
+ align-items: flex-start;
1615
+ justify-content: space-between;
1616
+ gap: 10px;
1617
+ }
1618
+
1619
+ .trace-summary {
1620
+ display: flex;
1621
+ flex-wrap: wrap;
1622
+ align-items: center;
1623
+ gap: 8px;
1624
+ min-width: 0;
1625
+ padding-bottom: 2px;
1626
+ }
1627
+
1628
+ .trace-controls {
1629
+ display: flex;
1630
+ flex-wrap: wrap;
1631
+ align-items: center;
1632
+ justify-content: flex-end;
1633
+ gap: 8px;
1634
+ }
1635
+
1636
+ .trace-filter-group {
1637
+ display: inline-flex;
1638
+ align-items: center;
1639
+ gap: 6px;
1640
+ flex-wrap: wrap;
1641
+ }
1642
+
1643
+ .trace-filter-btn,
1644
+ .trace-action-btn {
1645
+ padding: 0.34rem 0.68rem;
1646
+ font-size: 12px;
1647
+ border-radius: 999px;
1648
+ }
1649
+
1650
+ .trace-filter-btn.is-active {
1651
+ border-color: var(--accent-soft-strong);
1652
+ background: var(--accent-soft);
1653
+ color: var(--accent);
1654
+ font-weight: 600;
1655
+ }
1656
+
1657
+ .trace-summary-badge,
1658
+ .trace-summary-status,
1659
+ .trace-entry-status,
1660
+ .trace-kind-badge {
1661
+ display: inline-flex;
1662
+ align-items: center;
1663
+ border-radius: 999px;
1664
+ padding: 0.16rem 0.52rem;
1665
+ font-size: 11px;
1666
+ font-weight: 600;
1667
+ letter-spacing: 0.01em;
1668
+ border: 1px solid var(--border);
1669
+ background: transparent;
1670
+ color: var(--muted);
1671
+ }
1672
+
1673
+ .trace-summary-badge {
1674
+ color: var(--text);
1675
+ background: var(--panel);
1676
+ }
1677
+
1678
+ .trace-kind-badge {
1679
+ color: var(--muted);
1680
+ background: var(--panel);
1681
+ }
1682
+
1683
+ .trace-summary-status.trace-status-running {
1684
+ color: var(--muted);
1685
+ border-color: var(--border);
1686
+ background: var(--panel);
1687
+ }
1688
+
1689
+ .trace-entry-status.trace-entry-status-streaming,
1690
+ .trace-entry-status.trace-entry-status-pending {
1691
+ color: var(--accent);
1692
+ border-color: var(--accent-soft-strong);
1693
+ background: var(--accent-soft);
1694
+ }
1695
+
1696
+ .trace-summary-status.trace-status-complete,
1697
+ .trace-entry-status.trace-entry-status-complete {
1698
+ color: var(--ok);
1699
+ border-color: var(--ok-border);
1700
+ background: rgba(115, 209, 61, 0.08);
1701
+ }
1702
+
1703
+ .trace-entry-status.trace-entry-status-error {
1704
+ color: var(--error);
1705
+ border-color: var(--error);
1706
+ background: rgba(255, 107, 107, 0.08);
1707
+ }
1708
+
1709
+ .trace-summary-meta,
1710
+ .trace-card-meta {
1711
+ color: var(--muted);
1712
+ font-size: 12px;
1713
+ }
1714
+
1715
+ .trace-list {
1716
+ display: flex;
1717
+ flex-direction: column;
1718
+ gap: 10px;
1719
+ }
1720
+
1721
+ .trace-card {
1722
+ display: flex;
1723
+ flex-direction: column;
1724
+ gap: 8px;
1725
+ padding: 10px 12px;
1726
+ border: 1px solid var(--border-muted);
1727
+ border-radius: 10px;
1728
+ background: var(--panel-2);
1729
+ box-shadow: 0 1px 0 rgba(0, 0, 0, 0.03);
1730
+ }
1731
+
1732
+ .trace-card-header {
1733
+ display: flex;
1734
+ flex-wrap: wrap;
1735
+ align-items: center;
1736
+ gap: 8px;
1737
+ min-width: 0;
1738
+ }
1739
+
1740
+ .trace-card-title {
1741
+ min-width: 0;
1742
+ font-weight: 600;
1743
+ color: var(--text);
1744
+ overflow-wrap: anywhere;
1745
+ }
1746
+
1747
+ .trace-section {
1748
+ display: flex;
1749
+ flex-direction: column;
1750
+ gap: 4px;
1751
+ }
1752
+
1753
+ .trace-section-label {
1754
+ font-size: 11px;
1755
+ font-weight: 600;
1756
+ color: var(--muted);
1757
+ text-transform: uppercase;
1758
+ letter-spacing: 0.04em;
1759
+ }
1760
+
1761
+ .trace-output {
1762
+ padding: 10px 11px;
1763
+ border-radius: 8px;
1764
+ border: 1px solid var(--border-muted);
1765
+ background: var(--panel);
1766
+ overflow-x: auto;
1767
+ white-space: pre-wrap;
1768
+ overflow-wrap: anywhere;
1769
+ }
1770
+
1771
+ .trace-empty,
1772
+ .trace-empty-inline {
1773
+ color: var(--muted);
1774
+ font-style: italic;
1775
+ }
1776
+
1777
+ .trace-empty {
1778
+ padding: 14px 2px 2px;
1779
+ }
1780
+
1605
1781
  footer {
1606
1782
  border-top: 1px solid var(--border-muted);
1607
1783
  padding: 8px 12px;
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,
@@ -6158,7 +6265,7 @@ ${cssVarsBlock}
6158
6265
  <option value="markdown">Response (Raw)</option>
6159
6266
  <option value="preview" selected>Response (Preview)</option>
6160
6267
  <option value="editor-preview">Editor (Preview)</option>
6161
- <option value="thinking">Thinking (Raw)</option>
6268
+ <option value="trace">Working</option>
6162
6269
  </select>
6163
6270
  </div>
6164
6271
  <div class="section-header-actions">
@@ -6265,6 +6372,9 @@ export default function (pi: ExtensionAPI) {
6265
6372
  let studioResponseHistory: StudioResponseHistoryItem[] = [];
6266
6373
  let latestSessionUserPrompt: string | null = null;
6267
6374
  let pendingTurnPrompt: string | null = null;
6375
+ let studioTraceState: StudioTraceState = createEmptyStudioTraceState();
6376
+ let activeStudioTraceAssistantEntryId: string | null = null;
6377
+ const studioTraceToolEntryIds = new Map<string, string>();
6268
6378
  let contextUsageSnapshot: StudioContextUsageSnapshot = {
6269
6379
  tokens: null,
6270
6380
  contextWindow: null,
@@ -6709,6 +6819,184 @@ export default function (pi: ExtensionAPI) {
6709
6819
  });
6710
6820
  };
6711
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
+
6712
7000
  const setTerminalActivity = (phase: TerminalActivityPhase, toolName?: string | null, label?: string | null) => {
6713
7001
  const nextPhase: TerminalActivityPhase =
6714
7002
  phase === "running" || phase === "tool" || phase === "responding"
@@ -7070,6 +7358,7 @@ export default function (pi: ExtensionAPI) {
7070
7358
  queuedSteeringCount: getQueuedStudioSteeringCount(),
7071
7359
  lastResponse: lastStudioResponse,
7072
7360
  responseHistory: studioResponseHistory,
7361
+ traceState: studioTraceState,
7073
7362
  initialDocument: initialStudioDocument,
7074
7363
  });
7075
7364
  return;
@@ -8362,6 +8651,7 @@ export default function (pi: ExtensionAPI) {
8362
8651
 
8363
8652
  pi.on("agent_start", async () => {
8364
8653
  agentBusy = true;
8654
+ resetStudioTraceForRun();
8365
8655
  emitDebugEvent("agent_start", { activeRequestId: activeRequest?.id ?? null, activeRequestKind: activeRequest?.kind ?? null });
8366
8656
  setTerminalActivity("running");
8367
8657
  });
@@ -8378,12 +8668,33 @@ export default function (pi: ExtensionAPI) {
8378
8668
  pi.on("tool_execution_start", async (event) => {
8379
8669
  if (!agentBusy) return;
8380
8670
  const label = deriveToolActivityLabel(event.toolName, event.args);
8671
+ ensureStudioTraceToolEntry(event.toolCallId, event.toolName, event.args);
8381
8672
  emitDebugEvent("tool_execution_start", { toolName: event.toolName, label, activeRequestId: activeRequest?.id ?? null, activeRequestKind: activeRequest?.kind ?? null });
8382
8673
  setTerminalActivity("tool", event.toolName, label);
8383
8674
  });
8384
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
+
8385
8688
  pi.on("tool_execution_end", async (event) => {
8386
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
+ );
8387
8698
  emitDebugEvent("tool_execution_end", { toolName: event.toolName, activeRequestId: activeRequest?.id ?? null, activeRequestKind: activeRequest?.kind ?? null });
8388
8699
  // Keep tool phase visible until the next tool call, assistant response phase,
8389
8700
  // or agent_end. This avoids tool labels flashing too quickly to read.
@@ -8394,12 +8705,26 @@ export default function (pi: ExtensionAPI) {
8394
8705
  emitDebugEvent("message_start", { role: role ?? "", activeRequestId: activeRequest?.id ?? null, activeRequestKind: activeRequest?.kind ?? null });
8395
8706
  if (role === "assistant") {
8396
8707
  persistPendingStudioPromptMetadata();
8708
+ ensureStudioTraceAssistantEntry();
8397
8709
  }
8398
8710
  if (agentBusy && role === "assistant") {
8399
8711
  setTerminalActivity("responding");
8400
8712
  }
8401
8713
  });
8402
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
+
8403
8728
  pi.on("message_end", async (event, ctx) => {
8404
8729
  const message = event.message as { stopReason?: string; role?: string };
8405
8730
  const stopReason = typeof message.stopReason === "string" ? message.stopReason : "";
@@ -8438,6 +8763,7 @@ export default function (pi: ExtensionAPI) {
8438
8763
 
8439
8764
  // Assistant is handing off to tool calls; request is still in progress.
8440
8765
  if (stopReason === "toolUse") {
8766
+ finalizeStudioTraceAssistantEntry(markdown, thinking, stopReason);
8441
8767
  emitDebugEvent("message_end_tool_use", {
8442
8768
  role,
8443
8769
  activeRequestId: activeRequest?.id ?? null,
@@ -8446,6 +8772,7 @@ export default function (pi: ExtensionAPI) {
8446
8772
  return;
8447
8773
  }
8448
8774
 
8775
+ finalizeStudioTraceAssistantEntry(markdown, thinking, stopReason);
8449
8776
  if (!markdown) return;
8450
8777
 
8451
8778
  if (suppressedStudioResponse) {
@@ -8560,6 +8887,7 @@ export default function (pi: ExtensionAPI) {
8560
8887
  });
8561
8888
  clearStudioDirectRunState();
8562
8889
  setTerminalActivity("idle");
8890
+ setStudioTraceRunStatus("complete");
8563
8891
  if (activeRequest) {
8564
8892
  const requestId = activeRequest.id;
8565
8893
  broadcast({
@@ -8583,6 +8911,7 @@ export default function (pi: ExtensionAPI) {
8583
8911
  clearPendingStudioCompletion();
8584
8912
  clearPreparedPdfExports();
8585
8913
  clearCompactionState();
8914
+ clearStudioTrace();
8586
8915
  setTerminalActivity("idle");
8587
8916
  await stopServer();
8588
8917
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-studio",
3
- "version": "0.5.52",
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",