From f7c089d8bae112edad5492b74d3984d25e9e1a27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Flor=C3=AAncio?= Date: Sun, 11 Jan 2026 12:35:30 +0000 Subject: [PATCH] feat: ws and zustand init --- .gitignore | 2 + apps/web/app/layout.tsx | 5 +- apps/web/app/page.tsx | 25 +++- apps/web/app/task/[id]/page-client.tsx | 166 +++++++++++++++++++++++ apps/web/app/task/[id]/page.tsx | 178 +++---------------------- apps/web/components/state-hydrator.tsx | 21 +++ apps/web/components/state-provider.tsx | 35 +++++ apps/web/lib/ssr/http.ts | 23 ++++ apps/web/lib/ssr/mapper.ts | 29 ++++ apps/web/lib/state/store.ts | 74 ++++++++++ apps/web/lib/types/backend.ts | 63 +++++++++ apps/web/lib/types/http.ts | 42 ++++++ apps/web/lib/ws/client.ts | 87 ++++++++++++ apps/web/lib/ws/router.ts | 55 ++++++++ apps/web/lib/ws/use-websocket.tsx | 29 ++++ apps/web/package-lock.json | 44 +++++- apps/web/package.json | 4 +- 17 files changed, 715 insertions(+), 167 deletions(-) create mode 100644 apps/web/app/task/[id]/page-client.tsx create mode 100644 apps/web/components/state-hydrator.tsx create mode 100644 apps/web/components/state-provider.tsx create mode 100644 apps/web/lib/ssr/http.ts create mode 100644 apps/web/lib/ssr/mapper.ts create mode 100644 apps/web/lib/state/store.ts create mode 100644 apps/web/lib/types/backend.ts create mode 100644 apps/web/lib/types/http.ts create mode 100644 apps/web/lib/ws/client.ts create mode 100644 apps/web/lib/ws/router.ts create mode 100644 apps/web/lib/ws/use-websocket.tsx diff --git a/.gitignore b/.gitignore index 3b206d05f9..567ad02ea5 100644 --- a/.gitignore +++ b/.gitignore @@ -66,3 +66,5 @@ tmp/ *-secret.yaml *-secrets.yaml +apps/backend/bin/ + diff --git a/apps/web/app/layout.tsx b/apps/web/app/layout.tsx index bcc23374e1..115bc60966 100644 --- a/apps/web/app/layout.tsx +++ b/apps/web/app/layout.tsx @@ -1,6 +1,7 @@ import type { Metadata } from "next"; import "./globals.css"; import { ThemeProvider } from "@/components/theme-provider"; +import { StateProvider } from "@/components/state-provider"; export const metadata: Metadata = { title: "Kandev - AI Kanban", @@ -15,7 +16,9 @@ export default function RootLayout({ return ( - {children} + + {children} + ); diff --git a/apps/web/app/page.tsx b/apps/web/app/page.tsx index 860576a99e..1bd9554404 100644 --- a/apps/web/app/page.tsx +++ b/apps/web/app/page.tsx @@ -1,7 +1,24 @@ -'use client'; - import { KanbanBoard } from '@/components/kanban-board'; +import { StateHydrator } from '@/components/state-hydrator'; +import { fetchBoardSnapshot, fetchBoards } from '@/lib/ssr/http'; +import { snapshotToState } from '@/lib/ssr/mapper'; -export default function Page() { - return ; +export default async function Page() { + try { + const boards = await fetchBoards(); + const boardId = boards[0]?.id; + if (!boardId) { + return ; + } + const snapshot = await fetchBoardSnapshot(boardId); + const initialState = snapshotToState(snapshot); + return ( + <> + + + + ); + } catch { + return ; + } } diff --git a/apps/web/app/task/[id]/page-client.tsx b/apps/web/app/task/[id]/page-client.tsx new file mode 100644 index 0000000000..5ba03a020e --- /dev/null +++ b/apps/web/app/task/[id]/page-client.tsx @@ -0,0 +1,166 @@ +'use client'; + +import { useCallback, useEffect, useState } from 'react'; +import '@git-diff-view/react/styles/diff-view.css'; +import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from '@/components/ui/resizable'; +import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; +import { TooltipProvider } from '@/components/ui/tooltip'; +import { Textarea } from '@/components/ui/textarea'; +import { getLocalStorage, setLocalStorage } from '@/lib/local-storage'; +import { TaskChatPanel } from '@/components/task/task-chat-panel'; +import { TaskTopBar } from '@/components/task/task-top-bar'; +import { TaskFilesPanel } from '@/components/task/task-files-panel'; +import { TaskChangesPanel } from '@/components/task/task-changes-panel'; +import { TaskRightPanel } from '@/components/task/task-right-panel'; +import { useAppStoreApi } from '@/components/state-provider'; +import { useWebSocket } from '@/lib/ws/use-websocket'; + +type ChatMessage = { + id: string; + role: 'user' | 'agent'; + content: string; +}; + +type ChatSession = { + id: string; + title: string; + messages: ChatMessage[]; +}; + +const AGENTS = [ + { id: 'codex', label: 'Codex' }, + { id: 'claude', label: 'Claude Code' }, +]; + +const INITIAL_CHATS: ChatSession[] = [ + { + id: 'chat-1', + title: 'Build overview', + messages: [ + { + id: 'm1', + role: 'agent', + content: 'I can help with this task. What should I tackle first?', + }, + { + id: 'm2', + role: 'user', + content: 'Please review the changes and summarize the key diffs.', + }, + ], + }, +]; + + +export default function TaskPage() { + const store = useAppStoreApi(); + useWebSocket(store, 'ws://localhost:8080/ws'); + const defaultHorizontalLayout: [number, number] = [75, 25]; + const [horizontalLayout, setHorizontalLayout] = useState(defaultHorizontalLayout); + const [horizontalSeed, setHorizontalSeed] = useState(0); + const [chats, setChats] = useState(INITIAL_CHATS); + const [leftTab, setLeftTab] = useState('chat'); + const [selectedDiffPath, setSelectedDiffPath] = useState(null); + const [notes, setNotes] = useState(''); + + const activeChat = chats[0]; + + useEffect(() => { + const storedHorizontal = getLocalStorage('task-layout-horizontal', defaultHorizontalLayout); + + if ( + storedHorizontal[0] !== defaultHorizontalLayout[0] || + storedHorizontal[1] !== defaultHorizontalLayout[1] + ) { + setHorizontalLayout(storedHorizontal); + setHorizontalSeed((value) => value + 1); + } + }, []); + + const handleSendMessage = useCallback((content: string) => { + setChats((currentChats) => { + const [first, ...rest] = currentChats; + if (!first) return currentChats; + return [ + { + ...first, + messages: [...first.messages, { id: crypto.randomUUID(), role: 'user', content }], + }, + ...rest, + ]; + }); + }, []); + + const handleSelectDiffPath = useCallback((path: string) => { + setSelectedDiffPath(path); + setLeftTab('changes'); + }, []); + + const topFilesPanel = ; + + return ( + +
+ + +
+ { + setHorizontalLayout(sizes as [number, number]); + setLocalStorage('task-layout-horizontal', sizes); + }} + > + +
+ setLeftTab(value)} + className="flex-1 min-h-0" + > + + + Notes + + + All changes + + + Chat {activeChat ? `• ${activeChat.title}` : ''} + + + + +