refactor: improve plan mode UX with composer-integrated toggle and guidance text

- Move plan mode toggle from pills row to composer send button area
- Plan toggle sits next to the send button as a mode switch icon
- Send button turns emerald when plan mode is active
- Add mode indicator strip below composer when plan mode is on
- Replace 'Implement this plan' auto-send button with instructional
  guidance text that tells the user to send a follow-up message
- Keep Dismiss button to clear the plan review banner

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
David Kaya
2026-03-26 23:46:04 +01:00
co-authored by Copilot
parent 231be36e6c
commit b670680a7d
2 changed files with 63 additions and 73 deletions
+55 -47
View File
@@ -130,12 +130,6 @@ export function ChatPane({
void onSend(content);
}
function handleImplementPlan() {
if (!pendingPlanReview) return;
onDismissPlanReview?.();
void onSend('Implement the plan.');
}
function handleDismissPlan() {
onDismissPlanReview?.();
}
@@ -399,7 +393,6 @@ export function ChatPane({
<div className="mb-3">
<PlanReviewBanner
onDismiss={handleDismissPlan}
onImplement={handleImplementPlan}
planReview={pendingPlanReview}
/>
</div>
@@ -426,22 +419,6 @@ export function ChatPane({
onUpdate={onUpdateSessionApprovalSettings}
/>
)}
{onSetInteractionMode && (
<button
aria-pressed={isPlanMode}
className={`inline-flex items-center gap-1 rounded-lg border px-2.5 py-1 text-[11px] font-medium transition ${
isPlanMode
? 'border-emerald-500/40 bg-emerald-500/15 text-emerald-300 hover:bg-emerald-500/25'
: 'border-zinc-700 text-zinc-400 hover:border-zinc-600 hover:text-zinc-300'
}`}
disabled={isComposerDisabled}
onClick={() => onSetInteractionMode(isPlanMode ? 'interactive' : 'plan')}
type="button"
>
<ClipboardList className="size-3" />
Plan
</button>
)}
{primaryAgent && (
<div className="ml-auto flex items-center gap-2">
<InlineModelPill
@@ -521,32 +498,63 @@ export function ChatPane({
: 'Message...'
}
>
<button
className={`absolute bottom-2 right-2 flex size-8 items-center justify-center rounded-lg transition ${
isSessionBusy
? 'bg-red-600/80 text-white hover:bg-red-500'
: canSubmitInput
? 'bg-indigo-600 text-white hover:bg-indigo-500'
: 'bg-zinc-800 text-zinc-600'
}`}
disabled={!canSubmitInput && !isSessionBusy}
onClick={() => {
if (isSessionBusy) {
onCancelTurn?.();
} else {
composerRef.current?.submit();
}
}}
type="button"
aria-label={isSessionBusy ? 'Stop generating' : 'Send message'}
>
{isSessionBusy ? (
<Square className="size-3.5" fill="currentColor" />
) : (
<ArrowUp className="size-4" />
<div className="absolute bottom-2 right-2 flex items-center gap-1">
{/* Plan mode toggle */}
{onSetInteractionMode && !isSessionBusy && (
<button
aria-label={isPlanMode ? 'Switch to interactive mode' : 'Switch to plan mode'}
aria-pressed={isPlanMode}
className={`flex size-8 items-center justify-center rounded-lg transition ${
isPlanMode
? 'bg-emerald-600/20 text-emerald-400 hover:bg-emerald-600/30'
: 'text-zinc-500 hover:bg-zinc-800 hover:text-zinc-300'
}`}
disabled={isComposerDisabled}
onClick={() => onSetInteractionMode(isPlanMode ? 'interactive' : 'plan')}
type="button"
>
<ClipboardList className="size-3.5" />
</button>
)}
</button>
{/* Send / Stop button */}
<button
className={`flex size-8 items-center justify-center rounded-lg transition ${
isSessionBusy
? 'bg-red-600/80 text-white hover:bg-red-500'
: canSubmitInput
? isPlanMode
? 'bg-emerald-600 text-white hover:bg-emerald-500'
: 'bg-indigo-600 text-white hover:bg-indigo-500'
: 'bg-zinc-800 text-zinc-600'
}`}
disabled={!canSubmitInput && !isSessionBusy}
onClick={() => {
if (isSessionBusy) {
onCancelTurn?.();
} else {
composerRef.current?.submit();
}
}}
type="button"
aria-label={isSessionBusy ? 'Stop generating' : isPlanMode ? 'Send as plan request' : 'Send message'}
>
{isSessionBusy ? (
<Square className="size-3.5" fill="currentColor" />
) : (
<ArrowUp className="size-4" />
)}
</button>
</div>
</MarkdownComposer>
{isPlanMode && !isSessionBusy && (
<div className="flex items-center gap-1.5 px-3 pb-1.5 pt-0.5">
<div className="size-1.5 rounded-full bg-emerald-500" />
<span className="text-[10px] font-medium text-emerald-400/80">
Plan mode the agent will propose a plan instead of implementing
</span>
</div>
)}
</div>
</div>
</div>
@@ -1,22 +1,16 @@
import { useCallback } from 'react';
import { ClipboardList, Play, X } from 'lucide-react';
import { ClipboardList, X } from 'lucide-react';
import { MarkdownContent } from '@renderer/components/MarkdownContent';
import type { PendingPlanReviewRecord } from '@shared/domain/planReview';
export function PlanReviewBanner({
planReview,
onImplement,
onDismiss,
}: {
planReview: PendingPlanReviewRecord;
onImplement: (planReview: PendingPlanReviewRecord) => void;
onDismiss: (planReview: PendingPlanReviewRecord) => void;
}) {
const handleImplement = useCallback(() => {
onImplement(planReview);
}, [planReview, onImplement]);
const handleDismiss = useCallback(() => {
onDismiss(planReview);
}, [planReview, onDismiss]);
@@ -63,26 +57,14 @@ export function PlanReviewBanner({
<MarkdownContent content={planReview.planContent} />
</div>
)}
</div>
</div>
{/* Actions */}
<div className="mt-3 flex items-center gap-2">
<button
className="inline-flex items-center gap-1.5 rounded-lg bg-emerald-600 px-3.5 py-1.5 text-[12px] font-medium text-white transition hover:bg-emerald-500"
onClick={handleImplement}
type="button"
>
<Play className="size-3" />
Implement this plan
</button>
<button
className="rounded-lg border border-zinc-600 px-3.5 py-1.5 text-[12px] font-medium text-zinc-300 transition hover:border-zinc-500 hover:bg-zinc-800 hover:text-white"
onClick={handleDismiss}
type="button"
>
Dismiss
</button>
{/* Guidance */}
<p className="mt-3 text-[12px] leading-relaxed text-zinc-400">
Send a follow-up message to proceed e.g.{' '}
<span className="text-zinc-300">&quot;implement the plan&quot;</span>,{' '}
<span className="text-zinc-300">&quot;adjust step 3&quot;</span>, or ask for a different approach.
</p>
</div>
</div>
</div>
);