Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,968 changes: 1,053 additions & 915 deletions ui/desktop/package-lock.json

Large diffs are not rendered by default.

4 changes: 3 additions & 1 deletion ui/desktop/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
"test-e2e:report": "playwright show-report",
"test-e2e:single": "npm run generate-api && playwright test -g",
"lint": "eslint \"src/**/*.{ts,tsx}\" --fix --no-warn-ignored",
"lint:check": "eslint \"src/**/*.{ts,tsx}\" --max-warnings 0 --no-warn-ignored",
"lint:check": "npm run typecheck && eslint \"src/**/*.{ts,tsx}\" --max-warnings 0 --no-warn-ignored",
"format": "prettier --write \"src/**/*.{ts,tsx,css,json}\"",
"format:check": "prettier --check \"src/**/*.{ts,tsx,css,json}\"",
"prepare": "cd ../.. && husky install",
Expand Down Expand Up @@ -67,12 +67,14 @@
"postcss": "^8.4.47",
"prettier": "^3.4.2",
"tailwindcss": "^3.4.14",
"typescript": "~5.5.0",
"vite": "^6.3.4"
},
"keywords": [],
"license": "Apache-2.0",
"lint-staged": {
"src/**/*.{ts,tsx}": [
"bash -c 'npm run typecheck'",
"eslint --fix --max-warnings 0 --no-warn-ignored",
"prettier --write"
],
Expand Down
49 changes: 33 additions & 16 deletions ui/desktop/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { useEffect, useRef, useState } from 'react';
import { IpcRendererEvent } from 'electron';
import { openSharedSessionFromDeepLink } from './sessionLinks';
import { openSharedSessionFromDeepLink, type SessionLinksViewOptions } from './sessionLinks';
import { type SharedSessionDetails } from './sharedSessions';
import { initializeSystem } from './utils/providerUtils';
import { ErrorUI } from './components/ErrorBoundary';
import { ConfirmationModal } from './components/ui/ConfirmationModal';
Expand All @@ -9,6 +10,7 @@ import { toastService } from './toasts';
import { extractExtensionName } from './components/settings/extensions/utils';
import { GoosehintsModal } from './components/GoosehintsModal';
import { type ExtensionConfig } from './extensions';
import { type Recipe } from './recipe';

import ChatView from './components/ChatView';
import SuspenseLoader from './suspense-loader';
Expand Down Expand Up @@ -52,20 +54,20 @@ export type ViewOptions = {
extensionId?: string;
showEnvVars?: boolean;
deepLinkConfig?: ExtensionConfig;
// Session view options

// Session view options
resumedSession?: SessionDetails;
sessionDetails?: SessionDetails;
error?: string;
shareToken?: string;
baseUrl?: string;

// Recipe editor options
config?: unknown;

// Permission view options
parentView?: View;

// Generic options
[key: string]: unknown;
};
Expand Down Expand Up @@ -237,12 +239,18 @@ export default function App() {
}, []);

useEffect(() => {
const handleOpenSharedSession = async (_event: IpcRendererEvent, link: string) => {
const handleOpenSharedSession = async (_event: IpcRendererEvent, ...args: unknown[]) => {
const link = args[0] as string;
window.electron.logInfo(`Opening shared session from deep link ${link}`);
setIsLoadingSharedSession(true);
setSharedSessionError(null);
try {
await openSharedSessionFromDeepLink(link, setView);
await openSharedSessionFromDeepLink(
link,
(view: View, options?: SessionLinksViewOptions) => {
setView(view, options as ViewOptions);
}
);
} catch (error) {
console.error('Unexpected error opening shared session:', error);
setView('sessions');
Expand Down Expand Up @@ -279,7 +287,8 @@ export default function App() {

useEffect(() => {
console.log('Setting up fatal error handler');
const handleFatalError = (_event: IpcRendererEvent, errorMessage: string) => {
const handleFatalError = (_event: IpcRendererEvent, ...args: unknown[]) => {
const errorMessage = args[0] as string;
console.error('Encountered a fatal error: ', errorMessage);
console.error('Current view:', view);
console.error('Is loading session:', isLoadingSession);
Expand All @@ -293,7 +302,8 @@ export default function App() {

useEffect(() => {
console.log('Setting up view change handler');
const handleSetView = (_event: IpcRendererEvent, newView: View) => {
const handleSetView = (_event: IpcRendererEvent, ...args: unknown[]) => {
const newView = args[0] as View;
console.log(`Received view change request to: ${newView}`);
setView(newView);
};
Expand Down Expand Up @@ -328,7 +338,8 @@ export default function App() {

useEffect(() => {
console.log('Setting up extension handler');
const handleAddExtension = async (_event: IpcRendererEvent, link: string) => {
const handleAddExtension = async (_event: IpcRendererEvent, ...args: unknown[]) => {
const link = args[0] as string;
try {
console.log(`Received add-extension event with link: ${link}`);
const command = extractCommand(link);
Expand Down Expand Up @@ -401,7 +412,7 @@ export default function App() {
}, [STRICT_ALLOWLIST]);

useEffect(() => {
const handleFocusInput = (_event: IpcRendererEvent) => {
const handleFocusInput = (_event: IpcRendererEvent, ..._args: unknown[]) => {
const inputField = document.querySelector('input[type="text"], textarea') as HTMLInputElement;
if (inputField) {
inputField.focus();
Expand All @@ -418,7 +429,9 @@ export default function App() {
console.log(`Confirming installation of extension from: ${pendingLink}`);
setModalVisible(false);
try {
await addExtensionFromDeepLinkV2(pendingLink, addExtension, setView);
await addExtensionFromDeepLinkV2(pendingLink, addExtension, (view: string, options) => {
setView(view as View, options as ViewOptions);
});
console.log('Extension installation successful');
} catch (error) {
console.error('Failed to add extension:', error);
Expand Down Expand Up @@ -522,7 +535,9 @@ export default function App() {
{view === 'schedules' && <SchedulesView onClose={() => setView('chat')} />}
{view === 'sharedSession' && (
<SharedSessionView
session={viewOptions?.sessionDetails}
session={
(viewOptions?.sessionDetails as unknown as SharedSessionDetails | null) || null
}
isLoading={isLoadingSharedSession}
error={viewOptions?.error || sharedSessionError}
onBack={() => setView('sessions')}
Expand All @@ -532,7 +547,9 @@ export default function App() {
try {
await openSharedSessionFromDeepLink(
`goose://sessions/${viewOptions.shareToken}`,
setView,
(view: View, options?: SessionLinksViewOptions) => {
setView(view, options as ViewOptions);
},
viewOptions.baseUrl
);
} catch (error) {
Expand All @@ -546,7 +563,7 @@ export default function App() {
)}
{view === 'recipeEditor' && (
<RecipeEditor
config={viewOptions?.config || window.electron.getConfig().recipeConfig}
config={(viewOptions?.config as Recipe) || window.electron.getConfig().recipeConfig}
/>
)}
{view === 'permission' && (
Expand Down
1 change: 0 additions & 1 deletion ui/desktop/src/components/AgentHeader.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@

interface AgentHeaderProps {
title: string;
profileInfo?: string;
Expand Down
62 changes: 31 additions & 31 deletions ui/desktop/src/components/ChatInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -94,11 +94,7 @@ export default function ChatInput({

// Set the image to loading state
setPastedImages((prev) =>
prev.map((img) =>
img.id === imageId
? { ...img, isLoading: true, error: undefined }
: img
)
prev.map((img) => (img.id === imageId ? { ...img, isLoading: true, error: undefined } : img))
);

try {
Expand Down Expand Up @@ -149,19 +145,21 @@ export default function ChatInput({

// Debounced function to update actual value
const debouncedSetValue = useMemo(
() => debounce((value: string) => {
setValue(value);
}, 150),
() =>
debounce((value: string) => {
setValue(value);
}, 150),
[setValue]
);

// Debounced autosize function
const debouncedAutosize = useMemo(
() => debounce((element: HTMLTextAreaElement) => {
element.style.height = '0px'; // Reset height
const scrollHeight = element.scrollHeight;
element.style.height = Math.min(scrollHeight, maxHeight) + 'px';
}, 150),
() =>
debounce((element: HTMLTextAreaElement) => {
element.style.height = '0px'; // Reset height
const scrollHeight = element.scrollHeight;
element.style.height = Math.min(scrollHeight, maxHeight) + 'px';
}, 150),
[maxHeight]
);

Expand All @@ -179,10 +177,10 @@ export default function ChatInput({

const handlePaste = async (evt: React.ClipboardEvent<HTMLTextAreaElement>) => {
const files = Array.from(evt.clipboardData.files || []);
const imageFiles = files.filter(file => file.type.startsWith('image/'));
const imageFiles = files.filter((file) => file.type.startsWith('image/'));

if (imageFiles.length === 0) return;

// Check if adding these images would exceed the limit
if (pastedImages.length + imageFiles.length > MAX_IMAGES_PER_MESSAGE) {
// Show error message to user
Expand All @@ -192,20 +190,20 @@ export default function ChatInput({
id: `error-${Date.now()}`,
dataUrl: '',
isLoading: false,
error: `Cannot paste ${imageFiles.length} image(s). Maximum ${MAX_IMAGES_PER_MESSAGE} images per message allowed.`
}
error: `Cannot paste ${imageFiles.length} image(s). Maximum ${MAX_IMAGES_PER_MESSAGE} images per message allowed.`,
},
]);

// Remove the error message after 3 seconds
setTimeout(() => {
setPastedImages((prev) => prev.filter(img => !img.id.startsWith('error-')));
setPastedImages((prev) => prev.filter((img) => !img.id.startsWith('error-')));
}, 3000);

return;
}

evt.preventDefault();

for (const file of imageFiles) {
// Check individual file size before processing
if (file.size > MAX_IMAGE_SIZE_MB * 1024 * 1024) {
Expand All @@ -216,18 +214,18 @@ export default function ChatInput({
id: errorId,
dataUrl: '',
isLoading: false,
error: `Image too large (${Math.round(file.size / (1024 * 1024))}MB). Maximum ${MAX_IMAGE_SIZE_MB}MB allowed.`
}
error: `Image too large (${Math.round(file.size / (1024 * 1024))}MB). Maximum ${MAX_IMAGE_SIZE_MB}MB allowed.`,
},
]);

// Remove the error message after 3 seconds
setTimeout(() => {
setPastedImages((prev) => prev.filter(img => img.id !== errorId));
setPastedImages((prev) => prev.filter((img) => img.id !== errorId));
}, 3000);

continue;
}

const reader = new FileReader();
reader.onload = async (e) => {
const dataUrl = e.target?.result as string;
Expand Down Expand Up @@ -365,7 +363,9 @@ export default function ChatInput({
LocalMessageStorage.addMessage(validPastedImageFilesPaths.join(' '));
}

handleSubmit(new CustomEvent('submit', { detail: { value: textToSend } }));
handleSubmit(
new CustomEvent('submit', { detail: { value: textToSend } }) as unknown as React.FormEvent
);

setDisplayValue('');
setValue('');
Expand Down Expand Up @@ -502,7 +502,7 @@ export default function ChatInput({
className="absolute -top-1 -right-1 bg-gray-700 hover:bg-red-600 text-white rounded-full w-5 h-5 flex items-center justify-center text-xs leading-none opacity-0 group-hover:opacity-100 focus:opacity-100 transition-opacity z-10"
aria-label="Remove image"
>
<Close size={14} />
<Close className="w-3.5 h-3.5" />
</button>
)}
</div>
Expand Down
30 changes: 19 additions & 11 deletions ui/desktop/src/components/ChatView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import {
ToolResponseMessageContent,
ToolConfirmationRequestMessageContent,
getTextContent,
TextContent,
} from '../types/message';

export interface ChatType {
Expand Down Expand Up @@ -245,12 +246,20 @@ function ChatContent({

// Create a new window for the recipe editor
console.log('Opening recipe editor with config:', response.recipe);
const recipeConfig = {
id: response.recipe.title || 'untitled',
name: response.recipe.title || 'Untitled Recipe',
description: response.recipe.description || '',
instructions: response.recipe.instructions || '',
activities: response.recipe.activities || [],
prompt: response.recipe.prompt || '',
};
window.electron.createChatWindow(
undefined, // query
undefined, // dir
undefined, // version
undefined, // resumeSessionId
response.recipe, // recipe config
recipeConfig, // recipe config
'recipeEditor' // view type
);

Expand All @@ -273,11 +282,8 @@ function ChatContent({

// Update chat messages when they change and save to sessionStorage
useEffect(() => {
setChat((prevChat: ChatType) => {
const updatedChat = { ...prevChat, messages };
return updatedChat;
});
}, [messages, setChat]);
setChat({ ...chat, messages });
}, [messages, setChat, chat]);

useEffect(() => {
if (messages.length > 0) {
Expand Down Expand Up @@ -354,10 +360,11 @@ function ChatContent({
// check if the last message is a real user's message
if (lastMessage && isUserMessage(lastMessage) && !isToolResponse) {
// Get the text content from the last message before removing it
const textContent = lastMessage.content.find((c) => c.type === 'text')?.text || '';
const textContent = lastMessage.content.find((c): c is TextContent => c.type === 'text');
const textValue = textContent?.text || '';

// Set the text back to the input field
_setInput(textContent);
_setInput(textValue);

// Remove the last user message if it's the most recent one
if (messages.length > 1) {
Expand Down Expand Up @@ -453,7 +460,8 @@ function ChatContent({
return filteredMessages
.reduce<string[]>((history, message) => {
if (isUserMessage(message)) {
const text = message.content.find((c) => c.type === 'text')?.text?.trim();
const textContent = message.content.find((c): c is TextContent => c.type === 'text');
const text = textContent?.text?.trim();
if (text) {
history.push(text);
}
Expand All @@ -468,7 +476,7 @@ function ChatContent({
const fetchSessionTokens = async () => {
try {
const sessionDetails = await fetchSessionDetails(chat.id);
setSessionTokenCount(sessionDetails.metadata.total_tokens);
setSessionTokenCount(sessionDetails.metadata.total_tokens || 0);
} catch (err) {
console.error('Error fetching session token count:', err);
}
Expand Down Expand Up @@ -535,7 +543,7 @@ function ChatContent({
{messages.length === 0 ? (
<Splash
append={append}
activities={Array.isArray(recipeConfig?.activities) ? recipeConfig.activities : null}
activities={Array.isArray(recipeConfig?.activities) ? recipeConfig!.activities : null}
title={recipeConfig?.title}
/>
) : (
Expand Down
Loading
Loading