Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 4 additions & 11 deletions src/components/Chat/ChatPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import { adoptDraftedProjectName } from '@/project/drafted-name';
import { recordBuildEvent, useBuildLogStore } from '@/report/build-log';
import { resetSubmitTracking } from '@/report/friction';
import { BuildReportCard } from './BuildReportCard';
import { startBuildFromPlan } from './build-from-plan';
import { MessageList } from './MessageList';
import { MessageInput } from './MessageInput';
import { CHUNK_MARKER, FILE_REQUEST_MARKER } from './display';
Expand Down Expand Up @@ -1044,17 +1045,9 @@ export function ChatPanel() {
handleSend(queuedMessage, attachments.length > 0 ? attachments : undefined);
}, [queuedMessage, isGenerating, setMode, handleSend]);

const handleBuildPlan = useCallback(() => {
setMode('build');
// On an existing project the plan is a delta — build only it. From
// scratch, the plan is the whole first build.
const existing = useProjectStore.getState().getFileCount() > 0;
handleSend(
existing
? 'Make the changes agreed in the plan above — only those changes, keeping everything else in the app exactly as it is. Generate the complete added or edited files with filename annotations. End by naming, in one line, anything you deliberately left for a later pass.'
: 'Build the first version of the app described in the plan above — the plan\'s First-build features, not its Later ones. Generate complete, working files with filename annotations, following the plan\'s look & feel and data decisions. End by naming, in one line, what you left for the next pass.',
);
}, [setMode, handleSend]);
// The action under the conversation and the readiness card's "Ready to
// build" are the same press — see build-from-plan.ts
const handleBuildPlan = useCallback(() => startBuildFromPlan(), []);

const handleStop = useCallback(() => {
const controller = useChatStore.getState().abortController;
Expand Down
40 changes: 32 additions & 8 deletions src/components/Chat/MessageList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { useProjectStore } from '@/store/project-store';
import { usePanelStore } from '@/store/panel-store';
import { artifactDisplay } from '@/project/display-name';
import { useUIStore } from '@/store/ui-store';
import { isReadyToBuildOption, startBuildFromPlan } from './build-from-plan';
import { renderableContent } from './display';
import { CodeBlock } from './CodeBlock';
import { ConnectionSuggestion } from './ConnectionSuggestion';
Expand Down Expand Up @@ -418,25 +419,36 @@ export function MessageList({ messages, onBuildPlan, isGenerating }: MessageList
}
}, [lastMessage]);

// From scratch, the action arrives with the drafted plan document — but it
// must not LEAVE with it. Refinements ("also add a lending toggle") come
// back as short conversational replies, and pinning the action to a
// document-shaped last message meant the invitation to build outlived the
// button that does it: the reply said "press Build this plan" and there was
// nothing to press. Once a plan has been drafted, the plan stands until it
// is built, and every settled reply carries the action. Refinements ride
// along with it — the send carries the whole conversation.
const planDrafted = useMemo(
() => messages.some(m => m.role === 'assistant' && m.isPlan && !m.isStreaming && isPlanDocument(m.content)),
[messages],
);

if (messages.length === 0) {
return null;
}

// The Build/Approve action belongs to a reply with something to approve.
// From scratch that means the drafted plan document — a conversational
// reply (exploring, questions) has nothing to build yet. On an existing
// project even a two-sentence change IS the plan, so any settled reply
// qualifies — except one that just asked questions, which wants answers,
// not approval.
// A reply that just asked questions is the exception either way: it wants
// answers, not approval — and when the question IS the readiness check, its
// "Ready to build" card is the press.
const showBuildAction =
!isGenerating &&
!!onBuildPlan &&
lastMessage?.role === 'assistant' &&
lastMessage.isPlan &&
!lastMessage.isStreaming &&
(hasProject
? extractPlanQuestions(lastMessage.content).length === 0
: isPlanDocument(lastMessage.content));
extractPlanQuestions(lastMessage.content).length === 0 &&
// On an existing project even a two-sentence change IS the plan
(hasProject || planDrafted);

return (
<div className="flex-1 relative min-h-0">
Expand Down Expand Up @@ -844,6 +856,10 @@ export function stripPlanQuestions(content: string): string {
* stage and send together once all are answered (or early via Send answers).
* Only the newest plan's cards are live; older plans keep a quiet transcript
* of what was asked.
*
* The readiness check ("Anything else to change, or ready to build?") comes
* through here like any other question — but its yes is not an answer to
* relay. It starts the build.
*/
function PlanQuestionCards({ message }: { message: DisplayMessage }) {
const questions = useMemo(() => extractPlanQuestions(message.content), [message.content]);
Expand All @@ -865,6 +881,14 @@ function PlanQuestionCards({ message }: { message: DisplayMessage }) {
};

const answer = (i: number, value: string) => {
// "Ready to build" IS the press — sending the words instead would spend a
// whole reply re-offering a button. Only when it stands alone: a yes
// tapped beside another open question would discard that answer.
if (questions.length === 1 && isReadyToBuildOption(value)) {
setSent(true);
startBuildFromPlan();
return;
}
const next = { ...answers, [i]: value };
setAnswers(next);
// One question sends on tap; several send once the last one is answered
Expand Down
55 changes: 55 additions & 0 deletions src/components/Chat/build-from-plan.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { useChatStore } from '@/store/chat-store';
import { useProjectStore } from '@/store/project-store';

/**
* What building the plan actually means — one definition, two doors: the
* action under the conversation, and the "Ready to build" answer on a
* readiness card.
*
* The send carries the WHOLE conversation, so refinements made after the plan
* document landed ("also add a lending toggle") ride along with it: "the plan
* above" is the plan as it now stands, not the first draft of it. That is why
* the action can outlive the message it first appeared under.
*/
export function buildFromPlanPrompt(existing: boolean): string {
return existing
? 'Make the changes agreed in the plan above — only those changes, keeping everything else in the app exactly as it is. Generate the complete added or edited files with filename annotations. End by naming, in one line, anything you deliberately left for a later pass.'
: 'Build the first version of the app described in the plan above — the plan\'s First-build features, not its Later ones. Generate complete, working files with filename annotations, following the plan\'s look & feel and data decisions. End by naming, in one line, what you left for the next pass.';
}

/**
* Start the build from the plan as it stands. Mode flips first — handleSend
* reads it fresh from the store — and the message rides the same queue the
* plan answers already use, so there is one send path, not two.
*/
export function startBuildFromPlan(): void {
const existing = useProjectStore.getState().getFileCount() > 0;
useChatStore.getState().setMode('build');
useChatStore.getState().queueMessage(buildFromPlanPrompt(existing));
}

/**
* Answer options that mean "go" — the person approving the plan rather than
* asking for another change. A tap on one of these IS the press: relaying the
* words to the model instead would spend a whole reply re-offering a button.
*
* Deliberately tight. The prompt asks for these exact words, and anything not
* recognised here simply sends as an ordinary answer — the action under the
* conversation is still there — so a miss costs a round trip, never a build
* nobody asked for.
*/
const READY_ANSWERS: readonly RegExp[] = [
/^(yes[,—-]?\s*)?(i'?m\s+)?ready(\s+to\s+build)?$/,
/^(yes[,—-]?\s*)?(let'?s\s+)?build\s+(it|this|this\s+plan)$/,
/^(yes[,—-]?\s*)?approve\s+(this|the)\s+plan$/,
];

export function isReadyToBuildOption(option: string): boolean {
const normalized = option
.toLowerCase()
.replace(/[*_`]/g, '')
.replace(/\s+/g, ' ')
.trim()
.replace(/[.!]+$/, '');
return READY_ANSWERS.some(re => re.test(normalized));
}
6 changes: 4 additions & 2 deletions src/knowledge/context-builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -432,13 +432,15 @@ const PLAN_INSTRUCTIONS = [
'',
'After the draft, feedback comes back conversationally. Do NOT re-output the entire plan — briefly say what changed (one or two lines), or answer what they asked. If their feedback genuinely reopens a build-shaping decision, ask it the one-tap way (the "## Question for you" format) in that short reply — never by appending questions to the plan.',
'',
'Close every such refinement reply with the readiness check, in that same one-tap format: ONE question — "Anything else to change, or ready to build?" — with a single answer option worded EXACTLY `Ready to build`. Tapping it starts the build; "Something else…" sits beside it for another change. Ask it after every refinement, so the next step is always one tap away. Never tell them to press a button instead: say what the plan now does and ask.',
'',
'Keep it readable for a non-technical neighborhood builder. Short sections beat exhaustive ones — but do not rush the visioning: two or three good questions before drafting is time well spent.',
'',
'Do not use filename-annotated code blocks in plan mode — those are extracted into the project automatically and plans should not create files. Small illustrative snippets without filename annotations are fine if truly needed.',
'',
'If the person brought a build plan from RTP Studio, treat it as the starting draft: honor its intent and lineage, adapt it to what they say, and call out anything you changed.',
'',
'End every plan by inviting the person to refine it or press **Build this plan** when it feels right.',
'End the DRAFTED PLAN itself by inviting the person to refine it or press **Build this plan** when it feels right — that action sits under the plan the moment it lands, and stays there through every refinement. The readiness check above belongs to the short replies that follow, not to the plan document.',
].join('\n');

/**
Expand Down Expand Up @@ -477,7 +479,7 @@ const PLAN_EXISTING_INSTRUCTIONS = [
'',
'Keep it readable for a non-technical neighborhood builder — short, concrete, in their words.',
'',
'When you\'ve sketched a change worth making, end by inviting the person to refine it or press **Approve this plan** — approving builds exactly what the plan says, nothing more.',
'When you\'ve sketched a change worth making, end by inviting the person to refine it or press **Approve this plan** — approving builds exactly what the plan says, nothing more. Once they have refined it, close each follow-up reply with the readiness check in the one-tap format instead: ONE question — "Anything else to change, or ready to build?" — with a single option worded EXACTLY `Approve this plan`. Tapping it starts the build.',
].join('\n');

export interface ContextOptions {
Expand Down