diff --git a/_features/_index.json b/_features/_index.json
index 298f66c8..23a168ba 100644
--- a/_features/_index.json
+++ b/_features/_index.json
@@ -1,6 +1,6 @@
[
{ "id": "infra-event-bus-ws", "status": "done", "name": "Infrastructure: Event Bus + WS Isolation + Global Store" },
- { "id": "issue-board-polish", "status": "in_progress", "name": "Issue Board & Detail Polish" },
+ { "id": "issue-board-polish", "status": "done", "name": "Issue Board & Detail Polish" },
{ "id": "workspace-permissions", "status": "done", "name": "Workspace & Permissions" },
{ "id": "inbox-notifications", "status": "done", "name": "Inbox & Notifications" }
]
diff --git a/apps/web/app/(dashboard)/_components/app-sidebar.tsx b/apps/web/app/(dashboard)/_components/app-sidebar.tsx
index 0f2bbc65..9ba915be 100644
--- a/apps/web/app/(dashboard)/_components/app-sidebar.tsx
+++ b/apps/web/app/(dashboard)/_components/app-sidebar.tsx
@@ -14,6 +14,7 @@ import {
Plus,
Check,
} from "lucide-react";
+import { toast } from "sonner";
import { MulticaIcon } from "@/components/multica-icon";
import {
Sidebar,
@@ -106,6 +107,7 @@ export function AppSidebar() {
await switchWorkspace(ws.id);
} catch (err) {
console.error("Failed to create workspace:", err);
+ toast.error("Failed to create workspace");
} finally {
setCreating(false);
}
diff --git a/apps/web/app/(dashboard)/inbox/page.tsx b/apps/web/app/(dashboard)/inbox/page.tsx
index 221ff6c7..d519e7e1 100644
--- a/apps/web/app/(dashboard)/inbox/page.tsx
+++ b/apps/web/app/(dashboard)/inbox/page.tsx
@@ -14,6 +14,7 @@ import {
import type { InboxItem, InboxItemType, InboxSeverity } from "@multica/types";
import Link from "next/link";
import { Button } from "@/components/ui/button";
+import { Skeleton } from "@/components/ui/skeleton";
import { api } from "@/shared/api";
// ---------------------------------------------------------------------------
@@ -226,8 +227,28 @@ export default function InboxPage() {
if (loading) {
return (
-
- Loading...
+
+
+
+
+
+
+ {Array.from({ length: 5 }).map((_, i) => (
+
+ ))}
+
+
+
+
+
+
+
);
}
diff --git a/apps/web/app/(dashboard)/issues/[id]/page.tsx b/apps/web/app/(dashboard)/issues/[id]/page.tsx
index 2d1e651c..c6ec6a65 100644
--- a/apps/web/app/(dashboard)/issues/[id]/page.tsx
+++ b/apps/web/app/(dashboard)/issues/[id]/page.tsx
@@ -31,6 +31,13 @@ import {
} from "@/components/ui/popover";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
+import { Textarea } from "@/components/ui/textarea";
+import {
+ Tooltip,
+ TooltipTrigger,
+ TooltipContent,
+ TooltipProvider,
+} from "@/components/ui/tooltip";
import { ActorAvatar } from "@/components/common/actor-avatar";
import type { Issue, Comment, UpdateIssueRequest } from "@multica/types";
import { StatusPicker, PriorityPicker, AssigneePicker } from "@/features/issues/components";
@@ -163,40 +170,53 @@ function AcceptanceCriteriaEditor({
onUpdate({ acceptance_criteria: criteria.filter((_, i) => i !== index) });
};
- if (criteria.length === 0 && !newItem) {
- return null;
- }
+ const [adding, setAdding] = useState(false);
return (
Acceptance Criteria
-
- {criteria.map((item, i) => (
-
- •
- {item}
-
-
- ))}
-
-
+ {criteria.length > 0 && (
+
+ {criteria.map((item, i) => (
+
+ •
+ {item}
+
+
+ ))}
+
+ )}
+ {(criteria.length > 0 || adding) ? (
+
+ ) : (
+
+ )}
);
}
@@ -224,48 +244,61 @@ function ContextRefsEditor({
onUpdate({ context_refs: refs.filter((_, i) => i !== index) });
};
- if (refs.length === 0 && !newRef) {
- return null;
- }
+ const [adding, setAdding] = useState(false);
const isUrl = (s: string) => s.startsWith("http://") || s.startsWith("https://");
return (
Context References
-
- {refs.map((ref, i) => (
-
-
- {isUrl(ref) ? (
-
- {ref}
-
- ) : (
-
{ref}
- )}
-
-
- ))}
-
-
+ {refs.length > 0 && (
+
+ {refs.map((ref, i) => (
+
+
+ {isUrl(ref) ? (
+
+ {ref}
+
+ ) : (
+
{ref}
+ )}
+
+
+ ))}
+
+ )}
+ {(refs.length > 0 || adding) ? (
+
+ ) : (
+
+ )}
);
}
@@ -291,6 +324,10 @@ export default function IssueDetailPage({
const [deleting, setDeleting] = useState(false);
const [editingCommentId, setEditingCommentId] = useState
(null);
const [editContent, setEditContent] = useState("");
+ const [editingTitle, setEditingTitle] = useState(false);
+ const [titleDraft, setTitleDraft] = useState("");
+ const [editingDesc, setEditingDesc] = useState(false);
+ const [descDraft, setDescDraft] = useState("");
// Watch the global issue store for real-time updates from other users/agents
const storeIssue = useIssueStore((s) => s.issues.find((i) => i.id === id));
@@ -316,14 +353,28 @@ export default function IssueDetailPage({
const handleSubmitComment = async (e: React.FormEvent) => {
e.preventDefault();
- if (!commentText.trim() || submitting) return;
+ if (!commentText.trim() || submitting || !user) return;
+ const content = commentText.trim();
+ const tempId = "temp-" + Date.now();
+ const tempComment: Comment = {
+ id: tempId,
+ issue_id: id,
+ author_type: "member",
+ author_id: user.id,
+ content,
+ type: "comment",
+ created_at: new Date().toISOString(),
+ updated_at: new Date().toISOString(),
+ };
+ setComments((prev) => [...prev, tempComment]);
+ setCommentText("");
setSubmitting(true);
try {
- const comment = await api.createComment(id, commentText.trim());
- setComments((prev) => [...prev, comment]);
- setCommentText("");
- } catch (err) {
- console.error("Failed to create comment:", err);
+ const comment = await api.createComment(id, content);
+ setComments((prev) => prev.map((c) => (c.id === tempId ? comment : c)));
+ } catch {
+ setComments((prev) => prev.filter((c) => c.id !== tempId));
+ toast.error("Failed to send comment");
} finally {
setSubmitting(false);
}
@@ -477,28 +528,72 @@ export default function IssueDetailPage({
{issue.id.slice(0, 8)}
-
- {issue.title}
-
+ {editingTitle ? (
+
setTitleDraft(e.target.value)}
+ onBlur={() => {
+ if (titleDraft.trim()) handleUpdateField({ title: titleDraft.trim() });
+ setEditingTitle(false);
+ }}
+ onKeyDown={(e) => {
+ if (e.key === "Enter") {
+ if (titleDraft.trim()) handleUpdateField({ title: titleDraft.trim() });
+ setEditingTitle(false);
+ } else if (e.key === "Escape") {
+ setEditingTitle(false);
+ }
+ }}
+ className="text-xl font-semibold leading-snug tracking-tight"
+ />
+ ) : (
+
{ setTitleDraft(issue.title); setEditingTitle(true); }}
+ >
+ {issue.title}
+
+ )}
- {issue.description && (
-
- {issue.description}
+ {editingDesc ? (
+