@quandev104/pi-style 0.2.3 → 0.2.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.2.5] - 2026-08-25
4
+
5
+ ### Bug Fixes
6
+
7
+ - *(messages)* Preserve text in tool-calling messages
8
+
3
9
  ## [0.2.3] - 2026-08-22
4
10
 
5
11
  ### Documentation
package/README.md CHANGED
@@ -17,7 +17,6 @@
17
17
  - **Editor** — compact/boxed/dock `CustomEditor` treatments with prompt glyph, metadata rows, and thinking-level border, preserving Pi keybindings and autocomplete.
18
18
  - **Startup** — compact gradient logo header and optional overlay with System & Context / Available Tools panels, rendered from snapshot data collected before mount.
19
19
  - **Messages** — assistant prefix and boxed compaction/skill/branch/MCP special blocks, and certified tool call/result selectors with pending/running/error markers.
20
- - **Interim narration hiding** — the text of assistant messages that also carry tool calls is hidden (`messages.hideInterimText`): the feed shows only the tool blocks, the run summary, and the final answer.
21
20
  - **Turn summaries** — when a turn completes, its finalized tool blocks collapse into one summary line (`➔ Read 2 files, ran 4 shell commands · 3.1s`); errors and interrupted turns stay visible, and Pi's global Ctrl+O toggle expands everything again (`tools.collapseAfterTurn`).
22
21
  - **User-prompt image previews** — images attached to your prompt render inline **below your message** as display-only session entries (never sent to the LLM) with `#N · WxH` labels tying each image to its marker; multiple images lay out side-by-side on kitty-capable terminals (up to 3 columns, stacked fallback elsewhere), sized by `messages.previewMaxWidth` (default 30) with Pi's global Ctrl+O expanding to 60 (`messages.showImagePreviews`).
23
22
  - **Clipboard image input** — the built-in `Ctrl+V` becomes a full image-paste flow with no other extension required: a sync native probe checks the clipboard at keystroke time, an `[Image #N] ` marker appears instantly in the editor (bytes attach asynchronously — no temp-file flash), backspacing right after a marker deletes it as one unit (discarding the image, image-paste semantics), and submit attaches real image attachments; raw pasted paths that bypass the editor still upgrade to `[image]` + attachment (`messages.clipboardImages`).
@@ -194,7 +193,7 @@ npm run check
194
193
 
195
194
  `npm run check` runs all required automated gates in order.
196
195
 
197
- Current test suite: 817 tests across 41 files.
196
+ Current test suite: 819 tests across 41 files.
198
197
 
199
198
  ---
200
199
 
@@ -343,6 +343,7 @@ function rewriteTokens(text, rewritten, tmpRoot) {
343
343
  // extension-src/pi-style/features/messages/image-preview.ts
344
344
  import {
345
345
  getCapabilities,
346
+ getCellDimensions,
346
347
  getImageDimensions,
347
348
  Image
348
349
  } from "@earendil-works/pi-tui";
@@ -569,6 +570,8 @@ var IMAGE_PREVIEW_EXPANDED_MAX_WIDTH_CELLS = 60;
569
570
  var IMAGE_PREVIEW_MAX_WIDTH_CELLS = 60;
570
571
  var GRID_GAP = 2;
571
572
  var GRID_MIN_COLUMN = 14;
573
+ var GRID_MAX_HEIGHT_RATIO = 1.5;
574
+ var FALLBACK_IMAGE_DIMENSIONS = { widthPx: 800, heightPx: 600 };
572
575
  function isPreviewableImage(value) {
573
576
  if (!value || typeof value !== "object") return false;
574
577
  const data = value.data;
@@ -624,10 +627,10 @@ function renderImagePreviewEntry(entry, options, theme) {
624
627
  )
625
628
  );
626
629
  const labels2 = images.map((_image, index) => imageLabel(themed, index + 1, dims[index]));
627
- cached = { images, components: components2, labels: labels2 };
630
+ cached = { images, dimensions: dims, components: components2, labels: labels2 };
628
631
  previewCache.set(entry, cached);
629
632
  }
630
- const { components, labels } = cached;
633
+ const { components, dimensions, labels } = cached;
631
634
  let memo;
632
635
  return {
633
636
  invalidate() {
@@ -643,7 +646,7 @@ function renderImagePreviewEntry(entry, options, theme) {
643
646
  let lines;
644
647
  if (width <= 0) lines = [];
645
648
  else if (images.length === 1 || !kitty) lines = renderStacked(width, maxWidth, labels, components);
646
- else lines = renderGrid(width, maxWidth, labels, components);
649
+ else lines = renderGrid(width, maxWidth, labels, components, dimensions);
647
650
  memo = { key, lines };
648
651
  return lines;
649
652
  }
@@ -667,7 +670,7 @@ function stripTrailingSpaces(line) {
667
670
  while (end > 0 && line.charCodeAt(end - 1) === 32) end--;
668
671
  return line.slice(0, end);
669
672
  }
670
- function renderGrid(width, maxWidth, labels, components) {
673
+ function renderGrid(width, maxWidth, labels, components, dimensions) {
671
674
  const usable = Math.max(1, width - 2);
672
675
  const maxColumns = Math.floor((usable + GRID_GAP) / (GRID_MIN_COLUMN + GRID_GAP));
673
676
  const columns = Math.max(1, Math.min(components.length, maxColumns, 3));
@@ -675,31 +678,56 @@ function renderGrid(width, maxWidth, labels, components) {
675
678
  const columnWidth = Math.min(maxWidth, Math.floor((usable - (columns - 1) * GRID_GAP) / columns));
676
679
  if (columnWidth < GRID_MIN_COLUMN) return renderStacked(width, maxWidth, labels, components);
677
680
  const lines = [];
681
+ const cellDimensions = getCellDimensions();
678
682
  for (let start = 0; start < components.length; start += columns) {
679
683
  const group = components.slice(start, start + columns);
680
684
  const groupLabels = labels.slice(start, start + columns);
685
+ const groupDimensions = dimensions.slice(start, start + columns);
681
686
  if (start > 0) lines.push("");
682
687
  const rendered = group.map((component) => component.render(columnWidth));
683
- const labelRow = groupLabels.map((label, _index) => {
684
- const pad = Math.max(0, columnWidth - visibleWidth(stripAnsi(label)));
688
+ const sizes = groupDimensions.map((imageDimensions) => imageCellSize(imageDimensions, columnWidth, cellDimensions));
689
+ const rowCounts = rendered.map((column) => column.length).filter((count) => count > 0);
690
+ const minRows = Math.min(...rowCounts);
691
+ const maxRows = Math.max(...rowCounts);
692
+ if (rowCounts.length > 1 && minRows > 0 && maxRows >= minRows * GRID_MAX_HEIGHT_RATIO) {
693
+ return renderStacked(width, maxWidth, labels, components);
694
+ }
695
+ const slotWidths = groupLabels.map(
696
+ (label, index) => Math.max(sizes[index]?.columns ?? 1, visibleWidth(stripAnsi(label)))
697
+ );
698
+ const labelRow = groupLabels.map((label, index) => {
699
+ const pad = Math.max(0, (slotWidths[index] ?? 0) - visibleWidth(stripAnsi(label)));
685
700
  return label + " ".repeat(pad);
686
701
  }).join(" ".repeat(GRID_GAP)).trimEnd();
687
702
  lines.push(labelRow);
688
703
  const rows = Math.max(...rendered.map((column) => column.length));
689
704
  for (let row = 0; row < rows; row++) {
690
705
  let line = "";
706
+ let startCol = 0;
691
707
  for (let column = 0; column < rendered.length; column++) {
692
- if (column > 0) {
693
- const startCol = column * (columnWidth + GRID_GAP);
694
- line += `\x1B[${startCol + 1}G`;
695
- }
708
+ if (column > 0) line += `\x1B[${startCol + 1}G`;
696
709
  line += rendered[column]?.[row] ?? "";
710
+ startCol += (slotWidths[column] ?? 0) + GRID_GAP;
697
711
  }
698
712
  lines.push(stripTrailingSpaces(line));
699
713
  }
700
714
  }
701
715
  return lines;
702
716
  }
717
+ function imageCellSize(dimensions, requestedWidth, cellDimensions) {
718
+ const imageDimensions = dimensions ?? FALLBACK_IMAGE_DIMENSIONS;
719
+ const maxWidth = Math.max(1, Math.min(requestedWidth - 2, 60));
720
+ const maxHeight = Math.max(1, Math.ceil(maxWidth * cellDimensions.widthPx / cellDimensions.heightPx));
721
+ const imageWidth = Math.max(1, imageDimensions.widthPx);
722
+ const imageHeight = Math.max(1, imageDimensions.heightPx);
723
+ const widthScale = maxWidth * cellDimensions.widthPx / imageWidth;
724
+ const heightScale = maxHeight * cellDimensions.heightPx / imageHeight;
725
+ const scale = Math.min(widthScale, heightScale);
726
+ return {
727
+ columns: Math.max(1, Math.min(maxWidth, Math.ceil(imageWidth * scale / cellDimensions.widthPx))),
728
+ rows: Math.max(1, Math.min(maxHeight, Math.ceil(imageHeight * scale / cellDimensions.heightPx)))
729
+ };
730
+ }
703
731
  function registerImagePreviewSurface(pi) {
704
732
  pi.registerEntryRenderer(IMAGE_PREVIEW_ENTRY_TYPE, renderImagePreviewEntry);
705
733
  }
@@ -7602,7 +7630,6 @@ var DEFAULT_CONFIG = Object.freeze({
7602
7630
  assistantPrefix: true,
7603
7631
  specialBlocks: true,
7604
7632
  hideThinkingLabel: true,
7605
- hideInterimText: true,
7606
7633
  showImagePreviews: true,
7607
7634
  clipboardImages: true,
7608
7635
  previewMaxWidth: 30
@@ -7722,7 +7749,6 @@ function normalizeConfig(input, defaults = DEFAULT_CONFIG) {
7722
7749
  assistantPrefix: bool(messages.assistantPrefix, defaults.messages.assistantPrefix),
7723
7750
  specialBlocks: bool(messages.specialBlocks, defaults.messages.specialBlocks),
7724
7751
  hideThinkingLabel: bool(messages.hideThinkingLabel, defaults.messages.hideThinkingLabel),
7725
- hideInterimText: bool(messages.hideInterimText, defaults.messages.hideInterimText),
7726
7752
  showImagePreviews: bool(messages.showImagePreviews, defaults.messages.showImagePreviews),
7727
7753
  clipboardImages: bool(messages.clipboardImages, defaults.messages.clipboardImages),
7728
7754
  previewMaxWidth: boundedInt(messages.previewMaxWidth, defaults.messages.previewMaxWidth, 8, 60)
@@ -7778,7 +7804,6 @@ var BOOL_PATHS = /* @__PURE__ */ new Set([
7778
7804
  "messages.assistantPrefix",
7779
7805
  "messages.specialBlocks",
7780
7806
  "messages.hideThinkingLabel",
7781
- "messages.hideInterimText",
7782
7807
  "messages.showImagePreviews",
7783
7808
  "messages.clipboardImages",
7784
7809
  "tools.enabled",
@@ -8250,7 +8275,6 @@ var allowedPaths = /* @__PURE__ */ new Set([
8250
8275
  "messages.assistantPrefix",
8251
8276
  "messages.specialBlocks",
8252
8277
  "messages.hideThinkingLabel",
8253
- "messages.hideInterimText",
8254
8278
  "messages.showImagePreviews",
8255
8279
  "messages.clipboardImages",
8256
8280
  "messages.previewMaxWidth",
@@ -8285,7 +8309,6 @@ function validatePathValue(path, value) {
8285
8309
  "messages.assistantPrefix",
8286
8310
  "messages.specialBlocks",
8287
8311
  "messages.hideThinkingLabel",
8288
- "messages.hideInterimText",
8289
8312
  "messages.showImagePreviews",
8290
8313
  "messages.clipboardImages",
8291
8314
  "messages.previewMaxWidth",
@@ -11839,8 +11862,7 @@ function prefixNative(lines, width, prefix) {
11839
11862
  function decorateMessageRender(original, instance, args, snapshot = {
11840
11863
  assistantPrefix: "\u2502 ",
11841
11864
  assistantEnabled: true,
11842
- collapseHiddenThinking: false,
11843
- hideInterimText: false
11865
+ collapseHiddenThinking: false
11844
11866
  }) {
11845
11867
  if (typeof original !== "function") return void 0;
11846
11868
  const width = typeof args[0] === "number" ? args[0] : 0;
@@ -11864,17 +11886,6 @@ function decorateMessageRender(original, instance, args, snapshot = {
11864
11886
  function isSpacerChild(child) {
11865
11887
  return typeof child?.setLines === "function";
11866
11888
  }
11867
- function isInterimTextChild(child) {
11868
- const candidate = child;
11869
- return typeof candidate?.setText === "function" && candidate.options !== void 0 && typeof candidate.options === "object" && candidate.defaultTextStyle === void 0;
11870
- }
11871
- function hasToolCallItems(message) {
11872
- if (!message || typeof message !== "object") return false;
11873
- const content = message.content;
11874
- return Array.isArray(content) && content.some(
11875
- (item) => item !== null && typeof item === "object" && item.type === "toolCall"
11876
- );
11877
- }
11878
11889
  function isBlankTextChild(child) {
11879
11890
  const candidate = child;
11880
11891
  if (typeof candidate?.setCustomBgFn !== "function" || typeof candidate.render !== "function") return false;
@@ -11891,8 +11902,7 @@ function markChildrenScanned(instance, children) {
11891
11902
  function decorateMessageUpdate(original, instance, args, snapshot = {
11892
11903
  assistantPrefix: "\u2502 ",
11893
11904
  assistantEnabled: true,
11894
- collapseHiddenThinking: false,
11895
- hideInterimText: false
11905
+ collapseHiddenThinking: false
11896
11906
  }) {
11897
11907
  if (typeof original !== "function") return void 0;
11898
11908
  const result = Reflect.apply(original, instance, args);
@@ -11906,12 +11916,6 @@ function decorateMessageUpdate(original, instance, args, snapshot = {
11906
11916
  if (isSpacerChild(children[index])) children.splice(index, 1);
11907
11917
  }
11908
11918
  }
11909
- if (snapshot.hideInterimText && hasToolCallItems(args[0])) {
11910
- for (let index = children.length - 1; index >= 0; index--) {
11911
- if (isInterimTextChild(children[index])) children.splice(index, 1);
11912
- }
11913
- if (children.every((child) => isSpacerChild(child))) children.length = 0;
11914
- }
11915
11919
  markChildrenScanned(instance, children);
11916
11920
  }
11917
11921
  return result;
@@ -12375,7 +12379,7 @@ function surfaceDisabled(spec, config) {
12375
12379
  if (!config.messages.enabled) return true;
12376
12380
  if (spec.subtype === "native-assistant-message" && spec.method === "render") return !config.messages.assistantPrefix;
12377
12381
  if (spec.subtype === "native-assistant-message" && spec.method === "updateContent")
12378
- return !config.messages.hideThinkingLabel && !config.messages.hideInterimText;
12382
+ return !config.messages.hideThinkingLabel;
12379
12383
  if (isSpecialBlock(spec)) return !config.messages.specialBlocks;
12380
12384
  return true;
12381
12385
  }
@@ -12749,8 +12753,7 @@ function createCompatibilityCoordinator(dispose = disposePiCompatibilityProbe) {
12749
12753
  messageSnapshot: {
12750
12754
  assistantPrefix: authorization.ascii ? "[assistant] " : "\u2502 ",
12751
12755
  assistantEnabled,
12752
- collapseHiddenThinking: thinkingCollapseEnabled,
12753
- hideInterimText: config.messages.hideInterimText && messagesEnabled
12756
+ collapseHiddenThinking: thinkingCollapseEnabled
12754
12757
  },
12755
12758
  toolSnapshot: {
12756
12759
  callMarker: authorization.ascii ? "[tool] " : "[tool] ",
@@ -13285,9 +13288,9 @@ function piStyleExtension(pi) {
13285
13288
  registerPiStyleCommand(pi, coordinator.app);
13286
13289
  registerImagePreviewSurface(pi);
13287
13290
  let stagedImagePreview;
13291
+ let previewFlushTimer;
13288
13292
  pi.on("before_agent_start", (event) => {
13289
- const staged = stageImagePreviewData(event.images ?? []);
13290
- if (staged) stagedImagePreview = staged;
13293
+ stagedImagePreview = stageImagePreviewData(event.images ?? []);
13291
13294
  });
13292
13295
  pi.on("session_start", async (event, ctx) => {
13293
13296
  resetUsageFromSessionCache(ctx.sessionManager);
@@ -13339,7 +13342,20 @@ function piStyleExtension(pi) {
13339
13342
  );
13340
13343
  pi.on("message_start", (event) => {
13341
13344
  closeActiveBatch();
13345
+ if (event.message?.role === "user" && stagedImagePreview && !previewFlushTimer) {
13346
+ const pending = stagedImagePreview;
13347
+ previewFlushTimer = setTimeout(() => {
13348
+ previewFlushTimer = void 0;
13349
+ if (stagedImagePreview !== pending) return;
13350
+ flushImagePreviewEntry(pi, pending);
13351
+ stagedImagePreview = void 0;
13352
+ }, 0);
13353
+ }
13342
13354
  if (stagedImagePreview && event.message?.role === "assistant") {
13355
+ if (previewFlushTimer) {
13356
+ clearTimeout(previewFlushTimer);
13357
+ previewFlushTimer = void 0;
13358
+ }
13343
13359
  flushImagePreviewEntry(pi, stagedImagePreview);
13344
13360
  stagedImagePreview = void 0;
13345
13361
  }