fix: subpath navigation

This commit is contained in:
Catriel Müller
2026-06-01 12:09:06 -03:00
parent cf85e0d51f
commit 7de2613daa
6 changed files with 58 additions and 24 deletions
@@ -14,7 +14,7 @@ import {
type RecentProjectItem,
type ProjectQuery,
} from "../../client"
import { type Path } from "../../shared/navigation"
import { strip, type Path } from "../../shared/navigation"
import { clean, friendly } from "../../shared/utils"
import {
projectStatus,
@@ -111,6 +111,7 @@ function Glyph(props: { name: "projects" | "settings" | "profile" }) {
export function AppSidebar(props: Props) {
const loc = useLocation()
const params = createMemo(() => new URLSearchParams(loc.search))
const route = createMemo(() => strip(loc.pathname))
const discoverable = () => shouldDiscover(params())
const fallback = () => base(params())
const [url, setUrl] = createSignal(fallback())
@@ -126,14 +127,14 @@ export function AppSidebar(props: Props) {
// project currently rendered by ProjectConsoleRoute — it owns unread tracking for its terminals
const activeProject = createMemo(() => {
const match = loc.pathname.match(/^\/projects\/([^/]+)/)
const match = route().match(/^\/projects\/([^/]+)/)
return match ? decodeURIComponent(match[1]) : undefined
})
const settings = () => `/settings${tail(params())}`
const selected = (item: RecentProjectItem) =>
loc.pathname.startsWith(`/projects/${encodeURIComponent(item.id)}/`) ||
loc.pathname === `/projects/${encodeURIComponent(item.id)}`
route().startsWith(`/projects/${encodeURIComponent(item.id)}/`) ||
route() === `/projects/${encodeURIComponent(item.id)}`
const nav = () => [
{ href: "/projects", label: "Projects", name: "projects", path: "/projects" },
@@ -18,6 +18,7 @@ import {
type TuiPatch,
} from "../client"
import { ConfigContext, type Task } from "./config"
import { strip } from "../shared/navigation"
import { clean, errMsg } from "../shared/utils"
import { useLocation, useParams } from "@solidjs/router"
@@ -45,7 +46,8 @@ export function ConfigProvider(props: { children?: JSX.Element }) {
const discoverable = () => shouldDiscover(search())
const fallback = () => base(search())
const [url, setUrl] = createSignal(fallback())
const scope = createMemo<Scope>(() => (loc.pathname.startsWith("/projects/") ? "project" : "global"))
const route = createMemo(() => strip(loc.pathname))
const scope = createMemo<Scope>(() => (route().startsWith("/projects/") ? "project" : "global"))
const [saving, setSaving] = createSignal<string | undefined>()
const [failure, setFailure] = createSignal<string | undefined>()
const needs = createMemo(() => scope() === "project")
@@ -8,6 +8,7 @@ import { CountTag, Tag } from "@kilocode/kilo-web-ui/tag"
import { CustomSelect, type SelectOption } from "../../components/CustomSelect"
import { SearchField } from "../../components/SearchField"
import { useConfig } from "../../context/config"
import { settings } from "../../shared/navigation"
import { toolCapabilities, toolName } from "../../shared/utils"
import { ConfigPage, SourceBadge } from "./ConfigPage"
import { ActionSelect, label as actionLabel, tone as actionTone } from "./PermissionsRoute"
@@ -30,13 +31,6 @@ const modes = [
{ value: "all", label: "Both" },
] satisfies SelectOption<"primary" | "subagent" | "all">[]
function base(input: string) {
const index = input.indexOf("/settings")
if (index > 0) return `${input.slice(0, index)}/settings`
if (input.startsWith("/config")) return "/config"
return "/settings"
}
function desc(item: AgentItem) {
return item.description ?? "No description available."
}
@@ -51,7 +45,7 @@ function useAgentLinks() {
const nav = useNavigate()
const href = (id?: string) => {
const suffix = id ? `/${encodeURIComponent(id)}` : ""
return `${base(loc.pathname)}/agents${suffix}${loc.search}`
return `${settings(loc.pathname)}/agents${suffix}${loc.search}`
}
return { href, nav }
}
@@ -3,6 +3,7 @@ import { createMemo, For } from "solid-js"
import { Icon } from "@kilocode/kilo-web-ui/icon"
import { configNav, type ConfigGroup, type ConfigNode } from "./sections"
import { friendly } from "../../shared/utils"
import { settings, strip } from "../../shared/navigation"
import { useConfig } from "../../context/config"
function repo(input: string) {
@@ -14,20 +15,17 @@ export function ConfigSidebar() {
const loc = useLocation()
const params = useParams()
const ctx = useConfig()
const project = createMemo(() => loc.pathname.startsWith("/projects/"))
const route = createMemo(() => strip(loc.pathname))
const project = createMemo(() => route().startsWith("/projects/"))
const scope = createMemo(() => {
if (!project()) return "Global"
const dir = ctx.query()?.dir
if (dir) return friendly(repo(dir))
return friendly(decodeURIComponent(params.project ?? "Project"))
})
const base = createMemo(() => {
const index = loc.pathname.indexOf("/settings")
if (index > 0) return `${loc.pathname.slice(0, index)}/settings`
return "/settings"
})
const base = createMemo(() => settings(route()))
const active = createMemo(() => {
const rest = loc.pathname.slice(base().length)
const rest = route().slice(base().length)
if (rest === "/models") return "/models/default"
return rest || "/"
})
@@ -0,0 +1,19 @@
import { expect, test } from "bun:test"
import { path, settings, strip } from "./navigation"
test("strips the deployed console base from route paths", () => {
expect(strip("/console/projects/demo/settings/agents", "/console")).toBe("/projects/demo/settings/agents")
expect(strip("/console", "/console")).toBe("/")
expect(strip("/consoleish/projects", "/console")).toBe("/consoleish/projects")
})
test("classifies routes after stripping the console base", () => {
expect(path("/console/projects/demo/settings/agents", "/console")).toBe("/project")
expect(path("/console/settings/agents", "/console")).toBe("/settings")
expect(path("/console/profile", "/console")).toBe("/profile")
})
test("builds settings roots without preserving the console base", () => {
expect(settings("/console/projects/demo/settings/agents", "/console")).toBe("/projects/demo/settings")
expect(settings("/console/settings/agents", "/console")).toBe("/settings")
})
+24 -4
View File
@@ -1,8 +1,28 @@
export type Path = "/projects" | "/project" | "/profile" | "/settings"
export function path(input: string): Path {
if (input === "/profile") return "/profile"
if (input.startsWith("/settings") || input.startsWith("/config")) return "/settings"
if (input.startsWith("/projects/")) return "/project"
function base() {
return (import.meta.env?.BASE_URL ?? "/").replace(/\/$/, "")
}
export function strip(input: string, prefix = base()) {
if (!prefix || prefix === "/") return input
if (input === prefix) return "/"
if (input.startsWith(`${prefix}/`)) return input.slice(prefix.length)
return input
}
export function settings(input: string, prefix = base()) {
const route = strip(input, prefix)
const index = route.indexOf("/settings")
if (index > 0) return `${route.slice(0, index)}/settings`
if (route.startsWith("/config")) return "/config"
return "/settings"
}
export function path(input: string, prefix = base()): Path {
const route = strip(input, prefix)
if (route === "/profile") return "/profile"
if (route.startsWith("/settings") || route.startsWith("/config")) return "/settings"
if (route.startsWith("/projects/")) return "/project"
return "/projects"
}