mirror of
https://github.com/samsonjs/Peekaboo.git
synced 2026-04-27 15:07:41 +00:00
Improve analyze description for multiple windows
This commit is contained in:
parent
06cf4f144e
commit
b3ec918363
2 changed files with 148 additions and 3 deletions
|
|
@ -92,6 +92,26 @@ export async function imageToolHandler(
|
||||||
analysisAttempted = true;
|
analysisAttempted = true;
|
||||||
const analysisResults: Array<{ label: string; text: string }> = [];
|
const analysisResults: Array<{ label: string; text: string }> = [];
|
||||||
|
|
||||||
|
// Helper function to generate descriptive labels for analysis
|
||||||
|
const getAnalysisLabel = (savedFile: SavedFile, isMultipleFiles: boolean): string => {
|
||||||
|
if (!isMultipleFiles) {
|
||||||
|
// For single files, use the item_label (app name or screen description)
|
||||||
|
return savedFile.item_label || "Unknown";
|
||||||
|
}
|
||||||
|
|
||||||
|
// For multiple files, prefer window_title if available
|
||||||
|
if (savedFile.window_title) {
|
||||||
|
return `"${savedFile.window_title}"`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fall back to item_label with window index if available
|
||||||
|
if (savedFile.window_index !== undefined) {
|
||||||
|
return `${savedFile.item_label || "Unknown"} (Window ${savedFile.window_index + 1})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return savedFile.item_label || "Unknown";
|
||||||
|
};
|
||||||
|
|
||||||
const configuredProviders = parseAIProviders(
|
const configuredProviders = parseAIProviders(
|
||||||
process.env.PEEKABOO_AI_PROVIDERS || "",
|
process.env.PEEKABOO_AI_PROVIDERS || "",
|
||||||
);
|
);
|
||||||
|
|
@ -101,7 +121,10 @@ export async function imageToolHandler(
|
||||||
logger.warn(analysisText);
|
logger.warn(analysisText);
|
||||||
} else {
|
} else {
|
||||||
// Iterate through all saved files for analysis
|
// Iterate through all saved files for analysis
|
||||||
|
const isMultipleFiles = captureData.saved_files.length > 1;
|
||||||
for (const savedFile of captureData.saved_files) {
|
for (const savedFile of captureData.saved_files) {
|
||||||
|
const analysisLabel = getAnalysisLabel(savedFile, isMultipleFiles);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const imageBase64 = await readImageAsBase64(savedFile.path);
|
const imageBase64 = await readImageAsBase64(savedFile.path);
|
||||||
logger.debug({ path: savedFile.path }, "Image read successfully for analysis.");
|
logger.debug({ path: savedFile.path }, "Image read successfully for analysis.");
|
||||||
|
|
@ -115,12 +138,12 @@ export async function imageToolHandler(
|
||||||
|
|
||||||
if (analysisResult.error) {
|
if (analysisResult.error) {
|
||||||
analysisResults.push({
|
analysisResults.push({
|
||||||
label: savedFile.item_label || "Unknown",
|
label: analysisLabel,
|
||||||
text: analysisResult.error,
|
text: analysisResult.error,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
analysisResults.push({
|
analysisResults.push({
|
||||||
label: savedFile.item_label || "Unknown",
|
label: analysisLabel,
|
||||||
text: analysisResult.analysisText || "",
|
text: analysisResult.analysisText || "",
|
||||||
});
|
});
|
||||||
modelUsed = analysisResult.modelUsed;
|
modelUsed = analysisResult.modelUsed;
|
||||||
|
|
@ -133,7 +156,7 @@ export async function imageToolHandler(
|
||||||
"Failed to read captured image for analysis",
|
"Failed to read captured image for analysis",
|
||||||
);
|
);
|
||||||
analysisResults.push({
|
analysisResults.push({
|
||||||
label: savedFile.item_label || "Unknown",
|
label: analysisLabel,
|
||||||
text: `Analysis skipped: Failed to read captured image at ${savedFile.path}. Error: ${readError instanceof Error ? readError.message : "Unknown read error"}`,
|
text: `Analysis skipped: Failed to read captured image at ${savedFile.path}. Error: ${readError instanceof Error ? readError.message : "Unknown read error"}`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -741,6 +741,128 @@ describe("Image Tool", () => {
|
||||||
// Verify that the temporary directory is no longer cleaned up (files preserved)
|
// Verify that the temporary directory is no longer cleaned up (files preserved)
|
||||||
expect(mockFsRm).not.toHaveBeenCalled();
|
expect(mockFsRm).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should use window titles for analysis labels when capturing multiple windows", async () => {
|
||||||
|
// Mock resolveImagePath to return a temporary directory path
|
||||||
|
mockResolveImagePath.mockResolvedValue({
|
||||||
|
effectivePath: MOCK_TEMP_IMAGE_DIR,
|
||||||
|
tempDirUsed: MOCK_TEMP_IMAGE_DIR,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Mock executeSwiftCli with two saved files that have window titles
|
||||||
|
const mockFile1: SavedFile = {
|
||||||
|
path: "/tmp/peekaboo-img-XXXXXX/chrome_window1.png",
|
||||||
|
mime_type: "image/png",
|
||||||
|
item_label: "Google Chrome",
|
||||||
|
window_title: "MCP Inspector",
|
||||||
|
window_index: 0,
|
||||||
|
window_id: 123,
|
||||||
|
};
|
||||||
|
const mockFile2: SavedFile = {
|
||||||
|
path: "/tmp/peekaboo-img-XXXXXX/chrome_window2.png",
|
||||||
|
mime_type: "image/png",
|
||||||
|
item_label: "Google Chrome",
|
||||||
|
window_title: "(9) Home / X",
|
||||||
|
window_index: 1,
|
||||||
|
window_id: 124,
|
||||||
|
};
|
||||||
|
const mockResponse = {
|
||||||
|
success: true,
|
||||||
|
data: { saved_files: [mockFile1, mockFile2] },
|
||||||
|
messages: ["Captured 2 Chrome windows"],
|
||||||
|
};
|
||||||
|
mockExecuteSwiftCli.mockResolvedValue(mockResponse);
|
||||||
|
|
||||||
|
// Mock readImageAsBase64 to return different base64 strings
|
||||||
|
mockReadImageAsBase64
|
||||||
|
.mockResolvedValueOnce("base64dataforwindow1")
|
||||||
|
.mockResolvedValueOnce("base64dataforwindow2");
|
||||||
|
|
||||||
|
// Mock performAutomaticAnalysis to return different analysis for each call
|
||||||
|
mockPerformAutomaticAnalysis
|
||||||
|
.mockResolvedValueOnce({
|
||||||
|
analysisText: "This shows the MCP Inspector interface.",
|
||||||
|
modelUsed: MOCK_MODEL_USED,
|
||||||
|
})
|
||||||
|
.mockResolvedValueOnce({
|
||||||
|
analysisText: "This shows the X (Twitter) home page.",
|
||||||
|
modelUsed: MOCK_MODEL_USED,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Call imageToolHandler with a question
|
||||||
|
const result = await imageToolHandler(
|
||||||
|
{ question: "What is shown in each window?" },
|
||||||
|
mockContext,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Verify the final analysis_text uses window titles instead of app names
|
||||||
|
expect(result.analysis_text).toBe(
|
||||||
|
'Analysis for "MCP Inspector":\nThis shows the MCP Inspector interface.\n\nAnalysis for "(9) Home / X":\nThis shows the X (Twitter) home page.'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Verify that the temporary directory is no longer cleaned up (files preserved)
|
||||||
|
expect(mockFsRm).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should fallback to window index when no window title is available", async () => {
|
||||||
|
// Mock resolveImagePath to return a temporary directory path
|
||||||
|
mockResolveImagePath.mockResolvedValue({
|
||||||
|
effectivePath: MOCK_TEMP_IMAGE_DIR,
|
||||||
|
tempDirUsed: MOCK_TEMP_IMAGE_DIR,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Mock executeSwiftCli with two saved files without window titles
|
||||||
|
const mockFile1: SavedFile = {
|
||||||
|
path: "/tmp/peekaboo-img-XXXXXX/app_window1.png",
|
||||||
|
mime_type: "image/png",
|
||||||
|
item_label: "Some App",
|
||||||
|
window_index: 0,
|
||||||
|
window_id: 123,
|
||||||
|
};
|
||||||
|
const mockFile2: SavedFile = {
|
||||||
|
path: "/tmp/peekaboo-img-XXXXXX/app_window2.png",
|
||||||
|
mime_type: "image/png",
|
||||||
|
item_label: "Some App",
|
||||||
|
window_index: 1,
|
||||||
|
window_id: 124,
|
||||||
|
};
|
||||||
|
const mockResponse = {
|
||||||
|
success: true,
|
||||||
|
data: { saved_files: [mockFile1, mockFile2] },
|
||||||
|
messages: ["Captured 2 app windows"],
|
||||||
|
};
|
||||||
|
mockExecuteSwiftCli.mockResolvedValue(mockResponse);
|
||||||
|
|
||||||
|
// Mock readImageAsBase64 to return different base64 strings
|
||||||
|
mockReadImageAsBase64
|
||||||
|
.mockResolvedValueOnce("base64dataforwindow1")
|
||||||
|
.mockResolvedValueOnce("base64dataforwindow2");
|
||||||
|
|
||||||
|
// Mock performAutomaticAnalysis to return different analysis for each call
|
||||||
|
mockPerformAutomaticAnalysis
|
||||||
|
.mockResolvedValueOnce({
|
||||||
|
analysisText: "Analysis for first window.",
|
||||||
|
modelUsed: MOCK_MODEL_USED,
|
||||||
|
})
|
||||||
|
.mockResolvedValueOnce({
|
||||||
|
analysisText: "Analysis for second window.",
|
||||||
|
modelUsed: MOCK_MODEL_USED,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Call imageToolHandler with a question
|
||||||
|
const result = await imageToolHandler(
|
||||||
|
{ question: "What is shown in each window?" },
|
||||||
|
mockContext,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Verify the final analysis_text uses window index fallback
|
||||||
|
expect(result.analysis_text).toBe(
|
||||||
|
"Analysis for Some App (Window 1):\nAnalysis for first window.\n\nAnalysis for Some App (Window 2):\nAnalysis for second window."
|
||||||
|
);
|
||||||
|
|
||||||
|
// Verify that the temporary directory is no longer cleaned up (files preserved)
|
||||||
|
expect(mockFsRm).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("buildSwiftCliArgs", () => {
|
describe("buildSwiftCliArgs", () => {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue