Skip to content
This repository was archived by the owner on Aug 6, 2026. It is now read-only.

feat(channels): stream agent turns into threads, read Activity per mention - #3534

Closed
adamleithp wants to merge 3 commits into
mainfrom
channels/agent-thread-messages
Closed

feat(channels): stream agent turns into threads, read Activity per mention#3534
adamleithp wants to merge 3 commits into
mainfrom
channels/agent-thread-messages

Conversation

@adamleithp

@adamleithp adamleithp commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

Agent turns and permission asks now land in a channel thread as messages you can read in place, and Activity treats a mention as read when you open its thread — not when you glance at the page.

Rebased 2026-07-20: #3380 merged after this branch was cut and already ships the agent-row rendering (author_kind/event/payload on TaskThreadMessage, robot avatar + "Agent" label). Those pieces were dropped from this PR in ef4444b; what remains below is net-new.

Thread messages

  • turn_complete / permission_request are typed. Added a TaskThreadMessageEvent union and pointed TaskThreadMessage.event at it, so agent rows render from a stable key rather than a freeform string.
  • No duplicate turn. The backend posts every agent turn as a durable turn_complete row keyed by payload.run_id. visibleThreadMessages drops that copy for a run the viewer is already streaming — the streamed turn is the one they watched arrive, and swapping it for the server's copy at the end made the message jump. A row with no run id is kept: a possible duplicate beats dropping the only copy.
  • Turns break where the work happened. Text arrives as mid-sentence chunks, so consecutive chunks join raw — but a tool call between two pieces of prose is a paragraph break. Without it, "…before committing." ran straight into "The patch rebased cleanly." Trailing calls add no dangling break, and a run of calls adds only one.
  • Long bodies get a framed window (ThreadScrollBody, built on quill's ChatStream): capped height, scrolled for the rest, so one essay doesn't push the next row off screen. It follows live output while streaming, and opens on the last line once finished. The border lives on the wrapper, not the stream — ChatStream's mask would otherwise eat a border on its own element exactly when there's more to read.

Activity

  • A mention opens its thread beside the list, instead of navigating away to the full task. Mentions whose channel folder doesn't resolve still fall through to the task view.
  • Read is per mention, earned by opening its thread. Opening the Activity page marked everything seen, which is backwards: the page is a list of things you haven't dealt with, not a receipt for them. isMentionUnread/countUnreadMentions replace the global watermark; lastSeenAt stays as a read-only historical watermark so the switch doesn't resurface old mentions as unread.
  • The open mention's row is highlighted (bg-fill-selected). Keyed on the mention, not the task — the panel tracks a task, and a task can be mentioned in several rows, so keying on it alone lit all of them for one click.
  • Read ids are bounded (500, oldest out first) and survive persistence — Set doesn't round-trip through JSON, so it's stored as an array and rebuilt on load.

Notifications

  • A task whose thread panel is open counts as "being viewed". The active target was route-only, so a task filling half the screen in a thread panel still got a "needs your input" toast. activeNotificationTarget now considers the panel — a collapsed panel is a rail with nothing legible in it, so it doesn't count. The host's getActiveTarget is thin glue over it.

Notes

  • @posthog/quill 0.3.0-beta.240.3.0-beta.25 for ChatStream.
  • Known gap: permission_request rows render as text; their Approve/Deny actions aren't wired yet.
  • Activity can't yet grey out superseded permission asks — TaskMentionDTO carries no event/payload/author_kind (Render agent thread updates: markdown links and agent-authored messages #3380 only added them to TaskThreadMessage), which is also why an agent's mention still shows as "Unknown" in the list. Needs a ../posthog change; not in this PR.
  • Pre-existing failure on main, unrelated and untouched here: @posthog/git detectDefaultBranch > prefers remote HEAD over local detection.

Testing

  • pnpm typecheck, pnpm lint, node scripts/check-host-boundaries.mjs (no new violations) all clean.
  • pnpm test: green except the pre-existing @posthog/git failure above.
  • New unit tests: visibleThreadMessages (threadTimeline), isMentionUnread/countUnreadMentions (mentionActivity), tool-call turn breaks (threadAgentTurns), activeNotificationTarget.
  • Driven live over CDP against the real app: agent turns streaming into a thread, the scroll window opening on its last line, and the Activity row highlighting only the mention whose thread is open.
  • Re-ran typecheck, lint, boundaries, and core/ui tests after the rebase + trim.

🤖 Generated with Claude Code

@trunk-io

trunk-io Bot commented Jul 17, 2026

Copy link
Copy Markdown

Merging to main in this repository is managed by Trunk.

  • To merge this pull request, check the box to the left or comment /trunk merge below.

After your PR is submitted to the merge queue, this comment will be automatically updated with its status. If the PR fails, failure details will also be posted here

@github-actions

github-actions Bot commented Jul 17, 2026

Copy link
Copy Markdown

React Doctor found no issues in the changed files. 🎉

Reviewed by React Doctor for commit ef4444b.

@greptile-apps

greptile-apps Bot commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

Reviews (1): Last reviewed commit: "feat(channels): stream agent turns into ..." | Re-trigger Greptile

Comment on lines +185 to +188
const threadChannelId = threadTaskId
? folderChannelIdFor(
items.find((item) => item.taskId === threadTaskId)?.channelName ?? null,
)

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 Selected Mention Loses Its Channel

When the same task has mentions in multiple channels, this lookup takes the channel from the first matching feed item rather than the mention that opened the panel. Clicking another mention can therefore give ThreadSidebar the wrong channel ID and make “Open full” navigate to the wrong channel.

Comment on lines 49 to +50
storage: electronStorage,
// Sets don't survive JSON; store the ids as an array and rebuild on load.

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 Early Reads Can Be Forgotten

If a mention is opened before the asynchronous persisted state finishes loading, electronStorage drops writes during that first-read window and hydration can replace the in-memory set with the saved IDs. The sidebar updates for the current session, but the mention becomes unread again after restart.

@adboio adboio self-assigned this Jul 20, 2026
adboio and others added 2 commits July 20, 2026 09:17
…ntion

Agent turns and permission asks now land in a channel thread as messages,
and Activity marks a mention read when you open its thread rather than when
you open the page.

Thread messages:
- Render agent rows from `author_kind`/`event`/`payload` instead of `author`,
  which is null by design on agent rows and read as "Unknown".
- Drop the durable `turn_complete` copy of a run the viewer is already
  watching stream, so the live turn doesn't get swapped at the end.
- Break an agent's turn into paragraphs at tool-call boundaries instead of
  gluing every chunk into one clump.
- Frame long bodies in a capped, bordered scroll window that opens on the
  last line and, while streaming, follows the output.

Activity:
- Open a mention's thread beside the list instead of navigating to the task.
- Track read per mention, so the badge survives a glance at the page.
- Highlight the row whose thread is open.
- Count a task's thread as "being viewed" when its panel is open, so a
  notification doesn't announce what's already on screen.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The committed lockfile was generated without pnpm-workspace.yaml's overrides
applied, so it dropped the top-level overrides:/patchedDependencies: blocks and
resolved overridden deps to their un-overridden versions (plain vite instead of
rolldown-vite, undici 6.27.0 instead of the pinned 8.4.1, node-abi 4.33.0
instead of 3.92.0). pnpm install --frozen-lockfile rejected this with
ERR_PNPM_LOCKFILE_CONFIG_MISMATCH, failing every CI job at the install step.

Regenerated with pnpm install --lockfile-only so the lockfile matches the
workspace config again.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LakkSu5zcYbTzMKwurvBD4
@adboio
adboio force-pushed the channels/agent-thread-messages branch from a2eaa7d to 03b212e Compare July 20, 2026 13:18
#3380 landed on main after this branch was cut and already renders agent
thread rows (author_kind on TaskThreadMessage, robot avatar + "Agent" label,
no hover menu) inside ThreadMessageRow. Remove this branch's parallel copy:
the duplicated TaskThreadMessage fields the rebase glued in, the separate
AgentThreadRow component, and isAgentThreadMessage, whose only consumer was
that branch. Keep the TaskThreadMessageEvent union and point the existing
event field at it — the one piece #3380 didn't cover.

Generated-By: PostHog Code
Task-Id: a0f85df1-4a8f-4467-8051-e471b5e95009

adboio commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

Rebased onto main and trimmed the parts #3380 (merged after this branch was cut) already ships:

  • Dropped: the TaskThreadMessage field additions (author_kind/event/payload — now on main) and the separate AgentThreadRow + isAgentThreadMessage rendering path (main's ThreadMessageRow already handles agent rows identically). See ef4444b.
  • Kept: the TaskThreadMessageEvent union, visibleThreadMessages streaming dedup, tool-call paragraph breaks, ThreadScrollBody + quill beta.25, and the entire Activity/notifications half — all still net-new vs main.

Typecheck, biome, host boundaries, and core/ui tests all green after the trim.

@adamleithp adamleithp closed this Jul 27, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants