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/src/index.ts CHANGED
@@ -1,14 +1,24 @@
1
1
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
2
  import { readClipboardImage } from "./clipboard.ts";
3
+ import { registerImageCompressionCommand } from "./compress.ts";
3
4
  import { type PasterConfig, resolvePasterConfig } from "./config.ts";
4
5
  import { PasterEditor } from "./editor.ts";
5
- import { imagesForTextOptimized } from "./image-utils.ts";
6
- import { CursorImagePreviewWidget, ImagePreviewMessage } from "./preview.ts";
6
+ import { appendImagePathContext, imagesForTextOptimized } from "./image-utils.ts";
7
+ import {
8
+ CursorImagePreviewWidget,
9
+ ImageCompressionReportMessage,
10
+ ImagePreviewMessage,
11
+ } from "./preview.ts";
7
12
  import { AttachmentStore } from "./store.ts";
8
13
  import { createImagePasteTerminalInputHandler } from "./terminal-input.ts";
9
- import type { ImageAttachment, PasterPreviewDetails } from "./types.ts";
14
+ import type {
15
+ ImageAttachment,
16
+ ImageCompressionReportDetails,
17
+ PasterPreviewDetails,
18
+ } from "./types.ts";
10
19
 
11
20
  export * from "./clipboard.ts";
21
+ export * from "./compress.ts";
12
22
  export * from "./optimize-image.ts";
13
23
  export * from "./config.ts";
14
24
  export * from "./editor.ts";
@@ -25,19 +35,44 @@ export function createPaster(config: PasterConfig = {}): (pi: ExtensionAPI) => v
25
35
  export default function paster(pi: ExtensionAPI, config: PasterConfig = {}): void {
26
36
  const resolvedConfig = resolvePasterConfig(config);
27
37
  const store = new AttachmentStore();
38
+ registerImageCompressionCommand(pi, resolvedConfig.imageCompression);
28
39
  let pendingPreview: ImageAttachment[] = [];
29
40
  let activeEditor: PasterEditor | undefined;
30
41
  let unsubscribeTerminalInput: (() => void) | undefined;
31
42
 
32
- pi.registerMessageRenderer<PasterPreviewDetails>("paster-preview", (message, _options, theme) => {
43
+ pi.registerMessageRenderer<ImageCompressionReportDetails>(
44
+ "paster-image-compress-report",
45
+ (message, options, theme) => {
46
+ const details = message.details;
47
+ if (!details) return undefined;
48
+ return new ImageCompressionReportMessage(
49
+ details,
50
+ {
51
+ background: (text) => theme.bg("toolSuccessBg", text),
52
+ title: (text) => theme.fg("toolTitle", theme.bold(text)),
53
+ muted: (text) => theme.fg("muted", text),
54
+ },
55
+ options.expanded,
56
+ );
57
+ },
58
+ );
59
+
60
+ pi.registerMessageRenderer<PasterPreviewDetails>("paster-preview", (message, options, theme) => {
33
61
  const placeholders = message.details?.placeholders ?? [];
34
62
  const attachments = store
35
63
  .list()
36
64
  .filter((attachment) => placeholders.includes(attachment.placeholder));
37
65
  if (attachments.length === 0) return undefined;
38
- return new ImagePreviewMessage(attachments, {
39
- fallbackColor: (text) => theme.fg("muted", text),
40
- });
66
+ return new ImagePreviewMessage(
67
+ attachments,
68
+ {
69
+ fallbackColor: (text) => theme.fg("muted", text),
70
+ background: (text) => theme.bg("toolSuccessBg", text),
71
+ title: (text) => theme.fg("toolTitle", theme.bold(text)),
72
+ muted: (text) => theme.fg("muted", text),
73
+ },
74
+ { expanded: options.expanded, style: resolvedConfig.submittedPreviewStyle },
75
+ );
41
76
  });
42
77
 
43
78
  pi.on("session_start", (_event, ctx) => {
@@ -142,10 +177,13 @@ export default function paster(pi: ExtensionAPI, config: PasterConfig = {}): voi
142
177
  // 32 MB/request caps. Per-attachment caching means each image is only
143
178
  // resized/recompressed once across the whole session.
144
179
  const images = await imagesForTextOptimized(store, event.text, event.images);
180
+ const text = resolvedConfig.includeImagePathsInPrompt
181
+ ? appendImagePathContext(event.text, attachments)
182
+ : event.text;
145
183
 
146
184
  return {
147
185
  action: "transform" as const,
148
- text: event.text,
186
+ text,
149
187
  images,
150
188
  };
151
189
  });
package/src/preview.ts CHANGED
@@ -1,12 +1,16 @@
1
1
  import {
2
+ Box,
3
+ Container,
2
4
  getCellDimensions,
3
5
  Image,
4
6
  type Component,
5
7
  type ImageTheme,
8
+ Spacer,
9
+ Text,
6
10
  truncateToWidth,
7
11
  visibleWidth,
8
12
  } from "@earendil-works/pi-tui";
9
- import type { ImageAttachment } from "./types.ts";
13
+ import type { ImageAttachment, ImageCompressionReportDetails } from "./types.ts";
10
14
 
11
15
  function formatAttachmentLine(
12
16
  attachment: ImageAttachment,
@@ -18,12 +22,21 @@ function formatAttachmentLine(
18
22
  return visibleWidth(line) > maxWidth ? truncateToWidth(line, maxWidth, "") : line;
19
23
  }
20
24
 
25
+ export type ImagePreviewMessageStyle = "raw" | "collapsible";
26
+
27
+ interface ImagePreviewMessageTheme extends ImageTheme {
28
+ background?: (text: string) => string;
29
+ title?: (text: string) => string;
30
+ muted?: (text: string) => string;
31
+ }
32
+
21
33
  export class ImagePreviewMessage implements Component {
22
34
  private readonly images: Image[];
23
35
 
24
36
  constructor(
25
37
  private readonly attachments: ImageAttachment[],
26
- private readonly theme: ImageTheme,
38
+ private readonly theme: ImagePreviewMessageTheme,
39
+ private readonly options: { expanded?: boolean; style?: ImagePreviewMessageStyle } = {},
27
40
  ) {
28
41
  this.images = attachments.map(
29
42
  (attachment) =>
@@ -35,7 +48,17 @@ export class ImagePreviewMessage implements Component {
35
48
  );
36
49
  }
37
50
 
51
+ invalidate(): void {
52
+ for (const image of this.images) image.invalidate();
53
+ }
54
+
38
55
  render(width: number): string[] {
56
+ return this.options.style === "collapsible"
57
+ ? this.renderCollapsible(width)
58
+ : this.renderRaw(width);
59
+ }
60
+
61
+ private renderRaw(width: number): string[] {
39
62
  const lines: string[] = [];
40
63
  const safeWidth = Math.max(1, width);
41
64
  for (let index = 0; index < this.attachments.length; index++) {
@@ -46,9 +69,75 @@ export class ImagePreviewMessage implements Component {
46
69
  return lines;
47
70
  }
48
71
 
49
- invalidate(): void {
50
- for (const image of this.images) image.invalidate();
72
+ private renderCollapsible(width: number): string[] {
73
+ const container = new Container();
74
+ container.addChild(new Spacer(1));
75
+ const box = new Box(1, 1, this.theme.background);
76
+ container.addChild(box);
77
+
78
+ const title = this.theme.title ?? this.theme.fallbackColor;
79
+ const muted = this.theme.muted ?? this.theme.fallbackColor;
80
+ const summary =
81
+ this.attachments.length === 1
82
+ ? `Attached ${this.attachments[0]!.placeholder}`
83
+ : `Attached ${this.attachments.length} images`;
84
+ const suffix = this.options.expanded ? " (ctrl+o to collapse)" : " (ctrl+o to expand)";
85
+ box.addChild(new Text(`${title(summary)}${muted(suffix)}`, 0, 0));
86
+
87
+ for (const attachment of this.attachments) {
88
+ box.addChild(new Text(formatAttachmentLine(attachment, width, muted), 0, 0));
89
+ }
90
+
91
+ const lines = container.render(width);
92
+ if (!this.options.expanded) return lines;
93
+
94
+ const safeWidth = Math.max(1, width);
95
+ for (let index = 0; index < this.attachments.length; index++) {
96
+ lines.push(...this.images[index]!.render(safeWidth));
97
+ }
98
+ return lines;
99
+ }
100
+ }
101
+
102
+ interface CompressionReportTheme {
103
+ background?: (text: string) => string;
104
+ title: (text: string) => string;
105
+ muted: (text: string) => string;
106
+ }
107
+
108
+ export class ImageCompressionReportMessage implements Component {
109
+ constructor(
110
+ private readonly details: ImageCompressionReportDetails,
111
+ private readonly theme: CompressionReportTheme,
112
+ private readonly expanded = false,
113
+ ) {}
114
+
115
+ render(width: number): string[] {
116
+ const container = new Container();
117
+ container.addChild(new Spacer(1));
118
+ const box = new Box(1, 1, this.theme.background);
119
+ container.addChild(box);
120
+
121
+ const suffix = this.expanded ? " (ctrl+o to collapse)" : " (ctrl+o to expand)";
122
+ box.addChild(
123
+ new Text(
124
+ `${this.theme.title(`Compressed ${this.details.imageCount} image block(s) into ${this.details.summaryCount} summary/summaries`)}${this.theme.muted(suffix)}`,
125
+ 0,
126
+ 0,
127
+ ),
128
+ );
129
+
130
+ if (this.expanded) {
131
+ for (const item of this.details.items) {
132
+ box.addChild(new Text(this.theme.muted(`Image ${item.index}:`), 0, 0));
133
+ box.addChild(new Text(truncateToWidth(item.summary, Math.max(1, width - 4), "…"), 0, 0));
134
+ }
135
+ }
136
+
137
+ return container.render(width);
51
138
  }
139
+
140
+ invalidate(): void {}
52
141
  }
53
142
 
54
143
  interface CursorPreviewTheme {
package/src/types.ts CHANGED
@@ -57,3 +57,14 @@ export type LoadImageResult =
57
57
  export interface PasterPreviewDetails {
58
58
  placeholders: string[];
59
59
  }
60
+
61
+ export interface ImageCompressionReportItem {
62
+ index: number;
63
+ summary: string;
64
+ }
65
+
66
+ export interface ImageCompressionReportDetails {
67
+ imageCount: number;
68
+ summaryCount: number;
69
+ items: ImageCompressionReportItem[];
70
+ }