@quandev104/pi-style 0.2.1 → 0.2.3

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.
Files changed (38) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/README.md +8 -3
  3. package/dist/extensions/pi-style.js +1328 -333
  4. package/dist/extensions/pi-style.js.map +1 -1
  5. package/extension-src/pi-style/app/command-service.ts +8 -0
  6. package/extension-src/pi-style/app/runtime.ts +33 -4
  7. package/extension-src/pi-style/domain/config-normalization.ts +11 -1
  8. package/extension-src/pi-style/domain/config-types.ts +12 -0
  9. package/extension-src/pi-style/domain/status-renderer.ts +41 -9
  10. package/extension-src/pi-style/domain/status.ts +28 -12
  11. package/extension-src/pi-style/domain/theme.ts +32 -1
  12. package/extension-src/pi-style/features/editor/index.ts +144 -5
  13. package/extension-src/pi-style/features/messages/image-input.ts +205 -0
  14. package/extension-src/pi-style/features/messages/image-preview.ts +288 -0
  15. package/extension-src/pi-style/features/messages/index.ts +302 -61
  16. package/extension-src/pi-style/features/messages/render-config.ts +36 -0
  17. package/extension-src/pi-style/features/status-line/index.ts +41 -11
  18. package/extension-src/pi-style/features/tools/boxed/bash.ts +101 -40
  19. package/extension-src/pi-style/features/tools/boxed/batch.ts +17 -1
  20. package/extension-src/pi-style/features/tools/boxed/edit.ts +20 -15
  21. package/extension-src/pi-style/features/tools/boxed/find.ts +9 -4
  22. package/extension-src/pi-style/features/tools/boxed/git.ts +46 -2
  23. package/extension-src/pi-style/features/tools/boxed/grep.ts +9 -2
  24. package/extension-src/pi-style/features/tools/boxed/ls.ts +9 -4
  25. package/extension-src/pi-style/features/tools/boxed/quick-edit.ts +18 -11
  26. package/extension-src/pi-style/features/tools/boxed/read.ts +4 -2
  27. package/extension-src/pi-style/features/tools/boxed/shared.ts +27 -1
  28. package/extension-src/pi-style/features/tools/boxed/turn-summary.ts +12 -0
  29. package/extension-src/pi-style/pi/index.ts +55 -2
  30. package/extension-src/pi-style/pi/session-coordinator.ts +13 -0
  31. package/extension-src/pi-style/shared/ansi.ts +17 -5
  32. package/extension-src/pi-style/shared/box.ts +70 -4
  33. package/extension-src/pi-style/shared/clipboard-path.ts +98 -0
  34. package/extension-src/pi-style/shared/clipboard-presence.ts +112 -0
  35. package/extension-src/pi-style/shared/pending-images.ts +199 -0
  36. package/extension-src/pi-style/shared/split-diff.ts +8 -5
  37. package/package.json +1 -1
  38. package/extension-src/pi-style/features/.gitkeep +0 -0
@@ -0,0 +1,199 @@
1
+ // Pending clipboard-image registry (ADR 0009): markers ↔ image bytes.
2
+ //
3
+ // Shared state between the editor feature (allocates/discards markers at
4
+ // keystroke time — instant feedback, image bytes arrive later) and the
5
+ // messages feature (fills entries from artifacts, resolves markers at
6
+ // submit). Lives in shared/ so both features can use it without
7
+ // cross-feature imports.
8
+ //
9
+ // Lifecycle: entries are created by allocatePendingImage() when a paste
10
+ // keystroke inserts an `[Image #N] ` marker; filled asynchronously by
11
+ // fillPendingImageFromPath() once the bytes exist (editor artifact read or
12
+ // fallback); consumed one-shot by resolvePendingImagesForSubmit() (markers
13
+ // present in the submitted text attach, removed markers discard); discarded
14
+ // by discardPendingImage() when the user backspaces a whole marker or a
15
+ // rollback removes it. Session boundaries reset everything.
16
+
17
+ import { readFile, stat } from "node:fs/promises";
18
+ import { clipboardImageMarker } from "./clipboard-path.js";
19
+
20
+ /** Mirrors image-paste's guard; larger files never register. */
21
+ const MAX_ATTACHABLE_BYTES = 20 * 1024 * 1024;
22
+
23
+ const MIME_BY_EXTENSION: Readonly<Record<string, string>> = Object.freeze({
24
+ png: "image/png",
25
+ jpg: "image/jpeg",
26
+ jpeg: "image/jpeg",
27
+ webp: "image/webp",
28
+ gif: "image/gif",
29
+ });
30
+
31
+ interface PendingEntry {
32
+ /** Resolves once fill completes (success or failure); submit awaits it. */
33
+ readonly fill: Promise<void>;
34
+ data?: string;
35
+ mimeType?: string;
36
+ settled: boolean;
37
+ }
38
+
39
+ const pendingImages = new Map<number, PendingEntry>();
40
+ let nextPendingIndex = 1;
41
+
42
+ /** Reset the registry and marker counter (session start/shutdown). */
43
+ export function resetPendingImageRegistry(): void {
44
+ pendingImages.clear();
45
+ nextPendingIndex = 1;
46
+ }
47
+
48
+ /** Allocate the next marker index (marker text: `[Image #N] `). */
49
+ export function allocatePendingImage(): { index: number; marker: string } {
50
+ const index = nextPendingIndex++;
51
+ let resolveFill: () => void = () => {};
52
+ const entry: PendingEntry = {
53
+ fill: new Promise<void>((resolve) => {
54
+ resolveFill = resolve;
55
+ }),
56
+ settled: false,
57
+ };
58
+ pendingImages.set(index, entry);
59
+ (entry as { resolveFill?: () => void }).resolveFill = resolveFill;
60
+ return { index, marker: `${clipboardImageMarker(index)} ` };
61
+ }
62
+
63
+ /** Fill an allocated entry from a clipboard artifact file (stat + read + base64).
64
+ * Guards: missing/empty/oversized/unknown-extension → entry stays unfilled
65
+ * (marker resolves to plain text at submit — nothing is lost). */
66
+ export async function fillPendingImageFromPath(
67
+ index: number,
68
+ path: string,
69
+ deps: {
70
+ readFile?: (path: string) => Promise<Uint8Array>;
71
+ statSize?: (path: string) => Promise<number | undefined>;
72
+ } = {},
73
+ ): Promise<void> {
74
+ const entry = pendingImages.get(index);
75
+ if (!entry || entry.settled) return;
76
+ const extension = path.split(".").pop() ?? "";
77
+ const mimeType = MIME_BY_EXTENSION[extension];
78
+ if (!mimeType) {
79
+ settle(entry);
80
+ return;
81
+ }
82
+ const readFileImpl = deps.readFile ?? ((p: string) => readFile(p));
83
+ const statSizeImpl =
84
+ deps.statSize ??
85
+ (async (p: string) => {
86
+ try {
87
+ return (await stat(p)).size;
88
+ } catch {
89
+ return undefined;
90
+ }
91
+ });
92
+ try {
93
+ const size = await statSizeImpl(path);
94
+ if (size === undefined || size <= 0 || size > MAX_ATTACHABLE_BYTES) {
95
+ settle(entry);
96
+ return;
97
+ }
98
+ const bytes = await readFileImpl(path);
99
+ if (bytes.length === 0 || bytes.length > MAX_ATTACHABLE_BYTES) {
100
+ settle(entry);
101
+ return;
102
+ }
103
+ // Zero-copy view (offset+length explicit so Uint8Array views over a larger
104
+ // backing buffer encode exactly their own range — Buffer.from(bytes)
105
+ // would copy the whole clipboard screenshot again).
106
+ entry.data = Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength).toString("base64");
107
+ entry.mimeType = mimeType;
108
+ } catch {
109
+ // Unreadable artifact: marker stays plain text.
110
+ }
111
+ settle(entry);
112
+ }
113
+
114
+ /** Fill an allocated entry directly from in-memory bytes. */
115
+ export function fillPendingImageBytes(index: number, data: string, mimeType: string): void {
116
+ const entry = pendingImages.get(index);
117
+ if (!entry || entry.settled) return;
118
+ if (data.length > 0) {
119
+ entry.data = data;
120
+ entry.mimeType = mimeType;
121
+ }
122
+ settle(entry);
123
+ }
124
+
125
+ /** Discard a pending entry (whole-marker backspace, rollback, user removal). */
126
+ export function discardPendingImage(index: number): void {
127
+ const entry = pendingImages.get(index);
128
+ if (!entry) return;
129
+ settle(entry);
130
+ pendingImages.delete(index);
131
+ }
132
+
133
+ /** Marker text for an allocated index (editor rollback surgery). */
134
+ export function pendingImageMarker(index: number): string | undefined {
135
+ return pendingImages.has(index) ? `${clipboardImageMarker(index)} ` : undefined;
136
+ }
137
+
138
+ /** True when `index` has an allocated (not yet consumed/discarded) entry. */
139
+ export function isPendingMarkerIndex(index: number): boolean {
140
+ return pendingImages.has(index);
141
+ }
142
+
143
+ /** Discard by marker index (atomic backspace / rollback). */
144
+ export function discardPendingMarkerIndex(index: number): void {
145
+ discardPendingImage(index);
146
+ }
147
+
148
+ /** True when the entry for `index` was successfully filled with bytes. */
149
+ export function isPendingImageFilled(index: number): boolean {
150
+ const entry = pendingImages.get(index);
151
+ return entry !== undefined && entry.data !== undefined && entry.mimeType !== undefined;
152
+ }
153
+
154
+ function settle(entry: PendingEntry): void {
155
+ entry.settled = true;
156
+ (entry as { resolveFill?: () => void }).resolveFill?.();
157
+ }
158
+
159
+ export interface PendingImageAttachment {
160
+ readonly type: "image";
161
+ readonly data: string;
162
+ readonly mimeType: string;
163
+ }
164
+
165
+ /**
166
+ * Resolve `[Image #N]` markers in a submitted text against the registry:
167
+ * awaits pending fills (a fast submit racing a slow artifact read), attaches
168
+ * filled images in ascending index order, keeps the text verbatim, and
169
+ * consumes the registry one-shot per submit — markers removed from the text
170
+ * discard their images (image-paste semantics: the pending queue never
171
+ * outlives the submit after the paste).
172
+ */
173
+ export async function resolvePendingImagesForSubmit(
174
+ text: string,
175
+ ): Promise<{ images: PendingImageAttachment[] } | undefined> {
176
+ if (pendingImages.size === 0) return undefined;
177
+ const markerRe = /\[Image #([0-9]+)\]/g;
178
+ const found: number[] = [];
179
+ for (const match of text.matchAll(markerRe)) {
180
+ const index = Number(match[1]);
181
+ if (Number.isInteger(index) && pendingImages.has(index)) found.push(index);
182
+ }
183
+ const matched = new Set(found);
184
+ // Wait for in-flight fills so a submit right after a paste still attaches.
185
+ await Promise.all(
186
+ [...pendingImages.entries()].filter(([index]) => matched.has(index)).map(([, entry]) => entry.fill),
187
+ );
188
+ const images: PendingImageAttachment[] = [];
189
+ for (const index of [...matched].sort((a, b) => a - b)) {
190
+ const entry = pendingImages.get(index);
191
+ if (entry?.data && entry.mimeType) {
192
+ images.push({ type: "image", data: entry.data, mimeType: entry.mimeType });
193
+ }
194
+ }
195
+ // One-shot consume: every pending entry is spent by this submit — matched
196
+ // (attached or discarded) and unmatched (their markers were removed).
197
+ for (const index of [...pendingImages.keys()]) discardPendingImage(index);
198
+ return images.length > 0 ? { images } : undefined;
199
+ }
@@ -56,7 +56,13 @@ type DiffEntry =
56
56
 
57
57
  const ESC = "\x1b";
58
58
  const BG_ANSI_PATTERN = new RegExp(`${ESC}\\[(?:4\\d|10\\d|48;5;\\d{1,3}|48;2;\\d{1,3};\\d{1,3};\\d{1,3}|49)m`, "g");
59
+ /** Any SGR escape (capture: parameter bytes). Shared with String.replace only —
60
+ * replace resets lastIndex, so the global flag is safe here. */
61
+ const ANSI_SGR_PATTERN = new RegExp(`${ESC}\\[([0-9;]*)m`, "g");
59
62
  const CONTROL_CHARS = "\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F";
63
+ /** Control characters (minus \r/\n, handled separately) stripped from diff
64
+ * text. Replace-only usage, so the global flag is safe (see above). */
65
+ const CONTROL_CHARS_PATTERN = new RegExp(`[${CONTROL_CHARS}]`, "g");
60
66
 
61
67
  const ADD_ROW_BACKGROUND_MIX_RATIO = 0.24;
62
68
  const REMOVE_ROW_BACKGROUND_MIX_RATIO = 0.12;
@@ -173,7 +179,7 @@ function resolveDiffPalette(theme: SplitDiffTheme): DiffPalette {
173
179
  function keepBackgroundAcrossResets(text: string, rowBgAnsi: string): string {
174
180
  if (!text) return text;
175
181
 
176
- return text.replace(new RegExp(`${ESC}\\[([0-9;]*)m`, "g"), (sequence, rawCodes) => {
182
+ return text.replace(ANSI_SGR_PATTERN, (sequence, rawCodes) => {
177
183
  const split = String(rawCodes ?? "")
178
184
  .split(";")
179
185
  .filter(Boolean);
@@ -233,10 +239,7 @@ function applyBackgroundToVisibleRange(
233
239
  // ── Text utilities ─────────────────────────────────────────────────
234
240
 
235
241
  function sanitizeSingleLineText(value: string): string {
236
- return value
237
- .replace(/\r/g, "")
238
- .replace(/\n/g, "")
239
- .replace(new RegExp(`[${CONTROL_CHARS}]`, "g"), "");
242
+ return value.replace(/\r/g, "").replace(/\n/g, "").replace(CONTROL_CHARS_PATTERN, "");
240
243
  }
241
244
 
242
245
  function stripInlineBreaksPreserveAnsi(value: string): string {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quandev104/pi-style",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
4
4
  "description": "A native-layout, cohesive visual style package for Pi.",
5
5
  "license": "MIT",
6
6
  "type": "module",
File without changes