diff --git a/biome.jsonc b/biome.jsonc index b817b3c1..7db55569 100644 --- a/biome.jsonc +++ b/biome.jsonc @@ -17,6 +17,7 @@ "./scripts/lint/no-chat-domain-outer-layer-imports.grit", "./scripts/lint/no-chat-signal-imports.grit", "./scripts/lint/no-chat-state-escape-hatches.grit", + "./scripts/lint/no-implicit-dom-bridges.grit", "./scripts/lint/no-pure-chat-state-side-effects.grit", "./scripts/lint/no-ui-root-imports.grit", diff --git a/docs/design.md b/docs/design.md index 3057b4b3..610e37d3 100644 --- a/docs/design.md +++ b/docs/design.md @@ -48,7 +48,7 @@ Chat-visible state belongs in `ChatStateStore` and named reducer actions. Signal Preact Signals are a shell-local projection adapter, not a second state system. Surface projections should read narrow shell-state contracts instead of making components or presenters depend on broad reducer slices. Domain, application, host, presentation, and component modules should keep using pure selectors, reducer actions, and explicit props rather than importing signals directly. -Imperative DOM bridges are allowed when an external API or measurement problem requires an `HTMLElement`. They should not become a second UI composition system inside Preact-owned surfaces. +Imperative DOM bridges are allowed when an external API or measurement problem requires an `HTMLElement`. They should be named as `.dom`, `.obsidian`, or `.measure` files and should not become a second UI composition system inside Preact-owned surfaces. ## Interaction Principles diff --git a/docs/development.md b/docs/development.md index f5c6b419..9825220f 100644 --- a/docs/development.md +++ b/docs/development.md @@ -14,7 +14,7 @@ Use this as the normal edit loop. `npm run format` applies Biome formatting and Use focused scripts for tight loops: `npm run typecheck`, `npm run test`, `npm run build`, or the targeted `npm run check:*` scripts. CI and release preflight run the same `npm run check` command as local development. -Biome owns formatting, import organization, general JavaScript/TypeScript/JSON/CSS linting, GritQL source-shape plugins, GritQL import-boundary restrictions, and GritQL CSS source policy. Biome warnings fail `npm run check`; info diagnostics stay advisory, including accessibility while Obsidian-specific UI semantics are reviewed rule by rule. The CSS usage script still checks dead authored classes. TypeScript owns strict type checking, unused locals and parameters, implicit return coverage, switch fallthrough prevention, and deep-readonly `ChatState` snapshots. ESLint remains for typed strict TypeScript rules that are not compiler options, Obsidian plugin policy, and the imperative-DOM responsibility-boundary rule that needs TypeScript type information. The typed strict preset is used with `@typescript-eslint/require-await` disabled because Obsidian and panel-owned async-shaped boundaries sometimes return already-complete work. +Biome owns formatting, import organization, general JavaScript/TypeScript/JSON/CSS linting, GritQL source-shape plugins, GritQL import-boundary restrictions, imperative-DOM bridge naming, and GritQL CSS source policy. Biome warnings fail `npm run check`; info diagnostics stay advisory, including accessibility while Obsidian-specific UI semantics are reviewed rule by rule. The CSS usage script still checks dead authored classes. TypeScript owns strict type checking, unused locals and parameters, implicit return coverage, switch fallthrough prevention, and deep-readonly `ChatState` snapshots. ESLint remains for typed strict TypeScript rules that are not compiler options and Obsidian plugin policy. The typed strict preset is used with `@typescript-eslint/require-await` disabled because Obsidian and panel-owned async-shaped boundaries sometimes return already-complete work. ## Generated and Loaded Files @@ -61,7 +61,7 @@ Generated app-server types should stay behind `src/app-server/` or chat-local ap Chat panel-visible state belongs in `ChatStateStore` and should flow through named reducer actions and the shell-state adapter. Use Preact Signals only in `src/features/chat/panel/shell-state.tsx`; lint enforces this boundary. When a surface needs fewer dependencies, add or reuse a named shell-state projection instead of importing `@preact/signals` elsewhere. -Use imperative DOM writes only for explicit bridge modules, Obsidian-owned API boundaries, or rendering and measurement code that cannot be expressed cleanly as Preact components. +Use imperative DOM writes only for explicit bridge modules, Obsidian-owned API boundaries, or rendering and measurement code that cannot be expressed cleanly as Preact components. Name those files with a `.dom`, `.obsidian`, or `.measure` suffix so Biome can enforce the boundary without file-specific allowlists. ## CSS Rules diff --git a/eslint.config.mjs b/eslint.config.mjs index 5f9b6411..73d28fee 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -1,91 +1,9 @@ import { defineConfig } from "eslint/config"; import obsidianmd from "eslint-plugin-obsidianmd"; -import ts from "typescript"; import tseslint from "typescript-eslint"; const sourceTypeScriptFiles = ["src/**/*.{ts,tsx}"]; const strictTypeCheckedTypeScriptRules = Object.assign({}, ...tseslint.configs.strictTypeChecked.map((config) => config.rules ?? {})); -// This local rule needs TypeScript type information. Keep it validated by -// eslint over real source files instead of synthetic Vitest fixtures, which -// would start the TypeScript project service during the test suite. -const chatExternalDomBridgeFiles = [ - "src/features/chat/ui/message-stream/markdown-renderer.ts", - "src/features/chat/ui/message-stream/stream-markdown-renderer.ts", - "src/features/chat/ui/message-stream/flow-scroll.ts", -]; -const chatPreactDomBridgeFiles = [ - "src/features/chat/ui/message-stream/text-content.tsx", - "src/features/chat/ui/message-stream/detail.tsx", - "src/features/chat/ui/message-stream/viewport.tsx", - "src/features/chat/ui/composer-dom.ts", - "src/features/chat/panel/shell.tsx", - "src/features/chat/ui/turn-diff/render.tsx", -]; -const chatImperativeDomBridgeFiles = [...chatExternalDomBridgeFiles, ...chatPreactDomBridgeFiles]; -const nonChatImperativeDomBridgeFiles = [ - "src/features/selection-rewrite/popover.tsx", - "src/features/thread-picker/modal.ts", - "src/features/threads-view/renderer.tsx", - "src/settings/tab.tsx", - "src/shared/diff/render.ts", - "src/shared/ui/components.tsx", - "src/shared/ui/textarea-autogrow.ts", - "src/shared/ui/textarea-caret.ts", - "src/shared/ui/ui-root.tsx", -]; -const imperativeDomWriteMethods = new Set([ - "addClass", - "addClasses", - "append", - "appendChild", - "after", - "before", - "createDiv", - "createEl", - "createSpan", - "empty", - "hide", - "insertAdjacentElement", - "insertAdjacentHTML", - "insertAdjacentText", - "insertBefore", - "prepend", - "removeClass", - "removeClasses", - "remove", - "removeChild", - "replaceChildren", - "replaceWith", - "setCssProps", - "setCssStyles", - "setText", - "show", - "setAttr", - "toggleClass", -]); -const imperativeDomEventMethods = new Set(["addEventListener", "removeEventListener"]); -const imperativeDomAssignmentProperties = new Set([ - "checked", - "innerHTML", - "onblur", - "onchange", - "onclick", - "ondblclick", - "onfocus", - "oninput", - "onkeydown", - "onkeyup", - "onmousedown", - "onmousemove", - "onmouseup", - "onpointerdown", - "onpointerup", - "onscroll", - "onselect", - "outerHTML", - "textContent", - "value", -]); export default defineConfig([ { @@ -132,7 +50,6 @@ export default defineConfig([ { files: sourceTypeScriptFiles, plugins: { - "codex-panel": codexPanelEslintPlugin(), obsidianmd, }, rules: { @@ -145,20 +62,6 @@ export default defineConfig([ ], }, }, - { - files: sourceTypeScriptFiles, - ignores: ["src/features/chat/**/*.{ts,tsx}", ...nonChatImperativeDomBridgeFiles], - rules: { - "codex-panel/no-imperative-dom": "error", - }, - }, - { - files: ["src/features/chat/**/*.{ts,tsx}"], - ignores: chatImperativeDomBridgeFiles, - rules: { - "codex-panel/no-imperative-dom": "error", - }, - }, ]); function obsidianRecommendedConfig(config) { @@ -172,117 +75,3 @@ function obsidianRecommendedConfig(config) { if (config.ignores) obsidianConfig.ignores = config.ignores; return obsidianConfig; } - -function codexPanelEslintPlugin() { - return { - rules: { - "no-imperative-dom": imperativeDomRule(), - }, - }; -} - -function imperativeDomRule() { - return { - meta: { - type: "problem", - docs: { - description: "Disallow imperative DOM writes and event wiring outside explicit bridge files.", - }, - messages: { - event: "Keep imperative DOM event wiring in an explicit bridge module or Obsidian-owned UI boundary.", - write: "Keep imperative DOM writes in an explicit bridge module or Obsidian-owned UI boundary.", - }, - schema: [], - }, - create(context) { - const typed = typedContext(context); - const isDomTarget = (node) => typeIncludesDom(typed.typeAt(node), typed.typeChecker()); - - return { - AssignmentExpression(node) { - if (!isMemberExpression(node.left)) return; - const property = staticPropertyName(node.left.property); - if (!property || !imperativeDomAssignmentProperties.has(property)) return; - if (isDomTarget(node.left.object)) context.report({ node: node.left, messageId: "write" }); - }, - CallExpression(node) { - if (!isMemberExpression(node.callee)) return; - const method = staticPropertyName(node.callee.property); - if (!method) return; - if (imperativeDomWriteMethods.has(method) && isDomTarget(node.callee.object)) { - context.report({ node: node.callee, messageId: "write" }); - return; - } - if (imperativeDomEventMethods.has(method) && isDomTarget(node.callee.object)) { - context.report({ node: node.callee, messageId: "event" }); - } - }, - }; - }, - }; -} - -function typedContext(context) { - let parserServices = null; - let checker = null; - - const services = () => { - if (!parserServices) { - parserServices = parserServicesFromContext(context); - checker = parserServices.program.getTypeChecker(); - } - return parserServices; - }; - const typeChecker = () => { - services(); - return checker; - }; - - return { - typeChecker, - typeAt(node) { - const tsNode = services().esTreeNodeToTSNodeMap.get(node); - return typeChecker().getTypeAtLocation(tsNode); - }, - }; -} - -function isMemberExpression(node) { - return node?.type === "MemberExpression"; -} - -function staticPropertyName(node) { - return node?.type === "Identifier" ? node.name : node?.type === "Literal" && typeof node.value === "string" ? node.value : null; -} - -function parserServicesFromContext(context) { - const services = context.sourceCode.parserServices; - if (!services?.program || !services.esTreeNodeToTSNodeMap) { - throw new Error("codex-panel typed ESLint rules require TypeScript parser services."); - } - return services; -} - -function typeIncludesDom(type, checker, seen = new Set()) { - if (!type || seen.has(type.id)) return false; - seen.add(type.id); - - if (type.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown | ts.TypeFlags.Never)) return false; - if (type.isUnionOrIntersection()) return type.types.some((item) => typeIncludesDom(item, checker, seen)); - - const typeName = checker.typeToString(type); - if (domTypeName(typeName)) return true; - - const symbolName = type.getSymbol()?.getName() ?? type.aliasSymbol?.getName() ?? ""; - if (domTypeName(symbolName)) return true; - - const apparent = checker.getApparentType(type); - if (apparent !== type && typeIncludesDom(apparent, checker, seen)) return true; - - const bases = typeof type.getBaseTypes === "function" ? (type.getBaseTypes() ?? []) : []; - return bases.some((base) => typeIncludesDom(base, checker, seen)); -} - -function domTypeName(name) { - return /\b(?:Document|Element|HTML[A-Za-z]*Element|HTMLElement|Node|SVG[A-Za-z]*Element|SVGElement|Window)\b/.test(name); -} diff --git a/scripts/lint/no-implicit-dom-bridges.grit b/scripts/lint/no-implicit-dom-bridges.grit new file mode 100644 index 00000000..9f50cf8d --- /dev/null +++ b/scripts/lint/no-implicit-dom-bridges.grit @@ -0,0 +1,22 @@ +language js + +or { + `$target.createElement($...)` as $stmt, + `$target.createTextNode($...)` as $stmt, + `$target.createEl($...)` as $stmt, + `$target.createDiv($...)` as $stmt, + `$target.createSpan($...)` as $stmt, + `$target.empty()` as $stmt, + `$target.appendChild($...)` as $stmt, + `$target.replaceChildren($...)` as $stmt, + `$target.insertAdjacentHTML($...)` as $stmt, + `$target.addEventListener($...)` as $stmt, + `$target.removeEventListener($...)` as $stmt +} where { + $filename <: r".*/src/.*\.tsx?$", + not { $filename <: r".*\.dom\.tsx?$" }, + not { $filename <: r".*\.obsidian\.tsx?$" }, + not { $filename <: r".*\.measure\.tsx?$" }, + not { $target <: `signal` }, + register_diagnostic(span=$stmt, message="Keep imperative DOM writes and event wiring in files named with a .dom, .obsidian, or .measure suffix.", severity="error") +} diff --git a/scripts/lint/no-ui-root-imports.grit b/scripts/lint/no-ui-root-imports.grit index b44e09cf..6026284b 100644 --- a/scripts/lint/no-ui-root-imports.grit +++ b/scripts/lint/no-ui-root-imports.grit @@ -2,12 +2,9 @@ language js JsImport() as $stmt where { $filename <: r".*/src/.*", - $stmt <: contains `$source` where { $source <: r"^[\"'].*shared/ui/ui-root[\"']$" }, - not { $filename <: r".*/src/features/chat/panel/shell\.tsx$" }, - not { $filename <: r".*/src/features/chat/ui/turn-diff/render\.tsx$" }, - not { $filename <: r".*/src/features/chat/ui/turn-diff/view\.ts$" }, - not { $filename <: r".*/src/features/selection-rewrite/popover\.tsx$" }, - not { $filename <: r".*/src/features/threads-view/renderer\.tsx$" }, - not { $filename <: r".*/src/settings/tab\.tsx$" }, + $stmt <: contains `$source` where { $source <: r"^[\"'].*shared/ui/ui-root\.dom[\"']$" }, + not { $filename <: r".*\.dom\.tsx?$" }, + not { $filename <: r".*\.obsidian\.tsx?$" }, + not { $filename <: r".*\.measure\.tsx?$" }, register_diagnostic(span=$stmt, message="Import the Preact root adapter only from explicit root bridge files.", severity="error") } diff --git a/src/features/chat/host/session.ts b/src/features/chat/host/session.ts index d2845203..4122507b 100644 --- a/src/features/chat/host/session.ts +++ b/src/features/chat/host/session.ts @@ -6,7 +6,7 @@ import { ConnectionWorkTracker } from "../../../shared/lifecycle/connection-work import { ChatResumeWorkTracker, type ChatViewDeferredTasks, type RestoredThreadPlaceholderState } from "../application/lifecycle"; import type { ChatState } from "../application/state/root-reducer"; import { type ChatStateStore, createChatStateStore } from "../application/state/store"; -import { renderChatPanelShell, unmountChatPanelShell } from "../panel/shell"; +import { renderChatPanelShell, unmountChatPanelShell } from "../panel/shell.dom"; import { type ChatPanelSnapshot, openPanelTurnLifecycle, parseRestoredThreadState } from "../panel/snapshot"; import { type ChatMessageScrollController, createChatMessageScrollController } from "../panel/surface/message-stream-scroll"; import { createChatViewDeferredTasks } from "./lifecycle"; diff --git a/src/features/chat/panel/composer-controller.ts b/src/features/chat/panel/composer-controller.ts index bb2f8c8d..55f59749 100644 --- a/src/features/chat/panel/composer-controller.ts +++ b/src/features/chat/panel/composer-controller.ts @@ -1,6 +1,6 @@ import type { CodexInput } from "../../../domain/chat/input"; import { isComposerSendKey, type SendShortcut } from "../../../shared/ui/keyboard"; -import { textareaCursorAtVisualBoundary } from "../../../shared/ui/textarea-caret"; +import { textareaCursorAtVisualBoundary } from "../../../shared/ui/textarea-caret.measure"; import { type ComposerBoundaryScrollAction, composerBoundaryScrollDirection } from "../application/composer/boundary-scroll"; import type { NoteCandidateProvider } from "../application/composer/note-context"; import { diff --git a/src/features/chat/panel/shell.tsx b/src/features/chat/panel/shell.dom.tsx similarity index 99% rename from src/features/chat/panel/shell.tsx rename to src/features/chat/panel/shell.dom.tsx index 0b245dc7..7596b879 100644 --- a/src/features/chat/panel/shell.tsx +++ b/src/features/chat/panel/shell.dom.tsx @@ -1,5 +1,5 @@ import type { ComponentChild as UiNode } from "preact"; -import { renderUiRoot, unmountUiRoot } from "../../../shared/ui/ui-root"; +import { renderUiRoot, unmountUiRoot } from "../../../shared/ui/ui-root.dom"; import type { ChatStateStore } from "../application/state/store"; import type { ToolbarActions } from "../ui/toolbar"; import { type ChatPanelShellState, ChatPanelShellStateContext, createChatPanelShellState, syncChatPanelShellState } from "./shell-state"; diff --git a/src/features/chat/panel/surface/message-stream-presenter.ts b/src/features/chat/panel/surface/message-stream-presenter.ts index b568b19e..2856ecb9 100644 --- a/src/features/chat/panel/surface/message-stream-presenter.ts +++ b/src/features/chat/panel/surface/message-stream-presenter.ts @@ -6,8 +6,8 @@ import type { PendingRequestBlockActions } from "../../application/pending-reque import type { ChatAction } from "../../application/state/root-reducer"; import type { ChatStateStore } from "../../application/state/store"; import type { ChatTurnDiffViewState } from "../../domain/turn-diff"; -import type { MessageStreamScrollControllerBinding } from "../../ui/message-stream/flow-scroll"; -import { MarkdownMessageRenderer, renderStreamMarkdown } from "../../ui/message-stream/markdown-renderer"; +import type { MessageStreamScrollControllerBinding } from "../../ui/message-stream/flow-scroll.measure"; +import { MarkdownMessageRenderer, renderStreamMarkdown } from "../../ui/message-stream/markdown-renderer.obsidian"; import { MessageStreamViewport, type MessageStreamViewportState } from "../../ui/message-stream/stream-blocks"; import { type ChatPanelMessageStreamShellState, messageStreamStateFromShellState, useChatPanelShellState } from "../shell-state"; import { diff --git a/src/features/chat/panel/surface/message-stream-scroll.ts b/src/features/chat/panel/surface/message-stream-scroll.ts index b9d37a2c..e399627e 100644 --- a/src/features/chat/panel/surface/message-stream-scroll.ts +++ b/src/features/chat/panel/surface/message-stream-scroll.ts @@ -3,7 +3,7 @@ import type { MessageStreamScrollCommand, MessageStreamScrollControllerBinding, MessageStreamScrollPort, -} from "../../ui/message-stream/flow-scroll"; +} from "../../ui/message-stream/flow-scroll.measure"; export interface ChatMessageScrollController extends MessageStreamScrollControllerBinding { showLatest(): void; diff --git a/src/features/chat/ui/composer-dom.ts b/src/features/chat/ui/composer.dom.ts similarity index 100% rename from src/features/chat/ui/composer-dom.ts rename to src/features/chat/ui/composer.dom.ts diff --git a/src/features/chat/ui/composer.tsx b/src/features/chat/ui/composer.tsx index d84adca8..307f63cc 100644 --- a/src/features/chat/ui/composer.tsx +++ b/src/features/chat/ui/composer.tsx @@ -1,10 +1,10 @@ import type { ButtonHTMLAttributes, Ref, ComponentChild as UiNode } from "preact"; import { useLayoutEffect, useRef, useState } from "preact/hooks"; -import { IconButton } from "../../../shared/ui/components"; -import { disposeDomListeners, listenDomEvent } from "../../../shared/ui/dom-events"; -import { syncTextareaHeight } from "../../../shared/ui/textarea-autogrow"; -import { renderComposerMetaIcon, scrollComposerSuggestionIntoView, updateComposerMetaStatusOverflow } from "./composer-dom"; +import { IconButton } from "../../../shared/ui/components.obsidian"; +import { disposeDomListeners, listenDomEvent } from "../../../shared/ui/dom-events.dom"; +import { syncTextareaHeight } from "../../../shared/ui/textarea-autogrow.measure"; +import { renderComposerMetaIcon, scrollComposerSuggestionIntoView, updateComposerMetaStatusOverflow } from "./composer.dom"; export interface ComposerSuggestion { display: string; diff --git a/src/features/chat/ui/goal.tsx b/src/features/chat/ui/goal.tsx index b9cf6a6a..6861b19a 100644 --- a/src/features/chat/ui/goal.tsx +++ b/src/features/chat/ui/goal.tsx @@ -2,10 +2,10 @@ import type { ComponentChild as UiNode } from "preact"; import { useEffect, useLayoutEffect, useRef, useState } from "preact/hooks"; import type { ThreadGoal, ThreadGoalStatus } from "../../../domain/threads/goal"; -import { IconButton } from "../../../shared/ui/components"; -import { disposeDomListeners, listenDomEvent } from "../../../shared/ui/dom-events"; +import { IconButton } from "../../../shared/ui/components.obsidian"; +import { disposeDomListeners, listenDomEvent } from "../../../shared/ui/dom-events.dom"; import { isComposerSendKey, type SendShortcut } from "../../../shared/ui/keyboard"; -import { syncTextareaHeight } from "../../../shared/ui/textarea-autogrow"; +import { syncTextareaHeight } from "../../../shared/ui/textarea-autogrow.measure"; export interface GoalPanelActions { onSave: (objective: string, tokenBudget: number | null) => void; diff --git a/src/features/chat/ui/message-stream/detail.tsx b/src/features/chat/ui/message-stream/detail.dom.tsx similarity index 99% rename from src/features/chat/ui/message-stream/detail.tsx rename to src/features/chat/ui/message-stream/detail.dom.tsx index 38cdc1d5..26f1154c 100644 --- a/src/features/chat/ui/message-stream/detail.tsx +++ b/src/features/chat/ui/message-stream/detail.dom.tsx @@ -1,7 +1,7 @@ import type { ComponentChild as UiNode } from "preact"; import { useLayoutEffect, useRef } from "preact/hooks"; -import { renderRawDiffLines } from "../../../../shared/diff/render"; +import { renderRawDiffLines } from "../../../../shared/diff/render.dom"; import type { DetailSection, DetailView } from "../../presentation/message-stream/detail-view"; import type { MessageStreamDisclosureState } from "./context"; diff --git a/src/features/chat/ui/message-stream/flow-scroll.ts b/src/features/chat/ui/message-stream/flow-scroll.measure.ts similarity index 100% rename from src/features/chat/ui/message-stream/flow-scroll.ts rename to src/features/chat/ui/message-stream/flow-scroll.measure.ts diff --git a/src/features/chat/ui/message-stream/markdown-renderer.ts b/src/features/chat/ui/message-stream/markdown-renderer.obsidian.ts similarity index 100% rename from src/features/chat/ui/message-stream/markdown-renderer.ts rename to src/features/chat/ui/message-stream/markdown-renderer.obsidian.ts diff --git a/src/features/chat/ui/message-stream/stream-blocks.tsx b/src/features/chat/ui/message-stream/stream-blocks.tsx index f6c2b149..d7f5d881 100644 --- a/src/features/chat/ui/message-stream/stream-blocks.tsx +++ b/src/features/chat/ui/message-stream/stream-blocks.tsx @@ -6,8 +6,8 @@ import type { MessageStreamViewBlock, } from "../../presentation/message-stream/view-model"; import type { MessageStreamContext, PendingRequestBlockContext } from "./context"; -import { detailNode } from "./detail"; -import { MessageStreamFlowFrame, type MessageStreamScrollControllerBinding } from "./flow-scroll"; +import { detailNode } from "./detail.dom"; +import { MessageStreamFlowFrame, type MessageStreamScrollControllerBinding } from "./flow-scroll.measure"; import { pendingRequestBlockNode } from "./pending-request-block"; import { agentRunSummaryNode, statusNode } from "./status"; import { textNode } from "./text"; diff --git a/src/features/chat/ui/message-stream/text-content.tsx b/src/features/chat/ui/message-stream/text-content.dom.tsx similarity index 100% rename from src/features/chat/ui/message-stream/text-content.tsx rename to src/features/chat/ui/message-stream/text-content.dom.tsx diff --git a/src/features/chat/ui/message-stream/text.tsx b/src/features/chat/ui/message-stream/text.tsx index 839482cb..5e8be574 100644 --- a/src/features/chat/ui/message-stream/text.tsx +++ b/src/features/chat/ui/message-stream/text.tsx @@ -1,7 +1,7 @@ import { Fragment, type ComponentChild as UiNode } from "preact"; import { useEffect, useRef } from "preact/hooks"; -import { IconButton } from "../../../../shared/ui/components"; -import { listenDomEvent } from "../../../../shared/ui/dom-events"; +import { IconButton } from "../../../../shared/ui/components.obsidian"; +import { listenDomEvent } from "../../../../shared/ui/dom-events.dom"; import type { EditedFilesTextView, MentionedFileTextView, @@ -10,7 +10,7 @@ import type { TextItemDetailSectionView, } from "../../presentation/message-stream/text-view"; import type { TextItemActionContext, TextItemContext, TextItemDetailStateContext, TextItemMetadataContext } from "./context"; -import { CollapsibleTextContent, TextContent } from "./text-content"; +import { CollapsibleTextContent, TextContent } from "./text-content.dom"; export function textNode(view: MessageStreamTextView, context: TextItemContext): UiNode { return ; diff --git a/src/features/chat/ui/toolbar.tsx b/src/features/chat/ui/toolbar.tsx index e9a0b67e..826cbb99 100644 --- a/src/features/chat/ui/toolbar.tsx +++ b/src/features/chat/ui/toolbar.tsx @@ -1,6 +1,6 @@ import type { ButtonHTMLAttributes, TargetedKeyboardEvent, ComponentChild as UiNode } from "preact"; import { useLayoutEffect, useRef } from "preact/hooks"; -import { IconButton } from "../../../shared/ui/components"; +import { IconButton } from "../../../shared/ui/components.obsidian"; import type { RateLimitSummary } from "../presentation/runtime/status"; type ButtonProps = ButtonHTMLAttributes & { diff --git a/src/features/chat/ui/turn-diff/render.tsx b/src/features/chat/ui/turn-diff/render.dom.tsx similarity index 95% rename from src/features/chat/ui/turn-diff/render.tsx rename to src/features/chat/ui/turn-diff/render.dom.tsx index 5a3e84fe..717f6bfd 100644 --- a/src/features/chat/ui/turn-diff/render.tsx +++ b/src/features/chat/ui/turn-diff/render.dom.tsx @@ -1,11 +1,11 @@ import type { ComponentChild as UiNode } from "preact"; import { useLayoutEffect, useRef } from "preact/hooks"; -import { renderDisplayDiffLines } from "../../../../shared/diff/render"; +import { renderDisplayDiffLines } from "../../../../shared/diff/render.dom"; import { displayDiffLines } from "../../../../shared/diff/unified"; import { shortThreadId } from "../../../../shared/id/thread-id"; -import { IconButton } from "../../../../shared/ui/components"; -import { renderUiRoot } from "../../../../shared/ui/ui-root"; +import { IconButton } from "../../../../shared/ui/components.obsidian"; +import { renderUiRoot } from "../../../../shared/ui/ui-root.dom"; import type { ChatTurnDiffViewState, PersistedChatTurnDiffViewState } from "../../domain/turn-diff"; export interface ChatTurnDiffViewActions { diff --git a/src/features/chat/ui/turn-diff/view.ts b/src/features/chat/ui/turn-diff/view.obsidian.ts similarity index 94% rename from src/features/chat/ui/turn-diff/view.ts rename to src/features/chat/ui/turn-diff/view.obsidian.ts index b81ce504..fb09f53d 100644 --- a/src/features/chat/ui/turn-diff/view.ts +++ b/src/features/chat/ui/turn-diff/view.obsidian.ts @@ -2,14 +2,14 @@ import { ItemView, type ViewStateResult } from "obsidian"; import { VIEW_TYPE_CODEX_TURN_DIFF } from "../../../../constants"; import { copyTextWithNotice } from "../../../../shared/ui/clipboard"; -import { unmountUiRoot } from "../../../../shared/ui/ui-root"; +import { unmountUiRoot } from "../../../../shared/ui/ui-root.dom"; import { type ChatTurnDiffViewState, isPersistedChatTurnDiffViewState, type PersistedChatTurnDiffViewState, persistedChatTurnDiffViewState, } from "../../domain/turn-diff"; -import { renderChatTurnDiffView } from "./render"; +import { renderChatTurnDiffView } from "./render.dom"; export class CodexChatTurnDiffView extends ItemView { private metadata: PersistedChatTurnDiffViewState | null = null; diff --git a/src/features/selection-rewrite/command.ts b/src/features/selection-rewrite/command.ts index 92b10321..0a7d66d4 100644 --- a/src/features/selection-rewrite/command.ts +++ b/src/features/selection-rewrite/command.ts @@ -1,7 +1,7 @@ import { type Editor, MarkdownView, Notice, type Plugin } from "obsidian"; import type { SendShortcut } from "../../shared/ui/keyboard"; import type { SelectionRewriteRuntimeSettings, SelectionRewriteState } from "./model"; -import { SelectionRewritePopover } from "./popover"; +import { SelectionRewritePopover } from "./popover.dom"; export interface SelectionRewriteCommandHost extends Plugin { settings: { diff --git a/src/features/selection-rewrite/popover.tsx b/src/features/selection-rewrite/popover.dom.tsx similarity index 98% rename from src/features/selection-rewrite/popover.tsx rename to src/features/selection-rewrite/popover.dom.tsx index d9a6d16d..78076da6 100644 --- a/src/features/selection-rewrite/popover.tsx +++ b/src/features/selection-rewrite/popover.dom.tsx @@ -2,13 +2,13 @@ import { type Editor, Notice } from "obsidian"; import type { TargetedKeyboardEvent, ComponentChild as UiNode } from "preact"; import { useLayoutEffect, useRef } from "preact/hooks"; -import { renderDisplayDiffLines } from "../../shared/diff/render"; +import { renderDisplayDiffLines } from "../../shared/diff/render.dom"; import { displayDiffLines } from "../../shared/diff/unified"; -import { IconButton } from "../../shared/ui/components"; +import { IconButton } from "../../shared/ui/components.obsidian"; import { isComposerSendKey, type SendShortcut } from "../../shared/ui/keyboard"; -import { syncTextareaHeight } from "../../shared/ui/textarea-autogrow"; -import { type TextareaCaretBoundaryDirection, textareaCursorAtVisualBoundary } from "../../shared/ui/textarea-caret"; -import { renderUiRoot, unmountUiRoot } from "../../shared/ui/ui-root"; +import { syncTextareaHeight } from "../../shared/ui/textarea-autogrow.measure"; +import { type TextareaCaretBoundaryDirection, textareaCursorAtVisualBoundary } from "../../shared/ui/textarea-caret.measure"; +import { renderUiRoot, unmountUiRoot } from "../../shared/ui/ui-root.dom"; import { buildSelectionUnifiedDiff } from "./diff"; import { canApplySelectionRewrite, type SelectionRewriteRuntimeSettings, type SelectionRewriteState } from "./model"; diff --git a/src/features/selection-rewrite/session.ts b/src/features/selection-rewrite/session.ts index 9d4ac7d7..e50644a5 100644 --- a/src/features/selection-rewrite/session.ts +++ b/src/features/selection-rewrite/session.ts @@ -1,4 +1,4 @@ -import type { TextareaCaretBoundaryDirection } from "../../shared/ui/textarea-caret"; +import type { TextareaCaretBoundaryDirection } from "../../shared/ui/textarea-caret.measure"; import { type SelectionRewriteLifecycleEvent, type SelectionRewriteRuntimeSettings, diff --git a/src/features/thread-picker/modal.ts b/src/features/thread-picker/modal.obsidian.ts similarity index 100% rename from src/features/thread-picker/modal.ts rename to src/features/thread-picker/modal.obsidian.ts diff --git a/src/features/threads-view/renderer.tsx b/src/features/threads-view/renderer.dom.tsx similarity index 99% rename from src/features/threads-view/renderer.tsx rename to src/features/threads-view/renderer.dom.tsx index 665f7da2..29ccfd23 100644 --- a/src/features/threads-view/renderer.tsx +++ b/src/features/threads-view/renderer.dom.tsx @@ -1,8 +1,8 @@ import type { ButtonHTMLAttributes, TargetedKeyboardEvent, ComponentChild as UiNode } from "preact"; import { useLayoutEffect, useRef } from "preact/hooks"; -import { IconButton } from "../../shared/ui/components"; -import { renderUiRoot, unmountUiRoot } from "../../shared/ui/ui-root"; +import { IconButton } from "../../shared/ui/components.obsidian"; +import { renderUiRoot, unmountUiRoot } from "../../shared/ui/ui-root.dom"; import type { ThreadsRowModel } from "./state"; type ButtonProps = ButtonHTMLAttributes & { diff --git a/src/features/threads-view/session.ts b/src/features/threads-view/session.ts index 497c358a..9142273b 100644 --- a/src/features/threads-view/session.ts +++ b/src/features/threads-view/session.ts @@ -12,7 +12,7 @@ import type { OpenCodexPanelSnapshot } from "../../workspace/panel-coordinator"; import type { ThreadCatalogActiveReader, ThreadCatalogEventSink } from "../../workspace/thread-catalog"; import { createThreadOperations, type ThreadOperations } from "../threads/thread-operations"; import { createThreadTitleService, type ThreadTitleService } from "../threads/thread-title-service"; -import { renderThreadsView, unmountThreadsView } from "./renderer"; +import { renderThreadsView, unmountThreadsView } from "./renderer.dom"; import { type ThreadsGeneratingRenameState, type ThreadsRenameLifecycleEvent, diff --git a/src/main.ts b/src/main.ts index b3436e46..a31651ac 100644 --- a/src/main.ts +++ b/src/main.ts @@ -2,12 +2,12 @@ import { Plugin } from "obsidian"; import { VIEW_TYPE_CODEX_PANEL, VIEW_TYPE_CODEX_THREADS, VIEW_TYPE_CODEX_TURN_DIFF } from "./constants"; import { CodexChatView } from "./features/chat/host/view"; -import { CodexChatTurnDiffView } from "./features/chat/ui/turn-diff/view"; +import { CodexChatTurnDiffView } from "./features/chat/ui/turn-diff/view.obsidian"; import { registerSelectionRewriteCommand } from "./features/selection-rewrite/command"; import { CodexThreadsView } from "./features/threads-view/view"; import { CodexPanelRuntime } from "./plugin-runtime"; import { type CodexPanelSettings, DEFAULT_SETTINGS, getVaultPath, normalizeSettings, settingsMatchNormalizedData } from "./settings/model"; -import { CodexPanelSettingTab } from "./settings/tab"; +import { CodexPanelSettingTab } from "./settings/tab.obsidian"; export default class CodexPanelPlugin extends Plugin { settings: CodexPanelSettings = DEFAULT_SETTINGS; diff --git a/src/plugin-runtime.ts b/src/plugin-runtime.ts index b355ab55..8f11676c 100644 --- a/src/plugin-runtime.ts +++ b/src/plugin-runtime.ts @@ -15,8 +15,8 @@ import type { ChatViewLifecycleSurface, ChatWorkspacePanelSurface, } from "./features/chat/host/surface-handle"; -import { CodexChatTurnDiffView } from "./features/chat/ui/turn-diff/view"; -import { openThreadPicker, type ThreadPickerHost } from "./features/thread-picker/modal"; +import { CodexChatTurnDiffView } from "./features/chat/ui/turn-diff/view.obsidian"; +import { openThreadPicker, type ThreadPickerHost } from "./features/thread-picker/modal.obsidian"; import type { CodexThreadsHost, CodexThreadsSettingsAccess } from "./features/threads-view/session"; import { CodexThreadsView } from "./features/threads-view/view"; import type { CodexPanelSettingTabHost } from "./settings/host"; diff --git a/src/settings/archived-section.tsx b/src/settings/archived-section.tsx index 1f012e9e..45552dce 100644 --- a/src/settings/archived-section.tsx +++ b/src/settings/archived-section.tsx @@ -3,7 +3,7 @@ import type { ComponentChild as UiNode } from "preact"; import type { Thread } from "../domain/threads/model"; import { threadArchiveDisplayTitle } from "../domain/threads/title"; import { shortThreadId } from "../shared/id/thread-id"; -import { ObsidianExtraButton, ObsidianTextInput, ObsidianToggle } from "../shared/ui/components"; +import { ObsidianExtraButton, ObsidianTextInput, ObsidianToggle } from "../shared/ui/components.obsidian"; import type { ArchivedThreadSectionState } from "./section-state"; import { SettingRow, SettingsGroup, SettingsHeading, SettingsItems, SettingsStatusRow } from "./setting-components"; diff --git a/src/settings/helper-section.tsx b/src/settings/helper-section.tsx index ece3639b..d7c8ceec 100644 --- a/src/settings/helper-section.tsx +++ b/src/settings/helper-section.tsx @@ -2,7 +2,7 @@ import type { ComponentChild as UiNode } from "preact"; import type { ModelMetadata, ReasoningEffort } from "../domain/catalog/metadata"; import { findModelMetadataByIdOrName, supportedEffortsForModelMetadata } from "../domain/catalog/metadata"; -import { ObsidianDropdown } from "../shared/ui/components"; +import { ObsidianDropdown } from "../shared/ui/components.obsidian"; import type { HelperSettingsState } from "./section-state"; import { SettingRow, SettingsGroup, SettingsHeading, SettingsItems } from "./setting-components"; diff --git a/src/settings/hook-section.tsx b/src/settings/hook-section.tsx index 5ac3db1f..fc5c1c23 100644 --- a/src/settings/hook-section.tsx +++ b/src/settings/hook-section.tsx @@ -1,7 +1,7 @@ import type { ComponentChild as UiNode } from "preact"; import type { HookItem } from "../domain/catalog/metadata"; -import { ObsidianButton } from "../shared/ui/components"; +import { ObsidianButton } from "../shared/ui/components.obsidian"; import type { HookSectionState } from "./section-state"; import { SettingRow, SettingsGroup, SettingsHeading, SettingsItems, SettingsStatusRow } from "./setting-components"; diff --git a/src/settings/tab.tsx b/src/settings/tab.obsidian.tsx similarity index 99% rename from src/settings/tab.tsx rename to src/settings/tab.obsidian.tsx index 89e8e1da..d1e1060c 100644 --- a/src/settings/tab.tsx +++ b/src/settings/tab.obsidian.tsx @@ -2,7 +2,7 @@ import { type App, Notice, type Plugin, PluginSettingTab, Setting, setIcon } fro import { DEFAULT_CODEX_PATH } from "../constants"; import type { ReasoningEffort } from "../domain/catalog/metadata"; -import { renderUiRoot, unmountUiRoot } from "../shared/ui/ui-root"; +import { renderUiRoot, unmountUiRoot } from "../shared/ui/ui-root.dom"; import { ArchivedThreadSection } from "./archived-section"; import { SettingsDynamicDataController, type SettingsDynamicDataDisplayTarget } from "./dynamic-data-controller"; import { HelperSettingsSection } from "./helper-section"; diff --git a/src/shared/diff/render.ts b/src/shared/diff/render.dom.ts similarity index 100% rename from src/shared/diff/render.ts rename to src/shared/diff/render.dom.ts diff --git a/src/shared/ui/components.tsx b/src/shared/ui/components.obsidian.tsx similarity index 100% rename from src/shared/ui/components.tsx rename to src/shared/ui/components.obsidian.tsx diff --git a/src/shared/ui/dom-events.ts b/src/shared/ui/dom-events.dom.ts similarity index 100% rename from src/shared/ui/dom-events.ts rename to src/shared/ui/dom-events.dom.ts diff --git a/src/shared/ui/textarea-autogrow.ts b/src/shared/ui/textarea-autogrow.measure.ts similarity index 100% rename from src/shared/ui/textarea-autogrow.ts rename to src/shared/ui/textarea-autogrow.measure.ts diff --git a/src/shared/ui/textarea-caret.ts b/src/shared/ui/textarea-caret.measure.ts similarity index 100% rename from src/shared/ui/textarea-caret.ts rename to src/shared/ui/textarea-caret.measure.ts diff --git a/src/shared/ui/ui-root.tsx b/src/shared/ui/ui-root.dom.tsx similarity index 100% rename from src/shared/ui/ui-root.tsx rename to src/shared/ui/ui-root.dom.tsx diff --git a/tests/features/chat/conversation/composer/controller.test.ts b/tests/features/chat/conversation/composer/controller.test.ts index 3396faa6..743627df 100644 --- a/tests/features/chat/conversation/composer/controller.test.ts +++ b/tests/features/chat/conversation/composer/controller.test.ts @@ -9,7 +9,7 @@ import { createChatStateStore } from "../../../../../src/features/chat/applicati import { ChatComposerController, type ChatComposerRenderActions } from "../../../../../src/features/chat/panel/composer-controller"; import type { ChatPanelComposerShellState } from "../../../../../src/features/chat/panel/shell-state"; import { ComposerShell } from "../../../../../src/features/chat/ui/composer"; -import { renderUiRoot, unmountUiRoot } from "../../../../../src/shared/ui/ui-root"; +import { renderUiRoot, unmountUiRoot } from "../../../../../src/shared/ui/ui-root.dom"; import { installObsidianDomShims } from "../../../../support/dom"; import { composerShellStateFromChatState } from "../../support/shell-state"; diff --git a/tests/features/chat/panel/shell.test.tsx b/tests/features/chat/panel/shell.test.tsx index 9eced4fb..73aec1d5 100644 --- a/tests/features/chat/panel/shell.test.tsx +++ b/tests/features/chat/panel/shell.test.tsx @@ -4,13 +4,13 @@ import { act } from "preact/test-utils"; import { describe, expect, it, vi } from "vitest"; import { messageStreamItems } from "../../../../src/features/chat/application/state/message-stream"; import { createChatStateStore } from "../../../../src/features/chat/application/state/store"; -import { type ChatPanelShellParts, renderChatPanelShell, unmountChatPanelShell } from "../../../../src/features/chat/panel/shell"; +import { type ChatPanelShellParts, renderChatPanelShell, unmountChatPanelShell } from "../../../../src/features/chat/panel/shell.dom"; import type { ChatPanelComposerSurface } from "../../../../src/features/chat/panel/surface/composer-projection"; import type { ChatPanelGoalSurface } from "../../../../src/features/chat/panel/surface/goal-projection"; import type { ChatPanelToolbarSurface } from "../../../../src/features/chat/panel/surface/toolbar-projection"; import { messageStreamViewBlocks } from "../../../../src/features/chat/presentation/message-stream/view-model"; import type { MessageStreamContext } from "../../../../src/features/chat/ui/message-stream/context"; -import type { MessageStreamScrollControllerBinding } from "../../../../src/features/chat/ui/message-stream/flow-scroll"; +import type { MessageStreamScrollControllerBinding } from "../../../../src/features/chat/ui/message-stream/flow-scroll.measure"; import { installObsidianDomShims } from "../../../support/dom"; installObsidianDomShims(); diff --git a/tests/features/chat/panel/surface/message-stream-presenter.test.ts b/tests/features/chat/panel/surface/message-stream-presenter.test.ts index 77937be2..00dc680a 100644 --- a/tests/features/chat/panel/surface/message-stream-presenter.test.ts +++ b/tests/features/chat/panel/surface/message-stream-presenter.test.ts @@ -16,9 +16,9 @@ import { createChatMessageScrollController, } from "../../../../../src/features/chat/panel/surface/message-stream-scroll"; import { MESSAGE_CONTENT_RENDERED_EVENT } from "../../../../../src/features/chat/ui/message-stream/content-events"; -import { MarkdownMessageRenderer } from "../../../../../src/features/chat/ui/message-stream/markdown-renderer"; +import { MarkdownMessageRenderer } from "../../../../../src/features/chat/ui/message-stream/markdown-renderer.obsidian"; import { MessageStreamViewport } from "../../../../../src/features/chat/ui/message-stream/stream-blocks"; -import { renderUiRoot, unmountUiRoot } from "../../../../../src/shared/ui/ui-root"; +import { renderUiRoot, unmountUiRoot } from "../../../../../src/shared/ui/ui-root.dom"; import { notices } from "../../../../mocks/obsidian"; import { installObsidianDomShims } from "../../../../support/dom"; import { withChatStateMessageStreamItems } from "../../support/message-stream"; diff --git a/tests/features/chat/panel/surface/projection.test.ts b/tests/features/chat/panel/surface/projection.test.ts index 0e736600..ac588870 100644 --- a/tests/features/chat/panel/surface/projection.test.ts +++ b/tests/features/chat/panel/surface/projection.test.ts @@ -17,7 +17,7 @@ import { ChatPanelGoal, type ChatPanelGoalSurface } from "../../../../../src/fea import { ChatPanelToolbar } from "../../../../../src/features/chat/panel/surface/toolbar-projection"; import { effortStatusLines, modelStatusLines, statusSummaryLines } from "../../../../../src/features/chat/presentation/runtime/status"; import type { ToolbarActions } from "../../../../../src/features/chat/ui/toolbar"; -import { renderUiRoot, unmountUiRoot } from "../../../../../src/shared/ui/ui-root"; +import { renderUiRoot, unmountUiRoot } from "../../../../../src/shared/ui/ui-root.dom"; import { installObsidianDomShims } from "../../../../support/dom"; import { withChatStateMessageStreamItems } from "../../support/message-stream"; import { composerShellStateFromChatState } from "../../support/shell-state"; diff --git a/tests/features/chat/panel/toolbar-archive-state.test.tsx b/tests/features/chat/panel/toolbar-archive-state.test.tsx index 012a4f90..b6da3cfb 100644 --- a/tests/features/chat/panel/toolbar-archive-state.test.tsx +++ b/tests/features/chat/panel/toolbar-archive-state.test.tsx @@ -6,13 +6,13 @@ import { describe, expect, it, vi } from "vitest"; import type { Thread } from "../../../../src/domain/threads/model"; import { createChatStateStore } from "../../../../src/features/chat/application/state/store"; import type { ThreadManagementActions } from "../../../../src/features/chat/application/threads/thread-management-actions"; -import { type ChatPanelShellParts, renderChatPanelShell, unmountChatPanelShell } from "../../../../src/features/chat/panel/shell"; +import { type ChatPanelShellParts, renderChatPanelShell, unmountChatPanelShell } from "../../../../src/features/chat/panel/shell.dom"; import type { ChatPanelComposerSurface } from "../../../../src/features/chat/panel/surface/composer-projection"; import type { ChatPanelGoalSurface } from "../../../../src/features/chat/panel/surface/goal-projection"; import type { ChatPanelToolbarSurface } from "../../../../src/features/chat/panel/surface/toolbar-projection"; import { createToolbarPanelActions, type ToolbarPanelActions } from "../../../../src/features/chat/panel/toolbar-actions"; import type { MessageStreamContext } from "../../../../src/features/chat/ui/message-stream/context"; -import type { MessageStreamScrollControllerBinding } from "../../../../src/features/chat/ui/message-stream/flow-scroll"; +import type { MessageStreamScrollControllerBinding } from "../../../../src/features/chat/ui/message-stream/flow-scroll.measure"; import { installObsidianDomShims } from "../../../support/dom"; installObsidianDomShims(); diff --git a/tests/features/chat/ui/message-stream/blocks-and-messages.test.tsx b/tests/features/chat/ui/message-stream/blocks-and-messages.test.tsx index b7fe49af..48902710 100644 --- a/tests/features/chat/ui/message-stream/blocks-and-messages.test.tsx +++ b/tests/features/chat/ui/message-stream/blocks-and-messages.test.tsx @@ -6,7 +6,7 @@ import { describe, expect, it, vi } from "vitest"; import { implementPlanTargetFromState } from "../../../../../src/features/chat/application/conversation/plan-implementation"; import type { MessageStreamItem } from "../../../../../src/features/chat/domain/message-stream/items"; import { MESSAGE_CONTENT_RENDERED_EVENT } from "../../../../../src/features/chat/ui/message-stream/content-events"; -import { MarkdownMessageRenderer } from "../../../../../src/features/chat/ui/message-stream/markdown-renderer"; +import { MarkdownMessageRenderer } from "../../../../../src/features/chat/ui/message-stream/markdown-renderer.obsidian"; import { deferred } from "../../../../support/async"; import { attributeValues, textContents, topLevelDetailsSummaries } from "../../../../support/dom"; import "./setup"; diff --git a/tests/features/chat/ui/message-stream/flow-scroll.test.ts b/tests/features/chat/ui/message-stream/flow-scroll.test.ts index cdda6fea..fecbc5a4 100644 --- a/tests/features/chat/ui/message-stream/flow-scroll.test.ts +++ b/tests/features/chat/ui/message-stream/flow-scroll.test.ts @@ -9,8 +9,8 @@ import { type MessageStreamScrollCommand, type MessageStreamScrollControllerBinding, type MessageStreamScrollPort, -} from "../../../../../src/features/chat/ui/message-stream/flow-scroll"; -import { renderUiRoot } from "../../../../../src/shared/ui/ui-root"; +} from "../../../../../src/features/chat/ui/message-stream/flow-scroll.measure"; +import { renderUiRoot } from "../../../../../src/shared/ui/ui-root.dom"; import { installObsidianDomShims } from "../../../../support/dom"; installObsidianDomShims(); diff --git a/tests/features/chat/ui/message-stream/stream-markdown-renderer.test.ts b/tests/features/chat/ui/message-stream/stream-markdown-renderer.test.ts index c4691655..25ef85d7 100644 --- a/tests/features/chat/ui/message-stream/stream-markdown-renderer.test.ts +++ b/tests/features/chat/ui/message-stream/stream-markdown-renderer.test.ts @@ -3,7 +3,7 @@ import { TFile } from "obsidian"; import { beforeEach, describe, expect, it, vi } from "vitest"; -import { renderStreamMarkdown } from "../../../../../src/features/chat/ui/message-stream/markdown-renderer"; +import { renderStreamMarkdown } from "../../../../../src/features/chat/ui/message-stream/markdown-renderer.obsidian"; import { notices } from "../../../../mocks/obsidian"; import { installObsidianDomShims } from "../../../../support/dom"; diff --git a/tests/features/chat/ui/message-stream/test-helpers.tsx b/tests/features/chat/ui/message-stream/test-helpers.tsx index 53283ffe..7023a1ee 100644 --- a/tests/features/chat/ui/message-stream/test-helpers.tsx +++ b/tests/features/chat/ui/message-stream/test-helpers.tsx @@ -16,10 +16,10 @@ import type { PendingRequestBlockActions, PendingRequestBlockContext, } from "../../../../../src/features/chat/ui/message-stream/context"; -import type { MessageStreamScrollControllerBinding } from "../../../../../src/features/chat/ui/message-stream/flow-scroll"; +import type { MessageStreamScrollControllerBinding } from "../../../../../src/features/chat/ui/message-stream/flow-scroll.measure"; import { pendingRequestBlockNode } from "../../../../../src/features/chat/ui/message-stream/pending-request-block"; import { MessageStreamViewport } from "../../../../../src/features/chat/ui/message-stream/stream-blocks"; -import { renderUiRoot, unmountUiRoot } from "../../../../../src/shared/ui/ui-root"; +import { renderUiRoot, unmountUiRoot } from "../../../../../src/shared/ui/ui-root.dom"; export function messageStreamBlocks(context: TestMessageStreamContext): [MessageStreamViewBlock, ...MessageStreamViewBlock[]] { const normalized = normalizeMessageStreamContext(context); diff --git a/tests/features/chat/ui/renderers/composer.test.ts b/tests/features/chat/ui/renderers/composer.test.ts index 8f179d70..18f9a42d 100644 --- a/tests/features/chat/ui/renderers/composer.test.ts +++ b/tests/features/chat/ui/renderers/composer.test.ts @@ -9,8 +9,8 @@ import { type ComposerSuggestion, syncComposerHeight, } from "../../../../../src/features/chat/ui/composer"; -import { scrollComposerSuggestionIntoView } from "../../../../../src/features/chat/ui/composer-dom"; -import { renderUiRoot } from "../../../../../src/shared/ui/ui-root"; +import { scrollComposerSuggestionIntoView } from "../../../../../src/features/chat/ui/composer.dom"; +import { renderUiRoot } from "../../../../../src/shared/ui/ui-root.dom"; import { waitForAsyncWork } from "../../../../support/async"; import { changeInputValue, composerSuggestionScrollFixture, installObsidianDomShims } from "../../../../support/dom"; diff --git a/tests/features/chat/ui/renderers/goal.test.tsx b/tests/features/chat/ui/renderers/goal.test.tsx index 58e73993..42e7b384 100644 --- a/tests/features/chat/ui/renderers/goal.test.tsx +++ b/tests/features/chat/ui/renderers/goal.test.tsx @@ -7,7 +7,7 @@ import { describe, expect, it, vi } from "vitest"; import type { ThreadGoal } from "../../../../../src/domain/threads/goal"; import { GoalPanel, type GoalPanelActions, type GoalPanelEditorState } from "../../../../../src/features/chat/ui/goal"; import type { SendShortcut } from "../../../../../src/shared/ui/keyboard"; -import { renderUiRoot } from "../../../../../src/shared/ui/ui-root"; +import { renderUiRoot } from "../../../../../src/shared/ui/ui-root.dom"; import { installObsidianDomShims } from "../../../../support/dom"; installObsidianDomShims(); diff --git a/tests/features/chat/ui/renderers/toolbar.test.ts b/tests/features/chat/ui/renderers/toolbar.test.ts index c18b3286..7305b565 100644 --- a/tests/features/chat/ui/renderers/toolbar.test.ts +++ b/tests/features/chat/ui/renderers/toolbar.test.ts @@ -4,7 +4,7 @@ import { h } from "preact"; import { describe, expect, it, vi } from "vitest"; import { Toolbar, type ToolbarActions, type ToolbarViewModel } from "../../../../../src/features/chat/ui/toolbar"; -import { renderUiRoot } from "../../../../../src/shared/ui/ui-root"; +import { renderUiRoot } from "../../../../../src/shared/ui/ui-root.dom"; import { changeInputValue, installObsidianDomShims } from "../../../../support/dom"; installObsidianDomShims(); diff --git a/tests/features/chat/ui/renderers/turn-diff.test.ts b/tests/features/chat/ui/renderers/turn-diff.test.ts index 159d817d..047d7d21 100644 --- a/tests/features/chat/ui/renderers/turn-diff.test.ts +++ b/tests/features/chat/ui/renderers/turn-diff.test.ts @@ -4,8 +4,8 @@ import type { WorkspaceLeaf } from "obsidian"; import { describe, expect, it, vi } from "vitest"; import { persistedChatTurnDiffViewState } from "../../../../../src/features/chat/domain/turn-diff"; -import { renderChatTurnDiffView } from "../../../../../src/features/chat/ui/turn-diff/render"; -import { CodexChatTurnDiffView } from "../../../../../src/features/chat/ui/turn-diff/view"; +import { renderChatTurnDiffView } from "../../../../../src/features/chat/ui/turn-diff/render.dom"; +import { CodexChatTurnDiffView } from "../../../../../src/features/chat/ui/turn-diff/view.obsidian"; import { installObsidianDomShims } from "../../../../support/dom"; installObsidianDomShims(); diff --git a/tests/features/chat/ui/ui-root.test.tsx b/tests/features/chat/ui/ui-root.test.tsx index fe638d43..6824dcd8 100644 --- a/tests/features/chat/ui/ui-root.test.tsx +++ b/tests/features/chat/ui/ui-root.test.tsx @@ -3,7 +3,7 @@ import { useLayoutEffect } from "preact/hooks"; import { describe, expect, it, vi } from "vitest"; -import { renderUiRoot, unmountUiRoot } from "../../../../src/shared/ui/ui-root"; +import { renderUiRoot, unmountUiRoot } from "../../../../src/shared/ui/ui-root.dom"; describe("Preact root adapter", () => { it("reuses roots that render no host children", () => { diff --git a/tests/features/selection-rewrite/selection-rewrite-command.test.ts b/tests/features/selection-rewrite/selection-rewrite-command.test.ts index ae9a4531..8f0a9551 100644 --- a/tests/features/selection-rewrite/selection-rewrite-command.test.ts +++ b/tests/features/selection-rewrite/selection-rewrite-command.test.ts @@ -4,7 +4,7 @@ import { MarkdownView, TFile } from "obsidian"; import { describe, expect, it, vi } from "vitest"; import { registerSelectionRewriteCommand } from "../../../src/features/selection-rewrite/command"; -import type { SelectionRewritePopoverOptions } from "../../../src/features/selection-rewrite/popover"; +import type { SelectionRewritePopoverOptions } from "../../../src/features/selection-rewrite/popover.dom"; const popoverMock = vi.hoisted(() => { const instances: { options: SelectionRewritePopoverOptions; open: ReturnType; close: ReturnType }[] = []; @@ -16,7 +16,7 @@ const popoverMock = vi.hoisted(() => { }; }); -vi.mock("../../../src/features/selection-rewrite/popover", () => { +vi.mock("../../../src/features/selection-rewrite/popover.dom", () => { class SelectionRewritePopover { readonly open = vi.fn(); readonly close = vi.fn(); diff --git a/tests/features/selection-rewrite/selection-rewrite.test.ts b/tests/features/selection-rewrite/selection-rewrite.test.ts index 422019df..064ca3ed 100644 --- a/tests/features/selection-rewrite/selection-rewrite.test.ts +++ b/tests/features/selection-rewrite/selection-rewrite.test.ts @@ -19,7 +19,7 @@ import { transitionSelectionRewriteState, } from "../../../src/features/selection-rewrite/model"; import { selectionRewriteOutputParseResultFromText } from "../../../src/features/selection-rewrite/output"; -import { SelectionRewritePopover } from "../../../src/features/selection-rewrite/popover"; +import { SelectionRewritePopover } from "../../../src/features/selection-rewrite/popover.dom"; import { positionSelectionRewritePopover } from "../../../src/features/selection-rewrite/position"; import { buildSelectionRewritePrompt } from "../../../src/features/selection-rewrite/prompt"; import * as selectionRewriteRunner from "../../../src/features/selection-rewrite/runner"; diff --git a/tests/features/thread-picker/modal.test.ts b/tests/features/thread-picker/modal.test.ts index 953bf513..cb9438c6 100644 --- a/tests/features/thread-picker/modal.test.ts +++ b/tests/features/thread-picker/modal.test.ts @@ -4,7 +4,7 @@ import { SuggestModal } from "obsidian"; import { describe, expect, it } from "vitest"; import type { Thread } from "../../../src/domain/threads/model"; -import { openThreadPicker, type ThreadPickerHost } from "../../../src/features/thread-picker/modal"; +import { openThreadPicker, type ThreadPickerHost } from "../../../src/features/thread-picker/modal.obsidian"; describe("threadPickerSuggestions", () => { it("orders title and id prefix matches before looser matches", async () => { diff --git a/tests/features/threads-view/renderer.test.ts b/tests/features/threads-view/renderer.test.ts index e5510242..33bcbfea 100644 --- a/tests/features/threads-view/renderer.test.ts +++ b/tests/features/threads-view/renderer.test.ts @@ -2,7 +2,7 @@ import { describe, expect, it, vi } from "vitest"; import type { Thread } from "../../../src/domain/threads/model"; -import { renderThreadsView } from "../../../src/features/threads-view/renderer"; +import { renderThreadsView } from "../../../src/features/threads-view/renderer.dom"; import { type ThreadsRowModel, threadRows } from "../../../src/features/threads-view/state"; import type { OpenCodexPanelSnapshot } from "../../../src/workspace/panel-coordinator"; import { changeInputValue, installObsidianDomShims } from "../../support/dom"; diff --git a/tests/scripts/grit-policy.test.mjs b/tests/scripts/grit-policy.test.mjs index c242357f..ae42c280 100644 --- a/tests/scripts/grit-policy.test.mjs +++ b/tests/scripts/grit-policy.test.mjs @@ -471,31 +471,73 @@ type Resume = AppServerClient["resumeThread"]; ]); }); + it("keeps imperative DOM bridges behind filename suffixes", async () => { + const cwd = await tempBiomeWorkspace(["no-implicit-dom-bridges.grit"]); + await writeFile( + path.join(cwd, "src/features/chat/ui/composer.tsx"), + ` +export function render(container: HTMLElement): void { + container.createDiv(); + container.addEventListener("click", () => undefined); +} +`.trimStart(), + ); + await writeFile( + path.join(cwd, "src/features/chat/ui/composer.dom.tsx"), + ` +export function render(container: HTMLElement): void { + container.createDiv(); + container.addEventListener("click", () => undefined); +} +`.trimStart(), + ); + await writeFile( + path.join(cwd, "src/app-server/services/abortable-operation.ts"), + ` +export function onAbort(signal: AbortSignal): void { + signal.addEventListener("abort", () => undefined); +} +`.trimStart(), + ); + + const report = biomeLint( + ["src/features/chat/ui/composer.tsx", "src/features/chat/ui/composer.dom.tsx", "src/app-server/services/abortable-operation.ts"], + cwd, + ); + + expect(pluginMessages(report, "src/features/chat/ui/composer.tsx")).toEqual([ + "Keep imperative DOM writes and event wiring in files named with a .dom, .obsidian, or .measure suffix.", + "Keep imperative DOM writes and event wiring in files named with a .dom, .obsidian, or .measure suffix.", + ]); + expect(pluginDiagnostics(report, "src/features/chat/ui/composer.dom.tsx")).toEqual([]); + expect(pluginDiagnostics(report, "src/app-server/services/abortable-operation.ts")).toEqual([]); + }); + it("keeps the Preact root adapter in explicit root bridge files", async () => { const cwd = await tempBiomeWorkspace(["no-ui-root-imports.grit"]); await writeFile( path.join(cwd, "src/features/chat/ui/composer.tsx"), ` -import { renderUiRoot } from '../../../shared/ui/ui-root'; +import { renderUiRoot } from '../../../shared/ui/ui-root.dom'; export const render = renderUiRoot; `.trimStart(), ); await writeFile( - path.join(cwd, "src/features/chat/panel/shell.tsx"), + path.join(cwd, "src/features/chat/panel/shell.dom.tsx"), ` -import { renderUiRoot } from "../../../shared/ui/ui-root"; +import { renderUiRoot } from "../../../shared/ui/ui-root.dom"; export const render = renderUiRoot; `.trimStart(), ); - const report = biomeLint(["src/features/chat/ui/composer.tsx", "src/features/chat/panel/shell.tsx"], cwd); + const report = biomeLint(["src/features/chat/ui/composer.tsx", "src/features/chat/panel/shell.dom.tsx"], cwd); expect(pluginMessages(report, "src/features/chat/ui/composer.tsx")).toEqual([ "Import the Preact root adapter only from explicit root bridge files.", ]); - expect(pluginDiagnostics(report, "src/features/chat/panel/shell.tsx")).toEqual([]); + expect(pluginDiagnostics(report, "src/features/chat/panel/shell.dom.tsx")).toEqual([]); }); it("keeps chat state transforms pure and catches global scheduling calls", async () => { diff --git a/tests/settings/settings-tab.test.ts b/tests/settings/settings-tab.test.ts index 2e07ceb9..454d555c 100644 --- a/tests/settings/settings-tab.test.ts +++ b/tests/settings/settings-tab.test.ts @@ -11,7 +11,7 @@ import type { Thread } from "../../src/domain/threads/model"; import { threadArchiveDisplayTitle } from "../../src/domain/threads/title"; import { SettingsDynamicDataController, type SettingsDynamicDataSnapshot } from "../../src/settings/dynamic-data-controller"; import type { CodexPanelSettingTabHost } from "../../src/settings/host"; -import { CodexPanelSettingTab } from "../../src/settings/tab"; +import { CodexPanelSettingTab } from "../../src/settings/tab.obsidian"; import { notices } from "../mocks/obsidian"; import { deferred } from "../support/async"; import { installObsidianDomShims } from "../support/dom"; diff --git a/tests/shared/ui/textarea-caret.test.ts b/tests/shared/ui/textarea-caret.test.ts index f3491d80..75358f0b 100644 --- a/tests/shared/ui/textarea-caret.test.ts +++ b/tests/shared/ui/textarea-caret.test.ts @@ -2,7 +2,7 @@ import { afterEach, describe, expect, it, vi } from "vitest"; -import { textareaCursorAtVisualBoundary } from "../../../src/shared/ui/textarea-caret"; +import { textareaCursorAtVisualBoundary } from "../../../src/shared/ui/textarea-caret.measure"; import { installObsidianDomShims } from "../../support/dom"; installObsidianDomShims();