diff --git a/.env.example b/.env.example index f809c5d..84208e9 100644 --- a/.env.example +++ b/.env.example @@ -50,3 +50,20 @@ CLICKSTACK_OTLP_URL= # secret behind the /gate page. Leave both unset to run without a gate. ACCESS_CODES= ACCESS_TOKEN_SECRET= + +# DoorDash ordering (optional). Requires the official dd-cli on the worker +# and a personal account; docs/DEPLOYMENT.md covers the whole ritual. +# Approval codes are emailed here and nowhere else; whoever owns this inbox +# owns every order. +RENDI_OWNER_EMAIL= +# Local dev signs in with `dd-cli login` (keychain). Deployed workers are +# headless and need an exported token instead: `dd-cli export-token`. +DD_CLI_ACCESS_TOKEN= +# Spend caps, enforced below the agent. Defaults: $100 per order, 3 orders +# per day, 15 approval requests per day. +DD_MAX_ORDER_CENTS= +DD_MAX_ORDERS_PER_DAY= +DD_MAX_APPROVALS_PER_DAY= +# Set to 1 when strangers hold gate codes: hides saved addresses, payment +# methods, and order history from the agent entirely. +DD_HIDE_PERSONAL= diff --git a/.env.trigger.example b/.env.trigger.example index 9613383..b35e0e8 100644 --- a/.env.trigger.example +++ b/.env.trigger.example @@ -22,3 +22,14 @@ RENDI_MODEL=claude-opus-4-8 RENDI_APP_URL= GITHUB_TOKEN= + +# DoorDash ordering (optional; leave unset to run without it). Deployed +# workers are headless: mint the token with `dd-cli export-token` on a +# machine where you are signed in. Approval codes are emailed to the owner +# address and nowhere else. +DD_CLI_ACCESS_TOKEN= +RENDI_OWNER_EMAIL= +DD_MAX_ORDER_CENTS= +DD_MAX_ORDERS_PER_DAY= +DD_MAX_APPROVALS_PER_DAY= +DD_HIDE_PERSONAL= diff --git a/app/(app)/c/[id]/page.tsx b/app/(app)/c/[id]/page.tsx index e95482c..4e47a95 100644 --- a/app/(app)/c/[id]/page.tsx +++ b/app/(app)/c/[id]/page.tsx @@ -20,6 +20,7 @@ export default async function ConversationPage({ const [transcript, canvas] = conversation ? await Promise.all([getTranscript(id), loadCanvas(id)]) : [[], undefined]; + const archived = conversation ? isArchived(conversation.triggerEnv) : false; return ( <>
- An archived conversation from Rendi’s build days, kept as a - gallery. + This conversation is kept as a live gallery.
- Everything renders live. To talk with Rendi, start a new + Everything still renders. To talk with Rendi, start a new conversation.
{errorText}
+ ) : output?.denied ? ( ++ {output.denied} +
+ ) : approvalId ? ( + phase === "approved" ? ( ++ approved; rendi is placing the order +
+ ) : phase === "cancelled" ? ( ++ approval cancelled; the cart is open again +
+ ) : phase === "voided" || phase === "expired" ? ( ++ {note ?? DEAD_NOTES[phase === "expired" ? "expired" : "gone"]} +
+ ) : ( ++ A one-time code is in the owner’s inbox. Type it here; + it never goes through the chat. +
++ {note} +
++ interrupted +
+ ) : ( ++ {cart.quote?.asapAvailable + ? (cart.quote?.etaRange ?? "") + : "closed right now"} +
+Dasher tip
++ {failed} +
+{line.name}
+ {line.options.length > 0 ? ( ++ {line.options.map((option) => option.name).join(" · ")} +
+ ) : null} ++ ${((line.priceCents * line.quantity) / 100).toFixed(2)} +
+ ) : null} +{errorText}
+interrupted
+ ) : ( ++ nothing left to order from that cart +
++ an open cart already exists at {output.existingCart.storeName} + {output.existingCart.itemsCount + ? ` with ${output.existingCart.itemsCount} item${output.existingCart.itemsCount === 1 ? "" : "s"}` + : ""} + ; rendi is asking whether to extend or replace it +
+{item.item_name}
+ {(item.required_options ?? []).map((group) => ( ++ {group.name}:{" "} + {(group.options ?? []) + .map((option) => option.name) + .filter(Boolean) + .join(" / ")} +
+ ))} ++ tell rendi which, and it will add the item +
++ dasher tip is now ${(output.tipCents / 100).toFixed(2)} +
+{store.name}
++ {[ + store.rating != null ? `${store.rating.toFixed(1)}★` : null, + store.delivery_time, + distance, + ] + .filter(Boolean) + .join(" · ")} +
+ {store.printable_address ? ( ++ {store.printable_address} +
+ ) : null} +{item.name}
+ {item.price != null ? ( ++ ${item.price.toFixed(2)} +
+ ) : null} ++ {item.description} +
+ ) : null} +{errorText}
+ ) : output ? ( + stores.length > 0 ? ( ++ {output.message?.trim() || + `${summarize(verb, output)} · in hand`} +
+ ) + ) : interrupted ? ( ++ interrupted +
+ ) : ( ++ {line} +
+ ))} ++ interrupted +
+ ) : ( +RENDI ORDER
+${input.code}
+Type this code into the order card. It works once and dies in ${input.minutes} minutes; any change to the cart voids it. Not you? Ignore this and nothing happens.
+rendi.help
+