Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
59 commits
Select commit Hold shift + click to select a range
d8cde13
Onlool x Mastra
abhiaiyer91 Jul 22, 2025
1316f73
Remove .mastra folder
abhiaiyer91 Jul 22, 2025
836109c
more mastra
abhiaiyer91 Jul 22, 2025
204dbc3
add
Kitenite Jul 24, 2025
47ac4d5
merge main
Kitenite Jul 24, 2025
e18f8ea
clean up
Kitenite Jul 24, 2025
9ac9456
update types
Kitenite Jul 24, 2025
db2149e
update types
Kitenite Jul 24, 2025
e807a90
working ui
Kitenite Jul 24, 2025
c5eb8e5
more type check
Kitenite Jul 24, 2025
4056b3a
more type check
Kitenite Jul 25, 2025
fd6426b
more updates
Kitenite Jul 25, 2025
dd2e1d5
more updates
Kitenite Jul 25, 2025
7984818
merge main
Kitenite Jul 25, 2025
bce6c23
merge main
Kitenite Jul 28, 2025
26edb34
update mastra
Kitenite Jul 28, 2025
8145c9c
add oid
Kitenite Jul 28, 2025
802ea8e
Merge branch 'main' into mastra-in-onlook
Kitenite Jul 29, 2025
7377786
saving progress
Kitenite Jul 29, 2025
329c239
merge from main
Kitenite Aug 2, 2025
137ab62
Merge branch 'main' into mastra-in-onlook
Kitenite Aug 4, 2025
f3e3cc6
clean up
Kitenite Aug 4, 2025
99080e9
working write
Kitenite Aug 4, 2025
2146e55
working memory
Kitenite Aug 4, 2025
d102224
merge main
Kitenite Aug 9, 2025
5a4fe7d
merge main
Kitenite Aug 9, 2025
a5924cd
working everything besides storage
Kitenite Aug 9, 2025
15a58f0
working storage
Kitenite Aug 9, 2025
dc14508
working resubmit
Kitenite Aug 10, 2025
ddd3418
clean up
Kitenite Aug 10, 2025
de70a98
infinite loop issue
Kitenite Aug 10, 2025
08021e2
save progress
Kitenite Aug 10, 2025
98ecef0
merge main
Kitenite Aug 11, 2025
d540def
saving before custom storage
Kitenite Aug 11, 2025
f8b7f1e
remove mastra storage
Kitenite Aug 11, 2025
8163751
update saving
Kitenite Aug 12, 2025
ff2ff00
restore behavior without storage
Kitenite Aug 12, 2025
2ae8a33
clean up
Kitenite Aug 12, 2025
027dd82
clean up
Kitenite Aug 12, 2025
6c5a1db
working history again
Kitenite Aug 12, 2025
5f3f4c9
revert eslint
Kitenite Aug 12, 2025
c6efc0f
update
Kitenite Aug 12, 2025
298b3fe
clean up usage record
Kitenite Aug 12, 2025
8917c7c
clean up
Kitenite Aug 12, 2025
4a6e279
add decrement handler
Kitenite Aug 12, 2025
e5a318d
handle error handler
Kitenite Aug 12, 2025
6ce9d6d
update messages
Kitenite Aug 12, 2025
038a1e6
update suggestions
Kitenite Aug 12, 2025
36ff530
clean up
Kitenite Aug 12, 2025
768d176
update test
Kitenite Aug 12, 2025
c8e1102
working commit snapshot
Kitenite Aug 12, 2025
85b04d0
add token counter
Kitenite Aug 12, 2025
cbe95cd
add toolcall trimmer
Kitenite Aug 12, 2025
38f7cbd
add toolcall trimmer
Kitenite Aug 12, 2025
1151c4b
use claude on edit
Kitenite Aug 12, 2025
8a4a7f1
update test
Kitenite Aug 12, 2025
378599f
use sonnet for titles
Kitenite Aug 12, 2025
80ddce9
update conversation title
Kitenite Aug 12, 2025
41f8581
add backwards compatible params to messages
Kitenite Aug 12, 2025
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
5 changes: 4 additions & 1 deletion apps/web/client/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -45,5 +45,8 @@ yarn-error.log*
# idea files
.idea

# mastra
.mastra/

# Ignore preload script changes unless the script should be updated. Uncomment to update.
/public/onlook-preload-script.js
/public/onlook-preload-script.js
7 changes: 4 additions & 3 deletions apps/web/client/next.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,12 @@ import path from 'node:path';
import './src/env';

const nextConfig: NextConfig = {
devIndicators: false,
// TODO: Remove this once we have a proper ESLint and TypeScript config
devIndicators: {
buildActivity: false,
},
eslint: {
ignoreDuringBuilds: true,
}
},
};

if (process.env.NODE_ENV === 'development') {
Expand Down
5 changes: 4 additions & 1 deletion apps/web/client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@
"@codemirror/lang-javascript": "^6.2.3",
"@codemirror/lang-json": "^6.0.1",
"@codemirror/lang-markdown": "^6.1.7",
"@mastra/core": "^0.13.1",
"@mastra/memory": "^0.12.1",
"@mastra/pg": "^0.13.3",
"@onlook/constants": "*",
"@onlook/code-provider": "*",
"@onlook/db": "*",
Expand Down Expand Up @@ -100,7 +103,7 @@
"use-resize-observer": "^9.1.0",
"uuid": "^11.1.0",
"webfontloader": "^1.6.28",
"zod": "^3.24.3"
"zod": "^3"
},
"devDependencies": {
"@eslint/eslintrc": "^3.3.1",
Expand Down
22 changes: 11 additions & 11 deletions apps/web/client/public/onlook-preload-script.js

Large diffs are not rendered by default.

125 changes: 125 additions & 0 deletions apps/web/client/src/app/api/chat/helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import { createClient as createTRPCClient } from '@/trpc/request-server';
import { createClient as createSupabaseClient } from '@/utils/supabase/request-server';
import { initModel } from '@onlook/ai';
import { LLMProvider, OPENROUTER_MODELS, UsageType, type Usage } from '@onlook/models';
import { generateObject, NoSuchToolError, type ToolCall, type ToolSet } from 'ai';
import { type NextRequest } from 'next/server';

export const checkMessageLimit = async (req: NextRequest): Promise<{
exceeded: boolean;
usage: Usage;
}> => {
const { api } = await createTRPCClient(req);
const usage = await api.usage.get();

const dailyUsage = usage.daily;
const dailyExceeded = dailyUsage.usageCount >= dailyUsage.limitCount;
if (dailyExceeded) {
return {
exceeded: true,
usage: dailyUsage,
};
}

const monthlyUsage = usage.monthly;
const monthlyExceeded = monthlyUsage.usageCount >= monthlyUsage.limitCount;
if (monthlyExceeded) {
return {
exceeded: true,
usage: monthlyUsage,
};
}

return {
exceeded: false,
usage: monthlyUsage,
};
}

export const getSupabaseUser = async (request: NextRequest) => {
const supabase = await createSupabaseClient(request);
const { data: { user } } = await supabase.auth.getUser();
return user;
}

export const repairToolCall = async ({ toolCall, tools, error }: { toolCall: ToolCall<string, any>, tools: ToolSet, error: Error }) => {
if (NoSuchToolError.isInstance(error)) {
throw new Error(
`Tool "${toolCall.toolName}" not found. Available tools: ${Object.keys(tools).join(', ')}`,
);
}
const tool = tools[toolCall.toolName as keyof typeof tools];

console.warn(
`Invalid parameter for tool ${toolCall.toolName} with args ${JSON.stringify(toolCall.args)}, attempting to fix`,
);

const { model } = await initModel({
provider: LLMProvider.OPENROUTER,
model: OPENROUTER_MODELS.CLAUDE_4_SONNET,
});

const { object: repairedArgs } = await generateObject({
model,
schema: tool?.parameters,
prompt: [
`The model tried to call the tool "${toolCall.toolName}"` +
` with the following arguments:`,
JSON.stringify(toolCall.args),
`The tool accepts the following schema:`,
JSON.stringify(tool?.parameters),
'Please fix the arguments.',
].join('\n'),
});

return {
...toolCall,
args: JSON.stringify(repairedArgs),
toolCallType: 'function' as const
};
}

export const incrementUsage = async (req: NextRequest): Promise<{
usageRecordId: string | undefined,
rateLimitId: string | undefined,
} | null> => {
try {
const user = await getSupabaseUser(req);
if (!user) {
throw new Error('User not found');
}
const { api } = await createTRPCClient(req);
const incrementRes = await api.usage.increment({
type: UsageType.MESSAGE,
});
return {
usageRecordId: incrementRes?.usageRecordId,
rateLimitId: incrementRes?.rateLimitId,
};
} catch (error) {
console.error('Error in chat usage increment', error);
}
return null;
}

export const decrementUsage = async (
req: NextRequest,
usageRecord: {
usageRecordId: string | undefined,
rateLimitId: string | undefined,
} | null
): Promise<void> => {
try {
if (!usageRecord) {
return;
}
const { usageRecordId, rateLimitId } = usageRecord;
if (!usageRecordId || !rateLimitId) {
return;
}
const { api } = await createTRPCClient(req);
await api.usage.revertIncrement({ usageRecordId, rateLimitId });
} catch (error) {
console.error('Error in chat usage decrement', error);
}
}
179 changes: 40 additions & 139 deletions apps/web/client/src/app/api/chat/route.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,11 @@
import { env } from '@/env';
import { createClient as createTRPCClient } from '@/trpc/request-server';
import { mastra } from '@/mastra';
import { CHAT_TYPE_KEY, ONLOOK_AGENT_KEY, type OnlookAgentRuntimeContext } from '@/mastra/agents';
import { trackEvent } from '@/utils/analytics/server';
import { createClient as createSupabaseClient } from '@/utils/supabase/request-server';
import { ASK_TOOL_SET, BUILD_TOOL_SET, getAskModeSystemPrompt, getCreatePageSystemPrompt, getSystemPrompt, initModel } from '@onlook/ai';
import { ChatType, type InitialModelPayload, LLMProvider, OPENROUTER_MODELS, type Usage, UsageType } from '@onlook/models';
import { generateObject, NoSuchToolError, streamText } from 'ai';
import { RuntimeContext } from '@mastra/core/runtime-context';
import { convertToStreamMessages } from '@onlook/ai';
import { ChatType, type ChatMessage } from '@onlook/models';
import { type NextRequest } from 'next/server';

const isProd = env.NODE_ENV === 'production';

const MainModelConfig: InitialModelPayload = isProd ? {
provider: LLMProvider.OPENROUTER,
model: OPENROUTER_MODELS.OPEN_AI_GPT_5,
} : {
provider: LLMProvider.OPENROUTER,
model: OPENROUTER_MODELS.OPEN_AI_GPT_5,
};
import { checkMessageLimit, decrementUsage, getSupabaseUser, incrementUsage, repairToolCall } from './helpers';

export async function POST(req: NextRequest) {
try {
Expand All @@ -29,7 +19,6 @@ export async function POST(req: NextRequest) {
headers: { 'Content-Type': 'application/json' }
});
}

const usageCheckResult = await checkMessageLimit(req);
if (usageCheckResult.exceeded) {
trackEvent({
Expand Down Expand Up @@ -63,130 +52,37 @@ export async function POST(req: NextRequest) {
}
}

export const checkMessageLimit = async (req: NextRequest): Promise<{
exceeded: boolean;
usage: Usage;
}> => {
const { api } = await createTRPCClient(req);
const usage = await api.usage.get();

const dailyUsage = usage.daily;
const dailyExceeded = dailyUsage.usageCount >= dailyUsage.limitCount;
if (dailyExceeded) {
return {
exceeded: true,
usage: dailyUsage,
};
}

const monthlyUsage = usage.monthly;
const monthlyExceeded = monthlyUsage.usageCount >= monthlyUsage.limitCount;
if (monthlyExceeded) {
return {
exceeded: true,
usage: monthlyUsage,
};
}

return {
exceeded: false,
usage: monthlyUsage,
};
}

export const getSupabaseUser = async (request: NextRequest) => {
const supabase = await createSupabaseClient(request);
const { data: { user } } = await supabase.auth.getUser();
return user;
}

export const streamResponse = async (req: NextRequest) => {
const { messages, maxSteps, chatType } = await req.json();
const { model, providerOptions, headers, maxTokens } = await initModel(MainModelConfig);
const { messages, maxSteps, chatType }: { messages: ChatMessage[], maxSteps: number, chatType: ChatType } = await req.json();
const agent = mastra.getAgent(ONLOOK_AGENT_KEY);
const runtimeContext = new RuntimeContext<OnlookAgentRuntimeContext>()
runtimeContext.set(CHAT_TYPE_KEY, chatType);

// Updating the usage record and rate limit is done here to avoid
// abuse in the case where a single user sends many concurrent requests.
// If the call below fails, the user will not be penalized.
let usageRecordId: string | undefined;
let rateLimitId: string | undefined;
let usageRecord: {
usageRecordId: string | undefined;
rateLimitId: string | undefined;
} | null;
if (chatType === ChatType.EDIT) {
const user = await getSupabaseUser(req);
if (!user) {
throw new Error('User not found');
}
const { api } = await createTRPCClient(req);
const incrementRes = await api.usage.increment({
type: UsageType.MESSAGE,
});
usageRecordId = incrementRes?.usageRecordId;
rateLimitId = incrementRes?.rateLimitId;
}

let systemPrompt: string;
switch (chatType) {
case ChatType.CREATE:
systemPrompt = getCreatePageSystemPrompt();
break;
case ChatType.ASK:
systemPrompt = getAskModeSystemPrompt();
break;
case ChatType.EDIT:
default:
systemPrompt = getSystemPrompt();
break;
usageRecord = await incrementUsage(req);
}
const toolSet = chatType === ChatType.ASK ? ASK_TOOL_SET : BUILD_TOOL_SET;
const result = streamText({
model,
headers,
messages: [
{
role: 'system',
content: systemPrompt,
providerOptions,
},
...messages,
],
const result = await agent.stream(convertToStreamMessages(messages), {
headers: {
'HTTP-Referer': 'https://onlook.com',
'X-Title': 'Onlook',
},
maxSteps,
tools: toolSet,
runtimeContext,
toolCallStreaming: true,
experimental_repairToolCall: async ({ toolCall, tools, parameterSchema, error }) => {
if (NoSuchToolError.isInstance(error)) {
throw new Error(
`Tool "${toolCall.toolName}" not found. Available tools: ${Object.keys(tools).join(', ')}`,
);
}
const tool = tools[toolCall.toolName as keyof typeof tools];

console.warn(
`Invalid parameter for tool ${toolCall.toolName} with args ${JSON.stringify(toolCall.args)}, attempting to fix`,
);

const { object: repairedArgs } = await generateObject({
model,
schema: tool?.parameters,
prompt: [
`The model tried to call the tool "${toolCall.toolName}"` +
` with the following arguments:`,
JSON.stringify(toolCall.args),
`The tool accepts the following schema:`,
JSON.stringify(parameterSchema(toolCall)),
'Please fix the arguments.',
].join('\n'),
});

return { ...toolCall, args: JSON.stringify(repairedArgs) };
},
experimental_repairToolCall: repairToolCall,
onError: async (error) => {
console.error('Error in chat', error);
// if there was an error with the API, do not penalize the user
if (usageRecordId && rateLimitId) {
await createTRPCClient(req)
.then(({ api }) => api.usage.revertIncrement({ usageRecordId, rateLimitId }))
.catch(error => console.error('Error in chat usage decrement', error));
}
},
});
await decrementUsage(req, usageRecord);
}
})

return result.toDataStreamResponse(
{
Expand All @@ -196,17 +92,22 @@ export const streamResponse = async (req: NextRequest) => {
}

export function errorHandler(error: unknown) {
if (error == null) {
return 'unknown error';
}
try {
console.error('Error in chat', error);
if (!error) {
return 'unknown error';
}

if (typeof error === 'string') {
return error;
}
if (typeof error === 'string') {
return error;
}

if (error instanceof Error) {
return error.message;
if (error instanceof Error) {
return error.message;
}
return JSON.stringify(error);
} catch (error) {
console.error('Error in errorHandler', error);
return 'unknown error';
}

return JSON.stringify(error);
}
Loading