diff --git a/CHANGELOG.md b/CHANGELOG.md index 3d05325..30d7a68 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,18 @@ All notable changes to CodeCraft. Versions follow [Semantic Versioning](https://semver.org/); until 1.0, minor versions may contain breaking changes. +## [Unreleased] + +### Added + +- **18 more chat icons** (gamepad, computer, food, coffee, home, book, code, camera, cart, heart, + star, globe, calendar, car, plane, bulb, money, palette). + +### Changed + +- **The ⋯ menu of a message sits inside its bubble** (in the foot next to the time, also in your own + bubbles); its popup opens upwards within the bubble and is no wider than the bubble. + ## [v202611-00027] – 2026-11-14 ### Added diff --git a/core/src/types.rs b/core/src/types.rs index 61b7356..524524e 100644 --- a/core/src/types.rs +++ b/core/src/types.rs @@ -205,6 +205,7 @@ pub const SESSION_COLORS: &[&str] = &["red", "orange", "yellow", "green", "aqua" pub const SESSION_ICONS: &[&str] = &[ "robot", "brain", "terminal", "wrench", "gear", "chart", "shield", "image", "music", "cube", "file-text", "folder", "search", "sparkle", "clock", "lock", "users", "toolbox", "plug", "scroll", "eye", "template", + "gamepad", "computer", "food", "coffee", "home", "book", "code", "camera", "cart", "heart", "star", "globe", "calendar", "car", "plane", "bulb", "money", "palette", ]; impl Session { @@ -299,3 +300,23 @@ pub fn new_id() -> String { pub fn now_ms() -> i64 { chrono::Utc::now().timestamp_millis() } + +#[cfg(test)] +mod icon_tests { + use super::*; + + /// The client's picker (ColorSwatches.tsx) and the server's whitelist must list the same names. + #[test] + fn session_icons_match_the_client() { + let ts = include_str!("../../web/src/components/ColorSwatches.tsx"); + let list = ts.split("SESSION_ICONS = [").nth(1).and_then(|r| r.split("] as const").next()).expect("icon list in ColorSwatches.tsx"); + let mut client: Vec<&str> = list.split('"').skip(1).step_by(2).collect(); + let mut server: Vec<&str> = SESSION_ICONS.to_vec(); + client.sort(); + server.sort(); + assert_eq!(client, server); + let mut dedup = server.clone(); + dedup.dedup(); + assert_eq!(dedup.len(), server.len(), "duplicate icon names"); + } +} diff --git a/web/src/components/ChatView.tsx b/web/src/components/ChatView.tsx index 6b88bf6..83f6713 100644 --- a/web/src/components/ChatView.tsx +++ b/web/src/components/ChatView.tsx @@ -381,16 +381,25 @@ function WorkBlock({ messages, steps, open: forced, baseDir, children }: { messa {answerText && ( <> - {answer && p.type === "tool_call")} />} - {answer && } + {answer && p.type === "tool_call")} />} />} )} ); } +/** Foot of a bubble: time on the left, the ⋯ menu inside the bubble on the right. */ +function MsgFoot({ ms, menu }: { ms: number; menu: ReactNode }) { + return ( +
+ + {menu} +
+ ); +} + /** Text parts of a message. */ const textOfMessage = (m: Message) => (m.parts.filter((p) => p.type === "text") as { text: string }[]).map((p) => p.text).join("\n\n"); @@ -424,9 +433,8 @@ const AssistantOrNote = memo(function AssistantOrNote({ if (m.role === "assistant") { return (
- p.type === "tool_call")} /> - + p.type === "tool_call")} />} /> {regenerate && }
); @@ -470,10 +478,8 @@ const MessageItem = memo(function MessageItem({ m, canEdit, onRewind, highlight {background.map((p, i) => ( ))} - p.text).join("\n\n")} forkable onRewind={canEdit ? () => onRewind(m) : undefined} />
p.text).join("\n\n")} /> - {imgs.length > 0 && (
{imgs.map((p, i) => ( @@ -483,6 +489,7 @@ const MessageItem = memo(function MessageItem({ m, canEdit, onRewind, highlight ))}
)} + p.text).join("\n\n")} forkable onRewind={canEdit ? () => onRewind(m) : undefined} />} />
); diff --git a/web/src/components/ColorSwatches.tsx b/web/src/components/ColorSwatches.tsx index fe47af7..59da775 100644 --- a/web/src/components/ColorSwatches.tsx +++ b/web/src/components/ColorSwatches.tsx @@ -8,6 +8,7 @@ export const SESSION_COLORS = ["red", "orange", "yellow", "green", "aqua", "blue 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", + "gamepad", "computer", "food", "coffee", "home", "book", "code", "camera", "cart", "heart", "star", "globe", "calendar", "car", "plane", "bulb", "money", "palette", ] as const; /** Row of colour dots; the selected one is ringed, a second click removes the tag. */ diff --git a/web/src/components/Icon.tsx b/web/src/components/Icon.tsx index 73ecab7..295ccd2 100644 --- a/web/src/components/Icon.tsx +++ b/web/src/components/Icon.tsx @@ -34,6 +34,97 @@ const P: Record = { download: , compact: , trash: , + gamepad: ( + <> + + + + ), + computer: ( + <> + + + + ), + food: ( + <> + + + + ), + home: ( + <> + + + + ), + book: ( + <> + + + + ), + code: , + camera: ( + <> + + + + ), + cart: ( + <> + + + + + ), + heart: , + star: , + globe: ( + <> + + + + ), + calendar: ( + <> + + + + ), + coffee: ( + <> + + + + ), + car: ( + <> + + + + + + ), + plane: ( + <> + + + + ), + bulb: , + money: ( + <> + + + + ), + palette: ( + <> + + + + ), reply: , edit: , undo: , diff --git a/web/src/styles.css b/web/src/styles.css index fdcbe53..876fe79 100644 --- a/web/src/styles.css +++ b/web/src/styles.css @@ -524,28 +524,50 @@ button.session-group:hover { position: relative; } /* Message actions: hover on desktop, faintly visible on touch. */ -.msg-menu { - position: absolute; - top: 2px; - right: 2px; - z-index: 3; - opacity: 0; +/* The ⋯ menu sits in the foot of the bubble (next to the time); its popup opens upwards inside + * the bubble and never gets wider than the bubble. Faint on touch, appears on hover on desktop. */ +.msg-foot { + display: flex; + align-items: center; + gap: 6px; + margin-top: 4px; +} +.msg-foot .msg-time { + margin-top: 0; + flex: 1; +} +.msg-menu.menu-pop { + position: static; /* the popup is placed against the bubble */ + flex-shrink: 0; + opacity: 0.45; transition: opacity 0.12s; } -.msg:hover > .msg-menu, +.msg:hover .msg-menu, .msg-menu:focus-within, .msg-menu[open] { opacity: 1; } -@media (pointer: coarse) { - .msg-menu { - opacity: 0.5; - } -} -.msg-menu > summary { - background: var(--bg1); - border: 1px solid var(--bg2); +.msg-menu.menu-pop > summary { + padding: 0 4px; + border-radius: 6px; line-height: 0; + background: transparent; + border: none; +} +.msg-menu.menu-pop > summary:hover { + background: color-mix(in srgb, var(--fg1) 14%, transparent); +} +.msg-menu.menu-pop .menu-pop-body { + top: auto; + bottom: 30px; + right: 8px; + z-index: 30; + min-width: 0; + width: max-content; + max-width: max(calc(100% - 16px), 11rem); +} +.msg-menu.menu-pop .menu-pop-body a { + white-space: normal; } /* Chat bubbles: the agent's answers on the left, your messages on the right in another colour. */ .msg-user { @@ -608,7 +630,7 @@ button.session-group:hover { font-variant-numeric: tabular-nums; white-space: nowrap; } -.msg-user .msg-time { +.msg-user .msg-foot .msg-time { text-align: right; } .msg-user img { diff --git a/web/src/whatsnew.ts b/web/src/whatsnew.ts index 0dd91d5..8925940 100644 --- a/web/src/whatsnew.ts +++ b/web/src/whatsnew.ts @@ -3,6 +3,10 @@ * under "unreleased"; scripts/release.sh renames the key to the release version. */ export const WHATS_NEW: Record = { + unreleased: { + en: ["18 more icons for chats (gamepad, computer, food, …); the ⋯ menu of a message now sits inside its bubble"], + de: ["18 weitere Icons für Chats (Gamepad, Computer, Essen, …); das ⋯-Menü einer Nachricht sitzt jetzt in der Sprechblase"], + }, "202611-00027": { en: ["Chat bubbles (yours on the right), icons for chats and more vivid, clearly different colour tags (⋯ menu of a chat)"], de: ["Sprechblasen (deine rechts), Icons für Chats und kräftigere, besser unterscheidbare Farbmarkierungen (⋯-Menü eines Chats)"],