Files
codex-app-server/cloud/console-react/src/hooks/useConsoleStore.ts
T
host 2d644f901b feat(console): automation panel edit and richer list
- AutomationPanel rows now show title, prompt content (160-char preview with expand/collapse), repo_url, last/next run, run_once badge and cancelled time.

- Active automations get an inline edit form (name/prompt/interval/repo) that PATCHes only changed fields via the existing automation_update endpoint; backend semantics unchanged (no last_run_at reset, cancelled stays terminal).

- Rebuilt cloud/console.html with tsc + vite (build green); automation python tests 18/18 OK.
2026-10-30 15:31:21 +08:00

159 lines
8.7 KiB
TypeScript

import { useCallback, useEffect, useRef, useState } from "react";
import { getKey } from "../api/client";
import { useCatalog } from "./useCatalog";
import { useTaskList } from "./useTaskList";
import { useTaskDetail } from "./useTaskDetail";
import { useProjectTools } from "./useProjectTools";
import { formatRefreshTime } from "./useAutoRefresh";
import type { ConsoleCtx } from "../context";
// Glue only: composes the domain hooks into one ConsoleCtx and owns
// the auto-refresh policy. No API calls and no rendering live here.
export function useConsoleStore(): ConsoleCtx {
const catalog = useCatalog();
const taskList = useTaskList();
const projectIdRef = useRef("");
projectIdRef.current = catalog.projectId;
const getProjectId = useCallback(() => projectIdRef.current, []);
const tools = useProjectTools(getProjectId);
const getRole = useCallback((pid?: string) => {
const p = catalog.projects.find((x) => x.id === pid);
return p ? (p.my_role || null) : null;
}, [catalog.projects]);
const onTasksReload = useCallback(() => { void taskList.refreshTasks(undefined, projectIdRef.current); }, [taskList]);
const detail = useTaskDetail(getRole, onTasksReload);
const [refreshStamp, setRefreshStamp] = useState("--:--:--");
const selectProject = useCallback((id: string) => {
catalog.setProjectId(id);
projectIdRef.current = id;
void taskList.refreshTasksFirstPage(id);
void catalog.refreshEnvs(id);
void catalog.refreshMembers(id);
void catalog.refreshSkills(id);
void tools.refreshAutomations(id);
void tools.refreshTriage(id);
}, [catalog, taskList, tools]);
// Fix: the old single page's project <select> defaulted to its first
// option once loaded, so tasks loaded without a click. The React
// version is a controlled select with projectId "" on every reload,
// so without this the task list stayed on "先选项目" forever and
// auto-refresh never fixed it. Auto-select the first project only
// while nothing is selected; an explicit user choice is never
// overridden.
const firstProjectId = catalog.projects[0]?.id;
useEffect(() => {
if (!catalog.projectId && firstProjectId) selectProject(firstProjectId);
}, [catalog.projectId, firstProjectId, selectProject]);
const saveKeyInput = useCallback((v: string) => {
catalog.saveKeyInput(v);
if (!v.trim()) return;
// Same reload set as the live saveKey handler.
void catalog.refreshHealth();
void catalog.refreshProjects();
void catalog.refreshWorkers();
if (projectIdRef.current) {
void taskList.refreshTasks(projectIdRef.current);
void catalog.refreshEnvs(projectIdRef.current);
void catalog.refreshMembers(projectIdRef.current);
}
void catalog.loadRelayModels();
void catalog.loadQuota();
void catalog.loadGithubRepos();
void catalog.probeAdmin();
}, [catalog, taskList]);
const refreshAll = useCallback((selectFocused: boolean) => {
setRefreshStamp(formatRefreshTime(new Date()));
if (!getKey()) { void catalog.refreshHealth(); return; }
void taskList.refreshTasks(undefined, projectIdRef.current);
void catalog.refreshHealth();
void catalog.loadQuota();
void catalog.refreshWorkers();
void catalog.refreshMembers();
void catalog.refreshSkills();
void tools.refreshAutomations();
void tools.refreshTriage();
void detail.refreshDetailLists();
if (!selectFocused) {
void catalog.refreshProjects();
void catalog.refreshEnvs();
}
}, [catalog, taskList, tools, detail]);
const refreshSlow = useCallback(() => {
if (!getKey()) return;
void catalog.loadRelayModels();
void catalog.loadGithubRepos();
}, [catalog]);
// Initial load, mirroring the live file's boot sequence.
useEffect(() => {
void catalog.refreshHealth();
void catalog.refreshProjects();
void catalog.refreshWorkers();
void catalog.loadQuota();
void catalog.loadGithubRepos();
if (getKey()) void catalog.loadRelayModels();
void catalog.probeAdmin();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return {
keySaved: catalog.keySaved, keyState: catalog.keyState,
saveKeyInput, clearKeyInput: catalog.clearKeyInput,
quotaText: catalog.quotaText,
healthText: catalog.healthText, refreshHealth: catalog.refreshHealth,
adminGate: catalog.adminGate, adminBody: catalog.adminBody, adminResult: catalog.adminResult,
adminUserId: catalog.adminUserId, setAdminUserId: catalog.setAdminUserId,
adminQuota: catalog.adminQuota, adminSweep: catalog.adminSweep, adminBackup: catalog.adminBackup,
projects: catalog.projects, projectId: catalog.projectId, projectMsg: catalog.projectMsg,
selectProject, refreshProjects: catalog.refreshProjects,
createProject: catalog.createProject,
githubRepos: catalog.githubRepos,
workers: catalog.workers, workersSummary: catalog.workersSummary, workersError: catalog.workersError, refreshWorkers: catalog.refreshWorkers,
members: catalog.members, memberMsg: catalog.memberMsg, memberError: catalog.memberError,
refreshMembers: () => void catalog.refreshMembers(), inviteMember: catalog.inviteMember, changeMemberRole: catalog.changeMemberRole,
skills: catalog.skills, apps: catalog.apps, mcpServers: catalog.mcpServers, skillMsg: catalog.skillMsg, skillError: catalog.skillError,
mcpMsg: catalog.mcpMsg, mcpOauthMsg: catalog.mcpOauthMsg, mcpOauthBox: catalog.mcpOauthBox,
refreshSkills: () => void catalog.refreshSkills(), reloadMcp: catalog.reloadMcp, mcpOauth: catalog.mcpOauth,
envs: catalog.envs, envCount: catalog.envCount, envMsg: catalog.envMsg, envRepoLine: catalog.envRepoLine, envRepoBar: catalog.envRepoBar, envRepoMsg: catalog.envRepoMsg, envSelect: catalog.envSelect, setEnvSelect: catalog.setEnvSelect,
refreshEnvs: () => void catalog.refreshEnvs(), createEnv: catalog.createEnv, archiveEnv: catalog.archiveEnv, saveEnvRepo: catalog.saveEnvRepo,
models: catalog.models, modelCustomVisible: catalog.modelCustomVisible,
tasks: taskList.tasks, taskCountText: taskList.taskCountText, taskError: taskList.taskError,
refreshTasks: () => void taskList.refreshTasks(undefined, projectIdRef.current),
statusFilter: taskList.statusFilter, setStatusFilter: taskList.setStatusFilter,
executorFilter: taskList.executorFilter, setExecutorFilter: taskList.setExecutorFilter,
searchText: taskList.searchText, setSearchText: taskList.setSearchText,
filteredTasks: taskList.filteredTasks,
taskPage: taskList.page, taskTotalPages: taskList.totalPages, taskTotal: taskList.taskTotal,
taskStatusCounts: taskList.taskStatusCounts, goToTaskPage: taskList.goToPage,
currentTask: detail.currentTask, detailBody: detail.detailBody, actionMsg: detail.actionMsg, actionReasons: detail.actionReasons,
showDetail: (id: string) => void detail.showDetail(id),
doAction: detail.doAction, taskCost: detail.taskCost, costBox: detail.costBox, costSummary: detail.costSummary,
forkVisible: detail.forkVisible, setForkVisible: detail.setForkVisible, forkTask: detail.forkTask, forkMsg: detail.forkMsg,
coordChildren: detail.coordChildren, coordText: "", agents: detail.agents, agentsNote: detail.agentsNote,
reviews: detail.reviews, reviewMsg: detail.reviewMsg, reviewError: detail.reviewError,
refreshReviews: detail.refreshReviews, postReview: detail.postReview, submitFindings: detail.submitFindings,
traceEntries: detail.traceEntries, traceNote: detail.traceNote, traceDone: detail.traceDone,
traceKind: detail.traceKind, setTraceKind: detail.setTraceKind, downloadTrace: detail.downloadTrace, traceMsg: detail.traceMsg,
sendChatMessage: detail.sendChatMessage, chatMsg: detail.chatMsg,
chatHistory: detail.chatHistory,
followups: detail.followups,
approvals: detail.approvals, approvalMsg: detail.approvalMsg, refreshApprovals: detail.refreshApprovals, decideApproval: detail.decideApproval,
peerMessages: detail.peerMessages, peerMsg: detail.peerMsg, postPeer: detail.postPeer,
automations: tools.automations, autoMsg: tools.autoMsg, refreshAutomations: () => void tools.refreshAutomations(), createAutomation: tools.createAutomation, updateAutomation: tools.updateAutomation, cancelAutomation: tools.cancelAutomation,
triage: tools.triage, triageError: tools.triageError, refreshTriage: () => void tools.refreshTriage(),
threadText: detail.threadText, threadGateNote: detail.threadGateNote, threadMsg: detail.threadMsg, threadList: detail.threadList, threadAction: detail.threadAction,
dispatch: async (body, atts) => {
const msg = await taskList.dispatch(body, atts);
void taskList.refreshTasksFirstPage(projectIdRef.current);
return msg;
},
dispatchMsg: "",
refreshAll, refreshSlow, refreshStamp,
};
}