Chat bubbles, icons for chats, more vivid and distinct colour tags
- Bubbles: own messages right-aligned in their own colour, agent answers left. - Chat icons (22, validated on the server), shown in list, top bar, project page. - Colour tags: eight fixed hues (new: pink) with theme-matched lightness and the highest chroma sRGB allows; stronger background tint. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
f34a0c7a73
commit
ce0d8afe69
13 files changed
+180
-37
No files matched your search
@@ -26,7 +26,7 @@ import { ShellsChip } from "./ShellsPanel";
|
||||
import { TodoList, ToolCard } from "./ToolCard";
|
||||
import { confirmDialog, promptDialog, toast } from "../dialogs";
|
||||
import { ChatTabBar, ChatTabPanes, useChatTabs } from "./ChatTabs";
|
||||
import { ColorSwatches } from "./ColorSwatches";
|
||||
import { ColorSwatches, IconPicker } from "./ColorSwatches";
|
||||
import { ProjectPermissions } from "./ProjectPermissions";
|
||||
import { OutboxBar } from "./OutboxBar";
|
||||
import { addToOutbox, isOffline } from "../outbox";
|
||||
@@ -374,7 +374,7 @@ function WorkBlock({ messages, steps, open: forced, baseDir, children }: { messa
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className="msg work-block" data-mid={answer?.id}>
|
||||
<div className={`msg work-block ${answerText ? "bubble agent" : ""}`} data-mid={answer?.id}>
|
||||
<button className="work-head" onClick={() => setOpen(true)} aria-expanded={false}>
|
||||
<Icon name="chevron-right" className="tiny" /> {t("chat.workSteps", { n: steps.length })}
|
||||
{files.size > 0 && <span> · {t("chat.workFiles", { n: files.size })}</span>}
|
||||
@@ -423,7 +423,7 @@ const AssistantOrNote = memo(function AssistantOrNote({
|
||||
const cls = `msg ${highlight === m.id ? "highlight" : ""}`;
|
||||
if (m.role === "assistant") {
|
||||
return (
|
||||
<div className={cls} data-mid={m.id}>
|
||||
<div className={`${cls} ${textOfMessage(m).trim() ? "bubble agent" : ""}`} data-mid={m.id}>
|
||||
<MessageMenu messageId={m.id} text={textOfMessage(m)} forkable={!m.parts.some((p) => p.type === "tool_call")} />
|
||||
<AssistantParts parts={m.parts} results={results} running={busy} live={false} baseDir={baseDir} />
|
||||
<MsgTime ms={m.created_at} />
|
||||
@@ -466,7 +466,7 @@ const MessageItem = memo(function MessageItem({ m, canEdit, onRewind, highlight
|
||||
const texts = allTexts.filter((p) => !isHarnessNote(p.text));
|
||||
const imgs = m.parts.filter((p) => p.type === "image") as { image: ImageData }[];
|
||||
return (
|
||||
<div className={`msg ${highlight === m.id ? "highlight" : ""}`} data-mid={m.id}>
|
||||
<div className={`msg user-msg ${highlight === m.id ? "highlight" : ""}`} data-mid={m.id}>
|
||||
{background.map((p, i) => (
|
||||
<HarnessMessage key={i} text={p.text} />
|
||||
))}
|
||||
@@ -781,6 +781,7 @@ export function ChatView({ sessionId, onMenu, onClosePane }: { sessionId: string
|
||||
<Icon name="undo" />
|
||||
</a>
|
||||
)}
|
||||
{session.icon && <Icon name={session.icon} className="chat-icon" />}
|
||||
{session.title || t("session.untitled")}
|
||||
{!readOnly && (
|
||||
<button className="ghost small title-edit" onClick={() => setEditingTitle(true)} title={t("rename.title")} aria-label={t("rename.title")}>
|
||||
@@ -877,6 +878,7 @@ export function ChatView({ sessionId, onMenu, onClosePane }: { sessionId: string
|
||||
<Icon name="edit" /> {t("instructions.title")}
|
||||
</a>
|
||||
<ColorSwatches value={session.color} onChange={(color) => void update({ color })} />
|
||||
<IconPicker value={session.icon} color={session.color} onChange={(icon) => void update({ icon })} />
|
||||
<a href="#" onClick={(e) => (e.preventDefault(), ((e.currentTarget.closest("details") as HTMLDetailsElement).open = false), void planJob())} title={t("job.planHint")}>
|
||||
<Icon name="clock" /> {t("job.plan")}
|
||||
</a>
|
||||
@@ -1012,7 +1014,7 @@ export function ChatView({ sessionId, onMenu, onClosePane }: { sessionId: string
|
||||
</button>
|
||||
)}
|
||||
{draft && (
|
||||
<div className="msg">
|
||||
<div className={`msg ${draft.parts.some((p) => p.type === "text" && p.text.trim()) ? "bubble agent" : ""}`}>
|
||||
<AssistantParts parts={draft.parts} results={results} running={busy} live baseDir={session.cwd} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -1,9 +1,16 @@
|
||||
import { useT, type Key } from "../i18n";
|
||||
import { Icon } from "./Icon";
|
||||
|
||||
/** Colour tags of chats: names, drawn in the colours of the active theme (see `[data-color]` in styles.css). */
|
||||
export const SESSION_COLORS = ["red", "orange", "yellow", "green", "aqua", "blue", "purple"] as const;
|
||||
export const SESSION_COLORS = ["red", "orange", "yellow", "green", "aqua", "blue", "purple", "pink"] as const;
|
||||
|
||||
/** Row of colour dots; the selected one is ringed, a second click (or ✕) removes the tag. */
|
||||
/** Icons a chat can get (must match SESSION_ICONS on the server). */
|
||||
export const SESSION_ICONS = [
|
||||
"robot", "brain", "terminal", "wrench", "gear", "chart", "shield", "image", "music", "cube", "file-text", "folder", "search", "sparkle", "clock", "lock",
|
||||
"users", "toolbox", "plug", "scroll", "eye", "template",
|
||||
] as const;
|
||||
|
||||
/** Row of colour dots; the selected one is ringed, a second click removes the tag. */
|
||||
export function ColorSwatches({ value, onChange }: { value?: string; onChange: (color: string) => void }) {
|
||||
const t = useT();
|
||||
return (
|
||||
@@ -27,3 +34,29 @@ export function ColorSwatches({ value, onChange }: { value?: string; onChange: (
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/** Grid of icons for a chat (in the colour of the chat); a second click removes the icon. */
|
||||
export function IconPicker({ value, color, onChange }: { value?: string; color?: string; onChange: (icon: string) => void }) {
|
||||
const t = useT();
|
||||
return (
|
||||
<div className="icon-picker" role="radiogroup" aria-label={t("icon.label")} data-color={color}>
|
||||
{SESSION_ICONS.map((i) => (
|
||||
<button
|
||||
key={i}
|
||||
type="button"
|
||||
role="radio"
|
||||
aria-checked={value === i}
|
||||
className={`icon-choice ${value === i ? "on" : ""}`}
|
||||
title={i}
|
||||
aria-label={i}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onChange(value === i ? "" : i);
|
||||
}}
|
||||
>
|
||||
<Icon name={i} />
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -96,7 +96,10 @@ export function ProjectPage({ cwd, onMenu }: { cwd: string; onMenu: () => void }
|
||||
<li key={s.id} data-color={s.color} className={s.color ? "tagged" : ""}>
|
||||
<a href={`#/s/${s.id}`} onClick={(e) => (e.preventDefault(), navigate(`/s/${s.id}`))}>
|
||||
<div className="tl-title">
|
||||
<span className={s.color ? "color-badge" : undefined}>{s.title || t("session.untitled")}</span>
|
||||
<span className={s.color ? "color-badge" : undefined}>
|
||||
{s.icon && <Icon name={s.icon} className="tiny chat-icon" />}
|
||||
{s.title || t("session.untitled")}
|
||||
</span>
|
||||
<span className="tl-when" title={fmtDate(s.updated_at)}>
|
||||
{fmtRelative(s.updated_at)}
|
||||
</span>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ColorSwatches } from "./ColorSwatches";
|
||||
import { ColorSwatches, IconPicker } from "./ColorSwatches";
|
||||
import { Icon } from "./Icon";
|
||||
import { StatusLeds } from "./StatusLeds";
|
||||
import { cycleThemeMode, getThemeMode, onThemeChange } from "../theme";
|
||||
@@ -26,7 +26,7 @@ function groupKey(ms: number): Key {
|
||||
const GROUP_BY = "codecraft.sidebarGroupBy";
|
||||
const COLLAPSED = "codecraft.sidebarCollapsed";
|
||||
|
||||
const organise = (id: string, body: Partial<Pick<Session, "pinned" | "archived">> & { folder?: string; color?: string }) => patch(`/sessions/${id}`, body).catch((e) => toast(e.message));
|
||||
const organise = (id: string, body: Partial<Pick<Session, "pinned" | "archived">> & { folder?: string; color?: string; icon?: string }) => patch(`/sessions/${id}`, body).catch((e) => toast(e.message));
|
||||
|
||||
/** One session in the list, with a menu to pin, file or archive it. */
|
||||
function SessionRow({ s, active, attention, busy, folders }: { s: Session; active: boolean; attention: boolean; busy: boolean; folders: string[] }) {
|
||||
@@ -57,7 +57,10 @@ function SessionRow({ s, active, attention, busy, folders }: { s: Session; activ
|
||||
<button className={`session-item ${active ? "active" : ""}`} data-color={s.color} onClick={() => navigate(`/s/${s.id}`)}>
|
||||
<div className="title">
|
||||
{attention ? <i className="attn-dot" title={t("sidebar.needsInput")} /> : busy && <i className="busy-ring" title={t("activity.working")} />}
|
||||
<span className={s.color ? "color-badge" : undefined}>{s.title || t("session.untitled")}</span>
|
||||
<span className={s.color ? "color-badge" : undefined}>
|
||||
{s.icon && <Icon name={s.icon} className="tiny chat-icon" />}
|
||||
{s.title || t("session.untitled")}
|
||||
</span>
|
||||
</div>
|
||||
{s.summary && <div className="summary" title={s.summary}>{s.summary}</div>}
|
||||
<div className="sub">
|
||||
@@ -96,6 +99,7 @@ function SessionRow({ s, active, attention, busy, folders }: { s: Session; activ
|
||||
</button>
|
||||
)}
|
||||
<ColorSwatches value={s.color} onChange={(color) => (setMenu(false), organise(s.id, { color }))} />
|
||||
<IconPicker value={s.icon} color={s.color} onChange={(icon) => (setMenu(false), organise(s.id, { icon }))} />
|
||||
<button role="menuitem" onClick={() => (setMenu(false), organise(s.id, { archived: !s.archived }))}>
|
||||
<Icon name="archive" /> {t(s.archived ? "sidebar.unarchive" : "sidebar.archiveOne")}
|
||||
</button>
|
||||
|
||||
Reference in new issue
Block a user