Skip to content

feat(tui): show background tasks in the tasks pane - #512

Merged
emal-avala merged 15 commits into
mainfrom
feat/tasks-pane
Jul 27, 2026
Merged

feat(tui): show background tasks in the tasks pane#512
emal-avala merged 15 commits into
mainfrom
feat/tasks-pane

Conversation

@emal-avala

@emal-avala emal-avala commented Jul 26, 2026

Copy link
Copy Markdown
Member

Update: drill-in added (D4-27, partial)

The pane listed work but offered no way into it — seeing that a build was running told you nothing about what it printed.

/ move the selection while the pane is open and the composer is empty; Enter opens the selected task's output as a tool card in the transcript. The keys are gated exactly like the queue pane's and yield to it when both are open, so nothing is double-bound. The read happens in the run loop, not on the key path, because read_output is async and touches the filesystem.

Only TaskManager rows can be drilled into, and this is a real limit rather than an oversight: a subagent row's id comes from resolve_subagent_id(input) — a subagent type, not a task id — so there is no output to read for one. The pane says so instead of opening an empty card (drilling_into_a_subagent_row_explains_there_is_no_output). Closing that properly needs the engine to capture per-subagent output, which is more than a pane change.

So D4-27 is partially closed: drill-in exists for every row that has something to drill into.

Added tests: selection wrap/clamp including the empty-pane case, drill-in requesting the right id, the subagent refusal, output landing in the transcript, and an unreadable task reporting the error rather than looking empty.

578 bin tests pass; clippy and fmt clean.

The pane was fed only by EngineEvent::SubagentUpdate, so a `&`-prefixed
shell job, a workflow or an MCP monitor never appeared in it — the only
way to see one was to type `/tasks list`. The pane claimed to be the
place where running work is visible while showing a subset of it.

Rows now carry a source and the pane groups them under "agents" and
"background" headings. Subagent rows keep arriving as events; background
rows are polled from the shared TaskManager, which emits none.

Two things that matter more than the feature:

The poll is gated on `live || !app.tasks.is_empty()`, matching how the
animation tick is gated, so an idle session still never wakes.

The sync returns whether anything actually changed and only marks the
frame dirty then. Without that, a finished task sitting in the list would
repaint the screen every 750ms forever and the zero-frame idle property
would be quietly gone.

Background rows are rebuilt wholesale rather than upserted, because the
manager owns them: a task that disappears there has to disappear here.

@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: 086682b218

ℹ️ 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 crates/cli/src/ui/modern/run.rs Outdated
Comment on lines +776 to +777
.into_iter()
.map(|t| {

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 Reconcile LocalAgent records instead of duplicating them

When an Agent call uses run_in_background, the query loop already emits a SubagentUpdate row keyed by subagent_id, while spawn_background_agent also registers the same run as a TaskKind::LocalAgent with a separate task ID. Mapping every manager record here therefore adds a second row for the same agent; moreover, the event-driven row remains working after the tool reports that it started in the background, while only the manager row eventually becomes done, leaving the pane indefinitely showing the agent as both working and finished. Filter or reconcile LocalAgent records rather than treating them as independent background work.

Useful? React with 👍 / 👎.

Comment thread crates/cli/src/ui/modern/run.rs Outdated
app.flush_stream();
}
// Background-task rows (`&` shell jobs, workflows, monitors).
_ = tasks_tick.tick(), if live || !app.tasks.is_empty() => {

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 Keep discovery active until manager tasks are synchronized

If the first manager task is created near the end of a turn—or the turn is cancelled before the next 750 ms tick—app.tasks can still be empty when live becomes false. This guard then disables the only TaskManager poll indefinitely, and because the manager emits no event, the task never appears until another turn happens. A final sync on turn completion or a manager-driven wakeup is needed so short or late-starting background jobs cannot be missed.

Useful? React with 👍 / 👎.

Comment on lines +642 to +646
if last_source != Some(t.source) {
if last_source.is_some() {
lines.push(Line::from(""));
}
lines.push(Line::from(Span::styled(

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 Allocate rows for the source headers

On terminals narrower than 110 columns, render.rs:68-72 allocates only a five-row task strip. With one subagent and one background task, these new per-source headings plus the separator and two lines per task require seven rows; because agents sort first, the background task's status and headline are clipped even though this change is intended to expose it. The strip must grow, scroll, or render a more compact grouped layout.

Useful? React with 👍 / 👎.

The tasks pane listed work but offered no way into it — seeing that a
build was running told you nothing about what it printed.

Arrow keys move the selection while the pane is open and the composer is
empty; Enter opens the selected task's output as a tool card in the
transcript. The keys are gated the same way the queue pane gates its
own, and yield to it when both are open, so nothing is double-bound.

The read happens in the run loop rather than on the key path, because
`read_output` is async and hits the filesystem.

Only TaskManager rows can be drilled into. A subagent row's id is a
subagent *type* rather than a task id, so there is no output to read for
one; the pane says that instead of opening an empty card. Closing that
gap needs the engine to capture per-subagent output, which is a larger
change than a pane can make.
@mintlify

mintlify Bot commented Jul 26, 2026

Copy link
Copy Markdown

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated (UTC)
agentcode 🟡 Building Jul 26, 2026, 4:19 AM

💡 Tip: Enable Workflows to automatically generate PRs for you.

… the pane

- fold TaskManager LocalAgent records into their event-driven subagent
  row via a new payload subagent_id instead of listing the agent twice;
  folded rows keep the manager task id so drill-in still opens output
- sync the manager once when a turn ends so a task registered moments
  before completion is not hidden behind the idle poll guard
- size the narrow-terminal strip to the grouped list (it clipped the
  background group) and report hidden rows instead of silent truncation
- scrub deceptive characters from pane headlines like other surfaces
@emal-avala

Copy link
Copy Markdown
Member Author

@codex review

Addressed all three findings on c649443:

  • P1 duplicate rows: LocalAgent records now carry the stream's subagent_id in their payload and fold into the event-driven row (manager state wins, since the stream stops at "working" for background runs). Folded rows keep the manager task id so the new drill-in still opens their output.
  • P1 poll starvation: the manager is synced once when a turn is reaped, so a task registered just before turn end (or a cancelled turn) appears even though the idle guard stops the periodic poll.
  • P2 strip rows: the narrow-terminal strip is sized to the grouped list (headings + 2 rows per task + title row), capped at half the transcript area, with an explicit "+n more" line when it still cannot fit.

Also routed pane headlines through the deceptive-character scrub (escape_deceptive), matching the transcript and permission surfaces.

@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: c649443c89

ℹ️ 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 crates/cli/src/ui/modern/run.rs Outdated
Comment on lines +1197 to +1200
(_, KeyCode::Enter)
if app.tasks_visible() && !app.show_queue_pane && app.input.is_empty() =>
{
app.drill_into_selected_task();

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+Enter when the tasks pane is visible

When the tasks pane is visible and the composer is empty, this modifier-agnostic arm also captures Ctrl+Enter before the later app.interject() arm. During a live turn with a queued prompt, Ctrl+Enter therefore opens task output instead of cancelling and sending the queued prompt as documented; restrict this arm to unmodified Enter so the global send-now binding remains reachable.

Useful? React with 👍 / 👎.

task_id: Some(row.id),
});
}
next.sort_by_key(|t| (t.source.heading(), t.state.order()));

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 the selection attached to the selected task

When a polled task changes state, this sort can move rows while tasks_selected remains a positional index. For example, selecting the second working task and then having the first task finish can make the marker—and the subsequent Enter drill-in—silently target a different task; preserve the selected task ID across reconciliation and derive its new index afterward.

Useful? React with 👍 / 👎.

Comment thread crates/cli/src/ui/modern/run.rs Outdated
// Drill-in: fetch the selected task's output off the UI path.
_ = std::future::ready(()), if app.pending_task_output.is_some() => {
if let Some(id) = app.pending_task_output.take() {
let out = task_manager.read_output(&id).await;

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 Move task-output reads off the TUI event loop

When a background task has a large output file or the cache filesystem is slow, awaiting this call blocks the sole TUI loop because TaskManager::read_output internally uses synchronous std::fs::read_to_string; terminal input, cancellation, and engine events cannot be handled until the entire file is read, after which the unbounded text is also inserted into the transcript. Run the read on a blocking worker or use async I/O, and bound or tail the displayed output.

AGENTS.md reference: AGENTS.md:L120-L125

Useful? React with 👍 / 👎.

Comment thread crates/cli/src/ui/modern/render.rs Outdated
let cut = max_rows - 1;
let visible = task_ends.iter().filter(|&&e| e < cut).count();
let hidden = app.tasks.len().saturating_sub(visible);
lines.truncate(cut);

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 overflowed task selections visible

When the pane cannot fit all tasks, this always truncates to the first rows even though Up/Down continues cycling tasks_selected through every task. Once the selection reaches a hidden task, the marker disappears and Enter opens output for a row the user cannot see; window or scroll the rendered rows around the selection rather than rendering a fixed prefix.

Useful? React with 👍 / 👎.

…ping

- read task output via tokio::fs off the event loop (spawned task +
  channel) instead of a sync read under the manager lock, and tail long
  output to the last 200 lines in the transcript card
- keep the pane selection attached to its task across re-sorts instead
  of the positional index silently retargeting Enter
- claim only unmodified Up/Down/Enter for the pane so Ctrl+Enter
  interject and Alt+Enter newline stay reachable
- window overflowed pane rows around the selection so the marked task
  is always visible
@emal-avala

Copy link
Copy Markdown
Member Author

@codex review

On 6762edf, addressing the four new findings:

  • P1 blocking read: TaskManager::read_output now clones the path out of the lock and uses tokio::fs; the TUI additionally runs the read in a spawned task and receives the result over a channel, so the event loop never awaits the filesystem. Long output is tailed to the last 200 lines in the transcript card.
  • P2 Ctrl+Enter: the pane's Up/Down/Enter arms now require no modifiers, so Ctrl+Enter interject and Alt+Enter newline fall through as documented.
  • P2 selection drift: the selection is re-derived from the selected task's identity (id + source) after every reconcile/re-sort, on both the poll path and the SubagentUpdate path, clamping only when the task disappeared.
  • P2 overflow selection: the pane windows its rows around the selection with a "+n more" line, so the marked task is always on screen.

Two findings from this round appear to be re-posts of items already fixed in c649443, unchanged in 6762edf — please re-check against the current code:

  • "Keep discovery active": the turn-reap block performs a final manager sync (run.rs, after handle.join), covering tasks registered just before completion or cancellation.
  • "Allocate rows for the source headers": the strip is no longer fixed at five rows; it is sized from tasks::pane_rows (headings + two rows per task + title row), capped at half the transcript area, with the overflow indicator handling the remainder.

@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: 6762edf92e

ℹ️ 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 crates/cli/src/ui/modern/app.rs Outdated
Comment on lines +2031 to +2033
if let Some(existing) = next.iter_mut().find(|t| t.agent_id == sid) {
existing.state = state;
existing.task_id = Some(row.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 Preserve tasks that share a subagent ID

When two background Agent calls have the same description, resolve_subagent_id gives them the same ID because the tool schema does not expose subagent_id and the fallback is the description's first 32 characters. This lookup therefore folds both distinct manager records into one entry, repeatedly overwriting its state and task_id; only one task appears and Enter opens whichever output was processed last, with TaskManager's hash-map iteration making that choice unstable. Reconcile only one matching manager record with an event row and retain additional task IDs as separate rows.

Useful? React with 👍 / 👎.

Comment thread crates/cli/src/ui/modern/render.rs Outdated
Comment on lines +708 to +713
if lines.len() > max_rows && max_rows >= 2 {
let visible_h = max_rows - 1;
let sel_end = task_ends.get(app.tasks_selected).copied().unwrap_or(0);
let offset = sel_end
.saturating_sub(visible_h - 1)
.min(lines.len() - visible_h);

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 the marker visible in one-row task windows

On a short terminal where the task block's inner height is two rows, reserving one row for the overflow message leaves visible_h == 1; anchoring the slice to task_ends then renders only the selected task's headline and omits its status row containing the marker. With an inner height below two, this branch is skipped and the paragraph clips from the list's beginning, which can hide the selected task entirely. The overflow window needs to prioritize the selected marker row when fewer than two task-content rows fit.

Useful? React with 👍 / 👎.

Comment on lines +798 to +801
// Background-task rows (`&` shell jobs, workflows, monitors).
_ = tasks_tick.tick(), if live || !app.tasks.is_empty() => {
let rows = manager_rows(&task_manager).await;
app.sync_background_tasks(rows);

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 Stop polling after only persistent UI rows remain

After any event-driven subagent row is added, app.tasks never becomes empty because those rows are intentionally retained by reconciliation, so this guard keeps the 750 ms timer active for the remainder of the session even when the TaskManager has no running work. Every otherwise-idle wake snapshots, allocates, sorts, and compares the manager rows, defeating the event loop's documented zero-wakeup idle behavior; gate polling on manager work that can still change, rather than the combined pane contents.

Useful? React with 👍 / 👎.

… marker

- a second LocalAgent record sharing a subagent id keeps its own row
  (manager rows are id-sorted, so the oldest task claims the event row)
- gate the 750 ms poll on manager work that can still change, restoring
  zero idle wakeups once persistent subagent rows exist; seed the pane
  once at startup so adopted tasks show before the first turn
- prioritize the selection's marker row when the overflow window is one
  or two rows tall
@emal-avala

Copy link
Copy Markdown
Member Author

@codex review

On fcef7a9, addressing this round's three new findings:

  • shared subagent ids: reconciliation now claims at most one manager record per event row; extra records sharing the id keep their own rows keyed by task id. manager_rows are sorted by task id, so which record folds is deterministic (oldest wins) and row order no longer follows hash-map iteration.
  • poll gating: the 750 ms tick is now gated on live turns or manager-backed rows still in a working state, not on the pane being non-empty, restoring zero idle wakeups once persistent subagent rows exist. The pane is seeded once at startup so adopted tasks appear before the first turn.
  • tiny windows: the overflow window anchors to the selection's status/marker row when fewer than two content rows fit, and a one-row pane renders exactly the marker row.

Restating from the previous round — these three re-posted findings were already fixed and remain fixed at this head; please verify against current code rather than the pre-fix context:

  • "Keep discovery active": final manager sync runs in the turn-reap block (run.rs, after handle.join()).
  • "Allocate rows for the source headers": the strip is sized from tasks::pane_rows, not a fixed five rows (render.rs layout).
  • "Keep the selection attached": selection is re-derived by task identity after every reconcile and SubagentUpdate (app.rs keep_task_selection).

@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: fcef7a9d44

ℹ️ 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 on lines +2019 to +2020
.filter(|t| t.source == TaskSource::Subagent)
.cloned()

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 Exclude synthesized rows before rebuilding the manager snapshot

When two live Agent calls share a subagent ID, the first sync creates an extra manager-backed row whose source is also Subagent; on the next 750 ms sync this filter copies that synthesized row, but reconciliation cannot claim it by the shared ID and appends the same task again. The modern TUI therefore grows by one duplicate on every poll while the extra task remains working. Preserve only event-owned rows here, or explicitly reuse existing rows by task_id when rebuilding.

Useful? React with 👍 / 👎.

Comment thread crates/cli/src/ui/modern/run.rs Outdated
// The manager's map iterates in arbitrary order; sort by task id so
// reconciliation (which record folds into an event row) and row
// order are stable across polls.
rows.sort_by(|a, b| a.id.cmp(&b.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 Compare the numeric task sequence when sorting manager rows

TaskManager::allocate_id emits unpadded IDs such as a9 and a10, so this lexical comparison places a10 before a9. For same-subagent records crossing a digit boundary, the newer task consequently claims the event row despite the stated oldest-wins behavior, and pane order is not chronological. Sort by the parsed numeric suffix, with a deterministic fallback for legacy IDs.

Useful? React with 👍 / 👎.

Comment on lines +805 to +807
_ = tasks_tick.tick(), if live || app.has_live_manager_tasks() => {
let rows = manager_rows(&task_manager).await;
app.sync_background_tasks(rows);

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 Resync the pane after /tasks clear

In the modern TUI, after a killed task has been polled into its terminal state, /tasks clear removes it from TaskManager while this guard remains false because no manager-backed row is working. The slash-command bridge does not perform its own pane sync, so the removed task remains visible indefinitely and drilling into it reports “Task not found” until another turn happens to trigger a sync. Manager-mutating slash commands need an explicit refresh without reintroducing an idle timer.

Useful? React with 👍 / 👎.

…d resync

- re-claim manager-backed rows by their task id so identical polls
  converge instead of appending a duplicate per tick; rows whose record
  vanished (/tasks clear) are dropped, pure event rows stay
- sort manager rows by the id's numeric sequence (a9 before a10) so the
  oldest-wins fold and pane order survive digit boundaries
- resync the pane after every slash command so /tasks kill and clear
  reflect immediately even while the gated poll is parked
@emal-avala

Copy link
Copy Markdown
Member Author

@codex review

On 0e28204, addressing this round's three new findings:

  • P1 synthesized-row growth: reconciliation now re-claims a row by its owning task id before falling back to an unclaimed event row, so identical polls converge; manager-backed rows whose record vanished are dropped (which also makes /tasks clear reflect in the pane), pure event rows persist.
  • P2 id ordering: manager rows sort by the id's parsed numeric sequence with a lexical tiebreak, so a9 precedes a10 and the oldest-wins fold holds across digit boundaries.
  • P2 /tasks clear staleness: the slash-command bridge now resyncs the pane after every command, covering kills/clears performed while the gated poll is parked.

Re-posted findings already fixed at this head (unchanged code, verified): turn-reap final sync (run.rs), pane strip sized from tasks::pane_rows (render.rs), selection re-derived by task identity after every reconcile (app.rs), and the poll gated on has_live_manager_tasks rather than pane emptiness (run.rs). Please evaluate against the current head.

@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: 0e28204e26

ℹ️ 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 on lines +1249 to +1253
(m, KeyCode::Enter)
if m.is_empty()
&& app.tasks_visible()
&& !app.show_queue_pane
&& app.input.is_empty() =>

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 Let Enter dispatch retained prompts before task drill-in

When a turn aborts or errors, the loop retains queued prompts and explicitly tells the user to press Enter to send them (lines 670–673). If any task row is visible—which is the default once tasks exist—and the queue pane is closed, this earlier arm captures that Enter and only opens task output, so the retained prompt is never dispatched. Prioritize a nonempty queue while idle or exclude that state from the drill-in guard.

Useful? React with 👍 / 👎.

@emal-avala

Copy link
Copy Markdown
Member Author

@codex review

On e266dee, addressing the one new finding: the pane's Enter arm now also requires an empty queue, so "queued prompts kept — press Enter to send" dispatches the retained prompt as promised; drill-in claims Enter only when nothing is waiting. Regression test added (queued_prompts_take_enter_before_task_drill_in).

The other five findings on 0e28204 were re-posts of items already fixed in the reviewed code — each is covered by an existing regression test at this head: turn-reap final sync; strip sized from tasks::pane_rows (narrow_terminal_strip_shows_the_background_group); selection identity across re-sorts (selection_follows_its_task_across_a_resort); poll gated on has_live_manager_tasks (poll_gate_tracks_live_manager_work_only); shared-id convergence via claim-by-task-id (tasks_sharing_a_subagent_id_stay_distinct asserts row count stays 2 across repeated identical polls); slash-bridge pane resync after every command (run.rs, after commands::execute). Please verify against the current head.

@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: e266dee0b6

ℹ️ 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 crates/cli/src/ui/modern/run.rs Outdated
let tm = task_manager.clone();
let tx = task_out_tx.clone();
tokio::spawn(async move {
let out = tm.read_output(&id).await;

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 Tail task output before loading it into memory

When a selected task has produced a very large output file—or is still rapidly appending to one—this drill-in path calls read_output, which uses read_to_string to allocate the entire file before show_task_output discards all but 200 lines. Moving the read to a spawned task keeps input responsive but does not prevent memory exhaustion and can still terminate the TUI; read a bounded tail from the file instead of materializing the complete output.

Useful? React with 👍 / 👎.

@emal-avala

Copy link
Copy Markdown
Member Author

@codex review

On a3fadab, addressing the one new finding: added TaskManager::read_output_tail (seeks to the last N bytes, notes how many were skipped, never materializes the full file) and the drill-in path now reads a 256 KiB tail before the 200-line display trim. Test: read_output_tail_skips_the_head_of_large_output.

The six other findings on e266dee are re-posts of items already fixed in the code under review, each with a regression test at this head (listed in the previous comment; the Enter/queue one is queued_prompts_take_enter_before_task_drill_in). Please evaluate against the current head — a thumbs-up or a finding on genuinely current code both work.

@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: a3fadab411

ℹ️ 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 on lines +2160 to +2163
match &row.task_id {
Some(id) => {
self.pending_task_output = Some(id.clone());
self.status_message = format!("loading output for {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 Avoid opening unwritten output for running tasks

When the selected task is still Working, this branch requests its output unconditionally, but run_command_task does not create or overwrite the .out file until finalize_shell_task runs after process exit (background.rs:897,915). Drilling into a newly running task therefore produces a read-error card instead of the intended “no output yet”; worse, because task IDs and the shared cache paths are reused across non-persistent process starts, an existing b1.out can display stale output from a previous run. Initialize/truncate the output file when registering the task, or prevent drill-in until output belongs to the current run.

Useful? React with 👍 / 👎.

Ids and cache paths recur across process starts, so a drill-in on a
freshly running task could read ENOENT or a previous run's leftovers.
Creating the file empty at registration makes both read as 'no output
yet'.
@emal-avala

Copy link
Copy Markdown
Member Author

@codex review

On fa9afc9, addressing the one new finding: both registration paths (spawn_command and register/register_with_color) now truncate-create the task's output file up front, so drilling into a running task reads as empty ("no output yet") instead of ENOENT, and a recycled id can never surface a previous run's output. Test: registering_truncates_any_stale_output_file.

The remaining six findings are verbatim re-posts of items already fixed and regression-tested at the reviewed head (detailed in prior comments).

@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: fa9afc9303

ℹ️ 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 crates/lib/src/services/background.rs Outdated
// Truncate up front: ids and cache paths recur across process
// starts, so a reader must never see a previous run's output —
// and reading before this run finishes means "empty", not ENOENT.
let _ = std::fs::File::create(&output_file);

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 truncating output owned by another session

When two Agent-Code processes run the same task kind, each TaskManager starts its counter at 1 and task_output_path places both records in the same global cache path (for example, both use a1.out). Consequently, registering a1 in the second process now truncates the first process's still-visible output immediately; there is no ownership check that distinguishes a stale file from one belonging to a live session. Use session-unique output paths or collision-resistant IDs rather than truncating solely by recycled task id.

Useful? React with 👍 / 👎.

Comment thread crates/cli/src/ui/modern/app.rs Outdated
Comment on lines +2041 to +2044
let unclaimed = next.iter().enumerate().position(|(i, t)| {
!claimed[i]
&& (t.task_id.as_deref() == Some(row.id.as_str())
|| (t.task_id.is_none() && t.agent_id == 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 Prefer the live event row when reconciling late updates

If a manager poll synthesizes a LocalAgent row before its queued SubagentUpdate is applied, the later event adds a second row. On subsequent polls this predicate chooses the earlier synthesized row through its matching task_id instead of folding into the unclaimed event row with agent_id == sid; when the manager row becomes terminal, polling can stop with both a stale working event row and a terminal synthesized row permanently visible. Prefer an unclaimed matching event row when one exists, and discard the synthesized row for the same task.

Useful? React with 👍 / 👎.

Comment thread crates/lib/src/services/background.rs Outdated
// Truncate up front: ids and cache paths recur across process
// starts, so a reader must never see a previous run's output —
// and reading before this run finishes means "empty", not ENOENT.
let _ = std::fs::File::create(&output_file);

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 Create task output files asynchronously

This synchronous filesystem operation runs directly inside async task registration, so a slow or stalled cache filesystem blocks the Tokio worker that is also driving tool execution and the TUI. Use tokio::fs::File::create(...).await (and handle the result) in both registration paths to preserve the repository's async-I/O invariant.

AGENTS.md reference: AGENTS.md:L125-L125

Useful? React with 👍 / 👎.

@emal-avala

Copy link
Copy Markdown
Member Author

@codex review

On 2e7baec, addressing two of the three new findings:

  • late-event duplicates: reconciliation now prefers an unclaimed live event row over the row a task synthesized earlier; the cleanup pass then retires the synthesized duplicate. Test: a_late_event_row_absorbs_the_synthesized_row.
  • async I/O: both registration paths pre-create the output file via tokio::fs, logging (not swallowing) failure.

Declared deviation on the third (cross-session truncation): concurrent processes colliding on recycled ids/shared cache paths is a pre-existing property of the task id scheme — child stdout redirection already overwrites the same path regardless of this PR's truncate. Making ids collision-resistant or output paths session-scoped is a task-identity redesign beyond this pane PR; it should be a follow-up issue (happy to file it). The truncate here strictly improves the common single-session case (no ENOENT, no stale reads).

The seven other findings are verbatim re-posts of items already fixed and regression-tested at the reviewed head; see prior comments for the mapping.

@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: 2e7baeca84

ℹ️ 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 crates/cli/src/ui/modern/tasks.rs Outdated
source: TaskSource,
) {
let state = TaskState::parse(state);
if let Some(existing) = tasks.iter_mut().find(|t| t.agent_id == agent_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 Match task rows by both ID and source

When a subagent's explicit ID or description-derived ID matches an existing manager task ID (for example, a foreground Agent call described as b1 while background task b1 exists), this lookup updates the background row instead of inserting a subagent row because it ignores source. The next manager sync rebuilds that background row and discards the subagent update entirely, so the active agent disappears from the pane; restrict the match to the requested source (and preserve the separate manager-backed reconciliation logic).

Useful? React with 👍 / 👎.

@emal-avala

Copy link
Copy Markdown
Member Author

@codex review

On 86e18d6: upserts now match rows by id AND source, so an agent id colliding with a background task id creates its own row instead of hijacking (and then losing) the background row. Test: colliding_ids_across_sources_stay_separate_rows.

The seven remaining findings are verbatim re-posts of items already fixed and regression-tested at the reviewed head (mapping in prior comments); the cross-session truncation deviation stands as declared.

@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: 86e18d6284

ℹ️ 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".

state,
headline: row.headline,
source: TaskSource::Subagent,
task_id: Some(row.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 Persist LocalAgent output before enabling drill-in

When a LocalAgentExecutor run is registered with the shared manager (the embeddable task-executor path), this synthesized row becomes drillable, but LocalAgentExecutor::execute only calls set_status after AgentTool.call and never writes the returned content to the task output file, unlike LocalWorkflowExecutor. Because registration pre-creates an empty .out file, pressing Enter on a completed LocalAgent row always renders (no output yet) instead of the subagent result; persist the result before marking the task terminal.

Useful? React with 👍 / 👎.

@emal-avala

Copy link
Copy Markdown
Member Author

@codex review

On 08172d7: LocalAgentExecutor now persists the subagent's result via TaskManager::write_output before setting terminal status, matching the workflow executor and the write_output contract, so drill-in on a completed executor-path agent shows the result instead of "(no output yet)".

The seven remaining findings are verbatim re-posts of items already fixed and regression-tested at the reviewed head; mapping in prior comments.

@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: 08172d7fa2

ℹ️ 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".

// Truncate up front: ids and cache paths recur across process
// starts, so a reader must never see a previous run's output —
// and reading before this run finishes means "empty", not ENOENT.
if let Err(e) = tokio::fs::File::create(&output_file).await {

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 Avoid truncating another manager's output file

When two CLI or embedded instances run under the same account, each TaskManager::new starts its counter at 1 while task_output_path uses the same global cache directory, so their first same-kind tasks share paths such as b1.out. This new eager File::create lets one instance immediately erase a completed or still-readable result owned by the other (and subsequent reads may expose the other task's output); use process/session-unique paths or coordinate ID allocation before truncating.

Useful? React with 👍 / 👎.

Comment on lines +169 to +171
if let Ok(r) = &outcome {
let _ = tm.write_output(id, &r.content).await;
}

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 Persist executor errors before marking the task failed

When AgentTool.call returns a non-cancellation Err such as a timeout or spawn failure, this if let Ok skips write_output and then marks the record failed. Completion injection and task drill-in read the pre-created output file rather than the TaskStatus::Failed message, so the failure reason is lost and drill-in reports (no output yet); persist e.to_string() for error outcomes before setting terminal status, as the workflow executor does.

Useful? React with 👍 / 👎.

@emal-avala

Copy link
Copy Markdown
Member Author

@codex review

On 626b103: executor Err outcomes are now persisted to the task output as well, so a Failed row's drill-in shows the error rather than "(no output yet)".

On the cross-manager truncation (re-raised as P1): the deviation declared two rounds ago stands. Concurrent managers sharing recycled ids and one global cache path collide with or without the pre-truncation — the child stdout redirect already overwrites the same file. The durable fix is session-scoped output paths or collision-resistant task ids, which is a task-identity redesign out of scope for this tasks-pane PR; I will file a follow-up issue referencing this thread.

The remaining eight findings are verbatim re-posts of items fixed and regression-tested at the reviewed head (mapping in prior comments).

@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: 626b103792

ℹ️ 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 crates/cli/src/ui/modern/app.rs Outdated
fn selected_task_key(&self) -> Option<(String, super::tasks::TaskSource)> {
self.tasks
.get(self.tasks_selected)
.map(|t| (t.agent_id.clone(), t.source))

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 Key reconciled selections by task ID

When a manager poll synthesizes a completed LocalAgent row before its delayed SubagentUpdate arrives, the selected row is keyed by its temporary agent_id (for example, a3). The next sync folds the manager record into the event row and deletes that synthetic row; because this key cannot recognize the replacement, keep_task_selection only clamps the old positional index. With another same-state agent in the pane, the selection can therefore move to that agent and Enter opens the wrong task's output. Use the manager task_id as the canonical selection identity whenever it exists.

Useful? React with 👍 / 👎.

@emal-avala

Copy link
Copy Markdown
Member Author

@codex review

On 3084ef3: the pane selection key now prefers the backing task id, so a selection survives the synthesized-row-to-event-row absorb where agent_id changes. Test extended: a_late_event_row_absorbs_the_synthesized_row asserts the selection lands on the absorbed row.

Nine remaining findings are verbatim re-posts of items fixed and regression-tested at the reviewed head, plus the declared cross-manager truncation deviation (follow-up issue to come). Mapping in prior comments.

@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: 3084ef3a77

ℹ️ 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 crates/cli/src/ui/modern/app.rs Outdated
name: "Task output".into(),
detail: id.to_string(),
result: Some(body),
is_error: 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 Mark output-read failures as errors

When a task is cleared or its output file becomes unreadable after drill-in starts, read_output_tail returns Err and this method formats a failure message, but the unconditional is_error: false renders the card with a green check and success styling. Derive is_error from the Result so users are not shown a successful “Task output” operation when the read actually failed.

Useful? React with 👍 / 👎.

@emal-avala

Copy link
Copy Markdown
Member Author

@codex review

On d23a494: the drill-in card now derives is_error from the read result, so a failed read renders with error styling. Test updated (an_unreadable_task_reports_the_error_instead_of_looking_empty asserts is_error).

Note: the Coverage failure on 3084ef3 was services::oauth::tests::browser_launcher_reports_non_zero_exit_status — an OAuth test this PR does not touch, green on every prior head; a tarpaulin flake that this push re-runs.

Ten remaining findings are verbatim re-posts of items fixed and regression-tested at the reviewed head, plus the declared cross-manager truncation deviation. Mapping in prior comments.

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Nice work!

Reviewed commit: d23a4945c9

ℹ️ 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".

@emal-avala
emal-avala merged commit d563788 into main Jul 27, 2026
15 checks passed
@emal-avala
emal-avala deleted the feat/tasks-pane branch July 27, 2026 04:49
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