Native images (#6619)

Co-authored-by: Douwe Osinga <douwe@squareup.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Douwe Osinga
2026-01-22 18:53:57 -05:00
committed by GitHub
parent 4578c77576
commit f8faf9620a
23 changed files with 398 additions and 602 deletions
+26
View File
@@ -22,6 +22,32 @@ export function errorMessage(err: Error | unknown, default_value?: string) {
}
}
export async function compressImageDataUrl(dataUrl: string): Promise<string> {
return new Promise((resolve, reject) => {
const img = new globalThis.Image();
img.onload = () => {
const maxDim = 1024;
const scale = Math.min(1, maxDim / Math.max(img.width, img.height));
const width = Math.floor(img.width * scale);
const height = Math.floor(img.height * scale);
const canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext('2d');
if (!ctx) {
reject(new Error('Failed to get canvas context'));
return;
}
ctx.drawImage(img, 0, 0, width, height);
resolve(canvas.toDataURL('image/jpeg', 0.85));
};
img.onerror = () => reject(new Error('Failed to load image'));
img.src = dataUrl;
});
}
export function formatAppName(name: string): string {
return name
.split(/[-_\s]+/)
-59
View File
@@ -1,59 +0,0 @@
/**
* Utility functions for detecting and handling image paths in messages
*/
/**
* Extracts image file paths from a message text
* Looks for paths that match the pattern of pasted images from the temp directory
*
* @param text The message text to extract image paths from
* @returns An array of image file paths found in the message
*/
export function extractImagePaths(text: string): string[] {
if (!text) return [];
// Match paths that look like pasted image paths from the temp directory
// Pattern: /path/to/goose-pasted-images/pasted-img-TIMESTAMP-RANDOM.ext
// This regex looks for:
// - Word boundary or start of string
// - A path containing "goose-pasted-images"
// - Followed by a filename starting with "pasted-"
// - Ending with common image extensions
// - Word boundary or end of string
const regex =
/(?:^|\s)((?:[^\s]*\/)?goose-pasted-images\/pasted-[^\s]+\.(png|jpg|jpeg|gif|webp))(?=\s|$)/gi;
const matches = [];
let match;
while ((match = regex.exec(text)) !== null) {
matches.push(match[1]);
}
return matches;
}
/**
* Removes image paths from the text
*
* @param text The original text
* @param imagePaths Array of image paths to remove
* @returns Text with image paths removed
*/
export function removeImagePathsFromText(text: string, imagePaths: string[]): string {
if (!text || imagePaths.length === 0) return text;
let result = text;
// Remove each image path from the text
imagePaths.forEach((path) => {
// Escape special regex characters in the path
const escapedPath = path.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
// Create a regex that matches the path with optional surrounding whitespace
const pathRegex = new RegExp(`(^|\\s)${escapedPath}(?=\\s|$)`, 'g');
result = result.replace(pathRegex, '$1');
});
// Clean up any extra whitespace
return result.replace(/\s+/g, ' ').trim();
}
+3 -2
View File
@@ -1,5 +1,6 @@
import { NavigateFunction } from 'react-router-dom';
import { Recipe } from '../api/types.gen';
import { Recipe } from '../api';
import { UserInput } from '../types/message';
export type View =
| 'welcome'
@@ -29,7 +30,7 @@ export type ViewOptions = {
parentView?: View;
parentViewOptions?: ViewOptions;
disableAnimation?: boolean;
initialMessage?: string;
initialMessage?: UserInput;
shareToken?: string;
resumeSessionId?: string;
pendingScheduleDeepLink?: string;
+2 -15
View File
@@ -1,4 +1,4 @@
import { getToolRequests, getTextContent, getToolResponses } from '../types/message';
import { getToolRequests, getTextAndImageContent, getToolResponses } from '../types/message';
import { Message } from '../api';
export function identifyConsecutiveToolCalls(messages: Message[]): number[][] {
@@ -9,7 +9,7 @@ export function identifyConsecutiveToolCalls(messages: Message[]): number[][] {
const message = messages[i];
const toolRequests = getToolRequests(message);
const toolResponses = getToolResponses(message);
const textContent = getTextContent(message);
const { textContent } = getTextAndImageContent(message);
const hasText = textContent.trim().length > 0;
if (toolResponses.length > 0 && toolRequests.length === 0) {
@@ -47,15 +47,6 @@ export function identifyConsecutiveToolCalls(messages: Message[]): number[][] {
return chains;
}
export function shouldHideMessage(messageIndex: number, chains: number[][]): boolean {
for (const chain of chains) {
if (chain.includes(messageIndex)) {
return chain[0] !== messageIndex;
}
}
return false;
}
export function shouldHideTimestamp(messageIndex: number, chains: number[][]): boolean {
for (const chain of chains) {
if (chain.includes(messageIndex)) {
@@ -69,7 +60,3 @@ export function shouldHideTimestamp(messageIndex: number, chains: number[][]): b
export function isInChain(messageIndex: number, chains: number[][]): boolean {
return chains.some((chain) => chain.includes(messageIndex));
}
export function getChainForMessage(messageIndex: number, chains: number[][]): number[] | null {
return chains.find((chain) => chain.includes(messageIndex)) || null;
}