mirror of
https://github.com/esengine/DeepSeek-Reasonix.git
synced 2026-11-07 10:45:59 -10:00
feat(studio): hooks and scheduled runs move from the extensions page into an Automation card on the Agent behaviour page, as the new Set-automation board draws it
This commit is contained in:
1 parent
ab6052b8a2
commit
5868b02bdf
7 files changed
+56
-23
No files matched your search
@@ -55,6 +55,8 @@ export const SETTINGS: Messages = {
|
||||
"settings.global_shortcuts_are_not_supported": { zh: "当前环境不支持全局快捷键。", en: "Global shortcuts are not supported in this environment." },
|
||||
"settings.global_show_hide_shortcut": { zh: "全局显示 / 隐藏快捷键", en: "Global show / hide shortcut" },
|
||||
"settings.gone_from_the_next_launch": { zh: "下次启动时不再显示,本次仍保留", en: "Gone from the next launch on; still here for this one" },
|
||||
"settings.hooks": { zh: "钩子", en: "Hooks" },
|
||||
"settings.hooks_hint": { zh: "在工具调用前后运行你自己的命令", en: "Run your own commands before and after tool calls" },
|
||||
"settings.how_it_thinks_how_it": { zh: "它怎么想、怎么问你、怎么协作", en: "How it thinks, how it asks you, how it works with others" },
|
||||
"settings.icon_rail": { zh: "图标栏", en: "Icon rail" },
|
||||
"settings.idle_page_sleep": { zh: "空闲网页休眠", en: "Idle page sleep" },
|
||||
@@ -84,8 +86,11 @@ export const SETTINGS: Messages = {
|
||||
"settings.n_10_minutes": { zh: "10 分钟", en: "10 minutes" },
|
||||
"settings.n_30_minutes": { zh: "30 分钟", en: "30 minutes" },
|
||||
"settings.n_4_hours": { zh: "4 小时", en: "4 hours" },
|
||||
"settings.n_hooks": { zh: "{n} 条", en: "{n}" },
|
||||
"settings.n_items": { zh: "{n} 项", en: "{n} items" },
|
||||
"settings.n_packages": { zh: "{n} 个包", en: "{n} packages" },
|
||||
"settings.n_paused": { zh: ",{n} 个暂停", en: ", {n} paused" },
|
||||
"settings.n_scheduled": { zh: "{n} 个", en: "{n}" },
|
||||
"settings.n_services": { zh: "{n} 个服务", en: "{n} services" },
|
||||
"settings.n_with_problems": { zh: "{n} 个异常", en: "{n} with problems" },
|
||||
"settings.narration_between_tool_calls": { zh: "回合间叙述", en: "Narration between tool calls" },
|
||||
@@ -132,6 +137,7 @@ export const SETTINGS: Messages = {
|
||||
"settings.runtime": { zh: "运行时", en: "Runtime" },
|
||||
"settings.sandbox": { zh: "沙箱", en: "Sandbox" },
|
||||
"settings.sandbox_and_execution": { zh: "沙箱与执行", en: "Sandbox and execution" },
|
||||
"settings.scheduled_runs_hint": { zh: "到点让 Agent 只读地跑一遍并留下报告", en: "Have the agent run a read-only pass on a schedule and leave a report" },
|
||||
"settings.search_conversations": { zh: "搜索会话", en: "Search conversations" },
|
||||
"settings.search_settings": { zh: "搜索设置", en: "Search settings" },
|
||||
"settings.send_after_the_whole_run": { zh: "整轮结束后发送", en: "Send after the whole run" },
|
||||
|
||||
@@ -19,10 +19,10 @@ function draw() {
|
||||
return go;
|
||||
}
|
||||
|
||||
it("is three cards of single rows, plan mode and the display switches living elsewhere", async () => {
|
||||
it("is four cards of single rows, plan mode and the display switches living elsewhere", async () => {
|
||||
draw();
|
||||
expect([...document.querySelectorAll(".prefs-card > header h3")].map((h) => h.textContent)).toEqual(["人设与预设", "计划与提问", "协作"]);
|
||||
for (const id of ["persona", "agent-presets", "plan-approval", "ask-timeout", "turn-narration", "progress-watch", "session-ref", "subagent-claims", "peers"]) {
|
||||
expect([...document.querySelectorAll(".prefs-card > header h3")].map((h) => h.textContent)).toEqual(["人设与预设", "计划与提问", "协作", "自动化"]);
|
||||
for (const id of ["persona", "agent-presets", "plan-approval", "ask-timeout", "turn-narration", "progress-watch", "session-ref", "subagent-claims", "peers", "hooks", "scheduled-runs"]) {
|
||||
await waitFor(() => expect(document.querySelector(`[data-setting="${id}"]`), id).toBeTruthy());
|
||||
}
|
||||
for (const id of ["plan-mode", "changed-files", "fold-work", "feedback-level-notices"]) expect(document.querySelector(`[data-setting="${id}"]`), id).toBeNull();
|
||||
@@ -35,3 +35,16 @@ it("opens the presets page from both the persona row and the presets row", () =>
|
||||
fireEvent.click(screen.getByRole("button", { name: "管理预设" }));
|
||||
expect(go).toHaveBeenLastCalledWith("agent-presets");
|
||||
});
|
||||
|
||||
it("opens the hooks and the scheduled-runs pages from the automation card", async () => {
|
||||
const go = draw();
|
||||
const card = [...document.querySelectorAll<HTMLElement>(".prefs-card")].find((c) => c.querySelector("header h3")?.textContent === "自动化")!;
|
||||
const buttons = [...card.querySelectorAll("button")].filter((b) => b.textContent === "管理");
|
||||
expect(buttons).toHaveLength(2);
|
||||
fireEvent.click(buttons[0]);
|
||||
expect(go).toHaveBeenLastCalledWith("hooks");
|
||||
fireEvent.click(buttons[1]);
|
||||
expect(go).toHaveBeenLastCalledWith("scheduled-runs");
|
||||
expect(card.textContent).toContain("在工具调用前后运行你自己的命令");
|
||||
expect(card.textContent).toContain("到点让 Agent 只读地跑一遍并留下报告");
|
||||
});
|
||||
@@ -1,8 +1,11 @@
|
||||
import { useContext } from "react";
|
||||
import { useContext, useEffect, useState } from "react";
|
||||
import { t } from "../i18n";
|
||||
import type { HubPort } from "../port/hub";
|
||||
import type { AgentPort } from "../port/port";
|
||||
import { AgentPresets } from "./AgentPresets";
|
||||
import { Hooks } from "./Hooks";
|
||||
import { Group } from "./Group";
|
||||
import { ScheduledRuns } from "./ScheduledRuns";
|
||||
import { ApplyNote } from "./Group";
|
||||
import { AskTimeout } from "./AskTimeout";
|
||||
import { PeersSection } from "./PeersSection";
|
||||
@@ -21,8 +24,26 @@ interface Props {
|
||||
onChanged: () => void;
|
||||
}
|
||||
|
||||
// How many of each there are, for the card's rows. A count that cannot be read
|
||||
// is left out rather than guessed.
|
||||
function useAutomationCounts(port: AgentPort) {
|
||||
const [counts, setCounts] = useState<{ hooks?: number; schedules?: number; paused?: number }>({});
|
||||
useEffect(() => {
|
||||
let live = true;
|
||||
void port.hooks().then((c) => live && setCounts((was) => ({ ...was, hooks: c.hooks.length }))).catch(() => {});
|
||||
void port.schedules().then((b) => live && setCounts((was) => ({ ...was, schedules: b.schedules.length, paused: b.schedules.filter((s) => s.status === "paused").length }))).catch(() => {});
|
||||
return () => { live = false; };
|
||||
}, [port]);
|
||||
return counts;
|
||||
}
|
||||
|
||||
export function AgentSettings({ port, hub, persona, onChanged }: Props) {
|
||||
const deep = useContext(SettingDeepContext);
|
||||
const counts = useAutomationCounts(port);
|
||||
const hooksHint = t("settings.hooks_hint") + (counts.hooks ? ` · ${t("settings.n_hooks", { n: counts.hooks })}` : "");
|
||||
const runsHint = t("settings.scheduled_runs_hint") + (counts.schedules
|
||||
? ` · ${t("settings.n_scheduled", { n: counts.schedules })}${counts.paused ? t("settings.n_paused", { n: counts.paused }) : ""}`
|
||||
: "");
|
||||
return (
|
||||
<>
|
||||
<SectionCard title={t("settings.persona_and_presets")}>
|
||||
@@ -51,6 +72,12 @@ export function AgentSettings({ port, hub, persona, onChanged }: Props) {
|
||||
<SubagentClaims port={port} onChanged={onChanged} />
|
||||
<PeersSection port={port} hub={hub} />
|
||||
</SectionCard>
|
||||
<SectionCard title={t("settings.automation")}>
|
||||
<Group id="hooks" title={t("settings.hooks")} hint={hooksHint} pill={t("settings.manage_storage")}>
|
||||
<Hooks port={port} onChanged={onChanged} />
|
||||
</Group>
|
||||
<ScheduledRuns port={port} hint={runsHint} pill={t("settings.manage_storage")} />
|
||||
</SectionCard>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -28,7 +28,7 @@ const SKIP: Record<string, string> = { missed: "panels.missed_its_time", budget_
|
||||
* shown back as a preview with the most it can spend, and only that preview's
|
||||
* own confirmation creates it. A report is the model's text: it is labelled as
|
||||
* such and drawn as plain text, never as markup. */
|
||||
export function ScheduledRuns({ port }: { port: AgentPort }) {
|
||||
export function ScheduledRuns({ port, hint, pill }: { port: AgentPort; hint?: string; pill?: string }) {
|
||||
const [board, setBoard] = useState<ScheduleBoard | null>(null);
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [error, setError] = useState("");
|
||||
@@ -76,7 +76,7 @@ export function ScheduledRuns({ port }: { port: AgentPort }) {
|
||||
);
|
||||
if (!board) {
|
||||
return unreadable ? (
|
||||
<Group id="scheduled-runs" title={t("panels.scheduled_runs")} hint={t("panels.runs_tasks_read_only_at")}>
|
||||
<Group id="scheduled-runs" title={t("panels.scheduled_runs")} hint={hint ?? t("panels.runs_tasks_read_only_at")} pill={pill}>
|
||||
<div className="box">{unreadable}</div>
|
||||
</Group>
|
||||
) : null;
|
||||
@@ -87,7 +87,7 @@ export function ScheduledRuns({ port }: { port: AgentPort }) {
|
||||
const draft = (patch: Partial<ScheduleDraft>) => setForm((f) => (f ? { ...f, ...patch } : f));
|
||||
|
||||
return (
|
||||
<Group id="scheduled-runs" title={t("panels.scheduled_runs")} hint={t("panels.runs_tasks_read_only_at")}>
|
||||
<Group id="scheduled-runs" title={t("panels.scheduled_runs")} hint={hint ?? t("panels.runs_tasks_read_only_at")} pill={pill}>
|
||||
<div className="box">
|
||||
{unreadable}
|
||||
<div className="lrow">
|
||||
|
||||
@@ -35,8 +35,6 @@ import { InstalledLocation } from "./InstalledLocation";
|
||||
import { ModelSettings } from "./ModelSettings";
|
||||
import { ServerRow } from "./ServerRow";
|
||||
import { SkillGroups } from "./SkillRow";
|
||||
import { Hooks } from "./Hooks";
|
||||
import { ScheduledRuns } from "./ScheduledRuns";
|
||||
import { ConfigTrouble } from "./ConfigTrouble";
|
||||
import { ModHost } from "./ModHost";
|
||||
import { DataSettings } from "./DataSettings";
|
||||
@@ -556,17 +554,6 @@ export function Settings({ hub, onError, port, networkPort, localWorkspacePort,
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{at === "ext" && extTab === "installed" && (
|
||||
<>
|
||||
<Group id="hooks"
|
||||
title={t("settings.automation")}
|
||||
hint={t("settings.run_your_own_commands_before")}
|
||||
>
|
||||
<Hooks port={port} onChanged={afterExtChange} />
|
||||
</Group>
|
||||
<ScheduledRuns port={port} />
|
||||
</>
|
||||
)}
|
||||
{at === "network" && (
|
||||
<ConnectSettings port={port} networkPort={networkPort ?? port} host={networkHost ?? ""} hub={hub} onError={onError} remote={shown("remote")} />
|
||||
)}
|
||||
|
||||
@@ -238,8 +238,8 @@ export const SETTINGS: SettingEntry[] = [
|
||||
// takes that choice as a parameter — hook.Scope, McpInstallScope, the skill
|
||||
// activation scope, a memory's project/global flag. Naming one authority here
|
||||
// would be wrong whenever the reader picks the other.
|
||||
{ section: "hooks", anchor: "scheduled-runs", page: true, title: "panels.scheduled_runs", scope: "machine", apply: "immediate", keywords: ["定时", "计划", "任务", "每天", "schedule", "cron", "unattended", "无人值守"] },
|
||||
{ section: "hooks", anchor: "hooks", page: true, title: "settings.automation", scope: "chosen", apply: "immediate", keywords: ["钩子", "hook", "触发"] },
|
||||
{ section: "session", anchor: "scheduled-runs", page: true, title: "panels.scheduled_runs", scope: "machine", apply: "immediate", keywords: ["定时", "计划", "任务", "每天", "schedule", "cron", "unattended", "无人值守"] },
|
||||
{ section: "session", anchor: "hooks", page: true, title: "settings.hooks", scope: "chosen", apply: "immediate", keywords: ["钩子", "hook", "触发"] },
|
||||
|
||||
{ section: "ext", anchor: "market", title: "ext.community_market", scope: "machine", apply: "immediate", keywords: ["市场", "market", "社区", "发现", "下载", "发布", "publish", "主题"] },
|
||||
{ section: "ext", anchor: "plugins", title: "settings.plugin_packages", scope: "machine", apply: "immediate", keywords: ["安装"] },
|
||||
|
||||
@@ -44,7 +44,7 @@ async function draw() {
|
||||
return { calls, onClose, find: () => screen.getByRole<HTMLInputElement>("textbox", { name: t("settings.search_settings") }) };
|
||||
}
|
||||
|
||||
it.each(["market", "plugins", "mcp", "skills", "hooks", "composition-picker"])(
|
||||
it.each(["market", "plugins", "mcp", "skills", "composition-picker"])(
|
||||
"finds the unmounted %s block by its displayed English title and lands on it",
|
||||
async (anchor) => {
|
||||
language("en");
|
||||
|
||||
Reference in new issue
Block a user