Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions cmd/sgai/webapp/src/lib/workspace-sort.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
const naturalWorkspaceLabelCollator = new Intl.Collator(undefined, {
numeric: true,
sensitivity: "base",
});

export function sortByVisibleLabel<T>(
items: readonly T[],
getLabel: (item: T) => string,
getKey: (item: T) => string,
): T[] {
return items
.map((item) => ({ item, label: getLabel(item), key: getKey(item) }))
.sort((left, right) => {
const labelComparison = naturalWorkspaceLabelCollator.compare(left.label, right.label);
if (labelComparison !== 0) {
return labelComparison;
}

return naturalWorkspaceLabelCollator.compare(left.key, right.key);
})
.map(({ item }) => item);
}
24 changes: 1 addition & 23 deletions cmd/sgai/webapp/src/pages/Dashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,15 +35,11 @@ import {
isSameWorkspace,
resolveWorkspaceByName,
} from "@/lib/workspace-identity";
import { sortByVisibleLabel } from "@/lib/workspace-sort";

type ForkEntry = NonNullable<ApiWorkspaceEntry["forks"]>[number];
type WorkspaceLabelSource = Pick<ApiWorkspaceEntry, "name" | "dir"> & Partial<Pick<ApiWorkspaceEntry, "title" | "computedTitle">>;

const naturalWorkspaceLabelCollator = new Intl.Collator(undefined, {
numeric: true,
sensitivity: "base",
});

function workspaceToForkEntry(ws: ApiWorkspaceEntry): ForkEntry {
return {
name: ws.name,
Expand Down Expand Up @@ -72,24 +68,6 @@ function getOrphanPinnedForkDisplayLabel(
return `${rootLabel}/${forkLabel}`;
}

function sortByVisibleLabel<T>(
items: readonly T[],
getLabel: (item: T) => string,
getKey: (item: T) => string,
): T[] {
return items
.map((item) => ({ item, label: getLabel(item), key: getKey(item) }))
.sort((left, right) => {
const labelComparison = naturalWorkspaceLabelCollator.compare(left.label, right.label);
if (labelComparison !== 0) {
return labelComparison;
}

return naturalWorkspaceLabelCollator.compare(left.key, right.key);
})
.map(({ item }) => item);
}

function getWorkspaceLabelSource(
workspace: WorkspaceLabelSource,
workspaceLookup: Map<string, ApiWorkspaceEntry>,
Expand Down
20 changes: 19 additions & 1 deletion cmd/sgai/webapp/src/pages/tabs/ForksTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
getWorkspaceDisplayLabel,
resolveWorkspaceByName,
} from "@/lib/workspace-identity";
import { sortByVisibleLabel } from "@/lib/workspace-sort";
import { useAdhocRun } from "@/hooks/useAdhocRun";
import type { ApiForkEntry, ApiActionEntry, ApiWorkspaceEntry } from "@/types";

Expand Down Expand Up @@ -385,6 +386,23 @@ export function ForksTab({ workspaceName, actions, actionConfigError, onActionCl
return map;
}, [allWorkspaces]);

const sortedForks = useMemo(() => {
const rawForks = workspace?.forks ?? [];
return sortByVisibleLabel(
rawForks,
(fork) => {
const forkWs = forkWorkspaceLookup.get(fork.dir);
const labelSource = {
...(forkWs ?? fork),
title: fork.title || forkWs?.title || "",
computedTitle: fork.computedTitle || forkWs?.computedTitle || "",
};
return getWorkspaceDisplayLabel(labelSource, workspaceNameDisambiguators);
},
(fork) => fork.dir,
);
}, [workspace?.forks, forkWorkspaceLookup, workspaceNameDisambiguators]);

if (fetchStatus === "fetching" && !workspace) return <ForksTabSkeleton />;

if (!workspace) {
Expand All @@ -398,7 +416,7 @@ export function ForksTab({ workspaceName, actions, actionConfigError, onActionCl
return null;
}

const forks = workspace.forks ?? [];
const forks = sortedForks;
const hasActionBar = Boolean((actions && actions.length > 0) || actionConfigError?.trim());
const workspaceLabel = getWorkspaceDisplayLabel(workspace, workspaceNameDisambiguators);

Expand Down
41 changes: 41 additions & 0 deletions cmd/sgai/webapp/src/pages/tabs/__tests__/ForksTab.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -473,6 +473,47 @@ describe("ForksTab", () => {
expect(screen.getByRole("button", { name: /^Delete$/ })).toBeTruthy();
});

it("renders forks sorted by visible label matching left-tree order", () => {
factoryState.workspaces = [
createMockWorkspace({
forks: [
{
name: "workspace-1-fork-c",
dir: "/path/to/workspace-1-fork-c",
running: false,
needsInput: false,
inProgress: false,
pinned: false,
title: "Charlie Fork",
},
{
name: "workspace-1-fork-a",
dir: "/path/to/workspace-1-fork-a",
running: false,
needsInput: false,
inProgress: false,
pinned: false,
title: "Alpha Fork",
},
{
name: "workspace-1-fork-b",
dir: "/path/to/workspace-1-fork-b",
running: false,
needsInput: false,
inProgress: false,
pinned: false,
title: "Bravo Fork",
},
],
}),
];

render(forksTabTestView());

const forkLabels = screen.getAllByText(/^(Alpha|Bravo|Charlie) Fork$/).map((el) => el.textContent);
expect(forkLabels).toEqual(["Alpha Fork", "Bravo Fork", "Charlie Fork"]);
});

it("hides running fork row actions when backend policy marks them hidden", async () => {
factoryState.workspaces = [
createMockWorkspace({
Expand Down