mixdog 0.9.97 → 0.9.99
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/package.json +9 -2
- package/src/rules/agent/30-explorer.md +43 -32
- package/src/runtime/agent/orchestrator/agent-trace.mjs +19 -5
- package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +7 -0
- package/src/runtime/agent/orchestrator/providers/media-normalization.mjs +30 -3
- package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +5 -3
- package/src/runtime/agent/orchestrator/providers/provider-catalog-cache.mjs +23 -1
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +2 -1
- package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +44 -6
- package/src/runtime/agent/orchestrator/session/manager/prompt-utils.mjs +2 -1
- package/src/runtime/agent/orchestrator/session/store/serialize.mjs +3 -6
- package/src/runtime/agent/orchestrator/tools/builtin/read-image-resize.mjs +103 -12
- package/src/runtime/agent/orchestrator/tools/builtin/read-special-files.mjs +10 -30
- package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +27 -84
- package/src/runtime/attachments/pdf-extract.mjs +83 -0
- package/src/runtime/attachments/store.mjs +515 -0
- package/src/runtime/attachments/store.test.mjs +188 -0
- package/src/runtime/channels/lib/interaction-handlers.mjs +1 -191
- package/src/runtime/channels/lib/runtime-paths.mjs +1 -4
- package/src/runtime/channels/lib/worker-ipc.mjs +0 -66
- package/src/runtime/channels/lib/worker-main.mjs +1 -20
- package/src/runtime/memory/lib/http-wire.mjs +39 -4
- package/src/runtime/shared/json-metrics.mjs +94 -0
- package/src/runtime/shared/turn-snapshot.mjs +13 -0
- package/src/runtime/shared/turn-worktree-snapshot.mjs +19 -0
- package/src/session-runtime/provider-models.mjs +11 -1
- package/src/session-runtime/quick-model-rows.mjs +38 -7
- package/src/session-runtime/quick-search-models.mjs +15 -17
- package/src/session-runtime/runtime-core.mjs +9 -4
- package/src/session-runtime/runtime-tunables.mjs +9 -6
- package/src/session-runtime/session-title.mjs +27 -3
- package/src/standalone/channel-transport.mjs +28 -12
- package/src/standalone/daemon.mjs +3 -0
- package/src/standalone/session-protocol.mjs +1 -0
- package/src/standalone/session-service.mjs +23 -1
- package/src/standalone/session-transport.mjs +3 -2
- package/src/tui/app/prompt-submit.mjs +7 -9
- package/src/tui/app/use-prompt-handlers.mjs +7 -3
- package/src/tui/dist/index.mjs +249 -43
- package/src/tui/paste-attachments.mjs +13 -7
- package/src/tui/prompt-history-store.mjs +52 -4
- package/src/tui/session/agent-job-feed.mjs +5 -1
- package/src/tui/session/live-share.mjs +21 -7
- package/src/tui/session/queue-helpers.mjs +6 -6
- package/src/tui/session/session-api-ext.mjs +12 -4
- package/src/tui/session/session-api.mjs +10 -1
- package/src/tui/session/session-flow.mjs +4 -2
- package/src/tui/session-local.mjs +15 -5
package/src/tui/dist/index.mjs
CHANGED
|
@@ -62,6 +62,40 @@ var init_plugin_paths = __esm({
|
|
|
62
62
|
});
|
|
63
63
|
|
|
64
64
|
// src/runtime/agent/orchestrator/tools/builtin/read-image-resize.mjs
|
|
65
|
+
import { createHash } from "node:crypto";
|
|
66
|
+
function imageProfileForProvider(provider) {
|
|
67
|
+
const value = String(provider || "").trim().toLowerCase();
|
|
68
|
+
return /^(?:openai|xai|grok|deepseek|opencode-go|ollama|lmstudio)(?:-|$)/.test(value) ? "openai" : "anthropic";
|
|
69
|
+
}
|
|
70
|
+
function openAIImagePatchCount(width, height) {
|
|
71
|
+
return Math.ceil(Math.max(1, Number(width) || 1) / OPENAI_IMAGE_PATCH_SIZE) * Math.ceil(Math.max(1, Number(height) || 1) / OPENAI_IMAGE_PATCH_SIZE);
|
|
72
|
+
}
|
|
73
|
+
function resizeCacheKey(buffer, ext, maxTokens, profile) {
|
|
74
|
+
return createHash("sha256").update(String(ext || "")).update("\0").update(String(maxTokens || 0)).update("\0").update(String(profile || "")).update("\0").update(buffer).digest("hex");
|
|
75
|
+
}
|
|
76
|
+
function cloneResizeResult(result) {
|
|
77
|
+
return {
|
|
78
|
+
...result,
|
|
79
|
+
...result?.dimensions ? { dimensions: { ...result.dimensions } } : {}
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
function rememberResizeResult(key, result) {
|
|
83
|
+
const bytes = Buffer.byteLength(String(result?.data || ""), "base64");
|
|
84
|
+
if (bytes <= 0 || bytes > IMAGE_RESIZE_CACHE_MAX_BYTES) return;
|
|
85
|
+
const existing = imageResizeCache.get(key);
|
|
86
|
+
if (existing) {
|
|
87
|
+
imageResizeCacheBytes -= existing.bytes;
|
|
88
|
+
imageResizeCache.delete(key);
|
|
89
|
+
}
|
|
90
|
+
imageResizeCache.set(key, { result: cloneResizeResult(result), bytes });
|
|
91
|
+
imageResizeCacheBytes += bytes;
|
|
92
|
+
while (imageResizeCacheBytes > IMAGE_RESIZE_CACHE_MAX_BYTES && imageResizeCache.size > 0) {
|
|
93
|
+
const oldest = imageResizeCache.keys().next().value;
|
|
94
|
+
const evicted = imageResizeCache.get(oldest);
|
|
95
|
+
imageResizeCache.delete(oldest);
|
|
96
|
+
imageResizeCacheBytes -= evicted?.bytes || 0;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
65
99
|
async function loadSharp() {
|
|
66
100
|
if (_sharpPromise === void 0) {
|
|
67
101
|
_sharpPromise = (async () => {
|
|
@@ -101,8 +135,21 @@ function imageMetadataText(dims, sourcePath) {
|
|
|
101
135
|
}
|
|
102
136
|
return `[Image: ${parts.join(", ")}]`;
|
|
103
137
|
}
|
|
104
|
-
async function resizeImageBuffer(buffer, ext, {
|
|
138
|
+
async function resizeImageBuffer(buffer, ext, {
|
|
139
|
+
maxTokens = DEFAULT_IMAGE_MAX_TOKENS,
|
|
140
|
+
profile = "anthropic"
|
|
141
|
+
} = {}) {
|
|
105
142
|
if (!Buffer.isBuffer(buffer) || buffer.length === 0) return null;
|
|
143
|
+
const normalizedProfile = profile === "openai" ? "openai" : "anthropic";
|
|
144
|
+
const cacheKey = resizeCacheKey(buffer, ext, maxTokens, normalizedProfile);
|
|
145
|
+
const cached = imageResizeCache.get(cacheKey);
|
|
146
|
+
if (cached) {
|
|
147
|
+
imageResizeCacheHits += 1;
|
|
148
|
+
imageResizeCache.delete(cacheKey);
|
|
149
|
+
imageResizeCache.set(cacheKey, cached);
|
|
150
|
+
return cloneResizeResult(cached.result);
|
|
151
|
+
}
|
|
152
|
+
imageResizeCacheMisses += 1;
|
|
106
153
|
const sharp = await loadSharp();
|
|
107
154
|
if (!sharp) return null;
|
|
108
155
|
try {
|
|
@@ -118,13 +165,20 @@ async function resizeImageBuffer(buffer, ext, { maxTokens = DEFAULT_IMAGE_MAX_TO
|
|
|
118
165
|
if (originalWidth && originalHeight) {
|
|
119
166
|
let width = originalWidth;
|
|
120
167
|
let height = originalHeight;
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
168
|
+
const maxWidth = normalizedProfile === "openai" ? OPENAI_IMAGE_MAX_DIMENSION : IMAGE_MAX_WIDTH;
|
|
169
|
+
const maxHeight = normalizedProfile === "openai" ? OPENAI_IMAGE_MAX_DIMENSION : IMAGE_MAX_HEIGHT;
|
|
170
|
+
let scale = Math.min(1, maxWidth / width, maxHeight / height);
|
|
171
|
+
if (normalizedProfile === "openai") {
|
|
172
|
+
scale = Math.min(
|
|
173
|
+
scale,
|
|
174
|
+
Math.sqrt(OPENAI_IMAGE_MAX_PATCHES * OPENAI_IMAGE_PATCH_SIZE ** 2 / (width * height))
|
|
175
|
+
);
|
|
124
176
|
}
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
177
|
+
width = Math.max(1, Math.floor(width * scale));
|
|
178
|
+
height = Math.max(1, Math.floor(height * scale));
|
|
179
|
+
while (normalizedProfile === "openai" && openAIImagePatchCount(width, height) > OPENAI_IMAGE_MAX_PATCHES) {
|
|
180
|
+
if (width >= height) width -= 1;
|
|
181
|
+
else height -= 1;
|
|
128
182
|
}
|
|
129
183
|
const needsResize = width !== originalWidth || height !== originalHeight;
|
|
130
184
|
if (needsResize || originalSize > IMAGE_TARGET_RAW_SIZE) {
|
|
@@ -161,16 +215,18 @@ async function resizeImageBuffer(buffer, ext, { maxTokens = DEFAULT_IMAGE_MAX_TO
|
|
|
161
215
|
}
|
|
162
216
|
}
|
|
163
217
|
}
|
|
164
|
-
|
|
218
|
+
const result = {
|
|
165
219
|
data: base64,
|
|
166
220
|
mimeType: `image/${mediaType}`,
|
|
167
221
|
dimensions: { originalWidth, originalHeight, displayWidth: displayWidth2, displayHeight }
|
|
168
222
|
};
|
|
223
|
+
rememberResizeResult(cacheKey, result);
|
|
224
|
+
return result;
|
|
169
225
|
} catch {
|
|
170
226
|
return null;
|
|
171
227
|
}
|
|
172
228
|
}
|
|
173
|
-
var API_IMAGE_MAX_BASE64_SIZE, IMAGE_TARGET_RAW_SIZE, IMAGE_MAX_WIDTH, IMAGE_MAX_HEIGHT, DEFAULT_IMAGE_MAX_TOKENS, _sharpPromise;
|
|
229
|
+
var API_IMAGE_MAX_BASE64_SIZE, IMAGE_TARGET_RAW_SIZE, IMAGE_MAX_WIDTH, IMAGE_MAX_HEIGHT, DEFAULT_IMAGE_MAX_TOKENS, OPENAI_IMAGE_MAX_DIMENSION, OPENAI_IMAGE_PATCH_SIZE, OPENAI_IMAGE_MAX_PATCHES, IMAGE_RESIZE_CACHE_MAX_BYTES, imageResizeCache, imageResizeCacheBytes, imageResizeCacheHits, imageResizeCacheMisses, _sharpPromise;
|
|
174
230
|
var init_read_image_resize = __esm({
|
|
175
231
|
"src/runtime/agent/orchestrator/tools/builtin/read-image-resize.mjs"() {
|
|
176
232
|
API_IMAGE_MAX_BASE64_SIZE = 5 * 1024 * 1024;
|
|
@@ -178,6 +234,14 @@ var init_read_image_resize = __esm({
|
|
|
178
234
|
IMAGE_MAX_WIDTH = 2e3;
|
|
179
235
|
IMAGE_MAX_HEIGHT = 2e3;
|
|
180
236
|
DEFAULT_IMAGE_MAX_TOKENS = Math.ceil(API_IMAGE_MAX_BASE64_SIZE * 0.125);
|
|
237
|
+
OPENAI_IMAGE_MAX_DIMENSION = 2048;
|
|
238
|
+
OPENAI_IMAGE_PATCH_SIZE = 32;
|
|
239
|
+
OPENAI_IMAGE_MAX_PATCHES = 1536;
|
|
240
|
+
IMAGE_RESIZE_CACHE_MAX_BYTES = 64 * 1024 * 1024;
|
|
241
|
+
imageResizeCache = /* @__PURE__ */ new Map();
|
|
242
|
+
imageResizeCacheBytes = 0;
|
|
243
|
+
imageResizeCacheHits = 0;
|
|
244
|
+
imageResizeCacheMisses = 0;
|
|
181
245
|
}
|
|
182
246
|
});
|
|
183
247
|
|
|
@@ -246,17 +310,6 @@ function pastedTextReferenceIds(input) {
|
|
|
246
310
|
const re = /\[Pasted text #(\d+) \+\d+ lines\]/g;
|
|
247
311
|
return new Set([...String(input || "").matchAll(re)].map((m) => Number(m[1]) || 0).filter(Boolean));
|
|
248
312
|
}
|
|
249
|
-
function expandPastedTextTokens(value, pastedTexts = {}) {
|
|
250
|
-
let out = String(value ?? "");
|
|
251
|
-
if (!pastedTexts || typeof pastedTexts !== "object") return out;
|
|
252
|
-
const re = /\[Pasted text #(\d+) \+\d+ lines\]/g;
|
|
253
|
-
out = out.replace(re, (match, idRaw) => {
|
|
254
|
-
const entry = pastedTexts[Number(idRaw)] || pastedTexts[idRaw];
|
|
255
|
-
if (entry && typeof entry.text === "string") return entry.text;
|
|
256
|
-
return match;
|
|
257
|
-
});
|
|
258
|
-
return out;
|
|
259
|
-
}
|
|
260
313
|
function buildPromptContentWithImages(text, pastedImages = {}) {
|
|
261
314
|
const value = String(text ?? "");
|
|
262
315
|
const refs = imageReferenceIds(value);
|
|
@@ -290,10 +343,14 @@ function splitPastedImagePathCandidates(text) {
|
|
|
290
343
|
}
|
|
291
344
|
return out;
|
|
292
345
|
}
|
|
293
|
-
async function imageAttachmentFromBuffer(buffer, mimeType, {
|
|
346
|
+
async function imageAttachmentFromBuffer(buffer, mimeType, {
|
|
347
|
+
filename = "Pasted image",
|
|
348
|
+
sourcePath = "",
|
|
349
|
+
provider = ""
|
|
350
|
+
} = {}) {
|
|
294
351
|
if (!Buffer2.isBuffer(buffer) || buffer.length === 0) throw new Error("image is empty");
|
|
295
352
|
const ext = (mimeType || "image/png").split("/")[1] || "png";
|
|
296
|
-
const resized = await resizeImageBuffer(buffer, ext);
|
|
353
|
+
const resized = await resizeImageBuffer(buffer, ext, { profile: imageProfileForProvider(provider) });
|
|
297
354
|
if (resized) {
|
|
298
355
|
return {
|
|
299
356
|
type: "image",
|
|
@@ -313,7 +370,7 @@ async function imageAttachmentFromBuffer(buffer, mimeType, { filename = "Pasted
|
|
|
313
370
|
}
|
|
314
371
|
return { type: "image", content: data, mediaType: mimeType || "image/png", filename, sourcePath };
|
|
315
372
|
}
|
|
316
|
-
async function readImageAttachmentFromPath(rawPath, cwd = process.cwd()) {
|
|
373
|
+
async function readImageAttachmentFromPath(rawPath, cwd = process.cwd(), { provider = "" } = {}) {
|
|
317
374
|
const cleaned = cleanPathText(rawPath);
|
|
318
375
|
const mimeType = mimeForPath(cleaned);
|
|
319
376
|
if (!mimeType) return null;
|
|
@@ -328,7 +385,8 @@ async function readImageAttachmentFromPath(rawPath, cwd = process.cwd()) {
|
|
|
328
385
|
const buffer = await readFileAsync(fullPath);
|
|
329
386
|
return imageAttachmentFromBuffer(buffer, mimeType, {
|
|
330
387
|
filename: basename2(fullPath),
|
|
331
|
-
sourcePath: fullPath
|
|
388
|
+
sourcePath: fullPath,
|
|
389
|
+
provider
|
|
332
390
|
});
|
|
333
391
|
}
|
|
334
392
|
async function readClipboardImageToTempFile() {
|
|
@@ -350,19 +408,19 @@ close access fp`;
|
|
|
350
408
|
}
|
|
351
409
|
return null;
|
|
352
410
|
}
|
|
353
|
-
async function readClipboardImageAttachment() {
|
|
411
|
+
async function readClipboardImageAttachment({ provider = "" } = {}) {
|
|
354
412
|
if (process.platform === "linux") {
|
|
355
413
|
const wl = await execFileBuffer("wl-paste", ["--type", "image/png"], { timeout: 3e3 });
|
|
356
|
-
if (wl.ok && wl.stdout?.length) return imageAttachmentFromBuffer(wl.stdout, "image/png");
|
|
414
|
+
if (wl.ok && wl.stdout?.length) return imageAttachmentFromBuffer(wl.stdout, "image/png", { provider });
|
|
357
415
|
const xc = await execFileBuffer("xclip", ["-selection", "clipboard", "-t", "image/png", "-o"], { timeout: 3e3 });
|
|
358
|
-
if (xc.ok && xc.stdout?.length) return imageAttachmentFromBuffer(xc.stdout, "image/png");
|
|
416
|
+
if (xc.ok && xc.stdout?.length) return imageAttachmentFromBuffer(xc.stdout, "image/png", { provider });
|
|
359
417
|
return null;
|
|
360
418
|
}
|
|
361
419
|
const file = await readClipboardImageToTempFile();
|
|
362
420
|
if (!file) return null;
|
|
363
421
|
try {
|
|
364
422
|
const buffer = await readFileAsync(file);
|
|
365
|
-
return await imageAttachmentFromBuffer(buffer, "image/png");
|
|
423
|
+
return await imageAttachmentFromBuffer(buffer, "image/png", { provider });
|
|
366
424
|
} finally {
|
|
367
425
|
try {
|
|
368
426
|
unlinkSync(file);
|
|
@@ -879,7 +937,7 @@ import {
|
|
|
879
937
|
} from "fs/promises";
|
|
880
938
|
import { dirname as dirname4, join as join4, resolve as resolve2 } from "path";
|
|
881
939
|
import { homedir as homedir2 } from "os";
|
|
882
|
-
import { createHash } from "crypto";
|
|
940
|
+
import { createHash as createHash2 } from "crypto";
|
|
883
941
|
function mixdogConfigBaseDir() {
|
|
884
942
|
return process.env.MIXDOG_CONFIG_DIR || join4(homedir2(), ".mixdog");
|
|
885
943
|
}
|
|
@@ -893,7 +951,7 @@ function safeReason(reason) {
|
|
|
893
951
|
return String(reason || "snapshot").replace(/[^a-z0-9_.-]+/gi, "-").slice(0, 48) || "snapshot";
|
|
894
952
|
}
|
|
895
953
|
function initMarkerPath(dataDir) {
|
|
896
|
-
const id =
|
|
954
|
+
const id = createHash2("sha256").update(String(dataDir || "unknown")).digest("hex").slice(0, 16);
|
|
897
955
|
return join4(getBackupRoot(), `.initialized-${id}.json`);
|
|
898
956
|
}
|
|
899
957
|
function isPlainObject(value) {
|
|
@@ -1497,7 +1555,7 @@ import React22 from "react";
|
|
|
1497
1555
|
import { render } from "../../../vendor/ink/build/index.js";
|
|
1498
1556
|
import { closeSync as closeSync2, constants as fsConstants, createWriteStream, mkdirSync as mkdirSync6, openSync as openSync2, readSync } from "node:fs";
|
|
1499
1557
|
import { tmpdir as tmpdir2 } from "node:os";
|
|
1500
|
-
import { dirname as dirname7, join as
|
|
1558
|
+
import { dirname as dirname7, join as join9 } from "node:path";
|
|
1501
1559
|
import { performance as performance3 } from "node:perf_hooks";
|
|
1502
1560
|
import { format } from "node:util";
|
|
1503
1561
|
|
|
@@ -14316,15 +14374,14 @@ function createPromptSubmit({
|
|
|
14316
14374
|
const textRefs = pastedTextReferenceIds(text);
|
|
14317
14375
|
const textSnapshot = Object.fromEntries(Object.entries(pastedTextsRef.current || {}).filter(([id]) => textRefs.has(Number(id))));
|
|
14318
14376
|
const hasTextSnapshot = Object.keys(textSnapshot).length > 0;
|
|
14319
|
-
const
|
|
14320
|
-
const
|
|
14377
|
+
const content = buildPromptContentWithImages(text, imageSnapshot);
|
|
14378
|
+
const imageRestoreMeta = Object.fromEntries(Object.entries(imageSnapshot).map(([id, image]) => {
|
|
14379
|
+
const { content: _content, ...metadata } = image;
|
|
14380
|
+
return [id, { ...metadata, sizeBytes: Math.floor(String(image.content || "").length * 3 / 4) }];
|
|
14381
|
+
}));
|
|
14321
14382
|
const accepted = submitPrompt(content, {
|
|
14322
|
-
|
|
14323
|
-
|
|
14324
|
-
// (pastedTexts entries are cleared on accept). History recall therefore
|
|
14325
|
-
// shows the full original text instead of the token — acceptable.
|
|
14326
|
-
displayText: expandedText,
|
|
14327
|
-
pastedImages: imageSnapshot,
|
|
14383
|
+
...hasImageSnapshot || hasTextSnapshot ? { displayText: text } : {},
|
|
14384
|
+
pastedImages: imageRestoreMeta,
|
|
14328
14385
|
pastedTexts: textSnapshot,
|
|
14329
14386
|
onCommitted: hasImageSnapshot || hasTextSnapshot ? () => {
|
|
14330
14387
|
clearPastedImagesSnapshot(imageSnapshot);
|
|
@@ -22091,6 +22148,10 @@ import { chmodSync, existsSync as existsSync6, mkdirSync as mkdirSync5, readFile
|
|
|
22091
22148
|
init_plugin_paths();
|
|
22092
22149
|
import { dirname as dirname6, join as join6, resolve as resolve3 } from "node:path";
|
|
22093
22150
|
var PROMPT_HISTORY_LIMIT2 = 50;
|
|
22151
|
+
var PROMPT_HISTORY_CACHE_LIMIT = Math.max(
|
|
22152
|
+
8,
|
|
22153
|
+
Number(process.env.MIXDOG_PROMPT_HISTORY_CACHE_LIMIT) || 128
|
|
22154
|
+
);
|
|
22094
22155
|
function promptHistoryKey2(value) {
|
|
22095
22156
|
return String(value || "").trim().replace(/\s+/g, " ");
|
|
22096
22157
|
}
|
|
@@ -22209,7 +22270,11 @@ function usePromptHandlers({
|
|
|
22209
22270
|
return Promise.all(chunks.map(async (chunk) => {
|
|
22210
22271
|
if (!chunk.imagePath) return chunk.text;
|
|
22211
22272
|
try {
|
|
22212
|
-
const image = await readImageAttachmentFromPath(
|
|
22273
|
+
const image = await readImageAttachmentFromPath(
|
|
22274
|
+
chunk.text,
|
|
22275
|
+
state.cwd || process.cwd(),
|
|
22276
|
+
{ provider: state.provider || "" }
|
|
22277
|
+
);
|
|
22213
22278
|
if (!image) return chunk.text;
|
|
22214
22279
|
const ref = registerPastedImage(image);
|
|
22215
22280
|
showPromptHint(`attached ${image.filename || "image"}`, "plain");
|
|
@@ -22242,7 +22307,7 @@ function usePromptHandlers({
|
|
|
22242
22307
|
return readClipboardText().then((clip) => {
|
|
22243
22308
|
const normalized = String(clip ?? "").replace(/\r\n?/g, "\n");
|
|
22244
22309
|
if (normalized) return processText(normalized, true);
|
|
22245
|
-
return readClipboardImageAttachment().then((image) => {
|
|
22310
|
+
return readClipboardImageAttachment({ provider: state.provider || "" }).then((image) => {
|
|
22246
22311
|
if (!image) {
|
|
22247
22312
|
showPromptHint("no text or image found on clipboard", "plain");
|
|
22248
22313
|
return false;
|
|
@@ -22257,7 +22322,7 @@ function usePromptHandlers({
|
|
|
22257
22322
|
});
|
|
22258
22323
|
}
|
|
22259
22324
|
return processText(value);
|
|
22260
|
-
}, [registerPastedImage, registerPastedText, showPromptHint, state.cwd]);
|
|
22325
|
+
}, [registerPastedImage, registerPastedText, showPromptHint, state.cwd, state.provider]);
|
|
22261
22326
|
const handlePromptHistoryNavigate = useCallback12((direction, currentText = "", meta = {}) => {
|
|
22262
22327
|
const currentValue = String(currentText || "");
|
|
22263
22328
|
const currentKey = promptHistoryKey2(currentValue);
|
|
@@ -23401,6 +23466,147 @@ import { performance as performance2 } from "node:perf_hooks";
|
|
|
23401
23466
|
var BOOT_PROFILE_ENABLED = /^(1|true|yes|on)$/i.test(String(process.env.MIXDOG_BOOT_PROFILE || ""));
|
|
23402
23467
|
var BOOT_PROFILE_START = globalThis.__mixdogBootProfileStart || (globalThis.__mixdogBootProfileStart = performance2.now());
|
|
23403
23468
|
|
|
23469
|
+
// src/runtime/attachments/store.mjs
|
|
23470
|
+
init_plugin_paths();
|
|
23471
|
+
import {
|
|
23472
|
+
readdir,
|
|
23473
|
+
readFile as readFileAsync3,
|
|
23474
|
+
stat,
|
|
23475
|
+
unlink
|
|
23476
|
+
} from "node:fs/promises";
|
|
23477
|
+
import { join as join8 } from "node:path";
|
|
23478
|
+
|
|
23479
|
+
// src/runtime/attachments/pdf-extract.mjs
|
|
23480
|
+
var DEFAULT_MAX_OUTPUT_BYTES = 1024 * 1024;
|
|
23481
|
+
|
|
23482
|
+
// src/runtime/attachments/store.mjs
|
|
23483
|
+
var ATTACHMENT_REF_RE = /^[a-f0-9]{64}$/;
|
|
23484
|
+
var MAX_PROMPT_TEXT_BYTES = 1024 * 1024;
|
|
23485
|
+
var MAX_PROMPT_PDF_BYTES = 20 * 1024 * 1024;
|
|
23486
|
+
var ATTACHMENT_CACHE_MAX_BYTES = 64 * 1024 * 1024;
|
|
23487
|
+
var ATTACHMENT_GC_MIN_AGE_MS = Math.max(
|
|
23488
|
+
6e4,
|
|
23489
|
+
Number(process.env.MIXDOG_ATTACHMENT_GC_MIN_AGE_MS) || 7 * 24 * 60 * 60 * 1e3
|
|
23490
|
+
);
|
|
23491
|
+
var ATTACHMENT_GC_INTERVAL_MS = 6 * 60 * 60 * 1e3;
|
|
23492
|
+
var ATTACHMENT_GC_START_DELAY_MS = 5e3;
|
|
23493
|
+
var bufferCache = /* @__PURE__ */ new Map();
|
|
23494
|
+
var bufferCacheBytes = 0;
|
|
23495
|
+
var attachmentGcTimer = null;
|
|
23496
|
+
var lastAttachmentGcAt = 0;
|
|
23497
|
+
function attachmentsDir() {
|
|
23498
|
+
return join8(resolvePluginData(), "prompt-attachments", "sha256");
|
|
23499
|
+
}
|
|
23500
|
+
function dropCachedAttachment(path2) {
|
|
23501
|
+
const cached = bufferCache.get(path2);
|
|
23502
|
+
if (!cached) return;
|
|
23503
|
+
bufferCache.delete(path2);
|
|
23504
|
+
bufferCacheBytes -= cached.length;
|
|
23505
|
+
}
|
|
23506
|
+
async function persistedAttachmentReferencePaths() {
|
|
23507
|
+
const root = resolvePluginData();
|
|
23508
|
+
const paths = [];
|
|
23509
|
+
for (const dirname8 of ["sessions", "turn-checkpoints"]) {
|
|
23510
|
+
const dir = join8(root, dirname8);
|
|
23511
|
+
let entries;
|
|
23512
|
+
try {
|
|
23513
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
23514
|
+
} catch (error) {
|
|
23515
|
+
if (error?.code === "ENOENT") continue;
|
|
23516
|
+
throw error;
|
|
23517
|
+
}
|
|
23518
|
+
for (const entry of entries) {
|
|
23519
|
+
if (entry.isFile() && entry.name.endsWith(".json")) paths.push(join8(dir, entry.name));
|
|
23520
|
+
}
|
|
23521
|
+
}
|
|
23522
|
+
paths.push(join8(root, "session-pending-messages.json"));
|
|
23523
|
+
return paths;
|
|
23524
|
+
}
|
|
23525
|
+
async function collectPromptAttachments({
|
|
23526
|
+
now = Date.now(),
|
|
23527
|
+
minAgeMs = ATTACHMENT_GC_MIN_AGE_MS
|
|
23528
|
+
} = {}) {
|
|
23529
|
+
const referenced = /* @__PURE__ */ new Set();
|
|
23530
|
+
let referenceFiles = 0;
|
|
23531
|
+
for (const path2 of await persistedAttachmentReferencePaths()) {
|
|
23532
|
+
let raw;
|
|
23533
|
+
try {
|
|
23534
|
+
raw = await readFileAsync3(path2, "utf8");
|
|
23535
|
+
} catch (error) {
|
|
23536
|
+
if (error?.code === "ENOENT") continue;
|
|
23537
|
+
return { deleted: 0, referenced: 0, fresh: 0, scanned: 0, incomplete: true };
|
|
23538
|
+
}
|
|
23539
|
+
referenceFiles += 1;
|
|
23540
|
+
const regex = /"attachmentRef"\s*:\s*"([a-f0-9]{64})"/g;
|
|
23541
|
+
let match;
|
|
23542
|
+
while ((match = regex.exec(raw)) !== null) referenced.add(match[1]);
|
|
23543
|
+
}
|
|
23544
|
+
const cutoff = Number(now) - Math.max(0, Number(minAgeMs) || 0);
|
|
23545
|
+
let prefixes;
|
|
23546
|
+
try {
|
|
23547
|
+
prefixes = await readdir(attachmentsDir(), { withFileTypes: true });
|
|
23548
|
+
} catch (error) {
|
|
23549
|
+
if (error?.code === "ENOENT") {
|
|
23550
|
+
return { deleted: 0, referenced: referenced.size, fresh: 0, scanned: 0, referenceFiles };
|
|
23551
|
+
}
|
|
23552
|
+
return { deleted: 0, referenced: referenced.size, fresh: 0, scanned: 0, referenceFiles, incomplete: true };
|
|
23553
|
+
}
|
|
23554
|
+
let deleted = 0;
|
|
23555
|
+
let fresh = 0;
|
|
23556
|
+
let scanned = 0;
|
|
23557
|
+
for (const prefix of prefixes) {
|
|
23558
|
+
if (!prefix.isDirectory() || !/^[a-f0-9]{2}$/.test(prefix.name)) continue;
|
|
23559
|
+
const dir = join8(attachmentsDir(), prefix.name);
|
|
23560
|
+
let entries;
|
|
23561
|
+
try {
|
|
23562
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
23563
|
+
} catch {
|
|
23564
|
+
continue;
|
|
23565
|
+
}
|
|
23566
|
+
for (const entry of entries) {
|
|
23567
|
+
if (!entry.isFile() || !ATTACHMENT_REF_RE.test(entry.name)) continue;
|
|
23568
|
+
scanned += 1;
|
|
23569
|
+
if (referenced.has(entry.name)) continue;
|
|
23570
|
+
const path2 = join8(dir, entry.name);
|
|
23571
|
+
try {
|
|
23572
|
+
const first = await stat(path2);
|
|
23573
|
+
if (first.mtimeMs > cutoff) {
|
|
23574
|
+
fresh += 1;
|
|
23575
|
+
continue;
|
|
23576
|
+
}
|
|
23577
|
+
const latest = await stat(path2);
|
|
23578
|
+
if (latest.mtimeMs > cutoff || latest.mtimeMs !== first.mtimeMs) {
|
|
23579
|
+
fresh += 1;
|
|
23580
|
+
continue;
|
|
23581
|
+
}
|
|
23582
|
+
await unlink(path2);
|
|
23583
|
+
dropCachedAttachment(path2);
|
|
23584
|
+
deleted += 1;
|
|
23585
|
+
} catch (error) {
|
|
23586
|
+
if (error?.code !== "ENOENT") fresh += 1;
|
|
23587
|
+
}
|
|
23588
|
+
}
|
|
23589
|
+
}
|
|
23590
|
+
return { deleted, referenced: referenced.size, fresh, scanned, referenceFiles };
|
|
23591
|
+
}
|
|
23592
|
+
function armAttachmentGc(delayMs) {
|
|
23593
|
+
if (attachmentGcTimer) return;
|
|
23594
|
+
attachmentGcTimer = setTimeout(() => {
|
|
23595
|
+
attachmentGcTimer = null;
|
|
23596
|
+
void collectPromptAttachments().catch(() => void 0).finally(() => {
|
|
23597
|
+
lastAttachmentGcAt = Date.now();
|
|
23598
|
+
armAttachmentGc(ATTACHMENT_GC_INTERVAL_MS);
|
|
23599
|
+
});
|
|
23600
|
+
}, Math.max(ATTACHMENT_GC_START_DELAY_MS, Number(delayMs) || 0));
|
|
23601
|
+
attachmentGcTimer.unref?.();
|
|
23602
|
+
}
|
|
23603
|
+
function scheduleAttachmentGc() {
|
|
23604
|
+
if (attachmentGcTimer) return;
|
|
23605
|
+
const elapsed = Date.now() - lastAttachmentGcAt;
|
|
23606
|
+
armAttachmentGc(lastAttachmentGcAt === 0 ? ATTACHMENT_GC_START_DELAY_MS : ATTACHMENT_GC_INTERVAL_MS - elapsed);
|
|
23607
|
+
}
|
|
23608
|
+
scheduleAttachmentGc();
|
|
23609
|
+
|
|
23404
23610
|
// src/tui/session/agent-job-feed.mjs
|
|
23405
23611
|
init_paste_attachments();
|
|
23406
23612
|
|
|
@@ -23656,7 +23862,7 @@ function scheduleHardExit(code = 0) {
|
|
|
23656
23862
|
timer2.unref?.();
|
|
23657
23863
|
}
|
|
23658
23864
|
function resolveTuiStderrLogPath() {
|
|
23659
|
-
return process.env.MIXDOG_TUI_STDERR_LOG ||
|
|
23865
|
+
return process.env.MIXDOG_TUI_STDERR_LOG || join9(process.env.MIXDOG_RUNTIME_ROOT || join9(tmpdir2(), "mixdog"), "mixdog-tui.stderr.log");
|
|
23660
23866
|
}
|
|
23661
23867
|
function ansiFg(rgb) {
|
|
23662
23868
|
const m = /rgb\((\d+),\s*(\d+),\s*(\d+)\)/.exec(String(rgb || ""));
|
|
@@ -8,6 +8,7 @@ import { basename, extname, isAbsolute, resolve } from 'node:path';
|
|
|
8
8
|
import {
|
|
9
9
|
API_IMAGE_MAX_BASE64_SIZE,
|
|
10
10
|
IMAGE_TARGET_RAW_SIZE,
|
|
11
|
+
imageProfileForProvider,
|
|
11
12
|
imageMetadataText,
|
|
12
13
|
resizeImageBuffer,
|
|
13
14
|
} from '../runtime/agent/orchestrator/tools/builtin/read-image-resize.mjs';
|
|
@@ -148,10 +149,14 @@ export function splitPastedImagePathCandidates(text) {
|
|
|
148
149
|
// Exported for the desktop engine capability surface (resizeImage): the GUI
|
|
149
150
|
// composer routes its attachments through this SAME pipeline so desktop and
|
|
150
151
|
// TUI submit byte-identical image payloads.
|
|
151
|
-
export async function imageAttachmentFromBuffer(buffer, mimeType, {
|
|
152
|
+
export async function imageAttachmentFromBuffer(buffer, mimeType, {
|
|
153
|
+
filename = 'Pasted image',
|
|
154
|
+
sourcePath = '',
|
|
155
|
+
provider = '',
|
|
156
|
+
} = {}) {
|
|
152
157
|
if (!Buffer.isBuffer(buffer) || buffer.length === 0) throw new Error('image is empty');
|
|
153
158
|
const ext = (mimeType || 'image/png').split('/')[1] || 'png';
|
|
154
|
-
const resized = await resizeImageBuffer(buffer, ext);
|
|
159
|
+
const resized = await resizeImageBuffer(buffer, ext, { profile: imageProfileForProvider(provider) });
|
|
155
160
|
if (resized) {
|
|
156
161
|
return {
|
|
157
162
|
type: 'image',
|
|
@@ -172,7 +177,7 @@ export async function imageAttachmentFromBuffer(buffer, mimeType, { filename = '
|
|
|
172
177
|
return { type: 'image', content: data, mediaType: mimeType || 'image/png', filename, sourcePath };
|
|
173
178
|
}
|
|
174
179
|
|
|
175
|
-
export async function readImageAttachmentFromPath(rawPath, cwd = process.cwd()) {
|
|
180
|
+
export async function readImageAttachmentFromPath(rawPath, cwd = process.cwd(), { provider = '' } = {}) {
|
|
176
181
|
const cleaned = cleanPathText(rawPath);
|
|
177
182
|
const mimeType = mimeForPath(cleaned);
|
|
178
183
|
if (!mimeType) return null;
|
|
@@ -190,6 +195,7 @@ export async function readImageAttachmentFromPath(rawPath, cwd = process.cwd())
|
|
|
190
195
|
return imageAttachmentFromBuffer(buffer, mimeType, {
|
|
191
196
|
filename: basename(fullPath),
|
|
192
197
|
sourcePath: fullPath,
|
|
198
|
+
provider,
|
|
193
199
|
});
|
|
194
200
|
}
|
|
195
201
|
|
|
@@ -210,19 +216,19 @@ async function readClipboardImageToTempFile() {
|
|
|
210
216
|
return null;
|
|
211
217
|
}
|
|
212
218
|
|
|
213
|
-
export async function readClipboardImageAttachment() {
|
|
219
|
+
export async function readClipboardImageAttachment({ provider = '' } = {}) {
|
|
214
220
|
if (process.platform === 'linux') {
|
|
215
221
|
const wl = await execFileBuffer('wl-paste', ['--type', 'image/png'], { timeout: 3000 });
|
|
216
|
-
if (wl.ok && wl.stdout?.length) return imageAttachmentFromBuffer(wl.stdout, 'image/png');
|
|
222
|
+
if (wl.ok && wl.stdout?.length) return imageAttachmentFromBuffer(wl.stdout, 'image/png', { provider });
|
|
217
223
|
const xc = await execFileBuffer('xclip', ['-selection', 'clipboard', '-t', 'image/png', '-o'], { timeout: 3000 });
|
|
218
|
-
if (xc.ok && xc.stdout?.length) return imageAttachmentFromBuffer(xc.stdout, 'image/png');
|
|
224
|
+
if (xc.ok && xc.stdout?.length) return imageAttachmentFromBuffer(xc.stdout, 'image/png', { provider });
|
|
219
225
|
return null;
|
|
220
226
|
}
|
|
221
227
|
const file = await readClipboardImageToTempFile();
|
|
222
228
|
if (!file) return null;
|
|
223
229
|
try {
|
|
224
230
|
const buffer = await readFileAsync(file);
|
|
225
|
-
return await imageAttachmentFromBuffer(buffer, 'image/png');
|
|
231
|
+
return await imageAttachmentFromBuffer(buffer, 'image/png', { provider });
|
|
226
232
|
} finally {
|
|
227
233
|
try { unlinkSync(file); } catch {}
|
|
228
234
|
}
|
|
@@ -9,6 +9,10 @@ import { dirname, join, resolve } from 'node:path';
|
|
|
9
9
|
import { resolvePluginData } from '../runtime/shared/plugin-paths.mjs';
|
|
10
10
|
|
|
11
11
|
export const PROMPT_HISTORY_LIMIT = 50;
|
|
12
|
+
export const PROMPT_HISTORY_CACHE_LIMIT = Math.max(
|
|
13
|
+
8,
|
|
14
|
+
Number(process.env.MIXDOG_PROMPT_HISTORY_CACHE_LIMIT) || 128,
|
|
15
|
+
);
|
|
12
16
|
|
|
13
17
|
export function promptHistoryKey(value) {
|
|
14
18
|
return String(value || '').trim().replace(/\s+/g, ' ');
|
|
@@ -101,10 +105,43 @@ const memCache = new Map(); // filePath -> entries[] (optimistic in-memory view)
|
|
|
101
105
|
const pendingAppends = new Map(); // filePath -> string[] appends since last flush
|
|
102
106
|
const pendingTimers = new Map(); // filePath -> timeout handle
|
|
103
107
|
|
|
108
|
+
function flushPendingSyncForEviction(filePath) {
|
|
109
|
+
const timer = pendingTimers.get(filePath);
|
|
110
|
+
if (timer) clearTimeout(timer);
|
|
111
|
+
pendingTimers.delete(filePath);
|
|
112
|
+
const pend = pendingAppends.get(filePath);
|
|
113
|
+
pendingAppends.delete(filePath);
|
|
114
|
+
if (!pend?.length) return;
|
|
115
|
+
const merged = reconcileWithDisk(filePath, pend);
|
|
116
|
+
writeEntriesSync(filePath, merged);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function trimMemCache() {
|
|
120
|
+
while (memCache.size > PROMPT_HISTORY_CACHE_LIMIT) {
|
|
121
|
+
const oldest = memCache.keys().next().value;
|
|
122
|
+
if (oldest === undefined) break;
|
|
123
|
+
// Never drop the only optimistic copy of an unflushed prompt. Cwd churn is
|
|
124
|
+
// rare; once the cache is full, synchronously landing the oldest dirty
|
|
125
|
+
// bucket gives all three maps a deterministic process-lifetime bound.
|
|
126
|
+
flushPendingSyncForEviction(oldest);
|
|
127
|
+
memCache.delete(oldest);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function rememberCachedEntries(filePath, entries) {
|
|
132
|
+
memCache.delete(filePath);
|
|
133
|
+
memCache.set(filePath, entries);
|
|
134
|
+
trimMemCache();
|
|
135
|
+
}
|
|
136
|
+
|
|
104
137
|
function cachedEntries(filePath) {
|
|
105
|
-
if (memCache.has(filePath))
|
|
138
|
+
if (memCache.has(filePath)) {
|
|
139
|
+
const entries = memCache.get(filePath);
|
|
140
|
+
rememberCachedEntries(filePath, entries);
|
|
141
|
+
return entries;
|
|
142
|
+
}
|
|
106
143
|
const entries = readEntries(filePath);
|
|
107
|
-
|
|
144
|
+
rememberCachedEntries(filePath, entries);
|
|
108
145
|
return entries;
|
|
109
146
|
}
|
|
110
147
|
|
|
@@ -141,13 +178,15 @@ async function writeBehindFlush(filePath) {
|
|
|
141
178
|
// and reconcile against the file we are about to write.
|
|
142
179
|
pendingAppends.set(filePath, []);
|
|
143
180
|
const merged = reconcileWithDisk(filePath, pend);
|
|
144
|
-
|
|
181
|
+
rememberCachedEntries(filePath, merged);
|
|
145
182
|
const ok = await writeEntriesAsync(filePath, merged);
|
|
146
183
|
if (!ok) {
|
|
147
184
|
// Retry: re-queue our appends ahead of any newer ones (oldest → newest).
|
|
148
185
|
const cur = pendingAppends.get(filePath) || [];
|
|
149
186
|
pendingAppends.set(filePath, pend.concat(cur));
|
|
150
187
|
scheduleWriteBehind(filePath);
|
|
188
|
+
} else if (!(pendingAppends.get(filePath)?.length)) {
|
|
189
|
+
pendingAppends.delete(filePath);
|
|
151
190
|
}
|
|
152
191
|
}
|
|
153
192
|
|
|
@@ -229,14 +268,23 @@ export function appendPromptHistory(cwd, value) {
|
|
|
229
268
|
if (!filePath) return null;
|
|
230
269
|
// Optimistic in-memory update for immediate return/reads.
|
|
231
270
|
const trimmed = applyAppend(cachedEntries(filePath), value);
|
|
232
|
-
memCache.set(filePath, trimmed);
|
|
233
271
|
// Queue the append for a durable, disk-reconciled flush (async, coalesced).
|
|
234
272
|
const pend = pendingAppends.get(filePath) || [];
|
|
235
273
|
pend.push(String(value).trim());
|
|
236
274
|
pendingAppends.set(filePath, pend);
|
|
275
|
+
rememberCachedEntries(filePath, trimmed);
|
|
237
276
|
// Bound crash-loss to FLUSH_CAP appends; otherwise coalesce over the window.
|
|
238
277
|
if (pend.length >= FLUSH_CAP) void writeBehindFlush(filePath);
|
|
239
278
|
else scheduleWriteBehind(filePath);
|
|
240
279
|
return trimmed;
|
|
241
280
|
}
|
|
242
281
|
|
|
282
|
+
export function promptHistoryStoreStatsForTest() {
|
|
283
|
+
return {
|
|
284
|
+
cacheEntries: memCache.size,
|
|
285
|
+
pendingEntries: pendingAppends.size,
|
|
286
|
+
timerEntries: pendingTimers.size,
|
|
287
|
+
cacheLimit: PROMPT_HISTORY_CACHE_LIMIT,
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
|
|
@@ -395,7 +395,11 @@ export function createAgentJobFeed({
|
|
|
395
395
|
if (modelContent) parts.push({ type: 'text', text: modelContent });
|
|
396
396
|
for (const p of imagePaths) {
|
|
397
397
|
let att = null;
|
|
398
|
-
try {
|
|
398
|
+
try {
|
|
399
|
+
att = await readImageAttachmentFromPath(p, process.cwd(), {
|
|
400
|
+
provider: getState()?.provider || '',
|
|
401
|
+
});
|
|
402
|
+
} catch { att = null; }
|
|
399
403
|
if (!att) continue;
|
|
400
404
|
if (att.metadataText) parts.push({ type: 'text', text: att.metadataText });
|
|
401
405
|
parts.push({ type: 'image', data: att.content, mimeType: att.mediaType || 'image/png' });
|