@bacnh85/pi-attachments 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/CHANGELOG.md +17 -0
- package/README.md +3 -1
- package/extensions/index.ts +40 -24
- package/extensions/lib/paths.ts +1 -1
- package/package.json +6 -6
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.2.2
|
|
4
|
+
|
|
5
|
+
- **Directories are not attachments.** Pasting/dropping a path that is not an
|
|
6
|
+
existing regular file (a directory, a typo) no longer becomes an
|
|
7
|
+
`[[attach:...]]` token + 📎 chip — it stays plain text in the prompt. The
|
|
8
|
+
model still sees the path and can `ls`/`read` it. The clipboard shortcut
|
|
9
|
+
(alt+shift+v) likewise skips folder copies; a folder-only clipboard reports
|
|
10
|
+
"No files in clipboard".
|
|
11
|
+
- Fixed: a dropped/pasted file whose name contains `]` (e.g. `we]ird.png`) now
|
|
12
|
+
attaches on submit instead of silently leaking a dead `[[attach:...]]` token;
|
|
13
|
+
inline mode no longer nests `<file>` blocks when a dropped token and typed
|
|
14
|
+
paths are combined.
|
|
15
|
+
|
|
16
|
+
## 0.2.1 (2026-09-05)
|
|
17
|
+
|
|
18
|
+
- Widen Pi SDK peer range to `>=0.85.0 <0.86.0` and bump devDep to `^0.85.0` for Pi 0.85.0 compatibility (no breaking changes; peer cap widening only).
|
|
19
|
+
|
|
3
20
|
## 0.2.0
|
|
4
21
|
|
|
5
22
|
- **Readable path references by default (read on demand).** `[[attach:name]]`
|
package/README.md
CHANGED
|
@@ -81,7 +81,9 @@ pi install @bacnh85/pi-attachments
|
|
|
81
81
|
dropped file — no dead tokens.
|
|
82
82
|
- Conservative matching: prose like "see main.rs" or "the .jpg extension" never
|
|
83
83
|
triggers anything — images require an existing file with an image extension,
|
|
84
|
-
text inlining requires an absolute existing path.
|
|
84
|
+
text inlining requires an absolute existing path. Pasted/dropped paths that
|
|
85
|
+
are not existing regular files (directories, typos) pass through as plain
|
|
86
|
+
text — no chip, no token.
|
|
85
87
|
- pi core auto-resizes attached images (see the `images.autoResize` setting).
|
|
86
88
|
- kitty's OSC 72 drag-drop protocol is not supported (requires raw stdin access
|
|
87
89
|
that extensions don't have); kitty <0.47 drops paths, which work.
|
package/extensions/index.ts
CHANGED
|
@@ -2,9 +2,10 @@
|
|
|
2
2
|
* pi-attachments — real attachments from pasted/dropped file paths.
|
|
3
3
|
*
|
|
4
4
|
* Drag-drop / clipboard-paste flow:
|
|
5
|
-
* 1. onTerminalInput intercepts
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* 1. onTerminalInput intercepts path-only bracketed pastes BEFORE the editor:
|
|
6
|
+
* each existing regular file becomes an [[attach:name]] token and a 📎 chip
|
|
7
|
+
* list shows above the editor (widget); non-file paths (directories, typos)
|
|
8
|
+
* stay literal text, and an all-non-file payload passes through untouched.
|
|
8
9
|
* 2. On submit, the input hook resolves tokens:
|
|
9
10
|
* - images → 📎 path text + real ImageContent parts
|
|
10
11
|
* - text files → 📎 path text (default; model reads on demand via read)
|
|
@@ -16,7 +17,7 @@ import type { ExtensionAPI, InputEvent, TerminalInputHandler } from "@earendil-w
|
|
|
16
17
|
import { detectSupportedImageMimeTypeFromFile } from "@earendil-works/pi-coding-agent";
|
|
17
18
|
import { readFile, stat } from "node:fs/promises";
|
|
18
19
|
import { readClipboardFilePaths } from "./lib/clipboard-files";
|
|
19
|
-
import { absolutePathSpans, extractImagePaths } from "./lib/paths";
|
|
20
|
+
import { absolutePathSpans, extractImagePaths, isFile } from "./lib/paths";
|
|
20
21
|
import { lookup, remember } from "./lib/registry";
|
|
21
22
|
import { loadSettings } from "./lib/settings";
|
|
22
23
|
import { AttachmentTray } from "./lib/tray";
|
|
@@ -57,10 +58,18 @@ export default function piAttachments(pi: ExtensionAPI): void {
|
|
|
57
58
|
if (!m || !looksLikePathPayload(m[1])) return undefined;
|
|
58
59
|
const tokens: string[] = [];
|
|
59
60
|
for (const raw of splitPathTokens(m[1])) {
|
|
60
|
-
const
|
|
61
|
+
const path = raw.replace(/\\ /g, " ");
|
|
62
|
+
if (!isFile(path)) {
|
|
63
|
+
// ponytail: directories/nonexistent paths stay literal text — the model
|
|
64
|
+
// reads or lists them itself; upgrade to 📁 chips if dir drops matter
|
|
65
|
+
tokens.push(raw);
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
const item = tray.add(path);
|
|
61
69
|
remember(item.name, item.path); // survive session restarts
|
|
62
70
|
tokens.push(item.token);
|
|
63
71
|
}
|
|
72
|
+
if (tokens.every((t) => !t.startsWith("[[attach:"))) return undefined;
|
|
64
73
|
updateWidget();
|
|
65
74
|
return { data: tokens.join(" ") };
|
|
66
75
|
};
|
|
@@ -88,13 +97,18 @@ export default function piAttachments(pi: ExtensionAPI): void {
|
|
|
88
97
|
|
|
89
98
|
const images: Array<{ type: "image"; data: string; mimeType: string }> = [...(event.images ?? [])];
|
|
90
99
|
const attachedImages = new Set<string>(); // paths already attached via tokens
|
|
91
|
-
|
|
100
|
+
// All replacements are spans in `raw` coordinates (tokens + typed paths are
|
|
101
|
+
// disjoint by construction), applied right-to-left at the end — so generated
|
|
102
|
+
// blocks are never re-scanned (no nested <file> inlining).
|
|
103
|
+
const replacements: Array<{ start: number; end: number; block: string }> = [];
|
|
92
104
|
|
|
93
105
|
// a. Resolve [[attach:name]] tokens (tray first, then persistent registry).
|
|
94
|
-
|
|
106
|
+
// `]` in a basename is allowed when not followed by the closing `]]`
|
|
107
|
+
// (e.g. a dropped file named "we]ird.png").
|
|
108
|
+
for (const m of raw.matchAll(/\[\[attach:((?:[^\]]|\](?!\]))+)\]\]/g)) {
|
|
95
109
|
const token = m[0];
|
|
96
110
|
const name = m[1];
|
|
97
|
-
|
|
111
|
+
const start = m.index;
|
|
98
112
|
const trayItem = tray.resolve(raw).find((i) => i.token === token);
|
|
99
113
|
const path = trayItem?.path ?? lookup(name);
|
|
100
114
|
if (!path) continue; // unknown token — leave as-is for the model
|
|
@@ -111,29 +125,29 @@ export default function piAttachments(pi: ExtensionAPI): void {
|
|
|
111
125
|
/* unreadable → skip */
|
|
112
126
|
}
|
|
113
127
|
}
|
|
114
|
-
|
|
128
|
+
replacements.push({ start, end: start + token.length, block: `📎 ${path}` });
|
|
115
129
|
continue;
|
|
116
130
|
}
|
|
117
131
|
|
|
118
132
|
// Non-image: inline as <file> only when inlineTextFiles is on and size allows;
|
|
119
133
|
// otherwise resolve to a 📎 path the model reads on demand.
|
|
134
|
+
let block = `📎 ${path}`;
|
|
120
135
|
if (settings.inlineTextFiles) {
|
|
121
136
|
try {
|
|
122
137
|
const s = await stat(path);
|
|
123
138
|
if (s.size <= settings.maxInlineBytes) {
|
|
124
139
|
const content = (await readFile(path, "utf-8")).replace(/^\uFEFF/, "").replace(/\n$/, "");
|
|
125
|
-
|
|
126
|
-
continue;
|
|
140
|
+
block = `<file name="${path}">\n${content}\n</file>`;
|
|
127
141
|
}
|
|
128
142
|
} catch {
|
|
129
|
-
/*
|
|
143
|
+
/* keep 📎 path */
|
|
130
144
|
}
|
|
131
145
|
}
|
|
132
|
-
|
|
146
|
+
replacements.push({ start, end: start + token.length, block });
|
|
133
147
|
}
|
|
134
148
|
|
|
135
149
|
// b. Existing image paths typed elsewhere in the message → attach too.
|
|
136
|
-
for (const p of extractImagePaths(
|
|
150
|
+
for (const p of extractImagePaths(raw)) {
|
|
137
151
|
if (attachedImages.has(p)) continue;
|
|
138
152
|
try {
|
|
139
153
|
const mimeType = await detectSupportedImageMimeTypeFromFile(p);
|
|
@@ -146,11 +160,10 @@ export default function piAttachments(pi: ExtensionAPI): void {
|
|
|
146
160
|
}
|
|
147
161
|
|
|
148
162
|
// c. Text-path inlining (opt-in old behavior): absolute text-file paths in
|
|
149
|
-
// the message → <file> blocks.
|
|
150
|
-
//
|
|
163
|
+
// the message → <file> blocks. One greedy regex pass yields disjoint
|
|
164
|
+
// spans that never overlap the token spans above.
|
|
151
165
|
if (settings.inlineTextFiles) {
|
|
152
|
-
const
|
|
153
|
-
for (const span of absolutePathSpans(text)) {
|
|
166
|
+
for (const span of absolutePathSpans(raw)) {
|
|
154
167
|
try {
|
|
155
168
|
const s = await stat(span.path);
|
|
156
169
|
if (s.size > settings.maxInlineBytes) continue;
|
|
@@ -160,11 +173,13 @@ export default function piAttachments(pi: ExtensionAPI): void {
|
|
|
160
173
|
/* unreadable file → skip */
|
|
161
174
|
}
|
|
162
175
|
}
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
let text = raw;
|
|
179
|
+
if (replacements.length) {
|
|
180
|
+
replacements.sort((a, b) => b.start - a.start);
|
|
181
|
+
for (const r of replacements) {
|
|
182
|
+
text = text.slice(0, r.start) + r.block + text.slice(r.end);
|
|
168
183
|
}
|
|
169
184
|
}
|
|
170
185
|
|
|
@@ -181,7 +196,8 @@ export default function piAttachments(pi: ExtensionAPI): void {
|
|
|
181
196
|
pi.registerShortcut(settings.pasteFileShortcut as Parameters<ExtensionAPI["registerShortcut"]>[0], {
|
|
182
197
|
description: "Paste file(s) from clipboard as attachments",
|
|
183
198
|
handler: async (ctx) => {
|
|
184
|
-
|
|
199
|
+
// only existing regular files are attachable — Finder/Explorer folder copies pass through
|
|
200
|
+
const paths = (await readClipboardFilePaths()).filter(isFile);
|
|
185
201
|
if (paths.length === 0) {
|
|
186
202
|
ctx.ui.notify("No files in clipboard", "info");
|
|
187
203
|
return;
|
package/extensions/lib/paths.ts
CHANGED
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bacnh85/pi-attachments",
|
|
3
|
-
"version": "0.2.
|
|
4
|
-
"description": "Image and file attachments for Pi
|
|
3
|
+
"version": "0.2.2",
|
|
4
|
+
"description": "Image and file attachments for Pi — converts pasted/dropped file paths into real image attachments, inlines text files, and pastes clipboard file references.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"publishConfig": {
|
|
7
7
|
"access": "public"
|
|
@@ -39,12 +39,12 @@
|
|
|
39
39
|
"typecheck": "tsc --noEmit"
|
|
40
40
|
},
|
|
41
41
|
"peerDependencies": {
|
|
42
|
-
"@earendil-works/pi-coding-agent": ">=0.84.3 <0.
|
|
42
|
+
"@earendil-works/pi-coding-agent": ">=0.84.3 <0.86.0"
|
|
43
43
|
},
|
|
44
44
|
"devDependencies": {
|
|
45
|
-
"@earendil-works/pi-
|
|
46
|
-
"@earendil-works/pi-
|
|
47
|
-
"@earendil-works/pi-tui": "^0.
|
|
45
|
+
"@earendil-works/pi-ai": "^0.85.0",
|
|
46
|
+
"@earendil-works/pi-coding-agent": "^0.85.0",
|
|
47
|
+
"@earendil-works/pi-tui": "^0.85.0",
|
|
48
48
|
"@types/mocha": "^10.0.10",
|
|
49
49
|
"@types/node": "^20.19.43",
|
|
50
50
|
"chai": "^4.5.0",
|