From 1a5434bf1e867d78306403a12ca5d3e42e642048 Mon Sep 17 00:00:00 2001 From: Volker Christian Date: Fri, 28 Aug 2026 00:49:13 +0200 Subject: [PATCH 1/2] Reconcile filesystem watches with sets --- src/codex/DiffViewer.cpp | 20 +++++++++++++------ tests/codex/GitChangesLiveTest.cpp | 31 ++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 6 deletions(-) diff --git a/src/codex/DiffViewer.cpp b/src/codex/DiffViewer.cpp index 40339ca..4cba283 100644 --- a/src/codex/DiffViewer.cpp +++ b/src/codex/DiffViewer.cpp @@ -22,6 +22,7 @@ #include #include #include +#include #include #include #include @@ -855,28 +856,35 @@ void DiffViewer::applySnapshot(const GitDiffSnapshot &value) { void DiffViewer::updateFileWatches() { QStringList desired; + QSet desiredSet; + const auto retain = [&desired, &desiredSet](const QString &path) { + if (!path.isEmpty() && !desiredSet.contains(path)) { + desired.push_back(path); + desiredSet.insert(path); + } + }; if (snapshot) { for (const GitDiffFile &file : snapshot->files) { const QFileInfo info(file.absolutePath); if (info.exists()) - desired.push_back(info.absoluteFilePath()); + retain(info.absoluteFilePath()); const QString parent = info.absolutePath(); - if (!parent.isEmpty() && QFileInfo(parent).isDir()) - desired.push_back(parent); + if (QFileInfo(parent).isDir()) + retain(parent); } } - desired.removeDuplicates(); const QStringList existing = fileWatcher->files() + fileWatcher->directories(); + const QSet existingSet(existing.begin(), existing.end()); QStringList removed; for (const QString &path : existing) { - if (!desired.contains(path)) + if (!desiredSet.contains(path)) removed.push_back(path); } if (!removed.isEmpty()) fileWatcher->removePaths(removed); QStringList added; for (const QString &path : desired) { - if (!existing.contains(path)) + if (!existingSet.contains(path)) added.push_back(path); } if (!added.isEmpty()) diff --git a/tests/codex/GitChangesLiveTest.cpp b/tests/codex/GitChangesLiveTest.cpp index f2c0549..73d6dce 100644 --- a/tests/codex/GitChangesLiveTest.cpp +++ b/tests/codex/GitChangesLiveTest.cpp @@ -208,6 +208,37 @@ bool testLiveWorkingTreeChanges() { }, 3500), "discovers a modified tracked file"); + const QString renamed = + directory.filePath(QStringLiteral("renamed-tracked.txt")); + result &= expect(writeFile(tracked, QByteArray("original\n")) && + QFile::rename(tracked, renamed) && + waitFor( + [&] { + return hasFile(viewer.currentSnapshot(), + QStringLiteral( + "renamed-tracked.txt"), + QStringLiteral("Renamed")); + }, + 1500), + "moves watches with a renamed tracked file"); + result &= expect(QFile::rename(renamed, tracked) && + waitFor( + [&] { + return viewer.currentSnapshot().files.empty(); + }, + 1500), + "restores watches when a renamed file moves back"); + result &= expect(writeFile(tracked, QByteArray("modified\n")), + "modifies the restored tracked file"); + viewer.refreshRepository(); + result &= expect(waitFor( + [&] { + return hasFile(viewer.currentSnapshot(), + QStringLiteral("tracked.txt"), + QStringLiteral("Modified")); + }, + 1500), + "rediscovers a modified file after a rename cycle"); result &= expect(writeFile(tracked, QByteArray("original\n")) && waitFor( [&] { From 94cd1a66d3ba308f3803c76238db09a20ad8141b Mon Sep 17 00:00:00 2001 From: Volker Christian Date: Fri, 28 Aug 2026 00:53:35 +0200 Subject: [PATCH 2/2] Humanize protocol labels at render boundaries --- src/codex/TurnSettingsWidget.cpp | 31 ++++++++--------------- src/codex/middle/ConversationCards.cpp | 22 ++++++++--------- src/codex/middle/InspectorPane.cpp | 4 +-- src/codex/ui/UiStyle.cpp | 34 ++++++++++++++++++++++++++ src/codex/ui/UiStyle.h | 1 + tests/codex/ApplicationLayoutTest.cpp | 21 ++++++++++++---- tests/codex/ConversationCardsTest.cpp | 24 +++++++++++++----- 7 files changed, 92 insertions(+), 45 deletions(-) diff --git a/src/codex/TurnSettingsWidget.cpp b/src/codex/TurnSettingsWidget.cpp index f03e7bc..dfb34b1 100644 --- a/src/codex/TurnSettingsWidget.cpp +++ b/src/codex/TurnSettingsWidget.cpp @@ -82,14 +82,6 @@ std::string stringValue(const nlohmann::json &object, const char *key) { : std::string{}; } -QString friendly(QString value) { - value.replace(QLatin1Char('-'), QLatin1Char(' ')); - value.replace(QLatin1Char('_'), QLatin1Char(' ')); - if (!value.isEmpty()) - value[0] = value[0].toUpper(); - return value; -} - void addChoice(QComboBox *combo, const QString &label, const QString &value) { if (combo->findData(value) < 0) combo->addItem(label, value); @@ -99,7 +91,8 @@ void selectValue(QComboBox *combo, const QString &value, const QString &fallback = {}) { int index = combo->findData(value); if (index < 0) { - combo->addItem(fallback.isEmpty() ? friendly(value) : fallback, value); + combo->addItem( + fallback.isEmpty() ? UiStyle::humanizeLabel(value) : fallback, value); index = combo->count() - 1; } combo->setCurrentIndex(index); @@ -122,7 +115,7 @@ QWidget *labelled(const QString &caption, QWidget *control, layout->setContentsMargins(0, 0, 0, 0); layout->setSpacing(SettingLabelSpacing); auto *label = new QLabel(caption); - label->setStyleSheet(QStringLiteral("color:#667085;font-weight:600;")); + label->setProperty("kind", "settingLabel"); const int labelHeight = label->fontMetrics().height(); label->setFixedHeight(labelHeight); label->setBuddy(buddy ? buddy : control); @@ -187,9 +180,6 @@ QString optionalString(const nlohmann::json &object, const char *key) { TurnSettingsWidget::TurnSettingsWidget(QWidget *parent) : QWidget(parent) { setObjectName(QStringLiteral("codexTurnSettings")); - setStyleSheet(QStringLiteral( - "QWidget#codexTurnSettings{background:#ffffff;border-top:1px solid " - "#d7dee8;}")); setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed); auto *root = new QGridLayout(this); root->setContentsMargins(10, 8, 10, 8); @@ -204,8 +194,6 @@ TurnSettingsWidget::TurnSettingsWidget(QWidget *parent) : QWidget(parent) { cwd = new QLineEdit; cwd->setObjectName(QStringLiteral("codexWorkspace")); cwd->setFixedHeight(SettingControlHeight); - cwd->setStyleSheet( - QStringLiteral("QLineEdit#codexWorkspace{min-height:30px;}")); cwd->setPlaceholderText(QStringLiteral("Thread default workspace")); auto *workspacePicker = new QWidget; workspacePicker->setFixedHeight(SettingControlHeight); @@ -269,7 +257,7 @@ TurnSettingsWidget::TurnSettingsWidget(QWidget *parent) : QWidget(parent) { addChoice(effort, QStringLiteral("Thread default"), DefaultValue); for (const char *value : {"minimal", "low", "medium", "high", "xhigh", "ultra"}) - addChoice(effort, friendly(QString::fromLatin1(value)), + addChoice(effort, UiStyle::humanizeLabel(QString::fromLatin1(value)), QString::fromLatin1(value)); addChoice(sandbox, QStringLiteral("Thread default"), DefaultValue); addChoice(sandbox, QStringLiteral("Workspace"), @@ -640,8 +628,8 @@ void TurnSettingsWidget::refreshModelOptions() { const std::string defaultEffort = stringValue(*definition, "defaultReasoningEffort"); if (!defaultEffort.empty()) - defaultLabel = - friendly(text(defaultEffort)) + QStringLiteral(" - default"); + defaultLabel = UiStyle::humanizeLabel(text(defaultEffort)) + + QStringLiteral(" - default"); } addChoice(effort, defaultLabel, DefaultValue); const nlohmann::json supported = @@ -652,12 +640,12 @@ void TurnSettingsWidget::refreshModelOptions() { for (const auto &option : supported) { const std::string key = stringValue(option, "reasoningEffort"); if (!key.empty()) - addChoice(effort, friendly(text(key)), text(key)); + addChoice(effort, UiStyle::humanizeLabel(text(key)), text(key)); } } else { for (const char *key : {"minimal", "low", "medium", "high", "xhigh", "ultra"}) - addChoice(effort, friendly(QString::fromLatin1(key)), + addChoice(effort, UiStyle::humanizeLabel(QString::fromLatin1(key)), QString::fromLatin1(key)); } const QString requestedEffort = @@ -699,7 +687,8 @@ void TurnSettingsWidget::refreshModelOptions() { if (legacyTiers.is_array()) { for (const auto &tier : legacyTiers) { if (tier.is_string()) - addChoice(serviceTier, friendly(text(tier.get())), + addChoice(serviceTier, + UiStyle::humanizeLabel(text(tier.get())), text(tier.get())); } } diff --git a/src/codex/middle/ConversationCards.cpp b/src/codex/middle/ConversationCards.cpp index b7c1bc1..b0eb8ea 100644 --- a/src/codex/middle/ConversationCards.cpp +++ b/src/codex/middle/ConversationCards.cpp @@ -273,12 +273,13 @@ bool setVisibleMarkdown(QLabel *label, const QString &markdown) { QString displayStatus(const QString &status) { const QByteArray encoded = status.toUtf8(); - const std::string_view display = - classifyStatus(std::string_view(encoded.constData(), - static_cast(encoded.size()))) - .text; - return QString::fromUtf8(display.data(), - static_cast(display.size())); + const PresentationStatus classified = classifyStatus(std::string_view( + encoded.constData(), static_cast(encoded.size()))); + const QString display = QString::fromUtf8( + classified.text.data(), static_cast(classified.text.size())); + return classified.kind == StatusKind::Unknown + ? UiStyle::humanizeLabel(display) + : display; } QString statusTone(const QString &status) { @@ -338,9 +339,7 @@ QString agentMetadata(const AgentActivityData &activity) { QString displayChangeKind(const QString &kind) { if (kind.isEmpty()) return QStringLiteral("Changed"); - QString result = kind; - result[0] = result[0].toUpper(); - return result; + return UiStyle::humanizeLabel(kind); } struct DiffCounts { @@ -960,8 +959,9 @@ class ConversationCard::Impl final { } case CardKind::GenericActivity: { const auto &activity = std::get(data.payload); - title->setText(activity.type.isEmpty() ? QStringLiteral("Activity") - : activity.type); + title->setText(activity.type.isEmpty() + ? QStringLiteral("Activity") + : UiStyle::humanizeLabel(activity.type)); metadata->setText(boundedGenericActivity(activity.raw)); metadata->setObjectName(QStringLiteral("genericActivityMetadata")); metadata->show(); diff --git a/src/codex/middle/InspectorPane.cpp b/src/codex/middle/InspectorPane.cpp index 9fe7c17..4309dcd 100644 --- a/src/codex/middle/InspectorPane.cpp +++ b/src/codex/middle/InspectorPane.cpp @@ -251,7 +251,6 @@ QFrame *InspectorPane::agentFrame(const AgentSnapshot &agent) { InspectorPane::InspectorPane(QWidget *parent) : QFrame(parent) { setObjectName(QStringLiteral("inspector")); - setStyleSheet(QStringLiteral("QFrame#inspector{background:#fbfcfe;}")); setMinimumWidth(300); setMaximumWidth(520); @@ -610,7 +609,8 @@ void InspectorPane::refreshRequests() { auto *layout = new QVBoxLayout(frame); layout->setContentsMargins(12, 10, 12, 10); layout->setSpacing(6); - layout->addWidget(makeLabel(text(request.kind), "title")); + layout->addWidget( + makeLabel(UiStyle::humanizeLabel(text(request.kind)), "title")); layout->addWidget( makeLabel(QStringLiteral("thread %1 | generation %2 | request %3") .arg(text(request.threadContext)) diff --git a/src/codex/ui/UiStyle.cpp b/src/codex/ui/UiStyle.cpp index d9ed12c..1b15d60 100644 --- a/src/codex/ui/UiStyle.cpp +++ b/src/codex/ui/UiStyle.cpp @@ -80,6 +80,8 @@ QString applicationStyleSheet() { font-size: %1pt; } QMainWindow, QWidget#applicationShell { background: #f6f8fb; } + QWidget#codexTurnSettings { background: #ffffff; border-top: 1px solid #d7dee8; } + QFrame#inspector { background: #fbfcfe; } QLabel { background: transparent; font-weight: 400; } QLabel[kind="muted"] { color: #667085; font-size: %1pt; } QLabel[kind="section"] { @@ -106,6 +108,7 @@ QString applicationStyleSheet() { QLabel[kind="code"] { font-family: monospace; font-size: %2pt; font-weight: 400; } QLabel[kind="meta"] { color: #667085; font-size: %1pt; } QLabel[kind="small"] { color: #667085; font-size: %1pt; } + QLabel[kind="settingLabel"] { color: #667085; font-weight: 600; } QLabel[tone="active"] { color: #285fca; } QLabel[tone="success"] { color: #176b45; } QLabel[tone="warning"] { color: #8a5208; } @@ -223,6 +226,7 @@ QString applicationStyleSheet() { selection-color: #1d2633; } QPlainTextEdit[empty="true"] { color: #98a2b3; } + QLineEdit#codexWorkspace { min-height: 30px; } QPlainTextEdit[kind="code"], QPlainTextEdit[kind="command"], QTextEdit[kind="code"], QTextEdit[kind="command"], QPlainTextEdit[kind="infoViewer"] { @@ -490,4 +494,34 @@ QString applicationStyleSheet() { .arg(compact, standard, section, heading, panelHeader); } +QString humanizeLabel(QString value) { + value = value.trimmed(); + QString result; + result.reserve(value.size() + 4); + bool space = false; + for (qsizetype index = 0; index < value.size(); ++index) { + QChar character = value[index]; + if (character.isSpace() || character == QLatin1Char('-') || + character == QLatin1Char('_') || character == QLatin1Char('.') || + character == QLatin1Char('/')) { + space = !result.isEmpty(); + continue; + } + const QChar previous = index > 0 ? value[index - 1] : QChar{}; + const QChar next = index + 1 < value.size() ? value[index + 1] : QChar{}; + const bool wordBoundary = + character.isUpper() && (previous.isLower() || previous.isDigit() || + (previous.isUpper() && next.isLower())); + if ((space || wordBoundary) && !result.endsWith(QLatin1Char(' '))) + result.append(QLatin1Char(' ')); + if (wordBoundary || (space && character.isUpper() && next.isLower())) + character = character.toLower(); + result.append(character); + space = false; + } + if (!result.isEmpty()) + result[0] = result[0].toUpper(); + return result; +} + } // namespace codexui::UiStyle diff --git a/src/codex/ui/UiStyle.h b/src/codex/ui/UiStyle.h index f7a4ffe..af980bc 100644 --- a/src/codex/ui/UiStyle.h +++ b/src/codex/ui/UiStyle.h @@ -50,6 +50,7 @@ inline constexpr auto redText = "#982f3d"; inline constexpr auto purple = "#6941c6"; QString applicationStyleSheet(); +QString humanizeLabel(QString value); enum class ChevronDirection { Down, Left, Right }; void drawChevron(QWidget *widget, const QRect &indicator, bool enabled, bool highlighted, diff --git a/tests/codex/ApplicationLayoutTest.cpp b/tests/codex/ApplicationLayoutTest.cpp index b5986c9..0c64f74 100644 --- a/tests/codex/ApplicationLayoutTest.cpp +++ b/tests/codex/ApplicationLayoutTest.cpp @@ -560,12 +560,21 @@ bool testIncrementalThreadSettings() { if (!model || !approval || !access || !network) return expect(false, "thread settings controls are discoverable"); + bool canonicalSettingsStyle = settings.styleSheet().isEmpty(); + for (const QLabel *label : settings.findChildren()) { + if (label->text() == QStringLiteral("Model")) + canonicalSettingsStyle &= label->property("kind") == "settingLabel" && + label->styleSheet().isEmpty(); + } + model->setCurrentIndex(model->findData(QStringLiteral("gpt-b"))); settings.setContext( "thread-a", {{"model", "gpt-a"}, {"approvalPolicy", "on-request"}}, models, nlohmann::json::array(), 1, {{"approvalPolicy", "on-request"}}); - bool result = expect( + bool result = expect(canonicalSettingsStyle, + "thread settings use canonical application styling"); + result &= expect( model->currentData().toString() == QStringLiteral("gpt-b") && approval->currentData().toString() == QStringLiteral("on-request"), "a partial authoritative update preserves unrelated pending settings"); @@ -1312,10 +1321,12 @@ bool testInspectorDetailParity() { "long agent Markdown follows visible metadata without surplus height"); inspector.tabs()->setCurrentIndex(3); spin(20); - result &= expect( - hasLabelContaining(inspector, QStringLiteral("thread Original title")) && - hasLabelContaining(inspector, QStringLiteral("3 questions")), - "Requests show their thread title and retained question count"); + result &= + expect(hasLabelContaining(inspector, QStringLiteral("User input")) && + hasLabelContaining(inspector, + QStringLiteral("thread Original title")) && + hasLabelContaining(inspector, QStringLiteral("3 questions")), + "Requests show their thread title and retained question count"); model.applyEvent(presentation::event( 5, 1, "thread.name.changed", {{"name", "Renamed title"}}, presentation::Authority::Replace, {{"threadId", "owner-thread"}})); diff --git a/tests/codex/ConversationCardsTest.cpp b/tests/codex/ConversationCardsTest.cpp index 5fc4c86..71e9f7f 100644 --- a/tests/codex/ConversationCardsTest.cpp +++ b/tests/codex/ConversationCardsTest.cpp @@ -1675,17 +1675,29 @@ bool testGeneratedImagePresentationAndGenericBound() { "generated", "turn", "unknown", - GenericActivityData{QStringLiteral("Unknown activity"), - {{"large", std::string(100000, 'x')}}}}; + GenericActivityData{QStringLiteral("contextCompaction"), + {{"type", "contextCompaction"}, + {"large", std::string(100000, 'x')}}}}; ConversationCard genericCard(generic); genericCard.show(); spin(); auto *details = genericCard.findChild( QStringLiteral("genericActivityMetadata")); - result &= expect(details && details->text().size() < 4200 && - details->text().endsWith( - QStringLiteral("[Activity details truncated]")), - "unknown activity text is bounded before Qt lays it out"); + const auto genericLabels = genericCard.findChildren(); + result &= expect( + std::ranges::any_of(genericLabels, + [](QLabel *label) { + return label->property("kind").toString() == + QStringLiteral("title") && + label->text() == + QStringLiteral("Context compaction"); + }) && + std::get(generic.payload).type == + QStringLiteral("contextCompaction") && + details && details->text().size() < 4200 && + details->text().endsWith( + QStringLiteral("[Activity details truncated]")), + "protocol labels are humanized without changing bounded raw details"); return result; }