From 683a703d854533eef7cdac7cb1cd22717676cb56 Mon Sep 17 00:00:00 2001 From: murashit Date: Tue, 23 Jun 2026 10:34:14 +0900 Subject: [PATCH] Consolidate message stream rendering modules --- .../panel/surface/message-stream-presenter.ts | 5 +- .../ui/message-stream/markdown-renderer.ts | 48 +++++++++++++++++- .../chat/ui/message-stream/stream-blocks.tsx | 26 +++++++++- .../stream-markdown-renderer.ts | 49 ------------------- .../chat/ui/message-stream/viewport.tsx | 29 ----------- .../surface/message-stream-presenter.test.ts | 2 +- .../stream-markdown-renderer.test.ts | 2 +- .../chat/ui/message-stream/test-helpers.tsx | 2 +- 8 files changed, 77 insertions(+), 86 deletions(-) delete mode 100644 src/features/chat/ui/message-stream/stream-markdown-renderer.ts delete mode 100644 src/features/chat/ui/message-stream/viewport.tsx diff --git a/src/features/chat/panel/surface/message-stream-presenter.ts b/src/features/chat/panel/surface/message-stream-presenter.ts index f845ba65..c52352eb 100644 --- a/src/features/chat/panel/surface/message-stream-presenter.ts +++ b/src/features/chat/panel/surface/message-stream-presenter.ts @@ -5,9 +5,8 @@ import { copyTextWithNotice } from "../../../../shared/ui/clipboard"; import type { ChatAction } from "../../application/state/root-reducer"; import type { ChatStateStore } from "../../application/state/store"; import type { MessageStreamScrollControllerBinding } from "../../ui/message-stream/flow-scroll"; -import { MarkdownMessageRenderer } from "../../ui/message-stream/markdown-renderer"; -import { renderStreamMarkdown } from "../../ui/message-stream/stream-markdown-renderer"; -import { MessageStreamViewport, type MessageStreamViewportState } from "../../ui/message-stream/viewport"; +import { MarkdownMessageRenderer, renderStreamMarkdown } from "../../ui/message-stream/markdown-renderer"; +import { MessageStreamViewport, type MessageStreamViewportState } from "../../ui/message-stream/stream-blocks"; import { messageStreamStateFromShellState, useChatPanelShellState, type ChatPanelMessageStreamShellState } from "../shell-state"; import type { PendingRequestBlockActions } from "../../application/pending-requests/block"; import type { ChatTurnDiffViewState } from "../../domain/turn-diff"; diff --git a/src/features/chat/ui/message-stream/markdown-renderer.ts b/src/features/chat/ui/message-stream/markdown-renderer.ts index 16d80e44..c8635a03 100644 --- a/src/features/chat/ui/message-stream/markdown-renderer.ts +++ b/src/features/chat/ui/message-stream/markdown-renderer.ts @@ -1,14 +1,20 @@ import { MarkdownRenderer, Notice, type App, type Component } from "obsidian"; +import { micromark } from "micromark"; import { isAbsoluteFileHref, vaultFileLinkTarget, vaultRelativeFileLinkTarget } from "../../../../shared/obsidian/file-links"; import { notifyMessageContentRendered } from "./content-events"; -export interface MarkdownMessageRendererOptions { +interface MarkdownMessageRendererOptions { app: App; owner: Component; vaultPath: string; } +interface StreamMarkdownRenderContext { + app: App; + vaultPath: string; +} + export class MarkdownMessageRenderer { private readonly renderGenerations = new WeakMap(); @@ -29,6 +35,16 @@ export class MarkdownMessageRenderer { } } +export function renderStreamMarkdown(parent: HTMLElement, text: string, context: StreamMarkdownRenderContext): void { + const html = micromark(text); + const DOMParserConstructor = parent.ownerDocument.defaultView?.DOMParser ?? DOMParser; + const parser = new DOMParserConstructor(); + const parsed = parser.parseFromString(html, "text/html"); + const nodes = Array.from(parsed.body.childNodes).map((node) => parent.ownerDocument.importNode(node, true)); + parent.replaceChildren(...nodes); + bindStreamMarkdownFileLinks(parent, context); +} + interface RenderedMarkdownLinkContext { app: App; vaultPath: string; @@ -65,3 +81,33 @@ function bindRenderedMarkdownFileLinks(parent: HTMLElement, sourcePath: string, }; }); } + +function bindStreamMarkdownFileLinks(parent: HTMLElement, context: StreamMarkdownRenderContext): void { + const sourcePath = context.app.workspace.getActiveFile()?.path ?? ""; + parent.querySelectorAll("a[href]").forEach((link) => { + const href = link.getAttribute("href") ?? ""; + const target = vaultFileLinkTarget(context.app, context.vaultPath, href); + if (target) { + link.addClass("codex-panel__filelink"); + link.onclick = (event) => { + event.preventDefault(); + void context.app.workspace.openLinkText(target, sourcePath, false); + }; + return; + } + + const vaultRelativeTarget = vaultRelativeFileLinkTarget(context.vaultPath, context.app.vault.configDir, href); + if (vaultRelativeTarget) { + link.onclick = (event) => { + event.preventDefault(); + }; + return; + } + + if (!isAbsoluteFileHref(href)) return; + link.onclick = (event) => { + event.preventDefault(); + new Notice("Cannot open files outside the vault."); + }; + }); +} diff --git a/src/features/chat/ui/message-stream/stream-blocks.tsx b/src/features/chat/ui/message-stream/stream-blocks.tsx index 7227cf99..0ae3c3a1 100644 --- a/src/features/chat/ui/message-stream/stream-blocks.tsx +++ b/src/features/chat/ui/message-stream/stream-blocks.tsx @@ -5,19 +5,43 @@ import { type MessageStreamRenderedItemView, type MessageStreamViewBlock, } from "../../presentation/message-stream/view-model"; +import { MessageStreamFlowFrame, type MessageStreamScrollControllerBinding } from "./flow-scroll"; import { pendingRequestBlockNode } from "./pending-request-block"; import { detailNode } from "./detail"; import { agentRunSummaryNode, statusNode } from "./status"; import type { MessageStreamContext, PendingRequestBlockContext } from "./context"; import { textNode } from "./text"; +export interface MessageStreamViewportState { + blocks: readonly MessageStreamViewBlock[]; + context: MessageStreamContext; + scrollController: MessageStreamScrollControllerBinding; +} + +interface MessageStreamViewportProps { + state: MessageStreamViewportState; + rootAttributes?: Partial>; +} + +export function MessageStreamViewport({ state, rootAttributes }: MessageStreamViewportProps): UiNode { + const { blocks, context, scrollController } = state; + return ( + } + {...(rootAttributes ? { rootAttributes } : {})} + /> + ); +} + function streamItemNode(item: MessageStreamRenderedItemView, context: MessageStreamContext): UiNode { if (item.kind === "text") return textNode(item.view, context); if (item.kind === "detail") return detailNode(item.view, context); return statusNode(item.view); } -export function MessageStreamBlockContent({ block, context }: { block: MessageStreamViewBlock; context: MessageStreamContext }): UiNode { +function MessageStreamBlockContent({ block, context }: { block: MessageStreamViewBlock; context: MessageStreamContext }): UiNode { return presentationBlockNode(block, context); } diff --git a/src/features/chat/ui/message-stream/stream-markdown-renderer.ts b/src/features/chat/ui/message-stream/stream-markdown-renderer.ts deleted file mode 100644 index 0891e73b..00000000 --- a/src/features/chat/ui/message-stream/stream-markdown-renderer.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { Notice, type App } from "obsidian"; -import { micromark } from "micromark"; - -import { isAbsoluteFileHref, vaultFileLinkTarget, vaultRelativeFileLinkTarget } from "../../../../shared/obsidian/file-links"; - -export interface StreamMarkdownRenderContext { - app: App; - vaultPath: string; -} - -export function renderStreamMarkdown(parent: HTMLElement, text: string, context: StreamMarkdownRenderContext): void { - const html = micromark(text); - const DOMParserConstructor = parent.ownerDocument.defaultView?.DOMParser ?? DOMParser; - const parser = new DOMParserConstructor(); - const parsed = parser.parseFromString(html, "text/html"); - const nodes = Array.from(parsed.body.childNodes).map((node) => parent.ownerDocument.importNode(node, true)); - parent.replaceChildren(...nodes); - bindStreamMarkdownFileLinks(parent, context); -} - -function bindStreamMarkdownFileLinks(parent: HTMLElement, context: StreamMarkdownRenderContext): void { - const sourcePath = context.app.workspace.getActiveFile()?.path ?? ""; - parent.querySelectorAll("a[href]").forEach((link) => { - const href = link.getAttribute("href") ?? ""; - const target = vaultFileLinkTarget(context.app, context.vaultPath, href); - if (target) { - link.addClass("codex-panel__filelink"); - link.onclick = (event) => { - event.preventDefault(); - void context.app.workspace.openLinkText(target, sourcePath, false); - }; - return; - } - - const vaultRelativeTarget = vaultRelativeFileLinkTarget(context.vaultPath, context.app.vault.configDir, href); - if (vaultRelativeTarget) { - link.onclick = (event) => { - event.preventDefault(); - }; - return; - } - - if (!isAbsoluteFileHref(href)) return; - link.onclick = (event) => { - event.preventDefault(); - new Notice("Cannot open files outside the vault."); - }; - }); -} diff --git a/src/features/chat/ui/message-stream/viewport.tsx b/src/features/chat/ui/message-stream/viewport.tsx deleted file mode 100644 index 86be9d24..00000000 --- a/src/features/chat/ui/message-stream/viewport.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import type { ComponentChild as UiNode } from "preact"; - -import { MessageStreamFlowFrame, type MessageStreamScrollControllerBinding } from "./flow-scroll"; -import type { MessageStreamContext } from "./context"; -import { MessageStreamBlockContent } from "./stream-blocks"; -import type { MessageStreamViewBlock } from "../../presentation/message-stream/view-model"; - -export interface MessageStreamViewportState { - blocks: readonly MessageStreamViewBlock[]; - context: MessageStreamContext; - scrollController: MessageStreamScrollControllerBinding; -} - -export interface MessageStreamViewportProps { - state: MessageStreamViewportState; - rootAttributes?: Partial>; -} - -export function MessageStreamViewport({ state, rootAttributes }: MessageStreamViewportProps): UiNode { - const { blocks, context, scrollController } = state; - return ( - } - {...(rootAttributes ? { rootAttributes } : {})} - /> - ); -} diff --git a/tests/features/chat/panel/surface/message-stream-presenter.test.ts b/tests/features/chat/panel/surface/message-stream-presenter.test.ts index 56707b5a..965e07b9 100644 --- a/tests/features/chat/panel/surface/message-stream-presenter.test.ts +++ b/tests/features/chat/panel/surface/message-stream-presenter.test.ts @@ -15,7 +15,7 @@ import { createChatMessageScrollController, type ChatMessageScrollController, } from "../../../../../src/features/chat/panel/surface/message-stream-scroll"; -import { MessageStreamViewport } from "../../../../../src/features/chat/ui/message-stream/viewport"; +import { MessageStreamViewport } from "../../../../../src/features/chat/ui/message-stream/stream-blocks"; import { MarkdownMessageRenderer } from "../../../../../src/features/chat/ui/message-stream/markdown-renderer"; import { MESSAGE_CONTENT_RENDERED_EVENT } from "../../../../../src/features/chat/ui/message-stream/content-events"; import { renderUiRoot, unmountUiRoot } from "../../../../../src/shared/ui/ui-root"; diff --git a/tests/features/chat/ui/message-stream/stream-markdown-renderer.test.ts b/tests/features/chat/ui/message-stream/stream-markdown-renderer.test.ts index ea99e3d9..25d0cdd2 100644 --- a/tests/features/chat/ui/message-stream/stream-markdown-renderer.test.ts +++ b/tests/features/chat/ui/message-stream/stream-markdown-renderer.test.ts @@ -3,7 +3,7 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; import { TFile } from "obsidian"; -import { renderStreamMarkdown } from "../../../../../src/features/chat/ui/message-stream/stream-markdown-renderer"; +import { renderStreamMarkdown } from "../../../../../src/features/chat/ui/message-stream/markdown-renderer"; import { notices } from "../../../../mocks/obsidian"; import { installObsidianDomShims } from "../../../../support/dom"; diff --git a/tests/features/chat/ui/message-stream/test-helpers.tsx b/tests/features/chat/ui/message-stream/test-helpers.tsx index f95a3a77..35b0e8e8 100644 --- a/tests/features/chat/ui/message-stream/test-helpers.tsx +++ b/tests/features/chat/ui/message-stream/test-helpers.tsx @@ -17,7 +17,7 @@ import { type MessageStreamViewBlock, } from "../../../../../src/features/chat/presentation/message-stream/view-model"; import type { MessageStreamTextActionTargets } from "../../../../../src/features/chat/presentation/message-stream/text-view"; -import { MessageStreamViewport } from "../../../../../src/features/chat/ui/message-stream/viewport"; +import { MessageStreamViewport } from "../../../../../src/features/chat/ui/message-stream/stream-blocks"; import type { MessageStreamScrollControllerBinding } from "../../../../../src/features/chat/ui/message-stream/flow-scroll"; import { renderUiRoot, unmountUiRoot } from "../../../../../src/shared/ui/ui-root";