mirror of
https://github.com/murashit/codex-panel.git
synced 2026-07-22 06:57:10 +00:00
Align thread stream CSS classes
This commit is contained in:
parent
36ed7fceb3
commit
4202748cc6
32 changed files with 407 additions and 407 deletions
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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.");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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>> {
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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 }));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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} />
|
||||
))}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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(" ")}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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 () => {
|
||||
|
|
|
|||
|
|
@ -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 () => {
|
||||
|
|
|
|||
|
|
@ -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(() => {
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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", () => {
|
||||
|
|
|
|||
Loading…
Reference in a new issue