pi-studio 0.9.18 → 0.9.20

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
@@ -392,7 +392,6 @@
392
392
  }
393
393
 
394
394
  body[data-studio-mode="editor-only"] #editorViewSelect,
395
- body[data-studio-mode="editor-only"] #rightViewSelect,
396
395
  body[data-studio-mode="editor-only"] #sendRunBtn,
397
396
  body[data-studio-mode="editor-only"] #queueSteerBtn,
398
397
  body[data-studio-mode="editor-only"] #sendEditorBtn,
@@ -424,7 +423,7 @@
424
423
  }
425
424
 
426
425
  body[data-studio-mode="editor-only"] #rightSectionHeader .section-header-main::before {
427
- content: "Preview";
426
+ content: "View";
428
427
  font-weight: 600;
429
428
  font-size: 14px;
430
429
  }
@@ -2506,6 +2505,35 @@
2506
2505
  white-space: nowrap;
2507
2506
  }
2508
2507
 
2508
+ .rendered-markdown .studio-html-artifact-comment-btn {
2509
+ flex: 0 0 auto;
2510
+ min-height: 24px;
2511
+ padding: 0 9px;
2512
+ border: 1px solid var(--control-border);
2513
+ border-radius: 999px;
2514
+ background: var(--panel);
2515
+ color: var(--text);
2516
+ font: inherit;
2517
+ font-size: 11px;
2518
+ line-height: 1;
2519
+ cursor: pointer;
2520
+ white-space: nowrap;
2521
+ }
2522
+
2523
+ .rendered-markdown .studio-html-artifact-comment-btn:not(:disabled):hover,
2524
+ .rendered-markdown .studio-html-artifact-comment-btn:focus-visible {
2525
+ background: var(--control-hover-bg, var(--inline-code-bg));
2526
+ border-color: var(--control-border-hover, var(--accent));
2527
+ outline: none;
2528
+ }
2529
+
2530
+ .rendered-markdown .studio-html-artifact-comment-btn.is-active,
2531
+ .rendered-markdown .studio-html-artifact-shell.is-comment-mode .studio-html-artifact-comment-btn.is-active {
2532
+ background: var(--accent-soft);
2533
+ border-color: var(--accent);
2534
+ color: var(--accent);
2535
+ }
2536
+
2509
2537
  .rendered-markdown .studio-html-artifact-zoom-controls {
2510
2538
  flex: 0 0 auto;
2511
2539
  display: inline-flex;
@@ -3153,7 +3181,7 @@
3153
3181
  .trace-card {
3154
3182
  display: flex;
3155
3183
  flex-direction: column;
3156
- gap: 8px;
3184
+ gap: 10px;
3157
3185
  padding: 10px 12px;
3158
3186
  border: 1px solid var(--panel-border);
3159
3187
  border-radius: 10px;
@@ -3179,10 +3207,23 @@
3179
3207
  .trace-section {
3180
3208
  display: flex;
3181
3209
  flex-direction: column;
3182
- gap: 4px;
3210
+ gap: 6px;
3211
+ padding: 8px;
3212
+ border: 1px solid var(--border-subtle);
3213
+ border-radius: 10px;
3214
+ background: var(--panel);
3215
+ }
3216
+
3217
+ .trace-section + .trace-section {
3218
+ margin-top: 2px;
3183
3219
  }
3184
3220
 
3185
3221
  .trace-section-label {
3222
+ align-self: flex-start;
3223
+ padding: 2px 7px;
3224
+ border: 1px solid var(--border-subtle);
3225
+ border-radius: 999px;
3226
+ background: var(--panel-2);
3186
3227
  font-size: 11px;
3187
3228
  font-weight: 600;
3188
3229
  color: var(--muted);
@@ -4023,6 +4064,10 @@
4023
4064
  line-height: 1.35;
4024
4065
  }
4025
4066
 
4067
+ body[data-studio-mode="editor-only"] .shortcuts-full-only {
4068
+ display: none !important;
4069
+ }
4070
+
4026
4071
  .scratchpad-textarea {
4027
4072
  width: 100%;
4028
4073
  min-height: 280px;
package/index.ts CHANGED
@@ -209,6 +209,8 @@ interface InitialStudioDocument {
209
209
  resourceDir?: string;
210
210
  }
211
211
 
212
+ type PersistedStudioReviewNoteAnchorKind = "source" | "html-selection" | "html-element" | "html-page";
213
+
212
214
  interface PersistedStudioReviewNote {
213
215
  id: string;
214
216
  text: string;
@@ -220,6 +222,11 @@ interface PersistedStudioReviewNote {
220
222
  lineEnd: number;
221
223
  selectedText: string;
222
224
  selectedDisplayText?: string;
225
+ anchorKind?: PersistedStudioReviewNoteAnchorKind;
226
+ htmlSelector?: string;
227
+ htmlTag?: string;
228
+ htmlLabel?: string;
229
+ htmlPreviewTitle?: string;
223
230
  }
224
231
 
225
232
  interface StudioPersistentState {
@@ -257,6 +264,7 @@ interface StudioTraceToolEntry {
257
264
  toolName: string;
258
265
  label: string | null;
259
266
  argsSummary: string | null;
267
+ args: string | null;
260
268
  output: string;
261
269
  images: StudioTraceImage[];
262
270
  startedAt: number;
@@ -429,6 +437,7 @@ interface SaveOverRequestMessage {
429
437
  interface RefreshFromDiskRequestMessage {
430
438
  type: "refresh_from_disk_request";
431
439
  requestId: string;
440
+ path?: string;
432
441
  }
433
442
 
434
443
  interface SendToEditorRequestMessage {
@@ -517,6 +526,7 @@ const MAX_PREPARED_PDF_EXPORTS = 8;
517
526
  const MAX_PREPARED_HTML_EXPORTS = 8;
518
527
  const STUDIO_TRACE_SNAPSHOT_MAX_ENTRIES = 80;
519
528
  const STUDIO_TRACE_SNAPSHOT_MAX_FIELD_CHARS = 20_000;
529
+ const STUDIO_TRACE_TOOL_ARGS_MAX_CHARS = 20_000;
520
530
  const STUDIO_TRACE_IMAGE_MAX_COUNT = 8;
521
531
  const STUDIO_TRACE_IMAGE_MAX_BASE64_CHARS = 2_500_000;
522
532
  const STUDIO_TRACE_SNAPSHOT_MAX_IMAGES = 12;
@@ -722,6 +732,10 @@ function createEmptyStudioPersistentState(): StudioPersistentState {
722
732
  };
723
733
  }
724
734
 
735
+ function normalizePersistedStudioReviewNoteAnchorKind(value: unknown): PersistedStudioReviewNoteAnchorKind {
736
+ return value === "html-selection" || value === "html-element" || value === "html-page" ? value : "source";
737
+ }
738
+
725
739
  function normalizePersistedStudioReviewNote(value: unknown): PersistedStudioReviewNote | null {
726
740
  if (!value || typeof value !== "object") return null;
727
741
  const candidate = value as Partial<PersistedStudioReviewNote>;
@@ -756,6 +770,11 @@ function normalizePersistedStudioReviewNote(value: unknown): PersistedStudioRevi
756
770
  lineEnd,
757
771
  selectedText: typeof candidate.selectedText === "string" ? candidate.selectedText : "",
758
772
  selectedDisplayText: typeof candidate.selectedDisplayText === "string" ? candidate.selectedDisplayText : "",
773
+ anchorKind: normalizePersistedStudioReviewNoteAnchorKind(candidate.anchorKind),
774
+ htmlSelector: typeof candidate.htmlSelector === "string" ? candidate.htmlSelector : "",
775
+ htmlTag: typeof candidate.htmlTag === "string" ? candidate.htmlTag : "",
776
+ htmlLabel: typeof candidate.htmlLabel === "string" ? candidate.htmlLabel : "",
777
+ htmlPreviewTitle: typeof candidate.htmlPreviewTitle === "string" ? candidate.htmlPreviewTitle : "",
759
778
  };
760
779
  }
761
780
 
@@ -8140,10 +8159,15 @@ function parseIncomingMessage(data: RawData): IncomingStudioMessage | null {
8140
8159
  };
8141
8160
  }
8142
8161
 
8143
- if (msg.type === "refresh_from_disk_request" && typeof msg.requestId === "string") {
8162
+ if (
8163
+ msg.type === "refresh_from_disk_request"
8164
+ && typeof msg.requestId === "string"
8165
+ && (msg.path === undefined || typeof msg.path === "string")
8166
+ ) {
8144
8167
  return {
8145
8168
  type: "refresh_from_disk_request",
8146
8169
  requestId: msg.requestId,
8170
+ path: typeof msg.path === "string" ? msg.path : undefined,
8147
8171
  };
8148
8172
  }
8149
8173
 
@@ -8567,15 +8591,17 @@ function createStudioTraceSnapshot(source: StudioTraceState): { traceState: Stud
8567
8591
  };
8568
8592
  }
8569
8593
  const argsSummary = truncateStudioTraceSnapshotText(entry.argsSummary ?? "");
8594
+ const args = truncateStudioTraceSnapshotText(entry.args ?? entry.argsSummary ?? "");
8570
8595
  const output = truncateStudioTraceSnapshotText(entry.output);
8571
8596
  const snapshotImages = copyStudioTraceImagesForSnapshot(entry.images, imageBudget);
8572
- truncated = truncated || argsSummary.truncated || output.truncated || snapshotImages.omitted > 0;
8597
+ truncated = truncated || argsSummary.truncated || args.truncated || output.truncated || snapshotImages.omitted > 0;
8573
8598
  const omittedImageNote = snapshotImages.omitted > 0
8574
8599
  ? `[${snapshotImages.omitted} image preview${snapshotImages.omitted === 1 ? "" : "s"} omitted from saved Working view to keep history bounded.]`
8575
8600
  : "";
8576
8601
  return {
8577
8602
  ...entry,
8578
8603
  argsSummary: argsSummary.text || null,
8604
+ args: args.text || null,
8579
8605
  output: [output.text, omittedImageNote].filter(Boolean).join("\n"),
8580
8606
  images: snapshotImages.images,
8581
8607
  };
@@ -8786,6 +8812,34 @@ function summarizeStudioTraceToolArgs(toolName: string, args: unknown): string |
8786
8812
  }
8787
8813
  }
8788
8814
 
8815
+ function truncateStudioTraceToolArgs(text: string): string {
8816
+ const value = sanitizeStudioTraceOutputText(String(text || "").trim());
8817
+ if (!value || value.length <= STUDIO_TRACE_TOOL_ARGS_MAX_CHARS) return value;
8818
+ const keepHead = Math.max(1_000, Math.floor(STUDIO_TRACE_TOOL_ARGS_MAX_CHARS * 0.65));
8819
+ const keepTail = Math.max(1_000, STUDIO_TRACE_TOOL_ARGS_MAX_CHARS - keepHead - 160);
8820
+ const omitted = value.length - keepHead - keepTail;
8821
+ return `${value.slice(0, keepHead)}\n\n… ${omitted} chars omitted from tool input …\n\n${value.slice(value.length - keepTail)}`;
8822
+ }
8823
+
8824
+ function formatStudioTraceToolArgs(toolName: string, args: unknown): string | null {
8825
+ const normalizedTool = String(toolName || "").trim().toLowerCase();
8826
+ const payload = (args && typeof args === "object") ? (args as Record<string, unknown>) : {};
8827
+ let raw = "";
8828
+ if (normalizedTool === "bash" && typeof payload.command === "string") {
8829
+ raw = payload.command;
8830
+ } else if ((normalizedTool === "repl_send" || normalizedTool === "studio_repl_send") && typeof payload.code === "string") {
8831
+ raw = payload.code;
8832
+ } else {
8833
+ try {
8834
+ raw = JSON.stringify(args, null, 2);
8835
+ } catch {
8836
+ raw = String(args ?? "");
8837
+ }
8838
+ }
8839
+ const truncated = truncateStudioTraceToolArgs(raw);
8840
+ return truncated ? truncated : null;
8841
+ }
8842
+
8789
8843
  function isStudioReplRuntime(value: unknown): value is StudioReplRuntime {
8790
8844
  return value === "shell"
8791
8845
  || value === "python"
@@ -9827,7 +9881,7 @@ ${cssVarsBlock}
9827
9881
  <button id="saveOverBtn" type="button" title="Overwrite current file with editor content. Shortcut: Cmd/Ctrl+S.">Save editor</button>
9828
9882
  <button id="refreshFromDiskBtn" type="button" title="Reload the current file-backed document from disk.">Refresh from disk</button>
9829
9883
  <button id="clearWorkspaceBtn" type="button" title="Clear editor text and reset this tab to a fresh blank draft. Saved files and responses are not changed.">Reset editor</button>
9830
- <label class="file-label" title="Load a local file into editor text.">Load file content<input id="fileInput" type="file" accept=".md,.markdown,.mdx,.qmd,.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>
9884
+ <label class="file-label" title="Import a browser-selected text file into the editor as an unsaved copy. It will not be refreshable from disk until you save it.">Import file copy…<input id="fileInput" type="file" accept=".md,.markdown,.mdx,.qmd,.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>
9831
9885
  <button id="loadGitDiffBtn" type="button" title="Load the current git diff from the Studio context into the editor.">Load git diff</button>
9832
9886
  <button id="getEditorBtn" type="button" title="Load the current terminal editor draft into Studio.">Load from pi editor</button>
9833
9887
  <button id="zenModeBtn" class="zen-mode-btn" type="button" title="Hide secondary Studio controls. Shortcut: F9.">Zen</button>
@@ -10004,14 +10058,14 @@ ${cssVarsBlock}
10004
10058
  <div class="scratchpad-header">
10005
10059
  <div>
10006
10060
  <h2 id="reviewNotesTitle">Comments</h2>
10007
- <p class="scratchpad-description">Local comments for editor text. Stay out of the text, anchored to selections or lines, and can be converted into inline <span class="review-notes-inline-token">[an: ...]</span> annotations.</p>
10061
+ <p class="scratchpad-description">Local comments for editor text and editor previews. They stay out of the text; source-anchored comments can be converted into inline <span class="review-notes-inline-token">[an: ...]</span> annotations.</p>
10008
10062
  </div>
10009
10063
  <button id="reviewNotesCloseBtn" type="button" class="scratchpad-close-btn" aria-label="Hide comments" title="Hide comments">✕</button>
10010
10064
  </div>
10011
10065
  <div class="review-notes-toolbar">
10012
10066
  <span id="reviewNotesMeta" class="scratchpad-meta">No comments</span>
10013
10067
  </div>
10014
- <div id="reviewNotesEmptyState" class="review-notes-empty">No comments yet for this document. Select text in <strong>Editor (Raw)</strong> or <strong>Editor (Preview)</strong> and use <em>Comment</em>, or use <em>Line comment</em> in <strong>Editor (Raw)</strong>.</div>
10068
+ <div id="reviewNotesEmptyState" class="review-notes-empty">No comments yet for this document. Select text in <strong>Editor (Raw)</strong> or <strong>Editor (Preview)</strong> and use <em>Comment</em>, use <em>Line comment</em> in <strong>Editor (Raw)</strong>, or use <em>Comment mode</em> in an editor HTML preview.</div>
10015
10069
  <div id="reviewNotesList" class="review-notes-list" aria-live="polite"></div>
10016
10070
  <div class="review-notes-dock-footer">
10017
10071
  <div class="scratchpad-actions">
@@ -10143,14 +10197,14 @@ ${cssVarsBlock}
10143
10197
  <h3>Editor</h3>
10144
10198
  <dl>
10145
10199
  <div><dt>Cmd/Ctrl+S</dt><dd>Save editor</dd></div>
10146
- <div><dt>Cmd/Ctrl+Enter</dt><dd>Run editor text, or queue steering during an active run</dd></div>
10200
+ <div class="shortcuts-full-only"><dt>Cmd/Ctrl+Enter</dt><dd>Run editor text, or queue steering during an active run</dd></div>
10147
10201
  <div><dt>Option/Alt+Tab or Cmd/Ctrl+Shift+Space</dt><dd>Suggest a completion at the editor cursor</dd></div>
10148
10202
  <div><dt>Tab</dt><dd>Insert a visible completion suggestion; otherwise indent selected editor text</dd></div>
10149
10203
  <div><dt>Esc</dt><dd>Dismiss a visible completion suggestion, close overlays, exit pane focus, or stop an active request</dd></div>
10150
10204
  <div><dt>Shift+Tab</dt><dd>Unindent selected editor text</dd></div>
10151
10205
  </dl>
10152
10206
  </section>
10153
- <section class="shortcuts-group">
10207
+ <section class="shortcuts-group shortcuts-full-only">
10154
10208
  <h3>Response</h3>
10155
10209
  <dl>
10156
10210
  <div><dt>Alt/Option+←</dt><dd>Previous response when not editing text</dd></div>
@@ -11040,7 +11094,17 @@ export default function (pi: ExtensionAPI) {
11040
11094
  const existingId = studioTraceToolEntryIds.get(toolCallId);
11041
11095
  if (existingId) {
11042
11096
  const existing = studioTraceState.entries.find((entry) => entry.id === existingId);
11043
- if (existing && existing.type === "tool") return existing;
11097
+ if (existing && existing.type === "tool") {
11098
+ if (args !== undefined) {
11099
+ existing.toolName = toolName;
11100
+ existing.label = deriveToolActivityLabel(toolName, args);
11101
+ existing.argsSummary = summarizeStudioTraceToolArgs(toolName, args);
11102
+ existing.args = formatStudioTraceToolArgs(toolName, args);
11103
+ existing.updatedAt = Date.now();
11104
+ upsertStudioTraceEntry(existing);
11105
+ }
11106
+ return existing;
11107
+ }
11044
11108
  }
11045
11109
  if (studioTraceState.runId == null || studioTraceState.status === "idle") {
11046
11110
  resetStudioTraceForRun();
@@ -11053,6 +11117,7 @@ export default function (pi: ExtensionAPI) {
11053
11117
  toolName,
11054
11118
  label: deriveToolActivityLabel(toolName, args),
11055
11119
  argsSummary: summarizeStudioTraceToolArgs(toolName, args),
11120
+ args: formatStudioTraceToolArgs(toolName, args),
11056
11121
  output: "",
11057
11122
  images: [],
11058
11123
  startedAt: now,
@@ -11075,6 +11140,8 @@ export default function (pi: ExtensionAPI) {
11075
11140
  images?: StudioTraceImage[],
11076
11141
  ) => {
11077
11142
  const entry = ensureStudioTraceToolEntry(toolCallId, toolName, args);
11143
+ if (!entry.argsSummary) entry.argsSummary = summarizeStudioTraceToolArgs(toolName, args);
11144
+ if (!entry.args) entry.args = formatStudioTraceToolArgs(toolName, args);
11078
11145
  entry.output = output;
11079
11146
  if (Array.isArray(images)) entry.images = images;
11080
11147
  entry.status = status;
@@ -12225,16 +12292,18 @@ export default function (pi: ExtensionAPI) {
12225
12292
  sendToClient(client, { type: "busy", requestId: msg.requestId, message: "Studio is busy." });
12226
12293
  return;
12227
12294
  }
12228
- if (!initialStudioDocument || !initialStudioDocument.path) {
12295
+ const requestedPath = typeof msg.path === "string" && msg.path.trim() ? msg.path.trim() : "";
12296
+ const refreshPath = requestedPath || initialStudioDocument?.path || "";
12297
+ if (!refreshPath) {
12229
12298
  sendToClient(client, {
12230
12299
  type: "error",
12231
12300
  requestId: msg.requestId,
12232
- message: "Refresh from disk is only available for file-backed documents.",
12301
+ message: "Refresh from disk needs a file path. Use Files → Open here, Files → Open file tab, or /studio-editor-only <path> for a refreshable editor tab.",
12233
12302
  });
12234
12303
  return;
12235
12304
  }
12236
12305
 
12237
- const refreshed = readStudioFile(initialStudioDocument.path, studioCwd);
12306
+ const refreshed = readStudioFile(refreshPath, studioCwd);
12238
12307
  if (refreshed.ok === false) {
12239
12308
  sendToClient(client, {
12240
12309
  type: "error",
@@ -12244,18 +12313,21 @@ export default function (pi: ExtensionAPI) {
12244
12313
  return;
12245
12314
  }
12246
12315
 
12247
- initialStudioDocument = {
12316
+ const refreshedDocument: InitialStudioDocument = {
12248
12317
  text: refreshed.text,
12249
12318
  label: refreshed.label,
12250
12319
  source: "file",
12251
12320
  path: refreshed.resolvedPath,
12252
12321
  resourceDir: dirname(refreshed.resolvedPath),
12253
12322
  };
12323
+ if (!requestedPath || initialStudioDocument?.path === refreshed.resolvedPath) {
12324
+ initialStudioDocument = refreshedDocument;
12325
+ }
12254
12326
 
12255
- broadcast({
12327
+ sendToClient(client, {
12256
12328
  type: "studio_document",
12257
12329
  requestId: msg.requestId,
12258
- document: initialStudioDocument,
12330
+ document: refreshedDocument,
12259
12331
  message: `Reloaded ${refreshed.label} from disk.`,
12260
12332
  });
12261
12333
  return;
@@ -13666,7 +13738,9 @@ export default function (pi: ExtensionAPI) {
13666
13738
  if (!agentBusy) return;
13667
13739
  const toolName = typeof event.toolName === "string" ? event.toolName : "";
13668
13740
  const input = (event as { input?: unknown }).input;
13741
+ const toolCallId = typeof event.toolCallId === "string" ? event.toolCallId : "";
13669
13742
  const label = deriveToolActivityLabel(toolName, input);
13743
+ if (toolCallId) ensureStudioTraceToolEntry(toolCallId, toolName, input);
13670
13744
  emitDebugEvent("tool_call", { toolName, label, activeRequestId: activeRequest?.id ?? null, activeRequestKind: activeRequest?.kind ?? null });
13671
13745
  setTerminalActivity("tool", toolName, label);
13672
13746
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-studio",
3
- "version": "0.9.18",
3
+ "version": "0.9.20",
4
4
  "description": "Two-pane browser workspace for pi with prompt/response editing, annotations, critiques, active quiz, prompt/response history, live previews, and tmux-backed REPL/literate REPL workflows",
5
5
  "type": "module",
6
6
  "license": "MIT",