Move Qt/Tauri/full Web SPA under archive/, ship hermit as CLI/TUI plus libcstcore and cst-webui/1 HTTP+SSE (turn lifecycle hooks, capabilities). Co-authored-by: Cursor <cursoragent@cursor.com>
93 lines
2.9 KiB
TypeScript
93 lines
2.9 KiB
TypeScript
import { useMemo, useState } from "react";
|
|
import type { BashRun } from "@/lib/bridge";
|
|
|
|
type Filter = "recent" | "running" | "done" | "all";
|
|
|
|
type Props = {
|
|
runs: BashRun[];
|
|
onClose: () => void;
|
|
};
|
|
|
|
function fmtDur(r: BashRun): string {
|
|
const end = r.ended_at ?? Date.now();
|
|
const ms = Math.max(0, end - r.started_at);
|
|
if (ms < 1000) return `${ms}ms`;
|
|
if (ms < 60_000) return `${Math.round(ms / 1000)}秒`;
|
|
return `${Math.floor(ms / 60_000)}分${Math.round((ms % 60_000) / 1000)}秒`;
|
|
}
|
|
|
|
export function BashPanel({ runs, onClose }: Props) {
|
|
const [filter, setFilter] = useState<Filter>("recent");
|
|
const [openId, setOpenId] = useState<string | null>(null);
|
|
|
|
const list = useMemo(() => {
|
|
const sorted = [...runs].sort((a, b) => b.started_at - a.started_at);
|
|
if (filter === "all") return sorted;
|
|
if (filter === "running") return sorted.filter((r) => r.status === "running");
|
|
if (filter === "done")
|
|
return sorted.filter((r) => r.status !== "running");
|
|
return sorted.slice(0, 20);
|
|
}, [runs, filter]);
|
|
|
|
const running = runs.filter((r) => r.status === "running").length;
|
|
|
|
return (
|
|
<div className="work-panel bash-panel">
|
|
<div className="work-panel-head">
|
|
<strong>后台 Bash · {running} 运行中</strong>
|
|
<button type="button" onClick={onClose}>
|
|
关闭
|
|
</button>
|
|
</div>
|
|
<div className="bash-filters">
|
|
{(
|
|
[
|
|
["recent", "最近"],
|
|
["running", "进行中"],
|
|
["done", "已完成"],
|
|
["all", "全部"],
|
|
] as const
|
|
).map(([k, label]) => (
|
|
<button
|
|
key={k}
|
|
type="button"
|
|
className={filter === k ? "active" : ""}
|
|
onClick={() => setFilter(k)}
|
|
>
|
|
{label}
|
|
</button>
|
|
))}
|
|
</div>
|
|
{list.length === 0 ? (
|
|
<div className="work-empty">还没有 shell 工具记录。</div>
|
|
) : (
|
|
<ul className="bash-list">
|
|
{list.map((r) => (
|
|
<li key={r.id} className={`bash-row ${r.status}`}>
|
|
<button
|
|
type="button"
|
|
className="bash-row-main"
|
|
onClick={() => setOpenId(openId === r.id ? null : r.id)}
|
|
>
|
|
<span className="bash-status" aria-hidden>
|
|
{r.status === "ok"
|
|
? "✓"
|
|
: r.status === "running"
|
|
? "…"
|
|
: "✕"}
|
|
</span>
|
|
<code className="bash-cmd">$ {r.command}</code>
|
|
<span className="bash-dur">{fmtDur(r)}</span>
|
|
<span className="bash-chev">{openId === r.id ? "▾" : "▸"}</span>
|
|
</button>
|
|
{openId === r.id && r.output ? (
|
|
<pre className="bash-out">{r.output}</pre>
|
|
) : null}
|
|
</li>
|
|
))}
|
|
</ul>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|