ship: project dashboard pages + sidebar/chat overhaul + log tooling
Ships accumulated WIP that was sitting uncommitted: - New (home) dashboard route pages: overview, code, data/tables, hosting, infrastructure, services, domains, integrations, agents, analytics, api, automations, billing, logs, market, marketing(+seo/social), product, security, storage, users, settings(app/auth). - dashboard-sidebar, project-icon-rail, chat-panel updates; mcp + anatomy route changes; package.json/lock dependency bumps. - Coolify log tooling (scripts/fetch-app-logs.mjs + fetch-app-logs-ssh.mjs) and ai-new-thread.md "Fetching Production Logs" section. Excludes throwaway debug scripts and telemetry audit dumps (the latter contain live credentials and must not be committed).
This commit is contained in:
@@ -1,80 +1,341 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
import {
|
||||
Code2,
|
||||
Search,
|
||||
LayoutGrid,
|
||||
ClipboardList,
|
||||
Globe,
|
||||
Database,
|
||||
BarChart2,
|
||||
Globe,
|
||||
Plug,
|
||||
ShieldCheck,
|
||||
Code2,
|
||||
Terminal,
|
||||
Settings,
|
||||
CreditCard,
|
||||
PlaneTakeoff,
|
||||
ChevronDown,
|
||||
ChevronRight,
|
||||
Users,
|
||||
HardDrive,
|
||||
Blocks,
|
||||
} from "lucide-react";
|
||||
|
||||
export function DashboardSidebar({ workspace, projectId, children }: { workspace: string, projectId: string, children: React.ReactNode }) {
|
||||
import { useAnatomy } from "@/components/project/use-anatomy";
|
||||
|
||||
export function DashboardSidebar({
|
||||
workspace,
|
||||
projectId,
|
||||
children,
|
||||
}: {
|
||||
workspace: string;
|
||||
projectId: string;
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
const pathname = usePathname() ?? "";
|
||||
const projectBase = `/${workspace}/project/${projectId}`;
|
||||
const isPreview = pathname === `${projectBase}/preview` || pathname.startsWith(`${projectBase}/preview/`);
|
||||
const isPreview =
|
||||
pathname === `${projectBase}/preview` ||
|
||||
pathname.startsWith(`${projectBase}/preview/`);
|
||||
|
||||
const [expandedSections, setExpandedSections] = useState<
|
||||
Record<string, boolean>
|
||||
>({
|
||||
settings: true,
|
||||
data: true,
|
||||
});
|
||||
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
|
||||
const { anatomy } = useAnatomy(projectId);
|
||||
const databases = anatomy?.infrastructure?.databases ?? [];
|
||||
|
||||
if (isPreview) {
|
||||
return <>{children}</>;
|
||||
}
|
||||
|
||||
const items = [
|
||||
{ segment: "plan", label: "Plan", Icon: ClipboardList },
|
||||
{ segment: "market", label: "Market", Icon: PlaneTakeoff },
|
||||
{ segment: "product", label: "Code", Icon: Code2, aliases: ["code"] },
|
||||
{ segment: "hosting", label: "Hosting", Icon: Globe },
|
||||
{ segment: "infrastructure", label: "Infra", Icon: Database },
|
||||
{ segment: "billing", label: "Billing", Icon: CreditCard },
|
||||
{ segment: "settings", label: "Settings", Icon: Settings },
|
||||
const handleSectionClick = (segment: string) => {
|
||||
if (!expandedSections[segment]) {
|
||||
setExpandedSections((prev) => ({ ...prev, [segment]: true }));
|
||||
}
|
||||
};
|
||||
|
||||
const menuItems = [
|
||||
{ segment: "overview", label: "Overview", Icon: LayoutGrid },
|
||||
{ segment: "plan", label: "Plan & Specs", Icon: ClipboardList },
|
||||
{ segment: "code", label: "Code", Icon: Code2 },
|
||||
{
|
||||
segment: "data",
|
||||
label: "Data",
|
||||
Icon: Database,
|
||||
hasChildren: true,
|
||||
children: databases.map((db) => ({
|
||||
segment: `data/tables?db=${db.uuid}`,
|
||||
label: db.name,
|
||||
})),
|
||||
},
|
||||
{ segment: "storage", label: "Storage", Icon: HardDrive },
|
||||
{ segment: "services", label: "Services", Icon: Blocks },
|
||||
{ segment: "users", label: "Auth / Users", Icon: Users },
|
||||
{ segment: "integrations", label: "Integrations", Icon: Plug },
|
||||
{ segment: "security", label: "Security", Icon: ShieldCheck },
|
||||
{ segment: "logs", label: "Logs", Icon: Terminal },
|
||||
{ segment: "domains", label: "Domains", Icon: Globe },
|
||||
{
|
||||
segment: "analytics",
|
||||
label: "Analytics",
|
||||
Icon: BarChart2,
|
||||
badge: "Soon",
|
||||
},
|
||||
{
|
||||
segment: "marketing",
|
||||
label: "Marketing",
|
||||
Icon: BarChart2,
|
||||
badge: "New",
|
||||
hasChildren: true,
|
||||
children: [
|
||||
{ segment: "marketing/seo", label: "SEO & GEO" },
|
||||
{ segment: "marketing/social", label: "Social content" },
|
||||
],
|
||||
},
|
||||
{
|
||||
segment: "settings",
|
||||
label: "Settings",
|
||||
Icon: Settings,
|
||||
hasChildren: true,
|
||||
children: [
|
||||
{ segment: "settings/app", label: "App Settings" },
|
||||
{ segment: "settings/auth", label: "Authentication" },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const filteredItems = menuItems.filter(
|
||||
(item) =>
|
||||
item.label.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
(item.children &&
|
||||
item.children.some((child) =>
|
||||
child.label.toLowerCase().includes(searchQuery.toLowerCase()),
|
||||
)),
|
||||
);
|
||||
|
||||
return (
|
||||
<div style={{ display: "flex", flex: 1, minHeight: 0, minWidth: 0 }}>
|
||||
<div style={{
|
||||
width: 240,
|
||||
borderRight: "1px solid #e4e4e7",
|
||||
background: "#fafafa",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
padding: "16px 12px",
|
||||
gap: 4,
|
||||
overflowY: "auto"
|
||||
}}>
|
||||
<div style={{ fontSize: "0.75rem", fontWeight: 600, color: "#a1a1aa", padding: "0 8px 8px", textTransform: "uppercase", letterSpacing: "0.05em" }}>
|
||||
<div
|
||||
style={{
|
||||
width: 250,
|
||||
borderRight: "1px solid #e4e4e7",
|
||||
background: "#ffffff",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
padding: "16px 12px",
|
||||
gap: 4,
|
||||
overflowY: "auto",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
fontSize: "0.85rem",
|
||||
fontWeight: 600,
|
||||
color: "#18181b",
|
||||
padding: "0 8px 12px",
|
||||
}}
|
||||
>
|
||||
Dashboard
|
||||
</div>
|
||||
{items.map(item => {
|
||||
const active = pathname === `${projectBase}/${item.segment}` || (item.aliases && item.aliases.some(a => pathname === `${projectBase}/${a}`));
|
||||
return (
|
||||
<Link
|
||||
key={item.segment}
|
||||
href={`${projectBase}/${item.segment}`}
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 10,
|
||||
padding: "8px 12px",
|
||||
borderRadius: 8,
|
||||
fontSize: "0.85rem",
|
||||
fontWeight: 500,
|
||||
color: active ? "#18181b" : "#52525b",
|
||||
background: active ? "#fff" : "transparent",
|
||||
boxShadow: active ? "0 1px 2px rgba(0,0,0,0.05)" : "none",
|
||||
border: active ? "1px solid #e4e4e7" : "1px solid transparent",
|
||||
textDecoration: "none",
|
||||
transition: "all 0.15s ease"
|
||||
}}
|
||||
>
|
||||
<item.Icon size={16} color={active ? "#18181b" : "#a1a1aa"} />
|
||||
{item.label}
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* Search Bar */}
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 8,
|
||||
background: "#f4f4f5",
|
||||
borderRadius: 8,
|
||||
padding: "6px 10px",
|
||||
marginBottom: 12,
|
||||
margin: "0 4px 12px 4px",
|
||||
}}
|
||||
>
|
||||
<Search size={14} color="#a1a1aa" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
style={{
|
||||
border: "none",
|
||||
background: "transparent",
|
||||
outline: "none",
|
||||
width: "100%",
|
||||
fontSize: "0.8rem",
|
||||
color: "#18181b",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 2 }}>
|
||||
{filteredItems.map((item) => {
|
||||
const isMainActive =
|
||||
pathname === `${projectBase}/${item.segment}` ||
|
||||
pathname.startsWith(`${projectBase}/${item.segment}/`);
|
||||
const isExpanded = expandedSections[item.segment];
|
||||
|
||||
return (
|
||||
<div key={item.segment}>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
padding: "8px 10px",
|
||||
borderRadius: 8,
|
||||
cursor: "pointer",
|
||||
background:
|
||||
isMainActive && !item.hasChildren
|
||||
? "#eff6ff"
|
||||
: "transparent",
|
||||
color:
|
||||
isMainActive && !item.hasChildren ? "#1d4ed8" : "#52525b",
|
||||
transition: "all 0.1s ease",
|
||||
}}
|
||||
onClick={() => {
|
||||
if (item.hasChildren) {
|
||||
setExpandedSections((prev) => ({
|
||||
...prev,
|
||||
[item.segment]: !prev[item.segment],
|
||||
}));
|
||||
}
|
||||
}}
|
||||
>
|
||||
{item.hasChildren ? (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 10,
|
||||
flex: 1,
|
||||
}}
|
||||
>
|
||||
<item.Icon size={16} />
|
||||
<span style={{ fontSize: "0.85rem", fontWeight: 500 }}>
|
||||
{item.label}
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
<Link
|
||||
href={`${projectBase}/${item.segment}`}
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 10,
|
||||
flex: 1,
|
||||
textDecoration: "none",
|
||||
color: "inherit",
|
||||
}}
|
||||
>
|
||||
<item.Icon size={16} />
|
||||
<span style={{ fontSize: "0.85rem", fontWeight: 500 }}>
|
||||
{item.label}
|
||||
</span>
|
||||
</Link>
|
||||
)}
|
||||
|
||||
<div
|
||||
style={{ display: "flex", alignItems: "center", gap: 6 }}
|
||||
>
|
||||
{item.badge && (
|
||||
<span
|
||||
style={{
|
||||
background: "#eef2ff",
|
||||
color: "#4f46e5",
|
||||
fontSize: "0.65rem",
|
||||
fontWeight: 600,
|
||||
padding: "2px 6px",
|
||||
borderRadius: 999,
|
||||
}}
|
||||
>
|
||||
{item.badge}
|
||||
</span>
|
||||
)}
|
||||
{item.hasChildren &&
|
||||
(isExpanded ? (
|
||||
<ChevronDown size={14} color="#a1a1aa" />
|
||||
) : (
|
||||
<ChevronRight size={14} color="#a1a1aa" />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Render Children if expanded */}
|
||||
{item.hasChildren && isExpanded && item.children && (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
marginTop: 2,
|
||||
}}
|
||||
>
|
||||
{item.children.map((child) => {
|
||||
const href = child.segment.includes("?")
|
||||
? `${projectBase}/${child.segment.split("?")[0]}?${child.segment.split("?")[1]}`
|
||||
: `${projectBase}/${child.segment}`;
|
||||
let isChildActive = false;
|
||||
if (child.segment.includes("?")) {
|
||||
const [basePath, searchStr] = child.segment.split("?");
|
||||
isChildActive =
|
||||
pathname === `${projectBase}/${basePath}` &&
|
||||
(typeof window !== "undefined"
|
||||
? window.location.search.includes(searchStr)
|
||||
: false);
|
||||
} else {
|
||||
isChildActive =
|
||||
pathname === `${projectBase}/${child.segment}`;
|
||||
}
|
||||
|
||||
return (
|
||||
<Link
|
||||
key={child.segment}
|
||||
href={href}
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
padding: "6px 10px 6px 14px",
|
||||
marginLeft: "18px",
|
||||
borderRadius: "0 8px 8px 0",
|
||||
fontSize: "0.8rem",
|
||||
fontWeight: 500,
|
||||
textDecoration: "none",
|
||||
color: isChildActive ? "#18181b" : "#52525b",
|
||||
background: "transparent",
|
||||
transition: "all 0.1s ease",
|
||||
borderLeft: isChildActive
|
||||
? "2px solid #18181b"
|
||||
: "2px solid transparent",
|
||||
}}
|
||||
>
|
||||
{child.label}
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ flex: 1, minWidth: 0, overflow: "auto", background: "#fff", display: "flex", flexDirection: "column" }}>
|
||||
<div
|
||||
style={{
|
||||
flex: 1,
|
||||
minWidth: 0,
|
||||
overflow: "auto",
|
||||
background: "#fff",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -37,6 +37,9 @@ export function ProjectIconRail({ workspace, projectId, actions }: Props) {
|
||||
fontSize: "0.75rem",
|
||||
fontWeight: 500,
|
||||
borderRadius: 6,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
height: 24, // Explicitly match device toggles height
|
||||
textDecoration: "none",
|
||||
background: isPreviewActive ? "#ffffff" : "transparent",
|
||||
color: isPreviewActive ? "#18181b" : "#71717a",
|
||||
@@ -53,6 +56,9 @@ export function ProjectIconRail({ workspace, projectId, actions }: Props) {
|
||||
fontSize: "0.75rem",
|
||||
fontWeight: 500,
|
||||
borderRadius: 6,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
height: 24, // Explicitly match device toggles height
|
||||
textDecoration: "none",
|
||||
background: !isPreviewActive ? "#ffffff" : "transparent",
|
||||
color: !isPreviewActive ? "#18181b" : "#71717a",
|
||||
@@ -237,6 +243,16 @@ function PreviewDeviceToggles() {
|
||||
}}
|
||||
/>
|
||||
|
||||
<span
|
||||
style={{
|
||||
opacity: 0.5,
|
||||
flexShrink: 0,
|
||||
paddingLeft: 4,
|
||||
fontFamily: "var(--font-mono), monospace",
|
||||
}}
|
||||
>
|
||||
/
|
||||
</span>
|
||||
<div
|
||||
style={{
|
||||
flex: 1,
|
||||
@@ -246,9 +262,14 @@ function PreviewDeviceToggles() {
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<select
|
||||
value={currentPath}
|
||||
onChange={(e) => setCurrentPath(e.target.value)}
|
||||
<input
|
||||
type="text"
|
||||
list="common-routes"
|
||||
value={currentPath === "/" ? "" : currentPath.replace(/^\//, "")}
|
||||
onChange={(e) =>
|
||||
setCurrentPath("/" + e.target.value.replace(/^\//, ""))
|
||||
}
|
||||
placeholder="path (e.g. dashboard)"
|
||||
style={{
|
||||
background: "transparent",
|
||||
border: "none",
|
||||
@@ -259,19 +280,22 @@ function PreviewDeviceToggles() {
|
||||
textOverflow: "ellipsis",
|
||||
fontFamily: "var(--font-mono), monospace",
|
||||
paddingRight: 16,
|
||||
appearance: "none",
|
||||
cursor: "pointer",
|
||||
}}
|
||||
>
|
||||
<option value="/">/</option>
|
||||
<option value="/dashboard">/dashboard</option>
|
||||
<option value="/login">/login</option>
|
||||
<option value="/signup">/signup</option>
|
||||
<option value="/about">/about</option>
|
||||
<option value="/contact">/contact</option>
|
||||
<option value="/pricing">/pricing</option>
|
||||
<option value="/settings">/settings</option>
|
||||
</select>
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
triggerRefresh(); // force reload iframe
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<datalist id="common-routes">
|
||||
<option value="dashboard" />
|
||||
<option value="login" />
|
||||
<option value="signup" />
|
||||
<option value="about" />
|
||||
<option value="contact" />
|
||||
<option value="pricing" />
|
||||
<option value="settings" />
|
||||
</datalist>
|
||||
<ChevronDown
|
||||
size={12}
|
||||
style={{
|
||||
@@ -401,12 +425,11 @@ const bar: React.CSSProperties = {
|
||||
alignItems: "center",
|
||||
flex: 1,
|
||||
minWidth: 0,
|
||||
height: "56px", // Explicitly set height
|
||||
height: "100%", // Inherit height from parent
|
||||
padding: "0 16px",
|
||||
gap: 12,
|
||||
boxSizing: "border-box",
|
||||
background: "#fafafa",
|
||||
borderBottom: "1px solid #e4e4e7",
|
||||
background: "#faf8f5",
|
||||
fontFamily: '"Outfit", "Inter", ui-sans-serif, sans-serif',
|
||||
};
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
Square,
|
||||
MousePointerClick,
|
||||
Paperclip,
|
||||
ChevronLeft,
|
||||
} from "lucide-react";
|
||||
import { ProjectIconRail } from "@/components/project/project-icon-rail";
|
||||
import {
|
||||
@@ -165,63 +166,63 @@ function friendlyToolName(name: string): string {
|
||||
"request.visual.qa": "Running visual QA",
|
||||
|
||||
// Core Platform Tools
|
||||
"projects.list": "📂 Listing workspace projects",
|
||||
"projects.get": "🗒️ Retrieving project spec sheets",
|
||||
"workspace.describe": "💼 Fetching workspace details",
|
||||
"gitea.credentials": "🔑 Resolving repository Git credentials",
|
||||
"projects.list": "Listing workspace projects",
|
||||
"projects.get": "Retrieving project spec sheets",
|
||||
"workspace.describe": "Fetching workspace details",
|
||||
"gitea.credentials": "Resolving repository Git credentials",
|
||||
"shell.exec": "Running command",
|
||||
ship: "Shipping",
|
||||
"generate.media": "📸 Generating visual media assets",
|
||||
"get.design.template": "📐 Retrieving design templates",
|
||||
"apps.templates.scaffold": "🧱 Scaffolding bento-grid layouts",
|
||||
"generate.media": "Generating visual media assets",
|
||||
"get.design.template": "Retrieving design templates",
|
||||
"apps.templates.scaffold": "Scaffolding bento-grid layouts",
|
||||
|
||||
// App & Database Deployment Tools
|
||||
"apps.list": "🖥️ Listing deployed web services",
|
||||
"apps.get": "🔍 Checking application build status",
|
||||
"apps.list": "Listing deployed web services",
|
||||
"apps.get": "Checking application build status",
|
||||
"apps.create": "Creating app",
|
||||
"apps.update": "⚙️ Updating application settings",
|
||||
"apps.delete": "❌ Removing deployed application",
|
||||
"apps.update": "Updating application settings",
|
||||
"apps.delete": "Removing deployed application",
|
||||
"apps.deploy": "Deploying app",
|
||||
"apps.deployments": "📜 Fetching recent deployment history",
|
||||
"apps.envs.list": "🔒 Loading environment variables",
|
||||
"apps.envs.upsert": "🔑 Injecting environment variables",
|
||||
"apps.envs.delete": "🗑️ Removing environment variables",
|
||||
"apps.domains.list": "🌐 Checking application domain routing",
|
||||
"apps.domains.set": "🔗 Binding custom domains",
|
||||
"apps.logs": "📋 Fetching application logs",
|
||||
"apps.exec": "🐚 Running command inside container",
|
||||
"databases.list": "🛢️ Listing database clusters",
|
||||
"databases.create": "🛢️ Provisioning database service",
|
||||
"databases.get": "🔌 Retrieving database connection credentials",
|
||||
"databases.update": "⚙️ Updating database configuration",
|
||||
"databases.delete": "🗑️ Removing database service",
|
||||
"apps.deployments": "Fetching recent deployment history",
|
||||
"apps.envs.list": "Loading environment variables",
|
||||
"apps.envs.upsert": "Injecting environment variables",
|
||||
"apps.envs.delete": "Removing environment variables",
|
||||
"apps.domains.list": "Checking application domain routing",
|
||||
"apps.domains.set": "Binding custom domains",
|
||||
"apps.logs": "Fetching application logs",
|
||||
"apps.exec": "Running command inside container",
|
||||
"databases.list": "Listing database clusters",
|
||||
"databases.create": "Provisioning database service",
|
||||
"databases.get": "Retrieving database connection credentials",
|
||||
"databases.update": "Updating database configuration",
|
||||
"databases.delete": "Removing database service",
|
||||
|
||||
// Domain & Git Tools
|
||||
"domains.search": "🔎 Searching open domain names",
|
||||
"domains.list": "🌐 Listing registered domains",
|
||||
"domains.get": "📄 Retrieving domain details",
|
||||
"domains.register": "💳 Registering domain name",
|
||||
"domains.attach": "🔌 Attaching domain reverse-proxy rules",
|
||||
"gitea.repos.list": "📦 Listing Gitea repositories",
|
||||
"gitea.repo.get": "🔍 Loading Gitea repository info",
|
||||
"gitea.repo.create": "🏗️ Initializing Gitea repository",
|
||||
"gitea.file.read": "📖 Reading file from Gitea",
|
||||
"gitea.file.write": "💾 Saving file to Gitea",
|
||||
"gitea.file.delete": "🗑️ Deleting file from Gitea",
|
||||
"gitea.branches.list": "🌿 Checking repository branches",
|
||||
"gitea.branch.create": "🌱 Creating Git branch",
|
||||
"devcontainer.ensure": "🐋 Spinning up secure Alpine dev container",
|
||||
"devcontainer.status": "💓 Probing dev container liveness",
|
||||
"devcontainer.suspend": "💤 Suspending dev container",
|
||||
"domains.search": "Searching open domain names",
|
||||
"domains.list": "Listing registered domains",
|
||||
"domains.get": "Retrieving domain details",
|
||||
"domains.register": "Registering domain name",
|
||||
"domains.attach": "Attaching domain reverse-proxy rules",
|
||||
"gitea.repos.list": "Listing Gitea repositories",
|
||||
"gitea.repo.get": "Loading Gitea repository info",
|
||||
"gitea.repo.create": "Initializing Gitea repository",
|
||||
"gitea.file.read": "Reading file from Gitea",
|
||||
"gitea.file.write": "Saving file to Gitea",
|
||||
"gitea.file.delete": "Deleting file from Gitea",
|
||||
"gitea.branches.list": "Checking repository branches",
|
||||
"gitea.branch.create": "Creating Git branch",
|
||||
"devcontainer.ensure": "Spinning up secure Alpine dev container",
|
||||
"devcontainer.status": "Probing dev container liveness",
|
||||
"devcontainer.suspend": "Suspending dev container",
|
||||
|
||||
// Planning / Specs Tools
|
||||
"plan.get": "📋 Loading specifications checklist",
|
||||
"plan.vision.set": "🎯 Saving feature product specification",
|
||||
"plan.idea.add": "💡 Adding planning ideation",
|
||||
"plan.task.add": "➕ Adding task to development roadmap",
|
||||
"plan.task.edit": "✏️ Updating development roadmap task",
|
||||
"plan.task.complete": "✅ Toggling checklist milestone as completed",
|
||||
"plan.document.update": "📝 Updating specs documentation",
|
||||
"plan.get": "Loading specifications checklist",
|
||||
"plan.vision.set": "Saving feature product specification",
|
||||
"plan.idea.add": "Adding planning ideation",
|
||||
"plan.task.add": "Adding task to development roadmap",
|
||||
"plan.task.edit": "Updating development roadmap task",
|
||||
"plan.task.complete": "Toggling checklist milestone as completed",
|
||||
"plan.document.update": "Updating specs documentation",
|
||||
};
|
||||
|
||||
return map[dotted] || dotted;
|
||||
@@ -1026,6 +1027,13 @@ export function ChatPanel({
|
||||
);
|
||||
const [showThreads, setShowThreads] = useState(false);
|
||||
const [mcpToken, setMcpToken] = useState<string | null>(null);
|
||||
const [isChatMinimized, setIsChatMinimized] = useState<boolean>(false);
|
||||
|
||||
// Auto-minimize when navigating to dashboard, auto-open when navigating to preview
|
||||
useEffect(() => {
|
||||
setIsChatMinimized(!pathname.includes("/preview"));
|
||||
}, [pathname]);
|
||||
|
||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||
const inputRef = useRef<HTMLTextAreaElement>(null);
|
||||
// AbortController for the in-flight /api/chat fetch. Lives in a ref
|
||||
@@ -2088,12 +2096,9 @@ export function ChatPanel({
|
||||
}}
|
||||
onInput={(e) => {
|
||||
const el = e.currentTarget;
|
||||
const newlines = (el.value.match(/\n/g) || []).length;
|
||||
if ((el as any).lastNewlines !== newlines) {
|
||||
(el as any).lastNewlines = newlines;
|
||||
el.style.height = "auto";
|
||||
el.style.height = Math.min(el.scrollHeight, 240) + "px";
|
||||
}
|
||||
// Only resize if height actually changed
|
||||
el.style.height = "auto";
|
||||
el.style.height = Math.min(el.scrollHeight, 240) + "px";
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
@@ -2319,7 +2324,7 @@ export function ChatPanel({
|
||||
alignItems: "stretch",
|
||||
flexShrink: 0,
|
||||
height: 48,
|
||||
borderBottom: "1px solid #e8e4dc",
|
||||
borderBottom: "1px solid #e4e4e7",
|
||||
background: "#faf8f5",
|
||||
boxSizing: "border-box",
|
||||
}}
|
||||
@@ -2334,7 +2339,7 @@ export function ChatPanel({
|
||||
padding: "0 12px",
|
||||
gap: 6,
|
||||
boxSizing: "border-box",
|
||||
borderRight: "1px solid #e8e4dc",
|
||||
borderRight: "1px solid #e4e4e7",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
@@ -2354,25 +2359,23 @@ export function ChatPanel({
|
||||
minWidth: 0,
|
||||
}}
|
||||
>
|
||||
{workspace ? (
|
||||
<Link
|
||||
href={`/${workspace}/projects`}
|
||||
title="All projects"
|
||||
style={{ flexShrink: 0, display: "flex" }}
|
||||
>
|
||||
<img
|
||||
src="/vibn-black-circle-logo.png"
|
||||
alt="VIBN"
|
||||
width={26}
|
||||
height={26}
|
||||
style={{
|
||||
borderRadius: 7,
|
||||
objectFit: "cover",
|
||||
display: "block",
|
||||
}}
|
||||
/>
|
||||
</Link>
|
||||
) : null}
|
||||
<Link
|
||||
href={workspace ? `/${workspace}/projects` : "/"}
|
||||
title="All projects"
|
||||
style={{ flexShrink: 0, display: "flex" }}
|
||||
>
|
||||
<img
|
||||
src="/vibn-black-circle-logo.png"
|
||||
alt="VIBN"
|
||||
width={26}
|
||||
height={26}
|
||||
style={{
|
||||
borderRadius: 7,
|
||||
objectFit: "cover",
|
||||
display: "block",
|
||||
}}
|
||||
/>
|
||||
</Link>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowThreads((v) => !v)}
|
||||
@@ -2493,21 +2496,80 @@ export function ChatPanel({
|
||||
flexDirection: "row",
|
||||
minHeight: 0,
|
||||
minWidth: 0,
|
||||
position: "relative", // Ensure relative positioning for z-index stacking context
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
width: 380,
|
||||
width: isChatMinimized ? 52 : 380,
|
||||
flexShrink: 0,
|
||||
borderRight: "1px solid #e8e4dc",
|
||||
borderRight: "1px solid #e4e4e7",
|
||||
background: "#fff",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
minHeight: 0,
|
||||
minWidth: 0,
|
||||
transition: "width 0.2s ease-in-out",
|
||||
overflow: "visible", // Changed from hidden to visible so the collapse button can hang over the edge
|
||||
position: "relative",
|
||||
zIndex: 40, // High z-index to pop over the right panel
|
||||
}}
|
||||
>
|
||||
{structuralChatBody}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
zIndex: 50,
|
||||
right: -12,
|
||||
top: 12,
|
||||
}}
|
||||
>
|
||||
<button
|
||||
onClick={() => setIsChatMinimized(!isChatMinimized)}
|
||||
style={{
|
||||
background: "#fff",
|
||||
border: "1px solid #e4e4e7",
|
||||
width: 24,
|
||||
height: 24,
|
||||
borderRadius: "50%",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
cursor: "pointer",
|
||||
color: "#a1a1aa",
|
||||
boxShadow: "0 2px 4px rgba(0,0,0,0.05)",
|
||||
}}
|
||||
title={isChatMinimized ? "Expand Chat" : "Minimize Chat"}
|
||||
>
|
||||
{isChatMinimized ? (
|
||||
<ChevronRight size={14} />
|
||||
) : (
|
||||
<ChevronLeft size={14} />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{isChatMinimized ? (
|
||||
<div
|
||||
style={{
|
||||
flex: 1,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
paddingTop: 16,
|
||||
}}
|
||||
></div>
|
||||
) : (
|
||||
<div
|
||||
style={{
|
||||
flex: 1,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
{structuralChatBody}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
key={pathname}
|
||||
@@ -2519,6 +2581,8 @@ export function ChatPanel({
|
||||
background: "#faf8f5",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
position: "relative",
|
||||
zIndex: 10, // Lower z-index so the collapse button stays on top
|
||||
}}
|
||||
>
|
||||
{artifactSlot}
|
||||
@@ -2604,25 +2668,23 @@ export function ChatPanel({
|
||||
minWidth: 0,
|
||||
}}
|
||||
>
|
||||
{workspace ? (
|
||||
<Link
|
||||
href={`/${workspace}/projects`}
|
||||
title="All projects"
|
||||
style={{ flexShrink: 0, display: "flex" }}
|
||||
>
|
||||
<img
|
||||
src="/vibn-black-circle-logo.png"
|
||||
alt="VIBN"
|
||||
width={26}
|
||||
height={26}
|
||||
style={{
|
||||
borderRadius: 7,
|
||||
objectFit: "cover",
|
||||
display: "block",
|
||||
}}
|
||||
/>
|
||||
</Link>
|
||||
) : null}
|
||||
<Link
|
||||
href={workspace ? `/${workspace}/projects` : "/"}
|
||||
title="All projects"
|
||||
style={{ flexShrink: 0, display: "flex" }}
|
||||
>
|
||||
<img
|
||||
src="/vibn-black-circle-logo.png"
|
||||
alt="VIBN"
|
||||
width={26}
|
||||
height={26}
|
||||
style={{
|
||||
borderRadius: 7,
|
||||
objectFit: "cover",
|
||||
display: "block",
|
||||
}}
|
||||
/>
|
||||
</Link>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowThreads((v) => !v)}
|
||||
|
||||
Reference in New Issue
Block a user