feat: configure supervisor model and story graph use per conversation
Add a supervisor text-model selector using existing connections and a per-conversation story graph switch above knowledge settings. Hide the switch when Supervisor is disabled and enforce conversation preferences at local and remote tool execution boundaries. Release note: Choose a dedicated supervisor model and control whether each conversation can query existing stories and experiences. Enabling graph access does not start a review or automatically insert context.
This commit is contained in:
@@ -586,6 +586,7 @@ const api: DesktopApi & RuntimeNativeClientApi = {
|
||||
replace: vi.fn(async () => {}),
|
||||
saveLocal: vi.fn(async () => {}),
|
||||
setPinned: vi.fn(async () => {}),
|
||||
setStoryGraph: vi.fn(async () => {}),
|
||||
branchLocal: vi.fn(async (input) => ({
|
||||
id: crypto.randomUUID(),
|
||||
branch: {
|
||||
@@ -8544,6 +8545,33 @@ describe("App", () => {
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("saves the story graph switch through the conversation API and restores it on reload", async () => {
|
||||
installRemoteProjectsSetting(false);
|
||||
const id = "00000000-0000-4000-8000-000000000311";
|
||||
const saved = { id, projectId: project.id, title: "Story preference", updatedAt: 1, messages: [] };
|
||||
vi.mocked(api.conversations.list).mockResolvedValue([saved]);
|
||||
const pending = deferred<void>();
|
||||
vi.mocked(api.conversations.setStoryGraph).mockImplementationOnce(() => pending.promise);
|
||||
const view = render(<App />);
|
||||
await screen.findByLabelText("向 GoodBuddy 提问");
|
||||
openComposerOptions();
|
||||
const toggle = await screen.findByRole("switch", { name: "使用故事图谱" });
|
||||
expect(toggle).toBeChecked();
|
||||
fireEvent.click(toggle);
|
||||
await waitFor(() => expect(api.conversations.setStoryGraph).toHaveBeenCalledWith({ conversationId: id, enabled: false }));
|
||||
expect(toggle).toBeChecked();
|
||||
expect(toggle).toBeDisabled();
|
||||
await act(async () => pending.resolve());
|
||||
await waitFor(() => { expect(toggle).not.toBeChecked(); expect(toggle).toBeEnabled(); });
|
||||
view.unmount();
|
||||
vi.mocked(api.conversations.list).mockResolvedValue([{ ...saved, storyGraphEnabled: false }]);
|
||||
render(<App />);
|
||||
await screen.findByLabelText("向 GoodBuddy 提问");
|
||||
openComposerOptions();
|
||||
expect(await screen.findByRole("switch", { name: "使用故事图谱" })).not.toBeChecked();
|
||||
expect(api.agent.run).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("groups composer tools and exposes clear control descriptions", async () => {
|
||||
render(<App />);
|
||||
|
||||
|
||||
@@ -853,6 +853,7 @@ function toConversationSnapshots(
|
||||
workMode: conversation.workMode,
|
||||
knowledgeLibraryIds: conversation.knowledgeLibraryIds,
|
||||
knowledgeRetrievalMode: conversation.knowledgeRetrievalMode,
|
||||
storyGraphEnabled: conversation.storyGraphEnabled,
|
||||
contextMetrics: conversation.contextMetrics,
|
||||
contextCompressionState: conversation.contextCompressionState,
|
||||
...(conversation.branch ? { branch: conversation.branch } : {}),
|
||||
@@ -6666,6 +6667,16 @@ function App(): React.JSX.Element {
|
||||
openImageViewer,
|
||||
openModelSettings: openImageModelSettings,
|
||||
setEnabledKnowledgeLibraryIds,
|
||||
setStoryGraphEnabled: async (conversationId, enabled) => {
|
||||
persistLocalConversationChanges();
|
||||
await conversationPersistence.enqueue(async () => {
|
||||
await window.goodbuddy.conversations.setStoryGraph({ conversationId, enabled });
|
||||
setConversations((current) => current.map((conversation) =>
|
||||
conversation.id === conversationId
|
||||
? { ...conversation, storyGraphEnabled: enabled, updatedAt: Date.now() }
|
||||
: conversation));
|
||||
});
|
||||
},
|
||||
setKnowledgeRetrievalMode: (mode) =>
|
||||
setConversations((current) =>
|
||||
current.map((conversation) =>
|
||||
@@ -7463,6 +7474,7 @@ function App(): React.JSX.Element {
|
||||
selectedRuntimeAgent={selectedRuntimeAgent}
|
||||
selectedRuntimeCommand={selectedRuntimeCommand}
|
||||
selectingContextFiles={selectingContextFiles}
|
||||
supervisorEnabled={applicationSettings?.heartbeatEnabled === true}
|
||||
updateAttachmentBusy={updateAttachmentBusy}
|
||||
voiceListening={voiceListening}
|
||||
voiceRecording={voiceRecording}
|
||||
@@ -7569,6 +7581,7 @@ function App(): React.JSX.Element {
|
||||
}
|
||||
>
|
||||
{isApplicationEnabled(applicationSettings, 'heartbeat') ? <HeartbeatCenter
|
||||
runtimeSettings={runtimeSettings}
|
||||
applicationSettings={applicationSettings}
|
||||
applicationSettingsPending={applicationSettingsPending}
|
||||
applicationSettingsLocked={applicationSettingsUnconfirmed}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { act, cleanup, render, screen } from "@testing-library/react";
|
||||
import { act, cleanup, fireEvent, render, screen, waitFor } from "@testing-library/react";
|
||||
import { createRef, Profiler } from "react";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { Conversation } from "./chat-conversation";
|
||||
@@ -98,6 +98,37 @@ afterEach(() => {
|
||||
});
|
||||
|
||||
describe("Composer render boundary", () => {
|
||||
it("hides the story graph switch with Supervisor off and keeps conversation state while saving or failing", async () => {
|
||||
const { props, rerender, store } = setup();
|
||||
fireEvent.click(screen.getByRole("button", { name: "Options" }));
|
||||
expect(screen.queryByRole("switch", { name: "Use story graph" })).toBeNull();
|
||||
let finish!: () => void;
|
||||
const save = vi.fn(() => new Promise<void>(resolve => { finish = resolve; }));
|
||||
const notify = vi.fn();
|
||||
const next = { ...props, supervisorEnabled: true, actions: { ...actions, setStoryGraphEnabled: save, notify } };
|
||||
rerender(<Composer {...next} />);
|
||||
const toggle = screen.getByRole("switch", { name: "Use story graph" });
|
||||
expect(toggle).toBeChecked();
|
||||
expect(toggle.closest("label")).toHaveClass("toggle-row");
|
||||
fireEvent.click(toggle);
|
||||
expect(save).toHaveBeenCalledWith("a", false);
|
||||
expect(toggle).toBeChecked();
|
||||
expect(toggle).toBeDisabled();
|
||||
await act(async () => {
|
||||
store.set(current => current.map(item => item.id === "a" ? { ...item, storyGraphEnabled: false } : item));
|
||||
finish();
|
||||
});
|
||||
await waitFor(() => { expect(toggle).not.toBeChecked(); expect(toggle).toBeEnabled(); });
|
||||
save.mockRejectedValueOnce(new Error("Save failed"));
|
||||
fireEvent.click(toggle);
|
||||
await waitFor(() => { expect(toggle).toBeEnabled(); expect(toggle).not.toBeChecked(); expect(notify).toHaveBeenCalledWith({ tone: "error", message: "Save failed" }); });
|
||||
rerender(<Composer {...next} conversationId="b" />);
|
||||
fireEvent.click(screen.getByRole("button", { name: "Options" }));
|
||||
expect(screen.getByRole("switch", { name: "Use story graph" })).toBeChecked();
|
||||
rerender(<Composer {...next} supervisorEnabled={false} />);
|
||||
expect(screen.queryByRole("switch", { name: "Use story graph", hidden: true })).toBeNull();
|
||||
});
|
||||
|
||||
it("does not re-render when another conversation changes", () => {
|
||||
const { renders, store } = setup();
|
||||
act(() => store.set((current) => current.map((item) =>
|
||||
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
type RefObject,
|
||||
} from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
@@ -127,6 +128,7 @@ export type ComposerProps = {
|
||||
selectedRuntimeAgent: string;
|
||||
selectedRuntimeCommand: string;
|
||||
selectingContextFiles: boolean;
|
||||
supervisorEnabled?: boolean;
|
||||
updateAttachmentBusy: (busy: boolean) => void;
|
||||
voiceListening: boolean;
|
||||
voiceRecording: boolean;
|
||||
@@ -194,6 +196,7 @@ export const Composer = memo(function Composer({
|
||||
selectedRuntimeAgent,
|
||||
selectedRuntimeCommand,
|
||||
selectingContextFiles,
|
||||
supervisorEnabled,
|
||||
updateAttachmentBusy,
|
||||
voiceListening,
|
||||
voiceRecording,
|
||||
@@ -202,6 +205,7 @@ export const Composer = memo(function Composer({
|
||||
}: ComposerProps): React.JSX.Element {
|
||||
const { t } = useTranslation("app");
|
||||
const view = useComposerConversationView(conversationStore, conversationId);
|
||||
const [storyGraphSaving, setStoryGraphSaving] = useState(false);
|
||||
const isRunning = view?.running ?? false;
|
||||
// Popups belong to one conversation, run state and view, like the former
|
||||
// reset in App: changing any of them shows every popup closed.
|
||||
@@ -329,7 +333,7 @@ export const Composer = memo(function Composer({
|
||||
}
|
||||
};
|
||||
const frame = requestAnimationFrame(() => {
|
||||
composerOptionsRef.current?.querySelector<HTMLButtonElement>("button:not(:disabled)")?.focus();
|
||||
composerOptionsRef.current?.querySelector<HTMLElement>("input:not(:disabled), button:not(:disabled)")?.focus();
|
||||
});
|
||||
document.addEventListener("pointerdown", dismissOutside);
|
||||
document.addEventListener("focusin", dismissOutside);
|
||||
@@ -858,6 +862,23 @@ export const Composer = memo(function Composer({
|
||||
}}
|
||||
>
|
||||
<strong>{t("composer.settings")}</strong>
|
||||
{supervisorEnabled && (
|
||||
<label className="toggle-row">
|
||||
<span>{t("composer.storyGraph.label")}</span>
|
||||
<input
|
||||
type="checkbox"
|
||||
role="switch"
|
||||
checked={view?.storyGraphEnabled !== false}
|
||||
disabled={storyGraphSaving}
|
||||
onChange={(event) => {
|
||||
setStoryGraphSaving(true);
|
||||
void actions.setStoryGraphEnabled(conversationId, event.target.checked)
|
||||
.catch((error: unknown) => actions.notify({ tone: "error", message: error instanceof Error ? error.message : String(error) }))
|
||||
.finally(() => setStoryGraphSaving(false));
|
||||
}}
|
||||
/>
|
||||
</label>
|
||||
)}
|
||||
{knowledgeLibraries.length > 0 && (
|
||||
<div
|
||||
className="knowledge-scope"
|
||||
|
||||
@@ -25,6 +25,7 @@ import { PageShell } from './WorkspacePrimitives'
|
||||
import i18n from './i18n'
|
||||
import { WorkspaceUnsavedChangesContext } from './workspace-unsaved-changes'
|
||||
import { applicationSettingsSchema, defaultLocalToolEnvironmentSettings } from '../../shared/application-settings-contracts'
|
||||
import type { RuntimeSettings } from '../../shared/contracts'
|
||||
|
||||
afterEach(cleanup)
|
||||
|
||||
@@ -334,6 +335,40 @@ describe('HeartbeatCenter', () => {
|
||||
view.rerender(<HeartbeatCenter {...props} applicationSettings={{ ...applicationSettings, supervisorOrganizeTimeoutSeconds: 30, supervisorModelConcurrency: 4 }} />)
|
||||
expect(screen.getByRole('button', { name: t('timeouts.save') })).toBeDisabled()
|
||||
})
|
||||
it.each(['zh-CN', 'en-US'])('saves an existing text model reference, retains failed drafts, and restores app default (%s)', async language => {
|
||||
await i18n.changeLanguage(language)
|
||||
const t = (key: string) => i18n.t(key, { ns: 'heartbeat' })
|
||||
const profileId = '00000000-0000-4000-8000-000000000201'
|
||||
const applicationSettings = applicationSettingsSchema.parse({ checkUpdatesOnStartup: true, updateSource: 'github', modelDownloadSource: 'modelscope', localToolEnvironment: defaultLocalToolEnvironmentSettings, conversationHtmlRenderingEnabled: true, remoteProjectsEnabled: false })
|
||||
const runtimeSettings = { modelProfiles: [
|
||||
{ id: profileId, name: 'Reviewer', modelName: 'review-model', protocol: 'openai-chat-completions' },
|
||||
{ id: 'image', name: 'Image only', modelName: 'image-model', protocol: 'openai-images-generations' }
|
||||
] } as RuntimeSettings
|
||||
const onUpdateApplicationSettings = vi.fn(async () => false)
|
||||
const props = createProps({ applicationSettings, runtimeSettings, onUpdateApplicationSettings })
|
||||
const view = render(<HeartbeatCenter {...props} />, false)
|
||||
fireEvent.click(screen.getByRole('tab', { name: t('supervisor.settings') }))
|
||||
const selector = screen.getByRole('combobox', { name: t('timeouts.model') })
|
||||
expect(selector).toHaveValue('')
|
||||
expect(within(selector).queryByText(/Image only/)).not.toBeInTheDocument()
|
||||
fireEvent.change(selector, { target: { value: profileId } })
|
||||
fireEvent.click(screen.getByRole('button', { name: t('timeouts.save') }))
|
||||
await waitFor(() => expect(onUpdateApplicationSettings).toHaveBeenCalledExactlyOnceWith({
|
||||
supervisorModelProfileId: profileId, supervisorOrganizeTimeoutSeconds: 240, supervisorModelConcurrency: 1
|
||||
}))
|
||||
view.rerender(<HeartbeatCenter {...props} applicationSettingsError="Save failed" />)
|
||||
expect(selector).toHaveValue(profileId)
|
||||
view.rerender(<HeartbeatCenter {...props} applicationSettings={{ ...applicationSettings, supervisorModelProfileId: profileId }} />)
|
||||
expect(screen.getByRole('button', { name: t('timeouts.save') })).toBeDisabled()
|
||||
view.rerender(<HeartbeatCenter {...props} runtimeSettings={{ ...runtimeSettings, modelProfiles: [] }} applicationSettings={{ ...applicationSettings, supervisorModelProfileId: profileId }} />)
|
||||
expect(within(selector).getByRole('option', { name: t('timeouts.unavailableModel') })).toBeDisabled()
|
||||
fireEvent.change(selector, { target: { value: '' } })
|
||||
fireEvent.click(screen.getByRole('button', { name: t('timeouts.save') }))
|
||||
await waitFor(() => expect(onUpdateApplicationSettings).toHaveBeenLastCalledWith({
|
||||
supervisorModelProfileId: null, supervisorOrganizeTimeoutSeconds: 240, supervisorModelConcurrency: 1
|
||||
}))
|
||||
})
|
||||
|
||||
it.each(['zh-CN', 'en-US'])('opens complete automatic supervision settings without creating defaults (%s)', async (language) => {
|
||||
await i18n.changeLanguage(language)
|
||||
const props = createProps({ configs: [], runs: [], entries: [] })
|
||||
|
||||
@@ -18,6 +18,7 @@ import type {
|
||||
HeartbeatUpdateInput
|
||||
} from '../../shared/assistant-contracts'
|
||||
import { HeartbeatSettings } from './HeartbeatSettings'
|
||||
import { isAgentRuntimeModelProtocol, type RuntimeSettings } from '../../shared/contracts'
|
||||
import { defaultSupervisionTimeoutSeconds, supervisionTimeoutSecondsSchema, defaultSupervisorModelConcurrency, supervisorModelConcurrencySchema, type ApplicationSettings, type ApplicationSettingsUpdate } from '../../shared/application-settings-contracts'
|
||||
import { SupervisionReviewSettings, type SupervisionSettingsSection } from './SupervisionReviewSettings'
|
||||
import { SupervisorWorkspace, type SupervisionGraphNavigation } from './SupervisorWorkspace'
|
||||
@@ -34,6 +35,7 @@ import {
|
||||
} from './WorkspacePrimitives'
|
||||
|
||||
export type HeartbeatCenterProps = {
|
||||
runtimeSettings?: RuntimeSettings
|
||||
applicationSettings?: ApplicationSettings
|
||||
applicationSettingsPending?: boolean
|
||||
applicationSettingsLocked?: boolean
|
||||
@@ -200,24 +202,37 @@ function SupervisionModelSettings(props: HeartbeatCenterProps): React.JSX.Elemen
|
||||
const { t } = useTranslation('heartbeat')
|
||||
const supervisor = props.applicationSettings?.supervisorOrganizeTimeoutSeconds ?? defaultSupervisionTimeoutSeconds
|
||||
const concurrency = props.applicationSettings?.supervisorModelConcurrency ?? defaultSupervisorModelConcurrency
|
||||
const profileId = props.applicationSettings?.supervisorModelProfileId ?? ''
|
||||
const profiles = useMemo(() => props.runtimeSettings?.modelProfiles.filter(profile => isAgentRuntimeModelProtocol(profile.protocol)) ?? [], [props.runtimeSettings])
|
||||
const [model, setModel] = useState(profileId)
|
||||
const [parallel, setParallel] = useState(String(concurrency))
|
||||
const [organize, setOrganize] = useState(String(supervisor))
|
||||
const [saved, setSaved] = useState({ supervisor, concurrency })
|
||||
if (saved.supervisor !== supervisor || saved.concurrency !== concurrency) {
|
||||
setSaved({ supervisor, concurrency })
|
||||
const [saved, setSaved] = useState({ supervisor, concurrency, profileId })
|
||||
if (saved.supervisor !== supervisor || saved.concurrency !== concurrency || saved.profileId !== profileId) {
|
||||
setSaved({ supervisor, concurrency, profileId })
|
||||
setModel(profileId)
|
||||
setParallel(String(concurrency))
|
||||
setOrganize(String(supervisor))
|
||||
}
|
||||
useWorkspaceUnsavedChanges(organize !== String(supervisor) || parallel !== String(concurrency))
|
||||
useWorkspaceUnsavedChanges(organize !== String(supervisor) || parallel !== String(concurrency) || model !== profileId)
|
||||
const valid = supervisionTimeoutSecondsSchema.safeParse(Number(organize)).success &&
|
||||
supervisorModelConcurrencySchema.safeParse(Number(parallel)).success
|
||||
const disabled = props.applicationSettingsPending || props.applicationSettingsLocked || !props.applicationSettings || !props.onUpdateApplicationSettings
|
||||
return <form className="heartbeat-settings heartbeat-settings__editor" onSubmit={(event) => {
|
||||
event.preventDefault()
|
||||
if (!valid || disabled) return
|
||||
void props.onUpdateApplicationSettings?.({ supervisorOrganizeTimeoutSeconds: Number(organize), supervisorModelConcurrency: Number(parallel) })
|
||||
void props.onUpdateApplicationSettings?.({ supervisorOrganizeTimeoutSeconds: Number(organize), supervisorModelConcurrency: Number(parallel),
|
||||
...(model !== profileId ? { supervisorModelProfileId: model || null } : {}) })
|
||||
}}>
|
||||
<h2>{t('timeouts.title')}</h2>
|
||||
<label className="heartbeat-settings__field">{t('timeouts.model')}
|
||||
<select value={model} aria-describedby="supervisor-model-help" disabled={disabled || !props.runtimeSettings} onChange={event => setModel(event.target.value)}>
|
||||
<option value="">{t('timeouts.followDefault')}</option>
|
||||
{model && !profiles.some(profile => profile.id === model) && <option value={model} disabled>{t('timeouts.unavailableModel')}</option>}
|
||||
{profiles.map(profile => <option key={profile.id} value={profile.id}>{profile.name} ({profile.modelName})</option>)}
|
||||
</select>
|
||||
</label>
|
||||
<p id="supervisor-model-help">{t('timeouts.modelHelp')}</p>
|
||||
<p>{t('timeouts.help')}</p>
|
||||
<label className="heartbeat-settings__field">{t('timeouts.organize')}
|
||||
<input type="number" min={30} max={600} step={1} value={organize} disabled={disabled}
|
||||
@@ -234,7 +249,7 @@ function SupervisionModelSettings(props: HeartbeatCenterProps): React.JSX.Elemen
|
||||
<p>{props.applicationSettingsError}</p>
|
||||
<button className="secondary-button" type="button" disabled={props.applicationSettingsPending} onClick={props.onRetryApplicationSettings}>{t('center.actions.retry')}</button>
|
||||
</div>}
|
||||
<button className="primary-button" type="submit" disabled={disabled || !valid || (Number(organize) === supervisor && Number(parallel) === concurrency)}>{t('timeouts.save')}</button>
|
||||
<button className="primary-button" type="submit" disabled={disabled || !valid || (Number(organize) === supervisor && Number(parallel) === concurrency && model === profileId)}>{t('timeouts.save')}</button>
|
||||
</form>
|
||||
}
|
||||
function HeartbeatSections({
|
||||
|
||||
@@ -61,6 +61,7 @@ export function toLocalConversationHeader(
|
||||
workMode: conversation.workMode,
|
||||
knowledgeLibraryIds: conversation.knowledgeLibraryIds,
|
||||
knowledgeRetrievalMode: conversation.knowledgeRetrievalMode,
|
||||
storyGraphEnabled: conversation.storyGraphEnabled,
|
||||
contextMetrics: conversation.contextMetrics,
|
||||
contextCompressionState: conversation.contextCompressionState,
|
||||
...(conversation.branch ? { branch: conversation.branch } : {}),
|
||||
|
||||
@@ -250,7 +250,7 @@ export function useActiveConversationView(
|
||||
*/
|
||||
export type ComposerConversationView = Pick<
|
||||
Conversation,
|
||||
| "id" | "runtimeSelection" | "knowledgeLibraryIds" | "knowledgeRetrievalMode"
|
||||
| "id" | "runtimeSelection" | "knowledgeLibraryIds" | "knowledgeRetrievalMode" | "storyGraphEnabled"
|
||||
| "remote" | "contextMetrics" | "contextCompressionState"
|
||||
> & { running: boolean };
|
||||
|
||||
@@ -263,6 +263,7 @@ export function selectComposerConversationView(
|
||||
runtimeSelection: conversation.runtimeSelection,
|
||||
knowledgeLibraryIds: conversation.knowledgeLibraryIds,
|
||||
knowledgeRetrievalMode: conversation.knowledgeRetrievalMode,
|
||||
storyGraphEnabled: conversation.storyGraphEnabled,
|
||||
remote: conversation.remote,
|
||||
contextMetrics: conversation.contextMetrics,
|
||||
contextCompressionState: conversation.contextCompressionState,
|
||||
@@ -277,6 +278,7 @@ export function sameComposerConversationView(
|
||||
if (left === right) return true;
|
||||
if (!left || !right) return false;
|
||||
return left.id === right.id && left.running === right.running &&
|
||||
left.storyGraphEnabled === right.storyGraphEnabled &&
|
||||
left.knowledgeRetrievalMode === right.knowledgeRetrievalMode &&
|
||||
sameArrayItems(left.knowledgeLibraryIds ?? [], right.knowledgeLibraryIds ?? []) &&
|
||||
(left.knowledgeLibraryIds === undefined) === (right.knowledgeLibraryIds === undefined) &&
|
||||
|
||||
@@ -563,6 +563,7 @@ export const app = {
|
||||
},
|
||||
removeAttachment: 'Remove {{name}}',
|
||||
settings: 'Conversation settings',
|
||||
storyGraph: { label: 'Use story graph' },
|
||||
options: 'Options',
|
||||
expertLabel: 'Expert role',
|
||||
modeLabel: 'Work mode',
|
||||
|
||||
@@ -43,6 +43,10 @@ export const heartbeat = {
|
||||
dismiss: 'Dismiss'
|
||||
},
|
||||
timeouts: {
|
||||
model: 'Model',
|
||||
followDefault: 'Follow app default',
|
||||
unavailableModel: 'Selected model is unavailable',
|
||||
modelHelp: 'Uses an existing text model for reviews, summaries, stories, experiences, and suggestions. Saving affects the next execution, including continue or retry; a review already running keeps its model.',
|
||||
title: 'Model timeouts and concurrency',
|
||||
report: 'Heartbeat report timeout (seconds)',
|
||||
organize: 'Supervisor organize timeout (seconds)',
|
||||
|
||||
@@ -543,6 +543,7 @@ export const app = {
|
||||
},
|
||||
removeAttachment: '移除 {{name}}',
|
||||
settings: '对话设置',
|
||||
storyGraph: { label: '使用故事图谱' },
|
||||
options: '选项',
|
||||
expertLabel: '专家角色',
|
||||
modeLabel: '工作模式',
|
||||
|
||||
@@ -40,6 +40,10 @@ export const heartbeat = {
|
||||
dismiss: '忽略'
|
||||
},
|
||||
timeouts: {
|
||||
model: '模型',
|
||||
followDefault: '跟随应用默认',
|
||||
unavailableModel: '所选模型已不可用',
|
||||
modelHelp: '复用已有文本模型,用于回顾、汇总、故事、经验和建议。保存后从下次执行生效,包括继续或重试;正在运行的回顾保持原模型。',
|
||||
title: '模型超时与并发',
|
||||
report: '心跳报告超时(秒)',
|
||||
organize: '监督者整理超时(秒)',
|
||||
|
||||
@@ -36,6 +36,7 @@ export type ComposerActions = {
|
||||
openModelSettings: () => void;
|
||||
setEnabledKnowledgeLibraryIds: (update: SetStateAction<string[]>) => void;
|
||||
setKnowledgeRetrievalMode: (mode: "auto" | "always") => void;
|
||||
setStoryGraphEnabled: (conversationId: string, enabled: boolean) => Promise<void>;
|
||||
selectExpert: (expertId: string) => void;
|
||||
selectRuntimeAgent: (agentId: string) => void;
|
||||
selectContinuePreset: (presetId: string) => void;
|
||||
|
||||
Reference in New Issue
Block a user