Skip to content

feat(chat): improved code selection attachments, inline undo/restore, and live-only event routing#7

Merged
chryzxc merged 1 commit into
mainfrom
feat/3-chat-attachments-undo-live-events
Jul 11, 2026
Merged

feat(chat): improved code selection attachments, inline undo/restore, and live-only event routing#7
chryzxc merged 1 commit into
mainfrom
feat/3-chat-attachments-undo-live-events

Conversation

@chryzxc

@chryzxc chryzxc commented Jul 11, 2026

Copy link
Copy Markdown
Owner

Summary

This PR delivers a comprehensive set of chat UX improvements across the VS Code extension host and React webview:

  • Code-selection attachment rendering with structured parts, attachment chips, and click-to-expand preview modal
  • Context menu "Add to OpenCode Thread" for editor selections, files, and folders (with recursion)
  • Undo/Revert with inline Restore — loading state, button swap, and persistence across reloads
  • Session error ordering — errors render as centralized transcript entries in tape order instead of a pinned top banner
  • Session error UI redesign — compact, full-width, non-dismissible bubble with per-turn rendering
  • Live-only event architecturesession.status, tui.show/tui.toast.show moved off centralized transcript onto a reusable live-only UI plumbing layer
  • File-context MIME fix — language IDs no longer sent as MIME types (server rejection fix)
  • Deferred SDK prompt data URI fix — code selections use data URI in both direct and deferred send paths (intermittent whole-file-read fix)
  • Synthetic user-text filtering — tool-call echoes and file dumps excluded from user message bubbles
  • System message classification — system messages render as full-width entries, not right-aligned user bubbles
  • Duplicate bubble reconciliation — pending optimistic overlay properly reconciled against canonical centralized message
  • Composer context chip — filename-only display with preserved line suffix, full path in metadata
  • Paste text attachment threshold — large text pastes convert to .txt even when clipboard has image items
  • Question popover agent fixpromptAsync fallback includes agent parameter
  • Custom answer gatingquestion.custom defaults to false instead of true

Detailed Changes

1. Code-Selection Attachment Rendering

Problem: When a user selected code (Ctrl+L / Cmd+L), the selection was sent to the AI as a flat {type:"text", text:"```lang\n// path:line\ncontent\n```"} part. The webview's stripping logic removed this markdown from the rendered bubble, so users saw their selection vanish — no attachment chip replaced it.

Solution:

  • Selections are now sent as structured {type:"file", source:{type:"file", path, text:{value,start,end}, lineInfo, languageId}} parts
  • url uses a data:text/plain;base64,... data URI (NOT a file URI) so the server can only access the exact selected lines, not the whole file
  • filename carries the full path plus line info (path:lineRange) for AI metadata
  • Webview renders a <FileIcon> attachment chip with filename:lineRange label
  • Click opens CodeSelectionPreviewModal with syntax-highlighted code (via highlight.js)
  • buildExplicitFileChipLabel() derives short basename labels from full-path metadata for display

Files:

  • src/providers/ChatViewProvider.ts — ctx.content branch (lines ~7079-7106)
  • webview/shared/src/chat/lib/types.tsCodeSelectionSource, CodeSelectionMessagePart, MessagePartSource
  • webview/shared/src/chat/MessageComponents.tsxisCodeSelectionPart, collectCodeSelectionsFromParts, parseLineRange, buildExplicitFileChipLabel, basenamePreservingLineSuffix, chip rendering, FileIcon integration
  • webview/shared/src/chat/CodeSelectionPreviewModal.tsxNEW FILE, mirrors ImagePreviewModal shell, uses highlight.js

2. Context Menu "Add to OpenCode Thread"

Problem: No right-click context menu to add files/selections/folders to the active chat thread.

Solution:

  • New command opencode.addToThread with editor/context and explorer/context menu contributions
  • Editor: if selection exists → sends selection with line info; if no selection → sends whole file reference
  • Explorer: files added directly; folders recursively scanned via vscode.workspace.findFiles with MAX_FILES=50 cap and exclusion patterns (node_modules, .git, dist, build, etc.)

Files:

  • package.json — command + menu contributions
  • src/extension.tsopencode.addToThread handler (~line 428)

3. Undo/Revert with Inline Restore

Problem: The undo button had no loading state, the "Changes from this message were reverted" banner was pinned at the top (not inline), and revert state didn't persist across reloads.

Solution:

  • Loading state: isUndoing local state shows <Loader2 className="animate-spin"> + "Undoing..." text while revert is in-flight. Cleared on revertStateUpdate message (success or failure).
  • Inline Restore: After successful revert, Undo button is replaced inline by a Restore button (<RotateCcw> icon). revertedMessageId tracks which message was reverted; when it matches undoMessageId, the swap happens.
  • Removed top banner: The pinned "Changes from this message were reverted" block is deleted from ChatShell.tsx.
  • Persistence: New syncRevertStateFromServer(sessionId) method fetches session.revert from the server during handleLoadSession and posts revertStateUpdate to the webview. On reload/session switch, the Restore button persists.
  • Error handling: v2 SDK ThrowOnError=false pattern — result.error checked explicitly before accessing result.data.
  • messageId fix: CentralizedSessionDiffEvent now carries messageId extracted from session.diff and message.updated events, so the undo button targets the correct message (not the last in block).

Files:

  • src/providers/ChatViewProvider.tshandleUndoMessageChanges, handleUnrevertSession, syncRevertStateFromServer, handleLoadSession call site
  • webview/shared/src/chat/MessageComponents.tsxFileChangesSection undo/restore UI
  • webview/shared/src/chat/ChatShell.tsx — banner removed
  • webview/shared/src/chat/lib/types.tsCentralizedSessionDiffEvent.messageId
  • webview/shared/src/chat/ChatShell.tsxparseCentralizedSessionDiffEvent messageId extraction

4. Session Error Ordering

Problem: Session errors rendered as a pinned top block, not in transcript order. Identical error text collapsed across separate errored turns.

Solution:

  • Session errors are now centralized transcript entries (kind: "session.error") ordered from the raw event tape via parseCentralizedSessionErrorEvent()
  • Dedup fingerprint includes event identity (id, rawIndex) so repeated identical error text still renders for each errored turn
  • Generic fallback errors (Model did not produce structured output, Unexpected server error, etc.) are suppressed when specific errors exist via isGenericSessionErrorMessage()
  • Error message extraction prefers nested error.data.message over top-level message

Files:

  • webview/shared/src/chat/ChatShell.tsxextractSessionErrorMessage, parseCentralizedSessionErrorEvent, ConversationRenderEntry union, buildCentralizedTranscriptProjection, inline transcript renderer

5. Session Error UI Redesign

Problem: Error card was a heavy red alert panel with glow effects, close button, and explanatory text.

Solution:

  • Compact, full-width row: w-full rounded-[14px] border px-3 py-2.5
  • No close button (errors are not dismissible — they're part of the transcript)
  • No glow/gradient/backdrop-blur effects
  • Flat surface using color-mix(in srgb, var(--vscode-errorForeground) 6%, var(--oc-panel-soft))
  • No explanatory sentence — just the error message directly
  • Aligned with other chat components (no outer horizontal padding)

Files:

  • webview/shared/src/chat/ChatShell.tsx — session.error render branch in MemoizedConversationTranscript

6. Live-Only Event Architecture

Problem: session.status and tui.show/tui.toast.show events were being inserted into centralized transcript data and rendered as transcript rows. The user wanted them as temporary live-only UI.

Solution:

  • NEW FILE webview/shared/src/chat/lib/liveEventRouter.ts — canonical discoverable routing table mapping live-only event types to their UI destinations:
    • tui.toast.show, tui.show"toast" destination
    • session.status"session-status" destination
  • centralizedDebugPayloadFilter.ts updated: session.status, tui.show added to excluded-path rules as "live-only" disposition. Normalizer now checks syncEvent.data.type paths.
  • messageHandler.ts refactored: single routeLiveEvent() entry point replaces scattered inline parsing. Critical fix: centralized tape append gate changed from !== "excluded-noise" to === "persist" — live-only events no longer enter centralized raw state.
  • StreamingState gained liveSessionStatus field; UPDATE_LIVE_SESSION_STATUS action added.
  • StreamingCard visibility extended to status-only states.
  • ResponseMessageInner renders temporary retry/busy/countdown row from live streaming state.
  • toastEvents.ts broadened: accepts tui.show and tui.toast.show, handles deeply wrapped payloads (syncEvent.data.type, nested properties, alternate field aliases).
  • All centralized session.status transcript code removed from ChatShell.tsx.

Files:

  • webview/shared/src/chat/lib/liveEventRouter.tsNEW FILE
  • src/shared/centralizedDebugPayloadFilter.ts — exclusion rules + normalizer fix + export
  • webview/shared/src/chat/lib/generated/centralizedDebugPayloadFilter.ts — synced copy
  • webview/shared/src/chat/lib/messageHandler.ts — routeLiveEvent + persist-only gate
  • webview/shared/src/chat/lib/store.tsUPDATE_LIVE_SESSION_STATUS reducer + streaming merge
  • webview/shared/src/chat/lib/types.tsStreamingState.liveSessionStatus
  • webview/shared/src/chat/lib/toastEvents.ts — broadened parser + LiveSessionStatus type
  • webview/shared/src/chat/StreamingComponents.tsxshouldShowStreamingCard includes liveSessionStatus
  • webview/shared/src/chat/MessageComponents.tsx — temporary status row in ResponseMessageInner
  • webview/shared/src/chat/ChatShell.tsx — centralized session.status code removed

7. File-Context MIME Fix

Problem: Language IDs (markdown, typescript) were being sent as file MIME types, causing server rejection: 'file part media type markdown' functionality not supported.

Solution: All six MIME-related lines in ChatViewProvider.ts now use mime: "text/plain" (except image data URLs which extract MIME from the data: prefix).

Files:

  • src/providers/ChatViewProvider.ts — direct path (3 branches) + deferred path

8. Deferred SDK Prompt Data URI Fix

Problem: The deferred SDK prompt path (buildDeferredSdkPrompt(), used when an assistant turn is active) sent uri: filePath (raw file path) for code selections. The server resolved the file from disk and read the whole file. This caused intermittent behavior: direct path worked, deferred path didn't.

Solution: Deferred path now mirrors direct path — constructs data:text/plain;base64,... data URI when content exists, with proper source shape (type: "file", path, text, lineInfo, languageId) and nameWithLine metadata.

Files:

  • src/providers/ChatViewProvider.tsbuildDeferredSdkPrompt() context loop

9. Synthetic User-Text Filtering

Problem: Synthetic tool-call echoes (Called the Read tool with the following input: ...) and file dumps (<path>...<content>...</content>) polluted user message bubbles and caused duplicate optimistic overlay bubbles.

Solution:

  • buildVisibleUserMessageText() in ChatShell.tsx adds .filter((part) => part?.synthetic !== true) before extracting text from parts
  • isRenderableUserTextPart() in MessageComponents.tsx checks part.synthetic === true and returns false for user role
  • messageBodyFromParts() now accepts role? parameter for role-aware filtering

Files:

  • webview/shared/src/chat/ChatShell.tsxbuildVisibleUserMessageText
  • webview/shared/src/chat/MessageComponents.tsxisRenderableUserTextPart, messageBodyFromParts

10. System Message Classification

Problem: System messages arriving via message.part.updated (without a matching message.updated event) had no role registered, defaulted to "user", and rendered as right-aligned user bubbles.

Solution:

  • Centralized builder now registers roles from message.part.updated events (not just message.updated)
  • Content-based fallback looksLikeStandaloneSystemMessage() routes messages starting with [system...], <tag>, or <!-- to system descriptors

Files:

  • webview/shared/src/chat/ChatShell.tsx — role registration + content fallback

11. Duplicate Bubble Reconciliation

Problem: The optimistic pending overlay bubble ("read this line") survived alongside the canonical attachment-bearing user message, creating a duplicate.

Root cause: Pending reconciliation compared the optimistic text against the canonical message's content/text, which was polluted by synthetic parts. The comparison failed, so the overlay was never removed.

Solution: Structural synthetic filtering (.synthetic !== true) ensures the canonical visible text matches the optimistic text, so reconciliation succeeds.

Files:

  • webview/shared/src/chat/ChatShell.tsxbuildVisibleUserMessageText

12. Composer Context Chip

Problem: Composer chip showed the full file path, taking up too much space and hiding the line range.

Solution:

  • contextChipDisplayParts() helper splits path into displayName (last segment) + lineSuffix (:N or :N-M)
  • Chip renders filename (truncated to 160px) + line suffix (shrink-0, never truncated)
  • Full path preserved in metadata for AI; UI shows basename only
  • basenamePreservingLineSuffix() derives short labels from full-path metadata

Files:

  • webview/shared/src/chat/PanelComponents.tsxcontextChipDisplayParts, chip renderer
  • webview/shared/src/chat/MessageComponents.tsxbuildExplicitFileChip, basenamePreservingLineSuffix

13. Paste Text Attachment Threshold

Problem: Large text pastes (≥2000 chars) showed as image-1 instead of .txt when the clipboard also contained an image item (common for IDE/browser rich-text copies).

Solution: Removed the !Array.from(items).some((it) => it.type.startsWith("image/")) condition. Large text pastes now always convert to .txt regardless of other clipboard representations.

Files:

  • webview/shared/src/chat/PanelComponents.tsxhandlePaste

14. Question Popover Agent Fix

Problem: Answering a stale popover question via custom answer caused Error: default agent "Sisyphus - Ultraworker" not found because the promptAsync fallback omitted the agent parameter.

Solution: promptAsync call now includes agent: this.modelAndAgentManager.getSelectedAgent().

Files:

  • src/providers/ChatViewProvider.ts — stale-question fallback in questionReply handler

15. Custom Answer Gating

Problem: question.custom defaulted to true, showing "Custom Answer..." field even when the server didn't explicitly enable it.

Solution: Changed asBoolean(question.custom, true)asBoolean(question.custom, false).

Files:

  • webview/shared/src/chat/lib/messageHandler.tsinteractiveEventsFromQuestionAskedPayload

Closes #5

Summary by CodeRabbit

  • New Features
    • Added “OpenCode: Add to Thread” from the editor and explorer context menus, supporting selections, files, and folders.
    • Added code-selection chips with syntax-highlighted preview modals.
    • Added live session status, toast notifications, and in-chat session error displays.
    • Added Restore controls for reverted message changes.
  • Bug Fixes
    • Improved file and selection context handling, duplicate message prevention, attachment labeling, and large-text paste behavior.
    • Preserved selected agents when resuming question responses and corrected custom-input defaults.

@chryzxc chryzxc merged commit 58640c9 into main Jul 11, 2026
1 check failed
@coderabbitai

coderabbitai Bot commented Jul 11, 2026

Copy link
Copy Markdown

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: e83b8566-b1e6-4282-b91c-a5e669ec1b49

📥 Commits

Reviewing files that changed from the base of the PR and between d67969c and 3109677.

⛔ Files ignored due to path filters (1)
  • webview/shared/src/chat/lib/generated/centralizedDebugPayloadFilter.ts is excluded by !**/generated/**
📒 Files selected for processing (38)
  • package.json
  • src/extension.ts
  • src/providers/ChatViewProvider.ts
  • src/shared/centralizedDebugPayloadFilter.ts
  • tests/integration/code-selection-flow.test.mjs
  • tests/integration/duplicate-bubble-reconciliation.test.mjs
  • tests/providers/add-to-thread-command.test.mjs
  • tests/providers/question-reply-agent-and-custom-answer.test.mjs
  • tests/providers/session-revert-handler.test.mjs
  • tests/providers/session-unrevert-handler.test.mjs
  • tests/unit/centralized-filter-normalizer-paths.test.mjs
  • tests/webview/activity-timeline-attached-payloads-regression.test.mjs
  • tests/webview/assistant-header-thinking-variant.test.mjs
  • tests/webview/assistant-message-collapsed-logic.test.mjs
  • tests/webview/centralized-render-source-of-truth-regression.test.mjs
  • tests/webview/code-selection-preview-modal.test.mjs
  • tests/webview/code-selection-rendering.test.mjs
  • tests/webview/file-changes-undo-message-id.test.mjs
  • tests/webview/file-chip-rendering.test.mjs
  • tests/webview/live-event-router-contract.test.mjs
  • tests/webview/message-handler-error-surfacing.test.mjs
  • tests/webview/mime-normalization.test.mjs
  • tests/webview/paste-text-attachment-threshold.test.mjs
  • tests/webview/revert-state-banner.test.mjs
  • tests/webview/revert-undo-restore-ui.test.mjs
  • tests/webview/selected-context-chip-label.test.mjs
  • tests/webview/session-error-banner.test.mjs
  • tests/webview/session-status-rendering.test.mjs
  • webview/shared/src/chat/ChatShell.tsx
  • webview/shared/src/chat/CodeSelectionPreviewModal.tsx
  • webview/shared/src/chat/MessageComponents.tsx
  • webview/shared/src/chat/PanelComponents.tsx
  • webview/shared/src/chat/StreamingComponents.tsx
  • webview/shared/src/chat/lib/liveEventRouter.ts
  • webview/shared/src/chat/lib/messageHandler.ts
  • webview/shared/src/chat/lib/store.ts
  • webview/shared/src/chat/lib/toastEvents.ts
  • webview/shared/src/chat/lib/types.ts

📝 Walkthrough

Walkthrough

This change adds an “Add to Thread” command, structured code-selection context and previews, live event routing and status rendering, revert/restore state synchronization, centralized session-error rendering, and regression coverage for related chat behaviors.

Changes

Chat context and rendering flow

Layer / File(s) Summary
Add-to-thread and code-selection context
package.json, src/extension.ts, src/providers/ChatViewProvider.ts, webview/shared/src/chat/CodeSelectionPreviewModal.tsx, webview/shared/src/chat/MessageComponents.tsx, webview/shared/src/chat/lib/types.ts, tests/integration/*, tests/providers/add-to-thread-command.test.mjs, tests/webview/code-selection-*.test.mjs
Registers opencode.addToThread, attaches editor or explorer files to chat, transports selections as structured file parts, and renders clickable code-selection previews.
Live event routing and streaming state
src/shared/centralizedDebugPayloadFilter.ts, webview/shared/src/chat/lib/{liveEventRouter,messageHandler,store,toastEvents,types}.ts, webview/shared/src/chat/{MessageComponents,StreamingComponents}.tsx, tests/unit/centralized-filter-normalizer-paths.test.mjs, tests/webview/{live-event-router-contract,session-status-rendering,message-handler-error-surfacing}.test.mjs
Normalizes live events, routes toasts and session status outside transcript persistence, and keeps status-only streaming cards visible.
Revert and restore state flow
src/providers/ChatViewProvider.ts, webview/shared/src/chat/{ChatShell,MessageComponents}.tsx, webview/shared/src/chat/lib/{messageHandler,store,types}.ts, tests/providers/session-*-handler.test.mjs, tests/webview/revert-*.test.mjs
Synchronizes server revert metadata, supports undo and restore actions, and updates webview state and controls during revert operations.
Transcript projection and assistant scoping
webview/shared/src/chat/{ChatShell,MessageComponents}.tsx, tests/integration/duplicate-bubble-reconciliation.test.mjs, tests/webview/{activity-timeline-attached-payloads-regression,assistant-*,centralized-render-source-of-truth-regression,session-error-banner}.test.mjs
Preserves message parts, filters synthetic user content, scopes assistant metadata by semantic message IDs, and renders ordered centralized session errors.
Supporting chat UX
src/providers/ChatViewProvider.ts, webview/shared/src/chat/PanelComponents.tsx, tests/providers/question-reply-agent-and-custom-answer.test.mjs, tests/webview/{mime-normalization,paste-text-attachment-threshold,selected-context-chip-label,file-chip-rendering}.test.mjs
Updates question-agent resumption, prompt MIME and path metadata, large-text paste handling, and context/file-chip labels.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Editor
  participant Extension
  participant ChatViewProvider
  participant Webview
  Editor->>Extension: invoke opencode.addToThread
  Extension->>ChatViewProvider: addContext with file or selection
  ChatViewProvider->>Webview: update chat context
  Extension->>Webview: focus opencode.chatView
Loading
sequenceDiagram
  participant Stream
  participant MessageHandler
  participant LiveEventRouter
  participant Store
  participant ChatComponents
  Stream->>MessageHandler: streamEvent or streamEventBatch
  MessageHandler->>LiveEventRouter: routeLiveEvent(payload)
  LiveEventRouter-->>MessageHandler: toast or session status
  MessageHandler->>Store: dispatch live state update
  Store-->>ChatComponents: render live status
Loading
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/3-chat-attachments-undo-live-events

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 Biome (2.5.1)
tests/providers/session-revert-handler.test.mjs

File contains syntax errors that prevent linting: Line 148: expected , but instead found test; Line 162: expected , but instead found ;

tests/webview/message-handler-error-surfacing.test.mjs

File contains syntax errors that prevent linting: Line 168: expected , but instead found test; Line 181: expected , but instead found ;

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

tests/providers/session-revert-handler.test.mjs

Parsing error: ',' expected.

tests/webview/message-handler-error-surfacing.test.mjs

Parsing error: ',' expected.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

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.

new feature add/pin line selection to context and revert/checkpoint

1 participant