Skip to content

feat(agents): open the latest transcript from agent activity - #1830

Open
tulsi-builder wants to merge 4 commits into
mainfrom
basil/transcript-latest-first
Open

tulsi-builder wants to merge 4 commits into
mainfrom
basil/transcript-latest-first

Conversation

@tulsi-builder

@tulsi-builder tulsi-builder commented Oct 28, 2026 •

Copy link
Copy Markdown
Contributor

Agent Activity transcripts were hard to read at a glance. They opened at the oldest turn, and clicking an agent's working indicator landed on the profile overview instead of the transcript the click was about (thread with Kenny and Arjun).

What changes

  • Transcript opens at the latest step. Turns stay oldest first, matching the conversation beside them, as Arjun suggested. The transcript scrolls its host to the end when it opens and follows new content unless the reader has scrolled up (ActivityTranscript). Builder Lab's hosted Activity opts out because it shares the profile page's scroll.
  • Working now opens the transcript. In the composer's Working now indicator, when an agent's work is in one known conversation (an explicit thread, or a single confirmed root), the activity destination asks for that conversation's transcript (&open=transcript). The agent's Activity tab then shows it directly, but only if the session is already known when the conversation is shown. A session a lookup finds later shows the usual View transcript button, so it never takes focus from what the reader is doing. The request is one-shot: the opening records it beside the target, not in it, so panel identity is unchanged (openProfileTranscript in profiles/target.ts). The Activity tab settles it, so neither switching sections nor the retained profile remounting on return to its channel reopens the transcript. Pi lists its conversations only after its tab shows, so it settles the request and shows the overview's View transcript button instead of opening late. In a tabbed host, closing that auto-opened transcript closes the profile panel. Close other tabs only clears detail tabs (keepOwner), and a transcript the reader opens from the profile still closes only itself. Each opened transcript keys its view, so it starts at the latest step even after the reader scrolled up in another one.
  • Working now rows don't navigate nowhere. When the agent works in the conversation already shown, the row opens its activity and the separate list icon is dropped. Rows for work in other threads are unchanged.
  • No guessing. Ambiguous or unconfirmed work, and the other activity openers (the message accessory, the sidebar list, the message menu), keep the plain destination and show the overview. A lookup that finishes later never replaces the reader's choice.

Non-goals: live streaming of transcript steps (transcripts still reread on turn start/end and Refresh), the sidebar Working now list, and newest-first ordering.

Validation

  • vitest run src/features src/bundled at e1e6dea: 10,897 passed, 16 skipped. tsc, biome and git diff --check are clean. Pre-commit and pre-push hooks passed.
  • New regressions: auto-open once with no reopen after Back (Claude Code); closing the auto-opened tab closes the panel (Codex, in PanelWorkspace); a held lookup doesn't override the reader's choice (OpenedConversation.test.tsx, fails without the fix); a plain target never requests a transcript (ProfilePanel.test.tsx, fails with the old mapping); target parsing; follow-latest scrolling; Working now row behavior.
  • Review follow-up (Kalvin's 5 findings): fail-then-pass shown for Close other tabs keeping the panel (Codex in PanelWorkspace), asking once across a section switch (ProfilePanel.test.tsx), a late lookup never auto-opening (OpenedConversation.test.tsx), a remounted profile not asking again (ProfilePanel.test.tsx), and Pi's late listing settling without opening (pi/tabs.test.tsx). The remount regression is component-level; no browser journey covers the channel round trip. Per-transcript follow reset (the key on each adapter's TranscriptView) has no dedicated test.
  • Agent review (Crack Catcher) found two Medium issues, ambiguous scope and the late lookup. Both are fixed and the re-review confirmed them.
  • Human testing (tulsi, native dev app, Claude Code agent): a single agent and two agents in a Solo chat, row click, and close behavior.

No browser journeys added or removed. Six existing cases in agent-activity.spec.mjs and local-agent-activity.spec.mjs were updated, because a Working now row whose conversation is already shown now opens activity itself instead of offering a navigate row plus an icon. The first push failed those cases in hosted CI on chromium and webkit. After the update, all four activity-related spec files pass locally in both engines (agent-activity, local-agent-activity, activity-navigation-focus, solo-workspace). After the review fixes, agent-activity, local-agent-activity, agent-activity-overhaul, channel-tabs and layout gave 103 of 104 passing in both engines. The one Chromium failure, a Working now hover in agent-activity.spec.mjs:129, passed 4 of 4 times on a Chromium rerun with --repeat-each 4, and also passed in WebKit.

Deferred

  • Codex, goose and Pi Activity were not exercised in the app; only unit coverage.
  • The review fixes (Close other tabs, switching sections, switching channels, late lookup) have not been re-tested by a human in the app yet.
  • At e1e6dea, agent-activity, local-agent-activity and agent-activity-overhaul pass locally in both engines (56/56). channel-tabs and layout were last run locally at 65cc403.
  • Scroll geometry was checked by the human in the app only, not in a browser engine. Enlarged text and the dark theme were not inspected.
  • Overlaps with drafts refactor(agents): keyed reads for harness Activity tabs #1728 (harness tabs.tsx) and Unify agent activity with incoming message previews #1729 (activity indicator), so expect small conflicts.

🤖 Generated with Claude Code

The transcript now opens at its latest step and follows new steps while
the reader stays at the end; the order stays oldest first, matching the
conversation beside it. Opening an agent from the composer's Working now
indicator, when its work is in one known conversation, shows that
conversation's transcript directly; closing it closes the profile panel.
A Working now row for work in the conversation already shown opens its
activity instead of navigating nowhere. Ambiguous or unconfirmed work
keeps the overview, and a transcript the reader chooses is never
replaced by a lookup that finishes later.

Co-authored-by: BasilBot <399581b6165ab01343e3a1324774b1024d76942aea563468f93c975e5ee5193c@buzz.block.builderlab.xyz>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: tulsi <tulsi@block.xyz>
Rows whose conversation is already shown now open the agent's activity
directly, so the browser journeys assert that row instead of the old
navigate-plus-icon pair.

Co-authored-by: BasilBot <399581b6165ab01343e3a1324774b1024d76942aea563468f93c975e5ee5193c@buzz.block.builderlab.xyz>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: tulsi <tulsi@block.xyz>

@kalvinnchau kalvinnchau left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🤖 Five actionable findings: failing browser journeys, two reproduced panel lifecycle defects, late-lookup focus loss, and retained follow state when switching transcripts. Required CI is failing.

: undefined;
// Going to the conversation already shown would do nothing, so the row
// opens its activity instead.
if (viewActivity && conversationRoot === threadRootId)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🤖 [P1] Update browser journeys for the changed row action

Same-conversation rows now drop Open conversation/thread for …, but tests/browser/local-agent-activity.spec.mjs:695–699 and the journeys at tests/browser/agent-activity.spec.mjs:124, :339, :466 and :704 still drive that button. Exact-head Chromium and WebKit jobs fail waiting for it; the base passed those shards. Update same-conversation journeys to use the row’s View … activity action and rerun both engines. Other-thread rows still retain the old action.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

🤖 Fixed in 44fac11. The same-conversation journeys in agent-activity.spec.mjs and local-agent-activity.spec.mjs now drive the row's View … activity action. agentRow matches only the row ([data-variant="row"]), so other-thread rows still assert Open thread for … plus the separate icon. Passes locally in Chromium and WebKit, and hosted CI is green at 65cc403.

},
back() {
setShown(undefined);
if (tabs && close && shown?.initial) close();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🤖 [P2] Keep owner-preserving detail cleanup from closing the profile

Auto-open a transcript, right-click its profile tab, then choose Close other tabs: the profile selected to survive disappears. PanelWorkspace.tsx:432–444 closes the kept owner’s detail tabs via tab.close(), which now invokes back() and escalates to the panel-level close(). This reproduced for all four adapters in Chromium and WebKit. Distinguish detail cleanup from explicit whole-panel dismissal, or remove the escalation.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

🤖 Fixed in 65cc403. PanelWorkspace's Close other tabs now closes the kept owner's details with tab.close({ keepOwner: true }). That reaches back() through PanelLocalTab and PanelSubview, and back() only escalates to the panel close() without it. Explicit close (✕, middle-click, Cmd+W, Escape) still closes the panel for an auto-opened transcript. Regression in codex/tabs.test.tsx ("keeps the panel when the profile's other tabs are closed") fails with the old tab.close(). I only exercised this in Codex in a browser-free PanelWorkspace. The other adapters share the same useOpenTranscript path.

// reopen it unasked.
if (shown && shown.place !== place) setShown(undefined);
// Kept after Back, so closing it shows the profile rather than reopening.
const [offered, setOffered] = useState("");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🤖 [P2] Consume the auto-open request at the persistent panel owner

After Working now auto-opens a transcript, select its profile tab, switch to Settings, then return to Activity. The transcript reopens and takes selection because section-scoped offered state is destroyed when Activity unmounts. The panel sequence reproduced for all four adapters in Chromium and WebKit. Clear or acknowledge initialTranscript at the persistent panel-target owner so changing sections does not offer the same request again.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

🤖 Fixed in 65cc403. ProfilePanel owns the request now: initialTranscript is a callback it passes once per conversation (asked keyed by channel/root). The Activity tab calls it after deciding, so a remount after Settings gets no request. close is passed to the Activity role independently, so the auto-opened tab still closes the panel after the request is consumed. Regression in ProfilePanel.test.tsx ("asks for the transcript once, not again on returning from another section") fails if the panel keeps asking.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🤖 At 65cc403, consumption now survives section switches but not panel remounts (src/bundled/profiles/ProfilePanel.tsx:79–83). Open the transcript, keep its profile via Close other tabs or profile → Settings, then navigate Alpha → Beta → Alpha. The transcript reopens and takes focus; closing its X removes the kept profile. Reproduced for all four adapters in Chromium and WebKit (16/16 cases); 8 control cases without open=transcript retain the profile without reopening. Store consumption with the retained panel entry or a one-shot opening option. Avoid rewriting the target because it participates in panel identity. Add a navigation/remount regression.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

🤖 Fixed in e1e6dea with a one-shot opening option. Working now opens through openProfileTranscript(target, open) (profiles/target.ts), which records the request beside the target, not in it, so panel identity is unchanged. A failed open withdraws it. ProfilePanel reads it via useSyncExternalStore and the Activity tab settles it, so a retained profile remounting on Alpha → Beta → Alpha gets no request and its ✕ only closes itself. This replaces the per-mount asked state. Regression in ProfilePanel.test.tsx: a second mount of the same target without a new opening doesn't ask, and it fails with the old per-mount logic. That test is component-level; I didn't add a browser journey for the channel round trip.

if (shown && shown.place !== place) setShown(undefined);
// Kept after Back, so closing it shows the profile rather than reopening.
const [offered, setOffered] = useState("");
if (initial !== undefined && place && offered !== place) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🤖 [P3] Preserve composer focus when a session lookup resolves late

If initial arrives asynchronously from a session lookup after the reader returns to typing in the composer, registering the transcript tab sets focusTab and moves focus away from the composer. A targeted component reproduction confirms the composer loses focus and the transcript tab gains it. The show() guard protects explicit transcript choice but does not protect this focus choice. Auto-open only when the session is initially known, or open late results without moving focus.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

🤖 Fixed in 65cc403 with your first option. useOpenTranscript decides when a conversation is first shown, so it auto-opens only if the session is known then. A session the lookup finds later shows the usual View transcript button and never registers a tab or moves focus. This also removed the show() guard. Trade-off: a brand-new conversation whose session isn't saved until its first turn ends opens to the overview instead. OpenedConversation.test.tsx ("never opens a transcript found only after the conversation was shown") fails with late opening restored.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🤖 At 65cc403, the delayed-session fix covers Codex, Claude and goose, but Pi still mounts PlaceList only after runtime.places() resolves (src/bundled/pi/tabs.tsx:701–735,762–767). Hold that listing, open the activity transcript target, type in the composer, then release it: the transcript opens and focus moves to its tab. Reproduced in Chromium and WebKit using the actual app with modeled adapter latency; real host startup timing was not measured. Consume the intent before the delayed child mounts, or prevent late auto-open from moving focus after the user leaves the panel. Add the Pi delayed-listing regression.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

🤖 Fixed in e1e6dea with your first option. Pi's ActivityTab settles the request as it mounts and no longer passes it to PlaceList, so a listing that arrives later shows the View transcript button and never moves focus. Pi's listing always resolves after mount, so in practice Pi never auto-opens: a click from Working now lands on the overview. Regression in pi/tabs.test.tsx ("settles a transcript request without opening it once Pi's late listing arrives") fails with the request passed through.

* unless the reader has scrolled up to read earlier turns. */
function useFollowLatest(content: unknown, enabled: boolean) {
const timeline = useRef<HTMLDivElement>(null);
const following = useRef(true);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🤖 [P3] Reset follow state when switching transcripts

In a tabbed host, selecting another View transcript reuses this un-keyed view. If the reader scrolled up in the previous transcript, useFollowLatest retains following.current === false, so the next transcript does not initially follow its latest step. This is supported by the component lifecycle trace and the existing follow test; the adapter-switch sequence was not exercised dynamically. Key the view by the opened row. The same pattern appears in Codex, goose and Pi.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

🤖 Fixed in 65cc403. All four adapters key TranscriptView by open.key, so each opened row gets fresh follow state. There's no dedicated test for this one; the existing follow-latest test covers the hook itself.

tulsi-builder and others added 2 commits October 28, 2026 13:14
Addresses review on #1830:
- Close other tabs clears the profile's detail tabs without closing the
  profile: the workspace marks that cleanup with keepOwner, and the
  auto-opened transcript only closes its panel on a real close.
- The profile panel asks for the transcript once per conversation, so
  leaving Activity for another section and returning does not reopen it.
- Only a transcript known when the conversation is shown opens by itself;
  one a lookup finds later never takes the reader's focus or choice.
- Each opened transcript keys its view, so it starts following its latest
  step even after the reader scrolled up in the previous one.

Co-authored-by: BasilBot <399581b6165ab01343e3a1324774b1024d76942aea563468f93c975e5ee5193c@buzz.block.builderlab.xyz>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: tulsi <tulsi@block.xyz>
Addresses review on #1830:
- Working now's opening records a one-shot transcript request beside the
  target, not in it, so panel identity is unchanged. The profile panel
  reads it through a subscription, and the Activity tab settles it, so a
  retained profile remounting on return to its channel asks for nothing.
- Pi lists its conversations only after its tab shows, so it settles the
  request without opening it rather than opening the transcript late,
  under the reader's focus.

Co-authored-by: BasilBot <399581b6165ab01343e3a1324774b1024d76942aea563468f93c975e5ee5193c@buzz.block.builderlab.xyz>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: tulsi <tulsi@block.xyz>
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.

2 participants