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",
|
||||
target: "es2022",
|
||||
jsx: "automatic",
|
||||
jsxImportSource: "preact",
|
||||
outfile: "main.js",
|
||||
sourcemap: false,
|
||||
minify: true,
|
||||
|
|
|
|||
86
package-lock.json
generated
86
package-lock.json
generated
|
|
@ -9,14 +9,11 @@
|
|||
"version": "2.4.0",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1"
|
||||
"preact": "^10.29.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^10.0.1",
|
||||
"@types/node": "^25.6.0",
|
||||
"@types/react": "^18.3.29",
|
||||
"@types/react-dom": "^18.3.7",
|
||||
"esbuild": "^0.28.0",
|
||||
"eslint": "^10.3.0",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
|
|
@ -2023,34 +2020,6 @@
|
|||
"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": {
|
||||
"version": "0.23.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/tern/-/tern-0.23.9.tgz",
|
||||
|
|
@ -3042,13 +3011,6 @@
|
|||
"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": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/data-urls/-/data-urls-7.0.0.tgz",
|
||||
|
|
@ -5678,6 +5640,7 @@
|
|||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/js-yaml": {
|
||||
|
|
@ -6211,6 +6174,7 @@
|
|||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
|
||||
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"js-tokens": "^3.0.0 || ^4.0.0"
|
||||
|
|
@ -6845,6 +6809,16 @@
|
|||
"dev": true,
|
||||
"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": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
|
||||
|
|
@ -6934,31 +6908,6 @@
|
|||
],
|
||||
"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": {
|
||||
"version": "16.13.1",
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
||||
|
|
@ -7252,15 +7201,6 @@
|
|||
"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": {
|
||||
"version": "7.8.0",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz",
|
||||
|
|
|
|||
|
|
@ -40,8 +40,6 @@
|
|||
"devDependencies": {
|
||||
"@eslint/js": "^10.0.1",
|
||||
"@types/node": "^25.6.0",
|
||||
"@types/react": "^18.3.29",
|
||||
"@types/react-dom": "^18.3.7",
|
||||
"esbuild": "^0.28.0",
|
||||
"eslint": "^10.3.0",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
|
|
@ -57,7 +55,6 @@
|
|||
"vitest": "^4.1.7"
|
||||
},
|
||||
"dependencies": {
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1"
|
||||
"preact": "^10.29.2"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import type { App, Component } from "obsidian";
|
||||
import type { ReactNode } from "react";
|
||||
import type { ReactNode } from "preact/compat";
|
||||
|
||||
import type { DisplayItem } from "./display/types";
|
||||
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 { ChatAction, ChatState, ChatStateStore } from "../../chat-state";
|
||||
|
|
|
|||
|
|
@ -70,6 +70,7 @@ export class ThreadResumeController {
|
|||
this.host.clearDeferredRestoredThreadHydration();
|
||||
this.host.resetThreadTurnPresence(false);
|
||||
this.host.notifyActiveThreadIdentityChanged();
|
||||
this.host.forceMessagesToBottom();
|
||||
this.host.render();
|
||||
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 { IconButton } from "../../../shared/ui/react-components";
|
||||
|
|
@ -21,6 +22,10 @@ export interface ComposerCallbacks {
|
|||
onSuggestionInsert: (suggestion: ComposerSuggestion) => void;
|
||||
}
|
||||
|
||||
type ButtonProps = ButtonHTMLAttributes & {
|
||||
disabled?: boolean | undefined;
|
||||
};
|
||||
|
||||
export function renderComposerShell(
|
||||
parent: HTMLElement,
|
||||
viewId: string,
|
||||
|
|
@ -102,7 +107,7 @@ function ComposerShell({
|
|||
onClick={callbacks.onUpdateSuggestions}
|
||||
onSelect={callbacks.onUpdateSuggestions}
|
||||
onKeyDown={(event) => {
|
||||
callbacks.onKeydown(event.nativeEvent);
|
||||
callbacks.onKeydown(event);
|
||||
}}
|
||||
/>
|
||||
<div className="codex-panel-ui__action-stack codex-panel__composer-actions">
|
||||
|
|
@ -152,7 +157,7 @@ function ComposerIconButton({
|
|||
icon: string;
|
||||
label: string;
|
||||
className: string;
|
||||
} & Omit<ButtonHTMLAttributes<HTMLButtonElement>, "className" | "type">): ReactNode {
|
||||
} & Omit<ButtonProps, "className" | "type">): ReactNode {
|
||||
return (
|
||||
<IconButton
|
||||
{...props}
|
||||
|
|
@ -237,7 +242,7 @@ function ComposerSuggestions({
|
|||
return (
|
||||
<div
|
||||
key={optionId}
|
||||
ref={selected ? selectedRef : undefined}
|
||||
ref={selected ? selectedRef : null}
|
||||
className={`suggestion-item codex-panel__composer-suggestion ${selected ? "is-selected" : ""}`}
|
||||
id={optionId}
|
||||
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 { 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 {
|
||||
approvalActionOptions,
|
||||
|
|
@ -312,6 +312,9 @@ function OtherUserInputOption({
|
|||
name={groupName}
|
||||
value="__other__"
|
||||
checked={otherSelected}
|
||||
onClick={(event) => {
|
||||
if (event.currentTarget.checked) selectOther();
|
||||
}}
|
||||
onChange={(event) => {
|
||||
if (event.currentTarget.checked) selectOther();
|
||||
}}
|
||||
|
|
@ -335,7 +338,7 @@ function OtherUserInputOption({
|
|||
}}
|
||||
onChange={(event) => {
|
||||
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);
|
||||
}}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -105,6 +105,9 @@ export class MessageScrollController {
|
|||
|
||||
this.observeMessageBlocks(container);
|
||||
this.cancelRenderFrame();
|
||||
if (plan.shouldScrollToBottom && this.options.messagesPinnedToBottom()) {
|
||||
this.pinToBottom(container);
|
||||
}
|
||||
this.renderFrame = container.win.requestAnimationFrame(() => {
|
||||
this.renderFrame = null;
|
||||
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 type { ChatState, ChatStateStore } from "../chat-state";
|
||||
|
|
@ -19,8 +19,11 @@ interface ChatPanelShellSlots {
|
|||
composer?: HTMLElement;
|
||||
}
|
||||
|
||||
const shellSlotsByContainer = new WeakMap<HTMLElement, ChatPanelShellSlots>();
|
||||
|
||||
export function renderChatPanelShell(container: HTMLElement, props: ChatPanelShellProps): void {
|
||||
const mountedSlots: ChatPanelShellSlots = {};
|
||||
const mountedSlots = shellSlotsByContainer.get(container) ?? {};
|
||||
shellSlotsByContainer.set(container, mountedSlots);
|
||||
container.addClass("codex-panel");
|
||||
renderReactRoot(
|
||||
container,
|
||||
|
|
@ -36,6 +39,7 @@ export function renderChatPanelShell(container: HTMLElement, props: ChatPanelShe
|
|||
|
||||
export function unmountChatPanelShell(container: HTMLElement | null): void {
|
||||
if (!container) return;
|
||||
shellSlotsByContainer.delete(container);
|
||||
unmountReactRoot(container);
|
||||
}
|
||||
|
||||
|
|
@ -50,6 +54,7 @@ function ChatPanelShell({
|
|||
return (
|
||||
<>
|
||||
<ChatPanelSlot
|
||||
key="toolbar"
|
||||
name="toolbar"
|
||||
className="codex-panel__toolbar"
|
||||
stateStore={stateStore}
|
||||
|
|
@ -57,9 +62,10 @@ function ChatPanelShell({
|
|||
slot={toolbar}
|
||||
onSlotReady={onSlotReady}
|
||||
/>
|
||||
<div className="codex-panel__body">
|
||||
<div className="codex-panel__slot codex-panel__slot--config" />
|
||||
<div key="body" className="codex-panel__body">
|
||||
<div key="config" className="codex-panel__slot codex-panel__slot--config" />
|
||||
<ChatPanelSlot
|
||||
key="messages"
|
||||
name="messages"
|
||||
className="codex-panel__slot codex-panel__slot--messages"
|
||||
renderTargetClassName="codex-panel__messages"
|
||||
|
|
@ -69,6 +75,7 @@ function ChatPanelShell({
|
|||
onSlotReady={onSlotReady}
|
||||
/>
|
||||
<ChatPanelSlot
|
||||
key="composer"
|
||||
name="composer"
|
||||
className="codex-panel__slot codex-panel__slot--composer"
|
||||
stateStore={stateStore}
|
||||
|
|
@ -106,7 +113,6 @@ function ChatPanelSlot({
|
|||
const snapshot = useSyncExternalStore(
|
||||
(listener) => stateStore.subscribe(listener),
|
||||
() => slot.snapshot(stateStore.getState()),
|
||||
() => slot.snapshot(stateStore.getState()),
|
||||
);
|
||||
const ref = useRef<HTMLDivElement | null>(null);
|
||||
const renderGeneration = useRef(0);
|
||||
|
|
@ -126,7 +132,15 @@ function ChatPanelSlot({
|
|||
}, [name, onSlotReady, renderKey, renderTargetClassName, slot]);
|
||||
|
||||
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}
|
||||
</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 { 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 { IconButton, ObsidianIcon } from "../../../shared/ui/react-components";
|
||||
import { renderReactRoot } from "../../../shared/ui/react-root";
|
||||
import type { ToolbarDiagnosticSection, ToolbarThreadRow, ToolbarViewModel } from "../toolbar-model";
|
||||
|
||||
type ButtonProps = ButtonHTMLAttributes & {
|
||||
disabled?: boolean | undefined;
|
||||
};
|
||||
|
||||
export interface ToolbarActions {
|
||||
toggleHistory: () => void;
|
||||
toggleAutoReview: () => void;
|
||||
|
|
@ -66,7 +71,7 @@ function ToolbarIconButton({
|
|||
icon: string;
|
||||
label: string;
|
||||
className?: string;
|
||||
} & Omit<ButtonHTMLAttributes<HTMLButtonElement>, "className" | "type">): ReactNode {
|
||||
} & Omit<ButtonProps, "className" | "type">): ReactNode {
|
||||
return (
|
||||
<IconButton
|
||||
{...props}
|
||||
|
|
@ -444,7 +449,7 @@ function ThreadRenameRow({ thread, actions }: { thread: ToolbarThreadRow; action
|
|||
onKeyDown={(event) => {
|
||||
if (event.key === "Enter") {
|
||||
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;
|
||||
}
|
||||
if (event.key === "Escape") {
|
||||
|
|
@ -498,7 +503,7 @@ function ToolbarPanelItem({
|
|||
renderContent?: () => ReactNode;
|
||||
onClick?: () => void;
|
||||
}): ReactNode {
|
||||
const onKeyDown = (event: KeyboardEvent<HTMLElement>) => {
|
||||
const onKeyDown = (event: TargetedKeyboardEvent<HTMLElement>) => {
|
||||
if (disabled || (event.key !== "Enter" && event.key !== " ")) return;
|
||||
event.preventDefault();
|
||||
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 { 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 { executionState } from "../display/state";
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
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 { displayDiffLines } from "../../shared/diff/unified";
|
||||
|
|
@ -16,6 +17,7 @@ import {
|
|||
type SelectionRewriteRuntimeSettings,
|
||||
type SelectionRewriteState,
|
||||
} from "./model";
|
||||
|
||||
import { SelectionRewriteOutputError } from "./output";
|
||||
import { positionSelectionRewritePopover } from "./position";
|
||||
import { buildSelectionRewritePrompt } from "./prompt";
|
||||
|
|
@ -285,11 +287,7 @@ export class SelectionRewritePopover {
|
|||
}}
|
||||
onInstructionKeyDown={(event) => {
|
||||
const hasReplacement = this.options.state.replacementText !== null;
|
||||
if (
|
||||
!(hasReplacement
|
||||
? isSelectionRewriteActionKey(event.nativeEvent)
|
||||
: isComposerSendKey(event.nativeEvent, this.options.sendShortcut))
|
||||
) {
|
||||
if (!(hasReplacement ? isSelectionRewriteActionKey(event) : isComposerSendKey(event, this.options.sendShortcut))) {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
|
|
@ -297,7 +295,7 @@ export class SelectionRewritePopover {
|
|||
void this.generate();
|
||||
}}
|
||||
onApplyKeyDown={(event) => {
|
||||
if (!isSelectionRewriteActionKey(event.nativeEvent)) return;
|
||||
if (!isSelectionRewriteActionKey(event)) return;
|
||||
event.preventDefault();
|
||||
this.apply();
|
||||
}}
|
||||
|
|
@ -342,11 +340,11 @@ interface SelectionRewritePopoverViewProps {
|
|||
instruction: string;
|
||||
instructionRef: (element: HTMLTextAreaElement | null) => void;
|
||||
onApply: () => void;
|
||||
onApplyKeyDown: (event: KeyboardEvent<HTMLButtonElement>) => void;
|
||||
onApplyKeyDown: (event: TargetedKeyboardEvent<HTMLButtonElement>) => void;
|
||||
onCancel: () => void;
|
||||
onGenerate: () => void;
|
||||
onInstructionInput: (value: string) => void;
|
||||
onInstructionKeyDown: (event: KeyboardEvent<HTMLTextAreaElement>) => void;
|
||||
onInstructionKeyDown: (event: TargetedKeyboardEvent<HTMLTextAreaElement>) => void;
|
||||
status: SelectionRewritePopoverStatusState;
|
||||
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 { renderReactRoot, unmountReactRoot } from "../../shared/ui/react-root";
|
||||
import type { ThreadsRowModel } from "./state";
|
||||
|
||||
type ButtonProps = ButtonHTMLAttributes & {
|
||||
disabled?: boolean | undefined;
|
||||
};
|
||||
|
||||
export interface ThreadsViewModel {
|
||||
status: string | null;
|
||||
loading: boolean;
|
||||
|
|
@ -82,7 +87,7 @@ function ThreadRow({ row, actions }: { row: ThreadsRowModel; actions: ThreadsVie
|
|||
if (row.rename.active || archiveConfirm.active) return;
|
||||
actions.openThread(row.thread.id);
|
||||
};
|
||||
const openFromKeyboard = (event: KeyboardEvent<HTMLDivElement>) => {
|
||||
const openFromKeyboard = (event: TargetedKeyboardEvent<HTMLDivElement>) => {
|
||||
if (row.rename.active || archiveConfirm.active) return;
|
||||
if (event.key !== "Enter" && event.key !== " ") return;
|
||||
event.preventDefault();
|
||||
|
|
@ -204,7 +209,7 @@ function RenameRow({ row, actions }: { row: ThreadsRowModel; actions: ThreadsVie
|
|||
onKeyDown={(event) => {
|
||||
if (event.key === "Enter") {
|
||||
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;
|
||||
}
|
||||
if (event.key === "Escape") {
|
||||
|
|
@ -247,7 +252,7 @@ function ThreadsIconButton({
|
|||
icon: string;
|
||||
label: string;
|
||||
className: string;
|
||||
} & Omit<ButtonHTMLAttributes<HTMLButtonElement>, "className" | "type">): ReactNode {
|
||||
} & Omit<ButtonProps, "className" | "type">): ReactNode {
|
||||
return (
|
||||
<IconButton
|
||||
{...props}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
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 {
|
||||
icon: string;
|
||||
|
|
@ -17,10 +18,12 @@ export function ObsidianIcon({ icon, className }: ObsidianIconProps): ReactNode
|
|||
return <span ref={ref} className={className} aria-hidden="true" />;
|
||||
}
|
||||
|
||||
export interface IconButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
export interface IconButtonProps extends ButtonHTMLAttributes {
|
||||
icon: string;
|
||||
label: string;
|
||||
buttonRef?: Ref<HTMLButtonElement>;
|
||||
disabled?: boolean | undefined;
|
||||
type?: "button" | "submit" | "reset" | undefined;
|
||||
}
|
||||
|
||||
export function IconButton({ icon, label, buttonRef, className, children, ...props }: IconButtonProps): ReactNode {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
import type { ReactNode } from "react";
|
||||
import { flushSync } from "react-dom";
|
||||
import { createRoot, type Root } from "react-dom/client";
|
||||
import type { ReactNode } from "preact/compat";
|
||||
import { flushSync } from "preact/compat";
|
||||
import { createRoot } from "preact/compat/client";
|
||||
|
||||
type Root = ReturnType<typeof createRoot>;
|
||||
|
||||
const roots = new WeakMap<HTMLElement, Root>();
|
||||
const guardedContainers = new WeakSet<HTMLElement>();
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
// @vitest-environment jsdom
|
||||
|
||||
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 { 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");
|
||||
|
||||
const details = expectPresent(result.querySelector<HTMLDetailsElement>("details"));
|
||||
act(() => {
|
||||
void act(() => {
|
||||
details.open = true;
|
||||
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");
|
||||
|
||||
if (details) {
|
||||
act(() => {
|
||||
void act(() => {
|
||||
details.open = true;
|
||||
details.dispatchEvent(new Event("toggle"));
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
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 { 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 {
|
||||
act(action);
|
||||
void act(action);
|
||||
}
|
||||
|
||||
export function renderMessageStreamBlocksInAct(parent: HTMLElement, blocks: MessageStreamBlock[]): void {
|
||||
act(() => {
|
||||
void act(() => {
|
||||
renderMessageStreamBlocks(parent, blocks);
|
||||
});
|
||||
}
|
||||
|
||||
export function renderReactRootInAct(parent: HTMLElement, node: ReactNode): void {
|
||||
act(() => {
|
||||
void act(() => {
|
||||
renderReactRoot(parent, node);
|
||||
});
|
||||
}
|
||||
|
||||
export function unmountReactRootInAct(parent: HTMLElement): void {
|
||||
act(() => {
|
||||
void act(() => {
|
||||
unmountReactRoot(parent);
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
// @vitest-environment jsdom
|
||||
|
||||
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 { 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)");
|
||||
|
||||
const details = expectPresent(group.querySelector<HTMLDetailsElement>(".codex-panel__activity-group"));
|
||||
act(() => {
|
||||
void act(() => {
|
||||
details.open = 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"]);
|
||||
|
||||
const details = expectPresent(block.querySelector<HTMLDetailsElement>("details"));
|
||||
act(() => {
|
||||
void act(() => {
|
||||
details.open = true;
|
||||
details.dispatchEvent(new Event("toggle", { bubbles: false }));
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
// @vitest-environment jsdom
|
||||
|
||||
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 { renderChatPanelShell, unmountChatPanelShell } from "../../../../src/features/chat/ui/shell";
|
||||
|
|
|
|||
|
|
@ -429,8 +429,7 @@ describe("CodexChatView connection lifecycle", () => {
|
|||
const view = await chatView();
|
||||
|
||||
await view.onOpen();
|
||||
const composer = composerElement(view);
|
||||
const focus = vi.spyOn(composer, "focus");
|
||||
const focus = vi.spyOn(HTMLTextAreaElement.prototype, "focus").mockImplementation(() => undefined);
|
||||
|
||||
await view.openThread("thread-1");
|
||||
await view.focusThread("thread-1");
|
||||
|
|
@ -509,20 +508,17 @@ describe("CodexChatView connection lifecycle", () => {
|
|||
const messages = view.containerEl.querySelector<HTMLElement>(".codex-panel__messages");
|
||||
expect(messages).not.toBeNull();
|
||||
if (!messages) return;
|
||||
Object.defineProperty(messages, "scrollHeight", { value: 1000, configurable: true });
|
||||
Object.defineProperty(messages, "clientHeight", { value: 100, configurable: true });
|
||||
const restoreMessagesLayout = mockMessagesLayout({ scrollHeight: 1000, clientHeight: 100 });
|
||||
messages.scrollTop = 0;
|
||||
|
||||
view.setComposerText("/resume thread-1");
|
||||
await (view as unknown as { submitComposerAction: () => Promise<void> }).submitComposerAction();
|
||||
await new Promise<void>((resolve) => {
|
||||
messages.win.requestAnimationFrame(() => {
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
await waitForMessagesFrame(messages);
|
||||
await waitForMessagesFrame(messages);
|
||||
|
||||
const renderedMessages = view.containerEl.querySelector<HTMLElement>(".codex-panel__messages");
|
||||
expect(renderedMessages?.scrollTop).toBe(1000);
|
||||
restoreMessagesLayout();
|
||||
});
|
||||
|
||||
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");
|
||||
expect(messages).not.toBeNull();
|
||||
if (!messages) return;
|
||||
Object.defineProperty(messages, "scrollHeight", { value: 1000, configurable: true });
|
||||
Object.defineProperty(messages, "clientHeight", { value: 100, configurable: true });
|
||||
const restoreMessagesLayout = mockMessagesLayout({ scrollHeight: 1000, clientHeight: 100 });
|
||||
messages.scrollTop = 0;
|
||||
|
||||
await view.openThread("thread-1");
|
||||
await new Promise<void>((resolve) => {
|
||||
messages.win.requestAnimationFrame(() => {
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
await waitForMessagesFrame(messages);
|
||||
await waitForMessagesFrame(messages);
|
||||
|
||||
const renderedMessages = view.containerEl.querySelector<HTMLElement>(".codex-panel__messages");
|
||||
expect(renderedMessages?.scrollTop).toBe(1000);
|
||||
restoreMessagesLayout();
|
||||
});
|
||||
|
||||
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");
|
||||
}
|
||||
|
||||
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 {
|
||||
return {
|
||||
settings: {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
// @vitest-environment jsdom
|
||||
|
||||
import { act } from "react";
|
||||
import { act } from "preact/test-utils";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
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"]'));
|
||||
expect(generate.disabled).toBe(true);
|
||||
|
||||
act(() => {
|
||||
void act(() => {
|
||||
setTextareaValue(instruction, "Make it concise.");
|
||||
instruction.dispatchEvent(new Event("input", { bubbles: true }));
|
||||
});
|
||||
|
|
@ -266,7 +266,7 @@ describe("selection rewrite popover", () => {
|
|||
|
||||
openPopover(popover);
|
||||
const instruction = expectPresent(document.querySelector<HTMLTextAreaElement>(".codex-panel-selection-rewrite__instruction"));
|
||||
act(() => {
|
||||
void act(() => {
|
||||
setTextareaValue(instruction, "Make it concise.");
|
||||
instruction.dispatchEvent(new Event("input", { bubbles: true }));
|
||||
});
|
||||
|
|
@ -414,13 +414,13 @@ function rewriteState(overrides: Partial<SelectionRewriteState> = {}): Selection
|
|||
}
|
||||
|
||||
function openPopover(popover: SelectionRewritePopover): void {
|
||||
act(() => {
|
||||
void act(() => {
|
||||
popover.open();
|
||||
});
|
||||
}
|
||||
|
||||
function closePopover(popover: SelectionRewritePopover): void {
|
||||
act(() => {
|
||||
void act(() => {
|
||||
popover.close();
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@
|
|||
"forceConsistentCasingInFileNames": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "react-jsx",
|
||||
"jsxImportSource": "preact",
|
||||
"types": ["node", "vitest"],
|
||||
"lib": ["ES2022", "DOM"]
|
||||
},
|
||||
|
|
|
|||
Loading…
Reference in a new issue