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/README.md +41 -6
- package/dist/index.d.mts +80 -2
- package/dist/index.mjs +372 -23
- package/package.json +2 -2
- package/src/compress.ts +329 -0
- package/src/config.ts +67 -0
- package/src/editor.ts +105 -11
- package/src/image-utils.ts +8 -0
- package/src/index.ts +46 -8
- package/src/preview.ts +93 -4
- package/src/types.ts +11 -0
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 = {
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
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 {
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
826
|
-
|
|
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-
|
|
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, {
|
|
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.
|
|
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.
|
|
61
|
+
"image": "https://unpkg.com/pi-paster@0.2.2/docs/preview.png"
|
|
62
62
|
}
|
|
63
63
|
}
|