Skip to content

Commit bc065ef

Browse files
committed
feat: enhance chat and editor components with new functionality and improved UI
- Updated the ProviderSelector component to allow selection of additional agent providers, including 'codex'. - Enhanced the EditorTabs component to support tab reordering and improved preview functionality. - Integrated model selection into the ChatInputBar and ChatHome components for better user experience. - Improved event handling in the GatewayTerminal to mirror agent replies from chat, ensuring a cohesive interaction. - Refined scrolling behavior in the MessageList component to enhance usability when navigating chat history.
1 parent 1d7976d commit bc065ef

16 files changed

Lines changed: 580 additions & 215 deletions

components/agent-panel.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1132,7 +1132,7 @@ export function AgentPanel({ onClose }: { onClose?: () => void } = {}) {
11321132
timestamp: Date.now(),
11331133
editProposals: editProposals.length > 0 ? editProposals : undefined,
11341134
})
1135-
emit('agent-reply')
1135+
emit('agent-reply', { content: reply, sessionKey, source: 'chat' })
11361136
logChatDebug('assistant reply appended from direct response', {
11371137
idempotencyKey: idemKey,
11381138
replyChars: reply.length,
@@ -1877,7 +1877,7 @@ export function AgentPanel({ onClose }: { onClose?: () => void } = {}) {
18771877
type: editProposals.length > 0 ? 'edit' : 'text',
18781878
editProposals: editProposals.length > 0 ? editProposals : undefined,
18791879
})
1880-
emit('agent-reply')
1880+
emit('agent-reply', { content: reply, sessionKey, source: 'chat' })
18811881
}
18821882
setIsStreaming(false)
18831883
setSending(false)

components/chat-home.tsx

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -602,6 +602,16 @@ export const ChatHome = memo(function ChatHome({
602602
</div>
603603
</motion.div>
604604

605+
{/* Model selector — ChatGPT/Cursor-style, above composer */}
606+
<motion.div
607+
initial={{ opacity: 0, y: 20 }}
608+
animate={{ opacity: 1, y: 0 }}
609+
transition={{ duration: 0.5, delay: 0.55 }}
610+
className="mb-1.5"
611+
>
612+
<ProviderSelector size="sm" />
613+
</motion.div>
614+
605615
{/* Composer */}
606616
<motion.div
607617
initial={{ opacity: 0, y: 20 }}
@@ -675,14 +685,6 @@ export const ChatHome = memo(function ChatHome({
675685

676686
{/* Mode selector */}
677687
<ModeSelector mode={agentMode} onChange={setAgentMode} size="sm" />
678-
679-
{/* Divider — desktop only */}
680-
<div className="hidden sm:block w-px h-4 bg-[var(--border)]" />
681-
682-
{/* Provider selector — desktop only */}
683-
<span className="hidden sm:inline-flex">
684-
<ProviderSelector size="sm" />
685-
</span>
686688
</div>
687689

688690
{/* Send button */}

components/chat/chat-input-bar.tsx

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -369,6 +369,11 @@ export function ChatInputBar({
369369
</div>
370370
)}
371371

372+
{/* Model selector — ChatGPT/Cursor-style, above input */}
373+
<div className="mb-1">
374+
<ProviderSelector size="sm" />
375+
</div>
376+
372377
{/* Unified input container with drag-drop zone */}
373378
<div
374379
className={`chat-input-shell overflow-hidden rounded-[18px] border bg-[var(--bg)] transition-colors ${
@@ -638,15 +643,12 @@ export function ChatInputBar({
638643
</div>
639644
</div>
640645

641-
{/* Bottom bar — mode + provider */}
642-
<div className="flex items-center justify-between mt-1">
646+
{/* Bottom bar — mode selector */}
647+
<div className="flex items-center mt-1">
643648
<div className="flex items-center gap-1.5">
644649
<ModeSelector mode={agentMode} onChange={setAgentMode} />
645650
<span className="hidden sm:inline text-[9px] text-[var(--text-disabled)]">⇧Tab</span>
646651
</div>
647-
<div className="hidden sm:flex items-center gap-2">
648-
<ProviderSelector size="sm" />
649-
</div>
650652
</div>
651653
</div>
652654
</>

components/chat/message-list.tsx

Lines changed: 34 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,7 @@ export function MessageList({
208208
const [lightboxSrc, setLightboxSrc] = useState<string | null>(null)
209209
const [showScrollToBottom, setShowScrollToBottom] = useState(false)
210210
const [copiedCode, setCopiedCode] = useState<string | null>(null)
211+
const shouldStickToBottomRef = useRef(true)
211212
const { chatFontSize, chatFontCss } = useChatAppearance()
212213

213214
const visibleMessages = useMemo(
@@ -216,10 +217,19 @@ export function MessageList({
216217
)
217218

218219
useEffect(() => {
219-
if (scrollRef.current) {
220-
scrollRef.current.scrollTop = scrollRef.current.scrollHeight
221-
}
222-
}, [messages, streamBuffer])
220+
const el = scrollRef.current
221+
if (!el) return
222+
223+
if (!shouldStickToBottomRef.current) return
224+
225+
const raf = requestAnimationFrame(() => {
226+
const nextEl = scrollRef.current
227+
if (!nextEl) return
228+
nextEl.scrollTop = nextEl.scrollHeight
229+
})
230+
231+
return () => cancelAnimationFrame(raf)
232+
}, [messages, streamBuffer, isStreaming])
223233

224234
const scrollToBottom = useCallback(() => {
225235
if (scrollRef.current) {
@@ -322,13 +332,13 @@ export function MessageList({
322332
className="h-full overflow-y-auto px-3 py-3 space-y-3 scroll-shadow"
323333
onScroll={(e) => {
324334
const el = e.currentTarget
335+
const distanceFromBottom = el.scrollHeight - el.scrollTop - el.clientHeight
336+
const nearBottom = distanceFromBottom < 80
337+
shouldStickToBottomRef.current = nearBottom
325338
el.classList.toggle('has-scroll-top', el.scrollTop > 8)
326-
el.classList.toggle(
327-
'has-scroll-bottom',
328-
el.scrollTop + el.clientHeight < el.scrollHeight - 8,
329-
)
330-
// Show scroll to bottom FAB if scrolled up more than 200px from bottom
331-
setShowScrollToBottom(el.scrollTop + el.clientHeight < el.scrollHeight - 200)
339+
el.classList.toggle('has-scroll-bottom', !nearBottom)
340+
// Show scroll to bottom FAB if the reader has intentionally drifted away.
341+
setShowScrollToBottom(distanceFromBottom > 200)
332342
}}
333343
>
334344
{visibleMessages.map((msg, idx) => {
@@ -699,6 +709,20 @@ export function MessageList({
699709
<span className="text-[10px] font-medium text-[var(--text-tertiary)]">
700710
Knot
701711
</span>
712+
{(agentActivities.length > 0 || turnElapsedMs > 0) && (
713+
<div className="ml-auto flex items-center gap-1.5 text-[9px] text-[var(--text-disabled)]">
714+
{agentActivities.length > 0 && (
715+
<span className="inline-flex items-center gap-1 rounded-full border border-[var(--border)] bg-[color-mix(in_srgb,var(--bg-elevated)_88%,transparent)] px-1.5 py-0.5">
716+
<Icon icon="lucide:wrench" width={9} height={9} />
717+
{agentActivities[agentActivities.length - 1]?.label ??
718+
`${agentActivities.length} actions`}
719+
</span>
720+
)}
721+
{turnElapsedMs > 999 && (
722+
<span className="tabular-nums">{Math.round(turnElapsedMs / 1000)}s</span>
723+
)}
724+
</div>
725+
)}
702726
</div>
703727
<div
704728
className="rounded-xl px-3 py-2 leading-relaxed bg-[var(--bg-subtle)] border border-[color-mix(in_srgb,var(--brand)_20%,var(--border))] text-[var(--text-primary)] rounded-bl-sm"

components/editor-tabs.tsx

Lines changed: 49 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import { useState, useCallback, useRef } from 'react'
44
import { Icon } from '@iconify/react'
5-
import { useEditor } from '@/context/editor-context'
5+
import { PREVIEW_TAB_ID, useEditor } from '@/context/editor-context'
66
import { useView } from '@/context/view-context'
77
import { formatShortcut } from '@/lib/platform'
88

@@ -49,7 +49,16 @@ function getFileIcon(path: string) {
4949
}
5050

5151
export function EditorTabs({ onTabSelect }: { onTabSelect?: (path: string) => void }) {
52-
const { files, activeFile, setActiveFile, closeFile, reorderFiles } = useEditor()
52+
const {
53+
tabs,
54+
files,
55+
activeFile,
56+
setActiveFile,
57+
closeFile,
58+
reorderTabs,
59+
closePreviewTab,
60+
openPreviewTab,
61+
} = useEditor()
5362
const { activeView, setView } = useView()
5463
const previewActive = activeView === 'preview'
5564
const [dragIndex, setDragIndex] = useState<number | null>(null)
@@ -60,7 +69,6 @@ export function EditorTabs({ onTabSelect }: { onTabSelect?: (path: string) => vo
6069
setDragIndex(index)
6170
dragNode.current = e.currentTarget as HTMLDivElement
6271
e.dataTransfer.effectAllowed = 'move'
63-
// Make drag image semi-transparent
6472
requestAnimationFrame(() => {
6573
if (dragNode.current) dragNode.current.style.opacity = '0.4'
6674
})
@@ -69,32 +77,38 @@ export function EditorTabs({ onTabSelect }: { onTabSelect?: (path: string) => vo
6977
const handleDragEnd = useCallback(() => {
7078
if (dragNode.current) dragNode.current.style.opacity = '1'
7179
if (dragIndex !== null && dropTarget !== null && dragIndex !== dropTarget) {
72-
reorderFiles(dragIndex, dropTarget)
80+
reorderTabs(dragIndex, dropTarget)
7381
}
7482
setDragIndex(null)
7583
setDropTarget(null)
7684
dragNode.current = null
77-
}, [dragIndex, dropTarget, reorderFiles])
85+
}, [dragIndex, dropTarget, reorderTabs])
7886

7987
const handleDragOver = useCallback((e: React.DragEvent, index: number) => {
8088
e.preventDefault()
8189
e.dataTransfer.dropEffect = 'move'
8290
setDropTarget(index)
8391
}, [])
8492

85-
if (files.length === 0) return null
93+
if (tabs.length === 0) return null
8694

8795
return (
8896
<div className="relative flex items-center border-b border-[var(--border)] bg-[var(--bg)] overflow-x-auto no-scrollbar shrink-0 h-[42px]">
89-
{files.map((file, index) => {
90-
const name = file.path.split('/').pop() ?? file.path
91-
const isActive = !previewActive && file.path === activeFile
97+
{tabs.map((tab, index) => {
98+
const isPreview = tab.type === 'preview'
99+
const tabPath = tab.type === 'file' ? tab.path : null
100+
const isActive = isPreview ? previewActive : !previewActive && tabPath === activeFile
92101
const isDragTarget = dropTarget === index && dragIndex !== null && dragIndex !== index
93-
const { icon, color } = getFileIcon(file.path)
102+
103+
const label = isPreview ? 'Preview' : (tabPath?.split('/').pop() ?? '')
104+
const dirty = tabPath ? files.find((file) => file.path === tabPath)?.dirty : false
105+
const iconMeta = isPreview
106+
? { icon: 'lucide:eye', color: 'var(--brand)' }
107+
: getFileIcon(tabPath ?? '')
94108

95109
return (
96110
<div
97-
key={file.path}
111+
key={tab.id}
98112
draggable
99113
onDragStart={(e) => handleDragStart(e, index)}
100114
onDragEnd={handleDragEnd}
@@ -110,90 +124,72 @@ export function EditorTabs({ onTabSelect }: { onTabSelect?: (path: string) => vo
110124
${isDragTarget ? 'border-b-[var(--brand)] bg-[var(--bg-subtle)]' : ''}
111125
`}
112126
onClick={() => {
113-
setActiveFile(file.path)
127+
if (isPreview) {
128+
openPreviewTab()
129+
setView('preview')
130+
return
131+
}
132+
if (!tabPath) return
133+
setActiveFile(tabPath)
114134
setView('editor')
115-
onTabSelect?.(file.path)
135+
onTabSelect?.(tabPath)
116136
}}
117137
>
118-
{/* Active indicator */}
119138
{isActive && (
120139
<div className="absolute bottom-0 left-0 right-0 h-[3px]">
121140
<div className="h-full bg-[var(--brand)] rounded-t-full" />
122141
</div>
123142
)}
124143

125-
{/* File icon */}
126144
<Icon
127-
icon={icon}
145+
icon={iconMeta.icon}
128146
width={17}
129147
height={17}
130-
style={{ color: isActive ? color : undefined }}
148+
style={{ color: isActive ? iconMeta.color : undefined }}
131149
className={`transition-colors duration-150 ${isActive ? '' : 'text-[var(--text-tertiary)]'}`}
132150
/>
133151

134-
{/* File name */}
135-
<span className="text-[13px] font-medium truncate max-w-[140px]" title={file.path}>
136-
{name}
152+
<span
153+
className="text-[13px] font-medium truncate max-w-[140px]"
154+
title={isPreview ? 'Preview' : (tabPath ?? '')}
155+
>
156+
{label}
137157
</span>
138158

139-
{/* Dirty indicator */}
140-
{file.dirty && (
159+
{dirty && (
141160
<span
142161
className="h-1.5 w-1.5 shrink-0 rounded-full bg-[var(--brand)]"
143162
title="Unsaved changes"
144163
/>
145164
)}
146165

147-
{/* Close button — show dot when dirty and not hovered */}
148166
<button
149167
onClick={(e) => {
150168
e.stopPropagation()
151-
closeFile(file.path)
169+
if (isPreview) {
170+
closePreviewTab()
171+
if (previewActive) setView('editor')
172+
return
173+
}
174+
if (tabPath) closeFile(tabPath)
152175
}}
153176
className="ml-1 cursor-pointer rounded-lg p-1.5 opacity-0 transition-colors hover:bg-[var(--bg)] group-hover:opacity-100 focus:opacity-100"
154177
title={`Close (${formatShortcut('meta+W')})`}
155178
>
156179
<Icon icon="lucide:x" width={14} height={14} />
157180
</button>
158181

159-
{/* Separator */}
160182
{!isActive && (
161183
<div className="absolute right-0 top-[6px] bottom-[6px] w-px bg-[var(--border)] opacity-30" />
162184
)}
163185
</div>
164186
)
165187
})}
166188

167-
<div
168-
className={`
169-
group relative flex items-center gap-2.5 px-4 h-full cursor-pointer transition-colors duration-150 select-none min-w-0 shrink-0
170-
${
171-
previewActive
172-
? 'bg-[var(--bg-elevated)] text-[var(--text-primary)]'
173-
: 'text-[var(--text-secondary)] hover:bg-[var(--bg-subtle)] hover:text-[var(--text-primary)]'
174-
}
175-
`}
176-
onClick={() => setView('preview')}
177-
>
178-
{previewActive && (
179-
<div className="absolute bottom-0 left-0 right-0 h-[3px]">
180-
<div className="h-full bg-[var(--brand)] rounded-t-full" />
181-
</div>
182-
)}
183-
<Icon
184-
icon="lucide:eye"
185-
width={17}
186-
height={17}
187-
className={`transition-colors duration-150 ${previewActive ? 'text-[var(--brand)]' : 'text-[var(--text-tertiary)]'}`}
188-
/>
189-
<span className="text-[13px] font-medium truncate max-w-[140px]">Preview</span>
190-
</div>
191-
192-
{/* Tab count indicator when many tabs are open */}
193-
{files.length > 6 && (
189+
{tabs.length > 6 && (
194190
<div className="sticky right-0 flex items-center px-2 h-full bg-gradient-to-l from-[var(--bg)] via-[var(--bg)] to-transparent shrink-0">
195191
<span className="text-[11px] font-mono font-bold text-[var(--text-tertiary)] bg-[var(--bg-subtle)] px-2.5 py-1 rounded-full border-[1.5px] border-[var(--border)]">
196-
{files.length}
192+
{tabs.length}
197193
</span>
198194
</div>
199195
)}

0 commit comments

Comments
 (0)