feat: landing page and pnpm workspaces

This commit is contained in:
Carlos Florêncio
2026-01-12 20:56:03 +00:00
parent c2d08271c9
commit 0cbbeece07
105 changed files with 2138 additions and 13416 deletions
+10
View File
@@ -71,3 +71,13 @@ apps/backend/bin/
# Go binaries (built locally)
apps/backend/kandev
apps/backend/agentctl
# Next.js
.next/
out/
node_modules/
pnpm-lock.yaml
# TypeScript
*.tsbuildinfo
apps/landing/next-env.d.ts
+31 -15
View File
@@ -4,9 +4,11 @@
# Directories
BACKEND_DIR := apps/backend
WEB_DIR := apps/web
LANDING_DIR := apps/landing
APPS_DIR := apps
# Tools
NPM := npm
PNPM := pnpm
MAKE := make
# Default target
@@ -23,17 +25,19 @@ help:
@echo "Development Commands:"
@echo " dev-backend Run backend in development mode (port 8080)"
@echo " dev-web Run web app in development mode (port 3000)"
@echo " dev-landing Run landing page in development mode (port 3001)"
@echo " dev Note: Run dev-backend and dev-web in separate terminals"
@echo ""
@echo "Build Commands:"
@echo " build Build both backend and web app"
@echo " build Build backend, web app, and landing page"
@echo " build-backend Build backend binary"
@echo " build-web Build web app for production"
@echo " build-landing Build landing page for production"
@echo ""
@echo "Installation:"
@echo " install Install all dependencies (backend + web)"
@echo " install Install all dependencies (backend + web + landing)"
@echo " install-backend Install backend dependencies"
@echo " install-web Install web dependencies"
@echo " install-web Install web dependencies (uses pnpm workspace)"
@echo ""
@echo "Testing:"
@echo " test Run all tests (backend + web)"
@@ -71,7 +75,7 @@ dev:
@echo "Backend: http://localhost:8080"
@echo "Web app: http://localhost:3000"
@echo ""
@$(MAKE) -C $(BACKEND_DIR) run & cd $(WEB_DIR) && $(NPM) run dev
@$(MAKE) -C $(BACKEND_DIR) run & cd $(APPS_DIR) && $(PNPM) --filter @kandev/web dev
.PHONY: dev-backend
dev-backend:
@@ -81,18 +85,24 @@ dev-backend:
.PHONY: dev-web
dev-web:
@echo "Starting web app on http://localhost:3000"
@cd $(WEB_DIR) && $(NPM) run dev
@cd $(APPS_DIR) && $(PNPM) --filter @kandev/web dev
.PHONY: dev-landing
dev-landing:
@echo "Starting landing page on http://localhost:3001"
@cd $(APPS_DIR) && $(PNPM) --filter @kandev/landing dev
#
# Build
#
.PHONY: build
build: build-backend build-web
build: build-backend build-web build-landing
@echo ""
@echo "✓ Build complete!"
@echo " Backend binary: $(BACKEND_DIR)/bin/kandev"
@echo " Web app: $(WEB_DIR)/.next"
@echo " Landing page: $(LANDING_DIR)/.next"
.PHONY: build-backend
build-backend:
@@ -102,7 +112,12 @@ build-backend:
.PHONY: build-web
build-web:
@echo "Building web app..."
@cd $(WEB_DIR) && $(NPM) run build
@cd $(APPS_DIR) && $(PNPM) --filter @kandev/web build
.PHONY: build-landing
build-landing:
@echo "Building landing page..."
@cd $(APPS_DIR) && $(PNPM) --filter @kandev/landing build
#
# Installation
@@ -120,8 +135,8 @@ install-backend:
.PHONY: install-web
install-web:
@echo "Installing web dependencies..."
@cd $(WEB_DIR) && $(NPM) install
@echo "Installing web and landing dependencies..."
@cd $(APPS_DIR) && $(PNPM) install
#
# Testing
@@ -140,7 +155,7 @@ test-backend:
.PHONY: test-web
test-web:
@echo "Running web app tests..."
@cd $(WEB_DIR) && $(NPM) test
@cd $(APPS_DIR) && $(PNPM) --filter @kandev/web test
#
# Code Quality
@@ -159,7 +174,7 @@ lint-backend:
.PHONY: lint-web
lint-web:
@echo "Linting web app..."
@cd $(WEB_DIR) && $(NPM) run lint
@cd $(APPS_DIR) && $(PNPM) --filter @kandev/web lint
.PHONY: fmt
fmt: fmt-backend fmt-web
@@ -174,7 +189,7 @@ fmt-backend:
.PHONY: fmt-web
fmt-web:
@echo "Formatting web code..."
@cd $(WEB_DIR) && $(NPM) run lint -- --fix || true
@cd $(APPS_DIR) && $(PNPM) --filter @kandev/web lint -- --fix || true
#
# Cleanup
@@ -192,8 +207,9 @@ clean-backend:
.PHONY: clean-web
clean-web:
@echo "Cleaning web artifacts..."
@rm -rf $(WEB_DIR)/.next $(WEB_DIR)/node_modules
@echo "Cleaning web and landing artifacts..."
@rm -rf $(WEB_DIR)/.next $(LANDING_DIR)/.next $(APPS_DIR)/node_modules
@rm -rf $(APPS_DIR)/packages/*/node_modules
.PHONY: clean-db
clean-db:
+105
View File
@@ -0,0 +1,105 @@
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@kandev/ui/card';
import { IconTarget, IconRocket, IconHeartHandshake } from '@tabler/icons-react';
const values = [
{
icon: IconTarget,
title: 'Developer First',
description: 'Built by developers, for developers. We understand your workflow because we live it every day.',
},
{
icon: IconRocket,
title: 'Ship Fast, Ship Safe',
description: 'Speed without sacrificing security. AI-powered automation with human oversight and approval.',
},
{
icon: IconHeartHandshake,
title: 'Open & Transparent',
description: 'Open source at heart. We believe in building in public and learning from the community.',
},
];
const timeline = [
{
year: '2026',
title: 'The Beginning',
description: 'kandev.ai is born from the need to combine visual task management with AI-assisted development.',
},
];
export default function AboutPage() {
return (
<div className="container mx-auto py-24 max-w-6xl px-4">
<div className="flex flex-col gap-4 mb-12">
<h1 className="text-4xl font-bold">About kandev.ai</h1>
<p className="text-lg text-muted-foreground">
We're building the future of development workflows.
</p>
</div>
<div className="prose prose-slate dark:prose-invert max-w-none mb-12">
<h2>Our Mission</h2>
<p>
kandev.ai exists to make software development more visual, more collaborative, and more efficient.
We believe that combining Kanban methodology with AI-assisted coding creates a workflow that's
greater than the sum of its parts.
</p>
<p>
Traditional development tools force you to context switch between project management, coding,
and deployment. We're building a unified experience where your Kanban board is the control center
for AI agents working in isolated, secure environments.
</p>
</div>
<div className="mb-12">
<h2 className="text-2xl font-bold mb-6">Our Values</h2>
<div className="grid gap-6 md:grid-cols-3">
{values.map((value) => {
const Icon = value.icon;
return (
<Card key={value.title}>
<CardHeader>
<Icon className="h-8 w-8 text-primary mb-2" />
<CardTitle>{value.title}</CardTitle>
</CardHeader>
<CardContent>
<CardDescription>{value.description}</CardDescription>
</CardContent>
</Card>
);
})}
</div>
</div>
<div className="mb-12">
<h2 className="text-2xl font-bold mb-6">Our Journey</h2>
<div className="space-y-6">
{timeline.map((item) => (
<Card key={item.year}>
<CardHeader>
<div className="text-sm text-muted-foreground mb-1">{item.year}</div>
<CardTitle>{item.title}</CardTitle>
</CardHeader>
<CardContent>
<p className="text-muted-foreground">{item.description}</p>
</CardContent>
</Card>
))}
</div>
</div>
<div className="prose prose-slate dark:prose-invert max-w-none">
<h2>Why Kanban + Development + AI?</h2>
<p>
Kanban boards provide visual clarity. Development tools provide execution. AI provides automation.
Together, they create a workflow where you can see all your work, execute it in parallel, and
automate the routine parts while maintaining full control.
</p>
<p>
We're just getting started. As we grow, we'll continue to focus on making development more
intuitive, more powerful, and more enjoyable.
</p>
</div>
</div>
);
}
+50
View File
@@ -0,0 +1,50 @@
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@kandev/ui/card';
import Link from 'next/link';
const blogPosts = [
{
title: 'Introducing kandev.ai: AI-Powered Kanban for Developers',
description: 'Learn how kandev.ai combines Kanban boards with AI agents, isolated worktrees, and Docker containers to accelerate your development workflow.',
date: 'January 10, 2026',
slug: 'introducing-kandev',
author: 'Kandev Team',
},
{
title: 'Running Multiple AI Agents in Parallel: Best Practices',
description: 'Discover how to effectively manage multiple AI coding agents working simultaneously on different tasks using isolated environments and approval workflows.',
date: 'January 5, 2026',
slug: 'parallel-ai-agents',
author: 'Kandev Team',
},
];
export default function BlogPage() {
return (
<div className="container mx-auto max-w-6xl py-24 px-4">
<h1 className="text-4xl font-bold mb-4">Blog</h1>
<p className="text-lg text-muted-foreground mb-12">
Insights and updates from the kandev.ai team.
</p>
<div className="grid gap-6 md:grid-cols-2">
{blogPosts.map((post) => (
<Link key={post.slug} href={`/blog/${post.slug}`}>
<Card className="h-full transition-transform hover:scale-[1.02] cursor-pointer">
<CardHeader>
<div className="flex items-center gap-2 text-sm text-muted-foreground mb-2">
<span>{post.date}</span>
<span>•</span>
<span>{post.author}</span>
</div>
<CardTitle className="text-2xl">{post.title}</CardTitle>
</CardHeader>
<CardContent>
<CardDescription className="text-base">{post.description}</CardDescription>
</CardContent>
</Card>
</Link>
))}
</div>
</div>
);
}
+10
View File
@@ -0,0 +1,10 @@
import { DocsSidebar } from '@/components/docs-sidebar';
export default function DocsLayout({ children }: { children: React.ReactNode }) {
return (
<div className="flex min-h-screen pt-16 justify-center">
<DocsSidebar />
<main className="flex-1 max-w-4xl py-8 px-6">{children}</main>
</div>
);
}
+149
View File
@@ -0,0 +1,149 @@
export default function DocsPage() {
return (
<div className="prose prose-slate dark:prose-invert max-w-none">
<div className="not-prose mb-8">
<h1 className="text-4xl font-bold mb-4">Getting Started with kandev.ai</h1>
<p className="text-xl text-muted-foreground">
kandev.ai combines Kanban task management with AI-powered development workflows. Manage tasks visually,
execute them with AI agents in isolated environments, and ship faster with confidence.
</p>
</div>
<div className="not-prose mb-8 p-6 bg-muted/50 rounded-lg border border-border">
<h2 className="text-2xl font-bold mb-3">What is kandev.ai?</h2>
<p className="text-muted-foreground">
kandev.ai is a development tool that bridges project management and AI-assisted coding. Each task on your
Kanban board can be executed by AI agents working in isolated git worktrees and Docker containers.
</p>
</div>
<h2 className="text-2xl font-bold mt-8 mb-4">Key Features</h2>
<div className="grid gap-4 not-prose mb-8">
<div className="p-4 border border-border rounded-lg">
<h3 className="font-semibold mb-2">Visual Task Management</h3>
<p className="text-sm text-muted-foreground">Organize development work with Kanban boards</p>
</div>
<div className="p-4 border border-border rounded-lg">
<h3 className="font-semibold mb-2">Isolated Worktrees</h3>
<p className="text-sm text-muted-foreground">Each task gets its own git worktree for parallel development</p>
</div>
<div className="p-4 border border-border rounded-lg">
<h3 className="font-semibold mb-2">Local Docker Execution</h3>
<p className="text-sm text-muted-foreground">AI agents run in secure Docker containers on your machine</p>
</div>
<div className="p-4 border border-border rounded-lg">
<h3 className="font-semibold mb-2">Multiple AI Agents</h3>
<p className="text-sm text-muted-foreground">Support for Claude Code, OpenAI Codex, Augment, and more</p>
</div>
<div className="p-4 border border-border rounded-lg">
<h3 className="font-semibold mb-2">Approval Workflows</h3>
<p className="text-sm text-muted-foreground">Review and approve agent changes before merging</p>
</div>
<div className="p-4 border border-border rounded-lg">
<h3 className="font-semibold mb-2">Multiple Workspaces</h3>
<p className="text-sm text-muted-foreground">Organize projects by client, team, or product</p>
</div>
</div>
<h2 className="text-2xl font-bold mt-8 mb-4">How It Works</h2>
<div className="not-prose space-y-4 mb-8">
<div className="flex gap-4">
<div className="flex-shrink-0 w-8 h-8 rounded-full bg-primary text-primary-foreground flex items-center justify-center font-bold">1</div>
<div>
<h3 className="font-semibold mb-1">Create a Task</h3>
<p className="text-sm text-muted-foreground">Add a task to your Kanban board with a description of what needs to be done</p>
</div>
</div>
<div className="flex gap-4">
<div className="flex-shrink-0 w-8 h-8 rounded-full bg-primary text-primary-foreground flex items-center justify-center font-bold">2</div>
<div>
<h3 className="font-semibold mb-1">Configure Repository</h3>
<p className="text-sm text-muted-foreground">Select a repository and base branch for the task</p>
</div>
</div>
<div className="flex gap-4">
<div className="flex-shrink-0 w-8 h-8 rounded-full bg-primary text-primary-foreground flex items-center justify-center font-bold">3</div>
<div>
<h3 className="font-semibold mb-1">Choose AI Agent</h3>
<p className="text-sm text-muted-foreground">Select which AI agent should work on the task</p>
</div>
</div>
<div className="flex gap-4">
<div className="flex-shrink-0 w-8 h-8 rounded-full bg-primary text-primary-foreground flex items-center justify-center font-bold">4</div>
<div>
<h3 className="font-semibold mb-1">Isolated Environment</h3>
<p className="text-sm text-muted-foreground">kandev creates an isolated git worktree and Docker container</p>
</div>
</div>
<div className="flex gap-4">
<div className="flex-shrink-0 w-8 h-8 rounded-full bg-primary text-primary-foreground flex items-center justify-center font-bold">5</div>
<div>
<h3 className="font-semibold mb-1">AI Execution</h3>
<p className="text-sm text-muted-foreground">The AI agent executes the task in the secure environment</p>
</div>
</div>
<div className="flex gap-4">
<div className="flex-shrink-0 w-8 h-8 rounded-full bg-primary text-primary-foreground flex items-center justify-center font-bold">6</div>
<div>
<h3 className="font-semibold mb-1">Review & Approve</h3>
<p className="text-sm text-muted-foreground">Review the changes and approve them for merging</p>
</div>
</div>
</div>
<div className="not-prose mb-8 p-6 bg-primary/10 rounded-lg border border-primary/20">
<h2 className="text-2xl font-bold mb-3">Why kandev.ai?</h2>
<p className="text-muted-foreground mb-4">
Traditional development workflows require constant context switching. kandev.ai eliminates this by:
</p>
<ul className="space-y-2">
<li className="flex items-start gap-2">
<span className="text-primary">✓</span>
<span className="text-sm">Visualizing all work in progress on a single board</span>
</li>
<li className="flex items-start gap-2">
<span className="text-primary">✓</span>
<span className="text-sm">Enabling parallel development without branch conflicts</span>
</li>
<li className="flex items-start gap-2">
<span className="text-primary">✓</span>
<span className="text-sm">Automating routine tasks with AI agents</span>
</li>
<li className="flex items-start gap-2">
<span className="text-primary">✓</span>
<span className="text-sm">Maintaining security through isolated execution environments</span>
</li>
</ul>
</div>
<div className="not-prose mb-8 p-6 bg-accent/10 rounded-lg border border-accent/20">
<h2 className="text-2xl font-bold mb-3">Security & Control</h2>
<p className="text-muted-foreground">
Every operation performed by AI agents is logged and requires approval. Agents run in isolated Docker
containers with restricted permissions, ensuring your codebase and system remain secure.
</p>
</div>
<div className="not-prose mt-12 p-8 bg-gradient-to-br from-primary/10 to-accent/10 rounded-lg border border-border text-center">
<h2 className="text-2xl font-bold mb-4">Ready to Get Started?</h2>
<p className="text-muted-foreground mb-6">
Check out our guides to install and configure kandev.ai
</p>
<div className="flex gap-4 justify-center">
<a
href="/docs/installation"
className="inline-flex items-center justify-center px-6 py-3 rounded-md bg-primary text-primary-foreground font-medium hover:bg-primary/90 transition-colors"
>
Installation Guide
</a>
<a
href="/docs/quick-start"
className="inline-flex items-center justify-center px-6 py-3 rounded-md border border-border bg-background hover:bg-muted font-medium transition-colors"
>
Quick Start Tutorial
</a>
</div>
</div>
</div>
);
}
+62
View File
@@ -0,0 +1,62 @@
'use client';
export default function GlobalError({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
return (
<html lang="en">
<head>
<title>Error - kandev.ai</title>
</head>
<body style={{
margin: 0,
padding: 0,
fontFamily: 'system-ui, sans-serif',
display: 'flex',
minHeight: '100vh',
alignItems: 'center',
justifyContent: 'center',
backgroundColor: '#ffffff'
}}>
<div style={{
textAlign: 'center',
padding: '2rem'
}}>
<h1 style={{
fontSize: '2rem',
fontWeight: 'bold',
marginBottom: '1rem',
color: '#000000'
}}>
Something went wrong!
</h1>
<p style={{
marginBottom: '2rem',
color: '#666666'
}}>
We're sorry, but something unexpected happened.
</p>
<button
onClick={() => reset()}
style={{
padding: '0.75rem 1.5rem',
backgroundColor: '#6B46C1',
color: '#ffffff',
border: 'none',
borderRadius: '0.5rem',
fontSize: '1rem',
fontWeight: '500',
cursor: 'pointer'
}}
>
Try again
</button>
</div>
</body>
</html>
);
}
+155
View File
@@ -0,0 +1,155 @@
@import "tailwindcss";
@import "tw-animate-css";
@import "shadcn/tailwind.css";
@import "@kandev/theme/fonts.css";
@source "../components/**/*.{js,ts,jsx,tsx}";
@source "../../packages/ui/src/**/*.{js,ts,jsx,tsx}";
@custom-variant dark (&:is(.dark *));
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--font-sans: var(--font-sans);
--font-mono: var(--font-geist-mono);
--color-sidebar-ring: var(--sidebar-ring);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar: var(--sidebar);
--color-chart-5: var(--chart-5);
--color-chart-4: var(--chart-4);
--color-chart-3: var(--chart-3);
--color-chart-2: var(--chart-2);
--color-chart-1: var(--chart-1);
--color-ring: var(--ring);
--color-input: var(--input);
--color-border: var(--border);
--color-destructive: var(--destructive);
--color-accent-foreground: var(--accent-foreground);
--color-accent: var(--accent);
--color-muted-foreground: var(--muted-foreground);
--color-muted: var(--muted);
--color-secondary-foreground: var(--secondary-foreground);
--color-secondary: var(--secondary);
--color-primary-foreground: var(--primary-foreground);
--color-primary: var(--primary);
--color-popover-foreground: var(--popover-foreground);
--color-popover: var(--popover);
--color-card-foreground: var(--card-foreground);
--color-card: var(--card);
--radius-sm: var(--radius);
--radius-md: var(--radius);
--radius-lg: var(--radius);
--radius-xl: var(--radius);
--radius-2xl: var(--radius);
--radius-3xl: var(--radius);
--radius-4xl: var(--radius);
}
:root {
--font-geist-sans: "Geist", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
--font-sans: "Figtree", "Geist", ui-sans-serif, system-ui, -apple-system, "Segoe UI",
sans-serif;
--font-geist-mono: "Geist Mono", ui-monospace, "SFMono-Regular", Menlo, Monaco, Consolas,
"Liberation Mono", "Courier New", monospace;
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
--card: oklch(1 0 0);
--card-foreground: oklch(0.145 0 0);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.145 0 0);
--primary: oklch(0.51 0.23 277);
--primary-foreground: oklch(0.96 0.02 272);
--secondary: oklch(0.967 0.001 286.375);
--secondary-foreground: oklch(0.21 0.006 285.885);
--muted: oklch(0.97 0 0);
--muted-foreground: oklch(0.556 0 0);
--accent: oklch(0.68 0.18 276);
--accent-foreground: oklch(0.98 0 0);
--destructive: oklch(0.58 0.22 27);
--border: oklch(0.922 0 0);
--input: oklch(0.922 0 0);
--ring: oklch(0.708 0 0);
--chart-1: oklch(0.79 0.10 275);
--chart-2: oklch(0.68 0.16 277);
--chart-3: oklch(0.59 0.20 277);
--chart-4: oklch(0.51 0.23 277);
--chart-5: oklch(0.46 0.21 277);
--radius: 0.375rem;
--sidebar: oklch(0.985 0 0);
--sidebar-foreground: oklch(0.145 0 0);
--sidebar-primary: oklch(0.51 0.23 277);
--sidebar-primary-foreground: oklch(0.96 0.02 272);
--sidebar-accent: oklch(0.97 0 0);
--sidebar-accent-foreground: oklch(0.205 0 0);
--sidebar-border: oklch(0.922 0 0);
--sidebar-ring: oklch(0.708 0 0);
}
.dark {
--background: oklch(0.145 0 0);
--foreground: oklch(0.985 0 0);
--card: oklch(0.205 0 0);
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.205 0 0);
--popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.59 0.20 277);
--primary-foreground: oklch(0.96 0.02 272);
--secondary: oklch(0.274 0.006 286.033);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.269 0 0);
--muted-foreground: oklch(0.708 0 0);
--accent: oklch(0.62 0.18 276);
--accent-foreground: oklch(0.98 0 0);
--destructive: oklch(0.704 0.191 22.216);
--border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%);
--ring: oklch(0.556 0 0);
--chart-1: oklch(0.79 0.10 275);
--chart-2: oklch(0.68 0.16 277);
--chart-3: oklch(0.59 0.20 277);
--chart-4: oklch(0.51 0.23 277);
--chart-5: oklch(0.46 0.21 277);
--sidebar: oklch(0.205 0 0);
--sidebar-foreground: oklch(0.985 0 0);
--sidebar-primary: oklch(0.68 0.16 277);
--sidebar-primary-foreground: oklch(0.96 0.02 272);
--sidebar-accent: oklch(0.269 0 0);
--sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(1 0 0 / 10%);
--sidebar-ring: oklch(0.556 0 0);
}
@layer base {
* {
@apply border-border outline-ring/50;
}
html {
scrollbar-gutter: stable;
}
body {
@apply bg-background text-foreground;
}
}
@layer utilities {
.animate-pulse-slow {
animation: pulse-slow 8s ease-in-out infinite;
}
}
@keyframes pulse-slow {
0%, 100% {
transform: scale(1);
opacity: 0.6;
}
50% {
transform: scale(1.1);
opacity: 0.8;
}
}
+32
View File
@@ -0,0 +1,32 @@
import type { Metadata } from 'next';
import './globals.css';
import { ThemeProvider } from '@/components/theme-provider';
import { Navbar } from '@/components/navbar';
import { Footer } from '@/components/footer';
export const dynamic = 'force-dynamic';
export const metadata: Metadata = {
title: 'kandev.ai - AI-powered Kanban for Developers',
description: 'Kanban meets AI development. Manage tasks with isolated worktrees, Docker containers, and AI agents.',
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en" suppressHydrationWarning>
<body className="font-sans antialiased">
<ThemeProvider>
<Navbar />
<main className="pt-14">
{children}
</main>
<Footer />
</ThemeProvider>
</body>
</html>
);
}
+15
View File
@@ -0,0 +1,15 @@
import { HeroSection } from '@/components/hero-section';
import { FeaturesSection } from '@/components/features-section';
import { AgentsSection } from '@/components/agents-section';
import { FutureFeaturesSection } from '@/components/future-features-section';
export default function HomePage() {
return (
<main>
<HeroSection />
<FeaturesSection />
<AgentsSection />
<FutureFeaturesSection />
</main>
);
}
@@ -0,0 +1,54 @@
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@kandev/ui/card';
const agents = [
{
name: 'Claude Code',
description: "Anthropic's official CLI for Claude. Powerful agent with excellent reasoning and code generation.",
icon: '🤖',
},
{
name: 'OpenAI Codex',
description: 'GPT-4 powered coding assistant with strong performance across multiple languages.',
icon: '🔮',
},
{
name: 'Augment',
description: 'AI coding assistant focused on code completion and refactoring workflows.',
icon: '⚡',
},
{
name: 'Opencode',
description: 'Open-source AI coding agent with customizable models and integrations.',
icon: '🌐',
},
];
export function AgentsSection() {
return (
<section className="w-full bg-muted/50 py-16 md:py-24">
<div className="container mx-auto max-w-6xl px-4">
<div className="flex flex-col items-center gap-4 text-center">
<h2 className="text-3xl font-bold tracking-tight sm:text-4xl md:text-5xl">
Supported AI Agents
</h2>
<p className="max-w-2xl text-lg text-muted-foreground">
Work with your favorite AI coding assistants. All agents run in isolated, secure environments.
</p>
</div>
<div className="mt-12 grid gap-6 md:grid-cols-2 lg:grid-cols-4">
{agents.map((agent) => (
<Card key={agent.name} className="transition-transform hover:scale-105 cursor-pointer">
<CardHeader>
<div className="text-4xl mb-2">{agent.icon}</div>
<CardTitle>{agent.name}</CardTitle>
</CardHeader>
<CardContent>
<CardDescription>{agent.description}</CardDescription>
</CardContent>
</Card>
))}
</div>
</div>
</section>
);
}
+86
View File
@@ -0,0 +1,86 @@
'use client';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
import { cn } from '@/lib/utils';
const sections = [
{
title: 'Getting Started',
items: [
{ title: 'Introduction', href: '/docs' },
{ title: 'Installation', href: '/docs/installation' },
{ title: 'Quick Start', href: '/docs/quick-start' },
],
},
{
title: 'Core Concepts',
items: [
{ title: 'Workspaces', href: '/docs/workspaces' },
{ title: 'Boards', href: '/docs/boards' },
{ title: 'Tasks', href: '/docs/tasks' },
],
},
{
title: 'Features',
items: [
{ title: 'Git Worktrees', href: '/docs/worktrees' },
{ title: 'AI Agents', href: '/docs/agents' },
{ title: 'Docker Execution', href: '/docs/docker' },
],
},
{
title: 'Integrations',
items: [
{ title: 'MCP Servers', href: '/docs/mcp' },
{ title: 'GitHub', href: '/docs/github' },
],
},
{
title: 'Security',
items: [
{ title: 'Approval Workflows', href: '/docs/approvals' },
{ title: 'Audit Logs', href: '/docs/audit-logs' },
],
},
{
title: 'Advanced',
items: [
{ title: 'Configuration', href: '/docs/configuration' },
{ title: 'CLI Reference', href: '/docs/cli' },
],
},
];
export function DocsSidebar() {
const pathname = usePathname();
return (
<aside className="sticky top-14 h-[calc(100vh-3.5rem)] w-64 shrink-0 overflow-y-auto border-r border-border py-8 pr-6">
<nav className="space-y-6">
{sections.map((section) => (
<div key={section.title}>
<h4 className="mb-2 font-semibold text-sm">{section.title}</h4>
<ul className="space-y-2">
{section.items.map((item) => (
<li key={item.href}>
<Link
href={item.href}
className={cn(
'block text-sm transition-colors hover:text-foreground',
pathname === item.href
? 'font-medium text-foreground'
: 'text-muted-foreground'
)}
>
{item.title}
</Link>
</li>
))}
</ul>
</div>
))}
</nav>
</aside>
);
}
@@ -0,0 +1,72 @@
import { IconGitBranch, IconCode, IconShieldCheck, IconPlugConnected, IconFolders, IconBrandDocker } from '@tabler/icons-react';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@kandev/ui/card';
const features = [
{
icon: IconCode,
title: 'Parallel Agent Execution',
description:
'Run multiple AI coding agents simultaneously on different tasks. Each agent works in isolated environments, accelerating development without conflicts.',
},
{
icon: IconGitBranch,
title: 'Isolated Worktrees',
description:
'Each task gets its own git worktree. Work on multiple features, bugs, and experiments in parallel without branch switching or stashing.',
},
{
icon: IconShieldCheck,
title: 'Built-in Code Review',
description:
'Automated code review and approval workflows. Review agent changes before merging, with full audit logs and rollback capabilities.',
},
{
icon: IconPlugConnected,
title: 'MCP Integration',
description:
'Connect with Model Context Protocol servers. Extend agent capabilities with custom tools, APIs, and integrations.',
},
{
icon: IconFolders,
title: 'Multiple Workspaces',
description:
'Organize projects by client, team, or product. Switch between workspaces seamlessly and keep your work isolated.',
},
{
icon: IconBrandDocker,
title: 'Local Docker',
description:
'AI agents run in local Docker containers for complete isolation. Secure execution with approval workflows and audit logs for every operation.',
},
];
export function FeaturesSection() {
return (
<section id="features" className="container mx-auto max-w-6xl py-16 md:py-24 px-4">
<div className="flex flex-col items-center gap-4 text-center">
<h2 className="text-3xl font-bold tracking-tight sm:text-4xl md:text-5xl">
Build for development velocity
</h2>
<p className="max-w-2xl text-lg text-muted-foreground">
Combine the visual clarity of Kanban boards with the power of AI-assisted development.
</p>
</div>
<div className="mt-12 grid gap-6 md:grid-cols-2 lg:grid-cols-3">
{features.map((feature) => {
const Icon = feature.icon;
return (
<Card key={feature.title}>
<CardHeader>
<Icon className="h-8 w-8 text-primary mb-2" />
<CardTitle>{feature.title}</CardTitle>
</CardHeader>
<CardContent>
<CardDescription>{feature.description}</CardDescription>
</CardContent>
</Card>
);
})}
</div>
</section>
);
}
+80
View File
@@ -0,0 +1,80 @@
import Link from 'next/link';
import { IconBrandGithub, IconBrandTwitter, IconBrandDiscord } from '@tabler/icons-react';
export function Footer() {
return (
<footer className="border-t border-border bg-background">
<div className="container mx-auto max-w-6xl py-12 md:py-16 px-4">
<div className="grid gap-8 md:grid-cols-3">
<div>
<h3 className="font-bold mb-4">kandev.ai</h3>
<p className="text-sm text-muted-foreground">
AI-powered Kanban for developers. Ship faster with isolated worktrees and secure Docker execution.
</p>
</div>
<div>
<h4 className="font-semibold mb-4">Product</h4>
<ul className="space-y-2 text-sm">
<li>
<Link href="/#features" className="text-muted-foreground hover:text-foreground">
Features
</Link>
</li>
<li>
<Link href="/docs" className="text-muted-foreground hover:text-foreground">
Documentation
</Link>
</li>
</ul>
</div>
<div>
<h4 className="font-semibold mb-4">Company</h4>
<ul className="space-y-2 text-sm">
<li>
<Link href="/about" className="text-muted-foreground hover:text-foreground">
About
</Link>
</li>
<li>
<Link href="/blog" className="text-muted-foreground hover:text-foreground">
Blog
</Link>
</li>
</ul>
</div>
</div>
<div className="mt-8 border-t border-border pt-8 flex flex-col md:flex-row justify-between items-center gap-4">
<p className="text-sm text-muted-foreground">
© 2026 kandev.ai. All rights reserved. Built with respect for your privacy and GDPR compliance.
</p>
<div className="flex gap-4">
<a
href="https://github.com/kdlbs/kandev"
target="_blank"
rel="noopener noreferrer"
className="text-muted-foreground hover:text-foreground"
>
<IconBrandGithub className="h-5 w-5" />
</a>
<a
href="https://twitter.com/kandev_ai"
target="_blank"
rel="noopener noreferrer"
className="text-muted-foreground hover:text-foreground"
>
<IconBrandTwitter className="h-5 w-5" />
</a>
<a
href="https://discord.gg/kandev"
target="_blank"
rel="noopener noreferrer"
className="text-muted-foreground hover:text-foreground"
>
<IconBrandDiscord className="h-5 w-5" />
</a>
</div>
</div>
</div>
</footer>
);
}
@@ -0,0 +1,60 @@
import { IconCloud, IconDeviceMobile, IconUsers } from '@tabler/icons-react';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@kandev/ui/card';
import { Badge } from '@kandev/ui/badge';
const futureFeatures = [
{
icon: IconCloud,
title: 'Remote Executors',
description: 'Run AI agents on cloud or remote machines. Scale your development with powerful remote compute.',
status: 'Coming Soon',
},
{
icon: IconDeviceMobile,
title: 'Mobile App',
description: 'Launch and monitor tasks from your mobile device. Stay connected to your development workflow.',
status: 'In Development',
},
{
icon: IconUsers,
title: 'Team Collaboration',
description: 'Share workspaces, assign tasks, and collaborate with your team in real-time.',
status: 'Planned',
},
];
export function FutureFeaturesSection() {
return (
<section className="container mx-auto max-w-6xl py-16 md:py-24 px-4">
<div className="flex flex-col items-center gap-4 text-center">
<h2 className="text-3xl font-bold tracking-tight sm:text-4xl md:text-5xl">
Coming Soon
</h2>
<p className="max-w-2xl text-lg text-muted-foreground">
We're constantly improving kandev.ai. Here's what's on our roadmap.
</p>
</div>
<div className="mt-12 grid gap-6 md:grid-cols-3">
{futureFeatures.map((feature) => {
const Icon = feature.icon;
return (
<Card key={feature.title} className="relative">
<CardHeader>
<Icon className="h-8 w-8 text-primary mb-2" />
<div className="flex items-center justify-between">
<CardTitle>{feature.title}</CardTitle>
</div>
<Badge variant="secondary" className="w-fit">
{feature.status}
</Badge>
</CardHeader>
<CardContent>
<CardDescription>{feature.description}</CardDescription>
</CardContent>
</Card>
);
})}
</div>
</section>
);
}
+72
View File
@@ -0,0 +1,72 @@
'use client';
import { useState } from 'react';
import { Button } from '@kandev/ui/button';
import { Badge } from '@kandev/ui/badge';
import { IconCheck, IconCopy } from '@tabler/icons-react';
export function HeroSection() {
const [copied, setCopied] = useState(false);
const copyCommand = () => {
navigator.clipboard.writeText('npx kandev');
setCopied(true);
setTimeout(() => setCopied(false), 2000);
};
return (
<section className="relative overflow-hidden">
{/* Animated gradient cloud */}
<div className="absolute inset-0 -z-10 flex items-center justify-center">
<div
className="absolute w-[800px] h-[800px] rounded-full filter blur-3xl opacity-60 animate-pulse-slow"
style={{
background: 'radial-gradient(circle, rgba(133, 110, 232, 0.3) 0%, rgba(177, 140, 254, 0.15) 40%, transparent 70%)'
}}
/>
</div>
<div className="container mx-auto max-w-6xl flex flex-col items-center gap-8 px-4 pt-32 pb-16 md:pt-40 md:pb-24">
<Badge variant="secondary" className="cursor-pointer border border-border px-6 py-3">
<span className="text-sm">kandev.ai – AI-powered Kanban for developers</span>
</Badge>
<h1 className="max-w-4xl text-center text-4xl font-bold tracking-tight sm:text-5xl md:text-6xl lg:text-7xl">
Kanban meets <span className="text-primary">AI development</span>
</h1>
<p className="max-w-2xl text-center text-lg text-muted-foreground md:text-xl">
Manage development tasks with isolated git worktrees, secure Docker execution, and AI agents.
Ship faster without breaking your workflow.
</p>
<div className="flex flex-col items-center gap-4 sm:flex-row">
<Button size="lg" asChild className="cursor-pointer">
<a href="#features">Get Started</a>
</Button>
<Button size="lg" variant="outline" asChild className="cursor-pointer">
<a href="#demo">Watch Demo</a>
</Button>
</div>
<div className="mt-8 w-full max-w-2xl">
<div className="flex items-center justify-center gap-2 rounded-lg border border-border bg-card p-4">
<code className="flex-1 text-sm font-mono text-foreground">npx kandev</code>
<Button
size="icon"
variant="ghost"
onClick={copyCommand}
className="cursor-pointer"
>
{copied ? (
<IconCheck className="h-4 w-4 text-green-500" />
) : (
<IconCopy className="h-4 w-4" />
)}
</Button>
</div>
</div>
<div className="mt-8 w-full max-w-5xl">
<div className="aspect-video rounded-lg border border-border bg-muted flex items-center justify-center">
<p className="text-muted-foreground">Video placeholder</p>
</div>
</div>
</div>
</section>
);
}
+69
View File
@@ -0,0 +1,69 @@
'use client';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
import { IconBrandGithub } from '@tabler/icons-react';
import { Button } from '@kandev/ui/button';
import { cn } from '@/lib/utils';
export function Navbar() {
const pathname = usePathname();
const stars = '2k'; // Dummy star count display
return (
<header className="fixed top-0 z-50 w-full border-b border-border/40 bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
<div className="container mx-auto flex h-14 max-w-6xl items-center justify-between px-4">
<Link href="/" className="flex items-center space-x-2">
<span className="font-bold">kandev.ai</span>
</Link>
<nav className="flex items-center gap-6 text-sm">
<Link
href="/#product"
className={cn(
'text-sm font-medium transition-colors hover:text-foreground cursor-pointer',
pathname === '/' ? 'text-foreground' : 'text-muted-foreground'
)}
>
Product
</Link>
<Link
href="/docs"
className={cn(
'text-sm font-medium transition-colors hover:text-foreground cursor-pointer',
pathname?.startsWith('/docs') ? 'text-foreground' : 'text-muted-foreground'
)}
>
Documentation
</Link>
<Link
href="/blog"
className={cn(
'text-sm font-medium transition-colors hover:text-foreground cursor-pointer',
pathname?.startsWith('/blog') ? 'text-foreground' : 'text-muted-foreground'
)}
>
Blog
</Link>
</nav>
<div className="flex items-center space-x-2">
<Button variant="outline" size="sm" asChild>
<a
href="https://github.com/kdlbs/kandev"
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-2"
>
<IconBrandGithub className="h-4 w-4" />
<span>Star</span>
<span className="ml-1 rounded-full bg-muted px-2 py-0.5 text-xs font-medium">
{stars}
</span>
</a>
</Button>
</div>
</div>
</header>
);
}
@@ -0,0 +1,12 @@
'use client';
import { ThemeProvider as NextThemesProvider } from 'next-themes';
import type { ReactNode } from 'react';
export function ThemeProvider({ children }: { children: ReactNode }) {
return (
<NextThemesProvider attribute="class" defaultTheme="dark" enableSystem={false}>
{children}
</NextThemesProvider>
);
}
+6
View File
@@ -0,0 +1,6 @@
import { type ClassValue, clsx } from 'clsx';
import { twMerge } from 'tailwind-merge';
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
+6
View File
@@ -0,0 +1,6 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
import "./.next/types/routes.d.ts";
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
+7
View File
@@ -0,0 +1,7 @@
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
transpilePackages: ["@kandev/ui", "@kandev/theme", "@kandev/types"],
};
export default nextConfig;
+34
View File
@@ -0,0 +1,34 @@
{
"name": "@kandev/landing",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev -p 3001",
"build": "NODE_ENV=production next build",
"start": "next start -p 3001",
"lint": "next lint"
},
"dependencies": {
"@kandev/theme": "workspace:*",
"@kandev/ui": "workspace:*",
"@tabler/icons-react": "^3.24.0",
"clsx": "^2.1.1",
"next": "16.1.1",
"next-themes": "^0.4.6",
"react": "19.2.3",
"react-dom": "19.2.3",
"shadcn": "^3.6.3",
"tailwind-merge": "^2.6.0",
"tw-animate-css": "^1.4.0"
},
"devDependencies": {
"@tailwindcss/postcss": "^4.0.0",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"eslint": "^9",
"eslint-config-next": "16.1.1",
"tailwindcss": "^4.0.0",
"typescript": "^5"
}
}
+7
View File
@@ -0,0 +1,7 @@
const config = {
plugins: {
"@tailwindcss/postcss": {},
},
};
export default config;
+42
View File
@@ -0,0 +1,42 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": ["./*"],
"@kandev/ui": ["../packages/ui/src"],
"@kandev/ui/*": ["../packages/ui/src/*"],
"@kandev/theme": ["../packages/theme/src"],
"@kandev/types": ["../packages/types/src"]
}
},
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts"
],
"exclude": [
"node_modules"
]
}
+19
View File
@@ -0,0 +1,19 @@
{
"name": "kandev-monorepo",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "pnpm -r run dev",
"dev:web": "pnpm --filter @kandev/web dev",
"dev:landing": "pnpm --filter @kandev/landing dev",
"build": "pnpm -r run build",
"build:web": "pnpm --filter @kandev/web build",
"build:landing": "pnpm --filter @kandev/landing build",
"lint": "pnpm -r run lint",
"clean": "pnpm -r run clean && rm -rf node_modules"
},
"devDependencies": {
"@types/node": "^20",
"typescript": "^5"
}
}
+20
View File
@@ -0,0 +1,20 @@
{
"name": "@kandev/theme",
"version": "0.1.0",
"private": true,
"main": "./src/index.ts",
"types": "./src/index.ts",
"exports": {
"./globals.css": "./src/globals.css",
"./fonts.css": "./src/fonts.css",
"./postcss.config.mjs": "./src/postcss.config.mjs"
},
"dependencies": {
"shadcn": "^3.6.3",
"tw-animate-css": "^1.4.0"
},
"devDependencies": {
"@tailwindcss/postcss": "^4.0.0",
"tailwindcss": "^4.0.0"
}
}
+71
View File
@@ -0,0 +1,71 @@
@font-face {
font-family: 'Figtree';
font-style: normal;
font-weight: 300 900;
font-display: swap;
src: url('/fonts/figtree/figtree-300-normal.woff2') format('woff2');
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
font-family: 'Figtree';
font-style: normal;
font-weight: 300 900;
font-display: swap;
src: url('/fonts/figtree/figtree-300-normal.woff2') format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
font-family: 'Geist';
font-style: normal;
font-weight: 100 900;
font-display: swap;
src: url('/fonts/geist/geist-100-normal.woff2') format('woff2');
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
@font-face {
font-family: 'Geist';
font-style: normal;
font-weight: 100 900;
font-display: swap;
src: url('/fonts/geist/geist-100-normal.woff2') format('woff2');
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
font-family: 'Geist';
font-style: normal;
font-weight: 100 900;
font-display: swap;
src: url('/fonts/geist/geist-100-normal.woff2') format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
font-family: 'Geist Mono';
font-style: normal;
font-weight: 100 900;
font-display: swap;
src: url('/fonts/geist-mono/geist-mono-100-normal.woff2') format('woff2');
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
@font-face {
font-family: 'Geist Mono';
font-style: normal;
font-weight: 100 900;
font-display: swap;
src: url('/fonts/geist-mono/geist-mono-100-normal.woff2') format('woff2');
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
font-family: 'Geist Mono';
font-style: normal;
font-weight: 100 900;
font-display: swap;
src: url('/fonts/geist-mono/geist-mono-100-normal.woff2') format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
+147
View File
@@ -0,0 +1,147 @@
@import "tailwindcss";
@import "tw-animate-css";
@import "shadcn/tailwind.css";
@custom-variant dark (&:is(.dark *));
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--font-sans: var(--font-sans);
--font-mono: var(--font-geist-mono);
--color-sidebar-ring: var(--sidebar-ring);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar: var(--sidebar);
--color-chart-5: var(--chart-5);
--color-chart-4: var(--chart-4);
--color-chart-3: var(--chart-3);
--color-chart-2: var(--chart-2);
--color-chart-1: var(--chart-1);
--color-ring: var(--ring);
--color-input: var(--input);
--color-border: var(--border);
--color-destructive: var(--destructive);
--color-accent-foreground: var(--accent-foreground);
--color-accent: var(--accent);
--color-muted-foreground: var(--muted-foreground);
--color-muted: var(--muted);
--color-secondary-foreground: var(--secondary-foreground);
--color-secondary: var(--secondary);
--color-primary-foreground: var(--primary-foreground);
--color-primary: var(--primary);
--color-popover-foreground: var(--popover-foreground);
--color-popover: var(--popover);
--color-card-foreground: var(--card-foreground);
--color-card: var(--card);
--radius-sm: var(--radius);
--radius-md: var(--radius);
--radius-lg: var(--radius);
--radius-xl: var(--radius);
--radius-2xl: var(--radius);
--radius-3xl: var(--radius);
--radius-4xl: var(--radius);
}
:root {
--font-geist-sans: "Geist", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
--font-sans: "Figtree", "Geist", ui-sans-serif, system-ui, -apple-system, "Segoe UI",
sans-serif;
--font-geist-mono: "Geist Mono", ui-monospace, "SFMono-Regular", Menlo, Monaco, Consolas,
"Liberation Mono", "Courier New", monospace;
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
--card: oklch(1 0 0);
--card-foreground: oklch(0.145 0 0);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.145 0 0);
--primary: oklch(0.51 0.23 277);
--primary-foreground: oklch(0.96 0.02 272);
--secondary: oklch(0.967 0.001 286.375);
--secondary-foreground: oklch(0.21 0.006 285.885);
--muted: oklch(0.97 0 0);
--muted-foreground: oklch(0.556 0 0);
--accent: oklch(0.68 0.18 276);
--accent-foreground: oklch(0.98 0 0);
--destructive: oklch(0.58 0.22 27);
--border: oklch(0.922 0 0);
--input: oklch(0.922 0 0);
--ring: oklch(0.708 0 0);
--chart-1: oklch(0.79 0.10 275);
--chart-2: oklch(0.68 0.16 277);
--chart-3: oklch(0.59 0.20 277);
--chart-4: oklch(0.51 0.23 277);
--chart-5: oklch(0.46 0.21 277);
--radius: 0.375rem;
--sidebar: oklch(0.985 0 0);
--sidebar-foreground: oklch(0.145 0 0);
--sidebar-primary: oklch(0.51 0.23 277);
--sidebar-primary-foreground: oklch(0.96 0.02 272);
--sidebar-accent: oklch(0.97 0 0);
--sidebar-accent-foreground: oklch(0.205 0 0);
--sidebar-border: oklch(0.922 0 0);
--sidebar-ring: oklch(0.708 0 0);
}
.dark {
--background: oklch(0.145 0 0);
--foreground: oklch(0.985 0 0);
--card: oklch(0.205 0 0);
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.205 0 0);
--popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.59 0.20 277);
--primary-foreground: oklch(0.96 0.02 272);
--secondary: oklch(0.274 0.006 286.033);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.269 0 0);
--muted-foreground: oklch(0.708 0 0);
--accent: oklch(0.62 0.18 276);
--accent-foreground: oklch(0.98 0 0);
--destructive: oklch(0.704 0.191 22.216);
--border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%);
--ring: oklch(0.556 0 0);
--chart-1: oklch(0.79 0.10 275);
--chart-2: oklch(0.68 0.16 277);
--chart-3: oklch(0.59 0.20 277);
--chart-4: oklch(0.51 0.23 277);
--chart-5: oklch(0.46 0.21 277);
--sidebar: oklch(0.205 0 0);
--sidebar-foreground: oklch(0.985 0 0);
--sidebar-primary: oklch(0.68 0.16 277);
--sidebar-primary-foreground: oklch(0.96 0.02 272);
--sidebar-accent: oklch(0.269 0 0);
--sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(1 0 0 / 10%);
--sidebar-ring: oklch(0.556 0 0);
}
@layer base {
* {
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
}
}
@layer utilities {
.kanban-task-pulse {
animation: kanban-task-pulse 5s ease-in-out infinite;
}
}
@keyframes kanban-task-pulse {
0%,
100% {
box-shadow: 0 0 0 0 rgba(79, 70, 229, 0.12);
}
50% {
box-shadow: 0 0 0 3px rgba(79, 70, 229, 0.22);
}
}
@@ -0,0 +1,7 @@
const config = {
plugins: {
"@tailwindcss/postcss": {},
},
};
export default config;
+12
View File
@@ -0,0 +1,12 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"composite": true,
"declaration": true,
"declarationMap": true,
"outDir": "./dist",
"rootDir": "./src"
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}
+11
View File
@@ -0,0 +1,11 @@
{
"name": "@kandev/types",
"version": "0.1.0",
"private": true,
"main": "./src/index.ts",
"types": "./src/index.ts",
"exports": {
".": "./src/index.ts",
"./*": "./src/*.ts"
}
}
+128
View File
@@ -0,0 +1,128 @@
export type BackendMessageType =
| 'kanban.update'
| 'task.created'
| 'task.updated'
| 'task.deleted'
| 'task.state_changed'
| 'agent.updated'
| 'terminal.output'
| 'diff.update'
| 'system.error'
| 'workspace.created'
| 'workspace.updated'
| 'workspace.deleted'
| 'board.created'
| 'board.updated'
| 'board.deleted'
| 'column.created'
| 'column.updated'
| 'column.deleted';
export type BackendMessage<T extends BackendMessageType, P> = {
id?: string;
type: 'request' | 'response' | 'notification' | 'error';
action: T;
payload: P;
timestamp?: string;
};
export type KanbanUpdatePayload = {
boardId: string;
columns: Array<{ id: string; title: string; color?: string; position?: number }>;
tasks: Array<{
id: string;
columnId: string;
title: string;
position?: number;
description?: string;
state?: string;
}>;
};
export type TaskEventPayload = {
task_id: string;
board_id: string;
column_id: string;
title: string;
description?: string;
state?: string;
priority?: number;
position?: number;
};
export type AgentUpdatePayload = {
agentId: string;
status: 'idle' | 'running' | 'error';
message?: string;
};
export type TerminalOutputPayload = {
terminalId: string;
data: string;
stream?: 'stdout' | 'stderr';
};
export type DiffUpdatePayload = {
taskId: string;
files: Array<{
path: string;
status: 'A' | 'M' | 'D';
plus: number;
minus: number;
}>;
};
export type SystemErrorPayload = {
message: string;
code?: string;
};
export type WorkspacePayload = {
id: string;
name: string;
description?: string;
owner_id?: string;
created_at?: string;
updated_at?: string;
};
export type BoardPayload = {
id: string;
workspace_id: string;
name: string;
description?: string;
created_at?: string;
updated_at?: string;
};
export type ColumnPayload = {
id: string;
board_id: string;
name: string;
position: number;
state: string;
color: string;
created_at?: string;
updated_at?: string;
};
export type BackendMessageMap = {
'kanban.update': BackendMessage<'kanban.update', KanbanUpdatePayload>;
'task.created': BackendMessage<'task.created', TaskEventPayload>;
'task.updated': BackendMessage<'task.updated', TaskEventPayload>;
'task.deleted': BackendMessage<'task.deleted', TaskEventPayload>;
'task.state_changed': BackendMessage<'task.state_changed', TaskEventPayload>;
'agent.updated': BackendMessage<'agent.updated', AgentUpdatePayload>;
'terminal.output': BackendMessage<'terminal.output', TerminalOutputPayload>;
'diff.update': BackendMessage<'diff.update', DiffUpdatePayload>;
'system.error': BackendMessage<'system.error', SystemErrorPayload>;
'workspace.created': BackendMessage<'workspace.created', WorkspacePayload>;
'workspace.updated': BackendMessage<'workspace.updated', WorkspacePayload>;
'workspace.deleted': BackendMessage<'workspace.deleted', WorkspacePayload>;
'board.created': BackendMessage<'board.created', BoardPayload>;
'board.updated': BackendMessage<'board.updated', BoardPayload>;
'board.deleted': BackendMessage<'board.deleted', BoardPayload>;
'column.created': BackendMessage<'column.created', ColumnPayload>;
'column.updated': BackendMessage<'column.updated', ColumnPayload>;
'column.deleted': BackendMessage<'column.deleted', ColumnPayload>;
};
+143
View File
@@ -0,0 +1,143 @@
export type TaskState =
| 'CREATED'
| 'SCHEDULING'
| 'TODO'
| 'IN_PROGRESS'
| 'REVIEW'
| 'BLOCKED'
| 'WAITING_FOR_INPUT'
| 'COMPLETED'
| 'FAILED'
| 'CANCELLED';
export type Board = {
id: string;
workspace_id: string;
name: string;
description?: string | null;
created_at: string;
updated_at: string;
};
export type Workspace = {
id: string;
name: string;
description?: string | null;
owner_id: string;
created_at: string;
updated_at: string;
};
export type Column = {
id: string;
board_id: string;
name: string;
position: number;
state: TaskState;
color: string;
created_at: string;
updated_at: string;
};
export type Repository = {
id: string;
workspace_id: string;
name: string;
source_type: string;
local_path: string;
provider: string;
provider_repo_id: string;
provider_owner: string;
provider_name: string;
default_branch: string;
setup_script: string;
cleanup_script: string;
created_at: string;
updated_at: string;
};
export type RepositoryScript = {
id: string;
repository_id: string;
name: string;
command: string;
position: number;
created_at: string;
updated_at: string;
};
export type Task = {
id: string;
workspace_id: string;
board_id: string;
column_id: string;
position: number;
title: string;
description: string;
state: TaskState;
priority: number;
agent_type?: string | null;
repository_url?: string | null;
branch?: string | null;
assigned_agent_id?: string | null;
created_at: string;
updated_at: string;
metadata?: Record<string, unknown> | null;
};
export type BoardSnapshot = {
board: Board;
columns: Column[];
tasks: Task[];
};
export type ListBoardsResponse = {
boards: Board[];
total: number;
};
export type ListColumnsResponse = {
columns: Column[];
total: number;
};
export type ListRepositoriesResponse = {
repositories: Repository[];
total: number;
};
export type ListRepositoryScriptsResponse = {
scripts: RepositoryScript[];
total: number;
};
export type LocalRepository = {
path: string;
name: string;
default_branch?: string;
};
export type RepositoryDiscoveryResponse = {
roots: string[];
repositories: LocalRepository[];
total: number;
};
export type RepositoryPathValidationResponse = {
path: string;
exists: boolean;
is_git: boolean;
allowed: boolean;
default_branch?: string;
message?: string;
};
export type RepositoryBranchesResponse = {
branches: string[];
total: number;
};
export type ListWorkspacesResponse = {
workspaces: Workspace[];
total: number;
};
+2
View File
@@ -0,0 +1,2 @@
export * from './backend';
export * from './http';
+12
View File
@@ -0,0 +1,12 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"composite": true,
"declaration": true,
"declarationMap": true,
"outDir": "./dist",
"rootDir": "./src"
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}
+38
View File
@@ -0,0 +1,38 @@
{
"name": "@kandev/ui",
"version": "0.1.0",
"private": true,
"main": "./src/index.ts",
"types": "./src/index.ts",
"exports": {
"./*": "./src/*.tsx"
},
"dependencies": {
"@radix-ui/react-alert-dialog": "^1.1.2",
"@radix-ui/react-collapsible": "^1.1.1",
"@radix-ui/react-dialog": "^1.1.2",
"@radix-ui/react-dropdown-menu": "^2.1.2",
"@radix-ui/react-label": "^2.1.0",
"@radix-ui/react-select": "^2.1.2",
"@radix-ui/react-separator": "^1.1.0",
"@radix-ui/react-slider": "^1.2.1",
"@radix-ui/react-slot": "^1.1.0",
"@radix-ui/react-switch": "^1.1.1",
"@radix-ui/react-tabs": "^1.1.1",
"@radix-ui/react-tooltip": "^1.1.4",
"@tabler/icons-react": "^3.24.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"react-resizable-panels": "^2.1.9",
"tailwind-merge": "^2.6.0"
},
"peerDependencies": {
"react": "^19.0.0",
"react-dom": "^19.0.0"
},
"devDependencies": {
"@types/react": "^19.0.1",
"@types/react-dom": "^19.0.2",
"typescript": "^5.7.2"
}
}
@@ -1,10 +1,10 @@
"use client"
import * as React from "react"
import { AlertDialog as AlertDialogPrimitive } from "radix-ui"
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
import { cn } from "./lib/utils"
import { Button } from "./button"
function AlertDialog({
...props
@@ -1,8 +1,8 @@
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { Slot } from "radix-ui"
import { Slot } from "@radix-ui/react-slot"
import { cn } from "@/lib/utils"
import { cn } from "./lib/utils"
const badgeVariants = cva(
"h-5 gap-1 rounded-4xl border border-transparent px-2 py-0.5 text-xs font-medium transition-all has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&>svg]:size-3! inline-flex items-center justify-center w-fit whitespace-nowrap shrink-0 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-colors overflow-hidden group/badge",
@@ -30,7 +30,7 @@ function Badge({
...props
}: React.ComponentProps<"span"> &
VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
const Comp = asChild ? Slot.Root : "span"
const Comp = asChild ? Slot : "span"
return (
<Comp
@@ -1,7 +1,7 @@
import * as React from "react"
import { Slot } from "radix-ui"
import { Slot } from "@radix-ui/react-slot"
import { cn } from "@/lib/utils"
import { cn } from "./lib/utils"
import { IconChevronRight, IconDots } from "@tabler/icons-react"
function Breadcrumb({ className, ...props }: React.ComponentProps<"nav">) {
@@ -45,7 +45,7 @@ function BreadcrumbLink({
}: React.ComponentProps<"a"> & {
asChild?: boolean
}) {
const Comp = asChild ? Slot.Root : "a"
const Comp = asChild ? Slot : "a"
return (
<Comp
@@ -1,8 +1,8 @@
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { Slot } from "radix-ui"
import { Slot } from "@radix-ui/react-slot"
import { cn } from "@/lib/utils"
import { cn } from "./lib/utils"
const buttonVariants = cva(
"focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 rounded-md border border-transparent bg-clip-padding text-sm font-medium focus-visible:ring-[3px] aria-invalid:ring-[3px] [&_svg:not([class*='size-'])]:size-4 inline-flex items-center justify-center whitespace-nowrap transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none shrink-0 [&_svg]:shrink-0 outline-none group/button select-none",
@@ -44,7 +44,7 @@ function Button({
VariantProps<typeof buttonVariants> & {
asChild?: boolean
}) {
const Comp = asChild ? Slot.Root : "button"
const Comp = asChild ? Slot : "button"
return (
<Comp
@@ -1,6 +1,6 @@
import * as React from "react"
import { cn } from "@/lib/utils"
import { cn } from "./lib/utils"
function Card({
className,
@@ -1,6 +1,6 @@
"use client"
import { Collapsible as CollapsiblePrimitive } from "radix-ui"
import * as CollapsiblePrimitive from "@radix-ui/react-collapsible"
function Collapsible({
...props
@@ -1,10 +1,10 @@
"use client"
import * as React from "react"
import { Dialog as DialogPrimitive } from "radix-ui"
import * as DialogPrimitive from "@radix-ui/react-dialog"
import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
import { cn } from "./lib/utils"
import { Button } from "./button"
import { IconX } from "@tabler/icons-react"
function Dialog({
@@ -1,9 +1,9 @@
"use client"
import * as React from "react"
import { DropdownMenu as DropdownMenuPrimitive } from "radix-ui"
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
import { cn } from "@/lib/utils"
import { cn } from "./lib/utils"
import { IconCheck, IconChevronRight } from "@tabler/icons-react"
function DropdownMenu({
+19
View File
@@ -0,0 +1,19 @@
import * as React from "react"
const MOBILE_BREAKPOINT = 768
export function useIsMobile() {
const [isMobile, setIsMobile] = React.useState<boolean | undefined>(undefined)
React.useEffect(() => {
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`)
const onChange = () => {
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
}
mql.addEventListener("change", onChange)
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
return () => mql.removeEventListener("change", onChange)
}, [])
return !!isMobile
}
+21
View File
@@ -0,0 +1,21 @@
export * from './alert-dialog';
export * from './badge';
export * from './breadcrumb';
export * from './button';
export * from './card';
export * from './collapsible';
export * from './dialog';
export * from './dropdown-menu';
export * from './input';
export * from './label';
export * from './resizable';
export * from './select';
export * from './separator';
export * from './sheet';
export * from './sidebar';
export * from './skeleton';
export * from './slider';
export * from './switch';
export * from './tabs';
export * from './textarea';
export * from './tooltip';
@@ -1,6 +1,6 @@
import * as React from "react"
import { cn } from "@/lib/utils"
import { cn } from "./lib/utils"
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
return (
@@ -1,9 +1,9 @@
"use client"
import * as React from "react"
import { Label as LabelPrimitive } from "radix-ui"
import * as LabelPrimitive from "@radix-ui/react-label"
import { cn } from "@/lib/utils"
import { cn } from "./lib/utils"
function Label({
className,
+6
View File
@@ -0,0 +1,6 @@
import { type ClassValue, clsx } from 'clsx';
import { twMerge } from 'tailwind-merge';
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
@@ -3,7 +3,7 @@
import * as React from "react"
import * as ResizablePrimitive from "react-resizable-panels"
import { cn } from "@/lib/utils"
import { cn } from "./lib/utils"
function ResizablePanelGroup({
className,
@@ -1,9 +1,9 @@
"use client"
import * as React from "react"
import { Select as SelectPrimitive } from "radix-ui"
import * as SelectPrimitive from "@radix-ui/react-select"
import { cn } from "@/lib/utils"
import { cn } from "./lib/utils"
import { IconSelector, IconCheck, IconChevronUp, IconChevronDown } from "@tabler/icons-react"
function Select({
@@ -1,9 +1,9 @@
"use client"
import * as React from "react"
import { Separator as SeparatorPrimitive } from "radix-ui"
import * as SeparatorPrimitive from "@radix-ui/react-separator"
import { cn } from "@/lib/utils"
import { cn } from "./lib/utils"
function Separator({
className,
@@ -1,10 +1,10 @@
"use client"
import * as React from "react"
import { Dialog as SheetPrimitive } from "radix-ui"
import * as SheetPrimitive from "@radix-ui/react-dialog"
import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
import { cn } from "./lib/utils"
import { Button } from "./button"
import { IconX } from "@tabler/icons-react"
function Sheet({ ...props }: React.ComponentProps<typeof SheetPrimitive.Root>) {
@@ -2,26 +2,26 @@
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { Slot } from "radix-ui"
import { Slot } from "@radix-ui/react-slot"
import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Separator } from "@/components/ui/separator"
import { cn } from "./lib/utils"
import { Button } from "./button"
import { Input } from "./input"
import { Separator } from "./separator"
import {
Sheet,
SheetContent,
SheetDescription,
SheetHeader,
SheetTitle,
} from "@/components/ui/sheet"
import { Skeleton } from "@/components/ui/skeleton"
} from "./sheet"
import { Skeleton } from "./skeleton"
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip"
import { useIsMobile } from "@/hooks/use-mobile"
} from "./tooltip"
import { useIsMobile } from "./hooks/use-mobile"
import { IconLayoutSidebar } from "@tabler/icons-react"
const SIDEBAR_COOKIE_NAME = "sidebar_state"
@@ -398,7 +398,7 @@ function SidebarGroupLabel({
asChild = false,
...props
}: React.ComponentProps<"div"> & { asChild?: boolean }) {
const Comp = asChild ? Slot.Root : "div"
const Comp = asChild ? Slot : "div"
return (
<Comp
@@ -418,7 +418,7 @@ function SidebarGroupAction({
asChild = false,
...props
}: React.ComponentProps<"button"> & { asChild?: boolean }) {
const Comp = asChild ? Slot.Root : "button"
const Comp = asChild ? Slot : "button"
return (
<Comp
@@ -503,7 +503,7 @@ function SidebarMenuButton({
isActive?: boolean
tooltip?: string | React.ComponentProps<typeof TooltipContent>
} & VariantProps<typeof sidebarMenuButtonVariants>) {
const Comp = asChild ? Slot.Root : "button"
const Comp = asChild ? Slot : "button"
const { isMobile, state } = useSidebar()
const button = (
@@ -549,7 +549,7 @@ function SidebarMenuAction({
asChild?: boolean
showOnHover?: boolean
}) {
const Comp = asChild ? Slot.Root : "button"
const Comp = asChild ? Slot : "button"
return (
<Comp
@@ -657,7 +657,7 @@ function SidebarMenuSubButton({
size?: "sm" | "md"
isActive?: boolean
}) {
const Comp = asChild ? Slot.Root : "a"
const Comp = asChild ? Slot : "a"
return (
<Comp
@@ -1,4 +1,4 @@
import { cn } from "@/lib/utils"
import { cn } from "./lib/utils"
function Skeleton({ className, ...props }: React.ComponentProps<"div">) {
return (
@@ -1,9 +1,9 @@
"use client"
import * as React from "react"
import { Slider as SliderPrimitive } from "radix-ui"
import * as SliderPrimitive from "@radix-ui/react-slider"
import { cn } from "@/lib/utils"
import { cn } from "./lib/utils"
function Slider({
className,
@@ -1,9 +1,9 @@
"use client"
import * as React from "react"
import { Switch as SwitchPrimitive } from "radix-ui"
import * as SwitchPrimitive from "@radix-ui/react-switch"
import { cn } from "@/lib/utils"
import { cn } from "./lib/utils"
function Switch({
className,
@@ -2,9 +2,9 @@
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { Tabs as TabsPrimitive } from "radix-ui"
import * as TabsPrimitive from "@radix-ui/react-tabs"
import { cn } from "@/lib/utils"
import { cn } from "./lib/utils"
function Tabs({
className,
@@ -1,6 +1,6 @@
import * as React from "react"
import { cn } from "@/lib/utils"
import { cn } from "./lib/utils"
function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
return (
@@ -3,7 +3,7 @@
import * as React from "react"
import * as TooltipPrimitive from "@radix-ui/react-tooltip"
import { cn } from "@/lib/utils"
import { cn } from "./lib/utils"
function TooltipProvider({
delayDuration = 0,
+12
View File
@@ -0,0 +1,12 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"composite": true,
"declaration": true,
"declarationMap": true,
"outDir": "./dist",
"rootDir": "./src"
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}
+4
View File
@@ -0,0 +1,4 @@
packages:
- 'web'
- 'landing'
- 'packages/*'
+17
View File
@@ -0,0 +1,17 @@
{
"compilerOptions": {
"target": "ES2017",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true
}
}
+4 -1
View File
@@ -1,7 +1,10 @@
@import "tailwindcss";
@import "tw-animate-css";
@import "shadcn/tailwind.css";
@import "./fonts.css";
@import "@kandev/theme/fonts.css";
@source "../components/**/*.{js,ts,jsx,tsx}";
@source "../../packages/ui/src/**/*.{js,ts,jsx,tsx}";
@custom-variant dark (&:is(.dark *));
+9 -9
View File
@@ -3,13 +3,13 @@
import { use, useState } from 'react';
import { useRouter } from 'next/navigation';
import { IconTrash } from '@tabler/icons-react';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
import { Separator } from '@/components/ui/separator';
import { Switch } from '@/components/ui/switch';
import { Slider } from '@/components/ui/slider';
import { Button } from '@kandev/ui/button';
import { Input } from '@kandev/ui/input';
import { Label } from '@kandev/ui/label';
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@kandev/ui/card';
import { Separator } from '@kandev/ui/separator';
import { Switch } from '@kandev/ui/switch';
import { Slider } from '@kandev/ui/slider';
import {
Dialog,
DialogContent,
@@ -17,9 +17,9 @@ import {
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
} from '@kandev/ui/dialog';
import { SETTINGS_DATA } from '@/lib/settings/dummy-data';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@kandev/ui/select';
import type { AgentProfile, AgentType } from '@/lib/settings/types';
const MODEL_OPTIONS: Record<AgentType, { value: string; label: string }[]> = {
+6 -6
View File
@@ -2,12 +2,12 @@
import { useState } from 'react';
import { IconPlus } from '@tabler/icons-react';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Card, CardContent } from '@/components/ui/card';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Separator } from '@/components/ui/separator';
import { Button } from '@kandev/ui/button';
import { Input } from '@kandev/ui/input';
import { Label } from '@kandev/ui/label';
import { Card, CardContent } from '@kandev/ui/card';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@kandev/ui/select';
import { Separator } from '@kandev/ui/separator';
import { AgentCard } from '@/components/settings/agent-card';
import { SETTINGS_DATA } from '@/lib/settings/dummy-data';
import type { AgentProfile, AgentType } from '@/lib/settings/types';
@@ -3,13 +3,13 @@
import { use, useState } from 'react';
import { useRouter } from 'next/navigation';
import { IconPlus, IconTrash } from '@tabler/icons-react';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Textarea } from '@/components/ui/textarea';
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
import { Separator } from '@/components/ui/separator';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Button } from '@kandev/ui/button';
import { Input } from '@kandev/ui/input';
import { Label } from '@kandev/ui/label';
import { Textarea } from '@kandev/ui/textarea';
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@kandev/ui/card';
import { Separator } from '@kandev/ui/separator';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@kandev/ui/select';
import {
Dialog,
DialogContent,
@@ -17,7 +17,7 @@ import {
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
} from '@kandev/ui/dialog';
import { generateUUID } from '@/lib/utils';
import { SETTINGS_DATA } from '@/lib/settings/dummy-data';
import type { Environment, EnvironmentType, BaseDocker, AgentType } from '@/lib/settings/types';
+7 -7
View File
@@ -2,13 +2,13 @@
import { useState } from 'react';
import { IconPlus } from '@tabler/icons-react';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Textarea } from '@/components/ui/textarea';
import { Card, CardContent } from '@/components/ui/card';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Separator } from '@/components/ui/separator';
import { Button } from '@kandev/ui/button';
import { Input } from '@kandev/ui/input';
import { Label } from '@kandev/ui/label';
import { Textarea } from '@kandev/ui/textarea';
import { Card, CardContent } from '@kandev/ui/card';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@kandev/ui/select';
import { Separator } from '@kandev/ui/separator';
import { EnvironmentCard } from '@/components/settings/environment-card';
import { KeyValueInput } from '@/components/settings/key-value-input';
import { SETTINGS_DATA } from '@/lib/settings/dummy-data';
+5 -5
View File
@@ -3,11 +3,11 @@
import { useState, useSyncExternalStore } from 'react';
import { useTheme } from 'next-themes';
import { IconPalette, IconCode, IconBell, IconServer } from '@tabler/icons-react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Label } from '@/components/ui/label';
import { Input } from '@/components/ui/input';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Separator } from '@/components/ui/separator';
import { Card, CardContent, CardHeader, CardTitle } from '@kandev/ui/card';
import { Label } from '@kandev/ui/label';
import { Input } from '@kandev/ui/input';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@kandev/ui/select';
import { Separator } from '@kandev/ui/separator';
import { SettingsSection } from '@/components/settings/settings-section';
import { SETTINGS_DATA } from '@/lib/settings/dummy-data';
import { getBackendConfig } from '@/lib/config';
+3 -3
View File
@@ -8,9 +8,9 @@ import {
IconBox,
IconCircleDot,
} from '@tabler/icons-react';
import { Button } from '@/components/ui/button';
import { Card, CardContent } from '@/components/ui/card';
import { Separator } from '@/components/ui/separator';
import { Button } from '@kandev/ui/button';
import { Card, CardContent } from '@kandev/ui/card';
import { Separator } from '@kandev/ui/separator';
type Integration = {
id: string;
+4 -4
View File
@@ -9,10 +9,10 @@ import {
IconKey,
IconShield,
} from '@tabler/icons-react';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Separator } from '@/components/ui/separator';
import { Badge } from '@/components/ui/badge';
import { Button } from '@kandev/ui/button';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@kandev/ui/card';
import { Separator } from '@kandev/ui/separator';
import { Badge } from '@kandev/ui/badge';
type Plugin = {
id: string;
@@ -3,11 +3,11 @@
import { useMemo, useState } from 'react';
import { useRouter } from 'next/navigation';
import { IconGitBranch, IconLoader2, IconPlus, IconTrash } from '@tabler/icons-react';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Separator } from '@/components/ui/separator';
import { Button } from '@kandev/ui/button';
import { Input } from '@kandev/ui/input';
import { Label } from '@kandev/ui/label';
import { Card, CardContent, CardHeader, CardTitle } from '@kandev/ui/card';
import { Separator } from '@kandev/ui/separator';
import {
Dialog,
DialogContent,
@@ -15,7 +15,7 @@ import {
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
} from '@kandev/ui/dialog';
import { SettingsSection } from '@/components/settings/settings-section';
import { RepositoryCard } from '@/components/settings/repository-card';
import { BoardCard } from '@/components/settings/board-card';
@@ -3,11 +3,11 @@
import { useEffect, useState } from 'react';
import Link from 'next/link';
import { IconFolder, IconPlus, IconChevronRight } from '@tabler/icons-react';
import { Button } from '@/components/ui/button';
import { Card, CardContent } from '@/components/ui/card';
import { Separator } from '@/components/ui/separator';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Button } from '@kandev/ui/button';
import { Card, CardContent } from '@kandev/ui/card';
import { Separator } from '@kandev/ui/separator';
import { Input } from '@kandev/ui/input';
import { Label } from '@kandev/ui/label';
import { createWorkspaceAction } from '@/app/actions/workspaces';
import type { Workspace } from '@/lib/types/http';
import { useRequest } from '@/lib/http/use-request';
+17 -7
View File
@@ -2,10 +2,10 @@
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 { ResizableHandle, ResizablePanel, ResizablePanelGroup } from '@kandev/ui/resizable';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@kandev/ui/tabs';
import { TooltipProvider } from '@kandev/ui/tooltip';
import { Textarea } from '@kandev/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';
@@ -29,11 +29,13 @@ type TaskPageClientProps = {
task: Task | null;
};
const DEFAULT_HORIZONTAL_LAYOUT: [number, number] = [75, 25];
export default function TaskPage({ task: initialTask }: TaskPageClientProps) {
const store = useAppStoreApi();
const defaultHorizontalLayout: [number, number] = [75, 25];
const [horizontalLayout, setHorizontalLayout] = useState<[number, number]>(() =>
getLocalStorage('task-layout-horizontal', defaultHorizontalLayout)
const [isMounted, setIsMounted] = useState(false);
const [horizontalLayout, setHorizontalLayout] = useState<[number, number]>(
getLocalStorage('task-layout-horizontal', DEFAULT_HORIZONTAL_LAYOUT)
);
const [leftTab, setLeftTab] = useState('chat');
const [selectedDiffPath, setSelectedDiffPath] = useState<string | null>(null);
@@ -52,6 +54,10 @@ export default function TaskPage({ task: initialTask }: TaskPageClientProps) {
state.kanban.tasks.find((t) => t.id === task?.id)
);
useEffect(() => {
setIsMounted(true);
}, []);
// Fetch task execution status from orchestrator on mount
useEffect(() => {
if (!task?.id) return;
@@ -212,6 +218,10 @@ export default function TaskPage({ task: initialTask }: TaskPageClientProps) {
branchesRequest.run(task.workspace_id, task.repository_url).catch(() => {});
}, [branchesRequest.run, task?.repository_url, task?.workspace_id]);
if (!isMounted) {
return <div className="h-screen w-full bg-background" />;
}
return (
<TooltipProvider>
<div className="h-screen w-full flex flex-col bg-background">
+2 -2
View File
@@ -1,7 +1,7 @@
'use client';
import { Badge } from '@/components/ui/badge';
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
import { Badge } from '@kandev/ui/badge';
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@kandev/ui/tooltip';
import { IconCircleFilled } from '@tabler/icons-react';
import { useAppStore } from '@/components/state-provider';
+1 -1
View File
@@ -1,7 +1,7 @@
'use client';
import { cn } from '@/lib/utils';
import { Badge } from '@/components/ui/badge';
import { Badge } from '@kandev/ui/badge';
type CommitStatBadgeProps = {
label: string;
+2 -2
View File
@@ -13,7 +13,7 @@ import {
import { KanbanColumn, Column } from './kanban-column';
import { KanbanCardPreview, Task } from './kanban-card';
import { ConnectionStatus } from './connection-status';
import { Button } from '@/components/ui/button';
import { Button } from '@kandev/ui/button';
import { IconPlus, IconSettings } from '@tabler/icons-react';
import { TaskCreateDialog } from './task-create-dialog';
import Link from 'next/link';
@@ -23,7 +23,7 @@ import {
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
} from '@kandev/ui/select';
import { useRouter } from 'next/navigation';
import { useAppStore, useAppStoreApi } from '@/components/state-provider';
import { getWebSocketClient } from '@/lib/ws/connection';
+2 -2
View File
@@ -3,13 +3,13 @@
import { useDraggable } from '@dnd-kit/core';
import { CSS } from '@dnd-kit/utilities';
import { IconAlertTriangle, IconCircleCheck, IconCircleX, IconDots, IconLoader2 } from '@tabler/icons-react';
import { Card, CardContent } from '@/components/ui/card';
import { Card, CardContent } from '@kandev/ui/card';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
} from '@kandev/ui/dropdown-menu';
import { cn } from '@/lib/utils';
export interface Task {
+1 -1
View File
@@ -2,7 +2,7 @@
import { useDroppable } from '@dnd-kit/core';
import { KanbanCard, Task } from './kanban-card';
import { Badge } from '@/components/ui/badge';
import { Badge } from '@kandev/ui/badge';
import { cn } from '@/lib/utils';
export interface Column {
+2 -2
View File
@@ -8,8 +8,8 @@ import {
DialogTitle,
DialogFooter,
DialogDescription,
} from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
} from '@kandev/ui/dialog';
import { Button } from '@kandev/ui/button';
import { IconCheck, IconRocket } from '@tabler/icons-react';
interface OnboardingDialogProps {
open: boolean;
+2 -2
View File
@@ -2,8 +2,8 @@
import Link from 'next/link';
import { IconRobot, IconChevronRight } from '@tabler/icons-react';
import { Card, CardContent } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import { Card, CardContent } from '@kandev/ui/card';
import { Badge } from '@kandev/ui/badge';
import type { AgentProfile, AgentType } from '@/lib/settings/types';
type AgentCardProps = {
+5 -5
View File
@@ -17,10 +17,10 @@ import {
IconTrash,
IconPlus,
} from '@tabler/icons-react';
import { Card, CardContent } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Card, CardContent } from '@kandev/ui/card';
import { Button } from '@kandev/ui/button';
import { Input } from '@kandev/ui/input';
import { Label } from '@kandev/ui/label';
import {
Dialog,
DialogContent,
@@ -28,7 +28,7 @@ import {
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
} from '@kandev/ui/dialog';
import type { Board, Column } from '@/lib/types/http';
import { useRequest } from '@/lib/http/use-request';
import { useToast } from '@/components/toast-provider';
@@ -2,8 +2,8 @@
import Link from 'next/link';
import { IconServer, IconChevronRight } from '@tabler/icons-react';
import { Card, CardContent } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import { Card, CardContent } from '@kandev/ui/card';
import { Badge } from '@kandev/ui/badge';
import type { Environment } from '@/lib/settings/types';
type EnvironmentCardProps = {
@@ -1,8 +1,8 @@
'use client';
import { IconPlus, IconX } from '@tabler/icons-react';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Button } from '@kandev/ui/button';
import { Input } from '@kandev/ui/input';
import type { KeyValue } from '@/lib/settings/types';
import { generateUUID } from '@/lib/utils';
@@ -2,11 +2,11 @@
import { useState } from 'react';
import { IconGitBranch, IconPlus, IconTrash, IconX } from '@tabler/icons-react';
import { Card, CardContent } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Textarea } from '@/components/ui/textarea';
import { Card, CardContent } from '@kandev/ui/card';
import { Button } from '@kandev/ui/button';
import { Input } from '@kandev/ui/input';
import { Label } from '@kandev/ui/label';
import { Textarea } from '@kandev/ui/textarea';
import {
Dialog,
DialogContent,
@@ -14,7 +14,7 @@ import {
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
} from '@kandev/ui/dialog';
import { useRequest } from '@/lib/http/use-request';
import { useToast } from '@/components/toast-provider';
import { UnsavedChangesBadge, UnsavedSaveButton } from '@/components/settings/unsaved-indicator';
@@ -26,8 +26,8 @@ import {
SidebarMenuSubButton,
SidebarMenuAction,
SidebarHeader,
} from '@/components/ui/sidebar';
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible';
} from '@kandev/ui/sidebar';
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@kandev/ui/collapsible';
import { SETTINGS_DATA } from '@/lib/settings/dummy-data';
import { useAppStore } from '@/components/state-provider';
@@ -6,10 +6,10 @@ import {
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
} from '@/components/ui/breadcrumb';
import { SidebarInset, SidebarProvider } from '@/components/ui/sidebar';
} from '@kandev/ui/breadcrumb';
import { SidebarInset, SidebarProvider } from '@kandev/ui/sidebar';
import { SettingsAppSidebar } from '@/components/settings/settings-app-sidebar';
import { TooltipProvider } from '@/components/ui/tooltip';
import { TooltipProvider } from '@kandev/ui/tooltip';
export function SettingsLayoutClient({ children }: { children: React.ReactNode }) {
return (
@@ -1,6 +1,6 @@
'use client';
import { Sheet, SheetContent } from '@/components/ui/sheet';
import { Sheet, SheetContent } from '@kandev/ui/sheet';
import { SettingsSidebar } from './settings-sidebar';
type SettingsMobileNavProps = {
@@ -1,7 +1,7 @@
'use client';
import { IconAlertCircle } from '@tabler/icons-react';
import { Button } from '@/components/ui/button';
import { Button } from '@kandev/ui/button';
import { RequestIndicator } from '@/components/request-indicator';
type UnsavedSaveButtonProps = {
+7 -7
View File
@@ -8,19 +8,19 @@ import {
DialogTitle,
DialogFooter,
DialogClose,
} from '@/components/ui/dialog';
import { Label } from '@/components/ui/label';
import { Input } from '@/components/ui/input';
import { Textarea } from '@/components/ui/textarea';
import { Button } from '@/components/ui/button';
} from '@kandev/ui/dialog';
import { Label } from '@kandev/ui/label';
import { Input } from '@kandev/ui/input';
import { Textarea } from '@kandev/ui/textarea';
import { Button } from '@kandev/ui/button';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
} from '@kandev/ui/select';
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@kandev/ui/tooltip';
import type { Repository, Task } from '@/lib/types/http';
import { getBackendConfig } from '@/lib/config';
import { createTask, listRepositories, listRepositoryBranches, updateTask } from '@/lib/http/client';
@@ -1,12 +1,12 @@
'use client';
import { memo, useMemo, useState } from 'react';
import { memo, useEffect, useMemo, useState } from 'react';
import { DiffModeEnum, DiffView } from '@git-diff-view/react';
import { IconArrowBackUp, IconCopy, IconLayoutColumns, IconLayoutRows } from '@tabler/icons-react';
import { useTheme } from 'next-themes';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
import { Badge } from '@kandev/ui/badge';
import { Button } from '@kandev/ui/button';
import { Tooltip, TooltipContent, TooltipTrigger } from '@kandev/ui/tooltip';
import { cn } from '@/lib/utils';
import { getLocalStorage, setLocalStorage } from '@/lib/local-storage';
import { buildDiffData, CHANGED_FILES, DIFF_SAMPLES } from '@/components/task/task-data';
@@ -16,16 +16,19 @@ type TaskChangesPanelProps = {
onClearSelected: () => void;
};
const DEFAULT_DIFF_MODE: 'unified' | 'split' = 'unified';
const TaskChangesPanel = memo(function TaskChangesPanel({
selectedDiffPath,
onClearSelected,
}: TaskChangesPanelProps) {
const defaultDiffMode: 'unified' | 'split' = 'unified';
const [diffViewMode, setDiffViewMode] = useState<'unified' | 'split'>(() =>
getLocalStorage('task-diff-view-mode', defaultDiffMode)
);
const [diffViewMode, setDiffViewMode] = useState<'unified' | 'split'>(DEFAULT_DIFF_MODE);
const { resolvedTheme } = useTheme();
useEffect(() => {
setDiffViewMode(getLocalStorage('task-diff-view-mode', DEFAULT_DIFF_MODE));
}, []);
const diffTargets = useMemo(
() => (selectedDiffPath ? [selectedDiffPath] : CHANGED_FILES.map((file) => file.path)),
[selectedDiffPath]
+5 -5
View File
@@ -20,16 +20,16 @@ import {
import ReactMarkdown from 'react-markdown';
import remarkBreaks from 'remark-breaks';
import remarkGfm from 'remark-gfm';
import { Button } from '@/components/ui/button';
import { Textarea } from '@/components/ui/textarea';
import { Button } from '@kandev/ui/button';
import { Textarea } from '@kandev/ui/textarea';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
} from '@kandev/ui/dropdown-menu';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@kandev/ui/select';
import { Tooltip, TooltipContent, TooltipTrigger } from '@kandev/ui/tooltip';
import { cn } from '@/lib/utils';
import { useAppStore } from '@/components/state-provider';
import type { Comment } from '@/lib/types/http';
@@ -9,12 +9,12 @@ import {
IconFile,
IconFolder,
} from '@tabler/icons-react';
import { Badge } from '@/components/ui/badge';
import { Badge } from '@kandev/ui/badge';
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from '@/components/ui/collapsible';
} from '@kandev/ui/collapsible';
import {
Sidebar,
SidebarContent,
@@ -26,9 +26,9 @@ import {
SidebarMenuSub,
SidebarProvider,
SidebarRail,
} from '@/components/ui/sidebar';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
} from '@kandev/ui/sidebar';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@kandev/ui/tabs';
import { Tooltip, TooltipContent, TooltipTrigger } from '@kandev/ui/tooltip';
import { LineStat } from '@/components/diff-stat';
import { cn } from '@/lib/utils';
import { CHANGED_FILES, FILE_TREE } from '@/components/task/task-data';
@@ -3,9 +3,9 @@
import type { ReactNode } from 'react';
import { memo, useState } from 'react';
import { IconChevronDown, IconChevronUp, IconX } from '@tabler/icons-react';
import { Badge } from '@/components/ui/badge';
import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from '@/components/ui/resizable';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { Badge } from '@kandev/ui/badge';
import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from '@kandev/ui/resizable';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@kandev/ui/tabs';
import { getLocalStorage, setLocalStorage } from '@/lib/local-storage';
import { COMMANDS } from '@/components/task/task-data';
@@ -13,10 +13,11 @@ type TaskRightPanelProps = {
topPanel: ReactNode;
};
const DEFAULT_RIGHT_LAYOUT: [number, number] = [55, 45];
const TaskRightPanel = memo(function TaskRightPanel({ topPanel }: TaskRightPanelProps) {
const defaultRightLayout: [number, number] = [55, 45];
const [rightLayout, setRightLayout] = useState<[number, number]>(() =>
getLocalStorage('task-layout-right', defaultRightLayout)
getLocalStorage('task-layout-right', DEFAULT_RIGHT_LAYOUT)
);
const [activeTerminalId, setActiveTerminalId] = useState(1);
const [terminalIds, setTerminalIds] = useState([1]);
+4 -4
View File
@@ -20,11 +20,11 @@ import {
IconPlayerPlay,
IconPlayerStop,
} from '@tabler/icons-react';
import { Button } from '@/components/ui/button';
import { Button } from '@kandev/ui/button';
import { CommitStatBadge, LineStat } from '@/components/diff-stat';
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@/components/ui/dropdown-menu';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@kandev/ui/dropdown-menu';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@kandev/ui/select';
import { Tooltip, TooltipContent, TooltipTrigger } from '@kandev/ui/tooltip';
type TaskTopBarProps = {
taskTitle?: string;
+1 -1
View File
@@ -2,7 +2,7 @@
import { useTheme } from 'next-themes';
import { IconMoon, IconSun } from '@tabler/icons-react';
import { Button } from '@/components/ui/button';
import { Button } from '@kandev/ui/button';
import { useSyncExternalStore } from 'react';
export function ThemeToggle() {
+1 -1
View File
@@ -2,7 +2,7 @@ export type AppConfig = {
apiBaseUrl: string;
};
const DEFAULT_API_BASE_URL = 'http://nova:8080';
const DEFAULT_API_BASE_URL = 'http://localhost:8080';
export function getBackendConfig(): AppConfig {
if (typeof window === 'undefined') {

Some files were not shown because too many files have changed in this diff Show More