chore(desktop): remove dead code from ui/desktop

Dead files (no importers): SessionViewComponents, RecipeNameField +
recipeNameUtils, ui/Pill, ui/sheet, ui/separator, ui/Dot, types/electron,
the providers/interfaces chain, and 26 unused icon components.

Dead feature paths: pauseQueueOnStop (hardcoded false end to end),
write-only queue sessionStorage persistence in ChatInput, unused
ChatInput props (recipe, recipeId, append), unused BaseChat props
(suppressEmptyState, onMessageSubmit, disableSearch, contentClassName,
renderHeader, customChatInputProps, customMainLayoutProps), and the
unused setActiveSessions prop on PairRouteWrapper.

Unused exports: deleted fully dead functions (clearToolsCache,
sanitizeName, createModelStruct, resumeSession, getAutoDownloadDisabled,
six analytics track* helpers, two date formatters, two AppEvents enum
members) and un-exported symbols only used within their own module.

Dependencies: removed 15 unused deps and 6 unused devDeps; added
@radix-ui/react-{collapsible,dropdown-menu,switch,tooltip}, which were
used but only available transitively through the removed
@radix-ui/themes.

Locale files: dropped translations for deleted components.

Verified with tsc, eslint, i18n:check, vitest (549 passed), and knip
(zero unused exports remain).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Douwe M Osinga 2026-07-07 15:59:15 -07:00
parent 1bced6616b
commit b9b9b819fe
192 changed files with 2208 additions and 5035 deletions

View file

@ -1,5 +1,5 @@
// Map of announcement file names to their content
export const announcementContents: Record<string, string> = {};
const announcementContents: Record<string, string> = {};
// Helper function to get announcement content by filename
export function getAnnouncementContent(filename: string): string | null {

View file

@ -53,31 +53,24 @@
"@agentclientprotocol/sdk": "^0.19.0",
"@mcp-ui/client": "6.1.0",
"@modelcontextprotocol/ext-apps": "^1.1.1",
"@radix-ui/react-accordion": "^1.2.12",
"@radix-ui/react-avatar": "^1.1.11",
"@radix-ui/react-collapsible": "^1.1.14",
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-icons": "^1.3.2",
"@radix-ui/react-popover": "^1.1.15",
"@radix-ui/react-radio-group": "^1.3.8",
"@radix-ui/react-dropdown-menu": "^2.1.18",
"@radix-ui/react-scroll-area": "^1.2.10",
"@radix-ui/react-select": "^2.2.6",
"@radix-ui/react-slot": "^1.2.4",
"@radix-ui/react-switch": "^1.3.1",
"@radix-ui/react-tabs": "^1.1.13",
"@radix-ui/themes": "^3.3.0",
"@radix-ui/react-tooltip": "^1.2.10",
"@tanstack/react-form": "1.28.3",
"@types/react-router-dom": "^5.3.3",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"compare-versions": "^6.1.1",
"cors": "^2.8.6",
"cronstrue": "^3.12.0",
"date-fns": "^4.1.0",
"dotenv": "^17.3.1",
"electron-log": "^5.4.3",
"electron-squirrel-startup": "^1.0.1",
"electron-updater": "^6.8.3",
"electron-window-state": "^5.0.3",
"express": "^5.2.1",
"framer-motion": "^12.34.3",
"katex": "^0.16.33",
"lodash": "^4.17.23",
@ -96,13 +89,9 @@
"remark-gfm": "^4.0.1",
"remark-math": "^6.0.0",
"shell-quote": "^1.8.3",
"split-type": "^0.3.4",
"swr": "^2.4.0",
"tailwind-merge": "^3.5.0",
"tailwindcss-animate": "^1.0.7",
"turndown": "^7.2.4",
"tw-animate-css": "^1.4.0",
"unist-util-visit": "^5.1.0",
"uuid": "^13.0.0",
"zod": "^3.25.76",
"zod-to-json-schema": "3.25.1"
@ -123,15 +112,12 @@
"@formatjs/icu-messageformat-parser": "3.5.3",
"@modelcontextprotocol/sdk": "^1.27.0",
"@playwright/test": "^1.58.2",
"@tailwindcss/line-clamp": "^0.4.4",
"@tailwindcss/typography": "^0.5.19",
"@tailwindcss/vite": "^4.2.1",
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.3.2",
"@testing-library/user-event": "^14.6.1",
"@types/cors": "^2.8.19",
"@types/electron-squirrel-startup": "^1.0.2",
"@types/express": "^5.0.6",
"@types/lodash": "^4.17.24",
"@types/node": "^25.5.0",
"@types/react": "^19.2.14",
@ -139,13 +125,11 @@
"@types/react-syntax-highlighter": "^15.5.13",
"@types/shell-quote": "^1.7.5",
"@types/turndown": "^5.0.6",
"@types/yauzl": "^2.10.3",
"@typescript-eslint/eslint-plugin": "^8.56.1",
"@typescript-eslint/parser": "^8.56.1",
"@vitejs/plugin-react": "^5.1.4",
"@vitest/coverage-v8": "^4.0.18",
"@vitest/ui": "^4.0.18",
"autoprefixer": "^10.4.24",
"electron": "41.0.0",
"electron-devtools-installer": "^4.0.0",
"eslint": "^9.39.2",
@ -153,7 +137,6 @@
"eslint-plugin-react-hooks": "^5.2.0",
"jsdom": "^28.1.0",
"knip": "^5.85.0",
"postcss": "^8.5.6",
"prettier": "^3.8.1",
"tailwindcss": "^4.2.1",
"typescript": "~5.9.3",

View file

@ -88,9 +88,6 @@ const PairRouteWrapper = ({
initialMessage?: UserInput;
noAutoSubmit?: boolean;
}>;
setActiveSessions: (
sessions: Array<{ sessionId: string; initialMessage?: UserInput; noAutoSubmit?: boolean }>
) => void;
}) => {
const { extensionsList } = useConfig();
const location = useLocation();
@ -637,15 +634,7 @@ export function AppInner() {
}
>
<Route index element={<HubRouteWrapper />} />
<Route
path="pair"
element={
<PairRouteWrapper
activeSessions={activeSessions}
setActiveSessions={setActiveSessions}
/>
}
/>
<Route path="pair" element={<PairRouteWrapper activeSessions={activeSessions} />} />
<Route path="settings" element={<SettingsRoute />} />
<Route
path="extensions"

View file

@ -353,7 +353,10 @@ describe('acpChatSessionController.updateMessage', () => {
resolvePromptCancellation!();
await updatePromise;
expect(acpTruncateSessionConversation).toHaveBeenCalledWith(SESSION_ID, existingMessage.created);
expect(acpTruncateSessionConversation).toHaveBeenCalledWith(
SESSION_ID,
existingMessage.created
);
expect(acpPromptSession).toHaveBeenCalled();
expect(acpChatSessionActions.clearPromptCancellation).not.toHaveBeenCalledWith(
SESSION_ID,

View file

@ -583,9 +583,7 @@ describe('createAcpSessionNotificationAdapter', () => {
status: { type: 'progress', message: 'Still working' },
})
);
expect(progressStateChanges).toEqual([
{ type: 'progressMessage', message: 'Still working' },
]);
expect(progressStateChanges).toEqual([{ type: 'progressMessage', message: 'Still working' }]);
});
});

View file

@ -105,18 +105,10 @@ export async function getAcpClient(): Promise<GooseClient> {
return (await getInitializedAcpClient()).client;
}
export function getAcpClientSync(): GooseClient | null {
return resolvedClient?.client ?? null;
}
export async function getAcpInitializeResponse(): Promise<InitializeResponse> {
return (await getInitializedAcpClient()).initializeResponse;
}
export function isAcpClientReady(): boolean {
return resolvedClient !== null;
}
async function getInitializedAcpClient(): Promise<InitializedAcpClient> {
if (resolvedClient) {
return resolvedClient;

View file

@ -11,10 +11,7 @@ function isSlashCommandItemType(value: unknown): value is SlashCommandItemType {
return typeof value === 'string' && SLASH_COMMAND_ITEM_TYPES.has(value);
}
function stringMetaValue(
meta: AvailableCommand['_meta'],
key: string
): string | undefined {
function stringMetaValue(meta: AvailableCommand['_meta'], key: string): string | undefined {
const value = meta?.[key];
return typeof value === 'string' && value.trim() ? value : undefined;
}
@ -33,7 +30,8 @@ export function availableCommandToDisplayItem(
}
const sourcePath = stringMetaValue(command._meta, 'sourcePath');
const extra = commandType === 'Recipe' ? sourcePath ?? command.description : command.description;
const extra =
commandType === 'Recipe' ? (sourcePath ?? command.description) : command.description;
return {
name: command.name,

View file

@ -1,7 +1,7 @@
import type { InitializeResponse } from '@agentclientprotocol/sdk';
import { getAcpInitializeResponse } from './acpConnection';
export interface AcpFeatureCapabilities {
interface AcpFeatureCapabilities {
localInference: boolean;
}

View file

@ -6,9 +6,8 @@ import { toolNotificationEvent } from './adapter/toolNotifications';
import { acpChatSessionActions, acpChatSessionStore } from './chatSessionStore';
export function handleAcpSessionNotification(notification: SessionNotification): Promise<void> {
const sessionNameBeforeNotification = acpChatSessionStore.getSnapshot(
notification.sessionId
)?.session?.name;
const sessionNameBeforeNotification = acpChatSessionStore.getSnapshot(notification.sessionId)
?.session?.name;
const updatedName =
notification.update.sessionUpdate === 'session_info_update'
? notification.update.title

View file

@ -5,11 +5,7 @@ import { ChatState } from '../types/chatState';
import type { Session } from '../types/session';
import { errorMessage } from '../utils/conversionUtils';
import { showExtensionLoadResults } from '../utils/extensionErrorUtils';
import {
createUserMessage,
getPendingToolConfirmationIds,
type Message,
} from '../types/message';
import { createUserMessage, getPendingToolConfirmationIds, type Message } from '../types/message';
import {
acpChatSessionActions,
acpChatSessionStore,
@ -29,19 +25,19 @@ import {
type AcpRecipeOptions,
} from './sessions';
export interface AcpLoadSessionOptions {
interface AcpLoadSessionOptions {
onSessionLoaded?: () => void;
}
export interface AcpSnapshotOptions {
interface AcpSnapshotOptions {
getCurrentSnapshot(): AcpChatSessionSnapshot | undefined;
}
export interface AcpSubmitMessageOptions extends AcpSnapshotOptions {
interface AcpSubmitMessageOptions extends AcpSnapshotOptions {
onFinish(error?: string): void | Promise<void>;
}
export interface AcpChatSessionController {
interface AcpChatSessionController {
createSession(
cwd: string,
gooseExtensions: GooseExtension[],

View file

@ -49,11 +49,11 @@ const initialTokenState: TokenState = {
accumulatedTotalTokens: 0,
};
export interface AcpChatSessionStore {
interface AcpChatSessionStore {
getSnapshot(sessionId: string): AcpChatSessionSnapshot | undefined;
}
export interface AcpChatSessionActions {
interface AcpChatSessionActions {
deleteSnapshot(sessionId: string): void;
applyAcpSessionNotification(notification: SessionNotification): AcpChatSessionSnapshot;

View file

@ -1,6 +1,6 @@
import { getAcpClient } from './acpConnection';
export type ConfigReadValue = unknown;
type ConfigReadValue = unknown;
export async function acpReadConfig(
key: string,

View file

@ -1,6 +1,6 @@
import type { Stream } from '@aaif/goose-sdk';
export type ClosableAcpStream = Stream & {
type ClosableAcpStream = Stream & {
close: () => void;
};

View file

@ -7,7 +7,7 @@ import { getAcpClient } from './acpConnection';
export type { DictationProviderStatusEntry };
export type DictationProviders = Record<string, DictationProviderStatusEntry>;
type DictationProviders = Record<string, DictationProviderStatusEntry>;
export type LocalDictationModel = DictationLocalModelStatus;
export type LocalDictationDownloadProgress = DictationDownloadProgress;

View file

@ -2,9 +2,9 @@ import type { ExtensionConfig, ExtensionEntry } from '../types/extensions';
import type { GooseExtension, GooseExtensionEntry } from '@aaif/goose-sdk';
import { getAcpClient } from './acpConnection';
export type ConfiguredExtensionEntry = ExtensionEntry & { configKey?: string };
type ConfiguredExtensionEntry = ExtensionEntry & { configKey?: string };
export interface ConfiguredExtensionsResponse {
interface ConfiguredExtensionsResponse {
extensions: ConfiguredExtensionEntry[];
warnings: string[];
}

View file

@ -5,7 +5,7 @@ import { getAcpClient } from './acpConnection';
type JsonRecord = Record<string, unknown>;
export type McpAppTool = ToolListItem;
export type McpAppResourceResponse = {
type McpAppResourceResponse = {
uri: string;
mimeType: string | null;
text: string;

View file

@ -1,9 +1,12 @@
import type { ToolListItem, ToolPermissionEntry, ToolPermissionLevel } from '@aaif/goose-sdk';
import { getAcpClient } from './acpConnection';
export type { ToolListItem, ToolPermissionEntry, ToolPermissionLevel };
export type { ToolListItem, ToolPermissionLevel };
export async function listTools(sessionId: string, extensionName?: string): Promise<ToolListItem[]> {
export async function listTools(
sessionId: string,
extensionName?: string
): Promise<ToolListItem[]> {
const client = await getAcpClient();
const response = await client.goose.toolsList_unstable({
sessionId,

View file

@ -1,7 +1,4 @@
import type {
GetPromptResponse_unstable,
PromptTemplateEntry,
} from '@aaif/goose-sdk';
import type { GetPromptResponse_unstable, PromptTemplateEntry } from '@aaif/goose-sdk';
import { getAcpClient } from './acpConnection';
export type PromptTemplate = PromptTemplateEntry;

View file

@ -6,7 +6,11 @@ import type {
ProviderTemplateCatalogEntryDto,
ProviderTemplateDto,
} from '@aaif/goose-sdk';
import type { ProviderDetails, ThinkingEffort, UpdateCustomProviderRequest } from '../types/providers';
import type {
ProviderDetails,
ThinkingEffort,
UpdateCustomProviderRequest,
} from '../types/providers';
import { getAcpClient } from './acpConnection';
export type { CanonicalModelInfoDto, ProviderSecretDto };

View file

@ -5,7 +5,7 @@ import type {
} from '@aaif/goose-sdk';
import { v7 as uuidv7 } from 'uuid';
export interface AcpRecipeParamRequest {
interface AcpRecipeParamRequest {
id: string;
sessionId: string;
parameters: RecipeParameterDto[];

View file

@ -42,19 +42,19 @@ export interface SessionListItem {
hasRecipe?: boolean;
}
export interface SessionListPage {
interface SessionListPage {
sessions: SessionListItem[];
nextCursor: string | null;
}
export interface LoadSessionMeta {
interface LoadSessionMeta {
recipe?: Recipe | null;
userRecipeValues?: Record<string, string> | null;
extensionResults?: ExtensionLoadResult[] | null;
workingDir?: string;
}
export interface AcpLoadSessionResult {
interface AcpLoadSessionResult {
sessionInfo: SessionInfo;
response: LoadSessionResponse;
meta: LoadSessionMeta;
@ -72,7 +72,7 @@ function parseSessionResponseMeta(rawMeta: unknown): LoadSessionMeta {
};
}
export function parseLoadMeta(response: LoadSessionResponse): LoadSessionMeta {
function parseLoadMeta(response: LoadSessionResponse): LoadSessionMeta {
return parseSessionResponseMeta(response._meta);
}
@ -131,7 +131,7 @@ function sessionInfoToListItem(s: SessionInfo): SessionListItem {
};
}
export interface SessionListFilter {
interface SessionListFilter {
keyword?: string;
}
@ -215,7 +215,7 @@ async function loadAcpSession(sessionId: string): Promise<AcpLoadSessionResult>
};
}
export interface AcpNewSessionResult {
interface AcpNewSessionResult {
sessionId: string;
sessionInfo: SessionInfo;
meta: LoadSessionMeta;
@ -302,10 +302,7 @@ export async function acpExportSession(sessionId: string): Promise<string> {
return response.data;
}
export async function acpImportSession(
input: string,
source: SessionImportSource
): Promise<void> {
export async function acpImportSession(input: string, source: SessionImportSource): Promise<void> {
const client = await getAcpClient();
await client.goose.sessionImport_unstable({ input, source });
}

View file

@ -7,11 +7,11 @@ const PROBE_TIMEOUT_MS = 1000;
type FetchInput = Parameters<typeof globalThis.fetch>[0];
type FetchInit = Parameters<typeof globalThis.fetch>[1];
export interface CheckServerStatusOptions {
interface CheckServerStatusOptions {
onEvent?: (name: string, details?: Record<string, unknown>) => void;
}
export interface CheckBackendStatusParams {
interface CheckBackendStatusParams {
baseUrl: string;
serverSecret: string;
fetch: typeof globalThis.fetch;
@ -19,7 +19,7 @@ export interface CheckBackendStatusParams {
options?: CheckServerStatusOptions;
}
export const isFatalError = (line: string): boolean => {
const isFatalError = (line: string): boolean => {
const fatalPatterns = [/panicked at/, /RUST_BACKTRACE/, /fatal error/i];
return fatalPatterns.some((pattern) => pattern.test(line));
};

View file

@ -43,13 +43,6 @@ const i18n = defineMessages({
interface BaseChatProps {
setChat: (chat: ChatType) => void;
onMessageSubmit?: (message: string) => void;
renderHeader?: () => React.ReactNode;
customChatInputProps?: Record<string, unknown>;
customMainLayoutProps?: Record<string, unknown>;
contentClassName?: string;
disableSearch?: boolean;
suppressEmptyState: boolean;
sessionId: string;
isActiveSession: boolean;
initialMessage?: UserInput;
@ -58,9 +51,6 @@ interface BaseChatProps {
export default function BaseChat({
setChat,
renderHeader,
customChatInputProps = {},
customMainLayoutProps = {},
sessionId,
initialMessage,
noAutoSubmit,
@ -96,7 +86,6 @@ export default function BaseChat({
sessionLoadError,
tokenState,
notifications: toolCallNotifications,
pauseQueueOnStop,
queueProcessingBlocked,
onMessageUpdate,
} = useChatSession({
@ -354,12 +343,7 @@ export default function BaseChat({
if (sessionLoadError) {
return (
<div className="h-full flex flex-col min-h-0">
<MainPanelLayout
backgroundColor={'bg-background-primary'}
removeTopPadding={true}
{...customMainLayoutProps}
>
{renderHeader && renderHeader()}
<MainPanelLayout backgroundColor={'bg-background-primary'} removeTopPadding={true}>
<div className="flex flex-col flex-1 min-h-0 relative">
<div className="flex-1 flex items-center justify-center">
<div className="flex flex-col items-center justify-center p-8">
@ -387,14 +371,7 @@ export default function BaseChat({
return (
<div className="h-full flex flex-col min-h-0">
<MainPanelLayout
backgroundColor={'bg-background-primary'}
removeTopPadding={true}
{...customMainLayoutProps}
>
{/* Custom header */}
{renderHeader && renderHeader()}
<MainPanelLayout backgroundColor={'bg-background-primary'} removeTopPadding={true}>
{/* Chat container with sticky recipe header */}
<div className="flex flex-col flex-1 min-h-0 relative">
{/* Goose watermark - top right */}
@ -483,7 +460,6 @@ export default function BaseChat({
chatState={chatState}
onStop={stopStreaming}
onSteerQueuedMessage={onSteerQueuedMessage}
pauseQueueOnStop={pauseQueueOnStop}
queueProcessingBlocked={queueProcessingBlocked}
commandHistory={commandHistory}
initialValue={initialPrompt}
@ -504,7 +480,6 @@ export default function BaseChat({
onFilesProcessed={() => setDroppedFiles([])} // Clear dropped files after processing
messages={messages}
disableAnimation={disableAnimation}
recipe={recipe}
recipeAccepted={!hasNotAcceptedRecipe}
initialPrompt={initialPrompt}
sessionModel={sessionModel}
@ -513,7 +488,6 @@ export default function BaseChat({
workingDir={session?.working_dir}
onWorkingDirChange={handleWorkingDirChange}
latestInference={latestInference}
{...customChatInputProps}
/>
</ChatInputCard>
</MainPanelLayout>

View file

@ -23,7 +23,6 @@ import { COST_TRACKING_ENABLED } from '../updates';
import { CostTracker } from './bottom_menu/CostTracker';
import { ContextWindowIndicator } from './bottom_menu/ContextWindowIndicator';
import { DroppedFile, useFileDrop } from '../hooks/useFileDrop';
import { Recipe } from '../recipe';
import { MessageQueue, QueuedMessage } from './MessageQueue';
import { detectInterruption } from '../utils/interruptionDetector';
import { DiagnosticsModal } from './ui/Diagnostics';
@ -162,7 +161,6 @@ interface ChatInputProps {
chatState: ChatState;
onStop?: () => void;
onSteerQueuedMessage?: (input: UserInput) => Promise<boolean>;
pauseQueueOnStop?: boolean;
queueProcessingBlocked?: boolean;
commandHistory?: string[];
initialValue?: string;
@ -175,11 +173,8 @@ interface ChatInputProps {
accumulatedCost?: number | null;
messages?: Message[];
disableAnimation?: boolean;
recipe?: Recipe | null;
recipeId?: string | null;
recipeAccepted?: boolean;
initialPrompt?: string;
append?: (message: Message) => void;
onWorkingDirChange?: (newDir: string) => Promise<void> | void;
inputRef?: React.RefObject<HTMLTextAreaElement | null>;
sessionModel?: string | null;
@ -197,7 +192,6 @@ export default function ChatInput({
chatState = ChatState.Idle,
onStop,
onSteerQueuedMessage,
pauseQueueOnStop = false,
queueProcessingBlocked = false,
commandHistory = [],
initialValue = '',
@ -210,11 +204,8 @@ export default function ChatInput({
accumulatedCost,
messages = [],
disableAnimation = false,
recipe: _recipe,
recipeId: _recipeId,
recipeAccepted,
initialPrompt,
append: _append,
onWorkingDirChange,
inputRef,
sessionModel,
@ -336,36 +327,6 @@ export default function ChatInput({
setWorkingDirOverride(null);
}, [sessionId, workingDir]);
// Save queue state (paused/interrupted) to storage
useEffect(() => {
try {
window.sessionStorage.setItem('goose-queue-paused', JSON.stringify(queuePausedRef.current));
} catch (error) {
console.error('Error saving queue pause state:', error);
}
}, [queuedMessages]); // Save when queue changes
useEffect(() => {
try {
window.sessionStorage.setItem('goose-queue-interruption', JSON.stringify(lastInterruption));
} catch (error) {
console.error('Error saving queue interruption state:', error);
}
}, [lastInterruption]);
// Cleanup effect - save final state on component unmount
useEffect(() => {
return () => {
// Save final queue state when component unmounts
try {
window.sessionStorage.setItem('goose-queue-paused', JSON.stringify(queuePausedRef.current));
window.sessionStorage.setItem('goose-queue-interruption', JSON.stringify(lastInterruption));
} catch (error) {
console.error('Error saving queue state on unmount:', error);
}
};
}, [lastInterruption]); // Include lastInterruption in dependency array
// Queue processing
useEffect(() => {
const becameIdle = wasLoadingRef.current && !isLoading;
@ -1434,13 +1395,6 @@ export default function ChatInput({
if (onStop) onStop();
};
const handleStop = () => {
if (pauseQueueOnStop && queuedMessages.length > 0) {
pauseRemainingQueue();
}
if (onStop) onStop();
};
const handleResumeQueue = () => {
queuePausedRef.current = false;
setLastInterruption(null);
@ -1807,7 +1761,7 @@ export default function ChatInput({
{isLoading && !hasSubmittableContent ? (
<Button
type="button"
onClick={handleStop}
onClick={onStop}
size="sm"
shape="round"
variant="ghost"

View file

@ -53,7 +53,6 @@ export default function ChatSessionsContainer({
sessionId={session.sessionId}
initialMessage={session.initialMessage}
noAutoSubmit={session.noAutoSubmit}
suppressEmptyState={false}
isActiveSession={isVisible}
/>
</div>

View file

@ -72,7 +72,9 @@ export function ErrorUI({ error }: { error: string }) {
<AlertTriangle className="w-8 h-8 text-destructive" />
</div>
<h1 className="text-2xl font-semibold text-foreground dark:text-white">{intl.formatMessage(i18n.heading)}</h1>
<h1 className="text-2xl font-semibold text-foreground dark:text-white">
{intl.formatMessage(i18n.heading)}
</h1>
<p className="text-base text-text-secondary dark:text-muted-foreground mb-2">
{version !== undefined

View file

@ -55,7 +55,9 @@ describe('ExtensionInstallModal', () => {
it('should handle trusted extension (default behaviour, no allowlist)', async () => {
mockElectron.getAllowedExtensions.mockResolvedValue([]);
renderWithIntl(<ExtensionInstallModal addExtension={mockAddExtension} setView={mockSetView} />);
renderWithIntl(
<ExtensionInstallModal addExtension={mockAddExtension} setView={mockSetView} />
);
const eventHandler = getAddExtensionEventHandler();
@ -72,7 +74,9 @@ describe('ExtensionInstallModal', () => {
it('should handle trusted extension (from allowlist)', async () => {
mockElectron.getAllowedExtensions.mockResolvedValue(['npx test-extension']);
renderWithIntl(<ExtensionInstallModal addExtension={mockAddExtension} setView={mockSetView} />);
renderWithIntl(
<ExtensionInstallModal addExtension={mockAddExtension} setView={mockSetView} />
);
const eventHandler = getAddExtensionEventHandler();
@ -90,7 +94,9 @@ describe('ExtensionInstallModal', () => {
});
mockElectron.getAllowedExtensions.mockResolvedValue(['uvx allowed-package']);
renderWithIntl(<ExtensionInstallModal addExtension={mockAddExtension} setView={mockSetView} />);
renderWithIntl(
<ExtensionInstallModal addExtension={mockAddExtension} setView={mockSetView} />
);
const eventHandler = getAddExtensionEventHandler();
@ -109,7 +115,9 @@ describe('ExtensionInstallModal', () => {
it('should handle i-ching-mcp-server as allowed command', async () => {
mockElectron.getAllowedExtensions.mockResolvedValue([]);
renderWithIntl(<ExtensionInstallModal addExtension={mockAddExtension} setView={mockSetView} />);
renderWithIntl(
<ExtensionInstallModal addExtension={mockAddExtension} setView={mockSetView} />
);
const eventHandler = getAddExtensionEventHandler();
@ -128,7 +136,9 @@ describe('ExtensionInstallModal', () => {
it('should handle blocked extension', async () => {
mockElectron.getAllowedExtensions.mockResolvedValue(['uvx allowed-package']);
renderWithIntl(<ExtensionInstallModal addExtension={mockAddExtension} setView={mockSetView} />);
renderWithIntl(
<ExtensionInstallModal addExtension={mockAddExtension} setView={mockSetView} />
);
const eventHandler = getAddExtensionEventHandler();
@ -147,7 +157,9 @@ describe('ExtensionInstallModal', () => {
it('should dismiss modal correctly', async () => {
mockElectron.getAllowedExtensions.mockResolvedValue([]);
renderWithIntl(<ExtensionInstallModal addExtension={mockAddExtension} setView={mockSetView} />);
renderWithIntl(
<ExtensionInstallModal addExtension={mockAddExtension} setView={mockSetView} />
);
const eventHandler = getAddExtensionEventHandler();
@ -168,7 +180,9 @@ describe('ExtensionInstallModal', () => {
vi.mocked(addExtensionFromDeepLink).mockResolvedValue(undefined);
mockElectron.getAllowedExtensions.mockResolvedValue([]);
renderWithIntl(<ExtensionInstallModal addExtension={mockAddExtension} setView={mockSetView} />);
renderWithIntl(
<ExtensionInstallModal addExtension={mockAddExtension} setView={mockSetView} />
);
const eventHandler = getAddExtensionEventHandler();

View file

@ -29,7 +29,8 @@ const i18n = defineMessages({
},
blockedMessage: {
id: 'extensionInstallModal.blockedMessage',
defaultMessage: 'This extension command is not in the allowed list and its installation is blocked.\n\nExtension: {name}\nCommand: {command}\n\nContact your administrator to request approval for this extension.',
defaultMessage:
'This extension command is not in the allowed list and its installation is blocked.\n\nExtension: {name}\nCommand: {command}\n\nContact your administrator to request approval for this extension.',
},
ok: {
id: 'extensionInstallModal.ok',
@ -41,15 +42,18 @@ const i18n = defineMessages({
},
untrustedSecurityMessage: {
id: 'extensionInstallModal.untrustedSecurityMessage',
defaultMessage: 'This extension command is not in the allowed list and will be able to access your conversations and provide additional functionality.\n\nInstalling extensions from untrusted sources may pose security risks.',
defaultMessage:
'This extension command is not in the allowed list and will be able to access your conversations and provide additional functionality.\n\nInstalling extensions from untrusted sources may pose security risks.',
},
untrustedMessageWithUrl: {
id: 'extensionInstallModal.untrustedMessageWithUrl',
defaultMessage: '{securityMessage}\n\nExtension: {name}\nURL: {url}\n\nContact your administrator if you are unsure about this.',
defaultMessage:
'{securityMessage}\n\nExtension: {name}\nURL: {url}\n\nContact your administrator if you are unsure about this.',
},
untrustedMessageWithCommand: {
id: 'extensionInstallModal.untrustedMessageWithCommand',
defaultMessage: '{securityMessage}\n\nExtension: {name}\nCommand: {command}\n\nContact your administrator if you are unsure about this.',
defaultMessage:
'{securityMessage}\n\nExtension: {name}\nCommand: {command}\n\nContact your administrator if you are unsure about this.',
},
installAnyway: {
id: 'extensionInstallModal.installAnyway',
@ -81,7 +85,8 @@ const i18n = defineMessages({
},
alreadyInstalledMessage: {
id: 'extensionInstallModal.alreadyInstalledMessage',
defaultMessage: "''{name}'' extension has already been installed successfully. Start a new chat session to use it.",
defaultMessage:
"''{name}'' extension has already been installed successfully. Start a new chat session to use it.",
},
installing: {
id: 'extensionInstallModal.installing',
@ -200,7 +205,8 @@ export function ExtensionInstallModal({ addExtension, setView }: ExtensionInstal
case 'blocked':
return {
title: intl.formatMessage(i18n.blockedTitle),
message: '\n\n' + intl.formatMessage(i18n.blockedMessage, { name, command: displayCommand }),
message:
'\n\n' + intl.formatMessage(i18n.blockedMessage, { name, command: displayCommand }),
confirmLabel: intl.formatMessage(i18n.ok),
cancelLabel: '',
showSingleButton: true,
@ -210,8 +216,16 @@ export function ExtensionInstallModal({ addExtension, setView }: ExtensionInstal
case 'untrusted': {
const securityMessage = '\n\n' + intl.formatMessage(i18n.untrustedSecurityMessage);
const message = remoteUrl
? intl.formatMessage(i18n.untrustedMessageWithUrl, { securityMessage, name, url: remoteUrl })
: intl.formatMessage(i18n.untrustedMessageWithCommand, { securityMessage, name, command: displayCommand });
? intl.formatMessage(i18n.untrustedMessageWithUrl, {
securityMessage,
name,
url: remoteUrl,
})
: intl.formatMessage(i18n.untrustedMessageWithCommand, {
securityMessage,
name,
command: displayCommand,
});
return {
title: intl.formatMessage(i18n.untrustedTitle),
@ -236,58 +250,59 @@ export function ExtensionInstallModal({ addExtension, setView }: ExtensionInstal
}
};
const handleExtensionRequest = useCallback(async (link: string): Promise<void> => {
if (processingLinkRef.current === link) {
return;
}
processingLinkRef.current = link;
try {
const command = extractCommand(link);
const remoteUrl = extractRemoteUrl(link);
const extName = extractExtensionName(link);
const extensionsList = await getExtensionsRef.current(true);
if (extensionsList?.find((ext) => ext.name === extName)) {
toastService.success({
title: intl.formatMessage(i18n.alreadyInstalledTitle, { name: extName }),
msg: intl.formatMessage(i18n.alreadyInstalledMessage, { name: extName }),
});
const handleExtensionRequest = useCallback(
async (link: string): Promise<void> => {
if (processingLinkRef.current === link) {
return;
}
processingLinkRef.current = link;
const extensionInfo: ExtensionInfo = {
name: extName,
command: command,
remoteUrl: remoteUrl || undefined,
link: link,
};
try {
const command = extractCommand(link);
const remoteUrl = extractRemoteUrl(link);
const extName = extractExtensionName(link);
const extensionsList = await getExtensionsRef.current(true);
const modalType = await determineModalType(command, remoteUrl);
if (extensionsList?.find((ext) => ext.name === extName)) {
toastService.success({
title: intl.formatMessage(i18n.alreadyInstalledTitle, { name: extName }),
msg: intl.formatMessage(i18n.alreadyInstalledMessage, { name: extName }),
});
return;
}
setModalState({
isOpen: true,
modalType,
extensionInfo,
isPending: false,
error: null,
});
const extensionInfo: ExtensionInfo = {
name: extName,
command: command,
remoteUrl: remoteUrl || undefined,
link: link,
};
setPendingLink(modalType === 'blocked' ? null : link);
const modalType = await determineModalType(command, remoteUrl);
window.electron.logInfo(`Extension modal opened: ${modalType} for ${extName}`);
} catch (error) {
console.error('Error processing extension request:', error);
setModalState((prev) => ({
...prev,
error: errorMessage(error, 'Unknown error'),
}));
} finally {
processingLinkRef.current = null;
}
}, [intl]);
setModalState({
isOpen: true,
modalType,
extensionInfo,
isPending: false,
error: null,
});
setPendingLink(modalType === 'blocked' ? null : link);
window.electron.logInfo(`Extension modal opened: ${modalType} for ${extName}`);
} catch (error) {
console.error('Error processing extension request:', error);
setModalState((prev) => ({
...prev,
error: errorMessage(error, 'Unknown error'),
}));
} finally {
processingLinkRef.current = null;
}
},
[intl]
);
const dismissModal = useCallback(() => {
setModalState({
@ -308,7 +323,6 @@ export function ExtensionInstallModal({ addExtension, setView }: ExtensionInstal
setModalState((prev) => ({ ...prev, isPending: true }));
try {
if (addExtension) {
await addExtensionFromDeepLink(
pendingLink,
@ -331,7 +345,6 @@ export function ExtensionInstallModal({ addExtension, setView }: ExtensionInstal
}, [pendingLink, dismissModal, addExtension, setView]);
useEffect(() => {
const handleAddExtension = async (_event: IpcRendererEvent, ...args: unknown[]) => {
const link = args[0] as string;
await handleExtensionRequest(link);

View file

@ -39,7 +39,9 @@ const ThemeSelector: React.FC<ThemeSelectorProps> = ({
return (
<div className={`${!horizontal ? 'px-1 py-2 space-y-2' : ''} ${className}`}>
{!hideTitle && <div className="text-xs text-text-primary px-3">{intl.formatMessage(i18n.theme)}</div>}
{!hideTitle && (
<div className="text-xs text-text-primary px-3">{intl.formatMessage(i18n.theme)}</div>
)}
<div
className={`${horizontal ? 'flex' : 'grid grid-cols-3'} gap-1 ${!horizontal ? 'px-3' : ''}`}
>

View file

@ -20,7 +20,8 @@ const i18n = defineMessages({
},
partiallyLoaded: {
id: 'groupedExtensionLoadingToast.partiallyLoaded',
defaultMessage: 'Loaded {successCount}/{totalCount, plural, one {# extension} other {# extensions}}',
defaultMessage:
'Loaded {successCount}/{totalCount, plural, one {# extension} other {# extensions}}',
},
failedToLoad: {
id: 'groupedExtensionLoadingToast.failedToLoad',
@ -158,7 +159,10 @@ export function GroupedExtensionLoadingToast({
{ext.status === 'error' && ext.error && (
<div className="ml-7 flex flex-col gap-2">
<div className="text-xs opacity-75 break-words">
{formatExtensionErrorMessage(ext.error, intl.formatMessage(i18n.failedToAddExtension))}
{formatExtensionErrorMessage(
ext.error,
intl.formatMessage(i18n.failedToAddExtension)
)}
</div>
<div className="flex gap-2">
{ext.recoverHints && setView && (
@ -185,7 +189,9 @@ export function GroupedExtensionLoadingToast({
setTimeout(() => setCopiedExtension(null), 2000);
}}
>
{copiedExtension === ext.name ? intl.formatMessage(i18n.copied) : intl.formatMessage(i18n.copyError)}
{copiedExtension === ext.name
? intl.formatMessage(i18n.copied)
: intl.formatMessage(i18n.copyError)}
</Button>
</div>
</div>
@ -202,7 +208,11 @@ export function GroupedExtensionLoadingToast({
<CollapsibleTrigger asChild>
<button
className="flex items-center justify-center gap-1 text-xs opacity-60 hover:opacity-100 transition-opacity mt-2 py-1.5 w-full"
aria-label={isOpen ? intl.formatMessage(i18n.collapseDetails) : intl.formatMessage(i18n.expandDetails)}
aria-label={
isOpen
? intl.formatMessage(i18n.collapseDetails)
: intl.formatMessage(i18n.expandDetails)
}
>
{isOpen ? (
<>

View file

@ -28,7 +28,7 @@ interface IconInfo {
color: string;
}
export const getItemIcon = (item: DisplayItem): IconInfo => {
const getItemIcon = (item: DisplayItem): IconInfo => {
switch (item.itemType) {
case 'Builtin':
return { Icon: Zap, color: '#3b82f6' }; // Blue

View file

@ -135,8 +135,14 @@ export const Navigation: React.FC<{ className?: string }> = ({ className }) => {
const isActive = useCallback((path: string) => location.pathname === path, [location.pathname]);
const { recentSessions, recentSessionsByProject, activeSessionId, fetchSessions, handleNavClick, handleSessionClick } =
useNavigationSessions();
const {
recentSessions,
recentSessionsByProject,
activeSessionId,
fetchSessions,
handleNavClick,
handleSessionClick,
} = useNavigationSessions();
const [sessionStatuses, setSessionStatuses] = useState<Map<string, SessionStatus>>(new Map());

View file

@ -301,7 +301,9 @@ const MarkdownContent = memo(function MarkdownContent({
<ConfirmationModal
isOpen={pendingLink !== null}
title={intl.formatMessage(i18n.openExternalLink)}
message={intl.formatMessage(i18n.openProtocolLink, { protocol: pendingLink?.protocol ?? '' })}
message={intl.formatMessage(i18n.openProtocolLink, {
protocol: pendingLink?.protocol ?? '',
})}
detail={intl.formatMessage(i18n.thisWillOpen, { href: pendingLink?.href ?? '' })}
onConfirm={handleConfirmOpen}
onCancel={handleCancelOpen}

View file

@ -36,15 +36,3 @@ export function getCachedTools(
cache.set(key, promise);
return promise;
}
export function clearToolsCache(sessionId?: string): void {
if (!sessionId) {
cache.clear();
return;
}
for (const key of cache.keys()) {
if (key.startsWith(`${sessionId}:`)) {
cache.delete(key);
}
}
}

View file

@ -26,7 +26,7 @@ interface UseDisplayModeOptions {
containerRef: React.RefObject<HTMLDivElement | null>;
}
export interface DisplayModeState {
interface DisplayModeState {
activeDisplayMode: GooseDisplayMode;
effectiveDisplayModes: McpUiDisplayMode[];
isStandalone: boolean;

View file

@ -548,5 +548,3 @@ export const MessageQueue: React.FC<MessageQueueProps> = ({
</div>
);
};
export default MessageQueue;

View file

@ -270,15 +270,17 @@ export default function ProgressiveMessageList({
const previousResolvedModel = currentResolvedModel ? getPreviousResolvedModel(index) : null;
const showModelChangeDisclosure = Boolean(
currentResolvedModel &&
previousResolvedModel &&
currentResolvedModel !== previousResolvedModel
previousResolvedModel &&
currentResolvedModel !== previousResolvedModel
);
const messageKey = message.id ?? `msg-${index}-${message.created}`;
return (
<Fragment key={messageKey}>
{showModelChangeDisclosure && currentResolvedModel && previousResolvedModel &&
{showModelChangeDisclosure &&
currentResolvedModel &&
previousResolvedModel &&
renderModelChangeDisclosure(previousResolvedModel, currentResolvedModel)}
<div
className={`relative ${index === 0 ? 'mt-0' : 'mt-4'} ${isUser ? 'user' : 'assistant'} ${messageIsInChain ? 'in-chain' : ''}`}

View file

@ -505,7 +505,10 @@ export default function SessionActionsHeader({
<Button variant="outline" onClick={() => setIsRenameOpen(false)} disabled={isRenaming}>
{intl.formatMessage(i18n.cancel)}
</Button>
<Button onClick={() => void handleRename()} disabled={isRenaming || !renameValue.trim()}>
<Button
onClick={() => void handleRename()}
disabled={isRenaming || !renameValue.trim()}
>
{isRenaming ? intl.formatMessage(i18n.saving) : intl.formatMessage(i18n.save)}
</Button>
</DialogFooter>

View file

@ -44,7 +44,10 @@ export const SessionIndicators = React.memo<SessionIndicatorsProps>(
if (isStreaming) {
return (
<div className="flex items-center gap-1">
<Loader2 className="w-3 h-3 text-blue-500 animate-spin" aria-label={intl.formatMessage(i18n.streaming)} />
<Loader2
className="w-3 h-3 text-blue-500 animate-spin"
aria-label={intl.formatMessage(i18n.streaming)}
/>
</div>
);
}
@ -52,7 +55,10 @@ export const SessionIndicators = React.memo<SessionIndicatorsProps>(
if (hasUnread) {
return (
<div className="flex items-center gap-1">
<div className="w-2 h-2 bg-green-500 rounded-full" aria-label={intl.formatMessage(i18n.newActivity)} />
<div
className="w-2 h-2 bg-green-500 rounded-full"
aria-label={intl.formatMessage(i18n.newActivity)}
/>
</div>
);
}

View file

@ -51,7 +51,7 @@ const globalApprovalState = new Map<
}
>();
export interface ToolApprovalData {
interface ToolApprovalData {
id: string;
toolName: string;
prompt?: string;
@ -133,7 +133,11 @@ export default function ToolApprovalButtons({ data }: { data: ToolApprovalData }
{intl.formatMessage(i18n.alwaysAllow)}
</Button>
)}
<Button className="rounded-full" variant="outline" onClick={() => handleAction('deny_once')}>
<Button
className="rounded-full"
variant="outline"
onClick={() => handleAction('deny_once')}
>
{intl.formatMessage(i18n.deny)}
</Button>
</div>

View file

@ -33,14 +33,18 @@ export function ToolCallArguments({ args }: ToolCallArgumentsProps) {
return (
<div className="font-sans text-sm mb-2">
<div className={`flex flex-row items-stretch ${!isExpanded && needsExpansion ? 'truncate min-w-0' : ''}`}>
<div
className={`flex flex-row items-stretch ${!isExpanded && needsExpansion ? 'truncate min-w-0' : ''}`}
>
<button
onClick={() => needsExpansion && toggleKey(key)}
className={`flex text-left text-text-secondary min-w-[140px] ${needsExpansion ? 'cursor-pointer' : 'cursor-default'}`}
>
<span>{key}</span>
</button>
<div className={`w-full flex items-stretch ${!isExpanded && needsExpansion ? 'truncate min-w-0' : ''}`}>
<div
className={`w-full flex items-stretch ${!isExpanded && needsExpansion ? 'truncate min-w-0' : ''}`}
>
{isExpanded ? (
<pre className="font-mono text-xs text-text-secondary whitespace-pre-wrap max-w-full overflow-x-auto">
{text}

View file

@ -16,10 +16,7 @@ interface ToolCallStatusIndicatorProps {
className?: string;
}
export const ToolCallStatusIndicator: React.FC<ToolCallStatusIndicatorProps> = ({
status,
className,
}) => {
const ToolCallStatusIndicator: React.FC<ToolCallStatusIndicatorProps> = ({ status, className }) => {
const intl = useIntl();
const getStatusStyles = () => {
switch (status) {

View file

@ -12,7 +12,6 @@ import {
ToolConfirmationData,
} from '../types/message';
import { cn, snakeToTitleCase } from '../utils';
import { LoadingStatus } from './ui/Dot';
import { ChevronRight, ExternalLink } from 'lucide-react';
import { TooltipWrapper } from './settings/providers/subcomponents/buttons/TooltipWrapper';
import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
@ -22,6 +21,8 @@ import McpAppRenderer from './McpApps/McpAppRenderer';
import ToolApprovalButtons from './ToolApprovalButtons';
import { defineMessages, useIntl } from '../i18n';
type LoadingStatus = 'loading' | 'success' | 'error';
const i18n = defineMessages({
viewSubagentSession: {
id: 'toolCallWithResponse.viewSubagentSession',

View file

@ -23,7 +23,8 @@ const i18n = defineMessages({
},
editInPlaceDescription: {
id: 'userMessage.editInPlaceDescription',
defaultMessage: '<b>Edit in Place</b> updates this session • <b>Fork Session</b> creates a new session',
defaultMessage:
'<b>Edit in Place</b> updates this session • <b>Fork Session</b> creates a new session',
},
cancel: {
id: 'userMessage.cancel',
@ -233,7 +234,11 @@ export default function UserMessage({ message, onMessageUpdate }: UserMessagePro
})}
</div>
<div className="flex gap-3">
<Button onClick={handleCancel} variant="ghost" aria-label={intl.formatMessage(i18n.cancelAriaLabel)}>
<Button
onClick={handleCancel}
variant="ghost"
aria-label={intl.formatMessage(i18n.cancelAriaLabel)}
>
{intl.formatMessage(i18n.cancel)}
</Button>
<Button
@ -292,7 +297,9 @@ export default function UserMessage({ message, onMessageUpdate }: UserMessagePro
}
}}
className="flex items-center gap-1 text-xs text-text-secondary hover:cursor-pointer hover:text-text-primary transition-all duration-200 opacity-0 group-hover:opacity-100 -translate-y-4 group-hover:translate-y-0 focus:outline-none focus:ring-2 focus:ring-blue-400 focus:ring-opacity-50 rounded"
aria-label={intl.formatMessage(i18n.editMessageAriaLabel, { preview: `${textContent.substring(0, 50)}${textContent.length > 50 ? '...' : ''}` })}
aria-label={intl.formatMessage(i18n.editMessageAriaLabel, {
preview: `${textContent.substring(0, 50)}${textContent.length > 50 ? '...' : ''}`,
})}
aria-expanded={isEditing}
title={intl.formatMessage(i18n.editMessageTitle)}
>

View file

@ -89,7 +89,11 @@ export const AlertBox = ({ alert, className }: AlertBoxProps) => {
}
} catch (error) {
console.error('Error saving threshold:', error);
window.alert(intl.formatMessage(i18n.failedToSaveThreshold, { error: errorMessage(error, 'Unknown error') }));
window.alert(
intl.formatMessage(i18n.failedToSaveThreshold, {
error: errorMessage(error, 'Unknown error'),
})
);
} finally {
setIsSaving(false);
}
@ -111,7 +115,9 @@ export const AlertBox = ({ alert, className }: AlertBoxProps) => {
<div className="flex items-center justify-center gap-1 min-h-[20px]">
{isEditingThreshold ? (
<>
<span className="text-[10px] opacity-70">{intl.formatMessage(i18n.autoCompactAt)}</span>
<span className="text-[10px] opacity-70">
{intl.formatMessage(i18n.autoCompactAt)}
</span>
<input
type="number"
min="1"

View file

@ -261,7 +261,7 @@ function SessionExtensionsMenu({ sessionId }: { sessionId: string }) {
const loadSessionExtensions = useCallback(
async (targetSessionId: string, signal?: GetSessionExtensionsSignal) => {
const extensions = await getAcpSessionExtensions(targetSessionId)
const extensions = await getAcpSessionExtensions(targetSessionId);
if (signal?.aborted || latestSessionIdRef.current !== targetSessionId) {
return;

View file

@ -11,7 +11,8 @@ const i18n = defineMessages({
},
costUnavailable: {
id: 'costTracker.costUnavailable',
defaultMessage: 'Cost data not available for {model} ({inputTokens} input, {outputTokens} output tokens)',
defaultMessage:
'Cost data not available for {model} ({inputTokens} input, {outputTokens} output tokens)',
},
totalSessionCost: {
id: 'costTracker.totalSessionCost',
@ -19,7 +20,8 @@ const i18n = defineMessages({
},
inputOutputTooltip: {
id: 'costTracker.inputOutputTooltip',
defaultMessage: 'Input: {inputTokens} tokens ({inputCost}) | Output: {outputTokens} tokens ({outputCost})',
defaultMessage:
'Input: {inputTokens} tokens ({inputCost}) | Output: {outputTokens} tokens ({outputCost})',
},
});
@ -116,8 +118,7 @@ export function CostTracker({
if (
accumulatedCost == null &&
(!costInfo ||
(costInfo.inputTokenCost === undefined && costInfo.outputTokenCost === undefined))
(!costInfo || (costInfo.inputTokenCost === undefined && costInfo.outputTokenCost === undefined))
) {
const freeProviders = ['ollama', 'local', 'localhost'];
if (freeProviders.includes(currentProvider.toLowerCase())) {
@ -160,19 +161,24 @@ export function CostTracker({
// Build tooltip content
const getTooltipContent = (): string => {
if (pricingFailed) {
return intl.formatMessage(i18n.pricingUnavailable, { model: `${currentProvider}/${currentModel}` });
return intl.formatMessage(i18n.pricingUnavailable, {
model: `${currentProvider}/${currentModel}`,
});
}
const currency = costInfo?.currency || '$';
if (accumulatedCost != null) {
return intl.formatMessage(i18n.totalSessionCost, { cost: `${currency}${totalCost.toFixed(4)}` })
+ `\n` + intl.formatMessage(i18n.inputOutputTooltip, {
return (
intl.formatMessage(i18n.totalSessionCost, { cost: `${currency}${totalCost.toFixed(4)}` }) +
`\n` +
intl.formatMessage(i18n.inputOutputTooltip, {
inputTokens: inputTokens.toLocaleString(),
inputCost: `${currency}${((inputTokens * (costInfo?.inputTokenCost || 0)) / 1_000_000).toFixed(6)}`,
outputTokens: outputTokens.toLocaleString(),
outputCost: `${currency}${((outputTokens * (costInfo?.outputTokenCost || 0)) / 1_000_000).toFixed(6)}`,
});
})
);
}
const inputCostStr = `${currency}${((inputTokens * (costInfo?.inputTokenCost || 0)) / 1_000_000).toFixed(6)}`;

View file

@ -197,7 +197,13 @@ export const InlineEditText: React.FC<InlineEditTextProps> = ({
`}
onClick={handleClick}
onDoubleClick={handleDoubleClick}
title={disabled ? '' : singleClickEdit ? intl.formatMessage(i18n.clickToEdit) : intl.formatMessage(i18n.doubleClickToEdit)}
title={
disabled
? ''
: singleClickEdit
? intl.formatMessage(i18n.clickToEdit)
: intl.formatMessage(i18n.doubleClickToEdit)
}
>
{value || <span className="text-text-subtle italic">{resolvedPlaceholder}</span>}
</div>

View file

@ -59,7 +59,7 @@ interface SearchBarProps {
/**
* SearchBar provides a search input with case-sensitive toggle and result navigation.
*/
export const SearchBar: React.FC<SearchBarProps> = ({
const SearchBar: React.FC<SearchBarProps> = ({
onSearch,
onClose,
onNavigate,

View file

@ -1,18 +0,0 @@
export default function Anthropic({ className = '' }) {
return (
<svg
width="24"
height="24"
viewBox="0 0 1200 1200"
fill="none"
xmlns="http://www.w3.org/2000/svg"
aria-hidden="true"
className={className}
>
<path
d="M 233.959793 800.214905 L 468.644287 668.536987 L 472.590637 657.100647 L 468.644287 650.738403 L 457.208069 650.738403 L 417.986633 648.322144 L 283.892639 644.69812 L 167.597321 639.865845 L 54.926208 633.825623 L 26.577238 627.785339 L 3.3e-05 592.751709 L 2.73832 575.27533 L 26.577238 559.248352 L 60.724873 562.228149 L 136.187973 567.382629 L 249.422867 575.194763 L 331.570496 580.026978 L 453.261841 592.671082 L 472.590637 592.671082 L 475.328857 584.859009 L 468.724915 580.026978 L 463.570557 575.194763 L 346.389313 495.785217 L 219.543671 411.865906 L 153.100723 363.543762 L 117.181267 339.060425 L 99.060455 316.107361 L 91.248367 266.01355 L 123.865784 230.093994 L 167.677887 233.073853 L 178.872513 236.053772 L 223.248367 270.201477 L 318.040283 343.570496 L 441.825592 434.738342 L 459.946411 449.798706 L 467.194672 444.64447 L 468.080597 441.020203 L 459.946411 427.409485 L 392.617493 305.718323 L 320.778564 181.932983 L 288.80542 130.630859 L 280.348999 99.865845 C 277.369171 87.221436 275.194641 76.590698 275.194641 63.624268 L 312.322174 13.20813 L 332.8591 6.604126 L 382.389313 13.20813 L 403.248352 31.328979 L 434.013519 101.71814 L 483.865753 212.537048 L 561.181274 363.221497 L 583.812134 407.919434 L 595.892639 449.315491 L 600.40271 461.959839 L 608.214783 461.959839 L 608.214783 454.711609 L 614.577271 369.825623 L 626.335632 265.61084 L 637.771851 131.516846 L 641.718201 93.745117 L 660.402832 48.483276 L 697.530334 24.000122 L 726.52356 37.852417 L 750.362549 72 L 747.060486 94.067139 L 732.886047 186.201416 L 705.100708 330.52356 L 686.979919 427.167847 L 697.530334 427.167847 L 709.61084 415.087341 L 758.496704 350.174561 L 840.644348 247.490051 L 876.885925 206.738342 L 919.167847 161.71814 L 946.308838 140.29541 L 997.61084 140.29541 L 1035.38269 196.429626 L 1018.469849 254.416199 L 965.637634 321.422852 L 921.825562 378.201538 L 859.006714 462.765259 L 819.785278 530.41626 L 823.409424 535.812073 L 832.75177 534.92627 L 974.657776 504.724915 L 1051.328979 490.872559 L 1142.818848 475.167786 L 1184.214844 494.496582 L 1188.724854 514.147644 L 1172.456421 554.335693 L 1074.604126 578.496765 L 959.838989 601.449829 L 788.939636 641.879272 L 786.845764 643.409485 L 789.261841 646.389343 L 866.255127 653.637634 L 899.194702 655.409424 L 979.812134 655.409424 L 1129.932861 666.604187 L 1169.154419 692.537109 L 1192.671265 724.268677 L 1188.724854 748.429688 L 1128.322144 779.194641 L 1046.818848 759.865845 L 856.590759 714.604126 L 791.355774 698.335754 L 782.335693 698.335754 L 782.335693 703.731567 L 836.69812 756.885986 L 936.322205 846.845581 L 1061.073975 962.81897 L 1067.436279 991.490112 L 1051.409424 1014.120911 L 1034.496704 1011.704712 L 924.885986 929.234924 L 882.604126 892.107544 L 786.845764 811.48999 L 780.483276 811.48999 L 780.483276 819.946289 L 802.550415 852.241699 L 919.087341 1027.409424 L 925.127625 1081.127686 L 916.671204 1098.604126 L 886.469849 1109.154419 L 853.288696 1103.114136 L 785.073914 1007.355835 L 714.684631 899.516785 L 657.906067 802.872498 L 650.979858 806.81897 L 617.476624 1167.704834 L 601.771851 1186.147705 L 565.530212 1200 L 535.328857 1177.046997 L 519.302124 1139.919556 L 535.328857 1066.550537 L 554.657776 970.792053 L 570.362488 894.68457 L 584.536926 800.134277 L 592.993347 768.724976 L 592.429626 766.630859 L 585.503479 767.516968 L 514.22821 865.369263 L 405.825531 1011.865906 L 320.053711 1103.677979 L 299.516815 1111.812256 L 263.919525 1093.369263 L 267.221497 1060.429688 L 287.114136 1031.114136 L 405.825531 880.107361 L 477.422913 786.52356 L 523.651062 732.483276 L 523.328918 724.671265 L 520.590698 724.671265 L 205.288605 929.395935 L 149.154434 936.644409 L 124.993355 914.01355 L 127.973183 876.885986 L 139.409409 864.80542 L 234.201385 799.570435 L 233.879227 799.8927 Z"
fill="currentColor"
/>
</svg>
);
}

View file

@ -1,23 +0,0 @@
interface ArrowRightProps {
className?: string;
}
export function ArrowRight({ className = '' }: ArrowRightProps) {
return (
<svg
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className={className}
>
<path
d="M3.33678 8.00138L12.6701 8.00555M12.6701 8.00555L8.00553 3.3368M12.6701 8.00555L8.00136 12.6701"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
}

View file

@ -1,20 +0,0 @@
export default function Back({ className = '' }) {
return (
<svg
width="1.5rem"
height="1.5rem"
fill="none"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
aria-hidden="true"
className={className}
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M.5 12c0-.413.17-.807.47-1.09l9-8.5a1.5 1.5 0 1 1 2.06 2.18L5.773 10.5H21.5a1.5 1.5 0 1 1 0 3H5.773l6.257 5.91a1.5 1.5 0 1 1-2.06 2.18l-9-8.5A1.5 1.5 0 0 1 .5 12Z"
fill="currentColor"
></path>
</svg>
);
}

View file

@ -1,17 +0,0 @@
export default function ChatGPT({ className = '' }) {
return (
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className={className}
>
<path
d="M22.2819 9.8211a5.9847 5.9847 0 0 0-.5157-4.9108 6.0462 6.0462 0 0 0-6.5098-2.9A6.0651 6.0651 0 0 0 4.9807 4.1818a5.9847 5.9847 0 0 0-3.9977 2.9 6.0462 6.0462 0 0 0 .7427 7.0966 5.98 5.98 0 0 0 .511 4.9107 6.051 6.051 0 0 0 6.5146 2.9001A5.9847 5.9847 0 0 0 13.2599 24a6.0557 6.0557 0 0 0 5.7718-4.2058 5.9894 5.9894 0 0 0 3.9977-2.9001 6.0557 6.0557 0 0 0-.7475-7.0729zm-9.022 12.6081a4.4755 4.4755 0 0 1-2.8764-1.0408l.1419-.0804 4.7783-2.7582a.7948.7948 0 0 0 .3927-.6813v-6.7369l2.02 1.1686a.071.071 0 0 1 .038.052v5.5826a4.504 4.504 0 0 1-4.4945 4.4944zm-9.6607-4.1254a4.4708 4.4708 0 0 1-.5346-3.0137l.142.0852 4.783 2.7582a.7712.7712 0 0 0 .7806 0l5.8428-3.3685v2.3324a.0804.0804 0 0 1-.0332.0615L9.74 19.9502a4.4992 4.4992 0 0 1-6.1408-1.6464zM2.3408 7.8956a4.485 4.485 0 0 1 2.3655-1.9728V11.6a.7664.7664 0 0 0 .3879.6765l5.8144 3.3543-2.0201 1.1685a.0757.0757 0 0 1-.071 0l-4.8303-2.7865A4.504 4.504 0 0 1 2.3408 7.872zm16.5963 3.8558L13.1038 8.364 15.1192 7.2a.0757.0757 0 0 1 .071 0l4.8303 2.7913a4.4944 4.4944 0 0 1-.6765 8.1042v-5.6772a.79.79 0 0 0-.407-.667zm2.0107-3.0231l-.142-.0852-4.7735-2.7818a.7759.7759 0 0 0-.7854 0L9.409 9.2297V6.8974a.0662.0662 0 0 1 .0284-.0615l4.8303-2.7866a4.4992 4.4992 0 0 1 6.6802 4.66zM8.3065 12.863l-2.02-1.1638a.0804.0804 0 0 1-.038-.0567V6.0742a4.4992 4.4992 0 0 1 7.3757-3.4537l-.142.0805L8.704 5.459a.7948.7948 0 0 0-.3927.6813zm1.0976-2.3654l2.602-1.4998 2.6069 1.4998v2.9994l-2.5974 1.4997-2.6067-1.4997Z"
fill="currentColor"
/>
</svg>
);
}

View file

@ -1,18 +0,0 @@
export default function ChatSmart({ className = '' }) {
return (
<svg
fill="none"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
aria-hidden="true"
className={className}
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M18.2625 2.72219C18.3379 2.49299 18.6621 2.49299 18.7375 2.72219L19.5742 5.26646C19.5991 5.34188 19.6582 5.40103 19.7336 5.42584L22.2779 6.2626C22.5071 6.33798 22.5071 6.66219 22.2779 6.73757L19.7336 7.57433C19.6582 7.59914 19.5991 7.65829 19.5742 7.73372L18.7375 10.278C18.6621 10.5072 18.3379 10.5072 18.2625 10.278L17.4258 7.73372C17.4009 7.65829 17.3418 7.59914 17.2664 7.57433L14.7221 6.73757C14.4929 6.66219 14.4929 6.33798 14.7221 6.2626L17.2664 5.42584C17.3418 5.40103 17.4009 5.34188 17.4258 5.26646L18.2625 2.72219ZM1.5 6.00016C1.5 4.89559 2.39543 4.00016 3.5 4.00016H12C12.5523 4.00016 13 4.44788 13 5.00016C13 5.55245 12.5523 6.00016 12 6.00016L3.5 6.00016V16.5002L9.68478 16.5002C10.0951 16.5002 10.4955 16.6264 10.8317 16.8617L14 19.0795V17.5002C14 17.2349 14.1054 16.9806 14.2929 16.7931C14.4804 16.6055 14.7348 16.5002 15 16.5002L20.5 16.5002V11.0002C20.5 10.4479 20.9477 10.0002 21.5 10.0002C22.0523 10.0002 22.5 10.4479 22.5 11.0002V16.5002C22.5 17.6047 21.6046 18.5002 20.5 18.5002L16 18.5002V21.0002C16 21.373 15.7925 21.7149 15.4618 21.8871C15.1311 22.0593 14.732 22.0332 14.4265 21.8194L9.68478 18.5002L3.5 18.5002C2.39543 18.5002 1.5 17.6047 1.5 16.5002V6.00016ZM14.2375 8.72234C14.1621 8.49315 13.8379 8.49315 13.7625 8.72234L13.297 10.1378C13.2722 10.2133 13.213 10.2724 13.1376 10.2972L11.7221 10.7628C11.4929 10.8381 11.4929 11.1623 11.7221 11.2377L13.1376 11.7033C13.213 11.7281 13.2722 11.7872 13.297 11.8626L13.7625 13.2781C13.8379 13.5073 14.1621 13.5073 14.2375 13.2781L14.703 11.8626C14.7278 11.7872 14.787 11.7281 14.8624 11.7033L16.2779 11.2377C16.5071 11.1623 16.5071 10.8381 16.2779 10.7628L14.8624 10.2972C14.787 10.2724 14.7278 10.2133 14.703 10.1378L14.2375 8.72234Z"
fill="currentColor"
/>
</svg>
);
}

View file

@ -1,20 +0,0 @@
export default function ChevronDown({ className }: { className?: string }) {
return (
<svg
width="1.5rem"
height="1.5rem"
fill="none"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
aria-hidden="true"
className={className}
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M12.293 10.293a1 1 0 0 1 1.414 0L18 14.586l4.293-4.293a1 1 0 1 1 1.414 1.414l-5 5a1 1 0 0 1-1.414 0l-5-5a1 1 0 0 1 0-1.414Z"
fill="currentColor"
></path>
</svg>
);
}

View file

@ -1,27 +0,0 @@
import React from 'react';
interface Props extends React.ComponentPropsWithoutRef<'svg'> {
className?: string;
}
export function ChevronRight({ className = '', ...props }: Props) {
return (
<svg
className={className}
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path
d="M9 18L15 12L9 6"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
}

View file

@ -1,20 +0,0 @@
export default function ChevronUp({ className = '' }) {
return (
<svg
width="1.5rem"
height="1.5rem"
fill="none"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
aria-hidden="true"
className={className}
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M17.293 9.293a1 1 0 0 1 1.414 0l5 5a1 1 0 0 1-1.414 1.414L18 11.414l-4.293 4.293a1 1 0 0 1-1.414-1.414l5-5Z"
fill="currentColor"
></path>
</svg>
);
}

View file

@ -1,18 +0,0 @@
export default function Discord({ className = '' }) {
return (
<svg
width="1.5rem"
height="1.5rem"
fill="none"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 640 512"
aria-hidden="true"
className={className}
>
<path
d="M524.5 69.8a1.5 1.5 0 0 0 -.8-.7A485.1 485.1 0 0 0 404.1 32a1.8 1.8 0 0 0 -1.9 .9 337.5 337.5 0 0 0 -14.9 30.6 447.8 447.8 0 0 0 -134.4 0 309.5 309.5 0 0 0 -15.1-30.6 1.9 1.9 0 0 0 -1.9-.9A483.7 483.7 0 0 0 116.1 69.1a1.7 1.7 0 0 0 -.8 .7C39.1 183.7 18.2 294.7 28.4 404.4a2 2 0 0 0 .8 1.4A487.7 487.7 0 0 0 176 479.9a1.9 1.9 0 0 0 2.1-.7A348.2 348.2 0 0 0 208.1 430.4a1.9 1.9 0 0 0 -1-2.6 321.2 321.2 0 0 1 -45.9-21.9 1.9 1.9 0 0 1 -.2-3.1c3.1-2.3 6.2-4.7 9.1-7.1a1.8 1.8 0 0 1 1.9-.3c96.2 43.9 200.4 43.9 295.5 0a1.8 1.8 0 0 1 1.9 .2c2.9 2.4 6 4.9 9.1 7.2a1.9 1.9 0 0 1 -.2 3.1 301.4 301.4 0 0 1 -45.9 21.8 1.9 1.9 0 0 0 -1 2.6 391.1 391.1 0 0 0 30 48.8 1.9 1.9 0 0 0 2.1 .7A486 486 0 0 0 610.7 405.7a1.9 1.9 0 0 0 .8-1.4C623.7 277.6 590.9 167.5 524.5 69.8zM222.5 337.6c-29 0-52.8-26.6-52.8-59.2S193.1 219.1 222.5 219.1c29.7 0 53.3 26.8 52.8 59.2C275.3 311 251.9 337.6 222.5 337.6zm195.4 0c-29 0-52.8-26.6-52.8-59.2S388.4 219.1 417.9 219.1c29.7 0 53.3 26.8 52.8 59.2C470.7 311 447.5 337.6 417.9 337.6z"
fill="currentColor"
/>
</svg>
);
}

View file

@ -1,20 +0,0 @@
export default function Document({ className = '' }) {
return (
<svg
width="1.5rem"
height="1.5rem"
fill="none"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
aria-hidden="true"
className={className}
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M14.546 4.503a6.26 6.26 0 0 0-.264-.003H6.6c-.297 0-.459 0-.575.01l-.014.001v.014C6 4.64 6 4.804 6 5.1v13.8c0 .297 0 .459.01.575l.001.014h.014c.116.01.278.011.575.011h10.8c.297 0 .459 0 .575-.01l.014-.001v-.014c.01-.116.011-.278.011-.575V8.608c0-.12 0-.18-.002-.224v-.003l-.002-.002a6.215 6.215 0 0 0-.147-.17l-3.12-3.508a6.24 6.24 0 0 0-.177-.195l-.002-.003h-.004Zm-.21-2.003c.206 0 .464-.001.716.064a2 2 0 0 1 .613.275c.216.145.387.339.523.492l.037.042 3.118 3.508.032.035c.116.13.263.295.372.49.094.17.164.352.206.542.048.218.047.438.047.613v10.371c0 .252 0 .498-.017.706a2.022 2.022 0 0 1-.201.77 2 2 0 0 1-.874.874 2.022 2.022 0 0 1-.77.201 9.144 9.144 0 0 1-.706.017H6.568c-.252 0-.498 0-.706-.017a2.022 2.022 0 0 1-.77-.2 2 2 0 0 1-.874-.875 2.021 2.021 0 0 1-.201-.77C4 19.43 4 19.184 4 18.932V5.068c0-.252 0-.498.017-.706.019-.229.063-.499.201-.77a2 2 0 0 1 .874-.874c.271-.138.541-.182.77-.201.208-.017.454-.017.706-.017h7.769ZM8 14a1 1 0 0 1 1-1h6a1 1 0 1 1 0 2H9a1 1 0 0 1-1-1Zm1-6a1 1 0 0 0 0 2h4a1 1 0 1 0 0-2H9Z"
fill="currentColor"
></path>
</svg>
);
}

View file

@ -1,20 +0,0 @@
export default function Idea({ className = '' }) {
return (
<svg
width="1.5rem"
height="1.5rem"
fill="none"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
aria-hidden="true"
className={className}
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M12 3C7.67283 3 4.98694 7.96755 7.41702 11.7635L8.84879 14L15.1514 14L16.5831 11.7633C19.013 7.96731 16.3271 3 12 3ZM5.73261 12.8418C2.49843 7.78981 6.00376 1 12 1C17.9961 1 21.5015 7.78948 18.2676 12.8415L16.7382 15.2309C16.4351 15.7043 15.9116 16 15.339 16L8.66122 16C8.0886 16 7.56511 15.7043 7.26208 15.2309L5.73261 12.8418ZM8.00002 18C8.00002 17.4477 8.44774 17 9.00002 17H12H15C15.5523 17 16 17.4477 16 18C16 18.5523 15.5523 19 15 19H12H9.00002C8.44774 19 8.00002 18.5523 8.00002 18ZM10 20C9.44774 20 9.00003 20.4477 9.00003 21C9.00003 21.5523 9.44774 22 10 22H12H14C14.5523 22 15 21.5523 15 21C15 20.4477 14.5523 20 14 20H12H10Z"
fill="currentColor"
/>
</svg>
);
}

View file

@ -1,35 +0,0 @@
interface KeyProps {
className?: string;
}
export function Key({ className = '' }: KeyProps) {
return (
<svg
width="16"
height="17"
viewBox="0 0 16 17"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className={className}
>
<g clipPath="url(#clip0_986_19974)">
<path
d="M10.3382 5.68265L11.8709 7.21667C11.9954 7.33888 12.1629 7.40737 12.3374 7.40745C12.5119 7.40753 12.6795 7.33918 12.8042 7.21709L14.2048 5.81771C14.327 5.69315 14.3955 5.52563 14.3956 5.35113C14.3957 5.17663 14.3273 5.00905 14.2052 4.88438L12.6726 3.35036M14.0064 2.01762L7.60358 8.41476M8.66915 11.0152C8.66825 13.0403 7.02589 14.6812 5.00085 14.6803C2.9758 14.6794 1.33492 13.037 1.33582 11.012C1.33673 8.98692 2.97908 7.34603 5.00413 7.34693C7.02917 7.34784 8.67006 8.9902 8.66915 11.0152Z"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
/>
</g>
<defs>
<clipPath id="clip0_986_19974">
<rect
width="16"
height="16"
fill="white"
transform="translate(0.00708008 0.677979) rotate(0.0256089)"
/>
</clipPath>
</defs>
</svg>
);
}

View file

@ -1,18 +0,0 @@
export default function LinkedIn({ className = '' }) {
return (
<svg
width="1.5rem"
height="1.5rem"
fill="none"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 448 512"
aria-hidden="true"
className={className}
>
<path
d="M416 32H31.9C14.3 32 0 46.5 0 64.3v383.4C0 465.5 14.3 480 31.9 480H416c17.6 0 32-14.5 32-32.3V64.3c0-17.8-14.4-32.3-32-32.3zM135.4 416H69V202.2h66.5V416zm-33.2-243c-21.3 0-38.5-17.3-38.5-38.5S80.9 96 102.2 96c21.2 0 38.5 17.3 38.5 38.5 0 21.3-17.2 38.5-38.5 38.5zm282.1 243h-66.4V312c0-24.8-.5-56.7-34.5-56.7-34.6 0-39.9 27-39.9 54.9V416h-66.4V202.2h63.7v29.2h.9c8.9-16.8 30.6-34.5 62.9-34.5 67.2 0 79.7 44.3 79.7 101.9V416z"
fill="currentColor"
/>
</svg>
);
}

View file

@ -1,18 +0,0 @@
export default function More({ className = '' }) {
return (
<svg
fill="none"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
aria-hidden="true"
className={className}
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M5 14a2 2 0 1 1 .001-4.001A2 2 0 0 1 5 14Zm7 0a2 2 0 1 1 .001-4.001A2 2 0 0 1 12 14Zm5-2a2 2 0 1 0 4.001-.001A2 2 0 0 0 17 12Z"
fill="currentColor"
></path>
</svg>
);
}

View file

@ -1,45 +0,0 @@
export default function Ollama({ className = '' }) {
return (
<svg
width="69"
height="88"
viewBox="0 0 69 88"
fill="none"
xmlns="http://www.w3.org/2000/svg"
aria-hidden="true"
className={className}
>
<path
d="M65 85L62.0637 70.8077C62.0222 70.6074 62.0432 70.3992 62.1238 70.2112L64.3359 65.0497C65.7194 61.8214 65.7378 58.1708 64.387 54.9287L62.4897 50.3753C61.8577 48.8586 61.8754 47.149 62.5386 45.6457L64.1296 42.0397C66.8761 35.8141 64.3334 28.5656 58.0958 25.8464C55.8537 24.869 53.319 23.8076 50.5427 22.7131C50.1848 22.572 49.843 22.3777 49.5389 22.1421C46.6697 19.9189 44.1695 18.0613 42.1059 16.5691C38.1157 13.6838 32.8534 13.645 28.9759 16.68C27.1886 18.0789 25.0635 19.8198 22.638 21.9398C22.2177 22.3071 21.7135 22.5905 21.1813 22.7586C18.6462 23.5593 16.4068 24.2677 14.4298 24.8938C6.83415 27.2996 3.28234 35.8919 6.92194 42.9796L8.28723 45.6383C9.05963 47.1424 9.16151 48.9027 8.5678 50.4859L6.65444 55.5882C5.5855 58.4387 5.55086 61.5739 6.55656 64.4473L8.42485 69.7853C8.47432 69.9266 8.49142 70.0773 8.47488 70.2261L7 83.5"
stroke="currentColor"
strokeWidth="5"
strokeLinecap="round"
/>
<circle cx="18.5" cy="40.5" r="3" fill="currentColor" />
<circle cx="52.5" cy="40.5" r="3" fill="currentColor" />
<path
d="M35.5 38C42.1664 38 47 42.3128 47 47C47 51.6872 42.1664 56 35.5 56C28.8336 56 24 51.6872 24 47C24 42.3128 28.8336 38 35.5 38Z"
stroke="currentColor"
strokeWidth="3"
/>
<path
d="M33.5 45L35.5 47M35.5 47L37.5 45M35.5 47V50"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
/>
<path
d="M25 20L23.8142 14.0708C23.2738 11.369 22.3756 8.75127 21.1434 6.28682L19.6522 3.30442C18.9533 1.90667 16.9727 1.86785 16.2196 3.23714V3.23714C13.7795 7.67371 12.5 12.6549 12.5 17.7183V26"
stroke="currentColor"
strokeWidth="4"
strokeLinecap="round"
/>
<path
d="M46.5 20.5L47.7083 14.6911C48.2867 11.9102 49.2445 9.22205 50.5549 6.70202L52.1438 3.64663C52.8504 2.28778 54.7805 2.24912 55.5409 3.57858V3.57858C58.1353 8.11412 59.5 13.2486 59.5 18.4737V26.5"
stroke="currentColor"
strokeWidth="4"
strokeLinecap="round"
/>
</svg>
);
}

View file

@ -1,17 +0,0 @@
export default function OpenAI({ className = '' }) {
return (
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className={className}
>
<path
d="M22.2819 9.8211a5.9847 5.9847 0 0 0-.5157-4.9108 6.0462 6.0462 0 0 0-6.5098-2.9A6.0651 6.0651 0 0 0 4.9807 4.1818a5.9847 5.9847 0 0 0-3.9977 2.9 6.0462 6.0462 0 0 0 .7427 7.0966 5.98 5.98 0 0 0 .511 4.9107 6.051 6.051 0 0 0 6.5146 2.9001A5.9847 5.9847 0 0 0 13.2599 24a6.0557 6.0557 0 0 0 5.7718-4.2058 5.9894 5.9894 0 0 0 3.9977-2.9001 6.0557 6.0557 0 0 0-.7475-7.0729zm-9.022 12.6081a4.4755 4.4755 0 0 1-2.8764-1.0408l.1419-.0804 4.7783-2.7582a.7948.7948 0 0 0 .3927-.6813v-6.7369l2.02 1.1686a.071.071 0 0 1 .038.052v5.5826a4.504 4.504 0 0 1-4.4945 4.4944zm-9.6607-4.1254a4.4708 4.4708 0 0 1-.5346-3.0137l.142.0852 4.783 2.7582a.7712.7712 0 0 0 .7806 0l5.8428-3.3685v2.3324a.0804.0804 0 0 1-.0332.0615L9.74 19.9502a4.4992 4.4992 0 0 1-6.1408-1.6464zM2.3408 7.8956a4.485 4.485 0 0 1 2.3655-1.9728V11.6a.7664.7664 0 0 0 .3879.6765l5.8144 3.3543-2.0201 1.1685a.0757.0757 0 0 1-.071 0l-4.8303-2.7865A4.504 4.504 0 0 1 2.3408 7.872zm16.5963 3.8558L13.1038 8.364 15.1192 7.2a.0757.0757 0 0 1 .071 0l4.8303 2.7913a4.4944 4.4944 0 0 1-.6765 8.1042v-5.6772a.79.79 0 0 0-.407-.667zm2.0107-3.0231l-.142-.0852-4.7735-2.7818a.7759.7759 0 0 0-.7854 0L9.409 9.2297V6.8974a.0662.0662 0 0 1 .0284-.0615l4.8303-2.7866a4.4992 4.4992 0 0 1 6.6802 4.66zM8.3065 12.863l-2.02-1.1638a.0804.0804 0 0 1-.038-.0567V6.0742a4.4992 4.4992 0 0 1 7.3757-3.4537l-.142.0805L8.704 5.459a.7948.7948 0 0 0-.3927.6813zm1.0976-2.3654l2.602-1.4998 2.6069 1.4998v2.9994l-2.5974 1.4997-2.6067-1.4997Z"
fill="currentColor"
/>
</svg>
);
}

View file

@ -1,44 +0,0 @@
export default function OpenRouter({ className = '' }) {
return (
<svg
width="202"
height="171"
viewBox="0 0 202 171"
fill="none"
xmlns="http://www.w3.org/2000/svg"
aria-hidden="true"
className={className}
>
<path
d="M2.20001 85.5004C8.10001 85.5004 30.8 80.4004 42.5 73.7004C54.3 67.1004 54.3 67.1004 78.6 49.8004C109.4 28.0004 131.1 35.3004 166.8 35.3004"
fill="currentColor"
/>
<path
d="M2.20001 85.5004C8.10001 85.5004 30.8 80.4004 42.5 73.7004C54.3 67.1004 54.3 67.1004 78.6 49.8004C109.4 28.0004 131.1 35.3004 166.8 35.3004"
stroke="currentColor"
strokeWidth="35.3"
/>
<path
d="M201.3 35.5002L141 70.3002V0.700195L201.3 35.5002Z"
fill="currentColor"
stroke="currentColor"
strokeWidth="0.4"
/>
<path
d="M1 85.5C6.9 85.5 29.6 90.5 41.4 97.2C53.1 103.9 53.1 103.9 77.4 121.2C108.2 143 129.9 135.7 165.6 135.7"
fill="currentColor"
/>
<path
d="M1 85.5C6.9 85.5 29.6 90.5 41.4 97.2C53.1 103.9 53.1 103.9 77.4 121.2C108.2 143 129.9 135.7 165.6 135.7"
stroke="currentColor"
strokeWidth="35.3"
/>
<path
d="M200.1 135.4L139.8 100.7V170.2L200.1 135.4Z"
fill="currentColor"
stroke="currentColor"
strokeWidth="0.4"
/>
</svg>
);
}

View file

@ -1,18 +0,0 @@
export default function Refresh({ className = '' }) {
return (
<svg
fill="none"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
aria-hidden="true"
className={className}
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M14.1508 1.74076C13.7315 1.38134 13.1002 1.4299 12.7407 1.84923C12.3813 2.26855 12.4299 2.89985 12.8492 3.25927L14.2968 4.50004H11.5001C6.8057 4.50004 3.00012 8.30564 3.00012 13.0001C3.00012 17.6945 6.80568 21.5 11.5001 21.5C16.1945 21.5 20.0001 17.6944 20.0001 13C20.0001 12.4477 19.5523 12 19.0001 12C18.4478 12 18.0001 12.4477 18.0001 13C18.0001 16.5899 15.0899 19.5 11.5001 19.5C7.91026 19.5 5.00012 16.5899 5.00012 13.0001C5.00012 9.4102 7.91027 6.50004 11.5001 6.50004H14.2967L12.8492 7.74076C12.4299 8.10018 12.3813 8.73148 12.7407 9.15081C13.1002 9.57014 13.7315 9.6187 14.1508 9.25927L17.6417 6.26706C17.8608 6.08362 18.0001 5.8081 18.0001 5.50004C18.0001 5.19294 17.8616 4.91817 17.6438 4.73474L14.1508 1.74076Z"
fill="currentColor"
/>
</svg>
);
}

View file

@ -1,18 +0,0 @@
export default function Send({ className = '' }) {
return (
<svg
fill="none"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
aria-hidden="true"
className={className}
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M20.707 3.293a1 1 0 0 1 .25.994l-5.4 18a1 1 0 0 1-1.886.084l-3.44-8.602-8.602-3.44a1 1 0 0 1 .084-1.887l18-5.4a1 1 0 0 1 .994.25Zm-8.534 9.948 2.292 5.729L18.509 5.49 5.03 9.535l5.73 2.292 1.333-1.334a1 1 0 0 1 1.414 1.414l-1.334 1.334Z"
fill="currentColor"
></path>
</svg>
);
}

View file

@ -1,20 +0,0 @@
export default function SensitiveHidden({ className = '' }) {
return (
<svg
width="1.5rem"
height="1.5rem"
fill="none"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
aria-hidden="true"
className={className}
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M19.731 5.182a1 1 0 1 0-1.462-1.364l-2.71 2.903C14.471 6.287 13.271 6 12 6 9.608 6 7.469 7.018 5.849 8.114a18.644 18.644 0 0 0-3.333 2.916 1.45 1.45 0 0 0 0 1.94 18.644 18.644 0 0 0 3.333 2.916c.231.157.473.312.725.462l-2.305 2.47a1 1 0 0 0 1.462 1.364l2.71-2.903C9.529 17.713 10.729 18 12 18c2.392 0 4.531-1.018 6.151-2.114a18.644 18.644 0 0 0 3.333-2.916 1.45 1.45 0 0 0 0-1.94 18.644 18.644 0 0 0-3.333-2.916 15.453 15.453 0 0 0-.725-.462l2.305-2.47ZM14.07 8.317A7.21 7.21 0 0 0 12 8c-1.833 0-3.58.789-5.03 1.77A16.633 16.633 0 0 0 4.343 12c.55.575 1.467 1.445 2.627 2.23.32.217.657.425 1.005.617l6.094-6.53Zm-4.138 7.366 6.094-6.53c.348.192.684.4 1.006.617A16.631 16.631 0 0 1 19.657 12a16.633 16.633 0 0 1-2.627 2.23C15.58 15.21 13.834 16 12 16c-.707 0-1.4-.117-2.07-.317Z"
fill="currentColor"
></path>
</svg>
);
}

View file

@ -1,20 +0,0 @@
export default function SensitiveVisible({ className = '' }) {
return (
<svg
width="1.5rem"
height="1.5rem"
fill="none"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
aria-hidden="true"
className={className}
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M6.97 14.23A16.635 16.635 0 0 1 4.343 12 16.633 16.633 0 0 1 6.97 9.77C8.42 8.79 10.167 8 12 8c1.833 0 3.58.789 5.03 1.77A16.631 16.631 0 0 1 19.657 12a16.633 16.633 0 0 1-2.627 2.23C15.58 15.21 13.834 16 12 16c-1.833 0-3.58-.789-5.03-1.77ZM12 6C9.608 6 7.469 7.018 5.849 8.114a18.644 18.644 0 0 0-3.333 2.916 1.45 1.45 0 0 0 0 1.94 18.644 18.644 0 0 0 3.333 2.916C7.469 16.982 9.609 18 12 18c2.392 0 4.531-1.018 6.151-2.114a18.644 18.644 0 0 0 3.333-2.916 1.45 1.45 0 0 0 0-1.94 18.644 18.644 0 0 0-3.333-2.916C16.531 7.018 14.392 6 12 6Zm0 8a2 2 0 1 0 0-4 2 2 0 0 0 0 4Z"
fill="currentColor"
></path>
</svg>
);
}

View file

@ -1,18 +0,0 @@
export default function Settings({ className = '' }) {
return (
<svg
fill="none"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
aria-hidden="true"
className={className}
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M10.6326 3.5C10.3981 3.5 10.1862 3.69609 10.1862 3.96499C10.1862 4.98498 9.56936 5.81161 8.80634 6.2491C8.77137 6.26915 8.73659 6.28953 8.702 6.31022C7.94634 6.76246 6.91891 6.89623 6.03237 6.3778C5.82611 6.25719 5.55643 6.3235 5.4295 6.54618L4.06296 8.94353C3.93162 9.17396 4.0146 9.46279 4.22888 9.5881C5.11053 10.1037 5.50474 11.0605 5.49681 11.938L5.49653 12L5.49681 12.062C5.50475 12.9395 5.11053 13.8963 4.22888 14.4119C4.0146 14.5372 3.93162 14.8261 4.06296 15.0565L5.42949 17.4538C5.55643 17.6765 5.82612 17.7428 6.03237 17.6222L6.53717 18.4854L6.03237 17.6222C6.91892 17.1038 7.94635 17.2375 8.70202 17.6898L8.70203 17.6898C8.73658 17.7105 8.77135 17.7308 8.80635 17.7509C9.56936 18.1884 10.1862 19.015 10.1862 20.035C10.1862 20.3039 10.3981 20.5 10.6326 20.5H13.3674C13.6019 20.5 13.8138 20.3039 13.8138 20.035C13.8138 19.015 14.4306 18.1884 15.1937 17.7509L15.6911 18.6184L15.1936 17.7509C15.1937 17.7509 15.1937 17.7509 15.1937 17.7509C15.2287 17.7308 15.2634 17.7105 15.298 17.6898L15.8115 18.5479L15.298 17.6898C16.0536 17.2375 17.0811 17.1038 17.9676 17.6222C18.1739 17.7428 18.4436 17.6765 18.5705 17.4538L19.937 15.0565C20.0684 14.8261 19.9854 14.5372 19.7711 14.4119C18.8895 13.8963 18.4953 12.9395 18.5032 12.062V12.062L18.5034 12.0265L19.497 12.0302L18.5034 12.0264L18.5035 12L18.5032 11.938V11.938C18.4953 11.0605 18.8895 10.1037 19.7711 9.5881C19.9854 9.46279 20.0684 9.17396 19.937 8.94354L18.5705 6.54618C18.4436 6.3235 18.1739 6.25719 17.9676 6.3778C17.0811 6.89623 16.0537 6.76246 15.298 6.31022L15.8115 5.45215L15.298 6.31022C15.2634 6.28953 15.2286 6.26915 15.1937 6.2491C14.4306 5.81161 13.8138 4.98498 13.8138 3.96499C13.8138 3.69609 13.6019 3.5 13.3674 3.5H10.6326ZM8.1862 3.96499C8.1862 2.6157 9.26947 1.5 10.6326 1.5H13.3674C14.7305 1.5 15.8138 2.6157 15.8138 3.96499C15.8138 4.13868 15.9259 4.36351 16.1885 4.51406C16.2343 4.54032 16.2798 4.56699 16.325 4.59407C16.5852 4.74977 16.8228 4.73044 16.958 4.65133C18.1358 3.96259 19.6359 4.37659 20.308 5.55575L21.6746 7.95311C22.3428 9.12533 21.9513 10.6301 20.7807 11.3146C20.6417 11.3959 20.5002 11.6037 20.5031 11.9199C20.5033 11.9466 20.5035 11.9733 20.5035 12L20.5034 12.0339V12.034L20.5031 12.0801C20.5002 12.3963 20.6417 12.6042 20.7807 12.6854C21.9513 13.37 22.3428 14.8747 21.6746 16.0469L20.308 18.4443C19.6359 19.6234 18.1358 20.0374 16.958 19.3487C16.8227 19.2696 16.5852 19.2502 16.325 19.4059C16.2798 19.433 16.2342 19.4597 16.1885 19.4859L15.7151 18.6603L16.1885 19.4859C15.9259 19.6365 15.8138 19.8613 15.8138 20.035C15.8138 21.3843 14.7305 22.5 13.3674 22.5H10.6326C9.26947 22.5 8.1862 21.3843 8.1862 20.035C8.1862 19.8613 8.0741 19.6365 7.81152 19.4859C7.76575 19.4597 7.72023 19.433 7.67497 19.4059C7.41481 19.2502 7.17725 19.2696 7.04197 19.3487C5.86418 20.0374 4.36409 19.6234 3.69195 18.4443L2.32542 16.0469C1.65723 14.8747 2.04873 13.37 3.21928 12.6854C3.35829 12.6042 3.49975 12.3963 3.49689 12.0801L4.49685 12.0711L3.49689 12.0801C3.49665 12.0534 3.49653 12.0267 3.49653 12C3.49653 11.9733 3.49665 11.9466 3.49689 11.9199C3.49975 11.6037 3.35829 11.3959 3.21928 11.3146C2.04873 10.6301 1.65724 9.12533 2.32542 7.95311L3.69195 5.55575C4.3641 4.37659 5.86418 3.96259 7.04197 4.65133C7.17724 4.73044 7.4148 4.74977 7.67495 4.59407L8.18848 5.45215L7.67495 4.59407C7.7202 4.56699 7.76573 4.54032 7.81152 4.51406C8.0741 4.36351 8.1862 4.13868 8.1862 3.96499ZM12 9.2C10.4923 9.2 9.24826 10.4415 9.24826 12C9.24826 13.5585 10.4923 14.8 12 14.8C13.5077 14.8 14.7517 13.5585 14.7517 12C14.7517 10.4415 13.5077 9.2 12 9.2ZM7.24826 12C7.24826 9.36112 9.36367 7.2 12 7.2C14.6363 7.2 16.7517 9.36112 16.7517 12C16.7517 14.6389 14.6363 16.8 12 16.8C9.36367 16.8 7.24826 14.6389 7.24826 12Z"
fill="currentColor"
/>
</svg>
);
}

View file

@ -1,22 +0,0 @@
export default function Tetrate({ className = '' }) {
return (
<svg
width="24"
height="24"
viewBox="0 0 694 701"
fill="none"
xmlns="http://www.w3.org/2000/svg"
aria-hidden="true"
className={className}
>
<path
d="M343.5 226C390.72 226 429 264.28 429 311.5C429 321.676 427.222 331.436 423.961 340.487C409.127 381.654 375 419.742 375 463.499C375 507.257 409.127 545.346 423.961 586.512C427.222 595.564 429 605.324 429 615.5C429 662.72 390.72 701 343.5 701C296.28 701 258 662.72 258 615.5C258 606.44 259.409 597.709 262.021 589.514C275.211 548.119 308 509.828 308 466.382V460.616C308 417.171 275.211 378.88 262.021 337.485C259.409 329.291 258 320.56 258 311.5C258 264.28 296.28 226 343.5 226Z"
fill="currentColor"
/>
<path
d="M272.622 39.9541C297.034 1.458 347.7 -11.255 387.546 11.75C395.392 16.2801 402.249 21.8668 408.04 28.2256C437.293 60.3466 454.06 107.888 491.685 129.61L496.678 132.493C534.303 154.216 583.859 144.966 626.303 154.239C634.705 156.075 642.971 159.22 650.817 163.75C691.711 187.36 705.722 239.652 682.112 280.546C658.502 321.44 606.211 335.451 565.317 311.841C556.505 306.753 548.941 300.333 542.733 292.982C514.499 259.553 498.577 210.955 460.682 189.076C436.139 174.906 406.497 174.321 376.956 173.566C358.811 173.102 340.613 173.735 322.491 174.754C290.52 176.551 257.865 176.779 231.635 192.853C194.326 215.717 179.682 264.716 152.333 298.874C146.319 306.385 138.926 313 130.25 318.317C89.9879 342.99 37.3483 330.353 12.6754 290.091C-11.9974 249.829 0.639271 197.189 40.901 172.517C48.6259 167.783 56.8065 164.422 65.1578 162.367C107.345 151.985 157.126 159.934 194.17 137.233L199.086 134.221C232.774 113.576 248.658 72.037 272.622 39.9541Z"
fill="currentColor"
/>
</svg>
);
}

View file

@ -1,18 +0,0 @@
export default function Time({ className = '' }) {
return (
<svg
fill="none"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
aria-hidden="true"
className={className}
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M6.34315 6.34314C9.46734 3.21895 14.5327 3.21895 17.6569 6.34315C20.781 9.46734 20.781 14.5327 17.6569 17.6569C14.5327 20.781 9.46734 20.781 6.34315 17.6569C3.21895 14.5327 3.21895 9.46734 6.34315 6.34314ZM19.0711 4.92893C15.1658 1.02369 8.83417 1.02369 4.92893 4.92893C1.02369 8.83417 1.02369 15.1658 4.92893 19.0711C8.83418 22.9763 15.1658 22.9763 19.0711 19.0711C22.9763 15.1658 22.9763 8.83418 19.0711 4.92893ZM13 8.5C13 7.94772 12.5523 7.5 12 7.5C11.4477 7.5 11 7.94772 11 8.5V13C11 13.5523 11.4477 14 12 14H15.5C16.0523 14 16.5 13.5523 16.5 13C16.5 12.4477 16.0523 12 15.5 12H13V8.5Z"
fill="currentColor"
/>
</svg>
);
}

View file

@ -1,18 +0,0 @@
export default function Youtube({ className = '' }) {
return (
<svg
width="1.5rem"
height="1.5rem"
fill="none"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 576 512"
aria-hidden="true"
className={className}
>
<path
d="M549.7 124.1c-6.3-23.7-24.8-42.3-48.3-48.6C458.8 64 288 64 288 64S117.2 64 74.6 75.5c-23.5 6.3-42 24.9-48.3 48.6-11.4 42.9-11.4 132.3-11.4 132.3s0 89.4 11.4 132.3c6.3 23.7 24.8 41.5 48.3 47.8C117.2 448 288 448 288 448s170.8 0 213.4-11.5c23.5-6.3 42-24.2 48.3-47.8 11.4-42.9 11.4-132.3 11.4-132.3s0-89.4-11.4-132.3zm-317.5 213.5V175.2l142.7 81.2-142.7 81.2z"
fill="currentColor"
/>
</svg>
);
}

View file

@ -1,51 +1,25 @@
import ArrowDown from './ArrowDown';
import ArrowUp from './ArrowUp';
import Attach from './Attach';
import Back from './Back';
import ChatGPT from './ChatGPT';
import { Bird1 } from './Bird1';
import { Bird2 } from './Bird2';
import { Bird3 } from './Bird3';
import { Bird4 } from './Bird4';
import { Bird5 } from './Bird5';
import { Bird6 } from './Bird6';
import ChatSmart from './ChatSmart';
import Check from './Check';
import ChevronDown from './ChevronDown';
import ChevronUp from './ChevronUp';
import { ChevronRight } from './ChevronRight';
import Close from './Close';
import { CodeXml } from './CodeXml';
import { Cog } from './Cog';
import CoinIcon from './CoinIcon';
import Copy from './Copy';
import Discord from './Discord';
import Document from './Document';
import Edit from './Edit';
import { Fuel } from './Fuel';
import { GalleryHorizontalEnd } from './GalleryHorizontalEnd';
import { Gavel } from './Gavel';
import { GlassWater } from './GlassWater';
import { Grape } from './Grape';
import Idea from './Idea';
import LinkedIn from './LinkedIn';
import More from './More';
import OpenRouter from './OpenRouter';
import Ollama from './Ollama';
import Refresh from './Refresh';
import SensitiveHidden from './SensitiveHidden';
import SensitiveVisible from './SensitiveVisible';
import Send from './Send';
import Settings from './Settings';
import Time from './Time';
import { Gear } from './Gear';
import Youtube from './Youtube';
import { Goose } from './Goose';
import Anthropic from './Anthropic';
import { ArrowRight } from './ArrowRight';
import { Key } from './Key';
import OpenAI from './OpenAI';
import Tetrate from './Tetrate';
import { Microphone } from './Microphone';
import { Watch0 } from './Watch0';
import { Watch1 } from './Watch1';
@ -59,45 +33,25 @@ export {
ArrowDown,
ArrowUp,
Attach,
Back,
ChatGPT,
Bird1,
Bird2,
Bird3,
Bird4,
Bird5,
Bird6,
ChatSmart,
Check,
ChevronDown,
ChevronRight,
ChevronUp,
Close,
CodeXml,
Cog,
CoinIcon,
Copy,
Discord,
Document,
Edit,
Fuel,
GalleryHorizontalEnd,
Gavel,
Gear,
GlassWater,
Grape,
Idea,
LinkedIn,
Microphone,
More,
OpenRouter,
Ollama,
Refresh,
SensitiveHidden,
SensitiveVisible,
Send,
Settings,
Time,
Watch0,
Watch1,
Watch2,
@ -105,11 +59,5 @@ export {
Watch4,
Watch5,
Watch6,
Youtube,
Goose,
Anthropic,
ArrowRight,
Key,
OpenAI,
Tetrate,
};

View file

@ -1,16 +0,0 @@
export const Harddrive = ({ className }: { className?: string }) => (
<svg
width="11"
height="11"
viewBox="0 0 11 11"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className={className}
>
<rect width="11" height="11" rx="2" fill="#1E1E20" />
<path
d="M5 3.5C5 4.32843 4.32843 5 3.5 5C2.67157 5 2 4.32843 2 3.5C2 2.67157 2.67157 2 3.5 2C4.32843 2 5 2.67157 5 3.5Z"
fill="white"
/>
</svg>
);

View file

@ -8,7 +8,6 @@ export { FileEdit } from './FileEdit';
export { FilePlus } from './FilePlus';
export { FileText } from './FileText';
export { Globe } from './Globe';
export { Harddrive } from './Harddrive';
export { Monitor } from './Monitor';
export { Numbers } from './Numbers';
export { Save } from './Save';

View file

@ -61,7 +61,8 @@ const i18n = defineMessages({
},
localModelsNote: {
id: 'localModelPicker.localModelsNote',
defaultMessage: 'Local models keep everything on your machine for full privacy. Performance and context window size may vary compared to cloud providers depending on your hardware and model size.',
defaultMessage:
'Local models keep everything on your machine for full privacy. Performance and context window size may vary compared to cloud providers depending on your hardware and model size.',
},
failedToLoad: {
id: 'localModelPicker.failedToLoad',
@ -310,7 +311,9 @@ export default function LocalModelPicker({ onConfigured }: LocalModelPickerProps
onClick={() => setShowAllModels(!showAllModels)}
className="text-sm text-blue-500 hover:text-blue-400 transition-colors flex items-center gap-1"
>
{showAllModels ? intl.formatMessage(i18n.hideOtherSizes) : intl.formatMessage(i18n.showOtherSizes, { count: otherModels.length })}
{showAllModels
? intl.formatMessage(i18n.hideOtherSizes)
: intl.formatMessage(i18n.showOtherSizes, { count: otherModels.length })}
<svg
className={`w-3.5 h-3.5 transition-transform ${showAllModels ? 'rotate-180' : ''}`}
fill="none"
@ -376,10 +379,12 @@ export default function LocalModelPicker({ onConfigured }: LocalModelPickerProps
{selectedModel?.status.state === 'Downloaded'
? intl.formatMessage(i18n.useModel, { modelId: selectedModel.id })
: selectedModel
? intl.formatMessage(i18n.downloadModel, { modelId: selectedModel.id, size: formatSize(selectedModel.sizeBytes) })
? intl.formatMessage(i18n.downloadModel, {
modelId: selectedModel.id,
size: formatSize(selectedModel.sizeBytes),
})
: intl.formatMessage(i18n.selectModel)}
</button>
</div>
)}
@ -427,7 +432,9 @@ export default function LocalModelPicker({ onConfigured }: LocalModelPickerProps
) : (
<div className="flex items-center gap-3">
<div className="animate-spin rounded-full h-4 w-4 border-t-2 border-b-2 border-text-muted"></div>
<span className="text-sm text-text-muted">{intl.formatMessage(i18n.startingDownload)}</span>
<span className="text-sm text-text-muted">
{intl.formatMessage(i18n.startingDownload)}
</span>
</div>
)}
</div>

View file

@ -106,7 +106,12 @@ export default function OnboardingGuard({ children }: OnboardingGuardProps) {
}, []);
useEffect(() => {
if (!isCheckingProvider && !hasProvider && !checkProviderError && !hasTrackedOnboardingStart.current) {
if (
!isCheckingProvider &&
!hasProvider &&
!checkProviderError &&
!hasTrackedOnboardingStart.current
) {
trackOnboardingStarted();
hasTrackedOnboardingStart.current = true;
}
@ -152,11 +157,13 @@ export default function OnboardingGuard({ children }: OnboardingGuardProps) {
<div className="mb-4">
<Goose className="size-8 mx-auto" />
</div>
<h1 className="text-xl font-light mb-3">{intl.formatMessage(i18n.checkProviderErrorTitle)}</h1>
<p className="text-text-muted mb-6">{intl.formatMessage(i18n.checkProviderErrorDescription)}</p>
<Button onClick={() => checkProvider()}>
{intl.formatMessage(i18n.retry)}
</Button>
<h1 className="text-xl font-light mb-3">
{intl.formatMessage(i18n.checkProviderErrorTitle)}
</h1>
<p className="text-text-muted mb-6">
{intl.formatMessage(i18n.checkProviderErrorDescription)}
</p>
<Button onClick={() => checkProvider()}>{intl.formatMessage(i18n.retry)}</Button>
</div>
</div>
);
@ -185,7 +192,9 @@ export default function OnboardingGuard({ children }: OnboardingGuardProps) {
<div className="mb-4">
<Goose className="size-8" />
</div>
<h1 className="text-2xl sm:text-4xl font-light mb-3">{intl.formatMessage(i18n.welcomeTitle)}</h1>
<h1 className="text-2xl sm:text-4xl font-light mb-3">
{intl.formatMessage(i18n.welcomeTitle)}
</h1>
<p className="text-text-muted text-base sm:text-lg">
{intl.formatMessage(i18n.welcomeDescription)}
</p>

View file

@ -24,7 +24,8 @@ const i18n = defineMessages({
},
privacyDescription: {
id: 'onboardingSuccess.privacyDescription',
defaultMessage: 'Anonymous usage data helps improve goose. We never collect your conversations, code, or personal data.',
defaultMessage:
'Anonymous usage data helps improve goose. We never collect your conversations, code, or personal data.',
},
learnMore: {
id: 'onboardingSuccess.learnMore',
@ -80,7 +81,9 @@ export default function OnboardingSuccess({ providerName, onFinish }: Onboarding
</div>
<div className="w-full p-4 bg-transparent border rounded-xl text-left mb-6">
<h3 className="font-medium text-text-default text-sm mb-1">{intl.formatMessage(i18n.privacyTitle)}</h3>
<h3 className="font-medium text-text-default text-sm mb-1">
{intl.formatMessage(i18n.privacyTitle)}
</h3>
<p className="text-text-muted text-sm">
{intl.formatMessage(i18n.privacyDescription)}{' '}
<button
@ -97,7 +100,9 @@ export default function OnboardingSuccess({ providerName, onFinish }: Onboarding
onChange={(e) => setTelemetryOptIn(e.target.checked)}
className="rounded"
/>
<span className="text-text-muted text-sm">{intl.formatMessage(i18n.shareUsageData)}</span>
<span className="text-text-muted text-sm">
{intl.formatMessage(i18n.shareUsageData)}
</span>
</label>
</div>

View file

@ -8,7 +8,8 @@ const i18n = defineMessages({
},
description: {
id: 'privacyInfoModal.description',
defaultMessage: 'Anonymous usage data helps us understand how goose is used and identify areas for improvement.',
defaultMessage:
'Anonymous usage data helps us understand how goose is used and identify areas for improvement.',
},
whatWeCollect: {
id: 'privacyInfoModal.whatWeCollect',
@ -40,7 +41,8 @@ const i18n = defineMessages({
},
neverCollect: {
id: 'privacyInfoModal.neverCollect',
defaultMessage: 'We never collect your conversations, code, tool arguments, error messages, or any personal data. You can change this setting anytime in Settings.',
defaultMessage:
'We never collect your conversations, code, tool arguments, error messages, or any personal data. You can change this setting anytime in Settings.',
},
});
@ -60,10 +62,10 @@ export default function PrivacyInfoModal({ isOpen, onClose }: PrivacyInfoModalPr
</DialogHeader>
<div>
<p className="text-text-muted text-sm mb-3">
{intl.formatMessage(i18n.description)}
<p className="text-text-muted text-sm mb-3">{intl.formatMessage(i18n.description)}</p>
<p className="font-medium text-text-default text-sm mb-1.5">
{intl.formatMessage(i18n.whatWeCollect)}
</p>
<p className="font-medium text-text-default text-sm mb-1.5">{intl.formatMessage(i18n.whatWeCollect)}</p>
<ul className="text-text-muted text-sm list-disc list-outside space-y-0.5 ml-5 mb-3">
<li>{intl.formatMessage(i18n.collectOs)}</li>
<li>{intl.formatMessage(i18n.collectVersion)}</li>
@ -72,9 +74,7 @@ export default function PrivacyInfoModal({ isOpen, onClose }: PrivacyInfoModalPr
<li>{intl.formatMessage(i18n.collectSession)}</li>
<li>{intl.formatMessage(i18n.collectErrors)}</li>
</ul>
<p className="text-text-muted text-sm">
{intl.formatMessage(i18n.neverCollect)}
</p>
<p className="text-text-muted text-sm">{intl.formatMessage(i18n.neverCollect)}</p>
</div>
</DialogContent>
</Dialog>

View file

@ -146,7 +146,9 @@ const ParameterInput: React.FC<ParameterInputProps> = ({
title={intl.formatMessage(i18n.unusedWarningTitle)}
>
<AlertTriangle className="w-4 h-4 text-orange-500" />
<span className="text-xs text-orange-500 font-normal">{intl.formatMessage(i18n.unused)}</span>
<span className="text-xs text-orange-500 font-normal">
{intl.formatMessage(i18n.unused)}
</span>
</div>
)}
</div>

View file

@ -47,7 +47,8 @@ const i18n = defineMessages({
},
reviewWarning: {
id: 'importRecipeForm.reviewWarning',
defaultMessage: 'Ensure you review contents of recipe files before adding them to your goose interface.',
defaultMessage:
'Ensure you review contents of recipe files before adding them to your goose interface.',
},
cancel: {
id: 'importRecipeForm.cancel',
@ -67,7 +68,8 @@ const i18n = defineMessages({
},
schemaDescription: {
id: 'importRecipeForm.schemaDescription',
defaultMessage: 'Your YAML or JSON file should follow this structure. Required fields are: title, description, and either instructions or prompt.',
defaultMessage:
'Your YAML or JSON file should follow this structure. Required fields are: title, description, and either instructions or prompt.',
},
});
@ -208,7 +210,9 @@ export default function ImportRecipeForm({ isOpen, onClose, onSuccess }: ImportR
<>
<div className="fixed inset-0 z-[300] flex items-center justify-center bg-black/50">
<div className="bg-background-primary border border-border-primary rounded-lg p-6 w-[500px] max-w-[90vw]">
<h3 className="text-lg font-medium text-text-primary mb-4">{intl.formatMessage(i18n.importRecipeTitle)}</h3>
<h3 className="text-lg font-medium text-text-primary mb-4">
{intl.formatMessage(i18n.importRecipeTitle)}
</h3>
<form
onSubmit={(e) => {
@ -354,7 +358,9 @@ export default function ImportRecipeForm({ isOpen, onClose, onSuccess }: ImportR
disabled={!canSubmit || importing || isSubmitting}
variant="default"
>
{importing || isSubmitting ? intl.formatMessage(i18n.importing) : intl.formatMessage(i18n.importRecipeButton)}
{importing || isSubmitting
? intl.formatMessage(i18n.importing)
: intl.formatMessage(i18n.importRecipeButton)}
</Button>
)}
</importRecipeForm.Subscribe>
@ -368,7 +374,9 @@ export default function ImportRecipeForm({ isOpen, onClose, onSuccess }: ImportR
<div className="fixed inset-0 z-[400] flex items-center justify-center bg-black/50">
<div className="bg-background-primary border border-border-primary rounded-lg p-6 w-[800px] max-w-[90vw] max-h-[80vh] flex flex-col">
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-medium text-text-primary">{intl.formatMessage(i18n.expectedRecipeStructure)}</h3>
<h3 className="text-lg font-medium text-text-primary">
{intl.formatMessage(i18n.expectedRecipeStructure)}
</h3>
<button
type="button"
onClick={() => setShowSchemaModal(false)}

View file

@ -9,7 +9,8 @@ const i18n = defineMessages({
},
activitiesDescription: {
id: 'recipeActivityEditor.activitiesDescription',
defaultMessage: 'The top-line prompts and activity buttons that will display in the recipe chat window.',
defaultMessage:
'The top-line prompts and activity buttons that will display in the recipe chat window.',
},
messageLabel: {
id: 'recipeActivityEditor.messageLabel',
@ -17,11 +18,13 @@ const i18n = defineMessages({
},
messageDescription: {
id: 'recipeActivityEditor.messageDescription',
defaultMessage: 'A formatted message that will appear at the top of the recipe. Supports markdown formatting.',
defaultMessage:
'A formatted message that will appear at the top of the recipe. Supports markdown formatting.',
},
messagePlaceholder: {
id: 'recipeActivityEditor.messagePlaceholder',
defaultMessage: 'Enter a user facing introduction message for your recipe (supports **bold**, *italic*, `code`, etc.)',
defaultMessage:
'Enter a user facing introduction message for your recipe (supports **bold**, *italic*, `code`, etc.)',
},
activityButtonsLabel: {
id: 'recipeActivityEditor.activityButtonsLabel',
@ -29,7 +32,8 @@ const i18n = defineMessages({
},
activityButtonsDescription: {
id: 'recipeActivityEditor.activityButtonsDescription',
defaultMessage: 'Clickable buttons that will appear below the message to help users interact with your recipe.',
defaultMessage:
'Clickable buttons that will appear below the message to help users interact with your recipe.',
},
addNewActivityPlaceholder: {
id: 'recipeActivityEditor.addNewActivityPlaceholder',

View file

@ -221,12 +221,24 @@ export default function CreateSubRecipeInline({
toastError({
title: intl.formatMessage(i18n.saveFailed),
msg: intl.formatMessage(i18n.saveFailedMsg, { error: error instanceof Error ? error.message : 'Unknown error' }),
msg: intl.formatMessage(i18n.saveFailedMsg, {
error: error instanceof Error ? error.message : 'Unknown error',
}),
});
} finally {
setIsSaving(false);
}
}, [form, name, toolDescription, sequentialWhenRepeated, values, existingSubRecipes, onSubRecipeSaved, onClose, intl]);
}, [
form,
name,
toolDescription,
sequentialWhenRepeated,
values,
existingSubRecipes,
onSubRecipeSaved,
onClose,
intl,
]);
if (!isOpen) return null;
@ -236,10 +248,10 @@ export default function CreateSubRecipeInline({
{/* Header */}
<div className="flex items-center justify-between p-6 border-b border-borderSubtle">
<div>
<h2 className="text-xl font-medium text-textProminent">{intl.formatMessage(i18n.title)}</h2>
<p className="text-textSubtle text-sm">
{intl.formatMessage(i18n.subtitle)}
</p>
<h2 className="text-xl font-medium text-textProminent">
{intl.formatMessage(i18n.title)}
</h2>
<p className="text-textSubtle text-sm">{intl.formatMessage(i18n.subtitle)}</p>
</div>
<Button
onClick={onClose}
@ -270,9 +282,7 @@ export default function CreateSubRecipeInline({
className="w-full p-3 border border-border-subtle rounded-lg bg-background-primary text-text-standard focus:outline-none focus:ring-2 focus:ring-ring"
placeholder={intl.formatMessage(i18n.namePlaceholder)}
/>
<p className="text-xs text-text-muted mt-1">
{intl.formatMessage(i18n.nameHint)}
</p>
<p className="text-xs text-text-muted mt-1">{intl.formatMessage(i18n.nameHint)}</p>
</div>
{/* Title Field */}
@ -283,7 +293,8 @@ export default function CreateSubRecipeInline({
htmlFor="subrecipe-title"
className="block text-sm font-medium text-text-standard mb-2"
>
{intl.formatMessage(i18n.recipeTitleLabel)} <span className="text-text-danger">*</span>
{intl.formatMessage(i18n.recipeTitleLabel)}{' '}
<span className="text-text-danger">*</span>
</label>
<input
id="subrecipe-title"
@ -306,7 +317,8 @@ export default function CreateSubRecipeInline({
htmlFor="recipe-description"
className="block text-sm font-medium text-text-standard mb-2"
>
{intl.formatMessage(i18n.recipeDescriptionLabel)} <span className="text-text-danger">*</span>
{intl.formatMessage(i18n.recipeDescriptionLabel)}{' '}
<span className="text-text-danger">*</span>
</label>
<input
id="recipe-description"
@ -329,7 +341,8 @@ export default function CreateSubRecipeInline({
htmlFor="subrecipe-instructions"
className="block text-sm font-medium text-text-standard mb-2"
>
{intl.formatMessage(i18n.instructionsLabel)} <span className="text-text-danger">*</span>
{intl.formatMessage(i18n.instructionsLabel)}{' '}
<span className="text-text-danger">*</span>
</label>
<textarea
id="subrecipe-instructions"

View file

@ -107,7 +107,9 @@ Use {{parameter_name}} syntax for any user-provided values.`;
>
<div className="bg-background-primary border border-border-primary rounded-lg p-6 w-[900px] max-w-[90vw] max-h-[90vh] overflow-hidden flex flex-col">
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-medium text-text-primary">{intl.formatMessage(i18n.title)}</h3>
<h3 className="text-lg font-medium text-text-primary">
{intl.formatMessage(i18n.title)}
</h3>
<button
type="button"
onClick={handleCancel}
@ -120,7 +122,9 @@ Use {{parameter_name}} syntax for any user-provided values.`;
<div className="flex-1 flex flex-col min-h-0">
<div className="mb-4">
<div className="flex items-center justify-between mb-2">
<label className="block text-sm font-medium text-text-primary">{intl.formatMessage(i18n.label)}</label>
<label className="block text-sm font-medium text-text-primary">
{intl.formatMessage(i18n.label)}
</label>
<Button
type="button"
onClick={insertExample}

View file

@ -127,7 +127,9 @@ export default function JsonSchemaEditor({
>
<div className="bg-background-primary border border-border-primary rounded-lg p-6 w-[800px] max-w-[90vw] max-h-[90vh] overflow-hidden flex flex-col">
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-medium text-text-primary">{intl.formatMessage(i18n.title)}</h3>
<h3 className="text-lg font-medium text-text-primary">
{intl.formatMessage(i18n.title)}
</h3>
<button
type="button"
onClick={handleCancel}

View file

@ -185,9 +185,7 @@ export const RecipeModelSelector = ({
<label className="block text-sm font-medium text-textStandard mb-2">
{intl.formatMessage(i18n.providerLabel)}
</label>
<p className="text-xs text-textSubtle mb-2">
{intl.formatMessage(i18n.providerHint)}
</p>
<p className="text-xs text-textSubtle mb-2">{intl.formatMessage(i18n.providerHint)}</p>
<Select
options={providerOptions}
value={
@ -203,7 +201,9 @@ export const RecipeModelSelector = ({
<div>
<div className="flex justify-between items-center mb-2">
<label className="block text-sm font-medium text-textStandard">{intl.formatMessage(i18n.modelLabel)}</label>
<label className="block text-sm font-medium text-textStandard">
{intl.formatMessage(i18n.modelLabel)}
</label>
{isCustomModel && (
<button
onClick={() => {
@ -217,9 +217,7 @@ export const RecipeModelSelector = ({
</button>
)}
</div>
<p className="text-xs text-textSubtle mb-2">
{intl.formatMessage(i18n.modelHint)}
</p>
<p className="text-xs text-textSubtle mb-2">{intl.formatMessage(i18n.modelHint)}</p>
{isCustomModel ? (
<Input
type="text"

View file

@ -1,81 +0,0 @@
import { recipeNameSchema, RECIPE_NAME_PLACEHOLDER } from './recipeNameUtils';
import { defineMessages, useIntl } from '../../../i18n';
const i18n = defineMessages({
defaultLabel: {
id: 'recipeNameField.defaultLabel',
defaultMessage: 'Recipe Name',
},
formatHint: {
id: 'recipeNameField.formatHint',
defaultMessage: 'Will be automatically formatted (lowercase, dashes for spaces)',
},
});
interface RecipeNameFieldProps {
id: string;
value: string;
onChange: (value: string) => void;
onBlur: () => void;
errors: string[];
label?: string;
required?: boolean;
disabled?: boolean;
}
export function RecipeNameField({
id,
value,
onChange,
onBlur,
errors,
label,
required = true,
disabled = false,
}: RecipeNameFieldProps) {
const intl = useIntl();
const displayLabel = label || intl.formatMessage(i18n.defaultLabel);
return (
<div>
<label htmlFor={id} className="block text-sm font-medium text-text-primary mb-2">
{displayLabel} {required && <span className="text-red-500">*</span>}
</label>
<input
id={id}
type="text"
value={value}
onChange={(e) => {
// Allow typing normally, only filter out invalid characters but keep spaces
const rawValue = e.target.value;
const filtered = rawValue.replace(/[^a-zA-Z0-9\s-]/g, '');
onChange(filtered);
}}
onBlur={(e) => {
// Transform on blur: convert to lowercase and replace spaces with dashes
const rawValue = e.target.value;
const transformed = rawValue
.toLowerCase()
.replace(/\s+/g, '-')
.replace(/[^a-z0-9-]/g, '')
.replace(/-+/g, '-')
.replace(/^-+|-+$/g, '');
onChange(transformed);
onBlur();
}}
disabled={disabled}
className={`w-full p-3 border rounded-lg bg-background-primary text-text-primary focus:outline-none focus:ring-2 focus:ring-blue-500 ${
errors.length > 0 ? 'border-red-500' : 'border-border-primary'
} ${disabled ? 'opacity-50 cursor-not-allowed' : ''}`}
placeholder={RECIPE_NAME_PLACEHOLDER}
data-testid="recipe-name-input"
/>
<p className="text-xs text-text-secondary mt-1">
{intl.formatMessage(i18n.formatHint)}
</p>
{errors.length > 0 && <p className="text-red-500 text-sm mt-1">{errors[0]}</p>}
</div>
);
}
export { recipeNameSchema };

View file

@ -22,7 +22,8 @@ const i18n = defineMessages({
},
description: {
id: 'subRecipeEditor.description',
defaultMessage: 'Subrecipes are recipes that can be called as tools during execution. They enable multi-step workflows and reusable components.',
defaultMessage:
'Subrecipes are recipes that can be called as tools during execution. They enable multi-step workflows and reusable components.',
},
sequential: {
id: 'subRecipeEditor.sequential',
@ -118,7 +119,9 @@ export default function SubRecipeEditor({ subRecipes, onChange }: SubRecipeEdito
return (
<div>
<div className="flex items-center justify-between mb-2">
<label className="block text-md text-textProminent font-bold">{intl.formatMessage(i18n.label)}</label>
<label className="block text-md text-textProminent font-bold">
{intl.formatMessage(i18n.label)}
</label>
<div className="flex gap-2">
<Button
type="button"
@ -143,9 +146,7 @@ export default function SubRecipeEditor({ subRecipes, onChange }: SubRecipeEdito
</div>
</div>
<p className="text-textSubtle text-sm mb-4">
{intl.formatMessage(i18n.description)}
</p>
<p className="text-textSubtle text-sm mb-4">{intl.formatMessage(i18n.description)}</p>
{subRecipes.length > 0 && (
<div className="space-y-2">
@ -170,7 +171,9 @@ export default function SubRecipeEditor({ subRecipes, onChange }: SubRecipeEdito
)}
{subRecipe.values && Object.keys(subRecipe.values).length > 0 && (
<div className="mt-2">
<p className="text-xs text-text-muted mb-1">{intl.formatMessage(i18n.preconfiguredValues)}</p>
<p className="text-xs text-text-muted mb-1">
{intl.formatMessage(i18n.preconfiguredValues)}
</p>
<div className="flex flex-wrap gap-1">
{Object.entries(subRecipe.values).map(([key, value]) => (
<span

View file

@ -179,11 +179,11 @@ export default function SubRecipeModal({
<div className="flex items-center justify-between p-6 border-b border-borderSubtle">
<div>
<h2 className="text-xl font-medium text-textProminent">
{subRecipe ? intl.formatMessage(i18n.configureTitle) : intl.formatMessage(i18n.addTitle)}
{subRecipe
? intl.formatMessage(i18n.configureTitle)
: intl.formatMessage(i18n.addTitle)}
</h2>
<p className="text-textSubtle text-sm">
{intl.formatMessage(i18n.subtitle)}
</p>
<p className="text-textSubtle text-sm">{intl.formatMessage(i18n.subtitle)}</p>
</div>
<Button
onClick={onClose}
@ -214,9 +214,7 @@ export default function SubRecipeModal({
className="w-full p-3 border border-border-subtle rounded-lg bg-background-primary text-text-standard focus:outline-none focus:ring-2 focus:ring-ring"
placeholder={intl.formatMessage(i18n.namePlaceholder)}
/>
<p className="text-xs text-text-muted mt-1">
{intl.formatMessage(i18n.nameHint)}
</p>
<p className="text-xs text-text-muted mt-1">{intl.formatMessage(i18n.nameHint)}</p>
</div>
{/* Path Field */}
@ -246,9 +244,7 @@ export default function SubRecipeModal({
{intl.formatMessage(i18n.browse)}
</Button>
</div>
<p className="text-xs text-text-muted mt-1">
{intl.formatMessage(i18n.pathHint)}
</p>
<p className="text-xs text-text-muted mt-1">{intl.formatMessage(i18n.pathHint)}</p>
</div>
{/* Description Field */}

View file

@ -11,9 +11,6 @@ const parameterSchema = z.object({
options: z.array(z.string()).nullable().optional(),
});
// Export the parameter type for use in components
export type RecipeParameter = z.infer<typeof parameterSchema>;
// Zod schema for SubRecipe - matching ACP SubRecipeDto type
const subRecipeSchema = z.object({
name: z.string().min(1, 'Subrecipe name is required'),

View file

@ -1,38 +0,0 @@
import { z } from 'zod';
/**
* Validation schema for recipe names
*/
export const recipeNameSchema = z.string().min(3, 'Recipe name must be at least 3 characters');
/**
* Transform a string to a valid recipe name format:
* - Convert to lowercase
* - Replace spaces with dashes
* - Remove invalid characters
* - Trim whitespace and dashes
*/
export function transformToRecipeName(input: string): string {
return input
.toLowerCase()
.replace(/[^a-zA-Z0-9\s-]/g, '') // Remove invalid characters
.replace(/\s+/g, '-') // Replace spaces with dashes
.replace(/--+/g, '-') // Replace multiple dashes with single dash
.replace(/^-+|-+$/g, '') // Remove leading/trailing dashes
.trim();
}
/**
* Generate a recipe name from a title
*/
export function generateRecipeNameFromTitle(title: string): string {
if (!title.trim()) {
return '';
}
return transformToRecipeName(title);
}
/**
* Common placeholder text for recipe name inputs
*/
export const RECIPE_NAME_PLACEHOLDER = 'my-awesome-recipe';

View file

@ -25,14 +25,32 @@ import { defineMessages, useIntl } from '../../i18n';
import { useNavigation } from '../../hooks/useNavigation';
const i18n = defineMessages({
scheduleNotFound: { id: 'scheduleDetailView.scheduleNotFound', defaultMessage: 'Schedule Not Found' },
noScheduleId: { id: 'scheduleDetailView.noScheduleId', defaultMessage: 'No schedule ID provided. Return to schedules list.' },
scheduleNotFound: {
id: 'scheduleDetailView.scheduleNotFound',
defaultMessage: 'Schedule Not Found',
},
noScheduleId: {
id: 'scheduleDetailView.noScheduleId',
defaultMessage: 'No schedule ID provided. Return to schedules list.',
},
scheduleDetails: { id: 'scheduleDetailView.scheduleDetails', defaultMessage: 'Schedule Details' },
viewingScheduleId: { id: 'scheduleDetailView.viewingScheduleId', defaultMessage: 'Viewing Schedule ID: {id}' },
scheduleInformation: { id: 'scheduleDetailView.scheduleInformation', defaultMessage: 'Schedule Information' },
loadingSchedule: { id: 'scheduleDetailView.loadingSchedule', defaultMessage: 'Loading schedule...' },
viewingScheduleId: {
id: 'scheduleDetailView.viewingScheduleId',
defaultMessage: 'Viewing Schedule ID: {id}',
},
scheduleInformation: {
id: 'scheduleDetailView.scheduleInformation',
defaultMessage: 'Schedule Information',
},
loadingSchedule: {
id: 'scheduleDetailView.loadingSchedule',
defaultMessage: 'Loading schedule...',
},
errorPrefix: { id: 'scheduleDetailView.errorPrefix', defaultMessage: 'Error: {error}' },
currentlyRunning: { id: 'scheduleDetailView.currentlyRunning', defaultMessage: 'Currently Running' },
currentlyRunning: {
id: 'scheduleDetailView.currentlyRunning',
defaultMessage: 'Currently Running',
},
paused: { id: 'scheduleDetailView.paused', defaultMessage: 'Paused' },
scheduleLabel: { id: 'scheduleDetailView.scheduleLabel', defaultMessage: 'Schedule:' },
cronExpression: { id: 'scheduleDetailView.cronExpression', defaultMessage: 'Cron Expression:' },
@ -45,37 +63,83 @@ const i18n = defineMessages({
editSchedule: { id: 'scheduleDetailView.editSchedule', defaultMessage: 'Edit Schedule' },
unpauseSchedule: { id: 'scheduleDetailView.unpauseSchedule', defaultMessage: 'Unpause Schedule' },
pauseSchedule: { id: 'scheduleDetailView.pauseSchedule', defaultMessage: 'Pause Schedule' },
inspectRunningJob: { id: 'scheduleDetailView.inspectRunningJob', defaultMessage: 'Inspect Running Job' },
inspectRunningJob: {
id: 'scheduleDetailView.inspectRunningJob',
defaultMessage: 'Inspect Running Job',
},
killRunningJob: { id: 'scheduleDetailView.killRunningJob', defaultMessage: 'Kill Running Job' },
cannotModifyRunning: { id: 'scheduleDetailView.cannotModifyRunning', defaultMessage: 'Cannot trigger or modify a schedule while it\'s already running.' },
pausedWarning: { id: 'scheduleDetailView.pausedWarning', defaultMessage: 'This schedule is paused and will not run automatically. Use "Run Schedule Now" to trigger it manually or unpause to resume automatic execution.' },
cannotModifyRunning: {
id: 'scheduleDetailView.cannotModifyRunning',
defaultMessage: "Cannot trigger or modify a schedule while it's already running.",
},
pausedWarning: {
id: 'scheduleDetailView.pausedWarning',
defaultMessage:
'This schedule is paused and will not run automatically. Use "Run Schedule Now" to trigger it manually or unpause to resume automatic execution.',
},
recentSessions: { id: 'scheduleDetailView.recentSessions', defaultMessage: 'Recent Sessions' },
loadingSessions: { id: 'scheduleDetailView.loadingSessions', defaultMessage: 'Loading sessions...' },
noSessions: { id: 'scheduleDetailView.noSessions', defaultMessage: 'No sessions found for this schedule.' },
loadingSessions: {
id: 'scheduleDetailView.loadingSessions',
defaultMessage: 'Loading sessions...',
},
noSessions: {
id: 'scheduleDetailView.noSessions',
defaultMessage: 'No sessions found for this schedule.',
},
sessionId: { id: 'scheduleDetailView.sessionId', defaultMessage: 'Session ID: {id}' },
created: { id: 'scheduleDetailView.created', defaultMessage: 'Created: {date}' },
messages: { id: 'scheduleDetailView.messages', defaultMessage: 'Messages: {count}' },
dir: { id: 'scheduleDetailView.dir', defaultMessage: 'Dir: {path}' },
idLabel: { id: 'scheduleDetailView.idLabel', defaultMessage: 'ID:' },
jobCancelled: { id: 'scheduleDetailView.jobCancelled', defaultMessage: 'Job Cancelled' },
jobCancelledMsg: { id: 'scheduleDetailView.jobCancelledMsg', defaultMessage: 'The job was cancelled while starting up.' },
scheduleCompleted: { id: 'scheduleDetailView.scheduleCompleted', defaultMessage: 'Run completed' },
completedSession: { id: 'scheduleDetailView.completedSession', defaultMessage: 'Session: {sessionId}' },
runScheduleError: { id: 'scheduleDetailView.runScheduleError', defaultMessage: 'Run Schedule Error' },
scheduleUnpaused: { id: 'scheduleDetailView.scheduleUnpaused', defaultMessage: 'Schedule Unpaused' },
jobCancelledMsg: {
id: 'scheduleDetailView.jobCancelledMsg',
defaultMessage: 'The job was cancelled while starting up.',
},
scheduleCompleted: {
id: 'scheduleDetailView.scheduleCompleted',
defaultMessage: 'Run completed',
},
completedSession: {
id: 'scheduleDetailView.completedSession',
defaultMessage: 'Session: {sessionId}',
},
runScheduleError: {
id: 'scheduleDetailView.runScheduleError',
defaultMessage: 'Run Schedule Error',
},
scheduleUnpaused: {
id: 'scheduleDetailView.scheduleUnpaused',
defaultMessage: 'Schedule Unpaused',
},
unpausedMsg: { id: 'scheduleDetailView.unpausedMsg', defaultMessage: 'Unpaused "{id}"' },
schedulePaused: { id: 'scheduleDetailView.schedulePaused', defaultMessage: 'Schedule Paused' },
pausedMsg: { id: 'scheduleDetailView.pausedMsg', defaultMessage: 'Paused "{id}"' },
pauseUnpauseError: { id: 'scheduleDetailView.pauseUnpauseError', defaultMessage: 'Pause/Unpause Error' },
pauseUnpauseError: {
id: 'scheduleDetailView.pauseUnpauseError',
defaultMessage: 'Pause/Unpause Error',
},
jobKilled: { id: 'scheduleDetailView.jobKilled', defaultMessage: 'Job Killed' },
killJobError: { id: 'scheduleDetailView.killJobError', defaultMessage: 'Kill Job Error' },
jobInspection: { id: 'scheduleDetailView.jobInspection', defaultMessage: 'Job Inspection' },
inspectNoInfo: { id: 'scheduleDetailView.inspectNoInfo', defaultMessage: 'No detailed information available' },
inspectJobError: { id: 'scheduleDetailView.inspectJobError', defaultMessage: 'Inspect Job Error' },
inspectNoInfo: {
id: 'scheduleDetailView.inspectNoInfo',
defaultMessage: 'No detailed information available',
},
inspectJobError: {
id: 'scheduleDetailView.inspectJobError',
defaultMessage: 'Inspect Job Error',
},
scheduleUpdated: { id: 'scheduleDetailView.scheduleUpdated', defaultMessage: 'Schedule Updated' },
updatedMsg: { id: 'scheduleDetailView.updatedMsg', defaultMessage: 'Updated "{id}"' },
updateScheduleError: { id: 'scheduleDetailView.updateScheduleError', defaultMessage: 'Update Schedule Error' },
scheduleNotFoundError: { id: 'scheduleDetailView.scheduleNotFoundError', defaultMessage: 'Schedule not found' },
updateScheduleError: {
id: 'scheduleDetailView.updateScheduleError',
defaultMessage: 'Update Schedule Error',
},
scheduleNotFoundError: {
id: 'scheduleDetailView.scheduleNotFoundError',
defaultMessage: 'Schedule not found',
},
});
interface ScheduleDetailViewProps {
@ -125,23 +189,26 @@ const ScheduleDetailView: React.FC<ScheduleDetailViewProps> = ({ scheduleId, onN
}
};
const fetchSchedule = useCallback(async (sId: string) => {
setIsLoadingSchedule(true);
setScheduleError(null);
try {
const allSchedules = await acpListSchedules();
const schedule = allSchedules.find((s) => s.id === sId);
if (schedule) {
setScheduleDetails(schedule);
} else {
setScheduleError(intl.formatMessage(i18n.scheduleNotFoundError));
const fetchSchedule = useCallback(
async (sId: string) => {
setIsLoadingSchedule(true);
setScheduleError(null);
try {
const allSchedules = await acpListSchedules();
const schedule = allSchedules.find((s) => s.id === sId);
if (schedule) {
setScheduleDetails(schedule);
} else {
setScheduleError(intl.formatMessage(i18n.scheduleNotFoundError));
}
} catch (err) {
setScheduleError(errorMessage(err, 'Failed to fetch schedule'));
} finally {
setIsLoadingSchedule(false);
}
} catch (err) {
setScheduleError(errorMessage(err, 'Failed to fetch schedule'));
} finally {
setIsLoadingSchedule(false);
}
}, [intl]);
},
[intl]
);
useEffect(() => {
if (scheduleId) {
@ -150,12 +217,15 @@ const ScheduleDetailView: React.FC<ScheduleDetailViewProps> = ({ scheduleId, onN
}
}, [scheduleId, fetchSchedule]);
const openSession = useCallback((sessionId: string) => {
setView('pair', {
disableAnimation: true,
resumeSessionId: sessionId,
});
}, [setView]);
const openSession = useCallback(
(sessionId: string) => {
setView('pair', {
disableAnimation: true,
resumeSessionId: sessionId,
});
},
[setView]
);
const handleRunNow = async () => {
if (!scheduleId) return;
@ -164,7 +234,10 @@ const ScheduleDetailView: React.FC<ScheduleDetailViewProps> = ({ scheduleId, onN
const result = await acpRunScheduleNow(scheduleId);
trackScheduleRunNow(true);
if (result.status === 'completed' && result.sessionId) {
toastSuccess({ title: intl.formatMessage(i18n.scheduleCompleted), msg: intl.formatMessage(i18n.completedSession, { sessionId: result.sessionId }) });
toastSuccess({
title: intl.formatMessage(i18n.scheduleCompleted),
msg: intl.formatMessage(i18n.completedSession, { sessionId: result.sessionId }),
});
}
await fetchSessions(scheduleId);
await fetchSchedule(scheduleId);
@ -186,10 +259,16 @@ const ScheduleDetailView: React.FC<ScheduleDetailViewProps> = ({ scheduleId, onN
try {
if (scheduleDetails.paused) {
await acpUnpauseSchedule(scheduleId);
toastSuccess({ title: intl.formatMessage(i18n.scheduleUnpaused), msg: intl.formatMessage(i18n.unpausedMsg, { id: scheduleId }) });
toastSuccess({
title: intl.formatMessage(i18n.scheduleUnpaused),
msg: intl.formatMessage(i18n.unpausedMsg, { id: scheduleId }),
});
} else {
await acpPauseSchedule(scheduleId);
toastSuccess({ title: intl.formatMessage(i18n.schedulePaused), msg: intl.formatMessage(i18n.pausedMsg, { id: scheduleId }) });
toastSuccess({
title: intl.formatMessage(i18n.schedulePaused),
msg: intl.formatMessage(i18n.pausedMsg, { id: scheduleId }),
});
}
await fetchSchedule(scheduleId);
} catch (err) {
@ -235,7 +314,10 @@ const ScheduleDetailView: React.FC<ScheduleDetailViewProps> = ({ scheduleId, onN
msg: `Session: ${result.sessionId}\nRunning for: ${duration}`,
});
} else {
toastSuccess({ title: intl.formatMessage(i18n.jobInspection), msg: intl.formatMessage(i18n.inspectNoInfo) });
toastSuccess({
title: intl.formatMessage(i18n.jobInspection),
msg: intl.formatMessage(i18n.inspectNoInfo),
});
}
} catch (err) {
const errorMsg = errorMessage(err, 'Failed to inspect job');
@ -253,7 +335,10 @@ const ScheduleDetailView: React.FC<ScheduleDetailViewProps> = ({ scheduleId, onN
setIsActionLoading(true);
try {
await acpUpdateSchedule(scheduleId, payload as string);
toastSuccess({ title: intl.formatMessage(i18n.scheduleUpdated), msg: intl.formatMessage(i18n.updatedMsg, { id: scheduleId }) });
toastSuccess({
title: intl.formatMessage(i18n.scheduleUpdated),
msg: intl.formatMessage(i18n.updatedMsg, { id: scheduleId }),
});
await fetchSchedule(scheduleId);
setIsModalOpen(false);
} catch (err) {
@ -271,10 +356,10 @@ const ScheduleDetailView: React.FC<ScheduleDetailViewProps> = ({ scheduleId, onN
return (
<div className="h-screen w-full flex flex-col items-center justify-center bg-white dark:bg-gray-900 text-text-primary p-8">
<BackButton onClick={onNavigateBack} />
<h1 className="text-2xl font-medium text-text-primary mt-4">{intl.formatMessage(i18n.scheduleNotFound)}</h1>
<p className="text-text-secondary mt-2">
{intl.formatMessage(i18n.noScheduleId)}
</p>
<h1 className="text-2xl font-medium text-text-primary mt-4">
{intl.formatMessage(i18n.scheduleNotFound)}
</h1>
<p className="text-text-secondary mt-2">{intl.formatMessage(i18n.noScheduleId)}</p>
</div>
);
}
@ -293,17 +378,24 @@ const ScheduleDetailView: React.FC<ScheduleDetailViewProps> = ({ scheduleId, onN
<div className="h-screen w-full flex flex-col bg-background-primary text-text-primary">
<div className="px-8 pt-6 pb-4 border-b border-border-primary flex-shrink-0">
<BackButton onClick={onNavigateBack} />
<h1 className="text-4xl font-light mt-1 mb-1 pt-8">{intl.formatMessage(i18n.scheduleDetails)}</h1>
<p className="text-sm text-text-secondary mb-1">{intl.formatMessage(i18n.viewingScheduleId, { id: scheduleId })}</p>
<h1 className="text-4xl font-light mt-1 mb-1 pt-8">
{intl.formatMessage(i18n.scheduleDetails)}
</h1>
<p className="text-sm text-text-secondary mb-1">
{intl.formatMessage(i18n.viewingScheduleId, { id: scheduleId })}
</p>
</div>
<ScrollArea className="flex-grow">
<div className="p-8 space-y-6">
<section>
<h2 className="text-xl font-semibold text-text-primary mb-3">{intl.formatMessage(i18n.scheduleInformation)}</h2>
<h2 className="text-xl font-semibold text-text-primary mb-3">
{intl.formatMessage(i18n.scheduleInformation)}
</h2>
{isLoadingSchedule && (
<div className="flex items-center text-text-secondary">
<Loader2 className="mr-2 h-4 w-4 animate-spin" /> {intl.formatMessage(i18n.loadingSchedule)}
<Loader2 className="mr-2 h-4 w-4 animate-spin" />{' '}
{intl.formatMessage(i18n.loadingSchedule)}
</div>
)}
{scheduleError && (
@ -334,13 +426,16 @@ const ScheduleDetailView: React.FC<ScheduleDetailViewProps> = ({ scheduleId, onN
</div>
</div>
<p className="text-sm text-text-primary">
<span className="font-semibold">{intl.formatMessage(i18n.scheduleLabel)}</span> {readableCron}
<span className="font-semibold">{intl.formatMessage(i18n.scheduleLabel)}</span>{' '}
{readableCron}
</p>
<p className="text-sm text-text-primary">
<span className="font-semibold">{intl.formatMessage(i18n.cronExpression)}</span> {scheduleDetails.cron}
<span className="font-semibold">{intl.formatMessage(i18n.cronExpression)}</span>{' '}
{scheduleDetails.cron}
</p>
<p className="text-sm text-text-primary">
<span className="font-semibold">{intl.formatMessage(i18n.recipeSource)}</span> {scheduleDetails.source}
<span className="font-semibold">{intl.formatMessage(i18n.recipeSource)}</span>{' '}
{scheduleDetails.source}
</p>
<p className="text-sm text-text-primary">
<span className="font-semibold">{intl.formatMessage(i18n.lastRun)}</span>{' '}
@ -348,13 +443,17 @@ const ScheduleDetailView: React.FC<ScheduleDetailViewProps> = ({ scheduleId, onN
</p>
{scheduleDetails.currentlyRunning && scheduleDetails.currentSessionId && (
<p className="text-sm text-text-primary">
<span className="font-semibold">{intl.formatMessage(i18n.currentSession)}</span>{' '}
<span className="font-semibold">
{intl.formatMessage(i18n.currentSession)}
</span>{' '}
{scheduleDetails.currentSessionId}
</p>
)}
{scheduleDetails.currentlyRunning && scheduleDetails.jobStartTime && (
<p className="text-sm text-text-primary">
<span className="font-semibold">{intl.formatMessage(i18n.processStarted)}</span>{' '}
<span className="font-semibold">
{intl.formatMessage(i18n.processStarted)}
</span>{' '}
{formatToLocalDateWithTimezone(scheduleDetails.jobStartTime)}
</p>
)}
@ -364,7 +463,9 @@ const ScheduleDetailView: React.FC<ScheduleDetailViewProps> = ({ scheduleId, onN
</section>
<section>
<h2 className="text-xl font-semibold text-text-primary mb-3">{intl.formatMessage(i18n.actions)}</h2>
<h2 className="text-xl font-semibold text-text-primary mb-3">
{intl.formatMessage(i18n.actions)}
</h2>
<div className="flex flex-col md:flex-row gap-2">
<Button
onClick={handleRunNow}
@ -448,8 +549,12 @@ const ScheduleDetailView: React.FC<ScheduleDetailViewProps> = ({ scheduleId, onN
</section>
<section>
<h2 className="text-xl font-semibold text-text-primary mb-4">{intl.formatMessage(i18n.recentSessions)}</h2>
{isLoadingSessions && <p className="text-text-secondary">{intl.formatMessage(i18n.loadingSessions)}</p>}
<h2 className="text-xl font-semibold text-text-primary mb-4">
{intl.formatMessage(i18n.recentSessions)}
</h2>
{isLoadingSessions && (
<p className="text-text-secondary">{intl.formatMessage(i18n.loadingSessions)}</p>
)}
{sessionsError && (
<p className="text-text-danger text-sm p-3 bg-background-danger border border-border-danger rounded-md">
{intl.formatMessage(i18n.errorPrefix, { error: sessionsError })}
@ -482,7 +587,9 @@ const ScheduleDetailView: React.FC<ScheduleDetailViewProps> = ({ scheduleId, onN
{sessionName || intl.formatMessage(i18n.sessionId, { id: sessionId })}
</h3>
<p className="text-xs text-text-secondary mt-1">
{intl.formatMessage(i18n.created, { date: createdAt ? formatToLocalDateWithTimezone(createdAt) : 'N/A' })}
{intl.formatMessage(i18n.created, {
date: createdAt ? formatToLocalDateWithTimezone(createdAt) : 'N/A',
})}
</p>
{messageCount !== undefined && (
<p className="text-xs text-text-secondary mt-1">
@ -498,7 +605,8 @@ const ScheduleDetailView: React.FC<ScheduleDetailViewProps> = ({ scheduleId, onN
</p>
)}
<p className="text-xs text-text-secondary mt-1">
{intl.formatMessage(i18n.idLabel)} <span className="font-mono">{sessionId}</span>
{intl.formatMessage(i18n.idLabel)}{' '}
<span className="font-mono">{sessionId}</span>
</p>
</Card>
);

View file

@ -11,30 +11,60 @@ import { defineMessages, useIntl } from '../../i18n';
const i18n = defineMessages({
editSchedule: { id: 'scheduleModal.editSchedule', defaultMessage: 'Edit Schedule' },
createNewSchedule: { id: 'scheduleModal.createNewSchedule', defaultMessage: 'Create New Schedule' },
createNewSchedule: {
id: 'scheduleModal.createNewSchedule',
defaultMessage: 'Create New Schedule',
},
nameLabel: { id: 'scheduleModal.nameLabel', defaultMessage: 'Name:' },
namePlaceholder: { id: 'scheduleModal.namePlaceholder', defaultMessage: 'e.g., daily-summary-job' },
namePlaceholder: {
id: 'scheduleModal.namePlaceholder',
defaultMessage: 'e.g., daily-summary-job',
},
sourceLabel: { id: 'scheduleModal.sourceLabel', defaultMessage: 'Source:' },
yaml: { id: 'scheduleModal.yaml', defaultMessage: 'YAML' },
deepLink: { id: 'scheduleModal.deepLink', defaultMessage: 'Deep link' },
browseYaml: { id: 'scheduleModal.browseYaml', defaultMessage: 'Browse for YAML file...' },
selected: { id: 'scheduleModal.selected', defaultMessage: 'Selected: {path}' },
deepLinkPlaceholder: { id: 'scheduleModal.deepLinkPlaceholder', defaultMessage: 'Paste goose://recipe link here...' },
deepLinkPlaceholder: {
id: 'scheduleModal.deepLinkPlaceholder',
defaultMessage: 'Paste goose://recipe link here...',
},
recipeParsed: { id: 'scheduleModal.recipeParsed', defaultMessage: 'Recipe parsed successfully' },
recipeTitle: { id: 'scheduleModal.recipeTitle', defaultMessage: 'Title: {title}' },
recipeDescription: { id: 'scheduleModal.recipeDescription', defaultMessage: 'Description: {description}' },
recipeDescription: {
id: 'scheduleModal.recipeDescription',
defaultMessage: 'Description: {description}',
},
scheduleLabel: { id: 'scheduleModal.scheduleLabel', defaultMessage: 'Schedule:' },
cancel: { id: 'scheduleModal.cancel', defaultMessage: 'Cancel' },
updating: { id: 'scheduleModal.updating', defaultMessage: 'Updating...' },
creating: { id: 'scheduleModal.creating', defaultMessage: 'Creating...' },
updateSchedule: { id: 'scheduleModal.updateSchedule', defaultMessage: 'Update Schedule' },
createSchedule: { id: 'scheduleModal.createSchedule', defaultMessage: 'Create Schedule' },
invalidDeepLink: { id: 'scheduleModal.invalidDeepLink', defaultMessage: 'Invalid deep link. Please use a goose://recipe link.' },
failedReadFile: { id: 'scheduleModal.failedReadFile', defaultMessage: 'Failed to read the selected file.' },
failedParseRecipe: { id: 'scheduleModal.failedParseRecipe', defaultMessage: 'Failed to parse recipe from file.' },
invalidFileType: { id: 'scheduleModal.invalidFileType', defaultMessage: 'Invalid file type: Please select a YAML file (.yaml or .yml)' },
scheduleIdRequired: { id: 'scheduleModal.scheduleIdRequired', defaultMessage: 'Schedule ID is required.' },
provideValidRecipe: { id: 'scheduleModal.provideValidRecipe', defaultMessage: 'Please provide a valid recipe source.' },
invalidDeepLink: {
id: 'scheduleModal.invalidDeepLink',
defaultMessage: 'Invalid deep link. Please use a goose://recipe link.',
},
failedReadFile: {
id: 'scheduleModal.failedReadFile',
defaultMessage: 'Failed to read the selected file.',
},
failedParseRecipe: {
id: 'scheduleModal.failedParseRecipe',
defaultMessage: 'Failed to parse recipe from file.',
},
invalidFileType: {
id: 'scheduleModal.invalidFileType',
defaultMessage: 'Invalid file type: Please select a YAML file (.yaml or .yml)',
},
scheduleIdRequired: {
id: 'scheduleModal.scheduleIdRequired',
defaultMessage: 'Schedule ID is required.',
},
provideValidRecipe: {
id: 'scheduleModal.provideValidRecipe',
defaultMessage: 'Please provide a valid recipe source.',
},
});
export interface NewSchedulePayload {
@ -86,26 +116,29 @@ export const ScheduleModal: React.FC<ScheduleModalProps> = ({
setScheduleId(cleanId);
};
const handleDeepLinkChange = useCallback(async (value: string) => {
setDeepLinkInput(value);
setInternalValidationError(null);
const handleDeepLinkChange = useCallback(
async (value: string) => {
setDeepLinkInput(value);
setInternalValidationError(null);
if (value.trim()) {
try {
const recipe = await parseDeeplink(value.trim());
if (!recipe) throw new Error();
setParsedRecipe(recipe);
if (recipe.title) {
setScheduleIdFromTitle(recipe.title);
if (value.trim()) {
try {
const recipe = await parseDeeplink(value.trim());
if (!recipe) throw new Error();
setParsedRecipe(recipe);
if (recipe.title) {
setScheduleIdFromTitle(recipe.title);
}
} catch {
setParsedRecipe(null);
setInternalValidationError(intl.formatMessage(i18n.invalidDeepLink));
}
} catch {
} else {
setParsedRecipe(null);
setInternalValidationError(intl.formatMessage(i18n.invalidDeepLink));
}
} else {
setParsedRecipe(null);
}
}, [intl]);
},
[intl]
);
useEffect(() => {
if (isOpen) {
@ -199,7 +232,9 @@ export const ScheduleModal: React.FC<ScheduleModalProps> = ({
<img src={ClockIcon} alt="Clock" className="w-8 h-8" />
<div className="flex-1">
<h2 className="text-base font-semibold text-text-primary">
{isEditMode ? intl.formatMessage(i18n.editSchedule) : intl.formatMessage(i18n.createNewSchedule)}
{isEditMode
? intl.formatMessage(i18n.editSchedule)
: intl.formatMessage(i18n.createNewSchedule)}
</h2>
{isEditMode && <p className="text-sm text-text-secondary">{schedule.id}</p>}
</div>
@ -240,7 +275,7 @@ export const ScheduleModal: React.FC<ScheduleModalProps> = ({
<div>
<label className={modalLabelClassName}>
{intl.formatMessage(i18n.sourceLabel)} <span className="text-red-500">*</span>
{intl.formatMessage(i18n.sourceLabel)} <span className="text-red-500">*</span>
</label>
<div className="space-y-2">
<div className="flex bg-gray-100 dark:bg-gray-700 rounded-full p-1">
@ -304,7 +339,9 @@ export const ScheduleModal: React.FC<ScheduleModalProps> = ({
{intl.formatMessage(i18n.recipeTitle, { title: parsedRecipe.title })}
</p>
<p className="text-xs text-green-600 dark:text-green-400">
{intl.formatMessage(i18n.recipeDescription, { description: parsedRecipe.description })}
{intl.formatMessage(i18n.recipeDescription, {
description: parsedRecipe.description,
})}
</p>
</div>
)}

View file

@ -40,25 +40,41 @@ const i18n = defineMessages({
refreshing: { id: 'schedulesView.refreshing', defaultMessage: 'Refreshing...' },
refresh: { id: 'schedulesView.refresh', defaultMessage: 'Refresh' },
createSchedule: { id: 'schedulesView.createSchedule', defaultMessage: 'Create Schedule' },
description: { id: 'schedulesView.description', defaultMessage: 'Create and manage scheduled tasks to run recipes automatically at specified times.' },
description: {
id: 'schedulesView.description',
defaultMessage:
'Create and manage scheduled tasks to run recipes automatically at specified times.',
},
errorPrefix: { id: 'schedulesView.errorPrefix', defaultMessage: 'Error: {error}' },
noSchedules: { id: 'schedulesView.noSchedules', defaultMessage: 'No schedules yet' },
scheduleUpdated: { id: 'schedulesView.scheduleUpdated', defaultMessage: 'Schedule Updated' },
scheduleUpdatedMsg: { id: 'schedulesView.scheduleUpdatedMsg', defaultMessage: 'Successfully updated schedule "{id}"' },
scheduleUpdatedMsg: {
id: 'schedulesView.scheduleUpdatedMsg',
defaultMessage: 'Successfully updated schedule "{id}"',
},
confirmDelete: {
id: 'schedulesView.confirmDelete',
defaultMessage: 'Remove schedule "{id}"? The recipe will be kept.',
},
schedulePaused: { id: 'schedulesView.schedulePaused', defaultMessage: 'Schedule Paused' },
schedulePausedMsg: { id: 'schedulesView.schedulePausedMsg', defaultMessage: 'Successfully paused schedule "{id}"' },
schedulePausedMsg: {
id: 'schedulesView.schedulePausedMsg',
defaultMessage: 'Successfully paused schedule "{id}"',
},
pauseError: { id: 'schedulesView.pauseError', defaultMessage: 'Pause Schedule Error' },
scheduleUnpaused: { id: 'schedulesView.scheduleUnpaused', defaultMessage: 'Schedule Unpaused' },
scheduleUnpausedMsg: { id: 'schedulesView.scheduleUnpausedMsg', defaultMessage: 'Successfully unpaused schedule "{id}"' },
scheduleUnpausedMsg: {
id: 'schedulesView.scheduleUnpausedMsg',
defaultMessage: 'Successfully unpaused schedule "{id}"',
},
unpauseError: { id: 'schedulesView.unpauseError', defaultMessage: 'Unpause Schedule Error' },
jobKilled: { id: 'schedulesView.jobKilled', defaultMessage: 'Job Killed' },
killError: { id: 'schedulesView.killError', defaultMessage: 'Kill Job Error' },
jobInspection: { id: 'schedulesView.jobInspection', defaultMessage: 'Job Inspection' },
inspectNoInfo: { id: 'schedulesView.inspectNoInfo', defaultMessage: 'No detailed information available for this job' },
inspectNoInfo: {
id: 'schedulesView.inspectNoInfo',
defaultMessage: 'No detailed information available for this job',
},
inspectError: { id: 'schedulesView.inspectError', defaultMessage: 'Inspect Job Error' },
});
@ -493,7 +509,9 @@ const SchedulesView: React.FC<SchedulesViewProps> = ({ onClose: _onClose }) => {
className="flex items-center gap-2"
>
<RefreshCw className={`h-4 w-4 ${isRefreshing ? 'animate-spin' : ''}`} />
{isRefreshing ? intl.formatMessage(i18n.refreshing) : intl.formatMessage(i18n.refresh)}
{isRefreshing
? intl.formatMessage(i18n.refreshing)
: intl.formatMessage(i18n.refresh)}
</Button>
<Button
onClick={() => {
@ -519,7 +537,9 @@ const SchedulesView: React.FC<SchedulesViewProps> = ({ onClose: _onClose }) => {
<div className="h-full relative">
{apiError && (
<div className="mb-4 p-4 bg-background-danger border border-border-danger rounded-md">
<p className="text-text-danger text-sm">{intl.formatMessage(i18n.errorPrefix, { error: apiError })}</p>
<p className="text-text-danger text-sm">
{intl.formatMessage(i18n.errorPrefix, { error: apiError })}
</p>
</div>
)}

File diff suppressed because it is too large Load diff

View file

@ -1,212 +0,0 @@
import React from 'react';
import { MessageSquare, AlertCircle } from 'lucide-react';
import { defineMessages, useIntl } from '../../i18n';
import { Card } from '../ui/card';
import { Button } from '../ui/button';
import { ScrollArea } from '../ui/scroll-area';
import MarkdownContent from '../MarkdownContent';
import ToolCallWithResponse from '../ToolCallWithResponse';
import ImagePreview from '../ImagePreview';
import {
getTextAndImageContent,
getThinkingContent,
ToolRequestMessageContent,
ToolResponseMessageContent,
type Message,
} from '../../types/message';
import { formatMessageTimestamp } from '../../utils/timeUtils';
const i18n = defineMessages({
errorLoadingDetails: {
id: 'sessionViewComponents.error.loading',
defaultMessage: 'Error Loading Session Details',
},
tryAgain: {
id: 'sessionViewComponents.error.tryAgain',
defaultMessage: 'Try Again',
},
noMessages: {
id: 'sessionViewComponents.empty.title',
defaultMessage: 'No messages found',
},
noMessagesDesc: {
id: 'sessionViewComponents.empty.description',
defaultMessage: "This session doesn't contain any messages",
},
you: {
id: 'sessionViewComponents.role.user',
defaultMessage: 'You',
},
goose: {
id: 'sessionViewComponents.role.assistant',
defaultMessage: 'Goose',
},
});
/**
* Get tool responses map from messages
*/
export const getToolResponsesMap = (
messages: Message[],
messageIndex: number,
toolRequests: ToolRequestMessageContent[]
) => {
const responseMap = new Map();
if (messageIndex >= 0) {
for (let i = messageIndex + 1; i < messages.length; i++) {
const responses = messages[i].content
.filter((c) => c.type === 'toolResponse')
.map((c) => c as ToolResponseMessageContent);
for (const response of responses) {
const matchingRequest = toolRequests.find((req) => req.id === response.id);
if (matchingRequest) {
responseMap.set(response.id, response);
}
}
}
}
return responseMap;
};
interface SessionMessagesProps {
messages: Message[];
isLoading: boolean;
error: string | null;
onRetry: () => void;
}
/**
* Common component for displaying session messages
*/
export const SessionMessages: React.FC<SessionMessagesProps> = ({
messages,
isLoading,
error,
onRetry,
}) => {
const intl = useIntl();
return (
<ScrollArea className="h-full w-full">
<div className="p-4">
<div className="flex flex-col space-y-4">
<div className="space-y-4 mb-6">
{isLoading ? (
<div className="flex justify-center items-center py-12">
<div className="animate-spin rounded-full h-8 w-8 border-t-2 border-b-2"></div>
</div>
) : error ? (
<div className="flex flex-col items-center justify-center py-8 text-text-secondary">
<div className="text-red-500 mb-4">
<AlertCircle size={32} />
</div>
<p className="text-md mb-2">{intl.formatMessage(i18n.errorLoadingDetails)}</p>
<p className="text-sm text-center mb-4">{error}</p>
<Button onClick={onRetry} variant="default">
{intl.formatMessage(i18n.tryAgain)}
</Button>
</div>
) : messages?.length > 0 ? (
messages
.map((message, index) => {
const { textContent, imagePaths } = getTextAndImageContent(message);
const thinkingContent = getThinkingContent(message);
// Get tool requests from the message
const toolRequests = message.content
.filter((c) => c.type === 'toolRequest')
.map((c) => c as ToolRequestMessageContent);
// Get tool responses map using the helper function
const toolResponsesMap = getToolResponsesMap(messages, index, toolRequests);
// Skip pure tool response messages for cleaner display
const isOnlyToolResponse =
message.content.length > 0 &&
message.content.every((c) => c.type === 'toolResponse');
if (message.role === 'user' && isOnlyToolResponse) {
return null;
}
return (
<Card
key={index}
className={`p-4 ${
message.role === 'user'
? 'bg-bgSecondary border border-border-primary'
: 'bg-background-secondary'
}`}
>
<div className="flex justify-between items-center mb-2">
<span className="font-medium text-text-primary">
{message.role === 'user' ? intl.formatMessage(i18n.you) : intl.formatMessage(i18n.goose)}
</span>
<span className="text-xs text-text-secondary">
{formatMessageTimestamp(message.created)}
</span>
</div>
<div className="flex flex-col w-full">
{/* Thinking content */}
{thinkingContent && (
<div className="mb-2 text-sm text-gray-400 italic">
<MarkdownContent content={thinkingContent} />
</div>
)}
{/* Text content */}
{textContent && (
<div
className={`${toolRequests.length > 0 || imagePaths.length > 0 ? 'mb-4' : ''}`}
>
<MarkdownContent content={textContent} />
</div>
)}
{imagePaths.length > 0 && (
<div className="flex flex-wrap gap-2 mt-2 mb-2">
{imagePaths.map((imagePath, imageIndex) => (
<ImagePreview key={imageIndex} src={imagePath} />
))}
</div>
)}
{/* Tool requests and responses */}
{toolRequests.length > 0 && (
<div className="goose-message-tool bg-background-primary border border-border-primary dark:border-gray-700 rounded-b-2xl px-4 pt-4 pb-2 mt-1">
{toolRequests.map((toolRequest) => (
<ToolCallWithResponse
// In the session history page, if no tool response found for given request, it means the tool call
// is broken or cancelled.
isCancelledMessage={
toolResponsesMap.get(toolRequest.id) == undefined
}
isPendingApproval={false}
key={toolRequest.id}
toolRequest={toolRequest}
toolResponse={toolResponsesMap.get(toolRequest.id)}
/>
))}
</div>
)}
</div>
</Card>
);
})
.filter(Boolean) // Filter out null entries
) : (
<div className="flex flex-col items-center justify-center py-8 text-text-secondary">
<MessageSquare className="w-12 h-12 mb-4" />
<p className="text-lg mb-2">{intl.formatMessage(i18n.noMessages)}</p>
<p className="text-sm">{intl.formatMessage(i18n.noMessagesDesc)}</p>
</div>
)}
</div>
</div>
</div>
</ScrollArea>
);
};

View file

@ -56,12 +56,12 @@ const i18n = defineMessages({
},
certFingerprintHelp: {
id: 'externalBackendSection.certFingerprintHelp',
defaultMessage: 'Pin a specific TLS certificate fingerprint. If omitted, the certificate is trusted on first use (TOFU).',
defaultMessage:
'Pin a specific TLS certificate fingerprint. If omitted, the certificate is trusted on first use (TOFU).',
},
restartNote: {
id: 'externalBackendSection.restartNote',
defaultMessage:
'Changes apply to new chat windows. Restart Goose to update existing windows.',
defaultMessage: 'Changes apply to new chat windows. Restart Goose to update existing windows.',
},
urlProtocolError: {
id: 'externalBackendSection.urlProtocolError',
@ -77,7 +77,8 @@ const i18n = defineMessages({
},
urlBaseError: {
id: 'externalBackendSection.urlBaseError',
defaultMessage: 'URL must be the backend base URL before /acp, without query parameters or fragments',
defaultMessage:
'URL must be the backend base URL before /acp, without query parameters or fragments',
},
});
@ -95,10 +96,7 @@ export default function ExternalBackendSection() {
loadSettings();
}, []);
const validateUrl = (
value: string,
certFingerprint = config.certFingerprint
): boolean => {
const validateUrl = (value: string, certFingerprint = config.certFingerprint): boolean => {
if (!value) {
setUrlError(null);
return true;
@ -175,14 +173,14 @@ export default function ExternalBackendSection() {
<Card className="pb-2">
<CardHeader className="pb-0">
<CardTitle>{intl.formatMessage(i18n.title)}</CardTitle>
<CardDescription>
{intl.formatMessage(i18n.description)}
</CardDescription>
<CardDescription>{intl.formatMessage(i18n.description)}</CardDescription>
</CardHeader>
<CardContent className="pt-4 space-y-4 px-4">
<div className="flex items-center justify-between">
<div>
<h3 className="text-text-primary text-xs">{intl.formatMessage(i18n.useExternalServer)}</h3>
<h3 className="text-text-primary text-xs">
{intl.formatMessage(i18n.useExternalServer)}
</h3>
<p className="text-xs text-text-secondary max-w-md mt-[2px]">
{intl.formatMessage(i18n.useExternalServerDescription)}
</p>

View file

@ -65,7 +65,9 @@ describe('AuthSettingsSection', () => {
renderWithIntl(<AuthSettingsSection />);
expect(screen.getByText('Loading credentials...')).toBeInTheDocument();
expect(await screen.findByText('No locally stored provider credentials were found.')).toBeInTheDocument();
expect(
await screen.findByText('No locally stored provider credentials were found.')
).toBeInTheDocument();
});
it('renders provider credentials with storage and expiry status', async () => {
@ -98,9 +100,7 @@ describe('AuthSettingsSection', () => {
it('deletes a credential after confirmation and refreshes the list', async () => {
const user = userEvent.setup();
mockedListProviderSecrets
.mockResolvedValueOnce([providerSecret])
.mockResolvedValueOnce([]);
mockedListProviderSecrets.mockResolvedValueOnce([providerSecret]).mockResolvedValueOnce([]);
renderWithIntl(<AuthSettingsSection />);
@ -108,7 +108,9 @@ describe('AuthSettingsSection', () => {
await user.click(screen.getByRole('button', { name: 'Delete credential' }));
expect(screen.getByText('Delete the OPENAI_API_KEY credential for OpenAI?')).toBeInTheDocument();
expect(
screen.getByText('Delete the OPENAI_API_KEY credential for OpenAI?')
).toBeInTheDocument();
expect(
screen.getByText(
'This is the active provider. New requests may fail until you configure another credential.'
@ -123,7 +125,9 @@ describe('AuthSettingsSection', () => {
await waitFor(() => {
expect(mockedToast.success).toHaveBeenCalledWith('Credential deleted');
});
expect(await screen.findByText('No locally stored provider credentials were found.')).toBeInTheDocument();
expect(
await screen.findByText('No locally stored provider credentials were found.')
).toBeInTheDocument();
});
it('configures the permanent Hugging Face credential row', async () => {
@ -143,16 +147,14 @@ describe('AuthSettingsSection', () => {
configureProvider: 'huggingface',
};
mockedListProviderSecrets
.mockResolvedValueOnce([huggingFaceSecret])
.mockResolvedValueOnce([
{
...huggingFaceSecret,
configured: true,
hasSecret: true,
canDelete: true,
},
]);
mockedListProviderSecrets.mockResolvedValueOnce([huggingFaceSecret]).mockResolvedValueOnce([
{
...huggingFaceSecret,
configured: true,
hasSecret: true,
canDelete: true,
},
]);
renderWithIntl(<AuthSettingsSection />);

Some files were not shown because too many files have changed in this diff Show more