Skip to content

Commit 16cfcf7

Browse files
patnikoCopilot
andcommitted
redesign: move all prompt bar buttons inside textarea
All buttons now live inside the textarea container: - πŸ“Ž Attach: bottom-left inside textarea - β‹― Session menu: bottom-right (New/Load/Split Session) - 🎀 Mic: bottom-right - ✈ Send / ⏹ Stop: bottom-right, rounded square No more external buttons that need height-matching. The textarea is the only element in the bar, with buttons absolutely positioned inside it. Scales naturally with multiline content. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent f2358f8 commit 16cfcf7

1 file changed

Lines changed: 132 additions & 128 deletions

File tree

β€Žsrc/renderer/components/PromptBar.tsxβ€Ž

Lines changed: 132 additions & 128 deletions
Original file line numberDiff line numberDiff line change
@@ -320,15 +320,56 @@ export default function PromptBar({ panelId, onSend, onGeneratingChange, cwd, on
320320
)}
321321
</AnimatePresence>
322322

323-
<div className="flex items-center gap-3">
324-
{/* Attach button */}
325-
<button
326-
onClick={() => fileInputRef.current?.click()}
327-
className="px-2 py-2.5 text-[var(--text-secondary)] hover:text-[var(--accent-purple)] transition-colors cursor-pointer text-lg shrink-0"
328-
title="Attach image"
329-
>
330-
πŸ“Ž
331-
</button>
323+
<div className="relative">
324+
{/* Waveform overlay when listening */}
325+
<AnimatePresence>
326+
{voice.isListening && (
327+
<motion.div
328+
className="absolute inset-0 z-10 flex flex-col items-center justify-center rounded-xl border border-[var(--accent-purple)] bg-[var(--bg-secondary)]"
329+
initial={{ opacity: 0 }}
330+
animate={{ opacity: 1 }}
331+
exit={{ opacity: 0 }}
332+
transition={{ duration: 0.2 }}
333+
>
334+
{/* Waveform bars */}
335+
<div className="flex items-center justify-center gap-[2px] h-8 px-4 w-full">
336+
{voice.audioLevels.map((level, i) => (
337+
<div
338+
key={i}
339+
className="w-[3px] rounded-full bg-[var(--accent-purple)]"
340+
style={{
341+
height: `${Math.max(3, level * 28)}px`,
342+
opacity: 0.4 + level * 0.6,
343+
transition: 'height 80ms ease-out, opacity 80ms ease-out',
344+
}}
345+
/>
346+
))}
347+
</div>
348+
{/* Streaming transcript */}
349+
{(voice.finalTranscript || voice.interimTranscript) && (
350+
<p className="text-xs text-[var(--text-secondary)] mt-1 px-4 truncate max-w-full">
351+
<span className="text-[var(--text-primary)]">{voice.finalTranscript}</span>
352+
{voice.interimTranscript && (
353+
<span className="text-[var(--text-secondary)] italic">{voice.interimTranscript}</span>
354+
)}
355+
</p>
356+
)}
357+
</motion.div>
358+
)}
359+
</AnimatePresence>
360+
361+
<textarea
362+
ref={textareaRef}
363+
value={prompt}
364+
onChange={(e) => setPrompt(e.target.value)}
365+
onKeyDown={handleKeyDown}
366+
onPaste={handlePaste}
367+
placeholder={attachments.length > 0 ? 'Add a message or press Enter to send…' : isGenerating ? 'Type to steer or queue next message…' : 'Enter your prompt…'}
368+
rows={1}
369+
className="w-full bg-transparent outline-none text-[var(--text-primary)] placeholder-[var(--text-secondary)] resize-none rounded-xl border border-[var(--border-color)] pl-11 pr-20 py-3 focus:border-[var(--accent-purple)] focus:shadow-[0_0_8px_var(--accent-purple)] transition-shadow"
370+
style={{ lineHeight: `${LINE_HEIGHT}px` }}
371+
/>
372+
332373
<input
333374
ref={fileInputRef}
334375
type="file"
@@ -341,74 +382,74 @@ export default function PromptBar({ panelId, onSend, onGeneratingChange, cwd, on
341382
}}
342383
/>
343384

344-
<div className="relative flex-1">
345-
{/* Waveform overlay when listening */}
346-
<AnimatePresence>
347-
{voice.isListening && (
348-
<motion.div
349-
className="absolute inset-0 z-10 flex flex-col items-center justify-center rounded-lg border border-[var(--accent-purple)] bg-[var(--bg-secondary)]"
350-
initial={{ opacity: 0 }}
351-
animate={{ opacity: 1 }}
352-
exit={{ opacity: 0 }}
353-
transition={{ duration: 0.2 }}
354-
>
355-
{/* Waveform bars */}
356-
<div className="flex items-center justify-center gap-[2px] h-8 px-4 w-full">
357-
{voice.audioLevels.map((level, i) => (
358-
<div
359-
key={i}
360-
className="w-[3px] rounded-full bg-[var(--accent-purple)]"
361-
style={{
362-
height: `${Math.max(3, level * 28)}px`,
363-
opacity: 0.4 + level * 0.6,
364-
transition: 'height 80ms ease-out, opacity 80ms ease-out',
365-
}}
366-
/>
367-
))}
385+
{/* Attach button β€” bottom left inside textarea */}
386+
<button
387+
onClick={() => fileInputRef.current?.click()}
388+
className="absolute left-3 bottom-2.5 z-20 flex items-center justify-center w-7 h-7 rounded-full text-[var(--text-secondary)] hover:text-[var(--accent-purple)] transition-colors cursor-pointer"
389+
title="Attach image"
390+
>
391+
πŸ“Ž
392+
</button>
393+
394+
{/* Right-side buttons β€” bottom right inside textarea */}
395+
<div className="absolute right-2.5 bottom-2 z-20 flex items-center gap-1">
396+
{/* Session menu (β‹―) */}
397+
<div className="relative">
398+
<button
399+
onClick={() => setSendMenuOpen(!sendMenuOpen)}
400+
className="flex items-center justify-center w-7 h-7 rounded-full text-[var(--text-secondary)] hover:text-[var(--text-primary)] hover:bg-[var(--bg-secondary)] transition-colors cursor-pointer opacity-60 hover:opacity-100"
401+
title="Session actions"
402+
>
403+
<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor"><circle cx="5" cy="12" r="2"/><circle cx="12" cy="12" r="2"/><circle cx="19" cy="12" r="2"/></svg>
404+
</button>
405+
{sendMenuOpen && (
406+
<>
407+
<div className="fixed inset-0 z-40" onClick={() => setSendMenuOpen(false)} />
408+
<div className="absolute bottom-full right-0 mb-2 z-50 w-44 rounded-lg border border-[var(--border-color)] bg-[var(--bg-secondary)] shadow-xl overflow-hidden">
409+
<button
410+
onClick={() => { setSendMenuOpen(false); onNewSession?.(); }}
411+
className="w-full text-left px-3 py-2 text-xs text-[var(--text-primary)] hover:bg-[var(--bg-primary)] transition-colors cursor-pointer flex items-center gap-2"
412+
>
413+
<span className="text-[var(--text-secondary)]">+</span> New Session
414+
</button>
415+
<button
416+
onClick={() => { setSendMenuOpen(false); onLoadSession?.(); }}
417+
className="w-full text-left px-3 py-2 text-xs text-[var(--text-primary)] hover:bg-[var(--bg-primary)] transition-colors cursor-pointer flex items-center gap-2"
418+
>
419+
<svg width="12" height="12" viewBox="0 0 16 16" fill="currentColor" className="text-[var(--text-secondary)]"><path d="M1.5 1.5v13h13v-13h-13zM0 1a1 1 0 011-1h14a1 1 0 011 1v14a1 1 0 01-1 1H1a1 1 0 01-1-1V1zm4.5 2.5h7v1h-7v-1zm0 3h7v1h-7v-1zm0 3h4v1h-4v-1z"/></svg>
420+
Load Session
421+
</button>
422+
<button
423+
onClick={() => { setSendMenuOpen(false); onSplitSession?.(); }}
424+
className="w-full text-left px-3 py-2 text-xs text-[var(--text-primary)] hover:bg-[var(--bg-primary)] transition-colors cursor-pointer flex items-center gap-2"
425+
>
426+
<svg width="12" height="12" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" className="text-[var(--text-secondary)]">
427+
<rect x="1" y="2" width="14" height="12" rx="1" />
428+
<line x1="8" y1="2" x2="8" y2="14" />
429+
</svg>
430+
Split Session
431+
</button>
368432
</div>
369-
{/* Streaming transcript */}
370-
{(voice.finalTranscript || voice.interimTranscript) && (
371-
<p className="text-xs text-[var(--text-secondary)] mt-1 px-4 truncate max-w-full">
372-
<span className="text-[var(--text-primary)]">{voice.finalTranscript}</span>
373-
{voice.interimTranscript && (
374-
<span className="text-[var(--text-secondary)] italic">{voice.interimTranscript}</span>
375-
)}
376-
</p>
377-
)}
378-
</motion.div>
433+
</>
379434
)}
380-
</AnimatePresence>
381-
382-
<textarea
383-
ref={textareaRef}
384-
value={prompt}
385-
onChange={(e) => setPrompt(e.target.value)}
386-
onKeyDown={handleKeyDown}
387-
onPaste={handlePaste}
388-
placeholder={attachments.length > 0 ? 'Add a message or press Enter to send…' : isGenerating ? 'Type to steer or queue next message…' : 'Enter your prompt…'}
389-
rows={1}
390-
className="w-full bg-transparent outline-none text-[var(--text-primary)] placeholder-[var(--text-secondary)] resize-none rounded-lg border border-[var(--border-color)] px-4 py-3 pr-10 focus:border-[var(--accent-purple)] focus:shadow-[0_0_8px_var(--accent-purple)] transition-shadow"
391-
style={{ lineHeight: `${LINE_HEIGHT}px` }}
392-
/>
435+
</div>
393436

394-
{/* Mic / Stop button overlay */}
437+
{/* Mic button */}
395438
{voice.isSupported && (
396439
<button
397440
onClick={() => voice.isListening ? voice.stop() : voice.start()}
398-
className={`absolute right-2.5 bottom-2.5 z-20 flex items-center justify-center w-7 h-7 rounded-full transition-all cursor-pointer ${
441+
className={`flex items-center justify-center w-7 h-7 rounded-full transition-all cursor-pointer ${
399442
voice.isListening
400443
? 'bg-red-500/15 text-red-400 hover:bg-red-500/25'
401444
: 'text-[var(--text-secondary)] hover:text-[var(--accent-purple)] opacity-50 hover:opacity-100'
402445
}`}
403446
title={voice.isListening ? 'Stop recording' : 'Voice input'}
404447
>
405448
{voice.isListening ? (
406-
/* Stop icon */
407449
<svg width="12" height="12" viewBox="0 0 12 12" fill="currentColor">
408450
<rect x="1" y="1" width="10" height="10" rx="1.5" />
409451
</svg>
410452
) : (
411-
/* Microphone icon */
412453
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
413454
<rect x="9" y="1" width="6" height="12" rx="3" />
414455
<path d="M5 10a7 7 0 0 0 14 0" />
@@ -417,77 +458,40 @@ export default function PromptBar({ panelId, onSend, onGeneratingChange, cwd, on
417458
)}
418459
</button>
419460
)}
420-
</div>
421461

422-
<AnimatePresence mode="wait">
423-
{isGenerating ? (
424-
<motion.button
425-
key="stop"
426-
initial={{ opacity: 0, scale: 0.8 }}
427-
animate={{ opacity: 1, scale: 1 }}
428-
exit={{ opacity: 0, scale: 0.8 }}
429-
transition={{ duration: 0.15 }}
430-
onClick={handleAbort}
431-
className="px-4 py-3 bg-red-600 text-white font-bold rounded-lg text-sm whitespace-nowrap cursor-pointer flex items-center shrink-0"
432-
>
433-
⏹ STOP
434-
</motion.button>
435-
) : (
436-
<motion.div
437-
key="send-group"
438-
initial={{ opacity: 0, scale: 0.8 }}
439-
animate={{ opacity: 1, scale: 1 }}
440-
exit={{ opacity: 0, scale: 0.8 }}
441-
transition={{ duration: 0.15 }}
442-
className="relative flex items-center shrink-0"
443-
>
444-
<button
445-
onClick={handleSend}
446-
className="px-3 py-3 bg-[var(--accent-gold)] text-black font-bold rounded-l-lg cursor-pointer flex items-center justify-center"
462+
{/* Send / Stop button */}
463+
<AnimatePresence mode="wait">
464+
{isGenerating ? (
465+
<motion.button
466+
key="stop"
467+
initial={{ opacity: 0, scale: 0.8 }}
468+
animate={{ opacity: 1, scale: 1 }}
469+
exit={{ opacity: 0, scale: 0.8 }}
470+
transition={{ duration: 0.15 }}
471+
onClick={handleAbort}
472+
className="flex items-center justify-center w-8 h-8 rounded-lg bg-red-600 text-white cursor-pointer"
473+
title="Stop generating"
447474
>
448-
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round"><line x1="22" y1="2" x2="11" y2="13"/><polygon points="22 2 15 22 11 13 2 9 22 2"/></svg>
449-
</button>
450-
<div className="w-px bg-black/20 self-stretch" />
451-
<button
452-
onClick={() => setSendMenuOpen(!sendMenuOpen)}
453-
className="px-1.5 py-3 bg-[var(--accent-gold)] text-black font-bold rounded-r-lg cursor-pointer flex items-center justify-center hover:bg-[var(--accent-gold)]/80 transition-colors"
475+
<svg width="12" height="12" viewBox="0 0 12 12" fill="currentColor">
476+
<rect x="1" y="1" width="10" height="10" rx="1.5" />
477+
</svg>
478+
</motion.button>
479+
) : (
480+
<motion.button
481+
key="send"
482+
initial={{ opacity: 0, scale: 0.8 }}
483+
animate={{ opacity: 1, scale: 1 }}
484+
exit={{ opacity: 0, scale: 0.8 }}
485+
transition={{ duration: 0.15 }}
486+
onClick={handleSend}
487+
className="flex items-center justify-center w-8 h-8 rounded-lg bg-[var(--accent-gold)] text-black cursor-pointer"
488+
title="Send (Enter)"
454489
>
455-
<svg width="10" height="10" viewBox="0 0 8 8" fill="currentColor" className={`transition-transform ${sendMenuOpen ? 'rotate-180' : ''}`}><path d="M0 2l4 4 4-4z"/></svg>
456-
</button>
457-
458-
{sendMenuOpen && (
459-
<>
460-
<div className="fixed inset-0 z-40" onClick={() => setSendMenuOpen(false)} />
461-
<div className="absolute bottom-full right-0 mb-2 z-50 w-44 rounded-lg border border-[var(--border-color)] bg-[var(--bg-secondary)] shadow-xl overflow-hidden">
462-
<button
463-
onClick={() => { setSendMenuOpen(false); onNewSession?.(); }}
464-
className="w-full text-left px-3 py-2 text-xs text-[var(--text-primary)] hover:bg-[var(--bg-primary)] transition-colors cursor-pointer flex items-center gap-2"
465-
>
466-
<span className="text-[var(--text-secondary)]">+</span> New Session
467-
</button>
468-
<button
469-
onClick={() => { setSendMenuOpen(false); onLoadSession?.(); }}
470-
className="w-full text-left px-3 py-2 text-xs text-[var(--text-primary)] hover:bg-[var(--bg-primary)] transition-colors cursor-pointer flex items-center gap-2"
471-
>
472-
<svg width="12" height="12" viewBox="0 0 16 16" fill="currentColor" className="text-[var(--text-secondary)]"><path d="M1.5 1.5v13h13v-13h-13zM0 1a1 1 0 011-1h14a1 1 0 011 1v14a1 1 0 01-1 1H1a1 1 0 01-1-1V1zm4.5 2.5h7v1h-7v-1zm0 3h7v1h-7v-1zm0 3h4v1h-4v-1z"/></svg>
473-
Load Session
474-
</button>
475-
<button
476-
onClick={() => { setSendMenuOpen(false); onSplitSession?.(); }}
477-
className="w-full text-left px-3 py-2 text-xs text-[var(--text-primary)] hover:bg-[var(--bg-primary)] transition-colors cursor-pointer flex items-center gap-2"
478-
>
479-
<svg width="12" height="12" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" className="text-[var(--text-secondary)]">
480-
<rect x="1" y="2" width="14" height="12" rx="1" />
481-
<line x1="8" y1="2" x2="8" y2="14" />
482-
</svg>
483-
Split Session
484-
</button>
485-
</div>
486-
</>
487-
)}
488-
</motion.div>
489-
)}
490-
</AnimatePresence>
490+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round"><line x1="22" y1="2" x2="11" y2="13"/><polygon points="22 2 15 22 11 13 2 9 22 2"/></svg>
491+
</motion.button>
492+
)}
493+
</AnimatePresence>
494+
</div>
491495
</div>
492496
</div>
493497
);

0 commit comments

Comments
Β (0)