diff --git a/.changeset/bright-project-picker.md b/.changeset/bright-project-picker.md new file mode 100644 index 00000000000..18458aaa856 --- /dev/null +++ b/.changeset/bright-project-picker.md @@ -0,0 +1,5 @@ +--- +"kilo-code": minor +--- + +Let Agent Manager users choose the repository when creating or importing a worktree in multi-project mode. diff --git a/.kilo/plans/agent-manager-new-worktree-project-selector.md b/.kilo/plans/agent-manager-new-worktree-project-selector.md new file mode 100644 index 00000000000..5f98fd086f7 --- /dev/null +++ b/.kilo/plans/agent-manager-new-worktree-project-selector.md @@ -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(() => ( + 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 + 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 = (props) => ( +
+ + {(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 ( + + ) + }} + +
+) +``` + +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("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. */} + +
+
+ + + + + {t("agentManager.dialog.project.select")} + + } + > + {projectLabel()} + + + + + + + } + > + { + track("project_select", { changed: id !== props.activeProjectId }) + setProject(id) + setProjectOpen(false) + }} + labels={{ + untrusted: t("agentManager.dialog.project.untrusted"), + missing: t("agentManager.dialog.project.missing"), + }} + /> + +
+
+
+``` + +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.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. diff --git a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager/new-worktree-project-dropdown-chromium-linux.png b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager/new-worktree-project-dropdown-chromium-linux.png new file mode 100644 index 00000000000..3f0dd7a5811 --- /dev/null +++ b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager/new-worktree-project-dropdown-chromium-linux.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:88a4490322f4f70694f7b785cff41c5900b4ef8c14da104396fe55d264a624b4 +size 14535 diff --git a/packages/kilo-vscode/src/agent-manager/AgentManagerProvider.ts b/packages/kilo-vscode/src/agent-manager/AgentManagerProvider.ts index 50c1f9cfb60..490fb674ff1 100644 --- a/packages/kilo-vscode/src/agent-manager/AgentManagerProvider.ts +++ b/packages/kilo-vscode/src/agent-manager/AgentManagerProvider.ts @@ -811,15 +811,15 @@ export class AgentManagerProvider implements Disposable { private onImportMessage(m: AgentManagerInMessage): Record | 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 } } @@ -1055,6 +1055,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, diff --git a/packages/kilo-vscode/src/agent-manager/types.ts b/packages/kilo-vscode/src/agent-manager/types.ts index 76389d4f288..7064f16d224 100644 --- a/packages/kilo-vscode/src/agent-manager/types.ts +++ b/packages/kilo-vscode/src/agent-manager/types.ts @@ -299,6 +299,7 @@ interface BranchesMessage { interface ImportResultMessage { type: "agentManager.importResult" + projectId?: string success: boolean message: string errorCode?: WorktreeSetupErrorCode diff --git a/packages/kilo-vscode/src/agent-manager/worktree-importer.ts b/packages/kilo-vscode/src/agent-manager/worktree-importer.ts index f589a7c4ebe..c44d7d9cf95 100644 --- a/packages/kilo-vscode/src/agent-manager/worktree-importer.ts +++ b/packages/kilo-vscode/src/agent-manager/worktree-importer.ts @@ -21,10 +21,10 @@ export class WorktreeImporter { constructor(private readonly host: WorktreeImporterHost) {} - async branches(): Promise { + async branches(projectId?: string): Promise { 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 { - await this.run({ branch }) + async branch(branch: string, projectId?: string): Promise { + await this.run({ branch }, projectId) } - async pr(url: string): Promise { - await this.run({ url }) + async pr(url: string, projectId?: string): Promise { + await this.run({ url }, projectId) } - private async run(target: { branch: string } | { url: string }): Promise { + private async run(target: { branch: string } | { url: string }, projectId?: string): Promise { 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 }) } } diff --git a/packages/kilo-vscode/tests/unit/agent-manager-arch.test.ts b/packages/kilo-vscode/tests/unit/agent-manager-arch.test.ts index 08820212b49..3d41e4e7dcd 100644 --- a/packages/kilo-vscode/tests/unit/agent-manager-arch.test.ts +++ b/packages/kilo-vscode/tests/unit/agent-manager-arch.test.ts @@ -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") }) diff --git a/packages/kilo-vscode/tests/unit/agent-manager-new-worktree-project.test.ts b/packages/kilo-vscode/tests/unit/agent-manager-new-worktree-project.test.ts new file mode 100644 index 00000000000..35e439bc876 --- /dev/null +++ b/packages/kilo-vscode/tests/unit/agent-manager-new-worktree-project.test.ts @@ -0,0 +1,56 @@ +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 app = readFileSync(join(root, "webview-ui", "agent-manager", "AgentManagerApp.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"') + expect(dialog).toContain('type: "agentManager.importFromBranch"') + }) + + it("does not replace a pending cross-project activation", () => { + expect(app).toContain("if (pendingCreate()) return") + expect(app).toContain('msg.type === "agentManager.importResult"') + expect(app).toContain("!msg.success && pendingCreate()?.projectId === msg.projectId") + }) + + 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}"`) + } + }) +}) diff --git a/packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx b/packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx index a68c1099f47..bed9b30d9d6 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx +++ b/packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx @@ -27,6 +27,7 @@ import type { AgentManagerWorktreeDiffLoadingMessage, AgentManagerWorktreeDiffNoticeMessage, AgentManagerDiffBranchesMessage, + AgentManagerImportResultMessage, AgentManagerApplyWorktreeDiffResultMessage, AgentManagerWorktreeStatsMessage, AgentManagerLocalStatsMessage, @@ -266,6 +267,12 @@ const AgentManagerContent: Component = () => { const [currentProjectId, setCurrentProjectId] = createSignal() const [projectStates, setProjectStates] = createSignal>({}) const activeProjectId = () => projectList().find((p) => p.active)?.id ?? currentProjectId() + const [pendingCreate, setPendingCreate] = createSignal<{ projectId: string }>() + const scheduleCreate = (projectId: string) => { + if (projectId === activeProjectId()) return + if (pendingCreate()) return + setPendingCreate({ projectId }) + } const isActivePayload = (pid: string | undefined) => projectList().length === 0 || pid === undefined || pid === activeProjectId() @@ -282,6 +289,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 +1386,15 @@ 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 }, + }) + } + if (ev.status === "error" && pending?.projectId === ev.projectId) setPendingCreate(undefined) const store = ev.projectId ? registry.ensure(ev.projectId) : registry.active() const updateBusy: Setter> = (value) => store.setBusy(value) if (ev.status === "ready" || ev.status === "error") { @@ -1412,6 +1436,9 @@ const AgentManagerContent: Component = () => { } } + if (msg.type === "agentManager.importResult" && !msg.success && pendingCreate()?.projectId === msg.projectId) + setPendingCreate(undefined) + if (msg.type === "agentManager.sessionAdded") { const ev = msg as { type: string; sessionId: string; worktreeId: string } saveTabMemory() @@ -1453,6 +1480,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 +1899,15 @@ const AgentManagerContent: Component = () => { if (!loaded()) return expandSidebar() dialog.show(() => ( - dialog.close()} defaultBaseBranch={repoDefaultBranch()} /> + dialog.close()} + projectId={multiProject() ? activeProjectId() : undefined} + projects={multiProject() ? projectList : undefined} + activeProjectId={activeProjectId()} + defaultBase={defaultBase} + onCreate={scheduleCreate} + /> )) } @@ -2348,6 +2384,8 @@ const AgentManagerContent: Component = () => { selection={selection() ?? undefined} currentSessionID={session.currentSessionID} mode={mode} + defaultBase={defaultBase} + onCreate={scheduleCreate} bindings={kb()} t={t} onSearchRef={(ref) => (sidebarSearchMenu = ref)} diff --git a/packages/kilo-vscode/webview-ui/agent-manager/NewWorktreeDialog.tsx b/packages/kilo-vscode/webview-ui/agent-manager/NewWorktreeDialog.tsx index bb216b39eec..a2d88dfbd5e 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/NewWorktreeDialog.tsx +++ b/packages/kilo-vscode/webview-ui/agent-manager/NewWorktreeDialog.tsx @@ -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" @@ -124,8 +126,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() @@ -140,11 +145,20 @@ export const NewWorktreeDialog: Component<{ const click = metrics.click const [tab, setTab] = createSignal("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([]) 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 --- @@ -367,18 +381,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 = () => { @@ -394,6 +415,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(() => { @@ -427,10 +462,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, @@ -579,12 +616,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) { @@ -623,7 +662,9 @@ export const NewWorktreeDialog: Component<{ const url = prUrl().trim() if (!url || isPending()) return setPrPending(true) - vscode.postMessage({ type: "agentManager.importFromPR", projectId: props.projectId, url }) + const target = project() + if (target) props.onCreate?.(target) + vscode.postMessage({ type: "agentManager.importFromPR", projectId: target, url }) } const handleBranchSelect = (name: string) => { @@ -632,7 +673,9 @@ export const NewWorktreeDialog: Component<{ setImportPending(true) setBranchOpen(false) setBranchSearch("") - vscode.postMessage({ type: "agentManager.importFromBranch", projectId: props.projectId, branch: name }) + const target = project() + if (target) props.onCreate?.(target) + vscode.postMessage({ type: "agentManager.importFromBranch", projectId: target, branch: name }) } return ( @@ -655,6 +698,62 @@ export const NewWorktreeDialog: Component<{ > {t("agentManager.dialog.tab.import")} + {/* Project scope applies to both New and Import tabs. */} + +
+
+ + + + + {t("agentManager.dialog.project.select")} + + } + > + {projectLabel()} + + + + + + + } + > + { + track("project_select", { changed: id !== props.activeProjectId }) + setProject(id) + setProjectOpen(false) + }} + labels={{ + untrusted: t("agentManager.dialog.project.untrusted"), + missing: t("agentManager.dialog.project.missing"), + }} + /> + +
+
+
{/* New tab */} diff --git a/packages/kilo-vscode/webview-ui/agent-manager/ProjectList.tsx b/packages/kilo-vscode/webview-ui/agent-manager/ProjectList.tsx index 382f60bc2a7..1f714f6d8ed 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/ProjectList.tsx +++ b/packages/kilo-vscode/webview-ui/agent-manager/ProjectList.tsx @@ -39,6 +39,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 @@ -141,12 +143,14 @@ export const ProjectList: Component = (props) => { return select({ projectId: item.projectId, kind: "session", sessionId: item.sessionId }) } const newWorktree = (projectId: string) => { - const state = props.states[projectId] dialog.show(() => ( props.projects} + activeProjectId={props.selectedProject} + defaultBase={props.defaultBase} + onCreate={props.onCreate} mode={props.mode} - defaultBaseBranch={state?.defaultBaseBranch ?? props.local[projectId]?.branch} onClose={() => dialog.close()} /> )) diff --git a/packages/kilo-vscode/webview-ui/agent-manager/ProjectSelect.tsx b/packages/kilo-vscode/webview-ui/agent-manager/ProjectSelect.tsx new file mode 100644 index 00000000000..ea37b09a140 --- /dev/null +++ b/packages/kilo-vscode/webview-ui/agent-manager/ProjectSelect.tsx @@ -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 = (props) => ( +
+ + {(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 ( + + ) + }} + +
+) diff --git a/packages/kilo-vscode/webview-ui/agent-manager/agent-manager.css b/packages/kilo-vscode/webview-ui/agent-manager/agent-manager.css index 5527f461e33..50dd02d2a85 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/agent-manager.css +++ b/packages/kilo-vscode/webview-ui/agent-manager/agent-manager.css @@ -2867,6 +2867,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; @@ -3655,6 +3660,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 { @@ -3906,6 +3927,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 { diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/ar.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/ar.ts index 586079b908d..04124ebe748 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/ar.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/ar.ts @@ -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": "جديد", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/br.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/br.ts index 64ad7cc61a9..a50bc100b83 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/br.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/br.ts @@ -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", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/bs.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/bs.ts index a42401b977e..3071b152ece 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/bs.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/bs.ts @@ -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", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/da.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/da.ts index 705758f1b94..ef6342f1ac3 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/da.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/da.ts @@ -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", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/de.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/de.ts index 234e551f1aa..844eccb0e73 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/de.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/de.ts @@ -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", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/en.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/en.ts index 56954f2c7ac..5e0e27eed38 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/en.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/en.ts @@ -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", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/es.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/es.ts index f1ec88f48c3..4b438f22249 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/es.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/es.ts @@ -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", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/fa.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/fa.ts index e128e95a674..d318ab4256c 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/fa.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/fa.ts @@ -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": "وارد کردن", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/fr.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/fr.ts index 3a2f7871314..10d707785b0 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/fr.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/fr.ts @@ -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", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/it.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/it.ts index 8f0cfabc3d1..4105961ff05 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/it.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/it.ts @@ -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", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/ja.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/ja.ts index c689b6ac005..e3396a9144e 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/ja.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/ja.ts @@ -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": "新規", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/ko.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/ko.ts index 28855e0bb7b..db772c76315 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/ko.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/ko.ts @@ -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": "새로 만들기", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/nl.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/nl.ts index 81a003f9f03..125733840f8 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/nl.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/nl.ts @@ -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", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/no.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/no.ts index 4a0946996f5..194199df5e1 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/no.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/no.ts @@ -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", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/pl.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/pl.ts index 47ba2cbffbe..a20890b35d0 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/pl.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/pl.ts @@ -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", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/ru.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/ru.ts index 52cf8082e0c..84ef5b9ed46 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/ru.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/ru.ts @@ -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": "Новый", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/th.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/th.ts index 320a1bebbc3..5a7bb8eef8f 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/th.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/th.ts @@ -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": "ใหม่", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/tr.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/tr.ts index d02a01e95e7..820bbee740b 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/tr.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/tr.ts @@ -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", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/uk.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/uk.ts index 54587c0c96a..b03f6b4fd8b 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/uk.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/uk.ts @@ -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": "Нове", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/zh.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/zh.ts index 4732e36dd68..39b3d811568 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/zh.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/zh.ts @@ -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": "新建", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/zht.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/zht.ts index 0357ac87df4..33d267caa5d 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/zht.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/zht.ts @@ -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": "新建", diff --git a/packages/kilo-vscode/webview-ui/src/hooks/useSlashCommand.ts b/packages/kilo-vscode/webview-ui/src/hooks/useSlashCommand.ts index 6dbfc9b979c..6b99617a3b1 100644 --- a/packages/kilo-vscode/webview-ui/src/hooks/useSlashCommand.ts +++ b/packages/kilo-vscode/webview-ui/src/hooks/useSlashCommand.ts @@ -58,6 +58,7 @@ export function useSlashCommand( exclude?: Set | Accessor>, include?: Set | Accessor>, scope?: string, + extra?: SlashCommandEntry[], ): SlashCommand { const [server, setServer] = createSignal([]) const [query, setQuery] = createSignal(null) @@ -191,6 +192,7 @@ export function useSlashCommand( }, }, ] + all.push(...(extra ?? [])) const excluded = () => { if (typeof exclude === "function") return exclude() diff --git a/packages/kilo-vscode/webview-ui/src/stories/agent-manager.stories.tsx b/packages/kilo-vscode/webview-ui/src/stories/agent-manager.stories.tsx index 9ebef0f8766..cc0a8696769 100644 --- a/packages/kilo-vscode/webview-ui/src/stories/agent-manager.stories.tsx +++ b/packages/kilo-vscode/webview-ui/src/stories/agent-manager.stories.tsx @@ -26,8 +26,16 @@ import { Icon } from "@kilocode/kilo-ui/icon" import { TooltipKeybind } from "@kilocode/kilo-ui/tooltip" import { ContextMenu } from "@kilocode/kilo-ui/context-menu" import { ThinkingSelectorBase } from "../components/shared/ThinkingSelector" +import { DeferredPopover } from "../components/shared/DeferredPopover" +import { ProjectSelect } from "../../agent-manager/ProjectSelect" import { createSignal, onCleanup, onMount, type JSX } from "solid-js" -import type { WorktreeFileDiff, WorktreeState, WorktreeGitStats, PRStatus } from "../types/messages" +import type { + AgentProjectSnapshot, + WorktreeFileDiff, + WorktreeState, + WorktreeGitStats, + PRStatus, +} from "../types/messages" import type { ReviewComment } from "../../diff-viewer/review-comments" import { createModeRouter } from "../../agent-manager/mode-router" import "../../agent-manager/agent-manager.css" @@ -1075,6 +1083,115 @@ export const NewWorktreeVariantDropdown1280: Story = { ), } +const projectPickerProjects: AgentProjectSnapshot[] = [ + { + id: "project-main", + root: "/workspace/kilocode", + label: "kilocode", + pinned: true, + active: true, + expanded: true, + initialized: true, + trusted: true, + missing: false, + }, + { + id: "project-cloud", + root: "/workspace/cloud", + label: "cloud", + pinned: false, + active: false, + expanded: false, + initialized: true, + trusted: true, + missing: false, + }, + { + id: "project-untrusted", + root: "/workspace/sample-app", + label: "sample-app", + pinned: false, + active: false, + expanded: false, + initialized: false, + trusted: false, + missing: false, + }, +] + +export const NewWorktreeProjectDropdown: Story = { + name: "NewWorktreeDialog — project dropdown open", + parameters: { layout: "fullscreen" }, + render: () => ( + +
+
+
+
+
+
New Worktree
+
+
+
+ + +
+
+ undefined} + placement="bottom-start" + flip={false} + sameWidth + portal={false} + deferDismiss + class="am-dropdown" + trigger={ + + } + > + undefined} + labels={{ + untrusted: "Trust this project in the sidebar first", + missing: "Repository not found", + }} + /> + +
+
+
+
+
+
+
+ VERSIONS +
+
+
+
+
+
+
+
+ + ), +} + const searchSection = { id: "polish", name: "Polish", color: "Blue", order: 0, collapsed: false } const slackedSection = { id: "slacked", name: "SLACKED", color: "Yellow", order: 1, collapsed: false } const sidebarSearchItems: SidebarSearchItem[] = [ @@ -1198,12 +1315,7 @@ export const SidebarSearchOpen: Story = { // --------------------------------------------------------------------------- import { ProjectList } from "../../agent-manager/ProjectList" -import type { - AgentManagerStateMessage, - AgentProjectSnapshot, - LocalGitStats, - ProjectSessionInfo, -} from "../types/messages" +import type { AgentManagerStateMessage, LocalGitStats, ProjectSessionInfo } from "../types/messages" const projectA: AgentProjectSnapshot = { id: "prj-aaaa1111aaaa", diff --git a/packages/kilo-vscode/webview-ui/src/types/messages/extension-messages.ts b/packages/kilo-vscode/webview-ui/src/types/messages/extension-messages.ts index 859482cae8a..8e300f4d714 100644 --- a/packages/kilo-vscode/webview-ui/src/types/messages/extension-messages.ts +++ b/packages/kilo-vscode/webview-ui/src/types/messages/extension-messages.ts @@ -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