diff --git a/.changeset/fn-200-unified-chat-model-thinking-popover.md b/.changeset/fn-200-unified-chat-model-thinking-popover.md new file mode 100644 index 0000000000..531b8a08c1 --- /dev/null +++ b/.changeset/fn-200-unified-chat-model-thinking-popover.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": minor +--- + +summary: Unify Task Chat model and thinking controls in one Brain popover. +category: feature +dev: ChatThinkingLevelControl now supports model-only targeting, picker labels, target identity, default targets, and echo-safe dismissal. diff --git a/docs/dashboard-guide.md b/docs/dashboard-guide.md index a536d7637c..68708d1e74 100644 --- a/docs/dashboard-guide.md +++ b/docs/dashboard-guide.md @@ -276,7 +276,7 @@ On mobile viewports, the Right Dock never renders. The compact Header actions an ## Task-detail Chat -Task-detail **Chat** uses the project’s configured Direct Chat default model and thinking level rather than the task’s planning model. It remains task-aware: the server builds the task definition, dependencies, activity, metrics, steering, and refinement context, and the existing `task-planner:` session keeps one transcript per task. Task context always comes from the selected project’s authoritative store, including while that project’s engine has not started or is unavailable; another project’s task with the same ID cannot supply its context. The composer exposes the same model and thinking controls as Direct Chat; its compact model trigger opens the same readable, viewport-clamped menu on desktop and mobile. Model choices remain model-targeted and do not replace the synthetic task-scoped permission contract. Changing the project default does not hide history, and the next explicit send applies the current target to the existing idle session. There is no separate planner-model lane for this conversation. +Task-detail **Chat** uses the project’s configured Direct Chat default model and thinking level rather than the task’s planning model. It remains task-aware: the server builds the task definition, dependencies, activity, metrics, steering, and refinement context, and the existing `task-planner:` session keeps one transcript per task. Task context always comes from the selected project’s authoritative store, including while that project’s engine has not started or is unavailable; another project’s task with the same ID cannot supply its context. Its single **Brain** popover holds the model picker and thinking-level list in the same readable, viewport-clamped control on desktop and mobile. Task Chat remains model-only, so choices do not replace the synthetic task-scoped permission contract with a Direct Chat agent. Selecting a model keeps the popover open for a thinking-level choice; selecting a thinking level, switching conversations, or receiving a target change that was not selected closes it. Changing the project default does not hide history, and the next explicit send applies the current target to the existing idle session. There is no separate planner-model lane for this conversation. ## Chat message editing and rewind @@ -806,7 +806,7 @@ Mailbox Inbox, Outbox, and agent lists exclude archived correspondence and unrea -- A small **Brain**-icon button next to the composer's attach button lets you change an already-created direct chat session's target and thinking level mid-conversation, without starting a new chat. Its **Model / Agent** section can switch the session to another model via the shared model picker or to a real agent from the agent list; its **Thinking level** section still lists the six thinking levels plus **Default** (clear/inherit, labeled with the current resolved default such as **Default (medium)**). Each selection persists immediately and applies starting with the session's next send, including on mobile/tablet touch viewports and narrow floating Chat windows or compact docks where the popup stays fitted to the chat surface. This control appears only for non-CLI Direct sessions — it is not shown for CLI-agent-backed sessions or in Chat Rooms, neither of which support this per-session retargeting control. +- A small **Brain**-icon button next to the composer's attach button lets you change an already-created direct chat session's target and thinking level mid-conversation, without starting a new chat. Its **Model / Agent** section can switch the session to another model via the shared model picker or to a real agent from the agent list; its **Thinking level** section still lists the six thinking levels plus **Default** (clear/inherit, labeled with the current resolved default such as **Default (medium)**). Selecting a model or agent keeps the popover open so you can choose a thinking level in the same interaction. Selecting a thinking level deliberately closes it; switching to another conversation or receiving a target change that did not echo your selection also closes it to avoid stale options. Each selection persists immediately and applies starting with the session's next send, including on mobile/tablet touch viewports and narrow floating Chat windows or compact docks where the popup stays fitted to the chat surface. This control appears only for non-CLI Direct sessions — it is not shown for CLI-agent-backed sessions or in Chat Rooms, neither of which support this per-session retargeting control. - Full Chat and Quick Chat both consume the same streamed `/api/chat/sessions/:id/messages` response contract, and both now prefer the authoritative assistant `message` snapshot on `done` while still accumulating `text` chunks when present (so providers without incremental text streaming still render output immediately) - Stopping a Direct/Quick Chat or task Chat model-loop response retains any non-empty text already streamed as one interrupted assistant conversation message, including after refresh/remount and in the next turn's file-backed model context. Chat Rooms and CLI-agent-backed chat sessions are excluded from this model-loop continuity contract. @@ -1618,7 +1618,7 @@ For related global/project configuration behavior, see [Settings reference](./se Inspect task definition, logs, review feedback, comments, artifacts, workflow outcomes, model overrides, and task routing from a single modal. - Editable tasks with descriptions show **Summarize as title** beside the read-mode title; it asks AI to generate a concise title from the description and saves it without opening the edit form. -- The top-level **Chat** tab appears first for active task details and is the default landing tab for non-`done` tasks. It uses the project Direct Chat default model and thinking level, and exposes reusable model/thinking selectors without impersonating a Direct Chat agent. Opening the tab is lookup-only: Fusion creates the task-scoped Chat session only after you send a composer message, starter prompt, or question answer. Once a user message exists, the resumable planner chat can appear in the global Chat list; interacted chats are kept when the task reaches `done` and removed when the task is archived. Each send includes bounded server-built task context so the planner can answer current status, progress, recent activity, dependency, and task definition questions. It shows starter prompts for common planning questions, can render structured planner questions, and converts only explicit operator steering intent through the scoped steering tool. The composer stays pinned while the transcript, loading, error, starter, history, and streaming states scroll internally; on mobile/narrow task detail, the default focused Chat layout hides nonessential title/metadata/tab/action rows until you collapse it from the in-view expand control. +- The top-level **Chat** tab appears first for active task details and is the default landing tab for non-`done` tasks. It uses the project Direct Chat default model and thinking level, and exposes one Brain popover with model-only targeting and thinking-level selection without impersonating a Direct Chat agent. Opening the tab is lookup-only: Fusion creates the task-scoped Chat session only after you send a composer message, starter prompt, or question answer. Once a user message exists, the resumable planner chat can appear in the global Chat list; interacted chats are kept when the task reaches `done` and removed when the task is archived. Each send includes bounded server-built task context so the planner can answer current status, progress, recent activity, dependency, and task definition questions. It shows starter prompts for common planning questions, can render structured planner questions, and converts only explicit operator steering intent through the scoped steering tool. The composer stays pinned while the transcript, loading, error, starter, history, and streaming states scroll internally; on mobile/narrow task detail, the default focused Chat layout hides nonessential title/metadata/tab/action rows until you collapse it from the in-view expand control. - The **Activity → Live**, **Feed**, and **Raw Logs** segments remain immediately after **Chat** and share an expand/collapse control that lets the active Activity segment fill the task-detail modal, then restores the normal header, tabs, and action footer when collapsed. - The **Summary** tab appears for `done` tasks and remains their default landing tab. It shows the recorded completion summary, the **Merge Details** card (merge status, commit, PR, timestamp, and message), changed-file/merge stats when available, completed steps, workflow results, retry counts, and a token usage & cost section broken down by model from the already-loaded task detail; unpriced models show cost as unavailable rather than `$0`. @@ -1718,7 +1718,7 @@ Recommended workflow: ordinary chains stay as `Blocks N` so noise stays low, hig The **Activity** tab is the first task-detail tab by default and presents **Live**, **Feed**, and **Raw Logs** as a segmented control on wider layouts and as a fixed, root-portaled **Activity views** dropdown on narrow mobile layouts. The dropdown stays above its owning task-detail modal or task popup and follows the Activity tab while a popup is dragged or resized. Live contains the live, chat-styled transcript of task agent output. Consecutive entries are grouped by role and labeled as Planner, Executor, Reviewer, or Merger; legacy log rows without an agent role use the neutral Agent fallback. Agent group headers and user message headers show a small muted relative timestamp (for example, “just now”, “1m ago”, or “2h ago”) based on the transcript timestamp, while agent group metadata still includes the entry count. Consecutive text/message chunks inside a role group render as one continuous markdown bubble, while consecutive tool/tool-result/tool-error rows collapse into one expandable, compact tool-call summary that stays collapsed by default and mirrors regular Chat's dense treatment; the summary stays single-line/ellipsis-friendly on desktop and mobile, counts tool invocations, lists deduped tool names with overflow, and shows an error count when failures are present, while the expanded body pairs each call with its result or error in dense entry cards. Thinking entries render in a collapsible block that starts expanded for `in-progress` and `in-review` tasks so active reasoning is visible at a glance; blocks start collapsed for other task columns and remain user-toggleable in every state. The transcript opens at the latest output whenever the tab loads or becomes active, then follows new live output when you are already near the bottom while preserving your scroll position when you review older messages. When older task-agent history exists, scrolling to the top or selecting **Load previous messages** prepends earlier transcript entries without moving the message you were reading. When you scroll away from the bottom of a populated transcript, a sticky **Latest** button appears inside the transcript so you can jump back to the newest message and resume live follow. For non-`done` tasks, the Activity Live composer sends typed guidance through the same steering path used by comments, including active planning/triage, `in-progress`, and `in-review` sessions, plus live CLI-agent sessions reported by the session bridge; an `in-review` Activity Live message or Comments-tab task comment re-engages an executor unless an open PR blocks moving the task back, and other messages are still saved as queued guidance when no session is currently live. Feed and Raw Logs do not show the composer. On a `done` task, the same composer starts a refinement task using the typed text as feedback and shows a success toast with the new task ID, while the current task detail modal remains on the completed task. The task-detail Activity Live segment keeps the composer pinned and visible on mobile and desktop while the transcript scrolls internally; its textarea placeholder reads “Steer the currently executing agent” for steering mode and switches to refinement copy for completed tasks, with the same inline, icon-only send affordance to the right of the input at every breakpoint. In the composer, plain **Enter** sends, **Shift+Enter** inserts a newline, and **Cmd/Ctrl+Enter** remains a supported send shortcut. -The top-level **Chat** tab opens a task-aware Chat conversation for the same task instead of posting steering comments. It targets the project Direct Chat default model and thinking level, with the same model and thinking selectors available in general Chat; it retains the synthetic `task-planner:` session so server-built task context and scoped tools remain intact. It appears after Activity by default, or before Activity when **Settings → Appearance → Open task details with Chat first** is enabled. Each send includes server-built, bounded context for the task id, status/column/progress/current step, dependencies, recent activity/comment excerpts, prompt/plan content, and available source/review state; unavailable sections are labeled so the planner states uncertainty rather than inventing execution evidence. Opening the tab with no existing history does not create a database chat row; when no task-Chat history is found, Chat shows a guided empty state with starter prompts for recent activity, current status/blockers, next best action, and plan/definition review. Selecting a starter creates/resumes the task Chat session and sends that prompt as an ordinary chat message through the task-context-aware task-Chat composer/stream path, including for completed tasks. On live tasks, clear bounded implementation-change requests are routed to task steering; on `done` tasks, clear follow-up implementation or improvement requests are routed through a task-scoped planner refinement tool that calls the same refinement creation path as the completed-task Activity composer. The starter prompts disappear while history is loading or after conversation history exists, so Activity Live, Feed, Raw Logs, and the steering/refinement composer remain separate. Task Chat uses the same standard chat bubble, markdown/plain assistant rendering, thinking details, tool-call/question cards, and mobile first-tap send/stop affordance as the main Chat view while keeping task-scoped planner sessions separate. Planner Chat defaults to focused mode, keeps its composer visible at the bottom while only the transcript scrolls, and on narrow/mobile task-detail layouts collapses nonessential rows above the chat until the user selects the Chat collapse control. +The top-level **Chat** tab opens a task-aware Chat conversation for the same task instead of posting steering comments. It targets the project Direct Chat default model and thinking level through one Brain popover with model-only targeting; it retains the synthetic `task-planner:` session so server-built task context and scoped tools remain intact. It appears after Activity by default, or before Activity when **Settings → Appearance → Open task details with Chat first** is enabled. Each send includes server-built, bounded context for the task id, status/column/progress/current step, dependencies, recent activity/comment excerpts, prompt/plan content, and available source/review state; unavailable sections are labeled so the planner states uncertainty rather than inventing execution evidence. Opening the tab with no existing history does not create a database chat row; when no task-Chat history is found, Chat shows a guided empty state with starter prompts for recent activity, current status/blockers, next best action, and plan/definition review. Selecting a starter creates/resumes the task Chat session and sends that prompt as an ordinary chat message through the task-context-aware task-Chat composer/stream path, including for completed tasks. On live tasks, clear bounded implementation-change requests are routed to task steering; on `done` tasks, clear follow-up implementation or improvement requests are routed through a task-scoped planner refinement tool that calls the same refinement creation path as the completed-task Activity composer. The starter prompts disappear while history is loading or after conversation history exists, so Activity Live, Feed, Raw Logs, and the steering/refinement composer remain separate. Task Chat uses the same standard chat bubble, markdown/plain assistant rendering, thinking details, tool-call/question cards, and mobile first-tap send/stop affordance as the main Chat view while keeping task-scoped planner sessions separate. Planner Chat defaults to focused mode, keeps its composer visible at the bottom while only the transcript scrolls, and on narrow/mobile task-detail layouts collapses nonessential rows above the chat until the user selects the Chat collapse control. The **Raw Logs** segment is designed for debugging long-running and tool-heavy sessions, while legacy links that requested the former top-level Logs tab land on Activity → Feed: diff --git a/packages/dashboard/app/components/ChatThinkingLevelControl.tsx b/packages/dashboard/app/components/ChatThinkingLevelControl.tsx index 84f76635ac..2d777e1df0 100644 --- a/packages/dashboard/app/components/ChatThinkingLevelControl.tsx +++ b/packages/dashboard/app/components/ChatThinkingLevelControl.tsx @@ -22,8 +22,8 @@ introducing a parallel thinking-level list. FNXC:Chat-ThinkingLevel 2026-07-12-20:08: The Default entry must describe the resolved project/global default supplied by ChatView, while omitted props preserve the legacy isolated fallback label `Default (off)`. -FNXC:Chat-ModelSwitch 2026-07-12-00:00: -The same brain-icon popup now owns active direct-session targeting too: model-loop sessions can switch provider/model via CustomModelDropdown, and agent sessions can switch to a real agent from the existing list. Selecting either closes the popup and persists immediately through useChat.setSessionModel, while CLI composers stay gated in ChatView. +FNXC:Chat-ModelSwitch 2026-08-27-12:03: +Task Chat reuses this one brain-icon popover with model-only targeting, so selecting its model can never impersonate a durable agent. Direct Chat retains the agent lane; hosts that opt out of it render only the model picker and the shared thinking-level list. FNXC:Chat-ThinkingLevel 2026-07-16-00:34: FN-8030 lets room composers reuse this control with showTargetSection={false}. A room's thinking effort is the default reasoning effort for every responder, and rooms have no per-composer model or agent target to switch. @@ -44,6 +44,16 @@ export interface ChatThinkingLevelControlProps { defaultThinkingLevel?: string; /** Show direct-chat model/agent targeting controls; rooms render only the thinking-level list. */ showTargetSection?: boolean; + /** Keep the direct-chat agent lane visible; model-only hosts never render agent controls. */ + showAgentTarget?: boolean; + /** Optional accessible label forwarded to the embedded model picker. */ + modelPickerLabel?: string; + /** Optional inherited/default entry label forwarded to the embedded model picker. */ + modelDefaultOptionLabel?: string; + /** Conversation identity; omitted and null are both a stable legacy identity. */ + targetKey?: string | null; + /** Concrete target that this host applies when the picker chooses its default entry. */ + defaultModelValue?: string; models?: ModelInfo[]; favoriteProviders?: string[]; favoriteModels?: string[]; @@ -57,11 +67,19 @@ export interface ChatThinkingLevelControlProps { type TargetMode = "model" | "agent"; +type TargetExpectation = { agent: string; model: string }; +type TargetSnapshot = TargetExpectation & { key: string | null; level: string }; + export function ChatThinkingLevelControl({ level, onChange, defaultThinkingLevel = "off", showTargetSection = true, + showAgentTarget = true, + modelPickerLabel, + modelDefaultOptionLabel, + targetKey, + defaultModelValue, models = [], favoriteProviders = [], favoriteModels = [], @@ -74,11 +92,19 @@ export function ChatThinkingLevelControl({ }: ChatThinkingLevelControlProps) { const { t } = useTranslation("app"); const [open, setOpen] = useState(false); - const [targetMode, setTargetMode] = useState(() => (agentId && agentId !== FN_AGENT_ID ? "agent" : "model")); + const [targetMode, setTargetMode] = useState(() => (showAgentTarget && agentId && agentId !== FN_AGENT_ID ? "agent" : "model")); const rootRef = useRef(null); const normalizedLevel = level ?? ""; const currentModelValue = modelProvider && modelId ? `${modelProvider}/${modelId}` : ""; const selectedAgentId = agentId && agentId !== FN_AGENT_ID ? agentId : ""; + const normalizedTargetKey = targetKey ?? null; + const pendingTargetRef = useRef(null); + const lastTargetSnapshotRef = useRef({ + key: normalizedTargetKey, + level: normalizedLevel, + agent: selectedAgentId, + model: currentModelValue, + }); const selectedModel = useMemo(() => { if (!showTargetSection || selectedAgentId || !currentModelValue) return undefined; const slashIdx = currentModelValue.indexOf("/"); @@ -103,6 +129,7 @@ export function ChatThinkingLevelControl({ */ const clickedInsideRoot = rootRef.current?.contains(target); if (!clickedInsideRoot && !isInsidePortaledModelMenu(target)) { + pendingTargetRef.current = null; setOpen(false); } }; @@ -114,13 +141,43 @@ export function ChatThinkingLevelControl({ }; }, [open]); - // Close the popup whenever the underlying level or target changes out from under us - // (e.g. the active session switched) so it never leaks open across a - // session switch showing the previous session's options. + /* + FNXC:Chat-ModelSwitch 2026-08-27-12:03: + Drop stale options when a host changes the conversation or target underneath an open popover. + Conversation identity is structural rather than inferred from target values because another + conversation can carry the exact target just selected. Within one identity, only an exact, + single-use target echo remains open; every other prop change closes it. Thinking-level picks + are the only deliberate selection that dismisses the popover. + */ useEffect(() => { - setOpen(false); - setTargetMode(selectedAgentId ? "agent" : "model"); - }, [normalizedLevel, selectedAgentId, currentModelValue]); + const previous = lastTargetSnapshotRef.current; + const next: TargetSnapshot = { + key: normalizedTargetKey, + level: normalizedLevel, + agent: selectedAgentId, + model: currentModelValue, + }; + const targetKeyMoved = previous.key !== next.key; + const targetMoved = previous.agent !== next.agent || previous.model !== next.model; + const levelMoved = previous.level !== next.level; + + if (targetKeyMoved) { + pendingTargetRef.current = null; + setOpen(false); + } else if (targetMoved) { + const pending = pendingTargetRef.current; + pendingTargetRef.current = null; + if (!pending || pending.agent !== next.agent || pending.model !== next.model) { + setOpen(false); + } + } else if (levelMoved) { + pendingTargetRef.current = null; + setOpen(false); + } + + lastTargetSnapshotRef.current = next; + setTargetMode(showAgentTarget && selectedAgentId ? "agent" : "model"); + }, [currentModelValue, normalizedLevel, normalizedTargetKey, selectedAgentId, showAgentTarget]); const selectedAgent = useMemo( () => agents.find((agent) => agent.id === selectedAgentId), @@ -135,20 +192,28 @@ export function ChatThinkingLevelControl({ }; const chooseLevel = (value: string) => { + pendingTargetRef.current = null; setOpen(false); void onChange(value); }; + const armTargetExpectation = (candidate: TargetExpectation) => { + if (!onChangeModel || (candidate.agent === selectedAgentId && candidate.model === currentModelValue)) return; + pendingTargetRef.current = candidate; + }; + const chooseModel = (value: string) => { const slashIdx = value.indexOf("/"); - if (slashIdx <= 0) return; - setOpen(false); - void onChangeModel?.({ modelProvider: value.slice(0, slashIdx), modelId: value.slice(slashIdx + 1) }); + if (value !== "" && (slashIdx <= 0 || slashIdx === value.length - 1)) return; + armTargetExpectation({ agent: "", model: value === "" ? defaultModelValue ?? "" : value }); + void onChangeModel?.(value === "" + ? { modelProvider: null, modelId: null } + : { modelProvider: value.slice(0, slashIdx), modelId: value.slice(slashIdx + 1) }); }; const chooseAgent = (nextAgentId: string) => { if (!nextAgentId) return; - setOpen(false); + armTargetExpectation({ agent: nextAgentId, model: "" }); void onChangeModel?.({ agentId: nextAgentId }); }; @@ -166,6 +231,7 @@ export function ChatThinkingLevelControl({ const handleTriggerKeyDown = (event: KeyboardEvent) => { if (event.key === "Escape") { + pendingTargetRef.current = null; setOpen(false); } }; @@ -173,6 +239,7 @@ export function ChatThinkingLevelControl({ const handleOptionKeyDown = (event: KeyboardEvent, value: string) => { if (event.key === "Escape") { event.preventDefault(); + pendingTargetRef.current = null; setOpen(false); return; } @@ -185,6 +252,7 @@ export function ChatThinkingLevelControl({ const handleAgentKeyDown = (event: KeyboardEvent, nextAgentId: string) => { if (event.key === "Escape") { event.preventDefault(); + pendingTargetRef.current = null; setOpen(false); return; } @@ -206,7 +274,10 @@ export function ChatThinkingLevelControl({ aria-label={t("chat.thinkingLevelButton", "Thinking level")} title={t("chat.thinkingLevelButton", "Thinking level")} disabled={disabled} - onClick={() => setOpen((value) => !value)} + onClick={() => setOpen((value) => { + if (value) pendingTargetRef.current = null; + return !value; + })} onKeyDown={handleTriggerKeyDown} > @@ -215,47 +286,50 @@ export function ChatThinkingLevelControl({ {open ? (
{showTargetSection ? ( -
-
{t("chat.modelAgentSection", "Model / Agent")}
-
- - -
+
+
{showAgentTarget ? t("chat.modelAgentSection", "Model / Agent") : t("chat.newChatModeModel", "Model")}
+ {showAgentTarget ? ( +
+ + +
+ ) : null} - {targetMode === "model" ? ( + {targetMode === "model" || !showAgentTarget ? (
)} - {selectedAgent ? ( + {showAgentTarget && selectedAgent ? (
{t("chat.currentAgentTarget", "Current agent: {{name}}", { name: selectedAgent.name || selectedAgent.id })}
diff --git a/packages/dashboard/app/components/ChatView.tsx b/packages/dashboard/app/components/ChatView.tsx index 63926ba1d5..7f21dde06c 100644 --- a/packages/dashboard/app/components/ChatView.tsx +++ b/packages/dashboard/app/components/ChatView.tsx @@ -2937,6 +2937,7 @@ export function ChatView({ projectId, addToast, floating = false, compactLayout agentId={activeSession?.agentId} modelProvider={activeSession?.modelProvider} modelId={activeSession?.modelId} + targetKey={activeSession?.id ?? null} onChange={(level) => { if (activeSession) { void setSessionThinkingLevel(activeSession.id, level); diff --git a/packages/dashboard/app/components/TaskPlannerChatTab.css b/packages/dashboard/app/components/TaskPlannerChatTab.css index 286905f65b..85046e7894 100644 --- a/packages/dashboard/app/components/TaskPlannerChatTab.css +++ b/packages/dashboard/app/components/TaskPlannerChatTab.css @@ -211,29 +211,11 @@ FNXC:TaskDetailPlannerChat 2026-07-07-00:00: The Planner Chat streaming Stop button must occupy the same width footprint as the Send button it replaces (no shift/shrink on swap) and mirror the regular Chat view's stop-button sizing (`.chat-input-row`'s `--chat-input-control-size` in ChatView.css). The shared `.chat-input-send` / `.chat-input-stop` classes read that custom property from `.chat-input-row`, which the Planner composer never renders inside of, so the property was undefined here and `width` fell back to `auto`, sizing each button from its own content only. Declare the same control-size formula scoped to `.task-planner-chat-composer` and give `.task-planner-chat-send` (present on both the send and stop button variants) a matching `min-inline-size` floor so neither button can render narrower than the other on desktop, without touching ChatView.css's own token. */ /* -FNXC:TaskChatDefaultModel 2026-08-19-12:12: -Task Chat keeps model and thinking controls reachable beside the composer, reusing the Direct Chat primitives instead of adding a task-only selector style. The compact control row wraps on narrow layouts so the task transcript and send affordance remain usable. +FNXC:TaskChatDefaultModel 2026-08-27-12:03: +Task Chat reuses the shared brain popover for model-only targeting and thinking level, so the +composer has one control without an agent-impersonation lane. Its positioned full-width ancestor +keeps the wider shared popover inside the narrower task panel at every breakpoint. */ -.task-planner-chat-target-controls { - display: flex; - flex: 0 1 auto; - align-items: center; - gap: var(--space-xs); - min-width: 0; -} - -.task-planner-chat-target-controls .model-combobox { - min-width: 0; - max-width: calc(var(--space-2xl) * 8); -} - -.task-planner-chat-target-controls .model-combobox-trigger { - max-width: 100%; -} - -.task-planner-chat-target-controls .chat-thinking-level-root { - flex: 0 0 auto; -} /* FNXC:ChatMemoryFocusSelector 2026-08-24-03:40: @@ -256,6 +238,7 @@ composer ancestor. The chip inherits its control-size token from the composer be /* FN-7634: same formula as ChatView.css's `.chat-input-row { --chat-input-control-size: … }` so the Planner stop/send buttons share the regular Chat view's control-size floor. */ --chat-input-control-size: calc(var(--space-lg) * 2.5); + position: relative; display: flex; flex: 0 0 auto; flex-wrap: wrap; @@ -263,6 +246,18 @@ composer ancestor. The chip inherits its control-size token from the composer be gap: var(--space-sm); } +.task-planner-chat-composer .chat-thinking-level-root { + position: static; +} + +.task-planner-chat-composer .chat-thinking-popover { + left: var(--space-md); + right: var(--space-md); + width: auto; + max-width: none; + max-inline-size: none; +} + /* FNXC:ChatComposer 2026-08-20-19:25: FN-076 requires Planner Chat to grow automatically through five rendered lines, internally scroll excess content, and shrink after deletion. Native mouse resizing is disabled and CSS leaves the controller's measured cap authoritative. @@ -317,17 +312,6 @@ height rather than relying on the global button minimum. gap: var(--space-xs); } - .task-planner-chat-target-controls { - flex: 1 1 100%; - order: 0; - justify-content: flex-end; - } - - .task-planner-chat-target-controls .model-combobox { - flex: 1 1 auto; - max-width: none; - } - .task-planner-chat-input { flex: 1 1 auto; min-width: 0; diff --git a/packages/dashboard/app/components/TaskPlannerChatTab.tsx b/packages/dashboard/app/components/TaskPlannerChatTab.tsx index 455d3da736..79222ff1aa 100644 --- a/packages/dashboard/app/components/TaskPlannerChatTab.tsx +++ b/packages/dashboard/app/components/TaskPlannerChatTab.tsx @@ -14,7 +14,6 @@ import { parseQuestionToolCall, type ParsedQuestionToolCall } from "../utils/par import { ChatQuestionResponse } from "./ChatQuestionResponse"; import { PendingChatMessageQueue } from "./PendingChatMessageQueue"; import { ProviderIcon } from "./ProviderIcon"; -import { CustomModelDropdown } from "./CustomModelDropdown"; import { ChatThinkingLevelControl } from "./ChatThinkingLevelControl"; import { useModelsCache } from "../hooks/useModelsCache"; import { StandardChatActionButton, StandardChatMessageItem, StandardStreamingMessage, formatModelTag } from "./StandardChatSurface"; @@ -1660,32 +1659,26 @@ export function TaskPlannerChatTab({ task, columnFlags, projectId, active, expan
)}
-
- void handleTaskChatModelChange(value)} - placeholder={t("model.selectPlaceholder", "Select a model…")} - defaultOptionLabel={t("models.useDefault", "Use project default")} - /* - FNXC:TaskChatModelMenu 2026-08-21-01:12: - Task Chat keeps its compact composer trigger, but long provider/model names need Direct Chat's readable, viewport-clamped portaled menu on desktop and mobile. - */ - menuWidth="readable" - favoriteProviders={favoriteProviders} - favoriteModels={favoriteModels} - disabled={queueActionPending || composerState === "sending"} - /> - void handleTaskChatThinkingChange(level)} - disabled={queueActionPending || composerState === "sending"} - /> -
+ void handleTaskChatThinkingChange(level)} + onChangeModel={(selection) => void handleTaskChatModelChange( + selection.modelProvider && selection.modelId ? `${selection.modelProvider}/${selection.modelId}` : "", + )} + disabled={queueActionPending || composerState === "sending"} + />