diff --git a/ui/colony/package-lock.json b/ui/colony/package-lock.json index 9f90fa9..53887e0 100644 --- a/ui/colony/package-lock.json +++ b/ui/colony/package-lock.json @@ -10,6 +10,7 @@ "dependencies": { "@base-ui/react": "^1.3.0", "@fontsource-variable/geist": "^5.2.8", + "@fontsource/jetbrains-mono": "^5.2.8", "@tailwindcss/vite": "^4.2.2", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", @@ -906,6 +907,15 @@ "url": "https://github.com/sponsors/ayuhito" } }, + "node_modules/@fontsource/jetbrains-mono": { + "version": "5.2.8", + "resolved": "https://registry.npmjs.org/@fontsource/jetbrains-mono/-/jetbrains-mono-5.2.8.tgz", + "integrity": "sha512-6w8/SG4kqvIMu7xd7wt6x3idn1Qux3p9N62s6G3rfldOUYHpWcc2FKrqf+Vo44jRvqWj2oAtTHrZXEP23oSKwQ==", + "license": "OFL-1.1", + "funding": { + "url": "https://github.com/sponsors/ayuhito" + } + }, "node_modules/@hono/node-server": { "version": "1.19.11", "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.11.tgz", diff --git a/ui/colony/package.json b/ui/colony/package.json index fd5e95a..d2770a9 100644 --- a/ui/colony/package.json +++ b/ui/colony/package.json @@ -12,6 +12,7 @@ "dependencies": { "@base-ui/react": "^1.3.0", "@fontsource-variable/geist": "^5.2.8", + "@fontsource/jetbrains-mono": "^5.2.8", "@tailwindcss/vite": "^4.2.2", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", diff --git a/ui/colony/src/App.css b/ui/colony/src/App.css deleted file mode 100644 index f90339d..0000000 --- a/ui/colony/src/App.css +++ /dev/null @@ -1,184 +0,0 @@ -.counter { - font-size: 16px; - padding: 5px 10px; - border-radius: 5px; - color: var(--accent); - background: var(--accent-bg); - border: 2px solid transparent; - transition: border-color 0.3s; - margin-bottom: 24px; - - &:hover { - border-color: var(--accent-border); - } - &:focus-visible { - outline: 2px solid var(--accent); - outline-offset: 2px; - } -} - -.hero { - position: relative; - - .base, - .framework, - .vite { - inset-inline: 0; - margin: 0 auto; - } - - .base { - width: 170px; - position: relative; - z-index: 0; - } - - .framework, - .vite { - position: absolute; - } - - .framework { - z-index: 1; - top: 34px; - height: 28px; - transform: perspective(2000px) rotateZ(300deg) rotateX(44deg) rotateY(39deg) - scale(1.4); - } - - .vite { - z-index: 0; - top: 107px; - height: 26px; - width: auto; - transform: perspective(2000px) rotateZ(300deg) rotateX(40deg) rotateY(39deg) - scale(0.8); - } -} - -#center { - display: flex; - flex-direction: column; - gap: 25px; - place-content: center; - place-items: center; - flex-grow: 1; - - @media (max-width: 1024px) { - padding: 32px 20px 24px; - gap: 18px; - } -} - -#next-steps { - display: flex; - border-top: 1px solid var(--border); - text-align: left; - - & > div { - flex: 1 1 0; - padding: 32px; - @media (max-width: 1024px) { - padding: 24px 20px; - } - } - - .icon { - margin-bottom: 16px; - width: 22px; - height: 22px; - } - - @media (max-width: 1024px) { - flex-direction: column; - text-align: center; - } -} - -#docs { - border-right: 1px solid var(--border); - - @media (max-width: 1024px) { - border-right: none; - border-bottom: 1px solid var(--border); - } -} - -#next-steps ul { - list-style: none; - padding: 0; - display: flex; - gap: 8px; - margin: 32px 0 0; - - .logo { - height: 18px; - } - - a { - color: var(--text-h); - font-size: 16px; - border-radius: 6px; - background: var(--social-bg); - display: flex; - padding: 6px 12px; - align-items: center; - gap: 8px; - text-decoration: none; - transition: box-shadow 0.3s; - - &:hover { - box-shadow: var(--shadow); - } - .button-icon { - height: 18px; - width: 18px; - } - } - - @media (max-width: 1024px) { - margin-top: 20px; - flex-wrap: wrap; - justify-content: center; - - li { - flex: 1 1 calc(50% - 8px); - } - - a { - width: 100%; - justify-content: center; - box-sizing: border-box; - } - } -} - -#spacer { - height: 88px; - border-top: 1px solid var(--border); - @media (max-width: 1024px) { - height: 48px; - } -} - -.ticks { - position: relative; - width: 100%; - - &::before, - &::after { - content: ''; - position: absolute; - top: -4.5px; - border: 5px solid transparent; - } - - &::before { - left: 0; - border-left-color: var(--border); - } - &::after { - right: 0; - border-right-color: var(--border); - } -} diff --git a/ui/colony/src/App.tsx b/ui/colony/src/App.tsx index 46a5992..2c935ab 100644 --- a/ui/colony/src/App.tsx +++ b/ui/colony/src/App.tsx @@ -1,121 +1,120 @@ -import { useState } from 'react' -import reactLogo from './assets/react.svg' -import viteLogo from './assets/vite.svg' -import heroImg from './assets/hero.png' -import './App.css' +import { useCallback, useEffect, useRef, useState } from "react"; +import type { Channel } from "@/types/Channel"; +import type { Message } from "@/types/Message"; +import { getChannels, getMessages } from "@/api"; +import { ChannelSidebar } from "@/components/ChannelSidebar"; +import { MessageItem } from "@/components/MessageItem"; +import { ComposeBox } from "@/components/ComposeBox"; -function App() { - const [count, setCount] = useState(0) +export default function App() { + const [channels, setChannels] = useState([]); + const [activeChannelId, setActiveChannelId] = useState(null); + const [messages, setMessages] = useState([]); + const [replyTo, setReplyTo] = useState(null); + const scrollRef = useRef(null); + + const loadChannels = useCallback(async () => { + const chs = await getChannels(); + setChannels(chs); + if (!activeChannelId && chs.length > 0) { + setActiveChannelId(chs[0].id); + } + }, [activeChannelId]); + + const loadMessages = useCallback(async () => { + if (!activeChannelId) return; + const msgs = await getMessages(activeChannelId); + setMessages(msgs); + }, [activeChannelId]); + + useEffect(() => { + loadChannels(); + }, [loadChannels]); + + useEffect(() => { + loadMessages(); + setReplyTo(null); + }, [loadMessages]); + + // Auto-scroll on new messages + useEffect(() => { + if (scrollRef.current) { + scrollRef.current.scrollTop = scrollRef.current.scrollHeight; + } + }, [messages]); + + // Poll until WebSocket (S5) + useEffect(() => { + const interval = setInterval(loadMessages, 3000); + return () => clearInterval(interval); + }, [loadMessages]); + + const messagesById = new Map(messages.map((m) => [m.id, m])); + const activeChannel = channels.find((c) => c.id === activeChannelId); return ( - <> -
-
- - React logo - Vite logo -
-
-

Get started

-

- Edit src/App.tsx and save to test HMR -

-
- -
+
+ setActiveChannelId(id)} + onChannelCreated={loadChannels} + /> -
- -
-
- -

Documentation

-

Your questions, answered

- +
+ {/* Channel header */} +
+ {activeChannel ? ( + <> + # + + {activeChannel.name} + + {activeChannel.description && ( + + {activeChannel.description} + + )} + + {messages.length} msg + + + ) : ( + + select a channel + + )}
-
- -

Connect with us

-

Join the Vite community

- -
-
-
-
- - ) + {/* Messages */} +
+ {messages.length === 0 && activeChannelId && ( +
+ no messages yet +
+ )} + {messages.map((msg) => ( + + ))} +
+ + {/* Compose */} + {activeChannelId && ( + setReplyTo(null)} + onMessageSent={loadMessages} + /> + )} +
+ + ); } - -export default App diff --git a/ui/colony/src/assets/hero.png b/ui/colony/src/assets/hero.png deleted file mode 100644 index cc51a3d..0000000 Binary files a/ui/colony/src/assets/hero.png and /dev/null differ diff --git a/ui/colony/src/assets/react.svg b/ui/colony/src/assets/react.svg deleted file mode 100644 index 6c87de9..0000000 --- a/ui/colony/src/assets/react.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/ui/colony/src/assets/vite.svg b/ui/colony/src/assets/vite.svg deleted file mode 100644 index 5101b67..0000000 --- a/ui/colony/src/assets/vite.svg +++ /dev/null @@ -1 +0,0 @@ -Vite diff --git a/ui/colony/src/components/ChannelSidebar.tsx b/ui/colony/src/components/ChannelSidebar.tsx index f2474d0..53007a6 100644 --- a/ui/colony/src/components/ChannelSidebar.tsx +++ b/ui/colony/src/components/ChannelSidebar.tsx @@ -1,9 +1,5 @@ import { useState } from "react"; import type { Channel } from "@/types/Channel"; -import { Button } from "@/components/ui/button"; -import { Input } from "@/components/ui/input"; -import { ScrollArea } from "@/components/ui/scroll-area"; -import { Separator } from "@/components/ui/separator"; import { createChannel } from "@/api"; interface Props { @@ -13,7 +9,12 @@ interface Props { onChannelCreated: () => void; } -export function ChannelSidebar({ channels, activeId, onSelect, onChannelCreated }: Props) { +export function ChannelSidebar({ + channels, + activeId, + onSelect, + onChannelCreated, +}: Props) { const [newName, setNewName] = useState(""); const [creating, setCreating] = useState(false); @@ -27,39 +28,50 @@ export function ChannelSidebar({ channels, activeId, onSelect, onChannelCreated } return ( -
-
Colony
- - -
- {channels.map((ch) => ( - - ))} +
+ {/* Header */} +
+
+ COLONY
- - -
- + apes.unslope.com +
+
+ + {/* Channel list */} +
+
+ CHANNELS +
+ {channels.map((ch) => ( + + ))} +
+ + {/* New channel input */} +
+ setNewName(e.target.value)} onKeyDown={(e) => e.key === "Enter" && handleCreate()} - className="text-sm h-8" + disabled={creating} + className="w-full bg-sidebar-accent text-[11px] text-sidebar-foreground placeholder:text-muted-foreground px-2 py-1 rounded-sm border border-sidebar-border focus:outline-none focus:border-[var(--color-agent-glow)]" /> -
); diff --git a/ui/colony/src/components/ComposeBox.tsx b/ui/colony/src/components/ComposeBox.tsx new file mode 100644 index 0000000..199ec9e --- /dev/null +++ b/ui/colony/src/components/ComposeBox.tsx @@ -0,0 +1,110 @@ +import { useState } from "react"; +import type { MessageType } from "@/types/MessageType"; +import { postMessage } from "@/api"; + +interface Props { + channelId: string; + replyTo: string | null; + onClearReply: () => void; + onMessageSent: () => void; +} + +const TYPES: { value: MessageType; label: string; key: string }[] = [ + { value: "text", label: "TXT", key: "1" }, + { value: "code", label: "CODE", key: "2" }, + { value: "result", label: "RES", key: "3" }, + { value: "error", label: "ERR", key: "4" }, + { value: "plan", label: "PLAN", key: "5" }, +]; + +export function ComposeBox({ + channelId, + replyTo, + onClearReply, + onMessageSent, +}: Props) { + const [content, setContent] = useState(""); + const [msgType, setMsgType] = useState("text"); + const [sending, setSending] = useState(false); + + async function handleSend() { + if (!content.trim() || sending) return; + setSending(true); + await postMessage(channelId, { + content: content.trim(), + type: msgType, + reply_to: replyTo ?? undefined, + }); + setContent(""); + setMsgType("text"); + onClearReply(); + onMessageSent(); + setSending(false); + } + + return ( +
+ {replyTo && ( +
+ replying to #{replyTo.slice(0, 8)} + +
+ )} +
+ {/* Type selector */} +
+ {TYPES.map((t) => ( + + ))} +
+ + {/* Input */} + setContent(e.target.value)} + onKeyDown={(e) => { + if (e.key === "Enter" && !e.shiftKey) { + e.preventDefault(); + handleSend(); + } + // Alt+1-5 for type switching + if (e.altKey && e.key >= "1" && e.key <= "5") { + setMsgType(TYPES[parseInt(e.key) - 1].value); + } + }} + placeholder={`message #${channelId.slice(0, 8)}...`} + disabled={sending} + className="flex-1 bg-input text-[13px] text-foreground placeholder:text-muted-foreground px-3 py-1.5 rounded-sm border border-border focus:outline-none focus:border-[var(--color-agent-glow)]" + /> + + +
+
+ ); +} diff --git a/ui/colony/src/components/MessageItem.tsx b/ui/colony/src/components/MessageItem.tsx index f43d06f..fdea25d 100644 --- a/ui/colony/src/components/MessageItem.tsx +++ b/ui/colony/src/components/MessageItem.tsx @@ -1,5 +1,4 @@ import type { Message } from "@/types/Message"; -import { Badge } from "@/components/ui/badge"; interface Props { message: Message; @@ -7,75 +6,134 @@ interface Props { onReply: (id: string) => void; } -const TYPE_STYLES: Record = { - text: "", - code: "bg-muted font-mono text-sm whitespace-pre-wrap", - result: "border-l-4 border-green-500 pl-3", - error: "border-l-4 border-red-500 pl-3 text-red-700 dark:text-red-400", - plan: "border-l-4 border-blue-500 pl-3", +const TYPE_CONFIG: Record< + string, + { border: string; label: string; labelColor: string } +> = { + text: { border: "", label: "", labelColor: "" }, + code: { + border: "border-l-2 border-[var(--color-msg-code)]", + label: "CODE", + labelColor: "text-[var(--color-msg-code)]", + }, + result: { + border: "border-l-2 border-[var(--color-msg-result)]", + label: "RESULT", + labelColor: "text-[var(--color-msg-result)]", + }, + error: { + border: "border-l-2 border-[var(--color-msg-error)]", + label: "ERROR", + labelColor: "text-[var(--color-msg-error)]", + }, + plan: { + border: "border-l-2 border-[var(--color-msg-plan)]", + label: "PLAN", + labelColor: "text-[var(--color-msg-plan)]", + }, }; export function MessageItem({ message, replyTarget, onReply }: Props) { const isAgent = message.user.role === "agent"; const isDeleted = !!message.deleted_at; + const cfg = TYPE_CONFIG[message.type] || TYPE_CONFIG.text; + const meta = message.metadata as Record | null; return ( -
+
+ {/* Agent glow line */} + {isAgent && ( +
+ )} + + {/* Reply context */} {replyTarget && ( -
- replying to +
+ ^ {replyTarget.user.display_name} - {replyTarget.content} + + {replyTarget.content} +
)} -
+ +
+ {/* Avatar */}
{message.user.display_name[0]}
+
-
- {message.user.display_name} + {/* Header line */} +
+ + {message.user.display_name} + {isAgent && ( - - agent - + + AGENT + )} - {message.type !== "text" && ( - - {message.type} - + {cfg.label && ( + + {cfg.label} + )} - - {new Date(message.created_at).toLocaleTimeString()} + + {new Date(message.created_at).toLocaleTimeString("en-US", { + hour12: false, + hour: "2-digit", + minute: "2-digit", + second: "2-digit", + })} + + + #{Number(message.seq)}
-
+ + {/* Content */} +
{isDeleted ? ( - [deleted] + + [deleted] + ) : ( message.content )}
- {message.metadata && isAgent && ( -
- {(message.metadata as Record).model && ( - {String((message.metadata as Record).model)} - )} - {(message.metadata as Record).hostname && ( - {String((message.metadata as Record).hostname)} + + {/* Agent metadata strip */} + {meta && isAgent && ( +
+ {meta.model && {String(meta.model)}} + {meta.hostname && {String(meta.hostname)}} + {meta.cwd && {String(meta.cwd)}} + {meta.skill && ( + + {String(meta.skill)} + )}
)} diff --git a/ui/colony/src/index.css b/ui/colony/src/index.css index fb3c7e9..a9cc1d3 100644 --- a/ui/colony/src/index.css +++ b/ui/colony/src/index.css @@ -1,130 +1,87 @@ @import "tailwindcss"; -@import "tw-animate-css"; -@import "shadcn/tailwind.css"; -@import "@fontsource-variable/geist"; +@import "@fontsource/jetbrains-mono/400.css"; +@import "@fontsource/jetbrains-mono/500.css"; +@import "@fontsource/jetbrains-mono/700.css"; @custom-variant dark (&:is(.dark *)); @theme inline { - --font-heading: var(--font-sans); - --font-sans: 'Geist Variable', sans-serif; - --color-sidebar-ring: var(--sidebar-ring); - --color-sidebar-border: var(--sidebar-border); - --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); - --color-sidebar-accent: var(--sidebar-accent); - --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); - --color-sidebar-primary: var(--sidebar-primary); - --color-sidebar-foreground: var(--sidebar-foreground); - --color-sidebar: var(--sidebar); - --color-chart-5: var(--chart-5); - --color-chart-4: var(--chart-4); - --color-chart-3: var(--chart-3); - --color-chart-2: var(--chart-2); - --color-chart-1: var(--chart-1); - --color-ring: var(--ring); - --color-input: var(--input); - --color-border: var(--border); - --color-destructive: var(--destructive); - --color-accent-foreground: var(--accent-foreground); - --color-accent: var(--accent); - --color-muted-foreground: var(--muted-foreground); - --color-muted: var(--muted); - --color-secondary-foreground: var(--secondary-foreground); - --color-secondary: var(--secondary); - --color-primary-foreground: var(--primary-foreground); - --color-primary: var(--primary); - --color-popover-foreground: var(--popover-foreground); - --color-popover: var(--popover); - --color-card-foreground: var(--card-foreground); - --color-card: var(--card); - --color-foreground: var(--foreground); - --color-background: var(--background); - --radius-sm: calc(var(--radius) * 0.6); - --radius-md: calc(var(--radius) * 0.8); - --radius-lg: var(--radius); - --radius-xl: calc(var(--radius) * 1.4); - --radius-2xl: calc(var(--radius) * 1.8); - --radius-3xl: calc(var(--radius) * 2.2); - --radius-4xl: calc(var(--radius) * 2.6); + --font-mono: 'JetBrains Mono', ui-monospace, monospace; + --font-sans: 'JetBrains Mono', ui-monospace, monospace; + + --color-ring: var(--ring); + --color-input: var(--input); + --color-border: var(--border); + --color-destructive: var(--destructive); + --color-accent-foreground: var(--accent-foreground); + --color-accent: var(--accent); + --color-muted-foreground: var(--muted-foreground); + --color-muted: var(--muted); + --color-secondary-foreground: var(--secondary-foreground); + --color-secondary: var(--secondary); + --color-primary-foreground: var(--primary-foreground); + --color-primary: var(--primary); + --color-popover-foreground: var(--popover-foreground); + --color-popover: var(--popover); + --color-card-foreground: var(--card-foreground); + --color-card: var(--card); + --color-foreground: var(--foreground); + --color-background: var(--background); + --color-sidebar-foreground: var(--sidebar-foreground); + --color-sidebar: var(--sidebar); + --color-sidebar-accent: var(--sidebar-accent); + --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); + --color-sidebar-border: var(--sidebar-border); + --radius-sm: 2px; + --radius-md: 4px; + --radius-lg: 6px; + + /* message type accents */ + --color-msg-result: oklch(0.72 0.19 145); + --color-msg-error: oklch(0.63 0.24 25); + --color-msg-plan: oklch(0.68 0.16 250); + --color-msg-code: oklch(0.75 0.12 80); + --color-agent-glow: oklch(0.55 0.15 250 / 0.3); } :root { - --background: oklch(1 0 0); - --foreground: oklch(0.145 0 0); - --card: oklch(1 0 0); - --card-foreground: oklch(0.145 0 0); - --popover: oklch(1 0 0); - --popover-foreground: oklch(0.145 0 0); - --primary: oklch(0.205 0 0); - --primary-foreground: oklch(0.985 0 0); - --secondary: oklch(0.97 0 0); - --secondary-foreground: oklch(0.205 0 0); - --muted: oklch(0.97 0 0); - --muted-foreground: oklch(0.556 0 0); - --accent: oklch(0.97 0 0); - --accent-foreground: oklch(0.205 0 0); - --destructive: oklch(0.577 0.245 27.325); - --border: oklch(0.922 0 0); - --input: oklch(0.922 0 0); - --ring: oklch(0.708 0 0); - --chart-1: oklch(0.87 0 0); - --chart-2: oklch(0.556 0 0); - --chart-3: oklch(0.439 0 0); - --chart-4: oklch(0.371 0 0); - --chart-5: oklch(0.269 0 0); - --radius: 0.625rem; - --sidebar: oklch(0.985 0 0); - --sidebar-foreground: oklch(0.145 0 0); - --sidebar-primary: oklch(0.205 0 0); - --sidebar-primary-foreground: oklch(0.985 0 0); - --sidebar-accent: oklch(0.97 0 0); - --sidebar-accent-foreground: oklch(0.205 0 0); - --sidebar-border: oklch(0.922 0 0); - --sidebar-ring: oklch(0.708 0 0); -} - -.dark { - --background: oklch(0.145 0 0); - --foreground: oklch(0.985 0 0); - --card: oklch(0.205 0 0); - --card-foreground: oklch(0.985 0 0); - --popover: oklch(0.205 0 0); - --popover-foreground: oklch(0.985 0 0); - --primary: oklch(0.922 0 0); - --primary-foreground: oklch(0.205 0 0); - --secondary: oklch(0.269 0 0); - --secondary-foreground: oklch(0.985 0 0); - --muted: oklch(0.269 0 0); - --muted-foreground: oklch(0.708 0 0); - --accent: oklch(0.269 0 0); - --accent-foreground: oklch(0.985 0 0); - --destructive: oklch(0.704 0.191 22.216); - --border: oklch(1 0 0 / 10%); - --input: oklch(1 0 0 / 15%); - --ring: oklch(0.556 0 0); - --chart-1: oklch(0.87 0 0); - --chart-2: oklch(0.556 0 0); - --chart-3: oklch(0.439 0 0); - --chart-4: oklch(0.371 0 0); - --chart-5: oklch(0.269 0 0); - --sidebar: oklch(0.205 0 0); - --sidebar-foreground: oklch(0.985 0 0); - --sidebar-primary: oklch(0.488 0.243 264.376); - --sidebar-primary-foreground: oklch(0.985 0 0); - --sidebar-accent: oklch(0.269 0 0); - --sidebar-accent-foreground: oklch(0.985 0 0); - --sidebar-border: oklch(1 0 0 / 10%); - --sidebar-ring: oklch(0.556 0 0); + --background: oklch(0.12 0.005 260); + --foreground: oklch(0.85 0 0); + --card: oklch(0.15 0.005 260); + --card-foreground: oklch(0.85 0 0); + --popover: oklch(0.15 0.005 260); + --popover-foreground: oklch(0.85 0 0); + --primary: oklch(0.85 0 0); + --primary-foreground: oklch(0.12 0.005 260); + --secondary: oklch(0.2 0.005 260); + --secondary-foreground: oklch(0.75 0 0); + --muted: oklch(0.2 0.005 260); + --muted-foreground: oklch(0.55 0 0); + --accent: oklch(0.22 0.01 260); + --accent-foreground: oklch(0.85 0 0); + --destructive: oklch(0.63 0.24 25); + --border: oklch(0.22 0.01 260); + --input: oklch(0.2 0.005 260); + --ring: oklch(0.55 0.15 250); + --sidebar: oklch(0.1 0.005 260); + --sidebar-foreground: oklch(0.7 0 0); + --sidebar-accent: oklch(0.18 0.01 260); + --sidebar-accent-foreground: oklch(0.9 0 0); + --sidebar-border: oklch(0.2 0.01 260); } @layer base { * { - @apply border-border outline-ring/50; - } + @apply border-border; + } body { - @apply bg-background text-foreground; - } - html { - @apply font-sans; - } -} \ No newline at end of file + @apply bg-background text-foreground font-mono; + font-size: 13px; + line-height: 1.5; + } + html, body, #root { + height: 100%; + margin: 0; + overflow: hidden; + } +}