feat(lanes): add Lanes section (skills install, housekeeping) to Settings (F4)
This commit is contained in:
@@ -132,6 +132,7 @@ const SETTINGS_SECTIONS: {
|
||||
}[] = [
|
||||
{ id: "pricing", labelKey: "pricing.title", Icon: DollarSign },
|
||||
{ id: "hooks", labelKey: "hooks.title", Icon: Plug },
|
||||
{ id: "lanes", labelKey: "lanesSection.title", Icon: GitBranch },
|
||||
{ id: "claude-home", labelKey: "claudeHome.title", Icon: FolderOpen },
|
||||
{ id: "import", labelKey: "import.title", fallback: "Import", Icon: History },
|
||||
{
|
||||
@@ -468,6 +469,7 @@ export function Settings() {
|
||||
}, []);
|
||||
const [abandonHours, setAbandonHours] = useState("24");
|
||||
const [purgeDays, setPurgeDays] = useState("90");
|
||||
const [gcDryRun, setGcDryRun] = useState(false);
|
||||
const [claudeHome, setClaudeHomeState] = useState("");
|
||||
const [claudeHomeInput, setClaudeHomeInput] = useState("");
|
||||
const [claudeHomeSaving, setClaudeHomeSaving] = useState(false);
|
||||
@@ -727,6 +729,18 @@ export function Settings() {
|
||||
return res.ok ? t("hooks.success") : t("hooks.failed");
|
||||
});
|
||||
|
||||
const handleSkillsInstall = () =>
|
||||
runAction("skills-install", async () => {
|
||||
const res = await api.settings.skillsInstall();
|
||||
return t("lanesSection.installSkillSuccess", { path: res.path });
|
||||
});
|
||||
|
||||
const handleGc = () =>
|
||||
runAction("gc", async () => {
|
||||
const res = await api.settings.gc({ dryRun: gcDryRun });
|
||||
return t("lanesSection.gcResult", { reaped: res.reaped.length, capped: res.capped.length });
|
||||
});
|
||||
|
||||
const handleResetPricing = () =>
|
||||
runAction("reset-pricing", async () => {
|
||||
const res = await api.settings.resetPricing();
|
||||
@@ -1357,6 +1371,58 @@ export function Settings() {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* ─── LANES ─── */}
|
||||
<section id="lanes" className="scroll-mt-24">
|
||||
<h3 className="text-sm font-medium text-fg-secondary flex items-center gap-2 mb-1">
|
||||
<GitBranch className="w-4 h-4 text-fg-muted" />
|
||||
{t("lanesSection.title")}
|
||||
</h3>
|
||||
<p className="text-xs text-fg-muted mb-4">{t("lanesSection.description")}</p>
|
||||
|
||||
<div className="card p-5 space-y-4">
|
||||
<div className="flex items-center justify-between flex-wrap gap-3">
|
||||
<span className="text-xs text-fg-secondary">{t("lanesSection.installSkill")}</span>
|
||||
<button
|
||||
onClick={handleSkillsInstall}
|
||||
disabled={actionLoading !== null}
|
||||
className="btn-ghost text-xs disabled:opacity-50"
|
||||
>
|
||||
{actionLoading === "skills-install" ? (
|
||||
<RefreshCw className="w-3.5 h-3.5 animate-spin" />
|
||||
) : (
|
||||
<RotateCcw className="w-3.5 h-3.5" />
|
||||
)}
|
||||
{t("lanesSection.installSkill")}
|
||||
</button>
|
||||
</div>
|
||||
{actionBanner(["skills-install"])}
|
||||
|
||||
<div className="flex items-center justify-between flex-wrap gap-3">
|
||||
<label className="flex items-center gap-2 text-xs text-fg-secondary">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={gcDryRun}
|
||||
onChange={(e) => setGcDryRun(e.target.checked)}
|
||||
/>
|
||||
{t("lanesSection.dryRun")}
|
||||
</label>
|
||||
<button
|
||||
onClick={handleGc}
|
||||
disabled={actionLoading !== null}
|
||||
className="btn-ghost text-xs disabled:opacity-50"
|
||||
>
|
||||
{actionLoading === "gc" ? (
|
||||
<RefreshCw className="w-3.5 h-3.5 animate-spin" />
|
||||
) : (
|
||||
<RotateCcw className="w-3.5 h-3.5" />
|
||||
)}
|
||||
{t("lanesSection.runHousekeeping")}
|
||||
</button>
|
||||
</div>
|
||||
{actionBanner(["gc"])}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* ─── CLAUDE HOME ─── */}
|
||||
<section id="claude-home" className="scroll-mt-24">
|
||||
<h3 className="text-sm font-medium text-fg-secondary flex items-center gap-2 mb-1">
|
||||
|
||||
Reference in New Issue
Block a user