Skip to content

feat(tui): add a Vim-style terminal workspace - #43

Merged
TroyMitchell911 merged 11 commits into
muggle-stack:masterfrom
TroyMitchell911:feat/tui-workspace
Sep 16, 2026
Merged

TroyMitchell911 merged 11 commits into
muggle-stack:masterfrom
TroyMitchell911:feat/tui-workspace

Conversation

@TroyMitchell911

@TroyMitchell911 TroyMitchell911 commented Sep 14, 2026

Copy link
Copy Markdown
Collaborator

Summary

Add an optional, full-screen Textual terminal workspace for cc-remote, based
on the current upstream master (ddc03f0). This is a client of the existing
relay and wrapper, not a separate Codex/Claude runner. Queue reordering adds protocol v67 and requires a coordinated
Relay/Web/Wrapper/TUI upgrade. The TUI itself does not restart, replace, or
take over the shared app-server.

The goal is to make multi-session work practical from a keyboard without
managing a separate tmux pane for every conversation. Reading position,
selection, and the unsent draft stay independent.

Features

  • Session workspace: a searchable directory tree, explicit Codex/Claude
    and Code/Work scopes, responsive tabs, background activity/unread markers,
    per-session drafts and cached projections, and a privately persisted open
    tab set. Closing a tab does not delete a session or stop its task.
  • Vim-oriented interaction: Normal/Insert modes, counts, operators, text
    objects, selections, jump history, and previous/next user or assistant
    message navigation. Copying or quoting text preserves the reading cursor.
    Non-Vim shortcuts use a configurable, searchable, layered registry.
  • Conversation rendering: Markdown, tables, terminal Mermaid flowcharts,
    visible link URLs, distinct
    user/final/progress/tool presentation, live elapsed times, and two-level
    activity folds. Outer progress sections start open; tool groups beneath
    each progress message start closed and summarize calls/file changes.
  • Stable reading: follow new output only when already at the bottom,
    independently of focus. Reflow, streamed updates, folding, and image layout
    retain the viewport or its source anchor instead of following the cursor.
  • Shared controls: session creation and lifecycle actions, models,
    permissions, goals/plans, usage/context, wrapper-owned queues, background
    work, and report/action panels reuse the Python protocol. Async question
    answers steer a running session rather than interrupting it; blocking
    questions retain their original response path.
  • Files and images: directory picking, read-only document previews,
    clipboard attachments, inline image placement, and a pannable/zoomable
    image viewer. Kitty image resources are reused during navigation. New
    upstream context-capacity, directory-browsing, and turn-file-page actions
    are exposed through the same scoped action/report system.

Architecture and review guide

The branch contains 11 feature-focused, GPG-signed commits. Fixes to this
unmerged TUI are folded into their originating feature modules; only the
pre-existing Work error-correlation bug and Viewer test race remain separate.
Suggested review order:

  1. tui.py, tui_local.py, and tui_actions.py: authenticated transport,
    local startup discovery, routing, and protocol-backed commands.
  2. tui_state.py, tui_presentation.py, tui_activity.py, and
    tui_questions.py: bounded history/live projections and turn semantics.
  3. tui_vim.py, tui_modal.py, tui_text_objects.py, and tui_keys.py:
    independent editing modes and scoped key dispatch.
  4. tui_tree.py, tui_buffers.py, tui_tab_store.py, tui_panels.py, and
    tui_settings.py: session navigation and controls.
  5. tui_app.py, Markdown/preview/image modules, and tests/test_tui*.py:
    rendering, viewport behavior, and mounted keyboard/frame regressions.

The relay implementation is unchanged. Python/Web wire schemas and build
metadata move to v67 for a bounded, compare-and-swap queue-order command.
The wrapper atomically reorders pending queries, broadcasts the authoritative
queue, rejects stale/starting changes, and retains prompt/attachment identity.
It also correlates existing Work deletion errors with their requests.
Textual dependencies are optional for runtime installations and are included
in development requirements for the tests. The original line client remains
available with --line-mode.

Authentication and boundaries

  • Same-user Linux startup can reuse the running user relay's configuration
    without another password prompt. It does not disable authentication,
    persist a second password, or send discovered credentials to another
    endpoint. Other configurations still require explicit authentication.
  • Device/session routing, read-only ownership, explicit preview
    authorization, and mutation confirmation remain enforced.
  • Browsing history and running the demo do not start a model turn. Closing
    the client leaves wrapper-owned running tasks and deferred queries intact.
  • The client must match the relay/wrapper protocol version. Installing this
    client does not deploy or restart either service.

Try it

In an existing development checkout:

uv pip install --python .venv/bin/python -r requirements-tui.txt
.venv/bin/python -m cc_remote.tui --demo
# Connect to your configured relay:
.venv/bin/python -m cc_remote.tui --engine codex

Space h opens help, Space e opens the session tree, H/L switch tabs,
and Ctrl+j/Ctrl+k switch between draft and transcript. Complete English
and Chinese guides and a shortcut configuration example are included in
docs/tui.md, docs/tui_zh.md, and docs/tui-keys.example.toml.

Ctrl+x stops the active writable turn without clearing its deferred queue.
The binding is configurable and does not fire from tree/dialog interactions.

Terminal image support is capability-dependent. PDF, interactive Viewer,
and other browser-oriented artifacts retain an explicit Web handoff rather
than pretending to provide a full browser inside the terminal.

Review follow-up

  • Space l provides queue management: i edits the full prompt, d opens
    cancellation confirmation, and K/J move the selected item earlier/later.
    All queue keys are configurable and indexed. Failed-preflight prompts
    remain editable; server replies, not optimistic local mutations, drive state.

  • Closing a preview opened from a numbered file list returns to that same
    list page and selection, rather than discarding the list and returning to chat.

  • Queued submissions retain the complete prompt privately until authoritative
    acceptance. A correlated rejection restores it without overwriting a newer
    draft; transport ACKs do not discard it, and immediate rejections do not
    clear the original draft.

  • Generic authorize_preview forms populate the original challenge request
    ID and preserve it when constructing the command. Other actions still reject
    caller-supplied transport request IDs.

  • Visual selections include both endpoints, including backward selections.

  • Invalid effective ENGINE values fail before the reconnect loop starts.

  • Local Markdown source/line limits show an explicit truncation marker.

  • Side-chat deletion uses CloseBtw and waits for authoritative closure.

  • Mermaid flowcharts render as bounded selectable boxes/arrows in chat and
    Markdown previews, including Unicode labels and line wrapping. Unsupported
    syntax stays visible as source; no external renderer or script is executed.

  • Explicit engine/space arguments and ENGINE override saved tab scope;
    unspecified axes can still be restored.

  • The WebSocket always selects its machine explicitly, including default.

  • Work deletion errors carry the command ID and finish the matching waiter.

  • Detail pages retain both directions: o reads older details, and configurable
    O returns toward newer content. Nested folds display the effective keys.

  • Blocking question choices render the protocol's ds descriptions.

  • Queue edits wait for a matching server result, preserve rejected drafts, and
    do not treat an ACK or local outbox admission as success.

  • Directory traversal tests no longer depend on a locally installed fzf.
    Tail-follow tests validate the completed history response, not a transient
    state. Late directory-picker callbacks cannot access torn-down controls.

  • Unicode link and image destinations display readable characters, including
    Chinese filenames. Encoded URL syntax, controls and literal code stay intact.

  • Supplemental answers use Web's exact canonical envelope, one native question
    message at a time. Other unanswered questions remain reachable.

  • Every pending blocking question is read from the full session collection,
    not the legacy single-question slot.

  • Background session errors remain in their own transcript without replacing
    the focused workspace notice; global errors remain visible.

  • External-file authorization reuses the challenged request ID and accepts
    only the matching authorization result, not a late file-read response.

  • Direct session IDs wait for catalog-provided engine/space before prewarming;
    retained tabs removed from an authoritative catalog become unavailable.

  • Ambiguous supplemental replies keep their candidate questions pending, and
    blank blocking answers are rejected before entering the reliable outbox.

  • Attachment payloads survive transport ACKs until authoritative delivery;
    correlated rejections restore them without replacing newer draft files.

  • Completion acknowledgements can retry after reliable-outbox backpressure.

  • Discontinuous history replaces old source rows while preserving a proven
    newer live tail. Rollback artifact epochs invalidate inline image caches.

  • Projection comparisons and rendering use the same session/cwd/artifact
    identity, avoiding a transcript rebuild on every 100 ms timer tick.

  • Missing optional TUI dependencies produce installation guidance; unrelated
    import failures remain visible rather than being masked.

  • Explicit Work deletions enter the correlated confirmation/result flow.

  • WebSocket and image-authorization fixtures model catalog discovery and
    original request IDs. The Viewer revocation test uses atomic registry
    updates and checks denial both before and after catalog propagation.

  • The launcher resolves chained symlinks without GNU-only readlink -f,
    including relative targets and paths with spaces on BSD/macOS utilities.

  • Rejected catalog requests restore their prior correlation fence and retry
    after outbox backpressure. Invalidated catalogs stay fenced; immediate
    replies to admitted requests are still accepted.

  • BTW side chats refresh inherited scope, directory, and title after parent
    catalogs arrive, so Hello ordering does not strand Work side chats in Code.

  • Goal read fences apply only to identified GetGoal replies and retire on
    completion. Set/Clear/Dismiss confirmations remain authoritative, and late
    reads cannot overwrite a newer mutation.

  • Full queued prompts go only to the correlated one-shot editor, never to
    persistent Reports, including late replies after editor teardown.

  • Partial account catalog failures preserve the unavailable profile's prior
    sessions and control state, matching Web normalization. Successful profiles
    still apply deletions normally, and incoming rows replace retained metadata.

Validation

Latest follow-up: 6cdbbbf

  • 420 targeted Python regressions passed, covering queue edits/cancellation,
    atomic reorder, automatic drain order, stale/starting rejection, replay
    deduplication, preview navigation, configurable keys and protocol routing.
  • Ruff 0.15.13 passed for cc_remote, tests, and deploy.
  • Node 24 Web build, Python/Web protocol v67 validation and
    git diff --check passed.
  • All 11 feature-focused commits retain verified GPG signatures. The signed
    autosquash preserved the exact tested source tree.
  • The complete local CI gate was not rerun for this directly-pushed update;
    these targeted results are not a claim that full or remote CI is green.
  • This update does not deploy or restart running services. Protocol v67 must
    be deployed to all tiers together before connecting this client.

Previous validated head: 47f75d1

All required local gate commands completed successfully against 47f75d1.
The container's tracked-source checksums matched that committed snapshot.

  • Ubuntu 24.04, Node 24.18.0, and Python 3.13.13.
  • Python: 3,954 passed, 4 skipped. The predefined skips cover the
    root-bypassed /proc permission check, macOS kernel API, optional fzf,
    and explicitly opt-in real-model E2E scenario.
  • Additional non-root native Python run: 3,956 passed, 2 skipped;
    this also exercises the /proc permission and real fzf cases.
  • History browser matrix: 565 passed, 25 predefined conditional skips.
  • Viewer browser matrix: 44 passed, 6 predefined conditional skips;
    Viewer resource/discovery unit tests also passed.
  • Web build, reliability tests, and lint passed.
  • Ruff 0.15.13, deployment shell syntax, ShellCheck, and git diff --check
    passed.
  • All 42 commits at that head had verified GPG signatures.

Web source, browser assertions, and browser skip conditions are unchanged.
Native-host browser attempts with layout/scroll failures were not counted as
passes; the complete passing browser gate ran in the Ubuntu CI environment.
No running relay, wrapper, or Codex daemon was deployed or restarted.
Remote CI must also be green before merge.

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 14, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-16T07:57:03.158110Z c25d03f New commits
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 28bf02b82d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread cc_remote/tui_app.py
Comment on lines +125 to +127
if not self.want_sid:
self.engine, self.space = saved["scope"]
self.attached_engine = self.engine

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Honor explicit startup scope when restoring tabs

When tab state already exists and no session ID is supplied, this unconditionally replaces the requested engine and space with the saved scope. Consequently, cc-remote-tui --engine claude --space work (and an explicit ENGINE) can reopen an unrelated saved Codex/Code surface, contrary to the documented promise that these options choose the initial surface. Preserve the saved scope only when the user did not explicitly select one.

Useful? React with 👍 / 👎.

Comment thread cc_remote/tui.py Outdated
Comment on lines +334 to +336
socket_url = self.url
if self.machine_id != "default":
socket_url += "?" + urlencode({"machine": self.machine_id})

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Route the default machine explicitly

When machine_id is default, omitting the query parameter does not select that machine: the relay's /ws handler instead chooses the first connected authorized machine, and RelayHub.machine_ids is sorted, so a named device such as aaa wins over default. Thus even an explicit --machine default can connect to the wrong wrapper while the TUI sends/stores the identity as default; include the route parameter for this valid machine ID as well.

AGENTS.md reference: AGENTS.md:L102-L106

Useful? React with 👍 / 👎.

Comment thread cc_remote/tui_deletion.py
Comment on lines +101 to +104
else:
request = event.get("request_id")
receipt = self.pending.get(request)
if receipt is None or receipt.future.done():

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Correlate Work deletion errors before waiting

When deleting a running or queued Work session, _handle_delete_work_session emits its rejection with sid and to but no request_id. This lookup therefore ignores the error, while the later ACK cannot satisfy confirmed, leaving the deletion task blocked for 30 seconds before it replaces the real error with a timeout. The deletion workflow needs to correlate the wrapper's existing Work rejection shape or the wrapper must attach the command ID to those errors.

AGENTS.md reference: AGENTS.md:L147-L148

Useful? React with 👍 / 👎.

Comment thread cc_remote/tui_state.py
Comment on lines +1069 to +1072
cursor = (
event.get("oldest_cursor") if event.get("has_more") else None
)
view.details[turn] = cursor

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep a route back from older detail pages

After o requests an older source-disjoint TurnDetail page, the reducer replaces the displayed detail payload but records only oldest_cursor/has_more, ignoring the response's has_newer and newer_cursor. Further o presses can only move farther backward, and collapsing/re-expanding reuses the replaced page, so the newest detail content becomes inaccessible until the projection is reset or the TUI restarts. Track both paging directions or retain the previously loaded page.

Useful? React with 👍 / 👎.

Comment thread cc_remote/tui_panels.py
Comment on lines +462 to +465
for index, option in enumerate(self.ask.get("options", []), 1):
labels.append(
f"{index}. {option['label']}\n {option.get('description', '')}"
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Render the wire-format option descriptions

Blocking AskUser options carry their description in the protocol's ds field, but this dialog reads description, so every explanatory sentence is silently blank in the full-screen TUI even though the line-mode client and Web UI display it. For choices whose labels only summarize the action, this removes the impact/tradeoff information the user needs before answering; read option.get('ds') here.

Useful? React with 👍 / 👎.

Comment thread cc_remote/tui_panels.py Outdated
Comment on lines +749 to +752
if await self.client._send(command):
self.query_one(ModalEditor).set_mode("NORMAL")
self.query_one("#queue-result", Static).update(
"Submitted. Queue/result updates are authoritative; already-started messages cannot be edited."

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Surface authoritative queue-edit rejections

A successful _send only means the edit entered the reliable outbox, yet this immediately reports it as submitted and never correlates the subsequent QueuedQueryUpdated result. If the prompt was emptied without attachments, the queue exceeded its byte cap, or the item started between the read and update, the wrapper returns updated=false with an error while this screen continues implying success. Wait for the matching request_id and display that result so users do not assume a rejected edit changed the server-owned prompt.

AGENTS.md reference: AGENTS.md:L142-L149

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 47f75d193b

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread cc_remote/tui_panels.py Outdated
Comment on lines +601 to +604
text = "\n\n".join(
f"{q['title']}\n{answer}"
for q, answer in zip(self.questions, self.answers)
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Use the shared async-answer envelope

When a Codex async question is answered from the TUI and the same session is later viewed in Web, this plain title\nanswer format is not recognized as an answer. Web marks questions answered only when the subsequent prompt exactly matches supplementalAnswerPrompt in web/src/async-question-presentation.ts, so the question remains actionable and another client can submit a duplicate answer. Build the prompt with the same canonical envelope used by the Web client.

Useful? React with 👍 / 👎.

Comment thread cc_remote/tui_app.py
Comment on lines +205 to +207
def _line(self, text: str) -> None:
# Trusted legacy color sequences are removed along with remote controls.
self.notice = _safe_remote_text(Text.from_ansi(text).plain)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep background errors out of the focused notice

When another resident session emits a broadcast Error, the inherited Tui._handle calls _line without checking _for_me; this override consequently replaces the focused workspace's global notice with that background session's failure. The error is already projected into its own SessionView, so only sessionless or focused-session errors should update this notice.

AGENTS.md reference: AGENTS.md:L120-L126

Useful? React with 👍 / 👎.

Comment thread cc_remote/tui_app.py
Comment on lines +2311 to +2315
ask = self.client._pending_ask_for_attached()
if not ask:
sid = self.client.attached_sid
questions = (
pending_async(self.client.workspace.view(sid)) if sid else []

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve every pending blocking question

When concurrent tools or subagents leave more than one blocking AskUser pending for a session, the wrapper sends every entry in ctx.pending_asks, but the legacy pending_asks projection keeps only one value per SID. This action consults only that single value rather than view.presentation.questions; after answering it, answer_for removes the SID entry entirely, leaving the other still-blocking question visible in the Questions report but impossible to open or answer from the TUI. Select from the complete per-session question collection instead.

Useful? React with 👍 / 👎.

Comment thread cc_remote/tui_preview.py Outdated
Comment on lines +92 to +99
request_id = "tui-preview-" + uuid.uuid4().hex
future = asyncio.get_running_loop().create_future()
client.preview_waiters[request_id] = (sid, future)
command = (
AuthorizePreview(
sid=sid,
authorization_id=challenge,
request_id=request_id,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reuse the challenged preview request ID

When a file outside the session directory requires authorization, this function generates a new request_id for the subsequent AuthorizePreview. The wrapper binds each challenge to the original file-read request and explicitly requires challenge.request_id == cmd.request_id in _handle_authorize_preview, so every TUI authorization attempt is returned as expired and external files can never be opened. Retain the request ID from PreviewAuthorizationRequired and send it with the authorization command.

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d72fd98554

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread cc_remote/tui.py
Comment on lines 535 to +538
engine = self.session_engines.get(
sid, self.attached_engine if sid == self.attached_sid else "claude")
if not await self._send_raw(serialize(SwitchSession(
session_id=sid, engine=engine))):
session_id=sid, engine=engine, space=self._session_space(sid)))):

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Resolve a direct session's scope before prewarming it

When the positional session ID belongs to a different engine or space than the startup defaults, recovery immediately sends SwitchSession using attached_engine and _session_space() before any catalog identifies the session. For example, a bare Claude ID now uses the new Codex default, while a Work ID defaults to Code; the wrapper rejects or attempts the wrong backend, and restore_pending prevents the later catalogs from triggering a corrected attach. Delay this prewarm until the ID's catalog row supplies its actual engine and space.

Useful? React with 👍 / 👎.

Comment on lines +88 to +91
if len(matches) == 1:
pending.pop(matches[0])
else:
pending.clear()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve questions when a canonical reply is ambiguous

If two pending async-question messages have identical question metadata, one canonical supplemental reply matches both candidates, so len(matches) > 1 reaches this else and clears every question. The Web projection deliberately leaves ambiguous matches unanswered, and the TUI can therefore hide both still-actionable questions while another client continues offering them; only the uniquely matched candidate should be removed.

Useful? React with 👍 / 👎.

Comment thread cc_remote/tui_app.py
Comment on lines +1714 to +1716
text, starts = reader.parent.project(
text, starts, (sid, row.get("cwd"))
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Invalidate inline image caches after rollback

When rollback restores a previously referenced image path, the session ID and cwd remain unchanged, so this projection identity does not reset TranscriptViewport.cache; the same (block.id, path) key then reuses pixels decoded before the rollback. Although the wrapper emits artifact_invalidated, no artifact epoch reaches this identity, leaving the terminal preview stale until the user switches cwd/session or restarts the TUI. Include an invalidation generation in the image projection identity or explicitly clear the viewport cache on that event.

Useful? React with 👍 / 👎.

Comment thread cc_remote/tui_app.py
Comment on lines +996 to +997
elif ask.get("allow_text"):
answer = text

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reject blank free-text answers before sending

When a blocking question allows free text, submitting whitespace takes this branch and is accepted into the reliable outbox, after which answer_for removes the question locally. The wrapper rejects such answers because _handle_answer_question requires answer.strip() to be nonempty but leaves the question pending, so the TUI can no longer reopen it until a reconnect supplies a fresh ask sync; an actually empty string can instead raise during AnswerQuestion validation. Validate a nonempty trimmed answer before constructing or sending the command.

Useful? React with 👍 / 👎.

Comment thread cc_remote/tui_app.py Outdated
Comment on lines +934 to +935
if not async_questions:
view.attachments.clear()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Retain attachments until the server confirms the message

A successful _send only means the complete command entered the local reliable outbox, but this immediately discards every staged attachment. If the wrapper then rejects the query or queued delivery because ownership/control state changed, the correlated error marks only the text receipt failed and the binary/file contents are no longer present in either the draft or outbox after ACK processing, forcing the user to locate and attach them again. Clear attachments on the authoritative user_msg/turn_steered echo, or restore them on a correlated rejection.

Useful? React with 👍 / 👎.

Comment thread cc_remote/tui_app.py
and self.app_focus
and not self.client.demo
):
self.acknowledged.add(key)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Retry completion receipts when the outbox rejects them

If the reliable outbox is at its command or byte cap when an unread completion is painted, the key is added to acknowledged before _send reports failure. Once earlier commands drain, every later paint still skips this completion, so the server-side unread receipt—and therefore the badge on other clients—remains uncleared for the rest of this TUI process. Record the key only after _send accepts the acknowledgement, or remove it again on failure.

Useful? React with 👍 / 👎.

Comment thread cc_remote/tui_state.py
Comment on lines +591 to +595
remaining = [
b
for b in old
if b.id not in ids and (b.turn not in covered or b.id in fresh)
]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Drop old turns when history continuity changes

When a newest History page arrives with a new revision whose continuity_revision does not preserve the previous epoch, this merge still retains every old block whose turn is outside the four-turn incoming page. A Codex projection-family/source switch can therefore leave phantom turns from the previous source mixed into the new canonical conversation; only an explicitly continuous revision may retain uncovered cached rows, while a discontinuity must replace them.

AGENTS.md reference: AGENTS.md:L166-L167

Useful? React with 👍 / 👎.

Comment thread cc_remote/tui_state.py
Comment on lines +990 to +994
self.catalog = {
sid: row
for sid, row in self.catalog.items()
if sid.startswith("btw-")
or (row.get("engine", "claude"), row.get("space", "code"))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Disable tabs removed by an authoritative catalog

When another client deletes an open session, this authoritative scoped replacement removes its catalog row but leaves the existing SessionView.write_state unchanged. An attached tab that was writable therefore continues accepting prompts for the deleted SID, and only discovers the deletion through a server rejection; retained transcript and draft state can remain visible, but the removed view should be marked unavailable (as the side-chat removal path already does) or the tab should be closed.

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 8c3473bfca

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread cc_remote/tui_app.py Outdated
Comment on lines +1678 to +1679
if reader.parent.identity != (sid, row.get("cwd")):
self.rendered_version = -1

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Compare the complete projection identity

After the first render, TranscriptViewport.identity is the three-element tuple (sid, cwd, artifact_epoch) assigned below, so comparing it with this two-element tuple is always unequal. Because paint() runs every 100 ms, this continually resets rendered_version to -1 and forces the entire transcript, Markdown, and inline-image projection to be rebuilt even when nothing changed, causing persistent CPU usage and severe lag on larger histories. Compare against the same three-element identity used by project().

Useful? React with 👍 / 👎.

Comment thread cc_remote/tui.py
Comment on lines +1191 to +1195
from cc_remote.tui_app import WorkspaceClient, run_workspace
except ModuleNotFoundError as exc:
if exc.name != "textual":
raise
parser.exit(1, "Install requirements-tui.txt, or use --line-mode.\n")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Handle every missing optional TUI module

When a checkout has only the core requirements, or an incomplete optional TUI installation, importing tui_app can fail first on rich, markdown_it, or textual_image; this handler only translates an error whose name is exactly textual. Those expected missing optional modules therefore produce a traceback instead of the advertised Install requirements-tui.txt, or use --line-mode guidance. Handle all modules supplied by the optional TUI dependency set here while continuing to re-raise unrelated import failures.

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a92fb0add6

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread scripts/cc-remote-tui Outdated
#!/usr/bin/env bash
# Resolve the checkout owning this launcher, including when installed by symlink.
set -euo pipefail
launcher=$(readlink -f -- "${BASH_SOURCE[0]}")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Avoid GNU-only readlink in the launcher

On macOS/BSD, readlink does not support GNU's -f, --canonicalize option, so the documented symlink installation exits at this assignment before locating the checkout or starting Python. Use a portable symlink-resolution loop or a Python-based realpath so cc-remote-tui works on supported macOS hosts as well as GNU/Linux.

Useful? React with 👍 / 👎.

Comment thread cc_remote/tui_app.py Outdated
Comment on lines +164 to +165
self.session_catalog_requests[message.engine, message.space] = (
message.cmd_id

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Register catalog fences only after outbox admission

When the reliable outbox is at its command or byte cap, this records a fresh session_catalog_requests ID before super()._send() rejects the unsent ListSessions. A surface switch has already cleared catalog_ready, and subsequent real catalog responses are then discarded because their request IDs do not match the unsent fence; even after ACKs free the outbox, no automatic retry occurs, leaving the surface on “Loading…” until an explicit action sends another catalog read. Commit this fence only after admission succeeds, or restore the previous request ID on failure.

Useful? React with 👍 / 👎.

Comment thread cc_remote/tui_state.py
Comment on lines +1018 to +1021
"engine": row["engine"],
"cwd": parent.get("cwd"),
"state": row.get("state", "idle"),
"space": parent.get("space", "code"),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reconcile side chats after parent catalogs arrive

On a fresh TUI connection that inherits an existing Work side chat, the wrapper deliberately sends the authoritative BtwSync during Hello before the per-engine SessionList replies, so the parent is not yet in self.catalog and this permanently assigns the fork to code with no cwd. Later session-list handling preserves btw-* rows but never updates them from the newly arrived parent, causing the Work fork to disappear from its proper surface and appear under Code with incorrect scope metadata. Reconcile retained side rows whenever their parent catalog row arrives, or carry the parent scope in the BTW catalog.

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b6862e7129

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread cc_remote/tui_app.py
Comment on lines +439 to +440
if ticket and event["request_id"] != ticket[0]:
return

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve goal mutation confirmations

After the automatic GetGoal issued when a session is attached, read_tickets[(sid, "goal_state")] remains populated indefinitely. A later SetGoal, ClearGoal, or DismissGoal emits an authoritative GoalState carrying that mutation's different command ID, so this branch discards the update and leaves the progress strip and Goal panel stale until another explicit refresh or unsolicited goal event occurs. Only fence one-shot GetGoal replies here, and retire the ticket once its matching response is processed.

Useful? React with 👍 / 👎.

Comment thread cc_remote/tui_state.py Outdated
"agent_detail",
"work_dashboard",
"work_artifacts",
"queued_query_detail",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep queued prompts out of persistent reports

Whenever QueueEdit performs its private full-prompt read, the same queued_query_detail is also copied into SessionPresentation.reports, retaining up to 64 KiB of the queued prompt after the editor closes and exposing it later in the general Reports panel. Route this response only to the correlated one-shot queue-detail consumer rather than including it in REPORT_TYPES.

AGENTS.md reference: AGENTS.md:L148-L150

Useful? React with 👍 / 👎.

Comment thread cc_remote/tui_state.py
Comment on lines +1081 to +1082
or (row.get("engine", "claude"), row.get("space", "code"))
!= (engine, event.get("space", "code"))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve sessions from unavailable account profiles

When a multi-profile SessionList reports an error for one Claude or Codex profile, that response intentionally omits the unavailable profile's rows while retaining successful profiles, but this replacement drops every prior row in the engine/space before re-adding the partial result. Those sessions disappear and are marked unavailable; because their existing control revision is retained, even a later same-revision SessionControl can be ignored, leaving restored tabs unwritable. Retain prior rows owned by profiles whose catalog entry carries an error, as normalizeSessionList already does for Web.

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e6714ebcd7

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread cc_remote/tui_app.py
Comment on lines +2382 to +2384
self.client.workspace.view(sid).draft = ""
if self.client.attached_sid == sid and editor.text == text:
editor.load_text("")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Retain queued text until the wrapper accepts it

When a Ctrl+e submission enters the reliable outbox but the wrapper later rejects it because the server queue reached its item/byte limit or the session became non-writable, these lines clear both copies of the draft immediately. A queued submission creates no pending_messages block, and the correlated error path restores only attachments, so the complete prompt is unrecoverable after the rejection; retain a private receipt until the matching query_queue projection or restore the text on the correlated Error.

AGENTS.md reference: AGENTS.md:L142-L150

Useful? React with 👍 / 👎.

Comment thread cc_remote/tui_actions.py Outdated
Comment on lines +179 to +180
if "request_id" in cls.model_fields:
values["request_id"] = command_id

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reuse challenge IDs in the generic preview action

Although the specialized preview flow now reuses its challenged request ID, the remaining authorize_preview entry in the All Actions palette always reaches this code, which hides the original request_id from the form and replaces it with a fresh command ID. After a generic get_file_preview exposes PreviewAuthorizationRequired in Reports, the wrapper's challenge.request_id == cmd.request_id check therefore always returns expired; preserve the reported challenge ID for this command or remove the unsupported generic action.

Useful? React with 👍 / 👎.

Comment thread cc_remote/tui.py Outdated
url,
password,
origin,
args.engine or os.environ.get("ENGINE") or ENGINE,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reject invalid ENGINE values before starting the client

When ENGINE contains a typo such as CODEX or foo and no --engine flag overrides it, this passes the unchecked value into the client even though the previous entry point constrained the environment value. The connection loop later constructs ListSessions(engine=<invalid>); Pydantic rejects it, the broad connection handler retries, and the full-screen workspace remains stuck reconnecting instead of reporting an invalid configuration or selecting a valid fallback.

Useful? React with 👍 / 👎.

Comment on lines +87 to +89
def show_markdown(self, source):
self.source = _safe_remote_text(source[: 256 * 1024])
self.reflow()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Mark locally truncated file previews

When the wrapper returns a valid Markdown preview between 256 KiB and its 512 KiB wire limit with truncated=false, this silently discards everything after the first 256 KiB; reflow() can additionally discard output after 5,000 rendered lines. The preview status consequently still says only “Read-only,” so users inspecting a long file can mistake the locally shortened document for its complete contents; preserve the full bounded response or display an explicit truncation marker and Web handoff.

Useful? React with 👍 / 👎.

Comment thread cc_remote/tui_tree.py Outdated
Comment on lines +283 to +286
await client.execute_action(DeleteSession(
session_id=sid, engine=engine, space=space,
client_id=client.client_id,
))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Close side chats with their dedicated command

When the highlighted tree leaf is a btw-* side conversation, this still sends DeleteSession. A BTW ID is the wrapper's ephemeral routing key rather than a native Claude/Codex catalog ID, so the native deletion handler rejects it and the side conversation remains open despite the tree presenting a valid deletion action; dispatch CloseBtw for these rows or explicitly disable the deletion shortcut for them.

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: cf00cb4eed

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread cc_remote/tui_app.py
and self.app_focus
and not self.client.demo
):
self.acknowledged.add(key)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Retry completion acknowledgements after server rejection

When the wrapper accepts this command into the outbox but later returns an Error—for example, when the completion presentation store is temporarily unavailable—the key remains in acknowledged, while the subsequent command ACK removes the failed command from the reliable outbox. The unread completion_state therefore continues to render but can never trigger another acknowledgement during this TUI process. The new cleanup only covers _send() returning false; correlate the command's server result and discard this key on an authoritative rejection as well.

Useful? React with 👍 / 👎.

Comment thread cc_remote/tui_app.py
Comment on lines +2289 to +2291
if isinstance(self.focused, TextArea):
self.focused.action_paste()
return

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve Ctrl+V paste in secret answer fields

When a blocking AskUser has secret=true, the focused editor is ModalInput, which derives from Input rather than TextArea. The priority global Ctrl+V binding therefore reaches this branch, skips action_paste(), and returns, so Ctrl+V silently does nothing even while the secret field is in Insert mode; modal TextArea fields do receive ordinary paste here. Delegate the paste to editable Input fields as well while keeping desktop-image capture restricted to the main composer.

Useful? React with 👍 / 👎.

Comment thread cc_remote/tui_state.py
if done
else "running",
)
self.presentation.turns[tid] = current

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Bound turn metadata while paging older history

When a long session is paged backward, every historical turn is inserted into presentation.turns, but history() never prunes that mapping even after trim() limits the visible blocks. After more than 160 turns have been loaded, the next live event invokes SessionPresentation.event()'s insertion-order [-160:] trim; because older pages were inserted after the newest page, this can evict the current active turn while retaining old offscreen turns. Subsequent terminal events then cannot find the active TurnDisplay, so elapsed/status headers and the local completion badge can be lost, in addition to metadata growing without a bound until another live event arrives. Prune turn metadata alongside retained blocks while explicitly preserving the active/live turn.

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: da3d10439e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread cc_remote/tui_app.py
Comment on lines +473 to +474
if ticket and event["request_id"] != ticket[0]:
return

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Retire stale status-read fences before mutation refreshes

After the automatic GetStatus issued on attach succeeds, its read_tickets[(sid, "status_report")] entry is never removed. If the user later runs ConsumeRateLimitResetCredit, _handle_consume_rate_limit_reset_credit emits the refreshed StatusReport using the mutation's command ID, so this mismatch check discards that authoritative refresh and leaves the displayed reset-credit and quota state stale, potentially encouraging another redemption attempt. Retire a matched GetStatus ticket after its response, or explicitly accept the mutation-correlated status refresh.

Useful? React with 👍 / 👎.

Comment thread cc_remote/tui_vim.py
Comment on lines +99 to +100
if not pending and visual and char in {"d", "c", "y"}:
self.operate(char, *sorted(self.selection))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve linewise semantics for Visual Line operations

When the editor is in VISUAL LINE mode, pressing d, c, or y takes this generic visual branch and calls operate() without linewise=True. Consequently V followed by y creates a characterwise register, while Vd/Vc leave the selected line boundary behind; a later p inserts text within a line instead of putting whole lines. Pass a full-line range and retain the linewise flag for operations originating from VISUAL LINE.

Useful? React with 👍 / 👎.

Comment thread cc_remote/tui_state.py
remaining -= cost + len(block.text)
if remaining < len(TRUNCATED):
break
view.detail_blocks[turn] = children

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reject turn-detail pages superseded by newer navigation

When detail reads are slow, repeated o navigation can leave duplicate GetTurnDetail reads for one cursor in flight, and the wrapper runs those history commands in independent tasks. After one reply advances the view and the user requests the next older page, a delayed reply for the previous before cursor reaches this unconditional assignment and replaces the newer page, snapping the panel back to stale tool output. Track the expected before cursor or a local request generation per turn and ignore replies superseded by later navigation.

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: afb5be43cb

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread cc_remote/tui_mermaid.py
Comment on lines +99 to +100
option = document.body.split("\n", 1)[0].strip()
if option and option not in {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Inspect the first nonempty line before classifying Mermaid

When an unfenced assistant message starts with a recognized non-flowchart keyword on its own line, document.body begins with \n, so this split produces an empty option and bypasses the prose guard. For example, info\nabout the server is replaced by the Mermaid “info” projection rather than displayed as the original answer; other family names such as timeline and pie have the same issue. Validate the first nonempty body line (and reject incomplete family syntax) before treating ordinary Markdown as a standalone diagram.

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 3ed235f74f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread cc_remote/tui_app.py
Comment on lines +735 to +736
if sid in self._history_requested:
return False

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Retry history refreshes blocked by an in-flight read

If history_invalidated arrives while the initial GetHistory is still represented in _history_requested, this guard rejects the forced refresh after _flush_history_refreshes has already consumed its retry flag. The old response is then rejected by SessionView.pending_revision, but nothing schedules another read, leaving the rollback-cleared transcript blank until the user explicitly reloads or reattaches. Preserve the refresh request until the in-flight read completes, then issue a new revision-aware read.

AGENTS.md reference: AGENTS.md:L166-L167

Useful? React with 👍 / 👎.

Comment thread cc_remote/tui_app.py
Comment on lines +300 to +303
if event.get("type") in {
"tool_use", "tool_result", "tool_delta", "process", "state",
"assistant_msg_start", "assistant_msg_end", "delta", "turn_end",
}:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Prevent user messages from replacing the workspace notice

For an attached session's user_msg or turn_steered event, super()._handle(event) invokes the legacy _line() renderer, which this client maps to self.notice; unlike the other transcript event types listed here, these two types do not restore the prior notice. Consequently an own echo or a message sent from another client replaces statuses such as “Message submitted” with the prompt text in the footer until another action updates it. Suppress these narrative types from the legacy notice path as well.

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 10bc1f0905

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread cc_remote/tui_deletion.py
Comment on lines +117 to +121
if any(profile.get("error") for profile in event.get(
command.engine + "_profiles", []
)):
receipt.confirmed = False
return # An unavailable catalog cannot prove absence.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Scope deletion confirmation to the target profile

When the target belongs to a healthy account but any sibling profile is temporarily unavailable, the successful archive/delete response still includes that sibling's profile error, so this blanket check refuses the catalog proof. The command is then ACKed and may already have deleted the session, but the TUI waits 30 seconds, retains the tab, and—during the Codex archive prerequisite—never sends the deletion at all. Capture the target row's profile identity when starting the receipt and distrust only an error for that profile.

Useful? React with 👍 / 👎.

Comment thread cc_remote/tui_app.py
Comment on lines +384 to +388
if expected_profile is None:
expected_profile = self.workspace.reports.get(
event["engine"] + "_profiles", {}
).get("default_" + profile)
if expected_profile != event.get(profile):

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Retain model replies until the default profile is known

If a new-session or settings form is opened before the first SessionList arrives, the pending GetModels has no explicit profile and workspace.reports has no default profile yet. The wrapper resolves that request to a concrete profile ID, so this comparison drops the response; when the later catalog supplies the default, nothing reprocesses the discarded models reply, leaving model and effort choices unavailable until the user closes and reopens the form. Cache the unmatched reply or reconcile it when default-profile metadata arrives.

Useful? React with 👍 / 👎.

Comment thread cc_remote/tui_state.py
Comment on lines +656 to +658
self.blocks = (
incoming + remaining
if event.get("before")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reject superseded history pagination replies

When an older-page History frame is received but its command ACK is lost, reconnect recovery safely reruns that request from the reliable outbox. If the user has meanwhile requested the next older cursor, the two history reads run independently, and a delayed duplicate of the previous same-revision page reaches this unconditional prepend after the newer page; it moves the middle turns ahead of older turns and resets oldest/has_more, causing subsequent paging to repeat or skip the real boundary. Track the expected before cursor or a navigation generation and ignore superseded pages.

AGENTS.md reference: AGENTS.md:L158-L167

Useful? React with 👍 / 👎.

Comment thread cc_remote/tui_state.py
else {}
),
),
append=True,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Replace streamed tool output at completion

When Codex sends item/commandExecution/outputDelta events and later finishes with a ToolResult containing the aggregated output, this append=True path concatenates the authoritative result after the already-streamed bytes. Expanding the tool therefore displays command output twice; streamed diffs are similarly followed by the complete final diff, and the duplication can consume the 64 KiB block limit and hide the actual tail. Keep the streamed fields separate and replace them with the authoritative result at completion instead of appending it.

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9b20039158

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread cc_remote/tui_state.py
Comment on lines +742 to +745
elif kind == "error" and event.get("msg_id") in self.pending_messages:
pending = self.pending_messages.pop(event["msg_id"])
pending.data["status"] = "failed"
self.put(pending)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Correlate failed sends by command ID

When a session is deleted or evicted after _handle_query() resolves its context but before it acquires query_lock, _missing_session_error() returns an Error containing the command's request_id but no msg_id. This branch therefore never removes the corresponding optimistic user block, leaving the rejected prompt permanently displayed as “awaiting confirmation” even after the error and command ACK arrive. Store the command ID with pending_messages and accept either correlation field, as the attachment and queued-message receipt paths already do.

Useful? React with 👍 / 👎.

Comment thread cc_remote/tui_panels.py
Comment on lines +463 to +466
for index, option in enumerate(self.ask.get("options", []), 1):
labels.append(
f"{index}. {option['label']}\n {option.get('ds', '')}"
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Sanitize question choices before rendering

When a model supplies an AskUser option label or description containing C0/C1, ANSI, or bidi control characters, this interpolation places the untrusted text directly into the Textual reader without passing through _safe_remote_text; the protocol only length-bounds these fields. Unlike the question text and the async-question picker, such choices can therefore inject terminal control sequences or visually reorder the confirmation UI. Sanitize both fields before constructing the panel text.

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6cdbbbf1be

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread cc_remote/tui_app.py
Comment on lines +300 to +303
if event.get("type") in {
"tool_use", "tool_result", "tool_delta", "process", "state",
"assistant_msg_start", "assistant_msg_end", "delta", "turn_end",
}:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep background context reports out of the focused notice

When a nonfocused resident session emits a context_report—for example, from its automatic post-turn context probe—super()._handle(event) unconditionally calls the legacy _render_context() and replaces self.notice. Because this restoration list omits context_report, the focused workspace footer then shows the background session's token count and overwrites statuses such as submission confirmations; preserve the prior notice for nonfocused context reports as is already done for background errors and transcript events.

Useful? React with 👍 / 👎.

Comment thread cc_remote/tui_settings.py
if all(
word in label.casefold() for word in text.casefold().split()
):
listing.add_option(Option(Text(label), id=str(index)))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Sanitize remote settings choices before rendering

In the full-screen model and permission-profile pickers, label can contain a server-provided model display name or permission-profile description, but it is rendered directly without _safe_remote_text. These protocol fields are length-bounded but may still contain ANSI, C0/C1, or bidi controls, allowing a catalog entry to visually spoof or reorder a permission choice in the terminal; sanitize the complete label before constructing Text.

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 27dd7cc2cd

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread cc_remote/tui_actions.py
p.ClearGoal,
p.DismissGoal,
p.AcknowledgeCompletion,
p.GetQueuedQuery,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Route generic queued-query reads to a consumer

GetQueuedQuery remains available through the generic Actions palette, but that form has no consumer for its response: _on_event() stores the full prompt in queue_details and returns, while only a QueueEdit instance polling its own request ID ever removes or displays it. Selecting this generic action therefore reports “Submitted” without showing the result and retains the private prompt until unrelated reads evict it. Remove this command from the generic inventory or open the dedicated one-shot queue editor for it.

AGENTS.md reference: AGENTS.md:L148-L150

Useful? React with 👍 / 👎.

Comment thread cc_remote/tui_deletion.py
))
stage = "Deletion"
self.client.notice = "Deleting; waiting for server confirmation…"
await self.wait_for(command)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep tracking timed-out deletion commands

If the wrapper remains offline for more than 30 seconds after a deletion is admitted to the reliable outbox, this await times out and the receipt/task is discarded, but the deletion frame remains in _outbox and is replayed when the wrapper reconnects. The permanent deletion can therefore complete after the TUI stopped tracking it; the later catalog refresh removes the row, but the local buffer remains attached and no success confirmation is shown. Preserve an observer for admitted deletion commands after the UI timeout so their eventual authoritative result can finish local cleanup.

Useful? React with 👍 / 👎.

Comment thread cc_remote/tui_state.py
Comment on lines +857 to +859
+ json.dumps(
event.get("input", {}), ensure_ascii=False, indent=2
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Render the bounded tool input instead of the original payload

When a live tool_use carries a large input, the code first creates the bounded inputs value and even discards it when its serialized display exceeds 64 KiB, but then formats event["input"] again for the block text. A multi-megabyte Write/Edit/tool payload is therefore fully pretty-printed on the Textual event loop before put() truncates it, causing avoidable allocations and UI stalls while also defeating the preceding display-size check. Serialize the already-bounded inputs value here.

Useful? React with 👍 / 👎.

Comment thread cc_remote/tui_app.py
Comment on lines +1807 to +1810
self.update_chrome(
"#attachments",
"Attachments: " + ", ".join(a["name"] for a in view.attachments)
if view.attachments

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Sanitize attachment names before rendering status chrome

When the user attaches a file whose basename contains bidi, C0/C1, or ANSI control characters—for example, a crafted filename from an untrusted checkout—this joins the raw name directly into a Text object via update_chrome(). Unlike transcript filenames, the attachment strip never passes through _safe_remote_text, so the filename can visually reorder or spoof the persistent terminal chrome. Sanitize each displayed attachment name before constructing this string.

Useful? React with 👍 / 👎.

Comment thread cc_remote/tui_panels.py
Comment on lines +493 to +495
editor = self.query_one("#answer")
value = editor.value if isinstance(editor, Input) else editor.text
if await self.client.answer_for(self.ask, value):

ghost Sep 16, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reject oversized free-text answers inside the dialog

When a blocking question permits free text and the user enters more than the protocol's 4,096-character answer limit, answer_for() constructs AnswerQuestion and raises a Pydantic validation error. This dialog awaits it without validation or exception handling, so the Textual action fails instead of updating #answer-result and retaining a usable answer flow; secret-answer fields are affected as well. Validate the limit before sending or catch ValueError here and display it without dismissing the dialog.

Useful? React with 👍 / 👎.

Troy Mitchell added 4 commits September 16, 2026 14:46
- Attach command IDs to missing, busy and failed Work deletions.
- Preserve authoritative rejection reporting for every control client.
- Exercise the native atomic registry transaction during concurrent reads.
- Assert denial before catalog propagation and expiration afterwards.
- Reuse device-scoped transport and bounded reliable command delivery.
- Discover same-user local relay configuration without exporting secrets.
- Validate engine selection and support portable checkout-based launchers.
- Project bounded history, live activity, goals, questions and control state.
- Preserve authoritative profile catalogs, scoped results and queue receipts.
- Keep private queue payloads in one-shot consumers, outside general reports.

ghost left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b046829736

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread cc_remote/tui.py
Comment on lines +341 to +343
socket_url = self.url + "?" + urlencode(
{"machine": self.machine_id}
)

ghost Sep 16, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve automatic device selection when machine is omitted

When the account has only a named/enrolled wrapper, launching either the default workspace or --line-mode without --machine now always requests machine=default. This bypasses the relay's no-query auto-selection path, so restricted accounts are closed as unauthorized and wildcard accounts connect to an empty default route instead of their available wrapper; the previous line client omitted this query and worked in that setup. Keep an omitted CLI value distinct and append the machine query only when the user explicitly selects one.

AGENTS.md reference: AGENTS.md:L102-L106

Useful? React with 👍 / 👎.

Comment thread cc_remote/tui_app.py
Comment on lines +1085 to +1092
if picks and all(p.isdigit() for p in picks) and options:
indices = [int(p) - 1 for p in picks]
if (
any(i < 0 or i >= len(options) for i in indices)
or len(set(indices)) != len(indices)
or (len(indices) > 1 and not ask.get("multi_select"))
):
self.notice = "Invalid option selection"

ghost Sep 16, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Allow numeric free-text question answers

When an AskUser prompt has both options and allow_text=true, any all-numeric response is unconditionally interpreted as an option index. A valid custom answer such as 2026 or 0 therefore produces “Invalid option selection” instead of being sent as free text, and the dialog provides no escape syntax to force the intended interpretation. Fall back to the allowed text path when the numeric input is not a valid selection, or provide an explicit choice/text mode.

Useful? React with 👍 / 👎.

+ self.client.keys.layer_label("picker", "open")
+ " browses", markup=False
)
yield Label(self.path, id="directory-path", markup=False)

ghost Sep 16, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Sanitize the initial directory heading

When the directory picker is opened for a session whose cwd contains C0/C1, ANSI, or bidi controls—possible with a crafted Unix checkout name—the initial path is rendered directly before the asynchronous wrapper listing returns. Later refreshes sanitize the path, but this first frame can still inject or visually reorder terminal content; pass the initial value through _safe_remote_text as the update path already does.

Useful? React with 👍 / 👎.

Troy Mitchell added 7 commits September 16, 2026 15:40
- Share operators, text objects, inclusive Visual selection and jump motions.
- Keep reading and editing cursors independent across modal key layers.
- Index configurable shortcuts without overriding Vim text commands.
- Expose protocol-backed models, permissions, goals and work controls.
- Keep modal search immediate and directory discovery live and bounded.
- Correlate private reads, queue edits and explicit authorization results.
- Add discoverable queue editing, cancellation and atomic server ordering.
- Require coordinated protocol v67 deployment for queue order commands.
- Browse engine/workspace scopes with searchable folders and cached tabs.
- Persist the open working set and group archived sessions separately.
- Confirm native deletion and close ephemeral side chats through CloseBtw.
- Render Markdown with visible Unicode URLs and stable source coordinates.
- Place cached images inline and support full-resolution pan and zoom.
- Scope external-file grants and expose local preview truncation clearly.
- Connect session navigation, settings, transcript rendering and drafts.
- Follow only at the viewport bottom and preserve anchors during reflow.
- Integrate shared queues, questions, confirmations and a configurable stop.
- Cover interaction, replay, background events and control-link regressions.
- Provide English and Chinese keyboard, setup and shared-control guides.
- Document shortcut configuration, preview boundaries and scroll debugging.
- Project diagram families as bounded, selectable terminal text.
- Lay out sequence lifelines, messages and fragments in terminal cells.
- Retain unprojected syntax explicitly instead of hiding information.
- Offer configurable Web handoff for graphics and interaction.
- Keep diagram source and credentials out of browser URLs.
- Cover Unicode, bounds, family examples and mounted shortcut dispatch.

ghost left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c25d03f7d0

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

title = {"thinking": "Thinking", "final": "Answering"}.get(
e.get("channel"), "Processing"
)
turn.activity = str(e.get("title") or e.get("tool") or title)

ghost Sep 16, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Sanitize activity labels before transcript headers

When a tool or process event supplies a title containing ANSI, C0/C1, or bidi controls, this stores the untrusted value verbatim in TurnDisplay.activity. SessionView.block_header() later interpolates turn.label() into the transcript header, which lies outside the assistant body sanitized by the Markdown projection, so a model or MCP tool can spoof or visually reorder transcript chrome. Pass the title/tool value through _safe_remote_text before retaining or rendering it.

Useful? React with 👍 / 👎.

Comment thread cc_remote/tui_app.py
Comment on lines +733 to +735
if not accepted:
self.attached_sid, self.attached_engine = previous, previous_engine
self.engine, self.space = previous_scope

ghost Sep 16, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Restore the previous tab when a switch is rejected

When a catalog entry disappears, changes Code/Work ownership, or otherwise fails to resume between listing and selection, _send() still returns true because it only admits the reliable command to the outbox. The wrapper later emits an Error, but this rollback runs only for local send failure, leaving attached_sid on the rejected target even though the wrapper remains focused on the previous session; the workspace then shows an unusable stale tab until the user manually switches away. Track the pending switch and restore the prior focus when the latest target is authoritatively rejected.

AGENTS.md reference: AGENTS.md:L127-L131

Useful? React with 👍 / 👎.

Comment thread cc_remote/tui_settings.py
Comment on lines +207 to +211
if self.pending_change and event in settings and settings[event] is not previous:
self.query_one("#form-result", Static).update(
"Effective setting confirmed by the server."
)
self.pending_change = None

ghost Sep 16, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Confirm the requested setting value before clearing pending state

When another client changes the same setting type while this form's command is pending, the new settings dictionary is always a different object from previous, even if its value differs from the value requested here. This branch therefore reports the local change as confirmed and clears pending_change; if the local command is then rejected, its action_results error is no longer inspected and the user sees a false success. Compare the authoritative field value with the requested value (including the fast/default mapping) before retiring the pending ticket.

Useful? React with 👍 / 👎.

@TroyMitchell911
TroyMitchell911 merged commit 849ff1c into muggle-stack:master Sep 16, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant