enhancement
major
minor
defect
critical
major
minor
major
#30143
TL Chat: While an agent is performing multiple steps, their last message remains visible, and the status bar displays the current step instead of just showing "…" and "Working on …"
Context
An agent that makes many model requests during a user session (think → tool → think → tool …) shows almost nothing in the chat (tl-chat, display trace="summary") throughout the entire process: Below the “Working…” progress bar, only the three dots indicating a pending response appear—often for minutes at a time. Texts that the agent writes in between, as well as its thought process, appear briefly and immediately disappear into collapsed “Output”/“Thought” entries in the history as soon as the next tool call (e.g., Bash) begins. Observed with the coding agent of the tl-factory control application (Claude Code in the workspace), but affects every multi-stage agent in tl-chat.
Cause (tl-chat/ui/src):
- Every model request begins with `turn_started`; `chat-store.ts` (`on('turn_started', …)`) creates a new, empty response message for this (`ADD_ASSISTANT ` with `text: ''`).
- `lastAnswer` (`thread.tsx`) takes the most recent ` assistant_text` message, i.e., the empty one just created. This makes pending = busy && !answer?.text true, and PendingAnswer displays the dots; traceEvents sorts the texts of earlier requests into the history, where TraceEntry starts collapsed.
- The run summary (RunSummary, currentThought) displays “Thinking: …” only as long as the last step is a thought; during a tool call, it displays “Working …” without specifying which tool is doing what. Entries for a tool call in the history have no preview, even though, for example, Claude Code’s Bash tool sends a ` description ` with every call.
Behavior
- The last text remains visible: As long as the round is running, an empty response message is not considered a response. The most recent non-empty text in the round serves as the response until a newer text replaces it; the replaced text moves to the history as before. The ellipses appear only as long as the round has no text at all. After the round ends, the response is, as before, its last text.
- The status bar displays the current step: during thinking, as before, “Thinking: <beginning of the latest paragraph>”; during a tool call, “<Tool>: <Preview>,” e.g., Bash: Build with tests. The preview is the call’s description, if it has one; otherwise, it is its first argument, truncated as in “Thinking.” “Working…” remains only for the time during which no discernible step is taking place.
- Tool entries in the history show the same preview as “Thinking” and “Output” when collapsed.
- Order in the history: The thinking process for a model query appears before its text, both live and after reloading.
- This applies equally in real time and after reloading a running conversation (a window that joins a session later).
Acceptance
- UI tests (thread/`chat-store`): a round with text → tool → `turn_started ` without text displays the text as the response and no points; a round without any text displays the points; after the round ends, the response is the last text.
- Log line during a tool call with ` description`, with only one argument, and without arguments.
- Testing in the browser with a multi-stage agent (e.g., tl-factory, Workspace agent with a task that must build).
Solution
Browser part of tl-chat (ui/src):
- `lastAnswer` (`thread.tsx`) skips empty `assistant_text` messages during a running round and returns the most recent non-empty text. The points (`PendingAnswer`) appear only when there is no text at all. Since the rule only reads the event list, it applies both in real time and after reloading.
- toolPreview(call): the description argument, if a string; otherwise, the value of the first argument (a string directly, a list of simple values as comma-separated values—e.g., the IDs from getInstance—or JSON); otherwise empty; Whitespace consolidated, truncated to 80 characters. The arguments are read using the same function as in the release card (parseArguments in prompts.tsx), i.e., also from the JSON text of the stream.
- currentStep (instead of currentThought): Thinking → “Thinking: …”, but only until the text for the same request begins streaming; ongoing tool call → “<Tool>: <Preview>” or just the tool name without a preview; otherwise “Working …”.
- Tool entries in the history receive the preview from ` toolPreview` when collapsed.
- reducer.ts (APPEND_THINKING): A model query creates its response message at startup and thinks before writing; the thinking process is therefore inserted before the still-empty response message.
Server (tl-ai-core):
- ConversationLogListener wrote first the buffered text, then the buffered thinking to the history at each boundary (tool call, release, end of turn)—including in the parts of a delegation. It now writes the thinking first. This ensures the order is correct after reloading, and ConversationLogReplay replays the responses to the model in the order in which it generated them (thinking before text). Previously saved logs retain their original order.
Tested in the browser using the tl-ai-demo chat: synchronous delegation to a subagent (run line “wait_for_subagent: sa-1” for the duration of the wait time), getInstance calls(“getInstance: GenericObject:2916”), reloading during a run, and the order of the conversation history both live and after reloading.