Update workspace and app changes

This commit is contained in:
Mohamed Boudra
2026-03-07 19:09:00 +07:00
parent 1d1c7058f1
commit 7d76da3249
40 changed files with 2693 additions and 976 deletions
@@ -68,6 +68,12 @@ describe("workspace route parsing", () => {
it("builds host new-agent routes", () => {
expect(buildHostNewAgentRoute("local")).toBe("/h/local/new-agent");
expect(
buildHostNewAgentRoute("local", {
workingDir: "/tmp/repo",
worktreeMode: "create",
})
).toBe("/h/local/new-agent?workingDir=%2Ftmp%2Frepo&worktreeMode=create");
});
it("builds workspace routes with open intent query", () => {
+39 -2
View File
@@ -399,12 +399,46 @@ export function buildHostAgentsRoute(serverId: string): string {
return `${base}/agents`;
}
export function buildHostNewAgentRoute(serverId: string): string {
export function buildHostNewAgentRoute(
serverId: string,
options?: {
workingDir?: string;
provider?: string;
modeId?: string;
model?: string;
thinkingOptionId?: string;
worktreeMode?: "create" | "attach";
}
): string {
const base = buildHostRootRoute(serverId);
if (base === "/") {
return "/";
}
return `${base}/new-agent`;
const searchParams = new URLSearchParams();
const entries: Array<[string, string | undefined]> = [
["workingDir", options?.workingDir],
["provider", options?.provider],
["modeId", options?.modeId],
["model", options?.model],
["thinkingOptionId", options?.thinkingOptionId],
["worktreeMode", options?.worktreeMode],
];
for (const [key, value] of entries) {
const normalized = trimNonEmpty(value);
if (normalized) {
searchParams.set(key, normalized);
}
}
const search = searchParams.toString();
return search ? `${base}/new-agent?${search}` : `${base}/new-agent`;
}
export function buildHostOpenProjectRoute(serverId: string): string {
const base = buildHostRootRoute(serverId);
if (base === "/") {
return "/";
}
return `${base}/open-project`;
}
export function buildHostSettingsRoute(serverId: string): string {
@@ -435,6 +469,9 @@ export function mapPathnameToServer(
if (suffix.startsWith("new-agent")) {
return `${base}/new-agent`;
}
if (suffix.startsWith("open-project")) {
return `${base}/open-project`;
}
const workspaceRoute = parseHostWorkspaceRouteFromPathname(pathname);
if (workspaceRoute) {
const workspacePath = buildHostWorkspaceRoute(
@@ -0,0 +1,103 @@
import { describe, expect, it } from 'vitest'
import { buildSidebarProjectRowModel } from './sidebar-project-row-model'
import type {
SidebarProjectEntry,
SidebarWorkspaceEntry,
} from '@/hooks/use-sidebar-workspaces-list'
function workspace(overrides: Partial<SidebarWorkspaceEntry> = {}): SidebarWorkspaceEntry {
return {
workspaceKey: 'srv:/repo',
serverId: 'srv',
workspaceId: '/repo',
workspaceKind: 'directory',
name: 'paseo',
activityAt: null,
statusBucket: 'done',
...overrides,
}
}
function project(overrides: Partial<SidebarProjectEntry> = {}): SidebarProjectEntry {
return {
projectKey: 'project-1',
projectName: 'paseo',
projectKind: 'git',
iconWorkingDir: '/repo',
statusBucket: 'done',
activeCount: 0,
totalWorkspaces: 1,
latestActivityAt: null,
workspaces: [workspace()],
...overrides,
}
}
describe('buildSidebarProjectRowModel', () => {
it('flattens non-git projects with one workspace into a direct workspace row model', () => {
const flattenedWorkspace = workspace({
workspaceId: '/repo/non-git',
workspaceKind: 'directory',
statusBucket: 'running',
})
const result = buildSidebarProjectRowModel({
project: project({
projectKind: 'non_git',
workspaces: [flattenedWorkspace],
}),
collapsed: false,
})
expect(result).toEqual({
interaction: 'navigate',
chevron: 'disclosure',
trailingAction: 'none',
flattenedWorkspace,
selected: false,
})
})
it('marks flattened non-git project rows as selected when their workspace is active', () => {
const flattenedWorkspace = workspace({
serverId: 'srv-2',
workspaceId: '/repo/non-git',
})
const result = buildSidebarProjectRowModel({
project: project({
projectKind: 'non_git',
workspaces: [flattenedWorkspace],
}),
collapsed: false,
serverId: 'srv-2',
activeWorkspaceSelection: {
serverId: 'srv-2',
workspaceId: '/repo/non-git',
},
})
expect(result.selected).toBe(true)
})
it('keeps git projects as expandable sections with a new worktree action', () => {
const result = buildSidebarProjectRowModel({
project: project({
projectKind: 'git',
workspaces: [
workspace({ workspaceId: '/repo/main', workspaceKind: 'local_checkout' }),
workspace({ workspaceId: '/repo/feature', workspaceKind: 'worktree' }),
],
}),
collapsed: true,
})
expect(result).toEqual({
interaction: 'toggle',
chevron: 'expand',
trailingAction: 'new_worktree',
flattenedWorkspace: null,
selected: false,
})
})
})
@@ -0,0 +1,47 @@
import type {
SidebarProjectEntry,
SidebarWorkspaceEntry,
} from '@/hooks/use-sidebar-workspaces-list'
export interface SidebarProjectRowModel {
interaction: 'toggle' | 'navigate'
chevron: 'expand' | 'collapse' | 'disclosure'
trailingAction: 'new_worktree' | 'none'
flattenedWorkspace: SidebarWorkspaceEntry | null
selected: boolean
}
export function buildSidebarProjectRowModel(input: {
project: SidebarProjectEntry
collapsed: boolean
serverId?: string | null
activeWorkspaceSelection?: { serverId: string; workspaceId: string } | null
}): SidebarProjectRowModel {
const flattenedWorkspace =
input.project.projectKind === 'non_git' && input.project.workspaces.length === 1
? input.project.workspaces[0] ?? null
: null
const selected =
flattenedWorkspace !== null &&
Boolean(input.serverId) &&
input.activeWorkspaceSelection?.serverId === input.serverId &&
input.activeWorkspaceSelection?.workspaceId === flattenedWorkspace.workspaceId
if (flattenedWorkspace) {
return {
interaction: 'navigate',
chevron: 'disclosure',
trailingAction: 'none',
flattenedWorkspace,
selected,
}
}
return {
interaction: 'toggle',
chevron: input.collapsed ? 'expand' : 'collapse',
trailingAction: input.project.projectKind === 'git' ? 'new_worktree' : 'none',
flattenedWorkspace: null,
selected: false,
}
}
@@ -8,6 +8,7 @@ function workspace(serverId: string, cwd: string): SidebarWorkspaceEntry {
workspaceKey: `${serverId}:${cwd}`,
serverId,
workspaceId: cwd,
workspaceKind: "local_checkout",
name: cwd,
activityAt: null,
statusBucket: "done",
@@ -18,6 +19,7 @@ function project(projectKey: string, workspaces: SidebarWorkspaceEntry[]): Sideb
return {
projectKey,
projectName: projectKey,
projectKind: "git",
iconWorkingDir: workspaces[0]?.workspaceId ?? "",
statusBucket: "done",
activeCount: 0,