S2: Colony chat UI — dark industrial design, JetBrains Mono
- Channel sidebar with create - Message timeline with type-based styling (code/result/error/plan) - Agent messages get glow line + AGENT badge - Agent metadata strip (model, hostname, cwd, skill) - Reply-to with context preview - Compose box with message type selector (Alt+1-5) - 3s polling for live updates (WebSocket in S5) - Vite proxy to backend, TypeScript strict mode, Biome linting Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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<string, string> = {
|
||||
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<string, unknown> | null;
|
||||
|
||||
return (
|
||||
<div className="group px-4 py-2 hover:bg-muted/50 transition-colors">
|
||||
<div
|
||||
className={`group relative px-4 py-2 transition-colors hover:bg-[oklch(0.15_0.005_260)] ${
|
||||
isAgent ? "bg-[oklch(0.13_0.008_260)]" : ""
|
||||
}`}
|
||||
>
|
||||
{/* Agent glow line */}
|
||||
{isAgent && (
|
||||
<div className="absolute left-0 top-0 bottom-0 w-[2px] bg-[var(--color-agent-glow)]" />
|
||||
)}
|
||||
|
||||
{/* Reply context */}
|
||||
{replyTarget && (
|
||||
<div className="text-xs text-muted-foreground ml-8 mb-1 flex items-center gap-1">
|
||||
<span className="text-muted-foreground/50">replying to</span>
|
||||
<div className="ml-6 mb-1 text-[11px] text-muted-foreground flex items-center gap-1 opacity-70">
|
||||
<span className="select-none">^</span>
|
||||
<span className="font-medium">{replyTarget.user.display_name}</span>
|
||||
<span className="truncate max-w-64">{replyTarget.content}</span>
|
||||
<span className="truncate max-w-80 opacity-60">
|
||||
{replyTarget.content}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-start gap-2">
|
||||
|
||||
<div className={`flex items-start gap-3 ${cfg.border} ${cfg.border ? "pl-3" : ""}`}>
|
||||
{/* Avatar */}
|
||||
<div
|
||||
className={`w-7 h-7 rounded-full flex-shrink-0 flex items-center justify-center text-xs font-bold ${
|
||||
className={`w-6 h-6 flex-shrink-0 flex items-center justify-center text-[10px] font-bold rounded-sm ${
|
||||
isAgent
|
||||
? "bg-primary text-primary-foreground"
|
||||
: "bg-secondary text-secondary-foreground"
|
||||
? "bg-[var(--color-agent-glow)] text-foreground"
|
||||
: "bg-secondary text-muted-foreground"
|
||||
}`}
|
||||
>
|
||||
{message.user.display_name[0]}
|
||||
</div>
|
||||
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-semibold text-sm">{message.user.display_name}</span>
|
||||
{/* Header line */}
|
||||
<div className="flex items-center gap-2 text-[11px]">
|
||||
<span className={`font-bold ${isAgent ? "text-[oklch(0.75_0.12_250)]" : "text-foreground"}`}>
|
||||
{message.user.display_name}
|
||||
</span>
|
||||
{isAgent && (
|
||||
<Badge variant="outline" className="text-[10px] py-0 h-4">
|
||||
agent
|
||||
</Badge>
|
||||
<span className="text-[10px] px-1 py-0 bg-[var(--color-agent-glow)] rounded-sm text-[oklch(0.8_0.1_250)]">
|
||||
AGENT
|
||||
</span>
|
||||
)}
|
||||
{message.type !== "text" && (
|
||||
<Badge variant="secondary" className="text-[10px] py-0 h-4">
|
||||
{message.type}
|
||||
</Badge>
|
||||
{cfg.label && (
|
||||
<span className={`text-[10px] font-bold ${cfg.labelColor}`}>
|
||||
{cfg.label}
|
||||
</span>
|
||||
)}
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{new Date(message.created_at).toLocaleTimeString()}
|
||||
<span className="text-muted-foreground tabular-nums">
|
||||
{new Date(message.created_at).toLocaleTimeString("en-US", {
|
||||
hour12: false,
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
second: "2-digit",
|
||||
})}
|
||||
</span>
|
||||
<span className="text-muted-foreground opacity-40 tabular-nums">
|
||||
#{Number(message.seq)}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onReply(message.id)}
|
||||
className="text-xs text-muted-foreground opacity-0 group-hover:opacity-100 hover:text-foreground transition-opacity"
|
||||
className="text-[10px] text-muted-foreground opacity-0 group-hover:opacity-60 hover:!opacity-100 transition-opacity ml-auto"
|
||||
>
|
||||
reply
|
||||
REPLY
|
||||
</button>
|
||||
</div>
|
||||
<div className={`mt-0.5 text-sm ${TYPE_STYLES[message.type] || ""}`}>
|
||||
|
||||
{/* Content */}
|
||||
<div className={`mt-0.5 text-[13px] leading-relaxed ${
|
||||
message.type === "code" ? "font-mono bg-[oklch(0.1_0.005_260)] px-2 py-1 rounded-sm whitespace-pre-wrap" : ""
|
||||
} ${
|
||||
message.type === "error" ? "text-[var(--color-msg-error)]" : ""
|
||||
}`}>
|
||||
{isDeleted ? (
|
||||
<span className="italic text-muted-foreground">[deleted]</span>
|
||||
<span className="italic text-muted-foreground opacity-40">
|
||||
[deleted]
|
||||
</span>
|
||||
) : (
|
||||
message.content
|
||||
)}
|
||||
</div>
|
||||
{message.metadata && isAgent && (
|
||||
<div className="mt-1 flex gap-2 text-[10px] text-muted-foreground">
|
||||
{(message.metadata as Record<string, unknown>).model && (
|
||||
<span>{String((message.metadata as Record<string, unknown>).model)}</span>
|
||||
)}
|
||||
{(message.metadata as Record<string, unknown>).hostname && (
|
||||
<span>{String((message.metadata as Record<string, unknown>).hostname)}</span>
|
||||
|
||||
{/* Agent metadata strip */}
|
||||
{meta && isAgent && (
|
||||
<div className="mt-1 flex gap-3 text-[10px] text-muted-foreground opacity-50">
|
||||
{meta.model && <span>{String(meta.model)}</span>}
|
||||
{meta.hostname && <span>{String(meta.hostname)}</span>}
|
||||
{meta.cwd && <span>{String(meta.cwd)}</span>}
|
||||
{meta.skill && (
|
||||
<span className="text-[var(--color-msg-plan)]">
|
||||
{String(meta.skill)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user