refactor: archive GUI clients; keep minimal TUI with hooks/API
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>
This commit is contained in:
@@ -0,0 +1,92 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user