<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Claire Y</title>
    <description>The latest articles on DEV Community by Claire Y (@claire_y).</description>
    <link>https://dev.to/claire_y</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4138466%2Fa932b094-c0cd-4dc2-9fc2-b3876a4443b1.jpg</url>
      <title>DEV Community: Claire Y</title>
      <link>https://dev.to/claire_y</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/claire_y"/>
    <language>en</language>
    <item>
      <title>Round Chat: a floating partner chat for debate rounds</title>
      <dc:creator>Claire Y</dc:creator>
      <pubDate>Wed, 23 Sep 2026 13:59:49 +0000</pubDate>
      <link>https://dev.to/claire_y/round-chat-a-floating-partner-chat-for-debate-rounds-2ko2</link>
      <guid>https://dev.to/claire_y/round-chat-a-floating-partner-chat-for-debate-rounds-2ko2</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/sanity-2026-09-16"&gt;Sanity Challenge, Path Two: Vibe-Code Something Strange&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Round Chat&lt;/strong&gt; is a tiny chat window for debate partners that floats on top of every other window during a round. When you're flowing on one tab and reading evidence on another, you can still see what your partner just sent, without switching tabs.&lt;/p&gt;

&lt;p&gt;This communication window does three things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Chat with your partner&lt;/strong&gt;, the main function split into &lt;strong&gt;speech tabs&lt;/strong&gt; (Constructive, CX, 1NC…), so a note about the constructive stays with the constructive tab. Tabs with unread messages get a red dot.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A stopwatch&lt;/strong&gt; in the header for timing speeches mid-round.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pop out.&lt;/strong&gt; One click puts the chat in an always-on-top window, using Chrome's Document Picture-in-Picture API.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Everything else is in the content model:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The round's life is a workflow stored as data.&lt;/strong&gt; Draft → Ready → In round → Finished. An AI agent can draft a round, but only a person can approve it. The chat only opens in states the workflow marks as &lt;code&gt;chatOpen&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Each round has a private join code&lt;/strong&gt;, so strangers can't infiltrate, even though the dataset is public.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Round Control&lt;/strong&gt;, an App SDK app, shows every round, its workflow history, and its messages in real time.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;As a participant in Public Forum debate, I realize traditional note passing during rounds is both time-consuming and unusable in the digital age, when speeches are both delivered and drafted digitally. Creating this chat window can both expedite communication and retain accessibility regardless of computer software. &lt;/p&gt;

&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Live site:&lt;/strong&gt; &lt;a href="https://debate-round-chat.vercel.app" rel="noopener noreferrer"&gt;https://debate-round-chat.vercel.app&lt;/a&gt;. You'll need a round's join code to get into a chat.&lt;/p&gt;

&lt;p&gt;The chat at the size of the pop-out window. The red dot on &lt;strong&gt;1AC&lt;/strong&gt; means my partner posted there and I haven't looked yet:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjvsr3v5zo74j8s5od2tz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjvsr3v5zo74j8s5od2tz.png" alt="The chat at pop-out size, with a stopwatch running and an unread dot on the 1AC tab" width="760" height="1120"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Each speech tab keeps its own thread:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fq4jr5xv58jzgd5ec27vb.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fq4jr5xv58jzgd5ec27vb.png" alt="The 1AC tab showing a partner's note about the plan text" width="760" height="1120"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Joining a round takes a name and the round's join code:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8tjh93htge9xtf4wqumr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8tjh93htge9xtf4wqumr.png" alt="Join screen with name and join code fields" width="800" height="1067"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Only rounds the workflow has opened for chat are listed:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fv189h7dev6trjy7ei806.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fv189h7dev6trjy7ei806.png" alt="Home page listing the practice round, which is In round" width="800" height="378"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Only people have control of initializing chat windows; agents play a low role in the functionality of the app.&lt;/p&gt;

&lt;h2&gt;
  
  
  Code
&lt;/h2&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/claire-yuan-xi" rel="noopener noreferrer"&gt;
        claire-yuan-xi
      &lt;/a&gt; / &lt;a href="https://github.com/claire-yuan-xi/Debate-Round-Chat" rel="noopener noreferrer"&gt;
        Debate-Round-Chat
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      
    &lt;/h3&gt;
  &lt;/div&gt;
&lt;/div&gt;


&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Folder&lt;/th&gt;
&lt;th&gt;What's in it&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/claire-yuan-xi/Debate-Round-Chat/tree/main/studio" rel="noopener noreferrer"&gt;&lt;code&gt;studio/&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Sanity Studio: schemas, the sidebar built from the workflow, the Move round action and in-form workflow buttons, the join code panel, and &lt;code&gt;scripts/agent.ts&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/claire-yuan-xi/Debate-Round-Chat/tree/main/app" rel="noopener noreferrer"&gt;&lt;code&gt;app/&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Round Control, an App SDK app&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/claire-yuan-xi/Debate-Round-Chat/tree/main/web" rel="noopener noreferrer"&gt;&lt;code&gt;web/&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;The Next.js 16 chat, with the pop-out window, live updates, server actions and join code checks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/claire-yuan-xi/Debate-Round-Chat/tree/main/shared" rel="noopener noreferrer"&gt;&lt;code&gt;shared/&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;The workflow engine and join code helpers used by the Studio, the app and the agent&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  My Build Process
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Tool:&lt;/strong&gt; Claude Code in the Claude desktop app (Code tab), running Claude Opus 5.5, with the &lt;code&gt;sanity-best-practices&lt;/code&gt; agent skill. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stack:&lt;/strong&gt; a standalone Sanity Studio, an App SDK app, and a Next.js 16 site, side by side in one repo, plus a small &lt;code&gt;shared/&lt;/code&gt; folder of workflow logic used by the Studio, the App SDK app and an agent script.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Setup, and a false start
&lt;/h3&gt;

&lt;p&gt;I started with Sanity's own onboarding prompt ("set up Sanity using the &lt;code&gt;sanity-best-practices&lt;/code&gt; skill… confirm &lt;code&gt;studio&lt;/code&gt; and &lt;code&gt;web&lt;/code&gt; are both in your working directory"). I scaffolded the Studio myself with &lt;code&gt;npm create sanity&lt;/code&gt;. While the install was running, I scaffolded &lt;code&gt;web/&lt;/code&gt; with &lt;code&gt;create-next-app&lt;/code&gt;. I then ran &lt;code&gt;create-next-app web&lt;/code&gt; myself in the terminal.&lt;/p&gt;

&lt;p&gt;To prove the connection worked, the agent built a throwaway "challenge" schema with a list page and a detail page. The first reload didn't show my published document because of the 30-second ISR window. The second one did.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Deciding what to build
&lt;/h3&gt;

&lt;p&gt;Before building anything, I asked whether this chat could even do the challenge, and what I'd just set up. Then I pitched my idea: &lt;strong&gt;a chat window that stays on top of my other Chrome tabs so my partner and I can talk during a debate round.&lt;/strong&gt; The agent pointed out that a normal web page can't float over other tabs. It suggested the &lt;strong&gt;Document Picture-in-Picture API&lt;/strong&gt;, which lets a page pop out a small, always-on-top window with any HTML in it, and which keeps the project inside "Next.js in front, Sanity behind".&lt;/p&gt;

&lt;p&gt;My one real constraint was &lt;strong&gt;keep the features to a bare minimum&lt;/strong&gt;: chat with my partner, a stopwatch, and tabs so a message can belong to a specific speech (1AC, CX, 1NC…). Everything else went into the content model instead of the UI.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. The content model
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;Why it's shaped this way&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;round&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Title, speech tabs (an array of &lt;code&gt;{_key, name}&lt;/code&gt;), workflow state and workflow history. &lt;code&gt;liveEdit: true&lt;/code&gt;, because chat, the workflow and the agent all change it live and drafts would just get in the way.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;message&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Its own document with a reference to the round and a &lt;code&gt;speechKey&lt;/code&gt; pointing at a speech's &lt;code&gt;_key&lt;/code&gt;, rather than an array inside the round. Two people typing at once never fight over one document, and live listeners only move small messages around.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;workflow&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;A singleton (&lt;code&gt;workflow-round&lt;/code&gt;) that &lt;em&gt;is&lt;/em&gt; the process: states, which state new rounds start in, transitions, and who may take each transition (&lt;code&gt;agent&lt;/code&gt;, &lt;code&gt;person&lt;/code&gt; or both). Each state also has a &lt;code&gt;chatOpen&lt;/code&gt; flag, so "can you chat in this state?" is data too.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;roundAccess&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;A private companion document per round that holds its join code (see section 6).&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  4. Workflows: the process as data
&lt;/h3&gt;

&lt;p&gt;The workflow is &lt;strong&gt;Draft → Ready → In round → Finished&lt;/strong&gt;. "Approve setup" (Draft → Ready) is person-only. Start and finish can be done by either. Reopen is person-only.&lt;/p&gt;

&lt;p&gt;The agent wrote one small engine, &lt;code&gt;shared/workflow.ts&lt;/code&gt;, that reads the workflow document and turns "take transition X as actor Y" into a patch, or into a readable refusal. Three places use it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The Studio&lt;/strong&gt;, through a custom input and a document action.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Round Control&lt;/strong&gt;, the App SDK app.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;An agent CLI&lt;/strong&gt;, &lt;code&gt;npm run agent -- draft | move | list&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The agent drafted a round, then tried to approve it, and got back &lt;strong&gt;"Approve setup" can't be done by an agent.&lt;/strong&gt; Then I approved it in the Studio. The website listens for changes to the round &lt;em&gt;and&lt;/em&gt; to the workflow document, so the chat opens without a reload. &amp;lt;!-- TODO: confirm you saw this happen live, or reword. --&amp;gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Studio customizations
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A sidebar built from the workflow.&lt;/strong&gt; One list per state, generated from the workflow document's states, so adding a state in the content adds a list in the Studio.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A "Move round" action and a state badge on every round.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;In-form workflow buttons.&lt;/strong&gt; This one came from a bug. I couldn't find the Move round button. The agent dug into the Studio v6 source and found that for &lt;code&gt;liveEdit&lt;/code&gt; documents, the footer never shows a custom action as a button; it always goes into the "⋯" menu. So I coded a custom input that puts the current state and its allowed moves at the top of the form. Moving that field right under the title made it more accessible for future Studio use.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Renaming a state without a migration.&lt;/strong&gt; The Studio showed two green/red "Live" badges: mine, and Sanity's own marker for live-edit documents. I asked for a clearer name. Only the title changed ("In round"). The state's ID stayed &lt;code&gt;live&lt;/code&gt;, so no data had to be migrated.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A join code panel&lt;/strong&gt; at the top of each round (section 6).&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  6. Join codes on a public dataset
&lt;/h3&gt;

&lt;p&gt;Before real use, I implemented a join code to add a layer of security under my app. The dataset is public, so a code stored on the round would be readable by anyone with the project ID. The agent stored each code in its own document with a dot in the ID (&lt;code&gt;roundAccess.&amp;lt;roundId&amp;gt;&lt;/code&gt;). Sanity treats these codes as private paths that anonymous queries can't see. I checked this with an anonymous query, which returned nothing.&lt;/p&gt;

&lt;p&gt;The website asks for the code, remembers it in an httpOnly cookie, and checks it on the server for every message and every new tab. Wrong guesses wait 800 ms. &lt;strong&gt;New code&lt;/strong&gt; in the Studio signs everyone out, creating a barrier to entry for strangers.&lt;/p&gt;

&lt;p&gt;The code stops strangers from posting and hides the chat on the site, but messages themselves are still readable through the public API. Making the dataset private would mean routing live updates through the Next.js server instead of straight from Sanity to the browser. That's the next thing I'd change.&lt;/p&gt;

&lt;h3&gt;
  
  
  7. App SDK: Round Control
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;app/&lt;/code&gt; is a custom React app on the App SDK that runs inside the Sanity dashboard:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a real-time list of rounds with their states;&lt;/li&gt;
&lt;li&gt;the same workflow buttons as the Studio, acting as the logged-in person;&lt;/li&gt;
&lt;li&gt;a timeline of every transition, with who made it (agent or person);&lt;/li&gt;
&lt;li&gt;every message in the round, labeled by speech tab, updating live.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It uses &lt;code&gt;useDocuments&lt;/code&gt; and &lt;code&gt;useDocumentProjection&lt;/code&gt; for lists, &lt;code&gt;useDocument&lt;/code&gt; for the round, and &lt;code&gt;useApplyDocumentActions&lt;/code&gt; with &lt;code&gt;editDocument&lt;/code&gt; on a &lt;code&gt;liveEdit&lt;/code&gt; handle for transitions. The scaffold's &lt;code&gt;node_modules&lt;/code&gt; came out corrupted: TypeScript crashed with "Unexpected end of input" inside its own compiler. A clean reinstall fixed it. The first scaffold command also failed on a flag (&lt;code&gt;--skip-mcp&lt;/code&gt;) that the current CLI doesn't have, so the agent read &lt;code&gt;--help&lt;/code&gt; and used &lt;code&gt;--no-mcp&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;To share &lt;code&gt;shared/workflow.ts&lt;/code&gt; between packages without a monorepo tool, the Studio and the app each allow Vite to read one folder up (&lt;code&gt;server.fs.allow: ['..']&lt;/code&gt;).&lt;/p&gt;

&lt;h3&gt;
  
  
  8. The chat itself
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Live updates&lt;/strong&gt; come from &lt;code&gt;client.listen()&lt;/code&gt; in the browser. Any change to the round, its messages or the workflow triggers a refetch.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Writes&lt;/strong&gt; go through server actions with a server-only token, so the token never reaches the browser.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pop-out&lt;/strong&gt;: "Pop out chat" calls &lt;code&gt;documentPictureInPicture.requestWindow()&lt;/code&gt;, copies the page's styles into the new window, and renders the chat there with a React portal. State lives in the parent component, so the stopwatch and the message you're typing survive popping in and out.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Unread dots&lt;/strong&gt; mark speech tabs with messages from your partner that you haven't seen.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Things that went wrong here:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The pop-out failed&lt;/strong&gt; in the agent's built-in browser ("no window"), because that browser won't open extra windows. However, after troubleshooting, I ran the pop-out window in Chrome tabs and realized that the app's functionality works best in actual practice. The agent added a visible error when I ran the app within its own browser.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;React's lint rules&lt;/strong&gt; rejected setting state inside effects, three times. The fixes were better code: &lt;code&gt;useSyncExternalStore&lt;/code&gt; for the saved name and feature detection, and computing the active tab instead of syncing it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A GROQ scope bug.&lt;/strong&gt; &lt;code&gt;^.^.workflowState&lt;/code&gt; returned &lt;code&gt;null&lt;/code&gt;. The agent tested variants against the live API and found that array filters inside a subquery don't add a scope level, so the correct form is &lt;code&gt;^.workflowState&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  9. Deploying
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Studio:&lt;/strong&gt; &lt;code&gt;sanity deploy&lt;/code&gt; → &lt;a href="https://debate-round-chat.sanity.studio" rel="noopener noreferrer"&gt;https://debate-round-chat.sanity.studio&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Round Control:&lt;/strong&gt; &lt;code&gt;sanity deploy --create&lt;/code&gt; → in the Sanity dashboard.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Website: Vercel.&lt;/strong&gt; The first deploy built fine, but every page returned &lt;strong&gt;404&lt;/strong&gt;. &lt;code&gt;vercel link&lt;/code&gt; had created the project with the "Other" framework preset. A one-line &lt;code&gt;vercel.json&lt;/code&gt; (&lt;code&gt;"framework": "nextjs"&lt;/code&gt;) fixed it.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  10. Security mistakes, fixed
&lt;/h3&gt;

&lt;p&gt;While checking on my Vercel login, the agent read my terminal, and my Sanity write token from earlier was still on screen. It told me the token was now in the transcript. So, I created a new token, added it to Vercel as a Secret, and deleted the old one. The agent then checked with a dry-run write that the new token works and the old one is rejected (401). The old token was then rendered dead.&lt;/p&gt;

&lt;p&gt;The agent also left a few things for me to complete under my own discretion:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Deleting my old test document.&lt;/li&gt;
&lt;li&gt;Entering tokens into Vercel.&lt;/li&gt;
&lt;li&gt;Logging in for me.&lt;/li&gt;
&lt;li&gt;Clicking "Approve" while it was testing in the Studio. The workflow says a person must approve, and the click would have been logged under my name.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It slowed things down a little, but it's the right call for a tool that can act on your accounts.&lt;/p&gt;

&lt;h3&gt;
  
  
  11. A bug the screenshots caught
&lt;/h3&gt;

&lt;p&gt;Taking the screenshots for this post turned up an empty grey bubble in the chat. It was a real &lt;code&gt;message&lt;/code&gt; document with no author, text or time. It had been created from the Studio's "New document" button, and because messages save instantly, an empty one was saved on the spot. The fix had three parts:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The website skips messages without text or a send time.&lt;/li&gt;
&lt;li&gt;Round Control does the same.&lt;/li&gt;
&lt;li&gt;The Studio no longer offers "create message", since messages should only come from the chat.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Prompts that worked, and ones that didn't
&lt;/h3&gt;

&lt;p&gt;Quoted as I typed them, typos included.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Worked: asking before building.&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;without doing what the prompt says, just answer: do you think in this chat i can have u do this prompt from the dev-challenge sanity challenge with the files we have set up here?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I got a plain-English tour of what I'd set up (Studio vs. website vs. TypeGen), a "yes, and here's how" for the challenge, and the Picture-in-Picture idea, all before any code was written.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Worked: tight scope, spelled out.&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;use document picture in picture, and it can have that extra functionality like tabs but keep the number of functions to a bare minimum; just the ability to chat with the partner during the round, maybe a stop watch, and the ability to create subtabs so a message can be tied to a specific speech. […] now start building studio custommizations, an app sdk app and a workflow schema.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This one prompt produced most of the project. Naming the three features kept the UI small, and naming the three Sanity areas pushed the depth into the content model.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Worked: short follow-ups with one clear goal.&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;rename the round state to something clearer&lt;/p&gt;

&lt;p&gt;yes commit it, then add the join code&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Didn't work at first: describing the UI in words.&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;where is the move round button within the studio, there is none&lt;/p&gt;

&lt;p&gt;wheres the pop out chat&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The first needed the agent to open the Studio itself and dig into Studio v6's source to find where the button went. The second was a mix-up: I was looking in the Studio, and the chat lives on the website. Sending a &lt;strong&gt;screenshot&lt;/strong&gt; settled it in one reply. Next time I'd send the screenshot first.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Didn't work: running my own commands alongside the agent's.&lt;/strong&gt;&lt;br&gt;
Running &lt;code&gt;create-next-app web&lt;/code&gt; myself after the agent already had caused a conflict, and so did an old Studio server still holding port 3333. Clicking "Run" on a &lt;code&gt;git push&lt;/code&gt; command four times opened four terminals, all waiting for a password. I eventually switched to an SSH key.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sanity Project Details
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Project ID:&lt;/strong&gt; &lt;code&gt;jelfmjhs&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dataset:&lt;/strong&gt; &lt;code&gt;production&lt;/code&gt; (public)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Studio:&lt;/strong&gt; &lt;a href="https://debate-round-chat.sanity.studio" rel="noopener noreferrer"&gt;https://debate-round-chat.sanity.studio&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can read the content model from the public API:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://jelfmjhs.api.sanity.io/v2026-09-22/data/query/production?query=*%5B_id%20%3D%3D%20%22workflow-round%22%5D%5B0%5D%7Btitle%2C%20initialState%2C%20states%5B%5D%7Bid%2C%20title%2C%20chatOpen%7D%2C%20transitions%5B%5D%7Bid%2C%20title%2C%20from%2C%20to%2C%20actors%7D%7D" rel="noopener noreferrer"&gt;The round workflow document&lt;/a&gt;: its states, transitions and who is allowed to take each one.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://jelfmjhs.api.sanity.io/v2026-09-22/data/query/production?query=*%5B_type%20%3D%3D%20%22round%22%5D%7Btitle%2C%20workflowState%2C%20speeches%5B%5D%7Bname%7D%2C%20workflowHistory%5B%5D%7Btransition%2C%20actorType%2C%20at%7D%7D" rel="noopener noreferrer"&gt;Rounds with their speech tabs and workflow history&lt;/a&gt;, including which transitions an agent made and which a person made.&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;Role&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;round&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Live-edit. Title, speech tabs, &lt;code&gt;workflowState&lt;/code&gt; and &lt;code&gt;workflowHistory&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;message&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Live-edit. Reference to its round, &lt;code&gt;speechKey&lt;/code&gt;, author, body, &lt;code&gt;sentAt&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;workflow&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Singleton &lt;code&gt;workflow-round&lt;/code&gt;: states (with &lt;code&gt;chatOpen&lt;/code&gt;), &lt;code&gt;initialState&lt;/code&gt;, and transitions with allowed actors&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;roundAccess&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Private &lt;code&gt;roundAccess.&amp;lt;roundId&amp;gt;&lt;/code&gt; document holding the join code. Not visible to anonymous readers&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

</description>
      <category>devchallenge</category>
      <category>sanitychallenge</category>
      <category>sanity</category>
      <category>ai</category>
    </item>
  </channel>
</rss>
