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/codex-architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -521,8 +521,11 @@ stable visual card key retains its user-selected collapsed state in the
new activity cards default collapsed. The card owns one header and one content
container, so streamed payload updates remain live while folded without
changing visible height. `ConversationView` owns the fold geometry transaction,
including title anchoring and lower-limit compensation, alongside its existing
single-owner scrolling calculations.
including title anchoring within the natural scroll range, alongside its
existing single-owner scrolling calculations. Expansion scrolls only as needed
to reveal the complete card when it fits in the unobscured viewport above any
grown composer overlay. At the lower limit, normal range clamping may move the
selected title rather than creating artificial blank space.

### 7.4 Changes and Diff Presentation

Expand Down
13 changes: 8 additions & 5 deletions docs/ui-behavior.md
Original file line number Diff line number Diff line change
Expand Up @@ -139,11 +139,14 @@ activity cards initially render collapsed. A user-selected state survives
streaming updates, authoritative prompt replacement, and thread switching for
the lifetime of the CodexUI process.

Folding is an explicit geometry transaction. The selected title row keeps its
exact viewport position: collapsing shifts only following cards upward, while
expanding grows only downward. The gesture pauses follow-latest. At the lower
scroll limit, bounded bottom compensation prevents scrollbar clamping from
moving the selected title; later expansion consumes that compensation.
Folding is an explicit geometry transaction. Collapsing keeps the selected
title row fixed while the natural scroll range permits and shifts following
cards upward. Expanding grows downward when the complete card remains visible;
otherwise the viewport scrolls only enough to reveal it, so the title may move
upward. The visible boundary excludes any extra composer height currently
overlaying the conversation. The gesture pauses follow-latest. At the lower
scroll limit, the viewport accepts the natural clamp instead of retaining
artificial blank space.

Reasoning items remain visible as stable progress cards even when the app-server
provides no public summary; later content updates the same card in place.
Expand Down
34 changes: 16 additions & 18 deletions src/codex/PresentationModel.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
#include "codex/PresentationStatus.h"

#include <algorithm>
#include <unordered_set>

namespace codexui::codex {
namespace {
Expand Down Expand Up @@ -576,15 +577,6 @@ std::size_t PresentationModel::pendingRequestCount() const noexcept {
return pendingRequests.size();
}

std::size_t PresentationModel::pendingRequestCount(
const std::string &threadId) const noexcept {
return static_cast<std::size_t>(
std::count_if(pendingRequests.begin(), pendingRequests.end(),
[&threadId](const auto &entry) {
return entry.second.threadId == threadId;
}));
}

const ConnectionPresentation &PresentationModel::connection() const noexcept {
return connectionState;
}
Expand Down Expand Up @@ -612,24 +604,29 @@ void PresentationModel::mergeThreadList(const nlohmann::json &listedThreads) {
if (!listedThreads.is_array())
return;

std::vector<std::string> listedIds;
std::unordered_set<std::string> listedIds;
listedIds.reserve(listedThreads.size());
std::vector<std::string> nextOrder;
nextOrder.reserve(listedThreads.size() + orderedThreads.size());
for (const auto &raw : listedThreads) {
const std::string id = stringValue(raw, "id");
if (id.empty())
continue;
upsertThread(raw, false);
listedIds.push_back(id);
upsertThread(raw, false, false);
if (listedIds.insert(id).second)
nextOrder.push_back(id);
}

for (const std::string &id : listedIds)
std::erase(orderedThreads, id);
orderedThreads.insert(orderedThreads.begin(), listedIds.begin(),
listedIds.end());
for (const std::string &id : orderedThreads) {
if (!listedIds.contains(id))
nextOrder.push_back(id);
}
orderedThreads = std::move(nextOrder);
}

ThreadPresentation &PresentationModel::upsertThread(const nlohmann::json &raw,
bool replaceTurns) {
bool replaceTurns,
bool prependNewThread) {
const std::string id = stringValue(raw, "id");
if (id.empty()) {
static ThreadPresentation ignored;
Expand All @@ -639,7 +636,8 @@ ThreadPresentation &PresentationModel::upsertThread(const nlohmann::json &raw,
ThreadPresentation &result = iterator->second;
if (inserted) {
result.id = id;
orderedThreads.insert(orderedThreads.begin(), id);
if (prependNewThread)
orderedThreads.insert(orderedThreads.begin(), id);
}
const std::string previousThreadStatus = result.status;
std::unordered_map<std::string, std::string> terminalTurnStatuses;
Expand Down
5 changes: 2 additions & 3 deletions src/codex/PresentationModel.h
Original file line number Diff line number Diff line change
Expand Up @@ -101,8 +101,6 @@ class PresentationModel final {
[[nodiscard]] std::optional<std::string>
activeTurnId(const std::string &threadId) const;
[[nodiscard]] std::size_t pendingRequestCount() const noexcept;
[[nodiscard]] std::size_t
pendingRequestCount(const std::string &threadId) const noexcept;
[[nodiscard]] const ConnectionPresentation &connection() const noexcept;
[[nodiscard]] const nlohmann::json &modelCatalog() const noexcept;
[[nodiscard]] const std::unordered_map<std::string, nlohmann::json> &
Expand All @@ -117,7 +115,8 @@ class PresentationModel final {
void applyValidatedEvent(const nlohmann::json &event);
void mergeThreadList(const nlohmann::json &listedThreads);
ThreadPresentation &upsertThread(const nlohmann::json &raw,
bool replaceTurns);
bool replaceTurns,
bool prependNewThread = true);
TurnPresentation &upsertTurn(ThreadPresentation &thread,
const nlohmann::json &raw, bool replaceItems);
ItemPresentation &upsertItem(ThreadPresentation &thread,
Expand Down
15 changes: 9 additions & 6 deletions src/codex/ShellWidget.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -700,6 +700,13 @@ void ShellWidget::Impl::refreshStatus() {
}
}
const bool active = model.activeTurnId(selectedThreadId).has_value();
const std::size_t selectedPending = static_cast<std::size_t>(std::count_if(
model.pendingRequestPresentations().begin(),
model.pendingRequestPresentations().end(),
[this](const auto &entry) {
return entry.second.threadId == selectedThreadId;
}));
const std::size_t totalPending = model.pendingRequestCount();
const std::string serialized = nlohmann::json{
{"connected", connection.connected},
{"retrying", connection.retrying},
Expand All @@ -714,9 +721,8 @@ void ShellWidget::Impl::refreshStatus() {
{"agentCount", thread ? thread->agents.size() : 0U},
{"runningAgents", runningAgents},
{"active", active},
{"selectedPending", model.pendingRequestCount(selectedThreadId)},
{"totalPending",
model.pendingRequestCount()}}.dump();
{"selectedPending", selectedPending},
{"totalPending", totalPending}}.dump();
const QByteArray next(serialized.data(),
static_cast<qsizetype>(serialized.size()));
if (next == statusSnapshot)
Expand Down Expand Up @@ -764,9 +770,6 @@ void ShellWidget::Impl::refreshStatus() {
: QStringLiteral("Claim control"));
controllerButton->setEnabled(connection.connected);

const std::size_t selectedPending =
model.pendingRequestCount(selectedThreadId);
const std::size_t totalPending = model.pendingRequestCount();
requestButton->setText(QStringLiteral("Requests (%1)")
.arg(static_cast<qulonglong>(totalPending)));
requestButton->setVisible(totalPending != 0);
Expand Down
Loading