feat: add project connection to chat

This commit is contained in:
sai karthik
2026-07-26 21:00:08 +05:30
parent 39b27a229f
commit 58ff7942bd
4 changed files with 173 additions and 1 deletions

View File

@@ -58,6 +58,8 @@ export const MobileFlowPage = ({ screen }: MobileFlowPageProps) => {
assistantError: projectWorkspace.error?.message,
localStatus: workspace.statusMessage,
});
const handleRepositoryChange =
projectWorkspace.projectWorkspace.setRepository;
const handleOpenUnit = (selectedWorkUnitId?: string) => {
const targetId =
@@ -135,6 +137,7 @@ export const MobileFlowPage = ({ screen }: MobileFlowPageProps) => {
data={projectWorkspace.data}
modelId={MODEL_ID}
modelLabel={MODEL_LABEL}
onAddProject={() => workspace.setProjectManagerOpen(true)}
onComposerChange={workspace.handleComposerChange}
onComposerSubmit={workspace.handleComposerSubmit}
onOpenUnit={(selectedWorkUnitId) =>
@@ -145,6 +148,12 @@ export const MobileFlowPage = ({ screen }: MobileFlowPageProps) => {
projectId as Id<"projects">
)
}
onProjectManagerClose={() => workspace.setProjectManagerOpen(false)}
onRepositoryChange={handleRepositoryChange}
onRepositoryConnect={(event) => {
event.preventDefault();
void projectWorkspace.projectWorkspace.connectRepository();
}}
onRestoreChecked={handleRestoreChecked}
onSettingsClose={() => workspace.setSettingsOpen(false)}
onSettingsOpen={() => workspace.setSettingsOpen(true)}
@@ -153,6 +162,9 @@ export const MobileFlowPage = ({ screen }: MobileFlowPageProps) => {
}}
onWorkUnitChecked={handleWorkUnitChecked}
onWorkUnitSentBack={handleWorkUnitSentBack}
pendingAction={projectWorkspace.projectWorkspace.pendingAction}
projectManagerOpen={workspace.projectManagerOpen}
repositoryValue={projectWorkspace.projectWorkspace.repository}
settingsOpen={workspace.settingsOpen}
statusMessage={statusMessage}
userEmail={user?.email}

View File

@@ -0,0 +1,118 @@
import { LoaderCircle, X } from "lucide-react";
import type { FormEvent } from "react";
import type { MobileWorkspaceView } from "./models";
export interface MobileProjectManagerPanelProps {
readonly data: MobileWorkspaceView;
readonly onClose: () => void;
readonly onProjectSelect: (projectId: string) => void;
readonly onRepositoryChange: (value: string) => void;
readonly onRepositoryConnect: (event: FormEvent<HTMLFormElement>) => void;
readonly pendingAction?: string | null;
readonly repositoryValue: string;
readonly statusMessage?: string;
}
export const MobileProjectManagerPanel = ({
data,
onClose,
onProjectSelect,
onRepositoryChange,
onRepositoryConnect,
pendingAction,
repositoryValue,
statusMessage,
}: MobileProjectManagerPanelProps) => {
const connecting = pendingAction === "connect";
return (
<section
aria-label="Add project"
className="absolute inset-0 z-40 flex flex-col bg-[#f7f8f5] text-[#11110f]"
>
<header className="flex h-20 shrink-0 items-center border-b border-[#e9eae6] bg-white px-4">
<div>
<h2 className="text-[18px] font-semibold">Projects</h2>
<p className="text-[11px] text-[#807b76]">
Connect a repository to Zopu.
</p>
</div>
<button
aria-label="Close projects"
className="ml-auto flex size-10 items-center justify-center rounded-full bg-[#f2f3ef]"
onClick={onClose}
type="button"
>
<X className="size-5" />
</button>
</header>
<div className="min-h-0 flex-1 overflow-y-auto p-4">
<div className="rounded-[24px] bg-white p-4">
<label
className="text-[10px] font-semibold tracking-[0.08em] text-[#807b76] uppercase"
htmlFor="stack-project-selector"
>
Connected project
</label>
<select
className="mt-2 h-11 w-full rounded-[14px] bg-[#f2f3ef] px-3 text-[13px] font-semibold outline-none focus-visible:ring-2 focus-visible:ring-[#315dc0]"
disabled={data.projects.length === 0}
id="stack-project-selector"
onChange={(event) => onProjectSelect(event.currentTarget.value)}
value={data.selectedProjectId ?? ""}
>
{data.projects.length === 0 ? (
<option value="">No connected projects</option>
) : (
data.projects.map((project) => (
<option key={project.id} value={project.id}>
{project.repositoryPath ?? project.name}
</option>
))
)}
</select>
<form className="mt-5" onSubmit={onRepositoryConnect}>
<label
className="text-[10px] font-semibold tracking-[0.08em] text-[#807b76] uppercase"
htmlFor="stack-repository-url"
>
Add project
</label>
<input
aria-label="Git repository URL"
className="mt-2 h-12 w-full rounded-[14px] bg-[#f2f3ef] px-3 text-[12px] outline-none placeholder:text-[#aaa7a3] focus-visible:ring-2 focus-visible:ring-[#315dc0]"
disabled={connecting}
id="stack-repository-url"
onChange={(event) =>
onRepositoryChange(event.currentTarget.value)
}
placeholder="https://git.openputer.com/owner/repo.git"
required
type="url"
value={repositoryValue}
/>
<button
className="mt-3 flex h-11 w-full items-center justify-center gap-2 rounded-[14px] bg-[#c8ff00] text-[13px] font-semibold text-black disabled:opacity-50"
disabled={connecting || !repositoryValue.trim()}
type="submit"
>
{connecting ? (
<LoaderCircle className="size-4 animate-spin" />
) : null}
{connecting ? "Connecting…" : "Connect project"}
</button>
</form>
</div>
{statusMessage ? (
<p className="mt-3 text-center text-[11px] text-[#807b76]">
{statusMessage}
</p>
) : null}
</div>
</section>
);
};

View File

@@ -1,11 +1,12 @@
import { cn } from "@code/ui/lib/utils";
import { ChevronDown } from "lucide-react";
import { ChevronDown, Plus } from "lucide-react";
import type { MobileProjectView } from "./models";
export interface MobileProjectSelectProps {
readonly activeCount: number;
readonly className?: string;
readonly onAddProject: () => void;
readonly onProjectSelect: (projectId: string) => void;
readonly projects: readonly MobileProjectView[];
readonly selectedProjectId?: string;
@@ -14,6 +15,7 @@ export interface MobileProjectSelectProps {
export const MobileProjectSelect = ({
activeCount,
className,
onAddProject,
onProjectSelect,
projects,
selectedProjectId,
@@ -49,5 +51,14 @@ export const MobileProjectSelect = ({
</label>
<span aria-hidden="true">·</span>
<span className="shrink-0 tabular-nums">{activeCount} active</span>
<button
aria-label="Add project"
className="ml-0.5 flex size-5 shrink-0 items-center justify-center rounded-full bg-[#c8ff00] text-black"
onClick={onAddProject}
title="Add project"
type="button"
>
<Plus className="size-3.5" strokeWidth={2.5} />
</button>
</div>
);

View File

@@ -10,6 +10,7 @@ import {
} from "lucide-react";
import type { FormEvent, MouseEvent } from "react";
import { MobileProjectManagerPanel } from "./mobile-project-manager-panel";
import { MobileProjectSelect } from "./mobile-project-select";
import { MobileSettingsPanel } from "./mobile-settings-panel";
import type {
@@ -42,6 +43,7 @@ const dateLabel = new Intl.DateTimeFormat("en", {
interface WorkStackHeaderProps {
readonly activeCount: number;
readonly onAddProject: () => void;
readonly onOpenSettings?: () => void;
readonly onProjectSelect: (projectId: string) => void;
readonly projects: readonly MobileProjectView[];
@@ -51,6 +53,7 @@ interface WorkStackHeaderProps {
const WorkStackHeader = ({
activeCount,
onAddProject,
onOpenSettings,
onProjectSelect,
projects,
@@ -67,6 +70,7 @@ const WorkStackHeader = ({
</h1>
<MobileProjectSelect
activeCount={activeCount}
onAddProject={onAddProject}
onProjectSelect={onProjectSelect}
projects={projects}
selectedProjectId={selectedProjectId}
@@ -298,16 +302,23 @@ export interface MobileWorkStackScreenProps {
readonly data: MobileWorkspaceView;
readonly modelId: string;
readonly modelLabel: string;
readonly onAddProject: () => void;
readonly onComposerChange: (value: string) => void;
readonly onComposerSubmit: (event: FormEvent<HTMLFormElement>) => void;
readonly onOpenUnit: (workUnitId: string) => void;
readonly onProjectSelect: (projectId: string) => void;
readonly onProjectManagerClose: () => void;
readonly onRepositoryChange: (value: string) => void;
readonly onRepositoryConnect: (event: FormEvent<HTMLFormElement>) => void;
readonly onRestoreChecked: () => void;
readonly onSettingsClose: () => void;
readonly onSettingsOpen: () => void;
readonly onSignOut?: () => void;
readonly onWorkUnitChecked: (workUnitId: string) => void;
readonly onWorkUnitSentBack: (workUnitId: string) => void;
readonly pendingAction?: string | null;
readonly projectManagerOpen: boolean;
readonly repositoryValue: string;
readonly settingsOpen: boolean;
readonly statusMessage?: string;
readonly userEmail?: string;
@@ -322,16 +333,23 @@ export const MobileWorkStackScreen = ({
data,
modelId,
modelLabel,
onAddProject,
onComposerChange,
onComposerSubmit,
onOpenUnit,
onProjectManagerClose,
onProjectSelect,
onRepositoryChange,
onRepositoryConnect,
onRestoreChecked,
onSettingsClose,
onSettingsOpen,
onSignOut,
onWorkUnitChecked,
onWorkUnitSentBack,
pendingAction,
projectManagerOpen,
repositoryValue,
settingsOpen,
statusMessage,
userEmail,
@@ -350,6 +368,7 @@ export const MobileWorkStackScreen = ({
<div className="relative mx-auto flex h-[100dvh] max-h-[858px] min-h-[700px] w-full max-w-[390px] flex-col overflow-hidden bg-white text-[#0b0b0a]">
<WorkStackHeader
activeCount={data.activeCount}
onAddProject={onAddProject}
onOpenSettings={onSettingsOpen}
onProjectSelect={onProjectSelect}
projects={data.projects}
@@ -458,6 +477,18 @@ export const MobileWorkStackScreen = ({
statusMessage={statusMessage}
value={composerValue}
/>
{projectManagerOpen ? (
<MobileProjectManagerPanel
data={data}
onClose={onProjectManagerClose}
onProjectSelect={onProjectSelect}
onRepositoryChange={onRepositoryChange}
onRepositoryConnect={onRepositoryConnect}
pendingAction={pendingAction}
repositoryValue={repositoryValue}
statusMessage={statusMessage}
/>
) : null}
{settingsOpen ? (
<MobileSettingsPanel
modelId={modelId}