mirror of
https://github.com/murashit/codex-panel.git
synced 2026-07-22 06:57:10 +00:00
Update Preact compatibility docs
This commit is contained in:
parent
86cfda539c
commit
e32bff8a9b
11 changed files with 33 additions and 43 deletions
15
.github/dependabot.yml
vendored
15
.github/dependabot.yml
vendored
|
|
@ -24,18 +24,3 @@ updates:
|
|||
update-types:
|
||||
- "version-update:semver-minor"
|
||||
- "version-update:semver-major"
|
||||
# Keep React on 18.x for Obsidian Community plugin review compatibility.
|
||||
# React DOM 19 bundles runtime code that Obsidian's automated review flags
|
||||
# as script injection; see docs/development.md.
|
||||
- dependency-name: "react"
|
||||
update-types:
|
||||
- "version-update:semver-major"
|
||||
- dependency-name: "react-dom"
|
||||
update-types:
|
||||
- "version-update:semver-major"
|
||||
- dependency-name: "@types/react"
|
||||
update-types:
|
||||
- "version-update:semver-major"
|
||||
- dependency-name: "@types/react-dom"
|
||||
update-types:
|
||||
- "version-update:semver-major"
|
||||
|
|
|
|||
|
|
@ -30,9 +30,9 @@ The source tree is organized by responsibility rather than by the original singl
|
|||
|
||||
Keep new code near the state or API it owns. Feature code should not import from another feature directly; move shared behavior to `src/shared/`, `src/domain/`, `src/app-server/`, or `src/runtime/` when more than one feature needs it.
|
||||
|
||||
Codex Panel's runtime UI is React-owned. Keep chat panel UI, the Threads view, and the selection rewrite popover in React components. Imperative DOM writes are limited to explicit bridge modules or Obsidian-owned API boundaries such as `MarkdownRenderer`, diff rendering, icon rendering, `SuggestModal`, and the Obsidian `Setting`-based settings tab. ESLint enforces this boundary with allowlisted bridge files; add a new allowlist entry only when the code is genuinely bridging an external DOM API.
|
||||
Codex Panel's runtime UI is Preact-owned through `preact/compat`. Keep chat panel UI, the Threads view, and the selection rewrite popover in Preact components, using the React-compatible adapter layer until a targeted compat-removal pass is worthwhile. Imperative DOM writes are limited to explicit bridge modules or Obsidian-owned API boundaries such as `MarkdownRenderer`, diff rendering, icon rendering, `SuggestModal`, and the Obsidian `Setting`-based settings tab. ESLint enforces this boundary with allowlisted bridge files; add a new allowlist entry only when the code is genuinely bridging an external DOM API.
|
||||
|
||||
React is pinned to 18.x for Obsidian Community plugin review compatibility. React DOM 19 currently bundles runtime code that creates `<script>` elements, which is flagged by Obsidian's automated review even though Codex Panel does not dynamically load external scripts. Revisit this pin only after Obsidian's review tooling can distinguish that React runtime code from plugin-controlled script injection.
|
||||
The bundled UI runtime should stay on Preact rather than React/React DOM. This avoids React DOM runtime code that creates `<script>` elements and can be flagged by Obsidian Community plugin review automation, while preserving the current component model through `preact/compat`. Do not reintroduce `react`, `react-dom`, `@types/react`, or `@types/react-dom` unless the review tradeoff is explicitly revisited.
|
||||
|
||||
## App-Server Bindings
|
||||
|
||||
|
|
|
|||
|
|
@ -25,10 +25,10 @@ const imperativeDomRestrictions = [
|
|||
message: "Keep imperative DOM event wiring in an explicit bridge module or Obsidian-owned UI boundary.",
|
||||
},
|
||||
];
|
||||
const reactFormRestrictions = [
|
||||
const preactFormRestrictions = [
|
||||
{
|
||||
selector: "JSXAttribute[name.name=/^(defaultValue|defaultChecked)$/]",
|
||||
message: "Keep React form state explicit with controlled value or checked props.",
|
||||
message: "Keep Preact form state explicit with controlled value or checked props.",
|
||||
},
|
||||
];
|
||||
const removedChatStateEscapeHatchRestrictions = [
|
||||
|
|
@ -194,7 +194,12 @@ export default defineConfig([
|
|||
files: ["src/**/*.{ts,tsx}"],
|
||||
ignores: ["src/features/chat/**/*.{ts,tsx}", ...nonChatImperativeDomBridgeFiles],
|
||||
rules: {
|
||||
"no-restricted-syntax": ["error", ...removedChatStateEscapeHatchRestrictions, ...imperativeDomRestrictions, ...reactFormRestrictions],
|
||||
"no-restricted-syntax": [
|
||||
"error",
|
||||
...removedChatStateEscapeHatchRestrictions,
|
||||
...imperativeDomRestrictions,
|
||||
...preactFormRestrictions,
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
@ -205,7 +210,7 @@ export default defineConfig([
|
|||
"error",
|
||||
...removedChatStateEscapeHatchRestrictions,
|
||||
...imperativeDomRestrictions,
|
||||
...reactFormRestrictions,
|
||||
...preactFormRestrictions,
|
||||
...chatStateRestrictions,
|
||||
],
|
||||
},
|
||||
|
|
@ -213,13 +218,13 @@ export default defineConfig([
|
|||
{
|
||||
files: chatImperativeDomBridgeFiles,
|
||||
rules: {
|
||||
"no-restricted-syntax": ["error", ...removedChatStateEscapeHatchRestrictions, ...reactFormRestrictions, ...chatStateRestrictions],
|
||||
"no-restricted-syntax": ["error", ...removedChatStateEscapeHatchRestrictions, ...preactFormRestrictions, ...chatStateRestrictions],
|
||||
},
|
||||
},
|
||||
{
|
||||
files: nonChatImperativeDomBridgeFiles,
|
||||
rules: {
|
||||
"no-restricted-syntax": ["error", ...removedChatStateEscapeHatchRestrictions, ...reactFormRestrictions],
|
||||
"no-restricted-syntax": ["error", ...removedChatStateEscapeHatchRestrictions, ...preactFormRestrictions],
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
@ -229,7 +234,7 @@ export default defineConfig([
|
|||
"error",
|
||||
...removedChatStateEscapeHatchRestrictions,
|
||||
...imperativeDomRestrictions,
|
||||
...reactFormRestrictions,
|
||||
...preactFormRestrictions,
|
||||
...chatStateRestrictions,
|
||||
...pureChatModelRestrictions,
|
||||
],
|
||||
|
|
|
|||
|
|
@ -185,7 +185,7 @@ describe("ChatMessageRenderer scroll pinning", () => {
|
|||
expect(state.messagesPinnedToBottom).toBe(false);
|
||||
});
|
||||
|
||||
it("unmounts the React message stream root on dispose", () => {
|
||||
it("unmounts the Preact message stream root on dispose", () => {
|
||||
const state = createChatState();
|
||||
state.activeThreadId = "thread";
|
||||
state.displayItems = [{ id: "message", kind: "message", role: "assistant", text: "Rendered message", turnId: "turn" }];
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ import {
|
|||
} from "./test-helpers";
|
||||
|
||||
describe("message stream block identity and message actions", () => {
|
||||
it("reuses keyed React message block hosts across rerenders", () => {
|
||||
it("reuses keyed Preact message block hosts across rerenders", () => {
|
||||
const parent = document.createElement("div");
|
||||
renderMessageStreamBlocksInAct(parent, [testMessageStreamBlock("one", createElement("section", null, "first"))]);
|
||||
|
||||
|
|
@ -47,7 +47,7 @@ describe("message stream block identity and message actions", () => {
|
|||
unmountReactRootInAct(parent);
|
||||
});
|
||||
|
||||
it("leaves stable ordered React message block hosts in place during repeated renders", () => {
|
||||
it("leaves stable ordered Preact message block hosts in place during repeated renders", () => {
|
||||
const parent = document.createElement("div");
|
||||
renderMessageStreamBlocksInAct(parent, [
|
||||
testMessageStreamBlock("one", createElement("section", null, "one")),
|
||||
|
|
@ -128,7 +128,7 @@ describe("message stream block identity and message actions", () => {
|
|||
unmountReactRootInAct(parent);
|
||||
});
|
||||
|
||||
it("renders the history bar as a React block", () => {
|
||||
it("renders the history bar as a Preact block", () => {
|
||||
const loadOlderTurns = vi.fn();
|
||||
const [historyBlock] = messageStreamBlocks({
|
||||
activeThreadId: "thread",
|
||||
|
|
@ -158,7 +158,7 @@ describe("message stream block identity and message actions", () => {
|
|||
unmountReactRootInAct(parent);
|
||||
});
|
||||
|
||||
it("renders the empty message stream state as a React block", () => {
|
||||
it("renders the empty message stream state as a Preact block", () => {
|
||||
const [emptyBlock] = messageStreamBlocks({
|
||||
activeThreadId: null,
|
||||
turnLifecycle: idleTurnLifecycle(),
|
||||
|
|
@ -252,7 +252,7 @@ describe("message stream block identity and message actions", () => {
|
|||
expect([...element.querySelectorAll(".codex-panel__output-title")].map((title) => title.textContent)).toEqual([]);
|
||||
});
|
||||
|
||||
it("keeps tool result React details mounted in the message stream host", () => {
|
||||
it("keeps tool result Preact details mounted in the message stream host", () => {
|
||||
const parent = document.createElement("div");
|
||||
const onDetailsToggle = vi.fn();
|
||||
const renderTextWithWikiLinks = vi.fn((element: HTMLElement, text: string) => {
|
||||
|
|
@ -307,7 +307,7 @@ describe("message stream block identity and message actions", () => {
|
|||
unmountReactRootInAct(parent);
|
||||
});
|
||||
|
||||
it("renders file change diffs through the React tool result adapter", () => {
|
||||
it("renders file change diffs through the Preact tool result adapter", () => {
|
||||
const parent = document.createElement("div");
|
||||
|
||||
renderMessageStreamBlocksInAct(
|
||||
|
|
@ -537,7 +537,7 @@ describe("message stream block identity and message actions", () => {
|
|||
expect(onForkItem).toHaveBeenCalledWith(expect.objectContaining({ id: "a1" }), true);
|
||||
});
|
||||
|
||||
it("keeps message React actions mounted in the message stream host", () => {
|
||||
it("keeps message Preact actions mounted in the message stream host", () => {
|
||||
const parent = document.createElement("div");
|
||||
const copyText = vi.fn();
|
||||
const onImplementPlanItem = vi.fn();
|
||||
|
|
@ -580,7 +580,7 @@ describe("message stream block identity and message actions", () => {
|
|||
unmountReactRootInAct(parent);
|
||||
});
|
||||
|
||||
it("renders message markdown through the React content adapter", () => {
|
||||
it("renders message markdown through the Preact content adapter", () => {
|
||||
const parent = document.createElement("div");
|
||||
const renderMarkdown = vi.fn((element: HTMLElement, text: string) => {
|
||||
element.createDiv({ text: `rendered:${text}` });
|
||||
|
|
|
|||
|
|
@ -473,7 +473,7 @@ describe("pending request renderer decisions", () => {
|
|||
expect(expectPresent(blocks[1]).node).not.toBeUndefined();
|
||||
});
|
||||
|
||||
it("keeps pending request React events mounted in the message stream host", () => {
|
||||
it("keeps pending request Preact events mounted in the message stream host", () => {
|
||||
const parent = document.createElement("div");
|
||||
const approval = pendingApproval();
|
||||
const resolveApproval = vi.fn();
|
||||
|
|
|
|||
|
|
@ -330,7 +330,7 @@ describe("work log renderer decisions", () => {
|
|||
expect(element.querySelector(".codex-panel__output pre")?.textContent).toBe("feedback: ok");
|
||||
});
|
||||
|
||||
it("keeps completed-turn activity group items mounted through React", () => {
|
||||
it("keeps completed-turn activity group items mounted through Preact", () => {
|
||||
const parent = document.createElement("div");
|
||||
const onDetailsToggle = vi.fn();
|
||||
|
||||
|
|
@ -440,7 +440,7 @@ describe("work log renderer decisions", () => {
|
|||
expect(element.textContent).toContain("[>]Patch UI");
|
||||
});
|
||||
|
||||
it("keeps task progress React items mounted in the message stream host", () => {
|
||||
it("keeps task progress Preact items mounted in the message stream host", () => {
|
||||
const parent = document.createElement("div");
|
||||
|
||||
renderMessageStreamBlocksInAct(
|
||||
|
|
@ -682,7 +682,7 @@ describe("work log renderer decisions", () => {
|
|||
expect(element.textContent).toContain("childcompleted: Done");
|
||||
});
|
||||
|
||||
it("keeps agent React details mounted in the message stream host", () => {
|
||||
it("keeps agent Preact details mounted in the message stream host", () => {
|
||||
const parent = document.createElement("div");
|
||||
const onDetailsToggle = vi.fn();
|
||||
|
||||
|
|
@ -858,7 +858,7 @@ describe("work log renderer decisions", () => {
|
|||
expect(summary.textContent).not.toContain("donecompleted");
|
||||
});
|
||||
|
||||
it("renders the compact live agent summary as a React block", () => {
|
||||
it("renders the compact live agent summary as a Preact block", () => {
|
||||
const parent = document.createElement("div");
|
||||
|
||||
renderMessageStreamBlocksInAct(
|
||||
|
|
@ -901,7 +901,7 @@ describe("work log renderer decisions", () => {
|
|||
unmountReactRootInAct(parent);
|
||||
});
|
||||
|
||||
it("renders active reasoning as a React message stream block", () => {
|
||||
it("renders active reasoning as a Preact message stream block", () => {
|
||||
const parent = document.createElement("div");
|
||||
|
||||
renderMessageStreamBlocksInAct(
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { describe, expect, it } from "vitest";
|
|||
|
||||
import { renderReactRoot, unmountReactRoot } from "../../../../src/shared/ui/react-root";
|
||||
|
||||
describe("React root adapter", () => {
|
||||
describe("Preact compat root adapter", () => {
|
||||
it("reuses roots that render no host children", () => {
|
||||
const parent = document.createElement("div");
|
||||
|
||||
|
|
|
|||
|
|
@ -153,7 +153,7 @@ describe("chat turn diff view decisions", () => {
|
|||
expect(parent.querySelector(".codex-panel-chat-turn-diff__diff")).toBeNull();
|
||||
});
|
||||
|
||||
it("unmounts the turn diff React root when the view closes", async () => {
|
||||
it("unmounts the turn diff Preact root when the view closes", async () => {
|
||||
const containerEl = document.createElement("div");
|
||||
const view = new CodexChatTurnDiffView({ containerEl } as unknown as WorkspaceLeaf);
|
||||
|
||||
|
|
|
|||
|
|
@ -167,7 +167,7 @@ describe("CodexChatView connection lifecycle", () => {
|
|||
);
|
||||
});
|
||||
|
||||
it("renders the React shell on the view content root", async () => {
|
||||
it("renders the Preact shell on the view content root", async () => {
|
||||
const view = await chatView();
|
||||
|
||||
await view.onOpen();
|
||||
|
|
|
|||
|
|
@ -291,7 +291,7 @@ describe("selection rewrite popover", () => {
|
|||
expect(onClose).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("unmounts and removes the React popover when closed", () => {
|
||||
it("unmounts and removes the Preact popover when closed", () => {
|
||||
const onClose = vi.fn();
|
||||
const popover = new SelectionRewritePopover(popoverOptions({ onClose }));
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue