diff --git a/esbuild.config.mjs b/esbuild.config.mjs index 20768bae..74e44164 100644 --- a/esbuild.config.mjs +++ b/esbuild.config.mjs @@ -12,6 +12,7 @@ await esbuild.build({ platform: "node", target: "es2022", jsx: "automatic", + jsxImportSource: "preact", outfile: "main.js", sourcemap: false, minify: true, diff --git a/package-lock.json b/package-lock.json index aac80b0a..b5f0bc09 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 554f85f7..a5489aac 100644 --- a/package.json +++ b/package.json @@ -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" } } diff --git a/src/features/chat/chat-message-renderer.ts b/src/features/chat/chat-message-renderer.ts index aa01f760..b05f2d28 100644 --- a/src/features/chat/chat-message-renderer.ts +++ b/src/features/chat/chat-message-renderer.ts @@ -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"; diff --git a/src/features/chat/controllers/requests/pending-request-controller.ts b/src/features/chat/controllers/requests/pending-request-controller.ts index 79d608bf..4aa27b3b 100644 --- a/src/features/chat/controllers/requests/pending-request-controller.ts +++ b/src/features/chat/controllers/requests/pending-request-controller.ts @@ -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"; diff --git a/src/features/chat/controllers/thread/thread-resume-controller.ts b/src/features/chat/controllers/thread/thread-resume-controller.ts index 45a9195d..bffdc976 100644 --- a/src/features/chat/controllers/thread/thread-resume-controller.ts +++ b/src/features/chat/controllers/thread/thread-resume-controller.ts @@ -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(); } diff --git a/src/features/chat/ui/composer.tsx b/src/features/chat/ui/composer.tsx index 729907a7..222eadc5 100644 --- a/src/features/chat/ui/composer.tsx +++ b/src/features/chat/ui/composer.tsx @@ -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); }} />
@@ -152,7 +157,7 @@ function ComposerIconButton({ icon: string; label: string; className: string; -} & Omit, "className" | "type">): ReactNode { +} & Omit): ReactNode { return ( { + 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); }} /> diff --git a/src/features/chat/ui/scroll.ts b/src/features/chat/ui/scroll.ts index 124ae63a..76a3d442 100644 --- a/src/features/chat/ui/scroll.ts +++ b/src/features/chat/ui/scroll.ts @@ -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; diff --git a/src/features/chat/ui/shell.tsx b/src/features/chat/ui/shell.tsx index 980c3877..3e157185 100644 --- a/src/features/chat/ui/shell.tsx +++ b/src/features/chat/ui/shell.tsx @@ -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(); + 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 ( <> -
-
+
+
stateStore.subscribe(listener), () => slot.snapshot(stateStore.getState()), - () => slot.snapshot(stateStore.getState()), ); const ref = useRef(null); const renderGeneration = useRef(0); @@ -126,7 +132,15 @@ function ChatPanelSlot({ }, [name, onSlotReady, renderKey, renderTargetClassName, slot]); return ( -
+
{ + ref.current = element; + if (!element) return; + const renderTarget = renderTargetClassName ? element.querySelector(`:scope > .${renderTargetClassName}`) : element; + if (renderTarget) onSlotReady(name, renderTarget); + }} + className={className} + > {renderTargetClassName ?
: null}
); diff --git a/src/features/chat/ui/tool-result.tsx b/src/features/chat/ui/tool-result.tsx index a4ec70a2..0260512c 100644 --- a/src/features/chat/ui/tool-result.tsx +++ b/src/features/chat/ui/tool-result.tsx @@ -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"; diff --git a/src/features/chat/ui/toolbar.tsx b/src/features/chat/ui/toolbar.tsx index 406aaeb2..e0380c91 100644 --- a/src/features/chat/ui/toolbar.tsx +++ b/src/features/chat/ui/toolbar.tsx @@ -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, "className" | "type">): ReactNode { +} & Omit): ReactNode { return ( { 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) => { + const onKeyDown = (event: TargetedKeyboardEvent) => { if (disabled || (event.key !== "Enter" && event.key !== " ")) return; event.preventDefault(); onClick?.(); diff --git a/src/features/chat/ui/turn-diff.tsx b/src/features/chat/ui/turn-diff.tsx index 3b394226..acb131ab 100644 --- a/src/features/chat/ui/turn-diff.tsx +++ b/src/features/chat/ui/turn-diff.tsx @@ -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"; diff --git a/src/features/chat/ui/work-items.tsx b/src/features/chat/ui/work-items.tsx index 13142ca3..0cbe8086 100644 --- a/src/features/chat/ui/work-items.tsx +++ b/src/features/chat/ui/work-items.tsx @@ -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"; diff --git a/src/features/selection-rewrite/popover.tsx b/src/features/selection-rewrite/popover.tsx index 7a3bac5a..8bae6a91 100644 --- a/src/features/selection-rewrite/popover.tsx +++ b/src/features/selection-rewrite/popover.tsx @@ -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) => void; + onApplyKeyDown: (event: TargetedKeyboardEvent) => void; onCancel: () => void; onGenerate: () => void; onInstructionInput: (value: string) => void; - onInstructionKeyDown: (event: KeyboardEvent) => void; + onInstructionKeyDown: (event: TargetedKeyboardEvent) => void; status: SelectionRewritePopoverStatusState; streamPreview: string; } diff --git a/src/features/threads-view/renderer.tsx b/src/features/threads-view/renderer.tsx index 63908ebe..46d1d863 100644 --- a/src/features/threads-view/renderer.tsx +++ b/src/features/threads-view/renderer.tsx @@ -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) => { + const openFromKeyboard = (event: TargetedKeyboardEvent) => { 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, "className" | "type">): ReactNode { +} & Omit): ReactNode { return (