From 4202748cc60e7b385f6b0af4c28a030db284f6cd Mon Sep 17 00:00:00 2001 From: murashit Date: Thu, 9 Jul 2026 06:52:19 +0900 Subject: [PATCH] Align thread stream CSS classes --- src/features/chat/panel/shell.dom.tsx | 2 +- .../panel/surface/thread-stream-presenter.ts | 12 +- .../panel/surface/thread-stream-projection.ts | 4 +- .../presentation/thread-stream/detail-view.ts | 6 +- .../presentation/thread-stream/text-view.ts | 18 +- src/features/chat/ui/goal.tsx | 8 +- .../ui/thread-stream/agent-thread-action.tsx | 2 +- .../content-rendered-event.dom.ts | 6 +- src/features/chat/ui/thread-stream/detail.tsx | 2 +- .../ui/thread-stream/flow-scroll.measure.ts | 180 +++++++++--------- .../markdown-renderer.obsidian.ts | 10 +- .../thread-stream/pending-request-block.tsx | 6 +- src/features/chat/ui/thread-stream/status.tsx | 32 ++-- .../chat/ui/thread-stream/stream-blocks.tsx | 6 +- .../ui/thread-stream/text-content.dom.tsx | 16 +- src/features/chat/ui/thread-stream/text.tsx | 6 +- src/styles/00-tokens.css | 4 +- src/styles/20-chat-shell.css | 2 +- src/styles/22-chat-goal.css | 2 +- ...e-stream.css => 23-chat-thread-stream.css} | 80 ++++---- src/styles/24-chat-stream-items.css | 14 +- src/styles/order.json | 2 +- .../chat/host/view-connection.test.ts | 14 +- tests/features/chat/panel/shell.test.tsx | 12 +- .../surface/thread-stream-presenter.test.ts | 88 ++++----- .../blocks-and-messages.test.tsx | 152 +++++++-------- .../chat/ui/thread-stream/flow-scroll.test.ts | 32 ++-- .../thread-stream/pending-requests.test.tsx | 26 +-- .../ui/thread-stream/stream-items.test.tsx | 48 ++--- .../chat/ui/thread-stream/test-helpers.tsx | 12 +- tests/scripts/grit-policy.test.mjs | 2 +- tests/styles.test.ts | 8 +- 32 files changed, 407 insertions(+), 407 deletions(-) rename src/styles/{23-chat-message-stream.css => 23-chat-thread-stream.css} (80%) diff --git a/src/features/chat/panel/shell.dom.tsx b/src/features/chat/panel/shell.dom.tsx index 232c583a..9d61748a 100644 --- a/src/features/chat/panel/shell.dom.tsx +++ b/src/features/chat/panel/shell.dom.tsx @@ -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 { diff --git a/src/features/chat/panel/surface/thread-stream-presenter.ts b/src/features/chat/panel/surface/thread-stream-presenter.ts index 1f823faf..e8b3072e 100644 --- a/src/features/chat/panel/surface/thread-stream-presenter.ts +++ b/src/features/chat/panel/surface/thread-stream-presenter.ts @@ -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 { + private async copyDialogueText(text: string): Promise { await copyTextWithNotice(text, "Copied message.", "Could not copy message."); } } diff --git a/src/features/chat/panel/surface/thread-stream-projection.ts b/src/features/chat/panel/surface/thread-stream-projection.ts index 7b7cff2f..fd9dccbb 100644 --- a/src/features/chat/panel/surface/thread-stream-projection.ts +++ b/src/features/chat/panel/surface/thread-stream-projection.ts @@ -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); }, diff --git a/src/features/chat/presentation/thread-stream/detail-view.ts b/src/features/chat/presentation/thread-stream/detail-view.ts index fb2fc258..1f64898b 100644 --- a/src/features/chat/presentation/thread-stream/detail-view.ts +++ b/src/features/chat/presentation/thread-stream/detail-view.ts @@ -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", ); } diff --git a/src/features/chat/presentation/thread-stream/text-view.ts b/src/features/chat/presentation/thread-stream/text-view.ts index 0f699c8a..bd72fd4c 100644 --- a/src/features/chat/presentation/thread-stream/text-view.ts +++ b/src/features/chat/presentation/thread-stream/text-view.ts @@ -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: Key, value: Value | undefined): Partial> { diff --git a/src/features/chat/ui/goal.tsx b/src/features/chat/ui/goal.tsx index aad63af0..2d9491eb 100644 --- a/src/features/chat/ui/goal.tsx +++ b/src/features/chat/ui/goal.tsx @@ -118,7 +118,7 @@ export function GoalPanel({ ) : null} @@ -126,7 +126,7 @@ export function GoalPanel({ ) : null} @@ -134,7 +134,7 @@ export function GoalPanel({ ) : null} @@ -142,7 +142,7 @@ export function GoalPanel({ ) : null} diff --git a/src/features/chat/ui/thread-stream/agent-thread-action.tsx b/src/features/chat/ui/thread-stream/agent-thread-action.tsx index f8954a2c..54116e01 100644 --- a/src/features/chat/ui/thread-stream/agent-thread-action.tsx +++ b/src/features/chat/ui/thread-stream/agent-thread-action.tsx @@ -13,7 +13,7 @@ export function OpenAgentThreadAction({ { event.preventDefault(); event.stopPropagation(); diff --git a/src/features/chat/ui/thread-stream/content-rendered-event.dom.ts b/src/features/chat/ui/thread-stream/content-rendered-event.dom.ts index 09667c7b..1d14e6fc 100644 --- a/src/features/chat/ui/thread-stream/content-rendered-event.dom.ts +++ b/src/features/chat/ui/thread-stream/content-rendered-event.dom.ts @@ -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 })); } diff --git a/src/features/chat/ui/thread-stream/detail.tsx b/src/features/chat/ui/thread-stream/detail.tsx index 811a485c..0410e4bb 100644 --- a/src/features/chat/ui/thread-stream/detail.tsx +++ b/src/features/chat/ui/thread-stream/detail.tsx @@ -72,7 +72,7 @@ function DetailHeader({ openThreadInNewView?: ((threadId: string) => void) | undefined; }): UiNode { const content = ( - + {view.label} {openThreadInNewView && view.summaryThreadIds.length > 0 ? ( diff --git a/src/features/chat/ui/thread-stream/flow-scroll.measure.ts b/src/features/chat/ui/thread-stream/flow-scroll.measure.ts index 04eec2ed..5492e423 100644 --- a/src/features/chat/ui/thread-stream/flow-scroll.measure.ts +++ b/src/features/chat/ui/thread-stream/flow-scroll.measure.ts @@ -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 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 extends Component> { - 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 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>): MessageFlowSnapshot | null { - return captureMessageFlowSnapshot(this.runtime, this.scrollElement, previousProps.blocks, this.props.blocks); + override getSnapshotBeforeUpdate(previousProps: Readonly>): ThreadStreamFlowSnapshot | null { + return captureThreadStreamFlowSnapshot(this.runtime, this.scrollElement, previousProps.blocks, this.props.blocks); } override componentDidUpdate( previousProps: Readonly>, _previousState: Readonly>, - 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): UiNode { @@ -88,18 +88,18 @@ export class ThreadStreamFlowFrame { ...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 }; 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 } } -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 void>(); +const cleanupThreadStreamFlowContainer = new WeakMap 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(".codex-panel__message-block")); +function threadStreamFlowBlockElements(container: HTMLElement): HTMLElement[] { + return Array.from(container.querySelectorAll(".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; } diff --git a/src/features/chat/ui/thread-stream/markdown-renderer.obsidian.ts b/src/features/chat/ui/thread-stream/markdown-renderer.obsidian.ts index 2162b92d..d55b0e7c 100644 --- a/src/features/chat/ui/thread-stream/markdown-renderer.obsidian.ts +++ b/src/features/chat/ui/thread-stream/markdown-renderer.obsidian.ts @@ -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(); - 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); }); } } diff --git a/src/features/chat/ui/thread-stream/pending-request-block.tsx b/src/features/chat/ui/thread-stream/pending-request-block.tsx index cabe975b..9abae289 100644 --- a/src/features/chat/ui/thread-stream/pending-request-block.tsx +++ b/src/features/chat/ui/thread-stream/pending-request-block.tsx @@ -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 ( -
-
Request
+
+
Request
{approvals.map((approval) => ( ))} diff --git a/src/features/chat/ui/thread-stream/status.tsx b/src/features/chat/ui/thread-stream/status.tsx index e13863b2..e195465b 100644 --- a/src/features/chat/ui/thread-stream/status.tsx +++ b/src/features/chat/ui/thread-stream/status.tsx @@ -15,13 +15,13 @@ export function statusNode(view: ThreadStreamStatusView): UiNode { return ; } -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 ( - +
{view.summary}
-
+ ); } function TaskProgress({ view }: { view: Extract }): UiNode { return ( - + {view.summary ?
{view.summary}
: null} {view.checklist.length === 0 ? (
Plan updated
@@ -69,23 +69,23 @@ function TaskProgress({ view }: { view: Extract )} -
+ ); } function ContextCompaction({ view }: { view: Extract }): UiNode { return ( - +
{view.text}
-
+ ); } function GenericStatus({ view }: { view: Extract }): UiNode { return ( - +
{view.text}
-
+ ); } @@ -107,7 +107,7 @@ function Reasoning({ view }: { view: Extract -
{label}
+
{label}
{children}
); diff --git a/src/features/chat/ui/thread-stream/stream-blocks.tsx b/src/features/chat/ui/thread-stream/stream-blocks.tsx index fb1c5aec..2d650bec 100644 --- a/src/features/chat/ui/thread-stream/stream-blocks.tsx +++ b/src/features/chat/ui/thread-stream/stream-blocks.tsx @@ -92,7 +92,7 @@ function HistoryBar({ loadingHistory, loadOlderTurns }: { loadingHistory: boolea } function EmptyMessage(): UiNode { - return
Send a message to start a conversation.
; + return
Send a message to start a conversation.
; } function ActivityGroup({ @@ -124,9 +124,9 @@ function ActivityGroup({ function SteeringActivity({ activity }: { activity: Extract }): UiNode { return ( -
+
- {activity.label} + {activity.label}
{activity.text}
diff --git a/src/features/chat/ui/thread-stream/text-content.dom.tsx b/src/features/chat/ui/thread-stream/text-content.dom.tsx index e64f73a8..c761bc68 100644 --- a/src/features/chat/ui/thread-stream/text-content.dom.tsx +++ b/src/features/chat/ui/thread-stream/text-content.dom.tsx @@ -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