Skip to content
391 changes: 328 additions & 63 deletions src/codex/PresentationModel.cpp

Large diffs are not rendered by default.

32 changes: 30 additions & 2 deletions src/codex/PresentationModel.h
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,13 @@ struct AgentPresentation {
nlohmann::json raw = nlohmann::json::object();
};

struct ChildThreadOwnership {
std::string parentThreadId;
std::string agentId;

bool operator==(const ChildThreadOwnership &) const = default;
};

struct ThreadPresentation {
std::string id;
std::string title;
Expand All @@ -57,8 +64,8 @@ struct ThreadPresentation {
std::uint64_t settingsRevision = 0;
std::vector<std::string> agentOrder;
std::unordered_map<std::string, AgentPresentation> agents;
std::vector<std::string> childThreadOrder;
bool archived = false;
bool agentThread = false;
};

struct PendingRequestPresentation {
Expand Down Expand Up @@ -98,6 +105,8 @@ class PresentationModel final {
[[nodiscard]] const std::vector<std::string> &threadOrder() const noexcept;
[[nodiscard]] const ThreadPresentation *
thread(const std::string &threadId) const noexcept;
[[nodiscard]] const ChildThreadOwnership *
childOwnership(const std::string &childThreadId) const noexcept;
[[nodiscard]] std::optional<std::string>
activeTurnId(const std::string &threadId) const;
[[nodiscard]] std::size_t pendingRequestCount() const noexcept;
Expand Down Expand Up @@ -125,7 +134,25 @@ class PresentationModel final {
void upsertAgentActivity(ThreadPresentation &owner,
const nlohmann::json &scope,
const nlohmann::json &activity, bool live = true);
void correlateAgentThread(const std::string &childThreadId);
void assignChildOwnership(ThreadPresentation &parent,
AgentPresentation &agent,
const std::string &childThreadId, bool live);
void releaseChildOwnership(const std::string &childThreadId,
bool promoteToRoot);
void synchronizeOwningAgent(const std::string &childThreadId,
bool clearMissingResult = false);
AgentPresentation *owningAgent(const std::string &childThreadId);
ItemPresentation *agentSourceItem(ThreadPresentation &parent,
const AgentPresentation &agent);
void setAgentStatus(ThreadPresentation &parent, AgentPresentation &agent,
const std::string &status);
void setAgentResult(ThreadPresentation &parent, AgentPresentation &agent,
const std::string &resultText);
void clearAgentResult(ThreadPresentation &parent, AgentPresentation &agent);
void updateOwningAgentStatus(const std::string &childThreadId,
const std::string &status);
void updateOwningAgentResult(const std::string &childThreadId,
const std::string &resultText);
void removeThread(const std::string &threadId);
void clearProviderState();
void retainDomainEvent(const std::string &type, const nlohmann::json &data,
Expand All @@ -137,6 +164,7 @@ class PresentationModel final {

std::vector<std::string> orderedThreads;
std::unordered_map<std::string, ThreadPresentation> threads;
std::unordered_map<std::string, ChildThreadOwnership> childOwnerships;
std::unordered_map<std::string, PendingRequestPresentation> pendingRequests;
ConnectionPresentation connectionState;
nlohmann::json models = nlohmann::json::array();
Expand Down
56 changes: 40 additions & 16 deletions src/codex/ShellWidget.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -208,14 +208,16 @@ struct ShellWidget::Impl final {
void renderConversation();
void refreshSettings();
void refreshStatus();
void hydrateHistoricalAgents();
void hydrateHistoricalChildren(const std::string &parentThreadId,
bool retryFailed = false);
void showNotice(QString message, bool error = true);
void resetRuntimeForConnection();

void selectThread(std::string threadId);
void beginNewThread();
void readThread(const std::string &threadId, bool forced = false);
void ensureThreadHydrated(const std::string &threadId);
void hydrateThreadForSelection(const std::string &threadId);
void ensureThreadSettingsHydrated(const std::string &threadId);
void resumeThreadForSettings(const std::string &threadId);
void renameThread(const std::string &threadId);
Expand Down Expand Up @@ -576,7 +578,11 @@ void ShellWidget::Impl::handleEvent(const nlohmann::json &event) {
ensureThreadSettingsHydrated(selectedThreadId);
}

hydrateHistoricalAgents();
if (!staleReadResult && kind == "result" && action == "thread.read" &&
event.value("ok", false))
hydrateHistoricalChildren(eventThreadId);
else if (kind == "event" && type == "agents.activity.upsert")
hydrateHistoricalChildren(eventThreadId);
scheduleRender();
}

Expand Down Expand Up @@ -820,28 +826,35 @@ void ShellWidget::Impl::refreshStatus() {
middleRegion->composer().setSettingsEnabled(canSubmit && !snapshot.active);
}

void ShellWidget::Impl::hydrateHistoricalAgents() {
const ThreadPresentation *thread = model.thread(selectedThreadId);
void ShellWidget::Impl::hydrateHistoricalChildren(
const std::string &parentThreadId, bool retryFailed) {
const ThreadPresentation *thread = model.thread(parentThreadId);
if (!thread)
return;
for (const std::string &id : thread->agentOrder) {
const auto agent = thread->agents.find(id);
if (agent == thread->agents.end() || agent->second.childThreadId.empty() ||
agent->second.status != "started")
for (const std::string &childThreadId : thread->childThreadOrder) {
const ChildThreadOwnership *ownership =
model.childOwnership(childThreadId);
if (!ownership || ownership->parentThreadId != parentThreadId)
continue;
const auto agent = thread->agents.find(ownership->agentId);
if (agent == thread->agents.end() ||
!isActiveStatus(agent->second.status))
continue;
// Historical child hydration shares the same monotonic read boundary as
// user-selected threads, so a pre-reconnect result cannot replace newer
// child/agent presentation state.
readThread(agent->second.childThreadId);
const auto runtime = runtimeByThread.find(childThreadId);
const bool failed = runtime != runtimeByThread.end() &&
runtime->second.hydration == Hydration::Failed;
// Background activity never retries a failed read. Explicit navigation
// supplies a new bounded retry boundary without creating a retry loop.
if (!failed || retryFailed)
readThread(childThreadId, failed);
}
}

void ShellWidget::Impl::selectThread(std::string threadId) {
if (threadId.empty())
return;
if (threadId == selectedThreadId) {
ensureThreadHydrated(threadId);
ensureThreadSettingsHydrated(threadId);
hydrateThreadForSelection(threadId);
return;
}
selectedThreadId = std::move(threadId);
Expand All @@ -850,8 +863,7 @@ void ShellWidget::Impl::selectThread(std::string threadId) {
newThreadName.clear();
newThreadWorkspace.clear();
historyWindows.try_emplace(selectedThreadId);
ensureThreadHydrated(selectedThreadId);
ensureThreadSettingsHydrated(selectedThreadId);
hydrateThreadForSelection(selectedThreadId);
render();
}

Expand Down Expand Up @@ -1005,6 +1017,18 @@ void ShellWidget::Impl::ensureThreadHydrated(const std::string &threadId) {
readThread(threadId);
}

void ShellWidget::Impl::hydrateThreadForSelection(
const std::string &threadId) {
const auto runtime = runtimeByThread.find(threadId);
if (runtime != runtimeByThread.end() &&
runtime->second.hydration == Hydration::Failed)
readThread(threadId, true);
else
ensureThreadHydrated(threadId);
ensureThreadSettingsHydrated(threadId);
hydrateHistoricalChildren(threadId, true);
}

void ShellWidget::Impl::renameThread(const std::string &threadId) {
const ThreadPresentation *thread = model.thread(threadId);
if (!thread)
Expand Down
63 changes: 40 additions & 23 deletions src/codex/middle/InspectorPane.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -184,33 +184,48 @@ QFrame *InspectorPane::agentFrame(const AgentSnapshot &agent) {
auto *layout = new QVBoxLayout(frame);
layout->setContentsMargins(12, 10, 12, 10);
layout->setSpacing(6);
const QString title =
!agent.childThreadId.empty()
? QStringLiteral("Subagent")
: agent.tool.empty() ? QStringLiteral("Agent activity")
: QStringLiteral("Agent %1").arg(text(agent.tool));
layout->addWidget(makeLabel(title, "title"));
QStringList metadata;
for (const std::string *value :
{&agent.agentPath, &agent.tool, &agent.model, &agent.reasoningEffort}) {
if (!value->empty())
metadata << text(*value);
const QString agentPath = text(agent.agentPath);
const QStringList pathParts = agentPath.split('/', Qt::SkipEmptyParts);
QString agentName;
if (!pathParts.isEmpty())
agentName = pathParts.back();
else if (!agent.tool.empty())
agentName = text(agent.tool);
auto *heading = new QHBoxLayout;
heading->setContentsMargins(0, 0, 0, 0);
heading->setSpacing(6);
auto *titleLabel = makeLabel(QStringLiteral("Agent"), "title");
titleLabel->setObjectName(QStringLiteral("agentTitle"));
titleLabel->setWordWrap(false);
titleLabel->setContentsMargins(0, 0, 0, 1);
titleLabel->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Preferred);
heading->addWidget(titleLabel, 0, Qt::AlignBaseline);
if (!agentName.isEmpty()) {
auto *nameLabel = makeLabel(agentName, "code");
nameLabel->setObjectName(QStringLiteral("agentName"));
nameLabel->setWordWrap(false);
// The fixed-width font's descent sits one pixel below the proportional
// labels. Preserve their visual baseline without changing its font.
nameLabel->setContentsMargins(0, 0, 0, 1);
nameLabel->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Preferred);
if (!agentPath.isEmpty())
nameLabel->setToolTip(agentPath);
heading->addWidget(nameLabel, 0, Qt::AlignBottom);
}
auto *metadataRow = new QHBoxLayout;
metadataRow->setContentsMargins(0, 0, 0, 0);
metadataRow->setSpacing(6);
heading->addStretch();
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);
heading->addWidget(status, 0, Qt::AlignBaseline);
layout->addLayout(heading);
QStringList metadata;
if (!agent.tool.empty() && !agentPath.isEmpty())
metadata << text(agent.tool);
for (const std::string *value : {&agent.model, &agent.reasoningEffort}) {
if (!value->empty())
metadata << text(*value);
}
metadataRow->addStretch();
layout->addLayout(metadataRow);
if (!metadata.isEmpty())
layout->addWidget(makeLabel(metadata.join(QStringLiteral(" · ")), "meta"));
if (!agent.prompt.empty())
layout->addWidget(makeLabel(text(agent.prompt)));
if (!agent.resultText.empty()) {
Expand Down Expand Up @@ -287,6 +302,8 @@ InspectorPane::InspectorPane(QWidget *parent) : QFrame(parent) {
scroll->setFrameShape(QFrame::NoFrame);
scroll->setWidgetResizable(true);
scroll->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
scroll->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);
scroll->verticalScrollBar()->setProperty("kind", "infoViewer");
scroll->setWidget(content);
return scroll;
};
Expand Down
Loading