mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-09-17 06:31:43 +08:00
Twelve findings from a review of this PR against the recent gh, worktree,
staleness, and pooling work. The theme is that the hardening landed on the
paths this PR touched, while adjacent paths kept the old semantics.
VS Code:
- The batched GraphQL lookup became the primary PR path after 81b9c21c24,
and it bypassed everything this PR added: a 20s budget instead of
BUDGET.gh, no timeout classification, and no quarantine or health gate,
so a parked worktree still cost a git process per full sync and could
become the working directory of the whole `gh api graphql` call. It now
runs on the shared budgets, skips parked worktrees, and reports a
timeout as a timeout instead of retrying it as an unsupported field.
- Orphan reporting re-checks git registration before naming a directory,
so a worktree the pool created mid-pass is no longer offered for
deletion, and an unanswerable re-check reports nothing.
- Restoring a worktree clears its PR polling backoff, instead of leaving
the worktree the user just repaired parked for up to half an hour.
- The orphan kind reaches the webview: a directory that still holds a
checkout is marked and gets a confirmation that says it may hold
uncommitted work, rather than "nothing here is tracked by git".
- Removing a stale entry no longer dead-ends when terminals cannot be
stopped. Nothing on that path deletes files, so for an unregistered
worktree the only action offered used to fail with an unrelated error.
- PR import runs gh on BUDGET.gh and reports a hang as a timeout.
- findWorktreeByPath and prepareWorktreePath compare with pathKey, so a
symlinked parent or a case variant cannot hide a registration.
- Quarantine gained a non-mutating peek, so generating a diagnostics
report no longer releases the quarantines it is reporting.
- The stats loop gained the same per-worktree backoff as the PR loop; an
explicit refresh still measures a parked worktree.
JetBrains:
- A poll prunes stale worktree metadata only when no mutation holds the
repository lock, and never waits for it. `git worktree add` registers a
worktree before its checkout is written, so pruning underneath one
could delete the metadata of a worktree being created.
- `git status` is off the 5s probe budget: it scans the working tree, so
on a large or cold worktree the probe budget reported unavailable for a
measurement that would have succeeded. The sync prune uses the write
budget its docstring already claimed.
509 lines
24 KiB
TypeScript
509 lines
24 KiB
TypeScript
import { For, Show, createMemo, createSignal, type Component } from "solid-js"
|
||
import { Icon } from "@kilocode/kilo-ui/icon"
|
||
import {
|
||
DragDropProvider,
|
||
DragDropSensors,
|
||
DragOverlay,
|
||
SortableProvider,
|
||
createSortable,
|
||
type DragEvent,
|
||
} from "@thisbeyond/solid-dnd"
|
||
import type {
|
||
LocalGitStats,
|
||
ManagedSessionState,
|
||
PRStatus,
|
||
RunStatus,
|
||
SectionState,
|
||
WorktreeGitStats,
|
||
WorktreeState,
|
||
} from "../src/types/messages"
|
||
import type { LanguageContextValue } from "../src/context/language"
|
||
import type { SidebarSearchItem } from "./sidebar-search"
|
||
import { LOCAL, adjacentHint } from "./navigate"
|
||
import { applyTabOrder, reorderTabs } from "./tab-order"
|
||
import { buildTopLevelItems, isGroupEnd, isGroupStart, isGrouped } from "./section-helpers"
|
||
import { createWorktreeCompletion } from "./worktree-completion"
|
||
import { worktreeDropReference } from "./worktree-references"
|
||
import { beginPromptMentionDrop, endPromptMentionDrop } from "../src/utils/prompt-mention-drop"
|
||
import { outsideSidebar, sectionAwareDetector } from "./section-dnd"
|
||
import { ConstrainDragXAxis } from "./constrain-drag-x"
|
||
import { useVSCode } from "../src/context/vscode"
|
||
import { OrphanNotice } from "./OrphanNotice"
|
||
import type { OrphanDirectory } from "./project/store"
|
||
import SectionHeader from "./SectionHeader"
|
||
import { SidebarSectionHeader } from "./SidebarSectionHeader"
|
||
import { WorktreeItem, actionable } from "./WorktreeItem"
|
||
import { useBaseUpdate } from "./update-from-base"
|
||
import { WorktreeSectionActions } from "./WorktreeSectionActions"
|
||
import { StatsSkeleton, WorktreeSkeleton } from "./Skeleton"
|
||
import type { SidebarSearchMenuRef } from "./SidebarSearchMenu"
|
||
import { LocalActivity } from "../src/components/shared/ActivityIcon"
|
||
import { label, type Activity } from "../src/utils/session-activity"
|
||
|
||
const isMac = typeof navigator !== "undefined" && /Mac|iPhone|iPad/.test(navigator.userAgent)
|
||
|
||
/** Everything the legacy single-project sidebar body reads from the app. */
|
||
export interface SidebarBodyProps {
|
||
t: LanguageContextValue["t"]
|
||
projectId?: string
|
||
selection: () => string | null
|
||
currentSessionID: () => string | undefined
|
||
selectLocal: () => void
|
||
selectWorktree: (id: string) => void
|
||
onOpenComments?: (id: string) => void
|
||
onOpenPR?: (id: string) => void
|
||
activityFor: (id: string | null) => Activity
|
||
repoBranch: () => string | undefined
|
||
localStats: () => LocalGitStats | undefined
|
||
search: { items: () => SidebarSearchItem[]; current: () => SidebarSearchItem | undefined }
|
||
bindings: () => Record<string, string>
|
||
defaultBranch: () => string
|
||
isGitRepo: () => boolean
|
||
loaded: () => boolean
|
||
worktreesLoaded: () => boolean
|
||
sessionsLoaded: () => boolean
|
||
onSearchRef: (ref: SidebarSearchMenuRef | undefined) => void
|
||
onSearchSelect: (item: SidebarSearchItem) => void
|
||
onCreateWorktree: () => void
|
||
onNewWorktree: () => void
|
||
onNewSection: () => void
|
||
onShortcuts: () => void
|
||
onHistory: () => void
|
||
sections: () => SectionState[]
|
||
sortedWorktrees: () => WorktreeState[]
|
||
sidebarOrder: () => { id: string }[]
|
||
sidebarWorktreeOrder: () => string[]
|
||
setSidebarWorktreeOrder: (fn: (prev: string[]) => string[]) => void
|
||
draggingWorktree: () => string | undefined
|
||
setDraggingWorktree: (id: string | undefined) => void
|
||
moveToSection: (ids: string[], sectionId: string | null) => void
|
||
moveSection: (id: string, dir: -1 | 1) => void
|
||
renamingSection: () => string | null
|
||
setRenamingSection: (id: string | null) => void
|
||
managedSessions: () => ManagedSessionState[]
|
||
worktreeLabel: (wt: WorktreeState) => string
|
||
worktreeSubtitle: (wt: WorktreeState) => string | undefined
|
||
pendingDelete: () => string | null
|
||
busy: (id: string) => boolean
|
||
blocked: (id: string) => boolean
|
||
isStaleWorktree: (id: string) => boolean
|
||
/** Why an unhealthy worktree is unhealthy, when known. */
|
||
worktreeHealth?: (id: string) => "absent-restorable" | "absent-gone" | "unregistered" | "unavailable" | undefined
|
||
/** Leftover folders under `.kilo/worktrees/` that no worktree claims. */
|
||
orphanDirectories?: () => OrphanDirectory[]
|
||
/** Restore a deleted worktree folder from its branch. */
|
||
onRestoreWorktree?: (id: string) => void
|
||
/** Drop the entry but move its sessions to Local. */
|
||
onRemoveStaleKeepSessions?: (id: string) => void
|
||
shortcutMap: () => Map<string, number>
|
||
worktreeStats: () => Record<string, WorktreeGitStats>
|
||
prStatuses: () => Record<string, PRStatus | null>
|
||
runStatuses: () => Record<string, RunStatus>
|
||
cancelPendingDelete: () => void
|
||
handleDeleteWorktree: (id: string, e: MouseEvent) => void
|
||
confirmRemoveStaleWorktree: (id: string) => void
|
||
track: (event: string, source: string, action: () => void) => () => void
|
||
}
|
||
|
||
/** Legacy single-project sidebar body: local repo, worktrees, unassigned sessions. */
|
||
export const SidebarBody: Component<SidebarBodyProps> = (props) => {
|
||
const completion = createWorktreeCompletion(props.sortedWorktrees, () => props.projectId, props.worktreeLabel)
|
||
const sorted = completion.rows
|
||
const ungrouped = createMemo(() => sorted().filter((wt) => !wt.sectionId))
|
||
const top = createMemo(() =>
|
||
buildTopLevelItems(props.sections(), ungrouped(), sorted(), props.sidebarWorktreeOrder()),
|
||
)
|
||
const vscode = useVSCode()
|
||
const updateBase = useBaseUpdate()
|
||
const localState = () => props.activityFor(null)
|
||
// Captured at worktree drag start so a release outside the sidebar, or a drop
|
||
// on the prompt, can undo a reorder applied while passing over sibling rows.
|
||
let origin: string[] | undefined
|
||
|
||
return (
|
||
<>
|
||
{/* Local repo item */}
|
||
<button
|
||
class={`am-local-item ${props.selection() === LOCAL ? "am-local-item-active" : ""}`}
|
||
data-sidebar-id="local"
|
||
onClick={() => props.selectLocal()}
|
||
>
|
||
<LocalActivity state={localState()} label={props.t(label(localState()))} />
|
||
<div class="am-local-text">
|
||
<span class="am-local-label">{props.t("agentManager.local")}</span>
|
||
<Show when={props.repoBranch()}>
|
||
<span class="am-local-branch">{props.repoBranch()}</span>
|
||
</Show>
|
||
</div>
|
||
<div class="am-wt-actions-cell">
|
||
<Show when={props.localStats() === undefined}>
|
||
<StatsSkeleton />
|
||
</Show>
|
||
<Show
|
||
when={
|
||
props.localStats() &&
|
||
(props.localStats()!.files > 0 ||
|
||
props.localStats()!.additions > 0 ||
|
||
props.localStats()!.deletions > 0 ||
|
||
props.localStats()!.ahead > 0 ||
|
||
props.localStats()!.behind > 0)
|
||
}
|
||
>
|
||
<div class="am-worktree-stats">
|
||
<Show
|
||
when={props.localStats()!.additions > 0 || props.localStats()!.deletions > 0}
|
||
fallback={
|
||
<Show when={props.localStats()!.files > 0}>
|
||
<span class="am-stat-files">{props.localStats()!.files}f</span>
|
||
</Show>
|
||
}
|
||
>
|
||
<div class="am-worktree-stats-row">
|
||
<Show when={props.localStats()!.additions > 0}>
|
||
<span class="am-stat-additions">+{props.localStats()!.additions}</span>
|
||
</Show>
|
||
<Show when={props.localStats()!.deletions > 0}>
|
||
<span class="am-stat-deletions">
|
||
{"−"}
|
||
{props.localStats()!.deletions}
|
||
</span>
|
||
</Show>
|
||
</div>
|
||
</Show>
|
||
<Show when={props.localStats()!.ahead > 0 || props.localStats()!.behind > 0}>
|
||
<div class="am-worktree-stats-row">
|
||
<Show when={props.localStats()!.ahead > 0}>
|
||
<span class="am-worktree-commits">
|
||
{"↑"}
|
||
{props.localStats()!.ahead}
|
||
</span>
|
||
</Show>
|
||
<Show when={props.localStats()!.behind > 0}>
|
||
<span class="am-worktree-behind">
|
||
{"↓"}
|
||
{props.localStats()!.behind}
|
||
</span>
|
||
</Show>
|
||
</div>
|
||
</Show>
|
||
</div>
|
||
</Show>
|
||
<div class="am-wt-hover-actions">
|
||
<span class="am-shortcut-badge">{isMac ? "⌘" : "Ctrl+"}1</span>
|
||
</div>
|
||
</div>
|
||
</button>
|
||
|
||
{/* WORKTREES section */}
|
||
<div class="am-section am-section-grow">
|
||
<SidebarSectionHeader
|
||
class="am-section-header"
|
||
label={<span class="am-section-label">{props.t("agentManager.section.worktrees")}</span>}
|
||
actions={
|
||
<WorktreeSectionActions
|
||
items={props.search.items}
|
||
current={props.search.current}
|
||
bindings={props.bindings()}
|
||
branch={props.defaultBranch()}
|
||
git={props.isGitRepo()}
|
||
loaded={props.loaded()}
|
||
t={props.t}
|
||
onRef={(value) => props.onSearchRef(value)}
|
||
onSelect={props.onSearchSelect}
|
||
onCreate={props.onCreateWorktree}
|
||
onNew={props.onNewWorktree}
|
||
onSection={props.onNewSection}
|
||
onShortcuts={props.onShortcuts}
|
||
onHistory={props.onHistory}
|
||
onSettings={() =>
|
||
vscode.postMessage({ type: "openSettingsPanel", tab: "agentManager", projectId: props.projectId })
|
||
}
|
||
/>
|
||
}
|
||
/>
|
||
<div class="am-worktree-list">
|
||
<Show when={props.worktreesLoaded() && props.sessionsLoaded()} fallback={<WorktreeSkeleton />}>
|
||
<Show when={!props.isGitRepo()}>
|
||
<div class="am-not-git-notice">
|
||
<Icon name="warning" size="small" />
|
||
<span>{props.t("agentManager.notGitRepo")}</span>
|
||
</div>
|
||
</Show>
|
||
<Show when={props.isGitRepo()}>
|
||
{(() => {
|
||
const [renamingWt, setRenamingWt] = createSignal<string | null>(null)
|
||
const [renameValue, setRenameValue] = createSignal("")
|
||
|
||
const startRename = (wtId: string, current: string) => {
|
||
setRenamingWt(wtId)
|
||
setRenameValue(current)
|
||
}
|
||
|
||
let cancelled = false
|
||
|
||
const commitRename = (wtId: string) => {
|
||
if (cancelled) {
|
||
cancelled = false
|
||
return
|
||
}
|
||
const value = renameValue().trim()
|
||
setRenamingWt(null)
|
||
if (!value) return
|
||
vscode.postMessage({ type: "agentManager.renameWorktree", worktreeId: wtId, label: value })
|
||
}
|
||
|
||
const cancelRename = () => {
|
||
cancelled = true
|
||
setRenamingWt(null)
|
||
}
|
||
|
||
const hasSections = createMemo(() => props.sections().length > 0)
|
||
const wtIds = createMemo(() => sorted().map((wt) => wt.id))
|
||
const secIds = createMemo(() => new Set(props.sections().map((s) => s.id)))
|
||
const home = () => new Map(sorted().map((w) => [w.id, w.sectionId] as const))
|
||
const sectionAware = sectionAwareDetector(secIds, home)
|
||
|
||
const onWtDragStart = (event: DragEvent) => {
|
||
const id = event.draggable?.id
|
||
if (typeof id === "string") {
|
||
props.setDraggingWorktree(id)
|
||
origin = props.sidebarWorktreeOrder()
|
||
const wt = sorted().find((item) => item.id === id)
|
||
if (wt) {
|
||
beginPromptMentionDrop({
|
||
kind: "worktree",
|
||
worktree: worktreeDropReference(
|
||
wt,
|
||
props.worktreeLabel(wt),
|
||
props
|
||
.managedSessions()
|
||
.filter((session) => session.worktreeId === wt.id)
|
||
.map((session) => ({ id: session.id })),
|
||
wt.id === props.selection() || props.isStaleWorktree(wt.id) || props.busy(wt.id),
|
||
),
|
||
})
|
||
}
|
||
}
|
||
document.body.classList.add("am-wt-dragging-active")
|
||
}
|
||
const onWtDragOver = (event: DragEvent) => {
|
||
// Once the card leaves the sidebar it is on its way to the
|
||
// prompt, so stop reordering the list under it.
|
||
if (outsideSidebar(event.draggable)) return
|
||
const from = event.draggable?.id
|
||
const to = event.droppable?.id
|
||
if (typeof from !== "string" || typeof to !== "string") return
|
||
if (secIds().has(to)) return
|
||
props.setSidebarWorktreeOrder((prev) => {
|
||
const cur = applyTabOrder(
|
||
sorted().map((w) => ({ id: w.id })),
|
||
prev,
|
||
).map((item: { id: string }) => item.id)
|
||
return reorderTabs(cur, from, to) ?? prev
|
||
})
|
||
}
|
||
const onWtDragEnd = (event: DragEvent) => {
|
||
const handled = endPromptMentionDrop()
|
||
const from = event.draggable?.id
|
||
const to = event.droppable?.id
|
||
props.setDraggingWorktree(undefined)
|
||
document.body.classList.remove("am-wt-dragging-active")
|
||
// A drop on the prompt inserts a mention. Do not also move the
|
||
// worktree to whatever section happens to be under the pointer.
|
||
// Both this path and an outside release undo the pass-over
|
||
// reorder so the sidebar matches the persisted order.
|
||
if (handled || outsideSidebar(event.draggable)) {
|
||
if (origin) props.setSidebarWorktreeOrder(() => origin!)
|
||
origin = undefined
|
||
return
|
||
}
|
||
origin = undefined
|
||
if (typeof from === "string" && typeof to === "string" && secIds().has(to)) {
|
||
props.moveToSection([from], to)
|
||
return
|
||
}
|
||
vscode.postMessage({ type: "agentManager.setWorktreeOrder", order: props.sidebarWorktreeOrder() })
|
||
}
|
||
|
||
return (
|
||
<DragDropProvider
|
||
onDragStart={onWtDragStart}
|
||
onDragEnd={onWtDragEnd}
|
||
onDragOver={onWtDragOver}
|
||
collisionDetector={sectionAware}
|
||
>
|
||
<DragDropSensors />
|
||
<ConstrainDragXAxis />
|
||
<SortableProvider ids={wtIds()}>
|
||
{(() => {
|
||
const renderWt = (wt: WorktreeState, idx: () => number, list?: WorktreeState[]) => {
|
||
const wtSessions = createMemo(() =>
|
||
props.managedSessions().filter((ms) => ms.worktreeId === wt.id),
|
||
)
|
||
const navHint = () =>
|
||
adjacentHint(
|
||
wt.id,
|
||
props.selection() ?? props.currentSessionID() ?? "",
|
||
props.sidebarOrder().map((f) => f.id),
|
||
props.bindings().previousSession ?? "",
|
||
props.bindings().nextSession ?? "",
|
||
)
|
||
const groupSize = () =>
|
||
!wt.groupId ? 0 : sorted().filter((w) => w.groupId === wt.groupId).length
|
||
const sortable = createSortable(wt.id)
|
||
void sortable
|
||
return (
|
||
<div
|
||
use:sortable
|
||
class={`am-wt-sortable ${sortable.isActiveDraggable ? "am-wt-dragging" : ""}`}
|
||
>
|
||
<WorktreeItem
|
||
completed={completion.completed(wt.id)}
|
||
onCompletionEnd={() => completion.release(wt.id)}
|
||
worktree={wt}
|
||
label={props.worktreeLabel(wt)}
|
||
subtitle={props.worktreeSubtitle(wt)}
|
||
active={props.selection() === wt.id}
|
||
pendingDelete={props.pendingDelete() === wt.id}
|
||
busy={props.busy(wt.id)}
|
||
activity={props.activityFor(wt.id)}
|
||
blocked={props.blocked(wt.id)}
|
||
stale={props.isStaleWorktree(wt.id) || actionable(props.worktreeHealth?.(wt.id))}
|
||
health={props.worktreeHealth?.(wt.id)}
|
||
shortcut={props.shortcutMap().get(wt.id)}
|
||
stats={props.worktreeStats()[wt.id]}
|
||
navHint={navHint()}
|
||
sessions={wtSessions().length}
|
||
grouped={isGrouped(wt)}
|
||
groupStart={isGroupStart(wt, idx(), list ?? sorted())}
|
||
groupEnd={isGroupEnd(wt, idx(), list ?? sorted())}
|
||
groupSize={groupSize()}
|
||
renaming={renamingWt() === wt.id}
|
||
renameValue={renameValue()}
|
||
closeKeybind={props.bindings().closeWorktree ?? ""}
|
||
openKeybind={props.bindings().openWorktree ?? ""}
|
||
pr={
|
||
props.prStatuses()[wt.id] !== undefined
|
||
? (props.prStatuses()[wt.id] ?? undefined)
|
||
: undefined
|
||
}
|
||
runStatus={props.runStatuses()[wt.id]}
|
||
onOpenComments={() => props.onOpenComments?.(wt.id)}
|
||
onOpenPR={props.track("open_pull_request", "worktree_menu", () =>
|
||
props.onOpenPR?.(wt.id),
|
||
)}
|
||
sections={props.sections()}
|
||
currentSectionId={wt.sectionId}
|
||
onMoveToSection={(secId) => props.moveToSection([wt.id], secId)}
|
||
onMoveToNewSection={props.track("new_section", "worktree_menu", () =>
|
||
props.onNewSection(),
|
||
)}
|
||
onClick={() => props.selectWorktree(wt.id)}
|
||
onCancelDelete={props.cancelPendingDelete}
|
||
onDelete={(e) => props.handleDeleteWorktree(wt.id, e)}
|
||
onStartRename={(current) => startRename(wt.id, current)}
|
||
onRenameInput={(v) => setRenameValue(v)}
|
||
onCommitRename={() => commitRename(wt.id)}
|
||
onCancelRename={cancelRename}
|
||
onRemoveStale={() => props.confirmRemoveStaleWorktree(wt.id)}
|
||
onRestore={props.onRestoreWorktree ? () => props.onRestoreWorktree?.(wt.id) : undefined}
|
||
onRemoveKeepSessions={
|
||
props.onRemoveStaleKeepSessions
|
||
? () => props.onRemoveStaleKeepSessions?.(wt.id)
|
||
: undefined
|
||
}
|
||
onUpdateBase={() =>
|
||
updateBase(
|
||
wt.id,
|
||
props.projectId,
|
||
wtSessions().find((item) => item.id === props.currentSessionID())?.id,
|
||
)
|
||
}
|
||
onCopyPath={() => navigator.clipboard.writeText(wt.path)}
|
||
onOpen={props.track("open_worktree_window", "worktree_menu", () =>
|
||
vscode.postMessage({ type: "agentManager.openWorktree", worktreeId: wt.id }),
|
||
)}
|
||
/>
|
||
</div>
|
||
)
|
||
}
|
||
if (hasSections()) {
|
||
const post = vscode.postMessage.bind(vscode)
|
||
return (
|
||
<For each={top()}>
|
||
{(item, idx) => {
|
||
if (item.kind === "section") {
|
||
const sec = item.section
|
||
const members = createMemo(() => sorted().filter((wt) => wt.sectionId === sec.id))
|
||
return (
|
||
<SectionHeader
|
||
section={sec}
|
||
count={members().length}
|
||
autoRename={props.renamingSection() === sec.id}
|
||
onRenameEnd={() => props.setRenamingSection(null)}
|
||
onToggle={() =>
|
||
post({ type: "agentManager.toggleSectionCollapsed", sectionId: sec.id })
|
||
}
|
||
onRename={(name) =>
|
||
post({ type: "agentManager.renameSection", sectionId: sec.id, name })
|
||
}
|
||
onDelete={() => post({ type: "agentManager.deleteSection", sectionId: sec.id })}
|
||
onSetColor={(color) =>
|
||
post({ type: "agentManager.setSectionColor", sectionId: sec.id, color })
|
||
}
|
||
isFirst={idx() === 0}
|
||
isLast={idx() === top().length - 1}
|
||
onMoveUp={() => props.moveSection(sec.id, -1)}
|
||
onMoveDown={() => props.moveSection(sec.id, 1)}
|
||
>
|
||
<Show when={!sec.collapsed}>
|
||
<div class="am-section-group-body">
|
||
<For each={members()}>{(wt, wtIdx) => renderWt(wt, wtIdx, members())}</For>
|
||
</div>
|
||
</Show>
|
||
</SectionHeader>
|
||
)
|
||
}
|
||
const ug = ungrouped()
|
||
const wtIdx = () => ug.indexOf(item.wt)
|
||
return renderWt(item.wt, wtIdx, ug)
|
||
}}
|
||
</For>
|
||
)
|
||
}
|
||
return <For each={sorted()}>{(wt, idx) => renderWt(wt, idx)}</For>
|
||
})()}
|
||
</SortableProvider>
|
||
<DragOverlay>
|
||
{(() => {
|
||
const wt = sorted().find((w) => w.id === props.draggingWorktree())
|
||
if (!wt) return null
|
||
return (
|
||
<div class="am-wt-overlay">
|
||
<Icon name="branch" size="small" />
|
||
<span>{props.worktreeLabel(wt)}</span>
|
||
</div>
|
||
)
|
||
})()}
|
||
</DragOverlay>
|
||
</DragDropProvider>
|
||
)
|
||
})()}
|
||
<Show when={sorted().length === 0}>
|
||
<button class="am-worktree-create" onClick={props.onNewWorktree}>
|
||
<Icon name="plus" size="small" />
|
||
<span>{props.t("agentManager.worktree.new")}</span>
|
||
</button>
|
||
</Show>
|
||
</Show>
|
||
</Show>
|
||
<OrphanNotice
|
||
orphans={props.orphanDirectories?.() ?? []}
|
||
onClean={(paths) => vscode.postMessage({ type: "agentManager.cleanOrphanDirectories", paths })}
|
||
/>
|
||
</div>
|
||
</div>
|
||
</>
|
||
)
|
||
}
|