From 75ee797ed97ce7bbb8747a6bcf4bd65ada0ffddf Mon Sep 17 00:00:00 2001 From: murashit Date: Fri, 5 Jun 2026 11:31:24 +0900 Subject: [PATCH] Add composer status interactions --- src/features/chat/chat-composer-controller.ts | 12 + .../chat/chat-view-controller-assembly.ts | 3 + src/features/chat/ui/composer.tsx | 245 ++++++++++++++++-- src/features/chat/view-model.ts | 50 +++- src/features/chat/view-snapshot.ts | 6 + src/features/chat/view.ts | 10 +- src/styles/33-chat-composer.css | 67 ++++- .../chat/chat-composer-controller.test.ts | 54 ++++ .../chat/ui/renderers/composer.test.ts | 123 ++++++++- tests/features/chat/view-connection.test.ts | 3 + tests/features/chat/view-model.test.ts | 4 + tests/styles.test.ts | 34 ++- 12 files changed, 571 insertions(+), 40 deletions(-) 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 ( -
-