@narumitw/pi-webui 0.21.0 → 0.25.0
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 +59 -27
- package/package.json +3 -1
- package/src/attachments.ts +715 -0
- package/src/conversation.ts +23 -6
- package/src/drafts.ts +226 -0
- package/src/image-limits.ts +36 -0
- package/src/image-pipeline.ts +325 -0
- package/src/images.ts +182 -140
- package/src/pi-settings.ts +11 -6
- package/src/runtime.ts +138 -34
- package/src/sent-images.ts +279 -0
- package/src/server.ts +490 -30
- package/src/settings.ts +102 -7
- package/src/vendor.d.ts +30 -0
- package/src/web/app.js +558 -100
- package/src/web/image-drag.js +86 -0
- package/src/web/index.html +25 -2
- package/src/web/state.js +178 -8
- package/src/web/styles.css +176 -11
- package/src/web/transcript.js +66 -9
package/src/settings.ts
CHANGED
|
@@ -9,15 +9,35 @@ import {
|
|
|
9
9
|
} from "node:fs/promises";
|
|
10
10
|
import { dirname, join } from "node:path";
|
|
11
11
|
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
12
|
+
import {
|
|
13
|
+
DEFAULT_IMAGE_LIMITS,
|
|
14
|
+
IMAGE_HARD_LIMITS,
|
|
15
|
+
type ImageLimits,
|
|
16
|
+
imageLimits,
|
|
17
|
+
} from "./image-limits.js";
|
|
12
18
|
|
|
13
19
|
export const SETTINGS_FILE = "pi-webui.json";
|
|
14
20
|
|
|
15
|
-
|
|
21
|
+
const MIB = 1024 * 1024;
|
|
22
|
+
|
|
23
|
+
export interface WebUISettings extends ImageLimits {
|
|
16
24
|
startOnSessionStart: boolean;
|
|
25
|
+
retainSentImages: boolean;
|
|
26
|
+
maxRetainedImages: number;
|
|
27
|
+
maxRetainedBytes: number;
|
|
17
28
|
}
|
|
18
29
|
|
|
19
30
|
export const DEFAULT_SETTINGS: Readonly<WebUISettings> = Object.freeze({
|
|
20
31
|
startOnSessionStart: false,
|
|
32
|
+
retainSentImages: false,
|
|
33
|
+
maxRetainedImages: 32,
|
|
34
|
+
maxRetainedBytes: 128 * MIB,
|
|
35
|
+
...DEFAULT_IMAGE_LIMITS,
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
export const RETENTION_HARD_LIMITS = Object.freeze({
|
|
39
|
+
maxRetainedImages: 128,
|
|
40
|
+
maxRetainedBytes: 512 * MIB,
|
|
21
41
|
});
|
|
22
42
|
|
|
23
43
|
export interface SettingsLoadResult {
|
|
@@ -49,17 +69,66 @@ export function settingsFilePath(): string {
|
|
|
49
69
|
export function normalizeSettings(value: unknown): WebUISettings | undefined {
|
|
50
70
|
if (!isRecord(value)) return undefined;
|
|
51
71
|
if (
|
|
52
|
-
Object.hasOwn(value, "startOnSessionStart") &&
|
|
53
|
-
|
|
72
|
+
(Object.hasOwn(value, "startOnSessionStart") &&
|
|
73
|
+
typeof value.startOnSessionStart !== "boolean") ||
|
|
74
|
+
(Object.hasOwn(value, "retainSentImages") && typeof value.retainSentImages !== "boolean")
|
|
54
75
|
) {
|
|
55
76
|
return undefined;
|
|
56
77
|
}
|
|
57
|
-
|
|
78
|
+
for (const [key, maximum] of [
|
|
79
|
+
["maxRetainedImages", RETENTION_HARD_LIMITS.maxRetainedImages],
|
|
80
|
+
["maxRetainedBytes", RETENTION_HARD_LIMITS.maxRetainedBytes],
|
|
81
|
+
["maxImages", IMAGE_HARD_LIMITS.maxImages],
|
|
82
|
+
["maxImageBytes", IMAGE_HARD_LIMITS.maxImageBytes],
|
|
83
|
+
["maxBatchBytes", IMAGE_HARD_LIMITS.maxBatchBytes],
|
|
84
|
+
["maxImagePixels", IMAGE_HARD_LIMITS.maxImagePixels],
|
|
85
|
+
] as const) {
|
|
86
|
+
if (!Object.hasOwn(value, key)) continue;
|
|
87
|
+
const candidate = value[key];
|
|
88
|
+
if (
|
|
89
|
+
typeof candidate !== "number" ||
|
|
90
|
+
!Number.isSafeInteger(candidate) ||
|
|
91
|
+
candidate <= 0 ||
|
|
92
|
+
candidate > maximum
|
|
93
|
+
) {
|
|
94
|
+
return undefined;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
const normalized = {
|
|
58
98
|
startOnSessionStart:
|
|
59
99
|
typeof value.startOnSessionStart === "boolean"
|
|
60
100
|
? value.startOnSessionStart
|
|
61
101
|
: DEFAULT_SETTINGS.startOnSessionStart,
|
|
102
|
+
retainSentImages:
|
|
103
|
+
typeof value.retainSentImages === "boolean"
|
|
104
|
+
? value.retainSentImages
|
|
105
|
+
: DEFAULT_SETTINGS.retainSentImages,
|
|
106
|
+
maxRetainedImages:
|
|
107
|
+
typeof value.maxRetainedImages === "number"
|
|
108
|
+
? value.maxRetainedImages
|
|
109
|
+
: DEFAULT_SETTINGS.maxRetainedImages,
|
|
110
|
+
maxRetainedBytes:
|
|
111
|
+
typeof value.maxRetainedBytes === "number"
|
|
112
|
+
? value.maxRetainedBytes
|
|
113
|
+
: DEFAULT_SETTINGS.maxRetainedBytes,
|
|
114
|
+
...imageLimits({
|
|
115
|
+
maxImages: typeof value.maxImages === "number" ? value.maxImages : DEFAULT_SETTINGS.maxImages,
|
|
116
|
+
maxImageBytes:
|
|
117
|
+
typeof value.maxImageBytes === "number"
|
|
118
|
+
? value.maxImageBytes
|
|
119
|
+
: DEFAULT_SETTINGS.maxImageBytes,
|
|
120
|
+
maxBatchBytes:
|
|
121
|
+
typeof value.maxBatchBytes === "number"
|
|
122
|
+
? value.maxBatchBytes
|
|
123
|
+
: DEFAULT_SETTINGS.maxBatchBytes,
|
|
124
|
+
maxImagePixels:
|
|
125
|
+
typeof value.maxImagePixels === "number"
|
|
126
|
+
? value.maxImagePixels
|
|
127
|
+
: DEFAULT_SETTINGS.maxImagePixels,
|
|
128
|
+
}),
|
|
62
129
|
};
|
|
130
|
+
if (normalized.maxImageBytes > normalized.maxBatchBytes) return undefined;
|
|
131
|
+
return normalized;
|
|
63
132
|
}
|
|
64
133
|
|
|
65
134
|
export async function loadSettings(path = settingsFilePath()): Promise<SettingsLoadResult> {
|
|
@@ -86,8 +155,16 @@ export async function loadSettings(path = settingsFilePath()): Promise<SettingsL
|
|
|
86
155
|
const document = JSON.parse(text) as unknown;
|
|
87
156
|
if (!isRecord(document)) return invalid(path, "the top level must be a JSON object");
|
|
88
157
|
const settings = normalizeSettings(document);
|
|
89
|
-
if (!settings) return invalid(path, "
|
|
90
|
-
|
|
158
|
+
if (!settings) return invalid(path, "recognized settings have an invalid type or limit");
|
|
159
|
+
const warning = elevatedLimitWarning(settings);
|
|
160
|
+
return {
|
|
161
|
+
kind: "loaded",
|
|
162
|
+
path,
|
|
163
|
+
settings,
|
|
164
|
+
source: "settings file",
|
|
165
|
+
document,
|
|
166
|
+
...(warning ? { warning } : {}),
|
|
167
|
+
};
|
|
91
168
|
} catch (error) {
|
|
92
169
|
return invalid(path, formatError(error));
|
|
93
170
|
}
|
|
@@ -99,7 +176,17 @@ export async function saveSettings(
|
|
|
99
176
|
path = settingsFilePath(),
|
|
100
177
|
operations: Partial<SettingsFileOperations> = {},
|
|
101
178
|
): Promise<Record<string, unknown>> {
|
|
102
|
-
const nextDocument = {
|
|
179
|
+
const nextDocument = {
|
|
180
|
+
...document,
|
|
181
|
+
startOnSessionStart: settings.startOnSessionStart,
|
|
182
|
+
retainSentImages: settings.retainSentImages,
|
|
183
|
+
maxRetainedImages: settings.maxRetainedImages,
|
|
184
|
+
maxRetainedBytes: settings.maxRetainedBytes,
|
|
185
|
+
maxImages: settings.maxImages,
|
|
186
|
+
maxImageBytes: settings.maxImageBytes,
|
|
187
|
+
maxBatchBytes: settings.maxBatchBytes,
|
|
188
|
+
maxImagePixels: settings.maxImagePixels,
|
|
189
|
+
};
|
|
103
190
|
const directory = dirname(path);
|
|
104
191
|
await mkdir(directory, { recursive: true });
|
|
105
192
|
const temporaryPath = temporaryFilePath(path);
|
|
@@ -147,6 +234,14 @@ export async function initializeSettings(
|
|
|
147
234
|
}
|
|
148
235
|
}
|
|
149
236
|
|
|
237
|
+
function elevatedLimitWarning(settings: WebUISettings): string | undefined {
|
|
238
|
+
const elevated = (Object.keys(DEFAULT_IMAGE_LIMITS) as Array<keyof ImageLimits>).filter(
|
|
239
|
+
(key) => settings[key] > DEFAULT_IMAGE_LIMITS[key],
|
|
240
|
+
);
|
|
241
|
+
if (elevated.length === 0) return undefined;
|
|
242
|
+
return `${SETTINGS_FILE} uses image limits above safe defaults: ${elevated.join(", ")}. Higher limits increase Pi-process memory and processing cost.`;
|
|
243
|
+
}
|
|
244
|
+
|
|
150
245
|
function invalid(path: string, reason: string): SettingsLoadResult {
|
|
151
246
|
return {
|
|
152
247
|
kind: "invalid",
|
package/src/vendor.d.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
declare module "bmp-js" {
|
|
2
|
+
interface DecodedBmp {
|
|
3
|
+
data: Buffer;
|
|
4
|
+
width: number;
|
|
5
|
+
height: number;
|
|
6
|
+
}
|
|
7
|
+
export function decode(buffer: Buffer, withAlpha?: boolean): DecodedBmp;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
declare module "heic-decode" {
|
|
11
|
+
interface DecodedImage {
|
|
12
|
+
width: number;
|
|
13
|
+
height: number;
|
|
14
|
+
data: Uint8ClampedArray;
|
|
15
|
+
}
|
|
16
|
+
interface DeferredImage {
|
|
17
|
+
width: number;
|
|
18
|
+
height: number;
|
|
19
|
+
decode(): Promise<DecodedImage>;
|
|
20
|
+
}
|
|
21
|
+
interface DeferredImages extends Array<DeferredImage> {
|
|
22
|
+
dispose(): void;
|
|
23
|
+
}
|
|
24
|
+
interface Decode {
|
|
25
|
+
(options: { buffer: Uint8Array }): Promise<DecodedImage>;
|
|
26
|
+
all(options: { buffer: Uint8Array }): Promise<DeferredImages>;
|
|
27
|
+
}
|
|
28
|
+
const decode: Decode;
|
|
29
|
+
export default decode;
|
|
30
|
+
}
|