diff --git a/app/lib/persistence/message.ts b/app/lib/persistence/message.ts
index 83074627..da397ca2 100644
--- a/app/lib/persistence/message.ts
+++ b/app/lib/persistence/message.ts
@@ -8,6 +8,11 @@ interface MessageBase {
id: string;
role: MessageRole;
repositoryId?: string;
+ peanuts?: number;
+
+ // Not part of the protocol, indicates whether the user has explicitly approved
+ // the message. Once approved, the approve/reject UI is not shown again for the message.
+ approved?: boolean;
}
interface MessageText extends MessageBase {
diff --git a/app/lib/replay/DevelopmentServer.ts b/app/lib/replay/DevelopmentServer.ts
index f4efee94..1a3f8d26 100644
--- a/app/lib/replay/DevelopmentServer.ts
+++ b/app/lib/replay/DevelopmentServer.ts
@@ -56,20 +56,28 @@ class DevelopmentServerManager {
let gActiveDevelopmentServer: DevelopmentServerManager | undefined;
-export async function updateDevelopmentServer(repositoryId: string) {
+export async function updateDevelopmentServer(repositoryId: string | undefined) {
console.log('UpdateDevelopmentServer', new Date().toISOString(), repositoryId);
- workbenchStore.showWorkbench.set(true);
+ workbenchStore.showWorkbench.set(repositoryId !== undefined);
workbenchStore.repositoryId.set(repositoryId);
workbenchStore.previewURL.set(undefined);
workbenchStore.previewError.set(false);
+ if (!repositoryId) {
+ return;
+ }
+
if (!gActiveDevelopmentServer) {
gActiveDevelopmentServer = new DevelopmentServerManager();
}
const url = await gActiveDevelopmentServer.setRepositoryContents(repositoryId);
+ if (workbenchStore.repositoryId.get() != repositoryId) {
+ return;
+ }
+
if (url) {
workbenchStore.previewURL.set(url);
} else {
diff --git a/app/lib/replay/SimulationPrompt.ts b/app/lib/replay/SimulationPrompt.ts
index 5f606fa8..0c2c9ee7 100644
--- a/app/lib/replay/SimulationPrompt.ts
+++ b/app/lib/replay/SimulationPrompt.ts
@@ -11,6 +11,7 @@ import type { Message } from '~/lib/persistence/message';
import { database } from '~/lib/persistence/db';
import { chatStore } from '~/lib/stores/chat';
import { debounce } from '~/utils/debounce';
+import { getSupabase } from '~/lib/supabase/client';
function createRepositoryIdPacket(repositoryId: string): SimulationPacket {
return {
@@ -64,6 +65,15 @@ class ChatManager {
await this.client.initialize();
+ const {
+ data: { user },
+ } = await getSupabase().auth.getUser();
+ const userId = user?.id || null;
+
+ if (userId) {
+ await this.client.sendCommand({ method: 'Nut.setUserId', params: { userId } });
+ }
+
const { chatId } = (await this.client.sendCommand({ method: 'Nut.startChat', params: {} })) as { chatId: string };
console.log('ChatStarted', new Date().toISOString(), chatId);
@@ -235,7 +245,7 @@ function startChat(repositoryId: string | undefined, pageData: SimulationData) {
* Called when the repository has changed. We'll start a new chat
* and update the remote development server.
*/
-export const simulationRepositoryUpdated = debounce((repositoryId: string) => {
+export const simulationRepositoryUpdated = debounce((repositoryId: string | undefined) => {
startChat(repositoryId, []);
updateDevelopmentServer(repositoryId);
}, 500);
diff --git a/app/lib/supabase/peanuts.ts b/app/lib/supabase/peanuts.ts
new file mode 100644
index 00000000..fa89c462
--- /dev/null
+++ b/app/lib/supabase/peanuts.ts
@@ -0,0 +1,37 @@
+import { getSupabase } from './client';
+
+export async function supabaseAddRefund(peanuts: number) {
+ const supabase = getSupabase();
+
+ // Get the current user ID if available
+ const {
+ data: { user },
+ } = await supabase.auth.getUser();
+ const userId = user?.id || null;
+
+ const { data, error } = await supabase.from('profiles').select('peanuts_refunded').eq('id', userId).single();
+
+ if (error) {
+ console.error('AddPeanutsRefund:ErrorFetchingData', { error });
+ return;
+ }
+
+ const currentPeanutsRefunded = data.peanuts_refunded;
+ if (typeof currentPeanutsRefunded !== 'number') {
+ console.error('AddPeanutsRefund:InvalidPeanutsRefunded', { currentPeanutsRefunded });
+ return;
+ }
+
+ const newPeanutsRefunded = Math.round(currentPeanutsRefunded + peanuts);
+
+ // Note: this is not atomic.
+ // https://linear.app/replay/issue/PRO-1122/update-api-usage-atomically
+ const { error: updateError } = await supabase
+ .from('profiles')
+ .update({ peanuts_refunded: newPeanutsRefunded })
+ .eq('id', userId);
+
+ if (updateError) {
+ console.error('AddPeanutsRefund:ErrorUpdatingData', { updateError });
+ }
+}
diff --git a/app/routes/about.tsx b/app/routes/about.tsx
index aa16aadf..afcc86a5 100644
--- a/app/routes/about.tsx
+++ b/app/routes/about.tsx
@@ -15,10 +15,9 @@ function AboutPage() {
About Nut
- Nut is an agentic app builder for reliably developing full stack apps using AI.
- When you ask Nut to build or change an app, it will do its best to get the code
- changes right the first time. Afterwards it will check the app to make sure it's
- working as expected, writing tests and fixing problems those tests uncover.
+ Nut is an agentic app builder for reliably developing full stack apps using AI. When you ask Nut to build or
+ change an app, it will do its best to get the code changes right the first time. Afterwards it will check
+ the app to make sure it's working as expected, writing tests and fixing problems those tests uncover.
@@ -45,9 +44,8 @@ function AboutPage() {
rel="noopener noreferrer"
>
Replay.io
- {' '} team.
- We'd love to hear from you! Leave us some feedback at the top of the page,
- join our{' '}
+ {' '}
+ team. We'd love to hear from you! Leave us some feedback at the top of the page, join our{' '}
Discord
{' '}
- or reach us at {' '}
+ or reach us at{' '}
hi@replay.io
- .
+
+ .
diff --git a/supabase/migrations/2025035000000_create_profiles_table.sql b/supabase/migrations/2025035000000_create_profiles_table.sql
index c8ece0ae..247e441c 100644
--- a/supabase/migrations/2025035000000_create_profiles_table.sql
+++ b/supabase/migrations/2025035000000_create_profiles_table.sql
@@ -6,7 +6,9 @@ CREATE TABLE IF NOT EXISTS public.profiles (
username TEXT UNIQUE,
full_name TEXT,
avatar_url TEXT,
- is_admin BOOLEAN DEFAULT FALSE NOT NULL
+ is_admin BOOLEAN DEFAULT FALSE NOT NULL,
+ peanuts_used INTEGER DEFAULT 0 NOT NULL,
+ peanuts_refunded INTEGER DEFAULT 0 NOT NULL
);
-- Create a trigger to update the updated_at column