Skip to content

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

Open
adamleithp wants to merge 2 commits into
mainfrom
channels/agent-thread-messages
Open

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

Conversation

@adamleithp

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.

Thread messages

  • Agent rows say "Agent", not "Unknown". Agent rows are authorless by design (author_kind: "agent", no author), so anything resolving author alone named them "Unknown". They now render from author_kind + event + payload, with a robot avatar.
  • TaskThreadMessage carries the agent's shape. Added optional author_kind, event, payload to the shared type, plus a TaskThreadMessageEvent union (turn_complete | permission_request). Optional so older payloads still parse; absent means human, which is what every message was before agents could post.
  • 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, so the same missing fields are 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/isAgentThreadMessage (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.

🤖 Generated with Claude Code

…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>
@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 a2eaa7d.

@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.

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
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