More chat icons; message menu inside the bubble
- 18 more icons (gamepad, computer, food, ...) with a test that keeps the server whitelist and the client picker identical. - The ⋯ menu of a message moves into the bubble foot (next to the time); its popup opens upwards inside the bubble and is limited to the bubble's width. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
44dd6978d4
commit
1b3e3af4b7
7 files changed
+180
-22
No files matched your search
@@ -3,6 +3,18 @@
|
|||||||
All notable changes to CodeCraft. Versions follow [Semantic Versioning](https://semver.org/);
|
All notable changes to CodeCraft. Versions follow [Semantic Versioning](https://semver.org/);
|
||||||
until 1.0, minor versions may contain breaking changes.
|
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
|
## [v202611-00027] – 2026-11-14
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
@@ -205,6 +205,7 @@ pub const SESSION_COLORS: &[&str] = &["red", "orange", "yellow", "green", "aqua"
|
|||||||
pub const SESSION_ICONS: &[&str] = &[
|
pub const SESSION_ICONS: &[&str] = &[
|
||||||
"robot", "brain", "terminal", "wrench", "gear", "chart", "shield", "image", "music", "cube", "file-text", "folder", "search", "sparkle", "clock", "lock",
|
"robot", "brain", "terminal", "wrench", "gear", "chart", "shield", "image", "music", "cube", "file-text", "folder", "search", "sparkle", "clock", "lock",
|
||||||
"users", "toolbox", "plug", "scroll", "eye", "template",
|
"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 {
|
impl Session {
|
||||||
@@ -299,3 +300,23 @@ pub fn new_id() -> String {
|
|||||||
pub fn now_ms() -> i64 {
|
pub fn now_ms() -> i64 {
|
||||||
chrono::Utc::now().timestamp_millis()
|
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");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -381,16 +381,25 @@ function WorkBlock({ messages, steps, open: forced, baseDir, children }: { messa
|
|||||||
</button>
|
</button>
|
||||||
{answerText && (
|
{answerText && (
|
||||||
<>
|
<>
|
||||||
{answer && <MessageMenu messageId={answer.id} text={answerText} forkable={!answer.parts.some((p) => p.type === "tool_call")} />}
|
|
||||||
<Markdown text={answerText} baseDir={baseDir} />
|
<Markdown text={answerText} baseDir={baseDir} />
|
||||||
<ImageRefs text={answerText} baseDir={baseDir} />
|
<ImageRefs text={answerText} baseDir={baseDir} />
|
||||||
{answer && <MsgTime ms={answer.created_at} />}
|
{answer && <MsgFoot ms={answer.created_at} menu={<MessageMenu messageId={answer.id} text={answerText} forkable={!answer.parts.some((p) => p.type === "tool_call")} />} />}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 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 (
|
||||||
|
<div className="msg-foot">
|
||||||
|
<MsgTime ms={ms} />
|
||||||
|
{menu}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/** Text parts of a message. */
|
/** 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");
|
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") {
|
if (m.role === "assistant") {
|
||||||
return (
|
return (
|
||||||
<div className={`${cls} ${textOfMessage(m).trim() ? "bubble agent" : ""}`} 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} />
|
<AssistantParts parts={m.parts} results={results} running={busy} live={false} baseDir={baseDir} />
|
||||||
<MsgTime ms={m.created_at} />
|
<MsgFoot ms={m.created_at} menu={<MessageMenu messageId={m.id} text={textOfMessage(m)} forkable={!m.parts.some((p) => p.type === "tool_call")} />} />
|
||||||
{regenerate && <RegenerateButton sessionId={regenerate.sessionId} messageId={m.id} model={regenerate.model} />}
|
{regenerate && <RegenerateButton sessionId={regenerate.sessionId} messageId={m.id} model={regenerate.model} />}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -470,10 +478,8 @@ const MessageItem = memo(function MessageItem({ m, canEdit, onRewind, highlight
|
|||||||
{background.map((p, i) => (
|
{background.map((p, i) => (
|
||||||
<HarnessMessage key={i} text={p.text} />
|
<HarnessMessage key={i} text={p.text} />
|
||||||
))}
|
))}
|
||||||
<MessageMenu messageId={m.id} text={texts.map((p) => p.text).join("\n\n")} forkable onRewind={canEdit ? () => onRewind(m) : undefined} />
|
|
||||||
<div className="msg-user">
|
<div className="msg-user">
|
||||||
<UserText text={texts.map((p) => p.text).join("\n\n")} />
|
<UserText text={texts.map((p) => p.text).join("\n\n")} />
|
||||||
<MsgTime ms={m.created_at} />
|
|
||||||
{imgs.length > 0 && (
|
{imgs.length > 0 && (
|
||||||
<div className="user-files">
|
<div className="user-files">
|
||||||
{imgs.map((p, i) => (
|
{imgs.map((p, i) => (
|
||||||
@@ -483,6 +489,7 @@ const MessageItem = memo(function MessageItem({ m, canEdit, onRewind, highlight
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
<MsgFoot ms={m.created_at} menu={<MessageMenu messageId={m.id} text={texts.map((p) => p.text).join("\n\n")} forkable onRewind={canEdit ? () => onRewind(m) : undefined} />} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ export const SESSION_COLORS = ["red", "orange", "yellow", "green", "aqua", "blue
|
|||||||
export const SESSION_ICONS = [
|
export const SESSION_ICONS = [
|
||||||
"robot", "brain", "terminal", "wrench", "gear", "chart", "shield", "image", "music", "cube", "file-text", "folder", "search", "sparkle", "clock", "lock",
|
"robot", "brain", "terminal", "wrench", "gear", "chart", "shield", "image", "music", "cube", "file-text", "folder", "search", "sparkle", "clock", "lock",
|
||||||
"users", "toolbox", "plug", "scroll", "eye", "template",
|
"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;
|
] as const;
|
||||||
|
|
||||||
/** Row of colour dots; the selected one is ringed, a second click removes the tag. */
|
/** Row of colour dots; the selected one is ringed, a second click removes the tag. */
|
||||||
|
|||||||
@@ -34,6 +34,97 @@ const P: Record<string, ReactNode> = {
|
|||||||
download: <path d="M12 4v11M7 10l5 5 5-5M5 20h14" />,
|
download: <path d="M12 4v11M7 10l5 5 5-5M5 20h14" />,
|
||||||
compact: <path d="M8 4l4 4 4-4M8 20l4-4 4 4M4 12h16" />,
|
compact: <path d="M8 4l4 4 4-4M8 20l4-4 4 4M4 12h16" />,
|
||||||
trash: <path d="M4 7h16M9 7V4h6v3M6 7l1 13h10l1-13M10 11v6M14 11v6" />,
|
trash: <path d="M4 7h16M9 7V4h6v3M6 7l1 13h10l1-13M10 11v6M14 11v6" />,
|
||||||
|
gamepad: (
|
||||||
|
<>
|
||||||
|
<path d="M7 8h10a4 4 0 0 1 4 4.2l-.6 3.8a2.4 2.4 0 0 1-4.1 1.3L14 15h-4l-2.3 2.3a2.4 2.4 0 0 1-4.1-1.3L3 12.2A4 4 0 0 1 7 8Z" />
|
||||||
|
<path d="M8 10.5v3M6.5 12h3M15.5 11h.01M17.5 13h.01" />
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
computer: (
|
||||||
|
<>
|
||||||
|
<rect x="3" y="4" width="18" height="12" rx="2" />
|
||||||
|
<path d="M8 20h8M12 16v4" />
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
food: (
|
||||||
|
<>
|
||||||
|
<path d="M5 3v6a2 2 0 0 0 4 0V3M7 11v10" />
|
||||||
|
<path d="M17 21V3c-2.6 1.4-3.5 4.2-3.5 7.5H17" />
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
home: (
|
||||||
|
<>
|
||||||
|
<path d="M4 11.5 12 4l8 7.5V20H4Z" />
|
||||||
|
<path d="M10 20v-5h4v5" />
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
book: (
|
||||||
|
<>
|
||||||
|
<path d="M5 4h11a3 3 0 0 1 3 3v13H8a3 3 0 0 1-3-3Z" />
|
||||||
|
<path d="M5 17a3 3 0 0 1 3-3h11" />
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
code: <path d="m8 7-5 5 5 5M16 7l5 5-5 5M14 5l-4 14" />,
|
||||||
|
camera: (
|
||||||
|
<>
|
||||||
|
<path d="M4 8h3l1.5-2h7L17 8h3v11H4Z" />
|
||||||
|
<circle cx="12" cy="13" r="3.5" />
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
cart: (
|
||||||
|
<>
|
||||||
|
<path d="M3 4h2.5l2 11h10l2-8H7" />
|
||||||
|
<circle cx="9" cy="19" r="1.4" />
|
||||||
|
<circle cx="17" cy="19" r="1.4" />
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
heart: <path d="M12 20s-8-4.6-8-10.5A4.5 4.5 0 0 1 12 7a4.5 4.5 0 0 1 8 2.5C20 15.4 12 20 12 20Z" />,
|
||||||
|
star: <path d="m12 3.5 2.6 5.4 5.9.8-4.3 4.1 1 5.9L12 16.9l-5.2 2.8 1-5.9-4.3-4.1 5.9-.8Z" />,
|
||||||
|
globe: (
|
||||||
|
<>
|
||||||
|
<circle cx="12" cy="12" r="8.5" />
|
||||||
|
<path d="M3.5 12h17M12 3.5c3 3 3 14 0 17M12 3.5c-3 3-3 14 0 17" />
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
calendar: (
|
||||||
|
<>
|
||||||
|
<rect x="4" y="5" width="16" height="15" rx="2" />
|
||||||
|
<path d="M4 10h16M8 3v4M16 3v4" />
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
coffee: (
|
||||||
|
<>
|
||||||
|
<path d="M5 9h11v5a5 5 0 0 1-5 5H10a5 5 0 0 1-5-5Z" />
|
||||||
|
<path d="M16 10h1.5a2.5 2.5 0 0 1 0 5H16M8 3v3M12 3v3" />
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
car: (
|
||||||
|
<>
|
||||||
|
<path d="M3.5 16v-4l2-5h13l2 5v4Z" />
|
||||||
|
<path d="M3.5 12h17" />
|
||||||
|
<circle cx="7.5" cy="16" r="1.5" />
|
||||||
|
<circle cx="16.5" cy="16" r="1.5" />
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
plane: (
|
||||||
|
<>
|
||||||
|
<path d="M21 3 3 10.5l6.5 2.5L12 20Z" />
|
||||||
|
<path d="M9.5 13 21 3" />
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
bulb: <path d="M9 18h6M10 21h4M12 3a6 6 0 0 0-3.5 10.9c.6.5 1 1.3 1 2.1h5c0-.8.4-1.6 1-2.1A6 6 0 0 0 12 3Z" />,
|
||||||
|
money: (
|
||||||
|
<>
|
||||||
|
<circle cx="12" cy="12" r="8.5" />
|
||||||
|
<path d="M14.5 9.2c-.5-.8-1.4-1.2-2.5-1.2-1.5 0-2.5.8-2.5 1.9 0 2.7 5 1.2 5 4 0 1.1-1 2-2.5 2-1.1 0-2.1-.5-2.6-1.4M12 6.5V8M12 16v1.5" />
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
palette: (
|
||||||
|
<>
|
||||||
|
<path d="M12 3.5a8.5 8.5 0 1 0 0 17c1.3 0 1.8-1 1.3-2-.6-1.2.2-2.5 1.6-2.5H17a3.5 3.5 0 0 0 3.5-3.5C20.5 7 16.8 3.5 12 3.5Z" />
|
||||||
|
<path d="M7.5 11h.01M9.5 7.5h.01M14 7.5h.01" />
|
||||||
|
</>
|
||||||
|
),
|
||||||
reply: <path d="M9 7 4 12l5 5M4 12h9a7 7 0 0 1 7 7" />,
|
reply: <path d="M9 7 4 12l5 5M4 12h9a7 7 0 0 1 7 7" />,
|
||||||
edit: <path d="M4 20h4L19 9l-4-4L4 16v4ZM13.5 6.5l4 4" />,
|
edit: <path d="M4 20h4L19 9l-4-4L4 16v4ZM13.5 6.5l4 4" />,
|
||||||
undo: <path d="M9 14 4 9l5-5M4 9h10a6 6 0 0 1 0 12h-3" />,
|
undo: <path d="M9 14 4 9l5-5M4 9h10a6 6 0 0 1 0 12h-3" />,
|
||||||
|
|||||||
+38
-16
@@ -524,28 +524,50 @@ button.session-group:hover {
|
|||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
/* Message actions: hover on desktop, faintly visible on touch. */
|
/* Message actions: hover on desktop, faintly visible on touch. */
|
||||||
.msg-menu {
|
/* The ⋯ menu sits in the foot of the bubble (next to the time); its popup opens upwards inside
|
||||||
position: absolute;
|
* the bubble and never gets wider than the bubble. Faint on touch, appears on hover on desktop. */
|
||||||
top: 2px;
|
.msg-foot {
|
||||||
right: 2px;
|
display: flex;
|
||||||
z-index: 3;
|
align-items: center;
|
||||||
opacity: 0;
|
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;
|
transition: opacity 0.12s;
|
||||||
}
|
}
|
||||||
.msg:hover > .msg-menu,
|
.msg:hover .msg-menu,
|
||||||
.msg-menu:focus-within,
|
.msg-menu:focus-within,
|
||||||
.msg-menu[open] {
|
.msg-menu[open] {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
@media (pointer: coarse) {
|
.msg-menu.menu-pop > summary {
|
||||||
.msg-menu {
|
padding: 0 4px;
|
||||||
opacity: 0.5;
|
border-radius: 6px;
|
||||||
}
|
|
||||||
}
|
|
||||||
.msg-menu > summary {
|
|
||||||
background: var(--bg1);
|
|
||||||
border: 1px solid var(--bg2);
|
|
||||||
line-height: 0;
|
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. */
|
/* Chat bubbles: the agent's answers on the left, your messages on the right in another colour. */
|
||||||
.msg-user {
|
.msg-user {
|
||||||
@@ -608,7 +630,7 @@ button.session-group:hover {
|
|||||||
font-variant-numeric: tabular-nums;
|
font-variant-numeric: tabular-nums;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
.msg-user .msg-time {
|
.msg-user .msg-foot .msg-time {
|
||||||
text-align: right;
|
text-align: right;
|
||||||
}
|
}
|
||||||
.msg-user img {
|
.msg-user img {
|
||||||
|
|||||||
@@ -3,6 +3,10 @@
|
|||||||
* under "unreleased"; scripts/release.sh renames the key to the release version.
|
* under "unreleased"; scripts/release.sh renames the key to the release version.
|
||||||
*/
|
*/
|
||||||
export const WHATS_NEW: Record<string, { en: string[]; de: string[] }> = {
|
export const WHATS_NEW: Record<string, { en: string[]; de: string[] }> = {
|
||||||
|
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": {
|
"202611-00027": {
|
||||||
en: ["Chat bubbles (yours on the right), icons for chats and more vivid, clearly different colour tags (⋯ menu of a chat)"],
|
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)"],
|
de: ["Sprechblasen (deine rechts), Icons für Chats und kräftigere, besser unterscheidbare Farbmarkierungen (⋯-Menü eines Chats)"],
|
||||||
|
|||||||
Reference in new issue
Block a user