Integrate mobile chat workspace and OpenRouter agent flow

This commit is contained in:
sai karthik
2026-07-25 17:49:11 +05:30
parent a17aa5c283
commit 48200a11df
24 changed files with 1142 additions and 211 deletions

View File

@@ -0,0 +1,161 @@
"use client";
import {
InputGroup,
InputGroupButton,
InputGroupTextarea,
} from "@code/ui/components/input-group";
import { cn } from "@code/ui/lib/utils";
import type { ChatStatus } from "ai";
import { ArrowUpIcon, LoaderCircleIcon, SquareIcon } from "lucide-react";
import type {
ComponentProps,
FormEvent,
FormEventHandler,
HTMLAttributes,
KeyboardEventHandler,
ReactNode,
} from "react";
export interface PromptInputMessage {
files: [];
text: string;
}
export type PromptInputProps = Omit<ComponentProps<"form">, "onSubmit"> & {
onSubmit: (
message: PromptInputMessage,
event: FormEvent<HTMLFormElement>
) => Promise<void> | void;
};
export const PromptInput = ({
children,
className,
onSubmit,
...props
}: PromptInputProps) => {
const handleSubmit: FormEventHandler<HTMLFormElement> = (event) => {
event.preventDefault();
const text = new FormData(event.currentTarget)
.get("message")
?.toString()
.trim();
if (!text) {
return;
}
const submitMessage = async () => {
try {
await onSubmit({ files: [], text }, event);
} catch {
// The owning chat hook exposes submission failures in the composer status.
}
};
void submitMessage();
};
return (
<form
className={cn("w-full", className)}
onSubmit={handleSubmit}
{...props}
>
<InputGroup>{children}</InputGroup>
</form>
);
};
export type PromptInputBodyProps = HTMLAttributes<HTMLDivElement>;
export const PromptInputBody = ({
className,
...props
}: PromptInputBodyProps) => (
<div className={cn("contents", className)} {...props} />
);
export type PromptInputTextareaProps = ComponentProps<
typeof InputGroupTextarea
>;
export const PromptInputTextarea = ({
className,
onKeyDown,
...props
}: PromptInputTextareaProps) => {
const handleKeyDown: KeyboardEventHandler<HTMLTextAreaElement> = (event) => {
onKeyDown?.(event);
if (
event.defaultPrevented ||
event.key !== "Enter" ||
event.shiftKey ||
event.nativeEvent.isComposing
) {
return;
}
event.preventDefault();
event.currentTarget.form?.requestSubmit();
};
return (
<InputGroupTextarea
className={cn("field-sizing-content", className)}
name="message"
onKeyDown={handleKeyDown}
{...props}
/>
);
};
export type PromptInputButtonProps = ComponentProps<typeof InputGroupButton>;
export const PromptInputButton = ({
className,
size = "icon-sm",
...props
}: PromptInputButtonProps) => (
<InputGroupButton
className={cn(className)}
size={size}
type="button"
{...props}
/>
);
export type PromptInputSubmitProps = ComponentProps<typeof InputGroupButton> & {
status?: ChatStatus;
};
export const PromptInputSubmit = ({
children,
className,
disabled,
size = "icon-sm",
status = "ready",
...props
}: PromptInputSubmitProps) => {
const isBusy = status === "submitted" || status === "streaming";
let content: ReactNode = children ?? <ArrowUpIcon className="size-4" />;
if (status === "streaming") {
content = <SquareIcon className="size-4 fill-current" />;
} else if (status === "submitted") {
content = <LoaderCircleIcon className="size-4 animate-spin" />;
}
return (
<InputGroupButton
aria-busy={isBusy}
className={cn(className)}
disabled={disabled}
size={size}
type="submit"
{...props}
>
{content}
</InputGroupButton>
);
};

View File

@@ -8,7 +8,6 @@ import {
FileText,
ListChecks,
LoaderCircle,
Mic,
MoreHorizontal,
Paperclip,
Plus,
@@ -17,6 +16,23 @@ import {
} from "lucide-react";
import type { FormEvent, ReactNode } from "react";
import {
Conversation,
ConversationContent,
ConversationScrollButton,
} from "./ai-elements/conversation";
import {
Message,
MessageContent,
MessageResponse,
} from "./ai-elements/message";
import {
PromptInput,
PromptInputBody,
PromptInputButton,
PromptInputSubmit,
PromptInputTextarea,
} from "./ai-elements/prompt-input";
import type {
MobileProjectView,
MobileWorkspaceView,
@@ -36,21 +52,32 @@ export interface MobileWorkspaceRendererProps {
createIssueBody: string;
createIssueTitle: string;
data: MobileWorkspaceView;
modelId?: string;
modelLabel?: string;
onBack?: () => void;
onComposerChange: (value: string) => void;
onComposerMessageSubmit?: (message: string) => Promise<void>;
onComposerSubmit: (event: FormEvent<HTMLFormElement>) => void;
onCreateBodyChange: (value: string) => void;
onCreateIssue: (event: FormEvent<HTMLFormElement>) => void;
onCreateIssueFromSignal?: () => void;
onCreateTitleChange: (value: string) => void;
onManageProjects?: () => void;
onProjectManagerClose?: () => void;
onOpenAssistant?: () => void;
onOpenUnit?: (workUnitId?: string) => void;
onProjectSelect?: (projectId: string) => void;
onRepositoryChange?: (value: string) => void;
onRepositoryConnect?: (event: FormEvent<HTMLFormElement>) => void;
onSettingsClose?: () => void;
onSettingsOpen?: () => void;
onReviewUnit?: (reviewUrl: string) => void;
onStartUnit?: (workUnitId: string) => void;
onViewWork?: () => void;
pendingAction?: string | null;
projectManagerOpen?: boolean;
repositoryValue?: string;
settingsOpen?: boolean;
statusMessage?: string;
variant: MobileWorkspaceVariant;
}
@@ -294,7 +321,9 @@ const ProjectSwitcher = ({
) : (
data.projects.map((project: MobileProjectView) => (
<option key={project.id} value={project.id}>
{project.name} {project.connected ? "" : "(not connected)"}
{project.repositoryPath ?? project.name}
{project.host ? ` · ${project.host}` : ""}
{project.connected ? "" : " (not connected)"}
</option>
))
)}
@@ -310,6 +339,184 @@ const ProjectSwitcher = ({
</div>
);
const ProjectManagementPanel = ({
createIssueBody,
createIssueTitle,
data,
onClose,
onCreateBodyChange,
onCreateIssue,
onCreateTitleChange,
onProjectSelect,
onRepositoryChange,
onRepositoryConnect,
pendingAction,
repositoryValue = "",
statusMessage,
}: {
readonly createIssueBody: string;
readonly createIssueTitle: string;
readonly data: MobileWorkspaceView;
readonly onClose?: () => void;
readonly onCreateBodyChange: (value: string) => void;
readonly onCreateIssue: (event: FormEvent<HTMLFormElement>) => void;
readonly onCreateTitleChange: (value: string) => 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;
}) => {
const connecting = pendingAction === "connect";
return (
<section
aria-label="Project workspace"
className="absolute inset-0 z-30 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">Project workspace</h2>
<p className="text-[11px] text-[#807b76]">
Connect repositories and create durable work.
</p>
</div>
<button
aria-label="Close project workspace"
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 px-4 py-4">
<div className="rounded-[22px] bg-white p-4">
<p className="text-[10px] font-semibold tracking-[0.08em] text-[#807b76] uppercase">
Connected project
</p>
<select
aria-label="Active project"
className="mt-2 h-11 w-full rounded-[14px] bg-[#f2f3ef] px-3 text-[13px] font-semibold outline-none"
disabled={data.projects.length === 0}
onChange={(event) => onProjectSelect?.(event.target.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-4" onSubmit={onRepositoryConnect}>
<label
className="text-[10px] font-semibold tracking-[0.08em] text-[#807b76] uppercase"
htmlFor="mobile-repository-url"
>
Connect another repository
</label>
<input
aria-label="Public Git repository URL"
className="mt-2 h-11 w-full rounded-[14px] bg-[#f2f3ef] px-3 text-[12px] outline-none placeholder:text-[#aaa7a3]"
disabled={connecting}
id="mobile-repository-url"
onChange={(event) => onRepositoryChange?.(event.target.value)}
placeholder="https://git.openputer.com/owner/repo.git"
required
type="url"
value={repositoryValue}
/>
<button
className="mt-2 flex h-10 w-full items-center justify-center gap-2 rounded-[14px] bg-[#c8ff00] text-[12px] font-semibold text-black disabled:opacity-50"
disabled={connecting || !repositoryValue.trim()}
type="submit"
>
{connecting ? (
<LoaderCircle className="size-3.5 animate-spin" />
) : null}
{connecting ? "Connecting…" : "Connect repository"}
</button>
</form>
</div>
{data.selectedProjectId ? (
<div className="mt-4">
<CreateWorkForm
body={createIssueBody}
onBodyChange={onCreateBodyChange}
onSubmit={onCreateIssue}
onTitleChange={onCreateTitleChange}
pending={pendingAction === "issue"}
title={createIssueTitle}
/>
</div>
) : null}
{statusMessage ? (
<p className="mt-3 text-center text-[11px] text-[#807b76]">
{statusMessage}
</p>
) : null}
</div>
</section>
);
};
const ChatSettingsPanel = ({
modelId = "openrouter/openai/gpt-oss-20b:free",
modelLabel = "GPT-OSS 20B Free",
onClose,
}: {
readonly modelId?: string;
readonly modelLabel?: string;
readonly onClose?: () => void;
}) => (
<section
aria-label="Chat settings"
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">Settings</h2>
<p className="text-[11px] text-[#807b76]">
Configure how Zopu responds.
</p>
</div>
<button
aria-label="Close settings"
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="p-4">
<div className="rounded-[22px] bg-white p-4">
<label
className="text-[10px] font-semibold tracking-[0.08em] text-[#807b76] uppercase"
htmlFor="mobile-chat-model"
>
Chat model
</label>
<select
className="mt-2 h-11 w-full rounded-[14px] bg-[#f2f3ef] px-3 text-[13px] font-semibold outline-none"
id="mobile-chat-model"
value={modelId}
>
<option value={modelId}>{modelLabel}</option>
</select>
<p className="mt-2 text-[11px] leading-4 text-[#807b76]">
A tool-capable free OpenRouter model is active for the local Zopu
agent.
</p>
</div>
</div>
</section>
);
const WorkUnitActions = ({
onReview,
onStart,
@@ -1224,16 +1431,126 @@ const assistantStatusDotClass: Record<
red: "bg-[#e04b3f]",
};
const ChatWorkUnitCard = ({
onOpen,
onReview,
onStart,
pendingAction,
workUnit,
}: {
readonly onOpen?: (workUnitId?: string) => void;
readonly onReview?: (reviewUrl: string) => void;
readonly onStart?: (workUnitId: string) => void;
readonly pendingAction?: string | null;
readonly workUnit?: MobileWorkUnitView;
}) => {
if (!workUnit) {
return null;
}
return (
<article
className={cn(
"mt-5 rounded-[22px] p-[14px]",
workUnitToneClass[workUnit.tone]
)}
>
<button
className="block w-full text-left"
onClick={() => onOpen?.(workUnit.id)}
type="button"
>
<div className="flex items-center gap-2">
<span
className={cn(
"text-[10px] font-semibold uppercase",
workUnitStatusClass[workUnit.tone]
)}
>
{workUnit.statusLabel}
</span>
<span className="text-[10px] opacity-60">{workUnit.code}</span>
<ChevronRight className="ml-auto size-4" />
</div>
<h2 className="mt-2 text-[17px] leading-5 font-semibold">
{workUnit.title}
</h2>
<p className="mt-1 line-clamp-2 text-[12px] leading-4 opacity-70">
{workUnit.summary}
</p>
<div className="mt-3 h-1.5 rounded-full bg-current/10">
<div
className="h-full rounded-full bg-current"
style={{ width: `${workUnit.progress}%` }}
/>
</div>
<div className="mt-1.5 flex text-[10px] font-semibold">
<span>{workUnit.progress}% complete</span>
<span className="ml-auto opacity-60">{workUnit.nextAction}</span>
</div>
</button>
<WorkUnitActions
onReview={onReview}
onStart={onStart}
pending={pendingAction === `issue:${workUnit.id}`}
workUnit={workUnit}
/>
</article>
);
};
const CharacterChat = ({
createIssueBody,
createIssueTitle,
data,
modelId,
modelLabel,
onChange,
onMessageSubmit,
onCreateIssueFromSignal,
onSubmit,
onCreateBodyChange,
onCreateIssue,
onCreateTitleChange,
onManageProjects,
onOpen,
onProjectManagerClose,
onProjectSelect,
onRepositoryChange,
onRepositoryConnect,
onReview,
onSettingsClose,
onSettingsOpen,
onStart,
pendingAction,
projectManagerOpen,
repositoryValue,
settingsOpen,
statusMessage,
value,
}: ComposerProps & {
createIssueBody: string;
createIssueTitle: string;
data: MobileWorkspaceView;
modelId?: string;
modelLabel?: string;
onCreateBodyChange: (value: string) => void;
onCreateIssue: (event: FormEvent<HTMLFormElement>) => void;
onCreateIssueFromSignal?: () => void;
onCreateTitleChange: (value: string) => void;
onManageProjects?: () => void;
onOpen?: (workUnitId?: string) => void;
onProjectManagerClose?: () => void;
onProjectSelect?: (projectId: string) => void;
onRepositoryChange?: (value: string) => void;
onRepositoryConnect?: (event: FormEvent<HTMLFormElement>) => void;
onMessageSubmit?: (message: string) => Promise<void>;
onReview?: (reviewUrl: string) => void;
onSettingsClose?: () => void;
onSettingsOpen?: () => void;
onStart?: (workUnitId: string) => void;
pendingAction?: string | null;
projectManagerOpen?: boolean;
repositoryValue?: string;
settingsOpen?: boolean;
}) => (
<div className="mx-auto h-[min(100dvh,858px)] w-full max-w-[390px] overflow-hidden bg-black">
<main className="relative h-full overflow-hidden rounded-[30px] bg-white px-4 text-[#11110f]">
@@ -1253,146 +1570,218 @@ const CharacterChat = ({
{data.assistant.statusLabel}
</p>
</div>
<MoreHorizontal className="ml-auto size-5" />
<button
aria-label="Open settings"
className="ml-auto flex size-9 items-center justify-center rounded-full bg-[#f2f3ef]"
onClick={onSettingsOpen}
type="button"
>
<MoreHorizontal className="size-5" />
</button>
</header>
<section className="absolute inset-x-4 top-[106px] bottom-[105px] overflow-y-auto pb-6">
{data.assistant.messages.length === 0 ? (
<>
<div className="w-fit rounded-full bg-[#f4f4f2] px-2.5 py-1.5 text-[10px] text-[#aaa7a3]">
TODAY
</div>
<div className="mt-10 flex items-center gap-2 text-[14px] text-[#55524f]">
<span className="flex size-6 items-center justify-center rounded-[8px] bg-[#f4f4f2]">
<Sparkles className="size-4" />
</span>
<strong>Zopu</strong>
<span className="text-[#b5b2ae]"> a fresh start</span>
</div>
<p className="mt-3 text-[17px] leading-5">
Good morning. What are we making clearer today?
</p>
<div className="mt-4 flex gap-7 text-[12px] font-semibold text-[#55524f]">
<button
className="flex items-center gap-2"
onClick={() => onChange("Summarize my latest project signals")}
type="button"
>
<FileText className="size-4" />
Summarize
</button>
<button
className="flex items-center gap-2"
onClick={() => onChange("Plan the next steps for my project")}
type="button"
>
<ListChecks className="size-4" />
Plan
</button>
<button
className="flex items-center gap-2"
onClick={() => onChange("Explore risks in my active work")}
type="button"
>
Explore
</button>
</div>
{data.latestSignal ? (
<section className="mt-12 rounded-[22px] bg-[#f4f4f2] p-[14px]">
<div className="flex items-center">
<span className="flex size-8 items-center justify-center rounded-[10px] bg-[#c8ff00]">
</span>
<div className="ml-3 min-w-0">
<h2 className="truncate text-[15px] leading-4 font-semibold">
{data.latestSignal.title}
</h2>
<p className="text-[10px] text-[#b5b2ae]">
Latest project signal
</p>
</div>
<ChevronRight className="ml-auto size-4" />
</div>
<p className="mt-3 text-[13px] leading-[18px]">
{data.latestSignal.summary}
</p>
<div className="absolute inset-x-0 top-[82px]">
<ProjectSwitcher
data={data}
onManageProjects={onManageProjects}
onProjectSelect={onProjectSelect}
/>
</div>
<Conversation className="absolute inset-x-4 top-[127px] bottom-[105px]">
<ConversationContent className="min-h-full gap-0 p-0 pb-6">
{data.assistant.messages.length === 0 ? (
<>
<div className="w-fit rounded-full bg-[#f4f4f2] px-2.5 py-1.5 text-[10px] text-[#aaa7a3]">
TODAY
</div>
<div className="mt-10 flex items-center gap-2 text-[14px] text-[#55524f]">
<span className="flex size-6 items-center justify-center rounded-[8px] bg-[#f4f4f2]">
<Sparkles className="size-4" />
</span>
<strong>Zopu</strong>
<span className="text-[#b5b2ae]"> a fresh start</span>
</div>
<p className="mt-3 text-[17px] leading-5">
Good morning. What are we making clearer today?
</p>
<div className="mt-4 flex gap-7 text-[12px] font-semibold text-[#55524f]">
<button
className="mt-3 h-8 rounded-[11px] bg-black px-3 text-[11px] font-semibold text-white"
onClick={onCreateIssueFromSignal}
className="flex items-center gap-2"
onClick={() =>
onChange("Summarize my latest project signals")
}
type="button"
>
Create work from signal
<FileText className="size-4" />
Summarize
</button>
</section>
) : null}
</>
) : (
<div className="grid gap-5">
{data.assistant.messages.map((message) =>
message.role === "user" ? (
<div
className="ml-auto max-w-[86%] rounded-[22px] bg-[#0c0c0b] px-4 py-3 text-[16px] leading-6 whitespace-pre-wrap text-white"
<button
className="flex items-center gap-2"
onClick={() => onChange("Plan the next steps for my project")}
type="button"
>
<ListChecks className="size-4" />
Plan
</button>
<button
className="flex items-center gap-2"
onClick={() => onChange("Explore risks in my active work")}
type="button"
>
Explore
</button>
</div>
{data.latestSignal ? (
<section className="mt-12 rounded-[22px] bg-[#f4f4f2] p-[14px]">
<div className="flex items-center">
<span className="flex size-8 items-center justify-center rounded-[10px] bg-[#c8ff00]">
</span>
<div className="ml-3 min-w-0">
<h2 className="truncate text-[15px] leading-4 font-semibold">
{data.latestSignal.title}
</h2>
<p className="text-[10px] text-[#b5b2ae]">
Latest project signal
</p>
</div>
<ChevronRight className="ml-auto size-4" />
</div>
<p className="mt-3 text-[13px] leading-[18px]">
{data.latestSignal.summary}
</p>
<button
className="mt-3 h-8 rounded-[11px] bg-black px-3 text-[11px] font-semibold text-white"
onClick={onCreateIssueFromSignal}
type="button"
>
Create work from signal
</button>
</section>
) : null}
</>
) : (
<div className="grid gap-5">
{data.assistant.messages.map((message) => (
<Message
className="max-w-full gap-0"
from={message.role}
key={message.id}
>
{message.text}
</div>
) : (
<div key={message.id}>
<div className="flex items-center gap-2">
<span className="flex size-7 items-center justify-center rounded-[9px] bg-[#c8ff00]">
<Sparkles className="size-4" />
</span>
<strong className="text-[14px]">Zopu</strong>
<span className="rounded-full bg-[#f1f1ef] px-2 py-1 text-[9px] text-[#aaa7a3]">
THINKING PARTNER
</span>
</div>
<p className="mt-3 text-[16px] leading-[21px] whitespace-pre-wrap">
{message.text}
</p>
</div>
)
)}
{data.assistant.isBusy ? (
<p className="text-[13px] text-[#807b76]">Zopu is thinking</p>
) : null}
</div>
)}
</section>
<form
className="absolute inset-x-3 bottom-[34px] flex h-[53px] items-center rounded-[27px] bg-[#f4f4f2] p-2"
onSubmit={onSubmit}
>
<button
aria-label="Attach file"
className="flex size-10 items-center justify-center rounded-full bg-white"
type="button"
>
<Paperclip className="size-5" />
</button>
<input
className="min-w-0 flex-1 bg-transparent px-2 text-[14px] outline-none placeholder:text-[#b5b2ae]"
onChange={(event) => onChange(event.target.value)}
placeholder="Ask anything..."
value={value}
<MessageContent
className={cn(
"max-w-full gap-0 overflow-visible p-0",
message.role === "user"
? "ml-auto w-fit max-w-[86%] rounded-[22px] bg-[#0c0c0b] px-4 py-3 text-[16px] leading-6 text-white group-[.is-user]:rounded-[22px] group-[.is-user]:bg-[#0c0c0b] group-[.is-user]:px-4 group-[.is-user]:py-3 group-[.is-user]:text-white"
: "w-full bg-transparent text-[#11110f] dark:text-[#11110f]"
)}
>
{message.role === "assistant" ? (
<>
<div className="flex items-center gap-2">
<span className="flex size-7 items-center justify-center rounded-[9px] bg-[#c8ff00]">
<Sparkles className="size-4" />
</span>
<strong className="text-[14px]">Zopu</strong>
<span className="rounded-full bg-[#f1f1ef] px-2 py-1 text-[9px] text-[#aaa7a3]">
THINKING PARTNER
</span>
</div>
<MessageResponse
className="mt-3 text-[16px] leading-[21px] text-[#11110f] dark:text-[#11110f]"
isAnimating={data.assistant.isBusy}
>
{message.text}
</MessageResponse>
</>
) : (
<span className="whitespace-pre-wrap">
{message.text}
</span>
)}
</MessageContent>
</Message>
))}
{data.assistant.isBusy ? (
<p className="text-[13px] text-[#807b76]">Zopu is thinking</p>
) : null}
</div>
)}
<ChatWorkUnitCard
onOpen={onOpen}
onReview={onReview}
onStart={onStart}
pendingAction={pendingAction}
workUnit={data.selectedWorkUnit}
/>
</ConversationContent>
<ConversationScrollButton
aria-label="Scroll to latest message"
className="bottom-2 size-9 rounded-full border border-[#dededb] bg-white text-black shadow-sm"
/>
<button
aria-label="Record voice message"
className="flex size-10 items-center justify-center rounded-full bg-white"
type="button"
>
<Mic className="size-5" />
</button>
<button
aria-label="Send"
className="ml-1 flex size-10 items-center justify-center rounded-full bg-black text-white"
type="submit"
>
<ArrowUp className="size-5" />
</button>
</form>
</Conversation>
<PromptInput
className="absolute inset-x-3 bottom-[34px] w-auto [&_[data-slot=input-group]]:min-h-[56px] [&_[data-slot=input-group]]:rounded-full [&_[data-slot=input-group]]:border-0 [&_[data-slot=input-group]]:bg-[#f4f4f2] [&_[data-slot=input-group]]:px-2 [&_[data-slot=input-group]]:py-1.5 [&_[data-slot=input-group]]:shadow-none [&_[data-slot=input-group]]:dark:bg-[#f4f4f2]"
onSubmit={async (message) => {
if (onMessageSubmit) {
await onMessageSubmit(message.text);
return;
}
onChange(message.text);
}}
>
<PromptInputBody>
<PromptInputButton
aria-label="Attach file"
className="size-11 shrink-0 rounded-full bg-white text-black hover:bg-white/80"
size="icon-sm"
>
<Paperclip className="size-5" />
</PromptInputButton>
<PromptInputTextarea
aria-label="Ask anything..."
className="max-h-20 min-h-10 flex-1 resize-none bg-transparent px-2 py-2.5 text-[15px] leading-5 shadow-none outline-none placeholder:text-[#aaa7a3] dark:bg-transparent"
onChange={(event) => onChange(event.currentTarget.value)}
placeholder="Ask anything..."
value={value}
/>
<PromptInputSubmit
aria-label="Send"
className="size-11 shrink-0 rounded-full bg-black text-white hover:bg-black/85 disabled:bg-black disabled:text-white disabled:opacity-100"
disabled={!value.trim() || data.assistant.isBusy}
size="icon-sm"
status={data.assistant.isBusy ? "streaming" : "ready"}
>
<ArrowUp className="size-5" />
</PromptInputSubmit>
</PromptInputBody>
</PromptInput>
<p className="absolute inset-x-0 bottom-[13px] text-center text-[9px] text-[#b5b2ae]">
{statusMessage ?? "Press Enter to send · Shift + Enter for a new line"}
</p>
{projectManagerOpen ? (
<ProjectManagementPanel
createIssueBody={createIssueBody}
createIssueTitle={createIssueTitle}
data={data}
onClose={onProjectManagerClose}
onCreateBodyChange={onCreateBodyChange}
onCreateIssue={onCreateIssue}
onCreateTitleChange={onCreateTitleChange}
onProjectSelect={onProjectSelect}
onRepositoryChange={onRepositoryChange}
onRepositoryConnect={onRepositoryConnect}
pendingAction={pendingAction}
repositoryValue={repositoryValue}
statusMessage={statusMessage}
/>
) : null}
{settingsOpen ? (
<ChatSettingsPanel
modelId={modelId}
modelLabel={modelLabel}
onClose={onSettingsClose}
/>
) : null}
</main>
</div>
);
@@ -1402,21 +1791,32 @@ export const MobileWorkspaceScreenRenderer = ({
createIssueBody,
createIssueTitle,
data,
modelId,
modelLabel,
onBack,
onComposerChange,
onComposerMessageSubmit,
onComposerSubmit,
onCreateBodyChange,
onCreateIssue,
onCreateIssueFromSignal,
onCreateTitleChange,
onManageProjects,
onProjectManagerClose,
onOpenAssistant,
onOpenUnit,
onProjectSelect,
onRepositoryChange,
onRepositoryConnect,
onReviewUnit,
onSettingsClose,
onSettingsOpen,
onStartUnit,
onViewWork,
pendingAction,
projectManagerOpen,
repositoryValue,
settingsOpen,
statusMessage,
variant,
}: MobileWorkspaceRendererProps) => {
@@ -1432,8 +1832,30 @@ export const MobileWorkspaceScreenRenderer = ({
return (
<CharacterChat
{...composerProps}
createIssueBody={createIssueBody}
createIssueTitle={createIssueTitle}
data={data}
modelId={modelId}
modelLabel={modelLabel}
onCreateBodyChange={onCreateBodyChange}
onCreateIssue={onCreateIssue}
onCreateIssueFromSignal={onCreateIssueFromSignal}
onCreateTitleChange={onCreateTitleChange}
onManageProjects={onManageProjects}
onMessageSubmit={onComposerMessageSubmit}
onOpen={onOpenUnit}
onProjectManagerClose={onProjectManagerClose}
onProjectSelect={onProjectSelect}
onRepositoryChange={onRepositoryChange}
onRepositoryConnect={onRepositoryConnect}
onReview={onReviewUnit}
onSettingsClose={onSettingsClose}
onSettingsOpen={onSettingsOpen}
onStart={onStartUnit}
pendingAction={pendingAction}
projectManagerOpen={projectManagerOpen}
repositoryValue={repositoryValue}
settingsOpen={settingsOpen}
/>
);
}

View File

@@ -2,8 +2,10 @@ export type MobileWorkUnitTone = "blue" | "green" | "orange" | "purple";
export interface MobileProjectView {
readonly connected: boolean;
readonly host?: string;
readonly id: string;
readonly name: string;
readonly repositoryPath?: string;
}
export interface MobileWorkUnitView {