feat(go): chat UI overhaul — copy, collapse, html blocks, content sidebar + chips#378
Merged
Merged
Conversation
Adds a small "Copy" / "Copied" button under each agent message that puts the raw markdown content on the system pasteboard. Mirrors the React UI affordance so users can roundtrip agent output to other markdown-aware tools. - iOS: UIPasteboard.general.string - macOS: NSPasteboard.general.setString(_, forType: .string) - 2s "Copied" confirmation, then reverts.
When an agent is producing a long chain of thinking/tool-call steps, the full history overwhelms the chat. Only the most recent two steps are rendered by default now; earlier ones collapse behind a tappable "N earlier steps" header that unrolls the full list. - Default state: collapsed (tail of 2 visible) - Tap header → expand / collapse with a small ease - Rolling window keeps updating as the agent produces more steps
Adds a new MarkdownSegment.htmlBlock variant alongside .code/.table. The segmenter routes \`\`\`html fences to it; the bubble renderer mounts them in a sandboxed WKWebView so agents shipping HTML previews show the rendered output inline, not the raw source. - JavaScript disabled at the WKPreferences level — defaults to safe preview, not arbitrary script execution. - Document scaffold sets color-scheme so embedded inputs/forms match the surrounding chat theme; body margins zeroed so the measured height matches visible content. - Reported height capped at 400pt with internal scroll so a long artifact doesn't blow out the chat scroll position. - Other languages (xml, htm, code, etc.) still render as code blocks — only the explicit `html` tag opts into the live preview.
Adds a right-hand "Content" panel to the chat view that lists every
file in the workspace, sorted most-recent first. Toggled from a
sidebar.right icon in the chat header (macOS) or top-bar toolbar (iOS).
- Default off. Side-by-side on macOS/iPad regular hsize; slides up as a
sheet on iPhone compact since 280pt of sidebar would crush the chat.
- Image files render a thumbnail via a small AuthorizedAsyncImage
loader — AsyncImage can't carry headers, but /v1/files/{id} requires
X-Workspace-Token, so it builds a request via WorkspaceStore and
decodes on the shared URLSession.
- Non-image kinds get a tinted badge + SF Symbol so the type is legible
at a glance.
- v1 lists the whole workspace, not a per-thread artifact view; thread-
scoping is a follow-up.
GET /v1/files?network=<id>&status=active&limit=100 — backend endpoint
unchanged. New WorkspaceFile model handles the snake_case payload via
explicit CodingKeys.
|
@baryhuang is attempting to deploy a commit to the Raphael's projects Team on Vercel. A member of the Team first needs to authorize it. |
When an agent posts a link to a workspace file (`/v1/files/<id>`) on its own line, render it as a tappable pill instead of an inline anchor. Tapping the chip opens the Content sidebar focused on that file. Tapping a file in the sidebar list opens the same detail panel. A back chevron returns to the file list. Key pieces: - `ContentSidebarController` is the shared state owner — `isPresented` + `selectedFileId`. Lives in `ChatView` as @State, exposed to chips and the sidebar through @Environment so the chip can drive navigation without prop-drilling. - `MarkdownSegments.fileChip(fileId, label)` is a new segment. A post- pass over prose segments splits out lines that match `[label](https://.../v1/files/UUID)` or bare URLs. Restricting to whole-line matches keeps the prose flow predictable instead of stamping chips into the middle of a sentence. - `FileDetailView` renders content by file kind: image via `AuthorizedAsyncImage` (.fit), text/code via UTF-8 decode in a monospaced scroll view (capped at 512 KB), HTML via the shared sandboxed `WebView`. PDF/audio/video/archive get a stub card for now. - `WebView` and `AuthorizedAsyncImage` extracted into `Helpers/` so both `HTMLBlockView` (in-chat) and `FileDetailView` (sidebar) can reuse them. `AuthorizedAsyncImage` gained a `contentMode:` parameter so thumbnails still cover-crop while the detail view uses fit. - New `GET /v1/files/{id}/info` wrapper on `WorkspaceAPI` for fetching authoritative metadata when we only have a file id from a chip. The sidebar's toggle button just opens the list (default). Chips open to a detail view directly. The back button on the detail header returns to the list without closing the sidebar.
Cuts a release on top of the four-fix UX wave (copy button, collapsed thinking, html block rendering, content sidebar + file chips). DMG artifact at packages/go/dist/OpenAgents Go-0.2.4-arm64.dmg.
Three small fixes on top of the chat UI wave:
1. Chat blank-on-load + sometimes-stuck-at-top
The empty-state guard treated "load in flight" identically to "no
messages here" — users saw the "say hi" copy during the network
round-trip, then a half-painted thread that only fully rendered
after manually scrolling up. Root cause was LazyVStack racing
ScrollViewReader.scrollTo("bottom-anchor"): the 1pt anchor sat at
the bottom of an essentially-empty content height because the rows
above hadn't been instantiated yet.
- Added `loadingHistory` to ChannelMessages; loadHistory sets it
before the await so the chat view can show a spinner.
- Replaced the racing onAppear scrollTo with `.defaultScrollAnchor(.bottom)`
— iOS 17 / macOS 14 primitive that anchors the bottom of content
to the bottom of the viewport across content-size changes. Also
makes "load older" prepends stop jumping the visible content.
- `.id(session.sessionId)` on the ScrollView so thread switches
give us a fresh ScrollView that re-applies the anchor cleanly.
2. ContentSidebar file detail: "Failed to decode response — data
couldn't be read because it is missing"
`GET /v1/files/{id}/info` returns the file metadata without a
`status` field (only the list endpoint does). The Swift model
required it. Made `status` optional so one model decodes both
endpoint shapes.
3. ContentSidebar resizable up to two columns
Added a draggable handle between the chat column and the sidebar
(resize-cursor on macOS via NSCursor.resizeLeftRight). Width is
clamped between `singleColumnWidth` (280pt) and `twoColumnWidth`
(560pt). Swapped the file list from LazyVStack to LazyVGrid with
`.adaptive(minimum: 240)`, so the layout switches to 2 columns
automatically once the user drags the sidebar wide enough.
zomux
approved these changes
May 12, 2026
Contributor
zomux
left a comment
There was a problem hiding this comment.
Clean, well-structured UI overhaul. WKWebView sandboxing is correct (JS disabled + baseURL nil). No bugs or security concerns. Minor nits (non-blocking): ISO8601DateFormatter could be shared/static, and byteString/relativeTime helpers are duplicated between FileCard and FileDetailView.
Merge origin/develop into the Go UI overhaul branch, keeping both the PR's new features (content sidebar, file chips, HTML blocks, copy button, collapse steps) and develop's additions (push notifications, slash commands, stop button, ComposerTextView, Firebase/ImageDownsampler). Version kept at 0.2.4 (PR's bump).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Native chat-surface UI overhaul for the iOS/macOS Go app. Six improvements ship together.
1. Copy button on agent messages
Small
Copy/Copiedbutton under each agent message that writes the raw markdown to the system pasteboard so output roundtrips to other markdown-aware tools without losing structure. Mirrors the React UI affordance.2. Collapse thinking steps to last 2
Long thinking / tool-call chains used to overwhelm the chat. Now only the most recent two steps render by default; earlier ones collapse behind a tappable
N earlier stepsheader that unrolls the full list. Rolling window — while the agent keeps producing steps, the last-2 view tracks the latest.3. Render fenced
```htmlblocks inlineNew
MarkdownSegment.htmlBlockvariant alongside.code/.table. The segmenter routes```htmlfences to it; the bubble renderer mounts them in a sandboxedWKWebView.WKPreferenceslevel — default is safe preview, not arbitrary script execution.color-schemeso embedded inputs/forms match the surrounding chat theme.xml,htm,code, etc. still render as code blocks — only the explicithtmltag opts into the live preview.4. Content sidebar listing workspace files
Right-hand
Contentpanel toggled from asidebar.righticon in the chat header (macOS) / top-bar toolbar (iOS). Lists every file in the workspace, most-recent first.NSCursor.resizeLeftRight. Width clamps between 280 pt (single column) and 560 pt (two columns).LazyVGridwith.adaptive(minimum: 240), so the layout switches to 2 columns automatically once the user drags the sidebar wide enough.AuthorizedAsyncImageloader (sinceAsyncImagecan't carry headers and/v1/files/{id}requiresX-Workspace-Token).Backend unchanged:
GET /v1/files?network=<id>&status=active&limit=100.5. Workspace-file chips + sidebar file detail
When an agent posts a link to a workspace file (
/v1/files/<id>) on its own line, it renders as a tappable pill instead of an inline anchor. Tapping the chip opens the Content sidebar focused on that file. Tapping a file in the sidebar list opens the same detail panel. A back chevron returns to the file list.ContentSidebarControlleris the shared state owner (isPresented+selectedFileId). Surfaced through@Environmentso chips drive navigation without prop-drilling.MarkdownSegments.fileChip(fileId, label)segment; a post-pass over prose segments splits out lines matching[label](https://.../v1/files/UUID)or bare URLs. Whole-line matches only — chips don't stamp into the middle of a sentence.FileDetailViewrenders content by kind: image viaAuthorizedAsyncImage(.fit), text/code via UTF-8 decode in a monospaced scroll view (capped at 512 KB), HTML via the shared sandboxedWebView. PDF/audio/video/archive get a stub card.WebViewandAuthorizedAsyncImageextracted intoHelpers/so bothHTMLBlockView(in-chat) andFileDetailView(sidebar) can reuse them.GET /v1/files/{id}/infowrapper for authoritative metadata when we only have an id from a chip. Returns metadata without astatusfield (only the list endpoint does), so the Swift model acceptsstatusas optional and decodes both endpoint shapes.6. Chat history scroll-to-bottom + loading state
History load used to leave the chat blank or stuck part-way down — users had to manually scroll up before the latest messages painted. The empty-state guard also treated "load in flight" identically to "no messages here", so threads showed the "say hi" copy during the network round-trip.
Root cause was
LazyVStackracingScrollViewReader.scrollTo("bottom-anchor"): the 1pt anchor sat at the bottom of an essentially-empty content height because rows above hadn't been instantiated yet.loadingHistorytoChannelMessages;loadHistorysets it before the await so the chat view can show a spinner..onAppear { scrollTo }with.defaultScrollAnchor(.bottom)— iOS 17 / macOS 14 primitive that anchors the bottom of content to the viewport across content-size changes. Also stops "load older" prepends from jumping visible content..id(session.sessionId)on the ScrollView so thread switches re-apply the anchor cleanly.Test plan
packages/go/dist/OpenAgents Go-0.2.4-arm64.dmg```htmlfence → renders as styled HTML; JS does NOT execute.