pi-studio 0.5.59 → 0.6.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 CHANGED
@@ -4,6 +4,19 @@ All notable changes to `pi-studio` are documented here.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.6.0] — 2026-04-27
8
+
9
+ ### Added
10
+ - The comments rail now includes **Comments → prompt**, which turns non-empty local comments into an editor prompt with line anchors and file labels when available.
11
+
12
+ ### Changed
13
+ - The refreshed Studio layout is now the default, with the classic layout still available via the footer UI switch or `?uiRefresh=0`.
14
+ - Working-view tool output now replaces image/base64 payloads with compact placeholders instead of dumping raw image data.
15
+
16
+ ### Fixed
17
+ - Queued steering now updates Studio's active effective-prompt metadata so response history and prompt loading reflect the original run plus steering messages.
18
+ - Newly arrived responses now force the right response pane to reset to the top, while editor-preview/document views still preserve scroll.
19
+
7
20
  ## [0.5.59] — 2026-04-27
8
21
 
9
22
  ### Added
@@ -132,6 +132,7 @@
132
132
  const reviewNotesListEl = document.getElementById("reviewNotesList");
133
133
  const reviewNotesEmptyStateEl = document.getElementById("reviewNotesEmptyState");
134
134
  const reviewNotesAddBtn = document.getElementById("reviewNotesAddBtn");
135
+ const reviewNotesPromptBtn = document.getElementById("reviewNotesPromptBtn");
135
136
  const reviewNotesInlineAllBtn = document.getElementById("reviewNotesInlineAllBtn");
136
137
  const reviewNotesDeleteAllBtn = document.getElementById("reviewNotesDeleteAllBtn");
137
138
  const reviewNotesCloseBtn = document.getElementById("reviewNotesCloseBtn");
@@ -674,21 +675,21 @@
674
675
  const queryValue = initialQueryParams.has("uiRefresh")
675
676
  ? initialQueryParams.get("uiRefresh")
676
677
  : (initialQueryParams.has("studioUiRefresh") ? initialQueryParams.get("studioUiRefresh") : null);
677
- const isTruthy = (value) => ["1", "true", "yes", "on", "v2", "refresh"].indexOf(normalize(value)) !== -1;
678
- const isFalsey = (value) => ["0", "false", "no", "off"].indexOf(normalize(value)) !== -1;
678
+ const isTruthy = (value) => ["1", "true", "yes", "on", "v2", "refresh", "fresh"].indexOf(normalize(value)) !== -1;
679
+ const isFalsey = (value) => ["0", "false", "no", "off", "classic"].indexOf(normalize(value)) !== -1;
679
680
  if (queryValue !== null) {
680
- const enabled = isTruthy(queryValue) || (!isFalsey(queryValue) && normalize(queryValue) !== "");
681
+ const normalizedQuery = normalize(queryValue);
682
+ const enabled = isTruthy(queryValue) || (!isFalsey(queryValue) && normalizedQuery !== "");
681
683
  try {
682
- if (enabled) window.localStorage && window.localStorage.setItem(STUDIO_UI_REFRESH_STORAGE_KEY, "1");
683
- else window.localStorage && window.localStorage.removeItem(STUDIO_UI_REFRESH_STORAGE_KEY);
684
+ window.localStorage && window.localStorage.setItem(STUDIO_UI_REFRESH_STORAGE_KEY, enabled ? "1" : "0");
684
685
  } catch {}
685
686
  return enabled;
686
687
  }
687
688
  try {
688
- return Boolean(window.localStorage && window.localStorage.getItem(STUDIO_UI_REFRESH_STORAGE_KEY) === "1");
689
- } catch {
690
- return false;
691
- }
689
+ const stored = window.localStorage ? window.localStorage.getItem(STUDIO_UI_REFRESH_STORAGE_KEY) : null;
690
+ if (stored !== null) return stored !== "0" && !isFalsey(stored);
691
+ } catch {}
692
+ return true;
692
693
  }
693
694
 
694
695
  function makeStudioUiRefreshElement(tagName, className, text) {
@@ -841,8 +842,7 @@
841
842
 
842
843
  function setStudioUiRefreshPreference(enabled) {
843
844
  try {
844
- if (enabled) window.localStorage && window.localStorage.setItem(STUDIO_UI_REFRESH_STORAGE_KEY, "1");
845
- else window.localStorage && window.localStorage.removeItem(STUDIO_UI_REFRESH_STORAGE_KEY);
845
+ window.localStorage && window.localStorage.setItem(STUDIO_UI_REFRESH_STORAGE_KEY, enabled ? "1" : "0");
846
846
  } catch {}
847
847
  try {
848
848
  const url = new URL(window.location.href);
@@ -855,7 +855,7 @@
855
855
 
856
856
  function setupStudioUiRefreshToggleButton() {
857
857
  if (!footerMetaEl || document.getElementById("studioUiRefreshToggleBtn")) return;
858
- const button = makeStudioUiRefreshElement("button", "footer-compact-btn studio-ui-refresh-toggle", studioUiRefreshEnabled ? "UI: Refresh" : "UI: Classic");
858
+ const button = makeStudioUiRefreshElement("button", "footer-compact-btn studio-ui-refresh-toggle", studioUiRefreshEnabled ? "UI: Fresh" : "UI: Classic");
859
859
  button.id = "studioUiRefreshToggleBtn";
860
860
  button.type = "button";
861
861
  button.title = studioUiRefreshEnabled
@@ -1265,7 +1265,24 @@
1265
1265
  }
1266
1266
  }
1267
1267
 
1268
- function formatContextUsageText() {
1268
+ function formatCompactNumber(value) {
1269
+ if (typeof value !== "number" || !Number.isFinite(value)) return "?";
1270
+ const sign = value < 0 ? "-" : "";
1271
+ const abs = Math.abs(value);
1272
+ if (abs < 1000) return sign + formatNumber(abs);
1273
+ const units = [
1274
+ { divisor: 1_000_000_000, suffix: "B" },
1275
+ { divisor: 1_000_000, suffix: "M" },
1276
+ { divisor: 1_000, suffix: "k" },
1277
+ ];
1278
+ const unit = units.find((entry) => abs >= entry.divisor) || units[units.length - 1];
1279
+ const scaled = abs / unit.divisor;
1280
+ const decimals = scaled >= 100 ? 0 : 1;
1281
+ return sign + scaled.toFixed(decimals).replace(/\.0$/, "") + unit.suffix;
1282
+ }
1283
+
1284
+ function formatContextUsageText(compact) {
1285
+ const formatContextNumber = compact ? formatCompactNumber : formatNumber;
1269
1286
  const hasWindow = typeof contextWindow === "number" && Number.isFinite(contextWindow) && contextWindow > 0;
1270
1287
  const hasTokens = typeof contextTokens === "number" && Number.isFinite(contextTokens) && contextTokens >= 0;
1271
1288
  let percentValue = typeof contextPercent === "number" && Number.isFinite(contextPercent)
@@ -1280,12 +1297,12 @@
1280
1297
  return "Context: unknown";
1281
1298
  }
1282
1299
  if (!hasTokens && hasWindow) {
1283
- return "Context: ? / " + formatNumber(contextWindow);
1300
+ return "Context: ? / " + formatContextNumber(contextWindow);
1284
1301
  }
1285
1302
 
1286
- let text = "Context: " + formatNumber(contextTokens);
1303
+ let text = "Context: " + formatContextNumber(contextTokens);
1287
1304
  if (hasWindow) {
1288
- text += " / " + formatNumber(contextWindow);
1305
+ text += " / " + formatContextNumber(contextWindow);
1289
1306
  }
1290
1307
  if (percentValue != null && Number.isFinite(percentValue)) {
1291
1308
  const bounded = Math.max(0, Math.min(100, percentValue));
@@ -1515,14 +1532,16 @@
1515
1532
  function updateFooterMeta() {
1516
1533
  const modelText = modelLabel && modelLabel.trim() ? modelLabel.trim() : "none";
1517
1534
  const terminalText = terminalSessionLabel && terminalSessionLabel.trim() ? terminalSessionLabel.trim() : "unknown";
1518
- const contextText = formatContextUsageText();
1535
+ const contextText = formatContextUsageText(true);
1536
+ const contextTitleText = formatContextUsageText(false);
1519
1537
  const text = "Model: " + modelText + " · Terminal: " + terminalText + " · " + contextText;
1538
+ const titleText = "Model: " + modelText + " · Terminal: " + terminalText + " · " + contextTitleText;
1520
1539
  if (footerMetaTextEl) {
1521
1540
  footerMetaTextEl.textContent = text;
1522
- footerMetaTextEl.title = text;
1541
+ footerMetaTextEl.title = titleText;
1523
1542
  } else if (footerMetaEl) {
1524
1543
  footerMetaEl.textContent = text;
1525
- footerMetaEl.title = text;
1544
+ footerMetaEl.title = titleText;
1526
1545
  }
1527
1546
  updateDocumentTitle();
1528
1547
  }
@@ -5750,6 +5769,87 @@
5750
5769
  };
5751
5770
  }
5752
5771
 
5772
+ function getDiffFileLabelForLine(source, lineNumber) {
5773
+ const lines = String(source || "").replace(/\r\n/g, "\n").split("\n");
5774
+ const safeLine = Math.max(1, Math.min(Math.floor(Number(lineNumber) || 1), Math.max(1, lines.length)));
5775
+ let currentFile = "";
5776
+ for (let i = 0; i < safeLine; i += 1) {
5777
+ const line = String(lines[i] || "");
5778
+ const diffMatch = line.match(/^diff --git\s+a\/(.+?)\s+b\/(.+?)\s*$/);
5779
+ if (diffMatch) {
5780
+ currentFile = diffMatch[2] || diffMatch[1] || currentFile;
5781
+ continue;
5782
+ }
5783
+ const plusMatch = line.match(/^\+\+\+\s+(?:b\/)?(.+)\s*$/);
5784
+ if (plusMatch && plusMatch[1] && plusMatch[1] !== "/dev/null") {
5785
+ currentFile = plusMatch[1];
5786
+ }
5787
+ }
5788
+ return currentFile.trim();
5789
+ }
5790
+
5791
+ function getReviewNotePromptFileLabel(note, source) {
5792
+ if (sourceState && sourceState.path) return String(sourceState.path);
5793
+ const bounds = getResolvedReviewNoteLineBounds(note, source);
5794
+ const diffFile = bounds ? getDiffFileLabelForLine(source, bounds.lineStart) : "";
5795
+ if (diffFile) return diffFile;
5796
+ const descriptor = getCurrentStudioDocumentDescriptor();
5797
+ return descriptor && descriptor.fileBacked ? descriptor.label : "";
5798
+ }
5799
+
5800
+ function formatReviewNotePromptLineRange(bounds, note) {
5801
+ const start = bounds ? bounds.lineStart : Math.max(1, Number(note && note.lineStart) || 1);
5802
+ const end = bounds ? bounds.lineEnd : Math.max(start, Number(note && note.lineEnd) || start);
5803
+ return start === end ? "L" + start : ("L" + start + "-L" + end);
5804
+ }
5805
+
5806
+ function buildReviewNotesPrompt() {
5807
+ const source = String(sourceTextEl && sourceTextEl.value ? sourceTextEl.value : "");
5808
+ const notes = getDisplayReviewNotes().filter((note) => String(note && note.text ? note.text : "").trim());
5809
+ if (!notes.length) return "";
5810
+
5811
+ const descriptor = getCurrentStudioDocumentDescriptor();
5812
+ const documentLabel = descriptor && descriptor.label ? descriptor.label : (sourceState && sourceState.label ? sourceState.label : "Studio document");
5813
+ const parts = [
5814
+ "Please address the following Studio comments. Use the file names and line numbers as anchors. The full document is not included here, only the comments and their anchors.",
5815
+ "Document: " + documentLabel,
5816
+ "",
5817
+ "## Comments",
5818
+ ];
5819
+
5820
+ notes.forEach((note, index) => {
5821
+ const bounds = getResolvedReviewNoteLineBounds(note, source);
5822
+ const fileLabel = getReviewNotePromptFileLabel(note, source);
5823
+ const location = (fileLabel ? (fileLabel + ":") : "") + formatReviewNotePromptLineRange(bounds, note);
5824
+ const comment = String(note && note.text ? note.text : "").trim();
5825
+ const anchor = String(note && (note.selectedDisplayText || note.selectedText) ? (note.selectedDisplayText || note.selectedText) : "")
5826
+ .replace(/\s+/g, " ")
5827
+ .trim();
5828
+ parts.push(
5829
+ "### Comment " + (index + 1) + " — " + location,
5830
+ "",
5831
+ comment,
5832
+ );
5833
+ if (anchor) {
5834
+ parts.push("", "> " + anchor.replace(/\n/g, "\n> "));
5835
+ }
5836
+ parts.push("");
5837
+ });
5838
+
5839
+ return parts.join("\n").replace(/\n{3,}/g, "\n\n").trim() + "\n";
5840
+ }
5841
+
5842
+ function loadReviewNotesPromptIntoEditor() {
5843
+ const prompt = buildReviewNotesPrompt();
5844
+ if (!prompt.trim()) {
5845
+ setStatus("No non-empty comments to load as a prompt.", "warning");
5846
+ return;
5847
+ }
5848
+ setEditorText(prompt, { preserveScroll: false, preserveSelection: false });
5849
+ setSourceState({ source: "blank", label: "comments prompt", path: null });
5850
+ setStatus("Loaded comments prompt into editor.", "success");
5851
+ }
5852
+
5753
5853
  function buildReviewNoteLineMap(text) {
5754
5854
  const source = String(text || "");
5755
5855
  const lineMap = new Map();
@@ -8573,12 +8673,19 @@
8573
8673
  ? "Select preview text and use Comment for a local preview-anchored comment."
8574
8674
  : "Switch to Editor (Raw) to comment on the current line.");
8575
8675
  }
8676
+ if (reviewNotesPromptBtn) {
8677
+ const promptCandidates = reviewNotes.filter((note) => String(note && note.text ? note.text : "").trim());
8678
+ reviewNotesPromptBtn.disabled = uiBusy || promptCandidates.length === 0;
8679
+ reviewNotesPromptBtn.title = promptCandidates.length > 0
8680
+ ? "Load local comments, line numbers, and file labels into the editor as a prompt."
8681
+ : "No non-empty local comments to load as a prompt.";
8682
+ }
8576
8683
  if (reviewNotesInlineAllBtn) {
8577
8684
  const currentText = String(sourceTextEl && sourceTextEl.value ? sourceTextEl.value : "");
8578
8685
  const toggleCandidates = getDisplayReviewNotes().filter((note) => getReviewNoteInlineState(note, currentText).canToggle);
8579
8686
  const allInline = toggleCandidates.length > 0 && toggleCandidates.every((note) => getReviewNoteInlineState(note, currentText).exists);
8580
8687
  reviewNotesInlineAllBtn.disabled = uiBusy || toggleCandidates.length === 0;
8581
- reviewNotesInlineAllBtn.textContent = allInline ? "All inline: On" : "All inline: Off";
8688
+ reviewNotesInlineAllBtn.textContent = allInline ? "Inline: On" : "Inline: Off";
8582
8689
  reviewNotesInlineAllBtn.setAttribute("aria-pressed", allInline ? "true" : "false");
8583
8690
  reviewNotesInlineAllBtn.title = allInline
8584
8691
  ? "Inline annotations derived from all non-empty comments are currently on. Click to remove them."
@@ -9705,6 +9812,7 @@
9705
9812
  setBusy(false);
9706
9813
  setWsState("Ready");
9707
9814
 
9815
+ pendingResponseScrollReset = true;
9708
9816
  let appliedFromHistory = false;
9709
9817
  if (Array.isArray(message.responseHistory)) {
9710
9818
  appliedFromHistory = setResponseHistory(message.responseHistory, {
@@ -9733,6 +9841,9 @@
9733
9841
  if (pendingRequestId) return;
9734
9842
 
9735
9843
  const hasHistory = Array.isArray(message.responseHistory);
9844
+ if (followLatest) {
9845
+ pendingResponseScrollReset = true;
9846
+ }
9736
9847
  if (hasHistory) {
9737
9848
  setResponseHistory(message.responseHistory, {
9738
9849
  autoSelectLatest: followLatest,
@@ -9756,7 +9867,7 @@
9756
9867
  return;
9757
9868
  }
9758
9869
 
9759
- if (!hasHistory && applyLatestPayload(payload)) {
9870
+ if (!hasHistory && applyLatestPayload(payload, { resetScroll: true })) {
9760
9871
  queuedLatestResponse = null;
9761
9872
  updateResultActionButtons();
9762
9873
  setStatus("Updated from latest response.", "success");
@@ -10926,6 +11037,12 @@
10926
11037
  });
10927
11038
  }
10928
11039
 
11040
+ if (reviewNotesPromptBtn) {
11041
+ reviewNotesPromptBtn.addEventListener("click", () => {
11042
+ loadReviewNotesPromptIntoEditor();
11043
+ });
11044
+ }
11045
+
10929
11046
  if (reviewNotesInlineAllBtn) {
10930
11047
  reviewNotesInlineAllBtn.addEventListener("click", () => {
10931
11048
  toggleAllReviewNotesInlineAnnotations();
package/client/studio.css CHANGED
@@ -2113,7 +2113,13 @@
2113
2113
 
2114
2114
  .review-notes-dock-footer .scratchpad-actions {
2115
2115
  width: 100%;
2116
- justify-content: space-between;
2116
+ gap: 6px;
2117
+ justify-content: flex-end;
2118
+ }
2119
+
2120
+ .review-notes-dock-footer .scratchpad-actions button {
2121
+ padding: 5px 7px;
2122
+ font-size: 11px;
2117
2123
  }
2118
2124
 
2119
2125
  .outline-list {
@@ -2315,48 +2321,48 @@
2315
2321
  }
2316
2322
  }
2317
2323
 
2318
- /* Opt-in editor-side layout refresh prototype. Enabled with ?uiRefresh=1 or localStorage piStudio.uiRefresh=1. */
2324
+ /* Default refreshed Studio layout. Classic layout remains available with ?uiRefresh=0 or the footer UI switch. */
2319
2325
  body.studio-ui-refresh {
2320
- font-size: 14px;
2326
+ font-size: 13px;
2321
2327
  }
2322
2328
 
2323
2329
  body.studio-ui-refresh > header {
2324
- padding: 9px 14px;
2325
- gap: 10px;
2330
+ padding: 8px 12px;
2331
+ gap: 9px;
2326
2332
  }
2327
2333
 
2328
2334
  body.studio-ui-refresh h1 {
2329
- font-size: 17px;
2335
+ font-size: 16px;
2330
2336
  }
2331
2337
 
2332
2338
  body.studio-ui-refresh .app-logo {
2333
- font-size: 20px;
2339
+ font-size: 18px;
2334
2340
  }
2335
2341
 
2336
2342
  body.studio-ui-refresh .app-subtitle {
2337
- font-size: 11px;
2343
+ font-size: 10px;
2338
2344
  }
2339
2345
 
2340
2346
  body.studio-ui-refresh > header .controls {
2341
- gap: 6px;
2347
+ gap: 5px;
2342
2348
  }
2343
2349
 
2344
2350
  body.studio-ui-refresh > header button,
2345
2351
  body.studio-ui-refresh > header .file-label,
2346
2352
  body.studio-ui-refresh #responseActions button,
2347
2353
  body.studio-ui-refresh #responseActions select {
2348
- padding: 6px 8px;
2349
- font-size: 13px;
2354
+ padding: 5px 7px;
2355
+ font-size: 12px;
2350
2356
  }
2351
2357
 
2352
2358
  body.studio-ui-refresh main {
2353
- gap: 10px;
2354
- padding: 10px;
2359
+ gap: 9px;
2360
+ padding: 9px;
2355
2361
  }
2356
2362
 
2357
2363
  body.studio-ui-refresh footer {
2358
- padding: 8px 10px;
2359
- font-size: 12px;
2364
+ padding: 7px 9px;
2365
+ font-size: 11px;
2360
2366
  }
2361
2367
 
2362
2368
  .studio-ui-refresh-toggle {
@@ -2378,9 +2384,9 @@
2378
2384
  position: relative;
2379
2385
  z-index: 30;
2380
2386
  display: grid;
2381
- gap: 7px;
2387
+ gap: 6px;
2382
2388
  background: transparent;
2383
- padding: 8px 10px 7px;
2389
+ padding: 7px 9px 6px;
2384
2390
  overflow: visible;
2385
2391
  }
2386
2392
 
@@ -2402,7 +2408,7 @@
2402
2408
  body.studio-ui-refresh .studio-refresh-action-line {
2403
2409
  display: flex;
2404
2410
  align-items: center;
2405
- gap: 7px;
2411
+ gap: 6px;
2406
2412
  min-width: 0;
2407
2413
  flex-wrap: wrap;
2408
2414
  }
@@ -2412,7 +2418,7 @@
2412
2418
  display: grid;
2413
2419
  grid-template-columns: minmax(0, 1fr) auto;
2414
2420
  align-items: center;
2415
- gap: 10px;
2421
+ gap: 9px;
2416
2422
  min-width: 0;
2417
2423
  }
2418
2424
 
@@ -2420,8 +2426,8 @@
2420
2426
  display: grid;
2421
2427
  grid-template-columns: auto minmax(0, 1fr);
2422
2428
  align-items: center;
2423
- column-gap: 10px;
2424
- row-gap: 6px;
2429
+ column-gap: 9px;
2430
+ row-gap: 5px;
2425
2431
  }
2426
2432
 
2427
2433
  body.studio-ui-refresh .studio-refresh-title-group,
@@ -2437,7 +2443,7 @@
2437
2443
  }
2438
2444
 
2439
2445
  body.studio-ui-refresh .studio-refresh-title-group {
2440
- gap: 3px;
2446
+ gap: 2px;
2441
2447
  }
2442
2448
 
2443
2449
  body.studio-ui-refresh .studio-refresh-pane-tools {
@@ -2448,15 +2454,15 @@
2448
2454
  body.studio-ui-refresh .studio-refresh-sep {
2449
2455
  display: inline-block;
2450
2456
  width: 1px;
2451
- height: 18px;
2457
+ height: 16px;
2452
2458
  background: var(--border-muted);
2453
2459
  margin: 0 1px;
2454
2460
  flex: 0 0 1px;
2455
2461
  }
2456
2462
 
2457
2463
  body.studio-ui-refresh .studio-refresh-icon {
2458
- width: 17px;
2459
- height: 17px;
2464
+ width: 15px;
2465
+ height: 15px;
2460
2466
  stroke: currentColor;
2461
2467
  stroke-width: 1.85;
2462
2468
  stroke-linecap: round;
@@ -2473,7 +2479,7 @@
2473
2479
  body.studio-ui-refresh .studio-refresh-toolbar select {
2474
2480
  border-color: transparent;
2475
2481
  background: transparent;
2476
- font-size: 14px;
2482
+ font-size: 13px;
2477
2483
  }
2478
2484
 
2479
2485
  body.studio-ui-refresh #leftSectionHeader select:hover,
@@ -2487,7 +2493,7 @@
2487
2493
 
2488
2494
  body.studio-ui-refresh #leftSectionHeader #editorViewSelect,
2489
2495
  body.studio-ui-refresh #rightSectionHeader #rightViewSelect {
2490
- font-size: 15px;
2496
+ font-size: 14px;
2491
2497
  font-weight: 750;
2492
2498
  padding: 3px 5px;
2493
2499
  max-width: 230px;
@@ -2495,7 +2501,7 @@
2495
2501
 
2496
2502
  body.studio-ui-refresh .studio-refresh-static-title {
2497
2503
  color: var(--text);
2498
- font-size: 15px;
2504
+ font-size: 14px;
2499
2505
  font-weight: 700;
2500
2506
  padding: 3px 5px;
2501
2507
  max-width: 230px;
@@ -2504,9 +2510,9 @@
2504
2510
 
2505
2511
  body.studio-ui-refresh #leftFocusBtn,
2506
2512
  body.studio-ui-refresh #rightFocusBtn {
2507
- width: 32px;
2508
- min-width: 32px;
2509
- min-height: 32px;
2513
+ width: 29px;
2514
+ min-width: 29px;
2515
+ min-height: 29px;
2510
2516
  padding: 0;
2511
2517
  color: var(--muted);
2512
2518
  align-items: center;
@@ -2518,8 +2524,8 @@
2518
2524
  body.studio-ui-refresh #resourceDirLabel {
2519
2525
  border-color: transparent;
2520
2526
  background: transparent;
2521
- padding: 4px 6px;
2522
- font-size: 14px;
2527
+ padding: 3px 5px;
2528
+ font-size: 13px;
2523
2529
  border-radius: 8px;
2524
2530
  }
2525
2531
 
@@ -2548,11 +2554,11 @@
2548
2554
  body.studio-ui-refresh #syncBadge {
2549
2555
  display: inline-flex;
2550
2556
  align-items: center;
2551
- gap: 6px;
2552
- min-height: 26px;
2557
+ gap: 5px;
2558
+ min-height: 24px;
2553
2559
  border: 0;
2554
2560
  border-radius: 999px;
2555
- padding: 3px 9px;
2561
+ padding: 2px 8px;
2556
2562
  background: var(--panel-2);
2557
2563
  color: var(--muted);
2558
2564
  opacity: 1;
@@ -2564,8 +2570,8 @@
2564
2570
 
2565
2571
  body.studio-ui-refresh #syncBadge::before {
2566
2572
  content: "";
2567
- width: 7px;
2568
- height: 7px;
2573
+ width: 6px;
2574
+ height: 6px;
2569
2575
  border-radius: 999px;
2570
2576
  background: var(--success, #22c55e);
2571
2577
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--success, #22c55e) 14%, transparent);
@@ -2577,8 +2583,8 @@
2577
2583
  body.studio-ui-refresh #exportPdfBtn,
2578
2584
  body.studio-ui-refresh .studio-refresh-tool-tab {
2579
2585
  font-weight: 500;
2580
- padding: 6px 9px;
2581
- border-radius: 9px;
2586
+ padding: 5px 8px;
2587
+ border-radius: 8px;
2582
2588
  }
2583
2589
 
2584
2590
  body.studio-ui-refresh #exportPdfBtn {
@@ -2611,33 +2617,99 @@
2611
2617
  }
2612
2618
 
2613
2619
  body.studio-ui-refresh .source-body {
2614
- padding: 8px;
2620
+ padding: 7px;
2621
+ }
2622
+
2623
+ body.studio-ui-refresh textarea {
2624
+ font-size: 12px;
2625
+ line-height: 1.42;
2626
+ }
2627
+
2628
+ body.studio-ui-refresh .editor-highlight,
2629
+ body.studio-ui-refresh .editor-line-number-measure {
2630
+ font-size: 12px;
2631
+ line-height: 1.42;
2632
+ }
2633
+
2634
+ body.studio-ui-refresh #sourceText,
2635
+ body.studio-ui-refresh .editor-highlight {
2636
+ padding: 9px 9px 9px calc(9px + var(--editor-review-note-gutter-width) + var(--editor-line-number-gutter-width));
2637
+ }
2638
+
2639
+ body.studio-ui-refresh .editor-line-number-gutter-content {
2640
+ padding: 9px 7px 9px 0;
2641
+ font-size: 11px;
2642
+ line-height: 1.42;
2643
+ }
2644
+
2645
+ body.studio-ui-refresh .editor-review-note-gutter-content {
2646
+ padding: 9px 4px;
2647
+ }
2648
+
2649
+ body.studio-ui-refresh .panel-scroll {
2650
+ padding: 11px;
2651
+ line-height: 1.48;
2652
+ font-size: 13px;
2653
+ }
2654
+
2655
+ body.studio-ui-refresh .rendered-markdown {
2656
+ line-height: 1.52;
2657
+ font-size: 13.5px;
2658
+ }
2659
+
2660
+ body.studio-ui-refresh .plain-markdown,
2661
+ body.studio-ui-refresh .response-markdown-highlight {
2662
+ font-size: 12px;
2663
+ line-height: 1.42;
2664
+ }
2665
+
2666
+ body.studio-ui-refresh .rendered-markdown pre {
2667
+ padding: 11px 13px;
2668
+ }
2669
+
2670
+ body.studio-ui-refresh .trace-panel,
2671
+ body.studio-ui-refresh .trace-card,
2672
+ body.studio-ui-refresh .trace-toolbar,
2673
+ body.studio-ui-refresh .trace-summary,
2674
+ body.studio-ui-refresh .trace-controls,
2675
+ body.studio-ui-refresh .trace-card-header {
2676
+ gap: 7px;
2677
+ }
2678
+
2679
+ body.studio-ui-refresh .trace-card {
2680
+ padding: 9px 11px;
2681
+ }
2682
+
2683
+ body.studio-ui-refresh .trace-output {
2684
+ padding: 9px 10px;
2685
+ font-size: 12px;
2686
+ line-height: 1.42;
2615
2687
  }
2616
2688
 
2617
2689
  body.studio-ui-refresh .studio-refresh-toolbar {
2618
2690
  position: relative;
2619
- padding: 9px 12px 10px;
2691
+ padding: 8px 10px 9px;
2620
2692
  overflow: visible;
2621
2693
  }
2622
2694
 
2623
2695
  body.studio-ui-refresh .studio-refresh-toolbar-main {
2624
2696
  display: grid;
2625
2697
  grid-template-columns: minmax(0, 1fr) auto;
2626
- gap: 12px;
2698
+ gap: 10px;
2627
2699
  align-items: start;
2628
2700
  min-width: 0;
2629
2701
  }
2630
2702
 
2631
2703
  body.studio-ui-refresh .studio-refresh-toolbar-actions {
2632
2704
  display: grid;
2633
- gap: 7px;
2705
+ gap: 6px;
2634
2706
  justify-items: start;
2635
2707
  min-width: 0;
2636
2708
  }
2637
2709
 
2638
2710
  body.studio-ui-refresh .studio-refresh-toolbar-state {
2639
2711
  display: grid;
2640
- gap: 7px;
2712
+ gap: 6px;
2641
2713
  justify-items: end;
2642
2714
  align-content: start;
2643
2715
  min-width: max-content;
@@ -2646,18 +2718,18 @@
2646
2718
  body.studio-ui-refresh .studio-refresh-chip {
2647
2719
  display: inline-flex;
2648
2720
  align-items: center;
2649
- gap: 6px;
2650
- border-radius: 9px;
2721
+ gap: 5px;
2722
+ border-radius: 8px;
2651
2723
  color: var(--text);
2652
2724
  white-space: nowrap;
2653
- padding: 6px 9px;
2654
- font-size: 14px;
2725
+ padding: 5px 8px;
2726
+ font-size: 13px;
2655
2727
  }
2656
2728
 
2657
2729
  body.studio-ui-refresh .studio-refresh-chip::after {
2658
2730
  content: "⌄";
2659
2731
  color: var(--muted);
2660
- font-size: 15px;
2732
+ font-size: 14px;
2661
2733
  line-height: 1;
2662
2734
  transform: translateY(-1px);
2663
2735
  }
@@ -2749,27 +2821,24 @@
2749
2821
  }
2750
2822
 
2751
2823
  body.studio-ui-refresh #copyDraftBtn:only-child {
2752
- min-height: 32px;
2753
- padding: 5px 9px;
2824
+ min-height: 29px;
2825
+ padding: 4px 8px;
2754
2826
  }
2755
2827
 
2756
2828
  body.studio-ui-refresh #sendRunBtn,
2757
2829
  body.studio-ui-refresh #queueSteerBtn,
2758
2830
  body.studio-ui-refresh #loadResponseBtn:not([hidden]) {
2759
- height: 30px;
2760
- min-height: 30px;
2761
- padding: 4px 10px;
2762
- font-size: 13px;
2831
+ height: 28px;
2832
+ min-height: 28px;
2833
+ padding: 4px 9px;
2834
+ font-size: 12px;
2763
2835
  line-height: 1.2;
2764
2836
  border-radius: 8px;
2765
2837
  }
2766
2838
 
2767
- body.studio-ui-refresh #sendRunBtn {
2768
- min-width: 9.2rem;
2769
- }
2770
-
2839
+ body.studio-ui-refresh #sendRunBtn,
2771
2840
  body.studio-ui-refresh #queueSteerBtn {
2772
- min-width: auto;
2841
+ min-width: 8.6rem;
2773
2842
  }
2774
2843
 
2775
2844
  body.studio-ui-refresh #queueSteerBtn:not(:disabled) {
package/index.ts CHANGED
@@ -5764,34 +5764,74 @@ function createEmptyStudioTraceState(): StudioTraceState {
5764
5764
  };
5765
5765
  }
5766
5766
 
5767
+ function sanitizeStudioTraceOutputText(text: string): string {
5768
+ return String(text || "")
5769
+ .replace(/data:image\/([a-zA-Z0-9.+-]+);base64,[A-Za-z0-9+/=\r\n]+/g, (_match, subtype: string) => `[Image: image/${subtype || "unknown"} data omitted]`)
5770
+ .replace(/(\"(?:data|image|base64|content)\"\s*:\s*\")[A-Za-z0-9+/=]{1000,}(\")/g, "$1[base64 data omitted]$2")
5771
+ .replace(/\b[A-Za-z0-9+/]{3000,}={0,2}\b/g, "[base64 data omitted]");
5772
+ }
5773
+
5774
+ function isStudioTraceImageBlock(block: unknown): boolean {
5775
+ if (!block || typeof block !== "object") return false;
5776
+ const payload = block as Record<string, unknown>;
5777
+ const type = typeof payload.type === "string" ? payload.type.toLowerCase() : "";
5778
+ if (type.includes("image")) return true;
5779
+ const mime = typeof payload.mimeType === "string"
5780
+ ? payload.mimeType
5781
+ : (typeof payload.media_type === "string" ? payload.media_type : "");
5782
+ if (mime.toLowerCase().startsWith("image/")) return true;
5783
+ const source = payload.source && typeof payload.source === "object" ? payload.source as Record<string, unknown> : null;
5784
+ const sourceMime = source && typeof source.media_type === "string" ? source.media_type : "";
5785
+ return sourceMime.toLowerCase().startsWith("image/");
5786
+ }
5787
+
5788
+ function describeStudioTraceImageBlock(block: unknown): string {
5789
+ const payload = (block && typeof block === "object") ? block as Record<string, unknown> : {};
5790
+ const source = payload.source && typeof payload.source === "object" ? payload.source as Record<string, unknown> : null;
5791
+ const mime = typeof payload.mimeType === "string"
5792
+ ? payload.mimeType
5793
+ : (typeof payload.media_type === "string"
5794
+ ? payload.media_type
5795
+ : (source && typeof source.media_type === "string" ? source.media_type : "image"));
5796
+ return `[Image: ${mime || "image"} output omitted from Working view]`;
5797
+ }
5798
+
5799
+ function stringifyStudioTraceObject(value: unknown): string {
5800
+ try {
5801
+ return sanitizeStudioTraceOutputText(JSON.stringify(value, (_key, item) => {
5802
+ if (typeof item === "string") {
5803
+ if (/^data:image\//i.test(item)) return "[image data URI omitted]";
5804
+ if (/^[A-Za-z0-9+/=]{1000,}$/.test(item)) return "[base64 data omitted]";
5805
+ }
5806
+ return item;
5807
+ }, 2));
5808
+ } catch {
5809
+ return sanitizeStudioTraceOutputText(String(value));
5810
+ }
5811
+ }
5812
+
5767
5813
  function formatStudioTraceOutput(result: unknown): string {
5768
5814
  if (result == null) return "";
5769
- if (typeof result === "string") return result;
5815
+ if (typeof result === "string") return sanitizeStudioTraceOutputText(result);
5770
5816
  if (Array.isArray(result)) {
5771
5817
  return result.map((item) => formatStudioTraceOutput(item)).filter(Boolean).join("\n");
5772
5818
  }
5773
5819
  if (typeof result === "object") {
5820
+ if (isStudioTraceImageBlock(result)) return describeStudioTraceImageBlock(result);
5774
5821
  const payload = result as { content?: Array<{ type?: string; text?: string }> };
5775
5822
  if (Array.isArray(payload.content)) {
5776
5823
  return payload.content
5777
5824
  .map((block) => {
5778
- if (block && block.type === "text" && typeof block.text === "string") return block.text;
5779
- try {
5780
- return JSON.stringify(block, null, 2);
5781
- } catch {
5782
- return String(block);
5783
- }
5825
+ if (isStudioTraceImageBlock(block)) return describeStudioTraceImageBlock(block);
5826
+ if (block && block.type === "text" && typeof block.text === "string") return sanitizeStudioTraceOutputText(block.text);
5827
+ return stringifyStudioTraceObject(block);
5784
5828
  })
5785
5829
  .filter(Boolean)
5786
5830
  .join("\n");
5787
5831
  }
5788
- try {
5789
- return JSON.stringify(result, null, 2);
5790
- } catch {
5791
- return String(result);
5792
- }
5832
+ return stringifyStudioTraceObject(result);
5793
5833
  }
5794
- return String(result);
5834
+ return sanitizeStudioTraceOutputText(String(result));
5795
5835
  }
5796
5836
 
5797
5837
  function summarizeStudioTraceToolArgs(toolName: string, args: unknown): string | null {
@@ -6260,8 +6300,9 @@ ${cssVarsBlock}
6260
6300
  <div id="reviewNotesList" class="review-notes-list" aria-live="polite"></div>
6261
6301
  <div class="review-notes-dock-footer">
6262
6302
  <div class="scratchpad-actions">
6263
- <button id="reviewNotesAddBtn" type="button" title="Create a new local comment on the current editor line.">Line comment</button>
6264
- <button id="reviewNotesInlineAllBtn" type="button" title="Toggle inline annotations for all non-empty comments.">All inline: Off</button>
6303
+ <button id="reviewNotesAddBtn" type="button" title="Create a new local comment on the current editor line.">Line</button>
6304
+ <button id="reviewNotesPromptBtn" type="button" title="Load local comments, line numbers, and file labels into the editor as a prompt.">Comments prompt</button>
6305
+ <button id="reviewNotesInlineAllBtn" type="button" title="Toggle inline annotations for all non-empty comments.">Inline: Off</button>
6265
6306
  <button id="reviewNotesDeleteAllBtn" type="button" title="Delete all local comments for this document or draft.">Delete all</button>
6266
6307
  <button id="reviewNotesDoneBtn" type="button" title="Hide the comments rail.">Hide</button>
6267
6308
  </div>
@@ -7228,6 +7269,20 @@ export default function (pi: ExtensionAPI) {
7228
7269
  promptTriggerText: descriptor.promptTriggerText,
7229
7270
  };
7230
7271
  queuedStudioDirectRequests.push(queuedRequest);
7272
+
7273
+ // Steering is delivered into the currently running Studio turn rather than
7274
+ // becoming a fully separate visible response request. Keep the active direct
7275
+ // request metadata aligned with the effective prompt chain so persisted
7276
+ // prompt metadata, response history, and "Load effective prompt" all refer
7277
+ // to the original run plus queued steering, not just the original run.
7278
+ if (activeRequest && activeRequest.kind === "direct") {
7279
+ activeRequest.prompt = descriptor.prompt;
7280
+ activeRequest.promptMode = descriptor.promptMode;
7281
+ activeRequest.promptTriggerKind = descriptor.promptTriggerKind;
7282
+ activeRequest.promptSteeringCount = descriptor.promptSteeringCount;
7283
+ activeRequest.promptTriggerText = descriptor.promptTriggerText;
7284
+ }
7285
+
7231
7286
  return queuedRequest;
7232
7287
  };
7233
7288
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-studio",
3
- "version": "0.5.59",
3
+ "version": "0.6.0",
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",