diff --git a/src/features/chat/chat-composer-controller.ts b/src/features/chat/chat-composer-controller.ts index 572aeab8..7e6de384 100644 --- a/src/features/chat/chat-composer-controller.ts +++ b/src/features/chat/chat-composer-controller.ts @@ -29,6 +29,9 @@ export interface ChatComposerControllerOptions { composerPlaceholder: () => string; composerMeta: () => ComposerMetaViewModel; currentModelForSuggestions: () => string | null; + togglePlan: () => void; + toggleAutoReview: () => void; + toggleFast: () => void; renderIfDetached: () => void; onDraftChange: () => void; onComposerResize: () => void; @@ -108,6 +111,15 @@ export class ChatComposerController { onSendOrInterrupt: () => { this.options.onSubmit(); }, + onTogglePlan: () => { + this.options.togglePlan(); + }, + onToggleAutoReview: () => { + this.options.toggleAutoReview(); + }, + onToggleFast: () => { + this.options.toggleFast(); + }, onSuggestionHover: (index) => { this.selectSuggestion(index); }, diff --git a/src/features/chat/chat-view-controller-assembly.ts b/src/features/chat/chat-view-controller-assembly.ts index d871e334..688c329d 100644 --- a/src/features/chat/chat-view-controller-assembly.ts +++ b/src/features/chat/chat-view-controller-assembly.ts @@ -253,6 +253,9 @@ export function createChatViewControllerAssembly(host: ChatViewControllerAssembl composerPlaceholder: host.composerPlaceholder, composerMeta: host.composerMetaViewModel, currentModelForSuggestions: () => currentModel(host.runtimeSnapshot()), + togglePlan: () => void runtimeSettings.toggleCollaborationMode(), + toggleAutoReview: () => void runtimeSettings.toggleAutoReview(), + toggleFast: () => void runtimeSettings.toggleFastMode(), renderIfDetached: host.effects.render.now, onDraftChange: host.effects.liveState.refresh, onComposerResize: () => { diff --git a/src/features/chat/ui/composer.tsx b/src/features/chat/ui/composer.tsx index 5394b53a..08d6185f 100644 --- a/src/features/chat/ui/composer.tsx +++ b/src/features/chat/ui/composer.tsx @@ -1,9 +1,11 @@ +import { setIcon } from "obsidian"; import type { ButtonHTMLAttributes, ComponentChild as UiNode, Ref } from "preact"; -import { useLayoutEffect, useRef } from "preact/hooks"; +import { useLayoutEffect, useRef, useState } from "preact/hooks"; import type { ComposerSuggestion } from "../composer/suggestions"; import type { ComposerMetaViewModel } from "../view-model"; -import { IconButton, ObsidianIcon } from "../../../shared/ui/components"; +import type { ToolbarChoice } from "../toolbar-model"; +import { IconButton } from "../../../shared/ui/components"; import { renderUiRoot } from "../../../shared/ui/ui-root"; import { syncTextareaHeight } from "../../../shared/ui/textarea-autogrow"; @@ -17,6 +19,9 @@ export interface ComposerCallbacks { onUpdateSuggestions: () => void; onKeydown: (event: KeyboardEvent) => void; onSendOrInterrupt: () => void; + onTogglePlan?: () => void; + onToggleAutoReview?: () => void; + onToggleFast?: () => void; onSuggestionHover: (index: number) => void; onSuggestionInsert: (suggestion: ComposerSuggestion) => void; } @@ -36,11 +41,14 @@ const DEFAULT_COMPOSER_META: ComposerMetaViewModel = { ], percent: "--%", }, + statusSummary: "Context unavailable, plan off, auto-review off, fast off, model default, reasoning effort default", model: "default", effort: null, planActive: false, autoReviewActive: false, fastActive: false, + modelChoices: [], + effortChoices: [], }; export function renderComposerShell( @@ -143,7 +151,7 @@ function ComposerShell({ callbacks.onKeydown(event); }} /> - + void; + callbacks: ComposerCallbacks; }): UiNode { + const metaRef = useRef(null); const statusRef = useRef(null); + const modelTriggerRef = useRef(null); + const effortTriggerRef = useRef(null); + const [picker, setPicker] = useState(null); useLayoutEffect(() => { const status = statusRef.current; if (!status) return; @@ -190,40 +202,134 @@ function ComposerMeta({ win.removeEventListener("resize", scheduleUpdate); }; }, [meta]); + useLayoutEffect(() => { + if (!picker) return; + const metaRoot = metaRef.current; + const doc = metaRoot?.ownerDocument; + if (!metaRoot || !doc) return; + const closeOnOutsideMouse = (event: MouseEvent) => { + if (event.target && metaRoot.contains(event.target as Node)) return; + setPicker(null); + }; + const closeOnEscape = (event: KeyboardEvent) => { + if (event.key === "Escape") setPicker(null); + }; + doc.addEventListener("mousedown", closeOnOutsideMouse); + doc.addEventListener("keydown", closeOnEscape); + return () => { + doc.removeEventListener("mousedown", closeOnOutsideMouse); + doc.removeEventListener("keydown", closeOnEscape); + }; + }, [picker]); + const openPicker = (kind: ComposerMetaPickerKind) => { + const nextPicker = composerMetaPickerState( + kind, + kind === "model" ? modelTriggerRef.current : effortTriggerRef.current, + metaRef.current, + ); + setPicker((current) => (current?.kind === kind ? null : nextPicker)); + }; + const closePicker = () => { + setPicker(null); + }; if (meta.fatal) { return (
{meta.fatal} - +
); } return ( -
-