fix(kanban): make scratch cleanup explicit in dashboard (#63123)

This commit is contained in:
Teknium 2026-07-12 04:15:59 -07:00 committed by GitHub
parent 837077dfae
commit f67aae3230
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 84 additions and 14 deletions

View file

@ -1072,6 +1072,7 @@
error ? h("div", { className: "text-xs text-destructive px-2" }, error) : null,
h(BoardColumns, {
board: filteredBoard,
boardMeta: boardList.find(function (item) { return item.slug === board; }) || null,
laneByProfile,
selectedIds,
failedIds,
@ -2384,6 +2385,7 @@
return h(Column, {
key: col.name,
column: col,
boardMeta: props.boardMeta,
laneByProfile: props.laneByProfile,
selectedIds: props.selectedIds,
failedIds: props.failedIds,
@ -2504,6 +2506,8 @@
showCreate ? h(InlineCreate, {
columnName: props.column.name,
allTasks: props.allTasks,
defaultWorkspaceKind: (props.boardMeta && props.boardMeta.default_workspace_kind) || "scratch",
defaultWorkspacePath: (props.boardMeta && props.boardMeta.default_workdir) || "",
onSubmit: function (body) {
props.onCreate(body).then(function () { setShowCreate(false); });
},
@ -2745,12 +2749,13 @@
const [priority, setPriority] = useState(0);
const [parent, setParent] = useState("");
const [skills, setSkills] = useState("");
// Workspace controls. `scratch` (default) ignores path; `worktree` optionally
// takes a path (dispatcher derives one from the assignee profile otherwise);
// `dir` requires a path. Backend enforces the rule — we only hide/show the
// input here to save vertical space in the common `scratch` case.
const [workspaceKind, setWorkspaceKind] = useState("scratch");
const [workspacePath, setWorkspacePath] = useState("");
// A board with a configured workdir defaults to a persistent workspace:
// worktree for git repositories, dir for ordinary directories. Boards
// without one keep scratch for disposable research and ops tasks.
const defaultWorkspaceKind = props.defaultWorkspaceKind || "scratch";
const defaultWorkspacePath = props.defaultWorkspacePath || "";
const [workspaceKind, setWorkspaceKind] = useState(defaultWorkspaceKind);
const [workspacePath, setWorkspacePath] = useState(defaultWorkspacePath);
// Goal-mode: when on, the dispatched worker runs the Ralph-style /goal
// loop — a judge re-checks the card after each turn and the worker keeps
// going in the same session until done, or the turn budget runs out
@ -2793,15 +2798,15 @@
}
props.onSubmit(body);
setTitle(""); setAssignee(""); setPriority(0); setParent(""); setSkills("");
setWorkspaceKind("scratch"); setWorkspacePath("");
setWorkspaceKind(defaultWorkspaceKind); setWorkspacePath(defaultWorkspacePath);
setGoalMode(false); setGoalMaxTurns("");
};
const showPathInput = workspaceKind !== "scratch";
const pathPlaceholder = workspaceKind === "dir"
? tx(t, "workspacePathDir", "workspace path (required, e.g. ~/projects/my-app)")
? tx(t, "workspacePathDir", "workspace path (required without a board workdir)")
: tx(t, "workspacePathOptional",
"workspace path (optional, derived from assignee if blank)");
"repository path (optional when the board has a workdir)");
return h("div", { className: "hermes-kanban-inline-create" },
h("textarea", {
@ -2877,12 +2882,15 @@
h("div", { className: "flex gap-2" },
h(Select, Object.assign({
value: workspaceKind,
title: "scratch: isolated temp dir (default). worktree: git worktree on the assignee profile. dir: exact path (required below).",
className: "h-7 text-xs w-28",
title: "Choose whether task files are temporary or preserved after completion.",
className: "h-7 text-xs flex-1",
}, selectChangeHandler(setWorkspaceKind)),
h(SelectOption, { value: "scratch" }, "scratch"),
h(SelectOption, { value: "worktree" }, "worktree"),
h(SelectOption, { value: "dir" }, "dir"),
h(SelectOption, { value: "scratch" },
tx(t, "workspaceScratch", "Temporary — deleted on completion")),
h(SelectOption, { value: "worktree" },
tx(t, "workspaceWorktree", "Git worktree — preserved")),
h(SelectOption, { value: "dir" },
tx(t, "workspaceDir", "Directory — preserved")),
),
showPathInput ? h(Input, {
value: workspacePath,
@ -2891,6 +2899,11 @@
className: "h-7 text-xs flex-1",
}) : null,
),
workspaceKind === "scratch" ? h("div", {
className: "text-xs text-destructive",
role: "alert",
}, tx(t, "workspaceScratchWarning",
"This workspace and any files left in it are deleted when the task completes.")) : null,
h(Select, Object.assign({
value: parent,
className: "h-7 text-xs",

View file

@ -2010,6 +2010,17 @@ def _board_counts(slug: str) -> dict[str, int]:
return {}
def _default_workspace_kind(board: dict[str, Any]) -> str:
"""Recommend a non-destructive task workspace from board metadata."""
workdir = str(board.get("default_workdir") or "").strip()
if not workdir:
return "scratch"
try:
return "worktree" if kanban_db._git_toplevel(Path(workdir)) else "dir"
except (OSError, ValueError):
return "dir"
@router.get("/boards")
def list_boards(include_archived: bool = Query(False)):
"""Return every board on disk with task counts and the active slug."""
@ -2019,6 +2030,7 @@ def list_boards(include_archived: bool = Query(False)):
b["is_current"] = (b["slug"] == current)
b["counts"] = _board_counts(b["slug"])
b["total"] = sum(b["counts"].values())
b["default_workspace_kind"] = _default_workspace_kind(b)
return {"boards": boards, "current": current}