Skip to content

Commit b6bedc9

Browse files
authored
fix(web-react): restore TopBar, panel system, full-screen settings, correct theme/settings placement (#123)
2 parents cfddd78 + 09fdc37 commit b6bedc9

10 files changed

Lines changed: 1299 additions & 75 deletions

File tree

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
.DS_Store
22
/jcode
3+
/jcode-new
34
/weixin_poc
45
internal/web/dist
6+
internal/web/dist-react
57
internal/model/registry_generated.go
68
web/src/styles/tokens.generated.css
79
web/src/composables/themes.generated.ts

internal/model/registry_generated.go

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

web-react/src/App.tsx

Lines changed: 102 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
* The WS bridge is a module-level singleton created here via useEffect (once).
1212
*/
1313

14-
import { useEffect, useRef } from 'react'
14+
import { useCallback, useEffect, useRef, useState } from 'react'
1515
import {
1616
RuntimeProvider,
1717
ToolRegistryProvider,
@@ -40,6 +40,9 @@ import { CommandPalette } from './components/CommandPalette'
4040
import { AuthGate } from './components/AuthGate'
4141
import { SetupView } from './components/SetupView'
4242
import { SettingsDialog } from './components/SettingsDialog'
43+
import { TopBar } from './components/TopBar'
44+
import { RightPanel } from './components/RightPanel'
45+
import { TerminalPanel } from './components/TerminalPanel'
4346

4447
export default function App() {
4548
const dispatch = useAppDispatch()
@@ -159,22 +162,118 @@ function store_getState() {
159162
return store.getState()
160163
}
161164

165+
type PanelType = 'terminal' | 'files' | 'changes' | 'plan'
166+
162167
function Shell({ activeView }: { activeView: 'chat' | 'automations' | 'channels' | 'automation-run' }) {
163168
const runtime = useChatRuntime()
164169
const registry = useRef(createDefaultToolRegistry()).current
165170
const paletteOpen = useAppSelector((s) => s.ui.paletteOpen)
171+
const isRunning = useAppSelector((s) => s.chat.isRunning)
172+
const wsConnected = useAppSelector((s) => s.session.wsConnected)
173+
174+
// Panel state — mirrors Vue App.vue: a right panel (files/changes/plan) and a
175+
// bottom panel (terminal) that can be open simultaneously.
176+
const [rightPanelOpen, setRightPanelOpen] = useState(false)
177+
const [rightPanelTab, setRightPanelTab] = useState<'files' | 'changes' | 'plan'>('files')
178+
const [bottomPanel, setBottomPanel] = useState<'none' | 'terminal'>('none')
179+
const [bottomPanelHeight, setBottomPanelHeight] = useState(260)
180+
181+
const togglePanel = useCallback((panel: PanelType) => {
182+
if (panel === 'terminal') {
183+
setBottomPanel((p) => (p === 'terminal' ? 'none' : 'terminal'))
184+
return
185+
}
186+
setRightPanelTab((current) => {
187+
if (rightPanelOpen && current === panel) {
188+
setRightPanelOpen(false)
189+
return current
190+
}
191+
setRightPanelOpen(true)
192+
return panel
193+
})
194+
}, [rightPanelOpen])
195+
196+
// Panel keyboard shortcuts: ⇧⌘P (plan), ⇧⌘E (files), ⇧⌘G (changes), ⌘` (terminal).
197+
useEffect(() => {
198+
function onKey(e: KeyboardEvent) {
199+
const meta = e.metaKey || e.ctrlKey
200+
if (meta && e.shiftKey && e.key.toLowerCase() === 'p') {
201+
e.preventDefault(); togglePanel('plan')
202+
} else if (meta && e.shiftKey && e.key.toLowerCase() === 'e') {
203+
e.preventDefault(); togglePanel('files')
204+
} else if (meta && e.shiftKey && e.key.toLowerCase() === 'g') {
205+
e.preventDefault(); togglePanel('changes')
206+
} else if (meta && e.key === '`') {
207+
e.preventDefault(); togglePanel('terminal')
208+
}
209+
}
210+
window.addEventListener('keydown', onKey)
211+
return () => window.removeEventListener('keydown', onKey)
212+
}, [togglePanel])
213+
214+
// Bottom-panel resize handle.
215+
const startResize = useCallback((e: React.MouseEvent) => {
216+
e.preventDefault()
217+
const startY = e.clientY
218+
const startH = bottomPanelHeight
219+
function onMove(ev: MouseEvent) {
220+
const diff = startY - ev.clientY
221+
setBottomPanelHeight(Math.max(120, Math.min(600, startH + diff)))
222+
}
223+
function onUp() {
224+
document.removeEventListener('mousemove', onMove)
225+
document.removeEventListener('mouseup', onUp)
226+
}
227+
document.addEventListener('mousemove', onMove)
228+
document.addEventListener('mouseup', onUp)
229+
}, [bottomPanelHeight])
166230

167231
return (
168232
<RuntimeProvider runtime={runtime}>
169233
<ToolRegistryProvider registry={registry}>
170-
<div className="flex h-screen overflow-hidden bg-[var(--color-background)] text-[var(--color-foreground)]">
234+
<div className="relative flex h-[100dvh] overflow-hidden bg-[var(--color-background)] text-[var(--color-foreground)]">
235+
{/* TopBar — floated top-right (only on chat view, like Vue). */}
236+
{activeView === 'chat' && (
237+
<TopBar
238+
isRunning={isRunning}
239+
wsConnected={wsConnected}
240+
activePanel={rightPanelOpen ? rightPanelTab : 'none'}
241+
terminalOpen={bottomPanel === 'terminal'}
242+
onTogglePanel={togglePanel}
243+
/>
244+
)}
245+
171246
<Sidebar />
172-
<main className="flex min-w-0 flex-1 flex-col">
247+
248+
<main className="relative flex min-w-0 flex-1 flex-col">
173249
{activeView === 'chat' && <ChatView />}
174250
{activeView === 'automations' && <AutomationsView />}
175251
{activeView === 'channels' && <ChannelsView />}
176252
{activeView === 'automation-run' && <ChatView readOnly />}
253+
254+
{/* Bottom panel (terminal) */}
255+
{bottomPanel === 'terminal' && (
256+
<div className="relative shrink-0 border-t border-[var(--color-border)]" style={{ height: bottomPanelHeight }}>
257+
<div
258+
className="absolute -top-1 left-0 right-0 z-10 h-2 cursor-row-resize"
259+
onMouseDown={startResize}
260+
>
261+
<div className="absolute left-1/2 top-[3px] h-1 w-8 -translate-x-1/2 rounded-full bg-[var(--color-border)]" />
262+
</div>
263+
<TerminalPanel onClose={() => setBottomPanel('none')} />
264+
</div>
265+
)}
177266
</main>
267+
268+
{/* Right panel (files/changes/plan) */}
269+
{rightPanelOpen && (
270+
<RightPanel
271+
activeTab={rightPanelTab}
272+
onClose={() => setRightPanelOpen(false)}
273+
onSwitchTab={setRightPanelTab}
274+
/>
275+
)}
276+
178277
{paletteOpen && <CommandPalette />}
179278
<SettingsDialog />
180279
</div>

web-react/src/components/ChatView.tsx

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
/**
2-
* ChatView — the main chat column. Composes jcode-ui's Thread + ChatInput with
3-
* a header (project path, connection status) and a GoalBanner. The data flows
4-
* from the RTK store via the RuntimeProvider wired in App.tsx.
2+
* ChatView — the main chat column. The TopBar (floated top-right) carries the
3+
* panel menu + connection status, so there is NO separate header here (matches
4+
* the Vue app, where the chat canvas has no header bar). Just GoalBanner +
5+
* Thread + ChatInput.
56
*/
67

78
import { Thread } from 'jcode-ui'
89
import { GoalBanner } from './GoalBanner'
9-
import { ProjectHeader } from './ProjectHeader'
1010
import { ChatInput } from './ChatInput'
1111

1212
export interface ChatViewProps {
@@ -17,7 +17,6 @@ export interface ChatViewProps {
1717
export function ChatView({ readOnly }: ChatViewProps) {
1818
return (
1919
<div className="flex min-h-0 flex-1 flex-col">
20-
<ProjectHeader />
2120
<GoalBanner />
2221
<div className="min-h-0 flex-1">
2322
<Thread overscanBottom={readOnly ? 8 : 96} />

0 commit comments

Comments
 (0)