Resizable workbench with proper scroll and fix chat message contrast.
Workbench drag handle for SQL/agent panels; dark-theme user bubbles and input field readable.
This commit is contained in:
@@ -34,7 +34,7 @@ export function AgentWorkbench({ agent, lines, busy, onSendPrompt, compact }: Pr
|
|||||||
}, [lines, busy])
|
}, [lines, busy])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full min-h-0 flex-col">
|
<div className="flex h-full min-h-0 flex-col overflow-hidden">
|
||||||
<header className={cn('flex shrink-0 items-center gap-2 border-b border-border/60 px-2', compact ? 'py-1' : 'py-3')}>
|
<header className={cn('flex shrink-0 items-center gap-2 border-b border-border/60 px-2', compact ? 'py-1' : 'py-3')}>
|
||||||
<span
|
<span
|
||||||
className={cn('flex shrink-0 items-center justify-center rounded-lg border border-border/80 bg-surface-overlay', compact ? 'h-7 w-7' : 'h-10 w-10')}
|
className={cn('flex shrink-0 items-center justify-center rounded-lg border border-border/80 bg-surface-overlay', compact ? 'h-7 w-7' : 'h-10 w-10')}
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ export function CommandBar({ busy, selectedAgent, onSubmit }: Props) {
|
|||||||
onChange={(e) => setInput(e.target.value)}
|
onChange={(e) => setInput(e.target.value)}
|
||||||
placeholder={selectedAgent ? `Command ${selectedAgent.name.split(' ·')[0]}…` : 'Enter command — auto-routed to specialist…'}
|
placeholder={selectedAgent ? `Command ${selectedAgent.name.split(' ·')[0]}…` : 'Enter command — auto-routed to specialist…'}
|
||||||
disabled={busy}
|
disabled={busy}
|
||||||
className="flex-1"
|
className="flex-1 bg-surface text-foreground placeholder:text-foreground-faint dark:bg-[#0d1520] dark:text-slate-100 dark:placeholder:text-slate-500"
|
||||||
/>
|
/>
|
||||||
<Button type="submit" disabled={busy || !input.trim()}>
|
<Button type="submit" disabled={busy || !input.trim()}>
|
||||||
<Send className="h-3.5 w-3.5" />
|
<Send className="h-3.5 w-3.5" />
|
||||||
|
|||||||
@@ -45,8 +45,8 @@ export function CommsPanel({ messages, agents, selectedAgent, busy }: Props) {
|
|||||||
const agMeta = ag ? getAgentMeta(ag.id) : null
|
const agMeta = ag ? getAgentMeta(ag.id) : null
|
||||||
return (
|
return (
|
||||||
<div key={i} className={cn('flex gap-2', m.role === 'user' && 'flex-row-reverse')}>
|
<div key={i} className={cn('flex gap-2', m.role === 'user' && 'flex-row-reverse')}>
|
||||||
<div className={cn('max-w-[85%] rounded-lg border px-2 py-1.5', m.role === 'user' ? 'border-docker/25 bg-docker-light' : 'border-border bg-surface-overlay')}>
|
<div className={cn('max-w-[85%] rounded-lg border px-2 py-1.5', m.role === 'user' ? 'border-docker/40 bg-docker/15 dark:border-blue-400/35 dark:bg-[#0f2744]' : 'border-border bg-surface-overlay')}>
|
||||||
<p className="mb-0.5 text-[8px] text-foreground-muted">
|
<p className={cn('mb-0.5 text-[8px]', m.role === 'user' ? 'text-docker dark:text-blue-300' : 'text-foreground-muted')}>
|
||||||
{m.role === 'user' ? 'You' : ag?.name || m.agent}
|
{m.role === 'user' ? 'You' : ag?.name || m.agent}
|
||||||
{m.ts && ` · ${new Date(m.ts).toLocaleTimeString('en-US', { hour12: false })}`}
|
{m.ts && ` · ${new Date(m.ts).toLocaleTimeString('en-US', { hour12: false })}`}
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -144,10 +144,10 @@ export function SqlWorkbench({ engine, compact }: Props) {
|
|||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div className="flex min-h-0 flex-1">
|
<div className="flex min-h-0 flex-1 overflow-hidden">
|
||||||
<aside className={cn('shrink-0 border-r border-border/60 p-1.5', compact ? 'w-44' : 'w-52')}>
|
<aside className={cn('flex min-h-0 shrink-0 flex-col border-r border-border/60 p-1.5', compact ? 'w-44' : 'w-52')}>
|
||||||
<p className="mb-0.5 text-[8px] font-semibold uppercase tracking-wider text-foreground-faint">10 demo commands</p>
|
<p className="mb-0.5 text-[8px] font-semibold uppercase tracking-wider text-foreground-faint">10 demo commands</p>
|
||||||
<div className="scrollbar-thin max-h-full space-y-0.5 overflow-y-auto">
|
<div className="scrollbar-thin min-h-0 flex-1 space-y-0.5 overflow-y-auto">
|
||||||
{samples.map((s) => (
|
{samples.map((s) => (
|
||||||
<button
|
<button
|
||||||
key={s.id}
|
key={s.id}
|
||||||
@@ -166,8 +166,8 @@ export function SqlWorkbench({ engine, compact }: Props) {
|
|||||||
value={sql}
|
value={sql}
|
||||||
onChange={(e) => setSql(e.target.value)}
|
onChange={(e) => setSql(e.target.value)}
|
||||||
className={cn(
|
className={cn(
|
||||||
'shrink-0 resize-none border-b border-border/60 bg-black/40 p-1.5 font-mono text-[10px] text-emerald-100 outline-none focus:ring-1 focus:ring-docker/40',
|
'shrink-0 resize-y border-b border-border/60 bg-[#0d1117] p-1.5 font-mono text-[10px] text-emerald-100 outline-none focus:ring-1 focus:ring-docker/40',
|
||||||
compact ? 'min-h-[40px]' : 'min-h-[56px]',
|
compact ? 'min-h-[48px] max-h-[96px]' : 'min-h-[56px] max-h-[120px]',
|
||||||
)}
|
)}
|
||||||
spellCheck={false}
|
spellCheck={false}
|
||||||
placeholder={`${meta.queryLabel}…`}
|
placeholder={`${meta.queryLabel}…`}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { useEffect, useRef, useState } from 'react'
|
||||||
|
import { GripHorizontal } from 'lucide-react'
|
||||||
import type { Agent, TerminalLine } from '../../types'
|
import type { Agent, TerminalLine } from '../../types'
|
||||||
import { AgentWorkbench } from './AgentWorkbench'
|
import { AgentWorkbench } from './AgentWorkbench'
|
||||||
import { SqlWorkbench } from './SqlWorkbench'
|
import { SqlWorkbench } from './SqlWorkbench'
|
||||||
@@ -10,18 +12,63 @@ type Props = {
|
|||||||
onSendPrompt: (message: string, agentId?: string) => void
|
onSendPrompt: (message: string, agentId?: string) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const MIN_H = 200
|
||||||
|
const DEFAULT_H = 300
|
||||||
|
const MAX_VH = 58
|
||||||
|
|
||||||
export function WorkbenchPanel({ mode, agent, lines, busy, onSendPrompt }: Props) {
|
export function WorkbenchPanel({ mode, agent, lines, busy, onSendPrompt }: Props) {
|
||||||
|
const [height, setHeight] = useState(DEFAULT_H)
|
||||||
|
const dragging = useRef(false)
|
||||||
|
const startY = useRef(0)
|
||||||
|
const startH = useRef(0)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const onMove = (e: MouseEvent) => {
|
||||||
|
if (!dragging.current) return
|
||||||
|
const maxH = window.innerHeight * (MAX_VH / 100)
|
||||||
|
const next = startH.current + (startY.current - e.clientY)
|
||||||
|
setHeight(Math.max(MIN_H, Math.min(maxH, next)))
|
||||||
|
}
|
||||||
|
const onUp = () => { dragging.current = false }
|
||||||
|
window.addEventListener('mousemove', onMove)
|
||||||
|
window.addEventListener('mouseup', onUp)
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('mousemove', onMove)
|
||||||
|
window.removeEventListener('mouseup', onUp)
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
if (!mode) return null
|
if (!mode) return null
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="flex h-[190px] max-h-[26vh] shrink-0 flex-col border-t border-docker/25 bg-[#0a0e14] shadow-[0_-4px_20px_rgba(0,0,0,0.25)]">
|
<section
|
||||||
{mode === 'agent' && agent && (
|
style={{ height }}
|
||||||
<AgentWorkbench agent={agent} lines={lines} busy={busy} onSendPrompt={onSendPrompt} compact />
|
className="flex shrink-0 flex-col overflow-hidden border-t border-docker/25 bg-[#0a0e14] shadow-[0_-4px_20px_rgba(0,0,0,0.25)]"
|
||||||
)}
|
>
|
||||||
{mode === 'sql-postgres' && <SqlWorkbench engine="postgres" compact />}
|
<div
|
||||||
{mode === 'sql-mysql' && <SqlWorkbench engine="mysql" compact />}
|
role="separator"
|
||||||
{mode === 'sql-mongodb' && <SqlWorkbench engine="mongodb" compact />}
|
aria-label="Resize workbench"
|
||||||
{mode === 'sql-trino' && <SqlWorkbench engine="trino" compact />}
|
onMouseDown={(e) => {
|
||||||
|
dragging.current = true
|
||||||
|
startY.current = e.clientY
|
||||||
|
startH.current = height
|
||||||
|
e.preventDefault()
|
||||||
|
}}
|
||||||
|
className="flex h-2.5 shrink-0 cursor-ns-resize items-center justify-center border-b border-border/50 bg-surface-overlay/60 hover:bg-docker/10"
|
||||||
|
title="Sleep omhoog/omlaag om grootte aan te passen"
|
||||||
|
>
|
||||||
|
<GripHorizontal className="h-3 w-3 text-foreground-faint" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="min-h-0 flex-1 overflow-hidden">
|
||||||
|
{mode === 'agent' && agent && (
|
||||||
|
<AgentWorkbench agent={agent} lines={lines} busy={busy} onSendPrompt={onSendPrompt} compact />
|
||||||
|
)}
|
||||||
|
{mode === 'sql-postgres' && <SqlWorkbench engine="postgres" compact />}
|
||||||
|
{mode === 'sql-mysql' && <SqlWorkbench engine="mysql" compact />}
|
||||||
|
{mode === 'sql-mongodb' && <SqlWorkbench engine="mongodb" compact />}
|
||||||
|
{mode === 'sql-trino' && <SqlWorkbench engine="trino" compact />}
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ export function Input({ className, ...props }: Props) {
|
|||||||
return (
|
return (
|
||||||
<input
|
<input
|
||||||
className={cn(
|
className={cn(
|
||||||
'h-8 w-full rounded-md border border-border bg-surface px-2.5 text-sm text-foreground placeholder:text-foreground-faint focus:border-docker/50 focus:outline-none focus:ring-1 focus:ring-docker/30',
|
'h-8 w-full rounded-md border border-border bg-surface px-2.5 text-sm text-foreground placeholder:text-foreground-faint focus:border-docker/50 focus:outline-none focus:ring-1 focus:ring-docker/30 dark:bg-[#0d1520] dark:text-slate-100 dark:placeholder:text-slate-500',
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
Reference in New Issue
Block a user