mirror of
https://github.com/HKUDS/nanobot.git
synced 2026-08-31 00:03:01 +03:00
feat(tui): distinguish cross-workspace sessions
This commit is contained in:
@@ -62,4 +62,68 @@ describe("SessionMenu", () => {
|
|||||||
"Release checklist",
|
"Release checklist",
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("shows compact workspace names only when they distinguish sessions", async () => {
|
||||||
|
setup = await createTestRenderer({ width: 100, height: 18, screenMode: "alternate-screen" })
|
||||||
|
const menu = new SessionMenu(setup.renderer, {
|
||||||
|
text: "#FFFFFF",
|
||||||
|
muted: "#999999",
|
||||||
|
border: "#555555",
|
||||||
|
})
|
||||||
|
setup.renderer.root.add(menu.root)
|
||||||
|
const scoped = sessions.map((session) => ({
|
||||||
|
...session,
|
||||||
|
workspaceScope: {
|
||||||
|
project_path: "/work/nanobot",
|
||||||
|
project_name: "nanobot",
|
||||||
|
access_mode: "restricted" as const,
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
|
menu.open(scoped, "one", 6)
|
||||||
|
await setup.renderOnce()
|
||||||
|
expect(setup.captureCharFrame()).not.toContain("nanobot · Codex")
|
||||||
|
|
||||||
|
menu.open([
|
||||||
|
scoped[0]!,
|
||||||
|
{
|
||||||
|
...scoped[1]!,
|
||||||
|
workspaceScope: {
|
||||||
|
project_path: "C:\\work\\desktop",
|
||||||
|
project_name: "desktop",
|
||||||
|
access_mode: "restricted",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
], "one", 6)
|
||||||
|
await setup.renderOnce()
|
||||||
|
const frame = setup.captureCharFrame()
|
||||||
|
expect(frame).toContain("nanobot · Codex")
|
||||||
|
expect(frame).toContain("desktop")
|
||||||
|
|
||||||
|
menu.update("desktop", 6)
|
||||||
|
expect(menu.choose()?.chatId).toBe("two")
|
||||||
|
|
||||||
|
menu.open([
|
||||||
|
{
|
||||||
|
...scoped[0]!,
|
||||||
|
workspaceScope: {
|
||||||
|
project_path: "/work/frontend/nanobot",
|
||||||
|
project_name: "nanobot",
|
||||||
|
access_mode: "restricted",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
...scoped[1]!,
|
||||||
|
workspaceScope: {
|
||||||
|
project_path: "/work/backend/nanobot",
|
||||||
|
project_name: "nanobot",
|
||||||
|
access_mode: "restricted",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
], "one", 6)
|
||||||
|
await setup.renderOnce()
|
||||||
|
const duplicates = setup.captureCharFrame()
|
||||||
|
expect(duplicates).toContain("frontend/nanobot")
|
||||||
|
expect(duplicates).toContain("backend/nanobot")
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -25,6 +25,8 @@ function updatedLabel(value: string | null): string {
|
|||||||
export class SessionMenu {
|
export class SessionMenu {
|
||||||
readonly root: BoxRenderable
|
readonly root: BoxRenderable
|
||||||
private readonly picker: PickerMenu<SessionMenuRow>
|
private readonly picker: PickerMenu<SessionMenuRow>
|
||||||
|
private readonly workspaceLabels = new Map<string, string>()
|
||||||
|
private showWorkspaces = false
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
renderer: CliRenderer,
|
renderer: CliRenderer,
|
||||||
@@ -38,11 +40,14 @@ export class SessionMenu {
|
|||||||
session.modelPreset || "",
|
session.modelPreset || "",
|
||||||
session.preview,
|
session.preview,
|
||||||
session.chatId,
|
session.chatId,
|
||||||
|
session.workspaceScope?.project_name || "",
|
||||||
|
session.workspaceScope?.project_path || "",
|
||||||
].join(" "),
|
].join(" "),
|
||||||
render: (session) => {
|
render: (session) => {
|
||||||
const age = updatedLabel(session.updatedAt)
|
const age = updatedLabel(session.updatedAt)
|
||||||
const preview = session.preview.trim()
|
const preview = session.preview.trim()
|
||||||
const detail = [
|
const detail = [
|
||||||
|
this.showWorkspaces ? this.workspaceLabel(session) : "",
|
||||||
session.modelPreset,
|
session.modelPreset,
|
||||||
age,
|
age,
|
||||||
preview && preview !== sessionLabel(session) ? preview : "",
|
preview && preview !== sessionLabel(session) ? preview : "",
|
||||||
@@ -63,6 +68,7 @@ export class SessionMenu {
|
|||||||
}
|
}
|
||||||
|
|
||||||
open(sessions: SessionSummary[], currentChatId: string, limit: number): void {
|
open(sessions: SessionSummary[], currentChatId: string, limit: number): void {
|
||||||
|
this.prepareWorkspaceLabels(sessions)
|
||||||
const rows = sessions
|
const rows = sessions
|
||||||
.map((session) => ({ ...session, active: session.chatId === currentChatId }))
|
.map((session) => ({ ...session, active: session.chatId === currentChatId }))
|
||||||
.sort((left, right) => {
|
.sort((left, right) => {
|
||||||
@@ -92,4 +98,41 @@ export class SessionMenu {
|
|||||||
setTheme(theme: PickerMenuTheme): void {
|
setTheme(theme: PickerMenuTheme): void {
|
||||||
this.picker.setTheme(theme)
|
this.picker.setTheme(theme)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private prepareWorkspaceLabels(sessions: SessionSummary[]): void {
|
||||||
|
this.workspaceLabels.clear()
|
||||||
|
const scopes = sessions.flatMap((session) => {
|
||||||
|
const path = normalizeWorkspacePath(session.workspaceScope?.project_path)
|
||||||
|
return path ? [{ path, name: session.workspaceScope?.project_name?.trim() || pathName(path) }] : []
|
||||||
|
})
|
||||||
|
const paths = new Set(scopes.map(({ path }) => path))
|
||||||
|
this.showWorkspaces = paths.size > 1
|
||||||
|
if (!this.showWorkspaces) return
|
||||||
|
const namePaths = new Map<string, Set<string>>()
|
||||||
|
for (const { path, name } of scopes) {
|
||||||
|
const pathsForName = namePaths.get(name) || new Set<string>()
|
||||||
|
pathsForName.add(path)
|
||||||
|
namePaths.set(name, pathsForName)
|
||||||
|
}
|
||||||
|
for (const { path, name } of scopes) {
|
||||||
|
this.workspaceLabels.set(path, namePaths.get(name)?.size === 1 ? name : shortPath(path))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private workspaceLabel(session: SessionSummary): string {
|
||||||
|
return this.workspaceLabels.get(normalizeWorkspacePath(session.workspaceScope?.project_path)) || ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeWorkspacePath(value: string | undefined): string {
|
||||||
|
return (value || "").trim().replace(/\\/gu, "/").replace(/\/+$/u, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
function pathName(path: string): string {
|
||||||
|
return path.split("/").filter(Boolean).at(-1) || path
|
||||||
|
}
|
||||||
|
|
||||||
|
function shortPath(path: string): string {
|
||||||
|
const parts = path.split("/").filter(Boolean)
|
||||||
|
return parts.slice(-2).join("/") || path
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user