mirror of
https://github.com/murashit/codex-panel.git
synced 2026-07-22 06:57:10 +00:00
Modernize React UI DOM contracts
This commit is contained in:
parent
900b387313
commit
afddc633a0
5 changed files with 158 additions and 132 deletions
|
|
@ -110,12 +110,7 @@ function ComposerShell({
|
|||
/>
|
||||
<ComposerIconButton icon="send" label="Send" className="codex-panel__send" onClick={callbacks.onSendOrInterrupt} />
|
||||
</div>
|
||||
<div
|
||||
ref={suggestionsRef}
|
||||
className="suggestion-container codex-panel__composer-suggestions"
|
||||
id={`${viewId}-composer-suggestions`}
|
||||
role="listbox"
|
||||
/>
|
||||
<div ref={suggestionsRef} className="codex-panel__composer-suggestions" id={`${viewId}-composer-suggestions`} role="listbox" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -256,8 +251,8 @@ function ComposerSuggestions({
|
|||
callbacks.onSuggestionInsert(suggestion);
|
||||
}}
|
||||
>
|
||||
<span className="suggestion-title codex-panel__suggestion-label">{suggestion.display}</span>
|
||||
{suggestion.detail ? <span className="suggestion-note codex-panel__suggestion-detail">{suggestion.detail}</span> : null}
|
||||
<span className="codex-panel__suggestion-label">{suggestion.display}</span>
|
||||
{suggestion.detail ? <span className="codex-panel__suggestion-detail">{suggestion.detail}</span> : null}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { useLayoutEffect, type ReactNode } from "react";
|
||||
import { useLayoutEffect, useRef, type ReactNode } from "react";
|
||||
|
||||
import { displayBlocksForItems } from "../display/blocks";
|
||||
import { displayItemSignature, isMessageCopyActionVisible } from "../display/signature";
|
||||
|
|
@ -117,61 +117,40 @@ export function messageRenderBlocks(context: MessageStreamContext): MessageRende
|
|||
return blocks;
|
||||
}
|
||||
|
||||
export function syncMessageRenderBlocks(parent: HTMLElement, blocks: MessageRenderBlock[], signatures: Map<string, string>): void {
|
||||
const existing = new Map<string, HTMLElement>();
|
||||
parent.querySelectorAll<HTMLElement>(":scope > [data-codex-panel-block-key]").forEach((element) => {
|
||||
const key = element.dataset["codexPanelBlockKey"];
|
||||
if (key) existing.set(key, element);
|
||||
});
|
||||
|
||||
const seen = new Set<string>();
|
||||
let nextPosition: ChildNode | null = parent.firstChild;
|
||||
for (const block of blocks) {
|
||||
const current = existing.get(block.key);
|
||||
let element = current;
|
||||
const currentWasNext = current === nextPosition;
|
||||
if (!element || signatures.get(block.key) !== block.signature) {
|
||||
element = block.render();
|
||||
element.dataset["codexPanelBlockKey"] = block.key;
|
||||
element.dataset["codexPanelBlockSignature"] = shortSignature(block.signature);
|
||||
signatures.set(block.key, block.signature);
|
||||
if (current) {
|
||||
current.replaceWith(element);
|
||||
if (currentWasNext) nextPosition = element;
|
||||
}
|
||||
}
|
||||
if (element !== nextPosition) {
|
||||
parent.insertBefore(element, nextPosition);
|
||||
}
|
||||
nextPosition = element.nextSibling;
|
||||
seen.add(block.key);
|
||||
}
|
||||
|
||||
for (const [key, element] of existing) {
|
||||
if (!seen.has(key)) {
|
||||
signatures.delete(key);
|
||||
element.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function renderMessageRenderBlocks(parent: HTMLElement, blocks: MessageRenderBlock[], signatures: Map<string, string>): void {
|
||||
renderReactRoot(parent, <MessageRenderBlockCommit parent={parent} blocks={blocks} signatures={signatures} />);
|
||||
renderReactRoot(parent, <MessageRenderBlocks blocks={blocks} signatures={signatures} />);
|
||||
}
|
||||
|
||||
function MessageRenderBlockCommit({
|
||||
parent,
|
||||
blocks,
|
||||
signatures,
|
||||
}: {
|
||||
parent: HTMLElement;
|
||||
blocks: MessageRenderBlock[];
|
||||
signatures: Map<string, string>;
|
||||
}): ReactNode {
|
||||
function MessageRenderBlocks({ blocks, signatures }: { blocks: MessageRenderBlock[]; signatures: Map<string, string> }): ReactNode {
|
||||
return (
|
||||
<>
|
||||
{blocks.map((block) => (
|
||||
<MessageRenderBlockHost key={block.key} block={block} signatures={signatures} />
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function MessageRenderBlockHost({ block, signatures }: { block: MessageRenderBlock; signatures: Map<string, string> }): ReactNode {
|
||||
const ref = useRef<HTMLDivElement | null>(null);
|
||||
useLayoutEffect(() => {
|
||||
syncMessageRenderBlocks(parent, blocks, signatures);
|
||||
}, [parent, blocks, signatures]);
|
||||
return null;
|
||||
const host = ref.current;
|
||||
if (!host) return;
|
||||
if (signatures.get(block.key) !== block.signature) {
|
||||
host.replaceChildren(block.render());
|
||||
signatures.set(block.key, block.signature);
|
||||
}
|
||||
host.dataset["codexPanelBlockSignature"] = shortSignature(block.signature);
|
||||
}, [block, signatures]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const key = block.key;
|
||||
return () => {
|
||||
signatures.delete(key);
|
||||
};
|
||||
}, [block.key, signatures]);
|
||||
|
||||
return <div ref={ref} className="codex-panel__message-block" data-codex-panel-block-key={block.key} />;
|
||||
}
|
||||
|
||||
function createHistoryBarElement(loadingHistory: boolean, loadOlderTurns: () => void): HTMLElement {
|
||||
|
|
|
|||
|
|
@ -84,12 +84,12 @@ function ApprovalCard({
|
|||
}): ReactNode {
|
||||
return (
|
||||
<PendingRequestCard className="codex-panel__approval">
|
||||
<div className="setting-item-info codex-panel__pending-request-info">
|
||||
<div className="setting-item-name codex-panel__pending-request-title">{approvalTitle(approval)}</div>
|
||||
<div className="setting-item-description codex-panel__pending-request-body">{approvalSummary(approval)}</div>
|
||||
<div className="codex-panel__pending-request-info">
|
||||
<div className="codex-panel__pending-request-title">{approvalTitle(approval)}</div>
|
||||
<div className="codex-panel__pending-request-body">{approvalSummary(approval)}</div>
|
||||
<ApprovalDetails approval={approval} openDetails={openDetails} />
|
||||
</div>
|
||||
<div className="setting-item-control codex-panel__pending-request-actions">
|
||||
<div className="codex-panel__pending-request-actions">
|
||||
{approvalActionOptions(approval).map((option) => (
|
||||
<ActionButton
|
||||
key={option.label}
|
||||
|
|
@ -140,14 +140,14 @@ function UserInputCard({
|
|||
}): ReactNode {
|
||||
return (
|
||||
<PendingRequestCard className="codex-panel__user-input">
|
||||
<div className="setting-item-info codex-panel__pending-request-info">
|
||||
<div className="setting-item-name codex-panel__pending-request-title">Codex needs input</div>
|
||||
<div className="setting-item-description codex-panel__pending-request-body">
|
||||
<div className="codex-panel__pending-request-info">
|
||||
<div className="codex-panel__pending-request-title">Codex needs input</div>
|
||||
<div className="codex-panel__pending-request-body">
|
||||
Answer {String(input.params.questions.length)} Plan mode question{input.params.questions.length === 1 ? "" : "s"} to continue.
|
||||
</div>
|
||||
<UserInputQuestions input={input} drafts={drafts} />
|
||||
</div>
|
||||
<div className="setting-item-control codex-panel__pending-request-actions">
|
||||
<div className="codex-panel__pending-request-actions">
|
||||
<ActionButton
|
||||
label="Submit"
|
||||
className="mod-cta"
|
||||
|
|
@ -168,7 +168,7 @@ function UserInputCard({
|
|||
}
|
||||
|
||||
function PendingRequestCard({ className, children }: { className: string; children: ReactNode }): ReactNode {
|
||||
return <div className={`setting-item codex-panel__pending-request-card ${className}`}>{children}</div>;
|
||||
return <div className={`codex-panel__pending-request-card ${className}`}>{children}</div>;
|
||||
}
|
||||
|
||||
function UserInputQuestions({ input, drafts }: { input: PendingUserInput; drafts: PendingRequestMessageDrafts }): ReactNode {
|
||||
|
|
|
|||
123
styles.css
123
styles.css
|
|
@ -120,6 +120,12 @@
|
|||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.codex-panel-threads__toolbar-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--codex-panel-toolbar-button-gap);
|
||||
}
|
||||
|
||||
.codex-panel-threads__list {
|
||||
display: flex;
|
||||
flex: 1 1 auto;
|
||||
|
|
@ -238,36 +244,6 @@
|
|||
opacity: 1;
|
||||
}
|
||||
|
||||
.codex-panel-threads__icon-button:hover,
|
||||
.codex-panel-threads__icon-button:focus,
|
||||
.codex-panel-threads__icon-button:focus-visible,
|
||||
.codex-panel-threads__icon-button:active {
|
||||
background: transparent;
|
||||
box-shadow: none;
|
||||
color: var(--icon-color-active);
|
||||
}
|
||||
|
||||
.codex-panel-threads__icon-button {
|
||||
flex: 0 0 auto;
|
||||
padding: var(--codex-panel-control-gap);
|
||||
}
|
||||
|
||||
.codex-panel-threads__toolbar-button {
|
||||
--icon-size: var(--codex-panel-control-icon-size);
|
||||
--icon-stroke: var(--icon-m-stroke-width, 1.75px);
|
||||
}
|
||||
|
||||
.codex-panel-threads__row-button {
|
||||
--icon-size: var(--codex-panel-size-icon-s);
|
||||
--icon-stroke: var(--icon-s-stroke-width, 1.75px);
|
||||
}
|
||||
|
||||
.codex-panel-threads__icon-button svg {
|
||||
width: var(--icon-size);
|
||||
height: var(--icon-size);
|
||||
stroke-width: var(--icon-stroke);
|
||||
}
|
||||
|
||||
.codex-panel-threads__rename-form {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
|
@ -344,20 +320,43 @@
|
|||
color: var(--codex-panel-text-normal);
|
||||
}
|
||||
|
||||
.codex-panel-ui__toolbar-control {
|
||||
.codex-panel-ui__toolbar-control,
|
||||
.codex-panel-ui__icon-button,
|
||||
.codex-panel-threads__icon-button {
|
||||
box-sizing: border-box;
|
||||
display: inline-flex;
|
||||
flex: 0 0 auto;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
appearance: none;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
box-shadow: none;
|
||||
color: var(--icon-color);
|
||||
font: inherit;
|
||||
line-height: var(--line-height-tight);
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.codex-panel-ui__toolbar-control:disabled,
|
||||
.codex-panel-ui__icon-button:disabled,
|
||||
.codex-panel-threads__icon-button:disabled {
|
||||
cursor: default;
|
||||
opacity: 0.45;
|
||||
}
|
||||
|
||||
.codex-panel-ui__toolbar-control {
|
||||
--icon-size: var(--codex-panel-control-icon-size);
|
||||
--icon-stroke: var(--icon-m-stroke-width, 1.75px);
|
||||
min-width: var(--codex-panel-icon-button-inline-size);
|
||||
width: auto;
|
||||
height: auto;
|
||||
padding: var(--codex-panel-control-gap) var(--codex-panel-item-gap);
|
||||
border: 0;
|
||||
border-radius: var(--codex-panel-control-radius);
|
||||
}
|
||||
|
||||
.codex-panel-ui__icon-button {
|
||||
box-sizing: border-box;
|
||||
--icon-size: var(--codex-panel-control-icon-size);
|
||||
--icon-stroke: var(--icon-m-stroke-width, 1.75px);
|
||||
min-width: var(--codex-panel-composer-control-size);
|
||||
|
|
@ -367,17 +366,39 @@
|
|||
border-radius: var(--codex-panel-control-radius);
|
||||
}
|
||||
|
||||
.codex-panel-threads__icon-button {
|
||||
--icon-size: var(--codex-panel-control-icon-size);
|
||||
--icon-stroke: var(--icon-m-stroke-width, 1.75px);
|
||||
padding: var(--codex-panel-control-gap);
|
||||
}
|
||||
|
||||
.codex-panel-threads__toolbar-button {
|
||||
--icon-size: var(--codex-panel-control-icon-size);
|
||||
--icon-stroke: var(--icon-m-stroke-width, 1.75px);
|
||||
}
|
||||
|
||||
.codex-panel-threads__row-button {
|
||||
--icon-size: var(--codex-panel-size-icon-s);
|
||||
--icon-stroke: var(--icon-s-stroke-width, 1.75px);
|
||||
}
|
||||
|
||||
.codex-panel-threads__icon-button:hover,
|
||||
.codex-panel-threads__icon-button:focus,
|
||||
.codex-panel-threads__icon-button:focus-visible,
|
||||
.codex-panel-threads__icon-button:active {
|
||||
background: transparent;
|
||||
box-shadow: none;
|
||||
color: var(--icon-color-active);
|
||||
}
|
||||
|
||||
.codex-panel-ui__toolbar-control svg,
|
||||
.codex-panel-ui__icon-button svg {
|
||||
.codex-panel-ui__icon-button svg,
|
||||
.codex-panel-threads__icon-button svg {
|
||||
width: var(--icon-size);
|
||||
height: var(--icon-size);
|
||||
stroke-width: var(--icon-stroke);
|
||||
}
|
||||
|
||||
.codex-panel-ui__icon-button:disabled {
|
||||
opacity: 0.45;
|
||||
}
|
||||
|
||||
.codex-panel-ui__text-input {
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
|
|
@ -457,7 +478,7 @@
|
|||
background: var(--background-modifier-active-hover);
|
||||
}
|
||||
|
||||
.codex-panel .clickable-icon.codex-panel__runtime-model {
|
||||
.codex-panel__runtime-model {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
flex: 0 1 auto;
|
||||
|
|
@ -478,16 +499,16 @@
|
|||
user-select: none;
|
||||
}
|
||||
|
||||
.codex-panel .clickable-icon.codex-panel__runtime-model:hover,
|
||||
.codex-panel .clickable-icon.codex-panel__runtime-model.is-active {
|
||||
.codex-panel__runtime-model:hover,
|
||||
.codex-panel__runtime-model.is-active {
|
||||
color: var(--text-normal);
|
||||
}
|
||||
|
||||
.codex-panel .clickable-icon.codex-panel__runtime-model.is-active {
|
||||
.codex-panel__runtime-model.is-active {
|
||||
background: var(--background-modifier-hover);
|
||||
}
|
||||
|
||||
.codex-panel .clickable-icon.codex-panel__runtime-model:hover {
|
||||
.codex-panel__runtime-model:hover {
|
||||
background: var(--background-modifier-hover);
|
||||
}
|
||||
|
||||
|
|
@ -1298,6 +1319,10 @@
|
|||
margin-bottom: var(--codex-panel-section-gap);
|
||||
}
|
||||
|
||||
.codex-panel__message-block {
|
||||
display: flow-root;
|
||||
}
|
||||
|
||||
.codex-panel__message {
|
||||
margin-bottom: var(--codex-panel-edge-padding-x);
|
||||
padding-left: var(--codex-panel-message-indent);
|
||||
|
|
@ -1637,7 +1662,8 @@
|
|||
font-family: var(--font-interface);
|
||||
}
|
||||
|
||||
.codex-panel__pending-request-card.setting-item {
|
||||
.codex-panel__pending-request-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
gap: var(--codex-panel-control-gap);
|
||||
|
|
@ -1647,12 +1673,12 @@
|
|||
background: transparent;
|
||||
}
|
||||
|
||||
.codex-panel__pending-request-card.setting-item + .codex-panel__pending-request-card.setting-item {
|
||||
.codex-panel__pending-request-card + .codex-panel__pending-request-card {
|
||||
padding-top: var(--codex-panel-item-gap);
|
||||
border-top: 1px solid var(--codex-panel-border-muted-color);
|
||||
}
|
||||
|
||||
.codex-panel__pending-request-info.setting-item-info {
|
||||
.codex-panel__pending-request-info {
|
||||
min-width: 0;
|
||||
margin-right: 0;
|
||||
}
|
||||
|
|
@ -1677,7 +1703,8 @@
|
|||
font-size: var(--font-ui-smaller);
|
||||
}
|
||||
|
||||
.codex-panel__pending-request-actions.setting-item-control {
|
||||
.codex-panel__pending-request-actions {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--codex-panel-control-gap);
|
||||
|
|
@ -2163,7 +2190,7 @@
|
|||
color: var(--icon-color-active);
|
||||
}
|
||||
|
||||
.codex-panel__composer-suggestions.suggestion-container {
|
||||
.codex-panel__composer-suggestions {
|
||||
position: absolute;
|
||||
right: var(--codex-panel-edge-padding-x);
|
||||
bottom: calc(100% - var(--codex-panel-control-gap));
|
||||
|
|
|
|||
|
|
@ -15,12 +15,13 @@ import { renderPendingRequestMessage } from "../../../../src/features/chat/ui/pe
|
|||
import { renderToolbar, toolbarSignature, type ToolbarViewModel } from "../../../../src/features/chat/ui/toolbar";
|
||||
import { displayItemSignature } from "../../../../src/features/chat/display/signature";
|
||||
import { implementPlanCandidateFromState } from "../../../../src/features/chat/chat-message-renderer";
|
||||
import { messageRenderBlocks as rawMessageRenderBlocks, syncMessageRenderBlocks } from "../../../../src/features/chat/ui/message-stream";
|
||||
import { messageRenderBlocks as rawMessageRenderBlocks, renderMessageRenderBlocks } from "../../../../src/features/chat/ui/message-stream";
|
||||
import { displayDiffLines, persistedChatTurnDiffViewState, renderChatTurnDiffView } from "../../../../src/features/chat/ui/turn-diff";
|
||||
import { composerSuggestionScrollFixture, installObsidianDomShims, topLevelDetailsSummaries } from "./dom-test-helpers";
|
||||
import { renderThreadsView } from "../../../../src/features/threads-view/renderer";
|
||||
import { liveStateForSnapshots, threadRows, type ThreadsRowModel } from "../../../../src/features/threads-view/state";
|
||||
import type { Thread } from "../../../../src/generated/app-server/v2/Thread";
|
||||
import { unmountReactRoot } from "../../../../src/shared/ui/react-root";
|
||||
|
||||
installObsidianDomShims();
|
||||
|
||||
|
|
@ -153,31 +154,47 @@ describe("message stream block identity and message actions", () => {
|
|||
);
|
||||
});
|
||||
|
||||
it("reuses message block nodes while signatures are stable", () => {
|
||||
it("reuses React message block hosts while signatures are stable", () => {
|
||||
const parent = document.createElement("div");
|
||||
const signatures = new Map<string, string>();
|
||||
const first = document.createElement("section");
|
||||
first.textContent = "first";
|
||||
const firstRender = vi.fn(() => first);
|
||||
|
||||
syncMessageRenderBlocks(parent, [{ key: "one", signature: "same", render: () => first }], signatures);
|
||||
syncMessageRenderBlocks(parent, [{ key: "one", signature: "same", render: () => document.createElement("aside") }], signatures);
|
||||
renderMessageRenderBlocks(parent, [{ key: "one", signature: "same", render: firstRender }], signatures);
|
||||
|
||||
expect(parent.firstElementChild).toBe(first);
|
||||
const host = expectPresent(parent.querySelector<HTMLElement>('[data-codex-panel-block-key="one"]'));
|
||||
expect(host.classList.contains("codex-panel__message-block")).toBe(true);
|
||||
expect(host.firstElementChild).toBe(first);
|
||||
expect(firstRender).toHaveBeenCalledOnce();
|
||||
|
||||
const skippedRender = vi.fn(() => document.createElement("aside"));
|
||||
renderMessageRenderBlocks(parent, [{ key: "one", signature: "same", render: skippedRender }], signatures);
|
||||
|
||||
expect(parent.querySelector('[data-codex-panel-block-key="one"]')).toBe(host);
|
||||
expect(host.firstElementChild).toBe(first);
|
||||
expect(skippedRender).not.toHaveBeenCalled();
|
||||
|
||||
const replacement = document.createElement("article");
|
||||
syncMessageRenderBlocks(parent, [{ key: "one", signature: "changed", render: () => replacement }], signatures);
|
||||
renderMessageRenderBlocks(parent, [{ key: "one", signature: "changed", render: () => replacement }], signatures);
|
||||
|
||||
expect(parent.firstElementChild).toBe(replacement);
|
||||
expect(parent.querySelector('[data-codex-panel-block-key="one"]')).toBe(host);
|
||||
expect(host.firstElementChild).toBe(replacement);
|
||||
expect(signatures.get("one")).toBe("changed");
|
||||
|
||||
renderMessageRenderBlocks(parent, [], signatures);
|
||||
expect(parent.querySelector('[data-codex-panel-block-key="one"]')).toBeNull();
|
||||
expect(signatures.has("one")).toBe(false);
|
||||
unmountReactRoot(parent);
|
||||
});
|
||||
|
||||
it("leaves stable ordered message block nodes in place during repeated syncs", () => {
|
||||
it("leaves stable ordered React message block hosts in place during repeated renders", () => {
|
||||
const parent = document.createElement("div");
|
||||
const signatures = new Map<string, string>();
|
||||
const first = document.createElement("section");
|
||||
const second = document.createElement("article");
|
||||
|
||||
syncMessageRenderBlocks(
|
||||
renderMessageRenderBlocks(
|
||||
parent,
|
||||
[
|
||||
{ key: "one", signature: "same-one", render: () => first },
|
||||
|
|
@ -185,19 +202,26 @@ describe("message stream block identity and message actions", () => {
|
|||
],
|
||||
signatures,
|
||||
);
|
||||
const firstHost = expectPresent(parent.querySelector<HTMLElement>('[data-codex-panel-block-key="one"]'));
|
||||
const secondHost = expectPresent(parent.querySelector<HTMLElement>('[data-codex-panel-block-key="two"]'));
|
||||
|
||||
const insertBefore = vi.spyOn(parent, "insertBefore");
|
||||
syncMessageRenderBlocks(
|
||||
const skippedRender = vi.fn(() => document.createElement("aside"));
|
||||
renderMessageRenderBlocks(
|
||||
parent,
|
||||
[
|
||||
{ key: "one", signature: "same-one", render: () => document.createElement("aside") },
|
||||
{ key: "two", signature: "same-two", render: () => document.createElement("aside") },
|
||||
{ key: "one", signature: "same-one", render: skippedRender },
|
||||
{ key: "two", signature: "same-two", render: skippedRender },
|
||||
],
|
||||
signatures,
|
||||
);
|
||||
|
||||
expect(insertBefore).not.toHaveBeenCalled();
|
||||
expect([...parent.children]).toEqual([first, second]);
|
||||
expect(skippedRender).not.toHaveBeenCalled();
|
||||
expect([...parent.children]).toEqual([firstHost, secondHost]);
|
||||
expect(firstHost.firstElementChild).toBe(first);
|
||||
expect(secondHost.firstElementChild).toBe(second);
|
||||
unmountReactRoot(parent);
|
||||
});
|
||||
|
||||
it("inserts completed-turn activity groups without replacing stable conversation nodes", () => {
|
||||
|
|
@ -215,7 +239,7 @@ describe("message stream block identity and message actions", () => {
|
|||
renderTextWithWikiLinks: (element: HTMLElement, text: string) => element.createDiv({ text }),
|
||||
};
|
||||
|
||||
syncMessageRenderBlocks(
|
||||
renderMessageRenderBlocks(
|
||||
parent,
|
||||
messageRenderBlocks({
|
||||
...baseContext,
|
||||
|
|
@ -229,7 +253,7 @@ describe("message stream block identity and message actions", () => {
|
|||
const userNode = parent.querySelector<HTMLElement>('[data-codex-panel-block-key="item:u1"]');
|
||||
const assistantNode = parent.querySelector<HTMLElement>('[data-codex-panel-block-key="item:a1"]');
|
||||
|
||||
syncMessageRenderBlocks(
|
||||
renderMessageRenderBlocks(
|
||||
parent,
|
||||
messageRenderBlocks({
|
||||
...baseContext,
|
||||
|
|
@ -260,6 +284,7 @@ describe("message stream block identity and message actions", () => {
|
|||
expect(parent.querySelector('[data-codex-panel-block-key="activity:turn-t1-activity"] summary')?.textContent).toBe(
|
||||
"Work details: hook",
|
||||
);
|
||||
unmountReactRoot(parent);
|
||||
});
|
||||
|
||||
it("does not invalidate generic tool blocks when only the workspace root changes", () => {
|
||||
|
|
|
|||
Loading…
Reference in a new issue