pi-paster 0.2.0 → 0.2.2

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/dist/index.mjs CHANGED
@@ -1,10 +1,10 @@
1
1
  import { spawnSync } from "node:child_process";
2
2
  import { randomUUID } from "node:crypto";
3
- import { existsSync, readFileSync, statSync, unlinkSync } from "node:fs";
3
+ import { existsSync, mkdtempSync, readFileSync, rmSync, statSync, unlinkSync, writeFileSync } from "node:fs";
4
4
  import { homedir, tmpdir } from "node:os";
5
5
  import { isAbsolute, join, resolve } from "node:path";
6
6
  import { platform } from "node:process";
7
- import { Image, getCellDimensions, getImageDimensions, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
7
+ import { Box, Container, Image, Spacer, Text, getCellDimensions, getImageDimensions, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
8
8
  import { CustomEditor } from "@earendil-works/pi-coding-agent";
9
9
  //#region src/types.ts
10
10
  const EXTENSION_NAME = "paster";
@@ -447,6 +447,10 @@ function imagesForText(store, text, existing = []) {
447
447
  data: attachment.data
448
448
  }))];
449
449
  }
450
+ function appendImagePathContext(text, attachments) {
451
+ if (attachments.length === 0) return text;
452
+ return `${text}\n\nAttached image paths:\n${attachments.map((attachment) => `- ${attachment.placeholder}: ${attachment.originalPath}`).join("\n")}`;
453
+ }
450
454
  /**
451
455
  * Async variant of imagesForText that runs each attachment through the
452
456
  * Anthropic-aware image optimizer (resize to 8000px cap, JPEG ladder to stay
@@ -643,24 +647,282 @@ function readMacOSClipboardImage(maxBytes) {
643
647
  };
644
648
  }
645
649
  //#endregion
650
+ //#region src/compress.ts
651
+ const DEFAULT_IMAGE_SUMMARY_PROMPT = "Summarize this image in 2-4 concise sentences. Include important visible text, UI elements, errors, diagrams, and details that may matter for future coding or design work.";
652
+ function isImageBlock(block) {
653
+ return !!block && typeof block === "object" && block.type === "image" && typeof block.data === "string" && typeof block.mimeType === "string";
654
+ }
655
+ function imageKey(image) {
656
+ return `${image.mimeType}:${image.data.length}:${image.data.slice(0, 48)}`;
657
+ }
658
+ function collectImages(entries) {
659
+ const images = [];
660
+ for (const entry of entries) {
661
+ const content = entry.type === "custom_message" ? entry.content : entry.message?.content;
662
+ if (!Array.isArray(content)) continue;
663
+ for (const block of content) {
664
+ if (!isImageBlock(block)) continue;
665
+ images.push({
666
+ key: imageKey(block),
667
+ image: block
668
+ });
669
+ }
670
+ }
671
+ return images;
672
+ }
673
+ function extensionForMime(mimeType) {
674
+ switch (mimeType) {
675
+ case "image/jpeg": return ".jpg";
676
+ case "image/webp": return ".webp";
677
+ case "image/gif": return ".gif";
678
+ default: return ".png";
679
+ }
680
+ }
681
+ function normalizeSummary(stdout) {
682
+ return stdout.trim().replace(/\n{3,}/g, "\n\n");
683
+ }
684
+ async function summarizeImage(pi, ctx, image, config) {
685
+ const dir = mkdtempSync(join(tmpdir(), "pi-paster-image-compress-"));
686
+ try {
687
+ const imagePath = join(dir, `image${extensionForMime(image.mimeType)}`);
688
+ writeFileSync(imagePath, Buffer.from(image.data, "base64"));
689
+ const args = [
690
+ "--no-session",
691
+ "--no-extensions",
692
+ "--no-tools",
693
+ "--mode",
694
+ "text",
695
+ "-p"
696
+ ];
697
+ if (config.model) args.splice(0, 0, "--model", config.model);
698
+ args.push(`@${imagePath}`, config.prompt);
699
+ const result = await pi.exec(config.piCommand, args, {
700
+ cwd: ctx.cwd,
701
+ timeout: config.timeoutMs
702
+ });
703
+ if (result.code !== 0) {
704
+ const detail = result.stderr.trim() || result.stdout.trim() || `exit code ${result.code}`;
705
+ throw new Error(`image summarization failed: ${detail}`);
706
+ }
707
+ const summary = normalizeSummary(result.stdout);
708
+ if (!summary) throw new Error("image summarization returned an empty response");
709
+ return summary;
710
+ } finally {
711
+ rmSync(dir, {
712
+ recursive: true,
713
+ force: true
714
+ });
715
+ }
716
+ }
717
+ function summaryBlock(summary) {
718
+ return {
719
+ type: "text",
720
+ text: `Image summary: ${summary}`
721
+ };
722
+ }
723
+ function transformContent(content, summaries) {
724
+ if (!Array.isArray(content)) return content;
725
+ return content.map((block) => {
726
+ if (!isImageBlock(block)) return block;
727
+ return summaryBlock(summaries.get(imageKey(block)) ?? "Image summary unavailable.");
728
+ });
729
+ }
730
+ function buildReportDetails(images, summaries) {
731
+ const items = [...summaries.values()].map((summary, index) => ({
732
+ index: index + 1,
733
+ summary
734
+ }));
735
+ return {
736
+ imageCount: images.length,
737
+ summaryCount: summaries.size,
738
+ items
739
+ };
740
+ }
741
+ function appendTransformedEntry(sessionManager, entry, summaries) {
742
+ const manager = sessionManager;
743
+ switch (entry.type) {
744
+ case "thinking_level_change":
745
+ if (entry.thinkingLevel) manager.appendThinkingLevelChange(entry.thinkingLevel);
746
+ return;
747
+ case "model_change":
748
+ if (entry.provider && entry.modelId) manager.appendModelChange(entry.provider, entry.modelId);
749
+ return;
750
+ case "compaction":
751
+ if (entry.summary) manager.appendCustomMessageEntry("paster-image-compress-compaction-summary", entry.summary, false, entry.details);
752
+ return;
753
+ case "branch_summary":
754
+ if (entry.summary) manager.appendCustomMessageEntry("paster-image-compress-branch-summary", entry.summary, false, entry.details);
755
+ return;
756
+ case "custom":
757
+ if (entry.customType) manager.appendCustomEntry(entry.customType, entry.data);
758
+ return;
759
+ case "session_info":
760
+ if (typeof entry.name === "string") manager.appendSessionInfo(entry.name);
761
+ return;
762
+ case "custom_message":
763
+ if (entry.customType === "paster-preview") return;
764
+ if (entry.customType && entry.content !== void 0) manager.appendCustomMessageEntry(entry.customType, transformContent(entry.content, summaries) ?? "", entry.display ?? true, entry.details);
765
+ return;
766
+ case "message":
767
+ if (entry.message) manager.appendMessage({
768
+ ...entry.message,
769
+ content: transformContent(entry.message.content, summaries)
770
+ });
771
+ return;
772
+ default: return;
773
+ }
774
+ }
775
+ function resolveCommandOptions(args, config) {
776
+ const model = args.trim() || config.model;
777
+ return {
778
+ ...config,
779
+ model
780
+ };
781
+ }
782
+ function setCompressionProgress(ctx, current, total, model) {
783
+ if (!ctx.hasUI) return;
784
+ const progress = `Summarizing image ${current}/${total}`;
785
+ ctx.ui.setStatus("paster-image-compress", progress);
786
+ ctx.ui.setWidget("paster-image-compress-progress", [`paster: ${progress}`, model ? `model: ${model}` : "model: pi default"], { placement: "aboveEditor" });
787
+ }
788
+ function clearCompressionProgress(ctx) {
789
+ if (!ctx.hasUI) return;
790
+ ctx.ui.setStatus("paster-image-compress", void 0);
791
+ ctx.ui.setWidget("paster-image-compress-progress", void 0, { placement: "aboveEditor" });
792
+ }
793
+ function registerImageCompressionCommand(pi, config) {
794
+ if (!config.enabled) return;
795
+ pi.registerCommand(config.command, {
796
+ description: "Summarize images in the current branch and switch to a new session where image blocks are replaced with text summaries.",
797
+ handler: async (args, ctx) => {
798
+ await ctx.waitForIdle();
799
+ const commandConfig = resolveCommandOptions(args, config);
800
+ const branch = ctx.sessionManager.getBranch();
801
+ const images = collectImages(branch);
802
+ if (images.length === 0) {
803
+ if (ctx.hasUI) ctx.ui.notify("No images found in the current branch", "warning");
804
+ return;
805
+ }
806
+ const uniqueImages = [...new Map(images.map((item) => [item.key, item.image])).entries()];
807
+ const summaries = /* @__PURE__ */ new Map();
808
+ if (ctx.hasUI) ctx.ui.notify(`Summarizing ${uniqueImages.length} image(s)...`, "info");
809
+ setCompressionProgress(ctx, 0, uniqueImages.length, commandConfig.model);
810
+ try {
811
+ for (let index = 0; index < uniqueImages.length; index++) {
812
+ const [key, image] = uniqueImages[index];
813
+ setCompressionProgress(ctx, index + 1, uniqueImages.length, commandConfig.model);
814
+ summaries.set(key, await summarizeImage(pi, ctx, image, commandConfig));
815
+ }
816
+ } catch (error) {
817
+ const reason = error instanceof Error ? error.message : String(error);
818
+ const message = `Image compression failed while using ${commandConfig.model || "pi's default model"}. ${reason}\n\nTo try another model once, run /${commandConfig.command} provider/model. To change the default, configure pi-paster's imageCompression.model option in your wrapper extension. See https://github.com/beowulf11/pi-paster#configuration.`;
819
+ if (ctx.hasUI) ctx.ui.notify(message, "error");
820
+ else console.error(message);
821
+ return;
822
+ } finally {
823
+ clearCompressionProgress(ctx);
824
+ }
825
+ const parentSession = ctx.sessionManager.getSessionFile();
826
+ const reportDetails = buildReportDetails(images, summaries);
827
+ await ctx.newSession({
828
+ parentSession,
829
+ setup: async (sessionManager) => {
830
+ for (const entry of branch) appendTransformedEntry(sessionManager, entry, summaries);
831
+ if (commandConfig.includeReport) sessionManager.appendCustomMessageEntry("paster-image-compress-report", "", true, reportDetails);
832
+ },
833
+ withSession: async (newCtx) => {
834
+ if (newCtx.hasUI) newCtx.ui.notify(`Compressed ${images.length} image block(s) into text summaries`, "info");
835
+ }
836
+ });
837
+ }
838
+ });
839
+ }
840
+ //#endregion
646
841
  //#region src/config.ts
647
- const DEFAULT_PASTER_CONFIG = { customEditor: {
648
- enabled: true,
649
- showImagePreview: true,
650
- deletePlaceholderAsBlock: true
651
- } };
842
+ const DEFAULT_PASTER_CONFIG = {
843
+ submittedPreviewStyle: "raw",
844
+ includeImagePathsInPrompt: true,
845
+ imageCompression: {
846
+ enabled: true,
847
+ command: "image-compress",
848
+ model: "openai-codex/gpt-5.4-mini",
849
+ prompt: "Summarize this image in 2-4 concise sentences. Include important visible text, UI elements, errors, diagrams, and details that may matter for future coding or design work.",
850
+ piCommand: "pi",
851
+ timeoutMs: 12e4,
852
+ includeReport: true
853
+ },
854
+ customEditor: {
855
+ enabled: true,
856
+ showImagePreview: true,
857
+ deletePlaceholderAsBlock: true
858
+ }
859
+ };
652
860
  function resolvePasterConfig(config = {}) {
653
- return { customEditor: {
654
- enabled: config.customEditor?.enabled ?? DEFAULT_PASTER_CONFIG.customEditor.enabled,
655
- showImagePreview: config.customEditor?.showImagePreview ?? DEFAULT_PASTER_CONFIG.customEditor.showImagePreview,
656
- deletePlaceholderAsBlock: config.customEditor?.deletePlaceholderAsBlock ?? DEFAULT_PASTER_CONFIG.customEditor.deletePlaceholderAsBlock
657
- } };
861
+ return {
862
+ submittedPreviewStyle: config.submittedPreviewStyle ?? DEFAULT_PASTER_CONFIG.submittedPreviewStyle,
863
+ includeImagePathsInPrompt: config.includeImagePathsInPrompt ?? DEFAULT_PASTER_CONFIG.includeImagePathsInPrompt,
864
+ imageCompression: {
865
+ enabled: config.imageCompression?.enabled ?? DEFAULT_PASTER_CONFIG.imageCompression.enabled,
866
+ command: config.imageCompression?.command ?? DEFAULT_PASTER_CONFIG.imageCompression.command,
867
+ model: config.imageCompression?.model ?? DEFAULT_PASTER_CONFIG.imageCompression.model,
868
+ prompt: config.imageCompression?.prompt ?? DEFAULT_PASTER_CONFIG.imageCompression.prompt,
869
+ piCommand: config.imageCompression?.piCommand ?? DEFAULT_PASTER_CONFIG.imageCompression.piCommand,
870
+ timeoutMs: config.imageCompression?.timeoutMs ?? DEFAULT_PASTER_CONFIG.imageCompression.timeoutMs,
871
+ includeReport: config.imageCompression?.includeReport ?? DEFAULT_PASTER_CONFIG.imageCompression.includeReport
872
+ },
873
+ customEditor: {
874
+ enabled: config.customEditor?.enabled ?? DEFAULT_PASTER_CONFIG.customEditor.enabled,
875
+ showImagePreview: config.customEditor?.showImagePreview ?? DEFAULT_PASTER_CONFIG.customEditor.showImagePreview,
876
+ deletePlaceholderAsBlock: config.customEditor?.deletePlaceholderAsBlock ?? DEFAULT_PASTER_CONFIG.customEditor.deletePlaceholderAsBlock
877
+ }
878
+ };
658
879
  }
659
880
  //#endregion
660
881
  //#region src/editor.ts
661
882
  const PASTE_START = "\x1B[200~";
662
883
  const PASTE_END = "\x1B[201~";
663
884
  const PLACEHOLDER_REGEX = /\[#image \d+\]/g;
885
+ const PASTE_MARKER_REGEX = /\[paste #(\d+)( (\+\d+ lines|\d+ chars))?\]/g;
886
+ const baseSegmenter = new Intl.Segmenter();
887
+ function atomicSpansForText(text, validPasteIds) {
888
+ const spans = [];
889
+ for (const match of text.matchAll(PASTE_MARKER_REGEX)) {
890
+ const id = Number.parseInt(match[1], 10);
891
+ if (!validPasteIds.has(id)) continue;
892
+ spans.push({
893
+ start: match.index,
894
+ end: match.index + match[0].length
895
+ });
896
+ }
897
+ for (const match of text.matchAll(PLACEHOLDER_REGEX)) {
898
+ const placeholder = match[0];
899
+ spans.push({
900
+ start: match.index,
901
+ end: match.index + placeholder.length
902
+ });
903
+ }
904
+ return spans.sort((a, b) => a.start - b.start || a.end - b.end);
905
+ }
906
+ function segmentTextWithAtomicImages(text, store, validPasteIds = /* @__PURE__ */ new Set()) {
907
+ const spans = atomicSpansForText(text, validPasteIds);
908
+ if (spans.length === 0) return [...baseSegmenter.segment(text)];
909
+ const result = [];
910
+ let spanIndex = 0;
911
+ for (const segment of baseSegmenter.segment(text)) {
912
+ while (spanIndex < spans.length && spans[spanIndex].end <= segment.index) spanIndex++;
913
+ const span = spans[spanIndex];
914
+ if (span && segment.index >= span.start && segment.index < span.end) {
915
+ if (segment.index === span.start) result.push({
916
+ segment: text.slice(span.start, span.end),
917
+ index: span.start,
918
+ input: text
919
+ });
920
+ continue;
921
+ }
922
+ result.push(segment);
923
+ }
924
+ return result;
925
+ }
664
926
  function findPlaceholderAtCursor(store, lines, cursor, mode) {
665
927
  const line = lines[cursor.line] ?? "";
666
928
  for (const match of line.matchAll(PLACEHOLDER_REGEX)) {
@@ -668,21 +930,24 @@ function findPlaceholderAtCursor(store, lines, cursor, mode) {
668
930
  const start = match.index;
669
931
  const end = start + placeholder.length;
670
932
  const attachment = store.get(placeholder);
671
- if (!attachment) continue;
933
+ if (!attachment && mode !== "hover") continue;
672
934
  if (mode === "hover" && cursor.col >= start && cursor.col < end) return {
673
935
  attachment,
936
+ placeholder,
674
937
  line: cursor.line,
675
938
  start,
676
939
  end
677
940
  };
678
941
  if (mode === "backspace" && cursor.col > start && cursor.col <= end) return {
679
942
  attachment,
943
+ placeholder,
680
944
  line: cursor.line,
681
945
  start,
682
946
  end
683
947
  };
684
948
  if (mode === "delete" && cursor.col >= start && cursor.col < end) return {
685
949
  attachment,
950
+ placeholder,
686
951
  line: cursor.line,
687
952
  start,
688
953
  end
@@ -696,6 +961,7 @@ var PasterEditor = class extends CustomEditor {
696
961
  super(tui, theme, pasterKeybindings);
697
962
  this.pasterKeybindings = pasterKeybindings;
698
963
  this.pasterOptions = pasterOptions;
964
+ this.installAtomicImageSegmentation();
699
965
  this.onPasteImage = () => {
700
966
  this.handlePasteClipboardImage();
701
967
  };
@@ -707,6 +973,7 @@ var PasterEditor = class extends CustomEditor {
707
973
  }
708
974
  handleInput(data) {
709
975
  if (this.handleBracketedPaste(data)) return;
976
+ if (this.handleAtomicPlaceholderNavigation(data)) return;
710
977
  if (this.pasterOptions.deletePlaceholderAsBlock && this.handleAtomicPlaceholderDelete(data)) return;
711
978
  super.handleInput(data);
712
979
  this.updateCursorPreview();
@@ -715,6 +982,10 @@ var PasterEditor = class extends CustomEditor {
715
982
  this.activePreviewPlaceholder = void 0;
716
983
  this.pasterOptions.setCursorPreview(void 0);
717
984
  }
985
+ installAtomicImageSegmentation() {
986
+ const editor = this;
987
+ editor.segment = (text) => segmentTextWithAtomicImages(text, this.pasterOptions.store, new Set(editor.pastes?.keys() ?? []));
988
+ }
718
989
  async handlePasteClipboardImage() {
719
990
  const attachment = await this.pasterOptions.pasteClipboardImage?.();
720
991
  if (!attachment) return;
@@ -755,6 +1026,20 @@ var PasterEditor = class extends CustomEditor {
755
1026
  this.updateCursorPreview();
756
1027
  return true;
757
1028
  }
1029
+ handleAtomicPlaceholderNavigation(data) {
1030
+ const isLeft = this.pasterKeybindings.matches(data, "tui.editor.cursorLeft");
1031
+ const isRight = this.pasterKeybindings.matches(data, "tui.editor.cursorRight");
1032
+ if (!isLeft && !isRight) return false;
1033
+ const line = this.getLines()[this.getCursor().line] ?? "";
1034
+ const cursor = this.getCursor();
1035
+ const matches = [...line.matchAll(PLACEHOLDER_REGEX)];
1036
+ const target = isRight ? matches.find((match) => cursor.col >= match.index && cursor.col < match.index + match[0].length) : matches.find((match) => cursor.col > match.index && cursor.col <= match.index + match[0].length);
1037
+ if (!target) return false;
1038
+ this.setCursor(target.index + (isRight ? target[0].length : 0));
1039
+ this.updateCursorPreview();
1040
+ this.tui.requestRender();
1041
+ return true;
1042
+ }
758
1043
  handleAtomicPlaceholderDelete(data) {
759
1044
  const isBackspace = this.pasterKeybindings.matches(data, "tui.editor.deleteCharBackward");
760
1045
  const isDelete = this.pasterKeybindings.matches(data, "tui.editor.deleteCharForward");
@@ -766,14 +1051,18 @@ var PasterEditor = class extends CustomEditor {
766
1051
  this.updateCursorPreview();
767
1052
  return true;
768
1053
  }
1054
+ setCursor(col) {
1055
+ const editor = this;
1056
+ if (editor.setCursorCol) editor.setCursorCol(col);
1057
+ else editor.state.cursorCol = col;
1058
+ }
769
1059
  deleteLineRange(lineIndex, start, end) {
770
1060
  const editor = this;
771
1061
  editor.pushUndoSnapshot?.();
772
1062
  const line = editor.state.lines[lineIndex] ?? "";
773
1063
  editor.state.lines[lineIndex] = line.slice(0, start) + line.slice(end);
774
1064
  editor.state.cursorLine = lineIndex;
775
- if (editor.setCursorCol) editor.setCursorCol(start);
776
- else editor.state.cursorCol = start;
1065
+ this.setCursor(start);
777
1066
  editor.lastAction = null;
778
1067
  editor.historyIndex = -1;
779
1068
  this.onChange?.(this.getText());
@@ -788,7 +1077,7 @@ var PasterEditor = class extends CustomEditor {
788
1077
  }
789
1078
  updateCursorPreview() {
790
1079
  const target = findPlaceholderAtCursor(this.pasterOptions.store, this.getLines(), this.getCursor(), "hover");
791
- const nextPlaceholder = target?.attachment.placeholder;
1080
+ const nextPlaceholder = target?.attachment?.placeholder;
792
1081
  if (nextPlaceholder === this.activePreviewPlaceholder) return;
793
1082
  this.activePreviewPlaceholder = nextPlaceholder;
794
1083
  this.pasterOptions.setCursorPreview(target?.attachment);
@@ -803,16 +1092,23 @@ function formatAttachmentLine(attachment, width, style) {
803
1092
  }
804
1093
  var ImagePreviewMessage = class {
805
1094
  images;
806
- constructor(attachments, theme) {
1095
+ constructor(attachments, theme, options = {}) {
807
1096
  this.attachments = attachments;
808
1097
  this.theme = theme;
1098
+ this.options = options;
809
1099
  this.images = attachments.map((attachment) => new Image(attachment.data, attachment.mimeType, theme, {
810
1100
  maxWidthCells: 60,
811
1101
  maxHeightCells: 16,
812
1102
  filename: attachment.placeholder
813
1103
  }));
814
1104
  }
1105
+ invalidate() {
1106
+ for (const image of this.images) image.invalidate();
1107
+ }
815
1108
  render(width) {
1109
+ return this.options.style === "collapsible" ? this.renderCollapsible(width) : this.renderRaw(width);
1110
+ }
1111
+ renderRaw(width) {
816
1112
  const lines = [];
817
1113
  const safeWidth = Math.max(1, width);
818
1114
  for (let index = 0; index < this.attachments.length; index++) {
@@ -822,10 +1118,45 @@ var ImagePreviewMessage = class {
822
1118
  }
823
1119
  return lines;
824
1120
  }
825
- invalidate() {
826
- for (const image of this.images) image.invalidate();
1121
+ renderCollapsible(width) {
1122
+ const container = new Container();
1123
+ container.addChild(new Spacer(1));
1124
+ const box = new Box(1, 1, this.theme.background);
1125
+ container.addChild(box);
1126
+ const title = this.theme.title ?? this.theme.fallbackColor;
1127
+ const muted = this.theme.muted ?? this.theme.fallbackColor;
1128
+ const summary = this.attachments.length === 1 ? `Attached ${this.attachments[0].placeholder}` : `Attached ${this.attachments.length} images`;
1129
+ const suffix = this.options.expanded ? " (ctrl+o to collapse)" : " (ctrl+o to expand)";
1130
+ box.addChild(new Text(`${title(summary)}${muted(suffix)}`, 0, 0));
1131
+ for (const attachment of this.attachments) box.addChild(new Text(formatAttachmentLine(attachment, width, muted), 0, 0));
1132
+ const lines = container.render(width);
1133
+ if (!this.options.expanded) return lines;
1134
+ const safeWidth = Math.max(1, width);
1135
+ for (let index = 0; index < this.attachments.length; index++) lines.push(...this.images[index].render(safeWidth));
1136
+ return lines;
827
1137
  }
828
1138
  };
1139
+ var ImageCompressionReportMessage = class {
1140
+ constructor(details, theme, expanded = false) {
1141
+ this.details = details;
1142
+ this.theme = theme;
1143
+ this.expanded = expanded;
1144
+ }
1145
+ render(width) {
1146
+ const container = new Container();
1147
+ container.addChild(new Spacer(1));
1148
+ const box = new Box(1, 1, this.theme.background);
1149
+ container.addChild(box);
1150
+ const suffix = this.expanded ? " (ctrl+o to collapse)" : " (ctrl+o to expand)";
1151
+ box.addChild(new Text(`${this.theme.title(`Compressed ${this.details.imageCount} image block(s) into ${this.details.summaryCount} summary/summaries`)}${this.theme.muted(suffix)}`, 0, 0));
1152
+ if (this.expanded) for (const item of this.details.items) {
1153
+ box.addChild(new Text(this.theme.muted(`Image ${item.index}:`), 0, 0));
1154
+ box.addChild(new Text(truncateToWidth(item.summary, Math.max(1, width - 4), "…"), 0, 0));
1155
+ }
1156
+ return container.render(width);
1157
+ }
1158
+ invalidate() {}
1159
+ };
829
1160
  var CursorImagePreviewWidget = class {
830
1161
  image;
831
1162
  constructor(attachment, theme) {
@@ -934,14 +1265,32 @@ function createPaster(config = {}) {
934
1265
  function paster(pi, config = {}) {
935
1266
  const resolvedConfig = resolvePasterConfig(config);
936
1267
  const store = new AttachmentStore();
1268
+ registerImageCompressionCommand(pi, resolvedConfig.imageCompression);
937
1269
  let pendingPreview = [];
938
1270
  let activeEditor;
939
1271
  let unsubscribeTerminalInput;
940
- pi.registerMessageRenderer("paster-preview", (message, _options, theme) => {
1272
+ pi.registerMessageRenderer("paster-image-compress-report", (message, options, theme) => {
1273
+ const details = message.details;
1274
+ if (!details) return void 0;
1275
+ return new ImageCompressionReportMessage(details, {
1276
+ background: (text) => theme.bg("toolSuccessBg", text),
1277
+ title: (text) => theme.fg("toolTitle", theme.bold(text)),
1278
+ muted: (text) => theme.fg("muted", text)
1279
+ }, options.expanded);
1280
+ });
1281
+ pi.registerMessageRenderer("paster-preview", (message, options, theme) => {
941
1282
  const placeholders = message.details?.placeholders ?? [];
942
1283
  const attachments = store.list().filter((attachment) => placeholders.includes(attachment.placeholder));
943
1284
  if (attachments.length === 0) return void 0;
944
- return new ImagePreviewMessage(attachments, { fallbackColor: (text) => theme.fg("muted", text) });
1285
+ return new ImagePreviewMessage(attachments, {
1286
+ fallbackColor: (text) => theme.fg("muted", text),
1287
+ background: (text) => theme.bg("toolSuccessBg", text),
1288
+ title: (text) => theme.fg("toolTitle", theme.bold(text)),
1289
+ muted: (text) => theme.fg("muted", text)
1290
+ }, {
1291
+ expanded: options.expanded,
1292
+ style: resolvedConfig.submittedPreviewStyle
1293
+ });
945
1294
  });
946
1295
  pi.on("session_start", (_event, ctx) => {
947
1296
  store.clear();
@@ -1017,7 +1366,7 @@ function paster(pi, config = {}) {
1017
1366
  const images = await imagesForTextOptimized(store, event.text, event.images);
1018
1367
  return {
1019
1368
  action: "transform",
1020
- text: event.text,
1369
+ text: resolvedConfig.includeImagePathsInPrompt ? appendImagePathContext(event.text, attachments) : event.text,
1021
1370
  images
1022
1371
  };
1023
1372
  });
@@ -1029,4 +1378,4 @@ function paster(pi, config = {}) {
1029
1378
  });
1030
1379
  }
1031
1380
  //#endregion
1032
- export { ANTHROPIC_MAX_DIMENSION, ANTHROPIC_MAX_IMAGE_BYTES, AttachmentStore, CursorImagePreviewWidget, DEFAULT_PASTER_CONFIG, EXTENSION_NAME, ImagePreviewMessage, MAX_IMAGE_BYTES, PASTE_END, PASTE_START, PasterEditor, createImagePasteTerminalInputHandler, createPaster, paster as default, describeReject, detectImageMimeType, dimensionsForImage, imagesForText, imagesForTextOptimized, isWindowsDrivePath, isWindowsLikePath, isWindowsUncPath, isWsl, loadImageFromPath, optimizeImageBytes, readClipboardImage, replaceImagePathsInText, resolveImagePath, resolvePasterConfig, shellUnescape, tokenizePathLikeText, windowsToWslPath };
1381
+ export { ANTHROPIC_MAX_DIMENSION, ANTHROPIC_MAX_IMAGE_BYTES, AttachmentStore, CursorImagePreviewWidget, DEFAULT_IMAGE_SUMMARY_PROMPT, DEFAULT_PASTER_CONFIG, EXTENSION_NAME, ImageCompressionReportMessage, ImagePreviewMessage, MAX_IMAGE_BYTES, PASTE_END, PASTE_START, PasterEditor, appendImagePathContext, createImagePasteTerminalInputHandler, createPaster, paster as default, describeReject, detectImageMimeType, dimensionsForImage, imagesForText, imagesForTextOptimized, isWindowsDrivePath, isWindowsLikePath, isWindowsUncPath, isWsl, loadImageFromPath, optimizeImageBytes, readClipboardImage, registerImageCompressionCommand, replaceImagePathsInText, resolveImagePath, resolvePasterConfig, segmentTextWithAtomicImages, shellUnescape, tokenizePathLikeText, windowsToWslPath };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-paster",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "description": "Pi extension that turns pasted image paths into first-class image attachments.",
5
5
  "keywords": [
6
6
  "image-attachments",
@@ -58,6 +58,6 @@
58
58
  "extensions": [
59
59
  "./src/index.ts"
60
60
  ],
61
- "image": "https://unpkg.com/pi-paster@0.2.0/docs/preview.png"
61
+ "image": "https://unpkg.com/pi-paster@0.2.2/docs/preview.png"
62
62
  }
63
63
  }