diff --git a/docs/ui-behavior.md b/docs/ui-behavior.md index eec579a..7cad721 100644 --- a/docs/ui-behavior.md +++ b/docs/ui-behavior.md @@ -31,6 +31,9 @@ keyed by stable thread, turn, and item IDs; local prompt cards are keyed by their submission IDs. The same keyed reconcile path handles initial display and updates, mutating a card in place when its visible data changes. An identical visible projection does not rebuild widgets or change geometry. +The complete prompt content, including attachments, adds the canonical 8 px +structural section gap before its first nested turn card. This spacing is +layout geometry and never becomes part of authored Markdown. Local prompt admission resumes bottom following when the only pause was caused by composer overlay growth, so the complete pending prompt becomes visible. diff --git a/src/codex/middle/ConversationCards.cpp b/src/codex/middle/ConversationCards.cpp index ee39da6..c0a64e9 100644 --- a/src/codex/middle/ConversationCards.cpp +++ b/src/codex/middle/ConversationCards.cpp @@ -1009,7 +1009,9 @@ class ConversationCard::Impl final { nestedCards = new QWidget(owner); nestedCards->setObjectName(QStringLiteral("conversationNestedCards")); nestedLayout = new QVBoxLayout(nestedCards); - nestedLayout->setContentsMargins(0, 0, 0, 0); + // Keep a visible section boundary between the complete prompt and the + // activity nested beneath it, in addition to ordinary widget spacing. + nestedLayout->setContentsMargins(0, 8, 0, 0); nestedLayout->setSpacing(8); nestedCards->hide(); layout->addWidget(nestedCards); diff --git a/tests/codex/ConversationCardsTest.cpp b/tests/codex/ConversationCardsTest.cpp index 015e0f2..befee16 100644 --- a/tests/codex/ConversationCardsTest.cpp +++ b/tests/codex/ConversationCardsTest.cpp @@ -1458,7 +1458,24 @@ bool testCardFoldingGeometryAndRetention() { view.resize(700, 820); view.setTrailingSpaceHeight(500); view.show(); - bool result = expect(view.reconcile(snapshot), "folding fixture renders"); + ConversationSnapshot promptOnly = snapshot; + promptOnly.sections.front().cards = {user}; + bool result = + expect(view.reconcile(promptOnly), "prompt-only folding fixture renders"); + spin(); + + ConversationCard *promptOnlyCard = card(view, stableKey(user.key)); + QWidget *promptOnlyNestedCards = + promptOnlyCard + ? promptOnlyCard->findChild( + QStringLiteral("conversationNestedCards"), + Qt::FindDirectChildrenOnly) + : nullptr; + result &= expect(promptOnlyCard && promptOnlyNestedCards && + promptOnlyNestedCards->isHidden(), + "an initial turn prompt reserves no nested-card gap"); + result &= expect(view.reconcile(snapshot), + "first nested activity extends the folding fixture"); spin(); ConversationCard *userCard = card(view, stableKey(user.key)); @@ -1483,10 +1500,12 @@ bool testCardFoldingGeometryAndRetention() { disclosure(reasoningCard)->property("chevronDirection") == "left", "all cards share disclosure controls with role-correct initial state"); result &= expect( - userCard && userCard->property("authoritativeTurnActive").toBool() && + userCard && userCard == promptOnlyCard && + userCard->property("authoritativeTurnActive").toBool() && !agentCardWidget->property("authoritativeTurnActive").toBool() && !userCard->findChild(QStringLiteral("activeTurnAnimation")), - "only the running outer You card receives a static emphasized border"); + "the retained running outer You card receives a static emphasized " + "border"); snapshot.activeTurnId.reset(); result &= expect(view.reconcile(snapshot) && userCard == card(view, stableKey(user.key)) && @@ -1519,6 +1538,20 @@ bool testCardFoldingGeometryAndRetention() { userCard->isAncestorOf(reasoningCard) && agentCardWidget->property("nestedConversationCard").toBool(), "the first You card structurally owns its turn activity"); + QWidget *promptContent = userCard->findChild( + QStringLiteral("conversationCardContent"), Qt::FindDirectChildrenOnly); + const int promptContentBottom = + promptContent ? promptContent->geometry().y() + promptContent->height() + : -1; + const int firstNestedTop = agentCardWidget->mapTo(userCard, QPoint{}).y(); + QWidget *nestedCards = userCard->findChild( + QStringLiteral("conversationNestedCards"), Qt::FindDirectChildrenOnly); + result &= expect( + promptContent && nestedCards && nestedCards->layout() && + nestedCards->layout()->contentsMargins().top() == 8 && + firstNestedTop - promptContentBottom == 14, + "turn prompt content adds a visible canonical 8 px section boundary " + "before its first nested card"); const LocalPromptKey steeringKey{4343}; VisibleCardData steering{ diff --git a/web/src/styles.css b/web/src/styles.css index 5225413..32da6de 100644 --- a/web/src/styles.css +++ b/web/src/styles.css @@ -89,7 +89,7 @@ h1, h2, h3, p { margin: 0; } .conversation-scroll::-webkit-scrollbar-track { margin-block-end: calc(var(--composer-overlay-height) + 8px); } .turn-section { display: flex; flex-direction: column; gap: 10px; margin-bottom: 14px; } .turn-section:last-child { margin-bottom: 0; } -.turn-nested { display: flex; flex-direction: column; gap: 8px; margin-top: 10px; } +.turn-nested { display: flex; flex-direction: column; gap: 8px; margin-top: 8px; } .conversation-card { border: 1px solid #dce2eb; border-radius: 11px; background: #fff; padding: 13px 15px; box-shadow: 0 2px 5px #1720330a; overflow: hidden; } .conversation-card > header { display: flex; justify-content: space-between; align-items: baseline; margin-bottom: 8px; color: #667085; } .conversation-card > header span { color: #38445a; font-size: 11px; font-weight: 750; text-transform: uppercase; letter-spacing: .07em; } diff --git a/web/tests/responsive-layout.test.mjs b/web/tests/responsive-layout.test.mjs index 4ae5a8c..342069e 100644 --- a/web/tests/responsive-layout.test.mjs +++ b/web/tests/responsive-layout.test.mjs @@ -93,6 +93,7 @@ test("responsive CSS keeps the desktop grid and removes the old document-width f assert.match(css, /\.thread-row \.status-dot\.danger\s*\{[^}]*background:\s*#c43d4d/u); assert.match(css, /\.conversation-card\.userMessage\.steering\s*\{[^}]*background:\s*#eefafa;[^}]*border-color:\s*#9fd7d8/u); assert.match(css, /\.conversation-card\.localPrompt\.steering\s*\{[^}]*background:\s*#eefafa;[^}]*border-color:\s*#5caeb1/u); + assert.match(css, /\.turn-nested\s*\{[^}]*gap:\s*8px;[^}]*margin-top:\s*8px/u); assert.doesNotMatch(css, /acknowledgment-fade/u); assert.match(css, /conversation-card\.turn-container\.active-turn[^}]*#6f98e8/u); assert.match(css, /\.send-button\.steer\s*\{[^}]*background:\s*#167b80[^}]*color:\s*#fff/u);