♻️ feat: implement session management for PTY sessions in the server

- Add SessionManager class to handle PTY sessions with WebSocket connections.
- Implement methods for creating, retrieving, and destroying sessions.
- Handle PTY output and WebSocket messages for terminal interaction.
- Ensure graceful session destruction and cleanup.

feat: initialize web application with Next.js and Tailwind CSS

- Create initial Next.js application structure with TypeScript support.
- Set up Tailwind CSS for styling with custom theme configurations.
- Add ESLint configuration for code quality and consistency.

feat: implement chat API and UI components

- Create chat API route to handle chat requests and responses.
- Develop chat layout with sidebar, header, chat window, and input components.
- Integrate Zustand for state management of conversations and messages.
- Add utility functions for formatting dates and managing class names.

chore: add environment variables and configuration files

- Create .env.example for environment variable setup.
- Add configuration files for PostCSS, Tailwind CSS, and TypeScript.
- Set up package.json with necessary dependencies and scripts for development.
This commit is contained in:
nirholas
2026-03-31 12:35:31 +00:00
parent d31c2bec03
commit 38648ae5f4
53 changed files with 4177 additions and 4 deletions

View File

@@ -0,0 +1,56 @@
"use client";
import { Sun, Moon, Monitor } from "lucide-react";
import { useTheme } from "./ThemeProvider";
import { useChatStore } from "@/lib/store";
import { MODELS } from "@/lib/constants";
import { cn } from "@/lib/utils";
export function Header() {
const { theme, setTheme } = useTheme();
const { settings, updateSettings } = useChatStore();
const themeIcons = {
light: Sun,
dark: Moon,
system: Monitor,
} as const;
const ThemeIcon = themeIcons[theme];
const nextTheme = theme === "dark" ? "light" : theme === "light" ? "system" : "dark";
return (
<header className="flex items-center justify-between px-4 py-2.5 border-b border-surface-800 bg-surface-900/50 backdrop-blur-sm">
<div className="flex items-center gap-3">
<h1 className="text-sm font-medium text-surface-100">Chat</h1>
</div>
<div className="flex items-center gap-2">
{/* Model selector */}
<select
value={settings.model}
onChange={(e) => updateSettings({ model: e.target.value })}
className={cn(
"text-xs bg-surface-800 border border-surface-700 rounded-md px-2 py-1",
"text-surface-300 focus:outline-none focus:ring-1 focus:ring-brand-500"
)}
>
{MODELS.map((m) => (
<option key={m.id} value={m.id}>
{m.label}
</option>
))}
</select>
{/* Theme toggle */}
<button
onClick={() => setTheme(nextTheme)}
className="p-1.5 rounded-md text-surface-400 hover:text-surface-100 hover:bg-surface-800 transition-colors"
title={`Switch to ${nextTheme} theme`}
>
<ThemeIcon className="w-4 h-4" />
</button>
</div>
</header>
);
}

View File

@@ -0,0 +1,93 @@
"use client";
import { useState } from "react";
import { Plus, MessageSquare, Trash2, Settings } from "lucide-react";
import { useChatStore } from "@/lib/store";
import { cn, formatDate, truncate } from "@/lib/utils";
interface SidebarProps {
className?: string;
}
export function Sidebar({ className }: SidebarProps) {
const {
conversations,
activeConversationId,
createConversation,
setActiveConversation,
deleteConversation,
} = useChatStore();
const [hoveredId, setHoveredId] = useState<string | null>(null);
return (
<aside
className={cn(
"flex flex-col h-full bg-surface-900 border-r border-surface-800 w-64",
className
)}
>
{/* Header */}
<div className="flex items-center justify-between px-4 py-3 border-b border-surface-800">
<span className="text-sm font-semibold text-surface-100">Claude Code</span>
<button
onClick={createConversation}
className="p-1.5 rounded-md text-surface-400 hover:text-surface-100 hover:bg-surface-800 transition-colors"
title="New conversation"
>
<Plus className="w-4 h-4" />
</button>
</div>
{/* Conversation list */}
<nav className="flex-1 overflow-y-auto py-2">
{conversations.length === 0 ? (
<div className="px-4 py-8 text-center text-surface-500 text-sm">
No conversations yet
</div>
) : (
conversations.map((conv) => (
<div
key={conv.id}
className={cn(
"group relative flex items-center px-3 py-2 mx-2 rounded-md cursor-pointer",
"hover:bg-surface-800 transition-colors",
activeConversationId === conv.id && "bg-surface-800"
)}
onClick={() => setActiveConversation(conv.id)}
onMouseEnter={() => setHoveredId(conv.id)}
onMouseLeave={() => setHoveredId(null)}
>
<MessageSquare className="w-3.5 h-3.5 text-surface-500 flex-shrink-0 mr-2" />
<div className="flex-1 min-w-0">
<p className="text-sm text-surface-200 truncate">
{truncate(conv.title, 30)}
</p>
<p className="text-xs text-surface-500">{formatDate(conv.updatedAt)}</p>
</div>
{hoveredId === conv.id && (
<button
onClick={(e) => {
e.stopPropagation();
deleteConversation(conv.id);
}}
className="p-1 rounded text-surface-500 hover:text-red-400 hover:bg-surface-700 transition-colors"
>
<Trash2 className="w-3.5 h-3.5" />
</button>
)}
</div>
))
)}
</nav>
{/* Footer */}
<div className="px-4 py-3 border-t border-surface-800">
<button className="flex items-center gap-2 text-sm text-surface-400 hover:text-surface-100 transition-colors w-full">
<Settings className="w-4 h-4" />
<span>Settings</span>
</button>
</div>
</aside>
);
}

View File

@@ -0,0 +1,56 @@
"use client";
import { createContext, useContext, useEffect, useState } from "react";
import { useChatStore } from "@/lib/store";
type Theme = "light" | "dark" | "system";
interface ThemeContextValue {
theme: Theme;
setTheme: (theme: Theme) => void;
resolvedTheme: "light" | "dark";
}
const ThemeContext = createContext<ThemeContextValue>({
theme: "dark",
setTheme: () => {},
resolvedTheme: "dark",
});
export function ThemeProvider({ children }: { children: React.ReactNode }) {
const { settings, updateSettings } = useChatStore();
const [resolvedTheme, setResolvedTheme] = useState<"light" | "dark">("dark");
useEffect(() => {
const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
const resolve = () => {
if (settings.theme === "system") {
return mediaQuery.matches ? "dark" : "light";
}
return settings.theme;
};
const apply = () => {
const resolved = resolve();
setResolvedTheme(resolved);
document.documentElement.classList.toggle("dark", resolved === "dark");
};
apply();
mediaQuery.addEventListener("change", apply);
return () => mediaQuery.removeEventListener("change", apply);
}, [settings.theme]);
const setTheme = (theme: Theme) => {
updateSettings({ theme });
};
return (
<ThemeContext.Provider value={{ theme: settings.theme, setTheme, resolvedTheme }}>
{children}
</ThemeContext.Provider>
);
}
export const useTheme = () => useContext(ThemeContext);