Skip to content

feat(go): chat UI overhaul — copy, collapse, html blocks, content sidebar + chips#378

Merged
zomux merged 8 commits into
openagents-org:developfrom
baryhuang:fix/go-ui
May 12, 2026
Merged

feat(go): chat UI overhaul — copy, collapse, html blocks, content sidebar + chips#378
zomux merged 8 commits into
openagents-org:developfrom
baryhuang:fix/go-ui

Conversation

@baryhuang
Copy link
Copy Markdown
Collaborator

@baryhuang baryhuang commented May 12, 2026

Native chat-surface UI overhaul for the iOS/macOS Go app. Six improvements ship together.

1. Copy button on agent messages

Small Copy / Copied button 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 steps header that unrolls the full list. Rolling window — while the agent keeps producing steps, the last-2 view tracks the latest.

3. Render fenced ```html blocks inline

New MarkdownSegment.htmlBlock variant alongside .code / .table. The segmenter routes ```html fences to it; the bubble renderer mounts them in a sandboxed WKWebView.

  • JavaScript disabled at the WKPreferences level — default is safe preview, not arbitrary script execution.
  • Document scaffold sets color-scheme so embedded inputs/forms match the surrounding chat theme.
  • Reported height capped at 400 pt with internal scroll so a long artifact doesn't blow out the chat scroll position.
  • xml, htm, code, etc. still render as code blocks — only the explicit html tag opts into the live preview.

4. Content sidebar listing workspace files

Right-hand Content panel toggled from a sidebar.right icon in the chat header (macOS) / top-bar toolbar (iOS). Lists every file in the workspace, most-recent first.

  • Default off. Side-by-side on macOS / iPad regular hsize; slides up as a sheet on iPhone compact since the sidebar would otherwise crush the chat.
  • Resizable. Drag the handle between chat and sidebar to widen; resize cursor on macOS via NSCursor.resizeLeftRight. Width clamps between 280 pt (single column) and 560 pt (two columns).
  • File list is a LazyVGrid with .adaptive(minimum: 240), so the layout switches to 2 columns automatically once the user drags the sidebar wide enough.
  • Image files render a thumbnail via a small AuthorizedAsyncImage loader (since AsyncImage can't carry headers and /v1/files/{id} requires X-Workspace-Token).
  • Non-image kinds get a tinted badge + SF Symbol so the type is legible at a glance.

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.

  • ContentSidebarController is the shared state owner (isPresented + selectedFileId). Surfaced through @Environment so 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.
  • FileDetailView renders content by 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.
  • WebView and AuthorizedAsyncImage extracted into Helpers/ so both HTMLBlockView (in-chat) and FileDetailView (sidebar) can reuse them.
  • New GET /v1/files/{id}/info wrapper for authoritative metadata when we only have an id from a chip. Returns metadata without a status field (only the list endpoint does), so the Swift model accepts status as 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 LazyVStack racing ScrollViewReader.scrollTo("bottom-anchor"): the 1pt anchor sat at the bottom of an essentially-empty content height because 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 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

  • iOS Debug clean
  • macOS Debug clean
  • macOS Release DMG built — packages/go/dist/OpenAgents Go-0.2.4-arm64.dmg
  • Manual: open a long agent message → tap Copy → paste into Bear/Notion → markdown preserved.
  • Manual: trigger a long thinking run (>5 steps) → only last 2 visible by default → tap header → full history expands.
  • Manual: agent posts a ```html fence → renders as styled HTML; JS does NOT execute.
  • Manual: toggle Content sidebar → file list appears, sorted recent first; image files show thumbnails; drag the left edge → sidebar grows; at ~560pt the grid becomes 2 columns.
  • Manual: agent posts a markdown link to a workspace file → renders as a pill → tap → sidebar opens to detail view; back chevron returns to list.
  • Manual: switch threads → chat lands at the bottom of the new thread on first paint, not stuck at top; spinner shows during load instead of "say hi" copy.
  • Manual on iPhone: toggle Content from toolbar → sheet slides up rather than crushing the chat.

baryhuang added 4 commits May 12, 2026 11:26
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.
@vercel
Copy link
Copy Markdown

vercel Bot commented May 12, 2026

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

baryhuang added 3 commits May 12, 2026 11:40
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.
@baryhuang baryhuang changed the title feat(go): four chat UI improvements (copy, collapse, html, content sidebar) feat(go): chat UI overhaul — copy, collapse, html blocks, content sidebar + chips May 12, 2026
Copy link
Copy Markdown
Contributor

@zomux zomux left a comment

Choose a reason for hiding this comment

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

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).
@zomux zomux merged commit 8a52f78 into openagents-org:develop May 12, 2026
1 of 2 checks passed
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.

3 participants