mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-08-28 19:11:03 +08:00
Merge pull request #13407 from Kilo-Org/plan-agent-manager-sessions
feat(agent-manager): replace sessions list with a per-project history button
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"kilo-code": patch
|
||||
---
|
||||
|
||||
Replace the per-project SESSIONS list in the Agent Manager sidebar with a per-project history button. The sessions view is now scoped to the clicked project and offers per-session actions to resume it in the project's local tabs or in a freshly created worktree.
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:00088eaafbe9b65c5baf0eb1ce185a95da3e8492bf3c77a940f20d329ff93046
|
||||
size 39409
|
||||
oid sha256:490320ae56acbbc5ea7283d72fb8dd4b93a28ce3f1ec9678ce8ff3ea1bab953e
|
||||
size 34649
|
||||
|
||||
@@ -70,6 +70,10 @@ const icons: Record<string, { path: string; viewBox: string }> = {
|
||||
viewBox: "0 0 24 24",
|
||||
path: `<path d="M12 14L9 10M12 14L15 10M21 15C21 18.866 17.866 22 14 22H10C6.134 22 3 18.866 3 15V9C3 5.134 6.134 2 10 2H14C17.866 2 21 5.134 21 9V15Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>`,
|
||||
},
|
||||
local: {
|
||||
viewBox: "0 0 20 20",
|
||||
path: `<rect x="2.5" y="3.5" width="15" height="10" rx="1" stroke="currentColor"/><path d="M6 16.5H14" stroke="currentColor" stroke-linecap="square"/><path d="M10 13.5V16.5" stroke="currentColor"/>`,
|
||||
},
|
||||
}
|
||||
|
||||
type Name = keyof typeof icons
|
||||
|
||||
@@ -74,7 +74,7 @@ import type { ProjectContext } from "./project/context"
|
||||
import { ProjectContexts } from "./project/contexts"
|
||||
import { hydrateExpanded } from "./project/hydrate"
|
||||
import { createMultiVersion, type MultiVersionHost } from "./provider-multi-version"
|
||||
import { handleProjectMessage, type ProjectMessageDeps } from "./project/messages"
|
||||
import { handleProjectMessage, routeProjectSession, type ProjectMessageDeps } from "./project/messages"
|
||||
import { createProjectWiring, type ProjectWiring } from "./project/wiring"
|
||||
import { ProjectScope } from "./project/scope"
|
||||
import type { AgentManagerOutMessage, AgentManagerInMessage } from "./types"
|
||||
@@ -197,6 +197,7 @@ export class AgentManagerProvider implements Disposable {
|
||||
pushState: (ctx) => this.pushState(ctx),
|
||||
changed: () => this.onWorkspaceChanged(),
|
||||
selected: (target) => this.postToWebview({ type: "agentManager.selectionActivated", target }),
|
||||
routeSession: (pid, sid, dir, gen) => routeProjectSession(this.panel?.sessions, pid, sid, dir, gen),
|
||||
})
|
||||
this.registry = wiring.registry
|
||||
this.contexts = wiring.contexts
|
||||
@@ -323,9 +324,8 @@ export class AgentManagerProvider implements Disposable {
|
||||
}
|
||||
const info = ev.properties?.info
|
||||
const dir = info?.directory
|
||||
// Session events from sync or older backends can lack time/directory; a
|
||||
// throw here would escape into the SSE dispatch loop and starve the other
|
||||
// listeners (there is no per-listener error isolation).
|
||||
// Session events from sync or older backends can lack time/directory; a throw
|
||||
// would escape into the SSE dispatch loop and starve the other listeners.
|
||||
if (!info?.time || !dir || (info.parentID !== undefined && info.parentID !== null)) return
|
||||
const ctx = this.contexts.byDirectory(dir)
|
||||
if (!ctx || ctx.lifecycle !== "ready") return
|
||||
|
||||
@@ -12,7 +12,25 @@ import type { ProjectRegistry } from "./registry"
|
||||
import type { ProjectContext, ProjectInitResult } from "./context"
|
||||
import type { ProjectContexts } from "./contexts"
|
||||
import { projectIdFor, resolveProjectRoot, samePath } from "./paths"
|
||||
import type { SidebarTarget } from "./route"
|
||||
import type { SidebarTarget, SessionRef } from "./route"
|
||||
|
||||
/** Route one session to a directory inside a project via the shared session provider. */
|
||||
export function routeProjectSession(
|
||||
sessions:
|
||||
| {
|
||||
setSessionDirectory(id: string, directory: string): void
|
||||
registerSessionRoute?(ref: SessionRef, directory: string, generation: number): void
|
||||
}
|
||||
| undefined,
|
||||
projectId: string,
|
||||
sessionId: string,
|
||||
directory: string,
|
||||
generation: number,
|
||||
): void {
|
||||
if (!sessions) return
|
||||
sessions.setSessionDirectory(sessionId, directory)
|
||||
sessions.registerSessionRoute?.({ projectId, sessionId }, directory, generation)
|
||||
}
|
||||
|
||||
export interface ProjectMessageDeps {
|
||||
registry: ProjectRegistry
|
||||
@@ -35,6 +53,8 @@ export interface ProjectMessageDeps {
|
||||
openSettings: (tab?: string, projectId?: string) => void
|
||||
/** Ensure a context's repository state is ready (no-op once initialized). */
|
||||
ready: (ctx: ProjectContext) => Promise<ProjectInitResult>
|
||||
/** Route one session to a directory inside a project (session override + project route). */
|
||||
routeSession?: (projectId: string, sessionId: string, directory: string, generation: number) => void
|
||||
log: (...args: unknown[]) => void
|
||||
}
|
||||
|
||||
@@ -64,6 +84,11 @@ export async function handleProjectMessage(m: AgentManagerInMessage, deps: Proje
|
||||
await activateSelection(m.target, deps, m.restore === true)
|
||||
return true
|
||||
}
|
||||
if (m.type === "agentManager.openSessionLocally") {
|
||||
if (!m.projectId) return false
|
||||
await openSessionLocally(m.projectId, m.sessionId, deps)
|
||||
return true
|
||||
}
|
||||
if (m.type === "agentManager.rememberTarget") {
|
||||
rememberTarget(m.projectId, m.target, deps)
|
||||
return true
|
||||
@@ -106,6 +131,35 @@ async function activateSelection(requested: SidebarTarget, deps: ProjectMessageD
|
||||
finish(target, deps)
|
||||
}
|
||||
|
||||
/**
|
||||
* Move a worktree-bound session back to the project root and open it in the
|
||||
* project's local tabs. Fall back to local gracefully when the worktree is
|
||||
* already gone (the session may be live only).
|
||||
*/
|
||||
async function openSessionLocally(projectId: string, sessionId: string, deps: ProjectMessageDeps): Promise<void> {
|
||||
if (disabled(deps)) return
|
||||
const ctx = deps.contexts.resolve(projectId)
|
||||
if (!ctx || !deps.contexts.usable(projectId)) {
|
||||
deps.error("The project is unavailable. Check that the repository still exists.")
|
||||
return
|
||||
}
|
||||
const result = await deps.ready(ctx)
|
||||
if (!result.current || !result.ok) {
|
||||
deps.error("The project is not ready yet. Expand it before selecting a worktree or session.")
|
||||
deps.push()
|
||||
return
|
||||
}
|
||||
const state = ctx.peekState()
|
||||
if (!state?.getSession(sessionId) && !ctx.hasLiveSession(sessionId)) {
|
||||
deps.log(`openSessionLocally: unknown session ${sessionId}`)
|
||||
return
|
||||
}
|
||||
state?.moveSession(sessionId, null)
|
||||
deps.routeSession?.(projectId, sessionId, ctx.root, ctx.generation)
|
||||
deps.push()
|
||||
finish({ projectId, kind: "session", sessionId }, deps)
|
||||
}
|
||||
|
||||
/** Commit the active project, persist the target, and acknowledge the selection. */
|
||||
function finish(target: SidebarTarget, deps: ProjectMessageDeps): void {
|
||||
const previous = deps.contexts.active()?.id
|
||||
|
||||
@@ -9,6 +9,7 @@ export const STATE_GATED = new Set<string>([
|
||||
"agentManager.deleteWorktree",
|
||||
"agentManager.removeStaleWorktree",
|
||||
"agentManager.openLocally",
|
||||
"agentManager.openSessionLocally",
|
||||
"agentManager.addSessionToWorktree",
|
||||
"agentManager.closeSession",
|
||||
"agentManager.persistSession",
|
||||
|
||||
@@ -44,6 +44,8 @@ export function createProjectWiring(opts: {
|
||||
changed: () => void
|
||||
/** Acknowledge an atomically validated sidebar selection. */
|
||||
selected: (target: import("./route").SidebarTarget) => void
|
||||
/** Route one session to a directory inside a project (override + project route). */
|
||||
routeSession?: (projectId: string, sessionId: string, directory: string, generation: number) => void
|
||||
}): ProjectWiring {
|
||||
const registry = new ProjectRegistry(
|
||||
{ read: () => opts.host.readProjects(), write: (value) => opts.host.writeProjects(value) },
|
||||
@@ -66,6 +68,7 @@ export function createProjectWiring(opts: {
|
||||
ready: opts.ready,
|
||||
push: opts.push,
|
||||
selected: opts.selected,
|
||||
routeSession: opts.routeSession,
|
||||
error: (message) => opts.host.showError(message),
|
||||
openSettings: (tab, projectId) => opts.host.openSettings(tab, projectId),
|
||||
log: opts.log,
|
||||
|
||||
@@ -575,6 +575,13 @@ interface AddSessionToWorktreeIn {
|
||||
sessionId?: string
|
||||
}
|
||||
|
||||
/** Move a session back to the project root and open it in the local tabs. */
|
||||
interface OpenSessionLocallyIn {
|
||||
type: "agentManager.openSessionLocally"
|
||||
projectId?: string
|
||||
sessionId: string
|
||||
}
|
||||
|
||||
interface CloseSessionIn {
|
||||
type: "agentManager.closeSession"
|
||||
sessionId: string
|
||||
@@ -1081,6 +1088,7 @@ export type AgentManagerInMessage =
|
||||
| RemoveStaleWorktreeIn
|
||||
| PromoteSessionIn
|
||||
| OpenLocallyIn
|
||||
| OpenSessionLocallyIn
|
||||
| AddSessionToWorktreeIn
|
||||
| CloseSessionIn
|
||||
| PersistSessionIn
|
||||
|
||||
@@ -24,7 +24,7 @@ const TSX_FILES = [
|
||||
path.join(ROOT, "webview-ui/agent-manager/AgentManagerApp.tsx"),
|
||||
path.join(ROOT, "webview-ui/agent-manager/SubagentPanel.tsx"),
|
||||
path.join(ROOT, "webview-ui/agent-manager/EditPreviewPanel.tsx"),
|
||||
path.join(ROOT, "webview-ui/agent-manager/UnassignedSessionsSection.tsx"),
|
||||
path.join(ROOT, "webview-ui/agent-manager/SessionRowActions.tsx"),
|
||||
path.join(ROOT, "webview-ui/agent-manager/NewWorktreeDialog.tsx"),
|
||||
path.join(ROOT, "webview-ui/agent-manager/ProjectSelect.tsx"),
|
||||
path.join(ROOT, "webview-ui/agent-manager/sortable-tab.tsx"),
|
||||
@@ -456,9 +456,9 @@ describe("Agent Manager Worktree Actions", () => {
|
||||
})
|
||||
|
||||
it("does not attribute the new-worktree shortcut to session promotion", () => {
|
||||
const source = fs.readFileSync(path.join(ROOT, "webview-ui/agent-manager/UnassignedSessionsSection.tsx"), "utf-8")
|
||||
const source = fs.readFileSync(path.join(ROOT, "webview-ui/agent-manager/SessionRowActions.tsx"), "utf-8")
|
||||
|
||||
expect(source).toContain('<Tooltip value={t("agentManager.session.openInWorktree")}')
|
||||
expect(source).toContain('t("agentManager.session.openInWorktree")')
|
||||
expect(source).not.toContain("TooltipKeybind")
|
||||
})
|
||||
})
|
||||
|
||||
@@ -5,13 +5,11 @@ import {
|
||||
validateLocalSession,
|
||||
adjacentHint,
|
||||
canOpenRootSession,
|
||||
filterUnassignedSessions,
|
||||
remoteSessions,
|
||||
buildProjectNavOrder,
|
||||
resolveProjectNav,
|
||||
localNavId,
|
||||
worktreeNavId,
|
||||
sessionNavId,
|
||||
type ProjectNavInput,
|
||||
LOCAL,
|
||||
} from "../../webview-ui/agent-manager/navigate"
|
||||
@@ -203,105 +201,6 @@ describe("adjacentHint", () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe("filterUnassignedSessions", () => {
|
||||
const at = (day: number) => `2026-01-${String(day).padStart(2, "0")}T00:00:00.000Z`
|
||||
const info = (id: string, day: number, parentID: string | null = null) => ({
|
||||
id,
|
||||
createdAt: at(day),
|
||||
parentID,
|
||||
})
|
||||
|
||||
it("filters sparse session updates until ancestry is known", () => {
|
||||
const result = filterUnassignedSessions([{ id: "unknown", createdAt: at(1) }], new Set(), new Set())
|
||||
|
||||
expect(result).toEqual([])
|
||||
})
|
||||
|
||||
it("keeps root sessions with null parent IDs", () => {
|
||||
const result = filterUnassignedSessions([info("root", 1, null)], new Set(), new Set())
|
||||
|
||||
expect(result.map((s) => s.id)).toEqual(["root"])
|
||||
})
|
||||
|
||||
it("filters child sessions with parent IDs", () => {
|
||||
const result = filterUnassignedSessions(
|
||||
[info("parent", 2), info("child", 3, "parent"), info("orphan", 4, "missing")],
|
||||
new Set(),
|
||||
new Set(),
|
||||
)
|
||||
|
||||
expect(result.map((s) => s.id)).toEqual(["parent"])
|
||||
})
|
||||
|
||||
it("filters string parent IDs even when they are empty", () => {
|
||||
const result = filterUnassignedSessions([info("blank", 2, ""), info("root", 1)], new Set(), new Set())
|
||||
|
||||
expect(result.map((s) => s.id)).toEqual(["root"])
|
||||
})
|
||||
|
||||
it("filters worktree sessions while keeping other roots", () => {
|
||||
const result = filterUnassignedSessions(
|
||||
[info("root", 1), info("worktree", 3), info("other", 2)],
|
||||
new Set(["worktree"]),
|
||||
new Set(),
|
||||
)
|
||||
|
||||
expect(result.map((s) => s.id)).toEqual(["other", "root"])
|
||||
})
|
||||
|
||||
it("filters local tab sessions while keeping other roots", () => {
|
||||
const result = filterUnassignedSessions(
|
||||
[info("root", 1), info("local", 3), info("other", 2)],
|
||||
new Set(),
|
||||
new Set(["local"]),
|
||||
)
|
||||
|
||||
expect(result.map((s) => s.id)).toEqual(["other", "root"])
|
||||
})
|
||||
|
||||
it("applies child, worktree, and local filters before sorting", () => {
|
||||
const result = filterUnassignedSessions(
|
||||
[info("old-root", 1), info("child", 6, "old-root"), info("worktree", 5), info("local", 4), info("new-root", 3)],
|
||||
new Set(["worktree"]),
|
||||
new Set(["local"]),
|
||||
)
|
||||
|
||||
expect(result.map((s) => s.id)).toEqual(["new-root", "old-root"])
|
||||
})
|
||||
|
||||
it("returns an empty list when every session is filtered", () => {
|
||||
const result = filterUnassignedSessions(
|
||||
[info("child", 3, "root"), info("worktree", 2), info("local", 1)],
|
||||
new Set(["worktree"]),
|
||||
new Set(["local"]),
|
||||
)
|
||||
|
||||
expect(result).toEqual([])
|
||||
})
|
||||
|
||||
it("does not mutate the input order", () => {
|
||||
const sessions = [info("old", 1), info("new", 3), info("mid", 2)]
|
||||
|
||||
filterUnassignedSessions(sessions, new Set(), new Set())
|
||||
|
||||
expect(sessions.map((s) => s.id)).toEqual(["old", "new", "mid"])
|
||||
})
|
||||
|
||||
it("preserves session objects and extra fields", () => {
|
||||
const root = { ...info("root", 1), title: "Existing session" }
|
||||
const result = filterUnassignedSessions([root], new Set(), new Set())
|
||||
|
||||
expect(result[0]).toBe(root)
|
||||
expect(result[0]?.title).toBe("Existing session")
|
||||
})
|
||||
|
||||
it("keeps a parent root when its child is filtered", () => {
|
||||
const result = filterUnassignedSessions([info("root", 1), info("child", 2, "root")], new Set(), new Set())
|
||||
|
||||
expect(result.map((s) => s.id)).toEqual(["root"])
|
||||
})
|
||||
})
|
||||
|
||||
describe("canOpenRootSession", () => {
|
||||
const sessions = [{ id: "root", parentID: null }, { id: "child", parentID: "root" }, { id: "sparse" }]
|
||||
|
||||
@@ -338,17 +237,10 @@ describe("remoteSessions", () => {
|
||||
})
|
||||
|
||||
describe("buildProjectNavOrder", () => {
|
||||
const project = (
|
||||
p: Omit<ProjectNavInput, "sessionsCollapsed"> & { sessionsCollapsed?: boolean },
|
||||
): ProjectNavInput => ({
|
||||
...p,
|
||||
sessionsCollapsed: p.sessionsCollapsed ?? false,
|
||||
})
|
||||
|
||||
it("builds A Local -> A worktree -> B Local -> B worktree -> B session across expanded projects", () => {
|
||||
it("builds A Local -> A worktree -> B Local -> B worktree across expanded projects", () => {
|
||||
const order = buildProjectNavOrder([
|
||||
project({ id: "A", expanded: true, worktrees: [{ id: "aw1" }], sections: [], unassigned: [] }),
|
||||
project({ id: "B", expanded: true, worktrees: [{ id: "bw1" }], sections: [], unassigned: [{ id: "bs1" }] }),
|
||||
{ id: "A", expanded: true, worktrees: [{ id: "aw1" }], sections: [] },
|
||||
{ id: "B", expanded: true, worktrees: [{ id: "bw1" }], sections: [] },
|
||||
])
|
||||
|
||||
expect(order.map((e) => e.id)).toEqual([
|
||||
@@ -356,33 +248,27 @@ describe("buildProjectNavOrder", () => {
|
||||
worktreeNavId("A", "aw1"),
|
||||
localNavId("B"),
|
||||
worktreeNavId("B", "bw1"),
|
||||
sessionNavId("B", "bs1"),
|
||||
])
|
||||
expect(order.map((e) => e.target)).toEqual([
|
||||
{ projectId: "A", kind: "local" },
|
||||
{ projectId: "A", kind: "worktree", worktreeId: "aw1" },
|
||||
{ projectId: "B", kind: "local" },
|
||||
{ projectId: "B", kind: "worktree", worktreeId: "bw1" },
|
||||
{ projectId: "B", kind: "session", sessionId: "bs1" },
|
||||
])
|
||||
})
|
||||
|
||||
it("uses project-qualified composite ids, never raw worktree/session ids", () => {
|
||||
const order = buildProjectNavOrder([
|
||||
project({ id: "A", expanded: true, worktrees: [{ id: "aw1" }], sections: [], unassigned: [{ id: "as1" }] }),
|
||||
])
|
||||
it("uses project-qualified composite ids, never raw worktree ids", () => {
|
||||
const order = buildProjectNavOrder([{ id: "A", expanded: true, worktrees: [{ id: "aw1" }], sections: [] }])
|
||||
const ids = order.map((e) => e.id)
|
||||
expect(ids).not.toContain("aw1")
|
||||
expect(ids).not.toContain("as1")
|
||||
expect(ids).toContain("A:local")
|
||||
expect(ids).toContain("A:wt:aw1")
|
||||
expect(ids).toContain("A:sess:as1")
|
||||
})
|
||||
|
||||
it("excludes collapsed projects entirely", () => {
|
||||
const order = buildProjectNavOrder([
|
||||
project({ id: "A", expanded: true, worktrees: [{ id: "aw1" }], sections: [], unassigned: [] }),
|
||||
project({ id: "C", expanded: false, worktrees: [{ id: "cw1" }], sections: [], unassigned: [{ id: "cs1" }] }),
|
||||
{ id: "A", expanded: true, worktrees: [{ id: "aw1" }], sections: [] },
|
||||
{ id: "C", expanded: false, worktrees: [{ id: "cw1" }], sections: [] },
|
||||
])
|
||||
expect(order.map((e) => e.id)).toEqual([localNavId("A"), worktreeNavId("A", "aw1")])
|
||||
expect(order.some((e) => e.target.kind === "worktree" && e.target.worktreeId === "cw1")).toBe(false)
|
||||
@@ -390,33 +276,31 @@ describe("buildProjectNavOrder", () => {
|
||||
|
||||
it("excludes worktrees inside collapsed sections but keeps ungrouped ones", () => {
|
||||
const order = buildProjectNavOrder([
|
||||
project({
|
||||
{
|
||||
id: "A",
|
||||
expanded: true,
|
||||
worktrees: [{ id: "aw1", sectionId: "s1" }, { id: "aw2" }],
|
||||
sections: [{ id: "s1", collapsed: true }],
|
||||
unassigned: [],
|
||||
}),
|
||||
},
|
||||
])
|
||||
expect(order.map((e) => e.id)).toEqual([localNavId("A"), worktreeNavId("A", "aw2")])
|
||||
})
|
||||
|
||||
it("renders ungrouped worktrees before section members (matching the project body)", () => {
|
||||
const order = buildProjectNavOrder([
|
||||
project({
|
||||
{
|
||||
id: "A",
|
||||
expanded: true,
|
||||
worktrees: [{ id: "aw1", sectionId: "s1" }, { id: "aw2" }],
|
||||
sections: [{ id: "s1", collapsed: false }],
|
||||
unassigned: [],
|
||||
}),
|
||||
},
|
||||
])
|
||||
expect(order.map((e) => e.id)).toEqual([localNavId("A"), worktreeNavId("A", "aw2"), worktreeNavId("A", "aw1")])
|
||||
})
|
||||
|
||||
it("follows persisted top-level section order and worktree order", () => {
|
||||
const order = buildProjectNavOrder([
|
||||
project({
|
||||
{
|
||||
id: "A",
|
||||
expanded: true,
|
||||
worktrees: [{ id: "aw1", sectionId: "s1" }, { id: "aw2" }, { id: "aw3", sectionId: "s2" }],
|
||||
@@ -425,8 +309,7 @@ describe("buildProjectNavOrder", () => {
|
||||
{ id: "s1", collapsed: false },
|
||||
{ id: "s2", collapsed: false },
|
||||
],
|
||||
unassigned: [],
|
||||
}),
|
||||
},
|
||||
])
|
||||
|
||||
expect(order.map((e) => e.id)).toEqual([
|
||||
@@ -439,14 +322,13 @@ describe("buildProjectNavOrder", () => {
|
||||
|
||||
it("keeps multi-version worktrees adjacent", () => {
|
||||
const order = buildProjectNavOrder([
|
||||
project({
|
||||
{
|
||||
id: "A",
|
||||
expanded: true,
|
||||
worktrees: [{ id: "aw1", groupId: "g" }, { id: "aw2" }, { id: "aw3", groupId: "g" }],
|
||||
worktreeOrder: ["aw1", "aw2", "aw3"],
|
||||
sections: [],
|
||||
unassigned: [],
|
||||
}),
|
||||
},
|
||||
])
|
||||
|
||||
expect(order.map((e) => e.id)).toEqual([
|
||||
@@ -459,7 +341,7 @@ describe("buildProjectNavOrder", () => {
|
||||
|
||||
it("matches raw ungrouped order when sections are present", () => {
|
||||
const order = buildProjectNavOrder([
|
||||
project({
|
||||
{
|
||||
id: "A",
|
||||
expanded: true,
|
||||
worktrees: [
|
||||
@@ -470,8 +352,7 @@ describe("buildProjectNavOrder", () => {
|
||||
],
|
||||
worktreeOrder: ["aw1", "aw2", "aw3", "s1", "aw4"],
|
||||
sections: [{ id: "s1", collapsed: false }],
|
||||
unassigned: [],
|
||||
}),
|
||||
},
|
||||
])
|
||||
|
||||
expect(order.map((e) => e.id)).toEqual([
|
||||
@@ -483,49 +364,26 @@ describe("buildProjectNavOrder", () => {
|
||||
])
|
||||
})
|
||||
|
||||
it("excludes unassigned sessions when the sessions section is collapsed", () => {
|
||||
const order = buildProjectNavOrder([
|
||||
project({
|
||||
id: "A",
|
||||
expanded: true,
|
||||
worktrees: [{ id: "aw1" }],
|
||||
sections: [],
|
||||
sessionsCollapsed: true,
|
||||
unassigned: [{ id: "as1" }, { id: "as2" }],
|
||||
}),
|
||||
])
|
||||
expect(order.map((e) => e.id)).toEqual([localNavId("A"), worktreeNavId("A", "aw1")])
|
||||
})
|
||||
|
||||
it("returns an empty order when every project is collapsed", () => {
|
||||
const order = buildProjectNavOrder([
|
||||
project({ id: "A", expanded: false, worktrees: [{ id: "aw1" }], sections: [], unassigned: [] }),
|
||||
])
|
||||
const order = buildProjectNavOrder([{ id: "A", expanded: false, worktrees: [{ id: "aw1" }], sections: [] }])
|
||||
expect(order).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
describe("resolveProjectNav", () => {
|
||||
// A Local -> A worktree -> B Local -> B worktree -> B session
|
||||
// A Local -> A worktree -> B Local -> B worktree
|
||||
const inputs: ProjectNavInput[] = [
|
||||
{ id: "A", expanded: true, worktrees: [{ id: "aw1" }], sections: [], sessionsCollapsed: false, unassigned: [] },
|
||||
{
|
||||
id: "B",
|
||||
expanded: true,
|
||||
worktrees: [{ id: "bw1" }],
|
||||
sections: [],
|
||||
sessionsCollapsed: false,
|
||||
unassigned: [{ id: "bs1" }],
|
||||
},
|
||||
{ id: "A", expanded: true, worktrees: [{ id: "aw1" }], sections: [] },
|
||||
{ id: "B", expanded: true, worktrees: [{ id: "bw1" }], sections: [] },
|
||||
]
|
||||
const order = buildProjectNavOrder(inputs)
|
||||
// Collapsed project C must not appear in the order
|
||||
const withCollapsed = buildProjectNavOrder([
|
||||
...inputs,
|
||||
{ id: "C", expanded: false, worktrees: [{ id: "cw1" }], sections: [], sessionsCollapsed: false, unassigned: [] },
|
||||
{ id: "C", expanded: false, worktrees: [{ id: "cw1" }], sections: [] },
|
||||
])
|
||||
|
||||
it("walks forward A Local -> A worktree -> B Local -> B worktree -> B session", () => {
|
||||
it("walks forward A Local -> A worktree -> B Local -> B worktree", () => {
|
||||
let current: string | undefined = undefined
|
||||
const trail: string[] = []
|
||||
for (let i = 0; i < 6; i++) {
|
||||
@@ -534,17 +392,11 @@ describe("resolveProjectNav", () => {
|
||||
current = entry.id
|
||||
trail.push(entry.id)
|
||||
}
|
||||
expect(trail).toEqual([
|
||||
localNavId("A"),
|
||||
worktreeNavId("A", "aw1"),
|
||||
localNavId("B"),
|
||||
worktreeNavId("B", "bw1"),
|
||||
sessionNavId("B", "bs1"),
|
||||
])
|
||||
expect(trail).toEqual([localNavId("A"), worktreeNavId("A", "aw1"), localNavId("B"), worktreeNavId("B", "bw1")])
|
||||
})
|
||||
|
||||
it("walks in reverse B session -> B worktree -> B Local -> A worktree -> A Local", () => {
|
||||
let current: string | undefined = sessionNavId("B", "bs1")
|
||||
it("walks in reverse B worktree -> B Local -> A worktree -> A Local", () => {
|
||||
let current: string | undefined = worktreeNavId("B", "bw1")
|
||||
const trail: string[] = [current]
|
||||
for (let i = 0; i < 6; i++) {
|
||||
const entry = resolveProjectNav("up", current, order)
|
||||
@@ -552,13 +404,7 @@ describe("resolveProjectNav", () => {
|
||||
current = entry.id
|
||||
trail.push(current)
|
||||
}
|
||||
expect(trail).toEqual([
|
||||
sessionNavId("B", "bs1"),
|
||||
worktreeNavId("B", "bw1"),
|
||||
localNavId("B"),
|
||||
worktreeNavId("A", "aw1"),
|
||||
localNavId("A"),
|
||||
])
|
||||
expect(trail).toEqual([worktreeNavId("B", "bw1"), localNavId("B"), worktreeNavId("A", "aw1"), localNavId("A")])
|
||||
})
|
||||
|
||||
it("returns undefined at the top boundary (up from first)", () => {
|
||||
@@ -566,12 +412,12 @@ describe("resolveProjectNav", () => {
|
||||
})
|
||||
|
||||
it("returns undefined at the bottom boundary (down from last)", () => {
|
||||
expect(resolveProjectNav("down", sessionNavId("B", "bs1"), order)).toBeUndefined()
|
||||
expect(resolveProjectNav("down", worktreeNavId("B", "bw1"), order)).toBeUndefined()
|
||||
})
|
||||
|
||||
it("does not wrap around", () => {
|
||||
expect(resolveProjectNav("up", localNavId("A"), order)).toBeUndefined()
|
||||
expect(resolveProjectNav("down", sessionNavId("B", "bs1"), order)).toBeUndefined()
|
||||
expect(resolveProjectNav("down", worktreeNavId("B", "bw1"), order)).toBeUndefined()
|
||||
})
|
||||
|
||||
it("treats an unknown current as before-first (down -> first, up -> undefined)", () => {
|
||||
@@ -592,7 +438,7 @@ describe("resolveProjectNav", () => {
|
||||
it("collapsed project C is excluded from the order", () => {
|
||||
expect(withCollapsed.length).toBe(order.length)
|
||||
expect(withCollapsed.some((e) => e.id === worktreeNavId("C", "cw1"))).toBe(false)
|
||||
// Forward walk still ends at B session, never reaching C
|
||||
// Forward walk still ends at B worktree, never reaching C
|
||||
let current: string | undefined = undefined
|
||||
let last: string | undefined
|
||||
for (let i = 0; i < 10; i++) {
|
||||
@@ -601,7 +447,7 @@ describe("resolveProjectNav", () => {
|
||||
current = entry.id
|
||||
last = entry.id
|
||||
}
|
||||
expect(last).toBe(sessionNavId("B", "bs1"))
|
||||
expect(last).toBe(worktreeNavId("B", "bw1"))
|
||||
})
|
||||
})
|
||||
|
||||
@@ -636,25 +482,15 @@ describe("createProjectNav", () => {
|
||||
worktrees: { id: string; sectionId?: string }[],
|
||||
sessions: { id: string; worktreeId: string | null }[],
|
||||
sections: { id: string; collapsed: boolean }[] = [],
|
||||
sessionsCollapsed = false,
|
||||
): AgentManagerStateMessage =>
|
||||
({
|
||||
type: "agentManager.state",
|
||||
worktrees: worktrees as never,
|
||||
sessions: sessions as never,
|
||||
sections,
|
||||
sessionsCollapsed,
|
||||
}) as AgentManagerStateMessage
|
||||
|
||||
const session = (id: string): ProjectSessionInfo => ({
|
||||
id,
|
||||
title: id,
|
||||
createdAt: "",
|
||||
updatedAt: "",
|
||||
worktreeId: null,
|
||||
})
|
||||
|
||||
// A (expanded): worktree aw1. B (expanded): worktree bw1 + unassigned bs1.
|
||||
// A (expanded): worktree aw1. B (expanded): worktree bw1.
|
||||
// C (collapsed): worktree cw1 — must never be reached.
|
||||
const projects = () => [project("A", true), project("B", true), project("C", false)]
|
||||
const states = () => ({
|
||||
@@ -662,7 +498,6 @@ describe("createProjectNav", () => {
|
||||
B: state([{ id: "bw1" }], [{ id: "bs1", worktreeId: null }]),
|
||||
C: state([{ id: "cw1" }], []),
|
||||
})
|
||||
const sessions = () => ({ A: [], B: [session("bs1")], C: [] })
|
||||
|
||||
const run = (
|
||||
fn: (nav: ReturnType<typeof createProjectNav>) => void,
|
||||
@@ -683,7 +518,6 @@ describe("createProjectNav", () => {
|
||||
focus,
|
||||
projects,
|
||||
states,
|
||||
sessions,
|
||||
activeProjectId: () => activeProjectId,
|
||||
selection: () => selection,
|
||||
currentSessionID: () => currentSessionID,
|
||||
@@ -724,16 +558,19 @@ describe("createProjectNav", () => {
|
||||
return posted
|
||||
}
|
||||
|
||||
it("multi-project step traverses A Local -> A worktree -> B Local -> B worktree -> B session", () => {
|
||||
it("multi-project step traverses A Local -> A worktree -> B Local -> B worktree", () => {
|
||||
expect(stepOnce("down", LOCAL, "A", undefined)).toEqual({ projectId: "A", kind: "worktree", worktreeId: "aw1" })
|
||||
expect(stepOnce("down", "aw1", "A", undefined)).toEqual({ projectId: "B", kind: "local" })
|
||||
expect(stepOnce("down", LOCAL, "B", undefined)).toEqual({ projectId: "B", kind: "worktree", worktreeId: "bw1" })
|
||||
expect(stepOnce("down", "bw1", "B", undefined)).toEqual({ projectId: "B", kind: "session", sessionId: "bs1" })
|
||||
expect(stepOnce("down", null, "B", "bs1")).toBeUndefined()
|
||||
expect(stepOnce("down", "bw1", "B", undefined)).toBeUndefined()
|
||||
})
|
||||
|
||||
it("multi-project step reverses B session -> B worktree -> B Local -> A worktree -> A Local", () => {
|
||||
expect(stepOnce("up", null, "B", "bs1")).toEqual({ projectId: "B", kind: "worktree", worktreeId: "bw1" })
|
||||
it("multi-project step treats a local session tab as B Local", () => {
|
||||
expect(stepOnce("down", null, "B", "bs1")).toEqual({ projectId: "B", kind: "worktree", worktreeId: "bw1" })
|
||||
expect(stepOnce("up", null, "B", "bs1")).toEqual({ projectId: "A", kind: "worktree", worktreeId: "aw1" })
|
||||
})
|
||||
|
||||
it("multi-project step reverses B worktree -> B Local -> A worktree -> A Local", () => {
|
||||
expect(stepOnce("up", "bw1", "B", undefined)).toEqual({ projectId: "B", kind: "local" })
|
||||
expect(stepOnce("up", LOCAL, "B", undefined)).toEqual({ projectId: "A", kind: "worktree", worktreeId: "aw1" })
|
||||
expect(stepOnce("up", "aw1", "A", undefined)).toEqual({ projectId: "A", kind: "local" })
|
||||
@@ -741,14 +578,13 @@ describe("createProjectNav", () => {
|
||||
})
|
||||
|
||||
it("collapsed project C is never reached via jump", () => {
|
||||
// Global order: A:local(0), A:aw1(1), B:local(2), B:bw1(3), B:bs1(4).
|
||||
// Global order: A:local(0), A:aw1(1), B:local(2), B:bw1(3).
|
||||
expect(jumpOnce(0)).toEqual({ projectId: "A", kind: "local" })
|
||||
expect(jumpOnce(1)).toEqual({ projectId: "A", kind: "worktree", worktreeId: "aw1" })
|
||||
expect(jumpOnce(2)).toEqual({ projectId: "B", kind: "local" })
|
||||
expect(jumpOnce(3)).toEqual({ projectId: "B", kind: "worktree", worktreeId: "bw1" })
|
||||
expect(jumpOnce(4)).toEqual({ projectId: "B", kind: "session", sessionId: "bs1" })
|
||||
// Past the end — and C's collapsed worktree is never reachable by any index.
|
||||
expect(jumpOnce(5)).toBeUndefined()
|
||||
expect(jumpOnce(4)).toBeUndefined()
|
||||
expect(jumpOnce(99)).toBeUndefined()
|
||||
})
|
||||
|
||||
|
||||
@@ -174,7 +174,7 @@ describe("buildSidebarOrder", () => {
|
||||
it("returns LOCAL + all sorted worktrees when no sections exist", () => {
|
||||
const sorted = [wt("a"), wt("b"), wt("c")]
|
||||
const items = buildTopLevelItems([], [], sorted, [])
|
||||
const result = buildSidebarOrder(items, sorted, [], () => [], [])
|
||||
const result = buildSidebarOrder(items, sorted, [], () => [])
|
||||
expect(result).toEqual([
|
||||
{ type: "local", id: "local" },
|
||||
{ type: "wt", id: "a" },
|
||||
@@ -191,7 +191,7 @@ describe("buildSidebarOrder", () => {
|
||||
const sorted = [w1, w2, w3]
|
||||
const items = buildTopLevelItems([s1], [w3], sorted, ["s1", "w3"])
|
||||
const members = (id: string) => (id === "s1" ? [w1, w2] : [])
|
||||
const result = buildSidebarOrder(items, sorted, [s1], members, [])
|
||||
const result = buildSidebarOrder(items, sorted, [s1], members)
|
||||
expect(result).toEqual([
|
||||
{ type: "local", id: "local" },
|
||||
{ type: "wt", id: "w3" },
|
||||
@@ -207,7 +207,7 @@ describe("buildSidebarOrder", () => {
|
||||
const sorted = [w1, w2]
|
||||
const items = buildTopLevelItems([s1], [w2], sorted, ["s1", "w2"])
|
||||
const members = (id: string) => (id === "s1" ? [w1] : [])
|
||||
const result = buildSidebarOrder(items, sorted, [s1], members, [])
|
||||
const result = buildSidebarOrder(items, sorted, [s1], members)
|
||||
expect(result).toEqual([
|
||||
{ type: "local", id: "local" },
|
||||
{ type: "wt", id: "w2" },
|
||||
@@ -227,20 +227,17 @@ describe("buildSidebarOrder", () => {
|
||||
if (id === "s2") return [w3]
|
||||
return []
|
||||
}
|
||||
const result = buildSidebarOrder(items, sorted, [s1, s2], members, [])
|
||||
const result = buildSidebarOrder(items, sorted, [s1, s2], members)
|
||||
expect(result.map((r) => r.id)).toEqual(["local", "w2", "w1", "w3"])
|
||||
})
|
||||
|
||||
it("appends unassigned sessions after worktrees", () => {
|
||||
it("appends nothing after worktrees (sessions live in the history view)", () => {
|
||||
const sorted = [wt("a")]
|
||||
const items = buildTopLevelItems([], [], sorted, [])
|
||||
const sessions = [{ id: "sess1" }, { id: "sess2" }]
|
||||
const result = buildSidebarOrder(items, sorted, [], () => [], sessions)
|
||||
const result = buildSidebarOrder(items, sorted, [], () => [])
|
||||
expect(result).toEqual([
|
||||
{ type: "local", id: "local" },
|
||||
{ type: "wt", id: "a" },
|
||||
{ type: "session", id: "sess1" },
|
||||
{ type: "session", id: "sess2" },
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
@@ -47,6 +47,7 @@ import type {
|
||||
TerminalDestination,
|
||||
TerminalFont,
|
||||
} from "../src/types/messages"
|
||||
import { historyRowActions as historyRowActionsFactory } from "./history-actions"
|
||||
import { readFontSize } from "../src/font-size"
|
||||
import { IndexingProvider } from "../src/context/indexing"
|
||||
import {} from "@thisbeyond/solid-dnd"
|
||||
@@ -120,7 +121,6 @@ import {
|
||||
isKnownRootSession,
|
||||
nextSelectionAfterDelete,
|
||||
adjacentHint,
|
||||
filterUnassignedSessions,
|
||||
focusChatSearch,
|
||||
LOCAL,
|
||||
} from "./navigate"
|
||||
@@ -186,8 +186,6 @@ import {
|
||||
sortWorktrees,
|
||||
type TopLevelItem,
|
||||
} from "./section-helpers"
|
||||
import {} from "./section-dnd"
|
||||
import {} from "./constrain-drag-x"
|
||||
import { mergeWorktreeDiffs } from "../diff-viewer/diff-state"
|
||||
import { DiffScopeControls } from "../diff-viewer/DiffScopeControls"
|
||||
import { scopeCapabilities } from "./diff-scope-state"
|
||||
@@ -291,14 +289,6 @@ const AgentManagerContent: Component = () => {
|
||||
const evictLocal = (sid: string) =>
|
||||
setLocalSessionIDs((prev) => (prev.includes(sid) ? prev.filter((id) => id !== sid) : prev))
|
||||
const [sidebarWidth, setSidebarWidth] = createSignal(persisted?.sidebarWidth ?? DEFAULT_SIDEBAR_WIDTH)
|
||||
const sessionsCollapsed = () => registry.active().sessionsCollapsed() ?? true
|
||||
const setSessionsCollapsed = (v: Parameters<Setter<boolean>>[0]) =>
|
||||
registry.active().setSessionsCollapsed(typeof v === "function" ? v(sessionsCollapsed()) : v)
|
||||
const toggleSessions = () => {
|
||||
const collapsed = !sessionsCollapsed()
|
||||
setSessionsCollapsed(collapsed)
|
||||
vscode.postMessage({ type: "agentManager.setSessionsCollapsed", collapsed })
|
||||
}
|
||||
const sidebar = createSidebarCollapse(vscode, { initial: persisted?.sidebarCollapsed })
|
||||
const sidebarCollapsed = sidebar.collapsed
|
||||
const expandSidebar = sidebar.expand
|
||||
@@ -308,6 +298,26 @@ const AgentManagerContent: Component = () => {
|
||||
let sidebarRaf: number | undefined
|
||||
let pendingSidebarWidth: number | undefined
|
||||
const [history, setHistory] = createSignal(false)
|
||||
/** Project whose sessions the history view is scoped to (multi-project). */
|
||||
const [historyProject, setHistoryProject] = createSignal<string | undefined>()
|
||||
const closeHistory = () => {
|
||||
setHistory(false)
|
||||
setHistoryProject(undefined)
|
||||
}
|
||||
/** Open the sessions view; a project id scopes it and activates that project. */
|
||||
const openHistory = (pid?: string) => {
|
||||
const scoped = pid !== undefined && multiProject()
|
||||
if (scoped) {
|
||||
// Activating the target project first lets the shared session store and
|
||||
// the pick routing operate in that project only.
|
||||
vscode.postMessage({
|
||||
type: "agentManager.activateSelection",
|
||||
target: { projectId: pid, kind: "local" },
|
||||
} as never)
|
||||
}
|
||||
setHistoryProject(scoped ? pid : undefined)
|
||||
setHistory(true)
|
||||
}
|
||||
const [sidePanel, setSidePanel] = createSignal<SidePanelState>(null)
|
||||
const diffOpen = () => sidePanel() === SidePanel.Diff
|
||||
const prOpen = () => sidePanel() === SidePanel.PR
|
||||
@@ -326,7 +336,7 @@ const AgentManagerContent: Component = () => {
|
||||
const [panelWidth, setPanelWidth] = createSignal(clampPanelWidth(persisted?.sidePanelWidth, window.innerWidth))
|
||||
const resizeSide = createPanelResize(setPanelWidth, () => window.innerWidth)
|
||||
const showSideTerminal = () => {
|
||||
setHistory(false)
|
||||
closeHistory()
|
||||
setReviewActive(false)
|
||||
setSidePanel(SidePanel.Terminal)
|
||||
}
|
||||
@@ -349,7 +359,7 @@ const AgentManagerContent: Component = () => {
|
||||
currentProjectId,
|
||||
() => sidePanel() === SidePanel.Documents,
|
||||
() => {
|
||||
setHistory(false)
|
||||
closeHistory()
|
||||
setReviewActive(false)
|
||||
setSidePanel(SidePanel.Documents)
|
||||
},
|
||||
@@ -364,7 +374,7 @@ const AgentManagerContent: Component = () => {
|
||||
sync: (id, parentID) => session.syncSession(id, parentID, "inspector"),
|
||||
unsync: (id) => session.unsyncSession(id, "inspector"),
|
||||
show: () => {
|
||||
setHistory(false)
|
||||
closeHistory()
|
||||
setReviewActive(false)
|
||||
setSidePanel(SidePanel.Subagents)
|
||||
},
|
||||
@@ -578,7 +588,7 @@ const AgentManagerContent: Component = () => {
|
||||
}
|
||||
const openWindow = metrics.click("open_worktree_window", "tab_toolbar", openWorktreeDirectory)
|
||||
const togglePRPanel = () => {
|
||||
setHistory(false)
|
||||
closeHistory()
|
||||
if (reviewActive()) closeReviewTab()
|
||||
const opening = sidePanel() !== SidePanel.PR
|
||||
setSidePanel((prev) => (prev === SidePanel.PR ? null : SidePanel.PR))
|
||||
@@ -741,10 +751,6 @@ const AgentManagerContent: Component = () => {
|
||||
|
||||
const localSet = createMemo(() => new Set(localSessionIDs()))
|
||||
|
||||
const unassignedSessions = createMemo(() =>
|
||||
filterUnassignedSessions(session.sessions(), worktreeSessionIds(), localSet()),
|
||||
)
|
||||
|
||||
const projectSessionsLive = createProjectSessionsLive({
|
||||
base: projectLive.sessions,
|
||||
pid: currentProjectId,
|
||||
@@ -754,6 +760,15 @@ const AgentManagerContent: Component = () => {
|
||||
locals: localSet,
|
||||
})
|
||||
|
||||
/** Session ids shown in the project-scoped history view (every session of the project). */
|
||||
const historySessionIds = createMemo(() => {
|
||||
const pid = historyProject()
|
||||
if (!pid || !multiProject()) return undefined
|
||||
const sessions = projectSessionsLive()[pid]
|
||||
if (!sessions) return undefined
|
||||
return new Set(sessions.filter(isKnownRootSession).map((s) => s.id))
|
||||
})
|
||||
|
||||
const localSessions = createLocalSessions({
|
||||
ids: localSessionIDs,
|
||||
sessions: () => {
|
||||
@@ -913,14 +928,12 @@ const AgentManagerContent: Component = () => {
|
||||
|
||||
/** Flat visual order of all visible sidebar items — used for navigation and shortcut assignment. */
|
||||
const sidebarOrder = createMemo(() =>
|
||||
buildSidebarOrder(topLevelItems(), sortedWorktrees(), sections(), worktreesInSection, unassignedSessions()),
|
||||
buildSidebarOrder(topLevelItems(), sortedWorktrees(), sections(), worktreesInSection),
|
||||
)
|
||||
/** Map from sidebar item id → 1-based shortcut number (⌘1 for LOCAL, ⌘2 for first worktree, etc.) */
|
||||
const shortcutMap = createMemo(() => buildShortcutMap(sidebarOrder()))
|
||||
const projectShortcutMap = createMemo(() =>
|
||||
buildShortcutMap(
|
||||
buildProjectNavEntries(projectList(), projectStates(), projectLive.sessions()).map((entry) => ({ id: entry.id })),
|
||||
),
|
||||
buildShortcutMap(buildProjectNavEntries(projectList(), projectStates()).map((entry) => ({ id: entry.id }))),
|
||||
)
|
||||
|
||||
const moveToSection = (ids: string[], sec: string | null) =>
|
||||
@@ -939,18 +952,9 @@ const AgentManagerContent: Component = () => {
|
||||
|
||||
const scrollIntoView = (el: HTMLElement) => el.scrollIntoView({ block: "nearest", behavior: "smooth" })
|
||||
|
||||
const selectUnassigned = (id: string) => {
|
||||
saveTabMemory()
|
||||
setSelection(null)
|
||||
setReviewActive(false)
|
||||
session.selectSession(id)
|
||||
requestChatFocus(true)
|
||||
}
|
||||
|
||||
const focusSidebarItem = (item: { type: string; id: string }) => {
|
||||
if (item.type === "local") selectLocal()
|
||||
else if (item.type === "wt") selectWorktree(item.id)
|
||||
else selectUnassigned(item.id)
|
||||
requestChatFocus(true)
|
||||
const el = document.querySelector(`[data-sidebar-id="${item.id}"]`)
|
||||
if (el instanceof HTMLElement) scrollIntoView(el)
|
||||
@@ -963,7 +967,6 @@ const AgentManagerContent: Component = () => {
|
||||
focus: focusSidebarItem,
|
||||
projects: projectList,
|
||||
states: projectStates,
|
||||
sessions: projectLive.sessions,
|
||||
activeProjectId,
|
||||
selection,
|
||||
currentSessionID: session.currentSessionID,
|
||||
@@ -1027,7 +1030,7 @@ const AgentManagerContent: Component = () => {
|
||||
const current = managedSessions().find((entry) => entry.id === sid)
|
||||
if (current?.worktreeId) return focusManagedSession(current.worktreeId, sid)
|
||||
saveTabMemory()
|
||||
setHistory(false)
|
||||
closeHistory()
|
||||
setReviewActive(false)
|
||||
appendToTabOrder(sel, sid)
|
||||
evictLocal(sid)
|
||||
@@ -1037,7 +1040,7 @@ const AgentManagerContent: Component = () => {
|
||||
|
||||
const focusManagedSession = (worktreeId: string, sid: string) => {
|
||||
selectWorktree(worktreeId)
|
||||
setHistory(false)
|
||||
closeHistory()
|
||||
session.selectSession(sid)
|
||||
requestChatFocus()
|
||||
return true
|
||||
@@ -1063,7 +1066,7 @@ const AgentManagerContent: Component = () => {
|
||||
const focusSidebarSearchItem = (item: SidebarSearchItem) => {
|
||||
if (item.section?.collapsed)
|
||||
vscode.postMessage({ type: "agentManager.toggleSectionCollapsed", sectionId: item.section.id })
|
||||
setHistory(false)
|
||||
closeHistory()
|
||||
if (item.kind === "local") return selectLocal()
|
||||
if (item.kind === "worktree") return selectWorktree(item.worktreeId)
|
||||
if (item.location === "local") selectLocal()
|
||||
@@ -1156,7 +1159,7 @@ const AgentManagerContent: Component = () => {
|
||||
first: () => undefined,
|
||||
close: () => setReviewActive(false),
|
||||
hide: () => setSidePanel(null),
|
||||
history: () => setHistory(false),
|
||||
history: () => closeHistory(),
|
||||
reset: subagents.reset,
|
||||
})
|
||||
}
|
||||
@@ -1172,7 +1175,7 @@ const AgentManagerContent: Component = () => {
|
||||
onMount(() => {
|
||||
const handler = (event: MessageEvent) => {
|
||||
const msg = event.data
|
||||
if (msg?.type === "navigate" && msg.view === "history") return setHistory(true)
|
||||
if (msg?.type === "navigate" && msg.view === "history") return openHistory()
|
||||
if (msg?.type !== "action") return
|
||||
if (msg.action === "sessionPrevious") projectNav.step("up")
|
||||
else if (msg.action === "sessionNext") projectNav.step("down")
|
||||
@@ -1580,7 +1583,9 @@ const AgentManagerContent: Component = () => {
|
||||
applyProjectSelection(msg, {
|
||||
active: (projectId) => activeProjectId() === projectId,
|
||||
applied: (projectId) => currentProjectId() === projectId,
|
||||
managed: (projectId) => projectLive.sessions()[projectId] ?? projectStates()[projectId]?.sessions ?? [],
|
||||
// Managed state is the placement authority; the live listing can briefly
|
||||
// keep a stale worktree tag after a session moved back to local.
|
||||
managed: (projectId) => projectStates()[projectId]?.sessions ?? projectLive.sessions()[projectId] ?? [],
|
||||
local: () => selectLocal(),
|
||||
worktree: (projectId, worktreeId) => selectWorktree(worktreeId),
|
||||
focusLocal: focusLocalSession,
|
||||
@@ -1914,10 +1919,21 @@ const AgentManagerContent: Component = () => {
|
||||
vscode.postMessage({ type: "agentManager.openLocally", sessionId: sid })
|
||||
}
|
||||
|
||||
const openUnassigned = (id: string) => {
|
||||
metrics.track("open_session_locally", "unassigned_session_menu")
|
||||
openLocally(id)
|
||||
}
|
||||
/** History row menu: start a session in a new worktree or back in the project's local tabs. */
|
||||
const historyRowActions = historyRowActionsFactory({
|
||||
t,
|
||||
onPromote: (sessionId) => {
|
||||
metrics.track("promote_session", "history_row")
|
||||
closeHistory()
|
||||
vscode.postMessage({ type: "agentManager.promoteSession", sessionId })
|
||||
},
|
||||
onLocal: (sessionId) => {
|
||||
const pid = historyProject()
|
||||
if (pid) vscode.postMessage({ type: "agentManager.openSessionLocally", projectId: pid, sessionId } as never)
|
||||
else openLocally(sessionId)
|
||||
closeHistory()
|
||||
},
|
||||
})
|
||||
|
||||
const handleAddSession = () => {
|
||||
const sel = selection()
|
||||
@@ -2317,6 +2333,7 @@ const AgentManagerContent: Component = () => {
|
||||
t={t}
|
||||
onSearchRef={(ref) => (sidebarSearchMenu = ref)}
|
||||
onShortcuts={handleShowKeyboardShortcuts}
|
||||
onHistory={openHistory}
|
||||
shortcutMap={projectShortcutMap}
|
||||
/>
|
||||
</Show>
|
||||
@@ -2330,8 +2347,6 @@ const AgentManagerContent: Component = () => {
|
||||
isLocalBusy={isLocalBusy}
|
||||
repoBranch={repoBranch}
|
||||
localStats={localStats}
|
||||
sessionsCollapsed={sessionsCollapsed}
|
||||
toggleSessions={toggleSessions}
|
||||
search={{ items: sidebarSearch.items, current: sidebarSearch.current }}
|
||||
bindings={kb}
|
||||
defaultBranch={repoDefaultBranch}
|
||||
@@ -2345,6 +2360,7 @@ const AgentManagerContent: Component = () => {
|
||||
onNewWorktree={showNewWorktreeDialog}
|
||||
onNewSection={newSection}
|
||||
onShortcuts={metrics.click("keyboard_shortcuts", "worktrees_header", handleShowKeyboardShortcuts)}
|
||||
onHistory={() => openHistory()}
|
||||
projectId={activeProjectId()}
|
||||
sections={sections}
|
||||
sortedWorktrees={sortedWorktrees}
|
||||
@@ -2375,10 +2391,6 @@ const AgentManagerContent: Component = () => {
|
||||
confirmDeleteWorktree={confirmDeleteWorktree}
|
||||
handleDeleteWorktree={handleDeleteWorktree}
|
||||
confirmRemoveStaleWorktree={confirmRemoveStaleWorktree}
|
||||
unassignedSessions={unassignedSessions}
|
||||
selectUnassigned={selectUnassigned}
|
||||
promoteSession={promoteSession}
|
||||
openUnassigned={openUnassigned}
|
||||
track={metrics.click}
|
||||
/>
|
||||
</Show>
|
||||
@@ -2465,7 +2477,7 @@ const AgentManagerContent: Component = () => {
|
||||
<HistoryView
|
||||
onSelectSession={(id) => {
|
||||
if (addSessionToCurrentWorktree(id)) return
|
||||
setHistory(false)
|
||||
closeHistory()
|
||||
if (localSessionIDs().includes(id)) {
|
||||
saveTabMemory()
|
||||
session.selectSession(id)
|
||||
@@ -2483,8 +2495,10 @@ const AgentManagerContent: Component = () => {
|
||||
}
|
||||
openLocally(id)
|
||||
}}
|
||||
onBack={() => setHistory(false)}
|
||||
worktreeSessionIds={activeWorktreeSessionIds}
|
||||
onBack={closeHistory}
|
||||
worktreeSessionIds={historyProject() ? undefined : activeWorktreeSessionIds}
|
||||
sessionIds={historySessionIds}
|
||||
rowActions={historyRowActions}
|
||||
/>
|
||||
</Show>
|
||||
<Show when={showDetailStack()}>
|
||||
@@ -2552,7 +2566,7 @@ const AgentManagerContent: Component = () => {
|
||||
}
|
||||
openLocally(id)
|
||||
}}
|
||||
onShowHistory={() => setHistory(true)}
|
||||
onShowHistory={() => openHistory()}
|
||||
onForkMessage={readOnly() ? undefined : handleForkSession}
|
||||
onForkSession={readOnly() ? undefined : handleForkSession}
|
||||
readonly={readOnly()}
|
||||
|
||||
@@ -47,6 +47,7 @@ interface Props {
|
||||
t: LanguageContextValue["t"]
|
||||
onSearchRef: (ref: SidebarSearchMenuRef) => void
|
||||
onShortcuts: () => void
|
||||
onHistory: (projectId: string) => void
|
||||
shortcutMap?: () => Map<string, number>
|
||||
}
|
||||
|
||||
@@ -202,6 +203,7 @@ export const ProjectList: Component<Props> = (props) => {
|
||||
})
|
||||
}
|
||||
onRemove={(projectId) => vscode.postMessage({ type: "agentManager.removeProject", projectId })}
|
||||
onHistory={props.onHistory}
|
||||
onExpand={(projectId, expanded) =>
|
||||
vscode.postMessage({ type: "agentManager.setProjectExpanded", projectId, expanded })
|
||||
}
|
||||
@@ -228,7 +230,6 @@ export const ProjectList: Component<Props> = (props) => {
|
||||
t={props.t}
|
||||
onSelectLocal={(projectId) => select({ projectId, kind: "local" })}
|
||||
onSelectWorktree={(projectId, worktreeId) => select({ projectId, kind: "worktree", worktreeId })}
|
||||
onSelectSession={(projectId, sessionId) => select({ projectId, kind: "session", sessionId })}
|
||||
onNewWorktree={newWorktree}
|
||||
shortcutMap={props.shortcutMap}
|
||||
/>
|
||||
|
||||
@@ -23,7 +23,6 @@ import { useVSCode } from "../src/context/vscode"
|
||||
import SectionHeader from "./SectionHeader"
|
||||
import { SidebarSectionHeader } from "./SidebarSectionHeader"
|
||||
import { WorktreeItem } from "./WorktreeItem"
|
||||
import { UnassignedSessionsSection } from "./UnassignedSessionsSection"
|
||||
import { ProjectActions } from "./ProjectActions"
|
||||
import { StatsSkeleton, WorktreeSkeleton } from "./Skeleton"
|
||||
import { applyTabOrder, firstOrderedTitle, reorderTabs } from "./tab-order"
|
||||
@@ -55,7 +54,6 @@ interface Props {
|
||||
t: LanguageContextValue["t"]
|
||||
onSelectLocal: (projectId: string) => void
|
||||
onSelectWorktree: (projectId: string, worktreeId: string) => void
|
||||
onSelectSession: (projectId: string, sessionId: string) => void
|
||||
onNewWorktree: (projectId: string) => void
|
||||
shortcutMap?: () => Map<string, number>
|
||||
}
|
||||
@@ -100,14 +98,11 @@ export const ProjectSidebarBody: Component<Props> = (props) => {
|
||||
const sections = () => store.sections()
|
||||
const worktrees = () => store.worktrees()
|
||||
const order = () => store.worktreeOrder()
|
||||
const localSessions = () => sessions(null)
|
||||
const sorted = createMemo(() => sortWorktrees(worktrees(), order()))
|
||||
const members = (sectionId: string) => sorted().filter((wt) => wt.sectionId === sectionId)
|
||||
const ungrouped = createMemo(() => sorted().filter((wt) => !wt.sectionId))
|
||||
const top = createMemo(() => buildTopLevelItems(sections(), ungrouped(), sorted(), order()))
|
||||
const sidebarOrder = createMemo(() =>
|
||||
projectSidebarOrder(top(), sorted(), sections(), members, state()?.sessionsCollapsed ? [] : localSessions()),
|
||||
)
|
||||
const sidebarOrder = createMemo(() => projectSidebarOrder(top(), sorted(), sections(), members))
|
||||
const post = (message: Record<string, unknown>) =>
|
||||
vscode.postMessage({ ...message, projectId: props.project.id } as never)
|
||||
|
||||
@@ -438,23 +433,6 @@ export const ProjectSidebarBody: Component<Props> = (props) => {
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<UnassignedSessionsSection
|
||||
sessions={localSessions}
|
||||
loaded={() => props.sessions !== undefined && state() !== undefined}
|
||||
collapsed={() => state()?.sessionsCollapsed === true}
|
||||
disabled={state() === undefined}
|
||||
active={() => undefined}
|
||||
onToggle={() => {
|
||||
const current = state()
|
||||
if (!current) return
|
||||
post({ type: "agentManager.setSessionsCollapsed", collapsed: !current.sessionsCollapsed })
|
||||
}}
|
||||
onSelect={(sessionId) => props.onSelectSession(props.project.id, sessionId)}
|
||||
onPromote={(sessionId) => post({ type: "agentManager.promoteSession", sessionId })}
|
||||
onOpen={(sessionId) => post({ type: "agentManager.openLocally", sessionId })}
|
||||
sidebarId={(sessionId) => `${props.project.id}:sess:${sessionId}`}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ interface ProjectsSectionProps {
|
||||
onSelect: (id: string) => void
|
||||
onRemove: (id: string) => void
|
||||
onExpand: (id: string, expanded: boolean) => void
|
||||
onHistory: (id: string) => void
|
||||
count: (id: string) => number | undefined
|
||||
tools?: JSX.Element
|
||||
body: (project: AgentProjectSnapshot) => JSX.Element
|
||||
@@ -69,18 +70,30 @@ export const ProjectsSection: Component<ProjectsSectionProps> = (props) => (
|
||||
</>
|
||||
}
|
||||
actions={
|
||||
<Show when={!project().pinned}>
|
||||
<div class="am-project-actions-row">
|
||||
<IconButton
|
||||
icon="close-small"
|
||||
icon="history"
|
||||
size="small"
|
||||
variant="ghost"
|
||||
label={props.t("agentManager.project.remove")}
|
||||
aria-label={props.t("session.showHistory")}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation()
|
||||
props.onRemove(project().id)
|
||||
props.onHistory(project().id)
|
||||
}}
|
||||
/>
|
||||
</Show>
|
||||
<Show when={!project().pinned}>
|
||||
<IconButton
|
||||
icon="close-small"
|
||||
size="small"
|
||||
variant="ghost"
|
||||
label={props.t("agentManager.project.remove")}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation()
|
||||
props.onRemove(project().id)
|
||||
}}
|
||||
/>
|
||||
</Show>
|
||||
</div>
|
||||
}
|
||||
onToggle={() => {
|
||||
if (project().missing) return
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
/** @jsxImportSource solid-js */
|
||||
|
||||
import type { Component } from "solid-js"
|
||||
import { IconButton } from "@kilocode/kilo-ui/icon-button"
|
||||
import { Tooltip } from "@kilocode/kilo-ui/tooltip"
|
||||
import type { LanguageContextValue } from "../src/context/language"
|
||||
|
||||
interface Props {
|
||||
t: LanguageContextValue["t"]
|
||||
/** Move the session into a freshly created worktree. */
|
||||
onWorktree: () => void
|
||||
/** Move the session back to the project root and open it in the local tabs. */
|
||||
onLocal: () => void
|
||||
}
|
||||
|
||||
/** Direct hover actions for sessions in the Agent Manager history view. */
|
||||
export const SessionRowActions: Component<Props> = (props) => (
|
||||
<>
|
||||
<Tooltip value={props.t("agentManager.session.openInWorktree")} placement="right">
|
||||
<IconButton
|
||||
icon="branch"
|
||||
size="small"
|
||||
variant="ghost"
|
||||
aria-label={props.t("agentManager.session.openInWorktree")}
|
||||
data-slot="session-row-action"
|
||||
onClick={props.onWorktree}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip value={props.t("agentManager.session.openLocally")} placement="right">
|
||||
<IconButton
|
||||
icon="local"
|
||||
size="small"
|
||||
variant="ghost"
|
||||
aria-label={props.t("agentManager.session.openLocally")}
|
||||
data-slot="session-row-action"
|
||||
onClick={props.onLocal}
|
||||
/>
|
||||
</Tooltip>
|
||||
</>
|
||||
)
|
||||
@@ -15,7 +15,6 @@ import type {
|
||||
PRStatus,
|
||||
RunStatus,
|
||||
SectionState,
|
||||
SessionInfo,
|
||||
WorktreeGitStats,
|
||||
WorktreeState,
|
||||
} from "../src/types/messages"
|
||||
@@ -31,7 +30,6 @@ import SectionHeader from "./SectionHeader"
|
||||
import { SidebarSectionHeader } from "./SidebarSectionHeader"
|
||||
import { WorktreeItem } from "./WorktreeItem"
|
||||
import { WorktreeSectionActions } from "./WorktreeSectionActions"
|
||||
import { UnassignedSessionsSection } from "./UnassignedSessionsSection"
|
||||
import { StatsSkeleton, WorktreeSkeleton } from "./Skeleton"
|
||||
import type { SidebarSearchMenuRef } from "./SidebarSearchMenu"
|
||||
|
||||
@@ -48,8 +46,6 @@ export interface SidebarBodyProps {
|
||||
isLocalBusy: () => boolean
|
||||
repoBranch: () => string | undefined
|
||||
localStats: () => LocalGitStats | undefined
|
||||
sessionsCollapsed: () => boolean
|
||||
toggleSessions: () => void
|
||||
search: { items: () => SidebarSearchItem[]; current: () => SidebarSearchItem | undefined }
|
||||
bindings: () => Record<string, string>
|
||||
defaultBranch: () => string
|
||||
@@ -63,6 +59,7 @@ export interface SidebarBodyProps {
|
||||
onNewWorktree: () => void
|
||||
onNewSection: () => void
|
||||
onShortcuts: () => void
|
||||
onHistory: () => void
|
||||
sections: () => SectionState[]
|
||||
sortedWorktrees: () => WorktreeState[]
|
||||
worktrees: () => WorktreeState[]
|
||||
@@ -92,10 +89,6 @@ export interface SidebarBodyProps {
|
||||
confirmDeleteWorktree: (id: string) => void
|
||||
handleDeleteWorktree: (id: string, e: MouseEvent) => void
|
||||
confirmRemoveStaleWorktree: (id: string) => void
|
||||
unassignedSessions: () => SessionInfo[]
|
||||
selectUnassigned: (id: string) => void
|
||||
promoteSession: (id: string) => void
|
||||
openUnassigned: (id: string) => void
|
||||
track: (event: string, source: string, action: () => void) => () => void
|
||||
}
|
||||
|
||||
@@ -184,7 +177,7 @@ export const SidebarBody: Component<SidebarBodyProps> = (props) => {
|
||||
</button>
|
||||
|
||||
{/* WORKTREES section */}
|
||||
<div class={`am-section ${props.sessionsCollapsed() ? "am-section-grow" : ""}`}>
|
||||
<div class="am-section am-section-grow">
|
||||
<SidebarSectionHeader
|
||||
class="am-section-header"
|
||||
label={<span class="am-section-label">{props.t("agentManager.section.worktrees")}</span>}
|
||||
@@ -203,6 +196,7 @@ export const SidebarBody: Component<SidebarBodyProps> = (props) => {
|
||||
onNew={props.onNewWorktree}
|
||||
onSection={props.onNewSection}
|
||||
onShortcuts={props.onShortcuts}
|
||||
onHistory={props.onHistory}
|
||||
onSettings={() =>
|
||||
vscode.postMessage({ type: "openSettingsPanel", tab: "agentManager", projectId: props.projectId })
|
||||
}
|
||||
@@ -448,17 +442,6 @@ export const SidebarBody: Component<SidebarBodyProps> = (props) => {
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<UnassignedSessionsSection
|
||||
sessions={props.unassignedSessions}
|
||||
loaded={props.sessionsLoaded}
|
||||
collapsed={props.sessionsCollapsed}
|
||||
active={() => (props.selection() === null ? props.currentSessionID() : undefined)}
|
||||
onToggle={props.toggleSessions}
|
||||
onSelect={props.selectUnassigned}
|
||||
onPromote={props.promoteSession}
|
||||
onOpen={props.openUnassigned}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -3,8 +3,6 @@ import { For, type Component } from "solid-js"
|
||||
/** Staggered widths so stacked placeholder rows read as a list, not a striped block. */
|
||||
const BRANCH = ["62%", "44%", "70%", "52%"]
|
||||
const SUB = ["38%", "30%", "46%", "34%"]
|
||||
const TITLE = ["70%", "55%", "65%", "48%"]
|
||||
|
||||
const rows = (count: number) => Array.from({ length: count }, (_, index) => index)
|
||||
|
||||
/** Offset each row's pulse so any row count keeps the wave effect. */
|
||||
@@ -33,23 +31,6 @@ export const WorktreeSkeleton: Component<{ count?: number }> = (props) => (
|
||||
</div>
|
||||
)
|
||||
|
||||
/** Placeholder session rows shown until a project's session list arrives. */
|
||||
export const SessionSkeleton: Component<{ count?: number }> = (props) => (
|
||||
<div class="am-skeleton-list">
|
||||
<For each={rows(props.count ?? 3)}>
|
||||
{(index) => (
|
||||
<div class="am-skeleton-session">
|
||||
<div
|
||||
class="am-skeleton-session-title"
|
||||
style={{ width: TITLE[index % TITLE.length], "animation-delay": delay(index) }}
|
||||
/>
|
||||
<div class="am-skeleton-session-time" style={{ "animation-delay": delay(index) }} />
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
)
|
||||
|
||||
/** Placeholder for the two-line git stats column on a local or worktree row. */
|
||||
export const StatsSkeleton: Component = () => (
|
||||
<div class="am-worktree-stats-skeleton">
|
||||
|
||||
@@ -1,92 +0,0 @@
|
||||
import { For, Show, type Accessor, type Component } from "solid-js"
|
||||
import { ContextMenu } from "@kilocode/kilo-ui/context-menu"
|
||||
import { Icon } from "@kilocode/kilo-ui/icon"
|
||||
import { IconButton } from "@kilocode/kilo-ui/icon-button"
|
||||
import { Tooltip } from "@kilocode/kilo-ui/tooltip"
|
||||
import type { SessionInfo } from "../src/types/messages"
|
||||
import { useLanguage } from "../src/context/language"
|
||||
import { formatRelativeDate } from "../src/utils/date"
|
||||
import { SidebarSectionHeader } from "./SidebarSectionHeader"
|
||||
import { SessionSkeleton } from "./Skeleton"
|
||||
|
||||
interface Props {
|
||||
sessions: Accessor<SessionInfo[]>
|
||||
loaded: Accessor<boolean>
|
||||
collapsed: Accessor<boolean>
|
||||
disabled?: boolean
|
||||
active: Accessor<string | undefined>
|
||||
onToggle: () => void
|
||||
onSelect: (id: string) => void
|
||||
onPromote: (id: string) => void
|
||||
onOpen: (id: string) => void
|
||||
sidebarId?: (id: string) => string
|
||||
}
|
||||
|
||||
export const UnassignedSessionsSection: Component<Props> = (props) => {
|
||||
const { t } = useLanguage()
|
||||
|
||||
const promote = (id: string, event: MouseEvent) => {
|
||||
event.stopPropagation()
|
||||
props.onPromote(id)
|
||||
}
|
||||
|
||||
return (
|
||||
<div class={`am-section ${props.collapsed() ? "" : "am-section-grow"}`}>
|
||||
<SidebarSectionHeader
|
||||
class="am-section-header am-section-toggle"
|
||||
expanded={!props.collapsed()}
|
||||
disabled={props.disabled}
|
||||
ariaLabel={t("agentManager.section.sessions")}
|
||||
label={<span class="am-section-label">{t("agentManager.section.sessions")}</span>}
|
||||
onToggle={props.onToggle}
|
||||
/>
|
||||
<Show when={!props.collapsed()}>
|
||||
<div class="am-list">
|
||||
<Show when={props.loaded()} fallback={<SessionSkeleton />}>
|
||||
<For each={props.sessions()}>
|
||||
{(session) => (
|
||||
<ContextMenu>
|
||||
<ContextMenu.Trigger as="div" style={{ display: "contents" }}>
|
||||
<button
|
||||
class={`am-item ${session.id === props.active() ? "am-item-active" : ""}`}
|
||||
data-sidebar-id={props.sidebarId?.(session.id) ?? session.id}
|
||||
onClick={() => props.onSelect(session.id)}
|
||||
>
|
||||
<span class="am-item-title" dir="auto">
|
||||
{session.title || t("agentManager.session.untitled")}
|
||||
</span>
|
||||
<span class="am-item-time">{formatRelativeDate(session.updatedAt)}</span>
|
||||
<div class="am-item-promote">
|
||||
<Tooltip value={t("agentManager.session.openInWorktree")} placement="right">
|
||||
<IconButton
|
||||
icon="branch"
|
||||
size="small"
|
||||
variant="ghost"
|
||||
label={t("agentManager.session.openInWorktree")}
|
||||
onClick={(event: MouseEvent) => promote(session.id, event)}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</button>
|
||||
</ContextMenu.Trigger>
|
||||
<ContextMenu.Portal>
|
||||
<ContextMenu.Content class="am-ctx-menu">
|
||||
<ContextMenu.Item onSelect={() => props.onPromote(session.id)}>
|
||||
<Icon name="branch" size="small" />
|
||||
<ContextMenu.ItemLabel>{t("agentManager.session.openInWorktree")}</ContextMenu.ItemLabel>
|
||||
</ContextMenu.Item>
|
||||
<ContextMenu.Item onSelect={() => props.onOpen(session.id)}>
|
||||
<Icon name="folder" size="small" />
|
||||
<ContextMenu.ItemLabel>{t("agentManager.session.openLocally")}</ContextMenu.ItemLabel>
|
||||
</ContextMenu.Item>
|
||||
</ContextMenu.Content>
|
||||
</ContextMenu.Portal>
|
||||
</ContextMenu>
|
||||
)}
|
||||
</For>
|
||||
</Show>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -5,7 +5,7 @@ import { Show } from "solid-js"
|
||||
import { DropdownMenu } from "@kilocode/kilo-ui/dropdown-menu"
|
||||
import { Icon } from "@kilocode/kilo-ui/icon"
|
||||
import { IconButton } from "@kilocode/kilo-ui/icon-button"
|
||||
import { TooltipKeybind } from "@kilocode/kilo-ui/tooltip"
|
||||
import { Tooltip, TooltipKeybind } from "@kilocode/kilo-ui/tooltip"
|
||||
import type { LanguageContextValue } from "../src/context/language"
|
||||
import { parseBindingTokens } from "./keybind-tokens"
|
||||
import { SidebarSearchMenu, type SidebarSearchMenuRef } from "./SidebarSearchMenu"
|
||||
@@ -26,6 +26,7 @@ interface WorktreeSectionActionsProps {
|
||||
onSection: () => void
|
||||
onShortcuts: () => void
|
||||
onSettings: () => void
|
||||
onHistory: () => void
|
||||
}
|
||||
|
||||
export const WorktreeSectionActions: Component<WorktreeSectionActionsProps> = (props) => (
|
||||
@@ -107,6 +108,15 @@ export const WorktreeSectionActions: Component<WorktreeSectionActionsProps> = (p
|
||||
onClick={props.onShortcuts}
|
||||
/>
|
||||
</TooltipKeybind>
|
||||
<Tooltip value={props.t("session.showHistory")} placement="bottom">
|
||||
<IconButton
|
||||
icon="history"
|
||||
size="small"
|
||||
variant="ghost"
|
||||
aria-label={props.t("session.showHistory")}
|
||||
onClick={props.onHistory}
|
||||
/>
|
||||
</Tooltip>
|
||||
<IconButton
|
||||
icon="settings-gear"
|
||||
size="small"
|
||||
|
||||
@@ -415,12 +415,6 @@ html[data-theme="kilo-vscode"]
|
||||
animation: am-skeleton-pulse 1.5s ease-in-out infinite;
|
||||
}
|
||||
|
||||
/* Collapsible section toggle */
|
||||
|
||||
.am-section-toggle:hover .am-section-label {
|
||||
color: var(--text-base);
|
||||
}
|
||||
|
||||
.am-section-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -1200,88 +1194,6 @@ html[data-theme="kilo-vscode"]
|
||||
border-left-color: var(--border-focus, #007fd4);
|
||||
}
|
||||
|
||||
/* Session list */
|
||||
|
||||
.am-list {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
/* Session items */
|
||||
|
||||
.am-item {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
padding: 6px 10px;
|
||||
border-radius: var(--radius-sm);
|
||||
cursor: pointer;
|
||||
background: none;
|
||||
border: none;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
color: var(--text-base);
|
||||
font-family: inherit;
|
||||
font-size: var(--font-size-base);
|
||||
}
|
||||
|
||||
.am-item:hover {
|
||||
background: var(--surface-inset-base-hover);
|
||||
}
|
||||
|
||||
.am-item:active {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.am-item-active {
|
||||
background: var(--surface-interactive-base);
|
||||
color: var(--text-on-interactive-base);
|
||||
}
|
||||
|
||||
.am-item-title {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
font-weight: 500;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.am-item-time {
|
||||
font-size: var(--font-size-small);
|
||||
white-space: nowrap;
|
||||
color: var(--text-weaker);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.am-item-active .am-item-time {
|
||||
color: var(--text-on-interactive-base);
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
/* Promote button on session rows */
|
||||
|
||||
.am-item-promote {
|
||||
position: absolute;
|
||||
right: 4px;
|
||||
flex-shrink: 0;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.am-item:hover .am-item-time {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.am-item:hover .am-item-promote {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Create worktree button (empty state) */
|
||||
|
||||
.am-worktree-create {
|
||||
@@ -2803,6 +2715,14 @@ body.am-wt-dragging-active * {
|
||||
min-width: 250px;
|
||||
}
|
||||
|
||||
/* Per-project row actions (history, remove) */
|
||||
|
||||
.am-project-actions-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
/* Fixed-width slot so menu items with and without a check mark align. */
|
||||
.am-menu-check {
|
||||
display: flex;
|
||||
@@ -3666,31 +3586,6 @@ body.am-wt-dragging-active * {
|
||||
animation: am-skeleton-pulse 1.5s ease-in-out infinite;
|
||||
}
|
||||
|
||||
/* Session skeleton — matches .am-item layout */
|
||||
.am-skeleton-session {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
padding: 6px 10px;
|
||||
}
|
||||
|
||||
.am-skeleton-session-title {
|
||||
height: 13px;
|
||||
border-radius: 3px;
|
||||
background: var(--text-base);
|
||||
animation: am-skeleton-pulse 1.5s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.am-skeleton-session-time {
|
||||
height: 10px;
|
||||
width: 52px;
|
||||
border-radius: 3px;
|
||||
background: var(--text-base);
|
||||
animation: am-skeleton-pulse 1.5s ease-in-out infinite;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* Tab switcher for New/Import tabs in the dialog */
|
||||
|
||||
.am-tab-switcher {
|
||||
@@ -4624,14 +4519,12 @@ body.vscode-high-contrast-light {
|
||||
|
||||
/* Sidebar items */
|
||||
.am-local-item,
|
||||
.am-worktree-item,
|
||||
.am-item {
|
||||
.am-worktree-item {
|
||||
border: 1px solid var(--vscode-contrastBorder, transparent);
|
||||
}
|
||||
|
||||
.am-local-item-active,
|
||||
.am-worktree-item-active,
|
||||
.am-item-active {
|
||||
.am-worktree-item-active {
|
||||
border-color: var(--vscode-contrastBorder, transparent);
|
||||
}
|
||||
|
||||
@@ -4808,7 +4701,6 @@ body.vscode-high-contrast-light {
|
||||
/* Focus states */
|
||||
.am-local-item:focus-visible,
|
||||
.am-worktree-item:focus-visible,
|
||||
.am-item:focus-visible,
|
||||
.am-tab:focus-visible,
|
||||
.am-worktree-create:focus-visible,
|
||||
.am-nv-pill:focus-visible,
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
/** @jsxImportSource solid-js */
|
||||
|
||||
import type { JSX } from "solid-js"
|
||||
import type { LanguageContextValue } from "../src/context/language"
|
||||
import type { SessionInfo } from "../src/types/messages"
|
||||
import { SessionRowActions } from "./SessionRowActions"
|
||||
|
||||
/**
|
||||
* Per-row actions for the Agent Manager sessions view.
|
||||
* `onLocal` and `onPromote` close over the app's scoped-project handlers.
|
||||
*/
|
||||
export function historyRowActions(opts: {
|
||||
t: LanguageContextValue["t"]
|
||||
onPromote: (sessionId: string) => void
|
||||
onLocal: (sessionId: string) => void
|
||||
}): (entry: SessionInfo) => JSX.Element {
|
||||
return (entry) => (
|
||||
<SessionRowActions t={opts.t} onWorktree={() => opts.onPromote(entry.id)} onLocal={() => opts.onLocal(entry.id)} />
|
||||
)
|
||||
}
|
||||
@@ -25,16 +25,6 @@ export function canOpenRootSession(id: string, sessions: Pick<SessionLike, "id"
|
||||
return !!session && isKnownRootSession(session)
|
||||
}
|
||||
|
||||
export function filterUnassignedSessions<T extends SessionLike>(
|
||||
sessions: T[],
|
||||
worktree: Set<string>,
|
||||
local: Set<string>,
|
||||
): T[] {
|
||||
return [...sessions]
|
||||
.filter((s) => isKnownRootSession(s) && !worktree.has(s.id) && !local.has(s.id))
|
||||
.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime())
|
||||
}
|
||||
|
||||
export function resolveNavigation(direction: "up" | "down", current: string | undefined, ids: string[]): NavResult {
|
||||
// Determine current position: -1 = local, 0..N-1 = session index
|
||||
if (!current) {
|
||||
@@ -166,22 +156,18 @@ export interface ProjectNavInput {
|
||||
/** Persisted top-level order containing worktree and section IDs. */
|
||||
worktreeOrder?: string[]
|
||||
sections: { id: string; collapsed: boolean }[]
|
||||
sessionsCollapsed: boolean
|
||||
/** Visible unassigned (root, no worktree) sessions in render order. */
|
||||
unassigned: { id: string }[]
|
||||
}
|
||||
|
||||
export const localNavId = (projectId: string) => `${projectId}:local`
|
||||
export const worktreeNavId = (projectId: string, worktreeId: string) => `${projectId}:wt:${worktreeId}`
|
||||
export const sessionNavId = (projectId: string, sessionId: string) => `${projectId}:sess:${sessionId}`
|
||||
|
||||
/**
|
||||
* Build one global visual order across expanded projects.
|
||||
*
|
||||
* For each expanded project (in input order): Local, then ungrouped worktrees,
|
||||
* then members of each non-collapsed section in top-level order, then visible
|
||||
* unassigned sessions. This matches `buildTopLevelItems` and the project body.
|
||||
* Collapsed projects contribute nothing.
|
||||
* then members of each non-collapsed section in top-level order. This matches
|
||||
* `buildTopLevelItems` and the project body. Collapsed projects contribute
|
||||
* nothing.
|
||||
*/
|
||||
export function buildProjectNavOrder(projects: ProjectNavInput[]): NavEntry[] {
|
||||
const order: NavEntry[] = []
|
||||
@@ -211,11 +197,6 @@ export function buildProjectNavOrder(projects: ProjectNavInput[]): NavEntry[] {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!p.sessionsCollapsed) {
|
||||
for (const s of p.unassigned) {
|
||||
order.push({ id: sessionNavId(pid, s.id), target: { projectId: pid, kind: "session", sessionId: s.id } })
|
||||
}
|
||||
}
|
||||
}
|
||||
return order
|
||||
}
|
||||
|
||||
@@ -6,24 +6,26 @@ import {
|
||||
resolveProjectNav,
|
||||
localNavId,
|
||||
worktreeNavId,
|
||||
sessionNavId,
|
||||
type NavEntry,
|
||||
type NavTarget,
|
||||
LOCAL,
|
||||
} from "./navigate"
|
||||
import type { SidebarItem } from "./section-helpers"
|
||||
import type { AgentManagerStateMessage, AgentProjectSnapshot, ProjectSessionInfo } from "../src/types/messages"
|
||||
import type { AgentManagerStateMessage, AgentProjectSnapshot } from "../src/types/messages"
|
||||
|
||||
/**
|
||||
* Sidebar keyboard-nav controller for the Agent Manager.
|
||||
*
|
||||
* Handles previous/next (⌘⌥↑/↓) and numeric-shortcut (⌘1-9) navigation across
|
||||
* the sidebar. In multi-project mode it builds one global visual order across
|
||||
* every expanded project — Local, ungrouped worktrees, then section members,
|
||||
* and visible unassigned sessions — using stable project-qualified composite
|
||||
* ids, and activates each target with a single atomic
|
||||
* `agentManager.activateSelection` dispatch. In single-project mode it keeps
|
||||
* the legacy in-process traversal over the active project's flat sidebar order.
|
||||
* every expanded project — Local, ungrouped worktrees, then section members —
|
||||
* using stable project-qualified composite ids, and activates each target with
|
||||
* a single atomic `agentManager.activateSelection` dispatch. In single-project
|
||||
* mode it keeps the legacy in-process traversal over the active project's flat
|
||||
* sidebar order.
|
||||
*
|
||||
* Sessions are reachable through the history view, not the tree, so they are
|
||||
* not part of the nav order.
|
||||
*
|
||||
* The pure order/resolution logic lives in {@link navigate.ts} so it stays
|
||||
* solid/DOM-free and unit-testable; this module owns the reactive wiring and
|
||||
@@ -31,13 +33,12 @@ import type { AgentManagerStateMessage, AgentProjectSnapshot, ProjectSessionInfo
|
||||
*/
|
||||
export interface ProjectNavDeps {
|
||||
multiProject: Accessor<boolean>
|
||||
/** Legacy flat sidebar order for single-project mode (LOCAL, worktrees, sessions). */
|
||||
/** Legacy flat sidebar order for single-project mode (LOCAL, worktrees). */
|
||||
sidebarOrder: Accessor<SidebarItem[]>
|
||||
/** Legacy in-process activator for single-project mode. */
|
||||
focus: (item: SidebarItem) => void
|
||||
projects: Accessor<AgentProjectSnapshot[]>
|
||||
states: Accessor<Record<string, AgentManagerStateMessage>>
|
||||
sessions: Accessor<Record<string, ProjectSessionInfo[]>>
|
||||
activeProjectId: Accessor<string | undefined>
|
||||
selection: Accessor<typeof LOCAL | string | null>
|
||||
currentSessionID: Accessor<string | undefined>
|
||||
@@ -52,13 +53,12 @@ export interface ProjectNav {
|
||||
export function buildProjectNavEntries(
|
||||
projects: AgentProjectSnapshot[],
|
||||
states: Record<string, AgentManagerStateMessage>,
|
||||
sessions: Record<string, ProjectSessionInfo[]>,
|
||||
): NavEntry[] {
|
||||
return buildProjectNavOrder(
|
||||
projects.map((p) => {
|
||||
const st = states[p.id]
|
||||
if (!st) {
|
||||
return { id: p.id, expanded: false, worktrees: [], sections: [], sessionsCollapsed: false, unassigned: [] }
|
||||
return { id: p.id, expanded: false, worktrees: [], sections: [] }
|
||||
}
|
||||
return {
|
||||
id: p.id,
|
||||
@@ -66,8 +66,6 @@ export function buildProjectNavEntries(
|
||||
worktrees: (st.worktrees ?? []).map((w) => ({ id: w.id, sectionId: w.sectionId, groupId: w.groupId })),
|
||||
worktreeOrder: st.worktreeOrder,
|
||||
sections: (st.sections ?? []).map((s) => ({ id: s.id, collapsed: s.collapsed })),
|
||||
sessionsCollapsed: st.sessionsCollapsed === true,
|
||||
unassigned: (sessions[p.id] ?? []).filter((s) => s.worktreeId === null).map((s) => ({ id: s.id })),
|
||||
}
|
||||
}),
|
||||
)
|
||||
@@ -77,7 +75,7 @@ export function buildProjectNavEntries(
|
||||
export const navSelector = (target: NavTarget): string => {
|
||||
if (target.kind === "local") return `[data-sidebar-id="${target.projectId}:local"]`
|
||||
if (target.kind === "worktree") return `[data-sidebar-id="${target.projectId}:${target.worktreeId}"]`
|
||||
return `[data-sidebar-id="${sessionNavId(target.projectId, target.sessionId)}"]`
|
||||
return `[data-sidebar-id="${target.projectId}:sess:${target.sessionId}"]`
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -97,9 +95,7 @@ export function createProjectNav(
|
||||
): ProjectNav {
|
||||
const projectOrder = createMemo((): NavEntry[] => {
|
||||
if (!deps.multiProject()) return []
|
||||
const states = deps.states()
|
||||
const live = deps.sessions()
|
||||
return buildProjectNavEntries(deps.projects(), states, live)
|
||||
return buildProjectNavEntries(deps.projects(), deps.states())
|
||||
})
|
||||
|
||||
const currentId = createMemo((): string | undefined => {
|
||||
@@ -109,10 +105,8 @@ export function createProjectNav(
|
||||
const sel = deps.selection()
|
||||
if (sel === LOCAL) return localNavId(pid)
|
||||
if (typeof sel === "string") return worktreeNavId(pid, sel)
|
||||
if (sel === null) {
|
||||
const sid = deps.currentSessionID()
|
||||
if (sid) return sessionNavId(pid, sid)
|
||||
}
|
||||
// A null selection with an open local session is "on local" for nav.
|
||||
if (sel === null && deps.currentSessionID()) return localNavId(pid)
|
||||
return undefined
|
||||
})
|
||||
|
||||
|
||||
@@ -111,14 +111,13 @@ export function buildTopLevelItems(
|
||||
/**
|
||||
* Build the flat visual order of all sidebar items matching what the user sees.
|
||||
* LOCAL is always first, then worktrees in visual order (ungrouped first, then sections,
|
||||
* skipping collapsed sections), then unassigned sessions.
|
||||
* skipping collapsed sections). Sessions are reachable through the history view, not the tree.
|
||||
*/
|
||||
export function buildSidebarOrder(
|
||||
items: TopLevelItem[],
|
||||
sorted: WorktreeState[],
|
||||
sections: SectionState[],
|
||||
members: (id: string) => WorktreeState[],
|
||||
sessions: { id: string }[],
|
||||
): SidebarItem[] {
|
||||
const result: SidebarItem[] = [{ type: "local", id: "local" }]
|
||||
if (sections.length > 0) {
|
||||
@@ -138,9 +137,6 @@ export function buildSidebarOrder(
|
||||
result.push({ type: "wt", id: wt.id })
|
||||
}
|
||||
}
|
||||
for (const s of sessions) {
|
||||
result.push({ type: "session", id: s.id })
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* Contains a source tab bar and an always-visible "Import session" button.
|
||||
*/
|
||||
|
||||
import { Component, Show, createEffect, createSignal, onCleanup, type Accessor } from "solid-js"
|
||||
import { Component, Show, createEffect, createSignal, onCleanup, type Accessor, type JSX } from "solid-js"
|
||||
import { Button } from "@kilocode/kilo-ui/button"
|
||||
import { useDialog } from "@kilocode/kilo-ui/context/dialog"
|
||||
import { useLanguage } from "../../context/language"
|
||||
@@ -13,11 +13,16 @@ import { useLocalTabs } from "../../context/local-tabs"
|
||||
import { CloudImportDialog } from "../chat/CloudImportDialog"
|
||||
import SessionList from "./SessionList"
|
||||
import CloudSessionList from "./CloudSessionList"
|
||||
import type { SessionInfo } from "../../types/messages"
|
||||
|
||||
interface HistoryViewProps {
|
||||
onSelectSession: (id: string) => void
|
||||
onBack?: () => void
|
||||
worktreeSessionIds?: Accessor<ReadonlySet<string> | undefined>
|
||||
/** Filter the Local tab to these session ids. */
|
||||
sessionIds?: Accessor<ReadonlySet<string> | undefined>
|
||||
/** Extra per-row actions rendered in the Local tab. */
|
||||
rowActions?: (session: SessionInfo) => JSX.Element
|
||||
}
|
||||
|
||||
type Source = "local" | "cloud" | "worktree"
|
||||
@@ -159,7 +164,13 @@ const HistoryView: Component<HistoryViewProps> = (props) => {
|
||||
aria-labelledby="history-tab-local"
|
||||
hidden={tab() !== "local"}
|
||||
>
|
||||
{tab() === "local" && <SessionList onSelectSession={props.onSelectSession} />}
|
||||
{tab() === "local" && (
|
||||
<SessionList
|
||||
onSelectSession={props.onSelectSession}
|
||||
sessionIds={props.sessionIds}
|
||||
rowActions={props.rowActions}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
class="history-view-content"
|
||||
|
||||
@@ -38,7 +38,9 @@ function dateGroupKey(iso: string): (typeof DATE_GROUP_KEYS)[number] {
|
||||
|
||||
interface SessionListProps {
|
||||
onSelectSession: (id: string) => void
|
||||
sessionIds?: Accessor<ReadonlySet<string>>
|
||||
sessionIds?: Accessor<ReadonlySet<string> | undefined>
|
||||
/** Extra per-row actions rendered after rename/delete (e.g. Agent Manager menus). */
|
||||
rowActions?: (session: SessionInfo) => JSX.Element
|
||||
}
|
||||
|
||||
const SessionList: Component<SessionListProps> = (props) => {
|
||||
@@ -159,6 +161,7 @@ const SessionList: Component<SessionListProps> = (props) => {
|
||||
aria-label={label(language.t("session.delete.title"), item)}
|
||||
onClick={(event) => confirmDelete(item, event.currentTarget)}
|
||||
/>
|
||||
<Show when={props.rowActions}>{props.rowActions?.(item)}</Show>
|
||||
</>
|
||||
}
|
||||
>
|
||||
|
||||
@@ -1550,6 +1550,7 @@ export const MultiProjectSidebar: Story = {
|
||||
t={t}
|
||||
onSearchRef={() => {}}
|
||||
onShortcuts={() => {}}
|
||||
onHistory={() => {}}
|
||||
/>
|
||||
</div>
|
||||
</StoryProviders>
|
||||
|
||||
@@ -697,6 +697,13 @@ export interface OpenLocallyRequest {
|
||||
sessionId: string
|
||||
}
|
||||
|
||||
// Move a worktree-bound session back to the project root and open it in the local tabs
|
||||
export interface OpenSessionLocallyRequest {
|
||||
type: "agentManager.openSessionLocally"
|
||||
projectId?: string
|
||||
sessionId: string
|
||||
}
|
||||
|
||||
// Add a new session to an existing worktree
|
||||
export interface AddSessionToWorktreeRequest {
|
||||
type: "agentManager.addSessionToWorktree"
|
||||
@@ -1596,6 +1603,7 @@ export type WebviewMessage =
|
||||
| RemoveStaleWorktreeRequest
|
||||
| PromoteSessionRequest
|
||||
| OpenLocallyRequest
|
||||
| OpenSessionLocallyRequest
|
||||
| AddSessionToWorktreeRequest
|
||||
| ForkSessionRequest
|
||||
| SidebarForkSessionRequest
|
||||
|
||||
Reference in New Issue
Block a user