Add goal TUI UX (5 / 5) (#18077)

Adds the TUI user experience for goals on top of the core runtime from
PR 4.

## Why

Users need a direct TUI control surface for long-running goals. The UI
should make the current goal visible, support common goal actions
without waiting for a model turn, and avoid confusing end-of-turn
notifications while an active goal is immediately continuing.

## What changed

- Added `/goal` summary rendering for the current goal, including
active, paused, budget-limited, and complete states.
- Added `/goal <objective>` creation/replacement through the app-server
goal API rather than a model prompt.
- Added `/goal clear`, `/goal pause`, and `/goal unpause` command
variants.
- Added a confirmation menu when the user enters a new goal while
another goal already exists.
- Updated `/goal` help and summary tip text so it reflects the supported
command variants without advertising slash-command token budgets.
- Added footer/statusline goal indicators, including elapsed time and
token budget display when a budget exists from API/tool-created goals.
- Consumes goal updated/cleared notifications so the TUI stays in sync
with external app-server changes.
- Suppresses end-of-turn desktop notifications only when a goal is still
active and follow-up work is expected.
- Preserves slash-command history behavior and avoids leaking queued
`/goal` state into unrelated submissions.

## Verification

- Added TUI unit and snapshot coverage for goal command availability,
summary rendering, control commands, replacement menu behavior,
status/footer display, notification handling, and command history.
This commit is contained in:
Eric Traut
2026-04-24 21:16:45 -07:00
committed by GitHub
Unverified
parent 4167628622
commit f1c963d77e
32 changed files with 2709 additions and 177 deletions
+61
View File
@@ -43,6 +43,13 @@ use codex_app_server_protocol::ThreadCompactStartParams;
use codex_app_server_protocol::ThreadCompactStartResponse;
use codex_app_server_protocol::ThreadForkParams;
use codex_app_server_protocol::ThreadForkResponse;
use codex_app_server_protocol::ThreadGoalClearParams;
use codex_app_server_protocol::ThreadGoalClearResponse;
use codex_app_server_protocol::ThreadGoalGetParams;
use codex_app_server_protocol::ThreadGoalGetResponse;
use codex_app_server_protocol::ThreadGoalSetParams;
use codex_app_server_protocol::ThreadGoalSetResponse;
use codex_app_server_protocol::ThreadGoalStatus;
use codex_app_server_protocol::ThreadInjectItemsParams;
use codex_app_server_protocol::ThreadInjectItemsResponse;
use codex_app_server_protocol::ThreadListParams;
@@ -667,6 +674,60 @@ impl AppServerSession {
Ok(())
}
pub(crate) async fn thread_goal_get(
&mut self,
thread_id: ThreadId,
) -> Result<ThreadGoalGetResponse> {
let request_id = self.next_request_id();
self.client
.request_typed(ClientRequest::ThreadGoalGet {
request_id,
params: ThreadGoalGetParams {
thread_id: thread_id.to_string(),
},
})
.await
.wrap_err("thread/goal/get failed in TUI")
}
pub(crate) async fn thread_goal_set(
&mut self,
thread_id: ThreadId,
objective: Option<String>,
status: Option<ThreadGoalStatus>,
token_budget: Option<Option<i64>>,
) -> Result<ThreadGoalSetResponse> {
let request_id = self.next_request_id();
self.client
.request_typed(ClientRequest::ThreadGoalSet {
request_id,
params: ThreadGoalSetParams {
thread_id: thread_id.to_string(),
objective,
status,
token_budget,
},
})
.await
.wrap_err("thread/goal/set failed in TUI")
}
pub(crate) async fn thread_goal_clear(
&mut self,
thread_id: ThreadId,
) -> Result<ThreadGoalClearResponse> {
let request_id = self.next_request_id();
self.client
.request_typed(ClientRequest::ThreadGoalClear {
request_id,
params: ThreadGoalClearParams {
thread_id: thread_id.to_string(),
},
})
.await
.wrap_err("thread/goal/clear failed in TUI")
}
pub(crate) async fn logout_account(&mut self) -> Result<()> {
let request_id = self.next_request_id();
let _: LogoutAccountResponse = self