TermAl

Feature Spec: Markdown Document View

Status

Active behavior spec.

Agent messages, normal .md/.mdx source files, and Markdown Git diffs all use the shared Markdown renderer. The Git diff path is the most important workflow: it must preserve the existing staged and unstaged Git semantics while giving the user a readable document-shaped editor.

This document is the product contract for the rendered Markdown file viewer and the rendered Markdown diff editor. Bugs and small implementation follow-ups live in docs/bugs.md.

The streaming-aware rendering used by live assistant message bubbles is documented separately in ./streaming-markdown.md. The viewer and diff-editor flows here always pass settled Markdown through MarkdownContent (isStreaming defaults to false), so the streaming-fragment placeholder never appears in those surfaces.

Problem

TermAl can edit files and inspect diffs, but Markdown is still treated mostly as source text outside agent messages. That makes docs, READMEs, plans, changelogs, prompts, and generated Markdown harder to review.

The highest-value workflow is Git diff:

Existing Behavior To Preserve

The Git diff selection semantics are already correct and must not change:

The Markdown viewer must follow the same comparison semantics. In particular, staged Markdown preview must render the index version, not a later working tree version that may include additional unstaged edits.

Goals

Non-goals For V1

Product Model

Markdown has two separate viewing contexts:

  1. Source document preview for an actual file buffer.
  2. Diff document preview for the before/after sides of a Git or agent patch.

These contexts can share the renderer, but they cannot share all data loading rules. A source document preview renders the current editor buffer. A Git diff preview renders a Git comparison side.

Normal Markdown Files

When the active source file is Markdown, show a view switcher:

Rules:

Git Markdown Diffs

When the active diff target is Markdown, add a Markdown-aware view to the existing diff panel:

Rules:

Git Side Semantics

The rendered Markdown document must match the selected Git section:

Section Before Side After Side
unstaged index working tree
staged HEAD index
untracked in unstaged empty document working tree
added in staged empty document index

This matters most when a file has both staged and unstaged changes. The staged Markdown preview must not render the working tree after side, because that may include edits that are not staged for commit.

Required Diff Data Contract

Markdown diff view needs document-side content, not only patch text.

The implementation should use the same source of truth already used for correct Git file comparison. If the current file-view pipeline already exposes before/after side contents, the Markdown diff view should consume that pipeline directly.

If only unified diff text is available at the diff panel boundary, add a narrow document-side data contract to the existing Git diff response or a companion endpoint:

type GitDiffDocumentSide = {
  content: string;
  source: "head" | "index" | "worktree" | "empty" | "patch";
};

type GitDiffDocumentContent = {
  before: GitDiffDocumentSide;
  after: GitDiffDocumentSide;
  canEdit: boolean;
  editBlockedReason?: string | null;
  isCompleteDocument: boolean;
  note?: string | null;
};

Expected behavior:

Markdown Diff View UX

Inside Markdown, render one document-shaped change view. This is both the preferred review surface and, when allowed by the data contract, the rendered Markdown diff editor.

Rules:

Optional follow-up:

Status labels:

Patch-only fallback text:

Rendered from patch context only. Unchanged document sections outside the diff are omitted.

Do not render raw unified diff text as Markdown.

Rendered Markdown Diff Editor Behavior

The rendered Markdown diff editor optimizes for review usability over perfect source fidelity. It lets the user make small documentation fixes while staying in the readable rendered view.

In-flight editor state (scroll, cursor, undo history) is expected to survive reloads and app restarts; see Editor Buffer Persistence for the cross-editor contract this editor participates in.

Editability Rules

Rendered Markdown sections are editable only when all of these are true:

Git-specific rules:

When editing is blocked, the UI must show the backend-provided reason near the rendered document.

Editing Model

Save Semantics

Keyboard And Cursor Behavior

Keyboard movement must feel like a document editor, not like page scrolling:

ArrowDown At The End Of The Last Editable Section

When the caret sits at the end of the last editable section and there is no adjacent editable section below (common when the section ends with a Mermaid fence whose last editable text node sits inside the preserved source code-block), ArrowDown must not leave the user visually stuck. The editor behaves like a document editor that lets you extend past the last line:

Do not modify the segment Markdown through the draft pipeline to produce the landing paragraph. normalizeEditedMarkdownSection in ui/src/panels/markdown-diff-segments.ts strips trailing newlines when the original segment did not end with one, and Markdown parsers do not emit a trailing empty paragraph for "\n\n" anyway. Writing the paragraph directly to the DOM sidesteps both issues: when the user types, handleInput serializes the new paragraph (now non-empty) back into segment Markdown; when the user navigates away without typing, the stray paragraph is reconciled away on the next re-render.

Line Numbers

Rendered Markdown line numbers are document chrome:

Agent Markdown Diffs

Agent diff cards often provide only filePath, summary, diff, and changeType. For those cards:

Editing Markdown Diff Targets

Markdown mode and Edit mode are both allowed to edit Markdown, but they have different jobs.

Markdown mode is the rendered document-shaped editor:

Edit mode keeps the existing source editing behavior:

Important distinction:

For staged Git diffs, editing the live file can include unstaged content. The UI must keep the Staged review view and live edit target conceptually separate. If the same file already has unstaged worktree changes, the staged rendered Markdown editor is read-only and should direct the user to the unstaged/live file path instead.

Markdown Feature Requirements

Required for v1:

Images:

If local image loading needs a backend endpoint, it can be delivered after the text-first Git diff workflow. The viewer must still behave correctly when images cannot be loaded.

Advanced follow-up:

Task Lists

Rendered task checkboxes are read-only in v1.

Rules:

Future behavior can allow clickable rendered task checkboxes, but only when the viewer is backed by an editable source buffer. Patch-only previews and read-only Git side snapshots should keep checkboxes non-interactive.

Markdown document links should reuse the existing source-link behavior from message Markdown.

Supported targets:

Rules:

Rendering Architecture

Create a shared document wrapper around the current message Markdown renderer:

type MarkdownDocumentViewProps = {
  markdown: string;
  workspaceRoot?: string | null;
  documentPath?: string | null;
  onOpenSourceLink?: (target: {
    path: string;
    line?: number;
    column?: number;
    openInNewTab?: boolean;
  }) => void;
  variant?: "source" | "diff";
  completeness?: "full" | "patch";
  note?: string | null;
};

Responsibilities:

Source Panel Integration

Add Markdown view state inside SourcePanel:

type SourceDocumentMode = "code" | "preview" | "split";

Rules:

Diff Panel Integration

Extend diff view mode:

type DiffViewMode = "all" | "changes" | "markdown" | "edit" | "raw";

Rules:

Default mode:

Testing

Automated tests:

Manual checks:

Delivery Plan

Phase 1: Shared Preview

Phase 2: Git Diff Preview

Phase 3: Editing Enhancements

Phase 4: Rich Markdown Extras