diff --git a/cloud/console-react/src/App.tsx b/cloud/console-react/src/App.tsx
index 8d26c16..5f1ca79 100644
--- a/cloud/console-react/src/App.tsx
+++ b/cloud/console-react/src/App.tsx
@@ -9,8 +9,7 @@ import ConversationSection from "./sections/ConversationSection";
import TaskDetailPanel from "./sections/TaskDetailPanel";
import ApiKeySection from "./sections/ApiKeySection";
import AdminSection from "./sections/AdminSection";
-import ProjectsSection from "./sections/ProjectsSection";
-import MembersPanel from "./sections/MembersPanel";
+import ProjectNav from "./sections/ProjectNav";
import EnvSection from "./sections/EnvSection";
import EnvContextPanel from "./sections/EnvContextPanel";
import WorkersSection from "./sections/WorkersSection";
@@ -25,7 +24,10 @@ import TodosSection from "./sections/TodosSection";
// data-cards row on top of a long stacked page, no detail stack
// hanging under the conversation. The new shell is an IDE-style
// workspace that never scrolls as one page:
-// 左侧栏:导航(按工作流分组)+ 数据卡片(紧凑版,始终可见)
+// 左侧栏:导航(按工作流分组)+ 项目模块(ProjectNav:项目列表 +
+// 创建项目 + 成员,id nav-projects,是全站唯一的项目选择器;
+// 因此不再有独立的“项目与成员”视图,旧 ProjectsSection 已删,
+// 工作台也不再带项目选择器)+ 数据卡片(紧凑版,始终可见)
// 任务工作台:左 = 任务列表轨(搜索/筛选/分页),中 = 派发 + 对话,
// 右 = 任务详情检查器(概览/协作/评审与审批/线程四个内页签,
// 见 TaskDetailPanel)
@@ -36,11 +38,10 @@ import TodosSection from "./sections/TodosSection";
// unmounted with a view. The view is synced to location.hash
// (#view-tasks etc.) so a reload or a shared link lands on the
// same view; unknown hashes fall back to the task workspace.
-type ViewId = "tasks" | "todos" | "projects" | "resources" | "automation" | "admin";
+type ViewId = "tasks" | "todos" | "resources" | "automation" | "admin";
const VIEWS: Array<[ViewId, string, string]> = [
["tasks", "任务工作台", "任务"],
["todos", "项目待办", "任务"],
- ["projects", "项目与成员", "管理"],
["resources", "环境与 Workers", "管理"],
["automation", "技能与自动化", "管理"],
["admin", "系统与 Key", "管理"],
@@ -67,6 +68,9 @@ function SideNav({ view, setView }: { view: ViewId; setView: (v: ViewId) => void
))}
+
概览
@@ -138,7 +142,6 @@ export default function App() {
) : (
{view === "todos" ? (<> >)
- : view === "projects" ? (<> >)
: view === "resources" ? (<> >)
: view === "automation" ? (<> >)
: (<> >)}
diff --git a/cloud/console-react/src/api/client.ts b/cloud/console-react/src/api/client.ts
index 47a9d49..a4a73f0 100644
--- a/cloud/console-react/src/api/client.ts
+++ b/cloud/console-react/src/api/client.ts
@@ -93,6 +93,7 @@ export function memberErr(e: Error): string {
if (m.includes("forbidden")) return "没有权限:只有项目 owner 或全局管理员可以管理成员";
if (m.includes("already_member")) return "该用户已是成员,改角色请用成员行的下拉框";
if (m.includes("cannot demote the only owner")) return "不能把唯一 owner 降为非 owner:请先把另一位成员升为 owner";
+ if (m.includes("cannot remove the only owner")) return "不能移除唯一 owner:请先把另一位成员升为 owner";
if (m.includes("user not found")) return "找不到该用户:请核对 user_id 或用户名是否正确";
if (m.includes("member not found")) return "该用户不是本项目成员";
if (m.includes("unknown role")) return "角色无效:只能是 viewer / contributor / maintainer / owner";
diff --git a/cloud/console-react/src/api/types.ts b/cloud/console-react/src/api/types.ts
index 44228fc..046048f 100644
--- a/cloud/console-react/src/api/types.ts
+++ b/cloud/console-react/src/api/types.ts
@@ -36,7 +36,9 @@ export interface Followup { mode?: string; status?: string; sender_task_id?: str
export interface ChatHistoryMessage { id: string; role: "user" | "assistant" | "tool" | "status"; source?: string; kind?: string; label?: string; text: string; ts_ms?: number | null }
export interface ChatHistory { task_id?: string; status?: string; messages: ChatHistoryMessage[]; continue_messages_stored?: number; legacy_continue_count?: number | null; trace_note?: string }
export interface PeerMessage { task_id?: string; mode?: string; status?: string; body?: string }
-export interface Automation { id: string; name?: string; prompt?: string; repo_url?: string; status?: string; due?: boolean; interval_seconds?: number; next_run_at?: number; last_run_at?: number; created_at?: number; cancelled_at?: number; run_once?: boolean }
+export interface Automation { id: string; name?: string; prompt?: string; repo_url?: string; status?: string; due?: boolean; interval_seconds?: number; next_run_at?: number; last_run_at?: number; last_claimed_at?: number; created_by?: string; created_at?: number; cancelled_at?: number; first_run_at?: number; run_once?: boolean; project_id?: string }
+export interface AutomationRun { id?: string; automation_id?: string; project_id?: string; task_id?: string; status?: string; detail?: string; created_at?: number }
+export interface AutomationDetail extends Automation { runs?: AutomationRun[]; runs_note?: string; heartbeat_binding?: unknown }
export interface Member { user_id: string; username?: string; role?: string }
export interface SkillItem { name?: string; id?: string; description?: string; detail?: string }
export interface McpServer { name?: string; id?: string; status?: string; detail?: string; details?: string; state?: string }
@@ -45,7 +47,7 @@ export interface Agent { kind?: string; name?: string; status?: string; prompt?:
export interface ThreadInfo { id?: string; name?: string; thread_name?: string; archived?: boolean | null; cwd?: string; turns?: number; turn_count?: number; turns_count?: number }
export interface Triage {
counts?: { automations_due?: number; failed_tasks?: number; pending_followups?: number; recent_runs?: number };
- automations_due?: Array<{ name?: string; interval_seconds?: number }>;
+ automations_due?: Array<{ id?: string; name?: string; status?: string; due?: boolean; interval_seconds?: number; next_run_at?: number }>;
failed_tasks?: Array<{ id?: string; status?: string; prompt?: string }>;
pending_followups?: Array<{ task_id?: string; body?: string }>;
recent_runs?: Array<{ automation_id?: string; status?: string; task_id?: string; task_status?: string }>;
diff --git a/cloud/console-react/src/context.ts b/cloud/console-react/src/context.ts
index 5118256..295d21a 100644
--- a/cloud/console-react/src/context.ts
+++ b/cloud/console-react/src/context.ts
@@ -1,5 +1,5 @@
import { createContext, useContext } from "react";
-import type { Automation, Env, Member, Project, Review, Approval, Followup, PeerMessage, Task, TaskCost, ThreadInfo, Triage, Worker, SkillItem, McpServer, Agent, DispatchBody, ReviewBody, TraceEntry, ChatHistory } from "./api/types";
+import type { Automation, AutomationDetail, Env, Member, Project, Review, Approval, Followup, PeerMessage, Task, TaskCost, ThreadInfo, Triage, Worker, SkillItem, McpServer, Agent, DispatchBody, ReviewBody, TraceEntry, ChatHistory } from "./api/types";
export interface ConsoleCtx {
keySaved: boolean; keyState: string;
@@ -7,13 +7,13 @@ export interface ConsoleCtx {
quotaText: string;
healthText: string; refreshHealth: () => void;
adminGate: string; adminBody: boolean; adminResult: string; adminUserId: string; setAdminUserId: (v: string) => void;
- adminQuota: () => void; adminSweep: () => void; adminBackup: () => void;
+ adminQuota: () => void; adminSetQuota: (uid: string, usdText: string) => void; adminSweep: () => void; adminBackup: () => void;
projects: Project[]; projectId: string; projectMsg: string;
selectProject: (id: string) => void; refreshProjects: () => void;
createProject: (name: string, repo: string) => Promise;
githubRepos: Array<{ clone_url: string; repo_full_name: string }>;
workers: Worker[] | null; workersSummary: string; workersError: string; refreshWorkers: () => void;
- members: Member[]; memberMsg: string; memberError: string; refreshMembers: () => void; inviteMember: (raw: string, role: string) => Promise; changeMemberRole: (uid: string, role: string) => Promise;
+ members: Member[]; memberMsg: string; memberError: string; refreshMembers: () => void; inviteMember: (raw: string, role: string) => Promise; changeMemberRole: (uid: string, role: string) => Promise; removeMember: (uid: string) => void;
skills: SkillItem[]; apps: SkillItem[]; mcpServers: McpServer[]; skillMsg: string; skillError: string; mcpMsg: string; mcpOauthMsg: string; mcpOauthBox: string;
refreshSkills: () => void; reloadMcp: () => void; mcpOauth: (name: string) => Promise;
envs: Env[]; envCount: string; envMsg: string; envRepoLine: string; envRepoBar: boolean; envRepoMsg: string; envSelect: string; setEnvSelect: (v: string) => void;
@@ -35,7 +35,7 @@ export interface ConsoleCtx {
followups: Followup[];
approvals: Approval[]; approvalMsg: string; refreshApprovals: () => void; decideApproval: (id: string, decision: string) => void;
peerMessages: PeerMessage[]; peerMsg: string; postPeer: (mode: string, target: string, text: string) => Promise;
- automations: Automation[]; autoMsg: string; refreshAutomations: () => void; createAutomation: (name: string, prompt: string, interval: string, repo: string) => Promise; updateAutomation: (a: Automation, name: string, prompt: string, interval: string, repo: string) => Promise; cancelAutomation: (id: string) => void;
+ automations: Automation[]; autoMsg: string; autoDetail: AutomationDetail | null; autoDetailMsg: string; refreshAutomations: () => void; createAutomation: (name: string, prompt: string, interval: string, repo: string) => Promise; updateAutomation: (a: Automation, name: string, prompt: string, interval: string, repo: string) => Promise; cancelAutomation: (id: string) => void; loadAutomationDetail: (id: string) => void; closeAutomationDetail: () => void;
triage: Triage | null; triageError: string; refreshTriage: () => void;
threadText: string; threadGateNote: string; threadMsg: string; threadList: ThreadInfo[]; threadAction: (kind: string, name: string) => void;
dispatch: (body: DispatchBody, atts: Array<{ path: string; file: File }>) => Promise; dispatchMsg: string;
diff --git a/cloud/console-react/src/hooks/useCatalog.ts b/cloud/console-react/src/hooks/useCatalog.ts
index 6e99ea9..84a3054 100644
--- a/cloud/console-react/src/hooks/useCatalog.ts
+++ b/cloud/console-react/src/hooks/useCatalog.ts
@@ -253,6 +253,17 @@ export function useCatalog() {
} catch (e: any) { setMemberMsg(memberErr(e)); }
}, []);
+ const removeMember = useCallback(async (uid: string) => {
+ const pid = projectIdRef.current;
+ if (!pid) return;
+ if (!window.confirm("确定移除该成员吗?移除后其项目权限立即失效(唯一 owner 不可移除,服务端会拒绝)。")) return;
+ try {
+ await api(paths.projectMember(pid, uid), "DELETE");
+ setMemberMsg("成员已移除");
+ await refreshMembers(pid);
+ } catch (e: any) { setMemberMsg(memberErr(e)); }
+ }, [refreshMembers]);
+
const reloadMcp = useCallback(async () => {
const pid = projectIdRef.current;
if (!pid) { setMcpMsg("先选项目"); return; }
@@ -289,6 +300,24 @@ export function useCatalog() {
} catch (e: any) { setAdminResult(adminErr(e)); }
}, [adminUserId]);
+ const adminSetQuota = useCallback(async (uid: string, usdText: string) => {
+ const target = uid.trim();
+ if (!target) { setAdminResult("请填要设置配额的用户 id"); return; }
+ // POST /admin/quotas {user_id, quota_usd_micros}: integer micros or
+ // null for unlimited, >= 0. The console takes dollars and converts.
+ let micros: number | null;
+ if (!usdText.trim()) micros = null;
+ else {
+ const usd = Number(usdText.trim());
+ if (!Number.isFinite(usd) || usd < 0) { setAdminResult("配额金额无效:请输入 >= 0 的美元数,留空表示不限额"); return; }
+ micros = Math.round(usd * 1000000);
+ }
+ try {
+ const d = await api<{ used_usd_micros?: number; quota_usd_micros?: number | null; remaining_usd_micros?: number | null; month?: string }>(paths.adminQuotas, "POST", { user_id: target, quota_usd_micros: micros });
+ setAdminResult("配额已设置:用户 " + target.slice(0, 8) + ":本月用量 " + fmtUsd(d.used_usd_micros) + " / 配额 " + fmtUsd(d.quota_usd_micros) + (d.remaining_usd_micros !== null && d.remaining_usd_micros !== undefined ? "(剩余 " + fmtUsd(d.remaining_usd_micros) + ")" : "") + " | 月份 " + (d.month || ""));
+ } catch (e: any) { setAdminResult(adminErr(e)); }
+ }, []);
+
const adminSweep = useCallback(async () => {
if (!window.confirm("确定手动触发 sweep 吗?超阈值的陈旧 running 任务会被标为 interrupted。")) return;
try {
@@ -314,6 +343,6 @@ export function useCatalog() {
models, modelCustomVisible,
refreshHealth, loadQuota, probeAdmin, refreshProjects, loadGithubRepos, loadRelayModels, refreshWorkers,
refreshEnvs, refreshMembers, refreshSkills, saveKeyInput, clearKeyInput, createProject, createEnv,
- archiveEnv, saveEnvRepo, inviteMember, changeMemberRole, reloadMcp, mcpOauth, adminQuota, adminSweep, adminBackup,
+ archiveEnv, saveEnvRepo, inviteMember, changeMemberRole, removeMember, reloadMcp, mcpOauth, adminQuota, adminSetQuota, adminSweep, adminBackup,
};
}
diff --git a/cloud/console-react/src/hooks/useConsoleStore.ts b/cloud/console-react/src/hooks/useConsoleStore.ts
index 3a1efef..4200972 100644
--- a/cloud/console-react/src/hooks/useConsoleStore.ts
+++ b/cloud/console-react/src/hooks/useConsoleStore.ts
@@ -108,14 +108,14 @@ export function useConsoleStore(): ConsoleCtx {
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,
+ adminQuota: catalog.adminQuota, adminSetQuota: catalog.adminSetQuota, 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,
+ refreshMembers: () => void catalog.refreshMembers(), inviteMember: catalog.inviteMember, changeMemberRole: catalog.changeMemberRole, removeMember: catalog.removeMember,
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,
@@ -144,7 +144,7 @@ export function useConsoleStore(): ConsoleCtx {
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,
+ automations: tools.automations, autoMsg: tools.autoMsg, autoDetail: tools.autoDetail, autoDetailMsg: tools.autoDetailMsg, refreshAutomations: () => void tools.refreshAutomations(), createAutomation: tools.createAutomation, updateAutomation: tools.updateAutomation, cancelAutomation: tools.cancelAutomation, loadAutomationDetail: tools.loadAutomationDetail, closeAutomationDetail: tools.closeAutomationDetail,
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) => {
diff --git a/cloud/console-react/src/hooks/useProjectTools.ts b/cloud/console-react/src/hooks/useProjectTools.ts
index 6d2a3e5..06fa09f 100644
--- a/cloud/console-react/src/hooks/useProjectTools.ts
+++ b/cloud/console-react/src/hooks/useProjectTools.ts
@@ -1,13 +1,15 @@
import { useCallback, useState } from "react";
import { api } from "../api/client";
import { paths } from "../api/paths";
-import type { Automation, Triage } from "../api/types";
+import type { Automation, AutomationDetail, Triage } from "../api/types";
// Project-scoped tools shown in the centre for their own views:
// automations (create/list/cancel) and the triage inbox (4 groups).
export function useProjectTools(getProjectId: () => string) {
const [automations, setAutomations] = useState([]);
const [autoMsg, setAutoMsg] = useState("");
+ const [autoDetail, setAutoDetail] = useState(null);
+ const [autoDetailMsg, setAutoDetailMsg] = useState("");
const [triage, setTriage] = useState(null);
const [triageError, setTriageError] = useState("");
@@ -66,10 +68,26 @@ export function useProjectTools(getProjectId: () => string) {
const pid = getProjectId();
if (!pid) return;
void api(paths.projectAutomationCancel(pid, id), "POST", {})
- .then(() => { setAutoMsg("自动化已取消"); void refreshAutomations(pid); })
+ .then(() => { setAutoMsg("自动化已取消"); setAutoDetail((prev) => (prev && prev.id === id ? null : prev)); void refreshAutomations(pid); })
.catch((e: any) => setAutoMsg("取消失败:" + e.message));
}, [getProjectId, refreshAutomations]);
+ // GET /projects/{pid}/automations/{aid}: full definition + recent
+ // runs (+ runs_note, heartbeat_binding). Verified against origin/main
+ // cloud/server.py _automation_get / cloud/automations.py get_automation.
+ const loadAutomationDetail = useCallback(async (id: string) => {
+ const pid = getProjectId();
+ if (!pid) { setAutoDetailMsg("先选项目"); return; }
+ try {
+ const d = await api(paths.projectAutomation(pid, id));
+ setAutoDetail(d);
+ setAutoDetailMsg("");
+ } catch (e: any) { setAutoDetailMsg("详情加载失败:" + e.message); }
+ }, [getProjectId]);
+
+ const closeAutomationDetail = useCallback(() => { setAutoDetail(null); setAutoDetailMsg(""); }, []);
+
+
const refreshTriage = useCallback(async (pid?: string) => {
const id = pid !== undefined ? pid : getProjectId();
if (!id) { setTriage(null); return; }
@@ -80,5 +98,5 @@ export function useProjectTools(getProjectId: () => string) {
} catch (e: any) { setTriageError("分诊加载失败:" + e.message); }
}, [getProjectId]);
- return { automations, autoMsg, triage, triageError, refreshAutomations, createAutomation, updateAutomation, cancelAutomation, refreshTriage };
+ return { automations, autoMsg, autoDetail, autoDetailMsg, triage, triageError, refreshAutomations, createAutomation, updateAutomation, cancelAutomation, loadAutomationDetail, closeAutomationDetail, refreshTriage };
}
diff --git a/cloud/console-react/src/sections/AdminSection.tsx b/cloud/console-react/src/sections/AdminSection.tsx
index 50c2669..8ffc563 100644
--- a/cloud/console-react/src/sections/AdminSection.tsx
+++ b/cloud/console-react/src/sections/AdminSection.tsx
@@ -1,8 +1,11 @@
+import { useState } from "react";
import { useConsole } from "../context";
import { Card, Bar } from "../components/ui";
export default function AdminSection() {
const ctx = useConsole();
+ const [setUser, setSetUser] = useState("");
+ const [setUsd, setSetUsd] = useState("");
return (
管理区 (仅全局管理员) >}>
{ctx.adminGate}
@@ -12,6 +15,11 @@ export default function AdminSection() {
ctx.setAdminUserId(e.target.value)} />
查该用户配额
+
+ setSetUser(e.target.value)} />
+ setSetUsd(e.target.value)} />
+ ctx.adminSetQuota(setUser, setUsd)}>设置配额
+
手动触发 sweep(清理陈旧 running 任务)
手动触发备份
diff --git a/cloud/console-react/src/sections/MembersPanel.tsx b/cloud/console-react/src/sections/MembersPanel.tsx
index ef3f1df..6e5182d 100644
--- a/cloud/console-react/src/sections/MembersPanel.tsx
+++ b/cloud/console-react/src/sections/MembersPanel.tsx
@@ -37,6 +37,7 @@ export default function MembersPanel() {
void ctx.changeMemberRole(m.user_id, e.target.value)}>
{ROLES.map((r) => {r} )}
+ void ctx.removeMember(m.user_id)}>移除
);
})}
diff --git a/cloud/console-react/src/sections/ProjectNav.tsx b/cloud/console-react/src/sections/ProjectNav.tsx
new file mode 100644
index 0000000..180be1b
--- /dev/null
+++ b/cloud/console-react/src/sections/ProjectNav.tsx
@@ -0,0 +1,51 @@
+import { useState } from "react";
+import { useConsole } from "../context";
+import { Card, Bar } from "../components/ui";
+import { shortId } from "../api/client";
+import MembersPanel from "./MembersPanel";
+
+// Left sidebar: the one project selector for the whole console.
+// Lists every project (name + short id + my role), click selects it;
+// below the list is the create-project form (same fields as the old
+// workbench ProjectsSection: name, GitHub repo select, repo_url) and
+// the selected project's members. The workbench no longer carries any
+// project-module UI; it only consumes ctx.projectId.
+export default function ProjectNav() {
+ const ctx = useConsole();
+ const [name, setName] = useState("");
+ const [repoSelect, setRepoSelect] = useState("");
+ const [repo, setRepo] = useState("");
+ return (
+
+ );
+}
diff --git a/cloud/console-react/src/sections/ProjectsSection.tsx b/cloud/console-react/src/sections/ProjectsSection.tsx
deleted file mode 100644
index 4449787..0000000
--- a/cloud/console-react/src/sections/ProjectsSection.tsx
+++ /dev/null
@@ -1,32 +0,0 @@
-import { useState } from "react";
-import { useConsole } from "../context";
-import { Card, Bar } from "../components/ui";
-import { shortId } from "../api/client";
-
-export default function ProjectsSection() {
- const ctx = useConsole();
- const [name, setName] = useState("");
- const [repoSelect, setRepoSelect] = useState("");
- const [repo, setRepo] = useState("");
- return (
-
-
- ctx.selectProject(e.target.value)}>
- {ctx.projects.length === 0 ? (先加载项目) : null}
- {ctx.projects.map((p) => {p.name} ({shortId(p.id)}) )}
-
- 刷新
-
-
- setName(e.target.value)} />
- { setRepoSelect(e.target.value); if (e.target.value) setRepo(e.target.value); }}>
- (从 GitHub App 安装仓库里选,或手动输入)
- {ctx.githubRepos.map((r) => {r.repo_full_name} )}
-
- setRepo(e.target.value)} />
- { await ctx.createProject(name, repo); setName(""); setRepo(""); setRepoSelect(""); }}>新建项目
-
- {ctx.projectMsg}
-
- );
-}
diff --git a/cloud/console-react/src/sections/TaskListSection.tsx b/cloud/console-react/src/sections/TaskListSection.tsx
index d6aca91..3cb5a51 100644
--- a/cloud/console-react/src/sections/TaskListSection.tsx
+++ b/cloud/console-react/src/sections/TaskListSection.tsx
@@ -5,9 +5,12 @@ import { firstChar, durationOf, tokenOf } from "../api/taskMeta";
// 任务列表行 meta:首字 / 总耗时 / token,计算见 api/taskMeta.ts,
// 与任务详情共用同一口径。
-// GitHub issues-style task list: search on top, the project selector
-// (the auto-project fix — do not remove it), a 主要聊天 header with a
-// + button, then hairline rows with a Primer status dot at the left,
+// GitHub issues-style task list: search on top, a 主要聊天 header with
+// a + button, then hairline rows with a Primer status dot at the
+// left. There is deliberately NO project selector here any more:
+// the left sidebar (ProjectNav) is the one project selector, and the
+// auto-select-first-project fix in the store keeps the list from
+// ever starting empty.
// a 14px title and a 12px muted meta line. The search input IS the
// existing task search (id/prompt); status and executor filters stay
// as a compact row below it. The selected row gets the light
@@ -22,12 +25,6 @@ export default function TaskListSection({ onNew }: { onNew: () => void }) {
ctx.setSearchText(e.target.value)} />
-
- ctx.selectProject(e.target.value)}>
- {ctx.projects.length === 0 ? (先加载项目) : null}
- {ctx.projects.map((p) => {p.name} ({shortId(p.id)}) )}
-
-
主要聊天
+
diff --git a/cloud/console-react/src/sections/WorkersSection.tsx b/cloud/console-react/src/sections/WorkersSection.tsx
index cd07cac..3d78a15 100644
--- a/cloud/console-react/src/sections/WorkersSection.tsx
+++ b/cloud/console-react/src/sections/WorkersSection.tsx
@@ -1,22 +1,52 @@
+import { useState } from "react";
import { useConsole } from "../context";
import { Card, Bar, DataListRaw } from "../components/ui";
import { workerAgeText, shortId } from "../api/client";
+// Workers are read-only by design (GET /workers + worker-side
+// heartbeat only — no start/stop/kill actions exist to surface), so
+// production-grade here is list + status + refresh + frontend-only
+// filtering of the already-fetched list. Non-admin users only get
+// the online/offline aggregate from the backend (see _workers_list),
+// so search/filter apply to the full list only and say so.
export default function WorkersSection() {
const ctx = useConsole();
+ const [query, setQuery] = useState("");
+ const [onlineFilter, setOnlineFilter] = useState<"all" | "online" | "offline">("all");
+ const isAlive = (w: { alive?: boolean | null; last_seen?: number; last_seen_seconds_ago?: number }) => {
+ const info = workerAgeText(w);
+ return w.alive !== undefined && w.alive !== null ? !!w.alive : (info.age !== null && info.age <= 120);
+ };
+ const q = query.trim().toLowerCase();
+ const filtered = (ctx.workers || []).filter((w) => {
+ if (q && !(((w.worker_id || w.id || "") + " " + (w.executor || "")).toLowerCase().includes(q))) return false;
+ if (onlineFilter === "online" && !isAlive(w)) return false;
+ if (onlineFilter === "offline" && isAlive(w)) return false;
+ return true;
+ });
return (
刷新
+
+ setQuery(e.target.value)} />
+ setOnlineFilter(e.target.value as "all" | "online" | "offline")}>
+ 在线状态:全部
+ 仅在线
+ 仅离线
+
+ {ctx.workers ? 显示 {filtered.length} / 共 {ctx.workers.length} 个 : null}
+
{ctx.workersError ? {ctx.workersError}
- : ctx.workers === null ? {ctx.workersSummary || "未加载"}
+ : ctx.workers === null ? {ctx.workersSummary || "未加载"}(完整清单仅全局管理员可见,搜索/筛选仅对完整清单生效)
: ctx.workers.length === 0 ? 无 worker 注册
+ : filtered.length === 0 ? 没有符合筛选条件的 worker
: (
- {ctx.workers.map((w, i) => {
+ {filtered.map((w, i) => {
const run = w.running === undefined || w.running === null ? "?" : w.running;
const cap = w.capacity === undefined || w.capacity === null ? "?" : w.capacity;
const info = workerAgeText(w);
- const alive = w.alive !== undefined && w.alive !== null ? !!w.alive : (info.age !== null && info.age <= 120);
+ const alive = isAlive(w);
const live = alive ? " / 在线" : " / 离线(占用为上次上报值)";
return (
diff --git a/cloud/console-react/src/sections/detail/ProjectToolsPanels.tsx b/cloud/console-react/src/sections/detail/ProjectToolsPanels.tsx
index 04eb972..f95c565 100644
--- a/cloud/console-react/src/sections/detail/ProjectToolsPanels.tsx
+++ b/cloud/console-react/src/sections/detail/ProjectToolsPanels.tsx
@@ -2,7 +2,7 @@ import { useState } from "react";
import { useConsole } from "../../context";
import { Bar, DataList } from "../../components/ui";
import { fmt, shortId } from "../../api/client";
-import type { Automation } from "../../api/types";
+import type { Automation, AutomationDetail } from "../../api/types";
const PROMPT_PREVIEW = 160;
@@ -14,6 +14,49 @@ function humanInterval(s?: number): string {
return "";
}
+// Automation detail (read-only display): backed by GET
+// /projects/{pid}/automations/{aid} — full definition + runs[] +
+// runs_note + heartbeat_binding. Editing is NOT duplicated here:
+// the single edit form is main's inline edit in the list below
+// (updateAutomation with the (a, name, prompt, interval, repo)
+// signature). Detail display + runs + task buttons are our side's
+// addition; cross-view jumps use location.hash because the IDE
+// shell switches views on hashchange (no sec-conv / sec-automations
+// element to scroll to).
+function AutomationDetailBox({ detail }: { detail: AutomationDetail }) {
+ const ctx = useConsole();
+ const openTask = (id?: string) => {
+ if (!id) return;
+ ctx.showDetail(id);
+ try { window.location.hash = "#view-tasks"; } catch { /* noop */ }
+ };
+ return (
+
+
自动化详情:{detail.name}
+
+ 状态 {detail.status}{detail.due ? "|到期" : ""} | 每 {detail.interval_seconds} 秒 | 下次 {fmt(detail.next_run_at)}
+ | 上次执行 {fmt(detail.last_run_at)} | 创建人 {detail.created_by ? shortId(detail.created_by) : ""} | 创建于 {fmt(detail.created_at)}
+ {detail.cancelled_at ? " | 已取消于 " + fmt(detail.cancelled_at) : ""}
+ {detail.heartbeat_binding ? " | 心跳绑定:已绑定" : ""}
+
+
{detail.prompt || ""}
+ {detail.repo_url ?
仓库 {detail.repo_url}
: null}
+
+ 关闭详情
+ 编辑请在下面列表里点“编辑”。{ctx.autoDetailMsg}
+
+
最近产出({(detail.runs || []).length})
+ (
+
+ {r.status}{r.created_at ? "|" + fmt(r.created_at) : ""}{r.task_id ? <> | 任务 openTask(r.task_id as string)}>{shortId(r.task_id)} > : ""}{r.detail ? "|" + String(r.detail).slice(0, 120) : ""}
+
+ ))} />
+
+ {detail.runs_note ?
{detail.runs_note}
: null}
+
+ );
+}
+
export function AutomationPanel() {
const ctx = useConsole();
const [name, setName] = useState("");
@@ -53,6 +96,7 @@ export function AutomationPanel() {
[{a.status}{a.due ? "|到期" : ""}{a.run_once ? "|一次性" : ""}] {a.name}|每 {a.interval_seconds} 秒{humanInterval(a.interval_seconds)}|上次 {fmt(a.last_run_at) || "未运行"}|下次 {fmt(a.next_run_at)}
+ void ctx.loadAutomationDetail(a.id)}>详情
{a.status === "active" ? <> startEdit(a)}>编辑 ctx.cancelAutomation(a.id)}>取消 > : null}
{a.repo_url ?
仓库:{a.repo_url}
: null}
@@ -80,6 +124,8 @@ export function AutomationPanel() {
);
})} />
+ {ctx.autoDetailMsg && !ctx.autoDetail ? {ctx.autoDetailMsg}
: null}
+ {ctx.autoDetail ? : null}
>
);
}
@@ -87,21 +133,45 @@ export function AutomationPanel() {
export function TriagePanel() {
const ctx = useConsole();
const t = ctx.triage;
+ // Clicking an actionable item opens that task's detail (tasks view)
+ // or the automation's detail (automation view). Field names are
+ // exactly the triage payload's: failed_tasks[].id, pending_followups
+ // [].task_id, recent_runs[].task_id (nullable), automations_due[].id.
+ // Jumps use location.hash because the IDE shell switches views on
+ // hashchange; there is no sec-conv / sec-automations to scroll to.
+ const openTask = (id?: string) => {
+ if (!id) return;
+ ctx.showDetail(id);
+ try { window.location.hash = "#view-tasks"; } catch { /* noop */ }
+ };
+ const openAutomation = (id?: string) => {
+ if (!id) return;
+ void ctx.loadAutomationDetail(id);
+ try { window.location.hash = "#view-automation"; } catch { /* noop */ }
+ };
if (ctx.triageError) return {ctx.triageError}
;
if (!t) return (先选项目)
;
return (
<>
到期自动化({t.counts?.automations_due || 0})
- {a.name}|每 {a.interval_seconds} 秒 )} />
+ (
+ {a.id ? openAutomation(a.id)}>{a.name}|每 {a.interval_seconds} 秒 : <>{a.name}|每 {a.interval_seconds} 秒>}
+ ))} />
失败/中断任务({t.counts?.failed_tasks || 0})
- {shortId(x.id)}|{x.status}|{String(x.prompt || "").slice(0, 80)} )} />
+ (
+ openTask(x.id)}>{shortId(x.id)}|{x.status}|{String(x.prompt || "").slice(0, 80)}
+ ))} />
待处理后续消息({t.counts?.pending_followups || 0})
- 任务 {shortId(f.task_id)}|{String(f.body || "").slice(0, 80)} )} />
+ (
+ openTask(f.task_id)}>任务 {shortId(f.task_id)}|{String(f.body || "").slice(0, 80)}
+ ))} />
最近自动化产出({t.counts?.recent_runs || 0})
- {shortId(r.automation_id)}|{r.status}{r.task_id ? "|任务 " + shortId(r.task_id) : ""}{r.task_status ? "|" + r.task_status : ""} )} />
+ (
+ {shortId(r.automation_id)}|{r.status}{r.task_id ? <>|任务 openTask(r.task_id)}>{shortId(r.task_id)} > : ""}{r.task_status ? "|" + r.task_status : ""}
+ ))} />
>
);
diff --git a/cloud/console-react/src/tokens.css b/cloud/console-react/src/tokens.css
index d746070..e311701 100644
--- a/cloud/console-react/src/tokens.css
+++ b/cloud/console-react/src/tokens.css
@@ -254,7 +254,7 @@ pre{background:var(--inset-bg);color:var(--text);border:1px solid var(--line);bo
which is still tight — just in a different order. */
.dash-col{display:contents}
#sec-dispatch{order:1} #sec-tasks{order:2} #sec-conv{order:3}
- #sec-projects{order:4} #sec-env{order:5} #sec-workers{order:6}
+ #sec-env{order:5} #sec-workers{order:6}
#sec-skills{order:7} #sec-automations{order:8} #sec-triage{order:9}
#sec-admin{order:10} #sec-key{order:11}
.dash-cards{grid-template-columns:repeat(2,1fr)}
@@ -285,6 +285,20 @@ pre{background:var(--inset-bg);color:var(--text);border:1px solid var(--line);bo
.shell-nav button{text-align:left;border:0;box-shadow:none;background:transparent;padding:8px;border-radius:var(--radius);font-weight:500}
.shell-nav button:hover{background:#fff}
.shell-nav button.active{background:var(--text);color:#fff}
+/* Project sidebar (ProjectNav): the one project selector for the whole
+ console — project list + create + members — living in the IDE
+ shell's left sidebar. Same Primer tokens as the rest of the shell;
+ the sidebar is 260px wide, so its bars stack full-width. */
+.shell-project-nav{padding:0 12px 16px;min-width:0}
+.shell-project-nav .project-nav{display:flex;flex-direction:column;gap:16px;min-width:0}
+.shell-project-nav .project-nav-list{list-style:none;margin:0;padding:0;border:1px solid var(--line);border-radius:var(--radius);overflow:hidden}
+.shell-project-nav .project-nav-item{padding:8px 12px;cursor:pointer;border-bottom:1px solid var(--line);background:#fff}
+.shell-project-nav .project-nav-item:last-child{border-bottom:0}
+.shell-project-nav .project-nav-item:hover{background:var(--subtle-bg)}
+.shell-project-nav .project-nav-item.selected{background:var(--selected-bg)}
+.shell-project-nav .project-nav-name{display:block;font-size:14px;font-weight:600;overflow-wrap:anywhere}
+.shell-project-nav .project-nav-meta{display:block;font-size:12px;color:var(--muted)}
+.shell-project-nav .bar > input[type="text"],.shell-project-nav .bar > select{flex:1 1 100%}
.shell-stats{margin-top:auto;border-top:1px solid var(--line);padding:16px 12px}
.shell-stats-label{font-size:12px;font-weight:600;color:var(--muted);padding:0 4px 8px}
.shell-stats .dash-cards{display:flex;flex-direction:column;gap:8px}
@@ -316,6 +330,7 @@ pre{background:var(--inset-bg);color:var(--text);border:1px solid var(--line);bo
.shell-side{flex-direction:row;flex-wrap:wrap;align-items:flex-start;border-right:0;border-bottom:1px solid var(--line);overflow:visible}
.shell-nav{flex-direction:row;flex-wrap:wrap}
.shell-nav-group{flex-direction:row;align-items:center}
+ .shell-project-nav{flex:1 1 100%}
.shell-stats{display:none}
.shell-hide-sm{display:none}
.workspace{grid-template-columns:1fr}
diff --git a/cloud/console.html b/cloud/console.html
index 11c70d6..dcd1e42 100644
--- a/cloud/console.html
+++ b/cloud/console.html
@@ -4,7 +4,7 @@
Codex 命令台
-
-
+`)}}S("流已断开")}catch(et){if(et.name==="AbortError")return;O(et instanceof Error?et.message:String(et)),S("未开始")}finally{x(!1)}},[r]),J=d.useCallback(()=>{var D;(D=H.current)==null||D.abort(),x(!1),S("已停止")},[]),gt=d.useCallback(async D=>{if(r)try{const X=await W(F.taskBrowserControl(r.id),"POST",{holder:D});C(X),O("")}catch(X){O(X instanceof Error?X.message:String(X))}},[r]),_t=d.useCallback(async D=>{if(!r||(y==null?void 0:y.holder)!=="user"||!L.current)return;const X=L.current.getBoundingClientRect(),et=Math.round((D.clientX-X.left)/X.width*1280),xt=Math.round((D.clientY-X.top)/X.height*800);try{await W(F.taskBrowserInput(r.id),"POST",{type:"mouse_click",x:et,y:xt})}catch(Mt){O(Mt instanceof Error?Mt.message:String(Mt))}},[r,y]),Dt=d.useCallback(async D=>{if(!(!r||(y==null?void 0:y.holder)!=="user")){D.preventDefault();try{D.key.length===1?await W(F.taskBrowserInput(r.id),"POST",{type:"type",text:D.key}):await W(F.taskBrowserInput(r.id),"POST",{type:"key",key:D.key})}catch(X){O(X instanceof Error?X.message:String(X))}}},[r,y]);if(!r)return c.jsx("div",{className:"muted",children:"点任务行查看详情"});const Q=(y==null?void 0:y.holder)||"—",ft=y!==null;return c.jsxs("div",{children:[c.jsxs("div",{style:{display:"flex",gap:8,flexWrap:"wrap",alignItems:"center"},children:[c.jsxs("span",{children:["状态 ",c.jsx("b",{children:M})]}),c.jsxs("span",{className:"muted",children:["会话 ",ft?"活跃":"未激活(轮次结束即消失)"]}),c.jsxs("span",{className:"muted",children:["控制权 ",Q]}),y!=null&&y.url?c.jsx("span",{className:"muted",title:y.url,children:y.url.slice(0,80)}):null]}),c.jsxs("div",{style:{display:"flex",gap:8,flexWrap:"wrap",marginTop:8},children:[Y?c.jsx("button",{type:"button",onClick:J,children:"停止观看"}):c.jsx("button",{type:"button",onClick:()=>void dt(),children:"开始观看"}),c.jsx("button",{type:"button",onClick:()=>void tt(),children:"刷新状态"}),Q!=="user"?c.jsx("button",{type:"button",onClick:()=>void gt("user"),disabled:!ft,children:"接管"}):c.jsx("button",{type:"button",onClick:()=>void gt("agent"),children:"交还给 Agent"})]}),_?c.jsx("div",{className:"muted",style:{marginTop:8},children:_}):null,Q==="user"?c.jsx("div",{className:"muted",style:{marginTop:8},children:"你已接管:点画面即点击,键盘输入直达页面。交还后 Agent 续跑前会先快照确认你的改动。"}):null,c.jsx("div",{style:{marginTop:8},children:h?c.jsx("img",{ref:L,src:h,alt:"浏览器实时画面",tabIndex:0,onClick:D=>void _t(D),onKeyDown:D=>void Dt(D),style:{width:"100%",maxWidth:1280,cursor:Q==="user"?"crosshair":"default",border:"1px solid var(--border, #ccc)"}}):c.jsx("div",{className:"muted",children:ft?"点“开始观看”接入画面流(接管后点画面即点击)":"浏览器会话未激活:任务本轮未开浏览器,或轮次已结束。"})})]})}function jg(){const i=Lt(),[r,h]=d.useState("uncommittedChanges"),[f,y]=d.useState(""),[C,M]=d.useState(""),[S,_]=d.useState("");return c.jsxs(c.Fragment,{children:[c.jsx("h3",{children:"评审队列"}),c.jsxs("select",{"aria-label":"评审目标",value:r,onChange:O=>h(O.target.value),children:[c.jsx("option",{value:"uncommittedChanges",children:"未提交改动(工作区)"}),c.jsx("option",{value:"baseBranch",children:"与分支的差异"}),c.jsx("option",{value:"commit",children:"指定提交"}),c.jsx("option",{value:"custom",children:"自定义评审指令"})]}),c.jsx("input",{className:"stacked",placeholder:"与分支的差异用:分支名(如 main)",style:{width:"100%"},value:f,onChange:O=>y(O.target.value)}),c.jsx("input",{className:"stacked",placeholder:"指定提交用:提交 sha",style:{width:"100%"},value:C,onChange:O=>M(O.target.value)}),c.jsx("textarea",{placeholder:"自定义评审指令用:评审指令正文",value:S,onChange:O=>_(O.target.value)}),c.jsxs(At,{children:[c.jsx("button",{className:"primary",onClick:()=>void i.postReview({target_type:r,branch:f.trim()||void 0,sha:C.trim()||void 0,instructions:S.trim()||void 0}),children:"登记评审需求"}),c.jsx("button",{onClick:i.refreshReviews,children:"刷新队列"}),c.jsx("span",{className:"muted",children:i.reviewMsg})]}),c.jsx("div",{className:"muted",children:"本基线只登记评审需求(requested),不自动跑评审;评审是否执行、结果如何,以评审队列里的记录为准。"}),i.reviewError?c.jsx("div",{className:"muted",children:i.reviewError}):i.reviews.length===0?c.jsx("div",{className:"muted",children:"点任务行查看详情后,点“刷新队列”查看评审记录"}):c.jsx(ou,{children:i.reviews.map(O=>{let Y=O.target_type||"";return O.branch&&(Y+=":"+O.branch),O.sha&&(Y+=":"+String(O.sha).slice(0,12)),O.instructions&&(Y+=":"+String(O.instructions).slice(0,80)),c.jsxs("li",{children:[c.jsxs("span",{className:"row-main",children:[c.jsx("b",{children:"评审"})," ",pe(O.id)," | ",Y," | ",c.jsx("span",{className:"st-"+(O.status||""),children:O.status}),O.created_at?" | "+Le(O.created_at):"",c.jsx("br",{}),O.findings?String(O.findings).slice(0,1e3):c.jsx("span",{className:"muted",children:"(暂无 findings:评审尚未执行或尚未写回)"})]}),c.jsx("button",{onClick:()=>i.submitFindings(O.id),children:"写 findings"})]},O.id)})})]})}function Tg(){const i=Lt();return i.currentTask?i.currentTask.coordinator_parts?i.coordChildren.length===0?c.jsx("div",{className:"muted",children:"(子任务尚未创建)"}):c.jsx(ou,{children:i.coordChildren.map(r=>c.jsx("li",{children:c.jsxs("span",{className:"row-main",children:["分工 ",c.jsx("b",{children:r.coordinator_part||""})," | 状态 ",c.jsx("span",{className:"st-"+r.status,children:r.status})," | ",pe(r.id)," | ",String(r.prompt||"").slice(0,60)]})},r.id))}):c.jsx("div",{className:"muted",children:"(非协调员任务)"}):c.jsx("div",{className:"muted",children:"点任务行查看详情后,这里显示协调员的子任务"})}function _g(){const i=Lt();if(!i.currentTask)return c.jsx("div",{className:"muted",children:"点任务行查看详情后,这里显示子代理链路"});if(i.agents.length===0)return c.jsxs("div",{className:"muted",children:["(暂无子代理)",i.agentsNote?c.jsx("span",{className:"err",children:i.agentsNote}):null]});const r={main:"主任务",delegate:"委派子任务",model:"模型子代理"},h={running:"进行中",completed:"已完成",failed:"失败",interrupted:"已中断",unknown:"未知"};return c.jsx(ou,{children:i.agents.map((f,y)=>c.jsx("li",{children:c.jsxs("span",{className:"row-main",children:[c.jsxs("b",{children:[r[f.kind||""]||f.kind||"",":"]}),f.name||""," ",c.jsx("span",{className:"st-"+(f.status||""),children:h[f.status||""]||f.status||"?"})," | 过程调用 "+(f.calls?f.calls.length:0)+" 条",f.updated_ts?" | 最近活动 "+Le(f.updated_ts/1e3):"",f.prompt?c.jsxs(c.Fragment,{children:[c.jsx("br",{}),c.jsxs("span",{className:"muted",children:["任务:",f.prompt]})]}):null,f.calls&&f.calls.length?c.jsxs(c.Fragment,{children:[c.jsx("br",{}),f.calls.slice(-50).map((C,M)=>c.jsx("div",{children:yo(C)},M))]}):c.jsxs(c.Fragment,{children:[c.jsx("br",{}),c.jsx("span",{className:"muted",children:"(暂无过程调用)"})]})]})},y))})}function Eg(){const i=Lt();return c.jsxs(c.Fragment,{children:[c.jsxs(At,{children:[c.jsx("button",{onClick:i.refreshApprovals,children:"刷新审批队列"}),c.jsx("span",{className:"muted",children:i.approvalMsg})]}),c.jsx("div",{className:"muted",children:"待审批项需人工点记录允许或记录拒绝;超时默认拒绝。当前版本只留回执,不拦截实时行为(实时请求的事前拦截待 host 接入后生效,在此之前实时行为不变)。"}),c.jsx(ol,{empty:"(暂无审批项)",items:i.approvals.map(r=>c.jsxs("span",{className:"row-main",children:["[",r.kind,"|",r.status,"] ",r.summary,["approved","denied","expired"].includes(r.status||"")?"|回执:"+(r.decision||r.status)+" by "+String(r.decided_by||"").slice(0,8)+" "+Le(r.decided_at):"",r.status==="pending"?c.jsxs(c.Fragment,{children:[" ",c.jsx("button",{onClick:()=>i.decideApproval(r.id,"approved"),children:"记录允许"})," ",c.jsx("button",{onClick:()=>i.decideApproval(r.id,"denied"),children:"记录拒绝"})]}):null]},r.id))})]})}function Ng(){const i=Lt(),[r,h]=d.useState(""),[f,y]=d.useState("");return c.jsxs(c.Fragment,{children:[c.jsx("input",{placeholder:"目标任务 id(与本任务同项目;对方运行中排队执行、已终态则续跑复活,绝不打断当前轮)",style:{width:"100%"},value:r,onChange:C=>h(C.target.value)}),c.jsx("textarea",{placeholder:"消息正文(以本任务名义发出;对方 agent 会看到发送方任务 id,可照此回消息)",value:f,onChange:C=>y(C.target.value)}),c.jsxs(At,{children:[c.jsx("button",{onClick:async()=>{await i.postPeer("queued",r,f),y("")},children:"排队发给对方"}),c.jsx("button",{className:"primary",onClick:async()=>{await i.postPeer("direct",r,f),y("")},children:"直接发送"}),c.jsx("span",{className:"muted",children:i.peerMsg})]}),c.jsx(ol,{empty:"(本任务尚未发出任务间消息)",items:i.peerMessages.map((C,M)=>c.jsxs("span",{className:"row-main",children:["[发给 ",String(C.task_id).slice(0,8),"|",C.mode,"|",C.status,"] ",String(C.body||"").slice(0,120)]},M))})]})}function Cg(){const i=Lt(),[r,h]=d.useState(""),f=i.currentTask?["completed","failed","interrupted"].includes(i.currentTask.status):!1;return c.jsxs(c.Fragment,{children:[c.jsx("div",{className:"muted",children:i.threadText||"点任务行查看详情后,这里显示线程元数据"}),c.jsxs(At,{children:[c.jsx("input",{type:"text",placeholder:"新线程名",value:r,onChange:y=>h(y.target.value)}),c.jsx("button",{disabled:!f,onClick:()=>i.threadAction("rename",r),children:"重命名"}),c.jsx("button",{disabled:!f,onClick:()=>i.threadAction("archive",""),children:"归档"}),c.jsx("button",{disabled:!f,onClick:()=>i.threadAction("unarchive",""),children:"取消归档"}),c.jsx("button",{disabled:!f,onClick:()=>i.threadAction("compact",""),children:"压缩"}),c.jsx("span",{className:"muted",children:i.threadGateNote}),c.jsx("span",{className:"muted",children:i.threadMsg})]}),c.jsx(ol,{empty:"暂无其他线程",items:i.threadList.map((y,C)=>c.jsxs("span",{className:"row-main",children:[y.name||y.thread_name||y.id||""," [",y.id||"","]",y.archived?" [已归档]":"",y.cwd?" "+y.cwd:""]},C))}),c.jsx("div",{className:"muted",children:"限制:Docker/异机任务线程操作一律 409;merged/merge-failed 不可改;turn/steer 与 review/start 不在本批。"})]})}function Ag(i){return/503|Service Unavailable/.test(i)?"上游模型服务暂时不可用(503):模型一轮都没跑出来,不是任务逻辑失败;等上游恢复或换模型重试":/429|rate limit/i.test(i)?"上游限流(429):模型拒绝了本次调用,可稍后重试":/timeout|timed out/i.test(i)?"上游调用超时:模型响应未在限时内返回":/401|403|Invalid API key|unauthorized/i.test(i)?"上游鉴权失败:网关 key 或权限有问题":/ECONNREFUSED|ENOTFOUND|network|fetch failed/i.test(i)?"连不上上游模型服务:网络或网关地址有问题":"任务失败(原因见原始错误文本)"}function Mg(i){return i===null||i<0?"未知":i<60?Math.floor(i)+" 秒前":i<3600?Math.floor(i/60)+" 分钟前":(i/3600).toFixed(1)+" 小时前"}const Og=[["overview","概览"],["collab","协作"],["review","评审与审批"],["threads","线程"],["browser","浏览器"]];function zg(){const i=Lt(),r=i.currentTask,[h,f]=d.useState("overview");if(!r)return c.jsx(Ft,{id:"nav-detail",title:"任务详情",children:c.jsx("div",{className:"muted",children:"点任务行查看详情"})});const[y,C]=$h(r),[M,S]=Ih(r),_=Wh(r),O=i.costSummary,Y=r.plan?"计划模式(只读)":r.goal_mode?r.no_time_limit?"goal 模式无敌版(不限时)":"goal 模式(不限轮次,模型宣告完成)":r.verify_command?"goal 模式(旧语义,验收命令)":null;return c.jsxs("div",{className:"detail-stack",id:"nav-detail",children:[c.jsx("nav",{className:"detail-tabs","aria-label":"任务详情页签",children:Og.map(([x,H])=>c.jsx("button",{type:"button",className:h===x?"active":"",onClick:()=>f(x),children:H},x))}),h==="overview"?c.jsxs(c.Fragment,{children:[c.jsxs(Ft,{title:"任务详情",children:[c.jsxs("div",{style:{display:"flex",alignItems:"center",gap:8,flexWrap:"wrap"},children:[c.jsx("span",{className:"issue-first",title:"任务首字:"+_,"aria-hidden":"true",children:_}),c.jsxs("span",{children:["状态 ",c.jsx("b",{className:"st-"+r.status,children:r.status}),"(",Wy[r.status]||"未知状态",")"]}),c.jsxs("span",{className:"muted",children:["更新 ",Le(r.updated_at)]})]}),c.jsxs("div",{className:"detail-cards",children:[c.jsxs("div",{className:"detail-card",title:C,children:[c.jsx("div",{className:"detail-card-label",children:"耗时"}),c.jsx("div",{className:"detail-card-value",children:y})]}),c.jsxs("div",{className:"detail-card",title:S,children:[c.jsx("div",{className:"detail-card-label",children:"token"}),c.jsx("div",{className:"detail-card-value",children:M})]}),c.jsxs("div",{className:"detail-card",title:"LLM 请求数 = GET /tasks/{id}/cost 的 llm_requests,即 cost_records 的 completions 之和:host.py 每收到一次 thread/tokenUsage/updated(一次上游补全)计 1,worker 每轮终局记账时累加;估算行(无真实用量)计 0;部署前已记账的老任务没有 completions 列,显示 0 而非回填数",children:[c.jsx("div",{className:"detail-card-label",children:"LLM 请求数"}),c.jsx("div",{className:"detail-card-value",children:O===void 0?"加载中…":O===null?"—":(O.llm_requests??0).toLocaleString()}),O===void 0?c.jsx("div",{className:"muted",children:"成本加载中…"}):O===null?c.jsx("div",{className:"muted",children:"成本明细加载失败,LLM 请求数不可用"}):c.jsxs("div",{className:"muted",children:["上游补全次数之和| 轮数 ",O.turns??0," | fallback ",O.fallback_count??0]})]}),c.jsxs("div",{className:"detail-card",title:"成本明细:GET /tasks/{id}/cost 聚合;美元只计有价行,无价 token 单列;缓存是输入 token 的子集,不另计",children:[c.jsx("div",{className:"detail-card-label",children:"成本"}),c.jsx("div",{className:"detail-card-value",children:O===void 0?"加载中…":O===null?"—":dg(O.usd_micros)}),O===void 0?c.jsx("div",{className:"muted",children:"成本加载中…"}):O===null?c.jsx("div",{className:"muted",children:"成本明细加载失败(“查成本”按钮仍可手动拉取原始 JSON)"}):c.jsxs("div",{className:"muted",children:["token ",(O.total_tokens??0).toLocaleString(),"(实际 ",(O.actual_tokens??0).toLocaleString()," / 估算 ",(O.estimated_tokens??0).toLocaleString(),",缓存 ",(O.cached_input_tokens??0).toLocaleString(),",无价 ",(O.unpriced_actual_tokens??0).toLocaleString(),")| fallback ",O.fallback_count??0]})]})]})]}),r.status==="running"||r.status==="queued"?(()=>{const x=Date.now()/1e3,H=r.last_heartbeat||r.updated_at||null,L=H?x-H:null;return c.jsxs(Ft,{title:"LLM 进度",children:[c.jsxs("div",{children:["状态 ",r.status==="running"?"进行中":"排队中"," · 已用 ",y,L!==null?" · 上次活动 "+Mg(L):""]}),L!==null&&L>180?c.jsx("div",{className:"muted",children:"超过 3 分钟无活动:可能在等上游模型响应(上游 503 时 worker 会重试后失败,失败后这里会显示失败原因;无活动不等于已断连,服务端没有“LLM 已连接”字段,不作此断言)"}):c.jsx("div",{className:"muted",children:"首轮无输出时属正常:模型第一轮回复到达前 trace 里没有消息;回复到达后会在会话区出现,成本里的 LLM 请求数随之增加"})]})})():null,r.error_text?c.jsxs(Ft,{title:"失败原因",children:[c.jsx("div",{children:Ag(r.error_text)}),c.jsx("pre",{style:{whiteSpace:"pre-wrap",overflowWrap:"anywhere"},children:r.error_text.slice(0,1e3)}),c.jsxs("div",{className:"muted",children:["完整错误文本仍在下方“操作与结果”里;模型 ",r.model||"默认"," · executor ",r.executor||""]})]}):null,c.jsx(Ft,{title:"基本信息",children:c.jsxs("dl",{className:"detail-kv",children:[c.jsxs("div",{children:[c.jsx("dt",{children:"任务"}),c.jsx("dd",{children:r.id})]}),c.jsxs("div",{children:[c.jsx("dt",{children:"turn"}),c.jsx("dd",{children:pe(r.turn_id)})]}),c.jsxs("div",{children:[c.jsx("dt",{children:"root turn"}),c.jsx("dd",{children:pe(r.root_turn_id)})]}),c.jsxs("div",{children:[c.jsx("dt",{children:"executor"}),c.jsx("dd",{children:r.executor||""})]}),c.jsxs("div",{children:[c.jsx("dt",{children:"主机"}),c.jsx("dd",{children:r.worktree_host||""})]}),c.jsxs("div",{children:[c.jsx("dt",{children:"模型"}),c.jsx("dd",{children:r.model||"默认"})]}),c.jsxs("div",{children:[c.jsx("dt",{children:"强度"}),c.jsx("dd",{children:r.effort||"默认"})]}),r.child_model||r.child_effort?c.jsxs("div",{children:[c.jsx("dt",{children:"子任务默认"}),c.jsx("dd",{children:(r.child_model||"跟随本任务")+" / "+(r.child_effort||"跟随本任务")})]}):null,c.jsxs("div",{children:[c.jsx("dt",{children:"权限"}),c.jsx("dd",{children:r.approval||"默认"})]}),c.jsxs("div",{children:[c.jsx("dt",{children:"项目"}),c.jsx("dd",{children:pe(r.project_id)})]}),Y?c.jsxs("div",{children:[c.jsx("dt",{children:"模式"}),c.jsx("dd",{children:Y})]}):null,r.forked_from?c.jsxs("div",{children:[c.jsx("dt",{children:"分支自"}),c.jsx("dd",{children:pe(r.forked_from)})]}):null,r.coordinator_parts?c.jsxs("div",{children:[c.jsx("dt",{children:"协调员"}),c.jsxs("dd",{children:[r.coordinator_parts.length," 个子任务(",r.coordinator_parts.map(x=>x.name).join("、"),")"]})]}):null,r.parent_task_id?c.jsxs("div",{children:[c.jsx("dt",{children:"协调子任务"}),c.jsxs("dd",{children:["父任务 ",pe(r.parent_task_id),r.coordinator_part?"("+r.coordinator_part+")":""]})]}):null,r.attachments&&r.attachments.length?c.jsxs("div",{children:[c.jsx("dt",{children:"附件"}),c.jsx("dd",{children:r.attachments.join("、")})]}):null,r.pr_url?c.jsxs("div",{children:[c.jsx("dt",{children:"PR"}),c.jsx("dd",{children:c.jsxs("a",{href:r.pr_url,target:"_blank",rel:"noopener noreferrer",children:["PR #",r.pr_number]})})]}):null]})}),c.jsx(Ft,{title:"prompt",children:c.jsx("div",{style:{whiteSpace:"pre-wrap",overflowWrap:"anywhere"},children:r.prompt||"(无 prompt)"})}),c.jsxs(Ft,{title:"操作与结果",children:[c.jsx(xg,{}),c.jsx("pre",{children:i.detailBody||"(暂无 result/error 输出)"}),i.costBox?c.jsx("pre",{children:i.costBox}):null]})]}):h==="collab"?c.jsxs(c.Fragment,{children:[c.jsx(Ft,{title:"协调子任务",children:c.jsx(Tg,{})}),c.jsx(Ft,{title:"子代理链路",children:c.jsx(_g,{})}),c.jsx(Ft,{title:"给其他任务发消息",children:c.jsx(Ng,{})})]}):h==="review"?c.jsxs(c.Fragment,{children:[c.jsx(Ft,{title:"评审队列",children:c.jsx(jg,{})}),c.jsx(Ft,{title:"运行时审批队列",children:c.jsx(Eg,{})})]}):h==="browser"?c.jsx(c.Fragment,{children:c.jsx(Ft,{title:"浏览器远控",children:c.jsx(bg,{})})}):c.jsx(c.Fragment,{children:c.jsx(Ft,{title:"线程",children:c.jsx(Cg,{})})})]})}function Rg(){const i=Lt(),[r,h]=d.useState(""),[f,y]=d.useState(""),[C,M]=d.useState([]),[S,_]=d.useState(null),[O,Y]=d.useState(!1),[x,H]=d.useState(""),[L,tt]=d.useState(""),dt=d.useCallback(async()=>{if(!i.keySaved){M([]);return}try{const Q=await W(F.userKeys);M(Q.keys||[]),tt("")}catch(Q){tt(Q instanceof Error?Q.message:"加载 key 列表失败")}},[i.keySaved]);d.useEffect(()=>{dt()},[dt]);const J=async()=>{H(""),tt(""),Y(!1);try{const Q=await W(F.userKeys,"POST",{label:f.trim()||"api-key"});_(Q),y(""),H("新 key 已生成,只显示一次:复制后请妥善保存。"),await dt()}catch(Q){tt(Q instanceof Error?Q.message:"生成 key 失败")}},gt=async()=>{if(S)try{await navigator.clipboard.writeText(S.api_key),Y(!0)}catch{tt("复制失败:请手动选中上方 key 文本复制")}},_t=()=>{S&&(i.saveKeyInput(S.api_key),_(null),H("已存为当前浏览器 key(原有 key 未被撤销,需要时可单独撤销)。"))},Dt=async Q=>{const ft=(()=>{try{return rl().slice(-4)}catch{return""}})(),D=Q.id==="legacy"?"(主 key:撤销后当前浏览器保存的 key 也可能随之失效,需重新登录拿新 key)":Q.hint&&Q.hint===ft?"(这就是本浏览器当前在用的 key,撤销后本页会立刻未连接)":"";if(window.confirm(`撤销 key「${Q.label}」(后 4 位 ${Q.hint||"无"})吗?${D}撤销不可恢复。`)){H(""),tt("");try{await W(F.userKey(Q.id),"DELETE"),H(`已撤销 key「${Q.label}」。`),await dt()}catch(X){tt(X instanceof Error?X.message:"撤销失败")}}};return c.jsxs(Ft,{id:"nav-key",title:"API Key",children:[c.jsxs(At,{children:[c.jsx("input",{type:"password",placeholder:"粘贴你的 X-API-Key(与调 API 用的同一个)",autoComplete:"off",value:r,onChange:Q=>h(Q.target.value)}),c.jsx("button",{className:"primary",onClick:()=>{i.saveKeyInput(r),h("")},children:"保存到本浏览器"}),c.jsx("button",{onClick:i.clearKeyInput,children:"清除"}),c.jsx("span",{className:"muted",children:i.keyState})]}),c.jsx("div",{children:c.jsx("span",{className:"muted",children:i.quotaText})}),!i.keySaved&&c.jsx("div",{children:c.jsx("span",{className:"muted",children:"先粘贴并保存已有 key,再生成或管理 key(生成接口本身也要用已有 key 鉴权)。"})}),i.keySaved&&c.jsxs(c.Fragment,{children:[c.jsxs(At,{children:[c.jsx("input",{placeholder:"新 key 备注(可选,默认 api-key,如 harness2-temp)",autoComplete:"off",value:f,onChange:Q=>y(Q.target.value)}),c.jsx("button",{className:"primary",onClick:()=>void J(),children:"生成新 key"}),c.jsx("button",{onClick:()=>void dt(),children:"刷新列表"})]}),S&&c.jsxs("div",{children:[c.jsx("div",{children:c.jsxs("span",{className:"muted",children:["新 key(",S.label,",后 4 位 ",S.hint,"):只显示一次,关闭后无法再查看。"]})}),c.jsxs(At,{children:[c.jsx("input",{type:"text",readOnly:!0,value:S.api_key,onFocus:Q=>Q.target.select()}),c.jsx("button",{className:"primary",onClick:()=>void gt(),children:O?"已复制":"复制"}),c.jsx("button",{onClick:_t,children:"存为当前浏览器 key"}),c.jsx("button",{onClick:()=>_(null),children:"关闭(不再显示)"})]})]}),x&&c.jsx("div",{children:c.jsx("span",{className:"muted",children:x})}),L&&c.jsx("div",{children:c.jsx("span",{children:L})}),c.jsxs("div",{className:"api-key-list",children:[C.length===0&&c.jsx("span",{className:"muted",children:"暂无 key(或列表加载失败,见上方报错)。"}),C.map(Q=>c.jsxs("div",{children:[c.jsx("span",{children:Q.label})," ",c.jsxs("span",{className:"muted",children:["后 4 位 ",Q.hint||"无"," · 创建 ",Le(Q.created_at)," · 最近使用 ",Q.last_used_at?Le(Q.last_used_at):"未使用过"]})," ",Q.revoked_at?c.jsxs("span",{className:"muted",children:["已撤销(",Le(Q.revoked_at),")"]}):c.jsx("button",{onClick:()=>void Dt(Q),children:"撤销"})]},Q.id))]})]})]})}function Dg(){const i=Lt(),[r,h]=d.useState(""),[f,y]=d.useState("");return c.jsxs(Ft,{id:"adminCard",title:c.jsxs(c.Fragment,{children:["管理区 ",c.jsx("span",{className:"muted",children:"(仅全局管理员)"})]}),children:[c.jsx("div",{className:"muted",children:i.adminGate}),i.adminBody?c.jsxs(c.Fragment,{children:[c.jsxs(At,{children:[c.jsx("input",{type:"text",placeholder:"要查配额的用户 id",value:i.adminUserId,onChange:C=>i.setAdminUserId(C.target.value)}),c.jsx("button",{onClick:i.adminQuota,children:"查该用户配额"})]}),c.jsxs(At,{children:[c.jsx("input",{type:"text",placeholder:"要设置配额的用户 id",value:r,onChange:C=>h(C.target.value)}),c.jsx("input",{type:"text",placeholder:"美元配额(留空 = 不限额)",value:f,onChange:C=>y(C.target.value)}),c.jsx("button",{className:"primary",onClick:()=>i.adminSetQuota(r,f),children:"设置配额"})]}),c.jsxs(At,{children:[c.jsx("button",{onClick:i.adminSweep,children:"手动触发 sweep(清理陈旧 running 任务)"}),c.jsx("button",{onClick:i.adminBackup,children:"手动触发备份"})]}),c.jsx("div",{className:"muted",children:i.adminResult})]}):null]})}function Ug(){const i=Lt(),[r,h]=d.useState(""),[f,y]=d.useState("contributor"),C=i.projects.find(M=>M.id===i.projectId);return c.jsxs(Ft,{title:"项目成员",children:[C?c.jsxs("div",{className:"muted",children:[C.name," | ",C.id," | 仓库 ",C.repo_url||"未绑定"," | 我的角色 ",C.my_role||"未知"]}):c.jsx("div",{className:"muted",children:"先选项目"}),c.jsxs(At,{children:[c.jsx("input",{type:"text",placeholder:"成员 user_id 或用户名",value:r,onChange:M=>h(M.target.value)}),c.jsxs("select",{"aria-label":"邀请角色",value:f,onChange:M=>y(M.target.value),children:[c.jsx("option",{value:"viewer",children:"viewer(只读)"}),c.jsx("option",{value:"contributor",children:"contributor(可建任务)"}),c.jsx("option",{value:"maintainer",children:"maintainer(可批合并)"}),c.jsx("option",{value:"owner",children:"owner(可管项目)"})]}),c.jsx("button",{onClick:async()=>{await i.inviteMember(r,f),h("")},children:"邀请成员"}),c.jsx("button",{onClick:i.refreshMembers,children:"刷新成员"})]}),i.memberError?c.jsx("div",{className:"muted",children:i.memberError}):i.members.length===0?c.jsx("div",{className:"muted",children:"先选项目"}):c.jsx(ou,{children:i.members.map(M=>{const S=(M.username||"")+(M.username?"("+String(M.user_id).slice(0,8)+")":String(M.user_id).slice(0,8));return c.jsxs("li",{children:[c.jsx("span",{className:"row-main",children:S}),c.jsx("select",{"aria-label":"成员角色",value:M.role||"viewer",onChange:_=>void i.changeMemberRole(M.user_id,_.target.value),children:$y.map(_=>c.jsx("option",{value:_,children:_},_))}),c.jsx("button",{onClick:()=>void i.removeMember(M.user_id),children:"移除"})]},M.user_id)})}),c.jsx("span",{className:"muted",children:i.memberMsg})]})}function wg(){const i=Lt(),[r,h]=d.useState(""),[f,y]=d.useState(""),[C,M]=d.useState("");return c.jsxs("div",{className:"project-nav",children:[c.jsxs(Ft,{title:"项目",children:[c.jsx(At,{children:c.jsx("button",{onClick:i.refreshProjects,children:"刷新项目"})}),i.projects.length===0?c.jsx("div",{className:"muted",children:"暂无项目"}):c.jsx("ul",{className:"project-nav-list",children:i.projects.map(S=>c.jsxs("li",{className:"project-nav-item"+(S.id===i.projectId?" selected":""),onClick:()=>i.selectProject(S.id),title:S.id,children:[c.jsx("span",{className:"project-nav-name",children:S.name}),c.jsxs("span",{className:"project-nav-meta",children:[pe(S.id),S.my_role?" · "+S.my_role:""]})]},S.id))}),c.jsx("h3",{children:"创建项目"}),c.jsx(At,{children:c.jsx("input",{type:"text",placeholder:"新项目名",value:r,onChange:S=>h(S.target.value)})}),c.jsx(At,{children:c.jsxs("select",{"aria-label":"从 GitHub 仓库选",value:f,onChange:S=>{y(S.target.value),S.target.value&&M(S.target.value)},children:[c.jsx("option",{value:"",children:"(从 GitHub App 安装仓库里选,或手动输入)"}),i.githubRepos.map(S=>c.jsx("option",{value:S.clone_url,children:S.repo_full_name},S.clone_url))]})}),c.jsxs(At,{children:[c.jsx("input",{type:"text",placeholder:"repo_url(可选)",value:C,onChange:S=>M(S.target.value)}),c.jsx("button",{className:"primary",onClick:async()=>{await i.createProject(r,C),h(""),M(""),y("")},children:"创建项目"})]}),c.jsx("span",{className:"muted",children:i.projectMsg})]}),c.jsx(Ug,{})]})}function kg(){const i=Lt(),[r,h]=d.useState(""),[f,y]=d.useState(""),[C,M]=d.useState("");return c.jsxs(Ft,{id:"nav-env",title:c.jsxs(c.Fragment,{children:["环境 ",c.jsx("span",{className:"muted",children:"(选、建、管:当前项目的基准检出)"})]}),children:[c.jsxs(At,{children:[c.jsx("button",{onClick:i.refreshEnvs,children:"刷新"}),c.jsx("span",{className:"muted",children:i.envCount})]}),i.envs.length===0?c.jsx("div",{className:"muted",children:"先选项目"}):c.jsx(ou,{children:i.envs.map(S=>c.jsxs("li",{children:[c.jsxs("span",{className:"row-main",children:[S.name,"(",pe(S.id),") / 分支 ",S.branch||"默认分支"," / 执行器 ",S.executor||"不限"," / 状态 ",S.status,S.status==="failed"&&S.error_text?" / 失败原因:"+S.error_text:""]}),S.status==="archived"?null:c.jsx("button",{onClick:()=>void i.archiveEnv(S.id),children:"归档"})]},S.id))}),c.jsxs(At,{children:[c.jsx("input",{type:"text",placeholder:"新环境名",value:r,onChange:S=>h(S.target.value)}),c.jsx("input",{type:"text",placeholder:"分支(可选,空=默认分支)",value:f,onChange:S=>y(S.target.value)}),c.jsxs("select",{"aria-label":"新环境执行器",value:C,onChange:S=>M(S.target.value),children:[c.jsx("option",{value:"",children:"执行器:不限"}),c.jsx("option",{value:"local",children:"local"}),c.jsx("option",{value:"docker",children:"docker"})]}),c.jsx("button",{onClick:async()=>{await i.createEnv(r,f,C),h(""),y("")},children:"新建环境"})]}),c.jsx("span",{className:"muted",children:i.envMsg})]})}function Hg(){const i=Lt(),[r,h]=d.useState("");return c.jsxs(Ft,{title:"仓库与环境上下文",children:[c.jsx("div",{className:"muted",children:i.envRepoLine||"先选项目"}),c.jsx("div",{className:"muted",children:i.envCount}),i.envRepoBar?c.jsxs(At,{children:[c.jsxs("select",{"aria-label":"补仓库",value:r,onChange:f=>h(f.target.value),children:[c.jsx("option",{value:"",children:"(从 GitHub App 安装仓库里选)"}),i.githubRepos.map(f=>c.jsx("option",{value:f.clone_url,children:f.repo_full_name},f.clone_url))]}),c.jsx("button",{onClick:()=>void i.saveEnvRepo(r),children:"保存仓库"})]}):null,c.jsx("span",{className:"muted",children:i.envRepoMsg})]})}function Bg(){const i=Lt(),[r,h]=d.useState(""),[f,y]=d.useState("all"),C=_=>{const O=vo(_);return _.alive!==void 0&&_.alive!==null?!!_.alive:O.age!==null&&O.age<=120},M=r.trim().toLowerCase(),S=(i.workers||[]).filter(_=>!(M&&!((_.worker_id||_.id||"")+" "+(_.executor||"")).toLowerCase().includes(M)||f==="online"&&!C(_)||f==="offline"&&C(_)));return c.jsxs(Ft,{id:"nav-workers",title:"Workers",children:[c.jsx(At,{children:c.jsx("button",{onClick:i.refreshWorkers,children:"刷新"})}),c.jsxs(At,{children:[c.jsx("input",{type:"text",placeholder:"搜索 worker_id / 执行器","aria-label":"搜索 Worker",value:r,onChange:_=>h(_.target.value)}),c.jsxs("select",{"aria-label":"在线状态筛选",value:f,onChange:_=>y(_.target.value),children:[c.jsx("option",{value:"all",children:"在线状态:全部"}),c.jsx("option",{value:"online",children:"仅在线"}),c.jsx("option",{value:"offline",children:"仅离线"})]}),i.workers?c.jsxs("span",{className:"muted",children:["显示 ",S.length," / 共 ",i.workers.length," 个"]}):null]}),i.workersError?c.jsx("div",{className:"muted",children:i.workersError}):i.workers===null?c.jsxs("div",{className:"muted",children:[i.workersSummary||"未加载","(完整清单仅全局管理员可见,搜索/筛选仅对完整清单生效)"]}):i.workers.length===0?c.jsx("div",{className:"muted",children:"无 worker 注册"}):S.length===0?c.jsx("div",{className:"muted",children:"没有符合筛选条件的 worker"}):c.jsx(ou,{children:S.map((_,O)=>{const Y=_.running===void 0||_.running===null?"?":_.running,x=_.capacity===void 0||_.capacity===null?"?":_.capacity,H=vo(_),L=C(_),tt=L?" / 在线":" / 离线(占用为上次上报值)";return c.jsx("li",{className:L?"is-online":"is-offline",children:c.jsxs("span",{className:"row-main",children:[_.worker_id||_.id||"?"," / ",_.executor||""," / 占用 ",Y,"/",x,tt," / 最后心跳 ",H.text]})},O)})})]})}function qg(){const i=Lt();return c.jsxs(Ft,{title:"健康与配额",children:[c.jsxs(At,{children:[c.jsx("span",{className:"muted",children:i.healthText}),c.jsx("button",{onClick:i.refreshHealth,children:"刷新"})]}),c.jsx("div",{children:c.jsx("span",{className:"muted",children:i.quotaText})}),c.jsx("div",{className:"muted",children:i.workersSummary})]})}function Yg(){const i=Lt();return c.jsxs(Ft,{id:"nav-skills",title:"技能与 MCP",children:[c.jsx("div",{className:"muted",children:"反映宿主租户 home,容器内 home 可能不同;重载只在 schema 枚举里,当前 pin 的二进制拒绝时原样 502;OAuth 只发起登录,不代表登录完成。"}),c.jsxs(At,{children:[c.jsx("button",{onClick:i.refreshSkills,children:"刷新"}),c.jsx("span",{className:"muted",children:i.skillMsg})]}),c.jsx("h3",{children:"技能"}),i.skillError?c.jsx("div",{className:"muted",children:i.skillError}):c.jsx(ol,{empty:"暂无技能",items:i.skills.map((r,h)=>c.jsxs("span",{className:"row-main",children:[r.name||r.id||"",r.description?" - "+r.description:"",r.detail?" - "+r.detail:""]},h))}),c.jsx("h3",{children:"应用"}),c.jsx(ol,{empty:"暂无应用",items:i.apps.map((r,h)=>c.jsxs("span",{className:"row-main",children:[r.name||r.id||"",r.description?" - "+r.description:""]},h))}),c.jsx("h3",{children:"MCP 服务"}),c.jsx(ol,{empty:"暂无 MCP 服务",items:i.mcpServers.map((r,h)=>{const f=r.detail||r.details||r.state||"";return c.jsxs("span",{className:"row-main",children:[r.name||r.id||""," | 状态 ",r.status||"",f?" | "+f:""]},h)})})]})}function Gg(){const i=Lt(),[r,h]=d.useState("");return c.jsxs(Ft,{title:"MCP 操作",children:[c.jsxs(At,{children:[c.jsx("button",{onClick:i.reloadMcp,children:"重载配置"}),c.jsx("span",{className:"muted",children:i.mcpMsg})]}),c.jsxs(At,{children:[c.jsx("input",{type:"text",placeholder:"MCP 服务名",value:r,onChange:f=>h(f.target.value)}),c.jsx("button",{onClick:()=>void i.mcpOauth(r),children:"发起 OAuth 登录"}),c.jsx("span",{className:"muted",children:i.mcpOauthMsg})]}),i.mcpOauthBox?c.jsx("pre",{children:i.mcpOauthBox}):null]})}const Vh=160;function Qg(i){return i?i%86400===0?"(每天)":i%3600===0?"(每小时)":i%60===0?"(每 "+i/60+" 分钟)":"":""}function Xg({detail:i}){const r=Lt(),h=f=>{if(f){r.showDetail(f);try{window.location.hash="#view-tasks"}catch{}}};return c.jsxs("div",{className:"card",children:[c.jsxs("h2",{children:["自动化详情:",i.name]}),c.jsxs("div",{className:"muted",children:["状态 ",i.status,i.due?"|到期":""," | 每 ",i.interval_seconds," 秒 | 下次 ",Le(i.next_run_at),"| 上次执行 ",Le(i.last_run_at)," | 创建人 ",i.created_by?pe(i.created_by):""," | 创建于 ",Le(i.created_at),i.cancelled_at?" | 已取消于 "+Le(i.cancelled_at):"",i.heartbeat_binding?" | 心跳绑定:已绑定":""]}),c.jsx("div",{className:"muted",style:{whiteSpace:"pre-wrap",marginTop:8},children:i.prompt||""}),i.repo_url?c.jsxs("div",{className:"muted",children:["仓库 ",i.repo_url]}):null,c.jsxs(At,{children:[c.jsx("button",{onClick:r.closeAutomationDetail,children:"关闭详情"}),c.jsxs("span",{className:"muted",children:["编辑请在下面列表里点“编辑”。",r.autoDetailMsg]})]}),c.jsxs("div",{className:"data-group",children:[c.jsxs("b",{children:["最近产出(",(i.runs||[]).length,")"]}),c.jsx(ol,{empty:"(无)",items:(i.runs||[]).map((f,y)=>c.jsxs("span",{className:"row-main",children:[f.status,f.created_at?"|"+Le(f.created_at):"",f.task_id?c.jsxs(c.Fragment,{children:[" | 任务 ",c.jsx("button",{onClick:()=>h(f.task_id),children:pe(f.task_id)})]}):"",f.detail?"|"+String(f.detail).slice(0,120):""]},y))})]}),i.runs_note?c.jsx("div",{className:"muted",children:i.runs_note}):null]})}function Lg(){const i=Lt(),[r,h]=d.useState(""),[f,y]=d.useState(""),[C,M]=d.useState(""),[S,_]=d.useState(""),[O,Y]=d.useState(""),[x,H]=d.useState(""),[L,tt]=d.useState(""),[dt,J]=d.useState(""),[gt,_t]=d.useState(""),[Dt,Q]=d.useState({}),ft=D=>{Y(D.id),H(D.name||""),tt(D.prompt||""),J(D.interval_seconds?String(D.interval_seconds):""),_t(D.repo_url||"")};return c.jsxs(c.Fragment,{children:[c.jsx("input",{placeholder:"自动化名称(同项目内启用中的名称唯一)",style:{width:"100%"},value:r,onChange:D=>h(D.target.value)}),c.jsx("textarea",{placeholder:"定时任务内容(创建只登记定义与到期状态;执行由后续执行器经 claim 抢单后建任务,不另起调度器)",value:f,onChange:D=>y(D.target.value)}),c.jsxs(At,{children:[c.jsx("input",{placeholder:"间隔秒数(默认 86400,最小 60)",style:{width:"40%"},value:C,onChange:D=>M(D.target.value)}),c.jsx("input",{placeholder:"仓库(可选)",style:{width:"40%"},value:S,onChange:D=>_(D.target.value)}),c.jsx("button",{className:"primary",onClick:async()=>{await i.createAutomation(r,f,C,S),h(""),y(""),M(""),_("")},children:"创建自动化"}),c.jsx("button",{onClick:i.refreshAutomations,children:"刷新自动化"}),c.jsx("span",{className:"muted",children:i.autoMsg})]}),c.jsx(ol,{empty:"(暂无自动化)",items:i.automations.map(D=>{const X=D.prompt||"",et=!!Dt[D.id],xt=X.length>Vh,Mt=X?et||!xt?X:X.slice(0,Vh)+"…":"(无内容)";return c.jsxs("div",{children:[c.jsxs("div",{className:"row-main",children:["[",D.status,D.due?"|到期":"",D.run_once?"|一次性":"","] ",D.name,"|每 ",D.interval_seconds," 秒",Qg(D.interval_seconds),"|上次 ",Le(D.last_run_at)||"未运行","|下次 ",Le(D.next_run_at),c.jsx("button",{onClick:()=>void i.loadAutomationDetail(D.id),children:"详情"}),D.status==="active"?c.jsxs(c.Fragment,{children:[" ",c.jsx("button",{onClick:()=>ft(D),children:"编辑"})," ",c.jsx("button",{onClick:()=>i.cancelAutomation(D.id),children:"取消"})]}):null]}),D.repo_url?c.jsxs("div",{className:"muted",children:["仓库:",D.repo_url]}):null,D.status==="cancelled"&&D.cancelled_at?c.jsxs("div",{className:"muted",children:["已取消于 ",Le(D.cancelled_at)]}):null,c.jsxs("div",{className:"muted",style:{whiteSpace:"pre-wrap",overflowWrap:"anywhere"},children:[Mt,xt?c.jsx("button",{onClick:()=>Q(mt=>({...mt,[D.id]:!mt[D.id]})),children:et?"收起":"展开"}):null]}),O===D.id?c.jsxs("div",{children:[c.jsx("input",{placeholder:"自动化名称",style:{width:"100%"},value:x,onChange:mt=>H(mt.target.value)}),c.jsx("textarea",{placeholder:"定时任务内容",value:L,onChange:mt=>tt(mt.target.value)}),c.jsxs(At,{children:[c.jsx("input",{placeholder:"间隔秒数(最小 60,留空保持原值)",style:{width:"40%"},value:dt,onChange:mt=>J(mt.target.value)}),c.jsx("input",{placeholder:"仓库(可选,清空则清除)",style:{width:"40%"},value:gt,onChange:mt=>_t(mt.target.value)}),c.jsx("button",{className:"primary",onClick:async()=>{const mt=i.automations.find(B=>B.id===O)||D;await i.updateAutomation(mt,x,L,dt,gt)&&Y("")},children:"保存"}),c.jsx("button",{onClick:()=>Y(""),children:"取消"})]})]}):null]},D.id)})}),i.autoDetailMsg&&!i.autoDetail?c.jsx("div",{className:"muted",children:i.autoDetailMsg}):null,i.autoDetail?c.jsx(Xg,{detail:i.autoDetail},i.autoDetail.id):null]})}function Vg(){var y,C,M,S;const i=Lt(),r=i.triage,h=_=>{if(_){i.showDetail(_);try{window.location.hash="#view-tasks"}catch{}}},f=_=>{if(_){i.loadAutomationDetail(_);try{window.location.hash="#view-automation"}catch{}}};return i.triageError?c.jsx("div",{className:"muted",children:i.triageError}):r?c.jsxs(c.Fragment,{children:[c.jsxs("div",{className:"data-group",children:[c.jsxs("b",{children:["到期自动化(",((y=r.counts)==null?void 0:y.automations_due)||0,")"]}),c.jsx(ol,{empty:"(无)",items:(r.automations_due||[]).map((_,O)=>c.jsx("span",{className:"row-main",children:_.id?c.jsxs("button",{onClick:()=>f(_.id),children:[_.name,"|每 ",_.interval_seconds," 秒"]}):c.jsxs(c.Fragment,{children:[_.name,"|每 ",_.interval_seconds," 秒"]})},O))})]}),c.jsxs("div",{className:"data-group",children:[c.jsxs("b",{children:["失败/中断任务(",((C=r.counts)==null?void 0:C.failed_tasks)||0,")"]}),c.jsx(ol,{empty:"(无)",items:(r.failed_tasks||[]).map((_,O)=>c.jsx("span",{className:"row-main",children:c.jsxs("button",{onClick:()=>h(_.id),children:[pe(_.id),"|",_.status,"|",String(_.prompt||"").slice(0,80)]})},O))})]}),c.jsxs("div",{className:"data-group",children:[c.jsxs("b",{children:["待处理后续消息(",((M=r.counts)==null?void 0:M.pending_followups)||0,")"]}),c.jsx(ol,{empty:"(无)",items:(r.pending_followups||[]).map((_,O)=>c.jsx("span",{className:"row-main",children:c.jsxs("button",{onClick:()=>h(_.task_id),children:["任务 ",pe(_.task_id),"|",String(_.body||"").slice(0,80)]})},O))})]}),c.jsxs("div",{className:"data-group",children:[c.jsxs("b",{children:["最近自动化产出(",((S=r.counts)==null?void 0:S.recent_runs)||0,")"]}),c.jsx(ol,{empty:"(无)",items:(r.recent_runs||[]).map((_,O)=>c.jsxs("span",{className:"row-main",children:[pe(_.automation_id),"|",_.status,_.task_id?c.jsxs(c.Fragment,{children:["|任务 ",c.jsx("button",{onClick:()=>h(_.task_id),children:pe(_.task_id)})]}):"",_.task_status?"|"+_.task_status:""]},O))})]})]}):c.jsx("div",{className:"muted",children:"(先选项目)"})}function Zg(){return c.jsx(Ft,{id:"nav-automations",title:"自动化",children:c.jsx(Lg,{})})}function Kg(){const i=Lt();return c.jsxs(Ft,{id:"nav-triage",title:"分诊收件箱",children:[c.jsx(At,{children:c.jsx("button",{onClick:i.refreshTriage,children:"刷新分诊"})}),c.jsx(Vg,{})]})}const ho=["pending","in_progress","blocked","completed","cancelled"],wc={pending:"待办",in_progress:"进行中",blocked:"受阻",completed:"已完成",cancelled:"已取消"},Jg=["low","medium","high"],Zh={low:"低",medium:"中",high:"高"};function Fg(i){return i?new Date(i*1e3).toLocaleString():""}function Wg(){const i=Lt(),[r,h]=d.useState([]),[f,y]=d.useState({}),[C,M]=d.useState(0),[S,_]=d.useState(""),[O,Y]=d.useState(""),[x,H]=d.useState(""),[L,tt]=d.useState(""),[dt,J]=d.useState(""),[gt,_t]=d.useState(""),[Dt,Q]=d.useState("medium"),[ft,D]=d.useState(""),[X,et]=d.useState(""),xt=d.useCallback(async()=>{if(!i.projectId){h([]),y({}),M(0);return}try{let B=F.projectTodos(i.projectId);const nt=new URLSearchParams;x&&nt.set("status",x),L.trim()&&nt.set("q",L.trim());const rt=nt.toString();rt&&(B+="?"+rt);const G=await W(B);h(G.todos||[]),y(G.counts||{}),M(G.total||0),Y("")}catch(B){Y("待办加载失败:"+((B==null?void 0:B.message)||String(B)))}},[i.projectId,x,L]);d.useEffect(()=>{xt()},[xt]);const Mt=async()=>{if(!i.projectId){_("先选项目");return}if(!dt.trim()){_("标题必填");return}_("");try{const B={title:dt.trim(),description:gt,priority:Dt};ft.trim()&&(B.task_id=ft.trim()),X&&(B.due_at=Math.floor(new Date(X).getTime()/1e3)),await W(F.projectTodos(i.projectId),"POST",B),J(""),_t(""),D(""),et(""),_("已创建"),await xt()}catch(B){_("创建失败:"+((B==null?void 0:B.message)||String(B)))}},mt=async(B,nt)=>{if(i.projectId)try{await W(F.projectTodo(i.projectId,B.id),"PATCH",nt),await xt()}catch(rt){Y("待办更新失败:"+((rt==null?void 0:rt.message)||String(rt)))}},Nt=async B=>{if(i.projectId&&window.confirm("删除待办“"+B.title+"”?删除后不可恢复(取消请改状态为已取消)。"))try{await W(F.projectTodo(i.projectId,B.id),"DELETE"),await xt()}catch(nt){Y("待办删除失败:"+((nt==null?void 0:nt.message)||String(nt)))}};return c.jsxs(Ft,{id:"nav-todos",title:"项目待办",children:[c.jsx("div",{className:"muted",children:i.projectId?"共 "+C+" 项 | "+ho.map(B=>wc[B]+" "+(f[B]||0)).join(" | "):"先选项目"}),O?c.jsx("div",{className:"err",children:O}):null,c.jsxs(At,{children:[c.jsxs("select",{"aria-label":"状态筛选",value:x,onChange:B=>H(B.target.value),children:[c.jsx("option",{value:"",children:"状态:全部"}),ho.map(B=>c.jsx("option",{value:B,children:wc[B]},B))]}),c.jsx("input",{type:"text",placeholder:"搜索标题/说明",value:L,onChange:B=>tt(B.target.value)}),c.jsx("button",{onClick:()=>void xt(),children:"刷新"})]}),c.jsx("input",{type:"text",placeholder:"待办标题(必填)",style:{width:"100%"},value:dt,onChange:B=>J(B.target.value)}),c.jsx("textarea",{placeholder:"说明(可选)",value:gt,onChange:B=>_t(B.target.value)}),c.jsxs(At,{children:[c.jsx("select",{"aria-label":"优先级",value:Dt,onChange:B=>Q(B.target.value),children:Jg.map(B=>c.jsxs("option",{value:B,children:["优先级:",Zh[B]]},B))}),c.jsx("input",{type:"text",placeholder:"关联任务 id(可选)",value:ft,onChange:B=>D(B.target.value)}),c.jsx("input",{type:"datetime-local","aria-label":"截止时间",value:X,onChange:B=>et(B.target.value)}),c.jsx("button",{className:"primary",onClick:()=>void Mt(),children:"新建待办"}),c.jsx("span",{className:"muted",children:S})]}),c.jsx(ol,{empty:i.projectId?"(暂无待办)":"(先选项目)",items:r.map(B=>c.jsxs("span",{className:"row-main",children:[c.jsx("b",{children:B.title})," | ",c.jsx("span",{className:"st-"+B.status,children:wc[B.status]||B.status})," | 优先级 "+(Zh[B.priority]||B.priority),B.task_id?" | 任务 "+pe(B.task_id):"",B.due_at?" | 截止 "+Fg(B.due_at):"",B.completed_at?" | 完成于 "+new Date(B.completed_at*1e3).toLocaleString():"",B.description?c.jsxs(c.Fragment,{children:[c.jsx("br",{}),c.jsx("span",{className:"muted",children:B.description})]}):null,c.jsx("br",{}),c.jsx("select",{"aria-label":"改状态",value:B.status,onChange:nt=>void mt(B,{status:nt.target.value}),children:ho.map(nt=>c.jsx("option",{value:nt,children:wc[nt]},nt))})," ",c.jsx("button",{onClick:()=>void mt(B,{status:"completed"}),children:"完成"})," ",c.jsx("button",{onClick:()=>void Nt(B),children:"删除"})]},B.id))}),c.jsx("div",{className:"muted",children:"状态与 Muse 待办同款五态;完成时服务端记完成时间,改回其它状态时清掉;已取消是软关闭,删除才是硬删除。"})]})}const So=[["tasks","任务工作台","任务"],["todos","项目待办","任务"],["resources","环境与 Workers","管理"],["automation","技能与自动化","管理"],["admin","系统与 Key","管理"]],$g=Object.fromEntries(So.map(([i,r])=>[i,r]));function Kh(){const i=window.location.hash.replace("#view-","");return So.some(([r])=>r===i)?i:"tasks"}function Ig({view:i,setView:r}){return c.jsxs("aside",{className:"shell-side",children:[c.jsx("div",{className:"shell-brand",children:"Codex 命令台"}),c.jsx("nav",{className:"shell-nav","aria-label":"导航",children:["任务","管理"].map(h=>c.jsxs("div",{className:"shell-nav-group",children:[c.jsx("div",{className:"shell-nav-group-label",children:h}),So.filter(([,,f])=>f===h).map(([f,y])=>c.jsx("button",{type:"button",className:i===f?"active":"",onClick:()=>r(f),children:y},f))]},h))}),c.jsx("div",{id:"nav-projects",className:"shell-project-nav",children:c.jsx(wg,{})}),c.jsxs("div",{className:"shell-stats",children:[c.jsx("div",{className:"shell-stats-label",children:"概览"}),c.jsx(og,{})]})]})}function Pg(){const i=Lt();return c.jsx("span",{className:i.keySaved?"dash-status ok":"dash-status off",children:i.keySaved?"● 已连接":"○ 未连接"})}function tp(){const i=rg();ig(S=>i.refreshAll(S),1e4),cg(()=>i.refreshSlow(),6e4);const[r,h]=d.useState(()=>Kh()),[f,y]=d.useState(!0),C=S=>{h(S);try{window.history.replaceState(null,"","#view-"+S)}catch{}};d.useEffect(()=>{const S=()=>h(Kh());return window.addEventListener("hashchange",S),()=>window.removeEventListener("hashchange",S)},[]);const M=()=>{C("tasks"),y(!0),setTimeout(()=>{var S;return(S=document.getElementById("sec-dispatch"))==null?void 0:S.scrollIntoView({behavior:"smooth"})},0)};return c.jsx(Fh.Provider,{value:i,children:c.jsxs("div",{className:"shell",children:[c.jsx(Ig,{view:r,setView:C}),c.jsxs("div",{className:"shell-main",children:[c.jsxs("header",{className:"shell-header",children:[c.jsx("h1",{children:$g[r]}),c.jsx(Pg,{}),c.jsx("span",{className:"muted shell-hide-sm",children:i.healthText}),c.jsx("span",{className:"muted shell-hide-sm",children:i.quotaText}),c.jsxs("span",{className:"muted shell-stamp",children:["自动刷新 · 每 10 秒 · 已刷新 ",i.refreshStamp]})]}),r==="tasks"?c.jsxs("div",{className:"workspace",children:[c.jsxs("section",{className:"workspace-rail","aria-label":"任务列表",children:[c.jsx("h2",{children:"任务列表"}),c.jsx(mg,{onNew:M})]}),c.jsxs("section",{className:"workspace-center",children:[c.jsx("div",{className:"composer-toggle",children:c.jsx("button",{type:"button",onClick:()=>y(!f),children:f?"收起新建任务":"新建任务"})}),f?c.jsx("section",{id:"sec-dispatch",children:c.jsx(fg,{})}):null,c.jsxs("section",{className:"card workspace-conv",children:[c.jsx("h2",{children:"对话"}),c.jsx(Sg,{})]})]}),c.jsx("section",{className:"workspace-inspector","aria-label":"任务详情",children:c.jsx(zg,{})})]}):c.jsx("main",{className:"page",children:r==="todos"?c.jsx(c.Fragment,{children:c.jsx(Wg,{})}):r==="resources"?c.jsxs(c.Fragment,{children:[c.jsx(kg,{}),c.jsx(Hg,{}),c.jsx(Bg,{}),c.jsx(qg,{})]}):r==="automation"?c.jsxs(c.Fragment,{children:[c.jsx(Yg,{}),c.jsx(Gg,{}),c.jsx(Zg,{}),c.jsx(Kg,{})]}):c.jsxs(c.Fragment,{children:[c.jsx(Dg,{}),c.jsx(Rg,{})]})})]})]})})}Jy.createRoot(document.getElementById("root")).render(c.jsx(Yy.StrictMode,{children:c.jsx(tp,{})}));
+