mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-09-01 15:32:11 +08:00
feat(agent-manager): target new worktrees by project
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"kilo-code": minor
|
||||
---
|
||||
|
||||
Let Agent Manager users choose the repository when creating or importing a worktree in multi-project mode.
|
||||
@@ -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.
|
||||
@@ -774,15 +774,15 @@ export class AgentManagerProvider implements Disposable {
|
||||
|
||||
private onImportMessage(m: AgentManagerInMessage): Record<string, unknown> | null | undefined {
|
||||
if (m.type === "agentManager.requestBranches") {
|
||||
void this.importer.branches()
|
||||
void this.importer.branches(m.projectId)
|
||||
return null
|
||||
}
|
||||
if (m.type === "agentManager.importFromBranch") {
|
||||
void this.importer.branch(m.branch)
|
||||
void this.importer.branch(m.branch, m.projectId)
|
||||
return null
|
||||
}
|
||||
if (m.type === "agentManager.importFromPR") {
|
||||
void this.importer.pr(m.url)
|
||||
void this.importer.pr(m.url, m.projectId)
|
||||
return null
|
||||
}
|
||||
}
|
||||
@@ -1018,6 +1018,7 @@ export class AgentManagerProvider implements Disposable {
|
||||
this.pushState()
|
||||
this.postToWebview({
|
||||
type: "agentManager.worktreeSetup",
|
||||
projectId: this.host.multiProject() ? this.context?.id : undefined,
|
||||
status: "ready",
|
||||
message: "Worktree ready",
|
||||
sessionId,
|
||||
|
||||
@@ -299,6 +299,7 @@ interface BranchesMessage {
|
||||
|
||||
interface ImportResultMessage {
|
||||
type: "agentManager.importResult"
|
||||
projectId?: string
|
||||
success: boolean
|
||||
message: string
|
||||
errorCode?: WorktreeSetupErrorCode
|
||||
|
||||
@@ -21,10 +21,10 @@ export class WorktreeImporter {
|
||||
|
||||
constructor(private readonly host: WorktreeImporterHost) {}
|
||||
|
||||
async branches(): Promise<void> {
|
||||
async branches(projectId?: string): Promise<void> {
|
||||
const manager = this.host.manager()
|
||||
if (!manager) {
|
||||
this.host.post({ type: "agentManager.branches", branches: [], defaultBranch: "main" })
|
||||
this.host.post({ type: "agentManager.branches", projectId, branches: [], defaultBranch: "main" })
|
||||
return
|
||||
}
|
||||
|
||||
@@ -46,31 +46,32 @@ export class WorktreeImporter {
|
||||
|
||||
this.host.post({
|
||||
type: "agentManager.branches",
|
||||
projectId,
|
||||
branches,
|
||||
defaultBranch: result.defaultBranch,
|
||||
})
|
||||
} catch (error) {
|
||||
this.host.log(`Failed to list branches: ${error}`)
|
||||
this.host.post({ type: "agentManager.branches", branches: [], defaultBranch: "main" })
|
||||
this.host.post({ type: "agentManager.branches", projectId, branches: [], defaultBranch: "main" })
|
||||
}
|
||||
}
|
||||
|
||||
async branch(branch: string): Promise<void> {
|
||||
await this.run({ branch })
|
||||
async branch(branch: string, projectId?: string): Promise<void> {
|
||||
await this.run({ branch }, projectId)
|
||||
}
|
||||
|
||||
async pr(url: string): Promise<void> {
|
||||
await this.run({ url })
|
||||
async pr(url: string, projectId?: string): Promise<void> {
|
||||
await this.run({ url }, projectId)
|
||||
}
|
||||
|
||||
private async run(target: { branch: string } | { url: string }): Promise<void> {
|
||||
private async run(target: { branch: string } | { url: string }, projectId?: string): Promise<void> {
|
||||
const manager = this.host.manager()
|
||||
const state = this.host.state()
|
||||
if (!manager || !state) {
|
||||
this.host.post({ type: "agentManager.importResult", success: false, message: "Not a git repository" })
|
||||
this.host.post({ type: "agentManager.importResult", projectId, success: false, message: "Not a git repository" })
|
||||
return
|
||||
}
|
||||
if (this.busy()) return
|
||||
if (this.busy(projectId)) return
|
||||
this.importing = true
|
||||
const branch = "branch" in target
|
||||
const creating = branch ? "Creating worktree from branch..." : "Resolving PR..."
|
||||
@@ -79,7 +80,7 @@ export class WorktreeImporter {
|
||||
? `Branch "${target.branch}" is already checked out in another worktree`
|
||||
: "This PR's branch is already checked out in another worktree"
|
||||
try {
|
||||
const progress = { type: "agentManager.worktreeSetup", status: "creating" } as const
|
||||
const progress = { type: "agentManager.worktreeSetup", projectId, status: "creating" } as const
|
||||
this.host.post({ ...progress, message: creating })
|
||||
const result = branch
|
||||
? await manager.createWorktree({ existingBranch: target.branch })
|
||||
@@ -102,7 +103,7 @@ export class WorktreeImporter {
|
||||
state.addSession(session.id, worktree.id)
|
||||
this.host.register(session.id, result.path)
|
||||
this.host.ready(session.id, result, worktree.id)
|
||||
this.host.post({ type: "agentManager.importResult", success: true, message: success })
|
||||
this.host.post({ type: "agentManager.importResult", projectId, success: true, message: success })
|
||||
this.host.log(`${log} as worktree ${worktree.id}`)
|
||||
} catch (error) {
|
||||
state.removeWorktree(worktree.id)
|
||||
@@ -111,27 +112,28 @@ export class WorktreeImporter {
|
||||
throw error
|
||||
}
|
||||
} catch (error) {
|
||||
this.importError(error, duplicate)
|
||||
this.importError(error, duplicate, projectId)
|
||||
} finally {
|
||||
this.importing = false
|
||||
}
|
||||
}
|
||||
|
||||
private busy(): boolean {
|
||||
private busy(projectId?: string): boolean {
|
||||
if (!this.importing) return false
|
||||
this.host.post({
|
||||
type: "agentManager.importResult",
|
||||
projectId,
|
||||
success: false,
|
||||
message: "Another import is already in progress",
|
||||
})
|
||||
return true
|
||||
}
|
||||
|
||||
private importError(error: unknown, duplicate: string): void {
|
||||
private importError(error: unknown, duplicate: string, projectId?: string): void {
|
||||
const raw = error instanceof Error ? error.message : String(error)
|
||||
const message = raw.includes("already used by worktree") || raw.includes("already checked out") ? duplicate : raw
|
||||
const code = classifyWorktreeError(message)
|
||||
this.host.post({ type: "agentManager.worktreeSetup", status: "error", message, errorCode: code })
|
||||
this.host.post({ type: "agentManager.importResult", success: false, message, errorCode: code })
|
||||
this.host.post({ type: "agentManager.worktreeSetup", projectId, status: "error", message, errorCode: code })
|
||||
this.host.post({ type: "agentManager.importResult", projectId, success: false, message, errorCode: code })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ const TSX_FILES = [
|
||||
path.join(ROOT, "webview-ui/agent-manager/AgentManagerApp.tsx"),
|
||||
path.join(ROOT, "webview-ui/agent-manager/UnassignedSessionsSection.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"),
|
||||
path.join(ROOT, "webview-ui/agent-manager/DiffPanel.tsx"),
|
||||
path.join(ROOT, "webview-ui/diff-viewer/FullScreenDiffView.tsx"),
|
||||
@@ -690,7 +691,7 @@ describe("Agent Manager Provider — onMessage routing", () => {
|
||||
|
||||
it("worktree import behavior lives in the cohesive importer", () => {
|
||||
const text = importer()
|
||||
for (const value of ["createFromPR", "createWorktree", "this.busy()"]) expect(text).toContain(value)
|
||||
for (const value of ["createFromPR", "createWorktree", "this.busy(projectId)"]) expect(text).toContain(value)
|
||||
expect(body("onImportMessage")).toContain("this.importer")
|
||||
})
|
||||
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
import { describe, expect, it } from "bun:test"
|
||||
import { readdirSync, readFileSync } from "node:fs"
|
||||
import { join } from "node:path"
|
||||
|
||||
const root = join(__dirname, "..", "..")
|
||||
const dialog = readFileSync(join(root, "webview-ui", "agent-manager", "NewWorktreeDialog.tsx"), "utf8")
|
||||
const importer = readFileSync(join(root, "src", "agent-manager", "worktree-importer.ts"), "utf8")
|
||||
const css = readFileSync(join(root, "webview-ui", "agent-manager", "agent-manager.css"), "utf8")
|
||||
|
||||
describe("Agent Manager New Worktree project targeting", () => {
|
||||
it("routes dialog operations through the selected project and rejects stale responses", () => {
|
||||
expect(dialog).toContain("const [project, setProject]")
|
||||
expect(dialog).toContain("if (ev.projectId !== project()) return")
|
||||
expect(dialog).toContain('type: "agentManager.requestBranches", projectId: id')
|
||||
expect(dialog).toContain('type: "agentManager.createMultiVersion"')
|
||||
expect(dialog).toContain("projectId: target")
|
||||
expect(dialog).toContain('type: "agentManager.importFromPR", projectId: project()')
|
||||
expect(dialog).toContain('type: "agentManager.importFromBranch", projectId: project()')
|
||||
})
|
||||
|
||||
it("tags branch and import responses with their owning project", () => {
|
||||
expect(importer).toContain("async branches(projectId?: string)")
|
||||
expect(importer).toContain('type: "agentManager.branches", projectId')
|
||||
expect(importer).toContain('type: "agentManager.importResult", projectId')
|
||||
expect(importer).toContain('type: "agentManager.worktreeSetup", projectId')
|
||||
})
|
||||
|
||||
it("keeps the project picker aligned with the dialog selector system", () => {
|
||||
expect(css).toContain(".am-nv-project-inline")
|
||||
expect(css).toContain(".am-project-option")
|
||||
expect(css).toContain('[data-component="dialog"]:has(.am-nv-project-inline [data-component="popover-content"])')
|
||||
})
|
||||
|
||||
it("defines project labels in every Agent Manager locale", () => {
|
||||
const keys = [
|
||||
"agentManager.dialog.project.select",
|
||||
"agentManager.dialog.project.untrusted",
|
||||
"agentManager.dialog.project.missing",
|
||||
]
|
||||
const locales = readdirSync(join(root, "webview-ui", "agent-manager", "i18n")).filter((file) =>
|
||||
file.endsWith(".ts"),
|
||||
)
|
||||
|
||||
for (const file of locales) {
|
||||
const source = readFileSync(join(root, "webview-ui", "agent-manager", "i18n", file), "utf8")
|
||||
for (const key of keys) expect(source, `${file} is missing ${key}`).toContain(`"${key}"`)
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -266,6 +266,11 @@ const AgentManagerContent: Component = () => {
|
||||
const [currentProjectId, setCurrentProjectId] = createSignal<string | undefined>()
|
||||
const [projectStates, setProjectStates] = createSignal<Record<string, AgentManagerStateMessage>>({})
|
||||
const activeProjectId = () => projectList().find((p) => p.active)?.id ?? currentProjectId()
|
||||
const [pendingCreate, setPendingCreate] = createSignal<{ projectId: string }>()
|
||||
const scheduleCreate = (projectId: string) => {
|
||||
if (projectId === activeProjectId()) return
|
||||
setPendingCreate({ projectId })
|
||||
}
|
||||
const isActivePayload = (pid: string | undefined) =>
|
||||
projectList().length === 0 || pid === undefined || pid === activeProjectId()
|
||||
|
||||
@@ -282,6 +287,14 @@ const AgentManagerContent: Component = () => {
|
||||
persisted: persisted ?? {},
|
||||
activeId: () => currentProjectId() ?? "single",
|
||||
})
|
||||
const defaultBase = (id: string) => {
|
||||
const store = registry.ensure(id)
|
||||
return (
|
||||
store.defaultBaseBranch() ??
|
||||
store.localStats()?.branch ??
|
||||
(id === activeProjectId() ? repoDetectedBranch() : undefined)
|
||||
)
|
||||
}
|
||||
const localSessionIDs = () => registry.active().tabs.ids()
|
||||
const setLocalSessionIDs = (next: string[] | ((prev: string[]) => string[])) => registry.active().tabs.set(next)
|
||||
/** Remove a session ID from the local tab (no-op if absent). */
|
||||
@@ -1371,6 +1384,14 @@ const AgentManagerContent: Component = () => {
|
||||
|
||||
if (msg.type === "agentManager.worktreeSetup") {
|
||||
const ev = msg as AgentManagerWorktreeSetupMessage
|
||||
const pending = pendingCreate()
|
||||
if (ev.status === "ready" && ev.projectId && pending?.projectId === ev.projectId && ev.worktreeId) {
|
||||
setPendingCreate(undefined)
|
||||
vscode.postMessage({
|
||||
type: "agentManager.activateSelection",
|
||||
target: { projectId: ev.projectId, kind: "worktree", worktreeId: ev.worktreeId },
|
||||
})
|
||||
}
|
||||
const store = ev.projectId ? registry.ensure(ev.projectId) : registry.active()
|
||||
const updateBusy: Setter<Map<string, WorktreeBusyState>> = (value) => store.setBusy(value)
|
||||
if (ev.status === "ready" || ev.status === "error") {
|
||||
@@ -1453,6 +1474,7 @@ const AgentManagerContent: Component = () => {
|
||||
// When a multi-version progress update arrives, mark newly created worktrees as loading
|
||||
if ((msg as { type: string }).type === "agentManager.multiVersionProgress") {
|
||||
const ev = msg as unknown as AgentManagerMultiVersionProgressMessage
|
||||
if (ev.status === "done" && pendingCreate()?.projectId === ev.projectId) setPendingCreate(undefined)
|
||||
if (ev.status === "done" && ev.groupId) {
|
||||
// Clear busy state for all worktrees in this group
|
||||
const store = ev.projectId ? registry.ensure(ev.projectId) : registry.active()
|
||||
@@ -1871,7 +1893,15 @@ const AgentManagerContent: Component = () => {
|
||||
if (!loaded()) return
|
||||
expandSidebar()
|
||||
dialog.show(() => (
|
||||
<NewWorktreeDialog mode={mode} onClose={() => dialog.close()} defaultBaseBranch={repoDefaultBranch()} />
|
||||
<NewWorktreeDialog
|
||||
mode={mode}
|
||||
onClose={() => dialog.close()}
|
||||
projectId={multiProject() ? activeProjectId() : undefined}
|
||||
projects={multiProject() ? projectList : undefined}
|
||||
activeProjectId={activeProjectId()}
|
||||
defaultBase={defaultBase}
|
||||
onCreate={scheduleCreate}
|
||||
/>
|
||||
))
|
||||
}
|
||||
|
||||
@@ -2348,6 +2378,8 @@ const AgentManagerContent: Component = () => {
|
||||
selection={selection() ?? undefined}
|
||||
currentSessionID={session.currentSessionID}
|
||||
mode={mode}
|
||||
defaultBase={defaultBase}
|
||||
onCreate={scheduleCreate}
|
||||
bindings={kb()}
|
||||
t={t}
|
||||
onSearchRef={(ref) => (sidebarSearchMenu = ref)}
|
||||
|
||||
@@ -2,10 +2,11 @@
|
||||
|
||||
/** @jsxImportSource solid-js */
|
||||
|
||||
import { type Component, For, Show, createSignal, createEffect, createMemo, onMount, onCleanup } from "solid-js"
|
||||
import { type Component, For, Show, createSignal, createEffect, createMemo, on, onMount, onCleanup } from "solid-js"
|
||||
import type {
|
||||
AgentManagerBranchesMessage,
|
||||
AgentManagerImportResultMessage,
|
||||
AgentProjectSnapshot,
|
||||
BranchInfo,
|
||||
EnhancePromptResultMessage,
|
||||
EnhancePromptErrorMessage,
|
||||
@@ -49,10 +50,11 @@ import { BranchSelect, BranchSelectPopover } from "../src/components/shared/Bran
|
||||
import { tracker } from "./telemetry"
|
||||
import { cycleAgent } from "../src/context/session-agent"
|
||||
import type { ModeRouter } from "./mode-router"
|
||||
import { ProjectSelect } from "./ProjectSelect"
|
||||
|
||||
type VersionCount = 1 | 2 | 3 | 4
|
||||
const VERSION_OPTIONS: VersionCount[] = [1, 2, 3, 4]
|
||||
const WORKTREE_PROMPT_COMMANDS = new Set(["models", "agents", "variant", "sandbox"])
|
||||
const WORKTREE_PROMPT_COMMANDS = new Set(["models", "agents", "variant", "sandbox", "project"])
|
||||
const WORKTREE_PROMPT_SCOPE = "agent-manager-worktree-prompt"
|
||||
|
||||
type DialogTab = "new" | "import"
|
||||
@@ -83,8 +85,11 @@ function sanitizeBranchName(name: string): string {
|
||||
|
||||
export const NewWorktreeDialog: Component<{
|
||||
onClose: () => void
|
||||
defaultBaseBranch?: string
|
||||
defaultBase?: (projectId: string) => string | undefined
|
||||
projectId?: string
|
||||
projects?: () => AgentProjectSnapshot[]
|
||||
activeProjectId?: string
|
||||
onCreate?: (projectId: string) => void
|
||||
mode: ModeRouter
|
||||
}> = (props) => {
|
||||
const { t } = useLanguage()
|
||||
@@ -99,11 +104,20 @@ export const NewWorktreeDialog: Component<{
|
||||
const click = metrics.click
|
||||
|
||||
const [tab, setTab] = createSignal<DialogTab>("new")
|
||||
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((item) => item.id === project())?.label ?? ""
|
||||
const base = () => {
|
||||
const id = project()
|
||||
return id ? props.defaultBase?.(id) : undefined
|
||||
}
|
||||
|
||||
// --- Shared branch data (used by both New tab's base branch selector and Import tab) ---
|
||||
const [branches, setBranches] = createSignal<BranchInfo[]>([])
|
||||
const [branchesLoading, setBranchesLoading] = createSignal(false)
|
||||
const [defaultBranch, setDefaultBranch] = createSignal(props.defaultBaseBranch ?? "main")
|
||||
const [defaultBranch, setDefaultBranch] = createSignal(base() ?? "main")
|
||||
const [branchSearch, setBranchSearch] = createSignal("")
|
||||
|
||||
// --- New tab state ---
|
||||
@@ -307,18 +321,25 @@ export const NewWorktreeDialog: Component<{
|
||||
if (session.agents().length < 2) hidden.add("agents")
|
||||
if (variants().length === 0) hidden.add("variant")
|
||||
if (!sandboxVisible()) hidden.add("sandbox")
|
||||
if (!showProject()) hidden.add("project")
|
||||
return hidden
|
||||
},
|
||||
WORKTREE_PROMPT_COMMANDS,
|
||||
WORKTREE_PROMPT_SCOPE,
|
||||
[
|
||||
{
|
||||
name: "project",
|
||||
description: t("agentManager.dialog.project.select"),
|
||||
hints: [],
|
||||
action: () => setProjectOpen(true),
|
||||
},
|
||||
],
|
||||
)
|
||||
const onFocusPrompt = () => restorePrompt()
|
||||
window.addEventListener("focusPrompt", onFocusPrompt)
|
||||
onCleanup(() => window.removeEventListener("focusPrompt", onFocusPrompt))
|
||||
|
||||
onMount(() => {
|
||||
setBranchesLoading(true)
|
||||
vscode.postMessage({ type: "agentManager.requestBranches", projectId: props.projectId })
|
||||
// Resize textarea if restoring a cached prompt
|
||||
if (prompt()) adjustHeight()
|
||||
const focus = () => {
|
||||
@@ -334,6 +355,20 @@ export const NewWorktreeDialog: Component<{
|
||||
})
|
||||
})
|
||||
|
||||
// Branch data and base-branch defaults belong to the selected project. Other
|
||||
// dialog state deliberately survives project changes.
|
||||
createEffect(
|
||||
on(project, (id) => {
|
||||
setBranches([])
|
||||
setBranchSearch("")
|
||||
setHighlightedIndex(0)
|
||||
setBaseBranch(null)
|
||||
setDefaultBranch(id ? (props.defaultBase?.(id) ?? "main") : "main")
|
||||
setBranchesLoading(true)
|
||||
vscode.postMessage({ type: "agentManager.requestBranches", projectId: id })
|
||||
}),
|
||||
)
|
||||
|
||||
const effectiveBaseBranch = () => baseBranch() ?? defaultBranch()
|
||||
|
||||
const filteredBranches = createMemo(() => {
|
||||
@@ -367,10 +402,12 @@ export const NewWorktreeDialog: Component<{
|
||||
const allocations = isCompare ? allocationsToArray(modelAllocations()) : undefined
|
||||
const count = total()
|
||||
const sel = isCompare ? null : model()
|
||||
const target = project()
|
||||
if (target) props.onCreate?.(target)
|
||||
|
||||
vscode.postMessage({
|
||||
type: "agentManager.createMultiVersion",
|
||||
projectId: props.projectId,
|
||||
projectId: target,
|
||||
text,
|
||||
name: name().trim() || undefined,
|
||||
versions: count,
|
||||
@@ -519,12 +556,14 @@ export const NewWorktreeDialog: Component<{
|
||||
const importUnsub = vscode.onMessage((msg) => {
|
||||
if (msg.type === "agentManager.branches") {
|
||||
const ev = msg as AgentManagerBranchesMessage
|
||||
if (ev.projectId !== project()) return
|
||||
setBranches(ev.branches)
|
||||
if (!props.defaultBaseBranch) setDefaultBranch(ev.defaultBranch)
|
||||
if (!base()) setDefaultBranch(ev.defaultBranch)
|
||||
setBranchesLoading(false)
|
||||
}
|
||||
if (msg.type === "agentManager.importResult") {
|
||||
const ev = msg as AgentManagerImportResultMessage
|
||||
if (ev.projectId !== project()) return
|
||||
setPrPending(false)
|
||||
setImportPending(false)
|
||||
if (ev.success) {
|
||||
@@ -563,7 +602,7 @@ export const NewWorktreeDialog: Component<{
|
||||
const url = prUrl().trim()
|
||||
if (!url || isPending()) return
|
||||
setPrPending(true)
|
||||
vscode.postMessage({ type: "agentManager.importFromPR", projectId: props.projectId, url })
|
||||
vscode.postMessage({ type: "agentManager.importFromPR", projectId: project(), url })
|
||||
}
|
||||
|
||||
const handleBranchSelect = (name: string) => {
|
||||
@@ -572,7 +611,7 @@ export const NewWorktreeDialog: Component<{
|
||||
setImportPending(true)
|
||||
setBranchOpen(false)
|
||||
setBranchSearch("")
|
||||
vscode.postMessage({ type: "agentManager.importFromBranch", projectId: props.projectId, branch: name })
|
||||
vscode.postMessage({ type: "agentManager.importFromBranch", projectId: project(), branch: name })
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -595,6 +634,62 @@ export const NewWorktreeDialog: Component<{
|
||||
>
|
||||
{t("agentManager.dialog.tab.import")}
|
||||
</button>
|
||||
{/* Project scope applies to both New and Import tabs. */}
|
||||
<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"
|
||||
aria-label={t("agentManager.dialog.project.select")}
|
||||
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>
|
||||
</div>
|
||||
|
||||
{/* New tab */}
|
||||
|
||||
@@ -34,6 +34,8 @@ interface Props {
|
||||
selection?: string
|
||||
currentSessionID?: () => string | undefined
|
||||
mode: ModeRouter
|
||||
defaultBase?: (projectId: string) => string | undefined
|
||||
onCreate?: (projectId: string) => void
|
||||
busy?: (projectId: string, id: string) => boolean
|
||||
working?: (projectId: string, id: string) => boolean
|
||||
localBusy?: (projectId: string) => boolean
|
||||
@@ -134,12 +136,14 @@ export const ProjectList: Component<Props> = (props) => {
|
||||
return select({ projectId: item.projectId, kind: "session", sessionId: item.sessionId })
|
||||
}
|
||||
const newWorktree = (projectId: string) => {
|
||||
const state = props.states[projectId]
|
||||
dialog.show(() => (
|
||||
<NewWorktreeDialog
|
||||
projectId={projectId}
|
||||
projects={() => props.projects}
|
||||
activeProjectId={props.selectedProject}
|
||||
defaultBase={props.defaultBase}
|
||||
onCreate={props.onCreate}
|
||||
mode={props.mode}
|
||||
defaultBaseBranch={state?.defaultBaseBranch ?? props.local[projectId]?.branch}
|
||||
onClose={() => dialog.close()}
|
||||
/>
|
||||
))
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
// 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>
|
||||
)
|
||||
@@ -2828,6 +2828,11 @@ body.am-wt-dragging-active * {
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
[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;
|
||||
}
|
||||
|
||||
.am-slash-command-dropdown {
|
||||
min-width: 0;
|
||||
max-width: none;
|
||||
@@ -3616,6 +3621,22 @@ body.am-wt-dragging-active * {
|
||||
color: var(--text-on-interactive-base) !important;
|
||||
}
|
||||
|
||||
/* 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;
|
||||
}
|
||||
|
||||
/* Import tab layout */
|
||||
|
||||
.am-import-tab {
|
||||
@@ -3867,6 +3888,68 @@ body.am-wt-dragging-active * {
|
||||
color: var(--text-weaker);
|
||||
}
|
||||
|
||||
/* Project option rows in the New Worktree project dropdown. */
|
||||
|
||||
.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);
|
||||
}
|
||||
|
||||
/* Import empty state */
|
||||
|
||||
.am-import-empty {
|
||||
|
||||
@@ -114,6 +114,9 @@ export const dict = {
|
||||
"agentManager.dialog.removeStaleWorktree.cancel": "إلغاء",
|
||||
"agentManager.dialog.removeStaleWorktree.confirm": "إزالة Worktree القديم",
|
||||
|
||||
"agentManager.dialog.project.select": "اختيار مشروع",
|
||||
"agentManager.dialog.project.untrusted": "يُرجى الوثوق بهذا المشروع من الشريط الجانبي أولًا",
|
||||
"agentManager.dialog.project.missing": "المستودع غير موجود",
|
||||
"agentManager.dialog.openWorktree": "شجرة عمل جديدة",
|
||||
"agentManager.dialog.configureWorktree": "تكوين Worktree جديد...",
|
||||
"agentManager.dialog.tab.new": "جديد",
|
||||
|
||||
@@ -116,6 +116,9 @@ export const dict = {
|
||||
"agentManager.dialog.removeStaleWorktree.cancel": "Cancelar",
|
||||
"agentManager.dialog.removeStaleWorktree.confirm": "Remover Worktree obsoleto",
|
||||
|
||||
"agentManager.dialog.project.select": "Selecionar projeto",
|
||||
"agentManager.dialog.project.untrusted": "Primeiro, confie neste projeto na barra lateral",
|
||||
"agentManager.dialog.project.missing": "Repositório não encontrado",
|
||||
"agentManager.dialog.openWorktree": "Novo Worktree",
|
||||
"agentManager.dialog.configureWorktree": "Configurar Novo Worktree...",
|
||||
"agentManager.dialog.tab.new": "Novo",
|
||||
|
||||
@@ -116,6 +116,9 @@ export const dict = {
|
||||
"agentManager.dialog.removeStaleWorktree.cancel": "Otkaži",
|
||||
"agentManager.dialog.removeStaleWorktree.confirm": "Ukloni zastarjeli Worktree",
|
||||
|
||||
"agentManager.dialog.project.select": "Odaberi projekat",
|
||||
"agentManager.dialog.project.untrusted": "Prvo vjeruj ovom projektu na bočnoj traci",
|
||||
"agentManager.dialog.project.missing": "Repozitorij nije pronađen",
|
||||
"agentManager.dialog.openWorktree": "Novi worktree",
|
||||
"agentManager.dialog.configureWorktree": "Konfiguriši Novi Worktree...",
|
||||
"agentManager.dialog.tab.new": "Novo",
|
||||
|
||||
@@ -117,6 +117,9 @@ export const dict = {
|
||||
"agentManager.dialog.removeStaleWorktree.cancel": "Annuller",
|
||||
"agentManager.dialog.removeStaleWorktree.confirm": "Fjern forældet Worktree",
|
||||
|
||||
"agentManager.dialog.project.select": "Vælg projekt",
|
||||
"agentManager.dialog.project.untrusted": "Godkend først dette projekt i sidepanelet",
|
||||
"agentManager.dialog.project.missing": "Repository ikke fundet",
|
||||
"agentManager.dialog.openWorktree": "Ny Worktree",
|
||||
"agentManager.dialog.configureWorktree": "Konfigurer Nyt Worktree...",
|
||||
"agentManager.dialog.tab.new": "Ny",
|
||||
|
||||
@@ -118,6 +118,9 @@ export const dict = {
|
||||
"agentManager.dialog.removeStaleWorktree.cancel": "Abbrechen",
|
||||
"agentManager.dialog.removeStaleWorktree.confirm": "Veralteten Worktree entfernen",
|
||||
|
||||
"agentManager.dialog.project.select": "Projekt auswählen",
|
||||
"agentManager.dialog.project.untrusted": "Vertrauen Sie diesem Projekt zuerst in der Seitenleiste",
|
||||
"agentManager.dialog.project.missing": "Repository nicht gefunden",
|
||||
"agentManager.dialog.openWorktree": "Neuer Worktree",
|
||||
"agentManager.dialog.configureWorktree": "Neuen Worktree konfigurieren...",
|
||||
"agentManager.dialog.tab.new": "Neu",
|
||||
|
||||
@@ -125,6 +125,9 @@ export const dict = {
|
||||
"agentManager.dialog.tab.new": "New",
|
||||
"agentManager.dialog.tab.import": "Import",
|
||||
"agentManager.dialog.namePlaceholder": "Worktree name (optional)",
|
||||
"agentManager.dialog.project.select": "Select project",
|
||||
"agentManager.dialog.project.untrusted": "Trust this project in the sidebar first",
|
||||
"agentManager.dialog.project.missing": "Repository not found",
|
||||
"agentManager.dialog.promptPlaceholder.mac": "Type a message (\u2318Enter to send)",
|
||||
"agentManager.dialog.promptPlaceholder.other": "Type a message (Ctrl+Enter to send)",
|
||||
"agentManager.dialog.advancedOptions": "Advanced options",
|
||||
|
||||
@@ -117,6 +117,9 @@ export const dict = {
|
||||
"agentManager.dialog.removeStaleWorktree.cancel": "Cancelar",
|
||||
"agentManager.dialog.removeStaleWorktree.confirm": "Eliminar Worktree obsoleto",
|
||||
|
||||
"agentManager.dialog.project.select": "Seleccionar proyecto",
|
||||
"agentManager.dialog.project.untrusted": "Confía primero en este proyecto desde la barra lateral",
|
||||
"agentManager.dialog.project.missing": "Repositorio no encontrado",
|
||||
"agentManager.dialog.openWorktree": "Nuevo Worktree",
|
||||
"agentManager.dialog.configureWorktree": "Configurar Nuevo Worktree...",
|
||||
"agentManager.dialog.tab.new": "Nuevo",
|
||||
|
||||
@@ -121,6 +121,9 @@ export const dict = {
|
||||
"agentManager.dialog.removeStaleWorktree.cancel": "لغو",
|
||||
"agentManager.dialog.removeStaleWorktree.confirm": "حذف Worktree قدیمی",
|
||||
|
||||
"agentManager.dialog.project.select": "انتخاب پروژه",
|
||||
"agentManager.dialog.project.untrusted": "ابتدا در نوار کناری به این پروژه اعتماد کنید",
|
||||
"agentManager.dialog.project.missing": "مخزن یافت نشد",
|
||||
"agentManager.dialog.openWorktree": "Worktree جدید",
|
||||
"agentManager.dialog.tab.new": "جدید",
|
||||
"agentManager.dialog.tab.import": "وارد کردن",
|
||||
|
||||
@@ -117,6 +117,9 @@ export const dict = {
|
||||
"agentManager.dialog.removeStaleWorktree.cancel": "Annuler",
|
||||
"agentManager.dialog.removeStaleWorktree.confirm": "Supprimer le Worktree obsolète",
|
||||
|
||||
"agentManager.dialog.project.select": "Sélectionner un projet",
|
||||
"agentManager.dialog.project.untrusted": "Approuvez d'abord ce projet dans la barre latérale",
|
||||
"agentManager.dialog.project.missing": "Dépôt introuvable",
|
||||
"agentManager.dialog.openWorktree": "Nouveau worktree",
|
||||
"agentManager.dialog.configureWorktree": "Configurer un Nouveau Worktree...",
|
||||
"agentManager.dialog.tab.new": "Nouveau",
|
||||
|
||||
@@ -123,6 +123,9 @@ export const dict = {
|
||||
"agentManager.dialog.removeStaleWorktree.cancel": "Annulla",
|
||||
"agentManager.dialog.removeStaleWorktree.confirm": "Rimuovi worktree obsoleto",
|
||||
|
||||
"agentManager.dialog.project.select": "Seleziona progetto",
|
||||
"agentManager.dialog.project.untrusted": "Prima, fidati di questo progetto nella barra laterale",
|
||||
"agentManager.dialog.project.missing": "Repository non trovata",
|
||||
"agentManager.dialog.openWorktree": "Nuovo worktree",
|
||||
"agentManager.dialog.tab.new": "Nuovo",
|
||||
"agentManager.dialog.tab.import": "Importa",
|
||||
|
||||
@@ -117,6 +117,9 @@ export const dict = {
|
||||
"agentManager.dialog.removeStaleWorktree.cancel": "キャンセル",
|
||||
"agentManager.dialog.removeStaleWorktree.confirm": "無効な Worktree を削除",
|
||||
|
||||
"agentManager.dialog.project.select": "プロジェクトを選択",
|
||||
"agentManager.dialog.project.untrusted": "まずサイドバーでこのプロジェクトを信頼してください",
|
||||
"agentManager.dialog.project.missing": "リポジトリが見つかりません",
|
||||
"agentManager.dialog.openWorktree": "新規ワークツリー",
|
||||
"agentManager.dialog.configureWorktree": "新規 Worktree の構成...",
|
||||
"agentManager.dialog.tab.new": "新規",
|
||||
|
||||
@@ -115,6 +115,9 @@ export const dict = {
|
||||
"agentManager.dialog.removeStaleWorktree.cancel": "취소",
|
||||
"agentManager.dialog.removeStaleWorktree.confirm": "오래된 Worktree 제거",
|
||||
|
||||
"agentManager.dialog.project.select": "프로젝트 선택",
|
||||
"agentManager.dialog.project.untrusted": "먼저 사이드바에서 이 프로젝트를 신뢰하세요",
|
||||
"agentManager.dialog.project.missing": "저장소를 찾을 수 없음",
|
||||
"agentManager.dialog.openWorktree": "새 워크트리",
|
||||
"agentManager.dialog.configureWorktree": "새 Worktree 구성...",
|
||||
"agentManager.dialog.tab.new": "새로 만들기",
|
||||
|
||||
@@ -122,6 +122,9 @@ export const dict = {
|
||||
"agentManager.dialog.removeStaleWorktree.cancel": "Annuleren",
|
||||
"agentManager.dialog.removeStaleWorktree.confirm": "Verouderde worktree verwijderen",
|
||||
|
||||
"agentManager.dialog.project.select": "Project selecteren",
|
||||
"agentManager.dialog.project.untrusted": "Vertrouw dit project eerst in de zijbalk",
|
||||
"agentManager.dialog.project.missing": "Repository niet gevonden",
|
||||
"agentManager.dialog.openWorktree": "Nieuwe worktree",
|
||||
"agentManager.dialog.configureWorktree": "Nieuwe Worktree Configureren...",
|
||||
"agentManager.dialog.tab.new": "Nieuw",
|
||||
|
||||
@@ -115,6 +115,9 @@ export const dict = {
|
||||
"agentManager.dialog.removeStaleWorktree.cancel": "Avbryt",
|
||||
"agentManager.dialog.removeStaleWorktree.confirm": "Fjern utdatert Worktree",
|
||||
|
||||
"agentManager.dialog.project.select": "Velg prosjekt",
|
||||
"agentManager.dialog.project.untrusted": "Stol på dette prosjektet i sidepanelet først",
|
||||
"agentManager.dialog.project.missing": "Repository ikke funnet",
|
||||
"agentManager.dialog.openWorktree": "Ny worktree",
|
||||
"agentManager.dialog.configureWorktree": "Konfigurer Nytt Worktree...",
|
||||
"agentManager.dialog.tab.new": "Ny",
|
||||
|
||||
@@ -117,6 +117,9 @@ export const dict = {
|
||||
"agentManager.dialog.removeStaleWorktree.cancel": "Anuluj",
|
||||
"agentManager.dialog.removeStaleWorktree.confirm": "Usuń nieaktualny Worktree",
|
||||
|
||||
"agentManager.dialog.project.select": "Wybierz projekt",
|
||||
"agentManager.dialog.project.untrusted": "Najpierw zaufaj temu projektowi na pasku bocznym",
|
||||
"agentManager.dialog.project.missing": "Nie znaleziono repozytorium",
|
||||
"agentManager.dialog.openWorktree": "Nowy Worktree",
|
||||
"agentManager.dialog.configureWorktree": "Skonfiguruj Nowe Worktree...",
|
||||
"agentManager.dialog.tab.new": "Nowy",
|
||||
|
||||
@@ -117,6 +117,9 @@ export const dict = {
|
||||
"agentManager.dialog.removeStaleWorktree.cancel": "Отмена",
|
||||
"agentManager.dialog.removeStaleWorktree.confirm": "Удалить устаревший Worktree",
|
||||
|
||||
"agentManager.dialog.project.select": "Выбрать проект",
|
||||
"agentManager.dialog.project.untrusted": "Сначала подтвердите доверие к этому проекту на боковой панели",
|
||||
"agentManager.dialog.project.missing": "Репозиторий не найден",
|
||||
"agentManager.dialog.openWorktree": "Новый worktree",
|
||||
"agentManager.dialog.configureWorktree": "Настроить новое Worktree...",
|
||||
"agentManager.dialog.tab.new": "Новый",
|
||||
|
||||
@@ -112,6 +112,9 @@ export const dict = {
|
||||
"agentManager.dialog.removeStaleWorktree.cancel": "ยกเลิก",
|
||||
"agentManager.dialog.removeStaleWorktree.confirm": "ลบ Worktree ที่ล้าสมัย",
|
||||
|
||||
"agentManager.dialog.project.select": "เลือกโปรเจกต์",
|
||||
"agentManager.dialog.project.untrusted": "โปรดเชื่อถือโปรเจกต์นี้ในแถบด้านข้างก่อน",
|
||||
"agentManager.dialog.project.missing": "ไม่พบ Repository",
|
||||
"agentManager.dialog.openWorktree": "Worktree ใหม่",
|
||||
"agentManager.dialog.configureWorktree": "กำหนดค่า Worktree ใหม่...",
|
||||
"agentManager.dialog.tab.new": "ใหม่",
|
||||
|
||||
@@ -123,6 +123,9 @@ export const dict = {
|
||||
"agentManager.dialog.removeStaleWorktree.cancel": "İptal",
|
||||
"agentManager.dialog.removeStaleWorktree.confirm": "Eskimiş worktree'yi kaldır",
|
||||
|
||||
"agentManager.dialog.project.select": "Proje seç",
|
||||
"agentManager.dialog.project.untrusted": "Önce kenar çubuğunda bu projeye güvenin",
|
||||
"agentManager.dialog.project.missing": "Depo bulunamadı",
|
||||
"agentManager.dialog.openWorktree": "Yeni Worktree",
|
||||
"agentManager.dialog.configureWorktree": "Yeni Worktree Yapılandır...",
|
||||
"agentManager.dialog.tab.new": "Yeni",
|
||||
|
||||
@@ -124,6 +124,9 @@ export const dict = {
|
||||
"agentManager.dialog.removeStaleWorktree.cancel": "Скасувати",
|
||||
"agentManager.dialog.removeStaleWorktree.confirm": "Видалити застаріле робоче дерево",
|
||||
|
||||
"agentManager.dialog.project.select": "Вибрати проєкт",
|
||||
"agentManager.dialog.project.untrusted": "Спочатку підтвердьте, що довіряєте цьому проєкту, на бічній панелі",
|
||||
"agentManager.dialog.project.missing": "Репозиторій не знайдено",
|
||||
"agentManager.dialog.openWorktree": "Нове робоче дерево",
|
||||
"agentManager.dialog.configureWorktree": "Налаштувати нове Worktree...",
|
||||
"agentManager.dialog.tab.new": "Нове",
|
||||
|
||||
@@ -111,6 +111,9 @@ export const dict = {
|
||||
"agentManager.dialog.removeStaleWorktree.cancel": "取消",
|
||||
"agentManager.dialog.removeStaleWorktree.confirm": "移除失效 Worktree",
|
||||
|
||||
"agentManager.dialog.project.select": "选择项目",
|
||||
"agentManager.dialog.project.untrusted": "请先在侧边栏中信任此项目",
|
||||
"agentManager.dialog.project.missing": "未找到仓库",
|
||||
"agentManager.dialog.openWorktree": "新建工作树",
|
||||
"agentManager.dialog.configureWorktree": "配置新 Worktree...",
|
||||
"agentManager.dialog.tab.new": "新建",
|
||||
|
||||
@@ -111,6 +111,9 @@ export const dict = {
|
||||
"agentManager.dialog.removeStaleWorktree.cancel": "取消",
|
||||
"agentManager.dialog.removeStaleWorktree.confirm": "移除失效 Worktree",
|
||||
|
||||
"agentManager.dialog.project.select": "選擇專案",
|
||||
"agentManager.dialog.project.untrusted": "請先在側邊欄信任此專案",
|
||||
"agentManager.dialog.project.missing": "找不到儲存庫",
|
||||
"agentManager.dialog.openWorktree": "新建工作樹",
|
||||
"agentManager.dialog.configureWorktree": "配置新 Worktree...",
|
||||
"agentManager.dialog.tab.new": "新建",
|
||||
|
||||
@@ -58,6 +58,7 @@ export function useSlashCommand(
|
||||
exclude?: Set<string> | Accessor<Set<string>>,
|
||||
include?: Set<string> | Accessor<Set<string>>,
|
||||
scope?: string,
|
||||
extra?: SlashCommandEntry[],
|
||||
): SlashCommand {
|
||||
const [server, setServer] = createSignal<SlashCommandInfo[]>([])
|
||||
const [query, setQuery] = createSignal<string | null>(null)
|
||||
@@ -191,6 +192,7 @@ export function useSlashCommand(
|
||||
},
|
||||
},
|
||||
]
|
||||
all.push(...(extra ?? []))
|
||||
|
||||
const excluded = () => {
|
||||
if (typeof exclude === "function") return exclude()
|
||||
|
||||
@@ -947,6 +947,7 @@ export interface AgentManagerBranchesMessage {
|
||||
// Agent Manager Import tab: result feedback (extension → webview)
|
||||
export interface AgentManagerImportResultMessage {
|
||||
type: "agentManager.importResult"
|
||||
projectId?: string
|
||||
success: boolean
|
||||
message: string
|
||||
errorCode?: WorktreeErrorCode
|
||||
|
||||
Reference in New Issue
Block a user