Skip to content

Commit 107e12e

Browse files
authored
feat(studio): switch a plan's scope choices and a reading's chips (#73)
* feat(studio): switch how a plan figure is counted from the plan card Decision rows that carry a scope choice list its options: the active one marked, available ones selectable, partial ones with their years and unavailable ones disabled with their reason. A pick sends a switch with the plan revision through the interrupt stream, keeps the card until the re-offered plan arrives, and shows what moved. A stale revision or an unavailable option leaves the card with a notice. The wire mirror gains the plan revision, requirements, operations and asks it lagged on. * fix(studio): keep the switch control usable after a refusal A stale-revision refusal stops further switches on that card and says why; options in flight keep focus; the moved note and notice stay mounted so screen readers announce them; a plain detail body reaches the notice. * fix(studio): replace a stale plan card with the one the harness holds A switch or approve refused as plan_revision_stale now takes the current_plan the harness returns and shows it with the refusal's detail, so switching and approving continue on the plan actually pending. Approve sends the plan revision the user saw. * feat(studio): render reading chips after an answer and switch on a click Studio shows the harness's apply_choice chips beside the follow-up prompts on the latest answer. A click sends a turn carrying the chosen option and no message, shows the chip label as the user's side, and streams the answer like any turn; a refused chip shows the reason and is removed. * docs(studio): tidy the plan switch and chip comments * feat(studio): send the turn and reading a chip was offered for A chip carries the turn whose answer offered it and the spec_id of the reading it switches, and the click sends both in apply_choice, so the harness can refuse a chip whose answer or reading is no longer current. * fix(studio): keep a stale plan unapprovable and own turn starts and ends in one place A switch refused as stale keeps the card's revision, so an approve is refused, and only the harness's switch refusals count as refused switches; any other failure ends the turn. stream() guards every refusal against a stopped or superseded turn, send and applyChoice open a turn through one startTurn, a chip clicked mid-request queues, a forked conversation shows no chips, and the card shows its notice. * fix(studio): count a chip click as a prompt A chip passes the guest prompt limit and is tracked as studio.prompt with intent apply_choice, through the one wrapper typed prompts use.
1 parent 6fe733d commit 107e12e

20 files changed

Lines changed: 1818 additions & 175 deletions

‎src/components/organisms/Studio/Studio.tsx‎

Lines changed: 28 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ import type { ShareKind } from '@/lib/chat/shareStore';
3939
import { track } from '@/lib/events/client';
4040
import { stashGuestSession, takeGuestSession } from '@/lib/studio/guestStash';
4141
import { logRefusedDescriptions } from '@/lib/studio/infoPanel';
42-
import { GUEST_PROMPTS } from '@/lib/studio/promptAllowance';
42+
import { GUEST_PROMPTS, promptEvent, type PromptIntent } from '@/lib/studio/promptAllowance';
4343
import { takeSeeds } from '@/lib/studio/seedStore';
4444
import { deriveTheme, themeVars } from '@/lib/studio/theme';
4545
import { isPrompt } from '@/lib/studio/transcript';
@@ -182,25 +182,39 @@ const Studio = ({
182182

183183
// Queued prompts hold their place in the limit until sent or removed.
184184
const walled = guest && guestUsed + conversation.queued.length >= GUEST_PROMPTS;
185-
// Every prompt (landing, follow-up, card menu) goes through here: past
186-
// the limit a guest's is dropped, the rest are counted as usage (turn,
187-
// intent and length — never the text).
188-
const send: typeof conversation.send = (text, options, seeds) => {
185+
// Every prompt (landing, follow-up, card menu, a clicked chip) goes
186+
// through here: past the limit a guest's is dropped, the rest are counted
187+
// as usage (turn, intent and a typed prompt's length, never the text). A
188+
// prompt the chat queues is counted here once; the queue's drain sends it
189+
// inside the chat, past this gate.
190+
const prompt = (
191+
intent: PromptIntent | undefined,
192+
length: number | undefined,
193+
run: () => void,
194+
) => {
189195
// Read the count afresh: the `?q=` handover fires from the first
190196
// render's closure, before the stored count has synced in.
191-
if (guest && readGuestUsed() + conversation.queued.length >= GUEST_PROMPTS) {
197+
const event = promptEvent({
198+
guest,
199+
used: readGuestUsed(),
200+
queued: conversation.queued.length,
201+
sent: sentCount,
202+
intent,
203+
length,
204+
});
205+
206+
if (!event) {
192207
return;
193208
}
194209

195-
track('studio.prompt', {
196-
turn: sentCount + conversation.queued.length + 1,
197-
intent: options?.intent ?? (sentCount === 0 ? 'new' : 'follow_up'),
198-
length: text.trim().length,
199-
guest,
200-
});
201-
conversation.send(text, options, seeds);
210+
track('studio.prompt', event);
211+
run();
202212
};
203-
const chat = { ...conversation, send };
213+
const send: typeof conversation.send = (text, options, seeds) =>
214+
prompt(options?.intent, text.trim().length, () => conversation.send(text, options, seeds));
215+
const applyChoice: typeof conversation.applyChoice = (chip) =>
216+
prompt('apply_choice', undefined, () => conversation.applyChoice(chip));
217+
const chat = { ...conversation, send, applyChoice };
204218

205219
// Usage: each card the harness puts on the canvas, once (a reopened or
206220
// shared conversation's cards were counted when they were made).

0 commit comments

Comments
 (0)