diff --git a/data/.auth_local_token b/data/.auth_local_token new file mode 100644 index 00000000..c3e3f661 --- /dev/null +++ b/data/.auth_local_token @@ -0,0 +1 @@ +oGAbVgPbRGzvhyj48oZzStTi3ROiCsaCtYLrkF7eZRY \ No newline at end of file diff --git a/data/agents.json b/data/agents.json new file mode 100644 index 00000000..906367af --- /dev/null +++ b/data/agents.json @@ -0,0 +1,654 @@ +{ + "agents": [ + { + "name": "atlas-persona", + "display_name": "atlas-persona", + "created_at": 1776542847, + "last_ingest_at": 0, + "total_chunks": 0, + "librarian": { + "enabled": true, + "model": null, + "tasks": { + "fact_extraction": true, + "preference_extraction": true, + "intake_classification": true, + "crystallise": true, + "reflect": true, + "catalog_enrichment": true, + "query_expansion": true, + "verification": true + }, + "fanout": { + "default": "low", + "auto_scale": true + } + } + }, + { + "name": "defaultagent", + "display_name": "defaultagent", + "created_at": 1776542847, + "last_ingest_at": 0, + "total_chunks": 0, + "librarian": { + "enabled": true, + "model": null, + "tasks": { + "fact_extraction": true, + "preference_extraction": true, + "intake_classification": true, + "crystallise": true, + "reflect": true, + "catalog_enrichment": true, + "query_expansion": true, + "verification": true + }, + "fanout": { + "default": "low", + "auto_scale": true + } + } + }, + { + "name": "customone", + "display_name": "customone", + "created_at": 1776542847, + "last_ingest_at": 0, + "total_chunks": 0, + "librarian": { + "enabled": true, + "model": null, + "tasks": { + "fact_extraction": true, + "preference_extraction": true, + "intake_classification": true, + "crystallise": true, + "reflect": true, + "catalog_enrichment": true, + "query_expansion": true, + "verification": true + }, + "fanout": { + "default": "low", + "auto_scale": true + } + } + }, + { + "name": "nosave", + "display_name": "nosave", + "created_at": 1776542847, + "last_ingest_at": 0, + "total_chunks": 0, + "librarian": { + "enabled": true, + "model": null, + "tasks": { + "fact_extraction": true, + "preference_extraction": true, + "intake_classification": true, + "crystallise": true, + "reflect": true, + "catalog_enrichment": true, + "query_expansion": true, + "verification": true + }, + "fanout": { + "default": "low", + "auto_scale": true + } + } + }, + { + "name": "atlas", + "display_name": "atlas", + "created_at": 1776542859, + "last_ingest_at": 0, + "total_chunks": 0, + "librarian": { + "enabled": true, + "model": null, + "tasks": { + "fact_extraction": true, + "preference_extraction": true, + "intake_classification": true, + "crystallise": true, + "reflect": true, + "catalog_enrichment": true, + "query_expansion": true, + "verification": true + }, + "fanout": { + "default": "low", + "auto_scale": true + } + } + }, + { + "name": "atlas2", + "display_name": "atlas2", + "created_at": 1776542860, + "last_ingest_at": 0, + "total_chunks": 0, + "librarian": { + "enabled": true, + "model": null, + "tasks": { + "fact_extraction": true, + "preference_extraction": true, + "intake_classification": true, + "crystallise": true, + "reflect": true, + "catalog_enrichment": true, + "query_expansion": true, + "verification": true + }, + "fanout": { + "default": "low", + "auto_scale": true + } + } + }, + { + "name": "atlas3", + "display_name": "atlas3", + "created_at": 1776542860, + "last_ingest_at": 0, + "total_chunks": 0, + "librarian": { + "enabled": true, + "model": null, + "tasks": { + "fact_extraction": true, + "preference_extraction": true, + "intake_classification": true, + "crystallise": true, + "reflect": true, + "catalog_enrichment": true, + "query_expansion": true, + "verification": true + }, + "fanout": { + "default": "low", + "auto_scale": true + } + } + }, + { + "name": "bg-test", + "display_name": "bg-test", + "created_at": 1776542893, + "last_ingest_at": 0, + "total_chunks": 0, + "librarian": { + "enabled": true, + "model": null, + "tasks": { + "fact_extraction": true, + "preference_extraction": true, + "intake_classification": true, + "crystallise": true, + "reflect": true, + "catalog_enrichment": true, + "query_expansion": true, + "verification": true + }, + "fanout": { + "default": "low", + "auto_scale": true + } + } + }, + { + "name": "status-test", + "display_name": "status-test", + "created_at": 1776542893, + "last_ingest_at": 0, + "total_chunks": 0, + "librarian": { + "enabled": true, + "model": null, + "tasks": { + "fact_extraction": true, + "preference_extraction": true, + "intake_classification": true, + "crystallise": true, + "reflect": true, + "catalog_enrichment": true, + "query_expansion": true, + "verification": true + }, + "fanout": { + "default": "low", + "auto_scale": true + } + } + }, + { + "name": "local-agent", + "display_name": "local-agent", + "created_at": 1776545704, + "last_ingest_at": 0, + "total_chunks": 0, + "librarian": { + "enabled": true, + "model": null, + "tasks": { + "fact_extraction": true, + "preference_extraction": true, + "intake_classification": true, + "crystallise": true, + "reflect": true, + "catalog_enrichment": true, + "query_expansion": true, + "verification": true + }, + "fanout": { + "default": "low", + "auto_scale": true + } + } + }, + { + "name": "cloud-agent", + "display_name": "cloud-agent", + "created_at": 1776545705, + "last_ingest_at": 0, + "total_chunks": 0, + "librarian": { + "enabled": true, + "model": null, + "tasks": { + "fact_extraction": true, + "preference_extraction": true, + "intake_classification": true, + "crystallise": true, + "reflect": true, + "catalog_enrichment": true, + "query_expansion": true, + "verification": true + }, + "fanout": { + "default": "low", + "auto_scale": true + } + } + }, + { + "name": "persistent", + "display_name": "persistent", + "created_at": 1776545707, + "last_ingest_at": 0, + "total_chunks": 0, + "librarian": { + "enabled": true, + "model": null, + "tasks": { + "fact_extraction": true, + "preference_extraction": true, + "intake_classification": true, + "crystallise": true, + "reflect": true, + "catalog_enrichment": true, + "query_expansion": true, + "verification": true + }, + "fanout": { + "default": "low", + "auto_scale": true + } + } + }, + { + "name": "emoji-agent", + "display_name": "emoji-agent", + "created_at": 1776545708, + "last_ingest_at": 0, + "total_chunks": 0, + "librarian": { + "enabled": true, + "model": null, + "tasks": { + "fact_extraction": true, + "preference_extraction": true, + "intake_classification": true, + "crystallise": true, + "reflect": true, + "catalog_enrichment": true, + "query_expansion": true, + "verification": true + }, + "fanout": { + "default": "low", + "auto_scale": true + } + } + }, + { + "name": "no-emoji-agent", + "display_name": "no-emoji-agent", + "created_at": 1776545708, + "last_ingest_at": 0, + "total_chunks": 0, + "librarian": { + "enabled": true, + "model": null, + "tasks": { + "fact_extraction": true, + "preference_extraction": true, + "intake_classification": true, + "crystallise": true, + "reflect": true, + "catalog_enrichment": true, + "query_expansion": true, + "verification": true + }, + "fanout": { + "default": "low", + "auto_scale": true + } + } + }, + { + "name": "chatter", + "display_name": "chatter", + "created_at": 1776545709, + "last_ingest_at": 0, + "total_chunks": 0, + "librarian": { + "enabled": true, + "model": null, + "tasks": { + "fact_extraction": true, + "preference_extraction": true, + "intake_classification": true, + "crystallise": true, + "reflect": true, + "catalog_enrichment": true, + "query_expansion": true, + "verification": true + }, + "fanout": { + "default": "low", + "auto_scale": true + } + } + }, + { + "name": "archiver", + "display_name": "archiver", + "created_at": 1776545709, + "last_ingest_at": 0, + "total_chunks": 0, + "librarian": { + "enabled": true, + "model": null, + "tasks": { + "fact_extraction": true, + "preference_extraction": true, + "intake_classification": true, + "crystallise": true, + "reflect": true, + "catalog_enrichment": true, + "query_expansion": true, + "verification": true + }, + "fanout": { + "default": "low", + "auto_scale": true + } + } + }, + { + "name": "archiver2", + "display_name": "archiver2", + "created_at": 1776545710, + "last_ingest_at": 0, + "total_chunks": 0, + "librarian": { + "enabled": true, + "model": null, + "tasks": { + "fact_extraction": true, + "preference_extraction": true, + "intake_classification": true, + "crystallise": true, + "reflect": true, + "catalog_enrichment": true, + "query_expansion": true, + "verification": true + }, + "fanout": { + "default": "low", + "auto_scale": true + } + } + }, + { + "name": "stuck", + "display_name": "stuck", + "created_at": 1776545710, + "last_ingest_at": 0, + "total_chunks": 0, + "librarian": { + "enabled": true, + "model": null, + "tasks": { + "fact_extraction": true, + "preference_extraction": true, + "intake_classification": true, + "crystallise": true, + "reflect": true, + "catalog_enrichment": true, + "query_expansion": true, + "verification": true + }, + "fanout": { + "default": "low", + "auto_scale": true + } + } + }, + { + "name": "snaprest", + "display_name": "snaprest", + "created_at": 1776545711, + "last_ingest_at": 0, + "total_chunks": 0, + "librarian": { + "enabled": true, + "model": null, + "tasks": { + "fact_extraction": true, + "preference_extraction": true, + "intake_classification": true, + "crystallise": true, + "reflect": true, + "catalog_enrichment": true, + "query_expansion": true, + "verification": true + }, + "fanout": { + "default": "low", + "auto_scale": true + } + } + }, + { + "name": "rest", + "display_name": "rest", + "created_at": 1776545711, + "last_ingest_at": 0, + "total_chunks": 0, + "librarian": { + "enabled": true, + "model": null, + "tasks": { + "fact_extraction": true, + "preference_extraction": true, + "intake_classification": true, + "crystallise": true, + "reflect": true, + "catalog_enrichment": true, + "query_expansion": true, + "verification": true + }, + "fanout": { + "default": "low", + "auto_scale": true + } + } + }, + { + "name": "envtest2", + "display_name": "envtest2", + "created_at": 1776545712, + "last_ingest_at": 0, + "total_chunks": 0, + "librarian": { + "enabled": true, + "model": null, + "tasks": { + "fact_extraction": true, + "preference_extraction": true, + "intake_classification": true, + "crystallise": true, + "reflect": true, + "catalog_enrichment": true, + "query_expansion": true, + "verification": true + }, + "fanout": { + "default": "low", + "auto_scale": true + } + } + }, + { + "name": "purgeable", + "display_name": "purgeable", + "created_at": 1776545712, + "last_ingest_at": 0, + "total_chunks": 0, + "librarian": { + "enabled": true, + "model": null, + "tasks": { + "fact_extraction": true, + "preference_extraction": true, + "intake_classification": true, + "crystallise": true, + "reflect": true, + "catalog_enrichment": true, + "query_expansion": true, + "verification": true + }, + "fanout": { + "default": "low", + "auto_scale": true + } + } + }, + { + "name": "chat-agent", + "display_name": "chat-agent", + "created_at": 1776545713, + "last_ingest_at": 0, + "total_chunks": 0, + "librarian": { + "enabled": true, + "model": null, + "tasks": { + "fact_extraction": true, + "preference_extraction": true, + "intake_classification": true, + "crystallise": true, + "reflect": true, + "catalog_enrichment": true, + "query_expansion": true, + "verification": true + }, + "fanout": { + "default": "low", + "auto_scale": true + } + } + }, + { + "name": "restore-agent", + "display_name": "restore-agent", + "created_at": 1776545714, + "last_ingest_at": 0, + "total_chunks": 0, + "librarian": { + "enabled": true, + "model": null, + "tasks": { + "fact_extraction": true, + "preference_extraction": true, + "intake_classification": true, + "crystallise": true, + "reflect": true, + "catalog_enrichment": true, + "query_expansion": true, + "verification": true + }, + "fanout": { + "default": "low", + "auto_scale": true + } + } + }, + { + "name": "idem-agent", + "display_name": "idem-agent", + "created_at": 1776545714, + "last_ingest_at": 0, + "total_chunks": 0, + "librarian": { + "enabled": true, + "model": null, + "tasks": { + "fact_extraction": true, + "preference_extraction": true, + "intake_classification": true, + "crystallise": true, + "reflect": true, + "catalog_enrichment": true, + "query_expansion": true, + "verification": true + }, + "fanout": { + "default": "low", + "auto_scale": true + } + } + }, + { + "name": "purge-agent", + "display_name": "purge-agent", + "created_at": 1776545715, + "last_ingest_at": 0, + "total_chunks": 0, + "librarian": { + "enabled": true, + "model": null, + "tasks": { + "fact_extraction": true, + "preference_extraction": true, + "intake_classification": true, + "crystallise": true, + "reflect": true, + "catalog_enrichment": true, + "query_expansion": true, + "verification": true + }, + "fanout": { + "default": "low", + "auto_scale": true + } + } + } + ] +} \ No newline at end of file diff --git a/desktop/src/apps/MessagesApp.tsx b/desktop/src/apps/MessagesApp.tsx index 211390c7..dc7b10ad 100644 --- a/desktop/src/apps/MessagesApp.tsx +++ b/desktop/src/apps/MessagesApp.tsx @@ -43,6 +43,17 @@ import { AttachmentGallery } from "./chat/AttachmentGallery"; import { uploadDiskFile, attachmentFromPath, type AttachmentRecord } from "@/lib/chat-attachments-api"; import { useThreadPanel } from "@/lib/use-thread-panel"; import { openFilePicker } from "@/shell/file-picker-api"; +import { MessageOverflowMenu } from "./chat/MessageOverflowMenu"; +import { MessageEditor } from "./chat/MessageEditor"; +import { MessageTombstone } from "./chat/MessageTombstone"; +import { PinBadge } from "./chat/PinBadge"; +import { PinnedMessagesPopover, type PinnedMessage } from "./chat/PinnedMessagesPopover"; +import { PinRequestAffordance } from "./chat/PinRequestAffordance"; +import { + pinMessage, unpinMessage, listPins, + editMessage as apiEditMessage, deleteMessage as apiDeleteMessage, + markUnread as apiMarkUnread, +} from "@/lib/chat-messages-api"; /* ------------------------------------------------------------------ */ /* Types */ @@ -136,12 +147,14 @@ interface Message { canvas_id?: string; canvas_url?: string; canvas_title?: string; + pin_requested?: boolean; [key: string]: unknown; }; state?: "pending" | "streaming" | "complete" | "error"; created_at: string; reactions?: Record; edited_at?: string; + deleted_at?: number | null; attachments?: AttachmentRecord[]; reply_count?: number; last_reply_at?: number | null; @@ -218,6 +231,11 @@ export function MessagesApp({ windowId: _windowId, title }: { windowId: string; const [sendError, setSendError] = useState(null); const [hoveredMessageId, setHoveredMessageId] = useState(null); const [pendingAttachments, setPendingAttachments] = useState([]); + const [overflowMenu, setOverflowMenu] = useState<{ messageId: string; x: number; y: number } | null>(null); + const [editingMessageId, setEditingMessageId] = useState(null); + const [pinnedPopoverOpen, setPinnedPopoverOpen] = useState(false); + const [pinnedMessages, setPinnedMessages] = useState([]); + const [currentUserId, setCurrentUserId] = useState(null); const { openThread, openThreadFor, closeThread } = useThreadPanel(); const wsRef = useRef(null); @@ -399,14 +417,26 @@ export function MessagesApp({ windowId: _windowId, title }: { windowId: string; setMessages((prev) => prev.map((m) => m.id === data.message_id - ? { ...m, content: data.content, edited_at: data.edited_at } + ? { + ...m, + ...(data.content !== undefined && { content: data.content }), + ...(data.edited_at !== undefined && { edited_at: data.edited_at }), + ...(data.metadata !== undefined && { metadata: data.metadata }), + } : m, ), ); break; case "message_delete": - setMessages((prev) => prev.filter((m) => m.id !== data.message_id)); + // Soft delete β€” keep the row so the UI can render the tombstone. + setMessages((prev) => + prev.map((m) => + m.id === data.message_id + ? { ...m, deleted_at: data.deleted_at ?? Date.now() / 1000 } + : m, + ), + ); break; } } catch { @@ -444,6 +474,14 @@ export function MessagesApp({ windowId: _windowId, title }: { windowId: string; }; }, [fetchChannels, fetchArchivedChannels, fetchAgentLists, connectWs]); + /* ---- fetch current user ---- */ + useEffect(() => { + fetch("/api/auth/me") + .then((r) => r.ok ? r.json() : null) + .then((u) => { if (u?.id) setCurrentUserId(u.id); }) + .catch(() => {}); + }, []); + /* ---- cross-app open-messages event ---- */ useEffect(() => { // Guard against the component unmounting while an admin-prompt @@ -513,6 +551,34 @@ export function MessagesApp({ windowId: _windowId, title }: { windowId: string; setTypingAgents([]); }, [selectedChannel, fetchMessages, markRead]); + /* ---- deep-link scroll on ?msg= β€” latch so it fires once per URL ---- */ + const deepLinkSeenRef = useRef(null); + useEffect(() => { + if (!selectedChannel || messages.length === 0) return; + const params = new URLSearchParams(window.location.search); + const msgId = params.get("msg"); + // Validate format: message ids are uuid4().hex[:12] β€” lowercase hex only. + // Guards against selector-injection via a crafted URL. + if (!msgId || !/^[a-zA-Z0-9_-]{1,64}$/.test(msgId)) return; + const key = `${selectedChannel}:${msgId}`; + if (deepLinkSeenRef.current === key) return; + const el = document.querySelector(`[data-message-id="${msgId}"]`) as HTMLElement | null; + if (el) { + deepLinkSeenRef.current = key; + el.scrollIntoView({ behavior: "smooth", block: "center" }); + el.classList.add("data-highlight"); + setTimeout(() => el.classList.remove("data-highlight"), 2000); + } + }, [selectedChannel, messages.length]); + + /* ---- fetch pins when channel changes ---- */ + useEffect(() => { + if (!selectedChannel) { setPinnedMessages([]); return; } + listPins(selectedChannel) + .then((pins) => setPinnedMessages(pins as PinnedMessage[])) + .catch(() => setPinnedMessages([])); + }, [selectedChannel]); + /* ---- fetch slash commands on channel switch ---- */ useEffect(() => { let alive = true; @@ -556,7 +622,7 @@ export function MessagesApp({ windowId: _windowId, title }: { windowId: string; const sendMessage = async () => { const text = input.trim(); if (!text && pendingAttachments.length === 0) return; - if (!selectedChannel || !wsRef.current || wsRef.current.readyState !== 1) return; + if (!selectedChannel) return; // Block send while uploads are in-flight if (pendingAttachments.some((a) => a.uploading)) { @@ -630,7 +696,31 @@ export function MessagesApp({ windowId: _windowId, title }: { windowId: string; } } setSendError(null); - wsRef.current.send(JSON.stringify({ type: "message", channel_id: selectedChannel, content: text })); + // WS fallback for plain text messages. If WS is down, POST to /api/chat/messages + // so the send still lands. + if (wsRef.current && wsRef.current.readyState === 1) { + wsRef.current.send(JSON.stringify({ type: "message", channel_id: selectedChannel, content: text })); + } else { + try { + const r = await fetch("/api/chat/messages", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + channel_id: selectedChannel, + author_id: "user", author_type: "user", + content: text, content_type: "text", + }), + }); + if (!r.ok) { + const body = await r.json().catch(() => ({})); + setSendError((body as { error?: string }).error || "couldn't send message"); + return; + } + } catch (e) { + setSendError((e as Error).message || "send failed"); + return; + } + } setInput(""); autoScrollRef.current = true; if (inputRef.current) inputRef.current.style.height = "auto"; @@ -773,6 +863,77 @@ export function MessagesApp({ windowId: _windowId, title }: { windowId: string; } }, [selectedChannel, fetchArchivedChannels]); + /* ---- overflow menu handlers ---- */ + const handleEdit = (msgId: string) => { + setEditingMessageId(msgId); + setOverflowMenu(null); + }; + + const handleSaveEdit = async (msgId: string, content: string) => { + try { + await apiEditMessage(msgId, content); + setEditingMessageId(null); + } catch (e) { + setSendError((e as Error).message); + } + }; + + const handleDelete = async (msgId: string) => { + setOverflowMenu(null); + if (!window.confirm("Delete this message?")) return; + try { + await apiDeleteMessage(msgId); + } catch (e) { + setSendError((e as Error).message); + } + }; + + const handleCopyLink = async (msgId: string) => { + setOverflowMenu(null); + if (!selectedChannel) return; + const url = `${window.location.origin}/chat/${selectedChannel}?msg=${msgId}`; + try { + await navigator.clipboard.writeText(url); + } catch { /* ignore */ } + }; + + const handlePin = async (msg: Message) => { + setOverflowMenu(null); + const isPinned = pinnedMessages.some((p) => p.id === msg.id); + try { + if (isPinned) await unpinMessage(msg.id); + else await pinMessage(msg.id); + if (selectedChannel) { + const pins = await listPins(selectedChannel); + setPinnedMessages(pins as PinnedMessage[]); + } + } catch (e) { + setSendError((e as Error).message); + } + }; + + const handleMarkUnread = async (msgId: string) => { + setOverflowMenu(null); + if (!selectedChannel) return; + try { + await apiMarkUnread(selectedChannel, msgId); + } catch (e) { + setSendError((e as Error).message); + } + }; + + const handlePinRequest = async (msgId: string) => { + try { + await pinMessage(msgId); + if (selectedChannel) { + const pins = await listPins(selectedChannel); + setPinnedMessages(pins as PinnedMessage[]); + } + } catch (e) { + setSendError((e as Error).message); + } + }; + /* ---- group channels by type ---- */ const grouped = { dm: channels.filter((c) => c.type === "dm"), @@ -1096,6 +1257,27 @@ export function MessagesApp({ windowId: _windowId, title }: { windowId: string; rel="noreferrer" className="ml-1 opacity-60 hover:opacity-100 text-[12px]" >? +
+ setPinnedPopoverOpen((open) => !open)} + /> + {pinnedPopoverOpen && ( + { + setPinnedPopoverOpen(false); + const el = document.querySelector(`[data-message-id="${id}"]`) as HTMLElement | null; + if (el) { + el.scrollIntoView({ behavior: "smooth", block: "center" }); + el.classList.add("data-highlight"); + setTimeout(() => el.classList.remove("data-highlight"), 2000); + } + }} + onClose={() => setPinnedPopoverOpen(false)} + /> + )} +
{currentChannel?.description && (
{currentChannel.description}
@@ -1150,6 +1332,7 @@ export function MessagesApp({ windowId: _windowId, title }: { windowId: string; return (
(edited)}
)} -
- {renderContent(msg.content)} - {msg.state === "pending" && ( - ... - )} - {msg.state === "streaming" && ( - - - - - - )} - {msg.state === "error" && ( - (error) - )} -
+ {msg.deleted_at ? ( + + ) : editingMessageId === msg.id ? ( + handleSaveEdit(msg.id, content)} + onCancel={() => setEditingMessageId(null)} + /> + ) : ( +
+ {renderContent(msg.content)} + {msg.state === "pending" && ( + ... + )} + {msg.state === "streaming" && ( + + + + + + )} + {msg.state === "error" && ( + (error) + )} +
+ )} + {msg.metadata?.pin_requested && msg.author_type === "agent" && ( + handlePinRequest(msg.id)} + /> + )} {/* canvas attachment */} {msg.content_type === "canvas" && (msg.metadata?.canvas_url || msg.metadata?.canvas_id) && ( @@ -1263,11 +1462,9 @@ export function MessagesApp({ windowId: _windowId, title }: { windowId: string; setShowEmoji(showEmoji === msg.id ? null : msg.id)} onReplyInThread={() => handleOpenThreadFor(msg.channel_id ?? selectedChannel ?? "", msg.id)} - onMore={(e) => { + onOverflow={(e) => { e.preventDefault(); - if (msg.author_type === "agent") { - setContextMenu({ slug: msg.author_id, x: e.clientX, y: e.clientY }); - } + setOverflowMenu({ messageId: msg.id, x: e.clientX, y: e.clientY }); }} /> @@ -1483,6 +1680,30 @@ export function MessagesApp({ windowId: _windowId, title }: { windowId: string; /> + {/* ---- Message Overflow Menu ---- */} + {overflowMenu && (() => { + const msg = messages.find((m) => m.id === overflowMenu.messageId); + if (!msg) return null; + return ( + <> +
setOverflowMenu(null)} /> +
+ p.id === msg.id)} + onEdit={() => handleEdit(msg.id)} + onDelete={() => handleDelete(msg.id)} + onCopyLink={() => handleCopyLink(msg.id)} + onPin={() => handlePin(msg)} + onMarkUnread={() => handleMarkUnread(msg.id)} + onClose={() => setOverflowMenu(null)} + /> +
+ + ); + })()} + {/* ---- Channel Settings Panel ---- */} {showSettings && currentChannel && ( { - await fetch("/api/chat/messages", { + const r = await fetch("/api/chat/messages", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ @@ -1520,6 +1741,10 @@ export function MessagesApp({ windowId: _windowId, title }: { windowId: string; attachments, }), }); + if (!r.ok) { + const body = await r.json().catch(() => ({})); + throw new Error((body as { error?: string }).error || `HTTP ${r.status}`); + } }} /> )} diff --git a/desktop/src/apps/chat/MessageEditor.tsx b/desktop/src/apps/chat/MessageEditor.tsx new file mode 100644 index 00000000..7ccd8331 --- /dev/null +++ b/desktop/src/apps/chat/MessageEditor.tsx @@ -0,0 +1,30 @@ +import { useState } from "react"; + +export function MessageEditor({ + initial, onSave, onCancel, +}: { + initial: string; + onSave: (content: string) => void; + onCancel: () => void; +}) { + const [value, setValue] = useState(initial); + return ( +