mirror of
https://github.com/murashit/codex-panel.git
synced 2026-07-22 06:57:10 +00:00
Switch UI runtime to Preact compat
This commit is contained in:
parent
2065bf3999
commit
86cfda539c
26 changed files with 160 additions and 150 deletions
|
|
@ -12,6 +12,7 @@ await esbuild.build({
|
||||||
platform: "node",
|
platform: "node",
|
||||||
target: "es2022",
|
target: "es2022",
|
||||||
jsx: "automatic",
|
jsx: "automatic",
|
||||||
|
jsxImportSource: "preact",
|
||||||
outfile: "main.js",
|
outfile: "main.js",
|
||||||
sourcemap: false,
|
sourcemap: false,
|
||||||
minify: true,
|
minify: true,
|
||||||
|
|
|
||||||
86
package-lock.json
generated
86
package-lock.json
generated
|
|
@ -9,14 +9,11 @@
|
||||||
"version": "2.4.0",
|
"version": "2.4.0",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"react": "^18.3.1",
|
"preact": "^10.29.2"
|
||||||
"react-dom": "^18.3.1"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^10.0.1",
|
"@eslint/js": "^10.0.1",
|
||||||
"@types/node": "^25.6.0",
|
"@types/node": "^25.6.0",
|
||||||
"@types/react": "^18.3.29",
|
|
||||||
"@types/react-dom": "^18.3.7",
|
|
||||||
"esbuild": "^0.28.0",
|
"esbuild": "^0.28.0",
|
||||||
"eslint": "^10.3.0",
|
"eslint": "^10.3.0",
|
||||||
"eslint-config-prettier": "^10.1.8",
|
"eslint-config-prettier": "^10.1.8",
|
||||||
|
|
@ -2023,34 +2020,6 @@
|
||||||
"undici-types": ">=7.24.0 <7.24.7"
|
"undici-types": ">=7.24.0 <7.24.7"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@types/prop-types": {
|
|
||||||
"version": "15.7.15",
|
|
||||||
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz",
|
|
||||||
"integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/@types/react": {
|
|
||||||
"version": "18.3.29",
|
|
||||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.29.tgz",
|
|
||||||
"integrity": "sha512-ch0qJdr2JY0r04NXSprbK6TXOgnaJ1Tz23fm5W+z0/CBah6BSBc3n96h7K9GOtwh0HrilNWHIBzE1Ko4Dcw/Wg==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@types/prop-types": "*",
|
|
||||||
"csstype": "^3.2.2"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@types/react-dom": {
|
|
||||||
"version": "18.3.7",
|
|
||||||
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz",
|
|
||||||
"integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"peerDependencies": {
|
|
||||||
"@types/react": "^18.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@types/tern": {
|
"node_modules/@types/tern": {
|
||||||
"version": "0.23.9",
|
"version": "0.23.9",
|
||||||
"resolved": "https://registry.npmjs.org/@types/tern/-/tern-0.23.9.tgz",
|
"resolved": "https://registry.npmjs.org/@types/tern/-/tern-0.23.9.tgz",
|
||||||
|
|
@ -3042,13 +3011,6 @@
|
||||||
"node": ">=4"
|
"node": ">=4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/csstype": {
|
|
||||||
"version": "3.2.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
|
||||||
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/data-urls": {
|
"node_modules/data-urls": {
|
||||||
"version": "7.0.0",
|
"version": "7.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/data-urls/-/data-urls-7.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/data-urls/-/data-urls-7.0.0.tgz",
|
||||||
|
|
@ -5678,6 +5640,7 @@
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||||
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
|
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/js-yaml": {
|
"node_modules/js-yaml": {
|
||||||
|
|
@ -6211,6 +6174,7 @@
|
||||||
"version": "1.4.0",
|
"version": "1.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
|
||||||
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
|
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"js-tokens": "^3.0.0 || ^4.0.0"
|
"js-tokens": "^3.0.0 || ^4.0.0"
|
||||||
|
|
@ -6845,6 +6809,16 @@
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/preact": {
|
||||||
|
"version": "10.29.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/preact/-/preact-10.29.2.tgz",
|
||||||
|
"integrity": "sha512-7tNmwg/7mzzAoB/8kSg6Hl37JraAZw3Z3A0JSY7VXlZwo82Xn0G7wKbNNs2qoF4ZEEsQGTwDAroNdqKs1ofJxQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/preact"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/prelude-ls": {
|
"node_modules/prelude-ls": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
|
||||||
|
|
@ -6934,31 +6908,6 @@
|
||||||
],
|
],
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/react": {
|
|
||||||
"version": "18.3.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
|
|
||||||
"integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"loose-envify": "^1.1.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=0.10.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/react-dom": {
|
|
||||||
"version": "18.3.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
|
|
||||||
"integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"loose-envify": "^1.1.0",
|
|
||||||
"scheduler": "^0.23.2"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"react": "^18.3.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/react-is": {
|
"node_modules/react-is": {
|
||||||
"version": "16.13.1",
|
"version": "16.13.1",
|
||||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
||||||
|
|
@ -7252,15 +7201,6 @@
|
||||||
"node": ">=v12.22.7"
|
"node": ">=v12.22.7"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/scheduler": {
|
|
||||||
"version": "0.23.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
|
|
||||||
"integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"loose-envify": "^1.1.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/semver": {
|
"node_modules/semver": {
|
||||||
"version": "7.8.0",
|
"version": "7.8.0",
|
||||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz",
|
"resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz",
|
||||||
|
|
|
||||||
|
|
@ -40,8 +40,6 @@
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^10.0.1",
|
"@eslint/js": "^10.0.1",
|
||||||
"@types/node": "^25.6.0",
|
"@types/node": "^25.6.0",
|
||||||
"@types/react": "^18.3.29",
|
|
||||||
"@types/react-dom": "^18.3.7",
|
|
||||||
"esbuild": "^0.28.0",
|
"esbuild": "^0.28.0",
|
||||||
"eslint": "^10.3.0",
|
"eslint": "^10.3.0",
|
||||||
"eslint-config-prettier": "^10.1.8",
|
"eslint-config-prettier": "^10.1.8",
|
||||||
|
|
@ -57,7 +55,6 @@
|
||||||
"vitest": "^4.1.7"
|
"vitest": "^4.1.7"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"react": "^18.3.1",
|
"preact": "^10.29.2"
|
||||||
"react-dom": "^18.3.1"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import type { App, Component } from "obsidian";
|
import type { App, Component } from "obsidian";
|
||||||
import type { ReactNode } from "react";
|
import type { ReactNode } from "preact/compat";
|
||||||
|
|
||||||
import type { DisplayItem } from "./display/types";
|
import type { DisplayItem } from "./display/types";
|
||||||
import { copyTextWithNotice } from "../../shared/ui/clipboard";
|
import { copyTextWithNotice } from "../../shared/ui/clipboard";
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import type { ReactNode } from "react";
|
import type { ReactNode } from "preact/compat";
|
||||||
|
|
||||||
import type { ApprovalAction, PendingApproval } from "../../approvals/model";
|
import type { ApprovalAction, PendingApproval } from "../../approvals/model";
|
||||||
import type { ChatAction, ChatState, ChatStateStore } from "../../chat-state";
|
import type { ChatAction, ChatState, ChatStateStore } from "../../chat-state";
|
||||||
|
|
|
||||||
|
|
@ -70,6 +70,7 @@ export class ThreadResumeController {
|
||||||
this.host.clearDeferredRestoredThreadHydration();
|
this.host.clearDeferredRestoredThreadHydration();
|
||||||
this.host.resetThreadTurnPresence(false);
|
this.host.resetThreadTurnPresence(false);
|
||||||
this.host.notifyActiveThreadIdentityChanged();
|
this.host.notifyActiveThreadIdentityChanged();
|
||||||
|
this.host.forceMessagesToBottom();
|
||||||
this.host.render();
|
this.host.render();
|
||||||
this.host.refreshLiveState();
|
this.host.refreshLiveState();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { useLayoutEffect, useRef, type ButtonHTMLAttributes, type ReactNode } from "react";
|
import type { ButtonHTMLAttributes } from "preact";
|
||||||
|
import { useLayoutEffect, useRef, type ReactNode } from "preact/compat";
|
||||||
|
|
||||||
import type { ComposerSuggestion } from "../composer/suggestions";
|
import type { ComposerSuggestion } from "../composer/suggestions";
|
||||||
import { IconButton } from "../../../shared/ui/react-components";
|
import { IconButton } from "../../../shared/ui/react-components";
|
||||||
|
|
@ -21,6 +22,10 @@ export interface ComposerCallbacks {
|
||||||
onSuggestionInsert: (suggestion: ComposerSuggestion) => void;
|
onSuggestionInsert: (suggestion: ComposerSuggestion) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ButtonProps = ButtonHTMLAttributes & {
|
||||||
|
disabled?: boolean | undefined;
|
||||||
|
};
|
||||||
|
|
||||||
export function renderComposerShell(
|
export function renderComposerShell(
|
||||||
parent: HTMLElement,
|
parent: HTMLElement,
|
||||||
viewId: string,
|
viewId: string,
|
||||||
|
|
@ -102,7 +107,7 @@ function ComposerShell({
|
||||||
onClick={callbacks.onUpdateSuggestions}
|
onClick={callbacks.onUpdateSuggestions}
|
||||||
onSelect={callbacks.onUpdateSuggestions}
|
onSelect={callbacks.onUpdateSuggestions}
|
||||||
onKeyDown={(event) => {
|
onKeyDown={(event) => {
|
||||||
callbacks.onKeydown(event.nativeEvent);
|
callbacks.onKeydown(event);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<div className="codex-panel-ui__action-stack codex-panel__composer-actions">
|
<div className="codex-panel-ui__action-stack codex-panel__composer-actions">
|
||||||
|
|
@ -152,7 +157,7 @@ function ComposerIconButton({
|
||||||
icon: string;
|
icon: string;
|
||||||
label: string;
|
label: string;
|
||||||
className: string;
|
className: string;
|
||||||
} & Omit<ButtonHTMLAttributes<HTMLButtonElement>, "className" | "type">): ReactNode {
|
} & Omit<ButtonProps, "className" | "type">): ReactNode {
|
||||||
return (
|
return (
|
||||||
<IconButton
|
<IconButton
|
||||||
{...props}
|
{...props}
|
||||||
|
|
@ -237,7 +242,7 @@ function ComposerSuggestions({
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={optionId}
|
key={optionId}
|
||||||
ref={selected ? selectedRef : undefined}
|
ref={selected ? selectedRef : null}
|
||||||
className={`suggestion-item codex-panel__composer-suggestion ${selected ? "is-selected" : ""}`}
|
className={`suggestion-item codex-panel__composer-suggestion ${selected ? "is-selected" : ""}`}
|
||||||
id={optionId}
|
id={optionId}
|
||||||
role="option"
|
role="option"
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { Fragment, forwardRef, useEffect, useLayoutEffect, useRef, useState, type ReactNode } from "react";
|
import { Fragment, forwardRef, useEffect, useLayoutEffect, useRef, useState, type ReactNode } from "preact/compat";
|
||||||
|
|
||||||
import { displayBlocksForItems } from "../display/blocks";
|
import { displayBlocksForItems } from "../display/blocks";
|
||||||
import { executionState } from "../display/state";
|
import { executionState } from "../display/state";
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { useEffect, useLayoutEffect, useRef, useState, type ReactNode } from "react";
|
import { useEffect, useLayoutEffect, useRef, useState, type ReactNode } from "preact/compat";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
approvalActionOptions,
|
approvalActionOptions,
|
||||||
|
|
@ -312,6 +312,9 @@ function OtherUserInputOption({
|
||||||
name={groupName}
|
name={groupName}
|
||||||
value="__other__"
|
value="__other__"
|
||||||
checked={otherSelected}
|
checked={otherSelected}
|
||||||
|
onClick={(event) => {
|
||||||
|
if (event.currentTarget.checked) selectOther();
|
||||||
|
}}
|
||||||
onChange={(event) => {
|
onChange={(event) => {
|
||||||
if (event.currentTarget.checked) selectOther();
|
if (event.currentTarget.checked) selectOther();
|
||||||
}}
|
}}
|
||||||
|
|
@ -335,7 +338,7 @@ function OtherUserInputOption({
|
||||||
}}
|
}}
|
||||||
onChange={(event) => {
|
onChange={(event) => {
|
||||||
setInputValue(event.currentTarget.value);
|
setInputValue(event.currentTarget.value);
|
||||||
const nativeEvent = event.nativeEvent as Event & { isComposing?: boolean };
|
const nativeEvent = event as Event & { isComposing?: boolean };
|
||||||
if (nativeEvent.isComposing !== true && !composingRef.current) commitOtherValue(event.currentTarget.value);
|
if (nativeEvent.isComposing !== true && !composingRef.current) commitOtherValue(event.currentTarget.value);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -105,6 +105,9 @@ export class MessageScrollController {
|
||||||
|
|
||||||
this.observeMessageBlocks(container);
|
this.observeMessageBlocks(container);
|
||||||
this.cancelRenderFrame();
|
this.cancelRenderFrame();
|
||||||
|
if (plan.shouldScrollToBottom && this.options.messagesPinnedToBottom()) {
|
||||||
|
this.pinToBottom(container);
|
||||||
|
}
|
||||||
this.renderFrame = container.win.requestAnimationFrame(() => {
|
this.renderFrame = container.win.requestAnimationFrame(() => {
|
||||||
this.renderFrame = null;
|
this.renderFrame = null;
|
||||||
if (plan.generation !== this.renderGeneration) return;
|
if (plan.generation !== this.renderGeneration) return;
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { useLayoutEffect, useRef, useSyncExternalStore, type ReactNode } from "react";
|
import { useLayoutEffect, useRef, useSyncExternalStore, type ReactNode } from "preact/compat";
|
||||||
|
|
||||||
import { renderReactRoot, unmountReactRoot } from "../../../shared/ui/react-root";
|
import { renderReactRoot, unmountReactRoot } from "../../../shared/ui/react-root";
|
||||||
import type { ChatState, ChatStateStore } from "../chat-state";
|
import type { ChatState, ChatStateStore } from "../chat-state";
|
||||||
|
|
@ -19,8 +19,11 @@ interface ChatPanelShellSlots {
|
||||||
composer?: HTMLElement;
|
composer?: HTMLElement;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const shellSlotsByContainer = new WeakMap<HTMLElement, ChatPanelShellSlots>();
|
||||||
|
|
||||||
export function renderChatPanelShell(container: HTMLElement, props: ChatPanelShellProps): void {
|
export function renderChatPanelShell(container: HTMLElement, props: ChatPanelShellProps): void {
|
||||||
const mountedSlots: ChatPanelShellSlots = {};
|
const mountedSlots = shellSlotsByContainer.get(container) ?? {};
|
||||||
|
shellSlotsByContainer.set(container, mountedSlots);
|
||||||
container.addClass("codex-panel");
|
container.addClass("codex-panel");
|
||||||
renderReactRoot(
|
renderReactRoot(
|
||||||
container,
|
container,
|
||||||
|
|
@ -36,6 +39,7 @@ export function renderChatPanelShell(container: HTMLElement, props: ChatPanelShe
|
||||||
|
|
||||||
export function unmountChatPanelShell(container: HTMLElement | null): void {
|
export function unmountChatPanelShell(container: HTMLElement | null): void {
|
||||||
if (!container) return;
|
if (!container) return;
|
||||||
|
shellSlotsByContainer.delete(container);
|
||||||
unmountReactRoot(container);
|
unmountReactRoot(container);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -50,6 +54,7 @@ function ChatPanelShell({
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ChatPanelSlot
|
<ChatPanelSlot
|
||||||
|
key="toolbar"
|
||||||
name="toolbar"
|
name="toolbar"
|
||||||
className="codex-panel__toolbar"
|
className="codex-panel__toolbar"
|
||||||
stateStore={stateStore}
|
stateStore={stateStore}
|
||||||
|
|
@ -57,9 +62,10 @@ function ChatPanelShell({
|
||||||
slot={toolbar}
|
slot={toolbar}
|
||||||
onSlotReady={onSlotReady}
|
onSlotReady={onSlotReady}
|
||||||
/>
|
/>
|
||||||
<div className="codex-panel__body">
|
<div key="body" className="codex-panel__body">
|
||||||
<div className="codex-panel__slot codex-panel__slot--config" />
|
<div key="config" className="codex-panel__slot codex-panel__slot--config" />
|
||||||
<ChatPanelSlot
|
<ChatPanelSlot
|
||||||
|
key="messages"
|
||||||
name="messages"
|
name="messages"
|
||||||
className="codex-panel__slot codex-panel__slot--messages"
|
className="codex-panel__slot codex-panel__slot--messages"
|
||||||
renderTargetClassName="codex-panel__messages"
|
renderTargetClassName="codex-panel__messages"
|
||||||
|
|
@ -69,6 +75,7 @@ function ChatPanelShell({
|
||||||
onSlotReady={onSlotReady}
|
onSlotReady={onSlotReady}
|
||||||
/>
|
/>
|
||||||
<ChatPanelSlot
|
<ChatPanelSlot
|
||||||
|
key="composer"
|
||||||
name="composer"
|
name="composer"
|
||||||
className="codex-panel__slot codex-panel__slot--composer"
|
className="codex-panel__slot codex-panel__slot--composer"
|
||||||
stateStore={stateStore}
|
stateStore={stateStore}
|
||||||
|
|
@ -106,7 +113,6 @@ function ChatPanelSlot({
|
||||||
const snapshot = useSyncExternalStore(
|
const snapshot = useSyncExternalStore(
|
||||||
(listener) => stateStore.subscribe(listener),
|
(listener) => stateStore.subscribe(listener),
|
||||||
() => slot.snapshot(stateStore.getState()),
|
() => slot.snapshot(stateStore.getState()),
|
||||||
() => slot.snapshot(stateStore.getState()),
|
|
||||||
);
|
);
|
||||||
const ref = useRef<HTMLDivElement | null>(null);
|
const ref = useRef<HTMLDivElement | null>(null);
|
||||||
const renderGeneration = useRef(0);
|
const renderGeneration = useRef(0);
|
||||||
|
|
@ -126,7 +132,15 @@ function ChatPanelSlot({
|
||||||
}, [name, onSlotReady, renderKey, renderTargetClassName, slot]);
|
}, [name, onSlotReady, renderKey, renderTargetClassName, slot]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={ref} className={className}>
|
<div
|
||||||
|
ref={(element) => {
|
||||||
|
ref.current = element;
|
||||||
|
if (!element) return;
|
||||||
|
const renderTarget = renderTargetClassName ? element.querySelector<HTMLElement>(`:scope > .${renderTargetClassName}`) : element;
|
||||||
|
if (renderTarget) onSlotReady(name, renderTarget);
|
||||||
|
}}
|
||||||
|
className={className}
|
||||||
|
>
|
||||||
{renderTargetClassName ? <div className={renderTargetClassName} /> : null}
|
{renderTargetClassName ? <div className={renderTargetClassName} /> : null}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { useLayoutEffect, useRef, useState, type ReactNode } from "react";
|
import { useLayoutEffect, useRef, useState, type ReactNode } from "preact/compat";
|
||||||
|
|
||||||
import { toolResultView, type ToolResultDetailSection, type ToolResultDisplayItem, type ToolResultView } from "../display/tool-view";
|
import { toolResultView, type ToolResultDetailSection, type ToolResultDisplayItem, type ToolResultView } from "../display/tool-view";
|
||||||
import { renderRawDiffLines } from "../../../shared/diff/render";
|
import { renderRawDiffLines } from "../../../shared/diff/render";
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,15 @@
|
||||||
import { useLayoutEffect, useRef, type ButtonHTMLAttributes, type KeyboardEvent, type ReactNode } from "react";
|
import type { ButtonHTMLAttributes, TargetedKeyboardEvent } from "preact";
|
||||||
|
import { useLayoutEffect, useRef, type ReactNode } from "preact/compat";
|
||||||
|
|
||||||
import type { EffectiveConfigSection, RateLimitSummary } from "../../../runtime/view";
|
import type { EffectiveConfigSection, RateLimitSummary } from "../../../runtime/view";
|
||||||
import { IconButton, ObsidianIcon } from "../../../shared/ui/react-components";
|
import { IconButton, ObsidianIcon } from "../../../shared/ui/react-components";
|
||||||
import { renderReactRoot } from "../../../shared/ui/react-root";
|
import { renderReactRoot } from "../../../shared/ui/react-root";
|
||||||
import type { ToolbarDiagnosticSection, ToolbarThreadRow, ToolbarViewModel } from "../toolbar-model";
|
import type { ToolbarDiagnosticSection, ToolbarThreadRow, ToolbarViewModel } from "../toolbar-model";
|
||||||
|
|
||||||
|
type ButtonProps = ButtonHTMLAttributes & {
|
||||||
|
disabled?: boolean | undefined;
|
||||||
|
};
|
||||||
|
|
||||||
export interface ToolbarActions {
|
export interface ToolbarActions {
|
||||||
toggleHistory: () => void;
|
toggleHistory: () => void;
|
||||||
toggleAutoReview: () => void;
|
toggleAutoReview: () => void;
|
||||||
|
|
@ -66,7 +71,7 @@ function ToolbarIconButton({
|
||||||
icon: string;
|
icon: string;
|
||||||
label: string;
|
label: string;
|
||||||
className?: string;
|
className?: string;
|
||||||
} & Omit<ButtonHTMLAttributes<HTMLButtonElement>, "className" | "type">): ReactNode {
|
} & Omit<ButtonProps, "className" | "type">): ReactNode {
|
||||||
return (
|
return (
|
||||||
<IconButton
|
<IconButton
|
||||||
{...props}
|
{...props}
|
||||||
|
|
@ -444,7 +449,7 @@ function ThreadRenameRow({ thread, actions }: { thread: ToolbarThreadRow; action
|
||||||
onKeyDown={(event) => {
|
onKeyDown={(event) => {
|
||||||
if (event.key === "Enter") {
|
if (event.key === "Enter") {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
if (!event.nativeEvent.isComposing && !generating) actions.saveRenameThread(thread.threadId, event.currentTarget.value);
|
if (!event.isComposing && !generating) actions.saveRenameThread(thread.threadId, event.currentTarget.value);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (event.key === "Escape") {
|
if (event.key === "Escape") {
|
||||||
|
|
@ -498,7 +503,7 @@ function ToolbarPanelItem({
|
||||||
renderContent?: () => ReactNode;
|
renderContent?: () => ReactNode;
|
||||||
onClick?: () => void;
|
onClick?: () => void;
|
||||||
}): ReactNode {
|
}): ReactNode {
|
||||||
const onKeyDown = (event: KeyboardEvent<HTMLElement>) => {
|
const onKeyDown = (event: TargetedKeyboardEvent<HTMLElement>) => {
|
||||||
if (disabled || (event.key !== "Enter" && event.key !== " ")) return;
|
if (disabled || (event.key !== "Enter" && event.key !== " ")) return;
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
onClick?.();
|
onClick?.();
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { useLayoutEffect, useRef, type ReactNode } from "react";
|
import { useLayoutEffect, useRef, type ReactNode } from "preact/compat";
|
||||||
|
|
||||||
import { renderDisplayDiffLines } from "../../../shared/diff/render";
|
import { renderDisplayDiffLines } from "../../../shared/diff/render";
|
||||||
import { displayDiffLines } from "../../../shared/diff/unified";
|
import { displayDiffLines } from "../../../shared/diff/unified";
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { useLayoutEffect, useState, type ReactNode } from "react";
|
import { useLayoutEffect, useState, type ReactNode } from "preact/compat";
|
||||||
|
|
||||||
import { activeAgentRunSummary } from "../display/agent";
|
import { activeAgentRunSummary } from "../display/agent";
|
||||||
import { executionState } from "../display/state";
|
import { executionState } from "../display/state";
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { Notice, type Editor } from "obsidian";
|
import { Notice, type Editor } from "obsidian";
|
||||||
import { useLayoutEffect, useRef, type KeyboardEvent, type ReactNode } from "react";
|
import type { TargetedKeyboardEvent } from "preact";
|
||||||
|
import { useLayoutEffect, useRef, type ReactNode } from "preact/compat";
|
||||||
|
|
||||||
import { renderDisplayDiffLines } from "../../shared/diff/render";
|
import { renderDisplayDiffLines } from "../../shared/diff/render";
|
||||||
import { displayDiffLines } from "../../shared/diff/unified";
|
import { displayDiffLines } from "../../shared/diff/unified";
|
||||||
|
|
@ -16,6 +17,7 @@ import {
|
||||||
type SelectionRewriteRuntimeSettings,
|
type SelectionRewriteRuntimeSettings,
|
||||||
type SelectionRewriteState,
|
type SelectionRewriteState,
|
||||||
} from "./model";
|
} from "./model";
|
||||||
|
|
||||||
import { SelectionRewriteOutputError } from "./output";
|
import { SelectionRewriteOutputError } from "./output";
|
||||||
import { positionSelectionRewritePopover } from "./position";
|
import { positionSelectionRewritePopover } from "./position";
|
||||||
import { buildSelectionRewritePrompt } from "./prompt";
|
import { buildSelectionRewritePrompt } from "./prompt";
|
||||||
|
|
@ -285,11 +287,7 @@ export class SelectionRewritePopover {
|
||||||
}}
|
}}
|
||||||
onInstructionKeyDown={(event) => {
|
onInstructionKeyDown={(event) => {
|
||||||
const hasReplacement = this.options.state.replacementText !== null;
|
const hasReplacement = this.options.state.replacementText !== null;
|
||||||
if (
|
if (!(hasReplacement ? isSelectionRewriteActionKey(event) : isComposerSendKey(event, this.options.sendShortcut))) {
|
||||||
!(hasReplacement
|
|
||||||
? isSelectionRewriteActionKey(event.nativeEvent)
|
|
||||||
: isComposerSendKey(event.nativeEvent, this.options.sendShortcut))
|
|
||||||
) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
@ -297,7 +295,7 @@ export class SelectionRewritePopover {
|
||||||
void this.generate();
|
void this.generate();
|
||||||
}}
|
}}
|
||||||
onApplyKeyDown={(event) => {
|
onApplyKeyDown={(event) => {
|
||||||
if (!isSelectionRewriteActionKey(event.nativeEvent)) return;
|
if (!isSelectionRewriteActionKey(event)) return;
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
this.apply();
|
this.apply();
|
||||||
}}
|
}}
|
||||||
|
|
@ -342,11 +340,11 @@ interface SelectionRewritePopoverViewProps {
|
||||||
instruction: string;
|
instruction: string;
|
||||||
instructionRef: (element: HTMLTextAreaElement | null) => void;
|
instructionRef: (element: HTMLTextAreaElement | null) => void;
|
||||||
onApply: () => void;
|
onApply: () => void;
|
||||||
onApplyKeyDown: (event: KeyboardEvent<HTMLButtonElement>) => void;
|
onApplyKeyDown: (event: TargetedKeyboardEvent<HTMLButtonElement>) => void;
|
||||||
onCancel: () => void;
|
onCancel: () => void;
|
||||||
onGenerate: () => void;
|
onGenerate: () => void;
|
||||||
onInstructionInput: (value: string) => void;
|
onInstructionInput: (value: string) => void;
|
||||||
onInstructionKeyDown: (event: KeyboardEvent<HTMLTextAreaElement>) => void;
|
onInstructionKeyDown: (event: TargetedKeyboardEvent<HTMLTextAreaElement>) => void;
|
||||||
status: SelectionRewritePopoverStatusState;
|
status: SelectionRewritePopoverStatusState;
|
||||||
streamPreview: string;
|
streamPreview: string;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,14 @@
|
||||||
import { useLayoutEffect, useRef, type ButtonHTMLAttributes, type KeyboardEvent, type ReactNode } from "react";
|
import type { ButtonHTMLAttributes, TargetedKeyboardEvent } from "preact";
|
||||||
|
import { useLayoutEffect, useRef, type ReactNode } from "preact/compat";
|
||||||
|
|
||||||
import { IconButton } from "../../shared/ui/react-components";
|
import { IconButton } from "../../shared/ui/react-components";
|
||||||
import { renderReactRoot, unmountReactRoot } from "../../shared/ui/react-root";
|
import { renderReactRoot, unmountReactRoot } from "../../shared/ui/react-root";
|
||||||
import type { ThreadsRowModel } from "./state";
|
import type { ThreadsRowModel } from "./state";
|
||||||
|
|
||||||
|
type ButtonProps = ButtonHTMLAttributes & {
|
||||||
|
disabled?: boolean | undefined;
|
||||||
|
};
|
||||||
|
|
||||||
export interface ThreadsViewModel {
|
export interface ThreadsViewModel {
|
||||||
status: string | null;
|
status: string | null;
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
|
|
@ -82,7 +87,7 @@ function ThreadRow({ row, actions }: { row: ThreadsRowModel; actions: ThreadsVie
|
||||||
if (row.rename.active || archiveConfirm.active) return;
|
if (row.rename.active || archiveConfirm.active) return;
|
||||||
actions.openThread(row.thread.id);
|
actions.openThread(row.thread.id);
|
||||||
};
|
};
|
||||||
const openFromKeyboard = (event: KeyboardEvent<HTMLDivElement>) => {
|
const openFromKeyboard = (event: TargetedKeyboardEvent<HTMLDivElement>) => {
|
||||||
if (row.rename.active || archiveConfirm.active) return;
|
if (row.rename.active || archiveConfirm.active) return;
|
||||||
if (event.key !== "Enter" && event.key !== " ") return;
|
if (event.key !== "Enter" && event.key !== " ") return;
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
@ -204,7 +209,7 @@ function RenameRow({ row, actions }: { row: ThreadsRowModel; actions: ThreadsVie
|
||||||
onKeyDown={(event) => {
|
onKeyDown={(event) => {
|
||||||
if (event.key === "Enter") {
|
if (event.key === "Enter") {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
if (!event.nativeEvent.isComposing && !row.rename.generating) actions.saveRename(row.thread.id, event.currentTarget.value);
|
if (!event.isComposing && !row.rename.generating) actions.saveRename(row.thread.id, event.currentTarget.value);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (event.key === "Escape") {
|
if (event.key === "Escape") {
|
||||||
|
|
@ -247,7 +252,7 @@ function ThreadsIconButton({
|
||||||
icon: string;
|
icon: string;
|
||||||
label: string;
|
label: string;
|
||||||
className: string;
|
className: string;
|
||||||
} & Omit<ButtonHTMLAttributes<HTMLButtonElement>, "className" | "type">): ReactNode {
|
} & Omit<ButtonProps, "className" | "type">): ReactNode {
|
||||||
return (
|
return (
|
||||||
<IconButton
|
<IconButton
|
||||||
{...props}
|
{...props}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { setIcon } from "obsidian";
|
import { setIcon } from "obsidian";
|
||||||
import { useLayoutEffect, useRef, type ButtonHTMLAttributes, type MutableRefObject, type ReactNode, type Ref } from "react";
|
import type { ButtonHTMLAttributes } from "preact";
|
||||||
|
import { useLayoutEffect, useRef, type MutableRefObject, type ReactNode, type Ref } from "preact/compat";
|
||||||
|
|
||||||
export interface ObsidianIconProps {
|
export interface ObsidianIconProps {
|
||||||
icon: string;
|
icon: string;
|
||||||
|
|
@ -17,10 +18,12 @@ export function ObsidianIcon({ icon, className }: ObsidianIconProps): ReactNode
|
||||||
return <span ref={ref} className={className} aria-hidden="true" />;
|
return <span ref={ref} className={className} aria-hidden="true" />;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IconButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
export interface IconButtonProps extends ButtonHTMLAttributes {
|
||||||
icon: string;
|
icon: string;
|
||||||
label: string;
|
label: string;
|
||||||
buttonRef?: Ref<HTMLButtonElement>;
|
buttonRef?: Ref<HTMLButtonElement>;
|
||||||
|
disabled?: boolean | undefined;
|
||||||
|
type?: "button" | "submit" | "reset" | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function IconButton({ icon, label, buttonRef, className, children, ...props }: IconButtonProps): ReactNode {
|
export function IconButton({ icon, label, buttonRef, className, children, ...props }: IconButtonProps): ReactNode {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
import type { ReactNode } from "react";
|
import type { ReactNode } from "preact/compat";
|
||||||
import { flushSync } from "react-dom";
|
import { flushSync } from "preact/compat";
|
||||||
import { createRoot, type Root } from "react-dom/client";
|
import { createRoot } from "preact/compat/client";
|
||||||
|
|
||||||
|
type Root = ReturnType<typeof createRoot>;
|
||||||
|
|
||||||
const roots = new WeakMap<HTMLElement, Root>();
|
const roots = new WeakMap<HTMLElement, Root>();
|
||||||
const guardedContainers = new WeakSet<HTMLElement>();
|
const guardedContainers = new WeakSet<HTMLElement>();
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
// @vitest-environment jsdom
|
// @vitest-environment jsdom
|
||||||
|
|
||||||
import { describe, expect, it, vi } from "vitest";
|
import { describe, expect, it, vi } from "vitest";
|
||||||
import { act, createElement } from "react";
|
import { createElement } from "preact/compat";
|
||||||
|
import { act } from "preact/test-utils";
|
||||||
|
|
||||||
import type { DisplayItem } from "../../../../../src/features/chat/display/types";
|
import type { DisplayItem } from "../../../../../src/features/chat/display/types";
|
||||||
import { implementPlanCandidateFromState } from "../../../../../src/features/chat/chat-message-renderer";
|
import { implementPlanCandidateFromState } from "../../../../../src/features/chat/chat-message-renderer";
|
||||||
|
|
@ -297,7 +298,7 @@ describe("message stream block identity and message actions", () => {
|
||||||
expect(renderTextWithWikiLinks).toHaveBeenCalledWith(expect.any(HTMLElement), "npm test");
|
expect(renderTextWithWikiLinks).toHaveBeenCalledWith(expect.any(HTMLElement), "npm test");
|
||||||
|
|
||||||
const details = expectPresent(result.querySelector<HTMLDetailsElement>("details"));
|
const details = expectPresent(result.querySelector<HTMLDetailsElement>("details"));
|
||||||
act(() => {
|
void act(() => {
|
||||||
details.open = true;
|
details.open = true;
|
||||||
details.dispatchEvent(new Event("toggle", { bubbles: false }));
|
details.dispatchEvent(new Event("toggle", { bubbles: false }));
|
||||||
});
|
});
|
||||||
|
|
@ -890,7 +891,7 @@ describe("message stream block identity and message actions", () => {
|
||||||
expect(details?.querySelector("summary")?.textContent).toBe("Show more");
|
expect(details?.querySelector("summary")?.textContent).toBe("Show more");
|
||||||
|
|
||||||
if (details) {
|
if (details) {
|
||||||
act(() => {
|
void act(() => {
|
||||||
details.open = true;
|
details.open = true;
|
||||||
details.dispatchEvent(new Event("toggle"));
|
details.dispatchEvent(new Event("toggle"));
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { vi } from "vitest";
|
import { vi } from "vitest";
|
||||||
import { act, type ReactNode } from "react";
|
import type { ReactNode } from "preact/compat";
|
||||||
|
import { act } from "preact/test-utils";
|
||||||
|
|
||||||
import type { PendingApproval } from "../../../../../src/features/chat/approvals/model";
|
import type { PendingApproval } from "../../../../../src/features/chat/approvals/model";
|
||||||
import type { PendingUserInput } from "../../../../../src/features/chat/user-input/model";
|
import type { PendingUserInput } from "../../../../../src/features/chat/user-input/model";
|
||||||
|
|
@ -50,23 +51,23 @@ export function renderMessageBlockElement(block: ReturnType<typeof rawMessageStr
|
||||||
}
|
}
|
||||||
|
|
||||||
export function actEvent(action: () => void): void {
|
export function actEvent(action: () => void): void {
|
||||||
act(action);
|
void act(action);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function renderMessageStreamBlocksInAct(parent: HTMLElement, blocks: MessageStreamBlock[]): void {
|
export function renderMessageStreamBlocksInAct(parent: HTMLElement, blocks: MessageStreamBlock[]): void {
|
||||||
act(() => {
|
void act(() => {
|
||||||
renderMessageStreamBlocks(parent, blocks);
|
renderMessageStreamBlocks(parent, blocks);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function renderReactRootInAct(parent: HTMLElement, node: ReactNode): void {
|
export function renderReactRootInAct(parent: HTMLElement, node: ReactNode): void {
|
||||||
act(() => {
|
void act(() => {
|
||||||
renderReactRoot(parent, node);
|
renderReactRoot(parent, node);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function unmountReactRootInAct(parent: HTMLElement): void {
|
export function unmountReactRootInAct(parent: HTMLElement): void {
|
||||||
act(() => {
|
void act(() => {
|
||||||
unmountReactRoot(parent);
|
unmountReactRoot(parent);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
// @vitest-environment jsdom
|
// @vitest-environment jsdom
|
||||||
|
|
||||||
import { describe, expect, it, vi } from "vitest";
|
import { describe, expect, it, vi } from "vitest";
|
||||||
import { act } from "react";
|
import { act } from "preact/test-utils";
|
||||||
|
|
||||||
import type { DisplayItem } from "../../../../../src/features/chat/display/types";
|
import type { DisplayItem } from "../../../../../src/features/chat/display/types";
|
||||||
import { topLevelDetailsSummaries } from "../../../../support/dom";
|
import { topLevelDetailsSummaries } from "../../../../support/dom";
|
||||||
|
|
@ -395,7 +395,7 @@ describe("work log renderer decisions", () => {
|
||||||
expect(group.querySelector(".codex-panel__agent-activity .codex-panel__tool-summary")?.textContent).toBe("spawn child (completed)");
|
expect(group.querySelector(".codex-panel__agent-activity .codex-panel__tool-summary")?.textContent).toBe("spawn child (completed)");
|
||||||
|
|
||||||
const details = expectPresent(group.querySelector<HTMLDetailsElement>(".codex-panel__activity-group"));
|
const details = expectPresent(group.querySelector<HTMLDetailsElement>(".codex-panel__activity-group"));
|
||||||
act(() => {
|
void act(() => {
|
||||||
details.open = false;
|
details.open = false;
|
||||||
details.dispatchEvent(new Event("toggle", { bubbles: false }));
|
details.dispatchEvent(new Event("toggle", { bubbles: false }));
|
||||||
});
|
});
|
||||||
|
|
@ -724,7 +724,7 @@ describe("work log renderer decisions", () => {
|
||||||
expect([...block.querySelectorAll("details summary")].map((summary) => summary.textContent)).toEqual(["Details"]);
|
expect([...block.querySelectorAll("details summary")].map((summary) => summary.textContent)).toEqual(["Details"]);
|
||||||
|
|
||||||
const details = expectPresent(block.querySelector<HTMLDetailsElement>("details"));
|
const details = expectPresent(block.querySelector<HTMLDetailsElement>("details"));
|
||||||
act(() => {
|
void act(() => {
|
||||||
details.open = true;
|
details.open = true;
|
||||||
details.dispatchEvent(new Event("toggle", { bubbles: false }));
|
details.dispatchEvent(new Event("toggle", { bubbles: false }));
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
// @vitest-environment jsdom
|
// @vitest-environment jsdom
|
||||||
|
|
||||||
import { describe, expect, it, vi } from "vitest";
|
import { describe, expect, it, vi } from "vitest";
|
||||||
import { act } from "react";
|
import { act } from "preact/test-utils";
|
||||||
|
|
||||||
import { chatTurnBusy, createChatStateStore } from "../../../../src/features/chat/chat-state";
|
import { chatTurnBusy, createChatStateStore } from "../../../../src/features/chat/chat-state";
|
||||||
import { renderChatPanelShell, unmountChatPanelShell } from "../../../../src/features/chat/ui/shell";
|
import { renderChatPanelShell, unmountChatPanelShell } from "../../../../src/features/chat/ui/shell";
|
||||||
|
|
|
||||||
|
|
@ -429,8 +429,7 @@ describe("CodexChatView connection lifecycle", () => {
|
||||||
const view = await chatView();
|
const view = await chatView();
|
||||||
|
|
||||||
await view.onOpen();
|
await view.onOpen();
|
||||||
const composer = composerElement(view);
|
const focus = vi.spyOn(HTMLTextAreaElement.prototype, "focus").mockImplementation(() => undefined);
|
||||||
const focus = vi.spyOn(composer, "focus");
|
|
||||||
|
|
||||||
await view.openThread("thread-1");
|
await view.openThread("thread-1");
|
||||||
await view.focusThread("thread-1");
|
await view.focusThread("thread-1");
|
||||||
|
|
@ -509,20 +508,17 @@ describe("CodexChatView connection lifecycle", () => {
|
||||||
const messages = view.containerEl.querySelector<HTMLElement>(".codex-panel__messages");
|
const messages = view.containerEl.querySelector<HTMLElement>(".codex-panel__messages");
|
||||||
expect(messages).not.toBeNull();
|
expect(messages).not.toBeNull();
|
||||||
if (!messages) return;
|
if (!messages) return;
|
||||||
Object.defineProperty(messages, "scrollHeight", { value: 1000, configurable: true });
|
const restoreMessagesLayout = mockMessagesLayout({ scrollHeight: 1000, clientHeight: 100 });
|
||||||
Object.defineProperty(messages, "clientHeight", { value: 100, configurable: true });
|
|
||||||
messages.scrollTop = 0;
|
messages.scrollTop = 0;
|
||||||
|
|
||||||
view.setComposerText("/resume thread-1");
|
view.setComposerText("/resume thread-1");
|
||||||
await (view as unknown as { submitComposerAction: () => Promise<void> }).submitComposerAction();
|
await (view as unknown as { submitComposerAction: () => Promise<void> }).submitComposerAction();
|
||||||
await new Promise<void>((resolve) => {
|
await waitForMessagesFrame(messages);
|
||||||
messages.win.requestAnimationFrame(() => {
|
await waitForMessagesFrame(messages);
|
||||||
resolve();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const renderedMessages = view.containerEl.querySelector<HTMLElement>(".codex-panel__messages");
|
const renderedMessages = view.containerEl.querySelector<HTMLElement>(".codex-panel__messages");
|
||||||
expect(renderedMessages?.scrollTop).toBe(1000);
|
expect(renderedMessages?.scrollTop).toBe(1000);
|
||||||
|
restoreMessagesLayout();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("routes slash archive through shared panel notifications", async () => {
|
it("routes slash archive through shared panel notifications", async () => {
|
||||||
|
|
@ -650,19 +646,16 @@ describe("CodexChatView connection lifecycle", () => {
|
||||||
const messages = view.containerEl.querySelector<HTMLElement>(".codex-panel__messages");
|
const messages = view.containerEl.querySelector<HTMLElement>(".codex-panel__messages");
|
||||||
expect(messages).not.toBeNull();
|
expect(messages).not.toBeNull();
|
||||||
if (!messages) return;
|
if (!messages) return;
|
||||||
Object.defineProperty(messages, "scrollHeight", { value: 1000, configurable: true });
|
const restoreMessagesLayout = mockMessagesLayout({ scrollHeight: 1000, clientHeight: 100 });
|
||||||
Object.defineProperty(messages, "clientHeight", { value: 100, configurable: true });
|
|
||||||
messages.scrollTop = 0;
|
messages.scrollTop = 0;
|
||||||
|
|
||||||
await view.openThread("thread-1");
|
await view.openThread("thread-1");
|
||||||
await new Promise<void>((resolve) => {
|
await waitForMessagesFrame(messages);
|
||||||
messages.win.requestAnimationFrame(() => {
|
await waitForMessagesFrame(messages);
|
||||||
resolve();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const renderedMessages = view.containerEl.querySelector<HTMLElement>(".codex-panel__messages");
|
const renderedMessages = view.containerEl.querySelector<HTMLElement>(".codex-panel__messages");
|
||||||
expect(renderedMessages?.scrollTop).toBe(1000);
|
expect(renderedMessages?.scrollTop).toBe(1000);
|
||||||
|
restoreMessagesLayout();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("renders resumed thread metadata before history hydration completes", async () => {
|
it("renders resumed thread metadata before history hydration completes", async () => {
|
||||||
|
|
@ -933,6 +926,43 @@ function composerPlaceholder(view: { containerEl: HTMLElement }): string | null
|
||||||
return composerElement(view).getAttribute("placeholder");
|
return composerElement(view).getAttribute("placeholder");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function waitForMessagesFrame(messages: HTMLElement): Promise<void> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
messages.win.requestAnimationFrame(() => {
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function mockMessagesLayout(metrics: { scrollHeight: number; clientHeight: number }): () => void {
|
||||||
|
const scrollHeightDescriptor = Object.getOwnPropertyDescriptor(HTMLElement.prototype, "scrollHeight");
|
||||||
|
const clientHeightDescriptor = Object.getOwnPropertyDescriptor(HTMLElement.prototype, "clientHeight");
|
||||||
|
Object.defineProperty(HTMLElement.prototype, "scrollHeight", {
|
||||||
|
configurable: true,
|
||||||
|
get() {
|
||||||
|
return this instanceof HTMLElement && this.classList.contains("codex-panel__messages") ? metrics.scrollHeight : 0;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
Object.defineProperty(HTMLElement.prototype, "clientHeight", {
|
||||||
|
configurable: true,
|
||||||
|
get() {
|
||||||
|
return this instanceof HTMLElement && this.classList.contains("codex-panel__messages") ? metrics.clientHeight : 0;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return () => {
|
||||||
|
restorePrototypeProperty(HTMLElement.prototype, "scrollHeight", scrollHeightDescriptor);
|
||||||
|
restorePrototypeProperty(HTMLElement.prototype, "clientHeight", clientHeightDescriptor);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function restorePrototypeProperty<T extends object>(target: T, property: keyof T, descriptor: PropertyDescriptor | undefined): void {
|
||||||
|
if (descriptor) {
|
||||||
|
Object.defineProperty(target, property, descriptor);
|
||||||
|
} else {
|
||||||
|
Reflect.deleteProperty(target, property);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function chatHost(overrides: Partial<CodexChatHost> = {}): CodexChatHost {
|
function chatHost(overrides: Partial<CodexChatHost> = {}): CodexChatHost {
|
||||||
return {
|
return {
|
||||||
settings: {
|
settings: {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
// @vitest-environment jsdom
|
// @vitest-environment jsdom
|
||||||
|
|
||||||
import { act } from "react";
|
import { act } from "preact/test-utils";
|
||||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
import { buildSelectionUnifiedDiff } from "../../../src/features/selection-rewrite/diff";
|
import { buildSelectionUnifiedDiff } from "../../../src/features/selection-rewrite/diff";
|
||||||
|
|
@ -243,7 +243,7 @@ describe("selection rewrite popover", () => {
|
||||||
const generate = expectPresent(document.querySelector<HTMLButtonElement>('button[aria-label="Generate"]'));
|
const generate = expectPresent(document.querySelector<HTMLButtonElement>('button[aria-label="Generate"]'));
|
||||||
expect(generate.disabled).toBe(true);
|
expect(generate.disabled).toBe(true);
|
||||||
|
|
||||||
act(() => {
|
void act(() => {
|
||||||
setTextareaValue(instruction, "Make it concise.");
|
setTextareaValue(instruction, "Make it concise.");
|
||||||
instruction.dispatchEvent(new Event("input", { bubbles: true }));
|
instruction.dispatchEvent(new Event("input", { bubbles: true }));
|
||||||
});
|
});
|
||||||
|
|
@ -266,7 +266,7 @@ describe("selection rewrite popover", () => {
|
||||||
|
|
||||||
openPopover(popover);
|
openPopover(popover);
|
||||||
const instruction = expectPresent(document.querySelector<HTMLTextAreaElement>(".codex-panel-selection-rewrite__instruction"));
|
const instruction = expectPresent(document.querySelector<HTMLTextAreaElement>(".codex-panel-selection-rewrite__instruction"));
|
||||||
act(() => {
|
void act(() => {
|
||||||
setTextareaValue(instruction, "Make it concise.");
|
setTextareaValue(instruction, "Make it concise.");
|
||||||
instruction.dispatchEvent(new Event("input", { bubbles: true }));
|
instruction.dispatchEvent(new Event("input", { bubbles: true }));
|
||||||
});
|
});
|
||||||
|
|
@ -414,13 +414,13 @@ function rewriteState(overrides: Partial<SelectionRewriteState> = {}): Selection
|
||||||
}
|
}
|
||||||
|
|
||||||
function openPopover(popover: SelectionRewritePopover): void {
|
function openPopover(popover: SelectionRewritePopover): void {
|
||||||
act(() => {
|
void act(() => {
|
||||||
popover.open();
|
popover.open();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function closePopover(popover: SelectionRewritePopover): void {
|
function closePopover(popover: SelectionRewritePopover): void {
|
||||||
act(() => {
|
void act(() => {
|
||||||
popover.close();
|
popover.close();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@
|
||||||
"forceConsistentCasingInFileNames": true,
|
"forceConsistentCasingInFileNames": true,
|
||||||
"isolatedModules": true,
|
"isolatedModules": true,
|
||||||
"jsx": "react-jsx",
|
"jsx": "react-jsx",
|
||||||
|
"jsxImportSource": "preact",
|
||||||
"types": ["node", "vitest"],
|
"types": ["node", "vitest"],
|
||||||
"lib": ["ES2022", "DOM"]
|
"lib": ["ES2022", "DOM"]
|
||||||
},
|
},
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue