Align thread stream CSS classes

This commit is contained in:
murashit 2026-07-09 06:52:19 +09:00
parent 36ed7fceb3
commit 4202748cc6
32 changed files with 407 additions and 407 deletions

View file

@ -97,7 +97,7 @@ function uiRootIntact(container: HTMLElement, showToolbar: boolean): boolean {
} else if (topLevel[0] !== body) {
return false;
}
return Boolean(shellRegion(body, "goal") && shellRegion(body, "message-stream") && shellRegion(body, "composer"));
return Boolean(shellRegion(body, "goal") && shellRegion(body, "thread-stream") && shellRegion(body, "composer"));
}
function shellRegion(container: HTMLElement, region: string): HTMLElement | null {

View file

@ -7,7 +7,7 @@ import type { PendingRequestBlockActions } from "../../application/pending-reque
import type { ChatAction } from "../../application/state/root-reducer";
import type { ChatStateStore } from "../../application/state/store";
import type { ThreadStreamScrollPortBinding } from "../../ui/thread-stream/flow-scroll.measure";
import { MarkdownMessageRenderer, renderStreamMarkdown } from "../../ui/thread-stream/markdown-renderer.obsidian";
import { renderStreamMarkdown, ThreadStreamMarkdownRenderer } from "../../ui/thread-stream/markdown-renderer.obsidian";
import { ThreadStreamViewport, type ThreadStreamViewportState } from "../../ui/thread-stream/stream-blocks";
import type { ChatPanelThreadStreamReadModel } from "../shell-read-model";
import { type ChatThreadStreamSurfaceContext, threadStreamSurfaceProjectionFromModel } from "./thread-stream-projection";
@ -25,7 +25,7 @@ export function ChatPanelThreadStream({
}): UiNode {
return h(ThreadStreamViewport, {
state: presenter.renderState(model),
rootAttributes: { "data-codex-panel-shell-region": "message-stream" },
rootAttributes: { "data-codex-panel-shell-region": "thread-stream" },
});
}
@ -75,11 +75,11 @@ export interface ThreadStreamPresenterOptions {
}
export class ThreadStreamPresenter {
private readonly obsidianMarkdownRenderer: MarkdownMessageRenderer;
private readonly obsidianMarkdownRenderer: ThreadStreamMarkdownRenderer;
private readonly surfaceContext: ChatThreadStreamSurfaceContext;
constructor(private readonly options: ThreadStreamPresenterOptions) {
this.obsidianMarkdownRenderer = new MarkdownMessageRenderer({
this.obsidianMarkdownRenderer = new ThreadStreamMarkdownRenderer({
app: options.obsidian.app,
owner: options.obsidian.owner,
vaultPath: options.workspace.vaultPath,
@ -104,7 +104,7 @@ export class ThreadStreamPresenter {
vaultPath: options.workspace.vaultPath,
});
},
copyMessageText: (text) => void this.copyMessageText(text),
copyDialogueText: (text) => void this.copyDialogueText(text),
actions: options.actions,
requests: options.requests,
};
@ -128,7 +128,7 @@ export class ThreadStreamPresenter {
this.options.scroll.dispose();
}
private async copyMessageText(text: string): Promise<void> {
private async copyDialogueText(text: string): Promise<void> {
await copyTextWithNotice(text, "Copied message.", "Could not copy message.");
}
}

View file

@ -30,7 +30,7 @@ export interface ChatThreadStreamSurfaceContext {
loadOlderTurns: () => void;
renderObsidianMarkdown: (element: HTMLElement, text: string) => void;
renderStreamMarkdown: (element: HTMLElement, text: string) => void;
copyMessageText: (text: string) => void;
copyDialogueText: (text: string) => void;
actions: ChatThreadStreamActions;
requests: ChatThreadStreamRequests;
}
@ -80,7 +80,7 @@ function threadStreamContextFromProjection(
loadOlderTurns: context.loadOlderTurns,
renderObsidianMarkdown: context.renderObsidianMarkdown,
renderStreamMarkdown: context.renderStreamMarkdown,
copyText: context.copyMessageText,
copyText: context.copyDialogueText,
onImplementPlan: (target) => {
context.actions.implementPlan(target.itemId);
},

View file

@ -72,7 +72,7 @@ function detailViewBase(
summaryThreadIds: readonly string[] = [],
): DetailView {
return {
className: `codex-panel__message codex-panel__message--tool ${className}`,
className: `codex-panel__stream-item codex-panel__stream-item--tool ${className}`,
label,
summary,
summaryThreadIds,
@ -175,7 +175,7 @@ function reviewDetailView(item: ReviewResultThreadStreamItem): DetailView {
item,
"auto-review",
messageDetailKey(item.id, "review-details"),
"codex-panel__message--review-result codex-panel__detail-item--review",
"codex-panel__stream-item--review-result codex-panel__detail-item--review",
);
}
@ -184,7 +184,7 @@ function approvalDetailView(item: ApprovalResultThreadStreamItem): DetailView {
item,
"approval",
messageDetailKey(item.id, "approval-details"),
"codex-panel__message--approval-result codex-panel__detail-item--approval",
"codex-panel__stream-item--approval-result codex-panel__detail-item--approval",
);
}

View file

@ -169,18 +169,18 @@ function executionClassName(state: ExecutionState): string {
}
function textItemClass(item: ThreadStreamItem): string {
const classes = ["codex-panel__message", messageRoleClassName(item.role)];
if (item.kind === "approvalResult") classes.push("codex-panel__message--approval-result");
if (item.kind === "userInputResult") classes.push("codex-panel__message--user-input-result");
if (item.kind === "reviewResult") classes.push("codex-panel__message--review-result");
const classes = ["codex-panel__stream-item", streamItemRoleClassName(item.role)];
if (item.kind === "approvalResult") classes.push("codex-panel__stream-item--approval-result");
if (item.kind === "userInputResult") classes.push("codex-panel__stream-item--user-input-result");
if (item.kind === "reviewResult") classes.push("codex-panel__stream-item--review-result");
return classes.join(" ");
}
function messageRoleClassName(role: ThreadStreamItem["role"]): string {
if (role === "assistant") return "codex-panel__message--assistant";
if (role === "system") return "codex-panel__message--system";
if (role === "tool") return "codex-panel__message--tool";
return "codex-panel__message--user";
function streamItemRoleClassName(role: ThreadStreamItem["role"]): string {
if (role === "assistant") return "codex-panel__stream-item--assistant";
if (role === "system") return "codex-panel__stream-item--system";
if (role === "tool") return "codex-panel__stream-item--tool";
return "codex-panel__stream-item--user";
}
function definedProp<Key extends string, Value>(key: Key, value: Value | undefined): Partial<Record<Key, Value>> {

View file

@ -118,7 +118,7 @@ export function GoalPanel({
<IconButton
icon="pencil"
label="Edit goal"
className="clickable-icon codex-panel__message-action codex-panel__goal-action"
className="clickable-icon codex-panel__stream-item-action codex-panel__goal-action"
onClick={actions.onStartEditing}
/>
) : null}
@ -126,7 +126,7 @@ export function GoalPanel({
<IconButton
icon="pause"
label="Pause goal"
className="clickable-icon codex-panel__message-action codex-panel__goal-action"
className="clickable-icon codex-panel__stream-item-action codex-panel__goal-action"
onClick={actions.onPause}
/>
) : null}
@ -134,7 +134,7 @@ export function GoalPanel({
<IconButton
icon="play"
label="Resume goal"
className="clickable-icon codex-panel__message-action codex-panel__goal-action"
className="clickable-icon codex-panel__stream-item-action codex-panel__goal-action"
onClick={actions.onResume}
/>
) : null}
@ -142,7 +142,7 @@ export function GoalPanel({
<IconButton
icon="x"
label="Clear goal"
className="clickable-icon codex-panel__message-action codex-panel__goal-action"
className="clickable-icon codex-panel__stream-item-action codex-panel__goal-action"
onClick={actions.onClear}
/>
) : null}

View file

@ -13,7 +13,7 @@ export function OpenAgentThreadAction({
<IconButton
icon="external-link"
label="Open agent thread"
className="clickable-icon codex-panel__message-action codex-panel__agent-open-thread"
className="clickable-icon codex-panel__stream-item-action codex-panel__agent-open-thread"
onClick={(event) => {
event.preventDefault();
event.stopPropagation();

View file

@ -1,5 +1,5 @@
export const MESSAGE_CONTENT_RENDERED_EVENT = "codex-panel:message-content-rendered";
export const THREAD_STREAM_CONTENT_RENDERED_EVENT = "codex-panel:thread-stream-content-rendered";
export function notifyMessageContentRendered(element: HTMLElement): void {
element.dispatchEvent(new Event(MESSAGE_CONTENT_RENDERED_EVENT, { bubbles: true }));
export function notifyThreadStreamContentRendered(element: HTMLElement): void {
element.dispatchEvent(new Event(THREAD_STREAM_CONTENT_RENDERED_EVENT, { bubbles: true }));
}

View file

@ -72,7 +72,7 @@ function DetailHeader({
openThreadInNewView?: ((threadId: string) => void) | undefined;
}): UiNode {
const content = (
<span className="codex-panel__message-role codex-panel__detail-label">
<span className="codex-panel__stream-item-role codex-panel__detail-label">
<span>{view.label}</span>
{openThreadInNewView && view.summaryThreadIds.length > 0 ? (
<span className="codex-panel__detail-header-actions">

View file

@ -1,17 +1,17 @@
import { Component, h, type ComponentChild as UiNode } from "preact";
import { disposeDomListeners, listenDomEvent } from "../../../../shared/dom/events.dom";
import { MESSAGE_CONTENT_RENDERED_EVENT } from "./content-rendered-event.dom";
import { THREAD_STREAM_CONTENT_RENDERED_EVENT } from "./content-rendered-event.dom";
type MessageScrollDirection = -1 | 1;
type ThreadStreamScrollDirection = -1 | 1;
const MESSAGE_FLOW_TEXT_LINE_SCROLL_LINES = 4;
const MESSAGE_FLOW_REPEATED_TEXT_LINE_SCROLL_LINES = 4;
const THREAD_STREAM_FLOW_TEXT_LINE_SCROLL_LINES = 4;
const THREAD_STREAM_FLOW_REPEATED_TEXT_LINE_SCROLL_LINES = 4;
export type ThreadStreamScrollCommand =
| { kind: "show-latest" }
| { kind: "scroll-to"; edge: "start" | "end" }
| { kind: "scroll-by"; amount: "text-lines" | "page"; direction: MessageScrollDirection; repeated?: boolean };
| { kind: "scroll-by"; amount: "text-lines" | "page"; direction: ThreadStreamScrollDirection; repeated?: boolean };
export interface ThreadStreamScrollPort {
dispatchScrollCommand(command: ThreadStreamScrollCommand): void;
@ -32,14 +32,14 @@ export interface ThreadStreamFlowFrameProps<Block extends ThreadStreamFlowBlockI
renderBlockContent: (block: Block) => UiNode;
}
interface MessageFlowReadingAnchor {
interface ThreadStreamFlowReadingAnchor {
key: string;
top: number;
}
type MessageFlowSnapshot = MessageFlowReadingAnchor | null;
type ThreadStreamFlowSnapshot = ThreadStreamFlowReadingAnchor | null;
interface MessageFlowRuntime {
interface ThreadStreamFlowRuntime {
container: HTMLElement | null;
followingEnd: boolean;
restoreFrame: number | null;
@ -47,10 +47,10 @@ interface MessageFlowRuntime {
}
export class ThreadStreamFlowFrame<Block extends ThreadStreamFlowBlockIdentity> extends Component<ThreadStreamFlowFrameProps<Block>> {
private readonly runtime = createMessageFlowRuntime();
private readonly runtime = createThreadStreamFlowRuntime();
private readonly scrollPort: ThreadStreamScrollPort = {
dispatchScrollCommand: (command) => {
applyMessageFlowScrollCommand(this.runtime, command);
applyThreadStreamFlowScrollCommand(this.runtime, command);
},
};
private scrollElement: HTMLElement | null = null;
@ -58,28 +58,28 @@ export class ThreadStreamFlowFrame<Block extends ThreadStreamFlowBlockIdentity>
override componentDidMount(): void {
this.mountScrollPort();
attachMessageFlowContainer(this.runtime, this.scrollElement);
completeMessageFlowRender(this.runtime, null);
attachThreadStreamFlowContainer(this.runtime, this.scrollElement);
completeThreadStreamFlowRender(this.runtime, null);
}
override getSnapshotBeforeUpdate(previousProps: Readonly<ThreadStreamFlowFrameProps<Block>>): MessageFlowSnapshot | null {
return captureMessageFlowSnapshot(this.runtime, this.scrollElement, previousProps.blocks, this.props.blocks);
override getSnapshotBeforeUpdate(previousProps: Readonly<ThreadStreamFlowFrameProps<Block>>): ThreadStreamFlowSnapshot | null {
return captureThreadStreamFlowSnapshot(this.runtime, this.scrollElement, previousProps.blocks, this.props.blocks);
}
override componentDidUpdate(
previousProps: Readonly<ThreadStreamFlowFrameProps<Block>>,
_previousState: Readonly<Record<string, never>>,
snapshot: MessageFlowSnapshot | null,
snapshot: ThreadStreamFlowSnapshot | null,
): void {
if (previousProps.scrollPortBinding !== this.props.scrollPortBinding) this.mountScrollPort();
if (this.runtime.container !== this.scrollElement) attachMessageFlowContainer(this.runtime, this.scrollElement);
completeMessageFlowRender(this.runtime, snapshot);
if (this.runtime.container !== this.scrollElement) attachThreadStreamFlowContainer(this.runtime, this.scrollElement);
completeThreadStreamFlowRender(this.runtime, snapshot);
}
override componentWillUnmount(): void {
this.unmountScrollPort?.();
this.unmountScrollPort = null;
disposeMessageFlowRuntime(this.runtime);
disposeThreadStreamFlowRuntime(this.runtime);
}
override render({ blocks, renderBlockContent, rootAttributes }: ThreadStreamFlowFrameProps<Block>): UiNode {
@ -88,18 +88,18 @@ export class ThreadStreamFlowFrame<Block extends ThreadStreamFlowBlockIdentity>
{
...rootAttributes,
ref: this.setScrollElement,
className: "codex-panel__region codex-panel__region--message-stream codex-panel__messages",
className: "codex-panel__region codex-panel__region--thread-stream codex-panel__thread-stream",
},
h(
"div",
{ className: "codex-panel__message-flow" },
{ className: "codex-panel__thread-stream-flow" },
blocks.map((block) =>
h(
"div",
{
ref: this.notifyBlockLayout,
key: block.key,
className: "codex-panel__message-block",
className: "codex-panel__thread-stream-block",
"data-codex-panel-block-key": block.key,
},
renderBlockContent(block),
@ -114,7 +114,7 @@ export class ThreadStreamFlowFrame<Block extends ThreadStreamFlowBlockIdentity>
};
private readonly notifyBlockLayout = (element: HTMLElement | null): void => {
handleMessageFlowBlockLayout(this.runtime, element);
handleThreadStreamFlowBlockLayout(this.runtime, element);
};
private mountScrollPort(): void {
@ -123,7 +123,7 @@ export class ThreadStreamFlowFrame<Block extends ThreadStreamFlowBlockIdentity>
}
}
function createMessageFlowRuntime(): MessageFlowRuntime {
function createThreadStreamFlowRuntime(): ThreadStreamFlowRuntime {
return {
container: null,
followingEnd: false,
@ -132,57 +132,57 @@ function createMessageFlowRuntime(): MessageFlowRuntime {
};
}
function captureMessageFlowSnapshot(
runtime: MessageFlowRuntime,
function captureThreadStreamFlowSnapshot(
runtime: ThreadStreamFlowRuntime,
container: HTMLElement | null,
previousBlocks: readonly ThreadStreamFlowBlockIdentity[],
nextBlocks: readonly ThreadStreamFlowBlockIdentity[],
): MessageFlowSnapshot | null {
if (!container || runtime.followingEnd || !messageFlowBlocksShifted(previousBlocks, nextBlocks)) return null;
return captureMessageFlowReadingAnchor(container);
): ThreadStreamFlowSnapshot | null {
if (!container || runtime.followingEnd || !threadStreamFlowBlocksShifted(previousBlocks, nextBlocks)) return null;
return captureThreadStreamFlowReadingAnchor(container);
}
function completeMessageFlowRender(runtime: MessageFlowRuntime, snapshot: MessageFlowSnapshot | null): void {
function completeThreadStreamFlowRender(runtime: ThreadStreamFlowRuntime, snapshot: ThreadStreamFlowSnapshot | null): void {
const container = runtime.container;
if (!container) return;
if (runtime.followingEnd) {
scrollMessageFlowToEnd(runtime);
scrollThreadStreamFlowToEnd(runtime);
return;
}
if (snapshot) restoreMessageFlowReadingAnchor(container, snapshot);
if (snapshot) restoreThreadStreamFlowReadingAnchor(container, snapshot);
}
function attachMessageFlowContainer(runtime: MessageFlowRuntime, container: HTMLElement | null): void {
function attachThreadStreamFlowContainer(runtime: ThreadStreamFlowRuntime, container: HTMLElement | null): void {
if (runtime.container === container) return;
detachMessageFlowContainer(runtime);
detachThreadStreamFlowContainer(runtime);
runtime.container = container;
runtime.followingEnd = container ? isMessageFlowAtEnd(container) || isMessageFlowViewportHidden(container) : false;
runtime.followingEnd = container ? isThreadStreamFlowAtEnd(container) || isThreadStreamFlowViewportHidden(container) : false;
if (!container) return;
const handleScroll = () => {
runtime.followingEnd = isMessageFlowAtEnd(container);
runtime.followingEnd = isThreadStreamFlowAtEnd(container);
};
const handleContentChange = () => {
if (!runtime.followingEnd) return;
scrollMessageFlowToEnd(runtime);
scheduleMessageFlowEndRestore(runtime);
scrollThreadStreamFlowToEnd(runtime);
scheduleThreadStreamFlowEndRestore(runtime);
};
const win = container.ownerDocument.defaultView;
if (win?.ResizeObserver) {
runtime.resizeObserver = new win.ResizeObserver(() => {
if (runtime.followingEnd) scheduleMessageFlowEndRestore(runtime);
if (runtime.followingEnd) scheduleThreadStreamFlowEndRestore(runtime);
});
runtime.resizeObserver.observe(container);
}
runtime.restoreFrame = null;
cleanupMessageFlowContainer.set(
cleanupThreadStreamFlowContainer.set(
runtime,
disposeDomListeners(
listenDomEvent(container, "scroll", handleScroll, { passive: true }),
listenDomEvent(container, MESSAGE_CONTENT_RENDERED_EVENT, handleContentChange, true),
listenDomEvent(container, THREAD_STREAM_CONTENT_RENDERED_EVENT, handleContentChange, true),
listenDomEvent(container, "toggle", handleContentChange, true),
() => {
runtime.resizeObserver?.disconnect();
@ -192,87 +192,87 @@ function attachMessageFlowContainer(runtime: MessageFlowRuntime, container: HTML
);
}
const cleanupMessageFlowContainer = new WeakMap<MessageFlowRuntime, () => void>();
const cleanupThreadStreamFlowContainer = new WeakMap<ThreadStreamFlowRuntime, () => void>();
function detachMessageFlowContainer(runtime: MessageFlowRuntime): void {
cancelMessageFlowEndRestore(runtime);
cleanupMessageFlowContainer.get(runtime)?.();
cleanupMessageFlowContainer.delete(runtime);
function detachThreadStreamFlowContainer(runtime: ThreadStreamFlowRuntime): void {
cancelThreadStreamFlowEndRestore(runtime);
cleanupThreadStreamFlowContainer.get(runtime)?.();
cleanupThreadStreamFlowContainer.delete(runtime);
runtime.container = null;
}
function disposeMessageFlowRuntime(runtime: MessageFlowRuntime): void {
detachMessageFlowContainer(runtime);
function disposeThreadStreamFlowRuntime(runtime: ThreadStreamFlowRuntime): void {
detachThreadStreamFlowContainer(runtime);
runtime.followingEnd = false;
}
function applyMessageFlowScrollCommand(runtime: MessageFlowRuntime, command: ThreadStreamScrollCommand): void {
function applyThreadStreamFlowScrollCommand(runtime: ThreadStreamFlowRuntime, command: ThreadStreamScrollCommand): void {
switch (command.kind) {
case "show-latest":
runtime.followingEnd = true;
scrollMessageFlowToEnd(runtime);
scheduleMessageFlowEndRestore(runtime);
scrollThreadStreamFlowToEnd(runtime);
scheduleThreadStreamFlowEndRestore(runtime);
break;
case "scroll-to":
if (command.edge === "start") {
scrollMessageFlowToStart(runtime, messageFlowManualScrollBehavior(runtime, false));
scrollThreadStreamFlowToStart(runtime, threadStreamFlowManualScrollBehavior(runtime, false));
} else {
runtime.followingEnd = true;
scrollMessageFlowToEnd(runtime, messageFlowManualScrollBehavior(runtime, false));
scrollThreadStreamFlowToEnd(runtime, threadStreamFlowManualScrollBehavior(runtime, false));
}
break;
case "scroll-by":
scrollMessageFlowBy(
scrollThreadStreamFlowBy(
runtime,
messageFlowScrollDelta(runtime, command.amount, command.direction, command.repeated === true),
messageFlowManualScrollBehavior(runtime, command.repeated === true),
threadStreamFlowScrollDelta(runtime, command.amount, command.direction, command.repeated === true),
threadStreamFlowManualScrollBehavior(runtime, command.repeated === true),
);
break;
}
}
function handleMessageFlowBlockLayout(runtime: MessageFlowRuntime, element: HTMLElement | null): void {
function handleThreadStreamFlowBlockLayout(runtime: ThreadStreamFlowRuntime, element: HTMLElement | null): void {
if (!element || !runtime.followingEnd) return;
scrollMessageFlowToEnd(runtime);
scheduleMessageFlowEndRestore(runtime);
scrollThreadStreamFlowToEnd(runtime);
scheduleThreadStreamFlowEndRestore(runtime);
}
function scrollMessageFlowBy(runtime: MessageFlowRuntime, delta: number, behavior: ScrollBehavior = "auto"): void {
function scrollThreadStreamFlowBy(runtime: ThreadStreamFlowRuntime, delta: number, behavior: ScrollBehavior = "auto"): void {
const container = runtime.container;
if (!container) return;
const targetTop = clampMessageFlowScrollTop(container, container.scrollTop + delta);
scrollMessageFlowToTop(container, targetTop, behavior);
runtime.followingEnd = isMessageFlowTopAtEnd(container, targetTop);
const targetTop = clampThreadStreamFlowScrollTop(container, container.scrollTop + delta);
scrollThreadStreamFlowToTop(container, targetTop, behavior);
runtime.followingEnd = isThreadStreamFlowTopAtEnd(container, targetTop);
}
function scrollMessageFlowToStart(runtime: MessageFlowRuntime, behavior: ScrollBehavior = "auto"): void {
function scrollThreadStreamFlowToStart(runtime: ThreadStreamFlowRuntime, behavior: ScrollBehavior = "auto"): void {
const container = runtime.container;
if (!container) return;
scrollMessageFlowToTop(container, 0, behavior);
scrollThreadStreamFlowToTop(container, 0, behavior);
runtime.followingEnd = false;
}
function messageFlowScrollDelta(
runtime: MessageFlowRuntime,
function threadStreamFlowScrollDelta(
runtime: ThreadStreamFlowRuntime,
amount: "text-lines" | "page",
direction: MessageScrollDirection,
direction: ThreadStreamScrollDirection,
repeated: boolean,
): number {
const container = runtime.container;
if (!container) return 0;
if (amount === "page") return Math.max(1, Math.floor(container.clientHeight * 0.8)) * direction;
const lines = repeated ? MESSAGE_FLOW_REPEATED_TEXT_LINE_SCROLL_LINES : MESSAGE_FLOW_TEXT_LINE_SCROLL_LINES;
const lines = repeated ? THREAD_STREAM_FLOW_REPEATED_TEXT_LINE_SCROLL_LINES : THREAD_STREAM_FLOW_TEXT_LINE_SCROLL_LINES;
return Math.max(1, Math.round(textLineHeight(container) * lines)) * direction;
}
function scrollMessageFlowToEnd(runtime: MessageFlowRuntime, behavior: ScrollBehavior = "auto"): void {
function scrollThreadStreamFlowToEnd(runtime: ThreadStreamFlowRuntime, behavior: ScrollBehavior = "auto"): void {
const container = runtime.container;
if (!container) return;
scrollMessageFlowToTop(container, messageFlowEndScrollTop(container), behavior);
scrollThreadStreamFlowToTop(container, threadStreamFlowEndScrollTop(container), behavior);
runtime.followingEnd = true;
}
function scrollMessageFlowToTop(container: HTMLElement, top: number, behavior: ScrollBehavior): void {
function scrollThreadStreamFlowToTop(container: HTMLElement, top: number, behavior: ScrollBehavior): void {
if (behavior === "smooth") {
container.scrollTo({ top, behavior });
return;
@ -281,37 +281,37 @@ function scrollMessageFlowToTop(container: HTMLElement, top: number, behavior: S
container.scrollTop = top;
}
function messageFlowManualScrollBehavior(runtime: MessageFlowRuntime, repeated: boolean): ScrollBehavior {
function threadStreamFlowManualScrollBehavior(runtime: ThreadStreamFlowRuntime, repeated: boolean): ScrollBehavior {
if (repeated) return "auto";
const win = runtime.container?.win;
return win?.matchMedia("(prefers-reduced-motion: reduce)").matches ? "auto" : "smooth";
}
function messageFlowEndScrollTop(container: HTMLElement): number {
function threadStreamFlowEndScrollTop(container: HTMLElement): number {
return Math.max(0, container.scrollHeight - container.clientHeight);
}
function clampMessageFlowScrollTop(container: HTMLElement, top: number): number {
return Math.max(0, Math.min(top, messageFlowEndScrollTop(container)));
function clampThreadStreamFlowScrollTop(container: HTMLElement, top: number): number {
return Math.max(0, Math.min(top, threadStreamFlowEndScrollTop(container)));
}
function scheduleMessageFlowEndRestore(runtime: MessageFlowRuntime): void {
function scheduleThreadStreamFlowEndRestore(runtime: ThreadStreamFlowRuntime): void {
const container = runtime.container;
if (!container || runtime.restoreFrame !== null) return;
runtime.restoreFrame = container.win.requestAnimationFrame(() => {
runtime.restoreFrame = null;
if (runtime.container === container && runtime.followingEnd) scrollMessageFlowToEnd(runtime);
if (runtime.container === container && runtime.followingEnd) scrollThreadStreamFlowToEnd(runtime);
});
}
function cancelMessageFlowEndRestore(runtime: MessageFlowRuntime): void {
function cancelThreadStreamFlowEndRestore(runtime: ThreadStreamFlowRuntime): void {
const container = runtime.container;
const frame = runtime.restoreFrame;
if (container && frame !== null) container.win.cancelAnimationFrame(frame);
runtime.restoreFrame = null;
}
function messageFlowBlocksShifted(
function threadStreamFlowBlocksShifted(
previous: readonly ThreadStreamFlowBlockIdentity[],
next: readonly ThreadStreamFlowBlockIdentity[],
): boolean {
@ -320,9 +320,9 @@ function messageFlowBlocksShifted(
return previous.some((block, index) => block.key !== next[index]?.key);
}
function captureMessageFlowReadingAnchor(container: HTMLElement): MessageFlowReadingAnchor | null {
function captureThreadStreamFlowReadingAnchor(container: HTMLElement): ThreadStreamFlowReadingAnchor | null {
const viewportTop = container.getBoundingClientRect().top;
for (const element of messageFlowBlockElements(container)) {
for (const element of threadStreamFlowBlockElements(container)) {
const rect = element.getBoundingClientRect();
if (rect.bottom >= viewportTop) {
const key = element.dataset["codexPanelBlockKey"];
@ -332,27 +332,27 @@ function captureMessageFlowReadingAnchor(container: HTMLElement): MessageFlowRea
return null;
}
function restoreMessageFlowReadingAnchor(container: HTMLElement, anchor: MessageFlowReadingAnchor): void {
const element = messageFlowBlockElements(container).find((candidate) => candidate.dataset["codexPanelBlockKey"] === anchor.key);
function restoreThreadStreamFlowReadingAnchor(container: HTMLElement, anchor: ThreadStreamFlowReadingAnchor): void {
const element = threadStreamFlowBlockElements(container).find((candidate) => candidate.dataset["codexPanelBlockKey"] === anchor.key);
if (!element) return;
const viewportTop = container.getBoundingClientRect().top;
const currentTop = element.getBoundingClientRect().top - viewportTop;
container.scrollTop += currentTop - anchor.top;
}
function messageFlowBlockElements(container: HTMLElement): HTMLElement[] {
return Array.from(container.querySelectorAll<HTMLElement>(".codex-panel__message-block"));
function threadStreamFlowBlockElements(container: HTMLElement): HTMLElement[] {
return Array.from(container.querySelectorAll<HTMLElement>(".codex-panel__thread-stream-block"));
}
function isMessageFlowAtEnd(container: HTMLElement): boolean {
return isMessageFlowTopAtEnd(container, container.scrollTop);
function isThreadStreamFlowAtEnd(container: HTMLElement): boolean {
return isThreadStreamFlowTopAtEnd(container, container.scrollTop);
}
function isMessageFlowTopAtEnd(container: HTMLElement, top: number): boolean {
return messageFlowEndScrollTop(container) - top <= 4;
function isThreadStreamFlowTopAtEnd(container: HTMLElement, top: number): boolean {
return threadStreamFlowEndScrollTop(container) - top <= 4;
}
function isMessageFlowViewportHidden(container: HTMLElement): boolean {
function isThreadStreamFlowViewportHidden(container: HTMLElement): boolean {
return container.clientWidth <= 0 || container.clientHeight <= 0;
}

View file

@ -3,9 +3,9 @@ import { type App, type Component, MarkdownRenderer, Notice } from "obsidian";
import { isAbsoluteFileHref, vaultRelativeFileLinkTarget } from "../../../../domain/vault/file-hrefs";
import { vaultFileLinkTarget } from "../../../../shared/obsidian/vault-file-links.obsidian";
import { notifyMessageContentRendered } from "./content-rendered-event.dom";
import { notifyThreadStreamContentRendered } from "./content-rendered-event.dom";
interface MarkdownMessageRendererOptions {
interface ThreadStreamMarkdownRendererOptions {
app: App;
owner: Component;
vaultPath: string;
@ -30,10 +30,10 @@ interface ObsidianAppWithInternalPlugins extends App {
};
}
export class MarkdownMessageRenderer {
export class ThreadStreamMarkdownRenderer {
private readonly renderGenerations = new WeakMap<HTMLElement, number>();
constructor(private readonly options: MarkdownMessageRendererOptions) {}
constructor(private readonly options: ThreadStreamMarkdownRendererOptions) {}
renderObsidianMarkdown(parent: HTMLElement, text: string): void {
const sourcePath = this.options.app.workspace.getActiveFile()?.path ?? "";
@ -46,7 +46,7 @@ export class MarkdownMessageRenderer {
bindRenderedWikiLinks(parent, sourcePath, this.options);
bindRenderedMarkdownFileLinks(parent, sourcePath, this.options);
bindRenderedTags(parent, this.options);
notifyMessageContentRendered(parent);
notifyThreadStreamContentRendered(parent);
});
}
}

View file

@ -12,7 +12,7 @@ import type {
} from "../../presentation/pending-requests/view-model";
import type { PendingRequestBlockActions } from "./context";
import { focusPendingRequestControl } from "./pending-request-block.dom";
import { createStatusMessageClassName } from "./status";
import { createStatusStreamItemClassName } from "./status";
export function pendingRequestBlockNode(
approvals: readonly PendingApprovalViewModel[],
@ -74,8 +74,8 @@ function PendingRequestBlock({
}, [autoFocusRequested, consumeAutoFocus, autoFocusSignature]);
if (!hasPendingRequests(pendingRequestCountsFromQueues({ approvals, pendingUserInputs, pendingMcpElicitations }))) return null;
return (
<div ref={requestRef} className={createStatusMessageClassName("codex-panel__pending-request-block", "warning")}>
<div className="codex-panel__message-role">Request</div>
<div ref={requestRef} className={createStatusStreamItemClassName("codex-panel__pending-request-block", "warning")}>
<div className="codex-panel__stream-item-role">Request</div>
{approvals.map((approval) => (
<ApprovalCard key={String(approval.requestId)} approval={approval} approvalDetails={approvalDetails} actions={actions} />
))}

View file

@ -15,13 +15,13 @@ export function statusNode(view: ThreadStreamStatusView): UiNode {
return <GenericStatus view={view} />;
}
export function createStatusMessageClassName(className: string, tone?: "warning"): string {
export function createStatusStreamItemClassName(className: string, tone?: "warning"): string {
return [
"codex-panel__message",
"codex-panel__message--tool",
"codex-panel__status-message",
"codex-panel__stream-item",
"codex-panel__stream-item--tool",
"codex-panel__status-stream-item",
className,
tone === "warning" ? "codex-panel__status-message--warning" : "",
tone === "warning" ? "codex-panel__status-stream-item--warning" : "",
]
.filter(Boolean)
.join(" ");
@ -35,16 +35,16 @@ function AgentRunSummary({
openThreadInNewView?: ((threadId: string) => void) | undefined;
}): UiNode {
return (
<StatusMessage label={view.label} className={view.className} state={view.state}>
<StatusStreamItem label={view.label} className={view.className} state={view.state}>
<div className="codex-panel__stream-summary">{view.summary}</div>
<AgentSummaryRows view={view} openThreadInNewView={openThreadInNewView} />
</StatusMessage>
</StatusStreamItem>
);
}
function TaskProgress({ view }: { view: Extract<ThreadStreamStatusView, { kind: "taskProgress" }> }): UiNode {
return (
<StatusMessage label={view.label} className={view.className} state={view.state}>
<StatusStreamItem label={view.label} className={view.className} state={view.state}>
{view.summary ? <div className="codex-panel__stream-summary">{view.summary}</div> : null}
{view.checklist.length === 0 ? (
<div className="codex-panel__stream-summary">Plan updated</div>
@ -69,23 +69,23 @@ function TaskProgress({ view }: { view: Extract<ThreadStreamStatusView, { kind:
))}
</ul>
)}
</StatusMessage>
</StatusStreamItem>
);
}
function ContextCompaction({ view }: { view: Extract<ThreadStreamStatusView, { kind: "contextCompaction" }> }): UiNode {
return (
<StatusMessage label={view.label} className={view.className} state={view.state}>
<StatusStreamItem label={view.label} className={view.className} state={view.state}>
<div className="codex-panel__stream-summary">{view.text}</div>
</StatusMessage>
</StatusStreamItem>
);
}
function GenericStatus({ view }: { view: Extract<ThreadStreamStatusView, { kind: "generic" }> }): UiNode {
return (
<StatusMessage label={view.label} className={view.className} state={view.state}>
<StatusStreamItem label={view.label} className={view.className} state={view.state}>
<div className="codex-panel__stream-summary">{view.text}</div>
</StatusMessage>
</StatusStreamItem>
);
}
@ -107,7 +107,7 @@ function Reasoning({ view }: { view: Extract<ThreadStreamStatusView, { kind: "re
);
}
function StatusMessage({
function StatusStreamItem({
label,
className,
state,
@ -118,10 +118,10 @@ function StatusMessage({
state: ExecutionState;
children: UiNode;
}): UiNode {
const classes = [createStatusMessageClassName(className), executionClassName(state)].filter(Boolean).join(" ");
const classes = [createStatusStreamItemClassName(className), executionClassName(state)].filter(Boolean).join(" ");
return (
<div className={classes}>
<div className="codex-panel__message-role">{label}</div>
<div className="codex-panel__stream-item-role">{label}</div>
{children}
</div>
);

View file

@ -92,7 +92,7 @@ function HistoryBar({ loadingHistory, loadOlderTurns }: { loadingHistory: boolea
}
function EmptyMessage(): UiNode {
return <div className="codex-panel__message codex-panel__message--system">Send a message to start a conversation.</div>;
return <div className="codex-panel__stream-item codex-panel__stream-item--system">Send a message to start a conversation.</div>;
}
function ActivityGroup({
@ -124,9 +124,9 @@ function ActivityGroup({
function SteeringActivity({ activity }: { activity: Extract<ThreadStreamActivityItemView, { type: "steering" }> }): UiNode {
return (
<div className="codex-panel__message codex-panel__message--tool codex-panel__detail-item codex-panel__detail codex-panel__detail--plain">
<div className="codex-panel__stream-item codex-panel__stream-item--tool codex-panel__detail-item codex-panel__detail codex-panel__detail--plain">
<div className="codex-panel__detail-header">
<span className="codex-panel__message-role codex-panel__detail-label">{activity.label}</span>
<span className="codex-panel__stream-item-role codex-panel__detail-label">{activity.label}</span>
</div>
<div className="codex-panel__stream-summary">{activity.text}</div>
</div>

View file

@ -3,7 +3,7 @@ import { useEffect, useLayoutEffect, useRef, useState } from "preact/hooks";
import { listenDomEvent, listenOutsideDomEvent } from "../../../../shared/dom/events.dom";
import type { ThreadStreamTextView } from "../../presentation/thread-stream/text-view";
import { MESSAGE_CONTENT_RENDERED_EVENT } from "./content-rendered-event.dom";
import { THREAD_STREAM_CONTENT_RENDERED_EVENT } from "./content-rendered-event.dom";
import type { TextItemContentContext } from "./context";
const USER_MESSAGE_COLLAPSE_HEIGHT_PX = 360;
@ -20,7 +20,7 @@ export function CollapsibleTextContent({ view, context }: { view: ThreadStreamTe
const update = () => {
setOverflows(content.scrollHeight > userMessageCollapseHeight(content) + 1);
};
const disposeRendered = listenDomEvent(content, MESSAGE_CONTENT_RENDERED_EVENT, update);
const disposeRendered = listenDomEvent(content, THREAD_STREAM_CONTENT_RENDERED_EVENT, update);
update();
content.win.requestAnimationFrame(update);
return disposeRendered;
@ -44,16 +44,16 @@ export function CollapsibleTextContent({ view, context }: { view: ThreadStreamTe
<div
ref={collapseRef}
className={[
"codex-panel__message-collapse",
overflows ? "codex-panel__message-collapse--overflow" : "",
overflows && expanded ? "codex-panel__message-collapse--expanded" : "",
"codex-panel__stream-item-collapse",
overflows ? "codex-panel__stream-item-collapse--overflow" : "",
overflows && expanded ? "codex-panel__stream-item-collapse--expanded" : "",
]
.filter(Boolean)
.join(" ")}
>
<TextContent key={view.contentKey} view={view} context={context} contentRef={contentRef} collapsed={overflows && !expanded} />
<details
className="codex-panel__message-collapse-details"
className="codex-panel__stream-item-collapse-details"
hidden={!overflows || expanded}
onToggle={(event) => {
if (!event.currentTarget.open) return;
@ -131,9 +131,9 @@ function TextContentContainer({
assignTextContentRef(contentRef, element);
}}
className={[
"codex-panel__message-content",
"codex-panel__stream-item-content",
markdown ? "markdown-rendered" : "",
collapsed ? "codex-panel__message-content--collapsed" : "",
collapsed ? "codex-panel__stream-item-content--collapsed" : "",
]
.filter(Boolean)
.join(" ")}

View file

@ -58,13 +58,13 @@ function TextHeader({ view, context }: { view: ThreadStreamTextView; context: Te
<TextAction
icon="copy"
label="Copy message"
className="codex-panel__copy-message"
className="codex-panel__copy-dialogue"
onClick={() => context.copyText?.(view.copyText ?? "")}
/>
) : null;
return (
<div ref={roleRef} className={`codex-panel__message-role${forkMenuOpen ? " codex-panel__message-role--fork-open" : ""}`}>
<div ref={roleRef} className={`codex-panel__stream-item-role${forkMenuOpen ? " codex-panel__stream-item-role--fork-open" : ""}`}>
<span>{view.roleLabel}</span>
{forkMenuOpen && fork ? (
<TextAction
@ -119,7 +119,7 @@ function TextAction({ icon, label, className, onClick }: { icon: string; label:
<IconButton
icon={icon}
label={label}
className={`clickable-icon codex-panel__message-action ${className}`}
className={`clickable-icon codex-panel__stream-item-action ${className}`}
onClick={(event) => {
event.preventDefault();
event.stopPropagation();

View file

@ -16,8 +16,8 @@
--codex-panel-size-settings-hook-list-max-height: 420px;
--codex-panel-size-toolbar-panel-max-height: 232px;
--codex-panel-size-toolbar-status-panel-max-height: 384px;
--codex-panel-size-message-collapse-max-height: 360px;
--codex-panel-size-message-collapse-fade-height: 72px;
--codex-panel-size-stream-item-collapse-max-height: 360px;
--codex-panel-size-stream-item-collapse-fade-height: 72px;
--codex-panel-size-goal-objective-collapse-max-height: 4.5lh;
--codex-panel-size-output-max-height: 420px;
--codex-panel-size-selection-rewrite-width: 520px;

View file

@ -32,7 +32,7 @@
grid-row: 1;
}
.codex-panel__region--message-stream {
.codex-panel__region--thread-stream {
grid-row: 2;
min-height: 0;
overflow: hidden;

View file

@ -80,7 +80,7 @@
right: 0;
bottom: 0;
left: 0;
height: var(--codex-panel-size-message-collapse-fade-height);
height: var(--codex-panel-size-stream-item-collapse-fade-height);
background: linear-gradient(to bottom, transparent, var(--codex-panel-surface));
pointer-events: none;
}

View file

@ -1,4 +1,4 @@
.codex-panel__messages {
.codex-panel__thread-stream {
box-sizing: border-box;
min-height: 0;
overflow-y: auto;
@ -12,7 +12,7 @@
margin-bottom: var(--codex-panel-section-gap);
}
.codex-panel__message-flow {
.codex-panel__thread-stream-flow {
width: 100%;
}
@ -60,12 +60,12 @@
white-space: pre-wrap;
}
.codex-panel__message-block {
.codex-panel__thread-stream-block {
display: flow-root;
min-width: 0;
}
.codex-panel__message {
.codex-panel__stream-item {
min-width: 0;
max-width: 100%;
margin-bottom: var(--codex-panel-edge-padding-x);
@ -76,7 +76,7 @@
user-select: text;
}
.codex-panel__message-role {
.codex-panel__stream-item-role {
display: flex;
align-items: center;
gap: var(--codex-panel-item-gap);
@ -86,50 +86,50 @@
font-weight: var(--codex-panel-role-label-weight);
}
.codex-panel__message-action {
.codex-panel__stream-item-action {
--icon-size: var(--codex-panel-size-icon-xs);
--icon-stroke: var(--icon-xs-stroke-width, 2px);
opacity: 0;
transition: opacity 120ms ease-in-out;
}
.codex-panel__message:hover .codex-panel__message-action,
.codex-panel__message-role--fork-open .codex-panel__message-action,
.codex-panel__message-action:focus-visible {
.codex-panel__stream-item:hover .codex-panel__stream-item-action,
.codex-panel__stream-item-role--fork-open .codex-panel__stream-item-action,
.codex-panel__stream-item-action:focus-visible {
opacity: 1;
}
.codex-panel__message--user {
.codex-panel__stream-item--user {
border-left-color: var(--codex-panel-color-accent);
}
.codex-panel__message--user .codex-panel__message-role {
.codex-panel__stream-item--user .codex-panel__stream-item-role {
color: var(--codex-panel-color-accent);
}
.codex-panel__message--assistant {
.codex-panel__stream-item--assistant {
border-left-color: var(--codex-panel-border-color);
}
.codex-panel__message--system,
.codex-panel__message--tool {
.codex-panel__stream-item--system,
.codex-panel__stream-item--tool {
color: var(--text-muted);
font-size: var(--font-ui-small);
}
.codex-panel__message--system {
.codex-panel__stream-item--system {
margin-bottom: var(--codex-panel-section-gap);
border-left-color: var(--codex-panel-border-color);
font-family: var(--font-interface);
line-height: var(--line-height-tight);
}
.codex-panel__message--system .codex-panel__message-role {
.codex-panel__stream-item--system .codex-panel__stream-item-role {
font-size: var(--codex-panel-secondary-label-size);
font-weight: var(--codex-panel-secondary-label-weight);
}
.codex-panel__message-content {
.codex-panel__stream-item-content {
min-width: 0;
max-width: 100%;
font-family: var(--codex-panel-content-font);
@ -139,14 +139,14 @@
user-select: text;
}
.codex-panel__message--system .codex-panel__message-content {
.codex-panel__stream-item--system .codex-panel__stream-item-content {
color: var(--text-muted);
font-family: var(--font-interface);
font-size: var(--font-ui-small);
line-height: var(--line-height-tight);
}
.codex-panel__message--system .codex-panel__output {
.codex-panel__stream-item--system .codex-panel__output {
margin-top: var(--codex-panel-item-gap);
}
@ -201,32 +201,32 @@
white-space: pre-wrap;
}
.codex-panel__message--system .codex-panel__meta-grid {
.codex-panel__stream-item--system .codex-panel__meta-grid {
grid-template-columns: minmax(max-content, 28%) minmax(0, 1fr);
margin-bottom: 0;
}
.codex-panel__message--review-result {
.codex-panel__stream-item--review-result {
border-left-color: var(--codex-panel-color-warning);
}
.codex-panel__message--review-result .codex-panel__message-role {
.codex-panel__stream-item--review-result .codex-panel__stream-item-role {
color: var(--codex-panel-color-warning);
}
.codex-panel__message--approval-result.codex-panel__execution--completed .codex-panel__message-role {
.codex-panel__stream-item--approval-result.codex-panel__execution--completed .codex-panel__stream-item-role {
color: var(--codex-panel-color-success);
}
.codex-panel__message--approval-result.codex-panel__execution--failed .codex-panel__message-role {
.codex-panel__stream-item--approval-result.codex-panel__execution--failed .codex-panel__stream-item-role {
color: var(--codex-panel-color-danger);
}
.codex-panel__message--user-input-result {
.codex-panel__stream-item--user-input-result {
border-left-color: var(--codex-panel-color-accent);
}
.codex-panel__message--user-input-result .codex-panel__message-role {
.codex-panel__stream-item--user-input-result .codex-panel__stream-item-role {
color: var(--codex-panel-color-accent);
}
@ -269,65 +269,65 @@
padding-bottom: var(--codex-panel-panel-gap);
}
.codex-panel__activity-group > .codex-panel__message,
.codex-panel__activity-group > .codex-panel__status-message,
.codex-panel__activity-group > .codex-panel__stream-item,
.codex-panel__activity-group > .codex-panel__status-stream-item,
.codex-panel__activity-group > .codex-panel__reasoning {
margin-top: var(--codex-panel-section-gap);
margin-bottom: var(--codex-panel-section-gap);
}
.codex-panel__message-content.markdown-rendered {
.codex-panel__stream-item-content.markdown-rendered {
white-space: normal;
}
.codex-panel__message-content.markdown-rendered > :first-child {
.codex-panel__stream-item-content.markdown-rendered > :first-child {
margin-top: 0;
}
.codex-panel__message-content.markdown-rendered > :last-child {
.codex-panel__stream-item-content.markdown-rendered > :last-child {
margin-bottom: 0;
}
.codex-panel__message-content.markdown-rendered pre {
.codex-panel__stream-item-content.markdown-rendered pre {
max-width: 100%;
overflow-x: auto;
white-space: pre-wrap;
}
.codex-panel__message-collapse {
.codex-panel__stream-item-collapse {
position: relative;
}
.codex-panel__message-content--collapsed {
.codex-panel__stream-item-content--collapsed {
position: relative;
max-height: min(var(--codex-panel-size-message-collapse-max-height), 45vh);
max-height: min(var(--codex-panel-size-stream-item-collapse-max-height), 45vh);
overflow: hidden;
}
.codex-panel__message-content--collapsed::after {
.codex-panel__stream-item-content--collapsed::after {
content: "";
position: absolute;
right: 0;
bottom: 0;
left: 0;
height: var(--codex-panel-size-message-collapse-fade-height);
height: var(--codex-panel-size-stream-item-collapse-fade-height);
background: linear-gradient(to bottom, transparent, var(--codex-panel-surface));
pointer-events: none;
}
.codex-panel__message-collapse-details {
.codex-panel__stream-item-collapse-details {
position: relative;
z-index: 1;
margin-top: var(--codex-panel-item-gap);
}
.codex-panel__message-collapse-details > summary {
.codex-panel__stream-item-collapse-details > summary {
cursor: default;
color: var(--text-muted);
font-size: var(--font-ui-smaller);
}
.codex-panel__message-collapse-details > summary:hover {
.codex-panel__stream-item-collapse-details > summary:hover {
color: var(--nav-item-color-hover, var(--text-normal));
}

View file

@ -1,4 +1,4 @@
.codex-panel__status-message {
.codex-panel__status-stream-item {
min-width: 0;
max-width: 100%;
margin-bottom: var(--codex-panel-section-gap);
@ -9,16 +9,16 @@
background: transparent;
}
.codex-panel__status-message .codex-panel__message-role {
.codex-panel__status-stream-item .codex-panel__stream-item-role {
font-size: var(--codex-panel-secondary-label-size);
font-weight: var(--codex-panel-secondary-label-weight);
}
.codex-panel__status-message--warning {
.codex-panel__status-stream-item--warning {
border-left-color: var(--codex-panel-color-warning);
}
.codex-panel__status-message--warning > .codex-panel__message-role {
.codex-panel__status-stream-item--warning > .codex-panel__stream-item-role {
color: var(--codex-panel-color-warning);
}
@ -33,7 +33,7 @@
background: transparent;
}
.codex-panel__detail-item .codex-panel__message-role {
.codex-panel__detail-item .codex-panel__stream-item-role {
font-size: var(--codex-panel-secondary-label-size);
font-weight: var(--codex-panel-secondary-label-weight);
}
@ -63,11 +63,11 @@
}
.codex-panel__detail-disclosure > .codex-panel__detail-header:hover,
.codex-panel__detail-disclosure > .codex-panel__detail-header:hover .codex-panel__message-role {
.codex-panel__detail-disclosure > .codex-panel__detail-header:hover .codex-panel__stream-item-role {
color: var(--nav-item-color-hover, var(--text-normal));
}
.codex-panel__detail-header .codex-panel__message-role {
.codex-panel__detail-header .codex-panel__stream-item-role {
margin-bottom: 0;
}

View file

@ -6,7 +6,7 @@
"20-chat-shell.css",
"21-chat-toolbar.css",
"22-chat-goal.css",
"23-chat-message-stream.css",
"23-chat-thread-stream.css",
"24-chat-stream-items.css",
"25-chat-pending-requests.css",
"26-chat-composer.css",

View file

@ -115,7 +115,7 @@ vi.mock("../../../../src/app-server/connection/connection-manager", () => {
installObsidianDomShims();
describe("CodexChatView connection lifecycle", () => {
let restoreDefaultMessageViewportMetrics: (() => void) | null = null;
let restoreDefaultThreadStreamViewportMetrics: (() => void) | null = null;
beforeAll(async () => {
({ CodexChatView } = await import("../../../../src/features/chat/host/view.obsidian"));
@ -125,7 +125,7 @@ describe("CodexChatView connection lifecycle", () => {
vi.useRealTimers();
notices.length = 0;
connectionMock.reset();
restoreDefaultMessageViewportMetrics = mockMessageViewportOffsetMetrics({ clientHeight: 320, clientWidth: 240 });
restoreDefaultThreadStreamViewportMetrics = mockThreadStreamViewportOffsetMetrics({ clientHeight: 320, clientWidth: 240 });
});
afterEach(async () => {
@ -134,8 +134,8 @@ describe("CodexChatView connection lifecycle", () => {
}
createdViews = [];
vi.useRealTimers();
restoreDefaultMessageViewportMetrics?.();
restoreDefaultMessageViewportMetrics = null;
restoreDefaultThreadStreamViewportMetrics?.();
restoreDefaultThreadStreamViewportMetrics = null;
document.body.replaceChildren();
});
@ -938,19 +938,19 @@ function composerPlaceholder(view: { containerEl: HTMLElement }): string | null
return composerElement(view).getAttribute("placeholder");
}
function mockMessageViewportOffsetMetrics(metrics: { clientHeight: number; clientWidth: number }): () => void {
function mockThreadStreamViewportOffsetMetrics(metrics: { clientHeight: number; clientWidth: number }): () => void {
const offsetHeightDescriptor = Object.getOwnPropertyDescriptor(HTMLElement.prototype, "offsetHeight");
const offsetWidthDescriptor = Object.getOwnPropertyDescriptor(HTMLElement.prototype, "offsetWidth");
Object.defineProperty(HTMLElement.prototype, "offsetHeight", {
configurable: true,
get() {
return this instanceof HTMLElement && this.classList.contains("codex-panel__messages") ? metrics.clientHeight : 0;
return this instanceof HTMLElement && this.classList.contains("codex-panel__thread-stream") ? metrics.clientHeight : 0;
},
});
Object.defineProperty(HTMLElement.prototype, "offsetWidth", {
configurable: true,
get() {
return this instanceof HTMLElement && this.classList.contains("codex-panel__messages") ? metrics.clientWidth : 0;
return this instanceof HTMLElement && this.classList.contains("codex-panel__thread-stream") ? metrics.clientWidth : 0;
},
});
return () => {

View file

@ -31,11 +31,11 @@ describe("ChatPanelShell", () => {
expect(container.classList.contains("codex-panel")).toBe(true);
expect(container.querySelector(".codex-panel__toolbar .codex-panel__toolbar-primary")).not.toBeNull();
expect(container.querySelector(".codex-panel__body > .codex-panel__region--message-stream")).toBe(
container.querySelector(".codex-panel__body > .codex-panel__messages"),
expect(container.querySelector(".codex-panel__body > .codex-panel__region--thread-stream")).toBe(
container.querySelector(".codex-panel__body > .codex-panel__thread-stream"),
);
expect(container.querySelector<HTMLTextAreaElement>(".codex-panel__region--composer textarea")?.value).toBe("");
expect(container.querySelector(".codex-panel__message-block")?.textContent).toContain("Send a message");
expect(container.querySelector(".codex-panel__thread-stream-block")?.textContent).toContain("Send a message");
await act(async () => {
unmountChatPanelShell(container);
@ -62,7 +62,7 @@ describe("ChatPanelShell", () => {
});
expect(container.querySelector<HTMLTextAreaElement>(".codex-panel__region--composer textarea")?.value).toBe("ready");
expect(container.querySelector(".codex-panel__message-block")?.textContent).toContain("Model set.");
expect(container.querySelector(".codex-panel__thread-stream-block")?.textContent).toContain("Model set.");
await act(async () => {
unmountChatPanelShell(container);
@ -294,7 +294,7 @@ describe("ChatPanelShell", () => {
});
expect(container.querySelector(".codex-panel__toolbar")).toBeNull();
expect(container.querySelector(".codex-panel__region--message-stream")).not.toBeNull();
expect(container.querySelector(".codex-panel__region--thread-stream")).not.toBeNull();
expect(container.querySelector(".codex-panel__region--composer")).not.toBeNull();
await act(async () => {
@ -358,7 +358,7 @@ describe("ChatPanelShell", () => {
expect(container.querySelector("section")).toBeNull();
expect(container.querySelector(".codex-panel__toolbar .codex-panel__toolbar-primary")).not.toBeNull();
expect(container.querySelector(".codex-panel__body .codex-panel__messages")).not.toBeNull();
expect(container.querySelector(".codex-panel__body .codex-panel__thread-stream")).not.toBeNull();
expect(container.querySelector<HTMLTextAreaElement>(".codex-panel__region--composer textarea")?.value).toBe("repair root");
await act(async () => {

View file

@ -15,7 +15,7 @@ import {
type ChatThreadStreamScrollBinding,
createChatThreadStreamScrollBinding,
} from "../../../../../src/features/chat/panel/thread-stream-scroll-binding";
import { MarkdownMessageRenderer } from "../../../../../src/features/chat/ui/thread-stream/markdown-renderer.obsidian";
import { ThreadStreamMarkdownRenderer } from "../../../../../src/features/chat/ui/thread-stream/markdown-renderer.obsidian";
import { ThreadStreamViewport } from "../../../../../src/features/chat/ui/thread-stream/stream-blocks";
import { renderUiRoot, unmountUiRoot } from "../../../../../src/shared/dom/preact-root.dom";
import { notices } from "../../../../mocks/obsidian";
@ -23,7 +23,7 @@ import { installObsidianDomShims } from "../../../../support/dom";
import { threadStreamReadModelFromChatState } from "../../support/shell-read-model";
import { chatStateFixture, chatStateWith } from "../../support/state";
import { withChatStateThreadStreamItems } from "../../support/thread-stream";
import { installMessageViewportMetrics, pendingApproval } from "../../ui/thread-stream/test-helpers";
import { installThreadStreamViewportMetrics, pendingApproval } from "../../ui/thread-stream/test-helpers";
const ESTIMATED_MESSAGE_BLOCK_HEIGHT = 96;
@ -242,15 +242,15 @@ describe("ThreadStreamPresenter scroll pinning", () => {
const { presenter, scrollPortBinding } = threadStreamPresenter(state);
renderThreadStreamPresenter(parent, presenter, state);
const messages = messageViewport(parent);
const messages = threadStreamViewport(parent);
Object.defineProperty(messages, "scrollHeight", { value: ESTIMATED_MESSAGE_BLOCK_HEIGHT, configurable: true });
installMessageViewportMetrics(messages, { clientHeight: 100 });
installThreadStreamViewportMetrics(messages, { clientHeight: 100 });
messages.scrollTop = 920;
const scrollIntoView = vi.spyOn(HTMLElement.prototype, "scrollIntoView");
scrollIntoView.mockClear();
scrollPortBinding.showLatest();
await settleMessageRender(messages);
await settleThreadStreamRender(messages);
expect(messages.scrollTop).toBe(0);
expect(scrollIntoView).not.toHaveBeenCalled();
@ -273,7 +273,7 @@ describe("ThreadStreamPresenter scroll pinning", () => {
const parent = document.createElement("div");
const { presenter, scrollPortBinding } = threadStreamPresenter(state);
const messages = parent.createDiv({ cls: "codex-panel__messages" });
const messages = parent.createDiv({ cls: "codex-panel__thread-stream" });
let scrollTop = 0;
let layoutSettled = false;
Object.defineProperties(messages, {
@ -298,14 +298,14 @@ describe("ThreadStreamPresenter scroll pinning", () => {
});
messages.scrollTop = 1000;
renderThreadStreamPresenter(messages, presenter, state);
await settleMessageRender(messages);
await settleThreadStreamRender(messages);
expect(messages.scrollTop).toBe(0);
scrollPortBinding.showLatest();
expect(messages.scrollTop).toBe(0);
layoutSettled = true;
await settleMessageRender(messages);
await settleThreadStreamRender(messages);
expect(messages.scrollTop).toBe(0);
});
@ -340,9 +340,9 @@ describe("ThreadStreamPresenter scroll pinning", () => {
const parent = document.createElement("div");
const { presenter, scrollPortBinding } = threadStreamPresenter(state);
renderThreadStreamPresenter(parent, presenter, state);
const messages = messageViewport(parent);
installMessageViewportMetrics(messages);
await settleMessageRender(messages);
const messages = threadStreamViewport(parent);
installThreadStreamViewportMetrics(messages);
await settleThreadStreamRender(messages);
unmountUiRoot(parent);
@ -369,17 +369,17 @@ describe("ThreadStreamPresenter scroll pinning", () => {
const parent = document.createElement("div");
const { presenter, scrollPortBinding } = threadStreamPresenter(state);
renderThreadStreamPresenter(parent, presenter, state);
const oldMessages = messageViewport(parent);
installMessageViewportMetrics(oldMessages, { clientHeight: 100, scrollHeight: 1000 });
await settleMessageRender(oldMessages);
const oldMessages = threadStreamViewport(parent);
installThreadStreamViewportMetrics(oldMessages, { clientHeight: 100, scrollHeight: 1000 });
await settleThreadStreamRender(oldMessages);
oldMessages.scrollTop = 125;
unmountUiRoot(parent);
renderThreadStreamPresenter(parent, presenter, state);
const newMessages = messageViewport(parent);
installMessageViewportMetrics(newMessages, { clientHeight: 100, scrollHeight: 1000 });
await settleMessageRender(newMessages);
const newMessages = threadStreamViewport(parent);
installThreadStreamViewportMetrics(newMessages, { clientHeight: 100, scrollHeight: 1000 });
await settleThreadStreamRender(newMessages);
scrollPortBinding.showLatest();
expect(newMessages.scrollTop).toBe(900);
@ -404,10 +404,10 @@ describe("ThreadStreamPresenter scroll pinning", () => {
const { presenter, scrollPortBinding } = threadStreamPresenter(state, vi.fn(), "/vault");
renderThreadStreamPresenter(parent, presenter, state);
const messages = messageViewport(parent);
installMessageViewportMetrics(messages, { clientHeight: 100, scrollHeight: 1000 });
const messages = threadStreamViewport(parent);
installThreadStreamViewportMetrics(messages, { clientHeight: 100, scrollHeight: 1000 });
scrollPortBinding.showLatest();
await settleMessageRender(messages);
await settleThreadStreamRender(messages);
expect(messages.scrollTop).toBe(900);
});
@ -429,13 +429,13 @@ describe("ThreadStreamPresenter scroll pinning", () => {
const parent = document.createElement("div");
const { presenter } = threadStreamPresenter(state);
const messages = parent.createDiv({ cls: "codex-panel__messages" });
installMessageViewportMetrics(messages);
const messages = parent.createDiv({ cls: "codex-panel__thread-stream" });
installThreadStreamViewportMetrics(messages);
renderThreadStreamPresenter(messages, presenter, state);
await settleMessageRender(messages);
await settleThreadStreamRender(messages);
Object.defineProperty(messages, "scrollHeight", { value: 1000, configurable: true });
installMessageViewportMetrics(messages, { clientHeight: 100 });
installThreadStreamViewportMetrics(messages, { clientHeight: 100 });
messages.scrollTop = 100;
messages.dispatchEvent(new Event("scroll"));
@ -453,7 +453,7 @@ describe("ThreadStreamPresenter scroll pinning", () => {
},
]);
renderThreadStreamPresenter(messages, presenter, state);
await settleMessageRender(messages);
await settleThreadStreamRender(messages);
expect(scrollIntoView).not.toHaveBeenCalled();
});
@ -475,9 +475,9 @@ describe("ThreadStreamPresenter scroll pinning", () => {
const parent = document.createElement("div");
const { presenter } = threadStreamPresenter(state);
const messages = parent.createDiv({ cls: "codex-panel__messages" });
const messages = parent.createDiv({ cls: "codex-panel__thread-stream" });
Object.defineProperty(messages, "scrollHeight", { value: 1000, configurable: true });
installMessageViewportMetrics(messages, { clientHeight: 100 });
installThreadStreamViewportMetrics(messages, { clientHeight: 100 });
messages.scrollTop = 920;
renderThreadStreamPresenter(messages, presenter, state);
@ -485,7 +485,7 @@ describe("ThreadStreamPresenter scroll pinning", () => {
scrollIntoView.mockClear();
messages.scrollTop = 100;
messages.dispatchEvent(new Event("scroll"));
await settleMessageRender(messages);
await settleThreadStreamRender(messages);
expect(scrollIntoView).not.toHaveBeenCalled();
});
@ -508,16 +508,16 @@ describe("ThreadStreamPresenter scroll pinning", () => {
const { presenter } = threadStreamPresenter(state);
renderThreadStreamPresenter(parent, presenter, state);
let messages = messageViewport(parent);
installMessageViewportMetrics(messages);
let messages = threadStreamViewport(parent);
installThreadStreamViewportMetrics(messages);
renderThreadStreamPresenter(parent, presenter, state);
messages = messageViewport(parent);
await settleMessageRender(messages);
expect(parent.querySelector(".codex-panel__messages")).not.toBeNull();
messages = threadStreamViewport(parent);
await settleThreadStreamRender(messages);
expect(parent.querySelector(".codex-panel__thread-stream")).not.toBeNull();
presenter.dispose();
expect(parent.querySelector(".codex-panel__messages")).not.toBeNull();
expect(parent.querySelector(".codex-panel__thread-stream")).not.toBeNull();
});
it("renders large thread streams through the flow viewport", async () => {
@ -539,11 +539,11 @@ describe("ThreadStreamPresenter scroll pinning", () => {
const { presenter } = threadStreamPresenter(state);
renderThreadStreamPresenter(parent, presenter, state);
const messages = messageViewport(parent);
installMessageViewportMetrics(messages, { clientHeight: 320, scrollHeight: 19_200 });
await settleMessageRender(messages);
const messages = threadStreamViewport(parent);
installThreadStreamViewportMetrics(messages, { clientHeight: 320, scrollHeight: 19_200 });
await settleThreadStreamRender(messages);
expect(parent.querySelector(".codex-panel__message-flow")).not.toBeNull();
expect(parent.querySelector(".codex-panel__thread-stream-flow")).not.toBeNull();
expect(parent.querySelectorAll("[data-codex-panel-block-key]").length).toBeGreaterThan(0);
});
});
@ -563,7 +563,7 @@ function testThreadStreamSurfaceContext(options: {
loadOlderTurns: vi.fn(),
renderObsidianMarkdown: vi.fn(),
renderStreamMarkdown: vi.fn(),
copyMessageText: vi.fn(),
copyDialogueText: vi.fn(),
actions: {
rollbackThread: vi.fn(),
forkThreadFromTurn: vi.fn(),
@ -614,7 +614,7 @@ async function renderedInternalLink(
staging.createEl("a", linkOptions);
return Promise.resolve();
});
const markdownRenderer = new MarkdownMessageRenderer({
const markdownRenderer = new ThreadStreamMarkdownRenderer({
app: context.app as never,
owner: {} as never,
vaultPath: context.vaultPath,
@ -645,7 +645,7 @@ async function renderedTag(
staging.createEl("a", linkOptions);
return Promise.resolve();
});
const markdownRenderer = new MarkdownMessageRenderer({
const markdownRenderer = new ThreadStreamMarkdownRenderer({
app: context.app as never,
owner: {} as never,
vaultPath: context.vaultPath,
@ -747,13 +747,13 @@ function tFile(path: string): TFile {
return Object.assign(new TFile(), { path, basename });
}
function messageViewport(parent: HTMLElement): HTMLElement {
const viewport = parent.querySelector<HTMLElement>(":scope > .codex-panel__messages");
function threadStreamViewport(parent: HTMLElement): HTMLElement {
const viewport = parent.querySelector<HTMLElement>(":scope > .codex-panel__thread-stream");
if (!viewport) throw new Error("Expected message viewport to be mounted.");
return viewport;
}
async function settleMessageRender(element: HTMLElement): Promise<void> {
async function settleThreadStreamRender(element: HTMLElement): Promise<void> {
await Promise.resolve();
await new Promise<void>((resolve) => {
element.win.requestAnimationFrame(() => {

View file

@ -6,21 +6,21 @@ import { describe, expect, it, vi } from "vitest";
import { implementPlanTargetFromState } from "../../../../../src/features/chat/application/turns/plan-implementation";
import { setCollaborationModeIntent } from "../../../../../src/features/chat/domain/runtime/intent";
import type { ThreadStreamItem } from "../../../../../src/features/chat/domain/thread-stream/items";
import { MESSAGE_CONTENT_RENDERED_EVENT } from "../../../../../src/features/chat/ui/thread-stream/content-rendered-event.dom";
import { MarkdownMessageRenderer } from "../../../../../src/features/chat/ui/thread-stream/markdown-renderer.obsidian";
import { THREAD_STREAM_CONTENT_RENDERED_EVENT } from "../../../../../src/features/chat/ui/thread-stream/content-rendered-event.dom";
import { ThreadStreamMarkdownRenderer } from "../../../../../src/features/chat/ui/thread-stream/markdown-renderer.obsidian";
import { deferred } from "../../../../support/async";
import { attributeValues, textContents, topLevelDetailsSummaries } from "../../../../support/dom";
import "./setup";
import {
expectPresent,
idleTurnLifecycle,
renderMessageBlockElement,
renderThreadStreamBlockElement,
renderThreadStreamBlocksInAct,
runningTurnLifecycle,
testDisclosures,
threadStreamBlocks,
unmountUiRootInAct,
withMessageContentScrollHeight,
withStreamItemContentScrollHeight,
} from "./test-helpers";
describe("thread stream rendering and message action menu", () => {
@ -112,16 +112,16 @@ describe("thread stream rendering and message action menu", () => {
renderThreadStreamBlocksInAct(parent, [activityBlock]);
const host = expectPresent(parent.querySelector<HTMLElement>(".codex-panel__message-block"));
const messageFlow = expectPresent(parent.querySelector<HTMLElement>(".codex-panel__message-flow"));
const host = expectPresent(parent.querySelector<HTMLElement>(".codex-panel__thread-stream-block"));
const threadStreamFlow = expectPresent(parent.querySelector<HTMLElement>(".codex-panel__thread-stream-flow"));
const activityGroup = expectPresent(parent.querySelector<HTMLDetailsElement>(".codex-panel__activity-group"));
Object.defineProperty(host, "offsetHeight", { value: 520, configurable: true });
await act(async () => {
activityGroup.dispatchEvent(new Event(MESSAGE_CONTENT_RENDERED_EVENT, { bubbles: true }));
activityGroup.dispatchEvent(new Event(THREAD_STREAM_CONTENT_RENDERED_EVENT, { bubbles: true }));
await Promise.resolve();
});
expect(messageFlow.style.height).toBe("");
expect(threadStreamFlow.style.height).toBe("");
expect(host.style.transform).toBe("");
Object.defineProperty(host, "offsetHeight", { value: 120, configurable: true });
@ -131,7 +131,7 @@ describe("thread stream rendering and message action menu", () => {
await Promise.resolve();
});
expect(messageFlow.style.height).toBe("");
expect(threadStreamFlow.style.height).toBe("");
expect(host.style.transform).toBe("");
unmountUiRootInAct(parent);
});
@ -144,14 +144,14 @@ describe("thread stream rendering and message action menu", () => {
renderThreadStreamBlocksInAct(parent, [block]);
const host = expectPresent(parent.querySelector<HTMLElement>(".codex-panel__message-block"));
const messageFlow = expectPresent(parent.querySelector<HTMLElement>(".codex-panel__message-flow"));
const host = expectPresent(parent.querySelector<HTMLElement>(".codex-panel__thread-stream-block"));
const threadStreamFlow = expectPresent(parent.querySelector<HTMLElement>(".codex-panel__thread-stream-flow"));
Object.defineProperty(host, "offsetHeight", { value: 520, configurable: true });
void act(() => {
host.dispatchEvent(new Event(MESSAGE_CONTENT_RENDERED_EVENT, { bubbles: true }));
host.dispatchEvent(new Event(THREAD_STREAM_CONTENT_RENDERED_EVENT, { bubbles: true }));
});
expect(messageFlow.style.height).toBe("");
expect(threadStreamFlow.style.height).toBe("");
expect(host.style.transform).toBe("");
Object.defineProperty(host, "offsetHeight", { value: 120, configurable: true });
@ -161,7 +161,7 @@ describe("thread stream rendering and message action menu", () => {
})[0],
]);
expect(messageFlow.style.height).toBe("");
expect(threadStreamFlow.style.height).toBe("");
expect(host.style.transform).toBe("");
unmountUiRootInAct(parent);
});
@ -171,11 +171,11 @@ describe("thread stream rendering and message action menu", () => {
items: [{ id: "review-1", kind: "reviewResult", role: "tool", text: "Auto-review denied this command." }],
})[0];
const element = renderMessageBlockElement(block);
const element = renderThreadStreamBlockElement(block);
expect(element.classList.contains("codex-panel__message--review-result")).toBe(true);
expect(element.classList.contains("codex-panel__stream-item--review-result")).toBe(true);
expect(element.classList.contains("codex-panel__detail--plain")).toBe(true);
expect(element.querySelector(".codex-panel__message-role")?.textContent).toBe("auto-review");
expect(element.querySelector(".codex-panel__stream-item-role")?.textContent).toBe("auto-review");
expect(element.textContent).toContain("Auto-review denied this command.");
expect(element.querySelector("details")).toBeNull();
});
@ -201,7 +201,7 @@ describe("thread stream rendering and message action menu", () => {
],
})[0];
const element = renderMessageBlockElement(block);
const element = renderThreadStreamBlockElement(block);
expect(element.classList.contains("codex-panel__execution--completed")).toBe(true);
expect(topLevelDetailsSummaries(element)).toEqual(["auto-review"]);
@ -243,11 +243,11 @@ describe("thread stream rendering and message action menu", () => {
renderMarkdown,
})[0];
const element = renderMessageBlockElement(block);
const element = renderThreadStreamBlockElement(block);
expect(element.classList.contains("codex-panel__message--system")).toBe(true);
expect(element.querySelector(".codex-panel__message-content")?.textContent).toBe("Available slash commands");
expect(element.querySelector(".codex-panel__message-content")?.classList.contains("markdown-rendered")).toBe(false);
expect(element.classList.contains("codex-panel__stream-item--system")).toBe(true);
expect(element.querySelector(".codex-panel__stream-item-content")?.textContent).toBe("Available slash commands");
expect(element.querySelector(".codex-panel__stream-item-content")?.classList.contains("markdown-rendered")).toBe(false);
expect(renderMarkdown).not.toHaveBeenCalled();
expect(element.querySelector("details")).toBeNull();
expect(element.querySelector(".codex-panel__output-title")).toBeNull();
@ -278,9 +278,9 @@ describe("thread stream rendering and message action menu", () => {
renderMarkdown: (element, text) => element.createDiv({ text }),
})[0];
const element = renderMessageBlockElement(block);
const element = renderThreadStreamBlockElement(block);
expect(element.classList.contains("codex-panel__message--tool")).toBe(true);
expect(element.classList.contains("codex-panel__stream-item--tool")).toBe(true);
expect(element.querySelector(".codex-panel__detail-label")?.textContent).toBe("goal");
expect(element.querySelector(".codex-panel__stream-summary")?.textContent).toBe("set: Ship the feature");
expect(element.querySelector("details")?.open).toBe(false);
@ -310,7 +310,7 @@ describe("thread stream rendering and message action menu", () => {
onRollback,
});
const rendered = blocks.map((block) => renderMessageBlockElement(block));
const rendered = blocks.map((block) => renderThreadStreamBlockElement(block));
expect(expectPresent(rendered[0]).querySelector(".codex-panel__rollback-turn")).toBeNull();
expect(expectPresent(rendered[1]).querySelector(".codex-panel__rollback-turn")).toBeNull();
@ -339,9 +339,9 @@ describe("thread stream rendering and message action menu", () => {
copyText,
});
const rendered = blocks.map((block) => renderMessageBlockElement(block));
const userButton = expectPresent(rendered[0]).querySelector<HTMLButtonElement>(".codex-panel__copy-message");
const assistantButton = expectPresent(rendered[1]).querySelector<HTMLButtonElement>(".codex-panel__copy-message");
const rendered = blocks.map((block) => renderThreadStreamBlockElement(block));
const userButton = expectPresent(rendered[0]).querySelector<HTMLButtonElement>(".codex-panel__copy-dialogue");
const assistantButton = expectPresent(rendered[1]).querySelector<HTMLButtonElement>(".codex-panel__copy-dialogue");
expect(userButton?.getAttribute("aria-label")).toBe("Copy message");
expect(assistantButton?.getAttribute("aria-label")).toBe("Copy message");
@ -373,8 +373,8 @@ describe("thread stream rendering and message action menu", () => {
onFork,
})[0];
const closedElement = renderMessageBlockElement(closedBlock);
expect(closedElement.querySelector(".codex-panel__copy-message")).not.toBeNull();
const closedElement = renderThreadStreamBlockElement(closedBlock);
expect(closedElement.querySelector(".codex-panel__copy-dialogue")).not.toBeNull();
const initialFork = expectPresent(closedElement.querySelector<HTMLButtonElement>(".codex-panel__fork-message"));
expect(initialFork.getAttribute("aria-label")).toBe("Fork from here");
expect(initialFork.getAttribute("data-icon")).toBe("lucide-split");
@ -390,8 +390,8 @@ describe("thread stream rendering and message action menu", () => {
onFork,
})[0];
const openElement = renderMessageBlockElement(openBlock);
expect(openElement.querySelector(".codex-panel__copy-message")).toBeNull();
const openElement = renderThreadStreamBlockElement(openBlock);
expect(openElement.querySelector(".codex-panel__copy-dialogue")).toBeNull();
expect(openElement.querySelector<HTMLButtonElement>(".codex-panel__fork-and-archive-message")?.getAttribute("aria-label")).toBe(
"Fork and archive",
);
@ -423,7 +423,7 @@ describe("thread stream rendering and message action menu", () => {
onFork,
})[0];
const element = renderMessageBlockElement(block);
const element = renderThreadStreamBlockElement(block);
expectPresent(element.querySelector<HTMLButtonElement>(".codex-panel__fork-and-archive-message")).click();
expect(onFork).toHaveBeenCalledWith({ itemId: "a1", turnId: "turn-1" }, true);
@ -452,8 +452,8 @@ describe("thread stream rendering and message action menu", () => {
],
}),
);
expect(parent.querySelector(".codex-panel__message-content")?.textContent).toBe("**answer** [[Note]]");
expect(parent.querySelector(".codex-panel__message-content a")).toBeNull();
expect(parent.querySelector(".codex-panel__stream-item-content")?.textContent).toBe("**answer** [[Note]]");
expect(parent.querySelector(".codex-panel__stream-item-content a")).toBeNull();
renderThreadStreamBlocksInAct(
parent,
@ -472,7 +472,7 @@ describe("thread stream rendering and message action menu", () => {
],
}),
);
expect(parent.querySelector(".codex-panel__message-content")?.textContent).toBe("markdown:**answer**");
expect(parent.querySelector(".codex-panel__stream-item-content")?.textContent).toBe("markdown:**answer**");
unmountUiRootInAct(parent);
});
@ -503,7 +503,7 @@ describe("thread stream rendering and message action menu", () => {
],
}),
);
expect(parent.querySelector('[data-codex-panel-block-key="item:a1"] .codex-panel__message-content')?.textContent).toBe(
expect(parent.querySelector('[data-codex-panel-block-key="item:a1"] .codex-panel__stream-item-content')?.textContent).toBe(
"markdown:first",
);
@ -525,7 +525,7 @@ describe("thread stream rendering and message action menu", () => {
}),
);
expect(parent.querySelector('[data-codex-panel-block-key="item:a1"] .codex-panel__message-content')?.textContent).toBe(
expect(parent.querySelector('[data-codex-panel-block-key="item:a1"] .codex-panel__stream-item-content')?.textContent).toBe(
"markdown:second",
);
unmountUiRootInAct(parent);
@ -545,7 +545,7 @@ describe("thread stream rendering and message action menu", () => {
element.textContent = `rendered:${text}`;
}),
);
const markdownRenderer = new MarkdownMessageRenderer({
const markdownRenderer = new ThreadStreamMarkdownRenderer({
app: { workspace: { getActiveFile: vi.fn(() => null) } } as never,
owner: {} as never,
vaultPath: "/vault",
@ -574,7 +574,7 @@ describe("thread stream rendering and message action menu", () => {
}),
);
await Promise.resolve();
const content = expectPresent(parent.querySelector<HTMLElement>(".codex-panel__message-content"));
const content = expectPresent(parent.querySelector<HTMLElement>(".codex-panel__stream-item-content"));
expect(content.textContent).toBe("rendered:old");
renderThreadStreamBlocksInAct(
@ -594,7 +594,7 @@ describe("thread stream rendering and message action menu", () => {
],
}),
);
const contentAfterUpdate = expectPresent(parent.querySelector<HTMLElement>(".codex-panel__message-content"));
const contentAfterUpdate = expectPresent(parent.querySelector<HTMLElement>(".codex-panel__stream-item-content"));
expect(contentAfterUpdate).toBe(content);
expect(contentAfterUpdate.textContent).toBe("rendered:old");
@ -640,7 +640,7 @@ describe("thread stream rendering and message action menu", () => {
}),
);
expect(parent.querySelector(".codex-panel__message-content")?.innerHTML).toBe("<strong>stream:old</strong>");
expect(parent.querySelector(".codex-panel__stream-item-content")?.innerHTML).toBe("<strong>stream:old</strong>");
expect(renderStreamMarkdown).toHaveBeenCalledWith(expect.any(HTMLElement), "old");
expect(baseContext.renderObsidianMarkdown).not.toHaveBeenCalled();
expect(renderMarkdown).not.toHaveBeenCalled();
@ -655,7 +655,7 @@ describe("thread stream rendering and message action menu", () => {
element.textContent = `obsidian:${text}`;
return Promise.resolve();
});
const markdownRenderer = new MarkdownMessageRenderer({
const markdownRenderer = new ThreadStreamMarkdownRenderer({
app: { workspace: { getActiveFile: vi.fn(() => null) } } as never,
owner: {} as never,
vaultPath: "/vault",
@ -683,7 +683,7 @@ describe("thread stream rendering and message action menu", () => {
);
await Promise.resolve();
expect(parent.querySelector(".codex-panel__message-content")?.textContent).toBe("obsidian:**done**");
expect(parent.querySelector(".codex-panel__stream-item-content")?.textContent).toBe("obsidian:**done**");
expect(renderMarkdown).toHaveBeenCalledOnce();
renderMarkdown.mockRestore();
unmountUiRootInAct(parent);
@ -704,7 +704,7 @@ describe("thread stream rendering and message action menu", () => {
element.textContent = `fresh:${text}`;
return Promise.resolve();
});
const markdownRenderer = new MarkdownMessageRenderer({
const markdownRenderer = new ThreadStreamMarkdownRenderer({
app: { workspace: { getActiveFile: vi.fn(() => null) } } as never,
owner: {} as never,
vaultPath: "/vault",
@ -746,8 +746,8 @@ describe("thread stream rendering and message action menu", () => {
const runningBlock = threadStreamBlocks(context)[0];
const completedBlock = threadStreamBlocks({ ...context, turnLifecycle: idleTurnLifecycle() })[0];
expect(renderMessageBlockElement(runningBlock).querySelector(".codex-panel__copy-message")).toBeNull();
expect(renderMessageBlockElement(completedBlock).querySelector(".codex-panel__copy-message")).not.toBeNull();
expect(renderThreadStreamBlockElement(runningBlock).querySelector(".codex-panel__copy-dialogue")).toBeNull();
expect(renderThreadStreamBlockElement(completedBlock).querySelector(".codex-panel__copy-dialogue")).not.toBeNull();
});
it("renders implement plan action for eligible proposed plans", () => {
@ -770,7 +770,7 @@ describe("thread stream rendering and message action menu", () => {
onImplementPlan,
})[0];
const element = renderMessageBlockElement(block);
const element = renderThreadStreamBlockElement(block);
const button = element.querySelector<HTMLButtonElement>(".codex-panel__implement-plan");
expect(button?.getAttribute("aria-label")).toBe("Implement plan");
@ -840,7 +840,7 @@ describe("thread stream rendering and message action menu", () => {
copyText: vi.fn(),
})[0];
expect(renderMessageBlockElement(block).querySelector(".codex-panel__copy-message")).toBeNull();
expect(renderThreadStreamBlockElement(block).querySelector(".codex-panel__copy-dialogue")).toBeNull();
});
it("renders copy and rollback actions together when both apply", () => {
@ -853,8 +853,8 @@ describe("thread stream rendering and message action menu", () => {
onRollback,
})[0];
const element = renderMessageBlockElement(block);
element.querySelector<HTMLButtonElement>(".codex-panel__copy-message")?.click();
const element = renderThreadStreamBlockElement(block);
element.querySelector<HTMLButtonElement>(".codex-panel__copy-dialogue")?.click();
element.querySelector<HTMLButtonElement>(".codex-panel__rollback-turn")?.click();
expect(copyText).toHaveBeenCalledWith("latest");
@ -862,7 +862,7 @@ describe("thread stream rendering and message action menu", () => {
});
it("collapses tall user messages without changing the copy payload", () => {
withMessageContentScrollHeight(500, () => {
withStreamItemContentScrollHeight(500, () => {
const parent = document.createElement("div");
const copyText = vi.fn();
const expandedMessages = new Set<string>();
@ -895,10 +895,10 @@ describe("thread stream rendering and message action menu", () => {
};
render();
const content = () => parent.querySelector<HTMLElement>(".codex-panel__message-content");
const details = () => parent.querySelector<HTMLDetailsElement>(".codex-panel__message-collapse-details");
const content = () => parent.querySelector<HTMLElement>(".codex-panel__stream-item-content");
const details = () => parent.querySelector<HTMLDetailsElement>(".codex-panel__stream-item-collapse-details");
expect(content()?.classList.contains("codex-panel__message-content--collapsed")).toBe(true);
expect(content()?.classList.contains("codex-panel__stream-item-content--collapsed")).toBe(true);
expect(details()?.hidden).toBe(false);
expect(details()?.querySelector("summary")?.textContent).toBe("Show more");
@ -911,7 +911,7 @@ describe("thread stream rendering and message action menu", () => {
}
render();
expect(expandedMessages.has("u1")).toBe(true);
expect(content()?.classList.contains("codex-panel__message-content--collapsed")).toBe(false);
expect(content()?.classList.contains("codex-panel__stream-item-content--collapsed")).toBe(false);
expect(details()?.hidden).toBe(true);
expect(onDisclosureToggle).toHaveBeenCalledWith("userMessageExpanded", "u1", true);
@ -920,11 +920,11 @@ describe("thread stream rendering and message action menu", () => {
});
render();
expect(expandedMessages.has("u1")).toBe(false);
expect(content()?.classList.contains("codex-panel__message-content--collapsed")).toBe(true);
expect(content()?.classList.contains("codex-panel__stream-item-content--collapsed")).toBe(true);
expect(details()?.hidden).toBe(false);
expect(onDisclosureToggle).toHaveBeenCalledWith("userMessageExpanded", "u1", false);
parent.querySelector<HTMLButtonElement>(".codex-panel__copy-message")?.click();
parent.querySelector<HTMLButtonElement>(".codex-panel__copy-dialogue")?.click();
expect(copyText).toHaveBeenCalledWith("full copied text");
unmountUiRootInAct(parent);
parent.remove();
@ -932,16 +932,16 @@ describe("thread stream rendering and message action menu", () => {
});
it("does not show the collapse control for short user messages or assistant messages", () => {
withMessageContentScrollHeight(120, () => {
withStreamItemContentScrollHeight(120, () => {
const shortUserBlock = threadStreamBlocks({
items: [{ id: "u1", kind: "dialogue", dialogueKind: "user", role: "user", text: "short", turnId: "turn-1" }],
})[0];
const shortUser = renderMessageBlockElement(shortUserBlock);
const shortUser = renderThreadStreamBlockElement(shortUserBlock);
expect(shortUser.querySelector<HTMLDetailsElement>(".codex-panel__message-collapse-details")?.hidden).toBe(true);
expect(shortUser.querySelector<HTMLDetailsElement>(".codex-panel__stream-item-collapse-details")?.hidden).toBe(true);
});
withMessageContentScrollHeight(500, () => {
withStreamItemContentScrollHeight(500, () => {
const assistantBlock = threadStreamBlocks({
items: [
{
@ -955,9 +955,9 @@ describe("thread stream rendering and message action menu", () => {
},
],
})[0];
const assistant = renderMessageBlockElement(assistantBlock);
const assistant = renderThreadStreamBlockElement(assistantBlock);
expect(assistant.querySelector(".codex-panel__message-collapse-details")).toBeNull();
expect(assistant.querySelector(".codex-panel__stream-item-collapse-details")).toBeNull();
});
});
@ -981,7 +981,7 @@ describe("thread stream rendering and message action menu", () => {
],
})[0];
const element = renderMessageBlockElement(block);
const element = renderThreadStreamBlockElement(block);
expect(element.querySelector(".codex-panel__stream-summary")?.textContent).toBe("npm run check (exit 1)");
expect(element.querySelector(".codex-panel__stream-summary")?.getAttribute("title")).toBeNull();
@ -1012,7 +1012,7 @@ describe("thread stream rendering and message action menu", () => {
],
})[0];
const element = renderMessageBlockElement(block);
const element = renderThreadStreamBlockElement(block);
const metaText = element.querySelector(".codex-panel__meta-grid")?.textContent ?? "";
expect(metaText).toContain("commandnpm run check");
@ -1042,7 +1042,7 @@ describe("thread stream rendering and message action menu", () => {
],
})[0];
const element = renderMessageBlockElement(block);
const element = renderThreadStreamBlockElement(block);
expect(textContents(element, "details summary")).toEqual(["read"]);
});
@ -1066,7 +1066,7 @@ describe("thread stream rendering and message action menu", () => {
],
})[0];
const element = renderMessageBlockElement(block);
const element = renderThreadStreamBlockElement(block);
expect(element.querySelector(".codex-panel__stream-summary")?.textContent).toBe('"semantic target" in src');
});
@ -1088,7 +1088,7 @@ describe("thread stream rendering and message action menu", () => {
],
})[0];
const element = renderMessageBlockElement(block);
const element = renderThreadStreamBlockElement(block);
expect(element.querySelector(".codex-panel__stream-summary")?.textContent).toBe("src/main.ts");
expect(topLevelDetailsSummaries(element)).toEqual(["file change"]);
@ -1113,7 +1113,7 @@ describe("thread stream rendering and message action menu", () => {
],
})[0];
const element = renderMessageBlockElement(block);
const element = renderThreadStreamBlockElement(block);
expect(element.querySelector(".codex-panel__stream-summary")?.textContent).toBe("src/main.ts (failed)");
});
@ -1145,7 +1145,7 @@ describe("thread stream rendering and message action menu", () => {
openTurnDiff,
});
const assistant = renderMessageBlockElement(expectPresent(blocks.find((block) => block.key === "item:a1")));
const assistant = renderThreadStreamBlockElement(expectPresent(blocks.find((block) => block.key === "item:a1")));
const button = assistant.querySelector<HTMLButtonElement>(".codex-panel__open-turn-diff");
const summary = assistant.querySelector<HTMLElement>(".codex-panel__edited-files summary");
@ -1184,9 +1184,9 @@ describe("thread stream rendering and message action menu", () => {
],
});
const user = renderMessageBlockElement(expectPresent(blocks.find((block) => block.key === "item:u1")));
const user = renderThreadStreamBlockElement(expectPresent(blocks.find((block) => block.key === "item:u1")));
expect(user.querySelector(".codex-panel__message-content")?.textContent).toBe("この続きです");
expect(user.querySelector(".codex-panel__stream-item-content")?.textContent).toBe("この続きです");
expect(user.querySelector(".codex-panel__referenced-thread")?.textContent).toContain("Referenced 参照元");
expect(user.querySelector(".codex-panel__referenced-thread")?.textContent).toContain("2/20 turns");
expect(user.querySelector<HTMLElement>(".codex-panel__referenced-thread")?.getAttribute("title")).toBeNull();
@ -1207,10 +1207,10 @@ describe("thread stream rendering and message action menu", () => {
],
});
const user = renderMessageBlockElement(expectPresent(blocks.find((block) => block.key === "item:u1")));
const user = renderThreadStreamBlockElement(expectPresent(blocks.find((block) => block.key === "item:u1")));
const summary = user.querySelector<HTMLElement>(".codex-panel__mentioned-files summary");
expect(user.querySelector(".codex-panel__message-content")?.textContent).toBe("Read [[Alpha]].");
expect(user.querySelector(".codex-panel__stream-item-content")?.textContent).toBe("Read [[Alpha]].");
expect(summary?.textContent).toBe("Context · 1 item");
expect(summary?.tabIndex).toBe(-1);
expect(user.querySelector(".codex-panel__mentioned-files")?.textContent).toContain("Alpha");
@ -1241,7 +1241,7 @@ describe("thread stream rendering and message action menu", () => {
openTurnDiff: vi.fn(),
});
const assistant = renderMessageBlockElement(expectPresent(blocks.find((block) => block.key === "item:a1")));
const assistant = renderThreadStreamBlockElement(expectPresent(blocks.find((block) => block.key === "item:a1")));
expect(assistant.querySelector(".codex-panel__edited-files")?.textContent).toContain("Edited 1 file");
expect(assistant.querySelector(".codex-panel__open-turn-diff")).toBeNull();

View file

@ -3,7 +3,7 @@
import { h, type ComponentChild as UiNode } from "preact";
import { act } from "preact/test-utils";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { MESSAGE_CONTENT_RENDERED_EVENT } from "../../../../../src/features/chat/ui/thread-stream/content-rendered-event.dom";
import { THREAD_STREAM_CONTENT_RENDERED_EVENT } from "../../../../../src/features/chat/ui/thread-stream/content-rendered-event.dom";
import {
ThreadStreamFlowFrame,
type ThreadStreamScrollCommand,
@ -101,14 +101,14 @@ describe("thread stream flow scrolling", () => {
setHeights({ first: 500, second: 300, third: 300 });
void act(() => {
blockElement("first").dispatchEvent(new Event(MESSAGE_CONTENT_RENDERED_EVENT, { bubbles: true }));
blockElement("first").dispatchEvent(new Event(THREAD_STREAM_CONTENT_RENDERED_EVENT, { bubbles: true }));
});
expect(messages.scrollTop).toBe(360);
});
it("keeps hidden thread streams pinned when the viewport size returns", () => {
const restoreInitialMetrics = installMessageViewportPrototypeMetrics({ scrollHeight: 600, viewport: { width: 0, height: 0 } });
const restoreInitialMetrics = installThreadStreamViewportPrototypeMetrics({ scrollHeight: 600, viewport: { width: 0, height: 0 } });
const { messages, resizeViewport } = renderFlowThreadStream(
["first", "second"],
{ first: 300, second: 300 },
@ -139,7 +139,7 @@ describe("thread stream flow scrolling", () => {
ref(element: HTMLDivElement | null) {
if (!element || nativeAnchoringApplied) return;
nativeAnchoringApplied = true;
const viewport = element.closest<HTMLElement>(".codex-panel__messages");
const viewport = element.closest<HTMLElement>(".codex-panel__thread-stream");
if (viewport) viewport.scrollTop += 180;
},
});
@ -163,7 +163,7 @@ describe("thread stream flow scrolling", () => {
setHeights({ message: 420 });
void act(() => {
blockElement("message").dispatchEvent(new Event(MESSAGE_CONTENT_RENDERED_EVENT, { bubbles: true }));
blockElement("message").dispatchEvent(new Event(THREAD_STREAM_CONTENT_RENDERED_EVENT, { bubbles: true }));
});
expect(messages.scrollTop).toBe(320);
@ -322,7 +322,7 @@ function renderFlowThreadStream(
};
render(keys, heights);
const messages = messageViewport(parent);
const messages = threadStreamViewport(parent);
installFlowMetrics(
messages,
() => currentKeys,
@ -348,8 +348,8 @@ function renderFlowThreadStream(
};
}
function messageViewport(parent: HTMLElement): HTMLElement {
const element = parent.querySelector<HTMLElement>(".codex-panel__messages");
function threadStreamViewport(parent: HTMLElement): HTMLElement {
const element = parent.querySelector<HTMLElement>(".codex-panel__thread-stream");
if (!element) throw new Error("Expected message viewport.");
return element;
}
@ -383,7 +383,7 @@ function installFlowMetrics(
messages.scrollTop = initialScrollTop;
messages.getBoundingClientRect = () => messageRect(0, viewport().height);
Array.from(messages.querySelectorAll<HTMLElement>(".codex-panel__message-block")).forEach((element) => {
Array.from(messages.querySelectorAll<HTMLElement>(".codex-panel__thread-stream-block")).forEach((element) => {
installBlockRect(element, messages, heights);
});
}
@ -450,7 +450,7 @@ function flushAnimationFrame(): void {
});
}
function installMessageViewportPrototypeMetrics(metrics: {
function installThreadStreamViewportPrototypeMetrics(metrics: {
scrollHeight: number;
viewport: { width: number; height: number };
}): () => void {
@ -463,31 +463,31 @@ function installMessageViewportPrototypeMetrics(metrics: {
Object.defineProperties(HTMLElement.prototype, {
scrollHeight: {
get() {
return this instanceof HTMLElement && this.classList.contains("codex-panel__messages") ? metrics.scrollHeight : 0;
return this instanceof HTMLElement && this.classList.contains("codex-panel__thread-stream") ? metrics.scrollHeight : 0;
},
configurable: true,
},
clientHeight: {
get() {
return this instanceof HTMLElement && this.classList.contains("codex-panel__messages") ? metrics.viewport.height : 0;
return this instanceof HTMLElement && this.classList.contains("codex-panel__thread-stream") ? metrics.viewport.height : 0;
},
configurable: true,
},
clientWidth: {
get() {
return this instanceof HTMLElement && this.classList.contains("codex-panel__messages") ? metrics.viewport.width : 0;
return this instanceof HTMLElement && this.classList.contains("codex-panel__thread-stream") ? metrics.viewport.width : 0;
},
configurable: true,
},
offsetHeight: {
get() {
return this instanceof HTMLElement && this.classList.contains("codex-panel__messages") ? metrics.viewport.height : 0;
return this instanceof HTMLElement && this.classList.contains("codex-panel__thread-stream") ? metrics.viewport.height : 0;
},
configurable: true,
},
offsetWidth: {
get() {
return this instanceof HTMLElement && this.classList.contains("codex-panel__messages") ? metrics.viewport.width : 0;
return this instanceof HTMLElement && this.classList.contains("codex-panel__thread-stream") ? metrics.viewport.width : 0;
},
configurable: true,
},
@ -513,7 +513,7 @@ function restorePrototypeProperty(object: object, key: PropertyKey, descriptor:
function installBlockRect(element: HTMLElement, messages: HTMLElement, heights: () => Record<string, number>): void {
element.getBoundingClientRect = () => {
const key = element.dataset["codexPanelBlockKey"] ?? "";
const siblings = Array.from(messages.querySelectorAll<HTMLElement>(".codex-panel__message-block"));
const siblings = Array.from(messages.querySelectorAll<HTMLElement>(".codex-panel__thread-stream-block"));
const index = siblings.indexOf(element);
const top = siblings
.slice(0, Math.max(0, index))

View file

@ -19,8 +19,8 @@ import {
pendingOtherUserInput,
pendingRequestActions,
pendingUserInput,
renderMessageBlockElement,
renderPendingRequestNode,
renderThreadStreamBlockElement,
renderThreadStreamBlocksInAct,
setNativeInputValue,
threadStreamBlocks,
@ -48,10 +48,10 @@ describe("pending request renderer decisions", () => {
);
expect(parent.querySelectorAll(".codex-panel__pending-request-block")).toHaveLength(1);
expect(parent.querySelector(".codex-panel__pending-request-block")?.classList.contains("codex-panel__status-message")).toBe(true);
expect(parent.querySelector(".codex-panel__pending-request-block")?.classList.contains("codex-panel__status-message--warning")).toBe(
true,
);
expect(parent.querySelector(".codex-panel__pending-request-block")?.classList.contains("codex-panel__status-stream-item")).toBe(true);
expect(
parent.querySelector(".codex-panel__pending-request-block")?.classList.contains("codex-panel__status-stream-item--warning"),
).toBe(true);
expect(parent.querySelector<HTMLButtonElement>(".codex-panel__pending-request-button.mod-cta")?.textContent).toBe("Submit");
expect(parent.querySelector(".codex-panel__user-input-answer .codex-panel__user-input-radio")).not.toBeNull();
expect(parent.querySelector<HTMLInputElement>(".codex-panel__user-input-radio")?.checked).toBe(true);
@ -381,11 +381,11 @@ describe("pending request renderer decisions", () => {
renderMarkdown,
})[0];
const element = renderMessageBlockElement(block);
const element = renderThreadStreamBlockElement(block);
expect(element.querySelector(".codex-panel__message-role")?.textContent).toBe("Input");
expect(element.querySelector(".codex-panel__message-content")?.textContent).toBe("Input submitted for 1 question.");
expect(element.querySelector(".codex-panel__message-content")?.classList.contains("markdown-rendered")).toBe(false);
expect(element.querySelector(".codex-panel__stream-item-role")?.textContent).toBe("Input");
expect(element.querySelector(".codex-panel__stream-item-content")?.textContent).toBe("Input submitted for 1 question.");
expect(element.querySelector(".codex-panel__stream-item-content")?.classList.contains("markdown-rendered")).toBe(false);
expect(renderMarkdown).not.toHaveBeenCalled();
expect(element.textContent).not.toContain("Approval");
expect(element.querySelector("details summary")?.textContent).toBe("Question: Scope");
@ -412,12 +412,12 @@ describe("pending request renderer decisions", () => {
],
})[0];
const element = renderMessageBlockElement(block);
const element = renderThreadStreamBlockElement(block);
expect(element.classList.contains("codex-panel__message--approval-result")).toBe(true);
expect(element.classList.contains("codex-panel__stream-item--approval-result")).toBe(true);
expect(element.classList.contains("codex-panel__detail")).toBe(true);
expect(element.classList.contains("codex-panel__execution--completed")).toBe(true);
expect(element.querySelector(".codex-panel__message-content")).toBeNull();
expect(element.querySelector(".codex-panel__stream-item-content")).toBeNull();
expect(element.querySelector(".codex-panel__detail-header")?.textContent).toBe("approval");
expect(element.querySelector(".codex-panel__stream-summary")?.textContent).toBe("Allowed for this session: Need access");
expect(element.querySelector("details summary")?.textContent).toBe("approval");
@ -458,7 +458,7 @@ describe("pending request renderer decisions", () => {
const block = blocks.find((candidate) => candidate.key === "item:assistant-1");
if (!block) throw new Error("Expected assistant block");
const element = renderMessageBlockElement(block);
const element = renderThreadStreamBlockElement(block);
expect(element.querySelector(".codex-panel__auto-reviews summary")?.textContent).toBe("Auto-reviewed 2 requests");
expect(element.querySelector(".codex-panel__auto-reviews")?.textContent).toContain("Auto-review approved: npm test");

View file

@ -7,7 +7,7 @@ import { textContents, topLevelDetailsSummaries } from "../../../../support/dom"
import "./setup";
import {
expectPresent,
renderMessageBlockElement,
renderThreadStreamBlockElement,
renderThreadStreamBlocksInAct,
runningTurnLifecycle,
testDisclosures,
@ -36,7 +36,7 @@ describe("thread stream item renderer decisions", () => {
],
})[0];
const element = renderMessageBlockElement(block);
const element = renderThreadStreamBlockElement(block);
expect(element.querySelector(".codex-panel__stream-summary")?.textContent).toBe("123");
expect(topLevelDetailsSummaries(element)).toEqual(["github.pull_request_read"]);
@ -71,7 +71,7 @@ describe("thread stream item renderer decisions", () => {
disclosures: testDisclosures({ activityGroups: ["turn"] }),
}).find((item) => item.key === "activity:turn-turn-activity");
const element = renderMessageBlockElement(expectPresent(block));
const element = renderThreadStreamBlockElement(expectPresent(block));
expect(element.querySelector(".codex-panel__detail-header")?.textContent).toBe("steer");
expect(element.querySelector(".codex-panel__stream-summary")?.textContent).toBe("also check tests and keep the summary compact");
@ -94,7 +94,7 @@ describe("thread stream item renderer decisions", () => {
],
})[0];
const element = renderMessageBlockElement(block);
const element = renderThreadStreamBlockElement(block);
expect(element.querySelector(".codex-panel__stream-summary")?.textContent).toBe("assets/image.png");
expect(element.querySelector(".codex-panel__stream-summary")?.getAttribute("title")).toBeNull();
@ -118,7 +118,7 @@ describe("thread stream item renderer decisions", () => {
],
})[0];
const element = renderMessageBlockElement(block);
const element = renderThreadStreamBlockElement(block);
expect(element.querySelector(".codex-panel__stream-summary")?.textContent).toBe("search: codex app-server");
});
@ -164,7 +164,7 @@ describe("thread stream item renderer decisions", () => {
],
})[0];
const element = renderMessageBlockElement(block);
const element = renderThreadStreamBlockElement(block);
expect(element.querySelector(".codex-panel__stream-summary")?.textContent).toBe("/tmp/image.png");
});
@ -185,7 +185,7 @@ describe("thread stream item renderer decisions", () => {
],
})[0];
const element = renderMessageBlockElement(block);
const element = renderThreadStreamBlockElement(block);
expect(element.querySelector(".codex-panel__stream-summary")?.textContent).toBe("/vault/project");
});
@ -213,7 +213,7 @@ describe("thread stream item renderer decisions", () => {
],
})[0];
const element = renderMessageBlockElement(block);
const element = renderThreadStreamBlockElement(block);
expect(topLevelDetailsSummaries(element)).toEqual(["hook"]);
expect(element.classList.contains("codex-panel__execution--completed")).toBe(true);
@ -259,7 +259,7 @@ describe("thread stream item renderer decisions", () => {
disclosures: testDisclosures({ activityGroups: ["turn"], details: ["hook-1:details"] }),
});
const element = renderMessageBlockElement(expectPresent(blocks.find((block) => block.key === "activity:turn-turn-activity")));
const element = renderThreadStreamBlockElement(expectPresent(blocks.find((block) => block.key === "activity:turn-turn-activity")));
expect(element).toBeDefined();
expect(element.querySelector(":scope > summary")?.textContent).toBe("Work details");
@ -291,11 +291,11 @@ describe("thread stream item renderer decisions", () => {
],
})[0];
const element = renderMessageBlockElement(block);
const element = renderThreadStreamBlockElement(block);
expect(element.classList.contains("codex-panel__status-message")).toBe(true);
expect(element.classList.contains("codex-panel__status-stream-item")).toBe(true);
expect(element.classList.contains("codex-panel__task-progress")).toBe(true);
expect(element.querySelector(".codex-panel__message-role")?.textContent).toBe("tasks");
expect(element.querySelector(".codex-panel__stream-item-role")?.textContent).toBe("tasks");
expect(element.textContent).toContain("[x]Inspect code");
expect(element.textContent).toContain("[>]Patch UI");
});
@ -487,11 +487,11 @@ describe("thread stream item renderer decisions", () => {
],
})[0];
const element = renderMessageBlockElement(block);
const element = renderThreadStreamBlockElement(block);
expect(element.classList.contains("codex-panel__detail")).toBe(true);
expect(element.classList.contains("codex-panel__agent-activity")).toBe(true);
expect(element.querySelector(".codex-panel__message-role")?.textContent).toBe("agent");
expect(element.querySelector(".codex-panel__stream-item-role")?.textContent).toBe("agent");
const summary = expectPresent(element.querySelector<HTMLElement>(".codex-panel__stream-summary"));
expect(summary.textContent).toBe("spawn child: Inspect the renderer. (completed)");
expect(textContents(element, "details summary")).toEqual(["agent"]);
@ -524,11 +524,11 @@ describe("thread stream item renderer decisions", () => {
],
})[0];
const element = renderMessageBlockElement(block);
const element = renderThreadStreamBlockElement(block);
const header = expectPresent(element.querySelector<HTMLElement>("details summary"));
const open = expectPresent(header.querySelector<HTMLButtonElement>('[aria-label="Open agent thread"]'));
expect(open.classList.contains("codex-panel__message-action")).toBe(true);
expect(open.classList.contains("codex-panel__stream-item-action")).toBe(true);
expect(open.classList.contains("codex-panel-ui__nav-row-action")).toBe(false);
expect(textContents(element, "details summary")).toEqual(["agent"]);
open.click();
@ -558,7 +558,7 @@ describe("thread stream item renderer decisions", () => {
],
})[0];
const element = renderMessageBlockElement(block);
const element = renderThreadStreamBlockElement(block);
expect(element.querySelector<HTMLButtonElement>('[aria-label="Open agent thread"]')).toBeNull();
});
@ -588,7 +588,7 @@ describe("thread stream item renderer decisions", () => {
],
})[0];
const element = renderMessageBlockElement(block);
const element = renderThreadStreamBlockElement(block);
expect(element.querySelector("details")?.hasAttribute("open")).toBe(true);
expectPresent(element.querySelector<HTMLButtonElement>("details summary [aria-label='Open agent thread']")).click();
@ -618,7 +618,7 @@ describe("thread stream item renderer decisions", () => {
],
})[0];
const element = renderMessageBlockElement(block);
const element = renderThreadStreamBlockElement(block);
const summary = expectPresent(element.querySelector<HTMLElement>(".codex-panel__stream-summary"));
expect(summary.textContent).toBe(`spawn child: Inspect the renderer. ${"a".repeat(73)}... (running)`);
@ -650,7 +650,7 @@ describe("thread stream item renderer decisions", () => {
onDisclosureToggle,
})[0];
const element = renderMessageBlockElement(block);
const element = renderThreadStreamBlockElement(block);
const agentRows = textContents(element, ".codex-panel__meta-grid dt, .codex-panel__meta-grid dd");
expect(agentRows).toContain("019e061e");
@ -693,9 +693,9 @@ describe("thread stream item renderer decisions", () => {
],
});
const summary = renderMessageBlockElement(expectPresent(blocks.at(-1)));
const summary = renderThreadStreamBlockElement(expectPresent(blocks.at(-1)));
expect(summary.classList.contains("codex-panel__status-message")).toBe(true);
expect(summary.classList.contains("codex-panel__status-stream-item")).toBe(true);
expect(summary.classList.contains("codex-panel__agent-summary")).toBe(true);
expect(summary.textContent).toContain("agents");
expect(summary.textContent).toContain("Agents 1 running, 1 done");
@ -731,7 +731,7 @@ describe("thread stream item renderer decisions", () => {
const running = expectPresent(
runningParent.querySelector<HTMLElement>('[data-codex-panel-block-key="item:compact-1"] .codex-panel__context-compaction'),
);
expect(running.querySelector(".codex-panel__message-role")?.textContent).toBe("context");
expect(running.querySelector(".codex-panel__stream-item-role")?.textContent).toBe("context");
expect(running.querySelector(".codex-panel__stream-summary")?.textContent).toBe("Compacting context...");
expect(running.classList.contains("codex-panel__execution--running")).toBe(true);
unmountUiRootInAct(runningParent);
@ -803,7 +803,7 @@ describe("thread stream item renderer decisions", () => {
],
});
const summary = renderMessageBlockElement(expectPresent(blocks.at(-1)));
const summary = renderThreadStreamBlockElement(expectPresent(blocks.at(-1)));
expect(summary.classList.contains("codex-panel__execution--failed")).toBe(true);
expect(summary.textContent).toContain("Agents 1 failed, 1 running");

View file

@ -149,7 +149,7 @@ export function dispatchComposingInputValue(input: HTMLInputElement, value: stri
input.dispatchEvent(event);
}
export function renderMessageBlockElement(block: ThreadStreamViewBlock): HTMLElement {
export function renderThreadStreamBlockElement(block: ThreadStreamViewBlock): HTMLElement {
const parent = document.createElement("div");
renderThreadStreamBlocksInAct(parent, [block]);
const host = expectPresent(parent.querySelector<HTMLElement>(`[data-codex-panel-block-key="${block.key}"]`));
@ -161,8 +161,8 @@ export function actEvent(action: () => void): void {
}
export function renderThreadStreamBlocksInAct(parent: HTMLElement, blocks: ThreadStreamViewBlock[]): void {
parent.addClass("codex-panel__messages");
installMessageViewportMetrics(parent);
parent.addClass("codex-panel__thread-stream");
installThreadStreamViewportMetrics(parent);
if (!parent.isConnected) document.body.appendChild(parent);
const context = threadStreamContextForBlocks(blocks);
void act(() => {
@ -189,7 +189,7 @@ function threadStreamContextForBlocks(blocks: readonly ThreadStreamViewBlock[]):
return context;
}
export function installMessageViewportMetrics(
export function installThreadStreamViewportMetrics(
element: HTMLElement,
metrics: { clientHeight?: number; clientWidth?: number; scrollHeight?: number } = {},
): void {
@ -283,12 +283,12 @@ function activeTurnIdForThreadStream(lifecycle: ThreadStreamTurnLifecycleState):
return lifecycle.kind === "running" ? lifecycle.turnId : null;
}
export function withMessageContentScrollHeight<T>(scrollHeight: number, fn: () => T): T {
export function withStreamItemContentScrollHeight<T>(scrollHeight: number, fn: () => T): T {
const descriptor = Object.getOwnPropertyDescriptor(HTMLElement.prototype, "scrollHeight");
Object.defineProperty(HTMLElement.prototype, "scrollHeight", {
configurable: true,
get() {
return this.classList.contains("codex-panel__message-content") ? scrollHeight : 0;
return this.classList.contains("codex-panel__stream-item-content") ? scrollHeight : 0;
},
});
try {

View file

@ -1092,7 +1092,7 @@ export const value = <button />;
font-weight: var(--font-normal);
}
.codex-panel__message-content.markdown-rendered pre {
.codex-panel__stream-item-content.markdown-rendered pre {
overflow-x: auto;
}

View file

@ -36,11 +36,11 @@ describe("panel CSS boundaries", () => {
});
describe("panel CSS layout invariants", () => {
it("lets the message stream scroll inside the shell grid", () => {
const messages = ruleBody(".codex-panel__messages");
it("lets the thread stream scroll inside the shell grid", () => {
const threadStream = ruleBody(".codex-panel__thread-stream");
expect(messages).toContain("overflow-y: auto");
expect(messages).not.toMatch(/^\s+height:/m);
expect(threadStream).toContain("overflow-y: auto");
expect(threadStream).not.toMatch(/^\s+height:/m);
});
it("keeps icon-only toolbar actions on Obsidian toolbar geometry", () => {