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
71 changes: 64 additions & 7 deletions src/codex/middle/ConversationCards.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -666,13 +666,25 @@ class ConversationCard::Impl final {
refreshFoldPresentation();
}

[[nodiscard]] bool canApply(const VisibleCardData &next) const noexcept {
return current.key == next.key && (current.kind == next.kind ||
(current.kind == CardKind::LocalPrompt &&
next.kind == CardKind::UserMessage));
}

bool apply(const VisibleCardData &next) {
if (current.key != next.key || current.kind != next.kind) {
if (!canApply(next)) {
Q_ASSERT_X(false, "ConversationCard::apply",
"a persistent conversation card cannot change key or kind");
"a persistent conversation card received an incompatible "
"key or kind");
return false;
}
const bool presentationChanged = !presentationEquals(current, next);
const bool becomingAuthoritative = current.kind == CardKind::LocalPrompt &&
next.kind == CardKind::UserMessage;
const bool presentationChanged =
becomingAuthoritative || !presentationEquals(current, next);
if (becomingAuthoritative)
promoteToAuthoritativeUserMessage();
current = next;
if (!presentationChanged)
return false;
Expand All @@ -683,6 +695,23 @@ class ConversationCard::Impl final {
return true;
}

void promoteToAuthoritativeUserMessage() {
if (animationTimer)
animationTimer->stop();
owner->setObjectName(QStringLiteral("conversationCard"));
owner->setProperty("conversationCardKind",
static_cast<int>(CardKind::UserMessage));
owner->setProperty("messageRole", "user");
owner->setStyleSheet(QString{});
for (QLabel *label : {title, body, metadata})
if (label)
label->setStyleSheet(QString{});
if (metadata) {
metadata->clear();
metadata->hide();
}
}

void setCollapsed(bool next) {
if (collapsed == next)
return;
Expand Down Expand Up @@ -720,6 +749,22 @@ class ConversationCard::Impl final {
break;
case CardKind::AgentMessage:
owner->setProperty("messageRole", "agent");
title->setText(QStringLiteral("Codex"));
title->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Preferred);
phaseSeparator = makeLabel(QStringLiteral("•"), "messagePhase", header);
phaseSeparator->setObjectName(
QStringLiteral("agentMessagePhaseSeparator"));
phaseSeparator->setWordWrap(false);
phaseSeparator->setSizePolicy(QSizePolicy::Fixed,
QSizePolicy::Preferred);
phase = makeLabel({}, "messagePhase", header);
phase->setObjectName(QStringLiteral("agentMessagePhase"));
phase->setWordWrap(false);
phase->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Preferred);
headerLayout->setStretch(0, 0);
headerLayout->insertWidget(1, phaseSeparator, 0, Qt::AlignVCenter);
headerLayout->insertWidget(2, phase, 0, Qt::AlignVCenter);
headerLayout->insertStretch(3, 1);
body = makeMarkdownLabel({}, content);
contentLayout->addWidget(body);
break;
Expand Down Expand Up @@ -779,8 +824,9 @@ class ConversationCard::Impl final {
break;
case CardKind::LocalPrompt:
owner->setObjectName(QStringLiteral("pendingPromptCard"));
owner->setStyleSheet(QStringLiteral(
"QFrame#pendingPromptCard{background:transparent;border:0;}"));
owner->setStyleSheet(
QStringLiteral("QFrame#pendingPromptCard{background:transparent;"
"border:1px solid transparent;border-radius:8px;}"));
title->setText(QStringLiteral("You"));
body = makeMarkdownLabel({}, content);
metadata = makeLabel({}, "meta", content);
Expand Down Expand Up @@ -831,8 +877,13 @@ class ConversationCard::Impl final {
}
case CardKind::AgentMessage: {
const auto &message = std::get<AgentMessageData>(data.payload);
title->setText(message.finalAnswer ? QStringLiteral("Codex")
: QStringLiteral("Codex activity"));
phase->setText(message.finalAnswer ? QStringLiteral("final answer")
: QStringLiteral("update"));
const QString phaseStatus = message.finalAnswer
? QStringLiteral("completed")
: QStringLiteral("inProgress");
setStatusTone(phaseSeparator, phaseStatus);
setStatusTone(phase, phaseStatus);
layout->setContentsMargins(12, message.finalAnswer ? 10 : 8, 12,
message.finalAnswer ? 10 : 8);
setVisibleMarkdown(body, message.text);
Expand Down Expand Up @@ -973,6 +1024,8 @@ class ConversationCard::Impl final {
QWidget *header = nullptr;
QHBoxLayout *headerLayout = nullptr;
QLabel *title = nullptr;
QLabel *phaseSeparator = nullptr;
QLabel *phase = nullptr;
CardDisclosureButton *disclosure = nullptr;
QWidget *content = nullptr;
QVBoxLayout *contentLayout = nullptr;
Expand Down Expand Up @@ -1022,6 +1075,10 @@ bool ConversationCard::apply(const VisibleCardData &data) {
return impl_->apply(data);
}

bool ConversationCard::canApply(const VisibleCardData &data) const noexcept {
return impl_->canApply(data);
}

void ConversationCard::paintEvent(QPaintEvent *event) {
QFrame::paintEvent(event);
if (impl_->current.kind != CardKind::LocalPrompt)
Expand Down
8 changes: 5 additions & 3 deletions src/codex/middle/ConversationCards.h
Original file line number Diff line number Diff line change
Expand Up @@ -87,9 +87,11 @@ class ConversationCard : public QFrame {
void
restoreCommandOutputScrollState(const CommandOutputView::ScrollState &state);

// A key and kind identify the persistent widget. apply() updates all card
// kinds in place and returns false when neither content nor presentation
// changed. Passing a different key or kind is a programming error.
[[nodiscard]] bool canApply(const VisibleCardData &data) const noexcept;

// A key identifies the persistent widget. apply() updates matching card
// kinds in place and also performs the one supported semantic transition
// from an admitted local prompt to its authoritative user message.
bool apply(const VisibleCardData &data);

signals:
Expand Down
3 changes: 2 additions & 1 deletion src/codex/middle/ConversationProjection.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -321,7 +321,8 @@ ConversationSnapshot ConversationProjection::project(
if (binding != bindings.end() &&
binding->second->localCardVisible(nowMilliseconds))
continue;
CardKey visualKey = item.key;
CardKey visualKey =
item.localPromptKey ? CardKey{*item.localPromptKey} : CardKey{item.key};
if (binding != bindings.end())
visualKey = LocalPromptKey{binding->second->id};
const std::size_t position =
Expand Down
8 changes: 5 additions & 3 deletions src/codex/middle/ConversationView.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -330,7 +330,7 @@ bool ConversationView::reconcile(const ConversationSnapshot &snapshot) {
if (desired != desiredCards.end() && card != cards_.end() &&
desired->second.section == section &&
desired->second.position == static_cast<int>(index) &&
card->second->cardKind() == desired->second.data->kind)
card->second->canApply(*desired->second.data))
continue;
delete section->cards->takeAt(static_cast<int>(index));
if (desired != desiredCards.end())
Expand All @@ -342,7 +342,7 @@ bool ConversationView::reconcile(const ConversationSnapshot &snapshot) {
for (auto iterator = cards_.begin(); iterator != cards_.end();) {
const auto desired = desiredCards.find(iterator->first);
if (desired != desiredCards.end() &&
iterator->second->cardKind() == desired->second.data->kind) {
iterator->second->canApply(*desired->second.data)) {
++iterator;
continue;
}
Expand Down Expand Up @@ -393,8 +393,10 @@ bool ConversationView::reconcile(const ConversationSnapshot &snapshot) {
desired.insert = true;
visualChange = true;
}
if (desired.insert)
if (desired.insert) {
section->cards->insertWidget(cardIndex, card);
card->show();
}
++cardIndex;
}
section->cardKeys = std::move(desiredSection.cardKeys);
Expand Down
16 changes: 10 additions & 6 deletions src/codex/middle/InspectorPane.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -199,12 +199,16 @@ QFrame *InspectorPane::agentFrame(const AgentSnapshot &agent) {
auto *metadataRow = new QHBoxLayout;
metadataRow->setContentsMargins(0, 0, 0, 0);
metadataRow->setSpacing(6);
metadataRow->addWidget(statusLabel(agent.status));
if (!metadata.isEmpty())
metadataRow->addWidget(
makeLabel(QStringLiteral("| ") +
metadata.join(QStringLiteral(" | ")),
"meta"));
auto *status = statusLabel(agent.status);
status->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Preferred);
metadataRow->addWidget(status);
if (!metadata.isEmpty()) {
auto *details = makeLabel(
QStringLiteral("| ") + metadata.join(QStringLiteral(" | ")),
"meta");
details->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Preferred);
metadataRow->addWidget(details);
}
metadataRow->addStretch();
layout->addLayout(metadataRow);
if (!agent.prompt.empty())
Expand Down
46 changes: 45 additions & 1 deletion src/codex/middle/PromptCoordinator.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -239,9 +239,24 @@ bool PromptCoordinator::reassignThread(const std::string &fromThreadId,
const std::string &toThreadId) {
if (fromThreadId == toThreadId)
return true;
const auto reassignAliases = [this, &fromThreadId, &toThreadId] {
auto aliases = visualAliasesByThread.find(fromThreadId);
if (aliases == visualAliasesByThread.end())
return;
auto moved = std::move(aliases->second);
visualAliasesByThread.erase(aliases);
auto &target = visualAliasesByThread[toThreadId];
for (const auto &[key, localKey] : moved) {
AuthoritativeItemKey reassigned = key;
reassigned.threadId = toThreadId;
target.insert_or_assign(std::move(reassigned), localKey);
}
};
auto source = byThread.find(fromThreadId);
if (source == byThread.end())
if (source == byThread.end()) {
reassignAliases();
return true;
}
auto destination = byThread.find(toThreadId);
const bool sourceInFlight =
std::any_of(source->second.begin(), source->second.end(),
Expand Down Expand Up @@ -270,6 +285,7 @@ bool PromptCoordinator::reassignThread(const std::string &fromThreadId,
target.insert(target.end(), std::make_move_iterator(moved.begin()),
std::make_move_iterator(moved.end()));
std::ranges::sort(target, {}, &PromptSubmission::admissionOrdinal);
reassignAliases();
return true;
}

Expand All @@ -284,10 +300,15 @@ void PromptCoordinator::reconcile(const std::string &threadId,
void PromptCoordinator::reconcile(const std::string &threadId,
AuthoritativeItemIndex &authoritativeItems,
qint64 nowMilliseconds) {
applyVisualAliases(threadId, authoritativeItems);
auto found = byThread.find(threadId);
if (found == byThread.end())
return;
std::vector<bool> claimed(authoritativeItems.ordered.size());
for (std::size_t index = 0; index < authoritativeItems.ordered.size();
++index)
if (authoritativeItems.ordered[index].localPromptKey)
claimed[index] = true;
for (const PromptSubmission &submission : found->second)
if (submission.materializedItem) {
const auto position =
Expand Down Expand Up @@ -354,11 +375,20 @@ void PromptCoordinator::reconcile(const std::string &threadId,
claimed[index] = true;
authoritativeItems.userMessagesByText.erase(candidate);
}
for (const PromptSubmission &submission : found->second) {
if (submission.state != PromptState::Accepted ||
!submission.materializedItem ||
submission.acceptedTransitionActive(nowMilliseconds))
continue;
visualAliasesByThread[threadId].insert_or_assign(
*submission.materializedItem, LocalPromptKey{submission.id});
}
std::erase_if(found->second, [nowMilliseconds](const auto &submission) {
return submission.state == PromptState::Accepted &&
submission.materializedItem &&
!submission.acceptedTransitionActive(nowMilliseconds);
});
applyVisualAliases(threadId, authoritativeItems);
}

std::span<const PromptSubmission>
Expand Down Expand Up @@ -402,6 +432,20 @@ std::vector<std::string> PromptCoordinator::queuedThreadIds() const {

void PromptCoordinator::clearThread(const std::string &threadId) {
byThread.erase(threadId);
visualAliasesByThread.erase(threadId);
}

void PromptCoordinator::applyVisualAliases(
const std::string &threadId,
AuthoritativeItemIndex &authoritativeItems) const {
const auto aliases = visualAliasesByThread.find(threadId);
if (aliases == visualAliasesByThread.end())
return;
for (const auto &[key, localKey] : aliases->second) {
const auto position = authoritativeItems.position(key);
if (position)
authoritativeItems.ordered[*position].localPromptKey = localKey;
}
}

PromptSubmission *PromptCoordinator::find(const std::string &threadId,
Expand Down
8 changes: 7 additions & 1 deletion src/codex/middle/PromptCoordinator.h
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ struct PromptDispatch {
struct AuthoritativeItem {
AuthoritativeItemKey key;
const ItemPresentation *presentation = nullptr;
std::optional<LocalPromptKey> localPromptKey;
};

struct AuthoritativeItemIndex {
Expand Down Expand Up @@ -114,7 +115,8 @@ class PromptCoordinator final {
// Correlates prompts with authoritative userMessage items. Exact client ids
// may bind before acknowledgement so the awaiting card is never duplicated;
// the content fallback is used only after the real operation callback. Fully
// resolved submissions are removed after their accepted transition.
// resolved submissions are removed after their accepted transition while a
// compact visual-key alias retains the admitted card identity.
void reconcile(const std::string &threadId,
const ThreadPresentation &authoritativeThread,
qint64 nowMilliseconds);
Expand All @@ -135,8 +137,12 @@ class PromptCoordinator final {
private:
[[nodiscard]] PromptSubmission *find(const std::string &threadId,
std::uint64_t submissionId) noexcept;
void applyVisualAliases(const std::string &threadId,
AuthoritativeItemIndex &authoritativeItems) const;

std::map<std::string, std::vector<PromptSubmission>> byThread;
std::map<std::string, std::map<AuthoritativeItemKey, LocalPromptKey>>
visualAliasesByThread;
std::uint64_t nextSubmissionId = 1;
std::uint64_t nextAdmissionOrdinal = 1;
};
Expand Down
1 change: 1 addition & 0 deletions src/codex/ui/UiStyle.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ QString applicationStyleSheet() {
QLabel[kind="applicationTitle"] { font-weight: 700; }
QLabel[kind="brand"] { font-size: %3pt; font-weight: 600; }
QLabel[kind="title"] { font-size: %2pt; font-weight: 600; }
QLabel[kind="messagePhase"] { font-size: %2pt; font-weight: 400; }
QLabel[kind="body"] { font-size: %2pt; }
QLabel[kind="meta"] { color: #667085; font-size: %1pt; }
QLabel[kind="small"] { color: #667085; font-size: %1pt; }
Expand Down
Loading