Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 19 additions & 2 deletions app/components/@settings/tabs/features/FeaturesTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -111,10 +111,12 @@ export default function FeaturesTab() {
isLatestBranch,
contextOptimizationEnabled,
eventLogs,
frameworkLock,
setAutoSelectTemplate,
enableLatestBranch,
enableContextOptimization,
setEventLogs,
setFrameworkLock,
setPromptId,
promptId,
} = useSettings();
Expand Down Expand Up @@ -169,12 +171,17 @@ export default function FeaturesTab() {
toast.success(`Event logging ${enabled ? 'enabled' : 'disabled'}`);
break;
}
case 'frameworkLock': {
setFrameworkLock(enabled);
toast.success(`Framework lock ${enabled ? 'enabled' : 'disabled'}`);
break;
}

default:
break;
}
},
[enableLatestBranch, setAutoSelectTemplate, enableContextOptimization, setEventLogs],
[enableLatestBranch, setAutoSelectTemplate, enableContextOptimization, setEventLogs, setFrameworkLock],
);

const features = {
Expand Down Expand Up @@ -212,7 +219,17 @@ export default function FeaturesTab() {
tooltip: 'Enabled by default to record detailed logs of system events and user actions',
},
],
beta: [],
beta: [
{
id: 'frameworkLock',
title: 'Framework Lock',
description: 'Keep the assistant aligned with the detected stack unless you ask to change it',
icon: 'i-ph:lock-closed',
enabled: frameworkLock,
beta: true,
tooltip: 'Uses package.json to detect the framework and keeps suggestions within that stack',
},
],
};

return (
Expand Down
29 changes: 26 additions & 3 deletions app/components/chat/Chat.client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@ import { useStore } from '@nanostores/react';
import type { Message } from 'ai';
import { useChat } from '@ai-sdk/react';
import { useAnimate } from 'framer-motion';
import { memo, useCallback, useEffect, useRef, useState } from 'react';
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { toast } from 'react-toastify';
import { useMessageParser, usePromptEnhancer, useShortcuts } from '~/lib/hooks';
import { description, useChatHistory } from '~/lib/persistence';
import { chatStore } from '~/lib/stores/chat';
import { workbenchStore } from '~/lib/stores/workbench';
import { DEFAULT_MODEL, DEFAULT_PROVIDER, PROMPT_COOKIE_KEY, PROVIDER_LIST } from '~/utils/constants';
import { DEFAULT_MODEL, DEFAULT_PROVIDER, PROMPT_COOKIE_KEY, PROVIDER_LIST, WORK_DIR } from '~/utils/constants';
import { cubicEasingFn } from '~/utils/easings';
import { createScopedLogger, renderLogger } from '~/utils/logger';
import { BaseChat } from './BaseChat';
Expand All @@ -28,6 +28,7 @@ import type { ElementInfo } from '~/components/workbench/Inspector';
import type { TextUIPart, FileUIPart, Attachment } from '@ai-sdk/ui-utils';
import { useMCPStore } from '~/lib/stores/mcp';
import type { LlmErrorAlertType } from '~/types/actions';
import { detectFrameworkFromFiles } from '~/utils/framework';

const logger = createScopedLogger('Chat');

Expand Down Expand Up @@ -100,7 +101,28 @@ export const ChatImpl = memo(
(project) => project.id === supabaseConn.selectedProjectId,
);
const supabaseAlert = useStore(workbenchStore.supabaseAlert);
const { activeProviders, promptId, autoSelectTemplate, contextOptimizationEnabled } = useSettings();
const { activeProviders, promptId, autoSelectTemplate, contextOptimizationEnabled, frameworkLock } = useSettings();
const packageJsonEntry = files[`${WORK_DIR}/package.json`] ?? files['package.json'];
const packageJsonContent = useMemo(() => {
if (!packageJsonEntry || packageJsonEntry.type !== 'file') {
return undefined;
}

return packageJsonEntry.content;
}, [packageJsonEntry]);
const frameworkHint = useMemo(() => {
if (!frameworkLock || !packageJsonContent) {
return undefined;
}

const detected = detectFrameworkFromFiles(files);

if (!detected) {
return undefined;
}

return `Detected framework: ${detected}. Stay within this stack unless the user explicitly asks to change frameworks.`;
}, [frameworkLock, packageJsonContent]);
const [llmErrorAlert, setLlmErrorAlert] = useState<LlmErrorAlertType | undefined>(undefined);
const [model, setModel] = useState(() => {
const savedModel = Cookies.get('selectedModel');
Expand Down Expand Up @@ -140,6 +162,7 @@ export const ChatImpl = memo(
contextOptimization: contextOptimizationEnabled,
chatMode,
designScheme,
frameworkHint,
supabase: {
isConnected: supabaseConn.isConnected,
hasSelectedProject: !!selectedProject,
Expand Down
12 changes: 12 additions & 0 deletions app/lib/.server/llm/stream-text.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ export async function streamText(props: {
messageSliceId?: number;
chatMode?: 'discuss' | 'build';
designScheme?: DesignScheme;
frameworkHint?: string;
}) {
const {
messages,
Expand All @@ -79,6 +80,7 @@ export async function streamText(props: {
summary,
chatMode,
designScheme,
frameworkHint,
} = props;
let currentModel = DEFAULT_MODEL;
let currentProvider = DEFAULT_PROVIDER.name;
Expand Down Expand Up @@ -219,6 +221,16 @@ export async function streamText(props: {
console.log('No locked files found from any source for prompt.');
}

if (frameworkHint) {
systemPrompt = `${systemPrompt}

PROJECT FRAMEWORK (DETECTED):
${sanitizeText(frameworkHint)}
IMPORTANT: Stay within this framework unless the user explicitly asks to change stacks.
---
`;
}

logger.info(`Sending llm call to ${provider.name} with model ${modelDetails.name}`);

// Log reasoning model detection and token parameters
Expand Down
12 changes: 12 additions & 0 deletions app/lib/hooks/useSettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
latestBranchStore,
autoSelectStarterTemplate,
enableContextOptimizationStore,
frameworkLockStore,
tabConfigurationStore,
resetTabConfiguration as resetTabConfig,
updateProviderSettings as updateProviderSettingsStore,
Expand All @@ -15,6 +16,7 @@ import {
updateContextOptimization,
updateEventLogs,
updatePromptId,
updateFrameworkLock,
} from '~/lib/stores/settings';
import { useCallback, useEffect, useState } from 'react';
import Cookies from 'js-cookie';
Expand Down Expand Up @@ -58,6 +60,8 @@ export interface UseSettingsReturn {
setAutoSelectTemplate: (enabled: boolean) => void;
contextOptimizationEnabled: boolean;
enableContextOptimization: (enabled: boolean) => void;
frameworkLock: boolean;
setFrameworkLock: (enabled: boolean) => void;

// Tab configuration
tabConfiguration: TabWindowConfig;
Expand All @@ -78,6 +82,7 @@ export function useSettings(): UseSettingsReturn {
const autoSelectTemplate = useStore(autoSelectStarterTemplate);
const [activeProviders, setActiveProviders] = useState<ProviderInfo[]>([]);
const contextOptimizationEnabled = useStore(enableContextOptimizationStore);
const frameworkLock = useStore(frameworkLockStore);
const tabConfiguration = useStore(tabConfigurationStore);
const [settings, setSettings] = useState<Settings>(() => {
const storedSettings = getLocalStorage('settings');
Expand Down Expand Up @@ -143,6 +148,11 @@ export function useSettings(): UseSettingsReturn {
logStore.logSystem(`Context optimization ${enabled ? 'enabled' : 'disabled'}`);
}, []);

const setFrameworkLock = useCallback((enabled: boolean) => {
updateFrameworkLock(enabled);
logStore.logSystem(`Framework lock ${enabled ? 'enabled' : 'disabled'}`);
}, []);

const setTheme = useCallback(
(theme: Settings['theme']) => {
saveSettings({ theme });
Expand Down Expand Up @@ -197,6 +207,8 @@ export function useSettings(): UseSettingsReturn {
setAutoSelectTemplate,
contextOptimizationEnabled,
enableContextOptimization,
frameworkLock,
setFrameworkLock,
setTheme,
setLanguage,
setNotifications,
Expand Down
8 changes: 8 additions & 0 deletions app/lib/stores/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,7 @@ const SETTINGS_KEYS = {
EVENT_LOGS: 'isEventLogsEnabled',
PROMPT_ID: 'promptId',
DEVELOPER_MODE: 'isDeveloperMode',
FRAMEWORK_LOCK: 'frameworkLock',
} as const;

// Initialize settings from localStorage or defaults
Expand Down Expand Up @@ -287,6 +288,7 @@ const getInitialSettings = () => {
eventLogs: getStoredBoolean(SETTINGS_KEYS.EVENT_LOGS, true),
promptId: isBrowser ? localStorage.getItem(SETTINGS_KEYS.PROMPT_ID) || 'default' : 'default',
developerMode: getStoredBoolean(SETTINGS_KEYS.DEVELOPER_MODE, false),
frameworkLock: getStoredBoolean(SETTINGS_KEYS.FRAMEWORK_LOCK, false),
};
};

Expand All @@ -298,6 +300,7 @@ export const autoSelectStarterTemplate = atom<boolean>(initialSettings.autoSelec
export const enableContextOptimizationStore = atom<boolean>(initialSettings.contextOptimization);
export const isEventLogsEnabled = atom<boolean>(initialSettings.eventLogs);
export const promptStore = atom<string>(initialSettings.promptId);
export const frameworkLockStore = atom<boolean>(initialSettings.frameworkLock);

// Helper functions to update settings with persistence
export const updateLatestBranch = (enabled: boolean) => {
Expand Down Expand Up @@ -325,6 +328,11 @@ export const updatePromptId = (id: string) => {
localStorage.setItem(SETTINGS_KEYS.PROMPT_ID, id);
};

export const updateFrameworkLock = (enabled: boolean) => {
frameworkLockStore.set(enabled);
localStorage.setItem(SETTINGS_KEYS.FRAMEWORK_LOCK, JSON.stringify(enabled));
};

// Initialize tab configuration from localStorage or defaults
const getInitialTabConfiguration = (): TabWindowConfig => {
const defaultConfig: TabWindowConfig = {
Expand Down
46 changes: 29 additions & 17 deletions app/routes/api.chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,24 +48,34 @@ async function chatAction({ context, request }: ActionFunctionArgs) {
},
});

const { messages, files, promptId, contextOptimization, supabase, chatMode, designScheme, maxLLMSteps } =
await request.json<{
messages: Messages;
files: any;
promptId?: string;
contextOptimization: boolean;
chatMode: 'discuss' | 'build';
designScheme?: DesignScheme;
supabase?: {
isConnected: boolean;
hasSelectedProject: boolean;
credentials?: {
anonKey?: string;
supabaseUrl?: string;
};
const {
messages,
files,
promptId,
contextOptimization,
supabase,
chatMode,
designScheme,
maxLLMSteps,
frameworkHint,
} = await request.json<{
messages: Messages;
files: any;
promptId?: string;
contextOptimization: boolean;
chatMode: 'discuss' | 'build';
designScheme?: DesignScheme;
frameworkHint?: string;
supabase?: {
isConnected: boolean;
hasSelectedProject: boolean;
credentials?: {
anonKey?: string;
supabaseUrl?: string;
};
maxLLMSteps: number;
}>();
};
maxLLMSteps: number;
}>();

const cookieHeader = request.headers.get('Cookie');
const apiKeys = JSON.parse(parseCookies(cookieHeader || '').apiKeys || '{}');
Expand Down Expand Up @@ -278,6 +288,7 @@ async function chatAction({ context, request }: ActionFunctionArgs) {
contextFiles: filteredFiles,
chatMode,
designScheme,
frameworkHint,
summary,
messageSliceId,
});
Expand Down Expand Up @@ -319,6 +330,7 @@ async function chatAction({ context, request }: ActionFunctionArgs) {
contextFiles: filteredFiles,
chatMode,
designScheme,
frameworkHint,
summary,
messageSliceId,
});
Expand Down
Loading
Loading