mirror of
https://github.com/murashit/codex-panel.git
synced 2026-07-22 06:57:10 +00:00
Consolidate message stream rendering modules
This commit is contained in:
parent
4cb72e957d
commit
683a703d85
8 changed files with 77 additions and 86 deletions
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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<HTMLElement, number>();
|
||||
|
||||
|
|
@ -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<HTMLAnchorElement>("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.");
|
||||
};
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<Record<`data-${string}`, string>>;
|
||||
}
|
||||
|
||||
export function MessageStreamViewport({ state, rootAttributes }: MessageStreamViewportProps): UiNode {
|
||||
const { blocks, context, scrollController } = state;
|
||||
return (
|
||||
<MessageStreamFlowFrame
|
||||
blocks={blocks}
|
||||
scrollController={scrollController}
|
||||
renderBlockContent={(block) => <MessageStreamBlockContent block={block} context={context} />}
|
||||
{...(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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<HTMLAnchorElement>("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.");
|
||||
};
|
||||
});
|
||||
}
|
||||
|
|
@ -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<Record<`data-${string}`, string>>;
|
||||
}
|
||||
|
||||
export function MessageStreamViewport({ state, rootAttributes }: MessageStreamViewportProps): UiNode {
|
||||
const { blocks, context, scrollController } = state;
|
||||
return (
|
||||
<MessageStreamFlowFrame
|
||||
blocks={blocks}
|
||||
scrollController={scrollController}
|
||||
renderBlockContent={(block) => <MessageStreamBlockContent block={block} context={context} />}
|
||||
{...(rootAttributes ? { rootAttributes } : {})}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue