diff --git a/app/components/chat/ToolCall.tsx b/app/components/chat/ToolCall.tsx
index 4d64a79cc..c78a2de0a 100644
--- a/app/components/chat/ToolCall.tsx
+++ b/app/components/chat/ToolCall.tsx
@@ -21,6 +21,7 @@ import { npmInstallToolParameters } from '~/lib/runtime/npmInstallTool';
import { loggingSafeParse } from '~/lib/zodUtil';
import { deployToolParameters } from '~/lib/runtime/deployTool';
import type { ZodError } from 'zod';
+import { getRelativePath } from '~/lib/stores/files';
export const ToolCall = memo((props: { partId: PartId; toolCallId: string }) => {
const { partId, toolCallId } = props;
@@ -382,7 +383,7 @@ function toolTitle(invocation: ConvexToolInvocation): React.ReactNode {
extra = ` (lines ${start} - ${endName})`;
}
if (args.success) {
- renderedPath = args.data.path || '/home/project';
+ renderedPath = getRelativePath(args.data.path) || '/home/project';
}
return (
@@ -418,9 +419,9 @@ function toolTitle(invocation: ConvexToolInvocation): React.ReactNode {
} else if (invocation.result?.startsWith('Error:')) {
if (
// This is a hack, but `npx convex dev` prints this out when the typecheck fails
- invocation.result.includes('To ignore failing typecheck') ||
+ invocation.result.includes('[ConvexTypecheck]') ||
// this is a bigger hack! TypeScript fails with error codes like TS
- invocation.result.includes('Error TS')
+ invocation.result.includes('[FrontendTypecheck]')
) {
return (
diff --git a/app/lib/runtime/action-runner.ts b/app/lib/runtime/action-runner.ts
index 8542f0387..4dafba574 100644
--- a/app/lib/runtime/action-runner.ts
+++ b/app/lib/runtime/action-runner.ts
@@ -372,31 +372,80 @@ export class ActionRunner {
case 'deploy': {
const container = await this.#webcontainer;
await waitForContainerBootState(ContainerBootState.READY);
- const convexProc = await container.spawn('sh', [
- '-c',
- 'convex dev --once && tsc --noEmit -p tsconfig.app.json',
- ]);
- action.abortSignal.addEventListener('abort', () => {
- convexProc.kill();
- });
+ result = '';
+
+ const convexTypecheckProc = await container.spawn('npx', ['tsc', '-p', 'convex']);
+ let abortListener: () => void = () => {
+ convexTypecheckProc.kill();
+ };
+ action.abortSignal.addEventListener('abort', abortListener);
- const { output, exitCode } = await streamOutput(convexProc, {
+ const { output: convexTypecheckOutput, exitCode: convexTypecheckExitCode } = await streamOutput(
+ convexTypecheckProc,
+ {
+ onOutput: (output) => {
+ this.terminalOutput.set(output);
+ },
+ debounceMs: 50,
+ },
+ );
+ const cleanedTypecheckOutput = cleanConvexOutput(convexTypecheckOutput);
+ if (convexTypecheckExitCode !== 0) {
+ throw new Error(
+ `[ConvexTypecheck] Failed with exit code ${convexTypecheckExitCode}: ${cleanedTypecheckOutput}`,
+ );
+ }
+ result += cleanedTypecheckOutput + '\n\n';
+ action.abortSignal.removeEventListener('abort', abortListener);
+
+ const convexDevProc = await container.spawn('npx', ['convex', 'dev', '--once', '--typecheck', 'disable']);
+ abortListener = () => {
+ convexDevProc.kill();
+ };
+ action.abortSignal.addEventListener('abort', abortListener);
+
+ const { output: convexDevOutput, exitCode: convexDevExitCode } = await streamOutput(convexDevProc, {
onOutput: (output) => {
- this.terminalOutput.set(output);
+ this.terminalOutput.set(result + output);
},
debounceMs: 50,
});
- const cleanedOutput = cleanConvexOutput(output);
- if (exitCode !== 0) {
- throw new Error(`Convex failed with exit code ${exitCode}: ${cleanedOutput}`);
+ const cleanedDevOutput = cleanConvexOutput(convexDevOutput);
+ if (convexDevExitCode !== 0) {
+ throw new Error(`[ConvexDev] Failed with exit code ${convexDevExitCode}: ${cleanedDevOutput}`);
+ }
+ result += cleanedDevOutput + '\n\n';
+ action.abortSignal.removeEventListener('abort', abortListener);
+
+ const frontendTypecheckProc = await container.spawn('npx', ['tsc', '--noEmit', '-p', 'tsconfig.app.json']);
+ abortListener = () => {
+ frontendTypecheckProc.kill();
+ };
+ action.abortSignal.addEventListener('abort', abortListener);
+
+ const { output: frontendTypecheckOutput, exitCode: frontendTypecheckExitCode } = await streamOutput(
+ frontendTypecheckProc,
+ {
+ onOutput: (output) => {
+ this.terminalOutput.set(result + output);
+ },
+ debounceMs: 50,
+ },
+ );
+ const cleanedFrontendTypecheckOutput = cleanConvexOutput(frontendTypecheckOutput);
+ if (frontendTypecheckExitCode !== 0) {
+ throw new Error(
+ `[FrontendTypecheck] Failed with exit code ${frontendTypecheckExitCode}: ${cleanedFrontendTypecheckOutput}`,
+ );
}
- result = cleanedOutput;
+ result += cleanedFrontendTypecheckOutput + '\n\n';
+ action.abortSignal.removeEventListener('abort', abortListener);
// Start the default preview if it’s not already running
if (!workbenchStore.isDefaultPreviewRunning()) {
const shell = this.#shellTerminal();
await shell.startCommand('npx vite --open');
- result += '\n\nDev server started successfully!';
+ result += 'Dev server started successfully!';
}
break;
diff --git a/convex/openaiProxy.ts b/convex/openaiProxy.ts
index df0cfe82b..fdb6da866 100644
--- a/convex/openaiProxy.ts
+++ b/convex/openaiProxy.ts
@@ -1,115 +1,121 @@
-import { ConvexError, v } from "convex/values";
-import { httpAction, internalMutation, mutation } from "./_generated/server";
-import { getCurrentMember } from "./sessions";
-import { internal } from "./_generated/api";
+import { ConvexError, v } from 'convex/values';
+import { httpAction, internalMutation, mutation } from './_generated/server';
+import { getCurrentMember } from './sessions';
+import { internal } from './_generated/api';
export const openaiProxy = httpAction(async (ctx, req) => {
- if (!openaiProxyEnabled()) {
- return new Response("Convex OpenAI proxy is disabled.", { status: 400 });
- }
- if (!process.env.OPENAI_API_KEY) {
- throw new Error("OPENAI_API_KEY is not set");
- }
- const headers = new Headers(req.headers);
- const authHeader = headers.get("Authorization");
- if (!authHeader) {
- return new Response("Unauthorized", { status: 401 });
- }
- if (!authHeader.startsWith("Bearer ")) {
- return new Response("Invalid authorization header", { status: 401 });
- }
- const token = authHeader.slice(7);
- const result = await ctx.runMutation(internal.openaiProxy.decrementToken, { token });
- if (!result.success) {
- return new Response(result.error, { status: 401 });
- }
+ if (!openaiProxyEnabled()) {
+ return new Response('Convex OpenAI proxy is disabled.', { status: 400 });
+ }
+ if (!process.env.OPENAI_API_KEY) {
+ throw new Error('OPENAI_API_KEY is not set');
+ }
+ const headers = new Headers(req.headers);
+ const authHeader = headers.get('Authorization');
+ if (!authHeader) {
+ return new Response('Unauthorized', { status: 401 });
+ }
+ if (!authHeader.startsWith('Bearer ')) {
+ return new Response('Invalid authorization header', { status: 401 });
+ }
+ const token = authHeader.slice(7);
+ const result = await ctx.runMutation(internal.openaiProxy.decrementToken, { token });
+ if (!result.success) {
+ return new Response(result.error, { status: 401 });
+ }
- let body: any;
- try {
- body = await req.json();
- } catch (error) {
- return new Response("Invalid request body", { status: 400 });
- }
- if (body.model != 'gpt-4o-mini') {
- return new Response("Only gpt-4o-mini is supported", { status: 400 });
- }
+ let body: any;
+ try {
+ body = await req.json();
+ } catch (error) {
+ return new Response('Invalid request body', { status: 400 });
+ }
+ if (body.model != 'gpt-4o-mini') {
+ return new Response('Only gpt-4o-mini is supported', { status: 400 });
+ }
- const response = await fetch("https://api.openai.com/v1/chat/completions", {
- method: "POST",
- headers: {
- "Content-Type": "application/json",
- "Authorization": `Bearer ${process.env.OPENAI_API_KEY}`,
- },
- body: JSON.stringify(body),
- });
- return response;
+ const response = await fetch('https://api.openai.com/v1/chat/completions', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ Authorization: `Bearer ${process.env.OPENAI_API_KEY}`,
+ },
+ body: JSON.stringify(body),
+ });
+ return response;
});
export const issueOpenAIToken = mutation({
- handler: async (ctx) => {
- if (!openaiProxyEnabled()) {
- return null;
- }
- const member = await getCurrentMember(ctx);
- if (!member) {
- console.error("Not authorized", member)
- return null;
- }
- const existing = await ctx.db.query('memberOpenAITokens')
- .withIndex('byMemberId', (q) => q.eq('memberId', member._id))
- .unique();
- if (existing) {
- return existing.token;
- }
- const token = crypto.randomUUID();
- await ctx.db.insert('memberOpenAITokens', {
- memberId: member._id,
- token,
- requestsRemaining: included4oMiniRequests(),
- lastUsedTime: 0,
- });
- return token;
+ handler: async (ctx) => {
+ if (!openaiProxyEnabled()) {
+ return null;
+ }
+ const member = await getCurrentMember(ctx);
+ if (!member) {
+ console.error('Not authorized', member);
+ return null;
}
-})
+ const existing = await ctx.db
+ .query('memberOpenAITokens')
+ .withIndex('byMemberId', (q) => q.eq('memberId', member._id))
+ .unique();
+ if (existing) {
+ return existing.token;
+ }
+ const token = crypto.randomUUID();
+ await ctx.db.insert('memberOpenAITokens', {
+ memberId: member._id,
+ token,
+ requestsRemaining: included4oMiniRequests(),
+ lastUsedTime: 0,
+ });
+ return token;
+ },
+});
export const decrementToken = internalMutation({
- args: {
- token: v.string(),
- },
- handler: async (ctx, args) => {
- if (!openaiProxyEnabled()) {
- return { success: false, error: "Convex OpenAI proxy is disabled."};
- }
- const token = await ctx.db.query('memberOpenAITokens')
- .withIndex('byToken', (q) => q.eq('token', args.token))
- .unique();
- if (!token) {
- return { success: false, error: "Invalid OPENAI_API_TOKEN" };
- }
- if (token.requestsRemaining <= 0) {
- return { success: false, error: "Convex OPENAI_API_TOKEN has no requests remaining. Go sign up for an OpenAI API key at https://platform.openai.com and update your app to use that." };
- }
- await ctx.db.patch(token._id, {
- requestsRemaining: token.requestsRemaining - 1,
- lastUsedTime: Date.now(),
- });
- return { success: true };
+ args: {
+ token: v.string(),
+ },
+ handler: async (ctx, args) => {
+ if (!openaiProxyEnabled()) {
+ return { success: false, error: 'Convex OpenAI proxy is disabled.' };
+ }
+ const token = await ctx.db
+ .query('memberOpenAITokens')
+ .withIndex('byToken', (q) => q.eq('token', args.token))
+ .unique();
+ if (!token) {
+ return { success: false, error: 'Invalid OPENAI_API_TOKEN' };
}
-})
+ if (token.requestsRemaining <= 0) {
+ return {
+ success: false,
+ error:
+ 'Convex OPENAI_API_TOKEN has no requests remaining. Go sign up for an OpenAI API key at https://platform.openai.com and update your app to use that.',
+ };
+ }
+ await ctx.db.patch(token._id, {
+ requestsRemaining: token.requestsRemaining - 1,
+ lastUsedTime: Date.now(),
+ });
+ return { success: true };
+ },
+});
// Cost per gpt-4o-mini request (2025-04-09):
// 16384 max output tokens @ $0.6/1M
// 128K max input tokens @ $0.15/1M
// => ~$0.03 per request.
function included4oMiniRequests() {
- const fromEnv = process.env.OPENAI_PROXY_INCLUDED_REQUESTS;
- if (!fromEnv) {
- return 100;
- }
- return Number(fromEnv);
+ const fromEnv = process.env.OPENAI_PROXY_INCLUDED_REQUESTS;
+ if (!fromEnv) {
+ return 100;
+ }
+ return Number(fromEnv);
}
function openaiProxyEnabled() {
- const fromEnv = process.env.OPENAI_PROXY_ENABLED;
- return fromEnv && fromEnv == "1";
-}
\ No newline at end of file
+ const fromEnv = process.env.OPENAI_PROXY_ENABLED;
+ return fromEnv && fromEnv == '1';
+}