diff --git a/ui/public/paperclip-thinking.svg b/ui/public/paperclip-thinking.svg new file mode 100644 index 00000000..5fb88a0c --- /dev/null +++ b/ui/public/paperclip-thinking.svg @@ -0,0 +1,17 @@ + + + + + + \ No newline at end of file diff --git a/ui/src/App.tsx b/ui/src/App.tsx index ff1daecf..e97d3d68 100644 --- a/ui/src/App.tsx +++ b/ui/src/App.tsx @@ -6,6 +6,8 @@ import { Layout } from "./components/Layout"; import { OnboardingWizard } from "./components/OnboardingWizard"; import { authApi } from "./api/auth"; import { healthApi } from "./api/health"; +import { Artifacts } from "./pages/Artifacts"; +import { Chat } from "./pages/Chat"; import { Dashboard } from "./pages/Dashboard"; import { Companies } from "./pages/Companies"; import { Agents } from "./pages/Agents"; @@ -113,6 +115,8 @@ function boardRoutes() { <> } /> } /> + } /> + } /> } /> } /> } /> @@ -317,6 +321,8 @@ export function App() { } /> } /> } /> + } /> + } /> } /> } /> } /> diff --git a/ui/src/adapters/openclaw-gateway/config-fields.tsx b/ui/src/adapters/openclaw-gateway/config-fields.tsx index 19780d94..ead29ab6 100644 --- a/ui/src/adapters/openclaw-gateway/config-fields.tsx +++ b/ui/src/adapters/openclaw-gateway/config-fields.tsx @@ -157,7 +157,7 @@ export function OpenClawGatewayConfigFields({ className={inputClass} > - + diff --git a/ui/src/components/ArtifactsPanel.tsx b/ui/src/components/ArtifactsPanel.tsx new file mode 100644 index 00000000..ef634f0c --- /dev/null +++ b/ui/src/components/ArtifactsPanel.tsx @@ -0,0 +1,339 @@ +import { useState } from "react"; +import { useQuery } from "@tanstack/react-query"; +import type { IssueWorkProduct } from "@paperclipai/shared"; +import { issuesApi } from "../api/issues"; +import { queryKeys } from "../lib/queryKeys"; +import { MarkdownBody } from "./MarkdownBody"; +import { cn } from "../lib/utils"; +import { + FileText, + ExternalLink, + GitBranch, + GitCommit, + Globe, + Server, + Package, + Loader2, + ArrowLeft, + X, + CheckCircle2, + XCircle, + RotateCcw, +} from "lucide-react"; +import { Button } from "@/components/ui/button"; + +interface ArtifactsPanelProps { + taskId: string; + isAgentWorking?: boolean; + /** Open the document viewer directly to a specific doc */ + openDocKey?: string | null; + openDocTitle?: string | null; + onClearOpenDoc?: () => void; + /** Approval callbacks — called from the document viewer */ + onApprove?: () => void; + onReject?: () => void; +} + +type FilterValue = "all" | "in_progress" | "for_review" | "completed"; + +const FILTERS: Array<{ label: string; value: FilterValue }> = [ + { label: "All", value: "all" }, + { label: "In Progress", value: "in_progress" }, + { label: "For Review", value: "for_review" }, + { label: "Completed", value: "completed" }, +]; + +function matchesFilter(wp: IssueWorkProduct, filter: FilterValue): boolean { + if (filter === "all") return true; + if (filter === "in_progress") return wp.status === "active" || wp.status === "draft"; + if (filter === "for_review") return wp.status === "ready_for_review"; + if (filter === "completed") return wp.status === "approved" || wp.status === "merged"; + return true; +} + +function typeIcon(type: string) { + switch (type) { + case "document": return FileText; + case "pull_request": return GitBranch; + case "branch": return GitBranch; + case "commit": return GitCommit; + case "preview_url": return Globe; + case "runtime_service": return Server; + case "artifact": return Package; + default: return FileText; + } +} + +function statusBadge(status: string) { + switch (status) { + case "active": + case "draft": + return { label: "In Progress", className: "bg-blue-500/10 text-blue-600 dark:text-blue-400" }; + case "ready_for_review": + return { label: "For Review", className: "bg-amber-500/10 text-amber-600 dark:text-amber-400" }; + case "approved": + case "merged": + return { label: "Completed", className: "bg-green-500/10 text-green-600 dark:text-green-400" }; + case "changes_requested": + return { label: "Changes Requested", className: "bg-orange-500/10 text-orange-600 dark:text-orange-400" }; + case "failed": + return { label: "Failed", className: "bg-red-500/10 text-red-600 dark:text-red-400" }; + default: + return { label: status, className: "bg-muted text-muted-foreground" }; + } +} + +export function ArtifactsPanel({ taskId, isAgentWorking, openDocKey, openDocTitle, onClearOpenDoc, onApprove, onReject }: ArtifactsPanelProps) { + const [filter, setFilter] = useState("all"); + const [viewingDoc, setViewingDoc] = useState<{ key: string; title: string } | null>(null); + + const { data: workProducts, isLoading } = useQuery({ + queryKey: queryKeys.issues.workProducts(taskId), + queryFn: () => issuesApi.listWorkProducts(taskId), + refetchInterval: 5000, + }); + + // Open doc from parent (e.g. clicking plan link in chat) + const effectiveViewingDoc = openDocKey + ? { key: openDocKey, title: openDocTitle ?? "Document" } + : viewingDoc; + + const handleBack = () => { + setViewingDoc(null); + onClearOpenDoc?.(); + }; + + // Find the work product for the currently viewed doc to know its status + const viewedWorkProduct = effectiveViewingDoc + ? (workProducts ?? []).find((wp) => wp.title === effectiveViewingDoc.title) + : null; + + const filtered = (workProducts ?? []).filter((wp) => matchesFilter(wp, filter)); + + // Document viewer + if (effectiveViewingDoc) { + return ( + + ); + } + + return ( +
+
+ +

Artifacts

+
+ + {/* Filter chips */} +
+ {FILTERS.map((f) => ( + + ))} +
+ + {/* Work products list */} +
+ {isLoading ? ( +
+ + Loading... +
+ ) : filtered.length === 0 ? ( +
+ +

+ {workProducts?.length === 0 + ? "Your team's deliverables and plans will appear here as they're produced." + : "No artifacts match this filter."} +

+
+ ) : ( +
+ {filtered.map((wp) => { + const Icon = typeIcon(wp.type); + const badge = statusBadge(wp.status); + const isDraft = wp.status === "draft" || wp.status === "active"; + const showGenerating = isDraft && isAgentWorking; + return ( + + ); + })} +
+ )} +
+
+ ); +} + +function DocumentViewer({ + taskId, + docKey, + title, + onBack, + status, + reviewState, + onApprove, + onReject, +}: { + taskId: string; + docKey: string; + title: string; + onBack: () => void; + status: string | null; + reviewState: string | null; + onApprove?: () => void; + onReject?: () => void; +}) { + const { data: doc, isLoading, error } = useQuery({ + queryKey: queryKeys.issues.documents(taskId), + queryFn: () => issuesApi.getDocument(taskId, docKey), + }); + + const needsAction = status === "ready_for_review" || reviewState === "needs_board_review"; + const isApproved = status === "approved" || reviewState === "approved"; + const isRejected = status === "changes_requested" || reviewState === "changes_requested"; + + return ( +
+
+ +

{title}

+ +
+
+ {isLoading ? ( +
+ + Loading document... +
+ ) : error ? ( +

Document not available yet.

+ ) : doc?.body ? ( +
+ {doc.body} +
+ ) : ( +

Document is empty.

+ )} +
+ + {/* Sticky action footer */} + {needsAction && ( +
+

This document needs your review.

+
+ + +
+
+ )} + {isApproved && ( +
+
+ +

+ Approved — hire tasks created +

+
+
+ )} + {isRejected && ( +
+
+ +

+ Changes requested — CEO is revising +

+
+
+ )} +
+ ); +} diff --git a/ui/src/components/CEOChatPanel.tsx b/ui/src/components/CEOChatPanel.tsx new file mode 100644 index 00000000..bb604e81 --- /dev/null +++ b/ui/src/components/CEOChatPanel.tsx @@ -0,0 +1,664 @@ +import { useState, useRef, useEffect, useCallback, useMemo } from "react"; +import { useQuery, useQueryClient } from "@tanstack/react-query"; +import type { IssueComment } from "@paperclipai/shared"; +import { issuesApi } from "../api/issues"; +import { heartbeatsApi } from "../api/heartbeats"; +import { queryKeys } from "../lib/queryKeys"; +import { Button } from "@/components/ui/button"; +import { MarkdownBody } from "./MarkdownBody"; +import { cn } from "../lib/utils"; +import { + Loader2, + Send, + CheckCircle2, + Sparkles, + History, + Search, + X, + Plus, +} from "lucide-react"; + +export interface ChatConversation { + id: string; + title: string; + lastMessage?: string; + updatedAt: string; + isActive?: boolean; +} + +interface CEOChatPanelProps { + taskId: string; + agentId: string; + agentName: string; + companyId: string; + companyName?: string; + companyGoal?: string; + conversations?: ChatConversation[]; + onSwitchConversation?: (taskId: string) => void; + onNewConversation?: () => void; + onPlanDetected?: (planMarkdown: string) => void; + onPlanApproved?: () => void; + onAgentWorkingChange?: (working: boolean) => void; + onOpenArtifact?: (key: string, title: string) => void; +} + +/** Animated paperclip SVG thinking indicator */ +function PaperclipThinking({ className }: { className?: string }) { + return ( + + ); +} + +/** + * Detects whether a comment body contains a structured hiring plan. + */ +function detectHiringPlan(body: string): boolean { + const planPatterns = [ + /##?\s*(hiring|team|org|roles|plan)/i, + /##?\s*(proposed|recommended)\s*(roles|hires|team)/i, + /\n-\s+\*\*[^*]+\*\*/g, + /\|\s*role\s*\|/i, + ]; + return planPatterns.some((pattern) => pattern.test(body)); +} + +const QUEUED_MESSAGES = [ + "Heartbeat triggered, waking up...", + "Initializing...", + "Getting ready...", +]; + +const RUNNING_MESSAGES = [ + "Working on a response...", + "Reading the conversation...", + "Thinking through the plan...", + "Drafting a response...", + "Still working...", + "Almost there...", +]; + +const WAITING_MESSAGES = [ + "Waiting to wake up...", + "Heartbeat pending...", + "Should wake up soon...", +]; + +function getCyclingMessage(messages: string[], elapsed: number, agentName: string): string { + const idx = Math.floor(elapsed / 5) % messages.length; + return `${agentName} · ${messages[idx]}`; +} + +function getRunStatusMessage(status: string, agentName: string, elapsed: number): string { + switch (status) { + case "queued": + return getCyclingMessage(QUEUED_MESSAGES, elapsed, agentName); + case "running": + return getCyclingMessage(RUNNING_MESSAGES, elapsed, agentName); + case "succeeded": + return `${agentName} finished`; + case "failed": + return `${agentName} encountered an error`; + case "cancelled": + return `${agentName}'s run was cancelled`; + case "timed_out": + return `${agentName}'s run timed out`; + default: + return `${agentName} is thinking...`; + } +} + +/** Stepped progress indicator for long waits */ +function getProgressStep(elapsed: number): string | null { + if (elapsed < 10) return null; + if (elapsed < 30) return "Analyzing your mission..."; + if (elapsed < 60) return "Drafting the plan..."; + if (elapsed < 90) return "Detailing roles and responsibilities..."; + return "Almost ready..."; +} + +/** Context-aware suggestion chips */ +function getSuggestionChips( + hasActiveRun: boolean, + hasPlanDetected: boolean, + hasComments: boolean, +): Array<{ label: string; message: string }> { + if (hasPlanDetected) { + return [ + { label: "I want to make changes", message: "I'd like to make some changes to the plan before approving." }, + { label: "Add another role", message: "Can you add another role to the plan?" }, + ]; + } + if (hasActiveRun) { + return [ + { label: "What can I do while waiting?", message: "What can I do while you're working on the plan?" }, + { label: "Tell me about team structure", message: "Tell me about how you're thinking about the team structure." }, + ]; + } + if (hasComments) { + return [ + { label: "What should we prioritize?", message: "What should we prioritize first?" }, + { label: "Create a new project", message: "Let's create a new project to work on." }, + ]; + } + return [ + { label: "Let's talk strategy", message: "Before we hire anyone, I'd like to discuss our strategy and priorities." }, + { label: "What do you need from me?", message: "What information do you need from me to get started?" }, + ]; +} + +export function CEOChatPanel({ + taskId, + agentId, + agentName, + companyId, + companyName, + companyGoal, + conversations, + onSwitchConversation, + onNewConversation, + onPlanDetected, + onPlanApproved, + onAgentWorkingChange, + onOpenArtifact, +}: CEOChatPanelProps) { + const queryClient = useQueryClient(); + const [input, setInput] = useState(""); + const [sending, setSending] = useState(false); + const [detectedPlanCommentId, setDetectedPlanCommentId] = useState(null); + const [ignoreBeforeCommentId, setIgnoreBeforeCommentId] = useState(null); + const [usePaperclipIndicator, setUsePaperclipIndicator] = useState(true); + const [drawerOpen, setDrawerOpen] = useState(false); + const [drawerSearch, setDrawerSearch] = useState(""); + // Welcome typing animation — phases: typing → message + const [welcomePhase, setWelcomePhase] = useState<"typing" | "message">("typing"); + // Optimistic typing indicator — shows immediately after user sends + const [optimisticTyping, setOptimisticTyping] = useState(false); + const scrollRef = useRef(null); + const inputRef = useRef(null); + + // Poll comments — faster when waiting for a response + const { data: rawComments, isLoading } = useQuery({ + queryKey: queryKeys.issues.comments(taskId), + queryFn: () => issuesApi.listComments(taskId), + refetchInterval: optimisticTyping ? 2000 : 4000, + }); + + // Poll heartbeat — faster when actively waiting + const { data: activeRun } = useQuery({ + queryKey: queryKeys.issues.activeRun(taskId), + queryFn: () => heartbeatsApi.activeRunForIssue(taskId), + refetchInterval: optimisticTyping ? 1500 : 3000, + }); + + const comments = useMemo( + () => + rawComments + ? [...rawComments].sort( + (a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime(), + ) + : undefined, + [rawComments], + ); + + // Welcome typing animation — show "typing" for 2.5s then reveal message + useEffect(() => { + if (comments && comments.length === 0 && welcomePhase === "typing") { + const timer = setTimeout(() => setWelcomePhase("message"), 2500); + return () => clearTimeout(timer); + } + }, [comments, welcomePhase]); + + // Clear optimistic typing when a new agent comment arrives + useEffect(() => { + if (optimisticTyping && comments?.length) { + const lastComment = comments[comments.length - 1]; + if (lastComment.authorAgentId) { + setOptimisticTyping(false); + } + } + }, [comments, optimisticTyping]); + + // Auto-scroll + useEffect(() => { + if (scrollRef.current) { + scrollRef.current.scrollTop = scrollRef.current.scrollHeight; + } + }, [comments?.length]); + + // Detect hiring plan + useEffect(() => { + if (!comments || detectedPlanCommentId) return; + + let cutoffIdx = -1; + for (let i = comments.length - 1; i >= 0; i--) { + if (comments[i].authorUserId) { cutoffIdx = i; break; } + } + if (ignoreBeforeCommentId) { + const ignoreIdx = comments.findIndex((c) => c.id === ignoreBeforeCommentId); + if (ignoreIdx >= 0) cutoffIdx = Math.max(cutoffIdx, ignoreIdx); + } + + for (let i = comments.length - 1; i > cutoffIdx; i--) { + const c = comments[i]; + if (c.authorAgentId && detectHiringPlan(c.body)) { + setDetectedPlanCommentId(c.id); + // Update existing draft artifact to "ready_for_review", or create one + (async () => { + try { + const wps = await issuesApi.listWorkProducts(taskId); + const existing = wps.find((wp) => wp.title === "Hiring Plan"); + if (existing) { + await issuesApi.updateWorkProduct(existing.id, { + status: "ready_for_review", + reviewState: "needs_board_review", + summary: "Hiring plan is ready for your review", + }); + } else { + await issuesApi.createWorkProduct(taskId, { + type: "document", + title: "Hiring Plan", + provider: "paperclip", + status: "ready_for_review", + reviewState: "needs_board_review", + isPrimary: true, + summary: "Hiring plan is ready for your review", + }); + } + } catch { /* non-critical */ } + })(); + // Notify parent + issuesApi.getDocument(taskId, "plan").then((doc) => { + onPlanDetected?.(doc.body ?? c.body); + }).catch(() => { + onPlanDetected?.(c.body); + }); + // Invalidate work products so ArtifactsPanel picks it up + queryClient.invalidateQueries({ + queryKey: queryKeys.issues.workProducts(taskId), + }); + break; + } + } + }, [comments, detectedPlanCommentId, ignoreBeforeCommentId, taskId, onPlanDetected, queryClient]); + + // Send message + const sendMessage = useCallback(async (body: string) => { + const trimmed = body.trim(); + if (!trimmed || sending) return; + setSending(true); + try { + try { + await issuesApi.update(taskId, { assigneeUserId: null }); + } catch { /* may already be null */ } + try { + await issuesApi.update(taskId, { + assigneeAgentId: agentId, + status: "in_progress", + }); + } catch { /* may already be assigned */ } + + await issuesApi.addComment(taskId, trimmed, true, true); + setInput(""); + setOptimisticTyping(true); // Show typing indicator immediately + const latestId = comments?.[comments.length - 1]?.id ?? null; + setIgnoreBeforeCommentId(latestId); + setDetectedPlanCommentId(null); + queryClient.invalidateQueries({ + queryKey: queryKeys.issues.comments(taskId), + }); + } finally { + setSending(false); + inputRef.current?.focus(); + } + }, [sending, taskId, agentId, queryClient, comments]); + + const handleSend = useCallback(() => { + sendMessage(input); + }, [input, sendMessage]); + + const handleKeyDown = useCallback( + (e: React.KeyboardEvent) => { + if (e.key === "Enter" && !e.shiftKey) { + e.preventDefault(); + handleSend(); + } + }, + [handleSend], + ); + + // Status indicators + const lastComment = comments?.[comments.length - 1]; + const isWaitingForAgent = lastComment && lastComment.authorUserId && !lastComment.authorAgentId; + const hasActiveRun = activeRun && (activeRun.status === "queued" || activeRun.status === "running"); + const showStatus = isWaitingForAgent || hasActiveRun; + + // Notify parent of working state changes + useEffect(() => { + onAgentWorkingChange?.(!!showStatus); + }, [showStatus, onAgentWorkingChange]); + + // Elapsed timer + const [elapsed, setElapsed] = useState(0); + const waitingSince = useMemo(() => { + if (!showStatus || !lastComment) return null; + if (lastComment.authorUserId) return new Date(lastComment.createdAt).getTime(); + if (hasActiveRun && activeRun.createdAt) return new Date(activeRun.createdAt).getTime(); + return null; + }, [showStatus, lastComment, hasActiveRun, activeRun]); + + useEffect(() => { + if (!waitingSince) { setElapsed(0); return; } + setElapsed(Math.floor((Date.now() - waitingSince) / 1000)); + const interval = setInterval(() => { + setElapsed(Math.floor((Date.now() - waitingSince) / 1000)); + }, 1000); + return () => clearInterval(interval); + }, [waitingSince]); + + const elapsedStr = elapsed >= 60 + ? `${Math.floor(elapsed / 60)}m ${elapsed % 60}s` + : `${elapsed}s`; + + const progressStep = getProgressStep(elapsed); + const suggestionChips = getSuggestionChips(!!hasActiveRun, !!detectedPlanCommentId, !!comments?.length); + + // Dynamic placeholder + const placeholder = hasActiveRun + ? `${agentName} is working...` + : detectedPlanCommentId + ? "Ask your CEO to revise the plan..." + : "Send a message..."; + + if (isLoading) { + return ( +
+ + Loading conversation... +
+ ); + } + + const filteredConversations = (conversations ?? []).filter((c) => + !drawerSearch || c.title.toLowerCase().includes(drawerSearch.toLowerCase()), + ); + + return ( +
+ {/* Chat header */} +
+ + {agentName} +
+ + {/* Chat history drawer — slides over chat */} + {drawerOpen && ( +
+
+ + Conversations + {onNewConversation && ( + + )} +
+
+
+ + setDrawerSearch(e.target.value)} + autoFocus + /> +
+
+
+ {filteredConversations.length === 0 ? ( +
+ {conversations?.length === 0 ? "No conversations yet" : "No matches"} +
+ ) : ( + filteredConversations.map((conv) => ( + + )) + )} +
+
+ )} + + {/* Progress step indicator */} + {showStatus && progressStep && ( +
+ + {progressStep} +
+ )} + + {/* Messages */} +
+ {/* CEO Welcome — typing indicator then message */} + {comments?.length === 0 && welcomePhase === "typing" && ( +
+ {usePaperclipIndicator ? ( + + ) : ( + + + + + )} + {agentName} is composing a message... +
+ )} + {comments?.length === 0 && welcomePhase === "message" && ( +
+
+ + {agentName} + +
+

+ Hello! I'm {agentName}{companyName ? <>, your CEO at {companyName} : ", your CEO"}. +

+ {companyGoal && ( +

+ Our mission: {companyGoal} +

+ )} +

+ I'd love to understand your vision and priorities before we start building the team. What's most important to you right now? +

+
+ )} + + {comments?.map((comment) => { + const isAgent = Boolean(comment.authorAgentId); + const isPlan = detectedPlanCommentId === comment.id; + return ( +
+
+
+ + {isAgent ? agentName : "You"} + + {isPlan && ( + + + Hiring plan detected + + )} +
+
+ + {isAgent + ? comment.body.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1") + : comment.body} + +
+
+ + {/* Inline plan link — opens in artifacts pane */} + {isPlan && ( + + )} +
+ ); + })} + + {/* Status indicator — click to toggle between paperclip SVG and blue dot */} + {showStatus && ( + + )} + {/* Optimistic typing indicator — shows immediately after user sends */} + {optimisticTyping && !showStatus && ( +
+ {usePaperclipIndicator ? ( + + ) : ( + + + + + )} + {agentName} is typing... +
+ )} +
+ + {/* Suggestion chips */} +
+ {suggestionChips.map((chip) => ( + + ))} +
+ + {/* Input area */} +
+ setInput(e.target.value)} + onKeyDown={handleKeyDown} + autoFocus + /> + +
+
+ ); +} + diff --git a/ui/src/components/CommandPalette.tsx b/ui/src/components/CommandPalette.tsx index 9d84be52..73c59c7c 100644 --- a/ui/src/components/CommandPalette.tsx +++ b/ui/src/components/CommandPalette.tsx @@ -106,7 +106,7 @@ export function CommandPalette() { if (v && isMobile) setSidebarOpen(false); }}> @@ -121,7 +121,7 @@ export function CommandPalette() { }} > - Create new issue + Create new task C go("/issues")}> - Issues + Tasks go("/projects")}> @@ -179,7 +179,7 @@ export function CommandPalette() { {visibleIssues.length > 0 && ( <> - + {visibleIssues.slice(0, 10).map((issue) => ( void; +} + +export function FrontDoor({ onChoose }: FrontDoorProps) { + return ( +
+
+

+ Welcome to Paperclip +

+

+ How would you like to get started? +

+
+ +
+ + + +
+
+ ); +} diff --git a/ui/src/components/IssuesList.tsx b/ui/src/components/IssuesList.tsx index 442f8ae4..b21d39d0 100644 --- a/ui/src/components/IssuesList.tsx +++ b/ui/src/components/IssuesList.tsx @@ -322,9 +322,9 @@ export function IssuesList({ setIssueSearch(e.target.value); onSearchChange?.(e.target.value); }} - placeholder="Search issues..." + placeholder="Search tasks..." className="pl-7 text-xs sm:text-sm" - aria-label="Search issues" + aria-label="Search tasks" /> @@ -586,8 +586,8 @@ export function IssuesList({ {!isLoading && filtered.length === 0 && viewState.viewMode === "list" && ( openNewIssue(newIssueDefaults())} /> )} diff --git a/ui/src/components/MobileBottomNav.tsx b/ui/src/components/MobileBottomNav.tsx index daa17318..e05bc4b9 100644 --- a/ui/src/components/MobileBottomNav.tsx +++ b/ui/src/components/MobileBottomNav.tsx @@ -42,7 +42,7 @@ export function MobileBottomNav({ visible }: MobileBottomNavProps) { const items = useMemo( () => [ { type: "link", to: "/dashboard", label: "Home", icon: House }, - { type: "link", to: "/issues", label: "Issues", icon: CircleDot }, + { type: "link", to: "/issues", label: "Tasks", icon: CircleDot }, { type: "action", label: "Create", icon: SquarePen, onClick: () => openNewIssue() }, { type: "link", to: "/agents/all", label: "Agents", icon: Users }, { diff --git a/ui/src/components/NewIssueDialog.tsx b/ui/src/components/NewIssueDialog.tsx index ad8efb67..5d43944d 100644 --- a/ui/src/components/NewIssueDialog.tsx +++ b/ui/src/components/NewIssueDialog.tsx @@ -814,7 +814,7 @@ export function NewIssueDialog() { const hasSavedDraft = Boolean(savedDraft?.title.trim() || savedDraft?.description.trim()); const canDiscardDraft = hasDraft || hasSavedDraft; const createIssueErrorMessage = - createIssue.error instanceof Error ? createIssue.error.message : "Failed to create issue. Try again."; + createIssue.error instanceof Error ? createIssue.error.message : "Failed to create task. Try again."; const stagedDocuments = stagedFiles.filter((file) => file.kind === "document"); const stagedAttachments = stagedFiles.filter((file) => file.kind === "attachment"); @@ -981,7 +981,7 @@ export function NewIssueDialog() {