Files
codex-app-server/cloud/console-react/src/sections/TaskListSection.tsx
T
host df8c64bd64 feat(chv): 架构/可维护性/前端只读侧推进——生产实验分离、配置片段化、VM 只读展示
架构:WarmPool 从 cloud/chv/pool.py 移到 experimental_pool.py,
生产残留清理彻底分文件到 cloud/chv/orphans.py(facade 直接读
registry 文件清理,不构造池、不碰 prewarm/acquire/release);
cloud/chv 与 cloud/sandbox 不再导出 WarmPool,旧路径一律
ImportError(CHV 未合并、无生产用户,兼容保留已无保留理由);
worker 改走 orphans facade,不再持有 _warm_pool;#215 对齐从
接口形状推进到 REPLACEMENT_POINTS 替换清单(R1/R2 可机械替换、
R3 永不替换,#215 模块当前不存在由测试钉住)。

可维护性:build_chv_config 拆成 base/ssh/network/quota/disk/
timeout 六片段(组装只做 dict 合并,键冲突即报错;38 字段集
与取值不变,单项非法报错文案逐字一致);ADR-12 模块职责图
与禁 import 清单(AST 钉住);cloud/chv/ 公开 API 全量类型
标注、无 Any 逃逸,并修正三处旧标注错误;死代码扫一遍
(删实验池已无调用的 _quarantine 委托与未用 _LOGGER)。

前端(只读侧,不做控制按钮):chv.ts 新增纯逻辑谓词与文案
(执行器精确匹配、附件守卫、VM 状态文案、列表徽标);列表
行新增 VM 已报告死亡徽标;详情对全部 CHV 任务显示 VM 行
(已报告死亡/未报告死亡,不猜 live);WorkersSection 与成本
卡沿用已有数据,不新开池管理/成本页;服务端未新增第二个
生命周期字段(持久化事实只有三项,chv_vm_dead 已是完整派生),
只把派生抽成 _chv_vm_dead_from_row 并补 goal 回落形态测试。
2026-10-31 06:55:26 +00:00

95 lines
5.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useConsole } from "../context";
import { shortId } from "../api/client";
import { firstChar, durationOf, tokenOf } from "../api/taskMeta";
import { chvAvailability, chvQueueHint, chvVmBadge } from "../api/chv";
// 任务列表行 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,
// 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
// blue-grey background (#ddf4ff), not a bubble.
export default function TaskListSection({ onNew }: { onNew: () => void }) {
const ctx = useConsole();
const chvAvail = chvAvailability(ctx.workers, ctx.workersSummary, ctx.workersError);
return (
<div>
<div className="search-box">
<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" aria-hidden="true">
<circle cx="11" cy="11" r="7" /><path d="M21 21l-4.3-4.3" />
</svg>
<input type="text" placeholder="搜索" aria-label="搜索任务" value={ctx.searchText} onChange={(e) => ctx.setSearchText(e.target.value)} />
</div>
<div className="bar">
<select aria-label="项目" value={ctx.projectId} onChange={(e) => ctx.selectProject(e.target.value)}>
{ctx.projects.length === 0 ? <option value="">(先加载项目)</option> : null}
{ctx.projects.map((p) => <option key={p.id} value={p.id}>{p.name} ({shortId(p.id)})</option>)}
</select>
</div>
<div className="list-section-head">
<span>主要聊天</span>
<button type="button" aria-label="派发新任务" title="派发新任务" onClick={onNew}>+</button>
</div>
<div className="bar">
<select aria-label="状态筛选" value={ctx.statusFilter} onChange={(e) => ctx.setStatusFilter(e.target.value)}>
<option value="">状态:全部</option>
{["queued", "running", "completed", "failed", "interrupted", "merged", "merge-failed"].map((s) => <option key={s} value={s}>{s}</option>)}
</select>
<select aria-label="执行器筛选" value={ctx.executorFilter} onChange={(e) => ctx.setExecutorFilter(e.target.value)}>
<option value="">执行器:全部</option><option value="local">local</option><option value="docker">docker</option><option value="cloud">cloud</option><option value="cloud-hypervisor">cloud-hypervisor(实验)</option>
</select>
</div>
<div className="bar">
<button onClick={ctx.refreshTasks}>刷新</button>
<span className="muted">{ctx.taskCountText}</span>
</div>
<div className="bar">
<button disabled={ctx.taskPage <= 0} onClick={() => ctx.goToTaskPage(ctx.taskPage - 1)}>上一页</button>
<span className="muted">第 {ctx.taskPage + 1} / {ctx.taskTotalPages} 页(每页 20 条,最新在前)</span>
<button disabled={ctx.taskPage + 1 >= ctx.taskTotalPages} onClick={() => ctx.goToTaskPage(ctx.taskPage + 1)}>下一页</button>
</div>
{ctx.taskError ? <div className="err">{ctx.taskError}</div>
: !ctx.projectId ? <div className="muted">先选项目</div>
: ctx.filteredTasks.length === 0 ? <div className="muted">{ctx.taskTotal ? "没有符合筛选条件的任务" : "该项目暂无任务"}</div>
: (
<ul className="issue-list">
{ctx.filteredTasks.map((t) => {
const [dur, durTitle] = durationOf(t);
const [tok, tokTitle] = tokenOf(t);
const fc = firstChar(t);
return (
<li key={t.id} className={"issue-item" + (ctx.currentTask?.id === t.id ? " selected" : "")} onClick={() => ctx.showDetail(t.id)}>
<span className={"issue-state st-" + t.status} aria-hidden="true" />
<span className="issue-first" title={"任务首字:" + fc} aria-hidden="true">{fc}</span>
<span style={{ flex: 1, minWidth: 0 }}>
<div className="issue-title">
{t.plan ? "[计划] " : ""}{t.forked_from ? "[分支] " : ""}{t.coordinator_parts ? "[协调] " : ""}{t.parent_task_id ? "[协调子任务] " : ""}{String(t.prompt || shortId(t.id)).slice(0, 60)}
</div>
<div className="issue-meta">
{shortId(t.id)} · <span className={"st-" + t.status}>{t.status}</span> · {t.executor || ""}{t.updated_at ? " · " + new Date(t.updated_at * 1000).toLocaleString() : ""}
{" · "}<span title={durTitle}>耗时 {dur}</span>
{" · "}<span title={tokTitle}>{tok}</span>
</div>
{(t.status === "failed" || t.status === "interrupted" || t.status === "merge-failed") && t.error_text ? (
<div className="issue-meta" title={t.error_text}>{/503|Service Unavailable/.test(t.error_text) ? "上游 503:模型服务暂时不可用" : t.error_text.replace(/\s+/g, " ").slice(0, 60)}</div>
) : null}
{chvQueueHint(t.executor, t.status, chvAvail) ? (
<div className="issue-meta" title={chvQueueHint(t.executor, t.status, chvAvail)}>{chvQueueHint(t.executor, t.status, chvAvail)}</div>
) : null}
{chvVmBadge(t) ? (
<div className="issue-meta" title="后端只读字段 chv_vm_dead(worker 终局写回的 VM 死亡原因派生);无徽标时不代表 VM 活着">{chvVmBadge(t)}</div>
) : null}
</span>
</li>
);
})}
</ul>
)}
</div>
);
}