Files
openchamber/packages/ui/src/components/chat/message/parts/DOCUMENTATION.md
T

7.9 KiB

Chat Message Parts: Rendering Architecture

This folder contains renderers for chat message parts (text, tools, reasoning, placeholders) and shared tool presentation helpers.

Use this doc when you ask an agent to change tool/header/description behavior.

High-level flow

  • Message parts are rendered from MessageBody.tsx.
  • There are two tool rendering paths:
    • Static grouped tools -> StaticToolRow in ProgressiveGroup.tsx
    • Expandable tools -> ToolPart.tsx
  • Shared tool icon mapping is centralized in toolPresentation.tsx (getToolIcon).

Which file controls what

  • ProgressiveGroup.tsx

    • Renders grouped Activity rows and grouped static tools.
    • Contains StaticToolRow.
    • Contains static tool short description logic (getToolShortDescription).
    • If you want to change how read/grep/perplexity/webfetch/... look in compact/grouped mode, edit here.
  • ToolPart.tsx

    • Renders expandable tool rows (bash/edit/write/question/task + fallback).
    • Controls expandable header title/description/diff stats/timer and expanded output body.
    • If you want to change expandable tool layout, edit here.
  • taskToolModel.ts

    • Owns Task metadata parsing and child-session summary projection.
    • part.state.metadata.sessionId is the only live identity contract between a Task and its child session.
    • A running Task may briefly have no sessionId; render it as waiting until the authoritative part update arrives. Never match parallel children by order, title, timestamp, or status.
    • Part-level metadata and output parsing exist only for older persisted records and never override state metadata.
  • toolPresentation.tsx

    • Shared icon mapping for tool names (getToolIcon).
    • Used by both ProgressiveGroup.tsx and ToolPart.tsx.
  • toolRenderUtils.ts

    • Core classification helpers:
      • isExpandableTool
      • isStaticTool
      • isStandaloneTool
      • getStaticGroupToolName
    • If a tool should switch between static vs expandable, change it here.
  • ReasoningPart.tsx

    • Thinking block UI (ReasoningTimelineBlock), summary + optional duration.
  • JustificationBlock.tsx

    • Justification block wrapper over ReasoningTimelineBlock.

Current important behavior

  • Assistant markdown treats raw HTML as inert visible text. The final generated HTML is sanitized as defense in depth, with script and style elements forbidden, so message content cannot inject active DOM or application-wide CSS into any runtime surface.
  • Final assistant Markdown rendering is independent from image gallery extraction: gallery presence never changes the chat body. Assistant image syntax consistently renders as a shared image icon followed by its filename, without loading the image in the body; tool and simple Markdown retain normal inline image rendering. The gallery separately collects HTTP(S), embedded, and workspace-local PNG/JPEG/GIF/WebP image candidates into one 100px thumbnail gallery in the message-completion area after all message text and above the turn's changed files. Each muted filename caption includes the shared image-file icon. HTTP(S) images keep their browser URL. Embedded and workspace-local images are limited to 10 MiB and validated as PNG/JPEG/GIF/WebP. Chat Markdown uses the assistant image-label policy without gallery-specific link rewriting, completion-state switching, or hidden placeholders. A completed assistant message hydrates at most 12 unique image candidates, including persisted text parts that omit their optional part-level end time. In server-backed runtimes, a gallery approaching the viewport prepares all local candidates in one message-level request, then reuses the authenticated /api/fs/raw asset route. Each URL loads only when its thumbnail approaches the viewport. VS Code instead loads workspace-contained images through its local filesystem bridge and never calls the server grant route; OpenCode temporary-directory images remain unsupported there. Mounted historical messages therefore do not eagerly read every image. Gallery clicks do not introduce or alter preview chrome: desktop and mobile both reuse the pre-existing attachment image preview overlay. Workspace-external images receive the existing path-bound outsideFileGrant only when the server verifies the exact source in the owning assistant message and the real file is inside OpenCode's dedicated temporary directory.
  • read and skill are static navigation tools and render via StaticToolRow.
  • Every other tool, including search/fetch, OpenCode built-ins, custom tools, plugins, and MCP tools, is expandable and renders through ToolPart.
  • The managed openchamber plugin tool uses the expandable path and hides its broad protocol input. The plugin supplies the selected action's human description as the native tool title; the UI renders that metadata without owning an action map. The full versioned result envelope renders through the same neutral JSON summary/tree/raw views as other tools, without a tool-specific output card.
  • ToolPart defers expanded content after a user toggle, preventing large tool input/output payloads from mounting during the initial chat render.
  • The rich tool diff preview lives in ToolPartDiffPreview.tsx and is lazy-loaded from ToolPart. It is the only tool-card piece that imports the @pierre/diffs + Shiki rendering stack, keeping that stack out of the eager chat startup graph. While its chunk loads (first rendered diff only) the plain-text patch from PlainDiffFallback.tsx renders as the Suspense fallback, mirroring the preview's error fallback. ToolPart itself must not statically import @pierre/diffs runtime modules or @/lib/shiki/appThemeRegistry.
  • Running bash output falls back to state.metadata.output until canonical state.output arrives. Its fixed-height output viewport follows new output until the user scrolls up, then resumes following when the user returns to the bottom. Live output appends or replaces rewritten snapshots as plain text without worker highlighting; finalized output normalizes ANSI terminal controls with a bounded synthetic-cell budget, bypasses the throttle, and receives the normal one-time highlighted rendering.
  • Thinking/Justification duration is hidden in sorted mode (handled in ReasoningPart.tsx + JustificationBlock.tsx).

"I want to change description for Perplexity" (example recipe)

If task is: "change text shown near Read or Skill in compact mode":

  1. Edit ProgressiveGroup.tsx -> getToolShortDescription(activity).
  2. Update the branch that handles read or skill in StaticToolRow.
  3. Keep all other tool header/output behavior in ToolPart.tsx.
  4. Keep icon changes (if any) in toolPresentation.tsx.

Why: only navigation tools use the compact static path; all other tools need observable input and output.

"I want tool to become expandable" (example)

  1. Update toolRenderUtils.ts:
    • add/remove a tool name from STATIC_TOOL_NAMES only when it has a reliable direct in-app navigation action
  2. Ensure ToolPart.tsx supports desired header + expanded output format for that tool.
  3. Validate both modes (sorted and live).

Safe editing checklist

  • Do not duplicate icon logic; keep it in toolPresentation.tsx.
  • For static tool copy changes, prefer ProgressiveGroup.tsx first.
  • For expanded output changes, edit ToolPart.tsx.
  • After edits run:
    • bun run type-check
    • bun run lint
    • bun run build

Quick map of files in this folder

  • Text: AssistantTextPart.tsx, UserTextPart.tsx
  • Tools: ToolPart.tsx, ToolPartDiffPreview.tsx, PlainDiffFallback.tsx, ProgressiveGroup.tsx, toolPresentation.tsx, toolRenderUtils.ts, ToolRevealOnMount.tsx
  • Reasoning/justification: ReasoningPart.tsx, JustificationBlock.tsx
  • Status/placeholders: WorkingPlaceholder.tsx, SessionActiveSpinner.tsx, MigratingPart.tsx, BusyDots.tsx
  • Utility renderers: VirtualizedCodeBlock.tsx, MinDurationShineText.tsx