feat(chat): improved code selection attachments, inline undo/restore, and live-only event routing#7
Conversation
|
Caution Review failedThe pull request is closed. ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: ⛔ Files ignored due to path filters (1)
📒 Files selected for processing (38)
📝 WalkthroughWalkthroughThis 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. ChangesChat context and rendering flow
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
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
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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.mjsFile contains syntax errors that prevent linting: Line 148: expected tests/webview/message-handler-error-surfacing.test.mjsFile contains syntax errors that prevent linting: Line 168: expected 🔧 ESLint
tests/providers/session-revert-handler.test.mjsParsing error: ',' expected. tests/webview/message-handler-error-surfacing.test.mjsParsing 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. Comment |
Summary
This PR delivers a comprehensive set of chat UX improvements across the VS Code extension host and React webview:
session.status,tui.show/tui.toast.showmoved off centralized transcript onto a reusable live-only UI plumbing layer.txteven when clipboard has image itemspromptAsyncfallback includes agent parameterquestion.customdefaults tofalseinstead oftrueDetailed 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:
{type:"file", source:{type:"file", path, text:{value,start,end}, lineInfo, languageId}}partsurluses adata:text/plain;base64,...data URI (NOT a file URI) so the server can only access the exact selected lines, not the whole filefilenamecarries the full path plus line info (path:lineRange) for AI metadata<FileIcon>attachment chip withfilename:lineRangelabelCodeSelectionPreviewModalwith syntax-highlighted code (viahighlight.js)buildExplicitFileChipLabel()derives short basename labels from full-path metadata for displayFiles:
src/providers/ChatViewProvider.ts— ctx.content branch (lines ~7079-7106)webview/shared/src/chat/lib/types.ts—CodeSelectionSource,CodeSelectionMessagePart,MessagePartSourcewebview/shared/src/chat/MessageComponents.tsx—isCodeSelectionPart,collectCodeSelectionsFromParts,parseLineRange,buildExplicitFileChipLabel,basenamePreservingLineSuffix, chip rendering,FileIconintegrationwebview/shared/src/chat/CodeSelectionPreviewModal.tsx— NEW FILE, mirrorsImagePreviewModalshell, useshighlight.js2. Context Menu "Add to OpenCode Thread"
Problem: No right-click context menu to add files/selections/folders to the active chat thread.
Solution:
opencode.addToThreadwitheditor/contextandexplorer/contextmenu contributionsvscode.workspace.findFileswith MAX_FILES=50 cap and exclusion patterns (node_modules,.git,dist,build, etc.)Files:
package.json— command + menu contributionssrc/extension.ts—opencode.addToThreadhandler (~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:
isUndoinglocal state shows<Loader2 className="animate-spin">+ "Undoing..." text while revert is in-flight. Cleared onrevertStateUpdatemessage (success or failure).<RotateCcw>icon).revertedMessageIdtracks which message was reverted; when it matchesundoMessageId, the swap happens.ChatShell.tsx.syncRevertStateFromServer(sessionId)method fetchessession.revertfrom the server duringhandleLoadSessionand postsrevertStateUpdateto the webview. On reload/session switch, the Restore button persists.ThrowOnError=falsepattern —result.errorchecked explicitly before accessingresult.data.CentralizedSessionDiffEventnow carriesmessageIdextracted fromsession.diffandmessage.updatedevents, so the undo button targets the correct message (not the last in block).Files:
src/providers/ChatViewProvider.ts—handleUndoMessageChanges,handleUnrevertSession,syncRevertStateFromServer,handleLoadSessioncall sitewebview/shared/src/chat/MessageComponents.tsx—FileChangesSectionundo/restore UIwebview/shared/src/chat/ChatShell.tsx— banner removedwebview/shared/src/chat/lib/types.ts—CentralizedSessionDiffEvent.messageIdwebview/shared/src/chat/ChatShell.tsx—parseCentralizedSessionDiffEventmessageId extraction4. 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:
kind: "session.error") ordered from the raw event tape viaparseCentralizedSessionErrorEvent()id,rawIndex) so repeated identical error text still renders for each errored turnModel did not produce structured output,Unexpected server error, etc.) are suppressed when specific errors exist viaisGenericSessionErrorMessage()error.data.messageover top-levelmessageFiles:
webview/shared/src/chat/ChatShell.tsx—extractSessionErrorMessage,parseCentralizedSessionErrorEvent,ConversationRenderEntryunion,buildCentralizedTranscriptProjection, inline transcript renderer5. Session Error UI Redesign
Problem: Error card was a heavy red alert panel with glow effects, close button, and explanatory text.
Solution:
w-full rounded-[14px] border px-3 py-2.5color-mix(in srgb, var(--vscode-errorForeground) 6%, var(--oc-panel-soft))Files:
webview/shared/src/chat/ChatShell.tsx— session.error render branch inMemoizedConversationTranscript6. Live-Only Event Architecture
Problem:
session.statusandtui.show/tui.toast.showevents were being inserted into centralized transcript data and rendered as transcript rows. The user wanted them as temporary live-only UI.Solution:
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"destinationsession.status→"session-status"destinationcentralizedDebugPayloadFilter.tsupdated:session.status,tui.showadded to excluded-path rules as"live-only"disposition. Normalizer now checkssyncEvent.data.typepaths.messageHandler.tsrefactored: singlerouteLiveEvent()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.StreamingStategainedliveSessionStatusfield;UPDATE_LIVE_SESSION_STATUSaction added.StreamingCardvisibility extended to status-only states.ResponseMessageInnerrenders temporary retry/busy/countdown row from live streaming state.toastEvents.tsbroadened: acceptstui.showandtui.toast.show, handles deeply wrapped payloads (syncEvent.data.type, nested properties, alternate field aliases).session.statustranscript code removed fromChatShell.tsx.Files:
webview/shared/src/chat/lib/liveEventRouter.ts— NEW FILEsrc/shared/centralizedDebugPayloadFilter.ts— exclusion rules + normalizer fix + exportwebview/shared/src/chat/lib/generated/centralizedDebugPayloadFilter.ts— synced copywebview/shared/src/chat/lib/messageHandler.ts— routeLiveEvent + persist-only gatewebview/shared/src/chat/lib/store.ts—UPDATE_LIVE_SESSION_STATUSreducer + streaming mergewebview/shared/src/chat/lib/types.ts—StreamingState.liveSessionStatuswebview/shared/src/chat/lib/toastEvents.ts— broadened parser +LiveSessionStatustypewebview/shared/src/chat/StreamingComponents.tsx—shouldShowStreamingCardincludes liveSessionStatuswebview/shared/src/chat/MessageComponents.tsx— temporary status row inResponseMessageInnerwebview/shared/src/chat/ChatShell.tsx— centralized session.status code removed7. 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.tsnow usemime: "text/plain"(except image data URLs which extract MIME from the data: prefix).Files:
src/providers/ChatViewProvider.ts— direct path (3 branches) + deferred path8. Deferred SDK Prompt Data URI Fix
Problem: The deferred SDK prompt path (
buildDeferredSdkPrompt(), used when an assistant turn is active) senturi: 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 whencontentexists, with proper source shape (type: "file",path,text,lineInfo,languageId) andnameWithLinemetadata.Files:
src/providers/ChatViewProvider.ts—buildDeferredSdkPrompt()context loop9. 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()inChatShell.tsxadds.filter((part) => part?.synthetic !== true)before extracting text from partsisRenderableUserTextPart()inMessageComponents.tsxcheckspart.synthetic === trueand returns false for user rolemessageBodyFromParts()now acceptsrole?parameter for role-aware filteringFiles:
webview/shared/src/chat/ChatShell.tsx—buildVisibleUserMessageTextwebview/shared/src/chat/MessageComponents.tsx—isRenderableUserTextPart,messageBodyFromParts10. System Message Classification
Problem: System messages arriving via
message.part.updated(without a matchingmessage.updatedevent) had no role registered, defaulted to"user", and rendered as right-aligned user bubbles.Solution:
message.part.updatedevents (not justmessage.updated)looksLikeStandaloneSystemMessage()routes messages starting with[system...],<tag>, or<!--to system descriptorsFiles:
webview/shared/src/chat/ChatShell.tsx— role registration + content fallback11. 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.tsx—buildVisibleUserMessageText12. 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 intodisplayName(last segment) +lineSuffix(:Nor:N-M)basenamePreservingLineSuffix()derives short labels from full-path metadataFiles:
webview/shared/src/chat/PanelComponents.tsx—contextChipDisplayParts, chip rendererwebview/shared/src/chat/MessageComponents.tsx—buildExplicitFileChip,basenamePreservingLineSuffix13. Paste Text Attachment Threshold
Problem: Large text pastes (≥2000 chars) showed as
image-1instead of.txtwhen 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.txtregardless of other clipboard representations.Files:
webview/shared/src/chat/PanelComponents.tsx—handlePaste14. Question Popover Agent Fix
Problem: Answering a stale popover question via custom answer caused
Error: default agent "Sisyphus - Ultraworker" not foundbecause thepromptAsyncfallback omitted theagentparameter.Solution:
promptAsynccall now includesagent: this.modelAndAgentManager.getSelectedAgent().Files:
src/providers/ChatViewProvider.ts— stale-question fallback inquestionReplyhandler15. Custom Answer Gating
Problem:
question.customdefaulted totrue, 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.ts—interactiveEventsFromQuestionAskedPayloadCloses #5
Summary by CodeRabbit