FN-7907: Add project chat default (model/agent) setting and New Chat default behavior
Adds a project-scoped Direct-chat default (model pair or durable agent) plus a New Chat behavior toggle (prompt vs. always-use-default), surfaced in Project Models settings using the standard model dropdown. - Add ProjectSettings fields: chatNewSessionMode, chatDefaultKind, chatDefaultAgentId, chatDefaultModelProvider, chatDefaultModelId, chatDefaultThinkingLevel - Extend settings-schema validation for the new chat default fields - Add a "Chat" subsection to ProjectModelsSection with New Chat behavior selector, model/agent target toggle, and the standard CustomModelDropdown for model selection - Update ChatView.tsx handleNewChat() flow to honor the configured default (prompt vs. immediate session creation) - Add i18n strings across en/es/fr/ko/zh-CN/zh-TW locales and regenerate resources.d.ts - Add changeset (@runfusion/fusion minor) and update docs/settings-reference.md and docs/dashboard-guide.md - Add settings-parity, ChatView new-chat-default, and ProjectModelsSection chatDefault test coverage Files changed: .changeset/fn-7907-chat-default.md | 7 + docs/dashboard-guide.md | 3 +- docs/settings-reference.md | 8 + packages/core/src/__tests__/settings-parity.test.ts | 20 ++ packages/core/src/settings-schema.ts | 6 + packages/core/src/types.ts | 15 + packages/dashboard/app/components/ChatView.tsx | 93 +++++- packages/dashboard/app/components/__tests__/ChatView.new-chat-default.test.tsx | 357 +++++++++++++++++++++ packages/dashboard/app/components/settings/sections/ProjectModelsSection.tsx | 77 +++++ packages/dashboard/app/components/settings/sections/__tests__/ProjectModelsSection.chatDefault.test.tsx | 193 +++++++++++ packages/i18n/locales/en/app.json | 20 +- packages/i18n/locales/es/app.json | 20 +- packages/i18n/locales/fr/app.json | 20 +- packages/i18n/locales/ko/app.json | 20 +- packages/i18n/locales/zh-CN/app.json | 20 +- packages/i18n/locales/zh-TW/app.json | 20 +- packages/i18n/src/resources.d.ts | 18 ++ 17 files changed, 899 insertions(+), 18 deletions(-) Fusion-Task-Id: FN-7907 Fusion-Task-Lineage: 86a81b9c-7e0c-4033-9e2a-6f7380810fe9 Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
7
.changeset/fn-7907-chat-default.md
Normal file
7
.changeset/fn-7907-chat-default.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
"@runfusion/fusion": minor
|
||||
---
|
||||
|
||||
summary: Add a project Chat default (model or agent) with prompt-each-time or always-use-default New Chat behavior.
|
||||
category: feature
|
||||
dev: Adds project chatNewSessionMode/chatDefaultKind/chatDefaultAgentId/chatDefaultModelProvider/chatDefaultModelId/chatDefaultThinkingLevel settings, a Project Models "Chat" subsection, and a shared ChatView handleNewChat() flow.
|
||||
@@ -515,7 +515,8 @@ Chat view provides project-scoped conversations with agents.
|
||||
|
||||
- Entering `/new` or `/clear` (exact match after trimming) in the composer starts a fresh thread for the current chat target instead of sending the literal command to the model
|
||||
- On mobile, the New Chat and Delete Conversation dialogs use a compact inset treatment (centered, viewport-bounded, internally scrollable) instead of the app's default full-height mobile modal chrome.
|
||||
- In the New Chat dialog's **Model** mode, the model picker includes a **Thinking Level** selector. Choosing **Default** leaves the session unset so Fusion uses the project/global reasoning-effort default; choosing a concrete level stores it on that chat session and applies it to model-loop replies. The Default label shows the current resolved default (for example **Default (medium)**) and falls back to **Default (off)** when no default is configured.
|
||||
- **Settings → Project Models → Chat** controls New Chat defaults per project. Choose a default target kind (**Model** with provider/model and optional Thinking Level, or **Agent** with a durable agent id) and a mode: **Prompt for model each time** opens the New Chat dialog with that default preselected, while **Always use configured default** creates the session immediately from the resolved default. If the configured target is incomplete or missing, New Chat falls back to the dialog instead of creating an unroutable session.
|
||||
- In the New Chat dialog's **Model** mode, the model picker includes a **Thinking Level** selector. Choosing **Default** leaves the session unset so Fusion uses the project/global reasoning-effort default; choosing a concrete level stores it on that chat session and applies to model-loop replies. The Default label shows the current resolved default (for example **Default (medium)**) and falls back to **Default (off)** when no default is configured.
|
||||
<!-- FNXC:Chat-ThinkingLevel 2026-07-12-19:30: FN-7898 closes the gap left by the create-time-only picker above: an existing session's reasoning-effort level can now be changed mid-conversation from the composer itself. -->
|
||||
<!-- FNXC:Chat-ThinkingLevel 2026-07-12-20:13: FN-7905: chat thinking-level Default entries display the resolved project/global default in both the New Chat picker and in-chat Brain popup; choosing Default still clears the per-session override. -->
|
||||
- A small **Brain**-icon button next to the composer's attach button lets you change an already-created direct chat session's thinking level mid-conversation, without starting a new chat. It opens a popup listing the six thinking levels plus **Default** (clear/inherit, labeled with the current resolved default such as **Default (medium)**); the selection persists immediately and applies starting with the session's next send. This control appears only for model-loop (non-CLI) direct sessions — it is not shown for CLI-agent-backed sessions or in Chat Rooms, neither of which support a per-session thinking level.
|
||||
|
||||
@@ -697,6 +697,12 @@ GitLab configuration examples: leave both URL fields blank for GitLab.com (`http
|
||||
| `showQuickChatFAB` | `boolean` | `false` | Show floating quick-chat button (chat remains available via More menu). |
|
||||
| `quickChatCloseOnOutsideClick` | `boolean` | `true` | Close the desktop Quick Chat floating window when clicking outside it; disable to keep it open until explicitly closed. |
|
||||
| `chatAutoCleanupDays` | `0 \| 7 \| 14 \| 30 \| 60 \| 90` | `0` | Auto-cleanup retention window for idle chat sessions and chat rooms. `0` is off (default). When enabled, periodic self-healing maintenance deletes rows with `updatedAt` older than the configured day window. |
|
||||
| `chatNewSessionMode` | `"prompt" \| "always-default"` | `undefined` | Project Direct-chat New Chat behavior. Unset/`"prompt"` opens the New Chat picker with any configured chat default preselected; `"always-default"` creates a session immediately when `chatDefault*` resolves to a complete model or agent target. |
|
||||
| `chatDefaultKind` | `"model" \| "agent"` | `undefined` | Project Direct-chat default target kind. `"agent"` uses `chatDefaultAgentId`; `"model"` uses `chatDefaultModelProvider` + `chatDefaultModelId`. Incomplete defaults fall back to the picker. |
|
||||
| `chatDefaultAgentId` | `string` | `undefined` | Durable agent id used by New Chat when `chatDefaultKind` is `"agent"`. |
|
||||
| `chatDefaultModelProvider` | `string` | `undefined` | Provider for the model-mode Direct-chat default; must pair with `chatDefaultModelId`. |
|
||||
| `chatDefaultModelId` | `string` | `undefined` | Model id for the model-mode Direct-chat default; must pair with `chatDefaultModelProvider`. |
|
||||
| `chatDefaultThinkingLevel` | `ThinkingLevel` | `undefined` | Optional thinking-level override for model-mode New Chat defaults. Empty/unset inherits the resolved project/global default thinking level. |
|
||||
| `mailAutoCleanupDays` | `0 \| 7 \| 14 \| 30 \| 60 \| 90` | `0` | Auto-prune retention window for inbox/outbox mail messages. `0` is off (default). When enabled, periodic self-healing maintenance deletes `messages` rows where `updatedAt < cutoff` for the configured day window. Suggested setting: `7`. |
|
||||
| `operationalLogRetentionDays` | `0 \| 7 \| 14 \| 30 \| 60 \| 90` | `30` | Retention window for SQLite operational-log tables (`activityLog`, `runAuditEvents`, `agentHeartbeats`), terminal `agentRuns` rows (by `endedAt`), and `agentConfigRevisions` (by `createdAt`). `0` is off. Lower values mean Reliability metrics/charts and the Activity feed will not show history older than the configured window; per-task task detail history is unaffected. Periodic maintenance prunes timestamped operational-log rows older than this many days while always preserving in-flight `agentRuns` (`endedAt IS NULL`) and the most-recent `agentConfigRevisions` row per agent. |
|
||||
| `agentLogFileRetentionDays` | `number` | `0` | Retention window for per-task `.fusion/tasks/{ID}/agent-log.jsonl` files after a task is soft-deleted or archived. Periodic maintenance removes JSONL entries older than this many days; active tasks are never pruned. Set `0` to disable pruning. |
|
||||
@@ -979,6 +985,8 @@ Short-lived token bounds are enforced server-side:
|
||||
|
||||
Fusion resolves task models through workflow-backed lane values first, then global lane defaults, then the project/global default model fallback. The common workflow lanes are stored as setting values on the project's default workflow and can be edited with dropdown controls from Settings -> Project Models -> Default workflow model lanes (persisted by the Settings modal's primary Save) or from workflow editor -> Settings -> Values for declared workflow lanes and fallbacks. General-scope fallback selection remains the global Fallback Model picker in Settings -> General Models.
|
||||
|
||||
Direct-chat defaults are project-scoped and independent of task workflow lanes. Configure them in **Settings -> Project Models -> Chat**. `chatDefaultKind: "agent"` resolves only when `chatDefaultAgentId` is set; `chatDefaultKind: "model"` resolves only when both `chatDefaultModelProvider` and `chatDefaultModelId` are set, with optional `chatDefaultThinkingLevel`. If `chatNewSessionMode` is `"always-default"` and that target resolves, every New Chat entry point creates the session directly. If the target is incomplete, or the mode is unset/`"prompt"`, Fusion opens the New Chat dialog instead and preselects the resolved default when one exists.
|
||||
|
||||
Settings model lanes can also carry optional thinking/reasoning effort overrides in the same model dropdown. Primary workflow lanes declare `executionThinkingLevel`, `planningThinkingLevel`, or `validatorThinkingLevel` per `(workflow, project)`; planning/reviewer fallback lanes declare `planningFallbackThinkingLevel` and `validatorFallbackThinkingLevel`; global fallback uses `fallbackThinkingLevel`; and project title summarization fallback uses `titleSummarizerFallbackThinkingLevel`. Empty thinking values inherit through the lane/global/default chain and explicit values are cleared by the lane reset action. Runtime thinking precedence for task/workflow execution is node/step `config.thinkingLevel` > task `thinkingLevel` > workflow lane thinking override > global lane thinking override > project default thinking override > global `defaultThinkingLevel`. Model-mode Chat sessions use the same executor-lane resolver with session `thinkingLevel` in the task slot, so an empty chat-session value inherits project/global defaults while a concrete New Chat selection wins for that session. The resolved value still flows through pi.ts' existing thinking/reasoning-conflict fallback (Fusion retries without the explicit level when a provider rejects conflicting thinking parameters).
|
||||
|
||||
When the planning lane has neither `planningFallback*` nor a global `fallback*` pair configured, triage now derives an **implicit fallback** from the resolved project/global default (execution) model (FN-7719). This lets a retryable primary planner-model failure (e.g. a provider 404/429) recover via one distinct swap instead of permanently failing triage with "no fallback configured" — the operator's chosen primary planner lane is unchanged, and the implicit fallback is skipped when it would equal the primary model or when test mode is active.
|
||||
|
||||
@@ -170,6 +170,26 @@ describe("settings key parity", () => {
|
||||
expect(isGlobalSettingsKey("chatAutoCleanupDays")).toBe(false);
|
||||
});
|
||||
|
||||
it("keeps Direct-chat default target settings project-scoped with unset defaults", () => {
|
||||
const chatDefaultKeys = [
|
||||
"chatNewSessionMode",
|
||||
"chatDefaultKind",
|
||||
"chatDefaultAgentId",
|
||||
"chatDefaultModelProvider",
|
||||
"chatDefaultModelId",
|
||||
"chatDefaultThinkingLevel",
|
||||
] as const;
|
||||
|
||||
for (const key of chatDefaultKeys) {
|
||||
expect(DEFAULT_PROJECT_SETTINGS[key]).toBeUndefined();
|
||||
expect(isProjectSettingsKey(key)).toBe(true);
|
||||
expect(PROJECT_SETTINGS_KEYS).toContain(key);
|
||||
expect(isGlobalSettingsKey(key)).toBe(false);
|
||||
expect(isGlobalOnlySettingsKey(key)).toBe(false);
|
||||
expect(GLOBAL_SETTINGS_KEYS).not.toContain(key);
|
||||
}
|
||||
});
|
||||
|
||||
it("keeps room compaction defaults project-scoped with expanded retention", () => {
|
||||
expect(DEFAULT_PROJECT_SETTINGS.chatRoomRecentVerbatimMessages).toBe(25);
|
||||
expect(DEFAULT_PROJECT_SETTINGS.chatRoomCompactionFetchLimit).toBe(200);
|
||||
|
||||
@@ -641,6 +641,12 @@ export const DEFAULT_PROJECT_SETTINGS = {
|
||||
*/
|
||||
showTaskChatsInCommonFeed: false,
|
||||
chatAutoCleanupDays: 0,
|
||||
chatNewSessionMode: undefined,
|
||||
chatDefaultKind: undefined,
|
||||
chatDefaultAgentId: undefined,
|
||||
chatDefaultModelProvider: undefined,
|
||||
chatDefaultModelId: undefined,
|
||||
chatDefaultThinkingLevel: undefined,
|
||||
mailAutoCleanupDays: 0,
|
||||
operationalLogRetentionDays: 30,
|
||||
agentLogFileRetentionDays: 0,
|
||||
|
||||
@@ -4119,6 +4119,21 @@ export interface ProjectSettings {
|
||||
* Optional project default-lane thinking override used when a task does not set its own thinking level.
|
||||
*/
|
||||
defaultThinkingLevelOverride?: ThinkingLevel;
|
||||
/**
|
||||
* FNXC:ChatModels 2026-07-12-20:45:
|
||||
* Projects can pin a default Direct-chat target as either a model pair with optional thinking level or a durable agent, then choose whether New Chat prompts with that default preselected or creates the session immediately.
|
||||
*/
|
||||
chatNewSessionMode?: "prompt" | "always-default";
|
||||
/** Which configured default target kind New Chat should use or preselect. */
|
||||
chatDefaultKind?: "model" | "agent";
|
||||
/** Durable agent id used when `chatDefaultKind === "agent"`. */
|
||||
chatDefaultAgentId?: string;
|
||||
/** Model provider used when `chatDefaultKind === "model"`; must be paired with `chatDefaultModelId`. */
|
||||
chatDefaultModelProvider?: string;
|
||||
/** Model id used when `chatDefaultKind === "model"`; must be paired with `chatDefaultModelProvider`. */
|
||||
chatDefaultModelId?: string;
|
||||
/** Optional thinking-level override for the model chat default; undefined inherits the resolved project/global default. */
|
||||
chatDefaultThinkingLevel?: ThinkingLevel;
|
||||
/** Project-level AI model provider for task execution (executor agent).
|
||||
* This is the execution lane that overrides the global `executionGlobalProvider`.
|
||||
* Must be set together with `executionModelId`. Falls back to
|
||||
|
||||
@@ -280,16 +280,19 @@ export function resolveSessionProvider(
|
||||
interface NewChatDialogProps {
|
||||
projectId?: string;
|
||||
defaultModel: DefaultModelSelection;
|
||||
defaultKind?: "model" | "agent";
|
||||
defaultAgentId?: string;
|
||||
defaultThinkingLevel?: string;
|
||||
defaultSelectedThinkingLevel?: string;
|
||||
onClose: () => void;
|
||||
onCreate: (input: { agentId: string; modelProvider?: string; modelId?: string; thinkingLevel?: string }) => void;
|
||||
}
|
||||
|
||||
function NewChatDialog({ projectId, defaultModel, defaultThinkingLevel, onClose, onCreate }: NewChatDialogProps) {
|
||||
function NewChatDialog({ projectId, defaultModel, defaultKind, defaultAgentId, defaultThinkingLevel, defaultSelectedThinkingLevel, onClose, onCreate }: NewChatDialogProps) {
|
||||
const { t } = useTranslation("app");
|
||||
const [chatMode, setChatMode] = useState<"agent" | "model">("agent");
|
||||
const [chatMode, setChatMode] = useState<"agent" | "model">(defaultKind ?? "agent");
|
||||
const { agents, loading: agentsLoading } = useAgentsMapCache(projectId);
|
||||
const [selectedAgentId, setSelectedAgentId] = useState<string>("");
|
||||
const [selectedAgentId, setSelectedAgentId] = useState<string>(defaultAgentId ?? "");
|
||||
const { models, favoriteProviders: cachedFavoriteProviders, favoriteModels: cachedFavoriteModels, loading: modelsLoading, refresh } = useModelsCache();
|
||||
const defaultModelValue = defaultModel.provider && defaultModel.modelId
|
||||
? `${defaultModel.provider}/${defaultModel.modelId}`
|
||||
@@ -299,7 +302,7 @@ function NewChatDialog({ projectId, defaultModel, defaultThinkingLevel, onClose,
|
||||
* FNXC:Chat-ThinkingLevel 2026-07-10-00:00:
|
||||
* New model-mode chats expose the shared inline thinking selector; an empty value means Default and is omitted from the create-session payload so the backend resolves project/global reasoning effort.
|
||||
*/
|
||||
const [thinkingLevel, setThinkingLevel] = useState<string>("");
|
||||
const [thinkingLevel, setThinkingLevel] = useState<string>(defaultSelectedThinkingLevel ?? "");
|
||||
const [favoriteProviders, setFavoriteProviders] = useState<string[]>(cachedFavoriteProviders);
|
||||
const [favoriteModels, setFavoriteModels] = useState<string[]>(cachedFavoriteModels);
|
||||
|
||||
@@ -311,6 +314,19 @@ function NewChatDialog({ projectId, defaultModel, defaultThinkingLevel, onClose,
|
||||
setFavoriteModels(cachedFavoriteModels);
|
||||
}, [cachedFavoriteModels]);
|
||||
|
||||
useEffect(() => {
|
||||
if (defaultKind) {
|
||||
setChatMode(defaultKind);
|
||||
}
|
||||
}, [defaultKind]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!defaultAgentId) {
|
||||
return;
|
||||
}
|
||||
setSelectedAgentId((current) => current || defaultAgentId);
|
||||
}, [defaultAgentId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!defaultModelValue) {
|
||||
return;
|
||||
@@ -318,6 +334,13 @@ function NewChatDialog({ projectId, defaultModel, defaultThinkingLevel, onClose,
|
||||
setSelectedModel((current) => current || defaultModelValue);
|
||||
}, [defaultModelValue]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!defaultSelectedThinkingLevel) {
|
||||
return;
|
||||
}
|
||||
setThinkingLevel((current) => current || defaultSelectedThinkingLevel);
|
||||
}, [defaultSelectedThinkingLevel]);
|
||||
|
||||
const handleToggleFavorite = useCallback(async (provider: string) => {
|
||||
const currentFavorites = favoriteProviders;
|
||||
const isFavorite = currentFavorites.includes(provider);
|
||||
@@ -511,6 +534,7 @@ export function ChatView({ projectId, addToast, floating = false, compactLayout
|
||||
*/
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
setChatSettings(null);
|
||||
fetchSettings(projectId)
|
||||
.then((settings) => {
|
||||
if (!cancelled) {
|
||||
@@ -527,6 +551,27 @@ export function ChatView({ projectId, addToast, floating = false, compactLayout
|
||||
};
|
||||
}, [projectId]);
|
||||
const resolvedDefaultThinkingLevel = chatSettings?.defaultThinkingLevel ?? "off";
|
||||
const chatDefaultTarget = useMemo(() => {
|
||||
/*
|
||||
FNXC:ChatModels 2026-07-12-20:45:
|
||||
New Chat has one project-scoped default target resolver shared by every affordance. A complete agent default wins only when kind=agent; a complete model pair wins only when kind=model; incomplete always-default settings fall back to the picker instead of creating an unroutable session.
|
||||
*/
|
||||
if (chatSettings?.chatDefaultKind === "agent" && chatSettings.chatDefaultAgentId) {
|
||||
return {
|
||||
kind: "agent" as const,
|
||||
agentId: chatSettings.chatDefaultAgentId,
|
||||
};
|
||||
}
|
||||
if (chatSettings?.chatDefaultKind === "model" && chatSettings.chatDefaultModelProvider && chatSettings.chatDefaultModelId) {
|
||||
return {
|
||||
kind: "model" as const,
|
||||
modelProvider: chatSettings.chatDefaultModelProvider,
|
||||
modelId: chatSettings.chatDefaultModelId,
|
||||
thinkingLevel: chatSettings.chatDefaultThinkingLevel,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}, [chatSettings]);
|
||||
|
||||
const {
|
||||
activeSession,
|
||||
@@ -594,6 +639,12 @@ export function ChatView({ projectId, addToast, floating = false, compactLayout
|
||||
const agentsMap = useMemo(() => (chatAgentsMap.size > 0 ? chatAgentsMap : cachedAgentsMap), [cachedAgentsMap, chatAgentsMap]);
|
||||
const { models, defaultProvider, defaultModelId } = useModelsCache();
|
||||
const defaultModel = useMemo<DefaultModelSelection>(() => ({ provider: defaultProvider, modelId: defaultModelId }), [defaultModelId, defaultProvider]);
|
||||
const dialogDefaultModel = useMemo<DefaultModelSelection>(() => {
|
||||
if (chatDefaultTarget?.kind === "model") {
|
||||
return { provider: chatDefaultTarget.modelProvider, modelId: chatDefaultTarget.modelId };
|
||||
}
|
||||
return defaultModel;
|
||||
}, [chatDefaultTarget, defaultModel]);
|
||||
const { skills: discoveredSkills, loading: skillsLoading } = useDiscoveredSkillsCache(projectId);
|
||||
const [showSkillMenu, setShowSkillMenu] = useState(false);
|
||||
const [skillFilter, setSkillFilter] = useState("");
|
||||
@@ -1486,9 +1537,26 @@ export function ChatView({ projectId, addToast, floating = false, compactLayout
|
||||
addToast(t("chat.failedToCreateSession", "Failed to create chat session"), "error");
|
||||
}
|
||||
},
|
||||
[createSession, addToast, isChatMobile],
|
||||
[createSession, addToast, isChatMobile, t],
|
||||
);
|
||||
|
||||
const handleNewChat = useCallback(() => {
|
||||
if (chatSettings?.chatNewSessionMode === "always-default" && chatDefaultTarget) {
|
||||
if (chatDefaultTarget.kind === "agent") {
|
||||
void handleCreateSession({ agentId: chatDefaultTarget.agentId });
|
||||
return;
|
||||
}
|
||||
void handleCreateSession({
|
||||
agentId: FN_AGENT_ID,
|
||||
modelProvider: chatDefaultTarget.modelProvider,
|
||||
modelId: chatDefaultTarget.modelId,
|
||||
thinkingLevel: chatDefaultTarget.thinkingLevel,
|
||||
});
|
||||
return;
|
||||
}
|
||||
setShowNewDialog(true);
|
||||
}, [chatDefaultTarget, chatSettings?.chatNewSessionMode, handleCreateSession]);
|
||||
|
||||
const resizeComposer = useCallback((textarea?: HTMLTextAreaElement | null) => {
|
||||
const composer = textarea ?? inputRef.current;
|
||||
if (!composer) {
|
||||
@@ -2252,7 +2320,7 @@ export function ChatView({ projectId, addToast, floating = false, compactLayout
|
||||
<div className="chat-empty-state">
|
||||
<MessageSquare size={48} strokeWidth={1.5} />
|
||||
<h2>{t("chat.startNewConversation", "Start a new conversation")}</h2>
|
||||
<button className="btn btn-primary" onClick={() => setShowNewDialog(true)}>
|
||||
<button className="btn btn-primary" onClick={handleNewChat}>
|
||||
<Plus size={16} />
|
||||
{t("chat.newChat", "New Chat")}
|
||||
</button>
|
||||
@@ -2850,7 +2918,7 @@ export function ChatView({ projectId, addToast, floating = false, compactLayout
|
||||
))}
|
||||
{/*
|
||||
FNXC:Chat 2026-06-27-00:00:
|
||||
Mobile Direct-scope quick session switching must let users start a new chat without leaving the open thread. Route this affordance through the same setShowNewDialog(true) / NewChatDialog path as the header and sidebar-footer controls.
|
||||
Mobile Direct-scope quick session switching must let users start a new chat without leaving the open thread. Route this affordance through the same handleNewChat() path as the header and sidebar-footer controls so project chatNewSessionMode is honored everywhere.
|
||||
*/}
|
||||
<button
|
||||
type="button"
|
||||
@@ -2859,7 +2927,7 @@ export function ChatView({ projectId, addToast, floating = false, compactLayout
|
||||
data-testid="chat-mobile-session-new"
|
||||
onClick={() => {
|
||||
setMobileSessionMenuOpen(false);
|
||||
setShowNewDialog(true);
|
||||
handleNewChat();
|
||||
}}
|
||||
>
|
||||
<Plus size={16} aria-hidden="true" />
|
||||
@@ -2925,7 +2993,7 @@ export function ChatView({ projectId, addToast, floating = false, compactLayout
|
||||
{!isChatMobile ? (
|
||||
<button
|
||||
className="btn btn-sm btn-primary chat-view-header-new-chat"
|
||||
onClick={() => setShowNewDialog(true)}
|
||||
onClick={handleNewChat}
|
||||
data-testid="chat-new-btn"
|
||||
>
|
||||
<Plus size={14} />
|
||||
@@ -3211,7 +3279,7 @@ export function ChatView({ projectId, addToast, floating = false, compactLayout
|
||||
<div className="chat-sidebar-footer">
|
||||
<button
|
||||
className="btn btn-sm btn-primary chat-sidebar-footer-btn"
|
||||
onClick={() => setShowNewDialog(true)}
|
||||
onClick={handleNewChat}
|
||||
data-testid="chat-new-btn"
|
||||
>
|
||||
<Plus size={14} />
|
||||
@@ -3684,8 +3752,11 @@ export function ChatView({ projectId, addToast, floating = false, compactLayout
|
||||
{showNewDialog && (
|
||||
<NewChatDialog
|
||||
projectId={projectId}
|
||||
defaultModel={defaultModel}
|
||||
defaultModel={dialogDefaultModel}
|
||||
defaultKind={chatDefaultTarget?.kind}
|
||||
defaultAgentId={chatDefaultTarget?.kind === "agent" ? chatDefaultTarget.agentId : undefined}
|
||||
defaultThinkingLevel={resolvedDefaultThinkingLevel}
|
||||
defaultSelectedThinkingLevel={chatDefaultTarget?.kind === "model" ? chatDefaultTarget.thinkingLevel : undefined}
|
||||
onClose={() => setShowNewDialog(false)}
|
||||
onCreate={handleCreateSession}
|
||||
/>
|
||||
|
||||
@@ -0,0 +1,357 @@
|
||||
// @vitest-environment jsdom
|
||||
import { act, fireEvent, render as rtlRender, screen, waitFor } from "@testing-library/react";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { ChatView } from "../ChatView";
|
||||
import * as api from "../../api";
|
||||
import * as useChatModule from "../../hooks/useChat";
|
||||
import * as useChatRoomsModule from "../../hooks/useChatRooms";
|
||||
import type { ChatSessionInfo, UseChatReturn } from "../../hooks/useChat";
|
||||
import type { UseChatRoomsResult } from "../../hooks/useChatRooms";
|
||||
import { _resetInitialViewportHeight } from "../../hooks/useMobileKeyboard";
|
||||
|
||||
Element.prototype.scrollIntoView = vi.fn();
|
||||
|
||||
vi.mock("../SessionTerminal", () => ({
|
||||
SessionTerminal: () => <div data-testid="session-terminal">terminal</div>,
|
||||
}));
|
||||
|
||||
vi.mock("../../hooks/useChat");
|
||||
vi.mock("../../hooks/useChatRooms");
|
||||
vi.mock("../../hooks/useNavigationHistory", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("../../hooks/useNavigationHistory")>();
|
||||
return {
|
||||
...actual,
|
||||
useNavigationHistoryContext: () => ({ pushNav: vi.fn(), replaceCurrent: vi.fn() }),
|
||||
};
|
||||
});
|
||||
vi.mock("../../hooks/useModelsCache", () => ({
|
||||
useModelsCache: () => ({
|
||||
models: [{ id: "gpt-4o", provider: "openai", name: "GPT-4o" }],
|
||||
favoriteProviders: [],
|
||||
favoriteModels: [],
|
||||
defaultProvider: "openai",
|
||||
defaultModelId: "gpt-4o",
|
||||
loading: false,
|
||||
refresh: vi.fn(async () => undefined),
|
||||
}),
|
||||
}));
|
||||
vi.mock("../../hooks/useAgentsMapCache", () => ({
|
||||
useAgentsMapCache: () => ({
|
||||
loading: false,
|
||||
agents: [
|
||||
{ id: "agent-alpha", name: "Alpha", role: "engineer" },
|
||||
{ id: "agent-beta", name: "Beta", role: "reviewer" },
|
||||
],
|
||||
agentsMap: new Map([
|
||||
["agent-alpha", { id: "agent-alpha", name: "Alpha", role: "engineer" }],
|
||||
["agent-beta", { id: "agent-beta", name: "Beta", role: "reviewer" }],
|
||||
]),
|
||||
refresh: vi.fn(async () => undefined),
|
||||
}),
|
||||
}));
|
||||
vi.mock("../CustomModelDropdown", () => ({
|
||||
CustomModelDropdown: ({ value, thinkingLevel, defaultThinkingLevel }: { value?: string; thinkingLevel?: string; defaultThinkingLevel?: string }) => (
|
||||
<div
|
||||
data-testid="custom-model-dropdown"
|
||||
data-value={value ?? ""}
|
||||
data-thinking-value={thinkingLevel ?? ""}
|
||||
data-default-thinking={defaultThinkingLevel ?? ""}
|
||||
/>
|
||||
),
|
||||
}));
|
||||
vi.mock("../../api", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("../../api")>();
|
||||
return {
|
||||
...actual,
|
||||
fetchDiscoveredSkills: vi.fn().mockResolvedValue([]),
|
||||
fetchTasks: vi.fn().mockResolvedValue([]),
|
||||
fetchSettings: vi.fn().mockResolvedValue({}),
|
||||
searchFiles: vi.fn().mockResolvedValue({ files: [] }),
|
||||
};
|
||||
});
|
||||
|
||||
const mockUseChat = vi.mocked(useChatModule.useChat);
|
||||
const mockUseChatRooms = vi.mocked(useChatRoomsModule.useChatRooms);
|
||||
const mockFetchSettings = vi.mocked(api.fetchSettings);
|
||||
|
||||
async function renderWithAct(ui: Parameters<typeof rtlRender>[0]) {
|
||||
let result: ReturnType<typeof rtlRender> | undefined;
|
||||
await act(async () => {
|
||||
result = rtlRender(ui);
|
||||
});
|
||||
return result!;
|
||||
}
|
||||
|
||||
function makeSession(overrides: Partial<ChatSessionInfo> = {}): ChatSessionInfo {
|
||||
return {
|
||||
id: "sess-1",
|
||||
agentId: "agent-alpha",
|
||||
status: "active",
|
||||
title: "Alpha chat",
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function chatState(overrides: Partial<UseChatReturn> = {}): UseChatReturn {
|
||||
const activeSession = "activeSession" in overrides ? overrides.activeSession ?? null : null;
|
||||
const sessions = "sessions" in overrides ? overrides.sessions ?? [] : activeSession ? [activeSession] : [];
|
||||
return {
|
||||
sessions,
|
||||
activeSession,
|
||||
sessionsLoading: false,
|
||||
messages: [],
|
||||
messagesLoading: false,
|
||||
isStreaming: false,
|
||||
streamingText: "",
|
||||
streamingThinking: "",
|
||||
streamingToolCalls: [],
|
||||
selectSession: vi.fn(),
|
||||
createSession: vi.fn(),
|
||||
archiveSession: vi.fn(),
|
||||
renameSession: vi.fn(),
|
||||
setSessionThinkingLevel: vi.fn(),
|
||||
deleteSession: vi.fn(),
|
||||
sendMessage: vi.fn(),
|
||||
editMessageAndResend: vi.fn(),
|
||||
stopStreaming: vi.fn(),
|
||||
pendingMessages: [],
|
||||
clearPendingMessage: vi.fn(),
|
||||
loadMoreMessages: vi.fn(),
|
||||
hasMoreMessages: false,
|
||||
searchQuery: "",
|
||||
setSearchQuery: vi.fn(),
|
||||
filteredSessions: sessions,
|
||||
refreshSessions: vi.fn(),
|
||||
agentsMap: new Map(),
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function roomsState(overrides: Partial<UseChatRoomsResult> = {}): UseChatRoomsResult {
|
||||
return {
|
||||
rooms: [],
|
||||
roomsLoading: false,
|
||||
roomsError: null,
|
||||
activeRoom: null,
|
||||
activeRoomMembers: [],
|
||||
messages: [],
|
||||
messagesLoading: false,
|
||||
selectRoom: vi.fn(),
|
||||
createRoom: vi.fn(),
|
||||
deleteRoom: vi.fn(),
|
||||
sendRoomMessage: vi.fn(),
|
||||
refreshRooms: vi.fn(),
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function mockDesktopViewport() {
|
||||
Object.defineProperty(window, "innerWidth", { value: 1280, configurable: true });
|
||||
Object.defineProperty(window, "matchMedia", {
|
||||
configurable: true,
|
||||
writable: true,
|
||||
value: vi.fn((query: string) => ({
|
||||
matches: false,
|
||||
media: query,
|
||||
onchange: null,
|
||||
addListener: vi.fn(),
|
||||
removeListener: vi.fn(),
|
||||
addEventListener: vi.fn(),
|
||||
removeEventListener: vi.fn(),
|
||||
dispatchEvent: vi.fn(),
|
||||
})),
|
||||
});
|
||||
}
|
||||
|
||||
function mockMobileViewport() {
|
||||
Object.defineProperty(window, "innerWidth", { value: 375, configurable: true });
|
||||
Object.defineProperty(window, "matchMedia", {
|
||||
configurable: true,
|
||||
writable: true,
|
||||
value: vi.fn((query: string) => ({
|
||||
matches: query.includes("max-width: 768px") || query.includes("max-height: 480px"),
|
||||
media: query,
|
||||
onchange: null,
|
||||
addListener: vi.fn(),
|
||||
removeListener: vi.fn(),
|
||||
addEventListener: vi.fn(),
|
||||
removeEventListener: vi.fn(),
|
||||
dispatchEvent: vi.fn(),
|
||||
})),
|
||||
});
|
||||
}
|
||||
|
||||
async function waitForSettings() {
|
||||
await waitFor(() => expect(mockFetchSettings).toHaveBeenCalled());
|
||||
await act(async () => undefined);
|
||||
}
|
||||
|
||||
describe("ChatView New Chat project default behavior", () => {
|
||||
beforeEach(() => {
|
||||
_resetInitialViewportHeight();
|
||||
localStorage.clear();
|
||||
vi.clearAllMocks();
|
||||
mockDesktopViewport();
|
||||
mockUseChatRooms.mockReturnValue(roomsState());
|
||||
mockFetchSettings.mockResolvedValue({ defaultThinkingLevel: "medium" } as Awaited<ReturnType<typeof api.fetchSettings>>);
|
||||
});
|
||||
|
||||
it("always-default model creates directly from the desktop New Chat entry without opening the dialog", async () => {
|
||||
const createSession = vi.fn();
|
||||
mockFetchSettings.mockResolvedValue({
|
||||
chatNewSessionMode: "always-default",
|
||||
chatDefaultKind: "model",
|
||||
chatDefaultModelProvider: "anthropic",
|
||||
chatDefaultModelId: "claude-sonnet-4-5",
|
||||
chatDefaultThinkingLevel: "high",
|
||||
defaultThinkingLevel: "medium",
|
||||
} as Awaited<ReturnType<typeof api.fetchSettings>>);
|
||||
mockUseChat.mockReturnValue(chatState({ createSession }));
|
||||
|
||||
await renderWithAct(<ChatView projectId="project-a" addToast={vi.fn()} />);
|
||||
await waitForSettings();
|
||||
|
||||
fireEvent.click(screen.getAllByTestId("chat-new-btn")[0]);
|
||||
|
||||
expect(createSession).toHaveBeenCalledWith({
|
||||
agentId: "__fn_agent__",
|
||||
modelProvider: "anthropic",
|
||||
modelId: "claude-sonnet-4-5",
|
||||
thinkingLevel: "high",
|
||||
});
|
||||
expect(screen.queryByRole("dialog")).toBeNull();
|
||||
});
|
||||
|
||||
it("always-default agent creates directly without opening the dialog", async () => {
|
||||
const createSession = vi.fn();
|
||||
mockFetchSettings.mockResolvedValue({
|
||||
chatNewSessionMode: "always-default",
|
||||
chatDefaultKind: "agent",
|
||||
chatDefaultAgentId: "agent-beta",
|
||||
} as Awaited<ReturnType<typeof api.fetchSettings>>);
|
||||
mockUseChat.mockReturnValue(chatState({ createSession }));
|
||||
|
||||
await renderWithAct(<ChatView projectId="project-a" addToast={vi.fn()} />);
|
||||
await waitForSettings();
|
||||
|
||||
fireEvent.click(screen.getAllByTestId("chat-new-btn")[0]);
|
||||
|
||||
expect(createSession).toHaveBeenCalledWith({ agentId: "agent-beta" });
|
||||
expect(screen.queryByRole("dialog")).toBeNull();
|
||||
});
|
||||
|
||||
it("prompt mode opens the dialog prefilled with the configured model default", async () => {
|
||||
const createSession = vi.fn();
|
||||
mockFetchSettings.mockResolvedValue({
|
||||
chatNewSessionMode: "prompt",
|
||||
chatDefaultKind: "model",
|
||||
chatDefaultModelProvider: "anthropic",
|
||||
chatDefaultModelId: "claude-sonnet-4-5",
|
||||
chatDefaultThinkingLevel: "low",
|
||||
defaultThinkingLevel: "medium",
|
||||
} as Awaited<ReturnType<typeof api.fetchSettings>>);
|
||||
mockUseChat.mockReturnValue(chatState({ createSession }));
|
||||
|
||||
await renderWithAct(<ChatView projectId="project-a" addToast={vi.fn()} />);
|
||||
await waitForSettings();
|
||||
|
||||
fireEvent.click(screen.getAllByTestId("chat-new-btn")[0]);
|
||||
|
||||
expect(createSession).not.toHaveBeenCalled();
|
||||
expect(screen.getByRole("dialog")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("chat-new-dialog-mode-model")).toHaveClass("chat-new-dialog-mode-btn--active");
|
||||
expect(screen.getByTestId("custom-model-dropdown")).toHaveAttribute("data-value", "anthropic/claude-sonnet-4-5");
|
||||
expect(screen.getByTestId("custom-model-dropdown")).toHaveAttribute("data-thinking-value", "low");
|
||||
});
|
||||
|
||||
it("unset mode opens the dialog prefilled with the configured agent default", async () => {
|
||||
const createSession = vi.fn();
|
||||
mockFetchSettings.mockResolvedValue({
|
||||
chatDefaultKind: "agent",
|
||||
chatDefaultAgentId: "agent-alpha",
|
||||
} as Awaited<ReturnType<typeof api.fetchSettings>>);
|
||||
mockUseChat.mockReturnValue(chatState({ createSession }));
|
||||
|
||||
await renderWithAct(<ChatView projectId="project-a" addToast={vi.fn()} />);
|
||||
await waitForSettings();
|
||||
|
||||
fireEvent.click(screen.getAllByTestId("chat-new-btn")[0]);
|
||||
|
||||
expect(createSession).not.toHaveBeenCalled();
|
||||
expect(screen.getByTestId("chat-new-dialog-mode-agent")).toHaveClass("chat-new-dialog-mode-btn--active");
|
||||
expect(screen.getByTestId("agent-option-agent-alpha")).toHaveClass("chat-new-dialog-agent-item--selected");
|
||||
});
|
||||
|
||||
it("always-default without a resolvable default falls back to the dialog", async () => {
|
||||
const createSession = vi.fn();
|
||||
mockFetchSettings.mockResolvedValue({
|
||||
chatNewSessionMode: "always-default",
|
||||
chatDefaultKind: "model",
|
||||
chatDefaultModelProvider: "anthropic",
|
||||
} as Awaited<ReturnType<typeof api.fetchSettings>>);
|
||||
mockUseChat.mockReturnValue(chatState({ createSession }));
|
||||
|
||||
await renderWithAct(<ChatView projectId="project-a" addToast={vi.fn()} />);
|
||||
await waitForSettings();
|
||||
|
||||
fireEvent.click(screen.getAllByTestId("chat-new-btn")[0]);
|
||||
|
||||
expect(createSession).not.toHaveBeenCalled();
|
||||
expect(screen.getByRole("dialog")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("mobile session switcher New Chat shares the always-default model path", async () => {
|
||||
mockMobileViewport();
|
||||
const createSession = vi.fn();
|
||||
const session = makeSession();
|
||||
mockFetchSettings.mockResolvedValue({
|
||||
chatNewSessionMode: "always-default",
|
||||
chatDefaultKind: "model",
|
||||
chatDefaultModelProvider: "anthropic",
|
||||
chatDefaultModelId: "claude-sonnet-4-5",
|
||||
} as Awaited<ReturnType<typeof api.fetchSettings>>);
|
||||
mockUseChat.mockReturnValue(chatState({ activeSession: session, sessions: [session], filteredSessions: [session], createSession }));
|
||||
|
||||
await renderWithAct(<ChatView projectId="project-a" addToast={vi.fn()} />);
|
||||
await waitForSettings();
|
||||
|
||||
fireEvent.click(screen.getByTestId(`chat-session-${session.id}`));
|
||||
fireEvent.click(await screen.findByTestId("chat-mobile-session-trigger"));
|
||||
fireEvent.click(screen.getByTestId("chat-mobile-session-new"));
|
||||
|
||||
expect(createSession).toHaveBeenCalledWith({
|
||||
agentId: "__fn_agent__",
|
||||
modelProvider: "anthropic",
|
||||
modelId: "claude-sonnet-4-5",
|
||||
thinkingLevel: undefined,
|
||||
});
|
||||
expect(screen.queryByRole("dialog")).toBeNull();
|
||||
});
|
||||
|
||||
it("switching projects clears the previous default while the new project settings load", async () => {
|
||||
const createSession = vi.fn();
|
||||
mockFetchSettings.mockImplementation(async (projectId?: string) => {
|
||||
if (projectId === "project-a") {
|
||||
return {
|
||||
chatNewSessionMode: "always-default",
|
||||
chatDefaultKind: "agent",
|
||||
chatDefaultAgentId: "agent-beta",
|
||||
} as Awaited<ReturnType<typeof api.fetchSettings>>;
|
||||
}
|
||||
return {} as Awaited<ReturnType<typeof api.fetchSettings>>;
|
||||
});
|
||||
mockUseChat.mockReturnValue(chatState({ createSession }));
|
||||
|
||||
const { rerender } = await renderWithAct(<ChatView projectId="project-a" addToast={vi.fn()} />);
|
||||
await waitFor(() => expect(mockFetchSettings).toHaveBeenCalledWith("project-a"));
|
||||
await act(async () => undefined);
|
||||
|
||||
rerender(<ChatView projectId="project-b" addToast={vi.fn()} />);
|
||||
fireEvent.click(screen.getAllByTestId("chat-new-btn")[0]);
|
||||
|
||||
expect(createSession).not.toHaveBeenCalled();
|
||||
expect(screen.getByRole("dialog")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -8,6 +8,7 @@ import { applyPresetToSelection } from "../../../utils/modelPresets";
|
||||
import type { ToastType } from "../../../hooks/useToast";
|
||||
import type { ModelLane, SectionBaseProps, SectionSaveHandler, SettingsFormState } from "./context";
|
||||
import { LoadingSpinner } from "../../LoadingSpinner";
|
||||
import { useAgentsMapCache } from "../../../hooks/useAgentsMapCache";
|
||||
type LaneStatus = "inherited" | "overridden";
|
||||
type WorkflowModelPair = {
|
||||
id: "planning" | "execution" | "validator" | "planning-fallback" | "validator-fallback";
|
||||
@@ -135,6 +136,7 @@ export interface ProjectModelsSectionProps extends SectionBaseProps {
|
||||
}
|
||||
export function ProjectModelsSection({ scopeBanner, form, setForm, models, projectId, onOpenWorkflowSettings, registerWorkflowLaneSaver, }: ProjectModelsSectionProps) {
|
||||
const { t } = useTranslation("app");
|
||||
const { agents, loading: agentsLoading } = useAgentsMapCache(projectId);
|
||||
const { modelLanes, getLaneStatus, getLaneValue, updateLaneValue, resetLaneValue, getLaneThinkingValue, updateLaneThinkingValue, resetLaneThinkingValue, availableModels, modelsLoading, favoriteProviders, favoriteModels, onToggleFavorite, onToggleModelFavorite, editingPresetId, setEditingPresetId, presetDraft, setPresetDraft, onSavePresetDraft, confirmDelete, } = models;
|
||||
const presets = form.modelPresets || [];
|
||||
const presetOptions = presets.map((preset) => ({ id: preset.id, name: preset.name }));
|
||||
@@ -316,6 +318,36 @@ export function ProjectModelsSection({ scopeBanner, form, setForm, models, proje
|
||||
const resetTitleSummarizerFallbackValue = () => {
|
||||
setForm((f) => ({ ...f, titleSummarizerFallbackProvider: undefined, titleSummarizerFallbackModelId: undefined, titleSummarizerFallbackThinkingLevel: undefined } as SettingsFormState));
|
||||
};
|
||||
const chatDefaultKind = form.chatDefaultKind ?? "model";
|
||||
const chatDefaultModelValue = form.chatDefaultModelProvider && form.chatDefaultModelId
|
||||
? `${form.chatDefaultModelProvider}/${form.chatDefaultModelId}`
|
||||
: "";
|
||||
const chatDefaultThinkingValue = typeof form.chatDefaultThinkingLevel === "string"
|
||||
? form.chatDefaultThinkingLevel
|
||||
: "";
|
||||
const chatDefaultCustomized = Boolean(form.chatNewSessionMode || form.chatDefaultKind || form.chatDefaultAgentId || chatDefaultModelValue || chatDefaultThinkingValue);
|
||||
const setChatDefaultModelValue = (value: string) => {
|
||||
if (!value) {
|
||||
setForm((f) => ({ ...f, chatDefaultKind: "model", chatDefaultAgentId: undefined, chatDefaultModelProvider: undefined, chatDefaultModelId: undefined, chatDefaultThinkingLevel: undefined } as SettingsFormState));
|
||||
return;
|
||||
}
|
||||
const slashIdx = value.indexOf("/");
|
||||
if (slashIdx <= 0)
|
||||
return;
|
||||
setForm((f) => ({
|
||||
...f,
|
||||
chatDefaultKind: "model",
|
||||
chatDefaultAgentId: undefined,
|
||||
chatDefaultModelProvider: value.slice(0, slashIdx),
|
||||
chatDefaultModelId: value.slice(slashIdx + 1),
|
||||
} as SettingsFormState));
|
||||
};
|
||||
const setChatDefaultThinkingValue = (value: string) => {
|
||||
setForm((f) => ({ ...f, chatDefaultThinkingLevel: value || undefined } as SettingsFormState));
|
||||
};
|
||||
const resetChatDefaultValue = () => {
|
||||
setForm((f) => ({ ...f, chatNewSessionMode: undefined, chatDefaultKind: undefined, chatDefaultAgentId: undefined, chatDefaultModelProvider: undefined, chatDefaultModelId: undefined, chatDefaultThinkingLevel: undefined } as SettingsFormState));
|
||||
};
|
||||
return (<>
|
||||
{scopeBanner}
|
||||
|
||||
@@ -379,6 +411,51 @@ export function ProjectModelsSection({ scopeBanner, form, setForm, models, proje
|
||||
</div>
|
||||
</>)}
|
||||
|
||||
{/* FNXC:ChatModels 2026-07-12-20:45: Project Models owns the Direct-chat default because New Chat needs a project-scoped model-or-agent target plus prompt-vs-direct creation mode without changing workflow or in-chat switcher settings. */}
|
||||
<h4 className="settings-section-heading settings-section-heading--spaced">{t("settings.projectModels.chatHeading", "Chat")}</h4>
|
||||
<p className="settings-description">{t("settings.projectModels.chatDescription", "Choose the default target for new Direct chats and whether New Chat should prompt or immediately use that default.")}</p>
|
||||
<div className="form-group" data-testid="project-models-chat-mode">
|
||||
<label htmlFor="chatNewSessionMode">{t("settings.projectModels.chatNewSessionMode", "New Chat behavior")}</label>
|
||||
<select id="chatNewSessionMode" value={form.chatNewSessionMode ?? "prompt"} onChange={(event) => setForm((f) => ({ ...f, chatNewSessionMode: event.target.value === "always-default" ? "always-default" : undefined } as SettingsFormState))}>
|
||||
<option value="prompt">{t("settings.projectModels.chatNewSessionModePrompt", "Prompt for model each time")}</option>
|
||||
<option value="always-default">{t("settings.projectModels.chatNewSessionModeAlwaysDefault", "Always use configured default")}</option>
|
||||
</select>
|
||||
<small>{t("settings.projectModels.chatNewSessionModeHelp", "Prompt mode opens New Chat with this default preselected. Always-default mode skips the dialog when the configured default is complete.")}</small>
|
||||
</div>
|
||||
<div className="form-group" data-testid="project-models-chat-kind">
|
||||
<label>{t("settings.projectModels.chatDefaultKind", "Chat default target")}</label>
|
||||
<div className="chat-new-dialog-mode-toggle" data-testid="project-models-chat-kind-toggle">
|
||||
<button type="button" className={`chat-new-dialog-mode-btn${chatDefaultKind === "model" ? " chat-new-dialog-mode-btn--active" : ""}`} onClick={() => setForm((f) => ({ ...f, chatDefaultKind: "model", chatDefaultAgentId: undefined } as SettingsFormState))}>
|
||||
{t("settings.projectModels.chatDefaultKindModel", "Model")}
|
||||
</button>
|
||||
<button type="button" className={`chat-new-dialog-mode-btn${chatDefaultKind === "agent" ? " chat-new-dialog-mode-btn--active" : ""}`} onClick={() => setForm((f) => ({ ...f, chatDefaultKind: "agent", chatDefaultModelProvider: undefined, chatDefaultModelId: undefined, chatDefaultThinkingLevel: undefined } as SettingsFormState))}>
|
||||
{t("settings.projectModels.chatDefaultKindAgent", "Agent")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{chatDefaultKind === "model" ? (<div className="form-group" data-testid="project-models-chat-model">
|
||||
<label htmlFor="chatDefaultModel">{t("settings.projectModels.chatDefaultModel", "Chat Default Model")}</label>
|
||||
<div className="settings-model-lane-control-row">
|
||||
<div className="settings-model-lane-control-main">
|
||||
<CustomModelDropdown id="chatDefaultModel" label={t("settings.projectModels.chatDefaultModel", "Chat Default Model")} models={availableModels} value={chatDefaultModelValue} onChange={setChatDefaultModelValue} placeholder={t("settings.projectModels.selectChatDefaultModel", "Select a chat default model")} favoriteProviders={favoriteProviders} onToggleFavorite={onToggleFavorite} favoriteModels={favoriteModels} onToggleModelFavorite={onToggleModelFavorite} menuWidth="readable" showThinkingLevel={true} thinkingLevel={chatDefaultThinkingValue} onThinkingLevelChange={setChatDefaultThinkingValue} defaultThinkingLevel={form.defaultThinkingLevel}/>
|
||||
</div>
|
||||
{chatDefaultCustomized && (<button type="button" className="btn btn-ghost btn-sm" title={t("settings.projectModels.chatDefaultReset", "Reset Chat default")} onClick={resetChatDefaultValue}>{t("settings.projectModels.reset", " Reset ")}</button>)}
|
||||
</div>
|
||||
<small>{t("settings.projectModels.chatDefaultModelHelp", "Model-mode New Chat uses the built-in Fusion chat agent with this provider/model pair. Leave empty to fall back to prompting.")}</small>
|
||||
</div>) : (<div className="form-group" data-testid="project-models-chat-agent">
|
||||
<label htmlFor="chatDefaultAgentId">{t("settings.projectModels.chatDefaultAgent", "Chat Default Agent")}</label>
|
||||
<div className="settings-model-lane-control-row">
|
||||
<div className="settings-model-lane-control-main">
|
||||
<select id="chatDefaultAgentId" value={form.chatDefaultAgentId ?? ""} disabled={agentsLoading || agents.length === 0} onChange={(event) => setForm((f) => ({ ...f, chatDefaultKind: "agent", chatDefaultAgentId: event.target.value || undefined, chatDefaultModelProvider: undefined, chatDefaultModelId: undefined, chatDefaultThinkingLevel: undefined } as SettingsFormState))}>
|
||||
<option value="">{agentsLoading ? t("settings.projectModels.loadingAgents", "Loading agents…") : t("settings.projectModels.selectChatDefaultAgent", "Select a chat default agent")}</option>
|
||||
{agents.map((agent) => (<option key={agent.id} value={agent.id}>{agent.name} ({agent.role})</option>))}
|
||||
</select>
|
||||
</div>
|
||||
{chatDefaultCustomized && (<button type="button" className="btn btn-ghost btn-sm" title={t("settings.projectModels.chatDefaultReset", "Reset Chat default")} onClick={resetChatDefaultValue}>{t("settings.projectModels.reset", " Reset ")}</button>)}
|
||||
</div>
|
||||
<small>{agents.length === 0 && !agentsLoading ? t("settings.projectModels.chatDefaultAgentEmpty", "No agents are available for this project yet.") : t("settings.projectModels.chatDefaultAgentHelp", "Agent-mode New Chat starts a Direct chat with the selected durable agent.")}</small>
|
||||
</div>)}
|
||||
|
||||
{/* --- Default workflow model lanes --- */}
|
||||
<h4 className="settings-section-heading settings-section-heading--spaced">{t("settings.projectModels.defaultWorkflowModelLanes", "Default workflow model lanes")}</h4>
|
||||
<p className="settings-description">
|
||||
|
||||
@@ -0,0 +1,193 @@
|
||||
// @vitest-environment jsdom
|
||||
import { useState } from "react";
|
||||
import { cleanup, fireEvent, render, screen } from "@testing-library/react";
|
||||
import * as jestDomMatchers from "@testing-library/jest-dom/matchers";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { ProjectModelsSection } from "../ProjectModelsSection";
|
||||
import type { ProjectModelsSectionModelProps } from "../ProjectModelsSection";
|
||||
import type { SettingsFormState } from "../context";
|
||||
import { fetchWorkflow, fetchWorkflowSettingValues, updateWorkflowSettingValues } from "../../../../api";
|
||||
|
||||
vi.mock("react-i18next", () => ({
|
||||
useTranslation: () => ({
|
||||
t: (_key: string, fallback: string) => fallback,
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock("../../../../api", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("../../../../api")>();
|
||||
return {
|
||||
...actual,
|
||||
fetchWorkflow: vi.fn(async () => ({ id: "builtin:coding", name: "Coding", ir: {} })),
|
||||
fetchWorkflowSettingValues: vi.fn(async () => ({ stored: {}, effective: {}, orphaned: [] })),
|
||||
updateWorkflowSettingValues: vi.fn(async () => ({ stored: {}, effective: {}, orphaned: [] })),
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("../../../CustomModelDropdown", () => ({
|
||||
CustomModelDropdown: ({ id, label, value, onChange, showThinkingLevel, thinkingLevel, onThinkingLevelChange, defaultThinkingLevel }: {
|
||||
id?: string;
|
||||
label: string;
|
||||
value?: string;
|
||||
onChange?: (value: string) => void;
|
||||
showThinkingLevel?: boolean;
|
||||
thinkingLevel?: string;
|
||||
onThinkingLevelChange?: (value: string) => void;
|
||||
defaultThinkingLevel?: string;
|
||||
}) => (
|
||||
<div data-testid={`mock-model-host-${id ?? label}`} data-value={value ?? ""} data-default-thinking={defaultThinkingLevel ?? ""}>
|
||||
<button type="button" data-testid={`mock-model-dropdown-${id ?? label}`} onClick={() => onChange?.("anthropic/claude-sonnet-4-5")}>{label}</button>
|
||||
{showThinkingLevel ? <button type="button" data-testid={`mock-thinking-${id ?? label}`} onClick={() => onThinkingLevelChange?.(thinkingLevel ? "" : "high")}>thinking:{thinkingLevel || "inherit"}</button> : null}
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock("../../../../hooks/useAgentsMapCache", () => ({
|
||||
useAgentsMapCache: () => ({
|
||||
loading: false,
|
||||
agents: [
|
||||
{ id: "agent-alpha", name: "Alpha", role: "engineer" },
|
||||
{ id: "agent-review", name: "Review", role: "reviewer" },
|
||||
],
|
||||
agentsMap: new Map(),
|
||||
}),
|
||||
}));
|
||||
|
||||
expect.extend(jestDomMatchers);
|
||||
|
||||
afterEach(() => cleanup());
|
||||
|
||||
beforeEach(() => {
|
||||
vi.mocked(fetchWorkflow).mockResolvedValue({ id: "builtin:coding", name: "Coding", ir: {} } as never);
|
||||
vi.mocked(fetchWorkflowSettingValues).mockResolvedValue({ stored: {}, effective: {}, orphaned: [] });
|
||||
vi.mocked(updateWorkflowSettingValues).mockResolvedValue({ stored: {}, effective: {}, orphaned: [] });
|
||||
});
|
||||
|
||||
const models: ProjectModelsSectionModelProps = {
|
||||
modelLanes: [],
|
||||
getLaneStatus: () => "inherited",
|
||||
getLaneValue: () => "",
|
||||
updateLaneValue: vi.fn(),
|
||||
resetLaneValue: vi.fn(),
|
||||
getLaneThinkingValue: () => "",
|
||||
updateLaneThinkingValue: vi.fn(),
|
||||
resetLaneThinkingValue: vi.fn(),
|
||||
availableModels: [{ id: "claude-sonnet-4-5", provider: "anthropic", name: "Claude Sonnet" }],
|
||||
modelsLoading: false,
|
||||
favoriteProviders: [],
|
||||
favoriteModels: [],
|
||||
onToggleFavorite: vi.fn(),
|
||||
onToggleModelFavorite: vi.fn(),
|
||||
editingPresetId: null,
|
||||
setEditingPresetId: vi.fn(),
|
||||
presetDraft: null,
|
||||
setPresetDraft: vi.fn(),
|
||||
onSavePresetDraft: vi.fn(),
|
||||
confirmDelete: vi.fn(async () => true),
|
||||
};
|
||||
|
||||
function renderSection(initialForm: SettingsFormState = { defaultThinkingLevel: "medium" } as SettingsFormState) {
|
||||
let latestForm = initialForm;
|
||||
function Host() {
|
||||
const [form, setForm] = useState(initialForm);
|
||||
latestForm = form;
|
||||
return (
|
||||
<ProjectModelsSection
|
||||
scopeBanner={null}
|
||||
form={form}
|
||||
setForm={setForm}
|
||||
models={models}
|
||||
projectId="project-1"
|
||||
addToast={vi.fn()}
|
||||
/>
|
||||
);
|
||||
}
|
||||
render(<Host />);
|
||||
return { getForm: () => latestForm };
|
||||
}
|
||||
|
||||
describe("ProjectModelsSection Chat default settings", () => {
|
||||
it("renders the Chat subsection with prompt mode and model controls by default", () => {
|
||||
renderSection();
|
||||
|
||||
expect(screen.getByRole("heading", { name: "Chat" })).toBeInTheDocument();
|
||||
expect(screen.getByLabelText("New Chat behavior")).toHaveValue("prompt");
|
||||
expect(screen.getByTestId("project-models-chat-model")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("mock-model-host-chatDefaultModel")).toHaveAttribute("data-default-thinking", "medium");
|
||||
});
|
||||
|
||||
it("swaps between model and agent controls", () => {
|
||||
renderSection();
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: "Agent" }));
|
||||
expect(screen.getByTestId("project-models-chat-agent")).toBeInTheDocument();
|
||||
expect(screen.getByLabelText("Chat Default Agent")).toHaveValue("");
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: "Model" }));
|
||||
expect(screen.getByTestId("project-models-chat-model")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("selecting a model writes provider/model and clears the agent", () => {
|
||||
const { getForm } = renderSection({ chatDefaultKind: "agent", chatDefaultAgentId: "agent-alpha" } as SettingsFormState);
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: "Model" }));
|
||||
fireEvent.click(screen.getByTestId("mock-model-dropdown-chatDefaultModel"));
|
||||
|
||||
expect(getForm()).toMatchObject({
|
||||
chatDefaultKind: "model",
|
||||
chatDefaultModelProvider: "anthropic",
|
||||
chatDefaultModelId: "claude-sonnet-4-5",
|
||||
chatDefaultAgentId: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it("selecting an agent writes the agent id and clears model fields", () => {
|
||||
const { getForm } = renderSection({
|
||||
chatDefaultKind: "model",
|
||||
chatDefaultModelProvider: "anthropic",
|
||||
chatDefaultModelId: "claude-sonnet-4-5",
|
||||
chatDefaultThinkingLevel: "high",
|
||||
} as SettingsFormState);
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: "Agent" }));
|
||||
fireEvent.change(screen.getByLabelText("Chat Default Agent"), { target: { value: "agent-review" } });
|
||||
|
||||
expect(getForm()).toMatchObject({
|
||||
chatDefaultKind: "agent",
|
||||
chatDefaultAgentId: "agent-review",
|
||||
chatDefaultModelProvider: undefined,
|
||||
chatDefaultModelId: undefined,
|
||||
chatDefaultThinkingLevel: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it("setting the mode writes chatNewSessionMode", () => {
|
||||
const { getForm } = renderSection();
|
||||
|
||||
fireEvent.change(screen.getByLabelText("New Chat behavior"), { target: { value: "always-default" } });
|
||||
|
||||
expect(getForm().chatNewSessionMode).toBe("always-default");
|
||||
});
|
||||
|
||||
it("Reset clears all chat default fields", () => {
|
||||
const { getForm } = renderSection({
|
||||
chatNewSessionMode: "always-default",
|
||||
chatDefaultKind: "model",
|
||||
chatDefaultModelProvider: "anthropic",
|
||||
chatDefaultModelId: "claude-sonnet-4-5",
|
||||
chatDefaultThinkingLevel: "high",
|
||||
chatDefaultAgentId: "agent-alpha",
|
||||
} as SettingsFormState);
|
||||
|
||||
fireEvent.click(screen.getByTitle("Reset Chat default"));
|
||||
|
||||
expect(getForm()).toMatchObject({
|
||||
chatNewSessionMode: undefined,
|
||||
chatDefaultKind: undefined,
|
||||
chatDefaultAgentId: undefined,
|
||||
chatDefaultModelProvider: undefined,
|
||||
chatDefaultModelId: undefined,
|
||||
chatDefaultThinkingLevel: undefined,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -6407,7 +6407,25 @@
|
||||
"whenEnabledTasksCreatedWithoutATitleBut": " When enabled, tasks created without a title but with descriptions over 200 characters will automatically get an AI-generated title (max 60 characters). The same model is also used to generate fallback merge commit message bodies when the branch's commit log is empty (e.g. squash merges with no unique commits), and GitHub tracking issue titles when a tracked task has no title yet. Default: disabled. ",
|
||||
"autoSelectModelPresetHint": "Default: disabled.",
|
||||
"prTitlePromptInstructionsHelp": "Guides the AI-generated Create PR title. Leave blank to use the default PR metadata prompt. No default — unset.",
|
||||
"prDescriptionPromptInstructionsHelp": "Guides the AI-generated Create PR summary, changes, and testing sections. Leave blank to use the default PR metadata prompt. No default — unset."
|
||||
"prDescriptionPromptInstructionsHelp": "Guides the AI-generated Create PR summary, changes, and testing sections. Leave blank to use the default PR metadata prompt. No default — unset.",
|
||||
"chatHeading": "Chat",
|
||||
"chatDescription": "Choose the default target for new Direct chats and whether New Chat should prompt or immediately use that default.",
|
||||
"chatNewSessionMode": "New Chat behavior",
|
||||
"chatNewSessionModePrompt": "Prompt for model each time",
|
||||
"chatNewSessionModeAlwaysDefault": "Always use configured default",
|
||||
"chatNewSessionModeHelp": "Prompt mode opens New Chat with this default preselected. Always-default mode skips the dialog when the configured default is complete.",
|
||||
"chatDefaultKind": "Chat default target",
|
||||
"chatDefaultKindModel": "Model",
|
||||
"chatDefaultKindAgent": "Agent",
|
||||
"chatDefaultModel": "Chat Default Model",
|
||||
"selectChatDefaultModel": "Select a chat default model",
|
||||
"chatDefaultModelHelp": "Model-mode New Chat uses the built-in Fusion chat agent with this provider/model pair. Leave empty to fall back to prompting.",
|
||||
"chatDefaultAgent": "Chat Default Agent",
|
||||
"loadingAgents": "Loading agents…",
|
||||
"selectChatDefaultAgent": "Select a chat default agent",
|
||||
"chatDefaultAgentEmpty": "No agents are available for this project yet.",
|
||||
"chatDefaultAgentHelp": "Agent-mode New Chat starts a Direct chat with the selected durable agent.",
|
||||
"chatDefaultReset": "Reset Chat default"
|
||||
},
|
||||
"remote": {
|
||||
"acceptRoutes": " Accept routes ",
|
||||
|
||||
@@ -6387,7 +6387,25 @@
|
||||
"whenEnabledTasksCreatedWithoutATitleBut": "",
|
||||
"autoSelectModelPresetHint": "",
|
||||
"prTitlePromptInstructionsHelp": "",
|
||||
"prDescriptionPromptInstructionsHelp": ""
|
||||
"prDescriptionPromptInstructionsHelp": "",
|
||||
"chatHeading": "Chat",
|
||||
"chatDescription": "Choose the default target for new Direct chats and whether New Chat should prompt or immediately use that default.",
|
||||
"chatNewSessionMode": "New Chat behavior",
|
||||
"chatNewSessionModePrompt": "Prompt for model each time",
|
||||
"chatNewSessionModeAlwaysDefault": "Always use configured default",
|
||||
"chatNewSessionModeHelp": "Prompt mode opens New Chat with this default preselected. Always-default mode skips the dialog when the configured default is complete.",
|
||||
"chatDefaultKind": "Chat default target",
|
||||
"chatDefaultKindModel": "Model",
|
||||
"chatDefaultKindAgent": "Agent",
|
||||
"chatDefaultModel": "Chat Default Model",
|
||||
"selectChatDefaultModel": "Select a chat default model",
|
||||
"chatDefaultModelHelp": "Model-mode New Chat uses the built-in Fusion chat agent with this provider/model pair. Leave empty to fall back to prompting.",
|
||||
"chatDefaultAgent": "Chat Default Agent",
|
||||
"loadingAgents": "Loading agents…",
|
||||
"selectChatDefaultAgent": "Select a chat default agent",
|
||||
"chatDefaultAgentEmpty": "No agents are available for this project yet.",
|
||||
"chatDefaultAgentHelp": "Agent-mode New Chat starts a Direct chat with the selected durable agent.",
|
||||
"chatDefaultReset": "Reset Chat default"
|
||||
},
|
||||
"remote": {
|
||||
"acceptRoutes": "",
|
||||
|
||||
@@ -6387,7 +6387,25 @@
|
||||
"whenEnabledTasksCreatedWithoutATitleBut": "",
|
||||
"autoSelectModelPresetHint": "",
|
||||
"prTitlePromptInstructionsHelp": "",
|
||||
"prDescriptionPromptInstructionsHelp": ""
|
||||
"prDescriptionPromptInstructionsHelp": "",
|
||||
"chatHeading": "Chat",
|
||||
"chatDescription": "Choose the default target for new Direct chats and whether New Chat should prompt or immediately use that default.",
|
||||
"chatNewSessionMode": "New Chat behavior",
|
||||
"chatNewSessionModePrompt": "Prompt for model each time",
|
||||
"chatNewSessionModeAlwaysDefault": "Always use configured default",
|
||||
"chatNewSessionModeHelp": "Prompt mode opens New Chat with this default preselected. Always-default mode skips the dialog when the configured default is complete.",
|
||||
"chatDefaultKind": "Chat default target",
|
||||
"chatDefaultKindModel": "Model",
|
||||
"chatDefaultKindAgent": "Agent",
|
||||
"chatDefaultModel": "Chat Default Model",
|
||||
"selectChatDefaultModel": "Select a chat default model",
|
||||
"chatDefaultModelHelp": "Model-mode New Chat uses the built-in Fusion chat agent with this provider/model pair. Leave empty to fall back to prompting.",
|
||||
"chatDefaultAgent": "Chat Default Agent",
|
||||
"loadingAgents": "Loading agents…",
|
||||
"selectChatDefaultAgent": "Select a chat default agent",
|
||||
"chatDefaultAgentEmpty": "No agents are available for this project yet.",
|
||||
"chatDefaultAgentHelp": "Agent-mode New Chat starts a Direct chat with the selected durable agent.",
|
||||
"chatDefaultReset": "Reset Chat default"
|
||||
},
|
||||
"remote": {
|
||||
"acceptRoutes": "",
|
||||
|
||||
@@ -6387,7 +6387,25 @@
|
||||
"whenEnabledTasksCreatedWithoutATitleBut": "",
|
||||
"autoSelectModelPresetHint": "",
|
||||
"prTitlePromptInstructionsHelp": "",
|
||||
"prDescriptionPromptInstructionsHelp": ""
|
||||
"prDescriptionPromptInstructionsHelp": "",
|
||||
"chatHeading": "Chat",
|
||||
"chatDescription": "Choose the default target for new Direct chats and whether New Chat should prompt or immediately use that default.",
|
||||
"chatNewSessionMode": "New Chat behavior",
|
||||
"chatNewSessionModePrompt": "Prompt for model each time",
|
||||
"chatNewSessionModeAlwaysDefault": "Always use configured default",
|
||||
"chatNewSessionModeHelp": "Prompt mode opens New Chat with this default preselected. Always-default mode skips the dialog when the configured default is complete.",
|
||||
"chatDefaultKind": "Chat default target",
|
||||
"chatDefaultKindModel": "Model",
|
||||
"chatDefaultKindAgent": "Agent",
|
||||
"chatDefaultModel": "Chat Default Model",
|
||||
"selectChatDefaultModel": "Select a chat default model",
|
||||
"chatDefaultModelHelp": "Model-mode New Chat uses the built-in Fusion chat agent with this provider/model pair. Leave empty to fall back to prompting.",
|
||||
"chatDefaultAgent": "Chat Default Agent",
|
||||
"loadingAgents": "Loading agents…",
|
||||
"selectChatDefaultAgent": "Select a chat default agent",
|
||||
"chatDefaultAgentEmpty": "No agents are available for this project yet.",
|
||||
"chatDefaultAgentHelp": "Agent-mode New Chat starts a Direct chat with the selected durable agent.",
|
||||
"chatDefaultReset": "Reset Chat default"
|
||||
},
|
||||
"remote": {
|
||||
"acceptRoutes": "",
|
||||
|
||||
@@ -6387,7 +6387,25 @@
|
||||
"whenEnabledTasksCreatedWithoutATitleBut": "",
|
||||
"autoSelectModelPresetHint": "",
|
||||
"prTitlePromptInstructionsHelp": "",
|
||||
"prDescriptionPromptInstructionsHelp": ""
|
||||
"prDescriptionPromptInstructionsHelp": "",
|
||||
"chatHeading": "Chat",
|
||||
"chatDescription": "Choose the default target for new Direct chats and whether New Chat should prompt or immediately use that default.",
|
||||
"chatNewSessionMode": "New Chat behavior",
|
||||
"chatNewSessionModePrompt": "Prompt for model each time",
|
||||
"chatNewSessionModeAlwaysDefault": "Always use configured default",
|
||||
"chatNewSessionModeHelp": "Prompt mode opens New Chat with this default preselected. Always-default mode skips the dialog when the configured default is complete.",
|
||||
"chatDefaultKind": "Chat default target",
|
||||
"chatDefaultKindModel": "Model",
|
||||
"chatDefaultKindAgent": "Agent",
|
||||
"chatDefaultModel": "Chat Default Model",
|
||||
"selectChatDefaultModel": "Select a chat default model",
|
||||
"chatDefaultModelHelp": "Model-mode New Chat uses the built-in Fusion chat agent with this provider/model pair. Leave empty to fall back to prompting.",
|
||||
"chatDefaultAgent": "Chat Default Agent",
|
||||
"loadingAgents": "Loading agents…",
|
||||
"selectChatDefaultAgent": "Select a chat default agent",
|
||||
"chatDefaultAgentEmpty": "No agents are available for this project yet.",
|
||||
"chatDefaultAgentHelp": "Agent-mode New Chat starts a Direct chat with the selected durable agent.",
|
||||
"chatDefaultReset": "Reset Chat default"
|
||||
},
|
||||
"remote": {
|
||||
"acceptRoutes": "",
|
||||
|
||||
@@ -6387,7 +6387,25 @@
|
||||
"whenEnabledTasksCreatedWithoutATitleBut": "",
|
||||
"autoSelectModelPresetHint": "",
|
||||
"prTitlePromptInstructionsHelp": "",
|
||||
"prDescriptionPromptInstructionsHelp": ""
|
||||
"prDescriptionPromptInstructionsHelp": "",
|
||||
"chatHeading": "Chat",
|
||||
"chatDescription": "Choose the default target for new Direct chats and whether New Chat should prompt or immediately use that default.",
|
||||
"chatNewSessionMode": "New Chat behavior",
|
||||
"chatNewSessionModePrompt": "Prompt for model each time",
|
||||
"chatNewSessionModeAlwaysDefault": "Always use configured default",
|
||||
"chatNewSessionModeHelp": "Prompt mode opens New Chat with this default preselected. Always-default mode skips the dialog when the configured default is complete.",
|
||||
"chatDefaultKind": "Chat default target",
|
||||
"chatDefaultKindModel": "Model",
|
||||
"chatDefaultKindAgent": "Agent",
|
||||
"chatDefaultModel": "Chat Default Model",
|
||||
"selectChatDefaultModel": "Select a chat default model",
|
||||
"chatDefaultModelHelp": "Model-mode New Chat uses the built-in Fusion chat agent with this provider/model pair. Leave empty to fall back to prompting.",
|
||||
"chatDefaultAgent": "Chat Default Agent",
|
||||
"loadingAgents": "Loading agents…",
|
||||
"selectChatDefaultAgent": "Select a chat default agent",
|
||||
"chatDefaultAgentEmpty": "No agents are available for this project yet.",
|
||||
"chatDefaultAgentHelp": "Agent-mode New Chat starts a Direct chat with the selected durable agent.",
|
||||
"chatDefaultReset": "Reset Chat default"
|
||||
},
|
||||
"remote": {
|
||||
"acceptRoutes": "",
|
||||
|
||||
18
packages/i18n/src/resources.d.ts
vendored
18
packages/i18n/src/resources.d.ts
vendored
@@ -6372,6 +6372,21 @@ export default interface Resources {
|
||||
"autoSelectPresetBasedOnTaskSize": " Auto-select preset based on task size ",
|
||||
"autoSummarizeLongDescriptionsAsTitles": " Auto-summarize long descriptions as titles ",
|
||||
"automaticallyCompactContextWhenApproachingThisTokenCount": "Automatically compact context when approaching this token count. Leave empty for no cap (compact only on overflow errors). Set a number to proactively compact when reaching this token count. No default — unset (no cap).",
|
||||
"chatDefaultAgent": "Chat Default Agent",
|
||||
"chatDefaultAgentEmpty": "No agents are available for this project yet.",
|
||||
"chatDefaultAgentHelp": "Agent-mode New Chat starts a Direct chat with the selected durable agent.",
|
||||
"chatDefaultKind": "Chat default target",
|
||||
"chatDefaultKindAgent": "Agent",
|
||||
"chatDefaultKindModel": "Model",
|
||||
"chatDefaultModel": "Chat Default Model",
|
||||
"chatDefaultModelHelp": "Model-mode New Chat uses the built-in Fusion chat agent with this provider/model pair. Leave empty to fall back to prompting.",
|
||||
"chatDefaultReset": "Reset Chat default",
|
||||
"chatDescription": "Choose the default target for new Direct chats and whether New Chat should prompt or immediately use that default.",
|
||||
"chatHeading": "Chat",
|
||||
"chatNewSessionMode": "New Chat behavior",
|
||||
"chatNewSessionModeAlwaysDefault": "Always use configured default",
|
||||
"chatNewSessionModeHelp": "Prompt mode opens New Chat with this default preselected. Always-default mode skips the dialog when the configured default is complete.",
|
||||
"chatNewSessionModePrompt": "Prompt for model each time",
|
||||
"configuredPresets": "Configured presets",
|
||||
"configuresTheModelUsedForTwoShortSummary": " Configures the model used for two short-summary jobs: auto-generating task titles from long descriptions, and generating merge commit summaries from step commits and diff stats. ",
|
||||
"defaultWorkflowModelLaneActions": "Default workflow model lane actions",
|
||||
@@ -6380,6 +6395,7 @@ export default interface Resources {
|
||||
"edit": " Edit ",
|
||||
"executorModel": "Executor model",
|
||||
"fallsBackTo": " Falls back to: ",
|
||||
"loadingAgents": "Loading agents…",
|
||||
"loadingAvailableModels": "Loading available models…",
|
||||
"loadingWorkflowModelLanes": "Loading workflow model lanes…",
|
||||
"modelLanes": "Model Lanes",
|
||||
@@ -6401,6 +6417,8 @@ export default interface Resources {
|
||||
"resetToInheritFromGlobal": "Reset to inherit from global",
|
||||
"resetToInheritFromWorkflow": "Reset to inherit from workflow",
|
||||
"reviewerModel": "Reviewer model",
|
||||
"selectChatDefaultAgent": "Select a chat default agent",
|
||||
"selectChatDefaultModel": "Select a chat default model",
|
||||
"theseProjectOverridesApplyToTheActiveDefault": " These project overrides apply to the active default workflow. ",
|
||||
"tokenCap": "Token Cap",
|
||||
"useDefault": "Use default",
|
||||
|
||||
Reference in New Issue
Block a user