From 33d549db136b95ec260ead1d0d2533215cd48543 Mon Sep 17 00:00:00 2001 From: Forgotten Date: Wed, 25 Feb 2026 21:36:06 -0600 Subject: [PATCH] feat(ui): mobile UX improvements, comment attachments, and cost breakdown Add PWA meta tags for iOS home screen. Fix mobile properties drawer with safe area insets. Add image attachment button to comment thread. Improve sidebar with collapsible sections, project grouping, and mobile bottom nav. Show token and billing type breakdown on costs page. Fix inbox loading state to show content progressively. Various mobile overflow and layout fixes. Co-Authored-By: Claude Opus 4.6 --- ui/index.html | 3 + ui/src/components/CommentThread.tsx | 47 +++- ui/src/components/InlineEditor.tsx | 34 ++- ui/src/components/IssueProperties.tsx | 6 +- ui/src/components/IssuesList.tsx | 148 ++++++------ ui/src/components/Layout.tsx | 20 +- ui/src/components/LiveRunWidget.tsx | 65 ++++-- ui/src/components/MobileBottomNav.tsx | 4 +- ui/src/components/NewIssueDialog.tsx | 36 ++- ui/src/components/NewProjectDialog.tsx | 207 +++++++++++++++-- ui/src/components/ProjectProperties.tsx | 296 ++++++++++++++++++------ ui/src/components/Sidebar.tsx | 2 +- ui/src/index.css | 5 + ui/src/pages/Costs.tsx | 22 +- ui/src/pages/Inbox.tsx | 19 +- ui/src/pages/IssueDetail.tsx | 2 +- 16 files changed, 688 insertions(+), 228 deletions(-) diff --git a/ui/index.html b/ui/index.html index 3a033cb4..906158e4 100644 --- a/ui/index.html +++ b/ui/index.html @@ -4,6 +4,9 @@ + + + Paperclip diff --git a/ui/src/components/CommentThread.tsx b/ui/src/components/CommentThread.tsx index 6931a1f4..70c64d66 100644 --- a/ui/src/components/CommentThread.tsx +++ b/ui/src/components/CommentThread.tsx @@ -1,7 +1,8 @@ -import { useEffect, useMemo, useRef, useState } from "react"; +import { useEffect, useMemo, useRef, useState, type ChangeEvent } from "react"; import { Link } from "react-router-dom"; import type { IssueComment, Agent } from "@paperclip/shared"; import { Button } from "@/components/ui/button"; +import { Paperclip } from "lucide-react"; import { Identity } from "./Identity"; import { MarkdownBody } from "./MarkdownBody"; import { MarkdownEditor, type MarkdownEditorRef, type MentionOption } from "./MarkdownEditor"; @@ -18,7 +19,10 @@ interface CommentThreadProps { issueStatus?: string; agentMap?: Map; imageUploadHandler?: (file: File) => Promise; + /** Callback to attach an image file to the parent issue (not inline in a comment). */ + onAttachImage?: (file: File) => Promise; draftKey?: string; + liveRunSlot?: React.ReactNode; } const CLOSED_STATUSES = new Set(["done", "cancelled"]); @@ -52,11 +56,13 @@ function clearDraft(draftKey: string) { } } -export function CommentThread({ comments, onAdd, issueStatus, agentMap, imageUploadHandler, draftKey }: CommentThreadProps) { +export function CommentThread({ comments, onAdd, issueStatus, agentMap, imageUploadHandler, onAttachImage, draftKey, liveRunSlot }: CommentThreadProps) { const [body, setBody] = useState(""); const [reopen, setReopen] = useState(true); const [submitting, setSubmitting] = useState(false); + const [attaching, setAttaching] = useState(false); const editorRef = useRef(null); + const attachInputRef = useRef(null); const draftTimer = useRef | null>(null); const isClosed = issueStatus ? CLOSED_STATUSES.has(issueStatus) : false; @@ -112,6 +118,18 @@ export function CommentThread({ comments, onAdd, issueStatus, agentMap, imageUpl } } + async function handleAttachFile(evt: ChangeEvent) { + const file = evt.target.files?.[0]; + if (!file || !onAttachImage) return; + setAttaching(true); + try { + await onAttachImage(file); + } finally { + setAttaching(false); + if (attachInputRef.current) attachInputRef.current.value = ""; + } + } + return (

Comments ({comments.length})

@@ -122,7 +140,7 @@ export function CommentThread({ comments, onAdd, issueStatus, agentMap, imageUpl
{sorted.map((comment) => ( -
+
{comment.authorAgentId ? ( @@ -153,6 +171,8 @@ export function CommentThread({ comments, onAdd, issueStatus, agentMap, imageUpl ))}
+ {liveRunSlot} +
+ {onAttachImage && ( + <> + + + + )} {isClosed && (