Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions docs/ui-behavior.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,8 +126,11 @@ Generated-image items show the app-server-saved image as a bounded thumbnail.
Selecting it opens the shared non-modal image viewer; encoded image data is
never displayed as generic activity text.

Process cards remain neutral so they support rather than dominate the user and
Codex conversation. Status text alone uses canonical semantic state colors.
User messages use the canonical soft-violet identity surface. Final Codex
messages use the canonical soft-blue identity surface, while interim Codex
updates remain neutral and identify their phase in the header. Process cards
also remain neutral so they support rather than dominate the primary exchange.
Status text alone uses canonical semantic state colors.

Every conversation card with visible detail uses the same keyboard-focusable
disclosure chevron: down when expanded and left when collapsed. Title-only
Expand Down
31 changes: 17 additions & 14 deletions src/codex/middle/ConversationCards.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -783,6 +783,13 @@ class ConversationCard::Impl final {
}

void updateComposition(const AgentMessageData &message) {
const QString messagePhase =
message.finalAnswer ? QStringLiteral("final") : QStringLiteral("update");
if (owner->property("messagePhase").toString() != messagePhase) {
owner->setProperty("messagePhase", messagePhase);
owner->style()->unpolish(owner);
owner->style()->polish(owner);
}
phase->setText(message.finalAnswer ? QStringLiteral("final answer")
: QStringLiteral("update"));
const QString phaseStatus = message.finalAnswer
Expand Down Expand Up @@ -974,10 +981,8 @@ class ConversationCard::Impl final {
const bool waiting = prompt->state == PromptState::Queued ||
prompt->state == PromptState::InFlight;
const bool failed = prompt->state == PromptState::Failed;
const QString foreground = waiting || transitioning
? QStringLiteral("#536b8f")
: failed ? QStringLiteral("#982f3d")
: QStringLiteral("#1d2633");
const QString foreground = failed ? QStringLiteral("#982f3d")
: QStringLiteral("#53389e");
const QString style =
QStringLiteral("background:transparent;color:%1;").arg(foreground);
bool changed = false;
Expand Down Expand Up @@ -1084,13 +1089,11 @@ void ConversationCard::paintEvent(QPaintEvent *event) {
const bool transitioning = acceptedTransitionActive(*prompt, now);
const bool failed = prompt->state == PromptState::Failed;
const QColor background = waiting || transitioning
? QColor(QStringLiteral("#dbe7f8"))
? QColor(QStringLiteral("#eee8fb"))
: failed ? QColor(QStringLiteral("#fff0f2"))
: QColor(QStringLiteral("#eaf2ff"));
const QColor border = waiting || transitioning
? QColor(QStringLiteral("#9eb9df"))
: failed ? QColor(QStringLiteral("#efb8c0"))
: QColor(QStringLiteral("#bfd3f9"));
: QColor(QStringLiteral("#f4f0ff"));
const QColor border = failed ? QColor(QStringLiteral("#efb8c0"))
: QColor(QStringLiteral("#d4c5f2"));
painter.setBrush(background);
painter.setPen(QPen(border, 1.0));
painter.drawRoundedRect(bounds, 8.0, 8.0);
Expand All @@ -1110,9 +1113,9 @@ void ConversationCard::paintEvent(QPaintEvent *event) {
const qreal center = bounds.left() + position * bounds.width();
const qreal radius = std::max(28.0, bounds.width() * 0.24);
QLinearGradient sweep(center - radius, 0.0, center + radius, 0.0);
sweep.setColorAt(0.0, QColor(47, 111, 235, 0));
sweep.setColorAt(0.5, QColor(117, 160, 239, 105));
sweep.setColorAt(1.0, QColor(47, 111, 235, 0));
sweep.setColorAt(0.0, QColor(105, 65, 198, 0));
sweep.setColorAt(0.5, QColor(155, 128, 214, 105));
sweep.setColorAt(1.0, QColor(105, 65, 198, 0));
QPainterPath clip;
clip.addRoundedRect(bounds, 8.0, 8.0);
painter.save();
Expand All @@ -1121,7 +1124,7 @@ void ConversationCard::paintEvent(QPaintEvent *event) {
painter.restore();

painter.setBrush(Qt::NoBrush);
painter.setPen(QPen(QColor(QStringLiteral("#79a0d7")), 1.5));
painter.setPen(QPen(QColor(QStringLiteral("#b49bdf")), 1.5));
painter.drawRoundedRect(bounds, 8.0, 8.0);
}

Expand Down
7 changes: 5 additions & 2 deletions src/codex/ui/UiStyle.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -204,8 +204,11 @@ QString applicationStyleSheet() {
QFrame[kind="panel"] { background: #ffffff; }
QFrame[kind="raised"] { background: #ffffff; border: 1px solid #d7dee8; border-radius: 10px; }
QFrame[kind="raised"][tone="warning"] { background: #fff6df; border-color: #e5c77d; }
QFrame[messageRole="user"] { background: #eaf2ff; border: 1px solid #bfd3f9; border-radius: 8px; }
QFrame[messageRole="agent"] { background: #ffffff; border: 0; border-radius: 8px; }
QFrame[messageRole="user"] { background: #f4f0ff; border: 1px solid #d4c5f2; border-radius: 8px; }
QFrame[messageRole="user"] QLabel[kind="title"] { color: #53389e; }
QFrame[messageRole="agent"][messagePhase="final"] { background: #eaf2ff; border: 1px solid #bfd3f9; border-radius: 8px; }
QFrame[messageRole="agent"][messagePhase="final"] QLabel[kind="title"] { color: #285fca; }
QFrame[messageRole="agent"][messagePhase="update"] { background: #ffffff; border: 1px solid #d7dee8; border-radius: 8px; }
QFrame[kind="summary"] { background: #f8fafc; border: 1px solid #d7dee8; border-radius: 7px; }
QFrame[kind="standardDivider"] { background: #d7dee8; border: none; }
QFrame[kind="greenBadge"] { background: #e9f7f0; border: 1px solid #a9d8c1; border-radius: 6px; }
Expand Down
5 changes: 5 additions & 0 deletions src/codex/ui/UiStyle.h
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,11 @@ inline constexpr auto redSurface = "#fff0f2";
inline constexpr auto redBorder = "#efb8c0";
inline constexpr auto redText = "#982f3d";
inline constexpr auto purple = "#6941c6";
inline constexpr auto purpleHover = "#5b37ad";
inline constexpr auto purplePressed = "#4b2e90";
inline constexpr auto purpleSurface = "#f4f0ff";
inline constexpr auto purpleBorder = "#d4c5f2";
inline constexpr auto purpleText = "#53389e";

QString applicationStyleSheet();
QString humanizeLabel(QString value);
Expand Down
68 changes: 66 additions & 2 deletions tests/codex/ConversationCardsTest.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
#include "codex/ui/UiStyle.h"

#include <QApplication>
#include <QColor>
#include <QCoreApplication>
#include <QDateTime>
#include <QElapsedTimer>
Expand Down Expand Up @@ -179,6 +180,62 @@ ConversationSnapshot conversation(const std::string &threadId, int count) {
return result;
}

bool testMessageIdentityPalette() {
const QString originalStyleSheet = qApp->styleSheet();
qApp->setStyleSheet(codexui::UiStyle::applicationStyleSheet());
ConversationCard user(VisibleCardData{
AuthoritativeItemKey{"identity-palette", "turn", "user"},
CardKind::UserMessage,
"identity-palette",
"turn",
"user",
UserMessageData{QStringLiteral("Prompt"), {}}});
ConversationCard update(VisibleCardData{
AuthoritativeItemKey{"identity-palette", "turn", "update"},
CardKind::AgentMessage,
"identity-palette",
"turn",
"update",
AgentMessageData{QStringLiteral("Working"), false}});
ConversationCard final(VisibleCardData{
AuthoritativeItemKey{"identity-palette", "turn", "final"},
CardKind::AgentMessage,
"identity-palette",
"turn",
"final",
AgentMessageData{QStringLiteral("Response"), true}});
for (ConversationCard *card : {&user, &update, &final}) {
card->resize(600, card->sizeHint().height());
card->show();
}
spin();

const auto titleColor = [](ConversationCard &card) {
for (QLabel *label : card.findChildren<QLabel *>())
if (label->property("kind").toString() == QStringLiteral("title"))
return label->palette().color(QPalette::WindowText);
return QColor{};
};
const auto surfaceColor = [](ConversationCard &card) {
const QImage rendered = card.grab().toImage();
return rendered.pixelColor(rendered.width() - 10,
rendered.height() - 10);
};
const bool result = expect(
titleColor(user) ==
QColor(QString::fromLatin1(codexui::UiStyle::purpleText)) &&
surfaceColor(user) ==
QColor(QString::fromLatin1(codexui::UiStyle::purpleSurface)) &&
surfaceColor(update) ==
QColor(QString::fromLatin1(codexui::UiStyle::panel)) &&
titleColor(final) ==
QColor(QString::fromLatin1(codexui::UiStyle::blueHover)) &&
surfaceColor(final) == QColor(QStringLiteral("#eaf2ff")),
"You is violet, interim Codex is neutral, and final Codex is blue");
qApp->setStyleSheet(originalStyleSheet);
return result;
}

ConversationCard *card(ConversationView &view, const std::string &key) {
for (QWidget *widget : view.findChildren<QWidget *>()) {
auto *candidate = dynamic_cast<ConversationCard *>(widget);
Expand Down Expand Up @@ -1503,7 +1560,13 @@ bool testPendingPromptAnimation() {
const QImage second = card.grab().toImage();
bool result =
expect(first != second,
"an unacknowledged prompt visibly animates its blue sweep");
"an unacknowledged prompt visibly animates its violet sweep");
result &= expect(
first.pixelColor(10, first.height() - 10).blue() >
first.pixelColor(10, first.height() - 10).green() &&
first.pixelColor(10, first.height() - 10).red() >
first.pixelColor(10, first.height() - 10).green(),
"the temporary You card stays in the violet identity family");

auto &accepted = std::get<LocalPromptData>(pending.payload);
accepted.state = PromptState::Accepted;
Expand Down Expand Up @@ -1742,7 +1805,8 @@ bool testGeneratedImagePresentationAndGenericBound() {
int main(int argc, char **argv) {
QApplication application(argc, argv);
using namespace codexui::codex::middle;
bool result = testStructuralOrderAndIdentity();
bool result = testMessageIdentityPalette();
result &= testStructuralOrderAndIdentity();
result &= testFollowPauseAndStableAnchor();
result &= testPausedExpandedCommandStaysPainted();
result &= testThreadLocalScrollAndComposerExtent();
Expand Down
7 changes: 5 additions & 2 deletions ui-review/UX-DESIGN-DECISIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,10 @@ This document records the implemented CodexUI visual and interaction contract.
white-text contrast.
- Hover, focus, selection, disabled, warning, error, pending, and active states
remain visually distinct.
- User messages are blue-tinted cards. Codex narrative is visually lighter.
Commands, tool activity, files, and collaboration activity use raised cards.
- User messages use the violet identity family. Final Codex narrative uses the
canonical blue identity family; interim Codex updates remain neutral and are
identified by their phase label. Commands, tool activity, files, and
collaboration activity also use neutral raised cards.
- Scrollbars use one compact application style across conversation, nested
output, State, Protocol, and Inspector surfaces.
- The three primary panels use one prominent neutral 24 px header row:
Expand Down Expand Up @@ -45,6 +47,7 @@ are not mapped to the canonical application scale.
| Green | `#18865e` | `#14734f` | `#105f41` | `#e9f7f0` | `#a9d8c1` | `#176b45` |
| Orange | `#a85d0c` | `#8e4d09` | `#743e07` | `#fff6df` | `#e5c77d` | `#8a5208` |
| Red | `#c43d4d` | `#aa3342` | `#8f2b38` | `#fff0f2` | `#efb8c0` | `#982f3d` |
| Violet | `#6941c6` | `#5b37ad` | `#4b2e90` | `#f4f0ff` | `#d4c5f2` | `#53389e` |

Neutral separators and borders use three canonical intensity steps:

Expand Down
5 changes: 3 additions & 2 deletions web/src/app/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -93,14 +93,15 @@ function SafeMarkdown({text}: {text: string}) {
function Card({card, active, collapsed, onToggle}: {card: VisibleCardData; active: boolean; collapsed: boolean; onToggle: () => void}) {
let title = humanize(card.kind);
let body: ReactNode;
let phaseClass = "";
if (card.kind === "userMessage") {
const data = card.payload as UserMessageData; title = "You";
body = <><div className="card-text">{data.text}</div>{data.imagePaths.map(path => <code key={path}>{path}</code>)}</>;
} else if (card.kind === "localPrompt") {
const data = card.payload as LocalPromptData; title = data.state === "failed" ? "Not sent" : "You";
body = <><div className="card-text">{data.prompt}</div>{data.error && <div className="error-text">{data.error}</div>}</>;
} else if (card.kind === "agentMessage") {
const data = card.payload as AgentMessageData; title = data.finalAnswer ? "Codex" : "Agent message";
const data = card.payload as AgentMessageData; title = "Codex"; phaseClass = data.finalAnswer ? "final" : "update";
body = <SafeMarkdown text={data.text} />;
} else if (card.kind === "reasoning") {
const data = card.payload as ReasoningData; title = "Reasoning";
Expand Down Expand Up @@ -128,7 +129,7 @@ function Card({card, active, collapsed, onToggle}: {card: VisibleCardData; activ
}
const foldable = ["agentMessage", "commandExecution", "agentActivity", "reasoning", "fileChanges", "genericActivity"].includes(card.kind)
&& !(card.kind === "reasoning" && !(card.payload as ReasoningData).summary);
return <article className={`conversation-card ${card.kind} ${collapsed ? "collapsed" : ""}`} data-card-key={stableKey(card.key)}>
return <article className={`conversation-card ${card.kind} ${phaseClass} ${collapsed ? "collapsed" : ""}`} data-card-key={stableKey(card.key)}>
<header><span>{title}</span><span className="card-meta"><small>{card.itemId}</small>{foldable && <button onClick={onToggle} aria-label={collapsed ? "Expand card" : "Collapse card"}>{collapsed ? "+" : "−"}</button>}</span></header>{!collapsed && body}
</article>;
}
Expand Down
9 changes: 6 additions & 3 deletions web/src/styles.css

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading