feat(ui): finish the workspace-sessions plan — tool filter, session search, explicit Resume

- Sidebar: a tool filter (shown once workspaces of two tools exist, kept in
  agenthub.sidebar.projects → tool; reordering under it keeps the hidden
  workspaces' places), and search that also finds a workspace by one of its
  session names, showing only the matching sessions.
- Workspace home: the repository it belongs to (root, main branch, worktree
  count) under the title, and each recent's cost.
- Transcript page: an explicit Resume for a Claude session started in a
  workspace's own folder (SessionResumeButton). It warns that a resume re-reads
  the whole transcript, re-reads the live panes, then starts in the first free
  pane and opens it; a live session offers Open pane instead.
This commit is contained in:
2026-10-02 01:35:34 +07:00
parent d0535af626
commit 5b269698f6
15 changed files with 597 additions and 36 deletions
+37 -2
View File
@@ -22,8 +22,8 @@ import {
type PermissionMode,
type RunHandle,
} from "../../lib/api";
import type { Lane, Session } from "../../lib/types";
import { timeAgo } from "../../lib/format";
import type { Lane, Project, Session } from "../../lib/types";
import { fmtCost, timeAgo } from "../../lib/format";
import { useAiTools } from "../../hooks/useAiTools";
import { useProjectTreePrefs } from "../../lib/projectTreePrefs";
import { ModelPicker, PromptEditor } from "../run/RunSetup";
@@ -137,6 +137,22 @@ function HomeBody({
};
}, [lane.id]);
// The repository this workspace belongs to (Settings → Projects used to
// show this): read once per visit, from the server's cached project list.
const [project, setProject] = useState<Project | null>(null);
useEffect(() => {
let live = true;
api.projects
.list()
.then(({ projects }) => {
if (live) setProject(projects.find((p) => p.workspaces.includes(lane.id)) ?? null);
})
.catch(() => undefined);
return () => {
live = false;
};
}, [lane.id]);
// Suggestions are read when the folder form opens, not on every visit.
useEffect(() => {
if (!folderOpen) return;
@@ -268,6 +284,20 @@ function HomeBody({
<MoreHorizontal className="h-5 w-5" />
</button>
</div>
{project?.root && (
<p
data-testid="workspace-home-facts"
className="mt-1 flex flex-wrap gap-x-3 font-mono text-[11px] text-fg-muted"
>
<span title={t("home.repoRoot")}>{project.root}</span>
{project.worktrees.find((w) => w.main)?.branch && (
<span title={t("home.mainBranch")}>
{project.worktrees.find((w) => w.main)?.branch}
</span>
)}
<span>{t("home.worktreeCount", { count: project.worktrees.length })}</span>
</p>
)}
<div className="card mt-5 p-3">
<PromptEditor
@@ -340,6 +370,11 @@ function HomeBody({
<span className="min-w-0 flex-1 truncate text-sm text-fg-primary">
{s.name || s.id.slice(0, 8)}
</span>
{!!s.cost && s.cost > 0 && (
<span className="shrink-0 text-xs tabular-nums text-fg-muted">
{fmtCost(s.cost)}
</span>
)}
<span className="shrink-0 text-xs text-fg-muted">
{timeAgo(s.last_activity ?? s.started_at)}
</span>