mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-20 15:33:54 +00:00
fix(kanban): make scratch cleanup explicit in dashboard (#63123)
This commit is contained in:
parent
837077dfae
commit
f67aae3230
3 changed files with 84 additions and 14 deletions
41
plugins/kanban/dashboard/dist/index.js
vendored
41
plugins/kanban/dashboard/dist/index.js
vendored
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ from __future__ import annotations
|
|||
|
||||
import importlib.util
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
|
@ -114,6 +115,50 @@ def test_create_task_appears_on_board(client):
|
|||
assert "researcher" in data["assignees"]
|
||||
|
||||
|
||||
def test_board_list_recommends_persistent_workspace_for_configured_workdir(
|
||||
client, tmp_path
|
||||
):
|
||||
"""Board metadata should tell the UI which safe task default to use."""
|
||||
repo = tmp_path / "repo"
|
||||
repo.mkdir()
|
||||
subprocess.run(["git", "init", "-q", str(repo)], check=True)
|
||||
kb.write_board_metadata("default", default_workdir=str(repo))
|
||||
|
||||
plain_dir = tmp_path / "notes"
|
||||
plain_dir.mkdir()
|
||||
kb.create_board("notes", default_workdir=str(plain_dir))
|
||||
kb.create_board("disposable")
|
||||
|
||||
response = client.get("/api/plugins/kanban/boards")
|
||||
|
||||
assert response.status_code == 200
|
||||
boards = {board["slug"]: board for board in response.json()["boards"]}
|
||||
assert boards["default"]["default_workspace_kind"] == "worktree"
|
||||
assert boards["notes"]["default_workspace_kind"] == "dir"
|
||||
assert boards["disposable"]["default_workspace_kind"] == "scratch"
|
||||
|
||||
|
||||
def test_dashboard_workspace_picker_explains_persistence_contract():
|
||||
"""Task creation must make scratch deletion visible without a hover."""
|
||||
bundle = (
|
||||
Path(__file__).resolve().parents[2]
|
||||
/ "plugins"
|
||||
/ "kanban"
|
||||
/ "dashboard"
|
||||
/ "dist"
|
||||
/ "index.js"
|
||||
).read_text(encoding="utf-8")
|
||||
|
||||
assert "Temporary — deleted on completion" in bundle
|
||||
assert "Git worktree — preserved" in bundle
|
||||
assert "Directory — preserved" in bundle
|
||||
assert "defaultWorkspacePath: (props.boardMeta && props.boardMeta.default_workdir) || \"\"" in bundle
|
||||
assert (
|
||||
"This workspace and any files left in it are deleted when the task completes."
|
||||
in bundle
|
||||
)
|
||||
|
||||
|
||||
def test_scheduled_tasks_have_their_own_column_not_todo(client):
|
||||
"""Scheduled/time-delay tasks must not be silently bucketed into todo."""
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue