mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-08-28 19:11:03 +08:00
feat(agent-manager): target new worktrees by project
This commit is contained in:
@@ -0,0 +1,834 @@
|
||||
# Agent Manager — New Worktree Project Selector
|
||||
|
||||
Status: implemented 2026-08-06. The implementation is uncommitted.
|
||||
|
||||
This is Slice 4 item 6 ("Add project-aware New Worktree targeting") from
|
||||
`agent-manager-multi-project-uniform-ui.md`, the last unimplemented item of that slice.
|
||||
`agent-manager-multi-project-runtime.md:29` deferred it out of the backend-first scope.
|
||||
Everything the extension side needs already exists; this is almost entirely a webview
|
||||
change.
|
||||
|
||||
Implementation notes:
|
||||
|
||||
- The project catalog is passed into the dialog as an accessor so the picker reflects
|
||||
registry changes while it remains open.
|
||||
- The per-project default-base resolver returns `undefined` when the project has no
|
||||
configured/local branch, allowing the backend-detected branch response to remain the
|
||||
fallback instead of being replaced by a hardcoded `main`.
|
||||
- Branch, import-result, and worktree-ready messages carry `projectId` in multi-project
|
||||
mode, which makes fast project changes and cross-project creation activation safe.
|
||||
- The slash-command hook accepts optional caller-owned commands; `/project` is scoped to
|
||||
this dialog and is hidden when multi-project mode is unavailable.
|
||||
|
||||
---
|
||||
|
||||
## Problem
|
||||
|
||||
With `kilo-code.new.experimental.multiProject` enabled, the New Worktree dialog has no
|
||||
notion of which repository it targets, and the user cannot see or change it.
|
||||
|
||||
`Cmd+N` opens the dialog with no project at all:
|
||||
|
||||
```tsx
|
||||
// AgentManagerApp.tsx:1870-1876
|
||||
const showNewWorktreeDialog = () => {
|
||||
if (!loaded()) return
|
||||
expandSidebar()
|
||||
dialog.show(() => (
|
||||
<NewWorktreeDialog mode={mode} onClose={() => dialog.close()} defaultBaseBranch={repoDefaultBranch()} />
|
||||
))
|
||||
}
|
||||
```
|
||||
|
||||
`projectId` is `undefined`, so every message the dialog sends omits it
|
||||
(`agentManager.requestBranches` at `NewWorktreeDialog.tsx:321`,
|
||||
`agentManager.createMultiVersion` at `:373`, `agentManager.importFromPR` at `:566`,
|
||||
`agentManager.importFromBranch` at `:575`). The extension then silently falls back to the
|
||||
active project in `messageProject()` (`AgentManagerProvider.ts:474`) before running the
|
||||
message inside `ProjectScope`.
|
||||
|
||||
The result is correct but opaque:
|
||||
|
||||
- The dialog never shows which repository the worktree lands in.
|
||||
- The only way to target a specific project is the per-project `+` button
|
||||
(`ProjectList.tsx:136-146`), which does pass `projectId` explicitly.
|
||||
- `defaultBaseBranch` is resolved from the *active* project
|
||||
(`AgentManagerApp.tsx:261,272`), so even Advanced options' base-branch list and default
|
||||
badge are implicitly single-project.
|
||||
|
||||
The desired behavior, per the original request: the dialog should show the assigned
|
||||
project and let the user change it. Defaulting to the last selected project is fine as a
|
||||
default; it just must be visible and overridable.
|
||||
|
||||
---
|
||||
|
||||
## Design decisions
|
||||
|
||||
### Placement: inline with the tab switcher
|
||||
|
||||
The selector renders inside the New/Import pill row (`NewWorktreeDialog.tsx:620-699`),
|
||||
right-aligned with a constrained width, so it is shared by both tabs without adding a
|
||||
full-width form row.
|
||||
|
||||
```
|
||||
┌─ New Worktree ──────────────────────────────┐
|
||||
│ [ New ] [ Import ] [ folder kilocode ▾ ] │ ← inline, multiProject only
|
||||
│ [ Worktree name (optional) ] │
|
||||
│ [ prompt … ] Code ▾ GPT-5.6 ▾ None ▾ │
|
||||
│ › Advanced options │
|
||||
│ VERSIONS 1 2 3 4 ⧉ Compare Models │
|
||||
│ [ Create Worktree ] │
|
||||
└──────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
Rejected alternatives and why:
|
||||
|
||||
- **Inside Advanced options.** Wrong category. Advanced options holds refinements of a
|
||||
known target (branch name, base branch). The project *is* the target: changing it
|
||||
invalidates the branch list, base branch, default-branch badge, and setup scripts.
|
||||
Hiding it also fails the stated requirement of seeing which project is assigned.
|
||||
- **A full-width row directly beneath the tabs.** It covered both tabs, but consumed
|
||||
unnecessary vertical space and made the project control look like a primary form field.
|
||||
- **In the dialog title** (`New Worktree in [kilocode ▾]`). Reads nicely but requires
|
||||
widening `Dialog`'s `title` prop to accept JSX, which touches kilo-ui for one caller.
|
||||
|
||||
### Visibility
|
||||
|
||||
Render the row only when `multiProject` is true. With the flag off (the default) the
|
||||
dialog stays byte-identical to today, so there is no regression surface for the
|
||||
all-user path. When the flag is on but only the pinned project exists, still render it:
|
||||
showing the target is informative and the requirement is explicitly about seeing the
|
||||
assignment.
|
||||
|
||||
### Default value
|
||||
|
||||
`props.projectId ?? activeProjectId()`. `activeProjectId()` already exists at
|
||||
`AgentManagerApp.tsx:268` (`projectList().find((p) => p.active)?.id ?? currentProjectId()`).
|
||||
|
||||
No new persistence. The active project is already the durable "last selected" state
|
||||
(persisted per project as `activeTarget` in each repo's `.kilo/agent-manager.json`, plus
|
||||
the registry's ordering). Adding a separate "last dialog project" key would create a
|
||||
second source of truth that can disagree with the sidebar.
|
||||
|
||||
### Reuse, no new CSS
|
||||
|
||||
The row uses the existing `am-advanced-field` + `am-nv-config-label` +
|
||||
`am-selector-wrapper` + `am-selector-trigger` markup, i.e. exactly the structure the base
|
||||
branch selector already uses at `NewWorktreeDialog.tsx:813-905`, with `DeferredPopover`
|
||||
(already imported) instead of `BranchSelectPopover`.
|
||||
|
||||
### Component extraction
|
||||
|
||||
`NewWorktreeDialog.tsx` is already 1136 lines. The selector and its popover list go into a
|
||||
new `webview-ui/agent-manager/ProjectSelect.tsx` (roughly `BranchSelect.tsx`'s role):
|
||||
presentational, takes `projects`, `value`, `onSelect`, and labels, and owns nothing but
|
||||
its own list rendering. The dialog keeps only the signal, the popover trigger, and the
|
||||
effects.
|
||||
|
||||
Note: `webview-ui/agent-manager/NewWorktreeDialog.tsx` is not under a `maxLines` cap
|
||||
(`tests/unit/agent-manager-arch.test.ts` caps `src/agent-manager/*.ts` only), but the file
|
||||
is on the arch test's watched list and the caps exist to discourage exactly this kind of
|
||||
growth.
|
||||
|
||||
---
|
||||
|
||||
## Changes
|
||||
|
||||
### 1. `webview-ui/agent-manager/ProjectSelect.tsx` (new)
|
||||
|
||||
Presentational popover body listing projects:
|
||||
|
||||
- Row = project label + dimmed root path (tooltip on the full root, matching
|
||||
`ProjectsSection.tsx:60`).
|
||||
- Check mark on the selected project.
|
||||
- Untrusted and missing projects are disabled and carry the same affordances the accordion
|
||||
uses: `lock` icon + trust hint, `warning` icon + missing hint
|
||||
(`ProjectsSection.tsx:67-75`). Selecting them is not possible; trust happens in the
|
||||
sidebar, not in this dialog. Keeps the dialog free of trust-flow branching.
|
||||
- There is intentionally no Add project action in this picker. Project registration and
|
||||
trust management stay in the Agent Manager Projects toolbar.
|
||||
|
||||
### 2. `webview-ui/agent-manager/NewWorktreeDialog.tsx`
|
||||
|
||||
Props change:
|
||||
|
||||
```ts
|
||||
export const NewWorktreeDialog: Component<{
|
||||
onClose: () => void
|
||||
projectId?: string // now: initial value, not fixed target
|
||||
projects?: AgentProjectSnapshot[] // omitted / empty => single-project, row hidden
|
||||
activeProjectId?: string
|
||||
defaultBase?: (projectId: string) => string // replaces defaultBaseBranch?: string
|
||||
mode: ModeRouter
|
||||
}>
|
||||
```
|
||||
|
||||
`defaultBaseBranch?: string` must become a per-project lookup because each project has its
|
||||
own configured default and its own local branch. `ProjectList.tsx:142` already computes
|
||||
that expression (`state?.defaultBaseBranch ?? props.local[projectId]?.branch`); hoist it
|
||||
into the callback so both call sites share it.
|
||||
|
||||
New state and derived values:
|
||||
|
||||
```ts
|
||||
const [project, setProject] = createSignal(props.projectId ?? props.activeProjectId)
|
||||
const [projectOpen, setProjectOpen] = createSignal(false)
|
||||
const selectable = () => (props.projects ?? []).filter((p) => p.trusted && !p.missing)
|
||||
const showProject = () => (props.projects?.length ?? 0) > 0
|
||||
```
|
||||
|
||||
Every outbound message switches from `props.projectId` to `project()`:
|
||||
`:321` `requestBranches`, `:373` `createMultiVersion`, `:566` `importFromPR`,
|
||||
`:575` `importFromBranch`.
|
||||
|
||||
Reload branch data on project change, replacing the one-shot `onMount` request at `:319-321`:
|
||||
|
||||
```ts
|
||||
createEffect(
|
||||
on(project, (id) => {
|
||||
setBranches([])
|
||||
setBranchSearch("")
|
||||
setHighlightedIndex(0)
|
||||
setBaseBranch(null) // custom base is project-specific
|
||||
setDefaultBranch(props.defaultBase?.(id) ?? "main")
|
||||
setBranchesLoading(true)
|
||||
vscode.postMessage({ type: "agentManager.requestBranches", projectId: id })
|
||||
}),
|
||||
)
|
||||
```
|
||||
|
||||
Drop stale branch replies in the `agentManager.branches` handler (`:520-525`).
|
||||
`AgentManagerBranchesMessage` **already declares an optional `projectId`**
|
||||
(`extension-messages.ts:939-945`, `src/agent-manager/types.ts:293-298`); the field is
|
||||
simply never populated or read today. Without this guard, switching projects twice quickly
|
||||
can race a wrong branch list into the base-branch popover:
|
||||
|
||||
```ts
|
||||
if (ev.projectId && ev.projectId !== project()) return
|
||||
```
|
||||
|
||||
Also replace the `if (!props.defaultBaseBranch) setDefaultBranch(ev.defaultBranch)` guard
|
||||
at `:523` — with a per-project lookup, the guard must consult
|
||||
`props.defaultBase?.(project())` instead of a fixed prop.
|
||||
|
||||
Preserved across a project change (all project-agnostic): prompt text and its
|
||||
`advancedDialogPrompt` persistence, images, name, agent, model, variant, versions, compare
|
||||
allocations, sandbox override.
|
||||
|
||||
Keyboard: add `project` to `WORKTREE_PROMPT_COMMANDS` so `/project` opens the popover,
|
||||
consistent with mode/model/variant/sandbox already being reachable from the dialog's slash
|
||||
menu (`:302-314`). Hide it from the list when `showProject()` is false, using the same
|
||||
`hidden` set mechanism already used for `agents` / `variant` / `sandbox`.
|
||||
|
||||
### 3. `webview-ui/agent-manager/AgentManagerApp.tsx`
|
||||
|
||||
`showNewWorktreeDialog` (`:1870-1876`) passes the catalog and a per-project default
|
||||
resolver instead of a single branch string:
|
||||
|
||||
```tsx
|
||||
<NewWorktreeDialog
|
||||
mode={mode}
|
||||
onClose={() => dialog.close()}
|
||||
projects={multiProject() ? projectList() : undefined}
|
||||
activeProjectId={activeProjectId()}
|
||||
defaultBase={defaultBase}
|
||||
/>
|
||||
```
|
||||
|
||||
where `defaultBase(id)` reads `registry.ensure(id).defaultBaseBranch() ?? registry.ensure(id).localStats()?.branch ?? repoDetectedBranch() ?? "main"`.
|
||||
`registry.ensure` and both store fields already exist
|
||||
(`project/registry.ts:39`, `project/store.ts:62,67,113,123`).
|
||||
|
||||
### 4. `webview-ui/agent-manager/ProjectList.tsx`
|
||||
|
||||
`newWorktree(projectId)` (`:136-146`) passes the same `projects` / `activeProjectId` /
|
||||
`defaultBase` props with `projectId` as the initial value, so the per-project `+` button
|
||||
opens the dialog pre-scoped but still switchable. Its current inline
|
||||
`state?.defaultBaseBranch ?? props.local[projectId]?.branch` expression is replaced by the
|
||||
shared resolver passed down from `AgentManagerApp`.
|
||||
|
||||
### 5. `src/agent-manager/worktree-importer.ts`
|
||||
|
||||
Stamp `projectId` on all three `agentManager.branches` posts (`:27`, `:48`, `:54`). The
|
||||
field is already in the type; the value is available from the ambient `ProjectScope`
|
||||
context the message runs in (`AgentManagerProvider.ts:479`). Without this the stale-reply
|
||||
guard in the webview is inert.
|
||||
|
||||
### 6. Activate the created worktree when the project differs
|
||||
|
||||
Creating in a non-active project currently leaves the sidebar where it is:
|
||||
`createMultiVersion` never activates (no activation call in `provider-multi-version.ts`),
|
||||
and the new worktree just appears in that project's accordion. That is right for the
|
||||
per-project `+` button, but for `Cmd+N` where the user deliberately switched projects,
|
||||
landing in the new worktree is what the flow implies.
|
||||
|
||||
Post an `agentManager.activateSelection` for the first created worktree when the chosen
|
||||
project differs from the active one. `activateSelection` already handles readiness, trust,
|
||||
and stale-target fallback (`project/messages.ts:76-99`), so this is one message, not new
|
||||
machinery. Hook it to the existing `agentManager.worktreeSetup` / `multiVersionProgress`
|
||||
handling in `AgentManagerApp.tsx:1453-1471`, which already carries `projectId`.
|
||||
|
||||
### 7. i18n
|
||||
|
||||
New keys in `webview-ui/agent-manager/i18n/en.ts` (near the existing
|
||||
`agentManager.dialog.*` block at `:130`):
|
||||
|
||||
- `agentManager.dialog.project.select` — "Select project"
|
||||
- `agentManager.dialog.project.untrusted` — "Trust this project in the sidebar first"
|
||||
- `agentManager.dialog.project.missing` — "Repository not found"
|
||||
|
||||
Then translate the four keys into the other 20 locale files in that directory via the
|
||||
`translator` subagent.
|
||||
|
||||
---
|
||||
|
||||
## Implementation order
|
||||
|
||||
1. `ProjectSelect.tsx` with the presentational list, plus i18n keys in `en.ts`.
|
||||
2. Dialog: `project` signal, prop rename to `defaultBase`, route all four outbound
|
||||
messages through `project()`, render the row behind `showProject()`.
|
||||
3. Dialog: `createEffect(on(project, …))` branch reload, base-branch reset, stale-reply
|
||||
guard.
|
||||
4. Call-site updates in `AgentManagerApp.tsx` and `ProjectList.tsx`, shared `defaultBase`
|
||||
resolver.
|
||||
5. `projectId` stamp in `worktree-importer.ts`.
|
||||
6. `/project` slash command.
|
||||
7. Post-create activation when the target project differs.
|
||||
8. Locale fan-out.
|
||||
9. Changeset (`minor`, user-facing): worktree creation targets an explicit project.
|
||||
|
||||
---
|
||||
|
||||
## Tests
|
||||
|
||||
Existing source-text unit tests already assert against this dialog and will need to stay
|
||||
green: `tests/unit/new-worktree-dialog-sandbox.test.ts`,
|
||||
`tests/unit/prompt-input-bidirectional.test.ts`, and the dialog entry in
|
||||
`tests/unit/agent-manager-arch.test.ts`.
|
||||
|
||||
New coverage:
|
||||
|
||||
- The dialog posts `createMultiVersion` / `requestBranches` / `importFromBranch` /
|
||||
`importFromPR` with the *selected* project id, not the prop, after a project change.
|
||||
- A `agentManager.branches` reply carrying a non-current `projectId` does not mutate the
|
||||
branch list (the race guard).
|
||||
- Changing project clears `baseBranch` and re-derives `defaultBranch` from `defaultBase`.
|
||||
- Prompt text survives a project change (no accidental reset through the shared
|
||||
`advancedDialogPrompt` cache).
|
||||
- The row does not render when `projects` is empty, so the single-project dialog is
|
||||
unchanged.
|
||||
- Untrusted and missing projects are not selectable.
|
||||
|
||||
Checks to run before declaring done, from `packages/kilo-vscode/`:
|
||||
`bun run typecheck`, `bun run lint`, `bun run test:unit`, `bun run knip`.
|
||||
|
||||
---
|
||||
|
||||
## Manual verification
|
||||
|
||||
In the isolated harness (`bun run extension:isolated`) with
|
||||
`kilo-code.new.experimental.multiProject` enabled and two repositories registered:
|
||||
|
||||
1. `Cmd+N` from project A shows "Project: A". Switch to B, create, and confirm the
|
||||
worktree lands in B's accordion and the sidebar activates it.
|
||||
2. Switch project with Advanced options open and confirm the base-branch list and default
|
||||
badge follow the new project rather than showing A's branches.
|
||||
3. Switch project rapidly back and forth and confirm the branch list matches the selected
|
||||
project (the race guard).
|
||||
4. Type a prompt, switch project, confirm the prompt is retained.
|
||||
5. Use the Import tab after switching project and confirm branches and PR import target
|
||||
the selected repository.
|
||||
6. Turn the flag off and confirm the dialog is visually identical to today.
|
||||
|
||||
---
|
||||
|
||||
## Out of scope
|
||||
|
||||
- Trusting or removing a project from inside the dialog. Trust stays in the sidebar; the
|
||||
dialog only disables untrusted entries.
|
||||
- Any change to how the active project is persisted.
|
||||
- The quick-create path (`Cmd+Shift+N` → `agentManager.createWorktree`,
|
||||
`AgentManagerApp.tsx:1863-1867`). It has no dialog, so it keeps targeting the active
|
||||
project. Worth revisiting only if the explicit-target rule should apply there too.
|
||||
- Per-project setup-script or agent selection in the dialog.
|
||||
|
||||
## Risks
|
||||
|
||||
- **Stale branch data** is the only real correctness risk, and it is why the `projectId`
|
||||
stamp plus the reply guard are mandatory rather than optional polish.
|
||||
- **Prop signature change** (`defaultBaseBranch: string` → `defaultBase: (id) => string`)
|
||||
touches both call sites; a partial migration would silently show one project's default
|
||||
branch while creating in another.
|
||||
- **Dialog file growth**; mitigated by extracting `ProjectSelect.tsx`.
|
||||
|
||||
---
|
||||
|
||||
# Appendix: exact UI and styling specification
|
||||
|
||||
Everything below is copy-paste ready. Class names, tokens, and icon names are all verified
|
||||
against the current tree. Do not invent new tokens or new class names beyond the ones
|
||||
listed here.
|
||||
|
||||
## A. Visual layout
|
||||
|
||||
```
|
||||
┌─ New Worktree ─────────────────────────────────────────── X ─┐
|
||||
│ │
|
||||
│ [ New ][ Import ] [ 📁 kilocode ⌃⌄ ] │
|
||||
│ └────────────────────────────────────────────┘ │
|
||||
│ ┌──────────────────────────────────────────────────────┐ │
|
||||
│ │ Worktree name (optional) │ │
|
||||
│ └──────────────────────────────────────────────────────┘ │
|
||||
│ ┌──────────────────────────────────────────────────────┐ │
|
||||
│ │ prompt … │ │
|
||||
│ │ Code ▾ OpenAI / GPT-5.6 ▾ None ▾ ✨ 🔒 🎤 │ │
|
||||
│ └──────────────────────────────────────────────────────┘ │
|
||||
│ › Advanced options │
|
||||
│ VERSIONS [1][2][3][4] [⧉ Compare Models] │
|
||||
│ ┌──────────────────────────────────────────────────────┐ │
|
||||
│ │ Create Worktree │ │
|
||||
│ └──────────────────────────────────────────────────────┘ │
|
||||
└──────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
Open dropdown (anchored under the trigger, same width as the trigger):
|
||||
|
||||
```
|
||||
┌────────────────────────────────────────────┐
|
||||
│ 📁 kilocode ~/Documents/git/kilocode ✓│ ← .am-project-option-active
|
||||
│ 📁 cloud ~/Documents/git/cloud │
|
||||
│ 🔒 sample-app ~/dev/sample-app │ ← disabled, 50% opacity
|
||||
│ ⚠ old-repo ~/dev/old-repo │ ← disabled, 50% opacity
|
||||
└────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
Rules:
|
||||
|
||||
- The selector is inline with the New/Import buttons inside the tab-switcher flex row, so
|
||||
it applies to New and Import alike.
|
||||
- The project name and folder icon identify the scope without a separate visible label.
|
||||
- The trigger is the same control as the Advanced options base-branch trigger
|
||||
(`.am-selector-trigger`), so the dialog has one visual language for "pick a thing".
|
||||
- The row is **not rendered at all** when `props.projects` is empty or undefined. That is
|
||||
the single-project / flag-off case, which must stay pixel-identical to today.
|
||||
|
||||
## B. Exact icon names
|
||||
|
||||
Only these, from `packages/ui/src/components/icon.tsx`:
|
||||
|
||||
| Where | `Icon name` | Notes |
|
||||
|---|---|---|
|
||||
| Trigger left | `folder` | Always, regardless of project state. |
|
||||
| Trigger right | `selector` | Same as every other `.am-selector-trigger`. |
|
||||
| Option row, normal | `folder` | |
|
||||
| Option row, untrusted | `lock` | Matches the sidebar accordion affordance. |
|
||||
| Option row, missing | `warning` | Matches the sidebar accordion affordance. |
|
||||
| Option row, selected | `check-small` | Right-aligned. |
|
||||
|
||||
All at `size="small"`. Do not use `folder-add-left`, `check`, or `plus`.
|
||||
|
||||
## C. New file: `webview-ui/agent-manager/ProjectSelect.tsx`
|
||||
|
||||
```tsx
|
||||
// Project picker list for the New Worktree dialog
|
||||
|
||||
/** @jsxImportSource solid-js */
|
||||
|
||||
import { For, Show, type Component } from "solid-js"
|
||||
import { Icon } from "@kilocode/kilo-ui/icon"
|
||||
import type { AgentProjectSnapshot } from "../src/types/messages"
|
||||
|
||||
interface ProjectSelectProps {
|
||||
projects: AgentProjectSnapshot[]
|
||||
selected?: string
|
||||
onSelect: (id: string) => void
|
||||
labels: { untrusted: string; missing: string }
|
||||
}
|
||||
|
||||
export const ProjectSelect: Component<ProjectSelectProps> = (props) => (
|
||||
<div class="am-dropdown-list">
|
||||
<For each={props.projects}>
|
||||
{(project) => {
|
||||
const blocked = () => !project.trusted || project.missing
|
||||
const hint = () => {
|
||||
if (project.missing) return props.labels.missing
|
||||
if (!project.trusted) return props.labels.untrusted
|
||||
return project.root
|
||||
}
|
||||
const icon = () => {
|
||||
if (project.missing) return "warning" as const
|
||||
if (!project.trusted) return "lock" as const
|
||||
return "folder" as const
|
||||
}
|
||||
return (
|
||||
<button
|
||||
class="am-project-option"
|
||||
classList={{ "am-project-option-active": props.selected === project.id }}
|
||||
disabled={blocked()}
|
||||
title={hint()}
|
||||
onClick={() => props.onSelect(project.id)}
|
||||
type="button"
|
||||
>
|
||||
<span class="am-project-option-left">
|
||||
<Icon name={icon()} size="small" />
|
||||
<span class="am-project-option-name">{project.label}</span>
|
||||
<span class="am-project-option-root">{project.root}</span>
|
||||
</span>
|
||||
<Show when={props.selected === project.id}>
|
||||
<Icon name="check-small" size="small" />
|
||||
</Show>
|
||||
</button>
|
||||
)
|
||||
}}
|
||||
</For>
|
||||
</div>
|
||||
)
|
||||
```
|
||||
|
||||
Notes for the implementer:
|
||||
|
||||
- Wrap in `.am-dropdown-list`, not a bare fragment: that class supplies the scroll cap and
|
||||
4px padding, and `.am-dropdown [data-slot="popover-body"]` zeroes the popover padding.
|
||||
- No search input. A project list is short; adding one would need keyboard nav plumbing for
|
||||
no benefit.
|
||||
- `props.labels` is passed in rather than calling `useLanguage()` here, matching how
|
||||
`BranchSelect` and `SidebarSearchMenu` take label props.
|
||||
|
||||
## D. Exact JSX inserted into `NewWorktreeDialog.tsx`
|
||||
|
||||
### D.1 Imports
|
||||
|
||||
Add to the existing type import block at lines 6-12:
|
||||
|
||||
```ts
|
||||
AgentProjectSnapshot,
|
||||
```
|
||||
|
||||
Add after line 48 (`import { BranchSelect, BranchSelectPopover } …`):
|
||||
|
||||
```ts
|
||||
import { ProjectSelect } from "./ProjectSelect"
|
||||
```
|
||||
|
||||
`Icon`, `Show`, `DeferredPopover`, `createSignal`, `createEffect` are already imported.
|
||||
`on` from `solid-js` must be added to the line 5 import list.
|
||||
|
||||
### D.2 Props
|
||||
|
||||
Replace the component signature at lines 84-89 with:
|
||||
|
||||
```tsx
|
||||
export const NewWorktreeDialog: Component<{
|
||||
onClose: () => void
|
||||
/** Resolves the default base branch for one project. */
|
||||
defaultBase?: (projectId: string) => string | undefined
|
||||
/** Initial target project. The user can change it while the dialog is open. */
|
||||
projectId?: string
|
||||
/** Full project catalog. Empty or undefined hides the project row entirely. */
|
||||
projects?: () => AgentProjectSnapshot[]
|
||||
/** Project the sidebar currently has active; used as the default target. */
|
||||
activeProjectId?: string
|
||||
mode: ModeRouter
|
||||
}> = (props) => {
|
||||
```
|
||||
|
||||
### D.3 State
|
||||
|
||||
Immediately after line 101 (`const [tab, setTab] = createSignal<DialogTab>("new")`):
|
||||
|
||||
```tsx
|
||||
const [project, setProject] = createSignal(props.projectId ?? props.activeProjectId)
|
||||
const [projectOpen, setProjectOpen] = createSignal(false)
|
||||
const projects = () => props.projects?.() ?? []
|
||||
const showProject = () => projects().length > 0
|
||||
const projectLabel = () => projects().find((p) => p.id === project())?.label ?? ""
|
||||
```
|
||||
|
||||
`defaultBranch` (line 106) changes from `props.defaultBaseBranch ?? "main"` to:
|
||||
|
||||
```tsx
|
||||
const [defaultBranch, setDefaultBranch] = createSignal(
|
||||
(project() && props.defaultBase?.(project()!)) || "main",
|
||||
)
|
||||
```
|
||||
|
||||
### D.4 The inline selector
|
||||
|
||||
Insert inside the tab switcher after the Import button:
|
||||
|
||||
```tsx
|
||||
{/* Project scope — applies to both tabs. Hidden unless multi-project is on. */}
|
||||
<Show when={showProject()}>
|
||||
<div class="am-nv-project-inline">
|
||||
<div class="am-selector-wrapper">
|
||||
<DeferredPopover
|
||||
open={projectOpen()}
|
||||
onOpenChange={setProjectOpen}
|
||||
placement="bottom-start"
|
||||
flip={false}
|
||||
sameWidth
|
||||
portal={false}
|
||||
deferDismiss
|
||||
class="am-dropdown"
|
||||
trigger={
|
||||
<button class="am-selector-trigger" type="button" disabled={starting() || isPending()}>
|
||||
<span class="am-selector-left">
|
||||
<Icon name="folder" size="small" />
|
||||
<Show
|
||||
when={projectLabel()}
|
||||
fallback={
|
||||
<span class="am-selector-value am-selector-placeholder">
|
||||
{t("agentManager.dialog.project.select")}
|
||||
</span>
|
||||
}
|
||||
>
|
||||
<span class="am-selector-value">{projectLabel()}</span>
|
||||
</Show>
|
||||
</span>
|
||||
<span class="am-selector-right">
|
||||
<Icon name="selector" size="small" />
|
||||
</span>
|
||||
</button>
|
||||
}
|
||||
>
|
||||
<ProjectSelect
|
||||
projects={projects()}
|
||||
selected={project()}
|
||||
onSelect={(id) => {
|
||||
track("project_select", { changed: id !== props.activeProjectId })
|
||||
setProject(id)
|
||||
setProjectOpen(false)
|
||||
}}
|
||||
labels={{
|
||||
untrusted: t("agentManager.dialog.project.untrusted"),
|
||||
missing: t("agentManager.dialog.project.missing"),
|
||||
}}
|
||||
/>
|
||||
</DeferredPopover>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
```
|
||||
|
||||
Critical details, in order of how easily they get wrong:
|
||||
|
||||
1. `placement="bottom-start"`, **not** `top-start`. The rest of this dialog uses
|
||||
`top-start` because those triggers sit near the bottom of the panel. This one sits at
|
||||
the top, so it must open downward.
|
||||
2. `portal={false}` plus the escape CSS in section E.3. Do not switch to a portal unless
|
||||
the clipping fallback in E.3 is needed.
|
||||
3. `sameWidth` so the dropdown matches the trigger width, consistent with the base-branch
|
||||
and compare-models popovers.
|
||||
4. Never send the project label, root, or id as a telemetry property. `track` takes only
|
||||
the boolean shown above.
|
||||
|
||||
### D.5 Reactive reload on project change
|
||||
|
||||
Delete the one-shot request at lines 319-321 inside `onMount` and replace it with an effect
|
||||
placed next to the other `createEffect` calls:
|
||||
|
||||
```tsx
|
||||
// Project scope owns the branch data, the base branch, and the default badge.
|
||||
// Prompt, name, model, agent, versions and attachments are project-agnostic and survive.
|
||||
createEffect(
|
||||
on(project, (id) => {
|
||||
if (!id) return
|
||||
setBranches([])
|
||||
setBranchSearch("")
|
||||
setHighlightedIndex(0)
|
||||
setBaseBranch(null)
|
||||
setDefaultBranch(props.defaultBase?.(id) ?? "main")
|
||||
setBranchesLoading(true)
|
||||
vscode.postMessage({ type: "agentManager.requestBranches", projectId: id })
|
||||
}),
|
||||
)
|
||||
```
|
||||
|
||||
`on(project, …)` without `{ defer: true }` runs immediately, which replaces the removed
|
||||
`onMount` request. Keep the textarea focus logic in `onMount` untouched.
|
||||
|
||||
In the `agentManager.branches` handler (lines 520-525), replace the body with:
|
||||
|
||||
```tsx
|
||||
if (msg.type === "agentManager.branches") {
|
||||
const ev = msg as AgentManagerBranchesMessage
|
||||
if (ev.projectId && ev.projectId !== project()) return
|
||||
setBranches(ev.branches)
|
||||
const id = project()
|
||||
if (!id || !props.defaultBase?.(id)) setDefaultBranch(ev.defaultBranch)
|
||||
setBranchesLoading(false)
|
||||
}
|
||||
```
|
||||
|
||||
### D.6 Outbound project id
|
||||
|
||||
Four call sites change from `props.projectId` to `project()`:
|
||||
|
||||
| Line | Message |
|
||||
|---|---|
|
||||
| 321 (now inside the effect) | `agentManager.requestBranches` |
|
||||
| 373 | `agentManager.createMultiVersion` |
|
||||
| 566 | `agentManager.importFromPR` |
|
||||
| 575 | `agentManager.importFromBranch` |
|
||||
|
||||
Grep afterwards: `props.projectId` must appear exactly once in the file, in the `project`
|
||||
signal initializer.
|
||||
|
||||
## E. Exact CSS
|
||||
|
||||
All of it goes into `webview-ui/agent-manager/agent-manager.css`. No changes to kilo-ui.
|
||||
|
||||
### E.1 The inline selector
|
||||
|
||||
Insert directly after the `.am-tab-switcher-pill-active` rule (agent-manager.css:3614-3617),
|
||||
before the `/* Import tab layout */` comment at line 3619:
|
||||
|
||||
```css
|
||||
/* Project scope selector — inline with the New/Import tabs */
|
||||
|
||||
.am-nv-project-inline {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
flex: 0 1 260px;
|
||||
min-width: 0;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.am-nv-project-inline .am-selector-wrapper {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
```
|
||||
|
||||
The `flex: 0 1 260px` cap keeps the project control compact while allowing long project
|
||||
names to truncate. `margin-left: auto` keeps it aligned to the right of the New/Import
|
||||
buttons.
|
||||
|
||||
### E.2 The dropdown rows
|
||||
|
||||
Insert after the `.am-dropdown-empty` rule (agent-manager.css:3863-3868), before the
|
||||
`/* Import empty state */` comment at line 3870:
|
||||
|
||||
```css
|
||||
/* Project option rows in the New Worktree project dropdown.
|
||||
Deliberately distinct from .am-project-item, which styles the sidebar accordion. */
|
||||
|
||||
.am-project-option {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
padding: 6px 8px;
|
||||
border: none;
|
||||
border-radius: var(--radius-sm);
|
||||
background: none;
|
||||
color: var(--text-base);
|
||||
font-size: var(--font-size-base);
|
||||
font-family: inherit;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.am-project-option:hover:not(:disabled) {
|
||||
background: var(--surface-inset-base-hover);
|
||||
}
|
||||
|
||||
.am-project-option-active {
|
||||
background: var(--surface-inset-base);
|
||||
}
|
||||
|
||||
.am-project-option:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.am-project-option-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.am-project-option-left [data-component="icon"] {
|
||||
color: var(--text-weaker);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.am-project-option-name {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
flex-shrink: 0;
|
||||
max-width: 45%;
|
||||
}
|
||||
|
||||
.am-project-option-root {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
min-width: 0;
|
||||
font-size: var(--kilo-font-size-11);
|
||||
color: var(--text-weaker);
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
Why not reuse `.am-branch-item`: it is defined twice (lines 3211 and 3810) and the earlier
|
||||
definition sets `font-family: var(--font-mono, monospace)` on `.am-branch-item-name`, which
|
||||
would render project labels in monospace. Reusing it also couples project rows to future
|
||||
branch-row changes. `.am-project-item` is likewise off limits: it already styles the
|
||||
sidebar project accordion header (line 325).
|
||||
|
||||
### E.3 Popover clipping escape
|
||||
|
||||
`[data-slot="dialog-body"]` is `overflow: hidden` in `packages/ui/src/components/dialog.css:99-105`,
|
||||
and `[data-slot="dialog-content"]` is `overflow: auto` (line 38). The existing escape rules
|
||||
at agent-manager.css:2822-2828 only match popovers **inside** `.am-nv-dialog`, and this row
|
||||
is deliberately outside it. Without the following, the dropdown is clipped by the dialog.
|
||||
|
||||
Add to that same rule group (extend the existing selector list rather than duplicating the
|
||||
declaration):
|
||||
|
||||
```css
|
||||
[data-component="dialog"]:has(.am-nv-project-inline [data-component="popover-content"]) [data-slot="dialog-content"],
|
||||
[data-component="dialog"]:has(.am-nv-project-inline [data-component="popover-content"]) [data-slot="dialog-body"] {
|
||||
overflow: visible;
|
||||
}
|
||||
```
|
||||
|
||||
Verification step, not optional: open the dropdown with four or more projects registered
|
||||
and confirm no row is cut off and no inner scrollbar appears on the dialog. If it still
|
||||
clips, the documented fallback is to drop `portal={false}` from the `DeferredPopover` in
|
||||
D.4 and delete this rule; the dialog already sets `overflow: visible` on
|
||||
`[data-slot="dialog-content"]` for portal-based dropdowns (agent-manager.css:2755-2760).
|
||||
|
||||
## F. i18n
|
||||
|
||||
Add to `webview-ui/agent-manager/i18n/en.ts`, immediately after
|
||||
`"agentManager.dialog.namePlaceholder"`:
|
||||
|
||||
```ts
|
||||
"agentManager.dialog.project.select": "Select project",
|
||||
"agentManager.dialog.project.untrusted": "Trust this project in the sidebar first",
|
||||
"agentManager.dialog.project.missing": "Repository not found",
|
||||
```
|
||||
|
||||
Then add the same three keys to all 20 sibling locale files in that directory (`ar bs br da
|
||||
de es fa fr it ja ko nl no pl ru th tr uk zh zht`) via the `translator` subagent.
|
||||
|
||||
## G. What must not change
|
||||
|
||||
- No new CSS variables or tokens. Only the ones listed above, all already in use in this
|
||||
file.
|
||||
- No edits to `packages/kilo-ui/` or `packages/ui/`.
|
||||
- No change to `.am-project-item`, `.am-branch-item`, `.am-selector-trigger`,
|
||||
`.am-nv-config-label`, or any other existing rule. The only existing rule touched is the
|
||||
`overflow: visible` selector group in E.3, and only by adding selectors to it.
|
||||
- No new message types. `agentManager.addProject`, `agentManager.requestBranches`,
|
||||
`agentManager.createMultiVersion`, `agentManager.importFromBranch`, and
|
||||
`agentManager.importFromPR` all already exist and already accept what is needed.
|
||||
- With `props.projects` empty, the rendered dialog markup must be identical to before the
|
||||
change. Verify by toggling `kilo-code.new.experimental.multiProject` off.
|
||||
Reference in New Issue
Block a user