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
27 changes: 14 additions & 13 deletions docs/codex-architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,8 +112,8 @@ deliberately small and protocol-complete:
or focusing the composer and preserving its local-admission scroll behavior;
- an optional read-only frame observer feeds the bounded Protocol diagnostic
without giving that renderer state or replay authority;
- absolute wakeups let the existing Qt timer drive deferred dispatch and visual
acknowledgment transitions without introducing another scheduler.
- absolute wakeups let the existing Qt timer drive deferred dispatch and the
pending-feedback threshold without introducing another scheduler.

Downward communication uses the value-type `PresentationClient`: generic
correlated `execute(action, data, completion)`, fire-and-forget
Expand Down Expand Up @@ -661,19 +661,20 @@ are updated in place, absent keys are removed, new keys are inserted at their
projected positions, and an identical typed projection is a true visual no-op.

Prompt admission and app-server acknowledgment are separate states. On Send or
Steer, CodexUI immediately appends a client-local pending user card to the
destination thread. The card uses a muted blue user-prompt treatment and a
Qt-painted highlight sweeping left and right until the correlated app-server
result callback arrives. Only the matching `turn.start` or `turn.steer`
Steer, CodexUI immediately appends a calm client-local user card with an
emphasized blue or teal border to the destination thread. If the correlated
app-server result has not arrived after one second, a Qt-painted highlight
begins sweeping left and right. Only the matching `turn.start` or `turn.steer`
completion callback acknowledges the prompt; conversation events cannot infer
acknowledgment. Each request carries a unique `clientUserMessageId`, allowing
the resulting user item to bind exactly even when prompts have identical text.
A fast successful result retains a 500-millisecond accepted transition so the
state change remains visible. Pending cards survive thread switching and
The matching success or definitive failure stops the sweep immediately; the
one-second wakeup changes presentation only and cannot acknowledge a request.
Pending cards survive thread switching and
become normal authoritative user messages when the corresponding app-server
item materializes. The pending and authoritative forms share one visual key
and anchor during the accepted transition. Once materialization and that
transition are complete, the local submission is removed and the retained item
item materializes. The pending and authoritative forms share one visual key,
anchor, and active-turn border during that transition. Once materialization and
acknowledgment are complete, the local submission is removed and the retained item
uses its authoritative identity. Failure produces a retained error card.

The composer remains enabled while acknowledgments are outstanding. Multiple
Expand Down Expand Up @@ -1595,8 +1596,8 @@ The remaining presentation-level choices are implemented as follows:
- the Info/Protocol view retains at most 2,000 text blocks and the presentation
model retains at most 256 authority-free telemetry records; the protocol
statistics summary is below the expanding log;
- pending prompt acknowledgment uses a per-thread animated card rather than an
application-wide busy state or composer lock;
- overdue prompt acknowledgment uses delayed per-thread card feedback rather
than an application-wide busy state or composer lock;
- reaching the conversation bottom re-enables automatic following, including
after scrolling through composer-added trailing space or a contraction clamp;
- paused conversation updates preserve the first visible stable card and its
Expand Down
54 changes: 36 additions & 18 deletions docs/ui-behavior.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ bottom or is owned by the user.
unaffected.
- The Plan inspector preserves app-server step states while the owning turn is
active. If a stale step still reports `inProgress` after its owning turn or
thread becomes terminal, the display reconciles that step to Completed,
thread becomes terminal, the display reconciles that step to `completed`,
Failed, or Interrupted. Pending steps remain Pending, and retained protocol
data is not rewritten.
- Each visible thread is presented as a compact card. Its status indicator is
Expand Down Expand Up @@ -110,9 +110,10 @@ events and Enter used to confirm an active input-method composition never
submit a prompt.

Submitting a prompt creates a client-local pending prompt card at the bottom of
the destination thread immediately. The card uses a muted version of the normal
blue user-card treatment, with a brighter blue highlight sweeping left and
right across it until the app-server acknowledges the operation.
the destination thread immediately. The card begins with the calm blue
user-card treatment and an emphasized border. If the correlated app-server
result has not arrived after one second, a brighter blue highlight begins
sweeping left and right.
Ordinary attached files appear as local Markdown links at the bottom of that
card from its first frame. The same composed Markdown is sent to app-server and
retained by the authoritative user message, so acknowledgment does not reflow
Expand All @@ -121,27 +122,34 @@ encoded as path content rather than being misread as a fragment or query.

Each pending prompt has a process-wide client-local submission ID and remains
associated with its destination thread. It therefore remains visible when the
user switches threads and returns. On
successful acknowledgment, the card shows a short accepted sweep before it
becomes a normal user message. If the authoritative app-server item arrives
during that transition, it inherits the pending card's stable visual anchor and
replaces it after the 500-millisecond transition completes. Only the correlated
user switches threads and returns. Successful acknowledgment or definitive
failure stops delayed feedback immediately. The one-second timer controls only
whether pending feedback is visible; it never acknowledges or promotes the
prompt. If the authoritative app-server item arrives before or after the
result, it inherits the pending card's stable visual anchor and replaces it as
soon as both correlation and acknowledgment are complete. Only the correlated
`turn.start` or `turn.steer` completion callback acknowledges a prompt;
conversation events never infer acknowledgment. Each operation carries a
unique `clientUserMessageId`, which binds the authoritative user item without
confusing identical prompt text. A failed submission remains visible with an
explicit error state.

A prompt that starts a turn is the outer soft-blue turn card. A prompt admitted
through `turn.steer` appears immediately inside the active turn as an animated
teal `You · steering` card. After acknowledgment, the same widget becomes a
soft-teal inset steering card with the canonical teal border and title treatment.
through `turn.steer` appears immediately inside the active turn as a calm teal
`You` card with a right-aligned `steering` specialization. It uses the same
one-second delayed-feedback rule as the outer card. After
acknowledgment, the same widget becomes a soft-teal inset steering card with
the canonical teal border and title treatment.
No optimistic card is exchanged for a second widget, and the turn grows around
it without changing existing nested card identity.

After acknowledgment, the authoritative outer You card uses a stronger static
blue border while its turn remains active. It has no animation, glow, shading,
or geometry change. Completion restores the canonical border in place.
At acknowledgment, the retained outer You card immediately uses the stronger
static blue running border. That border belongs to the card across its local-
prompt-to-authoritative-message morph while pending feedback stops; it
has no animation, glow, shading, or geometry change. A successful `turn.start`
result retains active ownership until the separate authoritative lifecycle
catches up, so the optimistic-to-running handoff has no neutral-border frame.
Completion restores the canonical border in place.

The composer is cleared immediately after local admission and remains enabled.
Users may enter additional prompts while earlier prompts await acknowledgment.
Expand Down Expand Up @@ -200,7 +208,13 @@ User messages use the canonical soft-blue identity surface. Final Codex
messages use the canonical soft-violet identity surface, while interim Codex
updates remain neutral and identify their phase in the header. Process cards
also remain neutral so they support rather than dominate the primary exchange.
Status text alone uses canonical semantic state colors.
Their lifecycle status is a normal-weight lowercase value at the right of the
header, immediately before Copy, and uses canonical semantic state colors.
Thread rows, conversation metadata, Inspector entries, and process cards share
the same vocabulary: `running`, `completed`, `failed`, `interrupted`, `pending`,
and `not loaded`. Command exit
code, cwd, and duration; file totals; and agent execution context remain below
the primary content. Generated-image cards have no duplicate body-status row.

Every conversation card with visible detail uses the same keyboard-focusable
disclosure chevron: down when expanded and left when collapsed. Title-only
Expand Down Expand Up @@ -231,6 +245,10 @@ appears at the action. Web clipboard failure uses the same
local overlay with canonical error styling; reduced-motion mode suppresses the
breath without suppressing the result.

Message specializations (`steering`, `update`, and `final answer`) use normal
font weight and sit at the right of the header immediately before Copy. The
title remains at the left; no separator glyph is rendered.

Pending-request dialogs validate required answers and structured MCP content
before accepting the modal. Invalid input keeps the dialog and all entered
content open for correction.
Expand Down Expand Up @@ -411,8 +429,8 @@ correct launcher and taskbar icon.

Long-running operations need scoped progress presentation rather than a global
busy state. Candidate scopes include prompt acknowledgment, thread creation,
and loading a long thread. Pending prompt acknowledgment already has its own
animated highlight sweep. Any additional progress indicator must preserve input
and loading a long thread. Overdue prompt acknowledgment already has its own
delayed highlight sweep. Any additional progress indicator must preserve input
and navigation that can safely remain interactive, identify the operation it
represents, and avoid suggesting that unrelated threads are blocked. No general
spinner contract is defined yet.
51 changes: 46 additions & 5 deletions src/codex/PresentationStatus.h
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
#ifndef CODEXUI_CODEX_PRESENTATIONSTATUS_H
#define CODEXUI_CODEX_PRESENTATIONSTATUS_H

#include <cctype>
#include <string>
#include <string_view>

namespace codexui::codex {
Expand All @@ -13,6 +15,8 @@ enum class StatusKind {
Completed,
Failed,
Interrupted,
Pending,
NotLoaded,
};

struct PresentationStatus {
Expand All @@ -24,17 +28,54 @@ struct PresentationStatus {
constexpr PresentationStatus classifyStatus(std::string_view status) noexcept {
if (status == "active" || status == "inProgress" || status == "running" ||
status == "started")
return {StatusKind::Active, "Running", "active"};
return {StatusKind::Active, "running", "active"};
if (status == "completed" || status == "idle")
return {StatusKind::Completed, "Completed", "success"};
return {StatusKind::Completed, "completed", "success"};
if (status == "failed" || status == "systemError")
return {StatusKind::Failed, "Failed", "danger"};
return {StatusKind::Failed, "failed", "danger"};
if (status == "interrupted")
return {StatusKind::Interrupted, "Interrupted", "warning"};
return {StatusKind::Unknown, status.empty() ? "Unknown" : status,
return {StatusKind::Interrupted, "interrupted", "warning"};
if (status == "pending")
return {StatusKind::Pending, "pending", {}};
if (status == "notLoaded")
return {StatusKind::NotLoaded, "not loaded", {}};
return {StatusKind::Unknown, status.empty() ? "unknown" : status,
std::string_view{}};
}

inline std::string displayStatus(std::string_view status) {
const PresentationStatus classified = classifyStatus(status);
if (classified.kind != StatusKind::Unknown || status.empty())
return std::string(classified.text);

std::string result;
result.reserve(status.size() + 4);
bool pendingSpace = false;
for (std::size_t index = 0; index < status.size(); ++index) {
const unsigned char character = static_cast<unsigned char>(status[index]);
if (std::isspace(character) || character == '-' || character == '_' ||
character == '.' || character == '/') {
pendingSpace = !result.empty();
continue;
}
const unsigned char previous =
index == 0 ? 0 : static_cast<unsigned char>(status[index - 1]);
const unsigned char next =
index + 1 == status.size()
? 0
: static_cast<unsigned char>(status[index + 1]);
const bool upper = std::isupper(character);
const bool boundary =
upper && (std::islower(previous) || std::isdigit(previous) ||
(std::isupper(previous) && std::islower(next)));
if ((pendingSpace || boundary) && !result.empty() && result.back() != ' ')
result.push_back(' ');
result.push_back(static_cast<char>(std::tolower(character)));
pendingSpace = false;
}
return result.empty() ? std::string("unknown") : result;
}

constexpr bool isActiveStatus(std::string_view status) noexcept {
return classifyStatus(status).kind == StatusKind::Active;
}
Expand Down
Loading
Loading