console: React 生产级重写(左列表 + 中对话 + 右详情)

旧单页 console.html 换成 Vite + React + TypeScript 单文件构建产物
(309525 字节,sha 90345ec8…,重建字节一致),源码在
cloud/console-react/,改页面只改 src 再 npm run build:console,
产物不手改。布局:左侧列表(模块导航 + 任务列表)、中间标准对话
(实时链路时间线 + 继续会话聊天框,共用 Chat 组件)、右侧详情
(元数据、操作、评审、审批、协调子任务、子代理、线程)。
API 与后端语义未改,key 只存浏览器 localStorage。
6 个 console 标记单测随实现改名更新(意图不变:壳确实调了
这些接口、turn 信息仍在详情里、旧 goal 输入仍不存在);
P1 标记单测未动且通过。
This commit is contained in:
host committed 2026-10-29 06:32:38 +08:00
1 parent 3f4e849a93
commit ebc5624893
57 files changed
+4789 -1746

No files matched your search

@@ -0,0 +1,143 @@
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.refreshTasks(id);
void catalog.refreshEnvs(id);
void catalog.refreshMembers(id);
void catalog.refreshSkills(id);
void tools.refreshAutomations(id);
void tools.refreshTriage(id);
}, [catalog, taskList, tools]);
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,
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,
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,
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, 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.refreshTasks(projectIdRef.current);
return msg;
},
dispatchMsg: "",
refreshAll, refreshSlow, refreshStamp,
};
}