console: 重排版为侧边导航 + 三栏工作台 + 任务详情内页签
旧排版(顶栏页签 + 数据卡片置顶 + 长页堆叠 + 详情卡片堆在对话下)全部去掉,功能不变: - 左侧栏:导航按任务/管理分组,底部紧凑概览卡片始终可见;整页不再单页长滚动,各 pane 各自滚动 - 任务工作台:左任务列表轨(搜索/筛选/分页),中派发(可收起)+ 单列对话,右任务详情检查器 - 详情检查器:概览/协作/评审与审批/线程四个内页签,一次只显示一组 - 其余功能各成独立页面:项目与成员、环境与 Workers、技能与自动化、系统与 Key - 视图同步到 location.hash(#view-tasks 等) 构建:npm run build(tsc + vite singlefile)通过,产物 332130 字节;本地全量 cloud 1275 项全过(skipped 4)。
This commit is contained in:
1 parent
f0aba700e0
commit
44b080971e
4 files changed
+187
-148
No files matched your search
+86
-113
@@ -20,156 +20,129 @@ import SkillsToolsPanel from "./sections/SkillsToolsPanel";
|
||||
import AutomationsSection from "./sections/AutomationsSection";
|
||||
import TriageSection from "./sections/TriageSection";
|
||||
|
||||
// Layout: GitHub Primer style, data cards on top, then TAB pages
|
||||
// instead of one endless page. The old layout stacked 11 sections
|
||||
// (dispatch, tasks, conversation, projects, env, workers, skills,
|
||||
// automations, triage, admin, API Key, plus detail panels) on one
|
||||
// page; tabbing is what actually cuts the visible length, while
|
||||
// list pagination (see useTaskList) cuts the data fetched per page.
|
||||
// Tabs:
|
||||
// 工作台:派发 + 任务列表(分页)+ 对话 + 任务详情
|
||||
// 项目:项目 + 成员
|
||||
// 环境与 Workers:环境 + 环境上下文 + Workers + 健康
|
||||
// 技能与自动化:技能 + 技能工具 + 自动化 + 分诊
|
||||
// 管理与 Key:管理区 + API Key
|
||||
// Selected task survives tab switches: the store (useConsoleStore)
|
||||
// lives in App, above the tabs, so currentTask / detail / chat
|
||||
// state is not unmounted with a tab — switching back shows the same
|
||||
// task, and the 10 s refresh re-syncs its detail lists. The tab is
|
||||
// synced to location.hash (#tab-workbench etc.) so a reload or a
|
||||
// shared link lands on the same tab; unknown hashes fall back to
|
||||
// the workbench.
|
||||
type TabId = "workbench" | "projects" | "env" | "skills" | "admin";
|
||||
const TABS: Array<[TabId, string]> = [
|
||||
["workbench", "工作台"], ["projects", "项目"], ["env", "环境与 Workers"],
|
||||
["skills", "技能与自动化"], ["admin", "管理与 Key"],
|
||||
// Redesign (2026-10-30): the old layout is gone — no top tab bar, no
|
||||
// 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:
|
||||
// 左侧栏:导航(按工作流分组)+ 数据卡片(紧凑版,始终可见)
|
||||
// 任务工作台:左 = 任务列表轨(搜索/筛选/分页),中 = 派发 + 对话,
|
||||
// 右 = 任务详情检查器(概览/协作/评审与审批/线程四个内页签,
|
||||
// 见 TaskDetailPanel)
|
||||
// 其余功能各成独立页面(项目 / 资源 / 自动化 / 系统),从左侧栏
|
||||
// 切入,页面内只放该组功能,不再把 11 个模块堆在同一页。
|
||||
// Selected task survives view switches: the store lives in App,
|
||||
// above the views, so currentTask / detail / chat state is not
|
||||
// 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" | "projects" | "resources" | "automation" | "admin";
|
||||
const VIEWS: Array<[ViewId, string, string]> = [
|
||||
["tasks", "任务工作台", "任务"],
|
||||
["projects", "项目与成员", "管理"],
|
||||
["resources", "环境与 Workers", "管理"],
|
||||
["automation", "技能与自动化", "管理"],
|
||||
["admin", "系统与 Key", "管理"],
|
||||
];
|
||||
function tabFromHash(): TabId {
|
||||
const h = window.location.hash.replace("#tab-", "");
|
||||
return (TABS.some(([id]) => id === h) ? h : "workbench") as TabId;
|
||||
const VIEW_TITLES: Record<ViewId, string> = Object.fromEntries(
|
||||
VIEWS.map(([id, title]) => [id, title]),
|
||||
) as Record<ViewId, string>;
|
||||
function viewFromHash(): ViewId {
|
||||
const h = window.location.hash.replace("#view-", "");
|
||||
return (VIEWS.some(([id]) => id === h) ? h : "tasks") as ViewId;
|
||||
}
|
||||
|
||||
function TopBar({ tab, setTab }: { tab: TabId; setTab: (t: TabId) => void }) {
|
||||
function SideNav({ view, setView }: { view: ViewId; setView: (v: ViewId) => void }) {
|
||||
return (
|
||||
<header className="dash-topbar">
|
||||
<div className="dash-topbar-row">
|
||||
<span className="dash-brand">Codex 命令台</span>
|
||||
<TopStatus />
|
||||
<span className="muted dash-hide-sm"><TopHealth /></span>
|
||||
<span className="muted dash-hide-sm"><TopQuota /></span>
|
||||
<span className="muted dash-stamp"><TopStamp /></span>
|
||||
</div>
|
||||
<nav className="dash-nav tab-nav" aria-label="页面签">
|
||||
{TABS.map(([id, label]) => (
|
||||
<button key={id} type="button" className={tab === id ? "active" : ""} onClick={() => setTab(id)}>{label}</button>
|
||||
<aside className="shell-side">
|
||||
<div className="shell-brand">Codex 命令台</div>
|
||||
<nav className="shell-nav" aria-label="导航">
|
||||
{["任务", "管理"].map((group) => (
|
||||
<div key={group} className="shell-nav-group">
|
||||
<div className="shell-nav-group-label">{group}</div>
|
||||
{VIEWS.filter(([, , g]) => g === group).map(([id, label]) => (
|
||||
<button key={id} type="button" className={view === id ? "active" : ""} onClick={() => setView(id)}>{label}</button>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</nav>
|
||||
</header>
|
||||
<div className="shell-stats">
|
||||
<div className="shell-stats-label">概览</div>
|
||||
<DataCards />
|
||||
</div>
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
|
||||
// Tiny readers so the top bar re-renders from the store.
|
||||
function TopStatus() {
|
||||
const ctx = useConsole();
|
||||
return <span className={ctx.keySaved ? "dash-status ok" : "dash-status off"}>{ctx.keySaved ? "● 已连接" : "○ 未连接"}</span>;
|
||||
}
|
||||
function TopHealth() {
|
||||
return <>{useConsole().healthText}</>;
|
||||
}
|
||||
function TopQuota() {
|
||||
return <>{useConsole().quotaText}</>;
|
||||
}
|
||||
function TopStamp() {
|
||||
return <>自动刷新 · 每 10 秒 · 已刷新 {useConsole().refreshStamp}</>;
|
||||
}
|
||||
|
||||
export default function App() {
|
||||
const store = useConsoleStore();
|
||||
useAutoRefresh((selectFocused) => store.refreshAll(selectFocused), 10000);
|
||||
useIntervalMs(() => store.refreshSlow(), 60000);
|
||||
const [tab, setTabState] = useState<TabId>(() => tabFromHash());
|
||||
const setTab = (t: TabId) => {
|
||||
setTabState(t);
|
||||
try { window.history.replaceState(null, "", "#tab-" + t); } catch { /* noop */ }
|
||||
const [view, setViewState] = useState<ViewId>(() => viewFromHash());
|
||||
const [composerOpen, setComposerOpen] = useState(true);
|
||||
const setView = (v: ViewId) => {
|
||||
setViewState(v);
|
||||
try { window.history.replaceState(null, "", "#view-" + v); } catch { /* noop */ }
|
||||
};
|
||||
useEffect(() => {
|
||||
const onHash = () => setTabState(tabFromHash());
|
||||
const onHash = () => setViewState(viewFromHash());
|
||||
window.addEventListener("hashchange", onHash);
|
||||
return () => window.removeEventListener("hashchange", onHash);
|
||||
}, []);
|
||||
const scrollToDispatch = () => {
|
||||
setTab("workbench");
|
||||
setView("tasks");
|
||||
setComposerOpen(true);
|
||||
setTimeout(() => document.getElementById("sec-dispatch")?.scrollIntoView({ behavior: "smooth" }), 0);
|
||||
};
|
||||
|
||||
return (
|
||||
<ConsoleContext.Provider value={store}>
|
||||
<TopBar tab={tab} setTab={setTab} />
|
||||
<main className="dash-page dash-page-tabs">
|
||||
<DataCards />
|
||||
{tab === "workbench" ? (
|
||||
<div className="tab-content tab-workbench">
|
||||
<div className="dash-col dash-col-left">
|
||||
<section id="sec-tasks" className="dash-section card">
|
||||
<div className="shell">
|
||||
<SideNav view={view} setView={setView} />
|
||||
<div className="shell-main">
|
||||
<header className="shell-header">
|
||||
<h1>{VIEW_TITLES[view]}</h1>
|
||||
<TopStatus />
|
||||
<span className="muted shell-hide-sm">{store.healthText}</span>
|
||||
<span className="muted shell-hide-sm">{store.quotaText}</span>
|
||||
<span className="muted shell-stamp">自动刷新 · 每 10 秒 · 已刷新 {store.refreshStamp}</span>
|
||||
</header>
|
||||
{view === "tasks" ? (
|
||||
<div className="workspace">
|
||||
<section className="workspace-rail" aria-label="任务列表">
|
||||
<h2>任务列表</h2>
|
||||
<TaskListSection onNew={scrollToDispatch} />
|
||||
</section>
|
||||
</div>
|
||||
<div className="dash-col dash-col-mid">
|
||||
<section id="sec-dispatch" className="dash-section">
|
||||
<DispatchSection />
|
||||
</section>
|
||||
<section id="sec-conv" className="dash-section">
|
||||
<div className="card">
|
||||
<section className="workspace-center">
|
||||
<div className="composer-toggle">
|
||||
<button type="button" onClick={() => setComposerOpen(!composerOpen)}>
|
||||
{composerOpen ? "收起新建任务" : "新建任务"}
|
||||
</button>
|
||||
</div>
|
||||
{composerOpen ? <section id="sec-dispatch"><DispatchSection /></section> : null}
|
||||
<section className="card workspace-conv">
|
||||
<h2>对话</h2>
|
||||
<ConversationSection />
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
<section className="workspace-inspector" aria-label="任务详情">
|
||||
<TaskDetailPanel />
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
) : tab === "projects" ? (
|
||||
<div className="tab-content">
|
||||
<section id="sec-projects" className="dash-section">
|
||||
<ProjectsSection />
|
||||
<MembersPanel />
|
||||
</section>
|
||||
</div>
|
||||
) : tab === "env" ? (
|
||||
<div className="tab-content">
|
||||
<section id="sec-env" className="dash-section">
|
||||
<EnvSection />
|
||||
<EnvContextPanel />
|
||||
</section>
|
||||
<section id="sec-workers" className="dash-section">
|
||||
<WorkersSection />
|
||||
<HealthPanel />
|
||||
</section>
|
||||
</div>
|
||||
) : tab === "skills" ? (
|
||||
<div className="tab-content">
|
||||
<section id="sec-skills" className="dash-section">
|
||||
<SkillsSection />
|
||||
<SkillsToolsPanel />
|
||||
</section>
|
||||
<section id="sec-automations" className="dash-section">
|
||||
<AutomationsSection />
|
||||
</section>
|
||||
<section id="sec-triage" className="dash-section">
|
||||
<TriageSection />
|
||||
</section>
|
||||
</div>
|
||||
) : (
|
||||
<div className="tab-content">
|
||||
<section id="sec-admin" className="dash-section">
|
||||
<AdminSection />
|
||||
</section>
|
||||
<section id="sec-key" className="dash-section">
|
||||
<ApiKeySection />
|
||||
</section>
|
||||
</div>
|
||||
)}
|
||||
</main>
|
||||
) : (
|
||||
<main className="page">
|
||||
{view === "projects" ? (<><ProjectsSection /><MembersPanel /></>)
|
||||
: view === "resources" ? (<><EnvSection /><EnvContextPanel /><WorkersSection /><HealthPanel /></>)
|
||||
: view === "automation" ? (<><SkillsSection /><SkillsToolsPanel /><AutomationsSection /><TriageSection /></>)
|
||||
: (<><AdminSection /><ApiKeySection /></>)}
|
||||
</main>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</ConsoleContext.Provider>
|
||||
);
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import { useState } from "react";
|
||||
import { useConsole } from "../context";
|
||||
import { Card } from "../components/ui";
|
||||
import { shortId, fmt } from "../api/client";
|
||||
@@ -7,19 +8,25 @@ import DetailActions from "./detail/DetailActions";
|
||||
import ReviewPanel from "./detail/ReviewPanel";
|
||||
import { CoordPanel, AgentsPanel, ApprovalPanel, PeerPanel, ThreadPanel } from "./detail/DetailLists";
|
||||
|
||||
// Right detail column for the task view, card-style layout.
|
||||
// Everything structured lives here — never inside a chat bubble.
|
||||
// Layout only: data, actions and sub-sections are unchanged,
|
||||
// just grouped into cards instead of one <br>-separated block:
|
||||
// 概览(状态 + 耗时/token/成本三张指标卡)
|
||||
// 基本信息(键值网格)
|
||||
// prompt(单独一张卡,长 prompt 不再挤在信息行末尾)
|
||||
// 操作与结果
|
||||
// 协调子任务 / 子代理链路 / 评审队列 / 运行时审批队列 /
|
||||
// 给其他任务发消息 / 线程(各一张卡)
|
||||
// Task inspector (redesign 2026-10-30): the old detail stack —
|
||||
// 概览 + 基本信息 + prompt + 操作 + 协调 + 子代理 + 评审 +
|
||||
// 审批 + 任务间消息 + 线程, all stacked under the conversation —
|
||||
// is gone. The same panels now live in four inspector tabs so
|
||||
// only one group is visible at a time:
|
||||
// 概览:状态/耗时/token/成本 + 基本信息 + prompt + 操作与结果
|
||||
// 协作:协调子任务 / 子代理链路 / 给其他任务发消息
|
||||
// 评审与审批:评审队列 / 运行时审批队列
|
||||
// 线程:线程管理
|
||||
// Data, actions and sub-sections are unchanged; only grouping
|
||||
// and visibility changed.
|
||||
type DetailTab = "overview" | "collab" | "review" | "threads";
|
||||
const DETAIL_TABS: Array<[DetailTab, string]> = [
|
||||
["overview", "概览"], ["collab", "协作"], ["review", "评审与审批"], ["threads", "线程"],
|
||||
];
|
||||
export default function TaskDetailPanel() {
|
||||
const ctx = useConsole();
|
||||
const t = ctx.currentTask;
|
||||
const [tab, setTab] = useState<DetailTab>("overview");
|
||||
if (!t) return <Card id="nav-detail" title="任务详情"><div className="muted">点任务行查看详情</div></Card>;
|
||||
const [dur, durTitle] = durationOf(t);
|
||||
const [tok, tokTitle] = tokenOf(t);
|
||||
@@ -30,6 +37,12 @@ export default function TaskDetailPanel() {
|
||||
: t.verify_command ? "goal 模式(旧语义,验收命令)" : null;
|
||||
return (
|
||||
<div className="detail-stack" id="nav-detail">
|
||||
<nav className="detail-tabs" aria-label="任务详情页签">
|
||||
{DETAIL_TABS.map(([id, label]) => (
|
||||
<button key={id} type="button" className={tab === id ? "active" : ""} onClick={() => setTab(id)}>{label}</button>
|
||||
))}
|
||||
</nav>
|
||||
{tab === "overview" ? (<>
|
||||
<Card title="任务详情">
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 8, flexWrap: "wrap" }}>
|
||||
<span className="issue-first" title={"任务首字:" + fc} aria-hidden="true">{fc}</span>
|
||||
@@ -84,12 +97,16 @@ export default function TaskDetailPanel() {
|
||||
<pre>{ctx.detailBody || "(暂无 result/error 输出)"}</pre>
|
||||
{ctx.costBox ? <pre>{ctx.costBox}</pre> : null}
|
||||
</Card>
|
||||
</>) : tab === "collab" ? (<>
|
||||
<Card title="协调子任务"><CoordPanel /></Card>
|
||||
<Card title="子代理链路"><AgentsPanel /></Card>
|
||||
<Card title="给其他任务发消息"><PeerPanel /></Card>
|
||||
</>) : tab === "review" ? (<>
|
||||
<Card title="评审队列"><ReviewPanel /></Card>
|
||||
<Card title="运行时审批队列"><ApprovalPanel /></Card>
|
||||
<Card title="给其他任务发消息"><PeerPanel /></Card>
|
||||
</>) : (<>
|
||||
<Card title="线程"><ThreadPanel /></Card>
|
||||
</>)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -272,3 +272,52 @@ pre{background:var(--inset-bg);color:var(--text);border:1px solid var(--line);bo
|
||||
.bar > input[type="text"],.bar > input[type="password"],.bar > select{flex:1 1 100%}
|
||||
th,td{padding:8px 12px}
|
||||
}
|
||||
|
||||
/* Redesign shell (2026-10-30): replaces the old top-tab + stacked-page
|
||||
layout. Sidebar + workspace grid, each pane scrolls on its own;
|
||||
the page itself never scrolls as one long column. */
|
||||
.shell{display:grid;grid-template-columns:260px minmax(0,1fr);height:100vh;background:#fff}
|
||||
.shell-side{background:#f6f8fa;border-right:1px solid var(--line);display:flex;flex-direction:column;min-height:0;overflow-y:auto}
|
||||
.shell-brand{font-weight:600;font-size:16px;padding:16px 16px 12px}
|
||||
.shell-nav{display:flex;flex-direction:column;gap:16px;padding:0 12px 16px}
|
||||
.shell-nav-group-label{font-size:12px;font-weight:600;color:var(--muted);padding:0 8px 6px}
|
||||
.shell-nav-group{display:flex;flex-direction:column;gap:2px}
|
||||
.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}
|
||||
.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}
|
||||
.shell-stats .dash-card{padding:12px}
|
||||
.shell-stats .dash-card-value{font-size:18px}
|
||||
.shell-main{display:flex;flex-direction:column;min-width:0;min-height:0}
|
||||
.shell-header{display:flex;gap:14px;align-items:center;flex-wrap:wrap;padding:12px 20px;border-bottom:1px solid var(--line);background:#fff}
|
||||
.shell-header h1{font-size:16px;margin:0}
|
||||
.shell-header .dash-status{font-size:13px;font-weight:600}
|
||||
.shell-header .dash-status.ok{color:var(--success)}
|
||||
.shell-stamp{margin-left:auto}
|
||||
.workspace{display:grid;grid-template-columns:300px minmax(0,1fr) 360px;flex:1;min-height:0}
|
||||
.workspace-rail,.workspace-center,.workspace-inspector{overflow-y:auto;padding:16px;min-width:0}
|
||||
.workspace-rail{border-right:1px solid var(--line);background:#fff}
|
||||
.workspace-rail h2,.workspace-conv h2{font-size:14px;margin:0 0 12px;font-weight:600}
|
||||
.workspace-rail .issue-list{max-height:none}
|
||||
.workspace-center{background:#fff;display:flex;flex-direction:column;gap:16px}
|
||||
.composer-toggle{display:flex}
|
||||
.workspace-conv{flex:1}
|
||||
.workspace-inspector{border-left:1px solid var(--line);background:var(--subtle-bg)}
|
||||
.workspace-inspector .detail-stack{display:flex;flex-direction:column;gap:12px}
|
||||
.detail-tabs{display:flex;gap:4px;flex-wrap:wrap}
|
||||
.detail-tabs button{padding:6px 12px;font-size:13px;font-weight:600}
|
||||
.detail-tabs button.active{background:var(--text);color:#fff;border-color:var(--text)}
|
||||
.page{flex:1;overflow-y:auto;padding:20px;display:flex;flex-direction:column;gap:20px;max-width:1100px;width:100%}
|
||||
.page > *{min-width:0}
|
||||
@media (max-width:1100px){
|
||||
.shell{grid-template-columns:1fr;height:auto;min-height:100vh}
|
||||
.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-stats{display:none}
|
||||
.shell-hide-sm{display:none}
|
||||
.workspace{grid-template-columns:1fr}
|
||||
.workspace-rail,.workspace-inspector{border:0;border-bottom:1px solid var(--line)}
|
||||
}
|
||||
Reference in new issue
Block a user